31 实战:定时任务案例 我在开发的时候曾经遇到了这样一个问题,产品要求给每个在线预约看病的患者,距离预约时间的前一天发送一条提醒推送,以防止患者错过看病的时间。这个时候就要求我们给每个人设置一个定时任务,用前面文章说的延迟队列也可以实现,但延迟队列的实现方式需要开启一个无限循环任务,那有没有其他的实现方式呢?

答案是肯定的,接下来我们就用 Keyspace Notifications(键空间通知)来实现定时任务,定时任务指的是指定一个时间来执行某个任务,就叫做定时任务

开启键空间通知

默认情况下 Redis 服务器端是不开启键空间通知的,需要我们手动开启。

键空间开启分为两种方式:

  • 命令设置方式
  • 配置文件设置方式

接下来,我们分别来看。

命令设置方式

使用 redis-cli 连接到服务器端之后,输入

config set notify-keyspace-events Ex 命令,可以直接开启键空间通知功能,返回“OK”则表示开启成功,如下命令所示: 127.0.0.1:6379> config set notify-keyspace-events Ex OK

优点:

  • 设置方便,无序启动 Redis 服务。

缺点:

  • 这种方式设置的配置信息是存储在内存中的,重启 Redis 服务之后,配置项会丢失。

配置文件设置方式

找到 Redis 的配置文件 redis.conf,设置配置项

notify-keyspace-events Ex ,然后重启 Redis 服务器。

优点:

  • 无论 Redis 服务器重启多少次,配置都不会丢失。

缺点:

  • 需要重启 Redis 服务。

配置说明

可以看出无论是那种方式,都是设置 notify-keyspace-events Ex,其中 Ex 表示开启键事件通知里面的 key 过期事件。

更多配置项说明如下:

  • K:键空间通知,所有通知以

__keyspace@__ 为前缀

  • E:键事件通知,所有通知以

__keyevent@__ 为前缀

  • g:DEL、EXPIRE、RENAME 等类型无关的通用命令的通知
  • $:字符串命令的通知
  • l:列表命令的通知
  • s:集合命令的通知
  • h:哈希命令的通知
  • z:有序集合命令的通知
  • x:过期事件,每当有过期键被删除时发送
  • e:驱逐(evict)事件,每当有键因为 maxmemory 政策而被删除时发送
  • A:参数 g$lshzxe 的别名

以上配置项可以自由组合,例如我们订阅列表事件就是 El,但需要注意的是,如果 notify-keyspace-event 的值设置为空,则表示不开启任何通知,有值则表示开启通知

功能实现

我们要实现定时任务需要使用 Pub/Sub 订阅者和发布者的功能,使用订阅者订阅元素的过期事件,然后再执行固定的任务,这就是定时任务的实现思路。

以本文开头的问题为例,我们是这样实现此定时任务的,首先根据每个患者预约的时间往前推一天,然后再计算出当前时间和目标时间(预约前一天的时间)的毫秒值,把这个值作为元素的过期时间设置到 Redis 中,当这个键过期的时候,我们使用订阅者模式就可以订阅到此信息,然后再发提醒消息给此用户,这样就实现了给每个患者开启一个单独的分布式定时任务的功能。

我们先用命令的模式来模拟一下此功能的实现,首先,我们使用 redis-cli 开启一个客户端,监听

keyevent@0:expired 键过期事件,此监听值

keyevent@0:expired 为固定的写法,其中 0 表示第一个数据库,我们知道 Redis 中一共有 16 个数据,默认使用的是第 0 个,我们建议新开一个非 0 的数据库专门用来实现定时任务,这样就可以避免很多无效的事件监听。

命令监听如下: 127.0.0.1:6379> psubscribe keyevent@0:expired 1) “psubscribe” 2) “keyevent@0:expired” 3) (integer) 1

此时我们开启另一个客户端,添加两条测试数据试试,命令如下:

127.0.0.1:6379> set key value ex 3 OK 127.0.0.1:6379> set user xiaoming ex 3 OK

等过去 3 秒钟之后,我们去看监听结果如下:

127.0.0.1:6379> psubscribe keyevent@0:expired 1) “psubscribe” 2) “keyevent@0:expired” 3) (integer) 1 1) “pmessage” 2) “keyevent@0:expired” 3) “keyevent@0:expired” 4) “key” /#接收到过期信息 key 1) “pmessage” 2) “keyevent@0:expired” 3) “keyevent@0:expired” 4) “user” /#接收到过期信息 user

已经成功的介绍到两条过期信息了。

代码实战

本文我们使用 Jedis 来实现定时任务,代码如下: import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPubSub; import utils.JedisUtils; /// /* 定时任务 // public class TaskExample { public static final String _TOPIC = “keyevent@0:expired”; // 订阅频道名称 public static void main(String[] args) { Jedis jedis = JedisUtils.getJedis(); // 执行定时任务 doTask(jedis); } ///* /* 订阅过期消息,执行定时任务 /* @param jedis Redis 客户端 /*/ public static void doTask(Jedis jedis) { // 订阅过期消息 jedis.psubscribe(new JedisPubSub() { @Override public void onPMessage(String pattern, String channel, String message) { // 接收到消息,执行定时任务 System.out.println(“收到消息:” + message); } }, _TOPIC); } }

小结

本文我们通过开启 Keyspace Notifications 和 Pub/Sub 消息订阅的方式,可以拿到每个键值过期的事件,我们利用这个机制实现了给每个人开启一个定时任务的功能,过期事件中我们可以获取到过期键的 key 值,在 key 值中我们可以存储每个用户的 id,例如“user_1001”的方式,其中数字部分表示用户的编号,通过此编号就可以完成给对应人发送消息通知的功能。

参考资料

https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/Redis%20%e6%a0%b8%e5%bf%83%e5%8e%9f%e7%90%86%e4%b8%8e%e5%ae%9e%e6%88%98/31%20%e5%ae%9e%e6%88%98%ef%bc%9a%e5%ae%9a%e6%97%b6%e4%bb%bb%e5%8a%a1%e6%a1%88%e4%be%8b.md