一次处理Deadlock的实战经历
26 Nov 2014数据库死锁是并发系统开发中有时会碰到的问题。由于其往往难以重现,对于同学们来说经常是一个比较头疼的问题。本文记录了一次处理Deadlock问题的实战过程,涉及到Hibernate和事务的隔离级别等内容。出于保密原因,不会出现具体的代码。
一个线上的批处理系统报Deadlock。由于系统启用了一些保护机制,当长时间数据库没有相应时,DB的connection就会timeout掉并抛exception。这是一个挺好的实践经验,既保护了整个系统,又留下了一些信息供事后分析。
从日志文件里的Exception信息发现,死锁是在两个并行执行的工作线程同时执行一段事务时发生的。看了一下相关代码,主要干了以下事情:
-
开Transaction
-
通过Hibernate往数据库里插入一个objectA进去;
-
通过Hibernate从数据库读一个objectB出来;
-
commit transaction
有经验的同学可能已经发现一些端倪了。看了一下objectB的定义,其中一个字段上赫然放了一个@OneToOne的注解,和另一个类ClassC联系起来,而ClassC里面的一个字段就是objectA的Class。从异常的错误信息也可以看出来,hibernate生成了一个很长的SQL,其中就join了objectA对应的那张表。
OneToOne注解的一个属性叫FetchType。这个属性指定了读取当前object时,是否一块把关联的字段数据一起读出来。Hibernate规定了两种FetchType:
由于在代码中没有显式指定FetchType,就使用了默认的值Eager。所以在第2步读objectB的时候,会访问objectA对应的那张表。
这段代码会怎么执行还要看事务的隔离级别是怎么设定的。我们知道数据库的transaction有四种隔离界别:
-
Read Uncommited(最低级别,没有隔离);
-
Read Commited(不会去读别的事务里没有提交的数据,解决了脏读问题);
-
Repeatable Read(保证了两次读同一行数据是一样的,解决了不可重复读问题);
-
Serializable(保证了两次读同一张表是一样的,解决了幻读问题);
不同数据库对不同隔离级别实现说白了就是加锁(共享锁,排他锁,行锁,表锁)。在代码里面没有显式指定隔离级别,所以使用的是数据库的默认级别,一般是第2个。
通过调试这段代码,发现当执行到第1步的时候,会有一把行锁放到objectA对应的表上面去。这样当两个线程同时执行这段代码的时候,第1步各放了一个行锁(写锁)到objectA的表,然后同时执行第2步,然后,就没有然后了。。。
找到原因之后,修这段代码的方式有很多。从业务上来看,这两个线程涉及的资源本身就是相互隔离的。而且这个事务的用法本身就是值得商榷的。我这里使用了最简单一种修改的方法:显式的降低了隔离级别。
还有要注意的是,在指定Hibernate对象之间的关系时,如果没有特殊需要,一般要使用Lazy的FetchType。这对于性能还有避免不必要的麻烦都是有好处的。