原文出自:MySQL中Spring管理的事务开启后不提交引起的事故_adrninistrat0r的博客-CSDN博客_mysql事务长时间不提交
相关文章:
https://www.cnblogs.com/youzhibing/p/16458860.html
Druid connection holder is null未解之谜 - 简书
Cause: java.sql.SQLException: connection closed_YouluBank的博客-CSDN博客
1. 前言
了解到一个事故,在MySQL数据库中,使用Spring管理的事务在开启以后没有在操作结束时提交或回滚,使得原有线程在后续执行数据库操作时可能继续使用原有事务,且不会提交,导致对数据库的修改在Java应用层认为是成功,但在数据库层最终是没有生效的,产生了比较严重的后果
2. 问题分析
2.1. 出现问题的相关代码
transaction.start(); try { // 新增的代码-开始 if(xxx){ // 以下return导致没有执行后续的commit或rollback return; } // 新增的代码-结束 // 执行数据库操作的代码,略 transaction.commit(); } catch (Exception e) { transaction.rollback(); throw e; }
以上的transaction对象是项目中封装的Transaction类,底层通过Spring对事务进行管理
在用于开启事务的start()方法中,会调用Spring的
org.springframework.transaction.PlatformTransactionManager类的getTransaction()方法,开启事务,使用的Spring事务传播行为是REQUIRES_NEW
2.2. 问题根本原因
以上新增的代码会导致事务开启后不提交/回滚,由Spring管理事务时,当前线程的上下文ThreadLocal中会保留当前使用的数据库连接信息
当前线程的当前操作执行完毕后,当前线程会回到线程池中,当前线程后续还会执行其他的操作,由于线程的上下文中已有数据库连接信息,因此后续的处理若不使用事务,或使用默认的Spring事务传播行为,会继续使用原有的连接执行,且该连接对应的事务一直没有提交
当前线程执行的后续数据库操作都在原有的事务中执行,且不会提交,最终事务会回滚
(应用停止时关闭数据库连接,或事务超时后触发),导致相关的数据库操作都没有生效
2.3. 可能产生的影响
在线程中开启事务后未提交/回滚,可能产生以下影响:
后续使用原有线程执行数据库操作时,若不使用事务,或使用事务且使用特定的Spring事务传播行为时,后续的数据库操作执行时会返回成功,但不会提交,最终会回滚,不会生效
线程池的最大线程数通常配置为几百,例如线程池中共有200个线程,有2个线程开启事务后未提交/回滚时,对应实例约1%的交易执行的数据库操作最终可能不会生效
通常大部分的数据库操作是不使用事务的;使用事务执行数据库操作时,也可能使用默认的Spring事务传播行为。在以上情况下,对应的数据库操作最终不会生效
后续使用其他线程执行数据库操作时,可能出现无法获取可用数据库连接的问题,导致数据库操作无法执行
2.4. 相关组件及版本
组件 | 版本 |
spring | 5.3.20 |
mybatis | 3.5.9 |
mybatis-spring | 2.0.6 |
druid | 1.2.8 |
mysql-connector-java | 8.0.27 |
2.5. 事务未提交/回滚的直接影响
开启事务后未提交/回滚,假如当前事务有执行sql语句,则不会被立即提交/回滚
同时,当前事务对应的数据库连接会被当前线程占用,其他线程无法再获取到
2.6. 事务未提交/回滚的后续影响
开启事务后未提交/回滚,对于后续其他线程的影响,需要分情况进行分析
2.6.1. 后续使用原有线程、不使用事务
开启事务后未提交/回滚,假如后续使用线程池中原有的线程
,执行数据库操作时不使用事务
,情况如下:
- 分析
后续使用线程池中原有的线程进行操作时,执行数据库操作的过程中,由于ThreadLocal仍然记录有对应的数据库连接信息,因此会继续使用原有的数据库连接执行数据库操作
由于原有的数据库连接已开启事务,不会自动提交,因此后续不使用事务的数据库操作也不会被自动提交
由于后续数据库操作不使用事务,因此也不会执行commit/rollback语句。后续数据库操作,以及原有线程对应的数据库连接事务中累积的数据库操作,都不会被立即提交或回滚
结论
后续使用原有线程、不使用事务时,新执行的数据库操作,及原有事务中的数据库操作,都不会被提交或回滚
2.6.2. 后续使用原有线程、使用事务
开启事务后未提交/回滚,假如后续使用线程池中原有的线程,执行数据库操作时使用事务,情况如下:
分析
后续使用线程池中原有的线程进行操作时,执行数据库操作的过程中,由于ThreadLocal仍然记录有对应的数据库连接信息,会进行已经存在事务情况下的处理,需要根据新事务的Spring事务传播行为决定对应的操作:
- 结论
后续使用原有线程、使用事务时,无论新事务使用哪种Spring事务传播行为,原有事务均不会提交
若新事务使用的事务传播行为是REQUIRED、SUPPORTS、MANDATORY、NESTED、NEVER
,则新的事务也不会提交
若新事务使用的事务传播行为是REQUIRES_NEW、NOT_SUPPORTED
,则新的事务(或不使用事务)成功获取到连接时可以提交
2.6.3. 后续使用其他线程
开启事务后未提交/回滚,假如后续使用线程池中其他线程(开启事务后有执行提交/回滚操作),执行数据库操作时无论是否使用事务,情况如下:
后续使用线程池中其他线程进行操作时,执行数据库操作的过程中,由于ThreadLocal中没有对应的数据库连接信息,因此会从数据源获取可用的连接,假如能够获取到可用的连接,则可以正常执行数据库操作
当有线程开启事务后未提交/回滚时,数据库连接会被相关的线程占用,不会归还到数据库连接池,也无法被其他线程获取到。由于线程池中配置的线程数通常比数据库连接池的连接数大,当线程开启事务后未提交/回滚发生次数超过数据源连接池最大允许连接数时,会导致所有的连接都被相关线程占用,其他线程无法获取到可用连接,无法执行数据库操作
3.1.1. MySQL事务相关内容说明
在MySQL服务中,由连接管理线程处理客户端连接请求,每个客户端连接都会关联到一个MySQL服务线程,每个客户端提交的SQL语句都在对应的MySQL服务线程中执行。
默认情况下,MySQL的自动提交模式是启用的,即不使用事务时,每条SQL语句是原子的,执行后就会生效,就好像SQL语句被包含在START TRANSACTION与COMMIT中执行一样。不使用事务时,无法使用ROLLBACK撤销SQL语句的执行结果。当SQL语句执行过程中出现异常时,才会被回滚。
使用START TRANSACTION可以隐示地禁用自动提交,执行START TRANSACTION后,在执行COMMIT或ROLLBACK结束事务之前,自动提交都会保持禁用;结束事务之后,自动提交模式会恢复为之前的状态。
使用SET autocommit=0;语句可以显式地禁用自动提交,autocommit是一个会话变量,必须对每个会话进行设置。
在InnoDB存储引擎中,所有的用户行为都在一个事务中发生,假如启用了自动提交模式,则每条SQL语句都会形成一个独立的事务。
在MySQL服务中,thd代表线程结构,MySQL服务将事务相关的数据保存在thd->transaction结构中。
当新的连接建立时,thd->transaction中的成员变量会初始化为空状态。假如SQL语句中使用了某个数据库表,则相关的存储引擎都会记录下来。
在SQL语句执行结束时,MySQL服务调用所有相关存储引擎的提交或回滚;当提交或回滚结束后,以上信息会被清空。
对于每个客户端连接,MySQL服务创建一个单独的线程,使用THD类作为线程/连接的描述符。
MySQL服务需要调用存储引擎的start_stmt()或external_lock(),以使存储引擎开始事务。
存储引擎在每个连接的内存中保存了事务信息,也在MySQL服务中注册了事务信息,以使MySQL服务随后能够发起COMMIT或ROLLBACK操作。
在事务需要结束时,MySQL服务会调用存储引擎的commit()或rollback()方法。
MySQL事务在执行时,需要MySQL客户端连接上MySQL服务器,客户端与服务器都知道当前事务的存在,此时在客户端与服务器都存在一个对应的连接,连接在事务执行期间是独占的;在服务器中还存在一个对应的线程,线程也是事务执行期间独占的。
连接ID与线程ID是相同的,因此可以认为,在某个时间点,连接ID(线程ID)可以唯一确定一个事务。
MySQL客户端与服务器通信使用TCP/IP协议(大部分使用场景下),根据RFC793,“TRANSMISSION CONTROL PROTOCOL”RFC 793 - Transmission Control Protocol,在TCP连接中,IP地址加端口形成的套接字在连接中是唯一的。
MySQL事务在执行时,客户端IP、服务器IP、服务器端口是固定的,因此可以认为,在某个时间点,客户端端口可以唯一确定一个连接,也就是可以唯一确定一个事务。
3.1.2. MySQL事务总结
在MySQL中,为了使用事务执行sql语句,步骤如下:
MySQL客户端首先需要与MySQL服务器建立TCP连接,MySQL服务器使用单独的线程处理当前事务;
在当前连接中执行“set autocommit=0;”语句,将对应的会话级系统变量由默认值1修改为0,即关闭自动提交,以开启事务;
后续执行sql语句时,需要在同一个连接中执行,可以执行一条或多条sql语句;
所有sql语句执行完毕后,再执行commit/rollback语句,以提交或回滚事务;
最后需要执行“set autocommit=1;”语句,将自动提交恢复为默认值开启。
以上所有的操作都需要在同一个数据库连接中执行,事务才能够生效
在事务执行的过程中,MySQL服务器是使用一个单独的线程执行的,在此期间这个线程被当前事务独占,不会被其他线程使用,这样才能保证不同事务之间的操作不会相互影响
MySQL服务器使用一个线程执行对应的事务,在MySQL客户端也有维护对应事务,双方通信时使用TCP连接,一个事务对应一个TCP连接,因此通过MySQL服务器的线程ID、连接ID,或者MySQL客户端连接的客户端端口,可以确定当前sql语句在哪个事务中执行
3.2. 不使用事务执行SQL语句的过程
3.2.1. 过程分析
不使用事务执行SQL语句,每次执行SQL语句时的大致阶段如下:
从连接池借出连接(可能需要创建新连接) 执行SQL语句 归还连接至连接池
如下图所示:
不使用事务执行SQL语句时,主要由MyBatis完成,与Spring关系不大。
3.3. 使用事务执行SQL语句的过程
3.3.1. 过程分析
在使用事务执行SQL语句时,每次执行SQL语句时的大致阶段如下
从连接池借出连接(可能需要创建新连接) 关闭自动提交 执行SQL语句1 执行SQL语句n 提交/回滚事务 开启自动提交 归还连接至连接池
如下图所示:
使用事务执行SQL语句时,事务管理主要通过Spring完成,SQL语句执行主要通过MyBatis完成
使用事务执行数据库操作时的步骤及作用如下:
首先需要从连接池借出连接
再关闭当前连接的自动提交标志,以使事务开启
之后执行对应的sql语句,可能有一条或多条
在此之后根据需要对事务进行提交或回滚
无论是对事务执行了提交还是回滚,都需要开启当前连接自动提交标志,使当前连接归还到连接池之前恢复默认的自动提交标志(默认自动提交,即不使用事务)
完成以上操作之后,再将连接归还到连接池,在归还之前,对应的连接是被Java应用相关线程独占的,其他线程无法使用(保证不同的线程、事务、连接之间的数据库操作不会相互影响)
3.3.2. 记录ThreadLocal的时间点
Spring在开启事务时,会调用AbstractPlatformTransactionManager.getTransaction()方法(使用@Transactional注解或TransactionTemplate时都会调用),在TransactionSynchronizationManager.bindResource()方法中会在ThreadLocal中记录当前线程对应的连接信息,对应的调用堆栈如下:
org.springframework.transaction.support.AbstractPlatformTransactionManager.getTransaction(AbstractPlatformTransactionManager:373) org.springframework.transaction.support.AbstractPlatformTransactionManager.startTransaction(AbstractPlatformTransactionManager:400) org.springframework.jdbc.datasource.DataSourceTransactionManager.doBegin(DataSourceTransactionManager:300) org.springframework.transaction.support.TransactionSynchronizationManager.bindResource(TransactionSynchronizationManager:168)
TransactionSynchronizationManager.bindResource()方法中相关代码如下:
Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key); Assert.notNull(value, "Value must not be null"); Map<Object, Object> map = resources.get(); // set ThreadLocal Map if none found if (map == null) { map = new HashMap<>(); resources.set(map); } Object oldValue = map.put(actualKey, value);
3.3.3. 清理ThreadLocal的时间点
Spring在提交及清理事务时,均会清理ThreadLocal中的连接信息
提交事务时清理ThreadLocal的调用堆栈如下:
org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager:711) org.springframework.transaction.support.AbstractPlatformTransactionManager.processCommit(AbstractPlatformTransactionManager:790) org.springframework.transaction.support.AbstractPlatformTransactionManager.cleanupAfterCompletion(AbstractPlatformTransactionManager:992) org.springframework.jdbc.datasource.DataSourceTransactionManager.doCleanupAfterCompletion(DataSourceTransactionManager:371) org.springframework.transaction.support.TransactionSynchronizationManager.unbindResource(TransactionSynchronizationManager:197)
回滚事务时清理ThreadLocal的调用堆栈如下:
org.springframework.transaction.support.AbstractPlatformTransactionManager.rollback(AbstractPlatformTransactionManager:809) org.springframework.transaction.support.AbstractPlatformTransactionManager.processRollback(AbstractPlatformTransactionManager:875) org.springframework.transaction.support.AbstractPlatformTransactionManager.cleanupAfterCompletion(AbstractPlatformTransactionManager:992) org.springframework.jdbc.datasource.DataSourceTransactionManager.doCleanupAfterCompletion(DataSourceTransactionManager:371) org.springframework.transaction.support.TransactionSynchronizationManager.unbindResource(TransactionSynchronizationManager:195)
可以看到,提交及回滚事务时,均是调用TransactionSynchronizationManager.unbindResource()方法清理ThreadLocal中的连接信息
在TransactionSynchronizationManager.unbindResource()方法中,会调用doUnbindResource()方法,对应代码如下:
Map<Object, Object> map = resources.get(); if (map == null) { return null; } Object value = map.remove(actualKey); // Remove entire ThreadLocal if empty... if (map.isEmpty()) { resources.remove(); }