如何优雅地Spring事务编程

news2024/12/23 7:43:10

本文已收录至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() {
            @Override
            public 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 方法获取到这个连接。

    @Nullable
    public 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;
    }

    @Nullable
    private 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;
    }

    @Override
    public void beforeCommit(boolean readOnly) {
        // 在事务提交前不执行任何操作
    }

    @Override
    public void beforeCompletion() {
        // 在事务即将完成时不执行任何操作
    }

    @Override
    public void afterCommit() {
        // 在事务提交后发送订单支付成功的消息通知
        sendMessage("订单支付成功", orderNo);
    }

    @Override
    public void afterCompletion(int status) {
        // 在事务完成后不执行任何操作
    }

    private void sendMessage(String message, String orderNo) {
        // 发送消息通知的具体实现逻辑
        System.out.println(message + ": " + orderNo);
    }
}
    @Transactional
    public void finishOrder(String orderNo) {
        // 修改订单成功
        updateOrderSuccess(orderNo);
        // 发送消息到 MQ
        TransactionSynchronizationManager.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 {

    @Autowired
    private OrderRepository orderRepository;

    @Autowired
    private ApplicationEventPublisher eventPublisher;

    @Transactional
    public 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 {

    @Autowired
    private 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://www.coloradmin.cn/o/1620778.html

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

相关文章

[Meachines][Medium]IClean

Main $ nmap -p- -sC -sV 10.10.11.12 -Pn --min-rate 1000 $ echo "10.10.11.12 capiclean.htb">>/etc/hosts 这题可能和python的SSTI有关 $ gobuster dir --url "http://capiclean.htb" --wordlist /usr/share/seclists/Discovery/Web-Content/c…

ENVI操作:GF2影像全色与多光谱融合

1、插件下载安装 1.1、插件下载 ENVI 处理 GF2影像需利用特定的 App 插件进行处理&#xff1b;进入下述官方网站&#xff0c;点击 下载 即可&#xff1b;仅支持 ENVI 5.3.1 以上版本的软件&#xff1b; 下载地址&#xff1a;https://envi.geoscene.cn/appstore/ 1.2、属性设…

VS2022配置和搭建QT

一、下载QT 可以去QT官网下载:https://www.qt.io/product/development-tools。 直接安装。 二、安装qt插件 直接在vs插件市场搜索就行。 安装的时候根据提示&#xff0c;关闭vs自动安装 再次进去vs提示你选择qt版本&#xff0c;psth里边找到安装版本的qmake.exe就行 配…

11.盛最多水的容器 C++

一开始我最先想到的是暴力解法&#xff0c;就是两个循环嵌套依次遍历&#xff0c;所有情况都过一遍找出最大值&#xff0c;这样示例的结果虽然是正确的&#xff0c;但是超时。所以暴力解法行不通&#xff0c;双指针思考才是正道&#xff0c;双指针一般都是一边一个&#xff0c;…

裸金属服务器是什么

自推出裸金属服务器以来&#xff0c;它一直断断续续地出现在我们面前。最近&#xff0c;关于裸金属服务器、什么是裸金属服务器、裸金属服务器可以做什么、数据托架共享的讨论越来越多&#xff1a; 裸金属服务器&#xff08;bare metal server&#xff0c;BMS&#xff09;的官…

数据库——实 验 8 SQL 编程

1.T-SQL 语言简介 SQL Server 使用的语言称作 Transact-SQL, 它不仅包括基本 SQL 操作的内容&#xff0c;如 SQL 的数据查询功能和数据操作功能等&#xff0c;还有一般程序设计的能力。 2. 局部变量和全局变量的概念 1)局部变量 局部变量是一个能够拥有特定数据类型的对…

掌握Linux Shell脚本函数:提高脚本效率与可维护性

目录标题 1、什么是Shell函数&#xff1f;2、如何定义Shell函数&#xff1f;3、Shell函数参数4、返回值5、实例&#xff1a;使用函数进行文件备份6、为什么使用函数&#xff1f;7、最佳实践 在编写Linux shell脚本时&#xff0c;函数是组织和重用代码的重要手段。本文将介绍如何…

python getsize如何使用

第一步&#xff0c;点击键盘 winr&#xff0c;打开运行窗口&#xff1b;在窗口中输入“cmd"&#xff0c;点击确定&#xff0c;打开windows命令行窗口。 第二步&#xff0c;在windows命令行窗口中&#xff0c;输入“python”&#xff0c;进入python交互窗口。 第三步&#…

C++ —— 继承

什么是继承&#xff1f; 继承是指一种代码可以被复用的机制&#xff0c;在一个类的基础上进行扩展&#xff0c;产生的新类叫做派生类&#xff0c;被继承的类叫基类。&#xff08;也可称为子类和父类&#xff09; 继承的写法&#xff1a; class B : 继承方式 A (…

Laravel 6 - 第十三章 请求

​ 文章目录 Laravel 6 - 第一章 简介 Laravel 6 - 第二章 项目搭建 Laravel 6 - 第三章 文件夹结构 Laravel 6 - 第四章 生命周期 Laravel 6 - 第五章 控制反转和依赖注入 Laravel 6 - 第六章 服务容器 Laravel 6 - 第七章 服务提供者 Laravel 6 - 第八章 门面 Laravel 6 - …

SpringBoot引入第三方jar包或本地jar包

idea2018创建spring boot项目 New Project窗口选择Spring Initializr Type选择Maven(Generate…),有两个Maven选择这一个。 勾选Spring Web。 pom.xml中version改成2.5.10。 在resources中新建jar目录&#xff0c;将第三方jar包fastjson2-2.0.47.jar放入其中。&#xff08…

【C++】日期计算机

个人主页&#xff1a;救赎小恶魔 欢迎大家来到小恶魔频道 好久不见&#xff0c;甚是想念 今天我们要讲述的是一个日期类计算机的代码实现 引言&#xff1a; 我们日常生活中可能会有一个烦恼。 今天几月几号&#xff1f;过n天后又是几月几号&#xff1f;某年某月某天和x年…

Python读写文本URL蓝牙WIFI自动连接电子名片位置坐标智能海报等NDEF标签

本示例使用的发卡器&#xff1a;https://item.taobao.com/item.htm?id615391857885&spma1z10.5-c.w4002-21818769070.11.60ad789erlonvk 近场通信&#xff08;Near Field Communication&#xff0c;简称NFC&#xff09;&#xff0c;是一种新兴的技术&…

实验:使用apache + yum实现自制yum仓库

实验准备 Web服务器端&#xff1a;cenos-1&#xff08;IP&#xff1a;10.9.25.33&#xff09; 客户端&#xff1a;centos-2 保证两台机器网络畅通&#xff0c;原yum仓库可用&#xff0c;关闭防火墙和selinux Web服务器端 ①安装httpd并运行&#xff0c;设置开机自启动 安装…

【人工智能基础】人工神经网络

一、人工神经网络的三要素 人工神经元数理模型 MP模型是世界上第一个神经计算模型&#xff0c;为神经网络理论提供了基础 MP模型功能 对树突输入u的线性加权求和对净输入的非线性转换\ 作用函数的功能作用函数的功能 MP神经元模型的作用函数是单位阶跃函数。当x≥0时f(x)…

实现Node.js安装与配置。

一 、Node.js简介 Node.js是一个基于Chrome V8引擎的JavaScript运行时环境&#xff0c;用于构建高性能、可扩展的网络应用程序。它发布于2009年5月&#xff0c;由Ryan Dahl开发&#xff0c;允许使用JavaScript进行服务器端编程&#xff0c;使开发者能够在前后端都使用同一种编程…

ASP.NET教务平台—学籍管理模块开发与设计

摘 要 教务平台之学籍管理模块是一个典型的教务信息管理系统(MIS)&#xff0c;其开发主要包括后台数据库的建立和前端应用程序的开发两个方面。对于后台数据库要求实现数据的完整性、一致性和安全性&#xff1b;对于前台应用程序开发则要求模块功能完备、界面友好、易使用等特…

进程状态和优先级(进程第2篇)【Linux复习篇】

目录 一、进程状态 1、进程有什么状态&#xff1f; 2、 Linux下的进程状态有什么&#xff1f; 二、进程优先级 1、进程优先级是什么&#xff1f; 2、为什么要有优先级 3、怎么改进程优先级&#xff1f;要改吗&#xff1f; 4、操作系统如何根据优先级开展调度的&#xff…

Spring MVC和Spring Boot

上节已经提到过请求&#xff0c;这次梳理响应。 响应 响应基本上都要被Controller所托管&#xff0c;告诉Spring帮我们管理这个代码&#xff0c;我们在后面需要访问时&#xff0c;才可以进行访问&#xff0c;否则将会报错。并且其是由RestController分离出来的&#xff0c;Re…

【MCU】栈溢出问题

项目场景&#xff1a; 硬件&#xff1a;STM32F407&#xff0c;操作系统&#xff1a;rt_thread master分支 问题描述 问题栈溢出 id 499 ide 00 rtr 00 len 8 9 Function[rt_completion_wait] shall not be used in ISR (0) assertion failed at function:rt_completion_wait,…