一文吃透 Spring 事务传播行为:7 种场景+代码实战

作为后端开发,Spring 事务是日常工作的基础,但不少人只会用 @Transactional 注解加个 rollbackFor,对底层的事务传播行为一知半解。直到遇到"嵌套调用事务不回滚""重复提交导致数据异常"等问题,才发现对传播行为的理解不足会踩大坑。

其实事务传播行为的核心很简单:当一个带有事务的方法,调用另一个方法时,如何决定新方法的事务边界(是复用当前事务,还是新建事务,或是不参与事务)。Spring 定义了 7 种标准传播行为,本文结合实际业务场景,逐一拆解每种行为的用法、代码示例和适用场景,帮你彻底吃透。

先铺垫两个基础前提,避免理解偏差:

  • 所有示例基于 Spring Boot 2.x+,依赖 spring-boot-starter-data-jpamybatis-plus(本文用 JPA 简化数据库操作);

  • 事务传播行为仅对 @Transactional 注解修饰的方法生效,且必须通过 Spring 代理调用(同类方法内部调用需注意代理失效问题)。

一、Spring 7 种事务传播行为全解析

Spring 事务传播行为通过 propagation 属性配置,默认值为 REQUIRED。下面按"日常使用率"排序,逐一讲解。

1. REQUIRED(默认):如果有事务就复用,没有就新建

核心逻辑:这是最常用的传播行为,遵循"能复用则复用,无则新建"的原则。如果调用方已经存在事务,被调用方就加入当前事务,两者共用一个事务边界(要么一起提交,要么一起回滚);如果调用方没有事务,被调用方就新建一个独立事务。

业务场景:绝大多数核心业务流程,比如"创建订单+扣减库存",两者必须在同一事务中,要么都成功,要么都失败。

代码示例

java 复制代码
@Service
public class OrderService {

    @Autowired
    private OrderRepository orderRepository;
    @Autowired
    private StockService stockService;

    // 调用方:带有事务
    @Transactional(rollbackFor = Exception.class)
    public void createOrder(Long productId, Integer count, Long userId) {
        // 1. 创建订单
        Order order = new Order();
        order.setProductId(productId);
        order.setCount(count);
        order.setUserId(userId);
        order.setStatus(1); // 待支付
        orderRepository.save(order);

        // 2. 调用扣减库存方法(复用当前事务)
        stockService.deductStock(productId, count);
    }
}

@Service
public class StockService {

    @Autowired
    private StockRepository stockRepository;

    // 被调用方:传播行为为 REQUIRED(默认,可省略)
    @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
    public void deductStock(Long productId, Integer count) {
        Stock stock = stockRepository.findByProductId(productId)
                .orElseThrow(() -> new RuntimeException("库存不存在"));
        
        if (stock.getCount() < count) {
            throw new RuntimeException("库存不足");
        }

        stock.setCount(stock.getCount() - count);
        stockRepository.save(stock);
    }
}

结果说明

  • 如果 deductStock 抛出异常(如库存不足),createOrder 的订单创建操作会一起回滚,不会出现"有订单无库存扣减"的脏数据;

  • 如果调用方 createOrder 没有加 @TransactionaldeductStock 会新建独立事务,仅库存扣减操作受事务控制。

2. SUPPORTS:如果有事务就复用,没有就无事务

核心逻辑:被调用方"被动"参与事务,不主动创建事务。如果调用方有事务,就加入其中;如果调用方没有事务,就以无事务方式执行。

业务场景:查询类方法,既可以在事务中执行(保证查询到未提交的事务数据,如分布式事务中的一致性查询),也可以独立执行(普通查询场景)。

代码示例

java 复制代码
@Service
public class OrderQueryService {

    @Autowired
    private OrderRepository orderRepository;

    // 传播行为为 SUPPORTS,不主动创建事务
    @Transactional(propagation = Propagation.SUPPORTS, readOnly = true)
    public Order getOrderById(Long orderId) {
        return orderRepository.findById(orderId)
                .orElseThrow(() -> new RuntimeException("订单不存在"));
    }
}

// 调用场景1:调用方有事务
@Service
public class OrderOperateService {
    @Autowired
    private OrderQueryService queryService;

    @Transactional(rollbackFor = Exception.class)
    public void updateOrderStatus(Long orderId, Integer status) {
        // 复用当前事务查询订单(能查询到未提交的临时数据)
        Order order = queryService.getOrderById(orderId);
        order.setStatus(status);
        orderRepository.save(order);
    }
}

// 调用场景2:调用方无事务
@Controller
public class OrderController {
    @Autowired
    private OrderQueryService queryService;

    @GetMapping("/order/{id}")
    public ResponseEntity<Order> getOrder(@PathVariable Long id) {
        // 无事务方式执行查询
        return ResponseEntity.ok(queryService.getOrderById(id));
    }
}

注意点:SUPPORTS 修饰的方法如果在无事务环境下执行,所有数据库操作都是自动提交的,无法回滚。

3. MANDATORY:必须在已有事务中执行,否则抛异常

核心逻辑 :被调用方强制要求调用方有事务,自身不新建事务。如果调用方没有事务,直接抛出 IllegalTransactionStateException 异常,拒绝执行。

业务场景:必须依赖调用方事务的操作,比如"订单状态变更日志记录",必须和订单状态变更在同一事务中,确保日志与业务操作一致,不允许独立执行。

代码示例

java 复制代码
@Service
public class OrderLogService {

    @Autowired
    private OrderLogRepository logRepository;

    // 必须在已有事务中执行,否则抛异常
    @Transactional(propagation = Propagation.MANDATORY, rollbackFor = Exception.class)
    public void recordLog(Long orderId, Integer oldStatus, Integer newStatus) {
        OrderLog log = new OrderLog();
        log.setOrderId(orderId);
        log.setOldStatus(oldStatus);
        log.setNewStatus(newStatus);
        log.setOperateTime(LocalDateTime.now());
        logRepository.save(log);
    }
}

// 正确调用:调用方有事务
@Service
public class OrderService {
    @Autowired
    private OrderLogService logService;

    @Transactional(rollbackFor = Exception.class)
    public void updateOrderStatus(Long orderId, Integer newStatus) {
        Order order = orderRepository.findById(orderId).orElseThrow();
        Integer oldStatus = order.getStatus();
        order.setStatus(newStatus);
        orderRepository.save(order);

        // 正常执行,复用当前事务
        logService.recordLog(orderId, oldStatus, newStatus);
    }

    // 错误调用:调用方无事务
    public void errorUpdateStatus(Long orderId, Integer newStatus) {
        // 调用 recordLog 时会抛 IllegalTransactionStateException
        logService.recordLog(orderId, 1, newStatus);
    }
}

4. REQUIRES_NEW:无论是否有事务,都新建独立事务

核心逻辑:被调用方强制新建一个独立事务,与调用方事务完全隔离(两个事务互不影响,各自提交/回滚)。如果调用方已有事务,会先暂停当前事务,待新事务执行完成后,再恢复原事务。

业务场景:需要独立存在的操作,比如"订单创建失败后记录异常日志",即使订单创建事务回滚,日志也必须保留,不能被回滚影响。

代码示例

java 复制代码
@Service
public class OrderErrorLogService {

    @Autowired
    private OrderErrorLogRepository errorLogRepository;

    // 新建独立事务,与调用方事务隔离
    @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
    public void recordErrorLog(Long productId, Integer count, String errorMsg) {
        OrderErrorLog errorLog = new OrderErrorLog();
        errorLog.setProductId(productId);
        errorLog.setCount(count);
        errorLog.setErrorMsg(errorMsg);
        errorLog.setCreateTime(LocalDateTime.now());
        errorLogRepository.save(errorLog);
    }
}

@Service
public class OrderService {
    @Autowired
    private OrderErrorLogService errorLogService;

    @Transactional(rollbackFor = Exception.class)
    public void createOrder(Long productId, Integer count, Long userId) {
        try {
            // 模拟订单创建失败(如库存不足)
            throw new RuntimeException("订单创建失败:库存不足");
        } catch (Exception e) {
            // 记录错误日志,新建独立事务,即使当前事务回滚,日志也会保留
            errorLogService.recordErrorLog(productId, count, e.getMessage());
            // 重新抛出异常,让当前事务回滚
            throw e;
        }
    }
}

结果说明createOrder 事务回滚,但 recordErrorLog 新建的独立事务会正常提交,错误日志成功保存,实现"业务回滚但日志留存"的需求。

5. NOT_SUPPORTED:无论是否有事务,都以无事务方式执行

核心逻辑:被调用方拒绝参与任何事务。如果调用方有事务,会先暂停当前事务,待被调用方无事务执行完成后,再恢复原事务;如果调用方无事务,直接正常执行。

业务场景:不需要事务的耗时操作,比如"订单创建后发送短信通知",即使通知失败,也不能影响订单创建事务的提交;或者不允许在事务中执行的操作(如批量数据同步,避免长时间占用事务资源)。

代码示例

java 复制代码
@Service
public class SmsService {

    // 拒绝参与事务,以无事务方式执行
    @Transactional(propagation = Propagation.NOT_SUPPORTED)
    public void sendOrderSms(Long userId, Long orderId) {
        // 模拟短信发送(耗时操作,无事务)
        try {
            Thread.sleep(1000);
            System.out.println("向用户" + userId + "发送订单" + orderId + "创建成功短信");
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException("短信发送失败");
        }
    }
}

@Service
public class OrderService {
    @Autowired
    private SmsService smsService;

    @Transactional(rollbackFor = Exception.class)
    public void createOrder(Long productId, Integer count, Long userId) {
        // 1. 创建订单(事务内操作)
        Order order = new Order();
        // ... 订单赋值逻辑
        orderRepository.save(order);

        // 2. 发送短信(无事务,即使失败也不影响订单提交)
        try {
            smsService.sendOrderSms(userId, order.getId());
        } catch (Exception e) {
            // 仅记录异常,不回滚订单事务
            System.err.println("短信发送失败:" + e.getMessage());
        }
    }
}

6. NEVER:必须在无事务环境下执行,否则抛异常

核心逻辑 :被调用方严格禁止在事务中执行。如果调用方有事务,直接抛出 IllegalTransactionStateException 异常;如果调用方无事务,正常执行。

业务场景:完全不允许事务控制的操作,比如"数据归档脚本""第三方接口调用(自身已保证幂等)",避免事务长时间占用资源或导致数据一致性问题。

代码示例

java 复制代码
@Service
public class DataArchiveService {

    // 必须无事务执行,有事务则抛异常
    @Transactional(propagation = Propagation.NEVER)
    public void archiveOldOrder(LocalDateTime endTime) {
        // 模拟归档3个月前的订单数据(无事务,避免长时间锁表)
        List<Order> oldOrders = orderRepository.findByCreateTimeBefore(endTime);
        // ... 归档逻辑
    }
}

// 错误调用:调用方有事务
@Service
public class OrderService {
    @Autowired
    private DataArchiveService archiveService;

    @Transactional(rollbackFor = Exception.class)
    public void doArchive() {
        // 调用 archiveOldOrder 时会抛异常,因为当前有事务
        archiveService.archiveOldOrder(LocalDateTime.now().minusMonths(3));
    }
}

7. NESTED:嵌套事务,依赖调用方事务

核心逻辑:被调用方在调用方事务内创建一个"嵌套子事务",子事务依赖于父事务(调用方事务)。父事务提交时,子事务才会提交;父事务回滚时,子事务必然回滚;但子事务回滚时,父事务可以选择继续执行(不会被子事务回滚影响)。

注意:嵌套事务依赖数据库支持(如 MySQL 的 SAVEPOINT 保存点机制),并非所有数据库都支持;与 REQUIRES_NEW 的区别是:NESTED 是子事务,与父事务同属一个事务上下文;REQUIRES_NEW 是完全独立的事务。

业务场景:父事务中包含可选操作,子事务失败不影响父事务核心逻辑,比如"创建订单时尝试扣减优惠券",优惠券扣减失败(子事务回滚),但订单创建(父事务)可以继续执行。

代码示例

java 复制代码
@Service
public class CouponService {

    @Autowired
    private CouponRepository couponRepository;

    // 嵌套事务,依赖调用方事务
    @Transactional(propagation = Propagation.NESTED, rollbackFor = Exception.class)
    public void deductCoupon(Long couponId, Long userId) {
        Coupon coupon = couponRepository.findByIdAndUserId(couponId, userId)
                .orElseThrow(() -> new RuntimeException("优惠券不存在"));

        if (coupon.getIsUsed()) {
            throw new RuntimeException("优惠券已使用");
        }

        coupon.setIsUsed(true);
        couponRepository.save(coupon);
    }
}

@Service
public class OrderService {
    @Autowired
    private CouponService couponService;

    @Transactional(rollbackFor = Exception.class)
    public void createOrder(Long productId, Integer count, Long userId, Long couponId) {
        // 1. 创建订单(父事务核心逻辑)
        Order order = new Order();
        // ... 订单赋值逻辑
        orderRepository.save(order);

        // 2. 尝试扣减优惠券(子事务,失败不影响订单)
        try {
            couponService.deductCoupon(couponId, userId);
        } catch (Exception e) {
            // 子事务回滚,父事务继续执行
            System.err.println("优惠券扣减失败:" + e.getMessage());
        }
    }
}

结果说明:如果优惠券扣减失败(子事务回滚),订单创建操作(父事务)依然会正常提交;如果订单创建失败(父事务回滚),优惠券扣减操作(子事务)也会跟着回滚。

二、常见误区与实战建议

1. 同类方法内部调用,传播行为失效

Spring 事务基于动态代理实现,同类方法内部调用时,不会经过代理,导致 @Transactional 注解失效,传播行为自然不生效。

java 复制代码
@Service
public class OrderService {
    // 错误示例:内部调用,传播行为失效
    @Transactional(rollbackFor = Exception.class)
    public void createOrder() {
        // 内部调用 deductStock,@Transactional 注解失效
        this.deductStock();
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void deductStock() {
        // ... 库存扣减逻辑
    }
}

解决方案:通过 Spring 上下文获取自身代理对象调用,或拆分方法到不同 Service 类。

2. 传播行为与隔离级别区分开

不少人会混淆"传播行为"和"隔离级别":传播行为控制的是"事务之间的调用关系",隔离级别控制的是"事务内部对数据的可见性"(如脏读、不可重复读),两者互不影响,可独立配置。

3. 优先使用默认传播行为,按需选型

日常开发中,REQUIRED(默认)能覆盖 80% 以上场景;需要独立事务用 REQUIRES_NEW;查询方法用 SUPPORTS;严格依赖/禁止事务用 MANDATORY/NEVER;嵌套事务谨慎使用(依赖数据库支持)。

三、总结

Spring 事务传播行为的本质是"事务边界的控制规则",核心是解决"多方法调用时事务如何协同"的问题。掌握每种行为的适用场景,结合实际业务选择,才能避免事务漏洞(如数据不一致、事务失效、资源浪费)。

建议实际开发中,先明确"方法间事务是否需要协同",再选择对应的传播行为,同时搭配 rollbackFor(指定回滚异常类型)、readOnly(查询优化)等属性,让事务控制更精准、高效。

相关推荐
萧曵 丶6 天前
事务ACID特性详解
数据库·事务·acid
heartbeat..6 天前
Spring 声明式事务:原理、使用及失效场景详解
java·spring·面试·事务
optimistic_chen7 天前
【Redis系列】事务特性
数据库·redis·笔记·缓存·事务
七夜zippoe10 天前
Spring Data JPA原理与实战 Repository接口的魔法揭秘
java·ffmpeg·事务·jpa·repository
利刃大大15 天前
【SpringBoot】Spring事务 && @Transactional详解 && Spring事务失效问题
spring boot·spring·事务
a1879272183115 天前
MySQL 事务
数据库·mysql·事务·mvcc·acid·readview·可见性判断算法
七夜zippoe16 天前
Spring与MyBatis整合原理及事务管理
java·spring·mybatis·事务·mapper
lkbhua莱克瓦2419 天前
基础-事务
开发语言·数据库·笔记·mysql·事务
橘子真甜~21 天前
Reids命令原理与应用2 - Redis网络层与优化,pipeline,发布订阅与事务
数据库·redis·缓存·事务·发布订阅·lua脚本·acid特性