熔断状态重启丢失是因为ThinkPHP默认用内存或文件存储,进程重启即清空;应使用Redis持久化,通过原子操作在recordFailure、canCall等关键节点同步状态,确保多进程/Swoole环境下数据一致。

熔断状态为什么重启就丢了
ThinkPHP 默认用内存(array 或 Cache::store('file'))存熔断器状态,进程一重启,内存清空,文件缓存若没配好路径或权限,也可能读不到旧状态。这不是 ThinkPHP 的 bug,是熔断器(比如 Resilience4j 风格实现)默认设计就不考虑跨进程持久——它假设你用的是长生命周期服务(如 Swoole),而 TP 传统 FPM 模式下每次请求都是新进程。
用 Redis 存熔断状态最稳妥
必须把状态落地到外部存储,Redis 是首选:速度快、支持过期、天然支持并发读写。别用数据库,写太重;也别用本地文件,FPM 下多 worker 容易写冲突或读脏。
-
Redis::set($key, json_encode($state), 'EX', $ttl)写入时显式设 TTL,避免状态永久滞留 - 键名建议带环境前缀,比如
tp:circuit:api_v1_user_info,防止测试/生产混用 - 读取后要校验
json_decode是否失败,空值或解码错误得兜底初始化为关闭态("state": "CLOSED") - TP 8+ 可直接用
think-redis扩展;TP 6 若用cache.redis配置,注意确认cache组件底层是否真走 Redis(有些项目 alias 错了,实际还在用 file)
自定义熔断器类里怎么插 Redis 读写逻辑
别魔改框架源码,自己写个继承 CircuitBreaker 的子类,在 recordFailure()、recordSuccess()、canCall() 里加 Redis 读写。关键不是“能不能”,而是“什么时候读、什么时候写”:
- 每次
canCall()前先从 Redis 读一次状态,而不是只在构造时读——否则其他进程改了状态你不知道 -
recordFailure()后立即写 Redis,但要用 Lua 脚本或原子操作更新失败计数+时间戳,避免并发写覆盖(比如两个请求同时失败,只该 +1 却变成 1) - 状态切换(CLOSED → OPEN)必须写 Redis,且带上切换时间戳,否则恢复窗口(
waitDurationInOpenState)没法算准 - 示例片段:
Redis::eval("if redis.call('hget', KEYS[1], 'state') == 'OPEN' then redis.call('hset', KEYS[1], 'last_transition_time', ARGV[1]) end", 1, $key, time());
TP 6/8 在 Swoole 下的特殊注意点
Swoole 进程常驻,看似不用 Redis 也能保状态,但别信这个直觉:
- 如果用了多 Worker(
swoole.worker_num > 1),每个 Worker 还是独立内存,A Worker 熔断了,B Worker 不知道 - 热更新或 reload 会重启所有 Worker,内存照样丢
- 所以哪怕上 Swoole,Redis 仍是唯一靠谱方案;只是可适当调大 Redis TTL(比如 24 小时),减少无效读
- 别依赖
Runtime::getAppPath()存本地状态文件——Swoole 下 Runtime 目录可能被多个 Worker 并发写崩
熔断状态持久化真正的复杂点不在代码怎么写,而在「什么时候以什么精度去同步」。一次失败要不要立刻落库?OPEN 态持续多久才允许 HALF_OPEN?这些策略一旦和 Redis 读写时机耦合错,就会出现“明明熔断了却还在打下游”或者“早该恢复了却卡死在 OPEN”。别省那几毫秒网络开销。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!










