package nats import ( "context" "fmt" "github.com/gogf/gf/v2/frame/g" "github.com/nats-io/nats.go/jetstream" "time" ) // TaskStreamConfig 任务流配置 type TaskStreamConfig struct { StreamName string // 流名称 Subjects []string // 主题数组(支持任务优先级,如 ["tasks.high","tasks.normal", "tasks.low"]) PublishSubject string // 发布使用的主题(仅用于记录,不影响流配置) Storage StorageType // 存储类型 Retention RetentionType // 保留策略 MaxAge time.Duration // 最大保留时间 Duplicates time.Duration // 消息去重窗口时间 Replicas int // 副本数 MaxMsgSize int32 // 单条消息最大大小(字节) MaxBytes int64 // 流最大存储大小(字节) MaxMsgs int64 // 流中最大消息数 MaxMsgsPerSubject int64 // 每个主题最大消息数 MaxConsumers int // 最大消费者数量 DenyPurge bool // 是否禁止清理流 AllowRollup bool // 是否允许汇总消息 DenyDelete bool // 是否禁止删除 DiscardPerSubject bool // 是否按主题限制(工作队列模式) Republish *RePublishConfig // 死信队列重新发布配置 } // RePublishConfig 重新发布配置(用于死信队列) type RePublishConfig struct { Source string // 源主题 Destination string // 目标主题 HeadersOnly bool // 仅复制消息头 } // StorageType 存储类型 type StorageType string const ( StorageFile StorageType = "file" // 文件存储(持久化) StorageMemory StorageType = "memory" // 内存存储 ) // RetentionType 保留策略 type RetentionType string const ( RetentionLimit RetentionType = "limit" // 消息数量限制 RetentionPolicy RetentionType = "interest" // 基于兴趣 RetentionWorkQueue RetentionType = "workqueue" // 工作队列 ) // parseStorageType 解析存储类型 func parseStorageType(st StorageType) jetstream.StorageType { switch st { case StorageMemory: return jetstream.MemoryStorage default: return jetstream.FileStorage } } // parseRetentionType 解析保留策略 func parseRetentionType(rt RetentionType) jetstream.RetentionPolicy { switch rt { case RetentionLimit: return jetstream.LimitsPolicy case RetentionPolicy: return jetstream.InterestPolicy default: return jetstream.WorkQueuePolicy } } // createTaskStreamSimple 简化版创建任务流(适用于大多数场景) // 只需提供流名称和主题数组,其他使用默认配置 func createTaskStreamSimple(ctx context.Context, streamName string, subjects []string) error { return createTaskStream(ctx, TaskStreamConfig{ StreamName: streamName, Subjects: subjects, }) } // createTaskStreamWithPriority 创建支持优先级的任务流 func createTaskStreamWithPriority(ctx context.Context, streamPrefix string) error { subjects := []string{ fmt.Sprintf("%s.high.>", streamPrefix), fmt.Sprintf("%s.normal.>", streamPrefix), fmt.Sprintf("%s.low.>", streamPrefix), } return createTaskStream(ctx, TaskStreamConfig{ StreamName: streamPrefix, Subjects: subjects, }) } // CreateTaskStream 配置: 文件存储 + 工作队列策略 // CreateTaskStream 创建任务消息队列流(JetStream 2.10+) // 核心设计思路: // 1. 严格持久化:使用文件存储,任务消息不会因为服务器重启而丢失 // 2. 支持任务优先级:通过主题分级实现,如 ["tasks.high", "tasks.low"] // 3. 死信队列支持:通过 RePublish 配置将失败任务路由到专门的 DLQ 流 // 4. 灵活保留策略:根据任务重要性设置不同的保留时长(MaxAge) // 5. 工作队列模式:确保每个任务只被一个消费者处理(DiscardPerSubject) func createTaskStream(ctx context.Context, config TaskStreamConfig) error { if !IsConnected() { return fmt.Errorf("NATS 未连接") } if g.IsNil(config.StreamName) { return fmt.Errorf("流名称不能为空") } if len(config.Subjects) == 0 { return fmt.Errorf("主题数组不能为空") } // 设置默认值 if config.Storage == "" { config.Storage = StorageFile // 默认文件存储 } if config.Retention == "" { config.Retention = RetentionWorkQueue // 默认工作队列策略 } if config.MaxAge == 0 { config.MaxAge = 24 * time.Hour // 默认保留24小时 } if config.Replicas == 0 { config.Replicas = 1 // 默认单副本 } if config.MaxBytes == 0 { config.MaxBytes = 10 * 1024 * 1024 * 1024 // 默认10GB } if config.MaxMsgs == 0 { config.MaxMsgs = 100000 // 默认10万条消息 } if config.MaxMsgSize == 0 { config.MaxMsgSize = 1024 * 1024 // 默认1MB } if config.DiscardPerSubject { config.DenyDelete = true // 工作队列模式下禁止删除 } // 构建流配置 jsConfig := jetstream.StreamConfig{ Name: config.StreamName, Subjects: config.Subjects, Storage: parseStorageType(config.Storage), Retention: parseRetentionType(config.Retention), MaxAge: config.MaxAge, Duplicates: config.Duplicates, Replicas: config.Replicas, MaxMsgSize: config.MaxMsgSize, MaxBytes: config.MaxBytes, MaxMsgs: config.MaxMsgs, MaxMsgsPerSubject: config.MaxMsgsPerSubject, MaxConsumers: config.MaxConsumers, AllowRollup: config.AllowRollup, DenyDelete: config.DenyDelete, DenyPurge: config.DenyPurge, Discard: jetstream.DiscardOld, // 默认删除旧消息 DiscardNewPerSubject: config.DiscardPerSubject, } // 配置死信队列重新发布(如果设置了) if config.Republish != nil { jsConfig.RePublish = &jetstream.RePublish{ Source: config.Republish.Source, Destination: config.Republish.Destination, HeadersOnly: config.Republish.HeadersOnly, } } else { // 使用固定的死信队列命名规范:{StreamName}.DLQ dlqSubject := fmt.Sprintf("%s.DLQ", config.StreamName) // 死信队列配置 jsConfig.RePublish = &jetstream.RePublish{ Source: ">", Destination: dlqSubject, HeadersOnly: true, } } // 检查流是否已存在 stream, err := js.Stream(ctx, config.StreamName) if err == nil { // 流已存在,更新配置 _, err = js.UpdateStream(ctx, jsConfig) if err != nil { return fmt.Errorf("更新任务流失败: %w", err) } g.Log().Infof(ctx, "✅ 任务消息队列流已更新: %s", stream.CachedInfo().Config.Name) return nil } // 创建新流 stream, err = js.CreateStream(ctx, jsConfig) if err != nil { return fmt.Errorf("创建任务流失败: %w", err) } // 记录配置信息 configInfo := fmt.Sprintf("存储=%s, 策略=%s, 副本=%d, 保留=%v", config.Storage, config.Retention, config.Replicas, config.MaxAge) if config.Republish != nil { configInfo += fmt.Sprintf(", 死信队列=%s->%s", config.Republish.Source, config.Republish.Destination) } g.Log().Infof(ctx, "✅ 任务消息队列流创建成功: %s (%s)", stream.CachedInfo().Config.Name, configInfo) return nil }