分布式事务
常见分布式事务解决方案浅谈
背景
和小伙伴讨论一个问题,员工的新建编辑页面无底薪编辑入口,但是详情页却有入口。
了解到,当时这么做主要有两个考虑:
- 两个信息不属于同一域,不应该放在也一个页面(员工属于商户底薪属于业绩)
- 如果放在一个页面,事务无法保证(新建员工成功但是底薪设置失败,员工即透出了但是又没有底薪)
问题1:我的理解,不同域的数据放在一个页面编辑我觉得没啥毛病,这里更像是因为技术问题产品上做了妥协。但今天我们主要讨论问题2,如果两个信息同时放在1个页面上编辑,应该如何实现?即创建员工接口成功但是保存底薪接口失败的问题如何解决?
类似常见问题其实很很多,最典型的场景如银行转账、下单扣库存核销权益等。
事务
在执行SQL语句的时候,某些业务要求,一系列操作必须全部执行,而不能仅执行一部分。例如,一个转账操作:张三给李四转账100元
-- 第一步:将id=1的A账户余额减去100 UPDATE accounts SET balance = balance - 100 WHERE id = 1; -- 第二步:将id=2的B账户余额加上100 UPDATE accounts SET balance = balance + 100 WHERE id = 2;
这两条SQL语句必须全部执行,或者,由于某些原因,如果第一条语句成功,第二条语句失败,就必须全部撤销。这种把多条语句作为一个整体进行操作的功能,被称为数据库事务。
数据库事务的四大特性:ACID
本地事务
begin transaction; //1.本地数据库操作:张三减少金额 //2.本地数据库操作:李四增加金额 commit transation;
分布式事务
begin transaction; //1.本地数据库操作:张三减少金额 //2.远程调用:让李四增加金额 commit transation;
当远程调用让李四增加金额成功了,由于网络问题远程调用并没有返回,此时本地事务提交失败就回滚了张三减少金额的操作,此时张三和李四的数据就不一致了。
因此在分布式架构的基础上,传统数据库事务就无法使用了,张三和李四的账户不在一个数据库中甚至不在一个应用系统里,实现转账事务需要通过远程调用,由于网络问题就会导致分布式事务问题。
理论基础
CAP
借用一下维基百科CAP理论一文中关于C、A、P三者的定义。
Consistency : Every read receives the most recent write or an error
Availability : Every request receives a (non-error) response – without the guarantee that it contains the most recent write
Partition tolerance : The system continues to operate despite an arbitrary number of messages being dropped (or delayed) by the network between nodes
cap是 Consistency、Availability、Partition tolerance 三个单词的缩写,分别表示一致性、可用性、分区容忍性。
CAP 理论告诉我们一个分布式系统最多只能同时满足一致性(Consistency)、可用性(Availability)和分区容忍性(Partition tolerance)这三项中的两项。
- AP
放弃一致性,追求分区容忍性和可用性。这是很多分布式系统设计时的选择。
通常实现 AP 都会保证最终一致性,后面将的 BASE 理论就是根据 AP 来扩展的,一些业务场景比如:订单退款,今日退款成功,明日账户到账,只要用户可以接受在一定的时间内到账即可。 - CP
放弃可用性,追求一致性和分区容错性,zookeeper 其实就是追求的强一致,又比如跨行转账,一次转账请求要等待双方银行系统都完成整个事务才算完成。 - CA
放弃分区容忍性,即不进行分区,不考虑由于网络不通或结点挂掉的问题,则可以实现一致性和可用性。那么系统将不是一个标准的分布式系统,架构如下:
BASE
BASE 是 Basically Available(基本可用)、Soft state(软状态)和 Eventually consistent (最终一致性)三个短语的缩写。BASE 理论是对 CAP 中 AP 的一个扩展,通过牺牲强一致性来获得可用性,当出现故障允许部分不可用但要保证核心功能可用,允许数据在一段时间内是不一致的,但最终达到一致状态。满足BASE理论的事务,我们称之为“柔性事务”。
- 基本可用:分布式系统在出现故障时,允许损失部分可用功能,保证核心功能可用。如电商网站交易付款出现问题了,商品依然可以正常浏览。
- 软状态:由于不要求强一致性,所以BASE允许系统中存在中间状态(也叫软状态),这个状态不影响系统可用性,如订单的"支付中"、“数据同步中”等状态,待数据最终一致后状态改为“成功”状态。
- 最终一致:最终一致是指经过一段时间后,所有节点数据都将会达到一致。如订单的"支付中"状态,最终会变 为“支付成功”或者"支付失败",使订单状态与实际交易结果达成一致,但需要一定时间的延迟、等待。
刚性事务和柔性事务
- 刚性事务(传统):遵循ACID原则,强一致性。
- 柔性事务:遵循BASE理论,最终一致性;与刚性事务不同,柔性事务允许一定时间内,不同节点的数据不一致,但要求最终一致。
解决方案
2PC
Two-phase commit protocol,中文叫二阶段提交,是一种强一致性设计。是将整个事务流程分为两个阶段,准备阶段(Prepare phase)、提交阶段(commit phase)。过程涉及协调者和参与者。
成功情况
失败情况
2PC的传统方案(XA)是在数据库层面实现的,如 Oracle、MySQL 都支持 2PC 协议,为了统一标准减少行业内不必要的对接成本,需要制定标准化的处理模型及接口标准,国际开放标准组织 Open Group 定义了分布式事务处理模型DTP(Distributed Transaction Processing Reference Model)。
DTP 模型定义如下角色:
- AP(Application Program):即应用程序,可以理解为使用 DTP 分布式事务的程序。
- RM(Resource Manager):即资源管理器,可以理解为事务的参与者,一般情况下是指一个数据库实例,通过资源管理器对该数据库进行控制,资源管理器控制着分支事务。
- TM(Transaction Manager):事务管理器,负责协调和管理事务,事务管理器控制着全局事务,管理事务生命周期,并协调各个 RM。全局事务是指分布式事务处理环境中,需要操作多个数据库共同完成一个工作,这个工作即是一个全局事务。
下面以新用户注册送积分为例来说明:
DTP 模型定义TM和RM之间通讯的接口规范叫 XA,简单理解为数据库提供的 2PC 接口协议,基于数据库的 XA 协议来实现 2PC 又称为 XA 方案
执行流程如下:
- 应用程序(AP)持有用户库和积分库两个数据源。
- 应用程序(AP)通过 TM 通知用户库 RM 新增用户,同时通知积分库RM为该用户新增积分,RM 此时并未提交事务,此时用户和积分资源锁定。
- TM 收到执行回复,只要有一方失败则分别向其他 RM 发起回滚事务,回滚完毕,资源锁释放。
- TM 收到执行回复,全部成功,此时向所有 RM 发起提交事务,提交完毕,资源锁释放。
2PC存在的问题:
- 单点故障:事务的发起、提交还是取消,均是由老大协调者管理的,只要协调者宕机,那就凉凉了。
- 同步阻塞缺点:从上面介绍以及例子可看出,我们的参与系统中在没收到老大的真正提交还是取消事务指令的时候,就是锁定当前的资源,并不真正的做些事务相关操作,所以,整个分布式系统环境就是阻塞的。
- 数据不一致缺点:就是说在老大协调者向小弟们发送真正提交事务的时候,部分网路故障,造成部分系统没收到真正的指令,那么就会出现部分提交部分没提交,因此,这就会导致数据的不一致。
3PC
三阶段提交协议(Three-phase commit protocol),包含CanCommit 阶段、PreCommit 阶段、DoCommit 阶段,是二阶段提交(2PC)的改进版本。与两阶段提交不同的是,三阶段提交有两个改动点:
- 引入超时机制。同时在协调者和参与者中都引入超时机制。
- 在第一阶段和第二阶段中插入一个准备阶段。保证了在最后提交阶段之前各参与节点的状态是一致的。
即 3PC 把 2PC 的准备阶段再次一分为二,这样三阶段提交就有 CanCommit、PreCommit、DoCommit 三个阶段。当 CanCommit、PreCommit、DoCommit的任意一个步骤失败或者等待超时,执行RollBack。
超时和预提交机制。
TCC
关于 TCC(Try-Confirm-Cancel)的概念,最早是由 Pat Helland 于 2007 年发表的一篇名为《Life beyond Distributed Transactions:an Apostate’s Opinion》的论文提出。
TCC 指的是Try - Confirm - Cancel。
- Try 指的是预留,即资源的预留和锁定,注意是预留。
- Confirm 指的是确认操作,这一步其实就是真正的执行了。
- Cancel 指的是撤销操作,可以理解为把预留阶段的动作撤销了。
成功
失败
2PC 和 3PC 都是数据库层面的,而 TCC 是业务层面的分布式事务,就像我前面说的分布式事务不仅仅包括数据库的操作,还包括发送短信等,这时候 TCC 就派上用场了。TCC 事务机制相比于 XA的2PC,解决了其几个缺点:
- 解决了协调者单点,由主业务方发起并完成这个业务活动。业务活动管理器也变成多点,引入集群。
- 同步阻塞:引入超时,超时后进行补偿,并且不会锁定整个资源,将资源转换为业务逻辑形式,粒度变小。
- 数据一致性,有了补偿机制之后,由业务活动管理器控制一致性
可靠消息(本地消息表)
本地消息表这个方案最初是 ebay 架构师 Dan Pritchett 在 2008 年发表给 ACM 的文章。该方案中会有消息生产者与消费者两个角色,假设系统 A 是消息生产者,系统 B 是消息消费者,其大致流程如下:
- 当系统 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 次,最后还是不行就放弃。
最大努力通知与可靠消息一致性有什么不同?
- 解决方案思想不同
可靠消息一致性,发起通知方需要保证将消息发出去,并且将消息发到接收通知方,消息的可靠性关键由发起通知方来保证。最大努力通知,发起通知方尽最大的努力将业务处理结果通知为接收通知方,但是可能消息接收不到,此时需要接 收通知方主动调用发起通知方的接口查询业务处理结果,通知的可靠性关键在接收通知方。 - 两者的业务应用场景不同
可靠消息一致性关注的是交易过程的事务一致,以异步的方式完成交易。最大努力通知关注的是交易后的通知事务,即将交易结果可靠的通知出去。 - 技术解决方向不同
可靠消息一致性要解决消息从发出到接收的一致性,即消息发出并且被接收到。最大努力通知无法保证消息从发出到接收的一致性,只提供消息接收的可靠性机制。可靠机制是,最大努力的将消息通知给接收方,当消息无法被接收方接收时,由接收方主动查询消息(业务处理结果)
Saga
Saga是分布式事务领域最有名气的解决方案之一,最初出现在1987年Hector Garcaa-Molrna & Kenneth Salem发表的论文SAGAS里。
Saga是由一系列的本地事务构成。每一个本地事务在更新完数据库之后,会发布一条消息或者一个事件来触发Saga中的下一个本地事务的执行。如果一个本地事务因为某些业务规则无法满足而失败,Saga会执行在这个失败的事务之前成功提交的所有事务的补偿操作。
Saga的实现有很多种方式,其中最流行的两种方式是:
- 基于事件的方式。这种方式没有协调中心,整个模式的工作方式就像舞蹈一样,各个舞蹈演员按照预先编排的动作和走位各自表演,最终形成一只舞蹈。处于当前Saga下的各个服务,会产生某类事件,或者监听其它服务产生的事件并决定是否需要针对监听到的事件做出响应。
- 基于命令的方式。这种方式的工作形式就像一只乐队,由一个指挥家(协调中心)来协调大家的工作。协调中心来告诉Saga的参与方应该执行哪一个本地事务。
实践框架
Seata
Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。
官网:https://seata.io/zh-cn/docs/overview/what-is-seata.html
在用列表:https://github.com/seata/seata/issues/1246
RocketMQ
总结
参考:
https://zhuanlan.zhihu.com/p/263555694
https://seata.io/zh-cn/docs/overview/what-is-seata.html
https://xiaomi-info.github.io/2020/01/02/distributed-transaction/