[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.jarcom.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

相关推荐
考虑考虑34 分钟前
JDK9中的dropWhile
java·后端·java ee
想躺平的咸鱼干42 分钟前
Volatile解决指令重排和单例模式
java·开发语言·单例模式·线程·并发编程
hqxstudying1 小时前
java依赖注入方法
java·spring·log4j·ioc·依赖
·云扬·1 小时前
【Java源码阅读系列37】深度解读Java BufferedReader 源码
java·开发语言
Bug退退退1232 小时前
RabbitMQ 高级特性之重试机制
java·分布式·spring·rabbitmq
小皮侠2 小时前
nginx的使用
java·运维·服务器·前端·git·nginx·github
Zz_waiting.3 小时前
Javaweb - 10.4 ServletConfig 和 ServletContext
java·开发语言·前端·servlet·servletconfig·servletcontext·域对象
全栈凯哥3 小时前
02.SpringBoot常用Utils工具类详解
java·spring boot·后端
兮动人3 小时前
获取终端外网IP地址
java·网络·网络协议·tcp/ip·获取终端外网ip地址
呆呆的小鳄鱼3 小时前
cin,cin.get()等异同点[面试题系列]
java·算法·面试