Redis服務(wù)器輔助的客戶端緩存
客戶端緩存是一種用于創(chuàng)建高性能服務(wù)的技術(shù)。它利用應(yīng)用服務(wù)器中的可用內(nèi)存,這些服務(wù)器通常是與數(shù)據(jù)庫節(jié)點(diǎn)不同的計算機(jī),以便將數(shù)據(jù)庫信息的某些子集直接存儲在應(yīng)用程序端。
通常當(dāng)需要一些數(shù)據(jù)時,應(yīng)用服務(wù)器會向數(shù)據(jù)庫詢問這些信息,如下圖所示:
+-------------+ +----------+
| | ------- GET user:1234 -------> | |
| Application | | Database |
| | <---- username = Alice ------- | |
+-------------+ +----------+
當(dāng)使用客戶端緩存時,應(yīng)用程序?qū)⒅苯釉趹?yīng)用程序內(nèi)存中存儲流行查詢的答復(fù),以便以后可以重用這些答復(fù),而無需再次聯(lián)系數(shù)據(jù)庫。
+-------------+ +----------+
| | | |
| Application | ( No chat needed ) | Database |
| | | |
+-------------+ +----------+
| Local cache |
| |
| user:1234 = |
| username |
| Alice |
+-------------+
雖然用于本地緩存的應(yīng)用程序內(nèi)存可能不是很大,但是訪問本地計算機(jī)內(nèi)存所需的時間比請求數(shù)據(jù)庫之類的網(wǎng)絡(luò)服務(wù)要小幾個數(shù)量級。由于經(jīng)常非常頻繁地訪問同一小部分?jǐn)?shù)據(jù),因此這種模式可以大大減少應(yīng)用程序獲取數(shù)據(jù)的延遲,同時也減少數(shù)據(jù)庫端的負(fù)載。
此外,有許多數(shù)據(jù)集中的項很少更改。例如,社交網(wǎng)絡(luò)中的大多數(shù)用戶帖子要么是不可變的,要么很少被用戶編輯。再加上一個事實,通常只有一小部分的帖子非常受歡迎,要么是因為一小部分用戶有很多關(guān)注者,要么是因為最近的帖子有更多的可見性,這就很清楚為什么這種模式非常有用。
通常,客戶端緩存的兩個主要優(yōu)點(diǎn)是:
- 數(shù)據(jù)可用,延遲非常小。
- 數(shù)據(jù)庫系統(tǒng)接收的查詢較少,允許使用較少的節(jié)點(diǎn)為同一數(shù)據(jù)集提供服務(wù)。
存在問題
上述模式的一個問題是如何使應(yīng)用程序所保存的信息無效,以避免向用戶呈現(xiàn)過時的數(shù)據(jù)。例如,在上面的應(yīng)用程序本地緩存用戶:1234信息,Alice可能會將她的用戶名更新為Flora。然而,應(yīng)用程序可能繼續(xù)為用戶1234提供舊用戶名。
根據(jù)我們所建模的應(yīng)用程序的具體情況,這個問題并不是什么大問題,因此客戶機(jī)只會使用固定的最大“生存時間”來存儲緩存的信息。一旦經(jīng)過給定的時間,信息將不再被視為有效。在使用Redis時,更復(fù)雜的模式利用Pub/Sub系統(tǒng)向監(jiān)聽的客戶端發(fā)送無效消息。這是可以實現(xiàn)的,但是從所使用的帶寬的角度來看,這是非常棘手和昂貴的,因為這樣的模式通常涉及到向應(yīng)用程序中的每個客戶端發(fā)送無效消息,即使某些客戶端可能沒有無效數(shù)據(jù)的任何副本。此外,每個更改數(shù)據(jù)的應(yīng)用程序查詢都需要使用PUBLISH命令,這會使數(shù)據(jù)庫花費(fèi)更多的CPU時間來處理該命令。
不管使用什么模式,有一個簡單的事實:許多非常大的應(yīng)用程序?qū)崿F(xiàn)某種形式的客戶端緩存,因為這是擁有快速存儲或快速緩存服務(wù)器的下一個邏輯步驟。為此,redis6實現(xiàn)了對客戶端緩存的直接支持,以使該模式實現(xiàn)起來更簡單、更易訪問、更可靠、更高效。
客戶端緩存的Redis實現(xiàn)
Redis客戶端緩存支持稱為跟蹤,有兩種模式:
- 在默認(rèn)模式下,服務(wù)器會記住給定客戶機(jī)訪問的密鑰,并在修改相同的密鑰時發(fā)送無效消息。這會消耗服務(wù)器端的內(nèi)存,但只會為客戶端可能在內(nèi)存中擁有的一組密鑰發(fā)送無效消息。
- 在廣播模式下,服務(wù)器不會試圖記住給定客戶機(jī)訪問的密鑰,因此這種模式在服務(wù)器端根本不消耗任何內(nèi)存。相反,客戶機(jī)訂閱密鑰前綴,如object:或user:,并且在每次碰到與該前綴匹配的密鑰時都會收到一條通知消息。
回顧一下,現(xiàn)在讓我們暫時忘掉廣播模式,集中討論第一種模式。稍后我們將更詳細(xì)地描述廣播。
- 如果需要,客戶端可以啟用跟蹤。連接在未啟用跟蹤的情況下啟動。
- 啟用跟蹤后,服務(wù)器會記住每個客戶端在連接生存期內(nèi)請求的密鑰(通過發(fā)送關(guān)于這些密鑰的read命令)。
- 當(dāng)某個客戶端修改了某個密鑰,或者因為該密鑰具有相關(guān)的過期時間而被逐出,或者由于maxmemory策略而被逐出時,所有啟用了跟蹤且可能緩存了該密鑰的客戶端都將收到一條無效消息。
- 當(dāng)客戶機(jī)接收到無效消息時,它們需要刪除相應(yīng)的密鑰,以避免提供過時的數(shù)據(jù)。
這是協(xié)議的一個例子:
Client 1 -> Server: CLIENT TRACKING ON
Client 1 -> Server: GET foo
(The server remembers that Client 1 may have the key "foo" cached)
(Client 1 may remember the value of "foo" inside its local memory)
Client 2 -> Server: SET foo SomeOtherValue
Server -> Client 1: INVALIDATE "foo"
從表面上看,這看起來很不錯,但是如果你認(rèn)為,在每一個長時間連接的故事中,有10k個連接的客戶端都需要數(shù)百萬個密鑰,那么服務(wù)器最終會存儲太多的信息。為此,Redis使用了兩個關(guān)鍵思想來限制服務(wù)器端的內(nèi)存使用量,以及處理實現(xiàn)該功能的數(shù)據(jù)結(jié)構(gòu)的CPU成本:
- 服務(wù)器會記住可能已在單個全局表中緩存給定鍵的客戶端列表。這個表叫做失效表。這樣的無效表可以包含最大數(shù)量的條目,如果插入了一個新的鍵,服務(wù)器可以通過假裝該鍵被修改(即使沒有修改),并向客戶端發(fā)送無效消息來逐出舊條目。這樣做,它可以回收用于此密鑰的內(nèi)存,即使這將迫使擁有密鑰本地副本的客戶端將其逐出。
- 在失效表中,我們實際上不需要存儲指向客戶端結(jié)構(gòu)的指針,這將在客戶端斷開連接時強(qiáng)制執(zhí)行垃圾回收過程:相反,我們所做的只是存儲客戶端ID(每個Redis客戶端都有一個唯一的數(shù)字ID)。如果一個客戶機(jī)斷開連接,信息將隨著緩存槽的失效而逐漸被垃圾回收。
- 只有一個鍵名稱空間,不除以數(shù)據(jù)庫編號。因此,如果一個客戶機(jī)正在緩存數(shù)據(jù)庫2中的key foo,而其他一些客戶機(jī)更改了數(shù)據(jù)庫3中key foo的值,那么仍然會發(fā)送一條無效消息。通過這種方式,我們可以忽略數(shù)據(jù)庫編號,從而降低內(nèi)存使用量和實現(xiàn)復(fù)雜性。
雙連接方式
使用Redis 6支持的新版Redis協(xié)議RESP3,可以在同一連接中運(yùn)行數(shù)據(jù)查詢和接收失效消息。然而,許多客戶端實現(xiàn)可能更喜歡使用兩個獨(dú)立的連接來實現(xiàn)客戶端緩存:一個用于數(shù)據(jù),另一個用于無效消息。因此,當(dāng)客戶端啟用跟蹤時,它可以指定通過指定不同連接的“客戶端ID”將無效消息重定向到另一個連接。許多數(shù)據(jù)連接可以將無效消息重定向到同一個連接,這對于實現(xiàn)連接池的客戶端很有用。雙連接模型是唯一支持RESP2的模型(它缺乏在同一連接中復(fù)用不同類型信息的能力)。
我們將展示一個例子,這一次在舊的RRESP2模式下使用實際的Redis協(xié)議,如何完成會話,包括以下步驟:啟用跟蹤重定向到另一個連接,請求密鑰,以及在該密鑰被修改后獲得無效消息。
首先,客戶機(jī)打開第一個將用于失效的連接,請求連接ID,并通過Pub/Sub訂閱用于在RESP2模式下獲取失效消息的特殊通道(記住RESP2是通常的Redis協(xié)議,而不是可以與Redis一起使用的更高級的協(xié)議)6使用HELLO命令):
(Connection 1 -- used for invalidations)
CLIENT ID
:4
SUBSCRIBE __redis__:invalidate
*3
$9
subscribe
$20
__redis__:invalidate
:1
現(xiàn)在我們可以從數(shù)據(jù)連接啟用跟蹤:
(Connection 2 -- data connection)
CLIENT TRACKING on REDIRECT 4
+OK
GET foo
$3
bar
客戶機(jī)可能決定在本地內(nèi)存中緩存“foo”=>“bar”。
另一個客戶端現(xiàn)在將修改“foo”鍵的值:
(Some other unrelated connection)
SET foo bar
+OK
因此,失效連接將接收一條消息,使指定的鍵失效。
(Connection 1 -- used for invalidations)
*3
$7
message
$20
__redis__:invalidate
*1
$3
foo
客戶端將檢查這種緩存槽中是否有緩存的密鑰,并將逐出不再有效的信息。
請注意,發(fā)布/訂閱消息的第三個元素不是一個鍵,而是一個只有一個元素的Redis數(shù)組。因為我們發(fā)送了一個數(shù)組,如果有一組鍵要失效,我們可以在一條消息中完成。
要了解RESP2使用的客戶端緩存和用于讀取無效消息的Pub/Sub連接的一個非常重要的事情是,為了重用舊的客戶端實現(xiàn),使用Pub/Sub完全是一個技巧,但實際上消息并不是真正發(fā)送到一個通道并由訂閱它的所有客戶端接收。只有我們在CLIENT命令的REDIRECT參數(shù)中指定的連接才會真正接收Pub/Sub消息,這使得特性更具可伸縮性。
當(dāng)改為使用RESP3時,無效消息將作為推送消息發(fā)送(在同一連接中,或者在使用重定向時在輔助連接中發(fā)送)(有關(guān)詳細(xì)信息,請閱讀RESP3規(guī)范)。
什么跟蹤記錄
默認(rèn)情況下,客戶機(jī)不需要告訴服務(wù)器它們正在緩存哪些密鑰。服務(wù)器會跟蹤只讀命令上下文中提到的每個鍵,因為它可以被緩存。
這樣做的明顯優(yōu)點(diǎn)是不需要客戶機(jī)告訴服務(wù)器它在緩存什么。此外,在許多客戶機(jī)實現(xiàn)中,這正是您所希望的,因為一個好的解決方案可以是使用先進(jìn)先出的方法緩存所有尚未緩存的對象:我們可能希望緩存固定數(shù)量的對象,我們檢索到的每一個新數(shù)據(jù)都可以緩存它,丟棄最舊的緩存對象。更高級的實現(xiàn)可能會放棄使用最少的對象或類似對象。
請注意,不管怎樣,如果服務(wù)器上有寫流量,緩存槽將在這段時間內(nèi)失效。一般情況下,當(dāng)服務(wù)器假定我們得到的東西也要緩存時,我們正在權(quán)衡:
- 當(dāng)客戶機(jī)傾向于用一個歡迎新對象的策略緩存許多東西時,這種方法更有效。
- 服務(wù)器將被迫保留有關(guān)客戶端密鑰的更多數(shù)據(jù)。
- 客戶端將收到關(guān)于它沒有緩存的對象的無效消息。
標(biāo) 題:《Redis 6 客戶端緩存》
作 者:zeekling
提 示:轉(zhuǎn)載請注明文章轉(zhuǎn)載自個人博客:小令童鞋