示例达到的目的
1.通过消息队列执行通知、回调、延迟执行的任务等
2.实现延迟消息,可设定消息在多少时间后抵达
3.实现持久化,确保当项目重启、rabbitmq重启等异常情况发生时,重启完成后消息能继续被执行
4.可异步处理消息,根据服务器配置调整,避免阻塞主线程
5.完整的重试机制,可配置重试次数、阶段性等待时长
安装并启用rabbitmq
1.从docker拉取最新的rabbitmq镜像
docker pull rabbitmq
2.创建并运行rabbitmq容器(参数不做解释,不懂问chatgpt)
docker run -d -p 15673:15672 -p 5674:5672 \
--restart=always \
-e RABBITMQ_DEFAULT_VHOST=ant_applet \
-e RABBITMQ_DEFAULT_USER=admin \
-e RABBITMQ_DEFAULT_PASS=dainiquheiye \
--hostname antApplet \
--name rabbitmq\
rabbitmq:latest
3.启动web客户端,通过ip:15673访问
docker exec -it rabbitmq rabbitmq-plugins enable rabbitmq_management
访问web客户端可能会遇到Stats in management UI are disabled on this node弹窗提示,按以下步骤可关闭弹窗
# 进入到启动的rabbitmq的容器中
docker exec -it rabbitmq /bin/bash
# 切换到rabbitmq的配置文件目录
cd /etc/rabbitmq/conf.d/
# 修改配置文件
echo management_agent.disable_metrics_collector = false > management_agent.disable_metrics_collector.conf
# 查看配置文件,看看是否修改成功
cat management_agent.disable_metrics_collector.conf
# 退出容器
exit
# 重启容器
docker restart rabbitmq
4.开启延迟插件
# 下载插件,选择与rabbitmq版本相近的版本
https://github.com/rabbitmq/rabbitmq-delayed-message-exchange/releases
# 上传至服务器
cd /home/rabbitmq
docker cp rabbitmq_delayed_message_exchange-3.13.0.ez rabbitmq:/plugins
# 进入RabbitMQ容器:
docker exec -it rabbitmq bash
(此时执行rabbitmq-plugins list可以看到其他插件的版本,可参照版本号进行下载)
# 启动插件
rabbitmq-plugins enable rabbitmq_delayed_message_exchange
# 退出容器
exit
5.在maven或gradle中配置依赖
//gradle
implementation 'org.springframework.boot:spring-boot-starter-amqp'
6.在application.yml中配置rabbitmq
spring:
rabbitmq:
host: 127.0.0.1
port: 5674
username: admin
password: dainiquheiye
virtual-host: ant_applet # 虚拟主机配置
listener:
simple:
acknowledge-mode: manual # 手动确认
异步配置
package com.ant.rabbitmq;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
/**
* 异步配置
*/
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
/**
* 引入日志
*/
private final static Logger logger = LoggerFactory.getLogger(AsyncConfig.class);
@Bean(name = "async")
public ThreadPoolTaskExecutor executor() {
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
//核心线程数
taskExecutor.setCorePoolSize(5);
//线程池维护线程的最大数量,只有在缓存队列满了之后才会申请超过核心线程数的线程
taskExecutor.setMaxPoolSize(10);
//缓存队列
taskExecutor.setQueueCapacity(20);
//设置线程的空闲时间,当超过了核心线程之外的线程在空闲时间到达之后会被销毁
taskExecutor.setKeepAliveSeconds(300);
//异步方法内部线程名称
taskExecutor.setThreadNamePrefix("ant-async-");
/*
* AbortPolicy 丢弃任务并抛出异常
* DiscardPolicy 丢弃任务,不抛出异常
* DiscardOldestPolicy 丢弃队列最前面的任务,然后重新尝试该任务
* CallerRunsPolicy 重试当前任务,直到成功为止
*/
taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
taskExecutor.initialize();
return taskExecutor;
}
/**
* 指定默认线程池
*/
@Override
public Executor getAsyncExecutor() {
return executor();
}
/**
* 异常捕捉
*/
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return (e, method, params) -> logger.error("线程池执行任务异常:" + e.getMessage() + ",执行方法:" + method.getName());
}
}
配置消息队列
package com.ant.rabbitmq;
import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 配置消息队列
*/
@Configuration
public class RabbitMQConfig {
/**
* 定义队列名称
*/
public static final String DELAY_QUEUE = "delay.queue";
/**
* 定义RoutingKey
*/
public static final String ROUTING_KEY = "delay";
/**
* 定义交换器名称
*/
public static final String DELAY_EXCHANGE = "delay.exchange";
/**
* 重试机制次数上限
*/
// public static final int[] maxRetry = {10};
public static final int[] maxRetry = {10, 50, 100};
/**
* 重试等待时长(毫秒ms)
*/
// public static final int[] retryDelay = {3000};
public static final int[] retryDelay = {3000, 30000, 300000};
/**
* 配置队列
*/
@Bean
public Queue queue() {
// 创建一个持久化的队列
return new Queue(DELAY_QUEUE, true);
}
/**
* 配置交换器
*/
@Bean
public DirectExchange exchange() {
return ExchangeBuilder.directExchange(DELAY_EXCHANGE).delayed().durable(true).build();
}
/**
* 配置队列与交换器的绑定
*/
@Bean
public Binding binding() {
return BindingBuilder.bind(queue()).to(exchange()).with(ROUTING_KEY);
}
}
创建生产者
package com.ant.rabbitmq;
import com.alibaba.fastjson.JSON;
import com.ant.common.util.StringUtil;
import com.ant.context.cache.CacheContext;
import com.ant.context.data.DataContext;
import jakarta.annotation.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.MessageDeliveryMode;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.web.bind.annotation.*;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
/**
* RabbitMQ 消息队列
*/
@RestController
@RequestMapping("/rabbitmq")
@EnableAsync
public class RabbitMQProducer {
/**
* 引入日志
*/
private final static Logger logger = LoggerFactory.getLogger(RabbitMQProducer.class);
/**
* 引入缓存工具类
*/
@Resource
private CacheContext cacheContext;
private static RabbitTemplate rabbitTemplate;
public RabbitMQProducer(RabbitTemplate rabbitTemplate) {
RabbitMQProducer.rabbitTemplate = rabbitTemplate;
}
/**
* 主动发送消息
*/
@PostMapping("/send/{key}")
public void sendMessage(@PathVariable String key, @RequestBody Map<String, Object> params) {
send(key, "", params, 0, cacheContext);
}
/**
* 发送消息
*/
public static void send(String key, String id, Map<String, Object> params, int delay, CacheContext cacheContext) {
int retryCount = 0;
id = StringUtil.isBlank(id) ? DataContext.getSequenceId("RT") : id;
//判断参数
if (cacheContext == null) {
logger.error("[" + id + "]:执行任务 " + key + " 时发生逻辑错误(cacheContext缓存对象不能为null)");
return;
}
try {
Map<String, Object> message = new HashMap<>();
message.put("key", key);
message.put("id", id);
message.put("params", params);
//从缓存中获取该id的重试次数
Object obj = cacheContext.getTempObj(id);
if (obj != null) {
retryCount = Integer.parseInt(obj.toString());
}
//执行消息,获取返回值
if (retryCount == 0) {
logger.info("[" + id + "]:即将执行任务 " + key + ",携带参数:" + JSON.toJSONString(params));
}
//将消息转换为字节数组
byte[] messageBytes = JSON.toJSONString(message).getBytes(StandardCharsets.UTF_8);
//执行发送
rabbitTemplate.convertAndSend(RabbitMQConfig.DELAY_EXCHANGE, RabbitMQConfig.ROUTING_KEY, messageBytes, msg -> {
msg.getMessageProperties().setDeliveryMode(MessageDeliveryMode.PERSISTENT);
msg.getMessageProperties().setDelay(delay);
return msg;
});
} catch (Exception e) {
logger.error("[" + id + "]:任务 " + key + " 在进行第" + (retryCount + 1) + "次尝试时发生异常(" + e.getMessage() + ")");
retryCount++;
cacheContext.setTempHour(id, retryCount, 24);
RabbitMQReceiver rabbitMQReceiver = new RabbitMQReceiver();
rabbitMQReceiver.retry(key, id, params, cacheContext);
}
}
}
创建消费者
package com.ant.rabbitmq;
import com.alibaba.fastjson.JSON;
import com.ant.common.bean.ApiStatus;
import com.ant.common.util.StringUtil;
import com.ant.context.cache.CacheContext;
import com.ant.task.Task;
import com.rabbitmq.client.Channel;
import jakarta.annotation.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Map;
/**
* 接收消息并处理
*/
@Component
@EnableAsync
public class RabbitMQReceiver {
/**
* 引入缓存工具类
*/
@Resource
private CacheContext cacheContext;
/**
* 引入日志
*/
private final static Logger logger = LoggerFactory.getLogger(RabbitMQReceiver.class);
/**
* 接收并处理消息
*/
@Async
@RabbitListener(queues = RabbitMQConfig.DELAY_QUEUE)
public void receiveMessage(byte[] messageByte, Message message, Channel channel) throws IOException {
String key = "";
String id = "";
int retryCount = 0;
try {
//将消息转换成map
String messageStr = new String(messageByte, StandardCharsets.UTF_8);
Map<String, Object> data = JSON.parseObject(messageStr);
//获取消息内容
key = StringUtil.getString(data, "key");
id = StringUtil.getString(data, "id");
Map<String, Object> params = JSON.parseObject(StringUtil.getString(data, "params"));
//从缓存中获取该id的重试次数
Object obj = cacheContext.getTempObj(id);
if (obj != null) {
retryCount = Integer.parseInt(obj.toString());
}
//执行任务
Map<String, Object> result = Task.doTask(key, id, params);
int flag = StringUtil.getInt(result, "flag");
String msg = StringUtil.getString(result, "message");
//执行成功,清空重试计数
if (flag == ApiStatus.SUCCESS) {
logger.info("[" + id + "]:执行任务 " + key + " 成功");
cacheContext.deleteTemp(id);
//手动答应消费完成,从队列中删除该消息
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
return;
}
//需要重试
if (flag == ApiStatus.RETRY_TASK) {
retryCount++;
//判断重试次数上限
if (retryCount < Arrays.stream(RabbitMQConfig.maxRetry).max().getAsInt()) {
logger.error("[" + id + "]:第" + retryCount + "次执行任务 " + key + " 失败");
//执行重试
cacheContext.setTempHour(id, retryCount, 24);
retry(key, id, params, cacheContext);
//手动答应消费完成,从队列中删除该消息(不重回队列)
channel.basicReject(message.getMessageProperties().getDeliveryTag(), false);
} else {
//达到最大重试次数,记录日志或执行其他操作
logger.error("[" + id + "]:第" + retryCount + "次执行任务 " + key + " 失败,已达到最大重试次数");
cacheContext.deleteTemp(id);
//手动答应消费完成,从队列中删除该消息(不重回队列)
channel.basicReject(message.getMessageProperties().getDeliveryTag(), false);
}
return;
}
//执行失败,通常是代码逻辑错误或参数不齐全,没有重试的意义,直接返回异常
logger.error("[" + id + "]:执行任务 " + key + " 时发生逻辑错误(" + msg + ")");
cacheContext.deleteTemp(id);
//手动答应消费完成,从队列中删除该消息(不重回队列)
channel.basicReject(message.getMessageProperties().getDeliveryTag(), false);
} catch (Exception e) {
logger.error("[" + id + "]:执行任务 " + key + " 时发生异常(" + e.getMessage() + ")");
cacheContext.deleteTemp(id);
//手动答应消费完成,从队列中删除该消息(不重回队列)
channel.basicReject(message.getMessageProperties().getDeliveryTag(), false);
}
}
/**
* 重试
*/
@Async
public void retry(String key, String id, Map<String, Object> params, CacheContext cacheContext) {
int retryCount = 1;
//从缓存中获取该id的重试次数
Object obj = cacheContext.getTempObj(id);
if (obj != null) {
retryCount = Integer.parseInt(obj.toString());
}
//判断重试次数上限
if (retryCount >= Arrays.stream(RabbitMQConfig.maxRetry).max().getAsInt()) {
//达到最大重试次数,记录日志或执行其他操作
logger.error("[" + id + "]:第" + (retryCount + 1) + "次执行任务 " + key + " 失败,已达到最大重试次数");
cacheContext.deleteTemp(id);
return;
}
//根据重试次数设置等待时间
int delay = getRetryDelay(retryCount);
logger.warn("[" + id + "]:任务 " + key + " 将在" + delay + "ms后进行第" + (retryCount + 1) + "次尝试");
//发送消息到指定的交换器和队列
RabbitMQProducer.send(key, id, params, delay, cacheContext);
}
private static int getRetryDelay(int currentRetryCount) {
for (int i = 0; i < RabbitMQConfig.maxRetry.length; i++) {
if (currentRetryCount <= RabbitMQConfig.maxRetry[i]) {
return RabbitMQConfig.retryDelay[i];
}
}
//如果超出最高等级,则使用最高等级的等待时长
return RabbitMQConfig.retryDelay[RabbitMQConfig.retryDelay.length - 1];
}
}
创建一个任务管理器
package com.ant.task;
import com.ant.common.bean.ApiStatus;
import java.util.HashMap;
import java.util.Map;
/**
* 配置消息键key对应的消息实体
*/
public class Task {
/**
* 在此处执行消息键key所对应的消息体
*/
public static Map<String, Object> doTask(String key, String id, Map<String, Object> params) {
//定义返回值
Map<String, Object> result = new HashMap<>();
switch (key) {
/**
* 重试机制测试
*/
case "retry-test" -> result = TestTask.retryTest(params);
default -> {
result.put("flag", ApiStatus.ERROR);
result.put("message", "[" + id + "]根据消息键[" + key + "]没有找到对应可执行的消息体");
return result;
}
}
return result;
}
}
编写需要执行的任务
package com.ant.task;
import com.ant.common.bean.ApiStatus;
import com.ant.common.util.StringUtil;
import java.util.HashMap;
import java.util.Map;
/**
* 测试任务
*/
public class TestTask {
/**
* 重试机制测试
*/
public static Map<String, Object> retryTest(Map<String, Object> params) {
int type = StringUtil.getInt(params, "type");
Map<String, Object> result = new HashMap<>();
switch (type) {
case 0 -> {
result.put("flag", ApiStatus.SUCCESS);
result.put("message", "测试结果:成功");
}
case 1 -> {
result.put("flag", ApiStatus.RETRY_TASK);
result.put("message", "测试结果:需要重试");
}
case 2 -> {
result.put("flag", ApiStatus.ERROR);
result.put("message", "测试结果:失败");
}
}
return result;
}
}
现在,我们来发送一条消息
- 项目外
//POST
http://127.0.0.1:9000/antApi/rabbitmq/send/retry-test
其中 retry-test 代表key,即需要执行的消息键,对应执行任务管理器中配置的任务
通过body传入该任务需要的所有参数
{
"type": 0 //0模拟执行成功 1模拟需要重试 2模拟直接失败
}
- 项目内
/**
* 发送消息到指定的交换器和队列
* key 消息键
* id 消息的唯一id,首次执行传空字符串,会自动生成
* params 消息键对应的任务需要的所有参数
* delay 消息延迟达到的时间
* cacheContext 缓存对象,由于此处我使用的缓存,在对内函数无法获取缓存对象,所以需要进行传递(可自行更改为其他缓存)
*/
RabbitMQProducer.send(key, id, params, delay, cacheContext);