一、什么是分布式事务?
简单来说,分布式事务 就是一次业务操作需要跨多个独立的服务或数据库来完成,并且必须保证所有这些操作要么全部成功,要么全部失败。
在分布式系统中,一个业务操作经常需要调用多个服务,这些服务可能使用不同的数据库。为了确保数据的正确性,这些跨服务的操作集合必须满足事务的ACID特性:
- 原子性(Atomicity):所有操作要么全部成功,要么全部失败。
- 一致性(Consistency):事务执行前后,数据的状态保持一致。
- 隔离性(Isolation):并发执行的事务之间互不干扰。
- 持久性(Durability):事务一旦提交,其结果就是永久性的。
分布式事务面临的挑战
在分布式系统中实现上述特性,远比单体应用复杂,主要面临以下几方面的挑战:

你的代码里是否使用了事务控制?是否存在潜在的事务问题?我想,我们都需要通过严谨的业务逻辑结合充分的测试来保证事务的正确性。但凡涉及多个数据操作(多个DML),就必须考虑事务问题。这需要我们重视,但也不必过度焦虑,结合不同场景多进行测试,往往能快速发现问题。(扪心自问:是没测到问题,还是考虑不周,或是偷懒了?)
单体应用的事务控制在之前的文章中已有介绍,本文主要聚焦于分布式事务。我将通过使用中间件和不使用中间件两种思路来探讨分布式事务的解决方案。首先,我们来聊聊不依赖特定中间件的常见实现方案。
二、无中间件分布式事务方案
在不引入特定中间件的情况下,常见的分布式事务解决方案主要有以下几种:
- TCC(Try-Confirm-Cancel)
- 可靠消息最终一致性(本地消息表 + 定时重试) - 最稳健、最通用的方案之一
- 最大努力通知 + 幂等 - 简单业务场景的首选
- 手写 SAGA 补偿 - 适用于复杂的长业务流程
(以上方案相信大部分开发者都接触或使用过,但我们往往缺少的是对它们的系统化沉淀和在实际场景中的择优选用。)
1. TCC 方案
TCC 可以说是最贴合业务逻辑的分布式事务方案了。虽然名字(Try-Confirm-Cancel)听起来有点抽象,但通过一个电商下单的场景,你就能立刻明白。
场景示例:电商下单支付
用户下单成功,实际上包含了“创建订单”和“支付成功”两个关键环节。在支付成功之前,系统需要先执行 Try 阶段:
- 锁定库存(防止超卖)
- 锁定优惠券(防止重复使用)
- 锁定礼品卡余额
如果用户取消订单或支付超时,系统则执行 Cancel 阶段,释放所有在 Try 阶段锁定的资源。只有当支付真正成功时,才执行 Confirm 阶段,进行实际的扣减操作(扣库存、核销优惠券、扣减礼品卡金额)。
关键理解:不要被术语牵着走,理解“预留资源 → 确认/取消”这个核心流程就清晰了。
优点:强一致性。用户支付成功后,订单、库存、账户金额等状态立刻达成一致,没有中间态。
缺点:业务侵入性极强。每个参与事务的服务都需要改造出 Try、Confirm、Cancel 三个接口,并且需要处理“空回滚”(Try未执行却收到Cancel)和“防悬挂”(Cancel早于Try到达)等复杂异常。
TCC 的一个“理想化”漏洞
上述流程看似完美,但假设事务执行过程中,有人直接修改了数据库呢?那么这一连串的锁定、提交、回滚逻辑可能完全失效。
这是个非常实战化的问题,也是面试中关于 TCC 的必考点:
TCC 的锁是应用层的业务逻辑锁,并非数据库的强约束(如行锁、事务锁)。 一旦有人绕过应用,直接执行 UPDATE stock SET count = count - 1 这样的 SQL,应用完全感知不到,冻结表、事务状态表全被绕过,最终导致超卖、数据不一致等严重问题。
防护与兜底方案:
对于 TCC 这类强一致性方案,防止人为误操作没有“银弹”,必须建立一套纵深防御体系:
- 权限管控是基础:生产环境数据库的写权限必须严格收紧,通常通过堡垒机进行运维操作。
- 流程规范是保障:建立“申请-审批-复核-执行-验证”的闭环变更流程,关键操作需双人复核。
- 监控对账是眼睛:必须建立核心业务数据(如库存、资金)的定时对账任务,这是发现问题最后的屏障。
- 审计追溯是依据:所有数据库操作必须留有日志,做到可追溯,用于定责和复盘。
在实际工程中,技术防护(权限控制)+ 管理流程(规范与复核)+ 核心对账的组合策略是最常用且有效的。最终目标是:即使发生了人为错误,也能在极短时间内(如分钟级)发现并启动修复流程,将业务影响降到最低。
2. 可靠消息最终一致性
场景:订单支付成功后,需要异步发送短信通知、App推送、更新用户积分、生成购物记录等。这些操作不应阻塞核心支付流程,但要保证最终一定会执行。
核心思想:
- 事务发起方(如订单服务)在本地事务中,将需要发送的消息和业务数据一同存入本地消息表,状态为“待发送”。
- 提交本地事务。
- 有一个定时任务轮询本地消息表,将“待发送”的消息投递到消息中间件(如 Kafka、RabbitMQ)。
- 消费者服务从消息中间件获取消息并处理,处理成功后,通知消息发送方(或由发送方主动查询),将本地消息状态更新为“已发送”。
- 对于发送或处理失败的消息,依靠定时任务进行重试,确保最终成功。
3. 最大努力通知
场景:支付成功后,需要通知外部的第三方物流系统准备发货。外部系统可能不稳定,我们不应被其阻塞,只需尽最大努力去通知,最终可以接受通知失败,通过事后对账来弥补。
核心思想:
- 系统内部事务完成后,向外部系统发起通知。
- 如果通知失败,按照预设策略(如间隔逐渐增大的策略)进行多次重试。
- 达到最大重试次数后仍然失败,则不再重试,记录日志并告警,等待后续人工或定时对账处理。
可靠消息最终一致性与最大努力通知的核心区别
| 维度 |
可靠消息最终一致性 |
最大努力通知 |
| 核心目标 |
必须成功,保证系统内部数据最终一致。 |
尽力而为,接受最终可能失败,依赖事后对账。 |
| 设计哲学 |
“不丢消息,保证送达”,是闭环。 |
“通知到最好,不行再对账”,是开环。 |
| 一致性保证方 |
消息发送方与消费方共同保证。 |
主要是通知发起方,接收方无强保证。 |
| 后续处理 |
依赖重试机制确保消息被消费。 |
依赖对账系统修正不一致。 |
4. Saga 方案
场景特征:
- 业务流程非常长,可能持续分钟、小时甚至天级别。
- 涉及多个服务或系统。
- 可以接受最终一致性。
- 通常有高并发、高性能要求。
电商示例:
- 订单履约流程(下单 → 支付 → 发货 → 收货)
- 售后流程(申请退货 → 审核 → 退款)
- 会员升级流程
- 跨系统数据同步
核心思想:Saga 是一种长事务解决方案,它将一个长事务拆分为一系列本地短事务。每个短事务正常提交,但如果其中某个事务失败,则会按相反顺序触发之前所有已提交事务的补偿操作,来回滚整个业务流程。
TCC 与 Saga 的核心区别
- TCC:通过 Try 阶段锁定资源,Confirm/Cancel 阶段进行二阶段提交/回滚。强一致性,无脏读。
- Saga:每个步骤都直接提交,不锁定资源。通过事后补偿来回滚。最终一致性,存在脏读可能。
TCC 与 Saga 混合使用示例(电商订单完整流程)
我们可以将核心的、对一致性要求极高的部分(如扣款、扣库存)用 TCC 实现,而将后续的、流程较长的部分(如履约、发货)用 Saga 实现。
// 阶段1:下单支付(采用TCC,保证强一致)
class OrderPaymentPhase {
// Try阶段
public boolean tryPhase() {
// 1. 创建订单(待支付状态)
orderService.createOrder(order, OrderStatus.PENDING);
// 2. 锁定库存
inventoryService.lockStock(order.getItems());
// 3. 冻结优惠券
couponService.freezeCoupon(order.getCouponId());
// 4. 冻结资金
paymentService.freezeAmount(order.getUserId(), order.getAmount());
}
// Confirm阶段
public boolean confirmPhase() {
// 1. 更新订单为已支付
orderService.confirmOrder(orderId);
// 2. 扣减锁定库存
inventoryService.deductLockedStock(order.getItems());
// 3. 核销优惠券
couponService.useCoupon(order.getCouponId());
// 4. 扣款
paymentService.deductAmount(order.getUserId(), order.getAmount());
}
// Cancel阶段
public boolean cancelPhase() {
// 回滚所有Try阶段的操作(释放库存、优惠券、资金)
}
}
// 阶段2:订单履约(采用Saga,实现最终一致)
class OrderFulfillmentSaga {
public void executeSaga(Order order) {
try {
// 正向操作,每一步都直接提交
// 1. 创建发货单
shippingService.createShipping(order);
// 2. 通知仓库拣货
warehouseService.pickGoods(order);
// 3. 物流发货
logisticsService.deliver(order);
// 4. 更新订单状态为已发货
orderService.updateStatus(order.getId(), OrderStatus.SHIPPED);
} catch (Exception e) {
// 任何一步失败,执行反向补偿操作(补偿必须幂等)
logisticsService.cancelDelivery(order);
warehouseService.restoreGoods(order);
shippingService.cancelShipping(order);
// 更新订单状态为履约失败
orderService.updateStatus(order.getId(), OrderStatus.FULFILLMENT_FAILED);
}
}
}
最佳实践建议
- 混合使用:核心资金、库存交易使用 TCC 保证强一致;后续物流、通知等流程使用 Saga 保证最终一致。
- 超时设置:TCC 的 Try 阶段需要设置合理的超时时间(如 10-30 秒),避免资源长时间锁定。
- 补偿幂等:Saga 的补偿操作必须设计成幂等的,防止重试导致重复补偿。
- 状态可见:在 Saga 长流程中,给用户展示合适的中间状态(如“发货处理中”),提升体验。
- 监控告警:无论是 TCC 还是 Saga,都需要完善的监控、告警以及人工干预机制。
- 版本管理:服务接口变更时,需考虑向前兼容,避免因版本问题导致补偿逻辑错乱。
总结
以上主要梳理了不依赖特定中间件的分布式事务解决方案。每种方案都有其适用的场景,关键在于深入理解业务需求(强一致还是最终一致?流程长短?),从而做出最适合的技术选型。选择合适的方案,并在云栈社区这样的技术论坛与同行交流实战经验,能帮助我们更好地应对分布式系统下的数据一致性挑战。后续我将再找时间分享基于 Seata 等中间件的分布式事务实践。