基于 Redis 实现 Laravel 分布式 Session 存取及底层源码探究
Session 存储器选择
Laravel 没有使用 PHP 内置的 Session 功能,而是自行实现了一套 Session 组件,和其他 Laravel 系统组件一样,Session 组件也支持多种驱动作为存储器实现,包括文件、数据库、Memcached、Redis 等,默认使用的是文件驱动:
SESSION_DRIVER=file
如果应用只部署在一台服务器上,使用文件驱动就够了,但是如果采用集群部署,文件驱动就有问题了:比如用户登录请求被负载均衡到服务器 A 上,Session 数据就会存储到这台机器的 Session 文件中,而该用户的下一个请求被负载均衡到了服务器 B 上,在那台机器上的 Session 文件中找不到对应的 Session 数据,就会导致用户明明已经登录,但是读取不到 Session 数据而显式未登录的情况,这就是 Bug 了。
为了解决这个问题,需要将 Session 数据存储到所有应用集群机器可以共享的存储媒介,比如数据库、Memcached、Redis 等,数据库在应对高并发请求的时候性能不及 Memcached、Redis 等基于内存的存储媒介,而 Memcached 作为缓存系统是没的说,但是不支持数据持久化,系统重启后所有数据就都丢了,所以最佳选择就剩下 Redis。
使用 Redis 作为分布式 Session 存储器后,所有应用集群内的机器可以到统一的 Redis 服务实例存取 Session 数据,就不会再有文件驱动那种「各人自扫门前雪,休管他人瓦上霜」的数据孤岛情况出现了。
Laravel Session 使用入门
基本配置
要使用 Redis 作为 Laravel Session 的存储器驱动,只需要将环境配置文件 .env
中的 SESSION_DRIVER
配置为 redis
即可:
SESSION_DRIVER=redis
Laravel Session 数据的默认有效期是 120 分钟,即两个小时,要修改这个值,可以修改 SESSION_LIFETIME
配置值:
SESSION_LIFETIME=120
更多 Session 配置,可以到 config/session.php
中查看和维护。
存储&获取
在 Laravel 中,你可以通过多种方式获取 Session 实例对 Session 数据进行管理。
通过 session 辅助函数
$name = session('name', function () {
return session('name', '学院君');
});
dd($name);
通过 Session 门面
$name = Session::get('name', fn() => Session::put('name', '学院君'));
dd($name);
通过 Request 实例
$request->session()->put('name', '学院君');
$name = $request->session()->get('name');
dd($name);
Session 的基本使用非常简单,更多使用语法,可参考 Laravel Session 文档。
底层实现源码分析
接下来,我们来看看 Laravel 底层是如何基于 Redis 实现 Session 数据的管理的。
Session 服务注册和驱动器实例
不管是 Session
门面还是辅助函数,底层都是通过绑定在服务容器中的 session
服务实例对 Session 数据进行管理的:
服务注册源码位于
Illuminate\Session\SessionServiceProvider
。
当我们调用 put
、get
方法存取 Session 数据时,实际上调用的都是这个 SessionManager
上的方法,但继续追根溯源,会发现 SessionManager
也没有定义这些方法,而是通过其父类定义的魔术方法 __call
调用 $this->driver()
返回的 Session 驱动实例上的方法:
这个 $this->driver()
方法会读取配置文件中配置的 Session 驱动,这里是 redis
,最终通过 createRedisDriver
方法,调用 $this->createCacheHandler('redis')
创建基于 Redis 驱动的 CacheBasedSessionHandler
来构建 Session 存储器,并将其作为最终的 Session 驱动实例:
所有上层业务调用的 Session 数据管理操作最终都是通过这个实例执行的,但它也是一个封装了 CacheBasedSessionHandler
的壳,我们前面介绍的不同驱动实现都位于 SessionHandler(处理器)这一层,Session 存储器是在其基础上做了统一的封装,最终数据的读取、存储是通过 SessionHanlder 与更底层的文件、数据库、缓存驱动打交道的。
Session 处理器与存储器驱动实现
下面我们就以 Redis 驱动为例来深入分析下 Session 处理器和存储器的底层实现。
CacheBasedSessionHandler
实现了 PHP 提供的 SessionHandlerInterface 接口来自定义兼容 PHP 官方约定的 Session 处理器:
<?php
namespace Illuminate\Session;
use Illuminate\Contracts\Cache\Repository as CacheContract;
use SessionHandlerInterface;
class CacheBasedSessionHandler implements SessionHandlerInterface
{
protected $cache;
protected $minutes;
public function __construct(CacheContract $cache, $minutes)
{
$this->cache = $cache;
$this->minutes = $minutes;
}
public function open($savePath, $sessionName)
{
return true;
}
public function close()
{
return true;
}
public function read($sessionId)
{
return $this->cache->get($sessionId, '');
}
public function write($sessionId, $data)
{
return $this->cache->put($sessionId, $data, $this->minutes * 60);
}
public function destroy($sessionId)
{
return $this->cache->forget($sessionId);
}
public function gc($lifetime)
{
return true;
}
public function getCache()
{
return $this->cache;
}
}
其中的 cache
属性对应的是基于 Redis 实现的缓存服务实例,所以说,基于 Redis 的 Session 处理器最终是通过缓存组件管理Session 数据的,对于缓存系统而言,有自己的一套数据过期管理机制,所以 gc
方法可以留空。
回到 createRedisDriver
方法,通过最后一行代码,进入 buildSession
,看看基于 Session 存储器的 Session 驱动最终是如何实现的:
在这个方法中,由于默认的 session.encrypt
被配置为 false
,所以最终返回的是通过 Cookie 名称和 Redis 缓存服务驱动的 Session 处理器构建的 Illuminate\Session\Store
实例,它是最终对 Session 服务进行管理的 Session 驱动实现。
阅读 Illuminate\Session\Store
源码,不难看出 Laravel 应用 Session 服务的启动、Session 数据的管理、一次性数据存取都是通过它实现的。
Session 数据存取的底层原理
以 Session 存取方法 get
、put
实现为例,当用户发起一个 Web 请求后,首先会通过 StartSession
中间件(web
中间件组中注册)中的 startSession
方法开启 Session 服务:
这个方法最终调用的是 Session 驱动实例 Illuminate\Session\Store
的 start
方法:
public function start()
{
$this->loadSession();
if (! $this->has('_token')) {
$this->regenerateToken();
}
return $this->started = true;
}
该方法会调用 loadSession
方法从 Session 处理器中合并存储在 Redis 缓存中的当前用户 Session 数据到当前驱动实例的 Session 属性数组 $attributes
中:
注:Redis 缓存系统会在键名中通过 Session ID 对不同用户的 Session 数据进行区分,而 Session ID 又会每次随着请求 Cookie 传递过来,
StartSession
中间件初始化 Session 驱动实例时会基于 Cookie 中读取到的值来设置 Session ID,如果没有的话,会重新设置,所以上述合并只会合并当前用户的 Session 数据,由此也确保了 HTTP 请求变得有状态(Sateful)其不会出现不同用户的 Session 数据错乱。
这样一来,当我们在请求处理代码中通过 Session 存储器的 get
方法获取 Session 数据时,就可以读取到之前该用户请求设置过的 Session 数据了:
public function get($key, $default = null)
{
return Arr::get($this->attributes, $key, $default);
}
下面我们再来看 Session 数据的存储实现:
public function put($key, $value = null)
{
if (! is_array($key)) {
$key = [$key => $value];
}
foreach ($key as $arrayKey => $arrayValue) {
Arr::set($this->attributes, $arrayKey, $arrayValue);
}
}
设置 Session 数据时,先直接写入驱动实例的 $attributes
属性数组,因此,在当前请求周期内读取时,可以直接从这个属性数组里面获取。对于 Session 有效期内的后续请求,在当前请求处理完毕发送响应数据给用户前,会通过 StartSession
中间件进行后续 Session 持久化处理。
回到 StartSession
中间件的 handleStatefulRequest
方法,在请求处理之后,会先通过 addCookieToResponse
方法添加包含 Session ID 的 Cookie 到响应头,然后通过 saveSession
方法保存当前 Session 数据到 Redis 缓存,该方法最终调用的是 Session 驱动实例 Illuminate\Session\Store
的 save
方法:
也就是将当前用户 Session ID 对应的 Session 数据(位于 $attributes
属性数组)通过 Session 处理器实例持久化到 Redis 缓存中。
这样一来,下次请求时又会从处理器中合并这些 Session 数据到当前驱动实例的 $attributes
数组,从而 get
方法获取,周而复始。
Session 数据过期维护
这里,还有一个问题,就是 Session 数据不是有个生命周期吗,这是如何维护的呢?
原来,在存储 Session 数据到 Redis 缓存时,会读取 Session 生命周期时长作为对应键名的有效期,然后通过 Redis 本身的机制即可自动销毁过期的 Session 数据。对于那些不支持这种自动清除过期数据的 Session 驱动,比如文件、数据库,在 StartSession
中间件每次启动 Session 服务后,请求处理前,会调用 collectGarbage
方法清理已过期的 Session 数据:
以数据库驱动处理器 DatabaseSessionHandler
为例,对应的 gc
实现方法如下:
public function gc($lifetime)
{
$this->getQuery()->where('last_activity', '<=', $this->currentTime() - $lifetime)->delete();
}
这是一种惰性清理机制,如果用户没有发起请求,则不会清除,所以你可以通过编写调度任务及时清理这些过期数据,以免垃圾数据堆积。
小结
以上就是 Laravel 底层 Session 服务从启动、到数据存储、读取、清理的完整实现,到这里,我们也已经演示和分析完了 Laravel 底层的所有系统组件,Redis 的实战入门篇也已经进入尾声。下篇教程,学院君将开始给大家介绍 Redis 的进阶应用和 Redis 自身功能的底层实现原理。
本系列教程的源码可以从 Github 获取:https://github.com/nonfu/redis-demo/。
4 Comments
大佬,请教一下你,同一个顶级域名下的两个项目:www.test.com和api.test.com,一个是session驱动的用户鉴权,一个token驱动的用户鉴权怎么打通啊,就是在一个登录后,另一个也是登录状态
cookie 写到 .test.com 域名下即可
学院君,能说的具体点吗,小白不是很理解,www.test.com这边用session驱动的我能理解,api.test.com这边不是接收头部的token去鉴权的吗,拿到cookie要怎么鉴权呢?
这个就更简单了啊 用户表是共用的吧 这个 token 是用户级的啊