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 a9b6dddf70 fix: 迁移成功后立即清理内存中已迁移的会话
- migrateSessions() 写入 Redis 后删除 MemoryStore 中对应数据
- 避免残留数据等到 TTL 自然过期才清理(最长 30 天)
2026-05-31 12:22:26 +08:00

274 lines
7.3 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 // 是否正在执行迁移(避免并发迁移)
recovering atomic.Bool // 是否正在执行快速恢复轮询
}
// 健康检查间隔:正常模式下定期检测 Redis 连通性
const healthCheckInterval = 30 * time.Second
// 快速恢复间隔:降级模式下高频轮询,快速切回 Redis
const recoveryInterval = 5 * time.Second
// NewFallbackStore 创建降级保护存储
func NewFallbackStore(redis *RedisStore, memory *MemoryStore) *FallbackStore {
fs := &FallbackStore{
redis: redis,
memory: memory,
client: redis.client,
}
fs.healthy.Store(true)
// 后台健康检测 + 快速恢复(独立 goroutine 互不阻塞)
go fs.healthLoop()
go fs.recoveryLoop()
log.Println("[FallbackStore] 已启动 Redis 健康检测(健康检测 30s降级恢复 5s含自动迁移")
return fs
}
// healthLoop 定期 ping Redis30s 间隔),自动升降级 + 静默迁移
func (fs *FallbackStore) healthLoop() {
ticker := time.NewTicker(healthCheckInterval)
defer ticker.Stop()
for range ticker.C {
prev := fs.healthy.Load()
ok := fs.ping()
if ok && !prev {
// Redis 恢复:先切到 Redis新会话立即走 Redis再迁移内存存量
fs.healthy.Store(true)
fs.migrateSessions()
fs.degraded.Store(false)
} else if !ok && prev {
// Redis 掉线,标记降级(快速恢复由 recoveryLoop 负责)
fs.healthy.Store(false)
fs.degraded.Store(true)
log.Println("[FallbackStore] Redis 不可用,已降级为内存存储")
} else {
fs.healthy.Store(ok)
}
}
}
// recoveryLoop 启动后持续监听退化状态一旦降级立即启动快速恢复轮询5s 间隔)
// 退出条件Redis 恢复成功或 connectOnce 竞争到恢复权限后执行迁移
func (fs *FallbackStore) recoveryLoop() {
ticker := time.NewTicker(recoveryInterval)
defer ticker.Stop()
for range ticker.C {
if fs.healthy.Load() {
continue // 正常模式,无需恢复
}
// 降级模式:尝试恢复
if !fs.recovering.CompareAndSwap(false, true) {
continue // 已有恢复在进行中
}
if !fs.ping() {
fs.recovering.Store(false)
continue // 仍未恢复
}
// Redis 已恢复
fs.healthy.Store(true)
fs.migrateSessions()
fs.degraded.Store(false)
fs.recovering.Store(false)
}
}
// 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
}
// 迁移成功后清理内存中的残留数据
for _, s := range sessions {
_ = fs.memory.Delete(s.ID)
}
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 接口实现Redis 操作失败时立即降级到内存)----
// 不依赖周期性健康检查,操作级故障即时响应
func (fs *FallbackStore) Get(id string) (*Session, error) {
if fs.healthy.Load() {
s, err := fs.redis.Get(id)
if err == nil {
return s, nil
}
log.Printf("[FallbackStore] Redis Get 失败,降级到内存: %v", err)
fs.healthy.Store(false)
fs.degraded.Store(true)
}
return fs.memory.Get(id)
}
func (fs *FallbackStore) Set(s *Session) error {
if fs.healthy.Load() {
if err := fs.redis.Set(s); err == nil {
return nil
}
log.Printf("[FallbackStore] Redis Set 失败,降级到内存")
fs.healthy.Store(false)
fs.degraded.Store(true)
}
return fs.memory.Set(s)
}
func (fs *FallbackStore) Delete(id string) error {
if fs.healthy.Load() {
if err := fs.redis.Delete(id); err == nil {
return nil
}
log.Printf("[FallbackStore] Redis Delete 失败,降级到内存")
fs.healthy.Store(false)
fs.degraded.Store(true)
}
return fs.memory.Delete(id)
}
func (fs *FallbackStore) DeleteByUID(uid uint) error {
if fs.healthy.Load() {
if err := fs.redis.DeleteByUID(uid); err == nil {
return nil
}
log.Printf("[FallbackStore] Redis DeleteByUID 失败,降级到内存")
fs.healthy.Store(false)
fs.degraded.Store(true)
}
return fs.memory.DeleteByUID(uid)
}
func (fs *FallbackStore) DeleteByUIDExclude(uid uint, excludeSID string) error {
if fs.healthy.Load() {
if err := fs.redis.DeleteByUIDExclude(uid, excludeSID); err == nil {
return nil
}
log.Printf("[FallbackStore] Redis DeleteByUIDExclude 失败,降级到内存")
fs.healthy.Store(false)
fs.degraded.Store(true)
}
return fs.memory.DeleteByUIDExclude(uid, excludeSID)
}
func (fs *FallbackStore) ListByUID(uid uint) ([]*Session, error) {
if fs.healthy.Load() {
sessions, err := fs.redis.ListByUID(uid)
if err == nil {
return sessions, nil
}
log.Printf("[FallbackStore] Redis ListByUID 失败,降级到内存: %v", err)
fs.healthy.Store(false)
fs.degraded.Store(true)
}
return fs.memory.ListByUID(uid)
}
func (fs *FallbackStore) Cleanup() int {
if fs.healthy.Load() {
return fs.redis.Cleanup()
}
return fs.memory.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,
ActiveCount: -1, // Redis 模式不统计活跃会话数(代价高),模板 ge 0 判断自动隐藏
}
if !healthy {
m.Type = "memory"
m.Degraded = true
m.ActiveCount = fs.memory.Metrics().ActiveCount
} else if isDegraded {
m.Degraded = true
m.DegradedNote = "Redis 已恢复,但存在历史降级记录(重启后清除)"
}
return m
}