
Redis 把热点数据放进内存,能显著降低数据库压力,但它与数据库是两套独立存储,读写顺序或过期策略不当,就会出现数据不一致甚至击穿数据库。本文讲透穿透、击穿、雪崩三类风险的防护方案。
一、缓存读写策略与失效顺序
1. 旁路缓存是主流做法
旁路缓存(Cache Aside)由业务代码同时操作缓存与数据库:读时先查 Redis,未命中再查库并回写;写时先更新数据库,再删除缓存。选择删除而非更新,是并发下两个事务的更新顺序可能与写库顺序相反,反而写出更旧的值。
2. 先更新库还是先删缓存
若先删缓存再更库,两步之间若有读请求进来,会把旧值重新加载回缓存,造成长期不一致;先更库再删缓存,只在删缓存前出现短暂旧值,窗口更小。因此默认“先更库、后删缓存”。
二、穿透、击穿与雪崩的防护
1. 缓存穿透与布隆过滤器
穿透指查询的键在缓存和数据库都不存在,请求每次都落到库上。防护一是在入口用布隆过滤器拦截,它用位数组加多个哈希函数判断元素“可能存在”或“一定不存在”,判定不存在的键直接返回;二是给不存在的键写入短 TTL 的空值占位。
// 布隆过滤器判定不存在的键直接返回,不查库
if (!$redis->getBit('user:bf:' . ($uid % 8), $uid)) {
return null;
}
$key = 'user:' . $uid;
$data = $redis->get($key);
if ($data === false) {
$data = $db->query('SELECT ... FROM users WHERE id = ?', [$uid]);
$redis->setex($key, $data ? 600 : 60, json_encode($data)); // 空结果写短 TTL
}
2. 缓存击穿与互斥重建
击穿指某个热点键过期的瞬间,大量并发请求同时未命中,一起涌向数据库重建缓存。常用解法是加互斥锁,只放一个请求查库重建,其余等待后重试读缓存。
$key = 'hot:goods:' . $gid;
$data = $redis->get($key);
if ($data === false) {
// SET NX EX 抢锁,只让一个请求重建缓存
if ($redis->set('lock:' . $key, 1, ['nx', 'ex' => 5])) {
try {
$data = $db->query('SELECT ... FROM goods WHERE id = ?', [$gid]);
$redis->setex($key, 300 + rand(0, 120), json_encode($data));
} finally {
$redis->del('lock:' . $key);
}
} else {
usleep(50000); // 等待后重试读缓存
$data = $redis->get($key);
}
}
3. 缓存雪崩与过期打散
雪崩指大批键在同一时刻集中过期,或 Redis 实例宕机,流量瞬间全压到数据库。防护要点有三:给过期时间加随机抖动;核心数据做本地加 Redis 的多级缓存;Redis 用集群保证高可用。
三、落地实践与监控
1. 空值占位与热点预热
空值占位写入后,读取时要统一判断,不能把占位当有效数据返回。大促前对已知热点键做预热,避免洪峰时集中回源重建。日常还要盯住命中率与内存水位:命中率持续下滑通常意味着批量键过期,内存逼近 maxmemory 后会触发淘汰,需确认淘汰策略与预期一致。
关键配置提示
- 空值占位 TTL 取 30 到 60 秒,避免长期占内存。
- 互斥锁必须同时设置过期时间与 finally 释放,防止异常时死锁。
- 统一给过期时间加随机抖动,是成本最低的雪崩防护手段。

快来评论,快来抢沙发吧~