记录一下秋招面试遇到关于 消息队列 的问题,会持续更新。
文章目录- 1. 消息队列
- 1.1 怎样设计消息队列
- 1.2 消息队列的作用
- 2. RabbitMQ
- 2.1 架构设计
- 2.2 多消费者消息分配
- 2.3 消费失败处理机制
- 2.4 保证消息只会被消费一次
分析:1. 从整体到细节,从业务场景到技术实现。2. 以现有产品为基础。
实现思路:
- 1.实现一个单机的队列数据结构。 - 高效、可扩展。
- 消息队列需要支持可伸缩性,选择 BlockingQueue。
- 对 Message 进行封装。
- 2.将单机队列扩展成为分布式队列。 - 分布式集群管理。
- 使用 zookeeper 实现配置管理。
- 3.基于 topic 定制消息路由策略。 - 发送者路由策略,消费者与队列对应关系,消费者路由策略。
- 参考 RabbitMQ 的模型,通过 exchange + routingKey 进行路由。
- 设计一个队列只能由一个消费者消费,保证队列的先进先出这个特性,根据网络距离去绑定队列和消费者,实现 N 对 1 的关系。
- 异步线程取消息,一批取 32 个消息,保证消息链路不被冲破。
- 取消息的时候先锁队列,消费完一个队列再去消费下个队列,保证消息顺序。
- 4.实现高效的网络通信。 - Netty、Http。
- 使用 Netty 进行网络通信。
- IO 线程模型:同步非阻塞,最少资源做更多的事。
- 内存零拷贝:尽量减少不必要的内存拷贝,实现了更高效率的传输。
- 内存池设计:申请的内存可以重用,主要指直接内存。内部实现是用一颗二叉查找树管理内存分配情况。
- 串行化处理读写:避免使用锁带来的性能开销。
- 高性能序列化协议:支持 protobuf 等高性能序列化协议。
- 5.规划日志文件,实现文件高效读写。 - 零拷贝,顺序写。
- 参考 kafka,RocketMQ 通过零拷贝技术来优化文件读写。服务重启后快速还原运行现场。
- 6.定制高级功能,贴合实际。 - 死信队列,延迟队列,事务消息等等。
- 比如支持定时操作,使用 DelayQueue 去完成十分钟未支付取消订单等等。
异步:加快响应速度
解耦:解除上下游服务之间的耦合性
削峰:从消息队列慢慢拉去消息,降低数据库的压力
对应的就需要去考虑系统的可用性(MQ 挂了怎么办),复杂度,一致性(消息重复消费等)。
2. RabbitMQ 2.1 架构设计Broker:RabbitMQ 的服务节点。
Queue:存储消息的队列。多个消费者可以订阅同一个队列,消息轮询给多个消费者消费,而不是每个消费者都收到所有消息进行消费。
Exchange:生产者投递的消息发送到交换机,路由到一个或多个队列中。如果路由不到,就返回给生产者或者丢弃,或者做其他处理。
RoutingKey:路由的 key。
Bingding:通过绑定将交换机和队列关联起来。
信道:信道是建立在 Connection 之上的虚拟连接。当应用程序与 RabbitMQ Broker 建立 TCP 连接的时候,客户端紧接着建立一个 AMQP 信道,每个信道分配一个唯一 ID。RabbitMQ 处理的每条 AMQP 指定都是通过信道完成的。(信道就是电缆中的光纤束)
2.2 多消费者消息分配channel.basicQos(1);
单个队列,多个消费者的时候,队列会以轮询的方式将消息分别发送给各个消费者去处理。但是如果消费者群里面的消费消息的能力不均匀的话,可以设置为 1,这样每个消费者在 ACK 之后。队列才会分配给它下一条消息。
2.3 消费失败处理机制消费者在声明队列时,可以指定 noAck 参数,当 noAck = false 时,RabbitMQ 会等待消费者显式发回 ack 信号后才从内存(磁盘)中移除。否则,消息被消费后会被立即删除。
消费者接受每一条消息后必须进行确认(消息接受和消息确认是两个不同的操作),只有消费者确认了消息,RabbitMQ 才能安全地把消息从队列中删除。
RabbitMQ 不会为未 ack 的消息设置超时时间,它判断此消息是否需要重新投递给消费者的唯一依据是消费该消息的消费者连接是否已经断开,这么设计的原因是 RabbitMQ 允许消费者消费一条消息的时间很长,一是为了限流,二是为了保证数据的最终一致性。
如果消费者返回 ack 之前断开连接,RabbitMQ 会把消息重新分配给下一个订阅的消费者,这里有重复消费的隐患,所以需要业务代码那边去实现接口幂等性。
2.4 保证消息只会被消费一次所有 MQ 产品并没有提供主动解决幂等性的机制,需要由消费者自行控制。
解决方案:统一 ID 分配,借此进行幂等性判断。(orderID, messageID)



