延时队列实现数据的最终一致性

实现场景

在分布式微服务场景下,实现用户提交订单的最后一步 submitOrder() 方法,首先需要将 order 实体保存到订单服务对应的数据库下,然后需要去仓储服务下锁定(预留)库存,锁定库存后还可能调用其他服务做其他事,又或者在当前订单服务下做其他事。

预留库存的原因是客户并不是立即支付的,往往会有预留一定的时间。下文的场景中,用户可以有 30 分钟的时间来支付。

图示:
场景实现
使用 @Transactional 注解声明以上所有方法是事务后

可能出现的问题

考虑以下场景:

  1. saveOrder() 方法出现异常
  2. lockStore() 方法出现异常
  3. doSomething() 方法出现异常

结果是:

  1. 执行 saveOrder() 时抛出异常,submitOrder() 方法事务回滚,后面的方法不执行,无事发生。
  2. 执行 lockStore() 时抛出异常,该方法事务回滚,submitOrder() 方法事务回滚,后面的方法不执行,无事发生。
  3. 执行 doSomething() 时抛出异常,该方法事务回滚,submitOrder() 方法事务回滚,lockStore() 方法不会回滚。导致的结果是,订单没有生成,锁了库存。多来几次可能导致,一个订单没有,货没了。
  4. 执行 lockStore() 时正常,库存被锁,返回数据时远程调用超时,submitOrder() 方法事务回滚。导致的结果同上。

CAP 定理

CAP原则又称CAP定理,指的是在一个分布式系统中,一致性(Consistency)、可用性(Availability)、分区容错性(Partition tolerance)。CAP 原则指的是,这三个要素最多只能同时实现两点,不可能三者兼顾。

P 相当重要,系统需要在部分出现问题的情况继续运作,要想保持数据一致性,那么部分出现问题的数据库就不能去访问;要保持所有数据库可用,就可能出现数据不一致的情况。

BASE 定理

BASE 是 Basically Available(基本可用)、Soft state(软状态)和 Eventually consistent(最终一致性)三个短语的简写。

BASE 是对 CAP 中一致性和可用性权衡的结果,其来源于对大规模互联网系统分布式实践的结论,是基于 CAP 定理逐步演化而来的,其核心思想是即使无法做到强一致性(Strong consistency),但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性(Eventual
consistency)。

Raft 算法

Raft 算法官方文档

解决方案——Seata

Seata 中文文档

点进去映入眼帘的全局锁,可以使用 Seata 实现数据的强一致性。但是数据库性能严重下降。

解决方案——延时队列

使用 RabbitMQ 的死信机制实现延时队列,用延时队列实现数据的弱一致性

再次声明,以上问题的根源问题是订单存在的情况下,才有必要锁库存。

死信(Dead Letter,DL): 顾名思义,死掉的消息

死信的产生方式:

  • 消息被拒,且没有重新入队
  • 消息过期,超过了设置的 ttl 时间
  • 队列中消息数量超过最大数目

死信交换机(Dead Letter Exchanges,DLX): 用来路由死信的交换机

死信路由键(Dead Letter Routing-Key,DLK): 用来路由死信的路由键

可以用以下思路实现延时队列:生产者向交换机发送消息,消息根据路由键来到 A 队列(实现延迟的队列),该队列设置一个 TTL,设置死信交换机和死信路由键,并不设置任何消费者监听。

经过 TTL 之后,原来 A 队列中的消息成为死信,来到死信交换机,经过死信路由键来到 B 队列,让消费者监听 B 队列,此时消费者收到的消息即是延迟了 TTL 的消息。

在当前场景下,在仓储服务中,锁库之后向 MQ 中发送一个消息,消息包括的内容是自己锁了哪个订单的库存信息,经过一定的延迟时间,仓储服务再收到该消息,去订单服务中查询该订单到底是否真的存在,有没有被支付或者被放弃(此功能可结合用户支付或者取消订单)。

如果真的存在,消息消费完成;如果不存在,则要根据该消息,把仓储服务下的数据库恢复原样。

延时队列方案具体实现

/**
 * @author: lvshui5u
 * @date: 2021/8/16 10:27
 * @describe:
 */
@Configuration
public class MyMQConfig {

    @Bean
    public Queue stockDelayQueue(){
        Map<String, Object> arguments = new HashMap<>();
        // 死信交换机
        arguments.put("x-dead-letter-exchange", "stock-event-exchange");
        // 死信路由键
        arguments.put("x-dead-letter-routing-key","stock.release");
        // ddl,用来实现延迟
        arguments.put("x-message-ttl",120000);
        Queue queue = new Queue(
                "stock.delay.queue",
                true,
                false,
                false, arguments);
        return queue;
    }

    @Bean
    public Queue stockReleaseStockQueue(){
        Queue queue = new Queue(
                "stock.release.stock.queue",
                true,
                false,
                false);
        return queue;
    }

    @Bean
    public Exchange orderEventExchange(){
        TopicExchange exchange = new TopicExchange(
                "stock-event-exchange",
                true,
                false);
        return exchange;
    }

    @Bean
    public Binding stockLockedBinding(){
        Binding binding = new Binding(
                "stock.delay.queue",
                Binding.DestinationType.QUEUE,
                "stock-event-exchange",
                "stock.locked",
                null);

        return binding;
    }

    @Bean
    public Binding stockReleaseStockBinding(){
        Binding binding = new Binding(
                "stock.release.stock.queue",
                Binding.DestinationType.QUEUE,
                "stock-event-exchange",
                "stock.release.#",
                null);

        return binding;
    }

}

以上代码用来在服务启动时创建 Exchange、Queue、 Binding。

实现场景
以上为实现场景

消费者监听到锁库相关信息后,会拿到订单号,可以根据订单号去订单服务中查询是否真的有这个订单(目前未考虑支付相关问题)。

  • 有该订单,保留对库存的修改,向消息队列 channel 发送 ack 回复。
  • 没有该订单,还原数据库,向消息队列 channel 发送 ack 回复。
  • 去订单服务查询这个远程方法调用失败,将该消息拒绝并送回队列。

使用消息队列的相关问题

消息队列可能出现的问题:

  • 消息丢失
  • 消息积压
  • 消息重复

消息丢失:

消息丢失可以发生在以下情况:

  • 生产者向 MQ 服务器中投递消息时丢失 publisher->broker
  • 消息到达 MQ 服务器的交换机,交换机找不到正确的路由键进行路由 exchange->queue
  • 消息到达正确的队列,消费者没有监听到 queue->consumer

根据以上几种情况,分别有以下对应方案:

  • 开启 publisher (MQ 服务器)端的 confirmCallback 机制
  • 开启 publisher (MQ 服务器)端的 returnCallback 机制
  • 开启 consumer 端的 手动 ack/reject 机制

消息积压: 消费者消费的能力远低于生产者的生产能力,造成消息积压在消息队列里。

解决方案:

  • 上线更多的消费者
  • 将消息持久化,之后慢慢处理

消息重复: 保证接口的幂等性,处理消息时采用“乐观锁” 的思维模式,可以维持一个状态量表示是否已经被处理过。

如需详细代码实现请留言

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值