PHP无内置熔断降级机制,必须通过Redis等外部存储实现跨请求状态共享;每次new实例会导致状态隔离,FPM下static无效、Swoole中易状态串扰,故需统一使用Redis存储并配合Guzzle中间件集成。

PHP 本身不提供熔断降级能力,必须靠应用层封装实现;直接在控制器里写 try-catch + 计数器,状态无法跨请求共享,等于没做。
为什么不能每次 new 一个 CircuitBreaker 实例
常见错误是把熔断器塞进 Controller 构造函数或方法里,每次请求都新建实例。这样 $failureCount 和 $state 完全隔离,连续失败 10 次也永远不会触发 open 状态。
- FPM 场景下,每个 worker 进程独立内存,
static属性只在单进程内有效,仍无法全局共享 - Swoole 协程中,
new CircuitBreaker()更危险:多个协程共用同一对象时状态会串,不用又没状态 - 正确做法是使用外部存储——Redis 最稳妥,APCu 仅限单机且无持久性,不推荐用于生产
- Redis key 建议带服务标识和日期粒度,例如
circuit:payment_service:20260501,避免 key 冲突和过期混乱
Guzzle 中间件里怎么插熔断检查
别在每个 $client->get() 前手动判断 canCall(),容易漏、难维护。Guzzle 的 Middleware 是标准接入点,所有 HTTP 调用自动经过它。
- 中间件需在请求发出前调用
CircuitBreaker::canCall($service),返回false就直接抛CircuitBreakerOpenException - 成功回调里必须调
recordSuccess($service),否则半开状态永远卡住,不会自动恢复 - 失败回调要区分异常类型:
GuzzleHttp\Exception\ConnectException、GuzzleHttp\Exception\TimeoutException和 HTTP 5xx 必须计入失败;4xx(如404、400)一般不计,属于业务逻辑范畴 - 注意:Guzzle 默认不重试,熔断器内部也不该重试——那是重试器的事,熔断器只管“该不该发”
兜底数据结构为什么必须和正常响应完全一致
前端代码按约定结构解构 JSON,比如期待 data.user_id,你降级返回 {"code":-1,"msg":"服务不可用"},JS 一读 data 就是 undefined,直接报错。
- 兜底值不是“有响应就行”,而是字段名、嵌套层级、空值类型(
null还是"")、甚至布尔值格式都得对齐 - 从 Redis 读缓存兜底时,务必加
json_last_error() === JSON_ERROR_NONE校验,防止缓存损坏导致 JSON 解析失败 - 如果兜底来自 APCu 或本地文件,记得写入时打上
fallback:true标识,方便日志里快速区分是真实响应还是降级数据 - 别在 fallback 函数里写
return []或return false——上游foreach会崩,isset($res['data'])也会挂
最易被忽略的点是半开状态的探测控制:不加限流的话,一旦超时窗口结束,几十个并发请求同时涌入,可能瞬间打垮刚恢复的服务。必须用原子操作(如 Redis 的 GETSET)确保同一时间最多只放行 1 次试探请求。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











