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