【Easylive】手动实现分布式事务解决方案流程解析
【Easylive】项目常见问题解答(自用&持续更新中…) 汇总版
分布式事务解决方案深度解析
一、两阶段提交(2PC)
核心流程
-
准备阶段
- 协调者发送
prepare
请求,参与者执行事务但不提交 - 参与者锁定资源并记录undo/redo日志
- 返回
Yes/No
响应
- 协调者发送
-
提交阶段
- 全票通过则发送
commit
命令 - 任一失败则发送
rollback
命令
- 全票通过则发送
实现方案
-- 事务状态表
CREATE TABLE transaction_log (tx_id VARCHAR(64) PRIMARY KEY,status ENUM('PREPARED', 'COMMITTED', 'ROLLBACKED'),create_time DATETIME
);
// 协调者伪代码
public class Coordinator {public boolean execute(TransactionContext ctx) {// 阶段1:准备for (Participant p : participants) {if (!p.prepare(ctx)) return false;}// 阶段2:提交try {for (Participant p : participants) {p.commit(ctx);}return true;} catch (Exception e) {for (Participant p : participants) {p.rollback(ctx);}return false;}}
}
优缺点
✅ 强一致性保证
❌ 同步阻塞、协调者单点故障
❌ 网络分区可能导致资源锁定
二、补偿事务(TCC)
三阶段操作
阶段 | 操作说明 |
---|---|
Try | 资源预留(如冻结库存) |
Confirm | 确认执行(实际扣减) |
Cancel | 补偿回滚(释放冻结资源) |
关键实现
public interface PaymentServiceTCC {@Transactionalboolean tryPayment(Long orderId, BigDecimal amount);@Transactionalboolean confirmPayment(Long orderId);@Transactionalboolean cancelPayment(Long orderId);
}
注意事项
- 幂等控制:需处理重复调用
- 空补偿:处理未执行Try直接Cancel的情况
- 悬挂问题:防止Try超时后Cancel先执行
三、本地消息表
实现架构
[业务事务] → [消息表记录] → [定时任务] → [MQ] → [消费者]
核心代码
CREATE TABLE local_message (id BIGINT AUTO_INCREMENT PRIMARY KEY,biz_id VARCHAR(64) NOT NULL,content TEXT NOT NULL,status TINYINT DEFAULT 0 COMMENT '0-待发送,1-已发送',retry_count INT DEFAULT 0,create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
@Transactional
public void createOrder(Order order) {// 1. 业务操作orderMapper.insert(order);// 2. 记录消息(同事务)LocalMessage msg = new LocalMessage();msg.setBizId(order.getOrderNo());msg.setContent(JSON.toJSONString(order));messageMapper.insert(msg);
}
优势
- 实现简单,与业务解耦
- 天然支持重试机制
四、Saga模式
执行模式对比
类型 | 特点 |
---|---|
协同式 | 通过事件驱动协调 |
编排式 | 中央协调器控制流程 |
状态机示例
public void placeOrder() {try {// 正向操作inventoryService.reserveStock();paymentService.charge();shippingService.createShipment();} catch (Exception e) {// 逆向补偿shippingService.cancelShipment();paymentService.refund();inventoryService.releaseStock();}
}
日志追踪设计
CREATE TABLE saga_log (saga_id VARCHAR(64) NOT NULL,step_name VARCHAR(32) NOT NULL,status VARCHAR(16) NOT NULL,params TEXT,create_time TIMESTAMP,PRIMARY KEY (saga_id, step_name)
);
方案选型指南
对比矩阵
方案 | 一致性 | 复杂度 | 性能 | 适用场景 |
---|---|---|---|---|
2PC | 强一致 | 高 | 低 | 银行转账等强一致场景 |
TCC | 最终一致 | 中高 | 中 | 电商交易、支付系统 |
本地消息表 | 最终一致 | 低 | 高 | 物流通知、积分系统 |
Saga | 最终一致 | 中 | 中高 | 长流程业务(保险理赔) |
组合方案推荐
- 支付+库存:TCC + 本地消息表
- 订单履约:Saga + 异步补偿
- 数据一致性:2PC + 超时补偿机制
最佳实践原则
- 业务分析:根据CAP理论权衡一致性需求
- 降级方案:设计合理的补偿机制
- 监控体系:建立事务状态追踪看板
- 压力测试:验证方案在高并发下的表现
注:所有方案都需要配合幂等控制、重试机制和日志追踪才能保证可靠性
本地消息表(Local Message Table)方案专业解析
1. 核心设计思想
本地消息表是一种基于最终一致性的分布式事务解决方案,通过异步消息传递+事务日志实现跨服务数据同步:
- 事务拆分:将分布式事务拆分为多个本地事务
- 消息驱动:通过本地消息表记录待处理操作,异步触发下游服务
- 重试补偿:定时任务保证消息可靠投递,失败时自动重试
2. 技术实现组件
组件 | 作用 | 技术实现示例 |
---|---|---|
业务事务表 | 记录主业务数据(如订单表) | MySQL order 表 |
本地消息表 | 存储待分发的消息(状态机模式) | MySQL local_message 表 |
定时任务调度器 | 扫描未处理消息,触发下游服务调用 | Spring @Scheduled + 线程池 |
幂等处理器 | 防止下游服务重复消费 | Redis唯一ID/数据库唯一约束 |
监控告警模块 | 捕获长期失败消息,触发人工干预 | Prometheus + Grafana + 企业微信报警 |
3. 关键流程(ACID特性保障)
4. 技术关键点
原子性(Atomicity)保障
@Transactional
public void createOrder(Order order) {orderDao.insert(order); // 业务数据messageDao.insert(toMessage(order)); // 事务消息
}
可靠性(Durability)设计
- 消息表与业务表同库同实例,利用RDBMS的WAL日志保证持久化
- 定时任务采用至少一次(at-least-once)投递语义
幂等性(Idempotency)控制
@PostMapping("/api/process")
public Response process(@RequestBody Message msg) {if (redis.setnx(msg.getId(), "1", 24h)) { // 分布式锁realProcess(msg); // 真实业务逻辑}return Response.success();
}
一致性(Consistency)恢复
long delay = Math.min(1000 * Math.pow(2, retryCount), 3600000);
5. 生产级优化建议
消息表分库分表
CREATE TABLE local_message_${hash(id)%16} (...);
批量消息处理
@Scheduled(fixedDelay = 5000)
public void batchProcess() {List<Message> batch = messageDao.scan(100);CompletableFuture[] futures = batch.stream().map(msg -> asyncProcess(msg)).toArray(CompletableFuture[]::new);CompletableFuture.allOf(futures).join();
}
死信队列处理
if (msg.getRetryCount() > MAX_RETRY) {deadLetterQueue.add(msg); // 转入死信队列alarmService.notifyAdmin(msg);
}
6. 方案局限性
-
时效性缺陷
依赖定时任务扫描,消息处理延迟通常在秒级 -
架构约束
要求业务消息必须可序列化存储 -
维护成本
需额外维护消息表、定时任务等组件
7. 适用场景评估
场景 | 适用性 | 理由 |
---|---|---|
订单创建→库存扣减 | ★★★★★ | 允许短暂延迟,业务容忍最终一致 |
支付成功→短信通知 | ★★★★☆ | 通知类操作对实时性要求较低 |
金融账户转账 | ★★☆☆☆ | 需要强一致性,建议使用TCC或Saga |
日志数据同步 | ★★★★★ | 天然适合异步处理 |
该方案在电商、物流等互联网业务中广泛应用,是平衡实现复杂度与可靠性的典型折中方案
🍱 用"外卖订餐"理解本地消息表
现实场景 vs 技术实现
餐馆运营问题 | 分布式系统问题 | 解决方案 |
---|---|---|
前台接单记录 | 业务数据存储 | MySQL订单表 |
厨房小票 | 事务消息 | local_message表 |
服务员送小票 | 消息投递 | 定时任务扫描 |
厨房小黑板 | 幂等控制 | Redis唯一标识 |
店长监督 | 监控告警 | Prometheus+钉钉 |
核心四步流程
1️⃣ 接单存双录(事务原子性)
@Transactional // 原子操作保证
public void 接单(订单 order) {// 记录主订单(账本)订单库.save(order); // 生成厨房小票(消息)小票机.save(new 小票(order.id, "新订单", LocalDateTime.now()));
}
💡 就像收银机:按一次按钮同时打印顾客账单和厨房小票
2️⃣ 异步送小票(最终一致性)
3️⃣ 厨房防重做(幂等性)
def 做菜(订单号):if redis.get(订单号) == "处理中":return "已在制作"redis.set(订单号, "处理中", ex=3600)实际做菜操作()return "开始制作"
👨🍳 相当于厨师长看到相同订单号会说:“这份已经在炒了!”
4️⃣ 异常处理三板斧
// 1. 指数退避重试
Thread.sleep(1000 * Math.pow(2, 重试次数));// 2. 死信队列监控
if(小票.重试次数 > 3){钉钉报警("请店长处理订单:"+小票.订单号);
}// 3. 人工补偿入口
@PostMapping("/手动重试")
public String 人工重试(String 订单号){消息 msg = 小票机.find(订单号);厨房服务.做菜(msg.getContent());
}
🌟 方案优势
- 高可靠:小票机相当于WAL日志,断电也不丢单
- 可扩展:多个服务员(消费者)并行处理小票
- 解耦合:厨房装修(服务升级)不影响前台接单
- 可追溯:所有小票永久存档,随时审计
🚨 注意事项
- 小票内容要包含全部必要信息(如顾客忌口)
- 厨房处理能力要匹配送单频率(背压问题)
- 定期归档历史小票(消息表分库策略)
🍜 就像优秀的外卖系统:订单可能稍有延迟,但绝不会丢失或重复!
用这种模式可以处理:订单→库存、支付→通知等大多数最终一致性场景。