不止于方案,用代码示例讲解RabbitMQ顺序消费

RabbitMQ系列文章
深入RabbitMQ世界:探索3种队列、4种交换机、7大工作模式及常见概念
不止于纸上谈兵,用代码案例分析如何确保RabbitMQ消息可靠性?
不止于方案,用代码示例讲解RabbitMQ顺序消费
RabbitMQ常见问题持续汇总

文章导图

在这里插入图片描述

前言

关于消息队列(MQ)顺序消费的讨论比比皆是,网上一搜便是铺天盖地的文章。然而,很多文章只是浅谈思路,对于具体的代码实现却往往一笔带过,让许多开发者(包括我)感到一头雾水。所以有时候空谈方案不来点代码理解一下就等于纸上谈兵~

场景分析

在生产中经常会有一些类似报表系统这样的系统,需要做 MySQL 的 binlog 同步。比如订单系统要同步订单表的数据到大数据部门的 MySQL 库中用于报表统计分析,通常的做法是基于 Canal 这样的中间件去监听订单数据库的 binlog,然后把这些 binlog 发送到 MQ 中,再由消费者从 MQ 中获取 binlog 落地到大数据部门的 MySQL 中。

在这个过程中,可能会有对某个订单的增删改操作,比如有三条 binlog 执行顺序是新增、修改、删除;消费者愣是换了顺序给执行成修改、新增、增加,这样肯定是不行的!

场景:发送消息的顺序

正常来说,我们发送消息的时候都是按照既定的业务顺序发送的,这点是无疑的。所以发送有序本来不是啥大事,问题在于,有的时候我们的项目是集群化部署,同一个项目有多个实例,当多个不同的实例分布于不同的服务器上运行的时候,都向 MQ 发消息,此时就无法确保消息的有序了。

场景:消费消息的顺序

场景1:一个queue,多个consumer去消费

在这里插入图片描述

假如只有一个queue,有多个consumer去消费,这样就会造成顺序的错误,consumer从MQ里面读取数据是有序的,但是每个consumer的执行时间是不固定的,无法保证先读到消息的consumer一定先完成操作,这样就会出现消息并没有按照顺序执行,造成数据顺序错误。

场景2: 一个queue对应一个consumer,consumer多线程消费

在这里插入图片描述

  • 假如只有一个queue,也只有一个consumer消费者,但是消费的时候开启了MQ本身的多线程并发消费,如下所示:
@RabbitListener(queues = RabbitConfig.JAVABOY_QUEUE_NAME,concurrency = "10")
public void handleMsg(String msg) {
   
   
    logger.info("msg:{}", msg);
}

concurrency = "10"这个相当于建立了 10 个 channel 去同时消费消息,对于这种情况,也是没法保证消费的有序的,因为本地代码执行的快慢、是否抛异常等等,都有可能会影响到消息的顺序。

  • 或者也有可能自己开启了线程池去并发处理:
//使用阿里巴巴推荐的创建线程池的方式
//通过ThreadPoolExecutor构造函数自定义参数创建
ThreadPoolExecutor executor = new ThreadPoolExecutor(
        CORE_POOL_SIZE,
        MAX_POOL_SIZE,
        KEEP_ALIVE_TIME,
        TimeUnit.SECONDS,
        new ArrayBlockingQueue<>(QUEUE_CAPACITY),
        new ThreadPoolExecutor.CallerRunsPolicy());


@RabbitListener(queues = RabbitConfig.JAVABOY_QUEUE_NAME)
public void handleMsg(String msg) {
   
   
    logger.info("msg:{}", msg);
	executor.execute(()->{
   
   
        //处理业务逻辑
    });

}

结合MQ如何解决?

这里我们讲解一下结合MQ的一些特性来实现,下文还会讲到依靠代码来实现!

发送消息的顺序

这里发送消息顺序代码会在后面内容更详细介绍

刚刚上面已经提到了发送消息也有可能导致错乱,那么对于这种情况,我们可以考虑使用 Redis 分布式锁来实现,发送消息之前,先去 Redis 上获取到锁,能拿到锁,然后再去发送消息,避免并发发送。

参考代码如下:

 /**
     * 发送顺序消息,可确保(busId+exchange+routingKey)相同的情况下,消息的顺序性
     * 这里我们使用的是rabbitmq,rabbitmq中 exchange+routingKey 可以确保消息被路由到相同的队列
     * <p>
     * 这里为什么要加入busId?
     * 以订单消息举例,所有订单消息都会进入同样的队列,订单有3种类型消息(创建、支付、发货)
     * 同一个订单的这3种类型的消息是要有序的,但是不同订单之间,他们不需要有序,所以这里加了个busId,此时就可以将订单id作为busId
     *
     * @param busId      业务id
     * @param exchange
     * @param routingKey
     * @param msgBody    消息体
     */
@Override
public void sendSequentialMsg(String busId, String exchange, String routingKey, Msg<?> msg) {
	Objects.nonNull(busId);

	String groupId = String.format("busId:%s,exchange:%s,routingKey:%s", busId, StringUtils.defaultString(exchange), StringUtils.defaultString(routingKey));

	//加分布式锁,保证同一个groupId时,顺序消息发送串行执行
	this.distributeLock.accept("sendSequentialMsg:" + groupId, lockKey -> {
		this.transactionTemplate.executeWithoutResult(action -> {
			//设置顺序消息groupId
			msg.setSequentialMsgGroupId(groupId);
			//设置顺序消息的编号
			msg.setSequentialMsgNumbering(this.sequentialMsgNumberGeneratorService.get(groupId));
			this.send(exchange, routingKey, msg);
		});
	});
}

队列中消息的顺序

RabbitMQ中,消息最终会保存在队列中,在同一个队列中,消息是顺序的,先进先出原则,这个由Rabbitmq保证,通常也不需要开发关心。

提示:不同队列中的消息顺序,是没有保证的,例如:进地铁站的时候,排了三个队伍,不同队伍之间的,不能确保谁先进站。

消费消息的顺序

场景1解决:依靠MQ本身-单活模式

刚刚我们已经分析了,一个queue,多个consumer去消费会导致消息错乱,究其原因就是有多个消费者,那么从源头解决,就是保证只有一个消费者去消费消息即可!

针对这种情况,我们采用MQ本身的特性,可以设置队列的“单活模式”。

x-single-active-consumer(单活模式): 单一活跃消费者(Single Active Consumer)表示队列中可以注册多个消费者,但是只允许一个消费者消费消息,只有在此消费者出现异常时,才会自动转移到另一个消费者进行消费。单一活跃消费者适用于需要保证消息消费顺序性,同时提供高可靠能力的场景。

仅RabbitMQ 3.8.35版本以后支持单一活跃消费者特性。

注意:如果一个队列已经创建为非x-single-active-consumer,而你想更改其为x-single-active-consumer,代码是会报错的,错误信息是:声明的队列的和server上的队列不一致。把原来队列删除了即可。

代码实现

创建一个配置类来定义队列、交换机和绑定,并启用 x-single-active-consumer 参数:

@Configuration
public class RabbitConfig {
   
   

    @Bean
    public Queue myQueue() {
   
   
        Map<String, Object> args = new HashMap<>();
		//最主要的区别就是声明了这个队列是单活模式
        args.put("x-single-active-consumer", true);
        return new Queue("myQueue", true, false, false, args);
    }

    @Bean
    public DirectExchange myExchange() {
   
   
        return new DirectExchange("myExchange");
    }

    @Bean
    public Binding binding(Queue myQueue, DirectExchange myExchange) {
   
   
        return BindingBuilder.bind(myQueue).to(myExchange).with("routingKey");
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Apple_Web

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值