手记

Spring Boot 事务传播行为:定义、分类及业务场景案例

事务传播行为(Transaction Propagation)是 Spring 事务核心特性之一,解决的是多个嵌套的事务方法相互调用时,事务如何传递(创建、复用、挂起) 的问题。它定义了“当前方法执行时,若已有事务上下文(调用方事务),当前方法的事务该如何处理”。

一、核心概念铺垫

  • 当前事务上下文:调用方方法是否开启了事务(即是否有 active 的事务)。
  • 传播行为作用域:仅对 @Transactional 注解修饰的方法生效,且需基于 Spring 声明式事务(底层是 AOP 代理)。
  • 核心类org.springframework.transaction.annotation.Propagation 枚举类,包含 7 种传播行为。

二、7种传播行为分类及业务场景案例

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

定义

  • 若当前存在事务,加入该事务;
  • 若当前无事务,创建新事务;
  • 是 Spring 事务的默认传播行为,也是最常用的类型。

适用场景

核心业务流程的统一事务控制:多个操作必须在同一个事务中完成(要么全成,要么全败)。

实际案例

电商下单流程:下单时需同时完成“扣减库存”+“生成订单”+“扣减用户余额”,三个操作必须在同一个事务中。

@Service
public class OrderService {
    @Resource
    private StockService stockService;
    @Resource
    private UserBalanceService userBalanceService;

    // 主事务:下单(默认REQUIRED)
    @Transactional(propagation = Propagation.REQUIRED)
    public void createOrder(Long productId, Long userId, Integer num) {
        // 1. 扣减库存(复用当前事务)
        stockService.deductStock(productId, num);
        // 2. 生成订单(省略订单入库逻辑)
        createOrderRecord(productId, userId, num);
        // 3. 扣减用户余额(复用当前事务)
        userBalanceService.deductBalance(userId, calculateAmount(productId, num));
    }

    @Service
    public class StockService {
        // 扣减库存(REQUIRED,复用下单的事务)
        @Transactional(propagation = Propagation.REQUIRED)
        public void deductStock(Long productId, Integer num) {
            // SQL:UPDATE stock SET count = count - ? WHERE product_id = ?
            jdbcTemplate.update("UPDATE stock SET count = count - ? WHERE product_id = ?", num, productId);
            // 若此处抛异常,整个下单事务回滚(库存、订单、余额都回滚)
        }
    }
}

关键说明

  • createOrder 调用 deductStock 时已有事务,deductStock 直接复用;若 createOrder 无事务,deductStock 新建事务。
  • 任意子方法异常,整个事务回滚,符合“下单要么全成,要么全败”的业务要求。

2. SUPPORTS:有事务就复用,没有就以非事务方式执行

定义

  • 若当前存在事务,加入该事务;
  • 若当前无事务,以非事务方式执行(无事务控制)。

适用场景

查询类操作:查询结果是否需要事务隔离性,取决于调用方是否有事务(如“查询订单详情”,若在下单事务中调用则复用事务,保证数据一致性;单独调用则无事务)。

实际案例

订单详情查询

@Service
public class OrderQueryService {
    // 订单详情查询(SUPPORTS)
    @Transactional(propagation = Propagation.SUPPORTS, readOnly = true)
    public OrderVO getOrderDetail(Long orderId) {
        // 1. 查询订单主信息
        Order order = jdbcTemplate.queryForObject(
            "SELECT id, user_id, product_id FROM t_order WHERE id = ?",
            new BeanPropertyRowMapper<>(Order.class),
            orderId
        );
        // 2. 查询订单明细(关联查询)
        List<OrderItem> items = jdbcTemplate.query(
            "SELECT id, order_id, sku_id FROM t_order_item WHERE order_id = ?",
            new BeanPropertyRowMapper<>(OrderItem.class),
            orderId
        );
        return assembleOrderVO(order, items);
    }
}

// 场景1:在下单事务中调用(复用事务,保证未提交的订单也能查询到)
@Service
public class OrderService {
    @Transactional
    public void createOrder(...) {
        // 生成订单(未提交)
        createOrderRecord(...);
        // 查询订单详情(SUPPORTS,复用当前事务,能查到未提交的订单)
        OrderVO vo = orderQueryService.getOrderDetail(newOrderId);
    }
}

// 场景2:单独调用(非事务方式,性能更高)
@Controller
public class OrderController {
    @GetMapping("/order/{id}")
    public ResponseEntity<OrderVO> getOrder(@PathVariable Long id) {
        // 无事务上下文,以非事务方式执行
        return ResponseEntity.ok(orderQueryService.getOrderDetail(id));
    }
}

关键说明

  • readOnly = true 配合 SUPPORTS 使用,查询场景下可提升数据库性能(数据库优化只读事务);
  • 非事务执行时,无法保证隔离性(如可能读到未提交数据),但查询场景通常可接受。

3. MANDATORY:必须有事务,否则抛异常

定义

  • 若当前存在事务,加入该事务;
  • 若当前无事务,抛出 IllegalTransactionStateException 异常。

适用场景

核心写操作:必须在事务中执行(如“资金转账”,绝对不允许无事务执行,否则会导致资金不一致)。

实际案例

银行转账的核心扣款操作

@Service
public class TransferService {
    // 核心扣款(MANDATORY:必须在事务中执行)
    @Transactional(propagation = Propagation.MANDATORY)
    public void deductAccount(Long accountId, BigDecimal amount) {
        // 校验余额
        BigDecimal balance = jdbcTemplate.queryForObject(
            "SELECT balance FROM t_account WHERE id = ?",
            BigDecimal.class,
            accountId
        );
        if (balance.compareTo(amount) < 0) {
            throw new InsufficientBalanceException("余额不足");
        }
        // 扣减余额
        jdbcTemplate.update(
            "UPDATE t_account SET balance = balance - ? WHERE id = ?",
            amount, accountId
        );
    }

    // 转账主方法(REQUIRED:创建事务)
    @Transactional(propagation = Propagation.REQUIRED)
    public void transfer(Long fromId, Long toId, BigDecimal amount) {
        // 扣款(MANDATORY,复用transfer的事务)
        deductAccount(fromId, amount);
        // 加款(MANDATORY,复用transfer的事务)
        addAccount(toId, amount);
    }

    // 若直接调用deductAccount(无事务),会抛异常
    public void testDeductWithoutTransaction() {
        // 抛出 IllegalTransactionStateException:No existing transaction found for transaction marked with propagation 'mandatory'
        deductAccount(1L, new BigDecimal("100"));
    }
}

关键说明

  • MANDATORY 强制绑定外层事务,避免核心操作被“裸调用”导致数据不一致;
  • 通常用于“子操作必须依赖外层事务”的场景,如转账的扣款/加款操作。

4. REQUIRES_NEW:无论是否有事务,都新建事务(挂起当前事务)

定义

  • 若当前存在事务,先挂起该事务,创建新事务执行;
  • 若当前无事务,直接创建新事务;
  • 新事务与原事务完全独立(提交/回滚互不影响)。

适用场景

日志记录、消息发送等“独立事务”操作:即使核心业务事务回滚,这类操作也需要确保执行成功。

实际案例

下单失败时记录操作日志

@Service
public class OrderService {
    @Resource
    private OrderLogService orderLogService;

    // 下单(REQUIRED)
    @Transactional(propagation = Propagation.REQUIRED)
    public void createOrder(Long productId, Long userId, Integer num) {
        try {
            // 扣减库存、生成订单(核心业务)
            stockService.deductStock(productId, num);
            createOrderRecord(productId, userId, num);
        } catch (Exception e) {
            // 记录失败日志(REQUIRES_NEW:独立事务,即使下单回滚,日志仍保存)
            orderLogService.recordFailLog(userId, productId, num, e.getMessage());
            throw new OrderCreateException("下单失败", e);
        }
    }
}

@Service
public class OrderLogService {
    // 记录日志(REQUIRES_NEW:强制新建事务)
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void recordFailLog(Long userId, Long productId, Integer num, String reason) {
        jdbcTemplate.update(
            "INSERT INTO t_order_log (user_id, product_id, num, reason, create_time) VALUES (?, ?, ?, ?, NOW())",
            userId, productId, num, reason
        );
    }
}

关键说明

  • 若下单事务回滚(如库存不足),recordFailLog 的新事务已独立提交,日志仍会保存;
  • REQUIRES_NEW 会增加事务开销,需谨慎使用(仅用于“必须独立执行”的场景)。

5. NOT_SUPPORTED:以非事务方式执行,挂起当前事务

定义

  • 若当前存在事务,先挂起该事务,以非事务方式执行;
  • 若当前无事务,直接以非事务方式执行。

适用场景

耗时较长的非核心操作:不需要事务,且避免占用外层事务资源(如导出报表、生成大文件)。

实际案例

订单报表导出(耗时操作)

@Service
public class OrderReportService {
    // 导出订单报表(NOT_SUPPORTED:非事务执行,避免占用外层事务)
    @Transactional(propagation = Propagation.NOT_SUPPORTED)
    public void exportOrderReport(Long userId, LocalDate startDate, LocalDate endDate, OutputStream os) {
        // 1. 查询用户订单(大量数据,耗时)
        List<Order> orderList = jdbcTemplate.query(
            "SELECT id, create_time, amount FROM t_order WHERE user_id = ? AND create_time BETWEEN ? AND ?",
            new BeanPropertyRowMapper<>(Order.class),
            userId, startDate, endDate
        );
        // 2. 生成Excel并写入输出流(耗时IO操作)
        generateExcel(orderList, os);
    }
}

@Service
public class UserService {
    // 会员操作(REQUIRED:有事务)
    @Transactional
    public void updateUserVip(Long userId) {
        // 1. 更新会员等级(核心事务操作)
        jdbcTemplate.update("UPDATE t_user SET vip_level = 2 WHERE id = ?", userId);
        // 2. 导出该用户订单报表(NOT_SUPPORTED:挂起当前事务,非事务执行)
        try (OutputStream os = new FileOutputStream("/report/" + userId + ".xlsx")) {
            orderReportService.exportOrderReport(userId, LocalDate.now().minusMonths(1), LocalDate.now(), os);
        } catch (Exception e) {
            // 报表导出失败不影响会员升级
            log.error("导出报表失败", e);
        }
    }
}

关键说明

  • NOT_SUPPORTED 避免了“长耗时操作”占用外层事务资源(如数据库连接),防止事务超时;
  • 非事务执行的操作失败,不会影响外层事务的提交。

6. NEVER:以非事务方式执行,有事务则抛异常

定义

  • 若当前存在事务,抛出 IllegalTransactionStateException 异常;
  • 若当前无事务,以非事务方式执行。

适用场景

绝对不允许在事务中执行的操作:如“缓存刷新”“静态资源更新”,事务中执行会导致性能问题或逻辑冲突。

实际案例

刷新商品缓存(禁止事务执行)

@Service
public class ProductCacheService {
    // 刷新商品缓存(NEVER:禁止在事务中执行)
    @Transactional(propagation = Propagation.NEVER)
    public void refreshProductCache(Long productId) {
        // 1. 查询商品最新数据
        Product product = jdbcTemplate.queryForObject(
            "SELECT id, name, price FROM t_product WHERE id = ?",
            new BeanPropertyRowMapper<>(Product.class),
            productId
        );
        // 2. 更新Redis缓存
        redisTemplate.opsForValue().set("product:" + productId, JSON.toJSONString(product));
    }
}

// 场景1:非事务调用(正常执行)
@Controller
public class ProductController {
    @GetMapping("/product/refresh/{id}")
    public ResponseEntity<String> refreshCache(@PathVariable Long id) {
        productCacheService.refreshProductCache(id);
        return ResponseEntity.ok("缓存刷新成功");
    }
}

// 场景2:事务中调用(抛异常)
@Service
public class ProductService {
    @Transactional
    public void updateProductPrice(Long productId, BigDecimal newPrice) {
        // 更新商品价格(事务操作)
        jdbcTemplate.update("UPDATE t_product SET price = ? WHERE id = ?", newPrice, productId);
        // 尝试在事务中刷新缓存(NEVER:抛出 IllegalTransactionStateException)
        productCacheService.refreshProductCache(productId);
    }
}

关键说明

  • NEVER 强制禁止事务上下文,避免“缓存操作”被事务阻塞(如事务未提交时,缓存已更新导致数据不一致);
  • 适用于“纯非事务、高并发”的操作。

7. NESTED:嵌套事务(基于保存点)

定义

  • 若当前存在事务,在当前事务内创建“嵌套事务”(基于数据库保存点 Savepoint);
  • 若当前无事务,创建新事务(等价于 REQUIRED);
  • 嵌套事务特点:外层事务回滚会导致内层嵌套事务回滚,但内层嵌套事务回滚不会影响外层事务(仅回滚到保存点)。

适用场景

批量操作的部分回滚:如“批量下单”,某一个订单创建失败,仅回滚该订单,其他订单仍可提交。

实际案例

批量创建订单(支持部分失败)

@Service
public class BatchOrderService {
    @Resource
    private OrderService orderService;

    // 批量下单(REQUIRED:外层事务)
    @Transactional(propagation = Propagation.REQUIRED)
    public void batchCreateOrder(List<OrderRequest> requestList) {
        for (OrderRequest request : requestList) {
            try {
                // 单个订单创建(NESTED:嵌套事务)
                orderService.createSingleOrder(request);
            } catch (Exception e) {
                // 单个订单失败,仅回滚该嵌套事务,外层事务继续
                log.error("创建订单失败:{}", request.getProductId(), e);
            }
        }
    }
}

@Service
public class OrderService {
    // 单个订单创建(NESTED:嵌套事务)
    @Transactional(propagation = Propagation.NESTED)
    public void createSingleOrder(OrderRequest request) {
        // 扣减库存、生成订单(嵌套事务内操作)
        stockService.deductStock(request.getProductId(), request.getNum());
        createOrderRecord(request.getUserId(), request.getProductId(), request.getNum());
    }
}

关键说明

  • NESTED 依赖数据库对“保存点”的支持(如 MySQL、Oracle 均支持);
  • 内层嵌套事务回滚仅影响自身(回滚到保存点),外层事务可继续提交其他操作;
  • 若外层事务回滚,所有嵌套事务都会回滚(区别于 REQUIRES_NEW)。

三、传播行为核心对比表

传播行为 有外层事务 无外层事务 核心适用场景
REQUIRED 加入外层事务 新建事务 核心业务流程(下单、转账)
SUPPORTS 加入外层事务 非事务执行 查询操作(订单详情、数据列表)
MANDATORY 加入外层事务 抛异常 核心写操作(扣款、加款)
REQUIRES_NEW 挂起外层事务,新建事务 新建事务 独立日志、消息发送(需确保执行)
NOT_SUPPORTED 挂起外层事务,非事务执行 非事务执行 长耗时操作(报表导出、文件生成)
NEVER 抛异常 非事务执行 纯非事务操作(缓存刷新)
NESTED 嵌套事务(保存点) 新建事务 批量操作(部分失败可回滚)

四、使用注意事项

  1. 代理失效问题:传播行为仅对“跨 Bean 调用”生效(Spring 事务基于 AOP 代理),同一个 Bean 内的方法调用不会触发传播行为(需通过 ApplicationContext 获取代理对象);
  2. 数据库支持:NESTED 依赖数据库保存点,REQUIRES_NEW 依赖数据库事务隔离级别;
  3. 性能考量:REQUIRES_NEW/NESTED 会增加事务开销,避免滥用;
  4. 只读优化:查询场景(SUPPORTS)建议加 readOnly = true,数据库会优化只读事务(如 MySQL 跳过锁检查)。
0人推荐
随时随地看视频
慕课网APP