redigo订阅发布以及stream
redis 订阅发布
Redis 订阅发布(Pub/Sub)是一种消息传递模式,用于实现消息的发布和订阅。在这种模式下,消息的发送者称为发布者(Publisher),而消息的接收者称为订阅者(Subscriber)。发布者将消息发送到一个特定的频道(Channel),而订阅者则可以订阅一个或多个频道,以接收发布者发送的消息。
为了支持消息的多播机制,redis 引入了发布订阅模块;
生产者生产一次消息,由redis负责将消息复制到多个消息队列中,每个消息队列由相应的消费者进行消费;
它是分布式系统中常用的一种解耦方式,用于将多个消费者的逻辑进行拆分。多个消费者的逻辑就可以放到不同的子系统中完成;
Redis 的发布订阅模式具有以下特点:
- 一对多的消息传递:发布者可以将消息发送给多个订阅者,从而实现一对多的消息传递。
- 松耦合:发布者和订阅者之间是松耦合的关系,它们不需要直接知道彼此的存在,而是通过共享的频道进行通信。
- 实时性:消息是实时发送的,订阅者可以立即收到发布者发送的消息。
- 动态扩展:可以动态地添加和移除订阅者,以适应系统的变化。
Redis 的发布订阅模式使用以下几个命令来实现:
-
PUBLISH channel message:发布一条消息到指定的频道。
-
SUBSCRIBE channel [channel …]:订阅一个或多个频道,接收发布者发送的消息。
-
UNSUBSCRIBE [channel [channel …]]:取消订阅一个或多个频道,停止接收发布者发送的消息。
-
PSUBSCRIBE pattern [pattern …]:订阅与给定模式匹配的一个或多个频道。
-
PUNSUBSCRIBE [pattern [pattern …]]:取消订阅与给定模式匹配的一个或多个频道。
以下是一个简单的示例,演示了如何在 Redis 中使用发布订阅模式:
发布者发送消息到频道:
127.0.0.1:6379> PUBLISH channel1 "Hello, subscribers!"
(integer) 1
订阅者订阅频道,并接收消息:
127.0.0.1:6379> SUBSCRIBE channel1
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "channel1"
3) (integer) 1
1) "message"
2) "channel1"
3) "Hello, subscribers!"
在这个示例中,发布者将消息 “Hello, subscribers!” 发送到频道 “channel1”,而订阅者订阅了 “channel1” 频道,并成功接收到了发布者发送的消息。
缺点
发布订阅的生产者传递过来一个消息,redis会直接找到相应的消费者并传递过去;假如没有消费
者,消息直接丢弃;假如开始有2个消费者,一个消费者突然挂掉了,另外一个消费者依然能收到
消息,但是如果刚挂掉的消费者重新连上后,在断开连接期间的消息对于该消费者来说彻底丢失
了;
另外,redis停机重启,pubsub的消息是不会持久化的,所有的消息被直接丢弃;
stream
Redis Stream 是 Redis 5.0 引入的新数据结构,用于支持高性能的消息流处理。它提供了**持久化的、有序的、可扩展的消息传递机制,**适用于实时数据处理、消息队列、日志处理等场景。
Redis Stream 包含以下关键概念:
-
消息(Message):消息是 Stream 中的基本单元,它包含了一个唯一标识符(ID)和一个由键值对组成的数据体。消息的 ID
是递增的,确保了消息的有序性。 -
Stream:Stream 是由一系列消息组成的持久化数据流。每个 Stream 都有一个名称,可以通过该名称对 Stream进行操作。
-
消费者组(Consumer Group):消费者组是一组消费者的集合,它们共同消费一个 Stream中的消息。消费者组由一个唯一的名称标识,并维护了消费者之间的消息分配信息。
-
消费者(Consumer):消费者是消费者组中的一个成员,它负责从 Stream读取消息并进行处理。每个消费者都有一个唯一的名称,并且属于某个消费者组。
Redis 提供了一系列命令用于操作 Stream,包括:
XADD:向 Stream 中添加消息。
XLEN:获取 Stream 中的消息数量。
XRANGE、XREVRANGE:获取指定范围内的消息。
XREAD、XREADGROUP:从 Stream 中读取消息。
XGROUP:创建、销毁和管理消费者组。
XACK、XPENDING:对消息进行确认和查询未确认消息。
XDEL:删除消息。
支持多播的可持久化消息队列;
一个消息链表将加入的消息都串起来,每个消息都有一个唯一的消息ID和对应的内容;消息都是持久化的,redis 重启后,内容还在;
每个 stream 对象通过一个 key 来唯一索引;每个 stream 都可以挂多个消费组(consumer group),每个消费组会有个游标 last_delivered_id 在 stream 数组之上往前移动,表示当前消费
组已经消费到哪条消息了。
stream 在第一次使用 xadd 命令后自动创建;而消费组不会自动创建,需要通过命令 xgroup create 进行创建,并且需要指定从 stream 的某个消息 ID 开始消费;
每个消费组都是相互独立的,互相不受影响;也就是同一份 stream 内部的消息会被每个消费组都消费到;
同一个消费组可以挂接多个消费者,这些消费者之间是竞争关系,任意一个消费者读取了消息都会使游标往前移动;
消费者内部会有一个状态变量 pending_ids,它记录了当前已经被客户端读取,但是还没有 ack 的消息。当客户端 ack 一条消息后,pending_ids 将会删除该消息 ID;它用来确保客户端至少消费了消息一次,而不会在网络传输的中途丢失了而没有被处理;
# 向 stream 追加消息
XADD key ID field string [field string ...]
# 从 stream 中删除消息
XDEL key ID [ID ...]
# 获取 stream 中消息列表,会自动过滤已经删除的消息
XRANGE key start end [COUNT count]
# 获取 stream 消息长度
XLEN key
# 删除 stream 中所有消息
DEL key
# 独立消费
XREAD [COUNT count] [BLOCK milliseconds] STREAMS key [key ...] ID [ID ...]
# 创建消费者
XGROUP [CREATE key groupname id-or-$] [SETID key id-or-$] [DESTROY key
groupname] [DELCONSUMER key groupname consumername]
# 消费消息
XREADGROUP GROUP group consumer [COUNT count] [BLOCK milliseconds] STREAMS
key [key ...] ID [ID ...]
# > 意味着消费者希望只接收从未发送给任何其他消费者的消息。最新的消息
# 任意其他id 发送待处理的消息
# 确认消费消息
XACK key group ID [ID ...]
例子:
package main
import (
"context"
"fmt"
"time"
"github.com/go-redis/redis/v8"
)
func main() {
// 创建 Redis 客户端连接
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379", // Redis 服务器地址
})
// 定义消息发送函数
sendMessage := func(ctx context.Context, streamName string, message map[string]interface{}) error {
// 将消息发送到指定的 Stream
_, err := rdb.XAdd(ctx, &redis.XAddArgs{
Stream: streamName, // Stream 名称
Values: message, // 消息内容
}).Result()
return err
}
// 定义消息接收函数
receiveMessages := func(ctx context.Context, streamName string, consumerGroup string, consumerName string) {
for {
// 从 Stream 中读取消息
msgs, err := rdb.XReadGroup(ctx, &redis.XReadGroupArgs{
Group: consumerGroup, // 消费者组名称
Consumer: consumerName, // 消费者名称
Streams: []string{streamName, ">"}, // 读取指定 Stream 中的消息
Count: 10, // 最多读取 10 条消息
Block: 0, // 阻塞时间,0 表示一直阻塞直到有消息到达
}).Result()
if err != nil {
fmt.Println("Error reading messages:", err)
continue
}
// 处理接收到的消息
for _, msg := range msgs {
stream := msg.Stream
for _, message := range msg.Messages {
id := message.ID
data := message.Values
fmt.Printf("Received message from stream %s, ID: %s, Data: %v\n", stream, id, data)
}
}
}
}
// 创建一个新的消费者组
const streamName = "my_stream"
const consumerGroup = "my_consumer_group"
const consumerName = "my_consumer"
ctx := context.Background()
if err := rdb.XGroupCreate(ctx, streamName, consumerGroup, "$").Err(); err != nil {
fmt.Println("Error creating consumer group:", err)
return
}
// 启动消息接收协程
go receiveMessages(ctx, streamName, consumerGroup, consumerName)
// 发送一些测试消息
for i := 1; i <= 5; i++ {
message := map[string]interface{}{
"number": i,
"time": time.Now().Unix(),
}
if err := sendMessage(ctx, streamName, message); err != nil {
fmt.Println("Error sending message:", err)
return
}
fmt.Println("Sent message:", message)
time.Sleep(time.Second)
}
// 等待接收消息
select {}
}
创建了一个简单的 Redis 客户端,然后定义了一个消息发送函数 sendMessage 和一个消息接收函数 receiveMessages。在 main 函数中,我们创建了一个名为 my_stream 的 Stream,并创建了一个消费者组 my_consumer_group,然后启动了一个消息接收协程,并在主线程中发送了一些测试消息。消息接收协程会一直阻塞并等待接收消息。