缓存模式
一、缓存架构
缓存层次
┌─────────────────────────────────────────────────────────────────┐
│ 缓存层次架构 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 客户端 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 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 账号登录后即可参与讨论