前言:
你还在死记硬背Java事务传播方式吗,你是否为事务传播特性差异难以理解而苦恼吗,下面我将通过一个简单的事务行为分类,让你彻底理解和记住Java的七种事务传播行为。
首先我们看一下Java支持哪些事务传播行为
| 传播行为 | 核心逻辑(面对当前事务) | 是否必须有事务? |
|---|---|---|
REQUIRED(默认) | 有事务就加入,没有就新建一个。 | 是(没有就建) |
SUPPORTS | 有事务就加入,没有就以非事务方式运行。 | 否(随缘) |
MANDATORY | 必须已有事务,否则抛异常。 | 是(必须存在) |
REQUIRES_NEW | 挂起当前事务,永远新建一个独立事务。 | 是(强制新建) |
NOT_SUPPORTED | 挂起当前事务,以非事务方式运行。 | 否(拒绝事务) |
NEVER | 必须没有事务,否则抛异常。 | 否(强制非事务) |
NESTED | 如果存在事务,则创建嵌套事务(保存点);否则新建事务。 | 是(存在则嵌套) |
我们通过这些行为的特性,分类为下面四种:支持派、反对派、独立派和分支派
| 派系 | 关键字 | 包含行为 | 核心逻辑(人话版) | 经典场景 |
|---|---|---|---|---|
| 支持派 | 同生共死 | REQUIREDSUPPORTSMANDATORY | 有事务就加入, 没事务时
| 常规业务(增删改查),99%的代码都用REQUIRED |
| 独立派 | 各不相干 | REQUIRES_NEWNOT_SUPPORTED | 挂起外面的事务,自己单独玩
| 审计日志(业务失败也要存)、大文件导出(释放锁) |
| 反对派 | 拒之门外 | NEVER | 检测到有事务就直接报错,绝不妥协 | 缓存清理、全文索引(防止死锁) |
| 分支派 | 主干分支 | NESTED | 寄生在主事务里,但自己打标记;局部失败只回滚自己,主干失败全完蛋 | 批量导入(1000条里坏几条,只回滚坏的那几条) |
想象下面这个后厨工作场景
主厨(主事务):正在炖一锅巨大的佛跳墙(主事务)。
支持派:你进来帮主厨切菜,切坏了(异常)整锅倒掉。
独立派:你端走一口新锅,去旁边单独炒个青菜,炒糊了只倒自己这盘。
反对派:你看到主厨在炖佛跳墙,嫌油烟大,直接摔围裙走人(报错),要求必须关火你才进厨房。
分支派(
NESTED):主厨让你在这个大锅旁边拿个小碗调点酱汁。调坏了(异常)只把你小碗里的倒掉(回滚到保存点),大锅继续炖;但如果主厨心情不好,把整锅佛跳墙都泼了(主事务回滚),你那碗酱汁就算调好了也得跟着倒进下水道。
代码样例:
@Service public class OrderService { @Autowired private InventoryService inventoryService; @Autowired private AuditLogService auditLogService; @Autowired private PromotionService promotionService; @Autowired private NotificationService notificationService; @Autowired private QueryService queryService; @Autowired private SecurityService securityService; @Autowired private CacheService cacheService; /** * 场景 1:REQUIRED(默认) * 主事务入口,整个下单流程的大事务。 * 如果后续子事务抛出异常(且未捕获),所有 REQUIRED 和 NESTED 操作都会回滚。 */ @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class) public void placeOrder(String orderId) { System.out.println("========== 主事务开始 =========="); // 1. 扣减核心库存(REQUIRED -> 加入当前事务) inventoryService.deductCoreStock(orderId); // 2. 校验安全规则(MANDATORY -> 必须在当前事务内) securityService.verifySecurity(orderId); // 3. 批量扣减优惠券(NESTED -> 使用保存点,部分回滚) promotionService.batchDeductCoupons(orderId); // 4. 记录操作日志(REQUIRES_NEW -> 独立事务,不受主事务回滚影响) auditLogService.logOrderCreation(orderId); // 5. 发送短信(NOT_SUPPORTED -> 挂起当前事务,非事务执行) notificationService.sendSms(orderId); // 6. 查询额外数据(SUPPORTS -> 随缘事务,只读查询) queryService.getProductInfo(orderId); // 7. 清除本地缓存(NEVER -> 若当前有事务则抛异常) cacheService.evictLocalCache(orderId); System.out.println("========== 主事务提交 =========="); } } @Service public class InventoryService { /** * 场景 2:REQUIRED(加入主事务) * 扣减核心库存。如果这里抛异常,主事务整体回滚。 */ @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class) public void deductCoreStock(String orderId) { System.out.println(" [REQUIRED] 扣减核心库存,加入主事务"); // 模拟数据库操作 // int rows = stockMapper.deduct("product_001", 1); // if (rows == 0) throw new RuntimeException("库存不足"); } } @Service public class SecurityService { /** * 场景 3:MANDATORY(必须存在事务) * 强制要求调用方必须在事务内。如果脱离事务单独调用,直接抛出异常。 * 适用:敏感操作强制与业务事务绑定。 */ @Transactional(propagation = Propagation.MANDATORY) public void verifySecurity(String orderId) { System.out.println(" [MANDATORY] 校验安全规则(必须在事务中)"); // 如果当前没有事务,这里会抛出 IllegalTransactionStateException } } @Service public class PromotionService { /** * 场景 4:NESTED(嵌套事务,基于保存点 Savepoint) * 批量扣减多张优惠券。假设第3张券失效,仅回滚到保存点(即只回滚第3张), * 不影响主事务和其他券的扣减。 */ @Transactional(propagation = Propagation.NESTED) public void batchDeductCoupons(String orderId) { System.out.println(" [NESTED] 开始批量扣减优惠券"); String[] coupons = {"COUPON_A", "COUPON_B", "COUPON_C"}; for (int i = 0; i < coupons.length; i++) { // 模拟扣减第3张券时失败 if (i == 2) { System.out.println(" [NESTED] 优惠券 C 扣减失败,仅回滚本保存点"); throw new RuntimeException("优惠券无效"); } System.out.println(" [NESTED] 优惠券 " + coupons[i] + " 扣减成功"); } // 注意:即使这里抛异常,主事务中前面扣减的库存不会回滚,除非主事务捕获异常后主动回滚。 // 在实际开发中,通常需要捕获 NESTED 的异常,决定是否继续。 } } @Service public class AuditLogService { /** * 场景 5:REQUIRES_NEW(独立事务) * 无论主订单是否回滚(比如库存不足导致下单失败), * "用户尝试下单"的操作日志必须持久化保存,用于审计排查。 */ @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class) public void logOrderCreation(String orderId) { System.out.println(" [REQUIRES_NEW] 独立事务:记录操作日志"); // 这里先挂起主事务,开启新连接/事务,执行 insert,立即提交。 } } @Service public class NotificationService { /** * 场景 6:NOT_SUPPORTED(不支持事务) * 发送短信或邮件涉及网络IO,耗时长。为了避免长时间占用数据库连接, * 挂起当前事务,以非事务方式执行。 * 如果发送失败,不影响主事务的回滚(因为它不在事务内)。 */ @Transactional(propagation = Propagation.NOT_SUPPORTED) public void sendSms(String orderId) { System.out.println(" [NOT_SUPPORTED] 非事务:发送下单短信通知"); // 模拟耗时短信发送 try { Thread.sleep(100); } catch (InterruptedException e) {} // 这里异常不会导致主事务回滚(因为主事务被挂起后,这里无事务) } } @Service public class QueryService { /** * 场景 7:SUPPORTS(支持事务,但不强制) * 查询操作。如果当前有事务,则加入(并可以使用 readOnly 优化); * 如果没有事务,则以普通非事务查询执行。 * 通常用于 Service 层内部的只读查询。 */ @Transactional(propagation = Propagation.SUPPORTS, readOnly = true) public void getProductInfo(String orderId) { System.out.println(" [SUPPORTS] 只读查询(有事务则加入,无则忽略)"); // select * from product where id = ... } } @Service public class CacheService { /** * 场景 8:NEVER(禁止事务) * 强制要求当前不能存在事务。如果在主事务中被调用,会直接抛出异常。 * 场景:某些缓存更新(如 Redis)或全文索引同步,如果在事务中执行, * 可能会因为数据库长事务导致死锁或性能下降。 */ @Transactional(propagation = Propagation.NEVER) public void evictLocalCache(String orderId) { System.out.println(" [NEVER] 清除本地缓存(强制无事务)"); // 如果当前存在事务,抛出 IllegalTransactionStateException } }