高并发下laravel查询变慢主因是eloquent默认行为放大压力:连接复用失效、n+1查询、索引未生效、缓存击穿;须从连接层→查询层→缓存层逐级优化,而非仅加with()或redis。

高并发下 Laravel 数据库查询变慢,八成不是数据库扛不住,而是 Eloquent 默认行为在放大压力:连接复用失效、N+1 爆发、索引没被真正用上、缓存键冲突或击穿。必须从连接层 → 查询层 → 缓存层逐级加固,不能只调 with() 或加 Redis 就完事。
DB 连接池失效?先确认 PDO 实例是否真复用
Laravel 没有真正的连接池,靠 PHP-FPM 进程内 PDO 复用。若复用失败,每个请求都新建连接,高并发时 MySQL 连接数瞬间打满,报错 Too many connections。
- 检查
config/database.php中 MySQL 配置的'options' => [PDO::ATTR_PERSISTENT => false]—— 必须显式设为false,持久连接在 Laravel 下反而导致状态污染和泄漏 - 在控制器里加
dd(DB::connection()->getPdo() === DB::connection()->getPdo()),返回true才说明同一请求内 PDO 实例被复用 - 确认
php.ini中mysql.allow_persistent = Off,否则 FPM 重启后旧连接残留,新请求无法复用
EXPLAIN 不跑一遍,别信“加了索引就快”
高并发场景下,哪怕一条慢查询也会拖垮整条链路。但 toSql() 输出的 SQL 和真实执行的往往不一致(绑定参数缺失),直接看 EXPLAIN FORMAT=JSON 才是唯一可信依据。
- 对任意慢查询,在 Tinker 或 MySQL 客户端执行
EXPLAIN FORMAT=JSON SELECT ...,重点盯"type": "ALL"(全表扫描)、"key": null(索引未命中)、"rows": 500000(扫描行数远超结果) - 复合查询如
where status = ? and created_at > ?,必须建联合索引(status, created_at),反过来建(created_at, status)只能用上第一个字段 - 软删除字段
deleted_at出现在 WHERE 中?立刻加覆盖索引(deleted_at, id),否则WHERE deleted_at IS NULL一样全表扫
remember() 缓存不是开关,是精细阀门
高并发下缓存击穿或雪崩比慢查询更致命:Cache::remember('users', 3600, fn() => User::get()) 这种宽泛键名,一旦过期,所有请求同时穿透到 DB,瞬间压垮连接池。
- 缓存键必须带业务维度,例如
'active_users_v2'、'posts_by_status_published_202605',避免全局锁竞争 - 读多写少数据才缓存;用户会话类、实时订单状态类,缓存反而引入不一致风险
- 慎用
rememberForever(),它不支持自动失效,线上改数据后必须手动Cache::forget() - Redis 内存不足时,Laravel 默认用 LRU 踢数据 —— 若缓存键无业务前缀,可能把关键数据踢掉,建议统一加命名空间如
cache:users:active
分页和 chunkById() 别在事务里硬刚
高并发列表页或后台任务中,paginate() 的 COUNT(*) 和 chunk() 的 BETWEEN 在大数据量下会成为瓶颈,且事务内使用 chunkById() 会导致死锁。
- 百万级数据分页,禁用
paginate(),改用cursorPaginate()或游标分页(where id > ?+limit),跳过COUNT(*)开销 -
chunkById(1000)确实比chunk()抗删,但它依赖多次独立查询推进 —— 若包裹在 DB 事务里,前一批结果会被锁住,后续查询阻塞,直接卡死 - 导出类任务需分批处理时,确保外层无事务,或改用基于时间戳的分段:
where created_at between ? and ?,并给(deleted_at, created_at)加覆盖索引
真正卡住高并发的,往往不是某一行代码,而是连接复用断了、索引建反了、缓存键撞了、分页算总条数了——这些点单看都不起眼,但并发一上来就连锁崩塌。动手前,先跑一遍 EXPLAIN,再查一次 DB::connection()->getPdo(),比调十次缓存更管用。











