分享一下個人網易架構師同事在spring boot下使用redis的心得~redis
首先總結了redis服務端單線程工做模型,redis四種部署方式及使用場景,而後從源碼的角度上,分析springboot在jedis和lettuce客戶端下使用redis的一些坑~尤爲是在集羣模式下的一些不兼容問題!spring
最近整理的Java架構學習視頻和大廠項目底層知識點,須要的同窗歡迎私信我發給你~一塊兒學習進步!安全
redis 內部使用文件事件處理(file event handler)處理客戶端的請求,文件事件處理器是單線程的,因此redis才叫作單線程的模型。springboot
文件事件處理器的結構包含4個部分:多個socket、IO 多路複用程序、文件事件分派器、事件處理器(鏈接應答處理器、命令請求處理器、命令回覆處理器)。服務器
文件事件處理器採用 IO 多路複用機制同時監聽多個 socket,根據 socket 上的事件來選擇對應的事件處理器進行處理。markdown
Redis客戶端經過socket鏈接reids服務端,多個 socket 可能會併發產生不一樣的操做,每一個操做對應不一樣的文件事件,可是 IO 多路複用程序會監聽多個 socket,將 socket 產生的事件放入隊列中排隊,事件分派器每次從隊列中取出一個事件,把該事件交給對應的事件處理器進行處理。網絡
redis 單線程模型也能效率高的緣由:多線程
爲何redis採用單線程模型呢?架構
若是採用多線程模型,cpu須要進行上下文切換,假設1MB的數據由多個線程讀取了1000次,那麼就有1000次時間上下文的切換,那麼就有1500ns * 1000 = 1500us,而單線程的讀完1MB數據才250us ,因此徹底不必使用多線程。併發
何時適合採用多線程的方案呢?
對於慢速設備:磁盤,網絡,SSD 等等,將請求和處理的線程不綁定,請求的線程將請求放在一個buff裏,而後等buff快滿了,處理的線程再去處理這個buff。而後由這個buff 統一的去寫入磁盤,或者讀磁盤,這樣效率最高。
Redis線程安全嗎?
redis其實是採用了線程封閉的觀念,把任務封閉在一個線程,天然避免了線程安全問題,不過對於須要依賴多個redis操做的複合操做來講,依然須要鎖,並且有多是分佈式鎖。
單節點模式只有一個節點,通常用來測試
主從模式包括一個主節點和多個從節點,通常來講,主節點用來讀寫操做,從節點用戶讀操做,主節點的數據能夠同步到從節點,因此從節點即使支持寫操做也沒有意義。
哨兵模式是基於主從模式的,哨兵模式爲了實現主從模式的高可用,監控主從節點的狀態,當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實例中。
cluster的出現是爲了解決單機Redis容量有限的問題,將Redis的數據根據必定的規則分配到多臺機器。
cluster能夠說是sentinel和主從模式的結合體,經過cluster能夠實現主從和master重選功能,因此若是配置兩個副本三個分片的話,就須要六個Redis實例。
如圖所示,部署了三主三從的redis集羣,redis cluster有固定的16384個hash slot,對每一個key計算CRC16值,而後對16384取模,能夠獲取key對應的hash slot,從而將數據存儲至對應的slot上。
spring-boot-starter-data-redis支持兩種redis客戶端:jedis和lettuce
Springboot2.0默認使用的客戶端是lettuce,下面跟蹤源碼來分析springboot如何加在lettuce客戶端的,首先找到springboot自動加載的jar包下redis相關的加載配置類RedisAutoConfiguration
這裏採用@Configuration @bean的方式向容器中注入RedisTemplate和StringRedisTemplate,注入二者的方法中須要傳入RedisConnectionFactory,RedisConnectionFactory經過@Import導入的LettuceConnectionConfiguration和JedisConnectionConfiguration生成
能夠看到在沒有RedisConnectionFactory的狀況下,會默認向Spring容器中注入LettuceConnectionFactory,若是要使用jedis客戶端,只須要手動配置一個JedisConnectionFactory並注入容器便可。
從源碼角度分析jedis客戶端執行每一個命令的過程
首先借助於Client類的對應方法去執行命令
而後藉助於Connection類的sendCommand方法執行
sendCommand方法每次執行都會調用connect方法
從connect方法中能夠看到,socket是一個共享變量,假如兩個線程公用一個jedis實例,當前尚未創建socket鏈接,兩個線程同時進入創建socket鏈接
線程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。
集羣中每一個節點只負責部分slot, slot可能從一個節點遷移到另外一節點,形成客戶端有可能會向錯誤的節點發起請求。所以須要有一種機制來對其進行發現和修正,這就是請求重定向。
集羣拓撲刷新是在ClusterTopologyRefreshScheduler中進行,下面進入類中一探究竟
ClusterTopologyRefreshScheduler類實現了ClusterEventListener接口,用來監聽redis集羣事件,集羣事件包括ask重定向,move重定向,以及從新鏈接等。
在重定向方法中首先調用isEnabled方法判斷是否開啓刷新集羣拓撲,而後調用indicateTopologyRefreshSignal方法刷新集羣拓撲
判斷集羣是否開啓刷新拓撲結構,依據ClusterTopologyRefreshOptions中自適應刷新的trigger中是否包含指定的重定向trigger,在默認配置下,這個trigger是什麼樣的呢?
能夠看到默認狀況下自適應刷新的trigger是空的,因此在集羣模式下,使用默認的lettuce配置,若是主節點宕機,是不會刷新集羣拓撲的,也就是會致使redis鏈接失敗。
在enableAllAdaptiveRefreshTriggers方法中能夠開啓自適應刷新集羣拓撲。開啓自適應刷新集羣拓撲後,又是如何刷新的呢?
在indicateTopologyRefreshSignal方法中提交一個刷新集羣拓撲的clusterTopologyRefreshTask
在task中調用RedisClusterClient類的reloadPartitions方法從新加載集羣拓撲信息,達到刷新的效果。
除了經過開始自適應刷新集羣拓撲以外,還能夠經過開啓週期刷新的方式刷新集羣拓撲
開啓週期刷新集羣拓撲後,在初始化集羣拓撲的時,會調用activateTopologyRefreshIfNeeded開啓週期刷新集羣拓撲任務
這裏會判斷是否開啓週期刷新,開啓後纔會提交一個定時任務。
週期刷新和自適應刷新比較:週期刷新和自適應刷新兩種方法,最好仍是使用自適應刷新,由於週期刷新的週期須要設置,設置太長會致使服務可能一段時間不可用,設置過短對資源是一種浪費,而自適應刷新根據服務端的響應來刷新集羣拓撲。
兩種刷新方法不必都開啓,都開啓對資源也是一種浪費。
redis使用lua腳本的好處:
那Jedis客戶端是如何支持lua腳本的呢?
Jedis執行lua腳本是經過ScriptExecutor類的execute方法執行的,在方法中進一步調用eval方法
進一步調用RedisScriptingCommands類的eval方法,由於實在集羣模式下使用jedis客戶端,因此調用JedisClusterScriptingCommands實現類的eval方法
再看JedisClusterScriptingCommands實現類的eval方法,竟然直接拋出異常,集羣模式下不支持腳本。
解決方法是使用lettuce客戶端,LettuceScriptingCommands類中的eval方法支持腳本
看到這裏的小夥伴,若是你喜歡這篇文章的話,別忘了轉發、收藏、留言互動!
若是對文章有任何問題,歡迎在留言區和我交流~
最近我新整理了一些Java資料,包含面經分享、模擬試題、和視頻乾貨,若是你須要的話,歡迎私信我!