基于Redis的zset数据结构实现延迟消息功能
1. 定义延迟队列
@Component
public class RedisDelayedQueue {
@Resource
private StringRedisTemplate stringRedisTemplate;
/**
* 添加消息延迟队列
* @param queueName 队列名称
* @param message 消息体
* @param delaySeconds 过期时间(s)
*/
public void addMessage(String queueName, String message, long delaySeconds) {
long score = System.currentTimeMillis() / 1000 + delaySeconds;
stringRedisTemplate.opsForZSet().add(queueName, message, score);
}
/**
* 获取并移除已到期的消息
* @param queueName
* @return
*/
public String pollMessage(String queueName) {
long now = System.currentTimeMillis() / 1000;
Set<String> messages = stringRedisTemplate.opsForZSet().rangeByScore(queueName, 0, now, 0, 1);
if (CollUtil.isEmpty(messages)) {
return null;
}
String message = messages.iterator().next();
this.removeMessage(queueName, message);
return message;
}
/**
* 移除消息
* @param queueName
* @param message
*/
public void removeMessage(String queueName, String message) {
try {
stringRedisTemplate.opsForZSet().remove(queueName, message);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 阻塞获取消息
* @param queueName
* @return
* @throws InterruptedException
*/
public String takeMessage(String queueName) throws InterruptedException {
String message;
while ((message = pollMessage(queueName)) == null) {
TimeUnit.SECONDS.sleep(1); // 每秒检查一次
}
return message;
}
}
2. 启动延迟队列
@Slf4j
@Component
public class RedisDelayQueueRunner implements CommandLineRunner {
@Autowired
private RedisDelayedQueue redisDelayedQueue;
@Override
public void run(String... args) throws Exception {
// 启动子线程轮询获取队列中是否存在过期值
ThreadPoolUtils.execute(() -> {
while (true) {
try {
String value = redisDelayedQueue.takeMessage(queueName);
if (StrUtil.isNotBlank(value)) {
log.info("【检测队列中存在过期value】,value:{}", value);
// 执行业务逻辑......
}
} catch (Exception e) {
log.error("【消费延迟队列消息失败】", e);
}
}
});
}
}