【spring】spring源码系列之九:spring事务管理(上)
系列文章目录
前言
在开始spring事务管理的源码分析之前,我们先自己尝试简单实现一下事务管理,实现事务的传递
一、事务的使用
有了spring之后,事务的使用变得简单,但是封装得也更深,功能也更复杂,也更难以理解。所以我们要先回顾一下最基础的使用,由浅入深
1. jdbc使用事务
我们最开始使用事务是通过jdbc的方式,这也是最直接的方式,跟在数据库使用事务没什么区别
先贴一下代码:
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;public class TransactionExample {public static void main(String[] args) {// 数据库连接信息String url = "jdbc:mysql://localhost:3306/your_database_name?serverTimezone=UTC";String user = "your_username";String password = "your_password";Connection conn = null;PreparedStatement pstmt1 = null;PreparedStatement pstmt2 = null;try {// 加载JDBC驱动Class.forName("com.mysql.cj.jdbc.Driver");// 获取数据库连接conn = DriverManager.getConnection(url, user, password);// 将自动提交设置为falseconn.setAutoCommit(false);// 从Alice的账户转出500String sql1 = "UPDATE user_account SET balance = balance - 500 WHERE name = 'Alice'";pstmt1 = conn.prepareStatement(sql1);pstmt1.executeUpdate();// 模拟异常,用于测试事务回滚// if (true) throw new SQLException("模拟异常,触发回滚");// 向Bob的账户转入500String sql2 = "UPDATE user_account SET balance = balance + 500 WHERE name = 'Bob'";pstmt2 = conn.prepareStatement(sql2);pstmt2.executeUpdate();// 提交事务conn.commit();System.out.println("事务处理成功!");} catch (Exception e) {e.printStackTrace();// 发生异常,回滚事务if (conn != null) {try {conn.rollback();System.out.println("事务回滚!");} catch (SQLException ex) {ex.printStackTrace();}}} finally {// 关闭资源try {if (pstmt1 != null) pstmt1.close();if (pstmt2 != null) pstmt2.close();if (conn != null) conn.close();} catch (SQLException ex) {ex.printStackTrace();}}}
}
过程为
- 获取数据库连接
- 将连接的提交方式从自动提交改为手动提交(开启事务,为什么不需要Begin,是因为mysql改为手动提交后,执行增删改语句,就会隐式的开启事务,直到commit或者rollback)
- 执行sql
- commit提交事务
- 如果遇到错误,rollback回滚事务
这个过程与你在数据库连接工具中直接操作数据库事务一样,很好理解。但是每次都进行获取连接,关闭连接,开启事务,提交事务,回滚事务这些与业务无关的操作,代码既繁琐,又不利于维护。
所以我们需要将获取连接,关闭连接,开启事务,提交事务,回滚事务这些与业务无关的操作抽取出来,形成一个事务管理器。
2.自己实现事务管理器
2.1 简单实现
先将获取连接、关闭连接抽取出来
public class ConnectionUtil {private static final String DB_DRIVER = "com.mysql.jdbc.Driver";private static final String DB_URL = "jdbc:mysql://localhost:3306/j1910";private static final String USER = "root";private static final String PASS = "root";/**获取连接*/public static Connection getConnection() {try {Class.forName(DB_DRIVER);} catch (ClassNotFoundException e) {e.printStackTrace();}Connection connection = null;try {connection = DriverManager.getConnection(DB_URL, USER, PASS);} catch (SQLException throwables) {throwables.printStackTrace();}return connection;}/**关闭连接*/public static void closeConnection(Connection conn) {if (conn != null) {try {conn.close();} catch (SQLException e) {// 记录关闭连接时的异常,通常不会抛出e.printStackTrace();}}}
}
再来将开启事务、提交事务和回滚事务抽取到MyTransactionManager,并提供事务调用的方法doWithTransaction。开启事务、提交事务和回滚事务都是私有的,暴露出来的是事务调用的方法。我希望每次事务调用都是独立的,所以每次都应该重新获取连接,而不能将连接保存起来
public class MyTransactionManager{/**获取连接*/private Connection getConnection() {return ConnectionUtil.getConnection();}/**关闭连接*/private void closeConnection(Connection connection) {ConnectionUtil.closeConnection(connection);}/**开启事务*/private void startTransaction(Connection connection) {try {connection.setAutoCommit(false);} catch (SQLException throwables) {throwables.printStackTrace();}}/**提交事务*/private void commit() {try {getConnection().commit();} catch (SQLException throwables) {throwables.printStackTrace();}}/**回滚事务*/private void rollback() {try {getConnection().rollback();} catch (SQLException throwables) {throwables.printStackTrace();}}/**执行事务*/public void doWithTransaction(SqlAction action) {//获取连接Connection connection = getConnection();//开启事务startTransaction(connection);try {//执行sqlaction.execute(connection);//提交事务commit();} catch (Exception e) {e.printStackTrace();//回滚事务rollback();}finally {closeConnection(connection);}}/**函数式接口,调用的时候实现*/interface SqlAction {void execute(Connection connection);}
}
测试一下,其中QueryRunner是org.apache.commons.dbutils中的工具类,是对PrepareStatemet的封装,简化了sql执行操作。测试结果因为int i = 1/0报错,所以回滚了
public class MainTest {public static void main(String[] args) {MyTransactionManager transactionManager = new MyTransactionManager();transactionManager.doWithTransaction(connection -> {QueryRunner queryRunner = new QueryRunner();try {queryRunner.update(connection,"update student set stu_name = ? where stu_id = ?","lisi",7);int i = 1/0;queryRunner.update(connection,"update student set stu_name = ? where stu_id = ?","wangwu",8);} catch (SQLException throwables) {//把错误抛出去throw new RuntimeException(throwables);}});}
}
但是这里还有一个问题,比如两个使用事务的方法,方法A和方法B,方法A调用了方法B,即使方法B失败回滚了,方法A还是能提交成功,因为两个方法使用的是不同的数据库连接
public void A(){transactionManager.doWithTransaction(connection -> {executeSql;//数据库操作B()//调用了B方法}
}public void B(){transactionManager.doWithTransaction(connection -> {数据库操作}
}
那如果我希望在事务A调用事务B的时候,使用的是同一个事务,那怎么办呢?那自然需要有个地方去维护数据库连接,最理想的方式就是用ThreadLocal去保存连接了。
2.2 新的实现
但是这样就能解决问题了吗?也不能,因为如果事务A调用事务B时,事务B成功了提交事务时,因为是同一个数据库连接,也会把事务A已完成的操作提交了,假如事务A后面又出错,想回滚也回滚不了了。所以我们应该在事务提交或者回滚前判断一下是不是前面已经有事务,如果有就不提交也不回滚了,只是给这个连接标记一下是否应该回滚,所以我们写一个新的事务管理器MyNewTransactionManager,同时也要增加几个组件,功能如下:
- ConnectionHolder:对数据库连接的封装,让他具有标记是否回滚的功能
- ConnectionSyncManager:用于存放ConnectionHolder到ThreadLocal
- TransactionObject:事务对象,判断是否是新事务
- MyNewTransactionManager:管理事务(开启、提交、回滚事务)
- MyTransactionTemplate:提供事务调用的方法(从MyNewTransactionManager中抽取出来,这样MyNewTransactionManager只需要管理事务)
代码如下:
public class ConnectionHolder {private Connection connection;/**是否需要回滚*/private boolean rollback = false;public Connection getConnection() {return connection;}public void setConnection(Connection connection) {this.connection = connection;}public boolean isRollback() {return rollback;}public void setRollback(boolean rollback) {this.rollback = rollback;}
}
public class ConnectionSyncManager {private static ThreadLocal<ConnectionHolder> THREAD_CONNECTIONS = new ThreadLocal<>();public static ConnectionHolder getConnectionHolder() {return THREAD_CONNECTIONS.get();}public static void setConnectionHolder(ConnectionHolder connection) {THREAD_CONNECTIONS.set(connection);}
}
public class MyNewTransactionManager {/**新建连接*/private Connection getConnection() {return ConnectionUtil.getConnection();}/**关闭连接*/private void closeConnection(Connection connection) {ConnectionUtil.closeConnection(connection);}/**开启事务*/public void startTransaction(TransactionObject transactionObject) {//如果已经有连接了,说明之前开启过事务了,就不用重复开启了,如果没有连接,就新建连接,开启事务ConnectionHolder connectionHolder = transactionObject.getConnectionHolder();if (connectionHolder == null) {connectionHolder = new ConnectionHolder();connectionHolder.setConnection(getConnection());//新建连接transactionObject.setNewConnection(true);//标记为新的事务transactionObject.setConnectionHolder(connectionHolder);//维护连接ConnectionSyncManager.setConnectionHolder(connectionHolder);//将新连接放到同步器中//开启事务try {connectionHolder.getConnection().setAutoCommit(false);} catch (SQLException throwables) {throwables.printStackTrace();}}else {transactionObject.setNewConnection(false);}}/**提交事务*/public void commit(TransactionObject transactionObject) {//如果是新的事务,处理事务。如果不是新的事务,不处理if (transactionObject.isNewConnection()) {try {ConnectionHolder connectionHolder = transactionObject.getConnectionHolder();//如果connectionHolder中rollback为true则回滚if (connectionHolder.isRollback()) {rollback(transactionObject);}else {//rollback不为true提交事务connectionHolder.getConnection().commit();}} catch (SQLException throwables) {throwables.printStackTrace();}}}/**回滚事务*/public void rollback(TransactionObject transactionObject) {//如果是新的事务,直接回滚。如果不是新的事务,将ConnectionHolder的rollback标记为trueif (transactionObject.isNewConnection()) {try {transactionObject.getConnectionHolder().getConnection().rollback();} catch (SQLException throwables) {throwables.printStackTrace();}}else {transactionObject.getConnectionHolder().setRollback(true);}}/**事务完成后关闭连接*/public void completeTransaction(TransactionObject transactionObject) {//如果是新的事务,关闭连接if (transactionObject.isNewConnection()) {closeConnection(transactionObject.getConnectionHolder().getConnection());}}/**获取事务对象*/public TransactionObject getTransactionObject() {//每个事务都新建一个事务对象TransactionObject transactionObject = new TransactionObject();//先从ConnectionSyncManager里面找连接ConnectionHolder connectionHolder = ConnectionSyncManager.getConnectionHolder();transactionObject.setConnectionHolder(connectionHolder);return transactionObject;}/**事务对象,每次事务调用就生成一个事务对象*/public class TransactionObject {private ConnectionHolder connectionHolder;/**是不是新创建的连接,用来判断是否前面已经有事务了*/private boolean newConnection;public ConnectionHolder getConnectionHolder() {return connectionHolder;}public void setConnectionHolder(ConnectionHolder connectionHolder) {this.connectionHolder = connectionHolder;}public boolean isNewConnection() {return newConnection;}public void setNewConnection(boolean newConnection) {this.newConnection = newConnection;}}
}``````java
public class MyTransactionTemplate {private MyNewTransactionManager transactionManager;public MyTransactionTemplate(MyNewTransactionManager transactionManager) {this.transactionManager = transactionManager;}/**执行事务*/public void doWithTransaction(SqlAction action) {//获取事务MyNewTransactionManager.TransactionObject transactionObject = transactionManager.getTransactionObject();//开启事务transactionManager.startTransaction(transactionObject);try {//执行sqlaction.execute(transactionObject.getConnectionHolder().getConnection());//提交事务transactionManager.commit(transactionObject);} catch (Exception e) {e.printStackTrace();//回滚事务transactionManager.rollback(transactionObject);}finally {transactionManager.completeTransaction(transactionObject);}}/**函数式接口,调用的时候实现*/interface SqlAction {void execute(Connection connection);}
}
好了,我们来测试一下吧。为了代码简便,我们直接new了MyNewTransactionManager和transactionTemplate,调用ServiceA方法时直接传入(如果不嫌麻烦,可以将MyNewTransactionManager和transactionTemplate给spring管理,然后注入到ServiceA和ServiceB中,篇幅有限,我就不贴了)
public class MainTest2 {public static void main(String[] args) {MyNewTransactionManager transactionManager = new MyNewTransactionManager();MyTransactionTemplate transactionTemplate = new MyTransactionTemplate(transactionManager);ServiceA(transactionTemplate);}public static void ServiceA(MyTransactionTemplate transactionTemplate) {transactionTemplate.doWithTransaction(connection -> {QueryRunner queryRunner = new QueryRunner();try {queryRunner.update(connection,"update student set stu_name = ? where stu_id = ?","zhangsan",7);ServiceB(transactionTemplate);} catch (SQLException throwables) {//把错误抛出去throw new RuntimeException(throwables);}});}public static void ServiceB(MyTransactionTemplate transactionTemplate) {transactionTemplate.doWithTransaction(connection -> {QueryRunner queryRunner = new QueryRunner();try {queryRunner.update(connection,"update student set stu_name = ? where stu_id = ?","lisi",8);int i = 1/0;} catch (SQLException throwables) {//把错误抛出去throw new RuntimeException(throwables);}});}
}
结果,因为ServiceB方法报错后给ConnectionHolder设置了回滚标记,最后整体都回滚了
到这里,其实已经把spring的事务管理器DataSourceTransactionManager的基本实现过程展示出来了(可以认为是对PROPAGATION_REQUIRED这种传播方式的实现),为避免篇幅过长,下一篇我们再来看看DataSourceTransactionManager是怎样实现事务管理的吧
总结
- 我们循序渐进,手写了MyNewTransactionManager ,完成了事务方法A调用事务方法B使用同一个事务,还原了事务传递的关键步骤:
1)使用相同的数据库连接(放到ThreadLocal中)
2)需要一个标识(newConnection)判断是否是新的事务,并根据这个标识决定是真正提交、回滚,还是只标记全局的状态(rollbackOnly)
3)需要一个全局的状态(rollbackOnly),用于真正提交、回滚
相关文章:
【spring】spring源码系列之九:spring事务管理(上)
系列文章目录 前言 在开始spring事务管理的源码分析之前,我们先自己尝试简单实现一下事务管理,实现事务的传递 一、事务的使用 有了spring之后,事务的使用变得简单,但是封装得也更深,功能也更复杂,也更…...

牛客网 NC22167: 多组数据a+b
牛客网 NC22167: 多组数据ab 题目分析 这道题目来自牛客网(题号:NC22167),要求我们计算两个整数a和b的和。乍看简单,但有以下特殊点需要注意: 输入包含多组测试数据每组输入两个整数当两个整数都为0时表示…...

K8S Ingress、IngressController 快速开始
假设有如下三个节点的 K8S 集群: k8s31master 是控制节点 k8s31node1、k8s31node2 是工作节点 容器运行时是 containerd 一、理论介绍 1)什么是 Ingress 定义:Ingress 是 Kubernetes 中的一种资源对象,它定义了外部访问集群内…...
GitHub 趋势日报 (2025年05月14日)
本日报由 TrendForge 系统生成 https://trendforge.devlive.org/ 🌐 本日报中的项目描述已自动翻译为中文 📈 今日整体趋势 Top 10 排名项目名称项目描述今日获星总星数语言1xming521/WeClone🚀从聊天记录创造数字分身的一站式解决方案&…...

快消零售AI转型:R²AIN SUITE如何破解效率困局
引言 快消零售行业正经历从“规模扩张”到“精益运营”的转型阵痛,消费者需求迭代加速、供应链复杂度攀升、人力成本持续走高,倒逼企业通过技术升级实现业务重塑[1]。RAIN SUITE以AI应用中台为核心,针对快消零售场景打造全链路提效方案&…...

电路中零极点的含义
模拟电路中的零极点设计非常重要,涉及到系统的稳定。零点是开环传输函数分子为0时对应的频率。极点就是开环传递函数分子为0时对应的频率。 零点表征电路中能量输出路径的抵消效应,当不同支路的信号大小相等、方向相反时,导致特定频率下响应…...

解读RTOS 第八篇 · 内核源码解读:以 FreeRTOS 为例
1. 引言 FreeRTOS 作为最流行的嵌入式实时操作系统之一,其内核源码简洁且功能完善。通过剖析其关键模块(任务管理、调度器、队列、内存管理和移植层),不仅能够更深入地理解 RTOS 的运行机制,还能掌握根据项目需求进行内核定制与优化的能力。本章将带你以 FreeRTOS 10.x 版…...

2025年长三角+山东省赛+ 认证杯二阶段资料助攻说明
长三角高校数模B题 完整论文代码已经在售后群 网盘链接 发布 长三角更新时间轴 5.15 23:00 B站发布 完整论文讲解视频 5.16 18:00 j降重说明 5.17 22:00 无水印版本可视化无水印代码 其余时间 写手老师 售后群在线答疑 山东省助攻C道 认证杯二阶段助攻C题 山东省认证杯…...
平滑过滤值策略
该策略是一种基于技术分析的交易策略,主要通过计算一系列指标来判断市场趋势,并根据这些指标生成交易信号。 策略概述 该策略的核心在于利用多个技术指标来分析市场动态,并据此制定交易决策。它结合了价格动量、波动性和趋势跟踪等多种因素,旨在提高交易的准确性和效率。…...
MATLAB安装全攻略:常见问题与解决方案
MATLAB安装常见问题与解决方案 一、系统兼容性验证 安装前需确认操作系统满足MATLAB版本要求: Windows 10版本1903及以上(64位)macOS Monterey 12.6及以上Ubuntu 22.04 LTS及以上 验证命令示例: # Linux系统验证 lsb_release…...
Apache HttpClient 5 用法-Java调用http服务
Apache HttpClient 5 核心用法详解 Apache HttpClient 5 是 Apache 基金会推出的新一代 HTTP 客户端库,相比 4.x 版本在性能、模块化和易用性上有显著提升。以下是其核心用法及最佳实践: 一、添加依赖 Maven 项目: <dependency><…...

鸿蒙电脑:五年铸剑开新篇,国产操作系统新引擎
出品 | 何玺 排版 | 叶媛 前不久,玺哥发布的《鸿蒙电脑,刺向垄断的利刃,将重塑全球PC市场格局》发布后,获得了读者朋友的积极反馈,不少都期望鸿蒙电脑早日发布。 如今,它真来了! 5月8日&…...
AI大模型:(二)2.5 人类对齐训练自己的模型
目录 1.人类对齐原理 1.1. 偏好学习(人类反馈,RLHF/DPO) 1.2. 奖励模型(AI的“打分老师”) 1.3. 价值观约束(如宪法AI) 2.如何人类对齐训练 2.1.对比学习(人类反馈 RLHF/DPO) 2.2.考试评分(奖励模型训练) 2.3.底线教育(安全防护) 2.4.持续优化(在线学习…...
算法图表总结:查找、排序与递归(含 Mermaid 图示)
算法图表总结:查找、排序与递归(含 Mermaid 图示) 分类标签:算法、数据结构、Mermaid、技术图表 关键词: 算法可视化、Mermaid 图表、数据结构、二分查找、快速排序、递归树 摘要: 本文通过 Mermaid 图表…...
【redis】jedis客户端的使用
Jedis是Redis官方推荐的Java客户端库,提供了对Redis数据库的全面支持,适用于单机、哨兵及集群模式。作为最老牌的Java Redis客户端,其API设计直观,与Redis命令高度对应,例如set、get等方法与原生命令一致,降…...

SQLMesh信号机制详解:如何精准控制模型评估时机
SQLMesh的信号机制为数据工程师提供了更精细的模型评估控制能力。本文深入解析信号机制的工作原理,通过简单和高级示例展示如何自定义信号,并提供实用的使用技巧和测试方法,帮助读者优化数据管道的调度效率。 一、为什么需要信号机制…...
TCP(传输控制协议)建立连接的过程
TCP(传输控制协议)建立连接的过程称为 三次握手(Three-Way Handshake)。这是为了确保通信双方能够可靠地建立连接,并同步初始序列号。以下是详细步骤: 三次握手过程(通俗比喻:打电话…...

通义千问-langchain使用构建(二)
目录 序言xinference应用构建构建过程简单概述成效 chatchat应用构建过程成效 总结 序言 在昨天的使用langchain的基础上。又尝试了构建智能问答应用。 使用langchain chatchat这个开源包,构建了一下智能问答系统。 前置项,是使用了一下xinference框架&…...

[IMX] 02.GPIO 寄存器
目录 手册对应章节 1.GPIO 复用(引脚功能选择)- IOMUXC_SW_MUX_CTL_PAD_xxx 2.GPIO 电气特性 - IOMUXC_SW_PAD_CTL_PAD_xxx 3.GPIO 数据与控制寄存器 3.1.数据 - DR 3.2.输入/输出选择 - GDIR 3.3.状态 - PSR 3.4.中断触发控制 - ICR 3.5.中断使…...

【电子通识】热敏纸的静态发色性能和动态发色性能测试方法
静态发色性能的测定 测定治具 测定静态发色曲线需要使用三个仪器,包括静态发色仪、秒表(分辨力为0.01 s)、反射光密度计(符合 GB/T23649)。 静态发色曲线使用的测试仪为静态发色仪。其结构如下图所示:包括了保湿压板、金属加热板、温度显示器、控制面板。温度能在50℃到…...
Nginx 返回 504 状态码表示 网关超时(Gateway Timeout)原因排查
Nginx 返回 504 状态码表示 网关超时(Gateway Timeout),这意味着 Nginx 作为反向代理服务器,在等待上游服务器(如后端应用服务器、数据库服务器等)响应时,超过了预设的时间限制,最终…...

AIbase推出全球MCP Server集合平台 收录超12万个MCP服务器客户端
2025年,AI领域迎来了一项重要的技术进展——MCP(Model Context Protocol,模型上下文协议)的广泛应用。全球MCP Server集合平台AIbase(https://mcp.aibase.cn/)应运而生,为AI开发者提供了一站式的MCP服务器和客户端整合…...

使用CMake中的configure_file命令自动生成项目版本信息
1 背景 随着实际项目的完善,可维护变的更加重要。在日志中保存项目的版本或是构建信息是一个非常有用的方法。 CMake提供了configure_file()命令,可以帮助开发者在构建项目时,自动生成版本或是构建信息,便于开发者在代码中直接引…...

Linux的进程管理和用户管理
gcc与g的区别 比如有两个文件:main.c mainc.cpp(分别是用C语言和C语言写的)如果要用gcc编译: gcc -o mainc main.c gcc -o mainc mainc.cpp -lstdc表明使用C标准库; 区别一: gcc默认只链接C库&#x…...

【springcloud学习(dalston.sr1)】Eureka服务端集群的搭建(含源代码)(二)
该系列项目整体介绍及源代码请参照前面写的一篇文章【springcloud学习(dalston.sr1)】项目整体介绍(含源代码)(一) 这篇文章主要介绍多个eureka服务端的集群环境是如何搭建的。 (一)eureka的简要说明 Eu…...
【匹配】Needleman–Wunsch
Needleman-Wunsch 文章目录 Needleman-Wunsch1. 算法介绍2. 公式及原理3. 伪代码 1. 算法介绍 背景与目标 Needleman–Wunsch 算法由 Saul B. Needleman 和 Christian D. Wunsch 于1970年提出,是用于生物序列(如蛋白质或 DNA)全局比对&#x…...

崩坏星穹铁道 3.3 版本前瞻活动攻略:在黎明升起时坠落
《崩坏星穹铁道》3.3 版本 “在黎明升起时坠落” 将于 5 月 21 日正式上线。本次版本更新内容丰富,新角色、新地图、新活动和新周本 BOSS 等精彩内容,等待开拓者们前去体验。下面就为大家带来 3.3 版本的前瞻活动攻略。 一、新角色与卡池 1.上半卡池&am…...

OneNote内容太多插入标记卡死的解决办法
OneNote内容太多插入标记卡死的解决办法 针对平板电脑的OneNote用户适合此类情况: 当向电脑导入几百页pdf可以正常使用,唯独插入标记的时候OneNote直接罢工,只能关闭。关闭时还可能会出现0x000000fxxxxx的错误。 注:仅对于平板…...

fpga系列 HDL : Microchip FPGA开发软件 Libero Soc 安装 license申请
启动 注册账号:https://login.microchip.com/申请免费许可:https://www.microchipdirect.com/fpga-software-products C:\Windows\System32>vol驱动器 C 中的卷是 Windows卷的序列号是 ****-****为“D:\Microsemi\License.dat”创建环境变量“LM_LICE…...

极简主义现代商务风格PPT模版6套一组分享下载
现代商务风格PPT模版下载https://pan.quark.cn/s/12fbc52124d9 第一张PPT模版,简约风,橄榄绿背景,黑色竖条装饰,文字有中英文标题和占位符。需要提取关键元素:简约、橄榄绿、对称布局、占位文本的位置。 风格&#…...