PHP无原生熔断机制,需用Redis+Lua脚本原子统计失败率(如60秒内失败率≥80%且请求数≥5),单独存储state键控制closed/open/half-open状态,并在Guzzle中间件中统一捕获各类网络异常、区分DNS与业务失败,避免APCu进程隔离导致的单点假熔断。

PHP 没有原生熔断机制,所谓“基于失败率自动开启熔断”,必须自己建状态跟踪+共享存储+阈值判断三件套;光设 timeout 或捕获异常根本不算熔断,只是单次请求中断。
怎么用 Redis 实现失败率统计和熔断开关
失败率不是看“最近 5 次有没有失败”,而是“过去 60 秒内失败数 ÷ 总请求数 ≥ 80% 且总请求数 ≥ 5”——这个窗口和比例必须可配,不能硬编码。
- 别用
INCR+EXPIRE分两步更新计数器:非原子操作,在并发下会丢失败记录 - 改用 Lua 脚本一次性完成:读时间窗口、过滤旧记录、累加新失败、写回并设 TTL
- 熔断开关单独存一个 key,比如
circuit:payment_api:state,值为open/closed/half-open,带EX 300(5 分钟过期)防永久卡死 - 每次请求前先查这个 state key,是
open就直接 fallback,不碰下游
Guzzle 中间件怎么插熔断逻辑而不漏异常
只监听 onRejected 或 catch RequestException 不够——ConnectException、TimeoutException、甚至 DNS 解析失败(getaddrinfo failed)都得归口处理,但 DNS 失败不该计入失败率。
- 在 Guzzle Middleware 的前置逻辑里调用
canCall($serviceKey),返回 false 就抛GuzzleHttp\Exception\ConnectException模拟连接失败,让上层统一捕获 - 在
on_stats回调里判断:$stats->hasError()且$stats->getTransferTime() > $timeoutThreshold才算超时失败 - HTTP 状态码 5xx 计入失败,4xx 不计入(通常是客户端问题);
curl_error中含Connection refused或Operation timed out才记,Could not resolve host跳过 - 成功响应后必须调用
recordSuccess($serviceKey)清空失败计数器,否则半开永远进不去
为什么用 APCu 做熔断存储在 FPM 下大概率失效
APCu 是进程内缓存,PHP-FPM 每个 worker 是独立进程,A 进程熔断了,B 进程完全不知道,照样疯狂发请求——这不是熔断,是“单线程假装熔断”。
立即学习“PHP免费学习笔记(深入)”;
- 仅限 CLI 脚本或 PHPUnit 测试用
array驱动,生产环境必须用 Redis - 如果硬要用 APCu,key 必须带进程标识,比如
"cb_".md5($serviceKey."_".getmypid()),但这解决不了跨进程协同问题 - APCu 的
apcu_store()过期时间别设太长,熔断状态本质是临时决策,60秒比0(永不过期)安全得多 - Swoole 场景下也别依赖内存:多 Worker 时仍存在状态不同步,Redis 仍是唯一靠谱选择
真正难的不是写个 if ($state === 'open') return $fallback(),而是怎么让失败统计不被并发冲掉、怎么让半开试探不变成全量打爆、怎么区分基础设施故障和业务异常——这些细节没压住,熔断器反而会成为雪崩加速器。



















