Files
user-system/internal/cache/l2.go
long-agent 7b047e2f11 perf: Sprint 19 P0/P1 性能优化落地
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 无漏洞
2026-04-18 22:57:44 +08:00

208 lines
4.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}
}