获取"死锁发生在尝试获取锁时;尝试重新启动事务"

42

我的应用程序(Java Spring Core)有多个线程同时运行并访问数据库,我在一些高峰期间遇到了异常

07:43:33,400 WARN  [org.hibernate.util.JDBCExceptionReporter] SQL Error: 1213, SQLState: 40001
07:43:33,808 ERROR [org.hibernate.util.JDBCExceptionReporter] Deadlock found when trying to get lock; try restarting transaction
07:43:33,808 ERROR [org.hibernate.event.def.AbstractFlushingEventListener] Could not synchronize database state with session
org.hibernate.exception.LockAcquisitionException: could not insert: [com.xminds.bestfriend.frontend.model.Question]
    at org.hibernate.exception.SQLStateConverter.convert(SQLStateConverter.java:107)
    at org.hibernate.exception.JDBCExceptionHelper.convert(JDBCExceptionHelper.java:66)
    at org.hibernate.persister.entity.AbstractEntityPersister.insert(AbstractEntityPersister.java:2436)
    at org.hibernate.persister.entity.AbstractEntityPersister.insert(AbstractEntityPersister.java:2856)
    at org.hibernate.action.EntityInsertAction.execute(EntityInsertAction.java:79)
    at org.hibernate.engine.ActionQueue.execute(ActionQueue.java:273)
    at org.hibernate.engine.ActionQueue.executeActions(ActionQueue.java:265)
    at org.hibernate.engine.ActionQueue.executeActions(ActionQueue.java:184)
    at org.hibernate.event.def.AbstractFlushingEventListener.performExecutions(AbstractFlushingEventListener.java:321)
    at org.hibernate.event.def.DefaultFlushEventListener.onFlush(DefaultFlushEventListener.java:51)
    at org.hibernate.impl.SessionImpl.flush(SessionImpl.java:1216)
    at org.hibernate.impl.SessionImpl.managedFlush(SessionImpl.java:383)
    at org.hibernate.transaction.JDBCTransaction.commit(JDBCTransaction.java:133)
    at org.springframework.orm.hibernate3.HibernateTransactionManager.doCommit(HibernateTransactionManager.java:656)
    at org.springframework.transaction.support.AbstractPlatformTransactionManager.processCommit(AbstractPlatformTransactionManager.java:754)
    at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:723)
    at org.springframework.transaction.support.TransactionTemplate.execute(TransactionTemplate.java:147)
    at com.xminds.bestfriend.consumers.Base.onMessage(Base.java:96)
    at org.springframework.jms.listener.adapter.MessageListenerAdapter.onMessage(MessageListenerAdapter.java:339)
    at org.springframework.jms.listener.AbstractMessageListenerContainer.doInvokeListener(AbstractMessageListenerContainer.java:535)
    at org.springframework.jms.listener.AbstractMessageListenerContainer.invokeListener(AbstractMessageListenerContainer.java:495)
    at org.springframework.jms.listener.AbstractMessageListenerContainer.doExecuteListener(AbstractMessageListenerContainer.java:467)
    at org.springframework.jms.listener.AbstractPollingMessageListenerContainer.doReceiveAndExecute(AbstractPollingMessageListenerContainer.java:325)
    at org.springframework.jms.listener.AbstractPollingMessageListenerContainer.receiveAndExecute(AbstractPollingMessageListenerContainer.java:263)
    at org.springframework.jms.listener.DefaultMessageListenerContainer$AsyncMessageListenerInvoker.invokeListener(DefaultMessageListenerContainer.java:1058)
    at org.springframework.jms.listener.DefaultMessageListenerContainer$AsyncMessageListenerInvoker.executeOngoingLoop(DefaultMessageListenerContainer.java:1050)
    at org.springframework.jms.listener.DefaultMessageListenerContainer$AsyncMessageListenerInvoker.run(DefaultMessageListenerContainer.java:947)
    at java.lang.Thread.run(Thread.java:662)
Caused by: com.mysql.jdbc.exceptions.jdbc4.MySQLTransactionRollbackException: Deadlock found when trying to get lock; try restarting transaction
    at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
    at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:39)
    at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:27)
    at java.lang.reflect.Constructor.newInstance(Constructor.java:513)
    at com.mysql.jdbc.Util.handleNewInstance(Util.java:411)
    at com.mysql.jdbc.Util.getInstance(Util.java:386)
    at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:1065)
    at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:4074)
    at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:4006)
    at com.mysql.jdbc.MysqlIO.sendCommand(MysqlIO.java:2468)
    at com.mysql.jdbc.MysqlIO.sqlQueryDirect(MysqlIO.java:2629)
    at com.mysql.jdbc.ConnectionImpl.execSQL(ConnectionImpl.java:2719)
    at com.mysql.jdbc.PreparedStatement.executeInternal(PreparedStatement.java:2155)
    at com.mysql.jdbc.PreparedStatement.executeUpdate(PreparedStatement.java:2450)
    at com.mysql.jdbc.PreparedStatement.executeUpdate(PreparedStatement.java:2371)
    at com.mysql.jdbc.PreparedStatement.executeUpdate(PreparedStatement.java:2355)
    at com.mchange.v2.c3p0.impl.NewProxyPreparedStatement.executeUpdate(NewProxyPreparedStatement.java:105)
    at org.hibernate.jdbc.NonBatchingBatcher.addToBatch(NonBatchingBatcher.java:46)
    at org.hibernate.persister.entity.AbstractEntityPersister.insert(AbstractEntityPersister.java:2416)
    ... 25 more

我的代码看起来

try
{
       this.consumerTransactionTemplate.execute(new TransactionCallbackWithoutResult(){

                    @Override
                    protected void doInTransactionWithoutResult(
                            TransactionStatus status)
                    {
                        process();
                    }

                });

  }
  catch(Exception e){
     logger.error("Exception occured " , e);
      //TODO: Exception handling
  }

https://dev.mysql.com/doc/refman/8.0/en/innodb-deadlocks-handling.html - Chloe
6个回答

45

MySQL的InnoDB引擎采用行级锁定,即使您的代码正在插入或更新单个行(特别是如果在正在更新的表上有几个索引),也可能导致死锁。最好的方法是围绕此设计代码,以便在由于死锁而失败时重试事务。有关MySQL死锁诊断和可能的解决方法的一些有用信息可以在这里找到。

Spring中通过AOP实现死锁重试的有趣实现可以在这里找到。这样,您只需要将注释添加到想要在死锁情况下重试的方法即可。


4
给定的链接已经移动到新位置 https://dzone.com/articles/automatic-deadlock-retry。 - USKMobility

27

Emir的回答很好,并描述了你遇到的问题。但是我建议你尝试spring-retry

这是一个通过注解实现重试模式的杰出框架。

例如:

 @Retryable(maxAttempts = 4, backoff = @Backoff(delay = 500))
 public void doSomethingWithMysql() {
   consumerTransactionTemplate.execute(
             new TransactionCallbackWithoutResult(){
                @Override
                protected void doInTransactionWithoutResult(                 
                      TransactionStatus status)
                {
                    process();
                }

            });
 } 

在出现异常情况下,它将使用500毫秒的回退策略最多重试(调用)4次方法doSomethingWithMysql()


3
这正是我实施的内容,但是死锁错误正在阻碍我的日志记录。有没有一种干净的方法可以抑制异常? - Chognificent
当一个事务方法调用另一个事务方法时,这个方法还有效吗? - undefined

3
如果您正在使用JPA/Hibernate,则只需按照以下步骤避免死锁。一旦您获得了锁,请勿在事务中的任何地方使用相同ID调用数据库(我的意思是说,您不应再次获取相同ID的实体),您可以修改并保存锁定对象,没有问题。
服务级别:
employee=empDao.getForUpdate(id);

Dao级别:

public employee getForUpdate(String id)
return mySqlRepository.getForUpdate(id)

仓库(Repository)接口:

@Lock(LockModeType.PESSIMITSIC_WRITE)
@Query("select e from employee e where id=?1")
public employee getForUpdate(String id)

2
这是一个使用纯Spring而没有额外框架的示例。
    TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager); // autowired
    transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_SERIALIZABLE); // this increases deadlocks, but prevents multiple simultaneous similar requests from inserting multiple rows
    Object o = transactionTemplate.execute(txStatus -> {
            for (int i=0; i<3; i++) {
                try {
                    return findExistingOrCreate(...);
                } catch (DeadlockLoserDataAccessException e) {
                    Logger.info(TAG, "create()", "Deadlock exception when trying to find or create. Retrying "+(2-i)+" more times...");
                    try { Thread.sleep(2^i*1000); } catch (InterruptedException e2) {}
                }
            }
            return null;
    });
    if (o == null) throw new ApiException(HttpStatus.SERVICE_UNAVAILABLE, "Possible deadlock or busy database, please try again later.");

使用可序列化事务隔离级别是因为它将 SELECT 转换为 SELECT ... IN SHARE MODE / SELECT ... FOR UPDATE 并锁定这些行,这符合我的情况。 findExistingOrCreate()正在进行大量复杂的搜索以查找现有行,并自动生成名称并检查不良单词等。当同时出现许多相同的请求时,它会创建多个行。使用可序列化事务隔离级别,它现在是幂等的;它现在锁定行,创建一个单独的行,所有后续请求都返回新的现有行。

我读到过,你在异常发生后不应该重复使用实体管理器。 - undefined

1
当你面临“死锁检测”这种错误时,应检查查询执行情况,并验证是否有两个或更多并发事务可能会引起死锁。这些事务应按相同顺序获取数据库锁,以避免死锁。

7
如果问题不在代码中,而是在一个并发系统中,该系统试图同时访问同一资源,那该怎么办? - Aritz
1
@XtremeBiker - 我想当重试交易是一种合理的解决方案时。 - youcantryreachingme

0

这种情况也可能发生在没有并发的应用程序中,其中一个线程连续插入记录。如果一个表具有唯一约束条件,MySQL在提交之后会“构建”该约束条件。这会锁定表,并且可能干扰下一个插入操作,从而导致上述死锁。虽然我只注意到在Windows上出现了这个错误。

与其他数据库 - PostgreSQL、Oracle或H2 - 不需要这种变通方法,它们可以正常工作。


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