分布式事务数据一致性解决方案

tech2022-08-28  115

Base理论 Basically Available 基本可用 Soft state 软状态 Eventually consistent 最终一致性 三个短语的 缩写,通过牺牲强一致性来获取最终一致性,当出现故障时允许部分不可用但要保证核心功能可用,允许数据在一段时间内不一致,但最终达到一致状态,满足BASE理论的事物,称之为柔性事务。 (1)基本可用:分布式系统出现故障,允许损失部分可用功能,但是要保证核心功能可用,例如电商系统中,交易付款出现故障,但是商品依然可以正常浏览 (2)软状态:不要求强一致性,所有BASE允许出现中间状态(也叫软状态),如支付中、数据同步中等,等到数据最终一致后再将状态改为成功状态。 (3)最终一致:经过一段时间后,所有的分布式节点数据达到一致状态,如支付中变为支付成功或支付失败,但需要一定时间的延迟  

一、2PC

将事务流程分为准备Prepare phase 提交commit phase两个阶段。整个事务过程分为事务管理器和事务参与者两部分,事务管理器决定整个分布式事务的提交和回滚,事务参与者负责自己本地事务的提交和回滚。大部分关系型数据库如Oracle、mysql均支持两阶段提交协议       (1)准备阶段:事务管理器给各个事务参与者发送prepare消息,每个参与者在本地执行事务,并写本地的Undo/Redo log,此时事务并没有提交(undo log记录修改前数据用于回滚,redo log记录修改后数据,用户提交事务时写入数据文件)       (2)提交阶段:若事务管理器收到某事务参与者执行失败的消息,直接给每个参与者发送回滚rollback 消息;否则,发送提交commit消息,参与者根据收到的事务管理器的指令执行提交或回滚操作,并释放在本地事务处理过程中占用的锁资源。

1.XA方案 

如注册用户送积分        (1)应用程序AP持有用户库和积分库两个数据源        (2)应用程序AP通过TM通知用户库RM新增用户信息,同时通知积分库RM为该用户新增积分,各RM此时并未提交事务,该用户和积分资源锁定        (3)TM收到执行失败回复,则向其他RM发起通知回滚事务,回滚完毕,释放锁资源        (4)收到全部执行成功回复,向各RM发出提交通知,提交完毕,释放锁资源

AP应用程序、TM事务管理器、RM资源管理器可以理解为事务参与者,DTP模型定义了TM和RM的接口通讯规范称为XA,简单理解为数据库基于XA协议来实现2PC称为XA方案。TM向AP提供应用程序编程接口,AP通过TM来提交或回滚事务;TM中间件通过XA接口来通知RM数据库事务开始、结束、提交或回滚。 XA方案的缺点:需要数据库支持XA协议;资源锁只有等到两个阶段完成之后才会释放,系统的可用性较差。  

2.Seata方案 

Seata是阿里开源的一个分布式事务解决方案,它支持2PC模式(也支持TCC模式)。Seata不要求数据库支持XA协议,且不长时间持有资源锁,性能比较好,是工作在应用层(业务层)的中间件。对代码0侵入,基本上加一个注解就可以了。        Seata也包含了全局事务和分支事务两部分,且有三个组件来完成分布式事务的处理过程:        (1)Transaction Coordinator TC事务协调器,是一个独立的中间件,需要独立部署运行,它维护全局事务的运行状态,接收TM指令发起全局事务的提交与回滚,负责与RM通信协调各分支事务的提交与回滚,会有一个单独的seata-server服务需要部署。        (2)Transaction Manager TM事务管理器,TM需要嵌入到应用程序中工作,负责开启一个全局事务,并最终向TC发起全局事务的提交或回滚        (3)Resource Manager RM资源管理器,控制分支事务,负责分支注册、状态汇报,接收TC指令用于驱动本地事务的提交或回滚

再以用户注册送积分服务解释一下Seata 2PC的执行过程:        (1)用户服务上开启TM向TC申请创建全局事务并获得XID,使用@GlobalTransactional开启一个TM,从TC获取全局事务ID XID        (2)用户服务RM向TC注册分支事务,执行用户创建逻辑,并将其纳入全局事务XID的管辖范围,会获取一个branchId 分支事务id,此时branchId和XID已经在TC绑定        (3)用户服务执行本地事务,将数据存入用户信息库中并写undo_log表,向TC上报分支事务执行结果,与XA方案不同此时用户服务本地事务已提交,持用资源锁的时间短        (4)下一步执行送积分,调用积分服务(在调用链路上会将XID传递),向TC注册分支事务,执行执行送积分逻辑,将本地事务纳入XID的管理范围        (5)送积分本地事务执行,向积分库中插入数据,记录undo_log,积分服务执行完成,上报执行结果,返回用户服务        (6)用户服务执行完成        (7)TM判断分支事务是否都执行成功,向TC发起针对XID全局提交或回滚通知        (8)TC调度XID管辖下的所有分支事务执行提交或回滚,如果需要回滚则根据undo_log回滚数据,完成后会删除undo_log记录

由于Seata在第一阶段各分支事务就会提交本地事务,所以各RM中需要有一个undo_log表,用于在TC通知RM回滚时,回滚数据。

Seata与传统XA实现2PC的区别        (1)架构层次方面,传统的RM实际作用在数据库层,通过XA协议实现,而Seata的RM是以注解的形式放在请求发起方,开启全局事务,在整个调用链路上传递XID,将所有分支事务纳入全局事务管理之中,用单独的TC服务去执行提交或回滚,不再要求数据库支持XA协议

(2)传统2PC本地事务的提交要等到第二阶段,资源锁需要持有到第二阶段结束才释放,对服务性能影响较大,Seata在第一阶段就会提交本地事务释放锁资源,减少锁的持有时间。

二、TCC

TCC是预处理Try、确认Confirm、撤销Cancel的缩写。在Try中做业务检查及资源预留,Confirm做业务确认操作,Cancel实现一个与Try相反的操作即回滚。TM首先发起所有分支事务的Try操作,当所有的分支事务都执行成功,则执行所有分支的Confirm,若有Try执行失败,TM将发起全部分支的Cancel操作,其中Confirm/Cancel执行失败时会进行重试或人工处理,默认当Try都成功了,Confirm一定会成功。TM事务管理器 创建全局事务,生成全局事务id,由于Confirm和Cancel失败需要重试,所以它要实现幂等。在每一步都要提交事务,使用的是最终一致性。

Seata方案 也支持TCC,但是需要实现try confirm cancel三个操作业务侵入性比较强。

三、可靠消息最终一致性

当事务发起方完成本地事务后发出一条消息,事务参与方接收消息消费进行自身事务的处理,此种方案只强调发送消息给事务参与方且数据保持最终一致性。可靠消息最终一致性需要解决以下几个问题:        (1)本地事务与消息发送的原子性问题,即事务发起方在本地事务执行完成后,必须保证消息能发出去,本地事务和消息发送两者要有原子性,要么都成功要么都失败。        (2)事务参与方接收消息的可靠性,事务参与方必须要能从消息队列接收到消息,接收失败可以重复接收。        (3)消息重复消费问题,事务参与方要保证处理消息处理幂等性。

1.本地消息表方案

通过本地事务保证事务发起方业务操作成功时将需要发送的消息内容也存在数据表中,之后通过定时任务扫描该表通过消息中间件如kafka进行发送,当发送成功后经该条记录标为已发送,利用ack机制保障消息一定发送到消息中间件server上,由消息接收方实现消息重复消费幂等。本地业务与消息内容落库、消息标记已发送和消息中间件发送消息在同一个本地事务中。

2.RocketMQ方案 

RocketMq支持事务消息,保证本地事务和消息发送的原子性,执行流程如下所示,以注册送积分介绍下执行流程

RocketMQ提供了一个接口叫RocketMQLocalTransactionListener,其中有两个方法一个叫excuteLocalTransaction为当提交(half)prepare消息成功后该方法用于执行本地事务,重写该方法执行本地事务操作,返回值就是事务的执行结果;另一个方法是checkLocalTransaction,该方法就是用于查询本地事务的执行结果。需要定义一个listener去实现RockMQLocalTransactionListener接口,重写这两个方法,实现类上需要使用@RocketMQTransactionListener注解,在注解中标明这是哪个group的listener(使用rocketMq发送消息时需要指明group),在其中实现执行本地事务和事务执行结果查询的方法。发送消息的时候可以使用UUID等方法获取一个事务id用于记录事务的状态,便于checkLocalTransaction回查执行结果,以及在消费者端做幂等。

再以注册送积分为例介绍下执行流程:

      (1)Producer发送给用户增加积分消息至MQ Server,MQ Server将此消息标记为prepared状态,此时消费者是无法消费该消息的       (2)MQ Server回执Producer消息已经接收成功       (3)Producer执行本地事务,将用户信息存入数据库中,若事务执行成功则向MQ Server发送commit消息,事务执行失败就发送rollback消息       (4)MQ Server接收到执行结果消息后根据消息类型判断是将消息标为可消费,此时消费者就可以正常消费消息,还是将该消息丢弃       (5)当MQ Server一段时间内都没有接收到事务的执行结果时,就会利用Producer提供的回查接口,定时请求查询本地事务执行结果,通过执行结果将消息标记为可消费或丢弃

利用RocketMq的消息事务机制,将生产者本地数据库事务与发送消息到broker绑定在一起,保证当生产者本地数据修改成功之后,通知与其协作的其他服务数据修改的消息一定会被写入到broker中;之后由消费者从broker中读取消息,进行处理,在customer端需要保证幂等,避免重复进行数据处理。当消息处理完毕之后,需要写入到一张记录表中,每一次消费新消息时,都要从该表中进行查询,看它之前有没有被处理过,如果已经处理过,则直接丢弃或返回与上次执行相同的结果。如果消费者端处理消息失败,阿里给我们的建议是人工进行处理,因为如果要代码处理的话,那么就需要回滚整个操作,这个开发业务量是十分巨大的,并且这种应该是小概率事件。这种方式就极大提高了系统的可用性,提供最终一致性,那么到达最终一致性的时间越短,用户体验就越良好。

四、最大努力通知

下面以工作中保司产品对接来举例:        (1)用户在保险经纪平台页面投保,如果不是经济公司代扣而是对接方保司直接扣款,则在点击立即支付时需要调用保司支付接口,当然参数中会给出用于标示该保单的no,支付成功/失败/超时回调地址,此时会返回支付页面url        (2)当用户在支付页面完成相应操作后,保司会根据我们传递的回调地址去进行结果通知        (3)当收到通知后根据根据通知内容修改保单的状态        (4)当然还需要有一个定时任务用于主动去查询某段时间内的保单状态,避免没有收到回调通过造成保单状态不一致或检查当前保单状态是否正确       发起通知方通过一定的机制最大努力将业务处理结果通知到接收方,在上面举的例子中,保司就是通知方,保险经纪平台就是接收方。

最大努力通知有以下要求:        (1)有一定的消息重复通知机制,也就是当保司调用经济平台支付结果回调接口时如果返回了错误,则需要重复推送通知,例如失败后每隔1分钟再通知一次,超过5次后每10分钟通知一次,再超过5次后,每半个小时通知一次,最多15次;与通知方支持重复通知相对的,接收方相对的接收方也需要实现通知消息处理的幂等        (2)消息校对机制,如果尽最大努力也没有通知成功,或消息接收方有通知结果校验的机制,消息通知方需要提供一个接口用于让接收方查询操作处理的结果。  

https://www.jianshu.com/p/cc5c10221aa1

最新回复(0)