- 行业动态 >
- 资讯详情
如何保证分(fēn)布式系统数据一致性?这六种方案你不该错过
在電(diàn)商(shāng)等业務(wù)中,系统一般由多(duō)个独立的服務(wù)组成,如何解决分(fēn)布式调用(yòng)时候数据的一致性?
具體(tǐ)业務(wù)场景如下,比如一个业務(wù)操作,如果同时调用(yòng)服務(wù)A、B、C,需要满足要么同时成功;要么同时失败。A、B、C可(kě)能(néng)是多(duō)个不同部门开发、部署在不同服務(wù)器上的遠(yuǎn)程服務(wù)。在分(fēn)布式系统来说,如果不想牺牲一致性,CAP理(lǐ)论告诉我们只能(néng)放弃可(kě)用(yòng)性,这显然不能(néng)接受。為(wèi)了便于讨论问题,先简单介绍下数据一致性的基础理(lǐ)论。
强一致:当更新(xīn)操作完成之后,任何多(duō)个后续进程或者線(xiàn)程的访问都会返回最新(xīn)的更新(xīn)过的值。这种是对用(yòng)户最友好的,就是用(yòng)户上一次写什么,下一次就保证能(néng)读到什么。根据CAP理(lǐ)论,这种实现需要牺牲可(kě)用(yòng)性。
弱一致性:系统并不保证续进程或者線(xiàn)程的访问都会返回最新(xīn)的更新(xīn)过的值。系统在数据写入成功之后,不承诺立即可(kě)以读到最新(xīn)写入的值,也不会具體(tǐ)的承诺多(duō)久之后可(kě)以读到。
最终一致性:弱一致性的特定形式。系统保证在没有(yǒu)后续更新(xīn)的前提下,系统最终返回上一次更新(xīn)操作的值。在没有(yǒu)故障发生的前提下,不一致窗口的时间主要受通信延迟,系统负载和复制副本的个数影响。DNS是一个典型的最终一致性系统。
在工程实践上,為(wèi)了保障系统的可(kě)用(yòng)性,互联网系统大多(duō)将强一致性需求转换成最终一致性的需求,并通过系统执行幂等性的保证,保证数据的最终一致性。但在電(diàn)商(shāng)等场景中,对于数据一致性的解决方法和常见的互联网系统(如MySQL主从同步)又(yòu)有(yǒu)一定區(qū)别,群友的讨论分(fēn)成以下6种解决方案。
1. 规避分(fēn)布式事務(wù)——业務(wù)整合
业務(wù)整合方案主要采用(yòng)将接口整合到本地执行的方法。拿(ná)问题场景来说,则可(kě)以将服務(wù)A、B、C整合為(wèi)一个服務(wù)D给业務(wù),这个服務(wù)D再通过转换為(wèi)本地事務(wù)的方式,比如服務(wù)D包含本地服務(wù)和服務(wù)E,而服務(wù)E是本地服務(wù)A~C的整合。
优点:解决(规避)了分(fēn)布式事務(wù)。
缺点:显而易见,把本来规划拆分(fēn)好的业務(wù),又(yòu)耦合到了一起,业務(wù)职责不清晰,不利于维护。
由于这个方法存在明显缺点,通常不建议使用(yòng)。
2. 经典方案 - eBay 模式
此方案的核心是将需要分(fēn)布式处理(lǐ)的任務(wù)通过消息日志(zhì)的方式来异步执行。消息日志(zhì)可(kě)以存储到本地文(wén)本、数据库或消息队列,再通过业務(wù)规则自动或人工发起重试。人工重试更多(duō)的是应用(yòng)于支付场景,通过对账系统对事后问题的处理(lǐ)。
消息日志(zhì)方案的核心是保证服務(wù)接口的幂等性。
考虑到网络通讯失败、数据丢包等原因,如果接口不能(néng)保证幂等性,数据的唯一性将很(hěn)难保证。
eBay方式的主要思路如下。
Base:一种 Acid 的替代方案
此方案是eBay的架构师DanPritchett在2008年发表给ACM的文(wén)章,是一篇解释BASE原则,或者说最终一致性的经典文(wén)章。文(wén)中讨论了BASE与ACID原则在保证数据一致性的基本差异。
如果ACID為(wèi)分(fēn)區(qū)的数据库提供一致性的选择,那么如何实现可(kě)用(yòng)性呢(ne)?答(dá)案是BASE(basicallyavailable,softstate,eventuallyconsistent)。
BASE的可(kě)用(yòng)性是通过支持局部故障而不是系统全局故障来实现的。下面是一个简单的例子:如果将用(yòng)户分(fēn)區(qū)在5个数据库服務(wù)器上,BASE设计鼓励类似的处理(lǐ)方式,一个用(yòng)户数据库的故障只影响这台特定主机那20%的用(yòng)户。这里不涉及任何魔法,不过它确实可(kě)以带来更高的可(kě)感知的系统可(kě)用(yòng)性。
文(wén)章中描述了一个最常见的场景,如果产生了一筆(bǐ)交易,需要在交易表增加记录,同时还要修改用(yòng)户表的金额。这两个表属于不同的遠(yuǎn)程服務(wù),所以就涉及到分(fēn)布式事務(wù)一致性的问题。
文(wén)中提出了一个经典的解决方法,将主要修改操作以及更新(xīn)用(yòng)户表的消息放在一个本地事務(wù)来完成。同时為(wèi)了避免重复消费用(yòng)户表消息带来的问题,达到多(duō)次重试的幂等性,增加一个更新(xīn)记录表updates_applied来记录已经处理(lǐ)过的消息。
系统的执行伪代码如下
基于以上方法,在第一阶段,通过本地的数据库的事務(wù)保障,增加了transaction表及消息队列。
在第二阶段,分(fēn)别读出消息队列(但不删除),通过判断更新(xīn)记录表updates_applied来检测相关记录是否被执行,未被执行的记录会修改user表,然后增加一条操作记录到updates_applied,事務(wù)执行成功之后再删除队列。
通过以上方法,达到了分(fēn)布式系统的最终一致性。进一步了解eBay的方案可(kě)以参考文(wén)末链接。
3.去哪儿网分(fēn)布式事務(wù)方案
随着业務(wù)规模不断地扩大,電(diàn)商(shāng)网站一般都要面临拆分(fēn)之路。就是将原来一个单體(tǐ)应用(yòng)拆分(fēn)成多(duō)个不同职责的子系统。比如以前可(kě)能(néng)将面向用(yòng)户、客户和运营的功能(néng)都放在一个系统里,现在拆分(fēn)為(wèi)订单中心、代理(lǐ)商(shāng)管理(lǐ)、运营系统、报价中心、库存管理(lǐ)等多(duō)个子系统。
拆分(fēn)首先要面临的是什么呢(ne)?
最开始的单體(tǐ)应用(yòng)所有(yǒu)功能(néng)都在一起,存储也在一起。比如运营要取消某个订单,那直接去更新(xīn)订单表状态,然后更新(xīn)库存表就ok了。因為(wèi)是单體(tǐ)应用(yòng),库在一起,这些都可(kě)以在一个事務(wù)里,由关系数据库来保证一致性。
但拆分(fēn)之后就不同了,不同的子系统都有(yǒu)自己的存储。比如订单中心就只管理(lǐ)自己的订单库,而库存管理(lǐ)也有(yǒu)自己的库。那么运营系统取消订单的时候就是通过接口调用(yòng)等方式来调用(yòng)订单中心和库存管理(lǐ)的服務(wù)了,而不是直接去操作库。这就涉及一个『分(fēn)布式事務(wù)』的问题。
分(fēn)布式事務(wù)有(yǒu)两种解决方式
1.优先使用(yòng)异步消息。
上文(wén)已经说过,使用(yòng)异步消息Consumer端需要实现幂等。
幂等有(yǒu)两种方式,一种方式是业務(wù)逻辑保证幂等。比如接到支付成功的消息订单状态变成支付完成,如果当前状态是支付完成,则再收到一个支付成功的消息则说明消息重复了,直接作為(wèi)消息成功处理(lǐ)。
另外一种方式如果业務(wù)逻辑无法保证幂等,则要增加一个去重表或者类似的实现。对于producer端在业務(wù)数据库的同实例上放一个消息库,发消息和业務(wù)操作在同一个本地事務(wù)里。发消息的时候消息并不立即发出,而是向消息库插入一条消息记录,然后在事務(wù)提交的时候再异步将消息发出,发送消息如果成功则将消息库里的消息删除,如果遇到消息队列服務(wù)异常或网络问题,消息没有(yǒu)成功发出那么消息就留在这里了,会有(yǒu)另外一个服務(wù)不断地将这些消息扫出重新(xīn)发送。
2.有(yǒu)的业務(wù)不适合异步消息的方式,事務(wù)的各个参与方都需要同步的得到结果。
这种情况的实现方式其实和上面类似,每个参与方的本地业務(wù)库的同实例上面放一个事務(wù)记录库。
比如A同步调用(yòng)B,C。A本地事務(wù)成功的时候更新(xīn)本地事務(wù)记录状态,B和C同样。如果有(yǒu)一次A调用(yòng)B失败了,这个失败可(kě)能(néng)是B真的失败了,也可(kě)能(néng)是调用(yòng)超时,实际B成功。则由一个中心服務(wù)对比三方的事務(wù)记录表,做一个最终决定。假设现在三方的事務(wù)记录是A成功,B失败,C成功。那么最终决定有(yǒu)两种方式,根据具體(tǐ)场景:
· 重试B,直到B成功,事務(wù)记录表里记录了各项调用(yòng)参数等信息;
· 执行A和B的补偿操作(一种可(kě)行的补偿方式是回滚)。
对b场景做一个特殊说明:比如B是扣库存服務(wù),在第一次调用(yòng)的时候因為(wèi)某种原因失败了,但是重试的时候库存已经变為(wèi)0,无法重试成功,这个时候只有(yǒu)回滚A和C了。
那么可(kě)能(néng)有(yǒu)人觉得在业務(wù)库的同实例里放消息库或事務(wù)记录库,会对业務(wù)侵入,业務(wù)还要关心这个库,是否一个合理(lǐ)的设计?
实际上可(kě)以依靠运维的手段来简化开发的侵入,我们的方法是让DBA在公司所有(yǒu)MySQL实例上预初始化这个库,通过框架层(消息的客户端或事務(wù)RPC框架)透明的在背后操作这个库,业務(wù)开发人员只需要关心自己的业務(wù)逻辑,不需要直接访问这个库。
总结起来,其实两种方式的根本原理(lǐ)是类似的,也就是将分(fēn)布式事務(wù)转换為(wèi)多(duō)个本地事務(wù),然后依靠重试等方式达到最终一致性。
4.蘑菇街(jiē)交易创建过程中的分(fēn)布式一致性方案
交易创建的一般性流程
我们把交易创建流程抽象出一系列可(kě)扩展的功能(néng)点,每个功能(néng)点都可(kě)以有(yǒu)多(duō)个实现(具體(tǐ)的实现之间有(yǒu)组合/互斥关系)。把各个功能(néng)点按照一定流程串起来,就完成了交易创建的过程。
面临的问题
每个功能(néng)点的实现都可(kě)能(néng)会依赖外部服務(wù)。那么如何保证各个服務(wù)之间的数据是一致的呢(ne)?比如锁定优惠券服務(wù)调用(yòng)超时了,不能(néng)确定到底有(yǒu)没有(yǒu)锁券成功,该如何处理(lǐ)?再比如锁券成功了,但是扣减库存失败了,该如何处理(lǐ)?
方案选型
服務(wù)依赖过多(duō),会带来管理(lǐ)复杂性增加和稳定性风险增大的问题。试想如果我们强依赖10个服務(wù),9个都执行成功了,最后一个执行失败了,那么是不是前面9个都要回滚掉?这个成本还是非常高的。
所以在拆分(fēn)大的流程為(wèi)多(duō)个小(xiǎo)的本地事務(wù)的前提下,对于非实时、非强一致性的关联业務(wù)写入,在本地事務(wù)执行成功后,我们选择发消息通知、关联事務(wù)异步化执行的方案。
消息通知往往不能(néng)保证100%成功;且消息通知后,接收方业務(wù)是否能(néng)执行成功还是未知数。前者问题可(kě)以通过重试解决;后者可(kě)以选用(yòng)事務(wù)消息来保证。
但是事務(wù)消息框架本身会给业務(wù)代码带来侵入性和复杂性,所以我们选择基于DB事件变化通知到MQ的方式做系统间解耦,通过订阅方消费MQ消息时的ACK机制,保证消息一定消费成功,达到最终一致性。由于消息可(kě)能(néng)会被重发,消息订阅方业務(wù)逻辑处理(lǐ)要做好幂等保证。
所以目前只剩下需要实时同步做、有(yǒu)强一致性要求的业務(wù)场景了。在交易创建过程中,锁券和扣减库存是这样的两个典型场景。
要保证多(duō)个系统间数据一致,乍一看,必须要引入分(fēn)布式事務(wù)框架才能(néng)解决。但引入非常重的类似二阶段提交分(fēn)布式事務(wù)框架会带来复杂性的急剧上升;在電(diàn)商(shāng)领域,绝对的强一致是过于理(lǐ)想化的,我们可(kě)以选择准实时的最终一致性。
我们在交易创建流程中,首先创建一个不可(kě)见订单,然后在同步调用(yòng)锁券和扣减库存时,针对调用(yòng)异常(失败或者超时),发出废单消息到MQ。如果消息发送失败,本地会做时间阶梯式的异步重试;优惠券系统和库存系统收到消息后,会进行判断是否需要做业務(wù)回滚,这样就准实时地保证了多(duō)个本地事務(wù)的最终一致性。
5.支付宝及蚂蚁金融云的分(fēn)布式服務(wù)DTS方案
业界常用(yòng)的还有(yǒu)支付宝的一种xts方案,由支付宝在2PC的基础上改进而来。主要思路如下,大部分(fēn)信息引用(yòng)自官方网站。
分(fēn)布式事務(wù)服務(wù)简介
分(fēn)布式事務(wù)服務(wù)(DistributedTransactionService,DTS)是一个分(fēn)布式事務(wù)框架,用(yòng)来保障在大规模分(fēn)布式环境下事務(wù)的最终一致性。DTS从架构上分(fēn)為(wèi)xts-client和xts-server两部分(fēn),前者是一个嵌入客户端应用(yòng)的JAR包,主要负责事務(wù)数据的写入和处理(lǐ);后者是一个独立的系统,主要负责异常事務(wù)的恢复。
核心特性
传统关系型数据库的事務(wù)模型必须遵守ACID原则。在单数据库模式下,ACID模型能(néng)有(yǒu)效保障数据的完整性,但是在大规模分(fēn)布式环境下,一个业務(wù)往往会跨越多(duō)个数据库,如何保证这多(duō)个数据库之间的数据一致性,需要其他(tā)行之有(yǒu)效的策略。在JavaEE规范中使用(yòng)2PC(2PhaseCommit,两阶段提交)来处理(lǐ)跨DB环境下的事務(wù)问题,但是2PC是反可(kě)伸缩模式,也就是说,在事務(wù)处理(lǐ)过程中,参与者需要一直持有(yǒu)资源直到整个分(fēn)布式事務(wù)结束。这样,当业務(wù)规模达到千万级以上时,2PC的局限性就越来越明显,系统可(kě)伸缩性会变得很(hěn)差。基于此,我们采用(yòng)BASE的思想实现了一套类似2PC的分(fēn)布式事務(wù)方案,这就是DTS。DTS在充分(fēn)保障分(fēn)布式环境下高可(kě)用(yòng)性、高可(kě)靠性的同时兼顾数据一致性的要求,其最大的特点是保证数据最终一致(Eventuallyconsistent)。
简单的说,DTS框架有(yǒu)如下特性:
· 最终一致:事務(wù)处理(lǐ)过程中,会有(yǒu)短暂不一致的情况,但通过恢复系统,可(kě)以让事務(wù)的数据达到最终一致的目标。
· 协议简单:DTS定义了类似2PC的标准两阶段接口,业務(wù)系统只需要实现对应的接口就可(kě)以使用(yòng)DTS的事務(wù)功能(néng)。
· 与RPC服務(wù)协议无关:在SOA架构下,一个或多(duō)个DB操作往往被包装成一个一个的Service,Service与Service之间通过RPC协议通信。DTS框架构建在SOA架构上,与底层协议无关。
· 与底层事務(wù)实现无关:DTS是一个抽象的基于Service层的概念,与底层事務(wù)实现无关,也就是说在DTS的范围内,无论是关系型数据库MySQL,Oracle,还是KV存储MemCache,或者列存数据库HBase,只要将对其的操作包装成DTS的参与者,就可(kě)以接入到DTS事務(wù)范围内。
以下是分(fēn)布式事務(wù)框架的流程图
实现
· 一个完整的业務(wù)活动由一个主业務(wù)服務(wù)与若干从业務(wù)服務(wù)组成。
· 主业務(wù)服務(wù)负责发起并完成整个业務(wù)活动。
· 从业務(wù)服務(wù)提供TCC型业務(wù)操作。
业務(wù)活动管理(lǐ)器控制业務(wù)活动的一致性,它登记业務(wù)活动中的操作,并在活动提交时确认所有(yǒu)的两阶段事務(wù)的confirm操作,在业務(wù)活动取消时调用(yòng)所有(yǒu)两阶段事務(wù)的cancel操作。”
与2PC协议比较
· 没有(yǒu)单独的Prepare阶段,降低协议成本
· 系统故障容忍度高,恢复简单
6.农信网数据一致性方案
1.電(diàn)商(shāng)业務(wù)
公司的支付部门,通过接入其它第三方支付系统来提供支付服務(wù)给业務(wù)部门,支付服務(wù)是一个基于Dubbo的RPC服務(wù)。
对于业務(wù)部门来说,電(diàn)商(shāng)部门的订单支付,需要调用(yòng)
· 支付平台的支付接口来处理(lǐ)订单;
· 同时需要调用(yòng)积分(fēn)中心的接口,按照业務(wù)规则,给用(yòng)户增加积分(fēn)。
从业務(wù)规则上需要同时保证业務(wù)数据的实时性和一致性,也就是支付成功必须加积分(fēn)。
我们采用(yòng)的方式是同步调用(yòng),首先处理(lǐ)本地事務(wù)业務(wù)。考虑到积分(fēn)业務(wù)比较单一且业務(wù)影响低于支付,由积分(fēn)平台提供增加与回撤接口。
具體(tǐ)的流程是先调用(yòng)积分(fēn)平台增加用(yòng)户积分(fēn),再调用(yòng)支付平台进行支付处理(lǐ),如果处理(lǐ)失败,catch方法调用(yòng)积分(fēn)平台的回撤方法,将本次处理(lǐ)的积分(fēn)订单回撤。
2.用(yòng)户信息变更
公司的用(yòng)户信息,统一由用(yòng)户中心维护,而用(yòng)户信息的变更需要同步给各业務(wù)子系统,业務(wù)子系统再根据变更内容,处理(lǐ)各自业務(wù)。用(yòng)户中心作為(wèi)MQ的producer,添加通知给MQ。APPServer订阅该消息,同步本地数据信息,再处理(lǐ)相关业務(wù)比如APP退出下線(xiàn)等。
我们采用(yòng)异步消息通知机制,目前主要使用(yòng)ActiveMQ,基于VirtualTopic的订阅方式,保证单个业務(wù)集群订阅的单次消费。
总结
分(fēn)布式服務(wù)对衍生的配套系统要求比较多(duō),特别是我们基于消息、日志(zhì)的最终一致性方案,需要考虑消息的积压、消费情况、监控、报警等。