分散式系統應用中生成全域性唯一ID的演算法(snowflake)----java 實現,單例模式
概述
在分散式系統中,有很多的地方需要生成全域性id的場景,比方說,訂單模組,使用者id等。這種情況下大多數的做法是通過UUID來做處理。首先,UUID是36位的一個字串,相對來說是比較長的,一般我們採用的資料庫會是MySQL,因為大多數的情況下,我們都希望我們的資料是可以回滾的,那麼我們的資料表會採用innoDB,innoDB採用B+Tree實現其索引結構。所以一般對主鍵有以下的要求!
- 越短越好——越短在一個Page中儲存的節點越多,檢索速度就越快。
- 順序增長——如果每一條插入的資料的主鍵都比前面的主鍵大,那麼B-Tree上的節點也是順序增長的,不會造成頻繁的B-Tree分割。
越短越好是為了查詢的速度快,順序增長是為了插入速度快,所以UUID就感覺不是太優秀。
並且在後期資料量非常大的時候,我們採用分庫分表的情況下,可以更好的橫向擴充套件!
結構
snowflake的結構如下(每部分用-分開):
0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000
第一位為未使用,接下來的41位為毫秒級時間(41位的長度可以使用69年),然後是5位datacenterId和5位workerId(10位的長度最多支援部署1024個節點) ,最後12位是毫秒內的計數(12位的計數順序號支援每個節點每毫秒產生4096個ID序號)
一共加起來剛好64位,為一個Long型。(轉換成字串後長度最多19)
snowflake生成的ID整體上按照時間自增排序,並且整個分散式系統內不會產生ID碰撞(由datacenter和workerId作區分),並且效率較高。經測試snowflake每秒能夠產生26萬個ID。
原始碼網上多的是,但是經過我的小範圍測試,如果例項太多的話,單個系統就會出現重複的情況。如果採用spring來管理的話,應該來說是沒有問題的,但是為了嚴謹期間,我把原始碼改造成一個單例模式的類。這樣,不管怎麼呼叫,單個系統生成的id永遠不會重複!
package main.java; /** * 生成分散式系統唯一的主鍵id * string型別 * Snowflake 的結構如下 * 0-00000000 00000000 00000000 00000000 00000000 00000000 - 00000 - 0000 -000000000000 * 第一位是符號為,41位表示當前時間戳跟定義好的時間戳的差值,5位 * 41位的表示(1L << 41) / (1000L * 3600 * 24 * 365L) == 69年; * 指定10位的機器位,可以部署1024臺機器 * 所有的資料加起來就是64位,正好是一個Long型資料 */ public class SnowflakeIdWorker { /** * 開始時間戳 */ private final long startTime = 1420041600000L; /** * 機器id所佔的位數 */ private final long workIdBits = 5L; /** * 資料id所佔的位數 */ private final long dataIdBits = 5L; /** * 二進位制中,負數採用其絕對值的值得補碼得到 * long型的-1 的值就32個1 */ private final long maxWorkerId = -1L ^ (-1L << workIdBits); /** * 同上 */ private final long maxDataGenID = -1L ^ (-1L << dataIdBits); /** * 12位的序列,表示1個毫秒內可生成 2的12次冪個數據,即4096個數據 */ private final long sequenceBits = 12L; /** * 資料id存放的位置應該是向左移動12位序列值和機器碼 */ private final long dataShiftBits = sequenceBits + workIdBits; /** * 時間戳存放的位置應該是從22位開始的,左移22位 */ private final long timestampLeftShift = dataShiftBits + dataIdBits; /** * 4095 生成序列的最大值 */ private final long maxSequence = -1 ^ (-1 << sequenceBits); /** * 機器碼id 小於31 */ private long worderId; /** * 資料中心id 小於31 */ private long dataId; /** * 毫秒內計數(0-4095) */ private long sequence = 0L; /** * 上一次生成id的時間戳 */ private long lastTimeStamp = -1L; /** * 單例模式構造方法 */ private SnowflakeIdWorker() { } /** * 使用靜態內部類實現單例 */ private static class SnowflakeIdWorkerInstance { private static SnowflakeIdWorker INSTANCE = new SnowflakeIdWorker(); } public static SnowflakeIdWorker getInstance() { return SnowflakeIdWorkerInstance.INSTANCE; } /** * 初始化並配置機器碼id和資料id * * @param workerId 0-31 * @param dataId 0-31 */ public void init(long workerId, long dataId) { if (workerId > maxWorkerId || worderId < 0) { throw new IllegalArgumentException(String.format("worker Id can't greater than %d or less than 0", maxWorkerId)); } if (dataId > maxDataGenID || dataId < 0) { throw new IllegalArgumentException(String.format("data Id can't greater than %d or less than 0", maxDataGenID)); } this.worderId = workerId; this.dataId = dataId; } /** * 生成主鍵id,理論上應該在呼叫了init方法之後,呼叫生成的方式是有效的 * 不然所有的id都預設是按照機器碼和資料id都是0的情況處理 * * @return 8個位元組的長整型 */ public synchronized long genNextId() { long timeStamp = genTimeStamp(); /**表示系統的時間修改了*/ if (timeStamp < this.lastTimeStamp) { throw new RuntimeException(String.format("System clock moved;currentTimeStamp %d,lastTimeStamp = %d", timeStamp, this.lastTimeStamp)); } if (timeStamp == this.lastTimeStamp) { /**檢視序列是否溢位*/ this.sequence = (this.sequence + 1) & maxSequence; if (this.sequence == 0) { /**當出現溢位的時候,阻塞到下一個毫秒*/ timeStamp = this.toNextMillis(this.lastTimeStamp); } } else { /**此時表示時間戳跟最後的時間戳不一致,需要重置序列*/ this.sequence = 0L; } this.lastTimeStamp = timeStamp; //通過移位或運算拼接組成64ID號 return ((timeStamp - startTime) << timestampLeftShift) | (dataId << dataShiftBits) | (worderId << sequenceBits) | sequence; } /** * 生成當前時間戳,單獨寫一個方法的原因是,若之後的時候修改擴充套件,不影響之前的業務, * 只在這個方法裡面處理我們需要的資料 * * @return */ private long genTimeStamp() { return System.currentTimeMillis(); } /** * 阻塞到下一個毫秒,直到獲得新的時間戳 * * @param lastTimeStamp 上傳生成id的時間戳 * @return */ private long toNextMillis(long lastTimeStamp) { long timeStamp = this.genTimeStamp(); while (timeStamp <= lastTimeStamp) { timeStamp = this.genTimeStamp(); } return timeStamp; } //--------------------------test-------------------------------------- public static void main(String[] args) { SnowflakeIdWorker worker = SnowflakeIdWorker.getInstance(); /**第一次使用的時候希望初始化*/ worker.init(30, 14); for (int i = 0; i < 10000; i++) { long workerId = worker.genNextId(); System.out.println(workerId); System.out.println(Long.toBinaryString(workerId)); } } }
下面是測試的結果,紅框表示阻塞的情況!
參考連結
結語:夜已深,休息啦,以後再有新的點子了跟大家分享!