Hibernate: 尝试获取锁时发现死锁

15

我在项目中使用Hibernate,在执行非常简单的数据库操作时,出现了随机的表面死锁。

以下是其中之一的堆栈跟踪:https://gist.github.com/knyttl/8999006 - 令我困惑的是,第一个异常是RollbackException,然后是LockAquisition异常。

问题经常发生在类似子句上:

@Transactional
public void setLastActivity() {
    User user = em.findById(...);
    user.setLastActivity(new Date());
    em.merge(user);
    em.flush();
}

我很困惑,不知道这是Hibernate、MySQL还是C3PO的问题。

我的Hibernate配置:

            <prop key="hibernate.dialect">${database.dialect}</prop>
            <prop key="hibernate.hbm2ddl.auto">${database.structure}</prop>
            <prop key="hibernate.connection.url">${database.connection}</prop>
            <prop key="hibernate.connection.username">${database.username}</prop>
            <prop key="hibernate.connection.password">${database.password}</prop>
            <prop key="hibernate.connection.driver_class">${database.driver}</prop>
            <prop key="hibernate.connection.shutdown">true</prop>
            <prop key="hibernate.connection.writedelay">0</prop>
            <prop key="hibernate.connection.characterEncoding">UTF-8</prop>
            <prop key="hibernate.connection.charSet">UTF-8</prop>
            <prop key="hibernate.show_sql">${database.show_sql}</prop>
            <prop key="hibernate.format_sql">false</prop>
            <prop key="hibernate.ejb.metamodel.generation">disabled</prop>
            <!-- Use the C3P0 connection pool provider -->
            <prop key="hibernate.connection.provider_class">org.hibernate.connection.C3P0ConnectionProvider</prop>
            <prop key="hibernate.c3p0.min_size">0</prop>
            <prop key="hibernate.c3p0.max_size">50</prop>
            <prop key="hibernate.c3p0.timeout">120</prop>
            <prop key="hibernate.c3p0.max_statements">0</prop>
            <prop key="hibernate.c3p0.max_statementsPerConnection">0</prop>
            <prop key="hibernate.c3p0.maxStatementsPerConnection">0</prop>
            <prop key="hibernate.c3p0.idle_test_period">120</prop>
            <prop key="hibernate.c3p0.acquire_increment">1</prop>
            <prop key="hibernate.c3p0.numHelperThreads">8</prop>

编辑1:

  • 我之前写道有看似发生死锁的情况 - 这是错误的,只会出现“尝试获取锁时发现死锁”的情况。

编辑2:

这也会发生在这些方法上 - 需要用 @Transactional 注释进行注释:

@Transactional
public void setLastActivity() {
    em.insertNative("table")
           .values(...)
           .execute();
}

您可能需要将autocommit设置为false。 - brettw
Gimby:给出了堆栈跟踪 - 仔细看看。brettw:你能给更多信息吗?为什么会有帮助? - Vojtěch
你使用了“Apparent Deadlock”这个短语,但是在你的gist中并没有出现。你是否在某个时候看到了c3p0发出的APPARENT DEADLOCK消息?如果是这样,能否通过随后的长状态消息来重现它? - Steve Waldman
1
当你去掉 @Transactional 注解时会发生什么? 最近由于 @Transactional 注解,我在 hibernate 中遇到了死锁问题。 - Jan Vladimir Mostert
1
setLastActivity方法中有些奇怪的地方,用户是一个已附加的实体,因此不需要合并它,因为合并对已附加的实体没有影响。此外,在方法结束前刷新也不是必要的,因为Spring会为我们执行刷新操作。是否有特殊原因需要手动刷新?这可能与其他相关事项有关。 - Angular University
显示剩余8条评论
2个回答

24
由于死锁经常发生,看起来应用程序的一些线程在持有锁的时间上很长。每个应用程序中的线程在访问数据库时将使用自己的数据库连接/连接,因此从数据库的角度来看,两个线程是竞争数据库锁的两个不同客户端。如果一个线程长时间持有锁,并按特定顺序获取锁,而第二个线程以不同的顺序获取相同的锁,则死锁就会发生(请参见here以了解这种常见死锁原因的详细信息)。另外,死锁发生在读操作中,这意味着某些线程也正在获取读锁。如果线程在REPEATABLE_READ隔离级别或SERIALIZABLE上运行事务,则会发生这种情况。要解决这个问题,请尝试搜索项目中Isolation.REPEATABLE_READ和Isolation.SERIALIZABLE的使用情况,以查看是否正在使用它们。
作为替代方案,使用默认的READ_COMMITTED隔离级别,并使用@Version注释实体,使用乐观锁定处理并发。

还要尝试识别长时间运行的事务,有时会发生这种情况,当@Transactional被放置在错误的位置时,在批处理的示例中,它会包装整个文件的处理,而不是逐行进行事务处理。

这是一个log4j配置,用于记录实体管理器和事务的创建/删除以及开始/提交/回滚:

   <!-- spring entity manager and transactions -->
<logger name="org.springframework.orm.jpa" additivity ="false">
    <level value="debug" />
    <appender-ref ref="ConsoleAppender" />
</logger >
<logger name="org.springframework.transaction" additivity ="false">
    <level value="debug" />
    <appender-ref ref="ConsoleAppender" />
</logger >
  1. 我是否可以以某种方式执行更新查询(无论是JPA / Native),而不必通过@Transactional锁定表格?

可以通过本地查询或JPQL进行更新查询。

  1. 我是否可以在不使用@Transactional的情况下进入会话? 例如,计划线程尝试读取实体上的延迟字段会导致LazyInitializationException - 如果该方法未注释为@Transactional,则没有会话。

在没有@Transactional 的方法中,查询将在自己的实体管理器中执行,并仅返回分离的实体,因为查询运行后会立即关闭会话。

因此,在没有@Transactional 的方法中出现懒加载初始化异常是正常的。您也可以将它们设置为@Transactional(readOnly=true)


谢谢您的回复。我没有找到任何隔离设置:默认始终使用。对我来说最有趣的是找出哪些事务导致了锁定。从异常中,我只看到那些无法获取锁定的事务。有没有办法找到它们? - Vojtěch
1
我刚刚添加了一些log4j配置来记录事务的开始/提交/回滚,它还将记录隔离级别和传播行为。同时也有记录实体管理器的创建/销毁的日志。 - Angular University
1
DBA们是否查看了数据库,以确定是否有其他进程在运行,导致死锁的原因?他们应该能够提供大量的信息。 - Angular University
不幸的是,我是数据库管理员,所以 :-) 好吧,我会接受你的回答,因为现在我正在正确的轨道上。我有一些小问题要问你:1. 我能否在没有通过 @Transactional 锁定表的情况下执行更新查询(JPA / Native)?2. 我能否在不使用 @Transactional 的情况下进入会话?例如,定时线程尝试读取实体上的延迟字段会导致 LazyInitializationException - 没有会话,如果该方法未带有 @Transactional 注释。 - Vojtěch
我已经在答案中添加了这两个问题,可以进行批量更新,并且在没有@Transactional注解的方法中出现LazyInitializationException是正常的。 - Angular University
广告:“通过本地查询或JPQL,可以进行更新查询。”我会做这些操作,但是它们“需要”@Transactional,否则我会得到“需要事务”异常。 广告2)好的,谢谢。 - Vojtěch

8

这是MySQL报错信息。

解决和避免死锁最简单的方法是重新排列应用程序中发生的数据库操作。

死锁通常发生在多个资源/连接尝试以相反的顺序获取多个锁时,如下所示:

connection 1: locks key(1), locks key(2);
connection 2: locks key(2), locks key(1);

当两个连接同时执行时,连接1将锁定键(1),连接2将锁定键(2)。之后,这两个连接将等待另一个释放密钥的锁。这会导致死锁。

但是,稍微调整一下事务的顺序,就可以避免死锁。

connection 1: locks key(1), locks key(2);
connection 2: locks key(1), locks key(2);

以上的重新排序方法可以避免死锁问题。其他避免死锁的方法包括使用事务管理机制。Spring 的事务管理几乎是即插即用的。此外,您还可以采取死锁重试策略。有趣的 Spring AOP 死锁重试方法可以在这里找到。这样做只需将注释添加到希望在死锁情况下进行重试的方法即可。
为了查找哪些语句可能引起死锁,可以尝试运行 "show engine innodb status" 诊断工具以获取更多调试日志。此外,您还可以查看 如何处理死锁更新:事务型数据库操作中发生死锁的场景如下:
在事务型数据库中,当两个进程各自在其自己的事务中以相反的顺序更新两行信息时,就会发生死锁。例如,进程 A 在精确的时间框架内先更新第一行然后在更新第二行,而进程 B 在更新第二行然后在更新第一行。进程 A 无法完成更新第二行,直到进程 B 完成,但在进程 A 完成更新第一行之前,它无法完成更新第一行。无论允许多长时间,这种情况都永远不会自行解决,因此数据库管理系统通常会中止做的工作量较少的进程的事务。
Shishir

我理解这个问题,而且我已经阅读了类似的描述。但是这种情况发生在非常简单的事务中,就像我上面提到的那样。我没有手动锁定表,这是由Hibernate和@Transactional注释完成的。 - Vojtěch
2
只是好奇,如果您删除@Transactional注释,应用程序是否可以在没有死锁情况下工作?Hibernate中95%的死锁情况源于无序的DB事务,并且如果DDL和DML重新排序为更同步的顺序,则可以避免这种情况。 - Shishir Kumar
我曾认为所有的entitymanager调用都必须在事务中进行包装 - 这就是为什么它被注释的原因。所以这不是必要的吗? - Vojtěch
事务是必要的,但在应用程序中可能会有多个事务同时运行,如果操作没有按正确顺序处理,可能会导致死锁。我已经在我的答案中添加了关于事务数据库中死锁的另一个解释。 - Shishir Kumar

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接