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_COMMITTED 或 REPEATABLE_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_NEW | REQUIRES_NEW | 总是新建一个独立事务(外层事务会被挂起) | 日志记录、异步任务记录、邮件发送、积分变动监控 | ★★★★ |
| NESTED | NESTED | 如果有外层事务,则创建保存点(savepoint),允许内层部分回滚 | 批量操作部分失败继续(外层捕获异常继续) | ★★★ |
| SUPPORTS | SUPPORTS | 有事务就加入,没有就不用事务 | 查询类方法(可事务也可非事务) | ★★ |
| NOT_SUPPORTED | NOT_SUPPORTED | 强制非事务执行(有外层事务则挂起) | 发 MQ、调用第三方接口、写 Redis 等不想参与事务 | ★★ |
| MANDATORY | MANDATORY | 必须在已有事务中运行,否则抛异常 | 强制要求必须有事务的子方法 | ★ |
| NEVER | NEVER | 禁止在事务中运行,有事务就抛异常 | 极少数只读初始化逻辑 | ★ |
最核心的三种对比(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% 根据业务特性微调即可。
有具体业务场景想讨论如何设置传播/隔离吗?可以告诉我,我帮你分析最合适的配置。