Redis的键空间通知实现延迟任务

为什么要用键空间通知


可以通过与Crontab的对比来得到某些场景下使用键空间通知的优势。

场景 Crontab 键空间通知(Redis Keyspace Notification)
一个30min定时提醒事项 每一分钟轮询系统,查询到时间没有,有则向系统发送通知 设置30min后过期并向系统发送通知
用户触发24h后发推送 记录触发事件,每一小时计算差值是否达到24h,有则向系统发送通知 设置24h后过期并向系统发送通知
商品下单0.5h内不支付就撤单 记录开始时间,每一分钟查询整个数据库把未支付且时间超过半小时的单改状态 设置30min后过期并向系统发送通知。订单支付成功时触发退出redis过期队列

可以看到,有一些场景下,redis的键空间通知对比Crontab优势是,把颗粒度从整个系统下降到每一个事件

Crontab因为不能定点每一个事件,所以每一次都要去查整个数据库去筛选事件,而键空间通知则是需要处理的事件自己在倒数事件,到点了就给系统发通知。这种颗粒度的下降,能够在编码的时候大大降低开发量,因为有的时候写出一个符合筛选条件的Crontab命令还是很麻烦的。


键空间通知

介绍

键空间通知功能,即Keyspace notification,是通过Redis的订阅与发布功能(Pub/Sub)来实现的,使得客户端通过订阅频道来收集一些Redis修改事件。

Redis在2.8.0版本及其以上版本才支持键空间通知(Keyspace Notification)功能。

那么收集的Redis修改事件举个例子:

  • 所有修改键的命令
  • 所有接收到LPUSH命令的键
  • 0号数据库中所有已过期的键

需要注意的是:

  • 键空间通知依赖的订阅与发布功能具有即发即忘(fire and forget)的特性,是不可靠的,所以在订阅与发布功能处于客户端断开连接,或者稍后重连,那么在失联这段时间的所有数据事件都会丢失。

特性

所有的 Redis 数据操作都会发送2种类型的事件。
举个例子,对0号数据库中一个key为mykey的键进行DEL(删除)操作,那么相当于执行了以下2个命令

1
2
PUBLISH __keyspace@0__:mykey del
PUBLISH __keyevent@0__:del mykey

第一个频道keyspace@0:mykey,是在监听0号数据库中,键值为mykey的所有修改事件,包括这里的del命令。
第二个频道keyevent@0:del,是在监听0号数据库中,所有执行del命令的键,包括这里的mykey键位。
keyspace为前缀的频道,叫做键空间通知(Key-space notification),这个频道订阅者将收到被执行事件的名字,上面例子就是del
keyevent为前缀的频道,叫做键事件通知(Key-event notification),这个频道订阅者将收到被执行事件的键的名字,上面例子就是mykey

配置

默认配置下,键空间通知功能因为会消耗一些CPU性能而被禁用,需要手动开启。
可以再redis.conf中的notify-keyspace-events中开启,或者使用CONFIG SET命令。
如果这个参数为空,那么功能关闭,否则开启。

参数如下

1
2
3
4
5
6
7
8
9
10
11
K 键空间通知(Keyspace events),通过**__keyspace@<db>__**前缀来分发信息
E 键事件通知(Keyevent events),通过**__keyevent@<db>__**前缀来分发信息
g 例如DEL, EXPIRE, RENAME等未特殊类型的常规命令
$ 字符串命令
l List命令
s Set命令
h Hash命令
z Sorted Set命令
x 过期事件(每当有事件过期被删除时发出)
e 驱逐事件(每当有键因为超过内存而被驱逐时发出)
A 是g$lshzxe(上面的所有类)的别名,所以你输入“AKE”代表监听所有事件

需要注意,参数可以随机组合配对,但是一定要有一个K或者E,不然就收不到任何一个事件的分发。


使用键空间通知来实现延迟任务


整个流程就是,我们的代码在用户操作后触发了向Redis写入有过期时间的key,同时部署一个常驻的监听客户端,监听Redis中有哪些key过期,并通过key里的信息对应出我们的业务逻辑进行下一步操作。

举个例子,用户点击拍下物品,触发后向Redis写入过期时间为30分钟的名为「order_009394」的key。那么30分钟后这个key过期,我们的监听客户端捕获到这个key,并通过key知道了具体订单ID做下一步的逻辑操作,如作废订单等等。

以下流程以PHP代码作为演示。

前期准备

  1. Redis开启了键空间通知功能,配置了参数Ex(捕获过期事件).
  2. PHP安装了扩展PHP-Redis

设置一个监听客户端

  1. 初始化一个Redis对象
  2. 订阅keyevent@0:expired频道,即监听0号数据库中过期事件键事件通知.(不了解的可以往上翻了解命令的含义)
  3. 设置回掉后的逻辑操作,这里是直接输出传入的参数。(这个psubscribe函数的回调是预设好的4个参数,你可以输出看看结果分别是什么)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
// Client.php

// Init Redis
$redis = new \Redis();
$redis->connect('127.0.0.1', '6379'); // connect('Redis服务器IP', 'Redis服务器端口')
$redis->auth('123456'); // auth('Redis服务器密码')
$redis->setOption(\Redis::OPT_READ_TIMEOUT, -1); // 设置这个避免PHP读取Redis的链接超时

// Listen to Keyspace Notification
$redis->psubscribe(['__keyevent@0__:expired'], function ($redis, $pattern, $chan, $msg) {
echo "pattern => {$pattern}" . PHP_EOL; // PHP_EOL 命令行下的换行符
echo "chan => {$chan}" . PHP_EOL;
echo "msg => {$msg}"; // $msg=>你插入的key值
});

模拟一个任务写入

  1. 初始化一个Redis对象
  2. 往Redis推入一个有过期时间TTL的键值
1
2
3
4
5
6
7
8
9
10
<?php
// Test.php

// Init Redis
$redis = new \Redis();
$redis->connect('127.0.0.1', '6379'); // connect('Redis服务器IP', 'Redis服务器端口')
$redis->auth('123456'); // auth('Redis服务器密码')

// Push A Job Into Redis
$redis->setex('order_009394', 10, 1); //(key, interval, value) => (key值,过期事件按秒计算,值内容)

观察监听客户端的结果

得到如图下结果

额外注意

Redis通过两种方式来删除一个过期的key:

  • 访问key的时候发现其过期了,将其删除
  • 为了删除一些再也不会被访问的过期键,后台会自动逐步增量的寻找过期的键。

所以过期事件并不完全等于TTL时间结束就会触发,因为过期事件是Redis清除key的时候才会触发。

引用与参考