PHP多进程共享缓存需防脏写,APCu用原子操作(如apcu_inc)或乐观锁,Redis用GETSET/Lua;缓存与DB双写应先更DB再删缓存并加重试和短TTL;APCu不跨CLI/Web,应改用Redis;穿透、击穿、雪崩需分别用空值缓存、互斥锁、随机TTL防御。

PHP多进程/多实例下共享缓存如何避免脏读
用 apcu 或 redis 做共享缓存时,多个 PHP 进程(如 FPM worker)同时读写同一键,不加控制就会出现「A 读旧值 → B 更新 → A 基于旧值覆盖写」这类脏写。这不是缓存失效问题,而是并发写冲突。
核心解法是「读-改-写」必须原子化,不能拆成三步。常见错误是先 apcu_fetch(),再算新值,再 apcu_store() —— 这中间可能已被其他进程覆盖。
- 对简单计数器类场景,优先用
apcu_inc()/apcu_dec(),它们底层调用原子指令,无需额外锁 - 对结构化数据(如用户配置数组),用
redis的GETSET或 Lua 脚本封装读写逻辑,保证服务端原子性 - 若必须用
apcu处理复杂逻辑,需配合apcu_add()做乐观锁:生成唯一 token 尝试设锁键,成功才执行业务,完成后删锁;失败则重试或降级
缓存与数据库双写时怎么防不一致
典型模式是「先更新 DB,再删缓存」,但网络延迟或进程崩溃可能导致删缓存失败,留下脏数据。更糟的是「先删缓存,再更新 DB」—— 若 DB 更新失败,下次读会把空或旧缓存重新加载。
关键不是选哪种顺序,而是补上兜底机制:
立即学习“PHP免费学习笔记(深入)”;
- 删缓存操作必须有重试(如写入消息队列,由消费者确保最终删除)
- 给缓存加短 TTL(如 30 秒),即使删失败,也能自然过期,避免永久不一致
- 对强一致性要求高的接口(如支付状态),绕过缓存直查 DB,或用
redis的WATCH+MULTI做事务校验
APCu 在 CLI 和 Web 环境间不共享怎么办
apcu 默认按进程隔离:Web 请求的 FPM worker 和 CLI 脚本(如定时任务)各自拥有独立的 APCu 内存空间,apcu_store() 在 CLI 里设的值,FPM 完全看不到。
这不是 bug,是设计使然。想跨环境共享,只有两个实际可行路径:
- 改用
redis或memcached—— 它们天然进程无关,所有 PHP 实例连同一服务端即可 - 强制让 CLI 使用 FPM 的 APCu(不推荐):在 CLI 启动时加
-d apc.enabled=1 -d apc.shm_size=64M,且确保和 FPM 共享同一apc.shm_segments配置,但极易因权限或内存映射失败而静默降级
多数项目踩坑都卡在这里:本地 CLI 测试时一切正常,上线后定时任务更新了缓存,Web 却读不到 —— 实际根本没共享。
Redis 缓存穿透、击穿、雪崩的实操防御点
这三类问题本质都是「缓存没拦住请求,大量打到 DB」,但触发条件和解法差异很大,不能套同一个方案。
- 穿透(查不存在的 ID):对
null结果也缓存(如设redis.setex("user:999999", 60, "NULL")),注意客户端读到 "NULL" 字符串要识别并返回空,而非当作真实数据 - 击穿(热点 key 过期瞬间并发请求):用
setnx抢锁,抢到的进程查 DB 并回填缓存,其他等待;或直接用永不过期 + 后台异步更新(需维护更新时间戳) - 雪崩(大量 key 同一时刻过期):给 TTL 加随机偏移,比如基础 3600 秒,再
+ rand(1, 300),分散过期时间
真正难的不是加这些措施,而是监控 —— 没有 redis 的 slowlog、key 过期率、DB 查询 P99 对比,你根本不知道哪类问题正在发生。











