上一篇文章( 亿级流量架构之分布式事务思路及方法)中梳理事务到分布式事务的演变过程, 以及分布式事务的处理思路,这篇文章主要从应用的角度对比目前较为流行的一些分布式事务方案,以及一些商业应用。
想让数据具有高可用性,就得写多份数据,写多份数据就会有数据一致性问题,数据已执行问题又会引发性能问题,所以如何权衡,是一件仁者见仁、智者见者的问题,目前的数据一致性,即分布式事务,大概有如下几种解决方案:
- Master-Slave 方案。
- Master-Master 方案。
- 两阶段和三阶段提交方案。
- Paxos 方案。
第3点在上篇文章中已经讲过, 1、2这儿会简单梳理,重点是第四个方案, 目前很多公司的事务处理方式,例如阿里巴巴的TCC(Try–Confirm–Cancel),亚马逊的PRC(Plan–Reserve–Confirm)都是两阶段提交的变种, 凡是通过业务补偿,或者是在业务层面上做的分布式事务,基本都是两阶段提交的玩法,但是这否是应用层事务处理方式,而在数据层解决事务问题,Paxos是不二之选。
Master-Slave 方案
这个也叫主从模式,Slave一般是Master的备份。在这样的系统中,一般是如下设计的:
1)读写请求都由Master负责。
2)写请求写到Master上后,由Master同步到Slave上。
从Master同步到Slave上,你可以使用异步,也可以使用同步,可以使用Master来push,也可以使用Slave来pull。 通常来说是Slave来周期性的pull,所以,是最终一致性。
这个设计的问题是,如果Master在pull周期内垮掉了,那么会导致这个时间片内的数据丢失。如果你不想让数据丢掉,Slave只能成为Read-Only的方式等Master恢复。
如果可以容忍数据丢掉的话,你可以马上让Slave代替Master工作(对于只负责计算的结点来说,没有数据一致性和数据丢失的问题,Master-Slave的方式就可以解决单点问题了) ,Master Slave也可以是强一致性的, 比如:当我们写Master的时候,Master负责先写自己,等成功后,再写Slave,两者都成功后返回成功,整个过程是同步的,如果写Slave失败了,那么两种方法,一种是标记Slave不可用报错并继续服务(等Slave恢复后同步Master的数据,可以有多个Slave,这样少一个,还有备份,也就是多个Slave),另一种是回滚自己并返回写失败。
注:一般不先写Slave,因为如果写Master自己失败后,还要回滚Slave,此时如果回滚Slave失败,就得手工订正数据
Master-Master 方案
Master-Master,主主模式,又叫Multi-master,是指一个系统存在两个或多个Master,每个Master都提供read-write服务。这个模型是Master-Slave的加强版,数据间同步一般是通过Master间的异步完成,所以是最终一致性。 Master-Master的好处是,一台Master挂了,别的Master可以正常做读写服务,他和Master-Slave一样,当数据没有被复制到别的Master上时,数据会丢失。很多数据库都支持Master-Master的Replication的机制。
这种模式的问题在于: 如果多个Master对同一个数据进行修改的时候,这个模型的恶梦就出现了——对数据间的冲突合并,这并不是一件容易的事情。为了解决这问题, Dynamo提出了一种解决办法, 记录数据的版本号和修改者, 这也就意味着数据冲突这个事是交给用户自己搞的。
两阶段和三阶段提交方案
这个是业务层分布式事务处理的核心, ,在上篇文章( 亿级流量架构之分布式事务思路及方法)中”二三阶段提交协议”介绍得比较详细了,这儿不多说。需要注意是这是重点,不太了解的朋友,为了更好的理解后面的方案, 建议看看相关部分。
Paxos 方案
理解Paxos算法之前,先讲一个情景——两将军问题,来理解这个算法是解决了什么问题。
两将军问题
有两支军队,它们分别有一位将军领导,现在准备攻击一座修筑了防御工事的城市。这两支军队都驻扎在那座城市的附近,分占一座山头。一道山谷把两座山分隔开来,并且两位将军唯一的通信方式就是派各自的信使来往于山谷两边。不幸的是,这个山谷已经被那座城市的保卫者占领,并且存在一种可能,那就是任何被派出的信使通过山谷是会被捕。 请注意,虽然两位将军已经就攻击那座城市达成共识,但在他们各自占领山头阵地之前,并没有就进攻时间达成共识。两位将军必须让自己的军队同时进攻城市才能取得成功。因此,他们必须互相沟通,以确定一个时间来攻击,并同意就在那时攻击。如果只有一个将军进行攻击,那么这将是一个灾难性的失败。 这个思维实验就包括考虑他们如何去做这件事情。下面是我们的思考:
1)第一位将军先发送一段消息“让我们在上午9点开始进攻”。然而,一旦信使被派遣,他是否通过了山谷,第一位将军就不得而知了。任何一点的不确定性都会使得第一位将军攻击犹豫,因为如果第二位将军不能在同一时刻发动攻击,那座城市的驻军就会击退他的军队的进攻,导致他的军对被摧毁。
2)知道了这一点,第二位将军就需要发送一个确认回条:“我收到您的邮件,并会在9点的攻击。”但是,如果带着确认消息的信使被抓怎么办?所以第二位将军会犹豫自己的确认消息是否能到达。
3)于是,似乎我们还要让第一位将军再发送一条确认消息——“我收到了你的确认”。然而,如果这位信使被抓怎么办呢?
4)这样一来,是不是我们还要第二位将军发送一个“确认收到你的确认”的信息。
靠,于是你会发现,这事情很快就发展成为不管发送多少个确认消息,都没有办法来保证两位将军有足够的自信自己的信使没有被敌军捕获。
Paxos 算法
Paxos 算法解决的问题是在一个可能发生上述异常的分布式系统中如何就某个值达成一致,保证不论发生以上任何异常,都不会破坏决议的一致性。一个典型的场景是,在一个分布式数据库系统中,如果各节点的初始状态一致,每个节点都执行相同的操作序列,那么他们最后能得到一个一致的状态。为保证每个节点执行相同的命令序列,需要在每一条指令上执行一个「一致性算法」以保证每个节点看到的指令一致。一个通用的一致性算法可以应用在许多场景中,是分布式计算中的重要问题。从20世纪80年代起对于一致性算法的研究就没有停止过。
这个算法详细解释可以参考维基百科以及raft算法(Paxos改进)作者的视频(B站,YouTube),这儿我用自己的话叙述:
这个算法将节点分了很多类,官方定义为:Client、Propose、 Acceptor、 Learner,含义不重要,先来理解他们的作用, 看我的文字就行了。
在我们读书的时候,班级里有组长,有班委成员,有班务记录员,下面一一对应起来:
加入我们班决定这周没去游玩,现在班会讨论去玩什么项目:
Client : 代表普通同学,可以提出方案,需要将方案交给组长(Propose)
Propose : 组长,一个班肯定有很多组长,主要任务是将组员的方案提出来给班委投票确认, 同时统计班委们返回的投票结果,并将结果告诉所有人。
Acceptor:班委,这儿的班委具有投票权,但是班委不关心投票的内容,哈哈哈哈,是不是有点奇怪, 先记住,后面就理解了,班委只关心方案是不是已经被提过,只要这个方案之前没有被提过就会同意,将同意的信息返回给提案的组长。
当组长收到各个班委回复的信息之后,会统计同意的人数,如果人数过半(\(\frac{N}{2}+1\)),那么就会广播这个提案已经被认可,这时候Learner会在班务本子上记录下这个提案。
如上图,有一个组员(Client), 一个组长(Proposer), 三个班委(Acceptor), 两个Learner,当组员提出方案时,组长会将方案提交给三位班委,班委会看看这个方案之前是不是已经提过(主要是根据方案编号,也就是方案编号一致变化,默认是递增),没提过的话会通过这个提案,然后组长统计通过的比例,过半数之后会将方案通过的编号进行广播,班委会回馈信息,此时Learner会记录下来同时回馈。
刚刚留下了一个疑惑,为什么开始阶段(Prepare(1))时班委不关心内容呢?
在这几个角色中,Acceptor(班委)是数据库(其实也不是数据库,仅仅是为了方便理解),Learner也是数据库(备份),当你准备提交一条消息时,第一步仅仅是看能不能与之建立正常的连接,其次看看这个数据之前是不是已经提交过,如果大部分都可以建立正常的连接并且没有被提交过,那么说明我们的数据就可以提交了。
Paxos活锁
前面说过,三个班委(Acceptor)只要接受到的提案是未提交的且过半的话,就会通过,如果一个提案1的组长Proposer正在投票信息准备通知时,另一个组长Proposer又提交了提案2,那么班委就会开始讨论提案2, 放弃提案1的讨论,此时提案1被丢弃,那组长1会将提案重新提交,这导致了死锁的诞生,为了解决这个问题,可以让提交方案的组长随机睡眠一段时间。
Paxos改进版
前面例子可以看出是一个两阶段提交的过程, 改进版最主要的点在于,在Acceptor中选出一个主节点,要提交议案直接交给主节点,由主节点将这些消息同步给其他节点,如果此时过半数,那么久将数据提交,然后将信息返回给提议员(组长),什么意思呢,就是组长提交方案之后,交给班委的头目,班委头目统计好投票结果,如果通过了直接通知所有班委以及记录员,同时将信息返回给提议员(也就是组长)。
主节点的选举
对于三个班委(Acceptor)而言,有一个时间周期,如果这个周期内收到主节点的心跳包,那么就会相安无事,如果周期内没收到心跳包,那么就会向其他节点发出请求包,这个包主要是自己要当主节点,请大家投票,所有接受到这个请求包的节点,回复同意,当一个节点收到的同意信息过半之后,就会成为主节点,同时广播这个信息,收到信息的节点就成为了从节点。
所有有关操作都会通过这个主节点,主节点再在其余的节点之间进行投票,通过之后主节点直接提交事务然后将信息返回给调用者。
Paxos与数据提交
简单说来,Paxos的目的是让整个集群的结点对某个值的变更达成一致。Paxos算法基本上来说是个民主选举的算法——大多数的决定会成个整个集群的统一决定。任何一个点都可以提出要修改某个数据的提案,是否通过这个提案取决于这个集群中是否有超过半数的结点同意(所以Paxos算法需要集群中的结点是单数)。
这个算法有两个阶段(假设这个有三个结点:A,B,C):
第一阶段:Prepare阶段
A把申请修改的请求Prepare Request发给所有的结点A,B,C。注意,Paxos算法会有一个Sequence Number(\可以认为是一个提案号,这个数不断递增,而且是唯一的,也就是说A和B不可能有相同的提案号),这个提案号会和修改请求一同发出,任何结点在“Prepare阶段”时都会拒绝其值小于当前提案号的请求。所以,结点A在向所有结点申请修改请求的时候,需要带一个提案号,越新的提案,这个提案号就越是是最大的。
如果接收结点收到的提案号n大于其它结点发过来的提案号,这个结点会回应Yes(本结点上最新的被批准提案号),并保证不接收其它<n的提案。这样一来,结点上在Prepare阶段里总是会对最新的提案做承诺。
优化:在上述 prepare 过程中,如果任何一个结点发现存在一个更高编号的提案,则需要通知 提案人,提醒其中断这次提案。
第二阶段:Accept阶段
如果提案者A收到了超过半数的结点返回的Yes,然后他就会向所有的结点发布Accept Request(同样,需要带上提案号n),如果没有超过半数的话,那就返回失败。
当结点们收到了Accept Request后,如果对于接收的结点来说,n是最大的了,那么,它就会修改这个值,如果发现自己有一个更大的提案号,那么,结点就会拒绝修改。
我们可以看以,这似乎就是一个“两段提交”的优化。其实,2PC/3PC都是分布式一致性算法的残次版本,Google Chubby的作者Mike Burrows说过这个世界上只有一种一致性算法,那就是Paxos,其它的算法都是残次品。
我们还可以看到:对于同一个值的在不同结点的修改提案就算是在接收方被乱序收到也是没有问题的。
商业产品
这儿主要了解GTS、LCN、TXC,另外还有上一篇文章讲过的TCC,这儿就不继续重复了。
GTS
GTS是目前业界第一款,也是唯一的一款通用的解决微服务分布式事务问题的中间件,而且可以保证数据的强一致性。本文将对GTS简单概述,详情可以参考阿里巴巴官方文档介绍。
GTS是一款分布式事务中间件,由阿里巴巴中间件部门研发,可以为微服务架构中的分布式事务提供一站式解决方案。GTS方案的基本思路是:将分布式事务与具体业务分离,在平台层面开发通用的事务中间件GTS,由事务中间件协调各服务的调用一致性,负责分布式事务的生命周期管理、服务调用失败的自动回滚。
GTS方案有三方面的优势:
第一、它将微服务从分布式事务中解放出来,微服务的实现不需要再考虑反向接口、幂等、回滚策略等复杂问题,只需要业务自己的接口即可,大大降低了微服务开发的难度与工作量。将分布式事务从所谓的“贵族技术”变为大家都能使用的“平民技术 ”,有利于微服务的落地与推广。
第二、GTS对业务代码几乎没有侵入,只需要通过注解@TxcTransaction界定事务边界即可,微服务接入GTS的成本非常低。
第三、性能方面GTS也非常优秀,是传统XA方案的8~10倍。
GTS原理
GTS中间件主要包括客户端(GTS Client)、资源管理器(GTS RM)和事务协调器(GTS Server)三部分。GTS Client主要完成事务的发起与结束。GTS RM完成分支事务的开启、提交、回滚等操作。GTS Server主要负责分布式事务的整体推进,事务生命周期的管理。
GTS和微服务集成后的结构图如上图所示。GTS Client需要和业务应用集成部署,RM与微服务集成部署。当业务应用发起服务调用时,首先会通过GTS Client向TC注册新的全局事务。之后GTS Server会给业务应用返回全局唯一的事务编号xid。业务应用调用服务时会将xid传播到服务端。微服务在执行数据库操作时会通过GTS RM向GTS Server注册分支事务,并完成分支事务的提交。如果A、B、C三个服务均调用成功,GTS Client会通知GTS Server结束事务。假设C调用失败,GTS Client会要求GTS Server发起全局回滚。然后由各自的RM完成回滚工作。
LCN
TX-LCN定位是于一款事务协调性框架,框架本事并不操作事务,而是基于对事务的协调从而达到事务一致性的效果。TX-LCN由两大模块组成, TxClient、TxManager,TxClient作为模块的依赖框架,提供TX-LCN的标准支持,TxManager作为分布式事务的控制放。事务发起方或者参与反都由TxClient端来控制。
下图来自LCN官网,与LCN有关详情可以访问官方仓库 。
核心的步骤
- 创建事务组,是指在事务发起方开始执行业务代码之前先调用TxManager创建事务组对象,然后拿到事务标示GroupId的过程。
- 加入事务组,添加事务组是指参与方在执行完业务方法以后,将该模块的事务信息通知给TxManager的操作。
- 通知事务组,是指在发起方执行完业务代码以后,将发起方执行结果状态通知给TxManager,TxManager将根据事务最终状态和事务组的信息来通知相应的参与模块提交或回滚事务,并返回结果给事务发起方。
LCN事务模式
LCN主要有三种事务模式,分别是LCN模式、TCC模式、TXC模式。
LCN模式
原理:
LCN模式是通过代理Connection的方式实现对本地事务的操作,然后在由TxManager统一协调控制事务。当本地事务提交回滚或者关闭连接时将会执行假操作,该代理的连接将由LCN连接池管理。
特点:
- 该模式对代码的嵌入性为低。
- 该模式仅限于本地存在连接对象且可通过连接对象控制事务的模块。
- 该模式下的事务提交与回滚是由本地事务方控制,对于数据一致性上有较高的保障。
- 该模式缺陷在于代理的连接需要随事务发起方一共释放连接,增加了连接占用的时间。
TCC模式
原理:
TCC事务机制相对于传统事务机制(X/Open XA Two-Phase-Commit),其特征在于它不依赖资源管理器(RM)对XA的支持,而是通过对(由业务系统提供的)业务逻辑的调度来实现分布式事务。主要由三步操作,Try: 尝试执行业务、 Confirm:确认执行业务、 Cancel: 取消执行业务。
特点:
- 该模式对代码的嵌入性高,要求每个业务需要写三种步骤的操作。
- 该模式对有无本地事务控制都可以支持使用面广。
- 数据一致性控制几乎完全由开发者控制,对业务开发难度要求高。
TXC模式
原理:
TXC模式命名来源于淘宝,实现原理是在执行SQL之前,先查询SQL的影响数据,然后保存执行的SQL快走信息和创建锁。当需要回滚的时候就采用这些记录数据回滚数据库,目前锁实现依赖redis分布式锁控制。
特点:
- 该模式同样对代码的嵌入性低。
- 该模式仅限于对支持SQL方式的模块支持。
- 该模式由于每次执行SQL之前需要先查询影响数据,因此相比LCN模式消耗资源与时间要多。
- 该模式不会占用数据库的连接资源。
TXC
TXC(Taobao Transaction Constructor)是阿里巴巴的一个分布式事务中间件,它可以通过极少的代码侵入,实现分布式事务。
在大部分情况下,应用只需要引入TXC Client的jar包,进行几项简单配置,以及以行计的代码改造,即可轻松保证分布式数据一致性。TXC同时提供了丰富的编程和配置策略,以适应各种长尾的应用需求。
TXC标准模式(AT模式)
TXC标准模式(Automatic TXC)是最主要的事务模式,通过基于TDDL的TXC数据源,它对SQL语句提供了分布式事务支持。它帮助应用方以最小的改造代价来实现TDDL下的事务的功能。
在标准模式下,当开启了TXC分布式事务时,TXC框架将自动的根据执行的SQL语句,进行事务分支划分(每个物理库上的一个本地事务作为一个分布式事务分支),把各个分支统一纳入事务。
分布式事务的隔离级别可以配置,读未提交(read uncommitted)和读已提交(read committed)。读未提交是缺省设置。
标准模式适合于TDDL分库分表、多TDDL数据源、跨进程的多TDDL数据源等几乎任何TDDL应用场景下的分布式事务。
TXC自定义模式(MT模式)
MT(Manual TXC)模式,提供用户可以介入两阶段提交过程的一种模式。在这种模式下,用户可以根据自身业务需求自定义在TXC的两阶段中每个阶段的具体行为。MT模式提供了更多的灵活性,可能性,以达到特殊场景下的自定义优化,及特殊功能的实现。
MT模式不依赖于TDDL,这是它相对于标准模式的一个最大的优势。MT模式几乎满足任何我们想到的事务场景。
TXC重试模式
RT(Retry TXC)模式严格地说,不属于传统的事务范围。在TXC将其定义为一种特殊的事务,它通过在用户指定时间内不停的异步重试失败的SQL语句,来保证SQL语句最终成功。
RT模式也是基于TXC数据源的。
当我们通过TDDL执行一个需要分库的SQL,假设在第一个库执行成功了,但是在第二个库执行失败了。如果采用TXC标准模式,第一个库的SQL会回滚。对用户来说,他的SQL失败了,在两个库上是一致的。
如果采用RT模式,第二个库执行失败的SQL会保存下来,TXC不断重试这个SQL,直到成功。对用户来说, 他的SQL成功了,在两个库上最终是一致的。当然,TXC不会一直重试SQL,用户可以指定一个超时时间,超过这个时间限制,TXC会发送告警信息到用户。用户拿到告警信息后,可以从业务库的RT SQL表中拿到对应的SQL语句,决定下一步怎么处理。
试想一下,当一个SQL涉及到分库,我们执行这个SQL失败了,通常来说,我们需要通过log查出它在哪几个库成功了哪几个库失败了,并且在失败的库上不断重试。这是很繁琐的。RT模式把用户从这种繁琐工作中解脱出来,用户不再需要关注哪些库上SQL失败了,也不需要自己重试SQL。