分布式事务与一致性模式:CAP理论、BASE理论、TCC模式、Saga模式、最终一致性、Seata实践
聊到分布式系统,有个话题永远绕不开——分布式事务。
我记得刚带团队做微服务改造那会儿,大家最头疼的就是「数据不一致」。订单系统拆成了订单、库存、支付三个服务,结果用户下单后,库存扣了,支付却失败了。你说这账怎么算?
嗯,今天我们就来系统梳理一下,分布式事务到底该怎么玩。
一、CAP理论:分布式系统的「不可能三角」
先说说CAP。这个理论我每次面试都会问,但真正理解透的人不多。
CAP说的是:一个分布式系统,最多只能同时满足一致性(Consistency)、可用性(Availability)、分区容错性(Partition Tolerance)中的两个。
- C(一致性):所有节点在同一时刻看到的数据是一样的。
- A(可用性):每个请求都能收到一个响应,不管成功还是失败。
- P(分区容错性):系统内部出现网络分区,依然能正常工作。
核心结论:在分布式环境下,P是必须选的。你只能在CP和AP之间做取舍。
我在项目中遇到过这样的场景:一个金融交易系统,我们选择了CP(强一致性),结果网络抖动时整个服务不可用。后来改成了AP(最终一致性),用户体验好了很多,但需要额外处理数据对账。
我的建议:业务场景决定取舍。金融、支付类选CP;社交、内容类选AP。别死磕理论,要结合实际。
二、BASE理论:AP的「妥协艺术」
CAP说不能同时满足三者,那怎么办?BASE理论给出了答案。
BASE是Basically Available(基本可用)、Soft State(软状态)、Eventually Consistent(最终一致性)的缩写。
- 基本可用:系统允许部分功能降级,比如双十一时把「查看历史订单」改成只查最近三个月。
- 软状态:允许数据中间状态存在,不要求实时一致。
- 最终一致性:经过一段时间后,数据最终会达成一致。
说白了,BASE就是「别那么较真,等一会儿就好了」。你想想看,你在淘宝下单后,订单状态从「待支付」变成「已支付」可能需要几秒钟,这就是最终一致性。
注意:最终一致性不是「不管了」,而是「有保障的延迟一致」。你需要设计补偿机制,比如定时对账、消息重试。
三、TCC模式:两阶段提交的「改良版」
TCC(Try-Confirm-Cancel)是我个人比较喜欢的一种模式。它把事务拆成三个阶段:
- Try:预留资源。比如扣库存时,先冻结库存,不实际扣减。
- Confirm:确认执行。如果所有Try都成功,就执行Confirm,真正扣减库存。
- Cancel:取消回滚。如果某个Try失败,就执行Cancel,释放预留的资源。
我曾经在一个电商项目中用过TCC。当时库存服务需要预留库存,支付服务需要冻结金额。如果支付失败,库存的Cancel会释放预留库存。整个过程看起来挺完美,但有个坑——空回滚和幂等性。
避坑指南:我曾经因为没处理好空回滚,导致Cancel操作把未预留的库存释放了,结果库存变成负数。解决方案是:每个Try操作都生成一个全局事务ID,Cancel时先检查该ID是否存在。
代码示例(伪代码):
// Try阶段
public boolean try(TransactionContext ctx, Long orderId, Long productId, int quantity) {
// 生成事务记录
transactionRecordDao.insert(ctx.getTxId(), orderId, productId, quantity, "TRY");
// 冻结库存
return inventoryService.freezeStock(productId, quantity);
}
// Confirm阶段
public boolean confirm(TransactionContext ctx) {
// 检查状态
if (transactionRecordDao.getStatus(ctx.getTxId()) != "TRY") {
return false; // 幂等性检查
}
// 实际扣减库存
inventoryService.deductStock(ctx.getProductId(), ctx.getQuantity());
transactionRecordDao.updateStatus(ctx.getTxId(), "CONFIRMED");
return true;
}
// Cancel阶段
public boolean cancel(TransactionContext ctx) {
if (transactionRecordDao.getStatus(ctx.getTxId()) == "CANCELED") {
return true; // 防止重复取消
}
inventoryService.unfreezeStock(ctx.getProductId(), ctx.getQuantity());
transactionRecordDao.updateStatus(ctx.getTxId(), "CANCELED");
return true;
}
四、Saga模式:长事务的「分步走」
Saga模式跟TCC不同,它不要求预留资源,而是把一个大事务拆成多个本地事务,每个本地事务都有对应的补偿事务。
举个例子:一个旅游预订系统,需要依次预订机票、酒店、租车。如果租车失败,就需要取消机票和酒店的预订。
Saga有两种实现方式:
- 编排式(Choreography):每个服务监听事件,自己决定下一步。适合简单场景。
- 协调式(Orchestration):一个协调器统一调度。适合复杂场景。
我的经验:项目初期用编排式,代码很简洁。但业务复杂后,事件满天飞,调试起来想哭。后来我改成了协调式,虽然多写了一个协调器,但可维护性好了很多。
协调式Saga的代码骨架:
public class BookingSagaOrchestrator {
public void execute(BookingRequest request) {
try {
// 步骤1:预订机票
String flightId = flightService.bookFlight(request);
// 步骤2:预订酒店
String hotelId = hotelService.bookHotel(request);
// 步骤3:预订租车
String carId = carService.bookCar(request);
// 全部成功
sagaState = SagaState.COMPLETED;
} catch (Exception e) {
// 执行补偿
compensate(request, flightId, hotelId, carId);
}
}
private void compensate(BookingRequest request, String flightId, String hotelId, String carId) {
// 逆序取消
if (carId != null) carService.cancelBooking(carId);
if (hotelId != null) hotelService.cancelBooking(hotelId);
if (flightId != null) flightService.cancelBooking(flightId);
sagaState = SagaState.COMPENSATED;
}
}
五、最终一致性:消息驱动的「异步方案」
最终一致性是我用得最多的方案。核心思想是:通过消息队列,保证数据最终一致。
流程很简单:
- 主服务执行本地事务,同时发送一条消息到MQ。
- 从服务消费消息,执行自己的本地事务。
- 如果从服务失败,MQ会重试,直到成功。
这里有个关键点:本地事务和消息发送必须保证原子性。我见过很多团队直接用「先发消息,再执行本地事务」,结果本地事务失败,消息已经发出去了。
解决方案:使用「本地消息表」或「事务消息」。RocketMQ的事务消息就是为此设计的。
RocketMQ事务消息示例:
// 发送半消息
TransactionMQProducer producer = new TransactionMQProducer("order_group");
producer.setTransactionListener(new TransactionListener() {
@Override
public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
// 执行本地事务(创建订单)
boolean success = orderService.createOrder((Order) arg);
return success ? LocalTransactionState.COMMIT_MESSAGE : LocalTransactionState.ROLLBACK_MESSAGE;
}
@Override
public LocalTransactionState checkLocalTransaction(MessageExt msg) {
// 回查本地事务状态
String orderId = msg.getKeys();
return orderService.isOrderCreated(orderId) ?
LocalTransactionState.COMMIT_MESSAGE : LocalTransactionState.ROLLBACK_MESSAGE;
}
});
producer.sendMessageInTransaction(msg, order);
六、Seata实践:开箱即用的分布式事务框架
说了这么多理论,来点实战的。Seata是阿里开源的分布式事务框架,支持AT、TCC、Saga、XA四种模式。
我个人最常用的是AT模式。它基于两阶段提交,但对业务代码侵入很小。你只需要在业务方法上加个注解:
@GlobalTransactional(name = "create-order", timeoutMills = 30000)
public void createOrder(Order order) {
// 1. 创建订单
orderDao.insert(order);
// 2. 扣减库存
inventoryService.deduct(order.getProductId(), order.getQuantity());
// 3. 扣减余额
accountService.debit(order.getUserId(), order.getAmount());
}
Seata会自动拦截这个方法的SQL,生成前置镜像和后置镜像。如果后续步骤失败,Seata会根据镜像数据自动回滚。
注意:AT模式依赖数据库的本地事务,且要求SQL必须是可回滚的。如果你用了MyBatis的批量更新,或者调用了第三方API,AT模式就搞不定了。这时候可以考虑TCC或Saga。
Seata的部署架构也很简单:
- TC(事务协调器):独立部署的服务,维护全局事务状态。
- TM(事务管理器):嵌入在业务应用中,负责开启、提交、回滚全局事务。
- RM(资源管理器):管理分支事务,与TC通信。
七、知识体系总览
下面这张图,是我梳理的分布式事务知识体系。你可以把它当作一个「决策地图」:
八、总结与选择建议
说了这么多,到底该怎么选?我根据自己的经验,给个参考:
| 场景 | 推荐模式 | 理由 |
|---|---|---|
| 金融交易、支付 | TCC 或 Seata AT | 强一致性要求,但需要高可用 |
| 订单、库存 | Saga 或 最终一致性 | 允许短暂不一致,补偿逻辑清晰 |
| 跨系统数据同步 | 消息驱动 + 最终一致性 | 解耦性好,扩展性强 |
| 简单场景、小团队 | Seata AT | 侵入性低,上手快 |
最后说一句:没有银弹。分布式事务的本质是「用复杂度换一致性」。如果你的业务可以用单机事务解决,就别碰分布式事务。我见过太多团队为了「微服务而微服务」,结果引入了一堆分布式事务问题。
嗯,记住一句话:能不拆,就不拆;非要拆,先想好怎么补偿。
公众号:蓝海资料掘金营,微信deep3321