自主研发平台POIN4.2揭秘系列 --分布式事务
NO.1
背 景
随着行内分布式架构的推进,越来越多的业务系统进行了微服务化的改造,由一个单体的应用系统拆分成了多个独立的微服务。一般系统的拆分会涉及到两个纬度。
应用服务拆分
将原来揉在一个系统的业务进行梳理,拆分出能独立体系的一个个子系统,例如用户服务,账户服务,支付服务等;经过业务服务拆分后,原来一个服务就能完成的业务操作现在需要跨多个服务进行。
数据库拆分,数据库的拆分一般有两个维度:垂直拆分和水平拆分。
垂直拆分是将耦合在一起的业务数据都放到同一个库中,如用户相关的都存到用户库中,账户相关的统一都存到账户库中,支付相关的都存到支付库中。
水平拆分是将同一张表水平分成多份,如用户信息按照用户id散列到不通的用户表。
我们可以看到,在分布式架构中,原本由单个应用或者单个数据库就能完成的业务处理,将需要多个服务配合完成,这些服务操作的数据可能在一个机房中,也可能跨机房存在。如何保证在处理过程中数据的一致性,也就是分布式事务需要解决的问题。
NO.2
理论基础
分布式事务是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上,一般通过二阶段或者三阶段提交协议来保证在跨节点的调用过程数据的一致性。
二阶段协议(2pc) :
2pc是将整个事务流程分为两个阶段,准备阶段(Prepare phase)、提交阶段(Commit phase),2是指两个阶段,P是指准备阶段,C是指提交阶段。整个事务过程由事务协调者和参与者组成,事务协调者负责决策整个分布式事务的提交和回滚,事务参与者负责自己本地事务的提交和回滚。
一阶段,提交事务请求:协调者向所有的参与者节点发送事务内容,询问是否可以执行事务操作,并等待其他参与者节点的反馈。参与者节点收到协调者的事务操作后,执行操作,但不提交;各参与者节点将操作结果反馈给协调者。
二阶段,事务提交:根据一阶段各个参与者节点反馈的结果,如果所有参与者节点反馈成功,则协调者通知个参与者执行事务提交,否则通知中断事务。
2pc协议存在的问题:
同步阻塞:二阶段提交过程中,所有参与事务操作的节点处于同步阻塞状态,无法进行其他的操作。
单点问题:协调者在整个两阶段提交过程中扮演着举足轻重的作用,一旦协调者所在服务器宕机,就会影响整个数据库集群的正常运行。比如在第二阶段中,如果协调者因为故障不能正常发送事务提交或回滚通知,那么参与者们将一直处于阻塞状态,整个数据库集群将无法提供服务。
数据一致性问题:两阶段提交协议虽然是分布式数据强一致性所设计,但仍然存在数据不一致性的可能性。比如在第二阶段中,假设协调者发出了事务 commit 通知,但是因为网络问题该通知仅被一部分参与者所收到并执行了commit 操作,其余的参与者则因为没有收到通知一直处于阻塞状态,这时候就产生了数据的不一致性。
三阶段协议(3PC) :
三阶段提交协议(3PC)是二阶段提交(2PC)的改进版本。主要有两个改动点。
引入超时机制。同时在协调者和参与者中都引入超时机制。
在第一阶段和第二阶段中插入一个准备阶段,保证了在最后提交阶段之前各参与节点状态的一致。
也就是说,除了引入超时机制之外,3PC把2PC的投票阶段再次一分为二,这样三阶段提交就有Can-Commit、Pre-Commit、Do-Commit三个阶段。 引入Can-Commit阶段,主要是为了在预执行之前,保证所有参与者都具备可执行条件,从而减少资源浪费。
常见的解决方案 :
XA模型:本质上基于数据库的两阶段提交,是基于资源层的底层分布式解决方案。该方案由于性能较差,较少被采用。
柔性XA:XA模式的提升,将两阶段提交从资源层上升到应用层,由应用来保证本地事务,最终由事务管理器统一通知本地事务提交或者回滚,下文中提到的SEATA的AT模式就属于这一类。
SAGA事务模型:按照逻辑依次调用服务,当出现异常时,对已经调用成功的事务进行补偿,现在很多业务系统中的冲正接口就是类似的逻辑。这样会使得数据有短暂的不一致,也就是不能保证隔离性。
TCC事务模型:TCC是Try、Confirm和Cancel的缩写。在业务处理之前先对本地事务节点进行业务资源的预留检查(try),或者说一种中间状态,如果各个参与方的检查都成功,则进行真正的事务处理(confirm),反之则进行回滚(cancel)。tcc在保证强一致性的同时,最大限度的提高系统的可伸缩性和可用性,同时try操作可以灵活选择业务资源的锁定粒度,而不是锁住整个资源,提高了并发处理能力。但是这种模式开发成本较大,业务系统对于参与分布式事务的交易都要去实现try、confirm和cancel的方法。
可靠消息最终一致性模型:主要有本地消息表与事务消息两类实现方式,本质上也是属于SAGA模式的一种特定实现。
本地消息表:将需要其他微服务执行的操作做为消息写入本地数据库,通过本地事务保证主事务执行与消息写入的原子性,该事务依赖的其他参与方消费该消息,完成业务逻辑。一般使用MQ发送消息到消费方。
事务消息:依赖于支持事务消息的消息队列。利用消息中间件支持两阶段提交,将本地事务和发消息放在了一个分布式事务里,保证要么本地操作成功且对外发消息成功,要么两者都失败。目前阿里开源的消息队列RocketMQ可以支持事务消息。
最大努力通知模型:最大努力通知方案主要也是借助MQ消息系统来进行事务控制,这一点上与可靠消息最终一致方案一样。它本质上就是通过定期校对通知来实现数据一致性,是一种比较简单的分布式事务方案。
NO.3
行内的发展和落地
框架的选型:
我们自2019年8月份开始了分布式事务相关技术的研究工作,同时也对业内主流的分布式事务框架进行了深入的调研:
从性能、数据隔离、业务改造成本、软件生态前景以及结合POIN现有的技术栈等各方面来考量,我们引入阿里开源的分布式事务框架SEATA。SEATA
是github上受关注度最高的分布式事务开源框架,已有18.2k个star;支持AT,TCC、SAGA和XA等事务模式。
SEATA也是基于2pc实现的分布式事务框架,协调器称为TC,事务发起方称之为TM,其他分支事务的参与方称为RM,主要交互流程如下:
在事务开启时由TM端去TC端注册一个全局事务,并得到一个用来标识该事务的XID。
在各个分支事务执行时会向TC注册一个分支事务。
TC端汇总了各个RM的事务状态后来决定二阶段是执行提交还是回滚,并将通知发送给各事务参与方。
流程图如下所示:
框架的扩展:
POIN平台从2020年3月开始基于SEATA打造了行内的分布式事务框架,下图是整个框架的总体架构图。
总体框架图
在引入框架的同时,结合行内的实际,POIN平台也对SEATA做了很多的功能的扩展和适配性的改造,下面将详细的介绍AT和TCC模式做的优化和扩展。
AT模式的优化和扩展:
AT模式简单来说就是在本地数据库维护了一份undo_log表,在对数据进行操作前会往该表中插入修改前的镜像。如果事务需要回滚,则将undo_log表中的镜像进行恢复,否则将镜像删除。整个过程都由框架统一处理,业务系统无感知,对业务代码也无侵入,只需要在方法上加入平台的注解即可。对于并发量低、调用链条不复杂的系统推荐使用该模式,比如流程管理类的系统;
AT模式的优化和扩展主要涉及以下三个方面:
1. 封装客户端sdk,对接TC,屏蔽应用端与TC的交互,其中sdk增加监控埋点数据;
2. 增加运维管理台功能,提供对事务监控,全局锁监控,异常事务监控等功能;
3. TC端增加对全局锁释放异常的处理,增加按照服务级对客户端进行限流等功能。
功能扩展图
管理后台监控视图
TCC模式的优化和扩展:
相比较于AT模式对业务无侵入的分布式方案,TCC模式需要用户根据自己的场景提供三段业务逻辑:
1. 初步操作Try:完成所有业务检查,预留必须的业务资源;
2. 确认操作Confirm:真正执行业务逻辑,使用Try阶段预留的业务资源。同时,Confirm操作需要满足幂等性,保证一笔分布式事务能且只能成功一次;
3. 取消操作Cancel:释放Try阶段预留的业务资源。同样的,cancel操作也要满足幂等性。
虽然TCC对业务的侵入比较大,但在性能上远高于基于全局锁的AT模式。适用于账户类和支付类等并发率高的交易系统。
目前SEATA的TCC对异常的处理机制还不是很完善,对一些常见的异常场景支持的还不够好,为此我们针对性的增加了很多异常处理的优化。主要涉及空回滚、幂等和悬挂三类。
1. 空回滚:在没有调用 TCC 资源 Try 方法的情况下,调用了二阶段的 Cancel 方法,Cancel 方法需要识别出这是一个空回滚,然后直接返回成功;
2. 幂等:保证TCC二阶段提交重试机制不会引发数据不一致,要求TCC的三个接口保证幂等;
3. 悬挂:二阶段cancel比try接口先执行,Try再执行时,业务预留的资源就没法再继续处理。
优化的方案是在分支事务端新增一个事务状态控制表,来兼容处理这三类异常。
具体来说,在TCC的每个阶段都会往表里插入一条记录,同时在每个阶段执行前会先对该表进行检查,再确定该阶段是否执行。比如在执行Cancel前会对该表进行检查,若已存在一条状态为初始化的分支事务记录,则进行正常回滚,如不存在,则直接返回成功,这也就处理了空回滚的问题;同样为了处理悬挂问题,在执行Try前也会检查该事务是否已经执行完,若二阶段已经执行完则不再执行该Try操作;通过在实际执行前对该表检查,也就从框架层面支持了幂等的功能。
为了进一步提升SEATA的性能,我们也从以下几个方向进行了更加深入的思考:
1. 进一步减少和TC交互次数
现在在执行全局事务时主事务和每个分支事务都需要去和TC端交互,整个过程rpc次数比较多,对性能影响比较大。那能不能通过减少和TC的交互次数来进一步提升性能呢?答案就是同库模式:除了主事务开启时和TC交互外,各个分支事务开启时不再去TC注册,而是在执行完Try操作后接着执行二阶段的方法;当二阶段异常时才会异步通知TC,由TC端再进行统一的调度处理,也就是重试二阶段的方法。
考虑到大部分交易都会正常执行,只有少数异常情况下才会往数据库和mq发送失败消息的情况,该优化显著减少了RM和TC端的交互,大幅度的提升了性能。
2. TC端全局事务表分库分表
在对框架做性能压测时我们能很直观的看出瓶颈主要是在TC的DB库(单库单表)。为此提高TC端在写入时的性能,同时也增加其高可用,我们利用行内自研的分布式数据库EverDB对DB进行分库分表扩展,显著的提升了写入数据的性能。
图文/王永远
编辑/赵小娟