(C#版)使用TCC分布式事务改造现有下单流程(一)

1.引言

这篇博文打算分两篇来阐述:

第一篇介绍优化背景和优化思路;

第二篇对支撑我们改造的跨语言TCC中间件dtm进行讲解。

另外,我们项目使用的是.net core开发的微服务项目,使用的语言是C#。

2.现状

2.1订单创建流程

为了说明问题,将下单流程极简化:

本地验证优惠券是否被使用->根据前端传递的参数构造订单。

优惠券的验证

使用本地验证,看是否已与已购买的订单进行了绑定,如果绑定说明被使用不允许下单。

库存没有处理

现状是下单时不处理库存,支付成功后才通过消息异步通知商品中心对库存进行扣减,如果库存不足,则进行退款处理,这种补偿机制肯定体验不好。

2.2问题

优惠券的验证

在高并发情况下,优惠券的验证不准确,应该去促销中心验证,这种本地验证的问题就是,只验证了已被使用的优惠券,如果优惠券也正在被其他下单请求使用和校验的话,会因为对资源的并发访问而导致均验证通过。

会发生超卖的情况

在高访问量情况下,必然会出现同一个商品多人同时购买的情况发生,在下单阶段没有对购买的sku的库存进行冻结,会导致超卖的发生。

3.解决思路

3.1 对于优惠券

  下单过程中,向促销中心发送对优惠券的使用申请,占用该优惠券,如果已经被占用,则终止当前下单操作。如果未占用,则锁定该优惠券,等下单成功,更改优惠券的使用状态为已使用。

3.2 对于库存

  也是类似的思路,比如我要买2个商品A,向商品中心发送对所购买商品库存的申请,占用2个客户要购买的商品库存:

  •   如果库存不足,终止下单
  •   如果库存充足,此时,商品中心的占用库存+2,可用库存-2。继续下单

  下单成功后,商品中心的占用库存-2,可用库存不变。

3.3 引出TCC理论

为什么上述过程可以避免因为并发而造成的共享资源数据不准确?

  上述过程将共享资源,分为“预留资源”和“可用资源”,在业务执行的时候,只操作预留资源,几乎不会涉及到锁和资源的争用,所以它具有很高的性能潜力。即使加锁,也只是在对库存进行加减计算那很短的时间加锁,锁的范围很小。

分布式事务解决方案之TCC

  上述其实是TCC分布式事务的思想,也是当前使用较广泛的解决类似“超售”分布式事务问题的技术方案。

  可靠消息队列,是我们项目目前使用的分布式事务方案,可靠消息队列的局限性是,隔离性差,在并发情况下,共享资源的处理会混乱,会导致优惠券或库存的处理不准确。

  对于微服务的分布式事务的解决:可靠消息队列和TCC以及saga的组合,能解决大部分分布式事务问题,目前TCC是我们的微服务技术中缺失的比较重要的一块,已经到了无法满足业务需求的程度了,需要进行技术突破。

3.4 TCC改造后的下单时序图

 

 

  • 第一步,用户向订单服务发送下单请求。
  • 第二步,创建事务,生成事务 ID(主和子事务),记录在活动日志中,进入 Try 阶段:
    • 促销中心:检查优惠券的占用状态,如果未占用,通知下一步进入 Confirm 阶段;不可用的话,通知下一步进入 Cancel 阶段。
    • 商品中心:检查库存,库存充足,则增加占用库存,减少可用库存,通知下一步进入 Confirm 阶段;库存不足,通知下一步进入 Cancel 阶段。
  • 第三步,如果第二步中所有业务都反馈业务可行,就将活动日志中的状态记录为 Confirm,进入 Confirm 阶段:
    • 促销中心:修改占用优惠券状态为'已使用'
    • 商品中心:被占用库存的商品状态改为‘待出货’,减去占用库存,可用库存不变
  • 第四步,如果第三步的操作全部完成了,事务就会宣告正常结束。而如果第三步中的任何一方出现了异常,不论是业务异常还是网络异常,都将会根据活动日志中的记录,来重复执行该服务的 Confirm 操作,即进行“最大努力交付”。
  • 第五步,如果是在第二步,有任意一方反馈业务不可行,或是任意一方出现了超时,就将活动日志的状态记录为 Cancel,进入 Cancel 阶段:
    • 促销中心:恢复优惠券状态为'未使用'
    • 商品中心:恢复占用库存
  • 第六步,如果第五步全部完成了,事务就会宣告以失败回滚结束。而如果第五步中的任何一方出现了异常,不论是业务异常还是网络异常,也都将会根据活动日志中的记录,来重复执行该服务的 Cancel 操作,即进行“最大努力交付”。

3.5 改造的可行性

  由于 TCC 的业务侵入性比较高,需要开发编码配合,在一定程度上增加了不少工作量,需要投入一定的开发成本,比如更换事务实现方案的替换成本。

  所以,通常并不会完全靠裸编码来实现 TCC,而是会基于某些分布式事务中间件(如阿里开源的Seata)来完成,以尽量减轻一些编码工作量。

  但Seata有语言限制,仅支持java,对.net core来说,适合的TCC中间件有:https://github.com/yedf/dtm,是用go语言开发的支持大部分主流的开发语言,其中针对.net core有相应的sdk可供使用(是张善友大佬提供的)。

4.其他常用分布式事务的适用场景

4.1 可靠消息队列

  可靠消息队列的实现原理,虽然它也能保证最终的结果是相对可靠的,过程也足够简单(相对于 TCC 来说),但可靠消息队列的整个实现过程完全没有任何隔离性可言。

  假如订单创建成功,创建之前已经检测了库存正好够,于是乎发送消息通知仓库服务扣减库存准备发货,这是正常无并发情况的逻辑。但由于该商品热卖,有大量购买该商品的请求并发,而碰巧在我检测库存之后创建订单发送消息时(还未来得及扣减库存),正好已经有交易先于我完成了(它抢先一步扣减了库存),这样就导致此刻仓库发现现在已经没有库存可被扣除了,正常情况是无法发货,订单应该作废的。但因为base理论要求只要我发出去成功的消息后,我就当整个创建订单是成功的了,剩下的就交给仓库服务去解决它的问题了,哪怕仓库服务由于并发的出现而没有库存了,也得让整个过程达到最终一致,所以消息不断重发,仓库服务不断的尝试扣减库存,直至操作成功(比如补充了库存),或者被人工介入为止。

  可靠消息队列属于base理论的技术实现手段之一,但它仅适合对资源隔离性要求不高的场景,或者说是共享资源并发低到不会发生冲突的场景。但对下单扣减商品库存这种并发场景无法避免的情况下,缺乏隔离性,就会导致类似“超售”的问题。

  如果这件事情是发生在刚性事务且隔离级别足够的情况下,其实是可以完全避免的。比如mysql“可重复读”(Repeatable Read)的隔离级别,可以保证后面提交的事务会因为无法获得锁而导致失败。但用可靠消息队列就无法保证这一点了。

  然而锁定库存,显然系统的性能会极地,在促销活动的场景,服务甚至不可用,所以分布式事务要避免粗粒度的锁的使用。

4.2 saga事务

  SAGA 的意思是“长篇故事、长篇记叙、一长串事件”,它的思想是基于数据补偿代替回滚的解决思路。回滚意味着相当于没发生,而补偿意味着发生了,但我可以弥补你。比如订单创建成功了,但我突然发现没有库存了,那我再把订单取消掉,这样给客户不友好,前一秒还给我提示成功了,后一面又提示库存不足,取消订单。

  saga最初提出了一种如何提升“长时间事务”(Long Lived Transaction)运作效率的方法,大致思路是把一个大事务分解为可以交错运行的一系列子事务的集合。原本提出 SAGA 的目的,是为了避免大事务长时间锁定数据库的资源,后来才逐渐发展成将一个分布式环境中的大事务,分解为一系列本地事务的设计模式。

  saga介绍相关文章:https://www.jianshu.com/p/e4b662407c66

 

posted on 2021-10-29 21:17  moonfeeling  阅读(762)  评论(0编辑  收藏  举报

导航