This repository has been archived on 2026-06-21. You can view files and clone it, but cannot push or open issues or pull requests.
Files
MetaLab/internal/session/fallback_store.go
Victor_Jay 37c83d4c12 feat: FallbackStore 恢复时静默迁移内存会话到 Redis
- MemoryStore 新增 Snapshot() 返回活跃会话副本
- Redis 恢复时自动将降级期间的内存会话 Pipeline 写入 Redis
- 迁移成功后再切换 healthy 标记,用户无需重新登录
- 迁移失败则保持内存模式,避免切到空的 Redis 再被迫降级
2026-05-31 11:46:50 +08:00

186 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 session
import (
"context"
"encoding/json"
"fmt"
"log"
"sync/atomic"
"time"
"github.com/redis/go-redis/v9"
)
// FallbackStore 会话存储降级保护层
// Redis 正常时委托给 RedisStore异常时自动降级到 MemoryStore
// Redis 恢复时自动将降级期间的内存会话静默迁移回 Redis用户无感知
type FallbackStore struct {
redis *RedisStore
memory *MemoryStore
client *redis.Client
healthy atomic.Bool // Redis 当前是否健康
degraded atomic.Bool // 是否处于降级模式(曾因异常而切换到 Memory
migrating atomic.Bool // 是否正在执行迁移(避免并发迁移)
}
// NewFallbackStore 创建降级保护存储
// checkInterval: 健康检测间隔,建议与 session cleanup_interval 一致
func NewFallbackStore(redis *RedisStore, memory *MemoryStore, 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()
if ok && !prev {
// Redis 恢复:先迁移内存会话,再切回 Redis
fs.migrateSessions()
fs.healthy.Store(true)
fs.degraded.Store(false)
} else if !ok && prev {
// Redis 掉线,标记降级
fs.healthy.Store(false)
fs.degraded.Store(true)
log.Println("[FallbackStore] Redis 不可用,已降级为内存存储")
} else {
fs.healthy.Store(ok)
}
}
}
// migrateSessions 将降级期间内存中的活跃会话同步到 Redis
func (fs *FallbackStore) migrateSessions() {
if !fs.migrating.CompareAndSwap(false, true) {
return // 已有迁移在进行中
}
defer fs.migrating.Store(false)
sessions := fs.memory.Snapshot()
if len(sessions) == 0 {
log.Println("[FallbackStore] 无需迁移(降级期间无活跃会话)")
return
}
ctx := context.Background()
pipe := fs.client.Pipeline()
for _, s := range sessions {
data, err := json.Marshal(s)
if err != nil {
log.Printf("[FallbackStore] 迁移序列化失败 sid=%s err=%v", s.ID[:16]+"...", err)
continue
}
ttl := fs.redis.idleTimeout
if s.RememberMe {
ttl = fs.redis.rememberTimeout
}
pipe.Set(ctx, keyPrefix+s.ID, data, ttl)
pipe.SAdd(ctx, userKeyPrefix+fmt.Sprint(s.UserID), s.ID)
}
_, err := pipe.Exec(ctx)
if err != nil {
log.Printf("[FallbackStore] 迁移到 Redis 失败: %v保持内存模式", err)
return
}
log.Printf("[FallbackStore] Redis 已恢复,静默迁移 %d 条会话", len(sessions))
}
// 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 返回当前应使用的 StoreRedis 健康用 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
}