RocketMQ
RocketMQ是一款由阿里巴巴开源、现为 Apache 顶级项目的分布式消息中间件,以高吞吐、低延迟、金融级可靠和丰富的高级特性著称,是电商、金融、大数据等核心业务场景的主流选择。
核心角色
RocketMQ有4个核心角色:
- NameServer(路由注册中心)
- 轻量级的无状态路由发现服务,可集群部署
- 它的定位就是给消费者和生产者拉取broker的ip以及topic对应哪些broker、哪些队列的
- 仅仅提供broker的信息,不承担消息转发任务,也就是,发送者和生产者,拿到broker以及topic、队列信息后,它们直接和broker建立长连接,发送和消费消息,不经过nameServer
- Broker(消息存储与转发核心)
- 最核心的角色,承担消息的存储、转发、高可用
- 接收Producer的发来的消息,并按照配置将它们持久化,以及同步到从节点,返回消息接收结果
- 向消费端提供消息拉取服务,并维护消费组里每个消费者对应队列的offset,offset只有存在Broker里才能实现持久化,以及恢复,消息不重新消费,而是在断点位置继续消费
- 支持延迟消息、事务消息、死信队列、重试丢列
- 支持高可用,主从同步、Dledger 自动选主、多主多从
- Producer(消息生产者)
- 生产者,发送消息端,从NameServer拉取路由,负载均衡选Broker/Queue
- 支持同步、异步、单向发送
- Consumer(消息消费者)
- 消息的消费者,从NameServer拉取路由,建立和broker的连接
- 集群消费:同组消费者负载均衡,一条消息消费一次
- 广播消费:同组每个消费者都消费同一消息一次
- 一个Consumer可配置线程池线程数量去拉取队列消息,也可以配置同一时间同个队列只能有一个线程进行消费
整体理念
- Topic
- 一个topic对应于同一类消息(比方订单消息、库存消息)
- 一个topic会被分为4/8个消息队列,每个消息队列对应一个物理分片,即一个消息队列存储到一个broker里,一个topic下的所有消息队列会被均匀的分给集群里的所有broker,以实现高并发
- ConsumerGroup
- 一个ConsumerGroup内会有多个Consumer
- 一个ConsumerGroup订阅一个Topic,这个Topic下的所有消息队列会被均匀的分给group下的consumer,一个consumer可能会被分到1个或者多个消息队列,取决于group内的consumer数量以及topic的消息队列数量
- 当ConsumerGroup里只有一个consumer时,这个consumer承担ConsumerGroup订阅的topic的所有消息队列
- topic内的同一条消息,只会给一个ConsumerGroup消费一次。当有多个ConsumerGroup订阅同一个Topic时,同一条消息会给每个ConsumerGroup都消费一次
总结就是:
1个topic → 4/8个消息队列 → 均匀的分给brokers
1个consumerGroup(订阅1个topic) → n个consumer → 均匀的承担这个topic下的消息队列
怎么保证消息的可靠性?
从以下3个方面来保证可靠性
- 发送端
- 保证消息发送成功,发送失败重试,重试一定次数后存本地消息表定时任务兜底
- 使用MQ的事务消息,和本地事务保证原子性
- 使用同步发送,等broker确认
- broker
- 开启同步刷盘,消息commitLog落盘后才算成功
- 开启主从同步复制,消息等从节点同步成功后才返回
- 主节点挂了,重启后从本地恢复,消息还在。主节点恢复不了,由于从节点是同步复制的,Dledger选主,从节点顶上,数据和主节点一致
- 消费端
- 保证消息至少消费一次
- 消费失败 → 重试 → 丢死信队列
- 消费幂等
- 顺序消费要做好超时控制,避免阻塞整个队列
怎么保证消息的顺序性?
由于RocketMQ架构上已经决定了一个Topic会分成多个消息队列,然后一个消费组里的所有消费者均分这些消息队列,所以投递到这个topic的消息,必然会被多个消费者给消费,每个消费者消费完消息肯定是不确定顺序的。即使是在同一个队列里的消息,虽然它取出来是按顺序的,但是一个consumer去消费这个消息队列是可以配多个线程的,那每个线程执行完的顺序就是不确定的了。
我们在无法打破这个架构的情况下,要怎么保证消息消费的有序性呢?
首先,消息是一定要投递到同一个消息队列里,这样取出来才是按顺序的,其次消费端去消费的时候,要保证同一时间,只有一个线程取了消息,并且在消费消息,等到这个线程消费成功了,才允许线程池取下一个消息,这样在保证了取消息有序性以及消费消息有序性后,最终保证了消息消费得顺序性。
所幸,我们不需要自己实现这个逻辑,RocketMQ已经帮我们想好了。
发送端把消息发到同一个消息队列,通过orderId进行哈希取模,相同orderId的消息,选了同一个消息队列存,不同orderId可能存到不同的消息队列里,既保证单订单顺序,又保证整体并发(不同orderId仍能交给不同的consumer消费):
java
Message msg = new Message("order_topic", tags, orderId, content.getBytes());
// 手动选择队列:相同 orderId 进入同一个队列
producer.send(msg, new MessageQueueSelector() {
@Override
public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
String orderId = (String) arg; // 订单ID
int index = orderId.hashCode() % mqs.size(); // 固定哈希
return mqs.get(index);
}
}, orderId);
消费端开启同一时间只能有一个线程消费同一个topic下的消息
java
consumer.registerMessageListener(new MessageListenerOrderly() {
@Override
public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
// 单队列内严格顺序消费
// 一个队列同一时间只被一个线程消费
return ConsumeOrderlyStatus.SUCCESS;
}
});
缺点也很明显,同一队列里的消息是可能存在不同订单的(hash取模后归到一个队列),只要有一个异常订单卡住,那这个队列里,后面的消息就无法消费了,只能通过尽量增大队列数量,以及控制超时时间、重试次数进死信队列来缓解这个问题。
RocketMQ实现延迟队列底层逻辑?
5.x以下版本给Message 设置setDelayTimeLevel可实现延迟消息,level从1秒、5秒到2个小时,总共18个级别,底层是建了18个队列,分别存储不同级别,以及每个级别都有一个线程去盯着,每个线程只看队列的头部,假如头部的消息3秒后到时间,那线程就会休眠3秒后,去取消息。
使用18个级别去做的好处是添加消息时,永远往队列最后添加即可,不需要排序,取消息时,永远只需要关注队头的消息到期时间即可,时间复杂度时O(1).
取到到时间的消息,就会往正常topic的队列里放,然后被消费掉,偷天换日,其实一开始没往我们的topic队列里放,而是放到了18个级别的延迟队列里
5.x以上版本可以设置任意时间戳,底层是定长数据+链表,通过时间轮算法实现的,投递消息时,会计算这个消息属于哪个槽,每 1ms/100ms/1s 跑一次(按精度配置)取下一个槽,当前槽存的是个链表,链表里是当前轮和下n个周期轮的消息,会遍历链表取出过期的消息,投到正常topic的消息队列里,其他没到期的,等时间轮走完一周后,回到这个位置,再重新计算时间是否到期。