From 3e2643160226d0179323ec636c26b1c76ef57786 Mon Sep 17 00:00:00 2001 From: Victor_Jay Date: Sun, 31 May 2026 12:14:10 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20Redis=20=E9=99=8D=E7=BA=A7=E6=9C=BA?= =?UTF-8?q?=E5=88=B6=E4=BC=98=E5=8C=96=EF=BC=9A=E5=90=AF=E5=8A=A8=E7=A6=BB?= =?UTF-8?q?=E7=BA=BF=E4=B8=8D=E5=B4=A9=E6=BA=83=20+=20=E6=93=8D=E4=BD=9C?= =?UTF-8?q?=E7=BA=A7=E5=8D=B3=E6=97=B6=E9=99=8D=E7=BA=A7=20+=20=E5=BF=AB?= =?UTF-8?q?=E9=80=9F=E6=81=A2=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 启动时 Redis 不可达不再 Fatal 退出,降级为内存模式继续运行 - FallbackStore 改为操作级降级:Redis 操作失败立即切换到内存,不依赖周期健康检查 - 新增 recoveryLoop:降级后每 5s 轮询尝试恢复,恢复后自动迁移内存会话回 Redis - 健康检查与清理解耦:检查 30s 间隔,恢复轮询 5s 间隔(原 300s) - 优化 go-redis 连接池参数(DialTimeout 1s, MaxRetries 1,减少故障阻塞时间) --- cmd/server/main.go | 8 +- internal/common/redis.go | 18 ++-- internal/router/deps_core.go | 2 +- internal/session/fallback_store.go | 129 ++++++++++++++++++++++++----- 4 files changed, 127 insertions(+), 30 deletions(-) diff --git a/cmd/server/main.go b/cmd/server/main.go index 2620cd4..e319014 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -70,14 +70,16 @@ func main() { r.Static("/uploads/avatars", "./storage/uploads/avatars") r.Static("/uploads/posts", "./storage/uploads/posts") - // Redis 客户端(可选) + // Redis 客户端(可选,不可达时自动降级为内存存储) var redisClient *redis.Client if cfg.Redis.Enabled { redisClient, err = common.NewRedisClient(cfg.Redis) if err != nil { - log.Fatalf("Redis 连接失败: %v", err) + log.Printf("Redis 不可用,降级为内存存储: %v", err) + redisClient = nil // 强制走纯内存模式 + } else { + log.Printf("Redis 已连接 %s:%s (DB=%d)", cfg.Redis.Host, cfg.Redis.Port, cfg.Redis.DB) } - log.Printf("Redis 已连接 %s:%s (DB=%d)", cfg.Redis.Host, cfg.Redis.Port, cfg.Redis.DB) } else { log.Println("Redis 未启用,使用内存存储") } diff --git a/internal/common/redis.go b/internal/common/redis.go index 7ecae6c..02f1766 100644 --- a/internal/common/redis.go +++ b/internal/common/redis.go @@ -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 diff --git a/internal/router/deps_core.go b/internal/router/deps_core.go index 3a659f5..6c43439 100644 --- a/internal/router/deps_core.go +++ b/internal/router/deps_core.go @@ -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 { // 纯内存模式 diff --git a/internal/session/fallback_store.go b/internal/session/fallback_store.go index a7874df..7c69598 100644 --- a/internal/session/fallback_store.go +++ b/internal/session/fallback_store.go @@ -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) {