Laravel 13.36:长驻队列 worker 的读为什么一直压在主流上
laravel/framework v13.36.0 于 2026-10-13 发布,要求 PHP ^8.3。这个版本里有一个和队列 worker 相关的改动:允许在任务之间清掉粘性(sticky)的数据库连接。
- PR:#61915 Allow queue workers to flush sticky connections between jobs(jackbayliss 提交,taylorotwell 于 2026-10-07 合入 13.x)
- Release:v13.36.0
现象:一个写任务,后面一万个读都走主库
读写分离的配置通常长这样:
'connections' => [
'mysql' => [
'read' => [
'host' => ['192.168.1.1'],
],
'write' => [
'host' => ['196.168.1.2'],
],
'sticky' => true,
'driver' => 'mysql',
// ...
],
],
打开 sticky 之后,写操作会把该连接实例的读钉在 write 连接上,避免刚写完立刻读到从库的旧数据。
问题出在队列 worker 的生命周期上。worker 是长驻进程,一个进程会连续 pop 成千上万个任务。只要其中有一个任务执行了写操作,这个进程持有的连接就一直是 sticky 状态,后续所有任务的读都落到主库——直到进程退出(下一次部署)才会重置。PR 描述里写得很直接:
当一个连接是 sticky 的,写入会把它上面的读钉在写连接上,所以对一个 worker 来说——只要它有一个任务写了,就可能意味着后面 10,000 个任务都在用写连接……或者直到我们下一次强制部署,而这实际上让我们失去了拥有从库的意义。
Web 请求里这个问题不明显:请求结束连接就销毁了。worker 不结束。
13.36 新增的开关
新增的是 Worker 上的一个静态属性,默认 false,需要手动打开:
// AppServiceProvider::boot()
use Illuminate\Queue\Worker;
public function boot(): void
{
Worker::$flushStickyConnections = true;
}
打开后,worker 每执行完一个任务就刷新一次粘性连接,把读重新交还给从库。PR 里提到这个行为倾向在 14.x 变成默认,13.x 保持 opt-in 是为了不引入 BC 破坏:
我觉得这个在 14.x 应该是默认的,只是不想在这里做 b/c……(总会有人在某处依赖这个行为)
打开之后的代价
读回到从库,就意味着会碰到复制延迟。上一个任务刚写的数据,下一个任务读的时候可能读不到——因为新任务走的是从库,而从库还没追上主库。
如果两个任务之间本来就有先后依赖,正确做法是 chain 或 batch 把它们绑在一起,而不是指望同一个 worker 进程恰好接住下一个任务、恰好复用同一条 sticky 连接。PR 原话:
如果任务依赖其他任务,它们应该是 chain 或者 batch,而不是悄悄依赖哪个 worker 进程碰巧 pop 到了它们。
换句话说,打开这个开关会把「隐式依赖 worker 进程复用连接」的实现暴露出来。
怎么判断自己要不要开
按顺序核对几件事:
- 是否配了读写分离。
read/write两个 host 都配了、sticky => true,才有这个问题;单库配置下开关没有实际意义。 - worker 是否长驻。默认的
queue:work就是长驻的,--memory给得越大、重启越少,sticky 状态持续得越久。queue:listen或--stop-when-empty影响较小。 - 观察主库负载。看主库 QPS、慢查询日志里是否有大量本应走从库的
SELECT,且来源是队列 worker 的连接。这类读通常不是业务必需的强一致读,只是被粘住了。
如果确实中了前两条、第三条也能对应上,打开开关;如果任务链条本身存在隐式顺序依赖,先把它们改成 chain / batch,再打开。
顺带一句:background 队列连接
同一版本里,框架自带的 config/queue.php 补上了 background 连接(#61917),应用骨架里早就有这个配置。在此之前,没有自己 queue 配置、或者用 php artisan config:publish queue 发布过旧版配置的应用,会报:
The [background] queue connection has not been configured
升级后如果需要这个连接,重新发布或手动补上对应配置即可。