Hugh's Blog

PHP Session 锁阻塞请求问题

PHP Session 默认是用文件存储的,当 Session 初始化时,即调用 session_start() 或者 session.auto_start 设置为 true 时,PHP 会在 session.save_path 下创建一个文件(如果是新会话)并锁定,然后把 Cookie ID 发送给服务器,例如文件:/var/lib/php/session/sess_$identifier,此时,所有会话的数据都保存在该文件中。

锁机制是为了保护文件中的数据不被覆盖或者损毁,只有当前脚本执行结束或者主动结束,才能执行之后的脚本。

脚本正常结束没什么好说的,执行完之后自动释放文件,这里主要讲的是主动结束,PHP 提供了方法:session_write_close,根据官方文档的说法:

End the current session and store session data.
Session data is usually stored after your script terminated without the need to call session_write_close(), but as session data is locked to prevent concurrent writes only one script may operate on a session at any time. When using framesets together with sessions you will experience the frames loading one by one due to this locking. You can reduce the time needed to load all the frames by ending the session as soon as all changes to session variables are done.

上面除了说明锁定文件防止并发更改数据外,还说只要完成了对 Session 变量的操作,就可以通过该函数来结束会话写入。

例如在 PHP 中可以这样调用:

session_start();
// 更改变量
$_SESSION['foo'] = 'bar';
// Session 结束
session_write_close();
// 接下来即使在 $_SESSION 写入数据,也不会生效

另外在 PHP7 中调用 session_start 时也可以这样写:

session_start([
    'read_and_close' => true,
]);

这里 read_and_close 的作用是控制 PHP 只有在会话中的数据发生更改的时候才写入文件,如果没有任何改动,那么 PHP 读取完会话数据之后,立即关闭会话存储文件(即释放锁)。

之所以会碰到 Session 锁问题的原因是有一个页面 Ajax 长轮询的功能,为了减少客户端连接数,做法是在服务端中把单个连接挂起,等到有数据更新之后再返回或者超时结束,然后客户端根据结果或者超时错误再次进行连接。

下面是 PHP 服务端代码:

// 提前结束 Session 的操作,避免阻塞其他请求
session_start();
session_write_close();

$data = [];

// 设置 5 分钟超时
// 如果没有结束 Session 的话,同一会话下其他请求都要等待 5 分钟之后才能执行
set_time_limit(60 * 5);
while (true) {

    // 检查数据是否有更新
    if (hasNewData()) {
        $data = $newData;
        break;
    }
    
    // 4 秒
    usleep(4000000);
}

return $data;

听同事说也可以用多线程来解决请求阻塞的问题,下次有空再试试。


参考

PHP Session Locking: How To Prevent Sessions Blocking in PHP requests

PHP session锁:如何避免session阻塞PHP请求