connection refused 错误本质是协程连接被提前释放,而非redis服务宕机;常见于pipeline/事务异常导致finally未执行释放、手动destroy上下文、跨协程未传递context等场景。

Connection refused 错误实际是协程连接被提前释放
看到 Connection refused 或 Redis server went away 不代表 Redis 服务挂了,大概率是当前协程拿到的连接已经被其他协程释放或归还。Hyperf 的连接池(如 Redis、MySQL)依赖协程上下文做连接绑定,一旦上下文被清理、覆盖或未正确传递,后续操作就会复用已失效的连接句柄。
- 典型触发场景:在
pipeline或transaction回调中抛出异常,finally块未执行到$this->releaseContextConnection() - 另一个高发点:手动调用
Context::destroy()清除了整个上下文,连带把连接 key(如redis.connection.123)一并删掉 - 注意
Context::set()和Context::get()默认操作的是当前协程 ID,但如果你显式传了$coroutineId参数,而该 ID 已退出,就可能读到空或脏数据
检查 Context 中是否残留连接引用
别只查日志,直接进协程运行时看连接状态。Hyperf 提供了 Context::has() 和 Context::get(),配合协程 ID 可快速验证连接是否还在上下文中:
// 在报错前插入诊断代码
$cid = Coroutine::id();
$key = $redis->getContextKey(); // 通常是 'redis.connection.' . $cid
if (Context::has($key)) {
$conn = Context::get($key);
var_dump('connection alive:', $conn instanceof \Swoole\Coroutine\Redis);
} else {
var_dump('connection missing in context', $cid, $key);
}
- 如果输出
connection missing in context,说明连接已被释放或从未绑定成功 - 如果连接对象存在但
isConnected()返回 false,说明连接已断开但未从上下文清理 - 不要依赖
isset($redis->connection),因为$redis实例是单例,它的属性在多协程下会被覆盖
管道/事务中异常导致连接泄漏的修复写法
Hyperf 的 pipeline() 方法内部有 try...finally,但它只在「无异常」路径下保证释放。一旦回调里 throw,finally 里的 releaseContextConnection() 就不会执行——这是最隐蔽的泄漏源头。
- 正确做法:自己包一层
defer,确保无论是否异常都清理 - 错误写法:
try { $redis->pipeline(...); } catch {}—— 连接已泄漏,catch 里补救无效 - 推荐写法:用
Context::override()+defer显式控制生命周期
$cid = Coroutine::id();
$contextKey = 'redis.pipeline.' . $cid;
Context::set($contextKey, true);
defer(function () use ($contextKey) {
Context::destroy($contextKey);
});
$result = $redis->pipeline(function ($pipe) {
$pipe->set('a', '1');
$pipe->get('b');
// 即使这里 throw,defer 仍会执行
throw new RuntimeException('simulated error');
});
跨协程调用时 Context 传递失败的常见原因
父子协程之间不自动继承上下文。如果你用 Coroutine::create() 启动子协程,又没手动传递,子协程的 Context 是空的,所有 Context::get() 都会回退到默认值或 null。
- Hyperf 的
go()函数会自动克隆父协程上下文,但原生Coroutine::create()不会 - 自定义进程里启动协程,必须显式调用
Coroutine::getContextFor($parentCid)拷贝数据 - RPC 调用(如 Nacos Consumer)若用了静态客户端实例,其内部连接也依赖上下文,跨协程后容易复用旧连接
真正难排查的不是报错本身,而是连接状态和上下文生命周期之间的时序差——比如协程 A 刚释放连接,协程 B 立刻 get() 到一个已 close 的 socket 句柄,错误却报在 B 的第一次 write 操作上。这种延迟暴露的问题,必须靠实时上下文快照,而不是事后翻日志。











