修改redis配置文件
该功能需要redis单节点,集群的没验证过,有时间的小伙伴可以验证一下。找到redis.conf文件,修改notify-keyspace-events。配置文件修改后需要重新启动redis。
# Example 2: to get the stream of the expired keys subscribing to channel
# name __keyevent@0__:expired use:
#
# notify-keyspace-events Ex
#
# By default all notifications are disabled because most users don't need
# this feature and the feature has some overhead. Note that if you don't
# specify at least one of K or E, no events will be delivered.
notify-keyspace-events Ex
修改pom配置文件
以下三种依赖都行,随便选择一个就行。
<dependency>
<groupId>com.zengtengpeng</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>1.0.8</version>
</dependency>
<!-- redis 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.11.0</version>
</dependency>
核心代码
RedisDelayQueueTestRunner.java
package cn.renkai721.redisDelay;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
/**
* @author renkai721@163.com
* @Description: 启动延迟队列 (如果是微服务,避免被其他服务消费,应该把此RUNNER和ENUM提取到其他相关delay的上一级)
* @date 2023-07-21 08:20
*/
@Slf4j
@Component
public class RedisDelayQueueTestRunner implements CommandLineRunner {
public static String pms_delay_key_mqtt_client_timeout = "pms_delay_key_mqtt_client_timeout";
@Resource
private RedisDelayQueueUtil redisDelayQueueUtil;
@Override
public void run(String... args) {
// 我这里只是用runner代替一下,实际中下面的代码应该出现在事件触发的接口中
// 自己需要用到的业务数据组合成KEY
String key = "F2300006";
redisDelayQueueUtil.addDelayQueue(key, 96, TimeUnit.SECONDS, pms_delay_key_mqtt_client_timeout);
key = "F2300007";
redisDelayQueueUtil.addDelayQueue(key, 125, TimeUnit.SECONDS, pms_delay_key_mqtt_client_timeout);
key = "F2300008";
redisDelayQueueUtil.addDelayQueue(key, 125, TimeUnit.SECONDS, pms_delay_key_mqtt_client_timeout);
}
}
RedisDelayQueueRunner.java
package cn.renkai721.redisDelay;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RBlockingDeque;
import org.redisson.api.RedissonClient;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* @author renkai721@163.com
* @Description: 启动延迟队列
* @date 2023-07-21 08:20
*/
@Slf4j
@Component
public class RedisDelayQueueRunner implements CommandLineRunner {
@Resource
private RedissonClient redissonClient;
@Resource
private RedisDelayQueueUtil redisDelayQueueUtil;
@Override
public void run(String... args) {
RBlockingDeque<String> blockingDeque = redissonClient.getBlockingDeque(RedisDelayQueueTestRunner.pms_delay_key_mqtt_client_timeout);
// 避免消息伪丢失(应用重启未消费),官网推荐
redissonClient.getDelayedQueue(blockingDeque) ;
new Thread(() -> {
while (true) {
try {
String msg = blockingDeque.take();
if(msg != null) {
// 这里处理自己的业务逻辑
// ...
log.info("Redis延迟队列触发,请及时处理您的业务。msg={}",msg);
// 处理完业务逻辑,需要删除延迟队列
redisDelayQueueUtil.removeDelayedQueue(msg, RedisDelayQueueTestRunner.pms_delay_key_mqtt_client_timeout);
}
} catch (InterruptedException e) {
log.error("(Redis延迟队列异常中断) {}", e.getMessage());
}
}
}).start();
}
}
RedisDelayQueueUtil.java
package cn.renkai721.redisDelay;
import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.redisson.api.RBlockingDeque;
import org.redisson.api.RDelayedQueue;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
/**
* @author renkai721@163.com
* @Description: 延迟队列工具
* @date 2023-07-21 08:20
*/
@Slf4j
@Component
public class RedisDelayQueueUtil {
@Resource
private RedissonClient redissonClient;
public <T> void addDelayQueue(T value, LocalDateTime endTime, String queueCode) {
long seconds = Duration.between(LocalDateTime.now(), endTime).getSeconds();
if (seconds > 0) {
addDelayQueue(value, seconds, TimeUnit.SECONDS, queueCode);
}
}
public <T> void addDelayQueue(T value, long delay, String queueCode) {
addDelayQueue(value, delay, TimeUnit.SECONDS, queueCode);
}
/**
* 添加延迟队列
* @param value 队列值
* @param delay 延迟时间
* @param timeUnit 时间单位
* @param queueCode 队列键
* @param <T> 泛型
*/
public <T> void addDelayQueue(T value, long delay, TimeUnit timeUnit, String queueCode){
try {
RBlockingDeque<Object> blockingDeque = redissonClient.getBlockingDeque(queueCode);
RDelayedQueue<Object> delayedQueue = redissonClient.getDelayedQueue(blockingDeque);
delayedQueue.offer(value, delay, timeUnit);
// log.info("(添加延时队列成功) 队列键:{},队列值:{},延迟时间:{}", queueCode, value, timeUnit.toSeconds(delay) + "秒");
//释放队列
delayedQueue.destroy();
} catch (Exception e) {
// log.error("(添加延时队列失败) {}", e.getMessage());
throw new RuntimeException("(添加延时队列失败)");
}
}
/**
* 获取延迟队列
* @param queueCode 队列主键
* @param <T> 泛型
* @return
* @throws InterruptedException
*/
public <T> T getDelayQueue(String queueCode) throws InterruptedException {
RBlockingDeque<Map> blockingDeque = redissonClient.getBlockingDeque(queueCode);
//避免消息伪丢失(应用重启未消费),官网推荐
redissonClient.getDelayedQueue(blockingDeque) ;
T value = (T) blockingDeque.take();
return value;
}
public boolean removeDelayedQueue(@NonNull Object o, @NonNull String queueCode) {
if (StringUtils.isBlank(queueCode) || Objects.isNull(o)) {
return false;
}
try {
RBlockingDeque<Object> blockingDeque = redissonClient.getBlockingDeque(queueCode);
RDelayedQueue<Object> delayedQueue = redissonClient.getDelayedQueue(blockingDeque);
boolean flag = delayedQueue.remove(o);
if(flag){
// log.info("(删除延时队列保证唯一性) 队列键:{},队列值:{}", queueCode, o);
}
delayedQueue.destroy();
return flag;
} catch (Exception e) {
// log.error("(删除延时队列异常) 队列键:{},队列值:{},错误信息:{}",queueCode, o, e.getMessage());
throw new RuntimeException("(删除延时队列异常)");
}
}
}
测试结果
大家看到的下图是开了一个服务的结果。我好奇这种演示消息队列在微服务下会不会重复消费,于是我们服务部署了2台,它尽然自己做了重复消费的逻辑,不需要我们单独加锁处理重复消费的问题。真的很棒!
-------------以上就是redis key过期后callback的功能代码了----------
-------------以上就是Redission延迟消息队列解决方案代码了---------
知识扩展
如果小伙伴需要监听的类型比较多,比如说用户离线监听,订单超时未支付,优惠卷过期提醒等多业务的时候,就要在RedisDelayQueueRunner类里面监听多个KEY了,这时候会显的代码不美观,这时候可以参考一下另一位博主的枚举做法。