Spring @Transactional 核心避坑
2025/6/6大约 5 分钟后端Java
@Transactional 事务核心复盘
Transactional事务,是依靠代理对象,通过异常触发事务的提交与回滚。
传播行为本质就是会不会影响到父与子之间的事务状态。
REQUIRED是在一条大船上,REQUIRES_NEW则是独立出来的救生艇。
- 当执行到
REQUIRES_NEW时,主线程并没有分裂。主线程只是把手里的“数据库连接 A(大船)”暂时放在桌子上,然后把手伸进连接池,拿了一根“全新的数据库连接 B(救生艇)”去执行子方法。执行完子方法提交后,主线程把连接 B 还原给连接池,重新从桌子上捡起连接 A 继续往前跑。- 当您的项目规模还没那么大,没有引入 RocketMQ/RabbitMQ 等中间件,或者业务要求极度严苛,日志必须和订单强同步写入(不能允许 MQ 的短暂延迟)时,我们才会在本地 MySQL 中使用
REQUIRES_NEW。(但这很有可能会带来数据库连接池的一个浪费,在高并发的情况下,这是绝对不被允许的)
@Transactional 事务核心复盘
├── 1. 为什么强制加 (rollbackFor = Exception.class)?
│ ├── Spring 默认策略:仅对 RuntimeException 和 Error 回滚,对 Checked Exception(如 IOException)放行提交。(🌟我们自己自定义的一些异常,一般来说是继承 exception 的,所以这就很有可能会导致问题的出现)
│ └── 生产灾难:优惠券核销抛出 Checked Exception 时,订单照样提交,导致“券没扣、单成了”的数据不一致。
│
├── 2. 事务传播行为 (Propagation) 常用的就两个
│ ├── REQUIRED(默认):抱团取暖。加入当前大事务,同生共死。
│ └── REQUIRES_NEW:划清界限。挂起大事务,向连接池要新连接,开启独立小事务,无论大事务死活必须提交(如:写审计日志)。
Propagation.SUPPORTS、Propagation.NOT_SUPPORTED
│
├── 3. 事务失效的两大本质病因
│ ├── 病因 A:代理机制失效(AOP 局限性)
│ │ └── 表现:在同类内部“直接调用”或“用 this. 调用”子方法。
│ │ └── 本质:JVM 内部直接跳转内存地址,请求没有撞上大门口的“代理对象(保安)”,注解形同虚设。
│ └── 病因 B:异常感知失效(异常被吞)
│ └── 表现:在事务方法内部使用 try-catch 把异常消化了,没有重新 throw。
│ └── 本质:门口的“保安”由于抓不到抛出的异常,以为一切顺利,直接触发了 commit 提交。
│
└── 4. 大师级的工程规避方案
├── 严格分家:凡是涉及不同的事务边界或 Propagation,必须将方法拆分到不同的类(Service)中,通过 @Autowired 注入后跨类调用。
└── 拒绝吞异常:如果一定要写 try-catch 记日志,在 catch 块的末尾必须 throw e 重新抛出;或者手动标记回滚。
类比几个:
- 事务(Transaction)是依靠代理对象,通过异常触发事务的提交与回滚。
- 异步(Async)是依靠代理对象,通过线程池触发新线程的开启与执行。
- 缓存(Cacheable)是依靠代理对象,通过切面拦截触发缓存的读取与写入。
🛠️ 优惠券/订单沙盘实战代码演练(正确姿势)
为了彻底规避同类调用失效以及异常被吞的问题,生产环境的标准多表更新架构应当无情地进行跨类拆分:
1. 订单核心主服务(管大局)
@Service
public class OrderService {
@Autowired
private CouponService couponService; // 🌟 注入外部服务,确保请求能撞上 CouponService 的代理保安
@Autowired
private AuditLogService auditLogService;
@Transactional(rollbackFor = Exception.class) // 🌟 1. 宽口径兜底,任何异常皆回滚
public void completeOrder_A(List<UserCoupon> coupons) {
// 步骤 1:执行本地 SQL,更新订单表
// database.insertOrder(...);
// 步骤 2:记录用户尝试下单的审计日志(不管大事务成败,日志必须留存)
// 🌟 REQUIRED_NEW 生效:通过注入的对象调用,保安为其向连接池申请新连接,独立提交
auditLogService.writeLog("用户尝试下单");
try {
// 步骤 3:核销优惠券
// 🌟 REQUIRED 生效:通过注入的对象调用,加入当前大事务,同生共死
couponService.deductCoupon(coupons);
} catch (Exception e) {
log.error("优惠券核销失败,准备抛出异常触发大事务回滚", e);
// 🌟 2. 绝不盲目吞异常:必须重新抛出,让门口的 OrderService 代理保安抓到,从而触发整体回滚
throw e;
}
}
}
2. 优惠券独立服务(管业务子局)
@Service
public class CouponService {
// 默认是 Propagation.REQUIRED,会自动融入主服务的事务中,共用同一个数据库连接
@Transactional(rollbackFor = Exception.class)
public void deductCoupon(List<UserCoupon> coupons) {
// 执行 SQL:UPDATE coupon SET status = 'USED' ...
}
}
3. 审计日志独立服务(管全局留痕)
@Service
public class AuditLogService {
// 🌟 划清界限:不管外面订单成不成功,日志必须铁打不动地写入数据库
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public void writeLog(String message) {
// 执行 SQL:INSERT INTO sys_audit_log ...
}
}
注意清单
- 是否 xxService 调用?不是很明显错了,立刻重构,通过外部注入或拆分服务来消灭内部调用,让 spring AOP 去管理代理类;
- 绝不能在当前类内部进行直接调用,或者使用
this关键字调用带事务的方法。由于内部调用属于 JVM 内存的直接跳转,会完美绕过 Spring 的动态代理对象,导致注解形同虚设。凡是涉及不同事务边界、不同传播行为的业务子逻辑,必须无情地将其解耦并拆分到外部服务(External Service)中,通过 Spring 依赖注入(DI)实现跨类调用,以此捍卫 AOP 管理机制的绝对权威。
- 绝不能在当前类内部进行直接调用,或者使用
- 异常自己吞掉?不对,异常要抛出去给代理对象去处理
