作者:京東科技 王晨
Redis異步客戶端選型及落地實踐
可視化服務編排系統是能夠通過線上可視化拖拽、配置的方式完成對介面的編排,可在線完成服務的除錯、測驗,實作業務需求的交付,詳細內容可參考:https://mp.weixin.qq.com/s/5oN9JqWN7n-4Zv6B9K8kWQ,
為了支持更加廣泛的業務場景,可視化編排系統近期需要支持對快取的操作功能,為保證編排系統的性能,服務的執行程序采用了異步的方式,因此我們考慮使用Redis的異步客戶端來完成對快取的操作,
Redis客戶端
Jedis/Lettuce
Redis官方推薦的Redis客戶端有Jedis、Lettuce等等,其中Jedis 是老牌的 Redis 的 Java 實作客戶端,提供了比較全面的 Redis 命令的支持,在spring-boot 1.x 默認使用Jedis,
但是Jedis使用阻塞的 IO,且其方法呼叫都是同步的,程式流需要等到 sockets 處理完 IO 才能執行,不支持異步,在并發場景下,使用Jedis客戶端會耗費較多的資源,
此外,Jedis 客戶端實體不是執行緒安全的,要想保證執行緒安全,必須要使用連接池,每個執行緒需要時從連接池取出連接實體,完成操作后或者遇到例外歸還實體,當連接數隨著業務不斷上升時,對物理連接的消耗也會成為性能和穩定性的潛在風險點,因此在spring-boot 2.x中,redis客戶端默認改用了Lettuce,
我們可以看下 Spring Data Redis 幫助檔案給出的對比表格,里面詳細地記錄了兩個主流Redis客戶端之間的差異,

異步客戶端Lettuce
Spring Boot自2.0版本開始默認使用Lettuce作為Redis的客戶端,Lettuce客戶端基于Netty的NIO框架實作,對于大多數的Redis操作,只需要維持單一的連接即可高效支持業務端的并發請求 —— 這點與Jedis的連接池模式有很大不同,同時,Lettuce支持的特性更加全面,且其性能表現并不遜于,甚至優于Jedis,
Netty是由JBOSS提供的一個java開源框架,現為 Github上的獨立專案,Netty提供異步的、事件驅動的網路應用程式框架和工具,用以快速開發高性能、高可靠性的網路服務器和客戶端程式,
也就是說,Netty 是一個基于NIO的客戶、服務器端的編程框架,使用Netty 可以確保你快速和簡單的開發出一個網路應用,例如實作了某種協議的客戶、服務端應用,Netty相當于簡化和流線化了網路應用的編程開發程序,例如:基于TCP和UDP的socket服務開發,

上圖展示了Netty NIO的核心邏輯,NIO通常被理解為non-blocking I/O的縮寫,表示非阻塞I/O操作,圖中Channel表示一個連接通道,用于承載連接管理及讀寫操作;EventLoop則是事件處理的核心抽象,一個EventLoop可以服務于多個Channel,但它只會與單一執行緒系結,EventLoop中所有I/O事件和用戶任務的處理都在該執行緒上進行;其中除了選擇器Selector的事件監聽動作外,對連接通道的讀寫操作均以非阻塞的方式進行 —— 這是NIO與BIO(blocking I/O,即阻塞式I/O)的重要區別,也是NIO模式性能優異的原因,
Lettuce憑借單一連接就可以支持業務端的大部分并發需求,這依賴于以下幾個因素的共同作用:
1.Netty的單個EventLoop僅與單一執行緒系結,業務端的并發請求均會被放入EventLoop的任務佇列中,最終被該執行緒順序處理,同時,Lettuce自身也會維護一個佇列,當其通過EventLoop向Redis發送指令時,成功發送的指令會被放入該佇列;當收到服務端的回應時,Lettuce又會以FIFO的方式從佇列的頭部取出對應的指令,進行后續處理,
2.Redis服務端本身也是基于NIO模型,使用單一執行緒處理客戶端請求,雖然Redis能同時維持成百上千個客戶端連接,但是在某一時刻,某個客戶端連接的請求均是被順序處理及回應的,
3.Redis客戶端與服務端通過TCP協議連接,而TCP協議本身會保證資料傳輸的順序性,

如此,Lettuce在保證請求處理順序的基礎上,天然地使用了管道模式(pipelining)與Redis互動 —— 在多個業務執行緒并發請求的情況下,客戶端不必等待服務端對當前請求的回應,即可在同一個連接上發出下一個請求,這在加速了Redis請求處理的同時,也高效地利用了TCP連接的全雙工特性(full-duplex),而與之相對的,在沒有顯式指定使用管道模式的情況下,Jedis只能在處理完某個Redis連接上當前請求的回應后,才能繼續使用該連接發起下一個請求,

在并發場景下,業務系統短時間內可能會發出大量請求,在管道模式中,這些請求被統一發送至Redis服務端,待處理完成后統一回傳,能夠大大提升業務系統的運行效率,突破性能瓶頸,R2M采用了Redis Cluster模式,在通過Lettuce連接R2M之前,應該先對Redis Cluster模式有一定的了解,
Redis Cluster模式
在redis3.0之前,如果想搭建一個集群架構還是挺復雜的,就算是基于一些第三方的中間件搭建的集群總感覺有那么點差強人意,或者基于sentinel哨兵搭建的主從架構在高可用上表現又不是很好,尤其是當資料量越來越大,單純主從結構無法滿足對性能的需求時,矛盾便產生了,
隨著redis cluster的推出,這種海量資料+高并發+高可用的場景真正從根本上得到了有效的支持,
cluster 模式是redis官方提供的集群模式,使用了Sharding 技術,不僅實作了高可用、讀寫分離、也實作了真正的分布式存盤,
集群內部通信
在redis cluster集群內部通過gossip協議進行通信,集群元資料分散的存在于各個節點,通過gossip進行元資料的交換,
不同于zookeeper分布式協調中間件,采用集中式的集群元資料存盤,redis cluster采用分布式的元資料管理,優缺點還是比較明顯的,在redis中集中式的元資料管理類似sentinel主從架構模式,集中式有點在于元資料更新實效性更高,但容錯性不如分布式管理,gossip協議優點在于大大增強集群容錯性,
redis cluster集群中單節點一般配置兩個埠,一個埠如6379對外提供api,另一個一般是加1w,比如16379進行節點間的元資料交換即用于gossip協議通訊,
gossip協議包含多種訊息,如ping pong,meet,fail等,
1.meet:集群中節點通過向新加入節點發送meet訊息,將新節點加入集群中,
2.ping:節點間通過ping命令交換元資料,
3.pong:回應ping,
4.fail:某個節點主觀認為某個節點宕機,會向其他節點發送fail訊息,進行客觀宕機判定,
分片和尋址演算法
hash slot即hash槽,redis cluster采用的正式這種hash槽演算法實作的尋址,在redis cluster中固定的存在16384個hash slot,

如上圖所示,如果我們有三個節點,每個節點都是一主一從的主從結構,redis cluster初始化時會自動均分給每個節點16384個slot,當增加一個節點4,只需要將原來node1~node3節點部分slot上的資料遷移到節點4即可,在redis cluster中資料遷移并不會阻塞主行程,對性能影響是十分有限的,總結一句話就是hash slot演算法有效的減少了當節點發生變化導致的資料漂移帶來的性能開銷,
集群高可用和主備切換
主觀宕機和客觀宕機:
某個節點會周期性的向其他節點發送ping訊息,當在一定時間內未收到pong訊息會主觀認為該節點宕機,即主觀宕機,然后該節點向其他節點發送fail訊息,其他超過半數節點也確認該節點宕機,即客觀宕機,十分類似sentinel的sdown和odown,
客觀宕機確認后進入主備切換階段及從節點選舉,
節點選舉:
檢查每個 slave node 與 master node 斷開連接的時間,如果超過了 cluster-node-timeout * cluster-slave-validity-factor,那么就沒有資格切換成 master,
每個從節點,都根據自己對 master 復制資料的 offset,來設定一個選舉時間,offset 越大(復制資料越多)的從節點,選舉時間越靠前,優先進行選舉,
所有的 master node 開始 slave 選舉投票,給要進行選舉的 slave 進行投票,如果大部分 master node(N/2 + 1)都投票給了某個從節點,那么選舉通過,那個從節點可以切換成 master,
從節點執行主備切換,從節點切換為主節點,
Lettuce的使用
建立連接
使用Lettuce大致分為以下三步:
1.基于Redis連接資訊創建RedisClient
2.基于RedisClient創建StatefulRedisConnection
3.從Connection中獲取Command,基于Command執行Redis命令操作,
由于Lettuce客戶端提供了回應式、同步和異步三種命令,從Connection中獲取Command時可以指定命令型別進行獲取,
在本地創建Redis Cluster集群,設定主從關系如下:
7003(M) --> 7001(S)
7004(M) --> 7002(S)
7005(M) --> 7000(S)
List<RedisURI> servers = new ArrayList<>();
servers.add(RedisURI.create("127.0.0.1", 7000));
servers.add(RedisURI.create("127.0.0.1", 7001));
servers.add(RedisURI.create("127.0.0.1", 7002));
servers.add(RedisURI.create("127.0.0.1", 7003));
servers.add(RedisURI.create("127.0.0.1", 7004));
servers.add(RedisURI.create("127.0.0.1", 7005));
//創建客戶端
RedisClusterClient client = RedisClusterClient.create(servers);
//創建連接
StatefulRedisClusterConnection<String, String> connection = client.connect();
//獲取異步命令
RedisAdvancedClusterAsyncCommands<String, String> commands = connection.async();
//執行GET命令
RedisFuture<String> future = commands.get("test-lettuce-key");
try {
String result = future.get();
log.info("Get命令回傳:{}", result);
} catch (Exception e) {
log.error("Get命令執行例外", e);
}
可以看到成功地獲取到了值,由日志可以看出該請求發送到了7004所在的節點上,順利拿到了對應的值并進行回傳,

作為一個需要長時間保持的客戶端,保持其與集群之間連接的穩定性是至關重要的,那么集群在運行程序中會發生哪些特殊情況呢?作為客戶端又應該如何應對呢?這就要引出智能客戶端(smart client)這個概念了,
智能客戶端
在Redis Cluster運行程序中,所有的資料不是永遠固定地保存在某一個節點上的,比如遇到cluster擴容、節點宕機、資料遷移等情況時,都會導致集群的拓撲結構發生變化,此時作為客戶端需要對這一類情況作出應對,來保證連接的穩定性以及服務的可用性,隨著以上問題的出現,smart client這個概念逐漸走到了人們的視野中,智能客戶端會在內部維護hash槽與節點的映射關系,大家耳熟能詳的Jedis和Lettuce都屬于smart client,客戶端在發送請求時,會先根據CRC16(key)%16384計算key對應的hash槽,通過映射關系,本地就可實作鍵到節點的查找,從而保證IO效率的最大化,
但如果出現故障轉移或者hash槽遷移時,這個映射關系是如何維護的呢?
客戶端重定向
MOVED
當Redis集群發生資料遷移時,當對應的hash槽已經遷移到變的節點時,服務端會回傳一個MOVED重定向錯誤,此時并告訴客戶端這個hash槽遷移后的節點IP和埠是多少;客戶端在接收到MOVED錯誤時,會更新本地的映射關系,并重新向新節點發送請求命令,

ASK
Redis集群支持在線遷移槽(slot)和資料來完成水平伸縮,當slot對應的資料從源節點到目標節點遷移程序中,客戶端需要做到智能識別,保證鍵命令可正常執行,例如當一個slot資料從源節點遷移到目標節點時,期間可能出現一部分資料在源節點,而另一部分在目標節點,如下圖所示

當出現上述情況時,客戶端鍵命令執行流程將發生變化,如下所示:
1)客戶端根據本地slots快取發送命令到源節點,如果存在鍵物件則直 接執行并回傳結果給客戶端
2)如果鍵物件不存在,則可能存在于目標節點,這時源節點會回復 ASK重定向例外,
3)客戶端從ASK重定向例外提取出目標節點資訊,發送asking命令到目標節點打開客戶端連接標識,再執行鍵命令,如果存在則執行,不存在則回傳不存在資訊,
在客戶端收到ASK錯誤時,不會更新本地的映射關系
節點宕機觸發主備切換
上文提到,如果redis集群在運行程序中,某個主節點由于某種原因宕機了,此時就會觸發集群的節點選舉機制,選舉其中一個從節點作為新的主節點,進入主備切換,在主備切換期間,新的節點沒有被選舉出來之前,打到該節點上的請求理論上是無法得到執行的,可能會產生超時錯誤,在主備切換完成之后,集群拓撲更新完成,此時客戶端應該向集群請求新的拓撲結構,并更新至本地的映射表中,以保證后續命令的正確執行,
有意思的是,Jedis在集群主備切換完成之后,是會主動拉取最新的拓撲結構并進行更新的,但是在使用Lettuce時,發現在集群主備切換完成之后,連接并沒有恢復,打到該節點上的命令依舊會執行失敗導致超時,必須要重啟業務程式才能恢復連接,
在使用Lettuce時,如果不進行設定,默認是不會觸發拓撲重繪的,因此在主備切換完成后,Lettuce依舊使用本地的映射表,將請求打到已經掛掉的節點上,就會導致持續的命令執行失敗的情況,
可以通過以下代碼來設定Lettuce的拓撲重繪策略,開啟基于事件的自適應拓撲重繪,其中包括了MOVED、 ASK、PERSISTENT_RECONNECTS等觸發器,當客戶端觸發這些事件,并且持續時間超過設定閾值后,觸發拓撲重繪,也可以通過enablePeriodicRefresh()設定定時重繪,不過建議這個時間不要太短,
// 設定基于事件的自適應重繪策略
ClusterTopologyRefreshOptions topologyRefreshOptions = ClusterTopologyRefreshOptions.builder()
//開啟自適應拓撲重繪
.enableAllAdaptiveRefreshTriggers()
//自適應拓撲重繪事件超時時間,超時后進行重繪
.adaptiveRefreshTriggersTimeout(Duration.ofSeconds(30))
.build();
redisClusterClient.setOptions(ClusterClientOptions.builder()
.topologyRefreshOptions(topologyRefreshOptions)
// redis命令超時時間
.timeoutOptions(TimeoutOptions.enabled(Duration.ofSeconds(30)))
.build());
進行以上設定并進行驗證,集群在主備切換完成后,客戶端在段時間內恢復了連接,能夠正常存取資料了,
總結
對于快取的操作,客戶端與集群之間連接的穩定性是保證資料不丟失的關鍵,Lettuce作為熱門的異步客戶端,對于集群中產生的一些突發狀況是具備處理能力的,只不過在使用的時候需要進行設定,本文目的在于將在開發快取操作功能時遇到的問題,以及將一些涉及到的底層知識做一下總結,也希望能給大家一些幫助,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/543264.html
標籤:其他
