开始语
一位普通的程序员,慢慢在努力变强!
开始学习rabbitmq的猿友想必已经对java的springboot、cloud等框架模式已经了解、使用比较成熟了,这里就省略创建springboot项目的步骤了!
1.pom添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
2.yml配置mq
spring:
application:
name: springboot-rabbitmq
# 配置rabbit连接 (填写你自己的mq地址:最好本机IP,而不是localhost)
rabbitmq:
host: tianyu.com.cn
port: 5672
username: guest
password: guest
# 环境隔离,默认使用“/”(虚拟主机)
virtual-host: /
connection-timeout: 2000000
3.mq模式的了解和学习
3.0 User对象创建
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* @author 猿仁
* @date 2023-01-31 09:38
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable {
private String name;
}
3.1 基础普通模式(模式一)
简介:
从上图可以看出,这个模式比较单一,一进一出,P是生产者,中间红色部分是消息缓冲区域(通道),C是消费者。
生产者也就是我们的发送者,将消息发送到消息缓冲区中,等待消费者接收消费。
3.1.1 生产者
温馨提示:需要到测试单元类中进行测试
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 第一种模型:点对点
*/
@Test
public void helloWordTest() {
// 字符串
rabbitTemplate.convertAndSend("hello","字符串模型!");
// 对象
rabbitTemplate.convertAndSend("hello",new User("对象模型"));
}
3.1.2 消费者
import com.net.entity.User;
import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.messaging.handler.annotation.Headers;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.Map;
/**
* 消费者监听 (第一种模型:点对点)
* 默认创建的是:持久化队列
*
* @author 猿仁
* @Queue(value = "队列名",durable = "是否持久化(默认持久【true】针对宕机等情况)",autoDelete = "是否自动删除(默认不删除【false】)")
* @data 2023-01-31 09:38
*/
@Component
@Slf4j
@RabbitListener(queuesToDeclare = @Queue(value = "hello", durable = "false", autoDelete = "false"))
public class HelloCustomer {
/**
* 消费者对象模型1
*
* @param data Body响应内容
* @param headers 请求头
* @param channel 通道
* @param message 消息
* @return void
* @author 猿仁
* @date 2023/1/31 9:38
*/
@RabbitHandler
public void helloWord1(@Payload User data, @Headers Map<String, Object> headers, Channel channel, Message message) throws IOException, NoSuchMethodException {
log.info("\nhelloWord1获取到了通道[hello]的数据!");
log.info("Payload: {}" , data);
log.info("Headers: {}" , headers);
log.info("Channel: {}" , channel);
log.info("Message: {}" , message);
}
/**
* 消费者字符串模型2
*
* @param data Body响应内容
* @param headers 请求头
* @param channel 通道
* @param message 消息
* @return void
* @author 猿仁
* @date 2023/1/31 9:38
*/
@RabbitHandler
public void helloWord2(@Payload String data, @Headers Map<String, Object> headers, Channel channel, Message message) throws IOException, NoSuchMethodException {
log.info("\nhelloWord2获取到了通道[hello]的数据!");
log.info("Payload: {}" , data);
log.info("Headers: {}" , headers);
log.info("Channel: {}" , channel);
log.info("Message: {}" , message);
}
}
3.1.3 结果验证
3.2 工作模式(模式二)
简介:
从上图可以看出,此模式为一对多,P是生产者,中间红色部分是消息缓冲区域(通道),C1、C2是消费者。
生产者也就是我们的发送者,将消息发送到消息缓冲区中,等待消费者接收消费。
这个和3.1模式不同的点就是多了一个消费者,大大的增强了消费能力, 当消息发送频率很高,导致队列中的消息累积、阻塞,增加消费者来实现多消费模式是一种简单的解决方案!
3.2.1 生产者
@Autowired
private RabbitTemplate rabbitTemplate;
@Autowired
private WorkCustomer workCustomer;
/**
* 第二种模型:一对多
*/
@Test
public void workTest() throws InterruptedException {
for (int i = 1; i <= 90; i++) {
rabbitTemplate.convertAndSend("work", "send 第" + i + "条消息!");
}
Thread.sleep(100000);
System.out.println("work1Count = "+workCustomer.work1Count);
System.out.println("work2Count = "+workCustomer.work2Count);
System.out.println("work3Count = "+workCustomer.work3Count);
}
3.2.2 消费者
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
/**
* 消费者监听 (第二种模型:一对多)
*
* @author 猿仁
* @data 2023-01-31 09:38
*/
@Component
@Slf4j
public class WorkCustomer {
// 记录消费者消费的数量
public int work1Count = 0;
public int work2Count = 0;
public int work3Count = 0;
/**
* 消费者1
*
* @param data Body响应内容
* @return void
* @author 猿仁
* @date 2023/1/31 9:38
*/
@RabbitListener(queuesToDeclare = @Queue("work"))
public void work1(String data) {
log.info("\nwork1获取到了通道[work]的数据!{}", data);
log.info("Payload: {}", data);
++work1Count;
}
/**
* 消费者2
*
* @param data Body响应内容
* @return void
* @author 猿仁
* @date 2023/1/31 9:38
*/
@RabbitListener(queuesToDeclare = @Queue("work"))
public void work2(String data) {
log.info("\nwork2获取到了通道[work]的数据!{}", data);
log.info("Payload: {}", data);
++work2Count;
}
/**
* 消费者2
*
* @param data Body响应内容
* @return void
* @author 猿仁
* @date 2023/1/31 9:38
*/
@RabbitListener(queuesToDeclare = @Queue("work"))
public void work3(String data) throws InterruptedException {
log.info("\nwork3获取到了通道[work]的数据!{}", data);
log.info("Payload: {}", data);
// 在现实中,可能消费水平、能力都不一样,有的快有的慢,这么模拟一个慢的,睡2秒
Thread.sleep(2000);
++work3Count;
}
}
3.2.3 结果验证
3.2.4 结果分析
官网的描述:默认是轮训,每个消费者得到的数量是一致的,消费慢的也会接收消息,只是在等待被指定的消费者慢慢消费!
如果出现3个消费者,只有2个消息的时候,这个时候就是轮训给,给到谁谁就消费,和消费者的消费水平无关
3.2.5 限流模式 prefetch
# application.yml配置,生产者、消费者都不动,添加完监听者配置直接运行查看结果
spring:
application:
name: springboot-rabbitmq
# 配置rabbit连接 (填写你自己的mq地址:最好本机IP,而不是localhost)
rabbitmq:
host: tianyu.com.cn
port: 5672
username: guest
password: guest
# 环境隔离,默认使用“/”(虚拟主机)
virtual-host: /
connection-timeout: 2000000
# 开启监听者配置
listener:
simple:
# 默认是轮训,平均分配,如果配置此选项,那么将有一个消费者达到指定的条数2,将不会再接收消息了。限流。也可以在监听者类中配置
prefetch: 2
3.3 交换机模式理解
交换机种类如下(4种):
直联交换机(direct):Direct是RabbitMQ默认的交换机模式,先匹配, 再投送。创建消息队列的时候需要绑定一个key,可以理解为是一个路由key,根据交换机所绑定的路由key指向的对应的队列中(消息存放)
主题交换机(topic):这个可以理解为是direct的升级版,在Routing key上做了类似于正则表达式的路由规则
*号: 代表一个单词
#号: 代表零个或者多个单词
标头交换机(headers ):类似于在交换机的Headers头部配置一个k,v这样的键值对,消息将根据键值对来路由到对应的队列中,通过获取消息头部来进行完全匹配。(显示开发中基本不用)
扇出交换机(fanout):转发消息是最快的,以广播的形式将消息发送到绑定的队列上,没有key的概念。
温馨提示:多消费者,就是多加几个队列! 除了fanout模式,其他三种模式多几个消费者符合路由规则的消费者都会接收到消息,而不是像fanout一个消息只由一个消费者消费
3.4 订阅发布者模式(模式三)
简介:
从上图可以看出,这个模式比上2个复杂了一点,P生产者,X为交换机,红色部分为队列,C1,C2为消费者.
生产者先绑定交换机,然后交换机bing绑定了队列,将消息发送到指定的队列中,然后有由不同的消费者对应队列进行消费(消息分发)
3.4.1 生产者
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 第三种模型:广播模式:fanout(消息分发,每个绑定交换机的队列都会收到消息)
*/
@Test
public void fanoutTest() {
for (int i = 1; i <= 3; i++) {
rabbitTemplate.convertAndSend("logs", "", "fanout send 【模型三】 " + i);
}
}
3.4.2 消费者
import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.ExchangeTypes;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.*;
import org.springframework.stereotype.Component;
import java.io.IOException;
/**
* 第三种模型
*/
@Component
@Slf4j
public class FanOutCustomer {
@RabbitListener(bindings = {
@QueueBinding(
value = @Queue,//绑定临时队列
exchange = @Exchange(value = "logs", type = ExchangeTypes.FANOUT) //绑定交换机
)
})
public void fanout1(String data, Channel channel, Message message) throws IOException {
log.info("【fanout1 execute consumption:】" + data + "消费成功!");
}
@RabbitListener(bindings = {
@QueueBinding(
value = @Queue,//绑定临时队列,可以给名称,那么就不是临时队列
exchange = @Exchange(value = "logs", type = ExchangeTypes.FANOUT) //绑定交换机
)
})
public void fanout2(String data, Channel channel, Message message) throws IOException {
log.info("【fanout2 execute consumption:】" + data + "消费成功!");
}
@RabbitListener(bindings = {
@QueueBinding(
value = @Queue(name = ExchangeTypes.FANOUT + "_consumer3", durable = "true"), //绑定队列,可以给名称,那么就不是临时队列
exchange = @Exchange(value = "logs", type = ExchangeTypes.FANOUT) //绑定交换机
)
})
public void fanout3(String data) throws IOException {
log.info("【fanout3 execute consumption:】" + data + "消费成功!");
}
}
3.4.3 结果验证
3.5 路由模式(模式四)
简介:
从上图可以看出,P是生产者,X是交换机,中间红色部分是消息缓冲区域(通道),C1、C2是消费者。
可以看出这个模式和订阅模式比较像,唯一的区别是存在一个路由规则,可以理解为交换机和队列之间是存在一个 【标识】进行连接,如果标识对不上,那么队列将接收不到消息。
3.5.1 生产者
@Autowired
private RouteCustomer routeCustomer;
/**
* 第四种模式:路由模式
*/
@Test
public void routeTest() throws InterruptedException {
User user = new User("info你好!");
rabbitTemplate.convertAndSend("route_direct", "info", user);
// error消费出现异常,会造成死循环,
// 解决次问题:1.直接吃掉异常,不要抛出,记录日志或者存库,2.开启消费者重试模式,限定次数
user = new User("error你好!");
rabbitTemplate.convertAndSend("route_direct", "error", user);
user = new User("json你好!");
rabbitTemplate.convertAndSend("route_direct", "json", user);
user = new User("debug你好!");
rabbitTemplate.convertAndSend("route_direct", "debug", user);
user = new User("warn你好!");
rabbitTemplate.convertAndSend("route_direct", "warn", user);
Thread.sleep(16000);
System.out.println("一共执行次数:"+routeCustomer.errorCount);
System.out.println("执行次数明细:"+routeCustomer.errorCountDetail);
}
3.5.2 消费者
import com.net.entity.User;
import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.ExchangeTypes;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.*;
import org.springframework.messaging.handler.annotation.Headers;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.stereotype.Component;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* 第四种模型:路由模式
*/
@Component
@Slf4j
public class RouteCustomer {
private static final String INFO = "info";
private static final String DEBUG = "debug";
private static final String ERROR = "error";
private static final String JSON = "json";
public Map<Integer, String> errorCountDetail = new HashMap<>();
public int errorCount = 0;
@RabbitListener(bindings = {@QueueBinding(value = @Queue(name = RouteCustomer.ERROR + "_queue"),//绑定临时队列,可以给名称,那么就不是临时队列
exchange = @Exchange(value = "route_direct", type = ExchangeTypes.DIRECT),//绑定交换机名称和类型(默认是direct类类型)
key = RouteCustomer.ERROR)})
public void error(User user, Channel channel, Message message, @Headers Map<String, Object> headers) {
log.info("交换机模式【direct】,key【error】,开始消费:{},Channel:{}, Message:{}, Headers :{}", user, channel, message, headers);
//try {
// int a = 0 / 0;
//}catch (Exception e) {
// System.out.println(e.getMessage());
// // 不抛出异常,吃掉异常,记录日志
// log.error("交换机模式【direct】,key【error】,消费异常:{},消息体为:{}",e.getMessage(), user);
// // 第一种吃掉异常:throw e;
//}
// 第二种限定重试次数为:3,抛出异常
try {
int a = 0 / 0;
} catch (Exception e) {
System.out.println(e.getMessage());
// 不抛出异常,吃掉异常,记录日志
log.error("交换机模式【direct】,key【error】,消费异常:{},消息体为:{}", e.getMessage(), user);
++errorCount;
errorCountDetail.put(errorCount, new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
throw e;
}
}
@RabbitListener(bindings = {@QueueBinding(value = @Queue,//绑定临时队列,可以给名称,那么就不是临时队列
exchange = @Exchange(value = "route_direct"),//绑定交换机名称和类型(默认是direct类类型)
key = RouteCustomer.DEBUG)})
public void debug(User user) {
log.info("交换机模式【direct】,key【debug】,开始消费:{}", user);
}
@RabbitListener(bindings = {@QueueBinding(value = @Queue,//绑定临时队列,可以给名称,那么就不是临时队列
exchange = @Exchange(value = "route_direct"),//绑定交换机名称和类型(默认是direct类类型)
key = RouteCustomer.JSON)})
public void json(@Payload User user) {
log.info("交换机模式【direct】,key【json】,开始消费:{}", user);
}
@RabbitListener(bindings = {@QueueBinding(value = @Queue,//绑定临时队列,可以给名称,那么就不是临时队列
exchange = @Exchange(value = "route_direct"),//绑定交换机名称和类型(默认是direct类类型)
key = RouteCustomer.INFO)})
public void info(@Payload User user) {
log.info("交换机模式【direct】,key【info】,开始消费:{}", user);
}
}
3.5.3 结果验证
3.6 主题模式(模式五)
简介:
从上图可以看出,P是生产者,X是交换机,单词部分是路由规则key,中间红色部分是消息缓冲区域(通道),C1、C2是消费者。
可以看出这个模式和路由模式是比较相似的,不同点是他的key,路由的规则,这里的路由规则是表达式(有点像是正则),官网也说明了。
*号代表一个单词,#号代号零个或多个单词
分析图中的路由规则:
生产者的路由key为:abc.orange.bcd 这个时候消息将分发到Q1, 因为Q1的匹配规则是:单词1个.orange.单词1个
生产者的路由key为:abc.bcd.rabbit 这个时候消息将分发到Q2, 因为Q2的匹配规则是:单词1个.单词1个.rabbit
生产者的路由key为:lazy 或者 lazy.abc 或者 lazy.bcd 或者 lazy.abc.bcd 这个时候消息将分发到Q2, 因为Q2的匹配规则是:lazy.单词(0个 或者 多个)
3.6.1 生产者
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 第五种:主题模式
*/
@Test
public void topicTest() {
rabbitTemplate.convertAndSend("topic", "user.1", "生产者 路由Key【user.1】 message");
rabbitTemplate.convertAndSend("topic", "user.1.2", "生产者 路由Key【user.1.2】 message");
rabbitTemplate.convertAndSend("topic", "user.admin.abc", "生产者 路由Key【user.admin.abc】 message");
rabbitTemplate.convertAndSend("topic", "user.admin.1.2", "生产者 路由Key【user.admin.1.2】 message");
rabbitTemplate.convertAndSend("topic", "user.admin", "生产者 路由Key【user.admin】 message");
rabbitTemplate.convertAndSend("topic", "user.ordinary.1.2", "生产者 路由Key【user.ordinary.1.2】 message");
rabbitTemplate.convertAndSend("topic", "user.ordinary.1", "生产者 路由Key【user.ordinary.1】 message");
}
3.6.2 消费者
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.ExchangeTypes;
import org.springframework.amqp.rabbit.annotation.Exchange;
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.QueueBinding;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
/**
* 第五种模型: 主题模式
*/
@Component
@Slf4j
public class TopicCustomer {
@RabbitListener(bindings = {
@QueueBinding(
value = @Queue,//绑定临时队列
exchange = @Exchange(value = "topic", type = ExchangeTypes.TOPIC),//绑定交换机
key = "user.*"
)
})
public void topic1(String message) {
log.info("topic1的key为:[user.*] 开始消费:{}" , message);
}
@RabbitListener(bindings = {
@QueueBinding(
value = @Queue,//绑定临时队列,可以给名称,那么就不是临时队列
exchange = @Exchange(value = "topic", type = ExchangeTypes.TOPIC),//绑定交换机
key = "user.#"
)
})
public void topic2(String message) {
log.info("topic2的key为:[user.#] 开始消费:{}" , message);
}
@RabbitListener(bindings = {
@QueueBinding(
value = @Queue,//绑定临时队列,可以给名称,那么就不是临时队列
exchange = @Exchange(value = "topic", type = ExchangeTypes.TOPIC),//绑定交换机
key = { "user.#", "user.*" } // 这个可以为数组,可以是多个key
)
})
public void topic3(String message) {
log.info("topic3的key为:[user.# , user.*] 开始消费:{}" , message);
}
@RabbitListener(bindings = {
@QueueBinding(
value = @Queue,//绑定临时队列,可以给名称,那么就不是临时队列
exchange = @Exchange(value = "topic", type = ExchangeTypes.TOPIC),//绑定交换机
key = { "user.admin.#", "user.*.*", "user.ordinary.*"} // 这个可以为数组,可以是多个key
)
})
public void topic4(String message) {
log.info("topic4的key为:[user.admin.#, user.*.*, user.ordinary.*] 开始消费:{}" , message);
}
@RabbitListener(bindings = {
@QueueBinding(
value = @Queue,//绑定临时队列,可以给名称,那么就不是临时队列
exchange = @Exchange(value = "topic", type = ExchangeTypes.TOPIC),//绑定交换机
key = "user.ordinary.*.*" // 这个可以为数组,可以是多个key
)
})
public void topic5(String message) {
log.info("topic5的key为:[user.ordinary.*.*] 开始消费:{}" , message);
}
3.6.3 结果验证
3.6.4 分析
根据简介的路由规则而分析:
具体分析生产者user.admin这个路由key:从上图查看有四个消费者,如下:
user.*
user.admin.#, user.., user.ordinary.*
user.# , user.*
user.#
分析:
user.admin是两个单词
的可以是user.
#的可以是user.# 、 user.admin.#
可以看出结果都是符合路由匹配规则的,验证了简介中说明的*、#号的路由规则
疑问,你这边没有测试#为0的啊,user能不能匹配呢,好的,下面给予测试结果:
从图中可以看出,user.#确实是匹配了0 || n的匹配规则,而*就不行必须是要有1个单词完全匹配才行
结束语
本章节完成了,各位正在努力的程序员们,如果你们觉得本文章对您有用的话,或者是你学到了一些东西,期待您用漂亮,帅气的小手点个赞+关注,支持一下猿仁!
持续更新中…