直接用 redis streams 做 composer 镜像同步队列可行,但必须将任务粒度定在 provider 文件级(如 p2/laravel/framework/10.0.0.json),并在消息中显式携带 url、sha256、retry_count、attempt_at 等字段,同时严格配置 xgroup create $ 起始点、xclaim 超时 pending 消息及多语言统一 schema,否则易引发幂等性缺失、写冲突与一致性崩溃。

直接用 Redis Streams 做 Composer 镜像同步任务的分布式队列,可行,但必须绕开两个关键陷阱:任务幂等性缺失和 provider 分片依赖未显式建模。
为什么不能直接 XADD 一个包名就当任务?
Composer 镜像同步不是“拉一个包”,而是按 provider-laravel~10.0.json 这类分片元数据文件逐个抓取、解析、下载、校验、写入本地镜像目录。一条消息若只含 "package": "laravel/framework",消费者无法知道该拉哪个 provider 片、是否已同步过、失败后重试时要不要跳过已存在的 tarball。
- 必须把任务粒度定在「provider 文件级」,例如
{"url": "https://repo.packagist.org/p2/laravel/framework/10.0.0.json", "sha256": "a1b2..."} - 消息体里要带
retry_count和attempt_at,否则网络超时后重复投递会触发多节点并发拉同一份 JSON,浪费带宽且可能写冲突 -
XADD时得用MAXLEN ~10000限长,否则持续写入providersStream 会导致 Redis 内存不可控——镜像站每分钟新增几十个 provider 片是常态
XGROUP CREATE 必须指定 $ 而非 0-0
新上线的同步 worker 不能从头消费所有历史 provider 请求。刚启动时,provider-laravel~9.0.json 可能早已同步完成,再重跑一遍既耗时又可能因版本号冲突导致索引损坏。
- 创建组用
XGROUP CREATE providers mirror-group $,确保新消费者只处理未来新增的 provider 片 - 已有 backlog 需人工干预:先
XRANGE providers - + COUNT 1查最新 ID,再XGROUP SETID providers mirror-group <latest_id></latest_id>对齐进度 - 别依赖
XINFO GROUPS的last-delivered-id判断是否空闲——它只反映上次成功XREADGROUP的 ID,不等于实际处理完成点
消费者 crash 后 pending 消息怎么救?
Worker 正在解析 provider-symfony~6.4.json 时 panic,这条消息会滞留在 PENDING 列表里,其他 worker 默认读不到它,导致该 provider 片长期卡住、下游包无法更新。
- 必须定期运行守护脚本,用
XCLAIM抢回超时 pending 消息:XCLAIM providers mirror-group recoverer 3600000 0-0 IDLE 1800000(抢 idle 超 30 分钟的消息) -
XCLAIM返回的消息需重新XADD回 stream 尾部,并增加retry_count,避免无限循环 - 不要在
XREADGROUP里设COUNT 1—— 单次只取一条,出错就断,效率低;建议COUNT 10批量处理,配合XAUTOCLAIM自动清理 idle 消息
如何让不同语言的 worker 共享同一套任务流?
PHP 侧用 predis/predis 写 provider 解析器,Go 侧用 go-redis/redis 做 tarball 下载器,两者必须对消息结构达成严格共识,否则字段错位会导致 silent fail。
- 定义统一 schema:强制所有消息含
type("provider"/"package")、url、checksum、retry_count字段,缺失即拒收 - PHP worker 发送前调用
json_encode($msg, JSON_THROW_ON_ERROR),Go worker 用json.Unmarshal并检查 error,不接受裸 map[string]interface{} - 别用
XRANGE做任务分发——它不提供 consumer group 语义,多语言混跑时无法保证同组内负载均衡,必须死守XREADGROUP
真正麻烦的不是 Redis 命令怎么写,而是每个 provider 片背后关联着几十甚至上百个具体包的 tarball URL,这些 URL 又依赖于上游 packages.json 的 hash 校验逻辑。任务消息里漏掉一个 content-hash 字段,整个镜像一致性就崩了。











