session是用来在服务端保存用户会话的,通常是用户登录成功之后,记录用户的会话信息,比如用户的ID和用户的openid等数据。然后用户在下次进行访问的时候,通过session_id就知道本次访问的用户对应的信息是什么。
php默认有session保存的机制,但是session保存是使用文件缓存的,我们不希望使用这种方式的话,我们就需要对session的保存方式进行重写。需要使用到PHP给我们提供的一些功能。下面就是介绍这个功能怎么使用的一个文档。
问与答
- session是什么时候被开启的
当PHP-fpm在处理请求的时候就会判断是否需要自动开启session。这是在php.ini文件决定的。但是如果设置的是不自动开启,那就需要在代码里面决定什么时候开启session。
- 每条请求都需要维护session吗?
就只有开启了session才需要维护session。如果我们要手动开启session的话需要执行session_start()函数
- session数据是保存在哪里的
在PHP的配置文件里面有一个默认的保存路劲,我们可以配置一个自己定义的保存路劲。默认都是以文件格式进行保存的。且保存的数据都是被序列化保存进去的,PHP会自动保存,我们不用太关心里面的内容长什么样。因为你也看不懂。
- 如果我们不想session保存在文件里面,我们想保存在Redis里面应该怎么做
这个问题就是我们今天要讨论的主要内容了。在下面会详细回答。这里贴一个参考文档
自定义session保存方式
应用场景和问题解析
我们通常有这样的需求,就是我们线上的服务器是做了负载均衡的,用户的请求是平均分配到每一台处理服务器上面去。在这种模式的情况下,就有可能出现,用户的请求可能一会是A服务器处理的,一会是B服务器处理的。如果在这种模式下我们还采用PHP自带的session的话,就会出现一个问题就是用户的信息记录在了A服务器的session里面。但是请求被分发到B服务器上面去的时候却没有这个用户的信息。(负载均衡的模式有很多种,也可以避免这种问题,但是这不是我们今天讨论的要点)。遇到这种情况,我们有几种解决方案。
第一种就是,不使用session了。就没有这种困惑了。
第二种是,把session保存到统一一个地方去,就比如我们的Redis里面。选择Redis的原因,也是因为他是内存级别的,他的速度很快,而且很适用于我们session这种场景。
实现方案
其实PHP给我们提供了一个函数session_set_save_handler
,这个函数是让我们写我们的session处理器,我们自己决定要怎么处理session的保存和读取还有过期时间等。然后PHP会来调用我们的这些处理器方法。
它的流程是这样的。
- 新建一个类实现
SessionHandlerInterface
接口。这个接口是里面有几个方法。将里面的几个方法实现了。然后等着PHP来调用就可以了。主要有一下几个
1 open(string $savePath, string $sessionName)open 回调函数类似于类的构造函数, 在会话打开的时候会被调用。 这是自动开始会话或者通过调用 session_start() 手动开始会话 之后第一个被调用的回调函数。 此回调函数操作成功返回
TRUE
,反之返回FALSE
。
1 close()close 回调函数类似于类的析构函数。 在 write 回调函数调用之后调用。 当调用 session_write_close() 函数之后,也会调用 close 回调函数。 此回调函数操作成功返回
TRUE
,反之返回FALSE
。这里请看write
1 read(string $sessionId)如果会话中有数据,read 回调函数必须返回将会话数据编码(序列化)后的字符串。 如果会话中没有数据,read 回调函数返回空字符串。
在自动开始会话或者通过调用 session_start() 函数手动开始会话之后,PHP 内部调用 read 回调函数来获取会话数据。 在调用 read 之前,PHP 会调用 open 回调函数。
read 回调返回的序列化之后的字符串格式必须与
write
回调函数保存数据时的格式完全一致。 PHP 会自动反序列化返回的字符串并填充 $_SESSION 超级全局变量。 虽然数据看起来和 serialize() 函数很相似, 但是需要提醒的是,它们是不同的。 请参考: session.serialize_handler。
1 write(string $sessionId, string $data)在会话保存数据时会调用
write
回调函数。 此回调函数接收当前会话 ID 以及 $_SESSION 中数据序列化之后的字符串作为参数。 序列化会话数据的过程由 PHP 根据 session.serialize_handler 设定值来完成。序列化后的数据将和会话 ID 关联在一起进行保存。 当调用
read
回调函数获取数据时,所返回的数据必须要和 传入write
回调函数的数据完全保持一致。PHP 会在脚本执行完毕或调用 session_write_close() 函数之后调用此回调函数。 注意,在调用完此回调函数之后,PHP 内部会调用
close
回调函数。Note:
PHP 会在输出流写入完毕并且关闭之后 才调用 write 回调函数, 所以在 write 回调函数中的调试信息不会输出到浏览器中。 如果需要在 write 回调函数中使用调试输出, 建议将调试输出写入到文件。
1 destroy($sessionId)当调用 session_destroy() 函数, 或者调用 session_regenerate_id() 函数并且设置 destroy 参数为
TRUE
时, 会调用此回调函数。此回调函数操作成功返回TRUE
,反之返回FALSE
。
1 gc($lifetime)为了清理会话中的旧数据,PHP 会不时的调用垃圾收集回调函数。 调用周期由 session.gc_probability 和 session.gc_divisor 参数控制。 传入到此回调函数的 lifetime 参数由 session.gc_maxlifetime 设置。 此回调函数操作成功返回
TRUE
,反之返回FALSE
。
1 create_sid()当需要新的会话 ID 时被调用的回调函数。 回调函数被调用时无传入参数, 其返回值应该是一个字符串格式的、有效的会话 ID。
sequenceDiagram
participant 业务代码
participant session
业务代码--> session: 实现SessionHandlerInterface接口并配置session_set_save_handler
note left of 业务代码: session_start()
session-->> session: 调用open
session-->> session: 调用read获取session数据
session->> 业务代码: 将数据赋值到$_SESSION全局变量中
note left of 业务代码: 写入数据到session里面
业务代码--> 业务代码: 给$_SESSION赋值
note right of 业务代码: 数据此时只会保存在全局变量里面
note left of 业务代码: 代码执行完毕
业务代码--> session: 代码执行完毕触发write事件
session-->> session: 调用write写入数据
session-->> session: 调用close关闭session
yii2 Redis Session组件讲解
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
<?php
namespace yii\redis;
use Yii;
use yii\base\InvalidConfigException;
class Session extends \yii\web\Session
{
public $redis = 'redis';
public $keyPrefix;
/**
* 组件初始化
*/
public function init()
{
// 先获取Redis对象
if (is_string($this->redis)) {
$this->redis = Yii::$app->get($this->redis);
} elseif (is_array($this->redis)) {
if (!isset($this->redis['class'])) {
$this->redis['class'] = Connection::className();
}
$this->redis = Yii::createObject($this->redis);
}
if (!$this->redis instanceof Connection) {
throw new InvalidConfigException("Session::redis must be either a Redis connection instance or the application component ID of a Redis connection.");
}
// 配置前缀
if ($this->keyPrefix === null) {
$this->keyPrefix = substr(md5(Yii::$app->id), 0, 5);
}
parent::init();
}
/**
* 表示自定义回话 永远返回true
*/
public function getUseCustomStorage()
{
return true;
}
/**
* 获取session的时候调用
*/
public function readSession($id)
{
$data = $this->redis->executeCommand('GET', [$this->calculateKey($id)]);
return $data === false || $data === null ? '' : $data;
}
/**
* 写入session的时候调用
*/
public function writeSession($id, $data)
{
return (bool) $this->redis->executeCommand('SET', [$this->calculateKey($id), $data, 'EX', $this->getTimeout()]);
}
/**
* 销毁session的时候调用
*/
public function destroySession($id)
{
$this->redis->executeCommand('DEL', [$this->calculateKey($id)]);
// @see https://github.com/yiisoft/yii2-redis/issues/82
return true;
}
/**
* 拼接键名
*/
protected function calculateKey($id)
{
return $this->keyPrefix . md5(json_encode([__CLASS__, $id]));
}
}
其实最主要的是它父级里面的这几个方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
/**
* Starts the session.
*/
public function open()
{
// 是否在运行状态了
if ($this->getIsActive()) {
return;
}
// 注册处理器
$this->registerSessionHandler();
// 设置cookie参数
$this->setCookieParamsInternal();
// 开启session
YII_DEBUG ? session_start() : @session_start();
if ($this->getIsActive()) {
Yii::info('Session started', __METHOD__);
$this->updateFlashCounters();
} else {
$error = error_get_last();
$message = isset($error['message']) ? $error['message'] : 'Failed to start session.';
Yii::error($message, __METHOD__);
}
}
/**
* Registers session handler.
* @throws \yii\base\InvalidConfigException
*/
protected function registerSessionHandler()
{
// 如果存在处理器
if ($this->handler !== null) {
if (!is_object($this->handler)) {
$this->handler = Yii::createObject($this->handler);
}
if (!$this->handler instanceof \SessionHandlerInterface) {
throw new InvalidConfigException('"' . get_class($this) . '::handler" must implement the SessionHandlerInterface.');
}
YII_DEBUG ? session_set_save_handler($this->handler, false) : @session_set_save_handler($this->handler, false);
} elseif ($this->getUseCustomStorage()) { // 是否使用自定义回话
// 设置处理器
if (YII_DEBUG) {
session_set_save_handler(
[$this, 'openSession'],
[$this, 'closeSession'],
[$this, 'readSession'],
[$this, 'writeSession'],
[$this, 'destroySession'],
[$this, 'gcSession']
);
} else {
@session_set_save_handler(
[$this, 'openSession'],
[$this, 'closeSession'],
[$this, 'readSession'],
[$this, 'writeSession'],
[$this, 'destroySession'],
[$this, 'gcSession']
);
}
}
}
代码就是这么简单。原理也是这么简单。可以自己亲自尝试去Redis里面看看有没有保存的数据了