-
2022-03-12 21:21:17
数据一致性解决方案
CAP理论
C:一致性、A:可用性、P:分区容错性
CAP只能满足两个
CA:两阶段提交的严格选举协议
CP弱A:RAFT协议等多数派选举协议
AP:GOSSIP等冲突解决协议数据一致性
时间一致性:所有相关数据副本任意时刻数据一致
事务一致性:事务之行前后数据一致
应用一致性:分布式事务一致BASE理论
基本可用:允许偶尔失败
软状态:数据状态要求任意时刻一致
最终一致性:弱一致性数据一致性解决方案
多副本数据一致性:
一般用于容灾和高可用:副本之间通过同步复制或者异步复制的方式达到数据一致。
分布式事务数据一致性:
本地事务是指数据库的单机事务处理,有点是严格的ACID特性,高效、可靠、状态可以只在资源管理器中维护、应用编程模型简单。缺点:不具备分布式事务处理能力,隔离的最小单位受限于资源管理器。
分布式事务:事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点上。强一致性解决方案:
一次请求对多个数据源的数据进行完整性以及一致性的操作,满足事务的特性,要么全部成功,要么全部失败,保证原子性以及可见性。
强一致性通过锁定资源的方式,但是会牺牲性能,使用之前需要谨慎评估。XA分布式事务
XA协议:
是X/OPEN组织提出的分布式事务规范,该规范主要定义了全局事务管理器(TM)和本地资源管理器(RM)之间的接口,本地资源管理器往往由数据库实现。
主流的数据库产品都提供了XA接口,XA接口是一个双向的系统接口,在事务管理器以及多个资源管理器之间作为通信桥梁。
之所以需要XA,是因为在分布式系统中从理论上讲,两台机器是无法达到一致性状态的,因此引入了一个单节点进行协调。
有全局事务管理器管理的机器可以跨多个资源和进程,负责各个本地资源的提交和回滚,全局事务管理器一般使用XA二阶段提交协议与数据库进行交互。二阶段提交(2PC)
准备阶段:
事务协调者向所有事务参与者发送事务内容,询问是否可以提交事务,并等待参与者回复。
提交阶段:
如果协调者收到了参与者的失败信息或者超时信息,直接给所有参与者发送回滚信息进行事务回滚;否则发送提交信息。
2PC方案实现起来比较简单,但是实际项目中使用比较少
缺陷:- 性能问题:所有参与者在事务提交阶段处于同步阻塞状态,容易造成性能问题;
- 可靠性问题:如果协调者存在单点故障问题,一旦协调者出现故障,参与者一直处于锁定状态;
- 数据一致性问题:第二阶段如果发生局部网络问题,一部分收到提交消息,一部分没有收到,会导致数据不一致。
三阶段提交(3PC)
三阶段提交是在二阶段提交的基础上的改进版本,主要是加入了超时机制,同时在协调者和参与者中都引用了超时机制。
三阶段是将二阶段里面的准备阶段拆分为两个阶段,插入了一个预提交阶段,这样原先二阶段准备之后,参与者发生崩溃或者错误所引起的问题。
第一阶段:
事务协调者向所有事务参与者发送事务内容,询问是否可以提交事务(canCommit),并等待参与者回复。参与者收到请求之后,检查是否可以提交,如果认为可以执行事务操作,返回Yes,并进入预备状态(参与者不执行事务操作),否则返回No。
第二阶段:
协调者根据第一阶段中参与者的相应情况来决定是否可以进行基于事务的预提交操作:
任何一个参与者反馈abord,即中断事务,协调者向所有参与者发送abord,abord或者超时都会中断事务。
第三阶段:
第二阶段中所有参与者均返回yes,会向所有参与者发送doCommit请求,参与者收到doCommit请求之后会正式执行事务提交,并释放整个事务期间的占用资源,各参与者向协调者返回完成的消息,协调者收到所有参与者的反馈的ack完成的消息后,完成事务的提交。
注意:
在进入第三阶段之后,无论是协调者出现问题,还是协调者与参与者的网络出现问题,都会导致参与者无法收到协调者的请求,此时参与者都会在等待超时之后继续执行事务提交(因为异常的情况毕竟是少数)弱一致性解决方案
弱一致性:复制是异步的,最终一致性是弱一致性的特例,保证用户最终能够读取到某操作对系统特定的数据更新。
弱一致性为了提高系统的吞吐量,允许一定程度上的数据脏读。最终一致性解决方案
强一致性的技术实现成本很高,运行性能低,很难满足真实业务场景下的高并发需求,在实际的生产环境中,通常采用最终一致性的解决方案。
最终一致性:不追求任意时刻系统都能满足数据完整且一致的要求,通过一段业务上可接受时间之后,系统能达到数据完整且一致的目标。消息队列
建立两个消息topic,一个用来处理正常的业务提交,一个用来处理异常冲正消息。请求服务向正常的topic提交业务请求,如果全部正常,则数据一致;如果出现任何异常,向异常的topic提交业务请求,负责回滚。
事务消息方案
核心: 消息队列必须支持半事务消息(如RocketMQ),半事务消息提供类似于X/OPEN XA二阶段提交的分布式事务功能,这样能够确保请求服务执行本地事务和发送消息在一个全局事务中,只有本地事务成功执行之后,消息才会被投递。消息一旦被投递,默认设计业务执行必须成功(通过重试机制)。如果因为某些异常导致业务最终执行失败,系统无法自愈。只能通过告警的方式等待人工干预。
数据订阅方案
核心: 监听数据库的更新日志,并转换成消息流供消费端进行订阅处理。业务订阅是异步的,会存在一定的消息延迟。
TCC事务补偿
TCC是服务化的二阶段编程模型,尝试(try)、确认(confirm)、取消(cancel)均由业务编码实现。
try阶段: 尝试执行业务,完成所有业务的检查,实现一致性;预留必须的业务资源,实现准隔离型。
confirm阶段: 真正执行业务,不做任何检查,仅适用于try阶段预留的业务资源,同时要满足幂等性。
cancel阶段: 取消执行业务,释放try阶段预留的业务资源,操作要满足幂等性。相比于二阶段提交:
TCC位于业务层服务,二阶段提交位于资源层
**性能提升: **业务来实现控制资源锁的粒度大小,不会锁定整个资源
数据最终一致性: 基于confirm和concel的幂等性,保证最终事务完成确认或者取消。
可靠性: 解决了XA协议的协调者单点故障的问题,由主业务方发起并控制整个业务活动,业务管理器也变成多点,引入集群。
缺点:
都要有具体业务实现,业务耦合度较高,提高开发成本。saga事务模式
核心思想: 将长事务拆成多个本地短事务,由saga事务协调器进行协调,如果正常结束,则正常完成;如果某个步骤失败,根据相反顺序依次调用补偿操作,达到事务的最终一致性。
基本协议:- saga事务由许多幂等的有序的小事务组成;
- 每个小事务都有对应的补偿,用于撤销执行后的结果;
和TCC相比,没有预留动作,直接提交。
有些场景不适合补偿,比如发送短信,发了之后如果撤销,就得再给用户发送说明撤销,体验较差。
更多相关内容 -
分布式数据一致性(数据多份副本一致性)
2019-05-22 15:19:34分布式数据库的数据一致性管理是其最重要的内核技术之一,也是保证分布式数据库满足数据库最基本的ACID特性中的 “一致性”(Consistency)的保障。在分布式技术发展下,数据一致性的解决方法和技术也在不断的演进,...前言
分布式数据库的数据一致性管理是其最重要的内核技术之一,也是保证分布式数据库满足数据库最基本的ACID特性中的 “一致性”(Consistency)的保障。在分布式技术发展下,数据一致性的解决方法和技术也在不断的演进,本文就以分布式数据库作为案例,介绍分布式数据库数据一致性的原理以及实际实现。
1、数据一致性
1.1 数据一致性是什么
大部份使用传统关系型数据库的DBA在看到“数据一致性”时,第一反应可能都是数据在跨表事务中的数据一致性场景。但是本文介绍的“数据一致性”,指的是**“数据在多份副本中存储时,如何保障数据的一致性”**场景。
由于在大数据领域,数据的安全不再由硬件来保证,而是通过软件手段,通过同时将数据写入到多个副本中,来确保数据的安全。数据库在同时向多个副本写入记录时,如何确保每个副本数据一致,称为“数据一致性”。
1.2 关系型数据库如何保障数据一致性
传统的关系型数据库对于运行环境–硬件要求都比较高,例如Oracle会建议用户使用小型机+共享存储作为数据库的运行环境,DB2 DPF也同样建议用户采用更好的服务器+高端存储来搭建数据库的运行环境。所以在数据存储安全的技术要求下,传统关系型数据库更多是依赖硬件的技术来保障数据的安全性。
因为关系型数据库的数据安全是基于硬件来保障,并且数据也不会通过同时存储多份来保障数据的安全,所以关系型数据库的用户默认认为数据存储是一致的。1.3 分布式存储如何保障数据一致性
本文在讨论分布式存储时,主要指的是大数据产品中的分布式文件系统和分布式数据库,例如:SequoiaDB和HDFS。
用户在搞明白分布式存储的数据一致性原理时,必须要先明白为什么他们就需要数据一致性,和分布式存储的数据存储与关系型数据库的数据存储又有什么区别。
大数据技术的诞生,确确实实让系统的性能有新的突破,并且支持硬件以水平扩展的方式来获得线性增长的性能和存储。这些都是过去传统关系型数据库所无法提供的。另外,大数据技术也抛弃了运行环境必须足够好的硬性要求,而是允许用户通过批量廉价X86服务器+本地磁盘的方式搭建规模集群,从而获得比过去依赖硬件垂直扩展所提供的更强的计算能力和更多的存储空间。
大数据技术的核心思想就是分布式,将一个大的工作任务分解成多个小任务,然后通过分布式并发操作的方式将其完成,从而提高整个系统的计算效率或者是存储能力。而在分布式环境下,由于硬件的要求降低,必然需要大数据产品提供另外一个重要的功能–数据安全。
大数据产品在解决数据安全的方式上,都比较接近,简单来说,就是让一份数据通过异步或者同步的方式保存在多台机器上,从而保障数据的安全。分布式存储在解决数据安全的技术难点后,又引入了一个新的技术问题,就是如何保障多个副本中的数据一致性。目前SequoiaDB是使用Raft算法来保证数据在多个副本中一致性。
2、Raft算法
2.1 Raft算法背景
在分布式环境下,最著名的一致性算法应该是Paxos算法,但是由于它实在过于晦涩难懂,并且实现起来极度困难,所以在2013年,Diego Ongaro、John Ousterhout两个人以易懂(Understandability)为目标设计了一套一致性算法Raft。Raft算法最大的特点在于简单易懂,并且实现起来简单
2.2 Raft算法概述
与Paxos不同,Raft强调的是易懂,Raft和Paxos一样只要保证n/2+1节点正常就能够提供服务。
众所周知当问题较为复杂时可以把问题分解为几个小问题来处理,Raft也使用了分而治之的思想。Raft算法重点解决三个子问题:选举(Leader election)、日志复制(Log replication)、安全性(Safety)。
Raft算法强化了Leader节点的功能,Follower节点的数据只能够从Leader中获取,所以Follower节点的实现就变得简单,只要负责和Leader保持通信,并且接受Leader推送的数据即可。
2.3 Raft算法原理
2.3.1 节点角色
Raft算法中,对节点的状态分为3种角色,分别是Leader(领导者)、Follower(追随者)和Candidate(候选者)。
Leader,负责处理来自客户端的请求,负责将日志同步到Follower中,并且保证与Follower之间的heartBeat联系;
Follower,当集群刚刚启动时,所有节点均为Follower状态,它的工作主要为响应Leader的日志同步请求,响应Candidate的请求,以及把请求到Follower的事务请求转发给Leader;
Candidate,选举Leader时负责投票,选举出来Leader后,节点将从Candidate状态变为Leader状态。
2.3.2 Terms
在分布式环境下,“时间同步”一直都是老大难的技术难题。Raft为了解决这个问题,将时间划分为一个一个的Term(可以理解为“逻辑时间”)来处理在不同时间段里的数据一致性。
Terms有以下原则
- 每个Term中,至多存在一个Leader
- 某些Term中,有可能存在由于选举失败,没有Leader的情况
- 每个节点自己维护本地的currentTerm
- 每个Term都是一个连续递增的编号
- 如果Follower的Term编号比别的Follower Term编号小时,该Follower Term编号将更新Term编号,以保持与其他Follower Term编号一致
2.3.3 选举
Raft的选举由定时器触发,每个节点的触发时间都不相同。
所有的节点在开始时状态都为Follower,当定时器触发选举后Term编号递增,该节点的状态由Follower转为Candidate,并且向其他节点发起RequestVote RPC请求,这时选举有3种情况可能发生:
发起RequestVote的节点收到n/2+1(过半数)个节点的投票,该节点将从Candidate状态变为Leader状态,开始向其他节点发送HeartBeat以保持Leader的正常状态
如果收到投票请求后,该节点发现发起投票的节点Term大于自己,则该节点状态从Candidate转为Follower,否则保持Candidate状态,并且拒绝该投票请求
选举期间发生了超时,则Term编号递增,重新发起选举
2.3.4 日志复制
日志复制主要的作用就是用来保证节点的数据一致性与高可用性。
当Leader被选举出来后,所有的事务操作都必须要经过Leader处理。这些事务操作成功后,将会被按顺序写入到LOG中,每个LOG都包含一个index编号。
Leader在LOG发生变化后,通过HeartBeat将新的LOG同步到Follower上,Follower在接收到LOG后,再向Leader发送ACK信息,当Leader接到大多数(2/n+1)Follower的ACK信息后,将该LOG设置为已提交,并且Leader将LOG追加到本地磁盘中。
同时Leader将在下一个HeartBeat中,通知所有的Follower将该LOG存储在各自的本地磁盘中。
2.3.5 安全性
安全性是用于确保每个节点都是按照相同的日志序列进行执行的安全机制。
如果当某个Follower在同步Leader的日志时失败,但是未来该Follower又可能被选举为Leader时,就有可能导致前一个Leader已经commit的日志发生覆盖,这样就导致了节点执行不同序列的日志。
Raft的安全性就是用于保证选举出来的Leader一定包含先前已经commit LOG 的机制,主要遵循的原则如下:
每个Term 只能选举一个Leader;
Leader的日志完整性,则当Candidate重新选举Leader时,新的Leader必须要包含先前已经commit的LOG;
Candidate在选举新的Leader时,使用Term来保证LOG的完整性;
3、分布式数据库数据一致性技术实现
以国产原厂的分布式数据库SequoiaDB为例,SequoiaDB在多副本的部署中,采用Raft算法保证数据在多副本环境中保持一致。
SequoiaDB集群中,总共包含3中角色节点,分别是协调节点、编目节点和数据节点。由于协调节点本身不存任何数据,所以只有编目节点和数据节点存在事务操作,换言之,编目分区组和数据分区组的副本同步采用Raft算法保证数据一致性。
3.1 编目节点和数据节点的事务日志介绍
编目节点和数据节点由于都是需要存储数据的,并且在集群部署中该,为了确保数据的安全,都是建议采用分布式的方式进行部署,所以在数据同步中,需要采用Raft算法的基本原理进行数据同步。
编目节点和数据节点在存储数据时,共包含两大部分,一个真实的数据文件,另一个是事务日志文件。
SequoiaDB的节点事务日志,默认情况下由20个64MB(总大小为1.25GB)的文件构成。节点的事务日志主要包含一个index编号和数据操作内容,index编号保持永远递增状态。另外,SequoiaDB节点的事务日志不会永久保存,而是当所有的事务日志写满后,再重新从第一个文件开始进行覆盖写入。
3.2 编目分区组的数据一致性
由于编目分区组是保存SequoiaDB集群的元信息,数据同步要求高,所以编目分区组的数据一致性要求为强一致性,即每次向编目分区组执行事务操作时,必须要确保所有的编目节点操作成功,才计算该操作执行成功,否则该事务操作将在整个编目分区组中回退事务日志,以保证分区组内的数据一致性。
另外,编目分区组还有一个比较重要的特性,即编目分区组必须要存在主节点才能够正常工作,如果老的主节点宕机了,编目分区组暂时没有主节点,则该编目分区组不能够对外提供任何事务操作和数据查询操作。
3.3 数据分区组的数据一致性
数据分区组的数据一致性默认情况下为最终一致性性,即只要求主节点执行事务操作成功即视为操作成功,主节点将在未来异步同步ReplicaLOG到从节点上。
3.4 主从节点的事务日志同步
SequoiaDB的主从节点是通过事务日志同步来保证数据一致性的,并且主从节点的事务日志同步是单线程完成。
如果当主节点和从节点的LSN差距为一条记录,则主节点会主动将最新的事务日志推送给从节点。
如果主节点和从节点的LSN差距超过一条记录,则从节点会主动向主节点请求同步事务日志,主节点收到同步请求后,会将从节点的LSN号到主节点最新的LSN号对应的事务日志打包一次性发送给从节点。
3.5 从节点日志重放
当从节点获取到主节点推送过来的事务日志后,就会自动解析事务日志和重放。从节点在重放事务日志时,默认情况下会以10并发来重放事务日志。
从节点在执行并发重放日志时有条件限制,即在集合的唯一索引个数<=1的情况下,INSERT、DELETE、UPDATE、LOB WRITE、LOBUPDATE、LOB REMOVE操作可以支持并发重放事务日志。从节点在做并发重放时,是通过记录的OID进行打散并发执行,这样就可以保证对相同记录的操作不会由于并发重放导致数据不一致。
但是用户需要注意,从节点在重放事务日志时, DROP CL操作不能够支持并发重放。
4、SequoiaDB数据一致性应用
目前SequoiaDB数据分区组的数据一致性是基于集合级别进行配置的。用户在使用SequoiaDB过程中,可以随时调整数据一致性的强度。
4.1 创建集合时指定
在一个多副本的SequoiaDB集群中,集合默认的数据一致性行级别为“最终一致性”。用户可以在创建集合时显式指定该集合的“数据一致性强度”,例如可以在SequoiaDB Shell中执行以下命令
db.CSNAME.createCL(“CLNAME”,{ReplSize:3})
ReplSize参数填写范围
4.2 修改已经存在的集合
如果集合在创建时没有设置“数据一致性”ReplSize参数,用户也可以对已经存在的集合进行修改,在SequoiaDB Shell修改命令如下
db.CSNAME.CLNAME.alter({ReplSize:3})
ReplSize的取值范围和创建集合时一致。
4.3 如何查看集合的ReplSize参数
如果用户希望检查当前集合的RepliSize参数值,可以通过数据库快照进行查看,在SequoiaDB Shell查看命令如下
db.snapshot(SDB_SNAP_CATALOG,{}, {“Name”:null, “IsMainCL”:null,“MainCLName”:null, “ReplSize”:null})
打印信息如下{
“MainCLName”:“test.main2”,
“Name”: “foo.bar2”,
“IsMainCL”: null,
“ReplSize”: null
}
{
“IsMainCL”: true,
“Name”: “test.main2”,
“MainCLName”: null,
“ReplSize”: null
}
{
“Name”: “foo.tt”,
“ReplSize”: 3,
“IsMainCL”: null,
“MainCLName”: null
}
5、总结
分布式的数据库,通过Raft算法来确保在分布式情况上数据的一致性,并且编目分区组和数据分区组对数据一致性要求又有所不同,编目分区组始终要求的是数据在多副本请情况下数据强一致性,而数据分区组则可以由用户在创建集合时来执行数据一致性的强度,强度越高,数据安全性越好,但是执行的效率就会相对较差,反之依然。
目前SequoiaDB在数据一致性场景上,用户的调整空间较大,可以根据不同的业务要求来调整数据一致性的强度,以满足业务或追求性能最优,或者数据最安全的技术要求。
-
分布式系统的一致性问题(汇总)
2019-09-02 15:32:19保证分布式系统数据一致性的6种方案 问题的起源 在电商等业务中,系统一般由多个独立的服务组成,如何解决分布式调用时候数据的一致性? 具体业务场景如下,比如一个业务操作,如果同时调用服务 A、B、C,需要...微信搜索:“二十同学” 公众号,欢迎关注一条不一样的成长之路
保证分布式系统数据一致性的6种方案
问题的起源
在电商等业务中,系统一般由多个独立的服务组成,如何解决分布式调用时候数据的一致性?
具体业务场景如下,比如一个业务操作,如果同时调用服务 A、B、C,需要满足要么同时成功;要么同时失败。A、B、C 可能是多个不同部门开发、部署在不同服务器上的远程服务。
在分布式系统来说,如果不想牺牲一致性,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 成功。那么最终决定有两种方式,根据具体场景:
-
重试 B,直到 B 成功,事务记录表里记录了各项调用参数等信息;
-
执行 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 事务范围内。
以下是分布式事务框架的流程图
实现-
一个完整的业务活动由一个主业务服务与若干从业务服务组成。
-
主业务服务负责发起并完成整个业务活动。
-
从业务服务提供 TCC 型业务操作。
-
业务活动管理器控制业务活动的一致性,它登记业务活动中的操作,并在活动提交时确认所有的两阶段事务的 confirm 操作,在业务活动取消时调用所有两阶段事务的 cancel 操作。”
与 2PC 协议比较
-
没有单独的 Prepare 阶段,降低协议成本
-
系统故障容忍度高,恢复简单
6. 农信网数据一致性方案
1. 电商业务
公司的支付部门,通过接入其它第三方支付系统来提供支付服务给业务部门,支付服务是一个基于 Dubbo 的 RPC 服务。
对于业务部门来说,电商部门的订单支付,需要调用
-
支付平台的支付接口来处理订单;
-
同时需要调用积分中心的接口,按照业务规则,给用户增加积分。
从业务规则上需要同时保证业务数据的实时性和一致性,也就是支付成功必须加积分。
我们采用的方式是同步调用,首先处理本地事务业务。考虑到积分业务比较单一且业务影响低于支付,由积分平台提供增加与回撤接口。
具体的流程是先调用积分平台增加用户积分,再调用支付平台进行支付处理,如果处理失败,catch 方法调用积分平台的回撤方法,将本次处理的积分订单回撤。
2. 用户信息变更
公司的用户信息,统一由用户中心维护,而用户信息的变更需要同步给各业务子系统,业务子系统再根据变更内容,处理各自业务。用户中心作为 MQ 的 producer,添加通知给 MQ。APP Server 订阅该消息,同步本地数据信息,再处理相关业务比如 APP 退出下线等。
我们采用异步消息通知机制,目前主要使用 ActiveMQ,基于 Virtual Topic 的订阅方式,保证单个业务集群订阅的单次消费。
总结
分布式服务对衍生的配套系统要求比较多,特别是我们基于消息、日志的最终一致性方案,需要考虑消息的积压、消费情况、监控、报警等。
解决分布式系统的一致性问题,我们需要了解哪些理论?
一致性是一个抽象的、具有多重含义的计算机术语,在不同应用场景下,有不同的定义和含义。在传统的IT时代,一致性通常指强一致性,强一致性通常体现在你中有我、我中有你、浑然一体;而在互联网时代,一致性的含义远远超出了它原有的含义,在我们讨论互联网时代的一致性之前,我们先了解一下互联网时代的特点,互联网时代信息量巨大、需要计算能力巨大,不但对用户响应速度要求快,而且吞吐量指标也要向外扩展(既:水平伸缩)。
于是单节点的服务器无法满足需求,服务节点开始池化,想想那个经典的故事,一只筷子一折就断,一把筷子怎么都折不断,可见人多力量大的思想是多么的重要,但是人多也不一定能解决所有事情,还得进行有序、合理的分配任务,进行有效的管理,于是互联网时代谈论最多的话题就是拆分,拆分一般分为“水平拆分”和“垂直拆分”(大家不要对应到数据库或者缓存拆分,这里主要表达一种逻辑)。
这里,“水平拆分”指的是同一个功能由于单机节点无法满足性能需求,需要扩展成为多节点,多个节点具有一致的功能,组成一个服务池,一个节点服务一部分的请求量,团结起来共同处理大规模高并发的请求量。“垂直拆分”指的是按照功能拆分,秉着“专业的人干专业的事儿”的原则,把一个复杂的功能拆分到多个单一的简单的元功能,不同的元功能组合在一起,和未拆分前完成的功能是一致的,由于每个元功能职责单一、功能简单,让维护和变更都变得更简单、安全,更易于产品版本的迭代,在这样的一个互联网的时代和环境,一致性指分布式服务化系统之间的弱一致性,包括应用系统一致性和数据一致性。
无论是水平拆分还是垂直拆分,都解决了特定场景下的特定问题,凡事有好的一面,都会有坏的一面,拆分后的系统或者服务化的系统最大的问题就是一致性问题,这么多个具有元功能的模块,或者同一个功能池中的多个节点之间,如何保证他们的信息是一致的、工作步伐是一致的、状态是一致的、互相协调有序的工作呢?
本文根据作者在互联网企业的实际项目经验,对服务化系统中最难解决的一致性问题进行研究和探讨,试图从实践经验中找到规律,抽象出模式,分享给大家,希望对大家的项目实施有所帮助,在对实践的总结中也会对相关的一致性术语做最朴实的解释,希望能帮助大家彻底理解一致性的本质,并能将其应用到实践,解决读者现实中遇到的服务化系统的一致性问题。
我们遇到的问题
本节列举不一致会导致的种种问题,这也包括一例生活中的问题。
案例1:买房
假如你想要享受生活的随意,只想买个两居,不想让房贷有太大压力,而你媳妇却想要买个三居,还得带花园的,那么你们就不一致了,不一致导致生活不愉快、不协调,严重情况下还会吵架,可见生活中的不一致问题影响很大。
案例2:转账
转账是经典的不一致案例,设想一下银行为你处理一笔转账,扣减你账户上的余额,然后增加别人账户的余额;如果扣减你的账户余额成功,增加别人账户余额失败,那么你就会损失这笔资金。反过来,如果扣减你的账户余额失败,增加别人账户余额成功,那么银行就会损失这笔资金,银行需要赔付。对于资金处理系统来说,上面任何一种场景都是不允许发生的,一旦发生就会有资金损失,后果是不堪设想的,严重情况会让一个公司瞬间倒闭,可参考案例。
案例3:下订单和扣库存
电商系统中也有一个经典的案例,下订单和扣库存如何保持一致,如果先下订单,扣库存失败,那么将会导致超卖;如果下订单没有成功,扣库存成功,那么会导致少卖。两种情况都会导致运营成本的增加,严重情况下需要赔付。
案例4:同步超时
服务化的系统间调用常常因为网络问题导致系统间调用超时,即使是网络很好的机房,在亿次流量的基数下,同步调用超时也是家常便饭。系统A同步调用系统B超时,系统A可以明确得到超时反馈,但是无法确定系统B是否已经完成了预定的功能或者没有完成预定的功能。于是,系统A就迷茫了,不知道应该继续做什么,如何反馈给使用方。(曾经的一个B2B产品的客户要求接口超时重新通知他们,这个在技术上是难以实现的,因为服务器本身可能并不知道自己超时,可能会继续正常的返回数据,只是客户端并没有接受到结果罢了,因此这不是一个合理的解决方案)。
案例5:异步回调超时
此案例和上一个同步超时案例类似,不过这个场景使用了异步回调,系统A同步调用系统B发起指令,系统B采用受理模式,受理后则返回受理成功,然后系统B异步通知系统A。在这个过程中,如果系统A由于某种原因迟迟没有收到回调结果,那么两个系统间的状态就不一致,互相认知不同会导致系统间发生错误,严重情况下会影响核心事务,甚至会导致资金损失。
案例6:掉单
分布式系统中,两个系统协作处理一个流程,分别为对方的上下游,如果一个系统中存在一个请求,通常指订单,另外一个系统不存在,则导致掉单,掉单的后果很严重,有时候也会导致资金损失。
案例7:系统间状态不一致
这个案例与上面掉单案例类似,不同的是两个系统间都存在请求,但是请求的状态不一致。
案例8:缓存和数据库不一致
交易相关系统基本离不开关系型数据库,依赖关系型数据库提供的ACID特性(后面介绍),但是在大规模高并发的互联网系统里,一些特殊的场景对读的性能要求极高,服务于交易的数据库难以抗住大规模的读流量,通常需要在数据库前垫缓存,那么缓存和数据库之间的数据如何保持一致性?是要保持强一致呢还是弱一致性呢?
案例9:本地缓存节点间不一致
一个服务池上的多个节点为了满足较高的性能需求,需要使用本地缓存,使用了本地缓存,每个节点都会有一份缓存数据的拷贝,如果这些数据是静态的、不变的,那永远都不会有问题,但是如果这些数据是半静态的或者常被更新的,当被更新的时候,各个节点更新是有先后顺序的,在更新的瞬间,各个节点的数据是不一致的,如果这些数据是为某一个开关服务的,想象一下重复的请求走进了不同的节点(在failover或者补偿导致的场景下,重复请求是一定会发生的,也是服务化系统必须处理的),一个请求走了开关打开的逻辑,同时另外一个请求走了开关关闭的逻辑,这导致请求被处理两次,最坏的情况下会导致灾难性的后果,就是资金损失。
案例10:缓存数据结构不一致
这个案例会时有发生,某系统需要种某一数据结构的缓存,这一数据结构有多个数据元素组成,其中,某个数据元素都需要从数据库中或者服务中获取,如果一部分数据元素获取失败,由于程序处理不正确,仍然将不完全的数据结构存入缓存,那么缓存的消费者消费的时候很有可能因为没有合理处理异常情况而出错。
模式以及思路
生活中不一致问题的解决
大家回顾一下上一节列举的生活中的案例1-买房,如果置身事外来看,解决这种不一致的办法有两个,一个是避免不一致的发生,如果已经是媳妇了就不好办了:),还有一种方法就是慢慢的补偿,先买个两居,然后慢慢的等资金充裕了再换三居,买比特币赚了再换带花园的房子,于是问题最终被解决了,最终大家处于一致的状态,都开心了。这样可以解决案例1的问题,很自然由于有了过渡的方法,问题在不经意间就消失了,可见“过渡”也是解决一致性问题的一个模式。
从案例1的解决方案来看,我们要解决一致性问题,一个最直接最简单的方法就是保持强一致性,对于案例1的情况,尽量避免在结婚前两个人能够互相了解达成一致,避免不一致问题的发生;不过有些事情事已至此,发生了就是发生了,出现了不一致的问题,我们应该考虑去补偿,尽最大的努力从不一致状态修复到一致状态,避免损失全部或者一部分,也不失为一个好方法。
因此,避免不一致是上策,出现了不一致及时发现及时修复是中策,有问题不积极解决留给他人解决是下策。
酸碱平衡理论
ACID在英文中的意思是“酸”,BASE的意识是“碱”,这一段讲的是“酸碱平衡”的故事。
1. ACID(酸)
如何保证强一致性呢?计算机专业的童鞋在学习关系型数据库的时候都学习了ACID原理,这里对ACID做个简单的介绍。如果想全面的学习ACID原理,请参考ACID。
关系型数据库天生就是解决具有复杂事务场景的问题,关系型数据库完全满足ACID的特性。
ACID指的是:
-
A: Atomicity,原子性
-
C: Consistency,一致性
-
I: Isolation,隔离性
-
D: Durability,持久性
具有ACID的特性的数据库支持强一致性,强一致性代表数据库本身不会出现不一致,每个事务是原子的,或者成功或者失败,事物间是隔离的,互相完全不影响,而且最终状态是持久落盘的,因此,数据库会从一个明确的状态到另外一个明确的状态,中间的临时状态是不会出现的,如果出现也会及时的自动的修复,因此是强一致的。
3个典型的关系型数据库Oracle、Mysql、Db2都能保证强一致性,Oracle和Mysql使用多版本控制协议实现,而DB2使用改进的两阶段提交协议来实现。
如果你在为交易相关系统做技术选型,交易的存储应该只考虑关系型数据库,对于核心系统,如果需要较好的性能,可以考虑使用更强悍的硬件,这种向上扩展(升级硬件)虽然成本较高,但是是最简单粗暴有效的方式,另外,Nosql完全不适合交易场景,Nosql主要用来做数据分析、ETL、报表、数据挖掘、推荐、日志处理等非交易场景。
前面提到的案例2-转账和案例3-下订单和扣库存都可以利用关系型数据库的强一致性解决。
然而,前面提到,互联网项目多数具有大规模高并发的特性,必须应用拆分的理念,对高并发的压力采取“大而化小、小而化了”的方法,否则难以满足动辄亿级流量的需求,即使使用关系型数据库,单机也难以满足存储和TPS上的需求。为了保证案例2-转账可以利用关系型数据库的强一致性,在拆分的时候尽量的把转账相关的账户放入一个数据库分片,对于案例3,尽量的保证把订单和库存放入同一个数据库分片,这样通过关系型数据库自然就解决了不一致的问题。
然而,有些时候事与愿违,由于业务规则的限制,无法将相关的数据分到同一个数据库分片,这个时候我们就需要实现最终一致性。
对于案例2-转账场景,假设账户数量巨大,对账户存储进行了拆分,关系型数据库一共分了8个实例,每个实例8个库,每个库8个表,共512张表,假如要转账的两个账户正好落在了一个库里,那么可以依赖关系型数据库的事务保持强一致性。
如果要转账的两个账户正好落在了不同的库里,转账操作是无法封装在同一个数据库事务中的,这个时候会发生一个库的账户扣减余额成功,另外一个库的账户增加余额失败的情况。
对于这种情况,我们需要继续探讨解决之道,CAP原理和BASE原理,BASE原理通过记录事务的中间的临时状态,实现最终一致性。
2. CAP(帽子理论)
由于对系统或者数据进行了拆分,我们的系统不再是单机系统,而是分布式系统,针对分布式系的帽子理论包含三个元素:
-
C:Consistency,一致性, 数据一致更新,所有数据变动都是同步的
-
A:Availability,可用性, 好的响应性能,完全的可用性指的是在任何故障模型下,服务都会在有限的时间处理响应
-
P:Partition tolerance,分区容错性,可靠性
帽子理论证明,任何分布式系统只可同时满足二点,没法三者兼顾。关系型数据库由于关系型数据库是单节点的,因此,不具有分区容错性,但是具有一致性和可用性,而分布式的服务化系统都需要满足分区容错性,那么我们必须在一致性和可用性中进行权衡,具体表现在服务化系统处理的异常请求在某一个时间段内可能是不完全的,但是经过自动的或者手工的补偿后,达到了最终的一致性。
BASE(碱)
BASE理论解决CAP理论提出了分布式系统的一致性和可用性不能兼得的问题,BASE在英文中有“碱”的意思,对应本节开头的ACID在英文中“酸”的意思,基于这两个名词提出了酸碱平衡的结论,简单来说是在不同的场景下,可以分别利用ACID和BASE来解决分布式服务化系统的一致性问题。
BASE模型与ACID模型截然不同,满足CAP理论,通过牺牲强一致性,获得可用性,一般应用在服务化系统的应用层或者大数据处理系统,通过达到最终一致性来尽量满足业务的绝大部分需求。
BASE模型包含个三个元素:
-
BA:Basically Available,基本可用
-
S:Soft State,软状态,状态可以有一段时间不同步
-
E:Eventually Consistent,最终一致,最终数据是一致的就可以了,而不是时时保持强一致
BASE模型的软状态是实现BASE理论的方法,基本可用和最终一致是目标。按照BASE模型实现的系统,由于不保证强一致性,系统在处理请求的过程中,可以存在短暂的不一致,在短暂的不一致窗口请求处理处在临时状态中,系统在做每步操作的时候,通过记录每一个临时状态,在系统出现故障的时候,可以从这些中间状态继续未完成的请求处理或者退回到原始状态,最后达到一致的状态。
以案例1-转账为例,我们把用户A给用户B转账分成四个阶段,第一个阶段用户A准备转账,第二个阶段从用户A账户扣减余额,第三个阶段对用户B增加余额,第四个阶段完成转账。系统需要记录操作过程中每一步骤的状态,一旦系统出现故障,系统能够自动发现没有完成的任务,然后,根据任务所处的状态,继续执行任务,最终完成任务,达到一致的最终状态。
在实际应用中,上面这个过程通常是通过持久化执行任务的状态和环境信息,一旦出现问题,定时任务会捞取未执行完的任务,继续未执行完的任务,直到执行完成为止,或者取消已经完成的部分操作回到原始状态。这种方法在任务完成每个阶段的时候,都要更新数据库中任务的状态,这在大规模高并发系统中不会有太好的性能,一个更好的办法是用Write-Ahead Log(写前日志),这和数据库的Bin Log(操作日志)相似,在做每一个操作步骤,都先写入日志,如果操作遇到问题而停止的时候,可以读取日志按照步骤进行恢复,并且继续执行未完成的工作,最后达到一致。写前日志可以利用机械硬盘的追加写而达到较好性能,因此,这是一种专业化的实现方式,多数业务系系统还是使用数据库记录的字段来记录任务的执行状态,也就是记录中间的“软状态”,一个任务的状态流转一般可以通过数据库的行级锁来实现,这比使用Write-Ahead Log实现更简单、更快速。
有了BASE理论作为基础,我们对复杂的分布式事务进行拆解,对其中的每一步骤都记录其状态,有问题的时候可以根据记录的状态来继续执行任务,达到最终的一致,通过这个方法我们可以解决案例2-转账和案例3-下订单和扣库存中遇到的问题。
4. 酸碱平衡的总结
-
使用向上扩展(强悍的硬件)运行专业的关系型数据库(例如:Oracle或者DB2)能够保证强一致性,钱能解决的问题就不是问题。
-
如果钱是问题,可以对廉价硬件运行的开源关系型数据库(例如:MySQL)进行分片,将相关的数据分到数据库的同一个片,仍然能够使用关系型数据库保证事务。
-
如果业务规则限制,无法将相关的数据分到同一个片,就需要实现最终一致性,通过记录事务的软状态(中间状态、临时状态),一旦处于不一致,可以通过系统自动化或者人工干预来修复不一致的情况。
分布式一致性协议
国际开放标准组织Open Group定义了DTS(分布式事务处理模型),模型中包含4个角色:应用程序、事务管理器、资源管理器、通信资源管理器四部分。事务处理器是统管全局的管理者,资源处理器和通信资源处理器是事务的参与者。
J2EE规范也包含此分布式事务处理模型的规范,并在所有的AppServer中进行实现,J2EE规范中定义了TX协议和XA协议,TX协议定义应用程序与事务管理器之间的接口,而XA协议定义了事务管理器与资源处理器之间的接口,在过去,大家使用AppServer,例如:Websphere、Weblogic、Jboss等配置数据源的时候会看见类似XADatasource的数据源,这就是实现了DTS的关系型数据库的数据源。企业级开发JEE中,关系型数据库、JMS服务扮演资源管理器的角色,而EJB容器则扮演事务管理器的角色。
下面我们就介绍两阶段提交协议、三阶段提交协议以及阿里巴巴提出的TCC,它们都是根据DTS这一思想演变出来的。
1. 两阶段提交协议
上面描述的JEE的XA协议就是根据两阶段提交来保证事务的完整性,并实现分布式服务化的强一致性。
两阶段提交协议把分布式事务分成两个过程,一个是准备阶段,一个是提交阶段,准备阶段和提交阶段都是由事务管理器发起的,为了接下来讲解方便,我们把事务管理器称为协调者,把资管管理器称为参与者。两阶段如下:
-
准备阶段:协调者向参与者发起指令,参与者评估自己的状态,如果参与者评估指令可以完成,参与者会写redo或者undo日志(这也是前面提起的Write-Ahead Log的一种),然后锁定资源,执行操作,但是并不提交。
-
提交阶段:如果每个参与者明确返回准备成功,也就是预留资源和执行操作成功,协调者向参与者发起提交指令,参与者提交资源变更的事务,释放锁定的资源;如果任何一个参与者明确返回准备失败,也就是预留资源或者执行操作失败,协调者向参与者发起中止指令,参与者取消已经变更的事务,执行undo日志,释放锁定的资源。
两阶段提交协议成功场景示意图如下:
我们看到两阶段提交协议在准备阶段锁定资源,是一个重量级的操作,并能保证强一致性,但是实现起来复杂、成本较高,不够灵活,更重要的是它有如下致命的问题:
-
阻塞:从上面的描述来看,对于任何一次指令必须收到明确的响应,才会继续做下一步,否则处于阻塞状态,占用的资源被一直锁定,不会被释放。
-
单点故障:如果协调者宕机,参与者没有了协调者指挥,会一直阻塞,尽管可以通过选举新的协调者替代原有协调者,但是如果之前协调者在发送一个提交指令后宕机,而提交指令仅仅被一个参与者接受,并且参与者接收后也宕机,新上任的协调者无法处理这种情况。
-
脑裂:协调者发送提交指令,有的参与者接收到执行了事务,有的参与者没有接收到事务,就没有执行事务,多个参与者之间是不一致的。
上面所有的这些问题,都是需要人工干预处理,没有自动化的解决方案,因此两阶段提交协议在正常情况下能保证系统的强一致性,但是在出现异常情况下,当前处理的操作处于错误状态,需要管理员人工干预解决,因此可用性不够好,这也符合CAP协议的一致性和可用性不能兼得的原理。
2. 三阶段提交协议
三阶段提交协议是两阶段提交协议的改进版本。它通过超时机制解决了阻塞的问题,并且把两个阶段增加为三个阶段:
-
询问阶段:协调者询问参与者是否可以完成指令,协调者只需要回答是还是不是,而不需要做真正的操作,这个阶段超时导致中止。
-
准备阶段:如果在询问阶段所有的参与者都返回可以执行操作,协调者向参与者发送预执行请求,然后参与者写redo和undo日志,执行操作,但是不提交操作;如果在询问阶段任何参与者返回不能执行操作的结果,则协调者向参与者发送中止请求,这里的逻辑与两阶段提交协议的的准备阶段是相似的,这个阶段超时导致成功。
-
提交阶段:如果每个参与者在准备阶段返回准备成功,也就是预留资源和执行操作成功,协调者向参与者发起提交指令,参与者提交资源变更的事务,释放锁定的资源;如果任何一个参与者返回准备失败,也就是预留资源或者执行操作失败,协调者向参与者发起中止指令,参与者取消已经变更的事务,执行undo日志,释放锁定的资源,这里的逻辑与两阶段提交协议的提交阶段一致。
三阶段提交协议成功场景示意图如下:
然而,这里与两阶段提交协议有两个主要的不同:
-
增加了一个询问阶段,询问阶段可以确保尽可能早的发现无法执行操作而需要中止的行为,但是它并不能发现所有的这种行为,只会减少这种情况的发生。
-
在准备阶段以后,协调者和参与者执行的任务中都增加了超时,一旦超时,协调者和参与者都继续提交事务,默认为成功,这也是根据概率统计上超时后默认成功的正确性最大。
三阶段提交协议与两阶段提交协议相比,具有如上的优点,但是一旦发生超时,系统仍然会发生不一致,只不过这种情况很少见罢了,好处就是至少不会阻塞和永远锁定资源。
3. TCC
上面两节讲解了两阶段提交协议和三阶段提交协议,实际上他们能解决案例2-转账和案例3-下订单和扣库存中的分布式事务的问题,但是遇到极端情况,系统会发生阻塞或者不一致的问题,需要运营或者技术人工解决。无论两阶段还是三阶段方案中都包含多个参与者、多个阶段实现一个事务,实现复杂,性能也是一个很大的问题,因此,在互联网高并发系统中,鲜有使用两阶段提交和三阶段提交协议的场景。
阿里巴巴提出了新的TCC协议,TCC协议将一个任务拆分成Try、Confirm、Cancel,正常的流程会先执行Try,如果执行没有问题,再执行Confirm,如果执行过程中出了问题,则执行操作的逆操Cancel,从正常的流程上讲,这仍然是一个两阶段的提交协议,但是,在执行出现问题的时候,有一定的自我修复能力,如果任何一个参与者出现了问题,协调者通过执行操作的逆操作来取消之前的操作,达到最终的一致状态。
可以看出,从时序上,如果遇到极端情况下TCC会有很多问题的,例如,如果在Cancel的时候一些参与者收到指令,而一些参与者没有收到指令,整个系统仍然是不一致的,这种复杂的情况,系统首先会通过补偿的方式,尝试自动修复的,如果系统无法修复,必须由人工参与解决。
从TCC的逻辑上看,可以说TCC是简化版的三阶段提交协议,解决了两阶段提交协议的阻塞问题,但是没有解决极端情况下会出现不一致和脑裂的问题。然而,TCC通过自动化补偿手段,会把需要人工处理的不一致情况降到到最少,也是一种非常有用的解决方案,根据线人,阿里在内部的一些中间件上实现了TCC模式。
我们给出一个使用TCC的实际案例,在秒杀的场景,用户发起下单请求,应用层先查询库存,确认商品库存还有余量,则锁定库存,此时订单状态为待支付,然后指引用户去支付,由于某种原因用户支付失败,或者支付超时,系统会自动将锁定的库存解锁供其他用户秒杀。
TCC协议使用场景示意图如下:
总结一下,两阶段提交协议、三阶段提交协议、TCC协议都能保证分布式事务的一致性,他们保证的分布式系统的一致性从强到弱,TCC达到的目标是最终一致性,其中任何一种方法都可以不同程度的解决案例2:转账、案例3:下订单和扣库存的问题,只是实现的一致性的级别不一样而已,对于案例4:同步超时可以通过TCC的理念解决,如果同步调用超时,调用方可以使用fastfail策略,返回调用方的使用方失败的结果,同时调用服务的逆向cancel操作,保证服务的最终一致性。
4. 保证最终一致性的模式
在大规模高并发服务化系统中,一个功能被拆分成多个具有单一功能的元功能,一个流程会有多个系统的多个元功能组合实现,如果使用两阶段提交协议和三阶段提交协议,确实能解决系统间一致性问题,除了这两个协议带来的自身的问题,这些协议的实现比较复杂、成本比较高,最重要的是性能并不好,相比来看,TCC协议更简单、容易实现,但是TCC协议由于每个事务都需要执行Try,再执行Confirm,略微显得臃肿,因此,在现实的系统中,底线要求仅仅需要能达到最终一致性,而不需要实现专业的、复杂的一致性协议,实现最终一致性有一些非常有效的、简单粗暴的模式,下面就介绍这些模式及其应用场景。
1) 查询模式
任何一个服务操作都需要提供一个查询接口,用来向外部输出操作执行的状态。服务操作的使用方可以通过查询接口,得知服务操作执行的状态,然后根据不同状态来做不同的处理操作。
为了能够实现查询,每个服务操作都需要有唯一的流水号标识,也可使用此次服务操作对应的资源ID来标志,例如:请求流水号、订单号等。
首先,单笔查询操作是必须提供的,我们也鼓励使用单笔订单查询,这是因为每次调用需要占用的负载是可控的,批量查询则根据需要来提供,如果使用了批量查询,需要有合理的分页机制,并且必须限制分页的大小,以及对批量查询的QPS需要有容量评估和流控等。
查询模式的示意图如下:
对于案例4:同步超时、案例5:异步回调超时、案例6:掉单、案例7:系统间状态不一致,我们都需要使用查询模式来了解被调用服务的处理情况,来决定下一步做什么:补偿未完成的操作还是回滚已经完成的操作。
2) 补偿模式
有了上面的查询模式,在任何情况下,我们都能得知具体的操作所处的状态,如果整个操作处于不正常的状态,我们需要修正操作中有问题的子操作,这可能需要重新执行未完成的子操作,后者取消已经完成的子操作,通过修复使整个分布式系统达到一致,为了让系统最终一致而做的努力都叫做补偿。
对于服务化系统中同步调用的操作,业务操作发起的主动方在还没有得到业务操作执行方的明确返回或者调用超时,场景可参考案例4:同步超时,这个时候业务发起的主动方需要及时的调用业务执行方获得操作执行的状态,这里使用查询模式,获得业务操作的执行方的状态后,如果业务执行方已经完预设的工作,则业务发起方给业务的使用方返回成功,如果业务操作的执行方的状态为失败或者未知,则会立即告诉业务的使用方失败,然后调用业务操作的逆向操作,保证操作不被执行或者回滚已经执行的操作,让业务的使用方、业务发起的主动方、业务的操作方最终达成一致的状态。
补偿模式的示意图如下:
补偿操作根据发起形式分为:
-
自动恢复:程序根据发生不一致的环境,通过继续未完成的操作,或者回滚已经完成的操作,自动来达到一致。
-
通知运营:如果程序无法自动恢复,并且设计时考虑到了不一致的场景,可以提供运营功能,通过运营手工进行补偿。
-
通知技术:如果很不巧,系统无法自动回复,又没有运营功能,那必须通过技术手段来解决,技术手段包括走数据库变更或者代码变更来解决,这是最糟的一种场景。
3) 异步确保模式
异步确保模式是补偿模式的一个典型案例,经常应用到使用方对响应时间要求并不太高,我们通常把这类操作从主流程中摘除,通过异步的方式进行处理,处理后把结果通过通知系统通知给使用方,这个方案最大的好处能够对高并发流量进行消峰,例如:电商系统中的物流、配送,以及支付系统中的计费、入账等。
实践中,将要执行的异步操作封装后持久入库,然后通过定时捞取未完成的任务进行补偿操作来实现异步确保模式,只要定时系统足够健壮,任何一个任务最终会被成功执行。
异步确保模式的示意图如下:
对于案例5:异步回调超时,使用的就是异步确保模式,这种情况下对于某个操作,如果迟迟没有收到响应,我们通过查询模式和补偿模式来继续未完成的操作。
4. 定期校对模式
既然我们在系统中实现最终一致性,系统在没有达到一致之前,系统间的状态是不一致的,甚至是混乱的,需要补偿操作来达到一致的目的,但是我们如何来发现需要补偿的操作呢?
在操作的主流程中的系统间执行校对操作,我们可以事后异步的批量校对操作的状态,如果发现不一致的操作,则进行补偿,补偿操作与补偿模式中的补偿操作是一致的。
另外,实现定期校对的一个关键就是分布式系统中需要有一个自始至终唯一的ID,ID的生成请参考SnowFlake。
在分布式系统中,全局唯一ID的示意图如下:
一般情况下,生成全局唯一ID有两种方法:
-
持久型:使用数据库表自增字段或者Sequence生成,为了提高效率,每个应用节点可以缓存一批次的ID,如果机器重启可能会损失一部分ID,但是这并不会产生任何问题。
-
时间型:一般由机器号、业务号、时间、单节点内自增ID组成,由于时间一般精确到秒或者毫秒,因此不需要持久就能保证在分布式系统中全局唯一、粗略递增能特点。
实践中,为了能在分布式系统中迅速的定位问题,一般的分布式系统都有技术支持系统,它能够跟踪一个请求的调用链,调用链是在二维的维度跟踪一个调用请求,最后形成一个调用树,原理可参考谷歌的论文Dapper, a Large-Scale Distributed Systems Tracing Infrastructure,一个开源的参考实现为pinpoint。
在分布式系统中,调用链的示意图如下:
全局的唯一流水ID可以把一个请求在分布式系统中的流转的路径聚合,而调用链中的spanid可以把聚合的请求路径通过树形结构进行展示,让技术支持人员轻松的发现系统出现的问题,能够快速定位出现问题的服务节点,提高应急效率。
关于订单跟踪、调用链跟踪、业务链跟踪,我们会在后续文章中详细介绍。
在分布式系统中构建了唯一ID,调用链等基础设施,我们很容易对系统间的不一致进行核对,通常我们需要构建第三方的定期核对系统,以第三方的角度来监控服务执行的健康程度。
定期核对系统示意图如下:
对于案例6:掉单、案例7:系统间状态不一致通常通过定期校对模式发现问题,并通过补偿模式来修复,最后完成系统间的最终一致性。
定期校对模式多应用在金融系统,金融系统由于涉及到资金安全,需要保证百分之百的准确性,所以,需要多重的一致性保证机制,包括:系统间的一致性对账、现金对账、账务对账、手续费对账等等,这些都属于定期校对模式,顺便说一下,金融系统与社交应用在技术上本质的区别在于社交应用在于量大,而金融系统在于数据的准确性。
到现在为止,我们看到通过查询模式、补偿模式、定期核对模式可以解决案例4到案例7的所有问题,对于案例4:同步超时,如果同步超时,我们需要查询状态进行补偿,对于案例5:异步回调超时,如果迟迟没有收到回调响应,我们也会通过查询状态进行补偿,对于案例6:掉单、案例7:系统间状态不一致,我们通过定期核对模式可以保证系统间操作的一致性,避免掉单和状态不一致导致问题。
5. 可靠消息模式
在分布式系统中,对于主流程中优先级比较低的操作,大多采用异步的方式执行,也就是前面提到的异步确保型,为了让异步操作的调用方和被调用方充分的解耦,也由于专业的消息队列本身具有可伸缩、可分片、可持久等功能,我们通常通过消息队列实现异步化,对于消息队列,我们需要建立特殊的设施保证可靠的消息发送以及处理机的幂等等。
消息的可靠发送
消息的可靠发送可以认为是尽最大努力发送消息通知,有两种实现方法:
第一种,发送消息之前,把消息持久到数据库,状态标记为待发送,然后发送消息,如果发送成功,将消息改为发送成功。定时任务定时从数据库捞取一定时间内未发送的消息,将消息发送。
第二种,实现方式与第一种类似,不同的是持久消息的数据库是独立的,并不耦合在业务系统中。发送消息之前,先发送一个预消息给某一个第三方的消息管理器,消息管理器将其持久到数据库,并标记状态为待发送,发送成功后,标记消息为发送成功。定时任务定时从数据库捞取一定时间内未发送的消息,回查业务系统是否要继续发送,根据查询结果来确定消息的状态。
一些公司把消息的可靠发送实现在了中间件里,通过Spring的注入,在消息发送的时候自动持久消息记录,如果有消息记录没有发送成功,定时会补偿发送。
消息处理器的幂等性
如果我们要保证消息可靠的发送,简单来说,要保证消息一定要发送出去,那么就需要有重试机制,有了重试机制,消息一定会重复,那么我们需要对重复做处理。
处理重复的最佳方式为保证操作的幂等性,幂等性的数学公式为:
f(f(x)) = f(x)
保证操作的幂等性常用的几个方法:
-
使用数据库表的唯一键进行滤重,拒绝重复的请求。
-
使用分布式表对请求进行滤重。
-
使用状态流转的方向性来滤重,通常使用行级锁来实现。
-
根据业务的特点,操作本身就是幂等的,例如:删除一个资源、增加一个资源、获得一个资源等。
6. 缓存一致性模型
大规模高并发系统中一个常见的核心需求就是亿级的读需求,显然,关系型数据库并不是解决高并发读需求的最佳方案,互联网的经典做法就是使用缓存抗读需求,下面有一些使用缓存的保证一致性的最佳实践:
-
如果性能要求不是非常的高,尽量使用分布式缓存,而不要使用本地缓存。
-
种缓存的时候一定种完全,如果缓存数据的一部分有效,一部分无效,宁可放弃种缓存,也不要把部分数据种入缓存。
-
数据库与缓存只需要保持弱一致性,而不需要强一致性,读的顺序要先缓存,后数据库,写的顺序要先数据库,后缓存。
这里的最佳实践能够解决案例8:缓存和数据库不一致、案例9:本地缓存节点间不一致、案例10:缓存数据结构不一致的问题,对于数据存储层、缓存与数据库、Nosql等的一致性是更深入的存储一致性技术,将会在后续文章单独介绍,这里的数据一致性主要是处理应用层与缓存、应用层与数据库、一部分的缓存与数据库的一致性。
专题模式
这一节介绍特殊场景下的一致性问题和解决方案。
迁移开关的设计
在大多数企业里,新项目和老项目一般会共存,大家都在努力的下掉老项目,但是由于种种原因总是下不掉,如果要彻底的下掉老项目,就必须要有非常完善的迁移方案,迁移是一项非常复杂而艰巨的任务,我会在将来的文章中详细探讨迁移方案、流程和技术,这里我们只对迁移中使用的开关进行描述。
迁移过程必须使用开关,开关一般都会基于多个维度来设计,例如:全局的、用户的、角色的、商户的、产品的等等,如果迁移过程中遇到问题,我们需要关闭开关,迁移回老的系统,这需要我们的新系统兼容老的数据,老的系统也兼容新的数据,从某种意义上来讲,迁移比实现新系统更加困难。
曾经看过很多简单的开关设计,有的开关设计在应用层次,通过一个curl语句调用,没有权限控制,这样的开关在服务池的每个节点都是不同步的、不一致的;还有的系统把开关配置放在中心化的配置系统、数据库或者缓存等,处理的每个请求都通过统一的开关来判断是否迁移等等,这样的开关有一个致命的缺点,服务请求在处理过程中,开关可能会变化,各个节点之间开关可能不同步、不一致,导致重复的请求可能走到新的逻辑又走了老的逻辑,如果新的逻辑和老的逻辑没有保证幂等性,这个请求就被重复处理了,如果是金融行业的应用,可能会导致资金损失,电商系统可能会导致发货并退款等问题。
这里面我们推荐使用订单开关,不管我们在什么维度上设计了开关,接收到服务请求后,我们在请求创建的关联实体(例如:订单)上标记开关,以后的任何处理流程,包括同步的和异步的处理流程,都通过订单上的开关来判断,而不是通过全局的或者基于配置的开关,这样在订单创建的时候,开关已经确定,不再变更,一旦一份数据不再发生变化,那么它永远是线程安全的,并且不会有不一致的问题。
这个模式在生产中使用比较频繁,建议每个企业都把这个模式作为设计评审的一项,如果不检查这一项,很多开发童鞋都会偷懒,直接在配置中或者数据库中做个开关就上线了。
总结思考
本文从一致性问题的实践出发,从大规模高并发服务化系统的实践经验中进行总结,列举导致不一致的具体问题,围绕着具体问题,总结出解决不一致的方法,并且抽象成模式,供大家在开发服务化系统的过程中参考。
系统分布式情况下最终一致性方案梳理
前言
目前的应用系统,不管是企业级应用还是互联网应用,最终数据的一致性是每个应用系统都要面临的问题,随着分布式的逐渐普及,数据一致性更加艰难,但是也很难有银弹的解决方案,也并不是引入特定的中间件或者特定的开源框架能够解决的,更多的还是看业务场景,根据场景来给出解决方案。根据笔者最近几年的了解,总结了几个点,更多的应用系统在编码的时候,更加关注数据的一致性,这样系统才是健壮的。
基础理论相关
说起事务,目前的几个理论,ACID事务特性,CAP分布式理论,以及BASE等,ACID在数据库事务中体现,CAP和BASE则是分布式事务的理论,结合业务系统,例如订单管理,例如仓储管理等,可以借鉴这些理论,从而解决问题。
-
ACID 特性
- A(原子性)事务的原子操作单元,对数据的修改,要么全部执行,要么全部不执行;
- C(一致性)在事务开始和完成时,数据必须保持一致状态,相关的数据规则必须应用于事务的修改,以保证数据的完整性,事务结束时,所有的内部数据结构必须正确;
- I(隔离性)保证事务不受外部并发操作的独立环境执行;
- D(持久性)事务完成之后,对于数据的修改是永久的,即使系统出现故障也能够保持;
-
CAP
- C(一致性)一致性是指数据的原子性,在经典的数据库中通过事务来保障,事务完成时,无论成功或回滚,数据都会处于一致的状态,在分布式环境下,一致性是指多个节点数据是否一致;
- A(可用性)服务一直保持可用的状态,当用户发出一个请求,服务能在一定的时间内返回结果;
- P(分区容忍性)在分布式应用中,可能因为一些分布式的原因导致系统无法运转,好的分区容忍性,使应用虽然是一个分布式系统,但是好像一个可以正常运转的整体
-
BASE
- BA: Basic Availability 基本业务可用性;
- S: Soft state 柔性状态;
- E: Eventual consistency 最终一致性;
最终一致性的几种做法
单数据库情况下的事务
如果应用系统是单一的数据库,那么这个很好保证,利用数据库的事务特性来满足事务的一致性,这时候的一致性是强一致性的。对于java应用系统来讲,很少直接通过事务的start和commit以及rollback来硬编码,大多通过spring的事务模板或者声明式事务来保证。
基于事务型消息队列的最终一致性
借助消息队列,在处理业务逻辑的地方,发送消息,业务逻辑处理成功后,提交消息,确保消息是发送成功的,之后消息队列投递来进行处理,如果成功,则结束,如果没有成功,则重试,直到成功,不过仅仅适用业务逻辑中,第一阶段成功,第二阶段必须成功的场景。对应上图中的C流程。
基于消息队列+定时补偿机制的最终一致性
前面部分和上面基于事务型消息的队列,不同的是,第二阶段重试的地方,不再是消息中间件自身的重试逻辑了,而是单独的补偿任务机制。其实在大多数的逻辑中,第二阶段失败的概率比较小,所以单独独立补偿任务表出来,可以更加清晰,能够比较明确的直到当前多少任务是失败的。对应上图的E流程。
业务系统业务逻辑的commit/rollback机制
这一点说的话确实不难,commit和rollback是数据库事务中的比较典型的概念,但是在系统分布式情况下,需要业务代码中实现这种,成功了commit,失败了rollback。
业务应用系统的幂等性控制
为啥要做幂等呢? 原因很简单,在系统调用没有达到期望的结果后,会重试。那重试就会面临问题,重试之后不能给业务逻辑带来影响,例如创建订单,第一次调用超时了,但是调用的系统不知道超时了是成功了还是失败了,然后他就重试,但是实际上第一次调用订单创建是成功了的,这时候重试了,显然不能再创建订单了。
- 查询
查询的API,可以说是天然的幂等性,因为你查询一次和查询两次,对于系统来讲,没有任何数据的变更,所以,查询一次和查询多次一样的。
- MVCC方案
多版本并发控制,update with condition,更新带条件,这也是在系统设计的时候,合理的选择乐观锁,通过version或者其他条件,来做乐观锁,这样保证更新及时在并发的情况下,也不会有太大的问题。例如update tablexxx set name=#name#,version=version+1 where version=#version# ,或者是 update tablexxx set quality=quality-#subQuality# where quality-#subQuality# >= 0 。
- 单独的去重表
如果涉及到的去重的地方特别多,例如ERP系统中有各种各样的业务单据,每一种业务单据都需要去重,这时候,可以单独搞一张去重表,在插入数据的时候,插入去重表,利用数据库的唯一索引特性,保证唯一的逻辑。
- 分布式锁
还是拿插入数据的例子,如果是分布是系统,构建唯一索引比较困难,例如唯一性的字段没法确定,这时候可以引入分布式锁,通过第三方的系统,在业务系统插入数据或者更新数据,获取分布式锁,然后做操作,之后释放锁,这样其实是把多线程并发的锁的思路,引入多多个系统,也就是分布式系统中得解决思路。
- 删除数据
删除数据,仅仅第一次删除是真正的操作数据,第二次甚至第三次删除,直接返回成功,这样保证了幂等。
- 插入数据的唯一索引
插入数据的唯一性,可以通过业务主键来进行约束,例如一个特定的业务场景,三个字段肯定确定唯一性,那么,可以在数据库表添加唯一索引来进行标示。
- API层面的幂等
这里有一个场景,API层面的幂等,例如提交数据,如何控制重复提交,这里可以在提交数据的form表单或者客户端软件,增加一个唯一标示,然后服务端,根据这个UUID来进行去重,这样就能比较好的做到API层面的唯一标示。
- 状态机幂等
在设计单据相关的业务,或者是任务相关的业务,肯定会涉及到状态机,就是业务单据上面有个状态,状态在不同的情况下会发生变更,一般情况下存在有限状态机,这时候,如果状态机已经处于下一个状态,这时候来了一个上一个状态的变更,理论上是不能够变更的,这样的话,保证了有限状态机的幂等。
异步回调机制的引入
A应用调用B,在同步调用的返回结果中,B返回成功给到A,一般情况下,这时候就结束了,其实在99.99%的情况是没问题的,但是有时候为了确保100%,记住最起码在系统设计中100%,这时候B系统再回调A一下,告诉A,你调用我的逻辑,确实成功了。其实这个逻辑,非常类似TCP协议中的三次握手。上图中的B流程。
类似double check机制的确认机制
还是上图中异步回调的过程,A在同步调用B,B返回成功了。这次调用结束了,但是A为了确保,在过一段时间,这个时间可以是几秒,也可以是每天定时处理,再调用B一次,查询一下之前的那次调用是否成功。例如A调用B更新订单状态,这时候成功了,延迟几秒后,A查询B,确认一下状态是否是自己刚刚期望的。上图中的D流程。
总结
上面的几点总结,更多的在业务系统中体现,在超复杂的系统中,数据的一致性,不是说简单的引入啥中间件能够解决的,更多的是根据业务场景,来灵活应对。
-
-
一文讲透微服务下如何保证事务的一致性
2020-02-08 17:31:00点击上方“朱小厮的博客”,选择“设为星标”后台回复”加群“获取公众号专属群聊入口随着业务的快速发展、业务复杂度越来越高,传统单体应用逐渐暴露出了一些问题,例如开发效率低、可维护性差、架构...点击上方“朱小厮的博客”,选择“设为星标”
后台回复”加群“获取公众号专属群聊入口
随着业务的快速发展、业务复杂度越来越高,传统单体应用逐渐暴露出了一些问题,例如开发效率低、可维护性差、架构扩展性差、部署不灵活、健壮性差等等。而微服务架构是将单个服务拆分成一系列小服务,且这些小服务都拥有独立的进程,彼此独立,很好地解决了传统单体应用的上述问题,但是在微服务架构下如何保证事务的一致性呢?本文作者将为大家详细解答。
1从本地事务到分布式事务的演变
什么是事务?回答这个问题之前,我们先来看一个经典的场景:支付宝等交易平台的转账。假设小明需要用支付宝给小红转账 100000 元,此时,小明帐号会少 100000 元,而小红帐号会多 100000 元。如果在转账过程中系统崩溃了,小明帐号少 100000 元,而小红帐号金额不变,就会出大问题,因此这个时候我们就需要使用事务了。
这里,体现了事务一个很重要的特性:原子性。事实上,事务有四个基本特性:原子性、一致性、隔离性、持久性。其中,原子性,即事务内的操作要么全部成功,要么全部失败,不会在中间的某个环节结束。一致性,即使数据库在一个事务执行之前和执行之后,数据库都必须处于一致性状态。如果事务执行失败,那么需要自动回滚到原始状态,换句话说,事务一旦提交,其他事务查看到的结果一致,事务一旦回滚,其他事务也只能看到回滚前的状态。隔离性,即在并发环境中,不同的事务同时修改相同的数据时,一个未完成事务不会影响另外一个未完成事务。持久性,即事务一旦提交,其修改的数据将永久保存到数据库中,其改变是永久性的。
本地事务通过 ACID 保证数据的强一致性。ACID 是 Atomic(原子性)、Consistency(一致性)、 Isolation(隔离性)和 Durability(持久性)的缩写 。在实际开发过程中,我们或多或少都有使用到本地事务。例如,MySQL 事务处理使用到 begin 开始一个事务,rollback 事务回滚,commit 事务确认。这里,事务提交后,通过 redo log 记录变更,通过 undo log 在失败时进行回滚,保证事务的原子性。笔者补充下,使用 Java 语言的开发者都接触过 Spring。Spring 使用 @Transactional 注解就可以搞定事务功能。事实上,Spring 封装了这些细节,在生成相关的 Bean 的时候,在需要注入相关的带有 @Transactional 注解的 bean 时候用代理去注入,在代理中为我们开启提交 / 回滚事务。
随着业务的高速发展,面对海量数据,例如,上千万甚至上亿的数据,查询一次所花费的时间会变长,甚至会造成数据库的单点压力。因此,我们就要考虑分库与分表方案了。分库与分表的目的在于,减小数据库的单库单表负担,提高查询性能,缩短查询时间。这里,我们先来看下单库拆分的场景。事实上,分表策略可以归纳为垂直拆分和水平拆分。垂直拆分,把表的字段进行拆分,即一张字段比较多的表拆分为多张表,这样使得行数据变小。一方面,可以减少客户端程序和数据库之间的网络传输的字节数,因为生产环境共享同一个网络带宽,随着并发查询的增多,有可能造成带宽瓶颈从而造成阻塞。另一方面,一个数据块能存放更多的数据,在查询时就会减少 I/O 次数。水平拆分,把表的行进行拆分。因为表的行数超过几百万行时,就会变慢,这时可以把一张的表的数据拆成多张表来存放。水平拆分,有许多策略,例如,取模分表,时间维度分表等。这种场景下,虽然我们根据特定规则分表了,我们仍然可以使用本地事务。但是,库内分表,仅仅是解决了单表数据过大的问题,但并没有把单表的数据分散到不同的物理机上,因此并不能减轻 MySQL 服务器的压力,仍然存在同一个物理机上的资源竞争和瓶颈,包括 CPU、内存、磁盘 IO、网络带宽等。对于分库拆分的场景,它把一张表的数据划分到不同的数据库,多个数据库的表结构一样。此时,如果我们根据一定规则将我们需要使用事务的数据路由到相同的库中,可以通过本地事务保证其强一致性。但是,对于按照业务和功能划分的垂直拆分,它将把业务数据分别放到不同的数据库中。这里,拆分后的系统就会遇到数据的一致性问题,因为我们需要通过事务保证的数据分散在不同的数据库中,而每个数据库只能保证自己的数据可以满足 ACID 保证强一致性,但是在分布式系统中,它们可能部署在不同的服务器上,只能通过网络进行通信,因此无法准确的知道其他数据库中的事务执行情况。
此外,不仅仅在跨库调用存在本地事务无法解决的问题,随着微服务的落地中,每个服务都有自己的数据库,并且数据库是相互独立且透明的。那如果服务 A 需要获取服务 B 的数据,就存在跨服务调用,如果遇到服务宕机,或者网络连接异常、同步调用超时等场景就会导致数据的不一致,这个也是一种分布式场景下需要考虑数据一致性问题。
总结一下,当业务量级扩大之后的分库,以及微服务落地之后的业务服务化,都会产生分布式数据不一致的问题。既然本地事务无法满足需求,因此分布式事务就要登上舞台。什么是分布式事务?我们可以简单地理解,它就是为了保证不同数据库的数据一致性的事务解决方案。这里,我们有必要先来了解下 CAP 原则和 BASE 理论。CAP 原则是 Consistency(一致性)、Availablity(可用性)和 Partition-tolerance(分区容错性)的缩写,它是分布式系统中的平衡理论。在分布式系统中,一致性要求所有节点每次读操作都能保证获取到最新数据;可用性要求无论任何故障产生后都能保证服务仍然可用;分区容错性要求被分区的节点可以正常对外提供服务。事实上,任何系统只可同时满足其中二个,无法三者兼顾。对于分布式系统而言,分区容错性是一个最基本的要求。那么,如果选择了一致性和分区容错性,放弃可用性,那么网络问题会导致系统不可用。如果选择可用性和分区容错性,放弃一致性,不同的节点之间的数据不能及时同步数据而导致数据的不一致。
此时,BASE 理论针对一致性和可用性提出了一个方案,BASE 是 Basically Available(基本可用)、Soft-state(软状态)和 Eventually Consistent(最终一致性)的缩写,它是最终一致性的理论支撑。简单地理解,在分布式系统中,允许损失部分可用性,并且不同节点进行数据同步的过程存在延时,但是在经过一段时间的修复后,最终能够达到数据的最终一致性。BASE 强调的是数据的最终一致性。相比于 ACID 而言,BASE 通过允许损失部分一致性来获得可用性。
现在,业内比较常用的分布式事务解决方案,包括强一致性的两阶段提交协议,三阶段提交协议,以及最终一致性的可靠事件模式、补偿模式,阿里的 TCC 模式。
2强一致性解决方案
二阶段提交协议
在分布式系统中,每个数据库只能保证自己的数据可以满足 ACID 保证强一致性,但是它们可能部署在不同的服务器上,只能通过网络进行通信,因此无法准确的知道其他数据库中的事务执行情况。因此,为了解决多个节点之间的协调问题,就需要引入一个协调者负责控制所有节点的操作结果,要么全部成功,要么全部失败。其中,XA 协议是一个分布式事务协议,它有两个角色:事务管理者和资源管理者。这里,我们可以把事务管理者理解为协调者,而资源管理者理解为参与者。
XA 协议通过二阶段提交协议保证强一致性。
二阶段提交协议,顾名思义,它具有两个阶段:第一阶段准备,第二阶段提交。这里,事务管理者(协调者)主要负责控制所有节点的操作结果,包括准备流程和提交流程。第一阶段,事务管理者(协调者)向资源管理者(参与者)发起准备指令,询问资源管理者(参与者)预提交是否成功。如果资源管理者(参与者)可以完成,就会执行操作,并不提交,最后给出自己响应结果,是预提交成功还是预提交失败。第二阶段,如果全部资源管理者(参与者)都回复预提交成功,资源管理者(参与者)正式提交命令。如果其中有一个资源管理者(参与者)回复预提交失败,则事务管理者(协调者)向所有的资源管理者(参与者)发起回滚命令。举个案例,现在我们有一个事务管理者(协调者),三个资源管理者(参与者),那么这个事务中我们需要保证这三个参与者在事务过程中的数据的强一致性。首先,事务管理者(协调者)发起准备指令预判它们是否已经预提交成功了,如果全部回复预提交成功,那么事务管理者(协调者)正式发起提交命令执行数据的变更。
注意的是,虽然二阶段提交协议为保证强一致性提出了一套解决方案,但是仍然存在一些问题。其一,事务管理者(协调者)主要负责控制所有节点的操作结果,包括准备流程和提交流程,但是整个流程是同步的,所以事务管理者(协调者)必须等待每一个资源管理者(参与者)返回操作结果后才能进行下一步操作。这样就非常容易造成同步阻塞问题。其二,单点故障也是需要认真考虑的问题。事务管理者(协调者)和资源管理者(参与者)都可能出现宕机,如果资源管理者(参与者)出现故障则无法响应而一直等待,事务管理者(协调者)出现故障则事务流程就失去了控制者,换句话说,就是整个流程会一直阻塞,甚至极端的情况下,一部分资源管理者(参与者)数据执行提交,一部分没有执行提交,也会出现数据不一致性。此时,读者会提出疑问:这些问题应该都是小概率情况,一般是不会产生的?是的,但是对于分布式事务场景,我们不仅仅需要考虑正常逻辑流程,还需要关注小概率的异常场景,如果我们对异常场景缺乏处理方案,可能就会出现数据的不一致性,那么后期靠人工干预处理,会是一个成本非常大的任务,此外,对于交易的核心链路也许就不是数据问题,而是更加严重的资损问题。
三阶段提交协议
二阶段提交协议诸多问题,因此三阶段提交协议就要登上舞台了。三阶段提交协议是二阶段提交协议的改良版本,它与二阶段提交协议不同之处在于,引入了超时机制解决同步阻塞问题,此外加入了预备阶段尽可能提早发现无法执行的资源管理者(参与者)并且终止事务,如果全部资源管理者(参与者)都可以完成,才发起第二阶段的准备和第三阶段的提交。否则,其中任何一个资源管理者(参与者)回复执行,或者超时等待,那么就终止事务。总结一下,三阶段提交协议包括:第一阶段预备,第二阶段准备,第二阶段提交。
三阶段提交协议很好的解决了二阶段提交协议带来的问题,是一个非常有参考意义的解决方案。但是,极小概率的场景下可能会出现数据的不一致性。因为三阶段提交协议引入了超时机制,如果出现资源管理者(参与者)超时场景会默认提交成功,但是如果其没有成功执行,或者其他资源管理者(参与者)出现回滚,那么就会出现数据的不一致性。
3最终一致性解决方案
TCC 模式
二阶段提交协议和三阶段提交协议很好的解决了分布式事务的问题,但是在极端情况下仍然存在数据的不一致性,此外它对系统的开销会比较大,引入事务管理者(协调者)后,比较容易出现单点瓶颈,以及在业务规模不断变大的情况下,系统可伸缩性也会存在问题。注意的是,它是同步操作,因此引入事务后,直到全局事务结束才能释放资源,性能可能是一个很大的问题。因此,在高并发场景下很少使用。因此,阿里提出了另外一种解决方案:TCC 模式。注意的是,很多读者把二阶段提交等同于二阶段提交协议,这个是一个误区,事实上,TCC 模式也是一种二阶段提交。
TCC 模式将一个任务拆分三个操作:Try、Confirm、Cancel。假如,我们有一个 func() 方法,那么在 TCC 模式中,它就变成了 tryFunc()、confirmFunc()、cancelFunc() 三个方法。
tryFunc(); confirmFunc(); cancelFunc();
在 TCC 模式中,主业务服务负责发起流程,而从业务服务提供 TCC 模式的 Try、Confirm、Cancel 三个操作。其中,还有一个事务管理器的角色负责控制事务的一致性。例如,我们现在有三个业务服务:交易服务,库存服务,支付服务。用户选商品,下订单,紧接着选择支付方式进行付款,然后这笔请求,交易服务会先调用库存服务扣库存,然后交易服务再调用支付服务进行相关的支付操作,然后支付服务会请求第三方支付平台创建交易并扣款,这里,交易服务就是主业务服务,而库存服务和支付服务是从业务服务。
我们再来梳理下,TCC 模式的流程。第一阶段主业务服务调用全部的从业务服务的 Try 操作,并且事务管理器记录操作日志。第二阶段,当全部从业务服务都成功时,再执行 Confirm 操作,否则会执行 Cancel 逆操作进行回滚。
现在,我们针对 TCC 模式说说大致业务上的实现思路。首先,交易服务(主业务服务)会向事务管理器注册并启动事务。其实,事务管理器是一个概念上的全局事务管理机制,可以是一个内嵌于主业务服务的业务逻辑,或者抽离出的一个 TCC 框架。事实上,它会生成全局事务 ID 用于记录整个事务链路,并且实现了一套嵌套事务的处理逻辑。当主业务服务调用全部的从业务服务的 try 操作,事务管理器利用本地事务记录相关事务日志,这个案例中,它记录了调用库存服务的动作记录,以及调用支付服务的动作记录,并将其状态设置成“预提交”状态。这里,调用从业务服务的 Try 操作就是核心的业务代码。那么, Try 操作怎么和它相对应的 Confirm、Cancel 操作绑定呢?其实,我们可以编写配置文件建立绑定关系,或者通过 Spring 的注解添加 confirm 和 cancel 两个参数也是不错的选择。当全部从业务服务都成功时,由事务管理器通过 TCC 事务上下文切面执行 Confirm 操作,将其状态设置成“成功”状态,否则执行 Cancel 操作将其状态设置成“预提交”状态,然后进行重试。因此,TCC 模式通过补偿的方式保证其最终一致性。
TCC 的实现框架有很多成熟的开源项目,例如 tcc-transaction 框架。(关于 tcc-transaction 框架的细节,可以阅读:https://github.com/changmingxie/tcc-transaction)。
tcc-transaction 框架主要涉及 tcc-transaction-core、tcc-transaction-api、tcc-transaction-spring 三个模块。其中,tcc-transaction-core 是 tcc-transaction 的底层实现,tcc-transaction-api 是 tcc-transaction 使用的 API,tcc-transaction-spring 是 tcc-transaction 的 Spring 支持。tcc-transaction 将每个业务操作抽象成事务参与者,每个事务可以包含多个参与者。参与者需要声明 try / confirm / cancel 三个类型的方法。这里,我们通过 @Compensable 注解标记在 try 方法上,并定义相应的 confirm / cancel 方法。
// try 方法 @Compensable(confirmMethod = "confirmRecord", cancelMethod = "cancelRecord", transactionContextEditor = MethodTransactionContextEditor.class) @Transactional public String record(TransactionContext transactionContext, CapitalTradeOrderDto tradeOrderDto) {} // confirm 方法 @Transactional public void confirmRecord(TransactionContext transactionContext, CapitalTradeOrderDto tradeOrderDto) {} // cancel 方法 @Transactional public void cancelRecord(TransactionContext transactionContext, CapitalTradeOrderDto tradeOrderDto) {}
对于 tcc-transaction 框架的实现,我们来了解一些核心思路。tcc-transaction 框架通过 @Compensable 切面进行拦截,可以透明化对参与者 confirm / cancel 方法调用,从而实现 TCC 模式。这里,tcc-transaction 有两个拦截器。
org.mengyun.tcctransaction.interceptor.CompensableTransactionInterceptor,可补偿事务拦截器。
org.mengyun.tcctransaction.interceptor.ResourceCoordinatorInterceptor,资源协调者拦截器。
这里,需要特别关注 TransactionContext 事务上下文,因为我们需要远程调用服务的参与者时通过参数的形式传递事务给远程参与者。在 tcc-transaction 中,一个事务
org.mengyun.tcctransaction.Transaction
可以有多个参与者org.mengyun.tcctransaction.Participant
参与业务活动。其中,事务编号 TransactionXid 用于唯一标识一个事务,它使用 UUID 算法生成,保证唯一性。当参与者进行远程调用时,远程的分支事务的事务编号等于该参与者的事务编号。通过事务编号的关联 TCC confirm / cancel 方法,使用参与者的事务编号和远程的分支事务进行关联,从而实现事务的提交和回滚。事务状态 TransactionStatus 包含 :尝试中状态 TRYING(1)、确认中状态 CONFIRMING(2)、取消中状态 CANCELLING(3)。此外,事务类型 TransactionType 包含 :根事务 ROOT(1)、分支事务 BRANCH(2)。当调用 TransactionManager#begin() 发起根事务时,类型为 MethodType.ROOT,并且事务 try 方法被调用。调用 TransactionManager#propagationNewBegin() 方法,传播发起分支事务。该方法在调用方法类型为 MethodType.PROVIDER 并且 事务 try 方法被调用。调用 TransactionManager#commit() 方法提交事务。该方法在事务处于 confirm / cancel 方法被调用。类似地,调用 TransactionManager#rollback() 方法,取消事务。此外,对于事务恢复机制,tcc-transaction 框架基于 Quartz 实现调度,按照一定频率对事务进行重试,直到事务完成或超过最大重试次数。如果单个事务超过最大重试次数时,tcc-transaction 框架不再重试,此时需要手工介入解决。
这里,我们要特别注意操作的幂等性。幂等机制的核心是保证资源唯一性,例如重复提交或服务端的多次重试只会产生一份结果。支付场景、退款场景,涉及金钱的交易不能出现多次扣款等问题。事实上,查询接口用于获取资源,因为它只是查询数据而不会影响到资源的变化,因此不管调用多少次接口,资源都不会改变,所以是它是幂等的。而新增接口是非幂等的,因为调用接口多次,它都将会产生资源的变化。因此,我们需要在出现重复提交时进行幂等处理。那么,如何保证幂等机制呢?事实上,我们有很多实现方案。其中,一种方案就是常见的创建唯一索引。在数据库中针对我们需要约束的资源字段创建唯一索引,可以防止插入重复的数据。但是,遇到分库分表的情况是,唯一索引也就不那么好使了,此时,我们可以先查询一次数据库,然后判断是否约束的资源字段存在重复,没有的重复时再进行插入操作。注意的是,为了避免并发场景,我们可以通过锁机制,例如悲观锁与乐观锁保证数据的唯一性。这里,分布式锁是一种经常使用的方案,它通常情况下是一种悲观锁的实现。但是,很多人经常把悲观锁、乐观锁、分布式锁当作幂等机制的解决方案,这个是不正确的。除此之外,我们还可以引入状态机,通过状态机进行状态的约束以及状态跳转,确保同一个业务的流程化执行,从而实现数据幂等。
补偿模式
上节,我们提到了重试机制。事实上,它也是一种最终一致性的解决方案:我们需要通过最大努力不断重试,保证数据库的操作最终一定可以保证数据一致性,如果最终多次重试失败可以根据相关日志并主动通知开发人员进行手工介入。注意的是,被调用方需要保证其幂等性。重试机制可以是同步机制,例如主业务服务调用超时或者非异常的调用失败需要及时重新发起业务调用。重试机制可以大致分为固定次数的重试策略与固定时间的重试策略。除此之外,我们还可以借助消息队列和定时任务机制。消息队列的重试机制,即消息消费失败则进行重新投递,这样就可以避免消息没有被消费而被丢弃,例如 RocketMQ 可以默认允许每条消息最多重试 16 次,每次重试的间隔时间可以进行设置。定时任务的重试机制,我们可以创建一张任务执行表,并增加一个“重试次数”字段。这种设计方案中,我们可以在定时调用时,获取这个任务是否是执行失败的状态并且没有超过重试次数,如果是则进行失败重试。但是,当出现执行失败的状态并且超过重试次数时,就说明这个任务永久失败了,需要开发人员进行手工介入与排查问题。
除了重试机制之外,也可以在每次更新的时候进行修复。例如,对于社交互动的点赞数、收藏数、评论数等计数场景,也许因为网络抖动或者相关服务不可用,导致某段时间内的数据不一致,我们就可以在每次更新的时候进行修复,保证系统经过一段较短的时间的自我恢复和修正,数据最终达到一致。需要注意的是,使用这种解决方案的情况下,如果某条数据出现不一致性,但是又没有再次更新修复,那么其永远都会是异常数据。
定时校对也是一种非常重要的解决手段,它采取周期性的进行校验操作来保证。关于定时任务框架的选型上,业内比较常用的有单机场景下的 Quartz,以及分布式场景下 Elastic-Job、XXL-JOB、SchedulerX 等分布式定时任务中间件。关于定时校对可以分为两种场景,一种是未完成的定时重试,例如我们利用定时任务扫描还未完成的调用任务,并通过补偿机制来修复,实现数据最终达到一致。另一种是定时核对,它需要主业务服务提供相关查询接口给从业务服务核对查询,用于恢复丢失的业务数据。现在,我们来试想一下电商场景的退款业务。在这个退款业务中会存在一个退款基础服务和自动化退款服务。此时,自动化退款服务在退款基础服务的基础上实现退款能力的增强,实现基于多规则的自动化退款,并且通过消息队列接收到退款基础服务推送的退款快照信息。但是,由于退款基础服务发送消息丢失或者消息队列在多次失败重试后的主动丢弃,都很有可能造成数据的不一致性。因此,我们通过定时从退款基础服务查询核对,恢复丢失的业务数据就显得特别重要了。
可靠事件模式
在分布式系统中,消息队列在服务端的架构中的地位非常重要,主要解决异步处理、系统解耦、流量削峰等场景。多个系统之间如果同步通信很容易造成阻塞,同时会将这些系统会耦合在一起。因此,引入了消息队列,一方面解决了同步通信机制造成的阻塞,另一方面通过消息队列进行业务解耦。
可靠事件模式,通过引入可靠的消息队列,只要保证当前的可靠事件投递并且消息队列确保事件传递至少一次,那么订阅这个事件的消费者保证事件能够在自己的业务内被消费即可。这里,请读者思考,是否只要引入了消息队列就可以解决问题了呢?事实上,只是引入消息队列并不能保证其最终的一致性,因为分布式部署环境下都是基于网络进行通信,而网络通信过程中,上下游可能因为各种原因而导致消息丢失。
其一,主业务服务发送消息时可能因为消息队列无法使用而发生失败。对于这种情况,我们可以让主业务服务(生产者)发送消息,再进行业务调用来确保。一般的做法是,主业务服务将要发送的消息持久化到本地数据库,设置标志状态为“待发送”状态,然后把消息发送给消息队列,消息队列收到消息后,也把消息持久化到其存储服务中,但并不是立即向从业务服务(消费者)投递消息,而是先向主业务服务(生产者)返回消息队列的响应结果,然后主业务服务判断响应结果执行之后的业务处理。如果响应失败,则放弃之后的业务处理,设置本地的持久化消息标志状态为“结束”状态。否则,执行后续的业务处理,设置本地的持久化消息标志状态为“已发送”状态。
public void doServer(){ // 发送消息 send(); // 执行业务 exec(); // 更新消息状态 updateMsg(); }
此外,消息队列发生消息后,也可能从业务服务(消费者)宕机而无法消费。绝大多数消息中间件对于这种情况,例如 RabbitMQ、RocketMQ 等引入了 ACK 机制。注意的是,默认的情况下,采用自动应答,这种方式中消息队列会发送消息后立即从消息队列中删除该消息。所以,为了确保消息的可靠投递,我们通过手动 ACK 方式,如果从业务服务(消费者)因宕机等原因没有发送 ACK,消息队列会将消息重新发送,保证消息的可靠性。从业务服务处理完相关业务后通过手动 ACK 通知消息队列,消息队列才从消息队列中删除该持久化消息。那么,消息队列如果一直重试失败而无法投递,就会出现消息主动丢弃的情况,我们需要如何解决呢?聪明的读者可能已经发现,我们在上个步骤中,主业务服务已经将要发送的消息持久化到本地数据库。因此,从业务服务消费成功后,它也会向消息队列发送一个通知消息,此时它是一个消息的生产者。主业务服务(消费者)接收到消息后,最终把本地的持久化消息标志状态为“完成”状态。说到这里,读者应该可以理解到我们使用“正反向消息机制”确保了消息队列可靠事件投递。当然,补偿机制也是必不可少的。定时任务会从数据库扫描在一定时间内未完成的消息并重新投递。
注意的是,因为从业务服务可能收到消息处理超时或者服务宕机,以及网络等原因导致而消息队列收不到消息的处理结果,因此可靠事件投递并且消息队列确保事件传递至少一次。这里,从业务服务(消费者)需要保证幂等性。如果从业务服务(消费者)没有保证接口的幂等性,将会导致重复提交等异常场景。此外,我们也可以独立消息服务,将消息服务独立部署,根据不同的业务场景共用该消息服务,降低重复开发服务的成本。
了解了“可靠事件模式”的方法论后,现在我们来看一个真实的案例来加深理解。首先,当用户发起退款后,自动化退款服务会收到一个退款的事件消息,此时,如果这笔退款符合自动化退款策略的话,自动化退款服务会先写入本地数据库持久化这笔退款快照,紧接着,发送一条执行退款的消息投递到给消息队列,消息队列接受到消息后返回响应成功结果,那么自动化退款服务就可以执行后续的业务逻辑。与此同时,消息队列异步地把消息投递给退款基础服务,然后退款基础服务执行自己业务相关的逻辑,执行失败与否由退款基础服务自我保证,如果执行成功则发送一条执行退款成功消息投递到给消息队列。最后,定时任务会从数据库扫描在一定时间内未完成的消息并重新投递。这里,需要注意的是,自动化退款服务持久化的退款快照可以理解为需要确保投递成功的消息,由“正反向消息机制”和“定时任务”确保其成功投递。此外,真正的退款出账逻辑在退款基础服务来保证,因此它要保证幂等性,及出账逻辑的收敛。当出现执行失败的状态并且超过重试次数时,就说明这个任务永久失败了,需要开发人员进行手工介入与排查问题。
总结一下,引入了消息队列并不能保证可靠事件投递,换句话说,由于网络等各种原因而导致消息丢失不能保证其最终的一致性,因此,我们需要通过“正反向消息机制”确保了消息队列可靠事件投递,并且使用补偿机制尽可能在一定时间内未完成的消息并重新投递。
4开源项目的分布式事务实现解读
开源项目中对分布式事务的应用有很多值得我们学习与借鉴的地方。本节,我们就来对其实现进行解读。
RocketMQ
Apache RocketMQ 是阿里开源的一款高性能、高吞吐量的分布式消息中间件。在历年双 11 中,RocketMQ 都承担了阿里巴巴生产系统全部的消息流转,在核心交易链路有着稳定和出色的表现,是承载交易峰值的核心基础产品之一。RocketMQ 同时存在商用版 MQ 可在阿里云上购买,阿里巴巴对于开源版本和商业版本,主要区别在于:会开源分布式消息所有核心的特性,而在商业层面,尤其是云平台的搭建上面,将运维管控、安全授权、深度培训等纳入商业重中之重。
Apache RocketMQ 4.3 版本正式支持分布式事务消息。RocketMQ 事务消息设计主要解决了生产者端的消息发送与本地事务执行的原子性问题,换句话说,如果本地事务执行不成功,则不会进行 MQ 消息推送。那么,聪明的你可能就会存在疑问:我们可以先执行本地事务,执行成功了再发送 MQ 消息,这样不就可以保证事务性的?但是,请你再认真的思考下,如果 MQ 消息发送不成功怎么办呢?事实上,RocketMQ 对此提供一个很好的思路和解决方案。RocketMQ 首先会发送预执行消息到 MQ,并且在发送预执行消息成功后执行本地事务。紧接着,它根据本地事务执行结果进行后续执行逻辑,如果本地事务执行结果是 commit,那么正式投递 MQ 消息,如果本地事务执行结果是 rollback,则 MQ 删除之前投递的预执行消息,不进行投递下发。注意的是,对于异常情况,例如执行本地事务过程中,服务器宕机或者超时,RocketMQ 将会不停的询问其同组的其他生产者端来获取状态。
ServiceComb
ServiceComb 基于华为内部的 CSE(Cloud Service Engine) 框架开源而来,它提供了一套包含代码框架生成,服务注册发现,负载均衡,服务可靠性(容错熔断,限流降级,调用链追踪)等功能的微服务框架。其中,ServiceComb Saga 是一个微服务应用的数据最终一致性解决方案。
Saga 拆分分布式事务为多个本地事务,然后由 Saga 引擎负责协调。如果整个流程正常结束,那么业务成功完成;如果在这过程中实现出现部分失败,那么 Saga 引擎调用补偿操作。Saga 有两种恢复的策略 :向前恢复和向后恢复。其中,向前恢复对失败的节点采取最大努力不断重试,保证数据库的操作最终一定可以保证数据一致性,如果最终多次重试失败可以根据相关日志并主动通知开发人员进行手工介入。向后恢复对之前所有成功的节点执行回滚的事务操作,这样保证数据达到一致的效果。
Saga 与 TCC 不同之处在于,Saga 比 TCC 少了一个 Try 操作。因此,Saga 会直接提交到数据库,然后出现失败的时候,进行补偿操作。Saga 的设计可能导致在极端场景下的补偿动作比较麻烦,但是对于简单的业务逻辑侵入性更低,更轻量级,并且减少了通信次数。
ServiceComb Saga 在其理论基础上进行了扩展,它包含两个组件:alpha 和 omega。alpha 充当协调者,主要负责对事务的事件进行持久化存储以及协调子事务的状态,使其得以最终与全局事务的状态保持一致。omega 是微服务中内嵌的一个 agent,负责对网络请求进行拦截并向 alpha 上报事务事件,并在异常情况下根据 alpha 下发的指令执行相应的补偿操作。在预处理阶段,alpha 会记录事务开始的事件;在后处理阶段,alpha 会记录事务结束的事件。因此,每个成功的子事务都有一一对应的开始及结束事件。在服务生产方,omega 会拦截请求中事务相关的 id 来提取事务的上下文。在服务消费方,omega 会在请求中注入事务相关的 id 来传递事务的上下文。通过服务提供方和服务消费方的这种协作处理,子事务能连接起来形成一个完整的全局事务。注意的是,Saga 要求相关的子事务提供事务处理方法,并且提供补偿函数。这里,添加 @EnableOmega 的注解来初始化 omega 的配置并与 alpha 建立连接。在全局事务的起点添加 @SagaStart 的注解,在子事务添加 @Compensable 的注解指明其对应的补偿方法。使用案例:https://github.com/apache/servicecomb-saga/tree/master/saga-demo
@EnableOmega public class Application{ public static void main(String[] args) { SpringApplication.run(Application.class, args); } } @SagaStart public void xxx() { } @Compensable public void transfer() { }
现在,我们来看一下它的业务流程图,请参见图 6-17。
想知道更多?扫描下面的二维码关注我
【精彩推荐】
朕已阅
-
ASPICE对追踪性和一致性要求
2019-02-20 21:11:31Automotive SPICE PAM V3.1中对双向追踪性和一致性要求主要在系统系统工程过程组、软件工程过程组、变更管理过程以及相关管理过程中。 双向追踪性和一致性整体要求如下: 系统工程和软件工程过程中,双向追踪性... -
分布式事务数据一致性解决方案
2020-09-03 01:01:532PC 两阶段提交 由一个事务协调器器去通知各事务执行者准备事务、提交或回滚事务,它解决了数据一致性问题,但是通信时间、资源锁定时间太长,系统的可用性受到影响 利用RocketMq的消息事务机制,将生产者本地... -
分布式数据库数据一致性的原理、与技术实现方案
2018-12-27 20:07:16背景 可用性(Availability)和一致性(Consistency)是分布式系统的基本问题,先有著名的CAP理论定义过分布式环境下二者不可兼... 在大数据场景下,分布式数据库的数据一致性管理是其最重要的内核技术之一,也... -
分布式服务化系统一致性(分布式事务、ACID、BASE、CAP)原理与解决方案
2017-04-18 09:41:41在传统的IT时代,一致性通常指强一致性,强一致性通常体现在你中有我、我中有你、浑然一体;而在互联网时代,一致性的含义远远超出了它原有的含义,在我们讨论互联网时代的一致性之前,我们先了解一下互联网时代的... -
移动端UI一致性解决方案
2020-11-26 20:00:21总第424篇2020年 第48篇外卖UI一致性项目是外卖UI设计团队与研发团队共建的项目,目的是改善用户端体验的一致性,提升多技术方案间组件的通用性和复用率,降低整体视觉改版带来的研发成... -
细说分布式下的数据一致性
2018-08-29 11:21:47单一数据库、单一系统无法支撑业务和数据的增长而出现拆分化的演进,数据存储于不同的事物管理单元但又要保证同步操作,这就产生了分布式一致性问题。 目前分布式一致性要求有多数据源一致性和SOA模式一致性,常见... -
分布式强一致性事务
2018-09-24 22:40:59事务是一组操作的执行单元,相对于数据库操作来讲,事务管理的是一组SQL指令,比如增加,修改,删除等,事务的一致性,要求,这个事务内的操作必须全部执行成功,如果在此过程种出现了差错,比如有一条SQL语句没有... -
一致性维度与数据仓库
2018-10-11 15:20:22一致性维度与数据仓库 1、一致性维度概念 维度建模的数据仓库中,有一个概念叫Conformed Dimension,中文一般翻译为“一致性维度”。一致性维度是Kimball的多维体系结构(MD)中的三个关键性概念之一,... -
解决分布式系统一致性的问题
2019-04-26 23:15:26一致性 随着公司的访问量增加,不但要求对用户的响应速度快,还要求吞吐量的指标向外扩展(即水平伸缩)。于是单节点的服务器已经无法满足需求,又随着人员的增多以及项目的多职责,于是我们谈论最多的话题就是拆分。... -
数据库缓存最终一致性的四种方案
2019-10-08 13:51:43而缓存一致性的保证,更是在面试中被反复问到,这里进行一下总结,针对不同的要求,选择恰到好处的一致性方案。 缓存是什么 存储的速度是有区别的。缓存就是把低速存储的结果,临时保存在高速存储的技术。 如图... -
从Paxos到Zookeeper:分布式一致性原理与实践
2019-01-03 09:13:26在上面三个例子中,相信读者一定已经看出来了,我们的终端用户在使用不同的计算机产品时对于数据一致性的需求是不一样的: 有些系统,既要快速地响应用户,同时还要保证系统的数据对于任意客户端都是真实可靠... -
事务四大特征:原子性,一致性,隔离性和持久性(ACID)
2016-10-13 16:57:46Transaction 也就是所谓的事务了,通俗理解就是一件事情。从小,父母就教育我们,做事情要...于是,人们就归纳出事务的第一个特性:原子性(Atomicity)。我靠,一点都不神秘嘛。 特别是在数据库领域,事务是一个 -
分布式服务化系统一致性的“最佳实干”
2017-03-29 09:54:19在传统的IT时代,一致性通常指强一致性,强一致性通常体现在你中有我、我中有你、浑然一体;而在互联网时代,一致性的含义远远超出了它原有的含义,在我们讨论互联网时代的一致性之前,我们先了解一下互联网时代的... -
2019年20个最佳产品信息管理(PIM)软件(一)
2019-07-31 12:31:40凭借其准确性和灵活性,PIM产品信息管理软件成为你可靠的产品内容来源。正如每个营销人员和商家都知道的那样,产品内容可以转化为更好的客户体验。 在本文中,我们将介绍这类软件领域的顶级参与者。 PIM的一些... -
分布式事务解决方案(二)【基于可靠消息的最终一致性】
2018-07-31 22:18:242. 最终一致性(基于可靠消息) 2.1 消息发送的一致性 指产生消息的业务动作与消息发送的一致。(也就是说,如果业务操作成功,那么由这个业务操作所产生的消息一定要成功投递出去,否则就丢消息) 2.1.1 ... -
关于分布式系统的数据一致性问题
2016-10-11 08:30:43现在的问题是,假设成都上海各自的数据中心有记录变更,需要先同步到主数据中心,主数据中心更新完成之后,在把最新的数据分发到上海,成都的地方数据中心A,地方数据中心更新数据,保持和主数据中心一致性(数据库... -
分布式理论CAP - C(一致性)A(可用性)P(分区容忍性)不可兼得
2019-04-26 16:46:00一致性(C):在分布式系统中的所有数据备份,在同一时刻是否同样的值。(等同于所有节点访问同一份最新的数据副本) 可用性(A):在集群中一部分节点故障后,集群整体是否还能响应客户端的读写请求。(对数据更新... -
如何高效完成产品生命周期管理
2018-08-15 20:41:54郭振宇,十年研发及技术管理经验,通过了CMMI5认证和PMP认证,擅长大数据集成、大数据治理、大数据安全、数据资产管理等领域的产品规划及技术研发。对技术团队管理和研发流程管理也有一定的研究及实践。 产品... -
CAP, BASE, 最终一致性和五分钟原则
2017-02-16 16:30:34CAP,BASE和最终一致性是NoSQL数据库存在的三大基石。而五分钟法则是内存数据存储的理论依据。这个是一切的源头。 CAP C: Consistency 一致性A: Availability 可用性(指的是快速获取数据)... -
Sql Server实用操作-数据库一致性检测工具(DBCC)
2017-08-23 09:57:16在危急时刻,数据库一致性检测(DBCC)可能是你最重要的工具。本文向你简单介绍DBCC的功能,它们包括: 检测表和相关目录的完整性。 检测整个数据库。 检测数据库页的完整性。 重建任何指定表中的目录。 ... -
分布式事务 - XA/TCC/最终一致性解决方案/Seata (原FESCAR)
2019-01-25 16:39:38(Consistency) : 在执行完该流程的操作后,所有的更新操作是否能保证整个系统的数据都被更新,保证数据的一致性 可用性 (Availability) : 每个操作能够在正常的响应时间内完成,并给客户端返回正常的数据 ... -
产品生命周期管理(PLM)
2020-11-18 23:03:10产品生命周期管理(PLM) PLM软件的集成应用是一项复杂的系统工程,涉及到软件开发的成熟度、企业的实际需求。制造企业是目前PLM应用的主体,分析制造企业的信息化现状,总的来说存在以下主要问题: 资讯分类 PLM... -
产品数据管理(PDM)技术概述
2018-11-02 15:47:441产品数据管理系统(PDM)发展及现状 1.1PDM技术的基本概念及产生的背景 产品数据管理(PDM)是以软件技术为基础,以产品为核心,实现对产品相关的数据、过程、资源一体化集成管理的技术。PDM明确定位为面向... -
分布式系统事务一致性到CAP,BASE理论
2017-02-08 10:44:00对于上面三个例子,相信大家一定看出来了,我们的终端用户在使用不同的计算机产品时对于数据一致性的需求是不一样的: 1、有些系统,既要快速地响应用户,同时还要保证系统的数据对于任意客户端都是真实可靠的,... -
分布式事务及分布式系统一致性解决方案
2016-07-02 10:46:24在分布式系统中,同时满足“一致性”、“可用性”和“分区容错性”三者是不可能的。分布式系统的事务一致性是一个技术难题,各种解决方案孰优孰劣? 在OLTP系统领域,我们在很多业务场景下都会面临事务一致性方面的...