Session自定义存储及分布式存储

@date:2016-05-15 21:59:00

默认情况下,PHP 使用内置的文件会话保存管理器(files)来完成会话的保存。我们无需设置,PHP默认将session以文件的形式保存到服务器。

通过调用函数 session_start() 即可手动开始一个会话。如果配置项 session.auto_start 设置为1, 那么请求开始的时候,会话会自动开始。

PHP也提供了自定义会话保存管理器功能。有时候我们希望session可以保存到其他地方,如数据库。

自定义Session存储函数 #

如果需要在数据库中或者以其他方式存储会话数据, 需要使用 session_set_save_handler() 函数来创建一系列用户级存储函数。

函数 session_set_save_handler() 的参数即为在会话生命周期内要调用的一组回调函数: openreadwrite 以及 close。 还有一些回调函数被用来完成垃圾清理:destroy 用来删除会话, gc用来进行周期性的垃圾收集。

PHP手册上说明了实现原理:

会话开始的时候,PHP 会调用 open 管理器,然后再调用 read 回调函数来读取内容,该回调函数返回已经经过编码的字符串。 然后 PHP 会将这个字符串解码,并且产生一个数组对象,然后保存至 $_SESSION 超级全局变量。

当 PHP 关闭的时候(或者调用了 session_write_close() 之后), PHP 会对 $_SESSION 中的数据进行编码, 然后和会话 ID 一起传送给 write 回调函数。 write 回调函数调用完毕之后,PHP 内部将调用 close 回调函数。

销毁会话时,PHP 会调用 destroy回调函数。

根据会话生命周期时间的设置,PHP 会不时地调用 gc 回调函数。 该函数会从持久化存储中删除超时的会话数据。 超时是指会话最后一次访问时间距离当前时间超过了 $lifetime 所指定的值。

需要实现:

session_set_save_handler ( callable $open , callable $close , callable $read , callable $write , callable $destroy , callable $gc [, callable $create_sid ] )

MySQL存储session示例:

<?php
/*
CREATE TABLE IF NOT EXISTS `sessions` (
    `id` varchar(40) NOT NULL,
    `ip_address` varchar(45) NOT NULL DEFAULT '',
    `timestamp` int(10) unsigned DEFAULT 0 NOT NULL,
    `data` blob NOT NULL,
    KEY `sessions_timestamp` (`timestamp`)
);
*/

$con = mysqli_connect("127.0.0.1", "user" , "pass", "session");
function open($save_path, $session_name) {
	return true;
}

function close() {
	return true;
}

function read($id, $con) {
	if ($result = mysqli_query($con, sprintf("select * from sessions where `id`='%s' and `timestamp` > '%s'", $id, time()))) {
		if ($row = mysqli_fetch_row($result)) {
			return $row["data"];
		}
	}else {
		return "";
	}
}

function write($id, $data, $con) {
	$expire = time() + SESSION_MAXLIFETIME;

	$sql = 'INSERT INTO `sessions` (`id`, `data`, `timestamp`) '
		. 'values (%s, %s, %s) '
		. 'ON DUPLICATE KEY UPDATE data = %s, timestamp = %s';

	if ($result = mysqli_query($con, $con, sprintf($sql, $id, $data, $expire, $data, $expire))) {
	return true;
	} else {
	return false;
	}
}

function destroy($id, $con) {
	if ($result = mysqli_query($con, sprintf("delete * from sessions where id=%s", $id))) {
		return true;
	}else {
		return false;
	}
}

function gc($maxlifetime, $con) {
	if ($result = mysqli_query($con, sprintf("delete * from sessions where timestamp < %s", time()))) {
		return true;
	} else {
		return false;
	}
}

session_set_save_handler("open", "close", "read", "write", "destroy", "gc");
session_start();

当然,也支持类里面的方法:

<?php
class MySQLSession{
    private $con;

    function open($save_path, $session_name) {
        $this->con = mysqli_connect("127.0.0.1", "user" , "pass", "session");
    }

    function close() {
        mysqli_close($this->con);
    }

    function read($id) {
        if ($result = mysqli_query($this->con, sprintf("select * from sessions where `id`=%s and `timestamp` > %s", $id, time()))) {
            if ($row = mysqli_fetch_row($result)) {
                return $row["data"];
            }
        }else {
            return "";
        }
    }

    function write($id, $data) {
        $expire = time() + SESSION_MAXLIFETIME;

        $sql = 'INSERT INTO `sessions` (`id`, `data`, `timestamp`) '
            . 'values (%s, %s, %s) '
            . 'ON DUPLICATE KEY UPDATE data = %s, timestamp = %s';

        if ($result = mysqli_query($this->con, sprintf($sql, $id, $data, $expire, $data, $expire))) {
            return true;
        } else {
            return false;
        }
    }

    function destroy($id) {
        if ($result = mysqli_query($this->con, sprintf("delete * from sessions where id=%s", $id))) {
            return true;
        }else {
            return false;
        }
    }

    function gc($maxlifetime) {
        if ($result = mysqli_query($this->con, sprintf("delete * from sessions where timestamp < %s", time()))) {
            return true;
        } else {
            return false;
        }
    }
}

$handler = new MySQLSession();
session_set_save_handler(
    array($handler, 'open'),
    array($handler, 'close'),
    array($handler, 'read'),
    array($handler, 'write'),
    array($handler, 'destroy'),
    array($handler, 'gc')
);
// 下面这行代码可以防止使用对象作为会话保存管理器时可能引发的非预期行为
register_shutdown_function ( 'session_write_close' );

session_start();

自 PHP 5.4 开始,可以使用下面的方式来注册自定义会话存储函数:

bool session_set_save_handler ( SessionHandlerInterface $sessionhandler [, bool $register_shutdown = true ] )

需要实现类里的open,write,read,destroy,gc,close方法。

<?php
 class  MySessionHandler  implements  SessionHandlerInterface{
  public function open($save_path, $session_id)
  {
   return true;
  }

  /**
   * 写session
   */
  public function write($session_id, $session_data)
  {
   // TODO: Implement write() method.
  }

  /**
   * 读取session
   */
  public function read($session_id)
  {
   // TODO: Implement read() method.
  }

  /**
   * 删除指定session
   */
  public function destroy($session_id)
  {
   // TODO: Implement destroy() method.
  }

  /**
   * 销毁过期session
   */
  public function gc($maxlifetime)
  {
   // TODO: Implement gc() method.
  }

  public function close()
  {
   return true;
  }
 }
 
$handler  = new  MySessionHandler ();
 session_set_save_handler ( $handler ,  true );
 session_start ();

分布式存储 #

当多台服务器负载均衡使用时,这时候还在使用默认的文件存储session方式,就会造成session不同步。这时候我们可以把session存储到同一个地方,如上面的mysql存储session方式。下面看看memcache、redis等如何存储session。

一些 PHP 扩展提供了内置的会话管理器,例如:redis, memcache, 可以通过配置项 session.save_handler 来使用它们。

对于文件会话保存管理器,会将会话数据保存到配置项 session.save_path 所指定的位置。
对于缓存类保存管理器,会将会话数据保存到配置项 session.save_path 所指定的地址。

Memcache #

<?php
ini_set("session.save_handler", "memcache"); // memcache
ini_set("session.save_path", "127.0.0.1:11211"); // 不要tcp:

session_start();

$mem = new memcache();
$mem->addServer('127.0.0.1', '11211');

//测试memcache是否正常
//$mem->add('uid', 6, 0, 3600);
//echo $mem->get('uid');

//设置一个session
//$_SESSION['uid'] = 10;
//var_dump($_SESSION);

//查看session在memcache里的存储
echo $mem->get(session_id());

Redis #

<?php
ini_set("session.save_handler", "redis"); // memcache
ini_set("session.save_path", "127.0.0.1:6379"); // 不要tcp:

session_start();

$redis = new redis();
$redis->connect('127.0.0.1', '6379');

//测试redis是否正常
//$redis->set('uid', 6, 3600);
//echo $redis->get('uid');

//设置一个session
$_SESSION['uid'] = 10;
//var_dump($_SESSION);

//查看session在redis里的存储
var_dump($redis->get('PHPREDIS_SESSION:'.session_id()));

实例 #

ThinkPHP3.13设置session入库 #

准备工作:

1、建立session表:

CREATE TABLE `thinkphp_session` (
  `session_id` varchar(255) NOT NULL,
  `session_expire` int(11) NOT NULL,
  `session_data` blob,
  UNIQUE KEY `session_id` (`session_id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8

这里的表名以实际为准。

2、修改配置文件
配置文件config.php中配置session的数据表,追加一下数据:

'SESSION_OPTIONS'       =>  array(
	'type'=> 'db',//session采用数据库保存
	'name'                =>  'PHPSESSION',   //设置session名
	'expire'              =>  3600 * 24 * 1, //SESSION保存时间
	'use_trans_sid'       =>  1,                               //跨页传递
	'use_only_cookies'    =>  0,                               //是否只开启基于cookies的session的会话方式
),
'SESSION_TABLE'=>'thinkphp_session',

3、确保相关驱动存在,位于Core\Extend\Driver\Session:
SessionDb.class.php
该文件默认使用mysql系列函数连接,需要修改为mysqli:

<?php 
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK IT ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006-2012 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------


defined('THINK_PATH') or exit();
/**
 * 数据库方式Session驱动
 *    CREATE TABLE think_session (
 *      session_id varchar(255) NOT NULL,
 *      session_expire int(11) NOT NULL,
 *      session_data blob,
 *      UNIQUE KEY `session_id` (`session_id`)
 *    );
 * @category   Extend
 * @package  Extend
 * @subpackage  Driver.Session
 * @author    liu21st <liu21st@gmail.com>
 */
class SessionDb {

    /**
     * Session有效时间
     */
   protected $lifeTime      = ''; 

    /**
     * session保存的数据库名
     */
   protected $sessionTable  = '';

    /**
     * 数据库句柄
     */
   protected $hander  = array(); 

    /**
     * 打开Session 
     * @access public 
     * @param string $savePath 
     * @param mixed $sessName  
     */
    public function open($savePath, $sessName) { 
       $this->lifeTime = C('SESSION_EXPIRE')?C('SESSION_EXPIRE'):ini_get('session.gc_maxlifetime');
       $this->sessionTable  =   C('SESSION_TABLE')?C('SESSION_TABLE'):C("DB_PREFIX")."session";
       //分布式数据库
       $host = explode(',',C('DB_HOST'));
       $port = explode(',',C('DB_PORT'));
       $name = explode(',',C('DB_NAME'));
       $user = explode(',',C('DB_USER'));
       $pwd  = explode(',',C('DB_PWD'));
       if(1 == C('DB_DEPLOY_TYPE')){
           //读写分离
           if(C('DB_RW_SEPARATE')){
               $w = floor(mt_rand(0,C('DB_MASTER_NUM')-1));
               if(is_numeric(C('DB_SLAVE_NO'))){//指定服务器读
                   $r = C('DB_SLAVE_NO');
               }else{
                   $r = floor(mt_rand(C('DB_MASTER_NUM'),count($host)-1));
               }
               //主数据库链接
               $hander = mysqli_connect(
                   $host[$w].(isset($port[$w])?':'.$port[$w]:':'.$port[0]),
                   isset($user[$w])?$user[$w]:$user[0],
                   isset($pwd[$w])?$pwd[$w]:$pwd[0]
                   );
               $dbSel = mysqli_select_db(
                   $hander,
                   isset($name[$w])?$name[$w]:$name[0]
               );
               if(!$hander || !$dbSel)
                   return false;
               $this->hander[0] = $hander;
               //从数据库链接
               $hander = mysqli_connect(
                   $host[$r].(isset($port[$r])?':'.$port[$r]:':'.$port[0]),
                   isset($user[$r])?$user[$r]:$user[0],
                   isset($pwd[$r])?$pwd[$r]:$pwd[0]
                   );
               $dbSel = mysqli_select_db(
                   $hander,
                   isset($name[$r])?$name[$r]:$name[0]
                   );
               if(!$hander || !$dbSel)
                   return false;
               $this->hander[1] = $hander;
               return true;
           }
       }
       //从数据库链接
       $r = floor(mt_rand(0,count($host)-1));
       $hander = mysqli_connect(
           $host[$r].(isset($port[$r])?':'.$port[$r]:':'.$port[0]),
           isset($user[$r])?$user[$r]:$user[0],
           isset($pwd[$r])?$pwd[$r]:$pwd[0]
           );
       $dbSel = mysqli_select_db(
           $hander,
           isset($name[$r])?$name[$r]:$name[0]
           );
       if(!$hander || !$dbSel) 
           return false; 
       $this->hander = $hander; 
       return true; 
    } 

    /**
     * 关闭Session 
     * @access public 
     */
   public function close() {
       if(is_array($this->hander)){
           $this->gc($this->lifeTime);
           return (mysqli_close($this->hander[0]) && mysqli_close($this->hander[1]));
       }
       $this->gc($this->lifeTime); 
       return mysqli_close($this->hander);
   } 

    /**
     * 读取Session 
     * @access public 
     * @param string $sessID 
     */
   public function read($sessID) { 
       $hander = is_array($this->hander)?$this->hander[1]:$this->hander;
       $res = mysqli_query($hander, "SELECT session_data AS data FROM ".$this->sessionTable." WHERE session_id = '$sessID'   AND session_expire >".time());
       if($res) {
           $row = mysqli_fetch_assoc($res);
           return $row['data']; 
       }
       return ""; 
   } 

    /**
     * 写入Session 
     * @access public 
     * @param string $sessID 
     * @param String $sessData  
     */
   public function write($sessID,$sessData) { 
       $hander = is_array($this->hander)?$this->hander[0]:$this->hander;
       $expire = time() + $this->lifeTime; 
       mysqli_query($hander, "REPLACE INTO  ".$this->sessionTable." (  session_id, session_expire, session_data)  VALUES( '$sessID', '$expire',  '$sessData')");
       if(mysqli_affected_rows($hander))
           return true; 
       return false; 
   } 

    /**
     * 删除Session 
     * @access public 
     * @param string $sessID 
     */
   public function destroy($sessID) { 
       $hander = is_array($this->hander)?$this->hander[0]:$this->hander;
       mysqli_query($hander, "DELETE FROM ".$this->sessionTable." WHERE session_id = '$sessID'");
       if(mysqli_affected_rows($hander))
           return true; 
       return false; 
   } 

    /**
     * Session 垃圾回收
     * @access public 
     * @param string $sessMaxLifeTime 
     */
   public function gc($sessMaxLifeTime) { 
       $hander = is_array($this->hander)?$this->hander[0]:$this->hander;
       mysqli_query($hander, "DELETE FROM ".$this->sessionTable." WHERE session_expire < ".time());
       return mysqli_affected_rows($hander);
   } 

    /**
     * 打开Session 
     * @access public 
     */
    public function execute() {
        session_set_save_handler(array(&$this,"open"), 
                         array(&$this,"close"), 
                         array(&$this,"read"), 
                         array(&$this,"write"), 
                         array(&$this,"destroy"), 
                         array(&$this,"gc"));
    }
}

经过测试,使用了函数session_set_save_handler后,session.save_handle后的值被修改为了user

Build by Loppo 0.6.14