前言:
你还在死记硬背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
}
}

1万+

被折叠的 条评论
为什么被折叠?



