火柴人武林大会
156.74M · 2026-02-04
如果在 Laravel 的错误日志里看到这就话,大概率会让后端兄弟们血压升高:
最近我在维护一个高并发项目时就撞上了这个鬼东西。环境是 Laravel + Supervisor (多进程队列) 。
最诡异的是,报错的那行代码看起来完全无辜,而真正的凶手却躲在一个为了“优化性能”而写的底层方法里。今天把整个排查过程记录下来,希望能帮大家避坑。
报错的代码是一段再普通不过的查询
$sql = "SELECT * FROM `{$tableName}` WHERE `companyId` = ? ...";
// 报错就发生在这里
$results = DB::connection('xxx')
->select($sql, [$companyId, $inspectionSn, $toWarehouseId]);
报错提示非常明确:当前连接上有未完成的查询(Unbuffered queries active),无法执行新查询。
这就好比你去银行柜台办事,柜员告诉你:“上一位客户的业务还没办完,数据还在传,你先等着。”但问题是,代码逻辑里并没有明显的“上一位客户”。
根据经验,这个错误通常有三个嫌疑人:
cursor() 或 chunk() :遍历中途 break 或抛出异常,导致 MySQL 还在吐数据,PHP 这边却断了。PDO::MYSQL_ATTR_USE_BUFFERED_QUERY 被设为了 false。我把代码翻了个底朝天:
->cursor(),没有相关业务代码。config/database.php,默认缓冲查询是开启的(Laravel 默认行为)。Model::where(...)->get(),这些底层都会自动处理缓冲。这就见鬼了。 代码看起来全是“良民”,为什么连接会被占用?
既然当前代码没问题,那肯定是在这个请求(或任务)执行之前,连接就已经脏了。
考虑到我们使用了 Supervisor 跑队列任务。在 daemon 模式下,PHP 进程是长驻的,一个进程会处理成千上万个 Job。
如果 Job A 把数据库连接搞脏了(比如改了设置、或者留下了未读完的数据),Job B 复用了同一个进程和同一个数据库连接,Job B 就会无辜躺枪。
我开始审查业务逻辑中那些“看起来有点骚操作”的地方,终于锁定了这一段自定义的分表查询逻辑:
// 业务代码调用
WarehouseModel::query($companyId)
->where(...)
->first();
点进去看 query 方法的实现:
public static function query($companyId) {
// 引起我警觉的是这一行
$instance = self::me();
$table = $instance->setSuffix($companyId);
$instance->setTable($table);
$instance->setConnection($conn);
return $instance->newQuery();
}
再看 self::me() 的实现,好家伙,一个手写的单例模式:
public static function me() {
$class = get_called_class();
// 如果缓存里有,就直接返回旧对象
if(! isset(self::$instanceMap[$class])) {
self::$instanceMap[$class] = new $class();
}
return self::$instanceMap[$class];
}
破案了!凶手就是这个“带状态的单例”!
在常规的 Web 请求(PHP-FPM)中,这个单例可能仅仅导致逻辑错误(A用户的请求改了表名,B用户进来复用了A的表名)。
但在 Supervisor (多进程长驻) 环境下,这简直是灾难:
Obj,把它的连接设置为了 Connection_A。Obj 依然活在内存里(因为是静态变量缓存的)。self::me(),拿到了同一个 Obj(也就是拿到了 Connection_A)。甚至不需要异常,光是状态的 “串台” 就足够致命。Laravel 底层会尝试重置连接,但我们在 Model 层强行复用旧对象,绕过了框架的保护机制。
很多时候我们写单例是为了“省内存”、“高性能”。但在 PHP 中,创建一个 Model 对象的开销极小(纳秒级),为了这点微不足道的性能去冒着“状态污染”的风险,绝对是得不偿失。
修复非常简单:把单例改成工厂模式。
修改 query 方法,直接 new static():
public static function query($companyId)
{
// 删掉单例调用
// $instance = self::me();
// 改为每次创建新对象
// 确保每个 Job、每个逻辑拿到的都是干干净净的实例
$instance = new static();
// ... 后续设置表名、连接的逻辑保持不变 ...
$instance->setTable(...);
return $instance->newQuery();
}
为什么用 new static() 而不是 new ModelName()?
new static() 是后期静态绑定,支持继承。如果你把这段代码复制到其他 Model 里,它会自动实例化当前的类,而不用改代码,更优雅。
改完上线后,General error: 2014 彻底消失,世界清静了。
几点血泪教训:
setXxx 操作)的单例。在常驻内存环境(Swoole/Octane/Supervisor)下,这通常是 Bug 之源。new。Laravel 里的 Model 设计就是随用随弃的,不要试图去缓存它。DB::reconnect() 强制重连,虽然暴力,但有效。希望这篇复盘能帮到同样在深夜看着日志发愁的你。