服务端共享数据刷新策略
解读
“服务端共享数据刷新策略”在国内 PHP 面试中通常不是考“缓存穿透/雪崩”八股文,而是考察候选人能否把“共享”与“刷新”这两个动作放在真实高并发业务里闭环:
- 共享——多进程(FPM)、多机(K8s 弹性 Pod)、多地域(单元化部署)如何拿到同一份数据;
- 刷新——当上游 DB/Binlog/运营后台变更后,如何在秒级甚至毫秒级让全部节点“看到”且“拿到”新数据,同时保证并发安全、对 DB 零冲击、对用户体验无损。
面试官常追问:
“如果凌晨 0 点做全量缓存重建,流量突增 3w QPS,你怎么保证没有瞬间集体回源打挂 MySQL?”
“如果用了 Redis,主从延迟 200ms,华南机房读到旧数据,用户支付成功却提示库存不足,你怎么解释?”
因此,回答必须落地到“PHP-FPM 生命周期 + Swoole/OpenSwoole 协程 + 国内云厂商 Redis/Memcached 特性 + 消息队列(RocketMQ/RabbitMQ)+ 监控(夜莺/Prometheus)”这一整套技术栈。
知识点
- PHP 运行期可见性:
- FPM 无共享内存,请求结束即释放;CLI 模式常驻才可用 APCu/Shmop。
- 常见共享介质:
- Redis(单线程、Lua 原子、支持 Gossip 集群)、Memcached(多线程、更轻量)、本地文件(OPcache 预加载配置)、MySQL 本身(行级锁 + 乐观锁)。
- 刷新语义:
- 失效(delete)、更新(set)、版本号(MVCC/etag)、异步重建(队列 + 多线程)。
- 并发安全:
- SET NX EX 分布式锁、RedLock、Lua 脚本、Watch/Multi、MySQL SELECT … FOR UPDATE。
- 国内云厂商差异:
- 阿里云 Tair 支持“瞬时键”秒级过期、腾讯云 CRS 提供“缓存分析”一键热 Key 探测、华为云 DCS 支持“异步淘汰”通知。
- 监控与灰度:
- 使用 Redis Keyspace Notification + Consumer Group 做实时刷新;夜莺告警配合 Grafana 看缓存命中率、回源 QPS、DB 连接池抖动。
答案
以下给出一套在 2024 年国内电商大促验证过的“三级缓存 + 异步消息 + 版本号”方案,可直接落地到 PHP 7.4/8.x + Laravel/Symfony 项目。
-
整体拓扑
用户请求 → CDN → 网关(Nginx+Lua) → PHP-FPM → 本地 LRU (APCu 256 MB) → Redis 集群 (16 分片) → MySQL 主从。
共享数据以“商品库存”为例,Key 规则:sku:stock:{sku_id},Value 为 JSON:{"stock":99,"version":1700000001}。 -
写路径(刷新源头)
运营后台或订单履约系统更新 MySQL 后,触发 Binlog → Canal → RocketMQ。
PHP 消费组(Swoole\Process 常驻)收到变更消息,执行:
a. 用 Lua 脚本保证原子:
local v = redis.call("GET",KEYS[1])
local obj = cjson.decode(v)
obj.stock = ARGV[1]
obj.version = tonumber(ARGV[2])
redis.call("SET",KEYS[1], cjson.encode(obj), "EX", 3600)
b. 发布 Redis Keyspace 通知:PUBLISH keyevent@0:set sku:stock:{sku_id}
c. 删除本地缓存:使用 UDP 广播(端口 9999)到同机房所有 Pod,APCuDelete(sku:stock:{sku_id}),1 ms 内完成。 -
读路径(共享可见)
PHP-FPM 收到查询请求:
a. 先读 APCu,命中直接返回;
b. 未命中读 Redis,若 Redis 也没有,则回源 MySQL,同时加分布式锁(SET sku:lock:{sku_id} NX EX 3),单线程重建;
c. 拿到数据后写入 Redis 并带随机 TTL 300~600 s,防止雪崩;
d. 回包前把数据塞进 APCu,TTL 5 s,降低热 Key 重复回源。 -
并发安全细节
- 库存扣减使用“Redis + Lua 脚本”预扣,保证原子性;
- 版本号递增由 Canal 消费组统一发号,使用 Redis INCR 全局序列,避免时钟回拨;
- 若 Redis 主从延迟,华南机房读到旧版本,则在网关层做“版本兜底”:如果 version < 期望版本,带 Stale-While-Revalidate 头,异步回源,前端弹层“数据更新中,请稍候”,用户可接受。
-
大促全量重建
凌晨低峰期通过“切片 + 限速”重建:- 把 5000 万 SKU 按尾号 0~9 拆 10 批,每批 500 ms 发 2000 条消息,整体限速 4w QPS;
- 采用 Redis Pipeline 批量 SET,每 100 条一次,RT 2 ms;
- 监控缓存命中率,若低于 90% 立即熔断重建任务,次日人工补数据。
-
灰度与回滚
在配置中心(Apollo/Nacos)放“缓存刷新开关”,按 SKU 维度灰度 1% 流量,观察 30 min,无异常再全量。
若出现缓存脏读,一键切换“强制读 DB”模式,DB 加索引可扛 1w QPS,10 分钟内修复。
拓展思考
- 如果业务升级到 Swoole 协程常驻内存,本地缓存可否用 Table/Channel 替代 APCu?
优势:无序列化开销,可存对象;劣势:多 Pod 间仍无法共享,需引入 Swow 共享内存扩展或 mmap 文件,运维复杂度升高。 - 在“单元化”架构下,上海单元与深圳单元各写各的 Redis,如何做到“异地多活”同时防止双写冲突?
可借鉴“单元封闭 + 全局版本号”方案:每个单元只写本单元 Redis,但版本号由全局 TSO(TiDB TSO 或自研 Snowflake)统一发放,消费侧按 version 做冲突检测,若 version 冲突则回退到 DB 乐观锁。 - 若未来库存系统迁到 Go/Java,PHP 仅做视图层,如何保持刷新协议不变?
把 Redis Key 规范、消息格式(JSON + 版本号)、Keyspace 通知主题、UDP 广播格式全部写入公司级“缓存刷新标准”,跨语言直接复用;PHP 侧只需保留轻量级 Client,实现“协议升级”热更新即可。