[tomcat-jdbc]Can‘t call commit when autocommit=true

Can't call commit when autocommit=true

问题

使用tomcat-jdbc时出现Can't call commit when autocommit=true,错误信息如下

log 复制代码
### Error committing transaction.  Cause: java.sql.SQLException: Can't call commit when autocommit=true
### Cause: java.sql.SQLException: Can't call commit when autocommit=true
; uncategorized SQLException for SQL []; SQL state [null]; error code [0]; Can't call commit when autocommit=true; nested exception is java.sql.SQLException: Can't call commit when autocommit=true] with root cause

java.sql.SQLException: Can't call commit when autocommit=true

分析

这个错误的意思是:自动提交的情况下不需要再执行commit操作。通常我们会把autocommit设置为true,这样非事务的操作都是自动提交的,只有开启事务的时候才需要把autocommit设置为false,并在事务结束的时候执行commit/rollback操作,然后再把autocommit设置为true。也就是说执行某个非事务操作完成之后执行了非预期的commit操作才会导致以上的异常,应该有两个检测autocommit的位置状态不一致导致的。

抛出Can't call commit when autocommit=true这个异常的代码在mysql-connector-java-5.1.42.jar的com.mysql.jdbc.ConnectionImpl中,使用mybatis获取数据库连接的org.mybatis.spring.transaction.SpringManagedTransaction.openConnection的地方通过this.connection.getAutoCommit()获取autoCommit的值,并在commit()通过this.connection != null && !this.isConnectionTransactional && !this.autoCommit判断是否需要在数据库连接上执行最终的提交this.connection.commit(),也就是这个地方的状态判断和数据库连接上的状态不一致导致执行了非预期的commit()操作。

通过调试跟踪最终确定是因为org.apache.tomcat.jdbc.pool.interceptor.ConnectionState缓存了状态,并且在异常的情况下和连接上的状态不一致导致,

其大概的逻辑代码(非实际代码)如下

java 复制代码
class ConnectionState {
  boolean getAutoCommit() {
    if (autoCommit == null) {
      autoCommit = connection.getAutoCommit();
    }
    return autoCommit;
  }
  void setAutoCommit(boolean value) {
    connection.setAutoCommit(value);
    this.autoCommit = value;
  }
}

以上代码中只要connection.setAutoCommit出现异常就会导致状态不一致,缓存的逻辑需要调整成

java 复制代码
class ConnectionState {
  void setAutoCommit(boolean value) {
    try {
      connection.setAutoCommit(value);
      this.autoCommit = value;
    } catch (Exception e) {
      this.autoCommit = null; // 异常时清空,以便重新获取最新状态
    }
  }
}

复现

知道了问题的原因,如何复现这个问题呢?

我们只需要在setAutoCommit(true)的时候模拟异常即可,为了更贴近实际应用的情况,需要模拟服务端断开连接,因此需要准备两个账户,一个用于执行业务代码,另一个用于Kill连接来模拟断开连接。

sql 复制代码
CREATE TABLE `test`(
    id INT(11) AUTO_INCREMENT NOT NULL,
    create_time TIMESTAMP,
    update_time TIMESTAMP,
    PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;

CREATE USER 'test-tomcat-jdbc'@'localhost' IDENTIFIED BY 'test';
GRANT ALL ON test.* TO 'test-tomcat-jdbc'@'localhost';

CREATE USER 'root-tomcat-jdbc'@'localhost' IDENTIFIED BY 'test';
GRANT ALL ON *.* TO 'root-tomcat-jdbc'@'localhost';

FLUSH PRIVILEGES;

为了使用ConnectionState需要在业务的数据库连接池的配置jdbc-interceptors中包含ConnectionState,正常来说如果连接被断开之后,连接会被剔除掉(tomcat-jdbc的连接池没有剔除导致一直不可用),因此还需要在数据库连接的url中增加autoReconnect=true确保断开后自动连接避免被剔除。

有了已经配置,运行应用之后请求新增接口之后就会触发Kill。

sh 复制代码
$ curl http://127.0.0.1:8080/test/add

此时会发现日志中有Communications link failure的错误信息,但是后续的检测数据库连接又是正常的,因为autoReconnect=true自动连接成功了。

但是再次请求则会报Can't call commit when autocommit=true的错误了

sh 复制代码
$ curl http://127.0.0.1:8080/test/get
{"timestamp":1690722907640,"status":500,"error":"Internal Server Error","exception":"org.springframework.jdbc.UncategorizedSQLException","message":"\n### Error committing transaction.  Cause: java.sql.SQLException: Can't call commit when autocommit=true\n### Cause: java.sql.SQLException: Can't call commit when autocommit=true\n; uncategorized SQLException for SQL []; SQL state [null]; error code [0]; Can't call commit when autocommit=true; nested exception is java.sql.SQLException: Can't call commit when autocommit=true","path":"/test/get"}

测试时只需要修改数据库的IP和端口即可,并通过配置以下不同的参数模拟断开连接的时机

yaml 复制代码
# 模拟连接到只读库
mock-readonly: false
# 开始事务后检测是否只读时断开连接
kill-condition: session.tx_read_only
# 开始事务并提交后重置自动提交时断开连接
#kill-condition: autocommit=1
#kill-condition: not-kill

结论

暂时不要使用ConnectionState或者使用其他数据库连接池。

复现代码lab-tomcat-jdbc-autocommit

相关推荐
猿究院_夏燊2 小时前
Spring Bean 生命周期
java·后端·spring
Wang's Blog2 小时前
Java 项目实战: 交友项目:登录链路回顾与今日任务地图
java·开发语言·状态模式·交友
信誓旦旦的程序猿3 小时前
【Python量化实战宝典 #08】可转债与债券接口手册:转股价·溢价率·纯债价值(Python 可跑)
java·python·股票数据api·股票数据api接口·股票api数据接口·股票量化数据api·股票量化数据接口
砚底藏山河3 小时前
python量化:行情数据回溯
java·开发语言·python·金融·maven
Jlzn88883 小时前
华东电芯分选通道数怎么配才不堵线
java·人工智能·架构·自动化·制造
香吧香3 小时前
GRPC连接机制---长连接分析
java·网络与传输协议
我是人✓4 小时前
SpringBoot项目的五种创建方式
java·spring boot·后端
Wang's Blog4 小时前
Java 项目实战: 交友项目 - 阿里云 OSS 入门案例:从创建 Bucket 到上传拿到可访问 URL
java·阿里云·交友
裕晟资质规划5 小时前
三类涉密资质延续时限差异分析:军工60日与集成印制3个月的边界
java·服务器·前端·网络·安全
念念清晰5 小时前
000-OpenJDK17 源码构建:MacOS环境(M芯片)
java·后端