天天看點

《Redis實戰》一2.1 登入和cookie緩存

本節書摘來異步社群《redis實戰》一書中的第2章,第2.1節,作者: 【美】josiah l. carlson(約西亞 l.卡爾森)譯者: 黃健宏 責編: 楊海玲,更多章節内容可以通路雲栖社群“異步社群”公衆号檢視。

每當我們登入網際網路服務(比如銀行賬戶或者電子郵件)的時候,這些服務都會使用cookie來記錄我們的身份。cookie由少量資料組成,網站會要求我們的浏覽器存儲這些資料,并且在每次服務送出請求時再将這些資料傳回給服務。對于用來登入的cookie,有兩種常見的方法可以将登入資訊存儲在cookie裡面:一種是簽名(signed)cookie,另一種是令牌(token)cookie。

簽名cookie通常會存儲使用者名,可能還有使用者id、使用者最後一次成功登入的時間,以及網站覺得有用的其他任何資訊。除了使用者的相關資訊之外,簽名cookie還包含一個簽名,伺服器可以使用這個簽名來驗證浏覽器發送的資訊是否未經改動(比如将cookie中的登入使用者名改成另一個使用者)。

令牌 cookie會在cookie裡面存儲一串随機位元組作為令牌,伺服器可以根據令牌在資料庫中查找令牌的擁有者。随着時間的推移,舊令牌會被新令牌取代。表2-1展示了簽名cookie和令牌cookie的優點與缺點。

《Redis實戰》一2.1 登入和cookie緩存

因為fake web retailer沒有實作簽名cookie的需求,是以我們選擇了使用令牌cookie來引用關系資料庫表中負責存儲使用者登入資訊的條目(entry)。除了使用者登入資訊之外,fake web retailer還可以将使用者的通路時長和已浏覽商品的數量等資訊存儲到資料庫裡面,這樣便于将來通過分析這些資訊來學習如何更好地向使用者推銷商品。

一般來說,使用者在決定購買某個或某些商品之前,通常都會先浏覽多個不同的商品,而記錄使用者浏覽過的所有商品以及使用者最後一次通路頁面的時間等資訊,通常會導緻大量的資料庫寫入。從長遠來看,使用者的這些浏覽資料的确非常有用,但問題在于,即使經過優化,大多數關系資料庫在每台資料庫伺服器上面每秒也隻能插入、更新或者删除200~2000 個資料庫行。盡管批量插入、批量更新和批量删除等操作可以以更快的速度執行,但因為用戶端每次浏覽網頁都隻更新少數幾個行,是以高速的批量插入在這裡并不适用。

因為fake web retailer目前一天的負載量相對比較大——平均情況下每秒大約1200次寫入,高峰時期每秒接近6000次寫入,是以它必須部署10台關系資料庫伺服器才能應對高峰時期的負載量。而我們要做的就是使用redis重新實作登入cookie功能,取代目前由關系資料庫實作的登入cookie功能。

首先,我們将使用一個散列來存儲登入cookie令牌與已登入使用者之間的映射。要檢查一個使用者是否已經登入,需要根據給定的令牌來查找與之對應的使用者,并在使用者已經登入的情況下,傳回該使用者的id。代碼清單2-1展示了檢查登入cookie的方法。

代碼清單2-1 check_token()函數

《Redis實戰》一2.1 登入和cookie緩存

對令牌進行檢查并不困難,因為大部分複雜的工作都是在更新令牌時完成的:使用者每次浏覽頁面的時候,程式都會對使用者存儲在登入散列裡面的資訊進行更新,并将使用者的令牌和目前時間戳添加到記錄最近登入使用者的有序集合裡面;如果使用者正在浏覽的是一個商品頁面,那麼程式還會将這個商品添加到記錄這個使用者最近浏覽過的商品的有序集合裡面,并在被記錄商品的數量超過25個時,對這個有序集合進行修剪。代碼清單2-2展示了程式更新令牌的方法。

代碼清單2-2 update_token()函數

《Redis實戰》一2.1 登入和cookie緩存

通過update_token()函數,我們可以記錄使用者最後一次浏覽商品的時間以及使用者最近浏覽了哪些商品。在一台最近幾年生産的伺服器上面,使用update_token()函數每秒至少可以記錄20 000件商品,這比fake web retailer高峰時期所需的6000次寫入要高3倍有餘。不僅如此,通過後面介紹的一些方法,我們還可以進一步優化update_token()函數的運作速度。但即使是現在這個版本的update_token()函數,比起原來的關系資料庫,性能也已經提升了10~100倍。

因為存儲會話資料所需的記憶體會随着時間的推移而不斷增加,是以我們需要定期清理舊的會話資料。為了限制會話資料的數量,我們決定隻儲存最新的1000萬個會話。①清理舊會話的程式由一個循環構成,這個循環每次執行的時候,都會檢查存儲最近登入令牌的有序集合的大小,如果有序集合的大小超過了限制,那麼程式就會從有序集合裡面移除最多100個最舊的令牌,并從記錄使用者登入資訊的散列裡面,移除被删除令牌對應的使用者的資訊,并對存儲了這些使用者最近浏覽商品記錄的有序集合進行清理。與此相反,如果令牌的數量未超過限制,那麼程式會先休眠1秒,之後再重新進行檢查。代碼清單2-3展示了清理舊會話程式的具體代碼。

代碼清單2-3 clean_sessions()函數

《Redis實戰》一2.1 登入和cookie緩存

讓我們通過計算來了解一下,這段簡單的代碼為什麼能夠妥善地處理每天500萬人次的通路:假設網站每天有500萬使用者通路,并且每天的使用者都和之前的不一樣,那麼隻需要兩天,令牌的數量就會達到1000萬個的上限,并将網站的記憶體空間消耗殆盡。因為一天有24×3600=86 400秒,而網站平均每秒産生5 000 000/86 400<58個新會話,如果清理函數和我們之前在代碼裡面定義的一樣,以每秒一次的頻率運作的話,那麼它每秒需要清理将近60個令牌,才能防止令牌數量過多的問題發生。但是實際上,我們定義的令牌清理函數在通過網絡來運作時,每秒能夠清理10 000多個令牌,在本地運作時,每秒能夠清理60 000多個令牌,這比所需的清理速度快了150~1000倍,是以因為舊令牌過多而導緻網站空間耗盡的問題不會出現。

在哪裡執行清理函數? 本書會包含一些類似代碼清單2-3的清理函數,它們可能會像代碼清單2-3那樣,以守護程序的方式來運作,也可能會作為定期作業(cron job)每隔一段時間運作一次,甚至在每次執行某個操作時運作一次(例如,6.3節就在一個擷取鎖操作裡面包含了一個清理操作)。一般來說,本書中包含while not quit:代碼的函數都應該作為守護程序來執行,不過如果有需要的話,也可以把它們改成周期性地運作。

redis 的過期資料處理 随着對redis的了解逐漸加深,讀者應該會慢慢發現本書展示的一些解決方案有時候并不是問題的唯一解決辦法。比如對于這個登入cookie例子來說,我們可以直接将登入使用者和令牌的資訊存儲到字元串鍵值對裡面,然後使用redis的expire指令,為這個字元串和記錄使用者商品浏覽記錄的有序集合設定過期時間,讓redis在一段時間之後自動删除它們,這樣就不需要再使用有序集合來記錄最近出現的令牌了。但是這樣一來,我們就沒有辦法将會話的數量限制在1000萬之内了,并且在将來有需要的時候,我們也沒辦法在會話過期之後對被廢棄的購物車進行分析了。

熟悉多線程程式設計或者并發程式設計的讀者可能會發現代碼清單2-3展示的清理函數實際上包含一個競争條件(race condition):如果清理函數正在删除某個使用者的資訊,而這個使用者又在同一時間通路網站的話,那麼競争條件就會導緻使用者的資訊被錯誤地删除。目前來看,這個競争條件除了會使得使用者需要重新登入一次之外,并不會對程式記錄的資料産生明顯的影響,是以我們暫時先擱置這個問題,之後的第3章和第4章會說明怎樣防止類似的競争條件發生,并進一步加快清理函數的執行速度。

通過使用redis來記錄使用者資訊,我們成功地将每天要對資料庫執行的行寫入操作減少了數百萬次。雖然這非常的了不起,但這隻是我們使用redis建構web應用程式的第一步,接下來的一節将向讀者們展示如何使用redis來處理另一種類型的cookie。