官方文档指出:
Basically expired events are generated when the Redis server deletes the key and not when the time to live theoretically reaches the value of zero
大概意思就是:
缓存过期的通知事件是当Redis服务器删除密钥时产生的,而不是当生存时间理论上达到0的值时,所以可能存在延迟,类似于一个轮询脚本定时检测过期的缓存key并向我们发送过期事件,所以我们不能完全依赖缓存过期的回调处理需要100%准时的业务
一、配置redis
首先需要配置redis.conf配置参数【notify-keyspace-events】
因为开启键空间通知功能需要消耗一些 CPU , 所以在默认配置下, 该功能处于关闭状态。
可以通过修改 redis.conf 文件, 或者直接使用 CONFIG SET 命令来开启或关闭键空间通知功能:
- 当 notify-keyspace-events 选项的参数为空字符串时,功能关闭。
- 另一方面,当参数不是空字符串时,功能开启。
notify-keyspace-events 的参数可以是以下字符的任意组合, 它指定了服务器该发送哪些类型的通知:
字符 | 发送的通知 |
K | 键空间通知,所有通知以 __keyspace@<db>__ 为前缀 |
E | 键事件通知,所有通知以 __keyevent@<db>__ 为前缀 |
g | DEL 、 EXPIRE 、 RENAME 等类型无关的通用命令的通知 |
$ | 字符串命令的通知 |
l | 列表命令的通知 |
s | 集合命令的通知 |
h | 哈希命令的通知 |
z | 有序集合命令的通知 |
x | 过期事件:每当有过期键被删除时发送 |
e | 驱逐(evict)事件:每当有键因为 maxmemory 政策而被删除时发送 |
A | 参数 g$lshzxe 的别名 |
#修改conf配置
notify-keyspace-events Ex
#或者在reids-cli里设置
CONFIG SET notify-keyspace-events Ex
开启后打开两个面包一个开启监听一个设置过期时间的缓存
二、在php中的使用方式
下面是例子,只是一部分,我们只需要php脚本挂起这个方法就可以让他一直在后台运行
class a{
protected $_redis;
public function keyExpireSubscription()
{
$this->_redis = new Redis();
/**
* 链接redis部分就省略了
*/
try {
//设置超时,设置为-1就代表用不超时会一直监听
$this->_redis->setOption(\Redis::OPT_READ_TIMEOUT, -1); //设置不超时
//监听过期需要配置redis.conf 配置 notify-keyspace-events Ex
$this->_redis->psubscribe(['__keyevent@?__:expired'],array($this,'ExpireCallback'));
// $this->_redis->psubscribe([监听的],array(类名,方法名));
} catch (\Throwable $e) {
//抛出异常
}
}
/**
* 过期订阅回调钩子
* @date 2022/6/15
* @param $redis
* @param $pattern
* @param $chan
* @param $msg
*/
public function ExpireCallback($redis, $pattern, $chan, $msg) {
echo "Pattern: $pattern\n";
echo "Channel: $chan\n";
echo "Payload: $msg\n\n"; //过期的key名
}
}
下面是我挂起脚本监听到的返回,可以看到Payload就是过期的key
虽然我们可以拿到过期的缓存key,但是缓存错综杂乱,而且有很多是拼接的变量值,像用户id啊 日期啊,根本无法对应的缓存是什么业务(目前是没找到什么好方式),后面我突发奇想,为了给换成区分过期业务我们可以在缓存的key上加分类
比如 chicken_duck_game:*:round:* 类型的缓存,我们可以在缓存后面加一个大分类如
chicken_duck_game:*:round:*--GAME
#得到的结果就是
chicken_duck_game:2022-06-15:round:6593--GAME
chicken_duck_game:2022-06-15:round:6580--GAME
public function ExpireCallback($redis, $pattern, $chan, $msg) {
echo "Pattern: $pattern\n";
echo "Channel: $chan\n";
echo "Payload: $msg\n\n"; //过期的key名
$expire_key_type = explode('--',$msg)[1] ?? null;
//下面是每个类型对应的过期逻辑
switch ($expire_key_type) {
case 'GAME': //过期处理
break;
default:
}
}
以此来区分过期的key对应的业务逻辑。
键空间通知(keyspace notification) — Redis 命令参考
序言:
后面使用发现,在回调监控的进程中无法再次使用redis连接查询缓存,所以优化了方法。
原理:使用pcntl_fork 创建子进程,在子进程中关闭缓存连接并重新创建新的缓存连接去维持后续业务使用redis
public function ExpireCallback($redis, $pattern, $chan, $msg) {
echo "Pattern: $pattern\n";
echo "Channel: $chan\n";
echo "Payload: $msg\n\n"; //过期的key名
//使用子进程处理业务,
$pid = pcntl_fork();
if ($pid == -1) {
die("could not fork\n");
} else if ($pid) {
$childs[] = $pid;
} else {
//关闭子进程缓存
$this->_redis->close();
$expire_key_type = explode('--',$msg)[1] ?? null;
//下面是每个类型对应的过期逻辑
switch ($expire_key_type) {
case 'GAME': //过期处理
$this->_redis->get('text');
break;
default:
}
exit(0); //退出
}
while (count($childs) > 0) {
foreach ($childs as $key => $pid) {
$res = pcntl_waitpid($pid, $status, WNOHANG);
//-1代表error, 大于0代表子进程已退出,返回的是子进程的pid,非阻塞时0代表没取到退出子进程
if ($res == -1 || $res > 0)
unset($childs[$key]);
}
sleep(1);
}
}
}