@Transactional声明式事务回调编程

文章目录

  • 1. 理论阐述
  • 2. 代码实现
    • 2.1. 问题代码
    • 2.2. 改进方案

本文参考:

事务回调编程

大事务问题

1. 理论阐述

最近在学习数据库事务的过程中,了解到了大事务的危害:

  1. 并发情况下,数据库连接资源容易耗尽
  2. 锁定数据较多,容易造成大量阻塞和锁超时,进而接口超时
  3. 执行时间长,容易造成主从延迟
  4. 回滚所需要的时间变长

那么大事务又是如何产生的呢?

  1. 单个事务操作数据库操作较多
  2. 事务中存在 RPC/MQ 等非 DB 耗时操作
  3. 大量的锁竞争

项目编程中我们经常会用到 Spring 的声明式事务 @Transactional 注解,我去反思了下项目中对事务的使用,还真的存在事务中嵌套 MQ 的用法,比方说本地数据库操作过程中穿插着 ES 写入消息(牺牲直连写入 ES 的时效性,中间加一层 MQ 可以提升容灾性),这就容易产生大事务,整体架构如下:
在这里插入图片描述

在分布式异常场景下这种模式也是有问题的:

比方说数据库操作执行报错,或者 MQ 消息超时,本地事务需要回滚,但是 MQ 消息已经发出去了,没法执行回滚操作,这就没法保证本地事务+MQ的原子性了。

在这里插入图片描述

想一下怎么尽可能避免发送MQ但又需要回滚的场景,其实就是把发MQ消息的时机往后放放,本地事务执行成功了,才发送 MQ 消息,这样子也避免大事务中嵌套 MQ,这在业务上也是可以接受的。

在这里插入图片描述

这种做法底层避免了数据库操作失败,MQ 需要回滚但是没法回滚的困境,但仍然有它的缺点,就是仍然没法保证 “数据库操作 + MQ” 的原子性,比方说下面,数据库事务提交了之后,App 重启或者宕机了,就不会发出 MQ 消息。
这其实涉及到了分布式事务的处理策略,我们当然可以用本地消息表或者其他分布式处理策略如TCC来解决这个问题。

所以这里谈论到的策略其实并不是一种分布式事务的处理方案,重点在于优化代码结构避免长事务,同时尽量保证“数据库操作 + MQ” 的原子性。

在这里插入图片描述

2. 代码实现

2.1. 问题代码

在@Transactional 声明式事务编程中,两个 insert 操作中穿插着发送MQ消息,典型的大事务问题。

@Transactional
public void doTransaction() {log.info("start tx");User user1 = new User();user1.setId(9);user1.setAge(2);user1.setName("jxz");user1.setEmail("111@qq.com");userMapper.insert(user1);log.info("insert user1...");log.info("调用其他 RPC 或者发送 MQ 消息");User user2 = new User();user2.setId(10);user2.setAge(3);user2.setName("jxz");user2.setEmail("111@qq.com");userMapper.insert(user2);log.info("insert user2...");log.info("end tx");
}

那正如前面所说的,我们可以在数据库本地事务提交以后,再去调用 RPC 或者 MQ。这个时候代码结构是需要调整的,如果只是单纯把 RPC 或者 MQ 从 @Transactional 注解声明的方法中抽取出来,后置调用,伪代码如下:

public void doRpcAfterTransaction() {// 原先 @Transactional 声明的数据库操作,事务失效doTransaction();log.info("调用其他 RPC 或者发送 MQ 消息");
}

@Transactional 注解也会失效,因为这属于方法内部调用 @Transactional 声明的方法,Spring 不是拿到的代理对象去调用。此外这种方式还增加了代码的复杂性,改动量太大。

2.2. 改进方案

那么是否存在一种代码改动量较小,能让人一眼看懂,最好在静态上还是内嵌在原来 @Transactional 声明式事务编程中;同时还能在当前事务执行完以后,能够及时回调 RPC/MQ 等第三方调用的。

仍然声明一下,这种方案是为了尽可能保证“本地事务+RPC/MQ”的原子性,并且代码结构简单,并不是分布式事务的解决方案

Spring 提供这样的 SPI 扩展,TransactionSynchronization 就提供事务执行完成以后回调的接口。
在这里插入图片描述

其中包括多个事务回调的拓展点:

在这里插入图片描述

其中 TransactionSynchronization#afterCompletion(int status) 就会根据事务执行结果(成功 commit 或者回滚 rollback),status 入参数代表事务执行状态,其实现就会执行事务后置处理。

这一切都建立在当前方法上下文存在活跃的事务,Spring 也提供了静态方法来让我们调用判断 TransactionSynchronizationManager#isActualTransactionActive()

最终写了个工具类实现代码如下:

package com.jxz.util;import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;/*** @Author jiangxuzhao* @Description* @Date 2024/10/2*/
public class TransactionUtils {/*** 事务后置处理 api,可以优化大事务提升数据库性能,尽量保证“本地事务 + RPC/MQ”的原子性** @param runnable 事务后置处理任务*/public static void doAfterTransaction(Runnable runnable) {if (TransactionSynchronizationManager.isActualTransactionActive()) {TransactionSynchronizationManager.registerSynchronization(new DoTransactionCompletion(runnable));}}/*** 实现 TransactionSynchronization 接口,重写其中的 afterCompletion 方法*/public static class DoTransactionCompletion implements TransactionSynchronization {// 待执行的任务Runnable runnable;public DoTransactionCompletion(Runnable runnable) {this.runnable = runnable;}// 在事务 commit/rollback 以后回调@Overridepublic void afterCompletion(int status) {// 当事务状态是 COMMITTED 时if (status == TransactionSynchronization.STATUS_COMMITTED) {runnable.run();}}}
}

在原先调用的地方修改也很简单:

内嵌在 @Transactional 声明式事务中,甚至连 RPC/MQ 调用的代码位置都不需要变动,内部实现的就是事务执行完成之后的后置回调。

@Transactional
public void doTransaction2() {log.info("start tx");User user1 = new User();user1.setId(13);user1.setAge(2);user1.setName("jxz");user1.setEmail("111@qq.com");userMapper.insert(user1);log.info("insert user1...");TransactionUtils.doAfterTransaction(() ->log.info("afterCommit, 调用其他 RPC 或者发送 MQ 消息"));User user2 = new User();user2.setId(14);user2.setAge(3);user2.setName("jxz");user2.setEmail("111@qq.com");userMapper.insert(user2);log.info("insert user2...");log.info("end tx");
}

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

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

相关文章

用java做一个简易版球球大作战

该界面模拟了一个简单的“吃球”游戏,一开始多个球在屏幕上移动,并检查每个大球是否可以吃掉其他小球,且更新状态,删除已经被吃掉的小球。通过图形绘制和逻辑处理实现了游戏的基本功能。 主界面World.java package gzeu.test.da…

边缘自适应粒子滤波(Edge-Adaptive Particle Filter)的MATLAB函数示例,以及相应的讲解

目录 讲解 初始化 预测步骤 观测模拟 权重更新 重采样 状态估计 总结 下面是一个简单的边缘自适应粒子滤波()的函数示例,以及相应的讲解。 程序源代码: function X_est edgeAdaptiveParticleFilter(numParticles, numS…

RabbitMQ(学习前言)

目录 学习MQ之前有必要先去温故下微服务知识体系,以加深本章节的理解 一、微服务间的通讯方式 1. 基本介绍 2. 同步通讯 2.1. 什么是同步通讯 2.2. 同步通讯存在的问题 问题一:耦合度高 问题二:性能和吞吐能力下降 问题三&#xff1a…

在线Xpath匹配定位测试工具

具体请前往:在线Xpath-匹配-定位-调试/测试工具

一文看懂计算机中的大小端(Endianess)

文章目录 前言一、什么是大小端二、如何判断大小端三、大小端的转换3.1 使用标准库函数3.2 手动实现大小端转换 前言 本文主要探讨计算机中大小端的相关概念以及如何进行大小端的判断和转换等。 一、什么是大小端 大小端(Endianess)是指计算机系统在存…

【AI学习笔记】基于Unity+DeepSeek开发的一些BUG记录解决方案

【AI学习笔记】基于UnityDeepSeek开发的一些BUG记录&解决方案 背景前摇:(省流可不看) Unity是大学学的,AI是研究生学的,DeepSeek是第一份实习偷师的,三合一的梦是最近开始做的,BUG是今天遇…

【数据结构】什么是哈希表(散列表)?

🦄个人主页:修修修也 🎏所属专栏:数据结构 ⚙️操作环境:Visual Studio 2022 目录 📌哈希表的概念 📌哈希函数的构造方法 🎏直接定址法 🎏除留余数法 🎏平方取中法 🎏折叠法 &#x…

Bolt.new:终极自动化编程工具

兄弟们,终极写代码工具来了—— Bolt.new!全方位的编程支持: StackBlitz 推出了 Bolt․new,这是一款结合了 AI 与 WebContainers 技术的强大开发平台,允许用户快速搭建并开发各种类型的全栈应用。 它的主要特点是无需…

前端reactvue3——实现滚动到底加载数据

文章目录 ⭐前言⭐react 实现滚动加载⭐vue3 实现滚动加载⭐总结⭐结束 ⭐前言 大家好,我是yma16,本文分享 前端react&vue3——实现滚动加载(到底部加载) scrollTop 属性 一个双精度浮点值,表示元素当前从原点垂直…

安全运营中心 (SOC) 团队对其安全工具感到失望

Vectra AI 表示,安全运营中心 (SOC) 从业人员认为,由于太多孤立的工具和缺乏准确的攻击信号,他们在检测和确定真实威胁的优先级方面正在失败。 人们对供应商的不信任感日益加深,认为供应商的工具在发现真正的攻击方面起的阻碍作用…

ctfshow-web入门(信息收集,持续更新中。。)

写在之前:近期打了个比赛,备受打击,入手了vip账号进修,加油! 文章目录 ctfshow-web1查看源代码ctfshow-web2burp抓包ctfshow-web3burp抓包ctfshow-web4访问robots.txtctfshow-web5dirscarch扫描PHPS文件泄露ctfshow-web6dirscarch扫描ctfshow-web7dirscarch扫描ctfshow-w…

【STM32开发之寄存器版】(六)-通用定时器中断

一、前言 STM32定时器分类 STM32103ZET6具备8个定时器TIMx(x 1,2,...,8)。其中,TIM1和TIM8为高级定时器,TIM2-TIM6为通用定时器,TIM6和TIM7为基本定时器,本文将以TIM3通用定时器为例,分析STM32定时器工作的底层寄存器…

You must konw JS!!(超详细的javascript套餐,适合计算机专业有基础的,包含常见前端开发面试题)

1.起源 JavaScript 起源于 1995 年,当时它主要是为了满足网页交互的需求而被创建。它最初的设计目的是为了让网页开发者能够在网页中添加一些简单的交互效果和动态内容。在那个时期,网页大多是静态的,而 JavaScript 的出现为网页带来了新的活…

jmeter学习(7)beanshell

beanshell preprocessor 发送请求前执行 beanshell postprocessor 发送请求前执行 获取请求相关信息 String body sampler.getArguments().getArgument(0).getValue(); String url sampler.getPath(); 获取响应报文 String responseprev.getResponseDataAsString(); 获…

CMake 教程跟做与翻译

目录 STEP 1: 入门与理解 cmake_minimum_required设置CMake版本的最小值 project声明工程属性 add_executable添加可执行文件 使用CMake构建工程 根据自己的构建工具自行构建 Reference STEP 1: 入门与理解 我们起手的,最基本的 CMake 项目是从单个源代码文件…

【Blender Python】1.概述和基础使用

概述 众所周知,Blender是一款开源免费的3D建模软件(当然不限于3D建模)。在Blender中,可以使用其内置的Python解释器执行Python代码,用于程序化的生成网格以及其他内容。你可以基于此创建Blender插件。 这个系列就是快…

Electron桌面应用打包现有的vue项目

1 环境准备 Node:v16.20.2(本地vue项目nodejs版本)Electron:22.3.7vue:2 版本管理 2 Vue项目准备 更新相关依赖npm install --registry https://registry.npmmirror.com/npm run dev 3、引入Electorn 安装指定版…

算法剖析:双指针

文章目录 双指针算法一、 移动零1. 题目2. 算法思想3. 代码实现 二、 复写零1. 题目2. 算法思想3. 代码实现 三、 快乐数1. 题目2. 算法思想3. 代码实现 四、 盛水最多的容器1. 题目2. 算法思想3. 代码实现 五、有效三角形的个数1. 题目2. 算法思想3. 代码实现 六、 和为 s 的两…

UART驱动学习三(TTY驱动部分源码解析)

目录 全局框架图一、tty_io.c 分析1. 关键数据结构和定义2. 文件操作结构体3. 初始化和注册4. 读写操作5. 挂起和恢复6. 信号处理7. 设备类8. 控制台通知9. 辅助函数10. 代码功能11. 带有注释的部分tty_io.c源码 二、tty_ldisc.c 分析1. 关键数据结构和定义2. 行规程操作函数3.…

Android车载——VehicleHal运行流程(Android 11)

1 概述 本篇主要讲解VehicleHal的主要运行流程,包括设置属性、获取属性、订阅属性、取消订阅、持续上报属性订阅等。 2 获取属性流程 2.1 获取属性流程源码分析 作为服务注册到hwServiceManager中的类是VehicleHalManager,所以,CarServic…