diff --git a/cache.html b/cache.html new file mode 100644 index 0000000..010c696 --- /dev/null +++ b/cache.html @@ -0,0 +1,2552 @@ + + + + + + 缓存三大问题:击穿、雪崩、穿透 机制与原理详解 + + + + +
+

🔥 缓存三大问题:机制与原理详解

+

缓存击穿 · 缓存雪崩 · 缓存穿透 — 深入理解问题本质与解决方案

+
+ +
+ +
+ + + +
+ + +
+ + +
+

💥 缓存击穿 — 问题描述

+ +
+ ⚠️ +
+ 核心问题:一个热点 Key(被大量并发访问的缓存数据)在过期的瞬间,大量并发请求同时发现缓存失效,全部穿透到数据库,导致数据库压力骤增甚至崩溃。 +
+
+ +
+

🎯 问题本质

+

缓存击穿的关键词是"热点"和"过期"。与缓存穿透不同,击穿针对的是确实存在的数据,只是恰好在某一时刻缓存失效了。

+
    +
  • 前提条件:某个 Key 是热点数据,访问量极大(如微博热搜、秒杀商品)
  • +
  • 触发条件:该 Key 的缓存恰好在某一时刻过期
  • +
  • 直接后果:瞬间大量请求同时 miss 缓存,全部打到数据库
  • +
  • 最终影响:数据库连接池耗尽,响应时间飙升,服务不可用
  • +
+
+ +

📊 时序图 — 击穿发生过程

+
+
+
+
T0 — 正常运行
+

热点数据 hot_key 存在于缓存中,所有请求直接命中缓存返回,数据库无压力。

+
+
+
T1 — 缓存过期
+

hot_key 的 TTL 到期,缓存中该数据被删除。

+
+
+
T2 — 并发穿透
+

同一时刻,10000+ 个请求到达,全部 cache miss,同时查询数据库。

+
+
+
T3 — 数据库过载
+

数据库瞬间承受 10000+ 并发查询,连接池耗尽,响应时间从 5ms 飙升到 5000ms+。

+
+
+
T4 — 恢复
+

第一个请求完成查询并回写缓存,后续请求重新命中缓存,系统逐渐恢复。

+
+
+
+ +

🔍 典型场景

+
+
+

🛒 秒杀活动

+

商品详情缓存过期瞬间,数万用户同时请求,全部穿透到数据库。

+
+
+

📰 热点新闻

+

突发新闻页面缓存失效,大量用户刷新页面导致数据库压力暴增。

+
+
+

👤 大V主页

+

知名用户的主页数据缓存过期,粉丝频繁访问导致数据库过载。

+
+
+
+ + +
+

🛡️ 解决方案总览

+ +
+

针对缓存击穿问题,核心思路是避免大量请求同时穿透到数据库。以下是两种主流解决方案:

+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
方案核心思路数据一致性可用性适用场景
🔒 逻辑过期(永不过期)缓存不设物理 TTL,在 value 中存储逻辑过期时间,过期后异步重建弱(可能返回旧数据)高(不阻塞)对一致性要求不高的热点数据
🔐 互斥锁排队缓存 miss 时用分布式锁控制,只允许一个线程查 DB 重建缓存强(等待最新数据)中(可能等待超时)对一致性要求较高的场景
+
+ +
+ 💡 +
+ 选择建议:如果业务允许短暂返回旧数据(如社交动态、商品浏览),优先选择逻辑过期方案保证高可用;如果对数据一致性要求严格(如库存、余额),选择互斥锁方案。 +
+
+
+ + +
+

🔒 解决方案一:逻辑过期(永不过期)

+ +
+ 💡 +
+ 核心思路:缓存不设置物理 TTL(永不过期),而是在 value 中存储一个逻辑过期时间。读取时检查是否逻辑过期,如果过期则由一个线程异步重建缓存,其他线程返回旧数据。 +
+
+ +

📐 设计原理

+
+
    +
  1. 缓存不设 TTL:数据一旦写入缓存,不会被 Redis 自动删除
  2. +
  3. 逻辑过期字段:在 value 中增加 expire_time 字段,记录业务层面的过期时间
  4. +
  5. 读取时判断: +
      +
    • 若未逻辑过期 → 直接返回缓存数据
    • +
    • 若已逻辑过期 → 尝试获取互斥锁,获取成功则异步重建缓存,获取失败则返回旧数据
    • +
    +
  6. +
  7. 异步重建:获取到锁的线程从数据库查询最新数据,更新缓存中的值和逻辑过期时间
  8. +
+
+ +

🔄 流程图

+
+
+
请求到达
+ → +
查询缓存
+ → +
检查逻辑过期?
+
+
+
未过期 → 直接返回
+ | +
已过期 → 获取互斥锁
+
+
+
获取成功 → 查DB → 更新缓存
+ | +
获取失败 → 返回旧数据
+
+
+ +

💻 Go 代码实现

+
+
+
+ logical_expire.go — 逻辑过期方案 +
+
package cache
+
+import (
+    "context"
+    "encoding/json"
+    "fmt"
+    "sync"
+    "time"
+
+    "github.com/redis/go-redis/v9"
+)
+
+// CacheData 带逻辑过期的缓存数据结构
+type CacheData struct {
+    Data       json.RawMessage `json:"data"`       // 实际业务数据
+    ExpireTime int64           `json:"expire_time"` // 逻辑过期时间(Unix时间戳)
+}
+
+// LogicalExpireCache 逻辑过期缓存实现
+type LogicalExpireCache struct {
+    rdb        *redis.Client
+    localMutex sync.Map // 本地互斥锁,防止多个goroutine同时重建
+}
+
+func NewLogicalExpireCache(rdb *redis.Client) *LogicalExpireCache {
+    return &LogicalExpireCache{rdb: rdb}
+}
+
+// Get 获取缓存数据(核心逻辑)
+func (c *LogicalExpireCache) Get(
+    ctx context.Context,
+    key string,
+    logicalTTL time.Duration,
+    loadFn func(ctx context.Context) (interface{}, error),
+) (interface{}, error) {
+    // Step 1: 从缓存中获取数据
+    val, err := c.rdb.Get(ctx, key).Result()
+    if err == redis.Nil {
+        // 缓存不存在(首次),需要加载并设置
+        return c.loadAndSet(ctx, key, logicalTTL, loadFn)
+    }
+    if err != nil {
+        return nil, fmt.Errorf("redis get error: %w", err)
+    }
+
+    // Step 2: 反序列化
+    var cacheData CacheData
+    if err := json.Unmarshal([]byte(val), &cacheData); err != nil {
+        return nil, fmt.Errorf("unmarshal error: %w", err)
+    }
+
+    // Step 3: 检查是否逻辑过期
+    if time.Now().Unix() < cacheData.ExpireTime {
+        // 未过期,直接返回缓存数据
+        var result interface{}
+        json.Unmarshal(cacheData.Data, &result)
+        return result, nil
+    }
+
+    // Step 4: 已过期,尝试获取互斥锁进行异步重建
+    lockKey := "lock:" + key
+    locked := c.tryLock(ctx, lockKey, 5*time.Second)
+    if locked {
+        // 获取锁成功,开启goroutine异步重建缓存
+        go func() {
+            defer c.unlock(ctx, lockKey)
+            c.loadAndSet(ctx, key, logicalTTL, loadFn)
+        }()
+    }
+    // 无论是否获取到锁,都返回旧数据(保证可用性)
+    var result interface{}
+    json.Unmarshal(cacheData.Data, &result)
+    return result, nil
+}
+
+// loadAndSet 从数据库加载数据并写入缓存
+func (c *LogicalExpireCache) loadAndSet(
+    ctx context.Context,
+    key string,
+    logicalTTL time.Duration,
+    loadFn func(ctx context.Context) (interface{}, error),
+) (interface{}, error) {
+    // 双重检查:再次从缓存获取(可能其他goroutine已重建)
+    val, _ := c.rdb.Get(ctx, key).Result()
+    if val != "" {
+        var cd CacheData
+        json.Unmarshal([]byte(val), &cd)
+        if time.Now().Unix() < cd.ExpireTime {
+            var result interface{}
+            json.Unmarshal(cd.Data, &result)
+            return result, nil
+        }
+    }
+
+    // 从数据库加载最新数据
+    data, err := loadFn(ctx)
+    if err != nil {
+        return nil, err
+    }
+
+    // 构造带逻辑过期的缓存值
+    dataBytes, _ := json.Marshal(data)
+    cacheData := CacheData{
+        Data:       dataBytes,
+        ExpireTime: time.Now().Add(logicalTTL).Unix(),
+    }
+    cacheBytes, _ := json.Marshal(cacheData)
+
+    // 写入Redis(不设TTL,永不过期)
+    c.rdb.Set(ctx, key, cacheBytes, 0) // TTL=0 表示永不过期
+
+    return data, nil
+}
+
+// tryLock 尝试获取分布式锁
+func (c *LogicalExpireCache) tryLock(ctx context.Context, key string, ttl time.Duration) bool {
+    return c.rdb.SetNX(ctx, key, "1", ttl).Val()
+}
+
+// unlock 释放分布式锁
+func (c *LogicalExpireCache) unlock(ctx context.Context, key string) {
+    c.rdb.Del(ctx, key)
+}
+
+ +
+ ⚡ +
+ 优缺点分析:
+ ✅ 优点:不会阻塞用户请求,始终返回数据(可能稍旧),可用性高
+ ❌ 缺点:数据一致性有延迟(过期后一段时间内返回旧数据);需要额外内存存储过期时间字段;缓存不会自动清理,需要额外机制处理不活跃数据 +
+
+
+ + +
+

🔐 解决方案二:互斥锁排队

+ +
+ 💡 +
+ 核心思路:缓存失效时,不立即去查数据库,而是先尝试获取互斥锁。只有获取到锁的线程才去查询数据库并重建缓存,其他线程等待或重试。 +
+
+ +

📐 设计原理

+
+
    +
  1. 缓存正常设置 TTL:使用正常的过期策略
  2. +
  3. 缓存 miss 时获取锁:使用 Redis 的 SETNX 实现分布式互斥锁
  4. +
  5. 获取锁成功:查询数据库 → 写入缓存 → 释放锁
  6. +
  7. 获取锁失败:短暂休眠后重试(sleep + retry),直到缓存重建完成
  8. +
+
+ +

💻 Go 代码实现

+
+
+
+ mutex_lock.go — 互斥锁排队方案 +
+
package cache
+
+import (
+    "context"
+    "errors"
+    "fmt"
+    "time"
+
+    "github.com/redis/go-redis/v9"
+)
+
+var (
+    ErrCacheMiss  = errors.New("cache miss")
+    ErrDBError    = errors.New("database error")
+)
+
+// MutexCache 互斥锁缓存实现
+type MutexCache struct {
+    rdb         *redis.Client
+    retryCount  int           // 重试次数
+    retryDelay  time.Duration // 重试间隔
+    lockTTL     time.Duration // 锁的过期时间
+}
+
+func NewMutexCache(rdb *redis.Client) *MutexCache {
+    return &MutexCache{
+        rdb:        rdb,
+        retryCount: 10,
+        retryDelay: 50 * time.Millisecond,
+        lockTTL:    10 * time.Second,
+    }
+}
+
+// Get 带互斥锁的缓存查询
+func (c *MutexCache) Get(
+    ctx context.Context,
+    key string,
+    ttl time.Duration,
+    loadFn func(ctx context.Context) (interface{}, error),
+) (interface{}, error) {
+    // Step 1: 尝试从缓存获取
+    val, err := c.rdb.Get(ctx, key).Result()
+    if err == nil {
+        // 缓存命中,直接返回
+        return val, nil
+    }
+    if err != redis.Nil {
+        return nil, fmt.Errorf("redis error: %w", err)
+    }
+
+    // Step 2: 缓存未命中,尝试获取互斥锁
+    lockKey := "lock:" + key
+    return c.getWithLock(ctx, key, lockKey, ttl, loadFn)
+}
+
+// getWithLock 带锁的缓存重建逻辑
+func (c *MutexCache) getWithLock(
+    ctx context.Context,
+    key, lockKey string,
+    ttl time.Duration,
+    loadFn func(ctx context.Context) (interface{}, error),
+) (interface{}, error) {
+    for i := 0; i < c.retryCount; i++ {
+        // 尝试获取分布式锁
+        locked, err := c.rdb.SetNX(ctx, lockKey, "1", c.lockTTL).Result()
+        if err != nil {
+            return nil, fmt.Errorf("set lock error: %w", err)
+        }
+
+        if locked {
+            // 获取锁成功!执行数据库查询和缓存重建
+            defer c.rdb.Del(ctx, lockKey) // 确保释放锁
+
+            // 双重检查:获取锁后再次检查缓存
+            // (可能其他线程已在我们等锁期间重建了缓存)
+            val, err := c.rdb.Get(ctx, key).Result()
+            if err == nil {
+                return val, nil
+            }
+
+            // 从数据库加载数据
+            data, err := loadFn(ctx)
+            if err != nil {
+                return nil, fmt.Errorf("load from db error: %w", err)
+            }
+
+            // 写入缓存
+            c.rdb.Set(ctx, key, data, ttl)
+            return data, nil
+        }
+
+        // 获取锁失败,等待后重试
+        time.Sleep(c.retryDelay)
+
+        // 重试时先检查缓存是否已重建
+        val, err := c.rdb.Get(ctx, key).Result()
+        if err == nil {
+            return val, nil
+        }
+    }
+
+    return nil, errors.New("timeout: failed to acquire lock after retries")
+}
+
+ +

📊 两种方案对比

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
对比维度逻辑过期(永不过期)互斥锁排队
数据一致性弱一致性(可能返回旧数据)强一致性(等待最新数据)
可用性高(不阻塞,始终返回数据)中(可能等待超时)
实现复杂度中等(需维护逻辑过期字段)中等(需处理锁竞争)
资源消耗额外内存(过期时间字段)线程等待(CPU/内存)
适用场景对一致性要求不高的热点数据对一致性要求较高的场景
+
+
+
+ + +
+ + +
+

🏔️ 缓存雪崩 — 问题描述

+ +
+ ⚠️ +
+ 核心问题:大量缓存 Key 在同一时刻集中过期,或者 Redis 服务整体宕机,导致所有请求全部穿透到数据库,引发数据库崩溃,进而整个系统瘫痪。 +
+
+ +
+

🎯 与缓存击穿的区别

+
+
+
❌ 缓存击穿
+
    +
  • 单个热点 Key 过期
  • +
  • 影响范围:一个数据的并发访问
  • +
  • 关键词:热点 单个Key
  • +
+
+
+
❌ 缓存雪崩
+
    +
  • 大量 Key 同时过期
  • +
  • 影响范围:整个系统的缓存层
  • +
  • 关键词:批量 同时过期
  • +
+
+
+
+ +

📊 雪崩发生的两种场景

+
+
+

场景一:大量 Key 同时过期

+

缓存数据在初始化时设置了相同的 TTL(如都是 30 分钟),30 分钟后所有缓存同时失效。

+
+ TTL 相同 + 同时失效 +
+
+
+

场景二:Redis 服务宕机

+

Redis 节点故障、网络分区、内存溢出等原因导致整个缓存服务不可用。

+
+ 服务不可用 + 全部 miss +
+
+
+ +

📈 雪崩影响示意

+
+
+
用户请求 (10000 QPS)
+
+
↓
+
+
Redis 缓存层 (全部失效/宕机)
+
+
↓ 全部穿透
+
+
MySQL (10000 QPS 直接冲击 💀)
+
+
↓
+
+
系统崩溃 ❌
+
+
+
+ + +
+

🛡️ 解决方案总览

+ +
+

针对缓存雪崩问题,需要从预防过期集中、限流降级保护、高可用架构三个层面进行防护:

+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
方案核心思路防护层面实现复杂度适用场景
🎲 随机失效时间在基础 TTL 上加随机值,分散 Key 的过期时间预防层极低所有场景(必做)
🔐 加锁排队 + 限流降级通过互斥锁 + 信号量 + 限流器控制并发,保护数据库保护层中等高并发场景
🏗️ Redis 高可用架构使用哨兵模式或 Cluster 集群,防止 Redis 整体宕机架构层中高生产环境(推荐)
+
+ +
+ 💡 +
+ 最佳实践:三种方案应组合使用。随机 TTL 是基础防御,限流降级是兜底保护,Redis 高可用是架构保障。生产环境建议至少使用哨兵模式,大型项目推荐 Redis Cluster。 +
+
+
+ + +
+

🎲 解决方案一:随机失效时间

+ +
+ 💡 +
+ 核心思路:在设置缓存 TTL 时,在基础过期时间上增加一个随机值,使得不同 Key 的过期时间分散开,避免同时过期。 +
+
+ +

📐 原理说明

+
+

公式:实际TTL = 基础TTL + random(0, 随机范围)

+

例如基础 TTL 为 30 分钟,随机范围为 1~5 分钟,则实际过期时间在 31~35 分钟之间随机分布。

+
    +
  • 即使有 100 万个 Key,它们的过期时间也会分散在 4 分钟的窗口内
  • +
  • 任何时刻过期的 Key 数量只是总量的很小一部分
  • +
  • 数据库压力被平滑分散
  • +
+
+ +

💻 Go 代码实现

+
+
+
+ random_ttl.go — 随机过期时间 +
+
package cache
+
+import (
+    "context"
+    "math/rand"
+    "time"
+
+    "github.com/redis/go-redis/v9"
+)
+
+// RandomTTLCache 随机TTL缓存实现
+type RandomTTLCache struct {
+    rdb       *redis.Client
+    baseTTL   time.Duration // 基础过期时间
+    jitterMax time.Duration // 随机抖动最大值
+}
+
+func NewRandomTTLCache(rdb *redis.Client, baseTTL, jitterMax time.Duration) *RandomTTLCache {
+    return &RandomTTLCache{
+        rdb:       rdb,
+        baseTTL:   baseTTL,
+        jitterMax: jitterMax,
+    }
+}
+
+// Set 设置缓存(带随机TTL)
+func (c *RandomTTLCache) Set(ctx context.Context, key string, value interface{}) error {
+    // 计算实际TTL = 基础TTL + 随机抖动
+    actualTTL := c.baseTTL + time.Duration(rand.Int63n(int64(c.jitterMax)))
+  
+    return c.rdb.Set(ctx, key, value, actualTTL).Err()
+}
+
+// SetBatch 批量设置缓存(每个Key独立随机TTL)
+func (c *RandomTTLCache) SetBatch(ctx context.Context, items map[string]interface{}) error {
+    pipe := c.rdb.Pipeline()
+  
+    for key, value := range items {
+        // 每个Key使用独立的随机TTL
+        actualTTL := c.baseTTL + time.Duration(rand.Int63n(int64(c.jitterMax)))
+        pipe.Set(ctx, key, value, actualTTL)
+    }
+  
+    _, err := pipe.Exec(ctx)
+    return err
+}
+
+// 使用示例
+func Example() {
+    rdb := redis.NewClient(&redis.Options{Addr: "localhost:6379"})
+  
+    // 基础TTL 30分钟,随机抖动 0~5分钟
+    cache := NewRandomTTLCache(rdb, 30*time.Minute, 5*time.Minute)
+  
+    // 设置商品缓存
+    ctx := context.Background()
+    cache.Set(ctx, "product:1001", productData1) // 可能 32分钟后过期
+    cache.Set(ctx, "product:1002", productData2) // 可能 34分钟后过期
+    cache.Set(ctx, "product:1003", productData3) // 可能 31分钟后过期
+    // ... 每个Key的过期时间都不同,避免雪崩
+}
+
+ +

📊 效果对比

+
+

固定 TTL vs 随机 TTL — 过期分布

+
+

❌ 固定 TTL (全部 30 分钟):

+
+
T=30min: 100% Key 同时过期 💥
+
+

✅ 随机 TTL (30~35 分钟):

+
+
+
+
+
+
+
+
+
+

每个时间点只有少量 Key 过期,数据库压力被平滑分散

+
+
+
+ + +
+

🔐 解决方案二:加锁排队(限流降级)

+ +
+ 💡 +
+ 核心思路:即使大量缓存同时失效,通过互斥锁控制同时查询数据库的并发数量,配合限流降级策略保护数据库。 +
+
+ +

💻 Go 代码实现 — 多级保护

+
+
+
+ avalanche_protection.go — 雪崩多级保护 +
+
package cache
+
+import (
+    "context"
+    "errors"
+    "math/rand"
+    "sync"
+    "time"
+
+    "github.com/redis/go-redis/v9"
+    "golang.org/x/time/rate"
+)
+
+// AvalancheProtection 雪崩综合防护
+type AvalancheProtection struct {
+    rdb       *redis.Client
+    limiter   *rate.Limiter      // 限流器
+    semaphore chan struct{}     // 信号量,控制并发查DB的数量
+    mu        sync.Mutex
+    baseTTL   time.Duration
+    jitterMax time.Duration
+}
+
+func NewAvalancheProtection(rdb *redis.Client, maxDBConcurrency int) *AvalancheProtection {
+    return &AvalancheProtection{
+        rdb:       rdb,
+        limiter:   rate.NewLimiter(1000, 2000), // 每秒1000个请求,桶容量2000
+        semaphore: make(chan struct{}, maxDBConcurrency), // 最多N个goroutine同时查DB
+        baseTTL:   30 * time.Minute,
+        jitterMax: 10 * time.Minute,
+    }
+}
+
+// Get 带多级保护的缓存查询
+func (p *AvalancheProtection) Get(
+    ctx context.Context,
+    key string,
+    loadFn func(ctx context.Context) (interface{}, error),
+) (interface{}, error) {
+    // 第一层保护:限流
+    if !p.limiter.Allow() {
+        return nil, errors.New("rate limit exceeded, please retry later")
+    }
+
+    // 尝试从缓存获取
+    val, err := p.rdb.Get(ctx, key).Result()
+    if err == nil {
+        return val, nil
+    }
+
+    // 第二层保护:信号量控制并发查DB数量
+    select {
+    case p.semaphore <- struct{}{}:
+        defer func() { <-p.semaphore }()
+    case <-time.After(3 * time.Second):
+        // 等待超时,返回降级数据
+        return p.getFallbackData(key)
+    case <-ctx.Done():
+        return nil, ctx.Err()
+    }
+
+    // 第三层保护:分布式锁,防止同一Key重复查DB
+    lockKey := "lock:" + key
+    locked := p.rdb.SetNX(ctx, lockKey, "1", 10*time.Second).Val()
+    if !locked {
+        // 其他goroutine正在重建该Key的缓存,等待
+        time.Sleep(100 * time.Millisecond)
+        // 重试从缓存获取
+        return p.rdb.Get(ctx, key).Result()
+    }
+    defer p.rdb.Del(ctx, lockKey)
+
+    // 查询数据库
+    data, err := loadFn(ctx)
+    if err != nil {
+        return p.getFallbackData(key) // 查询失败,返回降级数据
+    }
+
+    // 写入缓存(随机TTL)
+    actualTTL := p.baseTTL + time.Duration(rand.Int63n(int64(p.jitterMax)))
+    p.rdb.Set(ctx, key, data, actualTTL)
+
+    return data, nil
+}
+
+// getFallbackData 获取降级数据
+func (p *AvalancheProtection) getFallbackData(key string) (interface{}, error) {
+    // 降级策略:返回默认值/上次缓存的快照/空结果
+    return map[string]interface{}{
+        "status":  "degraded",
+        "message": "service temporarily unavailable",
+    }, nil
+}
+
+
+ + +
+

🏗️ 解决方案三:Redis 高可用架构

+ +
+ 💡 +
+ 核心思路:通过 Redis 的高可用架构(主从复制、哨兵模式、Cluster 集群),确保缓存服务本身不会成为单点故障。 +
+
+ +

📐 三种高可用方案对比

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
方案架构自动故障转移数据分片适用规模
主从复制1主N从❌ 需手动切换❌ 全量复制小型项目
哨兵模式1主N从+哨兵✅ 自动切换❌ 全量复制中型项目
Cluster 集群多主多从✅ 自动切换✅ 16384 哈希槽大型项目
+
+ +

🏗️ Redis Cluster 架构图

+
+
+
客户端 (Redis Cluster Client)
+
+
↓ 根据 CRC16(key) % 16384 定位槽
+
+
+ Master 1
Slots 0-5460 +
+
+ Master 2
Slots 5461-10922 +
+
+ Master 3
Slots 10923-16383 +
+
+
↕ 主从复制
+
+
+ Slave 1
热备 +
+
+ Slave 2
热备 +
+
+ Slave 3
热备 +
+
+

+ 任意 Master 宕机,对应 Slave 自动提升为 Master,保证缓存服务持续可用 +

+
+ +

💻 Go 代码 — Redis Cluster 客户端配置

+
+
+
+ redis_cluster.go — Cluster 高可用配置 +
+
package cache
+
+import (
+    "context"
+    "fmt"
+    "time"
+
+    "github.com/redis/go-redis/v9"
+)
+
+// NewRedisCluster 创建 Redis Cluster 客户端
+func NewRedisCluster() *redis.ClusterClient {
+    return redis.NewClusterClient(&redis.ClusterOptions{
+        // 集群节点地址(至少配置一个,客户端会自动发现其他节点)
+        Addrs: []string{
+            "192.168.1.10:7000",
+            "192.168.1.10:7001",
+            "192.168.1.11:7000",
+            "192.168.1.11:7001",
+            "192.168.1.12:7000",
+            "192.168.1.12:7001",
+        },
+      
+        // 密码认证
+        Password: "your_password",
+      
+        // 最大重试次数(故障转移期间自动重试)
+        MaxRedirects: 3,
+      
+        // 读取超时
+        ReadTimeout: 3 * time.Second,
+      
+        // 写入超时
+        WriteTimeout: 3 * time.Second,
+      
+        // 连接池配置
+        PoolSize:     100,
+        MinIdleConns: 20,
+      
+        // 路由函数:读请求可以路由到从节点(读写分离)
+        RouteByLatency: true,
+      
+        // 集群拓扑刷新间隔
+        ClusterSlotsRefreshInterval: 10 * time.Second,
+    })
+}
+
+// SentinelClient 哨兵模式客户端
+func NewSentinelClient() *redis.Client {
+    return redis.NewFailoverClient(&redis.FailoverOptions{
+        // 哨兵节点地址
+        SentinelAddrs: []string{
+            "192.168.1.10:26379",
+            "192.168.1.11:26379",
+            "192.168.1.12:26379",
+        },
+      
+        // 主节点名称(在哨兵配置中定义的)
+        MasterName: "mymaster",
+      
+        // 数据库密码
+        Password: "your_password",
+      
+        // 数据库编号
+        DB: 0,
+      
+        // 只读节点(读写分离)
+        ReplicaOnly: false,
+    })
+}
+
+// HealthCheck 缓存健康检查
+func HealthCheck(ctx context.Context, client *redis.ClusterClient) error {
+    if err := client.Ping(ctx).Err(); err != nil {
+        return fmt.Errorf("redis cluster health check failed: %w", err)
+    }
+  
+    // 检查集群状态
+    clusterInfo, err := client.ClusterInfo(ctx).Result()
+    if err != nil {
+        return fmt.Errorf("failed to get cluster info: %w", err)
+    }
+  
+    // 验证集群状态是否为ok
+    if !contains(clusterInfo, "cluster_state:ok") {
+        return fmt.Errorf("cluster state is not ok")
+    }
+  
+    return nil
+}
+
+ +
+ ✅ +
+ 高可用最佳实践总结: +
    +
  • 生产环境至少使用哨兵模式,推荐 Redis Cluster
  • +
  • 配置合理的连接池参数,避免连接耗尽
  • +
  • 实现健康检查和自动熔断机制
  • +
  • 开启读写分离,将读请求分散到从节点
  • +
  • 配置持久化策略(RDB + AOF),防止数据丢失
  • +
+
+
+
+
+ + +
+ + +
+

🕳️ 缓存穿透 — 问题描述

+ +
+ ⚠️ +
+ 核心问题:请求查询的数据在缓存和数据库中都不存在,每次请求都会穿透缓存直接打到数据库。如果这类请求量很大(如恶意攻击),数据库将承受巨大压力。 +
+
+ +
+

🎯 问题本质

+

缓存穿透的关键词是"数据不存在"。攻击者利用不存在的数据 ID 发起大量请求,这些请求在缓存中永远 miss,每次都穿透到数据库。

+
    +
  • 前提条件:请求的数据在缓存和数据库中都不存在
  • +
  • 触发条件:大量此类请求持续到达
  • +
  • 直接后果:所有请求全部打到数据库,缓存形同虚设
  • +
  • 典型攻击:使用不存在的用户 ID、订单号等频繁查询
  • +
+
+ +

📊 穿透攻击示意

+
+
+
恶意请求: user_id=-1, user_id=999999...
+
+
↓ 每次请求
+
+
Redis 缓存
❌ 不存在 → miss
+
+
↓ 穿透
+
+
MySQL 数据库
❌ 也不存在 → 返回空
+
+
↓ 循环往复
+
+
+ 10000 QPS 全部打到数据库 💀
+ 缓存完全没有起到保护作用 +
+
+
+
+ + +
+

🛡️ 解决方案总览

+ +
+

针对缓存穿透问题,核心思路是拦截无效请求,防止其穿透到数据库。推荐三层防护组合使用:

+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
方案核心思路防护能力内存开销适用场景
✅ 参数校验在请求到达缓存前,拦截明显不合法的参数(如负数 ID)低(仅拦截格式错误)无所有场景(必做)
📦 缓存空对象数据库查询为空时,缓存空值标记(短 TTL),防止重复穿透中(防止重复查询)中(空值占内存)数据量不大,攻击 ID 有限
🌸 布隆过滤器用位数组存储合法 ID,请求到达前先检查,不存在则直接拒绝高(从根源拦截)低(位数组省内存)数据量大,防恶意攻击
+
+ +
+ ✅ +
+ 推荐防护顺序: +
    +
  1. 第一层 — 参数校验:拦截明显不合法请求(零成本,必做)
  2. +
  3. 第二层 — 布隆过滤器:拦截数据一定不存在的请求(低成本,高效率)
  4. +
  5. 第三层 — 缓存空对象:兜底保护,处理布隆过滤器误判的情况
  6. +
+
+
+
+ + +
+

✅ 解决方案一:参数校验

+ +
+ 💡 +
+ 核心思路:在请求到达缓存之前,先对参数进行合法性校验,直接拦截明显不合法的请求(如 ID ≤ 0、格式错误等)。 +
+
+ +

💻 Go 代码实现

+
+
+
+ param_validation.go — 参数校验 +
+
package handler
+
+import (
+    "errors"
+    "fmt"
+    "regexp"
+    "strconv"
+)
+
+var (
+    ErrInvalidID     = errors.New("invalid parameter: id must be positive")
+    ErrInvalidFormat = errors.New("invalid parameter format")
+    ErrIDOutOfRange  = errors.New("id out of valid range")
+)
+
+// ParamValidator 参数校验器
+type ParamValidator struct {
+    maxUserID int64 // 最大合法用户ID(可根据业务设定)
+}
+
+func NewParamValidator(maxUserID int64) *ParamValidator {
+    return &ParamValidator{maxUserID: maxUserID}
+}
+
+// ValidateUserID 校验用户ID
+func (v *ParamValidator) ValidateUserID(idStr string) (int64, error) {
+    // 1. 非空校验
+    if idStr == "" {
+        return 0, fmt.Errorf("%w: id is empty", ErrInvalidFormat)
+    }
+
+    // 2. 格式校验(纯数字)
+    matched, _ := regexp.MatchString(`^\d+$`, idStr)
+    if !matched {
+        return 0, fmt.Errorf("%w: id must be numeric", ErrInvalidFormat)
+    }
+
+    // 3. 数值转换与范围校验
+    id, err := strconv.ParseInt(idStr, 10, 64)
+    if err != nil {
+        return 0, fmt.Errorf("%w: %v", ErrInvalidFormat, err)
+    }
+
+    // 4. 正数校验
+    if id <= 0 {
+        return 0, ErrInvalidID
+    }
+
+    // 5. 范围校验(防止超大ID)
+    if id > v.maxUserID {
+        return 0, fmt.Errorf("%w: max is %d", ErrIDOutOfRange, v.maxUserID)
+    }
+
+    return id, nil
+}
+
+// GetUserHandler 用户查询接口
+func (h *Handler) GetUserHandler(idStr string) (interface{}, error) {
+    // 第一道防线:参数校验
+    userID, err := h.validator.ValidateUserID(idStr)
+    if err != nil {
+        // 参数不合法,直接拒绝,不查缓存也不查数据库
+        return nil, fmt.Errorf("bad request: %w", err)
+    }
+
+    // 参数合法,继续走缓存查询逻辑...
+    return h.cache.Get(ctx, fmt.Sprintf("user:%d", userID), ...)
+}
+
+ +
+ ⚡ +
+ 局限性:参数校验只能拦截明显不合法的参数(如负数、格式错误),但无法拦截格式合法但数据不存在的请求(如 user_id=999999 格式正确但用户不存在)。因此需要配合其他方案。 +
+
+
+ + +
+

📦 解决方案二:缓存空对象

+ +
+ 💡 +
+ 核心思路:当数据库查询结果为空时,仍然在缓存中写入一个空值标记,并设置较短的 TTL。后续相同请求命中缓存中的空值后直接返回,不再穿透到数据库。 +
+
+ +

📐 流程说明

+
+
+
请求: user_id=999999
+ → +
查缓存
+
+
+ 第一次请求:缓存 miss → 查 DB → DB 也空 → 写入空值到缓存(TTL=60s) → 返回空 +
+
+
请求: user_id=999999
+ → +
查缓存 → 命中空值
+ → +
直接返回空 ✅
+
+
+ 后续请求:在 TTL 内全部命中缓存空值,不再穿透到数据库 +
+
+ +

💻 Go 代码实现

+
+
+
+ cache_null.go — 缓存空对象方案 +
+
package cache
+
+import (
+    "context"
+    "encoding/json"
+    "errors"
+    "time"
+
+    "github.com/redis/go-redis/v9"
+)
+
+var (
+    ErrDataNotFound = errors.New("data not found")
+)
+
+// 空值标记常量
+const (
+    NullValue     = "@@NULL@@"          // 空值标记字符串
+    NullTTL       = 60 * time.Second    // 空值的TTL(较短,防止长期占用内存)
+    NormalTTL     = 30 * time.Minute    // 正常数据的TTL
+)
+
+// NullCache 支持缓存空对象的缓存实现
+type NullCache struct {
+    rdb       *redis.Client
+    nullTTL   time.Duration
+    normalTTL time.Duration
+}
+
+func NewNullCache(rdb *redis.Client) *NullCache {
+    return &NullCache{
+        rdb:       rdb,
+        nullTTL:   NullTTL,
+        normalTTL: NormalTTL,
+    }
+}
+
+// Get 获取数据(自动处理空值缓存)
+func (c *NullCache) Get(
+    ctx context.Context,
+    key string,
+    loadFn func(ctx context.Context) (interface{}, error),
+) (interface{}, error) {
+    // Step 1: 从缓存获取
+    val, err := c.rdb.Get(ctx, key).Result()
+  
+    if err == nil {
+        // 缓存命中
+        // 检查是否是空值标记
+        if val == NullValue {
+            // 命中空值,直接返回"数据不存在",不查数据库
+            return nil, ErrDataNotFound
+        }
+        // 正常数据,反序列化返回
+        var result interface{}
+        json.Unmarshal([]byte(val), &result)
+        return result, nil
+    }
+
+    if err != redis.Nil {
+        return nil, err
+    }
+
+    // Step 2: 缓存未命中,查询数据库
+    data, err := loadFn(ctx)
+    if err != nil {
+        return nil, err
+    }
+
+    // Step 3: 判断数据库查询结果
+    if data == nil {
+        // 数据库也不存在 → 缓存空值
+        c.rdb.Set(ctx, key, NullValue, c.nullTTL)
+        return nil, ErrDataNotFound
+    }
+
+    // 数据库存在 → 缓存正常数据
+    dataBytes, _ := json.Marshal(data)
+    c.rdb.Set(ctx, key, string(dataBytes), c.normalTTL)
+    return data, nil
+}
+
+// 使用示例
+func Example() {
+    cache := NewNullCache(rdb)
+  
+    // 查询用户(用户不存在的情况)
+    data, err := cache.Get(ctx, "user:999999", func(ctx context.Context) (interface{}, error) {
+        return db.QueryUser(ctx, 999999) // 返回 nil, nil
+    })
+  
+    if errors.Is(err, ErrDataNotFound) {
+        // 数据不存在(已被缓存为空值,后续请求不再查DB)
+        return "user not found"
+    }
+}
+
+ +
+
+

✅ 优点

+
    +
  • 实现简单,改动小
  • +
  • 有效防止对不存在数据的重复查询
  • +
  • 空值 TTL 较短,不会长期占用内存
  • +
+
+
+

❌ 缺点

+
    +
  • 如果攻击者使用大量不同的随机 ID,仍会写入大量空值缓存
  • +
  • 内存可能被空值占满(内存浪费)
  • +
  • 存在短暂的数据不一致窗口(数据新建后,空值缓存未过期)
  • +
+
+
+
+ + +
+

🌸 解决方案三:布隆过滤器

+ +
+ 💡 +
+ 核心思路:使用布隆过滤器(Bloom Filter)存储所有合法的数据 ID。请求到达时先查询布隆过滤器,如果判定"不存在"则直接拒绝,从根源上杜绝无效请求穿透到缓存和数据库。 +
+
+ +

📐 布隆过滤器原理

+
+

数据结构

+

布隆过滤器由一个很长的二进制位数组和多个哈希函数组成。

+
    +
  • 插入元素:用 k 个哈希函数计算 k 个位置,将对应位全部置为 1
  • +
  • 查询元素:用 k 个哈希函数计算 k 个位置,检查对应位是否全为 1
  • +
+ +

核心特性

+
+
+
✅ 判定"不存在" — 一定准确
+

如果任一位为 0,则该元素一定不存在。这个判断 100% 准确。

+
+
+
⚠️ 判定"存在" — 可能误判
+

如果所有位都为 1,该元素可能存在(有一定误判率/假阳性率)。

+
+
+
+ +

🔬 布隆过滤器可视化

+
+

位数组状态演示

+

+ 假设位数组长度 16,使用 3 个哈希函数 H1, H2, H3 +

+ +

初始状态(全 0):

+
+
0
+
0
+
0
+
0
+
0
+
0
+
0
+
0
+
0
+
0
+
0
+
0
+
0
+
0
+
0
+
0
+
+ +

插入 "user:1001" → H1=1, H2=5, H3=12:

+
+
0
+
1
+
0
+
0
+
0
+
1
+
0
+
0
+
0
+
0
+
0
+
0
+
1
+
0
+
0
+
0
+
+ +

插入 "user:1002" → H1=3, H2=5, H3=9:

+
+
0
+
1
+
0
+
1
+
0
+
1
+
0
+
0
+
0
+
1
+
0
+
0
+
1
+
0
+
0
+
0
+
+ +

查询 "user:9999" → H1=2, H2=7, H3=14:

+
+
0
+
1
+
0
+
1
+
0
+
1
+
0
+
0
+
0
+
1
+
0
+
0
+
1
+
0
+
0
+
0
+
+

→ 位2、位7、位14 有0 → 一定不存在!直接拒绝 ✅

+
+ +

💻 Go 代码实现

+
+
+
+ bloom_filter.go — 布隆过滤器实现 +
+
package cache
+
+import (
+    "context"
+    "hash/fnv"
+    "math"
+
+    "github.com/redis/go-redis/v9"
+)
+
+// BloomFilter 基于 Redis 的布隆过滤器实现
+type BloomFilter struct {
+    rdb     *redis.Client
+    key     string  // Redis 中存储位数组的 key
+    bitSize uint    // 位数组大小
+    hashNum uint    // 哈希函数个数
+}
+
+// NewBloomFilter 创建布隆过滤器
+// expectedInsertions: 预计插入的元素数量
+// falsePositiveRate: 期望的误判率(如 0.01 表示 1%)
+func NewBloomFilter(
+    rdb *redis.Client,
+    key string,
+    expectedInsertions uint64,
+    falsePositiveRate float64,
+) *BloomFilter {
+    // 计算最优位数组大小: m = -(n * ln(p)) / (ln(2))^2
+    bitSize := uint(math.Ceil(
+        -float64(expectedInsertions) * math.Log(falsePositiveRate) /
+            (math.Log(2) * math.Log(2)),
+    ))
+
+    // 计算最优哈希函数数量: k = (m/n) * ln(2)
+    hashNum := uint(math.Ceil(
+        (float64(bitSize) / float64(expectedInsertions)) * math.Log(2),
+    ))
+
+    return &BloomFilter{
+        rdb:     rdb,
+        key:     key,
+        bitSize: bitSize,
+        hashNum: hashNum,
+    }
+}
+
+// Add 添加元素到布隆过滤器
+func (bf *BloomFilter) Add(ctx context.Context, value string) error {
+    pipe := bf.rdb.Pipeline()
+  
+    for i := uint(0); i < bf.hashNum; i++ {
+        index := bf.hash(value, i)
+        pipe.SetBit(ctx, bf.key, int64(index), 1)
+    }
+  
+    _, err := pipe.Exec(ctx)
+    return err
+}
+
+// AddBatch 批量添加元素
+func (bf *BloomFilter) AddBatch(ctx context.Context, values []string) error {
+    pipe := bf.rdb.Pipeline()
+  
+    for _, value := range values {
+        for i := uint(0); i < bf.hashNum; i++ {
+            index := bf.hash(value, i)
+            pipe.SetBit(ctx, bf.key, int64(index), 1)
+        }
+    }
+  
+    _, err := pipe.Exec(ctx)
+    return err
+}
+
+// Exists 检查元素是否可能存在
+// 返回 true: 元素可能存在(需要继续查缓存/DB)
+// 返回 false: 元素一定不存在(直接拒绝)
+func (bf *BloomFilter) Exists(ctx context.Context, value string) (bool, error) {
+    for i := uint(0); i < bf.hashNum; i++ {
+        index := bf.hash(value, i)
+        bit, err := bf.rdb.GetBit(ctx, bf.key, int64(index)).Result()
+        if err != nil {
+            return false, err
+        }
+        if bit == 0 {
+            // 任一位为0,元素一定不存在
+            return false, nil
+        }
+    }
+    // 所有位都为1,元素可能存在
+    return true, nil
+}
+
+// hash 使用 FNV 哈希 + 种子模拟多个哈希函数
+func (bf *BloomFilter) hash(value string, seed uint) uint {
+    h := fnv.New64a()
+    h.Write([]byte(value))
+    // 加入种子产生不同的哈希值
+    h.Write([]byte{byte(seed)})
+    return uint(h.Sum64()) % bf.bitSize
+}
+
+// InitFromDB 从数据库加载所有合法ID到布隆过滤器
+func (bf *BloomFilter) InitFromDB(ctx context.Context, loadAllIDs func() ([]string, error)) error {
+    ids, err := loadAllIDs()
+    if err != nil {
+        return err
+    }
+  
+    // 批量添加到布隆过滤器
+    batchSize := 1000
+    for i := 0; i < len(ids); i += batchSize {
+        end := i + batchSize
+        if end > len(ids) {
+            end = len(ids)
+        }
+        if err := bf.AddBatch(ctx, ids[i:end]); err != nil {
+            return err
+        }
+    }
+    return nil
+}
+
+ +

💻 完整集成方案 — 三层防护

+
+
+
+ penetration_full.go — 穿透三层防护完整方案 +
+
package cache
+
+import (
+    "context"
+    "errors"
+    "fmt"
+    "time"
+)
+
+var (
+    ErrInvalidParam   = errors.New("invalid parameter")
+    ErrDataNotExist   = errors.New("data does not exist")
+    ErrServiceDegraded = errors.New("service degraded")
+)
+
+// PenetrationGuard 缓存穿透防护器(三层防护)
+type PenetrationGuard struct {
+    bloomFilter *BloomFilter // 第三层:布隆过滤器
+    nullCache   *NullCache   // 第二层:空值缓存
+}
+
+// Get 带三层防护的缓存查询
+func (g *PenetrationGuard) Get(
+    ctx context.Context,
+    key string,
+    loadFn func(ctx context.Context) (interface{}, error),
+) (interface{}, error) {
+    // ========== 第一层:参数校验 ==========
+    // (在调用此方法之前已完成,此处省略)
+  
+    // ========== 第二层:布隆过滤器检查 ==========
+    exists, err := g.bloomFilter.Exists(ctx, key)
+    if err != nil {
+        // 布隆过滤器查询失败,降级处理(放行,让后续层处理)
+        // 这里选择放行而不是拒绝,保证可用性
+    }
+    if !exists {
+        // 布隆过滤器判定不存在 → 一定不存在 → 直接拒绝
+        return nil, ErrDataNotExist
+    }
+
+    // ========== 第三层:空值缓存 ==========
+    // 布隆过滤器说可能存在,再查缓存(含空值缓存)
+    data, err := g.nullCache.Get(ctx, key, loadFn)
+    if err != nil {
+        if errors.Is(err, ErrDataNotFound) {
+            // 数据库也不存在
+            return nil, ErrDataNotExist
+        }
+        return nil, err
+    }
+
+    return data, nil
+}
+
+// ============ 完整使用示例 ============
+func GetUserExample() {
+    // 初始化
+    guard := &PenetrationGuard{
+        bloomFilter: NewBloomFilter(rdb, "bloom:users", 1000000, 0.01),
+        nullCache:   NewNullCache(rdb),
+    }
+
+    // 启动时从数据库加载所有合法用户ID到布隆过滤器
+    guard.bloomFilter.InitFromDB(ctx, func() ([]string, error) {
+        return db.GetAllUserIDs()
+    })
+
+    // 处理请求
+    userID := "-1" // 恶意请求
+  
+    // 第一层:参数校验
+    if id, err := validateUserID(userID); err != nil {
+        return "参数不合法" // 直接拒绝,-1 不合法
+    }
+
+    // 第二层+第三层:布隆过滤器 + 空值缓存
+    data, err := guard.Get(ctx, "user:999999", func(ctx context.Context) (interface{}, error) {
+        return db.GetUser(ctx, 999999)
+    })
+  
+    if errors.Is(err, ErrDataNotExist) {
+        return "用户不存在" // 布隆过滤器或空值缓存拦截
+    }
+}
+
+ +

📊 三种方案综合对比

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
方案防护能力内存开销实现复杂度适用场景
参数校验低(只能拦截格式错误)无极低所有场景(必做)
缓存空对象中(防止重复查询)中(空值占内存)低数据量不大,攻击ID有限
布隆过滤器高(从根源拦截)低(位数组很省内存)中高数据量大,防恶意攻击
+
+ +
+ ✅ +
+ 最佳实践:三种方案组合使用! +
    +
  1. 第一层 — 参数校验:拦截明显不合法请求(零成本)
  2. +
  3. 第二层 — 布隆过滤器:拦截数据一定不存在的请求(低成本,高效率)
  4. +
  5. 第三层 — 缓存空对象:兜底保护,处理布隆过滤器误判的情况
  6. +
+
+
+
+ + +
+

🔧 布隆过滤器的维护与注意事项

+ +
+
+

⚠️ 不能删除元素

+

标准布隆过滤器不支持删除操作。如果需要删除,可使用计数布隆过滤器(Counting Bloom Filter),用计数器代替单个位。

+
+
+

⚠️ 误判率随元素增多而升高

+

当插入元素超过预期数量时,误判率会显著上升。需要定期重建或动态扩容。

+
+
+

✅ 定期重建策略

+

每隔一定时间(如每天凌晨),从数据库重新加载所有合法 ID 到新的布隆过滤器,然后切换。

+
+
+

✅ 增量更新

+

新增数据时同步添加到布隆过滤器。删除数据时,由于不能直接删除,可以标记为"逻辑删除",定期重建时过滤掉。

+
+
+ +
+
+
+ bloom_maintenance.go — 布隆过滤器维护 +
+
// RebuildBloomFilter 定期重建布隆过滤器
+func RebuildBloomFilter(ctx context.Context, bf *BloomFilter) error {
+    // 使用新key创建新的布隆过滤器
+    newKey := bf.key + ":new"
+    newBF := NewBloomFilter(bf.rdb, newKey, bf.estimatedCapacity, bf.falsePositiveRate)
+  
+    // 从数据库加载最新数据
+    err := newBF.InitFromDB(ctx, func() ([]string, error) {
+        return db.GetAllValidIDs(ctx) // 只加载有效数据
+    })
+    if err != nil {
+        return err
+    }
+  
+    // 原子切换:rename 操作在 Redis 中是原子的
+    // 将旧key备份,新key替换为正式key
+    pipe := bf.rdb.Pipeline()
+    pipe.Rename(ctx, bf.key, bf.key+":old")
+    pipe.Rename(ctx, newKey, bf.key)
+    _, err = pipe.Exec(ctx)
+    if err != nil {
+        return err
+    }
+  
+    // 延迟删除旧key
+    go func() {
+        time.Sleep(1 * time.Minute)
+        bf.rdb.Del(context.Background(), bf.key+":old")
+    }()
+  
+    return nil
+}
+
+// StartPeriodicRebuild 启动定时重建任务
+func StartPeriodicRebuild(ctx context.Context, bf *BloomFilter, interval time.Duration) {
+    ticker := time.NewTicker(interval)
+    defer ticker.Stop()
+  
+    for {
+        select {
+        case <-ticker.C:
+            if err := RebuildBloomFilter(ctx, bf); err != nil {
+                log.Printf("failed to rebuild bloom filter: %v", err)
+            } else {
+                log.Printf("bloom filter rebuilt successfully")
+            }
+        case <-ctx.Done():
+            return
+        }
+    }
+}
+
+
+
+
+ + + + + \ No newline at end of file