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