Web提速:避免php session拖慢运行速度
一、WHAT--并发访问,阻塞执行
1.1 不使用session
文件index.php:
文件ajax.php、ajax2.php、ajax3.php的内容都是
每个请求都sleep 1s,jq的ajax请求是异步的,也就是说这三个请求基本上是同时发出的,理论上最好的情况是浏览器等待1s,3个接口全部返回。
访问http://localhost,在chrome下查看测试结果:
图 1.1 不使用session测试结果
测试结果基本上跟理论一致
1.2 使用session
现在我们把文件ajax.php、ajax2.php、ajax3.php都改为
这样做会什么有什么区别吗?我们直接看测试结果:
图 1.2 使用session测试结果
不是每个请求只执行1s钟吗?为什么ajax2.php消耗了2s,ajax3.php消耗3s?
二、WHO -- Session锁
session锁的官方定义:
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.
大致的意思是: 从调用session_start()开始,直到显示调用session_write_close()或脚本结束,session的数据都会被锁起来,届时同一个SESSIONID用户的请求会被阻塞。
从图1.2的测试结果看到,ajax2.php与ajax3.php分别多执行了1s和2s,很明显session_start()操作造成了阻塞。
当index.php同时访问ajax.php、ajax2.php、ajax3.php,ajax.php首先执行,此时ajax2.php与ajax3.php都在等在ajax.php释放session锁,都消耗1s。
当ajax.php执行完成,释放了session锁,ajax2.php与ajax3.php再次竞争session锁,同理ajax3.php又等待了1s钟。所以我们得到的结果:
ajax.php 消耗1s
ajax2.php 消耗2s
ajax3.php 消耗3s
三、WHEN -- 什么时候会触发Session锁
在章节二中,session锁定义为session_start()开始即会触发session锁,所以对于现有大部分php框架(使用原生php session的情况下)存在session锁造成的用户请求阻塞的问题。试想,有2个请求,其中请求A需要3s钟才能返回结果,另外请求B仅需要10ms即能返回,前端同时请求这两个接口,假如后台先处理了A请求,那么B请求就要等待3s后再执行10ms才能返回结果。但是最优的情况是,同时发起请求,10ms后收到B请求返回,3s后收到A请求返回。
四、WHY -- Session内部执行机制
默认情况下php session采用文件为服务端存储介质。在php session模块的源码中,有一个比较重要的结构体:
图4.1 php session模块结构体 ps_module_struct
该结构体里面的几个函数指针分别对应了session操作的open、close、read、write、destory、gc回收等功能。看到这6个函数,是否想起了php的SessionHandlerInterface接口(版本>=php5.4)?
图4.2 php SessionHandlerInterface接口
用这个接口配合session_set_save_handler可以重写session的这几个关键操作。于是我们可以使用以下代码来探探php session在一个请求的生命周期中的运行顺序:
图4.3 session运行顺序测试代码
测试结果:
由测试结果可知一般的session执行顺序,在session_start()调用时,php回去调用open和read操作,脚本执行结束后(输出了php script run),再会调用write和close操作。
现在我们不妨做一个大胆的猜想,php 在 session的open或者read操作时,开启了session锁,并write或close后释放session锁。这样的猜想也符合我们在章节1的测试结果。
为了验证我们的猜想,就需要去php的源码探个究竟了。在php源码文件/ ext / session / mod_files.c中可以看到默认session的6个重要操作的部分实现。在156行(点击打开链接),看到open操作有一个打开文件操作:flock(data->fd, LOCK_EX);该操作以互斥锁定的方式打开文件。在110行关掉文件close(data->fd);。
看到这里,我们应该得到的结论是:
在默认情况下,所谓的php session锁其实就是文件锁
所以,当我们使用session_set_save_handler来自定session操作,改用memcache或其他介质时,只要我们在SessionHandlerInterface的接口中没有锁的逻辑,那么session锁自然也不会存在。作者私下也做了这样的实验,实践证明也的确如此。
五、HOW -- 如何避免Session锁带来的阻塞现象
首先,session锁不一定是坏事情,在一种情况下就非常好用,例如某接口对与同一个用户的请求默认同一时刻只能执行一次。这种时候,就可以用seesion_start()和session_write_close()把要阻塞的代码括起来。非常简单暴力实用。
但是大部分时候我们还是要避免这种锁的存在,解决方案:
1、在用完session的时候就马上session_write_close()掉,释放session锁
2、采用没有锁的session操作,如章节4中所说的用session_set_save_handler来自定义一个没有锁的session操作。
3、再使用默认php session时,个人比较中意的一个方案:大部分情况下,我们对session的操作基本上都是读操作,写操作一般都比较少。这种时候,我们可以自己写一个session类。
构造函数:将session读入cache,关闭session锁
写操作:打开session锁,写入值,关闭session锁
读操作:直接读cache
部分代码如下:
//将session读入全局变量$_SEESIONstatic private function init(){if(self::$not_init){ session_start(); session_write_close(); self::$not_init = false;}}
//读sessionstatic public function get($name){self::init();return $_SESSION[$name];}
//写sessionstatic public function set($name, $val){session_start();$_SESSION[$name] = $val;session_write_close();}
注意:如果是写操作频繁的操作,就不适合使用该方法。