您的位置:首页 > 数据库 > Redis

CodeIgniter框架源码笔记(15)——SESSION之Redis驱动实现

2016-08-25 21:21 489 查看

Session机制

session_start()时,调用了open(),read()方法。并有一定机率触发gc()方法。

session_commit()或session_write_close()时,触发write(),close()方法。

session_destory()会触发desotry()方法。

技术要点

1、驱动要实现open ,read ,write ,close ,destory ,gc六个方法。

open:连接redis数据库connect()。配置中的save_path用来存储连接redis的host、port、password、database、timeout信息。

read: 对本次请求的session加锁,然后根据session_id读取(get)对应key中的内容。

write:设置(set)有效期为
$this->_config['expiration']
的缓存。

close:释放锁,关闭redis连接。

destory:清空当前请求的session内容,即:从redis中删除session_id对应的键。

gc:因为redis的缓存有生命周期,过期自动被回收,所以不需要我们手工设置垃圾回收机制。

2、驱动要支持
session_regenerate_id()


3、驱动要实现session锁:锁是存储在键名
$lock_key = $this->_key_prefix.$session_id.':lock'
的缓存中,在存储时给了300秒生命周期。每个sessionid有一把锁。一次只允许一个http请求独占。CI加锁机制比数据库驱动中的做法靠谱,数据库驱动中一旦发现字符锁被占用,就直接返回FALSE了,而在Redis驱动中,会阻塞并每间隔一秒请求一次查看对方是否释放锁。

具体实现可以参考如下代码

$lock_key = $this->_key_prefix.$session_id.':lock';
$attempt = 0;
do
{
//如果key值为$lock_key的生命周期还没有到期,就尝试30次获取锁,中间间隔一秒。
//所以这里如果出现锁争用的情况,当前请求最长会阻塞30秒钟
if (($ttl = $this->_redis->ttl($lock_key)) > 0)
{
sleep(1);
continue;
}

//代码运行到这里说明另一个请求中的锁到期了或者释放了

//写入key为$lock_key的缓存,300秒生存周期
if ( ! $this->_redis->setex($lock_key, 300, time()))
{
//缓存写入失败进行日志记录
log_message('error', 'Session: Error while trying to obtain lock for '.$this->_key_prefix.$session_id);
return FALSE;
}
//代码运行到这里,创建锁成功
$this->_lock_key = $lock_key;
break;
}
while (++$attempt < 30);


接下来看整个源码

class CI_Session_redis_driver extends CI_Session_driver implements SessionHandlerInterface {

//phpRedis操作实例对像
protected $_redis;

//键名前缀
protected $_key_prefix = 'ci_session:';

//标记当前进程是否获得锁
protected $_lock_key;

// ------------------------------------------------------------------------

//通过加载配置文件获取redis连接信息,并存入$this->_config['save_path']
public function __construct(&$params)
{
parent::__construct($params);

if (empty($this->_config['save_path']))
{
log_message('error', 'Session: No Redis save path configured.');
}
elseif (preg_match('#(?:tcp://)?([^:?]+)(?:\:(\d+))?(\?.+)?#', $this->_config['save_path'], $matches))
{
isset($matches[3]) OR $matches[3] = ''; // Just to avoid undefined index notices below
$this->_config['save_path'] = array(
'host' => $matches[1],
'port' => empty($matches[2]) ? NULL : $matches[2],
'password' => preg_match('#auth=([^\s&]+)#', $matches[3], $match) ? $match[1] : NULL,
'database' => preg_match('#database=(\d+)#', $matches[3], $match) ? (int) $match[1] : NULL,
'timeout' => preg_match('#timeout=(\d+\.\d+)#', $matches[3], $match) ? (float) $match[1] : NULL
);

preg_match('#prefix=([^\s&]+)#', $matches[3], $match) && $this->_key_prefix = $match[1];
}
else
{
log_message('error', 'Session: Invalid Redis save path format: '.$this->_config['save_path']);
}

if ($this->_config['match_ip'] === TRUE)
{
$this->_key_prefix .= $_SERVER['REMOTE_ADDR'].':';
}
}

// ------------------------------------------------------------------------

//open()
//根据$this->_config['save_path']信息连接登陆redis,并选择用于存储session的库
public function open($save_path, $name)
{
if (empty($this->_config['save_path']))
{
return $this->_failure;
}

$redis = new Redis();
//connect() 连接
if ( ! $redis->connect($this->_config['save_path']['host'], $this->_config['save_path']['port'], $this->_config['save_path']['timeout']))
{
log_message('error', 'Session: Unable to connect to Redis with the configured settings.');
}
//auth() 登陆验证
elseif (isset($this->_config['save_path']['password']) && ! $redis->auth($this->_config['save_path']['password']))
{
log_message('error', 'Session: Unable to authenticate to Redis instance.');
}
//select() 选择存储session的库
elseif (isset($this->_config['save_path']['database']) && ! $redis->select($this->_config['save_path']['database']))
{
log_message('error', 'Session: Unable to select Redis database with index '.$this->_config['save_path']['database']);
}
else
{
$this->_redis = $redis;
return $this->_success;
}

return $this->_failure;
}

// ------------------------------------------------------------------------

//
public function read($session_id)
{
//获取锁
if (isset($this->_redis) && $this->_get_lock($session_id))
{
// Needed by write() to detect session_regenerate_id() calls
$this->_session_id = $session_id;
//获取$session_id对应的所有session内容
$session_data = (string) $this->_redis->get($this->_key_prefix.$session_id);
//生成摘要
$this->_fingerprint = md5($session_data);
return $session_data;
}

return $this->_failure;
}

// ------------------------------------------------------------------------

//写入
public function write($session_id, $session_data)
{
if ( ! isset($this->_redis))
{
return $this->_failure;
}
// Was the ID regenerated?
//通过传入$session_id与对像属性$this->_session_id(在read函数中赋值)比较,判断是不是调用了session_regenerate_id()
elseif ($session_id !== $this->_session_id)
{
//释放旧的sessionid占用的锁,同时获取新sessionid对应的锁
if ( ! $this->_release_lock() OR ! $this->_get_lock($session_id))
{
return $this->_failure;
}

$this->_fingerprint = md5('');
$this->_session_id = $session_id;
}

if (isset($this->_lock_key))
{
$this->_redis->setTimeout($this->_lock_key, 300);
if ($this->_fingerprint !== ($fingerprint = md5($session_data)))
{
//调用set设置有效期为$this->_config['expiration']的缓存
if ($this->_redis->set($this->_key_prefix.$session_id, $session_data, $this->_config['expiration']))
{
$this->_fingerprint = $fingerprint;
return $this->_success;
}

return $this->_failure;
}

return ($this->_redis->setTimeout($this->_key_prefix.$session_id, $this->_config['expiration']))
? $this->_success
: $this->_failure;
}

return $this->_failure;
}

// ------------------------------------------------------------------------

//close:释放锁,关闭redis连接
public function close()
{
if (isset($this->_redis))
{
try {
//如果当前连接redis是通的
if ($this->_redis->ping() === '+PONG')
{
//删除当前请求占用的锁。为什么不调用$this->_release_lock() ??
isset($this->_lock_key) && $this->_redis->delete($this->_lock_key);
//关闭redis连接
if ($this->_redis->close() === $this->_failure)
{
return $this->_failure;
}
}
}
catch (RedisException $e)
{
log_message('error', 'Session: Got RedisException on close(): '.$e->getMessage());
}

$this->_redis = NULL;
return $this->_success;
}

return $this->_success;
}

// ------------------------------------------------------------------------

//Destroy:清空当前请求的session内容,即:从redis中删除session_id对应的键
public function destroy($session_id)
{
//保证在当前请求获得锁的情况下才允许进行session_destory操作
if (isset($this->_redis, $this->_lock_key))
{
//服务端处理:删除$session_id对应的键
if (($result = $this->_redis->delete($this->_key_prefix.$session_id)) !== 1)
{
log_message('debug', 'Session: Redis::delete() expected to return 1, got '.var_export($result, TRUE).' instead.');
}
//客户端处理:删除保存sessionid的客户端cookie
$this->_cookie_destroy();
return $this->_success;
}

return $this->_failure;
}

// ------------------------------------------------------------------------

//因为redis有过期回收功能,所以不需要我们手工设置垃圾回收机制供php去调用。
public function gc($maxlifetime)
{
// Not necessary, Redis takes care of that.
//直接返回成功
return $this->_success;
}

// ------------------------------------------------------------------------

//为当前进程的session访问加锁
protected function _get_lock($session_id)
{
// 如果session_id对应的键存在,则重新设定存活时间为300秒
if ($this->_lock_key === $this->_key_prefix.$session_id.':lock')
{
//设定存活时间为300秒
return $this->_redis->setTimeout($this->_lock_key, 300);
}

// 30 attempts to obtain a lock, in case another request already has it
//接下来尝试30次获取锁,这样做了为了防止有别的请求占用了锁。
//这个比数据库驱动中的做法靠谱,数据库驱动中一旦发现字符锁被占用,就直接返回FALSE了
$lock_key = $this->_key_prefix.$session_id.':lock'; $attempt = 0; do { //如果key值为$lock_key的生命周期还没有到期,就尝试30次获取锁,中间间隔一秒。 //所以这里如果出现锁争用的情况,当前请求最长会阻塞30秒钟 if (($ttl = $this->_redis->ttl($lock_key)) > 0) { sleep(1); continue; } //代码运行到这里说明另一个请求中的锁到期了或者释放了 //写入key为$lock_key的缓存,300秒生存周期 if ( ! $this->_redis->setex($lock_key, 300, time())) { //缓存写入失败进行日志记录 log_message('error', 'Session: Error while trying to obtain lock for '.$this->_key_prefix.$session_id); return FALSE; } //代码运行到这里,创建锁成功 $this->_lock_key = $lock_key; break; } while (++$attempt < 30);

//如果尝试次数等于30,说明未能成功获得被占用的锁。
if ($attempt === 30)
{
log_message('error', 'Session: Unable to obtain lock for '.$this->_key_prefix.$session_id.' after 30 attempts, aborting.');
return FALSE;
}
//$ttl为-1过期的情况记录一个debug日志
elseif ($ttl === -1)
{
log_message('debug', 'Session: Lock for '.$this->_key_prefix.$session_id.' had no TTL, overriding.');
}

$this->_lock = TRUE;
return TRUE;
}

// ------------------------------------------------------------------------

//释放锁,即删除键值为$this->_lock_key的redis键值对
protected function _release_lock()
{
if (isset($this->_redis, $this->_lock_key) && $this->_lock)
{
//删除键值为$this->_lock_key的redis键值对
if ( ! $this->_redis->delete($this->_lock_key))
{
log_message('error', 'Session: Error while trying to free lock for '.$this->_lock_key);
return FALSE;
}
//清空当前对像的锁键值
$this->_lock_key = NULL;
//切换锁定状态为FALSE
$this->_lock = FALSE;
}

return TRUE;
}

}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: