SpringBoot 结合 redis 实现定时任务

最近要做一个答题活动相关项目,用户答题时间到后,如果前台没有提交答题,则由后台自动提交.
最简单的方法自然就是定时任务,简单了解了一下定时任务,感觉这里最简单的就是通过监听 redis 键值来实现试卷的自动提交。

实现思路

用户请求试卷时,向 redis 中插入一个 key/value 键值对,保证能通过 key 查询到指定试卷即可,value 为任意值,
同时,在插入时,设置好过期时间,我这里是根据试卷规定的答题时间加上设定的网络延迟时间,具体根据需求来就行了.

等到设置的 redis 过期时间一到,redis 就会通知程序,程序再执行相关方法,就可以解决自动提交的问题了。

ps:通过监听 redis 提供的过期队列来实现定时器功能,,redis 向监听者发送消息告知过期值时,监听者只能获取到对应的 key 值,而取不到 value 值,因为已经过期了,所以这里一定要保证能够通过 key 值定位到对应的试卷,value 为任意值。

开启 redis key 过期提醒

修改 redis 相关事件配置。
找到 redis 配置文件redis.conf,搜索 notify-keyspace-events配置项。
如果没有,添加notify-keyspace-events Ex,如果有值,则追加 Ex,相关参数说明如下:

  • K: keyspace 事件,事件以 keyspace@ 为前缀进行发布
  • E: keyevent 事件,事件以 keyevent@ 为前缀进行发布
  • A: g$lshzxe 的别名,因此”AKE”意味着所有事件
  • g: 一般性的,非特定类型的命令,比如 del,expire,rename 等
  • $: 字符串特定命令
  • l: 列表特定命令
  • s: 集合特定命令
  • h: 哈希特定命令
  • z: 有序集合特定命令
  • x: 过期事件,当某个键过期并删除时会产生该事件
  • e: 驱逐事件,当某个键因 maxmemore 策略而被删除时,产生该事件

SpringBoot 相关配置

引入依赖

我一般都用 maven,所以这里是在 pom 文件中引入 springboot redis 的相关依赖

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

项目中 redis 相关配置

新建配置类RedisListenerConfig来配置监听 redis key 过期事件

/**
 * @ClassName RedisListenerConfig
 * @Description redis消息处理
 * @Author ScarletDrop
 * @Date 2020/7/10 10:50
 * @Version 1.0
 **/
@Configuration
public class RedisListenerConfig {
    private final RedisConnectionFactory redisConnectionFactory;

    public RedisListenerConfig(RedisConnectionFactory redisConnectionFactory) {
        this.redisConnectionFactory = redisConnectionFactory;
    }

    @Bean
    public RedisMessageListenerContainer redisMessageListenerContainer() {
        RedisMessageListenerContainer redisMessageListenerContainer = new RedisMessageListenerContainer();
        redisMessageListenerContainer.setConnectionFactory(redisConnectionFactory);
        return redisMessageListenerContainer;
    }

    @Bean
    public RedisExpiredListener keyExpiredListener() {
        return new RedisExpiredListener(this.redisMessageListenerContainer());
    }
}

新建监听器 RedisExpiredListener 实现 KeyExpirationEventMessageListener 接口,该接口监听 redis 所有 db 的过期事件__keyevent@*__:expired

/**
 * @ClassName RedisMessageReceiver
 * @Description redis消息处理
 * @Author ScarletDrop
 * @Date 2020/7/10 10:58
 * @Version 1.0
 **/
@Slf4j
public class RedisExpiredListener extends KeyExpirationEventMessageListener {
    // 自定义监听redis db库 方法一 写死在代码里面
    /**
    * 配置监听的具体redis db库
    **/
    private static final Topic KEYEVENT_EXPIRED_TOPIC = new PatternTopic("__keyevent@11__:expired");
    /**
     * 注册订阅通道
     **/
    @Override
    protected void doRegister(RedisMessageListenerContainer listenerContainer) {
        listenerContainer.addMessageListener(this, KEYEVENT_EXPIRED_TOPIC);
    }
    // 自定义监听redis db库 方法二 从yml文件中获取redis db库
    /**
    * 获取监听的redis db库
    **/
    @Value("${spring.redis.database}")
    private String database;
    /**
     * 注册订阅通道
     **/
    @Override
    protected void doRegister(RedisMessageListenerContainer listenerContainer) {
        // 配置监听的具体redis库
        String dbExp = StringUtils.format("__keyevent@{}__:expired", database);
        log.info("redis监听库监听通道:{}", dbExp);
        Topic keyevent_expired_topic = new PatternTopic(dbExp);
        listenerContainer.addMessageListener(this, keyevent_expired_topic);
    }

    // ps 上面这两个方法的配置也可以跳过,不配的话就是走父类配置,监听全部redis库

    public RedisExpiredListener(RedisMessageListenerContainer listenerContainer) {
        super(listenerContainer);
    }

    // 这里是具体监听失效redis的方法
    @Override
    public void onMessage(Message message, byte[] pattern) {
        String channel = new String(message.getChannel(), StandardCharsets.UTF_8);
        //过期的key 可以在这之后进行相关操作
        String key = new String(message.getBody(), StandardCharsets.UTF_8);
        log.debug("redis key 过期:pattern={},channel={},key={}", new String(pattern), channel, key);
    }
}

ps:做完之后感觉挺简单,实际上最开始弄的时候是找的监听指定的 db,在网上查了半天,找了很多方法,结果都没生效,最后才找到的这个,特此记录。

参考文章

Q.E.D.

知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议

一介死肥宅程序员,希望世界和平