最近在一个基于 Web 的 IM 项目中,我采用异步向服务器发起请求拉取最新的聊天内容,服务器端通过 PHP 处理拉取请求,拉取过程是用 10 次循环查询数据库是否有最新的聊天内容。如发现新内容,则立即向浏览器输出,并结束掉本次请求的进程。在这 10 次的循环中,每次查询数据库后,均通过 Sleep 函数让进程暂停 1 秒,那么这个 PHP 进程可能会在服务器端保持 10 秒。
在测试过程中,我发现当这个拉取请求运行期间,其他向服务器端 PHP 发起的请求,均受到影响,响应变的非常慢。
经过一系列的排查,问题始终得不到解决,但当把代码中涉及到 SESSION 的部分全部跳过时,情况发生了变化,所有 PHP 进程都恢复正常的响应速度了。由此,联想到问题可能出在了 SESSION 阻塞机制上了。
关于 PHP 的 SESSION 阻塞机制,我们要先了解其工作状态,先看如下代码:
<?php
// 第 1 次打印 SESSION 状态
echo 'Status(1):' . session_status() . '<br>'; // 1
// 开启 SESSION
session_start();
// 第 2 次打印 SESSION 状态
echo 'Status(2):' . session_status() . '<br>'; // 2
$_SESSION['test'] = 'hello world';
// 等价于 write and close
session_commit();
echo $_SESSION['test'] . '<br>';
// 第 3 次打印 SESSION 状态
echo 'Status(3):' . session_status() . '<br>'; // 1
?>
上述代码输出的结果如下:
Status(1):1
Status(2):2
hello world
Status(3):1
通过 session_status() 这个函数可以得到 SESSION 的状态,返回值含义如下:
0 - 会话是被禁用的。
1 - 会话是启用的,但不存在当前会话。
2 - 会话是启用的,而且存在当前会话。
当上边的代码中第一次通过 session_status() 函数获取 SESSION 状态时,返回值为1,代表当前 SESSION 功能是可用的,但还没有处于激活状态的会话。
用我们非常熟悉的 session_start() 函数开启会话后,再次用 session_status() 函数获取状态,发现返回值已经变为2,这说明当前已经有了激活状态的会话。
重点在 session_commit() 这个函数被执行后,再次获取状态,返回值又变为1。
PHP 的 session_start() 函数执行时相当于完成了会话的 open 和 read 两个步骤,而 session_commit() 执行时相当于进行了会话的 write 和 close 两个步骤,与 session_write_close() 函数作用是一致的。
回到最初遇到的问题上,当 PHP 的 SESSION 开启后,进程会对会话的临时文件加锁,以保证同一时刻此文件只被一个进程修改。此时,如果会话没有 close 而其他进程又开启了会话,后来的进程就会被 PHP 暂时阻塞,等待临时文件解锁。
接下来看两段代码:
a.php
<?php
session_start();
$_SESSION['t1'] = time();
sleep(10);
echo $_SESSION['t1'];
?>
b.php
<?php
session_start();
$_SESSION['t2'] = time();
echo $_SESSION['t1'];
?>
我们将上边两段代码分别保存为文件 a.php 和 b.php,首先运行 a.php,紧接着运行 b.php,我们发现在 a.php 没有结束还处于 sleep 状态时,b.php始终被阻塞在那里迟迟无法输出结果,原因就是上边我们分析的会话临时文件被加锁,后来的进程被暂时阻塞的问题。
为了解决这个问题,我们可以在进程进入 sleep 前,通过 session_commit() 函数将会话 close 掉,从而让当前进程解锁会话临时文件,以便让其他进程获得文件的锁。
修改后的 a.php 代码如下:
<?php
session_start();
$_SESSION['t1'] = time();
// write and close
session_commit();
sleep(10);
echo $_SESSION['t1'];
?>
按上边的代码修改 a.php 后,我们再次在浏览器中运行两个文件,a.php 在 sleep 状态下,b.php 已经可以很正常的运行了。