保证分布式系统数据一致性的6种方案

编者按:本文由「高可用架构后花园」讨论整理而成。html

有人的地方,就有江湖数据库

有江湖的地方,就有纷争服务器

问题的起源网络

在电商等业务中,系统通常由多个独立的服务组成,如何解决分布式调用时候数据的一致性? 架构

具体业务场景以下,好比一个业务操做,若是同时调用服务 A、B、C,须要知足要么同时成功;要么同时失败。A、B、C 多是多个不一样部门开发、部署在不一样服务器上的远程服务。app

在分布式系统来讲,若是不想牺牲一致性,CAP 理论告诉咱们只能放弃可用性,这显然不能接受。为了便于讨论问题,先简单介绍下数据一致性的基础理论。框架

强一致运维

当更新操做完成以后,任何多个后续进程或者线程的访问都会返回最新的更新过的值。这种是对用户最友好的,就是用户上一次写什么,下一次就保证能读到什么。根据 CAP 理论,这种实现须要牺牲可用性。异步

弱一致性分布式

系统并不保证续进程或者线程的访问都会返回最新的更新过的值。系统在数据写入成功以后,不承诺当即能够读到最新写入的值,也不会具体的承诺多久以后能够读到。

最终一致性

弱一致性的特定形式。系统保证在没有后续更新的前提下,系统最终返回上一次更新操做的值。在没有故障发生的前提下,不一致窗口的时间主要受通讯延迟,系统负载和复制副本的个数影响。DNS 是一个典型的最终一致性系统。

在工程实践上,为了保障系统的可用性,互联网系统大多将强一致性需求转换成最终一致性的需求,并经过系统执行幂等性的保证,保证数据的最终一致性。但在电商等场景中,对于数据一致性的解决方法和常见的互联网系统(如 MySQL 主从同步)又有必定区别,群友的讨论分红如下 6 种解决方案。

1. 规避分布式事务——业务整合

业务整合方案主要采用将接口整合到本地执行的方法。拿问题场景来讲,则能够将服务 A、B、C 整合为一个服务 D 给业务,这个服务 D 再经过转换为本地事务的方式,好比服务 D 包含本地服务和服务 E,而服务 E 是本地服务 A ~ C 的整合。

优势:解决(规避)了分布式事务。

缺点:显而易见,把原本规划拆分好的业务,又耦合到了一块儿,业务职责不清晰,不利于维护。

因为这个方法存在明显缺点,一般不建议使用。

2. 经典方案 - eBay 模式

此方案的核心是将须要分布式处理的任务经过消息日志的方式来异步执行。消息日志能够存储到本地文本、数据库或消息队列,再经过业务规则自动或人工发起重试。人工重试更多的是应用于支付场景,经过对帐系统对过后问题的处理。

消息日志方案的核心是保证服务接口的幂等性。

考虑到网络通信失败、数据丢包等缘由,若是接口不能保证幂等性,数据的惟一性将很难保证。

eBay 方式的主要思路以下。

Base:一种 Acid 的替代方案

此方案是 eBay 的架构师 Dan Pritchett 在 2008 年发表给 ACM 的文章,是一篇解释 BASE 原则,或者说最终一致性的经典文章。文中讨论了 BASE 与 ACID 原则在保证数据一致性的基本差别。

若是 ACID 为分区的数据库提供一致性的选择,那么如何实现可用性呢?答案是

BASE (basically available, soft state, eventually consistent)

BASE 的可用性是经过支持局部故障而不是系统全局故障来实现的。下面是一个简单的例子:若是将用户分区在 5 个数据库服务器上,BASE 设计鼓励相似的处理方式,一个用户数据库的故障只影响这台特定主机那 20% 的用户。这里不涉及任何魔法,不过它确实能够带来更高的可感知的系统可用性。

文章中描述了一个最多见的场景,若是产生了一笔交易,须要在交易表增长记录,同时还要修改用户表的金额。这两个表属于不一样的远程服务,因此就涉及到分布式事务一致性的问题。

 

 

文中提出了一个经典的解决方法,将主要修改操做以及更新用户表的消息放在一个本地事务来完成。同时为了不重复消费用户表消息带来的问题,达到屡次重试的幂等性,增长一个更新记录表 updates_applied 来记录已经处理过的消息。

 

 

系统的执行伪代码以下

 

 

(点击可全屏缩放图片)

基于以上方法,在第一阶段,经过本地的数据库的事务保障,增长了 transaction 表及消息队列 。

在第二阶段,分别读出消息队列(但不删除),经过判断更新记录表 updates_applied 来检测相关记录是否被执行,未被执行的记录会修改 user 表,而后增长一条操做记录到 updates_applied,事务执行成功以后再删除队列。

经过以上方法,达到了分布式系统的最终一致性。进一步了解 eBay 的方案能够参考文末连接。

3. 去哪儿网分布式事务方案

随着业务规模不断地扩大,电商网站通常都要面临拆分之路。就是将原来一个单体应用拆分红多个不一样职责的子系统。好比之前可能将面向用户、客户和运营的功能都放在一个系统里,如今拆分为订单中心、代理商管理、运营系统、报价中心、库存管理等多个子系统。

拆分首先要面临的是什么呢?

最开始的单体应用全部功能都在一块儿,存储也在一块儿。好比运营要取消某个订单,那直接去更新订单表状态,而后更新库存表就 ok 了。由于是单体应用,库在一块儿,这些均可以在一个事务里,由关系数据库来保证一致性。

但拆分以后就不一样了,不一样的子系统都有本身的存储。好比订单中心就只管理本身的订单库,而库存管理也有本身的库。那么运营系统取消订单的时候就是经过接口调用等方式来调用订单中心和库存管理的服务了,而不是直接去操做库。这就涉及一个『分布式事务』的问题。 

 

 

分布式事务有两种解决方式

1. 优先使用异步消息。

上文已经说过,使用异步消息 Consumer 端须要实现幂等。

幂等有两种方式,一种方式是业务逻辑保证幂等。好比接到支付成功的消息订单状态变成支付完成,若是当前状态是支付完成,则再收到一个支付成功的消息则说明消息重复了,直接做为消息成功处理。

另一种方式若是业务逻辑没法保证幂等,则要增长一个去重表或者相似的实现。对于 producer 端在业务数据库的同实例上放一个消息库,发消息和业务操做在同一个本地事务里。发消息的时候消息并不当即发出,而是向消息库插入一条消息记录,而后在事务提交的时候再异步将消息发出,发送消息若是成功则将消息库里的消息删除,若是遇到消息队列服务异常或网络问题,消息没有成功发出那么消息就留在这里了,会有另一个服务不断地将这些消息扫出从新发送。

2. 有的业务不适合异步消息的方式,事务的各个参与方都须要同步的获得结果。这种状况的实现方式其实和上面相似,每一个参与方的本地业务库的同实例上面放一个事务记录库。

好比 A 同步调用 B,C。A 本地事务成功的时候更新本地事务记录状态,B 和 C 一样。若是有一次 A 调用 B 失败了,这个失败多是 B 真的失败了,也多是调用超时,实际 B 成功。则由一个中心服务对比三方的事务记录表,作一个最终决定。假设如今三方的事务记录是 A 成功,B 失败,C 成功。那么最终决定有两种方式,根据具体场景:

  1. 重试 B,直到 B 成功,事务记录表里记录了各项调用参数等信息;

  2. 执行 A 和 B 的补偿操做(一种可行的补偿方式是回滚)。

对 b 场景作一个特殊说明:好比 B 是扣库存服务,在第一次调用的时候由于某种缘由失败了,可是重试的时候库存已经变为 0,没法重试成功,这个时候只有回滚 A 和 C 了。

那么可能有人以为在业务库的同实例里放消息库或事务记录库,会对业务侵入,业务还要关心这个库,是否一个合理的设计?

实际上能够依靠运维的手段来简化开发的侵入,咱们的方法是让 DBA 在公司全部 MySQL 实例上预初始化这个库,经过框架层(消息的客户端或事务 RPC 框架)透明的在背后操做这个库,业务开发人员只须要关心本身的业务逻辑,不须要直接访问这个库。

总结起来,其实两种方式的根本原理是相似的,也就是将分布式事务转换为多个本地事务,而后依靠重试等方式达到最终一致性

4. 蘑菇街交易建立过程当中的分布式一致性方案

交易建立的通常性流程

咱们把交易建立流程抽象出一系列可扩展的功能点,每一个功能点均可以有多个实现(具体的实现之间有组合/互斥关系)。把各个功能点按照必定流程串起来,就完成了交易建立的过程。 

 

 

面临的问题

每一个功能点的实现均可能会依赖外部服务。那么如何保证各个服务之间的数据是一致的呢?好比锁定优惠券服务调用超时了,不能肯定到底有没有锁券成功,该如何处理?再好比锁券成功了,可是扣减库存失败了,该如何处理?

方案选型

服务依赖过多,会带来管理复杂性增长和稳定性风险增大的问题。试想若是咱们强依赖 10 个服务,9 个都执行成功了,最后一个执行失败了,那么是否是前面 9 个都要回滚掉?这个成本仍是很是高的。

因此在拆分大的流程为多个小的本地事务的前提下,对于非实时、非强一致性的关联业务写入,在本地事务执行成功后,咱们选择发消息通知、关联事务异步化执行的方案。

消息通知每每不能保证 100% 成功;且消息通知后,接收方业务是否能执行成功仍是未知数。前者问题能够经过重试解决;后者能够选用事务消息来保证。

可是事务消息框架自己会给业务代码带来侵入性和复杂性,因此咱们选择基于 DB 事件变化通知到 MQ 的方式作系统间解耦,经过订阅方消费 MQ 消息时的 ACK 机制,保证消息必定消费成功,达到最终一致性。因为消息可能会被重发,消息订阅方业务逻辑处理要作好幂等保证。

因此目前只剩下须要实时同步作、有强一致性要求的业务场景了。在交易建立过程当中,锁券和扣减库存是这样的两个典型场景。

要保证多个系统间数据一致,乍一看,必需要引入分布式事务框架才能解决。但引入很是重的相似二阶段提交分布式事务框架会带来复杂性的急剧上升;在电商领域,绝对的强一致是过于理想化的,咱们能够选择准实时的最终一致性。

咱们在交易建立流程中,首先建立一个不可见订单,而后在同步调用锁券和扣减库存时,针对调用异常(失败或者超时),发出废单消息到MQ。若是消息发送失败,本地会作时间阶梯式的异步重试;优惠券系统和库存系统收到消息后,会进行判断是否须要作业务回滚,这样就准实时地保证了多个本地事务的最终一致性。

 

 

 5. 支付宝及蚂蚁金融云的分布式服务 DTS 方案

业界经常使用的还有支付宝的一种 xts 方案,由支付宝在 2PC 的基础上改进而来。主要思路以下,大部分信息引用自官方网站。

分布式事务服务简介

分布式事务服务 (Distributed Transaction Service, DTS) 是一个分布式事务框架,用来保障在大规模分布式环境下事务的最终一致性。DTS 从架构上分为 xts-client 和 xts-server 两部分,前者是一个嵌入客户端应用的 JAR 包,主要负责事务数据的写入和处理;后者是一个独立的系统,主要负责异常事务的恢复。

核心特性

传统关系型数据库的事务模型必须遵照 ACID 原则。在单数据库模式下,ACID 模型能有效保障数据的完整性,可是在大规模分布式环境下,一个业务每每会跨越多个数据库,如何保证这多个数据库之间的数据一致性,须要其余行之有效的策略。在 JavaEE 规范中使用 2PC (2 Phase Commit, 两阶段提交) 来处理跨 DB 环境下的事务问题,可是 2PC 是反可伸缩模式,也就是说,在事务处理过程当中,参与者须要一直持有资源直到整个分布式事务结束。这样,当业务规模达到千万级以上时,2PC 的局限性就愈来愈明显,系统可伸缩性会变得不好。基于此,咱们采用 BASE 的思想实现了一套相似 2PC 的分布式事务方案,这就是 DTS。DTS在充分保障分布式环境下高可用性、高可靠性的同时兼顾数据一致性的要求,其最大的特色是保证数据最终一致 (Eventually consistent)。

简单的说,DTS 框架有以下特性:

  • 最终一致:事务处理过程当中,会有短暂不一致的状况,但经过恢复系统,可让事务的数据达到最终一致的目标。

  • 协议简单:DTS 定义了相似 2PC 的标准两阶段接口,业务系统只须要实现对应的接口就可使用 DTS 的事务功能。

  • 与 RPC 服务协议无关:在 SOA 架构下,一个或多个 DB 操做每每被包装成一个一个的 Service,Service 与 Service 之间经过 RPC 协议通讯。DTS 框架构建在 SOA 架构上,与底层协议无关。

  • 与底层事务实现无关: DTS 是一个抽象的基于 Service 层的概念,与底层事务实现无关,也就是说在 DTS 的范围内,不管是关系型数据库 MySQL,Oracle,仍是 KV 存储 MemCache,或者列存数据库 HBase,只要将对其的操做包装成 DTS 的参与者,就能够接入到 DTS 事务范围内。

如下是分布式事务框架的流程图 

 

 


实现

  1. 一个完整的业务活动由一个主业务服务与若干从业务服务组成。

  2. 主业务服务负责发起并完成整个业务活动。

  3. 从业务服务提供 TCC 型业务操做。

  4. 业务活动管理器控制业务活动的一致性,它登记业务活动中的操做,并在活动提交时确认全部的两阶段事务的 confirm 操做,在业务活动取消时调用全部两阶段事务的 cancel 操做。”

与 2PC 协议比较

  1. 没有单独的 Prepare 阶段,下降协议成本

  2. 系统故障容忍度高,恢复简单

6. 农信网数据一致性方案

1. 电商业务

公司的支付部门,经过接入其它第三方支付系统来提供支付服务给业务部门,支付服务是一个基于 Dubbo 的 RPC 服务。

对于业务部门来讲,电商部门的订单支付,须要调用

  1. 支付平台的支付接口来处理订单;

  2. 同时须要调用积分中心的接口,按照业务规则,给用户增长积分。

从业务规则上须要同时保证业务数据的实时性和一致性,也就是支付成功必须加积分。

咱们采用的方式是同步调用,首先处理本地事务业务。考虑到积分业务比较单一且业务影响低于支付,由积分平台提供增长与回撤接口。

具体的流程是先调用积分平台增长用户积分,再调用支付平台进行支付处理,若是处理失败,catch 方法调用积分平台的回撤方法,将本次处理的积分订单回撤。

 

 

(点击图片能够全屏缩放)

2. 用户信息变动

公司的用户信息,统一由用户中心维护,而用户信息的变动须要同步给各业务子系统,业务子系统再根据变动内容,处理各自业务。用户中心做为 MQ 的 producer,添加通知给 MQ。APP Server 订阅该消息,同步本地数据信息,再处理相关业务好比 APP 退出下线等。

咱们采用异步消息通知机制,目前主要使用 ActiveMQ,基于 Virtual Topic 的订阅方式,保证单个业务集群订阅的单次消费。

 

 

总结

分布式服务对衍生的配套系统要求比较多,特别是咱们基于消息、日志的最终一致性方案,须要考虑消息的积压、消费状况、监控、报警等。

参考资料

  • Base: An Acid Alternative (eBay 方案)

In partitioned databases, trading some consistency for availability can lead to dramatic improvements in scalability.

英文版 : http://queue.acm.org/detail.cfm?id=1394128 

中文版: http://article.yeeyan.org/view/167444/125572

感谢李玉福、余昭辉、蘑菇街七公提供方案,其余多位群成员对本文内容亦有贡献。

本文编辑李玉福、Tim Yang,转载请注明来自@高可用架构