死锁案例
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无法回滚
评论区