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

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;
    }

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值