RabbitMq
1,RabbitMq 简介
是实现了高级消息队列协议(AMQP)的开源消息代理软件(亦称面向消息的中间件)。RabbitMQ服务器是用Erlang语言编写的,而集群和故障转移是构建在开放电信平台框架上的。所有主要的编程语言均有与代理接口通讯的客户端库。
三个作用:异步处理,流量削峰,应用解耦
2,RabbitMq 几个术语
1. Exchange - 交换机
生产者将消息发送给交换机,交换机按照一定规则分发消息给指定队列。消息根据交换机类型和 binding 可以投递到多个队列中。
常用的交换机有四种。
- 直连交换机
directExchange: 根据 routeKey 匹配队列
@Bean public DirectExchange directExchangeDemo(){ /* * 直连交换机 * 一共四个参数:String name, boolean durable, boolean autoDelete, Map<String, Object> arguments * name: 名称 * durable: 持久化 * autoDelete:自动删除 * arguments:参数 * * */ return new DirectExchange("directExchangeTest",true,false); }
- 扇形交换机
FanoutExchange:不用匹配 routekey,所有队列都能获取扇形交换机分发的消息
@Bean public FanoutExchange fanoutExchangeDemo(){ /* 扇形交换机 */ return new FanoutExchange("fanoutExchangeTest",true,false); }
- 主题交换机
TopicExchange: 增强版的直连交换机,路由键 routekey 中,* 代表匹配任意一个单词,# 代表匹配任意一个或多个单侧, . 代表一个部分(www.# 可以匹配 www.aaa)
@Bean public TopicExchange topicExchangeDemo(){ return new TopicExchange("topicExchangeTest1",true,false); }
- 头部交换机
HeadersExchange : 通过头部键值对匹配队列的交换机
@Bean public HeadersExchange headersExchangeDemo(){ /* 头部交换机 */ return new HeadersExchange("headersExchangeTest",true,false); }
2. Broker
接收和分发消息的应用,就是 mq 的服务端。
3. Virtual host
虚拟分组,类似于 nameSpace。
4. Connection
publisher/customer 和 broker 直接的连接。
5. Channel
信道,复用 Connection。
6. Exchange
交换机,message 到达 Broker 的第一站,根据分发规则,匹配查询表中的 routing key,分发消息到 queue 中。
7. Queue
最终消息被送到这里等待被 customer 取走。
8. Binding
exchange 和 queue 之间的虚拟连接, binding 中可以包含 routing key, Binding 信息被保存到 exchange 中的查询表中,用于 message 的分发依据
3. 消息队列大致使用过程
- 启动一个消息队列服务器
- 客户端连接到消息队列服务器,打开一个 channel
- 客户端声明一个 exchange,并设置相关属性
- 客户端声明一个 queue,并设置相关属性
- 客户端使用 routing key,在 exchage 和 queue 中建立绑定关系
- 生产者投递消息到 exchange,exchange 接收到消息后,就根据消息的 key 和已经设置的 binding,进行消息路由,将消息投递到对应的队列中。
- 消费者消费队列中的消息。
4,消息应答
创建消费者:
/** * 消费者消费消息 * 1,消费哪个队列 * 2,消费成功之后是否要自动应答 "true" 代表自动应答 "false" 手动应答 * 3,消费者未成功消费的回调 * */ channel.basicConsume(String queue, boolean autoAck, DeliverCallback deliverCallback, CancelCallback cancelCallback);
确认消费:
/** * 参数 1,消息标记 * 2,false 表示只应答接收到那个传递的消息 * 用于肯定确认:RabbitMQ 已知道该消息并且成功的处理消息,可以将其丢弃了 * * multiple 的 true 和 false 代表不同意思: * true 表示批量应答 channel 上未应答的消息,false 表示只应答当前 channel 上的消息。 * */ Channel.basicAck(long deliveryTag, boolean multiple)
拒绝消费
/** * 参数 1,消息标记 * 2,是否应答 channel 上所有未应答的消息 * 3,是否重新入列 * 用于否定消息 * */ Channel.basicNack(long deliveryTag, boolean multiple, boolean requeue)
拒绝消费
/** * 参数 1,消息标记 * 3,是否重新入列 * 用于否定消息,相比 basicNack 缺少 multiple 参数,不能批量确认 * */ Channel.basicReject(long deliveryTag, boolean requeue)
手动确认 demo
public class Customer1 { public static void main(String[] args) throws IOException, TimeoutException { ConnectionFactory factory = new ConnectionFactory(); factory.setHost("127.0.0.1"); factory.setUsername("yanqi"); factory.setPassword("5211314"); factory.setVirtualHost("love"); Connection connection = factory.newConnection(); Channel channel = connection.createChannel(); System.out.println("Custom1 等待接收消息...."); //消费消息 DeliverCallback deliverCallback = new DeliverCallback() { @Override public void handle(String s, Delivery delivery) throws IOException { String message = new String(delivery.getBody()); System.out.println(message); channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false); } }; //取消消息 //取消消费的一个回调接口 如在消费的时候队列被删除掉了 CancelCallback cancelCallback = (consumerTag) -> { System.out.println(consumerTag); System.out.println("消息消费被中断"); }; /** * 消费者消费消息 * 1.消费哪个队列 * 2.消费成功之后是否要自动应答 true 代表自动应答 false 手动应答 * 3,消费成功 * 4.消费者未成功消费的回调 */ channel.basicConsume("中华艺术宫", false, deliverCallback, cancelCallback); } }
5,队列持久化
//durable:true 表示队列持久化,false 表示不持久化,重启 rabbitmq 队列就没了 Queue.DeclareOk queueDeclare(String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments);
/** * 生成一个队列 * 1.队列名称 * 2.队列里面的消息是否持久化 默认消息存储在内存中 * 3.该队列是否只供一个消费者进行消费 是否进行共享 true 可以多个消费者消费 * 4.是否自动删除 最后一个消费者端开连接以后 该队列是否自动删除 true 自动删除 * 5.其他参数 */ channel.queueDeclare("中华艺术宫", false, false, false, null);
6,消息持久化
//props 中添加 MessageProperties.PERSISTENT_TEXT_PLAIN basicPublish(String exchange, String routingKey, BasicProperties props, byte[] body);
/** * 发送一个消息 * 1.发送到那个交换机 * 2.路由的 key 是哪个 * 3.其他的参数信息,比如 MessageProperties.PERSISTENT_TEXT_PLAIN 消息持久化 * 4.发送消息的消息体 */ channel.basicPublish("", "中华艺术宫", MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes());
7,预取值
Channel 上未确认的缓冲区,通过 basicQos(int prefetchCount) 设置值,避免缓冲区无限制未确认大小。通过设置预取值,还可以根据不同消费者性能问题实现不公平分发。
8,发布确认
生成者将 Channel 设置成 confirm 模式,一旦消息被投递到所有匹配的队列之后, broker就会发送一个确认给生产者(包含消息的唯一 ID),这就使得生产者知道消息已经正确到达目的队列了。
发布确认
public class PushlierConfirm { public static void main(String[] args) { //创建一个连接工厂 ConnectionFactory factory = new ConnectionFactory(); factory.setHost("127.0.0.1"); factory.setPort(5672); factory.setUsername("yanqi"); factory.setPassword("5211314"); factory.setVirtualHost("love"); try ( Connection connection = factory.newConnection(); Channel channel = connection.createChannel()) { //发布确认 channel.confirmSelect(); /** * 生成一个队列 * 1.队列名称 * 2.队列里面的消息是否持久化 默认消息存储在内存中 * 3.该队列是否只供一个消费者进行消费 是否进行共享 true 可以多个消费者消费 * 4.是否自动删除 最后一个消费者端开连接以后 该队列是否自动删除 true 自动删除 * 5.其他参数 */ channel.queueDeclare("中华艺术宫", false, false, false, null); /** * 发送一个消息 * 1.发送到那个交换机 * 2.路由的 key 是哪个 * 3.其他的参数信息,比如 MessageProperties.PERSISTENT_TEXT_PLAIN 消息持久化 * 4.发送消息的消息体 */ int i = 0; while(true){ String message = "hello world--" + (++i); channel.basicPublish("", "中华艺术宫", MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes()); boolean b = channel.waitForConfirms(); if(b){ System.out.println("消息 " + i + " 发布成功!"); }else{ System.out.println("消息 " + i + " 发布失败!"); } Thread.sleep(3_000); } //System.out.println("消息发送完毕"); } catch (TimeoutException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } catch (InterruptedException e) { e.printStackTrace(); } } }
9,死信队列
无法被消费的消息。
来源:
1)消息 TTL 过期
2)队列达到最大长度,无法再添加数据到 mq 中
3)被拒绝的消息,并且 requeue = false
声明死信队列 demo
public static void rejectCustom() throws IOException, TimeoutException { Channel channel = ChannelUtil.getChannel(); /** * 声明死信队列 * queueDeclare(String queue, * boolean durable, * boolean exclusive, * boolean autoDelete, * Map<String, Object> arguments) * queue:队列名 * durable:是否持久化 * exclusive:该队列是否只供一个消费者进行消费 是否进行共享 true 可以多个消费者消费 * autoDelete:是否自动删除 最后一个消费者端开连接以后 该队列是否自动删除 true 自动删除 * arguments:其他参数 */ String dead_queue = "dead_queue"; channel.queueDeclare(dead_queue, false, false, false, null); /** * 私信队列绑定交换机 * */ String dead_exchange = "dead_exchange"; channel.exchangeDeclare(dead_exchange, BuiltinExchangeType.DIRECT); channel.queueBind(dead_queue, dead_exchange, "dead_routing"); //声明正常队列 String normal_queue = "normal_queue"; Map<String, Object> params = new HashMap<>(); params.put("x-dead-letter-exchange", dead_exchange); params.put("x-dead-letter-routing-key", "dead_routing"); channel.queueDeclare(normal_queue, false, false, false, params); //等待接收消息 System.out.println("等待接收消息----"); channel.basicConsume(normal_queue, false, (consumerTag, message) -> { System.out.println(new String(message.getBody(), "UTF-8")); channel.basicReject(message.getEnvelope().getDeliveryTag(), false); }, consumerTag -> { System.out.println("消费失败"); }); }
10,延时队列
TTL 是 RabbitMQ 中一个消息或者队列的属性,表明一条消息或者该队列中的所有消息的最大存活时间
设置超时时间
1)队列的 TTl,队列中的消息一旦过了 TTL 时间未被消费,就会丢弃(有死信队列就放到死信队列中)
2)消息的 TTL,即使消息过期,也不一定被马上丢弃
延时队列
//消息队列设置延时,投送消息到普通队列,ttl 时间内未被消费,投送到死信队列 public static void delay_queue() throws IOException, TimeoutException { Channel channel = ChannelUtil.getChannel(); //死信队列 String dead_queue = "delay_dead_queue"; channel.queueDeclare(dead_queue, false, false, false, null); /** * 死信队列绑定交换机 * */ String dead_exchange = "delay_dead_exchange"; String delay_routing_key = "delay_routing"; channel.exchangeDeclare(dead_exchange, BuiltinExchangeType.DIRECT); channel.queueBind(dead_queue, dead_exchange, delay_routing_key); //声明带有 ttl 的队列 String queue = "delay_queue"; Map<String, Object> params = new HashMap<>(); //设置队列的 ttl 时间 params.put("x-message-ttl", 5000); params.put("x-dead-letter-exchange", dead_exchange); params.put("x-dead-letter-routing-key", delay_routing_key); channel.queueDeclare(queue, false, false, false, params); channel.basicPublish("", queue, null, "延时队列数据:1".getBytes()); channel.basicPublish("", queue, null, "延时队列数据:2".getBytes()); channel.basicPublish("", queue, null, "延时队列数据:3".getBytes()); } /** * 消息延时 * 消息即使过期,也不一定会被马上丢弃,因为消息是否过期是在即将投递到消费者之前判定的,如果当前队列有严重的消息积压情况,则已过期的消息也许还能存活较长时间 * 可以用 java DelayQueue */ public static void delay_message() throws IOException, TimeoutException { Channel channel = ChannelUtil.getChannel(); //死信队列 String dead_queue = "delay_dead_queue"; channel.queueDeclare(dead_queue, false, false, false, null); /** * 死信队列绑定交换机 * */ String dead_exchange = "delay_dead_exchange"; String delay_routing_key = "delay_routing"; channel.exchangeDeclare(dead_exchange, BuiltinExchangeType.DIRECT); channel.queueBind(dead_queue, dead_exchange, delay_routing_key); //声明带有 ttl 的队列 String queue = "delay_queue"; Map<String, Object> params = new HashMap<>(); //设置队列的 ttl 时间 params.put("x-dead-letter-exchange", dead_exchange); params.put("x-dead-letter-routing-key", delay_routing_key); channel.queueDeclare(queue, false, false, false, params); channel.basicPublish("", queue, new AMQP.BasicProperties().builder().expiration("10000").build(), "延时队列数据:1".getBytes()); channel.basicPublish("", queue, new AMQP.BasicProperties().builder().expiration("3000").build(), "延时队列数据:2".getBytes()); channel.basicPublish("", queue, new AMQP.BasicProperties().builder().expiration("300").build(), "延时队列数据:3".getBytes()); }
11,如何保证消息不丢失
Rabbitmq 丢失消息的三种情况:
- 生产者弄丢了数据。生产者将数据发送到 RabbitMQ 的时候,可能数据就在半路给搞丢了,因为网络问题啥的,都有可能
- RabbitMQ 弄丢了数据。MQ还没有持久化自己挂了
- 消费端弄丢了数据。刚消费到,还没处理,结果进程挂了,比如重启了
解决方案
1,针对生产者
方案1 :开启RabbitMQ事务
同步的事务机制,不推荐
方案2: 使用confirm机制
事务机制和 confirm 机制最大的不同在于,事务机制是同步的,你提交一个事务之后会阻塞在那儿,但是 confirm 机制是异步的
在生产者开启了confirm模式之后,每次写的消息都会分配一个唯一的id,然后如果写入了rabbitmq之中,rabbitmq会给你回传一个ack消息,告诉你这个消息发送OK了;如果rabbitmq没能处理这个消息,会回调你一个nack接口,告诉你这个消息失败了,你可以进行重试。而且你可以结合这个机制知道自己在内存里维护每个消息的id,如果超过一定时间还没接收到这个消息的回调,那么你可以进行重发
定义消费者
@RequestMapping("/sendMessage/confirm") public void sendMessage3(@RequestParam String msg){ String time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()); //添加发布确认 rabbitTemplate.setConfirmCallback(myCallBack); /** * true:交换机无法将消息进行路由时,会将该消息返回给生产者 * false:如果发现消息无法进行路由,则直接丢弃 */ rabbitTemplate.setMandatory(true); //设置回退消息交给谁处理 rabbitTemplate.setReturnCallback(myCallBack); //第一条消息 System.out.println(String.format("当前时间:%s,发布一条消息:%s", time, msg)); String routingKey1 = "confirm_routing_key"; CorrelationData c1 = new CorrelationData("1"); rabbitTemplate.convertAndSend("confirm_exchange", routingKey1, msg + routingKey1, c1); //第二条消息 System.out.println(String.format("当前时间:%s,发布一条消息:%s", time, msg)); String routingKey2 = "confirm_routing_key1"; CorrelationData c2 = new CorrelationData("2"); rabbitTemplate.convertAndSend("confirm_exchange", routingKey2, msg + routingKey2, c2); }
发布成功或者失败回调
@Component public class MyCallBack implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnCallback { /** * 交换机不管是否收到消息的一个回调方法 * CorrelationData:消息相关数据 * ack:交换机是否收到消息 */ @Override public void confirm(CorrelationData correlationData, boolean ack, String cause) { String id = correlationData != null ? correlationData.getId() : ""; if (ack) { System.out.println( String.format("交换机已经收到 id 为: %s 的消息", id) ); } else { System.out.println( String.format("交换机还未收到 id 为: %s 消息,由于原因: %s", id, cause)); } } @Override public void returnedMessage(Message message, int i, String replyText, String exchange, String routingKey) { System.out.println( String.format("消息:%s 被服务器退回,退回原因: %s, 交换机是: %s, 路由 key:%s", new String(message.getBody()), replyText, exchange, routingKey)); } }
2,针对 RabbitMQ
方案一:消息持久化
1)Exchange 设置持久化(设置 durable )
@Bean public FanoutExchange backupExchange(){ FanoutExchange backupExchange = new FanoutExchange("backupExchange"); backupExchange.isDurable(); return backupExchange; }
2)Queue 设置持久化(设置 durable )
@Bean public Queue confirmQueue(){ return QueueBuilder.durable("confirm_queue").build(); }
3)Message持久化发送:发送消息设置发送模式deliveryMode=2,代表持久化消息
方案二:消息补偿机制
生产端将消息存库,根据消息状态提供补偿措施。
3,针对消费者
使用rabbitmq提供的ack机制,服务端首先关闭rabbitmq的自动ack,然后每次在确保处理完这个消息之后,在代码里手动调用ack。这样就可以避免消息还没有处理完就ack。才把消息从内存删除。
12,如何避免消息重复消费
为甚么会重复消费?
1 生产者由于网络等原因未收到确认,重复发送了这条消息
2 消费者的确认由于网络等原因未被收到,再次消费消息
保证消息的幂等性?
让每个消息携带一个全局的唯一ID,即可保证消息的幂等性,具体消费过程为:
消费者获取到消息后先根据id去查询redis/db是否存在该消息
如果不存在,则正常消费,消费完毕后写入redis/db
如果存在,则证明消息被消费过,直接丢弃
13,消息积压怎么处理?
消息积压的原因?
1,消息堆积即消息没及时被消费,是生产者生产消息速度快于消费者消费的速度导致的。
2,消费者消费慢可能是因为:本身逻辑耗费时间较长、阻塞了。
1. 针对生产端
- 给消息设置过期时间,超时就丢弃
- 考虑使用队列最大长度限制
- 限制生产:
//param1:prefetchSize,消息本身的大小 如果设置为0 那么表示对消息本身的大小不限制 //param2:prefetchCount,告诉rabbitmq不要一次性给消费者推送大于N个消息 //param3:global,是否将上面的设置应用于整个通道 void basicQos(int prefetchSize, int prefetchCount, boolean global) throws IOException;
2. 针对消费端
- 消费端扩容,多增加几个消费者
- 消费端性能优化,默认情况下,rabbitmq消费者为单线程串行消费。
org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer类的concurrentConsumers与txSize(对应prefetchCount)都是1),设置并发消费两个关键属性concurrentConsumers和prefetchCount。concurrentConsumers:设置的是对每个listener在初始化的时候设置的并发消费者的个数;prefetchCount:每次从broker里面取的待消费的消息的个数。
//配置方法:修改application.properties: spring.rabbitmq.listener.concurrency=m spring.rabbitmq.listener.prefetch=n
- 临时处理,先使用新的消费者把数据缓存下来,之后慢慢消费。
14,如何保证消费顺序
RabbitMQ 本身队列是有序的,但是消费的效率不同可能导致最终处理后的结构顺序不一致。
可能导致顺序不一致的情况:
1,一个队列对应多个消费者,每个消费者的效率不一致导致消费顺序不一致。
2,一个消费者内部使用多线程操作,也会导致消费顺序不一致。
解决方案:
1、拆分多个 queue,每个 queue 一个 consumer。
2、一个 queue,但是对应一个 consumer,然后这个 consumer 内部用内存队列(其实就是List而已)做排队,然后分发给底层不同的thread来处理(此方案可以支持高并发)。
https://blog.csdn.net/weixin_42039228/article/details/123526391
本文作者:Hi.PrimaryC
本文链接:https://www.cnblogs.com/cnff/p/14781671.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步