生订链路

  • 订单系统的业务,主要分为正向链路和逆向链路这两个维度,正向链路的核⼼业务流程,包括订单⽣单 链路、预⽀付和⽀付回调链路以及订单⽀付完成后的履约操作链路,⽽逆向链路的核⼼业务流程,包括 订单超时未⽀付的⾃动关单链路、⼿动取消订单链路、发起售后退货申请以及审核售后退货链路

生单.png

正向链路

  1. 风控服务-风控检查
    1. 检查是否存在刷单行为,或来自黑名单等
  2. 商品服务-计算价格
    1. 计算订单商品价格、运费等
  3. 验证订单金额
    1. 校验计算的订单价格和前端传过来的价格是否一致
  4. 锁定优惠券
    1. 如果使用了优惠券,则锁定优惠券,避免重复使用
  5. 锁定库存
    1. 锁定库存,避免因库存不足,无法进行履约
  6. 订单落库

    1. 在订单表和订单明细表等数据库表中创建对应记录

      Seata事务流程

      image.png

      流程

  7. 开启分布式事务:当服务A执⾏数据库的写操作时,服务A⾸先会向Seata Server服务发起⼀个请求,开启⼀个分布式事务。

    1. Seata Server会返回一个xid给服务A,这是全局事务的唯一标识,服务A调用其他事务的时候,会将这个xid带过去,这样其他分支事务就知道自己处于哪个全局事务中了;
  8. 开启分支事务:然后在执⾏本地库写操作时,服务A本地还会单独再开启⼀个分⽀事务,⼀个分布式事务中可以开启多 个分⽀事务,我们可以理解为,每个分⽀事务可以确保操作⼀个数据库的数据⼀致性
  9. 获取本地锁:服务A对应的分⽀事务进⾏写操作时,⾸先得要获取⼀把本地锁,⽐如服务A要对库中id=100这条数据 进⾏写操作,⾸先会在本地库中,对id=100这条数据获取⼀把本地锁。
  10. 执行本地事务:本地锁获取成功之后,才能在数据库中执⾏写操作,写完数据之后,Seata同时还会在本地数据库中的 undo_log表中,插⼊⼀条回滚⽇志,我们预先得要在数据库中,创建好Seata提供的回滚⽇志undo_log表,⽽回滚⽇志记录的,和你实际写操作是相反记录。
  11. 提交分支事务:⾸先得要先从Seata Server服务中获取⼀把全局锁。⽐如,前⾯我们在分⽀事务A中,针对id=100这条数据已经获取到本地锁了,此时还需要针对id=100这 条数据,到Seata Server中获取⼀把全局锁,当服务A获取全局锁成功之后,接下来才可以提交分⽀事务A,并且释放之前获取的本地锁,⽽全局锁得要等到整个分布式事务提交之后才会释放
  12. 提交全局事务:当服务A、服务B和服务C对应的所有分⽀事务都提交之后,Seata才会提交整个分布式事务,然后统⼀释放服务A、服务B以及服务C这三个服务之前获取的全局锁。

    Seata死锁问题和超时释放

  • 高并发场景下很容易产生死锁,并且要频繁的获取锁,并发性能也会比较差

    死锁发生

  • 请求1到服务A,假设这个请求服务A是对ID=10的数据进行操作,服务A开启全局事务1,服务A执行本地事务后,释放了对ID=10的本地锁,获取到ID=10的全局锁,然后调用服务B

  • 这时请求2到服务A,也是对ID=10的数据进行操作,服务A开启全局事务2,获取ID=10的本地锁,进行分支事务操作,但当要获取ID=10的全局锁进行分支事务提交时候,发现全局锁获取失败,于是等待ID=10的全局锁
  • 服务B处理请求1失败,进行事务回滚,而服务A进行分支事务回滚时候,需要获取ID=10的本地锁,但是本地锁被请求2占用,于是阻塞等待
  • 事务1和事务2之间,互相持有对方需要的资源,却又互相等待对方资源,于是就死锁

    超时机制

  • 如果超过一定时间还没有获取全局锁,则会取消这个分布式事务,释放本地锁

    库存锁定-TCC

    存储异构

  • 库存数据的实时行要求比较高,为了扛住尽量高的读请求,所以需要引入缓存Redis

  • 双写:写数据库+写缓存

    库存锁定

  • 整体采用AT+TCC

    • 减库存分支事务使用TCC
    • 优点:
      • 不用获取全局锁,库存服务并发性能提高,锁冲突情况降低

image.png

  • 一个SKU的库存记录,包含两个核心字段
    • 销售库存:表示当前可售卖的数量
    • 锁定库存:已经提交订单,但未完成扣减的库存数量
    • 锁库存:销售库存-1,锁定库存+1
  • 锁定策略:

    • try:销售库存减一
    • confirm:锁定库存加一
    • cancel:销售库存加一

      空回滚&悬挂问题

  • 当第一阶段try操作时候,加入数据库扣减销售库存的请求,因为网络原因阻塞没有执行,而Seata以为已经执行成功,就进行了缓存的try

  • 加入缓存的try执行失败,TCC进行cancel,数据库的销售数据就会+1,导致数据不一致问题
  • 空回滚
    • 像这样由于⽹络不通畅等原因,导致在try⽅法都还没有执⾏成功的前提下,就直接执⾏cancel⽅法进⾏回滚的现象,我们称为空回滚
  • 悬挂
    • try⽅法⼀直阻塞卡住⽽不能执⾏的现象,⼀般也被称为是悬挂
  • 解决:

    • 通过Map这样的数据结构来实现, Map中的key为接⼝名称、分布式事务的xid和以及商品的sku组成,表示当前是哪个接⼝类,在哪个 Seata分布式事务中,对哪个商品sku进⾏锁定库存操作 ⽽Map中的value值,则可以⽤于存放具体的操作状态,⽐如try操作开始执⾏时,可以在缓存中设置“TRY_START”字符串,表示当前try⽅法开始执⾏了;⽽当try⽅法执⾏成功之后,可以将该value值设置为“TRY_SUCESS”字符串,表示当前try⽅法已经执⾏成功了
    • cancel执行时候,判断key不存在,则cancel中业务逻辑不执行
    • try如果因为网络恢复在执行,则先判断key是否存在,如果存在,则不执行try逻辑

      幂等性问题

  • 所有try执行成功后,会依次执行confirm,完成核心业务逻辑

  • 如果confirm执行失败,TCC会进行重试,就有可能因为幂等性问题导致脏数据
  • cancel如果失败,也会进行重试

    订单支付链路

    image.png
    image.png

  • 分布式锁避免重复支付

    订单履约链路

    业务流程

    image.png

    消息丢失&消息顺序问题