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