session_set_save_handler
我们知道在php.ini
中session.save_handler
可以设置session
的处理机制,默认为file
,也支持redis
,这只是从语言层面去支持常用的技术,但是总有人会去使用一些自己开发的闭源的工具,从程序设计上来讲应该给予一把钥匙可以使你灵活的自定义 session 处理机制,这就是 session_set_save_handler
函数。
函数 session_set_save_handler 可以设置用户自定义会话存储函数,允许开发者针对session操作的每一个步骤设置自定义的回调函数。将不会额外的创建 session 文件。
直接上代码:SessionService.php
<?php
class SessionService implements SessionHandlerInterface
{
public $redis;
public $sessionExpire = 30;
public $log = "/www/logs.log"; // 使用绝对路径,否则在 write 和 close 时会写入到别的文件去了。
public function __construct()
{
$this->redis = new \Redis();
$this->redis->connect('r-m5e0hhv80jlu18d3ccpd.redis.rds.aliyuncs.com', '6379');
$this->redis->auth("testRedis_7sqB81LoH");
$this->redis->select(9);
}
/**
* 打开会话时被调用
* @param $savePath 配置的 session.save_path
* @param $sessionName 配置的 session.name
* @return bool
*/
public function open($savePath, $sessionName)
{
file_put_contents($this->log, 'open' . PHP_EOL, FILE_APPEND);
return true;
}
/**
* 读取会话数据时被调用
* @param $sessionId
* @return bool|string
*/
public function read($sessionId)
{
file_put_contents($this->log, 'read' . PHP_EOL, FILE_APPEND);
$data = $this->redis->get($sessionId);
if ($data) {
return $data;
} else {
return '';
}
}
/**
* 写入会话数据时被调用
* @param $sessionId
* @param $data
* @return bool
*/
public function write($sessionId, $data)
{
file_put_contents($this->log, 'write' . PHP_EOL, FILE_APPEND);
if ($this->redis->setex($sessionId, $this->sessionExpire, $data)) {
return true;
} else {
return false;
}
}
/**
* 关闭会话时被调用
* @return bool
*/
public function close()
{
file_put_contents($this->log, 'close' . PHP_EOL, FILE_APPEND);
return true;
}
/**
* 销毁会话时被调用
* 当调用 session_destroy() 函数, 或者调用 session_regenerate_id() 函数并且设置 destroy 参数为 true 时, 会调用此回调函数
* @param $sessionId
* @return bool
*/
public function destroy($sessionId)
{
file_put_contents($this->log, 'destroy' . PHP_EOL, FILE_APPEND);
if ($this->redis->delete($sessionId)) {
return true;
} else {
return false;
}
}
/**
* php执行会话清理算法时会被调用,调用周期由 session.gc_probability 和 session.gc_divisor 参数控制
* @param $lifetime 配置的 session.gc_maxlifetime 秒
* @return bool
*/
public function gc($lifetime)
{
file_put_contents($this->log, 'gc' . PHP_EOL, FILE_APPEND);
// cleanup old sessions -- todo
return true;
}
}
session_set.php
<?php
$handler = new SessionService();
// PHP > 5.4
session_set_save_handler($handler);
session_start();
$_SESSION['username'] = 'raoxiao';
echo $_SESSION['username'];
起初只能打印出 open, read, write, close
设置配置:
<?php
$handler = new SessionService();
// PHP > 5.4
session_set_save_handler($handler);
ini_set('session.gc_maxlifetime', 10);
ini_set('session.gc_probability ', 1);
ini_set('session.gc_divisor', 5);
session_start();
$_SESSION['username'] = 'raoxiao';
echo $_SESSION['username'];
可以打印出 open, read, write, close, gc
代码中并没使用序列化反序列化操作,这是因为PHP会自动调用 session_encode() 和 session_decode()
方法处理,他们是不同与 serialize
的序列化方法。
session 运行流程
实际上,session 运行流程为:
- open
- read
- 触发概率性gc
- 脚本运行完毕
- 收集 $_SESSION
- 关闭脚本,关闭输出流
- 内核执行write
- 内核执行close
所以内核将 session 信息读取到全局变量$_SESSION
,然后我们来操作变量$_SESSION
实现增删改查,最终在脚本执行完毕后内核又将 $_SESSION
内的数据重新序列化后写入文件,而不管 session 内容是否有变化,于是 session 文件的修改时间就发生变化,我们设置的 session 过期时间对比的是 session 文件的最后修改时间。
其实,$_SESSION['username'] = 'rao';
只是往一个全局变量里面写入值,echo $_SESSION['username'];
只是读取变量的值,都跟 session 操作无关。
PHP 会在脚本执行完毕或调用 session_write_close() 函数之后调用 write
。 注意,在调用完此回调函数之后,PHP 内部会调用 close
回调函数。PHP 会在输出流写入完毕并且关闭之后 才调用 write
回调函数, 所以在 write
回调函数中的调试信息不会输出到浏览器中。 如果需要在 write
回调函数中使用调试输出, 建议将调试输出写入到文件。
有流程可看出,gc 是在请求到来时触发的,在 file 模式下会存在性能问题。
session 并发问题
既然 session 是基于文件的,那么如何解决文件的并发写问题呢?在函数 session_write_close 的解释中已经说明了这一点,php使用的加锁的方式来确保并发安全的。
test_session.php
<?php
session_start();
$_SESSION["name"] = "raoxiaoya";
echo $_SESSION["name"], PHP_EOL;
sleep(20);
test_session_2.php
<?php
session_start();
$_SESSION["name"] = "raoxiaoya123";
echo $_SESSION["name"], PHP_EOL;
先访问 test_session.php
再访问test_session_2.php
,发现两个都阻塞了,在 web 项目中会存在这种情况,因为浏览器是并行的发出请求的,当开启了 session ,且同时有多个请求时,你会发现后面的请求要等到前面的请求结束后才会返回,并不像我们知道的那样(响应时间短的先返回)。
解决这种情况的方法是使用函数session_write_close
提前写入文件结束会话,当然前提是后面不再需要操作 session 否则会出现 session 信息混乱的问题。修改test_session.php
<?php
session_start();
$_SESSION["name"] = "raoxiaoya";
echo $_SESSION["name"], PHP_EOL;
session_write_close();
sleep(20);
如何精确的控制会话时间
从上面的知识点我们知道 session 本身的机制是做不到精确的,我们可以使用 redis 作为存储介质使其自动过期,或者在 session 中添加 expire 信息,每次读取的时候做个判断。