【结论】这三个不能混着治:穿透靠“证明数据不存在”,击穿靠“把重建串行化”,雪崩靠“打散过期时间 + 兜底降级”;用错方案,反而会让问题更严重。
【展开】你帖子末尾被截断的“逻辑过期”其实是击穿最优雅的解法,可以补完:key 本身不设 TTL,把 expire_at 写进 value;读到逻辑过期时先不删 key,用锁保证只有一个请求异步重建,其余请求直接把旧值返回。代价是这段时间存在脏读;换来的好处是永远不真正 miss。
但互斥锁那段有个经典问题要提醒:finally 里直接 del 可能删掉别人加的锁。A 的锁 10s 到期,B 已经抢到,A 才执行完 del,于是把 B 的锁删了。锁 value 要存请求唯一 ID,del 前比对,或用 Lua 脚本原子删除。另外 usleep(50000) 只重试一次,高并发下还是会有一批请求落到 DB,建议循环重试 3~5 次或直接降级返回旧值。
雪崩再补两点:① 批量写缓存时 TTL 一定加随机抖动,600 + mt_rand(0,120),同批 key 别用同一个过期时间点;② Redis 整台挂掉那种情况,前面那些方案都救不了,得靠多级缓存(进程内 APCu / 文件)+ 限流,至少让 DB 不被瞬间打穿。布隆过滤器也提醒一句:它有误判率,而且元素只能加不能删,数据删除后只能重建位图。
【落地】本站缓存统一走 Cache 层,Cache::remember($key, $ttl, fn() => $db->find($id)) 一行就能覆盖大部分场景,空值哨兵在回调里返回也行。不过它内部有没有做互斥重建,我不太确定,稳妥做法是在回调外面自己包一把锁。Redis 连不上时会自动回退文件驱动,相当于对“整台挂”有一点缓冲,但文件缓存并发能力有限,限流配置还是别省。
【最后一句】最常见的问题:空值哨兵用了一个和业务值可能重合的字符串(比如 0 或 ''),结果和“真没查到”分不开;统一用一个常量,比如 __NULL__,并在读取处唯一判断。
正式会员
认证极客






