第七篇:SpringBoot整合RabbitMQ

消息队列

什么是RabbitMQ

RabbitMQ是一个实现了AMQP(Advanced Message Queuing Protocol)高级消息队列协议的消息队列服务,是一个消息中间件,主要用于组件之间解耦,消息的发送者无需知道消息使用者的存在,用Erlang语言开发的。

应用场景

1. 异步:
举例:用户注册成功将用户信息写入数据库之后,需要发送注册短信、发送注册邮箱通知。
实现方式:
传统方式:串行方式和并行方式
RabbitMQ:异步方式

  • 串行方式串行方式
    将用户注册信息写入数据库之后,发送注册短信,再发送注册邮箱,最后以上三个任务完成之后再返回客户端提示注册成功。可发送注册短信和注册邮箱的步骤并不是必须的,只是一个通知,客户端没必要花时间等待,如果业务发生改变注册再加送会员、送积分等,那等待时间过长,用户体验很差。
  • 并行方式并行方式
    将用户注册信息写入数据库之后,发送注册短信,同时发送注册邮箱,缩短消耗的时间,提高了效率。
    假设三个业务节点分别使用50ms,串行方式使用时间150ms,并行使用时间100ms。虽然并性已经提高的处理时间,但是,前面说过,邮件和短信对我正常的使用网站没有任何影响,客户端没有必要等着短信、邮箱发送完成才显示注册成功,因此是写入数据库后就返回.
  • 异步方式异步方式
    引入消息队列后,把发送邮件,短信不是必须的业务逻辑异步处理,用户的响应时间就等于写入数据库的时间+写入消息队列的时间(可以忽略不计)。

2. 削峰:
举例:秒杀活动,一般会因为流量过大,导致应用挂掉,为了解决这个问题,一般在应用前端加入消息队列。
在这里插入图片描述
用户的请求,服务器收到之后,首先写入消息队列,加入消息队列长度超过最大值,则直接抛弃用户请求或跳转到错误页面。(这就是你秒杀时为啥没有成功了),再根据根据消息队列中的请求信息,再做后续处理。

3. 解耦:
举例:用户下单时,订单系统需要调用库存系统,可万一库存系统宕机了呢?(存在高耦合)
在这里插入图片描述
引入消息队列
订单系统:用户下单后,订单系统完成持久化处理,将消息写入消息队列,返回用户订单下单成功。
库存系统:订阅下单的消息,获取队列的下单消息,进行库操作。
就算库存系统出现故障,消息队列也能保证消息的可靠投递,不会导致消息丢失

缺点

1. 系统复杂性:
由于加入了消息中间件,所以需要考虑很多东西,比如:消息重复消费、消息丢失、消息的顺序消费等。

2. 数据一致性:
比如注册的用户下单成功之后,库存系统是否减少库存就不管了吗?并不是的,需要保证数据的一致性,
因此,可以通过分布式事务来解决,要么全部成功,要么全部失败。

3. 可用性:
如何保证RabbitMQ的高可用?如果RabbitMQ挂了怎么办?

相关概念

在这里插入图片描述
通常我们谈到队列服务,会有三个概念:发消息者、队列、收消息者。RabbitMQ 在这个基本概念之上,多做了一层抽象,在发消息者和队列之间加入了交换器(Exchange)。这样发消息者和队列就没有直接联系,转而变成发消息者把消息给交换器,交换器根据调度策略再把消息再给队列,消费者再从指定的队列中获取消息。
这里有一个比较重要的概念:路由键 。消息到交换机的时候,交互机会转发到对应的队列中,那么究竟转发到哪个队列,就要根据该路由键的情况,当消息携带的绑定值与交换机绑定的队列中的路由键匹配时,那么就把该消息转发到该队列(下面有详解)。

交换机(Exchange)

交换机的主要功能是接收消息并且转发到绑定的队列当中,交换机也是不存储消息的。交换机主要分为四种类型:Direct、Topic、Headers 和 Fanout 。

1. Direct Exchange

Direct Exchange 直连型交换机,是 RabbitMQ默认的交换机模式,根据消息携带的路由键投放给绑定的队列中。
一个队列绑定到一个交换机上面,并且赋予一个路由键 routing key,当一个消息发送过来的时候会携带一个绑定键 binding key,这个消息会由生产者发送给指定的交换机,交换机会根据这个绑定键 binding key去匹配路由键 routing key 对应的队列。

2. Fanout Exchange

Fanout Exchange 扇形交换机,这个没有路由键或者路由模式,即使你配置了路由键,那么它也会忽略,会直接把消息发送给绑定给它的全部队列。

3. Topic Exchange

Topic Exchange 主题交换机,它的特点是转发消息通过通配符,通常就是路由键和绑定值相匹配的时候交换机才能转发消息。
规则:

  1. *:用来表示一个单词 (必须包含)
  2. #:用来表示任意数量(零个或多个)单词
  3. 路由键必须是一串字符,用句号(.)隔开

例如:
队列Q1 绑定键为*.MM.*
队列Q2绑定键为 MM.#
如果一条消息携带的路由键为 A.MM.B,那么队列Q1将会收到;
如果一条消息携带的路由键为MM.AA.BB,那么队列Q2将会收到;

所以说:
当一个队列的绑定值为 “#” 时,那么它会无视所有消息的路由键,接收所以消息。
当一个队列的绑定值没有 “*“和”#” 的时候,那么它的功能就和Direct Exchange 直连型交换机一样。
主题交换机就有了Direct Exchange 直连型交换机和Fanout Exchange 扇形交换机的功能。

docker快速搭建RabbitMQ
  1. 拉去镜像
docker pull rabbitmq:management
  1. 运行容器,这里的命令详解不做解释,第六篇redis有详解(暗示小伙伴去看上一篇)
docker run -d -p 5672:5672 -p 15672:15672 --name rabbitmq rabbitmq:management

3.访问RabbitMQ管理界面:宿主机+端口
在这里插入图片描述
账号、密码默认为:guest

注意 RabbitMQ端口说明:

4369 -- erlang发现口

5672 --client端通信口

15672 -- 管理界面ui端口

25672 -- server间内部通信口
快速集成
  1. pom.xml 添加RabbitMQ依赖
		<!-- 引入RabbitMQ依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>
  1. application.yml 配置
#RabbitMQ配置
spring:
  rabbitmq: 
    host: 192.168.0.223
    port: 5672
    virtual-host: / #虚拟host 可以不设置,使用server默认host
    username: guest
    password: guest
通过代码实现以上模式

Direct Exchange 直连型交换机

  1. 创建 DirectRabbitConfig 配置类,定义队列、交换机,并将交换机和队列进行绑定
@Configuration
public class DirectRabbitConfig {

    /**
     * 队列 名为:testDirectQueue
     * @author 药岩
     * @date 2020/2/5
     * @param  * @param  
     * @return org.springframework.amqp.core.Queue
     */
    @Bean
    public Queue testDirectQueue(){
        return new Queue("TestDirectQueue", true);
    }

    /**
     * 交换机 名为:testDirectExchange
     * @author 药岩
     * @date 2020/2/5
     * @param  * @param  
     * @return org.springframework.amqp.core.DirectExchange
     */
    @Bean
    DirectExchange testDirectExchange(){
        return new DirectExchange("TestDirectExchange");
    }

    /**
     * 绑定 将队列和交换机绑定,并设置路由键 名为:TestDirectRouting
     * @author 药岩
     * @date 2020/2/5
     * @param  * @param  
     * @return org.springframework.amqp.core.Binding
     */
    @Bean
    Binding testBindingDirect(){
        return BindingBuilder.bind(testDirectQueue()).to(testDirectExchange()).with("TestDirectRouting");
    }
}
  1. 创建生产者 DirectSenderController
/**
 * 生产者
 * @ClassName DirectSenderController
 * @Author 药岩
 * @Date 2020/2/5
 * Version 1.0
 */

@RestController
public class DirectSenderController {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @GetMapping("/senderMessage")
    public String senderMessage(){
        String messageId = String.valueOf(UUID.randomUUID());
        String messageData = "Hi 药岩";
        String createTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
        Map<String, Object> map = new HashMap<>();
        map.put("messageId", messageId);
        map.put("messageData", messageData);
        map.put("createTime", createTime);
        // 发送消息到指定的交换器,指定的路由键,设置消息内容
        rabbitTemplate.convertAndSend("TestDirectExchange", "TestDirectRouting", map);
        return "消息发送成功!";
    }
}
  1. 运行项目,访问:http://localhost:8080/senderMessage
    在这里插入图片描述

  2. 访问RabbitMQ管理界面,查看是否推送成功
    在这里插入图片描述
    明显,推送成功了。
    再查看队列
    在这里插入图片描述

  3. 创建消费者DirectReceiver

/**
 * 消费者
 * queues = "TestDirectQueue" 监听的队列名称
 * @ClassName DirectReceiver
 * @Author 药岩
 * @Date 2020/2/5
 * Version 1.0
 */

@Component
@RabbitListener(queues = "TestDirectQueue")
public class DirectReceiver {

    /**
     * 从队列中接收执行的方法
     * @author devin zhu
     * @date 2020/3/6
     * @param  * @param map 
     * @return void
     */
    @RabbitHandler
    public void directConsumer(Map map){
        System.out.println("从队列中消费的消息:" + map.toString());
    }
}

启动项目,查看控制台中消费者在消费的消息
在这里插入图片描述
当不断访问:http://localhost:8080/senderMessage 消费者也不会不断的在消费,看控制台:
在这里插入图片描述
Fanout Exchange 扇形交换机

  1. 创建 FanoutRabbitConfig 配置类,配置三个队列、一个交换机,并且将这三个队列绑定到交换机里,扇形交换机没有路由键。
@Configuration
public class FanoutRabbitConfig {

    @Bean
    public Queue testQueueA(){
        return new Queue("FanoutA");
    }

    @Bean
    public Queue testQueueB(){
        return new Queue("FanoutB");
    }

    @Bean
    public Queue testQueueC(){
        return new Queue("FanoutC");
    }

    @Bean
    public FanoutExchange testFanoutExchange(){
        return new FanoutExchange("FanoutExchange");
    }

    @Bean
    public Binding testBindingFanoutA(){
        return BindingBuilder.bind(testQueueA()).to(testFanoutExchange());
    }

    @Bean
    public Binding testBindingFanoutB(){
        return BindingBuilder.bind(testQueueB()).to(testFanoutExchange());
    }

    @Bean
    public Binding testBindingFanoutC(){
        return BindingBuilder.bind(testQueueC()).to(testFanoutExchange());
    }
}
  1. 创建一个生产者,消息会通过交换机发送到三个队列里。
/**
 * 生产者
 * @ClassName FanoutSenderController
 * @Author 药岩
 * @Date 2020/2/5
 * Version 1.0
 */

@RestController
public class FanoutSenderController {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @GetMapping(value = "sendFanoutMessage")
    public String sendFanoutMessage(){
        String messageId = String.valueOf(UUID.randomUUID());
        String messageData = "Hi Fanout";
        String createTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
        Map<String, Object> map = new HashMap<>();
        map.put("messageId", messageId);
        map.put("messageData", messageData);
        map.put("createTime", createTime);
        // 发送消息到指定的交换机, 匹配的路由键, 消息内容
        rabbitTemplate.convertAndSend("FanoutExchange", null, map);
        return "消息发送成功";
    }
    
}
  1. 创建三个消费者,消费三个不同的队列FanoutReceiverA、FanoutReceiverB、FanoutReceiverC

@Component
@RabbitListener(queues = "FanoutA")
public class FanoutReceiverA {

    @RabbitHandler
    public void fanoutConsumer(Map message){
        System.out.println("消费了队列FanoutA:" + message);
    }
}
@Component
@RabbitListener(queues = "FanoutB")
public class FanoutReceiverB {

    @RabbitHandler
    public void fanoutConsumer(Map message){
        System.out.println("消费了队列FanoutB:" + message);
    }
}

@Component
@RabbitListener(queues = "FanoutC")
public class FanoutReceiverC {

    @RabbitHandler
    public void fanoutConsumer(Map message){
        System.out.println("消费了队列FanoutC:" + message);
    }
}
  1. 运行项目,访问:http://localhost:8080/sendFanoutMessage
    在这里插入图片描述
  2. 查看控制台打印输出
消费了队列FanoutC:{createTime=2020-03-06 19:50:11, messageId=3f2b622e-e861-4a4d-9a69-3bda618e5f37, messageData=Hi Fanout}
消费了队列FanoutA:{createTime=2020-03-06 19:50:11, messageId=3f2b622e-e861-4a4d-9a69-3bda618e5f37, messageData=Hi Fanout}
消费了队列FanoutB:{createTime=2020-03-06 19:50:11, messageId=3f2b622e-e861-4a4d-9a69-3bda618e5f37, messageData=Hi Fanout}

Topic Exchange 主题交换机
7. 创建 TopicRabbitConfig 主题交换机,设置队列、交换机,并且进行绑定。

/**
 * 主题交换机
 * @ClassName TopicRabbitConfig
 * @Author 药岩
 * @Date 2020/2/5
 * Version 1.0
 */

@Configuration
public class TopicRabbitConfig {

    private final static String boy = "topic.boy";
    private final static String girl = "topic.girl";

    @Bean
    public Queue testTopicQueue(){
        return new Queue(boy);
    }

    @Bean
    public Queue testTopic2Queue(){
        return new Queue(girl);
    }

    /**
     * 主题交换机 名为:topicExchange
     * @author 药岩
     * @date 2020/2/5
     * @param  * @param  
     * @return org.springframework.amqp.core.TopicExchange
     */
    @Bean
    public TopicExchange topicExchange(){
        return new TopicExchange("topicExchange");
    }

    /**
     * 将testTopicQueue队列和topicExchange交换机绑定,路由键为topic.boy
     * 当消息携带的路由键值为topic.boy才能分发到该队列
     * @author 药岩
     * @date 2020/2/5
     * @param  * @param  
     * @return org.springframework.amqp.core.Binding
     */
    @Bean
    public Binding bindingTopicExchange(){
        return BindingBuilder.bind(testTopicQueue()).to(topicExchange()).with(boy);
    }

    /**
     * 将testTopic2Queue队列和topicExchange交换机绑定,路由键为topic.#
     * 当消息携带的路由键值为topic.开头的就能分发到该队列了
     * @author 药岩
     * @date 2020/2/5
     * @param  * @param  
     * @return org.springframework.amqp.core.Binding
     */
    @Bean
    public Binding bindingTopicExchange2(){
        return BindingBuilder.bind(testTopic2Queue()).to(topicExchange()).with("topic.#");
    }

}
  1. 创建 TopicSenderController 生产者
@RestController
public class TopicSenderController {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @GetMapping(value = "sendTopicMessage")
    public String sendTopicMessage(){
        String messageId = String.valueOf(UUID.randomUUID());
        String messageData = "Hi boy";
        String createTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
        Map<String, Object> map = new HashMap<>();
        map.put("messageId", messageId);
        map.put("messageData", messageData);
        map.put("createTime", createTime);
        // 发送消息到指定的交换机, 携带的路由值, 消息内容
        rabbitTemplate.convertAndSend("topicExchange", "topic.boy", map);
        return "消息发送成功";
    }

    @GetMapping(value = "sendTopicMessage2")
    public String sendTopicMessage2(){
        String messageId = String.valueOf(UUID.randomUUID());
        String messageData = "Hi girl";
        String createTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
        Map<String, Object> map = new HashMap<>();
        map.put("messageId", messageId);
        map.put("messageData", messageData);
        map.put("createTime", createTime);
        // 发送消息到指定的交换机, 携带的路由值, 消息内容
        rabbitTemplate.convertAndSend("topicExchange", "topic.girl", map);
        return "消息发送成功";
    }

}
  1. 运行项目,访问:http://localhost:8080/sendTopicMessage 和 http://localhost:8080/sendTopicMessage2
    查看 RabbitMQ 管理界面,可以看出有三条未被消费的消息
    在这里插入图片描述
    查看队列,topic.boy 队列只有一条消息,因为该队列的路由键是 topic.boy 所以只能匹配消息携带的路由值的一条消息, topic.girl 队列有两条消息,因为该队列的路由键是 topic.# 这种匹配规则,只要消息携带的路由值的是topic.开头,所以匹配了两条消息。
    在这里插入图片描述
  2. 创建两个消费者监听两个队列,先创建 TopicBoyReceiver
/**
 * 消费者
 * 监听 topic.boy 队列的数据,当监听到队列里面有消息时,会交给topicConsumer方法去处理
 * 所以 @RabbitListener 可以标注在类上面,需配合 @RabbitHandler 注解一起使用
 * @ClassName TopicReceiver
 * @Author 药岩
 * @Date 2020/2/5
 * Version 1.0
 */

@Component
@RabbitListener(queues = "topic.boy")
public class TopicBoyReceiver {

    @RabbitHandler
    public void topicConsumer(Map message){
        System.out.println("消费了队列名为 topic.boy 队列的数据" + message.toString());
    }
}

再创建 TopicGirlReceiver

/**
 * 消费者
 * 监听 topic.girl 队列的数据,当监听到队列里面有消息时,会交给topicConsumer方法去处理
 * 所以 @RabbitListener 可以标注在类上面,需配合 @RabbitHandler 注解一起使用
 * @ClassName TopicReceiver
 * @Author 药岩
 * @Date 2020/2/5
 * Version 1.0
 */

@Component
@RabbitListener(queues = "topic.girl")
public class TopicGirlReceiver {

    @RabbitHandler
    public void topicComsumer(Map message){
        System.out.println("消费了队列名为 topic.girl 的消息:" + message.toString());
    }
}
  1. 启动项目,控制台打印输出
消费了队列名为 topic.boy 队列的数据{createTime=2020-03-06 16:52:53, messageId=ce6b6ebf-a932-4e09-9a94-4748a4a9f796, messageData=Hi boy}
消费了队列名为 topic.girl 的消息:{createTime=2020-03-06 16:52:53, messageId=ce6b6ebf-a932-4e09-9a94-4748a4a9f796, messageData=Hi boy}
消费了队列名为 topic.girl 的消息:{createTime=2020-03-06 16:54:27, messageId=726b138b-620a-4487-8c82-1bbed486e327, messageData=Hi girl}
发布了29 篇原创文章 · 获赞 43 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/CSDN_Qiang17/article/details/104676434