做爲一個全球人數最多的國家,一個再怎麼悽慘的行業,都能找出不少的人爲之付出。而在這個互聯網的時代,IT公司絕對比牛毛還多不少。可是大多數都是創業公司,長期存活的真的很少。大多數的IT項目在註冊量從0-100萬,日活躍1-5萬,說實話就這種系統隨便找一個有幾年工做經驗的高級工程師,而後帶幾個年輕工程師,隨便乾乾均可以作出來。
由於這樣的系統,實際上主要就是在前期快速的進行業務功能的開發,搞一個單塊系統部署在一臺服務器上,而後鏈接一個數據庫就能夠了。接着你們就是不停的在一個工程裏填充進去各類業務代碼,儘快把公司的業務支撐起來。html
可是若是真的發展的還能夠,可能就會遇到以下問題:
在運行的過程當中系統訪問數據庫的性能愈來愈差,單表數據量愈來愈大,一些複雜查詢 SQL直接拖垮!
這種時候就不得不考慮的解決方案:緩存,負載均衡,項目分塊(微服務);數據庫:讀寫分離,分庫分表等技術java
若是說此時你仍是一臺數據庫服務器在支撐每秒上萬的請求,負責任的告訴你,每次高峯期會出現下述問題:git
那麼百萬併發的數據庫架構如何設計呢?多數都是分庫分表加主從吧?算法
分庫分表:說白了就是大量分表來保證海量數據下的查詢性能。數據庫
其實大多數公司的瓶頸都在數據庫,其實若是把上面的解決方案,都實現了,基本上就沒的什麼問題了,舉例:
若是訂單一年有 1 億條數據,能夠把訂單表一共拆分爲 1024 張表,分散在5個庫中,這樣 1 億數據量的話,分散到每一個表裏也就才 10 萬量級的數據量,而後這上千張表分散在 5 臺數據庫裏就能夠了。
在寫入數據的時候,須要作兩次路由,先對訂單 id hash 後對數據庫的數量取模,能夠路由到一臺數據庫上,而後再對那臺數據庫上的表數量取模,就能夠路由到數據庫上的一個表裏了。
經過這個步驟,就可讓每一個表裏的數據量很是小,每一年 1 億數據增加,可是到每一個表裏才 10 萬條數據增加,這個系統運行 10 年,每一個表裏可能才百萬級的數據量。緩存
主從:讀寫分離服務器
這個時候總體效果已經挺不錯了,大量分表的策略保證可能將來 10 年,每一個表的數據量都不會太大,這能夠保證單表內的 SQL 執行效率和性能。網絡
而後多臺數據庫的拆分方式,能夠保證每臺數據庫服務器承載一部分的讀寫請求,下降每臺服務器的負載。多線程
可是此時還有一個問題,假如說每臺數據庫服務器承載每秒 2000 的請求,而後其中 400 請求是寫入,1600 請求是查詢。架構
也就是說,增刪改的 SQL 才佔到了 20% 的比例,80% 的請求是查詢。此時假如說隨着用戶量愈來愈大,又變成每臺服務器承載 4000 請求了。
那麼其中 800 請求是寫入,3200 請求是查詢,若是說你按照目前的狀況來擴容,就須要增長一臺數據庫服務器。
可是此時可能就會涉及到表的遷移,由於須要遷移一部分表到新的數據庫服務器上去,是否是很麻煩?
其實徹底不必,數據庫通常都支持讀寫分離,也就是作主從架構。
寫入的時候寫入主數據庫服務器,查詢的時候讀取從數據庫服務器,就可讓一個表的讀寫請求分開落地到不一樣的數據庫上去執行。
這樣的話,假如寫入主庫的請求是每秒 400,查詢從庫的請求是每秒 1600。
架構大體以下:
寫入主庫的時候,會自動同步數據到從庫上去,保證主庫和從庫數據一致。
而後查詢的時候都是走從庫去查詢的,這就經過數據庫的主從架構實現了讀寫分離的效果了。
如今的好處就是,假如說如今主庫寫請求增長到 800,這個無所謂,不須要擴容。而後從庫的讀請求增長到了 3200,須要擴容了。
這時,你直接給主庫再掛載一個新的從庫就能夠了,兩個從庫,每一個從庫支撐 1600 的讀請求,不須要由於讀請求增加來擴容主庫。
實際上線上生產你會發現,讀請求的增加速度遠遠高於寫請求,因此讀寫分離以後,大部分時候就是擴容從庫支撐更高的讀請求就能夠了。
並且另一點,對同一個表,若是你既寫入數據(涉及加鎖),還從該表查詢數據,可能會牽扯到鎖衝突等問題,不管是寫性能仍是讀性能,都會有影響。
因此一旦讀寫分離以後,對主庫的表就僅僅是寫入,沒任何查詢會影響他,對從庫的表就僅僅是查詢。
在分庫分表以後你必然要面對的一個問題,就是 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
其次在推薦一種算法:國美最近開源的分佈式ID:https://tech.meituan.com/2019/03/07/open-source-project-leaf.html
項目是一個遞進的過程,優先考慮緩存,其次讀寫分離,再分表分庫。固然這只是我的想法,各位夥伴仍是根據本身的項目和業務來綜合考慮實行方案。