31 实战:定时任务案例¶
我在开发的时候曾经遇到了这样一个问题,产品要求给每个在线预约看病的患者,距离预约时间的前一天发送一条提醒推送,以防止患者错过看病的时间。这个时候就要求我们给每个人设置一个定时任务,用前面文章说的延迟队列也可以实现,但延迟队列的实现方式需要开启一个无限循环任务,那有没有其他的实现方式呢?
答案是肯定的,接下来我们就用 Keyspace Notifications(键空间通知)来实现定时任务,定时任务指的是指定一个时间来执行某个任务,就叫做定时任务 。
开启键空间通知¶
默认情况下 Redis 服务器端是不开启键空间通知的,需要我们手动开启。
键空间开启分为两种方式:
- 命令设置方式
- 配置文件设置方式
接下来,我们分别来看。
命令设置方式 使用 redis-cli 连接到服务器端之后,输入 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:键空间通知,所有通知以
[[email protected]](/cdn-cgi/l/email-protection)<db>__
为前缀 - E:键事件通知,所有通知以
[[email protected]](/cdn-cgi/l/email-protection)<db>__
为前缀 - g:DEL、EXPIRE、RENAME 等类型无关的通用命令的通知
- $:字符串命令的通知
- l:列表命令的通知
- s:集合命令的通知
- h:哈希命令的通知
- z:有序集合命令的通知
- x:过期事件,每当有过期键被删除时发送
- e:驱逐(evict)事件,每当有键因为 maxmemory 政策而被删除时发送
- A:参数 g$lshzxe 的别名
以上配置项可以自由组合,例如我们订阅列表事件就是 El,但需要注意的是,如果 notify-keyspace-event 的值设置为空,则表示不开启任何通知,有值则表示开启通知 。
功能实现¶
我们要实现定时任务需要使用 Pub/Sub 订阅者和发布者的功能,使用订阅者订阅元素的过期事件,然后再执行固定的任务,这就是定时任务的实现思路。
以本文开头的问题为例,我们是这样实现此定时任务的,首先根据每个患者预约的时间往前推一天,然后再计算出当前时间和目标时间(预约前一天的时间)的毫秒值,把这个值作为元素的过期时间设置到 Redis 中,当这个键过期的时候,我们使用订阅者模式就可以订阅到此信息,然后再发提醒消息给此用户,这样就实现了给每个患者开启一个单独的分布式定时任务的功能。
我们先用命令的模式来模拟一下此功能的实现,首先,我们使用 redis-cli 开启一个客户端,监听 [[email protected]](/cdn-cgi/l/email-protection)__:expired
键过期事件,此监听值 [[email protected]](/cdn-cgi/l/email-protection)__:expired
为固定的写法,其中 0 表示第一个数据库,我们知道 Redis 中一共有 16 个数据,默认使用的是第 0 个,我们建议新开一个非 0 的数据库专门用来实现定时任务,这样就可以避免很多无效的事件监听。
命令监听如下:
127.0.0.1:6379> psubscribe [email protected]__:expired
1) "psubscribe"
2) "[email protected]__:expired"
3) (integer) 1
此时我们开启另一个客户端,添加两条测试数据试试,命令如下:
等过去 3 秒钟之后,我们去看监听结果如下:
127.0.0.1:6379> psubscribe [email protected]__:expired
1) "psubscribe"
2) "[email protected]__:expired"
3) (integer) 1
1) "pmessage"
2) "[email protected]__:expired"
3) "[email protected]__:expired"
4) "key" #接收到过期信息 key
1) "pmessage"
2) "[email protected]__:expired"
3) "[email protected]__: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 = "[email protected]__: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”的方式,其中数字部分表示用户的编号,通过此编号就可以完成给对应人发送消息通知的功能。