1. 项目概述:轻量级分布式事务补偿方案设计
在微服务架构中,分布式事务处理一直是个棘手的问题。传统方案如2PC、TCC往往需要引入额外中间件,增加了系统复杂度。最近我在一个电商订单项目中,尝试用SpringBoot+本地事务表+定时扫描的方案,实现了零中间件的分布式事务补偿机制。
这个方案的核心思想很简单:利用数据库本地事务保证操作记录和业务数据的原子性,通过定时任务扫描待补偿记录表进行后续处理。相比引入Seata等框架,这种方案的优势在于:
- 无额外组件依赖,纯数据库驱动
- 对现有代码侵入性小
- 适合中小规模分布式系统
- 实现成本低,运维简单
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心架构解析
2.1 本地事务表设计
补偿机制的核心是事务记录表,我的设计如下:
sql复制CREATE TABLE transaction_log (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
biz_id VARCHAR(64) NOT NULL COMMENT '业务ID',
biz_type VARCHAR(32) NOT NULL COMMENT '业务类型',
status TINYINT NOT NULL DEFAULT 0 COMMENT '0-待处理 1-处理中 2-成功 3-失败',
retry_count INT NOT NULL DEFAULT 0 COMMENT '重试次数',
next_retry_time DATETIME COMMENT '下次重试时间',
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_biz (biz_type, biz_id)
) ENGINE=InnoDB;
关键设计点:
- 业务ID+业务类型唯一索引,防止重复记录
- 状态机设计,支持补偿流程控制
- 指数退避重试机制,通过next_retry_time实现
2.2 事务处理流程
典型的事务处理分为三个阶段:
- 主事务阶段:
java复制@Transactional
public void createOrder(OrderDTO order) {
// 1. 保存订单主数据
orderMapper.insert(order);
// 2. 扣减库存(远程调用)
boolean success = inventoryService.reduceStock(order.getProductId(), order.getCount());
// 3. 记录事务日志
if(success) {
transactionLogMapper.insert(new TransactionLog(
order.getOrderNo(),
"ORDER_CREATE",
TransactionStatus.PENDING
));
}
}
- 补偿任务阶段:
java复制@Scheduled(fixedDelay = 10000)
public void compensateTransactions() {
List<TransactionLog> logs = transactionLogMapper.selectPending(
LocalDateTime.now(),
MAX_RETRY_COUNT);
logs.forEach(log -> {
try {
// 获取业务处理器
TransactionProcessor processor = processorFactory.getProcessor(log.getBizType());
// 处理业务逻辑
boolean result = processor.process(log);
// 更新状态
transactionLogMapper.updateStatus(
log.getId(),
result ? TransactionStatus.SUCCESS : TransactionStatus.FAILED
);
} catch (Exception e) {
// 更新重试信息
transactionLogMapper.updateRetry(
log.getId(),
TransactionStatus.PENDING,
calculateNextRetryTime(log.getRetryCount())
);
}
});
}
- 人工干预阶段:
对于超过最大重试次数的失败记录,需要提供管理界面进行人工处理。
3. 关键技术实现细节
3.1 事务一致性保障
方案的核心在于利用数据库本地事务保证业务操作和日志记录的原子性:
java复制@Transactional
public void createOrderWithTransaction(OrderDTO order) {
// 业务操作
orderService.create(order);
// 日志记录
transactionLogService.addLog(
order.getOrderNo(),
"ORDER_CREATE"
);
}
重要提示:必须确保业务方法和日志记录在同一个@Transactional注解范围内,这是整个方案的基础保障。
3.2 补偿任务设计要点
补偿任务的实现有几个关键注意事项:
- 幂等处理:所有补偿操作必须实现幂等
java复制public class OrderCreateProcessor implements TransactionProcessor {
@Override
public boolean process(TransactionLog log) {
// 先查询当前状态
Order order = orderService.getByNo(log.getBizId());
if(order == null) {
// 补偿创建订单
return recreateOrder(log);
}
return true;
}
}
- 退避策略:避免失败任务频繁重试
java复制private LocalDateTime calculateNextRetryTime(int retryCount) {
long delay = (long) Math.min(5 * Math.pow(2, retryCount), 3600);
return LocalDateTime.now().plusSeconds(delay);
}
- 并发控制:防止多个实例同时处理同一条记录
sql复制UPDATE transaction_log
SET status = 1
WHERE id = #{id} AND status = 0
3.3 监控与告警
完善的监控是补偿机制可靠运行的保障:
- Metrics监控:
java复制@Scheduled(fixedDelay = 10000)
public void monitorTransactions() {
// 统计各状态记录数
Map<Integer, Long> stats = transactionLogMapper.countByStatus();
// 暴露给监控系统
stats.forEach((status, count) ->
metrics.gauge("transaction.status." + status, count));
// 失败告警
if(stats.get(3) > WARNING_THRESHOLD) {
alertService.send("交易补偿失败数超过阈值");
}
}
- 日志追踪:
建议为每个补偿任务添加traceId,方便问题排查:
java复制MDC.put("traceId", log.getId() + "@" + log.getBizType());
4. 方案对比与选型建议
4.1 与传统方案对比
| 方案 | 一致性 | 可用性 | 复杂度 | 性能影响 | 适用场景 |
|---|---|---|---|---|---|
| 本地事务表+定时扫描 | 最终 | 高 | 低 | 小 | 中小系统,对实时性要求不高 |
| Seata AT模式 | 强一致 | 中 | 高 | 中 | 大型复杂系统 |
| TCC | 强一致 | 高 | 高 | 大 | 资金交易等关键业务 |
| 消息队列 | 最终 | 高 | 中 | 中 | 异步解耦场景 |
4.2 性能优化实践
- 批量处理:补偿任务应批量获取和处理记录
java复制@Scheduled(fixedDelay = 10000)
public void batchCompensate() {
List<TransactionLog> logs = transactionLogMapper.selectPendingBatch(
LocalDateTime.now(),
MAX_RETRY_COUNT,
BATCH_SIZE);
// 并行处理(注意控制并发度)
logs.parallelStream()
.forEach(this::processSingleLog);
}
- 索引优化:事务表必须建立合适的索引
sql复制ALTER TABLE transaction_log ADD INDEX idx_status_time (status, next_retry_time);
- 缓存优化:频繁访问的业务数据可以缓存
java复制@Cacheable(cacheNames = "orderCache", key = "#orderNo")
public Order getByNo(String orderNo) {
return orderMapper.selectByNo(orderNo);
}
5. 常见问题与解决方案
5.1 事务记录表过大
问题现象:
随着业务增长,事务表数据量达到千万级,扫描性能下降。
解决方案:
- 数据归档:定期将已完成记录迁移到历史表
sql复制INSERT INTO transaction_log_hist
SELECT * FROM transaction_log
WHERE status IN (2,3) AND create_time < DATE_SUB(NOW(), INTERVAL 30 DAY);
DELETE FROM transaction_log
WHERE status IN (2,3) AND create_time < DATE_SUB(NOW(), INTERVAL 30 DAY);
- 分表分库:按业务类型或日期分表
5.2 补偿任务堆积
问题现象:
待补偿记录持续增加,无法及时处理。
优化方案:
- 动态调整扫描频率
java复制@Scheduled(fixedDelayString = "${compensate.interval:10000}")
public void dynamicSchedule() {
long pendingCount = transactionLogMapper.countPending();
if(pendingCount > THRESHOLD) {
compensateExecutor.increaseWorker();
}
}
- 分级处理:按优先级处理不同业务类型
5.3 跨服务补偿
复杂场景:
当需要跨多个服务补偿时,可以采用状态机模式:
java复制public class CrossServiceProcessor {
public boolean process(TransactionLog log) {
TransactionContext context = parseContext(log);
switch(context.getCurrentStep()) {
case "STEP1":
if(serviceA.compensate(context)) {
context.setCurrentStep("STEP2");
updateContext(log, context);
}
break;
case "STEP2":
if(serviceB.compensate(context)) {
context.setCurrentStep("COMPLETE");
updateContext(log, context);
return true;
}
break;
default:
return false;
}
return false;
}
}
6. 生产环境部署建议
6.1 高可用配置
- 补偿任务部署:
- 多实例部署时需启用分布式锁
java复制@Scheduled(fixedDelay = 10000)
@SchedulerLock(name = "compensateTask", lockAtMostFor = "9s")
public void scheduledTask() {
// 任务逻辑
}
- 数据库配置:
yaml复制spring:
datasource:
hikari:
maximum-pool-size: 20
connection-timeout: 30000
6.2 灾备方案
- 事务表备份:
sql复制-- 每天全量备份
mysqldump -uuser -p dbname transaction_log > /backup/trans_log_$(date +%F).sql
- 补偿任务降级:
当系统负载过高时,可以动态降低补偿频率:
java复制@Autowired
private TaskScheduler taskScheduler;
public void adjustSchedule(int newInterval) {
taskScheduler.scheduleWithFixedDelay(
this::compensateTransactions,
newInterval
);
}
在实际项目中采用这套方案后,我们的分布式事务处理成功率从92%提升到了99.8%,而运维复杂度反而降低了。对于中小型分布式系统,这确实是个值得考虑的轻量级解决方案。
