缓存常见问题

Redis 常作为数据库前面的缓存层。缓存能降低数据库压力,但也会引入一致性、穿透、击穿、雪崩等问题。

一、缓存读取流程

常见读取流程:

1. 先查 Redis
2. 命中则直接返回
3. 未命中再查数据库
4. 数据库查到数据后写入 Redis,并设置 TTL
5. 返回数据

伪代码:

cache = redis.get(key)
if cache exists:
    return cache

data = db.query(id)
if data exists:
    redis.set(key, data, ttl)

return data

二、缓存穿透

缓存穿透指查询的数据在缓存和数据库里都不存在。

如果大量请求都查不存在的数据,每次都会打到数据库。

示例:

GET user:999999:profile
数据库也没有 user_id = 999999

常见处理:

方案说明
缓存空值数据库查不到时,给空结果设置短 TTL
参数校验明显非法的 ID 直接拦截
布隆过滤器先判断 ID 是否可能存在

缓存空值示例:

SET user:999999:profile "__NULL__" EX 60

读取时遇到 __NULL__,直接返回空结果,不再查数据库。

三、缓存击穿

缓存击穿指某个热点 key 过期,瞬间大量请求同时查数据库。

示例:

article:100:detail 是热点缓存
刚好过期
大量请求同时进来
数据库压力突然升高

常见处理:

方案说明
互斥锁重建缓存只有一个请求查数据库并回写缓存
热点 key 不过期后台定时刷新
逻辑过期value 里存过期时间,异步刷新

互斥锁思路:

SET lock:cache:article:100 rebuilding NX EX 10

拿到锁的请求查数据库并重建缓存;没拿到锁的请求可以短暂等待后重试,或返回旧值。

四、缓存雪崩

缓存雪崩指大量 key 在同一时间过期,导致请求集中打到数据库。

常见原因:

  • 批量导入缓存时 TTL 都相同。
  • Redis 节点异常。
  • 热点缓存集中失效。

常见处理:

方案说明
TTL 加随机值避免同一批 key 同时过期
提前预热系统启动或活动前提前加载热点数据
限流降级数据库压力过高时保护核心链路
Redis 高可用哨兵或集群降低单点风险

TTL 随机值示例:

基础 TTL:1800 秒
随机增加:0 到 300 秒
最终 TTL:1800 到 2100 秒

五、缓存和数据库一致性

常见写流程:

先更新数据库
再删除缓存

示例:

UPDATE articles SET title = 'new title' WHERE id = 100;
DEL article:100:detail

下一次读取时,缓存未命中,再从数据库加载新数据。

不推荐先更新缓存再更新数据库,因为数据库更新失败时缓存里可能已经是新值。

六、延迟双删

某些高并发场景中,可能出现旧数据被重新写回缓存。

延迟双删思路:

1. 删除缓存
2. 更新数据库
3. 等待一小段时间
4. 再删除一次缓存

这不是万能方案。等待时间取决于业务请求耗时和数据库延迟,使用前要评估复杂度。

多数普通后台系统先使用“更新数据库后删除缓存”,再根据实际并发问题优化。

七、缓存 key 排查

常用排查命令:

TYPE article:100:detail
TTL article:100:detail
MEMORY USAGE article:100:detail

批量扫描:

SCAN 0 MATCH article:* COUNT 100

生产环境避免直接执行:

KEYS *