一次数据库死锁原因分析

线上crm导出一份报表时长时没反应,日志一看:


2017-09-25 13:45:58 [http-nio-8080-exec-60] ERROR: jdbc.sqltiming#exceptionOccured : 1469. PreparedStatement.executeBatch() FAILED! batching 3 statements: 1: update uwc_applyloan_attach set ATTACH_ID=112103 where ID=44602 2:
update uwc_applyloan_attach set ATTACH_ID=112106 where ID=44603 3: update uwc_applyloan_attach
set ATTACH_ID=112109 where ID=44604
 {FAILED after 135899 msec}
java.sql.BatchUpdateException: Lock wait timeout exceeded; try restarting transaction
    at com.mysql.jdbc.SQLError.createBatchUpdateException(SQLError.java:1167)
    at com.mysql.jdbc.PreparedStatement.executeBatchSerially(PreparedStatement.java:1778)
    at com.mysql.jdbc.PreparedStatement.executeBatchInternal(PreparedStatement.java:1262)
    at com.mysql.jdbc.StatementImpl.executeBatch(StatementImpl.java:970)
    at net.sf.log4jdbc.StatementSpy.executeBatch(StatementSpy.java:526)
    at sun.reflect.GeneratedMethodAccessor387.invoke(Unknown Source)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:601)
    at org.apache.tomcat.jdbc.pool.StatementFacade$StatementProxy.invoke(StatementFacade.java:114)
    at com.sun.proxy.$Proxy33.executeBatch(Unknown Source)
    at org.hibernate.jdbc.BatchingBatcher.doExecuteBatch(BatchingBatcher.java:70)
    at org.hibernate.jdbc.AbstractBatcher.executeBatch(AbstractBatcher.java:268)
    at org.hibernate.engine.ActionQueue.executeActions(ActionQueue.java:268)
    at org.hibernate.engine.ActionQueue.executeActions(ActionQueue.java:185)
    at org.hibernate.event.def.AbstractFlushingEventListener.performExecutions(AbstractFlushingEventListener.java:321)
    at org.hibernate.event.def.DefaultAutoFlushEventListener.onAutoFlush(DefaultAutoFlushEventListener.java:64)
    at org.hibernate.impl.SessionImpl.autoFlushIfRequired(SessionImpl.java:1185)
    at org.hibernate.impl.SessionImpl.list(SessionImpl.java:1261)
    at org.hibernate.impl.QueryImpl.list(QueryImpl.java:102)
    at com.upg.ucars.framework.base.AbstractHibernateDAO$1.doInHibernate(AbstractHibernateDAO.java:172)
    at org.springframework.orm.hibernate3.HibernateTemplate.doExecute(HibernateTemplate.java:406)
    at org.springframework.orm.hibernate3.HibernateTemplate.executeFind(HibernateTemplate.java:343)
    at com.upg.ucars.framework.base.AbstractHibernateDAO.queryByParam(AbstractHibernateDAO.java:147)
    at com.upg.ucars.framework.base.AbstractHibernateDAO.queryByParam(AbstractHibernateDAO.java:92)
    at com.upg.tenant.yuntao.core.UwcApplyloanAttachDaoImpl.getByApplyId(UwcApplyloanAttachDaoImpl.java:19)
    at com.upg.es.seal.core.EsSealServiceImpl.generateDownloadData(EsSealServiceImpl.java:568)
    at com.upg.es.seal.core.EsSealServiceImpl$$FastClassByCGLIB$$f4dc2f5b.invoke(<generated>)
    at net.sf.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204)
    at org.springframework.aop.framework.Cglib2AopProxy$CglibMethodInvocation.invokeJoinpoint(Cglib2AopProxy.java:689)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:150)
    at org.springframework.aop.framework.adapter.ThrowsAdviceInterceptor.invoke(ThrowsAdviceInterceptor.java:124)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172)
    at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:110)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172)
    at org.springframework.aop.framework.adapter.ThrowsAdviceInterceptor.invoke(ThrowsAdviceInterceptor.java:124)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172)
    at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:110)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172)
    at org.springframework.aop.framework.adapter.ThrowsAdviceInterceptor.invoke(ThrowsAdviceInterceptor.java:124)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172)
    at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:110)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172)
    at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:90)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172)
    at org.springframework.aop.framework.Cglib2AopProxy$DynamicAdvisedInterceptor.intercept(Cglib2AopProxy.java:622)
    at com.upg.es.seal.core.EsSealServiceImpl$$EnhancerByCGLIB$$1d4e14b1.generateDownloadData(<generated>)

UwcApplyloanAttachDaoImpl.getByApplyId(UwcApplyloanAttachDaoImpl.java:19)这句造成了锁超时,但奇怪的是这个是个查询语句。

分析了下Hibernate源码,原来在查询中还需要先Flush一下,看org.hibernate.impl.SessionImpl源码:


public List list(String query, QueryParameters queryParameters) throws HibernateException {
        errorIfClosed();
        checkTransactionSynchStatus();
        queryParameters.validateParameters();
        HQLQueryPlan plan = getHQLQueryPlan( query, false );
        autoFlushIfRequired( plan.getQuerySpaces() );

        List results = CollectionHelper.EMPTY_LIST;
        boolean success = false;

        dontFlushFromFind++;   //stops flush being called multiple times if this method is recursively called
        try {
            results = plan.performList( queryParameters, this );
            success = true;
        }
        finally {
            dontFlushFromFind--;
            afterOperation(success);
        }
        return results;
    }

/**
     * detect in-memory changes, determine if the changes are to tables
     * named in the query and, if so, complete execution the flush
     */
    protected boolean autoFlushIfRequired(Set querySpaces) throws HibernateException {
        errorIfClosed();
        if ( ! isTransactionInProgress() ) {
            // do not auto-flush while outside a transaction
            return false;
        }
        AutoFlushEvent event = new AutoFlushEvent(querySpaces, this);
        AutoFlushEventListener[] autoFlushEventListener = listeners.getAutoFlushEventListeners();
        for ( int i = 0; i < autoFlushEventListener.length; i++ ) {
            autoFlushEventListener[i].onAutoFlush(event);
        }
        return event.isFlushRequired();
    }

org.hibernate.event.def.AbstractFlushingEventListener


protected void performExecutions(EventSource session) throws HibernateException {

        log.trace("executing flush");

        try {
            session.getJDBCContext().getConnectionManager().flushBeginning();
            // we need to lock the collection caches before
            // executing entity inserts/updates in order to
            // account for bidi associations
            session.getActionQueue().prepareActions();
            session.getActionQueue().executeActions();
        }
        finally {
            session.getJDBCContext().getConnectionManager().flushEnding();
        }
    }

org.hibernate.jdbc.AbstractBatcher


public void executeBatch() throws HibernateException {
        if (batchUpdate!=null) {
            try {
                try {
                    doExecuteBatch(batchUpdate);
                }
                finally {
                    closeStatement(batchUpdate);
                }
            }
            catch (SQLException sqle) {
                throw JDBCExceptionHelper.convert(
                        factory.getSQLExceptionConverter(),
                        sqle,
                        "Could not execute JDBC batch update",
                        batchUpdateSQL
                    );
            }
            finally {
                batchUpdate=null;
                batchUpdateSQL=null;
            }
        }
    }

看到这里就明白了, doExecuteBatch(batchUpdate); 如果有待Flush的update操作存在,前面一次操作的长事务还没结束而同时另外一个长事务操作又开始了,到了getByApplyId这里又去更新同样的记录就得等待锁释放了。

### 模拟数据库死锁 数据库死锁是指两个或多个事务相互等待对方释放资源,导致事务无法继续执行。要模拟数据库死锁并进行排查,可以按照以下步骤进行。 #### 模拟数据库死锁的步骤 1. **设置测试环境**:确保在测试环境中进行操作,避免影响生产数据。可以使用MySQL、PostgreSQL等支持事务的数据库系统。 2. **创建测试表**: ```sql CREATE TABLE test_table ( id INT PRIMARY KEY, value VARCHAR(100) ); ``` 3. **插入测试数据**: ```sql INSERT INTO test_table (id, value) VALUES (1, 'A'), (2, 'B'); ``` 4. **模拟死锁场景**: - 打开两个或多个数据库会话(Session A 和 Session B)。 - 在Session A中执行以下SQL语句: ```sql START TRANSACTION; UPDATE test_table SET value = 'A1' WHERE id = 1; ``` - 在Session B中执行以下SQL语句: ```sql START TRANSACTION; UPDATE test_table SET value = 'B1' WHERE id = 2; ``` - 回到Session A,尝试更新Session B已经更新的行: ```sql UPDATE test_table SET value = 'A2' WHERE id = 2; ``` - 回到Session B,尝试更新Session A已经更新的行: ```sql UPDATE test_table SET value = 'B2' WHERE id = 1; ``` 此时,数据库会检测到死锁,并自动回滚其中一个事务以解除死锁。MySQL通常会选择一个事务作为“牺牲品”进行回滚。 #### 排查数据库死锁的方法 1. **查看死锁日志**: - 对于MySQL,可以使用`SHOW ENGINE INNODB STATUS;`命令查看最近的死锁信息。输出结果中的`LATEST DETECTED DEADLOCK`部分会包含最近一次死锁的详细信息,包括涉及的事务、锁定的资源以及导致死锁的SQL语句[^2]。 2. **使用INFORMATION_SCHEMA.LOCKS视图**: - MySQL 8.0以后,提供了`INFORMATION_SCHEMA.LOCKS`视图,允许开发者查看当前系统中锁的情况。通过查询此视图,可以了解哪些事务持有锁、哪些事务正在等待锁[^2]。 3. **分析死锁日志和SQL语句**: - 仔细分析死锁日志中的信息,找出导致死锁的SQL语句和事务。优化这些SQL语句和事务逻辑,尽量缩短事务持有锁的时间,减少死锁的可能性[^3]。 4. **调整事务隔离级别**: - 通过将事务隔离级别降低为读未提交(或读已提交),可以减少死锁的发生概率。但是要注意隔离级别的降低可能引发脏读、不可重复读等数据一致性问题,在选择时需要权衡利弊。 5. **优化查询和事务逻辑**: - 分析造成死锁的原因,优化查询语句和事务逻辑。例如,按照相同的顺序获取锁,避免跨事务的循环依赖等。 6. **使用行级锁**: - 行级锁可以较小地限制锁的范围,从而减少死锁的可能性。将表的锁粒度调整为行级别,可以减少事务之间的冲突[^3]。 7. **设置合理的超时时间和重试机制**: - 当发生死锁时,可以设置适当的超时时间,在一定时间内尝试解锁操作。如果超过设定的时间仍未成功,则进行死锁处理,如终止较早请求的事务或进行回滚等。 ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值