1. 分布式事务控制的核心诉求
在SpringBoot应用中处理跨请求的事务控制是个典型的分布式事务场景。最近在开发一个电商订单系统时遇到了这样的需求:用户下单后需要先预留库存(此时不能立即扣减),等支付回调成功后再真正提交库存变更。这种"预占-确认"模式本质上需要将事务生命周期延长到多个HTTP请求之间。
传统的事务管理(比如@Transactional)只能覆盖单个请求内部的操作,无法跨越多个HTTP请求保持事务状态。经过多种方案对比,最终采用"事务日志+异步确认"的机制实现了这个需求。下面分享具体实现方案和踩坑经验。
2. 技术方案选型与对比
2.1 常见分布式事务方案分析
先看几种常见的分布式事务方案在跨请求场景下的表现:
| 方案 | 适用场景 | 跨请求支持 | 复杂度 | 性能影响 |
|---|---|---|---|---|
| 本地事务表 | 数据一致性要求高 | ✔️ | 中 | 较小 |
| MQ事务消息 | 异步最终一致性 | ✔️ | 高 | 中等 |
| TCC模式 | 严格资金交易 | ✔️ | 高 | 较大 |
| SAGA模式 | 长流程业务 | ✔️ | 高 | 较大 |
| @Transactional | 单请求内事务 | ❌ | 低 | 最小 |
2.2 最终方案设计
结合订单系统的实际需求(中等吞吐量、强一致性要求、已有MySQL基础设施),选择了基于事务日志表的实现方案:
- 创建事务日志表记录事务状态
- 第一阶段操作写入业务数据+日志记录(状态=PENDING)
- 第二阶段通过唯一ID查询并更新状态
- 定时任务补偿异常状态
核心优势在于:
- 完全基于现有技术栈(SpringBoot+MySQL)
- 不需要引入额外中间件
- 实现简单且易于调试
3. 核心实现细节
3.1 数据库设计
首先创建事务控制表:
sql复制CREATE TABLE transaction_log (
id VARCHAR(36) PRIMARY KEY,
business_type VARCHAR(50) NOT NULL,
business_key VARCHAR(100) NOT NULL,
status ENUM('PENDING','CONFIRMED','CANCELLED') NOT NULL,
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
UNIQUE KEY (business_type, business_key)
);
关键字段说明:
- business_type:区分不同业务线(如"order"、"inventory")
- business_key:业务唯一标识(如订单ID)
- status:事务三态设计
3.2 SpringBoot实现代码
事务发起方(订单创建):
java复制@Transactional
public String createOrder(OrderDTO dto) {
// 1. 生成事务ID
String txId = UUID.randomUUID().toString();
// 2. 保存订单(业务数据)
Order order = convertToEntity(dto);
order.setTxStatus("PENDING");
orderRepository.save(order);
// 3. 记录事务日志
TransactionLog log = new TransactionLog();
log.setId(txId);
log.setBusinessType("order");
log.setBusinessKey(order.getOrderNo());
log.setStatus("PENDING");
transactionLogRepository.save(log);
// 4. 预占库存(同样标记为PENDING)
inventoryService.lockStock(dto.getItems(), txId);
return txId;
}
事务确认方(支付回调):
java复制@Transactional
public void confirmOrder(String txId) {
// 1. 查询事务日志
TransactionLog log = transactionLogRepository.findById(txId)
.orElseThrow(() -> new BusinessException("事务不存在"));
// 2. 检查幂等性
if("CONFIRMED".equals(log.getStatus())) {
return;
}
// 3. 更新业务数据
orderRepository.updateTxStatus(log.getBusinessKey(), "CONFIRMED");
inventoryService.confirmLock(txId);
// 4. 更新事务状态
log.setStatus("CONFIRMED");
transactionLogRepository.save(log);
}
3.3 关键配置要点
在application.yml中需要特别配置事务超时时间:
yaml复制spring:
jpa:
properties:
hibernate:
transaction:
timeout: 30 # 单位秒,需要大于业务最长耗时
datasource:
hikari:
max-lifetime: 120000 # 连接池生命周期需要配合调整
4. 生产环境注意事项
4.1 必须实现的四个保障机制
- 幂等控制:
java复制@Transactional
public void confirmOrder(String txId) {
// 使用SELECT FOR UPDATE加锁
TransactionLog log = transactionLogRepository.findByIdForUpdate(txId);
if(log.getStatus().equals("CONFIRMED")) {
return; // 已经处理过直接返回
}
// ...后续处理
}
- 状态补偿:
java复制@Scheduled(fixedDelay = 300000) // 5分钟一次
public void compensatePendingTransactions() {
List<TransactionLog> pendings = transactionLogRepository
.findByStatusAndCreateTimeBefore(
"PENDING",
LocalDateTime.now().minusMinutes(30));
pendings.forEach(log -> {
// 根据业务规则决定确认或取消
if(shouldConfirm(log)) {
confirmOrder(log.getId());
} else {
cancelOrder(log.getId());
}
});
}
- 日志追踪:
建议在MDC中记录事务ID:
java复制MDC.put("txId", txId);
// 这样所有相关日志都会自动带上事务ID
- 监控报警:
配置Prometheus监控指标:
java复制@Bean
public MeterRegistryCustomizer<MeterRegistry> metrics() {
return registry -> {
Gauge.builder("tx.pending.count",
() -> transactionLogRepository.countByStatus("PENDING"))
.register(registry);
};
}
4.2 性能优化技巧
- 事务日志表需要单独放在不同的物理磁盘
- 对business_type+business_key建立联合索引
- 补偿任务的查询需要添加create_time条件避免全表扫描
- 考虑使用Redis缓存高频访问的事务状态
5. 常见问题排查
5.1 事务状态不一致
现象:业务数据已更新但日志状态未变
排查步骤:
- 检查事务方法是否抛出未被捕获的异常
- 查看数据库连接池是否耗尽
- 确认@Transactional注解是否生效(同类调用失效问题)
5.2 死锁问题
典型日志:
code复制Deadlock found when trying to get lock; try restarting transaction
解决方案:
- 统一事务操作顺序(先订单后库存)
- 减小事务粒度
- 添加重试机制:
java复制@Retryable(value = {DeadlockLoserDataAccessException.class},
maxAttempts = 3,
backoff = @Backoff(delay = 100))
public void confirmOrderWithRetry(String txId) {
confirmOrder(txId);
}
5.3 长事务阻塞
监控指标:
- 活跃事务持续时间 > 30s
- 锁等待超时错误增多
优化方案:
- 拆分大事务为多个小事务
- 将非核心操作移出事务(如发短信)
- 调整隔离级别为READ_COMMITTED
6. 扩展思考
这种模式其实实现了简化版的TCC(Try-Confirm-Cancel)事务:
- Try阶段:创建PENDING状态记录
- Confirm阶段:更新为CONFIRMED
- Cancel阶段:业务回滚+状态更新
对于更复杂的场景,可以考虑集成Seata等分布式事务框架。但在中小型系统中,这种轻量级实现已经能满足大部分需求。
实际使用中发现几个值得注意的点:
- 事务ID生成建议使用雪花算法代替UUID,便于排序和排查
- 补偿任务的执行频率需要根据业务特点调整
- 生产环境一定要有完善的状态看板和报警机制