96ce883d41
完成 instance+facade 一致性收尾,闭合三个原始问题: - cron/storage/cache/ratelimit 实例化(App 持 per-App 实例 + SwapDefault* 提升全局默认 + 包级 facade 代理,照 database.Manager/jwt.Manager 模式),消除单进程多 App 互相污染。 - trace 纳入 App 生命周期(WithTrace + doInit trace.Init + doShutdown trace.Close + config.TraceConfig + 自动装 Middleware)。不做实例隔离(OTel 进程全局)。 - 删除 WithoutWire 空函数技术债。 附带修复: - cache/ratelimit redis 注入(jwt 模型:注入优先、database.GetRedis() 兜底)。 - commitReplacedResources 不再关闭 previousDB/previousRedis/loggerSnapshot (多 App 下 previous 可能是另一 App 的活跃资源,关闭致 redis: client is closed)。 - app.RateLimitRegistry() + App-bound 限流中间件(多 App per-App 计数隔离)。 - StorageManager.Close() 闭合 Init/Close 对称性(io.Closer 断言,预留扩展点), closeResources 与 rollbackReplacedResources 对称收口。 新增公开 API:WithTrace / WithCronTask;app.Scheduler()/Cache()/RedisClient()/ RateLimitRegistry();cron/storage/cache/middleware 各 SwapDefault*;config.TraceConfig; middleware.RateLimitRegistry + App-bound 方法;middleware.WithRedisClient; storage.StorageManager.Close。 Breaking:删 WithoutWire;cron.AddTask pre-Init 注册失效(改用 WithCronTask); middleware.StopRateLimiters 语义收窄;commitReplacedResources 不再关 previous; cache.NewRedisCacheWithRedis / CacheManager.InitWithRedis 等签名变更。 详见 CHANGELOG [1.4.0] 升级说明。 验证:go build + go vet + go test -race ./... 全绿(-buildvcs=false)。 独立对抗性复审无 CRITICAL/HIGH。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
280 lines
8.1 KiB
Go
280 lines
8.1 KiB
Go
package cache
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"sync"
|
||
"sync/atomic"
|
||
"time"
|
||
|
||
"github.com/EthanCodeCraft/xlgo-core/database"
|
||
"github.com/EthanCodeCraft/xlgo-core/logger"
|
||
"github.com/redis/go-redis/v9"
|
||
"go.uber.org/zap"
|
||
)
|
||
|
||
// CacheService 缓存服务接口
|
||
type CacheService interface {
|
||
// Get 获取缓存值,命中则反序列化到 dest 并返回 (true, nil);未命中返回 (false, nil);
|
||
// Redis 未就绪、命令错误或反序列化失败返回 (false, err)。调用方须显式处理错误。
|
||
Get(ctx context.Context, key string, dest any) (bool, error)
|
||
// Set 设置缓存值
|
||
Set(ctx context.Context, key string, value any, ttl time.Duration) error
|
||
// Delete 删除缓存
|
||
Delete(ctx context.Context, key string) error
|
||
// DeleteByPattern 按模式删除缓存
|
||
DeleteByPattern(ctx context.Context, pattern string) error
|
||
// Exists 检查缓存是否存在;未命中返回 (false, nil),后端错误返回 (false, err)。
|
||
Exists(ctx context.Context, key string) (bool, error)
|
||
}
|
||
|
||
// redisCache Redis 缓存实现。
|
||
//
|
||
// 不在构造时快照 redis.Client(M12 修复:原 NewRedisCache 构造时取 database.GetRedis(),
|
||
// 若在 database.InitRedis 之前构造则永久 nil、即使后续 Redis 就绪也是 no-op)。
|
||
// 改为每次操作实时取 redis 客户端,使"先构造后 Init Redis"的顺序也能正确工作。
|
||
//
|
||
// Phase 4:持有可选的注入 client(NewRedisCacheWithRedis),注入优先、否则回退全局
|
||
// database.GetRedis()(照 jwt.TokenBlacklist.redisClient 模型)。App 经 InitWithRedis
|
||
// 注入 per-App redisManager.Client(),使缓存走 App 自己的 Redis 而非全局(修复跨 App 串越)。
|
||
type redisCache struct {
|
||
injectedClient *redis.Client
|
||
}
|
||
|
||
// client 返回缓存使用的 Redis 客户端:注入优先,否则回退全局 database.GetRedis()。
|
||
func (c *redisCache) client() *redis.Client {
|
||
if c != nil && c.injectedClient != nil {
|
||
return c.injectedClient
|
||
}
|
||
return database.GetRedis()
|
||
}
|
||
|
||
// NewRedisCache 创建 Redis 缓存实例(懒取全局 Redis,兼容 standalone 用法)。
|
||
func NewRedisCache() CacheService {
|
||
return &redisCache{}
|
||
}
|
||
|
||
// NewRedisCacheWithRedis 创建使用指定 Redis 客户端的缓存实例(多 Redis/测试隔离)。
|
||
func NewRedisCacheWithRedis(client *redis.Client) CacheService {
|
||
return &redisCache{injectedClient: client}
|
||
}
|
||
|
||
// Get 获取缓存值。命中返回 (true, nil);未命中返回 (false, nil);
|
||
// Redis 未就绪返回 (false, ErrRedisNotReady);Redis 命令错误或反序列化失败返回 (false, err)。
|
||
func (c *redisCache) Get(ctx context.Context, key string, dest any) (bool, error) {
|
||
cli := c.client()
|
||
if cli == nil {
|
||
return false, ErrRedisNotReady
|
||
}
|
||
|
||
val, err := cli.Get(ctx, key).Result()
|
||
if err != nil {
|
||
if err == redis.Nil {
|
||
return false, nil
|
||
}
|
||
return false, err
|
||
}
|
||
|
||
if err := json.Unmarshal([]byte(val), dest); err != nil {
|
||
return false, err
|
||
}
|
||
|
||
return true, nil
|
||
}
|
||
|
||
// Set 设置缓存值
|
||
func (c *redisCache) Set(ctx context.Context, key string, value any, ttl time.Duration) error {
|
||
cli := c.client()
|
||
if cli == nil {
|
||
return ErrRedisNotReady
|
||
}
|
||
|
||
data, err := json.Marshal(value)
|
||
if err != nil {
|
||
logger.Warn("缓存序列化失败", zap.String("key", key), zap.Error(err))
|
||
return err
|
||
}
|
||
|
||
if err := cli.Set(ctx, key, data, ttl).Err(); err != nil {
|
||
logger.Warn("缓存设置失败", zap.String("key", key), zap.Error(err))
|
||
return err
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// Delete 删除缓存
|
||
func (c *redisCache) Delete(ctx context.Context, key string) error {
|
||
cli := c.client()
|
||
if cli == nil {
|
||
return ErrRedisNotReady
|
||
}
|
||
|
||
if err := cli.Del(ctx, key).Err(); err != nil {
|
||
logger.Warn("缓存删除失败", zap.String("key", key), zap.Error(err))
|
||
return err
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// DeleteByPattern 按模式删除缓存(使用 SCAN 避免阻塞 Redis)
|
||
func (c *redisCache) DeleteByPattern(ctx context.Context, pattern string) error {
|
||
cli := c.client()
|
||
if cli == nil {
|
||
return ErrRedisNotReady
|
||
}
|
||
|
||
var cursor uint64
|
||
var deleted int
|
||
|
||
for {
|
||
// 使用 SCAN 命令迭代查找匹配的键
|
||
keys, nextCursor, err := cli.Scan(ctx, cursor, pattern, 100).Result()
|
||
if err != nil {
|
||
logger.Warn("缓存键扫描失败", zap.String("pattern", pattern), zap.Error(err))
|
||
return err
|
||
}
|
||
|
||
// 删除找到的键
|
||
if len(keys) > 0 {
|
||
if err := cli.Del(ctx, keys...).Err(); err != nil {
|
||
logger.Warn("缓存批量删除失败", zap.Strings("keys", keys), zap.Error(err))
|
||
return err
|
||
}
|
||
deleted += len(keys)
|
||
}
|
||
|
||
// 更新游标
|
||
cursor = nextCursor
|
||
|
||
// 游标为 0 表示遍历完成
|
||
if cursor == 0 {
|
||
break
|
||
}
|
||
}
|
||
|
||
if deleted > 0 {
|
||
logger.Debug("缓存批量删除完成", zap.String("pattern", pattern), zap.Int("count", deleted))
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// Exists 检查缓存是否存在。命中返回 (true, nil);未命中返回 (false, nil);
|
||
// Redis 未就绪返回 (false, ErrRedisNotReady);命令错误返回 (false, err)。
|
||
func (c *redisCache) Exists(ctx context.Context, key string) (bool, error) {
|
||
cli := c.client()
|
||
if cli == nil {
|
||
return false, ErrRedisNotReady
|
||
}
|
||
|
||
n, err := cli.Exists(ctx, key).Result()
|
||
if err != nil {
|
||
return false, err
|
||
}
|
||
return n > 0, nil
|
||
}
|
||
|
||
// CacheManager 缓存管理器(#10)。照 database.Manager 模式:
|
||
// 实例化 + DefaultCache 全局默认 + 包级 facade 代理,支持测试注入 mock 实现。
|
||
type CacheManager struct {
|
||
mu sync.Mutex
|
||
svc CacheService
|
||
}
|
||
|
||
// defaultCachePtr 全局默认缓存管理器(CK3 修复:atomic.Pointer 替代裸指针)。
|
||
var defaultCachePtr atomic.Pointer[CacheManager]
|
||
|
||
func init() {
|
||
defaultCachePtr.Store(NewCacheManager())
|
||
}
|
||
|
||
// NewCacheManager 创建缓存管理器实例。
|
||
func NewCacheManager() *CacheManager { return &CacheManager{} }
|
||
|
||
// GetDefaultCache 返回全局默认缓存管理器(并发安全,CK3 修复)。
|
||
func GetDefaultCache() *CacheManager {
|
||
return defaultCachePtr.Load()
|
||
}
|
||
|
||
// SetDefaultCacheManager 提升指定 CacheManager 为全局默认(atomic 置换,并发安全)。
|
||
func SetDefaultCacheManager(m *CacheManager) {
|
||
if m != nil {
|
||
defaultCachePtr.Store(m)
|
||
}
|
||
}
|
||
|
||
// SwapDefaultCacheManager 将指定 CacheManager 置为全局默认,返回被替换的旧 Manager。
|
||
// 旧 Manager 不会被关闭,供 App 初始化这类需要失败回滚的生命周期流程暂存(照
|
||
// SwapDefaultRedisManager / SwapDefaultStorageManager 模式)。nil 被忽略,返回当前默认。
|
||
func SwapDefaultCacheManager(m *CacheManager) *CacheManager {
|
||
if m == nil {
|
||
return defaultCachePtr.Load()
|
||
}
|
||
return defaultCachePtr.Swap(m)
|
||
}
|
||
|
||
// Init 初始化缓存服务(基于 DefaultRedis 的客户端)。
|
||
func (m *CacheManager) Init() {
|
||
m.mu.Lock()
|
||
defer m.mu.Unlock()
|
||
m.svc = NewRedisCache()
|
||
}
|
||
|
||
// InitWithRedis 用指定 Redis 客户端初始化缓存服务(多 Redis/测试隔离)。
|
||
func (m *CacheManager) InitWithRedis(client *redis.Client) {
|
||
m.mu.Lock()
|
||
defer m.mu.Unlock()
|
||
m.svc = NewRedisCacheWithRedis(client)
|
||
}
|
||
|
||
// Set 设置缓存服务实现(用于注入 mock 或自定义实现)。
|
||
func (m *CacheManager) Set(svc CacheService) {
|
||
m.mu.Lock()
|
||
defer m.mu.Unlock()
|
||
m.svc = svc
|
||
}
|
||
|
||
// Get 返回缓存服务(未初始化时延迟初始化)。
|
||
func (m *CacheManager) Get() CacheService {
|
||
m.mu.Lock()
|
||
defer m.mu.Unlock()
|
||
if m.svc == nil {
|
||
m.svc = NewRedisCache()
|
||
}
|
||
return m.svc
|
||
}
|
||
|
||
// --- 包级 facade(代理到 GetDefaultCache(),CK3 修复) ---
|
||
|
||
// Init 初始化全局缓存实例
|
||
func Init() {
|
||
GetDefaultCache().Init()
|
||
}
|
||
|
||
// GetCache 获取全局缓存实例
|
||
func GetCache() CacheService {
|
||
return GetDefaultCache().Get()
|
||
}
|
||
|
||
// Get 获取全局缓存值。命中返回 (true, nil);未命中返回 (false, nil);
|
||
// 缓存未初始化或 Redis 错误返回 (false, err)。
|
||
func Get(ctx context.Context, key string, dest any) (bool, error) {
|
||
svc := GetCache()
|
||
if svc == nil {
|
||
return false, ErrRedisNotReady
|
||
}
|
||
return svc.Get(ctx, key, dest)
|
||
}
|
||
|
||
// Exists 检查全局缓存是否存在。未命中返回 (false, nil);缓存未初始化或
|
||
// Redis 错误返回 (false, err)。
|
||
func Exists(ctx context.Context, key string) (bool, error) {
|
||
svc := GetCache()
|
||
if svc == nil {
|
||
return false, ErrRedisNotReady
|
||
}
|
||
return svc.Exists(ctx, key)
|
||
}
|