一、场景概述
想象一个用户注册流程:
- 保存用户基本信息(核心操作)
- 初始化用户账户(重要但可独立失败)
- 发送欢迎邮件(非关键操作)
二、代码事务传播分析
1. 主事务:用户注册(REQUIRED)
@Transactional(propagation = Propagation.REQUIRED)
public void register(User user) {
// 保存用户(主事务操作)
userDao.save(user);
// 初始化账户(嵌套事务)
accountService.initAccount(user.getId());
// 发送邮件(非事务操作)
emailService.sendWelcomeEmail(user.getEmail());
}
2. 子事务:账户初始化(NESTED)
@Transactional(propagation = Propagation.NESTED)
public void initAccount(Long userId) {
// 初始化账户操作
}
三、事务传播机制详解
1. REQUIRED(主事务)
- 行为:有则加入,无则新建
- 当前场景:当
register()
被调用时- 没有现有事务 → 创建新事务(事务A)
- 所有操作都在事务A中执行
2. NESTED(嵌套事务)
- 行为:在现有事务中创建"嵌套事务"
- 关键特性:
- 设置数据库保存点(savepoint)
- 可独立回滚不影响主事务
- 主事务回滚会连带回滚嵌套事务
四、场景执行分析
场景1:完美流程(全部成功)
- 保存用户 → 成功
- 初始化账户 → 成功
- 发送邮件 → 成功
- 提交主事务 → 所有操作生效
结果:用户创建成功,账户初始化完成,邮件已发送
场景2:账户初始化失败
- 保存用户 → 成功
- 初始化账户 → 失败(抛出异常)
- 捕获异常 → 记录日志
- 发送邮件 → 成功
- 提交主事务 → 仅保存用户操作生效
结果:
- ✅ 用户创建成功
- ❌ 账户初始化失败(但被捕获)
- ✅ 邮件已发送
场景3:主事务失败
- 保存用户 → 成功
- 初始化账户 → 成功
- 发送邮件前系统崩溃 → 主事务回滚
结果:
- ❌ 用户创建回滚
- ❌ 账户初始化回滚(连带回滚)
- ❌ 邮件未发送
五、嵌套事务的本质
1. 数据库保存点(Savepoint)
START TRANSACTION; -- 主事务开始
-- 主操作
INSERT INTO users (...) VALUES (...);
SAVEPOINT sp1; -- 设置保存点
-- 嵌套操作
INSERT INTO accounts (...) VALUES (...);
-- 如果嵌套操作失败
ROLLBACK TO sp1; -- 回滚到保存点
-- 继续其他操作...
COMMIT; -- 提交主事务
2. 嵌套事务特性
特性 | 说明 | 示例 |
---|---|---|
独立回滚 | 嵌套事务可单独回滚 | 账户初始化失败不影响用户保存 |
依赖提交 | 嵌套操作随主事务提交 | 主事务成功才真正生效 |
连带回滚 | 主事务回滚导致嵌套回滚 | 用户保存失败导致账户初始化回滚 |
六、为什么这样设计?
1. 业务需求分析
操作 | 重要性 | 事务要求 |
---|---|---|
保存用户 | 关键 | 必须成功 |
初始化账户 | 重要但可重试 | 可独立失败 |
发送邮件 | 非关键 | 无需事务 |
2. 事务选择依据
- REQUIRED:主操作必须保证原子性
- NESTED:重要但可失败的操作
- 无事务:非关键操作
七、对比其他传播行为
1. 如果使用REQUIRES_NEW
// 账户服务
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void initAccount(Long userId) { ... }
问题:
- 账户初始化完全独立事务
- 即使主事务回滚,账户操作仍可能提交
- 导致数据不一致(用户不存在但账户存在)
2. 如果使用SUPPORTS
// 账户服务
@Transactional(propagation = Propagation.SUPPORTS)
public void initAccount(Long userId) { ... }
问题:
- 没有独立事务控制
- 账户失败会导致主事务回滚
- 用户保存也会被回滚
八、最佳实践总结
1. 事务传播选择指南
场景 | 推荐传播行为 |
---|---|
核心操作(必须成功) | REQUIRED |
重要但可失败的操作 | NESTED |
非关键操作 | 无事务或NOT_SUPPORTED |
完全独立操作 | REQUIRES_NEW |
2. 嵌套事务使用要点
- 数据库支持:MySQL InnoDB等支持保存点的引擎
- 异常处理:必须捕获嵌套事务异常
- 性能考虑:不宜嵌套过深
- 业务对齐:嵌套事务必须属于同一业务单元
九、扩展思考
1. 如果邮件服务需要事务?
// 邮件服务
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void sendWelcomeEmail(String email) {
// 记录邮件发送日志(需要事务)
emailLogDao.save(new EmailLog(email));
// 实际发送邮件
emailClient.send(email);
}
解决方案
@Service
public class EmailService {
// 邮件日志服务(使用REQUIRES_NEW)
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void logEmail(String email) {
emailLogDao.save(new EmailLog(email));
}
// 发送服务(无事务)
public void sendWelcomeEmail(String email) {
logEmail(email); // 独立事务记录日志
emailClient.send(email); // 非事务操作
}
}
2. 多服务嵌套场景
public void register(User user) {
userDao.save(user); // 主事务
accountService.initAccount(user.getId()); // 嵌套事务
profileService.initProfile(user.getId()); // 另一个嵌套事务
}
处理原则:
- 每个嵌套事务设置独立保存点
- 分别捕获处理异常
- 确保主事务不受影响
十、总结
在这个用户注册场景中,我们通过合理的事务传播机制设计:
- REQUIRED 保证核心操作(用户保存)的原子性
- NESTED 处理重要但可失败的操作(账户初始化)
- 无事务 执行非关键操作(邮件发送)
这种设计实现了:
- 核心业务100%可靠
- 重要业务可独立失败不影响主流程
- 非关键业务不阻塞主事务
事务传播机制的本质是:根据业务重要性,为不同操作匹配合适的事务保证级别