Java雪花算法的原理和實現(xiàn)方法
一、概述
分布式高并發(fā)的環(huán)境下,常見的就是12306節(jié)日訂票,在大量用戶同是搶購一個方向的票,毫秒級的時間下可能生成數(shù)萬個訂單,此時為確保生成訂單ID的唯一性變得至關(guān)重要。此時秒殺環(huán)境下,不僅要保障ID唯一性,還得確保ID生成的優(yōu)先度。
二、生成ID規(guī)則部分硬性要求
- 全局唯一:不能出現(xiàn)重復(fù)的ID號,既然是唯一標識,這是最基本的要求。
- 趨勢遞增:在MySQL的InnoDB引擎中適用的是聚集索引,由于多數(shù)RDBMS使用B+Tree的數(shù)據(jù)結(jié)構(gòu)來存儲索引數(shù)據(jù),在主鍵的選擇上我們盡量使用有序的主鍵保證寫入性能。
- 單調(diào)遞增:保證下一個ID一定大于上一個ID,如事務(wù)版本號、排序等特殊需求。
- 信息安全:如果ID是連續(xù)的,惡意用戶的抓取工作就非常容易,直接按照順序下載指定URL即可;如果是訂單號就危險。
- 含有時間戳:生成的ID包含完整的時間戳信息。
三、ID號生成系統(tǒng)可用性要求
- 高可用:發(fā)一個獲取分布式ID的請求,服務(wù)器就是保證99.9999%的情況下給我創(chuàng)建一個唯一分布式ID。
- 低延遲:發(fā)一個獲取分布式ID的請求,服務(wù)器要快,極速。
- 高QPS:如果一次請求10萬個分布式ID,服務(wù)器要頂住并成功創(chuàng)建10萬個分布式ID。
四、解決分布式ID通用方案
4.1 UUID
UUID(Universally Unique Identifier)的標準型式包含32個16進制數(shù)字,以連字號分為五段,形式為:8-4-4-4-12的36個字符,示例:1E785B2B-111C-752A-997B-3346E7495CE2;UUID性能非常高,不依賴網(wǎng)絡(luò),本地生成。
UUID缺點:
無序,無法預(yù)測它的生成順序,不能生成遞增有序的數(shù)字。在MySql官方推薦主鍵約短越好,UUID是一個32位的字符串,所以不推薦使用。
索引,B+Tree索引的分裂
分布式Id是主鍵,主鍵是聚簇索引。Mysql的索引是B+Tree來實現(xiàn)的,每次新的UUID數(shù)據(jù)的插入,為了新的UUID數(shù)據(jù)的插入,為了查詢的優(yōu)化,都會對索引底部的B+Tree進行修改;因為UUID數(shù)據(jù)是無序的,所以每一次UUID數(shù)據(jù)的插入都會對主鍵的聚簇索引做很大的修改,在做數(shù)據(jù)Insert時,會插入主鍵是無序的,會導(dǎo)致一些中間節(jié)點的產(chǎn)生分裂,會導(dǎo)致大量不飽和的節(jié)點。這樣大大降低了數(shù)據(jù)庫插入的性能。
4.2 數(shù)據(jù)庫自增主鍵
單機
在分布式里面,數(shù)據(jù)庫的自增ID機制的主要原理是:數(shù)據(jù)庫自增ID和MySql數(shù)據(jù)庫的replace into實現(xiàn)的。
Replace into的含義是插入一條紀錄,如果表中唯一索引的值遇到?jīng)_突,則替換老數(shù)據(jù)。
在單體應(yīng)用的時候,自增長ID使用,但是在集群分布式應(yīng)用中單體應(yīng)用就不適合。
- 系統(tǒng)水平擴展比較困難,比如定義好了增長步長和機器臺數(shù)之后,在大量添加服務(wù)器時,需要重新設(shè)置初始值,這樣可操作性差,所以系統(tǒng)水平擴展方案復(fù)雜度高難以實現(xiàn)。
- 數(shù)據(jù)庫壓力大,每次獲取ID都需要讀寫一次數(shù)據(jù)庫,非常影響性能,不符合分布式ID里面的延遲低和要高QPS的規(guī)則(在高并發(fā)下,如果都去數(shù)據(jù)庫里面獲取Id,非常影響性能的。)
4.3 基于Redis生成全局id策略
在Redis集群情況下,同樣和MySql一樣需要設(shè)置不同的增長步長,同時key一定要設(shè)置有效期??梢允褂肦edis集群來獲取更高的吞吐量。
五、SnowFlake(雪花算法)
而Twitter的SnowFlake解決了這種需求,最初Twitter把存儲系統(tǒng)從MySQL遷移到Cassandra(由Facebook開發(fā)一套開源分布式NoSQL數(shù)據(jù)庫系統(tǒng)) 因為Cassandra沒有順序ID生成機制,所以開發(fā)了這樣一套全局唯一ID生成服務(wù)。SnowFlake每秒能產(chǎn)生26萬個自增可排序的ID。
5.1 SnowFlake特點
- Twitter的SnowFlake生成ID能夠按照時間有序生成。
- SnowFlake算法生成Id的結(jié)果是一個64bit大小的整數(shù),為一個Long型(轉(zhuǎn)換成字符串后長度最多19)。
- 分布式系統(tǒng)內(nèi)不會產(chǎn)生ID碰撞(由datacenter和workerid作為區(qū)分)并且效率較高。
5.2 SnowFlake結(jié)構(gòu)
5.3 雪花算法原理
雪花算法的原理就是生成一個的64位比特位的long類型的唯一id
- 最高1位固定值0,因為生成的id是正整數(shù),如果是1就是負值。
- 緊接著是41位存儲毫秒級時間戳,2^41/(1000 * 60 * 24 * 365) = 69 ,大概可以使用69年。
- 接下來10位存儲機器碼,包括5位DataCenterId和5位WorkerId,最多可以部署2^10=1024臺機器。
- 最后12位存儲序列號,同一毫秒時間戳?xí)r,通過這個遞增的序列號來區(qū)分,即對于同一臺機器而言,同一毫秒級時間戳下,可以生成2^12=4096個不重復(fù)id。
可以將雪花算法作為一個單獨的服務(wù)進行部署,然后需要全局唯一id的系統(tǒng),請求雪花算法服務(wù)獲取id即可。
對于每一個雪花算法服務(wù),需要先指定10位的機器碼,這個根據(jù)自身業(yè)務(wù)進行設(shè)定即可。例如機房號+機器號,機器號+服務(wù)號,或者時其他區(qū)別標識的10位比特位的整數(shù)都行。
5.4 算法實現(xiàn)
package com.goyeer; import java.util.Date; public class SnowFlakeUtil { private static SnowFlakeUtil snowFlakeUtil; static { snowFlakeUtil = new SnowFlakeUtil(); } // 初始時間戳(紀年),可用雪花算法服務(wù)上線時間戳的值 // private static final long INIT_EPOCH = 1694263918335L; // 時間位取& private static final long TIME_BIT = 0b1111111111111111111111111111111111111111110000000000000000000000L; // 記錄最后使用的毫秒時間戳,主要用于判斷是否同一毫秒,以及用于服務(wù)器時鐘回撥判斷 private long lastTimeMillis = -1L; // dataCenterId占用的位數(shù) private static final long DATA_CENTER_ID_BITS = 5L; // dataCenterId占用5個比特位,最大值31 // 0000000000000000000000000000000000000000000000000000000000011111 private static final long MAX_DATA_CENTER_ID = ~(-1L << DATA_CENTER_ID_BITS); // dataCenterId private long dataCenterId; // workId占用的位數(shù) private static final long WORKER_ID_BITS = 5L; // workId占用5個比特位,最大值31 // 0000000000000000000000000000000000000000000000000000000000011111 private static final long MAX_WORKER_ID = ~(-1L << WORKER_ID_BITS); // workId private long workerId; // 最后12位,代表每毫秒內(nèi)可產(chǎn)生最大序列號,即 2^12 - 1 = 4095 private static final long SEQUENCE_BITS = 12L; // 掩碼(最低12位為1,高位都為0),主要用于與自增后的序列號進行位與,如果值為0,則代表自增后的序列號超過了4095 // 0000000000000000000000000000000000000000000000000000111111111111 private static final long SEQUENCE_MASK = ~(-1L << SEQUENCE_BITS); // 同一毫秒內(nèi)的最新序號,最大值可為 2^12 - 1 = 4095 private long sequence; // workId位需要左移的位數(shù) 12 private static final long WORK_ID_SHIFT = SEQUENCE_BITS; // dataCenterId位需要左移的位數(shù) 12+5 private static final long DATA_CENTER_ID_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS; // 時間戳需要左移的位數(shù) 12+5+5 private static final long TIMESTAMP_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS + DATA_CENTER_ID_BITS; /** * 無參構(gòu)造 */ public SnowFlakeUtil() { this(1, 1); } /** * 有參構(gòu)造 * @param dataCenterId * @param workerId */ public SnowFlakeUtil(long dataCenterId, long workerId) { // 檢查dataCenterId的合法值 if (dataCenterId < 0 || dataCenterId > MAX_DATA_CENTER_ID) { throw new IllegalArgumentException( String.format("dataCenterId 值必須大于 0 并且小于 %d", MAX_DATA_CENTER_ID)); } // 檢查workId的合法值 if (workerId < 0 || workerId > MAX_WORKER_ID) { throw new IllegalArgumentException(String.format("workId 值必須大于 0 并且小于 %d", MAX_WORKER_ID)); } this.workerId = workerId; this.dataCenterId = dataCenterId; } /** * 獲取唯一ID * @return */ public static Long getSnowFlakeId() { return snowFlakeUtil.nextId(); } /** * 通過雪花算法生成下一個id,注意這里使用synchronized同步 * @return 唯一id */ public synchronized long nextId() { long currentTimeMillis = System.currentTimeMillis(); System.out.println(currentTimeMillis); // 當前時間小于上一次生成id使用的時間,可能出現(xiàn)服務(wù)器時鐘回撥問題 if (currentTimeMillis < lastTimeMillis) { throw new RuntimeException( String.format("可能出現(xiàn)服務(wù)器時鐘回撥問題,請檢查服務(wù)器時間。當前服務(wù)器時間戳:%d,上一次使用時間戳:%d", currentTimeMillis, lastTimeMillis)); } if (currentTimeMillis == lastTimeMillis) { // 還是在同一毫秒內(nèi),則將序列號遞增1,序列號最大值為4095 // 序列號的最大值是4095,使用掩碼(最低12位為1,高位都為0)進行位與運行后如果值為0,則自增后的序列號超過了4095 // 那么就使用新的時間戳 sequence = (sequence + 1) & SEQUENCE_MASK; if (sequence == 0) { currentTimeMillis = getNextMillis(lastTimeMillis); } } else { // 不在同一毫秒內(nèi),則序列號重新從0開始,序列號最大值為4095 sequence = 0; } // 記錄最后一次使用的毫秒時間戳 lastTimeMillis = currentTimeMillis; // 核心算法,將不同部分的數(shù)值移動到指定的位置,然后進行或運行 // <<:左移運算符, 1 << 2 即將二進制的 1 擴大 2^2 倍 // |:位或運算符, 是把某兩個數(shù)中, 只要其中一個的某一位為1, 則結(jié)果的該位就為1 // 優(yōu)先級:<< > | return // 時間戳部分 ((currentTimeMillis - INIT_EPOCH) << TIMESTAMP_SHIFT) // 數(shù)據(jù)中心部分 | (dataCenterId << DATA_CENTER_ID_SHIFT) // 機器表示部分 | (workerId << WORK_ID_SHIFT) // 序列號部分 | sequence; } /** * 獲取指定時間戳的接下來的時間戳,也可以說是下一毫秒 * @param lastTimeMillis 指定毫秒時間戳 * @return 時間戳 */ private long getNextMillis(long lastTimeMillis) { long currentTimeMillis = System.currentTimeMillis(); while (currentTimeMillis <= lastTimeMillis) { currentTimeMillis = System.currentTimeMillis(); } return currentTimeMillis; } /** * 獲取隨機字符串,length=13 * @return */ public static String getRandomStr() { return Long.toString(getSnowFlakeId()); } /** * 從ID中獲取時間 * @param id 由此類生成的ID * @return */ public static Date getTimeBySnowFlakeId(long id) { return new Date(((TIME_BIT & id) >> 22) + INIT_EPOCH); } public static void main(String[] args) { SnowFlakeUtil snowFlakeUtil = new SnowFlakeUtil(); long id = snowFlakeUtil.nextId(); System.out.println(id); Date date = SnowFlakeUtil.getTimeBySnowFlakeId(id); System.out.println(date); long time = date.getTime(); System.out.println(time); System.out.println(getRandomStr()); } }
5.5 雪花算法優(yōu)點
- 高并發(fā)分布式環(huán)境下生成不重復(fù) id,每秒可生成百萬個不重復(fù) id。
- 基于時間戳,以及同一時間戳下序列號自增,基本保證 id 有序遞增。
- 不依賴第三方庫或者中間件。
- 算法簡單,在內(nèi)存中進行,效率高。
5.6 雪花算法缺點
依賴服務(wù)器時間,服務(wù)器時鐘回撥時可能會生成重復(fù) id。算法中可通過記錄最后一個生成 id 時的時間戳來解決,每次生成 id 之前比較當前服務(wù)器時鐘是否被回撥,避免生成重復(fù) id。
六、總結(jié)
其實雪花算法每一部分占用的比特位數(shù)量并不是固定死的。例如你的業(yè)務(wù)可能達不到 69 年之久,那么可用減少時間戳占用的位數(shù),雪花算法服務(wù)需要部署的節(jié)點超過1024 臺,那么可將減少的位數(shù)補充給機器碼用。
注意,雪花算法中 41 位比特位不是直接用來存儲當前服務(wù)器毫秒時間戳的,而是需要當前服務(wù)器時間戳減去某一個初始時間戳值,一般可以使用服務(wù)上線時間作為初始時間戳值。
對于機器碼,可根據(jù)自身情況做調(diào)整,例如機房號,服務(wù)器號,業(yè)務(wù)號,機器 IP 等都是可使用的。對于部署的不同雪花算法服務(wù)中,最后計算出來的機器碼能區(qū)分開來即可。
以上就是Java雪花算法的原理和實現(xiàn)方法的詳細內(nèi)容,更多關(guān)于Java雪花算法的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
springboot整合mybatis中的問題及出現(xiàn)的一些問題小結(jié)
這篇文章主要介紹了springboot整合mybatis中的問題及出現(xiàn)的一些問題小結(jié),本文給出了解決方案,需要的朋友可以參考下2018-11-11SpringBoot中的@RequestMapping注解的用法示例
@RequestMapping注解是SpringBoot中最常用的注解之一,它可以幫助開發(fā)者定義和處理HTTP請求,本篇文章我們將詳細為大家介紹如何使用SpringBoot中的@RequestMapping注解,感興趣的同學(xué)跟著小編一起來學(xué)習(xí)吧2023-06-06Java 處理超大數(shù)類型之BigInteger案例詳解
這篇文章主要介紹了Java 處理超大數(shù)類型之BigInteger案例詳解,本篇文章通過簡要的案例,講解了該項技術(shù)的了解與使用,以下就是詳細內(nèi)容,需要的朋友可以參考下2021-09-09java 獲取HttpRequest Header的幾種方法(必看篇)
下面小編就為大家?guī)硪黄猨ava 獲取HttpRequest Header的幾種方法(必看篇)。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2016-09-09