MySQL中Spring管理的事务开启后不提交引起的事故

25 篇文章 0 订阅

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. 相关组件及版本

组件版本
spring5.3.20
mybatis3.5.9
mybatis-spring2.0.6
druid1.2.8
mysql-connector-java8.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. 新的数据库操作会使用新的连接执行,若能成功获取到连接则可以正常执行

2. 原有事务不会提交

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 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();
}

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#sysvar_wait_timeout

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.htmlhttps://dev.mysql.com/doc/refman/5.6/en/innodb-parameters.html#sysvar_innodb_lock_wait_timeout

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事务模板会在数据库操作正常执行结束后提交事务,出现异常时回滚事务,不会出现遗漏

  • 11
    点赞
  • 36
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
在项目配置 MySQL 事务Spring 事务的方法如下: 1. MySQL 事务配置 对于 MySQL 事务,需要在代码使用 JDBC API 来控制事务的开始、提交回滚等操作。具体步骤如下: - 获取数据库连接:使用 DriverManager 或 DataSource 获取数据库连接。 - 开启事务:调用 Connection 对象的 setAutoCommit(false) 方法,将自动提交设置为 false,即开启事务。 - 执行数据库操作:在事务执行一组数据库操作,可以使用 PreparedStatement 或 Statement 对象来执行 SQL 语句。 - 提交事务回滚事务:如果所有的数据库操作都执行成功,调用 Connection 对象的 commit() 方法提交事务;如果任何一个数据库操作失败,调用 Connection 对象的 rollback() 方法回滚事务。 - 关闭数据库连接:无论事务执行成功或失败,都需要关闭数据库连接。 2. Spring 事务配置 对于 Spring 事务,可以使用声明式事务或编程式事务来实现事务的控制。具体步骤如下: - 声明式事务配置:在 Spring 配置文件配置事务管理器(TransactionManager)和事务通知(TransactionAdvice),并在需要控制事务的方法上添加事务注解(@Transactional)。 - 编程式事务配置:在代码使用 TransactionTemplate 或 PlatformTransactionManager 等 Spring 提供的事务 API 来手动控制事务的开始、提交回滚等操作。 以上是 MySQL 事务Spring 事务的基本配置方法,具体实现方式还要根据实际的业务需求和技术栈来进行选择和优化。
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值