目 录CONTENT

文章目录

【实践】数据库异常分析

FatFish1
2025-06-10 / 0 评论 / 0 点赞 / 25 阅读 / 0 字 / 正在检测是否收录...

死锁案例

Ex in ******: org.springframework.dao.DeadlockLoserData*****#*#***** ### Error updating data*****#*#***** ql.SQLTransactionRollbackException: (conn=180366245) Deadlock found when trying to get lock; try restarting transaction[N]### The error may exist in file.....

查看告警是死锁导致,分析这段代码逻辑如下:

    @Transactional(rollbackFor = Exception.class)
    public void handleOneBatch(List<AccumulatorTmpFeeEntry> mainFeeTempList, AccumulatorFeeMatchKey batchKey) {
        // 业务操作
        doBusiness(...);

        // dao1的查询操作
        dao1.select(....);

        // 业务操作
        doBusiness(...);

        // 其他表的DML操作
        dao2.insert(....);

        // 业务操作
        doBusiness(...);

        // 其他表的DML操作
        dao2.update(....);

        // dao1的删除操作
        dao1.delete(....);
    }

异常点出现在dao1.delete(....);

初步推测是由于select操作加S锁,然而事务中存在大量的业务逻辑,导致单个事务耗时长,此时事务2也执行select操作加S锁,这是事务1执行dao1.delete()操作申请X锁,则申请不到,产生死锁

但是这个推测有一点站不住脚:

  • dao1.delete操作使用了主键索引,方法为range,应该可以使用行锁,只要两个事务处理的不是同一批数据,就应该不会导致锁竞争的问题,而方法上游是有redis加锁机制的

进一步分析,如果处理的不是同一批数据就不会竞争吗?

查阅资料发现,加锁的原理实际是对索引加锁。InnoDB 的行锁是通过给索引上的索引项加锁来实现的。

  • 如果使用的是主键索引,则直接对主键索引加锁;

  • 如果使用的是二级索引,先对二级索引加锁,然后再对主键索引加锁(因为要回表);

  • 不使用索引,则是全表加锁

且使用的是nextkey锁(行锁+GAP锁),即对一条数据及其一个范围的GAP加锁,可能导致不同数据出现冲突

但是再次查询数据库的隔离级别:READ COMMITTED,这个级别应该是使用MVCC限制读,不加nextkey锁,这一点依旧存疑

于是继续在网上找资料,偶然找到一篇wiki:https://blog.csdn.net/n88Lpo/article/details/127032963

其中分析的是在一些insert场景下,RC级别也可能加nextkey锁,因此考虑此场景是否有insert与delete并发的场景,由于目前没有拿到死锁的样本,计划下次发生死锁时执行show engine innodb status 捞取一些详细的加锁信息查看

事务嵌套导致的回滚失效案例

背景:使用shardingjdbc管理多个分库,将三种业务数据放在一个事务组中入库,使用了编程式事务

我们的源码逻辑如下:

DefaultTransactionDefinition transactionDefinition = new DefaultTransactionDefinition();
transactionDefinition.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
transactionDefinition.setTimeout(timeout);
TransactionStatus tx = transactionManager.getTransaction(transactionDefinition);

public boolean myFunc(List<AccumlatorFeeEntry> mainFees) {
    ……
    DefaultTransactionDefinition transactionDefinition = new DefaultTransactionDefinition();
    transactionDefinition.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
    transactionDefinition.setTimeout(timeout);
    TransactionStatus tx = transactionManager.getTransaction(transactionDefinition);
    try {
        // save businessData1
        save()...
        // save businessData2
        save()...

       // save businessData3
       DataSource dataSource 
           = pipelineShardingDataSourceFactory.getDataSource(databaseShardingAlgorithm.getOrSaveDbSourceName(factor));
       Connection connection = null;
       PreparedStatement idempotencePS = null;
       ……
       try {
           connection = dataSource.getConnection();
           connection.setAutoCommit(false);
           for (Map.Entry<String, List<BillingIdempotenceEntry>> entry : udrIdempotenceMap.entrySet()) {
               // doSave
           }
           connection.commit();
           connection.setAutoCommit(true);
       } catch (Exception e) {
           try {
               if (connection != null) {
                   connection.rollback();
               }
           } catch (SQLException ex) {
               log.error("roll back error, meet sql exception");
           }
           return 0;
       } finally {
           closeConnQuiet(...);
       } 
    } catch (Throwable e) {
        if (tx.isNewTransaction() && !tx.isCompleted()) {
            transactionManager.rollback(tx);
        }
    }
    if (tx.isNewTransaction() && !tx.isCompleted()) {
        transactionManager.commit(tx);
    }
    return true;
}

看这里的逻辑,首先通过spring-transaction显式声明了一个事务

spring-transaction有如下特点:通过threadLocal持有datasource,进而持有connection

而使用shardingJdbc时,对一个抽象的datasource开启事务,相当于逻辑开启,会在真实datasoruce获取时真正开启事务;而回滚时,会要求持有的所有真实datasoruce一起回滚

但是该实例中的常见,datasource是通过shardingJdbcFactory获取的,而connection是新建的,并未被spring-transaction管理

最终一旦报错,spring管理的data1、data2回滚,而data3无法回滚

0

评论区