MySQL分庫分表詳情

一、業務場景介紹

假設目前有一個電商系統使用的是MySQL,要設計大數據量存儲、高並發、高性能可擴展的方案,數據庫中有用戶表。用戶會非常多,並且要實現高擴展性,你會怎麼去設計? OK咱們先看傳統的分庫分表方式

當然還有些小夥伴知道按照省份/地區或一定的業務關系進行數據庫拆分

OK,問題來瞭,如何保證合理的讓數據存儲在不同的庫不同的表裡呢?讓庫減少並發壓力?應該怎麼去制定分庫分表的規則?不用急,這不就來瞭

二、水平分庫分表方法

1.RANGE

第一種方法們可以指定一個數據范圍來進行分表,例如從1~1000000,1000001-2000000,使用一百萬一張表的方式,如下圖所示

在這裡插入圖片描述 當然這種方法需要維護表的ID,特別是分佈式環境下,這種分佈式ID,在不使用第三方分表工具的情況下,建議使用RedisRedisincr操作可以輕松的維護分佈式的表ID。

RANGE方法優點: 擴容簡單,提前建好庫、表就好

RANGE方法缺點: 大部分讀和寫都訪會問新的數據,有IO瓶頸,這樣子造成新庫壓力過大,不建議采用。

2.HASH取模

針對上述RANGE方式分表有IO瓶頸的問題,咱們可以采用根據用戶ID HASG取模的方式進行分庫分表,如圖所示:

這樣就可以將數據分散在不同的庫、表中,避免瞭IO瓶頸的問題。

HASH取模方法優點: 能保證數據較均勻的分散落在不同的庫、表中,減輕瞭數據庫壓力

HASH取模方法缺點: 擴容麻煩、遷移數據時每次都需要重新計算hash值分配到不同的庫和表

3.一致性HASH

通過HASH取模也不是最完美的辦法,那什麼才是呢?

使用一致性HASH算法能完美的解決問題

普通HASH算法:

普通哈希算法將任意長度的二進制值映射為較短的固定長度的二進制值,這個小的二進制值稱為哈希值。哈希值是一段數據唯一且極其緊湊的數值表示形式。

普通的hash算法在分佈式應用中的不足:在分佈式的存儲系統中,要將數據存儲到具體的節點上,如果我們采用普通的hash算法進行路由,將數據映射到具體的節點上,如key%nkey是數據的key,n是機器節點數,如果有一個機器加入或退出集群,則所有的數據映射都無效瞭,如果是持久化存儲則要做數據遷移,如果是分佈式緩存,則其他緩存就失效瞭。

一致性HASH算法: 按照常用的hash算法來將對應的key哈希到一個具有2^32次方個節點的空間中,即0~ (2^32)-1的數字空間中。現在我們可以將這些數字頭尾相連,想象成一個閉合的環形,如下圖所示。

這個圓環首尾相連,那麼假設現在有三個數據庫服務器節點node1node2node3三個節點,每個節點負責自己這部分的用戶數據存儲,假設有用戶user1、user2、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-1node 1-2node 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算法來做,當然分佈式環境下也會產生業務數據數據一致性、分佈式事務問題,下期咱們再來探討數據一致性、分佈式事務的解決方案

到此這篇關於MySQL分庫分表詳情的文章就介紹到這瞭,更多相關MySQL分庫分表內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: