Skip to content

缓存模式

一、缓存架构

缓存层次

┌─────────────────────────────────────────────────────────────────┐
│                      缓存层次架构                                │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│   ┌─────────────────────────────────────────────────────────┐  │
│   │                      客户端                              │  │
│   └─────────────────────────────────────────────────────────┘  │
│                              │                                  │
│                              ▼                                  │
│   ┌─────────────────────────────────────────────────────────┐  │
│   │ L1: 浏览器缓存 / CDN                                     │  │
│   │     静态资源、图片、JS/CSS                               │  │
│   │     命中率: 80%+                                         │  │
│   └─────────────────────────────────────────────────────────┘  │
│                              │                                  │
│                              ▼                                  │
│   ┌─────────────────────────────────────────────────────────┐  │
│   │ L2: 网关缓存 / Nginx 缓存                               │  │
│   │     API 响应缓存                                         │  │
│   │     命中率: 50%+                                         │  │
│   └─────────────────────────────────────────────────────────┘  │
│                              │                                  │
│                              ▼                                  │
│   ┌─────────────────────────────────────────────────────────┐  │
│   │ L3: 应用本地缓存                                         │  │
│   │     热点数据、配置信息                                   │  │
│   │     (进程内存,速度最快)                                  │  │
│   └─────────────────────────────────────────────────────────┘  │
│                              │                                  │
│                              ▼                                  │
│   ┌─────────────────────────────────────────────────────────┐  │
│   │ L4: 分布式缓存 (Redis/Memcached)                        │  │
│   │     业务数据、Session                                    │  │
│   │     (网络开销,但容量大)                                  │  │
│   └─────────────────────────────────────────────────────────┘  │
│                              │                                  │
│                              ▼                                  │
│   ┌─────────────────────────────────────────────────────────┐  │
│   │ L5: 数据库                                               │  │
│   └─────────────────────────────────────────────────────────┘  │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

二、缓存读写模式

1. Cache Aside (旁路缓存)

┌─────────────────────────────────────────────────────────────────┐
│                   Cache Aside 模式                               │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│   读流程:                                                        │
│   ┌───────────────────────────────────────────────────────┐    │
│   │                                                        │    │
│   │   App ─────1.读缓存────▶ Cache                        │    │
│   │    │                        │                          │    │
│   │    │                    命中? │                         │    │
│   │    │                   ┌──────┴──────┐                 │    │
│   │    │                   │             │                 │    │
│   │    │                  Yes           No                 │    │
│   │    │                   │             │                 │    │
│   │    │               返回数据    2.读数据库               │    │
│   │    │                         ▼                         │    │
│   │    │◀────────────────────── DB                        │    │
│   │    │                         │                         │    │
│   │    └────3.写入缓存──────────▶│                         │    │
│   │                                                        │    │
│   └───────────────────────────────────────────────────────┘    │
│                                                                 │
│   写流程:                                                        │
│   ┌───────────────────────────────────────────────────────┐    │
│   │                                                        │    │
│   │   App ─────1.更新数据库────▶ DB                       │    │
│   │    │                                                   │    │
│   │    └─────2.删除缓存────────▶ Cache                    │    │
│   │                                                        │    │
│   └───────────────────────────────────────────────────────┘    │
│                                                                 │
│   特点:                                                         │
│   • 最常用的模式                                                │
│   • 应用控制缓存逻辑                                            │
│   • 可能存在短暂不一致                                          │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘
go
// Cache Aside 实现
type CacheAside struct {
    cache  *redis.Client
    db     *sql.DB
    ttl    time.Duration
}

func (c *CacheAside) Get(ctx context.Context, key string) (*User, error) {
    // 1. 先查缓存
    val, err := c.cache.Get(ctx, key).Result()
    if err == nil {
        var user User
        json.Unmarshal([]byte(val), &user)
        return &user, nil
    }

    // 2. 缓存未命中,查数据库
    user, err := c.getUserFromDB(key)
    if err != nil {
        return nil, err
    }

    // 3. 写入缓存
    data, _ := json.Marshal(user)
    c.cache.Set(ctx, key, data, c.ttl)

    return user, nil
}

func (c *CacheAside) Update(ctx context.Context, user *User) error {
    // 1. 更新数据库
    err := c.updateUserInDB(user)
    if err != nil {
        return err
    }

    // 2. 删除缓存
    c.cache.Del(ctx, fmt.Sprintf("user:%s", user.ID))

    return nil
}

2. Read/Write Through

┌─────────────────────────────────────────────────────────────────┐
│                Read/Write Through 模式                          │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│   读流程:                                                        │
│   ┌───────────────────────────────────────────────────────┐    │
│   │                                                        │    │
│   │   App ─────读────▶ Cache Provider ─────▶ DB           │    │
│   │    │◀──────────────────────────────────────            │    │
│   │                                                        │    │
│   │   缓存层封装了所有读写逻辑                              │    │
│   │                                                        │    │
│   └───────────────────────────────────────────────────────┘    │
│                                                                 │
│   写流程:                                                        │
│   ┌───────────────────────────────────────────────────────┐    │
│   │                                                        │    │
│   │   App ─────写────▶ Cache Provider ─────▶ DB           │    │
│   │                          │                             │    │
│   │                     更新缓存                            │    │
│   │                                                        │    │
│   └───────────────────────────────────────────────────────┘    │
│                                                                 │
│   特点:                                                         │
│   • 缓存层负责数据库交互                                        │
│   • 对应用透明                                                  │
│   • 实现复杂                                                    │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

3. Write Behind (Write Back)

┌─────────────────────────────────────────────────────────────────┐
│                   Write Behind 模式                             │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│   写流程:                                                        │
│   ┌───────────────────────────────────────────────────────┐    │
│   │                                                        │    │
│   │   App ─────写────▶ Cache ──────异步────▶ DB           │    │
│   │    │◀─立即返回───────│                                  │    │
│   │                                                        │    │
│   │   写入缓存后立即返回,异步刷新到数据库                   │    │
│   │                                                        │    │
│   └───────────────────────────────────────────────────────┘    │
│                                                                 │
│   特点:                                                         │
│   • 写性能最高                                                  │
│   • 可能丢数据                                                  │
│   • 适合写多读少场景                                            │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

三、缓存一致性问题

1. 双写不一致

┌─────────────────────────────────────────────────────────────────┐
│                   双写不一致问题                                 │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│   场景: 先更新数据库,再删除缓存                                 │
│                                                                 │
│   请求A (更新)          请求B (读取)         时间线              │
│   ────────────────────────────────────────────────────────    │
│   1. 更新DB (v2)                              T1                │
│                         2. 读缓存 (v1)        T2                │
│   3. 删除缓存                                 T3                │
│                                                                 │
│   问题: 请求B在T2读到了旧数据 (短暂不一致)                       │
│   影响: 通常可接受 (毫秒级)                                      │
│                                                                 │
│   ────────────────────────────────────────────────────────    │
│                                                                 │
│   更严重的场景: 缓存删除失败                                     │
│                                                                 │
│   请求A (更新)          缓存状态               时间线            │
│   ────────────────────────────────────────────────────────    │
│   1. 更新DB (v2)                              T1                │
│   2. 删除缓存失败        v1 (旧数据)          T2                │
│                                                                 │
│   问题: 缓存一直是旧数据                                        │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

2. 解决方案

go
// 方案 1: 延迟双删
func UpdateWithDelayDelete(ctx context.Context, user *User) error {
    key := fmt.Sprintf("user:%s", user.ID)

    // 1. 删除缓存
    cache.Del(ctx, key)

    // 2. 更新数据库
    err := db.Update(user)
    if err != nil {
        return err
    }

    // 3. 延迟再删一次 (防止并发读写导致的不一致)
    go func() {
        time.Sleep(500 * time.Millisecond)  // 等待读请求完成
        cache.Del(context.Background(), key)
    }()

    return nil
}

// 方案 2: 消息队列保证删除
func UpdateWithMQ(ctx context.Context, user *User) error {
    // 1. 更新数据库
    err := db.Update(user)
    if err != nil {
        return err
    }

    // 2. 发送删除缓存消息 (可靠投递)
    msg := CacheInvalidateMessage{
        Key: fmt.Sprintf("user:%s", user.ID),
    }
    return mq.Send(ctx, "cache-invalidate", msg)
}

// 消费者
func ConsumeCacheInvalidate(msg CacheInvalidateMessage) error {
    err := cache.Del(context.Background(), msg.Key)
    if err != nil {
        // 重试或记录失败
        return err
    }
    return nil
}

// 方案 3: 订阅 Binlog
// 使用 Canal 监听 MySQL Binlog
// 数据变更时自动删除/更新缓存
type BinlogHandler struct {
    cache *redis.Client
}

func (h *BinlogHandler) OnUpdate(table string, before, after map[string]interface{}) {
    if table == "users" {
        key := fmt.Sprintf("user:%v", after["id"])
        h.cache.Del(context.Background(), key)
    }
}

四、缓存问题与解决

1. 缓存穿透

┌─────────────────────────────────────────────────────────────────┐
│                      缓存穿透                                    │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│   问题: 查询不存在的数据,每次都穿透到数据库                     │
│                                                                 │
│   App ───查询 id=999999───▶ Cache (Miss) ───▶ DB (不存在)      │
│                                                                 │
│   恶意攻击可能导致数据库压力过大                                 │
│                                                                 │
│   解决方案:                                                      │
│                                                                 │
│   1. 缓存空值                                                   │
│   ┌─────────────────────────────────────────────────────────┐  │
│   │ if data == nil {                                         │  │
│   │     cache.Set(key, "NULL", 5*time.Minute)               │  │
│   │ }                                                        │  │
│   └─────────────────────────────────────────────────────────┘  │
│                                                                 │
│   2. 布隆过滤器                                                 │
│   ┌─────────────────────────────────────────────────────────┐  │
│   │ if !bloomFilter.MightContain(id) {                      │  │
│   │     return nil  // 一定不存在                            │  │
│   │ }                                                        │  │
│   │ // 可能存在,继续查询                                    │  │
│   └─────────────────────────────────────────────────────────┘  │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

2. 缓存击穿

┌─────────────────────────────────────────────────────────────────┐
│                      缓存击穿                                    │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│   问题: 热点 key 过期,大量请求同时查询数据库                    │
│                                                                 │
│   时刻 T: 热点 key 过期                                         │
│   时刻 T+1: 1000 个请求同时到达                                 │
│   结果: 1000 个请求都查数据库                                   │
│                                                                 │
│   解决方案:                                                      │
│                                                                 │
│   1. 互斥锁 (推荐)                                              │
│   ┌─────────────────────────────────────────────────────────┐  │
│   │ lock := cache.SetNX("lock:"+key, 1, 10*time.Second)     │  │
│   │ if lock {                                                │  │
│   │     data := db.Query(key)                               │  │
│   │     cache.Set(key, data, ttl)                           │  │
│   │     cache.Del("lock:"+key)                              │  │
│   │ } else {                                                 │  │
│   │     time.Sleep(100*time.Millisecond)                    │  │
│   │     return cache.Get(key)  // 重试                       │  │
│   │ }                                                        │  │
│   └─────────────────────────────────────────────────────────┘  │
│                                                                 │
│   2. 逻辑过期                                                   │
│   ┌─────────────────────────────────────────────────────────┐  │
│   │ type CacheData struct {                                  │  │
│   │     Data      interface{}                               │  │
│   │     ExpireAt  int64  // 逻辑过期时间                     │  │
│   │ }                                                        │  │
│   │ // 物理上不过期,逻辑上判断                               │  │
│   │ // 过期则异步刷新,返回旧数据                             │  │
│   └─────────────────────────────────────────────────────────┘  │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

3. 缓存雪崩

┌─────────────────────────────────────────────────────────────────┐
│                      缓存雪崩                                    │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│   问题: 大量缓存同时过期,或缓存服务宕机                         │
│                                                                 │
│   解决方案:                                                      │
│                                                                 │
│   1. 过期时间加随机值                                           │
│   ┌─────────────────────────────────────────────────────────┐  │
│   │ ttl := baseTTL + rand.Intn(300)  // 随机 0-300 秒        │  │
│   │ cache.Set(key, data, time.Duration(ttl)*time.Second)    │  │
│   └─────────────────────────────────────────────────────────┘  │
│                                                                 │
│   2. 缓存高可用 (集群)                                          │
│   Redis Cluster / Redis Sentinel                               │
│                                                                 │
│   3. 本地缓存兜底                                               │
│   ┌─────────────────────────────────────────────────────────┐  │
│   │ data := localCache.Get(key)                             │  │
│   │ if data == nil {                                         │  │
│   │     data = redis.Get(key)  // 可能失败                   │  │
│   │     if data != nil {                                     │  │
│   │         localCache.Set(key, data, 1*time.Minute)        │  │
│   │     }                                                    │  │
│   │ }                                                        │  │
│   └─────────────────────────────────────────────────────────┘  │
│                                                                 │
│   4. 限流降级                                                   │
│   数据库前加限流,超过阈值返回默认值                            │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

五、本地缓存

Go 本地缓存实现

go
import (
    "sync"
    "time"
)

type LocalCache struct {
    mu    sync.RWMutex
    items map[string]*cacheItem
}

type cacheItem struct {
    value    interface{}
    expireAt time.Time
}

func NewLocalCache() *LocalCache {
    cache := &LocalCache{
        items: make(map[string]*cacheItem),
    }
    go cache.cleanup()
    return cache
}

func (c *LocalCache) Set(key string, value interface{}, ttl time.Duration) {
    c.mu.Lock()
    defer c.mu.Unlock()

    c.items[key] = &cacheItem{
        value:    value,
        expireAt: time.Now().Add(ttl),
    }
}

func (c *LocalCache) Get(key string) (interface{}, bool) {
    c.mu.RLock()
    defer c.mu.RUnlock()

    item, ok := c.items[key]
    if !ok || time.Now().After(item.expireAt) {
        return nil, false
    }
    return item.value, true
}

func (c *LocalCache) cleanup() {
    ticker := time.NewTicker(time.Minute)
    for range ticker.C {
        c.mu.Lock()
        for key, item := range c.items {
            if time.Now().After(item.expireAt) {
                delete(c.items, key)
            }
        }
        c.mu.Unlock()
    }
}

// 使用成熟库: github.com/patrickmn/go-cache
// 或 github.com/allegro/bigcache (高并发优化)

六、检查清单

缓存设计检查

  • [ ] 缓存模式选择是否合适?
  • [ ] 缓存 key 设计是否规范?
  • [ ] TTL 设置是否合理?
  • [ ] 是否处理了缓存穿透/击穿/雪崩?

一致性检查

  • [ ] 是否有缓存更新策略?
  • [ ] 是否考虑了并发更新?
  • [ ] 删除失败是否有重试?
  • [ ] 是否需要强一致性?

💬 讨论

使用 GitHub 账号登录后即可参与讨论

基于 MIT 许可发布