11.spring消息队列
1. RabbitMq
docker安装
- docker pull rabbitmq:management # 带管理界面, 管理界面在15672端口
- docker pull rabbitmq #5672 端口
docker-compose
rabbitMq:
image: rabbitmq:management
container_name: rabbitMq
ports:
- "5672:5672"
- "15672:15672"
volumes:
- /home/young/app/rabbitmq/data:/var/lib/rabbitmq
docker启动后
- 访问
http://127.0.0.1:15672 - 帐号/密码: guest
- 在admin-> users 处添加用户或修改密码,修改后重新登录, 不要在弹出的框里输入
- 在admin菜单添加虚拟主机Virtual Host
1.1. 配置
依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
配置文件
spring.rabbitmq.host=127.0.0.1
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=abc123
spring.rabbitmq.virtualHost=rabbitMqHost1 #虚拟主机名字
配置类
在发送对象时,对象会被序列化成字节数组,若要反序列化对象,需要自定义 MessageConverter
在config目录创建RabbitMQConfig
下面提供了两种方式
- 基于 默认的SimpleMessageConverter
- 基于 Jackson
- 生产者需要配置 rabbitTemplate.setMessageConverter(new Jackson2JsonMessageConverter());
@Configuration
public class RabbitMQConfig {
// 1. 基于 默认的SimpleMessageConverter
@Bean
public RabbitListenerContainerFactory<?> rabbitListenerContainerFactory(ConnectionFactory connectionFactory){
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
factory.setConnectionFactory(connectionFactory);
factory.setMessageConverter(new MessageConverter() {
@Override
public Message toMessage(Object object, MessageProperties messageProperties) throws MessageConversionException {
return null;
}
@Override
public Object fromMessage(Message message) throws MessageConversionException {
try(ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(message.getBody()))){
return (User)ois.readObject();
}catch (Exception e){
e.printStackTrace();
return null;
}
}
});
return factory;
}
// 2.使用 Jackson 序列化器
@Bean
public RabbitListenerContainerFactory<?> rabbitListenerContainerFactory(ConnectionFactory connectionFactory){
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
factory.setConnectionFactory(connectionFactory);
factory.setMessageConverter(new Jackson2JsonMessageConverter());
return factory;
}
}
1.2. 结构
1.2.1. 生产者
- 注入
- @Autowired
private RabbitTemplate rabbitTemplate;
- @Autowired
- 发送
- 无交换机
- rabbitTemplate.convertAndSend("routingKey","消息内容")
- rabbitTemplate.convertAndSend("消费者name","消息内容")
- 有交换机
- rabbitTemplate.convertAndSend("exchange","routingKey","消息内容")
- rabbitTemplate.convertAndSend("交换机name","消费者name","消息内容")
- 无交换机
- 序列化
- 设置序列化器
- rabbitTemplate.setMessageConverter(new Jackson2JsonMessageConverter());
- 设置序列化器
1.2.2. 消费者
注解
- RabbitHandler
- 用在方法上,加了这个注解就成为了消费者
- RabbitListener 配置消费者参数,可用在类或方法上
- queues 监听的队列名称
- queuesToDeclare 定义消费者名称
- queuesToDeclare= @Queue(name = "消费者name") // routingKey name= 可省略
- exchange 交换机
- = @Exchange(value = "topic.exchange",durable = "true",type = "topic")
参数注解
- = @Exchange(value = "topic.exchange",durable = "true",type = "topic")
- Payload
- 请求体
- public void processMessage1(@Payload String body) {
- Headers
- 请求头
- public void processMessage1(@Headers Map<String,Object> headers) {
- 单个 @Header String token
1.2.3. broker
创建队列
- 队列名字 写在Durable或nonDurable里
private final static String QUEUE1="queue1";
@Bean(QUEUE1)
public Queue queue1() {
return QueueBuilder
.nonDurable(QUEUE1) //不持久化,重启时不保存该队列,注意队列名称在这里或下面定义
//.Durable("或者队列名称填这里") //持久化, 队列和消息会写入磁盘
.expires(60*1000) //队列存活时间,单位毫秒, 超过该时间删除队列
.ttl(30*1000) //队列中消息的存活时间,超过该时间从队列删除
.autoDelete() //没有消费者连接时,自动删除该队列
.exclusive() // 将队列与第一个连接它的消费者绑定,只传递消息给该消费者
.maxPriority(10) //最高优先级
.maxLength(10) // 最大消息数
.maxLengthBytes(1024) //每条消息最大字节数
.deadLetterExchange("deadExchange") // 关联死信交换机
.deadLetterRoutingKey("deadQueue") //关联死信队列
.build();
}
创建交换机
- 交换机类型
- topicExchange , directExchange , fanoutExchange , headersExchange
@Bean("exchange1")
public Exchange exchange1(){
return ExchangeBuilder
.topicExchange("topicExchange1") // topic交换机,名字topicExchange1
.delayed() // 创建延迟交换机
// .autoDelete() // 自动删除
// .durable(false) // 缺省持久化,指定false非持久化
.build();
}
绑定队列
- 形参 queue2, exchange1必须对应创建队列和交换机时Bean里指定的名字,
- 上面定义了 常量 QUEUE1="queue1", 这里要用queue1,而不能用QUEUE1
- bind 队列
- to 交换机
- with 设置路由规则
@Bean
public Binding binding2(Queue queue1,Exchange exchange1 ) {
return BindingBuilder
.bind(queue1)
.to(exchange1)
.with("#.mail").noargs();
}
1.3. 消息收发
生产者 -> 交换机 -> 队列 -> 消费者
- 交换机和他的队列组成一个虚拟主机, 虚拟主机在mq的服务器上
- 简单模式和工作队列模式不涉及交换机
1.3.1. 简单模式
- 只有一个生产者,一个消费者,不用定义交换机和队列
- 消费者定义自己的路由键,生产者发送消息时指定消费者的路由键
创建消费者类 TestConsumer.java
- 被 @RabbitHandler 注解的方法,接收的参数即是消息内容
- @RabbitListener, 定义消费者名字,交换机/队列等
@Slf4j
@Component
public class TestConsumer{
@RabbitHandler
@RabbitListener(queuesToDeclare= @Queue("消费者name))
public void process(String msg){ //接收消息
log.debug("消费者接收到消息内容:{}",msg)
}
}
创建生产者类 TestPubliser.java
- 主要就这一句,发送消息给指定的消费者rabbitTemplate.convertAndSend("消费者","消息")
@Component
public class TestPubliser{
@Autowired
private RabbitTemplate rabbitTemplate;
public void publiser(){
rabbitTemplate.convertAndSend("消费者name","消息内容")
}
}
测试类
建个控制类,在控制层测吧,下面这样直接测,报null了
- 调用方法要抛出异常
@SpringBootTest
public class RabbitTest
@Autowired
private TestPubliser tPubliser;
@Test
public void test() throws InterruptedException {
tPubliser.publiser();
}
传参为实体类
新建一个实体类如 User.java
- 需要实现Serializable接口
public class User implements Serializable{}
消费者接收参数的类型和生产者发送的消息类型都改为User
1.3.2. 工作队列模式
- 一个生产者,多个消费者,不涉及交换机
- 多个消费者轮流从队列读取消息
- 跟一对一模式相比,仅仅是创建多个消费者,并起同样的名字
流程
- 新建配置类
- 创建交换机
- 创建队列
- 绑定交换机和队列
- 创建消费者,分别监听各自的队列
- 创建生产者
1.3.3. Direct路由模式
路由键完全匹配
1.3.4. Topic 发布订阅模式
路由键通配符匹配
- 生产者端定义交换机和队列, 并进行绑定, 绑定时设置路由键
- 发送消息时生产者指定交换机和路由键,根据路由键到达指定队列
- 消费者监听队列
通配符
* 匹配一个词(有且只有一个)
*.abc
ab.abc 匹配
a.b.abc 不匹配
# 0或多个词
定义交换机,队列,在消费者config目录创建 RabbitMqConfig1
import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RabbitMqConfig1 {
//1.1 定义交换机
@Bean
public Exchange exchange1(){
return ExchangeBuilder
.topicExchange("topicExchange1") // topic交换机,名字topicExchange1
.durable(true) // durable=true,持久化
.build();
}
//2.1 定义队列1
@Bean("queue1") //用于绑定时的传参
public Queue queue1(){
return QueueBuilder
.durable(true) //持久化
// .ttl(60*1000) // 该队列内消息的存活时间,单位毫秒, 超过该时间,消息将从队列删除
.build();
}
//2.2 定义队列2
@Bean("queue2")
public Queue queue2(){
Queue queue = new Queue("queue");
// queue.
return queue;
}
/**3.1 定义绑定关系
*
* @param queue1 入参要和创建时的Bean 里的名字一致
* @param e1
* @return
*/
@Bean
public Binding binding1(Queue queue1, Exchange e1 ) {
return BindingBuilder
.bind(queue1).to(e1).with("msg.*") // 绑定关系,queue1,交换机e1,路由key:msg.*
.noargs();
}
@Bean
public Binding binding2(Queue queue2,Exchange e1 ) {
return BindingBuilder
.bind(queue2).to(e1).with("#.mail").noargs();
}
}
创建消费者
@Component
@RabbitListener(queues = "queue1") //要监听的队列
public class TopicConsumer {
@RabbitHandler //处理方法,根据传参确定处理方法
public void receive(String msg) {
System.out.println("TopicConsumer 接收到消息:" + msg);
}
@RabbitHandler
public void receive(Map msg) {
System.out.println("TopicConsumer 接收到消息:" + msg);
}
}
定义生产者
@GetMapping("/pub/{word}")
public ResultBody pub1(@PathVariable String word){
rabbitTemplate.convertAndSend("exchange1",word+".mail","hello msg "+word);
// 交换机名称, 路由键, 消息内容
return ResultBody.success();
}
@GetMapping("/pub2")
public ResultBody pub2(@RequestParam String word){
rabbitTemplate.convertAndSend("exchange1","msg."+word,"hello msg "+word);
return ResultBody.success();
}
1.3.5. Fanout 广播模式
广播,发给交换机绑定的所有队列
1.3.6. headers
1.3.7. 死信队列
当配置了死信队列时,被删除的消息都会进入死信队列,如:
- 普通队列已满,新来的消息进入死信队列
- 消息超过存活时间
- 拒签并且不重回队列
流程
- 创建死信交换机
- 创建死信队列
- 绑定1,2
- 创建交换机
- 创建队列,并关联死信交换机、队列
- 绑定4,5
//0.1 死信交换机
@Bean("deadExchange")
public Exchange deathExchange() {
return ExchangeBuilder
.topicExchange("deadExchange")
.durable(true)
.build();
}
//0.2 死信队列
@Bean("deadQueue")
public Queue deadQueue() {
return QueueBuilder
.durable()
.build();
}
// 0.3 绑定死信组,
@Bean
public Binding bindDead(Exchange deadExchange,Queue deadQueue){
return BindingBuilder
.bind(deadQueue)
.to(deadExchange)
.with("dead.#") // 接收所有dead开头的消息
.noargs();
}
//1.1 定义交换机
@Bean
public Exchange exchange1(){
return ExchangeBuilder
.topicExchange("topicExchange1") // topic交换机,名字topicExchange1
.durable(true) // durable=true,持久化
.build();
}
// 1.2 定义队列三,关联死信队列
@Bean("queue3")
public Queue queue3() {
return QueueBuilder
.durable()
.deadLetterExchange("deadExchange") // 关联死信交换机
.deadLetterRoutingKey("deadQueue") //关联死信队列
.build();
}
// 1.3 绑定
@Bean
public Binding binding3(Exchange e1,Queue queue3){
return BindingBuilder
.bind(queue3).to(e1).with("#.#").noargs();
}
1.3.8. 延迟队列
使用死信队列实现
实现机制:
- 创建一个死信队列
- 设置一个普通队列的消息过期时间为1分钟,关联死信队列
- 生产者发送一条消息,设置超时时间一分钟
- 一分钟后该条消息转移到死信队列
- 创建一个消费者绑定该死信队列
- 由于队列创建时,过期时间就固定了,因此如果有多个不同延时的需求
- 在convertAndSend里,第四个参数传入回调函数,同时队列的设置里,去掉ttl
(msg)->{ //发送消息的延时时长 msg.getMessageProperties().setExpiration(ttlTime); return msg; }
使用插件实现
安装插件,docker安装
- 下载对应版本插件 https://github.com/rabbitmq/rabbitmq-delayed-message-exchange/releases
- 插件放到 /var/lib/rabbitmq/plugins 目录
- 启用 rabbitmg-plugins enable rabbitmq_delayed_message_exchange
- 查看插件列表 rabbitmq-plugins list
- 重启生效
创建,实际上是创建了延迟交换机,只在创建交换机时有差别,创建延迟交换机,队列和绑定都和普通队列相同
- 方式一
@Bean("exchange1")
public Exchange exchange1(){
return ExchangeBuilder
.topicExchange("topicExchange1") // topic交换机,名字topicExchange1
.delayed() // 创建延迟交换机
.build();
}
- 方式二
@Bean("delayExchange")
public Exchange expireExchange() {
HashMap<String, Object> exchange = new HashMap<>();
exchange.put("x-delayed-type","topic"); // topic 类型的延迟交换机
return new CustomExchange("delayExchange","x-delayed-message",true,false,exchange);
/**
* 参数1:交换机名称
* 参数2:类型必须是 x-delayed-message
* 参数3:是否持久化
* 参数4:是否自动删除(当最后一个消费者断开连接之后队列是否自动被删除)
* 参数5:自定义交换机的 HashMap
*/
}
1.4. 消息确认
1.4.1. confirm, Return
- confirm 交换机确认收到消息
- 模式:生产者发送消息后, 服务器要给客户端发一条确认消息
- 解决的问题:应对场景,生产者发出的消息,服务器没收到
- Return 队列确认收到消息
- 模式:配置一个消费者专门处理目的地错误的消息
- 解决的问题:生产者的消息发送到了不存在的路由,
配置文件, 在生产者配置
- publisher-confirm-type confirm配置
- simple 消息发送成功失败都会调用
- correlated 消息发送成功调用
- publisher-return return配置
spring.rabbitmq.publisher-confirm-type=simple # 生产者 交换机确认收到消息
spring.rabbitmq.publisher-return=true #生产者 队列确认收到消息
在config目录 编写
@Component
public class MqMsgConfirmReturn implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnCallback {
@Resource
private RabbitTemplate rabbitTemplate;
@PostConstructor
public void init() {
rabbitTemplate.setConfirmCallback(this);
rabbitTemplate.setReturnCallback(this);
}
/**
* 监听交换机消息
* @param correlationData 消息ID
* @param ack 是否成功
* @param cause 原因
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
System.out.println("confirm: " + correlationData + " ack: " + ack + " cause: " + cause);
}
/**
* 分发到队列失败时执行
* @param message 消息内容
* @param replyCode 返回码
* @param replyText 失败原因
* @param exchange 目标交换机
* @param routingKey 目标路由
*/
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
System.out.println("returnedMessage: " + message + " replyCode: " + replyCode + " replyText: " + replyText + " exchange: " + exchange + " routingKey: " + routingKey);
}
}
1.4.2. ACK机制 消费者端
- 默认情况下,消费者接收消息后,无论是否处理成功,消息都会从队列中移出,导致失败的消息无法重新处理
- 开启ack模式:
- 消费者处理消息成功则进行签收,消息从队列移出
- 消费者处理消息失败则拒收,消息退回队列
配置文件,在消费者配置,开启手工模式,并指定每次拉取消息数量
spring.rabbitmq.listener.simple.acknowledge-mode=manual
spring.rabbitmq.listener.simple.prefetch=1 #每次拉取消息数量
创建消息者类, 投递方式
- basicAck():签收
- basicNack(): 拒收
- basicReject():拒收,相比二少了第二个参数批量签收
@Component
public class TopicAckConsumer {
@RabbitListerner(queues="queue2")
public void consume(Message message, Channel channel) {
long deliveryTag = message.getMessageProperties().getDeliveryTag();//获取投递次数,每投递一次,deliveryTag加1
byte[] body = message.getBody(); // 获取消息内容
try{
//对象反序列化为map
// ObjectInputStream objectInputStream = new ObjectInputStream(new ByteArrayInputStream(body));
// Map<String,String> msgMap = (Map<String,String>) objectInputStream.readObject();
// objectInputStream.close();
channel.basicAck(deliveryTag,true) ; //确认签收, 投递序号,是否一次签收多条
}catch(Exception e){
channel.basicNack(deliveryTag,true,true); //拒绝签收, 投递序号,是否一次签收多条,是否退回队列
log.warn(":{}",e);
}
System.out.println("TopicAckConsumer: " + message);
}
}
1.5. 消息属性
1.5.1. 消息内容
1.5.2. 过期时间
在队列中定义
- 该队列内所有消息的过期时间
- ttl 属性
- 类型 long
- 单位 毫秒
@Bean("queue1") //用于绑定时的传参
public Queue queue1(){
return QueueBuilder
.durable(true) //持久化
.ttl(60*1000) // 该队列内消息的存活时间,单位毫秒, 超过该时间,消息将从队列删除
.build();
}
在生产者发送消息时定义
- 每条消息到达队首时,判断是否过期,过期则删除
- 如果同时指定了队列过期时间,这两个过期时间哪个短用哪个
- 属性
- 类型 字符串
- 单位 毫秒
// 定义消息属性
MessageProperties messageProperties = new MessageProperties();
// 设置消息存活时间
messageProperties.setExpiration("1000");
// 创建消息对象
Message message = new Message("消息内容".getBytes(StandardCharsets.UTF_8), messageProperties);
rabbitTemplate.convertAndSend("exchange", "route", message);
1.5.3. 优先级
在队列中设置该队列的最高优先级,这会是发送消息时的最高优先级
@Bean("queue1")
public Queue queue1(){
return QueueBuilder
.maxPriority(10) // 最高优先级
.build();
}
生产者发消息时指定优先级
MessageProperties messagePropertie = new MessageProperties();
messagePropertie.setPriority(10); // 设置为该队列中的最高优先级10
Message message = new Message(msg.getBytes(), messagePropertie); // byte[] ,MessageProperties
rabbitTemplate.convertAndSend("exchange1", "msgword", message);
多次投递失败的,加入死信队列
设置默认队列,

浙公网安备 33010602011771号