文章目錄
- 在哪兒加緩存
- 緩存什么內容
- 緩存原始查庫結果
- 緩存數據對象
- 怎么查詢緩存結果
- 預留緩存模式
- 直讀模式
- 直寫模式
- 回寫式緩存
- 繞寫式緩存
- 提前刷新模式
- 緩存滿了如何處理
- 參考
讀寫分離、分庫分表、反范式化、采用 NoSQL……如果這些擴展手段全都上了,數據響應依舊越來越慢,還有什么解決辦法嗎?
有,加緩存。 利用緩存層來吸收不均勻的負載和流量高峰。
在哪兒加緩存
理論上,在數據層之前的任意一層加緩存都能夠阻擋流量,減少最終抵達數據庫的操作請求:
按緩存所處位置分為 4 種:
- 客戶端緩存:包括HTTP 緩存、瀏覽器緩存等
- Web 緩存:例如CDN、反向代理服務等
- 應用層緩存:例如Memcached、Redis等鍵值存儲
- 數據庫緩存:一些數據庫提供了內置的緩存支持,比如查詢緩存(query cache)
為了減輕數據庫的負載,我們在應用程序和數據存儲之間加個鍵值存儲作為緩沖層:通過內存中緩存的數據來響應一部分請求,而不必實際執行查庫操作,從而提升數據響應速度。
緩存什么內容
- Cached Database Queries:緩存原始查庫結果
- Cached Objects:緩存應用程序中的數據模型,比如重新組裝過的數據集,或者整個數據模型類實例
緩存原始查庫結果
根據查詢語句生成key,將查庫結果緩存起來,例如:
key = "user.%s" % user_id
user_blob = memcache.get(key)
if user_blob is None:user = mysql.query("SELECT * FROM users WHERE user_id=\"%s\"", user_id)if user:memcache.set(key, json.dumps(user))return user
else:return json.loads(user_blob)
這種模式的主要缺陷在于難以處理緩存過期,因為數據與key(即查詢語句)之間并沒有明確的關聯,數據發生變化后,很難精確地刪掉緩存中的所有相關條目。試想,一個單元格發生變化,會影響哪些查詢語句?
盡管如此,這仍然是最常用的緩存模式,因為可以做出妥協,比如:
- 只緩存與查詢語句有直接關聯的數據,排序、統計、篩選之類的計算結果統統都不存了
- 不求精確,把所有可能受影響的緩存條目都刪掉
緩存數據對象
另一種思路是將應用程序中的數據模型對象緩存起來,這樣原始數據與緩存之間就有了邏輯關聯,從而輕松解決緩存更新的難題。
無論數據是如何查詢,如何加工轉換的,只把最終得到的數據模型對象緩存起來,原始數據發生變化時,直接把相應的數據對象整個移除。對應用程序而言,數據對象比原始數據更容易管理和維護,因此,建議緩存數據對象,而不是原始數據。
怎么查詢緩存結果
常見的緩存數據訪問策略有 6 種:
- Cache-aside/Lazy loading:預留緩存
- Read-through:直讀式
- Write-through:直寫式
- Write-behind/Write-back:回寫式
- Write-around:繞寫式
- Refresh-ahead:刷新式
預留緩存模式
數據請求優先走緩存,未命中緩存時才查庫,并把結果緩存起來,所以緩存是按需的(Lazy loading),只有實際訪問過的數據才會被緩存起來。緩存與數據庫之間沒有直接關系(緩存位于一旁,所以叫 Cache-aside),由應用程序將需要的數據從數據庫中讀出并填充到緩存中。
主要問題在于:
- 未命中緩存時需要 3 步,延遲不容忽視(對于冷啟動可以手動預熱)
- 緩存可能會變舊(一般通過設置 TTL 來強制更新)
直讀模式
直讀模式下,緩存擋在數據庫之前,應用程序不與數據庫直接交互,而是直接從緩存中讀取數據。
未命中緩存時,由緩存負責查庫,并自己緩存起來。與預留緩存唯一的區別在于查庫的工作由緩存來完成,而不是應用程序。
直寫模式
類似于直讀模式,緩存也擋在數據庫之前,數據先寫到緩存,再寫入數據庫。
也就是說,所有寫操作必須先經過緩存。
一般與直讀式緩存相結合,雖然寫操作多過一層緩存(存在額外的延遲),但保證了緩存數據的一致性(避免緩存變舊)。此時,緩存就像數據庫的代理,讀寫都走緩存,緩存再查庫或將寫操作同步到數據庫。
回寫式緩存
與直寫式唯一的區別在于異步寫入數據庫,進而允許批處理以及寫操作合并。同樣能夠與直讀式緩存結合使用,而且不存在直寫式中寫操作的性能問題,但僅保證最終一致性。
繞寫式緩存
所謂繞寫式緩存就是寫操作不經過(繞過)緩存,由應用程序直接寫入數據庫,僅緩存讀操作。可與預留緩存或直讀緩存結合使用:
提前刷新模式
提前刷新,在緩存過期之前,自動刷新(重新加載)最近訪問過的條目。甚至可以通過預加載來減少延遲,但如果預測不準反而會導致性能下降。
緩存滿了如何處理
當然,緩存空間是極其有限的,所以還要有逐出策略(Eviction Policy),從緩存中剔除一些不太可能用到的條目,常用策略如下:
- LRU(Least Recently Used):最常用的一種策略,根據程序運行時的局部性原理,在一段時間內,大概率訪問相同的數據,所以將最近沒有用到的數據剔除出去
- LFU(Least Frequently Used):根據使用頻率,將最不常用的數據剔除出去
- MRU(Most Recently Used):在有些場景下,需要刪掉最近用過的條目
- FIFO(First In, First Out):先進先出,剔除最早訪問過的數據
參考
http://www.ayqy.net/blog/caching/