延时任务的解决方案之一

本文介绍通过 redis 的 键空间消息(Redis Keyspace Notifications) 结合 Subscribe 完成延时任务。

相关概念

发布/订阅(publish/subscribe)模式

发布/订阅模式可以实现进程间的消息传递,“发布/订阅”模式包含两种角色:发布者和订阅者。订阅者可以订阅一个或多个频道(channel),而发布者可以向指定的频道发送消息,所有订阅此频道的订阅者都会受到此消息。

而在延时任务的场景中,延时任务的产生就相当于发布者,订阅者根据接收到的消息执行延时任务。

键空间消息

功能描述

Keyspace notifications is a feature available since 2.8.0. It allow clients to subscribe to Pub/Sub channels in order to receive events affecting the Redis data set in some way.

键空间消息允许客户端订阅“发布频道”或“订阅频道”,以便接收以某种方式影响Redis数据集的事件。具体的方式有(key指redis数据结构中的键):

  • 所有影响给定key值的命令
  • 所有接收LPUSH操作的key值
  • 所有在database 0 中过期的key值

事件类型

键空间消息是通过给每一个影响Redis数据空间的操作发送两种不同类型的事件来实现的。

1
2
PUBLISH __keyspace@0__:mykey del # 被称为Key-space notification
PUBLISH __keyevent@0__:del mykey # 被称为Key-event notification

当然,为了发送特定的子事件,我们也可以仅使用一种事件消息类型。

启用配置

启用该功能会消耗一些CPU,所以默认状态是关闭的。可以通过设置 redis.conf 中的 notify-keyspace-events 为下列组合值时即可启用特定的事件通知类型:对于延时任务而言,设定值为 Ex 足矣。

1
2
3
4
5
6
7
8
9
10
11
12
K     Keyspace events, published with __keyspace@<db>__ prefix.
E Keyevent events, published with __keyevent@<db>__ prefix.
g Generic commands (non-type specific) like DEL, EXPIRE, RENAME, ...
$ String commands
l List commands
s Set commands
h Hash commands
z Sorted set commands
t Stream commands
x Expired events (events generated every time a key expires)
e Evicted events (events generated when a key is evicted for maxmemory)
A Alias for g$lshztxe, so that the "AKE" string means all the events.

过期事件的时间

Redis通过两种方式设置有效的key过期:

  • 当某个命令去访问该key却发现已经过期了
  • 通过后台系统寻找过期的key,逐一以便收集从未访问过的key

上述两种方式都不保证产生过期事件的时间与key的生存时间恰好为0完全一致。从本质上说,过期事件的产生是Redis服务器删除key,而不是key的生存事件为0,所以产生时间差是在所难免的。

代码实现

创建key值并设置过期时间:Produce.php

1
2
3
4
5
$redis = new \Redis();
$redis->connect('127.0.0.1', 6379);
// $redis->select('15'); // 默认是 0
$redis->set('age','20');
$redis->expire('age',20);

延时任务执行脚本:Task.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
#! /usr/local/php/bin/php
// 必须加这个,避免redis连接超时终端,导致退出监听
ini_set('default_socket_timeout', -1);
$redis = new \Redis();
$res = $redis->connect('127.0.0.1', 6379);

// $redis->select('15'); // 选择数据库
// $redis->psubscribe(array('__keyevent@15__:expired'), 'psCallback');

$redis->psubscribe(array('__keyevent@0__:expired'), 'psCallback');

function psCallback($redis, $pattern, $chan, $msg)
{
echo "Pattern: $pattern\n";
echo "Channel: $chan\n";
echo "Payload: $msg\n\n";

// 执行延时任务
}

执行步骤

  1. Linux终端先执行 php Task.php 开始监听
  2. 浏览器访问 Produce.php 文件设置 key 和 过期时间
  3. 20s 后输出信息
1
2
3
Pattern: __keyevent@15__:expired
Channel: __keyevent@15__:expired
Payload: age // Payload的值为删除的 key