分布式事务篇:基础知识筑基
前言什么是分布式事务分布式事务相关的一些基础概念XA规范2PC3PC分布式系统相关的核心理论CAP与BASE理论为什么只能保证CP或者APBASE理论业内使用的几种分布式事务解决方案XA方案TCC本地消息表可靠消息最终一致性方案最大努力通知方案总结
前言
作为分布式事务整体解决方案系列文章的第一篇,本文将系统介绍分布式事务的基础概念、分布式的相关的核心理论,以及几种分布式事务的几种解决方案和适应场景。
什么是分布式事务
为了文章的完整性,这里还是简要说一下分布式事务。我们都知道,数据库的事务是基于一个连接Connection的,在操作数据库的时候,都会先获取一个连接,然后开启事务,执行增删改操作,例如创建订单和锁定库存,最后提交或回滚事务,这就是本地事务。本地事务的ACID特性确保了多个操作,也就是这里的创建订单和锁定库存要么同时成功,要么多时失败。而分布式事务中就不这么简单了,当我们的业务系统本身是分布式或微服务化时,不同的系统之间使用的是不同的数据库,或者不同的数据库连接。这里需要注意的是,事务是基于Connection的,只要不是同一个Connection,都会有分布式事务的问题,而不是需要跨库操作才会有分布式事务问题,只是一般情况下,我们不同的业务系统都对应了不同的库。分布式事务就是为了解决不同数据库(连接)之间数据一致性问题。
分布式事务相关的一些基础概念
XA规范
X/Open组织提出的分布式事务处理模型(Distributed Transaction Processing Reference Model,简称DTP)中,定义了几个角色,即:AP(Application,应用程序),TM(Transaction Manager,事务管理器),RM(Resource Manager,资源管理器),CRM(Communication Resource Manager,通信资源管理器),其实Application可以理解为就是我们的系统,TM的话就是一个在系统里嵌入的一个专门管理横跨多个数据库的事务的一个组件,RM的话说白了就是数据库(比如MySQL),CRM可以是消息中间件(可选)。
这里TM管理的事务,其实是一个全局事务,也就是分布式事务,指一个横跨多个数据库的事务,在一个事务里涉及了多个数据库的操作,然后要保证多个数据库中任何一个操作失败了,其他所有库的操作全部回滚。
那么XA规范是啥呢?上面这套东西就是所谓的X/Open组织搞的一个分布式事务的模型,而XA规范基于这套分布式事务模型定义好TM与RM之间的接口规范,就是管理分布式事务的那个组件跟各个数据库之间通信的接口。以下的接口函数使事务管理器TM可以对资源管理器RM进行的操作:
1)xa_open,xa_close:建立和关闭与资源管理器的连接
2)xa_start,xa_end:开始和结束一个本地事务
3)xa_prepare,xa_commit,xa_rollback:预提交、提交和回滚一个本地事务
4)xa_recover:回滚一个已进行预提交的事务
5)ax_开头的函数使资源管理器可以动态地在事务管理器中进行注册,并可以对XID(TRANSACTION IDS)进行操作
6)ax_reg,ax_unreg;允许一个资源管理器在一个TMS(TRANSACTION MANAGER SERVER)中动态注册或撤消注册
XA规范的具体实现由数据库厂商来提供,例如MySQL就会提供XA规范的接口函数和类库实现。
2PC
Two-Phase-Commitment-Protocol,即两阶段提交协议。其实就是基于XA规范,来让分布式事务可以落地,定义了很多实现分布式事务过程中的一些细节:
准备阶段
简单来说,就是TM先发送个prepare消息给各个数据库,让各个库先把分布式事务里要执行的各种操作,先准备执行,其实此时各个库会先执行好,但是不提交。如果你硬是要理解一下的话,也可以认为是prepare消息一发,各个库先在本地开个事务,然后执行好SQL,而且注意这里各个数据库会准备好随时可以提交或者是回滚,有对应的日志记录。然后各个数据库都返回一个响应消息给事务管理器,如果成功了就发送一个成功的消息,如果失败了就发送一个失败的消息提交写段
如果准备阶段有RM返回了失败,这个时候TM直接判定这个分布式事务失败,然后TM通知所有的数据库全部回滚;如果准备阶段所有RM都返回了成功,这个时候TM就会通知所有数据库进行提交,提交完成后会通知TM,TM要是发现所有数据库的事务都提交成功了,就认为整个分布式事务成功了
2PC协议的缺陷:
同步阻塞:在阶段一里执行prepare操作会占用资源,一直到整个分布式事务完成,才会释放资源,这个过程中,如果有其他人要访问这个资源,就会被阻塞住
单点故障:TM是个单点
事务状态丢失:即使把TM做成一个双机热备的,一个TM挂了自动选举其他的TM出来,但是如果TM挂掉的同时,接收到commit消息的某个库也挂了,此时即使重新选举了其他的TM,新的TM根本不知道这个分布式事务当前的状态,因为不知道哪个库接收过commit消息
脑裂问题:在阶段二中,如果发生了脑裂问题,那么就会导致某些数据库没有接收到commit消息,有些库收到了commit消息,导致数据不一致
3PC
3PC,Three-Phase Commit,即三阶段提交,其将二阶段提交协议的"提交事务请求"过程一分为二,形成了CanCommit、PreCommit和DoCommit三个阶段组成的事务处理协议,是针对2PC做的一个改进,主要就是为了解决2PC协议的一些问题。
CanCommit阶段
TM发送一个CanCommit消息给各个数据库,然后各个库返回个结果,这里是不会执行实际的SQL语句,就是各个库看看自己网络环境啊,各方面是否readyPreCommit阶段
如果各个库对CanCommit消息返回的都是成功,那么就进入PreCommit阶段,TM发送PreCommit消息给各个库,这个时候就相当于2PC里的阶段一,其实就会执行各个SQL语句,只是不提交罢了;如果有个库对CanCommit消息返回了失败,TM发送abort消息给各个库,结束这个分布式事务DoCommit阶段
如果各个库对PreCommit阶段都返回了成功,那么发送DoCommit消息给各个库提交事务,各个库如果都返回提交成功给TM,那么分布式事务成功;如果有失败,或者超时一直没返回,那么TM认为分布式事务失败,直接发abort消息给各个库,回滚分布式事务。在DoCommit阶段,各个库自己也有超时机制,也就是说,如果一个库收到了PreCommit自己还返回成功了,等了一会儿,如果超时时间到了,还没收到TM发送的DoCommit消息或者是abort消息,直接判定为TM可能出故障了,会自动提交事务。
3PC解决了TM单点故障的问题,因为即使TM挂掉,RM自己也会提交事务,同时资源阻塞问题也能减轻一下,因为一个库如果一直接收不到DoCommit消息,不会一直锁着资源,RM会提交释放资源的,所有能减轻资源阻塞问题,比2PC稍微好一些。
3PC的缺陷:
如果TM在DoCommit阶段发送了abort消息给各个库,结果因为脑裂问题,某个库没接收到abort消息,RM还去进行commit操作,导致数据出错。
总结:无论是2PC还是3PC,都没法完全保证分布式事务的ok的,要明白这一点,总有一些特殊情况下会出问题的
分布式系统相关的核心理论
聊完了分布式事务中的一些基础概念,接下来谈谈分布式系统的CAP和BASE理论。分布式事务的核心概念+分布式系统的核心理论,组成了后文中提到分布式事务的几种解决实现方案,这几种具体实现方案需要是依靠这些理论来支撑落地实现的。
CAP与BASE理论
CAP,就是Consistency、Availability、Partition Tolerence的简称,简单来说,就是一致性、可用性、分区容忍性。
一致性
指一个分布式系统中,一旦你做了一个数据的修改,那么这个操作成功的时候,就必须是分布式系统的各个节点都是一样的。假如客户端发起一个数据修改的请求,然后服务器告诉他成功了,结果去查的时候,从某个节点上查询数据,发现这个数据不对,这样的话就成了数据不一致了,就是分布式系统的各个节点上的数据是不一样的,就是不一致。
一致性还分为强一致性、弱一致性以及最终一致性,啥叫强一致性呢,上面讲的那种就是强一致性;弱一致性呢,就是你更新个数据,并不知道能不能让各个节点都更新成功;最终一致性,就是可能更新数据后一段时间内数据不一致,但过了一段时间最终一致了。
最终一致性,应该是分布式系统中非常常见的实现,redis主从同步,你可以做成主从异步同步的,主节点同步数据到从节点上去的时候,异步,就是最终一致性的体现。可用性
客户端往分布式系统的各个节点发送请求,都是可以获取到响应的,要不是可以写入成功,要不是可以查询成功;什么叫做不可用呢?客户端往分布式系统中的各个节点发送请求的时候,获取不到响应结果,这个时候系统就是不可用了分区容忍性
分区,指定的网络分区,partition,network partition。分布式系统之间的网络环境出了故障,分布式系统的各个节点之间无法进行通信。
分区容忍性,就是指你的分布式系统可以容忍网络分区的故障,出现上面说的那种网络分区的故障之后,分布式系统的各个节点之间无法进行通信,无所谓,整套分布式系统各个节点,各自为战,该干嘛干嘛,只不过互相之间无法通信而已。如果不具备分区容忍性会怎么样呢?那就是说一旦网络故障,整套系统崩溃,你哪怕给各个节点发送消息,全部失败,系统无法接收请求
网络是不可靠的,随时可能发生问题,所以,分布式系统是一定要保证P,即保证分区容忍性的。但是,分布式系统不可能同时保证CAP,只能保证CP或者AP。基于这套理论,redis、mongodb、hbase等的分布式系统,都是参照着CAP理论来设计的,有些系统是CP,有些系统是AP。
为什么只能保证CP或者AP
CP:
假设出现了网络分区的故障,但是因为有P,所以分布式系统继续运转,但是此时分布式系统的节点之间无法进行通信,也就无法同步数据了,此时客户端要来查询数据,实际上是处于一个不一致的状态,因为各个节点之间的数据是不一样的,如果客户端来查询某条数据,你要是要保证CP的话,就得返回一个特殊的结果(异常)给客户端,而不能返回空,任何一个节点此时不接收任何查询的请求,返回一个异常(系统当前处于不一致的状态,无法查询),这样客户端是看不到不一致的数据的。这就保证了CAP里的C,一致性,分布式系统本身处于不一致的时候,客户端看不到不一致的数据,就保证了一致性,保证了CP,但是此时就牺牲掉了A,可用性,因为为了不让你看到不一致的数据,所以你发送请求过来是返回异常的,请求失败了,此时分布式系统就暂时处于不可用的状态下,也就是保证了CP,就没有了A。
AP:
如果网络故障,数据没同步,数据处于不一致的状态下,为了要保证A,可用性,各个节点都要允许任何客户端来查询,这样的话呢,整个系统就处于可用的状态下,但是此时就牺牲掉了C,由于网络原因其他节点还没有同步数据,此时从不同的节点查询数据,有可能是不一致的
BASE理论
所谓的BASE,Basicly Available、Soft State、Eventual Consistency,也就是基本可用、软状态、最终一致性。
BASE希望的是,CAP里面基本都可以同时实现,但是不要求同时全部100%完美的实现,CAP三者同时基本实现。
基本可用
正常情况下,查询可以负载均衡到各个节点去查,可以多节点抗高并发查询。但是此时网络故障,可以降级为所有客户端强制查询主节点,这样看到的数据暂时而言都是一样的,都是从主节点去查,但是因为客户端访问量太大了,同时用一个主节点来支撑扛不住怎么办呢,那就需要对主节点做限流降级,也就是说如果流量太大了,直接返回一个空,让你稍后再来查询,这样就保证了所谓的基本可用,由于降级的措施在里面,跟正常的可用是不一样的,比正常的可用要差一些,但是还是基本可以用的。软状态
各个节点都可以查询的,但是这个时候会发现有的节点可以返回数据,有的节点无法返回数据,会看到不一致的状态,这个不一致的状态,就是指的是BASE中的S,soft state,软状态最终一致性
一旦故障或者延迟解决了,数据过了一段时间最终一定是可以同步到其他节点的,数据最终一定是可以处于一致性的
业内使用的几种分布式事务解决方案
XA方案
即前文提到的XA规范以及两阶段提交协议,适用于单体系统需要跨库操作数据的场景,该方案其实是不常用的,一般来说某个系统内部如果出现跨多个库的这么一个操作,是不合规的,但可能有些遗留的老系统中,有这种情况。在JAVA中,可以基于JTA+Atomikos框架来实现。
JTA负责Atomikos框架的一个指挥和调度,让它按照事务的基本步骤来走,Atomikos框架相当于是DTP模型里的TM,跟各个MySQL(RM)通信,执行XA START、XA END、XA COMMIT、XA ROLLBACK等符合XA规范的一套接口调用指令。
实现原理:创建分布式事务的时候,创建一个代表了分布式事务的对象。在各个SQL执行的时候,必须从自定义的DataSource里面获取Connection,对Connection的prepareStatement()方法的调用,需要进行拦截,去对各个库执行XA START指令,以及定义好SQL;在提交事务的时候,需要去对各个库执行XA PREPARE指令,如果都成功,就执行XA COMMIT指令,如果失败,就执行XA ROLLBACK指令。
TCC
在分布式系统或微服务架构中很常见的一种方案,它是应用层面的2PC,TCC的全程是:Try、Confirm、Cancel。这个其实是用到了补偿的概念,分为了几个阶段:
1)Try阶段:这个阶段说的是对各个服务的资源做检测以及对资源进行锁定或者预留
2)Confirm阶段:这个阶段说的是在各个服务中执行实际的操作
3)Cancel阶段:如果任何一个服务的业务方法执行出错,那么这里就需要进行补偿,就是执行已经执行成功的业务逻辑的回滚操作
这种方案要求一个业务操作同时提供try、confirm、cancel三个接口,会造成代码量比较大,对业务入侵强,需要开发者先在try中锁定资源,然后在confirm中扣减资源,还需要在cancel中编写补偿逻辑,即如果失败,会调用cancel将try已经执行的锁定资源释放。
适用场景:适合一致性要求高,例如和资金、支付、交易相关的操作,是你系统中核心的逻辑操作,而且最好是你的各个业务执行的时间都比较短。
在JAVA语言中,知名的TCC实现框架有tcc-transaction、bytetcc、himly,如无特殊需求,可以直接选择以上框架中的一种拿来直接使用,否则可考虑自行开发或者基于某个框架做二次开发。
本地消息表
是ebay的搞出来的一套思想,大概意思是这样的:
1)A系统在自己本地一个事务里操作同时,插入一条数据到消息表
2)接着A系统将这个消息发送到MQ中去
3)B系统接收到消息之后,在一个事务里,往自己本地消息表里插入一条数据,同时执行其他的业务操作,如果这个消息已经被处理过了,那么此时这个事务会回滚,这样保证不会重复处理消息
4)B系统执行成功之后,就会更新自己本地消息表的状态以及A系统消息表的状态
5)如果B系统处理失败了,那么就不会更新消息表状态,那么此时A系统会定时扫描自己的消息表,如果有没处理的消息,会再次发送到MQ中去,让B再次处理
6)这个方案保证了最终一致性,哪怕B事务失败了,但是A会不断重发消息,直到B那边成功为止。
这个方案最大的问题就在于严重依赖于数据库的消息表来实现,会导致如果是高并发场景性能下降,一般很少使用
可靠消息最终一致性方案
是本地消息表方案的变种实现,去掉了消息表,直接基于MQ来实现事务。比如阿里的RocketMQ就支持消息事务。
大概的意思就是:
1)A系统先发送一个prepared消息到mq,如果这个prepared消息发送失败那么就直接取消操作别执行了
2)如果这个消息发送成功过了,那么接着执行本地事务,如果成功就告诉mq发送confirm消息给B系统,如果失败就告诉mq回滚消息
3)如果发送了确认消息,那么此时B系统会接收到确认消息,然后执行本地的事务
4)mq会自动定时轮询所有prepared消息回调A系统的接口,A系统返回这个数据是否在本地执行成功,如果成功,让mq发送confirm消息给B系统,如果失败了,回滚消息
5)这个方案里,要是系统B的事务失败了咋办?自动不断重试直到成功,如果实在是不行,要么就是针对重要的资金类业务进行回滚,比如B系统本地回滚后,想办法通知系统A也回滚;或者是发送报警由人工来手工回滚和补偿
如果不是使用的RocketMQ,那么就需要自己实现一套可靠消息服务,自己封装一套类似的逻辑出来。这个方案,适合大多的分布式事务场景
最大努力通知方案
1)系统A本地事务执行完之后,发送个消息到MQ
2)这里会有个专门消费MQ的最大努力通知服务,这个服务会消费MQ然后写入数据库中记录下来,或者是放入个内存队列也可以,接着调用系统B的接口
3)要是系统B执行成功就ok了;要是系统B执行失败了,那么最大努力通知服务就定时尝试重新调用系统B,反复N次,最后还是不行就放弃
适合对分布式事务要求不高的场景,例如下单完成发送短信给客户,尝试几次失败就算了,短信没有发送成功也无所谓。
总结
本篇文章主要是讲解了分布式事务的一些基本概念,分布式系统的CAP和BASE理论,只有有了这些理论层面的东西支撑,才能设计好分布式事务的具体实现;然后本文简单的介绍了各种分布式事务的实现方案及使用场景。如果没玩过分布式事务的朋友,也许对这里的概念和几种方案的具体实现感觉云里雾里,不知道具体如何实现。请不用着急,本文只是分布式事务的开篇,后续会陆续更新每种分布式事务的在代码层面的具体实现和使用,并提供源码参考学习,也会和大家剖析使用到的分布式事务框架的底层实现原理。