RabbitMQ
1. 什么是MQ(Message Queue)
用于服务之间进行异步通信的中间件。
2. 为什么要使用MQ
2.1 解耦
- 用户下单,访问订单系统,订单系统调用库存系统、物流系统。突然某天库存系统出现问题挂了,从而导致订单系统也跟着挂了,用户得到一个下单失败的反馈,系统容错性低。
- 某天产品经理找到订单系统的开发程序员,要添加一个X系统,只能修改订单系统代码,过两天又要添加一个Y系统,又要修改订单系统,订单系统的可维护性低。
- 使用了MQ,订单系统只需要将消息发送给MQ,返回用户下单成功。其他需要消息的系统只需要从MQ中取出消费即可。
提升了系统的容错性和可维护性。
2.2 异步
- 用户下单,订单系统向数据库写入耗时20ms,还需要访问库存、物流系统,耗时400ms、400ms,下单共耗时:20+400+400=820ms,太慢了。一般来说,请求在200ms以内对用户来说才是无感知的。
- 使用了MQ,订单系统只需要向MQ中发送消息,耗时5ms,此时下单共耗时:20+5=25ms,此时用户体验好到爆。
提升了用户体验和系统吞吐量。
2.3 削峰
- A系统每秒最大处理1000请求,某天搞活动,请求增加到每秒5000个,A系统瞬间宕机了。
- 使用了MQ,用户请求发到MQ中,A系统慢慢的从MQ中每秒拉取1000个请求处理消费,游刃有余。
提高了系统的稳定性。
3. MQ的缺点与解决
3.1 系统可用性降低
由于引入了MQ,还需要保证MQ的高可用,服务才不会崩溃,如何保证MQ饿高可用性?
3.1.1 RabbitMQ高可用
RabbitMQ基于主从模式做高可用。
主从模式:又称之为主备模式。如果主节点(提供读写能力)挂了,就切换到备用节点(不提供读写能力),并将备用节点升级为主节点使用。
- 单机模式,不用
- 普通集群模式
多个机器部署RabbitMQ,创建的queue只存在于其中一个节点,其它节点同步元数据,若存放queue的节点宕机了,则无法从其他节点拉取数据。 - 镜像集群模式(高可用)
创建的queue存在每一个节点上,写消息到queue时,自动同步到所有的节点的queue上。
3.2 系统复杂度提高
由于引入了MQ进行异步调用,如何保证消息没有被重复消费?怎么处理消息丢失情况,怎么保证消息传递的顺序性?
3.2.1 消息幂等性处理 (保证消息没有重复消费)
- 什么是消息幂等性?
用户对于同一操作发起的一次或者多次请求的结果是一致的。 - 幂等性实现方案
-
使用数据库乐观锁机制
-
唯一ID+指纹码
- 唯一ID在加上指纹码(可以由系统生成,或者指定某种机制生成)保证这次操作是唯一的。
- 优势 实现简单,就一个拼接,查询是否重复即可。
- 弊端 高并发下有单个数据库写入的性能瓶颈 。
- 解决方案 根据ID进行分库分表算法路由。
对 id 进行算法路由,落到一个具体的数据库,然后当这个 id 第二次来又会落到这个数据库,这时候就像单库时的查重一样了。利用算法路由把单库的幂等变成多库的幂等,分摊数据流量压力,提高性能。
-
利用Redis原子性
幂等性概念及业界主流解决方案.
-
3.2.2 处理消息丢失情况
- Producer弄丢数据
- 事务机制(同步)
吞吐量降低,耗性能 - confirm确认模式(异步,推荐)
生产者发送消息到Broker中,无论成功与否都会触发一个回调函数confirmCallback(),第一个参数中有一个不可变的唯一 id,第二个参数表示是否接受成功,第三个参数表示失败的原因。
- 事务机制(同步)
/**
* @param correlationData 相关配置信息
* @param ack 成功为true,失败为false
* @param cause 失败原因
**/
confirm(CorrelationData correlationData, Booleean ack, String cause)
- RabbitMQ宕机
开启持久化。 - exchange到queue投递失败
- return退回模式
投递失败时会触发一个回调函数returnCallback方法,需要设定mandatory=true,否则失败消息将不会返回。
- return退回模式
/*
3. @param message 消息对象
4. @param reply 错误码
5. @param replyText 错误信息
6. @param exchange 交换机
7. @param routingKey 路由key
**/
returnedMessage(Message message, int replyCode, String replyText,
String exchange, String routingKey);
- Consumer Ack(MQ到Consumer失败)
-
自动确认(默认)
设置acknowledge=“none”。
消息一旦被Consumer接收到,则自动确认收到,并将相应的message移除。很有可能消息接收到,在业务处理出现异常,name消息将会丢失。 -
手动确认
acknowledge=“manual”
需要在业务处理成功后调用channel.basicAck(),手动签收,如果出现异常,调用channel.basicNack()方法,让其自动重新发送消息。 -
根据异常情况确认(不做了解)
acknowledge=“auto”
-
3.2.3 如何保证消息的顺序性
3.3 一致性问题
- A 系统处理完了直接返回成功了,人都以为你这个请求就成功了;但是问题是,要是 BCD 三个系统那里,BD 两个系统写库成功了,结果 C 系统写库失败了,数据就不一致了。
Producer将消息入库。发送消息到Q1,Consumer监听Q1接收消息,操作业务,入库。发送确认接收信息到Q2,回调检查服务监听Q2将消息入库。延迟消息发送到Q3,回调检查服务监听Q3将消息与数据库中的比对,一致什么都不敢。若没有,则调用Producer重新发送消息。极端情况下,Producer发送的消息与延迟消息都失败,定时检查服务将比对MDB与Pruducer的DB,若不一致,将Producer中多出的消息重新发送。
此时如何发送延迟消息又成了一个问题。
3.3.1 发送延迟消息
- 使用插件
rabbitmq-delayed-message-exchange
- 使用TTL + 死信队列(DLX)
- 什么是TTL(Time To Live)?
消息到达过期时间,还未被消费,会被自动清除。 - 什么是死信队列(dead Letter Exchange)?
当消息成为Dead Message后,可以被重新发送到另一个交换机,这个交换机就是DLX - 消息如何成为Dead Message(死信)?
- 队列长度到达限制。
- 存在消息过期,到达过期时间未被消费。
- 消费者拒收消息,basicNack/basicReject,并且不把消息放入原目标队列(requeue = false)
- 什么是TTL(Time To Live)?