rabbitMQ学习记录
分布式系统通信的两种方式: 1 远程调用,2 借助消息队列
消息队列的优势: 1 应用解耦: 情况1:如果A系统直接调B,C系统,如果B/C系统报错,对A系统也有影响;情况2:如果要加个D系统,对于直接调用的方式需要修改A系统的代码,对于用消息队列的方式不用改A系统代码,D系统直接从消息队列取消息就行.
2:削峰填谷:MQ能处理的最大并发数远高于应用程序,可以将请求保存到MQ中,让消费者慢慢消费.
消息队列的劣势: 1 引入外部依赖,系统可靠性降低 2 复杂度上升,如何保证消费的顺序性,消息不丢失,不重复消费,消息一致性
总结:什么情况下适合用MQ: 1 A系统调B系统,如果需要B系统的反馈,则不能用MQ. 2
MQ工作模式
* 队列模式:

多个消费者之间是竞争关系,一条消息只能被一个消费者消费
队列模式代码就是在简单模式基础上再启动一个消费者
* 订阅模式:

生产者代码:
package com.seeyii.web.yk.pubSub; import com.rabbitmq.client.BuiltinExchangeType; import com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import com.rabbitmq.client.ConnectionFactory; import java.io.IOException; import java.util.concurrent.TimeoutException; /** * @PackageName:com.seeyii.web.yk * @Description: * @Author 杨坤 * @Date:2022/6/28 19:38 */ public class Producer { public static void main(String[] args) throws IOException, TimeoutException { //1 创建连接工厂 ConnectionFactory factory = new ConnectionFactory(); //2 设置参数 factory.setHost("192.168.xx.xx");// ip,默认localhost factory.setPort(5672);//端口,默认5672 factory.setVirtualHost("/yk");//虚拟机,默认值 / factory.setUsername("username"); //默认值 guest factory.setPassword("password"); //默认值 guest //3 创建连接 Connection connection = factory.newConnection(); //4 创建channel Channel channel = connection.createChannel(); // 创建交换机 //String exchange, BuiltinExchangeType type, boolean durable, boolean autoDelete, Map<String, Object> arguments /* exchange: 交换机名 type:交换机类型: direct(定向),fanout(广播,向所有绑定的队列发消息),topic(通配符方式),headers(参数匹配) durable: 持久化 autoDelete: 自动删除 arguments:参数 */ String exchange_name = "fanout_exchange"; channel.exchangeDeclare(exchange_name, BuiltinExchangeType.FANOUT,true,false,null); //channel和交换机绑定 // 5 创建两个队列 String queue_name1="fanout_queue1"; String queue_name2="fanout_queue2"; /* queue: 对列名, durable:是否持久化,当mq重启后是否还在 exclusive:是否排他, * 如果排他,只能有一个消费者监听这个队列,exclusive为true时,concurrency必须是1,concurrency是消费者创建几个线程去消费。
* 当连接关闭时是否删除队列 autoDelete:当没有连接时,是否自动删除 arguments: 参数 **/ //如果没有这个名字的队列则会创建,如果有就不创建
channel.queueDeclare(queue_name1,true,false,false,null);
channel.queueDeclare(queue_name2,true,false,false,null);
//绑定交换机和队列
/*queue:对列名 exchange:交换机名 routingKey:路由键名 */
channel.queueBind(queue_name1,exchange_name,"");//如果交换机类型是广播,路由键就是空串
channel.queueBind(queue_name2,exchange_name,"");
//6 发消息 /* exchange:交换机名称,简单模式下默认空串 routingKey:路由键,简单模式下就是channel名 BasicProperties props: 配置信息, byte[] body:发送的消息数据 **/
String body="hello,rabbitmq+++++"; channel.basicPublish("","hello_world",null,body.getBytes());
//7 释放资源 if(channel!=null){ channel.close(); } if(connection!=null){ connection.close(); } } }
消费者1代码:
package com.seeyii.web.yk.pubSub; import com.rabbitmq.client.*; import java.io.IOException; import java.util.concurrent.TimeoutException; /** * @PackageName:com.seeyii.web.yk * @Description: * @Author 杨坤 * @Date:2022/6/28 19:38 */ public class Consumer1 { public static void main(String[] args) throws IOException, TimeoutException { //1 创建连接工厂 ConnectionFactory factory = new ConnectionFactory(); //2 设置参数 factory.setHost("192.168..");// ip,默认localhost factory.setPort(5672);//端口,默认5672 factory.setVirtualHost("/yk");//虚拟机,默认值 / factory.setUsername(""); //默认值 guest factory.setPassword(""); //默认值 guest //3 创建连接 Connection connection = factory.newConnection(); //4 创建channel Channel channel = connection.createChannel(); // 5 创建队列,生产者已经创建过队列,消费者不用再创建 //6 接收消息 /* String queue:对列名称, boolean autoAck:是否自动确认, Consumer callback:回调对象 **/ Consumer consumer=new DefaultConsumer(channel){ //回调方法: 当收到消息后自动执行该方法 /*consumerTag:消息标识 envelope获取交换机,路由键的信息 properties:配置信息 body: 消息*/ @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { super.handleDelivery(consumerTag, envelope, properties, body); // System.out.println("consumerTag:"+consumerTag); // System.out.println("envelope-Exchange:"+envelope.getExchange()); // System.out.println("envelope-RoutingKey:"+envelope.getRoutingKey()); // System.out.println("properties:"+properties); System.out.println("消费者1收到消息: body:"+new String(body)); } }; channel.basicConsume("fanout_queue1",true,consumer); //消费者不要关闭连接资源 } }
消费者2代码:
package com.seeyii.web.yk.pubSub; import com.rabbitmq.client.*; import java.io.IOException; import java.util.concurrent.TimeoutException; /** * @PackageName:com.seeyii.web.yk * @Description: * @Author 杨坤 * @Date:2022/6/28 19:38 */ public class Consumer2 { public static void main(String[] args) throws IOException, TimeoutException { //1 创建连接工厂 ConnectionFactory factory = new ConnectionFactory(); //2 设置参数 factory.setHost("192.168..");// ip,默认localhost factory.setPort(5672);//端口,默认5672 factory.setVirtualHost("/yk");//虚拟机,默认值 / factory.setUsername(""); //默认值 guest factory.setPassword(""); //默认值 guest //3 创建连接 Connection connection = factory.newConnection(); //4 创建channel Channel channel = connection.createChannel(); // 5 创建队列,生产者已经创建过队列,消费者不用再创建//6 接收消息 /* String queue:对列名称, boolean autoAck:是否自动确认, Consumer callback:回调对象 **/ Consumer consumer=new DefaultConsumer(channel){ //回调方法: 当收到消息后自动执行该方法 /*consumerTag:消息标识 envelope获取交换机,路由键的信息 properties:配置信息 body: 消息*/ @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { super.handleDelivery(consumerTag, envelope, properties, body); // System.out.println("consumerTag:"+consumerTag); // System.out.println("envelope-Exchange:"+envelope.getExchange()); // System.out.println("envelope-RoutingKey:"+envelope.getRoutingKey()); // System.out.println("properties:"+properties); System.out.println("消费者2收到消息: body:"+new String(body)); } }; channel.basicConsume("fanout_queue2",true,consumer); //消费者不要关闭连接资源 } }
路由模式:

生产者代码:
package com.seeyii.web.yk.routingKey; import com.rabbitmq.client.BuiltinExchangeType; import com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import com.rabbitmq.client.ConnectionFactory; import java.io.IOException; import java.util.concurrent.TimeoutException; /** * @PackageName:com.seeyii.web.yk * @Description: * @Author 杨坤 * @Date:2022/6/28 19:38 */ public class Producer { public static void main(String[] args) throws IOException, TimeoutException { //1 创建连接工厂 ConnectionFactory factory = new ConnectionFactory(); //2 设置参数 factory.setHost("192.168..");// ip,默认localhost factory.setPort(5672);//端口,默认5672 factory.setVirtualHost("/yk");//虚拟机,默认值 / factory.setUsername(""); //默认值 guest factory.setPassword(""); //默认值 guest //3 创建连接 Connection connection = factory.newConnection(); //4 创建channel Channel channel = connection.createChannel(); // 创建交换机 //String exchange, BuiltinExchangeType type, boolean durable, boolean autoDelete, Map<String, Object> arguments /* exchange: 交换机名 type:交换机类型: direct(定向),fanout(广播,向所有绑定的队列发消息),topic(通配符方式),headers(参数匹配) durable: 持久化 autoDelete: 自动删除 arguments:参数 */ String exchange_name = "direct_exchange"; channel.exchangeDeclare(exchange_name, BuiltinExchangeType.DIRECT,true,false,null); //channel和交换机绑定 // 5 创建两个队列 String queue_name1="direct_queue1"; String queue_name2="direct_queue2"; /* queue: 对列名, durable:是否持久化,当mq重启后是否还在 exclusive:是否排他, * 如果排他,只能有一个消费者监听这个队列 * 当连接关闭时是否删除队列 autoDelete:当没有连接时,是否自动删除 arguments: 参数 **/ //如果没有这个名字的队列则会创建,如果有就不创建 channel.queueDeclare(queue_name1,true,false,false,null); channel.queueDeclare(queue_name2,true,false,false,null); //绑定交换机和队列 //String queue, String exchange, String routingKey /*queue:对列名 exchange:交换机名 routingKey:路由键名 */ //队列1绑定1个路由键,队列2绑定三个路由键 channel.queueBind(queue_name1,exchange_name,"error"); channel.queueBind(queue_name2,exchange_name,"info"); channel.queueBind(queue_name2,exchange_name,"warning"); channel.queueBind(queue_name2,exchange_name,"error"); //6 发消息 /* exchange:交换机名称,简单模式下默认空串 routingKey:路由键,简单模式下就是channel名 BasicProperties props: 配置信息, byte[] body:发送的消息数据 **/ String body="向direct类型交换机发数据,routingKey是info,交换机绑定两个队列"; channel.basicPublish(exchange_name,"info",null,body.getBytes()); //7 释放资源 if(channel!=null){ channel.close(); } if(connection!=null){ connection.close(); } } }
消费者代码:
package com.seeyii.web.yk.routingKey; import com.rabbitmq.client.*; import java.io.IOException; import java.util.concurrent.TimeoutException; /** * @PackageName:com.seeyii.web.yk * @Description: * @Author 杨坤 * @Date:2022/6/28 19:38 */ public class Consumer1 { public static void main(String[] args) throws IOException, TimeoutException { //1 创建连接工厂 ConnectionFactory factory = new ConnectionFactory(); //2 设置参数 factory.setHost("192.168..");// ip,默认localhost factory.setPort(5672);//端口,默认5672 factory.setVirtualHost("/yk");//虚拟机,默认值 / factory.setUsername(""); //默认值 guest factory.setPassword(""); //默认值 guest //3 创建连接 Connection connection = factory.newConnection(); //4 创建channel Channel channel = connection.createChannel(); // 5 创建队列,生产者已经创建过队列,消费者不用再创建 /* queue: 对列名, durable:是否持久化,当mq重启后是否还在 exclusive:是否排他, * 如果排他,只能有一个消费者监听这个队列 * 当连接关闭时是否删除队列 autoDelete:当没有连接时,是否自动删除 arguments: 参数 **/ //如果没有这个名字的队列则会创建,如果有就不创建 // channel.queueDeclare("hello_world",true,false,false,null); //6 接收消息 /* String queue:对列名称, boolean autoAck:是否自动确认, Consumer callback:回调对象 **/ Consumer consumer=new DefaultConsumer(channel){ //回调方法: 当收到消息后自动执行该方法 /*consumerTag:消息标识 envelope获取交换机,路由键的信息 properties:配置信息 body: 消息*/ @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { super.handleDelivery(consumerTag, envelope, properties, body); System.out.println("消费者1收到消息: body:"+new String(body)); } }; channel.basicConsume("direct_queue1",true,consumer); //消费者不要关闭连接资源 } }
package com.seeyii.web.yk.routingKey; import com.rabbitmq.client.*; import java.io.IOException; import java.util.concurrent.TimeoutException; /** * @PackageName:com.seeyii.web.yk * @Description: * @Author 杨坤 * @Date:2022/6/28 19:38 */ public class Consumer2 { public static void main(String[] args) throws IOException, TimeoutException { //1 创建连接工厂 ConnectionFactory factory = new ConnectionFactory(); //2 设置参数 factory.setHost("192.168..");// ip,默认localhost factory.setPort(5672);//端口,默认5672 factory.setVirtualHost("/yk");//虚拟机,默认值 / factory.setUsername(""); //默认值 guest factory.setPassword(""); //默认值 guest //3 创建连接 Connection connection = factory.newConnection(); //4 创建channel Channel channel = connection.createChannel(); // 5 创建队列,生产者已经创建过队列,消费者不用再创建 /* queue: 对列名, durable:是否持久化,当mq重启后是否还在 exclusive:是否排他, * 如果排他,只能有一个消费者监听这个队列 * 当连接关闭时是否删除队列 autoDelete:当没有连接时,是否自动删除 arguments: 参数 **/ //如果没有这个名字的队列则会创建,如果有就不创建 // channel.queueDeclare("hello_world",true,false,false,null); //6 接收消息 /* String queue:对列名称, boolean autoAck:是否自动确认, Consumer callback:回调对象 **/ Consumer consumer=new DefaultConsumer(channel){ //回调方法: 当收到消息后自动执行该方法 /*consumerTag:消息标识 envelope获取交换机,路由键的信息 properties:配置信息 body: 消息*/ @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { super.handleDelivery(consumerTag, envelope, properties, body); System.out.println("消费者2收到消息: body:"+new String(body)); } }; channel.basicConsume("direct_queue2",true,consumer); //消费者不要关闭连接资源 } }
主题模式:
routingKey里使用通配符,*代表一个单词,#代表0或多个单词,代码和发布订阅模式基本相同
Springboot整合rabbitMQ代码:
配置文件:
spring: rabbitmq: host: 192.168.xx.xx port: 5672 username: xx password: xx
配置类:
package com.seeyii.yk.boot; import org.springframework.amqp.core.*; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class BootConfig { public static final String EXCHANGE_NAME="boot_topic_exchange"; public static final String QUEUE_NAME="boot_queue"; // 1 创建交换机 @Bean("bootExchange") public Exchange bootExchange(){ return ExchangeBuilder.topicExchange(EXCHANGE_NAME).durable(true).build(); } // 2 创建队列 @Bean("bootQueue") public Queue bootQueue(){ return QueueBuilder.durable(QUEUE_NAME).build(); } // 3 队列和交换机绑定 @Bean public Binding bindQueueExchange(@Qualifier("bootQueue") Queue queue, @Qualifier("bootExchange") Exchange exchange){ return BindingBuilder.bind(queue).to(exchange).with("boot.#").noargs(); } }
生产者:
package com.seeyii.yk.boot; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.RestTemplate; @RestController @RequestMapping("/bootProducer") public class ProducerBoot { @Autowired private RabbitTemplate rabbitTemplate; @PostMapping("/testSendMQ") public String testSendMQ() throws InterruptedException { rabbitTemplate.convertAndSend(BootConfig.EXCHANGE_NAME,"boot.haha","boot,MQ,hello"); return "success"; } }
消费者:
package com.seeyii.yk.boot; import org.springframework.amqp.core.Message; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.stereotype.Component; @Component public class ConsumerBoot { @RabbitListener(queues = BootConfig.QUEUE_NAME) public void ListenerQueue(Message message){ System.out.println("收到消息: "+new String(message.getBody())); } }
rabbitMQ如何保证消息可靠投递:
rabbitMQ整个消息投递路径为: producer->exchange->queue->consumer
1 生产者发送消息到交换机和队列,这一步用交换机和队列收到消息后返回确认消息来实现(ConfirmCallback和returnCallback:)
RabbitMQ 配置(application.yml)
spring: rabbitmq: host: localhost port: 5672 username: guest password: guest # 开启消息发送确认 publisher-confirm-type: correlated #交换机确认 # 开启消息投递失败返回(消息未路由) publisher-returns: true # 列队确认 listener: simple: acknowledge-mode: manual # 手动 ack,默认时自动ack确认,就是无论成功还是失败都返回ack retry: enabled: true # 开启消费者重试 max-attempts: 3 initial-interval: 1000
@Component public class ReliableSender implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnsCallback { private final RabbitTemplate rabbitTemplate; public ReliableSender(RabbitTemplate rabbitTemplate) { this.rabbitTemplate = rabbitTemplate; this.rabbitTemplate.setConfirmCallback(this); this.rabbitTemplate.setReturnsCallback(this); } public void sendMessage(String msg) { rabbitTemplate.convertAndSend( RabbitConfig.EXCHANGE_NAME, RabbitConfig.ROUTING_KEY, msg, message -> { // 设置消息持久化 message.getMessageProperties().setDeliveryMode(MessageDeliveryMode.PERSISTENT); return message; } ); } // 发送到交换机成功或失败都会调 @Override public void confirm(CorrelationData correlationData, boolean ack, String cause) { if (ack) { System.out.println("✅ 消息已成功到达交换机"); } else { System.err.println("❌ 消息未到达交换机,原因: " + cause); } } // 消息发送到队列失败时才会调 @Override public void returnedMessage(ReturnedMessage returned) { System.err.println("❌ 消息未路由到队列:" + new String(returned.getMessage().getBody()));//ReturnedMessage 里有错误码,交换机名,路由键等信息
} }
2 声明交换机和队列时设置保存消息
3 消费端消费消息成功,返回ack信息,队列会删除该消息; 消费端消费消息失败,返回nack,队列会把消息设为待消费状态,这样就能重新消费
自动ack: acknowledge=none ,消费者收到消息后不管后面的处理逻辑是否成功都会给brocker发确认消息
手动ack:acknowledge=manual,代码控制何时发确认消息,可以在业务逻辑完成后再发
如果设置了手动ack,则需要在业务处理成功后,调用channel.basicAck(),手动签收,如果出现异常,则调用channel.basicNack()方法,让其自动重新发送消息.
消费者代码:
package com.seeyii.yk.boot; import com.rabbitmq.client.Channel; import org.springframework.amqp.core.Message; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.amqp.support.AmqpHeaders; import org.springframework.messaging.handler.annotation.Header; import org.springframework.stereotype.Component; import java.io.IOException; @Component public class ConsumerBoot { @RabbitListener(queues = BootConfig.QUEUE_NAME,ackMode = "MANUAL",concurrency = "4") public void ListenerQueue(Message message, Channel channel) throws IOException { System.out.println("收到消息: "+new String(message.getBody())); System.out.println("data: " + data);
long deliveryTag = message.getMessageProperties().getDeliveryTag(); if (data.contains("boot")) { // RabbitMQ的ack机制中,第二个参数返回true,表示需要将这条消息投递给其他的消费者重新消费 channel.basicAck(deliveryTag, false);//deliveryTag就是消息的标识
} else {
//获取当前消息是否是重复投递的
Boolean redeliered = message.getMessageProperties().getRedelivered()
if(redeliered ){
channel.basicNack(deliveryTag, false, false);//从队列中删除消息
}else{
channel.basicNack(deliveryTag, false, true); // 第三个参数true,表示这个消息会重新进入队列
}
} } }
总结:如何保证消息可靠性
1 持久化,包括消息持久化,交换机持久化,队列持久化
2 生产方确认confirmCallback,returnCallback,消费方手动ack
3 broker集群高可用
消费端限流:
1 配置文件中配置 RabbitListenerContainerFactory
@Bean public RabbitListenerContainerFactory rabbitListenerContainerFactory2(ConnectionFactory connectionFactory){ SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); factory.setConnectionFactory(connectionFactory); factory.setPrefetchCount(2);//表示消费者每次从mq中拉取两条消息消费,直到手动确认消费完毕后,才会再拉取消息 return factory; }
2 消费者 @RabbitListener 注解里指定containerFactory,并设置手动ack
package com.seeyii.yk.boot; import com.rabbitmq.client.Channel; import org.springframework.amqp.core.Message; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.amqp.support.AmqpHeaders; import org.springframework.messaging.handler.annotation.Header; import org.springframework.stereotype.Component; import java.io.IOException; @Component public class ConsumerBoot { @RabbitListener(queues = BootConfig.QUEUE_NAME,ackMode = "MANUAL",concurrency = "2",containerFactory = "rabbitListenerContainerFactory2") public void ListenerQueue(Message message, String data, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag, Channel channel) throws IOException { System.out.println("收到消息: "+new String(message.getBody())); System.out.println("deliveryTag: " + deliveryTag); System.out.println("data: " + data); if (data.contains("boot")) { System.out.println("业务逻辑处理完成"); // RabbitMQ的ack机制中,第二个参数返回true,表示需要将这条消息投递给其他的消费者重新消费 channel.basicAck(deliveryTag, false); } else { // 第三个参数true,表示这个消息会重新进入队列 channel.basicNack(deliveryTag, false, true); } } }
TTL:过期时间,可以给队列设置过期时间,到期后队列里所有消息会被清空,也可以单独
设置消息过期时间
MessageProperties messageProperties = new MessageProperties(); //设置消息的过期时间3s messageProperties.setExpiration("3000"); Message message = new Message("boot,hello".getBytes(),messageProperties); rabbitTemplate.convertAndSend(BootConfig.EXCHANGE_NAME,"boot.haha",message);
设置队列过期时间
@Bean("bootQueue")
public Queue bootQueue(){
return QueueBuilder.durable(QUEUE_NAME).ttl(5000).build();
}
死信队列:
消息成为死信的三种情况: 1:队列长度或数据大小超过限制.2消费者拒绝接收消息(basicNack),并且不把消息重新放回原目标队列.3 队列设置了超时时间,消息达到时间未被消费
设置死信队列代码
// 1 创建死信交换机 @Bean("deadExchange") public Exchange deadExchange(){ return ExchangeBuilder.topicExchange("dead_exchange").durable(true).build(); } // 3 死信队列和死信交换机绑定 @Bean public Binding deadBindQueueExchange(@Qualifier("deadQueue") Queue queue, @Qualifier("deadExchange") Exchange exchange){ return BindingBuilder.bind(queue).to(exchange).with("dead.#").noargs(); } // 2 创建队列,并绑定死信交换机和路由键 @Bean("bootQueue") public Queue bootQueue(){ return QueueBuilder.durable(QUEUE_NAME).deadLetterExchange("dead_exchange").deadLetterRoutingKey("dead.test").build(); }
队列过期时间(TTL)+死信队列实现延时队列
消息队列如何解决消息积压:消息积压基本都是因为消费者消费的慢导致的,基本不可能是生产者和mq本身问题,除非大促这种短时间内突然生产出大量消息,在程序设计之初就要保证消费者消费速度比生产者速度快,如果消息积压,排查消费者代码的方向:
是否在某个资源上卡住,是否有死锁,是否因为消息消费失败导致重复消费。解决方法还是先对消费者扩容,再排查代码。
浙公网安备 33010602011771号