golang:redis订阅键空间过期消息

重要: Keyspace notifications 从2.8.0版本开始启用

概叙

键空间通知使得客户端可以通过发布订阅(Redis自带)通道,来接收那些以某种方式改动了Redis 数据集的事件。

可以接受到通知的例子:

  • 影响到键的命令,对键进行操作的命令。
  • 所有接收到 LPUSH 操作的键。
  • Redis 0 号数据库中所有已过期的键。

事件通过Redis 的订阅与发布功能来进行分发, 因此所有支持订阅与发布功能的客户端都可以在无须做任何修改的情况下, 直接使用键空间通知功能。

因为 Redis 目前的订阅与发布功能采取的是发送即忘策略, 所以如果你的程序需要可靠事件通知, 那么目前的键空间通知可能并不适合你:当订阅事件的客户端断开连接时,当再次连接上的时候, 它会丢失所有在断开连接期间分发给它的事件。

未来将会支持更可靠的事件分发,这种支持可能会通过让订阅与发布功能本身变得更可靠来实现, 也可能会在 Lua 脚本中对消息(message)的订阅与发布进行监听,从而实现类似将事件推入到列表这样的操作。

原理

对于每个修改数据库的操作,键空间通知都会发送两种不同类型的事件消息:keyspace 和 keyevent。

比如说,对 0 号数据库的键 mykey 执行 DEL 命令时, 系统将分发两条消息, 相当于执行以下两个 PUBLISH 命令:

  • PUBLISH __keyspace@0__:sampleKey del
  • PUBLISH __keyevent@0__:del sampleKey

订阅第一个频道__keyspace@0__:mykey可以接收 0 号数据库中所有修改键 mykey 的事件,
而订阅第二个频道 __keyevent@0__:del 则可以接收 0 号数据库中所有执行 del 命令的键。

以 keyspace 为前缀的频道被称为键空间通知(key-space notification), 而以 keyevent 为前缀的频道则被称为键事件通知(key-event notification)。

当 del mykey 命令执行时:

  • 键空间频道的订阅者将接收到被执行的事件的名字,在这个例子中,就是 del 。
  • 键事件频道的订阅者将接收到被执行事件的键的名字,在这个例子中,就是 mykey 。

事件是用 __keyspace@DB__:KeyPattern或者 __keyevent@DB__:OpsType的格式来发布消息的。

  • DB表示在第几个库;
  • KeyPattern则是表示需要监控的键模式(可以用通配符,如:key*😗);
  • OpsType则表示操作类型。

因此,如果想要订阅特殊的Key上的事件,应该是订阅keyspace。

当然,我们需要先进行配置

有关配置

因为开启键空间通知功能需要消耗一些CPU , 所以在默认配置下, 该功能处于关闭状态。

可以通过修改 redis.conf 文件, 或者直接使用 CONFIG SET 命令来开启或关闭键空间通知功能:

  • 当 notify-keyspace-events 选项的参数为空字符串时,功能关闭。
  • 另一方面,当参数不是空字符串时,功能开启。

notify-keyspace-events 的参数可以是以下字符的任意组合, 它指定了服务器该发送哪些类型的通知:

在这里插入图片描述
输入的参数中至少要有一个 K 或者 E , 否则的话, 不管其余的参数是什么, 都不会有任何通知被分发。上表中斜体的部分为通用的操作或者事件,而黑体则表示特定数据类型的操作。

  • 配置文件中修改 notify-keyspace-events “Kx”,注意:这个双引号是一定要的,否则配置不成功,启动也不报错。例如,“Kx”表示想监控某个Key的失效事件, 将参数设为字符串 “AKE” 表示发送所有类型的通知。

  • 或者在redis cli命令行中通过config配置:CONFIG set notify-keyspace-events EKx(但非持久化)

  • 或者直接在命令行中配置: redis-cli config set notify-keyspace-events KEA(但非持久化)

设置好之后打开一个命令行,订阅消息:

>redis-cli subscribe __keyspace@0__:cool
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "__keyspace@0__:cool"
3) (integer) 1

打开另外一个命令行,设置失效:

   setex cool 1 val                         # cool=val 1秒失效

当失效时间到了,先前的命令行会收到失效消息:

   1) "message"                               # 返回值的类型:信息
        2) "__keyspace@0__:cool”          # 来源(从哪个Channel发送过来)
        3) “expired"                                 # 信息内容

注:对于psubscribe,消息会多一行

        1) “pmessage”                            #返回值的类型:信息
        2) "__keyspace@0__:cool*”         # 来源(从哪个ChannelPattern发送过来)
        3) "__keyspace@0__:cool"          # 实际的Channel
        4) “expired"                                 # 信息内容

代码

前提:配置好了

版本1

package main

import (
	//"github.com/go-redis/redis"
	"fmt"
	"time"
	"unsafe"

	log "github.com/astaxie/beego/logs"
	"github.com/gomodule/redigo/redis"
)


type PSubscribeCallback func (pattern, channel, message string)

type PSubscriber struct {
	client redis.PubSubConn
	cbMap map[string]PSubscribeCallback
}


func (c *PSubscriber) PConnect(ip string, port uint16) {
	conn, err := redis.Dial("tcp", "127.0.0.1:6379")
	if err != nil {
		log.Critical("redis dial failed.")
	}

	c.client = redis.PubSubConn{conn}
	c.cbMap = make(map[string]PSubscribeCallback)

	go func() {
		for {
			log.Debug("wait...")
			switch res := c.client.Receive().(type) {
			case redis.Message:
				pattern := (*string)(unsafe.Pointer(&res.Pattern))
				channel := (*string)(unsafe.Pointer(&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:
				log.Error("error handle...")
				continue
			}
		}
	}()

}
func (c *PSubscriber)Psubscribe(channel interface{}, cb PSubscribeCallback) {
	err := c.client.PSubscribe(channel)
	if err != nil{
		log.Critical("redis Subscribe error.")
	}

	c.cbMap[channel.(string)] = cb
}

func TestPubCallback(patter , chann, msg string){
	log.Debug( "TestPubCallback patter : " + patter + " channel : ", chann, " message : ", msg)
}

func main() {

	log.Info("===========main start============")


	var psub PSubscriber
	psub.PConnect("127.0.0.1", 6397)
	psub.Psubscribe("__keyevent@0__:expired", TestPubCallback)
	// 还可以是: `__keyspace@0__:cool`
	for{
		time.Sleep(1 * time.Second)
	}
}
  • __keyevent@0__:expired的意思是订阅0号数据库的所有key过期消息【内部使用队列实现】
    psub.Psubscribe("__keyevent@0__:expired", TestPubCallback): 指的是订阅__keyevent@0__:expired事件【相当于命令PSUBSCRIBE __keyevent@0__:expired】,当事件发生时,调用TestPubCallback函数

  • __keyspace@0__:cool的意思是订阅0号数据库的所有有关于键cool的所有操作

版本2

package main

import (
	//"github.com/go-redis/redis"
	"fmt"
	log "github.com/astaxie/beego/logs"
	"github.com/gomodule/redigo/redis"
	"time"
)


type PSubscribeCallback func (pattern, channel, message string)

type PSubscriber struct {
	client redis.PubSubConn
	cbMap map[string]PSubscribeCallback
}


func (c *PSubscriber) PConnect(ip string, port uint16) {
	conn, err := redis.Dial("tcp", "127.0.0.1:6379")
	//conn, err := redis.Dial("tcp", ip + ":" + strconv.Itoa(int(port)))
	if err != nil {
		log.Critical("redis dial failed.")
	}

	c.client = redis.PubSubConn{conn}
	c.cbMap = make(map[string]PSubscribeCallback)

	go func() {
		for {
			log.Debug("wait...")
			switch res := c.client.Receive().(type) {
			case redis.Message:
				pattern := res.Pattern
				channel := string(res.Channel)
				message := string(res.Data)
				if (pattern == "__keyspace@0__:blog*"){
					switch  message {
					case "set":
						// do something
						fmt.Println("set", channel)
					case "del":
						fmt.Println("del", channel)
					case "expire":
						fmt.Println("expire", channel)
					case "expired":
						fmt.Println("expired", channel)
					}
				}
			case redis.Subscription:
				fmt.Printf("%s: %s %d\n", res.Channel, res.Kind, res.Count)
			case error:
				log.Error("error handle...")
				continue
			}
		}
	}()

}
func (c *PSubscriber)Psubscribe(channel interface{}, cb PSubscribeCallback) {
	err := c.client.PSubscribe(channel)
	if err != nil{
		log.Critical("redis Subscribe error.")
	}

	c.cbMap[channel.(string)] = cb
}

func TestPubCallback(patter , chann, msg string){
	log.Debug( "TestPubCallback patter : " + patter + " channel : ", chann, " message : ", msg)
}

func main() {

	log.Info("===========main start============")


	var psub PSubscriber
	psub.PConnect("127.0.0.1", 6397)
	psub.Psubscribe("__keyspace@0__:blog*", TestPubCallback)
	for{
		time.Sleep(1 * time.Second)
	}
}

redis测试

订阅__keyevent@0__:expired

1、修改配置,键空间通知功能耗费CPU,默认关闭,需要修改配置文件redis.conf或 操作CONFIG set notify-keyspace-events Ex命令,设置notify-keyspace-events选项,来启用或关闭该功能。
在这里插入图片描述
2、设置带有过期时间的key,一旦key过期,程序一端就会收到消息通知
比如:set chaijunkun 123 PX 1
在这里插入图片描述
在这里插入图片描述

订阅__keyspace@0__:cool

1、修改配置,CONFIG set notify-keyspace-events KEA命令

2、redis-cli中设置
在这里插入图片描述
3、收到消息
在这里插入图片描述

参考

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值