通过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实现的延迟队列也能实现支付订单的自动取消,但是可用性相比专业的消息中间件还是尚有不足的。

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

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

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

相关推荐
如风暖阳1 小时前
Redis背景介绍
数据库·redis·缓存
lingllllove2 小时前
Redis脑裂问题详解及解决方案
数据库·redis·缓存
微光守望者3 小时前
Redis常见命令
数据库·redis·缓存
@_@哆啦A梦16 小时前
Redis 基础命令
java·数据库·redis
gentle coder18 小时前
Redis_Redission的入门案例、多主案例搭建、分布式锁进行加锁、解锁底层源码解析
java·redis·分布式
萝卜青今天也要开心18 小时前
读书笔记-《Redis设计与实现》(一)数据结构与对象(下)
java·数据结构·redis·学习
java1234_小锋20 小时前
说说Redis的内存淘汰策略?
数据库·redis·缓存
2的n次方_1 天前
【Redis】set 和 zset 类型的介绍和常用命令
数据库·redis·缓存
桂月二二1 天前
使用 Redis Streams 实现高性能消息队列
数据库·redis·缓存