目錄
UUID
自增ID
數據庫多主模式
號段模式
Redis
雪花算法
百度 UIDgenerator
美團Leaf
滴滴TinyID
實戰中的分布式ID生成器!保障數據唯一性的核心組件
怎么編寫Lua腳本是關鍵
怎么執行?
總結
分布式ID在構建大規模分布式系統時扮演著至關重要的角色,主要用于確保在分布式環境中數據的唯一性和一致性。以下是分布式ID的幾個主要作用:
-
確保唯一性:在分布式系統中,可能有成千上萬個實例同時請求ID。分布式ID生成系統能保證即使在高并發的情況下也能生成全局唯一的ID,避免數據沖突和覆蓋
-
便于水平擴展:分布式系統通常需要水平擴展以支持更多的用戶和業務。分布式ID生成機制允許系統在不同的機器、數據中心甚至地理區域中擴展,同時仍然能夠生成唯一的ID,無需擔心ID沖突
-
提高性能:通過避免依賴中心化的數據庫序列生成ID,分布式ID生成機制可以顯著提高應用性能。這些機制通常在內存中進行,減少了網絡延遲和磁盤I/O,從而加快了ID的生成速度
-
減少系統依賴:分布式ID生成不依賴特定的數據庫或存儲系統,減少了系統組件之間的耦合。這種獨立性使得系統更加健壯,減少了因數據庫故障導致的ID生成問題
-
時間有序性:某些分布式ID生成策略(如雪花算法)能夠生成大致按時間順序遞增的ID。這對于需要跟蹤記錄創建順序或進行時間序列分析的應用來說是一個重要特性
-
支持事務和日志追蹤:在復雜的分布式系統中,分布式ID可以用來追蹤和管理跨多個系統和組件的事務和日志。每個操作都可以關聯一個唯一ID,使得問題定位和性能監控變得更加容易。
-
安全性和隱私保護:通過生成不可預測的唯一ID,分布式ID機制還可以增加系統的安全性,防止惡意用戶通過ID預測和訪問未授權的數據
UUID
??UUID (Universally Unique Identifier),通用唯一識別碼。UUID是基于當前時間、計數器(counter)和硬件標識(通常為無線網卡的MAC地址)等數據計算生成的。
UUID由以下幾部分的組合:
-
當前日期和時間,UUID的第一個部分與時間有關,如果你在生成一個UUID之后,過幾秒又生成一個UUID,則第一個部分不同,其余相同。
-
時鐘序列。
-
全局唯一的IEEE機器識別號,如果有網卡,從網卡MAC地址獲得,沒有網卡以其他方式獲得。
UUID 是由一組32位數的16進制數字所構成,以連字號分隔的五組來顯示,形式為 8-4-4-4-12,總共有 36個字符(即三十二個英數字母和四個連字號)。
例如:
aefbbd3a-9cc5-4655-8363-a2a43e6e6c80
xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx
如果需求是只保證唯一性,那么UUID也是可以使用的,但是按照上面的分布式id的要求, UUID其實是不能做成分布式id的,原因如下:
首先分布式id一般都會作為主鍵,但是按照MySQL官方所推薦的主鍵要盡量越短越好,UUID每一個都很長,所以不是很推薦。
既然分布式id是主鍵,然后主鍵是包含索引的,然后Mysql的索引是通過b+樹來實現的,每一次新的UUID數據的插入,為了查詢的優化,都會對索引底層的b+樹進行修改,因為UUID數據是無序的,所以每一次UUID數據的插入都會對主鍵生成的b+樹進行很大的修改,這一點很不好。
信息不安全:基于MAC地址生成UUID的算法可能會造成MAC地址泄露,這個漏洞曾被用于尋找梅麗莎病毒的制作者位置。
自增ID
?針對表結構的主鍵,我們常規的操作是在創建表結構的時候給對應的ID設置 auto_increment
.也就是勾選自增選項。
?但是這種方式我們清楚在單個數據庫的場景中我們是可以這樣做的,但如果是在分庫分表的環境下,直接利用單個數據庫的自增肯定會出現問題。因為ID要唯一,但是分表分庫后只能保證一個表中的ID的唯一,而不能保證整體的ID唯一。??上面的情況我們可以通過單獨創建主鍵維護表來處理。
CREATE TABLE `order_id` (`id` bigint NOT NULL AUTO_INCREMENT,`title` char(1) NOT NULL,PRIMARY KEY (`id`),UNIQUE KEY `title` (`title`)
) ENGINE = InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET =utf8;
通過更新ID操作來獲取ID信息
BEGIN;REPLACE INTO order_id (title) values ('p') ;
SELECT LAST_INSERT_ID();COMMIT;
數據庫多主模式
?單點數據庫方式存在明顯的性能問題,可以對數據庫進行高可用優化,擔心一個主節點掛掉沒法使用,可以選擇做雙主模式集群,也就是兩個MySQL實例都能單獨生產自增的ID。
show variables like '%increment%'
??我們可以設置主鍵自增的步長從2開始。
但是這種方案在并發量比較高的情況下,如何保證其拓展性其實會是一個問題。在高并發的情況下無能為力。
號段模式
號段模式是目前分布式ID生成器的主流實現方式之一,號段模式可以理解為數據庫批量獲取自增ID,每次從數據庫中取一個號段范圍,例如(1,1000]代表1000個ID,具體的業務服務將本號段生成1~1000的自增ID并加載到內存中。
CREATE TABLE id_generator (id int(10) NOT NULL,max_id bigint(20) NOT NULL COMMENT '當前最大id',step int(20) NOT NULL COMMENT '號段的布長',biz_type int(20) NOT NULL COMMENT '業務類型',version int(20) NOT NULL COMMENT '版本號',PRIMARY KEY (`id`)
) biz_type :代表不同業務類型
max_id :當前最大的可用id
step :代表號段的長度
version :是一個樂觀鎖,每次都更新version,保證并發時數據的正確性
?等這批號段ID用完,再次向數據庫申請新號段,對max_id字段做一次update操作,update max_id= max_id + step,update成功則說明新號段獲取成功,新的號段范圍是(max_id ,max_id +step]
?由于多業務端可能同時操作,所以采用版本號version樂觀鎖方式更新,這種分布式ID生成方式不強依賴于數據庫,不會頻繁的訪問數據庫,對數據庫的壓力小很多。
但同樣也會存在一些缺點比如:服務器重啟,單點故障會造成ID不連續。
Redis
?基于全局唯一ID的特性,我們可以通過Redis的INCR命令來生成全局唯一ID。
同樣使用Redis也有對應的缺點:
-
ID 生成的持久化問題,如果Redis宕機了怎么進行恢復
-
當個節點宕機問題
當然針對故障問題我們可以通過Redis集群來處理,比如我們有三個Redis的Master節點。可以初始化每臺Redis的值分別是1,2,3,然后分別把分布式ID的KEY用Hash Tags固定每一個master節點,步長就是master節點的個數。各個Redis生成的ID為:
A:1,4,7 | B:2,5,8 | C:3,6,9
優點:
-
不依賴于數據庫,靈活方便,且性能優于數據庫
-
數字ID有序,對分頁處理和排序都很友好
-
防止了Redis的單機故障
缺點:
-
如果沒有Redis數據庫,需要安裝配置,增加復雜度
-
集群節點確定是3個后,后面調整不是很友好
/*** Redis 分布式ID生成器*/
@Component
public class RedisDistributedId {@Autowiredprivate StringRedisTemplate redisTemplate;private static final long BEGIN_TIMESTAMP = 1659312000l;/*** 生成分布式ID* 符號位 時間戳[31位] 自增序號【32位】* @param item* @return*/public long nextId(String item){// 1.生成時間戳LocalDateTime now = LocalDateTime.now();// 格林威治時間差long nowSecond = now.toEpochSecond(ZoneOffset.UTC);// 我們需要獲取的 時間戳 信息long timestamp = nowSecond - BEGIN_TIMESTAMP;// 2.生成序號 --> 從Redis中獲取// 當前當前的日期String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));// 獲取對應的自增的序號Long increment = redisTemplate.opsForValue().increment("id:" + item + ":" + date);return timestamp << 32 | increment;}}
雪花算法
??Snowflake,雪花算法是有Twitter開源的分布式ID生成算法,以劃分命名空間的方式將64bit位分割成了多個部分,每個部分都有具體的不同含義,在Java中64Bit位的整數是Long類型,所以在Java中Snowflake算法生成的ID就是long來存儲的。
第一部分:占用1bit,第一位為符號位,固定為0,二進制中最高位是符號位,1表示負數,0表示正數。ID都是正整數,所以固定為0。
第二部分:41位的時間戳,41bit位可以表示2(41個數,每個數代表的是毫秒,那么雪花算法的時間年限是(2)41)/(1000×60×60×24×365)=69年。時間戳帶有自增屬性。
第三部分:10bit表示是機器數,即 2^ 10 = 1024臺機器,通常不會部署這么多機器。此部分也可拆分成5位datacenterId和5位workerId,datacenterId表示機房ID,workerId表示機器ID。
第四部分:12bit位是自增序列,表示序列號,同一毫秒時間戳時,通過這個遞增的序列號來區分。即對于同一臺機器而言,同一毫秒時間戳下,可以生成 2^12=4096 個不重復 id。
雪花算法的特點:
-
由于在Java中64bit的整數是long類型,所以在Java中SnowFlake算法生成的id就是long來存儲的。
-
對于每一個雪花算法服務,需要先指定 10 位的機器碼,這個根據自身業務進行設定即可。例如機房號+機器號,機器號+服務號,或者是其他可區別標識的 10 位比特位的整數值都行。
優點:
-
高并發分布式環境下生成不重復 id,每秒可生成百萬個不重復 id。
-
基于時間戳,以及同一時間戳下序列號自增,基本保證 id 有序遞增。
-
不依賴第三方庫或者中間件。
-
算法簡單,在內存中進行,效率高。
缺點:
-
依賴服務器時間,服務器時鐘回撥時可能會生成重復 id。算法中可通過記錄最后一個生成 id 時的時間戳來解決,每次生成 id 之前比較當前服務器時鐘是否被回撥,避免生成重復 id。
值得注意的是:
-
雪花算法每一部分占用的比特位數量并不是固定死的。例如你的業務可能達不到 69 年之久,那么可用減少時間戳占用的位數,雪花算法服務需要部署的節點超過1024 臺,那么可將減少的位數補充給機器碼用。
-
雪花算法中 41 位比特位不是直接用來存儲當前服務器毫秒時間戳的,而是需要當前服務器時間戳減去某一個初始時間戳值,一般可以使用服務上線時間作為初始時間戳值。
-
對于機器碼,可根據自身情況做調整,例如機房號,服務器號,業務號,機器 IP 等都是可使用的。對于部署的不同雪花算法服務中,最后計算出來的機器碼能區分開來即可。
/*** Twitter_Snowflake* SnowFlake的結構如下(每部分用-分開):* 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000* 1位標識,由于long基本類型在Java中是帶符號的,最高位是符號位,正數是0,負數是1,所以id一般是正數,最高位是0* 41位時間截(毫秒級),注意,41位時間截不是存儲當前時間的時間截,而是存儲時間截的差值(當前時間截 - 開始時間截)* 得到的值),這里的的開始時間截,一般是我們的id生成器開始使用的時間,由我們程序來指定的(如下下面程序IdWorker類的startTime屬性)。41位的時間截,可以使用69年,年T = (1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69* 10位的數據機器位,可以部署在1024個節點,包括5位datacenterId和5位workerId* 12位序列,毫秒內的計數,12位的計數順序號支持每個節點每毫秒(同一機器,同一時間截)產生4096個ID序號* 加起來剛好64位,為一個Long型。* SnowFlake的優點是,整體上按照時間自增排序,并且整個分布式系統內不會產生ID碰撞(由數據中心ID和機器ID作區分),并且效率較高,經測試,SnowFlake每秒能夠產生26萬ID左右。* @version 1.0* @Author 振鵬* @Date 2025/5/6 9:58* @注釋*/
public class SnowflakeIdWorkerTest {/*** 開始時間截 (2020-11-03,一旦確定不可更改,否則時間被回調,或者改變,可能會造成id重復或沖突)*/private final long twepoch = 1604374294980L;// 定義位數// 機器ID所占的位數private final long workerIdBits = 5L;// 數據中心ID所占的位數private final long datacenterIdBits = 5L;// 支持的最大機器id,結果是31private final long maxWorkerId = -1L ^ (-1L << workerIdBits);// 支持的最大數據標識id,結果是31private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);// 序列在id中占的位數private final long sequenceBits = 12L;// 機器ID向左移12位private final long workerIdShift = sequenceBits;// 數據中心ID向左移17位(12+5)private final long datacenterIdShift = sequenceBits + workerIdBits;/*** 時間截向左移22位(5+5+12)*/private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;// 生成序列的掩碼,這里為4095 (0b111111111111=0xfff=4095)private final long sequenceMask = -1L ^ (-1L << sequenceBits);// 工作機器ID(0~31)private long workerId;// 數據中心ID(0~31)private long datacenterId;// 毫秒內序列(0~4095)private long sequence = 0L;// 上次生成ID的時間截private long lastTimestamp = -1L;//==============================Constructors=====================================/*** 構造函數**/public SnowflakeIdWorkerTest() {this.workerId = 0L;this.datacenterId = 0L;}/*** 構造函數* @param workerId 工作ID (0~31)* @param datacenterId 數據中心ID (0~31)*/public SnowflakeIdWorkerTest(long workerId, long datacenterId) {if (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;}//==============================Methods==/*** 獲得下一個ID (該方法是線程安全的)* @return SnowflakeId*/public synchronized long nextId() {long timestamp = timeGen();// 如果當前時間小于上一次ID生成的時間戳,說明系統時鐘回退過這個時候應當拋出異常if (timestamp < lastTimestamp) {throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));}// 如果是同一時間生成的,則進行序列號自增if (lastTimestamp == timestamp) {sequence = (sequence + 1) & sequenceMask;if (sequence == 0) {// 序列號溢出timestamp = tilNextMillis(lastTimestamp);}}// 時間戳改變,毫秒內序列重置else {sequence = 0L;}// 上次生成ID的時間截lastTimestamp = timestamp;//移位并通過或運算拼到一起組成64位的IDreturn ((timestamp - twepoch) << timestampLeftShift)| (datacenterId << datacenterIdShift)| (workerId << workerIdShift)| sequence;}/*** 防止產生的時間回撥* 當時間差距小的時候,等待時間差距,直到時間差距大于閾值,才重新生成id(阻塞線程)* @param lastTimestamp 上次生成ID的時間截* @return 當前時間戳*/protected long tilNextMillis(long lastTimestamp) {long timestamp = timeGen();while (timestamp <= lastTimestamp) {timestamp = timeGen();}return timestamp;}/*** 返回以毫秒為單位的當前時間* @return 當前時間(毫秒)*/protected long timeGen() {return System.currentTimeMillis();}/*** 隨機id生成,使用雪花算法** @return */public static String getSnowId() {SnowflakeIdWorkerTest sf = new SnowflakeIdWorkerTest();String id = String.valueOf(sf.nextId());return id;}public static void main(String[] args) {SnowflakeIdWorkerTest idWorker = new SnowflakeIdWorkerTest(0, 0);for (int i = 0; i < 10; i++) {long id = idWorker.nextId();System.out.println(id);}}
}
在生產中如何使用雪花算法來實現分布式ID?
如果發生了時鐘回撥,怎么進行解決?
-
回撥時間很短(<=100ms) :直接阻塞100毫秒
-
回撥時間適中(>100ms &<500ms):維護這500毫秒的時間戳最大的ID信息
-
回撥時間比較長(>=500ms & <1000ms):通過分布式ID服務器進行輪詢處理
-
回撥時間很長(>=1000ms):直接下線。
當使用雪花算法生成唯一ID時,如果時鐘回撥超過500毫秒,可以通過以下幾種方式來處理:
-
等待時鐘同步:等待系統時鐘同步到正確的時間后再繼續生成唯一ID。這樣雖然會造成一定的延遲,但可以保證生成的唯一ID是正確的。
-
保存歷史時間戳:在時鐘回撥時,記錄下回撥前的時間戳,當時鐘同步后,使用回撥前的時間戳來生成唯一ID。這樣可以避免重復生成相同的ID。
-
拋出異常或記錄日志:如果時鐘回撥超過500毫秒,可以拋出異常或記錄日志來提示系統管理員或開發人員出現了異常情況,需要及時處理。
百度 UIDgenerator
??UidGenerator是百度開源的Java語言實現,基于Snowflake算法的唯一ID生成器。它是分布式的,并克服了雪花算法的并發限制。單個實例的QPS能超過6000000。需要的環境:JDK8+,MySQL(用于分配WorkerId)。
??UidGenerator的時間部分只有28位,這就意味著UidGenerator默認只能承受8.5年(2^28-1/86400/365)也可以根據你業務的需求,UidGenerator可以適當調整delta seconds、worker node id和sequence占用位數。
官方地址:https://github.com/baidu/uid-generator/blob/master/README.zh_cn.md
美團Leaf
世界上沒有兩片完全相同的樹葉。
Leaf 最早期需求是各個業務線的訂單ID生成需求。在美團早期,有的業務直接通過DB自增的方式生成ID,有的業務通過redis緩存來生成ID,也有的業務直接用UUID這種方式來生成ID。以上的方式各自有各自的問題,因此我們決定實現一套分布式ID生成服務來滿足需求。具體Leaf 設計文檔見:?leaf 美團分布式ID生成服務
目前Leaf覆蓋了美團點評公司內部金融、餐飲、外賣、酒店旅游、貓眼電影等眾多業務線。在4C8G VM基礎上,通過公司RPC方式調用,QPS壓測結果近5w/s,TP999 1ms。
Leaf-segment方案:利用數據庫自增原理;可以生成趨勢遞增的ID,同時ID號是可計算的,不適用于訂單ID生成場景,比如競對在兩天中午12點分別下單,通過訂單id號相減就能大致計算出公司一天的訂單量,這個是不能忍受的。
Leaf同時支持號段模式和snowflake算法模式,可以切換使用。ID號碼是趨勢遞增的8byte的64位數字,滿足上述數據庫存儲的主鍵要求。
Leaf的snowflake模式依賴于ZooKeeper,利用zookeeper的順序節點原理;不同于原始snowflake算法也主要是在workId的生成上,Leaf中workId是基于ZooKeeper的順序Id來生成的,每個應用在使用Leaf-snowflake時,啟動時都會都在Zookeeper中生成一個順序Id,相當于一臺機器對應一個順序節點,也就是一個workId。
Leaf的號段模式是對直接用數據庫自增ID充當分布式ID的一種優化,減少對數據庫的頻率操作。相當于從數據庫批量的獲取自增ID,每次從數據庫取出一個號段范圍,例如 (1,1000] 代表1000個ID,業務服務將號段在本地生成1~1000的自增ID并加載到內存.。
特性:
1)全局唯一,絕對不會出現重復的ID,且ID整體趨勢遞增。
2)高可用,服務完全基于分布式架構,即使MySQL宕機,也能容忍一段時間的數據庫不可用。
3)高并發低延時,在CentOS 4C8G的虛擬機上,遠程調用QPS可達5W+,TP99在1ms內。
4)接入簡單,直接通過公司RPC服務或者HTTP調用即可接入。
Leaf采用雙buffer的方式,它的服務內部有兩個號段緩存區segment。當前號段已消耗10%時,還沒能拿到下一個號段,則會另啟一個更新線程去更新下一個號段。
簡而言之就是Leaf保證了總是會多緩存兩個號段,即便哪一時刻數據庫掛了,也會保證發號服務可以正常工作一段時間。
滴滴TinyID
由滴滴開發,開源項目鏈接:GitHub - didi/tinyid: ID Generator id生成器 分布式id生成系統,簡單易用、高性能、高可用的id生成系統
Tinyid是在美團(Leaf)的leaf-segment算法基礎上升級而來,不僅支持了數據庫多主節點模式,還提供了tinyid-client客戶端的接入方式,使用起來更加方便。但和美團(Leaf)不同的是,Tinyid只支持號段一種模式不支持雪花模式。Tinyid提供了兩種調用方式,一種基于Tinyid-server提供的http方式,另一種Tinyid-client客戶端方式。每個服務獲取一個號段(1000,2000]、(2000,3000]、(3000,4000]
特性:
1)全局唯一的long型ID
2)趨勢遞增的id
3)提供 http 和 java-client 方式接入
4)支持批量獲取ID
5)支持生成1,3,5,7,9...序列的ID
6)支持多個db的配置
適用場景:只關心ID是數字,趨勢遞增的系統,可以容忍ID不連續,可以容忍ID的浪費
不適用場景:像類似于訂單ID的業務,因生成的ID大部分是連續的,容易被掃庫、或者推算出訂單量等信息
實戰中的分布式ID生成器!保障數據唯一性的核心組件
案例中前置知識點:Redis+Lua實現分布式主鍵ID方案
我們了解了分布式ID應用中最出名的雪花算法以后,其中最需要考慮的就是datacenterId 和 workerId 了,datacenterId 表示機房ID,workerId 表示機器ID。而在Mybatis-Plus中,對這兩個字段都有進行了配置,但這種配置在k8s的環境下,依然會發生重復問題。
生成的策略與時間戳、mac地址、進程id、自增序列有關。
在k8s集群環境下,如果不是在同一個k8s環境中,mac地址有可能會重復
,
java服務進程id都為1,這就造成生成的id會可能重復。所以需要借助第三方來解決redis或zookeeper,因為redis比zookeeper更常用,最終決定用redis來生成
datacenterId
和workerId
實戰案例中,分布式id生成器對Mybatis-Plus中的雪花算法進行了改造優化,通過依靠redis來配置datacenterId 和 workerId,從而解決這個重復的問題,并且也集成了百度開源的UidGenerator,將依靠數據庫自增的方式替換成了依靠redis自增。
-
怎么編寫Lua腳本是關鍵
-
關鍵的邏輯有一點:workid和dataCenterId的初始化過程都結束
-- 如果work_id不存在,則將值初始化為0
if (redis.call('exists', snowflake_work_id_key) == 0) thenredis.call('set',snowflake_work_id_key,0)snowflake_work_id_flag = true
end
-- 如果data_center_id不存在,則將值初始化為0
if (redis.call('exists', snowflake_data_center_id_key) == 0) thenredis.call('set',snowflake_data_center_id_key,0)snowflake_data_center_id_flag = true
end
-- 如果work_id和data_center_id都是初始化了,那么執行返回初始化的值
if (snowflake_work_id_flag and snowflake_data_center_id_flag) thenreturn json_result
end-- 這是初始化的邏輯。
-- redis中work_id的key
local snowflake_work_id_key = KEYS[1]
-- redis中data_center_id的key
local snowflake_data_center_id_key = KEYS[2]
-- worker_id的最大閾值
local max_worker_id = tonumber(ARGV[1])
-- data_center_id的最大閾值
local max_data_center_id = tonumber(ARGV[2])
-- 返回的work_id
local return_worker_id = 0
-- 返回的data_center_id
local return_data_center_id = 0
-- work_id初始化flag
local snowflake_work_id_flag = false
-- data_center_id初始化flag
local snowflake_data_center_id_flag = false
-- 構建并返回JSON字符串
local json_result = string.format('{"%s": %d, "%s": %d}','workId', return_worker_id,'dataCenterId', return_data_center_id)-- 如果work_id不存在,則將值初始化為0
if (redis.call('exists', snowflake_work_id_key) == 0) thenredis.call('set',snowflake_work_id_key,0)snowflake_work_id_flag = true
end
-- 如果data_center_id不存在,則將值初始化為0
if (redis.call('exists', snowflake_data_center_id_key) == 0) thenredis.call('set',snowflake_data_center_id_key,0)snowflake_data_center_id_flag = true
end
-- 如果work_id和data_center_id都是初始化了,那么執行返回初始化的值
if (snowflake_work_id_flag and snowflake_data_center_id_flag) thenreturn json_result
end-- 獲得work_id的值
local snowflake_work_id = tonumber(redis.call('get',snowflake_work_id_key))
-- 獲得data_center_id的值
local snowflake_data_center_id = tonumber(redis.call('get',snowflake_data_center_id_key))-- 如果work_id的值達到了最大閾值
if (snowflake_work_id == max_worker_id) then-- 如果data_center_id的值也達到了最大閾值if (snowflake_data_center_id == max_data_center_id) then-- 將work_id的值初始化為0redis.call('set',snowflake_work_id_key,0)-- 將data_center_id的值初始化為0redis.call('set',snowflake_data_center_id_key,0)else-- 如果data_center_id的值沒有達到最大值,將進行自增,并將自增的結果返回return_data_center_id = redis.call('incr',snowflake_data_center_id_key)end
else-- 如果work_id的值沒有達到最大值,將進行自增,并將自增的結果返回return_worker_id = redis.call('incr',snowflake_work_id_key)
end
return string.format('{"%s": %d, "%s": %d}','workId', return_worker_id,'dataCenterId', return_data_center_id)
-
怎么執行?
在MyBatisPlus中實現雪花算法,我們需要實現一個接口
/*** Id生成器接口*/
public interface IdentifierGenerator {/*** 判斷是否分配 ID** @param idValue 主鍵值* @return true 分配 false 無需分配*/default boolean assignId(Object idValue) {return StringUtils.checkValNull(idValue);}/*** 生成Id** @param entity 實體* @return id*/Number nextId(Object entity);/*** 生成uuid** @param entity 實體* @return uuid*/default String nextUUID(Object entity) {return IdWorker.get32UUID();}
}
本案例中沒有這么做,我們是考慮后續可能存在一些新的可替代方案
為了脫離框架依賴,如果以后出現了比Mybatis-Plus更高效的持久化框架,可以更加方便的去替換。
所以選擇直接將Mybatis-Plus的雪花算法移植到組件中,并進行了優化。
-
IdGeneratorAutoConfig
public class IdGeneratorAutoConfig {@Beanpublic WorkAndDataCenterIdHandler workAndDataCenterIdHandler(StringRedisTemplate stringRedisTemplate){return new WorkAndDataCenterIdHandler(stringRedisTemplate);}@Beanpublic WorkDataCenterId workDataCenterId(WorkAndDataCenterIdHandler workAndDataCenterIdHandler){return workAndDataCenterIdHandler.getWorkAndDataCenterId();}@Beanpublic SnowflakeIdGenerator snowflakeIdGenerator(WorkDataCenterId workDataCenterId){return new SnowflakeIdGenerator(workDataCenterId);}
}// 定義返回的類型
@Data
public class WorkDataCenterId {private Long workId;private Long dataCenterId;
}
-
WorkAndDataCenterIdHandler
是執行lua腳本的執行器,執行完腳本后獲得了WorkDataCenterId
的實體,包好了workId
和dataCenterId
-
WorkDataCenterId
在注入到spring上下文的過程中,就調用了WorkAndDataCenterIdHandler#getWorkAndDataCenterId
方法在redis中加載workId
和dataCenterId
-
加載的過程就是我們上面所列出來Lua腳本。
@Slf4j
public class WorkAndDataCenterIdHandler {private final String SNOWFLAKE_WORK_ID_KEY = "snowflake_work_id";private final String SNOWFLAKE_DATA_CENTER_ID_key = "snowflake_data_center_id";public final List<String> keys = Stream.of(SNOWFLAKE_WORK_ID_KEY,SNOWFLAKE_DATA_CENTER_ID_key).collect(Collectors.toList());private StringRedisTemplate stringRedisTemplate;private DefaultRedisScript<String> redisScript;public WorkAndDataCenterIdHandler(StringRedisTemplate stringRedisTemplate){this.stringRedisTemplate = stringRedisTemplate;try {redisScript = new DefaultRedisScript<>();redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("lua/workAndDataCenterId.lua")));redisScript.setResultType(String.class);} catch (Exception e) {log.error("redisScript init lua error",e);}}public WorkDataCenterId getWorkAndDataCenterId(){WorkDataCenterId workDataCenterId = new WorkDataCenterId();try {String[] data = new String[2];data[0] = String.valueOf(IdGeneratorConstant.MAX_WORKER_ID);data[1] = String.valueOf(IdGeneratorConstant.MAX_DATA_CENTER_ID);String result = stringRedisTemplate.execute(redisScript, keys, data);workDataCenterId = JSON.parseObject(result,WorkDataCenterId.class);}catch (Exception e) {log.error("getWorkAndDataCenterId error",e);}return workDataCenterId;}
}
當創建SnowflakeIdGenerator時,將WorkDataCenterId注入進去
public SnowflakeIdGenerator(WorkDataCenterId workDataCenterId) {if (Objects.nonNull(workDataCenterId.getDataCenterId())) {this.workerId = workDataCenterId.getWorkId();this.datacenterId = workDataCenterId.getDataCenterId();}else {this.datacenterId = getDatacenterId(maxDatacenterId);workerId = getMaxWorkerId(datacenterId, maxWorkerId);}
}
SnowflakeIdGenerator
@Slf4j
public class SnowflakeIdGenerator {/*** 時間起始標記點,作為基準,一般取系統的最近時間(一旦確定不能變動)*/private static final long BASIS_TIME = 1288834974657L;/*** 機器標識位數*/private final long workerIdBits = 5L;private final long datacenterIdBits = 5L;private final long maxWorkerId = -1L ^ (-1L << workerIdBits);private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);/*** 毫秒內自增位*/private final long sequenceBits = 12L;private final long workerIdShift = sequenceBits;private final long datacenterIdShift = sequenceBits + workerIdBits;/*** 時間戳左移動位*/private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;private final long sequenceMask = -1L ^ (-1L << sequenceBits);private final long workerId;/*** 數據標識 ID 部分*/private final long datacenterId;/*** 并發控制*/private long sequence = 0L;/*** 上次生產 ID 時間戳*/private long lastTimestamp = -1L;/*** IP 地址*/private InetAddress inetAddress;public SnowflakeIdGenerator(WorkDataCenterId workDataCenterId) {if (Objects.nonNull(workDataCenterId.getDataCenterId())) {this.workerId = workDataCenterId.getWorkId();this.datacenterId = workDataCenterId.getDataCenterId();}else {this.datacenterId = getDatacenterId(maxDatacenterId);workerId = getMaxWorkerId(datacenterId, maxWorkerId);}}public SnowflakeIdGenerator(InetAddress inetAddress) {this.inetAddress = inetAddress;this.datacenterId = getDatacenterId(maxDatacenterId);this.workerId = getMaxWorkerId(datacenterId, maxWorkerId);initLog();}private void initLog() {if (log.isDebugEnabled()) {log.debug("Initialization SnowflakeIdGenerator datacenterId:" + this.datacenterId + " workerId:" + this.workerId);}}/*** 有參構造器** @param workerId 工作機器 ID* @param datacenterId 序列號*/public SnowflakeIdGenerator(long workerId, long datacenterId) {Assert.isFalse(workerId > maxWorkerId || workerId < 0,String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));Assert.isFalse(datacenterId > maxDatacenterId || datacenterId < 0,String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId));this.workerId = workerId;this.datacenterId = datacenterId;initLog();}/*** 獲取 maxWorkerId*/protected long getMaxWorkerId(long datacenterId, long maxWorkerId) {StringBuilder mpid = new StringBuilder();mpid.append(datacenterId);String name = ManagementFactory.getRuntimeMXBean().getName();if (StringUtils.isNotBlank(name)) {/** GET jvmPid*/mpid.append(name.split("@")[0]);}/** MAC + PID 的 hashcode 獲取16個低位*/return (mpid.toString().hashCode() & 0xffff) % (maxWorkerId + 1);}/*** 數據標識id部分*/protected long getDatacenterId(long maxDatacenterId) {long id = 0L;try {if (null == this.inetAddress) {this.inetAddress = InetAddress.getLocalHost();}NetworkInterface network = NetworkInterface.getByInetAddress(this.inetAddress);if (null == network) {id = 1L;} else {byte[] mac = network.getHardwareAddress();if (null != mac) {id = ((0x000000FF & (long) mac[mac.length - 2]) | (0x0000FF00 & (((long) mac[mac.length - 1]) << 8))) >> 6;id = id % (maxDatacenterId + 1);}}} catch (Exception e) {log.warn(" getDatacenterId: " + e.getMessage());}return id;}public long getBase(){int five = 5;long timestamp = timeGen();//閏秒if (timestamp < lastTimestamp) {long offset = lastTimestamp - timestamp;if (offset <= five) {try {wait(offset << 1);timestamp = timeGen();if (timestamp < lastTimestamp) {throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", offset));}} catch (Exception e) {throw new RuntimeException(e);}} else {throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", offset));}}if (lastTimestamp == timestamp) {// 相同毫秒內,序列號自增sequence = (sequence + 1) & sequenceMask;if (sequence == 0) {// 同一毫秒的序列數已經達到最大timestamp = tilNextMillis(lastTimestamp);}} else {// 不同毫秒內,序列號置為 1 - 2 隨機數sequence = ThreadLocalRandom.current().nextLong(1, 3);}lastTimestamp = timestamp;return timestamp;}/*** 獲取分布式id** @return id*/public synchronized long nextId() {long timestamp = getBase();// 時間戳部分 | 數據中心部分 | 機器標識部分 | 序列號部分return ((timestamp - BASIS_TIME) << timestampLeftShift)| (datacenterId << datacenterIdShift)| (workerId << workerIdShift)| sequence;}/*** 獲取訂單編號** @return orderNumber*/public synchronized long getOrderNumber(long userId,long tableCount) {long timestamp = getBase();long sequenceShift = log2N(tableCount);// 時間戳部分 | 數據中心部分 | 機器標識部分 | 序列號部分 | 用戶id基因return ((timestamp - BASIS_TIME) << timestampLeftShift)| (datacenterId << datacenterIdShift)| (workerId << workerIdShift)| (sequence << sequenceShift)| (userId % tableCount);}protected long tilNextMillis(long lastTimestamp) {long timestamp = timeGen();while (timestamp <= lastTimestamp) {timestamp = timeGen();}return timestamp;}protected long timeGen() {return SystemClock.now();}/*** 反解id的時間戳部分*/public static long parseIdTimestamp(long id) {return (id>>22)+ BASIS_TIME;}/*** 求log2(N)* */public long log2N(long count) {return (long)(Math.log(count)/ Math.log(2));}public long getMaxWorkerId() {return maxWorkerId;}public long getMaxDatacenterId() {return maxDatacenterId;}
}
總結
-
在構建
SnowflakeIdGenerator
時,如果通過lua執行加載獲取workDataCenterId
失敗,則還采取Mybiats-plus的生成策略 -
nextId
方法就是獲取分布式id的方法,其內部getBase()
是更新時間戳的部分,由 時間戳部分 | 數據中心部分 | 機器標識部分 | 序列號部分 這四個部分組成 -
getOrderNumber
方法是生成訂單編號,使用了基因替換法,來解決在分庫分表情況下,使用訂單id和用戶id查詢訂單時的全路由問題。