Session锁问题

对会话有一定了解的都知道,http协议本来是无状态的,为了保证用户登陆态,带出来了session会话的机制。

PHP中的会话

Session会话通常是保存在服务端的一个ticket票据,已证明用户是该会话的归属者,以PHP为例,通常我们可以在php.ini中设置会话的存储位置,亦或是自行定义一个会话的处理类作为会话的管理器,以下简单的介绍了PHP中的会话管理器以及相关函数。

Session管理器

  • session_set_save_hanler: 支持files、DB、Memcache、Redis等;
  • 会话管理器通常需要实现:
    • open(string $savePath, string $sessionName) : 会话打开的时候会被调用
    • write(string $sessionId, string $data) : 在会话保存数据时会调用 write 回调函数。
    • close: 在 write 回调函数调用之后调用,session_wrire_close()执行后也会被调用;
    • read(string $sessionId) : 如果会话中有数据,read 回调函数必须返回将会话数据编码(序列化)后的字符串。
    • destroy($sessionId) : 干掉会话,当session_destory()或者session_regenerate_id()执行时候生效。
    • gc($lifetime): 为了清理会话中的旧数据,PHP 会不时的调用垃圾收集回调函数。
    • create_sid() : 创建会话id。

相关的会话处理函数

结束当前会话以及会话存储: session_write_close/session_commit

默认情况下,无需调用该函数,因为在PHP执行完后会调用session_register_shutdown(),默认关闭会话函数就是session_write_close()

作为会话数据,在任何时间段内,仅允许一个脚本程序进行会话数据写入操作(考虑到数据一致性),此时其他脚本程序无法进行该会话数据操作,因为会话数据被lock住了(互斥访问),此时就出现了session锁问题了,即一个脚本执行完才,另一个脚本才被执行,即开头遇到的问题;

Session释放

  • 会话ID支持COOKIE或者URL传递
  • 清除会话
    • $_SESSION = [],重置当前整个会话的环境变量数据,
    • session_destory(),清理整个服务端内部存储的话数据;
    • 让客户端也做对应的Cookie清理,通过set_cookie()发送HTTP头信息给到客户端:set_cookie(session_name(), '' , time()-3600)

session锁模拟

通过a.php的page在处理当前会话,通过sleep(5)模拟一个长时间操作,比如mysql读写(正常情况不应该有这么久)

通过访问b.php,对统一会话进行读取操作,发现被阻塞,直至a.php完成才可以读取成功!

// a.php
<?php
session_start();
$_SESSION['count'] = isset($_SESSION['count']) ? ++ $_SESSION['count'] : 1;
printf('%s %s, %s',
    'Count:',
    $_SESSION['count'],
    sprintf('<a href="%s/%s">b.php</a>', dirname($_SERVER['REQUEST_URI']), 'b.php')
);

//释放session锁
//session_write_close(); 不开启的话,会导致导致程序不释放session锁,直至脚本执行结束
//模拟一个长时间操作
sleep(5);

// b.php
<?php
session_start();
$_SESSION['count'] = isset($_SESSION['count']) ? $_SESSION['count'] : 1;
echo $_SESSION['count'];

小结

出现Session锁的本质原因,还是由于脚本的执行时间过长,导致session锁长时间被占用无法被释放。比如,脚本中存在较长耗时的网络调用、大数据查询等,都会导致session锁被占用,若在会话锁未释放之前,存还有一些程序(比如ajax请求)也依赖于该会话开启,就会导致该脚本程序的阻塞,浏览器也一直处于pending状态。

针对session锁这块,可以考虑通过使用更快的读写缓存(比如Redis进行统一存储),避免使用文件存储会话信息;同时可以在会话数据设置完后或者信息读取后,主动调用session_write_close结束会话;

若以Redis作为会话存储管理,在高并发的网站上面,需要充分考虑到作为Redis的会话管理也可能成为瓶颈(频繁的Redis连接、以及读写操作)

另外,理论上是不应该存在同一用户在同一时刻进行高频的进行会话操作;