寫在前面
讀寫分離、分庫分表、反範式化、採用 NoSQL……如果這些擴展手段全都上了,數據響應依舊越來越慢,還有什麼解決辦法嗎?
有,加緩存。利用緩存層來吸收不均勻的負載和流量高峰:
Popular items can skew the distribution, causing bottlenecks. Putting a cache in front of a database can help absorb uneven loads and spikes in traffic.
一。在哪加?
理論上,在數據層之前的任意一層加緩存都能夠阻擋流量,減少最終抵達數據庫的操作請求:

按緩存所處位置分為 4 種:
-
客戶端緩存:包括 [HTTP 緩存](/articles/http 緩存/)、瀏覽器緩存等
-
數據庫緩存:一些數據庫提供了內置的緩存支持,比如查詢緩存(query cache)
為了減輕數據庫的負載,我們在應用程序和數據存儲之間加個鍵值存儲作為緩衝層:
A cache is a simple key-value store and it should reside as a buffering layer between your application and your data storage.
通過內存中緩存的數據來響應一部分請求,而不必實際執行查庫操作,從而提升數據響應速度
二。存什麼?
常見的有兩種緩存模式:
-
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:刷新式
Cache-aside

預留緩存模式下,緩存與數據庫之間沒有直接關係(緩存位於一旁,所以叫 Cache-aside),由應用程序將需要的數據從數據庫中讀出並填充到緩存中
數據請求優先走緩存,未命中緩存時才查庫,並把結果緩存起來,所以緩存是按需的(Lazy loading),只有實際訪問過的數據才會被緩存起來
主要問題在於:
-
未命中緩存時需要 3 步,延遲不容忽視(對於冷啟動可以手動預熱)
-
緩存可能會變舊(一般通過設置 TTL 來強制更新)
Read-through

直讀模式下,緩存擋在數據庫之前,應用程序不與數據庫直接交互,而是直接從緩存中讀取數據
未命中緩存時,由緩存負責查庫,並自己緩存起來。與預留緩存唯一的區別在於查庫的工作由緩存來完成,而不是應用程序
Write-through

類似於直讀模式,緩存也擋在數據庫之前,數據先寫到緩存,再寫入數據庫。也就是說,所有寫操作必須先經過緩存
一般與直讀式緩存相結合,雖然寫操作多過一層緩存(存在額外的延遲),但保證了緩存數據的一致性(避免緩存變舊)。此時,緩存就像數據庫的代理,讀寫都走緩存,緩存再查庫或將寫操作同步到數據庫
Write-behind/Write-back

回寫式緩存與直寫式很像,寫操作同樣要先經過緩存,唯一的區別在於異步寫入數據庫,進而允許批處理以及寫操作合併
同樣能夠與直讀式緩存結合使用,而且不存在直寫式中寫操作的性能問題,但僅保證最終一致性
Write-around
所謂繞寫式緩存就是寫操作不經過(繞過)緩存,由應用程序直接寫入數據庫,僅緩存讀操作。可與預留緩存或直讀緩存結合使用:

Refresh-ahead
提前刷新,在緩存過期之前,自動刷新(重新加載)最近訪問過的條目。甚至可以通過預加載來減少延遲,但如果預測不准反而會導致性能下降
四。塞滿了怎麼辦?
當然,緩存空間是極其有限的,所以還要有逐出策略(Eviction Policy),從緩存中剔除一些不太可能用到的條目,常用策略如下:
-
LRU(Least Recently Used):最常用的一種策略,根據程序運行時的局部性原理,在一段時間內,大概率訪問相同的數據,所以將最近沒有用到的數據剔除出去,比如訂機票,一段時間內大概率查詢同一路線
-
LFU(Least Frequently Used):根據使用頻率,將最不常用的數據剔除出去,比如輸入法大多是根據詞頻聯想的
-
MRU(Most Recently Used):在有些場景下,需要刪掉最近用過的條目,比如已讀、不再提醒、不感興趣等
-
FIFO(First In, First Out):先進先出,剔除最早訪問過的數據
這些策略還可以結合使用,比如 LRU + LFU 綜合考慮,取決於具體場景
暫無評論,快來發表你的看法吧