如何优雅地Spring事务编程

本文已收录至Github,推荐阅读 👉 Java随想录

微信公众号:Java随想录

在开发中,有时候我们需要对 Spring 事务的生命周期进行监控,比如在事务提交、回滚或挂起时触发特定的逻辑处理。那么如何实现这种定制化操作呢?

Spring 作为一个高度灵活和可扩展的框架,早就提供了一个强大的扩展点,即事务同步器 TransactionSynchronization 。通过 TransactionSynchronization ,我们可以轻松地控制事务生命周期中的关键阶段,实现自定义的业务逻辑与事务管理的结合。

package org.springframework.transaction.support;import java.io.Flushable;public interface TransactionSynchronization extends Flushable {/** 事务提交状态 */int STATUS_COMMITTED = 0;/** 事务回滚状态 */int STATUS_ROLLED_BACK = 1;/**系统异常状态 */int STATUS_UNKNOWN = 2;//挂起该事务同步器default void suspend() {}//恢复事务同步器default void resume() {}//flush底层的session到数据库default void flush() {}// 事务提交之前default void beforeCommit(boolean readOnly) {}// 操作完成之前(包含commit/rollback)default void beforeCompletion() {}// 事务提交之后default void afterCommit() {}// 操作完成之后(包含commit/rollback)default void afterCompletion(int status) {}
}

TransactionSynchronization 是一个接口,它里面定义了一系列与事务各生命周期阶段相关的方法。比如,我们可以这样使用:

public class UserService {@Transactional(rollbackFor = Exception.class)public void saveUser(User user) {TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {@Overridepublic void afterCommit() {System.out.println("saveUser事务已提交...");}});userDao.saveUser(user);}
}

在 Spring 事务刚开始的时候,我们向 TransactionSynchronizationManager 事务同步管理器注册了一个事务同步器,事务提交前/后,会遍历执行事务同步器中对应的事务同步方法(一个 Spring 事务可以注册多个事务同步器)。

需要注意的是注册事务同步器必须得在一个 Spring 事务中才能注册,否则会抛出 Transaction synchronization is not active 这个错误。

isSynchronizationActive() 方法用来判断当前是否存在事务(判断线程共享变量,是否存在 TransactionSynchronization)

Spring 在创建事务的时候,会初始化一个空集合放到 synchronizations 属性中,所以只要当前存在事务,isSynchronizationActive() 就为 true。

TransactionSynchronizationManager 解析

Spring 对于事务的管理都是基于 TransactionSynchronizationManager 这个类,先看下 TransactionSynchronizationManager 的一些属性:

    private static final ThreadLocal<Map<Object, Object>> resources = new NamedThreadLocal("Transactional resources");private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations = new NamedThreadLocal("Transaction synchronizations");private static final ThreadLocal<String> currentTransactionName = new NamedThreadLocal("Current transaction name");private static final ThreadLocal<Boolean> currentTransactionReadOnly = new NamedThreadLocal("Current transaction read-only status");private static final ThreadLocal<Integer> currentTransactionIsolationLevel = new NamedThreadLocal("Current transaction isolation level");private static final ThreadLocal<Boolean> actualTransactionActive = new NamedThreadLocal("Actual transaction active");
  • resources:保存连接资源,因为一个方法里面可能包含多个事务,所以就用 Map 来保存资源, key为 DataSource,value 为connectionHolder。线程可以通过该属性获取到同一个 Connection 对象。
  • synchronizations:事务同步器,是 Spring 交由程序员进行扩展的代码,每个线程可以注册N个事务同步器。
  • currentTransactionName:事务的名称。
  • currentTransactionReadOnly:事务是否是只读。
  • currentTransactionIsolationLevel:事务的隔离级别。
  • actualTransactionActive:用于保存当前事务是否还是 Active 状态(事务是否开启)。

Spring 创建事务时,DataSourceTransactionManager.doBegin 方法中,将新创建的 connection 包装成 connectionHolder ,通过 TransactionSynchronizationManager#bindResource 方法存入 resources 中。

然后标注到一个事务当中的其它数据库操作就可以通过 TransactionSynchronizationManager#getResource 方法获取到这个连接。

    @Nullablepublic static Object getResource(Object key) {Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key);Object value = doGetResource(actualKey);if (value != null && logger.isTraceEnabled()) {logger.trace("Retrieved value [" + value + "] for key [" + actualKey + "] bound to thread [" + Thread.currentThread().getName() + "]");}return value;}@Nullableprivate static Object doGetResource(Object actualKey) {Map<Object, Object> map = (Map)resources.get();if (map == null) {return null;} else {Object value = map.get(actualKey);if (value instanceof ResourceHolder && ((ResourceHolder)value).isVoid()) {map.remove(actualKey);if (map.isEmpty()) {resources.remove();}value = null;}return value;}}

从上面我们也能看到,Spring 对于多个数据库操作的事务实现是基于 ThreadLocal 的,所以 Spring 事务操作是无法使用多线程的。

应用场景

TransactionSynchronization 可以用于一些需要在事务结束后执行清理操作或其他相关任务的场景。

应用场景举例:

  • 资源释放:在事务提交或回滚后释放资源,如关闭数据库连接、释放文件资源等。
  • 日志记录:在事务结束后记录相关日志信息,例如记录事务的执行结果或异常情况。
  • 缓存更新:在事务完成后更新缓存数据,保持缓存和数据库数据的一致性。
  • 消息通知:在事务结束后发送消息通知相关系统或用户,如发送邮件或短信通知。

举例: 假设一个电商系统中存在订单支付的业务场景,当用户支付订单时,需要在事务提交后发送订单支付成功的消息通知给用户。

由于事务是和数据库连接相绑定的,如果把发送消息和数据库操作放在一个事务里面。当发送消息时间过长时会占用数据库连接,所以就要把数据库操作与发送消息到 MQ 解耦开来。

这时就可以通过 TransactionSynchronization 来实现在事务提交后发送消息通知的功能。具体示例代码如下:

@Component
public class OrderPaymentNotification implements TransactionSynchronization {private String orderNo;public OrderPaymentNotification(String orderNo) {this.orderNo = orderNo;}@Overridepublic void beforeCommit(boolean readOnly) {// 在事务提交前不执行任何操作}@Overridepublic void beforeCompletion() {// 在事务即将完成时不执行任何操作}@Overridepublic void afterCommit() {// 在事务提交后发送订单支付成功的消息通知sendMessage("订单支付成功", orderNo);}@Overridepublic void afterCompletion(int status) {// 在事务完成后不执行任何操作}private void sendMessage(String message, String orderNo) {// 发送消息通知的具体实现逻辑System.out.println(message + ": " + orderNo);}
}
    @Transactionalpublic void finishOrder(String orderNo) {// 修改订单成功updateOrderSuccess(orderNo);// 发送消息到 MQTransactionSynchronizationManager.registerSynchronization(new OrderPaymentNotification(orderNo));}

这样当事务成功提交之后,就会把消息发送给 MQ,并且不会占用数据库连接资源。

@TransactionalEventListener

在 Spring Framework 4.2版本后还可以使用 @TransactionalEventListener 注解处理数据库事务提交成功后的执行操作。

@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@EventListener
public @interface TransactionalEventListener {TransactionPhase phase() default TransactionPhase.AFTER_COMMIT;// 表明若没有事务的时候,对应的event是否需要执行,默认值为false表示,没事务就不执行了。boolean fallbackExecution() default false;@AliasFor(annotation = EventListener.class,attribute = "classes")Class<?>[] value() default {};@AliasFor(annotation = EventListener.class,attribute = "classes")Class<?>[] classes() default {};String condition() default "";
}public enum TransactionPhase {// 在事务commit之前执行BEFORE_COMMIT,// 在事务commit之后执行AFTER_COMMIT,// 在事务rollback之后执行AFTER_ROLLBACK,// 在事务完成后执行(包括commit/rollback)AFTER_COMPLETION;private TransactionPhase() {}
}

从命名上可以直接看出,它就是个 EventListener,效果跟 TransactionSynchronization 一样,但比 TransactionSynchronization 更加优雅。它的使用方式如下:

@Data
public class Order {private Long orderId;private String orderNumber;private BigDecimal totalAmount;
}@Service
public class OrderService {@Autowiredprivate OrderRepository orderRepository;@Autowiredprivate ApplicationEventPublisher eventPublisher;@Transactionalpublic void createOrder(Order order) {// 保存订单逻辑System.out.println("Creating order: " + order.getOrderNumber());orderRepository.save(order);// 发布订单创建事件OrderCreatedEvent orderCreatedEvent = new OrderCreatedEvent(order);eventPublisher.publishEvent(orderCreatedEvent);}
}@Getter
@Setter
public class OrderCreatedEvent {private Order order;public OrderCreatedEvent(Order order) {this.order = order;}
}@Component
@Slf4j
public class OrderEventListener {@Autowiredprivate EmailService emailService;/** @Async加了就是异步监听,没加就是同步(启动类要开启@EnableAsync注解)* 可以使用@Order定义监听者顺序,默认是按代码书写顺序* 可以使用SpEL表达式来设置监听器生效的条件* 监听器可以看做普通方法,如果监听器抛出异常,在publishEvent里处理即可*/@Async@Order(1)@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT, classes = OrderCreatedEvent.class)public void onOrderCreatedEvent(OrderCreatedEvent event) {// 处理订单创建事件,例如发送邮件通知log.info("Received OrderCreatedEvent for order: " + event.getOrder().getOrderNumber());emailService.sendOrderConfirmationEmail(event.getOrder());}
}

都看到这里了,如果觉得有帮助,还请您给我个小小的鼓励,动动手指,帮忙点个赞或收藏!谢谢喽,如果觉得文章有误,欢迎在评论区留言指正,我会第一时间讨论并改正!!!

也欢迎关注微信公众号「Java随想录」第一时间阅读,专注分享Java技术干货,文章持续更新。

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

如若内容造成侵权/违法违规/事实不符,请联系瞎胡闹网进行投诉反馈,一经查实,立即删除!

相关文章

windows安装nssm并将jar打包为服务

一、nssm 下载地址 二、安装nssm服务 将下载的压缩包复制到安装目录进行解压&#xff0c;解压后有两个版本 win32 和 win64&#xff0c;根据系统选择。打开系统 powershell 命令窗口&#xff0c;进入安装目录指定版本目录&#xff0c;就可以使用nssm服务了。 # 安装服务&…

接口测试和Mock学习路线(中)

1.什么是 swagger Swagger 是一个用于生成、描述和调用 RESTful 接口的 WEB 服务。 通俗的来讲&#xff0c;Swagger 就是将项目中所有想要暴露的接口展现在页面上&#xff0c;并且可以进行接口调用和测试的服务。 现在大部分的项目都使用了 swagger&#xff0c;因为这样后端…

SV-7041T IP网络有源音箱 教室广播多媒体音箱(带本地扩音功能)教学广播音箱 办公室背景音乐广播音箱 2.0声道壁挂式网络有源音箱

SV-7041T IP网络有源音箱 教室广播多媒体音箱&#xff08;带本地扩音功能&#xff09; 教学广播音箱 办公室背景音乐广播音箱 一、描述 SV-7041T是深圳锐科达电子有限公司的一款2.0声道壁挂式网络有源音箱&#xff0c;具有10/100M以太网接口&#xff0c;可将网络音源通过自带…

汇编期末复习知识点

参考文献1 第一章 概述 组成 计算机系统由硬件子系统和软件子系统组成。硬件子系统&#xff1a;组成计算机系统的所有电子的&#xff0c;机械的&#xff0c;光学的和磁性的元部件。 计算机中常用进制数表示 十进制(Decimal):数据尾部加一后缀D&#xff0c;如2355D二进制&a…

TFTLCD时序介绍(ILI9341 8080并口时序)

ILI9341 8080并口 ILI9341是一个常见的TFT LCD显示控制器&#xff0c;用于驱动各种小型和中型尺寸的液晶显示屏。它支持多种分辨率&#xff0c;最常见的是240x320像素。ILI9341控制器具有内置的GRAM&#xff08;Graphics RAM&#xff09;&#xff0c;可以通过SPI或8位并行接口…

初学python,怎样入门?

答案&#xff1a;乌龟绘图。 "乌龟绘图"通常指的是使用Logo语言的变种——Python中的turtle模块来进行图形绘制。在turtle模块中&#xff0c;一只名为“海龟”&#xff08;Turtle&#xff09;的小动物会在屏幕上移动&#xff0c;根据其行进路径来绘制图形。以下是一段…

百面算法工程师 | 激活函数 Activate Function

目录 10.1激活函数作用&#xff1a; 10.2 为什么激活函数都是非线性的 10.3 常见激活函数的优缺点及其取值范围 10.4 激活函数问题的汇总 10.4.1 Sigmoid的缺点&#xff0c;以及如何解决 10.4.2 ReLU在零点可导吗&#xff0c;如何进行反向传播 10.4.3 Softmax溢出怎么处…

如何解决非法采砂、过度开采的乱象?

非法采砂和过度开采是当今世界面临的重要环境问题之一。这些行为不仅破坏了生态平衡&#xff0c;还对河道安全、水资源保护等产生了严重影响。然而&#xff0c;经过多年的努力&#xff0c;河道采砂管理工作不断加强&#xff0c;全国采砂秩序总体可控。为了更有效地解决这一问题…

linux开发板开机启动向日葵

硬件&#xff1a;orangepi 5 pro 操作系统&#xff1a;ubuntu 20.4 lts 安装向日葵 根据我的实测&#xff0c;arm架构的ubuntu系统只能安装向日葵提供的麒麟系统的那个版本&#xff0c;具体安装方式官网下载页面有 允许任意用户连接到 X11 使用root用户登录后打开终端输入一下…

图形界面挂了?教你如何纯命令行下快速安装CentOS 7

在某些特定的系统或软件环境下&#xff0c;如使用 Parallels Desktop 18&#xff08;PD18&#xff09;虚拟化软件安装较老版本的操作系统&#xff08;如 CentOS 7&#xff09;&#xff0c;可能会遇到只能通过命令行进行安装的情况。这通常是由于内核版本与图形安装器的兼容性问…

docker系列7:docker安装ES

目录 传送门 Docker安装ES 确定版本 拉取镜像 执行拉取ES镜像 查看ES镜像 运行ES 创建一个新的docker网络 启动一个Elasticsearch容器 查看运行结果 ES启动内存不足 访问ES 公网访问 传送门 docker系列1&#xff1a;docker安装 docker系列2&#xff1a;阿里云镜…

血的教训之虚拟机重装[包含一系列虚拟机,c++,python,miniob配置]

一切都要从头开始&#xff0c;由于脑袋糊涂&#xff0c;没看到是虚拟机的文件&#xff0c;直接一口气全删掉了&#xff0c;哎&#xff01;&#xff01;数据恢复后发现也不行&#xff0c;磁盘文件还是缺失了一部分&#xff0c;只能重新再来了。 等待ing 看不到按钮&#xff0c;按…

前端CSS基础8(盒子模型(margin、border、padding、content))

前端CSS基础8&#xff08;盒子模型&#xff08;margin、border、padding、content&#xff09;&#xff09; CSS盒子模型CSS中常用的长度单位元素的分类&#xff0c;各个元素的显示模式修改元素的显示模式&#xff08;类型&#xff09;盒子模型的组成部分盒子内容区-contentCSS…

验证线缆(汽车线束、网线、多芯线)破损或断开与正常线缆的区别在哪里?依AEM CV-100 k50测试仪

工厂产线生产的线缆&#xff08;汽车线束、网线、多芯线&#xff09;做成成品&#xff0c;即2端都安装好了模块。在这种情况下如何快速的判定此条线缆是合格的呢&#xff0c;此处的合格为物理层面上的合格&#xff08;不会出现开路、短路&#xff09;&#xff0c;也就是最基本保…

Ansible自动化运维工具主机清单配置

作者主页&#xff1a;点击&#xff01; Ansible专栏&#xff1a;点击&#xff01; 创作时间&#xff1a;2024年4月24日12点21分 Ansible主机清单文件用于定义要管理的主机及其相关信息。它是Ansible的核心配置文件之一&#xff0c;用于Ansible识别目标主机并与其建立连接。 …

Leetcode_相交链表

✨✨所属专栏&#xff1a;LeetCode刷题专栏✨✨ ✨✨作者主页&#xff1a;嶔某✨✨ 题目&#xff1a; 题解&#xff1a; 看到这个题目首先我们要排除链表逆置的想法&#xff0c;如图、因为c1节点只有一个next指针&#xff0c;逆置后不可能同时指向a2和b3节点。 其次有的的同学…

刷代码随想录有感(44):对称二叉树

题干&#xff1a; 代码&#xff1a; class Solution { public:bool compare(TreeNode* left, TreeNode* right){//传入左右子树if(left NULL && right ! NULL) return false;//子else if(left ! NULL && right NULL) return false;//子else if(left NULL &…

CentOS-7安装Mysql并允许其他主机登录

一、通用设置&#xff08;分别在4台虚拟机设置&#xff09; 1、配置主机名 hostnamectl set-hostname --static 主机名2、修改hosts文件 vim /etc/hosts 输入&#xff1a; 192.168.15.129 master 192.168.15.133 node1 192.168.15.134 node2 192.168.15.136 node33、 保持服…

【draw.io的使用心得介绍】

&#x1f308;个人主页: 程序员不想敲代码啊 &#x1f3c6;CSDN优质创作者&#xff0c;CSDN实力新星&#xff0c;CSDN博客专家 &#x1f44d;点赞⭐评论⭐收藏 &#x1f91d;希望本文对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提出指正&#xff0c;让我们共…

你的RPCvs佬的RPC

一、课程目标 了解常见系统库的hook了解frida_rpc 二、工具 教程Demo(更新)jadx-guiVS CodejebIDLE 三、课程内容 1.Hook_Libart libart.so: 在 Android 5.0&#xff08;Lollipop&#xff09;及更高版本中&#xff0c;libart.so 是 Android 运行时&#xff08;ART&#x…