上一篇文章: Python--Redis實戰:第一章:初識Redis:第三節:你好Redis-文章投票試煉
下一篇文章: Python--Redis實戰:第二章:使用Redis構建Web應用:第二節:使用Redis實現購物車
從高層次的角度來看,Web應用就是經過HTTP協議對網頁瀏覽器發送的請求進行響應的服務器或者服務【service】。一個Web服務器對請求進行響應的典型步驟以下:數據庫
以上列舉的5個步驟從高層次的角度展現了典型Web服務器的運做方式,這種狀況下的Web請求被認爲是無狀態的【stateless】,也就是說,服務器自己不會記錄與過往有關的任何信息,這使得失效【fail】的服務器能夠很容易地被替換掉。有很多書籍專門介紹瞭如何優化響應過程的各個步驟,本章要作的事情也相似,不一樣之處是,咱們將介紹如何使用更快的Redis查詢來替代傳統的關係數據庫查詢,已經如何使用Redis來完場一些使用關係數據庫沒有辦法高效完場的任務。編程
本章的全部內容都是圍繞着發現並解決【Fake Web Retailer】這個虛構的大型網上商店來展開的,這個商店天天都會有大約500萬名不一樣的用戶,這些用戶會給網站帶來一億次點擊,並從網站購買超過10萬件商品。咱們之因此將這幾個數據量設置的特別大,是考慮【若是能夠在大數據背景下順利解決問題,那麼解決小數據量和中等數據量引起的問題就更不在話下】。segmentfault
每當咱們登陸互聯網服務的時候,這些服務都會使用cookie來記錄咱們的身份。cookie由少許數據組成,網站會要求咱們的瀏覽器存儲這些數據,並在每次服務器請求發送時將這些數據傳回給服務器。對於用來登陸的cookie,有兩種常見的方式能夠將登陸信息存儲在cookie裏面:瀏覽器
簽名cookie一般會存儲用戶名,可能還有用戶ID,用戶最後一次登陸成功的時間,以及網站以爲有用的其餘任何信息。除了用戶的相關信息以外,簽名cookie還包含了一個簽名,服務器可使用這個簽名來驗證瀏覽器發送的消息是否未經改動(好比將cookie中的登陸用戶名改爲另外一個用戶)。緩存
令牌cookie會在cookie裏面存儲一串隨機字節做爲令牌,服務器能夠根據令牌在數據庫中查詢令牌的擁有者。隨着時間的推移,舊令牌會被新令牌去掉。安全
cookie類型 | 優勢 | 缺點 |
---|---|---|
簽名cookie | 驗證cookie所需的一切信息都存儲在cookie裏面,cookie能夠包含額外的信息,而且對這些信息進行簽名也很容易。 | 正確的處理簽名很難,很容易忘記對數據進行簽名,或者忘記驗證數據的簽名,從而形成安全漏洞。 |
令牌cookie | 添加信息很是容易,cookie的體積很是小,所以移動終端和速度較慢的客戶端能夠更快地發送請求。 | 須要在服務中存儲更多信息,若是使用的是關係數據庫,那麼載入和存儲的cookie的代價可能會很高。 |
此次咱們使用令牌cookie來引用關係數據庫表中負責存儲用戶登陸信息的條目【entry】。除了用戶登陸信息以外,咱們還能夠將用戶的訪問時長和已瀏覽商品的數量等信息存儲到數據庫裏面,這樣便於未來經過分析這些信息來學習若是更好得向用戶推銷商品。服務器
通常來講,用戶在決定購買某個或某些商品以前,一般都會先瀏覽多個不一樣商品,而記錄用戶瀏覽過的全部商品以及用戶最後一次訪問頁面的時間等信息,一般會致使大量的數據庫寫入。從長遠來看,用戶的這些瀏覽數據的確很是有用,但問題是,即使通過優化,大多數關係數據庫在每臺數據庫服務器上每秒也只能插入、更細或者刪除200~2000個數據行。儘可能批量插入、批量更新和批量刪除等操做能夠更快地速度執行,但由於客戶端每次瀏覽網頁都只更新少數幾行,因此高速的批量插入在這裏並不適用。cookie
咱們假設咱們的網站天天的負載量都比較大:平均每秒大約1200次寫入,高峯時期每秒接近6000次寫入,因此它必須部署10臺關係數據服務器才能應對高峯時期的負載量。而咱們要作的就是適用Redis從新實現登陸cookie功能,取代由關係數據庫實現的登陸cookie功能。網絡
首先,咱們將使用一個散列來存儲登陸cookie令牌和已登陸用戶之間的映射。要檢查一個用戶是否已經登陸,須要根據給定的令牌來查找與之對應的用戶,並在用戶已經登陸的狀況下,返回該用戶的ID。session
def check_token(conn,token): #嘗試獲取並返回令牌對應的用戶 return conn.hget('login:',token)
對令牌進行檢查並不困難,由於大部分複雜的工做都是在更新令牌時完成的:用戶每次瀏覽頁面時,程序都會對用戶存儲在登陸散列裏面的信息進行更新,並將用戶的令牌和當前時間戳添加到記錄最近登陸用戶的有序集合裏面;若是用戶正在瀏覽的是一個商品頁面,那麼程序還會將這個商品添加到記錄這個用戶最近瀏覽過的商品的有序集合裏面,並在被記錄商品的數據超過25個時,對這個有序集合進行修建。
#更新令牌 import time def update_token(conn,token,user,item=None): timestamp=time.time() #h獲取當前時間戳 conn.hset('login:',token,user) #維持令牌與已登錄用戶之間的映射 conn.zadd('recent:',token,timestamp) #記錄領哦哎最後一次出現的時間 if item: conn.zadd('viewed:'+token,item,timestamp) #記錄用戶瀏覽郭的商品 conn.zremrangebyrank('viewed:'+token,0,-26) #移除舊的記錄,值保留用戶最近瀏覽過的25個商品
經過update_token()
函數,咱們能夠記錄用戶最後一次瀏覽商品的時間以及用戶最近瀏覽了哪些商品。在一臺最近幾年生產的服務器上面,使用update_token()
函數每秒至少記錄20000件商品,這比咱們預估的網站高峯期所需的6000次寫入要高3倍有餘。不只如此,經過後面介紹的一些方法,咱們還能夠進一步優化update_token()
函數的運行速度。但在優化前,性能也比原有的關係數據庫性能提高了10~100倍。
由於存儲會話數據所需的內存會隨着時間的推移而不斷增長,因此咱們須要按期清理舊的會話數據,爲了限制會話數據的數量,咱們決定只保留最新的1000萬個會話。清理舊會話的程序由一個循環構成,這個循環每次執行的時候,都會檢查存儲最新登陸令牌的有序集合大小,若是有序集合的大小超過了限制,那麼程序就會從有序集合裏面移除最多100個最舊的令牌,並從記錄用戶登陸頁面的散列表裏面,移除被刪除令牌對應的用戶的信息,並對存儲了這些用戶最近瀏覽商品記錄的有序集合進行清理。若是令牌的數量未超過限制,那麼程序會休眠1秒,以後再從新進行檢查。
#清理舊會話 import time QUIT=False LIMIT=10,000,000 def clean_sessions(conn): while not QUIT: #目前已有令牌的數量 size=conn.zcard('recent:') if size<=LIMIT: #令牌數量未超過限制,休眠1秒後再從新檢查 time.sleep(1) continue end_index=min(size-LIMIT,100) tokens=conn.zrange('recent:',0,end_index-1) session_keys=[] #爲那些將要刪除的令牌構建鍵名 for token in tokens: session_keys.append('viewed:'+token) #移除最舊的那些令牌 conn.delete(*session_keys) conn.hdel('login:',*tokens) conn.zrem('recent:',*tokens)
讓咱們經過計算來了解一下,這段簡短的代碼爲何可以妥善地處理天天500萬人次的訪問:假設網站天天有500萬用戶訪問,而且天天的用戶都和以前的不同,那麼只須要兩天,令牌的數量就會達到1000萬上限,並將網站的內存空間銷燬殆盡,由於一天有:24*3600=86400秒,而網站平均每秒產生5 000 000/86400<58個新會話,若是清理函數以每秒的頻率運行,那麼它每秒須要清理將近60個令牌,才能防止令牌的數量過多的問題發生。可是實際上,咱們定義的令牌清理函數在經過網絡來運行時,每秒可以清理10 000多個令牌,在本地運行時,每秒可以清理60 000多個令牌,這比所需的清理速度快樂150~1000倍,因此由於舊令牌過多而致使網站空間耗盡的問題不會出現。
熟悉多線程編程或者併發編程的讀者可能會發現上面的清理函數包含了一個競爭條件【race condition】:若是清理函數正在刪除某個用戶的信息,而這個用戶又在同一時間訪問網站的話,那麼競爭條件就會致使用戶的信息被錯誤的刪除。目前來看,這個競爭條件除了會使得用戶須要從新登陸一次以外,並不會對程序記錄的數據產生明顯的影響,因此咱們暫時擱置這個問題,以後會講解防止相似的競爭條件發生的方法。
經過使用Redis來記錄用戶信息,咱們成功地將天天要對數據庫執行的行寫入操做減小了數百萬次。雖然這很是的了不得,但這只是咱們使用Redis構建Web應用程序的第一步,接下來咱們將展現如何使用Redis來處理另外一種類型的cookie。
上一篇文章: Python--Redis實戰:第一章:初識Redis:第三節:你好Redis-文章投票試煉
下一篇文章: Python--Redis實戰:第二章:使用Redis構建Web應用:第二節:使用Redis實現購物車