我们知道,Redis经常被用来作为缓存服务器,其实除了缓存,redis也有发布订阅的功能,今天我们简单了解一下redis的发布订阅模式的功能和使用方法
一、基本原理
发布订阅模式,是一种消息传递的机制,其基本原理就是消息的发送者并不会直接发消息给消息的接受者,而是将消息发送给一个中间件,消息的接受者从中间件中间接受消息,这样就完成了一次消息的发送和接收。
发布订阅模式的好处是将消息发送者和接收者进行了隔离,这样可以灵活的扩展消息的接受者。起到将发送者和消息接收者解耦的目标。
在 Redis 中,发布订阅模式有两个主要的角色:发布者和订阅者。
- 发布者通过 PUBLISH 命令向指定的频道发送消息,
- 订阅者则通过 SUBSCRIBE 命令订阅/取消订阅指定的频道,并通过监听器(Callback)接收到发布者发送的消息。
发布订阅模式的结构图如下所示:
二、Redis发布订阅的常用命令
Redis 发布订阅是一种消息通信模式,通过这种模式可以让多个客户端之间进行消息的发布和订阅。Redis 提供了以下几个命令来实现发布订阅的功能:
1、PUBLISH channel message:将消息 message 发送到指定的频道 channel 中,返回值为接收到消息的订阅者数量。
2、SUBSCRIBE channel [channel …]:订阅一个或多个频道 channel,每当有新消息发布到订阅的频道时,就会收到相应的消息。
3、UNSUBSCRIBE [channel [channel …]]:取消订阅一个或多个频道 channel,如果不指定 channel,则取消订阅所有频道。
4、PSUBSCRIBE pattern [pattern …]:订阅一个或多个符合指定模式 pattern 的频道,每当有新消息发布到符合模式的频道时,就会收到相应的消息。
5、PUNSUBSCRIBE [pattern [pattern …]]:取消订阅一个或多个符合指定模式 pattern 的频道,如果不指定 pattern,则取消订阅所有模式。
6、PUBSUB subcommand [argument [argument …]]:查看订阅与发布系统状态,可以用来获取订阅与发布系统的各种信息,比如订阅者数量、频道列表等等。 其中,PUBLISH
命令用于向指定的频道发布消息,SUBSCRIBE 命令用于订阅一个或多个频道,PSUBSCRIBE
命令用于订阅一个或多个符合指定模式的频道,PUBSUB 命令用于查看订阅与发布系统状态。
三、发布订阅模式测试(基于命令行)
我们用命令行启动3个客户端,分别订阅对应的通道,然后再启动一个客户端作为发送者
1、基于频道(Channel)的发布/订阅
我们新建一个特定的通道test,来测试模式的使用,以下是三个客户端订阅消息的图:
我们再启动一个发送者的客户端,像发送者发送消息:
此时接受者们收到的消息如下:
2、基于模式(Pattern)的发布/订阅
基于模式的发布订阅,指的是一种特殊的频道,订阅的通道可以是包含通配符的名称,这样的话它就可以接收到能够匹配到的所有的频道的消息,在这个测试中,我们新建三个客户端,分别订阅test.*,test.news.*和test.news.hello三个频道,订阅的客户端如下:
用一个客户端分别给test,test.news.和test.news.hello发送消息
接收者接收到的消息如下:
从上面的结果中可以看出,订阅模式最大的test.*可以收到三次发送的消息,而最小的test.news.hello的只能接收到发送给自己的一次消息
四、go语言实现连接redis的发布订阅模式
我们编写一个订阅的客户端函数,启3个协程模拟3个客户端来订阅从redis中接收到的消息,代码如下:
package main
import (
"context"
"fmt"
"github.com/go-redis/redis/v8"
)
func main() {
go subscribeMessage("client1")
go subscribeMessage("client2")
go subscribeMessage("client3")
select {}
}
func subscribeMessage(clientName string) {
// 建立redis连接
ctx := context.Background()
client := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})
// 订阅频道
channel := "test"
pubsub := client.Subscribe(ctx, channel)
defer pubsub.Close()
// 接受消息
for {
message, err := pubsub.ReceiveMessage(ctx)
if err != nil {
fmt.Println("receive message error", err)
continue
}
fmt.Println(clientName, " - Receive message: ", message.Payload)
}
}
编写一个发布端,模拟发送消息:
package main
import (
"context"
"fmt"
"github.com/go-redis/redis/v8"
)
func main() {
// 建立redis连接
ctx := context.Background()
client := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})
// 发布消息
channel := "test"
message := "hello world !!! "
result := client.Publish(ctx, channel, message)
if result.Err() != nil {
fmt.Println("failed to publish message : ", result.Err().Error())
return
}
// 获取发送结果
fmt.Println("publish result : ", result.Val())
}
当运行程序以后,接收端的运行结果如下:
服务端运行结果如下:
五、用go语言编写一个redis订阅发布的类
我们用go实现一个可以注册回调函数实现多个客户端订阅消息的类,RedisClient.go代码如下:
package redis
import (
"context"
"fmt"
"log"
"reflect"
"runtime"
"strings"
"github.com/go-redis/redis/v8"
)
var Client *redis.Client
var ctx context.Context
type SubscribeCallback func(message *redis.Message)
func init() {
// 初始化redis客户端
Client = redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "",
DB: 0,
})
ctx = context.Background()
}
// 注册订阅
func RegisterSubscribe(channel string, callback SubscribeCallback) {
if callback == nil {
log.Print("register callback is nil .")
return
}
if channel == "" || strings.Trim(channel, " ") == "" {
log.Print("register channel is empty .")
return
}
pubSub := Client.Subscribe(ctx, channel)
// 接收订阅的信息
go receiveSubscribeMessage(channel, pubSub, callback)
}
// 发布消息
func PublishMessage(channel string, message interface{}) {
result := Client.Publish(ctx, channel, message)
if result.Err() != nil {
log.Print(fmt.Sprintf("failed to publish message , errMsg=%s \n", result.Err().Error()))
return
}
log.Printf("publish message success. result=%d \n", result.Val())
}
func receiveSubscribeMessage(channel string, pubSub *redis.PubSub, callback SubscribeCallback) {
callbackName := runtime.FuncForPC(reflect.ValueOf(callback).Pointer()).Name()
log.Print(fmt.Sprintf("wait subscribe message from redis:channel=%s ,callbackName=%s \n", channel, callbackName))
defer func(pubSub *redis.PubSub) {
_ = pubSub.Close()
}(pubSub)
// 接受消息
for {
message, err := pubSub.ReceiveMessage(ctx)
if err != nil {
log.Print(fmt.Sprintf("receive message error. err=%s \n", err.Error()))
continue
}
// 调用回调函数
go safeSubscribeCallback(channel, message, callback)
}
}
func safeSubscribeCallback(channel string, message *redis.Message, callback SubscribeCallback) {
callbackName := runtime.FuncForPC(reflect.ValueOf(callback).Pointer()).Name()
defer func() {
if err := recover(); err != nil {
log.Print(fmt.Sprintf("invoke receive callback error.channel = %s.callbackName=%s,errMsg=%s \n",
channel, callbackName, err))
}
}()
log.Print(fmt.Sprintf("invoke callback,callbackName=%s", callbackName))
callback(message)
}
UT测试类RedisClient_test.go如下:
package redis
import (
"fmt"
"strconv"
"testing"
"github.com/go-redis/redis/v8"
)
func Test_register_empty_channel(t *testing.T) {
defer func() {
if err := recover(); err != nil {
t.Error(err)
}
}()
RegisterSubscribe("", func(message *redis.Message) {
t.Log(message)
})
}
func Test_register_nil_callback(t *testing.T) {
defer func() {
if err := recover(); err != nil {
t.Error(err)
}
}()
RegisterSubscribe("test", nil)
}
// fuzz测试两个客户端接受随机消息
func Fuzz_publish_and_register(f *testing.F) {
publishSlice := []string{"aaa", "bbb", "ccc"}
for _, value := range publishSlice {
f.Add(value)
}
defer func() {
if err := recover(); err != nil {
f.Error(err)
}
}()
// 注册两个客户端
channel := "test_channel"
RegisterSubscribe(channel, func(message *redis.Message) {
fmt.Printf("client [1] recive message:channel=%s ,Payload=%s \n", message.Channel, message.Payload)
})
RegisterSubscribe(channel, func(message *redis.Message) {
fmt.Printf("client [2] recive message:channel=%s ,Payload=%s \n", message.Channel, message.Payload)
})
f.Fuzz(func(t *testing.T, str string) {
PublishMessage(channel, str)
})
}
// 测试当回调函数报错返回时,会不会影响第二次发送
func Test_callback_error(t *testing.T) {
defer func() {
if err := recover(); err != nil {
t.Error(err)
}
}()
// 注册一个客户端,其中客户端回调会报错
channel := "test_channel"
RegisterSubscribe(channel, func(message *redis.Message) {
fmt.Printf("client [1] recive message:channel=%s ,Payload=%s \n", message.Channel, message.Payload)
i, err := strconv.Atoi(message.Payload)
if err != nil {
fmt.Println("strconv error")
panic(err)
}
fmt.Printf("receive message = %d \n", i)
})
PublishMessage(channel, "aaaa")
PublishMessage(channel, "123")
}
后记
个人总结,欢迎转载、评论、批评指正