分布式一致性方案
目录
简介
系统开发过程中,我们通常会遇到一个服务涉及到多个PRC调用的场景,例如下单/秒杀扣减库存,优惠卷等。那么如果保证多个RPC服务最终的数据一致性,就成了我们通常需要解决的问题。
分布式系统中,RPC服务的调用通常存在三个状态,成功、失败和超时。超时场景时RPC可能是成功的,也肯能是失败的。针对上面这三种状态,在多个RPC调用或者本地数据与RPC如何保证一致的问题,存在已下多种解决方案。我们边学习,边记录。
场景举例
订单服务下单场景,需要执行三个操作,下单、扣库存,扣优惠。在分布式服务场景中,这三个服务分开独立部署。用户下单时,如何保证订单服务,库存服务,优惠服务数据一致,成了我们需要解决的问题。
分布式一致性解决方案
方案1:TCC与MQ保证最终一致性
TCC有三个操作,Try、Confirm和Cancle。每个操作对应上面三种状态:成功、失败和超时。
系统伪代码如下:
Order order = new Order();
//下单、扣库存、扣优惠串行执行
try {
// 创建订单
orderService.saveOrder(order);
// 库存服务根据订单扣减库存
stockService.reduceStock(order);
// 促销服务扣除优惠
promotionService.reduceCoupon(order);
} catch (Exception e) {
// 更新订单状态为创建失败
orderService.updateStatus(order, OrderStatus.FAIL);
// 归还库存, 额度
stockService.addStock(order);
// 归还优惠
promotionService.addCoupon(order);
}
上面伪代码,捕获了执行过程中失败的异常。如果执行失败,则订单创建失败、归还库存和优惠。实际上述伪代码存在一下几个问题:
- RPC调用超时:三个服务都是RPC服务,RPC可能存在成功、失败和超时三种情况。在超时情况下,是不RPC服务是否执行成功的(例如扣减库存)。所以当发生异常时,归还库存可能导致库存变多。为了解决这一个问题,一般会让RPC服务保证幂等。在发生异常时,不论是否扣减成功,都先再次执行扣库存,扣除之后再还库存。多次重试尽量保证成功,达到最终一致性。
- 系统宕机:在服务执行过程中,系统进程可能宕机。导致库存或者促销不一致。例如创建订单成功、扣减库存成功之后,系统宕机。此时为执行回归操作,导致促销优惠没有扣减,数据最终不一致
解决方案
下单伪代码如下:
Order order = new Order();
//发送预下单消息到MQ延时消息队列中(延迟30分钟)
mqService.sendMsg(Message.PREPARE,order,30min);
//下单、扣库存、扣优惠串行执行
try {
// 创建订单
orderService.saveOrder(order);
// 库存服务根据订单扣减库存
stockService.reduceStock(order);
// 促销服务扣除优惠
promotionService.reduceCoupon(order);
//更新订单创建成功
orderService.updateStatus(Success);
} catch (Exception e) {
//发送回滚消息
mqService.sendMsg(Message.ROLLBACK,order);
}
MQ消费伪代码:
//消费MQ
public void consumer(Message nessage){
Order order = orderService.getOrder();
//如果不存在订单,则直接消费成功(预下单消息发送后,DB创建订单失败)
if(order == null){
return;
}
if (Messsage.PRE) {
//如果是预下单消息(30min后才会收到)
OrderStatus status = orderService.getStatus(order);
//如果订单是创建中状态,则强制进行回归,不论库存、促销是否扣减成功
if (OrderStatus.CREATING.equals(status)) {
//直接执行回滚操作。回滚失败则MQ重试
rollback(order)
}
} else if (Message.ROLLBACK) {
//直接执行回滚操作。回滚失败则MQ重试
rollback(order)
}
}
//下单回滚(此方法可以做本地事务,好处:防止在更新DB成功后宕机了,跳过换库存+优惠的case)
public void rollback(Order order){
if(order == null) {
return;
}
//CAS 更新订单状态
bool update = orderService.updateStatus(OrderStatus.CREATING,OrderStatus.FAIL);
if (update) {
//归还库存(库存服务保证幂等,先扣再还)
stockService.reduceStock(order);
stockService.addStock(order);
//归还优惠(优惠服务保证幂等,先扣再还)
promotion.reduceCoupon(order);
promotion.addConpon(order);
}
}
为了更清楚的把问题解决方案,映射到TCC+MQ上,建单说明一下
- 上面下单伪代码:执行了try和commit的具体操作,发送了rollback指令。
- MQ: 消费代码执行了在收到try和rollback指令后,执行了回滚操作,利用MQ重试机制保证了最终一致性。
异常场景模拟分析
异常节点 | 宕机保证一致性 | 异常保持一致 |
---|---|---|
动作1之前 |
|
|
动作2之前 |
|
|
动作3前 动作4前 |
|
|
动作5之前 |
|
|