Redis的Keyspace notifications功能
1. 引入背景
公司内部有一套通讯系统,用于做内网和和外网的微服务网络打通(通过建立socket连接);为了维护通讯的稳定在通讯终端中断时候快速继续报警或者重连,所以打算客户端引入了心跳包机制,定期向服务端推送心跳包。
开始的时候我们通过定时器轮询轮询服务最后的心跳时间,进行判断连接是否断开:
但是后面我们发现这样的做法还是有点不好的地方:
- 当保存Map的连接对象(即维护的连接数变大后),遍历一次的时间开始慢慢变长,从而导致实时性变差
- 定时器的时间不好设置,设置太快了性能牺牲太大了,太慢了实时性也降低了
所以我们尝试寻找一个第三方服务,可以实现到过期后自动触发一个事件。
这个我们想到了2个方向:
- 通过mq做延时队列;
- redis 的事件推送功能;
我们选择了后者。
2. Redis的Keyspace notifications功能介绍
在Redis 2.8.0版本起,加入了“Keyspace notifications”(即“键空间通知”)的功能。
官网描述: 键空间通知,允许Redis客户端从“发布/订阅”通道中建立订阅关系,以便客户端能够在Redis中的数据因某种方式受到影响时收到相应事件。
其实根据描述我们并不难理解: 当redis中某个key发生了某种变化(某个事件),系统会将这个事件推送到我们指定的事件监听的进程中;
回归上述背景: 如果我们把心跳放到redis中缓存起来,通过订阅关系,当key 过期时候,redis会推送一条信息到事件监听的客户端;然后通过分析消息可以得知何种消息,分析消息内容可以知道是哪个key失效了。这样就可以间接实现开头所描述的功能。
3. Redis的Keyspace notifications功能使用
Keyspace notifications 功能默认是关闭的(默认地,Keyspace 时间通知功能是禁用的,因为它或多或少会使用一些CPU的资源),我们需要打开它。打开的方法也很简单,配置属性:notify-keyspace-events
redis.conf
notify-keyspace-events Ex
修改配置后,重启redis服务
添加过期事件订阅 开启一个终端,redis-cli 进入 redis 。开始订阅所有操作,等待接收消息。
vagrant@homestead ~ redis-cli
127.0.0.1:6379> psubscribe __keyevent@0__:expired
Reading messages... (press Ctrl-C to quit)
1) "psubscribe"
2) "__keyevent@0__:expired"
3) (integer) 1
再开启一个终端,redis-cli 进入 redis,新增一个 20秒过期的键:
127.0.0.1:6379> SETEX test 123 20
OK
另外一边执行了阻塞订阅操作后的终端,20秒过期后有如下信息输出:
1) "pmessage"
2) "__keyevent@0__:expired"
3) "__keyevent@0__:expired"
4) "test"
说明:说明对过期Key信息的订阅是成功的。
4. 用golang写了个简单监听Demo
package main
import (
"fmt"
"github.com/gomodule/redigo/redis"
"strconv"
"time"
"unsafe"
)
type PSubscribeCallback func (pattern, channel, message string)
type PSubscriber struct {
client redis.PubSubConn
cbMap map[string]PSubscribeCallback
}
func PConnect(ip, password string, port uint16) redis.Conn {
conn, err := redis.Dial("tcp", ip + ":" + strconv.Itoa(int(port)))
if err != nil {
print("redis dial failed.")
}
conn.Do("AUTH",password)
return conn
}
func (c *PSubscriber) ReceiveKeySpace(conn redis.Conn) {
c.client = redis.PubSubConn{conn}
c.cbMap = make(map[string]PSubscribeCallback)
go func() {
for {
switch res := c.client.Receive().(type) {
case redis.Message:
pattern := &res.Pattern
channel := &res.Channel
message := (*string)(unsafe.Pointer(&res.Data))
c.cbMap[*channel](*pattern, *channel, *message)
case redis.Subscription:
fmt.Printf("%s: %s %d\n", res.Channel, res.Kind, res.Count)
case error:
print("error handle...")
continue
}
}
}()
}
const expired = "__keyevent@0__:expired"
func (c *PSubscriber)Psubscribe() {
err := c.client.PSubscribe(expired)
if err != nil{
print("redis Subscribe error.")
}
c.cbMap[expired] = PubCallback
}
func PubCallback(patter , channel, msg string){
print( "PubCallback patter : " + patter + " channel : ", channel, " message : ", msg)
// TODO:拿到msg后进行后续的业务代码
}
func main() {
var sub PSubscriber
conn := PConnect("192.168.11.213","", 6379)
sub.ReceiveKeySpace(conn)
sub.Psubscribe()
for{
time.Sleep(time.Second)
}
}
5. Redis的Keyspace notifications功能注意事项
1. 过期事件:
- 当某个命令访问该密钥并发现该密钥已过期时。
- 通过后台系统,在后台逐步查找过期密钥,以便能够收集从未访问过的密钥。
官网文档可知
The expired events are generated when a key is accessed and is found to be expired by one of the above systems, as a result there are no guarantees that the Redis server will be able to generate the expired event at the time the key time to live reaches the value of zero.
If no command targets the key constantly, and there are many keys with a TTL associated, there can be a significant delay between the time the key time to live drops to zero, and the time the expired event is generated.
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.
其实这个不难理解:过期事件的触发,是在过期key被redis回收时才会触发 expired 推送。。即其实 Keyspace notifications 功能 redis 也无法保证百分的实时性(具体原因可查考 redis 的回收机制 GC),但如果key不是特别特别多的时候,这个时候实时性还是挺高的。
2. 事件丢失
当监听断开后,事件没有推送的端,当重新监听时候不会重新推送;这个如果需要做持久化的话可能还需要 依赖于 其他服务,比如:mysql等。。