Redis的发布订阅模式

  我们知道,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")
}


后记
  个人总结,欢迎转载、评论、批评指正

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值