【Spring 事务】事务隔离级别与事务传播机制:从理论到业务落地实

Spring 的事务管理是企业级应用中最核心、最常用的功能之一,主要通过 @Transactional 注解实现声明式事务。事务的两个最重要属性就是:

  • 隔离级别(Isolation):解决并发读写时的数据一致性问题
  • 传播机制(Propagation):解决方法嵌套调用时的事务边界问题

下面从理论到实际业务场景,一步步带你彻底搞懂。

一、事务隔离级别(Isolation)

数据库标准定义了4种隔离级别(SQL-92),Spring 在此基础上提供了5种(多了一个 DEFAULT)。

隔离级别(Spring常量)脏读不可重复读幻读性能典型业务场景
DEFAULT跟随数据库默认(MySQL InnoDB 默认 REPEATABLE_READ
READ_UNCOMMITTED最高极少数对一致性要求极低、追求极致性能的场景(如某些实时监控计数)
READ_COMMITTED多数互联网业务默认选择(很多公司把 MySQL 改成这个级别)
REPEATABLE_READ (MySQL 默认)有(MySQL 通过间隙锁部分解决)中等金融、对账、库存等大部分核心业务
SERIALIZABLE最低极强一致性要求场景(如票务系统秒杀极度严格控超卖)

常见并发异常现象举例(以转账场景说明)

  • 脏读:A 事务给 B 转 1000 元(未提交),B 看到余额增加了 1000 元;A 回滚后,B 看到的是“幽灵钱”
  • 不可重复读:A 事务先查 B 余额 5000,准备扣款;B 事务同时给 B 转入 2000 并提交;A 再查余额变成 7000(前后不一致)
  • 幻读:A 事务查“余额 > 1000 的用户有 10 个”;B 事务插入一条余额 2000 的记录并提交;A 再查变成 11 个(凭空多了一行)

业务落地推荐选择

  • 大部分普通业务:READ_COMMITTEDREPEATABLE_READ(看团队对幻读的容忍度)
  • 金融、库存、账户余额、积分等核心场景:REPEATABLE_READ(MySQL 默认,性能够用,间隙锁能防大部分幻读)
  • 秒杀严格防超卖、票务严格不重复发:SERIALIZABLE(性能代价大,慎用)
  • 报表统计、日志类非核心读:READ_COMMITTED(甚至可以考虑读写分离+READ_COMMITTED)

代码示例

@Service
public class AccountService {

    // 账户核心操作,建议用 REPEATABLE_READ
    @Transactional(isolation = Isolation.REPEATABLE_READ)
    public void transfer(Long fromId, Long toId, BigDecimal amount) {
        // 扣钱 + 加钱
    }

    // 报表统计,可以降低隔离级别提升并发
    @Transactional(isolation = Isolation.READ_COMMITTED, readOnly = true)
    public ReportVO getDailyReport() {
        // ...
    }
}

二、事务传播机制(Propagation)

传播行为一共有 7 种,决定了外层事务内层事务的关系。

传播行为(最常用)英文名行为说明典型业务场景使用频率
REQUIRED (默认)REQUIRED有事务就加入,没有就新建绝大多数业务场景★★★★★
REQUIRES_NEWREQUIRES_NEW总是新建一个独立事务(外层事务会被挂起)日志记录、异步任务记录、邮件发送、积分变动监控★★★★
NESTEDNESTED如果有外层事务,则创建保存点(savepoint),允许内层部分回滚批量操作部分失败继续(外层捕获异常继续)★★★
SUPPORTSSUPPORTS有事务就加入,没有就不用事务查询类方法(可事务也可非事务)★★
NOT_SUPPORTEDNOT_SUPPORTED强制非事务执行(有外层事务则挂起)发 MQ、调用第三方接口、写 Redis 等不想参与事务★★
MANDATORYMANDATORY必须在已有事务中运行,否则抛异常强制要求必须有事务的子方法
NEVERNEVER禁止在事务中运行,有事务就抛异常极少数只读初始化逻辑

最核心的三种对比(80% 的场景只用这三种)

场景推荐传播行为说明
正常业务逻辑REQUIRED内外一起成功/一起失败
记录业务日志、审计日志REQUIRES_NEW即使主业务失败,日志也要保存下来
批量导入,部分失败继续NESTED内层抛异常可以回滚部分操作,外层捕获异常继续处理剩余数据
发 MQ、调用外部 HTTP 接口NOT_SUPPORTED不想让这些慢操作把主事务拖得很长,甚至导致死锁
报表统计、排行榜读取SUPPORTS有事务环境就复用,没有就不开事务(节省资源)

真实业务落地案例(非常常见组合)

@Service
public class OrderService {

    @Autowired
    private OrderDao orderDao;
    @Autowired
    private LogDao logDao;
    @Autowired
    private PointsService pointsService;

    // 主订单创建事务
    @Transactional(rollbackFor = Exception.class)
    public void createOrder(CreateOrderDTO dto) {
        // 1. 创建订单
        Long orderId = orderDao.createOrder(dto);

        try {
            // 2. 扣库存(可能抛出库存不足异常)
            stockService.decreaseStock(dto.getSkuId(), dto.getQuantity());
        } catch (StockNotEnoughException e) {
            // 库存不足,订单失败,但日志仍需记录
            throw new BusinessException("库存不足");
        }

        // 3. 发积分(即使订单失败也要记录积分失败日志)
        pointsService.addPointsForOrder(orderId, dto.getUserId(), dto.getAmount());

        // 4. 记录操作日志(无论成功失败都要记录)
        logOrderOperation(orderId, "CREATE", "成功");
    }

    // 重要:日志必须独立事务
    @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
    public void logOrderOperation(Long orderId, String operation, String remark) {
        logDao.insertOrderLog(orderId, operation, remark);
    }
}

另一个经典场景:批量操作 + 部分失败继续

@Transactional(rollbackFor = Exception.class)
public void batchImport(List<User> users) {
    for (User user : users) {
        try {
            // 内层用 NESTED,支持部分回滚
            importSingleUser(user);
        } catch (Exception e) {
            log.error("导入用户失败,继续处理下一个", e);
            // 这里不抛出,外层继续
        }
    }
}

@Transactional(propagation = Propagation.NESTED, rollbackFor = Exception.class)
public void importSingleUser(User user) {
    // 插入用户 + 插入扩展信息
    // 任意一步失败 → 只回滚本条用户
}

三、快速记忆口诀

  • 隔离级别:越严格一致性越好,性能越差
    普通业务 → REPEATABLE_READ
    报表/非核心 → READ_COMMITTED
    极致一致 → SERIALIZABLE
  • 传播行为
    一起死一起活 → REQUIRED(默认)
    我死你活(日志、监控) → REQUIRES_NEW
    你可以部分死,我还能继续 → NESTED
    我不想掺和事务 → NOT_SUPPORTED / SUPPORTS

四、总结推荐配置(生产常用组合)

@Transactional(
    propagation = Propagation.REQUIRED,        // 默认就行
    isolation = Isolation.REPEATABLE_READ,      // 大部分公司核心业务用这个
    rollbackFor = {Exception.class},            // 建议全部回滚
    readOnly = false,                           // 读写事务
    timeout = 30                                // 超时时间根据业务设置
)

掌握了隔离级别与传播机制,就等于掌握了 Spring 事务 80% 的核心。实际项目中,80% 的方法用默认 REQUIRED + REPEATABLE_READ 就够用了,其余 20% 根据业务特性微调即可。

有具体业务场景想讨论如何设置传播/隔离吗?可以告诉我,我帮你分析最合适的配置。

文章已创建 4426

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

相关文章

开始在上面输入您的搜索词,然后按回车进行搜索。按ESC取消。

返回顶部