雪花算法(SnowFlake)
簡(jiǎn)介
現(xiàn)在的服務(wù)基本是分布式、微服務(wù)形式的,而且大數(shù)據(jù)量也導(dǎo)致分庫(kù)分表的產(chǎn)生,對(duì)于水平分表就需要保證表中 id 的全局唯一性。
對(duì)于 MySQL 而言,一個(gè)表中的主鍵 id 一般使用自增的方式,但是如果進(jìn)行水平分表之后,多個(gè)表中會(huì)生成重復(fù)的 id 值。那么如何保證水平分表后的多張表中的 id 是全局唯一性的呢?
如果還是借助數(shù)據(jù)庫(kù)主鍵自增的形式,那么可以讓不同表初始化一個(gè)不同的初始值,然后按指定的步長(zhǎng)進(jìn)行自增。例如有3張拆分表,初始主鍵值為1,2,3,自增步長(zhǎng)為3。
當(dāng)然也有人使用 UUID 來(lái)作為主鍵,但是 UUID 生成的是一個(gè)無(wú)序的字符串,對(duì)于 MySQL 推薦使用增長(zhǎng)的數(shù)值類(lèi)型值作為主鍵來(lái)說(shuō)不適合。
也可以使用 Redis 的自增原子性來(lái)生成唯一 id,但是這種方式業(yè)內(nèi)比較少用。
當(dāng)然還有其他解決方案,不同互聯(lián)網(wǎng)公司也有自己內(nèi)部的實(shí)現(xiàn)方案。雪花算法是其中一個(gè)用于解決分布式 id 的高效方案,也是許多互聯(lián)網(wǎng)公司在推薦使用的。
SnowFlake 雪花算法
SnowFlake 中文意思為雪花,故稱(chēng)為雪花算法。最早是 Twitter 公司在其內(nèi)部用于分布式環(huán)境下生成唯一 ID。在2014年開(kāi)源 scala 語(yǔ)言版本。
雪花算法的原理就是生成一個(gè)的 64 位比特位的 long 類(lèi)型的唯一 id。
- 最高 1 位固定值 0,因?yàn)樯傻?id 是正整數(shù),如果是 1 就是負(fù)數(shù)了。
- 接下來(lái) 41 位存儲(chǔ)毫秒級(jí)時(shí)間戳,2^41/(1000*60*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,通過(guò)這個(gè)遞增的序列號(hào)來(lái)區(qū)分。即對(duì)于同一臺(tái)機(jī)器而言,同一毫秒時(shí)間戳下,可以生成 2^12=4096 個(gè)不重復(fù) id。
可以將雪花算法作為一個(gè)單獨(dú)的服務(wù)進(jìn)行部署,然后需要全局唯一 id 的系統(tǒng),請(qǐng)求雪花算法服務(wù)獲取 id 即可。
對(duì)于每一個(gè)雪花算法服務(wù),需要先指定 10 位的機(jī)器碼,這個(gè)根據(jù)自身業(yè)務(wù)進(jìn)行設(shè)定即可。例如機(jī)房號(hào)+機(jī)器號(hào),機(jī)器號(hào)+服務(wù)號(hào),或者是其他可區(qū)別標(biāo)識(shí)的 10 位比特位的整數(shù)值都行。
算法實(shí)現(xiàn)
package util;import java.util.Date;/*** @ClassName: SnowFlakeUtil* @Author: jiaoxian* @Date: 2022/4/24 16:34* @Description:*/ public class SnowFlakeUtil {private static SnowFlakeUtil snowFlakeUtil;static {snowFlakeUtil = new SnowFlakeUtil();}// 初始時(shí)間戳(紀(jì)年),可用雪花算法服務(wù)上線(xiàn)時(shí)間戳的值// 1650789964886:2022-04-24 16:45:59private static final long INIT_EPOCH = 1650789964886L;// 時(shí)間位取&private static final long TIME_BIT = 0b1111111111111111111111111111111111111111110000000000000000000000L;// 記錄最后使用的毫秒時(shí)間戳,主要用于判斷是否同一毫秒,以及用于服務(wù)器時(shí)鐘回?fù)芘袛鄍rivate long lastTimeMillis = -1L;// dataCenterId占用的位數(shù)private static final long DATA_CENTER_ID_BITS = 5L;// dataCenterId占用5個(gè)比特位,最大值31// 0000000000000000000000000000000000000000000000000000000000011111private static final long MAX_DATA_CENTER_ID = ~(-1L << DATA_CENTER_ID_BITS);// dataCenterIdprivate long dataCenterId;// workId占用的位數(shù)private static final long WORKER_ID_BITS = 5L;// workId占用5個(gè)比特位,最大值31// 0000000000000000000000000000000000000000000000000000000000011111private static final long MAX_WORKER_ID = ~(-1L << WORKER_ID_BITS);// workIdprivate long workerId;// 最后12位,代表每毫秒內(nèi)可產(chǎn)生最大序列號(hào),即 2^12 - 1 = 4095private static final long SEQUENCE_BITS = 12L;// 掩碼(最低12位為1,高位都為0),主要用于與自增后的序列號(hào)進(jìn)行位與,如果值為0,則代表自增后的序列號(hào)超過(guò)了4095// 0000000000000000000000000000000000000000000000000000111111111111private static final long SEQUENCE_MASK = ~(-1L << SEQUENCE_BITS);// 同一毫秒內(nèi)的最新序號(hào),最大值可為 2^12 - 1 = 4095private long sequence;// workId位需要左移的位數(shù) 12private static final long WORK_ID_SHIFT = SEQUENCE_BITS;// dataCenterId位需要左移的位數(shù) 12+5private static final long DATA_CENTER_ID_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS;// 時(shí)間戳需要左移的位數(shù) 12+5+5private static final long TIMESTAMP_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS + DATA_CENTER_ID_BITS;/*** 無(wú)參構(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();}/*** 通過(guò)雪花算法生成下一個(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ù)軉?wèn)題if (currentTimeMillis < lastTimeMillis) {throw new RuntimeException(String.format("可能出現(xiàn)服務(wù)器時(shí)鐘回?fù)軉?wèn)題,請(qǐng)檢查服務(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)超過(guò)了4095// 那么就使用新的時(shí)間戳sequence = (sequence + 1) & SEQUENCE_MASK;if (sequence == 0) {currentTimeMillis = getNextMillis(lastTimeMillis);}} else { // 不在同一毫秒內(nèi),則序列號(hào)重新從0開(kāi)始,序列號(hào)最大值為4095sequence = 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)先級(jí):<< > |return// 時(shí)間戳部分((currentTimeMillis - INIT_EPOCH) << TIMESTAMP_SHIFT)// 數(shù)據(jù)中心部分| (dataCenterId << DATA_CENTER_ID_SHIFT)// 機(jī)器表示部分| (workerId << WORK_ID_SHIFT)// 序列號(hào)部分| sequence;}/*** 獲取指定時(shí)間戳的接下來(lái)的時(shí)間戳,也可以說(shuō)是下一毫秒* @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(), Character.MAX_RADIX);}/*** 從ID中獲取時(shí)間* @param id 由此類(lèi)生成的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());}}算法優(yōu)缺點(diǎn)
雪花算法有以下幾個(gè)優(yōu)點(diǎn):
- 高并發(fā)分布式環(huán)境下生成不重復(fù) id,每秒可生成百萬(wàn)個(gè)不重復(fù) id。
- 基于時(shí)間戳,以及同一時(shí)間戳下序列號(hào)自增,基本保證 id 有序遞增。
- 不依賴(lài)第三方庫(kù)或者中間件。
- 算法簡(jiǎn)單,在內(nèi)存中進(jìn)行,效率高。
雪花算法有如下缺點(diǎn):
- 依賴(lài)服務(wù)器時(shí)間,服務(wù)器時(shí)鐘回?fù)軙r(shí)可能會(huì)生成重復(fù) id。算法中可通過(guò)記錄最后一個(gè)生成 id 時(shí)的時(shí)間戳來(lái)解決,每次生成 id 之前比較當(dāng)前服務(wù)器時(shí)鐘是否被回?fù)?#xff0c;避免生成重復(fù) id。
注意事項(xiàng)
其實(shí)雪花算法每一部分占用的比特位數(shù)量并不是固定死的。例如你的業(yè)務(wù)可能達(dá)不到 69 年之久,那么可用減少時(shí)間戳占用的位數(shù),雪花算法服務(wù)需要部署的節(jié)點(diǎn)超過(guò)1024 臺(tái),那么可將減少的位數(shù)補(bǔ)充給機(jī)器碼用。
注意,雪花算法中 41 位比特位不是直接用來(lái)存儲(chǔ)當(dāng)前服務(wù)器毫秒時(shí)間戳的,而是需要當(dāng)前服務(wù)器時(shí)間戳減去某一個(gè)初始時(shí)間戳值,一般可以使用服務(wù)上線(xiàn)時(shí)間作為初始時(shí)間戳值。
對(duì)于機(jī)器碼,可根據(jù)自身情況做調(diào)整,例如機(jī)房號(hào),服務(wù)器號(hào),業(yè)務(wù)號(hào),機(jī)器 IP 等都是可使用的。對(duì)于部署的不同雪花算法服務(wù)中,最后計(jì)算出來(lái)的機(jī)器碼能區(qū)分開(kāi)來(lái)即可。
本文參考自:SnowFlake 雪花算法詳解與實(shí)現(xiàn) - 掘
總結(jié)
以上是生活随笔為你收集整理的雪花算法(SnowFlake)的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: mysql的三表查询语句_求三表联合查询
- 下一篇: Big Sur MacOS高清动态壁纸