文章目录
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");
}
}