通过Redisson构建延时队列并实现注解式消费

目录

一、序言

两个月前接了一个4万的私活,做一个线上商城小程序,在交易过程中不可避免的一个问题就是用户下单后的订单自动取消。

目前成熟的方案有通过RabbitMQ+死信队列RabbitMQ+延迟消息插件RocketMQ定时消息推送Redisson延时队列来实现。

考虑到商城的定位和用户体量,以及系统维护成本,其实完全没有必要引入消息中间件,借助Redis其实就可以轻松实现这个需求。

加上Redisson客户端本身就已经实现了很多分布式集合工具类,借助阻塞队列和延时队列就可轻松搞定。

当然,为了使用方便以及团队协作,顺便模仿@RabbitListener封装了一套基于注解的消息消费,废话不多说,直接上代码。


二、延迟队列实现

1、Redisson延时消息监听注解和消息体

延迟消息监听器定义:

java 复制代码
/**
 * Redisson延时队列监听器
 *
 * @author Nick Liu
 * @date 2024/11/13
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RedissonDelayedQueueListener {

	/**
	 * 队列名称
	 * @return
	 */
	String queueName();
}

消息体定义:

java 复制代码
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class RedisDelayedMsgDTO {

	/**
	 * 消息内容
	 */
	private String msg;
	/**
	 * 队列名称
	 */
	private String queueName;
	/**
	 * 延时时间
	 */
	private long delayTime;
	private TimeUnit timeUnit;
}

2、Redisson延时消息发布器

java 复制代码
@Slf4j
@Component
@RequiredArgsConstructor
public class RedissonDelayedMsgPublisher {

	private final RedissonClient redissonClient;

	/**
	 * 发布延时信息
	 * @param delayedMsgDTO
	 */
	public void publishDelayedMsg(RedisDelayedMsgDTO delayedMsgDTO) {
		log.info("开始发布延迟消息: {}", FastJsonUtils.toJsonString(delayedMsgDTO));
		RBlockingQueue<String> blockingQueue = redissonClient.getBlockingQueue(delayedMsgDTO.getQueueName());
		RDelayedQueue<String> delayedQueue = redissonClient.getDelayedQueue(blockingQueue);
		delayedQueue.offer(delayedMsgDTO.getMsg(), delayedMsgDTO.getDelayTime(), delayedMsgDTO.getTimeUnit());
	}
}

这里我们借助RBlockingQueueRDelayedQueue来实现,只有当延迟消息快到期时,消费者才能从阻塞队列拉取到消息,否则消费者将一直阻塞。

3、Redisson延时消息监听处理器

这里我们定义了一个BeanPostProcessor 的实现,目的就是为了扫描Spring容器中所有带RedissonDelayedQueueListener注解的Bean实例和方法。

java 复制代码
/**
 * Redisson延迟队列Bean后处理器
 * @author Nick Liu
 * @date 2025/1/3
 */
@Slf4j
@Component
@RequiredArgsConstructor
public class RedissonDelayedQueuePostProcessor implements BeanPostProcessor {

	private final RedissonClient redissonClient;

	@Override
	public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
		// 获取最终的目标运行时对象
		Class<?> clazz = AopProxyUtils.ultimateTargetClass(bean);
		Method[] methods = clazz.getDeclaredMethods();

		for (Method m : methods) {
			if (!m.isAnnotationPresent(RedissonDelayedQueueListener.class)) {
				continue;
			}
			// 如果Bean上的方法有Redisson队列监听注解,则启动一个线程监听队列
			RedissonDelayedQueueListener annotation = m.getAnnotation(RedissonDelayedQueueListener.class);
			CompletableFuture.runAsync(() -> {
				log.info("开始监听Redisson延时队列[{}]消息", annotation.queueName());
				while (true) {
					RBlockingQueue<String> blockingQueue = redissonClient.getBlockingQueue(annotation.queueName());
					redissonClient.getDelayedQueue(blockingQueue);
					try {
						String msg = blockingQueue.take();
						MDC.put(CommonConst.X_REQUEST_ID, SerialNoUtils.generateSimpleUUID());
						log.info("监听到队列[{}]延时消息: {}", annotation.queueName(), msg);
						m.invoke(bean, msg);
						MDC.remove(CommonConst.X_REQUEST_ID);
					} catch (Exception e) {
						log.error(e.getMessage(), e);
					}
				}
			});
		}

		return bean;
	}

}

这里我们扫描到指定Bean的方法后,会开启一个异步线程,并轮询拉取延时消息,如果消息没过期,异步线程将会一直阻塞等待。


三、测试用例

java 复制代码
/**
 * @author Nick Liu
 * @date 2025/2/2
 */
@Slf4j
@RestController
@RequiredArgsConstructor
public class RedissonDelayedMsgController {

	private static final String DELAYED_QUEUE = "redisson:delayed:queue";

	private final RedissonDelayedMsgPublisher redissonDelayedMsgPublisher;

	@GetMapping("/delayed/msg")
	public ResponseEntity<RedisDelayedMsgDTO> publishDelayedMsg() {
		RedisDelayedMsgDTO redisDelayedMsgDTO = new RedisDelayedMsgDTO();
		redisDelayedMsgDTO.setQueueName(DELAYED_QUEUE);
		redisDelayedMsgDTO.setMsg("This is a delayed msg");
		redisDelayedMsgDTO.setDelayTime(10);
		redisDelayedMsgDTO.setTimeUnit(TimeUnit.SECONDS);
		redissonDelayedMsgPublisher.publishDelayedMsg(redisDelayedMsgDTO);
		return ResponseEntity.ok(redisDelayedMsgDTO);
	}

	@RedissonDelayedQueueListener(queueName = DELAYED_QUEUE)
	public void handleDelayedMsg(String msg) {
		log.info("Received delayed msg: {}", msg);
	}
}

启动服务后,Bean后处理器会启动异步线程监听延时消息,如下:

bash 复制代码
2025-02-02 16:46:04.271 INFO  [] [ForkJoinPool.commonPool-worker-2] [c.xlyj.common.message.RedissonDelayedQueuePostProcessor.lambda$postProcessAfterInitialization$0():44] - 开始监听Redisson延时队列[redisson:delayed:queue]消息

浏览器直接输入http://localhost:8000/delayed/msg发布延时消息,10s后消费者进行处理,如下:

bash 复制代码
2025-02-02 16:43:11.107 INFO  [e810d175b0e24e71a4b9e517366b4aa6] [ForkJoinPool.commonPool-worker-2] [c.xlyj.common.message.RedissonDelayedQueuePostProcessor.lambda$postProcessAfterInitialization$0():51] - 监听到队列[redisson:delayed:queue]延时消息: This is a delayed msg
2025-02-02 16:43:11.108 INFO  [e810d175b0e24e71a4b9e517366b4aa6] [ForkJoinPool.commonPool-worker-2] [com.xlyj.contoller.RedissonDelayedMsgController.handleDelayedMsg():40] - Received delayed msg: This is a delayed msg

四、结语

虽说通过Redisson实现的延迟队列也能实现支付订单的自动取消,但是可用性相比专业的消息中间件还是尚有不足的。

比如消息生产者发送消息没有确认机制,消息消费也没有确认机制,这两个环节都有可能导致消息丢失。

当然我们可以通过其它保障机制去补偿,比如再加上定时任务扫表,把扫描时间可以设置长一点,保证最终的一致性。

在大型项目中还是优先推荐专业的消息中间件去实现延时消息消费。

相关推荐
Jabes.yang1 天前
Java面试大作战:从缓存技术到音视频场景的探讨
java·spring boot·redis·缓存·kafka·spring security·oauth2
朝九晚五ฺ1 天前
【Redis学习】持久化机制(RDB/AOF)
数据库·redis·学习
怪兽20141 天前
什么是 Redis?
java·数据库·redis·缓存·面试
wangmengxxw1 天前
Redis概述
数据库·redis·缓存
摇滚侠1 天前
Spring Boot 3零基础教程,Spring Boot 日志的归档与切割,笔记22
spring boot·redis·笔记
爬山算法1 天前
Redis(64)Redis的Lua脚本有哪些常见场景?
数据库·redis·lua
❀͜͡傀儡师1 天前
OpenResty + Lua + Redis 鉴权案例,适用于 x86 和 ARM 架构的 Docker 环境。
redis·lua·openresty
Knight_AL2 天前
Redis 限流解决方案:结合 Lua 脚本、AOP 和自定义注解的实现
redis·spring
Mr Aokey2 天前
解决Redis数据丢失难题:深入理解RDB与AOF持久化机制
数据库·redis·缓存
柳贯一(逆流河版)2 天前
Redis 分布式锁实战:解决马拉松报名并发冲突与 Lua 原子性优化
redis·分布式·lua