fix: Redis 降级机制优化:启动离线不崩溃 + 操作级即时降级 + 快速恢复
- 启动时 Redis 不可达不再 Fatal 退出,降级为内存模式继续运行 - FallbackStore 改为操作级降级:Redis 操作失败立即切换到内存,不依赖周期健康检查 - 新增 recoveryLoop:降级后每 5s 轮询尝试恢复,恢复后自动迁移内存会话回 Redis - 健康检查与清理解耦:检查 30s 间隔,恢复轮询 5s 间隔(原 300s) - 优化 go-redis 连接池参数(DialTimeout 1s, MaxRetries 1,减少故障阻塞时间)
This commit is contained in:
@ -3,25 +3,33 @@ package common
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
"metazone.cc/metalab/internal/config"
|
||||
)
|
||||
|
||||
// NewRedisClient 创建 Redis 客户端并验证连接
|
||||
// NewRedisClient 创建 Redis 客户端并尝试验证连接
|
||||
// Redis 不可达时不返回错误,而是返回 client(go-redis 自带自动重连),由调用方决定降级策略
|
||||
func NewRedisClient(cfg config.RedisConfig) (*redis.Client, error) {
|
||||
client := redis.NewClient(&redis.Options{
|
||||
Addr: fmt.Sprintf("%s:%s", cfg.Host, cfg.Port),
|
||||
Password: cfg.Password,
|
||||
DB: cfg.DB,
|
||||
Addr: fmt.Sprintf("%s:%s", cfg.Host, cfg.Port),
|
||||
Password: cfg.Password,
|
||||
DB: cfg.DB,
|
||||
DialTimeout: 1 * time.Second, // 连接超时降低(默认 5s),配合 FallbackStore 快速降级
|
||||
ReadTimeout: 1 * time.Second,
|
||||
WriteTimeout: 1 * time.Second,
|
||||
MaxRetries: 1, // 最多重试 1 次(默认 3 次),减少故障阻塞时间
|
||||
PoolSize: 5, // 会话存储不需要大连接池(默认 10*GOMAXPROCS)
|
||||
})
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err := client.Ping(ctx).Err(); err != nil {
|
||||
return nil, fmt.Errorf("Redis 连接失败: %w", err)
|
||||
log.Printf("[Redis] 连接失败: %v,将降级为内存存储", err)
|
||||
return client, fmt.Errorf("Redis 连接失败: %w", err)
|
||||
}
|
||||
|
||||
return client, nil
|
||||
|
||||
@ -51,7 +51,7 @@ func buildDepsCore(db *gorm.DB, cfg *config.Config, siteSettings *config.SiteSet
|
||||
if cfg.Redis.Enabled && redisClient != nil {
|
||||
// Redis 模式 + 自动降级保护
|
||||
redisStore := session.NewRedisStore(redisClient, idleTimeout, rememberTimeout)
|
||||
sessionStore = session.NewFallbackStore(redisStore, memStore, cleanupInterval)
|
||||
sessionStore = session.NewFallbackStore(redisStore, memStore)
|
||||
log.Println("[Router] 会话存储: Redis(带内存降级保护)")
|
||||
} else {
|
||||
// 纯内存模式
|
||||
|
||||
@ -19,14 +19,20 @@ type FallbackStore struct {
|
||||
memory *MemoryStore
|
||||
client *redis.Client
|
||||
|
||||
healthy atomic.Bool // Redis 当前是否健康
|
||||
degraded atomic.Bool // 是否处于降级模式(曾因异常而切换到 Memory)
|
||||
migrating atomic.Bool // 是否正在执行迁移(避免并发迁移)
|
||||
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 创建降级保护存储
|
||||
// checkInterval: 健康检测间隔,建议与 session cleanup_interval 一致
|
||||
func NewFallbackStore(redis *RedisStore, memory *MemoryStore, checkInterval time.Duration) *FallbackStore {
|
||||
func NewFallbackStore(redis *RedisStore, memory *MemoryStore) *FallbackStore {
|
||||
fs := &FallbackStore{
|
||||
redis: redis,
|
||||
memory: memory,
|
||||
@ -34,16 +40,17 @@ func NewFallbackStore(redis *RedisStore, memory *MemoryStore, checkInterval time
|
||||
}
|
||||
fs.healthy.Store(true)
|
||||
|
||||
// 后台健康检测 goroutine
|
||||
go fs.healthLoop(checkInterval)
|
||||
// 后台健康检测 + 快速恢复(独立 goroutine 互不阻塞)
|
||||
go fs.healthLoop()
|
||||
go fs.recoveryLoop()
|
||||
|
||||
log.Println("[FallbackStore] 已启动 Redis 健康检测(含自动迁移)")
|
||||
log.Println("[FallbackStore] 已启动 Redis 健康检测(健康检测 30s,降级恢复 5s,含自动迁移)")
|
||||
return fs
|
||||
}
|
||||
|
||||
// healthLoop 定期 ping Redis,自动升降级 + 静默迁移
|
||||
func (fs *FallbackStore) healthLoop(interval time.Duration) {
|
||||
ticker := time.NewTicker(interval)
|
||||
// healthLoop 定期 ping Redis(30s 间隔),自动升降级 + 静默迁移
|
||||
func (fs *FallbackStore) healthLoop() {
|
||||
ticker := time.NewTicker(healthCheckInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for range ticker.C {
|
||||
@ -52,12 +59,11 @@ func (fs *FallbackStore) healthLoop(interval time.Duration) {
|
||||
|
||||
if ok && !prev {
|
||||
// Redis 恢复:先切到 Redis(新会话立即走 Redis),再迁移内存存量
|
||||
// 先切后迁:Snapshot 和 healthy 切换之间零 gap,不存在遗漏
|
||||
fs.healthy.Store(true)
|
||||
fs.migrateSessions()
|
||||
fs.degraded.Store(false)
|
||||
} else if !ok && prev {
|
||||
// Redis 掉线,标记降级
|
||||
// Redis 掉线,标记降级(快速恢复由 recoveryLoop 负责)
|
||||
fs.healthy.Store(false)
|
||||
fs.degraded.Store(true)
|
||||
log.Println("[FallbackStore] Redis 不可用,已降级为内存存储")
|
||||
@ -67,6 +73,33 @@ func (fs *FallbackStore) healthLoop(interval time.Duration) {
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
@ -121,34 +154,88 @@ func (fs *FallbackStore) current() Store {
|
||||
return fs.memory
|
||||
}
|
||||
|
||||
// ---- Store 接口实现(委托给 current())----
|
||||
// ---- Store 接口实现(Redis 操作失败时立即降级到内存)----
|
||||
// 不依赖周期性健康检查,操作级故障即时响应
|
||||
|
||||
func (fs *FallbackStore) Get(id string) (*Session, error) {
|
||||
return fs.current().Get(id)
|
||||
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 {
|
||||
return fs.current().Set(s)
|
||||
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 {
|
||||
return fs.current().Delete(id)
|
||||
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 {
|
||||
return fs.current().DeleteByUID(uid)
|
||||
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 {
|
||||
return fs.current().DeleteByUIDExclude(uid, excludeSID)
|
||||
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) {
|
||||
return fs.current().ListByUID(uid)
|
||||
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 {
|
||||
return fs.current().Cleanup()
|
||||
if fs.healthy.Load() {
|
||||
return fs.redis.Cleanup()
|
||||
}
|
||||
return fs.memory.Cleanup()
|
||||
}
|
||||
|
||||
func (fs *FallbackStore) StartCleanup(interval time.Duration) {
|
||||
|
||||
Reference in New Issue
Block a user