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

配置选项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();
    }

}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值