数据一致性和分布式事务

数据一致性

引入事务的目的,是为了保证数据的“一致性”(Consistency)。

何为一致性,例如:处理一个转账业务,其中 A 向 B 转账 ¥50 元。无论是转账前、转账过程中、还是转账完成后,A 和 B 的总金额要求始终保持不变。这意味着数据在整个过程中都保持一致,符合业务约束。

想要达成数据的一致性,需要 3 个方面的努力:

  • 原子性(Atomic): 客户端发起一个请求(请求包含多个操作),如果全部步骤都成功了 → 太好了,交易完成;如果中间出了任何问题 → 所有已经完成的步骤都会被撤销,回到最初的状态。这就是原子性。即,要不同时成功,否则同时失败

  • 隔离性(Isolation):  同时运行的事务不应互相干扰。例如,当一个事务执行多次写入操作时,其他事务应仅能观察到该事务的最终完成结果,而非中间状态。隔离性旨在防止多个事务交叉操作导致的数据不一致问题。

  • 持久性(Durability): 事务处理完成后,对数据的修改应当是永久性的,即使系统发生故障也不会丢失。在单节点数据库中,持久性意味着数据已写入存储设备(如硬盘或 SSD)。而在分布式数据库中,持久性要求数据成功复制到多个节点。为确保持久性,数据库必须在完成数据复制后,才能确认事务已成功提交。

这也就是常说的事务的“ACID 特性”。应用程序借助数据库提供的原子性、隔离性和持久性,来实现一致性目标。也就是说,A、I、D 是手段,C(Consistency)是 3 者协作的目标,弄到一块完全是为了读起来更顺口。

当事务仅涉及本地操作时,一致性通常可以通过代码实现起来水到渠成。但倘若事务的操作对象扩展到外部系统,例如跨越多个微服务、数据源甚至数据中心时,再依赖传统的 A、I、D 手段来解决一致性问题变得非常困难。但是,一致性又是在分布式系统中不可回避且必须解决的核心问题。

这种情况下,我们需要转变观念,将一致性视为一个多维度的问题,而非简单的“是或否”的二元问题。根据不同场景的需求,对一致性的强度进行分级,在确保代价可承受的前提下,尽可能保障系统的一致性。一致性的强弱程度直接影响系统设计权衡。由此,事务从一个具体操作层面的“编程问题”转变成一个需要全局视角的“架构问题”。人们在探索这些架构设计的过程中产生了诸多思路和理论,这其中最为出名的是一致性与可用性的权衡 —— CAP 定理。

一致性与可用性的权衡

CAP 定理描述的是一个分布式系统中,涉及共享数据问题时,以下三个特性最多只能同时满足其中两个

CAP.png

  • 一致性Consistency):意味着数据在任何时刻、任何节点上看到的都是符合预期的。

  • 可用性Availability):可用性代表系统持续提供服务的能力

  • 分区容错性Partition tolerance):当部分节点由于网络故障或通信中断而无法相互联系,形成“网络分区”时,系统仍能够继续正确地提供服务

由于 CAP 定理已有严格的证明,我们不再探讨为何 CAP 不可兼得,直接分析如果舍弃 C、A、P 时所带来的不同影响。

  • 放弃分区容忍性(CA without P):如果没有 P(分区容错性),也就谈不上是真正的分布式系统。

  • 放弃可用性(CP without A):意味着我们将假设一旦网络发生分区,节点之间的信息同步时间可以无限制延长。在现实中,选择放弃可用性系统(又称为 CP 系统)适用于对数据一致性有严格要求的场景,如金融系统、库存管理系统等。这些应用场景中,数据的一致性和准确性通常比系统的可用性更为重要。

  • 放弃一致性(AP without C):意味着在网络分区发生时,节点之间的数据可能会出现不一致。系统会优先保证可用性,而不是一致性。选择放弃一致性系统(又称 AP 系统)已经成为设计分布式系统的主流选择。

对于分布式系统而言,必须实现分区容错性(P)。因此,CAP 定理实际上要求在可用性(A)和一致性(C)之间选择,即在 AP 和 CP 之间权衡取舍。

从上述分析可以看出,原本事务的主要目的是保证“一致性”。但在分布式环境中,一致性往往不得不成为牺牲的属性,AP 类型的系统反而成为了分布式系统的主流。但无论如何,我们设计系统终究还是要确保操作结果至少在最终交付的时刻是正确的,这个意思是允许数据中间不一致,但应该在输出时被修正过来。

为此,工程师们又重新给一致性下了定义,将 CAP、ACID 中讨论的一致性(C)称为“强一致性”,而把牺牲了 C 的 AP 系统但又要尽可能获得正确结果的行为称为追求“弱一致性”

不过,若只是单纯地谈论“弱一致性”,通常意味着不保证一致性。在弱一致性中,工程师们进一步总结出了一种较强的特例,称为“最终一致性”(Eventual Consistency),它由 eBay 的系统架构师 Dan Pritchett 在 BASE 理论中提出。

既然一致性被重新定义了,事务的概念自然也被拓展了。人们把符合 ACID 特性的事务称为“刚性事务”,把本节后面将要介绍的可靠事件队列、TCC、Saga 三种分布式事务统称为“柔性事务”。

柔性事务

BASE 是“Basically Available”、“Soft State”和“Eventually Consistent”的缩写,是对 CAP 定理中 AP(可用性和分区容错性)方案的延伸,强调在分布式系统中即使无法实现强一致性,也可以通过适当的方式使系统最终达到一致性。其核心理念为:

  • 基本可用(Basically Available):系统保证在大多数情况下能够提供服务,即使某些节点出现故障时,仍尽可能保持可用性。这意味着系统优先保障可用性,而非一致性。

  • 柔性状态(Soft state):系统状态允许在一段时间内处于不一致状态。与 ACID 强一致性的要求不同,BASE 允许系统在更新过程处于“柔性”状态,即数据在某些节点上可以暂时不一致。

  • 最终一致性(Eventually consistent):最终一致性强调,即使在网络分区或系统故障的情况下,在经过足够的时间和多次数据同步操作后,所有节点的数据一定会一致。

实现数据一致性的手段

3.1 可靠事件队列

以一个具体的例子帮助你理解“可靠事件队列”的具体做法。

可靠事件队列模型.png
由此可见,在可靠消息队列方案中,一旦第一步扣款成功,就不再考虑失败回滚的情况,后面只有成功一条路可选。

这种依赖持续重试来确保可靠性的解决方案在计算机领域被广泛应用。它还有专有的名称 —— “最大努力交付”(Best-Effort Delivery)。因此,可靠事件队列也称为“最大努力一次提交”(Best-Effort 1PC)机制,也就是将最容易出错的业务通过本地事务完成后,借助不断重试的机制促使同一个事务中其他操作也顺利完成。

3.2 TCC

TCC(Try、Confirm、Cancel)引入了一种新的事务模型,允许业务层自定义事务,并根据业务需求控制锁的粒度,从而解决了复杂业务中跨表、跨库等大粒度资源锁定的问题。

如同 TCC 事务模型的名字,它由三个阶段组成:

  • Try 阶段:该阶段的主要任务是预留资源或执行初步操作,但不提交事务。Try 阶段确保所有相关操作可以成功执行且没有资源冲突。例如,在预订系统中,这一阶段可能包括检查商品库存并暂时锁定商品。

  • Confirm 阶段:如果 Try 阶段成功,系统进入 Confirm 阶段。在此阶段,系统会提交所有操作,确保事务最终生效。由于 Try 阶段已保证资源的可用性和一致性,Confirm 阶段的执行是无条件的,不会发生失败。

  • Cancel 阶段:如果 Try 阶段失败,或需要回滚事务,系统进入 Cancel 阶段。此时,系统会撤销 Try 阶段中的所有预留操作并释放资源。Cancel 阶段确保事务无法完成时,系统能够恢复最初的状态。

TCC 事务模型.png
首先,用户向商店发送购买某商品的交易请求,金额为 ¥100。请看下面的过程:

  1. Try 阶段:创建事务,生成事务 ID,并记录在事务日志中,进入 Try 阶段。该阶段主要预留业务资源,以及做一些初始化工作:

    • 与支付服务通信,确认用户是否有足够的余额。若余额足够,将用户的 100 元设置为冻结状态,并通知进行 Confirm 阶段;如果不可行,通知进入 Cancel 阶段。
    • 与仓库服务通信,确认商品的库存是否满足。若库存充足,将仓库中该商品的一条库存设置为冻结状态,并通知进行 Confirm 阶段;如果不可行,通知进入 Cancel 阶段。
  2. Confirm 阶段:如果所有服务反馈业务可行,将事务日志状态更新为 Confirm,进入 Confirm 阶段。

    • 支付服务:扣除冻结的 100 元。
    • 仓库服务:标记冻结的库存为出库状态,并扣减库存。
  3. Cancel 阶:如果 Try 阶段任何一方反馈失败,将事务日志状态更新为 Cancel,进入 Cancel 阶段:

    • 支付服务:释放被冻结的 100 元。
    • 仓库服务:释放被冻结的库存。

按照 TCC 事务模型的规定,Confirm 和 Cancel 阶段只返回成功,不会返回失败。如果 Try 阶段之后,出现网络问题或者服务器宕机,那么事务管理器要不断重试 Confirm 阶段或者 Cancel 阶段,直至完成整个事务流程。

通常的情况,我们没必要裸编码实现 TCC 事务模型,而是利用分布式事务中间件(如 Seata、ByteTCC)降低编码工作,提升开发效率。

3.3 Saga事务模型

Saga 事务模型由两部分操作组成:

  1. 一部分是将大事务 T 拆分成若干小事务,命名为 T1,T2,Tn。每个子事务被应被视为原子行为,如果分布式事务 T 能够正常提交,那么它对数据的影响应该与连续按顺序成功提交子事务 Ti 等价。

  2. 另一部分是为每个子事务设计对应的补偿动作,命名为 C1,C2,Cn。Ti 与 Ci 满足以下条件:

    • Ti 与 Ci 具备幂等性。
    • Ti 与 Ci 满足交换律,即无论先执行 Ti 还是先执行 Ci,其效果都是一样的。
    • Ci 必须能成功提交,即不考虑 Ci 的失败回滚情况,如果出现失败持续重试直至成功或者被人工介入为止。

如果 T1 到 Tn 均执行成功,那么整个事务顺利完成,否则根据下面两种机制之一进行事务恢复。

  • 正向操作(Forward Recovery)如果 Ti 提交失败,则一直对 Ti 进行重试,直至成功为止(使用最大努力交付机制)。这种恢复方式不需要进行补偿,适用于事务最终都要执行成功的情况。如订单服务中银行已经扣款,那么就一定要发货。

  • 逆向恢复(Backward Recovery)如果 Ti 提交失败,则执行对应的补偿 Ci,直至恢复到 Ti 之前的状态,这里要求 Ci 必须成功(使用最大努力交付机制)。

Saga 模式非常适合处理流程较长且需要确保事务最终一致性的业务操作。例如,一个旅游预订平台,用户可以同时预订机票、酒店和租车服务,这三项服务可能由不同的微服务或第三方供应商提供。这个场景中,Saga 事务模型允许系统逐步执行每个操作,并在任一步骤失败时有序地进行补偿操作,从而确保系统的一致性和提升用户体验。

与 TCC 相比,Saga 通常采用事件驱动设计,即每个服务都是异步执行的,无需设计资源的冻结状态或处理撤销冻结的操作。然而,这种方式也存在一些问题,比如缺乏隔离性,多个 Saga 事务同时操作同一数据源时,因缺乏隔离机制,操作无法保证原子性,可能导致数据被覆盖的情况。

3.4 服务幂等性设计

幂等性是一个数学概念,后来被引入计算机科学中,用来描述某个操作可以安全地重试,即多次执行的结果与单次执行的结果完全一致。

前面介绍柔性事务通常基于“最大努力交付”机制,即在网络通信失败、节点宕机或进程崩溃时,通过重复请求来实现容错。因此,如果某些关键服务不具备幂等性,重复请求会导致数据不一致或其他问题。例如,重复请求一个不具备幂等性的退款接口,可能导致重复退款。

接下来,介绍两种实现系统幂等的方式.

3.4.1 全局唯一 ID 方案

全局唯一 ID 方案的核心思想是,为每个操作生成一个独一无二的标识符,以此判断是否已经执行过该操作。

局唯一 ID 方案的操作步骤如下:

  • 生成唯一 ID:每次执行操作前,根据业务操作生成一个全局唯一ID,这个 ID 可以利用 UUID、雪花算法(Snowflake) 、Uidgenerator 或 Leaf 等算法生成。

  • 附加到请求: 将生成的唯一 ID 附加到请求中,作为请求的一个参数、HTTP 头或请求体的一部分。

  • 处理请求: 服务器端接收到请求后,首先检查唯一 ID:

    • 如果 ID 已存在:说明该请求已经被处理过,服务器直接返回之前的响应结果,避免重复处理。
    • 如果 ID 不存在:执行请求的操作,并将操作结果和该 ID 存储在数据存储中(如数据库、缓存等),以供后续请求检查。

3.4.2 乐观锁

假设有一个账户表 accounts,包含字段 id(账户ID)和 balance(账户余额)。现在要给账户 ID 为 1 的账户增加余额

1
UPDATE accounts SET balance = balance + 100 WHERE id = 1;

乐观锁基本思想是,假设并发操作发生冲突的概率较低,允许多个事务或线程在不加锁的情况下同时读取数据,但在写入数据时再进行冲突检测。如果在写入前检测到数据已被其他事务修改,则放弃当前操作,避免数据不一致的情况。

结合上述增加余额的 SQL,请看下面具体的操作:

  • 增加版本号字段:在涉及更新的数据表中增加一个 version 字段,更新数据时,版本号随之增加。
  • 更新时检查版本号:执行更新操作时,通过 WHERE 子句检查当前版本号是否与读取时的版本号一致,如果一致则执行更新,并更新版本号。
  • 重试机制:如果更新操作失败,意味着数据库内的数据已经被修改。此时,业务层面请求最新的数据,更新本地 version 并发起重试,直至成功或达到最大重试次数。
1
2
3
4
UPDATE accounts 
SET balance = balance + ?,
version = version + 1
WHERE id = ? AND version = ?;

上面乐观锁的操作模式,是一种典型的 CAS(Compare And Swap | Compare And Set,比较并交换)操作。CAS 有时也被称为“轻量级事务”。

【出处】


数据一致性和分布式事务
https://stuartyang.site/2024/12/26/数据一致性和分布式事务/
作者
Stuart Yang
更新于
2025年3月27日
许可协议