RabbitMQ
一、消息队列解决的问题
消息队列的调用通常都是异步的,只有在可以接受是异步调用(无法立刻得知处理结果)的情况下,再考虑采用消息队列来实现。
1. 系统解偶
实现系统与系统之间的松偶合,可以将主要流程与次要流程分离。
比如: 当用户在电商网站上购买商品,生成订单、扣减库存 为主要流程,发优惠券、增加用户积分 为次要流程,不需要立即处理完成,此时就可以将次要流程交给消息队列下游的服务去处理。
2. 流量削峰填谷
秒杀场景,将秒杀请求暂存于消息队列,业务系统响应 "秒杀结果正在处理中。。。",之后系统将持续消化消息队列中的消息,避免因流量洪峰的到来,导致系统崩溃。
但因为消息的发送与处理属于异步请求,所以用户将无法立即收到处理结果,这是在采用消息队列技术时必须要考量的缺点。
3. 实现最终一致性
使用消息队列调用其他的微服务,依靠消息队列的重试机制,保证一系列的服务调用可以正确地运行完成。
记得必须考虑幂等性问题,避免出现多次调用产生不同结果的情况。
4. 可持久化计时器
使用死信队列与TTL,当消息过期时,消息会放入死信队列,从死信队列中获取消息,达到计时器目的。
使用死信队列作为定时器来使用会存在一个问题,就是在正常队列中,只会检查头部的第一个消息是否过期,其他消息不检查,这就导致如果每个消息TTL有长有短,死信队列将会失去定时器的作用,此问题可以使用"延迟队列插件"来解决。
TTL : Time To Live 过期时间
设置方式 : (1) Queue 属性,队列中所有消息相同 (2) 消息自身
设置冲突时,以较小值为准
二、消息队列选型
1. RabbitMQ
优点:
- 支持AMQP协议 (支持绝大多数编程语言)
- 消息延迟在微秒级别
缺点:
- 消息堆积会导致性能大幅下降 (队列状态机制导致,当消息过多时,索引与消息本体都会被放到磁盘上)
- 性能差 (单机每秒吞吐量1W量级)
- 使用Erlang语言开发,二次开发代价高
2. RocketMQ
优点:
- Java开发,阅读源码、二次开发 方便
- 消息延迟在毫秒级别
- 性能、稳定性高 (单机吞吐量在10W量级)
缺点:
- 与周边系统集成、兼容不是很好
3. Kafka
优点:
- 可靠性、稳定性 高
- 与周边系统兼容性好(尤其是与 大数据、流式计算 相关)
- 可伸缩,支持分区、副本、容错
缺点:
- 延迟高 (异步、批处理),不适合电商场景
三、消息主体类型
- 简单文本 (TextMessage)
- 可串行化对象 (ObjectMessage)
- 属性集合 (MapMessage)
- 字节流 (BytesMessage)
- 原始值流 (StreamMessage)
- 无有效负载 (Message)
四、AMQP 概念
消息发送过程
Publisher 指定消息的 RoutingKey,并将消息发送到 Exchange 中,Exchange 会根据 Bindings 查找此 RoutingKey 应该路由到哪些(0~∞个) Queue 中,并将消息路由过去,Consumer 从 Queue 中获取消息,并对消息进行处理 。
角色
- Publisher: 消息发送者。将指定 RoutingKey 的消息发送到 Exchange 中,则 Queue 可以收到对应的消息。
- Server: MQ服务的实例。也称为 Broker。
- Virtual Host: 虚拟主机。一个 Server 下可以有多个虚拟主机,主要用途为隔离项目。
- Exchange: 交换器。接收 Producer 发来的消息,并将消息路由到对应的 Queue 中。
- Routing Key: 路由键。指定消息的路由规则。Routing Key、Bindings、Exchange 三者须要互相配合使用。
- Bindings: 绑定。指定 Exchange 与 Queue 之间的绑定关系。
- Queue: 消息队列。存储 Message 的容器。
- Message: 消息。Publisher 想要告诉 Consumer 的消息。
- Consumer: 消息消费者。负责处理由 Publisher 发送过来的消息。
AMQP 在传输数据时使用 TCP/IP 流协议进行传输,而 TCP/IP 是没有办法界定数据侦(一个完整数据的范围),所以会在每个数据侦头部加上该数据侦的大小。
五、交换器类型
-
Fanout: 广播,不需要考虑 RoutingKey 与 Bindings。将消息路由到所有与该交换器绑定的队列中。
(发布/订阅模式: 在线消费者都能收到消息,如果消费者不在线,则无法接收消息)
-
Direct: 将消息路由到 RoutingKey 与 BindingKey 完全匹配的队列中。
针对单一队列,属于队列模式(进到该队列的一条消息只会由一个消费者获得)
但如果有多个不同的队列,都绑定同一个 BindingKey ,则多个队列都会收到消息
-
Topic: Direct类型的扩展,可以使用 " * "、"#" 关键字进行模糊匹配。" * " 匹配一个单词,"#" 匹配0或多个单词。
(发布/订阅模式: 在线消费者都能收到消息,如果消费者不在线,则无法接收消息)
RoutingKey 与 BindingKey 使用 "."分单词表示,EX: eroupe.user.news
六、消费者模式
- 推模式: 相当于监听器模式
- 拉模式: 消费者主动获取消息
七、存储机制
消息类型
- 持久化消息
-
消息到达队列时直接在磁盘进行备份
- 非持久化消息
- 当内存压力大时,将消息转存到磁盘上。MQ重启后丢失
消息文件
- 索引
-
纪录 (1) 消息位置 (2) 消息是否已被消费者接收 (3) 消息是否已被消费者ACK。文件后坠 .idx
-
当消息本体较小(默认4096B)时,消息本体会直接存储在索引中。
- 消息本体
- 键值对型式存储,所有队列使用同一块存储空间。文件后坠 .rdq
消息删除
-
删除消息时,只是从ETS表(Erlang Term Storage 负责记录消息在文档中的映射关系) 删除纪录,并不会对文档中的消息进行删除
-
只有当垃圾数据的比例大于 grabage_fraction 时(默认0.5),才会触发垃圾回收,将两个消息本体的文件进行合并
队列状态
- alpha: 索引、消息 都存内存
- beta: 索引放内存,消息存磁盘
- gama: 索引、消息 都放磁盘
八、消息可靠性
保证发送者的消息有投递到"队列"中
-
异常捕获
不是 100% 可靠
-
Channel事务
开销大,不推荐
-
发送端确认
信道(Channel)设为confirm模式,同步阻塞。可以使用 批处理、异步回调 来提高效率。
保证消息被"消费者"成功消费
设置 Listener 的 ACK Mode
消费者消费消息后,需要发送 ACK 给Broker,否则将重发消息直到消息过期。
-
NONE模式:
消费者自行捕获异常,有丢失数据的风险。
-
AUTO模式:
抛出异常,则将消息放回Queue中,重新发送消息。
-
MANUAL模式:
消费者手动调用 Channel 方法返回ACK。
九、持久化
MQ重启后不丢失
- Exchange 持久化: durable参数 = true
- Queue 持久化: durable参数 = ture
- 消息持久化: deliveryMode = 2
十、MQ服务器安全
阻塞生产者
不让生产者推消息到MQ中
- 磁盘可用空间
- 磁盘内存比
- 可用内存
阻塞消费者
不继续推消息到消费者中 (对推模式有效,拉模式无效,不支持NONE-ACK模式)
Channel 设置 basicQoS,当未确认消息达到上限后,则不继续发送消息。
保证消息幂等性
RabbitMQ 仅支持 "最多一次" 和 "最少一次" 的消息可靠传输
必须保证消息多次运行的结果与只运行一次的结果相同
-
数据库唯一索引
当向数据库插入相同的订单号,报错,事务回滚。
-
前置检查
先查找数据库中是否有对应的纪录,没有则利用排他锁完成操作(避免并发问题)。
-
消息唯一ID
对每条消息都生成唯一ID,消费前先判断在分布式缓存中是否存在。
接口幂等性
上游在调用时,传递一个GUID,如果发现已经处理过,则返回【和上次相同的处理结果】
十一、 提高性能
提升下游吞吐量
- 优化服务性能
- 增加消费者节点
- 增加并发消费线程数
分布式架构
具体集群搭建使用 HAProxy 作集群模式的负载均衡,结合镜像队列模式作高可用
-
主备模式
-
只有一个节点在工作(备份节点不能读写),且需要一个共享存储 - 不推荐
-
-
铲子模式 (Shovel)
-
利用插件实现跨机房复制
-
-
集群模式
-
所有节点存储 (1) 队列 (2) 交换器 (3)绑定 (4) vhost 的元数据信息
-
各节点存储各自的消息分片
-
-
镜像队列模式
针对集群模式的高可用功能补充
-
当节点失效,将自动切换到集群中的另一个镜像节点
-
使用多副本冗于,保证消息不丢失
-
读写都在Master,Slave仅用于替代Master
-