Skip to content

01 - 缓存架构

缓存在系统中的位置

请求链路中的多级缓存:

  客户端                                              数据库
    │                                                  │
    ▼                                                  │
  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌────────┐│
  │ 浏览器    │  │  CDN     │  │ 本地缓存  │  │ Redis  ││
  │ 缓存      │→│  缓存    │→│ (进程内)  │→│ 分布式  │→│
  │ (HTTP)   │  │ (静态)   │  │ Caffeine │  │ 缓存   ││
  └──────────┘  └──────────┘  └──────────┘  └────────┘│

  命中率:  很高      很高       ~90%        ~99%     最后兜底
  延迟:    0ms      ~10ms     ~0.1ms      ~1ms     ~10ms

  核心思想: 离用户越近,速度越快
  80/20 法则: 20% 的数据承载 80% 的请求

1. 缓存读写策略

Cache Aside(旁路缓存)— 最常用

读流程:
    ┌──────┐  1.查缓存  ┌──────┐
    │ App  │──────────▶│Cache │
    │      │◀──────────│      │
    │      │  命中返回   │      │
    └──┬───┘           └──────┘
       │ 未命中
       │ 2.查数据库

    ┌──────┐
    │  DB  │
    │      │
    └──┬───┘
       │ 3.写入缓存

    ┌──────┐
    │Cache │
    └──────┘

写流程:
    ┌──────┐  1.更新DB   ┌──────┐
    │ App  │───────────▶│  DB  │
    │      │            └──────┘
    │      │  2.删缓存   ┌──────┐
    │      │───────────▶│Cache │  ← 删除,不是更新!
    └──────┘            └──────┘

  ⚠️ 为什么是"删缓存"而不是"更新缓存"?
  ┌───────────────────────────────────────────────────┐
  │  假设更新缓存:                                     │
  │  线程A: 更新DB(x=1) → 更新缓存(x=1)               │
  │  线程B: 更新DB(x=2) → 更新缓存(x=2)               │
  │                                                   │
  │  如果执行顺序变成:                                  │
  │  线程A更新DB(x=1) → 线程B更新DB(x=2)              │
  │  → 线程B更新缓存(x=2) → 线程A更新缓存(x=1)        │
  │  结果: DB=2, 缓存=1  不一致!                       │
  │                                                   │
  │  删缓存就没问题: 下次读会从 DB 加载最新值            │
  └───────────────────────────────────────────────────┘

Write Through / Write Behind

┌────────────────────────────────────────────────────────────┐
│                                                            │
│  Write Through (同步写穿):                                  │
│  App ──写──▶ Cache ──同步写──▶ DB                           │
│  优点: 数据一致性强                                         │
│  缺点: 写延迟高(要等 DB 写完)                              │
│                                                            │
│  Write Behind (异步写回):                                   │
│  App ──写──▶ Cache ──异步批量──▶ DB                         │
│  优点: 写性能极高                                           │
│  缺点: Cache 宕机可能丢数据                                 │
│                                                            │
│  选择:                                                     │
│  ┌──────────────────────────────────────────────┐          │
│  │  大多数场景: Cache Aside(简单可靠)           │          │
│  │  强一致性:   Write Through                    │          │
│  │  高写吞吐:   Write Behind + 持久化保障        │          │
│  └──────────────────────────────────────────────┘          │
└────────────────────────────────────────────────────────────┘

2. 缓存三大问题

┌────────────────────────────────────────────────────────────────┐
│                                                                │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────────────┐ │
│  │  缓存穿透     │  │  缓存击穿     │  │  缓存雪崩            │ │
│  │              │  │              │  │                      │ │
│  │  查不存在的   │  │  热点key过期  │  │  大量key同时过期      │ │
│  │  数据,每次   │  │  大量请求直   │  │  或缓存服务宕机       │ │
│  │  都打到DB    │  │  接打到DB    │  │  请求全部涌向DB      │ │
│  └──────────────┘  └──────────────┘  └──────────────────────┘ │
│                                                                │
│  穿透:                   击穿:                   雪崩:          │
│  请求 → Cache(无)        请求×1万               请求×10万       │
│      → DB(无)              │                      │            │
│      → Cache(无)         ┌─▼──┐               ┌──▼───┐        │
│      → DB(无)           │Cache│ key过期        │Cache │ 全挂    │
│      → ...  死循环!      │ 无  │               │ 全无  │        │
│                         └─┬──┘               └──┬───┘        │
│                           ▼                      ▼            │
│                         ┌────┐               ┌─────┐         │
│                         │ DB │ 被打爆         │ DB  │ 被打爆   │
│                         └────┘               └─────┘         │
│                                                                │
└────────────────────────────────────────────────────────────────┘

解决方案:

  缓存穿透:
  ┌────────────────────────────────────────────────────┐
  │  方案1: 缓存空值 (null也缓存,设短过期时间)         │
  │  方案2: 布隆过滤器 (BloomFilter 预判数据是否存在)   │
  │                                                    │
  │  请求 → BloomFilter → 存在? → 查缓存 → 查DB       │
  │                    → 不存在? → 直接返回空           │
  └────────────────────────────────────────────────────┘

  缓存击穿:
  ┌────────────────────────────────────────────────────┐
  │  方案1: 互斥锁 (只让一个请求去查DB,其他等待)        │
  │  方案2: 永不过期 + 异步更新                         │
  │                                                    │
  │  if cache miss:                                    │
  │    if tryLock():                                   │
  │      data = queryDB()                              │
  │      setCache(data)                                │
  │      unlock()                                      │
  │    else:                                           │
  │      wait → read cache again                       │
  └────────────────────────────────────────────────────┘

  缓存雪崩:
  ┌────────────────────────────────────────────────────┐
  │  方案1: 过期时间加随机值 (TTL = base + random)      │
  │  方案2: 多级缓存 (本地缓存 + Redis)                 │
  │  方案3: 限流 + 熔断 (保护 DB)                       │
  │  方案4: Redis 集群高可用 (哨兵 / Cluster)           │
  └────────────────────────────────────────────────────┘

Go 实现缓存穿透保护(singleflight)

go
package main

import (
    "fmt"
    "sync"
    "time"

    "golang.org/x/sync/singleflight"
)

type Cache struct {
    data  map[string]string
    mu    sync.RWMutex
    group singleflight.Group  // 防缓存击穿的利器!
}

func NewCache() *Cache {
    return &Cache{data: make(map[string]string)}
}

func (c *Cache) Get(key string) (string, error) {
    // 先查缓存
    c.mu.RLock()
    if val, ok := c.data[key]; ok {
        c.mu.RUnlock()
        return val, nil
    }
    c.mu.RUnlock()

    // 缓存未命中 → singleflight 保证同一个 key 只查一次 DB
    // 100 个并发请求同一个 key,只有 1 个会真正查 DB
    val, err, _ := c.group.Do(key, func() (interface{}, error) {
        fmt.Printf("  [DB] 查询 key=%s (只执行一次!)\n", key)
        time.Sleep(100 * time.Millisecond) // 模拟 DB 查询
        result := "value-of-" + key

        // 写入缓存
        c.mu.Lock()
        c.data[key] = result
        c.mu.Unlock()

        return result, nil
    })

    if err != nil {
        return "", err
    }
    return val.(string), nil
}

func main() {
    cache := NewCache()
    var wg sync.WaitGroup

    // 模拟 50 个并发请求同一个 key
    for i := 0; i < 50; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            val, _ := cache.Get("hot-key")
            _ = val
        }(i)
    }

    wg.Wait()
    fmt.Println("完成! DB 只被查询了 1 次")
}
singleflight 原理:

  没有 singleflight:               有 singleflight:
  请求1 → 查DB                     请求1 → 查DB ─┐
  请求2 → 查DB                     请求2 → 等待   │
  请求3 → 查DB                     请求3 → 等待   ├→ 共享结果
  ...                              ...           │
  请求50 → 查DB (50次!)            请求50 → 等待  │
                                              ←──┘ 1次!

  Go 标准库 golang.org/x/sync/singleflight
  简单几行代码就解决了缓存击穿问题!

3. 小结

┌──────────────────────────────────────────────────────┐
│  缓存架构设计速查                                      │
├──────────────────────────────────────────────────────┤
│                                                      │
│  读写策略:  Cache Aside(最常用,先查缓存后查DB)      │
│  写策略:    先更新DB,再删缓存(不是更新缓存)          │
│                                                      │
│  三大问题:                                            │
│  ├── 穿透: 布隆过滤器 / 缓存空值                      │
│  ├── 击穿: singleflight / 互斥锁 / 永不过期           │
│  └── 雪崩: TTL加随机 / 多级缓存 / 限流                │
│                                                      │
│  Go 最佳实践: singleflight 是防击穿的利器             │
│                                                      │
└──────────────────────────────────────────────────────┘

下一节: 02 - 消息队列