公告
记得每天来更新数据哦,更新站点排名靠前。
本店目前百度权重2;谷歌权重2;根据中国站长数据为准;加入VIP快审自助服务 15元/站,软文快审自助服务10元/篇;
截至今日,处置违规站点790站,文章违规1147条,会员违规50位。

PHP的session文件锁让同一会话的请求排队,并发变串行,页面从400毫秒变2.4秒

2026-11-03 09:14 1 次浏览 来源:A5站长

一个后台页面同时发6个接口,逐个测都是400毫秒,一起发完页面转了2.4秒。接口之间没有依赖,数据库也没压力。6个接口挨个点都在半秒内返回,凑在一起才变慢。PHP默认把会话数据写在文件里,session.save_handler是files,目录由session.save_path指定,常见的位置是

一个后台页面同时发6个接口,逐个测都是400毫秒,一起发完页面转了2.4秒。接口之间没有依赖,数据库也没压力。6个接口挨个点都在半秒内返回,凑在一起才变慢。

PHP默认把会话数据写在文件里,session.save_handler是files,目录由session.save_path指定,常见的位置是/var/lib/php/sessions。session_start()打开文件时会加一把排他锁,同一条会话的第二个请求要等第一个跑完才拿得到。锁一直持有到脚本结束或者显式关闭,这段时间里同会话的请求全部排队,跟接口本身的快慢无关。锁是flock加的,用lsof能看到会话文件上挂着锁标记。请求拿不到锁就阻塞在打开文件那一步,PHP不报错,只是停在那里等。默认的文件处理器没有超时设置,等多久取决于前一个请求跑多久。

浏览器这边只有一条PHPSESSID,6个请求带的都是同一个cookie。服务端看它们属于同一条会话,就去抢同一个文件。串行不是PHP在排队,是文件锁的副作用。同一条会话在服务端就是一个文件,文件名是sess_前缀接会话ID。

你要确认,就看时间叠加。开两个终端,带同一个cookie同时打一个sleep(2)的测试页,第二个的total会接近4秒而不是2秒。用curl的-w看time_total,2次差多少就是等锁的时间。会话文件在目录里是sess_开头加一串字符,ls -l看修改时间也能看出有没有被反复打开。用ab或者wrk打并发反而不明显,它们默认不共享cookie,每个请求都是新会话。

最省事的改法是早点放锁。数据读完就调session_write_close(),后面的渲染、调用其他接口、生成报表都不再占着文件。购物车这类中途要改会话的,改完立刻关,别留到脚本末尾。这一步不改任何配置,改完并发就回来了。异常分支也放锁,比散在各处可靠。

只读接口用session_start(['read_and_close'=>true]),拿到数据就释放,连写回都省了。这一行放在入口文件里,比在每个方法里手工关要稳,漏掉一处就前功尽弃。这个选项PHP 7.0起才有,更早的版本仍要手工关闭。

会话改存Redis能绕开文件锁,php-redis的会话处理器默认不加锁,并发请求不再互相等。代价是多一个依赖,Redis挂了登录态全掉,得配持久化和主从。另一种做法是把登录态换成签名token,服务端不存会话,横向扩容省事,改造成本要看现有代码怎么写的。Redis这一层还要看maxmemory配了多少,会话被淘汰等于用户被登出。

改完再压一次,6个接口的total从2.4秒降到400毫秒上下,跟单个请求差不多。同一会话并发这条,在后台管理页和移动端首屏上最容易撞到。改完还慢的话,回去看接口里有没有第二次session_start。