twitter id生成演算法snowflake詳解
1 概述
分散式系統中,有一些需要使用全域性唯一ID的場景,這種時候為了防止ID衝突可以使用36位的UUID,但是UUID有一些缺點,首先他相對比較長,另外UUID一般是無序的。
為了滿足Twitter每秒上萬條訊息的請求,每條訊息都必須分配一條唯一的id,這些id還需要一些大致的順序(方便客戶端排序),並且在分散式系統中不同機器產生的id必須不同。
2 結構
snowflake生成64的id,剛好使用long來儲存,結構如下:
1位標識,由於long基本型別在Java中是帶符號的,最高位是符號位,正數是0,負數是1,所以id一般是正數,最高位是0
41位時間截,注意,41位時間截不是儲存當前時間的時間截,而是儲存時間截的差值(當前時間截 - 開始時間截 得到的值),這裡的的開始時間截,一般是我們的id生成器開始使用的時間,由我們程式來指定的(如下下面程式IdWorker類的startTime屬性)。41位的時間截,可以使用69年,年T = (1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69
10位的資料機器位,可以部署在1024個節點,包括5位datacenterId和5位workerId
12位序列,同一時間截,同一機器,可以生成4096個id
snowflake生成的ID整體上按照時間自增排序,並且整個分散式系統內不會產生ID碰撞
3 原始碼及註釋
public class IdWorker { //開始該類生成ID的時間截,1288834974657 (Thu, 04 Nov 2010 01:42:54 GMT) 這一時刻到當前時間所經過的毫秒數,佔 41 位(還有一位是符號位,永遠為 0)。 private final long startTime = 1463834116272L; //機器id所佔的位數 private long workerIdBits = 5L; //資料標識id所佔的位數 private long datacenterIdBits = 5L; //支援的最大機器id,結果是31,這個移位演算法可以很快的計算出幾位二進位制數所能表示的最大十進位制數(不信的話可以自己算一下,記住,計算機中儲存一個數都是儲存的補碼,結果是負數要從補碼得到原碼) private long maxWorkerId = -1L ^ (-1L << workerIdBits); //支援的最大資料標識id private long maxDatacenterId = -1L ^ (-1L << datacenterIdBits); //序列在id中佔的位數 private long sequenceBits = 12L; //機器id向左移12位 private long workerIdLeftShift = sequenceBits; //資料標識id向左移17位 private long datacenterIdLeftShift = workerIdBits + workerIdLeftShift; //時間截向左移5+5+12=22位 private long timestampLeftShift = datacenterIdBits + datacenterIdLeftShift; //生成序列的掩碼,這裡為1111 1111 1111 private long sequenceMask = -1 ^ (-1 << sequenceBits); private long workerId; private long datacenterId; //同一個時間截內生成的序列數,初始值是0,從0開始 private long sequence = 0L; //上次生成id的時間截 private long lastTimestamp = -1L; public IdWorker(long workerId, long datacenterId){ if(workerId < 0 || workerId > maxWorkerId){ throw new IllegalArgumentException( String.format("workerId[%d] is less than 0 or greater than maxWorkerId[%d].", workerId, maxWorkerId)); } if(datacenterId < 0 || datacenterId > maxDatacenterId){ throw new IllegalArgumentException( String.format("datacenterId[%d] is less than 0 or greater than maxDatacenterId[%d].", datacenterId, maxDatacenterId)); } this.workerId = workerId; this.datacenterId = datacenterId; } //生成id public synchronized long nextId(){ long timestamp = timeGen(); if(timestamp < lastTimestamp){ throw new RuntimeException( String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp)); } //如果是同一時間生成的,則自增 if(timestamp == lastTimestamp){ sequence = (sequence + 1) & sequenceMask; if(sequence == 0){ //生成下一個毫秒級的序列 timestamp = tilNextMillis(); //序列從0開始 sequence = 0L; } }else{ //如果發現是下一個時間單位,則自增序列回0,重新自增 sequence = 0L; } lastTimestamp = timestamp; //看本文第二部分的結構圖,移位並通過或運算拼到一起組成64位的ID return ((timestamp - startTime) << timestampLeftShift) | (datacenterId << datacenterIdLeftShift) | (workerId << workerIdLeftShift) | sequence; } protected long tilNextMillis(){ long timestamp = timeGen(); if(timestamp <= lastTimestamp){ timestamp = timeGen(); } return timestamp; } protected long timeGen(){ return System.currentTimeMillis(); } public static void main(String[] args) { class IdServiceThread implements Runnable { private Set<Long> set; @Autowired private IdService idService; public IdServiceThread(Set<Long> set, IdService idService) { this.set = set; this.idService = idService; } @Override public void run() { while (true) { long id = idService.nextId(); System.out.println("duplicate:" + id); if (!set.add(id)) { System.out.println("duplicate:" + id); } } } } Set<Long> set = new HashSet<Long>(); for(int i=0;i<100;i++){ Thread t1 = new Thread(new IdServiceThread(set, idService)); t1.setDaemon(true); t1.start(); try { Thread.sleep(30000); } catch (InterruptedException e) { e.printStackTrace(); } } } }
著重說一下-1L ^ (-1L << workerIdBits)
可以用System.out.println檢測一下,結果是31,那麼具體是怎麼操作的?
首先要明白,計算機中儲存資料都是補碼。這樣做的好處在於,採用補碼運算時,由於符號位與數值一樣參與運算,所以不必像原碼運算那樣對兩數的大小、符號作比較,從而使運算更簡單。
-1L補碼: 1111 1111
左移五位:1110 0000
異或操作:0001 1111
也就是最終的結果31