文章已收錄Github精選,歡迎Star:https://github.com/yehongzhi/learningSummary
為什麼使用雪花ID
在以前的項目中,最常見的兩種主鍵類型是自增Id和UUID,在比較這兩種ID之前首先要搞明白一個問題,就是為什麼主鍵有序比無序查詢效率要快,因為自增Id和UUID之間最大的不同點就在於有序性。
我們都知道,當我們定義了主鍵時,數據庫會選擇表的主鍵作為聚集索引(B+Tree),mysql 在底層是以數據頁為單位來存儲數據的。
也就是說如果主鍵為自增 id
的話,mysql 在寫滿一個數據頁的時候,直接申請另一個新數據頁接著寫就可以了。如果一個數據頁存滿了,mysql 就會去申請一個新的數據頁來存儲數據。如果主鍵是UUID
,為了確保索引有序,mysql 就需要將每次插入的數據都放到合適的位置上。這就造成了頁分裂,這個大量移動數據的過程是會嚴重影響插入效率的。
一句話總結就是,InnoDB表的數據寫入順序能和B+樹索引的葉子節點順序一致的話,這時候存取效率是最高的。
但是為什麼很多情況又不用自增id
作為主鍵呢?
- 容易導致主鍵重複。比如導入舊數據時,線上又有新的數據新增,這時就有可能在導入時發生主鍵重複的異常。為了避免導入數據時出現主鍵重複的情況,要選擇在應用停業後導入舊數據,導入完成後再啟動應用。顯然這樣會造成不必要的麻煩。而UUID作為主鍵就不用擔心這種情況。
- 不利於數據庫的擴展。當採用自增id時,分庫分表也會有主鍵重複的問題。UUID則不用擔心這種問題。
那麼問題就來了,自增id
會擔心主鍵重複,UUID
不能保證有序性,有沒有一種ID既是有序的,又是唯一的呢?
當然有,就是雪花ID
。
什麼是雪花ID
snowflake是Twitter開源的分佈式ID生成算法,結果是64bit的Long類型的ID,有著全局唯一和有序遞增的特點。
- 最高位是符號位,因為生成的 ID 總是正數,始終為0,不可用。
- 41位的時間序列,精確到毫秒級,41位的長度可以使用69年。時間位還有一個很重要的作用是可以根據時間進行排序。
- 10位的機器標識,10位的長度最多支持部署1024個節點。
- 12位的計數序列號,序列號即一系列的自增ID,可以支持同一節點同一毫秒生成多個ID序號,12位的計數序列號支持每個節點每毫秒產生4096個ID序號。
缺點也是有的,就是強依賴機器時鐘,如果機器上時鐘回撥,有可能會導致主鍵重複的問題。
Java實現雪花ID
下面是用Java實現雪花ID的代碼,供大家參考一下。
public class SnowflakeIdWorker {
/**
* 開始時間:2020-01-01 00:00:00
*/
private final long beginTs = 1577808000000L;
private final long workerIdBits = 10;
/**
* 2^10 - 1 = 1023
*/
private final long maxWorkerId = -1L ^ (-1L << workerIdBits);
private final long sequenceBits = 12;
/**
* 2^12 - 1 = 4095
*/
private final long maxSequence = -1L ^ (-1L << sequenceBits);
/**
* 時間戳左移22位
*/
private final long timestampLeftOffset = workerIdBits + sequenceBits;
/**
* 業務ID左移12位
*/
private final long workerIdLeftOffset = sequenceBits;
/**
* 合併了機器ID和數據標示ID,統稱業務ID,10位
*/
private long workerId;
/**
* 毫秒內序列,12位,2^12 = 4096個數字
*/
private long sequence = 0L;
/**
* 上一次生成的ID的時間戳,同一個worker中
*/
private long lastTimestamp = -1L;
public SnowflakeIdWorker(long workerId) {
if (workerId > maxWorkerId || workerId < 0) {
throw new IllegalArgumentException(String.format("WorkerId必須大於或等於0且小於或等於%d", maxWorkerId));
}
this.workerId = workerId;
}
public synchronized long nextId() {
long ts = System.currentTimeMillis();
if (ts < lastTimestamp) {
throw new RuntimeException(String.format("系統時鐘回退了%d毫秒", (lastTimestamp - ts)));
}
// 同一時間內,則計算序列號
if (ts == lastTimestamp) {
// 序列號溢出
if (++sequence > maxSequence) {
ts = tilNextMillis(lastTimestamp);
sequence = 0L;
}
} else {
// 時間戳改變,重置序列號
sequence = 0L;
}
lastTimestamp = ts;
// 0 - 00000000 00000000 00000000 00000000 00000000 0 - 00000000 00 - 00000000 0000
// 左移後,低位補0,進行按位或運算相當於二進制拼接
// 本來高位還有個0<<63,0與任何數字按位或都是本身,所以寫不寫效果一樣
return (ts - beginTs) << timestampLeftOffset | workerId << workerIdLeftOffset | sequence;
}
/**
* 阻塞到下一個毫秒
*
* @param lastTimestamp
* @return
*/
private long tilNextMillis(long lastTimestamp) {
long ts = System.currentTimeMillis();
while (ts <= lastTimestamp) {
ts = System.currentTimeMillis();
}
return ts;
}
public static void main(String[] args) {
SnowflakeIdWorker snowflakeIdWorker = new SnowflakeIdWorker(7);
for (int i = 0; i < 10; i++) {
long id = snowflakeIdWorker.nextId();
System.out.println(id);
}
}
}
main方法,測試結果如下:
184309536616640512
184309536616640513
184309536616640514
184309536616640515
184309536616640516
184309536616640517
184309536616640518
184309536616640519
184309536616640520
184309536616640521
總結
在大部分公司的開發項目中裡,雪花ID是主流的ID生成策略,除了自己實現之外,目前市場上也有很多開源的實現,比如:
- 美團開源的Leaf
- 百度開源的UidGenerator
有興趣的可以自行觀摩一下,那麼這篇文章就寫到這裡了,感謝大家的閱讀。
覺得有用就點個贊吧,你的點贊是我創作的最大動力~
我是一個努力讓大家記住的程序員。我們下期再見!!!
能力有限,如果有什麼錯誤或者不當之處,請大家批評指正,一起學習交流!