武漢自適應(yīng)網(wǎng)站沈陽線上教學(xué)
在互聯(lián)網(wǎng)的業(yè)務(wù)系統(tǒng)中,涉及到各種各樣的ID,如在支付系統(tǒng)中就會有支付ID、退款I(lǐng)D等。那一般生成ID都有哪些解決方案呢?特別是在復(fù)雜的分布式系統(tǒng)業(yè)務(wù)場景中,我們應(yīng)該采用哪種適合自己的解決方案是十分重要的。下面我們一一來列舉一下,不一定全部適合,這些解決方案僅供參考,或許對你有用。
一、分布式ID
1.什么是分布式ID
日常開發(fā)中,我們需要對系統(tǒng)中的各種數(shù)據(jù)使用 ID 唯一表示,比如 用戶 ID 對應(yīng)且僅對應(yīng)一個人,商品 ID 對應(yīng)且僅對應(yīng)一件商品,訂 單 ID 對應(yīng)且僅對應(yīng)一個訂單。
拿MySQL數(shù)據(jù)庫舉個例子:
在我們業(yè)務(wù)數(shù)據(jù)量不大的時候,單庫單表完全可以支撐現(xiàn)有業(yè)務(wù),數(shù)據(jù)再大一點搞個MySQL主從同步讀寫分離也能對付。
但隨著數(shù)據(jù)日漸增長,主從同步也扛不住了,就需要對數(shù)據(jù)庫進行分庫分表,但分庫分表后需要有一個唯一ID來標(biāo)識一條數(shù)據(jù),數(shù)據(jù)庫的自增ID顯然不能滿足需求;特別一點的如訂單、優(yōu)惠券也都需要有唯一ID做標(biāo)識。此時一個能夠生成全局唯一ID的系統(tǒng)是非常必要的。那么這個全局唯一ID就叫分布式ID。
2.分布式ID的特性
- 唯一性:確保生成的ID是全網(wǎng)唯一的。
- 有序遞增性:確保生成的ID是對于某個用戶或者業(yè)務(wù)是按一定的數(shù)字有序遞增的。
- 高可用性:確保任何時候都能正確的生成ID。
- 帶時間:ID里面包含時間,一眼掃過去就知道哪天的交易。
二、分布式ID的生成方案
1. UUID
算法的核心思想是結(jié)合機器的網(wǎng)卡、當(dāng)?shù)貢r間、一個隨記數(shù)來生成UUID。
優(yōu)點:
- 代碼實現(xiàn)簡單
- 本地生成,沒有性能問題,沒有高可用風(fēng)險
- 全球唯一的,數(shù)據(jù)遷移容易
缺點:
- 長度過長,存儲冗余,且無序不可讀,查詢效率低
- 每次生成的ID是無序的,不滿足趨勢遞增
- UUID是字符串,而且比較長,占用空間大,查詢效率低
- ID沒有含義,可讀性差
2. 數(shù)據(jù)庫自增ID
使用數(shù)據(jù)庫的id自增策略,如 MySQL 的 auto_increment。并且可以使用兩臺數(shù)據(jù)庫分別設(shè)置不同步長,生成不重復(fù)ID的策略來實現(xiàn)高可用。
- 優(yōu)點:數(shù)據(jù)庫生成的ID絕對有序,高可用實現(xiàn)方式簡單
- 缺點:需要獨立部署數(shù)據(jù)庫實例,成本高,有性能瓶頸
3. 批量生成ID
一次按需批量生成多個ID,每次生成都需要訪問數(shù)據(jù)庫,將數(shù)據(jù)庫修改為最大的ID值,并在內(nèi)存中記錄當(dāng)前值及最大值。
- 優(yōu)點:避免了每次生成ID都要訪問數(shù)據(jù)庫并帶來壓力,提高性能
- 缺點:屬于本地生成策略,存在單點故障,服務(wù)重啟造成ID不連續(xù)
4. Redis生成ID
Redis的所有命令操作都是單線程的,本身提供像 incr 和 increby 這樣的自增原子命令,所以能保證生成的 ID 肯定是唯一有序的。
- 優(yōu)點:有序遞增,可讀性強,性能較高。不依賴于數(shù)據(jù)庫,靈活方便,且性能優(yōu)于數(shù)據(jù)庫;數(shù)字ID天然排序,對分頁或者需要排序的結(jié)果很有幫助。
- 缺點:占用帶寬,依賴Redis:如果系統(tǒng)中沒有Redis,還需要引入新的組件,增加系統(tǒng)復(fù)雜度;需要編碼和配置的工作量比較大。
考慮到單節(jié)點的性能瓶頸,可以使用 Redis 集群來獲取更高的吞吐量。假如一個集群中有5臺 Redis??梢猿跏蓟颗_ Redis 的值分別是1, 2, 3, 4, 5,然后步長都是 5。各個 Redis 生成的 ID 為:
A:1, 6, 11, 16, 21
B:2, 7, 12, 17, 22
C:3, 8, 13, 18, 23
D:4, 9, 14, 19, 24
E:5, 10, 15, 20, 25
隨便負(fù)載到哪個機確定好,未來很難做修改。步長和初始值一定需要事先確定。使用 Redis 集群也可以防止單點故障的問題。另外,比較適合使用 Redis 來生成每天從0開始的流水號。比如訂單號 = 日期 + 當(dāng)日自增長號??梢悦刻煸?Redis 中生成一個 Key ,使用 INCR 進行累加。
5. Twitter的snowflake算法
Twitter開源的snowflake,以?時間戳+機器+遞增序列?組成,基本趨勢遞增,且性能很高。
snowflake生成的是一個Long類型的值,Long類型的數(shù)據(jù)占用8個 字節(jié),也就是64位。SnowFlake將64進行拆分,每個部分具有不同 的含義,當(dāng)然機器碼、序列號的位數(shù)可以自定義也可以。
把64-bit分別劃分成多段,分開來標(biāo)示機器、時間等,比如在snowflake中的64-bit分別表示如下圖(圖片來自網(wǎng)絡(luò))所示:
- 符號位 (1bit):預(yù)留的符號位,恒為零。(由于 long 類型在 java 中帶符號的,最高位為符號位,正數(shù)為 0,負(fù)數(shù)為 1,且實際系統(tǒng)中所使用的ID一般都是正數(shù),所以最高位為 0)
- 時間戳位 (41bit):41 位的時間戳可以容納的毫秒數(shù)是 2 的 41 次冪,一年所使用的毫秒數(shù)是:365 * 24 * 60 * 60 * 1000。通過計算可知:
Math.pow(2, 41) / (365 * 24 * 60 * 60 * 1000L);
結(jié)果約等于 69.73 年
ShardingSphere 的雪花算法的時間紀(jì)元從 2016 年 11 月 1 日零點開始,可以使用到 2086 年,相信能滿足絕大部分系統(tǒng)的要求。
- 工作進程位 (10bit):該標(biāo)志在 Java 進程內(nèi)是唯一的,如果是分布式應(yīng)用部署應(yīng)保證每個工作進程的 id 是不同的。該值默認(rèn)為 0,可通過屬性設(shè)置。10-bit機器可以分別表示1024臺機器。如果我們對IDC劃分有需求,還可以將10-bit分5-bit給IDC,分5-bit給工作機器。這樣就可以表示32個IDC,每個IDC下可以有32臺機器,可以根據(jù)自身需求定義。
- 序列號位 (12bit):該序列是用來在同一個毫秒內(nèi)生成不同的 ID。如果在這個毫秒內(nèi)生成的數(shù)量超過 4096 (2 的 12 次冪),那么生成器會等待到下個毫秒繼續(xù)生成。這 12 位計數(shù)支持每個節(jié)點每毫秒(同一臺機器,同一時刻)最多生成 1 << 12 = 4096個ID
優(yōu)點:本地生成,不依賴中間件。 生成的分布式id足夠小,只有8個字節(jié),而且是遞增的
- 能滿足高并發(fā)分布式系統(tǒng)環(huán)境下ID不重復(fù)
- 生成效率高
- 基于時間戳,可以保證基本有序遞增
- 不依賴于第三方的庫或者中間件
- 生成的
id
具有時序性和唯一性缺點:時鐘回?fù)軉栴},強烈依賴于服務(wù)器的時間,如果時間出現(xiàn)時間回?fù)?就可能出現(xiàn)重復(fù)的id
6. 百度UidGenerator
具體可以參考官網(wǎng)說明:https://github.com/baidu/uid-generator/blob/master/README.zh_cn.md
7. 美團Leaf
Leaf 是美團開源的分布式ID生成器,能保證全局唯一性、趨勢遞增、單調(diào)遞增、信息安全,里面也提到了幾種分布式方案的對比,但也需要依賴關(guān)系數(shù)據(jù)庫、ZooKeeper等中間件。
https://github.com/Meituan-Dianping/Leaf/tree/master/leaf-core
具體可以參考官網(wǎng)說明:https://tech.meituan.com/2017/04/21/mt-leaf.html
8.滴滴(Tinyid)
Tinyid由滴滴開發(fā),Github地址:https://github.com/didi/tinyid。
Tinyid是基于號段模式原理實現(xiàn)的與Leaf如出一轍,每個服務(wù)獲取一個號段(1000,2000]、(2000,3000]、(3000,4000]
說了這么多,我們今天就主要說一下?Twitter開源的snowflake
三、snowflake
1.流程
2.java 代碼實現(xiàn)
?如下示例,41bit給時間戳,5bit給IDC,5bit給工作機器,12bit給序列號,代碼中是寫死的,如果某些bit需要動態(tài)調(diào)整,可在成員屬性定義。計算過程需要一些位運算基礎(chǔ)。
import java.util.Date;/*** @Author allen* @Description TODO* @Date 2023-07-26 9:51* @Version 1.0*/
public class SnowFlakeUtil {private static SnowFlakeUtil snowFlakeUtil;static {snowFlakeUtil = new SnowFlakeUtil();}// 初始時間戳(紀(jì)年),可用雪花算法服務(wù)上線時間戳的值// 1650789964886:2022-04-24 16:45:59private static final long INIT_EPOCH = 1650789964886L;// 時間位取&private static final long TIME_BIT = 0b1111111111111111111111111111111111111111110000000000000000000000L;// 記錄最后使用的毫秒時間戳,主要用于判斷是否同一毫秒,以及用于服務(wù)器時鐘回?fù)芘袛鄍rivate long lastTimeMillis = -1L;// dataCenterId占用的位數(shù)private static final long DATA_CENTER_ID_BITS = 5L;// dataCenterId占用5個比特位,最大值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個比特位,最大值31// 0000000000000000000000000000000000000000000000000000000000011111private static final long MAX_WORKER_ID = ~(-1L << WORKER_ID_BITS);// workIdprivate long workerId;// 最后12位,代表每毫秒內(nèi)可產(chǎn)生最大序列號,即 2^12 - 1 = 4095private static final long SEQUENCE_BITS = 12L;// 掩碼(最低12位為1,高位都為0),主要用于與自增后的序列號進行位與,如果值為0,則代表自增后的序列號超過了4095// 0000000000000000000000000000000000000000000000000000111111111111private static final long SEQUENCE_MASK = ~(-1L << SEQUENCE_BITS);// 同一毫秒內(nèi)的最新序號,最大值可為 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ù) 12+5+5private 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);// 當(dāng)前時間小于上一次生成id使用的時間,可能出現(xiàn)服務(wù)器時鐘回?fù)軉栴}if (currentTimeMillis < lastTimeMillis) {throw new RuntimeException(String.format("可能出現(xiàn)服務(wù)器時鐘回?fù)軉栴},請檢查服務(wù)器時間。當(dāng)前服務(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開始,序列號最大值為4095sequence = 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(), Character.MAX_RADIX);}/*** 從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());/* System.out.println("============================");long startTime = System.currentTimeMillis();for (int i = 0; i < 10000000; i++) {long id2 = snowFlakeUtil.nextId();System.out.println(id2);}System.out.println(System.currentTimeMillis() - startTime);*/}}
主要就這個: long id = snowFlakeUtil.nextId();
?時鐘同步問題解決方案:
雪花算法-Java實現(xiàn)-解決時鐘回?fù)艿囊环N方法_雪花算法時鐘回?fù)躝fierys的博客-CSDN博客
這個代碼是借鑒另外一位博主的方案,可以借鑒一下,僅供參考
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;/**相較于標(biāo)準(zhǔn)算法,加入了時鐘回?fù)芙鉀Q方法,僅單機研究,僅個人思考,僅供參考*/
public class SnowFlow {//因為二進制里第一個 bit 為如果是 1,那么都是負(fù)數(shù),但是我們生成的 id 都是正數(shù),所以第一個 bit 統(tǒng)一都是 0。//機器ID 2進制5位 32位減掉1位 31個private long workerId;//機房ID 2進制5位 32位減掉1位 31個private long datacenterId;//代表一毫秒內(nèi)生成的多個id的最新序號 12位 4096 -1 = 4095 個private long sequence;//設(shè)置一個時間初始值 2^41 - 1 差不多可以用69年private long twepoch = 1420041600000L;//5位的機器idprivate long workerIdBits = 5L;//5位的機房id;?!畃rivate long datacenterIdBits = 5L;//每毫秒內(nèi)產(chǎn)生的id數(shù) 2 的 12次方private long sequenceBits = 12L;// 這個是二進制運算,就是5 bit最多只能有31個數(shù)字,也就是說機器id最多只能是32以內(nèi)private long maxWorkerId = -1L ^ (-1L << workerIdBits);// 這個是一個意思,就是5 bit最多只能有31個數(shù)字,機房id最多只能是32以內(nèi)private long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);private long workerIdShift = sequenceBits;private long datacenterIdShift = sequenceBits + workerIdBits;private long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;// -1L 二進制就是1111 1111 為什么?// -1 左移12位就是 1111 1111 0000 0000 0000 0000// 異或 相同為0 ,不同為1// 1111 1111 0000 0000 0000 0000// ^// 1111 1111 1111 1111 1111 1111// 0000 0000 1111 1111 1111 1111 換算成10進制就是4095private long sequenceMask = -1L ^ (-1L << sequenceBits);//記錄產(chǎn)生時間毫秒數(shù),判斷是否是同1毫秒private long lastTimestamp = -1L;public long getWorkerId(){return workerId;}public long getDatacenterId() {return datacenterId;}public long getTimestamp() {return System.currentTimeMillis();}//是否發(fā)生了時鐘回?fù)躳rivate boolean isBackwordsFlag = false;//是否是第一次發(fā)生時鐘回?fù)? 用于設(shè)置時鐘回?fù)軙r間點private boolean isFirstBackwordsFlag = true;//記錄時鐘回?fù)馨l(fā)生時間點, 用于判斷回?fù)芎蟮臅r間達到回?fù)軙r間點時, 跳過 已經(jīng)用過的 時鐘回?fù)馨l(fā)生時間點 之后的時間 到 未來時間的當(dāng)前時間點private long backBaseTimestamp = -1L;public SnowFlow() {}public SnowFlow(long workerId, long datacenterId, long sequence) {// 檢查機房id和機器id是否超過31 不能小于0if (workerId > maxWorkerId || workerId < 0) {throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0",maxWorkerId));}if (datacenterId > maxDatacenterId || datacenterId < 0) {throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0",maxDatacenterId));}this.workerId = workerId;this.datacenterId = datacenterId;this.sequence = sequence;}// 這個是核心方法,通過調(diào)用nextId()方法,// 讓當(dāng)前這臺機器上的snowflake算法程序生成一個全局唯一的idpublic synchronized long nextId() {// 這兒就是獲取當(dāng)前時間戳,單位是毫秒long timestamp = timeGen();//--20220813--1---------------------------------------if (isBackwordsFlag) {//當(dāng)回?fù)軙r間再次叨叨回?fù)軙r間點時, 跳過回?fù)苓@段時間里已經(jīng)使用了的未來時間if (timestamp >= backBaseTimestamp && timestamp < lastTimestamp) {//直接將當(dāng)前時間設(shè)置為最新的未來時間timestamp = lastTimestamp;} else if(timestamp > lastTimestamp) {//當(dāng)前時間已經(jīng)大于上次時間, 重置時鐘回?fù)軜?biāo)志isBackwordsFlag = false;isFirstBackwordsFlag = true;System.out.println("時間已恢復(fù)正常-->" + timestamp);} else {// timestamp == lastTimestamp 等于的情況在后面}}//--20220813--1----------------------------------------// 判斷是否小于上次時間戳,如果小于的話,就拋出異常if (timestamp < lastTimestamp) {System.err.printf("lastTimestamp=%d, timestamp=%d, l-t=%d \n", lastTimestamp, timestamp, lastTimestamp - timestamp);
// throw new RuntimeException(
// String.format("Clock moved backwards. Refusing to generate id for %d milliseconds",
// lastTimestamp - timestamp));//--20220813--2---------------------------------------//這里不再拋出異常, 改為記錄時鐘回?fù)馨l(fā)生時間點//發(fā)生時鐘回?fù)芎? 當(dāng)前時間 timestamp 就變成了 過去的時間//此時將 timestamp 設(shè)置為 上一次時間, 即相對于當(dāng)前時間的未來時間timestamp = lastTimestamp;isBackwordsFlag = true;//記錄時鐘回?fù)馨l(fā)生的時間點, 后續(xù)需要跳過已經(jīng)使用的未來時間段if (isFirstBackwordsFlag) {backBaseTimestamp = timestamp;isFirstBackwordsFlag = false;System.out.println("時鐘回?fù)芤寻l(fā)生-->" + backBaseTimestamp);}//--20220813--2---------------------------------------}// 下面是說假設(shè)在同一個毫秒內(nèi),又發(fā)送了一個請求生成一個id// 這個時候就得把seqence序號給遞增1,最多就是4096if (timestamp == lastTimestamp) {// 這個意思是說一個毫秒內(nèi)最多只能有4096個數(shù)字,無論你傳遞多少進來,//這個位運算保證始終就是在4096這個范圍內(nèi),避免你自己傳遞個sequence超過了4096這個范圍sequence = (sequence + 1) & sequenceMask;//當(dāng)某一毫秒的時間,產(chǎn)生的id數(shù) 超過4095,系統(tǒng)會進入等待,直到下一毫秒,系統(tǒng)繼續(xù)產(chǎn)生IDif (sequence == 0) {//timestamp = tilNextMillis(lastTimestamp);//--20220813--3---------------------------------------//這里也不能阻塞了, 因為阻塞方法中需要用到當(dāng)前時間, 改為將此時代表未來的時間 加 1if (isBackwordsFlag) {lastTimestamp++;//根據(jù)博友評論反饋, 這里可能需要重新賦值, 如果有人看到這個, 可以驗證//timestamp = lastTimestamp++;} else {timestamp = tilNextMillis(lastTimestamp);}//--20220813--3---------------------------------------}} else {//sequence = 0;//每毫秒的序列號都從0開始的話,會導(dǎo)致沒有競爭情況返回的都是偶數(shù)。解決方法是用時間戳&1,這樣就會隨機得到1或者0。sequence = timestamp & 1;}// 這兒記錄一下最近一次生成id的時間戳,單位是毫秒//lastTimestamp = timestamp;//--20220813--4---------------------------------------if(isBackwordsFlag) {//什么都不做} else {lastTimestamp = timestamp;}//--20220813--4---------------------------------------// 這兒就是最核心的二進制位運算操作,生成一個64bit的id// 先將當(dāng)前時間戳左移,放到41 bit那兒;將機房id左移放到5 bit那兒;將機器id左移放到5 bit那兒;將序號放最后12 bit// 最后拼接起來成一個64 bit的二進制數(shù)字,轉(zhuǎn)換成10進制就是個long型long sn = ((timestamp - twepoch) << timestampLeftShift) |(datacenterId << datacenterIdShift) |(workerId << workerIdShift) | sequence;if (isBackwordsFlag) {System.out.printf("sn=%d\n", sn);}return sn;}/*** 當(dāng)某一毫秒的時間,產(chǎn)生的id數(shù) 超過4095,系統(tǒng)會進入等待,直到下一毫秒,系統(tǒng)繼續(xù)產(chǎn)生ID* @param lastTimestamp* @return*/private long tilNextMillis(long lastTimestamp) {long timestamp = timeGen();while (timestamp <= lastTimestamp) {timestamp = timeGen();}return timestamp;}//獲取當(dāng)前時間戳private long timeGen(){return System.currentTimeMillis();}/*** main 測試類* @param args*/public static void main(String[] args) throws IOException, InterruptedException {SnowFlow snowFlow = new SnowFlow(1, 1, 1);int count = 10000000;//int count = 100;for (int i = 0; i < count; i++) {//實際測試發(fā)現(xiàn)遍歷太快輸出日志過多導(dǎo)致卡頓, 增加睡眠時間, 或輸出到文件snowFlow.nextId();//Thread.sleep(100);// System.out.println(snowFlow.nextId());// if (i == 1000) {//不具有管理員權(quán)限的用戶, 修改不成功//testClockMvoedBackwords(30);
// }//改為 手動修改, 右鍵cmd,以管理員權(quán)限打開后,使用time命令即可, time 16:15:00}System.out.println(System.currentTimeMillis());/*** 這里為什么特意輸出一個開始時間呢, 其實就是一個運行了兩年的程序突然有一天出bug了,導(dǎo)致了嚴(yán)重的生產(chǎn)事件!* 那么時間初始化影響什么呢, 答案是 序列的長度* 有人就說了, 這個一般是作為 主鍵用的, 長度貌似影響不大, 確實是這樣的* 這次的bug不是雪花算法本身的問題, 而是程序里面有個功能是嚴(yán)格長度截取的, 并且只考慮了長度不夠的情況, 沒有考慮到變長的情況* 最根本的原因是 本人截取的時候 序列的長度一直是18位, 然后截取9位的代碼是這么寫的 substring(9);* 當(dāng)未來的某一天序列長度增加到了19位,那么這個截取就會返回10位長度, 最終導(dǎo)致一個大范圍的交易失敗......* 鍋當(dāng)然是本人背, 這里提出這種情況, 供大家參考.* 經(jīng)過仔細(xì)研究所謂的序列可以使用69年, 序列的長度變化是這樣的, 假設(shè)以當(dāng)前時間為初始化值* 12 13 14 15 16 17 18(約7年) 19(約58年)* 長度隨時間增加, 長度越長, 保持相同長度的時間越長*/DateTimeFormatter dtf2 = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");String dateString = "2015-01-01 00:00:00";LocalDateTime localDateTime = LocalDateTime.parse(dateString,dtf2);System.out.println(localDateTime.toInstant(ZoneOffset.ofHours(8)).toEpochMilli());}//windows os 模擬時鐘回?fù)? 將當(dāng)前時間減去幾秒private static void testClockMvoedBackwords(long seconds) throws IOException {System.out.println(LocalDateTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss")));LocalDateTime localDateTime = LocalDateTime.now();String backTime = localDateTime.minusSeconds(seconds).format(DateTimeFormatter.ofPattern("HH:mm:ss"));System.out.println(backTime);if (System.getProperty("os.name").contains("Windows")) {String cmd = "cmd /c start time 15:41:56";// + backTime;//不具有管理員權(quán)限的用戶, 修改不生效, 提示 客戶端無所需特權(quán)Runtime.getRuntime().exec(cmd);
// Runtime.getRuntime().exec("cmd /c notepad");System.out.println(LocalDateTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss")));}}
}
3.Snowflake生產(chǎn)方案 時鐘回?fù)軉栴}解決思路
第一種辦法,就是關(guān)閉時鐘同步,避免產(chǎn)生時鐘同步問題,不過這個不太現(xiàn)實,因為強依賴時間的系統(tǒng),一般都得做時鐘同步,避免時間嚴(yán)重錯誤,在虛擬機上部署一些東西,玩兒虛擬機,休眠,再次恢復(fù)之后往往虛擬機里的時間和宿主機的時間是不同步的導(dǎo)致一些大數(shù)據(jù)的分布式系統(tǒng)會崩潰掉,節(jié)點之間通信會依賴時間戳進行比對,心跳過慢,就會導(dǎo)致節(jié)點掛掉
第二種辦法,記錄下來上一次生成ID的時間,如果發(fā)現(xiàn)本次生成ID的時候,時間戳小于上次的時間戳,說明時鐘回?fù)芰?#xff0c;此時就這個時間內(nèi)不允許生成ID,一直等,等待到當(dāng)前時間追上上一次生成時間,問題在于,萬一回?fù)艿臅r間太多了呢?可能要等很久,影響了系統(tǒng)的可用性,所以也不是特別好的辦法內(nèi)存里可以存上一次生成唯一ID的時間戳,時鐘回?fù)芰?#xff0c;把當(dāng)前時間戳?xí)負(fù)艿缴洗螘r間戳之前,請求過來,要生成唯一ID,你不要直接就返回一個ID給他,你先做一個比較,如果你發(fā)現(xiàn)當(dāng)前時間戳跟上一次生成唯一ID的時間戳想比,比他小判定,時鐘回?fù)?#xff0c;只要你生成ID,就有可能會發(fā)生ID的重復(fù)可用性這么差的話,人家業(yè)務(wù)服務(wù)萬一此時是要生成一個賬單數(shù)據(jù),申請一個ID,此時你好不容易等待了幾百毫秒之后,你還告訴他你內(nèi)部異常,沒法獲取到唯一ID,反復(fù)的重試,你會影響他的業(yè)務(wù)服務(wù)的運行。
?
第三種辦法,針對第二種辦法的優(yōu)化,如果發(fā)現(xiàn)時鐘回?fù)芴萘?#xff0c;比如超過了1分鐘,此時直接就報警,同時不再對外提供服務(wù),把自己從集群里摘了,比如你要是基于微服務(wù)注冊中心進行注冊的,就得主動做一個下線當(dāng)你發(fā)現(xiàn)當(dāng)前時間戳比上一次生成ID的時間戳要小,發(fā)現(xiàn)時鐘回?fù)芰?#xff0c;判斷一下,回?fù)芰硕嗌俸撩?#xff0c;比如說回?fù)軙r間在500ms以內(nèi),此時可以hang住請求,等待500ms,等到500ms之后,當(dāng)前時間戳比上一次生成ID的時間戳要大了
此時就可以正常生成唯一ID返回給業(yè)務(wù)方了,對于業(yè)務(wù)方而言,僅僅是在個別少數(shù)的時鐘回?fù)艿那闆r之下,請求平時只要50ms,500ms,還在接受范圍之內(nèi),所以說還是可以的,只不過請求慢了一些
如果你要是發(fā)現(xiàn)你當(dāng)前時間戳和上一次生成唯一ID的時間戳想比,你一比較,就發(fā)現(xiàn)超過了500ms了,超過了500ms了,但是在5s之內(nèi),此時你可以返回一個異常狀態(tài)+異常持續(xù)時間給客戶端,不要說有問題,可以通知他自行進行重試
重試機制,最好不要讓業(yè)務(wù)方自己去做,你完全可以去封裝一個你的唯一ID生成服務(wù)的客戶端,基于RPC請求你的接口,但是你在自己的客戶端里封裝一個自動重試的機制,他一旦發(fā)現(xiàn)某臺服務(wù)器返回的響應(yīng)說自己短時間內(nèi)沒法提供服務(wù),他自動就去請求其他機器上的服務(wù)獲取唯一ID了
如果要解決時鐘回?fù)?#xff0c;一般是第二種和第三種結(jié)合在一起來用,但是被動等待甚至主動下線,總是影響系統(tǒng)可用性的,都不是特別好
服務(wù)端的時鐘回?fù)軝z測機制 + 客戶端自己封裝
1s以內(nèi):阻塞請求等待,客戶端的超時時間,應(yīng)該也是1s,暴露1s內(nèi)每一毫秒生成過的唯一ID最大的序號,根據(jù)當(dāng)前時間戳的毫秒,定位到之前生成過ID的這一毫秒的最大ID序號,此時繼續(xù)生成ID,直接在之前生成過的這一毫秒的最大ID序號基礎(chǔ)上遞增就可以了,優(yōu)化之后,就可以保證不需要阻塞等待
1s~10s之間:返回異常碼和異常持續(xù)時間,客戶端在指定時間內(nèi)不請求這臺機器
10s以上:返回故障碼,請求服務(wù)注冊中心讓自己下線,客戶端收到故障碼之后,就直接把這個機器從服務(wù)機器列表里剔除掉,不請求他了,后續(xù)等到那臺機器部署的ID服務(wù)他發(fā)現(xiàn)自己的時間可能過了幾秒鐘,緩過來了,恢復(fù)了,可用了,就可以再次進行服務(wù)注冊,你客戶端刷新服務(wù)注冊列表的時候,就會發(fā)現(xiàn)他,此時可以再次去請求他
?
第四種辦法,要在內(nèi)存里維護最近幾秒內(nèi)生成的ID值,一般時鐘回?fù)芏际菐资撩氲綆装俸撩?#xff0c;很少會超過秒的,所以保存最近幾秒的就行了,然后如果發(fā)生了時鐘回?fù)?#xff0c;此時就看看回?fù)艿搅四囊缓撩?#xff0c;因為時間戳是毫秒級的,接著就看那一毫秒
從那一毫秒生產(chǎn)過的ID序號往后繼續(xù)生成就可以了,后續(xù)每一毫秒都是依次類推,這樣就可以完美避免重復(fù)問題,還不用等待
但是這里也得做一個兜底機制,就是比如你保留最近10s內(nèi)每一毫秒生成的ID,那么萬一時鐘回?fù)芘銮沙^了10s呢?此時這種概率很低,你可以跟二三兩個方案結(jié)合,設(shè)置幾個閾值,比如說,你保留最近10s的ID,回?fù)?0s內(nèi)都可以保證不重復(fù),不停頓;如果超過10s,在60s內(nèi),可以有一個等待的過程,讓他時間前進到你之前保留過的10s范圍內(nèi)去;如果回?fù)艹^了60s,直接下線
上一次生成唯一ID的時間戳也沒了,最近1s內(nèi)每一毫秒的最大ID序號也沒了,重啟之后,出現(xiàn)了時間回?fù)?#xff0c;發(fā)現(xiàn)不了時間回?fù)軉栴},其次也沒有辦法繼續(xù)按照之前的思路去生成不重復(fù)的唯一ID了
4.時鐘回?fù)軆?yōu)化
1、我們一般需要打開時鐘同步功能,這樣ID才能夠最大化的保證按照時間有序,但是時鐘同步打開后,就可能會時鐘回?fù)芰?#xff0c;如果時鐘回?fù)芰?#xff0c;那么生成的ID就會重復(fù),為此我們一般打開時鐘同步的同時關(guān)閉時鐘回?fù)芄δ?#xff1b;
2、序列號的位數(shù)有限,能表示的ID個數(shù)有限,時鐘同步的時候,如果某臺服務(wù)器快了很多,雖然關(guān)閉了時鐘回?fù)?#xff0c;但是在時間追趕上前,ID可能已經(jīng)用完,當(dāng)自增序列號用完了,我們可以做如下的工作:停止ID生成服務(wù)并告警、如果時鐘回?fù)苄∮谝欢ǖ拈撝祫t等待、如大于一定的閾值則通過第三方組件如ZK重新生成一個workerid或者自增時間戳借用下一個時間戳的ID;
3、服務(wù)重啟后,ID可能會重復(fù),為此我們一般需要定期保存時間戳,重啟后的時間戳必須大于保存的時間戳+幾倍保存間隔時間(如3倍),為什么要幾倍呢,主要是考慮到數(shù)據(jù)丟失的情況,但是如果保存到本地硬盤且每次保存都fsync,此時1倍即可。重啟后如果小于可以像第二點那樣類似處理;
4、如果請求ID的QPS不高,比如每毫秒一個,那么每次獲取的ID的尾號都是0,那么基于ID做分庫分表,可能數(shù)據(jù)分布就會不均,此時我們可以增加時間戳的時間間隔或者序列號每次從隨機一個值開始自增。
主要是生成id的這個方法,我把這個再優(yōu)化一下,這個是根據(jù)美團leaf做了進一步優(yōu)化,請參考
/*** 通過雪花算法生成下一個id,注意這里使用synchronized同步* @return 唯一id*/public synchronized long nextId() {long currentTimeMillis = System.currentTimeMillis();//System.out.println(currentTimeMillis);// 當(dāng)前時間小于上一次生成id使用的時間,可能出現(xiàn)服務(wù)器時鐘回?fù)軉栴}//long timestamp = timeGen();if (currentTimeMillis < lastTimeMillis) {long offset = lastTimeMillis - currentTimeMillis;if (offset <= 5) {try {wait(offset << 1);currentTimeMillis = timeGen();if (currentTimeMillis < lastTimeMillis) {throw new RuntimeException("id生成失敗");}} catch (InterruptedException e) {throw new RuntimeException("生成id時出現(xiàn)錯誤");}} else {throw new RuntimeException(String.format("可能出現(xiàn)服務(wù)器時鐘回?fù)軉栴},請檢查服務(wù)器時間。當(dāng)前服務(wù)器時間戳:%d,上一次使用時間戳:%d", currentTimeMillis,lastTimeMillis));}}/*if (currentTimeMillis < lastTimeMillis) {throw new RuntimeException(String.format("可能出現(xiàn)服務(wù)器時鐘回?fù)軉栴},請檢查服務(wù)器時間。當(dāng)前服務(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開始,序列號最大值為4095sequence = 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;}
【分布式】分布式唯一 ID 的 8 種生成方案
SnowFlake 雪花算法詳解與實現(xiàn) - 掘金
雪花算法(SnowFlake)_文丑顏不良啊的博客-CSDN博客
分布式唯一Id(雪花算法),原理+對比+方案 - 簡書
雪花算法snowflake分布式id生成原理詳解,以及對解決時鐘回?fù)軉栴}幾種方案討論_51CTO博客_snowflake 分布式id
Leaf——美團點評分布式ID生成系統(tǒng) - 美團技術(shù)團隊
?https://github.com/Meituan-Dianping/Leaf/tree/master/leaf-core
Snowflake生產(chǎn)方案 時鐘回?fù)軉栴}解決思路_啟動報錯 snowflake 時鐘回?fù)芙鉀Q方法_都是底層的博客-CSDN博客