本文探究Redis最新特性--客戶端快取在SpringBoot上的應用實戰。
Redis Tracking
Redis客戶端快取機制基於Redis Tracking機制實現的。我們先了解一下Redis Tracking機制。
為什麼需要Redis Tracking
Redis由於速度快、效能高,常常作為MySQL等傳統資料庫的快取資料庫。但由於Redis是遠端服務,查詢Redis需要通過網路請求,在高併發查詢情景中難免造成效能損耗。所以,高併發應用通常引入本地快取,在查詢Redis前先檢查本地快取是否存在資料。
假如使用MySQL儲存資料,那麼資料查詢流程下圖所示。
引入多端快取後,修改資料時,各資料快取端如何保證資料一致是一個難題。通常的做法是修改MySQL資料,並刪除Redis快取、本地快取。當使用者發現快取不存在時,會重新查詢MySQL資料,並設定Redis快取、本地快取。
在分散式系統中,某個節點修改資料後不僅要刪除當前節點的本地快取,還需要傳送請求給叢集中的其他節點,要求它們刪除該資料的本地快取,如下圖所示。如果分散式系統中節點很多,那麼該操作會造成不少效能損耗。
為此,Redis 6提供了Redis Tracking機制,對該快取方案進行了優化。開啟Redis Tracking後,Redis伺服器會記錄客戶端查詢的所有鍵,並在這些鍵發生變更後,傳送失效訊息通知客戶端這些鍵已變更,這時客戶端需要將這些鍵的本地快取刪除。基於Redis Tracking機制,某個節點修改資料後,不需要再在叢集廣播“刪除本地快取”的請求,從而降低了系統複雜度,並提高了效能。
Redis Tracking的應用
下表展示了Redis Tracking的基本使用
(1)為了支援Redis伺服器推送訊息,Redis在RESP2協議上進行了擴充套件,實現了RESP3協議。HELLO 3命令表示客戶端與Redis伺服器之間使用RESP3協議通訊。
注意:Redis 6.0提供了Redis Tracking機制,但該版本的redis-cli並不支援RESP3協議,所以這裡需要使用Redis 6.2版本的redis-cli進行演示。
(2)CLIENT TRACKING on命令的作用是開啟Redis Tracking機制,此後Redis伺服器會記錄客戶端查詢的鍵,並在這些鍵變更後推送失效訊息通知客戶端。失效訊息以invalidate開頭,後面是失效鍵陣列。
上表中的客戶端 client1 查詢了鍵 score 後,客戶端 client2 修改了該鍵,這時 Redis 伺服器會馬上推送失效訊息給客戶端 client1,但 redis-cli 不會直接展示它收到的推送訊息,而是在下一個請求返回後再展示該訊息,所以 client1 重新傳送了一個 PING請求。
上面使用的非廣播模式,另外,Redis Tracking還支援廣播模式。在廣播模式下,當變更的鍵以客戶端關注的字首開頭時,Redis伺服器會給所有關注了該字首的客戶端傳送失效訊息,不管客戶端之前是否查詢過這些鍵。
下表展示瞭如何使用Redis Tracking的廣播模式。
說明一下CLIENT TRACKING命令中的兩個引數:
BCAST引數:啟用廣播模式。
PREFIX引數:宣告客戶端關注的字首,即客戶端只關注cache開頭的鍵。
強調一下非廣播模式與廣播模式的區別:
非廣播模式:Redis伺服器記錄客戶查詢過的鍵,當這些鍵發生變化時,Redis傳送失效訊息給客戶端。
廣播模式:Redis伺服器不記錄客戶查詢過的鍵,當變更的鍵以客戶端關注的字首開頭時,Redis就會傳送失效訊息給客戶端。
關於Redis Tracking的更多內容,我已經在新書《Redis核心原理與實踐》中詳細分析,這裡不再贅述。
Redis客戶端快取
既然Redis提供了Tracking機制,那麼客戶端就可以基於該機制實現客戶端快取了。
Lettuce實現
Lettuce(6.1.5版本)已經支援Redis客戶端快取(單機模式下),使用CacheFrontend類可以實現客戶端快取。
public static void main(String[] args) throws InterruptedException {
// [1]
RedisURI redisUri = RedisURI.builder()
.withHost("127.0.0.1")
.withPort(6379)
.build();
RedisClient redisClient = RedisClient.create(redisUri);
// [2]
StatefulRedisConnection<String, String> connect = redisClient.connect();
Map<String, String> clientCache = new ConcurrentHashMap<>();
CacheFrontend<String, String> frontend = ClientSideCaching.enable(CacheAccessor.forMap(clientCache), connect,
TrackingArgs.Builder.enabled());
// [3]
while (true) {
String cachedValue = frontend.get("k1");
System.out.println("k1 ---> " + cachedValue);
Thread.sleep(3000);
}
}
- 構建RedisClient。
- 構建CacheFrontend。
ClientSideCaching.enable開啟客戶端快取,即傳送“CLIENT TRACKING”命令給Redis伺服器,要求Redis開啟Tracking機制。
最後一個引數指定了Redis Tracking的模式,這裡用的是最簡單的非廣播模式。
這裡可以看到,通過Map儲存客戶端快取的內容。 - 重複查詢同一個值,檢視快取是否生效。
我們可以通過Redis的Monitor命令監控Redis服務收到的命令,使用該命令就可以看到,開啟客戶端快取後,Lettuce不會重複查詢同一個鍵。
而且我們修改這個鍵後,Lettuce會重新查詢這個鍵的最新值。
通過Redis的Client List命令可以檢視連線的資訊
> CLIENT LIST
id=4 addr=192.168.56.1:50402 fd=7 name= age=23 idle=22 flags=t ...
flags=t
代表這個連線啟動了Tracking機制。
SpringBoot應用
那麼如何在SpringBoot上使用呢?請看下面的例子
@Bean
public CacheFrontend<String, String> redisCacheFrontend(RedisConnectionFactory redisConnectionFactory) {
StatefulRedisConnection connect = getRedisConnect(redisConnectionFactory);
if (connect == null) {
return null;
}
CacheFrontend<String, String> frontend = ClientSideCaching.enable(
CacheAccessor.forMap(new ConcurrentHashMap<>()),
connect,
TrackingArgs.Builder.enabled());
return frontend;
}
private StatefulRedisConnection getRedisConnect(RedisConnectionFactory redisConnectionFactory) {
if(redisConnectionFactory instanceof LettuceConnectionFactory) {
AbstractRedisClient absClient = ((LettuceConnectionFactory) redisConnectionFactory).getNativeClient();
if (absClient instanceof RedisClient) {
return ((RedisClient) absClient).connect();
}
}
return null;
}
其實也簡單,通過RedisConnectionFactory獲取一個StatefulRedisConnection連線,就可以建立CacheFrontend了。
這裡RedisClient#connect方法會建立一個新的連線,這樣可以將使用客戶端快取、不使用客戶端快取的連線區分。
結合Guava快取
Lettuce的StatefulRedisConnection類還提供了addListener方法,可以設定回撥方法處理Redis推送的訊息。
利用該方法,我們可以將Guava的快取與Redis客戶端快取結合
@Bean
public LoadingCache<String, String> redisGuavaCache(RedisConnectionFactory redisConnectionFactory) {
// [1]
StatefulRedisConnection connect = getRedisConnect(redisConnectionFactory);
if (connect != null) {
// [2]
LoadingCache<String, String> redisCache = CacheBuilder.newBuilder()
.initialCapacity(5)
.maximumSize(100)
.expireAfterWrite(5, TimeUnit.MINUTES)
.build(new CacheLoader<String, String>() {
public String load(String key) {
String val = (String)connect.sync().get(key);
return val == null ? "" : val;
}
});
// [3]
connect.sync().clientTracking(TrackingArgs.Builder.enabled());
// [4]
connect.addListener(message -> {
if (message.getType().equals("invalidate")) {
List<Object> content = message.getContent(StringCodec.UTF8::decodeKey);
List<String> keys = (List<String>) content.get(1);
keys.forEach(key -> {
redisCache.invalidate(key);
});
}
});
return redisCache;
}
return null;
}
- 獲取Redis連線。
- 建立Guava快取類LoadingCache,該快取類如果發現資料不存在,則查詢Redis。
- 開啟Redis客戶端快取。
- 新增回撥函式,如果收到Redis傳送的失效訊息,則清除Guava快取。
Redis Cluster模式
上面說的應用必須在Redis單機模式下(或者主從、Sentinel模式),遺憾的是,
目前發現Lettuce(6.1.5版本)還沒有支援Redis Cluster下的客戶端快取,
簡單看了一下原始碼,目前發現如下原因:
Cluster模式下,Redis命令需要根據命令的鍵,重定向到鍵的儲存節點執行。
而對於“CLIENT TRACKING”這個沒有鍵的命令,Lettuce並沒有將它傳送給Cluster中所有的節點,而是將它傳送給一個固定的預設的節點(可檢視ClusterDistributionChannelWriter類),所以通過StatefulRedisClusterConnection呼叫RedisAdvancedClusterCommands.clientTracking方法並沒有開啟Redis服務的Tracking機制。
這個其實也可以修改,有時間再研究一下。
需要注意的問題
那麼單機模式下,Lettuce的客戶端快取就真的沒有問題了嗎?
仔細思考一下Redis Tracking的設計,發現使用Redis客戶端快取有兩個點需要關注:
- 開啟客戶端快取後,Redis連線不能斷開。
如果Redis連線斷了,並且客戶端自動重連,那麼新的連線是沒有開啟Tracking機制的,該連線查詢的鍵不會受到失效訊息,後果很嚴重。
同樣,開啟Tracking的連線和查詢快取鍵的連線必須是同一個,不能使用A連線開啟Tracking機制,使用B連線去查詢快取鍵(所以客戶端不能使用連線池)。
Redis伺服器可以設定timeout配置,自動超過該配置沒有傳送請求的連線。
而Lettuce有自動重連機制,重連後的連線將收不到失效訊息。
有兩個解決思路:
(1)實現Lettuce心跳機制,定時傳送PING命令以維持連線。
(2)即使使用心跳機制,Redis連線依然可能斷開(網路跳動等原因),可以修改自動重連機制(ReconnectionHandler),增加如下邏輯:如果連線原來開啟了Tracking機制,則重連後需要自動開啟Tracking機制。
需要注意,如果使用的是非廣播模式,需要清空舊連線快取的資料,因為連線已經變更,Redis伺服器不會將舊連線的失效訊息傳送給新連線。
- 啟用快取的連線與未啟動快取的連線應該區分。
這點比較簡單,上例例子中都使用RedisClient#connect方法建立一個新的連線,專用於客戶端快取。
客戶端快取是一個強大的功能,需要我們去用好它。可惜當前暫時還沒有完善的Java客戶端支援,本書說了我的一些分析與思路,歡迎探討。我後續會關注繼續Lettuce的更新,如果Lettuce提供了完善的Redis客戶端快取支援,再更新本文。
關於Redis Tracking的詳細使用與實現原理,我在新書《Redis核心原理與實踐》做了詳盡分析,文章最後,介紹一下這本書:
本書通過深入分析Redis 6.0原始碼,總結了Redis核心功能的設計與實現。通過閱讀本書,讀者可以深入理解Redis內部機制及最新特性,並學習到Redis相關的資料結構與演算法、Unix程式設計、儲存系統設計,分散式系統架構等一系列知識。
經過該書編輯同意,我會繼續在個人技術公眾號(binecy)釋出書中部分章節內容,作為書的預覽內容,歡迎大家查閱,謝謝。