业务背景
在线聊天页面,当用户离开一定时间后,会触发超时结束会话的一个逻辑。
redis键空间通知
键空间通知, redis 2.8版本才支持的特性,当redis的key被删除或者过期后,会触发两种不同类型的事件通知
__keyspace@0__:mykey del
__keyevent@0__:del mykey
★订阅第一个频道 keyspace@0:mykey 可以接收 0 号数据库中所有修改键 mykey 的事件, 而订阅第二个频道 keyevent@0:del 则可以接收 0 号数据库中所有执行 del 命令的键。
”
开启键空间通知功能需要消耗一些 CPU , 所以在默认配置下, 该功能处于关闭状态
需要在redis.conf
中开启该配置或者直接使用CONFIG SET
命令
搬运一张图:
如果要开启键过期事件通知,可以在配置中写入notify-keyspace-events EX
。
至此,redis的配置改完。
然后在eggjs中,增加如下配置
const { redisNodes } = this.app.config;
redisNodes.forEach(node => { const sub = new Redis(node); // 保证连接成功后再去监听 sub.once('connect', () => { // 只订阅过期事件 sub.subscribe('__keyevent@0__:expired'); // 订阅消息回调 sub.on('message', async (channel, message) => { }); }); });
因为公司的redis服务是3主3从的架构,所以这里的redis是多台服务器,需要遍历去监听,因为这个订阅事件的回调,只会回调给当前监听的redis机器,集群模式下无法监听到,所以,并不能直接在eggjs中直接使用配置好的this.app.redis
这个变量去监听服务。
分布式锁
上面实现了redis键空间通知,但是第二个问题随之而来,node服务在两台不同的机子上,redis服务也在两台不同的机子上,因为我们开启监听是每台node服务都会去开启不同的redis服务监听,所以实际上会有一个资源抢占的现象,因为回调会同时会被触发,此时就需要用到分布式锁的概念了。
★redis的命令是原子性的
”
所谓原子性是说是:一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成
增加如下代码:
const sub = new Redis(node); // 保证连接成功后再去监听 sub.once('connect', () => { // 只订阅过期事件 sub.subscribe('__keyevent@0__:expired'); // 订阅消息回调 sub.on('message', async (channel, message) => { console.log('channel', channel, 'message:', message); // 如果不是指定的事件,直接返回 if (!message.startsWith('chat.leave')) { return; } // 实现分布式锁,因为node服务在两台机器上110,111 两台机器都会监听两个redis服务器比如,95和96 // redis在回调的时候,由于两台node服务器都监听这95,96端口,所以会去抢占谁去获得这个资源的执行权 const data = await this.app.redis.set(`__@NX_${message}`, '1', 'NX', 'EX', 10); // 正是因为同时做了set操作,这个操作只会让一台机器成功,其它的机器设置data都会报null if (data) { // 3. 业务处理
} }); });
注释写得比较清楚,主要是在set一个值的时候,redis不会让同一个key同时设置两次,主要是NX
这个命令起作用,当一台机器抢占到执行权时,另外一台机子set这个方法,就会返回null。
至此,就简单的实现了分布式锁的功能了。
其实关于键空间通知还有其它一些场景,比如:
- 订单超时自动取消
- 用户超过7天不登录短信提醒
- ...
本文使用 mdnice 排版