Redis Stream 作为队列的 ACK 机制
解读
国内一线互联网公司在高并发业务(秒杀、订单、消息推送)中普遍用 Redis Stream 替代 List 做轻量级队列。面试官问“ACK 机制”并不是想听“消费者读完就删掉”这种 List 思维,而是考察候选人是否理解 Stream 的“待处理条目列表(PEL)”设计、是否能在 PHP-FPM 或 Swoole 协程里正确 ack,以及是否具备“至少一次”到“恰好一次”的容错经验。答不出 XACK 命令、组消费模型、消息重放与死信策略,会被直接判定为“只用过 Redis 做缓存,没做过队列”。
知识点
- Stream 结构:消息 ID(时间戳-序号)、field-value 键值对、消费者组(group)、消费者(consumer)、PEL(Pending Entries List)。
- 消费流程:XGROUP CREATE -> XREADGROUP -> 业务处理 -> XACK;未 ack 的消息一直挂在 PEL,通过 XPENDING 可查看。
- ACK 命令:XACK key group ID [ID ...],返回整数表示成功 ack 数;ack 后消息从 PEL 移除,Redis 不自动删除消息,需 XDEL 或 XTRIM。
- 重试与死信:XPENDING 查出 idle 时间,用 XCLAIM 把超期消息转移给新消费者;国内常见阈值 30 s 重试 3 次,失败后用 Lua 脚本 XDEL 并写入 Redis Hash 做死信表。
- PHP 端实现:phpredis 5.3+ 支持 stream 命令;Swoole 协程下用 Co\Redis 可异步消费;需把 ack 与业务 DB 事务放在同一本地事务表,利用本地事务表做幂等,实现“至少一次”到“恰好一次”。
- 性能调优:pipeline 批量 XACK 降低 RTT;控制 stream 长度防止内存爆炸,生产端 XADD 带 MAXLEN ~ 100k;监控指标:PEL 长度、消费速率、重试率。
答案
“Redis Stream 的 ACK 机制是消费者组模型里的核心可靠性手段。流程分四步:
- 建组:XGROUP CREATE mq group1 0 MKSTREAM,确保组存在。
- 拉消息:XREADGROUP GROUP group1 consumer1 COUNT 5 BLOCK 2000 STREAMS mq >,返回 5 条消息,Redis 会立即把这些消息写入 group1 的 PEL,状态为“已投递未确认”。
- 业务处理:PHP 端拿到消息后先写入本地幂等表(order_id UNIQUE KEY),再做业务落库,保证 ack 前业务已持久化。
- 手动 ack:处理成功后执行 XACK mq group1 1652345689-0,Redis 把该 ID 从 PEL 移除,完成“至少一次”语义;若 PHP 进程崩溃,消息仍在 PEL,30 s 后另一消费者可用 XCLAIM 接管并重试,重试 3 次仍失败则 XDEL 并写入死信 Hash,供后台人工干预。 通过 XPENDING 可以实时监控堆积量,配合 Prometheus + Grafana 告警,实现高可用队列。”
拓展思考
- 顺序性与并行度矛盾:同一业务 key 若必须保序,可在 PHP 端用 ID 取模把不同 key 散列到不同 stream,或利用 XREADGROUP 的 COUNT 1 串行消费,但会降低吞吐,需在业务层权衡。
- 幂等键设计:国内大厂常用“业务主键+版本号”做 UK,防止重复扣款;ACK 与本地事务表放在同一 MySQL 事务,利用事务表成功才提交 XACK,崩溃回滚时消息仍留在 PEL,天然实现“恰好一次”。
- 云原生场景:在腾讯云 TKE 或阿里云 ACK 中,PHP 业务容器弹性伸缩,需把 consumer name 设为 pod 名,崩溃后旧 PEL 消息可被新 pod 用 XCLAIM 快速认领,避免消息停留在已销毁容器。
- 与 Kafka 对比:Redis Stream 轻量、延迟低(<1 ms),适合 10 万级 QPS 以下;Kafka 适合百万级且需要多副本、高吞吐场景;面试时可补充“选型看量级与运维成本”,体现架构判断力。