在开始整合之前,让我们先了解下rabbitmq,先来灵魂三问,rabbitmq是什么,它解决了什么问题,如何使用它。
带着这三大问题开始我今天的主题,rabbitmq是一款消息中间件,它可以为你的应用提供通用的消息发送与接收平台,并保证消息的安全性,在谈如何使用之前,我们先抛开rabbitmq来讨论下消息队列,使用消息队列的三大核心目的:业务解耦、流量削峰、消息异步。
业务解耦: 假如公司有a、b、c、d四个系统,a系统做完处理之后要通知bcd三个系统做响应处理,没有引入消息队列的时候我们可能会使用rpc调用,这时候新的业务线e上线了,它也需要a的通知,b系统业务下线了,它不要接受a的通知,a系统的同学疯了。這时候引入消息队列,a将消息发送到交换机,其它系统自己选择是否从交换机拉取消息,这样e系统的上线和b系统的下线与a无关,完成解耦。
流量削峰: 主要目的是保护数据库不被高并发请求打死,假设系统平时流量不高,数据库每秒能处理2000个请求,但每到中午流量激增到每秒5000个请求,這时候引入消息队列,让数据库慢慢处理请求,这样数据库不会被打死,有人会担心一旦引入了消息队列是否会影响平时的性能呢?毕竟消息队列也是有时间消耗的,消息队列在没有积压的情况下,延迟都在ms级别,rabbitmq更是可达微秒级,可以忽略不计的。
消息异步:和解耦场景类似,毕竟在a系统同步通知bcd系统需要等待bcd系统处理完业务之后才会返回,a系统反馈给用户的处理等待时间是a+b+c+d,假设abcd系统处理耗时相当,用户等待时间*4,这对用户是一种伤害,将关联性不强的操作异步处理可以给用户更好的体验,值得考虑。
使用消息队列解决特定问题的同时也引入了问题,它的缺点如下三点
1、系统复杂度增加:引入了mq之后,我们要考虑消息如何不被重复消费、消息丢失怎么办、保证消息传递的顺序性等
2、系统可用性降低:引入的依赖越多,系统可用性就越低,如何保证mq不挂掉是个问题
3、数据一致性:业务解耦之后,其它系统异常,数据未被正常写入怎么办?
带着如上问题进入今天的主题,springboot如何整合rabbitmq
第一步 引入maven依赖 主要引入spring-boot-starter-amqp 可以查看springboot 官方文档
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.7.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.eqxiu</groupId>
<artifactId>eqxiu-hdh5-rmq</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>eqxiu-hdh5-rmq</name>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
第二步添加配置文件
spring.rabbitmq.listener.concurrency=2 //最小消息监听线程数
spring.rabbitmq.listener.max-concurrency=2 //最大消息监听线程数
spring.rabbitmq.listener.prefetch=2 //每次从队列拉取消息数
spring.rabbitmq.listener.simple.retry.max-attempts=5 //消息消费失败重试次数
spring.rabbitmq.listener.simple.retry.enabled=true // 开启失败重试
spring.rabbitmq.listener.simple.retry.initial-interval=2000 // 重试的时间间隔 第一次2000ms 第二次是 multiplier*2000 ,第三次是 multiplier*2000*multiplier
spring.rabbitmq.listener.simple.retry.max-interval=10000 // 重试最大时间间隔
spring.rabbitmq.listener.simple.retry.multiplier=5 // 应用于上一重试间隔的乘数
#重试次数超过上面的设置之后是否丢弃(false不丢弃时需要写相应代码将该消息加入死信队列)
spring.rabbitmq.listener.simple.default-requeue-rejected=true
第三步编写消息发送者&接受者代码, 先交代下rabbitmq的几个基本名称, 队列(queue)、交换机(exchange)、路由(routing-key), rabbitmq有四种交换机,归类入下
交换类型 | 行为 |
---|---|
直连(direct) | 绑定密钥必须与路由密钥完全匹配-不支持通配符。 |
主题(topic) | 与直接相同,但绑定密钥中允许使用通配符。“#”匹配零个或多个点分隔的单词,“ *”恰好匹配一个这样的单词。 |
扇型(fanout) | 路由和绑定键将被忽略-所有发布的消息都将进入所有绑定的队列。 |
头(header) |
交换机通过路由和队列绑定,消息发送者将消息发到交换机,由交换机推送到对应的队列,消息接收者通过监听队列来拉取消息,建议看下官网网站翻译的amqp协议部分http://rabbitmq.mr-ping.com/AMQP/AMQP_0-9-1_Model_Explained.html,接下来逐一演示用法
直连交换机
@Component
public class RabbitMqConfig {
// 创建一队列 名字叫 direct.queue
@Bean
Queue queue() {
return new Queue("direct.queue", false);
}
// 创建一交换机 名字叫 directExchange
@Bean
public DirectExchange directExchange() {
return new DirectExchange("directExchange");
}
// 将交换机和队列绑定,路由key=direct.key
@Bean
public Binding directBingding() {
return BindingBuilder.bind(queue()).to(directExchange()).with("direct.key");
}
}
// 消息发送类
@Controller
public class homeController {
@Autowired
private RabbitTemplate rabbitTemplate;
@RequestMapping(value = {"/direct"})
@ResponseBody
public void directMsg() {
rabbitTemplate.convertAndSend("directExchange", "direct.key", msg);
}
}
//消息接收类
@Component
public class Consumner {
@RabbitListener(queues = "direct.queue")
public void receiveDirect(byte[] content) {
System.out.println("direct:" + new String(content));
}
}
topic交换机
注意代码中定义了两个队列,这两个队列与同一交换机绑定但使用了不同的routing-key, queue2采用的通配的方式
所以在发送消息topocMsg() 的方法中, queue1只能接受一条消息,而queue2可以接受到两个消息,执行结果如下:
topic 1hello topic queue!1
topic.#hello topic queue!1
topic.#hello topic queue!2
@Component
public class RabbitMqConfig {
public static final String TOPIC_QUEUE1 = "topic.queue1";
public static final String TOPIC_QUEUE2 = "topic.queue2";
@Bean
Queue queue1() {
return new Queue(TOPIC_QUEUE1, true);
}
@Bean
Queue queue2() {
return new Queue(TOPIC_QUEUE2, true);
}
@Bean
public TopicExchange topicExchange() {
return new TopicExchange(topic_Exchange);
}
@Bean
public Binding topicBinding1() {
return BindingBuilder.bind(queue1()).to(topicExchange()).with("topic.key1");
}
@Bean
public Binding topicBinding2() {
return BindingBuilder.bind(queue2()).to(topicExchange()).with("topic.#");
}
}
// 消息发送类
@Controller
public class homeController {
@Autowired
private RabbitTemplate rabbitTemplate;
@RequestMapping(value = {"/topic"})
@ResponseBody
public void topocMsg() {
rabbitTemplate.convertAndSend(RabbitMqConfig.topic_Exchange, "topic.key1", msg + "1");
rabbitTemplate.convertAndSend(RabbitMqConfig.topic_Exchange, "topic.key2", msg + "2");
}
}
//消息接收类
@Component
public class Consumner {
@RabbitListener(queues = RabbitMqConfig.TOPIC_QUEUE1)
public void receiveTopic1(String content) {
System.out.println("topic 1" + content);
}
@RabbitListener(queues = RabbitMqConfig.TOPIC_QUEUE2)
public void receiveTopic2(String content) {
System.out.println("topic.#" + content);
}
}
fanout交换机
fanout又称为扇形交换机,扇形像不像大喇叭的形状?我觉得可以理解为广播模式,这里偷个懒,队列和topic公用了,fanout交换机的特点是不需要指定routing-key,所有与交换机绑定的队列都可以监听到消息
@Component
public class RabbitMqConfig {
public static final String TOPIC_QUEUE1 = "topic.queue1";
public static final String TOPIC_QUEUE2 = "topic.queue2";
@Bean
Queue queue1() {
return new Queue(TOPIC_QUEUE1, true);
}
@Bean
Queue queue2() {
return new Queue(TOPIC_QUEUE2, true);
}
@Bean
public FanoutExchange fanoutExchange() {
return new FanoutExchange(FANOUT_EXCHANGE);
}
@Bean
public Binding FanoutBinding1() {
return BindingBuilder.bind(queue1()).to(fanoutExchange());
}
@Bean
public Binding FanoutBinding2() {
return BindingBuilder.bind(queue2()).to(fanoutExchange());
}
}
// 消息发送类
@Controller
public class homeController {
@Autowired
private RabbitTemplate rabbitTemplate;
@RequestMapping(value = {"/fanout"})
@ResponseBody
public void fanoutMsg() {
rabbitTemplate.convertAndSend(RabbitMqConfig.FANOUT_EXCHANGE, "",msg + "fanout");
}
}
//消息接收类
@Component
public class Consumner {
@RabbitListener(queues = RabbitMqConfig.TOPIC_QUEUE1)
public void receiveTopic1(String content) {
System.out.println("topic 1" + content);
}
@RabbitListener(queues = RabbitMqConfig.TOPIC_QUEUE2)
public void receiveTopic2(String content) {
System.out.println("topic.#" + content);
}
}
header交换机
顾名思义,就是带有key-value形式的头信息的交换机
@Component
public class RabbitMqConfig {
public static final String HEADER_QUEUE = "header.queue";
public static final String HEADERS_EXCHANGE = "headersExchange";
@Bean
public HeadersExchange headersExchange() {
return new HeadersExchange(HEADERS_EXCHANGE);
}
@Bean
public Queue headersQueue() {
return new Queue(HEADER_QUEUE);
}
@Bean
public Binding headersBinding() {
Map<String, Object> headerKeys = new HashMap<>();
headerKeys.put("superbing", "bing");
return BindingBuilder.bind(headersQueue()).to(headersExchange()).whereAll(headerKeys).match();
}
}
// 消息发送类
@Controller
public class homeController {
@Autowired
private RabbitTemplate rabbitTemplate;
@RequestMapping(value = {"/header"})
@ResponseBody
public void headerMsg() {
MessageProperties properties = new MessageProperties();
properties.setHeader("superbing", "bing");
Message obj = new Message(msg.toString().getBytes(), properties);
rabbitTemplate.convertAndSend(RabbitMqConfig.HEADERS_EXCHANGE, "", obj);
}
}
//消息接收类
@Component
public class Consumner {
@RabbitListener(queues = RabbitMqConfig.HEADER_QUEUE)
public void receiveHeader(byte[] content) {
System.out.println("header:" + new String(content));
}
}
至此,springboot整合rabbitmq就完成了,至于前面遗留的引入mq之后系统可用性降低,复杂度增加,数据一致性问题,我也没想好,你说气人不!