1. 前言
了解到一个事故,在MySQL数据库中,使用Spring管理的事务在开启以后没有在操作结束时提交或回滚,使得原有线程在后续执行数据库操作时可能继续使用原有事务,且不会提交,导致对数据库的修改在Java应用层认为是成功,但在数据库层最终是没有生效的,产生了比较严重的后果
与“数据源使用错误导致MySQL事务失效分析”https://blog.csdn.net/a82514921/article/details/126563573的原因类似,但更加复杂
以下对这个问题进行了分析,涉及的技术背景比较多,由几方面的原因导致了最后的事故
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 | 支持现有的事务,若不存在则创建新事务 (默认的Spring事务传播行为) | 使用原有连接 | 新事务及原有事务均不会提交 |
SUPPORTS | 支持现有的事务,若不存在则不创建新事务 | 使用原有连接 | 新事务及原有事务均不会提交 |
MANDATORY | 支持现有的事务,若不存在事务则抛出异常 | 使用原有连接 | 新事务及原有事务均不会提交 |
REQUIRES_NEW | 创建新的事务,若已存在事务则将其暂停 | 使用新的连接 | 1. 新的事务会使用新的连接执行数据库操作,若能成功获取到连接则可以正常执行 2. 原有事务不会提交 |
NOT_SUPPORTED | 不支持现有的事务,若存在则以不开启事务方式执行 | 使用新的连接 | 1. 新的数据库操作会使用新的连接执行,若能成功获取到连接则可以正常执行 原有事务不会提交 |
NEVER | 不支持现有的事务,若存在则抛出异常 | 会抛出异常,因此不需要考虑 | 新的数据库操作不会执行,会抛出异常 原有事务不会提交 |
NESTED | 假如现有存在事务,则在嵌套的事务中执行 行为与REQUIRED类似 | 使用原有连接 | 新事务及原有事务均不会提交 |
- 结论
后续使用原有线程、使用事务时,无论新事务使用哪种Spring事务传播行为,原有事务均不会提交
若新事务使用的事务传播行为是REQUIRED、SUPPORTS、MANDATORY、NESTED、NEVER
,则新的事务也不会提交
若新事务使用的事务传播行为是REQUIRES_NEW、NOT_SUPPORTED
,则新的事务(或不使用事务)成功获取到连接时可以提交
2.6.3. 后续使用其他线程
开启事务后未提交/回滚,假如后续使用线程池中其他线程
(开启事务后有执行提交/回滚操作),执行数据库操作时无论是否使用事务,情况如下:
后续使用线程池中其他线程进行操作时,执行数据库操作的过程中,由于ThreadLocal中没有对应的数据库连接信息,因此会从数据源获取可用的连接,假如能够获取到可用的连接,则可以正常执行数据库操作
当有线程开启事务后未提交/回滚时,数据库连接会被相关的线程占用,不会归还到数据库连接池,也无法被其他线程获取到。由于线程池中配置的线程数通常比数据库连接池的连接数大,当线程开启事务后未提交/回滚发生次数超过数据源连接池最大允许连接数时,会导致所有的连接都被相关线程占用,其他线程无法获取到可用连接,无法执行数据库操作
3. 事务相关的技术背景
3.1. MySQL事务能够提交成功的前提
在Java应用中访问MySQL服务时,涉及的内容如下图所示:
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”https://datatracker.ietf.org/doc/html/rfc793,在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. 过程分析
详细内容可参考“Spring、MyBatis、Druid、MySQL不使用事务执行SQL语句分析” https://blog.csdn.net/a82514921/article/details/126563515
不使用事务执行SQL语句,每次执行SQL语句时的大致阶段如下:
从连接池借出连接(可能需要创建新连接)执行SQL语句归还连接至连接池
如下图所示:
不使用事务执行SQL语句时,主要由MyBatis完成,与Spring关系不大。
3.3. 使用事务执行SQL语句的过程
3.3.1. 过程分析
详细内容可参考“Spring、MyBatis、Druid、MySQL使用事务执行SQL语句分析” https://blog.csdn.net/a82514921/article/details/126563542
在使用事务执行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()方法中会向ThreadLocal类型的resources字段中记录指定的数据,resources字段及相关代码如下:
private static final ThreadLocal<Map<Object, Object>> resources = new NamedThreadLocal<>("Transactional resources");Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key);Assert.notNull(value, "Value must not be null");Map<Object, Object> map = resources.get();// set ThreadLocal Map if none foundif (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();}
3.4. MySQL事务隐式提交的场景
参考“Transaction Life Cycle”https://dev.mysql.com/doc/internals/en/transactions-life-cycle.html。
在以下情况下,事务会被提交:
-
当用户执行COMMIT语句时;
-
MySQL事务隐式提交,当MySQL服务开始处理DDL或SET AUTOCOMMIT={0|1}语句时。
关于会导致事务隐式提交的SQL语句,在“Statements That Cause an Implicit Commit”https://dev.mysql.com/doc/refman/5.6/en/implicit-commit.html中有详细说明,包括DDL等。
在以上事务开启后未提交/回滚的场景下,对应的连接不会被其他线程再获取到,因此也不会触发MySQL事务隐式提交
3.5. 事务隐式回滚的场景
3.5.1. MySQL事务隐式回滚
除了在MySQL客户端执行ROLLBACK语句进行显式回滚外,以下情况下MySQL服务也会进行隐式回滚。
3.5.1.1. 连接断开与事务回滚
参考“LOCK TABLES and UNLOCK TABLES Statements”https://dev.mysql.com/doc/refman/5.6/en/lock-tables.html。
当一个客户端会话的连接断开时,假如存在活动的事务,则MySQL服务会将事务回滚。
3.5.1.2. 连接超时与事务回滚
参考https://dev.mysql.com/doc/refman/5.6/en/server-system-variables.html。
MySQL系统变量wait_timeout用于设置MySQL服务在关闭非交互式连接之前等待其活动的时间,单位为秒。默认值为28800,即8小时。可能需要同时修改全局系统变量wait_timeout与interactive_timeout,才能使会话系统wait_timeout的修改生效。
即MySQL连接不活动超过该时间后,MySQL服务会将该连接断开,对应的事务也会被回滚。
3.5.1.3. 行锁超时与事务回滚
参考“InnoDB Error Handling”https://dev.mysql.com/doc/refman/5.6/en/innodb-error-handling.html,https://dev.mysql.com/doc/refman/5.6/en/innodb-parameters.html。
InnoDB在等待获取行锁时,假如超过了一定的时间,则会进行回滚。
系统变量innodb_lock_wait_timeout用于设置以上获取行锁超时时间,单位为秒,默认为50。
当系统变量innodb_rollback_on_timeout为假时,InnoDB会将当前语句(即等待行锁并导致超时的语句)进行回滚(此时整个事务还是活动状态);当该系统变量为真时,InnoDB会将整个事务进行回滚。
该变量值默认值为假,即默认情况下,事务中执行的语句获取行锁超时,对应语句会被InnoDB回滚。
3.5.1.4. 死锁与事务回滚
参考“Deadlock Detection”https://dev.mysql.com/doc/refman/5.6/en/innodb-deadlock-detection.html,“InnoDB Error Handling”https://dev.mysql.com/doc/refman/5.6/en/innodb-error-handling.html。
InnoDB会自动检测事务死锁,当出现死锁时,会将一个或多个事务回滚以打破死锁。InnoDB尝试将“小”的事务回滚,事务的大小由插入、更新或删除的行数决定。
3.5.1.5. InnoDB其他错误与事务回滚
参考“InnoDB Error Handling”https://dev.mysql.com/doc/refman/5.6/en/innodb-error-handling.html。
假如在SQL语句中没有指定IGNORE,则出现重复键错误时,InnoDB会将对应的SQL语句回滚;
出现行太长错误时,InnoDB会将对应的SQL语句回滚;
其他错误大多由InnoDB存储引擎层之上的MySQL服务层检测,并将对应的SQL语句回滚。
3.5.2. Druid事务隐式回滚
MySQL connector中,用于回滚事务的方法为com.mysql.cj.jdbc.ConnectionImpl类,rollback()方法
在Druid中,com.alibaba.druid.pool.DruidPooledConnection类,rollback()方法,会调用以上方法,即调用Druid的回滚事务方法时,Druid会执行MySQL connector中回滚事务的方法
除此之外,com.alibaba.druid.pool.DruidDataSource类,recycle()方法中,也会调用以上方法,相关代码如下:
final boolean isAutoCommit = holder.underlyingAutoCommit;final boolean isReadOnly = holder.underlyingReadOnly;try { // check need to rollback? if ((!isAutoCommit) && (!isReadOnly)) { pooledConnection.rollback(); }
recycle()方法在归还数据库连接至连接池时会被执行,isAutoCommit代表当前连接是否启用autocommit(未开启事务),isReadOnly代表数据库是否只读
以上执行回滚的场景,是归还数据库连接至Druid连接池时,假如有开启事务,且数据库非只读,则会执行事务回滚操作
3.6. 分析不同线程是否使用相同数据库连接的方式
在Java应用中访问MySQL服务时,涉及Java应用、网络传输、MySQL服务这三层,在每一层都可以对执行的SQL语句与事务操作进行监控与观测
3.6.1. 数据库层
可在MySQL数据库中通过一般查询日志分析对应的SQL使用哪个线程/连接执行
可参考“MySQL SQL语句与事务执行及日志分析”https://blog.csdn.net/a82514921/article/details/126563449
需要DBA开启对应的日志,且一般查询日志的数据量太大,因此该方法不可行
3.6.2. 网络层
可对Java应用与MySQL服务器之间的数据进行抓包分析,检查执行SQL语句时使用的本地端口(可反映对应哪个连接)
可参考“tcpdump、Wireshark抓包分析MySQL SQL语句与事务执行”https://blog.csdn.net/a82514921/article/details/126563471
需要有服务器root权限才能执行相关命令,执行不方便
3.6.3. Java应用层
在Java应用层分析执行SQL语句时使用的连接是比较方便的
可参考“Spring、MyBatis、Druid、MySQL执行SQL语句与事务监控” https://blog.csdn.net/a82514921/article/details/126563558
为了分析事务开启后未提交/回滚时,相关线程后续是否使用原有连接继续执行sql语句,可以通过以下方式实现:
- 观察数据库操作各个重要节点的情况
使用Druid提供的Filter:stat、log4j2,内容略
- 确认使用事务执行sql语句时是否使用新连接
在spring-jdbc的org.springframework.jdbc.datasource.DataSourceTransactionManager类中,doBegin()方法会执行开启事务的操作
在以上方法中,若当前事务没有已存在的数据库连接,需要从数据库连接池中获取连接时,会在日志中打印DEBUG级别的日志“Acquired Connection … for JDBC transaction”
- 确认不使用事务执行sql语句时是否使用新连接
在spring-jdbc的org.springframework.jdbc.datasource.DataSourceUtils类中,doGetConnection()方法会执行获取数据库连接的操作
在以上方法中,若没有使用当前线程ThreadLocal中对应的数据库连接,而是从数据库连接池中获取连接时,会在日志中打印DEBUG级别的日志“Fetching JDBC Connection from DataSource”
- 确认现有事务是否被暂停或恢复
当前线程已存在对应的事务时,使用新事务(REQUIRES_NEW
),或不使用事务(NOT_SUPPORTED
)执行sql语句时,会对现有事务执行暂停及恢复,可通过以下方式观察日志
在spring-tx的org.springframework.transaction.support.AbstractPlatformTransactionManager类中,handleExistingTransaction()方法用于处理已存在的事务
在以上方法中,若需要暂停当前线程,会在日志中打印DEBUG级别的日志“Suspending current transaction”
cleanupAfterCompletion()方法用于在事务提交/回滚后进行清理操作
在以上方法中,若发现之前的事务被暂停后需要恢复,会在日志中打印DEBUG级别的日志“Resuming suspended transaction after completion of inner transaction”
3.7. Spring提供的主要的事务使用方式
- @Transactional注解
属于声明式事务,某些场景下无法实现编程式事务的效果
- 事务模板
属于编程式事务,主要是使用TransactionTemplate类
- 事务管理器
属于编程式事务,主要是使用PlatformTransactionManager接口的实例
以上接口提供了三个方法,分别用于开启事务、提交事务、回滚事务,由于开启事务与提交/回滚事务是需要分别调用的,因此有可能出现漏调用的情况
4. 细节分析
4.1. 为什么不使用事务时会使用ThreadLocal中的数据库连接
开启事务后未提交/回滚,后续使用线程池中原有的线程,执行数据库操作时不使用事务,会使用原有的连接,原因如下:
不使用事务执行数据库操作时,会调用org.springframework.jdbc.datasource.DataSourceUtils类的doGetConnection()方法获取连接,从MyBatis的Mapper接口对应类开始,到以上方法的调用堆栈如下:
com.sun.proxy.$Proxy37.updateByPrimaryKeySelective(Unknown Source)org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy:86)org.apache.ibatis.binding.MapperProxy$PlainMethodInvoker.invoke(MapperProxy:145)org.apache.ibatis.binding.MapperMethod.execute(MapperMethod:67)org.mybatis.spring.SqlSessionTemplate.update(SqlSessionTemplate:288)com.sun.proxy.$Proxy35.update(Unknown Source)org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate:427)java.lang.reflect.Method.invoke(Method:498)sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl:43)sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl:62)sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)org.apache.ibatis.session.defaults.DefaultSqlSession.update(DefaultSqlSession:194)org.apache.ibatis.executor.CachingExecutor.update(CachingExecutor:76)org.apache.ibatis.executor.BaseExecutor.update(BaseExecutor:117)org.apache.ibatis.executor.SimpleExecutor.doUpdate(SimpleExecutor:49)org.apache.ibatis.executor.SimpleExecutor.prepareStatement(SimpleExecutor:86)org.apache.ibatis.executor.BaseExecutor.getConnection(BaseExecutor:337)org.mybatis.spring.transaction.SpringManagedTransaction.getConnection(SpringManagedTransaction:67)org.mybatis.spring.transaction.SpringManagedTransaction.openConnection(SpringManagedTransaction:80)org.springframework.jdbc.datasource.DataSourceUtils.getConnection(DataSourceUtils:80)org.springframework.jdbc.datasource.DataSourceUtils.doGetConnection(DataSourceUtils:112)
DataSourceUtils.doGetConnection()方法的相关代码如下:
ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);if (conHolder != null && (conHolder.hasConnection() || conHolder.isSynchronizedWithTransaction())) { conHolder.requested(); if (!conHolder.hasConnection()) { logger.debug("Fetching resumed JDBC Connection from DataSource"); conHolder.setConnection(fetchConnection(dataSource)); } return conHolder.getConnection();}
在调用TransactionSynchronizationManager.getResource()方法获取ConnectionHolder类型的对象conHolder后,会判断conHolder是否满足非空,且hasConnection()或isSynchronizedWithTransaction()方法返回值为真,若满足则返回conHolder.getConnection()方法的返回值,即使用以上获取的连接
在org.springframework.transaction.support.TransactionSynchronizationManager类的getResource()方法中,会调用doGetResource()方法,在该方法中会从ThreadLocal类型的resources字段中获取对应的连接对象,resources字段及相关代码如下:
private static final ThreadLocal<Map<Object, Object>> resources = new NamedThreadLocal<>("Transactional resources");...Map<Object, Object> map = resources.get();if (map == null) { return null;}Object value = map.get(actualKey);// Transparently remove ResourceHolder that was marked as void...if (value instanceof ResourceHolder && ((ResourceHolder) value).isVoid()) { map.remove(actualKey); // Remove entire ThreadLocal if empty... if (map.isEmpty()) { resources.remove(); } value = null;}return value;
通过以上代码可知,不使用事务执行数据库操作时,若当前线程的ThreadLocal中的resources存在对应的连接时,会使用对应的连接执行数据库操作
当使用事务执行数据库操作时,会在ThreadLocal中设置以上信息,具体过程见后续内容
4.2. 为什么事务不提交/回滚时ThreadLocal会保持
在事务执行的正常流程中,开启事务时在ThreadLocal记录对应的连接信息,提交/回滚事务时进行清理
若开启事务后不执行提交/回滚事务的操作,则ThreadLocal中的连接信息会保持,直到对应的线程被线程池回收,或应用退出时被清理
4.3. 为什么使用事务时可能使用ThreadLocal中的数据库连接
4.3.1. 开启事务获取数据库连接阶段
org.springframework.transaction.support.AbstractPlatformTransactionManager类的getTransaction()方法用于开启事务,在该方法中,首先调用doGetTransaction()方法获取当前存在的事务
项目中实际使用的事务管理器类型为子类org.springframework.jdbc.datasource.DataSourceTransactionManager,该类的doGetTransaction()方法代码如下:
DataSourceTransactionObject txObject = new DataSourceTransactionObject();txObject.setSavepointAllowed(isNestedTransactionAllowed());ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(obtainDataSource());txObject.setConnectionHolder(conHolder, false);return txObject;
以上方法调用了TransactionSynchronizationManager.getResource()方法,获取ThreadLocal中的数据库连接(前文有分析)
后续会根据Spring事务传播行为进行处理,部分事务传播行为会使用现有的事务执行后续的数据库操作
4.3.2. 执行sql语句阶段
与前文“为什么不使用事务时会使用ThreadLocal中的数据库连接”原因相同,略
4.4. 为什么不同Spring事务传播行为使用的连接不同
AbstractPlatformTransactionManager类用于开启事务的getTransaction()方法中,判断是否已存在事务及相关的处理代码如下:
if (isExistingTransaction(transaction)) { // Existing transaction found -> check propagation behavior to find out how to behave. return handleExistingTransaction(def, transaction, debugEnabled);}
isExistingTransaction()方法用于判断当前是否已存在事务,项目中实际使用的事务管理器类型为子类org.springframework.jdbc.datasource.DataSourceTransactionManager,该类的isExistingTransaction()方法代码如下:
DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;return (txObject.hasConnectionHolder() && txObject.getConnectionHolder().isTransactionActive());
开启事务后未提交/回滚事务,后续使用原有线程执行时,以上txObject对象的hasConnectionHolder()及getConnectionHolder().isTransactionActive()方法均返回真,因此会执行handleExistingTransaction()方法
handleExistingTransaction()方法用于在存在事务时进行处理,定义在AbstractPlatformTransactionManager类中,部分代码如下,可以看到有根据事务传播行为进行对应的处理
if (definition.getPropagationBehavior() == TransactionDefinition.NEVER) { throw new IllegalTransactionStateException( "Existing transaction found for transaction marked with propagation 'never'");}if (definition.getPropagationBehavior() == TransactionDefinition.NOT_SUPPORTED) { return prepareTransactionStatus( definition, null, false, newSynchronization, debugEnabled, suspendedResources);}if (definition.getPropagationBehavior() == TransactionDefinition.REQUIRES_NEW) { return startTransaction(definition, transaction, debugEnabled, suspendedResources);}if (definition.getPropagationBehavior() == TransactionDefinition.NESTED) { if (useSavepointForNestedTransaction()) { DefaultTransactionStatus status = prepareTransactionStatus(definition, transaction, false, false, debugEnabled, null); status.createAndHoldSavepoint(); return status; } else { return startTransaction(definition, transaction, debugEnabled, null); }}...return prepareTransactionStatus(definition, transaction, false, newSynchronization, debugEnabled, null);
部分事务传播行为会调用startTransaction()方法时,会创建新的事务,即需要从数据库连接池获取可用的连接
部分事务传播行为会调用prepareTransactionStatus()方法时,若传入的参数2 transaction为null,则代表不使用事务执行数据库操作
;若非null,则代表使用指定的当前事务执行数据库操作
4.5. 为什么部分Spring事务传播行为使用原有连接时不会提交事务
若开启事务后不执行提交/回滚事务的操作,后续使用原有线程执行,新事务使用的事务传播行为是REQUIRED、SUPPORTS、MANDATORY、NESTED时,新的事务也不会提交,原因如下:
AbstractPlatformTransactionManager.commit()方法用于提交事务,该方法会调用processCommit()方法
在processCommit()方法中,仅当当前事务为新事务时,才会执行实际提交事务的doCommit()方法,相关代码如下:
else if (status.isNewTransaction()) { if (status.isDebug()) { logger.debug("Initiating transaction commit"); } unexpectedRollback = status.isGlobalRollbackOnly(); doCommit(status);}
以上isNewTransaction()方法在org.springframework.transaction.support.DefaultTransactionStatus类中,当其transaction字段非null,且newTransaction字段为真时,isNewTransaction()方法会返回真
DefaultTransactionStatus的newTransaction字段只能在构造函数中赋值
AbstractPlatformTransactionManager.newTransactionStatus()方法中会创建DefaultTransactionStatus对象
以上方法只有以下两种调用情况:
- 在startTransaction()方法中调用newTransactionStatus()方法,传入的参数3 newTransaction为true
- 在prepareTransactionStatus()方法中调用newTransactionStatus()方法,传入的参数3 newTransaction为prepareTransactionStatus()方法的参数3newTransaction
根据上一部分拷贝的Spring代码可知,当Spring事务传播行为是REQUIRES_NEW时,会调用startTransaction()方法,即newTransaction为true,最终可以提交事务
Spring事务传播行为是其他值时,会调用prepareTransactionStatus()方法,且参数3 newTransaction为false,最终不会提交事务
4.6. 为什么使用事务传播行为REQUIRES_NEW之后ThreadLocal中的连接不会被修改
4.6.1. 假设
事务开启后不提交/回滚,对应线程的ThreadLocal中就保留了对应的数据库连接信息
原有线程后续使用事务执行数据库操作,Spring事务传播行为使用REQUIRES_NEW,假如使用新的事务执行数据库操作后,会将ThreadLocal中的数据库连接信息清空,则当前线程就能够恢复正常,之后执行的数据库操作能够正常提交
(不能将ThreadLocal中保留为新事务对应的数据库连接信息,否则还是有类似的问题)
4.6.2. 分析
以上假设不满足,不符合Spring事务传播行为的设计
Spring的TransactionDefinition类中对REQUIRES_NEW事务传播行为的说明如下:
Create a new transaction, suspending the current transaction if one exists.
使用REQUIRES_NEW事务传播行为时,会创建新的事务,假如存在当前事务则暂停当前事务,当前事务还会在随后被恢复
- 暂停当前事务
AbstractPlatformTransactionManager.handleExistingTransaction()用于对现有事务进行处理,若事务传播行为是NOT_SUPPORTED、REQUIRES_NEW时,会调用suspend()方法暂停当前事务,相关代码如下:
if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NOT_SUPPORTED) { if (debugEnabled) { logger.debug("Suspending current transaction"); } Object suspendedResources = suspend(transaction);}if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW) { if (debugEnabled) { logger.debug("Suspending current transaction, creating new transaction with name [" + definition.getName() + "]"); } SuspendedResourcesHolder suspendedResources = suspend(transaction);}
suspend()方法中调用doSuspend()方法执行暂停当前事务的操作,并将当前事务的相关资源保存在suspendedResources对象中,相关代码如下:
Object suspendedResources = doSuspend(transaction);return new SuspendedResourcesHolder(suspendedResources);
以上方法调用的是子类org.springframework.jdbc.datasource.DataSourceTransactionManager的doSuspend()方法,该方法中会调用TransactionSynchronizationManager.unbindResource()方法将ThreadLocal中当前事务的数据库连接信息清空
,相关代码如下:
DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;txObject.setConnectionHolder(null);return TransactionSynchronizationManager.unbindResource(obtainDataSource());
执行到此后,当前线程ThreadLocal中的连接信息已被清空
后续会将新创建事务的连接信息记录在ThreadLocal中
- 恢复当前事务
AbstractPlatformTransactionManager类中,执行提交事务的方法为processCommit(),执行回滚事务的方法为processRollback(),以上两个方法都会在最后finally中调用cleanupAfterCompletion()方法,在事务结束后执行清理操作
在cleanupAfterCompletion()方法中,假如DefaultTransactionStatus类型的status对象的getSuspendedResources()方法返回值非空,即存在被暂停的事务相关资源时,会调用resume()方法对被暂停的事务进行恢复,相关代码如下:
if (status.getSuspendedResources() != null) { if (status.isDebug()) { logger.debug("Resuming suspended transaction after completion of inner transaction"); } Object transaction = (status.hasTransaction() ? status.getTransaction() : null); resume(transaction, (SuspendedResourcesHolder) status.getSuspendedResources());}
在resume()方法中会调用doResume()方法,对应子类DataSourceTransactionManager的doResume()方法,在该方法中会调用TransactionSynchronizationManager.bindResource()方法,将原有事务的数据库连接信息恢复到ThreadLocal中
4.7. 为什么Druid数据源中活跃连接的事务不能被其他线程提交
4.7.1. 假设
假如Druid数据源会将活跃连接放回连接池中,则后续有其他线程获取到原有的数据库连接时,可以将之前的会话提交
4.7.2. 分析
以上假设不成立,原因如下
- Druid不会自动提交事务
MySQL connector中,用于提交事务的方法为com.mysql.cj.jdbc.ConnectionImpl类,commit()方法
以上方法在com.alibaba.druid.pool.DruidPooledConnection类,commit()方法中被调用,即只有在调用Druid的提交事务方法时,Druid才会执行MySQL connector中提交事务的方法
即Druid不会自动提交事务
- Druid不会将活跃连接放回连接池
未发现Druid将活跃连接放回连接池的相关参数配置及代码
- Druid归还连接时会对事务隐式回滚
Druid将数据库连接归还到连接池时,会对事务隐式回滚,说明见前文
即使Druid会将活跃连接放回连接池,也会将对应的事务回滚,后续无法再提交
4.8. 为什么事务开启后不提交/回滚时后续不会被提交
事务开启后不提交/回滚,则对应的数据库连接在Druid数据源中会处于活动状态,无法被其他线程获取到,因此无法被其他线程提交
当前线程的ThreadLocal中有记录对应的数据库连接信息,假如当前线程后续又执行了其他数据库操作,分以下情况考虑
- 不使用事务
当前线程后续不使用事务,执行其他数据库操作时,会使用原有的连接,但因为不使用事务,不会执行commit,即不会提交原有事务
- 使用事务,使用原有连接
当前线程后续使用事务,使用原有连接(对应REQUIRED
等Spring事务传播行为),执行其他数据库操作时,因为此时事务不属于新事务,在尝试提交事务的过程中,不会实际执行提交事务的操作
- 使用事务,使用新的连接
当前线程后续使用事务,使用新的连接(对应REQUIRES_NEW
等Spring事务传播行为),会使用新的连接提交事务,与原有事务不属于同一个数据库连接,不会提交原有事务
根据以上内容可知,事务开启后不提交/回滚,不管是其他线程还是原有线程,后续都不会提交原有事务
以上情况可总结为如下表格:
执行操作的线程 | 是否使用事务 | Spring事务传播行为 | 不提交原有事务的原因 |
---|---|---|---|
其他线程 | 无论是否使用事务 | 无论哪种Spring事务传播行为 | 无法从数据源连接池中获取到原有连接,无法对原有连接执行提交事务操作 |
原有线程 | 不使用事务 | - | 不使用事务时不会执行commit |
原有线程 | 使用事务 | REQUIRED SUPPORTS MANDATORY NESTED | 使用原有事务,但不属于新事务,不会实际执行提交事务的操作 |
原有线程 | 使用事务 | REQUIRES_NEW NOT_SUPPORTED | 使用新的事务/连接提交,与原有事务无关 |
原有线程 | 使用事务 | NEVER | 新的事务会抛出异常,不会提交原有事务 |
4.9. 为什么事务开启后不提交/回滚时最终会被回滚
结合前文可知,经过一段时间后,在以下情况下,原有的事务会被回滚
- 当Java应用进程结束时,会关闭所有的数据库连接,原有的事务会回滚
- 当MySQL服务器kill对应的线程时,也会关闭对应的数据库连接,原有的事务会回滚
- 当事务超时(默认8小时),或在事务中获取行锁超时(默认50秒)时,原有的事务会回滚
因此事务开启后不提交/回滚时,最终会被回滚
4.10. Spring事务传播行为对事务嵌套执行的影响
以下分析Spring事务传播行为对事务嵌套执行的影响,即一个线程已开启一个事务后,又尝试开启事务的情况
4.10.1. 使用现有事务
一个线程已开启一个事务后,又使用事务传播行为REQUIRED
开启事务,情况如下:
第二个(及之后)事务开启时,不会开启新的事务,实际上还是使用现有事务;
第二个(及之后)事务执行完毕进行提交时,不会提交现有事务;
第一个事务执行完毕进行提交时,会对事务执行提交
即:使用现有的事务时,由第一次开启事务的代码最后对事务执行提交,后续的事务(没有开启新事务)不会对事务执行提交
以上流程如下所示:
4.10.2. 使用新的事务
一个线程已开启一个事务后,又使用事务传播行为REQUIRES_NEW
开启事务,情况如下:
第二个(及之后)事务开启时,会开启新的事务;
第二个(及之后)事务执行完毕进行提交时,只提交当前开启的事务,不会提交原有事务;
第一个事务执行完毕进行提交时,会对原有事务执行提交
即:使用新的事务时,每一次事务的开启后都由当前事务进行提交,不会提交原有事务
以上流程如下所示:
5. 编程式事务的建议使用方式
需要使用编程式事务时,建议使用Spring事务模板TransactionTemplate
5.1. Spring事务模板怎样保证事务提交/回滚
org.springframework.transaction.support.TransactionTemplate类的execute()方法用于通过事务执行数据库操作
setPropagationBehavior()方法可以用于设置TransactionTemplate对应的Spring事务传播行为
execute()方法部分代码如下:
TransactionStatus status = this.transactionManager.getTransaction(this);T result;try { result = action.doInTransaction(status);}catch (RuntimeException | Error ex) { // Transactional code threw application exception -> rollback rollbackOnException(status, ex); throw ex;}catch (Throwable ex) { // Transactional code threw unexpected exception -> rollback rollbackOnException(status, ex); throw new UndeclaredThrowableException(ex, "TransactionCallback threw undeclared checked exception");}this.transactionManager.commit(status);return result;
以上action为需要在事务中执行的自定义代码
在执行过程中出现异常时,会调用rollbackOnException()方法对事务进行回滚
若自定义代码执行完毕且未出现异常,则会调用transactionManager.commit()方法对事务进行提交
Spring事务模板会在数据库操作正常执行结束后提交事务,出现异常时回滚事务,不会出现遗漏
来源地址:https://blog.csdn.net/a82514921/article/details/128782544