來源:Ben Malec from Paylocity and RedisConf 2020 organized by Redis Labs
翻譯:Wen Hui
轉載:中間件小哥
這篇文章中我們介紹如何使用Redis 6中關于客戶端緩存的支持來設計我們的客戶端緩存機制。我們首先來看一個典型的web應用如下:
在loadbalancer后面我們有多個web服務器,并與相同的SQL數據庫相連接。另外,在每個web服務器中,我們有多個服務器端緩存用來在服務器端緩存SQL數據庫中的數據。
我們這樣設計的目的是避免每次數據讀操作都訪問數據庫從而帶來較高的系統延時。但是,這種設計模式帶來一個主要問題是如果其中一個web服務器接收到更新數據的請求,會更新數據庫中的數據,以及這個服務器中的服務器緩存,但是其他的web服務器中會繼續緩存舊的數據,從而帶來數據不一致的問題。我們可以想到有多個解決方案來解決這個問題,首先一種比較常見的方案是更新數據的web服務器可以將數據更新的請求廣播到其他的web服務器中。但是這種方式會帶來以下兩個主要問題:
1. 會很大程度上增加系統網絡的負載。
2. 會導致競態條件(race condition)以及其他一些問題。例如如果兩個web服務器同時更新同一個數據,那么系統網絡無法保證這兩個更新請求到達其他服務器的先后順序。
針對以上問題,一個常見的解決方案是我們可以將服務器端緩存替換成Redis,如下圖所示:
這樣做的好處是:
1. Redis比其他后端數據庫存儲要快許多。
2. 完全可以解決競態條件或數據不一致的問題,因為多個web服務器共享了redis實例,所以客戶端每次都會得到正確的結果。
但這樣也帶來了web服務器與Redis通信的網絡延時的問題。因為在一般情況下,內存訪問速度很快,所以網絡延時很容易成為這種設計的瓶頸。
如果我們綜合了以上兩種設計,我們會得到以下設計方案:
在這里,我們保留服務器端緩存,并在redis中數據改變的情況下,通過redis的廣播機制來更新其他web服務器中的緩存。整體設計如下:
1. Redis始終是系統緩存的單一數據源。
2. 當Redis緩存的值更新后,通過Redis的發布訂閱連接來更新其他web服務器中的緩存。
3. 在其他web服務器收到更新緩存的消息以后,會使本地服務器端緩存失效,然后再下次接收到讀數據請求的時候訪問redis實例以拿到更新后的值。
4. 這樣設計不僅可以保持數據一致性并且可以使用服務器端緩存以降低系統延時。
5. 在多個服務器同時更新的情況下,因為Redis是單一數據源,所以所有的服務器端緩存都會得到正確的值。
6. Paylocity做了進一步的優化,沒有將整個鍵來廣播給其他web服務器,而是只廣播鍵的hash值,這樣可以進一步降低網絡負載。但同時也增加一些程序的復雜度。
作者在Redis Conf 2018 對這種解決方案做了演講,但也收到Redis 作者Savatore提到的一些潛在的問題:
1. 所有的更新操作必須通過web服務器中的緩存邏輯,如果直接更改redis的話其他web服務器端不會收到更新。
2. 所有更新都會被廣播到所有的web服務器中,無論web服務器中是否緩存過更新的鍵。
3. 如果Redis內核可以為客戶端緩存的逐出提供幫助將是一個更好的方案。
到了Redis 6.0版本,redis實現了針對客戶端緩存的追蹤機制,具體特性如下:
1. 在Redis中添加了關于客戶端緩存追蹤的新命令。
2. Opt in:Redis客戶端可以選擇是否啟用客戶端追蹤。
3. 兩種模式: 默認和廣播模式。
4. Redis會記錄鍵值的改動,并記錄哪個客戶端對哪個鍵值感興趣。
5. 當鍵值改動后,Redis會發送給啟用緩存追蹤的客戶端發送緩存無效信息。
6. 客戶端會逐出特定的客戶端緩存,下一次的請求將訪問Redis以獲取數據。
下面具體介紹Redis 客戶端緩存追蹤的具體模式:
1. 默認模式
Redis 命令:CLIENT TRACKING ON
在默認模式下, Redis會顯式的記住每個客戶端感興趣哪個特定的鍵,如果鍵被更新時,Redis只給那些對這個鍵感興趣的客戶端發送緩存無效的信息。
優點:
可以最大限度利用網絡帶寬,不會有多余的消息發送給沒有緩存過這個鍵的客戶端。
缺點:
對于Redis服務器來說記住每個客戶端感興趣的鍵會導致使用更多的內存。
如果我們有幾千個客戶端和幾百萬個鍵,這種方式會消耗非常大的內存資源。
如果redis需要清理追蹤部分的內存的話,需要給客戶端發送緩存無效的消息, 即使特定的鍵值沒有被改變。
2. 廣播模式
Redis命令: CLIENT TRACKING ON BCAST
在廣播模式下,Redis會發送給啟用客戶端緩存追蹤的所有客戶端發送緩存失效消息,無論特定的鍵是否緩存在客戶端中。
優點:
沒有在Redis服務器實例中顯著使用內存。
缺點:
需要更多的網絡帶寬。
3. 帶注冊前綴的廣播模式
Redis命令: CLIENT TRACKING ON BCAST PREFIX key_prefix_value
在廣播模式下,可以通過注冊鍵前綴方式來限制在鍵被更新的情況下,只有注冊特定鍵前綴的客戶端才會收到緩存失效的消息。
需要注意的地方是:
1. 鍵的名字需要仔細定義,因為這樣可以減少緩存失效消息的數量。
2. 多個鍵前綴可以被同個客戶端指定。
3. 可以通過鍵前綴來告訴redis哪些鍵會被客戶端緩存而那些不會,例如:
clientSide:MyAppCode:keyname和MyAppCode:keyname。
4.默認模式中的Optin 模式
Redis命令:CLIENT TRACKING ON OPTIN
在Opt in模式下,客戶端將收到所指定的鍵緩存失效的消息,在默認模式中,所有鍵是默認注冊的,但在Opt in模式下,需要顯式指定哪些鍵需要被注冊。指定鍵被注冊的命令是CLIENT CACHING YES,接下來的一個讀請求的鍵將會被注冊在Redis服務器端的追蹤表里,當這個鍵被更新時客戶端會收到緩存失效消息。
例子如下:
CLIENT CACHING YES
+OK
GET MYKEY1
$8
MyValue6
現在如果MYKEY1的值被改變,則客戶端會收到緩存失效消息。
5 默認模式中的Opt Out模式
Redis 命令: CLIENT TRACKING ON OPTOUT
和OPTIN 模式相反,在OPTOUT模式下客戶端鍵默認會被追蹤,需要顯式指定哪些鍵不需要被追蹤。
如果需要關閉特定鍵的追蹤,需要向Redis發送讀請求之前使用CLIENT CACHING NO命令。
例子如下:
CLIENT CACHING NO
+OK
GET MYKEY2
$8
MyValue2
現在如果MYKEY2的值被改變,客戶端不會收到緩存失效的消息。
在介紹完客戶端緩存追蹤的幾種模式后,下一個問題就是在何種情況下需要使用何種模式。這個問題實際上跟具體的應用需求有關,實際上是在Redis內存使用和整個系統網絡資源上做取舍。
你的應用是否有很多緩存更新的場景?如果不是的話可以選用廣播模式,因為在這種場景下大部分的鍵不會被更新,所以沒有必要在Redis端記住所有的鍵信息。
相反的,如果應用相對來說有很多更新緩存鍵的應用場景,那么可以選用默認模式,尤其當有很多客戶端,但每個客戶端緩存鍵的數量相對較少的情況下。默認模式是最好的選擇。
另外在我們的設計中,我們會把CLIENT ID 也放到緩存無效信息中,這樣特定客戶端會忽略自己觸發的的緩存失效消息。(注在新版本的Redis 6.0.4中,可以使用NOLOOP來達到同樣的效果)
另外需要留意的是客戶端追蹤可以指定另一個REDIRECT選項,這個選項主要是為RESP2(舊版本)協議的客戶端提供的。因為在舊版本的協議中,不支持從Redis服務器端向客戶端推送消息。所以這個選項的作用是將緩存失效的消息推送到額外指定的客戶端。
具體的使用方法是:
1. 在客戶端啟用客戶端緩存前,創建一個新的客戶端,使用CLIENT ID命令記錄下這個客戶端的ID,讓后使用SUBSCRIBE redis:invalidate來訂閱緩存失效消息的頻道。
2. 在啟用客戶端緩存的時候使用這個客戶端ID來接收無效消息,例如如果ID為5,則使用CLIENT TRACKING YES REDIRECT 5。
3. 這樣緩存無效消息將發送到這個指定客戶端中,但是需要注意的是這種方式會潛在帶來潛在的競態條件。因為如果同時有另外客戶端更新Redis中的鍵數據,無法保證客戶端收到緩存失效消息的時間。
在新版本的Redis RESP3協議中支持從Redis服務器端推送消息,但在RESP2協議中只支持客戶端發送請求Redis服務器端處理并回復。
1. 現在客戶端可以使用兩種不同版本的協議。
2. 不需要創建新的TCP連接來接收緩存失效消息。創建新的連接對Paylocity來說是一個很大的問題,因為我們使用多個Redis集群,每個客戶端保留向集群中每一個節點的連接。
3. 但現在RESP3協議的主要問題是大部分的客戶端庫都沒有支持這個協議。
下面我們討論使用客戶端追蹤的應用具體實現步驟:
1. 在客戶端中,創建客戶端緩存并連接Redis
2. 使用CLIENT TRACKING ON 來啟用追蹤。
3. 更新數據時,更新Redis 和相應的客戶端緩存。需要記住的是Redis是單一數據來源。
4. 在讀取數據過程中,先從客戶端緩存中讀取,如果未找到數據則在Redis中讀取。如果在Redis中找到數據,則更新客戶端本地緩存,這樣的話下個請求將會通過本地緩存拿到新的數據。同時也需要從Redis中讀取TTL并設置本地緩存過期時間。
5. 監聽連接的無效消息頻道,如果客戶端接收到Redis發送的緩存失效消息,則更新本地緩存。
Paylocity的計劃:
利用Redis的客戶端追蹤功能:
像之前介紹的那樣,我們有比較少的web服務器,而且在大多數情況下鍵值不會經常改變,所以廣播模式比較適用于這種場景。
廣播前綴的方式非常適用于我們的場景,因為我們會創建一個大的Redis集群并共享給各個應用。所以我們使用應用代號來當作BROADCAST的前綴。
使用RESP3客戶端:
1. 可以極大的減少總的Redis連接數
2. 目前我們使用StackExchange Redis客戶端,一個非常好的客戶端,只是不確定以后對RESP3協議的支持。
3. 如果我們自己實現redis客戶端的話也不會有大的問題,因為我們只需要支持GET,SET 和接收緩存失效消息。
以下作者提供了使用Redis客戶端緩存支持的例子: