别手写,用 php-circuit-breaker(v2.0+)或 thephpleague/circuit-breaker;二者支持失败率判定、半开状态、Redis/APCu 存储,FPM 下必须用 Redis 避免进程隔离导致熔断失效。

熔断器该用哪个库,php-circuit-breaker 还是自己手写?
直接说结论:别手写,用 php-circuit-breaker(v2.0+)或 thephpleague/circuit-breaker。手写容易漏掉状态同步、超时重置、并发竞争这些细节,尤其在 FPM 多进程下,共享状态难做,RedisStorage 也得自己兜底。
这两个库都支持基于失败率 + 半开状态 + 自动恢复,且能对接 Redis 或 APCu 做跨请求状态存储。FPM 下必须用 Redis,否则每个进程独立计数,熔断形同虚设。
常见错误现象:curl_exec 超时后熔断不触发、连续失败 10 次却没进 HALF_OPEN 状态——大概率是用了 APCu 但没配 apc.enable_cli=1(CLI 测试时),或没指定 storage 实例。
- 初始化时务必传入
RedisStorage,不要依赖默认内存存储 - 失败判定别只看 HTTP 状态码;要捕获
curl_error、curl_getinfo($ch, CURLINFO_HTTP_CODE) === 0(连接失败) -
failureThreshold建议设为 5~10,太小易误熔,太大起不到保护作用
降级逻辑里怎么安全读缓存,避免缓存穿透和脏数据?
降级不是简单 getCache($key) 就完事。后端挂了,缓存过期或压根没值,这时候直接返回空或 500,用户照样卡住——这不是降级,是甩锅。
立即学习“PHP免费学习笔记(深入)”;
核心做法是「缓存兜底 + 过期时间分级」:主逻辑的缓存设短 TTL(比如 60s),降级专用缓存设长 TTL(比如 3600s),且只在熔断开启、上游失败时才读它。
示例场景:查商品详情,上游服务不可用,你得返回一个带基础字段(id、name、price)的简化版,而不是空数组。
- 降级缓存 key 要和主缓存 key 分离,例如加
_fallback后缀,避免互相覆盖 - 写降级缓存时,必须校验数据结构,比如
isset($data['id']) && is_numeric($data['price']),防止上游返回异常格式污染兜底数据 - 用
Redis的SETNX+EXPIRE组合写入,避免并发写入脏数据 - 如果连
Redis也挂了?那就只能返回预设的静态 fallback 数组,别再 try-catch 套娃
curl 调用下游时,超时和重试怎么配才不拖垮网关?
PHP 默认 curl 没超时限制,后端卡死,整个 PHP 进程就 hang 住,FPM worker 被占满,网关直接雪崩。
必须显式设 CURLOPT_TIMEOUT_MS(建议 800~1200ms)和 CURLOPT_CONNECTTIMEOUT_MS(建议 300ms)。别用 CURLOPT_TIMEOUT(秒级),精度不够,容易超时过长。
重试不能无脑套 for ($i = 0; $i :第一次失败可能是网络抖动,但第三次还在重试,熔断器可能已经打开,再发请求就是无效负载。
- 重试只在
CURLE_COULDNT_CONNECT或CURLE_OPERATION_TIMEDOUT时进行,HTTP 5xx 不重试(说明服务起来了但业务出错) - 重试间隔用指数退避,比如第 1 次等 100ms,第 2 次等 300ms,第 3 次等 900ms
- 重试前先检查熔断器状态:
if (!$breaker->canExecute()) { return getFallbackData(); } - 所有 curl 配置统一走封装函数,避免某处漏设
CURLOPT_TIMEOUT_MS
缓存失效后如何避免“击穿”,让降级数据平滑过渡?
这是最容易被忽略的一环:主缓存过期瞬间,大量请求涌向后端,还没来得及重建缓存,熔断器就被打爆,降级缓存又没更新,结果全量返回脏数据或空值。
解法不是加锁,而是「双缓存 + 异步刷新」:主缓存(product:123)快过期前 5 秒,启动一个低优先级异步任务(如 exec("nohup php refresh_cache.php 123 > /dev/null 2>&1 &"))去预热;同时降级缓存(product:123_fallback)延长保留,直到新主缓存写入成功。
- 异步刷新失败不报错、不阻塞主流程,最多打个日志
- 降级缓存的 TTL 必须大于主缓存 TTL,差值至少 30 秒,留出刷新窗口
- 刷新脚本里写缓存时,用
set($key, $data, ['nx', 'ex' => 3600])避免覆盖正在使用的有效主缓存 - 别依赖
pcntl_fork,FPM 下不可用;改用exec或消息队列(如 Redis List + worker)
APCu 可能失效,pcntl 直接报错,连 sleep() 都得掂量是不是阻塞协程。











