Redis 缓存策略
缓存模式
Cache Aside(旁路缓存)
应用程序同时维护缓存和数据库:
读:先查缓存 → 命中则返回 → 未命中则查 DB → 写入缓存 → 返回
写:先更新 DB → 使缓存失效
func GetUser(id string) (*User, error) {
// 1. 查缓存
user, err := cache.Get("user:" + id)
if err == nil {
return user, nil
}
// 2. 缓存未命中,查数据库
user, err = db.GetUser(id)
if err != nil {
return nil, err
}
// 3. 写入缓存(设置过期时间)
cache.Set("user:"+id, user, 1*time.Hour)
return user, nil
}
func UpdateUser(id string, data *User) error {
// 1. 更新数据库
err := db.UpdateUser(id, data)
if err != nil {
return err
}
// 2. 使缓存失效
cache.Del("user:" + id)
return nil
}
Read Through
缓存层负责从数据库加载数据,应用程序只与缓存交互。
Write Through
写入时先写缓存,缓存负责同步写入数据库。
Write Behind
写入时只写缓存,缓存异步批量写入数据库,性能最好但可能丢数据。
常见问题
缓存穿透
查询一个不存在的数据,导致每次请求都穿透到数据库。
解决方案:
// 1. 缓存空值
cache.Set("user:no_such_id", nil, 5*time.Minute)
// 2. 布隆过滤器
bloomFilter.Contains("user:no_such_id") // 快速判断不存在
缓存雪崩
大量缓存同时过期,导致请求全部落到数据库。
解决方案:
// 1. 过期时间加随机值
expire := 3600 + rand.Intn(600) // 基础时间 + 随机值
// 2. 多级缓存:本地缓存 + Redis
// 3. 限流降级:保护数据库
缓存击穿
热点 key 在过期瞬间被大量并发请求穿透。
解决方案:
// 互斥锁(分布式锁)
func GetUserWithLock(id string) (*User, error) {
// 1. 查缓存
user, _ := cache.Get("user:" + id)
if user != nil {
return user, nil
}
// 2. 获取分布式锁
lock := redisLock.Lock("lock:user:" + id)
if !lock {
// 没拿到锁,等待后重试
time.Sleep(50 * time.Millisecond)
return GetUserWithLock(id)
}
defer lock.Unlock()
// 3. 双重检查
user, _ = cache.Get("user:" + id)
if user != nil {
return user, nil
}
// 4. 查数据库
user, _ = db.GetUser(id)
cache.Set("user:"+id, user, 1*time.Hour)
return user, nil
}
缓存与数据库一致性问题
最终一致性方案:
- 先更新 DB,后删缓存(推荐)
- 配合消息队列,异步重试失败的缓存删除
- 设置合理的过期时间兜底
内存淘汰策略
| 策略 | 说明 | 适用场景 |
|---|---|---|
noeviction |
不淘汰,写满返回错误 | 不允许丢数据 |
allkeys-lru |
淘汰最近最少使用的 key | 通用缓存 |
allkeys-lfu |
淘汰最不经常使用的 key | 访问频率差异大 |
volatile-ttl |
淘汰即将过期的 key | 设置了过期时间的缓存 |
volatile-lru |
在设置了过期时间的 key 中 LRU | 混合场景 |
# 配置最大内存和淘汰策略
maxmemory 4gb
maxmemory-policy allkeys-lru