研究某个Mall项目的订单代码时,我发现一个有趣的现象:一个方法操作了6张表,包含14步业务逻辑,全程都在一个事务里,居然没有出问题。 深入分析后,我找到了6种比单纯使用 @Transactional 注解更灵活、更能解决实际痛点的方案,并通过一个demo项目进行了验证。
我们需要解决的痛点
在日常开发中,你是否也遇到过这些 @Transactional 难以应对的场景?
- 库存不足时:业务上希望保留订单记录并标记为“待补货”,但不知道如何避免事务回滚。
- 发送MQ消息:在事务方法里发送了消息,结果事务回滚了,消息却已经发出去了。
- 批量操作:100个订单需要发货,其中1个失败,结果所有操作都回滚了,但实际需求是让成功的继续。
- 记录日志:即使业务失败了,也想记录下日志用于分析,但事务一回滚,日志也没了。
- 隔离级别/超时:对
@Transactional 注解里的那些参数(如 isolation, timeout)具体怎么用、何时用感到模糊。
这篇文章将通过实际的代码演示,逐一拆解这6种解决方案。
文章目录
- 编程式事务:精准区分业务失败与系统异常
@Transactional 参数详解:隔离级别与超时设置的坑
- 事务同步器:确保在事务提交后再发送MQ消息
- 事务事件监听:解耦核心业务与副作用操作
- 手动控制事务:实现灵活、健壮的批量处理
- 事务传播机制:3种最常用场景的深度剖析
关于Demo项目
本文所有示例代码均来自项目:https://gitee.com/sh_wangwanbao/simple-transactional
项目特点:集成了测试框架,通过反射自动构建测试参数,启动即可运行测试,并自动生成Markdown格式的测试报告。你无需手动准备测试数据,也无需逐个执行测试用例。
使用方法:导入数据库脚本(doc/simple-transactional-init.sql),修改一下数据库配置,启动项目即可查看完整的测试结果。
编程式事务:区分业务失败和系统异常
这个场景源自真实项目:订单创建后需要调用风控服务进行检查。
- 风控不通过(业务规则):订单需要保留,标记为“待审核”,等待人工复核。
- 风控服务挂了(系统故障):订单必须回滚,不能留下脏数据。
使用 @Transactional 注解做不到这一点。 因为它只能通过抛出异常来触发回滚,无法区分这两种不同的“失败”类型。
TransactionTemplate 可以动态控制回滚
使用 TransactionTemplate 可以让我们在代码中精确控制事务的提交与回滚。
public OrderResult createOrder(OrderParam param) {
return transactionTemplate.execute(status -> {
try {
// 1. 创建订单
Order order = buildOrder(param);
orderMapper.insert(order);
// 2. 创建订单商品
List<OrderItem> items = buildOrderItems(order);
orderItemMapper.batchInsert(items);
// 3. 锁定库存
lockStock(param.getItems());
// 4. 调用风控服务检查
RiskCheckResult riskResult = riskService.check(order);
if (!riskResult.isPass()) {
// 风控不通过 - 业务失败,但不回滚
order.setStatus(OrderStatus.WAIT_AUDIT); // 待审核
order.setNote("风控检查未通过:" + riskResult.getReason());
orderMapper.updateById(order);
// 关键:不调用 status.setRollbackOnly()
// 订单和商品明细都会保留在数据库中
return OrderResult.fail("订单需人工审核");
}
// 风控通过,订单正常
return OrderResult.success(order.getId());
} catch (RiskServiceException e) {
// 风控服务异常 - 系统故障,必须回滚
log.error("风控服务异常", e);
status.setRollbackOnly();
return OrderResult.error("系统异常,请稍后重试");
} catch (Exception e) {
// 其他异常也回滚
status.setRollbackOnly();
return OrderResult.error(e.getMessage());
}
});
}
编程式事务的核心价值
下表清晰地展示了它与声明式事务的区别:
| 场景 |
@Transactional |
TransactionTemplate |
| 风控不通过(业务) |
抛异常 → 全回滚 |
不回滚,保留订单 |
| 风控服务挂了(系统) |
抛异常 → 全回滚 |
回滚,不留脏数据 |
| 库存不足(业务) |
抛异常 → 全回滚 |
保留订单,标记“待补货” |
核心区别:编程式事务能让我们在代码中动态决定是否回滚,从而清晰地区分“业务规则失败”和“系统异常故障”。
测试结果也印证了这一点:
# 测试风控不通过(高金额订单)
POST /programmatic/risk-check
# 结果
订单ID:8
订单状态:待审核
订单备注:风控检查未通过:金额过高
数据库:订单和商品明细都保留了
@Transactional 的参数,我被坑过
在原项目中,商品创建方法是这么写的:
@Transactional(
isolation = Isolation.REPEATABLE_READ, // 隔离级别
propagation = Propagation.REQUIRED, // 传播行为
timeout = 30, // 超时时间
rollbackFor = Exception.class // 回滚规则
)
public int createProduct(ProductParam param) {
// 插入8张表...
}
我以前也习惯直接写 @Transactional,很少加参数。直到踩了几次坑,才明白这些参数的重要性。
isolation 隔离级别参数要注意
有一次项目将数据库从 MySQL 切换到了 PostgreSQL,突然出现了幻读问题。
原因在于:
- MySQL 默认隔离级别是
REPEATABLE_READ(可重复读)
- PostgreSQL 默认隔离级别是
READ_COMMITTED(读已提交)
如果代码中没有显式指定隔离级别,更换数据库环境就可能导致程序行为不一致,进而引发隐蔽的并发问题。
因此,建议在关键事务方法中明确指定隔离级别:
// 明确指定隔离级别,不依赖数据库的默认值
@Transactional(isolation = Isolation.REPEATABLE_READ)
public void someMethod() {
// 这样可以避免因环境切换导致的事务行为变化
}
timeout 和 rollbackFor 参数
timeout:防止长事务锁住数据库表,影响系统整体性能。
@Transactional(timeout = 30) // 设置30秒超时
public void complexTask() {
// ... 长时间的业务操作
}
rollbackFor:Spring 默认只在抛出 RuntimeException 及其子类时才回滚事务,对于 Checked Exception(如 Exception)默认是不回滚的。这常常与直觉相悖。
@Transactional(rollbackFor = Exception.class) // 明确指定所有Exception都触发回滚
public void createOrder() throws Exception {
// ...
}
养成设置这两个参数的习惯,能帮你规避很多潜在的坑。
事务提交后发MQ,我之前都做错了
订单创建成功后,通常需要发送一个MQ消息(例如,30分钟后自动取消未支付的订单)。
我以前是这么写的:
@Transactional
public void createOrder() {
orderMapper.insert(order);
// 直接发送MQ
mqSender.send(“order.cancel.delay”, order.getId());
}
看起来逻辑清晰,但实际上存在一个致命问题。
问题出在时机上
问题的本质是:MQ消息成功发出去了,但随后事务因某些原因(如库存不足)回滚了,导致订单根本不存在于数据库中。30分钟后,消息消费者去取消一个不存在的订单,引发错误。
这就是 “副作用操作与事务一致性” 的问题:
- 订单插入、库存扣减 → 在同一个事务里,遵循原子性(要么全成功,要么全回滚)。
- 发送MQ消息 → 操作不在数据库事务管理范围内,一旦发出就无法撤回。
事务同步器解决这个问题
Spring 提供了事务生命周期的钩子(TransactionSynchronization),允许我们在事务的特定阶段执行回调逻辑。
@Transactional
public void createOrder() {
orderMapper.insert(order);
// 注册事务同步器
TransactionSynchronizationManager.registerSynchronization(
new TransactionSynchronization() {
@Override
public void afterCommit() {
// 只有事务提交成功,这里才会执行
mqSender.send(“order.cancel.delay”, order.getId());
log.info(“MQ消息已发送”);
}
@Override
public void afterCompletion(int status) {
if (status == STATUS_ROLLED_BACK) {
log.info(“事务回滚,MQ消息不会发送”);
}
}
}
);
}
现在的执行时序是健康的:只有当订单数据真正提交到数据库后,才会发送MQ消息。如果事务回滚了,消息则不会被发送。
事务同步器的4个生命周期钩子
TransactionSynchronization 提供了4个关键的回调点,让我们可以介入事务的不同阶段:
TransactionSynchronizationManager.registerSynchronization(
new TransactionSynchronization() {
@Override
public void beforeCommit(boolean readOnly) {
log.info(“【阶段1-beforeCommit】事务即将提交”);
// 可以在这里做最后的数据一致性校验
}
@Override
public void beforeCompletion() {
log.info(“【阶段2-beforeCompletion】事务即将完成”);
// 可以在这里清理线程局部变量等临时资源
}
@Override
public void afterCommit() {
log.info(“【阶段3-afterCommit】事务已提交”);
// 数据已持久化,可以安全地发MQ、清理缓存等
}
@Override
public void afterCompletion(int status) {
String statusStr = (status == STATUS_COMMITTED) ? “提交” : “回滚”;
log.info(“【阶段4-afterCompletion】事务已完成,状态:{}”, statusStr);
// 无论成功失败都会执行,适合做一些最终清理
}
}
);
哪些场景必须用 afterCommit
所有“对外部的、不可逆的副作用操作”都应该考虑放在 afterCommit 中执行:
- 场景1:发送MQ消息
@Override
public void afterCommit() {
mqSender.send(“order.cancel.delay”, orderId);
}
- 场景2:清理外部缓存(如 Redis)
@Override
public void afterCommit() {
redisTemplate.delete(“product:” + productId);
}
- 场景3:记录业务日志到另一个独立的数据库
@Override
public void afterCommit() {
logMapper.insert(businessLog); // 写到另一个库,不在当前事务内
}
- 场景4:调用第三方外部服务
@Override
public void afterCommit() {
thirdPartyService.notify(order);
}
核心原则:只有核心业务数据被持久化后,才能通知外部世界,避免产生不一致的状态。
同库的日志也要用 afterCommit 吗?
理论上,如果日志表和订单表在同一个数据库的同一个事务里,即使提前插入日志,事务回滚时日志也会被一起回滚,似乎没有问题。
但在实际业务中,我们通常希望:
- 解耦:将订单核心业务逻辑和日志记录逻辑分离。
- 性能:日志操作(可能是IO密集型)不应影响主事务的耗时。
- 健壮性:即使日志记录失败,也应该有独立的重试机制,而不应影响主业务事务的成功。
因此,即使日志库相同,也建议通过 afterCommit 或后续介绍的事件机制进行解耦。
测试输出验证了执行顺序的可靠性:
【阶段1-beforeCommit】事务即将提交
【阶段2-beforeCompletion】事务即将完成
【阶段3-afterCommit】事务已提交
【阶段4-afterCompletion】事务已完成,状态:提交
MQ消息已发送
事务事件监听:解耦副作用操作
设想订单创建成功后,需要做三件事:发送MQ消息、记录操作日志、发送用户通知。
如果全部写在主业务方法里,代码会变得臃肿且职责不清:
@Transactional
public void createOrder() {
orderMapper.insert(order);
// 业务逻辑越来越多,耦合严重
mqSender.send(...);
logService.save(...);
notifyService.send(...);
}
而且,像发送短信通知这样的外部调用也被包含在数据库事务里,这合理吗?
使用 @TransactionalEventListener 解耦
Spring 的事件发布/订阅模型结合事务监听器,可以优雅地解决这个问题。
第1步:定义领域事件
@Getter
@AllArgsConstructor
public class OrderCreatedEvent {
private String orderSn;
private Long memberId;
private BigDecimal amount;
}
第2步:在事务方法中发布事件
@Service
public class OrderService {
@Autowired
private ApplicationEventPublisher eventPublisher;
@Transactional
public void createOrder(OrderParam param) {
// 创建订单
orderMapper.insert(order);
// 发布事件
OrderCreatedEvent event = new OrderCreatedEvent(
order.getOrderSn(),
order.getMemberId(),
order.getTotalAmount()
);
eventPublisher.publishEvent(event);
log.info(“事件已发布”);
}
}
第3步:监听事件,并在事务提交后执行
@Component
public class OrderEventListener {
// 关键:使用 @TransactionalEventListener 并指定 phase = AFTER_COMMIT
// 事件会被“挂起”,直到事务提交成功后才被处理
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handleOrderCreated(OrderCreatedEvent event) {
log.info(“监听到订单创建:{}”, event.getOrderSn());
// 以下操作在事务提交后才执行
mqSender.send(“order.cancel”, event.getOrderSn());
logMapper.insert(log);
notifyService.send(event.getMemberId());
}
// 也可以监听事务回滚后的事件
@TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK)
public void handleOrderFailed(OrderCreatedEvent event) {
log.info(“订单创建失败:{}”, event.getOrderSn());
}
}
事件发布与事务执行时机的关系
这里有一个容易混淆的点:publishEvent() 方法调用本身是立即执行的,与事务无关。但监听器的执行时机,完全取决于其注解的配置。
关键区别:
| 监听方式 |
执行时机 |
事务回滚的影响 |
@EventListener |
立即执行 |
已执行的副作用无法撤销 |
@TransactionalEventListener(AFTER_COMMIT) |
事务提交后 |
事务回滚则不执行 |
@TransactionalEventListener(AFTER_ROLLBACK) |
事务回滚后 |
只有回滚才执行 |
@TransactionalEventListener 的4个事务阶段
// 提交前(可用于最后的业务校验)
@TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
public void beforeCommit(OrderCreatedEvent event) {
// 事务即将提交,可以做最后一次数据校验
}
// 提交后(执行所有对外副作用)
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void afterCommit(OrderCreatedEvent event) {
// 数据已持久化,可以安全地发MQ、清缓存、调外部接口
}
// 回滚后(记录失败日志)
@TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK)
public void afterRollback(OrderCreatedEvent event) {
// 事务失败,记录特定的失败日志
}
// 完成后(无论成功失败都执行,用于资源清理)
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMPLETION)
public void afterCompletion(OrderCreatedEvent event) {
// 无论成功失败都会执行
}
需要注意的细节
- 必须在事务方法内发布事件:
@TransactionalEventListener 需要在一个活跃的事务上下文中才能正常工作。如果发布事件的方法没有 @Transactional,则 AFTER_COMMIT 等监听器不会触发。
- 事件的传播与事务传播一致:如果在一个
REQUIRES_NEW 的子事务中发布事件,那么该事件的监听器将跟随这个子事务的提交而触发。
- 无事务环境下的回退执行:通过设置
fallbackExecution = true,可以让监听器在没有事务的环境下也执行。
与事务同步器的对比
| 方式 |
代码耦合度 |
扩展性 |
适用场景 |
TransactionSynchronization |
高(需在业务方法内注册) |
低 |
简单场景,只有1-2个后续操作 |
@TransactionalEventListener |
低(基于发布/订阅模型) |
高 |
复杂场景,有多个后续操作,且可能扩展 |
建议:当后续操作简单且固定时,可以使用事务同步器。但当需要执行多个操作,或者未来可能增加新的后续操作(如新增一个积分发放监听器)时,使用事务事件监听模式是更优雅的选择,它完美实现了代码解耦和开闭原则。
批量操作必须用手动事务
处理100个订单的批量发货,如果其中1个订单的发货逻辑失败,该怎么办?
如果使用 @Transactional 注解:
@Transactional
public void batchDelivery(List<Long> orderIds) {
for (Long orderId : orderIds) {
// 单个订单的发货逻辑
}
}
问题:这100个订单的处理被包裹在同一个大事务中。任何一条失败,都会导致整个事务回滚,所有成功发货的订单也被撤销。
而实际业务需求往往是:成功的订单正常发货,失败的订单记录下来,事后人工处理或重试。
通过编程方式手动管理事务,可以为每条数据开启独立的事务。
@Service
public class OrderBatchService {
@Autowired
private PlatformTransactionManager transactionManager;
public BatchResult batchDelivery(List<Long> orderIds) {
List<Long> success = new ArrayList<>();
List<String> failed = new ArrayList<>();
for (Long orderId : orderIds) {
// 为每个订单开启一个独立的事务
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
TransactionStatus status = transactionManager.getTransaction(def);
try {
// 发货逻辑
Order order = orderMapper.selectById(orderId);
order.setStatus(2); // 已发货
orderMapper.updateById(order);
reduceStock(order);
// 手动提交当前订单的事务
transactionManager.commit(status);
success.add(orderId);
} catch (Exception e) {
// 手动回滚当前订单的事务
transactionManager.rollback(status);
failed.add(“订单” + orderId + “:” + e.getMessage());
}
}
return new BatchResult(success, failed);
}
}
高级用法:为批量任务定制事务属性
对于后台定时任务、数据迁移等高并发批处理场景,可以显式地为每个独立事务设置更合适的属性。
public BatchResult batchCloseOrder(List<Long> orderIds) {
for (Long orderId : orderIds) {
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
// 强制开启新事务,与任何现有事务无关
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
// 降低隔离级别为读已提交,减少锁争用,提升并发度
def.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);
// 设置超时,避免单个失败任务长时间锁住资源
def.setTimeout(10);
TransactionStatus status = transactionManager.getTransaction(def);
try {
Order order = orderMapper.selectById(orderId);
order.setStatus(4); // 已关闭
orderMapper.updateById(order);
transactionManager.commit(status);
} catch (Exception e) {
transactionManager.rollback(status);
}
}
}
三种批量处理方式对比
| 方式 |
事务范围 |
单条失败的影响 |
适用场景 |
@Transactional |
整个批次一个事务 |
全部回滚 |
不适合需要部分成功的批量操作 |
| 手动事务(默认属性) |
每条记录独立事务 |
只回滚当前记录 |
普通的批处理任务 |
| 手动事务(定制属性) |
每条记录独立事务 |
只回滚当前记录 |
高并发、性能要求高的批处理任务 |
测试100个订单发货,97个成功,3个因库存不足失败。结果是97个成功发货,3个失败被准确记录。核心价值在于实现了“部分成功”的业务容错能力。
事务传播机制:3种常用场景深度解析
当一个有 @Transactional 的方法(如创建订单)内部调用了另一个也有 @Transactional 的方法(如插入订单商品)时,会发生什么?这就涉及到Spring的事务传播机制。
Spring定义了7种传播行为,但实际开发中最常用的主要是以下3种:REQUIRED、REQUIRES_NEW、NESTED。
REQUIRED(默认):同生共死
行为:如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新事务。这是最常用的设置。
// 父方法
@Transactional(propagation = Propagation.REQUIRED)
public void createOrder() {
orderMapper.insert(order);
createOrderItems(order.getId()); // 子方法加入当前事务
}
// 子方法
@Transactional(propagation = Propagation.REQUIRED)
public void createOrderItems(Long orderId) {
itemMapper.batchInsert(items);
}
关键点:
- 父方法
createOrder和子方法createOrderItems在同一个物理事务中执行。
- 如果
createOrderItems方法抛出异常,整个事务(包括父方法已插入的订单)将全部回滚。
- 这确保了订单主表和商品明细表的“同生共死”,数据强一致。
适用场景:绝大多数需要保证多个数据库操作原子性的业务场景,如“订单+订单商品”、“主表+明细表”。
REQUIRES_NEW:独善其身
行为:无论当前是否存在事务,都会创建一个全新的、独立的事务。会暂停(挂起)当前存在的事务。
// 父方法
@Transactional
public void createOrder() {
orderMapper.insert(order);
logService.saveLog(log); // 子方法在新事务中执行,独立提交
// 后续的订单逻辑可能失败...
}
// 子方法
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveLog(Log log) {
logMapper.insert(log);
}
关键点:
saveLog方法会在一个独立于createOrder方法的新事务中运行。
- 即使
saveLog方法失败回滚,也不会影响createOrder方法的事务。
- 反之,即使
createOrder方法在日志保存成功后失败回滚,已经提交的日志记录依然会保留。
- 这确保了审计日志、操作轨迹等必须落盘的数据不会丢失。
适用场景:记录审计日志、写入消息表、保存必须成功的通知记录等。
NESTED:嵌套事务与保存点
行为:如果当前存在事务,则在当前事务内创建一个“嵌套事务”(实质是使用数据库的保存点-Savepoint);如果当前没有事务,则其行为同REQUIRED。
// 父方法
@Transactional
public void createOrder() {
orderMapper.insert(order);
try {
createGift(order.getId()); // 嵌套事务
} catch (Exception e) {
// 赠品创建失败,但订单主流程继续
log.warn(“赠品创建失败,继续处理订单”);
}
// 订单正常提交
}
// 子方法
@Transactional(propagation = Propagation.NESTED)
public void createGift(Long orderId) {
giftMapper.insert(gift);
}
关键点:
- 嵌套事务是外部事务的一部分。
createGift失败时,只会回滚到它开始时的保存点,不会影响createOrder方法中已插入的订单记录。
- 但是,如果外部事务(
createOrder)回滚,则嵌套事务(已提交的保存点)也会一起回滚。
- 这需要数据库支持保存点功能(如InnoDB支持,MyISAM不支持)。
适用场景:主业务流程必须完成,但某些附加步骤(如发放赠品、优惠券)可以允许失败且不影响主流程的场景。也常用于批处理中实现单条记录的失败回滚。
三种传播行为对比总结
| 传播行为 |
事务关系 |
子方法失败的影响 |
父方法失败的影响 |
典型场景 |
REQUIRED (默认) |
共享同一事务 |
整个事务回滚 |
整个事务回滚 |
订单+商品(强一致) |
REQUIRES_NEW |
独立新事务 |
只回滚子事务 |
子事务已提交,不受影响 |
审计日志(必须落盘) |
NESTED |
嵌套事务(保存点) |
回滚到保存点 |
整个事务(含嵌套)回滚 |
赠品/优惠券(可选功能) |
选型建议
- 默认使用
REQUIRED:覆盖80%需要“同生共死”强一致性的业务场景。
- 需要独立落盘的操作用
REQUIRES_NEW:如记录关键的审计日志、消息表,确保其永不丢失。
- 需要局部回滚能力的用
NESTED:如批处理中单条记录的容错,或主流程中可选附加功能的实现。
注意:NESTED 传播行为需要底层使用 DataSourceTransactionManager 并且数据库支持保存点(如MySQL的InnoDB引擎)。
几个重要的最佳实践与避坑指南
1. 事务范围要尽可能小
将非数据库操作(如复杂计算、远程调用、大数据量查询)排除在事务范围之外,可以显著减少事务持有锁的时间,提升并发性能。
// 不佳的写法:事务范围过大
@Transactional
public void process() {
List<Data> data = queryBigData(); // 慢查询,不需要事务
Data result = calculate(data); // 复杂计算,不需要事务
mapper.save(result); // 只有这里需要事务
}
// 改进的写法:缩小事务边界
public void process() {
List<Data> data = queryBigData();
Data result = calculate(data);
saveInTransaction(result); // 将事务隔离在最小范围
}
@Transactional
private void saveInTransaction(Data data) {
mapper.save(data);
}
2. 批量插入务必使用 batchInsert
循环执行单条 insert 语句是性能杀手。务必使用MyBatis等框架提供的批量插入方法。
// 性能极差:N次数据库交互
@Transactional
public void save(List<Item> items) {
for (Item item : items) {
mapper.insert(item); // 产生N次数据库round-trip
}
}
// 性能优异:1次数据库交互
@Transactional
public void save(List<Item> items) {
mapper.batchInsert(items); // 通常通过一条SQL或批量参数完成
}
我曾实测,插入1000条数据,循环插入耗时约10秒,而批量插入仅需0.5秒,性能差异巨大。
3. 长事务一定要设置超时 (timeout)
对于可能执行较长时间的事务,必须设置合理的超时时间,防止其长时间占用数据库连接和锁资源,导致系统雪崩。
@Transactional(timeout = 30) // 设置30秒超时
public void longRunningTask() {
// 长时间的业务处理
}
在生产环境中,这应被视为一项强制规范。
总结
本文探讨的6种进阶事务管理方案,每种都瞄准了 @Transactional 注解在实际应用中的特定短板:
- 编程式事务 (
TransactionTemplate) → 解决业务失败需保留数据,系统异常需回滚的精细控制问题。
@Transactional 参数 (isolation, timeout, rollbackFor) → 解决环境适配、长事务阻塞、回滚规则不明确的配置问题。
- 事务同步器 (
TransactionSynchronization) → 解决事务提交后执行副作用操作(如发MQ、清缓存) 的数据一致性问题。
- 事务事件监听 (
@TransactionalEventListener) → 解决业务逻辑与多个后续操作耦合的解耦与扩展问题。
- 手动控制事务 (
PlatformTransactionManager) → 解决批量操作中部分成功、部分失败的容错处理问题。
- 事务传播机制 (
REQUIRED, REQUIRES_NEW, NESTED) → 解决多个事务方法嵌套调用时,事务边界如何划分的架构问题。
对于80%的常规业务场景,一个简单的 @Transactional 注解足以应对。但当遇到上述这些特殊、复杂的场景时,了解并选用合适的高级工具,将是写出健壮、高效、可维护代码的关键。记住,技术选型的最高原则是 “合适”而非“高级”,避免过度设计。
希望这些在实战中总结出的经验,能帮助你在 Spring Boot 和 Java 后端开发中更从容地处理事务问题。如果你在实践中遇到了其他有趣的事务难题或解决方案,欢迎在云栈社区与大家交流分享。
项目代码与资源
完整的可运行Demo项目地址:https://gitee.com/sh_wangwanbao/simple-transactional
项目包含所有示例代码及完整的集成测试。数据库初始化脚本位于 doc/simple-transactional-init.sql,导入后修改配置即可运行验证。