區別:Redis Sentinel 與 Redis Cluster

一、前言

互聯網高速發展的今天,對應用系統的抗壓能力要求越來越高,傳統的應用層+數據庫已經不能滿足當前的需要。所以一大批內存式數據庫和Nosql數據庫應運而生,其中redis,memcache,mongodb,hbase等被廣泛的使用來提高系統的吞吐性,所以如何正確使用cache是作爲開發的一項基技能。本文主要介紹Redis Sentinel Redis Cluster的區別及用法,Redis的基本操作可以自行去參看其官方文檔  其他幾種cache有興趣的可自行找資料去學習。

二、Redis Sentinel Redis Cluster 簡介

1Redis Sentinel

 Redis-Sentinel(哨兵模式)Redis官方推薦的高可用性(HA)解決方案,當用RedisMaster-slave的高可用方案時,假如master宕機了,Redis本身(包括它的很多客戶端)都沒有實現自動進行主備切換,而Redis-sentinel本身也是一個獨立運行的進程,它能監控多個master-slave集羣,發現master宕機後能進行自懂切換。它的主要功能有以下幾點:

  • 不時地監控redis是否按照預期良好地運行;
  • 如果發現某個redis節點運行出現狀況,能夠通知另外一個進程(例如它的客戶端);
  • 能夠進行自動切換。當一個master節點不可用時,能夠選舉出master的多個slave(如果有超過一個slave的話)中的一個來作爲新的master,其它的slave節點會將它所追隨的master的地址改爲被提升爲masterslave的新地址。

       Redis master-slave 模式如下圖:

 

從上圖片中可以看到,一個master 節點可以掛多個slave  Redis Sentinel 管理Redis 節點結構如下:

     

從上圖中可以得出Sentinel其實就是ClientRedis之間的橋樑,所有的客戶端都通過Sentinel程序獲取RedisMaster服務。首先Sentinel是集羣部署的,Client可以鏈接任何一個Sentinel服務所獲的結果都是一致的。其次,所有的Sentinel服務都會對Redis的主從服務進行監控,當監控到Master服務無響應的時候,Sentinel內部進行仲裁,從所有的 Slave選舉出一個做爲新的Master。並且把其他的slave作爲新的MasterSlave。最後通知所有的客戶端新的Master服務地址。如果舊的Master服務地址重新啓動,這個時候,它將被設置爲Slave服務。

Sentinel 可以管理master-slave節點,看似Redis的穩定性得到一個比較好的保障。但是如果Sentinel是單節點的話,如果Sentinel宕機了,那master-slave這種模式就不能發揮其作用了。幸好Sentinel也支持集羣模式,Sentinel的集羣模式主要有以下幾個好處:

  • 即使有一些sentinel進程宕掉了,依然可以進行redis集羣的主備切換;
  • 如果只有一個sentinel進程,如果這個進程運行出錯,或者是網絡堵塞,那麼將無法實現redis集羣的主備切換(單點問題);
  • 如果有多個sentinelredis的客戶端可以隨意地連接任意一個sentinel來獲得關於redis集羣中的信息。

     Redis Sentinel 集羣模式可以增強整個Redis集羣的穩定性與可靠性,但是當某個節點的master節點掛了要重新選取出新的master節點時,Redis Sentinel的集羣模式選取的複雜度顯然高於單點的Redis Sentinel 模式,此時需要一個比較靠譜的選取算法。下面就來介紹Redis Sentinel 集羣模式的仲裁會”(多個Redis Sentinel共同商量誰是Redis master節點)

1.1Redis Sentinel 集羣模式的仲裁會

 當一個mastersentinel集羣監控時,需要爲它指定一個參數,這個參數指定了當需要判決master爲不可用,並且進行failover時,所需要的sentinel數量,本文中我們暫時稱這個參數爲票數,不過,當failover主備切換真正被觸發後,failover並不會馬上進行,還需要sentinel中的大多數sentinel授權後纔可以進行failover。當ODOWN時,failover被觸發。failover一旦被觸發,嘗試去進行failoversentinel會去獲得大多數”sentinel的授權(如果票數比大多數還要大的時候,則詢問更多的sentinel)這個區別看起來很微妙,但是很容易理解和使用。例如,集羣中有5sentinel,票數被設置爲2,當2sentinel認爲一個master已經不可用了以後,將會觸發failover,但是,進行failover的那個sentinel必須先獲得至少3sentinel的授權纔可以實行failover。如果票數被設置爲5,要達到ODOWN狀態,必須所有5sentinel都主觀認爲master爲不可用,要進行failover,那麼得獲得所有5sentinel的授權。

2Redis Cluster

使用Redis Sentinel 模式架構的緩存體系,在使用的過程中,隨着業務的增加不可避免的要對Redis進行擴容,熟知的擴容方式有兩種,一種是垂直擴容,一種是水平擴容。垂直擴容表示通過加內存方式來增加整個緩存體系的容量比如將緩存大小由2G調整到4G,這種擴容不需要應用程序支持;水平擴容表示表示通過增加節點的方式來增加整個緩存體系的容量比如本來有1個節點變成2個節點,這種擴容方式需要應用程序支持。垂直擴容看似最便捷的擴容,但是受到機器的限制,一個機器的內存是有限的,所以垂直擴容到一定階段不可避免的要進行水平擴容,如果預留出很多節點感覺又是對資源的一種浪費因爲對業務的發展趨勢很快預測。Redis Sentinel 水平擴容一直都是程序猿心中的痛點,因爲水平擴容牽涉到數據的遷移。遷移過程一方面要保證自己的業務是可用的,一方面要保證儘量不丟失數據所以數據能不遷移就儘量不遷移。針對這個問題,Redis Cluster就應運而生了,下面簡單介紹一下RedisCluster

Redis ClusterRedis的分佈式解決方案,在Redis 3.0版本正式推出的,有效解決了Redis分佈式方面的需求。當遇到單機內存、併發、流量等瓶頸時,可以採用Cluster架構達到負載均衡的目的。分佈式集羣首要解決把整個數據集按照分區規則映射到多個節點的問題,即把數據集劃分到多個節點上,每個節點負責整個數據的一個子集。Redis Cluster採用哈希分區規則中的虛擬槽分區。虛擬槽分區巧妙地使用了哈希空間,使用分散度良好的哈希函數把所有的數據映射到一個固定範圍內的整數集合,整數定義爲槽(slot)。Redis Cluster槽的範圍是0 16383。槽是集羣內數據管理和遷移的基本單位。採用大範圍的槽的主要目的是爲了方便數據的拆分和集羣的擴展,每個節點負責一定數量的槽。Redis Cluster採用虛擬槽分區,所有的鍵根據哈希函數映射到0 16383,計算公式:slot = CRC16(key)&16383。每一個實節點負責維護一部分槽以及槽所映射的鍵值數據。下圖展現一個五個節點構成的集羣,每個節點平均大約負責3276個槽,以及通過計算公式映射到對應節點的對應槽的過程。

Redis Cluster節點相互之前的關係如下圖所示:

三、Redis Sentinel Redis Cluster 實踐

Redis Sentinel Redis Cluster 使用需要引入如下jar

  1. <dependency>
  2.     <groupId>redis.clients</groupId>
  3.     <artifactId>jedis</artifactId>
  4.     <version>2.9.0</version>
  5. </dependency>
  6.  
  7. <dependency>
  8.     <groupId>org.apache.commons</groupId>
  9.     <artifactId>commons-pool2</artifactId>
  10.     <version>2.5.0</version>
  11. </dependency>

1Redis Sentinel 使用

  1. package com.knowledge.cache.redis;
  2.  
  3. import redis.clients.jedis.Jedis;
  4. import redis.clients.jedis.JedisSentinelPool;
  5. import redis.clients.jedis.exceptions.JedisConnectionException;
  6.  
  7. import org.apache.commons.lang3.StringUtils;
  8. import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
  9. import java.util.Arrays;
  10. import java.util.HashSet;
  11. import java.util.Set;
  12.  
  13. /**
  14. * @author [email protected]
  15. * @desc redis sentinel 使用
  16. */
  17. public class RedisSentinelClient {
  18.     private static JedisSentinelPool pool = null;
  19.     private static String redisHosts = "127.0.0.1:26378;127.0.0.1:26379;127.0.0.1:26380";
  20.     private static String redisMaster = "";//master name
  21.     private static String password = "";//密碼,可選
  22.     private static final int MAX_IDLE = 200;//最大空閒數
  23.     private static final int MAX_TOTAL = 400;//最大連接數
  24.     private static final int MIN_IDLE = 200;//最小空閒數
  25.  
  26.     static {
  27.         //redis 連接池配置
  28.         GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
  29.         poolConfig.setMaxIdle(MAX_IDLE);
  30.         poolConfig.setMaxTotal(MAX_TOTAL);
  31.         poolConfig.setMinIdle(MIN_IDLE);
  32.         poolConfig.setTestOnBorrow(true);
  33.         poolConfig.setTestOnReturn(true);
  34.         Set<String> hosts = new HashSet<String>(Arrays.asList(redisHosts.split(";")));
  35.         if (StringUtils.isBlank(password)) {
  36.            pool = new JedisSentinelPool(redisMaster, hosts, poolConfig);
  37.         } else {
  38.             pool = new JedisSentinelPool(redisMaster, hosts, poolConfig, password);
  39.         }
  40.     }
  41.  
  42.     public String get(String key) throws JedisConnectionException {
  43.         Jedis jedis = pool.getResource();
  44.         try {
  45.             return jedis.get(key);
  46.         } catch (JedisConnectionException e) {
  47.             throw e;
  48.         } finally {
  49.             jedis.close();
  50.         }
  51.     }
  52. }

2Redis Cluster 使用

  1. package com.knowledge.cache.redis;
  2.  
  3. import redis.clients.jedis.HostAndPort;
  4. import redis.clients.jedis.JedisCluster;
  5. import redis.clients.jedis.exceptions.JedisConnectionException;
  6.  
  7. import org.apache.commons.lang3.StringUtils;
  8. import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
  9. import java.util.Arrays;
  10. import java.util.HashSet;
  11. import java.util.LinkedHashSet;
  12. import java.util.Set;
  13.  
  14. /**
  15. * @author [email protected]
  16. * @desc Redis cluster 使用
  17. */
  18. public class RedisClusterClient {
  19.     private static JedisCluster jedisCluster = null;
  20.     private static String redisHosts = "127.0.0.1:6378;127.0.0.1:6379;127.0.0.1:6380"; //:127.0.0.1:26379;127.0.0.1:26378
  21.     private static String password = "";//密碼,可選
  22.     private static final int CONNECT_TIMEOUT = 1000;//連接超時時間
  23.     private static final int SO_TIMEOUT = 1000;//響應超時時間
  24.     private static final int MAX_ATTEMPTS = 5;//最大嘗試次數
  25.     private static final int MAX_IDLE = 200;//最大空閒數
  26.     private static final int MAX_TOTAL = 400;//最大連接數
  27.     private static final int MIN_IDLE = 200;//最小空閒數
  28.  
  29.     static {
  30.         //連接池配置
  31.         GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
  32.         poolConfig.setMaxIdle(MAX_IDLE);
  33.         poolConfig.setMaxTotal(MAX_TOTAL);
  34.        poolConfig.setMinIdle(MIN_IDLE);
  35.         poolConfig.setTestOnBorrow(true);
  36.         poolConfig.setTestOnReturn(true);
  37.         //Redis Cluster 初始化
  38.         Set<String> hosts = new HashSet<String>(Arrays.asList(redisHosts.split(";")));
  39.         Set<HostAndPort> nodes = new LinkedHashSet<HostAndPort>();
  40.         for (String host : hosts) {
  41.             HostAndPort hostAndPort = new HostAndPort(host.split(":")[0], Integer.parseInt(host.split(":")[1]));
  42.             nodes.add(hostAndPort);
  43.         }
  44.  
  45.         if (StringUtils.isBlank(password)) {
  46.             jedisCluster = new JedisCluster(nodes, CONNECT_TIMEOUT, SO_TIMEOUT, MAX_ATTEMPTS, poolConfig);
  47.         } else {
  48.             jedisCluster = new JedisCluster(nodes, CONNECT_TIMEOUT, SO_TIMEOUT, MAX_ATTEMPTS, password, poolConfig);
  49.         }
  50.     }
  51.  
  52.     /**
  53.      * @param key
  54.      * @return
  55.      * @throws JedisConnectionException
  56.      */
  57.     public String get(String key) throws JedisConnectionException {
  58.         try {
  59.             return jedisCluster.get(key);
  60.         } catch (JedisConnectionException e) {
  61.             throw e;
  62.         }
  63.     }
  64.  
  65.     /**
  66.      * @param key
  67.      * @param value
  68.      * @return
  69.      * @throws JedisConnectionException
  70.      */
  71.     public String set(String key, String value) throws JedisConnectionException {
  72.  
  73.         try {
  74.             return jedisCluster.set(key, value);
  75.         } catch (JedisConnectionException e) {
  76.             throw e;
  77.         }
  78.     }
  79. }
  80.  

以上介紹了Redis Sentinel Redis Cluster的初始化過程及簡單的使用,其他比較複雜的應用可以參考Redis 的官方API

四、Redis的過期淘汰策略

1、定時刪除

  • 含義:在設置key的過期時間的同時,爲該key創建一個定時器,讓定時器在key的過期時間來臨時,對key進行刪除
  • 優點:保證內存被儘快釋放
  • 缺點:1)若過期key很多,刪除這些key會佔用很多的CPU時間,在CPU時間緊張的情況下,CPU不能把所有的時間用來做要緊的事兒,還需要去花時間刪除這些key;2)定時器的創建耗時,若爲每一個設置過期時間的key創建一個定時器(將會有大量的定時器產生),性能影響嚴重

2、懶漢式刪除

  • 含義:key過期的時候不刪除,每次通過key獲取值的時候去檢查是否過期,若過期,則刪除,返回null
  • 優點:刪除操作只發生在通過key取值的時候發生,而且只刪除當前key,所以對CPU時間的佔用是比較少的,而且此時的刪除是已經到了非做不可的地步(如果此時還不刪除的話,我們就會獲取到了已經過期的key
  • 缺點:若大量的key在超出超時時間後,很久一段時間內,都沒有被獲取過,那麼可能發生內存泄露(無用的垃圾佔用了大量的內存)

3、定期刪除

  • 含義:每隔一段時間執行一次刪除過期key操作
  • 優點:
    1)
    通過限制刪除操作的時長和頻率,來減少刪除操作對CPU時間的佔用--處理"定時刪除"的缺點;
    2)
    定期刪除過期key--處理"懶漢式刪除"的缺點
  • 缺點:
    1)
    在內存友好方面,不如"定時刪除"(會造成一定的內存佔用,但是沒有懶漢式那麼佔用內存);
    2)
    CPU時間友好方面,不如"懶漢式刪除"(會定期的去進行比較和刪除操作,cpu方面不如懶漢式,但是比定時好)
  • 難點:
    1)
    合理設置刪除操作的執行時長(每次刪除執行多長時間)和執行頻率(每隔多長時間做一次刪除)(這個要根據服務器運行情況來定了),每次執行時間太長,或者執行頻率太高對cpu都是一種壓力;
    2)
    每次進行定期刪除操作執行之後,需要記錄遍歷循環到了哪個標誌位,以便下一次定期時間來時,從上次位置開始進行循環遍歷

memcached只是用了惰性刪除,而redis同時使用了惰性刪除與定期刪除,這也是二者的一個不同點(可以看做是redis優於memcached的一點);

 

五、Redis 使用過程中踩過的坑

1、在生產環境中一定要配置GenericObjectPoolConfig中的 maxIdlemaxTotalminIdle.因爲裏面默認值太低了,如果生產環境中流量比較大的話,就會出現等待redis的連接的情況。

2、使用Redis Sentinel 一定要在最後執行jedis.close方法來釋放資源,這個close方法是表示將正常的連接放回去連接池中,將不正常的連接給關閉。之前jedis低版本中都是調用returnResource方法來釋放資源,如果連接不正常了會被重複使用,這時會出現很詭異的異常。所以建議使用比較高版本的jedis

3、爲了更好的使用redis 連接池,建議採用 JedisPoolConfig來替代GenericObjectPoolConfigJedisPoolConfig裏面有一些默認的參數

4maxIdlemaxTotal 最佳實踐爲 maxIdle = maxTotal

有遇到其他坑歡迎補充。。。。。。。

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章