Rabbitmq
RabbitMQ
RabbbitMq的作用:
解耦:
削峰:
- 基于队列的特性,讲消息压在队列中,保证服务器不会一次性接收到过多请求
异步:
- 会开一个线程来处理服务
RabbitMQ工作模型:
交换机:
1.Direct Exchange直连
- 消息通过routing key发送到交换机,交换机根据routing key匹配binding key路由到指定队列。
- 一对一
2.Topic Exchange主题
- 消息通过routing key发送到交换机,交换机根据routing key的通配符匹配binding key路由到指定队列。
- 一对多
3.Fanout Exchange广播
- 将消息发送到指定交换机下面所有队列
- 一对所有
死信队列:
概述:
- 顾名思义,就是无法被消费的消息,字面意思可以这样理解,一般来说,producter将消息投递到broker或者直接到queue里了,consumer从queue取出消息进行消费,但由于特定的原因导致queue中的某些消息无法被消费,这样的消息如果没用后续的处理,就变成了死信,有死信自然就有了死信队列。
应用场景:
- 为了保证订单业务的消息数据不丢失,需要使用到RabbitMq的死信队列机制,当消息发生异常时,将消息投入死信队列中。还有比如说:用户在商城下单成功并点击去支付在指定时间未支付时自动失效。
来源:
- 消息TTL过期
- 队列达到最大长度(队列满了,无法再添加到数据到mq中)
- 消息被拒绝(basic.reject或basic.nack)并且requeue=false
工作模型:
1.消息TTL过期
/**
* 普通队列
* @return
*/
@Bean
public Queue queue(){
//绑定死信交换机
Map<String, Object> args = new HashMap<>(3);
//声明当前队列绑定的死信交换机
args.put("x-dead-letter-exchange", RabbitMqUtils.DEAD_EXCHANGE_KEY);
//声明当前队列的死信路由 key
args.put("x-dead-letter-routing-key", "dead.key");
//延时队列的使用
//设置ttl过期时间,如果指定时间内,没有被消费,就会被放入死信队列中
args.put("x-message-ttl", 10000);
// return new Queue(RabbitMqUtils.DIRECT1_QUEUE_KEY);
return QueueBuilder.durable(RabbitMqUtils.DIRECT1_QUEUE_KEY).withArguments(args).build();
}
2.队列达到最大长度
//设置正常队列长度限制
args.put("x-max-length",5);
3.消息被拒绝
//拒绝单个消息,重新写入队列
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false,true);
//应答单个消息
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
延迟队列:
概念:
- 延时队列,队列内部是有序的,最重要的特性就体现在它的延时属性上,延时队列中的元素是希望再指定时间到了以后或之前取出和处理,简单来说,延时队列就是用来存放需要在指定时间被处理的元素的队列。
使用场景:
- 订单在十分钟之内未支付则自动取消
- 新创建的店铺,如果在十天内都没有上传过商品,则自动发送消息提醒
- 用户注册成功后,如果三天内没用登录则进行短信提醒
- 用户发起退款,如果三天内没用得到处理则通知相关运营人员。
- 预定会议后,需要在预定的时间点前十分钟通知各个与会人员参加会议
设置:
前提就是都需要绑定死信交换机
-
设置队列中所有消息都是TTL
-
@Bean public Queue queue(){ //绑定死信交换机 Map<String, Object> args = new HashMap<>(3); //声明当前队列绑定的死信交换机 args.put("x-dead-letter-exchange", RabbitMqUtils.DEAD_EXCHANGE_KEY); //声明当前队列的死信路由 key args.put("x-dead-letter-routing-key", "dead.key"); //延时队列的使用 //设置ttl过期时间,如果指定时间内,没有被消费,就会被放入死信队列中 args.put("x-message-ttl", 10000); //设置正常队列长度限制 args.put("x-max-length",5); // return new Queue(RabbitMqUtils.DIRECT1_QUEUE_KEY); return QueueBuilder.durable(RabbitMqUtils.DIRECT1_QUEUE_KEY).withArguments(args).build(); }
-
-
设置单条消息的TTL
-
@GetMapping("/send2") public void send2(){ Mq mq = new Mq(); mq.setMessageId(UUID.randomUUID().toString()); mq.setCount(0); mq.setStatus(0); mq.setMessageContent("测试ttl"); MessageProperties messageProperties = new MessageProperties(); messageProperties.setMessageId(mq.getMessageId()); messageProperties.setContentType("text/plain"); messageProperties.setContentEncoding("utf-8"); Message message = new Message("hello,message idempotent!".getBytes(), messageProperties); //将消息存入数据库中 mqService.save(mq); rabbitmq.convertAndSend( RabbitMqUtils.DIRECT_EXCHANGE_KEY, "queue2.exchange", message,c->{ MessageProperties messageProperties1 = c.getMessageProperties(); //设置时间为20秒过期 messageProperties.setExpiration("20000"); return c; } ); }
-
两者的区别:
如果设置了队列的TTL属性,那么消息一旦过期,就会被队列丢弃(如果配置了死信队列被丢到死信队列中),而第二种方式,消息即使过期,也不一定会被马上丢弃,因为消息是否过期是在即将投递到消费者之前判定的,如果当前队列有严重的积压情况,则已过期的消息也许还能存活较长时间;另外,还需要注意一点,如果不设置TTL,表示消息永远不会过期,如果将TTl设置为0,则表示此时可以直接投递该消息到消费者,否则该消息将会被丢弃。
单条消息TTL虽然好是好但是还有问题:
如果发送两条消息 ,第一条消息延时时间为10分钟,第二条消息为2分钟,那么RabbitMq只会检查第一个消息是否过期,如果过期则丢到死信队列,如果第一个消息的延时时长很长,而第二个消息的延时时长很短,第二个消息并不会优先得到执行。
解决方案:
- 安装延时插件
- 在官网上下载 https://www.rabbitmq.com/community-plugins.html,下载
rabbitmq_delayed_message_exchange 插件,然后解压放置到 RabbitMQ 的插件目录。
进入 RabbitMQ 的安装目录下的 plgins 目录,执行下面命令让该插件生效,然后重启 RabbitMQ
/usr/lib/rabbitmq/lib/rabbitmq_server-3.8.8/plugins
rabbitmq-plugins enable rabbitmq_delayed_message_exchange
- 在官网上下载 https://www.rabbitmq.com/community-plugins.html,下载
重新定义要给延时交换机,测试就会发现,第二条消息会被先执行。
备份队列:
概述:
- 消息回退与消息确认的升级版。
- 如果交换机路由到队列的过程中出现了问题,既没有消息确认,或根据路由key找不到队列,此时消息就会到备份交换机中。
代码实现:
- 需要定义一个备份交换机,普通交换机,一个警告队列,一个备份队列,一个普通队列
- 备份交换机一般设置为扇出类型,也就是广播类型
/**
* 警告队列
* @return
*/
@Bean
public Queue warningQueue(){
return new Queue(RabbitMqUtils.WARNING_QUEUE_KEY);
}
/**
* 备份队列
* @return
*/
@Bean
public Queue backupQueue(){
return new Queue(RabbitMqUtils.BACK_UP_QUEUE_KEY);
}
/**
* 备份交换机
* @return
*/
@Bean
public FanoutExchange backupExchange(){
return new FanoutExchange(RabbitMqUtils.BACK_UP_EXCHANGE_KEY);
}
/**
* 警告队列与备份交换机
*/
@Bean
public Binding warningqueueBindExchange(
@Qualifier("warningQueue") Queue queue,
@Qualifier("backupExchange")FanoutExchange fanoutExchange
){
return BindingBuilder.bind(queue).to(fanoutExchange);
}
/**
* 备份队列与备份交换机绑定
* @param queue
* @param fanoutExchange
* @return
*/
@Bean
public Binding backupqueueBindExchange(
@Qualifier("backupQueue") Queue queue,
@Qualifier("backupExchange")FanoutExchange fanoutExchange
){
return BindingBuilder.bind(queue).to(fanoutExchange);
}
/**
* 普通交换机与备份交换机
*/
@Bean
public DirectExchange directExchange(){
return ExchangeBuilder
.directExchange(RabbitMqUtils.DIRECT_EXCHANGE_KEY)
.durable(true)
// 绑定备份交换机
.alternate(RabbitMqUtils.BACK_UP_EXCHANGE_KEY)
.build();
}
发送消息的时候指定一个不存在的路由key,进行测试
//监听警告队列
@RabbitListener(queues = RabbitMqUtils.WARNING_QUEUE_KEY)
public void directWARNINGqueue(Message message, Channel channel) throws IOException {
// channel.basicConsume(RabbitMqUtils.DIRECT1_QUEUE_KEY,true,message);
String msg = new String(message.getBody());
log.info("当前时间:{},收到警告队列信息{}",new Date(),msg);
}
//监听备份队列
@RabbitListener(queues = RabbitMqUtils.BACK_UP_QUEUE_KEY)
public void directBACKqueue(Message message){
String msg = new String(message.getBody());
log.info("当前时间:{},收到备份队列信息{}",new Date(),msg);
channel.basicPublish("",RabbitMqUtils.DIRECT1_QUEUE_KEY,null,msg.getBytes());
log.info("消息被重新投递到队列中!");
}
//向普通队列发送消息
rabbitTemplate.convertAndSend(
RabbitMqUtils.DIRECT_EXCHANGE_KEY,
"queue.exchangelll",
"message1"
);
mandatory 参数与备份交换机可以一起使用的时候,如果两者同时开启,消息究竟何去何从?谁优先级高,经过上面结果显示答案是备份交换机优先级高。
优先级队列:
1.在web页面添加
2.在队列中添加
Map<String, Object> params = new HashMap();
params.put("x-max-priority", 10);
channel.queueDeclare("hello", true, false, false, params);
3.在消息发送的时候添加
Map<String, Object> params = new HashMap();
params.put("x-max-priority", 10);
channel.queueDeclare("hello", true, false, false, params);
AMQP.BasicProperties properties = new
AMQP.BasicProperties().builder().priority(5).build();
4.注意
要让队列实现优先级需要做的事情有如下事情:
- 队列需要设置为优先级队列
- 消息需要设置消息的优先级
- 消费者需要等待消息已经发送到队列中去消费
- 因为这样才有机会对消息进行排序
惰性队列:
概述:
- 惰性队列会尽可能的将消息存入磁盘中,而在消费者消费到相应的消息时才会被加载到内存中,它的一个重要的设计目标时能够支持更长的队列,即支持更多的i西澳西存储。当消费者由于各种各样的原因(比如消费者下线、宕机亦或者是由于维护而关闭等)而致使长时间内不能消费不能消费消息造成堆积时,惰性队列就很有必要了。
- 默认情况下,当生产者将消息发送到 RabbitMQ 的时候,队列中的消息会尽可能的存储在内存之中,
这样可以更加快速的将消息发送给消费者。即使是持久化的消息,在被写入磁盘的同时也会在内存中驻留
一份备份。当 RabbitMQ 需要释放内存的时候,会将内存中的消息换页至磁盘中,这个操作会耗费较长的
时间,也会阻塞队列的操作,进而无法接收新的消息。虽然 RabbitMQ 的开发者们一直在升级相关的算法,
但是效果始终不太理想,尤其是在消息量特别大的时候
两种模式:
队列具备两种模式:default 和 lazy。默认的为 default 模式,在 3.6.0 之前的版本无需做任何变更。lazy模式即为惰性队列的模式,可以通过调用 channel.queueDeclare 方法的时候在参数中设置,也可以通过Policy 的方式设置,如果一个队列同时使用这两种方式设置的话,那么 Policy 的方式具备更高的优先级。如果要通过声明的方式改变已有队列的模式的话,那么只能先删除队列,然后再重新声明一个新的。
在队列声明的时候可以通过“x-queue-mode”参数来设置队列的模式,取值为“default”和“lazy”。下面示
例中演示了一个惰性队列的声明细节:
Map<String, Object> args = new HashMap<String, Object>();
args.put("x-queue-mode", "lazy");
channel.queueDeclare("myqueue", false, false, false, args);
在发送 1 百万条消息,每条消息大概占 1KB 的情况下,普通队列占用内存是 1.2GB,而惰性队列仅仅占用 1.5MB
面试题:
1.为什么用MQ?为什么要用RabbitMQ?
- MQ是用来解决通信问题,解耦服务与服务之间的关系
2.如果消息发送到客户端,消息在消费的过程出错了,怎么解决?
- 使用消息手动确认模式,当消息被成功消费了,确认应答,如果消息消费的过程中失败了,拒绝应答,将消息放入死信队列,重试.
写一个综合案例:
-
消息发到交换机失败提示
-
在rabbitmq配置文件中配置
-
// 配置rabbitmq的消息转化器,默认是java的序列化,可以设置成json // rabbitTemplate.setMessageConverter(converter()); //消息是否成功发送到Exchange rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> { if (ack){ log.info("消息发送到Exchange成功{},cause{}",correlationData,cause); }else{ log.error("消息发送到Exchange失败{},cause{}",correlationData,cause); } });
-
-
消息发送到交换机,没有路由的队列放入备份交换机中
-
配置备份交换机
-
/** * 普通交换机与备份交换机 */ @Bean public DirectExchange directExchange(){ return ExchangeBuilder .directExchange(RabbitMqUtils.DIRECT_EXCHANGE_KEY) .durable(true) // 绑定备份交换机 .alternate(RabbitMqUtils.BACK_UP_EXCHANGE_KEY) .build(); }
-
-
消息发送成功之后消费者因为业务失败导致消息消费失败,放入死信队列中,重试
-
配置死信交换机
-
/** * 普通队列 * 绑定了死信交换机 * 设置列队列中TTL消息指定时间内没有被消费,就放入死信队列中 * 设置了队列长度限制 * @return */ @Bean public Queue queue(){ //绑定死信交换机 Map<String, Object> args = new HashMap<>(3); //声明当前队列绑定的死信交换机 args.put("x-dead-letter-exchange", RabbitMqUtils.DEAD_EXCHANGE_KEY); //声明当前队列的死信路由 key args.put("x-dead-letter-routing-key", "dead.key"); //延时队列的使用 //设置ttl过期时间,如果指定时间内,没有被消费,就会被放入死信队列中 args.put("x-message-ttl", 10000); //设置正常队列长度限制 args.put("x-max-length",5); // return new Queue(RabbitMqUtils.DIRECT1_QUEUE_KEY); return QueueBuilder.durable(RabbitMqUtils.DIRECT1_QUEUE_KEY).withArguments(args).build(); }
-
-
消息应答,消息确认,高级消息确认,消息入库
-
开启消息确认模式
-
rabbitmq: # 确保消息成功发送到交换机 publisher-confirm-type: correlated # 确保消息在未被队列接收时返回 publisher-returns: true listener: simple: # 指定消息没有被队列接收是是否强行退回还是直接丢弃 # acknowledge-mode: manual # 指定一个请求能处理多少个消息 prefetch: 100
-
配置setMandatory
-
//开启确认应答 rabbitTemplate.setMandatory(false); //消息从Exchange路由到Queue失败回调. rabbitTemplate.setReturnsCallback((returnCallback)->{ log.error("消息从Exchange路由到Queue失败:exchange:{},route:{},replyCode:{},replyText:{},message:{}", returnCallback.getExchange(), returnCallback.getRoutingKey(), returnCallback.getReplyCode(), returnCallback.getReplyText(), returnCallback.getMessage() ); });
-
手动消息确认与拒绝
-
/** * 消息确认: * 参数一:消息标签 * 参数二:true 以确认所有消息,包括提供的交付标签; false 仅确认提供的交付标签。 */ channel.basicAck(message.getMessageProperties().getDeliveryTag(),false); /** * 消息拒绝:(在业务出异常的时候调用) * 参数一:消息标签 * 参数二:true 以确认所有消息,包括提供的交付标签; false 仅确认提供的交付标签。 * 参数三:消息是丢弃还是从新排队,丢弃后会直接进入死信队列中 */ channel.basicNack(message.getMessageProperties().getDeliveryTag(), false,false);
-
当消息确认与备份交换机同时设置了,优先使用备份交换机配置
-
-
延迟队列,惰性队列
- 延时队列:
- 可以设置队列里的消息在多长时间之后执行。
- 惰性队列:
- 将消息加载进磁盘,在被消费的时候才会加载进内存。
本文中所有代码在码云上,欢迎各位大佬下载!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!