追求性能極致:客戶端快取帶來的革命

Redis系列1:深刻理解高性能Redis的本質
Redis系列2:數據持久化提高可用性
Redis系列3:高可用之主從架構
Redis系列4:高可用之Sentinel(哨兵模式)
Redis系列5:深入分析Cluster 集群模式
追求性能極致:Redis6.0的多執行緒模型

背景

前面一篇我們說到,2020年5月份,Redis官方推出了令人矚目的 Redis 6.0,提出很多新特性,包括了客戶端快取 (Client side caching)、ACL、Threaded I/O 和 Redis Cluster Proxy 等諸多新特性。如下:
image
我們也專門對 Redis 6.0的 Threaded I/O(多執行緒網路I/O 模式)做了很詳細的說明,有興趣的翻到前面一篇。
這一篇咱們就來聊下這個Client side caching(客戶端快取),看看Redis為什麼需要客戶端快取、是基於什麼原理實現的,以及具體應該怎麼使用。

1 為什麼需要客戶端快取

1.1 快取服務的目的

回顧一下我們 在第一篇 《深刻理解高性能Redis的本質》中說過的,Redis的讀寫操作都是在記憶體中實現了,相對其他的持久化存儲(如MySQL、File等,數據持久化在磁碟上),性能會高很多。因為我們在操作數據的時候,需要通過 IO 操作先將數據讀取到記憶體里,增加工作成本。
image
上面那張圖來源於網路,可以看看他的金字塔模型,越往上執行效率越高,價格也就越貴。下面給出每一層的執行耗時對比:


  • 暫存器:0.3 ns
  • L1高速快取:0.9 ns
  • L2高速快取:2.8 ns
  • L3高速快取:12.9 ns
  • 主存:120 ns
  • 本地二級存儲(SSD):50~150 us
  • 遠程二級存儲:30 ms

    我們舉個L1和SSD的直觀對比,如果L1耗時1s的話,SSD中差不多要15~45小時,所以記憶體層面的訪問效率遠遠比磁碟層面的訪問效率高很多。
    總之,快取的目的是基於對持久化在磁碟的數據(比如MySQL數據、文件數據等)的高效訪問,為了提升效率而實現的。《Redis in Action》中也提到, Redis 能夠提升普通關係型資料庫的 10 ~ 100 倍的性能。
    數據訪問過程如下圖,Redis 存儲了熱點數據,當天我們請求一個數據時,先去訪問快取層,如果不存在再去訪問資料庫,這樣可以解決大部分高效讀取數據的業務場景,性能是快取最重要的價值之一。
    image

1.2 存在的問題

雖然我們使用Redis提升了數據的訪問效率,但是依然存在一些問題。基於分散式訪問的快取服務是一個獨立的服務存在,一般情況下訪問它需要經過這幾個步驟:

  • 連接快取服務(一般不會跟計算服務在一個實例上)
  • 查找並讀取數據(I/O操作)
  • 網路傳輸
  • 數據序列化反序列化
    這些操作一樣的是對性能有影響的,隨著互聯網的發展,流量不斷的膨脹,很容易達到 Redis 的性能上限。
    所以,我們經常會使用進程快取(本地快取),來輔助處理,將一些高頻讀低頻寫的數據暫存在本地,讀取數據的時候,先檢查本地快取是否存在,不存在再訪問遠端快取服務的數據,進一步提高訪問效率。
    如果Redis也不存在,就只能去 資料庫 中查詢,查到的數據再設置到 Redis 和 本地快取中,這樣後續的請求就不用再走到資料庫中了。
    image
    一般我們會使用Memcachced、Guava Cache 等來做第一級別快取(本地快取),使用Redis作為第二級快取(快取服務),本地記憶體避免了 連接、查詢、網路傳輸、序列化等操作,性能比快取服務快很多,這種模式大大減少數據延遲。

2 客戶端快取實現原理

Redis自己實現了一個客戶端快取,用以協助服務端Redis的操作,叫做tracking
我們可以通過命令來配置它:

CLIENT TRACKING ON|OFF [REDIRECT client-id] [PREFIX prefix] [BCAST] [OPTIN] [OPTOUT] [NOLOOP]

客戶端快取最核心的問題就是當Redis中的快取變更或者失效了之後,如果能夠及時有效的通知到客戶端快取,來保證數據的一致性。
Redis 6.0 實現 Tracking 功能,這個功能提供了兩種方案來實現數據的一致性保證:

  • RESP2 協議版本的轉發模式
  • RESP3 協議版本的普通模式和廣播模式
    image
    接下來我們一個個來分析。

2.1 普通模式

Redis使用 TrackingTable 來存儲普通模式的客戶端數據,它的數據類型是基數樹 ( radix tree)。
radix tree是針對稀疏的長整型數據查找的多叉搜索樹,能快速且節省空間的完映射,想深入了解的可以看這篇介紹
image
如圖中,客戶端ID列表與Redis存儲鍵的指針具有映射關係。而Redis鍵對象的指針對應的就是記憶體地址,數據結構是Long。
當開啟了track 功能之後,操作具有以下特性:

  • 當Redis獲取一個鍵值資訊時,radix tree 會調用 enableTracking 方法記錄 key 和 clientId 的映射關係,記錄到 TrackingTable 中。
  • 當Redis刪除或者修改一個鍵值資訊時
    • radix tree 根據key調用 trackingInvalidateKey 方法查找對應的 Clinet ID
    • 調用 sendTrackingMessage 方法把失效的鍵值資訊(invalidate 消息) 發送給這些 Clinet ID。
    • 發送完成之後從TrackingTable中刪除映射關係。
  • Client關閉 track 功能後,遇到大量刪除操的時候,一般是懶刪除,只將 CLIENT_TRACKING 標誌位刪除。
  • 默認 track 模式是不開啟,需要通過命令開啟,參考如下:
CLIENT TRACKING ON|OFF
+OK
GET test
$7
archite

2.2 廣播模式(BCAST)

image
廣播模式與普通模式類似,也是採用映射關係來對照,但實現過程還是有區別的:

  • 存儲的內容不一樣:如圖,採用Prefix Table 來存儲客戶端數據,存儲的是 前綴字元串指針 和 客戶端數據(客戶端ID列表 + 需通知的key值列表) 的映射關係。
  • 刪除鍵值的時機不一樣:
    • radix tree 根據key調用 trackingInvalidateKey 方法查找PrefixTable。
    • 判斷是否為空,不為空則 調用 trackingRememberKeyToBroadcast 對鍵列表進行進行遍歷,找到符合前綴匹配規則的,並記錄位置。
    • 在事件處理周期函數 beforeSleep 中 調用 trackingBroadcastInvalidationMessages 函數來發送消息。
    • 發送完成之後從 PrefixTable 中刪除映射關係。

2.3 轉發模式

RESP 3 協議 是 Redis 6.0 新啟用的協議,使用普通模式或者廣播模式需要依賴這種協議,這樣對於RESP 2 協議的客戶端來說就會有問題。所以衍生除了另一種模式:重定向(redirect)。

  • RESP 2 無法直接 PUSH 失效消息,所以不能直接獲取到失效數據(Redis Client 2)。
  • 支援 RESP 3 協議的客戶端(Redis Clinet 1) 告訴 Server 將失效消息通過 Pus/Sub 通知給 RESP 2 客戶端。
  • 而Redis Client 2 (RESP 2 )是通過訂閱命令 SUBSCRIBE,專門訂閱用於發送失效消息的頻道 redis:invalidate。
    image

如下所示:

# Redis Client 2 (支援RESP 2)執行訂閱 
client id : 888
subscribe _redis_:invalidate

# Redis Client 1(支援RESP 3),轉發給 2
client tracking on bcast redirect 888

3 總結

3.1 默認模式(普通模式)

  • 服務端記錄客戶端操作過的 key,key 對應的值發生變化時,會發送 Invalidation Messages 給Redis 客戶端。
  • 服務端記錄key資訊會消耗一些記憶體,但是發送失效消息的範圍,限制在存儲的key範圍內,計算和網路傳輸變的輕量。
  • 優點是節省 CPU 以及流量頻寬,但是會佔用一些記憶體。

3.2 廣播模式

  • 服務端不記錄 key,而是訂閱 key 的特定前綴,當匹配前綴的 key 的值改變時,發送 Invalidation Messages 給 Redis客戶端。
  • 優點是服務端的記憶體消耗少,但是會損耗更多的 CPU 去做前綴匹配的計算。

3.3 轉發模式

  • 為了兼容 resp2 協議的一種過渡模式
  • 優點是佔用記憶體少,CPU佔用多

客戶端的快取

客戶端快取,需要業務側自己實現,Redis 服務端只負責通知你key 的變動(刪除、新增)。