结论:用 ngx.shared.dict 缓存 Composer 镜像源热点元数据可行高效,但须规避序列化开销与 TTL 失准——应缓存原始 JSON 字符串而非 PHP 数组,配合 get_stale + ETag 校验实现语义化过期,并用 safe_set 或锁前缀避免并发写入冲突。

直接说结论:用 ngx.shared.dict 缓存 Composer 镜像源的热点包元数据(如 packages.json、dist 信息)是可行且高效的,但必须绕开两个典型陷阱——序列化成本和过期判断失准。
为什么不能直接缓存 PHP 数组或 JSON 对象
ngx.shared.dict 只支持字符串、数字、布尔值、nil 这四种基础类型;传入 table、function、userdata 等会触发隐式序列化(实际调用 cjson.encode),而 Composer 的 packages.json 动辄几 MB,每次 set 都做一次完整 encode + 内存拷贝,worker CPU 负载会陡增。
- 错误写法:
dict:set("packagist:laravel/framework", require "cjson".encode(pkg_data), 300) - 正确做法:先用
cjson.encode转成字符串,再传给set,且只在必要时才 encode(比如首次加载或更新后) - 更优解:把原始 JSON 字符串(从上游响应体直接读取)原样缓存,避免任何 Lua 层 decode/encode
如何安全处理带 TTL 的元数据缓存
Composer 请求常含语义化版本约束(如 ^8.0),但镜像源返回的 packages.json 本身不含明确过期时间。若盲目设固定 TTL(如 300 秒),可能造成「缓存未过期但上游已删包」的不一致。
- 推荐策略:用
get_stale+ 校验 ETag 或 Last-Modified - 流程是:先
dict:get_stale("key")拿出旧数据(即使过期也返回),再发 HEAD 请求比对ETag;一致则直接回源缓存,不一致再 fetch 新数据并set - 注意:
get_stale返回的第三个值stale是布尔值,为true表示该 key 已过期但尚未被清理,可用于触发后台刷新逻辑
并发写入冲突怎么避免
当多个 worker 同时发现缓存 miss 并去 upstream 拉取同一份 packages.json,会造成重复请求和覆盖写入(后写的覆盖先写的,但内容可能不同)。
- 用
safe_set替代set:它只在 key 不存在时才写入,适合「首次填充」场景 - 更稳妥的是引入「锁前缀」:比如对
packagist:laravel/framework,先add("lock:packagist:laravel/framework", 1, 10),成功则去 fetch,失败则 sleep 后重试get - 别依赖
incr做分布式锁——incr不提供原子 compare-and-swap,无法防止 A/B 同时 incr 到 1 后都以为自己抢到了锁
真正难的不是 set 或 get,而是让缓存行为和 Composer 协议语义对齐:比如 packages.json 的变更不总是通过时间驱动,更多靠 ETag 或 Cache-Control: immutable;shared dict 的 TTL 机制只是辅助,不能替代 HTTP 缓存协议本身的校验逻辑。


















