休眠:尝试获取锁定时发现死锁

2022-09-03 10:07:37

我在我的项目中使用休眠,并且我得到了非常简单的数据库操作的随机明显死锁。

有一个堆栈跟踪:https://gist.github.com/knyttl/8999006 - 让我感到困惑的是,第一个异常是回滚异常,然后有LockAquisition异常。

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

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

我很困,因为我不知道这是Hibernate,MySQL还是C3P0的问题。

我的休眠配置:

            <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();
}

答案 1

由于死锁发生得非常频繁,因此看起来应用程序的某些线程长时间保持锁。

应用程序中的每个线程在访问数据库时都将使用自己的数据库连接/连接,因此从数据库的角度来看,两个线程是两个不同的客户端,它们竞争数据库锁。

如果一个线程长时间持有锁并按特定顺序获取它们,并且第二个线程来获取相同的锁,但顺序不同,则必然会发生死锁(有关此频繁死锁原因的详细信息,请参阅此处)。

读取操作中也发生死锁,这意味着某些线程也在获取读锁定。如果线程在隔离级别或 中运行事务,则会发生这种情况。REPEATABLE_READSERIALIZABLE

要解决此问题,请尝试在项目中搜索 和 的用法,以查看是否正在使用。Isolation.REPEATABLE_READIsolation.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的情况下以某种方式进入会话吗?例如,调度线程尝试读取实体上的 Lazy 字段,如果该方法未使用 @Transactional

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

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


答案 2

这是MySQL的错误。

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

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

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

在两个连接同时执行的情况下,连接 1 将获取 key(1) 上的锁定,而连接 2 将获取 key(2) 上的锁。之后,两个连接都将等待其他人释放钥匙上的锁。这会导致死锁。

但是,对交易顺序稍作调整,就可以避免死锁。

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

以上重新排序是死锁证明。

避免死锁的其他方法是使用事务管理机制。Spring的交易管理几乎是即插即用的。此外,您可以设置死锁重试策略。可以在此处找到通过Spring AOP进行的有趣的死锁重试。这样,您只需将注释添加到要在发生死锁时重试的方法。

要在死锁上获得更多调试日志以找出哪些语句可疑,请尝试运行“显示引擎 innodb 状态”诊断程序。另外,您可以看看 如何应对死锁

更新:事务性数据库操作中的死锁方案。

在事务数据库中,当两个进程在其自己的事务中分别更新两行信息但顺序相反时,就会发生死锁。例如,进程 A 更新行 1,然后更新确切时间范围进程 B 中的行 2,然后更新行 1。在进程 B 完成之前,进程 A 无法完成对行 2 的更新,但在进程 A 完成之前,它无法完成对行 1 的更新。无论允许经过多少时间,这种情况都永远不会自行解决,并且由于这种数据库管理系统通常会杀死已完成最少工作量的进程的事务。

希希尔


推荐