P0(高优先级): - P0-1: 确认数据库复合索引已存在(GORM tag),composite_index_test 验证通过 - P0-2: 连接池调优 MaxIdleConns 5→10, ConnMaxLifetime 30min→5min - P0-3: Redis 智能探测(ProbeRedis),无 Redis 自动降级到纯内存模式 P1(中优先级): - P1-1: GZIP 压缩中间件(compress/gzip 标准库,零新依赖) - P1-2: 权限缓存 TTL 30min→5min - P1-3: Argon2id 启动自适应校准(CalibrateArgon2id) 历史优化(含本次提交): - L1Cache O(n)→O(1) LRU 重构 - Auth 中间件 DB 查询合并 + 5s L1 缓存 - Logger 异步化(4096 缓冲通道) 验证: go build/vet/test 41/41 PASS, govulncheck 无漏洞
208 lines
4.7 KiB
Go
208 lines
4.7 KiB
Go
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
|
||
}
|
||
}
|