引言:为什么需要缓存?

在一个典型的电商系统中,读操作占比往往超过 80%。如果每次请求都打到数据库,单表的 QPS 上限大约在 1000-5000(取决于硬件配置)。当并发量达到 10 万+ 时,数据库必然成为瓶颈。

缓存是解决读性能问题的银弹,但它也带来了一系列挑战:数据一致性、缓存雪崩、缓存击穿、缓存穿透。本文将深入探讨这些问题及其解决方案。

一、缓存架构的演进路径

阶段1:无缓存时代

┌─────────┐
│ 客户端   │
└────┬────┘
     │
     ▼
┌─────────┐
│Web服务  │
└────┬────┘
     │
     ▼
┌─────────┐
│ 数据库  │ ← 瓶颈
└─────────┘

问题:数据库成为唯一瓶颈,QPS 上限低。

阶段2:单一 Redis 缓存

┌─────────┐
│ 客户端   │
└────┬────┘
     │
     ▼
┌─────────┐
│Web服务  │
└────┬────┘
     │
     ├───────────┐
     ▼           ▼
┌─────────┐  ┌─────────┐
│ Redis   │  │ 数据库  │
└─────────┘  └─────────┘

实现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
type CacheService struct {
    redis *redis.Client
    db    *sql.DB
}

func (s *CacheService) GetUser(ctx context.Context, userID string) (*User, error) {
    // 1. 尝试从 Redis 获取
    cached, err := s.redis.Get(ctx, fmt.Sprintf("user:%s", userID)).Result()
    if err == nil {
        var user User
        json.Unmarshal([]byte(cached), &user)
        return &user, nil
    }

    // 2. Redis 未命中,查询数据库
    user := &User{}
    if err := s.db.QueryRow("SELECT * FROM users WHERE id = ?", userID).
        Scan(user); err != nil {
        return nil, err
    }

    // 3. 写入 Redis
    data, _ := json.Marshal(user)
    s.redis.Set(ctx, fmt.Sprintf("user:%s", userID), data, 30*time.Minute)

    return user, nil
}

问题:

  • Redis 单点故障
  • 高并发下 Redis 网络带宽成为瓶颈
  • 热点 Key 导致 Redis 负载不均

阶段3:Redis 高可用集群

        ┌─────────┐
        │ 客户端   │
        └────┬────┘
             │
        ┌────▼────┐
        │ 代理层   │
        │ (Twemproxy)
        └────┬────┘
             │
    ┌────────┴────────┐
    │                 │
┌───▼────┐      ┌────▼───┐
│Redis   │      │Redis   │
│Master  │◄────►│Slave   │
└────────┘      └────────┘

Redis Sentinel 高可用配置:

1
2
3
4
5
6
# sentinel.conf
port 26379
sentinel monitor mymaster 127.0.0.1 6379 2
sentinel down-after-milliseconds mymaster 5000
sentinel parallel-syncs mymaster 1
sentinel failover-timeout mymaster 60000

Go 代码集成:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import "github.com/go-redis/redis/v8"

func NewRedisClient() *redis.Client {
    return redis.NewFailoverClient(&redis.FailoverOptions{
        MasterName:    "mymaster",
        SentinelAddrs: []string{":26379", ":26380", ":26381"},
        Password:      "",
        PoolSize:      100,
        MinIdleConns:  10,
    })
}

阶段4:多级缓存架构

┌─────────┐
│ 客户端   │
└────┬────┘
     │
     ▼
┌─────────┐
│  CDN    │ ← 第一级:静态资源
└────┬────┘
     │
     ▼
┌─────────┐
│ Nginx   │ ← 第二级:反向代理缓存
└────┬────┘
     │
     ▼
┌─────────┐
│Web服务  │
└────┬────┘
     │
     ├───────────┬───────────┐
     ▼           ▼           ▼
┌─────────┐ ┌─────────┐ ┌─────────┐
│本地缓存  │ │Redis集群│ │ 数据库  │
│(Bigcache)│ │         │ │         │
└─────────┘ └─────────┘ └─────────┘

多级缓存实现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
type MultiLevelCache struct {
    l1 *bigcache.BigCacheServer  // 本地缓存
    l2 *redis.ClusterClient      // Redis 集群
    db *sql.DB                   // 数据库
}

func (c *MultiLevelCache) Get(ctx context.Context, key string) ([]byte, error) {
    // L1: 本地缓存
    if data, err := c.l1.Get(key); err == nil {
        metrics.CacheHit("l1")
        return data, nil
    }

    // L2: Redis 集群
    data, err := c.l2.Get(ctx, key).Bytes()
    if err == nil {
        metrics.CacheHit("l2")
        // 回写 L1
        c.l1.Set(key, data)
        return data, nil
    }

    // L3: 数据库
    result := c.queryFromDB(ctx, key)
    if result != nil {
        // 异步回写 L1 和 L2
        go func() {
            c.l1.Set(key, result)
            c.l2.Set(ctx, key, result, 1*time.Hour)
        }()
    }

    return result, nil
}

二、本地缓存选型与实现

2.1 BigCache vs sync.Map vs Ristretto

特性BigCachesync.MapRistretto
并发性能⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
GC 压力极低高低
淘汰策略LRU无TinyLFU
过期时间固定无支持
适用场景高并发读低并发通用

2.2 BigCache 实战

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import (
    "github.com/allegro/bigcache/v3"
)

func NewBigCache() (*bigcache.BigCache, error) {
    return bigcache.New(context.Background(), bigcache.DefaultConfig(10 * time.Minute).
        WithShards(1024).                     // 分片数,必须是 2 的幂
        WithLifeWindow(10 * time.Minute).     // 条目过期时间
        WithCleanWindow(5 * time.Minute).      // 清理窗口
        WithMaxEntrySize(500).                // 最大条目大小(KB)
        WithMaxEntriesInWindow(10000 * 10).   // 窗口内最大条目数
        WithHardMaxCacheSize(256).            // 最大缓存大小(MB)
        WithLogger(log.New(os.Stderr, "BigCache: ", log.LstdFlags)))
}

本地缓存的问题:

  1. 数据不一致:多个实例的本地缓存数据不一致
  2. 内存占用:每个实例都有完整副本,内存消耗大
  3. 缓存失效:需要主动通知其他实例

解决方案:Redis Pub/Sub 通知

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
type CacheInvalidationNotifier struct {
    redis *redis.Client
    localCache *bigcache.BigCache
}

func (n *CacheInvalidationNotifier) Subscribe() {
    pubsub := n.redis.Subscribe(ctx, "cache:invalidate")
    _, err := pubsub.Receive(ctx)
    if err != nil {
        return
    }

    ch := pubsub.Channel()
    for msg := range ch {
        // 收到失效通知,删除本地缓存
        n.localCache.Delete(msg.Payload)
    }
}

func (n *CacheInvalidationNotifier) Invalidate(key string) {
    // 发布失效通知
    n.redis.Publish(ctx, "cache:invalidate", key)
    // 删除本地缓存
    n.localCache.Delete(key)
    // 删除 Redis 缓存
    n.redis.Del(ctx, key)
}

三、缓存问题的解决方案

3.1 缓存雪崩

问题:大量缓存同时失效,请求全部打到数据库

正常情况:
请求 -> 缓存命中 (99%) -> 数据库 (1%)

雪崩情况:
请求 -> 缓存全部失效 -> 数据库 (100% 爆炸)

解决方案1:随机过期时间

1
2
3
4
5
6
7
func SetWithRandomExpire(client *redis.Client, key string, value interface{}, baseExpire time.Duration) {
    // 在基础过期时间上增加随机偏移(±20%)
    randomOffset := time.Duration(rand.Int63n(int64(baseExpire) / 5))
    actualExpire := baseExpire + randomOffset - (baseExpire / 10)

    client.Set(ctx, key, value, actualExpire)
}

解决方案2:缓存预热

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
type CacheWarmer struct {
    cache *MultiLevelCache
    db    *sql.DB
}

func (w *CacheWarmer) WarmUp(keys []string) error {
    // 1. 从数据库批量查询
    rows, err := w.db.Query(`
        SELECT id, name, email FROM users WHERE id IN (?)
    `, strings.Join(keys, ","))
    if err != nil {
        return err
    }
    defer rows.Close()

    users := []User{}
    for rows.Next() {
        var user User
        rows.Scan(&user.ID, &user.Name, &user.Email)
        users = append(users, user)
    }

    // 2. 异步写入缓存(不阻塞启动)
    go func() {
        for _, user := range users {
            data, _ := json.Marshal(user)
            w.cache.Set(ctx, fmt.Sprintf("user:%s", user.ID), data, 1*time.Hour)
        }
    }()

    return nil
}

解决方案3:互斥锁防止击穿

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
type Lock struct {
    redis *redis.Client
}

func (l *Lock) TryLock(ctx context.Context, key string, expire time.Duration) bool {
    return l.redis.SetNX(ctx, fmt.Sprintf("lock:%s", key), 1, expire).Val()
}

func (l *Lock) Unlock(ctx context.Context, key string) {
    l.redis.Del(ctx, fmt.Sprintf("lock:%s", key))
}

// 使用互斥锁防止缓存击穿
func (s *CacheService) GetUserWithLock(ctx context.Context, userID string) (*User, error) {
    // 1. 尝试获取缓存
    if user, err := s.getFromCache(ctx, userID); err == nil {
        return user, nil
    }

    // 2. 缓存未命中,尝试获取锁
    lockKey := fmt.Sprintf("query_user:%s", userID)
    if !s.lock.TryLock(ctx, lockKey, 10*time.Second) {
        // 获取锁失败,等待 100ms 后重试(避免并发查询数据库)
        time.Sleep(100 * time.Millisecond)
        return s.GetUserWithLock(ctx, userID)
    }
    defer s.lock.Unlock(ctx, lockKey)

    // 3. 双重检查:可能其他 goroutine 已经更新了缓存
    if user, err := s.getFromCache(ctx, userID); err == nil {
        return user, nil
    }

    // 4. 查询数据库
    user := s.queryFromDB(ctx, userID)

    // 5. 写入缓存
    s.setToCache(ctx, userID, user)

    return user, nil
}

3.2 缓存击穿

问题:热点 Key 过期瞬间,大量请求直接打到数据库

解决方案:布隆过滤器预判

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import "github.com/bits-and-blooms/bloom/v3"

type CacheWithBloomFilter struct {
    cache *MultiLevelCache
    bloom *bloom.BloomFilter
}

func NewCacheWithBloomFilter() *CacheWithBloomFilter {
    // 预估 1000 万个 Key,误判率 0.01%
    bf := bloom.NewWithEstimates(10_000_000, 0.0001)

    // 初始化布隆过滤器(从数据库加载所有 Key)
    // ...

    return &CacheWithBloomFilter{
        cache: NewMultiLevelCache(),
        bloom: bf,
    }
}

func (c *CacheWithBloomFilter) Get(ctx context.Context, key string) ([]byte, error) {
    // 1. 布隆过滤器预判
    if !c.bloom.Test([]byte(key)) {
        // Key 一定不存在,直接返回
        return nil, ErrKeyNotFound
    }

    // 2. 尝试从缓存获取
    return c.cache.Get(ctx, key)
}

3.3 缓存穿透

问题:查询不存在的数据,缓存和数据库都没有,每次都打到数据库

解决方案1:缓存空值

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
func (s *CacheService) GetUser(ctx context.Context, userID string) (*User, error) {
    // 1. 尝试从缓存获取
    cached, err := s.redis.Get(ctx, fmt.Sprintf("user:%s", userID)).Result()
    if err == nil {
        if cached == "NULL" {
            // 缓存了空值
            return nil, ErrUserNotFound
        }
        var user User
        json.Unmarshal([]byte(cached), &user)
        return &user, nil
    }

    // 2. 查询数据库
    user := &User{}
    err = s.db.QueryRow("SELECT * FROM users WHERE id = ?", userID).Scan(user)
    if errors.Is(err, sql.ErrNoRows) {
        // 用户不存在,缓存空值(较短的过期时间)
        s.redis.Set(ctx, fmt.Sprintf("user:%s", userID), "NULL", 5*time.Minute)
        return nil, ErrUserNotFound
    }

    // 3. 写入缓存
    data, _ := json.Marshal(user)
    s.redis.Set(ctx, fmt.Sprintf("user:%s", userID), data, 30*time.Minute)

    return user, nil
}

解决方案2:布隆过滤器

见上文 3.2 的实现。

四、缓存更新策略

4.1 Cache Aside(旁路缓存)

最常用的缓存策略:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 读:先读缓存,未命中则读数据库,然后写缓存
func (s *CacheService) Get(ctx context.Context, key string) ([]byte, error) {
    if data, err := s.cache.Get(ctx, key); err == nil {
        return data, nil
    }

    data := s.db.Query(ctx, key)
    s.cache.Set(ctx, key, data)
    return data, nil
}

// 写:先更新数据库,然后删除缓存
func (s *CacheService) Set(ctx context.Context, key string, value []byte) error {
    if err := s.db.Update(ctx, key, value); err != nil {
        return err
    }

    // 延迟双删:先删除,休眠,再删除
    s.cache.Delete(ctx, key)
    time.Sleep(500 * time.Millisecond)
    s.cache.Delete(ctx, key)

    return nil
}

4.2 Write Through(写穿透)

写操作同时更新缓存和数据库:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
func (s *CacheService) SetWithWriteThrough(ctx context.Context, key string, value []byte) error {
    // 同时更新缓存和数据库
    if err := s.cache.Set(ctx, key, value); err != nil {
        return err
    }

    if err := s.db.Update(ctx, key, value); err != nil {
        // 数据库更新失败,回滚缓存
        s.cache.Delete(ctx, key)
        return err
    }

    return nil
}

4.3 Write Behind(异步写回)

写操作只更新缓存,异步批量写入数据库:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
type WriteBehindCache struct {
    cache   *bigcache.BigCache
    db      *sql.DB
    buffer  chan WriteOperation
    ticker  *time.Ticker
}

type WriteOperation struct {
    Key   string
    Value []byte
}

func NewWriteBehindCache() *WriteBehindCache {
    c := &WriteBehindCache{
        cache:  NewBigCache(),
        db:     NewDB(),
        buffer: make(chan WriteOperation, 10000),
        ticker: time.NewTicker(1 * time.Second),
    }

    go c.flusher()

    return c
}

func (c *WriteBehindCache) Set(key string, value []byte) {
    // 1. 立即写入缓存
    c.cache.Set(key, value)

    // 2. 异步写入数据库
    c.buffer <- WriteOperation{Key: key, Value: value}
}

func (c *WriteBehindCache) flusher() {
    batch := []WriteOperation{}

    for {
        select {
        case op := <-c.buffer:
            batch = append(batch, op)
            // 批量大小达到阈值,执行批量写入
            if len(batch) >= 1000 {
                c.batchWrite(batch)
                batch = []WriteOperation{}
            }
        case <-c.ticker.C:
            // 定时刷新
            if len(batch) > 0 {
                c.batchWrite(batch)
                batch = []WriteOperation{}
            }
        }
    }
}

func (c *WriteBehindCache) batchWrite(ops []WriteOperation) {
    tx := c.db.Begin()
    defer tx.Rollback()

    stmt, _ := tx.Prepare("INSERT INTO cache (key, value) VALUES (?, ?) ON DUPLICATE KEY UPDATE value = ?")
    defer stmt.Close()

    for _, op := range ops {
        stmt.Exec(op.Key, op.Value, op.Value)
    }

    tx.Commit()
}

五、监控与告警

5.1 缓存命中率监控

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
type CacheMetrics struct {
    hits   atomic.Int64
    misses atomic.Int64
}

func (m *CacheMetrics) RecordHit() {
    m.hits.Add(1)
}

func (m *CacheMetrics) RecordMiss() {
    m.misses.Add(1)
}

func (m *CacheMetrics) HitRate() float64 {
    total := m.hits.Load() + m.misses.Load()
    if total == 0 {
        return 0
    }
    return float64(m.hits.Load()) / float64(total)
}

// 暴露 Prometheus 指标
func (m *CacheMetrics) Describe(ch chan<- *prometheus.Desc) {
    prometheus.NewDesc("cache_hits_total", "Total cache hits", nil, nil)
    prometheus.NewDesc("cache_misses_total", "Total cache misses", nil, nil)
}

5.2 Big Key 检测

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
func (s *CacheService) ScanBigKeys(ctx context.Context) error {
    var cursor uint64
    for {
        keys, nextCursor, err := s.redis.Scan(ctx, cursor, "*", 100).Result()
        if err != nil {
            return err
        }

        for _, key := range keys {
            // 使用 DEBUG OBJECT 命令获取 Key 信息
            info, err := s.redis.Do(ctx, "debug", "object", key).Result()
            if err != nil {
                continue
            }

            // 解析序列化长度
            if serializedLength := extractSerializedLength(info); serializedLength > 1024*1024 {  // > 1MB
                log.Printf("Big Key detected: %s, size: %d bytes", key, serializedLength)
                // 发送告警
            }
        }

        cursor = nextCursor
        if cursor == 0 {
            break
        }
    }

    return nil
}

六、最佳实践总结

  1. 缓存不是银弹:过度缓存会带来复杂度,要从业务需求出发
  2. 多级缓存:L1(本地)+ L2(Redis)+ L3(数据库)是成熟方案
  3. 防止雪崩:随机过期时间 + 缓存预热 + 互斥锁
  4. 防止击穿:热点 Key 永不过期 + 布隆过滤器
  5. 防止穿透:缓存空值 + 布隆过滤器
  6. 监控先行:缓存命中率、大 Key、慢查询都要监控
  7. 优雅降级:缓存故障时,要有降级方案

缓存是性能优化的利器,但也是复杂度的来源。理解业务,合理设计,才是王道。