在前面的博客中我们已经详细的介绍了RabbitMQ的5种模型,接下来让我们来看看在SpringBoot中如何使用RabbitMQ。
RabbitMQ5种模型回顾
1.点对点模型:生产者不断向消息队列中生产消息,消费者不断的从队列中获取消息。生产者和消费者是一一对应关系。如下图所示:
2.工作队列模型: 让多个消费者绑定到同一个队列中,共同消费队列中的消息。并且队列中的消息一旦消费,就会消息,因此任务不会被重复执行。如下图所示:
3. 广播模型: 可以将一个消息发送给多个消费者。如下图所示:
4.路由模型:可以通过RoutingKey实现不同的消息被不同的队列消费。如下图所示:
5.主题(Topic)模型: 和路由模型基本一致,只不过可以基于多重条件进行路由选择。如下图所示:
这5种模型,我在前面的博客中已经详细介绍了,没有了解的朋友,可以去看看我前面的博客。
SpringBoot整合RabbitMQ
1.导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
2.在application.yml中加入如下配置
spring:
rabbitmq:
host: 172.16.114.135 //虚拟机的ip地址
port: 5672 //端口号
username: ems //用户名
password: 123 //密码
virtual-host: /ems //虚拟主机名称
这样导入RabbitMQ已经成功了,接下来讲一下RabbitMQ五种模式结合SpringBoot
HelloWorld
创建生产者
@SpringBootTest(classes = RabbitmqSpringbootApplication.class)
public class TestRabbitMq {
//注入rabbitTemplate
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
public void testHello(){
//发送消息 参数1:队列的名称 参数2: 发送消息的内容
//运行生产者并不会创建对应,需要有消费者才会去创建队列
rabbitTemplate.convertAndSend("hello","hello world");
}
}
这里面的RabbitTemplate是SpringBoot封装好的用来操作消息队列的。convertAndSend是里面的其中一个方法,用作生产者匹配交换机、队列以及携带消息的。
创建消费者
@Component //默认创建的队列是 持久化 非独占 不是自动删除队列
@RabbitListener(queuesToDeclare = @Queue(value = "hello",declare = "true"))
//durable:是否持久化 autoDelete:是否自动删除 值都是String类型
public class Customer {
@RabbitHandler //代表当我们从队列中取出消息时,是通过这个方法来处理消息的
public void receivel(String message){
System.out.println("message = "+message);
}
}
@RabbitListener 是用来绑定队列的,queuesToDeclare属性: 用来创建队列的,如果没有就创建队列,否则不创建,和@Queue搭配使用。
@Queue:用来配置一个队列,主要有以下属性:
- value: 队列名称
- declare: 是否持久化队列
- autoDelete:是否自动删除队列
- exclusive: 是否独占队列, true:表示当前队列只允许当前连接可以,false:其他连接也可用当前队列
- 需要注意的是:它们的值都是字符串类型,只能填"true" 或"false"
- 如果只指定@Queue(“hello”) 那么默认创建的队列是持久化、非独占、不是自动删除的队列
@RabbitHandler:是用来表示该方法是消费者接收消息的方法
需要注意的是在SpringBoot中使用RabbitMQ和前面博客中使用RabbitMQ有一点区别:
- 在前面博客中运行生产者就会直接创队列
- 但是在SpringBoot中运行生产者并不会创建队列,创建队列是根据消费者来创建的,只有有消费者的时候才会去创建队列。
工作队列
1.生产者
//注入rabbitTemplate
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
public void testWork(){
for(int i = 0;i<10;i++){
rabbitTemplate.convertAndSend("work","hello work"+i);
}
}
2.消费者
@Component
public class WorkCustomer {
@RabbitListener(queuesToDeclare = @Queue("work"))
public void receive1(String message){
System.out.println("work message1 = "+message);
}
@RabbitListener(queuesToDeclare = @Queue("work"))
public void receive2(String message){
System.out.println("work message2 = "+message);
}
}
注意: 默认在Spring AMQP实现中Work这种方式就是公平调度。
如果消费者1处理业务比较快,早早的干完自己的活,消费者2处理业务比较慢,但是在RabbitMQ默认的分发机制(轮询分发)下,默认情况下RabbitMQ会将接收到的消息逐个分发给消费者,并且是一次性分发完,所以消费者1和消费者2获取的消息数量是相同的。
这显然不是我们想要的,我们可以使用能者多劳机制解决这个问题,给处理慢的消费者少发点,给处理快的消费者多发点。 有关详细的了解可以看这篇博客 RabbitMQ系列–工作队列模型
1.修改配置文件
spring:
rabbitmq:
host: 172.16.114.135
port: 5672
username: ems
password: 123
virtual-host: /ems
listener:
simple:
prefetch: 1
prefetch=1:它表示限制每个Consumer在同一个时间点最多只能处理一个消息,我手里的活还没干完的话你就不能再给我分了。
2.在消费者2中使用Threed.sleep(1000)来模型处理业务慢的问题
@RabbitListener(queuesToDeclare = @Queue("work"))
public void receive2(String message) throws InterruptedException {
Thread.sleep(1000);
System.out.println("work message2 = "+message);
}
运行代码可以发现,消费者1处理的快,处理的就比较多。
Fanout模型
1.创建生产者
@Test
public void testFanout(){
rabbitTemplate.convertAndSend("logs","","这是日志广播");
}
2.创建消费者
@Component
public class FanoutCustomer {
@RabbitListener(bindings = @QueueBinding(
value = @Queue,
exchange = @Exchange(name="logs",type = "fanout")
))
public void receive1(String message){
System.out.println("message1 = " + message);
}
@RabbitListener(bindings = @QueueBinding(
value = @Queue, //创建临时队列
exchange = @Exchange(name="logs",type = "fanout") //绑定交换机类型
))
public void receive2(String message){
System.out.println("message2 = " + message);
}
}
@RabbitListener中的bingings属性是用来将交换机和队列进行绑定的
@QueueBinding: 绑定交换机和队列,主要参数如下:
- value: 队列 @Queue创建一个临时队列
- exchange: 交换机
- key: 路由名称 是一个数组,可以定义多个名称
@Exchange:用来创建一个交换机
- name:交换机名称
- type: 交换机类型
运行代码,当生产者发送一条消息,所有的消费者都会收到这条消息,如下图所示:
路由(Direct)
1.生产者
@Test
public void testDirect(){
//参数1:交换机名称 参数2: 路由名称 参数3:发送的消息
rabbitTemplate.convertAndSend("directs","error","error 的日志信息");
}
2.消费者
@Component
public class DirectCustomer {
@RabbitListener(bindings ={
@QueueBinding(
value = @Queue(),
key={"info","error"},
exchange = @Exchange(type = "direct",name="directs")
)})
public void receive1(String message){
System.out.println("message1 = " + message);
}
@RabbitListener(bindings ={
@QueueBinding(
value = @Queue(),
key={"error"},
exchange = @Exchange(type = "direct",name="directs")
)})
public void receive2(String message){
System.out.println("message2 = " + message);
}
}
在消费者代码中,消费者1只能接收生产者发送路由名为"info"或"error"的消息,消费者2只能接收生产者发送路由名为"error"的消息。
Topic(主题)
1.生产者
//topic
@Test
public void testTopic(){
rabbitTemplate.convertAndSend("topics","user.save.findAll","user.save.findAll 的消息");
}
2.消费者
@Component
public class TopCustomer {
@RabbitListener(bindings = {
@QueueBinding(
value = @Queue,
key = {"user.*"},
exchange = @Exchange(type = "topic",name = "topics")
)
})
public void receive1(String message){
System.out.println("message1 = " + message);
}
@RabbitListener(bindings = {
@QueueBinding(
value = @Queue,
key = {"user.#"},
exchange = @Exchange(type = "topic",name = "topics")
)
})
public void receive2(String message){
System.out.println("message2 = " + message);
}
}
*: 代表一个单词 #代表零个或多个单词。定义路由的名称用.隔开 比如在消费者1中定义路由key为user. *可以匹配user.z等消息。 消费者2可以匹配user 或user.save.findAll等消息。
其实我们也可以使用配置类来实现RabbitMQ, 这里拿Fanout模型举例
1.创建FanoutConfig类,然后创建队列,交换器,以及绑定队列与交换机
@Configuration
public class FanoutConfig {
//队列1
@Bean
public Queue fanoutQueue1(){
return new Queue("fanout.a");
}
//队列2
@Bean
public Queue fanoutQueue2(){
return new Queue("fanout.b");
}
//队列3
@Bean
public Queue fanoutQueue3(){
return new Queue("fanout.c");
}
//交换器
@Bean
FanoutExchange fanoutExchange(){
return new FanoutExchange("fanoutExchange");
}
//绑定交换机和队列1
@Bean
Binding bindingFanout1(){
return BindingBuilder.bind(fanoutQueue1()).to(fanoutExchange());
}
//绑定交换机和队列2
@Bean
Binding bindingFanout2(){
return BindingBuilder.bind(fanoutQueue2()).to(fanoutExchange());
}
//绑定交换机和队列3
@Bean
Binding bindingFanout3(){
return BindingBuilder.bind(fanoutQueue3()).to(fanoutExchange());
}
}
2.创建生产者
@Test
public void testFanout1(){
rabbitTemplate.convertAndSend("fanoutExchange","","这是日志广播");
}
fanoutExchange是我们在配置中,创建的交换机名字
3.创建消费者
@RabbitListener(queues = "fanout.a")
public void receive3(String message){
System.out.println("message3 = " + message);
}
@RabbitListener(queues = "fanout.b")
public void receive4(String message){
System.out.println("message4 = " + message);
}
@RabbitListener(queues = "fanout.c")
public void receive5(String message){
System.out.println("message5 = " + message);
}
queues: 绑定的在配置列中创建的队列名称。
6. MQ的应用场景
6.1 异步处理
场景说明:用户注册后,需要发注册邮件和注册短信,传统的做法有两种 1.串行的方式 2.并行的方式
串行方式:
将注册信息写入数据库后,发送注册邮件,再发送注册短信,以上三个任务全部完成后才返回给客户端。 这有一个问题是,邮件,短信并不是必须的,它只是一个通知,而这种做法让客户端等待没有必要等待的东西.
并行方式:
将注册信息写入数据库后,发送邮件的同时,发送短信,以上三个任务完成后,返回给客户端,并行的方式能提高处理的时间。
-
消息队列:
假设三个业务节点分别使用50ms,串行方式使用时间150ms,并行使用时间100ms。虽然并行已经提高的处理时间,但是,前面说过,邮件和短信对我正常的使用网站没有任何影响,客户端没有必要等着其发送完成才显示注册成功,应该是写入数据库后就返回.消息队列
: 引入消息队列后,把发送邮件,短信不是必须的业务逻辑异步处理
由此可以看出,引入消息队列后,用户的响应时间就等于写入数据库的时间+写入消息队列的时间(可以忽略不计),引入消息队列后处理后,响应时间是串行的3倍,是并行的2倍。
6.2 应用解耦
场景:双11是购物狂节,用户下单后,订单系统需要通知库存系统,传统的做法就是订单系统调用库存系统的接口.
这种做法有一个缺点:
当库存系统出现故障时,订单就会失败。 订单系统和库存系统高耦合. 引入消息队列
-
订单系统:
用户下单后,订单系统完成持久化处理,将消息写入消息队列,返回用户订单下单成功。 -
库存系统:
订阅下单的消息,获取下单消息,进行库操作。 就算库存系统出现故障,消息队列也能保证消息的可靠投递,不会导致消息丢失.
6.3 流量削峰
场景:
秒杀活动,一般会因为流量过大,导致应用挂掉,为了解决这个问题,一般在应用前端加入消息队列。
作用:
1.可以控制活动人数,超过此一定阀值的订单直接丢弃(我为什么秒杀一次都没有成功过呢^^)
2.可以缓解短时间的高流量压垮应用(应用程序按自己的最大处理能力获取订单)
1.用户的请求,服务器收到之后,首先写入消息队列,加入消息队列长度超过最大值,则直接抛弃用户请求或跳转到错误页面.
2.秒杀业务根据消息队列中的请求信息,再做后续处理.