目录
1.MQ的基本概念
1.1 MQ概述
MQ,其实就是在消息的传输过程中保存消息的容器。多使用于分布式系统之间进行通信。
总结:
1.MQ即是一个消息队列,存储消息的中间件
2.分布式系统通信的两种方式:直接远程调用和借助第三方完成间接通信
3.发送方称为生产者,接收方称为消费者
1.2MQ的优势和劣势
MQ的优势:
1.应用解耦
没有使用MQ中间件时:
(1) 此种情况没有中间件,订单系统和其他系统大大的耦合在一起
(2) 容错性低:如果说有一天,库存系统出现异常,那么与之连接的订单系统也发生异常。接着与订单系统耦合的其他三个系统,也会进行异常
(3) 可维护性低:当我们想要扩展一个系统业务时,就还得与订单系统完成交互验证,十分麻烦
使用MQ中间件使得应用间解耦,提升容错性和可维护性
(1) 增加一个MQ中间件,订单系统和其他子系统之间的耦合度大大降低
(2) 容错性提升:即使库存系统出现异常,也不会说影响到其他的系统
(3) 可维护性提升:当我们想要扩展一个子系统时,直接抽取MQ中间件中的消息信息资源即可
2.异步提速
没有使用MQ中间件时:
(1) 没有中间件时:当用户进行下单操作,我们需要同步的把保存到数据库数据,与库存系统,支付系统,物流系统等进行交互一个个步骤依次执行完,才可以返回下单成功的提示信息给用户,这个用户体验是极差的 !
(2) 所谓同步就是:在没有执行完订单系统与数据库交互的流程就不可以执行下一步与库存系统的交互,以此类推。。
使用MQ中间件后 相对于一种异步提速:
(1) 在使用MQ中间件之后,我们就完成了异步提速。
(2) 所谓异步提速就是:当用户点击下单之后,只需把订单相关信息保存到对应数据库以及投递到MQ中间件 然后就返回下单成功的提示信息给用户,这样用户体验更好。
(3)返回下单成功的提示信息给用户之后,我们再异步的进行不急不慌的执行与库存,支付,物流系统之间的交互(900ms)
3.削峰填谷
削峰:
(1) 当有一天推出了秒杀活动,在某一时刻需求量急剧增加,假设说达到每秒5000个请求。但是对于A系统来说每秒最大处理请求数量为1000个,如果用户请求直接打到系统,那么系统直接崩溃,给用户造成极差的体验感 !
此时就使用到了MQ中间件进行削峰。
(2) 当高并发达到5000个请求同时访问,使用MQ进行一并进行接收,然后A系统不急不慌的每秒从MQ中间件中拉取1000个请求进行处理
填谷:
MQ的劣势:
MQ的总结:
既然MQ有优势也有劣势,那么使用MQ需要满足什么条件呢?
1.生产者不需要从消费者处获取反馈。
分析:
所谓反馈即是,一个流程共有A->B->C三个阶段,请求从A到B执行完毕 并且从B获取执行完的反馈返回给A之后,才能执行C,这就是阻塞式的一步步的执行流程,即是同步进行的。
使用MQ就类似于一种异步执行,A在执行向B的过程中也可以同样执行向C的流程,异步同时进行。
2.容许短暂的不一致性
分析:
加上MQ中间件之后,是异步执行的。那么有可能会导致A B C这三个系统,有可能其中有一个系统执行失败,那么就会导致数据不一致
3 . 使用MQ中间件的成本权衡
1.3常见的MQ产品
1.4RabbitMQ简介
Rabbit的工作模式:
总结
1.RabbitMQ是基于AMQP协议使用Erlang语言开发的一款消息队列产品
2.RabbitMQ提供了六种工作模式,学习其中五种
3.AMQP是一种协议,类似于Http
4.JMS是API规范接口,类比JDBC
2.RabbitMQ 安装省略
RabbitMQ客户端界面登录
默认登录的用户名和密码都为guest 。
创建一个/itcast虚拟机和一个用户leomessi,并且把leomessi设置访问/itcast虚拟机的权限 !
3.RabbitMQ快速入门
1.两个模块都导入依赖
<dependencies> <!-- rabbitmq的Java客户端--> <dependency> <groupId>com.rabbitmq</groupId> <artifactId>amqp-client</artifactId> <version>5.9.0</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-clean-plugin</artifactId> <version>3.1.0</version> <!-- <configuration>--> <!-- <source>1.8</source>--> <!-- <target>1.8</target>--> <!-- </configuration>--> </plugin> </plugins> </build>
2.编写生产者:
**
代码:
public class Producer_HelloWorld { public static void main(String[] args) throws Exception { //1.创建连接工厂 ConnectionFactory connectionFactory = new ConnectionFactory() ; //2.设置参数 connectionFactory.setHost("192.168.204.134");//服务器ip地址 connectionFactory.setPort(5672);//提供的默认端口号 connectionFactory.setVirtualHost("/itcast");//创建的虚拟机名称 connectionFactory.setUsername("leomessi"); connectionFactory.setPassword("leomessi"); //3.创建连接Connection Connection connection = connectionFactory.newConnection() ; //4.创建Channel频道 Channel channel = connection.createChannel() ; //5.创建队列Queue /** * queueDeclare(String queue,boolean durable,boolean exclusive,boolean autoDelete,Map<String,Object> arguments) * * 1.queue :队列名称 * 2.durable:是否持久化,当rabbitmq重启之后,仍然存在 * 3.exclusive: * 是否独占,是否只能有一个消费者监听这个队列 当Connection关闭时,是否删除队列 * 4.autoDelete: * 是否自动删除,当没有Consumer时,自动删除掉 * 5.arguments: * 参数 */ channel.queueDeclare("hello_world",true,false,false,null) ; //6.发送消息 /** * basicPublish(String exchange, String routingKey, BasicProperties props, byte[] body) * 参数: * 1.exchange:交换机名称。简单模式下交换机使用默认的"" * 2.routingKey:路由名称(写成队列名称) 让交换机绑定到队列Queue * 3.props:配置信息 * 4.body:发送消息数据 */ String body = "hello rabbitmq~~~~" ; //6.发送消息 channel.basicPublish("","hello_world",null,body.getBytes()); //7.释放资源 channel.close(); connection.close(); } }
测试:
3.编写消费者:
代码:
public class MyConsumer { public static void main(String[] args) throws Exception{ ConnectionFactory connectionFactory = new ConnectionFactory() ; connectionFactory.setHost("192.168.204.134");//服务器ip地址 connectionFactory.setPort(5672);//提供的默认端口号 connectionFactory.setVirtualHost("/itcast");//创建的虚拟机名称 connectionFactory.setUsername("leomessi"); connectionFactory.setPassword("leomessi"); Connection connection = connectionFactory.newConnection() ; Channel channel = connection.createChannel() ; channel.queueDeclare("hello_world",true,false,false,null) ; /** * basicConsumer(String queue,boolean autoAck,Consumer callback) * 参数: * 1.queue:队列名称 * 2.autoAck:是否自动确认 * 3.callback:回调对象 */ //接收对象 Consumer consumer = new DefaultConsumer(channel) { /** * @param consumerTag:标识 * @param envelope:获取一些信息,交换机,路由key * @param properties:配置信息 * @param body:获取Message消息数据 * @throws IOException */ @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { System.out.println("consumerTag" + consumerTag) ; System.out.println("Exchange:"+envelope.getExchange()) ; System.out.println("RoutingKey:"+envelope.getRoutingKey()) ;//路由:即是队列名称 System.out.println("properties:"+properties) ; System.out.println("body:"+new String(body));//body这个消息数据是二进制类型的消息数据 所以进行转化 } } ; channel.basicConsume("hello_world",true,consumer) ; //消费者是一直进行监听着的,所以不要进行关闭连接 !!! } }
4.Rabbit工作模式
4.1 Work queues工作队列模式
很多个消费者进行监听同一个队列,但是每一个Message消息只可以由一个消费者得到。一般都是按照顺序,一人一条的进行获取
4.2 Pub/Sub订阅模式
很多个消费者进行监听很多个队列,每一个消费者对应一个队列,每一个消费者进行消费对应队列中的消息数据。
PubSub生产者:
代码:
@SuppressWarnings({"all"}) public class Producer_PubSub { public static void main(String[] args) throws Exception{ //1.创建连接工厂 ConnectionFactory connectionFactory = new ConnectionFactory() ; //2.设置参数 connectionFactory.setHost("192.168.204.134");//服务器ip地址 connectionFactory.setPort(5672);//提供的默认端口号 connectionFactory.setVirtualHost("/itcast");//创建的虚拟机名称 connectionFactory.setUsername("leomessi"); connectionFactory.setPassword("leomessi"); //3.创建连接 Connection connection = connectionFactory.newConnection() ; //4.创建Channel Channel channel = connection.createChannel() ; //5.创建交换机 /** * exchangeDeclare(String exchange,BuiltinExchangeType type,boolean durable, * boolean autoDelete,boolean internal,Object arguments) * 参数: * 1.exchange:交换机名称 * 2.type:交换机类型 * DIRECT("direct") :定向 * FANOUT("fanout") : 扇形(广播),发送消息到每一个与之绑定队列 * TOPIC("topic") :通配符的方式 * HEADERS("headers") : 参数匹配 * 3.durable:是否持久化 * 4.autoDelete:自动删除 * 5.internal:内部使用,一般为false * 6.arguments:参数 */ String exchangeName = "test_fanout" ; channel.exchangeDeclare(exchangeName, BuiltinExchangeType.FANOUT,true,false,false,null) ; //6.创建队列 /** * queueDeclare(String queue,boolean durable,boolean exclusive,boolean autoDelete,Map<String,Object> arguments) * * 1.queue :队列名称 * 2.durable:是否持久化,当rabbitmq重启之后,仍然存在 * 3.exclusive: * 是否独占,是否只能有一个消费者监听这个队列 当Connection关闭时,是否删除队列 * 4.autoDelete: * 是否自动删除,当没有Consumer时,自动删除掉 * 5.arguments: * 参数 */ String queue1Name = "test_fanout_queue1" ; String queue2Name = "test_fanout_queue2" ; channel.queueDeclare(queue1Name,true,false,false,null) ; channel.queueDeclare(queue2Name,true,false,false,null) ; //7.绑定队列和交换机 /** * queueBind(String queue,String exchange,String routingKey) * 参数: * 1.queue:队列名称 * 2.exchange:交换机 * 3.routingKey:路由键,绑定规则 * 如果交换机的类型为fanout,那么说明向四周广播所有队列,那么就不用指定routingKey,即是"" * 一般routingKey都指定的是队列名称 */ channel.queueBind(queue1Name,exchangeName,"") ; channel.queueBind(queue2Name,exchangeName,"") ; //8.发送消息 /** * basicPublish(String exchange, String routingKey, BasicProperties props, byte[] body) * 参数: * 1.exchange:交换机名称。简单模式下交换机使用默认的"" * 2.routingKey:路由名称(写成队列名称) 让交换机绑定到队列Queue * 3.props:配置信息 * 4.body:发送消息数据 */ String body = "MXY I MISS YOU" ; channel.basicPublish(exchangeName,"",null,body.getBytes()); //9.释放资源 channel.close(); connection.close(); } }
PubSub消费者:
队列1对应的消费者1:
@SuppressWarnings({"all"}) public class Consumer_PubSub1 { public static void main(String[] args) throws Exception{ ConnectionFactory connectionFactory = new ConnectionFactory() ; connectionFactory.setHost("192.168.204.134");//服务器ip地址 connectionFactory.setPort(5672);//提供的默认端口号 connectionFactory.setVirtualHost("/itcast");//创建的虚拟机名称 connectionFactory.setUsername("leomessi"); connectionFactory.setPassword("leomessi"); Connection connection = connectionFactory.newConnection() ; Channel channel = connection.createChannel() ; //声明这两个队列 String queue1Name = "test_fanout_queue1" ; String queue2Name = "test_fanout_queue2" ; /** * basicConsumer(String queue,boolean autoAck,Consumer callback) * 参数: * 1.queue:队列名称 * 2.autoAck:是否自动确认 * 3.callback:回调对象 */ //接收对象 Consumer consumer = new DefaultConsumer(channel) { /** * @param consumerTag:标识 * @param envelope:获取一些信息,交换机,路由key * @param properties:配置信息 * @param body:获取Message消息数据 * @throws IOException */ @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { System.out.println("consumerTag" + consumerTag) ; System.out.println("Exchange:"+envelope.getExchange()) ; System.out.println("RoutingKey:"+envelope.getRoutingKey()) ;//路由:即是队列名称 System.out.println("properties:"+properties) ; System.out.println("body:"+new String(body));//body这个消息数据是二进制类型的消息数据 所以进行转化 System.out.println("将日志信息打印到控制台......."); } } ; channel.basicConsume(queue1Name,true,consumer) ; //消费者是一直进行监听着的,所以不要进行关闭连接 !!! } }
队列2对应的消费者2:
@SuppressWarnings({"all"}) public class Consumer_PubSub2 { public static void main(String[] args) throws Exception{ ConnectionFactory connectionFactory = new ConnectionFactory() ; connectionFactory.setHost("192.168.204.134");//服务器ip地址 connectionFactory.setPort(5672);//提供的默认端口号 connectionFactory.setVirtualHost("/itcast");//创建的虚拟机名称 connectionFactory.setUsername("leomessi"); connectionFactory.setPassword("leomessi"); Connection connection = connectionFactory.newConnection() ; Channel channel = connection.createChannel() ; //声明这两个队列 String queue1Name = "test_fanout_queue1" ; String queue2Name = "test_fanout_queue2" ; /** * basicConsumer(String queue,boolean autoAck,Consumer callback) * 参数: * 1.queue:队列名称 * 2.autoAck:是否自动确认 * 3.callback:回调对象 */ //接收对象 Consumer consumer = new DefaultConsumer(channel) { /** * @param consumerTag:标识 * @param envelope:获取一些信息,交换机,路由key * @param properties:配置信息 * @param body:获取Message消息数据 * @throws IOException */ @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { System.out.println("consumerTag" + consumerTag) ; System.out.println("Exchange:"+envelope.getExchange()) ; System.out.println("RoutingKey:"+envelope.getRoutingKey()) ;//路由:即是队列名称 System.out.println("properties:"+properties) ; System.out.println("body:"+new String(body));//body这个消息数据是二进制类型的消息数据 所以进行转化 System.out.println("将日志信息保存到数据库......."); } } ; channel.basicConsume(queue2Name,true,consumer) ; //消费者是一直进行监听着的,所以不要进行关闭连接 !!! } }
4.3 Routing路由模式
依照4.2的代码进行修改即可:
(1)队列与交换机的绑定,不能再是任意的绑定了,而是要指定一个RoutingKey(路由Key)
(2)消息的发送方在向Exchange交换机发送消息时,也必须指定消息的RoutingKey
(3) Exchange不再把消息交给每一个队列,而是根据消息的RoutingKey进行判断,只有队列的RoutingKey与消息的RoutingKey完全一致,才可以接收到消息
消费者进行改变队列名称即可:
4.4 Topics通配符模式
生产者:
消费者:
同理即可:
4.5工作模式总结:
1.一对一的模式,其实是有一个默认的交换机的,我们标识为""
2.
(1)使用的是默认的交换机:""
(2)一个队列对应着多个消费者,当有消息进入到队列之后,消费者是轮询进行消费,一人一条进行消费。
(3)应用的场景就是:任务比较繁重的时候,多开启几个消费者进行消费,减缓压力。
3.PubSub订阅模式:
(1)设置的交换机类型为广播类型
(2)表示队列1和2可以接收到交换机发送出的所有Message
(3)消费者1只可以消费进入队列1的消息。消费者2同理即可
4.Routing路由模式:
5.Topics通配符模式:
5.Spring整合RabbitMQ
导入两个项目中的相同依赖
<dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.1.7.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.amqp</groupId> <artifactId>spring-rabbit</artifactId> <version>2.1.8.RELEASE</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>5.1.7.RELEASE</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.8.0</version> <configuration> <source>1.8</source> <target>1.8</target> </configuration> </plugin> </plugins> </build>
rabbitmq.properties:
rabbitmq.host=192.168.204.134 rabbitmq.port=5672 rabbitmq.username=leomessi rabbitmq.password=leomessi rabbitmq.virtual-host=/itcast
生产者的编写:
Spring配置文件:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:rabbit="http://www.springframework.org/schema/rabbit" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/rabbit http://www.springframework.org/schema/rabbit/spring-rabbit.xsd"> <!--加载配置文件--> <context:property-placeholder location="classpath:/rabbitmq.properties"/> <!-- 定义rabbitmq connectionFactory --> <rabbit:connection-factory id="connectionFactory" host="${rabbitmq.host}" port="${rabbitmq.port}" username="${rabbitmq.username}" password="${rabbitmq.password}" virtual-host="${rabbitmq.virtual-host}"/> <!--定义管理交换机、队列--> <rabbit:admin connection-factory="connectionFactory"/> <!--定义持久化队列,不存在则自动创建;不绑定到交换机则绑定到默认交换机 默认交换机类型为direct,名字为:"",路由键为队列的名称 --> <rabbit:queue id="spring_queue" name="spring_queue" auto-declare="true"/> <!-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~广播;所有队列都能收到消息~~~~~~~~~~~~~~~~~~~~~~~~~~~~ --> <!--定义广播交换机中的持久化队列,不存在则自动创建--> <rabbit:queue id="spring_fanout_queue_1" name="spring_fanout_queue_1" auto-declare="true"/> <!--定义广播交换机中的持久化队列,不存在则自动创建--> <rabbit:queue id="spring_fanout_queue_2" name="spring_fanout_queue_2" auto-declare="true"/> <!--定义广播类型交换机;并绑定上述两个队列--> <rabbit:fanout-exchange id="spring_fanout_exchange" name="spring_fanout_exchange" auto-declare="true"> <rabbit:bindings> <rabbit:binding queue="spring_fanout_queue_1"/> <rabbit:binding queue="spring_fanout_queue_2"/> </rabbit:bindings> </rabbit:fanout-exchange> <!-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~通配符;*匹配一个单词,#匹配多个单词 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ --> <!--定义广播交换机中的持久化队列,不存在则自动创建--> <rabbit:queue id="spring_topic_queue_star" name="spring_topic_queue_star" auto-declare="true"/> <!--定义广播交换机中的持久化队列,不存在则自动创建--> <rabbit:queue id="spring_topic_queue_well" name="spring_topic_queue_well" auto-declare="true"/> <!--定义广播交换机中的持久化队列,不存在则自动创建--> <rabbit:queue id="spring_topic_queue_well2" name="spring_topic_queue_well2" auto-declare="true"/> <rabbit:topic-exchange id="spring_topic_exchange" name="spring_topic_exchange" auto-declare="true"> <rabbit:bindings> <rabbit:binding pattern="heima.*" queue="spring_topic_queue_star"/> <rabbit:binding pattern="heima.#" queue="spring_topic_queue_well"/> <rabbit:binding pattern="itcast.#" queue="spring_topic_queue_well2"/> </rabbit:bindings> </rabbit:topic-exchange> <!--定义rabbitTemplate对象操作可以在代码中方便发送消息--> <rabbit:template id="rabbitTemplate" connection-factory="connectionFactory"/> </beans>
测试类:
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(locations = "classpath:spring-rabbitmq-producer.xml") public class ProducerTest { @Autowired private RabbitTemplate rabbitTemplate ; /** * 1.发送HelloWorld类型消息 */ @Test public void testHelloWorld() { //发送消息到队列中 rabbitTemplate.convertAndSend("spring_queue","hello world !"); } /** * 发送fanout消息 */ @Test public void testFanout() { rabbitTemplate.convertAndSend("spring_fanout_exchange","","发送内容1"); } /** * 发送topic消息 */ @Test public void testTopics() { rabbitTemplate.convertAndSend("spring_topic_exchange","","发送内容2"); } }
消费者的编写:
Spring配置文件:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:rabbit="http://www.springframework.org/schema/rabbit" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/rabbit http://www.springframework.org/schema/rabbit/spring-rabbit.xsd"> <!--加载配置文件--> <context:property-placeholder location="classpath:/rabbitmq.properties"/> <!-- 定义rabbitmq connectionFactory --> <rabbit:connection-factory id="connectionFactory" host="${rabbitmq.host}" port="${rabbitmq.port}" username="${rabbitmq.username}" password="${rabbitmq.password}" virtual-host="${rabbitmq.virtual-host}"/> <bean id="springQueueListener" class="SpringQueueListener"/> <!-- <bean id="fanoutListener1" class="FanoutListener1"/>--> <!-- <bean id="fanoutListener2" class="FanoutListener2"/>--> <!-- <bean id="topicListenerStar" class="TopicListenerStar"/>--> <!-- <bean id="topicListenerWell" class="TopicListenerWell"/>--> <!-- <bean id="topicListenerWell2" class="TopicListenerWell2"/>--> <rabbit:listener-container connection-factory="connectionFactory" auto-declare="true"> <rabbit:listener ref="springQueueListener" queue-names="spring_queue"/> <!-- <rabbit:listener ref="fanoutListener1" queue-names="spring_fanout_queue_1"/>--> <!-- <rabbit:listener ref="fanoutListener2" queue-names="spring_fanout_queue_2"/>--> <!-- <rabbit:listener ref="topicListenerStar" queue-names="spring_topic_queue_star"/>--> <!-- <rabbit:listener ref="topicListenerWell" queue-names="spring_topic_queue_well"/>--> <!-- <rabbit:listener ref="topicListenerWell2" queue-names="spring_topic_queue_well2"/>--> </rabbit:listener-container> </beans>
进行创建配置文件中对应的监听器即可:
public class SpringQueueListener implements MessageListener { @Override public void onMessage(Message message) { //消息数据是二进制类型,我们进行new String转化为String字符串类型 System.out.println(new String(message.getBody())); } }
public class FanoutListener1 implements MessageListener { @Override public void onMessage(Message message) { //消息数据是二进制类型,我们进行new String转化为String字符串类型 System.out.println(new String(message.getBody())); } }
其他的监听器的编写省略。。。。。同理即可 。
细节记录:
6.SpringBoot整合RabbitMQ
生产者的编写演示:
1.application.yml文件
#配置RabbitMQ的基本信息,ip地址,端口号,username,password spring: rabbitmq: host: 192.168.204.134 port: 5672 username: guest password: guest virtual-host: /
2.导入依赖
<!-- 父工程依赖--> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.0</version> </parent> <!--rabbitmq依赖--> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> </dependency> </dependencies>
3.指定配置类
@Configuration public class RabbitMQConfig { 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.Queue队列 @Bean("bootQueue") public Queue bootQueue() { return QueueBuilder.durable(QUEUE_NAME).build(); } //3.队列和交换机的绑定关系 Binding /** * 1.知道哪个队列 * 2.知道哪个交换机 * 3.routingKey */ @Bean public Binding bindingQueueExchange(@Qualifier("bootQueue") Queue queue, @Qualifier("bootExchange") Exchange exchange) { return BindingBuilder.bind(queue)//绑定哪个队列 .to(exchange)//绑定哪个交换机 .with("boot.#")//指定要想成功接收到交换机的消息所需满足的routingKey的要求(topic工作模式) .noargs() ;//不加入参数 } }
4.测试:
public class ProducerTest { //注入RabbitTemplate @Autowired private RabbitTemplate rabbitTemplate ; @Test public void test01() { //发送一个消息数据到队列中 rabbitTemplate.convertAndSend(RabbitMQConfig.EXCHANGE_NAME,"boot.haha","发送内容Message001"); } }
消费者的编写演示:
1.导入依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency>
2.application.yml
#配置RabbitMQ的基本信息,ip地址,端口号,username,password spring: rabbitmq: host: 192.168.204.134 port: 5672 username: guest password: guest virtual-host: /
3.监听类
@Component public class RabbitMQListener { @RabbitListener(queues = "boot_queue") public void ListenerQueue(Message message) { System.out.println(new String(message.getBody())); } }
总结:
对于SpringBoot来说,使用远程导入Maven依赖,简化了配置文件的复杂配置 !
1.RabbitMQ的高级特性
原有Spring配置类
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:rabbit="http://www.springframework.org/schema/rabbit" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/rabbit http://www.springframework.org/schema/rabbit/spring-rabbit.xsd"> <!--加载配置文件--> <context:property-placeholder location="classpath:/rabbitmq.properties"/> <!-- 定义rabbitmq connectionFactory --> <rabbit:connection-factory id="connectionFactory" host="${rabbitmq.host}" port="${rabbitmq.port}" username="${rabbitmq.username}" password="${rabbitmq.password}" virtual-host="${rabbitmq.virtual-host}" /> <!--定义管理交换机、队列--> <rabbit:admin connection-factory="connectionFactory"/> <!--定义rabbitTemplate对象操作可以在代码中方便发送消息--> <rabbit:template id="rabbitTemplate" connection-factory="connectionFactory"/> <!-- 消息可靠性投递(生产端)--> <rabbit:queue id="test_queue_confirm" name="test_queue_confirm"></rabbit:queue> <rabbit:direct-exchange name="test_exchange_confirm"> <rabbit:bindings> <rabbit:binding queue="test_queue_confirm" key="confirm"></rabbit:binding> </rabbit:bindings> </rabbit:direct-exchange> </beans>
1.1消息的可靠投递
confirm
1.
2.
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(locations = "classpath:spring-rabbitmq-producer.xml") public class ProducerTest { @Autowired private RabbitTemplate rabbitTemplate ; /** * 确认模式: * 步骤: * 1.确认模式开启: ConnectionFactory中开启 publisher-confirms="true" * 2.在rabbitTemplate定义ConfirmCallBack回调函数 */ @Test public void testConfirm() { //2.定义回调函数 rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() { @Override public void confirm(CorrelationData correlationData, boolean b, String s) { System.out.println("confirm方法被执行了........."); } }); //3.发送消息 rabbitTemplate.convertAndSend("test_exchange_confirm","confirm","Message001"); } }
其中细节:
return
1.
2.
/** * 回退模式:当消息发送给Exchange后,Exchange路由到Queue失败时 才会执行ReturnCallBack * 步骤: * 1.开启回退模式 * 2.设置ReturnCallBack * 3.设置Exchange处理消息的模式: * 1.如果消息没有路由到Queue,则丢弃消息(默认情况下) * 2.如果消息没有路由到Queue,返回给消息发送方ReturnCallBack */ @Test public void testReturn() { //设置交换机处理失败消息(即消息没有路由到Queue的消息) rabbitTemplate.setMandatory(true) ; //2.设置ReturnCallBack rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() { @Override public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) { System.out.println("return 执行了....."); } }); //3.发送消息 rabbitTemplate.convertAndSend("test_exchange_confirm","confirm","message confirm....."); }
细节记录:
方法参数含义:
总结:
对于confirm:
对于return:
1.2 Consumer Ack
ack指Acknowledge,确认。是消费端接收到生产者发送的消息后的确认方式
有三种确认方式:
自动确认:acknowledge="none"
手动确认:acknowledge="manual"
根据异常情况确认:acknowledge="auto"(这种方式使用麻烦,一般不使用)
步骤演示:
1.
2.进行配置手动签收的监听器:
/** * Consumer ACK机制: * 1.设置手动签收,acknowledge="manual" * 2.让监听器类实现ChannelAwareMessageListener接口 * 3.如果消息成功处理,则调用channel的basicAck()签收 * 4.如果消息处理失败,则调用channel的basicNack()进行拒绝签收,broker重新发送给consumer */ @Component public class AckListener implements ChannelAwareMessageListener { @Override public void onMessage(Message message, Channel channel) throws Exception { //投递消息的标识 long deliveryTag = message.getMessageProperties().getDeliveryTag() ; try { //1.接收转换信息 System.out.println(new String(message.getBody())); //2.处理业务逻辑 System.out.println("处理业务逻辑...."); //3.手动签收 //第二个参数表示 一次是否要进行签收多个消息数据?为true表示是的 channel.basicAck(deliveryTag,true); } catch (IOException e) { //4.出现错误,拒绝签收 /** * 第二个参数:一次是否要进行签收多个消息数据?为true表示是的 * 第三个参数:为true表示 重回队列,表示消息重新回到queue,broker会重新发送该消息给消费者 */ channel.basicNack(deliveryTag,true,true); } } }
消息可靠性总结
1.3消费端限流
业务场景1:
(1) 秒杀场景下,高并发访问量请求,我们先把这些请求打到中间件MQ中,然后每秒从MQ中拉取1000个请求打到A系统。
(2) 因为对于A系统来说,每秒最多只可以进行处理1000个请求,处理多了会崩溃。其实这就是消费端的限流
业务场景2:
(1) 我们做了MQ的限流方案,但是A系统宕机维护了一段时间。此时大量请求堆积在MQ中,当A系统修好后继续使用,此时大量积压的请
求会打到A系统,此时A系统又宕机了 !
(2) 所以,我们还要进行对消费端的限流操作
代码演示:
1.在<rabbit:listener-container中配置prefetch属性,设置消费端一次拉取多少条消息
2.消费端的确认模式为:acknowledge="manual",手动确认模式
3.配置消费端的监听器,每当读取一条消息之后,进行手动确认一下,然后再进行读取下一条消息数据。
/** * Consumer 限流机制: * 1.确保ack机制为手动添加 * 2.对<listener-container 配置属性 * perfetch = 1 :表示消费端每次从mq拉取一条消息来进行消费,直到手动确认消费完毕后,才会继续拉取下一条消息 */ public class QosListener implements ChannelAwareMessageListener { @Override public void onMessage(Message message, Channel channel) throws Exception { //1.获取消息 System.out.println(new String(message.getBody())); //2.处理业务逻辑 //3.签收 //第一个参数:为投递消息的标识 //第二个参数: 表示一次是否要签收多个消费数据?为true表示是的 channel.basicAck(message.getMessageProperties().getDeliveryTag(),true); } }
总结:
利用<rabbit:listener-container中配置prefetch属性,设置消费端一次拉取多少条消息,这样我们就可以达到消费端限流的实现。因此我
们对消费端服务器每次可以接收多少请求数量可以进行限流限制 !
1.4 TTL
TTL的全称为Time To Live(存活时间或过期时间)
当消息到达存活时间后,还没有被消费,会被自动清除掉 !
RabbitMQ可以对消息进行设置过期时间,也可以对整个队列(Queue)进行设置过期时间。
代码实现演示:
情况一:队列统一过期时间
(1) 只进行设置队列的过期时间,而不进行设置消息的过期时间。那么消息过期时间默认和队列过期时间一致 !都为10秒
(2) 进行测试,这十条消息 10秒之后跟着队列一块消失死亡
情况二:消息单独过期
(1)
(2)
(3)运行之后的测试结果:
队列中的消息数据在5秒之后消失死亡,队列在10秒之后消失死亡
(4)注意事项:
当队列过期之后,会将队列中所有的消息全部都移除掉。
当消息过期之后,只有当消息在队列的顶端时,才会进行判断该消息是否过期,如果过期,那么把该消息从队列中移除。
分析:所谓在队列的顶端,即是即将被进行消费的消息,这种情况下我们才会进行判断消息是否过期。这样就是为了进行保证性能与稳定。
总结:
1.5死信队列
(1) 当一个队列的中的消息 过期之后,会成为Dead message
(2) 该Dead message会被重新发送到另外一个交换机,这个交换机即是DLX
消息成为死信的三种情况:
1.队列消息长度到达限制
2.消费者拒接消费信息,basicNack/basicReject,并且不把消息重新放入到原目标队列 即requeue=false
3.原队列存在消息过期设置,消息到达超时时间未被消费
队列绑定死信交换机:
编码实现:
1.对应Spring配置文件:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:rabbit="http://www.springframework.org/schema/rabbit" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/rabbit http://www.springframework.org/schema/rabbit/spring-rabbit.xsd"> <!--加载配置文件--> <context:property-placeholder location="classpath:/rabbitmq.properties"/> <!-- 定义rabbitmq connectionFactory --> <rabbit:connection-factory id="connectionFactory" host="${rabbitmq.host}" port="${rabbitmq.port}" username="${rabbitmq.username}" password="${rabbitmq.password}" virtual-host="${rabbitmq.virtual-host}" publisher-confirms="true" publisher-returns="true" /> <!--定义管理交换机、队列--> <rabbit:admin connection-factory="connectionFactory"/> <!--定义rabbitTemplate对象操作可以在代码中方便发送消息--> <rabbit:template id="rabbitTemplate" connection-factory="connectionFactory"/> <!-- 死信队列: 1.声明正常的队列(test_queue_dix)和交换机(test_exchange_dix) 2.声明死信队列(queue_dix)和死信交换机(exchange_dix) 3.正常队列绑定死信交换机 设置两个参数: x-dead-letter-exchange:死信交换机的名称 x-dead-letter-routing-key:发送给死信交换机的routingKey --> <!-- 1.声明正常的队列(test_queue_dix)和交换机(test_exchange_dix)--> <rabbit:queue name="test_queue_dix" id="test_queue_dix"> <!-- 3.正常队列绑定死信交换机--> <rabbit:queue-arguments> <!-- 3.1 x-dead-letter-exchange:设置死信交换机的名称--> <entry key="x-dead-letter-exchange" value="exchange_dix"/> <!-- 3.2 x-dead-letter-routing-key:发送给死信交换机的routingKey--> <entry key="x-dead-letter-routing-key" value="dlx.hehe"/> <!-- 4.1 设置队列的过期时间 ttl--> <entry key="x-message-ttl" value="10000" value-type="java.lang.Integer"/> <!-- 4.2 设置队列的长度限制 max-length :即是队列中最多可以存储10条消息数据--> <entry key="x-max-length" value="10" value-type="java.lang.Integer"/> </rabbit:queue-arguments> </rabbit:queue> <!-- 表示工作模式为topic--> <rabbit:topic-exchange name="test_exchange_dix"> <rabbit:bindings> <rabbit:binding pattern="test.dlx.#" queue="test_queue_dix"/> </rabbit:bindings> </rabbit:topic-exchange> <!-- 2.声明死信队列(queue_dix)和死信交换机(exchange_dix)--> <rabbit:queue name="queue_dix" id="queue_dix"/> <!--工作模式设为topic--> <rabbit:topic-exchange name="exchange_dix"> <rabbit:bindings> <rabbit:binding pattern="dlx.#" queue="queue_dix"/> </rabbit:bindings> </rabbit:topic-exchange> </beans>
2.测试:
前两种成为死信消息的测试:
第三种:消息被拒收导致消息成为死信的测试
先进行创建一个监听器类:
再进行配置消费者端对应的Spring配置文件
消费者端的监听器配置完成之后,最后让生产者进行发送一条消息数据
细节记录:
1.
2.
3.
1.6 延迟队列
延迟队列:即消息进入队列后不会立即被进行消费,只有到达指定时间之后,才会被进行消费
需求:
实现方式:
1.使用定时器 [但是这种做法弊端较大 不优雅]
分析:
(1) 流程:确定一个定时器,找出一个与现在时间间隔为30分钟,然后每一分钟内进行检索该用户是否进行支付,如果支付,则进行更新数据库等操作。如果直到30分钟还没进行支付,那么就要取消订单,回滚库存
(2) 弊端:该定时器存在误差,假设设为一分钟进行检索一次,那么在查询完数据后的一两秒之间,我们进行购买,这样就会出现大概10
秒多的延时误差,用户明明购买了,但是我们返回的提示仍然是没有购买。因为我们是一分钟进行检索一次 !!这样给用户的体验感过
差。
假设说我们设置为1秒钟检索一次,这种方法下出现误差的概率降低,并且用户体验感提升。但是在1秒钟之内完成数据库的查询等一系列
操作,这样会给数据库带来极大的压力与性能损耗降低
2.所以我们推荐使用延迟队列
(1) 当提交一个订单之后,我们就把其放置到一个延迟队列中
(2) 等到30分钟后再进行消费消息,然后进入到库存系统进行判断订单状态 然后进行操作 !
(3) 总结:这种做法极大的增加了数据库的性能与消息的准确性
但是RabbitMQ中没有提供延迟队列
代码演示:
1.配置
<!-- 延迟队列: 1.定义正常交换机(order_exchange)和队列(order_queue) 2.定义死信交换机(order_exchange_dlx)和队列(order_queue_dlx) 3.绑定,设置正常队列过期时间为30分钟 --> <!--1.定义正常交换机(order_exchange)和队列(order_queue)--> <rabbit:queue id="order_queue" name="order_queue"> <!--3.绑定,设置正常队列过期时间为30分钟--> <rabbit:queue-arguments> <!-- 绑定交换机--> <entry key="x-dead-letter-exchange" value="order_queue_dlx"/> <entry key="x-dead-letter-routing-key" value="dlx.order.cancel"/> <entry key="x-message-ttl" value="10000" value-type="java.lang.Integer"/> </rabbit:queue-arguments> </rabbit:queue> <rabbit:topic-exchange name="order_exchange" id="order_exchange"> <rabbit:bindings> <rabbit:binding pattern="order.#" queue="order_queue"/> </rabbit:bindings> </rabbit:topic-exchange> <!--2.定义死信交换机(order_exchange_dlx)和队列(order_queue_dlx)--> <rabbit:queue id="order_queue_dlx" name="order_queue_dlx"> </rabbit:queue> <rabbit:topic-exchange name="order_exchange_dlx" id="order_exchange_dlx"> <rabbit:bindings> <rabbit:binding pattern="dlx.order.#"/> </rabbit:bindings> </rabbit:topic-exchange> </beans>
2.测试:
3.
4.