網易架構師心得:Springboot下使用redis踩過的坑

分享一下個人網易架構師同事在spring boot下使用redis的心得~redis

首先總結了redis服務端單線程工做模型,redis四種部署方式及使用場景,而後從源碼的角度上,分析springboot在jedis和lettuce客戶端下使用redis的一些坑~尤爲是在集羣模式下的一些不兼容問題!spring

最近整理的Java架構學習視頻和大廠項目底層知識點,須要的同窗歡迎私信我發給你~一塊兒學習進步!安全

1 Redis服務端單線程模型

![網易架構師心得:Springboot下使用redis踩過的坑](https://p3-tt.byteimg.com/origin/pgc-image/b3645477e82644bfa65c5dea095496cf?from=pc)

redis 內部使用文件事件處理(file event handler)處理客戶端的請求,文件事件處理器是單線程的,因此redis才叫作單線程的模型。springboot

文件事件處理器的結構包含4個部分:多個socket、IO 多路複用程序、文件事件分派器、事件處理器(鏈接應答處理器、命令請求處理器、命令回覆處理器)。服務器

文件事件處理器採用 IO 多路複用機制同時監聽多個 socket,根據 socket 上的事件來選擇對應的事件處理器進行處理。markdown

Redis客戶端經過socket鏈接reids服務端,多個 socket 可能會併發產生不一樣的操做,每一個操做對應不一樣的文件事件,可是 IO 多路複用程序會監聽多個 socket,將 socket 產生的事件放入隊列中排隊,事件分派器每次從隊列中取出一個事件,把該事件交給對應的事件處理器進行處理。網絡

redis 單線程模型也能效率高的緣由:多線程

  1. 純內存操做
  2. 基於非阻塞的 IO 多路複用機制
  3. 單線程反而避免了多線程的頻繁上下文切換問題

爲何redis採用單線程模型呢?架構

若是採用多線程模型,cpu須要進行上下文切換,假設1MB的數據由多個線程讀取了1000次,那麼就有1000次時間上下文的切換,那麼就有1500ns * 1000 = 1500us,而單線程的讀完1MB數據才250us ,因此徹底不必使用多線程。併發

何時適合採用多線程的方案呢?

對於慢速設備:磁盤,網絡,SSD 等等,將請求和處理的線程不綁定,請求的線程將請求放在一個buff裏,而後等buff快滿了,處理的線程再去處理這個buff。而後由這個buff 統一的去寫入磁盤,或者讀磁盤,這樣效率最高。

Redis線程安全嗎?

redis其實是採用了線程封閉的觀念,把任務封閉在一個線程,天然避免了線程安全問題,不過對於須要依賴多個redis操做的複合操做來講,依然須要鎖,並且有多是分佈式鎖。

2 redis部署方式

2.1 單節點模式

單節點模式只有一個節點,通常用來測試

2.2 主從模式

主從模式包括一個主節點和多個從節點,通常來講,主節點用來讀寫操做,從節點用戶讀操做,主節點的數據能夠同步到從節點,因此從節點即使支持寫操做也沒有意義。

2.3 哨兵模式

![網易架構師心得:Springboot下使用redis踩過的坑](https://p1-tt.byteimg.com/origin/pgc-image/418ef7116c464c428bfd6879c44ea748?from=pc)

哨兵模式是基於主從模式的,哨兵模式爲了實現主從模式的高可用,監控主從節點的狀態,當sentinel發現master節點掛了之後,sentinel就會從slave中從新選舉一個master。

通常來講,經過sentinel集羣能夠管理多個主從redis,sentinel最好不要和Redis部署在同一臺機器,否則redis的服務器掛了之後,sentinel也掛了。使用sentinel集羣也是爲了保證redis的高可用,避免哨兵節點掛了以後影響redis的使用。

當使用sentinel模式的時候,客戶端就不要直接鏈接Redis,而是鏈接sentinel的ip和port,由sentinel來提供具體的可提供服務的Redis實現,這樣當master節點掛掉之後,sentinel就會感知並將新的master節點提供給使用者。

sentinel模式基本能夠知足通常生產的需求,具有高可用性。可是當數據量過大到一臺服務器存放不下的狀況時,主從模式或sentinel模式就不能知足需求了,這個時候須要對存儲的數據進行分片,將數據存儲到多個Redis實例中。

2.4 集羣模式:

![網易架構師心得:Springboot下使用redis踩過的坑](https://p1-tt.byteimg.com/origin/pgc-image/3cecb10dab164cfa980cb7fb4c910b9f?from=pc)

cluster的出現是爲了解決單機Redis容量有限的問題,將Redis的數據根據必定的規則分配到多臺機器。

cluster能夠說是sentinel和主從模式的結合體,經過cluster能夠實現主從和master重選功能,因此若是配置兩個副本三個分片的話,就須要六個Redis實例。

如圖所示,部署了三主三從的redis集羣,redis cluster有固定的16384個hash slot,對每一個key計算CRC16值,而後對16384取模,能夠獲取key對應的hash slot,從而將數據存儲至對應的slot上。

3 Springboot使用redis總結

spring-boot-starter-data-redis支持兩種redis客戶端:jedis和lettuce

![網易架構師心得:Springboot下使用redis踩過的坑](https://p3-tt.byteimg.com/origin/pgc-image/90e381e478b74dc3aa7089e59cd8a234?from=pc)

Springboot2.0默認使用的客戶端是lettuce,下面跟蹤源碼來分析springboot如何加在lettuce客戶端的,首先找到springboot自動加載的jar包下redis相關的加載配置類RedisAutoConfiguration

![網易架構師心得:Springboot下使用redis踩過的坑](https://p6-tt.byteimg.com/origin/pgc-image/e12b2305e3dd44748a044cbf2816ef97?from=pc)

這裏採用@Configuration @bean的方式向容器中注入RedisTemplate和StringRedisTemplate,注入二者的方法中須要傳入RedisConnectionFactory,RedisConnectionFactory經過@Import導入的LettuceConnectionConfiguration和JedisConnectionConfiguration生成

![網易架構師心得:Springboot下使用redis踩過的坑](https://p6-tt.byteimg.com/origin/pgc-image/8a26a755349e47efb60941c82d5201c5?from=pc)
![網易架構師心得:Springboot下使用redis踩過的坑](https://p3-tt.byteimg.com/origin/pgc-image/380d0aa2f11e4e9c85a05bc1acfee4c7?from=pc)

能夠看到在沒有RedisConnectionFactory的狀況下,會默認向Spring容器中注入LettuceConnectionFactory,若是要使用jedis客戶端,只須要手動配置一個JedisConnectionFactory並注入容器便可。

3.1 jedis和lettuce的區別

  • Jedis在實現上是直接鏈接的redis server,若是在多線程環境下是非線程安全的,這個時候只有使用鏈接池,爲每一個Jedis實例增長物理鏈接。
  • Lettuce的鏈接是基於Netty的,鏈接實例(StatefulRedisConnection)能夠在多個線程間併發訪問, StatefulRedisConnection是線程安全的,因此一個鏈接實例(StatefulRedisConnection)就能夠知足多線程環境下的併發訪問,固然這個也是可伸縮的設計,一個鏈接實例不夠的狀況也能夠按需增長鏈接實例。

3.2 jedis非線程安全分析:

從源碼角度分析jedis客戶端執行每一個命令的過程

![網易架構師心得:Springboot下使用redis踩過的坑](https://p1-tt.byteimg.com/origin/pgc-image/4213190a7240423389c52e3334d3aff9?from=pc)

首先借助於Client類的對應方法去執行命令

![網易架構師心得:Springboot下使用redis踩過的坑](https://p6-tt.byteimg.com/origin/pgc-image/d710c432546f4782bcd66f1b46a8b814?from=pc)

而後藉助於Connection類的sendCommand方法執行

![網易架構師心得:Springboot下使用redis踩過的坑](https://p3-tt.byteimg.com/origin/pgc-image/9b383959d0444b55950c08306df92f9d?from=pc)

sendCommand方法每次執行都會調用connect方法

![網易架構師心得:Springboot下使用redis踩過的坑](https://p3-tt.byteimg.com/origin/pgc-image/6d1eda17b8a64557bba50ccd8f29bdc9?from=pc)

從connect方法中能夠看到,socket是一個共享變量,假如兩個線程公用一個jedis實例,當前尚未創建socket鏈接,兩個線程同時進入創建socket鏈接

![網易架構師心得:Springboot下使用redis踩過的坑](https://p6-tt.byteimg.com/origin/pgc-image/ca386a73ca5b48ad88a94c8849b19ce8?from=pc)

線程1創建socket鏈接後,開始獲取輸入輸出流,於此同時,線程2從新初始化socket,而且沒有執行到創建socket鏈接,此時線程1獲取輸入輸出流將失敗,由於此時的socket並無鏈接。

jedis自己不是多線程安全的,這並非jedis的bug,而是jedis的設計與redis自己就是單線程相關,jedis實例抽象的是發送命令相關,一個jedis實例使用一個線程與使用100個線程去發送命令沒有本質上的區別,因此不必設置爲線程安全的。可是若是須要用多線程方式訪問redis服務器怎麼作呢?那就使用多個jedis實例,每一個線程對應一個jedis實例,而不是一個jedis實例多個線程共享。一個jedis關聯一個Client,至關於一個客戶端,Client繼承了Connection,Connection維護了Socket鏈接,對於Socket這種昂貴的鏈接,通常都會作池化,jedis提供了JedisPool。

3.3 集羣模式下jedis和lettuce使用的一些坑

1. Lettuce在集羣模式下主節點宕機,從節點更新爲主節點,lettuce如何更新集羣拓撲結構

集羣中每一個節點只負責部分slot, slot可能從一個節點遷移到另外一節點,形成客戶端有可能會向錯誤的節點發起請求。所以須要有一種機制來對其進行發現和修正,這就是請求重定向。

集羣拓撲刷新是在ClusterTopologyRefreshScheduler中進行,下面進入類中一探究竟

![網易架構師心得:Springboot下使用redis踩過的坑](https://p3-tt.byteimg.com/origin/pgc-image/8e13b9f8135f4c899e16200d86471019?from=pc)
![網易架構師心得:Springboot下使用redis踩過的坑](https://p3-tt.byteimg.com/origin/pgc-image/ed42c38f1b9b427d91181574d7204e60?from=pc)

ClusterTopologyRefreshScheduler類實現了ClusterEventListener接口,用來監聽redis集羣事件,集羣事件包括ask重定向,move重定向,以及從新鏈接等。

![網易架構師心得:Springboot下使用redis踩過的坑](https://p1-tt.byteimg.com/origin/pgc-image/ff0d027024024f12bcdb734893aa03cd?from=pc)

在重定向方法中首先調用isEnabled方法判斷是否開啓刷新集羣拓撲,而後調用indicateTopologyRefreshSignal方法刷新集羣拓撲

![網易架構師心得:Springboot下使用redis踩過的坑](https://p1-tt.byteimg.com/origin/pgc-image/ae9671ebe1cc4ffa9ef0927de1d5b3f1?from=pc)

判斷集羣是否開啓刷新拓撲結構,依據ClusterTopologyRefreshOptions中自適應刷新的trigger中是否包含指定的重定向trigger,在默認配置下,這個trigger是什麼樣的呢?

![網易架構師心得:Springboot下使用redis踩過的坑](https://p3-tt.byteimg.com/origin/pgc-image/ba4aee2f4bee44ecb4d91f13adc10ac9?from=pc)

能夠看到默認狀況下自適應刷新的trigger是空的,因此在集羣模式下,使用默認的lettuce配置,若是主節點宕機,是不會刷新集羣拓撲的,也就是會致使redis鏈接失敗。

![網易架構師心得:Springboot下使用redis踩過的坑](https://p1-tt.byteimg.com/origin/pgc-image/753a9441bb7340158a7a5fd212ecf544?from=pc)

在enableAllAdaptiveRefreshTriggers方法中能夠開啓自適應刷新集羣拓撲。開啓自適應刷新集羣拓撲後,又是如何刷新的呢?

![網易架構師心得:Springboot下使用redis踩過的坑](https://p6-tt.byteimg.com/origin/pgc-image/3a55d9c2955f46ce8887977692190ae9?from=pc)

在indicateTopologyRefreshSignal方法中提交一個刷新集羣拓撲的clusterTopologyRefreshTask

![網易架構師心得:Springboot下使用redis踩過的坑](https://p3-tt.byteimg.com/origin/pgc-image/bffc5c6d11a145b291163ed4973f57f7?from=pc)

在task中調用RedisClusterClient類的reloadPartitions方法從新加載集羣拓撲信息,達到刷新的效果。

除了經過開始自適應刷新集羣拓撲以外,還能夠經過開啓週期刷新的方式刷新集羣拓撲

![網易架構師心得:Springboot下使用redis踩過的坑](https://p1-tt.byteimg.com/origin/pgc-image/cf1450853b1a42a086c8d19e73d06094?from=pc)

開啓週期刷新集羣拓撲後,在初始化集羣拓撲的時,會調用activateTopologyRefreshIfNeeded開啓週期刷新集羣拓撲任務

![網易架構師心得:Springboot下使用redis踩過的坑](https://p6-tt.byteimg.com/origin/pgc-image/629b7cbac700444bbc0cecc72f9943b8?from=pc)
![網易架構師心得:Springboot下使用redis踩過的坑](https://p6-tt.byteimg.com/origin/pgc-image/dc2fef6cfd814f63b2a790474fe1542d?from=pc)

這裏會判斷是否開啓週期刷新,開啓後纔會提交一個定時任務。

週期刷新和自適應刷新比較:週期刷新和自適應刷新兩種方法,最好仍是使用自適應刷新,由於週期刷新的週期須要設置,設置太長會致使服務可能一段時間不可用,設置過短對資源是一種浪費,而自適應刷新根據服務端的響應來刷新集羣拓撲。

兩種刷新方法不必都開啓,都開啓對資源也是一種浪費。

2.Jedis客戶端執行lua腳本的坑

redis使用lua腳本的好處:

  • 減小網絡開銷。能夠將多個請求經過腳本的形式一次發送,減小網絡時延。
  • 原子操做。redis會將整個腳本做爲一個總體執行,中間不會被其餘命令插入。所以在編寫腳本的過程當中無需擔憂會出現競態條件,無需使用事務。
  • 複用。客戶端發送的腳本會永久存在redis中,這樣,其餘客戶端能夠複用這一腳本而不須要使用代碼完成相同的邏輯。

那Jedis客戶端是如何支持lua腳本的呢?

![網易架構師心得:Springboot下使用redis踩過的坑](https://p1-tt.byteimg.com/origin/pgc-image/c329997eb76341c39e1632656259f93b?from=pc)

Jedis執行lua腳本是經過ScriptExecutor類的execute方法執行的,在方法中進一步調用eval方法

![網易架構師心得:Springboot下使用redis踩過的坑](https://p6-tt.byteimg.com/origin/pgc-image/720ac6755ee244ae86bb1512833c862e?from=pc)

進一步調用RedisScriptingCommands類的eval方法,由於實在集羣模式下使用jedis客戶端,因此調用JedisClusterScriptingCommands實現類的eval方法

![網易架構師心得:Springboot下使用redis踩過的坑](https://p1-tt.byteimg.com/origin/pgc-image/515bc169ff354de9b6d9913c1e22f5b8?from=pc)

再看JedisClusterScriptingCommands實現類的eval方法,竟然直接拋出異常,集羣模式下不支持腳本。

解決方法是使用lettuce客戶端,LettuceScriptingCommands類中的eval方法支持腳本

![網易架構師心得:Springboot下使用redis踩過的坑](https://p6-tt.byteimg.com/origin/pgc-image/e085c7ea5ce54ca38ed7e782da5faa0a?from=pc)

看到這裏的小夥伴,若是你喜歡這篇文章的話,別忘了轉發、收藏、留言互動

若是對文章有任何問題,歡迎在留言區和我交流~

最近我新整理了一些Java資料,包含面經分享、模擬試題、和視頻乾貨,若是你須要的話,歡迎私信我

還有,關注我!關注我!關注我!

相關文章
相關標籤/搜索