文章已收錄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
有興趣的可以自行觀摩一下,那麼這篇文章就寫到這裡了,感謝大家的閱讀。
覺得有用就點個贊吧,你的點贊是我創作的最大動力~
我是一個努力讓大家記住的程式員。我們下期再見!!!
能力有限,如果有什麼錯誤或者不當之處,請大家批評指正,一起學習交流!