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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/264010.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

JAVA算法和数据结构

一、Arrays类 1.1 Arrays基本使用 我们先认识一下Arrays是干什么用的&#xff0c;Arrays是操作数组的工具类&#xff0c;它可以很方便的对数组中的元素进行遍历、拷贝、排序等操作。 下面我们用代码来演示一下&#xff1a;遍历、拷贝、排序等操作。需要用到的方法如下 public…

APP的UI自动化demo(appium+java)

文章目录 appium连接手机java代码实现-第一版第二版-接入testng和隐式等待显示等待 appium连接手机 准备工作 1、查看连接手机模拟器是否连接成功&#xff0c;获取设备名称 执行命令&#xff1a;adb devices 2、查看android内核版本号—>paltformVersion 执行命令&#xf…

springBoot整合Redis(一、Jedis操作Redis)

在springboot环境下连接redis的方法有很多&#xff0c;首先最简单的就是直接通过jedis类来连接&#xff0c;jedis类就相当于是redis的客户端表示。 但是因为现在比较常用的是&#xff1a;StringRedisTemplate和RedisTemplate&#xff0c;所以jedis只做简单的介绍。 一、Jedis…

2023 龙蜥操作系统大会演讲实录:《兼容龙蜥的云原生大模型数据计算系统——πDataCS》

本文主要分三部分内容&#xff1a;第一部分介绍拓数派公司&#xff0c;第二部分介绍 πDataCS 产品&#xff0c;最后介绍 πDataCS 与龙蜥在生态上的合作。 杭州拓数派科技发展有限公司&#xff08;简称“拓数派”&#xff0c;英文名称“OpenPie”&#xff09;是国内基础数据计…

文献阅读:Transformers are Multi-State RNNs

文献阅读&#xff1a;Transformers are Multi-State RNNs 1. 内容简介2. 方法介绍 1. 基础回顾 1. RNN2. Transformer 2. Transformer解构 1. MSRNN2. Transformer 3. TOVA 1. 现有转换策略2. TOVA 3. 实验考察 & 结论 1. 实验设计2. 实验结果 1. LM2. 长文本理解3. 文本生…

仗剑天涯路 侠气传千古《有翡》湖北热血开播

由吴锦源执导&#xff0c;赵丽颖、王一博领衔主演&#xff0c;张慧雯、陈若轩、孙坚、周洁琼、张昕宇、冷纪元主演的古装武侠剧《有翡》&#xff0c;将于2月25日晚19:30登陆湖北卫视长江剧场。该剧改编自Priest小说《有匪》&#xff0c;讲述了南北朝年间&#xff0c;各方势力盘…

modbus-tcp协议详解

本文参考&#xff1a;Modbus协议中文版【完整版】.pdf&#xff0c;加上自己的理解的记录&#xff0c;该文章主要讲modbus-TCP协议。&#xff08;文档下载链接&#xff1a;【免费】modbus协议中文详细解释文档资源-CSDN文库&#xff09; 本系列文章分为三章&#xff1a; 1.mod…

Stable Diffusion 绘画入门教程(webui)-ControlNet(深度Depth)

上篇文章介绍了线稿约束&#xff0c;这篇文章介绍下深度Depth 文章目录 一、选大模型二、写提示词三、基础参数设置四、启用ControlNet 顾名思义&#xff0c;就是把原图预处理为深度图&#xff0c;而深度图可以区分出图像中各元素的远近关系&#xff0c;那么啥事深度图&#xf…

【README 小技巧】在项目README.md 中展示发布到使用的JDK 版本

在项目README.md 中展示发布到使用的JDK 版本 <a target"_blank" href"https://www.oracle.com/java/technologies/javase/jdk11-archive-downloads.html"><img src"https://img-home.csdnimg.cn/images/20230724024159.png?origin_urlhtt…

分布式知识整理

分布式锁 以商场系统超卖现象举例 超卖现象一 现象&#xff1a; 商品卖出数量超出了库存数量。 产生原因&#xff1a; 扣减库存的动作在程序中进行&#xff0c;在程序中计算剩余库存&#xff0c;在并发场景下&#xff0c;导致库存计算错误。 代码复现 es.shutdown(); cycl…

智慧应急与物联网相结合:物联网技术如何提升智慧应急响应能力

目录 一、引言 二、智慧应急与物联网技术的结合 三、物联网技术提升智慧应急响应能力的途径 四、物联网技术在智慧应急中的应用案例 五、物联网技术在智慧应急中面临的挑战与解决方案 挑战一&#xff1a;技术标准与规范不统一 解决方案&#xff1a; 挑战二&#xff1a;…

Linux之JAVA环境配置jdkTomcatMySQL

目录 一. 安装jdk 1.1 查询是否有jdk 1.2 解压 1.3 配置环境变量 二. 安装Tomcat&#xff08;开机自启动&#xff09; 2.1 解压 2.2 启动tomcat 2.3 防火墙设置 2.4 创建启动脚本&#xff08;设置自启动&#xff0c;服务器开启即启动&#xff09; 三. MySQL安装&#xff08;…

国漫年番成趋势?但只有这5部最值得看

自从《斗罗大陆》动画爆火之后&#xff0c;越来越多国漫都开始以年番形式播出&#xff0c;每周都能追自己喜欢的动画也是观众们所期待的。但其实年番对制作公司的要求很高&#xff0c;如果技术跟不上难免出现质量下滑的问题。今天就带大家盘点一下目前在播的最值得看的5部国漫年…

查看navicat保存的数据库连接密码

背景 经常使用navicat的朋友可能会碰到忘记数据库连接密码的情况&#xff0c;自然会想到navicat连接配置中就保存了密码。 个人经验&#xff0c;按以下步骤可查看密码明文 本人在mac上使用的navicat版本 1&#xff0c;导出connection_local.ncx 点击OK导出保存为connection_l…

基于频率增强的数据增广的视觉语言导航方法(VLN论文阅读)

基于频率增强的数据增广的视觉语言导航方法&#xff08;VLN论文阅读&#xff09; 摘要 视觉和语言导航&#xff08;VLN&#xff09;是一项具有挑战性的任务&#xff0c;它需要代理基于自然语言指令在复杂的环境中导航。 在视觉语言导航任务中&#xff0c;之前的研究主要是在空间…

Android RecyclerView 如何展示自定义列表 Kotlin

Android RecyclerView 如何展示自定义列表 Kotlin 一、前提 有这么一个对象 class DeviceDemo (val name: String, val type: String, val address: String)要展示一个包含这个对象的列表 bluetoothDevices.add(DeviceDemo("bb 9800", "LE", "32:…

Linux 内存管理概述(偏实战,略理论,附链接)

基础理论 1. 内存映射 可以参考&#xff1a; Linux内存映射 - 知乎 写的很详细&#xff0c;而且也有代码分析 2. 虚拟内存的空间分布 通过这张图你可以看到&#xff0c;用户空间内存&#xff0c;从低到高分别是五种不同的内存段。只读段&#xff0c;包括代码和常量等。数据段…

Unity学习之Unity中的MVC思想

文章目录 1 前言2 MVC的基本概念3 不使用MVC思想制作UI逻辑3.1 拼面板3.2 面板脚本3.3 角色面板逻辑3.4 角色升级 4 使用MVC思想制作UI逻辑4.1 Model数据脚本4.2 View界面脚本4.2.1 MainView主界面4.2.2 RoleView 角色面板界面 4.3 Controller业务逻辑脚本4.3.1 MainController…

使用openai-whisper实现语音转文字

使用openai-whisper实现语音转文字 1 安装依赖 1.1 Windows下安装ffmpeg FFmpeg是一套可以用来记录、转换数字音频、视频&#xff0c;并能将其转化为流的开源计算机程序。采用LGPL或GPL许可证。它提供了录制、转换以及流化音视频的完整解决方案。 # ffmpeg官网 https://ffm…

2024年2月20日v1.0.5更新·优雅草便民工具youyacao-tools

2024年2月20日v1.0.5更新优雅草便民工具youyacao-tools apk下载 https://fenfacun.youyacao.com/tools105.apk 介绍 优雅草便民工具是一款由成都市一颗优雅草科技有限公司打造的便民查询公益工具&#xff0c;2024年1月17日正式发布v1.0.0版本&#xff0c;本工具为了方便大众免…