DB::transaction()本身不自动加锁,仅保证原子性;高并发下需显式用SELECT ... FOR UPDATE锁定行,并控制各环节超时(连接、查询、HTTP、队列),避免无声卡死。

并发请求本身不设超时,真正要控的是每个环节的等待上限:数据库连接、查询、HTTP 调用、队列任务执行——混用或漏配就会出现“卡住但不报错”的假稳定。
DB::transaction() 里查不到锁超时?其实是没显式加锁
很多人以为 DB::transaction() 自带行锁保护,其实它只保证原子提交,不自动加锁。两个请求同时走 $product = Product::find($id); $product->stock--; $product->save();,都会读到旧值再写,最终库存少扣一次。
- 必须在事务开头就用
SELECT ... FOR UPDATE占住行:DB::select('SELECT * FROM products WHERE id = ? FOR UPDATE', [$id]) - 别用
Product::lockForUpdate()->find($id),Eloquent 可能触发额外查询(比如访问器、关联加载),延长锁持有时间 - MySQL 默认
innodb_lock_wait_timeout = 50秒,超时会抛出SQLSTATE[HY000]: General error: 1205 Deadlock found,不是静默失败 - 如果业务允许无锁更新,优先用原子 SQL:
DB::update('UPDATE products SET stock = stock - 1 WHERE id = ? AND stock >= 1', [$id]),返回影响行数为 0 就说明库存不足
队列任务 timeout 和 retry_after 不匹配,任务会“消失”
设置 $timeout = 60 没用,如果 config/queue.php 里对应驱动的 retry_after 是 50,任务会在第 50 秒被队列驱动判定为“过期”,直接重投或丢进 failed_jobs——你根本等不到 60 秒超时。
-
retry_after必须 >$timeout,建议至少多 10 秒缓冲(如$timeout = 60→retry_after = 70) - database 驱动下,
retry_after控制的是jobs.reserved_at字段有效期;若queue:work进程崩溃,这个时间戳不会更新,任务就永远卡住 - 命令行启动时加
--timeout=60会覆盖任务类里的$timeout,但不会改retry_after,两者仍需手动对齐 - 别在
handle()里写set_time_limit(30),队列 worker 是常驻进程,这个调用基本无效
外部 HTTP 请求卡住,PHP 层超时拦不住
像 file_get_contents() 或没设超时的 curl_exec(),会一直挂起,$timeout 和 max_execution_time 都拦不住——因为它们发生在 PHP 扩展层,不走 PHP 的计时器。
- 用 Guzzle 时必须显式设
timeout和connect_timeout,例如:['timeout' => 10, 'connect_timeout' => 3] - OpenAI 客户端通过
config/openai.php的request_timeout控制,环境变量OPENAI_REQUEST_TIMEOUT=30才生效 - 别依赖
pcntl_alarm()做中断:生产环境大多禁用pcntl扩展,且信号回调不保证执行时机 - 如果必须调用不可控的第三方 SDK,把它包进独立子进程,用系统级
timeout命令兜底(但要注意事务回滚、Redis 锁释放等副作用)
高并发下 DB 连接池耗尽,错误信息藏得深
不是所有“超时”都报 TimeoutException。当 MySQL 连接数打满,新请求可能卡在 connecting 状态,Laravel 报的可能是模糊的 PDOException: SQLSTATE[HY000] [2002] Connection refused 或直接 500,日志里却看不到明确超时源头。
- 在
config/database.php的 MySQL 配置里加'options' => [PDO::ATTR_TIMEOUT => 5],控制 PDO 连接建立超时(单位秒) - 同时配
'connect_timeout' => 3(MySQLi 参数,部分驱动支持),缩短建连失败判定时间 - 监控
show status like 'Threads_connected',对比max_connections,确认是否真被池子卡住 - 别在循环里反复调
DB::connection()->getPdo(),连接对象复用比新建更安全
最易被忽略的一点:超时配置是分层的,DB 连接超时、查询超时、HTTP 超时、队列任务超时、PHP 脚本超时——它们互不替代,漏掉任何一层,都可能让请求在某处无声卡死。











