@Transactional用法详解与事务避坑指南

 更新时间:2025年10月23日 10:49:56   作者:-朽木  
这篇文章主要介绍了@Transactional用法与事务避坑指南,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
  • 需求:业务单位需要观测订单动态,所以要在订单的各个状态节点上传状态至状态池系统
  • 方案:又是Easy的需求,不就是在每个订单状态改变时触发上传状态,为了不影响订单正常流转,增加一个消息中间件MQ

前言

在Java中,@Transactional是一个非常重要的注解,用于声明事务管理的行为。

它可以被应用在类级别或方法级别上,并且提供了多种选项来控制事务的传播行为、隔离级别、超时设置和回滚条件等。

一、Java的@Transactional解释

@Transactional 注解在 Java 中用于声明式事务管理,它可以应用于接口、接口方法、类以及类的方法上。在默认配置下,Spring 中的 @Transactional 注解会使得方法在事务的上下文中执行,其事务边界默认为方法开始执行时开始,方法正常结束时提交事务,方法执行过程中抛出异常时回滚事务。

但是,你可以通过在 @Transactional 注解中设置 propagation 属性来改变事务的传播行为,例如可以设置为Propagation.MANDATORY,这意味着该方法必须在一个已经存在的事务中执行,否则就会抛出异常。

另外,你可以通过设置 phase 属性来改变事务的提交时机,例如可以设置为Phase.AFTER_COMPLETION,这样事务就会在整个事务完成后提交,不论事务是正常结束还是异常结束。

以下是一个使用 @Transactional 注解的示例,其中 propagation 设置为Propagation.REQUIRED(默认值),表示如果当前存在事务,则加入该事务;如果不存在,则创建一个新的事务。

import org.springframework.transaction.annotation.Transactional;
import org.springframework.stereotype.Service;
 
@Service
public class MyService {
 
    @Transactional
    public void someTransactionalMethod() {
        // 方法执行的代码
    }
}

二、踩坑

接到的需求中:每个订单状态改变时触发上传状态,为了不影响订单正常流转,增加一个消息中间件MQ

流程图如下:

二、状态统一入口:推送

    private SendResult sendStatusToMq(SendStatusPoolMsgVo sendStatusPoolMsgVo) {
        String msg = JSON.toJSONString(sendStatusPoolMsgVo, SerializerFeature.PrettyFormat,
                SerializerFeature.WriteMapNullValue);
        LOGGER.info("sendStatusPoolMsgToMq,发送的消息{}", msg);
        Message<String> message = MessageBuilder.withPayload(msg).build();
        SendResult sendResult = new SendResult();
        try {
            sendResult = rocketMQTemplate.syncSend(statusPoolTopic, message, 3000, 2);
        } catch (Exception e) {
            e.printStackTrace();
            log.error("sendStatusToMq err", e);
            TaskMqModel taskMqModel = new TaskMqModel();
            taskMqModel.setTopic(statusPoolTopic);
            taskMqModel.setMessageType(MessageTypeConstant.MQ_SUPPLE_STATUSPOOL);
            taskMqModel.setDisposeFlag(RequestConstant.REQUEST_ORIGINAL);
            taskMqModel.setRawMessage(msg);
            taskMqModel.setCreateTime(new Date());
            taskMqService.save(taskMqModel);
        }
        return sendResult;
    }

我这里做了个状态统一入口,同时为了避免推送MQ服务失败,做了个异常捕获保存推送消息,方便重推。

三、MQ消费

// ... 其他业务逻辑 ...
        // 获取订单信息
        String orderNo = msgVo.getOrderNo();
        orderModel orderModel = orderService.getOrderByOrderNo(orderNo);
        //如果是客户下单再去待处理订单查询
        if (Objects.isNull(fmOrderModel)) {
            LOGGER.err("未查到信息,初始化失败!");
            return;
        }
//

然后我的日志全是:

[2024-08-8 08:36:50.161][ERROR][ConsumeMessageThread_2] 未查到信息,初始化失败!
[2024-08-8 08:37:57.015][ERROR][ConsumeMessageThread_2] 未查到信息,初始化失败!
[2024-08-8 08:42:45.536][ERROR][ConsumeMessageThread_2] 未查到信息,初始化失败!

我查看数据库是完全能查到的,有点奇怪❗❗❗❗❓❓❓❓

四、问题与解决

在Java中,@Transactional 注解通常用于声明方法执行的事务边界,确保方法内的一系列操作要么全部成功,要么全部失败。它通常与Spring框架的事务管理器一起使用,以提供声明式的事务管理。

如果你的订单数据保存操作使用了 @Transactional,并且在保存数据之后立即推送消息到消息队列(MQ),那么消费者可能查不到数据的原因可能与事务的隔离级别有关。

下面是我搜到的一些可能导致该问题的原因:

  1. 事务隔离级别:如果你的数据库事务使用了较高的隔离级别(如可重复读或串行化),那么在事务提交之前,其他事务(包括MQ消费者)可能看不到该事务所做的更改。
  2. 事务传播行为:如果MQ消费者也运行在Spring管理的事务中,并且事务的传播行为设置为不支持当前事务(例如,Propagation.NOT_SUPPORTED),那么消费者可能会在不同的事务上下文中运行,从而看不到未提交的更改。
  3. 延迟提交:如果事务在消息发送之后才提交,MQ消费者可能会在事务提交之前读取数据库,因此看不到新数据。
  4. MQ消息延迟:如果消息队列本身存在延迟,消费者可能会在数据还未写入数据库时就接收到消息。
  5. 数据库缓存:某些数据库实现可能会缓存数据,这可能导致即使数据已经写入,消费者也无法立即看到更新。
  6. MQ消费者处理逻辑:如果MQ消费者在处理消息时没有正确地处理事务或数据库查询,也可能导致查不到数据。

解决方式

在Spring框架中,我通过实现TransactionSynchronization接口来在事务提交后执行回调操作。

以下是实现事务提交回调发送消息的方法:

@Component
public class TransactionalMessageSender implements TransactionSynchronization {

    @Autowired
    private SomeMessageService messageService; // 消息发送服务

    @Override
    public void beforeCommit(boolean readOnly) {
        // 这里可以执行一些操作,但事务还未提交
    }

    @Override
    public void beforeCompletion() {
        // 事务即将提交,可以在这里准备发送消息
    }

    @Override
    public void afterCommit() {
        // 事务已经提交,可以在这里发送消息
        sendResult = rocketMQTemplate.syncSend(statusPoolTopic, message, 3000, 2);
    }

    @Override
    public void afterCompletion(TransactionStatus status) {
        // 事务已经完成,无论是提交还是回滚
        if (status == TransactionStatus.COMMITTED) {
            // 这里可以执行一些事务提交后的清理工作
        }
    }
}

五、总结

在 Spring 框架中使用 @Transactional 注解时,事务管理器会在事务边界内管理数据库操作。

当你在一个事务中执行数据库操作后立即发送消息到消息队列(MQ),可能会遇到一个问题:MQ 消费者在接收到消息并尝试查询数据库时,发现数据库中并没有预期的数据。

这个问题的根本原因在于事务的隔离级别和事务提交的时间点。

当事务尚未提交时,其他事务(包括 MQ 消费者的查询操作)是无法看到该事务内的修改的。

即使你在事务中已经执行了数据库操作(如插入或更新),这些修改对其他事务来说仍然是不可见的,直到当前事务提交。

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

相关文章

  • Java利用redis zset实现延时任务详解

    Java利用redis zset实现延时任务详解

    zset作为redis的有序集合数据结构存在,排序的依据就是score。本文就将利用zset score这个排序的这个特性,来实现延时任务,感兴趣的可以了解一下
    2022-08-08
  • SpringBoot最常用的50个注解总结(全是干货!)

    SpringBoot最常用的50个注解总结(全是干货!)

    SpringBoot提供多种注解简化配置与启动流程,如@SpringBootAppication、@RestController、@RequestMapping等,这篇文章主要介绍了SpringBoot最常用的50个注解的相关资料,需要的朋友可以参考下
    2024-09-09
  • springboot+shiro+jwtsession和token进行身份验证和授权

    springboot+shiro+jwtsession和token进行身份验证和授权

    最近和别的软件集成项目,需要提供给别人接口来进行数据传输,发现给他token后并不能访问我的接口,拿postman试了下还真是不行,检查代码发现项目的shiro配置是通过session会话来校验信息的,修改代码兼容token和session
    2024-06-06
  • mybatis-generator自动生成dao、mapping、bean配置操作

    mybatis-generator自动生成dao、mapping、bean配置操作

    这篇文章主要介绍了mybatis-generator自动生成dao、mapping、bean配置操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-08-08
  • spring缓存自定义resolver的方法

    spring缓存自定义resolver的方法

    这篇文章主要为大家详细介绍了spring缓存自定义resolver的方法,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下,希望能够给你带来帮助
    2022-03-03
  • java实现验证码类生成中文验证码

    java实现验证码类生成中文验证码

    java实现的汉字输入验证码,主要包含两个类,一个是生成验证码,一个是判断验证码输入是否正确,实现原理非常简单,将汉字和干扰线生成图片并将汉字保存到session,前台获取每次生成验证码图片并用文本框值和session值比较,功能就怎么简单
    2014-01-01
  • Java多线程中的CyclicBarrier使用方法详解

    Java多线程中的CyclicBarrier使用方法详解

    这篇文章主要介绍了Java多线程中的CyclicBarrier使用方法详解,CyclicBarrier是一种同步辅助工具,它允许一组线程都等待对方到达公共障碍点,在涉及固定大小的线程的程序中,CyclicBarriers非常有用,这些线程间必须相互等待,需要的朋友可以参考下
    2023-12-12
  • java 使用BeanFactory实现service与dao层解耦合详解

    java 使用BeanFactory实现service与dao层解耦合详解

    这篇文章主要介绍了java 使用BeanFactory实现service与dao层解耦合详解,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-12-12
  • Java 并发编程之ThreadLocal详解及实例

    Java 并发编程之ThreadLocal详解及实例

    这篇文章主要介绍了Java 并发编程之ThreadLocal详解及实例的相关资料,需要的朋友可以参考下
    2017-02-02
  • java阿拉伯数字转中文数字

    java阿拉伯数字转中文数字

    这篇文章主要为大家详细介绍了java实现阿拉伯数字转换为中文数字,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2019-04-04

最新评论