公司的某些业务用到了数据库的悲观锁 for update,但有些同事没有把 for update 放在 Spring 事务中执行,在并发场景下发生了严重的线程阻塞问题,为了把这个问题吃透,秉承着老司机的职业素养,我决定要给同事们一个交代。
1. 案发现场
最近公司的某些 Dubbo 服务之间的 RPC 调用过程中,偶然性地发生了若干起严重的超时问题,导致了某些模块不能正常提供服务。我们的数据库用的是 Oracle,经过 DBA 排查,发现了一些 sql 的执行时间特别长,对比发现这些执行时间长的 sql 都带有 for update 悲观锁,于是相关开发人员查看 sql 对应的业务代码,发现 for update 没有放在 Spring 事务中执行,但是按照常理来说,如果 for update 没有加 Spring 事务,每次执行完 Mybatis 都会帮我们 commit 释放掉资源,并发时出现的问题应该是没有锁住对应资源产生脏数据而不是发生阻塞。但是经过代码的调试,不加 Spring 事务并发执行确实会阻塞。
2. 案例分析
基于案发现场的问题所在,我特地写了几个针对问题的案例分析测试代码,”talk is cheap, show you the code”:
2.1 加 Spring 事务执行但不提交事务
publicvoidforupdateByTransaction() throws Exception { // 主线程获取独占锁reentrantLock.lock();newThread(()->transactionTemplate.execute(transactionStatus ->{ // select * from forupdate where name = #{name} for updatethis.forupdateMapper.findByName("testforupdate");System.out.println("==========for update==========");countDownLatch.countDown(); // 阻塞不让提交事务reentrantLock.lock();returnnull;})).start();countDownLatch.await();System.out.println("==========for update has countdown==========");this.forupdateMapper.updateByName("testforupdate");System.out.println("==========update success==========");reentrantLock.unlock();}
此时 for update 被包装在 Spring 事务中,将事务交由 Spring 管理,根据数据事务机制,sql 执行过程中,只有执行了 commit 或者 rollback 操作, 才会提交事务,所以此时每次执行 commit,for update 没有被释放,会锁住对应资源,直到提交事务释放 for udpate。所以此时的主线程执行更新操作会阻塞。
2.2 不加 Spring 事务并发执行
首先我们先将数据库连接池的初始化大小调大一点,使该次并发执行至少会获取 2 个以上 ID 不同的 connection 对象来执行 for update,以下是某一次的执行日志:
2020-09-10-QPh4M0
得到测试结果,发现如果有 2 个或以上 ID 不同的 connection 对象执行 sql,会发生阻塞,而 Mysql 不会发生阻塞,至于 Mysql 为什么不会发生阻塞,后面我再给大家解释。
而为什么当 druid 的 autoCommit=true 时,Mysql 依然不会阻塞呢?我先开启 Mysql 的日志打印:
2020-09-10-Cn8L1c
查看日志,发现 Mysql 会为每条执行的 sql 设置 autocommit=1,即自动提交事务,无须显式提交 commit,每条 sql 就是一个事务。
3.2 Spring 事务管理器
上面的案例分析中,加了 Spring 事务的并发执行,并不会产生阻塞现象,显然肯定是 Spring 事务做了一些不可描述的动作,Spring 的事务管理器有很多个,这里我们用的是数据库连接池那个管理器,叫 DataSourceTransactionManager,我这里为了灵活控制事务范围的细粒度,用的是声明式事务,我们继续走一波源码,从事务入口一路跟踪进来,发现第一步需要调用 doBegin 方法:
public void forupdateByConcurrent() {
AtomicInteger atomicInteger = new AtomicInteger();
for (int i = 0; i < 100; i++) {
new Thread(() -> {
// select * from forupdate where name = #{name} for update
this.forupdateMapper.findByName("testforupdate");
System.out.println("========ok:" + atomicInteger.getAndIncrement());
}).start();
}
}
private void forupdateByConcurrentAndTransaction() {
AtomicInteger atomicInteger = new AtomicInteger();
for (int i = 0; i < 100; i++) {
new Thread(() -> transactionTemplate.execute(transactionStatus -> {
// select * from forupdate where name = #{name} for update
this.forupdateMapper.findByName("testforupdate");
System.out.println("========ok:" + atomicInteger.getAndIncrement());
return null;
})).start();
}
}
public Transaction newTransaction(DataSource dataSource, TransactionIsolationLevel level, boolean autoCommit) {
return new SpringManagedTransaction(dataSource);
}
if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
// force commit even on non-dirty sessions because some databases require
// a commit/rollback before calling close()
sqlSession.commit(true);
}
public void commit() throws SQLException {
if (this.connection != null && !this.isConnectionTransactional && !this.autoCommit) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Committing JDBC Connection [" + this.connection + "]");
}
this.connection.commit();
}
}
set global general_log = 1;
// Switch to manual commit if necessary. This is very expensive in some JDBC drivers,
// so we don't want to do it unnecessarily (for example if we've explicitly
// configured the connection pool to set it already).
if (con.getAutoCommit()) {
txObject.setMustRestoreAutoCommit(true);
if (logger.isDebugEnabled()) {
logger.debug("Switching JDBC Connection [" + con + "] to manual commit");
}
con.setAutoCommit(false);
}
// Reset connection.
Connection con = txObject.getConnectionHolder().getConnection();
try {
if (txObject.isMustRestoreAutoCommit()) {
con.setAutoCommit(true);
}
DataSourceUtils.resetConnectionAfterTransaction(con, txObject.getPreviousIsolationLevel());
}
catch (Throwable ex) {
logger.debug("Could not reset JDBC Connection after transaction", ex);
}