緩存一梭子, 程序員的快樂就是如此簡單

緩存也是一把梭項目的標配,從業多年,有事無事set/getCache來一梭子。

夜深人靜的時候,頭腦裏冷不丁會出現一些問題,我竟一時無法自圓其說。

  1. 已經有cpu多級緩存、操作系統page cache,那爲什麼還需要定義應用緩存?
  2. 應用的多個副本緩存了同一份數據庫數據, 怎麼保證這些多副本的緩存一致性?

  1. 緩存在計算機體系中的地位

  2. 緩存和緩衝的區別

  3. 使用緩存時的業務考量?

  • 數據一致性要求不那麼嚴格的場景
  • 設計模式: 惰性、直寫
  1. 使用緩存時的技術考量?
  • 緩存與數據庫一致性
  • 過期策略
  • 驅逐策略

https://course.ccs.neu.edu/cs5600/paging-caching.html

緩存在計算機體系中的定位

內存層次結構

  1. 寄存器
  2. CPU Cache(L1,L2,L3)
  3. RAM (內存/主存)
  4. Disk or SSD(外存/輔助存儲)

靜態體系架構

  • 低級的緩存包含更多的存儲空間,高級的緩存有更好的存取速度(低延遲)。
  • RAM Page cache作爲磁盤緩存,CPU cache是RAM的緩存, 寄存器數據來自CPU Cache。

動態邏輯流程

  • 在搜索內存數據, 自上向下搜素(CPU、內存、磁盤),當在某層找到數據時,將在該層的較高層級保存一份副本, 以便將來可以迅速找到該內容。

如果在CPU Cache中沒找到數據,我們叫Cache miss;
如果在RAM內存中沒找到數據,我們叫Page miss/Page fault

  • 根據數據傳輸的時間/空間局部性原理,一次傳輸整個塊(而不是單個字節或內存字)時的數據傳輸效率更高 。
    根據這個設計:
    • 硬盤一次只能傳輸一個磁盤塊,而不是單個字節, 頁通常是磁盤塊的軟件視圖,磁盤塊、頁通常是4KB
    • CPU緩存的數據塊通常只有32B

Q:已經有cpu cache, page cache, 爲什麼我們還需要自定應用義程序cache。

A:不管是cpu級別的cpu cache,還是操作系統維護的page cache,都是對於最近訪問數據的緩存, 不針對特定的應用程序,機制對於程序員是透明的。

程序員日常工作的背景是快速靈活的利用內存數據,而不是cpu cache/page cache僅緩存最近訪問的數據塊(臨近32B/4KB),應用程序需要緩存的數據底層存儲可能是分散的或訪問頻次不固定,故應用程序需要做自定義的業務緩存。


作爲應用程序員,我們普遍說的Cache指的是應用程序Cache。

2. 緩存 vs 緩衝

緩存是對數據一致性要求不那麼嚴格的一種存取技術,利用內存訪問比外設訪問速度快的特性, 最終目的是加快入站請求的處理速度。

緩衝是提供一塊內存區域,用於入站請求頻繁地寫操作,之後一次性寫入到底層的外設,最終目的是減輕對外設的頻繁訪問。

3. 使用緩存的業務考量

亞馬遜的緩存最佳實踐

  • 使用緩存值是否安全。同一段數據在不同的上下文中可能具有不同的一致性要求。例如,在線結賬期間,您需要物品的確切價格,因此不適合使用緩存,但在其他頁面上,價格晚幾分鐘更新不會給用戶帶來負面影響。
  • 對該數據而言,維持緩存是否高效? 某些應用程序會生成不適合緩存的訪問模式。 例如,查詢頻繁變化的大型數據集的鍵空間,在這種情況下,保持緩存更新可能會抵消緩存帶來的優勢。
  • 數據結構是否適合緩存? 結構有無schema? or 聚合信息/獨立信息?

緩存的產生的方式:

  • 惰性緩存: 僅在應用程序實際請求對象時才填充緩存
  • 直寫: 緩存在數據庫更新時實時更新,由特定應用程序或者後臺程序更新,避免了緩存未命中,可幫助應用程序更好、更快捷地運行

對於第二個問題, 如何維護多個副本的緩存一致?
知乎經典回答:遇事先問“要不要”,而不是直接問”怎麼做“,如果定位爲緩存,那麼本來就有可能是過期的數據; 使用直寫來儘快保持一致, 要求更嚴格就不是緩存了,那就是分佈式一致性(CAP理論,共識算法)。

4. 使用緩存的技術考量

4.1 緩存和數據庫一致性

  1. cache-aside :(旁路緩存) 強調應用程序App與數據庫交互, Cache組件作爲旁路。
  • 如果讀取的數據沒有命中緩存,則從數據庫中讀取數據,然後將數據寫入到緩存,並且返回給用戶。
  • 更新: 先更新數據庫中的數據,再刪除緩存中的數據。
  1. read-through/write-through: (讀穿/寫穿) 強調App與Cache組件交互
  • 先查詢緩存中數據是否存在,如果存在則直接返回,如果不存在,則由緩存組件負責從數據庫查詢數據,並將結果寫入到緩存組件,最後緩存組件將數據返回給應用。
  • 更新: 如果緩存中數據已經存在,則更新緩存中的數據,並且由緩存組件同步更新到數據庫中,然後緩存組件告知應用程序更新完成。

旁路緩存與讀/寫穿緩存的差異在於 :誰來填充Database:App還是cache組件。 1,2中的寫緩存和寫數據庫的行爲是貫序同步的。

  1. write back:(寫回)在更新數據時,只更新Cache,標記Cache是髒數據,然後立馬返回;對於數據庫的更新會通過批量異步更新的方式。

寫回策略一般用在計算機系統中(上面的CPU Cache和文件系統的Pache Cahce)。


首先要知道緩存過期和緩存驅逐是不同的關注點, 我們以redis爲例。

redis 作爲內存鍵值對數據庫, 所有的KV都是以全局字典來實現,帶有過期時間的kv鍵值對也是維護在一個獨立的字典中。

4.2 redis緩存過期

redis帶有過期時間的KV項,並不是到期就被立即清除的, 考慮:

  • 到期刪除: 每一個設置了過期時間的kv項附帶一個計時器, 不消費內存,但是消耗cpu資源。
  • 惰性刪除: 每次訪問時,先去名單中判斷該k是否需要清理,不佔用cpu, 但是有可能kv項始終沒人訪問,造成過期的kv項內存堆積。
  • 定期刪除: 每隔一段時間從帶過期時間的kv字典中清理一部分。

redis以惰性刪除+定期刪除(默認100ms)來實現清理緩存過期KV項, 以實現cpu和mem的平衡使用。

(當然這2種策略還是會有一部分過期的kv值未能刪除)

4.3 redis緩存驅逐(內存淘汰)

redis是內存鍵值對數據庫,存儲受限於內存。考慮

  • 驅逐目的: 避免無限制佔用內存
  • 驅逐時機: maxmemory : 接近或者達到這個值會觸發緩存驅逐。
  • 驅逐策略:maxmemory-policy,8種策略(4維度和2種範圍的叉積的集合),默認是noeviction

https://cloud.tencent.com/developer/article/2045330

# volatile-lru -> Evict using approximated LRU among the keys with an expire set.
# allkeys-lru -> Evict any key using approximated LRU.
# volatile-lfu -> Evict using approximated LFU among the keys with an expire set.
# allkeys-lfu -> Evict any key using approximated LFU.
# volatile-random -> Remove a random key among the ones with an expire set.
# allkeys-random -> Remove a random key, any key.
# volatile-ttl -> Remove the key with the nearest expire time (minor TTL)
# noeviction -> Don't evict anything, just return an error on write operations.

分享一個與緩存有關的OOM案例:

應用定時(1min)滾動設置緩存KV項(1h),一開始使用golang bluele/gcache

2.5k star 內存緩存庫, 支持多種驅逐策略(LFU, LRU and ARC)。表現的像是一個固定長度的map。

func main() {
  gc := gcache.New(1000).   // 緩存項容量, 驅逐時機
    LRU().                  // 驅逐策略
    Build()
  gc.Set("key", "value")
}

注意看gcache的驅逐時機是基於緩存項容量,與內存無關
一開始緩存項容量設置的比較大,導致不容易觸發gcache的KV項驅逐,實際上這個時候gcache佔據的內存在滾動增長,最終應用OOM。

  • 案例可以通過設置較小的 緩存項容量來解決。
  • 案例也可以切換到patrickmn/go-cache緩存庫來解決

7.7k star 這個緩存的表現就類似redis

利用go-cache的緩存過期策略: 定期刪除過期項(purges expired items every 10 minutes),
搭配合適的過期時間和定時清理過期項的週期。

// Create a cache with a default expiration time of 5 minutes, and which
	// purges expired items every 10 minutes
	c := cache.New(5*time.Minute, 10*time.Minute)

	// Set the value of the key "foo" to "bar", with the default expiration time
	c.Set("foo", "bar", cache.DefaultExpiration)

	// Set the value of the key "baz" to 42, with no expiration time
	// (the item won't be removed until it is re-set, or removed using
	// c.Delete("baz")
	c.Set("baz", 42, cache.NoExpiration)

所以,除了SetCache(k,v)一把梭外,開發者應有更多前置心路歷程:

緩存在計算機系統中的地位、緩存的適用性、緩存的使用方式、 緩存作爲內存存儲的過期、驅逐實踐......

本文拋磚引玉,望構建更完整的緩存知識體系, 自勉。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章