當前位置:首頁 > PHP教程 > php會話 > 列表

PHP 中 Session阻塞及垃圾回收 Redis共享Session

發布:smiling 來源: PHP粉絲網  添加日期:2018-10-28 20:15:27 瀏覽: 評論:0 

Session 和 Cookie 有什么關系

Cookie 也是由于 HTTP 無狀態的特點而產生的技術。也被用于保存訪問者的身份標識和一些數據。每次客戶端發起 HTTP 請求時,會將 Cookie 數據加到 HTTP header 中,提交給服務端。這樣服務端就可以根據 Cookie 的內容知道訪問者的信息了。

可以說,Session 和 Cookie 做著相似的事情,只是 Session 是將數據保存在服務端,通過客戶端提交來的 session_id 來獲取對應的數據;而 Cookie 是將數據保存在客戶端,每次發起請求時將數據提交給服務端的。

上面提到,session_id 可以通過 URL 或 cookie 來傳遞,由于 URL 的方式比 cookie 的方式更加不安全且使用不方便,所以一般是采用 cookie 來傳遞 session_id。

服務端生成 session_id,通過 HTTP 報文發送給客戶端(比如瀏覽器),客戶端收到后按指示創建保存著 session_id 的 cookie。cookie 是以 key/value 形式保存的,看上去大概就這個樣子的:

PHPSESSID=e4tqo2ajfbqqia9prm8t83b1f2

在 PHP 中,保存 session_id 的 cookie 名稱默認叫作 PHPSESSID,這個名稱可以通過 php.ini 中 session.name 來修改,也可以通過函數 session_name() 來修改。

為什么不推薦使用 PHP 自帶的 files 型 Session 處理器

在 PHP 中,默認的 Session 處理器是 files,處理器可以用戶自己實現(參見:自定義會話管理器)。我知道的成熟的 Session 處理器還有很多:Redis、Memcached、MongoDB……為什么不推薦使用 PHP 自帶的 files 類型處理器,PHP 官方手冊中給出過這樣一段 Note:

無論是通過調用函數 session_start() 手動開啟會話, 還是使用配置項 session.auto_start 自動開啟會話, 對于基于文件的會話數據保存(PHP 的默認行為)而言, 在會話開始的時候都會給會話數據文件加鎖, 直到 PHP 腳本執行完畢或者顯式調用 session_write_close() 來保存會話數據。 在此期間,其他腳本不可以訪問同一個會話數據文件。

上述引用參見:Session 的基本用法

為了證明這段話,我們創建一下 2 個文件:

文件:session1.php

  1. <?php 
  2. session_start(); 
  3. sleep(5); 
  4. var_dump($_SESSION); 
  5. ?> 

文件:session2.php

  1. <?php 
  2. session_start(); 
  3. var_dump($_SESSION); 
  4. ?> 

在同一個瀏覽器中,先訪問 http://127.0.0.1/session1.php,然后在當前瀏覽器新的標簽頁立刻訪問 http://127.0.0.1/session2.php。實驗發現,session1.php 等了 5 秒鐘才有輸出,而 session2.php 也等到了將近 5 秒才有輸出。而單獨訪問 session2.php 是秒開的。在一個瀏覽器中訪問 session1.php,然后立刻在另外一個瀏覽器中訪問 session2.php。結果是 session1.php 等待 5 秒鐘有輸出,而 session2.php 是秒開的。

分析一下造成這個現象的原因:上面例子中,默認使用 Cookie 來傳遞 session_id,而且 Cookie 的作用域是相同。這樣,在同一個瀏覽器中訪問這 2 個地址,提交給服務器的 session_id 就是相同的(這樣才能標記訪問者,這是我們期望的效果)。當訪問 session1.php 時,PHP 根據提交的 session_id,在服務器保存 Session 文件的路徑(默認為 /tmp,通過 php.ini 中的 session.save_path 或者函數 session_save_path() 來修改)中找到了對應的 Session 文件,并對其加鎖。如果不顯式調用 session_write_close(),那么直到當前 PHP 腳本執行完畢才會釋放文件鎖。如果在腳本中有比較耗時的操作(比如例子中的 sleep(5)),那么另一個持有相同 session_id 的請求由于文件被鎖,所以只能被迫等待,于是就發生了請求阻塞的情況。

既然如此,在使用完 Session 后,立刻顯示調用 session_write_close() 是不是就解決問題了哩?比如上面例子中,在 sleep(5) 前面調用 session_write_close()。

確實,這樣 session2.php 就不會被 session1.php 所阻塞。但是,顯示調用了 session_write_close() 就意味著將數據寫到文件中并結束當前會話。那么,在后面代碼中要使用 Session 時,必須重新調用 session_start()。

例如:

  1. <?php 
  2. session_start(); 
  3. $_SESSION['name'] = 'Jing'
  4. var_dump($_SESSION); 
  5. session_write_close(); 
  6. sleep(5); 
  7. session_start(); 
  8. $_SESSION['name'] = 'Mr.Jing'
  9. var_dump($_SESSION); 
  10. ?> 

官方給出的方案:

對于大量使用 Ajax 或者并發請求的網站而言,這可能是一個嚴重的問題。 解決這個問題最簡單的做法是如果修改了會話中的變量, 那么應該盡快調用 session_write_close() 來保存會話數據并釋放文件鎖。 還有一種選擇就是使用支持并發操作的會話保存管理器來替代文件會話保存管理器。

我推薦的方式是使用 Redis 作為 Session 的處理器。

Session 數據是什么時候被刪除的,這是一道經常被面試官問起的問題。

先看看官方手冊中的說明:

session.gc_maxlifetime 指定過了多少秒之后數據就會被視為"垃圾"并被清除。 垃圾搜集可能會在 session 啟動的時候開始( 取決于 session.gc_probability 和 session.gc_divisor)。 session.gc_probability 與 session.gc_divisor 合起來用來管理 gc(garbage collection 垃圾回收)進程啟動的概率。此概率用 gc_probability/gc_divisor 計算得來。例如 1/100 意味著在每個請求中有 1% 的概率啟動 gc 進程。session.gc_probability 默認為 1,session.gc_divisor 默認為 100。

繼續用我上面那個不太恰當的比方吧:如果我們把物品放在超市的儲物箱中而不取走,過了很久(比如一個月),那么保安就要清理這些儲物箱中的物品了。當然并不是超過期限了保安就一定會來清理,也許他懶,又或者他壓根就沒有想起來這件事情。

再看看兩段手冊的引用:

如果使用默認的基于文件的會話處理器,則文件系統必須保持跟蹤訪問時間(atime)。Windows FAT 文件系統不行,因此如果必須使用 FAT 文件系統或者其他不能跟蹤 atime 的文件系統,那就不得不想別的辦法來處理會話數據的垃圾回收。自 PHP 4.2.3 起用 mtime(修改時間)來代替了 atime。因此對于不能跟蹤 atime 的文件系統也沒問題了。

GC 的運行時機并不是精準的,帶有一定的或然性,所以這個設置項并不能確保舊的會話數據被刪除。某些會話存儲處理模塊不使用此設置項。

對于這種刪除機制,我是存疑的。

比如 gc_probability/gc_divisor 設置得比較大,或者網站的請求量比較大,那么 GC 進程啟動就會比較頻繁。

還有,GC 進程啟動后都需要遍歷 Session 文件列表,對比文件的修改時間和服務端的當前時間,判斷文件是否過期而決定是否刪除文件。

這也是我覺得不應該使用 PHP 自帶的 files 型 Session 處理器的原因。而 Redis 或 Memcached 天生就支持 key/value 過期機制的,用于作為會話處理器很合適。或者自己實現一個基于文件的處理器,當根據 session_id 獲取對應的單個 Session 文件時判斷文件是否過期。

為什么重啟瀏覽器后 Session 數據就取不到了

session.cookie_lifetime 以秒數指定了發送到瀏覽器的 cookie 的生命周期。值為 0 表示"直到關閉瀏覽器"。默認為 0。

其實,并不是 Session 數據被刪除(也有可能是,概率比較小,參見上一節)。只是關閉瀏覽器時,保存 session_id 的 Cookie 沒有了。也就是你弄丟了打開超市儲物箱的鑰匙(session_id)。

同理,瀏覽器 Cookie 被手動清除或者其他軟件清除也會造成這個結果。

為什么瀏覽器開著,我很久沒有操作就被登出了,這個是稱為“防呆”,為了保護用戶賬戶安全的。

這個小節放進來,是因為這個功能的實現可能和 Session 的刪除機制有關(之所以說是可能,是因為這個功能不一定要借住 Session 實現,用 Cookie 也同樣可以實現)。

說簡單一點,就是長時間沒有操作,服務端的 Session 文件過期被刪除了。

一個有意思的事情

在我試驗的過程中,發現了小有意思的事情:我把 GC 啟動的概率設置為 100%。如果只有一個訪問者請求,該訪問者即使過了很久(超過了過期時間)后才發起第二次請求,那么 Session 數據也還是存在的('session.save_path' 目錄下面的 Session 文件存在)。是的,明明就超過了過期時間,卻沒有被 GC 刪除。這時,我用另外一個瀏覽器訪問時(相對于另一個訪問者),這次請求生成了新的 Session 文件,而上一個瀏覽器請求生成的那個 Session 文件終于沒有了(之前那個 Session 文件在 'session.save_path' 目錄下面的消失了)。

還有,發現 Session 文件被刪除后,再次請求,還是會生成和之前文件名相同的 Session 文件(因為瀏覽器并沒有關閉,再次請求發送的 session_id 是相同的,所以重新生成的 Session 文件的文件名還是一樣的)。但是,我不理解的是:這個重新出現的文件的創建時間竟然是第一次的那個創建時間,難道它是從回收站中回來的?(確實,我做這個試驗時是在 window 下進行的)

我猜測的原因是這樣:當啟動會話后,PHP 根據 session_id 找到并打開了對應的 Session 文件,然后才啟動 GC 進程。GC 進程就只檢查除了當前這個 Session 文件外的其他文件,發現過期的就干掉。所有,即使當前這個 Session 文件已經過期了,GC 也沒有刪除它。

我認為這個不合理的。

由于發生這種情況影響也不大(畢竟線上請求很多,當前請求的過期文件被其他請求喚起的 GC 干掉的可能性是比較大的),我沒有信心去看 PHP 源代碼,我并不在線上使用 PHP 自帶的 files 型 Session 處理器。所以,這個問題我就沒有深入研究了,請諒解。

  1. <?php 
  2. // 過期時間設置為 30 秒 
  3. ini_set('session.gc_maxlifetime''30'); 
  4. // GC 啟動概率設置為 100% 
  5. ini_set('session.gc_probability''100'); 
  6. ini_set('session.gc_divisor''100'); 
  7. session_start(); 
  8. $_SESSION['name'] = 'Jing'
  9. var_dump($_SESSION); 
  10. ?> 

如何設置一個嚴格30分鐘過期的Session

第一種回答

那么, 最常見的一種回答是: 設置Session的過期時間, 也就是session.gc_maxlifetime, 這種回答是不正確的, 原因如下:

1. 首先, 這個PHP是用一定的概率來運行session的gc的, 也就是session.gc_probability和session.gc_divisor(介紹參看 深入理解PHP原理之Session Gc的一個小概率Notice), 這個默認的值分別是1和100, 也就是有1%的機會, PHP會在一個Session啟動時, 運行Session gc. 不能保證到30分鐘的時候一定會過期.

2. 那設置一個大概率的清理機會呢? 還是不妥, 為什么? 因為PHP使用stat Session文件的修改時間來判斷是否過期, 如果增大這個概率一來會降低性能, 二來, PHP使用”一個”文件來保存和一個會話相關的Session變量, 假設我5分鐘前設置了一個a=1的Session變量, 5分鐘后又設置了一個b=2的Seesion變量, 那么這個Session文件的修改時間為添加b時刻的時間, 那么a就不能在30分鐘的時候, 被清理了. 另外還有下面第三個原因.

3. PHP默認的(Linux為例), 是使用/tmp 作為Session的默認存儲目錄, 并且手冊中也有如下的描述:

Note: 如果不同的腳本具有不同的 session.gc_maxlifetime 數值但是共享了同一個地方存儲會話數據,則具有最小數值的腳本會清理數據。此情況下,與 session.save_path 一起使用本指令。

也就是說, 如果有倆個應用都沒有指定自己獨立的save_path, 一個設置了過期時間為2分鐘(假設為A), 一個設置為30分鐘(假設為B), 那么每次當A的Session gc運行的時候, 就會同時刪除屬于應用B的Session files.

所以, 第一種答案是不”完全嚴格”正確的.

第二種答案

還有一種常見的答案是: 設置Session ID的載體, Cookie的過期時間, 也就是session.cookie_lifetime. 這種回答也是不正確的, 原因如下:

這個過期只是Cookie過期, 換個說法這點就考察Cookie和Session的區別, Session過期是服務器過期, 而Cookie過期是客戶端(瀏覽器)來保證的, 即使你設置了Cookie過期, 這個只能保證標準瀏覽器到期的時候, 不會發送這個Cookie(包含著Session ID), 而如果通過構造請求, 還是可以使用這個Session ID的值.

第三種答案

使用memcache, redis等, okey, 這種答案是一種正確答案. 不過, 很顯然出題者肯定還會接著問你, 如果只是使用PHP呢?

第四種答案

當然, 面試不是為了難道你, 而是為了考察思考的周密性. 在這個過程中我會提示出這些陷阱, 所以一般來說, 符合題意的做法是:

1. 設置Cookie過期時間30分鐘, 并設置Session的lifetime也為30分鐘.

2. 自己為每一個Session值增加Time stamp.

3. 每次訪問之前, 判斷時間戳.

最后, 有同學問, 為什么要設置30分鐘的過期時間: 這個, 首先這是為了面試, 第二, 實際使用場景的話, 比如30分鐘就過期的優惠??

為什么不能用memcached存儲Session

Titas Norkūnas是DevOps咨詢服務提供商Bear Mountain的聯合創始人。由于看到Ruby/Rails社區忽略了Dormando那兩篇文章所指出的問題,所以他近日撰文對此進行了進一步的闡述。他認為問題的根本在于,memcached是一個設計用于緩存數據而不是存儲數據的系統,因此不應該用于存儲Session。

對于Dormando的那兩篇文章,他認為第一篇文章給出的原因很容易理解,而人們經常會對第二篇文章給出的原因認識不足。因此他對這個原因進行了詳細地闡述:

Memcached使用“最近最少使用(LRU)”算法回收緩存。但memcached的LRU算法針對每個slab類執行,而不是針對整體。

這意味著,如果所有Session的大小大致相同,那么它們會分成兩三個slab類。所有其它大小大致相同的數據也會放入同一些slab,與Session爭用存儲空間。一旦slab滿了,即使更大的slab中還有空間,數據也會被回收,而不是放入更大的slab中……在特定的slab中,Session最老的用戶將會掉線。用戶將會開始隨機掉線,而最糟糕的是,你很可能甚至都不會注意到它,直至用戶開始抱怨……

另外,Norkūnas提到,如果Session中增加了新數據,那么Session變大也可能會導致掉線問題出現。

有人提出將Session和其它數據分別使用單獨的memcached緩存。不過,由于memcached的LRU算法是局部的,那種方式不僅導致內存使用率不高,而且也無法消除用戶因為Session回收而出現隨機掉線的風險。

如果讀者非常希望借助memcached提高Session讀取速度,那么可以借鑒Norkūnas提出的memcached+RDBMS(在有些情況下,NoSQL也可以)的模式:

當用戶登錄時,將Session “set”到memcached,并寫入數據庫;

在Session中增加一個字段,標識Session最后寫入數據庫的時間;

每個頁面加載的時候,優先從memcached讀取Session,其次從數據庫讀取;

每加載N頁或者Y分鐘后,再次將Session寫入數據庫;

從數據庫中獲取過期Session,優先從memcached中獲取最新數據。

Tags: Session Redis

分享到:

广西快3最大遗漏值统计表