2026-05-17 23:38:04 +08:00
|
|
|
|
package cache
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"context"
|
|
|
|
|
|
"fmt"
|
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
|
|
"github.com/redis/go-redis/v9"
|
|
|
|
|
|
"go.uber.org/zap"
|
|
|
|
|
|
|
|
|
|
|
|
redisPkg "with_you/internal/pkg/redis"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-05-25 14:51:46 +08:00
|
|
|
|
const seqKeyTTL = 24 * time.Hour
|
2026-05-17 23:38:04 +08:00
|
|
|
|
|
2026-05-25 14:51:46 +08:00
|
|
|
|
// SeqBufferConfig seq 预分配配置(保留结构以兼容配置文件,实际不再使用缓冲)
|
|
|
|
|
|
type SeqBufferConfig struct {
|
|
|
|
|
|
Enabled bool `mapstructure:"enabled"`
|
|
|
|
|
|
PrivateBufferSize int `mapstructure:"private_buffer_size"`
|
|
|
|
|
|
GroupBufferSize int `mapstructure:"group_buffer_size"`
|
|
|
|
|
|
LockTimeoutMs int `mapstructure:"lock_timeout_ms"`
|
|
|
|
|
|
MaxRetries int `mapstructure:"max_retries"`
|
2026-05-17 23:38:04 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-25 14:51:46 +08:00
|
|
|
|
// SeqBufferManager seq 分配管理器(简化版:每条消息一个 Redis INCR,无本地缓冲)
|
2026-05-17 23:38:04 +08:00
|
|
|
|
type SeqBufferManager struct {
|
2026-05-25 14:51:46 +08:00
|
|
|
|
rdb *redis.Client
|
|
|
|
|
|
config SeqBufferConfig
|
|
|
|
|
|
repo ConversationRepository
|
|
|
|
|
|
msgRepo MessageRepository
|
2026-05-17 23:38:04 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-25 14:51:46 +08:00
|
|
|
|
// NewSeqBufferManager 创建 seq 分配管理器
|
2026-05-17 23:38:04 +08:00
|
|
|
|
func NewSeqBufferManager(rdb *redis.Client, cfg SeqBufferConfig, repo ConversationRepository, msgRepo MessageRepository) *SeqBufferManager {
|
|
|
|
|
|
return &SeqBufferManager{
|
|
|
|
|
|
rdb: rdb,
|
|
|
|
|
|
config: cfg,
|
|
|
|
|
|
repo: repo,
|
|
|
|
|
|
msgRepo: msgRepo,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-25 14:51:46 +08:00
|
|
|
|
// GetNextSeq 获取下一个 seq(单次 Redis INCR,原子且无竞态)
|
|
|
|
|
|
func (m *SeqBufferManager) GetNextSeq(ctx context.Context, convID string, _ bool) (int64, error) {
|
2026-05-17 23:38:04 +08:00
|
|
|
|
seqKey := MessageSeqKey(convID)
|
|
|
|
|
|
|
2026-05-25 14:51:46 +08:00
|
|
|
|
// 1. 尝试 INCR(key 存在时直接递增,原子操作)
|
|
|
|
|
|
result, err := nextSeqLua.Run(ctx, m.rdb, []string{seqKey}).Int64()
|
|
|
|
|
|
if err == nil {
|
|
|
|
|
|
if result > 0 {
|
|
|
|
|
|
m.rdb.Expire(ctx, seqKey, seqKeyTTL)
|
|
|
|
|
|
return result, nil
|
2026-05-17 23:38:04 +08:00
|
|
|
|
}
|
2026-05-25 14:51:46 +08:00
|
|
|
|
// result == 0: 冷启动,需从 DB 初始化
|
|
|
|
|
|
return m.initAndIncr(ctx, convID, seqKey)
|
2026-05-17 23:38:04 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-25 14:51:46 +08:00
|
|
|
|
// Redis 不可用,降级到 DB
|
|
|
|
|
|
zap.L().Warn("seq INCR failed, falling back to DB", zap.String("convID", convID), zap.Error(err))
|
2026-05-17 23:38:04 +08:00
|
|
|
|
if m.msgRepo != nil {
|
|
|
|
|
|
if dbSeq, dbErr := m.msgRepo.GetNextSeq(convID); dbErr == nil {
|
2026-05-25 14:51:46 +08:00
|
|
|
|
return dbSeq, nil
|
2026-05-17 23:38:04 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-05-25 14:51:46 +08:00
|
|
|
|
return 0, fmt.Errorf("seq allocation failed for %s: %w", convID, err)
|
2026-05-17 23:38:04 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-25 14:51:46 +08:00
|
|
|
|
// initAndIncr 冷启动:从 DB 加载 last_seq,然后用 SETNX+INCR 原子初始化
|
|
|
|
|
|
func (m *SeqBufferManager) initAndIncr(ctx context.Context, convID, seqKey string) (int64, error) {
|
2026-05-17 23:38:04 +08:00
|
|
|
|
if m.repo == nil {
|
2026-05-25 14:51:46 +08:00
|
|
|
|
return 0, fmt.Errorf("conversation repository not configured for seq init")
|
2026-05-17 23:38:04 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
conv, err := m.repo.GetConversationByID(convID)
|
|
|
|
|
|
if err != nil {
|
2026-05-25 14:51:46 +08:00
|
|
|
|
return 0, fmt.Errorf("load conversation for seq init: %w", err)
|
2026-05-17 23:38:04 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-25 14:51:46 +08:00
|
|
|
|
initVal := conv.LastSeq
|
|
|
|
|
|
result, err := initSeqLua.Run(ctx, m.rdb, []string{seqKey}, initVal).Int64()
|
2026-05-17 23:38:04 +08:00
|
|
|
|
if err != nil {
|
2026-05-25 14:51:46 +08:00
|
|
|
|
return 0, fmt.Errorf("initSeqLua failed: %w", err)
|
2026-05-17 23:38:04 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-25 14:51:46 +08:00
|
|
|
|
m.rdb.Expire(ctx, seqKey, seqKeyTTL)
|
|
|
|
|
|
return result, nil
|
2026-05-17 23:38:04 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-25 14:51:46 +08:00
|
|
|
|
// InvalidateBuffer no-op(保留接口兼容)
|
|
|
|
|
|
func (m *SeqBufferManager) InvalidateBuffer(convID string) {}
|
2026-05-17 23:38:04 +08:00
|
|
|
|
|
2026-05-25 14:51:46 +08:00
|
|
|
|
// IsEnabled 是否启用
|
2026-05-17 23:38:04 +08:00
|
|
|
|
func (m *SeqBufferManager) IsEnabled() bool {
|
|
|
|
|
|
return m.config.Enabled && m.rdb != nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// NewSeqBufferManagerFromCache 从 ConversationCache 的 Cache 接口创建 SeqBufferManager
|
|
|
|
|
|
func NewSeqBufferManagerFromCache(cacheBackend Cache, cfg SeqBufferConfig, repo ConversationRepository, msgRepo MessageRepository) *SeqBufferManager {
|
|
|
|
|
|
redisCache, ok := cacheBackend.(*RedisCache)
|
|
|
|
|
|
if !ok {
|
|
|
|
|
|
zap.L().Warn("SeqBuffer: cache is not RedisCache, seq pre-allocation disabled")
|
|
|
|
|
|
return &SeqBufferManager{config: cfg, repo: repo, msgRepo: msgRepo}
|
|
|
|
|
|
}
|
|
|
|
|
|
rdb := redisCache.GetRedisClient().GetClient()
|
|
|
|
|
|
return NewSeqBufferManager(rdb, cfg, repo, msgRepo)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// NewSeqBufferManagerFromRedisPkg 从 pkg/redis.Client 创建 SeqBufferManager
|
|
|
|
|
|
func NewSeqBufferManagerFromRedisPkg(redisPkgClient *redisPkg.Client, cfg SeqBufferConfig, repo ConversationRepository, msgRepo MessageRepository) *SeqBufferManager {
|
|
|
|
|
|
if redisPkgClient == nil {
|
|
|
|
|
|
zap.L().Warn("SeqBuffer: redis client is nil, seq pre-allocation disabled")
|
|
|
|
|
|
return &SeqBufferManager{config: cfg, repo: repo, msgRepo: msgRepo}
|
|
|
|
|
|
}
|
|
|
|
|
|
rdb := redisPkgClient.GetClient()
|
|
|
|
|
|
return NewSeqBufferManager(rdb, cfg, repo, msgRepo)
|
2026-05-25 14:51:46 +08:00
|
|
|
|
}
|