分布式事务详解:分段提交与最终一致性
一、分布式事务简介
1、转账经典案例
跨地区和机构的转账的业务在实际生活中非常常见,基础流程如下:
账户 01 通过一系列服务和支付的流程,把钱转入账户 02,在这一过程中,如果账户 01 出现出账成功,但是账户 02 没有入账,这就导致数据不一致,违反了基本的事务原则。基于数据归属在不同服务和不同的数据库中,这种情况下的事务出错被称为分布式事务问题。
2、基本概念
分布式事务是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。
如上的转账案例,看似只有一次的转账操,实际上由不同的服务不同数据库的多个细节操作组成,这些无感知的细节操作分布在不同服务上,甚至属于不同的地区和应用,如何保证这些操作全部成功或者全部失败,即保证不同数据库间的数据一致性,这就是分布式事务需要解决的核心问题。
3、分布式事务特点
基于如下电商业务场景,基本分布式的架构思路:
数据库基于业务特点,进行分库分表;
数据库拆分,随之就是业务的服务化(SOA);
基于电商业务进行拆分,会出现常见的:订单,用户,库存,物流等一系列的服务,管理不同的业务数据库,在实际的下单支付应用场景下,需要同时操作用户,订单,库存等多个服务,就必须保证数据一致性,下单支付成功,库存必须就需要用到分布式事务。
二、CAP 基础理论
1、基础简介
说到分布式事务问题,必然会说下 CAP 理论,分布式系统的三大指标:
Consistency:一致性
单个事务执行更新写操作,操作结束成功返回,在同一时间的其他事务读取的数据完全一致,不存在中间状态。在分布式的系统中描述:用户下单支付,扣款,减库存,生成物流,必须一致。例如限量打折促销中,用户下单后库存没减少,这就导致不一致问题。
Availability:可用性
服务必须一直处于可用的状态,收到用户的请求,服务器必须在有限的时间给出回应,不管结果是处理成功或者处理失败。
Partition tolerance:分区容错
通俗说,在分布式系统中,一个流程里可能出现某个服务出错情况,这是无法绝对避免的,在程序设计上要能容忍这种错误发生。
2、CP 和 AP 模式
分布式系统很难同时满足一致性、可用性、分区容错性三个特点,在大部分的系统架构中,都会选择 CP 或者 AP 模式,即需要抛弃一个特点,说明一点,为何 P 没有抛弃,对于分布式系统而言,分区容错是该架构模式下的基本原则,不同的 SOA 服务和数据库是比如会被部署到不同的节点下。所以如何解决 C(一致性)和 A(可用性)就成分布式系统的最大痛点。
为何不能同时满足 C 和 A,这也是基于分布式架构特点看,不同服务直接不能保证通信是 100%成功,一旦出现失败情况,一致性和可用性就无法满足。
既然强一致性无法保证,那退一步,给处理时间,最后结果保证一致性,也可以,这就涉及到 BASE 理论。
三、BASE 基础理论
1、基础简介
BASE 理论是由 eBay 公司的架构师提出的,主要是对上述的 CAP 理论中一致性和可用性做的权衡结果,基于 CAP 定律逐步演化而来,核心思想;即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当策略实现数据的最终一致性。
Basically Available:基本可用
分布式系统在发生故障的时,允许损失部分可用性。例如常见电商清仓甩卖时,为保证主业务可以,一些不重要的服务直接降级提示。
Soft State:软状态
允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性。相对于原子性而言,要求多个节点的数据副本都是一致的,这是一种硬状态。
Eventual Consistency:最终一致
强调的数据更新操作,即软状态必须有个时间期限,在经过一段时间的同步之后,最终都能够达到一个一致的状态。因此,最终一致性的本质是需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性。时间期限长短取决于延时、负载、数据同步等各种因素。
BASE 理论提出是基于大规模高可用可扩展的分布式系统架构,不同于关系型数据库事务特点(ACID)的强一致性模型,通过牺牲强一致性来获取更高的可用性,并允许数据在一段时间内是不一致的,但最终达到一致状态。实际的业务场景下事物(ACID)基本特性和 BASE 理论也是要权衡考虑。
2、柔性事务
遵循 BASE 理论,利用业务特点,在指定期限内让事务保持最终一致性,柔性事务是一种思想,从根本上看,就是业务模式对于事务过程中不一致性有一定的容忍度,可以留出足够的时间执行事务最终一致的方法。
3、PAXOS 算法
Paxos 算法一种保障分布式系统最终一致性的共识算法,利用的是选举策略,少数服从多数的思想。PAXOS 不要求对所有节点做实时同步,实质上是考虑到了分区情况下的可用性,通过减少完成一次事务需要的参与者个数,来保障系统的可用性。
例如:N 个服务节点,有(N/2)+1 个节点达成共识,则认为系统达到了一致,并且按照 Paxos 原则,最终理论上也达到了一致,不会再改变,如此一来,只要保证有半数以上的服务存活,允许小部分服务挂掉,客户可以与大部分服务节点通信,那么就不会影响整体操作流程,也不需确保服务器全部处于工作状态,容错性非常好。操作影响的数据和结果随后会被异步的同步到其他节点上,从而保证最终一致性。
四、电商场景分析
1、场景描述
分布式事务在业务系统中是十分常见的,最经典的场景就是电商架构中的交易业务,如图:
客户端通过请求订单服务,执行下单操作,实际上从订单服务上又触发了多个服务链请求,基本步骤如下:
客户端请求在订单服务上创建订单;
订单服务调用账户服务扣款;
订单服务调用库存服务执行库存扣减;
订单通过物流服务,转化为物流运单;
这套流程在电商系统中是基本业务,在实际的开发中远比这里描述的复杂。
2、服务时序图
上述 1 中是业务性的流程概念描述,从系统开发层面,在微服务的架构模式下,通常的时序流如下:
这样服务间的通信时序图在程序设计中十分常见,在分布式系统中,清楚的描述各个服务间的通信流程是十分关键的。
上图描述的交易流程是在最理想的状态下,各个服务都执行成功,但是程序是不能 100%保证一直正常,经常出现如下情况:
服务间通信失败;
单个节点服务宕掉;
服务接口执行失败;
这些都是实际开发中经常出现的问题,比如订单创建成功,扣款成功,但是库存扣减失败,物流运单生成,那么这笔订单该如何处理?这就是分布式事务要解决的核心问题。
分布式事务机制要保证不同服务之间形成一个整体性的可控的事务,业务流程上的服务除非全部成功,否则任何服务的操作失败,都会导致所有服务上操作回滚,撤销已经完成的动作。
五、TCC 基础概念
1、分段提交协议
XA 是一个分布式事务协议,大致分为两部分:事务管理器和本地资源管理器,本地资源管理器基本由数据库实现,大多数关系型数据库都实现 XA 接口,而事务管理器作为全局事务的调度者,负责整个事务中本地资源的提交和回滚,基本原理如下:
阶段 1:事务询问
事务管理器向所有的参与事务的资源管理器发送确认请求,询问是否可以执行事务提交操作,并等待各参与者的响应,如果执事务操作成功,就反馈给事务管理器表示事务可以执行,如果没有成功执行事务,就反馈事务不可以执行;
阶段 2:事务提交
XA 根据第一阶段每个资源管理器是否都准备提交成功,判断是要事务整体提交还是回滚,正式执行事务提交操作,并在完成提交之后释放整个事务占用的资源;事务也会存在失败情况,导致流程取消回滚;
XA 事务具有强一致性,在两阶段提交的整个过程中,一直会持有资源的锁,性能不理想的缺点很明显,特别是在交易下单链路中,往往并发量很高,XA 无法满足该类高并发场景。
2、TCC 概念简介
Try(预处理)-Confirm(确认)-Cancel(取消)模式的简称 TCC。
Try 阶段
业务检查(一致性)及资源预留(隔离),该阶段是一个初步操作,提交事务前的检查及预留业务资源完成;例如购票系统中的占位成功,需要在 15 分钟内支付;
Confirm 阶段
确认执行业务操作,不在执行任何业务检查,基于 Try 阶段预留的业务资源,从理想状态下看只要 Try 成功,Confirm 也会成功,因为资源的检查和锁定都已经成功;该阶段出现问题,需要重试机制或者手动处理;购票系统中的占位成功并且 15 分钟内支付完成,购票成功;
Cancel 阶段
Cancel 阶段是在业务执行错误需要回滚到状态下执行分支事务的取消,预留资源的释放;购票系统中的占位成功但是 15 分钟内没有支付,取消占位;
3、TCC 对比 XA
XA 事务的强一致性,导致资源层的锁定;
TCC 在业务层面追求最终一致性,不会长久占用资源;
六、分段事务分析
现在回到模块一中的场景案例,在理想状态下流程全部成功是好的,但实际情况是突发情况很多,基于 TCC 模式分析上述电商的具体业务:
1、资源预留
在 TCC 模式下,通常表字段的状态设计思路为:订单(支付中.已支付.取消订单),账户(金额.冻结金额),库存(库存.冻结库存),物流(出库中.已出库,已撤回),这种状态管理在开发中非常常见。
所以在 TCC 模式里通常会如下处理资源预留:
假设订单总额为:200,状态:支付中,则此时资源预留情况如下:
tc_account 账户表:tc_total=1000,tc_ice=200,总金额 1000,冻结 200;
tc_inventory 库存表:tc_total=100,tc_ice=20,总库存 100 件,冻结 20 件;
tc_waybill 运单表:tc_state=1,运单状态,出库中;
这样下单链路上的相关资源已检查并且预留成功;
2、资源提交确认
资源预留成功之后,执行资源提交执行:
tc_account 账户表:tc_total=800,tc_ice=0,即订单扣款成功;
tc_inventory 库存表:tc_total=80,tc_ice=0,库存消减成功;
tc_waybill 运单表:tc_state=2,运单状态,已出库;
这样下单链路上的相关资源已全部提交处理成功,这是最理想的状态;
3、失败回滚
整个过程是可能执行失败的,或者用户直接自己发起回退,则要回滚整个链路上的数据:
tc_account 账户表:tc_total=1000,tc_ice=0,取消账户冻结的 200;
tc_inventory 库存表:tc_total=100,tc_ice=0,取消库存冻结的 20 件;
tc_waybill 运单表:tc_state=3,运单状态,已撤回;
这样下单链路上的相关数据都基于该笔订单做回退操作,恢复;
4、补偿机制
整个电商交易流程,不管是成功,还是完整的回退失败,都是需要在理想状态下,要求整个服务链路和数据是绝对正常的才行。但是在实际分布式架构下是很难保证的,所以在产品的设计上会预留很多操作入口,用来手动做事务补偿或回退操作:
大型复杂的业务系统中,直接修改数据库通常情况下是不允许的,一般核心流程会预留各种操作入口,用来处理突发状况,弥补数据的完整性,例如交易链路上,只要扣款成功,后续的数据无论如何都会补上,是不允许回滚的,当然如果没有扣款成功,订单有效期结束,该笔交易也就算做结束。
通过电商交易的案例,和 TCC 模式的概念,描述了分布式事务的流程和处理思路,在开发时通常会选择现有的分布式组件来具体实现事务控制。
七、最大努力通知
TCC 分段提交适用分布式架构中对一致性、实时性要求较高的业务场景,在实际业务中也存在实时性比较低的业务,例如常见的短信通知,客户端消息,运营体系更新等业务,这时候为了减轻核心流程的复杂度和压力,可以采取最大努力通知方式实现柔性事务的管理。
例如常见的第三方支付业务中,本地业务和支付端业务处理完成之后都会生成消息通知,基本流程如下:
本地业务预处理完成之后;
请求第三方支付服务;
支付操作成功对该账号发送消息;
支付服务回调本地业务;
本地业务生成系统通知消息;
上述流程的消息场景中有一些基础特点,在核心业务处理完成之后,发送消息通知,允许失败,在指定时间段内或者指定重试次数之后,允许消息丢失情况存在,即消息的不可靠性。
在实际的支付系统中,启动每日对账校验时会对当日的流水做校验,如果发现支付流水有未完成的流程,会有状态弥补,后续可以继续处理,这种手段在对账中很常用。
八、可靠消息
分布式事务基于可靠消息最终一致性的实现方案,既然是可靠消息,则要求 MQ 必须支持事务管理,这样才能保证业务前后一致性。
1、RocketMQ 事务消息
RocketMQ 在 4.3 版中开始支持分布式事务消息,采用 2PC 的思想来实现了提交事务消息,同时增加一个补偿逻辑来处理二阶段超时或者失败的消息,如下图所示:
上图说明了事务消息的大致方案,其中分为两个流程:正常事务消息的发送及提交、事务消息的补偿流程。
1.1 发送及提交
(1)发送消息(half 消息,即发送但不被消费);
(2)服务端响应消息写入结果;
(3)根据发送结果执行本地事务,如果写入失败,此时 half 消息对业务不可见,本地逻辑不执行;
(4) 根据本地事务状态执行 Commit 或者 Rollback(Commit 操作生成消息索引,消息对消费者可见)
1.1 补偿流程
(1)对没有 Commit/Rollback 的事务消息(pending 状态的消息),从服务端发起一次“回查”;
(2)Producer 收到回查消息,检查回查消息对应的本地事务的状态;
(3)根据本地事务状态,重新 Commit 或者 Rollback;
其中,补偿阶段用于解决消息 Commit 或者 Rollback 发生超时或者失败的情况。
1.3 设计原理
在 RocketMQ 事务消息的主要流程中,一阶段的消息如何对用户不可见。其中,事务消息相对普通消息最大的特点就是一阶段发送的消息对用户是不可见的。那么,如何做到写入消息但是对用户不可见呢?RocketMQ 事务消息的做法是:如果消息是 half 消息,将备份原消息的主题与消息消费队列,然后改变主题为 RMQ_SYS_TRANS_HALF_TOPIC。由于消费组未订阅该主题,故消费端无法消费 half 类型的消息,然后 RocketMQ 会开启一个定时任务,从 Topic 为 RMQ_SYS_TRANS_HALF_TOPIC 中拉取消息进行消费,根据生产者组获取一个服务提供者发送回查事务状态请求,根据事务状态来决定是提交或回滚消息。
2、最终一致性
基于上述 RocketMQ 事务消息可靠性的特点,即可以实现某类业务下事务的最终一致性。消息发送一致性是指产生消息的业务动作与消息发送一致,也就是说如果业务操作成功,那么由这个业务操作所产生的异步消息一定要发送出去,否则就业务失败回滚,消息也会丢弃。
流程基本如下:
发送 half 事务消息,无法被消费;
本地业务代码逻辑处理完成;
发送确认消息,标识该消息可以消费;
如果消息生产方异常,取消整体动作;
该流程主要针对消息生产方,在实际开发中,消息的消费方也一样很难处理,要保证最终一致性,必然会面对一个问题,消费方异常,消息不断的重试,可能存在部分业务处理成功,部分业务处理失败的情况,这时候就要解决服务接口的幂等性问题。
九、幂等接口
1、幂等简介
编程中一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。就是说,一次和多次请求某一个资源会产生同样的作用影响。
在复杂的异步流程中,尤其注意失败重试问题,通常支付流程中,每次接口被请求,对每一步数据更新的操作,都会前置一步状态查询的流程,用来判断下一步的数据更新是否该执行。
2、幂等接口
在系统服务接口请求中,任何明确的接口响应,例如失败或成功,这样业务流程都好处理,但是例如支付场景如果请求超时,如何判断服务的结果状态:客户端请求超时,本地服务超时,请求支付超时,支付回调超时,客户端响应超时等,或者基于 MQ 的不断重试机制,在部分业务异常状态下,始终没有返回成功,则消息会一直重试。
这就需要设计流程化的状态管理,尤其在消息重试机制下,很少会再次对重试的业务接口使用重度的事务控制,有些业务被执行完毕,只需要判断一个状态,下次消息重试跳过即可,只需要把未处理的业务补偿处理即可,在重试机制下,在部分业务没有全部执行成功之前,消息会一直重试,直到最终全部完成。
十、参考源码
版权声明: 本文为 InfoQ 作者【知了一笑】的原创文章。
原文链接:【http://xie.infoq.cn/article/5f344c5f1db0c39c235fc4561】。文章转载请联系作者。
评论