php与redis缓存不一致本质是可控性问题,需将不一致窗口压至业务可容忍范围:先更新数据库再删缓存,失败须重试;延迟双删需结合主从延迟与重建耗时;过期时间必加随机抖动;空值缓存与布隆过滤器防穿透。

PHP 与 Redis 缓存不一致不是“能不能避免”的问题,而是“在哪种程度上可控”的问题。强一致在绝大多数 Web 场景下既不可行也不必要;真正要做的,是把不一致的时间窗口压到业务可容忍范围,并让失败路径有明确兜底。
先删缓存再更新数据库?错,顺序必须反过来
很多人直觉认为“删缓存 → 更新 DB”能防止脏数据写入,但实际会放大竞态风险:请求 A 删除缓存后,请求 B 瞬间读不到缓存、查出旧数据并回填,接着请求 A 才完成 DB 更新——结果缓存里还是旧值。
- 正确顺序永远是:
UPDATE数据库成功后,再执行$redis->del('key') - 必须保证数据库操作已提交(事务已
COMMIT),否则删缓存后 DB 回滚,就真成空转了 - 如果
del失败(如 Redis 网络超时),不能静默忽略——要记录日志 + 触发异步重试,或走降级逻辑(比如强制设一个短过期)
延迟双删不是玄学,关键在时间窗口和重试机制
单次 del 后仍可能被并发请求“重建旧缓存”,延迟双删本质是用时间差堵住这个窗口。但它不是固定 sleep(1),而应结合业务 RT 和缓存重建耗时来定。
- 第一次
del在 DB 更新后立即执行 - 第二次
del在max(数据库主从同步延迟, 缓存重建平均耗时) + 100~200ms后触发,避免误杀新缓存 - 推荐用异步方式实现第二次删除(如投递到消息队列或定时任务),别阻塞主流程
- 如果业务对一致性极其敏感(如余额变更),延迟双删应配合
SETNX锁控制重建入口,防止多个请求同时回源
缓存失效策略必须带随机抖动
大量 key 集中过期会引发雪崩,而“统一设置 3600 秒”正是最常见诱因。哪怕你用了延迟双删,没加抖动的过期时间依然会让缓存层在整点集体失守。
- 所有
setex或set的$expire参数,都应叠加rand(0, 300)这类随机秒数 - 不要只在开发环境加——上线前检查所有缓存写入点,包括会话、配置、列表页等非核心路径
- 注意:抖动只适用于
setex类被动失效;主动删除(del)不受影响,该删还得删
空值缓存和布隆过滤器不是选配,是防穿透刚需
缓存穿透本身不会导致不一致,但它会绕过所有一致性保护逻辑——因为“不存在的 key”根本不会进你的更新链路。一旦攻击者高频刷 user:999999999,DB 直接被打挂,连谈一致性的资格都没了。
- 对确定不存在的数据(如被逻辑删除的用户),也写入
$redis->setex('user:123', 60, 'null'),并让业务层识别该值跳过后续处理 - 高频 ID 类查询(如订单号、商品 SKU)务必前置布隆过滤器,
BloomFilter::exists($sku)返回 false 就直接拦截,不碰 Redis 和 DB - 布隆过滤器需定期重建(如每天凌晨),避免误判率随数据增长上升;重建过程要支持热切换,不能停服
一致性最难的部分从来不是代码怎么写,而是你能否清晰定义“多长时间不一致算可接受”——这个数字决定了你要不要上分布式锁、要不要引入 MQ、要不要改表结构加版本号。没想清楚这点,所有优化都是在给错误目标堆参数。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











