1. 程式人生 > >分散式全域性唯一ID--SnowFlake演算法

分散式全域性唯一ID--SnowFlake演算法

    說到全域性唯一ID,之前做的一個專案,有遇到類似的需求,會有多併發,但是,又需要類似於id的這麼個存在。當時是直接採用的UUID(這個方案實施起來效率最高),當時為了趕進度,就匆匆忙忙的上線了。現在正好來總結一下。

   一般情況,實現全域性唯一ID,有三種方案,分別是通過中介軟體方式、UUID、雪花演算法。

    方案一,通過中介軟體方式,可以是把資料庫或者redis快取作為媒介,從中介軟體獲取ID。這種呢,優點是可以體現全域性的遞增趨勢(優點只能想到這個),缺點呢,倒是一大堆,比如,依賴中介軟體,假如中介軟體掛了,就不能提供服務了;依賴中介軟體的寫入和事務,會影響效率;資料量大了的話,你還得考慮部署叢集,考慮走代理。這樣的話,感覺問題複雜化了

   方案二,通過UUID的方式,java.util.UUID就提供了獲取UUID的方法,使用UUID來實現全域性唯一ID,優點是操作簡單,也能實現全域性唯一的效果,缺點呢,就是不能體現全域性視野的遞增趨勢;太長了,UUID是32位,有點浪費;最重要的,是插入的效率低,因為呢,我們使用mysql的話,一般都是B+tree的結構來儲存索引,假如是資料庫自帶的那種主鍵自增,節點滿了,會裂變出新的節點,新節點滿了,再去裂變新的節點,這樣利用率和效率都很高。而UUID是無序的,會造成中間節點的分裂,也會造成不飽和的節點,插入的效率自然就比較低下了。

    方案三,雪花演算法SnowFlake,是推特公司使用的一款通過劃分名稱空間並行生成的演算法,來解決全域性唯一ID的需求,類似的還有MongoDB的object_id。雪花演算法,是64位二進位制,轉換十進位制,不超過20位。第一位是符號位,一般是不變的0,第二階梯的是41位的毫秒,第三階梯是10位的機器ID,第四階梯是12位的序列號,雪花演算法能保證一毫秒內,支援1024*4096個併發,400多W了,對付絕大多數場景,都適用了。優點就是方案一和方案二的不足的反例---不依賴中介軟體、可以體現趨勢遞增,且通過第三階梯的機器ID可以知道是哪一臺機器生成的ID,缺點呢,就是因為雪花演算法是依賴毫秒,而毫秒又是通過本機來獲取的,假如本機的時鐘回撥了,那就亂套了,可能會造成ID衝突或者ID亂序。

   最後說下java的一個SnowFlake演算法的實現

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class SnowFlakeIdGenerator {
	// 初始時間截 (2017-01-01)
	private static final long INITIAL_TIME_STAMP = 1483200000000L;
	// 機器id所佔的位數
	private static final long WORKER_ID_BITS = 5L;
	// 資料標識id所佔的位數
	private static final long DATACENTER_ID_BITS = 5L;
	// 支援的最大機器id,結果是31 (這個移位演算法可以很快的計算出幾位二進位制數所能表示的最大十進位制數)
	private static final long MAX_WORKER_ID = ~(-1L << WORKER_ID_BITS);
	// 支援的最大資料標識id,結果是31
	private static final long MAX_DATACENTER_ID = ~(-1L << DATACENTER_ID_BITS);
	// 序列在id中佔的位數
	private final long SEQUENCE_BITS = 12L;
	// 機器ID的偏移量(12)
	private final long WORKERID_OFFSET = SEQUENCE_BITS;
	// 資料中心ID的偏移量(12+5)
	private final long DATACENTERID_OFFSET = SEQUENCE_BITS + WORKER_ID_BITS ;
	// 時間截的偏移量(5+5+12)
	private final long TIMESTAMP_OFFSET = WORKER_ID_BITS + DATACENTER_ID_BITS + SEQUENCE_BITS;
	// 生成序列的掩碼,這裡為4095 (0b111111111111=0xfff=4095)
	private final long SEQUENCE_MASK = ~(-1L << SEQUENCE_BITS);
	// 工作節點ID(0~31)
	private long workerId;
	// 資料中心ID(0~31)
	private long datacenterId;
	// 毫秒內序列(0~4095)
	private long sequence = 0L;
	// 上次生成ID的時間截
	private long lastTimestamp = -1L;

	/**
	 * 建構函式
	 * @param workerId 工作ID (0~31)
	 * @param datacenterId 資料中心ID (0~31)
	 */
	public SnowFlakeIdGenerator(long workerId, long datacenterId) {
		if (workerId > MAX_WORKER_ID || workerId < 0) {
			throw new IllegalArgumentException(String.format("WorkerID 不能大於 %d 或小於 0", MAX_WORKER_ID));
		}

		if (datacenterId > MAX_DATACENTER_ID || datacenterId < 0) {
			throw new IllegalArgumentException(String.format("DataCenterID 不能大於 %d 或小於 0", MAX_DATACENTER_ID));
		}

		this.workerId = workerId;
		this.datacenterId = datacenterId;
	}

	/**
	 * 獲得下一個ID (用同步鎖保證執行緒安全)
	 * @return SnowflakeId
	 */
	public synchronized long nextId() {
		long timestamp = System.currentTimeMillis();
		// 如果當前時間小於上一次ID生成的時間戳,說明系統時鐘回退過這個時候應當丟擲異常
		if (timestamp < lastTimestamp) {
			throw new RuntimeException("當前時間小於上一次記錄的時間戳!");
		}
		// 如果是同一時間生成的,則進行毫秒內序列
		if (lastTimestamp == timestamp) {
			sequence = (sequence + 1) & SEQUENCE_MASK;
			if (sequence == 0) {// sequence等於0說明毫秒內序列已經增長到最大值
				timestamp = tilNextMillis(lastTimestamp); // 阻塞到下一個毫秒,獲得新的時間戳
			}
		} else {// 時間戳改變,毫秒內序列重置
			sequence = 0L;
		}

		// 上次生成ID的時間截
		lastTimestamp = timestamp;
		// 移位並通過或運算拼到一起組成64位的ID
		return ((timestamp - INITIAL_TIME_STAMP) << TIMESTAMP_OFFSET)  | (datacenterId << DATACENTERID_OFFSET) | (workerId << WORKERID_OFFSET) | sequence;
	}

	/**
	 * 阻塞到下一個毫秒,直到獲得新的時間戳
	 * @param lastTimestamp 上次生成ID的時間截
	 * @return 當前時間戳
	 */
	protected long tilNextMillis(long lastTimestamp) {
		long timestamp = System.currentTimeMillis();
		while (timestamp <= lastTimestamp) {
			timestamp = System.currentTimeMillis();
		}
		return timestamp;
	}

	public static void main(String[] args) {
		final SnowFlakeIdGenerator idGenerator = new SnowFlakeIdGenerator(1, 1);
		// 執行緒池並行執行10000次ID生成
		ExecutorService executorService = Executors.newCachedThreadPool();
		for (int i = 0; i < 10000; i++) {
			executorService.execute(new Runnable() {
				@Override
				public void run() {
					long id = idGenerator.nextId();
					System.out.println(id);
				}
			});
		}
		executorService.shutdown();
	}
}


待討論的問題:時鐘回撥了,怎麼辦呢?只能while迴圈等待?

雪花演算法使用起來有哪些坑?