一次处理Deadlock的实战经历

数据库死锁是并发系统开发中有时会碰到的问题。由于其往往难以重现,对于同学们来说经常是一个比较头疼的问题。本文记录了一次处理Deadlock问题的实战过程,涉及到Hibernate和事务的隔离级别等内容。出于保密原因,不会出现具体的代码。

一个线上的批处理系统报Deadlock。由于系统启用了一些保护机制,当长时间数据库没有相应时,DB的connection就会timeout掉并抛exception。这是一个挺好的实践经验,既保护了整个系统,又留下了一些信息供事后分析。

从日志文件里的Exception信息发现,死锁是在两个并行执行的工作线程同时执行一段事务时发生的。看了一下相关代码,主要干了以下事情:

  1. 开Transaction

  2. 通过Hibernate往数据库里插入一个objectA进去;

  3. 通过Hibernate从数据库读一个objectB出来;

  4. commit transaction

有经验的同学可能已经发现一些端倪了。看了一下objectB的定义,其中一个字段上赫然放了一个@OneToOne的注解,和另一个类ClassC联系起来,而ClassC里面的一个字段就是objectA的Class。从异常的错误信息也可以看出来,hibernate生成了一个很长的SQL,其中就join了objectA对应的那张表。

OneToOne注解的一个属性叫FetchType。这个属性指定了读取当前object时,是否一块把关联的字段数据一起读出来。Hibernate规定了两种FetchType

  1. EAGER:读取当前对象时,会把关联对象一起读出来。对应执行了一个SQL的join

  2. LAZY:读取当前对象时,不会把关联对象读出来,相应的字段被置为NULL。

由于在代码中没有显式指定FetchType,就使用了默认的值Eager。所以在第2步读objectB的时候,会访问objectA对应的那张表。

这段代码会怎么执行还要看事务的隔离级别是怎么设定的。我们知道数据库的transaction有四种隔离界别:

  1. Read Uncommited(最低级别,没有隔离);

  2. Read Commited(不会去读别的事务里没有提交的数据,解决了脏读问题);

  3. Repeatable Read(保证了两次读同一行数据是一样的,解决了不可重复读问题);

  4. Serializable(保证了两次读同一张表是一样的,解决了幻读问题);

不同数据库对不同隔离级别实现说白了就是加锁(共享锁,排他锁,行锁,表锁)。在代码里面没有显式指定隔离级别,所以使用的是数据库的默认级别,一般是第2个。

通过调试这段代码,发现当执行到第1步的时候,会有一把行锁放到objectA对应的表上面去。这样当两个线程同时执行这段代码的时候,第1步各放了一个行锁(写锁)到objectA的表,然后同时执行第2步,然后,就没有然后了。。。

找到原因之后,修这段代码的方式有很多。从业务上来看,这两个线程涉及的资源本身就是相互隔离的。而且这个事务的用法本身就是值得商榷的。我这里使用了最简单一种修改的方法:显式的降低了隔离级别。

还有要注意的是,在指定Hibernate对象之间的关系时,如果没有特殊需要,一般要使用Lazy的FetchType。这对于性能还有避免不必要的麻烦都是有好处的。