diff --git a/internal/router/deps_core.go b/internal/router/deps_core.go index 5461c50..3a659f5 100644 --- a/internal/router/deps_core.go +++ b/internal/router/deps_core.go @@ -40,17 +40,21 @@ func buildDepsCore(db *gorm.DB, cfg *config.Config, siteSettings *config.SiteSet idleTimeout := time.Duration(cfg.Session.IdleTimeout) * time.Minute rememberTimeout := time.Duration(cfg.Session.RememberTimeout) * time.Minute + cleanupInterval := time.Duration(cfg.Session.CleanupInterval) * time.Second + + // 总是创建 MemoryStore 作为兜底 + memStore := session.NewMemoryStore(idleTimeout, rememberTimeout) + memStore.StartCleanup(cleanupInterval) var sessionStore session.Store if cfg.Redis.Enabled && redisClient != nil { - // Redis 模式(生产环境推荐) - sessionStore = session.NewRedisStore(redisClient, idleTimeout, rememberTimeout) - log.Println("[Router] 会话存储: Redis") + // Redis 模式 + 自动降级保护 + redisStore := session.NewRedisStore(redisClient, idleTimeout, rememberTimeout) + sessionStore = session.NewFallbackStore(redisStore, memStore, cleanupInterval) + log.Println("[Router] 会话存储: Redis(带内存降级保护)") } else { - // 内存模式(开发/单实例) - memStore := session.NewMemoryStore(idleTimeout, rememberTimeout) - memStore.StartCleanup(time.Duration(cfg.Session.CleanupInterval) * time.Second) + // 纯内存模式 sessionStore = memStore log.Println("[Router] 会话存储: 内存") } diff --git a/internal/session/fallback_store.go b/internal/session/fallback_store.go new file mode 100644 index 0000000..c770f4c --- /dev/null +++ b/internal/session/fallback_store.go @@ -0,0 +1,140 @@ +package session + +import ( + "context" + "log" + "sync/atomic" + "time" + + "github.com/redis/go-redis/v9" +) + +// FallbackStore 会话存储降级保护层 +// Redis 正常时委托给 RedisStore,异常时自动降级到 MemoryStore +// 后台健康检测 goroutine 周期性检测 Redis 状态并自动恢复 +type FallbackStore struct { + redis *RedisStore + memory Store + client *redis.Client + + healthy atomic.Bool // Redis 当前是否健康 + degraded atomic.Bool // 是否处于降级模式(曾因异常而切换到 Memory) +} + +// NewFallbackStore 创建降级保护存储 +// checkInterval: 健康检测间隔,建议与 session cleanup_interval 一致 +func NewFallbackStore(redis *RedisStore, memory Store, checkInterval time.Duration) *FallbackStore { + fs := &FallbackStore{ + redis: redis, + memory: memory, + client: redis.client, + } + fs.healthy.Store(true) + + // 后台健康检测 goroutine + go fs.healthLoop(checkInterval) + + log.Println("[FallbackStore] 已启动 Redis 健康检测") + return fs +} + +// healthLoop 定期 ping Redis,自动升降级 +func (fs *FallbackStore) healthLoop(interval time.Duration) { + ticker := time.NewTicker(interval) + defer ticker.Stop() + + for range ticker.C { + prev := fs.healthy.Load() + ok := fs.ping() + fs.healthy.Store(ok) + + if ok && !prev { + // Redis 恢复 + fs.degraded.Store(false) + log.Println("[FallbackStore] Redis 已恢复,切回 Redis 存储") + } else if !ok && prev { + // Redis 掉线,标记降级 + fs.degraded.Store(true) + log.Println("[FallbackStore] Redis 不可用,已降级为内存存储") + } + } +} + +// ping 检查 Redis 连接状态(带超时) +func (fs *FallbackStore) ping() bool { + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + return fs.client.Ping(ctx).Err() == nil +} + +// current 返回当前应使用的 Store:Redis 健康用 Redis,否则用 Memory +func (fs *FallbackStore) current() Store { + if fs.healthy.Load() { + return fs.redis + } + return fs.memory +} + +// ---- Store 接口实现(委托给 current())---- + +func (fs *FallbackStore) Get(id string) (*Session, error) { + return fs.current().Get(id) +} + +func (fs *FallbackStore) Set(s *Session) error { + return fs.current().Set(s) +} + +func (fs *FallbackStore) Delete(id string) error { + return fs.current().Delete(id) +} + +func (fs *FallbackStore) DeleteByUID(uid uint) error { + return fs.current().DeleteByUID(uid) +} + +func (fs *FallbackStore) DeleteByUIDExclude(uid uint, excludeSID string) error { + return fs.current().DeleteByUIDExclude(uid, excludeSID) +} + +func (fs *FallbackStore) ListByUID(uid uint) ([]*Session, error) { + return fs.current().ListByUID(uid) +} + +func (fs *FallbackStore) Cleanup() int { + return fs.current().Cleanup() +} + +func (fs *FallbackStore) StartCleanup(interval time.Duration) { + // MemoryStore 已在外部启动 Cleanup,此处无需重复 +} + +// ---- Metrics(供管理面板使用)---- + +// Metrics 返回存储状态信息 +func (fs *FallbackStore) Metrics() StoreMetrics { + healthy := fs.healthy.Load() + isDegraded := fs.degraded.Load() + + m := StoreMetrics{ + Type: "redis", + Healthy: healthy, + } + + if !healthy { + m.Type = "memory" + m.Degraded = true + } else if isDegraded { + m.Degraded = true + m.DegradedNote = "Redis 已恢复,但存在历史降级记录(重启后清除)" + } + + // 活跃会话数:Redis 模式不统计(代价高),Memory 模式可从 memory store 获取 + if !healthy { + if memMetrics := fs.memory.Metrics(); memMetrics.Type == "memory" { + m.ActiveCount = memMetrics.ActiveCount + } + } + + return m +} diff --git a/internal/session/manager.go b/internal/session/manager.go index 4959a07..11332fc 100644 --- a/internal/session/manager.go +++ b/internal/session/manager.go @@ -127,3 +127,8 @@ func (m *Manager) UpdateRemark(sid string, uid uint, remark string) error { s.Remark = remark return m.store.Set(s) } + +// GetStoreMetrics 返回当前存储的状态信息 +func (m *Manager) GetStoreMetrics() StoreMetrics { + return m.store.Metrics() +} diff --git a/internal/session/memory_store.go b/internal/session/memory_store.go index 293e228..7e0cff1 100644 --- a/internal/session/memory_store.go +++ b/internal/session/memory_store.go @@ -199,3 +199,15 @@ func (ms *MemoryStore) StartCleanup(interval time.Duration) { } }() } + +// Metrics 返回内存存储状态 +func (ms *MemoryStore) Metrics() StoreMetrics { + ms.mu.RLock() + count := len(ms.sessions) + ms.mu.RUnlock() + return StoreMetrics{ + Type: "memory", + Healthy: true, + ActiveCount: count, + } +} diff --git a/internal/session/redis_store.go b/internal/session/redis_store.go index b0278b7..0f2c824 100644 --- a/internal/session/redis_store.go +++ b/internal/session/redis_store.go @@ -229,3 +229,12 @@ func (rs *RedisStore) Cleanup() int { func (rs *RedisStore) StartCleanup(interval time.Duration) { // Redis TTL 自动过期,无需手动清理 } + +// Metrics 返回 Redis 存储状态(活跃会话数统计代价高,返回 -1) +func (rs *RedisStore) Metrics() StoreMetrics { + return StoreMetrics{ + Type: "redis", + Healthy: true, + ActiveCount: -1, + } +} diff --git a/internal/session/store.go b/internal/session/store.go index 24baa3c..3f9275e 100644 --- a/internal/session/store.go +++ b/internal/session/store.go @@ -1,6 +1,15 @@ package session -// Store 会话存储接口(内存实现 / Redis 实现均实现此接口) +// StoreMetrics 存储状态信息(供管理面板展示) +type StoreMetrics struct { + Type string // "redis" 或 "memory" + Healthy bool // Redis 健康状态(memory 模式始终为 true) + Degraded bool // 是否处于降级模式 + DegradedNote string // 降级提示语(可选) + ActiveCount int // 活跃会话数,-1 表示不统计 +} + +// Store 会话存储接口(内存实现 / Redis 实现 / FallbackStore 均实现此接口) type Store interface { // Get 按会话 ID 获取会话,不存在或已过期返回 nil Get(id string) (*Session, error) @@ -22,4 +31,7 @@ type Store interface { // Cleanup 清理过期会话,返回清理数量(由后台 goroutine 定期调用) Cleanup() int + + // Metrics 返回存储状态信息(供管理面板展示) + Metrics() StoreMetrics }