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

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

2016-08-25 21:09 399 查看
配置选项save_path用来作为表名。

存储Session的表结构如下:

'id' => $session_id,
'ip_address' => $_SERVER['REMOTE_ADDR'],
'timestamp' => time(),
'data' => $session_data


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

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

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

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

open:连接数据库db_connect()

read: 根据session_id读取对应行记录data字段内容,并设置字符串锁SELECT GET_LOCK(‘sessionid’,300)。约定表字段id保存session_id,字段data保存session数据。

write:session内容变更时,更新session_id对应的行记录。如果不存在该行则insert数据,如果存在则update数据。

close:释放字符串锁SELECT RELEASE_LOCK(‘sessionid’)。

destory:delete表中对应sessionid的记录

gc:根据timestamp字段判断记录是否过期,若过期则delete记录。

2、驱动要支持session_regenerate_id()。

3、驱动要实现session锁:一条记录只允许一个http请求独占。CI采用数据库字符锁,mysql下是
SELECT GET_LOCK('str',300) / SELECT RELEASE_LOCK


实现:数据库驱动

三个必须:数据库必须是短连接、数据库必须禁止缓存,另CI要求数据库类必须实现CI_DB_query_builder抽像类。

疑问:如果同一个$session_id锁被另一个http进程占用了,按照CI框架的写法,那么本次http请求中sesstion_start()并不会阻塞并等待另一边释放锁,而是直接返回空的SESSION,这样做对于应用程序来说是好事还是坏事???

<?php
class CI_Session_database_driver extends CI_Session_driver implements SessionHandlerInterface
{
//db对像
protected $_db;

//session_id对应记录是否存在标记。
protected $_row_exists = FALSE;

//数据库平台,CI中有mysql和postgre
protected $_platform;

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

//完成三个必须的判断
public function __construct(&$params)
{
parent::__construct($params);

$CI =& get_instance();
//加载database数据库驱动进来。注:数据库的接口与实现放在system/databses目录下
isset($CI->db) OR $CI->load->database();
$this->_db = $CI->db;
//数据库必须实现CI_DB_query_builder抽像类
if ( ! $this->_db instanceof CI_DB_query_builder)
{
throw new Exception('Query Builder not enabled for the configured database. Aborting.');
}
//数据库必须是短连接
elseif ($this->_db->pconnect)
{
throw new Exception('Configured database connection is persistent. Aborting.');
}
//数据库必须禁止缓存
elseif ($this->_db->cache_on)
{
throw new Exception('Configured database connection has cache enabled. Aborting.');
}
//判断用的哪个类型的数据库
$db_driver = $this->_db->dbdriver.(empty($this->_db->subdriver) ? '' : '_'.$this->_db->subdriver);
if (strpos($db_driver, 'mysql') !== FALSE)
{
$this->_platform = 'mysql';
}
elseif (in_array($db_driver, array('postgre', 'pdo_pgsql'), TRUE))
{
$this->_platform = 'postgre';
}

// 设置表名
isset($this->_config['save_path']) OR $this->_config['save_path'] = config_item('sess_table_name');
}

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

//open()主要完成数据库连接工作connect()
public function open($save_path, $name)
{
//调用db_connect连接数据库
if (empty($this->_db->conn_id) && ! $this->_db->db_connect())
{
return $this->_failure;
}

return $this->_success;
}

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

//读取session_id对应内容
public function read($session_id)
{
//抢占锁:该锁是由$session_id组合生成的字符串锁
//疑问:如果同一个$session_id锁被另一个http进程占用了,按照框架的写法,那么本次http请求中sesstion_start()并不会阻塞并等待另一边释放锁,而是直接返回空的SESSION
//这样做对于应用程序来说是好事还是坏事???
if ($this->_get_lock($session_id) !== FALSE)
{
// Prevent previous QB calls from messing with our queries
//将DB的查询请求复位
$this->_db->reset_query();

// Needed by write() to detect session_regenerate_id() calls
//存储当前session_id,主要用于session_regenerate_id()调用时,在write方法中与方法参数中的新sessionid对比
$this->_session_id = $session_id;

//查询:根据session_id查询data数据
$this->_db
->select('data')
->from($this->_config['save_path'])
->where('id', $session_id);

//如果配置中约定了session必须匹配客户端IP,那么再加上IP一致性这个条件
if ($this->_config['match_ip'])
{
$this->_db->where('ip_address', $_SERVER['REMOTE_ADDR']);
}
//如果没有对应的记录,则将_row_exists 标志设为FALSE,摘要为md5(''),返回''
if (($result = $this->_db->get()->row()) === NULL)
{
//将_row_exists 标志设为FALSE
$this->_row_exists = FALSE;

//摘要为md5('')
$this->_fingerprint = md5('');
return '';
}

// PostgreSQL's variant of a BLOB datatype is Bytea, which is a
// PITA to work with, so we use base64-encoded data in a TEXT
// field instead.

//----------以下是查询到对应记录的处理----------------

//PostgreSQL下需要把结果用base64_decode编码后输出。
$result = ($this->_platform === 'postgre')
? base64_decode(rtrim($result->data))
: $result->data;
//设置摘要,设置_row_exists标记
$this->_fingerprint = md5($result);
$this->_row_exists = TRUE;
return $result;
}

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

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

//
public function write($session_id, $session_data)
{
// Prevent previous QB calls from messing with our queries
//查询之前都要进行复位。
$this->_db->reset_query();

// Was the ID regenerated?

//处理session_regenerate_id()函数调用
//这时参数传进来的新的session_id与原来对像中保存的session_id不一致
if ($session_id !== $this->_session_id)
{
//写记录之前要做两件事:
//1、首先释放锁。因为session_id变更了,所以要更新$session_id组合生成的字符串锁,原来的锁作废。
//2、用新的session_id重新加锁。
if ( ! $this->_release_lock() OR ! $this->_get_lock($session_id))
{
return $this->_failure;
}
//如果是传进来的session_id与之前对像属性保存的不一致,说明是session_regenerate_id
//那么对应的新ID的记录是不存在的,所以设置$this->_row_exists = FALSE;
$this->_row_exists = FALSE;
$this->_session_id = $session_id;
}
//新老sessionid是一致的情况
//因为session_start()时触发的read()函数会加锁,所以$this->_lock到这里一定是TRUE,
//如果是FALSE就说明当前的http请求在read时抢占锁失败,同名的sessionid锁已经被另一个http请求占有
elseif ($this->_lock === FALSE)
{
return $this->_failure;
}

//如果不存在session_id对应的记录,则新增记录,insert后并根据数据库返回的成功失败进行返回。
if ($this->_row_exists === FALSE)
{
$insert_data = array(
'id' => $session_id,
'ip_address' => $_SERVER['REMOTE_ADDR'],
'timestamp' => time(),
'data' => ($this->_platform === 'postgre' ? base64_encode($session_data) : $session_data)
);

if ($this->_db->insert($this->_config['save_path'], $insert_data))
{
$this->_fingerprint = md5($session_data);
$this->_row_exists = TRUE;
return $this->_success;
}

return $this->_failure;
}

//若存在session_id对应的记录,则更新$data和更新时间timestamp
$this->_db->where('id', $session_id);
if ($this->_config['match_ip'])
{
$this->_db->where('ip_address', $_SERVER['REMOTE_ADDR']);
}

$update_data = array('timestamp' => time());
if ($this->_fingerprint !== md5($session_data))
{
$update_data['data'] = ($this->_platform === 'postgre')
? base64_encode($session_data)
: $session_data;
}

if ($this->_db->update($this->_config['save_path'], $update_data))
{
$this->_fingerprint = md5($session_data);
return $this->_success;
}

return $this->_failure;
}

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

//close函数主要是释放字符锁
public function close()
{
//释放字符锁
return ($this->_lock && ! $this->_release_lock())
? $this->_failure
: $this->_success;
}

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

/**
* Destroy
*
* Destroys the current session.
*
* @param   string  $session_id    Session ID
* @return  bool
*/
public function destroy($session_id)
{
if ($this->_lock)
{
// Prevent previous QB calls from messing with our queries
//复位查询
$this->_db->reset_query();
//生成where条件
$this->_db->where('id', $session_id);
if ($this->_config['match_ip'])
{
$this->_db->where('ip_address', $_SERVER['REMOTE_ADDR']);
}
//删除表中对应的记录
if ( ! $this->_db->delete($this->_config['save_path']))
{
return $this->_failure;
}
}
//调用close()释放锁
if ($this->close() === $this->_success)
{
//清除客户端cookie
$this->_cookie_destroy();
return $this->_success;
}

return $this->_failure;
}

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

/**
* Garbage Collector
*
* Deletes expired sessions
*
* @param   int     $maxlifetime   Maximum lifetime of sessions
* @return  bool
*/
public function gc($maxlifetime)
{
// Prevent previous QB calls from messing with our queries
$this->_db->reset_query();
//根据timestamp以及session生存周期session.gc_maxlifetime,删除超时的记录
return ($this->_db->delete($this->_config['save_path'], 'timestamp < '.(time() - $maxlifetime)))
? $this->_success
: $this->_failure;
}

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

/**
* Get lock
*
* Acquires a lock, depending on the underlying platform.
*
* @param   string  $session_id    Session ID
* @return  bool
*/
//覆盖抽像类中的_get_lock方法
//该方法实现对当前session操作加锁
protected function _get_lock($session_id)
{
//mysql库
if ($this->_platform === 'mysql')
{
$arg = $session_id.($this->_config['match_ip'] ? '_'.$_SERVER['REMOTE_ADDR'] : '');
//采用数据库字符锁SELECT GET_LOCK('".$arg."', 300)
//在没有释放锁之前,对同一个字符串加锁就会失败返回false
if ($this->_db->query("SELECT GET_LOCK('".$arg."', 300) AS ci_session_lock")->row()->ci_session_lock)
{
//更新对像属性$this->_lock
$this->_lock = $arg;
return TRUE;
}
//走到这里说明同名字符串锁被占用,本次申请加锁失败。
return FALSE;
}
//postgre
elseif ($this->_platform === 'postgre')
{
$arg = "hashtext('".$session_id."')".($this->_config['match_ip'] ? ", hashtext('".$_SERVER['REMOTE_ADDR']."')" : '');
if ($this->_db->simple_query('SELECT pg_advisory_lock('.$arg.')'))
{
$this->_lock = $arg;
return TRUE;
}

return FALSE;
}
//如果不是上述两种类型库,则调用父函数返回。不过意义不大
return parent::_get_lock($session_id);
}

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

/**
* Release lock
*
* Releases a previously acquired lock
*
* @return  bool
*/
//覆盖抽像类中的_release_lock方法
//该方法实现对当前session操作解锁
//如果锁被释放,那么无论$this->_lock之前是什么值,这时都要置为FALSE。
protected function _release_lock()
{
//通过$this->_lock判断当前对像是否加锁
//如果当前对像没有锁,就立即返回
if ( ! $this->_lock)
{
return TRUE;
}

//Mysql下
if ($this->_platform === 'mysql')
{
//释放由$session_id组合生成的字符串锁
//注意在加锁时,$this->_lock保存了锁的字符串。
if ($this->_db->query("SELECT RELEASE_LOCK('".$this->_lock."') AS ci_session_lock")->row()->ci_session_lock)
{
$this->_lock = FALSE;
return TRUE;
}

return FALSE;
}
elseif ($this->_platform === 'postgre')
{
if ($this->_db->simple_query('SELECT pg_advisory_unlock('.$this->_lock.')'))
{
$this->_lock = FALSE;
return TRUE;
}

return FALSE;
}

return parent::_release_lock();
}

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