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 等。。