今天停電,所以springboot源碼看不了,手頭剛好有本書,學習了下分布式發號器
一、方案
1、UUID
- 無法滿足業務特性。UUID雖然能保證ID的唯一性,但是無法滿足業務要求的很多其他特性,如有序性+可反解性(沒有提供反解方法,例如反解得到時間戳)+可制造性(手工生成、洗臟數據難度變大)
- 占用空間大。UUID比較長,利用JDK生成的一個UUID占用36字節(由於包含a-f,數據庫類型varchar類型):8-4-4-4-12,如果建立B+樹索引的話,導致單個索引節點包含關鍵字減少,索引節點變多,高度變高,性能下降
- 由於是無序的,作為InnoDB主鍵索引,可能會導致頁分裂,降低插入性能同時,還產生了很多內存碎片
public class UUIDTest { public static void main(String[] args) { //JDK中uuid屬於總共128個bit //time_low = 32bit //time_mid = 16bit //time_high_and_version= 16bit //variant_and_sequence= 16bit //node = 48bit //① 由於toString采用16進制(4bit)打印 8-4-4-4-12共36個字節 //② version=4的uuid,隨機數生成,沒有機器碼,所以分布式還是可能存在重復的 UUID uuid = UUID.randomUUID(); System.out.println(uuid.toString());//5db67001-7d8b-49a0-baa2-215629b00ae9 } }
2、數據庫生成
利用數據庫作為發號器,有兩種方法:表字段自增與自增序列sequence。雖然能保證id唯一性,但是會影響數據庫的性能與伸縮性。
增加數據庫壓力,降低性能。Id的產生是一種頻繁的操作,會頻繁的請求數據庫,導致數據庫性能下降。
降低數據庫審索引。無論表字段自增還是自增序列實現,都會影響數據庫分表分庫。
sequence是一種隱式字段,需要人工維護
-- 表字段自增 mysql中常用 id bigint(20) auto increment, -- 自增序列sequence oracle中常用 create sequence sequence_name increment by 1 -- 每次加的個數據 start with 1 -- 從1開始計數 nomaxvalue -- 不設置最大值 nocycle -- 一直累加,不循環 cache 10 ; sequence.nextval; -- 自增sequence並返回
3、Snowflake——雪花算法
Snowflake是Twitter開源的分布式發號器,類似uuid使用bit位控制生成。結構如下:
-- 總共64bit,
-- 時間戳放在高位,保證毫秒級的有序性;
-- 機器號+序列號處於低位,單機有序的,多機無序的 -- 1bit:無效位,其實可以作為版本號 -- 41bit:時間戳位 -- 10bit:機器號 -- 12bit:序列號
簡單實現,具體的肯定比這個復雜得多:
public class SnowFlakeIdGenerator { private final long version;//版本號 1bit private final long STARTTIMESTAMP = 1585286065061L;//創建ID生成器的時間 private long timestamp;//時間戳毫秒級 41bit private final long mcid;//機器id 10bit private int seq = -1;//自增seq private long lastTimstamp = -1;//記錄毫秒級 private long lastSeql = 0;//記錄同毫秒級的sql public SnowFlakeIdGenerator(long version,long mcid){ //版本號校驗 if (version>1 || version <0){ throw new IllegalArgumentException("version must in {0,1}"); } //機器號校驗 if (mcid>1023 || mcid < 0){ throw new IllegalArgumentException("mcid must in {0-1023}"); } this.version = version; this.mcid = mcid; } /** * 同毫秒級seq自增, * 不同毫秒級seq歸零 * @return */ private long getSeq(long current){ if (lastTimstamp == current){ //同毫秒級校驗seq是否超出范圍 if (seq > 1022){ throw new IllegalArgumentException("seq arrive max,generator failed!"); } }else{ //不同毫秒級seq歸零 lastTimstamp = current; seq = -1; System.out.println("============== 分割線 ================="); } return ++seq; } private long generator(){ long timestamp = System.currentTimeMillis()-STARTTIMESTAMP; long id = 0L; //1 + 41 + 10 + 12 由於時間處於高位,所以毫秒間是有序的 id |= version << 63; id |= timestamp << 22; id |= mcid << 12; id |= getSeq(timestamp); return id; } public static void main(String[] args) { ExecutorService pool = Executors.newFixedThreadPool(2); //開兩個線程模擬分布式兩台機器 //毫秒間是有序的 //毫秒內是單機有序的,整體是無序的 pool.submit(new Runnable() { @Override public void run() { SnowFlakeIdGenerator idGenerator = new SnowFlakeIdGenerator(0L,0L); for (int i = 0; i < 1000; i++) { System.out.println(idGenerator.generator()); } } }); pool.submit(new Runnable() { @Override public void run() { SnowFlakeIdGenerator idGenerator1 = new SnowFlakeIdGenerator(0,1L); for (int i = 0; i < 1000; i++) { System.out.println(idGenerator1.generator()); } } }); pool.shutdown(); } }
雪花算法優點:
占用空間比uuid小。由於返回的是一個長整型long,所以數據庫字段可使用bigint屬性創建,實際最大8字節。
粗略有序。通過時間戳+機器號+seq自增的設計實現了粗略有序,毫秒級內單機有序,多機無序,毫秒級間有序。作為InnoDB主鍵索引的話,降低頁分裂的可能。
沒有依賴於數據庫等中間件,不受中間件限制。
雪花算法的缺點:
趨勢遞增:由於seq放在末位,會暴露業務信息,例如競爭對手可以通過ID判斷出每天大致的業務量。
時間同步:依賴於系統時間,電子時間是需要同步的,例如每4年同步一次閏秒,可能會影響ID的生成。
4、開源項目——vesta-id-generator
自定義設計主要學習《可伸縮服務架構-框架與中間件》中的具體代碼實現,snowflake的優化版。
書上提供的開源項目地址刪除了,從github上找到了一個地址
采用兩種粒度模式的ID:最大峰值型(秒級有序)、最小峰值型(毫秒級有序)
-- 兩種類型的最大區別是:時間+序列號占用位數不同 -- 最大峰值型(秒級有序) -- 版本 : 1bit 0或1,默認0,一個版本可堅持30年,兩個就是60年了 -- 類型 : 1bit 0或1,控制粒度 -- 生成方式 : 2bit 00或01或10或11:標識三種發布模式。 -- 秒級時間 : 30bit 秒級時間從0-2^30-1,2^30/60/60/24/365=34,可使用30年,毫秒級也是。 -- 序列號 : 20bit -- 機器號 : 10bit 將序列號調整到機器號之前,避免遞增趨勢 -- 最小峰值型(毫秒級有序) -- 版本 : 1bit -- 類型 : 1bit -- 生成方式 : 2bit -- 毫秒級 : 40bit -- 序列號 : 10bit -- 機器號 : 10bit
