Files
common/redis/redis.go
2026-03-12 08:50:59 +08:00

347 lines
10 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package redis
import (
"context"
"strconv"
"time"
"github.com/gogf/gf/v2/frame/g"
"github.com/redis/go-redis/v9"
)
var RedisClient *redis.Client
func init() {
// 从 GoFrame 配置读取 Redis 配置
ctx := context.Background()
// 读取 Redis 配置
addr := g.Cfg().MustGet(ctx, "redis.default.address").String()
password := g.Cfg().MustGet(ctx, "redis.default.pass", "").String()
db := g.Cfg().MustGet(ctx, "redis.default.db", 0).Int()
// 读取超时配置
dialTimeout := g.Cfg().MustGet(ctx, "redis.default.dialTimeout", "30s").Duration()
readTimeout := g.Cfg().MustGet(ctx, "redis.default.readTimeout", "30s").Duration()
writeTimeout := g.Cfg().MustGet(ctx, "redis.default.writeTimeout", "30s").Duration()
// 创建 Redis 客户端
RedisClient = redis.NewClient(&redis.Options{
Addr: addr,
Password: password,
DB: db,
DialTimeout: dialTimeout,
ReadTimeout: readTimeout,
WriteTimeout: writeTimeout,
// 不设置 Protocol让 go-redis 自动协商)
// Protocol: 2,
})
}
// Stream 和消费者组常量
const (
// RAGFlow 请求 Stream Key
RAGFlowRequestStreamKey = "ragflow:request:stream"
// RAGFlow 消费者组名称
RAGFlowConsumerGroup = "ragflow:consumer:group"
// 会话最后活跃时间 Key 前缀
SessionLastActiveKeyPrefix = "ragflow:session:"
)
// StreamMessage Redis Stream 消息结构
type StreamMessage struct {
ID string // 消息ID自动生成
Values map[string]interface{} // 消息内容
}
// InitStreamGroup 初始化 Stream 和消费者组
// 在应用启动时调用一次,创建 Stream 和消费者组
// 使用 GoFrame Do() 方法执行 XGROUP CREATE 命令
// 参数:
// - streamKey: Stream 键名
// - groupName: 消费者组名称
//
// 返回error 初始化失败时返回错误
func InitStreamGroup(ctx context.Context, streamKey, groupName string) error {
// 使用 XGroupCreateMkStream 创建消费者组
// 如果 Stream 不存在会自动创建 (MKSTREAM)
// "0": 从 Stream 开头开始消费
err := RedisClient.XGroupCreateMkStream(ctx, streamKey, groupName, "0").Err()
if err != nil {
// 如果组已存在,忽略 BUSYGROUP 错误
if err.Error() == "BUSYGROUP Consumer Group name already exists" {
return nil
}
return err
}
return nil
}
// AddToStream 将消息添加到 Stream
// 用于 Controller 层将 RAGFlow 请求推入 Stream
// 参数:
// - streamKey: Stream 键名
// - values: 消息内容(键值对)
//
// 返回:
// - string: 消息ID
// - error: 添加失败时返回错误
func AddToStream(ctx context.Context, streamKey string, values map[string]interface{}) (string, error) {
// 使用 XAdd 添加消息到 Stream
messageID, err := RedisClient.XAdd(ctx, &redis.XAddArgs{
Stream: streamKey,
Values: values,
}).Result()
if err != nil {
return "", err
}
return messageID, nil
}
// ReadFromStream 从 Stream 读取消息(消费者组模式)
// 后台 Goroutine 使用此方法从 Stream 中取出请求进行处理
// 参数:
// - streamKey: Stream 键名
// - groupName: 消费者组名称
// - consumerName: 消费者名称(唯一标识)
// - count: 每次读取的消息数量
// - blockMs: 阻塞时间毫秒0表示不阻塞
//
// 返回:
// - []StreamMessage: 消息列表
// - error: 读取失败时返回错误
func ReadFromStream(ctx context.Context, streamKey, groupName, consumerName string, count int64, blockMs int64) ([]StreamMessage, error) {
// 使用 XReadGroup 从消费者组读取消息
// ">" 表示读取未被消费的新消息(只获取新消息)
// 如果使用 "0" 或其他 ID则返回 Pending 消息(未确认的消息)
streams, err := RedisClient.XReadGroup(ctx, &redis.XReadGroupArgs{
Group: groupName,
Consumer: consumerName,
Streams: []string{streamKey, ">"}, // Stream名称 + 起始ID
Count: count,
Block: time.Duration(blockMs) * time.Millisecond,
}).Result()
// 处理错误:超时或没有数据时返回 redis.Nil
if err != nil {
if err == redis.Nil {
// 超时或没有数据,返回空数组
return []StreamMessage{}, nil
}
return nil, err
}
// 解析返回的消息
var messages []StreamMessage
for _, stream := range streams {
for _, msg := range stream.Messages {
messages = append(messages, StreamMessage{
ID: msg.ID,
Values: msg.Values,
})
}
}
return messages, nil
}
// AckMessage 确认消息已处理
// 处理完消息后必须调用此方法确认,否则消息会保留在 Pending List (PEL)
// 确认后消息会从 PEL 中移除
// 参数:
// - streamKey: Stream 键名
// - groupName: 消费者组名称
// - messageIDs: 要确认的消息ID列表
//
// 返回error 确认失败时返回错误
func AckMessage(ctx context.Context, streamKey, groupName string, messageIDs ...string) error {
// 使用 XAck 确认消息
// 返回值是成功确认的消息数量
count, err := RedisClient.XAck(ctx, streamKey, groupName, messageIDs...).Result()
if err != nil {
return err
}
// 可以检查 count 是否等于 len(messageIDs)
_ = count
return nil
}
// GetStreamLength 获取 Stream 当前长度
// 用于监控 Stream 消息积压情况
// 参数:
// - streamKey: Stream 键名
//
// 返回:
// - int64: Stream 中消息数量
// - error: 操作失败时返回错误
func GetStreamLength(ctx context.Context, streamKey string) (int64, error) {
// 使用 XLen 获取 Stream 长度
length, err := RedisClient.XLen(ctx, streamKey).Result()
if err != nil {
return 0, err
}
return length, nil
}
// GetPendingMessages 获取待处理消息(未确认的消息)
// 用于监控和重试失败的消息
// 参数:
// - streamKey: Stream 键名
// - groupName: 消费者组名称
// - start: 起始ID"-" 表示最小ID
// - end: 结束ID"+" 表示最大ID
// - count: 返回数量
//
// 返回:
// - []redis.XPendingExt: Pending 消息列表
// - error: 操作失败时返回错误
func GetPendingMessages(ctx context.Context, streamKey, groupName string, start, end string, count int64) ([]redis.XPendingExt, error) {
// 使用 XPendingExt 获取详细的 Pending 消息
pending, err := RedisClient.XPendingExt(ctx, &redis.XPendingExtArgs{
Stream: streamKey,
Group: groupName,
Start: start,
End: end,
Count: count,
}).Result()
if err != nil {
return nil, err
}
return pending, nil
}
// ClaimPendingMessage 认领超时的 Pending 消息
// 当某个消费者故障后,其他消费者可以认领其未完成的消息
// 参数:
// - streamKey: Stream 键名
// - groupName: 消费者组名称
// - consumerName: 新消费者名称
// - minIdleTime: 消息空闲时间(毫秒),超过此时间才能被认领
// - messageIDs: 要认领的消息ID列表
//
// 返回:
// - []StreamMessage: 认领的消息列表
// - error: 操作失败时返回错误
func ClaimPendingMessage(ctx context.Context, streamKey, groupName, consumerName string, minIdleTime int64, messageIDs ...string) ([]StreamMessage, error) {
// 使用 XClaim 认领消息
msgs, err := RedisClient.XClaim(ctx, &redis.XClaimArgs{
Stream: streamKey,
Group: groupName,
Consumer: consumerName,
MinIdle: time.Duration(minIdleTime) * time.Millisecond,
Messages: messageIDs,
}).Result()
if err != nil {
return nil, err
}
// 转换为 StreamMessage
var messages []StreamMessage
for _, msg := range msgs {
messages = append(messages, StreamMessage{
ID: msg.ID,
Values: msg.Values,
})
}
return messages, nil
}
// SetSessionLastActive 设置用户最后活跃时间
// 用于控制是否发送追问:用户回复后更新活跃时间,避免重复追问
// 过期时间2小时超过2小时未活跃的记录会自动删除
// 参数:
// - userId: 用户ID
//
// 返回error 设置失败时返回错误
func SetSessionLastActive(ctx context.Context, userId string) error {
key := SessionLastActiveKeyPrefix + userId + ":last_active"
timestamp := time.Now().Unix()
// 设置过期时间为 2 小时
return RedisClient.Set(ctx, key, timestamp, 2*time.Hour).Err()
}
// GetSessionLastActive 获取用户最后活跃时间
// 参数:
// - userId: 用户ID
//
// 返回:
// - int64: Unix时间戳未找到返回0
// - error: 操作失败时返回错误
func GetSessionLastActive(ctx context.Context, userId string) (int64, error) {
key := SessionLastActiveKeyPrefix + userId + ":last_active"
result, err := RedisClient.Get(ctx, key).Result()
if err == redis.Nil {
return 0, nil // 未找到返回 0
}
if err != nil {
return 0, err
}
// 将字符串转换为 int64
timestamp, err := strconv.ParseInt(result, 10, 64)
if err != nil {
return 0, err
}
return timestamp, nil
}
// IsUserActive 检查用户是否在指定时间范围内活跃过
// 用于追问逻辑:如果用户最近活跃过,则不发送追问消息
// 参数:
// - userId: 用户ID
// - seconds: 时间范围例如传入300表示检查5分钟内是否活跃
//
// 返回:
// - bool: true表示用户在指定时间内活跃过
// - error: 操作失败时返回错误
func IsUserActive(ctx context.Context, userId string, seconds int64) (bool, error) {
lastActive, err := GetSessionLastActive(ctx, userId)
if err != nil {
return false, err
}
if lastActive == 0 {
return false, nil // 未找到记录,视为不活跃
}
now := time.Now().Unix()
return (now - lastActive) < seconds, nil
}
// SetSessionCache 缓存用户的 RAGFlow Session ID
// 避免每次请求都创建新 Session提高性能
// 过期时间7天超过7天未使用的Session会自动清理
// 参数:
// - userId: 用户ID
// - sessionId: RAGFlow返回的Session ID
//
// 返回error 设置失败时返回错误
func SetSessionCache(ctx context.Context, userId, sessionId string) error {
key := SessionLastActiveKeyPrefix + userId + ":session_id"
return RedisClient.Set(ctx, key, sessionId, 7*24*time.Hour).Err()
}
// GetSessionCache 获取缓存的 RAGFlow Session ID
// 如果缓存中存在则直接使用不存在则需要创建新Session
// 参数:
// - userId: 用户ID
//
// 返回:
// - string: Session ID未找到返回空字符串
// - error: 操作失败时返回错误
func GetSessionCache(ctx context.Context, userId string) (string, error) {
key := SessionLastActiveKeyPrefix + userId + ":session_id"
result, err := RedisClient.Get(ctx, key).Result()
if err == redis.Nil {
return "", nil // 未找到返回空字符串
}
if err != nil {
return "", err
}
return result, nil
}