共计 3146 个字符,预计需要花费 8 分钟才能阅读完成。
分布式事务的现实挑战
在微服务架构中,分布式事务一直是开发者最头疼的问题之一。我最近在电商项目中就遇到了这样的场景:用户支付成功后需要同时更新订单状态、扣减库存和增加积分。这个看似简单的操作,在分布式环境下却隐藏着三大致命痛点:

- 长事务阻塞 :一个跨服务调用可能阻塞整个业务流程
- 数据不一致 :部分服务成功部分失败导致脏数据
- 补偿逻辑复杂 :人工对账和修复成本极高
技术选型:为什么选择 TCC 模式
面对分布式事务难题,业界主要有以下几种解决方案:
- SAGA 模式
- 优点:不需要资源预留
-
缺点:缺乏隔离性,可能产生脏读
-
本地消息表
- 优点:实现简单
-
缺点:时效性差,可靠性依赖轮询
-
TCC 模式(Try-Confirm-Cancel)
- 优点:强一致性保证
- 缺点:业务侵入性强
CC Skill 最终选择 TCC 模式,主要是因为我们业务对数据一致性要求极高,且核心链路能接受一定的性能损耗。
核心实现方案
三阶段代码实现(Java 示例)
@Transactional
public class OrderTccService {
// Try 阶段:资源预留
@TccAction(name = "prepareOrder")
public boolean tryCreateOrder(OrderDTO order) {
// 1. 订单状态置为 "处理中"
orderDao.updateStatus(orderId, "PROCESSING");
// 2. 冻结库存
inventoryService.freeze(order.getSku(), order.getQuantity());
// 3. 预扣积分
pointsService.prepareDeduct(order.getUserId(), order.getPoints());
return true;
}
// Confirm 阶段:确认执行
@TccAction(name = "confirmOrder")
public boolean confirmOrder(Long orderId) {
// 1. 订单状态置为 "已完成"
orderDao.updateStatus(orderId, "CONFIRMED");
// 2. 实际扣减库存
inventoryService.confirmDeduct(...);
// 3. 实际扣除积分
pointsService.confirmDeduct(...);
return true;
}
// Cancel 阶段:取消补偿
@TccAction(name = "cancelOrder")
public boolean cancelOrder(Long orderId) {
// 1. 订单状态置为 "已取消"
orderDao.updateStatus(orderId, "CANCELLED");
// 2. 释放冻结库存
inventoryService.cancelFreeze(...);
// 3. 返还预扣积分
pointsService.cancelDeduct(...);
return true;
}
}
幂等控制设计
我们通过去重表确保每个事务只执行一次:
CREATE TABLE tcc_idempotent (
id BIGINT PRIMARY KEY,
biz_type VARCHAR(32) NOT NULL,
biz_id VARCHAR(64) NOT NULL,
status TINYINT NOT NULL,
created_time DATETIME NOT NULL,
UNIQUE KEY uk_biz (biz_type, biz_id)
) ENGINE=InnoDB;
在每次操作前先检查该表,确保不会重复处理。
超时事务自动补偿
通过定时任务扫描超时事务:
@Scheduled(cron = "0 */5 * * * ?")
public void compensateTimeoutTransactions() {List<Transaction> timeoutList = transactionDao.queryTimeout(30); // 30 分钟未完成
timeoutList.forEach(tx -> {if (tx.getStatus() == TRY_SUCCESS) {
// 尝试自动 Confirm
try {tccClient.confirm(tx);
} catch (Exception e) {
// Confirm 失败则执行 Cancel
tccClient.cancel(tx);
}
}
});
}
性能压测数据
我们对同步阻塞方案和 TCC 方案进行了对比测试(TPS):
| 并发用户数 | 同步阻塞 | TCC 模式 |
|---|---|---|
| 100 | 235 | 198 |
| 500 | 187 | 176 |
| 1000 | 92 | 158 |
可以看到:
– 低并发时 TCC 有约 15% 性能损耗
– 高并发时 TCC 反而表现更好,因为避免了长时间锁竞争
生产环境避坑指南
网络分区处理策略
- 采用最终一致性:允许短时不一致,通过补偿任务修复
- 设置合理的超时时间(建议 2 - 5 秒)
- 实现自动故障检测和路由切换
日志追踪最佳实践
- 使用全局 transactionId 串联所有日志
- 关键节点打标(如:”TCC_TRY_START”)
- 记录补偿操作的时间戳和操作人
补偿重试的指数退避
public void retryWithBackoff(Transaction tx, int retryCount) {long delay = (long) Math.pow(2, retryCount) * 1000; // 指数退避
scheduler.schedule(() -> {if (tx.getStatus() == TRY_SUCCESS) {tccClient.confirm(tx);
} else {tccClient.cancel(tx);
}
}, delay, TimeUnit.MILLISECONDS);
}
事务 ID 生成算法
public class TransactionIdGenerator {// 时间戳 (41bit) + 节点 ID(10bit) + 序列号 (12bit)
private static final long NODE_ID = Config.getNodeId();
private static long lastTimestamp = -1L;
private static long sequence = 0L;
public static synchronized long nextId() {long timestamp = System.currentTimeMillis();
if (timestamp < lastTimestamp) {throw new RuntimeException("Clock moved backwards");
}
if (lastTimestamp == timestamp) {sequence = (sequence + 1) & 0xFFF;
if (sequence == 0) {timestamp = tilNextMillis(lastTimestamp);
}
} else {sequence = 0L;}
lastTimestamp = timestamp;
return ((timestamp - 1288834974657L) << 22)
| (NODE_ID << 12)
| sequence;
}
}
重试策略的思考
在实际业务中,我们需要根据业务特性灵活调整 TCC 三个阶段的重试策略:
- Try 阶段 :快速失败,避免资源长时间预留
- Confirm 阶段 :可适当增加重试次数(如 3 次)
- Cancel 阶段 :必须保证最终成功,可能需要人工介入
比如对于支付订单这样的核心业务,Confirm 阶段的重试策略应该比普通业务更积极。而对于一些非关键业务,可以考虑降低 Cancel 阶段的重试频率,甚至允许一定比例的最终不一致。
经过这次实践,我深刻体会到分布式事务没有银弹。CC Skill 提供的 TCC 实现虽然有一定学习成本,但确实为我们解决了最头痛的数据一致性问题。希望这些经验对正在面临类似挑战的团队有所启发。
正文完
发表至: 未分类
近一天内
