提示:下方有源代碼地址,請自行拿取java
在互聯網項目中比較經常使用到的關係型數據庫是MySQL,隨着用戶和業務的增加,傳統的單庫單表模式難以知足大量的業務數據存儲以及查詢,單庫單表中大量的數據會使寫入、查詢效率很是之慢,此時應該採起分庫分表策略來解決。node
提示:如下是本篇文章正文內容,案例僅供參考git
假設目前有一個電商系統使用的是MySQL,要設計大數據量存儲、高併發、高性能可擴展的方案,數據庫中有用戶表。用戶會很是多,而且要實現高擴展性,你會怎麼去設計? OK我們先看傳統的分庫分表方式 算法
固然還有些小夥伴知道按照省份/地區或必定的業務關係進行數據庫拆分 OK,問題來了,如何保證合理的讓數據存儲在不一樣的庫不一樣的表裏呢?讓庫減小併發壓力?應該怎麼去制定分庫分表的規則?不用急,這不就來了數據庫
第一種方法們能夠指定一個數據範圍來進行分表,例如從1~1000000,1000001-2000000,使用一百萬一張表的方式,以下圖所示緩存
固然這種方法須要維護表的ID,特別是分佈式環境下,這種分佈式ID,在不使用第三方分表工具的狀況下,建議使用Redis,Redis的incr操做能夠輕鬆的維護分佈式的表ID。服務器
RANGE方法優勢: 擴容簡單,提早建好庫、表就好markdown
RANGE方法缺點: 大部分讀和寫都訪會問新的數據,有IO瓶頸,這樣子形成新庫壓力過大,不建議採用。架構
針對上述RANGE方式分表有IO瓶頸的問題,我們能夠採用根據用戶ID HASG取模的方式進行分庫分表,如圖所示: 併發
這樣就能夠將數據分散在不一樣的庫、表中,避免了IO瓶頸的問題。
HASH取模方法優勢: 能保證數據較均勻的分散落在不一樣的庫、表中,減輕了數據庫壓力
HASH取模方法缺點: 擴容麻煩、遷移數據時每次都須要從新計算hash值分配到不一樣的庫和表
經過HASH取模也不是最完美的辦法,那什麼纔是呢?
使用一致性HASH算法能完美的解決問題
普通HASH算法:
普通哈希算法將任意長度的二進制值映射爲較短的固定長度的二進制值,這個小的二進制值稱爲哈希值。哈希值是一段數據惟一且極其緊湊的數值表示形式。
普通的hash算法在分佈式應用中的不足:在分佈式的存儲系統中,要將數據存儲到具體的節點上,若是咱們採用普通的hash算法進行路由,將數據映射到具體的節點上,如key%n,key是數據的key,n是機器節點數,若是有一個機器加入或退出集羣,則全部的數據映射都無效了,若是是持久化存儲則要作數據遷移,若是是分佈式緩存,則其餘緩存就失效了。
一致性HASH算法: 按照經常使用的hash算法來將對應的key哈希到一個具備2^32次方個節點的空間中,即0~ (2^32)-1的數字空間中。如今咱們能夠將這些數字頭尾相連,想象成一個閉合的環形,以下圖所示。
這個圓環首尾相連,那麼假設如今有三個數據庫服務器節點node一、node二、node3三個節點,每一個節點負責本身這部分的用戶數據存儲,假設有用戶user一、user二、user3,咱們能夠對服務器節點進行HASH運算,假設HASH計算後,user1落在node1上,user2落在node2上,user3落在user3上
OK,如今我們假設node3節點失效了
user3將會落到node1上,而以前的node1和node2數據不會改變,再假設新增了節點node4
你會發現user3會落到node4上,你會發現,經過對節點的添加和刪除的分析,一致性哈希算法在保持了單調性的同時,仍是數據的遷移達到了最小,這樣的算法對分佈式集羣來講是很是合適的,避免了大量數據遷移,減少了服務器的的壓力。
固然還有一個問題還須要解決,那就是平衡性。從圖咱們能夠看出,當服務器節點比較少的時候,會出現一個問題,就是此時必然形成大量數據集中到一個節點上面,極少數數據集中到另外的節點上面。
爲了解決這種數據傾斜問題,一致性哈希算法引入了虛擬節點機制,即對每個服務節點計算多個哈希,每一個計算結果位置都放置一個節點,稱爲虛擬節點。具體作法能夠先肯定每一個物理節點關聯的虛擬節點數量,而後在ip或者主機名後面增長編號。例如上面的狀況,能夠爲每臺服務器計算三個虛擬節點,因而能夠分別計算 「node 1-1」、「node 1-2」、「node 1-3」、「node 2-1」、「node 2-2」、「node 2-3」、「node 3-1」、「node 3-2」、「node 3-3」的哈希值,這樣造成九個虛擬節點
例如user1定位到node 1-一、node 1-二、node 1-3上其實都是定位到node1這個節點上,這樣可以解決服務節點少時數據傾斜的問題,固然這個虛擬節點的個數不是說固定三個或者至多、至少三個,這裏只是一個例子,具體虛擬節點的多少,須要根據實際的業務狀況而定。
一致性HASH方法優勢: 經過虛擬節點方式能保證數據較均勻的分散落在不一樣的庫、表中,而且新增、刪除節點不影響其餘節點的數據,高可用、容災性強。
一致性取模方法缺點: 嗯,比起以上兩種,能夠認爲沒有。
OK,不廢話,接下來上單元測試,假設有三個節點,每一個節點有三個虛擬節點的狀況
package com.hyh.core.test;
import com.hyh.utils.common.StringUtils;
import org.junit.Test;
import java.util.LinkedList;
import java.util.List;
import java.util.SortedMap;
import java.util.TreeMap;
/**
* 一致性HASH TEST
*
* @Author heyuhua
* @create 2021/1/31 19:50
*/
public class ConsistentHashTest {
//待添加入Hash環的服務器列表
private static String[] servers = {"192.168.5.1", "192.168.5.2", "192.168.5.3"};
//真實結點列表,考慮到服務器上線、下線的場景,即添加、刪除的場景會比較頻繁,這裏使用LinkedList會更好
private static List<String> realNodes = new LinkedList<>();
//虛擬節點,key表示虛擬節點的hash值,value表示虛擬節點的名稱
private static SortedMap<Integer, String> virtualNodes = new TreeMap<>();
//一個真實結點對應3個虛擬節點
private static final int VIRTUAL_NODES = 3;
/**
* 測試有虛擬節點的一致性HASH
*/
@Test
public void testConsistentHash() {
initNodes();
String[] users = {"user1", "user2", "user3", "user4", "user5", "user6", "user7", "user8", "user9"};
for (int i = 0; i < users.length; i++)
System.out.println("[" + users[i] + "]的hash值爲" +
getHash(users[i]) + ", 被路由到結點[" + getServer(users[i]) + "]");
}
/**
* 先把原始的服務器添加到真實結點列表中
*/
public void initNodes() {
for (int i = 0; i < servers.length; i++)
realNodes.add(servers[i]);
for (String str : realNodes) {
for (int i = 0; i < VIRTUAL_NODES; i++) {
String virtualNodeName = str + "-虛擬節點" + String.valueOf(i);
int hash = getHash(virtualNodeName);
System.out.println("虛擬節點[" + virtualNodeName + "]被添加, hash值爲" + hash);
virtualNodes.put(hash, virtualNodeName);
}
}
System.out.println();
}
//使用FNV1_32_HASH算法計算服務器的Hash值,這裏不使用重寫hashCode的方法,最終效果沒區別
private static int getHash(String str) {
final int p = 16777619;
int hash = (int) 2166136261L;
for (int i = 0; i < str.length(); i++)
hash = (hash ^ str.charAt(i)) * p;
hash += hash << 13;
hash ^= hash >> 7;
hash += hash << 3;
hash ^= hash >> 17;
hash += hash << 5;
// 若是算出來的值爲負數則取其絕對值
if (hash < 0)
hash = Math.abs(hash);
return hash;
}
//獲得應當路由到的結點
private static String getServer(String key) {
//獲得該key的hash值
int hash = getHash(key);
// 獲得大於該Hash值的全部Map
SortedMap<Integer, String> subMap = virtualNodes.tailMap(hash);
String virtualNode;
if (subMap.isEmpty()) {
//若是沒有比該key的hash值大的,則從第一個node開始
Integer i = virtualNodes.firstKey();
//返回對應的服務器
virtualNode = virtualNodes.get(i);
} else {
//第一個Key就是順時針過去離node最近的那個結點
Integer i = subMap.firstKey();
//返回對應的服務器
virtualNode = subMap.get(i);
}
//virtualNode虛擬節點名稱要截取一下
if (StringUtils.isNotBlank(virtualNode)) {
return virtualNode.substring(0, virtualNode.indexOf("-"));
}
return null;
}
}
複製代碼
這裏模擬9個用戶對象hash後被路由的狀況,看下結果
分庫分表在分佈式微服務架構環境下建議強烈使用一致性HASH算法來作,固然分佈式環境下也會產生業務數據數據一致性、分佈式事務問題,下期我們再來探討數據一致性、分佈式事務的解決方案
路漫漫其修遠兮,吾願與君上下而求索,感謝各位帥哥、靚妹的點贊、收藏和評論,咱們下期見。
關注我帶你走進架構師的成長之路。
源碼地址:點此查看源碼.