1. 项目概述
在分布式系统开发中,事务一致性始终是个棘手的问题。传统方案往往依赖重量级中间件,这给中小型项目带来了不必要的复杂度。最近我在一个订单系统中尝试了基于SpringBoot的轻量级解决方案,仅用本地事务表和定时扫描补偿机制就实现了分布式事务的最终一致性,效果出奇地好。
这个方案的核心思想很简单:利用数据库本地事务的ACID特性保证操作记录可靠存储,再通过定时任务扫描异常状态进行补偿。相比引入Seata等分布式事务框架,它不需要额外中间件,部署简单,性能损耗小,特别适合业务逻辑清晰、数据量中等的场景。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心设计思路
2.1 为什么选择轻量级方案
分布式事务的常见解决方案有2PC、TCC、SAGA等,但它们要么实现复杂,要么需要额外组件。对于很多业务场景来说,我们其实只需要保证最终一致性,而不需要强一致性。这时轻量级的本地事务表方案就显示出优势:
- 无外部依赖:仅需数据库,无需引入消息队列或事务协调器
- 开发成本低:核心代码不超过200行
- 性能影响小:没有跨服务锁竞争
- 运维简单:不需要管理额外中间件
2.2 架构设计
整个方案包含三个核心组件:
- 本地事务表:记录所有需要保证一致性的业务操作
- 业务服务:执行业务逻辑并更新事务状态
- 补偿任务:定时扫描失败事务并重试
sql复制CREATE TABLE local_transaction (
id BIGINT PRIMARY KEY,
business_type VARCHAR(32) NOT NULL,
business_id VARCHAR(64) NOT NULL,
status TINYINT NOT NULL COMMENT '0-处理中 1-成功 2-失败',
retry_count INT DEFAULT 0,
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
UNIQUE KEY uk_business (business_type, business_id)
);
3. 实现细节
3.1 事务记录与处理
业务操作需要遵循以下流程:
- 在业务表中插入/更新数据前,先在local_transaction插入一条"处理中"记录
- 执行业务逻辑
- 成功则更新状态为"成功",失败则更新为"失败"
java复制@Transactional
public void createOrder(OrderDTO orderDTO) {
// 1. 记录事务
LocalTransaction transaction = new LocalTransaction();
transaction.setBusinessType("ORDER");
transaction.setBusinessId(orderDTO.getOrderNo());
transaction.setStatus(0);
localTransactionMapper.insert(transaction);
// 2. 执行业务逻辑
orderMapper.insert(orderDTO);
inventoryService.reduceStock(orderDTO.getProductId(), orderDTO.getQuantity());
// 3. 更新状态
transaction.setStatus(1);
localTransactionMapper.updateById(transaction);
}
3.2 补偿任务实现
补偿任务需要处理以下几种情况:
- 长时间处于"处理中"状态的事务(可能系统崩溃导致)
- 重试次数未达上限的"失败"事务
- 需要人工干预的异常事务
java复制@Scheduled(fixedDelay = 60000)
public void compensateTransactions() {
// 查询需要补偿的事务
List<LocalTransaction> transactions = localTransactionMapper.selectNeedCompensate();
for (LocalTransaction transaction : transactions) {
try {
// 根据业务类型执行不同补偿逻辑
if ("ORDER".equals(transaction.getBusinessType())) {
orderCompensator.compensate(transaction);
}
// 其他业务类型...
transaction.setStatus(1);
} catch (Exception e) {
transaction.setRetryCount(transaction.getRetryCount() + 1);
if (transaction.getRetryCount() > MAX_RETRY) {
transaction.setStatus(2);
// 发送告警通知人工干预
alertService.sendAlert(transaction);
}
}
localTransactionMapper.updateById(transaction);
}
}
4. 关键问题与优化
4.1 事务隔离问题
在高并发场景下,需要注意:
- 业务表和事务表的操作必须在同一个数据库事务中
- 补偿任务需要加分布式锁,避免多个实例同时处理同一条记录
- 对同一业务ID的操作需要做幂等控制
java复制// 使用Redis实现简单的分布式锁
public boolean tryLock(String key, long expireSeconds) {
return redisTemplate.opsForValue()
.setIfAbsent(key, "1", expireSeconds, TimeUnit.SECONDS);
}
4.2 性能优化
当数据量增大时,可以采取以下优化措施:
- 对事务表按业务类型分表
- 补偿任务分批处理,避免大事务
- 对已完成的事务定期归档
sql复制-- 按月份分表
CREATE TABLE local_transaction_order_202301 (
-- 同原表结构
) PARTITION BY RANGE (TO_DAYS(create_time)) (
PARTITION p0 VALUES LESS THAN (TO_DAYS('2023-02-01'))
);
5. 方案对比
与其他分布式事务方案相比:
| 方案 | 一致性 | 复杂度 | 性能 | 适用场景 |
|---|---|---|---|---|
| 本地事务表 | 最终 | 低 | 高 | 中小规模,业务清晰 |
| 2PC | 强 | 高 | 低 | 银行、支付等强一致场景 |
| TCC | 最终 | 中 | 中 | 高并发,需要柔性事务 |
| SAGA | 最终 | 中 | 中 | 长事务流程 |
6. 实践经验
在实际项目中踩过几个坑值得分享:
-
事务状态更新时机:一定要在业务操作完成后立即更新状态,我曾遇到过因为异常导致状态未更新,引发补偿任务误判的情况
-
补偿逻辑设计:补偿操作必须幂等,因为可能会被多次执行。建议在业务表中增加状态字段进行控制
-
日志记录:补偿任务的每次操作都要详细记录,方便问题排查。我通常会记录补偿前后的数据快照
-
监控报警:对长时间未完成的事务和重试次数过多的事务要设置报警,我们使用Prometheus+Grafana做了可视化监控
这个方案已经在我们的订单系统中稳定运行半年,日均处理10万+事务,补偿成功率99.9%以上。对于不需要强一致性的场景,这确实是个简单有效的解决方案。
