本文已参与「新人创作礼」活动,一起开启掘金创作之路。
RabbitMQ - 看这篇就够了
本文将从概念梳理,结合官网的6种工作模式引入 企业级 RabbitMQ最佳实战(附代码)
MQ的基本概念
1.MQ概述
MQ全称 Message Queue(消息队列),是在消息的传输过程中保存消息的容器。多用于分布式系统之间进行通信。
2.MQ的优劣势
-
MQ的优势
- 应用解耦;提高系统容错性和可维护性
- 异步提速;提升用户体验和系统吞吐量
- 削峰填谷;提高系统稳定性
-
MQ的劣势
- 系统可用性降低,系统引入的外部依赖越多,系统稳定性越差。
- 解决:保证MQ的高可用
- 系统复杂度提高,之前是同步的远程调用,现在是通过MQ进行异步调用。
- 解决:保证消息的不丢失、保证不重复消费、消费失败的重试处理等等
- 系统可用性降低,系统引入的外部依赖越多,系统稳定性越差。
3.常见的MQ产品
| RabbitMQ | ActiveMQ | RocketMQ | Kafka | |
|---|---|---|---|---|
| 公司/社区 | Rabbit | Apache | 阿里 | Apache |
| 开发语言 | Erlang | Java | Java | Scala&Java |
| 协议支持 | AMQP,XMPP,SMTP,STOMP | OpenWire,STOMP,REST,XMPP,AMQP | 自定义 | 自定义协议,社区封装了http协议支持 |
| 客户端支持语言 | 官方支持Erlang,Java,Ruby等,社区产出多种API,几乎支持所有语言 | Java,C,C++,Python,PHP,Perl,.net等 | Java,C++(不成熟) | 官方支持Java,社区产出多种API,如PHP,Python等 |
| 单机吞吐量 | 万级(其次) | 万级(最差) | 十万级(最好) | 十万级(次之) |
| 消息延迟 | 微妙级 | 毫秒级 | 毫秒级 | 毫秒以内 |
| 功能特性 | 并发能力强,性能极其好,延时低,社区活跃,管理界面丰富 | 老牌产品,成熟度高,文档较多 | MQ功能比较完备,扩展性佳 | 只支持主要的MQ功能,毕竟是为大数据领域准备的。 |
4.RabbitMQ基本概念
RabbitMQ 架构图
-
**Broker **: MQ接收和分发消息的应用;理解为 MQ服务
-
Virtual host : 类比 MySQL的一个database
- 出于多租户和安全因素设计的,把AMQP的基本组件划分到一个虚拟的分组中,类似于网络中的namespace概念。类比MySQL的一个database
- 当多个不同的用户使用同一个RabbitMQ serve提供的服务时,可以划分出多个vhost,每个用户在自己的vhost创建 exchange/queue等
-
Connection : publisher生产者 / consumer消费者 和 broker 之间的TCP连接
如果每一次访问 RabbitMQ 都建立一个 Connection,在消息量大的时候建立 TCP Connection的开销将是巨大的,效率也较低
- RabbitMQ 采用类似 NIO(Non-blocking I/O)的做法,选择 TCP 连接复用,不仅可以减少性能开销,同时也便于管理。
-
Channel 信道 : channel是建立在connection连接之上的一个虚拟连接,是双向数据流通道
- 一个Connection下可以建立多个Channel。 channel实例不能再线程间共享,每个线程开辟一个channel连接。
- Channel 是在 connection 内部建立的逻辑连接,如果应用程序支持多线程,通常每个thread创建单独的 channel 进行通讯,AMQP method 包含了channel id 帮助客户端和message broker 识别 channel,所以 channel 之间是完全隔离的。
- Channel 作为轻量级的 Connection 极大减少了操作系统建立 TCP connection 的开销
-
Connection 和 Channel 信道
- 保持Connection长链接
- 一个进程对应一个Connection
- Producer和Consumer分别使用不同的Connection进行消费发送和消费
-
Exchange : 交换机
- message 到达 broker 的第一站,根据分发规则,匹配查询表中的 routing key,分发消息到queue 中去。
- 常用的交换机类型有:
- fanout (multicast):广播,将消息交给所有绑定到交换机的队列
- direct (point-to-point):定向,把消息交给符合指定routing key的队列
- topic (publish-subscribe):把消息交给符合routing pattern(路由模式) 的队列
-
Queue : 队列
- 消息最终被送到这里等待 consumer 取走
-
**Binding **
- exchange 和 queue 之间的虚拟连接,binding 中可以包含 routing key。Binding 信息被保存到 exchange 中的查询表中,用于 message 的分发依据
六种工作模式
- 简单模式:一般不使用
- Work queues:工作队列模式(1、2两种工作模式不涉及交换机和路由Key)
-
Publish/Subscribe 发布与订阅模式:
-
Routing 路由模式:目前工作实践
- Topic 通配符模式
通配符规则:# 匹配一个或多个词,* 匹配不多不少恰好1个词, 例如:item.# 能够匹配 item.insert.abc 或者 item.insert,item.* 只能匹配 item.insert
- RPC 远程调用模式(远程调用,不太算 MQ;暂不作介绍)
RabbitMQ 高级特性
1.消息可靠性投递
- rabbitmq 整个消息投递的路径为: producer ---> 【 rabbitmq broker ---> exchange ---> queue 】---> consumer
生产者Product的callback
利用下面两个callback 控制消息的可靠性投递
-
消息从 producer —> exchange 则会返回一个 confirmCallback 。 返回参数中包含true false
-
设置ConnectionFactory的publisher-confirms="true" 开启 确认模式。
-
使用rabbitTemplate.setConfirmCallback设置回调函数。当消息发送到exchange后回调confirm方法。在方法中判断ack,如果为true,则发送成功,如果为false,则发送失败,需要处理。
//confirmCallback 代码示例: //测试 Confirm 模式 @Test public void testConfirm() { //定义回调 rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() { /** * * @param correlationData 相关配置信息 * @param ack exchange交换机 是否成功收到了消息。true 成功,false代表失败 * @param cause 失败原因 */ @Override public void confirm(CorrelationData correlationData, boolean ack, String cause) { //System.out.println("confirm方法被执行了...."+correlationData.getId()); //ack 为 true表示 消息已经到达交换机 if (ack) { //接收成功 System.out.println("接收成功消息" + cause); } else { //接收失败 System.out.println("接收失败消息" + cause); //做一些处理,让消息再次发送。 } } }); //进行消息发送 for (int i = 0; i < 5; i++) { rabbitTemplate.convertAndSend("test_exchange_confirm","confirm","message Confirm..."); } //进行睡眠操作 try { Thread.sleep(1000); } catch (Exception e) { e.printStackTrace(); } }
-
-
**消息从 exchange —> queue 投递失败则会返回一个 returnCallback **
-
设置ConnectionFactory的publisher-returns="true" 开启 退回模式。
-
使用rabbitTemplate.setReturnCallback设置退回函数,当消息从exchange路由到queue失败后,如果设置了rabbitTemplate.setMandatory(true)参数,则会将消息退回给producer。并执行回调函数returnedMessage。
//returnCallback 代码示例 //测试 return模式 @Test public void testReturn() { //设置交换机处理失败消息的模式 为true的时候,消息达到不了 队列时,会将消息重新返回给生产者 rabbitTemplate.setMandatory(true); //定义回调 rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() { /** * * @param message 消息对象 * @param replyCode 错误码 * @param replyText 错误信息 * @param exchange 交换机 * @param routingKey 路由键 */ @Override public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) { System.out.println("return 执行了...."); System.out.println("message:"+message); System.out.println("replyCode:"+replyCode); System.out.println("replyText:"+replyText); System.out.println("exchange:"+exchange); System.out.println("routingKey:"+routingKey); //处理 } }); //进行消息发送 rabbitTemplate.convertAndSend("test_exchange_confirm","confirm","message return..."); //进行睡眠操作 try { Thread.sleep(5000); } catch (Exception e) { e.printStackTrace(); } }
消费者Consumer ACK
ack指Acknowledge 确认。表示消费端收到消息后的确认方式。
-
无ack模式:acknowledge = “none”
NONE = no acks will be sent (incompatible with channelTransacted=true). RabbitMQ calls this “autoack” because the broker assumes all messages are acked without any action from the consumer. ————————————————
broker假设所有的消息都被ack了,**不需要消费者的任何操作。 **
消息一旦被Consumer接收到,则自动确认收到,并将相应 message 从 RabbitMQ 的消息缓存中移除。
-
无
ack模式:效率高,存在丢失大量消息的风险。 -
有
ack模式:效率低,不会丢消息。
-
-
手动确认:acknowledge = ”manual“
MANUAL= the listener must acknowledge all messages by calling Channel.basicAck().————————————————
MANUAL =监听器必须通过调用Channel.basicAck()来确认所有消息。
AcknowledgeMode.MANUAL模式需要人为地获取到channel之后调用方法向server发送ack(或消费失败时的nack)信息。
-
自动确认:acknowledge = "auto"
AcknowledgeMode.AUTO模式下,由spring-rabbit依据消息处理逻辑是否抛出异常自动发送ack(无异常)或nack(异常)到server端。
消息可靠性总结
- 持久化
- exchange要持久化
- queue要持久化
- message要持久化
- 生产者确认Confirm
- 消费者确认Ack模式
- MQ服务高可用集群
2.消费端限流
1.为什么要对消费端限流
假设一个场景,首先,我们 Rabbitmq 服务器积压了有上万条未处理的消息,我们随便打开一个消费者客户端,会出现这样情况: 巨量的消息瞬间全部推送过来,但是我们单个客户端无法同时处理这么多数据!
当数据量特别大的时候,我们对生产端限流肯定是不科学的,因为有时候并发量就是特别大,有时候并发量又特别少,我们无法约束生产端,这是用户的行为。所以我们应该对消费端限流,用于保持消费端的稳定,当消息数量激增的时候很有可能造成资源耗尽,以及影响服务的性能,导致系统的卡顿甚至直接崩溃。
2.限流的 api讲解
RabbitMQ 提供了一种 qos (服务质量保证)功能,即在非自动确认消息的前提下,如果一定数目的消息(通过基于 consume 或者 channel 设置 Qos 的值)未被确认前,不进行消费新的消息。
/**
* Request specific "quality of service" settings.
* These settings impose limits on the amount of data the server
* will deliver to consumers before requiring acknowledgements.
* Thus they provide a means of consumer-initiated flow control.
* @param prefetchSize maximum amount of content (measured in
* octets) that the server will deliver, 0 if unlimited
* @param prefetchCount maximum number of messages that the server
* will deliver, 0 if unlimited
* @param global true if the settings should be applied to the
* entire channel rather than each consumer
* @throws java.io.IOException if an error is encountered
*/
void basicQos(int prefetchSize, int prefetchCount, boolean global) throws IOException;
- prefetchSize:0,单条消息大小限制,0代表不限制
- prefetchCount:一次性消费的消息数量。会告诉 RabbitMQ 不要同时给一个消费者推送多于 N 个消息,即一旦有 N 个消息还没有 ack,则该 consumer 将 block 掉,直到有消息 ack。
- global:true、false 是否将上面设置应用于 channel,简单点说,就是上面限制是 channel 级别的还是 consumer 级别。当我们设置为 false 的时候生效,设置为 true 的时候没有了限流功能,因为 channel 级别尚未实现。
- 注意:prefetchSize 和 global 这两项,rabbitmq 没有实现,暂且不研究。特别注意一点,prefetchCount 在 no_ask=false 的情况下才生效,即在自动应答的情况下这两个值是不生效的。
3.如何对消费端进行限流
-
第一步,关闭自动ack; 消费端的确认模式一定为手动确认。acknowledge="manual"
-
第二步, listener.simpl.prefetch 参数,设置消费端 一次性拉取多少消息
spring: rabbitmq: listener: simple: acknowledge-mode: manual #开启手动签收 prefetch: 3 #一次就收三条
3.TTL 过期时间
- 设置队列过期时间使用参数:x-message-ttl,单位:ms(毫秒),会对整个队列消息统一过期。
@Bean
public Queue demoKdQueue(){
Map<String, Object> args = new HashMap<>(8);
// x-message-ttl 这里声明队列的TTL 消息存活时间
args.put("x-message-ttl", 20000);//10秒
//x-dead-letter-exchange 这里声明当前队列绑定的死信交换机
args.put("x-dead-letter-exchange", "dead-demoKdExchange");
//x-dead-letter-routing-key 这里声明当前队列的死信路由key
args.put("x-dead-letter-routing-key", "dead-demoKd");
//声明 demoKdQueue队列,并绑定死信;下面两种形式皆可
return QueueBuilder.durable("demoKdQueue").withArguments(args).build();
//return new Queue("demoKdQueue",true,false,false,args);
}
- 设置消息过期时间使用参数:expiration。单位:ms(毫秒),当该消息在队列头部时(消费时),会单独判断这一消息是否过期。
@RestController
public class TTLController {
@Autowired
private RabbitTemplate rabbitTemplate;
@PostMapping("/testTTL")
public String testTTL() {
MessageProperties messageProperties = new MessageProperties();
messageProperties.setExpiration("20000"); // 设置过期时间,单位:毫秒
byte[] msgBytes = "测试消息自动过期".getBytes();
Message message = new Message(msgBytes, messageProperties);
rabbitTemplate.convertAndSend("TTL_EXCHANGE", "TTL", message);
return "ok";
}
}
- 如果两者都进行了设置,以时间短的为准。
4.死信队列
- 1.死信交换机和死信队列和普通的没有区别
- 2. 当消息成为死信后,如果该队列绑定了死信交换机,则消息会被死信交换机重新路由到死信队列
- 3、消息成为死信的三种情况:
- 1. 队列消息长度到达限制;
- 2. 消费者拒接消费消息,basicNack/basicReject,并且不把消息重新放入原目标队列,requeue=false;
- 3. 原队列存在消息过期设置,消息到达超时时间未被消费
@Bean
public Queue demoKdQueue(){
Map<String, Object> args = new HashMap<>(8);
// x-message-ttl 这里声明队列的TTL 消息存活时间
args.put("x-message-ttl", 20000);//10秒
//x-dead-letter-exchange 这里声明当前队列绑定的死信交换机
args.put("x-dead-letter-exchange", "dead-demoKdExchange");
//x-dead-letter-routing-key 这里声明当前队列的死信路由key
args.put("x-dead-letter-routing-key", "dead-demoKd");
//声明 demoKdQueue队列,并绑定死信;下面两种形式皆可
return QueueBuilder.durable("demoKdQueue").withArguments(args).build();
//return new Queue("demoKdQueue",true,false,false,args);
}
5.延迟队列
- 1. 延迟队列 指消息进入队列后,可以被延迟一定时间,再进行消费。
- 2. RabbitMQ没有提供延迟队列功能,但是可以使用 : TTL + DLX 来实现延迟队列效果。
- 延迟队列的使用场景
- 1. 下单后,30分钟未支付,取消订单,回滚库存。
- 2. 新用户注册成功7天后,发送短信问候。
// 此队列无消费者监听,消费者监听死信队列:dead-demoKdQueue
@Bean
public Queue demoKdQueue(){
Map<String, Object> args = new HashMap<>(8);
// x-message-ttl 这里声明队列的TTL 消息存活时间
args.put("x-message-ttl", 20000);//10秒
//x-dead-letter-exchange 这里声明当前队列绑定的死信交换机
args.put("x-dead-letter-exchange", "dead-demoKdExchange");
//x-dead-letter-routing-key 这里声明当前队列的死信路由key
args.put("x-dead-letter-routing-key", "dead-demoKd");
//声明 demoKdQueue队列,并绑定死信;下面两种形式皆可
return QueueBuilder.durable("demoKdQueue").withArguments(args).build();
//return new Queue("demoKdQueue",true,false,false,args);
}
6.防止消息重复消费,幂等性保障
Redission 分布式锁
// 锁key
String lockKey = req.getSyncPosShopBaseInfoBo().getShopId().toString();
RLock lock = redissonClient.getLock(lockKey);
try {
// 最多等待10秒钟,获得锁返回true,10秒还未获得锁返回false,获得锁100秒后,如果锁没有释放,自动释放
if (lock.tryLock(10, 100, TimeUnit.SECONDS)) {
log.info("抢到锁");
//todo 执行业务:
} else {
log.error("没有抢到锁");
throw new Http400Exception("system_busy", "请稍后重试!");
}
} catch (Exception e) {
log.error("加锁时异常:",e);
//记录日志
throw new Http400Exception("error", "信息同步失败!");
} finally {
// 当前线程是否持有锁
if (lock.isHeldByCurrentThread()) {
// 释放锁
lock.unlock();
}
}
7.如何处理消息积压
产生的原因
- 消费者宕机积压
- 消费者消费能力不足导致积压
- 生产者发流量太大
解决方案
- 1、上线更多的消费者监听队列
- 2、先将积压的消息存到数据库中,然后依次取出慢慢消费
SpringBoot整合RabbitMQ
生产者
- 定义交换机,队列以及绑定关系的配置类
- 注入RabbitTemplate,调用方法,完成消息发送
- 如有需要做好 消息投递可靠性,利用两个callback
消费者
- 定义监听类,使用@RabbitListener注解完成队列监听。
三个场景 - 覆盖企业级RabbitMQ实战
1、异步解耦,消费失败不重试
- 配置消费者 手动确认消息
- 生产者 发送消息到 队列 ;消费者监听队列、
- 消费成功ack
- 消费失败nack,且不放回原队列;消息成为死信,自动丢到死信队列
- 死信队列消费者 监听 死信队列
- 做兜底业务:企业微信消息通知等等
2、解耦,消费失败需要重试3次
-
配置:自动确认 + 重试参数
- 注意:retry配置的重发是在消费端应用内处理的,不是rabbitMq服务重发
-
消费失败,记录错误日志,手动抛异常;消费端会按照retry配置参数进行重发
-
重发次数完,还是抛异常的话,消息自动成为死信,进入死信队列;
-
前提配置两个参数:
acknowledge-mode: auto default-requeue-rejected: false
3、订单在30分钟内未支付,则订单关闭
- 使用 TTL + 死信队列,实现延时队列
- 生产者 —>正常队列:队列TTL为 30min
- 定义死信交换机和死信队列;正常队列绑定死信交换机,死信路由
- 消费者监听 死信队列,判断订单是否支付,处理业务逻辑
附录:实战代码仓库:
项目启动后,可以访问http://127.0.0.1:8080/swagger-ui.html 灵活调用 controller接口测试