降级是当curl_exec卡住、Redis::get报错、PDO::query超时等故障发生时,在用户感知前切换至隔离的兜底逻辑,需严格控制超时、捕获异常并确保兜底数据零依赖、结构一致、业务合规。

降级不是“加个 if 返回空数组”,而是当 curl_exec 卡住、Redis::get 报 Connection refused、PDO::query 超过 200ms 还没返回时,你能否在用户感知前切到可靠兜底逻辑——这取决于超时控制、异常捕获和兜底数据来源是否真正隔离。
必须设 CURLOPT_TIMEOUT_MS,别只用 CURLOPT_TIMEOUT
PHP cURL 默认无超时,一旦下游网络抖动或服务假死,curl_exec 可能卡住 30 秒甚至更久,直接拖垮 FPM worker。秒级超时(CURLOPT_TIMEOUT)精度太粗,无法满足 P95 ≤ 800ms 的常见 SLA。
-
CURLOPT_TIMEOUT_MS设为800(单位毫秒),强制请求在 800ms 内结束,无论连接中还是读取中 - 同时配
CURLOPT_CONNECTTIMEOUT_MS(建议300),避免 DNS 解析或 TCP 握手阶段无限等待 - 务必关闭
CURLOPT_FAILONERROR:否则 4xx/5xx 响应会直接让curl_exec返回false,掩盖真实 HTTP 状态码 - 调用后必须检查
curl_errno($ch),而非只看返回值是否为false;curl_errno为28(CURLE_OPERATION_TIMEDOUT)或7(CURLE_COULDNT_CONNECT)才代表真失败
兜底数据不能从故障源读,得预加载或本地化
Redis 挂了,你还去 $redis->get('fallback:user')?那只是把单点故障扩散成雪崩。兜底路径必须零依赖网络、零阻塞 I/O。
- 启动时用
apcu_store('fallback_user', $data, 3600)预载,降级时直接apcu_fetch('fallback_user') - 若 APCu 不可用(如 CLI 模式或 opcache 关闭),改用
file_get_contents('/tmp/fallback_user.json'),但文件必须由部署脚本生成,运行时禁止fopen写入 - 硬编码兜底值仅限极低变场景,例如
['status' => 'maintenance'],且需带明确注释说明“此值不可热更新” - 别用
$_ENV或getenv()读兜底——环境变量可能为空、未加载,或被容器覆盖
用 php-circuit-breaker 管理状态,FPM 下必须配 Redis 存储
自己写计数器 + 时间戳,在 PHP-FPM 多进程下等于每个请求都新建熔断器,状态完全不共享。FPM 子进程重启后,APCu 数据全丢,static 变量也无效。
立即学习“PHP免费学习笔记(深入)”;
- 装
php-circuit-breaker:^2.0,初始化时显式传new RedisStorage($redis),别依赖默认内存存储 - failureThreshold 设为
5,timeout 设为60秒(即熔断后 60 秒自动进半开) - 失败判定要包含:
GuzzleHttp\Exception\ConnectException、RedisException、PDOException,以及curl_errno非零值 - 半开状态下,只允许每 30 秒放行 1 次探测请求;成功则闭合,失败则重置倒计时并延长熔断时间
降级响应结构必须和正常响应完全一致
前端代码假设 response.data.items 是数组,你降级返回 ['code' => 503],foreach 就直接报 Invalid argument。契约破坏比服务不可用更致命。
- 兜底数组字段名、嵌套层级、空值类型(
null还是'')必须和主逻辑输出一模一样 - 用
json_last_error() === JSON_ERROR_NONE校验兜底数据是否可 JSON 编码,避免缓存损坏导致格式错乱 - 若主逻辑返回
["items" => []],降级也必须返回该结构,哪怕 items 是空数组;不要省略字段或改名 - 测试时别只看 HTTP 状态码,用
json_decode($response, true)后检查 key 是否存在、类型是否匹配
最易被忽略的点是:兜底数据的“新鲜度”和“一致性”不在同一维度。你可以返回旧价格,但不能返回负价格;可以缓存用户昵称,但不能缓存已注销用户的 token。降级不是降低质量,而是守住底线——这个底线由业务规则定义,不是技术开关能自动判断的。



















