feat: 实现 FallbackStore 会话存储降级保护
- 新增 session/fallback_store.go,包装 RedisStore + MemoryStore 双后端 - 后台 goroutine 周期性 ping Redis,自动降级/恢复 - Store 接口新增 Metrics() 方法,MemoryStore/RedisStore/FallbackStore 均已实现 - deps_core.go 改造为始终创建 MemoryStore 兜底,Redis 启用时包装为 FallbackStore - Manager 新增 GetStoreMetrics() 暴露存储状态
This commit is contained in:
@ -40,17 +40,21 @@ func buildDepsCore(db *gorm.DB, cfg *config.Config, siteSettings *config.SiteSet
|
|||||||
|
|
||||||
idleTimeout := time.Duration(cfg.Session.IdleTimeout) * time.Minute
|
idleTimeout := time.Duration(cfg.Session.IdleTimeout) * time.Minute
|
||||||
rememberTimeout := time.Duration(cfg.Session.RememberTimeout) * 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
|
var sessionStore session.Store
|
||||||
|
|
||||||
if cfg.Redis.Enabled && redisClient != nil {
|
if cfg.Redis.Enabled && redisClient != nil {
|
||||||
// Redis 模式(生产环境推荐)
|
// Redis 模式 + 自动降级保护
|
||||||
sessionStore = session.NewRedisStore(redisClient, idleTimeout, rememberTimeout)
|
redisStore := session.NewRedisStore(redisClient, idleTimeout, rememberTimeout)
|
||||||
log.Println("[Router] 会话存储: Redis")
|
sessionStore = session.NewFallbackStore(redisStore, memStore, cleanupInterval)
|
||||||
|
log.Println("[Router] 会话存储: Redis(带内存降级保护)")
|
||||||
} else {
|
} else {
|
||||||
// 内存模式(开发/单实例)
|
// 纯内存模式
|
||||||
memStore := session.NewMemoryStore(idleTimeout, rememberTimeout)
|
|
||||||
memStore.StartCleanup(time.Duration(cfg.Session.CleanupInterval) * time.Second)
|
|
||||||
sessionStore = memStore
|
sessionStore = memStore
|
||||||
log.Println("[Router] 会话存储: 内存")
|
log.Println("[Router] 会话存储: 内存")
|
||||||
}
|
}
|
||||||
|
|||||||
140
internal/session/fallback_store.go
Normal file
140
internal/session/fallback_store.go
Normal file
@ -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
|
||||||
|
}
|
||||||
@ -127,3 +127,8 @@ func (m *Manager) UpdateRemark(sid string, uid uint, remark string) error {
|
|||||||
s.Remark = remark
|
s.Remark = remark
|
||||||
return m.store.Set(s)
|
return m.store.Set(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetStoreMetrics 返回当前存储的状态信息
|
||||||
|
func (m *Manager) GetStoreMetrics() StoreMetrics {
|
||||||
|
return m.store.Metrics()
|
||||||
|
}
|
||||||
|
|||||||
@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -229,3 +229,12 @@ func (rs *RedisStore) Cleanup() int {
|
|||||||
func (rs *RedisStore) StartCleanup(interval time.Duration) {
|
func (rs *RedisStore) StartCleanup(interval time.Duration) {
|
||||||
// Redis TTL 自动过期,无需手动清理
|
// Redis TTL 自动过期,无需手动清理
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Metrics 返回 Redis 存储状态(活跃会话数统计代价高,返回 -1)
|
||||||
|
func (rs *RedisStore) Metrics() StoreMetrics {
|
||||||
|
return StoreMetrics{
|
||||||
|
Type: "redis",
|
||||||
|
Healthy: true,
|
||||||
|
ActiveCount: -1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -1,6 +1,15 @@
|
|||||||
package session
|
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 {
|
type Store interface {
|
||||||
// Get 按会话 ID 获取会话,不存在或已过期返回 nil
|
// Get 按会话 ID 获取会话,不存在或已过期返回 nil
|
||||||
Get(id string) (*Session, error)
|
Get(id string) (*Session, error)
|
||||||
@ -22,4 +31,7 @@ type Store interface {
|
|||||||
|
|
||||||
// Cleanup 清理过期会话,返回清理数量(由后台 goroutine 定期调用)
|
// Cleanup 清理过期会话,返回清理数量(由后台 goroutine 定期调用)
|
||||||
Cleanup() int
|
Cleanup() int
|
||||||
|
|
||||||
|
// Metrics 返回存储状态信息(供管理面板展示)
|
||||||
|
Metrics() StoreMetrics
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user