当前位置: 首页 > news >正文

Spring事务模板及afterCommit存在的坑

大家好,我是墨哥(隐墨星辰)。今天的内容来源于两个线上问题,主要和大家聊聊为什么支付系统中基本只使用事务模板方法,而不使用声明式事务@Transaction注解,以及使用afterCommit()出现连接未按预期释放导致的性能问题。

1. 为什么不使用@Transaction注解

以前写管理平台的代码时,经常使用@Transaction注解,也就是所谓的声明式事务,简单而实用,但是在做支付后,基本上没有使用@Transaction,全部使用事务模板来做。主要有两个考虑:

1)事务的粒度控制不够灵活,容易出现长事务

@Transactional注解通常应用于方法级别,这意味着被注解的方法将作为一个整体运行在事务上下文中。在复杂的支付流程中,需要做各种运算处理,很多前置处理是不需要放在事务里面的。

而使用事务模板的话,就可以更精细的控制事务的开始和结束,以及更细粒度的错误处理逻辑。

@Transactional
public PayOrder process(PayRequest request) {validate(request);PayOrder payOrder = buildOrder(request);save(payOrder);// 其它处理otherProcess(payOrder);
}

比如上面的校验,构建订单,其它处理都不需要放在事务中。

如果把@Transactional从process()中拿走,放到save()方法,也会面临另外的问题:otherProcess()依赖数据库保存成功后才能执行,如果保存失败,不能执行otherProcess()处理。全部考虑进来后,使用注解处理起来就很麻烦。

2)事务传播行为的复杂性

@Transactional注解支持不同的事务传播行为,虽然这提供了灵活性,但在实际应用中,错误的事务传播配置可能导致难以追踪的问题,如意外的事务提交或回滚。

而且经常有多层子函数调用,很容易子函数有一个耗时操作(比如RPC调用或请求外部应用),一方面可能出事长事务,另一方面还可能因为外调抛异步,导致事务回滚,数据库中都没有记录保存。

以前就在生产上碰到过类似的问题,因为在父方法使用了@Transactional注解,子函数抛出异常,去数据库找问题单据,竟然没有记录,翻代码一行行看,才发现问题。

2. afterCommit存在的问题及解法

有一次参与线上压测,在流量上去后,应用持续报获取数据库连接超时,排查很久才找到原因,问题非常经典,值得和大家聊聊。

无论在支付系统,还是电商系统,还是其它各种业务系统,都存在这样的需求:在一个事务中既保存多个数据库表,又要外发请求,且这个外发请求耗时很长。

比如:方法A保存数据库表A,方法B保存数据库表B,并且要外发给其它系统且耗时长,方法C要保存数据库表C。这三个方法需要在一个事务里面。

我见过三种方案:

方案一:不管三七二十一,就直接放在一个事务中。请求量不大时,看不出长事务的影响。

方案二:知道使用Spring提供的模板方法:TransactionSynchronizationAdapter.afterCommit()。外发请求耗时长过长时,在大并发下仍然有连接未能及时释放的问题。

方案三:自己实现事务模板方法,在Spring提交事务并释放连接后,再执行耗时长的外发。

第一种没什么好说的,下面介绍方案二和方案三,两者区别如下图所示:

在支付系统中,经常需要做一些流程编排,这些流程操作需要放在一个事务中,比如先保存主单据,再保存流水单据,然后外发银行请求扣款,有同学写的代码类似这样:

主方法伪代码(流程引擎入口):

public void process(FlowContext context) {// 获取流程处理链List<FlowProcess> flows = fetchFlow(context);for (FlowProcess flow : flows) {// 使用事务模板dataSourceManager.getTransactionTemplate().execute(status -> {// 执行子流程flow.execute(context); // 更新主单信息context.getPayOrder().putJournal(context.getJournal());context.getPayOrder().transToNextStatus(context.getJournal().getTargetStatus());save(context.getPayOrder());return true;});}
}

其中一个外发银行子流程伪代码

public void execute(FlowContext context) {Journal journal = buildJournal(context);// 子函数里面保存了3张表的数据save(journal);TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {@Overridepublic void afterCommit() {// 事务提交后,再发送给外部银行gatewayService.sendToChannel(journal);}});
}

预期是事务提交后再调用发给银行。

但是实际情况却是,Spring提交事务后,调用了afterCommit(),但是并没有释放连接,导致在外发银行的长达1000多毫秒的时间内,数据库连接一直在保持,而不是提交事务后马上归还了连接,加上线上服务器的连接数只分配了30个给每台应用。这就意味着最大并发也小于30。

通过查看AbstractPlatformTransactionManager.java,发现是先调用:riggerAfterCommit(status),然后才清理并释放连接:cleanupAfterCompletion(status)。

    private void processCommit(DefaultTransactionStatus status) throws TransactionException {try {// 其它代码省略... ...// Trigger afterCommit callbacks, with an exception thrown there// propagated to callers but the transaction still considered as committed.try {triggerAfterCommit(status);}// 其它代码省略... ...}finally {cleanupAfterCompletion(status);}}

解决办法:自己创建一个事务模板,实现afterCommit()。

public class FlowTransactionTemplate { public static <R> R execute(FlowContext context, Supplier<R> callback) { TransactionTemplate template = context.getTransactionTemplate();Assert.notNull(template, "transactionTemplate cannot be null"); PlatformTransactionManager transactionManager = template.getTransactionManager();Assert.notNull(transactionManager, "transactionManager cannot be null"); boolean commit = false;try {TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition()); // Corrected "TranscationStatus" to "TransactionStatus"R result = null;try {result = callback.get();} catch (Exception e) {transactionManager.rollback(status); throw e;}transactionManager.commit(status);commit = true;return result;} finally {if (commit) {invokeAfterCommit(context);}}}private static void invokeAfterCommit(FlowContext context) {try {context.invokeAfterCommit();} catch (Exception e) {// 打印日志... ...}}
}

FlowContext加上事务提交后的执行的钩子方法,在钩子方法中实现一些长耗时工作:

public class FlowContext {// 其它代码不变... ...private List<AfterCommitHook> afterCommitHooks = new ArrayList<>();public void registerAfterCommitHook(AfterCommitHook hook) {afterCommitHooks.add(hook);}public void invokeAfterCommit() {try {for(AfterCommitHook hook : afterCommitHooks) {hook.afterCommit();}} catch (Exception e) {// 异常处理... ...} finally {// 钩子已执行完,清理掉afterCommitHooks.clear();}}public static abstract class AfterCommitHook {public abstract void afterCommit();}
}

主流程修改为直接调用:FlowTranscationTemplate.execute。

public void process(FlowContext context) {context.setTransactionTemplate(dataSourceManager.getTransactionTemplate());List<FlowProcess> flows = fetchFlow(context);for (FlowProcess flow : flows) {// 把Spring模板方法修改自己的模板方法,其它不变FlowTransactionTemplate.execute(context, () -> {flow.execute(context);context.getPayOrder().putJournal(context.getJournal());context.getPayOrder().transToNextStatus(context.getJournal().getTargetStatus());save(context.getPayOrder());return true;});}
}

子流程修改为把afterCommit要做的事注册到流程上下文中:

public void execute(FlowContext context) {Journal journal = buildJournal(context);// 子函数里面保存了3张表的数据save(journal);// 把外发动作注册到流程上下文中的钩子方法中,// 而不是直接使用Spring原生的TransactionSynchronizationAdapter.afterCommit()// 其它保持不变context.registerAfterCommitHook(() -> {// 事务提交后发给银行gatewayService.sendToChannel(journal);});
}

这样处理的优点有几个:

  1. 清晰的事务边界管理:通过显式控制事务的提交和回调执行,增加了代码的可控性。
  2. 资源使用优化:确保数据库连接在不需要时能够及时释放,提升了资源的使用效率。
  3. 灵活的后续操作扩展:允许注册多个回调,方便地添加事务提交后需要执行的操作,增强了代码的扩展性和复用性。

有个注意的点,就是确保invokeAfterCommit的稳健性,代码里是通过捕获异常打印日志,避免对其它操作有影响。

3. 扩展:长事务

长事务指的是在数据库管理和应用开发中,持续时间较长的事务处理过程。一般来说,在分布式应用中,每个服务器分配的连接数是有限的,比如每个服务器20个连接,这就要求我们必要尽量减少长事务,以便处理更多请求。

典型的方案有:

1)非事务类操作,就放在事务外面。比如前置处理,先请求下游获取资源,做各种校验,全部通过后,再启动事务。还有就是使用hook的方式,等事务提交后,再请求外部耗时的服务。

2)事务拆分。把一个长事务拆分为多个短事务。

3)异步处理。有点类似hook的方案。

4. 结束语

Spring事务管理提供了强大而灵活的机制来处理复杂的业务逻辑,但是每个特性和工具的使用都需要对其行为有深入的理解,而不能想当然。比如文中的afterCommit就是这样一个典型例子。

自定义事务模板的实践向我们展示了,虽然@Transcation注解很方便,但在一些特殊场景下,需要我们深入了解框架的工作原理并结合实际业务需求,既高效地利用Spring提供的工具,同时也规避潜在的坑点。

希望本文能够帮助读者更好地理解和应用Spring事务管理中的afterCommit钩子,以及如何在对资源或性能要求很严格的情况下,比如支付场景,如何定义自己的事务模板,帮助我们构建更健壮、更高效的应用。

这是《百图解码支付系统设计与实现》专栏系列文章中的第(30)篇。和墨哥(隐墨星辰)一起深入解码支付系统的方方面面。

欢迎转载。

Github(PDF文档全集,不定时更新):GitHub - yinmo-sc/Decoding-Payment-System-Book: 百图解码支付系统设计与实现

精选

专栏地址百图解码支付系统设计与实现
《百图解码支付系统设计与实现》专栏介绍
《百图解码支付系统设计与实现》专栏大纲及文章链接汇总(进度更新于2023.2.4)
领域相关(部分)
支付行业黑话:支付系统必知术语一网打尽
跟着图走,学支付:在线支付系统设计的图解教程
图解收单平台:打造商户收款的高效之道
图解结算平台:准确高效给商户结款
图解收银台:支付系统承上启下的关键应用
图解支付引擎:资产流动的枢纽
图解渠道网关:不只是对接渠道的接口(一)

技术专题(部分)
交易流水号的艺术:掌握支付系统的业务ID生成指南
揭密支付安全:为什么你的交易无法被篡改
金融密语:揭秘支付系统的加解密艺术
支付系统日志设计完全指南:构建高效监控和问题排查体系的关键基石
避免重复扣款:分布式支付系统的幂等性原理与实践
支付系统的心脏:简洁而精妙的状态机设计与核心代码实现
精确掌控并发:固定时间窗口算法在分布式环境下并发流量控制的设计与实现
精确掌控并发:滑动时间窗口算法在分布式环境下并发流量控制的设计与实现

相关文章:

Spring事务模板及afterCommit存在的坑

大家好&#xff0c;我是墨哥&#xff08;隐墨星辰&#xff09;。今天的内容来源于两个线上问题&#xff0c;主要和大家聊聊为什么支付系统中基本只使用事务模板方法&#xff0c;而不使用声明式事务Transaction注解&#xff0c;以及使用afterCommit()出现连接未按预期释放导致的…...

【区块链】联盟链

区块链中的联盟链 写在最前面**FAQs** 联盟链&#xff1a;区块链技术的新兴力量**联盟链的定义****联盟链的技术架构**共识机制智能合约加密技术身份认证 **联盟链的特点**高效性安全性可控性隐私保护 **联盟链的应用场景****金融服务****供应链管理****身份验证****跨境支付**…...

Oracle case when end和decode的区别

Oracle中的CASE WHEN和DECODE都是条件表达式&#xff0c;但它们在某些方面有所不同。 CASE WHEN&#xff1a; CASE WHEN是一个条件表达式&#xff0c;允许您基于条件返回不同的值。它具有以下结构&#xff1a; sql CASE WHEN condition1 THEN result1 WHEN condition2 THE…...

Java导出pdf格式文件

Java实现导出pdf &#xff5c;word &#xff5c;ppt 格式文件 controller层&#xff1a; ApiOperation("导出")GetMapping("/download")public void download(RequestParam("userId") Long userId ,HttpServletResponse response) {reportResul…...

Socket、UDP、TCP协议和简单实现基于UDP的客户端服务端

目录 Socket TCP和UDP区别 UDP&#xff1a;无连接&#xff0c;不可靠传输&#xff0c;面向数据报&#xff0c;全双工 TCP&#xff1a;有连接&#xff0c;可靠传输&#xff0c;面向字节流&#xff0c;全双工 无连接和有连接 可靠传输和不可靠传输 面向数据报和面向字节流…...

发布订阅模式:观察者模式的一种变体

发布-订阅模型&#xff08;Publish-Subscribe Model&#xff09;的底层机制通常基于观察者模式。 发布-订阅模型是观察者模式的一种变体。 在观察者模式中&#xff0c;主题&#xff08;或被观察者&#xff09;维护了一组观察者&#xff0c;当主题的状态发生变化时&#xff0c…...

TiDB离线部署、Tiup部署TiDB

先做tidb准备工作&#xff1a; 部署 TiDB 前的环境检查操作&#xff1a;TiDB 环境与系统配置检查 | PingCAP 文档中心 1.查看数据盘 fdisk -l &#xff08;2,3&#xff09;本人的分区已经是 ext4 文件系统不用分区&#xff0c;具体官方文档的分区&#xff1a; 4.查看数据盘…...

10GBase-T万兆电口模块助力数据中心实现高效数据传输

10GBase-T万兆电口模块一种高速、高效的网络连接解决方案&#xff0c;具有快速传输速度和稳定可靠的特点。它可以在数据中心中广泛应用&#xff0c;提供出色的网络性能和可扩展性&#xff0c;为数据中心的发展做出了重要的贡献。 一、10GBase-T万兆电口模块的特点与优势 高速传…...

使用Docker中部署GitLab 避坑指南

在容器化的世界中&#xff0c;Docker已经成为了我们部署和管理应用程序的首选工具。然而&#xff0c;在使用Docker部署GitLab时&#xff0c;我们可能会遇到一些问题&#xff0c;本文将为你提供一份详细的避坑指南。网上的教程有的都没说清楚&#xff0c;或者干脆是错的。摸索了…...

我的NPI项目之设备系统启动(八) -- Android14的GKI2.0开发步骤和注意事项

GKI是什么&#xff1f; Google为什么要推行GKI&#xff1f; GKI全称General Kernel Image。GKI在framework和kernel之间提供了标准接口&#xff0c;使得android OS能够轻松适配/维护/兼容不同的设备和linux kernel。 Google引入GKI的目的是将Framework和Kernel进一步的解耦。因…...

鼠标右键助手专业版 MouseBoost PRO for Mac v3.3.6中文破解

MouseBoost Pro mac版是一款简单实用的鼠标右键助手专业版&#xff0c;MouseBoost Pro for Mac只要轻点你的鼠标右键&#xff0c;就可以激活你想要的各种功能&#xff0c;让你的工作效率大幅度提高&#xff0c;非常好用。 软件下载&#xff1a;MouseBoost PRO for Mac v3.3.6中…...

React学习计划-react-hooks补充

React Hooks 1. 使用hooks理由 高阶组件为了复用&#xff0c;导致代码层级复杂生命周期的复杂 2. useState(保存组件状态) const [state, setstate] useState(initialState)3. useEffect(处理副作用)和useLayoutEffect(同步执行副作用) 使用方式&#xff1a; useEffect(…...

KTV点歌系统vue+springboot音乐歌曲播放器系统

目前现有的KTV点歌系统对于用户而言其在线点歌流程仍然过于繁琐&#xff0c;对于歌曲而言其系统安全性并不能保障。同时整套系统所使用的技术相对较为落后&#xff0c;界面不能动态化展示。相比较于其它同类型网站而言不能体现技术先进性。 1.2 项目目标 KTV点歌系统的后台开发…...

vue video 多个视频切换后视频不显示的解决方法

先说一下我这边的需求是视频需要轮播&#xff0c;一个人员有多个视频&#xff0c;左右轮播是轮播某个人员下的视频&#xff0c;上下切换是切换人员。 vue 代码 <el-carouselindicator-position"none"ref"carousel"arrow"always":interval&qu…...

多态与代码屎山

到底什么是多态呢?多态是面向未来的,比如企业采购为例: 一般分为线上合线下两种, 我们设计一个父类叫做"采购", 里面做一些共通的处理: 申请, 承认, 支付, 购买方式. 然后让各自的子类(线上,线下)实现自己的方法.实际调用过程中传入不同的对象就可以.到此为止项目开…...

Git基本操作(2)

Git基本操作&#xff08;2&#xff09; 上交文件之后&#xff0c;git文件的变化git cat-file HEAD指针里面有啥文件被修改git statusgit diff 文件名 版本回退&#xff08;git reset&#xff09;撤销回退git reflog 撤销的三种情况还没有addgit checkout -- [file] 已经add还没…...

编程笔记 Golang基础 023 切片

编程笔记 Golang基础 023 切片 一、切片二、定义与初始化三、基本操作四、示例 Go语言中的切片&#xff08;slices&#xff09;是基于数组的抽象数据类型&#xff0c;它提供了一种灵活的方式来处理可变长度的数据序列。切片本身不存储任何数据&#xff0c;而是指向底层数组的一…...

qt 软件发布(Windows)

1. 开发环境 QtCreator MSVC编译器 2. 源码编译 生成release或者debug版本的exe可执行文件(x64或x86) 3. windeployqt 打包 ①左下角开始菜单栏找到QT的命令交互对话框&#xff0c;如下图MSVC 2017 64-bit(根据第二步编译的类型选择64位或者32位)。 ②cd 切换到第二步可…...

《汇编语言》- 读书笔记 - 第11章-标志寄存器

《汇编语言》- 读书笔记 - 第11章-标志寄存器 标志寄存器指令与标志位关系11.1 ZF&#xff08;Zero Flag&#xff0c;零标志位&#xff09;11.2 PF&#xff08;Parity Flag&#xff0c;奇偶标志位&#xff09;11.3 SF&#xff08;Sign Flag&#xff0c;符号标志位&#xff09;处…...

1.QT简介(介绍、安装,项目创建等)

1. QT介绍 Qt&#xff08;官方发音 [kju:t]&#xff09;是一个跨平台的C开发库&#xff0c;主要用来开发图形用户界面&#xff08;Graphical User Interface&#xff0c;GUI&#xff09;程序 Qt 是纯 C 开发的&#xff0c;正常情况下需要先学习C语言、然后在学习C然后才能使用…...

铭豹扩展坞 USB转网口 突然无法识别解决方法

当 USB 转网口扩展坞在一台笔记本上无法识别,但在其他电脑上正常工作时,问题通常出在笔记本自身或其与扩展坞的兼容性上。以下是系统化的定位思路和排查步骤,帮助你快速找到故障原因: 背景: 一个M-pard(铭豹)扩展坞的网卡突然无法识别了,扩展出来的三个USB接口正常。…...

Java 语言特性(面试系列2)

一、SQL 基础 1. 复杂查询 &#xff08;1&#xff09;连接查询&#xff08;JOIN&#xff09; 内连接&#xff08;INNER JOIN&#xff09;&#xff1a;返回两表匹配的记录。 SELECT e.name, d.dept_name FROM employees e INNER JOIN departments d ON e.dept_id d.dept_id; 左…...

java_网络服务相关_gateway_nacos_feign区别联系

1. spring-cloud-starter-gateway 作用&#xff1a;作为微服务架构的网关&#xff0c;统一入口&#xff0c;处理所有外部请求。 核心能力&#xff1a; 路由转发&#xff08;基于路径、服务名等&#xff09;过滤器&#xff08;鉴权、限流、日志、Header 处理&#xff09;支持负…...

K8S认证|CKS题库+答案| 11. AppArmor

目录 11. AppArmor 免费获取并激活 CKA_v1.31_模拟系统 题目 开始操作&#xff1a; 1&#xff09;、切换集群 2&#xff09;、切换节点 3&#xff09;、切换到 apparmor 的目录 4&#xff09;、执行 apparmor 策略模块 5&#xff09;、修改 pod 文件 6&#xff09;、…...

基于ASP.NET+ SQL Server实现(Web)医院信息管理系统

医院信息管理系统 1. 课程设计内容 在 visual studio 2017 平台上&#xff0c;开发一个“医院信息管理系统”Web 程序。 2. 课程设计目的 综合运用 c#.net 知识&#xff0c;在 vs 2017 平台上&#xff0c;进行 ASP.NET 应用程序和简易网站的开发&#xff1b;初步熟悉开发一…...

uni-app学习笔记二十二---使用vite.config.js全局导入常用依赖

在前面的练习中&#xff0c;每个页面需要使用ref&#xff0c;onShow等生命周期钩子函数时都需要像下面这样导入 import {onMounted, ref} from "vue" 如果不想每个页面都导入&#xff0c;需要使用node.js命令npm安装unplugin-auto-import npm install unplugin-au…...

c++ 面试题(1)-----深度优先搜索(DFS)实现

操作系统&#xff1a;ubuntu22.04 IDE:Visual Studio Code 编程语言&#xff1a;C11 题目描述 地上有一个 m 行 n 列的方格&#xff0c;从坐标 [0,0] 起始。一个机器人可以从某一格移动到上下左右四个格子&#xff0c;但不能进入行坐标和列坐标的数位之和大于 k 的格子。 例…...

江苏艾立泰跨国资源接力:废料变黄金的绿色供应链革命

在华东塑料包装行业面临限塑令深度调整的背景下&#xff0c;江苏艾立泰以一场跨国资源接力的创新实践&#xff0c;重新定义了绿色供应链的边界。 跨国回收网络&#xff1a;废料变黄金的全球棋局 艾立泰在欧洲、东南亚建立再生塑料回收点&#xff0c;将海外废弃包装箱通过标准…...

React19源码系列之 事件插件系统

事件类别 事件类型 定义 文档 Event Event 接口表示在 EventTarget 上出现的事件。 Event - Web API | MDN UIEvent UIEvent 接口表示简单的用户界面事件。 UIEvent - Web API | MDN KeyboardEvent KeyboardEvent 对象描述了用户与键盘的交互。 KeyboardEvent - Web…...

JDK 17 新特性

#JDK 17 新特性 /**************** 文本块 *****************/ python/scala中早就支持&#xff0c;不稀奇 String json “”" { “name”: “Java”, “version”: 17 } “”"; /**************** Switch 语句 -> 表达式 *****************/ 挺好的&#xff…...