Redis分佈式方案及一致性Hash算法精講

 watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=

 

爲何Redis須要分佈式

高性能

咱們知道Redis自己的QPS已經很高了,可是在一些併發量很是高的狀況下,性能仍是會受到影響的。這個時候咱們但願更多的Redis服務來分攤壓力,實現負載均衡。java

高可用

若是隻有一個Redis服務,一但服務發生了宕機,那麼全部的客戶端都沒法訪問,會對業務形成很大的影響。另外,若是硬件損壞了,那上面的全部數據也是沒法恢復的,咱們須要個備份。node

可擴展

第三點是出於存儲的考慮,由於redis全部的數據都放在內存中,若是數據量大,很容易收到硬件的限制。好比一臺Redis只能存4G的容量,可是有8G的數據要存,因此只能放兩臺機器,這個就是橫向擴展,水平分片。redis

Redis分佈式方案

主從複製

跟Kafka、RocketMQ、MySQL、ZooKeeper同樣,Redis支持集羣的架構,集羣的節點有主節點和從節點之分。主節點叫master,從節點叫slave。slave會經過複製的技術,自動同步master的數據。算法

Redis主從複製解決了數據備份和一部分性能的問題。可是沒有解決高可用的問題,在一主一從或者一主多從的狀況下,若是主服務器掛了,對外提供的服務就不可用了,須要手動把從服務器切換成主服務器,而後再把剩餘節點設置爲它的從節點,這個比較費時,還會形成必定時間的服務不可用。數據庫

Sentinel哨兵

從Redis2.8版本起,提供了一個穩定版本的Sentinel哨兵來解決高可用的問題,它的思路是啓動奇數個Sentinel的服務來監控Redis服務器來保證服務的可用性。服務器

啓動Sentinel可用用腳本啓動,它本質上只是一個運行在特殊模式之下的Redis。Sentinel經過info命令獲得被監聽Redis機器的master,slave等信息。數據結構

./redis-sentinel ../sentinel.conf
# 或者
./redis-server ../sentinel.conf --sentinel

爲了保證監控服務器的可用性,咱們會對Sentinel作集羣部署,Sentinel既監控全部的Redis服務,Sentinel之間也相互監控。 Sentinel自己沒有主從之分,地位是平等的,只有Redis服務節點有主從之分。架構

watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=

Sentinel經過Raft共識算法,實現Sentinel選舉,選舉出一個leader來,由leader完成故障轉移。Raft算法的應用很普遍,好比加密貨幣BTB,Spring Cloud註冊中心Consul也用到了Raft算法。Raft算法的核心思想是:先到先得,少數服從多數。Sentinel的Raft實現跟原生的算法是有所區別的,可是大致思想一致。 Raft算法演示:thesecretlivesofdata.com/raft/併發

不管Jedis仍是Spring Boot(2.x版本默認是Lettuce),都只須要配置所有的哨兵地址,由哨兵返回當前的master節點地址。負載均衡

哨兵的不足:主從切換的過程當中會丟失數據,由於只有一個master;只能單點寫,沒有解決水平擴容的問題。

Redis Cluster

Redis Cluster是在Redis 3.0的版本正式推出的,用來解決分佈式的需求,同時也能夠實現高可用,它是去中心化的,客戶端能夠鏈接到任意一個可用的節點。Redis Cluster能夠當作是由多個Redis實例組成的數據集合。客戶端不須要關注數據的子集到底存儲在哪一個節點,只須要關注這個集合總體。

下面就是一個三主三從 Redis Cluster架構: watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=

Redis建立了16384個槽(slot),每一個節點負責必定區間的slot。好比Node1負責0-5460,Node2負責5461-10922,Node3負責10923-16383。

watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk= 對象分佈到Redis節點的時候,首先是對Key用CRC16算法計算再%16384,獲得一個slot的值,數據落到負責這個slot的Redis節點上。 查看Key屬於哪一個slot:

redis>cluster keyslot jack

key與slot的關係是永遠不會變的,會變的只有slot和Redis節點的關係。

咱們知道key經過CRC16算法取模後會分佈在不一樣的節點,若是想讓不少個key同時落在同一個節點怎麼辦呢,只須要在key裏面加入{hash tag}便可。Redis在計算槽編號的時候只會獲取{}之間的字符串進行槽編號計算,以下所示:

user{666}base=...
user{666}fin=...

主從切換過程:

當slave發現本身的master變成FAIL狀態時,便嘗試進行Failover,以期成爲新的master。因爲掛掉的master可能會有多個slave,從而存在多個slave競爭成爲master節點的過程,其過程以下:

  1. slave發現本身的master變成FAIL
  2. 將本身記錄的集羣currentEpoch加1,並廣播FAILOVER_AUTH_REQUEST信息
  3. 其餘節點收到該信息,只有master響應,判斷請求者的合法性,併發送FAILOVER_AUTH_ACK,對每個epoch只發送一次ack
  4. 嘗試failover的slave收集FAILOVER_AUTH_ACK
  5. 超過半數後變成新的Master
  6. 廣播Pong消息通知其餘集羣節點

好比三個小的主從A,B,C組成的集羣,A的master掛了,A的兩個小弟發起選舉,結果B的master投給A的小弟A1,C的master投給了A的小弟A2,這樣就會發起第二次選舉,選舉輪次標記+1繼續上面的流程。事實上從節點並非在主節點一進入 FAIL 狀態就立刻嘗試發起選舉,而是有必定延遲,必定的延遲確保咱們等待FAIL狀態在集羣中傳播,slave若是當即嘗試選舉,其它masters或許還沒有意識到FAIL狀態,可能會拒絕投票。

Redis Cluster特色

  1. 無中心結構。
  2. 數據按照slot存儲分佈在多個節點,節點間數據共享,可動態調整數據分佈。
  3. 可擴展性,可線性擴展到1000個節點(官網推薦不超過1000個),節點可動態添加或刪除。
  4. 高可用性,部分節點不可用時,集羣仍可用。經過增長Slave作standby數據副本,可以實現故障自動failover,節點之間經過gossip協議交換狀態信息,用投票機制完成Slave到Master的角色提高。
  5. 下降運維成本,提升系統的擴展性和可用性。

至此,三種Redis的分佈式方案介紹完了,Redis Cluster既能實現主從的角色分配,又可以實現主從切換,至關於集成了Replication和Sentinel的功能。

Redis分片方案

一共有三種方案,第一種是在客戶端實現相關的邏輯,例如用取模或者一致性哈希對key進行分片,查詢和修改都先判斷key的路由。

第二種是把分片處理的邏輯抽取出來,運行一個獨立的代理服務,客戶端鏈接到這個代理服務,代理服務作請求轉發。

第三種是基於服務端實現的,就是上面介紹的Redis Cluster。

客戶端

客戶端咱們以Jedis爲例,Jedis有幾種鏈接池,其中有一種支持分片,就是ShardedJedisPool。如今咱們來作個實驗,有兩個Redis的節點,經過JedisShardInfo往裏面set 100個key。

public class ShardingTest {
    public static void main(String[] args) {
        JedisPoolConfig poolConfig = new JedisPoolConfig();

        // Redis服務器
        JedisShardInfo shardInfo1 = new JedisShardInfo("127.0.0.1", 6379);
        JedisShardInfo shardInfo2 = new JedisShardInfo("192.168.8.205", 6379);

        // 鏈接池
        List<JedisShardInfo> infoList = Arrays.asList(shardInfo1, shardInfo2);
        ShardedJedisPool jedisPool = new ShardedJedisPool(poolConfig, infoList);

        ShardedJedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            for (int i = 0; i < 100; i++) {
                jedis.set("k" + i, "" + i);
            }
            for (int i = 0; i < 100; i++) {
                Client client = jedis.getShard("k" + i).getClient();
                System.out.println("取到值:" + jedis.get("k" + i) + "," + "當前key位於:" + client.getHost() + ":" + client.getPort());
            }
        } finally {
            if (jedis != null) {
                jedis.close();
            }
        }
    }
}

源碼在:com/xhj/jedis/shard/ShardingTest.java

最後的結果經過dbsize命令發現,一臺服務器有44個key,一臺服務器有56個key。從結果能夠發現確實是作到了負載均衡,那具體是怎麼作到的呢?咱們猜測是經過哈希取模,hash(key)%N,根據餘數,決定映射到哪個節點。這種方式比較簡單,屬於靜態的分片規則,可是一但節點數量發生了變化(新增或者減小),因爲取模N發生了變化,數據須要從新分佈。爲了解決這個問題,咱們又有了一致性哈希算法,ShardedJedisPool實際上用的就是一致性哈希算法。

一致性哈希算法

接下來介紹一致性哈希算法,咱們把全部的哈希值空間組織成一個虛擬的圓環(哈希環),整個空間按順時針方向組織。由於是環形空間,0和2^32-1是重疊的。

假設咱們有四臺機器,咱們先根據機器的名稱或者IP計算哈希值,而後分佈到哈希環中(粉色圓圈),以下圖所示:

watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=

如今有四個請求要set或者get,咱們對key進行哈希計算,獲得哈希環中的位置(藍色圓圈),沿哈希環順時針找到的第一個Node,就是數據存儲的節點。

watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=

新增節點5,隻影響一部分數據

watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=

刪除節點1,隻影響一部分數據 watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=

一致性哈希算法解決了動態增減節點時,全部數據都須要從新分佈的問題,它只會影響到下一個相鄰的節點,對其餘節點沒有影響。可是這樣的一致性算法仍是有缺點,就是節點不必定是均勻的分佈的,特別是在節點數比較少的狀況下,這是節點1的壓力很大,解決這個問題還須要引入虛擬節點

watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=

Node1引入了兩個虛擬節點,Node2引入了兩個虛擬節點,這時候的數據分佈將是很均勻的。 watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=

一致性哈希算法在分佈式系統中,負載均衡、分庫分表都有所應用,跟LRU同樣,是一個很基礎的算法。那麼在Java代碼中咱們是如何實現的,哈希環是一個什麼數據結構?虛擬節點又怎麼實現?

咱們點開Jedis的源碼,在redis.clients.util.Sharded.initialize()方法中,Redis的節點被放到了一顆紅黑樹TreeMap中。

private void initialize(List<S> shards) {
  //建立一顆紅黑樹
  nodes = new TreeMap<Long, S>();
  //for循環Redis節點
  for (int i = 0; i != shards.size(); ++i) {
    final S shardInfo = shards.get(i);
    //爲每一個節點建立160個虛擬節點,放入紅黑樹中
    if (shardInfo.getName() == null) for (int n = 0; n < 160 * shardInfo.getWeight(); n++) {
      //按名字hash
      nodes.put(this.algo.hash("SHARD-" + i + "-NODE-" + n), shardInfo);
    }
    else for (int n = 0; n < 160 * shardInfo.getWeight(); n++) {
      //按名字hash
      nodes.put(this.algo.hash(shardInfo.getName() + "*" + shardInfo.getWeight() + n), shardInfo);
    }
    //把redis節點信息放到map中
    resources.put(shardInfo, shardInfo.createResource());
  }
}

當咱們有個key須要get或者set的時候,咱們須要知道具體落在哪一個節點。

public R getShard(String key) {
  //從resources裏面拿出具體的節點
  return resources.get(getShardInfo(key));
}

public S getShardInfo(byte[] key) {
  //這裏把key進行hash,而後從紅黑樹上摘下比該值大的第一個節點信息
  SortedMap<Long, S> tail = nodes.tailMap(algo.hash(key));
  if (tail.isEmpty()) {
    //沒有比它大的了,直接從node中取出
    return nodes.get(nodes.firstKey());
  }
  //不然返回第一個比它大的節點信息
  return tail.get(tail.firstKey());
}

這裏以Jedis的源碼介紹一致性哈希算法,在別的使用場景中代碼的寫法也是大同小異的,在數據結構的選取上:

  1. 最簡單的實現是採用一個有序的list,每次從第0個元素開始查找,直到找到第一個比數據的hash值大的節點,則該數據屬於該節點對應的服務器。時間複雜度爲O(n)。
  2. 採用二叉查找樹,時間複雜度爲O(log n)。

咱們不能簡單地使用二叉查找樹,由於可能出現不平衡的狀況。平衡二叉查找樹有AVL樹、紅黑樹等,這裏使用紅黑樹,選用紅黑樹的緣由有兩點:

  1. 紅黑樹主要的做用是用於存儲有序的數據,這其實和第一種解決方案的思路又不謀而合了,可是它的效率很是高。
  2. JDK裏面提供了紅黑樹的代碼實現TreeMap和TreeSet。

代理Proxy

使用ShardedJedisPool之類的客戶端分片代碼的優點是配置簡單,不依賴其餘中間件,分區的邏輯能夠本身定,比較靈活,缺點就是不能實現動態的服務增減,每一個客戶端須要自行維護分片策略,存在重複代碼。因此這時候咱們的思路就是把分片的代碼抽取出來,作成一個公共服務,全部的客戶端都鏈接到這個代理層,由代理層來進行轉發。

架構圖以下所示,跟數據庫分表分庫中間件的Mycat的工做層次是同樣的。典型的代理分區方案有Twitter開源的Twemproxy和國內的豌豆莢開源的Codis。 watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=

可是會有一些問題,出現故障不能自動轉移,架構複雜,須要藉助其餘組件Zookeeper(或者etcd/本地文件),如今已經不多使用了,能夠說是在Redis Cluster出現以前的一個過渡方案,因此這裏不詳細介紹了。

Redis Cluster

Redis Cluster上文已經介紹過了,天生的集成了數據分片功能,能夠將數據分配到不一樣的實例上。這是最完美的Redis分佈式方案。

由於key和slot的關係是永遠不會變的,當新增了節點的時候,須要把原有的slot分配給新的節點負責,而且把相關的數據遷移過來。

添加一個新節點192.168.10.219:6378

redis-cli --cluster add-node 192.168.10.219:6378 192.168.10.219:6379

新增的節點沒有哈希槽,不能分佈數據,在原來的任意一個節點上執行:

redis-cli --cluster reshard 192.168.10.219:6379

輸入須要分配的哈希槽的數量(好比500),和哈希槽的來源節點(能夠輸入all或者id)。

 

相關文章
相關標籤/搜索