分布式事务解决方案
概念
分布式事务
分布式事务就是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点上。如下图所示,一次下单操作需要调用优惠券服务扣减优惠券和商品服务减少库存,分布式事务需要保证这些小操作要么全部成功,要么全部失败。本质上来说,分布式事务就是为了保证不同数据库的数据一致性。
强一致性、弱一致性、最终一致性
- 强一致性任何一次读都能读到某个数据的最近一次写的数据。系统中的所有进程,看到的操作顺序,都和全局时钟下的顺序一致。简言之,在任意时刻,所有节点中的数据是一样的。
- 弱一致性:数据更新后,如果能容忍后续的访问只能访问到部分或者全部访问不到,则是弱一致性。
- 最终一致性:不保证在任意时刻任意节点上的同一份数据都是相同的,但是随着时间的迁移,不同节点上的同一份数据总是在向趋同的方向变化。简单说,就是在一段时间后,节点间的数据会最终达到一致状态。
BASE理论
BASE 理论指的是基本可用 Basically Available,软状态 Soft State,最终一致性 Eventual Consistency,核心思想是即便无法做到强一致性,但应该采用适合的方式保证最终一致性。
BASE,Basically Available Soft State Eventual Consistency 的简写:BA:Basically Available 基本可用,分布式系统在出现故障的时候,允许损失部分可用性,即保证核心可用。S:Soft State 软状态,允许系统存在中间状态,而该中间状态不会影响系统整体可用性。E:Consistency 最终一致性,系统中的所有数据副本经过一定时间后,最终能够达到一致的状态。BASE 理论本质上是对 CAP 理论的延伸,是对 CAP 中 AP 方案的一个补充。
柔性事务
不同于 ACID 的刚性事务,在分布式场景下基于 BASE 理论,就出现了柔性事务的概念。要想通过柔性事务来达到最终的一致性,就需要依赖于一些特性,这些特性在具体的方案中不一定都要满足,因为不同的方案要求不一样;但是都不满足的话,是不可能做柔性事务的。
解决方案
业务整合接口,避免分布式事务
将一个业务流程中需要在一个事务里执行的多个相关业务接口包装整合到一个事务中,这属于“就具体问题具体分析”的做法。就问题场景来说,可以将服务A、B、C整合为一个服务D来实现单一事务的业务流程服务。如果在项目一开始就考虑到分布式事务的复杂问题,则采用这里的方案,精心规划和设计系统,避免分布式事务;对于实在不能避免的,则采用其他措施去解决,这应该是最好的做法。
两阶段提交/XA
XA协议是由X/Open组织提出的分布式事务处理规范,主要定义了事务管理器TM和局部资源管理器RM之间的接口。目前主流的数据库,比如oracle、DB2、mysql都是支持XA协议的。
XA把整个事务提交分为prepare和commit两个阶段:
- 事务协调者向事务参与者发送prepare请求,事务参与者收到请求后,如果可以提交事务,回复yes,否则回复 no。
- 如果所有事务参与者都回复了yes,事务协调者向所有事务参与者发commit请求,否则发送rollback请求。
XA会导致以下几个问题:
- 同步阻塞,本地事务在 prepare 阶段锁定资源,如果有其他事务也要操作,就必须等待前面的事务完成。这样就造成了系统性能下降。
- 协调节点单点故障,如果第一个阶段prepare成功了,但是第二个阶段协调节点发出commit指令之前宕机了,所有服务的数据资源处于锁定状态,事务将无限期地等待。
- 数据不一致,如果第一阶段prepare成功了,但是第二阶段协调节点向某个节点发送commit命令时失败,就会导致数据不一致。
TCC(Try Confirm Cancel)
过程图如下:
Try阶段:尝试执行,完成所有业务检查(一致性), 预留必须业务资源(准隔离性)
Confirm阶段:确认执行真正执行业务,不作任何业务检查,只使用Try阶段预留的业务资源,Confirm操作满足幂等性。要求具备幂等设计,Confirm失败后需要进行重试。
Cancel阶段:取消执行,释放Try阶段预留的业务资源Cancel操作满足幂等性Cancel阶段的异常和Confirm阶段异常处理方案基本上一致。
优点:
- 它把事务运行过程分成 Try、Confirm/Cancel 两个阶段
- 每个阶段由业务代码控制,这样事务的锁力度可以完全自由控制
- 不存在资源阻塞的问题,每个方法都直接进行事务的提交
缺点
- 在业务层编写代码实现的两阶段提交,原本一个方法,现在却需要三个方法来支持
- 对业务的侵入性很强,不能很好的复用
事务消息
事务消息发送分为两个阶段。第一阶段会发送一个半事务消息,半事务消息是指暂不能投递的消息,生产者已经成功地将消息发送到了 Broker,但是Broker 未收到生产者对该消息的二次确认,此时该消息被标记成“暂不能投递”状态,如果发送成功则执行本地事务,并根据本地事务执行成功与否,向 Broker 半事务消息状态(commit或者rollback),半事务消息只有 commit 状态才会真正向下游投递。如果由于网络闪断、生产者应用重启等原因,导致某条事务消息的二次确认丢失,Broker 端会通过扫描发现某条消息长期处于“半事务消息”时,需要主动向消息生产者询问该消息的最终状态(Commit或是Rollback)。这样最终保证了本地事务执行成功,下游就能收到消息,本地事务执行失败,下游就收不到消息。总而保证了上下游数据的一致性。
整个事务消息的详细交互流程如下图所示:
RocketMQ实现了事务消息。
本地消息表
- 系统被其他系统调用发生数据库表更操作,首先会更新数据库的业务表,其次会往相同数据库的消息表中插入一条数据,两个操作发生在同一个事务中,以图中商品服务为例,当调用扣减库存接口时会 先执行扣减库存,然后在消息表添加一条库存锁定记录,定时任务会定时扫描订单是否支付成功,如果支付成功将记录改为成功。
实现
XA/AT/TCC实现
Seata支持这3种,可参考 官方Demo
事务消息
本地消息表
以下单流程为例,下单需要创建订单、扣减库存、扣减优惠券等操作。以扣减优惠券为例,流程图如下,扣减库存与优惠券类似,不再过多阐述。
订单模块伪代码如下:
//验证价格,减去商品优惠券
this.checkPrice(orderItemList,orderRequest);
//锁定优惠券
this.lockCouponRecords(orderRequest ,orderOutTradeNo );
//锁定库存
this.lockProductStocks(orderItemList,orderOutTradeNo);
//创建订单
ProductOrderDO productOrderDO = this.saveProductOrder(orderRequest,loginUser,orderOutTradeNo,addressVO);
//创建订单项
this.saveProductOrderItems(orderOutTradeNo,productOrderDO.getId(),orderItemList);
//发送延迟消息,用于自动关单
OrderMessage orderMessage = new OrderMessage();
orderMessage.setOutTradeNo(orderOutTradeNo);
rabbitTemplate.convertAndSend(rabbitMQConfig.getEventExchange(),rabbitMQConfig.getOrderCloseDelayRoutingKey(),orderMessage);
//创建支付
PayInfoVO payInfoVO = new PayInfoVO(orderOutTradeNo,
productOrderDO.getPayAmount(),orderRequest.getPayType(),
orderRequest.getClientType(), orderItemList.get(0).getProductTitle(),"",TimeConstant.ORDER_PAY_TIMEOUT_MILLS);
优惠券锁定模块伪代码逻辑如下:
* 1)锁定优惠券记录
* 2)task表插入记录
* 3)发送延迟消息检查
总结
目前常见的有以下几种解决方案:
- 在业务层面进行整合,避免分布式事务。
- 最终一致性方案之eBay模式。主要采用了消息队列来辅助实现事务控制流程,其核心是将需要分布式处理的任务通过消息队列的方式来异步执行。如果事务失败,则可以发起人工重试的纠正流程。在电商等高并发场景一般使用该方式。
- XA二阶段提交。适用于并发不高的情况。因为TM要一直等待RM的响应,在高并发场景下不适用。
- TCC拥有较高的并发,不存在资源阻塞问题,但需要与业务进行深度绑定,增加工作量。