分布式一致性方案

目录

简介

场景举例

分布式一致性解决方案

方案1:TCC与MQ保证最终一致性

解决方案

异常场景模拟分析


简介

         系统开发过程中,我们通常会遇到一个服务涉及到多个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之前
  • MQ:无PRE消息
  • 订单:无订单
  • MQ:无PRE消息
  • 订单:无订单
动作2之前
  • MQ:有PRE消息,MQ监听之后无订单,直接消费成功
  • 订单:无订单
  • MQ:触发动作5,有ROLLBACK和PRE消息。MQ消费时,ROLLBACK先到达,无订单,直接消费成功。
  • 订单:无订单

动作3前

动作4前

  • MQ:触发动作5,有PRE消息,MQ消费PRE消息时,发现订单为创建状态,则执行回滚操作
  • 订单:触发回滚,订单状态最终为失败
  • 库存:触发回滚,库存一致
  • 优惠:触发回滚,优惠一致
  • MQ:触发动作5,有ROLLBACK和PRE消息。ROLLBACK先到达,MQ消费时,发现订单为创建状态,则执行回滚操作
  • 订单:触发回滚,订单状态最终为失败
  • 库存:触发回滚,库存一致
  • 优惠:触发回滚,优惠一致
动作5之前
  • MQ:ROLLBACK发送失败。有PRE消息,MQ消费时,发现订单为创建状态,则执行回滚操作
  • 订单:触发回滚,订单状态最终为失败
  • 库存:触发回滚,库存一致
  • 优惠:触发回滚,优惠一致
  • MQ:ROLLBACK发送失败。有PRE消息,MQ消费时,发现订单为创建状态,则执行回滚操作
  • 订单:触发回滚,订单状态最终为失败
  • 库存:触发回滚,库存一致
  • 优惠:触发回滚,优惠一致