Laravel中NativeSession::startSession耗时过高问题及优化诉求
Laravel会话阻塞问题解决方案
问题根源分析
虽然用Redis做会话存储,但PHP原生session_start()底层处理时,会对Redis中的会话数据加读写锁——当一个请求开启会话并持有锁时,同用户的其他请求会等待锁释放,这就是偶尔请求慢到7秒的核心原因,哪怕是只读请求也会被长耗时请求阻塞。
具体解决方法
方案1:给只读请求单独做无锁会话处理
针对不需要修改会话的查询、展示类请求,跳过原生会话启动逻辑,直接无锁读取Redis会话数据,避免锁竞争。
- 给只读路由加标记中间件
// routes/web.php 或 routes/api.php Route::get('/articles', [ArticleController::class, 'list'])->middleware('session.readonly'); Route::get('/user/profile', [UserController::class, 'show'])->middleware('session.readonly');
- 创建
session.readonly中间件
<?php namespace App\Http\Middleware; use Closure; use Illuminate\Http\Request; class SessionReadonly { public function handle(Request $request, Closure $next) { // 若会话未启动,直接从Redis读取会话数据(无锁) if (session_status() === PHP_SESSION_NONE) { $sessionId = $request->cookie(config('session.cookie')); if ($sessionId) { // 拼接Redis中会话的键名,默认前缀是laravel: $sessionKey = 'laravel:' . $sessionId; $sessionData = app('redis')->get($sessionKey); if ($sessionData) { $_SESSION = unserialize($sessionData); } // 标记会话为已启动,避免后续触发session_start() session_status(PHP_SESSION_ACTIVE); } } return $next($request); } }
- 注册中间件
在app/Http/Kernel.php的$routeMiddleware数组中添加:
protected $routeMiddleware = [ // ...其他中间件 'session.readonly' => \App\Http\Middleware\SessionReadonly::class, ];
方案2:缩短会话锁超时时间
如果不想改代码,可以先调整会话锁的超时配置,避免长耗时请求长时间占用锁。
修改config/session.php中的lock_timeout参数:
'lock_timeout' => 10, // 默认120秒,改为10秒,超时后自动释放锁
方案3:自定义无锁Redis会话驱动
对Laravel默认的Redis会话驱动做改造,让只读请求跳过锁逻辑:
- 创建自定义会话驱动类
<?php namespace App\Session; use Illuminate\Session\RedisSessionHandler; class ReadonlyRedisSessionHandler extends RedisSessionHandler { protected $readonly = false; public function setReadonly(bool $readonly) { $this->readonly = $readonly; } // 只读请求跳过锁相关操作 public function lock($sessionId) { return $this->readonly ? true : parent::lock($sessionId); } public function unlock($sessionId) { return $this->readonly ? true : parent::unlock($sessionId); } // 只读请求禁止写入会话 public function write($sessionId, $data) { return $this->readonly ? true : parent::write($sessionId, $data); } }
- 在服务提供者中替换驱动
在App\Providers\AppServiceProvider的boot方法中添加:
public function boot() { $this->app->extend('session.handler', function ($handler, $app) { // 判断当前请求是否使用只读中间件 if ($app->request->route() && $app->request->route()->hasMiddleware('session.readonly')) { $readonlyHandler = new \App\Session\ReadonlyRedisSessionHandler( $app['redis'], config('session.lottery'), config('session.expire_on_close'), config('session.redis') ); $readonlyHandler->setReadonly(true); return $readonlyHandler; } return $handler; }); }
验证效果
实施后用New Relic监控NativeSession::startSession的耗时占比,同时测试并发场景:
- 发起一个长耗时的写请求(比如表单提交后处理10秒)
- 同时发送多个只读请求,观察是否不再阻塞,响应时间回到800ms以内
内容的提问来源于stack exchange,提问作者SteDS
相关产品推荐
相关产品推荐

