关于session锁引起的接口请求响应慢的问题
问题描述
老贴,可以参考:访问
index.html 运行的时候会同时发送(并发)三个都非常耗时的异步sse请求:
index.html
|- A.php
|- B.php
|- C.php
ABC三个接口会在握手后,持续接收数据。其中ABC三个文件共同引用一个common.php,并且ABC三个接口都是使用session做用户登录状态以及信息的管理。如下:
common.php ->A
->B
->C
A:require('common.php')
B:require('common.php')
C:require('common.php')
common.php中:
session_start();
/** 一系列从数据库 读取用户数据操作 */
$_SESSION['id'] = 'xxx';
在并发的情况下,我发现三个请求都非常的慢!经过排查发现问题原因如下:
当使用PHP的默认session处理机制时,session数据通常存储在服务器的文件系统中。当一个请求开始访问session(通过 session_start() )时,PHP会对该session文件加锁,直到请求结束或显式调用 session_write_close() 。
在你的场景中:
- 1. A请求开始,调用 session_start() ,获取session文件锁
- 2. A的SSE连接建立,保持打开状态(长连接)
- 3. 由于SSE连接未关闭,session文件锁未释放
- 4. B请求开始,尝试调用 session_start() ,但必须等待session文件锁释放
- 5. B请求被阻塞,直到A的SSE连接关闭或超时
- 这就解释了为什么后续请求会很慢或失败 - 它们在等待前一个请求释放session锁。
解决方案
在后端修改session处理方式 :
- 在验证完用户身份后立即调用 session_write_close() 释放锁
- 使用无锁的session存储方式(如Redis、Memcached等)
- 对于SSE连接,可以在建立连接前验证用户身份,然后关闭session
前端修改请求方式 :
- 串行化请求,等待前一个请求完全结束(包括SSE关闭)后再发起下一个请求
- 使用token而非session进行身份验证
前端修改这里就不多废话了,着重讲一下后端的修改session_write_close()。
session_write_close
在 PHP 中,session 文件锁是由 PHP 的 session 处理机制自动管理的。当你使用 session_start() 开始一个会话时,PHP 会自动锁定对应的 session 文件,以防止并发访问导致的数据不一致问题。要解除 session 文件锁,你可以使用以下几种方法:
使用 session_write_close() 函数:
记住一定要是在哪个文件中开启的session_start(),就要在哪个文件中执行session_write_close(),对文件锁进行释放!!!不能在common.php中开启session_start(),然后在A.php中执行session_write_close(),这样可能会导致无效果!
<?php
session_start();
// 进行一些 session 操作
$_SESSION['user_id'] = 123;
// 完成操作后,写入并关闭 session,释放文件锁
session_write_close();
// 继续执行其他不需要修改 session 的代码
// ...
?>
使用 session_commit() 函数
(这是 session_write_close() 的别名):
<?php
session_start();
$_SESSION['user_id'] = 123;
// 释放 session 锁
session_commit();
// 继续执行其他代码
?>
在需要再次修改 session 时,可以重新启动 session
<?php
session_start();
$_SESSION['user_id'] = 123;
// 释放锁
session_write_close();
// 执行一些耗时操作
sleep(5);
// 需要再次修改 session 时,重新启动
session_start();
$_SESSION['last_activity'] = time();
session_write_close();
?>
这对于需要执行长时间运行的脚本特别有用,如果使用session进行验证的话,一定要重视这个问题!!并且一定要在耗时的操作开始前关闭session锁!!!可以避免长时间锁定 session 文件,从而提高并发性能。
注意:在使用 AJAX 或并发请求的应用中,及时释放 session 锁尤为重要,否则可能导致请求排队等待,影响用户体验。
close之后session数据依然有效
session_write_close() 执行后, $_SESSION 中的数据依然有效。这个函数只是释放了 session 文件锁,并将当前的 session 数据写入存储,但不会销毁或清除 session 数据。
<?php
session_start();
// 存储用户信息
$_SESSION['user_id'] = 123;
$_SESSION['username'] = 'test';
// 释放文件锁
session_write_close();
// 这里依然可以读取 session 数据
echo $_SESSION['user_id']; // 输出: 123
echo $_SESSION['username']; // 输出: test
// 如果需要修改 session 数据,需要重新启动 session
session_start();
$_SESSION['new_data'] = 'new value';
session_write_close();
?>
一些重要说明:
1. session_write_close() 后:
- 可以继续读取$_SESSION 中的数据
-不能修改 $_SESSION 中的数据,除非重新调用 session_start()
2.使用场景:
-在需要执行耗时操作前释放 session 锁
-在并发请求场景下提高性能
-在异步操作中避免 session 锁冲突
最佳实践:
<?php
session_start();
// 验证用户状态
$_SESSION['user_id'] = 123;
// 写入完成后立即释放锁
session_write_close();
// 执行一些耗时操作
sleep(5);
// 仍然可以读取数据
$user_id = $_SESSION['user_id'];
// 如果需要修改 session
session_start();
$_SESSION['new_data'] = 'value';
session_write_close();
?>
AI疑问解答
问题1
现在有个问题:
如果A与C都 引用了同一个文件common.php,common.php中:session_start() ,A与C如下:
// A.php
require_once 'common.php';
$_SESSION['id'] = 10;
// 在开始耗时操作前释放锁
session_write_close();
// 执行耗时的 SSE 请求...
// C.php
require_once 'common.php';
$id = $_SESSION['id'];
session_write_close();
回答:不会释放同一个 session 文件锁。
C 中执行的 session_write_close() 确实会有效,但它只会释放 C 这个脚本对 session 文件的锁定。
- PHP 的 session 锁是基于文件的,每个用户的 session 对应一个文件
- 当一个脚本调用 session_start() 时,会锁定该用户的 session 文件
- 不同的 PHP 脚本实例是独立的进程,各自管理自己的 session 锁
所以只要session是同一个用户,就会锁定。C中的 session_write_close() 只会释放该脚本对 session 文件的锁定,不会影响到 A 中的 session 锁。 A 会继续保持对 session 文件的锁定,直到它自己调用 session_write_close() 或脚本执行完毕。所以就是不要考虑在C中去解除A的锁,就算锁住的是同一个用户的,也不要在C中操作。
如果你希望在 A 中执行耗时操作前释放 session 锁,应该在A中的耗时操作前调用 session_write_close() :
// dify.php
session_start();
$_SESSION['id'] = 10;
// 在开始耗时操作前释放session锁
session_write_close();
// 然后执行耗时的SSE请求
// ...
如此,其他脚本(如 C )就可以在 dify.php 执行耗时操作期间访问和修改 session 数据,而不会被阻塞。
问题2
我发现当A执行 一直处于“待处理”的时候,然后我刷新页面,发现C也是会一直处于“待处理” 无法获取数据,必须要等一会(服务器设置一个请求最长等待3分钟)之后,突然C开始接收数据了。这是什么原因?
这种情况是因为 PHP 的 session 机制导致的。让我解释一下具体原因:
Session 文件锁机制:
- PHP 使用文件来存储 session 数据
- 当一个脚本访问 session 时,PHP 会锁定整个 session 文件
- 其他想要访问同一个 session 的请求必须等待锁释放
遇到的情况实际是:
A C
--------- --------------
session_start() 等待获取session锁...
执行耗时的SSE请求... 等待获取session锁...
(请求未完成,锁未释放) 等待获取session锁...
3分钟后请求超时 终于获得session锁
开始执行...
解决方案:在 dify.php 中,应该在开始 SSE 请求之前就释放 session 锁:
// dify.php
session_start();
// 读取需要的 session 数据
$user_id = $_SESSION['id'];
// 重要:在开始 SSE 请求前释放 session 锁
session_write_close();
// 然后再执行耗时的 SSE 请求
// ... SSE 请求代码 ...
- 释放锁后,其他脚本可以立即访问 session
- SSE 请求期间不需要修改 session,所以可以安全释放
- 避免阻塞其他需要访问 session 的请求
如果 SSE 请求过程中需要修改 session,可以临时重新开启:
session_start();
$_SESSION['new_data'] = 'value';
session_write_close();
确保在读取完所需的 session 数据后再释放锁。
问题3
你上一个回答告诉我说 :PHP 的 session 锁是基于文件的,每个用户的 session 对应一个文件,不同的 PHP 脚本实例是独立的进程,各自管理自己的 session 锁。现在我有点不懂了。 假设现在只有一个用户,不要考虑多个用户。他同时发送多个请求,A和B和C,如果A耗时长被锁住session,那么C就会受影响是吗?然后我如果在C中执行session_write_close()会解开session锁吗?
Session 锁的工作机制:
- 每个用户的 session 数据存储在一个文件中
- 同一个用户 的所有请求都会访问同一个 session 文件
- 当一个请求获得 session 锁时,其他请求必须等待锁释放
在你的场景中:
用户发起请求1 → A 用户发起请求2 → C
session_start() ✓获得锁 session_start() ❌等待锁
执行耗时的 SSE 请求... 继续等待...
继续执行... 继续等待...
所以:- 是的,如果 dify.php 持有 session 锁且耗时长,check_login.php 会被阻塞
- 在 C 中执行 session_write_close() 没有意义,因为它根本获取不到锁
- 正确的做法是在A中执行耗时操作前释放锁:
// dify.php
session_start();
$user_id = $_SESSION['id']; // 先读取需要的数据
session_write_close(); // 释放锁
// 执行耗时的 SSE 请求...
// check_login.php
session_start(); // 现在可以立即获得锁
$id = $_SESSION['id'];
session_write_close();
简单来说:
1. 同一个用户的所有请求共享同一个 session 文件和锁
2. 必须在持有锁的脚本中释放锁,其他脚本释放无效
3. 应该在执行耗时操作前释放锁,这样其他请求就不会被阻塞
问题4:exit会终止程序并自动释放session锁吗
比如有如下代码:
session_start()
if($one) {
if($two) {
...
session_write_close();
} else {
...
exit;
}
} else {
...
exit;
}
问题:我只有在$two这里释放了文件锁,没有在其他的else中 释放锁,然后在其他问题的时候 是直接执行了exit 请问执行了exit之后 PHP会自动释放锁吗?
回答:不会!
关于您的问题,我来解释一下:
PHP中的session锁和exit的关系:当PHP脚本执行到 exit 或 die 语句时,PHP会自动执行清理工作,包括关闭所有打开的资源,但不会自动调用 session_write_close() 。这意味着如果您在 exit 之前没有显式调用 session_write_close() ,那么session锁可能会一直保持到session垃圾回收机制触发(通常是session.gc_maxlifetime设置的时间)。
在您的代码中,我注意到只在$two有效的情况下调用了 session_write_close() ,而在其他分支中没有。这可能会导致session锁问题,特别是在高并发环境下。
建议:
if($one) {
if($two) {
...
session_write_close();
} else {
session_write_close();
...
exit;
}
} else {
session_write_close();
...
exit;
}
无论执行哪个分支,都会确保在返回响应前释放session锁,避免其他请求被阻塞。这对于提高并发性能和避免请求超时非常重要。