前言

在微服务架构盛行的今天,数据一致性是每个分布式系统工程师必须面对的终极挑战。从单体应用切换到微服务后,一次业务操作可能跨越多个服务、多个数据库,传统的本地事务(ACID)已经无法覆盖这种分布式场景。本文将从理论到实践,全面拆解分布式事务的解决方案。

一、为什么会有分布式事务问题

在单体架构中,一个请求通常只涉及单个数据库,通过数据库的本地事务(BEGIN/COMMIT/ROLLBACK)即可保证数据一致性。但在微服务架构下:

场景一:订单拆单 — 用户下单时需要同时扣减库存(库存服务)、创建订单(订单服务)、扣减余额(账户服务),这三个操作在三个不同的数据库中执行,任何一步失败都需要回滚整个业务流程。

场景二:跨服务状态机 — 电商支付流程涉及订单状态变更(待支付→已支付→已发货),支付服务完成支付后需要通过某种机制通知订单服务和履约服务。

场景三:数据同步 — 主数据库写入成功后,需要保证搜索引擎(Elasticsearch)、缓存(Redis)、数据仓库(Hive)中的数据最终一致。

二、分布式事务的理论基础

2.1 CAP 定理的再理解

CAP 定理指出,在一个分布式系统中,Consistency(一致性)、Availability(可用性)、Partition Tolerance(分区容错性)三者不可兼得。由于分区容错是分布式系统的基本要求,实际工程中的一致性方案本质上都是在 C 和 A 之间寻找平衡点。

2.2 BASE 理论

BASE 是对 CAP 中 AP 方案的延伸,核心思想是:

Basically Available(基本可用):系统出现故障时,允许损失部分可用性,比如响应时间变长、部分功能降级。

Soft State(软状态):允许系统中的数据存在中间状态,并认为该状态不影响系统整体可用性。

Eventually Consistent(最终一致性):经过一段时间后,所有副本的数据最终会达到一致状态。这是大多数分布式事务方案的目标。

2.3 一致性级别分级

强一致性(Strong Consistency):写入成功后,任何后续读取都能立即看到最新值。代表方案:2PC、Paxos、Raft。代价是低可用性、高延迟。

弱一致性(Weak Consistency):写入成功后,不保证后续读取能看到最新值。代表方案:DNS 缓存、CDN 缓存。

最终一致性(Eventual Consistency):写入成功后,经过一个时间窗口(通常毫秒级到秒级),数据最终达到一致。这是工程中最实用的平衡点。

三、核心方案一:两阶段提交(2PC)

3.1 协议流程

两阶段提交(Two-Phase Commit)是最经典的分布式事务协议,将事务过程分为两个阶段:

阶段一:Prepare(准备阶段) — 协调者(Coordinator)向所有参与者(Participant)发送 PREPARE 请求,参与者执行本地事务但不提交,将 undo/redo 日志写入,然后返回 YES/NO。

阶段二:Commit/Rollback(提交阶段) — 如果所有参与者都返回 YES,协调者发送 COMMIT 请求,参与者完成提交;如果任意一个返回 NO,协调者发送 ROLLBACK 请求,参与者回滚本地事务。

3.2 工程问题与应对

同步阻塞:参与者在 Prepare 阶段后持有锁直到收到协调者的最终决议,长时间阻塞导致系统吞吐量下降。应对:设置超时时间,超时后自动进入 abort 流程。

协调者单点故障:如果协调者在 Prepare 后、Commit 前宕机,参与者将无法收到决议,持有锁无法释放。应对:协调者使用 WAL(Write-Ahead Log)持久化事务状态,重启后恢复。

数据不一致(脑裂风险):如果协调者在部分参与者 Commit 后宕机,可能导致部分参与者提交了、部分未提交。应对:3PC 引入 CanCommit 阶段和超时提交机制来缓解,但无法完全消除。

3.3 适用场景

2PC 适合事务参与方少(2-3个)、事务生命周期短、且对一致性要求极高的内部系统(如银行核心系统批量处理)。不适合跨公网、参与方多、事务耗时长的微服务场景。

四、核心方案:三阶段提交(3PC)

3PC 是对 2PC 的改进,将准备阶段拆分为 CanCommit 和 PreCommit 两个子阶段,并引入参与者超时机制:

阶段一:CanCommit — 协调者询问参与者是否可以执行事务(不执行操作),降低锁定的资源范围。

阶段二:PreCommit — 参与者锁定资源、写日志、执行事务但不提交(类似 2PC 的 Prepare)。

阶段三:DoCommit — 协调者根据 PreCommit 结果发送提交或回滚指令。如果参与者在超时时间内未收到 DoCommit,则自动提交(基于一个乐观假设:既然大家都 PreCommit 成功了,提交应该是安全的)。

3PC 通过超时机制解决了 2PC 中参与者无限等待的问题,但在网络分区情况下仍可能出现数据不一致(超时提交与协调者 abort 冲突),且增加了通信轮次,延迟更高。实际工程中 3PC 使用很少。

五、核心方案:TCC(Try-Confirm-Cancel)

5.1 设计哲学

TCC 是一种业务层面的分布式事务方案,核心思想是:为每个操作都注册对应的确认(Confirm)和补偿(Cancel)操作,通过业务逻辑而非数据库事务来保证一致性。TCC 是 Compensating Transaction(补偿事务)思想的工程实现。

5.2 三阶段详解

Try 阶段:完成所有业务检查(一致性校验),预留业务资源(锁定资源但不扣减)。比如下单时 Try 不是直接扣库存,而是将库存从"可用"移到"预占"状态。

Confirm 阶段:执行真正的业务操作,使用 Try 阶段预留的资源。Confirm 必须是幂等的(重试安全)。例如将预占库存变为已扣减。

Cancel 阶段:执行回滚操作,释放 Try 阶段预留的资源。Cancel 也必须幂等。例如将预占库存释放回可用。

5.3 TCC 的幂等性保障

TCC 要求 Confirm 和 Cancel 必须支持幂等调用,因为网络超时时协调者可能重试。实现方式:

方案一:事务状态表 — 每个分支事务关联一张状态表,记录事务ID、状态(trying/confirmed/canceled),执行前检查状态防止重复执行。

方案二:业务唯一键约束 — 利用数据库唯一索引(如订单号+操作类型),重复插入失败即为已执行过。

方案三:Token 机制 — 客户端申请唯一 Token,服务端检查 Token 是否已消费。

5.4 空回滚与悬挂问题

空回滚(Empty Rollback):Try 阶段尚未执行时 Cancel 就被触发。比如网络超时导致协调者发起 Cancel,但 Try 请求实际还在路上。解决:Cancel 执行时检测到没有对应的事务记录,此时直接插入一条"Cancel 已执行"的记录,后续 Try 到达时看到该记录直接拒绝。

悬挂(Hanging):Cancel 先于 Try 执行了(空回滚场景),然后 Try 才到达并被上述逻辑拒绝,但此时 Try 的资源锁定永远不会被释放。解决:在 Try 阶段也检查是否已有 Cancel 记录,如果有则拒绝执行并记录。

5.5 TCC 落地框架

业界成熟的 TCC 框架包括:Seata(阿里开源)、ByteTee(字节开源)、Himly(滴滴开源)。以 Seata TCC 为例,开发者只需为每个服务定义 TccAction 接口(包含 prepare、commit、rollback 三个方法),框架负责协调各分支事务的提交和回滚,并提供防悬挂、幂等等基础能力。

六、核心方案:Saga 模式

6.1 设计思想

Saga 模式的核心思路是:将一个长事务拆分为多个短事务(本地事务),每个短事务成功后发布事件或消息触发下一个事务;如果某个短事务失败,则依次触发前面已完成事务的补偿操作(补偿事务)回滚。

Saga 非常适合长事务流程(如跨多国的物流报关、跨境支付),因为每个短事务独立提交,避免了长事务锁。

6.2 两种协调方式

编排式 Saga(Choreography):没有中心协调器,每个服务完成自身事务后发布领域事件,其他服务监听事件决定下一步操作。优点是去中心化、松耦合;缺点是流程分散难以追踪,回滚逻辑复杂。

协调器式 Saga(Orchestration):有一个中心协调器(Saga Orchestrator)负责按顺序调用各服务,记录每个步骤的执行状态。如果某步骤失败,协调器按逆序调用已完成步骤的补偿操作。优点:流程清晰、监控方便;缺点:协调器本身可能成为瓶颈。

6.3 Saga 的缺陷与对策

缺陷一:缺乏隔离性 — Saga 中每个本地事务提交后立即对其他事务可见,可能出现"脏读"。比如 A 服务扣了库存但流程还未完成(可能最终失败),B 服务读到扣减后的库存做了依赖决策。

对策:采用语义锁(Semantic Lock)— 在补偿操作完成后才真正删除数据,或采用业务层面的状态标识(如"预扣"状态)。

缺陷二:补偿事务的复杂性 — 不是所有操作都能补偿(如发送了邮件、通知了用户等副作用)。对策:将不可补偿的操作放在流程最后执行(如 Saga 最后一步才发通知),或者采用 TCC 替代。

缺陷三:数据丢失风险 — 协调器故障时 saga 状态丢失。对策:将 Saga 状态持久化到高可用存储(如数据库),协调器重启后恢复。

七、最终方案:基于消息队列的最终一致性

7.1 设计思路

这是目前互联网公司最广泛使用的方案,核心思路是:将分布式事务拆解为本地事务(带消息表)+ 消息队列投递 + 消费者重试 + 对账补偿。这种方案不保证实时一致性,但保证最终一致性(通常延迟在秒级到分钟级)。

7.2 本地消息表方案(经典实现)

步骤一:业务执行方(如订单服务)在同一本地事务中,执行业务操作(创建订单)并插入一条消息记录(消息表:msg_id, topic, payload, status),这两个操作要么一起成功要么一起失败。

步骤二:一个独立的任务(定时任务或消息中继服务)轮询消息表中 status=unconsumed 的记录,将其发送到消息队列(RocketMQ/Kafka/RabbitMQ),并将消息状态更新为 consumed。

步骤三:消费方(如库存服务)订阅该消息队列,执行消费逻辑(扣减库存),消费成功后 ack 消息。

步骤四:如果消费失败,消息队列通过重试机制重新投递。消费方需要实现幂等消费。

步骤五:对账服务定期扫描长时间处于 unconsumed 状态的消息,进行告警或补偿处理。

7.3 事务消息方案(RocketMQ)

RocketMQ 提供了原生的事务消息支持,简化了本地消息表的实现:

步骤一:发送方发送 half message(半消息)到 RocketMQ。此时消息对消费者不可见。

步骤二: RocketMQ 返回 half message 成功后,发送方执行本地事务(如创建订单)。

步骤三:发送方根据本地事务执行结果,向 RocketMQ 提交 commit 或 rollback 指令。commit 后消息对消费者可见;rollback 后消息被删除。

步骤四:如果发送方超时未提交(宕机或网络断连),RocketMQ 会主动回调发送方的 check 接口,查询本地事务的执行状态,根据结果决定 commit 或 rollback。

7.4 幂等消费的六种实现

最终一致性方案的核心挑战是防止重复消费。常用幂等方案:

方案一:数据库唯一约束 — 消费记录中插入业务唯一键(如订单号),重复消费会触发唯一键冲突,自动忽略。

方案二:Redis setnx 防重 — 消费前用 setnx 设置消息 ID,TTL 设为消息过期时间。如果已存在则说明重复,直接跳过。

方案三:状态机前置判断 — 消费前检查业务状态是否已到达目标状态(如订单是否已是"已扣库存"),是则跳过。

方案四:Token 令牌机制 — 消费者调用上游获取唯一 Token,消费时带上 Token 由消息系统验证。

方案五:全局唯一 ID + 去重表 — 每条消息带全局唯一 ID,消费后记录到去重表,消费前查询去重表。

方案六:版本号乐观锁 — 基于数据版本号的 CAS 操作,只处理匹配版本的消息,版本不匹配说明已被其他消息更新过。

八、方案选型指南

没有万能的分布式事务方案,选型需要综合考虑业务场景、性能要求和团队技术栈:

强一致性优先(金融结算、资金转账) → 2PC 或 TCC。因为涉及资金,必须保证实时一致,可以接受性能损耗。

最终一致性可接受(电商下单、积分发放) → 基于消息队列的最终一致性或 TCC。这是互联网最主流的选择。

长事务流程(跨服务编排、工作流) → Saga 模式。特别是补偿逻辑清晰、各步骤可逆的场景。

极致性能要求(秒杀、高并发写) → 异步消息 + 异步补偿。牺牲实时一致性换取吞吐量和响应时间。

已有成熟框架 → 优先使用 Seata 框架(支持 AT/TCC/Saga 三种模式),避免重复造轮子。

九、生产级分布式事务最佳实践

1. 幂等设计是底线 — 任何分布式事务方案都必须保证所有操作的幂等性,这是处理网络超时和重试的基础。

2. 异步化优先于同步 — 能用异步解决的不要用同步。同步方案(2PC)在高并发下性能急剧下降,异步方案(消息队列)天然适应高并发。

3. 补偿事务必须可观测 — 补偿不是万能的,必须有完善的监控、告警和对账机制。不可补偿的操作(如发邮件)应放在 Saga 最后一步。

4. 最小化锁定范围 — TCC 的 Try 阶段只锁定资源不扣减,远比长时间占用数据库锁更高效。Saga 每个短事务独立提交,也不会长期持有锁。

5. 超时无处不在 — 网络调用要有超时设置,事务锁要有过期时间,MQ 消息要有 TTL。没有超时机制的系统最终会因资源耗尽而崩溃。

6. 对账兜底 — 无论方案多么完善,总有对不上的边界场景。必须有定时对账任务发现不一致并自动修复或人工介入。

7. 业务侵入度权衡 — TCC 需要为每个操作设计 Try/Confirm/Cancel 三个接口,业务侵入度高。AT 模式(Seata)对业务无侵入但仅支持关系型数据库。选型时需考虑改造成本。

十、总结

分布式事务是微服务架构中最具挑战性的问题之一。从强一致的 2PC/3PC,到业务补偿的 TCC/Saga,再到最终一致的消息队列方案,每种方案都有其适用场景而没有银弹。

作为工程师,我们需要理解每种方案的本质约束和代价,结合业务场景做出合理的架构选择。大多数互联网业务选择最终一致性方案,不是因为不知道强一致的方案,而是在一致性、可用性和性能之间,最终一致性找到了最好的工程平衡点。

最后记住一句话:没有完美的分布式事务方案,只有最适合业务场景的一致性级别选择。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部