本次学习环境搭建在windows上。
RabbitMQ是采用Erlang语言开发的,所以系统环境必须提供Erlang环境,第一步就是安装Erlang。
安装中的坑:
- 安装目录不能存在中文,电脑账户名不能存在中文,启动会报错。
- Erlang语言的版本和Rabbit的版本是对应的,官网有对应图,版本不对应启动会失败,坑啊!在这搞了一上午!
RabbitMQ和spring是一个公司的所以在springboot中有很好的支持,原生的不多说。
- 配置环境,添加maven依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency> <dependency> <groupId>org.springframework.amqp</groupId> <artifactId>spring-rabbit-test</artifactId> <scope>test</scope> </dependency>
-
配置yml
server: port: 8801 spring: application: name: rabbitmq-provider # 配置RabbitMQ连接 rabbitmq: host: port: username: password: virtual-host: login publisher-confirms: true #消息发送到交换机确认机制,是否确认回调 publisher-returns: true #消息发送到交换机确认机制,是否返回回调 listener: simple: acknowledge-mode: manual #采用手动ack机制 retry: enabled: true initial-interval: 5000 max-attempts: 3
使用Fanout模式
- 依赖注入,使用此方法操作。
@Autowired private RabbitTemplate rabbitTemplate; // 使用RabbitTemplate,里面提供了接受的方法
-
创建生产者
/** * 不处理路由键,只需要简单的将队列绑定到交换机上发送到交换机的消息都会被转发到与该交换机绑定的所有队列上。 * Fanout交换机转发消息是最快的。 * */ @GetMapping("/sendFanoutMessage") public String sendFanoutMessage(){ // 消息体 String messageID = String.valueOf(UUID.randomUUID()); String messageDate = "message : 这是Fanout"; String creatTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); HashMap<String, Object> map = new HashMap<>(); map.put("messageID",messageID); map.put("messageDate",messageDate); map.put("creatTime",creatTime); // 发送到MQ rabbitTemplate.convertAndSend("fanoutExchangeOne",null,map); return "ok"; }
-
配置FanoutConfig,注意Queue的包不要倒错,所有的模式步骤都是创建队列,创建交换机,将交换机和队列绑定。
-
生产者发送消息,消息一定是发送到交换机的,消费者消费消息一定是从队列中取出的。消息的传递靠的就是交换机,如果不指定交换机,会使用mq默认的交换机。
import org.springframework.amqp.core.Binding; import org.springframework.amqp.core.BindingBuilder; import org.springframework.amqp.core.FanoutExchange; import org.springframework.amqp.core.Queue; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class FanoutRabbitConfig { /** * 1.创建三个对列 : fanoutA fanoutB fanoutC * 2.创建交换机 * 3.将队列绑定到交换机上 */ // 创建队列 @Bean public Queue fanoutA(){ return new Queue("fanout.A"); } @Bean public Queue fanoutB(){ return new Queue("fanout.B"); } @Bean public Queue fanoutC(){ return new Queue("fanout.C"); } // 创建交换机 @Bean FanoutExchange fanoutExchange(){ return new FanoutExchange("fanoutExchangeOne");//参数:1.交换机名称 2.持久化 3.不自动删除 } // 绑定关系 @Bean Binding bindingExchangeA(){ return BindingBuilder.bind(fanoutA()).to(fanoutExchange()); } @Bean Binding bindingExchangeB(){ return BindingBuilder.bind(fanoutB()).to(fanoutExchange()); } @Bean Binding bindingExchangeC(){ return BindingBuilder.bind(fanoutC()).to(fanoutExchange()); } }
-
创建3个消费者
-
@RabbitListener(queues = "***") ,表示监听的哪个队列。
-
@RabbitHandler ,表示消息到达的目的地。
-
运行发现生产者生产一条消息,3个消费者全都收到消息。
@Component @RabbitListener(queues = "fanout.A") public class FanoutReceiverA { @RabbitHandler public void process(Map testMessage){ System.out.println("FanoutReceiverA消费者收到消息 :"+testMessage.toString()); } } -- @Component @RabbitListener(queues = "fanout.B") public class FanoutReceiverB { @RabbitHandler public void process(Map testMessage){ System.out.println("FanoutReceiverB消费者收到消息 :"+testMessage.toString()); } } -- @Component @RabbitListener(queues = "fanout.C") public class FanoutReceiverC { @RabbitHandler public void process(Map testMessage){ System.out.println("FanoutReceiverC消费者收到消息 :"+testMessage.toString()); } }
使用direct模式
- 此模式跟fanout最大的区别就是,它可以有路由key,消息会根据携带的路由key所匹配。
- 此处指定两个消费者监听同一队列,发现消息只会有一个消费者消费。
- 创建生产者
/** *只有一个消费者接收到 轮询分发 * */ @GetMapping("/sendDirectMessage") public String sendDirectMessage(){ // 消息体 String messageID = String.valueOf(UUID.randomUUID()); String messageDate = "message : 这是Direct"; String creatTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); HashMap<String, Object> map = new HashMap<>(); map.put("messageID",messageID); map.put("messageDate",messageDate); map.put("creatTime",creatTime); // 发送到MQ 携带routingKey rabbitTemplate.convertAndSend("TestDirectExchange","TestDirectRoutingKey",map);// TestDirectRoutingKey 所携带的路由Key return "ok"; }
-
创建direct配置
@Configuration public class DirectRabbitConfig { // 创建队列 @Bean public Queue TestDirectQueue(){ // durable:是否持久化,默认是false,持久化队列:会被存储在磁盘上,当消息代理重启时仍然存在,暂存队列:当前连接有效 // exclusive:默认也是false,只能被当前创建的连接使用,而且当连接关闭后队列即被删除。此参考优先级高于durable // autoDelete:是否自动删除,当没有生产者或者消费者使用此队列,该队列会自动删除。 // 一般设置一下队列的持久化就好,其余两个就是默认false return new Queue("TestDirectQueue",true); } // 创建交换机 @Bean DirectExchange TestDirectExchange(){ return new DirectExchange("TestDirectExchange",true,false); } // 绑定交换机 并设置用于匹配的routingkey @Bean Binding bindingDirect(){ return BindingBuilder.bind(TestDirectQueue()).to(TestDirectExchange()).with("TestDirectRoutingKey"); } }
-
创建消费者
@Component
@RabbitListener(queues = "TestDirectQueue")
public class DirectReceiver {
@RabbitHandler
public void process(Map testMessage){
System.out.println("第一个DirectRexeiver消费者接收到消息"+testMessage.toString());
}
}
--
@Component
@RabbitListener(queues = "TestDirectQueue")
public class DirectReceiverTwo {
@RabbitHandler
public void process(Map testMessage){
System.out.println("第二个DirectRexeiver消费者接收到消息"+testMessage.toString());
}
}
使用Toplic模式
- 此模式路由key可以使用 * 和 # Toplic配置里有解释。
- 创建生产者
@GetMapping("/sendTopicMessage") public String sendTopicMessage(){ // 消息体 String messageID = String.valueOf(UUID.randomUUID()); String messageDate = "message : 这是Topic"; String creatTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); HashMap<String, Object> map = new HashMap<>(); map.put("messageID",messageID); map.put("messageDate",messageDate); map.put("creatTime",creatTime); // 发送到MQ rabbitTemplate.convertAndSend("topicExchange","topic.man",map); rabbitTemplate.convertAndSend("topicExchange","topic.woman",map); return "ok"; }
-
创建配置
@Configuration public class TopicRabbitConfig { // 绑定键 public final static String man = "topic.man"; public final static String woman = "topic.woman"; // 创建队列 @Bean public Queue firstQueue(){ return new Queue(TopicRabbitConfig.man); } @Bean public Queue secondQueue(){ return new Queue(TopicRabbitConfig.woman); } // 创建交换机 @Bean TopicExchange exchange(){ return new TopicExchange("topicExchange"); } // 将队列于交换机绑定关系 //将firstQueue和topicExchange绑定,而且绑定的键值为topic.man //这样只要是消息携带的路由键是topic.man,才会分发到该队列 @Bean Binding bindingExchangeTopic(){ return BindingBuilder.bind(firstQueue()).to(exchange()).with(man); } // # 代表一级或者多级目录 // * 代表一级至少有一级 @Bean Binding bindingExchangeTopic2(){ return BindingBuilder.bind(secondQueue()).to(exchange()).with("topic.#"); } }
-
创建2个消费者
@Component @RabbitListener(queues = "topic.man") public class TopicManReceiver { @RabbitHandler public void process(Map testMessage){ System.out.println("TopicManReceiver消费者收到消息 :"+testMessage.toString()); } } -- @Component @RabbitListener(queues = "topic.woman") public class TopicWManReceiver { @RabbitHandler public void process(Map testMessage){ System.out.println("TopicWManReceiver消费者收到消息 :"+testMessage.toString()); } }
运行发现生产者发送两条消息,消费者TopicManReceiver接收到1条消息,TopicWManReceiver接受到两条,原因是应为消息会通过路由key进行匹配。
设置队列过期时间
- 已direct模式为例子
// 创建队列 @Bean public Queue TestDirectQueue(){ // durable:是否持久化,默认是false,持久化队列:会被存储在磁盘上,当消息代理重启时仍然存在,暂存队列:当前连接有效 // exclusive:默认也是false,只能被当前创建的连接使用,而且当连接关闭后队列即被删除。此参考优先级高于durable // autoDelete:是否自动删除,当没有生产者或者消费者使用此队列,该队列会自动删除。 // 一般设置一下队列的持久化就好,其余两个就是默认false // 1.可以设置队列的过期时间 如下 HashMap<String, Object> args = new HashMap<>(); args.put("x-message-ttl",5000);//此处必须是int整数,5秒过期 return new Queue("TestDirectQueue2",true,false,false,args); }
设置消息过期时间
/**
*发送消息,并给消息设置过期时间
*
*
*/
@GetMapping("/sendDirectMessageOne")
public String sendDirectMessage1(){
// 消息体
String messageID = String.valueOf(UUID.randomUUID());
String messageDate = "message : 这是Direct";
String creatTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
HashMap<String, Object> map = new HashMap<>();
map.put("messageID",messageID);
map.put("messageDate",messageDate);
map.put("creatTime",creatTime);
//设置消息过期时间
MessagePostProcessor args = new MessagePostProcessor() {
@Override
public Message postProcessMessage(Message message) throws AmqpException {
message.getMessageProperties().setExpiration("5000");// 此处跟设置队列不一样,是String类型
return message;
}
};
// 发送到MQ 携带routingKey
rabbitTemplate.convertAndSend("TestDirectExchange","TestDirectRoutingKey1",map,args);
return "ok";
}
设置死信队列(接盘侠)
- 创建接盘侠配置
@Configuration public class DeadRabbitConfig {// 接盘侠配置 @Bean public Queue deadRabbitA(){ return new Queue("dead.A",true); } @Bean DirectExchange directExchange(){ return new DirectExchange("directExchange",true,false); } @Bean Binding bindingDead(){ return BindingBuilder.bind(deadRabbitA()).to(directExchange()).with("dead"); } }
-
接盘侠消费者就忽略,根据自己业务需求处理
-
设置接盘侠
// 创建队列 @Bean public Queue TestDirectQueue(){ // durable:是否持久化,默认是false,持久化队列:会被存储在磁盘上,当消息代理重启时仍然存在,暂存队列:当前连接有效 // exclusive:默认也是false,只能被当前创建的连接使用,而且当连接关闭后队列即被删除。此参考优先级高于durable // autoDelete:是否自动删除,当没有生产者或者消费者使用此队列,该队列会自动删除。 // 一般设置一下队列的持久化就好,其余两个就是默认false // 2.设置接盘侠,死信队列 HashMap<String, Object> args = new HashMap<>(); args.put("x-dead-letter-exchange","directExchange");// 绑定接盘侠交换机 args.put("x-dead-letter-routing-key","dead");// 绑定接盘侠路由Key(Fanout模式无需配置),注意已经创建的队列不在允许修改配置。变更队列参数并不会覆盖,而是会报错。 return new Queue("TestDirectQueue",true,false,false,args); }
RabbitMQ 内存磁盘监控
- 当内存使用超过配置的阈值或者磁盘空间剩余空间对于配置的阈值时,RabbitMQ会暂时阻塞客户端的连接,并且停止接收从客户端发来的消息,以此避免服务器的崩溃,客户端与服务端的心态检测机制也会失效。
-
如何设置mq内存,两种方式
-
命令设置
rabbitmqctl set_vm_memory_high_watermark <fraction>
rabbitmqctl set_vm_memory_high_watermark absolute 50MB // 绝对值方式
fraction/value 为内存阈值。默认情况是:0.4/2GB,代表的含义是:当RabbitMQ的内存超过40%时,就会产生警告并且阻塞所有生产者的连接。通过此命令修改阈值在Broker重启以后将会失效,通过修改配置文件方式设置的阈值则不会随着重启而消失,但修改了配置文件一样要重启broker才会生效。
-
配置文件方式修改 rabbitmq.conf
vm_memory_high_watermark.relative = 0.6 // 默认为0.4 建议取值在0.4 ~ 0.7之间,不建议超过0.7
vm_memory_high_watermark.absolute = 2GB // 使用绝对值方式 可以是KB,MB,GB
RabbitMQ的内存换页
- 在某个Broker节点及内存阻塞生产者之前,它会尝试将队列中的消息换页到磁盘以释放内存空间,持久化和非持久化的消息都会写入磁盘中,其中持久化的消息本身就在磁盘中有一个副本,所以在转移的过程中持久化的消息会先从内存中清除掉。
- 默认情况下,内存到达的阈值是50%时就会换页处理。
也就是说,在默认情况下该内存的阈值是0.4的情况下,当内存超过0.4*0.5=0.2时,会进行换页动作。 - 比如有1000MB内存,当内存的使用率达到了400MB,已经达到了极限,但是因为配置的换页内存0.5,这个时候会在达到极限400mb之前,会把内存中的200MB进行转移到磁盘中。从而达到稳健的运行。
- 设置 vm_memory_high_watermark_paging_ratio 来进行调整
vm_memory_high_watermark.relative = 0.4
vm_memory_high_watermark_paging_ratio = 0.7(设置小于1的值)为什么设置小于1,以为你如果你设置为1的阈值。内存都已经达到了极限了。你在去换页意义不是很大了。
RabbitMQ的磁盘预警
- 当磁盘的剩余空间低于确定的阈值时,RabbitMQ同样会阻塞生产者,这样可以避免因非持久化的消息持续换页而耗尽磁盘空间导致服务器崩溃。
- 默认情况下:磁盘预警为50MB的时候会进行预警。表示当前磁盘空间第50MB的时候会阻塞生产者并且停止内存消息换页到磁盘的过程。
这个阈值可以减小,但是不能完全的消除因磁盘耗尽而导致崩溃的可能性。比如在两次磁盘空间的检查空隙内,第一次检查是:60MB ,第二检查可能就是1MB,就会出现警告。
默认情况下:磁盘预警为50MB的时候会进行预警。表示当前磁盘空间第50MB的时候会阻塞生产者并且停止内存消息换页到磁盘的过程。
这个阈值可以减小,但是不能完全的消除因磁盘耗尽而导致崩溃的可能性。比如在两次磁盘空间的检查空隙内,第一次检查是:60MB ,第二检查可能就是1MB,就会出现警告。
- 通过以下命令修改
rabbitmqctl set_disk_free_limit <disk_limit>
rabbitmqctl set_disk_free_limit memory_limit <fraction>
disk_limit:固定单位 KB MB GB
fraction :是相对阈值,建议范围在:1.0~2.0之间。(相对于内存)
- 通过配置文件修改
disk_free_limit.relative = 3.0
disk_free_limit.absolute = 50mb@Queue注解为我们提供了队列相关的一些属性,具体如下: name: 队列的名称; durable: 是否持久化; exclusive: 是否独享、排外的; autoDelete: 是否自动删除; arguments:队列的其他属性参数,有如下可选项,可参看图2的arguments: x-message-ttl:消息的过期时间,单位:毫秒; x-expires:队列过期时间,队列在多长时间未被访问将被删除,单位:毫秒; x-max-length:队列最大长度,超过该最大值,则将从队列头部开始删除消息; x-max-length-bytes:队列消息内容占用最大空间,受限于内存大小,超过该阈值则从队列头部开始删除消息; x-overflow:设置队列溢出行为。这决定了当达到队列的最大长度时消息会发生什么。有效值是drop-head、reject-publish或reject-publish-dlx。仲裁队列类型仅支持drop-head; x-dead-letter-exchange:死信交换器名称,过期或被删除(因队列长度超长或因空间超出阈值)的消息可指定发送到该交换器中; x-dead-letter-routing-key:死信消息路由键,在消息发送到死信交换器时会使用该路由键,如果不设置,则使用消息的原来的路由键值 x-single-active-consumer:表示队列是否是单一活动消费者,true时,注册的消费组内只有一个消费者消费消息,其他被忽略,false时消息循环分发给所有消费者(默认false) x-max-priority:队列要支持的最大优先级数;如果未设置,队列将不支持消息优先级; x-queue-mode(Lazy mode):将队列设置为延迟模式,在磁盘上保留尽可能多的消息,以减少RAM的使用;如果未设置,队列将保留内存缓存以尽可能快地传递消息; x-queue-master-locator:在集群模式下设置镜像队列的主节点信息。 @RabbitListener 提供消费者配置 ackMode:覆盖容器工厂 AcknowledgeMode属性。 admin:参考AmqpAdmin. autoStartup:设置为 true 或 false,以覆盖容器工厂中的默认设置。 QueueBinding[] bindings:QueueBinding提供监听器队列名称以及交换和可选绑定信息的数组。 concurrency:消费并发数。 containerFactory:RabbitListenerContainerFactory的bean名称 ,没有则使用默认工厂。 converterWinsContentType:设置为“false”以使用“replyContentType”属性的值覆盖由消息转换器设置的任何内容类型标头。 errorHandler:消息异常时调用的方法名。 exclusive:当为true时,容器中的单个消费者将独占使用 queues(),从而阻止其他消费者从队列接收消息。 executor:线程池bean的名称 group:如果提供,则此侦听器的侦听器容器将添加到以该值作为其名称的类型为 的 bean 中Collection<MessageListenerContainer>。 id:为此端点管理的容器的唯一标识符。 messageConverter:消息转换器。 priority:此端点的优先级。 String[] queues:监听的队列名称 Queue[] queuesToDeclare:监听的队列Queue注解对象,与bindings()、queues()互斥。 replyContentType:用于设置回复消息的内容类型。 replyPostProcessor:在ReplyPostProcessor发送之前处理响应的 bean 名称 。 returnExceptions:设置为“true”以导致使用正常replyTo/@SendTo语义将侦听器抛出的异常发送给发送者。