在实际项目开发中很多时候都会使用到定时任务的需求,举个最简单的例子,一个用户推荐了另一个用户,我们定一个二十四小时之后的任务,看看被推荐的用户有没有来注册,如果没注册就给他搞一条短信过去。
我们需要借助Redis的键空间消息Redis Keyspace Notifications,它配合 2.0.0 版本之后的 SUBSCRIBE 就能完成这个定时任务的操作了,不过定时的单位是秒。
配置
即使你的 Redis 版本达标了,但是 Redis 默认是关闭这个功能的,你需要修改配置文件来打开它,或者直接在 CLI 里面通过指令修改。这里就说说配置文件的修改吧。
首先打开 Redis 的配置文件,在不同的系统和安装方式下文件位置可能不同,比如通过brew安装的 MacOS 下可能是在/usr/local/etc/redis.conf下面,通过 apt-get安装的 Ubuntu 下可能是在 /etc/redis/redis.conf下,总之找到配置文件。
然后找到一项叫 notify-keyspace-events的地方,如果找不到则自行添加,其值可以是 Ex、Klg 等等。这些字母的具体含义如下所示:
- K,表示 keyspace 事件,有这个字母表示会往 keyspace@<db> 频道推消息。
- E,表示 keyevent 事件,有这个字母表示会往 keyevent@<db> 频道推消息。
- g,表示一些通用指令事件支持,如 DEL、EXPIRE、RENAME 等等。
- $,表示字符串(String)相关指令的事件支持。
- l,表示列表(List)相关指令事件支持。
- s,表示集合(Set)相关指令事件支持。
- h,哈希(Hash)相关指令事件支持。
- z,有序集(Sorted Set)相关指令事件支持。
- x,过期事件,与 g 中的 EXPIRE 不同的是,g 的 EXPIRE 是指执行 EXPIRE -
key ttl 这条指令的时候顺便触发的事件,而这里是指那个 key 刚好过期的这个时间点触发的事件。 - e,驱逐事件,一个 key 由于内存上限而被驱逐的时候会触发的事件。
- A,g$lshzxe 的别名。也就是说 AKE 的意思就代表了所有的事件。
结合上述列表我们就能拼凑出自己所需要的事件支持字符串了,在我的需求中我只需要 Ex 就可以满足了,所以配置项就是这样的:
notify-keyspace-events Ex
然后保存配置文件,启动 Redis 就启用了过期事件的支持了。
找的时候注意这里会有三处notify-keyspace-events,两处是注释的,有一处是没有注释值为'',把这里改为Ex即可。
实践
我们先说任务的创造者吧。由于这里 Redis 的事件只会传键名,并不会传键值,而过期事件触发的时候那个键已经没了,你也无法获取键值,加上我的主系统和任务系统是分布式的,所以就把所有需要的信息往键名塞。
如果是循环触发定时任务的话,订阅与监听的redis分开定义,不要引用同一个,不然会报错
主文件 app.js
/**
* Created by qijun on 2017/11/29.
* 简单Demo
*/
const restify = require('restify');
const redis = require('redis'); // 这里重新定义一次 与db.js中分开
const server = restify.createServer({
name: 'demo',
version: '1.0.0'
});
server.use(restify.plugins.acceptParser(server.acceptable));
server.use(restify.plugins.queryParser());
server.use(restify.plugins.bodyParser());
server.listen('3000', '0.0.0.0', function () {
console.log('本地端口监听:','3000');
});
// 定时任务Demo
const subscribeKey1 = '__keyevent@1__:expired'; // 这里要固定写法 写其他会无法触发
const redis_db1 = redis.createClient({
'host': '127.0.0.1',
'port': 6379,
'password': '123456',
'db': 1
});
redis_db1.on('connect', function() {
console.log('Redis_1连接成功');
redis_db1.subscribe(subscribeKey1, function() {
//... 订阅频道成功
console.log('Redis_1订阅成功');
});
});
redis_db1.on('error', function() {
console.log('Redis_1连接失败');
});
redis_db1.on('message', function(channel, msg) {
console.log(channel); //__keyevent@1__:expired
console.log(msg); //Set的Key
});
const routes = require('./routes')(server);
控制器 controller.js
const redis_db1 = require('../common/db').redis_1;
class TestController {
//实际业务
test(req, res) {
redis_db1.multi()
.set('Test', 'ok')
// Test为具体业务逻辑 把定时任务触发需要发生的参数以某种形式写在这里
// 一个最简单的键名设计就是 任务类型 + ":" + JSON.stringify 化后的参数数组;
.expire('Test', 10)
.exec((error) => {
if(error) {
console.log(error);
return;
}
console.log('订阅成功,10秒后生效');
});
}
}
module.exports = new TestController();
数据库
const redis = require('redis');
const client_0 = redis.createClient({
'host': '127.0.0.1',
'port': 6379
'password': '123456',
'db': 0
});
client_0.on("error", function () {
console.log('Redis_0连接失败');
});
client_0.on('connect', function () {
console.log('Redis_0连接成功');
})
const client_1 = redis.createClient({ // 这里如果是需要循环触发的话 需要在定义一次
'host': '127.0.0.1',
'port': 6379,
'password': '123456',
'db': 1
});
exports.redis_0 = client_0;
exports.redis_1 = client_1;
这样一个简单的定时任务就完成了 当我触发的时候
本地端口监听: 3000
Redis_1连接成功
Redis_0连接成功
Redis_1订阅成功
订阅成功,10秒后生效
__keyevent@1__:expired 即channel
Test 即msg 拿到之后 可实现给目标手机号发送短信功能