TL;DR
以下内容不含对于带有自增列的表的 insert 操作。
在解决production issue的时候,遇到了一个transaction使用方式不正确,造成了nhibernate的session被污染,导致了大批量正确的操作被rollback。
正确的使用方式是,当transaction rollback了,与这个transaction关联的session立刻被close,而不是继续开新的transaction或者执行其他的数据库操作。

begin session
load all client-people
each client-peopel by loaded
            begin transaction
                update client-peple   --步骤一
                upldate client-peopel-account --步骤二
                transaction.commit --步骤三
            end transation
            --这里关闭session 
            onerror: rollback ,session.close,jump out loop
end session

否则的话
在一个session里重复开多个transaction,形如

    begin session    
        load all client-people
        each client-peopel by loaded
            begin transaction
                update client-peple   --步骤一
                upldate client-peopel-account --步骤二
                transaction.commit --步骤三
            end transation
            --这里不关闭session,继续下一个循环 
            onerror: rollback ,catch error and go to next client-people

    end session 

导致如果在某一个client-people的更新在步骤三失败,步骤一、二被rollback了,后续client-people的更新都会因为这个更新失败。
这是因为transaction的rollback只回滚了数据库状态,这个session缓存的数据依然保持了被更改后的状态,而session.flush时会将缓存里每一个更改过的数据都flush到数据库,这就使得错误的entity在不同的transaction被反复flush,导致其他transaction的失败。
引自Hibernate官方文档。

一个由Hibernate抛出的异常意味着你必须立即回滚数据库事务,并立即关闭Session (稍后会展开讨论)。如果你的Session绑定到一个应用程序上,你必 须停止该应用程序。回滚数据库事务并不会把你的业务对象退回到事务启动时候的状态。这 意味着数据库状态和业务对象状态不同步。通常情况下,这不是什么问题,因为异常是不可 恢复的,你必须在回滚之后重新开始执行。

以下,是整个事件的探索过程,基于hibernate文档的推理,以及比较全面的最佳实践建议。

引子

nhibernate这个玩意就如其拼写一样复杂。就像上个世纪的其他产物一样,我们需要依靠大量的文档和严格遵循其隐晦的约定,才能正确使用nhibernate强大的功能;一旦,我们在实践过程中做出了某种非常规的匹配,nhibernate就将在实际运行中的某个时刻抛出奇奇怪怪的问题,造成程序的崩溃。
最近,我在解决一个production issue的时候,就遇到了一个transaction使用方式不正确,造成了session被污染,导致了大批量正确的操作被rollback。
在github book上xj找到了份关于hibernate文档的翻译,本文将主要引用这里的文章。

问题背景

最近,产品环境的日志反复出现了一个后台服务的错误,数量多达几十万次,占了error的三分之一强...........。
这个服务其实很简单,大概的业务流程用伪代码描述如下:

received request
    begin session    
        load all client-people
        each client-peopel by loaded
            begin transaction
                update client-peple
                upldate client-peopel-account
                transaction.commit
            end transation
            onerror: rollback ,catch error and go to next client-people 
    end session
end response

经过一系列的分析,发现了3处错误

  • update client-people时候遇到业务错误
  • update client-people时候遇到乐观锁的问题
  • 前一个client-people更新失败后,后续所有的client-pepole都会更新失败
    有人问第三个问题怎么开出来的,看日志,日志形如“client-people-ld-5 update failed (client-people 1 flush failed)” 第三个问题就是就是导致大规模失败的原因了。

问题分析

前一个client-people更新失败后,后续所有的client-pepole都会更新失败
从代码上看,因为 update-client-people 处于一个单独的 transaction中,所以我们以前假定每次update-client-people是互不影响的。
从日志client-people-ld-5 update failed (client-people 1 flush failed)”来看,事实上这个假定是错误的。
为什么呢?
从文档session.flush来看

每间隔一段时间,Session会执行一些必需的SQL语句来把内存(个人更愿意称为NH的缓存)中的对象的状态同步到JDBC连接中。这个过程被称为刷出(flush),默认会在下面的时间点执行:

  • 在某些查询执行之前
  • 在调用org.hibernate.Transaction.commit()的时候
  • 在调用Session.flush()的时候

从这里看就有点眉目了,在调用session.flush或者Transaction.commit的时候,会将内存中的变更的对象同步到数据库中。(这里留一个小坑,实际因为flushmode的设置,代码里的Transaction.commit实际不会flush,这个在以后讨论)

没有显式的update()或save(),Hibernate会自动检测到集合已经被修改并需要更新回数据>库。这叫做自动脏检查(automatic dirty checking),你也可以尝试修改任何对象的name或者date属性,只要他们处于持久化状态,也就是被绑定到某个Hibernate 的Session上(如:他们刚刚在一个单元操作被加载或者保存),Hibernate监视任何改变并在后台隐式写的方式执行SQL。同步内存状态和数据库的过程,通常只在单元操作结束的时候发生,称此过程为清理缓存(flushing)。在我们的代码中,工作单元由数据库事务的提交(或者回滚)来结束——这是由CurrentSessionContext类的thread配置选项定义的。

更多的什么样的对象会被同步,可以参考 1.3.3. 使关联工作以及修改持久对象修改托管对象自动状态检查等.....这个零乱的我也醉了....

而在nhibernate中,默认的ITransaction只有AdoTransaction一种实现。
而AdoTransaction的注释Wraps an ADO.NET IDbTransaction to implement the ITransaction interface。阅读代码,我们也会发现当rollback的时候,并没有将session缓存里更改过的entity同时回滚。
到此,可以得出结论:
即使transaction rollback了,session中的entity也没有rollback。所以在这次transaction rollback之后,如果再session.flush依然会将之前flush失败的entity再次flush。

session与transaction的最佳实践

一个由Hibernate抛出的异常意味着你必须立即回滚数据库事务,并立即关闭Session (稍后会展开讨论)。如果你的Session绑定到一个应用程序上,你必 须停止该应用程序。回滚数据库事务并不会把你的业务对象退回到事务启动时候的状态。这 意味着数据库状态和业务对象状态不同步。通常情况下,这不是什么问题,因为异常是不可 恢复的,你必须在回滚之后重新开始执行。

额外的东东

session与unit work

实际上hibernate在设计session机制上,主要受到了martin fowler 《P Of EAA》一书中unit work的影响,那个言不称martin fowler,再谈企业架构也枉然的年代。
这种hibernate家的unit work的设计,使得hibernate可以最小化connect to 数据库和尽量提高transaction的performance,开启一级缓存,batch operation等。
而在实践这种设计的过程中,hibernate社区的人相继找到了session-per-user-session,session-per-application,session-per-request,session-per-request-with-detached-objects和session-per-conversation这么模式以及与之对应的应用场景。
例如:我们web api系统中就采用了session-per-request模式,当然这种模式在长时间、大数据量上有很多smell,可以考虑采用其他模式解决。

长session

建议大家继续关注下hibernate文档中后续对于session实践的建议,
长会话的实践建议,意味着session的长时间被保持在内存中,当然也有一些
尤其是如果我们的程序将开一个很长时间的session,那么有什么模式能提高我们程序的性能。
例如乐观锁、session-per-request-with-detached-objects模式、session-per-conversation模式来提高performance。

待解决的问题

session.flushMode = never会发生什么

如果在上述过程中,session.flushMode = never,那么Transaction.commit时将不会自动执行session.flush
只有在session.close时,才会session.flush。
如图:

此时,当 begin transaction 和 commit 的时候,NH 会向数据库发送 SQL 语句。
但是只有 flush 的时候,才会把 DML 语句发送到数据库。
除非是 insert 语句,主键是自增的。(这会在另外一片帖子中讨论)
要注意的是

  • Data Definition Language (DDL) Statements
  • Data Manipulation Language (DML) Statements
  • Transaction Control Statements
  • Session Control Statements
  • System Control Statement
  • Embedded SQL Statements

【以上来自 Orcale】
其中事务控制语句是不同于 DML