目录
解决方案也有很多种:
会话保持
会话复制
会话共享
环境准备
架构设计
SessionHandlerInterface接口
代码编写
总结
优化
前言:
小流量的网站中,我们往往只需要一台服务器就可以维持用户正常的访问以及相关的操作。
随着网站的访问用户剧增,网站业务变得庞大,一台服务器根本无法支撑,因此当今流行的网站应用都部署了多台服务器,实现均衡负载,用来支撑庞大的用户需求。
在多台服务器中,如何保证会话信息的一致性呢?这是我们设计系统值得关注的问题。
如果使用传统的方法,将会话信息保存在服务器中,假设用户A第一次访问的服务器是ServerA,下次访问的是ServerB,而恰好该用户在ServerA中保存有其登录信息,但ServerB中却不存在该登录信息,造成应用无法正确判断用户已经登录,这样的问题是致命,存在不足的。
解决方案也有很多种:
-
会话保持
Session保持(会话保持)是我们见到最多的名词之一,通过会话保持,负载均衡进行请求分发的时候保证每个客户端固定的访问到后端的同一台应用服务器。
会话保持方案在所有的负载均衡都有对应的实现。而且这是在负载均衡这一层就可以解决Session问题。
-
会话复制
会话复制在Tomcat上得到了支持,它是基于IP组播(multicast)来完成Session的复制,Tomcat的会话复制分为两种:
1)全局会话复制:利用Delta Manager复制会话中的变更信息到集群中的所有其他节点。
2)非全局复制:使用Backup Manager进行复制,它会把Session复制给一个指定的备份节点。
-
会话共享
既然会话保持和会话复制都不完美,那么我们为什么不把Session放在一个统一的地方呢,这样集群中的所有节点都在一个地方进行Session的存取就可以解决问题。
前两种解决方案都会遇到瓶颈问题,例如会话复制,在集群超过6个节点以上,就会出现各种问题,不推荐使用。
于是,我们需要在设计系统上,实现会话共享。我们可以将会话信息保存到mysql,redis等持久化服务中。
本文将介绍如何使用PHP将会话信息持久化到mysql中,并实现会话共享
环境准备
· laravel5 (php框架)
· mysql
· nginx(配置均衡负载)
架构设计
在PHP中,有一个函数session_set_save_handler(),该函数的作用是设置会话保存的handler,该函数接受一个SessionHandlerInterface实现。
SessionHandlerInterface接口
官方文档: PHP: session_set_save_handler - Manualhttps://www.php.net/manual/en/function.session-set-save-handler.php
通过查阅PHP官方文档可知,该接口定义了Session的各种处理句柄,开发者可以编写相关的实现类用来实现Session会话的持久化。
了解该接口后,我们现在来编写实现类
代码编写
首先我们需要编写一个获取和保存会话的接口,该接口可以有多种持久化服务的实现类 ,这样在后期系统需要变更持久化服务时候,只需要编写对应服务的驱动实现就可以了。
namespace App\Library\Authorize;
interface SessionSet
{
/**
* 获取会话信息数据
*/
public function getSessionData($session_id);
/**
* 创建会话数据
*/
public function createSessionData($session_id,$extra = "",$expires_time = 0);
/**
* 保存会话数据
*/
public function setSessionData($session_id,$extra = "");
/**
* 删除会话信息
*/
public function removeSessionData($session_id);
}
接着我们编写实现SessionHandlerInterface,SessionSet的基础实现类(Session)
namespace App\Library\Authorize;
abstract class Session implements \SessionHandlerInterface,SessionSet
{
//保存单一的Session对象
public static $instance = null;
private function __construct(){}
/**
* 获取实际驱动对象
*/
public static function getInstance($driver = 'MySql'){
if(self::$instance == null) {
$driver_model = ucfirst(strtolower($driver))."SessionDriver";
$class = "\App\Library\Authorize\\Drivers\\".$driver_model;
$uncheckClass = $class::getRealInstance();
if(!$uncheckClass instanceof SessionSet){
throw new SessionDriverException("非法驱动类");
}
self::$instance = $uncheckClass;
}
return self::$instance;
}
/**
* 关闭 session
*/
public function close()
{
return true;
}
/**
* 删除 session
*/
public function destroy($session_id)
{
return $this->removeSessionData($session_id);
}
/**
* 清理旧 sessions
*/
public function gc($maxlifetime)
{
return true;
}
/**
* 初始化 session
*/
public function open($save_path, $name)
{
return true;
}
/**
* 读取session数据
*/
public function read($session_id){
$session = $this->getSessionData($session_id);
$time = request()->time;
//如果设定过期时间则判断是否过期
if(empty($session) || $session->expires > 0 && $time > ($session->session_time + $session->expires)){
return "";
}
return (string)$session->info;
}
/**
* 保存session信息
*/
public function write($session_id, $session_data)
{
$session = $this->getSessionData($session_id);
$request = request();
if(empty($session)) {
//如果会话ID不存在则需要创建
$expires_time = isset($request->authorizeData['expires_time']) ? $request->authorizeData['expires_time'] : 0;
return $this->createSessionData($session_id,$session_data,$expires_time);
}
return $this->setSessionData($session_id,$session_data);
}
基础类Session实现了SessionHandlerInterface,SessionSet接口,该类负责Session的写入以及读取等其他操作。仅有基础类的功能还不够,我们还需要编写将会话数据持久化到mysql的驱动类。
新建一个驱动类MysqlSessionDriver,该类继承Session基础类,并实现相关的方法。
namespace App\Library\Authorize\Drivers;
use App\Library\Authorize\Session;
use App\Session as SessionModel;
class MysqlSessionDriver extends Session
{
public $model = null;
public static $instance = null;
private function __construct()
{
//该模型为laravel框架的Model,有关该模型的使用请查阅laravel官方相关文档
$this->model = new SessionModel();
}
/**
* 获取单例对象
*/
public static function getRealInstance()
{
if(self::$instance == null) {
self::$instance = new self();
}
return self::$instance;
}
/**
* 根据Session_ID获取一条Session数据记录
*/
public function getSessionData($session_id)
{
return $this->model->newQuery()->where([
'session_id' => $session_id
])->first();
}
/**
* 创建一条session数据
*/
public function createSessionData($session_id, $data = "", $expires_time = 0)
{
$insert_data = [
"session_id" => $session_id,
'session_time' => request()->time,
'info' => $data, //会话数据保存到这里
'expires' => $expires //过期时间
];
$rs = $this->newQuery()->insert($insert_data);
return $rs;
}
/**
* 删除session数据
*/
public function removeSessionData($session_id)
{
$r = $this->model->newQuery()->where(['session_id'=>$session_id])->delete();
if($r !== FALSE){
return true;
}
return false;
}
/**
* 保存session数据
*/
public function setSessionData($session_id, $data = "")
{
$model = $this->newQuery()->where("session_id",$session_id)->first();
if(empty($model)) {
return false;
}
$model->info = $data;
$model->session_time = request()->time;
return $model->save();
}
}
编写好驱动类之后,我们就建立了会话与Mysql之间的联系,php会根据你在session_set_save_hanlder中设置的处理类来进行session操作,开发者只需要在业务代码中操作Session,数据就只自动保存到Mysql中,如果不存在则会创建一条记录。
总结
session_set_save_hanlder能够帮助我们编写自己的持久化会话数据的处理程序,这样我们就能编写程序,将会话数据保存到mysql,redis等持久化服务当中,能真正解决多台服务器部署应用后会话信息不一致的问题。
SessionHandlerInterface接口除了read方法和write方法,还有其他几种方法,我们可以根据不同的业务需求进行编写,例如SessionHandlerInterface::gc()方法,该方法可以实现清除过期的session数据,当session_start()方法被调用的时候触发该方法。更多使用方法可以查阅php官方文档获知:
优化
虽然这样的方案很好的解决了均衡负载会话共享的问题,但是当数据量剧增,I/O的压力也随之增大,导致Mysql查询缓慢,因此我们可以编写redis驱动来替代mysql,更有利于系统的运行。