### 前言
互聯網公司中,絕大部分都沒有馬爸爸系列的公司那樣財大氣粗,他們即沒有強勁的服務器、也沒有錢去購買昂貴的海量數據庫。那他們是怎么應對大數據量高并發的業務場景的呢?
這個和當前的開源技術、海量數據架構都有著不可分割的關系。比如通過mysql、nginx等開源軟件,通過架構和低成本的服務器搭建千萬級別的用戶訪問系統。
怎么樣搭建一個好的系統架構,這個話題我們能聊上個七天七夜。這里我主要結合Redis集群來講一下一致性Hash的相關問題。
### Redis集群的使用
我們在使用Redis的過程中,為了保證Redis的高可用,我們一般會對Redis做主從復制,組成`Master-Master`或者`Master-Slave`的形式,進行數據的讀寫分離,如下圖1-1所示:

圖1-1:Master-Slave模式
當緩存數據量超過一定的數量時,我們就要對Redis集群做分庫分表的操作。
來個栗子,我們有一個電商平臺,需要使用Redis存儲商品的圖片資源,存儲的格式為鍵值對,key值為圖片名稱,Value為該圖片所在的文件服務器的路徑,我們需要根據文件名,查找到文件所在的文件服務器上的路徑,我們的圖片數量大概在3000w左右,按照我們的規則進行分庫,規則就是隨機分配的,我們以每臺服務器存500w的數量,部署12臺緩存服務器,并且進行主從復制,架構圖如下圖1-2所示:

圖1-2:Redis分庫分表
由于我們定義的規則是隨機的,所以我們的數據有可能存儲在任何一組Redis中,比如我們需要查詢"product.png"的圖片,由于規則的隨機性,我們需要遍歷所有Redis服務器,才能查詢得到。這樣的結果顯然不是我們所需要的。所以我們會想到按某一個字段值進行Hash值、取模。所以我們就看看使用Hash的方式是怎么進行的。
### 使用Hash的Redis集群
如果我們使用Hash的方式,每一張圖片在進行分庫的時候都可以定位到特定的服務器,示意圖如圖1-3所示:

圖1-3:使用Hash方式的命中緩存
從上圖中,我們需要查詢的是圖`product.png`,由于我們有6臺主服務器,所以計算的公式為:`hash(product.png) % 6 = 5`, 我們就可以定位到是5號主從,這們就省去了遍歷所有服務器的時間,從而大大提升了性能。
### 使用Hash時遇到的問題
在上述hash取模的過程中,我們雖然不需要對所有Redis服務器進行遍歷而提升了性能。但是,使用Hash算法緩存時會出現一些問題,`Redis服務器變動時,所有緩存的位置都會發生改變`。
比如,現在我們的Redis緩存服務器增加到了8臺,我們計算的公式從`hash(product.png) % 6 = 5`變成了`hash(product.png) % 8 = ?` 結果肯定不是原來的5了。
再者,6臺的服務器集群中,當某個主從群出現故障時,無法進行緩存,那我們需要把故障機器移除,所以取模數又會從6變成了5。我們計算的公式也會變化。
由于上面hash算法是使用取模來進行緩存的,為了規避上述情況,Hash一致性算法就誕生了~~
### 一致性Hash算法原理
一致性Hash算法也是使用取模的方法,不過,上述的取模方法是對服務器的數量進行取模,而一致性的Hash算法是對`2的32方`取模。即,一致性Hash算法將整個Hash空間組織成一個虛擬的圓環,Hash函數的值空間為`0 ~ 2^32 - 1(一個32位無符號整型)`,整個哈希環如下:

圖1-4:Hash圓環
整個圓環以`順時針方向組織`,圓環正上方的點代表0,0點右側的第一個點代表1,以此類推。
第二步,我們將各個服務器使用Hash進行一個哈希,具體可以選擇服務器的IP或主機名作為關鍵字進行哈希,這樣每臺服務器就確定在了哈希環的一個位置上,比如我們有三臺機器,使用IP地址哈希后在環空間的位置如圖1-4所示:

圖1-4:服務器在哈希環上的位置
現在,我們使用以下算法定位數據訪問到相應的服務器:
> 將數據Key使用相同的函數Hash計算出哈希值,并確定此數據在環上的位置,從此位置沿環順時針查找,遇到的服務器就是其應該定位到的服務器。
例如,現在有ObjectA,ObjectB,ObjectC三個數據對象,經過哈希計算后,在環空間上的位置如下:

圖1-5:數據對象在環上的位置
根據一致性算法,Object -> NodeA,ObjectB -> NodeB, ObjectC -> NodeC

### 一致性Hash算法的容錯性和可擴展性
現在,假設我們的Node C宕機了,我們從圖中可以看到,A、B不會受到影響,只有Object C對象被重新定位到Node A。所以我們發現,在一致性Hash算法中,如果一臺服務器不可用,受影響的數據僅僅是此服務器到其環空間前一臺服務器之間的數據(這里為Node C到Node B之間的數據),其他不會受到影響。如圖1-6所示:

圖1-6:C節點宕機情況,數據移到節點A上
另外一種情況,現在我們系統增加了一臺服務器Node X,如圖1-7所示:

圖1-7:增加新的服務器節點X
此時對象ObjectA、ObjectB沒有受到影響,只有Object C重新定位到了新的節點X上。
如上所述:
> 一致性Hash算法對于節點的增減都只需重定位環空間中的一小部分數據,有很好的容錯性和可擴展性。
### 數據傾斜問題
在一致性Hash算法服務節點太少的情況下,容易因為節點分布不均勻面造成`數據傾斜(被緩存的對象大部分緩存在某一臺服務器上)問題`,如圖1-8特例:

圖1-8:數據傾斜
這時我們發現有大量數據集中在節點A上,而節點B只有少量數據。為了解決數據傾斜問題,一致性Hash算法引入了`虛擬節點機制`,即對每一個服務器節點計算多個哈希,每個計算結果位置都放置一個此服務節點,稱為虛擬節點。
具體操作可以為服務器IP或主機名后加入編號來實現,實現如圖1-9所示:

圖1-9:增加虛擬節點情況
數據定位算法不變,只需要增加一步:虛擬節點到實際點的映射。
所以加入虛擬節點之后,即使在服務節點很少的情況下,也能做到數據的均勻分布。
### 具體實現
###### 算法接口類
~~~csharp
public interface IHashService {
Long hash(String key);
}
~~~
###### 算法接口實現類
~~~java
public class HashService implements IHashService {
/**
* MurMurHash算法,性能高,碰撞率低
*
* @param key String
* @return Long
*/
public Long hash(String key) {
ByteBuffer buf = ByteBuffer.wrap(key.getBytes());
int seed = 0x1234ABCD;
ByteOrder byteOrder = buf.order();
buf.order(ByteOrder.LITTLE_ENDIAN);
long m = 0xc6a4a7935bd1e995L;
int r = 47;
long h = seed ^ (buf.remaining() * m);
long k;
while (buf.remaining() >= 8) {
k = buf.getLong();
k *= m;
k ^= k >>> r;
k *= m;
h ^= k;
h *= m;
}
if (buf.remaining() > 0) {
ByteBuffer finish = ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN);
finish.put(buf).rewind();
h ^= finish.getLong();
h *= m;
}
h ^= h >>> r;
h *= m;
h ^= h >>> r;
buf.order(byteOrder);
return h;
}
}
~~~
###### 模擬機器節點
~~~tsx
public class Node<T> {
private String ip;
private String name;
public Node(String ip, String name) {
this.ip = ip;
this.name = name;
}
public String getIp() {
return ip;
}
public void setIp(String ip) {
this.ip = ip;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
/**
* 使用IP當做hash的Key
*
* @return String
*/
@Override
public String toString() {
return ip;
}
}
~~~
###### 一致性Hash操作
~~~kotlin
public class ConsistentHash<T> {
// Hash函數接口
private final IHashService iHashService;
// 每個機器節點關聯的虛擬節點數量
private final int numberOfReplicas;
// 環形虛擬節點
private final SortedMap<Long, T> circle = new TreeMap<Long, T>();
public ConsistentHash(IHashService iHashService, int numberOfReplicas, Collection<T> nodes) {
this.iHashService = iHashService;
this.numberOfReplicas = numberOfReplicas;
for (T node : nodes) {
add(node);
}
}
/**
* 增加真實機器節點
*
* @param node T
*/
public void add(T node) {
for (int i = 0; i < this.numberOfReplicas; i++) {
circle.put(this.iHashService.hash(node.toString() + i), node);
}
}
/**
* 刪除真實機器節點
*
* @param node T
*/
public void remove(T node) {
for (int i = 0; i < this.numberOfReplicas; i++) {
circle.remove(this.iHashService.hash(node.toString() + i));
}
}
public T get(String key) {
if (circle.isEmpty()) return null;
long hash = iHashService.hash(key);
// 沿環的順時針找到一個虛擬節點
if (!circle.containsKey(hash)) {
SortedMap<Long, T> tailMap = circle.tailMap(hash);
hash = tailMap.isEmpty() ? circle.firstKey() : tailMap.firstKey();
}
return circle.get(hash);
}
}
~~~
###### 測試類
~~~dart
public class TestHashCircle {
// 機器節點IP前綴
private static final String IP_PREFIX = "192.168.0.";
public static void main(String[] args) {
// 每臺真實機器節點上保存的記錄條數
Map<String, Integer> map = new HashMap<String, Integer>();
// 真實機器節點, 模擬10臺
List<Node<String>> nodes = new ArrayList<Node<String>>();
for (int i = 1; i <= 10; i++) {
map.put(IP_PREFIX + i, 0); // 初始化記錄
Node<String> node = new Node<String>(IP_PREFIX + i, "node" + i);
nodes.add(node);
}
IHashService iHashService = new HashService();
// 每臺真實機器引入100個虛擬節點
ConsistentHash<Node<String>> consistentHash = new ConsistentHash<Node<String>>(iHashService, 500, nodes);
// 將5000條記錄盡可能均勻的存儲到10臺機器節點上
for (int i = 0; i < 5000; i++) {
// 產生隨機一個字符串當做一條記錄,可以是其它更復雜的業務對象,比如隨機字符串相當于對象的業務唯一標識
String data = UUID.randomUUID().toString() + i;
// 通過記錄找到真實機器節點
Node<String> node = consistentHash.get(data);
// 再這里可以能過其它工具將記錄存儲真實機器節點上,比如MemoryCache等
// ...
// 每臺真實機器節點上保存的記錄條數加1
map.put(node.getIp(), map.get(node.getIp()) + 1);
}
// 打印每臺真實機器節點保存的記錄條數
for (int i = 1; i <= 10; i++) {
System.out.println(IP_PREFIX + i + "節點記錄條數:" + map.get(IP_PREFIX + i));
}
}
}
~~~
運行結果如下:

一致性hash測試結果
每臺機器映射的虛擬節點越多,則分布的越均勻~~~
感興趣的同學可以拷貝上面的代碼運行嘗試一下。
作者:oneape15
轉載鏈接:https://www.jianshu.com/p/528ce5cd7e8f
- 一.JVM
- 1.1 java代碼是怎么運行的
- 1.2 JVM的內存區域
- 1.3 JVM運行時內存
- 1.4 JVM內存分配策略
- 1.5 JVM類加載機制與對象的生命周期
- 1.6 常用的垃圾回收算法
- 1.7 JVM垃圾收集器
- 1.8 CMS垃圾收集器
- 1.9 G1垃圾收集器
- 2.面試相關文章
- 2.1 可能是把Java內存區域講得最清楚的一篇文章
- 2.0 GC調優參數
- 2.1GC排查系列
- 2.2 內存泄漏和內存溢出
- 2.2.3 深入理解JVM-hotspot虛擬機對象探秘
- 1.10 并發的可達性分析相關問題
- 二.Java集合架構
- 1.ArrayList深入源碼分析
- 2.Vector深入源碼分析
- 3.LinkedList深入源碼分析
- 4.HashMap深入源碼分析
- 5.ConcurrentHashMap深入源碼分析
- 6.HashSet,LinkedHashSet 和 LinkedHashMap
- 7.容器中的設計模式
- 8.集合架構之面試指南
- 9.TreeSet和TreeMap
- 三.Java基礎
- 1.基礎概念
- 1.1 Java程序初始化的順序是怎么樣的
- 1.2 Java和C++的區別
- 1.3 反射
- 1.4 注解
- 1.5 泛型
- 1.6 字節與字符的區別以及訪問修飾符
- 1.7 深拷貝與淺拷貝
- 1.8 字符串常量池
- 2.面向對象
- 3.關鍵字
- 4.基本數據類型與運算
- 5.字符串與數組
- 6.異常處理
- 7.Object 通用方法
- 8.Java8
- 8.1 Java 8 Tutorial
- 8.2 Java 8 數據流(Stream)
- 8.3 Java 8 并發教程:線程和執行器
- 8.4 Java 8 并發教程:同步和鎖
- 8.5 Java 8 并發教程:原子變量和 ConcurrentMap
- 8.6 Java 8 API 示例:字符串、數值、算術和文件
- 8.7 在 Java 8 中避免 Null 檢查
- 8.8 使用 Intellij IDEA 解決 Java 8 的數據流問題
- 四.Java 并發編程
- 1.線程的實現/創建
- 2.線程生命周期/狀態轉換
- 3.線程池
- 4.線程中的協作、中斷
- 5.Java鎖
- 5.1 樂觀鎖、悲觀鎖和自旋鎖
- 5.2 Synchronized
- 5.3 ReentrantLock
- 5.4 公平鎖和非公平鎖
- 5.3.1 說說ReentrantLock的實現原理,以及ReentrantLock的核心源碼是如何實現的?
- 5.5 鎖優化和升級
- 6.多線程的上下文切換
- 7.死鎖的產生和解決
- 8.J.U.C(java.util.concurrent)
- 0.簡化版(快速復習用)
- 9.鎖優化
- 10.Java 內存模型(JMM)
- 11.ThreadLocal詳解
- 12 CAS
- 13.AQS
- 0.ArrayBlockingQueue和LinkedBlockingQueue的實現原理
- 1.DelayQueue的實現原理
- 14.Thread.join()實現原理
- 15.PriorityQueue 的特性和原理
- 16.CyclicBarrier的實際使用場景
- 五.Java I/O NIO
- 1.I/O模型簡述
- 2.Java NIO之緩沖區
- 3.JAVA NIO之文件通道
- 4.Java NIO之套接字通道
- 5.Java NIO之選擇器
- 6.基于 Java NIO 實現簡單的 HTTP 服務器
- 7.BIO-NIO-AIO
- 8.netty(一)
- 9.NIO面試題
- 六.Java設計模式
- 1.單例模式
- 2.策略模式
- 3.模板方法
- 4.適配器模式
- 5.簡單工廠
- 6.門面模式
- 7.代理模式
- 七.數據結構和算法
- 1.什么是紅黑樹
- 2.二叉樹
- 2.1 二叉樹的前序、中序、后序遍歷
- 3.排序算法匯總
- 4.java實現鏈表及鏈表的重用操作
- 4.1算法題-鏈表反轉
- 5.圖的概述
- 6.常見的幾道字符串算法題
- 7.幾道常見的鏈表算法題
- 8.leetcode常見算法題1
- 9.LRU緩存策略
- 10.二進制及位運算
- 10.1.二進制和十進制轉換
- 10.2.位運算
- 11.常見鏈表算法題
- 12.算法好文推薦
- 13.跳表
- 八.Spring 全家桶
- 1.Spring IOC
- 2.Spring AOP
- 3.Spring 事務管理
- 4.SpringMVC 運行流程和手動實現
- 0.Spring 核心技術
- 5.spring如何解決循環依賴問題
- 6.springboot自動裝配原理
- 7.Spring中的循環依賴解決機制中,為什么要三級緩存,用二級緩存不夠嗎
- 8.beanFactory和factoryBean有什么區別
- 九.數據庫
- 1.mybatis
- 1.1 MyBatis-# 與 $ 區別以及 sql 預編譯
- Mybatis系列1-Configuration
- Mybatis系列2-SQL執行過程
- Mybatis系列3-之SqlSession
- Mybatis系列4-之Executor
- Mybatis系列5-StatementHandler
- Mybatis系列6-MappedStatement
- Mybatis系列7-參數設置揭秘(ParameterHandler)
- Mybatis系列8-緩存機制
- 2.淺談聚簇索引和非聚簇索引的區別
- 3.mysql 證明為什么用limit時,offset很大會影響性能
- 4.MySQL中的索引
- 5.數據庫索引2
- 6.面試題收集
- 7.MySQL行鎖、表鎖、間隙鎖詳解
- 8.數據庫MVCC詳解
- 9.一條SQL查詢語句是如何執行的
- 10.MySQL 的 crash-safe 原理解析
- 11.MySQL 性能優化神器 Explain 使用分析
- 12.mysql中,一條update語句執行的過程是怎么樣的?期間用到了mysql的哪些log,分別有什么作用
- 十.Redis
- 0.快速復習回顧Redis
- 1.通俗易懂的Redis數據結構基礎教程
- 2.分布式鎖(一)
- 3.分布式鎖(二)
- 4.延時隊列
- 5.位圖Bitmaps
- 6.Bitmaps(位圖)的使用
- 7.Scan
- 8.redis緩存雪崩、緩存擊穿、緩存穿透
- 9.Redis為什么是單線程、及高并發快的3大原因詳解
- 10.布隆過濾器你值得擁有的開發利器
- 11.Redis哨兵、復制、集群的設計原理與區別
- 12.redis的IO多路復用
- 13.相關redis面試題
- 14.redis集群
- 十一.中間件
- 1.RabbitMQ
- 1.1 RabbitMQ實戰,hello world
- 1.2 RabbitMQ 實戰,工作隊列
- 1.3 RabbitMQ 實戰, 發布訂閱
- 1.4 RabbitMQ 實戰,路由
- 1.5 RabbitMQ 實戰,主題
- 1.6 Spring AMQP 的 AMQP 抽象
- 1.7 Spring AMQP 實戰 – 整合 RabbitMQ 發送郵件
- 1.8 RabbitMQ 的消息持久化與 Spring AMQP 的實現剖析
- 1.9 RabbitMQ必備核心知識
- 2.RocketMQ 的幾個簡單問題與答案
- 2.Kafka
- 2.1 kafka 基礎概念和術語
- 2.2 Kafka的重平衡(Rebalance)
- 2.3.kafka日志機制
- 2.4 kafka是pull還是push的方式傳遞消息的?
- 2.5 Kafka的數據處理流程
- 2.6 Kafka的腦裂預防和處理機制
- 2.7 Kafka中partition副本的Leader選舉機制
- 2.8 如果Leader掛了的時候,follower沒來得及同步,是否會出現數據不一致
- 2.9 kafka的partition副本是否會出現腦裂情況
- 十二.Zookeeper
- 0.什么是Zookeeper(漫畫)
- 1.使用docker安裝Zookeeper偽集群
- 3.ZooKeeper-Plus
- 4.zk實現分布式鎖
- 5.ZooKeeper之Watcher機制
- 6.Zookeeper之選舉及數據一致性
- 十三.計算機網絡
- 1.進制轉換:二進制、八進制、十六進制、十進制之間的轉換
- 2.位運算
- 3.計算機網絡面試題匯總1
- 十四.Docker
- 100.面試題收集合集
- 1.美團面試常見問題總結
- 2.b站部分面試題
- 3.比心面試題
- 4.騰訊面試題
- 5.哈羅部分面試
- 6.筆記
- 十五.Storm
- 1.Storm和流處理簡介
- 2.Storm 核心概念詳解
- 3.Storm 單機版本環境搭建
- 4.Storm 集群環境搭建
- 5.Storm 編程模型詳解
- 6.Storm 項目三種打包方式對比分析
- 7.Storm 集成 Redis 詳解
- 8.Storm 集成 HDFS 和 HBase
- 9.Storm 集成 Kafka
- 十六.Elasticsearch
- 1.初識ElasticSearch
- 2.文檔基本CRUD、集群健康檢查
- 3.shard&replica
- 4.document核心元數據解析及ES的并發控制
- 5.document的批量操作及數據路由原理
- 6.倒排索引
- 十七.分布式相關
- 1.分布式事務解決方案一網打盡
- 2.關于xxx怎么保證高可用的問題
- 3.一致性hash原理與實現
- 4.微服務注冊中心 Nacos 比 Eureka的優勢
- 5.Raft 協議算法
- 6.為什么微服務架構中需要網關
- 0.CAP與BASE理論
- 十八.Dubbo
- 1.快速掌握Dubbo常規應用
- 2.Dubbo應用進階
- 3.Dubbo調用模塊詳解
- 4.Dubbo調用模塊源碼分析
- 6.Dubbo協議模塊