分布式事务解决方案(二)
在Java分布式系统中,强一致性事务方案(如2PC、3PC)虽能保障数据的绝对一致,但因阻塞、性能等问题难以适配高并发场景。相比之下,最终一致性方案通过牺牲强一致性换取可用性和性能,成为互联网架构的主流选择。本文将深入解析TCC(Try-Confirm-Cancel)和本地消息表两种经典最终一致性方案,结合Java代码示例与应用场景,探讨其原理、实现与实践要点。
一、TCC(Try-Confirm-Cancel)模式
1.1 TCC核心思想
TCC将分布式事务拆解为三个阶段:
- Try阶段:预留资源(如冻结金额、锁定库存),确保后续操作具备可行性。
- Confirm阶段:在Try成功后执行真正的业务逻辑(如扣款、扣减库存),此阶段需保证幂等性。
- Cancel阶段:若Try或Confirm失败,执行补偿操作(如解冻金额、释放库存),撤销已执行的Try操作。
1.2 示例场景:电商订单与支付事务
以电商场景为例,订单服务需协调库存服务(扣减库存)与支付服务(扣款),实现“下单-支付”原子性。
1.2.1 接口定义
// 库存服务接口
public interface StockService {boolean tryDeductStock(String orderId, int quantity); // Tryboolean confirmDeductStock(String orderId, int quantity); // Confirmboolean cancelDeductStock(String orderId, int quantity); // Cancel
}// 支付服务接口
public interface PaymentService {boolean tryFreezeAmount(String orderId, BigDecimal amount); // Tryboolean confirmPay(String orderId, BigDecimal amount); // Confirmboolean cancelFreeze(String orderId, BigDecimal amount); // Cancel
}
1.2.2 订单服务调用逻辑
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;@Service
public class OrderService {@Autowiredprivate StockService stockService;@Autowiredprivate PaymentService paymentService;@Transactionalpublic boolean createOrder(String orderId, int quantity, BigDecimal amount) {// Try阶段if (!stockService.tryDeductStock(orderId, quantity) ||!paymentService.tryFreezeAmount(orderId, amount)) {rollbackAll(orderId);return false;}try {// Confirm阶段if (!stockService.confirmDeductStock(orderId, quantity) ||!paymentService.confirmPay(orderId, amount)) {rollbackAll(orderId);return false;}return true;} catch (Exception e) {rollbackAll(orderId);return false;}}private void rollbackAll(String orderId) {stockService.cancelDeductStock(orderId, 0);paymentService.cancelFreeze(orderId, null);}
}
1.3 TCC实现难点与解决方案
-
幂等性处理:
- 方案:为每个事务添加唯一ID,在Confirm/Cancel阶段通过Redis或数据库记录操作状态,重复调用时直接返回成功。
-
空回滚与悬挂处理:
- 空回滚:Cancel阶段在Try未执行时被调用。
解决方案:在Cancel前检查Try是否执行(如数据库记录Try状态)。 - 悬挂:网络延迟导致Try请求后发先至。
解决方案:在Try前检查Cancel是否已执行,若已执行则直接返回失败。
- 空回滚:Cancel阶段在Try未执行时被调用。
-
异常补偿:
- 引入补偿任务调度(如XXL-Job、Elastic-Job),定时扫描未完成的事务并触发补偿。
二、本地消息表方案
2.1 核心原理
本地消息表通过**“事务消息化”**将分布式事务拆解为两个本地事务,借助消息队列实现异步最终一致性:
- 生产者:将业务操作与消息写入数据库(本地事务),消息状态标记为“待发送”。
- 消息发送:异步将消息发送至消息队列,并更新消息状态为“发送中”。
- 消费者:消费消息并执行对应业务(本地事务),成功后通知生产者更新消息状态为“已完成”。
- 补偿机制:通过定时任务扫描“发送中”或超时的消息,重试发送或人工介入。
2.2 示例场景:订单创建与物流通知
订单服务创建订单后,需通知物流服务生成运单。
2.2.1 数据库表设计
-- 订单表
CREATE TABLE orders (order_id VARCHAR(64) PRIMARY KEY,user_id VARCHAR(64),product_id VARCHAR(64),status TINYINT -- 0:待支付, 1:已支付...
);-- 消息表
CREATE TABLE message_table (message_id VARCHAR(64) PRIMARY KEY,order_id VARCHAR(64),topic VARCHAR(64), -- 消息主题,如"order_created"content TEXT, -- 消息内容status TINYINT, -- 0:待发送, 1:发送中, 2:已完成retry_count INT DEFAULT 0,create_time TIMESTAMP
);
2.2.2 Java代码实现
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.kafka.core.KafkaTemplate;@Service
public class OrderService {@Autowiredprivate JdbcTemplate jdbcTemplate;@Autowiredprivate KafkaTemplate<String, String> kafkaTemplate;@Transactionalpublic void createOrder(String orderId, String userId, String productId) {// 1. 插入订单数据jdbcTemplate.update("INSERT INTO orders (order_id, user_id, product_id, status) VALUES (?,?,?, 0)",orderId, userId, productId);// 2. 插入消息数据String messageId = UUID.randomUUID().toString();String messageContent = "{\"orderId\":\"" + orderId + "\"}";jdbcTemplate.update("INSERT INTO message_table (message_id, order_id, topic, content, status) VALUES (?,?,?,?, 0)",messageId, orderId, "order_created", messageContent);// 3. 异步发送消息sendMessage(messageId);}private void sendMessage(String messageId) {jdbcTemplate.update("UPDATE message_table SET status = 1 WHERE message_id = ?", messageId);try {String messageContent = jdbcTemplate.queryForObject("SELECT content FROM message_table WHERE message_id = ?",String.class, messageId);kafkaTemplate.send("order_topic", messageContent);jdbcTemplate.update("UPDATE message_table SET status = 2 WHERE message_id = ?", messageId);} catch (Exception e) {// 发送失败,后续由定时任务重试}}
}// 消息消费逻辑
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Component;@Component
public class MessageConsumer {@Autowiredprivate LogisticsService logisticsService;@Autowiredprivate JdbcTemplate jdbcTemplate;@KafkaListener(topics = "order_topic", groupId = "logistics_group")public void handleMessage(String message) {try {// 解析消息获取orderIdString orderId = JSON.parseObject(message).getString("orderId");// 执行物流服务logisticsService.generateWaybill(orderId);// 更新消息状态为已完成jdbcTemplate.update("UPDATE message_table SET status = 2 WHERE order_id = ?", orderId);} catch (Exception e) {// 消费失败,由生产者重试}}
}
2.3 方案优化点
- 幂等消费:消费者通过数据库唯一键或Redis记录已处理消息ID。
- 重试机制:设置消息重试次数阈值,超过后转为人工处理。
- 对账任务:每日扫描消息表与业务表,修复不一致数据。
三、TCC vs 本地消息表:方案对比
维度 | TCC模式 | 本地消息表 |
---|---|---|
一致性 | 强最终一致性(出现异常需补偿) | 最终一致性(依赖重试机制) |
性能 | 同步阻塞(Try/Confirm阶段) | 异步高并发(消息队列解耦) |
业务侵入性 | 高(需改造业务接口为TCC模式) | 中(需增加消息表与重试逻辑) |
适用场景 | 资金、库存等强一致性场景 | 订单通知、日志同步等弱一致性场景 |
典型案例 | 银行转账、电商交易 | 订单系统与物流系统异步协作 |
四、总结与实践建议
TCC和本地消息表作为Java分布式系统中最终一致性的核心方案,各有优劣。实际应用中,可根据业务场景特性组合使用:
- 核心链路场景(如支付、库存):优先选择TCC,确保资金与资源安全。
- 异步解耦场景(如订单通知、数据同步):采用本地消息表,提升系统吞吐量。
- 混合方案:结合TCC处理关键业务,本地消息表处理辅助业务,实现性能与一致性的平衡。