重构消息队列连接管理,支持多数据源配置
主要变更: 1. 重构NATS、RabbitMQ和Redis连接管理模块,支持多数据源配置 2. 统一连接管理接口,增加数据源名称参数 3. 优化连接状态检查和错误处理 4. 增加连接池管理和资源清理机制 5. 改进日志输出格式和内容
This commit is contained in:
@@ -3,11 +3,11 @@ package message
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/gogf/gf/v2/os/glog"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/glog"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
@@ -16,6 +16,9 @@ type RedisPublishMsgConfig struct {
|
||||
Data any
|
||||
}
|
||||
|
||||
type RedisPublishDelayMsgConfig struct {
|
||||
}
|
||||
|
||||
type RedisSubscribeMsgConfig struct {
|
||||
QueueName string
|
||||
ConsumerName string
|
||||
@@ -28,18 +31,22 @@ func (*RedisPublishMsgConfig) GetPublishMsgType() {
|
||||
|
||||
}
|
||||
|
||||
func (*RedisPublishDelayMsgConfig) GetPublishDelayMsgType() {}
|
||||
|
||||
func (*RedisSubscribeMsgConfig) GetSubscribeMsgType() {
|
||||
|
||||
}
|
||||
|
||||
func init() {
|
||||
// 注册 Redis 插件(连接由 RegisterPlugin 异步处理)
|
||||
registerPlugin(MessageRedis, func() messageUtil {
|
||||
return &redis{}
|
||||
})
|
||||
type redis struct {
|
||||
name string // 数据源名称
|
||||
}
|
||||
|
||||
type redis struct{}
|
||||
func init() {
|
||||
// 注册 Redis 插件(默认数据源)
|
||||
RegisterPlugin(context.Background(), "default", MessageRedis, func() messageUtil {
|
||||
return &redis{name: "default"}
|
||||
})
|
||||
}
|
||||
|
||||
// RedisStreamMessage Redis Stream 消息结构
|
||||
type redisStreamMessage struct {
|
||||
@@ -47,41 +54,19 @@ type redisStreamMessage struct {
|
||||
Values map[string]interface{}
|
||||
}
|
||||
|
||||
// Ping 检测 Redis 连接状态
|
||||
func (c *redis) ping(ctx context.Context) bool {
|
||||
conn, err := getDefaultDataSource()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return conn.redisPing(ctx)
|
||||
// Connect 连接 Redis
|
||||
func (c *redis) Connect(ctx context.Context) error {
|
||||
return redisConnect(ctx, c.name)
|
||||
}
|
||||
|
||||
// Reconnect 重连 Redis
|
||||
func (c *redis) reconnect(ctx context.Context) error {
|
||||
conn, err := getDefaultDataSource()
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取默认连接失败: %w", err)
|
||||
}
|
||||
|
||||
if err := conn.redisReconnect(ctx); err != nil {
|
||||
return fmt.Errorf("redis重连失败: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
// Ping 检测 Redis 连接状态
|
||||
func (c *redis) Ping(ctx context.Context) bool {
|
||||
return redisPing(ctx, c.name)
|
||||
}
|
||||
|
||||
// Close 关闭 Redis 连接
|
||||
func (c *redis) close(ctx context.Context) error {
|
||||
conn, err := getDefaultDataSource()
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取默认连接失败: %w", err)
|
||||
}
|
||||
|
||||
if err := conn.redisClose(ctx); err != nil {
|
||||
return fmt.Errorf("关闭redis连接失败: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
func (c *redis) Close(ctx context.Context) error {
|
||||
return redisClose(ctx, c.name)
|
||||
}
|
||||
|
||||
// Publish 发布消息
|
||||
@@ -96,14 +81,16 @@ func (c *redis) Publish(ctx context.Context, msgConfig messagePublishConfig) err
|
||||
if g.IsEmpty(cfg.Data) {
|
||||
return fmt.Errorf("数据不能为空")
|
||||
}
|
||||
conn, err := getDefaultDataSource()
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取默认连接失败: %w", err)
|
||||
}
|
||||
|
||||
if !conn.getIsConnected() {
|
||||
if err := conn.redisReconnect(ctx); err != nil {
|
||||
return fmt.Errorf("redis重连失败: %w", err)
|
||||
rc := getRedisConn(c.name)
|
||||
if !c.Ping(ctx) {
|
||||
if err := commonConnect(ctx, MessageRedis, c.name, func(ctx context.Context) error {
|
||||
return c.Connect(ctx)
|
||||
}, func(ctx context.Context) error {
|
||||
return c.Close(ctx)
|
||||
}); err != nil {
|
||||
g.Log().Errorf(ctx, "❌ [%s][%s] 连接失败: %v", MessageRedis, c.name, err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,7 +100,7 @@ func (c *redis) Publish(ctx context.Context, msgConfig messagePublishConfig) err
|
||||
for key, val := range values {
|
||||
args = append(args, key, val)
|
||||
}
|
||||
result, err := conn.getClient().Do(ctx, "XADD", args...)
|
||||
result, err := rc.Do(ctx, "XADD", args...)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "❌ Redis 发布消息失败: key=%s, err=%v", cfg.QueueName, err)
|
||||
return err
|
||||
@@ -122,6 +109,12 @@ func (c *redis) Publish(ctx context.Context, msgConfig messagePublishConfig) err
|
||||
return nil
|
||||
}
|
||||
|
||||
// PublishDelay 发布延迟消息
|
||||
func (c *redis) PublishDelay(ctx context.Context, _ messagePublishDelayConfig) error {
|
||||
g.Log().Errorf(ctx, "❌ Redis 不支持延迟消息")
|
||||
return fmt.Errorf("❌ Redis 不支持延迟消息")
|
||||
}
|
||||
|
||||
// Subscribe 订阅消息
|
||||
func (c *redis) Subscribe(ctx context.Context, msgConfig messageSubscribeConfig) error {
|
||||
cfg, ok := msgConfig.(*RedisSubscribeMsgConfig)
|
||||
@@ -142,162 +135,92 @@ func (c *redis) Subscribe(ctx context.Context, msgConfig messageSubscribeConfig)
|
||||
|
||||
// createSubscribe 内部订阅消息
|
||||
func (c *redis) createSubscribe(ctx context.Context, key, consumerName string, prefetchCount int, autoAck bool, handler func(ctx context.Context, message map[string]interface{}) error) error {
|
||||
go func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
g.Log().Errorf(ctx, "❌ Redis 消费者 panic: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
retryTicker := time.NewTicker(time.Second)
|
||||
defer retryTicker.Stop()
|
||||
LOOP:
|
||||
err := c.consumeMessages(ctx, key, consumerName, prefetchCount, autoAck, handler)
|
||||
if err != nil {
|
||||
// 对于超时错误,返回nil继续循环,而不是返回错误
|
||||
if strings.Contains(err.Error(), "i/o timeout") || strings.Contains(err.Error(), "timeout") ||
|
||||
strings.Contains(err.Error(), "context deadline exceeded") || strings.Contains(err.Error(), "context canceled") {
|
||||
|
||||
// 重试计数器
|
||||
var consecutiveErrors int
|
||||
const maxConsecutiveErrors = 3
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
g.Log().Infof(ctx, "🔕 Redis 消费者停止: topic=%s", key)
|
||||
return
|
||||
case <-retryTicker.C:
|
||||
err := c.consumeMessages(ctx, key, consumerName, prefetchCount, autoAck, handler)
|
||||
if err != nil {
|
||||
// 对于超时错误,返回nil继续循环,而不是返回错误
|
||||
if strings.Contains(err.Error(), "i/o timeout") || strings.Contains(err.Error(), "timeout") ||
|
||||
strings.Contains(err.Error(), "context deadline exceeded") || strings.Contains(err.Error(), "context canceled") {
|
||||
|
||||
consecutiveErrors++
|
||||
if consecutiveErrors > maxConsecutiveErrors {
|
||||
g.Log().Errorf(ctx, "Max retries exceeded, giving up")
|
||||
return
|
||||
}
|
||||
backoffTime := 5 * time.Second
|
||||
g.Log().Warningf(ctx, "⚠️ 等待 %v 后重试...", backoffTime)
|
||||
|
||||
time.Sleep(backoffTime)
|
||||
} else {
|
||||
// 非超时错误(严重错误)
|
||||
consecutiveErrors = 0 // 重置计数
|
||||
g.Log().Errorf(ctx, "严重错误,立即重试: %v", err)
|
||||
|
||||
// 短暂等待后重试
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-time.After(time.Second):
|
||||
// 继续循环
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 成功时重置错误计数器
|
||||
consecutiveErrors = 0
|
||||
}
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
goto LOOP
|
||||
} else {
|
||||
g.Log().Errorf(ctx, "❌ 严重错误: %v", err)
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
goto LOOP
|
||||
}
|
||||
|
||||
// consumeMessages 消费消息
|
||||
func (c *redis) consumeMessages(ctx context.Context, key, consumerName string, prefetchCount int, autoAck bool, handler func(ctx context.Context, message map[string]interface{}) error) error {
|
||||
conn, err := getDefaultDataSource()
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取默认连接失败: %w", err)
|
||||
if !c.Ping(ctx) {
|
||||
if err := commonConnect(ctx, MessageRedis, c.name, func(ctx context.Context) error {
|
||||
return c.Connect(ctx)
|
||||
}, func(ctx context.Context) error {
|
||||
return c.Close(ctx)
|
||||
}); err != nil {
|
||||
g.Log().Errorf(ctx, "❌ [%s][%s] 连接失败: %v", MessageRedis, c.name, err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if !conn.getIsConnected() {
|
||||
if err := conn.redisReconnect(ctx); err != nil {
|
||||
return fmt.Errorf("redis重连失败: %w", err)
|
||||
}
|
||||
rc := getRedisConn(c.name)
|
||||
if rc == nil {
|
||||
g.Log().Errorf(ctx, "❌ Redis [%s] 连接不存在", c.name)
|
||||
return fmt.Errorf("Redis 连接不存在")
|
||||
}
|
||||
|
||||
// 检查消费者组是否存在
|
||||
if err := c.createStreamGroup(ctx, key); err != nil {
|
||||
return fmt.Errorf("create stream group failed: %w", err)
|
||||
groupName := "default"
|
||||
_, err := rc.Do(ctx, "XGROUP", "CREATE", key, groupName, "0", "MKSTREAM")
|
||||
if err != nil {
|
||||
errStr := err.Error()
|
||||
if strings.Contains(errStr, "BUSYGROUP") && strings.Contains(errStr, "already exists") {
|
||||
glog.Infof(ctx, "✅ Redis [%s] 消费者组已存在: %s", c.name, key)
|
||||
return nil
|
||||
}
|
||||
g.Log().Errorf(ctx, "❌ 创建消费组失败: key=%s, err=%v", key, err)
|
||||
return err
|
||||
}
|
||||
glog.Infof(ctx, "✅ Redis [%s] 消费者组创建成功: %s", c.name, key)
|
||||
|
||||
// 使用带重试的命令执行
|
||||
result, err := conn.getClient().Do(ctx, "XREADGROUP", "GROUP", "default", consumerName, "COUNT", prefetchCount, "BLOCK", 0, "STREAMS", key, ">")
|
||||
result, err := rc.Do(ctx, "XREADGROUP", "GROUP", groupName, consumerName, "COUNT", prefetchCount, "BLOCK", 0, "STREAMS", key, ">")
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "i/o timeout") || strings.Contains(err.Error(), "timeout") ||
|
||||
strings.Contains(err.Error(), "context deadline exceeded") || strings.Contains(err.Error(), "context canceled") {
|
||||
|
||||
}
|
||||
return err
|
||||
}
|
||||
messages, err := c.parseStreamResult(result)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "❌ 解析消息失败: %v", err)
|
||||
return err
|
||||
}
|
||||
for _, msg := range messages {
|
||||
// 处理消息
|
||||
if err := handler(ctx, msg.Values); err != nil {
|
||||
g.Log().Errorf(ctx, "❌ 消息处理失败: messageID=%s, err=%v", msg.ID, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// ACK 消息
|
||||
if autoAck {
|
||||
if err := c.ackMessage(ctx, key, "default", msg.ID); err != nil {
|
||||
g.Log().Errorf(ctx, "❌ ACK 消息失败: messageID=%s, err=%v", msg.ID, err)
|
||||
// 如果不是自动ACK,则跳过当前消息
|
||||
if !autoAck {
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
g.Log().Infof(ctx, "✅ 消息处理成功: messageID=%s", msg.ID)
|
||||
}
|
||||
// ACK 消息
|
||||
args := make([]interface{}, 0, len(msg.ID)+2)
|
||||
args = append(args, key, groupName, msg.ID)
|
||||
_, err = rc.Do(ctx, "XACK", args...)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "❌ ACK 消息失败: messageID=%s, err=%v", msg.ID, err)
|
||||
} else {
|
||||
g.Log().Infof(ctx, "✅ ACK 消息成功: messageID=%s", msg.ID)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// createStreamGroup 内部单个创建消费组
|
||||
func (c *redis) createStreamGroup(ctx context.Context, key string) error {
|
||||
conn, err := getDefaultDataSource()
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取默认连接失败: %w", err)
|
||||
}
|
||||
|
||||
if !conn.getIsConnected() {
|
||||
if err := conn.redisReconnect(ctx); err != nil {
|
||||
return fmt.Errorf("redis重连失败: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
groupName := "default"
|
||||
_, err = conn.getClient().Do(ctx, "XGROUP", "CREATE", key, groupName, "0", "MKSTREAM")
|
||||
if err != nil {
|
||||
errStr := err.Error()
|
||||
if strings.Contains(errStr, "BUSYGROUP") && strings.Contains(errStr, "already exists") {
|
||||
glog.Infof(ctx, "✅ Redis 消费者组已存在: %s", groupName)
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("初始化消费者组失败: %w", err)
|
||||
}
|
||||
glog.Infof(ctx, "✅ Redis 消费者组创建成功: %s", groupName)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ackMessage ACK 消息
|
||||
func (c *redis) ackMessage(ctx context.Context, streamKey, groupName string, messageIDs ...string) error {
|
||||
conn, err := getDefaultDataSource()
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取默认连接失败: %w", err)
|
||||
}
|
||||
|
||||
if !conn.getIsConnected() {
|
||||
if err := conn.redisReconnect(ctx); err != nil {
|
||||
return fmt.Errorf("redis重连失败: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
args := make([]interface{}, 0, len(messageIDs)+2)
|
||||
args = append(args, streamKey, groupName)
|
||||
for _, id := range messageIDs {
|
||||
args = append(args, id)
|
||||
}
|
||||
_, err = conn.getClient().Do(ctx, "XACK", args...)
|
||||
return err
|
||||
}
|
||||
|
||||
// parseStreamResult 解析 Stream 结果
|
||||
func (c *redis) parseStreamResult(result interface{}) ([]redisStreamMessage, error) {
|
||||
if result == nil {
|
||||
|
||||
Reference in New Issue
Block a user