业务
电商系统三大黄金链路为:采购、购物、退货流程。每一个流程都牵涉了大量的分布式服务之间调用,其中最复杂核心的就是购物流程,以下是一个简单的模拟业务:
技术
1.分布式微服务调用
2.分布式事务
2.1 分布式事务常见方案:
(1) XA分布式事务:单系统多数据库
(2) TCC分布式事务:服务链式调用、解决大多数分布式事务场景,主要用于同步实时结果返回场景。注意:对于bytetcc简化版本主要针对于一个接口情况,简化版本通过不同注解代表的不同方法表明confirm、cancel、try,针对于多个接口都需要满足tcc事务的话,需要使用非简化版本的tcc,非简化版本使用不同的类实例实现的同一接口方法表示不同的conform、cancel、try。
(3) 可靠消息最终一致性:多用于耗时、异步 并且消息必须执行成功的场景
(4) 最大努力通知方案:多用于耗时、异步 并且消息可有可无的场景;比如:短信,
分布式事务混合使用注意:在很多业务场景下,我们的分布式事务可能是多个方案一同使用,需要将多个方案的代码分离,避免各种方案事务干扰。可以将接口调用拆分成多个接口。
2.2 分布式事务运用
创建订单场景(锁定库存)
(1) tcc分布式事务:订单的创建(订单创建主要是:库存中心、调度中心、wms中心锁定库存;可销售库存减1、锁定库存+1)中订单中心跟库存中心之间需要实时同步执行(对应上图5.1-5.5);订单中心出错,库存中心不能执行;订单中心成功,库存中心出错,库存中心本地事务回滚,同时通知订单中心之前操作进行cancel。当两中心都成功时候,才发送消息到MQ,通知调度中心、WMS中心异步进行库存锁定、出库调度。
(2)可靠消息最终一致性分布式事务:订单创建后通知调度中心、wms中心出库调度和库存锁定(对应上图5.6-5.8)使用可靠消息最终一致性方案,如果前面TCC分布式事务都执行成功的话,发送“待确认”消息给可靠消息服务;可靠消息服务投递消息到MQ,调度中心消费消息直至成功。
1、先执行第一步创建订单,第一步执行成功后再执行第二步:异步化锁定库存,出库调度。库存中心会发消息给可靠消息服务,可靠消息保证消息一定成功消费以及成功处理。
2、调度外呼接口服务在正确执行完步骤之后会告知可靠消息服务,然后可靠消息服务设置消息状态为已完成。
3、调度中心跟wms中心使用tcc分布式使用,保证一同成功/失败。
注意:第二步中,通过订单中中心基于可靠消息服务扣减库存时候时候,库存中心只是做一个代理转发调用调度中心,自己内部除了做可靠消息服务的业务逻辑操作外,不会做其他操作。因为调度中心的核心作用就是同步库存中心跟wms中心的库存。而一般库存中心 和wms中心不会直接调用。
支付订单场景(扣减库存)
支付订单的入口,我们从订单中心更改订单中状态为已支付开始;更改订单状态、库存中心扣减库存、会员中心增加会员积分使用同一个tcc事务。
分析以上图我们知道:
1、支付订单成功之后,通知订单中心;订单中心更新状态,订单中心调用库存中心扣减库存、订单中心调用会员中心增加会员积分需要实时保持事务一致性,使用tcc分布式事务。如果这一步失败的话,后面所有的 东西都不要去执行。(订单中心修改订单状态和库存中心扣减库存、会员中心增加会员积分同步保证一致性,一同成功/失败---图:9.1-9.3)
2、如果1执行成功的话,订单中心需要投递消息到可靠消息服务中去,然后基于可靠消息最终一致性方案实现,库存扣减跟销售出库。
订单中心可以直接通知可靠消息服务:调度出库;然后库存中心下游消费,库存中心调用调度中心调度出库,然后调度中心调用wms中心:创建出库单。
3、如果2执行成功之后,订单中心直接通知可靠消息服务增加回来积分消息,会员服务消费消息,然后增加本地数据库积分(这个也是可靠消息最终一致性方案)。
步骤3也执行完毕的话,订单中心消息需要发送消息,订单中心直接发送消息到mq,然后最大努力通知服务消费消息,然后存储消息到本地数据库,完成消息的发送。
注意:第二步中,通过订单中中心基于可靠消息服务扣减库存时候时候,库存中心只是做一个代理转发调用调度中心,自己内部除了做可靠消息服务的业务逻辑操作外,不会做其他操作。因为调度中心的核心作用就是同步库存中心跟wms中心的库存。而一般库存中心 和wms中心不会直接调用。
tcc分布式事务总结
tcc分布式事务的灵活应用是保障分布式事务简洁、高效条件;需要建立在对业务模块比较很熟的经验之上。
1、bytetcc简化模式跟非简化模式何时使用?
对于bytetcc简化版本主要针对于一个接口情况,简化版本通过不同注解代表的不同方法表明confirm、cancel、try,针对于多个接口都需要满足tcc事务的话,需要使用非简化版本的tcc,非简化版本使用不同的类实例实现的同一接口方法表示不同的conform、cancel、try。
2、业务中如何分辨tcc中的try,confirm,cancel?
tcc需要灵活使用,针对于同一个方法有前后两种状态的时候,一个方法里面做try,confirm,cancel逻辑判断
如果针对于一个业务多个方法时候,tcc的三步骤会贯穿各个方法中(先执行的业务方法当作try来预备资源和cancel释放资源,对于后执行的业务方法当作confirm来确认资源),针对于只有单个步骤的操作,我们的confirm有时候可能直接省略;try就是执行目标操作,cancel就是撤销目标操作(比如会员积分更改)
针对于有转账场景:
转账有转账人,被转账人之分;目标结果就是confirm需要实现的结果;
try阶段:转账人扣减金额到锁定金额
confirm阶段:转账人锁定金额扣减添加到被转账人金额
cancel阶段:cancel是对资源释放,对try的反向操作,此时执行:转账人金额从锁定金额变化到转账人金额。
电商中库存锁定场景:
在提交订单时候,库存中心的库存会变化,可销售库存减少,锁定库存增加,最后执行sql更新操作。针对于这种业务场景,目标结果业务是一步到位的,所以我们需要在这个方法里面进行tcc操作。
提交订单简化主要任务是:可销售库存减、锁定库存加;所以tcc最后目标是锁定库存加;因为
try阶段:可销售库存减
cancel阶段:可销售库存加
confirm阶段:锁定库存加
/**
* 更新销售库存: * try阶段:销售存库减 * cancel阶段:销售库存加 * @throws Exception
*/@Override
protected void updateSaleStockNum() throws Exception {
//订单的每一个商品的可销售库存减1
for (商品 do:商品列表) {
System.out.println("执行try操作:"+tccType);
do.setSaleStockNum(do.getSaleStockNum()-购买数量);
}else if(TCCType.CANCEL.equals(tccType)){
System.out.println("执行cancel操 作:"+tccType);
do.setSaleStockNum(do.getSaleStockNum()+购买数量);
}
}
}
/**
* 更新锁定库存: * confirm 锁定库存加1 * @throws Exception
*/@Override
protected void updateLockedNum() throws Exception {
//更新锁定库存
for (商品 do:商品列表) {
if(TCCType.CONFIRM.equals(tccType)){
System.out.println("执行confirm操作:"+tccType);
do.setLockedStockNum(do.getSaleStockNum()+购买数量);
}
}
}
支付订单:
支付中心支付完毕之后通知订单中心发起支付完成通知,订单中心会同步通过tcc通知库存中心扣减库存:目标锁定库存减、已销售库存加。所以我们把目标的第一步当作tcc的try阶段,confirm阶段在第二步。
@Override
protected void updateLockedStockNum() throws Exception {
for (商品 do:商品列表) {
if(TCCType.TRY.equals(tccType)){
do.setLockedStockNum(goodsStockDO.getLockedStockNum()-购买数量);
}else if(TCCType.CANCEL.equals(tccType)){
do.setLockedStockNum(do.getLockedStockNum()+购买数量);
}
}
}
@Override
protected void updateSaledStockNum() throws Exception {
for(商品 do:商品列表){
if(TCCType.CONFIRM.equals(tccType)){
do.setSaledStockNum(do.getSaledStockNum()+购买数量);
}
}
}
会员中心:
在购物流程中,订单支付时候,订单中心更改订单状态、库存中心更改库存、会员中心增加会员积分我们当成一个tcc分布式事务。会员中的两个操作更新积分和撤销积分已经对外提供接口,所以我们直接可以在tcc的controller外部调用。
@RestController
@Compensable(interfaceClass = MembershipApi.class,simplified = true)
public class MembershipTccService implements MembershipTccApi {
/**
* 通知会员中心,“支付订单”事件发生了 * @param userAccountId 用户账号id
* @param totalOrderAmount 订单总金额
* @return 处理结果
*/ @Override
@Transactional public Boolean payOrder(@PathVariable("userAccountId") Long userAccountId,
@RequestParam("totalOrderAmount") Double totalOrderAmount) throws Exception{
membershipExecutor.do(userAccountId,totalOrderAmount);
return true; }
@CompensableConfirm
@Transactional public Boolean confirmPayOrder(@PathVariable("userAccountId") Long userAccountId,
@RequestParam("totalOrderAmount") Double totalOrderAmount) throws Exception{
return true;
}
@CompensableCancel
@Transactional public Boolean cancelPayOrder(@PathVariable("userAccountId") Long userAccountId,
@RequestParam("totalOrderAmount") Double totalOrderAmount) throws Exception{
membershipExecutor.undo(userAccountId,totalOrderAmount);
return true; }
}
2.分布式幂等性
购物流程主要包含两个子流程:1、创建订单order流程(锁定库存) 2、支付订单(扣减库存,销售出库)
2.1 创建订单子流程
创建订单子流程关键是锁定库存;为支付订单后扣减库存创建销售出库单做准备。
从上面知道:创建订单分两步:第一步:同步锁定库存 第二步:异步锁定库存。
第一步:同步调用多次接口,网络超时或者请他情况会对同一个商品多次锁定库存,导致可销售库存重复减少,需要杜绝,这个时候需要满足幂等性3个条件:请求唯一标示由于是创建订单,所以订单id目前请求中是没有的,并且orderNo(订单编号)也是当时生成的;用户账号id和username都没法确定唯一一次创建订单请求(必须是在网站前端或者是手机APP,对用户的每一次创建订单的请求,都加一个类似于UUID的这么一个唯一的请求ID,一个请求ID唯一标识一个请求)。
因为订单创建接口唯一性标示后端没法唯一确认;这个事情的方案,不是纯后端可以实现的,要网站前端或者是手机APP要配合一下的:对每个订单发起的这个请求,都必须附加另外一个参数,必须是在网站前端或者是手机APP,对用户的每一次创建订单的请求,都加一个类似于UUID的这么一个唯一的请求ID,一个请求ID唯一标识一个请求。前端没法保证这个唯一id的话,也可以是在后端提供另外一个基础服务,基于snowflake唯一ID的算法,对所有的端都提供全局唯一的ID,网站前端或者手机APP发起一个创建订单的请求之前,都找后端的基础服务先获取一个全局唯一的ID,作为请求ID。
因为业务表里面没有相关请求唯一id的存储字段,所以我们使用去重表实现。
//1、幂等性保障
uniqueRecordMapper.insert("submit_order"+requestId);
//2、创建订单保存。
//3、tcc通知库存中心
订单中心通知可靠消息锁定库存
异步通知锁定库存,订单中心需要实现接口幂等性保障,对应的可靠消息服务,存在微服务之间的调用,也会存在重试,这个时候可靠消息服务转发到消息队列之后是库存中心去消费消息,然后进行锁定库存,这个时候我们可以看到。
库存中心:
由于订单中心 -> 库存中心,去锁定库存,这边是服务之间的调用,很有可能因为网络问题,也有可能会导致订单中心重复调用几次库存中心的接口,导致库存被多扣减了。
所以库存中心也需要改造:重复表去重
调度中心
库存中心调用调度中心,存在微服务减重试,需要在调度中心的tcc的try接口使用重复表去重策略去重,由于调度中心跟wms中心使用tcc分布式事务,所以只需要在调度中心保障。调度中心已经幂等性保障了,这个时候,又去调用了wms中心?以下有个疑问:tcc链式调用时候,try阶段就调用下一个服务的try接口,还是上游服务先调用try,然后confirm之后再次调用下一个接口try.分布式事务tcc调用时候,上一个服务的try调用下一个服务的try这样才能保证分布式事务同时成功失败回滚。
2.2 支付订单子流程
第一步:支付中心调用订单中心完成支付,订单中心同步事务通知库存中心扣减库存、会员中心增加积分。
以上存在分布式链路调用过程中重试,如果订单重复通知,积分库存多次变化是不好的,请求唯一id为订单id;
订单中心:订单中心是更改状态的我们可以使用业务表状态机(根据订单id查询订单状态是否是已支付,是的话return),当然也可以使用去重表。
库存中心:虽然使用了tcc事务,但是订单中心调用库存中心时候,存在多次重复调用情况,所以库存中心需要幂等性保障.使用去重表方案。
会员中心:同上,需要使用去重表方案。
第二步:订单中心投递消息到可靠消息服务,可靠消息服务投递消息到mq,库存中心消费消息然后调用调度中心进行调度。
订单中心:可靠消息上游服务,可靠消息服务下的mq消息只会消费一次,所以不需要
库存中心:可靠消息的下游服务,可靠消息服务下的mq消息只会消费一次,所以不需要
调度中心:存在多次重复调用情况,使用去重表完成。
wms中心:库存中心存在重复调度wms情况,传递订单,事务去重表。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。