當前位置:
首頁 > 知識 > 支撐百萬並發的資料庫架構如何設計?

支撐百萬並發的資料庫架構如何設計?

支撐百萬並發的資料庫架構如何設計?


前言

作為一個全球人數最多的國家,一個再怎麼凄慘的行業,都能找出很多的人為之付出。而在這個互聯網的時代,IT公司絕對比牛毛還多很多。但是大多數都是創業公司,長期存活的真的不多。大多數的IT項目在註冊量從0-100萬,日活躍1-5萬,說實話就這種系統隨便找一個有幾年工作經驗的高級工程師,然後帶幾個年輕工程師,隨便乾乾都可以做出來。

因為這樣的系統,實際上主要就是在前期快速的進行業務功能的開發,搞一個單塊系統部署在一台伺服器上,然後連接一個資料庫就可以了。接著大家就是不停的在一個工程里填充進去各種業務代碼,儘快把公司的業務支撐起來。

但是如果真的發展的還可以,可能就會遇到如下問題:

在運行的過程中系統訪問資料庫的性能越來越差,單表數據量越來越大,一些複雜查詢 SQL直接拖垮!

這種時候就不得不考慮的解決方案:緩存,負載均衡,項目分塊(微服務);資料庫:讀寫分離,分庫分表等技術

如果說此時你還是一台資料庫伺服器在支撐每秒上萬的請求,負責任的告訴你,每次高峰期會出現下述問題:

  • 資料庫伺服器的磁碟 IO、網路帶寬、CPU 負載、內存消耗,都會達到非常高的情況,資料庫所在伺服器的整體負載會非常重,甚至都快不堪重負了。
  • 高峰期時,本來你單表數據量就很大,SQL 性能就不太好,這時加上你的資料庫伺服器負載太高導致性能下降,就會發現你的 SQL 性能更差了。
  • 最明顯的一個感覺,就是你的系統在高峰期各個功能都運行的很慢,用戶體驗很差,點一個按鈕可能要幾十秒才出來結果。
  • 如果你運氣不太好,資料庫伺服器的配置不是特別的高的話,弄不好你還會經歷資料庫宕機的情況,因為負載太高對資料庫壓力太大了。

其實大多數公司的瓶頸都在資料庫,其實如果把上面的解決方案,都實現了,基本上就沒的什麼問題了,舉例:

如果訂單一年有 1 億條數據,可以把訂單表一共拆分為 1024 張表,分散在5個庫中,這樣 1 億數據量的話,分散到每個表裡也就才 10 萬量級的數據量,然後這上千張表分散在 5 台資料庫里就可以了。

在寫入數據的時候,需要做兩次路由,先對訂單 id hash 後對資料庫的數量取模,可以路由到一台資料庫上,然後再對那台資料庫上的表數量取模,就可以路由到資料庫上的一個表裡了。

通過這個步驟,就可以讓每個表裡的數據量非常小,每年 1 億數據增長,但是到每個表裡才 10 萬條數據增長,這個系統運行 10 年,每個表裡可能才百萬級的數據量。

全局唯一ID

在分庫分表之後你必然要面對的一個問題,就是 id 咋生成?因為要是一個表分成多個表之後,每個表的 id 都是從 1 開始累加自增長,那肯定不對啊。

舉個例子,你的訂單表拆分為了 1024 張訂單表,每個表的 id 都從 1 開始累加,這個肯定有問題了!

你的系統就沒辦法根據表主鍵來查詢訂單了,比如 id = 50 這個訂單,在每個表裡都有!

所以此時就需要分散式架構下的全局唯一 id 生成的方案了,在分庫分表之後,對於插入資料庫中的核心 id,不能直接簡單使用表自增 id,要全局生成唯一 id,然後插入各個表中,保證每個表內的某個 id,全局唯一。

比如說訂單表雖然拆分為了 1024 張表,但是 id = 50 這個訂單,只會存在於一個表裡。

那麼如何實現全局唯一 id 呢?有以下幾種方案:

方案一:獨立資料庫自增 id

這個方案就是說你的系統每次要生成一個 id,都是往一個獨立庫的一個獨立表裡插入一條沒什麼業務含義的數據,然後獲取一個資料庫自增的一個 id。拿到這個 id 之後再往對應的分庫分表裡去寫入。

比如說你有一個 auto_id 庫,裡面就一個表,叫做 auto_id 表,有一個 id 是自增長的。

那麼你每次要獲取一個全局唯一 id,直接往這個表裡插入一條記錄,獲取一個全局唯一 id 即可,然後這個全局唯一 id 就可以插入訂單的分庫分表中。

這個方案的好處就是方便簡單,誰都會用。缺點就是單庫生成自增 id,要是高並發的話,就會有瓶頸的,因為 auto_id 庫要是承載個每秒幾萬並發,肯定是不現實的了。

方案二:UUID

這個每個人都應該知道吧,就是用 UUID 生成一個全局唯一的 id。

好處就是每個系統本地生成,不要基於資料庫來了。不好之處就是,UUID 太長了,作為主鍵性能太差了,不適合用於主鍵。

如果你是要隨機生成個什麼文件名了,編號之類的,你可以用 UUID,但是作為主鍵是不能用 UUID 的。

方案三:獲取系統當前時間

這個方案的意思就是獲取當前時間作為全局唯一的 id。但是問題是,並發很高的時候,比如一秒並發幾千,會有重複的情況,這個肯定是不合適的。

一般如果用這個方案,是將當前時間跟很多其他的業務欄位拼接起來,作為一個 id,如果業務上你覺得可以接受,那麼也是可以的。

你可以將別的業務欄位值跟當前時間拼接起來,組成一個全局唯一的編號,比如說訂單編號:時間戳 + 用戶 id + 業務含義編碼。

方案四:SnowFlake 演算法的思想分析

SnowFlake 演算法,是 Twitter 開源的分散式 id 生成演算法。其核心思想就是:使用一個 64 bit 的 long 型的數字作為全局唯一 id。這 64 個 bit 中,其中 1 個 bit 是不用的,然後用其中的 41 bit 作為毫秒數,用 10 bit 作為工作機器 id,12 bit 作為序列號。

支撐百萬並發的資料庫架構如何設計?

給大家舉個例子吧,比如下面那個 64 bit 的 long 型數字:

  • 第一個部分,是 1 個 bit:0,這個是無意義的。
  • 第二個部分是 41 個 bit:表示的是時間戳。
  • 第三個部分是 5 個 bit:表示的是機房 id,10001。
  • 第四個部分是 5 個 bit:表示的是機器 id,1 1001。
  • 第五個部分是 12 個 bit:表示的序號,就是某個機房某台機器上這一毫秒內同時生成的 id 的序號,0000 00000000。

①1 bit:是不用的,為啥呢?

因為二進位里第一個 bit 為如果是 1,那麼都是負數,但是我們生成的 id 都是正數,所以第一個 bit 統一都是 0。

②41 bit:表示的是時間戳,單位是毫秒。

41 bit 可以表示的數字多達 2^41 - 1,也就是可以標識 2 ^ 41 - 1 個毫秒值,換算成年就是表示 69 年的時間。

③10 bit:記錄工作機器 id,代表的是這個服務最多可以部署在 2^10 台機器上,也就是 1024 台機器。

但是 10 bit 里 5 個 bit 代表機房 id,5 個 bit 代表機器 id。意思就是最多代表 2 ^ 5 個機房(32 個機房),每個機房裡可以代表 2 ^ 5 個機器(32 台機器)。

④12 bit:這個是用來記錄同一個毫秒內產生的不同 id。

12 bit 可以代表的最大正整數是 2 ^ 12 - 1 = 4096,也就是說可以用這個 12 bit 代表的數字來區分同一個毫秒內的 4096 個不同的 id。簡單來說,你的某個服務假設要生成一個全局唯一 id,那麼就可以發送一個請求給部署了 SnowFlake 演算法的系統,由這個 SnowFlake 演算法系統來生成唯一 id。

這個 SnowFlake 演算法系統首先肯定是知道自己所在的機房和機器的,比如機房 id = 17,機器 id = 12。

接著 SnowFlake 演算法系統接收到這個請求之後,首先就會用二進位位運算的方式生成一個 64 bit 的 long 型 id,64 個 bit 中的第一個 bit 是無意義的。

接著 41 個 bit,就可以用當前時間戳(單位到毫秒),然後接著 5 個 bit 設置上這個機房 id,還有 5 個 bit 設置上機器 id。

最後再判斷一下,當前這台機房的這台機器上這一毫秒內,這是第幾個請求,給這次生成 id 的請求累加一個序號,作為最後的 12 個 bit。

最終一個 64 個 bit 的 id 就出來了,類似於:

支撐百萬並發的資料庫架構如何設計?

這個演算法可以保證說,一個機房的一台機器上,在同一毫秒內,生成了一個唯一的 id。可能一個毫秒內會生成多個 id,但是有最後 12 個 bit 的序號來區分開來。

下面我們簡單看看這個 SnowFlake 演算法的一個代碼實現,這就是個示例,大家如果理解了這個意思之後,以後可以自己嘗試改造這個演算法。

總之就是用一個 64 bit 的數字中各個 bit 位來設置不同的標誌位,區分每一個 id。

SnowFlake 演算法JAVA版(含測試方法):

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import lombok.ToString;
/**
* Copyright: Copyright (c) 2019
*
* @ClassName: IdWorker.java
* @Description: <p>SnowFlake 演算法,是 Twitter 開源的分散式 id 生成演算法。
* 其核心思想就是:使用一個 64 bit 的 long 型的數字作為全局唯一 id。
* 這 64 個 bit 中,其中 1 個 bit 是不用的,然後用其中的 41 bit 作為毫秒數,
* 用 10 bit 作為工作機器 id,12 bit 作為序列號
* </p>
* @version: v1.0.0
* @author: BianPeng
* @date: 2019年4月11日 下午3:13:41
*
* Modification History:
* Date Author Version Description
*---------------------------------------------------------------*
* 2019年4月11日 BianPeng v1.0.0 initialize
*/
@ToString
public class SnowflakeIdFactory {

static Logger log = LoggerFactory.getLogger(SnowflakeIdFactory.class);

private final long twepoch = 1288834974657L;
private final long workerIdBits = 5L;
private final long datacenterIdBits = 5L;
private final long maxWorkerId = -1L ^ (-1L << workerIdBits);
private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
private final long sequenceBits = 12L;
private final long workerIdShift = sequenceBits;
private final long datacenterIdShift = sequenceBits + workerIdBits;
private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
private final long sequenceMask = -1L ^ (-1L << sequenceBits);

private long workerId;
private long datacenterId;
private long sequence = 0L;
private long lastTimestamp = -1L;

public SnowflakeIdFactory(long workerId, long datacenterId) {
if (workerId > maxWorkerId || workerId < 0) {
throw new IllegalArgumentException(String.format("worker Id can"t be greater than %d or less than 0", maxWorkerId));
}
if (datacenterId > maxDatacenterId || datacenterId < 0) {
throw new IllegalArgumentException(String.format("datacenter Id can"t be greater than %d or less than 0", maxDatacenterId));
}
this.workerId = workerId;
this.datacenterId = datacenterId;
}

public synchronized long nextId() {
long timestamp = timeGen();
if (timestamp < lastTimestamp) {
//伺服器時鐘被調整了,ID生成器停止服務.
throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
}
if (lastTimestamp == timestamp) {
sequence = (sequence + 1) & sequenceMask;
if (sequence == 0) {
timestamp = tilNextMillis(lastTimestamp);
}
} else {
sequence = 0L;
}

lastTimestamp = timestamp;
return ((timestamp - twepoch) << timestampLeftShift) | (datacenterId << datacenterIdShift) | (workerId << workerIdShift) | sequence;
}

protected long tilNextMillis(long lastTimestamp) {
long timestamp = timeGen();
while (timestamp <= lastTimestamp) {
timestamp = timeGen();
}
return timestamp;
}

protected long timeGen() {
return System.currentTimeMillis();
}

public static void testProductIdByMoreThread(int dataCenterId, int workerId, int n) throws InterruptedException {
List<Thread> tlist = new ArrayList<>();
Set<Long> setAll = new HashSet<>();
CountDownLatch cdLatch = new CountDownLatch(10);
long start = System.currentTimeMillis();
int threadNo = dataCenterId;
Map<String,SnowflakeIdFactory> idFactories = new HashMap<>();
for(int i=0;i<10;i++){
//用線程名稱做map key.
idFactories.put("snowflake"+i,new SnowflakeIdFactory(workerId, threadNo++));
}
for(int i=0;i<10;i++){
Thread temp =new Thread(new Runnable() {
@Override
public void run() {
Set<Long> setId = new HashSet<>();
SnowflakeIdFactory idWorker = idFactories.get(Thread.currentThread().getName());
for(int j=0;j<n;j++){
setId.add(idWorker.nextId());
}
synchronized (setAll){
setAll.addAll(setId);
log.info("{}生產了{}個id,並成功加入到setAll中.",Thread.currentThread().getName(),n);
}
cdLatch.countDown();
}
},"snowflake"+i);
tlist.add(temp);
}
for(int j=0;j<10;j++){
tlist.get(j).start();
}
cdLatch.await();

long end1 = System.currentTimeMillis() - start;

log.info("共耗時:{}毫秒,預期應該生產{}個id, 實際合併總計生成ID個數:{}",end1,10*n,setAll.size());

}

public static void testProductId(int dataCenterId, int workerId, int n){
SnowflakeIdFactory idWorker = new SnowflakeIdFactory(workerId, dataCenterId);
SnowflakeIdFactory idWorker2 = new SnowflakeIdFactory(workerId+1, dataCenterId);
Set<Long> setOne = new HashSet<>();
Set<Long> setTow = new HashSet<>();
long start = System.currentTimeMillis();
for (int i = 0; i < n; i++) {
setOne.add(idWorker.nextId());//加入set
}
long end1 = System.currentTimeMillis() - start;
log.info("第一批ID預計生成{}個,實際生成{}個<<<<*>>>>共耗時:{}",n,setOne.size(),end1);

for (int i = 0; i < n; i++) {
setTow.add(idWorker2.nextId());//加入set
}
long end2 = System.currentTimeMillis() - start;
log.info("第二批ID預計生成{}個,實際生成{}個<<<<*>>>>共耗時:{}",n,setTow.size(),end2);

setOne.addAll(setTow);
log.info("合併總計生成ID個數:{}",setOne.size());

}

public static void testPerSecondProductIdNums(){
SnowflakeIdFactory idWorker = new SnowflakeIdFactory(1, 2);
long start = System.currentTimeMillis();
int count = 0;
for (int i = 0; System.currentTimeMillis()-start<1000; i++,count=i) {
/** 測試方法一: 此用法純粹的生產ID,每秒生產ID個數為400w+ */
//idWorker.nextId();
/** 測試方法二: 在log中列印,同時獲取ID,此用法生產ID的能力受限於log.error()的吞吐能力.
* 每秒徘徊在10萬左右. */
log.info(""+idWorker.nextId());
}
long end = System.currentTimeMillis()-start;
System.out.println(end);
System.out.println(count);
}

public static void main(String[] args) {
/** case1: 測試每秒生產id個數?
* 結論: 每秒生產id個數400w+
*/
//testPerSecondProductIdNums();

/** case2: 單線程-測試多個生產者同時生產N個id,驗證id是否有重複?
* 結論: 驗證通過,沒有重複.
*/
//testProductId(1,2,10000);//驗證通過!
//testProductId(1,2,20000);//驗證通過!

/** case3: 多線程-測試多個生產者同時生產N個id, 全部id在全局範圍內是否會重複?
* 結論: 驗證通過,沒有重複.
*/
try {
testProductIdByMoreThread(1,2,100000);//單機測試此場景,性能損失至少折半!
} catch (InterruptedException e) {
e.printStackTrace();
}

}
}

這個演算法也叫雪花演算法我使用的類源碼:https://gitee.com/flying-cattle/earn_knife/blob/master/item-common/src/main/java/com/item/util/SnowflakeIdWorker.java

項目是一個遞進的過程,優先考慮緩存,其次讀寫分離,再分表分庫。當然這只是個人想法,各位夥伴還是根據自己的項目和業務來綜合考慮實行方案。

作者:邊鵬_尛爺鑫

原文:https://my.oschina.net/bianxin/blog/3035494

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 程序員小新人學習 的精彩文章:

TAG:程序員小新人學習 |