CodeIgniter架構源碼筆記(15)——SESSION之Redis驅動實現__session

來源:互聯網
上載者:User
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;    }}
相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.