分布式事务
分布式事务是指会涉及到操作多个数据库的事务,其实就是将对同一库事务的概念扩大到了对多个库的事务。目的是为了保证分布式系统中的数据一致性。
分布式事务处理的关键是:
- 需要记录事务在任何节点所做的所有动作;
- 事务进行的所有操作要么全部提交,要么全部回滚。
1.CAP理论
分布式系统的三个指标:
- 一致性 Consistency:对于客户端的每次读操作,要么读到的是最新的数据,要么读取失败。
- 可用性 Availability:任何客户端的请求都能得到响应数据,不会出现响应错误。
- 分区容错性 Partition tolerance:由于分布式系统通过网络进行通信,网络是不可靠的。当任意数量的消息丢失或延迟到达时,系统仍会继续提供服务,不会挂掉。
一个分布式系统,不可能同时做到这三点;
对于一个分布式系统而言,P是前提,必须保证,因为只要有网络交互就一定会有延迟和数据丢失,这种状况我们必须接受,必须保证系统不能挂掉。所以只剩下C、A可以选择。要么保证数据一致性(保证数据绝对正确),要么保证可用性(保证系统不出错)。
当选择了C(一致性)时,如果由于网络分区而无法保证特定信息是最新的,则系统将返回错误或超时。
当选择了A(可用性)时,系统将始终处理客户端的查询并尝试返回最新的可用的信息版本,即使由于网络分区而无法保证其是最新的。
CP架构
由于网络问题,节点A和节点B之前不能互相通讯。当有客户端(上图Actor)向节点A进行写入请求时(准备写入Message 2),节点A会不接收写入操作,导致写入失败,这样就保证了节点A和节点B的数据一致性,即保证了Consisteny(一致性)。
然后,如果有另一个客户端(上图另一个Actor)向B节点进行读请求的时候,B请求返回的是网络故障之前所保存的信息(Message 1),并且这个信息是与节点A一致的,是整个系统最后一次成功写入的信息,是能正常提供服务的,即保证了Partition tolerance(分区容错性)。
AP架构
由于网络问题,节点A和节点B之前不能互相通讯。当有客户端(上图Actor)向节点A进行写入请求时(准备写入Message 2),节点A允许写入,请求操作成功。但此时,由于A和B节点之前无法通讯,所以B节点的数据还是旧的(Message 1)。当有客户端向B节点发起读请求时候,读到的数据是旧数据,与在A节点读到的数据不一致。但由于系统能照常提供服务,所以满足了Availability(可用性)要求。
注意事项
对于开发者而言,构建服务的时候需要根据业务特性作出权衡考虑,哪些点是当前系统可以取舍的,哪些是应该重点保障的。
如在某个电商系统中,属于用户模块的数据(账密、钱包余额等)对一致性的要求很高,就可以采用CP架构。
而对于一些商品信息方面的数据对一致性要求没那么高,但为了照顾用户体验,所以对可用性要求更高一些,那么这个模块的数据就可以采用AP架构。
注:只能选择CP和AP,无法选择CA,这句话成立的前提条件是在系统发生了网络故障的情况下。在网络正常情况下,CA是可以实现的,我们也需要去保证在绝大多数时间下的CA架构。
2.Base理论
核心思想:既是无法做到强一致性(Strong consistency),但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性(Eventual consistency)。
Basically Available 基本可用
假设系统,出现了不可预知的故障,但还是能用,相比较正常的系统而言会有响应时间和功能上的损失:
- 响应时间上的损失:正常情况下的搜索引擎0.5秒即返回给用户结果,而基本可用的搜索引擎可以在2秒作用返回结果。
- 功能上的损失:在一个电商网站上,正常情况下,用户可以顺利完成每一笔订单。但是到了大促期间,为了保护购物系统的稳定性,部分消费者可能会被引导到一个降级页面。
Soft state(软状态)
相对于原子性而言,要求多个节点的数据副本都是一致的,这是一种“硬状态”。
软状态指的是:允许系统中的数据存在中间状态,并认为该状态不影响系统的整体可用性,即允许系统在多个不同节点的数据副本存在数据延时。
如先把订单状态改成已支付成功,然后告诉用户已经成功了;剩下在异步发送mq消息通知库存服务减库存,即使消费失败,MQ消息也会重新发送(重试)。
注:不可能一直是软状态,必须有个时间期限。在期限过后,应当保证所有副本保持数据一致性,从而达到数据的最终一致性。这个时间期限取决于网络延时、系统负载、数据复制方案设计等等因素。
Eventually consistent(最终一致性)
- 强一致性读操作要么处于阻塞状态,要么读到的是最新的数据
- 最终一致性通常是异步完成的,读到的数据刚开始可能是旧数据,但是过一段时间后读到的就是最新的数据
3.最终一致性解决方案
注意
- 可查询操作:业务方需要提供可查询接口,来查询数据信息和状态,供其他服务知道数据状态。
- 幂等操作:同样的参数执行同一个方法,返回的结果都一样。在分布式环境,难免会出现数据的不一致,很多时候为了保证数据的一致,我们都会进行重试。如果不保证幂等,即使重试成功了,也无法保证数据的一致性。我们可以通过业务本身实现实现幂等,比如数据库的唯一索引来约束;也可以缓存(记录)请求和操作结果,当检测到一样的请求时,返回之前的结果。
- 补偿操作:某些数据存在不正常的状态,需要通过额外的方式使数据达到最终一致性的操作。
XA规范
XA规范是由 X/Open组织(即现在的 Open Group )定义的分布式事务处理模型。\
XA 一共分为两阶段:
第一阶段(prepare):即所有的参与者 RM 准备执行事务并锁住需要的资源。参与者 ready 时,向 TM 报告已准备就绪。
第二阶段 (commit/rollback):当事务管理者(TM)确认所有参与者(RM)都 ready 后,向所有参与者发送 commit 命令。
目前主流的数据库基本都支持 XA 事务,包括 mysql、oracle、sqlserver、postgre
XA规范的组成
- 应用程序( AP )
- 事务管理器( TM ):交易中间件等
- 资源管理器( RM ):关系型数据库等
- 通信资源管理器( CRM ):消息中间件等
XA规范定义
XA规范定义了交易中间件与数据库之间的接口规范(即接口函数),交易中间件用它来通知数据库事务的开始、结束以及提交、回滚等。而XA接口函数由数据库厂商提供。
二阶提交协议和三阶提交协议就是基于XA规范提出的其中,二阶段提交就是实现XA分布式事务的关键。
XA编程规范
- 配置TM,给TM注册RM作为数据源。其中,一个TM可以注册多个RM。
- AP向TM发起一个全局事务。这时,TM会发送一个XID(全局事务ID)通知各个RM。
- AP从TM获取资源管理器的代理(例如:使用JTA接口,从TM管理的上下文中,获取出这个TM所管理的RM的JDBC连接或JMS连接)。
- AP通过从TM中获取的连接,间接操作RM进行业务操作。TM在每次AP操作时把XID传递给RM,RM正是通过这个XID关联来操作和事务的关系的。
- AP结束全局事务时,TM会通知RM全局事务结束。开始二段提交,也就是prepare - commit的过程。
二阶段提交协议 2PC
思路:每个参与者将操作成败通知协调者,再由协调者根据所有参与者的反馈情报,决定各参与者是否要提交操作还是中止操作。
- 第一阶段:准备阶段(投票阶段)
- 第二阶段:提交阶段(执行阶段)
准备阶段
a. 事务询问
协调者向所有的参与者询问,是否准备好了执行事务,并开始等待各参与者的响应。
b. 执行事务
各参与者节点执行事务操作。如果本地事务成功,将Undo和Redo信息记入事务日志中,但不提交;否则,直接返回失败,退出执行。
c. 各参与者向协调者反馈事务询问的响应
如果参与者成功执行了事务操作,那么就反馈给协调者 Yes响应,表示事务可以执行提交;如果参与者没有成功执行事务,就返回No给协调者,表示事务不可以执行提交。
提交阶段
在提交阶段中,会根据准备阶段的投票结果执行2种操作:执行事务提交,中断事务。
事务提交
a. 发送提交请求
协调者向所有参与者发出commit请求。
b. 事务提交
参与者收到commit请求后,会正式执行事务提交操作,并在完成提交之后,释放整个事务执行期间占用的事务资源。
c. 反馈事务提交结果
参与者在完成事务提交之后,向协调者发送Ack信息。
d. 事务提交确认
协调者接收到所有参与者反馈的Ack信息后,完成事务。
中断事务
a. 发送回滚请求
协调者向所有参与者发出Rollback请求。
b. 事务回滚
参与者接收到Rollback请求后,会利用其在提交阶段种记录的Undo信息,来执行事务回滚操作。在完成回滚之后,释放在整个事务执行期间占用的资源。
c. 反馈事务回滚结果
参与者在完成事务回滚之后,向协调者发送Ack信息。
d. 事务中断确认
协调者接收到所有参与者反馈的Ack信息后,完成事务中断。
优缺点
- 优点:原理简单,实现方便。
- 缺点:同步阻塞,单点问题,数据不一致,容错性不好。
问题
1、同步阻塞问题。执行过程中,所有参与节点都是事务阻塞型的。当参与者占有公共资源时,其他第三方节点访问公共资源不得不处于阻塞状态。
2、单点故障。由于协调者的重要性,一旦协调者发生故障。参与者会一直阻塞下去。尤其在第二阶段,协调者发生故障,那么所有的参与者还都处于锁定事务资源的状态中,而无法继续完成事务操作。(如果是协调者挂掉,可以重新选举一个协调者,但是无法解决因为协调者宕机导致的参与者处于阻塞状态的问题)
3、数据不一致。在二阶段提交的阶段二中,当协调者向参与者发送commit请求之后,发生了局部网络异常或者在发送commit请求过程中协调者发生了故障,这回导致只有一部分参与者接受到了commit请求。而在这部分参与者接到commit请求之后就会执行commit操作。但是其他部分未接到commit请求的机器则无法执行事务提交。于是整个分布式系统便出现了数据部一致性的现象。
4、二阶段无法解决的问题:协调者再发出commit消息之后宕机,而唯一接收到这条消息的参与者同时也宕机了。那么即使协调者通过选举协议产生了新的协调者,这条事务的状态也是不确定的,没人知道事务是否被已经提交
TCC 事务模式
TCC:Try(预处理)、Confirm(确认)、Cancel(取消)
三个阶段
- Try阶段:主要是对业务系统做检测及资源预留
- Confirm阶段:主要是对业务系统做确认提交,Try阶段执行成功并开始执行 Confirm阶段时,默认 Confirm阶段是不会出错的。即:只要Try成功,Confirm一定成功。如果confirm出错了,那么就需要引入补偿机制或者人工处理。
- Cancel阶段:主要是在业务执行错误,需要回滚的状态下执行的业务取消,预留资源释放。同样TCC中我们认为cancel阶段一定会执行成功,如果失败也需要引入重试或者人工处理。
三种异常处理
空回滚:分支事务异常调用失败,并未执行try方法,当恢复后事务执行回滚操作就会调用此分支事务的cancel方法,如果cancel方法不能处理此种情况就会出现空回滚。
解决:记录全局事务ID,当cancel执行时,先判断是否有该全局事务ID,有则回滚,否则不做任何操作。
幂等:由于服务宕机或者网络问题,方法的调用可能出现超时,为了保证事务正常执行我们往往会加入重试的机制,因此就需要保证confirm和cancel阶段操作的幂等性。
解决:在分支事务记录表中增加事务执行状态,每次执行confirm和cancel方法时都查询该事务的执行状态,以此判断事务的幂等性。
悬挂:调用try之前会先注册分支事务,注册分支事务之后,调用出现超时,此时try请求还未到达对应的服务,因为调用超时了,所以会执行cancel调用,此时cancel已经执行完了,然而这个时候try请求到达了,这个时候执行了try之后就没有后续的操作了,就会导致资源挂起,无法释放。
解决:执行try方法时我们可以判断confirm或者cancel方法是否执行,如果执行了那么就不执行try阶段。同样借助分支事务表中事务的执行状态。如果已经执行了confirm或者cancel那么try就执行。
优点: 跟2PC比起来,实现以及流程相对简单了一些,但数据的一致性比2PC也要差一些
缺点: 缺点还是比较明显的,在2,3步中都有可能失败。TCC属于应用层的一种补偿方式,所以需要程序员在实现的时候多写很多补偿的代码,在一些场景中,一些业务流程可能用TCC不太好定义及处理。
本地消息
将需要分布式处理的任务通过消息的方式来异步确保执行。
写本地消息和业务操作放在一个事务里,保证了业务和发消息的原子性,要么他们全都成功,要么全都失败。
- 当系统 A 被其他系统调用发生数据库表更操作,首先会更新数据库的业务表,其次会往相同数据库的消息表中插入一条数据,两个操作发生在同一个事务中
- 系统 A 的脚本定期轮询本地消息往 mq 中写入一条消息,如果消息发送失败会进行重试
- 系统 B 消费 mq 中的消息,并处理业务逻辑。如果本地事务处理失败,会在继续消费 mq 中的消息进行重试,如果业务上的失败,可以通知系统 A 进行回滚操作
本地消息表实现的条件:
- 消费者与生成者的接口都要支持幂等
- 生产者需要额外的创建消息表
- 需要提供补偿逻辑,如果消费者业务失败,需要生产者支持回滚操作
容错机制:
- 步骤 1 失败时,事务直接回滚
- 步骤 2、3 写 mq 与消费 mq 失败会进行重试
- 步骤 3 业务失败系统 B 向系统 A 发起事务回滚操作
此方案的核心是将需要分布式处理的任务通过消息日志的方式来异步执行。
消息日志可以存储到本地文本、数据库或消息队列,再通过业务规则自动或人工发起重试。人工重试更多的是应用于支付场景,通过对账系统对事后问题的处理。
可靠消息
- A 系统先向 mq 发送一条 prepare 消息,如果 prepare 消息发送失败,则直接取消操作
- 如果消息发送成功,则执行本地事务
- 如果本地事务执行成功,则 mq 发送一条 confirm 消息,如果发送失败,则发送回滚消息
- B 系统定期消费 mq 中的 confirm 消息,执行本地事务,并发送 ack 消息。如果 B 系统中的本地事务失败,会一直不断重试,如果是业务失败,会向 A 系统发起回滚请求
- mq 会定期轮询所有 prepared 消息调用系统 A 提供的接口查询消息的处理情况,如果该 prepare 消息本地事务处理成功,则重新发送 confirm 消息,否则直接回滚该消息
该方案与本地消息最大的不同是去掉了本地消息表,本地消息表依赖消息表重试写入 mq 这一步由本方案中的轮询 prepare 消息状态来重试或者回滚该消息替代。其实现条件与余容错方案基本一致。
目前市面上实现该方案的有阿里的 RocketMq。
最大努力通知
最大努力通知是最简单的一种柔性事务,适用于一些最终一致性时间敏感度低的业务,且被动方处理结果不影响主动方的处理结果。(需要反复通知的情况)
这个方案的大致意思就是:
- 系统 A 本地事务执行完之后,发送个消息到 MQ;
- 这里会有个专门消费 MQ 的服务,这个服务会消费 MQ 并调用系统 B 的接口;
- 要是系统 B 执行成功就 ok 了;要是系统 B 执行失败了,那么最大努力通知服务就定时尝试重新调用系统 B, 反复 N 次,最后还是不行就放弃。