來(lái)源:Ben Malec from Paylocity and RedisConf 2020 organized by Redis Labs
翻譯:Wen Hui
轉(zhuǎn)載:中間件小哥
這篇文章中我們介紹如何使用Redis 6中關(guān)于客戶端緩存的支持來(lái)設(shè)計(jì)我們的客戶端緩存機(jī)制。我們首先來(lái)看一個(gè)典型的web應(yīng)用如下:
在loadbalancer后面我們有多個(gè)web服務(wù)器尤揣,并與相同的SQL數(shù)據(jù)庫(kù)相連接缆镣。另外,在每個(gè)web服務(wù)器中躁倒,我們有多個(gè)服務(wù)器端緩存用來(lái)在服務(wù)器端緩存SQL數(shù)據(jù)庫(kù)中的數(shù)據(jù)。
我們這樣設(shè)計(jì)的目的是避免每次數(shù)據(jù)讀操作都訪問數(shù)據(jù)庫(kù)從而帶來(lái)較高的系統(tǒng)延時(shí)。但是,這種設(shè)計(jì)模式帶來(lái)一個(gè)主要問題是如果其中一個(gè)web服務(wù)器接收到更新數(shù)據(jù)的請(qǐng)求谋旦,會(huì)更新數(shù)據(jù)庫(kù)中的數(shù)據(jù),以及這個(gè)服務(wù)器中的服務(wù)器緩存,但是其他的web服務(wù)器中會(huì)繼續(xù)緩存舊的數(shù)據(jù)册着,從而帶來(lái)數(shù)據(jù)不一致的問題拴孤。我們可以想到有多個(gè)解決方案來(lái)解決這個(gè)問題,首先一種比較常見的方案是更新數(shù)據(jù)的web服務(wù)器可以將數(shù)據(jù)更新的請(qǐng)求廣播到其他的web服務(wù)器中甲捏。但是這種方式會(huì)帶來(lái)以下兩個(gè)主要問題:
1. 會(huì)很大程度上增加系統(tǒng)網(wǎng)絡(luò)的負(fù)載演熟。
2. 會(huì)導(dǎo)致競(jìng)態(tài)條件(race condition)以及其他一些問題。例如如果兩個(gè)web服務(wù)器同時(shí)更新同一個(gè)數(shù)據(jù)司顿,那么系統(tǒng)網(wǎng)絡(luò)無(wú)法保證這兩個(gè)更新請(qǐng)求到達(dá)其他服務(wù)器的先后順序绽媒。
針對(duì)以上問題,一個(gè)常見的解決方案是我們可以將服務(wù)器端緩存替換成Redis免猾,如下圖所示:
這樣做的好處是:
1. Redis比其他后端數(shù)據(jù)庫(kù)存儲(chǔ)要快許多。
2. 完全可以解決競(jìng)態(tài)條件或數(shù)據(jù)不一致的問題囤热,因?yàn)槎鄠€(gè)web服務(wù)器共享了redis實(shí)例猎提,所以客戶端每次都會(huì)得到正確的結(jié)果。
但這樣也帶來(lái)了web服務(wù)器與Redis通信的網(wǎng)絡(luò)延時(shí)的問題旁蔼。因?yàn)樵谝话闱闆r下锨苏,內(nèi)存訪問速度很快,所以網(wǎng)絡(luò)延時(shí)很容易成為這種設(shè)計(jì)的瓶頸棺聊。
如果我們綜合了以上兩種設(shè)計(jì)伞租,我們會(huì)得到以下設(shè)計(jì)方案:
在這里,我們保留服務(wù)器端緩存限佩,并在redis中數(shù)據(jù)改變的情況下葵诈,通過redis的廣播機(jī)制來(lái)更新其他web服務(wù)器中的緩存。整體設(shè)計(jì)如下:
1. Redis始終是系統(tǒng)緩存的單一數(shù)據(jù)源祟同。
2. 當(dāng)Redis緩存的值更新后作喘,通過Redis的發(fā)布訂閱連接來(lái)更新其他web服務(wù)器中的緩存。
3. 在其他web服務(wù)器收到更新緩存的消息以后晕城,會(huì)使本地服務(wù)器端緩存失效泞坦,然后再下次接收到讀數(shù)據(jù)請(qǐng)求的時(shí)候訪問redis實(shí)例以拿到更新后的值。
4. 這樣設(shè)計(jì)不僅可以保持?jǐn)?shù)據(jù)一致性并且可以使用服務(wù)器端緩存以降低系統(tǒng)延時(shí)砖顷。
5. 在多個(gè)服務(wù)器同時(shí)更新的情況下贰锁,因?yàn)镽edis是單一數(shù)據(jù)源,所以所有的服務(wù)器端緩存都會(huì)得到正確的值滤蝠。
6. Paylocity做了進(jìn)一步的優(yōu)化豌熄,沒有將整個(gè)鍵來(lái)廣播給其他web服務(wù)器,而是只廣播鍵的hash值几睛,這樣可以進(jìn)一步降低網(wǎng)絡(luò)負(fù)載房轿。但同時(shí)也增加一些程序的復(fù)雜度。
作者在Redis Conf 2018 對(duì)這種解決方案做了演講,但也收到Redis 作者Savatore提到的一些潛在的問題:
1. 所有的更新操作必須通過web服務(wù)器中的緩存邏輯囱持,如果直接更改redis的話其他web服務(wù)器端不會(huì)收到更新夯接。
2. 所有更新都會(huì)被廣播到所有的web服務(wù)器中,無(wú)論web服務(wù)器中是否緩存過更新的鍵纷妆。
3. 如果Redis內(nèi)核可以為客戶端緩存的逐出提供幫助將是一個(gè)更好的方案盔几。
到了Redis 6.0版本,redis實(shí)現(xiàn)了針對(duì)客戶端緩存的追蹤機(jī)制掩幢,具體特性如下:
1. 在Redis中添加了關(guān)于客戶端緩存追蹤的新命令逊拍。
2. Opt in:Redis客戶端可以選擇是否啟用客戶端追蹤。
3. 兩種模式: 默認(rèn)和廣播模式际邻。
4. Redis會(huì)記錄鍵值的改動(dòng)芯丧,并記錄哪個(gè)客戶端對(duì)哪個(gè)鍵值感興趣。
5. 當(dāng)鍵值改動(dòng)后世曾,Redis會(huì)發(fā)送給啟用緩存追蹤的客戶端發(fā)送緩存無(wú)效信息缨恒。
6. 客戶端會(huì)逐出特定的客戶端緩存,下一次的請(qǐng)求將訪問Redis以獲取數(shù)據(jù)轮听。
下面具體介紹Redis 客戶端緩存追蹤的具體模式:
1. 默認(rèn)模式
Redis 命令:CLIENT TRACKING ON
在默認(rèn)模式下, Redis會(huì)顯式的記住每個(gè)客戶端感興趣哪個(gè)特定的鍵骗露,如果鍵被更新時(shí),Redis只給那些對(duì)這個(gè)鍵感興趣的客戶端發(fā)送緩存無(wú)效的信息血巍。
優(yōu)點(diǎn):
可以最大限度利用網(wǎng)絡(luò)帶寬萧锉,不會(huì)有多余的消息發(fā)送給沒有緩存過這個(gè)鍵的客戶端。
缺點(diǎn):
對(duì)于Redis服務(wù)器來(lái)說記住每個(gè)客戶端感興趣的鍵會(huì)導(dǎo)致使用更多的內(nèi)存述寡。
如果我們有幾千個(gè)客戶端和幾百萬(wàn)個(gè)鍵柿隙,這種方式會(huì)消耗非常大的內(nèi)存資源。
如果redis需要清理追蹤部分的內(nèi)存的話辨赐,需要給客戶端發(fā)送緩存無(wú)效的消息优俘, 即使特定的鍵值沒有被改變。
2. 廣播模式
Redis命令: CLIENT TRACKING ON BCAST
在廣播模式下掀序,Redis會(huì)發(fā)送給啟用客戶端緩存追蹤的所有客戶端發(fā)送緩存失效消息帆焕,無(wú)論特定的鍵是否緩存在客戶端中。
優(yōu)點(diǎn):
沒有在Redis服務(wù)器實(shí)例中顯著使用內(nèi)存不恭。
缺點(diǎn):
需要更多的網(wǎng)絡(luò)帶寬叶雹。
3. 帶注冊(cè)前綴的廣播模式
Redis命令: CLIENT TRACKING ON BCAST PREFIX key_prefix_value
在廣播模式下,可以通過注冊(cè)鍵前綴方式來(lái)限制在鍵被更新的情況下换吧,只有注冊(cè)特定鍵前綴的客戶端才會(huì)收到緩存失效的消息折晦。
需要注意的地方是:
1. 鍵的名字需要仔細(xì)定義,因?yàn)檫@樣可以減少緩存失效消息的數(shù)量沾瓦。
2. 多個(gè)鍵前綴可以被同個(gè)客戶端指定满着。
3. 可以通過鍵前綴來(lái)告訴redis哪些鍵會(huì)被客戶端緩存而那些不會(huì)谦炒,例如:
clientSide:MyAppCode:keyname和MyAppCode:keyname。
4.默認(rèn)模式中的Optin 模式
Redis命令:CLIENT TRACKING ON OPTIN
在Opt in模式下风喇,客戶端將收到所指定的鍵緩存失效的消息宁改,在默認(rèn)模式中,所有鍵是默認(rèn)注冊(cè)的魂莫,但在Opt in模式下还蹲,需要顯式指定哪些鍵需要被注冊(cè)。指定鍵被注冊(cè)的命令是CLIENT CACHING YES耙考,接下來(lái)的一個(gè)讀請(qǐng)求的鍵將會(huì)被注冊(cè)在Redis服務(wù)器端的追蹤表里谜喊,當(dāng)這個(gè)鍵被更新時(shí)客戶端會(huì)收到緩存失效消息。
例子如下:
CLIENT CACHING YES
+OK
GET MYKEY1
$8
MyValue6
現(xiàn)在如果MYKEY1的值被改變倦始,則客戶端會(huì)收到緩存失效消息斗遏。
5 默認(rèn)模式中的Opt Out模式
Redis 命令: CLIENT TRACKING ON OPTOUT
和OPTIN 模式相反,在OPTOUT模式下客戶端鍵默認(rèn)會(huì)被追蹤鞋邑,需要顯式指定哪些鍵不需要被追蹤最易。
如果需要關(guān)閉特定鍵的追蹤,需要向Redis發(fā)送讀請(qǐng)求之前使用CLIENT CACHING NO命令炫狱。
例子如下:
CLIENT CACHING NO
+OK
GET MYKEY2
$8
MyValue2
現(xiàn)在如果MYKEY2的值被改變,客戶端不會(huì)收到緩存失效的消息剔猿。
在介紹完客戶端緩存追蹤的幾種模式后视译,下一個(gè)問題就是在何種情況下需要使用何種模式。這個(gè)問題實(shí)際上跟具體的應(yīng)用需求有關(guān)归敬,實(shí)際上是在Redis內(nèi)存使用和整個(gè)系統(tǒng)網(wǎng)絡(luò)資源上做取舍酷含。
你的應(yīng)用是否有很多緩存更新的場(chǎng)景?如果不是的話可以選用廣播模式汪茧,因?yàn)樵谶@種場(chǎng)景下大部分的鍵不會(huì)被更新椅亚,所以沒有必要在Redis端記住所有的鍵信息。
相反的舱污,如果應(yīng)用相對(duì)來(lái)說有很多更新緩存鍵的應(yīng)用場(chǎng)景呀舔,那么可以選用默認(rèn)模式,尤其當(dāng)有很多客戶端扩灯,但每個(gè)客戶端緩存鍵的數(shù)量相對(duì)較少的情況下媚赖。默認(rèn)模式是最好的選擇。
另外在我們的設(shè)計(jì)中珠插,我們會(huì)把CLIENT ID 也放到緩存無(wú)效信息中惧磺,這樣特定客戶端會(huì)忽略自己觸發(fā)的的緩存失效消息。(注在新版本的Redis 6.0.4中捻撑,可以使用NOLOOP來(lái)達(dá)到同樣的效果)
另外需要留意的是客戶端追蹤可以指定另一個(gè)REDIRECT選項(xiàng)磨隘,這個(gè)選項(xiàng)主要是為RESP2(舊版本)協(xié)議的客戶端提供的缤底。因?yàn)樵谂f版本的協(xié)議中,不支持從Redis服務(wù)器端向客戶端推送消息番捂。所以這個(gè)選項(xiàng)的作用是將緩存失效的消息推送到額外指定的客戶端个唧。
具體的使用方法是:
1. 在客戶端啟用客戶端緩存前,創(chuàng)建一個(gè)新的客戶端白嘁,使用CLIENT ID命令記錄下這個(gè)客戶端的ID坑鱼,讓后使用SUBSCRIBE redis:invalidate來(lái)訂閱緩存失效消息的頻道。
2. 在啟用客戶端緩存的時(shí)候使用這個(gè)客戶端ID來(lái)接收無(wú)效消息絮缅,例如如果ID為5鲁沥,則使用CLIENT TRACKING YES REDIRECT 5。
3. 這樣緩存無(wú)效消息將發(fā)送到這個(gè)指定客戶端中耕魄,但是需要注意的是這種方式會(huì)潛在帶來(lái)潛在的競(jìng)態(tài)條件画恰。因?yàn)槿绻瑫r(shí)有另外客戶端更新Redis中的鍵數(shù)據(jù),無(wú)法保證客戶端收到緩存失效消息的時(shí)間吸奴。
在新版本的Redis RESP3協(xié)議中支持從Redis服務(wù)器端推送消息允扇,但在RESP2協(xié)議中只支持客戶端發(fā)送請(qǐng)求Redis服務(wù)器端處理并回復(fù)。
1. 現(xiàn)在客戶端可以使用兩種不同版本的協(xié)議则奥。
2. 不需要?jiǎng)?chuàng)建新的TCP連接來(lái)接收緩存失效消息考润。創(chuàng)建新的連接對(duì)Paylocity來(lái)說是一個(gè)很大的問題,因?yàn)槲覀兪褂枚鄠€(gè)Redis集群读处,每個(gè)客戶端保留向集群中每一個(gè)節(jié)點(diǎn)的連接糊治。
3. 但現(xiàn)在RESP3協(xié)議的主要問題是大部分的客戶端庫(kù)都沒有支持這個(gè)協(xié)議。
下面我們討論使用客戶端追蹤的應(yīng)用具體實(shí)現(xiàn)步驟:
1. 在客戶端中罚舱,創(chuàng)建客戶端緩存并連接Redis
2. 使用CLIENT TRACKING ON 來(lái)啟用追蹤井辜。
3. 更新數(shù)據(jù)時(shí),更新Redis 和相應(yīng)的客戶端緩存管闷。需要記住的是Redis是單一數(shù)據(jù)來(lái)源粥脚。
4. 在讀取數(shù)據(jù)過程中,先從客戶端緩存中讀取包个,如果未找到數(shù)據(jù)則在Redis中讀取刷允。如果在Redis中找到數(shù)據(jù),則更新客戶端本地緩存碧囊,這樣的話下個(gè)請(qǐng)求將會(huì)通過本地緩存拿到新的數(shù)據(jù)恃锉。同時(shí)也需要從Redis中讀取TTL并設(shè)置本地緩存過期時(shí)間。
5. 監(jiān)聽連接的無(wú)效消息頻道呕臂,如果客戶端接收到Redis發(fā)送的緩存失效消息破托,則更新本地緩存。
Paylocity的計(jì)劃:
利用Redis的客戶端追蹤功能:
像之前介紹的那樣歧蒋,我們有比較少的web服務(wù)器土砂,而且在大多數(shù)情況下鍵值不會(huì)經(jīng)常改變州既,所以廣播模式比較適用于這種場(chǎng)景。
廣播前綴的方式非常適用于我們的場(chǎng)景萝映,因?yàn)槲覀儠?huì)創(chuàng)建一個(gè)大的Redis集群并共享給各個(gè)應(yīng)用吴叶。所以我們使用應(yīng)用代號(hào)來(lái)當(dāng)作BROADCAST的前綴。
使用RESP3客戶端:
1. 可以極大的減少總的Redis連接數(shù)
2. 目前我們使用StackExchange Redis客戶端序臂,一個(gè)非常好的客戶端蚌卤,只是不確定以后對(duì)RESP3協(xié)議的支持。
3. 如果我們自己實(shí)現(xiàn)redis客戶端的話也不會(huì)有大的問題奥秆,因?yàn)槲覀冎恍枰С諫ET,SET 和接收緩存失效消息逊彭。
以下作者提供了使用Redis客戶端緩存支持的例子: