文章目录
先来点真实的:分布式事务为什么让人头秃?
各位老铁们(拍桌子),搞过微服务的都懂!订单服务扣款成功,库存服务扣减失败;支付服务处理完毕,物流服务原地懵逼…这种分布式事务问题就像爱情里的误会——明明两个人都没错,但结果就是错了!(摔键盘)
传统单体应用的ACID事务在分布式场景下直接失效,这就好比用单车链条驱动航母(离大谱)。那咱们程序员该如何破局?今天老司机带你飙车,盘一盘五大主流解决方案!
方案一:两阶段提交(2PC)——简单粗暴的大家长
核心原理:
- 准备阶段:协调者(Coordinator)这个大家长挨个问所有参与者(Participants)“你准备好了吗?”
- 提交阶段:只要有一个参与者说"No",全体回滚;全说"Yes",集体提交
适用场景:
- 数据库层面跨库事务(比如分库分表)
- 对强一致性要求极高的金融交易
实战踩坑实录:
// 伪代码示例(别直接抄!)
try {
// 阶段一:预提交
orderService.prepareUpdate();
stockService.prepareDeduct();
// 阶段二:正式提交
if(allSuccess()){
orderService.commit();
stockService.commit();
} else {
rollbackAll(); // 回滚时流的泪,都是编码时脑子进的水
}
} catch (Exception e) {
// 这里绝对会收到各种奇葩异常!
}
致命缺陷:
- 协调者单点故障(它挂了全系统陪葬!)
- 同步阻塞导致性能雪崩(所有服务都得等最慢的那个)
- 数据不一致的幽灵依然存在(网络闪断时可能出鬼)
方案二:TCC模式——程序员的手动挡
三大绝招:
- Try:资源预留(比如先把库存冻结,别真扣)
- Confirm:确认执行(真的扣库存)
- Cancel:取消预留(把冻结的库存释放)
电商经典案例:
# 伪代码演示
def create_order():
try:
# 一阶段:资源预留
inventory_service.freeze(2) # 冻结2件库存
coupon_service.lock_coupon() # 锁定优惠券
balance_service.temp_deduct() # 临时扣款
# 二阶段:确认操作
if all_reserved():
inventory_service.confirm() # 真扣库存
coupon_service.confirm() # 核销优惠券
balance_service.confirm() # 实际扣款
else:
raise Exception("有服务没准备好!")
except:
# 三阶段:回滚所有预留
inventory_service.cancel() # 解冻库存
coupon_service.cancel() # 释放优惠券
balance_service.cancel() # 退回临时金额
必看避坑指南:
- 一定要做幂等处理(网络重试可能让你多扣几次钱!)
- 每个服务都要实现三个接口(代码量直接x3)
- 业务侵入性强到爆炸(改造成本高到流泪)
方案三:Saga模式——分布式事务的摆烂大师
核心思想:
"出错就补偿"的佛系哲学,把长事务拆成多个本地事务,每个步骤都有对应的补偿操作
两种流派:
- 编排式(Orchestration):
- 有个中央控制器统一调度
- 适合业务流程复杂的情况
- 协同式(Choreography):
- 各个服务自己发布事件
- 适合服务之间耦合度低的场景
经典补偿逻辑:
// 订票系统示例
async function bookTicket() {
try {
await pay(); // 支付
await lockSeat(); // 锁座
await sendSMS(); // 发短信
} catch (error) {
await refundPay(); // 退钱
await unlockSeat(); // 放回座位
await cancelSMS(); // 撤回短信
}
}
血泪教训:
- 补偿操作可能失败(要设计重试机制)
- 不保证隔离性(中间状态可能被其他事务看到)
- 调试难度上天(分布式调试你懂的)
方案四:本地消息表——MySQL也能玩分布式
四步搞定法:
- 业务操作+消息入库(同一个事务)
- 后台任务轮询消息表
- 调用下游服务
- 成功则标记完成,失败则重试
具体实现:
-- 创建消息表
CREATE TABLE transaction_log (
id BIGINT PRIMARY KEY,
service_name VARCHAR(50),
payload TEXT,
status TINYINT DEFAULT 0,
retry_count INT DEFAULT 0
);
// Java伪代码
@Transactional
public void createOrder(Order order) {
// 1. 写订单表
orderDao.insert(order);
// 2. 写消息表(同库事务)
TransactionLog log = new TransactionLog();
log.setServiceName("inventory");
log.setPayload(order.getItems());
logDao.insert(log);
}
// 定时任务扫描
@Scheduled(fixedRate = 5000)
public void processMessages() {
List<TransactionLog> logs = logDao.findUnprocessed();
logs.forEach(log -> {
try {
inventoryClient.deduct(log.getPayload());
logDao.markAsCompleted(log.getId());
} catch (Exception e) {
logDao.incrementRetry(log.getId()); // 记录失败次数
}
});
}
适用场景:
- 消息最终一致性要求
- 对实时性要求不高
- 不想引入复杂中间件
方案五:最大努力通知——互联网公司的摸鱼哲学
核心玩法:
- 服务A先完成本地事务
- 异步通知服务B,直到B返回成功
- 设置最大重试次数(比如8次)
- 实在不行就记录日志等人肉处理
重试策略参考:
重试次数 | 间隔时间 | 提醒方式 |
---|---|---|
1-3 | 立即重试 | 企业微信机器人 |
4-6 | 5分钟 | 邮件通知 |
7-8 | 1小时 | 打电话叫醒程序员 |
选型决策树(收藏级干货)
- 要强一致性 → 2PC/TCC
- 接受最终一致 → Saga/本地消息表
- 业务简单 → 本地消息表
- 流程复杂 → Saga编排式
- 预算有限 → 最大努力通知
- 想挑战自我 → 自己造轮子(慎重!)
最后说句大实话(敲黑板):没有银弹!选型时要综合考虑团队能力、业务场景、运维成本。比如双11秒杀用TCC,跨境电商用Saga,内部管理系统用本地消息表,这才是正确姿势!
(完)