同步场景分布式事务设计
同步场景
1、首页推荐商品列表
a、商品信息
b、用户信息
c、社交信息
2、购买商品
a、下单->A
b、减库存->B
c、支付->C
首页推荐商品列表里面包含,
1、商品的图片信息、价格、标题和描述;这些都是商品本身的信息,除了商品的本身信息;
2、商品发布人的信息:本人的头像、姓名、好友;
3、商品社交信息:点赞、留言
这个时候,APP拿到数据是发了一个请求还是多个请求呢?
很显然我们是发了一个请求,发三个请求肯定是不好的,因为发三个请求,延迟会变高,传输数据量也会变大。再者商品的展示其实是有一定的过滤规则的,比如说我一页是10条,我不允许在一页里面同一个人的商品出现两次,这就需要通过过滤来实现,如果只是随便从DB中取出10条,极端情况下有可能你拿出的10条商品都是同一个人发布的,一过滤就变成一条了,这就不符合我们的规则,所以肯定要一次多取一些,比如拿100条,再APP端再做一些排列组合,这样对APP来说,无论流量或者延迟来说都是浪费的。
所以在这种情况下更好的方式是:聚合操作,放到Server端去做,这时候我们的架构如下图所示:
image.pngAPP端发送商品列表请求到网关层,网关层路由到聚合服务层,聚合服务依次查询用户服务、用户服务、社交服务获取商品信息(图片、价格、标题)、用户信息(头像、姓名)、社交信息(评论、点赞);这就是一个读同步场景:商品->用户->社交;读的分布式事务一般意义不大,所以分布式事务一般是针对写的。
2、购买商品架构方案
a、下单->A
b、减库存->B
c、支付->C
image.png
同步场景分布式事务设计
解决方案
基于异步补偿的分布式事务
架构设计的三大关键点
1、记录请求调用链条
2、提供幂等补偿接口
3、基于补偿机制
比如上图的微服务架构,下单成功、减库存成功、支付失败,此时需要会滚,其中支付不用回滚,因为支付是本地事务,它会自动回滚,这时候呢有两种补偿办法:
1、第一种:聚合服务直接返回APP,告诉它这次请求失败了!然后再异步的补偿下单、补偿库存。
2、第二种:一旦支付业务层失败了,首先补偿库存、补偿下单,补偿成功以后再告诉APP,这次请求失败了。
很显然这是Fast Failture 快速失败原则,反正已经失败了,为什么还要业务方去等待呢,第一种方案是异步的,不会阻塞正常的业务流程。
这其实都是Saga 的“向后恢复”模式:补偿所有已完成的事务,如果任一事务失败!
而不是“向前恢复”;
如何补偿呢?
减库存,理论上我们是要加1 ,我们需要记录减库存的上下文;
下单,我们需要将订单取消;
支付:虽然支付失败,也要将失败记录记录下来
为什么要提供幂等的补偿接口呢?
例如减库存补偿的时候,补偿失败重试的时候一直加1、加1、加1....问题就变得很复杂,库存多加了很多。
比如A->B->C 那么每次只需前都要 通过Porxy 将调用参数写入TDB 库中;
执行A的本地事务,先将A的参数写入到数据库TDB中,然后写入A的本地事务
执行B的本地事务,先将B的参数写入到数据库TDB中,然后写入B的本地事务
执行C的本地事务,先将C的参数写入到数据库TDB中,然后写入C的本地事务
写入的记录格式为:tid(事务ID)->state(状态值)->ts(时间)
tschedule 会每隔5秒去扫描事务日志数据库是否有状态为要补偿的记录;
然后去补偿事务,补偿完成之后将事务的状态修改为“已补偿”;
rpc client 主要用来去调用“数据访问层”的补偿接口;
业务逻辑层Proxy设计(基于AOP实现)
1、逻辑层调用上加上事务注解@Around("execution(**(..))&&@annotation(TX)")
2、Proxy在真正业务逻辑被调用之前,生成一个全局唯一TXID标示事务组,TXID保存在ThreadLocal变量里,方法开始前写入,完成后清除,并向远端数据库写入TXID并把事务组置为开始状态;
3、业务逻辑层调用数据访问层之前,通过RPCProxy代理记录当前调用请求参数;
4、如果业务正常,调用完成后,当前方法的调用记录存档或删除
5、如果业务异常,查询调用链反向补偿
数据访问层设计
1、原子接口
2、补偿接口
a、谁来提供?
b、幂等性保证
3、基于接口方法,在方法名加注解标注补偿方法名
4、@Compensable(cancelMethod="cancelRecord")
比如:数据访问层的Order方法,它的补偿方法是cancelRecord,当然我们会打一个注解 Compensable,这个Order方法怎么样跟我的cancelRecord方法匹配起来,因为数据访问层在启动的时候,会通过Spring boot的扫包这样一些component-scan;这些记录可以放在配置中心里面,这个配置中心就有两条记录:一个是源方法 一个是补偿方法
K-V记录:order:cancelRecord
分布式事务补偿服务
1、事务组表(数据库表TDB)
a、记录事务组状态
b、txid、state、timestamp;
2、事务调用组表(数据库表TDB)
a、记录事务组内的每一次调用以及相关参数
b、Txid、actionid、callmethod、pramatype、params
比如A/B/C ,Txid为事务id,actionId:为第一步、第二步、第三步;
callmethod:补偿方法,可以从配置中心获取,数据访问层启动的时候回将每一个接口对应的补偿方法,通过注解的方式关联起来,放到配置中心;
业务层的Proxy 比如调用A方法,就通过配置中心可以拿到A方法的补偿方法:源方法A|补偿方法CA
paramtype:服务的类型,有可能你是web服务值就为1,有可能是RPC服务值就为2;
params:参数
事务组表:
txid | state | timestamp |
---|---|---|
1 | 1 | 2020-03-24 |
事务调用组表
Txid | actionid | callmethod | pramatype | params | |
---|---|---|---|---|---|
t1 | 1 | CA | 2(1、web;2、RPC) | PA | |
t1 | 2 | CB | 2 | PB | |
t1 | 3 | CC | 2 | PC |
如果补偿成功就将 事务组表的状修改为 2
txid | state | timestamp |
---|---|---|
1 | 2 | 2020-03-24 |
事务调用组表中因为 C是最后一步,所以CC无需补偿;如果CB补偿失败,因为CB是同一个事务所以会滚就行;所以问题在CA上;这时候不能再弄个分布式事务来补偿未补偿成功的事务,这时候要记录失败日志、报警、人工介入;
3、补偿策略
a、调用执行失败,修改事务组状态
b、分布式事务补偿服务异步执行补偿
分布式事务成功案例
商品交易创建订单事务组正常流程
1、锁库存->减红包->创建订单
2、代理层透明记录调用请求参数
a、记录事务域的开始与结束
b、在所有远程调用成功时
c、对业务逻辑不做侵入
分布式事务失败案例
商品交易创建订单事务异常流程
1、微服务数据访问层失败,代理更改事务组状态
2、微服务业务正常执行
3、事务补偿服务异步执行补偿
网友评论