两阶段提交(2PC, Two-phase Commit)
两阶段提交又称2PC,2PC是一个非常经典的中心化的原子提交协议,这里所说的中心化是指协议中有两类节点:
- 中心化协调者节点(coordinator)
- N个参与者节点(partcipant)
两个阶段:
- 投票阶段
- 提交/执行阶段
举例:订单服务需要调用支付服务去支付,支付成功则处理购物订单为待发货状态,否则就需要将购物订单处理成失败状态
第一阶段:投票阶段
第一阶段主要分为3步:
- 事务询问: 协调者向所有的参与者发送事务预处理请求,称之为Prepare,并开始等待各参与者的响应
- 执行本地事务: 各个参与者节点执行本地事务操作,但在执行完成后并不会真正提交数据库本地事务, 而是先向协调者报告能否处理
- 各参与者向协调者反馈事务询问的响应: 如果参与者成功执行了事务操作,那么就反馈给协调者Yes响应,表示事务可以执行,如果没有参与者 成功执行事务,那么就反馈给协调者NO响应,表示事务步不可以执行
第一阶段执行完后,会有两种可能:
- 所有都返回Yes(成功流程)
- 有一个或者多个返回No(异常流程)
第二阶段:提交/执行阶段(成功流程)
前提条件: 所有参与者都返回Yes
成功流程主要分为两步:
- 所有的参与者反馈给协调者的信息都是Yes,那么就会执行事务提交,协调者向所有参与者节点发出Commit请求
- 事务提交,参与者收到Commit请求之后,就会正式执行本地事务Commit操作,并在完成提交之后释放整个事务执行期间占用的事务资源
第三阶段:提交/执行阶段(异常流程)
前提条件: 有一个或者多个返回No
异常流程也分为两步
- 发送回滚请求,协调者向所有参与者节点发出rollback请求
- 事务回滚,参与者接收到rollback请求后,会回滚本地事务
2PC缺点
- rollback 请求无论是在第一阶段的过程中,还是在第二阶段,所有的参与者资源和协调者资源都是被锁住的,只有当所有的节点准备完毕,事务协调者才会通知进行全局提交,参与者进行本地事务提交后才会释放资源.这样过程会比较漫长,对性能影响比较大
- 单节点故障: 由于协调者的重要性,一旦协调者发生故障.参与者会一直阻塞下去,尤其在第二阶段,协调者发生故障,那么所有参与者还都处于锁定事务资源的状态中,而无法继续完成事务操作(虽然协调者挂掉,可以重新选举一个协调者,但是无法解决因为协调者宕机导致参与者处于阻塞状态的问题)
- 数据不一致: 在第二阶段中,当协调者向参与者发送commit请求之后,发生了局部网络异常,或者在发送commit请求过程中,协调者发生了故障,这回导致只有一部分参与者接收到了commit请求,而在这部分参与者接到commit请求之后就会执行commit操作,但是其他部分未接到commit请求的节点则无法进行事务提交,于是整个分布式系统便出现了数据不一致的现象
TCC 补偿模式
TCC又称补偿事务,其核心思想是:“针对每个操作都要注册一个与其对应的确认和补偿(撤销)操作”.它分为三个操作:
- Try阶段: 主要是对业务系统做检测及资源预留。
- Confirm阶段: 确认执行业务操作
- Cancel阶段: 取消执行业务操作
Confirm阶段和Cancel阶段是或者的关系.
一个订单支付之后,我们需要做下面的步骤:
- 更改订单的状态为已支付
- 扣减商品库存
- 给会员增加积分
如果库存服务扣减库存失败,直接通知订单服务异常,订单服务撤销修改订单的操作,然后通知积分服务撤销增加积分操作
如果一切正常就确认执行业务操作
总结
- 如果需要使用TCC分布式事务的话,首先需要选择某种TCC分布式事务框架,各个服务就会有这个TCC分布式事务框架在运行
- 然后原本的一个接口,要改造为3个逻辑,Try-Confirm-Cancel
- 先是服务调用链路依次执行Try逻辑
- 如果都正常的话,TCC分布式事务框架推进Confirm逻辑,完成整个事务
- 如果某个服务的Try逻辑有问题,TCC分布式事务框架感知到之后就会推进各个服务的Cancel逻辑,撤销之前执行的各种操作
- 这就是所谓的TCC分布式事务
- java中阿里开源的seata是支持TCC模式的,golang中有民间大神开源出来的seata-go也是支持TCC模式的
思考
- 如果有一些意外情况发生了,比如订单服务突然挂了,然后再次重启,TCC分布式事务框架是如何保证之前没执行完的分布式事务继续执行的呢?
- TCC事务框架都是要记录一些分布式事务的活动日志的,可以在磁盘的日志文件里记录,也可以在数据库里记录,保存下来分布式事务运行的各个阶段和状态,重启后如果发现有没有执行完的事务,会继续执行的
- 万一某个Cancel或者Confirm逻辑执行一直失败怎么办?
- TCC事务框架会通过活动日志记录各个服务的状态,如果某个服务的Cancel或者Confirm一直没成功,会不停的重试调用它的Cancel或者Confirm逻辑,务必要它成功,正常情况下,保证充足的测试,Confirm和Cancel都是可以成功的,如果实在解决不了,也属于一个小概率事件,可以发邮件通知人工处理
基于可靠消息最终一致性方案
目前RocketMQ支持事务消息,以创建订单为例:
- 生产者订单系统先发送一条half消息到Broker,half消息对消费者而言是不可见的
- 再创建订单,根据创建订单成功与否,向Broker发送commit和rollback
- 并且生产者订单系统还可以提供Broker回调接口,当Broker发现一段时间half消息没有收到任何操作命令(即没有commit也没有rollback),则会主动调用此接口来查询订单是否创建成功
- 一旦half消息commit了,消费者库存系统就会来消费,如果消费成功,则消息销毁,分布式事务成功结束
- 如果消费失败,则根据重试策略进行重试,最后还失败则进入私信队列,等待进一步处理
基于本地消息表实现最终一致性
以创建订单为例:
创建订单时,将减库存消息加入再本地事务中,一起提交到数据库存入本地消息表,然后调用库存系统,如果调用成功则修改本地消息状态为成功,如果调用库存系统失败,则由后台定时任务从本地消息表中取出未成功的消息,重试调用库存系统