package cache import ( "context" "encoding/json" "errors" "log" "strings" "time" redis "github.com/redis/go-redis/v9" ) // ProbeRedis 探测 Redis 是否可达。 // // 使用 2 秒超时发起 PING,成功返回 true,任何错误(连接拒绝、超时、DNS 解析失败) // 均返回 false 并打印 warn 日志,调用方可据此决定是否启用 Redis。 // // 此函数是幂等的,可在启动阶段安全调用多次。 func ProbeRedis(addr, password string, db int) bool { ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() client := redis.NewClient(&redis.Options{ Addr: addr, Password: password, DB: db, DialTimeout: 2 * time.Second, }) defer client.Close() if err := client.Ping(ctx).Err(); err != nil { log.Printf("redis probe: unreachable at %s — falling back to in-memory only (%v)", addr, err) return false } log.Printf("redis probe: reachable at %s — Redis L2 cache will be enabled", addr) return true } // L2Cache defines the distributed cache contract. type L2Cache interface { Set(ctx context.Context, key string, value interface{}, ttl time.Duration) error Get(ctx context.Context, key string) (interface{}, error) Delete(ctx context.Context, key string) error Exists(ctx context.Context, key string) (bool, error) Clear(ctx context.Context) error Increment(ctx context.Context, key string, delta int64, ttl time.Duration) (int64, error) Close() error } // RedisCacheConfig configures the Redis-backed L2 cache. type RedisCacheConfig struct { Enabled bool Addr string Password string DB int PoolSize int } // RedisCache implements L2Cache using Redis. type RedisCache struct { enabled bool client *redis.Client } // NewRedisCache keeps the old test-friendly constructor. func NewRedisCache(enabled bool) *RedisCache { return NewRedisCacheWithConfig(RedisCacheConfig{Enabled: enabled}) } // NewRedisCacheWithConfig creates a Redis-backed L2 cache. func NewRedisCacheWithConfig(cfg RedisCacheConfig) *RedisCache { cache := &RedisCache{enabled: cfg.Enabled} if !cfg.Enabled { return cache } addr := cfg.Addr if addr == "" { addr = "localhost:6379" } options := &redis.Options{ Addr: addr, Password: cfg.Password, DB: cfg.DB, } if cfg.PoolSize > 0 { options.PoolSize = cfg.PoolSize } cache.client = redis.NewClient(options) return cache } func (c *RedisCache) Set(ctx context.Context, key string, value interface{}, ttl time.Duration) error { if !c.enabled || c.client == nil { return nil } payload, err := json.Marshal(value) if err != nil { return err } return c.client.Set(ctx, key, payload, ttl).Err() } func (c *RedisCache) Get(ctx context.Context, key string) (interface{}, error) { if !c.enabled || c.client == nil { return nil, nil } raw, err := c.client.Get(ctx, key).Result() if errors.Is(err, redis.Nil) { return nil, nil } if err != nil { return nil, err } return decodeRedisValue(raw) } func (c *RedisCache) Delete(ctx context.Context, key string) error { if !c.enabled || c.client == nil { return nil } return c.client.Del(ctx, key).Err() } func (c *RedisCache) Exists(ctx context.Context, key string) (bool, error) { if !c.enabled || c.client == nil { return false, nil } count, err := c.client.Exists(ctx, key).Result() if err != nil { return false, err } return count > 0, nil } func (c *RedisCache) Clear(ctx context.Context) error { if !c.enabled || c.client == nil { return nil } return c.client.FlushDB(ctx).Err() } func (c *RedisCache) Close() error { if !c.enabled || c.client == nil { return nil } return c.client.Close() } func (c *RedisCache) Increment(ctx context.Context, key string, delta int64, ttl time.Duration) (int64, error) { if !c.enabled || c.client == nil { return 0, errors.New("redis is not enabled") } result, err := c.client.IncrBy(ctx, key, delta).Result() if err != nil { return 0, err } if ttl > 0 { c.client.Expire(ctx, key, ttl) } return result, nil } func decodeRedisValue(raw string) (interface{}, error) { decoder := json.NewDecoder(strings.NewReader(raw)) decoder.UseNumber() var value interface{} if err := decoder.Decode(&value); err != nil { return raw, nil } return normalizeRedisValue(value), nil } func normalizeRedisValue(value interface{}) interface{} { switch v := value.(type) { case json.Number: if n, err := v.Int64(); err == nil { return n } if n, err := v.Float64(); err == nil { return n } return v.String() case []interface{}: for i := range v { v[i] = normalizeRedisValue(v[i]) } return v case map[string]interface{}: for key, item := range v { v[key] = normalizeRedisValue(item) } return v default: return v } }