微服务架构下处理分布式事务,你必须知道的事儿
根据微服务架构的鼻祖 Martin Fowler 的忠告,微服务架构中应当尽量避免分布式事务。然而,在某些领域,分布式事务如同宿命中的对手无法避免。
在工程领域,分布式事务的讨论主要聚焦于强一致性和最终一致性的解决方案。
典型方案包括:
- 两阶段提交(2PC, Two-phase Commit)方案。
- eBay 事件队列方案。
- TCC 补偿模式。
- 缓存数据最终一致性。
一致性理论
分布式事务的目的是保障分库数据一致性,而跨库事务会遇到各种不可控制的问题,如个别节点永久性宕机,像单机事务一样的 ACID 是无法奢望的。
另外,业界著名的 CAP 理论也告诉我们,对分布式系统,需要将数据一致性和系统可用性、分区容忍性放在天平上一起考虑。
两阶段提交协议(简称2PC)是实现分布式事务较为经典的方案,但 2PC 的可扩展性很差,在分布式架构下应用代价较大,eBay 架构师 Dan Pritchett 提出了 BASE 理论,用于解决大规模分布式系统下的数据一致性问题。
BASE 理论告诉我们:可以通过放弃系统在每个时刻的强一致性来换取系统的可扩展性。
01.CAP 理论
在分布式系统中,一致性(Consistency)、可用性(Availability)和分区容忍性(Partition Tolerance)3 个要素最多只能同时满足两个,不可兼得。其中,分区容忍性又是不可或缺的。
- 一致性:分布式环境下,多个节点的数据是否强一致。
- 可用性:分布式服务能一直保证可用状态。当用户发出一个请求后,服务能在有限时间内返回结果。
- 分区容忍性:特指对网络分区的容忍性。
举例:Cassandra、Dynamo 等,默认优先选择 AP,弱化 C;HBase、MongoDB 等,默认优先选择 CP,弱化 A。
02.BASE 理论
核心思想:
- 基本可用(Basically Available):指分布式系统在出现故障时,允许损失部分的可用性来保证核心可用。
- 软状态(Soft State):指允许分布式系统存在中间状态,该中间状态不会影响到系统的整体可用性。
- 最终一致性(Eventual Consistency):指分布式系统中的所有副本数据经过一定时间后,最终能够达到一致的状态。
一致性模型
数据的一致性模型可以分成以下三类:
- 强一致性:数据更新成功后,任意时刻所有副本中的数据都是一致的,一般采用同步的方式实现。
- 弱一致性:数据更新成功后,系统不承诺立即可以读到最新写入的值,也不承诺具体多久之后可以读到。
- 最终一致性:弱一致性的一种形式,数据更新成功后,系统不承诺立即可以返回最新写入的值,但是保证最终会返回上一次更新操作的值。
分布式系统数据的强一致性、弱一致性和最终一致性可以通过 Quorum NRW 算法分析。
分布式事务解决方案
01.2PC 方案——强一致性
2PC 的核心原理是通过提交分阶段和记日志的方式,记录下事务提交所处的阶段状态,在组件宕机重启后,可通过日志恢复事务提交的阶段状态,并在这个状态节点重试。
如 Coordinator 重启后,通过日志可以确定提交处于 Prepare 还是 Prepare All 状态。若是前者,说明有节点可能没有 Prepare 成功,或所有节点 Prepare 成功但还没有下发 Commit,状态恢复后给所有节点下发 RollBack。
若是 Prepare All 状态,需要给所有节点下发 Commit,数据库节点需要保证 Commit 幂等。
2PC 方案的三个问题:
- 同步阻塞。
- 数据不一致。
- 单点问题。
升级的 3PC 方案旨在解决这些问题,主要有两个改进:
- 增加超时机制。
- 两阶段之间插入准备阶段。
但三阶段提交也存在一些缺陷,要彻底从协议层面避免数据不一致,可以采用 Paxos 或者 Raft 算法。
02.eBay 事件队列方案——最终一致性
eBay 的架构师 Dan Pritchett,曾在一篇解释 BASE 原理的论文《Base:An Acid Alternative》中提到一个 eBay 分布式系统一致性问题的解决方案。
它的核心思想是将需要分布式处理的任务通过消息或者日志的方式来异步执行,消息或日志可以存到本地文件、数据库或消息队列,再通过业务规则进行失败重试,它要求各服务的接口是幂等的。
描述的场景为,有用户表 user 和交易表 transaction,用户表存储用户信息、总销售额和总购买额。交易表存储每一笔交易的流水号、买家信息、卖家信息和交易金额。如果产生了一笔交易,需要在交易表增加记录,同时还要修改用户表的金额。
论文中提出的解决方法是将更新交易表记录和用户表更新消息放在一个本地事务来完成,为了避免重复消费用户表更新消息带来的问题,增加一个操作记录表 updates_applied 来记录已经完成的交易相关的信息。
这个方案的核心在于第二阶段的重试和幂等执行。失败后重试,这是一种补偿机制,它是能保证系统最终一致的关键流程。
03.TCC (Try-Confirm-Cancel)补偿模式——最终一致性
某业务模型如图,由服务 A、服务 B、服务 C、服务 D 共同组成的一个微服务架构系统。服务 A 需要依次调用服务 B、服务 C 和服务 D 共同完成一个操作。
当服务 A 调用服务 D 失败时,若要保证整个系统数据的一致性,就要对服务 B 和服务 C 的 invoke 操作进行回滚,执行反向的 revert 操作。回滚成功后,整个微服务系统是数据一致的。
实现的三个关键要素:
- 服务调用链必须被记录下来。
- 每个服务提供者都需要提供一组业务逻辑相反的操作,互为补偿,同时回滚操作要保证幂等。
- 必须按失败原因执行不同的回滚策略。
实现的两个难点:
- 补偿模式的特点是实现简单,但是想形成一定程度的通用方案比较困难,特别是服务链的记录,因为大部分时候,业务参数或者业务逻辑千差万别。
- 很多业务特征使得该服务无法提供一个安全的回滚操作。
04.缓存数据最终一致性
在我们的业务系统中,缓存(Redis 或者 Memcached)通常被用在数据库前面,作为数据读取的缓冲,使得 I/O 操作不至于直接落在数据库上。
以商品详情页为例,假如卖家修改了商品信息,并写回到数据库,但是这时候用户从商品详情页看到的信息还是从缓存中拿到的过时数据,这就出现了缓存系统和数据库系统中的数据不一致的现象。
要解决该场景下缓存和数据库数据不一致的问题,我们有以下两种解决方案:
- 为缓存数据设置过期时间。当缓存中数据过期后,业务系统会从数据库中获取数据,并将新值放入缓存。这个过期时间就是系统可以达到最终一致的容忍时间。
- 更新数据库数据后,同时清除缓存数据。数据库数据更新后,同步删除缓存中数据,使得下次对商品详情的获取直接从数据库中获取,并同步到缓存。
选择建议
在面临数据一致性问题的时候,首先要从业务需求的角度出发,确定我们对于三种一致性模型的接受程度,再通过具体场景来决定解决方案。
从应用角度看,分布式事务的现实场景常常无法规避,在有能力给出其他解决方案前,2PC 也是一个不错的选择。
对购物转账等电商和金融业务,中间件层的 2PC 最大问题在于业务不可见,一旦出现不可抗力或意想不到的一致性破坏。
如数据节点永久性宕机,业务难以根据 2PC 的日志进行补偿。金融场景下,数据一致性是命根,业务需要对数据有百分之百的掌控力。
建议使用 TCC 这类分布式事务模型,或基于消息队列的柔性事务框架,这两种方案都在业务层实现,业务开发者具有足够掌控力,可以结合 SOA 框架来架构,包括 Dubbo、Spring Cloud 等。