hyperf 3.1 异步队列任务因消息体超512kb导致redis报错或消费者静默退出,根本原因是php serialize未压缩致单条value过大;必须改用json+gzip压缩并校验体积,否则触发oom或任务丢失。

Hyperf 3.1 异步队列任务提交后 Redis 报错“MISCONF Redis is configured to save RDB snapshots, but it is currently not able to persist on disk”,或消费者进程静默退出、任务丢失,八成是消息体未经压缩直接塞进 Redis 导致单条 value 超出 512KB 安全阈值——这不是配置问题,而是序列化方式没改造成的内存溢出风险。
为什么必须限制消息体大小
Hyperf 默认用 PHP serialize() 序列化 Job 对象,一个含 5 万条订单明细的数组经 serialize 后可能达 6.8MB;Redis 单 key value 超过 1MB 就会显著拖慢响应,触发 qubf-free 告警并断连;更严重的是,Hyperf 的 RedisClient 输入缓冲区默认 1GB 但不可调,超大 payload 卡在缓冲区堆积,最终导致协程挂起、任务丢弃。
Redis 8.2.3 已修复 CVE-2025-62507 等高危漏洞,但对超大 value 的处理逻辑未变——它不会拒绝写入,而是 silently 拒绝消费或触发 OOM Kill。
强制压缩消息体(必须执行)
方法一:重写 Job 类的 serialize() 和 unserialize() 方法
在 Job 类中覆盖父类序列化逻辑,禁用原生 serialize,改用 json_encode + gzdeflate + base64_encode:
第一步:在 App\Job\SendEmailJob 中添加以下方法:
public function serialize(): string { $data = [ 'orderId' => $this->orderId, 'templateId' => $this->templateId, 'params' => $this->params, ]; $json = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); if (strlen($json) > 100 * 1024) { $compressed = gzdeflate($json); return base64_encode('gz:' . $compressed); } return base64_encode('raw:' . $json); }
第二步:对应实现 unserialize(),严格按前缀判断解压逻辑,【不加 try/catch 会导致协程中断且 RSS 不释放】:
public function unserialize(string $data): void { $decoded = base64_decode($data); if (substr($decoded, 0, 3) === 'gz:') { $content = @gzinflate(substr($decoded, 3)); if ($content === false) { throw new RuntimeException('Failed to inflate job data'); } $data = $content; } else { $data = substr($decoded, 4); } $arr = json_decode($data, true); $this->orderId = $arr['orderId'] ?? null; $this->templateId = $arr['templateId'] ?? null; $this->params = $arr['params'] ?? []; }
写入前体积校验(不可跳过)
在调用 $this->asyncQueue->push() 前插入校验逻辑,避免无效投递:
直接在 Job 实例化后、push 前加一行:if (strlen($job->serialize()) > 512 * 1024) { throw new RuntimeException('Job payload exceeds 512KB limit'); }
这一步操作起来很简单,直接把校验塞进控制器或服务层即可。不加校验,Redis 不报错但消费者拿不到完整数据,日志里只显示 “failed to unserialize”,排查时会绕进反序列化兼容性陷阱。
【key 名必须带 :gz 标记,例如 async-queue:send-email:20260806:gz】,否则后续灰度下线压缩逻辑时无法识别旧数据格式。
消费者端解压兜底(最后防线)
方法二:全局拦截 Consumer 解包流程
修改 vendor/hyperf/async-queue/src/Driver/RedisDriver.php 的 pop() 方法,在返回前插入解压分支:
找到 $payload = $this->redis->lPop($this->channel); 后一行,插入:
if (is_string($payload) && strlen($payload) > 1000 && str_starts_with($payload, 'gz:')) { $payload = @gzinflate(base64_decode(substr($payload, 3))); if ($payload === false) { $this->logger->error('Invalid gz-compressed job payload', ['raw' => base64_encode($payload)]); return null; } }
这行代码必须放在 pop 之后、反序列化之前,否则解压时机错误会导致 Job 构造失败。











