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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Smuggler:一款功能强大的HTTP请求走私和去同步安全测试工具

关于Smuggler Smuggler是一款功能强大的HTTP请求走私和去同步安全测试工具&#xff0c;该工具基于纯Python 3开发&#xff0c;可以帮助广大研究人员针对应用程序的HTTP协议执行安全分析和测试。 工具安装 由于该工具基于Python 3开发&#xff0c;因此我们首先需要在本地设备上…

【学习记录】HC32F460USB——U盘IAP升级app

从头开始&#xff0c;万物从解压开始 直奔猪蹄&#xff0c;找到usb下的工程文件 开始移植 主要移植IAP的boot和fatfs的文件系统&#xff0c;fatfs官网去下载ff15.0版本&#xff0c;目前用这个 放到项目里 添加到工程文件中 改引脚&#xff0c;给USB放电 编译&#xff0c;可以…

GCC如何产生core dump

先决条件 1.安装apport&#xff08;automatically generate crash reports for debugging&#xff09; 2.修改/etc/security/limits.conf文件&#xff0c;使允许core dump&#xff0c;或者用ulimit -c unlimited设置core dump文件的大小为unlimited&#xff08;临时方案&#x…

生成voc格式数据集

数据集存放格式&#xff1a;&#xff08;Annotations文件夹放标注的xml文件&#xff0c;JPEGImages文件夹放标注的图片&#xff09; 运行代码&#xff1a; import os import random import xml.etree.ElementTree as ETimport numpy as npdef get_classes(classes_path):with …

Java中的时间API:Date、Calendar到Java.time的演变

引言 在软件开发中&#xff0c;处理时间和日期是一项基本且不可或缺的任务。无论是日志记录、用户信息管理还是复杂的定时任务&#xff0c;准确地处理时间都显得至关重要。然而&#xff0c;时间的处理并不像它看起来那么简单&#xff0c;尤其是当我们考虑到时区、夏令时等因素…

计算机组成原理-第一/二章 概述和数据的表示和运算【期末复习|考研复习】

文章目录 前言第一章 计算机组成原理 概述及各种码1.1 计算机硬件的基本组成1.1.1 存储器1.1.2 运算器1.1.3 控制器 1.2 计算机的工作过程1.3 计算机的性能指标1.4 各个字长区别与联系 第二章 数据的表示与运算2.1 ASCII码2.2 各种码2.3 浮点数 总结 前言 给大家整理了一下计算…

域控操作七:让某人不执行某策略/单独放行某人

比如我设置的是禁用移动热点&#xff0c;我在这里对某人拒绝&#xff0c;那他就能用移动热点

Qt6内嵌CEF

一、下载CEF CEF下载地址&#xff1a;https://cef-builds.spotifycdn.com/index.html 或https://bitbucket.org/chromiumembedded/cef/src/master/ 选择对应系统的版本&#xff08;本教程选择的是116.0.19&#xff09; CMake下载地址&#xff1a;https://cmake.org/download…

大数据界面:客户又又又要求科技感了,如何破?

如果你问客户想要什么风格&#xff0c;大部分脱口而出科技感&#xff0c;不仅要求静态页&#xff0c;而且还要求动态效果&#xff0c;炫酷动画&#xff0c;贝格前端工场结合多个项目经历&#xff0c;帮助友友们梳理如何让界面科技动感。 一、没有科技感背后的潜台词 客户说大数…

SI523 替代CLRC522/RC523,软硬件开发资料

​si523是一个 13.56MHz 的非接触式读写器芯片&#xff0c;支持 ISO/IEC 14443 A/B/MIFARE协议。内部集成低功耗自动寻卡与定时唤醒功能&#xff0c;可编程寻卡时间间隔&#xff0c;寻卡过程无需 MCU 操作&#xff0c;寻卡成功中断唤醒或定时唤醒 MCU 以实现低功耗。 产品特性…

笑营宝高校选修课报名考勤系统源码开发方案

一、项目背景与目标 &#xff08;一&#xff09;项目背景 随着高等教育的普及和教学模式的不断创新&#xff0c;高校选修课程体系日趋复杂多变。学生对课程选择的自由度提高&#xff0c;使得传统的选课和考勤管理方式变得繁琐且效率低下。目前&#xff0c;许多高校仍然采用纸…

金三银四,自动化测试面试题精选【美团二面】

面试一般分为技术面和hr面&#xff0c;形式的话很少有群面&#xff0c;少部分企业可能会有一个交叉面&#xff0c;不过总的来说&#xff0c;技术面基本就是考察你的专业技术水平的&#xff0c;hr面的话主要是看这个人的综合素质以及家庭情况符不符合公司要求&#xff0c;一般来…

Nginx之rewrite重写功能

目录 一、rewrite概述 1、rewrite功能 2、跳转场景 二、标准配置指令 1、rewrite日志记录指令 2、未初始化变量告警日志记录指令 3、rewrite 指令 3.1 正则表达式 三、rewrite模块使用实例 1.基于域名的跳转 2.基于客户端 IP 访问跳转 3. 基于旧域名跳转到新域名后面…

Spring Cloud2022之OpenFeign使用以及部分源码分析

OpenFeign使用 Feign和OpenFeign Feign是Netflix开发的⼀个轻量级RESTful的HTTP服务客户端&#xff0c;可以使用⽤它来发起请求&#xff0c;进行远程调用。Fegin是以Java接口注解的⽅式调⽤Http请求&#xff0c;而不是像RestTemplate那样&#xff0c;在Java中通过封装HTTP请求…

【茶话数据结构】查找最短路径——Dijkstra算法详解(保姆式详细图解,步步紧逼,保你学会)

&#x1f4af; 博客内容&#xff1a;【茶话数据结构】查找最短路径——Dijkstra算法详解 &#x1f600; 作  者&#xff1a;陈大大陈 &#x1f989;所属专栏&#xff1a;数据结构笔记 &#x1f680; 个人简介&#xff1a;一个正在努力学技术的准前端&#xff0c;专注基础和实…

d3dcompiler_47.dll丢失的解决方法

d3dcompiler_47.dll 是一个属于 Microsoft DirectX 软件组件的动态链接库 (DLL) 文件。DirectX 是微软开发的一套广泛应用于 Windows 操作系统上的应用程序接口 (API)&#xff0c;主要用于处理多媒体、特别是与游戏和高级图形相关的任务&#xff0c;例如二维 (2D) 和三维 (3D) …

Aigtek高精度电流源仪器设计规范

高精度电流源仪器是一种用于产生和测量精确电流的设备&#xff0c;广泛应用于电子、通信、自动控制等领域。为了确保仪器的性能和可靠性&#xff0c;设计过程中需要遵循一些规范。 电流源仪器的设计要注重稳定性。稳定性是保证仪器输出电流精度的关键因素。设计过程中应选择高精…

稀疏图带负边的全源最短路Johnson算法

BellmanFord算法 Johnson算法解决的问题 带负权的稀疏图的全源最短路 算法流程 重新设置的每条边的权重都大于或等于0&#xff0c;跑完Djikstra后得到的全源最短路&#xff0c;记得要还原&#xff0c;即&#xff1a;f(u,v) d(u,v) - h[u] h[v] 例题

vue 解决:点击左侧相同菜单,右侧页面不重新加载的问题

1、问题描述&#xff1a; 其一、需求为&#xff1a; 无论是通过路由组件形成的平台管理系统&#xff0c;还是通过文件配置形成的平台管理系统&#xff0c;都存在通过切换左侧的导航栏而使右侧的页面切换的业务需求&#xff1b; 其二、问题描述为&#xff1a; A、步骤一&#…

【Unity】如何在Unity 中创建带有缩放效果的滚动视图(具有吸附效果的实现与优化)?

效果预览&#xff1a; 目录 效果预览&#xff1a; 一、引言&#xff1a; 二、问题描述 三、解决方案&#xff1a; 三、优化&#xff1a; 四、结论 一、引言&#xff1a; 在Unity开发中&#xff0c;经常需要实现滚动视图&#xff08;ScrollView&#xff09;中的内容吸附到…