Web 開發后端緩存思路
Web 應用是個典型的 io 數據流,
首先,瀏覽器發來一個 input,服務器獲取之后,做一些查詢或者計算,然后把生成的 output 返回給瀏覽器。
這些查詢或計算,還會有衍生的子 io 流。
緩存的目的就是讓把 input 變成一個 key,在條件允許的情況下,跳過計算,直接生成 output。在主流程中,或子流程中。
數據查詢緩存
resource: http://robbinfan.com/blog/3/orm-cache
n + 1 問題
n + 1 問題是 orm 竟然被詬病的地方。什么是 n + 1 問題呢? 比如一個用戶,它寫了 20 篇博客。當我們查詢這個用戶的首頁時,需要列出他的所有博客。 “高效”的思路是使用一個 join 語句,把 user 表和 blog 表做 join,然后一條語句取出所有想要的字段。 而 orm,會先取出 user 的記錄,再做 20 次遍歷,分別用 20 條語句取出他所有的博客。 按照 robbin 的說法,join 語句的結果很難被緩存利用,因為它發生的場景太過特定。 但如果使用 orm,按照 n + 1 的方式取數據。由于數據緩存的粒度比較小,緩存的命中率得到了提高。 首先,orm 內置的緩存一般會在同一個連接中,緩存同一 sql 語句的結果;其次,數據庫的緩存會記下特定 sql 語句的對應的結果,當再次收到相同語句時,數據庫不必進行掃描,可以直接 O(1) 復雜度地返回緩存結果。
robbin 認為,在這種情況下,n + 1 的查詢反而因為有效利用了緩存,而比 join 語句更快。
robbin 得出了這樣的結論:即使不使用對象緩存,ORM的n+1條SQL性能仍然很有可能超過SQL的大表關聯查詢,而且對數據庫磁盤IO造成的壓力要小很多
緩存層加入
利用 redis 或者 memcached,這個話題 google 一下會有很多。
json to orm 問題
CNode 使用的是 mongoose 這個 odm 來訪問 mongodb。在 mongoose 的 model 中,我們定義了不少【虛擬屬性】,所謂虛擬屬性,就是指:一個 user 實例,它有first_name和last_name字段,當我們定義一個名字為full_name的虛擬屬性時,user.full_name 會根據定義的函數自動拼接 first_name 和 last_name。也就是面向對象編程中的 getter 方法。
當緩存一個 mongoose 取出的文檔到 redis 時,我們會將它先裝換成 json,再以字符串形式存入。 再次取出并 JSON.parse 的時候,會發現 mongoose model 定義的虛擬屬性全都被丟棄了。所以這時,需要重新把這個 json 傳入 model 初始化一次,得到一個 model 實例。這樣,我們就恢復了原來內存中的那個 model 實例了。
數據寫入緩存:
在數據庫與服務端之間利用 redis
這是一個很常見的場景。比如文章的瀏覽數,每次文章被瀏覽時,瀏覽數都 +1。如果每次都回寫數據庫,不免數據量太大。加上數據庫看似簡單,其實做了不少關于一致性(請看官了解一下所謂【一致性】,【base】,【acid】)的檢查。 而同時,瀏覽數并不要求保證一致性,只要大概準確就行了。 所以這時候,我們可以先將瀏覽數寫入 redis,滿足一定條件后,再回寫數據庫。 比如,在 controller 中,讓每次瀏覽都在 redis 上 +1,+1 完成后,檢查瀏覽數是否除以 10 后余數為 0(count % 10 === 0),是的話,則回寫數據庫,并將緩存置為 0。
緩存過期策略
可以通過過期時間來控制內容新鮮期
那么就設置設緩存過期時間。比如在一個網站上,總會有一些每日之星用戶,或者今日推薦文章。
這些內容的新鮮期都很長,比如每日之星的數據,如果 20 分鐘更新一次,用戶也不會有異議。那么,我們在查詢出這些用戶后,可以將結果集存入緩存中,并設置過期時間為 20 分鐘。待自動失效后,再重新查詢。
無法通過過期時間來控制內容新鮮期
這時,又有兩個策略了。一個是【主動過期】策略,一個是【被動過期】策略。比如想要緩存一篇文章的內容 HTML,但文章的頁面中包含了評論信息。一些老文章被大量訪問而無人添加評論時,緩存的效果杠杠的。但一些近期文章會被用戶添加評論, 我們無法判斷用戶何時會添加評論,所以無法得到一個最佳實踐的文章過期時間。
主動過期
顧名思義,主動地去 delete 緩存。還是上面的文章例子。我們可以在評論的 model 中,設置一個回調邏輯。每當評論被更新時,同時去刪除評論所對應的文章的緩存內容。
被動過期
被動過期也不是完全不需要回調邏輯,只是相對主動過期來說。它不必理解緩存層的存在。
還是上面的例子,當我們緩存一個文章頁面時,不僅以文章的 id 為 cache key,還在 cache key 中拼入文章的 update_at 字段。 當評論更新時,讓評論去touch一下對應的文章,更新文章的最后修改日期。那么當用戶再次訪問文章時,由于 cache key 變動,過期的內容就不會被展現,從而實現了被動過期。
同樣的例子還有,一篇文章是以 markdown 寫成,每次輸出的時候,都要進行 markdown 渲染,這是個耗時操作。于是我們可以將'markdown_result_' + artical.id + artical.updated_at作為 key,來緩存 markdown 的渲染結果。每當文章更新時,被動地廢棄舊有的緩存結果。
當然,這里不能說主動過期好,還是被動過期好。細心的看客也許在上面兩個例子中發現了問題,那就是,當文章的內容沒有進行改變,而評論添加時,文章卻要重新渲染 markdown,可渲染結果其實是一樣的。
HTML 片段緩存
resource: https://ruby-china.org/topics/21488
以 CNode 為例,我簡單地劃分了 1 2 3 4 四個部分。每個部分在邏輯上都是一個相對獨立的 setion,它們使用不同的數據進行渲染。在代碼組織上,這些部分也是屬于不同的 view 文件來負責。
4 的部分就是我們所說的,可以通過過期時間來管理的片段。這個部分 10 分鐘更新一次沒有問題。
3 的部分類似上面 markdown 的例子,渲染是耗時的,而數據是經常不變的。所以我們可以通過類似'user_profile' + user.id + user.updated_at的 cache key 來將其緩存。
而 1 和 2 的部分,就類似上面【被動過期】的例子。1 中,不僅有帖子的標題,還有帖子的作者信息,還有帖子的最后回復者信息,粗略一算,這都是 3 條查詢。如果能緩存起來,那是大大滴有用。而 2,包含了所有 1 類似的部分,也可以被緩存。但如果 1 動了,2 怎么辦?所以在緩存 2 時,我們可以使用所有 1 中最新的那個帖子的更新時間來作為 key,當有帖子更新后,更新時間對不上,緩存就被動過期了。
如果是個大型站點,1 的內容頻繁動,那么會導致 2 的緩存命中率很低。這時,從業務上,我們判斷,主頁的新鮮期是可以在 5s 內不變的。這時,緩存策略可以改為,最新的帖子的更新時間,如果離現在的時間不超過 5s,則返回之前緩存的內容。我們一下就從【被動過期】的策略,變回【過期時間】的策略了。
所以具體采用什么策略,根據業務場景可以靈活選擇。
【被動過期】策略時,切記要讓上層片段的緩存 key 可以被下層 touch 更新。【過期時間】策略時,需要我們判斷一下內容的新鮮期。
并且有一點比較深入的知識點是,不同的 touch 策略,會對緩存命中率產生影響。這個知識點請參照本小節 resource 部分的鏈接去看看 Tower 在面對這個情況時的方案。
如果你要問我 CNode 在片段緩存上是怎么選擇的,我可以負責任并瀟灑地告訴你:目前沒有這方面的緩存~~~~
說起來啊,一是訪問量比較小,懶得做。二是,從技術上說,渲染是同步的,而在 Node.js 中,數據查詢是異步的。我思考了一下,做這個片段緩存不是簡單的事情。而 Rails 中做起來就簡單多了,雖然玩 Node 的人總是覺得 Node 可以原生異步并發取數據是一件優越的事情。但同步 io 模型在這個地方帶來的好處就是【惰性求值】 。Rails 在渲染時,可以判斷一下到底是【查詢 + 渲染】還是【直接取緩存】。而 Node 由于異步查詢和同步渲染之間的沖突,要解決這個問題,必須有個方便地支持異步渲染的模板方案出現。
last_modified 和 etag
resource: http://robbinfan.com/blog/13/http-cache-implement
這節我們討論的是靜態頁面在瀏覽器中的緩存思路。所以不是 max-age 和 cache-control 那套針對靜態資源的方案,而是 last_modified 和 etag 這一套。
上面的內容,一直在說數據庫,緩存數據庫。但有一點不可忽視的是,瀏覽器中其實也緩存了我們頁面的副本,這部分的緩存,也應該有效地利用起來。 最簡單利用方式,就是讓服務器判斷一下最終頁面生成的 etag 與瀏覽器 header 中傳來的 etag 是否相同的,相同的話,則返回 304,省去網絡傳輸的帶寬開銷。
注意,最簡單的方式是判斷最終內容生成的 etag!其實我們可以自定義 etag。在這里,etag 也可以理解成一定意義上上述的 cache key,只是這回,儲存介質變成了用戶的瀏覽器。
還是上面那個文章內容頁面的例子,我們文章頁面由 文章內容 + 評論 內容決定是否緩存。這時,我們可以把文章內容的更新時間和最新評論的更新時間拼成一個 etag,返回給用戶。下次用戶再訪問時,如果 etag 對得上,服務端根本都不需要再去緩存數據庫中取 HTML 片段數據,直接告訴用戶一個 304,【內容與上次一樣,沒變化】。這時瀏覽器就直接從自己的緩存中取出頁面進行展示了。既節省了寬帶占用,又節省了查詢開銷。
etag as cookie
這里說點題外話,etag 在一定意義上是可以拿來當 cookie 用的。首先我們要了解,瀏覽器針對每一個 url(包括 querystring 部分)都可以存儲一個 etag 值。
比如我是一個廣告服務商,我的廣告頁面是 https://cnodejs.org/ads。每當不同的用戶訪問這個頁面時,我都根據大數據黑魔法定位到這個匿名用戶到底是誰,然后返回他感興趣的內容。可如果用戶禁用了 cookie 的話,我該怎么定位用戶呢?這時候可以使用 etag。每當用戶不帶 etag 訪問時,都生成一個不沖突的 etag 給它,那么下次他再訪問我 url 時,etag 就回來了。
OK,結束了,結尾語是:Rails 社區代表 Web 開發世界的最先進生產力。
來自:https://cnodejs.org/topic/55210d88c4f5240812f55408