一、基础
1、泛谈
(1)概念:Erlang语言实现 AMQP的消息中间件
(2)优势
- 异步:A系统串行调用多个系统,时间是递增的,而发到MQ,然后多个系统各自订阅消费,A系统发完消息便可以返回(比如下单发邮件)
- 解耦:A系统强耦合多个系统,后期系统变更可能就得修改A系统,而用MQ作为中间人,其它系统按需使用(比如业务记录日志)
- 限流削峰:某些场景比如秒杀,突然并发比平时高很多而系统不够强大可能就崩了,而用MQ可以限制流量,按能力消费数据,让处理并发量维持在一个稳定的水平(比如抢购)
(3)问题
- 系统可用性降低:MQ 出问题可能导致系统不能用
- 系统复杂度:要多维护 MQ中间件,还要考虑消息丢失、重复消费、顺序性等
- 数据一致性:消息在多个系统中,可能部分成功部分失败导致数据的不一致
2、元数据
- Queue 元数据
- Exchange元数据
- Binding 元数据
- Vhost元数据
二、消息流转
1、架构
- Producer:生产者,发送消息的那方,消息包含消息体(payload)和标签。
- Consumer:消费者,接收消息的那方,它只拿消息体而标签在路由过程会被丢弃所以它不知道谁是生产者。
- Broker:服务节点,一个实例(包含:Virtual host、Exchange、Queue)。
- Virtual host:虚拟路径,将服务节点进行分区,就像 mysql实例中多个数据库一样(包含:Exchange、Queue)。
- Exchange:交换器,负责将消息路由到一、多个队列,有多种类型。
- Queue:队列,存储消息的地方。
- RoutingKey:路由键,它是在消息发送的时候找指定队列。
- BindingKey:绑定交换器和队列,RoutingKey匹配上 BindingKey时消息就到指定的队列,它可用正则如 * 匹配单个词,# 匹配多个词(fanout类型交换器就忽略它)。
- Connection:TCP连接,它是负责 Producer、Consumer、Broker之间的连接,它建立在 Channel通道上且可拥有多个 Channel,但 Channel是线程不安全的所以多线程中不要用同个 Channel。
2、工作模式
- 简单模式:单producer,单queue,单consumer,发一个收一个。
- work模式:单p,单q,多c,多个消费端处理不同的消息。
- 发布/订阅模式:单p,多q,多c,借助交换机发到绑定该交换机的所有队列,假如每个消费者单独队列,则所有收到消息一样(一样只是内容相同)。
- 路由模式:发到 routingKey完全匹配 bindingKey的队列(队列定义时用 bindingKey绑了路由器),发布/订阅是会发到所有绑定的队列但路由模式则加了一层 routingKey的限制。
- 主题模式:bindingKey可用“*”和“#” 去模糊匹配(如routingKey=“com.example.rabbitmq”,bindingKey=“ com.*.rabbitmq”),在路由模式上增加了 bindingKey模糊匹配。# 表示0~多个单词,* 只能匹配一个单词
注:rabbitmq的设计就是同个队列的每条消息只有一位消费者,而想要实现多个消费者接到相同消息(相同而不是同一条)则需要采用多个队列然后消费者各自监听一个队列
3、交换机类型
- Fanout:广播式,消息经由交换机发到所有绑定的队列,routingKey无效(对应 发布/订阅模式)
- Direct:直连式,根据 routingKey 只发给一个队列(对应 路由模式)
- Topic:主题式,使用通配符的方式投递到匹配的 一、多个队列(对应 主题模式)
- Headers:通过请求消息头 headers来匹配 Queue 和 Exchange 绑定时指定的键值对
3、消息发送
(1)发送过程
- Producer连接Broker,建立Connection,开启Channel(Producer会声明 Exchange和Queue 并用 RoutingKey绑定)
- Producer发送消息到Broker,包括RoutingKey、Exchange等信息
- Exchange根据接收的RoutingKey找到匹配的Queue,找到后,将消息存入,找不到则根据配置选择丢弃或者回退
- 关闭Channel和Connection
(2)路由异常策略
- (mandatory = true) + ReturnListener:无适配队列会执行 ReturnListener回调
- 备份交换机:定义交换机 channel.exchangeDeclare 时添加alternate-exchange,消息找不到队列则可进入备份交换机
4、消息消费
(1)消费过程
- Consumer连接Broker,建立Connection,开启Channel
- Consumer向Broker请求消费Queue中的消息,可设置回调函数,然后等待,接收消息
- Consumer ack接收到的消息,rabbitMQ删除队列中被 ack 的消息
- 关闭Channel和Connection
(2)消费模式(二选一)
- 推:服务器提前推消息给消费者,消费者本地有缓冲区存储待处理的消息,随时可用,但可能缓冲区溢出
- 拉:单条获取消息
(3)消费控制
消费者可设置相关属性,控制服务器给客户端推送的消息数量
(4)确认/拒绝
- 确认:自动确认和手动确认。
- 拒绝:单条拒绝和批量拒绝。
(5)消息分发
- 轮询分发:每个消费者分发的消息个数一样
- 能者多劳:根据消费者能力分发个数(需开启手动确认机制才行)
三、消息存储
1、为何存盘
- 发送消息时设置 持久化(元数据有 队列索引:rabbit_queue_index、消息:rabbit_msg_store(含msg_store_persistent & msg_store_transient))
- mq 服务器内存不足,非持久化消息暂存到硬盘(元数据有 队列索引:rabbit_queue_index-queues、消息:msg_store_transient)
2、存储时机
- 1M的buffer区满了
- 固定刷盘时间(25ms)
- 进程信箱没消息,即写完消息后续暂无写入,则timeout刷盘
3、设置
- 交换机持久:如果不持久,重启之后消息还在,但交换机元数据丢失,也就是交换机属性等丢失,就无法发消息给该交换机了(channel.exchangedeclare:durable -> true)
- 队列持久:如果不持久,重启之后队列和消息都不存在,因为消息依托于队列(channel.queueDeclare:durable -> true )
- 消息持久:如果不持久,重启就没了(deliveryMode -> 2)
注:如果交换机和队列一个持久一个不持久,则不能绑定;如果消息持久,其它的不持久,重启机器就用不了,毫无意义
4、消息删除
- 所有文件的垃圾消息比例大于阈值,(GARBAGE_FRACTION = 0.5)时,会触发文件合并操作(至少有三个文件存在的情况下),以提高磁盘利用率
- 消费者ack消息后,消息被逻辑删除,当一个文件的数据都被逻辑删除,则物理删除该文件
5、过期时间
- 消息过期:
- 队列过期:
四、消息可靠
1、发送可靠(两种实现)
- 开启事务:将 channel设置为事务模式,操作:channel.txSelect、channel.txCommit、channel.txRollBack
- 单一/批量 confirm:消息发送到 mq服务器,会指派被一个id,如果发送到队列则回复确认(Basic.Ack)给发送端,如果可持久化的则会等到落盘(物理存盘)之后才返回,如果出错则返回 nack(Basic.Nack)
2、存储可靠:同时设置 交换机、队列、消息的持久
3、消费可靠:设置手动确认机制
四、特殊队列
1、死信队列
(1)死信消息
- 消息被拒绝(Basic.Reject或Basic.Nack)并且设置 requeue 参数的值为 false
- 消息过期了
- 队列达到最大的长度
(2)设置:通过channel.queueDeclare的参数设置 x-dead-letter-exchange 来添加
2、延迟队列
- TTL+死信队列,设置不同过期时间的队列,并搭配一个对应的死信队列,消费者监听死信队列,主队列过期时间到就扔到对应的死信队列,刚好可以给消费者消费达到延迟效果(建议给队列设置过期时间而不要给消息单独设置,因为 rabbitmq的机制是先检查前面的消息的 ttl,如果它没过期即使后面的消息已经过期了还是得等前面的消息出去了才能检测到)
- 插件(3.5.8后)
3、优先级队列
- 队列优先:channel.queueDeclare 参数设置 x-max-priority
- 消息优先:channel.basicPublish加入 priority
4、惰性队列
- 消息直接进文件不经过内存:channel.queueDeclare 参数设置 x-queue-mode = lazy ,减少内存消耗
六、部署
1、单机模式
(1)机制:只有一个mq实例,宕机就没了
2、普通集群
(1)机制:所有节点都只是同步元数据,而一份消息实体只存一个节点,访问到的节点没有数据它会去指定节点获取返回给客户端;如果某节点宕机了,则那个节点的消息实体就访问不了,因为只有它那一份
3、mirror 镜像集群:每个队列数据在多个节点都存一份
(1)机制:
- 队列的主从,实现master故障切换;但 slave/mirrors 收到所有操作(队列发布、向消费者传递消息、跟踪消费者的确认)都先应用到master,然后master 把操作同步到 slave,完了再发给消费者
- 主从复制是异步的,master收到一条消息写入本地存储,然后再发起写入slave的请求。当所有slave写入成功,master才会异步给client返回ack说这次写入成功了。mq接口本来就是异步的,主从也是异步的。
(2)机器宕机
- 跟该机器的所有连接都会断开,如果该机是slave则断开后就没了,若master则进行后面的操作
- 选最老的slave为master,其它slave进行同步消息
- 新master节点requeue所有unack消息(因为它不能确定消息是否被正确消费,所以直接重来,所以客户端可能有重复消息)
- 若客户端连着slave,且Basic.Consume消费时指定x-cancel-on-ha-failover参数,则客户端会收到Consumer Cancellation Notification通知。若未指定x-cancal-on-ha-failover参数,则消费者无法感知master宕机,会一直等待下去
(3)扩容
- ha-sync-mode=manual(默认):不会自动同步数据到新节点
- ha-sync-mode=automatic:会自动同步数据到新节点
- sync_queue <name> - 同步,cancel_sync_queue <name> - 取消:手动同步某队列,队列会阻塞
注:一般需要master先启动;如果slave先启动,master需在30s内启动
八、扩展
1、权限
2、监控
3、消息幂等
4、消息堆积
- 丢弃
- 惰性队列
- 集群迁移-shovel
5、消息顺序性
- 每个队列只保留一个消费者,然后需要保证顺序的数据都发到同个队列
6、重复消费
- 可用数据库唯一主键 设置bizId
- redis的分布式锁控制唯一
- 消费前,判断消费状态
7、消息丢失
8、MQ选型
- rabbitMq:一致性、稳定性、可靠性
- activeMq
- rocketMq
- Kafka