数据库与缓存一致性需按业务选策略:Cache-Aside最常用,写后删缓存更安全;强一致可用延迟双删或版本号;穿透、击穿、雪崩须配空值、锁、随机过期等兜底。

数据库与缓存一致性是 PHP 应用中高频出现的难题,核心在于:写操作后缓存未及时更新或失效,导致读到旧数据。这不是“加个 Redis 就完事”的问题,而需要结合业务场景、数据敏感度和性能要求做取舍设计。
缓存更新策略选型要匹配业务特性
常见策略有三种,没有银弹,只看哪个更贴合你的数据读写模式:
- Cache-Aside(旁路缓存):最常用。读时先查缓存,未命中再查 DB 并回填;写时先更新 DB,再删缓存(不是更新缓存)。适合读多写少、允许短暂不一致的场景,比如商品详情页。
- Write-Through(写穿透):写请求直接打到缓存层,由缓存同步写入 DB。缓存始终最新,但写延迟高、实现复杂,PHP 中较少直接使用,更适合中间件层(如自研代理)。
- Write-Behind(写回):写请求只更新缓存,后台异步刷回 DB。吞吐高,但存在数据丢失风险,仅适用于日志、统计类弱一致性数据,一般不用于核心业务。
删除缓存比更新缓存更安全可靠
很多人习惯“更新 DB 后 set 新值到 Redis”,这容易引发竞态问题:两个并发写请求 A 和 B,A 查 DB 得旧值、B 先完成写 DB 并 set 新值,A 后 set 旧值——缓存被污染。删除缓存(DEL 或 UNLINK)天然规避该问题,且省去序列化/反序列化开销。
建议写操作流程为:
立即学习“PHP免费学习笔记(深入)”;
- 开启数据库事务(确保 DB 更新原子性)
- 执行 SQL UPDATE / INSERT / DELETE
- 事务提交成功后,调用
$redis->del('user:123')或批量删除相关 key(如带通配符的 tag 清理需配合 SCAN + DEL) - 若删除失败,记录告警并触发补偿任务(如基于 binlog 或消息队列重试)
用延迟双删+版本号应对极端并发
在极少数强一致性要求场景(如资金账户余额),仅删一次仍可能出问题:写请求删缓存 → DB 更新 → 读请求因缓存缺失查 DB 得旧值并写入缓存 → 写请求再删一次。此时可采用“延迟双删”:
- 先删缓存
- 更新 DB
- 休眠 100–500ms(根据主从复制延迟调整)
- 再删一次缓存
更稳健的做法是引入逻辑版本号:DB 表加 version 字段,缓存 value 存为 ['data' => ..., 'version' => 123],读时比对版本,过期则主动回源并更新缓存,避免脏数据长期滞留。
缓存穿透、击穿、雪崩要有兜底机制
一致性不只是“新旧问题”,更是“有无问题”:
-
穿透:查不存在的 ID(如 -1、超长字符串),大量打到 DB。解决方案:缓存空对象(
setex user:-1 '' 60),或用布隆过滤器前置拦截。 -
击穿:热点 key 过期瞬间大量并发读。解决方案:加互斥锁(Redis
SET key lock NX EX 30),首个请求加载 DB 并写缓存,其余等待后直读缓存。 -
雪崩:大量 key 同一时间过期。解决方案:设置随机过期时间(如
EXPIRE key rand(3600, 7200)),或用永不过期 + 主动刷新机制(后台定时任务 reload 热点数据)。
不复杂但容易忽略:所有缓存操作必须有超时和降级。比如 Redis 不可用时,自动降级为直连 DB,并记录监控指标。一致性设计的终点不是“永远正确”,而是“可控的错误边界”。











