SpringBoot整合RabbitMQ
实现延时队列
-
pom.xml
<?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>
<groupId>com.my</groupId>
<artifactId>springboot-rabbitmq</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springboot-rabbitmq</name>
<description>springboot-rabbitmq</description>
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-boot.version>2.6.13</spring-boot.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>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.47</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
<configuration>
<mainClass>com.my.SpringbootRabbitmqApplication</mainClass>
<skip>true</skip>
</configuration>
<executions>
<execution>
<id>repackage</id>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
-
配置交换机和队列
package com.my.demos.config;
import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class TTLQueueConfig {
// 普通交换机的名称
public static final String X_EXCHANGE = "X";
// 死信交换机名称
public static final String Y_Dead_LETTER_EXCHANGE = "Y";
// 普通队列名称
public static final String QUEUE_A = "QA";
public static final String QUEUE_B = "QB";
// 死信队列名称
public static final String Dead_LETTER_QUEUE_D = "QD";
// 声明xExchange
@Bean("xExchange")
public DirectExchange xExchange(){
return new DirectExchange(X_EXCHANGE);
}
// 声明YExchange
@Bean("yExchange")
public DirectExchange yExchange(){
return new DirectExchange(Y_Dead_LETTER_EXCHANGE);
}
// 声明队列
@Bean("queueA")
public Queue queueA(){
Map<String, Object> arguments = new HashMap<>(3);
// 配置死信交换机
arguments.put("x-dead-letter-exchange", Y_Dead_LETTER_EXCHANGE);
// 配置死信routingKey
arguments.put("x-dead-letter-routing-key", "YD");
// 配置TTL 单位是ms
arguments.put("x-message-ttl", 10000);
return QueueBuilder.durable(QUEUE_A).withArguments(arguments).build();
}
// 声明队列
@Bean("queueB")
public Queue queueB(){
Map<String, Object> arguments = new HashMap<>(3);
// 配置死信交换机
arguments.put("x-dead-letter-exchange", Y_Dead_LETTER_EXCHANGE);
// 配置死信routingKey
arguments.put("x-dead-letter-routing-key", "YD");
// 配置TTL 单位是ms
arguments.put("x-message-ttl", 40000);
return QueueBuilder.durable(QUEUE_B).withArguments(arguments).build();
}
// 声明死信队列
@Bean("queueD")
public Queue queueD(){
return QueueBuilder.durable(Dead_LETTER_QUEUE_D).build();
}
// 绑定QueueA
@Bean
public Binding queueABindingX(@Qualifier("queueA") Queue queueA,
@Qualifier("xExchange") DirectExchange xExchange){
return BindingBuilder.bind(queueA).to(xExchange).with("XA");
}
// 绑定QueueB
@Bean
public Binding queueBBindingX(@Qualifier("queueB") Queue queueB,
@Qualifier("xExchange") DirectExchange xExchange){
return BindingBuilder.bind(queueB).to(xExchange).with("XB");
}
// 绑定死信队列QueueD
@Bean
public Binding queueDBindingY(@Qualifier("queueD") Queue queueD,
@Qualifier("yExchange") DirectExchange yExchange){
return BindingBuilder.bind(queueD).to(yExchange).with("YD");
}
}
-
消费者
package com.my.demos.consumer;
import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import java.util.Date;
@Slf4j
@Component
public class DeadLetterQueueConsumer {
// 接收消息
@RabbitListener(queues = "QD")
public void receiveD(Message message, Channel channel) throws Exception{
String msg = new String(message.getBody(), "UTF-8");
log.info("当前时间:{}, 收到死信队列的消息:{}", new Date().toString(), msg);
}
}
生产者
package com.my.demos.controller;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.Date;
/**
* @author zhupanlin
* @version 1.0
* @description: TODO
* @date 2023/12/1 11:06
*/
@Slf4j
@RestController
@RequestMapping("/ttl")
public class SendMessageController {
@Resource
private RabbitTemplate rabbitTemplate;
@ApiOperation("发消息")
@GetMapping("/sendMsg/{message}")
public void sendMsg(@ApiParam(name = "message", value = "消息") @PathVariable("message") String message){
log.info("当前时间:{}, 发送一条消息给两个TTL队列:{}", new Date().toString(), message);
rabbitTemplate.convertAndSend("X", "XA", "消息来自TTL为10s的队列:" + message);
rabbitTemplate.convertAndSend("X", "XB", "消息来自TTL为40s的队列:" + message);
}
}
第一条消息在 10S 后变成了私信消息,然后被消费者消费掉,第二条消息在 40S 之后变成了死信消息,然后被消费掉,这样一个延时队列就打造完成了
不过,如果这样使用的话,岂不是每增加一个新的时间需求,就要新增一个队列,这里只有 10S 和 40S 两个时间选项,如果需要一个小时后处理,那么就需要增加 TTL 为一个小时的队列,如果是预定会议室然后提前通知这样的场景,岂不是要增加无数个队列才能满足需求?
解决方案:通过生产者来指定TTL
// 开始发消息 消息 TTL
@ApiOperation("开始发消息 消息 TTL")
@GetMapping("sendExpirationMsg/{message}/{ttlTime}")
public void sendMsg(@PathVariable("message") String message, @PathVariable("ttlTime") String ttlTime){
log.info("当前时间:{}, 发送一条时长{}毫秒TTL信息给队列QC:{}",
new Date().toString(), ttlTime, message);
rabbitTemplate.convertAndSend("X", "XC", "消息来自TTL为" + ttlTime + "的队列:" + message, message1 -> {
// 发送消息的延迟时长
message1.getMessageProperties().setExpiration("20000");
return message1;
});
}
RabbitMQ 只会检查第一个消息是否过期,如果过期则丢到死信队列,如果第一个消息的延时时间很长,而第二个消息的延时时间很短,第二个消息并不会得到优先执行。
可以通过 RabbitMQ 的插件 rabbitmq_delayed_message_exchange ,当消息过期时通过交换机发给死信队列,而不是通过普通队列发给死信队列,解决了上面的问题。
总结
演示队列在需要延时处理的场景下非常有用,使用 RabbitMQ 来实现延时队列可以很好的利用 RabbitMQ 的特性,如:消息可靠发送、消息可靠投递、死信队列来保障消息至少被消费一次以及未被正确处理的消息不会被丢弃。另外,通过 RabbitMQ 集群的特性,可以很好的解决单点故障问题,不会因为单个节点挂掉导致延时队列不可用或者消息丢失。
当然,延时队列还有很多其它选择,比如利用 Java 的 DelayQueue,利用 Redis 的 zset,利用 Quartz 后者利用 kafka 的时间轮,这些方式各有特点,看需要使用的场景。