生订链路
- 订单系统的业务,主要分为正向链路和逆向链路这两个维度,正向链路的核⼼业务流程,包括订单⽣单 链路、预⽀付和⽀付回调链路以及订单⽀付完成后的履约操作链路,⽽逆向链路的核⼼业务流程,包括 订单超时未⽀付的⾃动关单链路、⼿动取消订单链路、发起售后退货申请以及审核售后退货链路
正向链路
- 风控服务-风控检查
- 检查是否存在刷单行为,或来自黑名单等
- 商品服务-计算价格
- 计算订单商品价格、运费等
- 验证订单金额
- 校验计算的订单价格和前端传过来的价格是否一致
- 锁定优惠券
- 如果使用了优惠券,则锁定优惠券,避免重复使用
- 锁定库存
- 锁定库存,避免因库存不足,无法进行履约
订单落库
开启分布式事务:当服务A执⾏数据库的写操作时,服务A⾸先会向Seata Server服务发起⼀个请求,开启⼀个分布式事务。
- Seata Server会返回一个xid给服务A,这是全局事务的唯一标识,服务A调用其他事务的时候,会将这个xid带过去,这样其他分支事务就知道自己处于哪个全局事务中了;
- 开启分支事务:然后在执⾏本地库写操作时,服务A本地还会单独再开启⼀个分⽀事务,⼀个分布式事务中可以开启多 个分⽀事务,我们可以理解为,每个分⽀事务可以确保操作⼀个数据库的数据⼀致性
- 获取本地锁:服务A对应的分⽀事务进⾏写操作时,⾸先得要获取⼀把本地锁,⽐如服务A要对库中id=100这条数据 进⾏写操作,⾸先会在本地库中,对id=100这条数据获取⼀把本地锁。
- 执行本地事务:本地锁获取成功之后,才能在数据库中执⾏写操作,写完数据之后,Seata同时还会在本地数据库中的 undo_log表中,插⼊⼀条回滚⽇志,我们预先得要在数据库中,创建好Seata提供的回滚⽇志undo_log表,⽽回滚⽇志记录的,和你实际写操作是相反记录。
- 提交分支事务:⾸先得要先从Seata Server服务中获取⼀把全局锁。⽐如,前⾯我们在分⽀事务A中,针对id=100这条数据已经获取到本地锁了,此时还需要针对id=100这 条数据,到Seata Server中获取⼀把全局锁,当服务A获取全局锁成功之后,接下来才可以提交分⽀事务A,并且释放之前获取的本地锁,⽽全局锁得要等到整个分布式事务提交之后才会释放
- 提交全局事务:当服务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
- 优点:
- 不用获取全局锁,库存服务并发性能提高,锁冲突情况降低
- 一个SKU的库存记录,包含两个核心字段
- 销售库存:表示当前可售卖的数量
- 锁定库存:已经提交订单,但未完成扣减的库存数量
- 锁库存:销售库存-1,锁定库存+1
锁定策略:
当第一阶段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会进行重试,就有可能因为幂等性问题导致脏数据
-
订单支付链路
-
订单履约链路
业务流程
消息丢失&消息顺序问题