【Flask/React】本人BLOG的緩存實現
最近惡補了HTTP協議原理,發現一年前所做的前後端整個系統的緩存協議思路還是對的,這本質上是一套帶有新鮮度檢測的被動緩存機制,所以放上來或許也沒什麼。想來這些思想本來也很多年沒變過了,現在很多工程化的很多都是經典思想的延伸罷了(當年《計算機網路》這門課還是學到了些皮毛的www至少應用層->傳輸層->網路層->數據鏈路層->物理層的每部分還大概記得(至今記得當年做的數據鏈路層的數據包包轉發FIFO的FPGA模塊(逃
在我的BLOG上觀看獲取最佳體驗。
承接上篇【React/Redux】深入理解React服務端渲染,由於React服務端渲染帶來的開銷,在內存尚可的狀況下使用緩存是個不錯的選擇,而對於這個項目,由於前後端是分離的,所以需要在Node這邊和Flask那邊來協作完成最完備的緩存,本片文章就將記錄一下我的思路。
前端架構在前面兩篇文章均已說清,而後端架構則和這篇差不多(【Flask/React/MongoDB】BlogReworkIII-如何搭建一個動態Blog),沒怎麼改過。
問題
由於前端伺服器實現了服務端渲染,考慮這個博客的文章修改不會很頻繁,其修改的頻率遠遠小於訪問頻率,所以用Cache
來減少這個渲染的開銷是合適的。為了設計這個Cache,需要明確現在前後端的設計下一次客戶端請求的完整響應流程:
客戶端發起請求,服務端開始渲染,進入相應組件的生命周期並向後端發起ajax請求,後端響應後進行下一步操作。由於前後端是分離的,Blog內容管理的核心在後端,所以核心就在於判定後端返回什麼狀態時從Cache中拿東西,在什麼狀態時修改Cache,這就要求後端提供給前端一個約定的協議。
後端
在本Blog項目中,Cache這個方面,後端需要提供給前端的實際上是一個狀態,這個狀態表明前端向後端請求的數據是否發生了改動。一個顯而易見的方法是由後端先緩存所喲已經被請求過的數據,在下次請求時再拿從資料庫中得到的新數據來對比,根據比對的結果返回一個狀態碼(比如304),這樣後端響應前端的結果可以表述為:
{ content: {......}, code: 200(304)}
注意這個200和304雖然和HTTP標準中的狀態碼值一致,但卻是兩個東西,在兩種狀態下它們返回的狀態都是200,只不過數據中有個code
欄位來標示狀態。
這種做法很直觀,但每次響應除了原先的資料庫查詢操作外,還要多一步對象對比的過程,這顯然是不合適的,而且在這種設計下後端的這個Cache也失去了本來的意義,必須有其他的方案來解決這個問題。這裡可以回顧一下後端的實現:
可以發現,資料庫只會在程序監聽pages
文件夾內文件的改動後才會發生,並且這個改動事件是可以被程序捕獲到的,這就提供了另一個解決方案——同時維護一個cache
和與其對應的state
,cache中存儲響應內容,state中存儲改內容對應的修改狀態,而這個state中的內容除了初始化之外,只能伴隨文章改動的事件而修改。
在這個思想的基礎上,一個抽象的Cache和其成員便可以被構造出來:
class WebCache(): def __init__(self): self._cache = {} self._state = {} @property def flag(self): ...... def updateContent(self, parameters, content): ...... def modifyState(self, parameters): ..... def get(self, parameters): ..... def delete(self, parameters): ..... def has(self, parameters): ...... def is_modified(self, parameters): ......
這是一個Cache的基類,它的作用和後端架構中其他基類基本一致(詳見後端設計的那篇文章),它提供了一系列方法來維護內部的_cache
和_state
,其中updateContent
、modifyState
、delete
是維護它們的核心方法,其他方法則負責在請求到來時判定狀態和取值,由此,整個設計便完成如下:
文本解釋如下:
- 當前端請求到來時,如果cache中沒有請求對應的內容,則從資料庫查出內容並調用
updateContent
放入緩存,此時_cache
將被更新,同時_state
中的對應的值將被設為False
,表明未被修改,並直接返回一個code
為200的響應。 - 當前端請求到來時,如果cache中擁有對應的內容,並且
_state
中對應狀態為False
(表明未被修改),則直接返回一個code
為304的響應。 - 當前端請求到來時,如果cache中擁有對應的內容,並且
_state
中對應狀態為True
(表明已被修改),則重複1。 - 當文件監控器發現文件被更改、並進行到寫入資料庫這一步時,直接根據當前寫入的條目判斷該條目是否在cache中,如果在,則調用
modifyState
方法將_state
中對應的值設為True
。
如此,後端的緩存以及和前端的協議便已完成。
前端
前端伺服器的Cache結構和後端基本一致,但也有一些不同,最明顯在於前端的請求會有分頁這個參數而後端沒有,並且Redux中維護的狀態和最終渲染的頁面也並非對應,比如:
在文章列表、也就是/category/Skill/0
這種頁面中,其對應Redux的store中的state是一個維護著list
和payload
的變數,list
存儲著所有的文章,payload
負責分頁,由於Blog自身的性質,我設計的是在一次用於訪問中只會獲取一次list
,分頁通過設置payload
中的page
加之組件渲染邏輯來實現(本質上就是數組切片)。
在這種情況下,如果兩次請求的路由中分類都是一致的,只是分頁不同,那麼將其作為兩種請求來多做一次服務端渲染顯然是不合適的,因為我們完全可以利用第一次請求的list
、加之對payload
的修改來直接進行二次渲染,而不必再走一次狀態的初始化,這也就要求將Redux中的store和最終頁面的內容分別進行緩存:
let cacheStore = Immutable.fromJS({});let cachePage = Immutable.fromJS({});
cacheStore
用於緩存不同請求下的store,以後端介面url為key;cachePage
則用於緩存最終的結果,以客戶端請求url為key。有了Cache基本框架,便可以結合客戶端請求和後端響應來進行最後的設計:
- 當一次客戶端請求來到時,先解析出此次請求對應的後端介面url,並向後端發出請求。
- 得到後端響應,判斷狀態碼
code
,如果是200,直接重新走一遍完整的服務端渲染流程,如果是304,則先去查cachePage
中有沒有對應的數據,如果有,則直接返回其中的內容。 - 如果
code
是304但cachePage
中沒有對應的內容,假如cacheStore
中也沒有對應的store
,則走一遍完整的服務端渲染流程,如果有,則直接取出store
執行相應的store.dispatch
修改payload
,接著直接進入二次渲染。 - 在二次渲染結束後,更新兩個Cache。
改進
Cache帶來的性能提升以內存消耗為代價,典型的空間換時間,所以能減少內存消耗的事情該做的還是要做。這一點也很簡單,考慮到cachePage
中存的是最終返回給客戶端的頁面,而這種頁面終歸是要被壓縮的,所以在放入緩存之前就可以直接將其壓縮,這樣最後從裡面取到的也是壓縮後的數據,不但節省了空間還省去了每次的壓縮帶來的損耗:
cachePage = cachePage.set( frontUrl, zlib.gzipSync(page));
推薦閱讀: