在一次常规的业务监控中,订单管理后台突然出现页面响应时间从200毫秒上升到2秒以上的情况,同时数据库侧告警显示MySQL活跃连接数在很短时间内由常态的40左右飙升至130以上。起初容易怀疑是SQL慢查询或缓存失效,但检查慢日志并未发现明显问题。真正的原因隐藏在PHP的会话处理机制与Redis作为会话存储后端的交互方式之中。

PHP会话锁的工作机制及其在Redis后端下的表现
PHP在默认情况下使用文件存储会话,并且为了保障同一个用户并发请求时会话数据的一致性,会在脚本启动阶段调用session_start()时对当前会话加锁。这个锁在脚本执行结束或显式调用session_write_close()之前一直保持。当会话存储从文件改为Redis之后,PHP的phpredis或predis扩展依然沿用了“先取锁、再读写、最后释放”的语义,只不过锁的实现变成了针对Redis中特定key的占用或事务控制。
问题在于,当页面中存在较多耗时操作(例如远程API调用、大循环处理)且未提前关闭会话时,该用户的多个并发请求(比如浏览器同时请求列表页与详情接口)会串行等待同一个Redis会话锁。此时PHP进程被阻塞在Redis的读取或写入调用上,无法释放给PHP-FPM进程池。下面的代码展示了一个容易引发阻塞的典型写法:
<?php
session_start(); // 加Redis会话锁
$user_id = $_SESSION['uid'];
// 模拟耗时业务,未提前关闭会话
sleep(3);
$db = new PDO('mysql:host=127.0.0.1;dbname=test', 'u', 'p');
$stmt = $db->query("SELECT * FROM orders WHERE uid=$user_id");
$data = $stmt->fetchAll();
echo json_encode($data);
// 脚本结束才释放锁
?>
上述代码中,sleep(3)期间会话锁始终未释放。如果同一用户此时再打开另一个标签页请求其他接口,第二个PHP进程也会卡在session_start()处等待Redis响应。从系统层面看,大量PHP进程堆积在Redis网络调用上,既占用了PHP-FPM子进程,也占用了到Redis的连接,而原本应当快速返回的页面出现了延迟。
会话锁阻塞如何传导为MySQL连接数突增
很多人疑惑:Redis的锁等待怎么会令MySQL连接变多?核心传导链路在于进程池饱和与请求重试。当PHP-FPM的空闲子进程被卡在会话锁等待上时,Web服务器(如Nginx)接收到的新请求无法立即被分配处理进程,于是可能在上游产生队列或触发客户端超时重试。运维为缓解503错误,往往调大PHP-FPM的pm.max_children或增加服务器节点,这就直接抬高了可同时连入MySQL的PHP进程上限。
另一方面,被阻塞的进程在恢复后通常会继续执行后续的数据库查询,而重试请求也会新建数据库连接。于是短时间内大量“迟到”的数据库调用同时打向MySQL,连接数曲线骤升。我们可以通过一个简单的监控对照表来理解这种放大效应:
| 阶段 | PHP-FPM占用 | Redis连接 | MySQL连接 | 页面延迟 |
|---|---|---|---|---|
| 正常 | 40 | 45 | 40 | 低 |
| 会话锁竞争 | 120 | 130 | 45 | 中 |
| 重试与扩容后 | 150 | 160 | 130 | 高 |
从表中可见,锁竞争初期MySQL连接尚未大涨,但一旦运维介入扩容或客户端重试,MySQL侧就会承受连接洪峰。因此根因并不是数据库,而是会话层串行化导致的进程占用放大。若直接在MySQL侧限流,反而会让页面错误率上升,必须从源头解开会话阻塞。
解除阻塞的三种工程化方案与落地代码
最直接的方法是尽早释放会话锁。对于只读会话数据且后续不再修改的页面,在读取完所需字段后立即调用session_write_close(),该函式会写入并释放Redis上的锁,使同用户其他请求得以并行。改造后的代码如下:
<?php
session_start();
$user_id = $_SESSION['uid'];
session_write_close(); // 立即释放Redis锁
// 后续耗时操作不再占用会话锁
sleep(3);
$db = new PDO('mysql:host=127.0.0.1;dbname=test', 'u', 'p');
$stmt = $db->query("SELECT * FROM orders WHERE uid=$user_id");
echo json_encode($stmt->fetchAll());
?>
第二种方案是针对完全不需要会话状态的接口(如部分JSON API)直接关闭会话模块。在PHP中可通过session_abort()或不调用session_start()来避免任何锁。若业务必须写会话,可采用异步写入:先将数据存储于本地数组,在脚本末尾或借助消息队列再写回Redis,从而降低关键路径上的阻塞时间。
第三种方案是连接隔离与超时控制。将Redis会话连接与业务缓存连接分池,并为会话锁设置较短的session.lock_wait_time(某些扩展支持)或Redis超时;同时为MySQL配置wait_timeout与连接池上限,避免被放大的PHP进程拖垮。综合使用这些方法后,此前提到的连接数突增与页面延迟问题通常能在一周内彻底消失,且不需要改动数据库结构。
排查此类问题的通用思路与监控建议
当遇到MySQL连接突增但慢查询稀少时,应当优先检查应用进程状态而非数据库本身。在Linux上可通过php-fpm的慢日志、strace跟踪系统调用,观察进程是否密集卡在recvfrom等Redis网络读取上。若发现大量futex或Redis往返延迟,即可锁定会话锁嫌疑。
监控层面建议对Redis的阻塞客户端数、PHP-FPM活跃进程数、MySQL连接数三者做关联面板。一旦Redis阻塞数上升而MySQL尚未波动,就提前告警,避免扩容误操作放大故障。长期看,把会话存储从强一致锁模型改为更轻量的无锁令牌模型,是从架构上根除该类问题的终极路径。
PHP_session_lockRedis_blockingMySQL_connection_spike修改时间:2026-08-16 09:12:34