事务未能正确提交导致Druid报错connection holder is null(1)

本文涉及的产品
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
RDS MySQL Serverless 高可用系列,价值2615元额度,1个月
简介: 事务未能正确提交导致Druid报错connection holder is null

原文出自: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事务传播行为决定对应的操作:


image.png

  • 结论

后续使用原有线程、使用事务时,无论新事务使用哪种Spring事务传播行为,原有事务均不会提交

若新事务使用的事务传播行为是REQUIRED、SUPPORTS、MANDATORY、NESTED、NEVER,则新的事务也不会提交

若新事务使用的事务传播行为是REQUIRES_NEW、NOT_SUPPORTED,则新的事务(或不使用事务)成功获取到连接时可以提交

2.6.3. 后续使用其他线程

开启事务后未提交/回滚,假如后续使用线程池中其他线程(开启事务后有执行提交/回滚操作),执行数据库操作时无论是否使用事务,情况如下:


后续使用线程池中其他线程进行操作时,执行数据库操作的过程中,由于ThreadLocal中没有对应的数据库连接信息,因此会从数据源获取可用的连接,假如能够获取到可用的连接,则可以正常执行数据库操作


当有线程开启事务后未提交/回滚时,数据库连接会被相关的线程占用,不会归还到数据库连接池,也无法被其他线程获取到。由于线程池中配置的线程数通常比数据库连接池的连接数大,当线程开启事务后未提交/回滚发生次数超过数据源连接池最大允许连接数时,会导致所有的连接都被相关线程占用,其他线程无法获取到可用连接,无法执行数据库操作



7a8075debec546178e00933f2b4503cd.png

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语句
归还连接至连接池

如下图所示:




2c6de13823de495b885617f37476cff1.png

不使用事务执行SQL语句时,主要由MyBatis完成,与Spring关系不大。

3.3. 使用事务执行SQL语句的过程

3.3.1. 过程分析

在使用事务执行SQL语句时,每次执行SQL语句时的大致阶段如下

从连接池借出连接(可能需要创建新连接)
关闭自动提交
执行SQL语句1
执行SQL语句n
提交/回滚事务
开启自动提交
归还连接至连接池

如下图所示:


6254e332118c40bba859ee0958a2ceb0.png使用事务执行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();
}




相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
目录
相关文章
|
4月前
【bug记录】旋转链表与力扣报错:member access within null pointer of type ‘struct ListNode‘
【bug记录】旋转链表与力扣报错:member access within null pointer of type ‘struct ListNode‘
|
4月前
解决微软云Azure Function运行报错-Value cannot be null. (Parameter ‘provider‘)
解决微软云Azure Function运行报错-Value cannot be null. (Parameter ‘provider‘)
89 4
|
4月前
|
JavaScript 前端开发 C++
【Azure Function】调试 VS Code Javascript Function本地不能运行,报错 Value cannot be null. (Parameter 'provider')问题
【Azure Function】调试 VS Code Javascript Function本地不能运行,报错 Value cannot be null. (Parameter 'provider')问题
|
6月前
|
Java Spring
解决Springboot集成ElasticSearch 报错:A bean with that name has already been defined in null and overriding
解决Springboot集成ElasticSearch 报错:A bean with that name has already been defined in null and overriding
220 2
|
6月前
|
Web App开发 分布式计算 大数据
MaxCompute操作报错合集之配置归并节点,出现java.lang.NullPointerException: null错误提示,该怎么办
MaxCompute是阿里云提供的大规模离线数据处理服务,用于大数据分析、挖掘和报表生成等场景。在使用MaxCompute进行数据处理时,可能会遇到各种操作报错。以下是一些常见的MaxCompute操作报错及其可能的原因与解决措施的合集。
|
7月前
|
SQL 关系型数据库 MySQL
实时计算 Flink版产品使用合集之从MySQL同步数据到Doris时,历史数据时间字段显示为null,而增量数据部分的时间类型字段正常显示的原因是什么
实时计算Flink版作为一种强大的流处理和批处理统一的计算框架,广泛应用于各种需要实时数据处理和分析的场景。实时计算Flink版通常结合SQL接口、DataStreamAPI、以及与上下游数据源和存储系统的丰富连接器,提供了一套全面的解决方案,以应对各种实时计算需求。其低延迟、高吞吐、容错性强的特点,使其成为众多企业和组织实时数据处理首选的技术平台。以下是实时计算Flink版的一些典型使用合集。
|
7月前
|
SQL 关系型数据库 MySQL
python在mysql中插入或者更新null空值
这段代码是Python操作MySQL数据库的示例。它执行SQL查询从表`a_kuakao_school`中选取`id`,`university_id`和`grade`,当`university_id`大于0时按升序排列。然后遍历结果,根据`row[4]`的值决定`grade`是否为`NULL`。若不为空,`grade`被格式化为字符串;否则,设为`NULL`。接着构造UPDATE语句更新`university`表中对应`id`的`grade`值,并提交事务。重要的是,字符串`NULL`不应加引号,否则更新会失败。
170 2
|
4月前
|
SQL 关系型数据库 MySQL
在 MySQL 中使用 IS NULL
【8月更文挑战第12天】
664 0
在 MySQL 中使用 IS NULL
|
4月前
|
SQL 关系型数据库 MySQL
mysql不等于<>取特定值反向条件的时候字段有null值或空值读取不到数据
对于数据库开发的专业人士来说,理解NULL的特性并知道如何正确地在查询中处理它们是非常重要的。以上所介绍的技巧和实例可以帮助你更精准地执行数据库查询,并确保数据的完整性和准确性。在编写代码和设计数据库结构时,牢记这些细节将有助于你避免许多常见的错误,提高数据库应用的质量与性能。
140 0
|
5月前
|
SQL 存储 索引
MySQL设计规约问题之为什么应该把字段定义为NOT NULL并且提供默认值
MySQL设计规约问题之为什么应该把字段定义为NOT NULL并且提供默认值