java九種分布式ID解決方案
背景
在復(fù)雜的分布式系統(tǒng)中,往往需要對(duì)大量的數(shù)據(jù)進(jìn)行唯一標(biāo)識(shí),比如在對(duì)一個(gè)訂單表進(jìn)行了分庫(kù)分表操作,這時(shí)候數(shù)據(jù)庫(kù)的自增ID顯然不能作為某個(gè)訂單的唯一標(biāo)識(shí)。除此之外還有其他分布式場(chǎng)景對(duì)分布式ID的一些要求:
- 趨勢(shì)遞增:由于多數(shù)RDBMS使用B-tree的數(shù)據(jù)結(jié)構(gòu)來(lái)存儲(chǔ)索引數(shù)據(jù),在主鍵的選擇上面我們應(yīng)該盡量使用有序的主鍵保證寫(xiě)入性能。
- 單調(diào)遞增:保證下一個(gè)ID一定大于上一個(gè)ID,例如排序需求。
- 信息安全:如果ID是連續(xù)的,惡意用戶(hù)的扒取工作就非常容易做了;如果是訂單號(hào)就更危險(xiǎn)了,可以直接知道我們的單量。所以在一些應(yīng)用場(chǎng)景下,會(huì)需要ID無(wú)規(guī)則、不規(guī)則。
就不同的場(chǎng)景及要求,市面誕生了很多分布式ID解決方案。本文針對(duì)多個(gè)分布式ID解決方案進(jìn)行介紹,包括其優(yōu)缺點(diǎn)、使用場(chǎng)景及代碼示例。
1、UUID
UUID(Universally Unique Identifier)是基于當(dāng)前時(shí)間、計(jì)數(shù)器(counter)和硬件標(biāo)識(shí)(通常為無(wú)線(xiàn)網(wǎng)卡的MAC地址)等數(shù)據(jù)計(jì)算生成的。包含32個(gè)16進(jìn)制數(shù)字,以連字號(hào)分為五段,形式為8-4-4-4-12的36個(gè)字符,可以生成全球唯一的編碼并且性能高效。
JDK提供了UUID生成工具,代碼如下:
import java.util.UUID; public class Test { public static void main(String[] args) { System.out.println(UUID.randomUUID()); } }
輸出如下
b0378f6a-eeb7-4779-bffe-2a9f3bc76380
UUID完全可以滿(mǎn)足分布式唯一標(biāo)識(shí),但是在實(shí)際應(yīng)用過(guò)程中一般不采用,有如下幾個(gè)原因:
存儲(chǔ)成本高:UUID太長(zhǎng),16字節(jié)128位,通常以36長(zhǎng)度的字符串表示,很多場(chǎng)景不適用。
- 信息不安全:基于MAC地址生成的UUID算法會(huì)暴露MAC地址,曾經(jīng)梅麗莎病毒的制造者就是根據(jù)UUID尋找的。
- 不符合MySQL主鍵要求:MySQL官方有明確的建議主鍵要盡量越短越好,因?yàn)樘L(zhǎng)對(duì)MySQL索引不利:如果作為數(shù)據(jù)庫(kù)主鍵,在InnoDB引擎下,UUID的無(wú)序性可能會(huì)引起數(shù)據(jù)位置頻繁變動(dòng),嚴(yán)重影響性能。
2、數(shù)據(jù)庫(kù)自增ID
利用Mysql的特性ID自增,可以達(dá)到數(shù)據(jù)唯一標(biāo)識(shí),但是分庫(kù)分表后只能保證一個(gè)表中的ID的唯一,而不能保證整體的ID唯一。為了避免這種情況,我們有以下兩種方式解決該問(wèn)題。
2.1、主鍵表
通過(guò)單獨(dú)創(chuàng)建主鍵表維護(hù)唯一標(biāo)識(shí),作為ID的輸出源可以保證整體ID的唯一。舉個(gè)例子:
創(chuàng)建一個(gè)主鍵表
CREATE TABLE `unique_id` ( `id` bigint NOT NULL AUTO_INCREMENT, `biz` char(1) NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `biz` (`biz`) ) ENGINE = InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET =utf8;
業(yè)務(wù)通過(guò)更新操作來(lái)獲取ID信息,然后添加到某個(gè)分表中。
BEGIN; REPLACE INTO unique_id (biz) values ('o') ; SELECT LAST_INSERT_ID(); COMMIT;
2.2、ID自增步長(zhǎng)設(shè)置
我們可以設(shè)置Mysql主鍵自增步長(zhǎng),讓分布在不同實(shí)例的表數(shù)據(jù)ID做到不重復(fù),保證整體的唯一。
如下,可以設(shè)置Mysql實(shí)例1步長(zhǎng)為1,實(shí)例1步長(zhǎng)為2。
查看主鍵自增的屬性
show variables like '%increment%'
顯然,這種方式在并發(fā)量比較高的情況下,如何保證擴(kuò)展性其實(shí)會(huì)是一個(gè)問(wèn)題。
3、號(hào)段模式
號(hào)段模式是當(dāng)下分布式ID生成器的主流實(shí)現(xiàn)方式之一。其原理如下:
- 號(hào)段模式每次從數(shù)據(jù)庫(kù)取出一個(gè)號(hào)段范圍,加載到服務(wù)內(nèi)存中。業(yè)務(wù)獲取時(shí)ID直接在這個(gè)范圍遞增取值即可。
- 等這批號(hào)段ID用完,再次向數(shù)據(jù)庫(kù)申請(qǐng)新號(hào)段,對(duì)max_id字段做一次update操作,新的號(hào)段范圍是(max_id ,max_id +step]。
- 由于多業(yè)務(wù)端可能同時(shí)操作,所以采用版本號(hào)version樂(lè)觀鎖方式更新。
例如 (1,1000] 代表1000個(gè)ID,具體的業(yè)務(wù)服務(wù)將本號(hào)段生成1~1000的自增ID。表結(jié)構(gòu)如下:
CREATE TABLE id_generator ( id int(10) NOT NULL, max_id bigint(20) NOT NULL COMMENT '當(dāng)前最大id', step int(20) NOT NULL COMMENT '號(hào)段的長(zhǎng)度', biz_type int(20) NOT NULL COMMENT '業(yè)務(wù)類(lèi)型', version int(20) NOT NULL COMMENT '版本號(hào),是一個(gè)樂(lè)觀鎖,每次都更新version,保證并發(fā)時(shí)數(shù)據(jù)的正確性', PRIMARY KEY (`id`) )
這種分布式ID生成方式不強(qiáng)依賴(lài)于數(shù)據(jù)庫(kù),不會(huì)頻繁的訪(fǎng)問(wèn)數(shù)據(jù)庫(kù),對(duì)數(shù)據(jù)庫(kù)的壓力小很多。但同樣也會(huì)存在一些缺點(diǎn)比如:服務(wù)器重啟,單點(diǎn)故障會(huì)造成ID不連續(xù)。
4、Redis INCR
基于全局唯一ID的特性,我們可以通過(guò)Redis的INCR命令來(lái)生成全局唯一ID。
Redis分布式ID的簡(jiǎn)單案例
/** * Redis 分布式ID生成器 */ @Component public class RedisDistributedId { @Autowired private StringRedisTemplate redisTemplate; private static final long BEGIN_TIMESTAMP = 1659312000l; /** * 生成分布式ID * 符號(hào)位 時(shí)間戳[31位] 自增序號(hào)【32位】 * @param item * @return */ public long nextId(String item){ // 1.生成時(shí)間戳 LocalDateTime now = LocalDateTime.now(); // 格林威治時(shí)間差 long nowSecond = now.toEpochSecond(ZoneOffset.UTC); // 我們需要獲取的 時(shí)間戳 信息 long timestamp = nowSecond - BEGIN_TIMESTAMP; // 2.生成序號(hào) --》 從Redis中獲取 // 當(dāng)前當(dāng)前的日期 String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd")); // 獲取對(duì)應(yīng)的自增的序號(hào) Long increment = redisTemplate.opsForValue().increment("id:" + item + ":" + date); return timestamp << 32 | increment; } }
同樣使用Redis也有對(duì)應(yīng)的缺點(diǎn):ID 生成的持久化問(wèn)題,如果Redis宕機(jī)了怎么進(jìn)行恢復(fù)?
5、雪花算法
Snowflake,雪花算法是有Twitter開(kāi)源的分布式ID生成算法,以劃分命名空間的方式將64bit位分割成了多個(gè)部分,每個(gè)部分都有具體的不同含義,在Java中64Bit位的整數(shù)是Long類(lèi)型,所以在Java中Snowflake算法生成的ID就是long來(lái)存儲(chǔ)的。具體如下:
- 第一部分:占用1bit,第一位為符號(hào)位,不適用
- 第二部分:41位的時(shí)間戳,41bit位可以表示241個(gè)數(shù),每個(gè)數(shù)代表的是毫秒,那么雪花算法的時(shí)間年限是(241)/(1000×60×60×24×365)=69年
- 第三部分:10bit表示是機(jī)器數(shù),即 2^ 10 = 1024臺(tái)機(jī)器,通常不會(huì)部署這么多機(jī)器
- 第四部分:12bit位是自增序列,可以表示2^12=4096個(gè)數(shù),一秒內(nèi)可以生成4096個(gè)ID,理論上snowflake方案的QPS約為409.6w/s
雪花算法案例代碼:
public class SnowflakeIdWorker { // ==============================Fields=========================================== /** * 開(kāi)始時(shí)間截 (2020-11-03,一旦確定不可更改,否則時(shí)間被回調(diào),或者改變,可能會(huì)造成id重復(fù)或沖突) */ private final long twepoch = 1604374294980L; /** * 機(jī)器id所占的位數(shù) */ private final long workerIdBits = 5L; /** * 數(shù)據(jù)標(biāo)識(shí)id所占的位數(shù) */ private final long datacenterIdBits = 5L; /** * 支持的最大機(jī)器id,結(jié)果是31 (這個(gè)移位算法可以很快的計(jì)算出幾位二進(jìn)制數(shù)所能表示的最大十進(jìn)制數(shù)) */ private final long maxWorkerId = -1L ^ (-1L << workerIdBits); /** * 支持的最大數(shù)據(jù)標(biāo)識(shí)id,結(jié)果是31 */ private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits); /** * 序列在id中占的位數(shù) */ private final long sequenceBits = 12L; /** * 機(jī)器ID向左移12位 */ private final long workerIdShift = sequenceBits; /** * 數(shù)據(jù)標(biāo)識(shí)id向左移17位(12+5) */ private final long datacenterIdShift = sequenceBits + workerIdBits; /** * 時(shí)間截向左移22位(5+5+12) */ private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits; /** * 生成序列的掩碼,這里為4095 (0b111111111111=0xfff=4095) */ private final long sequenceMask = -1L ^ (-1L << sequenceBits); /** * 工作機(jī)器ID(0~31) */ private long workerId; /** * 數(shù)據(jù)中心ID(0~31) */ private long datacenterId; /** * 毫秒內(nèi)序列(0~4095) */ private long sequence = 0L; /** * 上次生成ID的時(shí)間截 */ private long lastTimestamp = -1L; //==============================Constructors===================================== /** * 構(gòu)造函數(shù) * */ public SnowflakeIdWorker() { this.workerId = 0L; this.datacenterId = 0L; } /** * 構(gòu)造函數(shù) * * @param workerId 工作ID (0~31) * @param datacenterId 數(shù)據(jù)中心ID (0~31) */ public SnowflakeIdWorker(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========================================== /** * 獲得下一個(gè)ID (該方法是線(xiàn)程安全的) * * @return SnowflakeId */ public synchronized long nextId() { long timestamp = timeGen(); //如果當(dāng)前時(shí)間小于上一次ID生成的時(shí)間戳,說(shuō)明系統(tǒng)時(shí)鐘回退過(guò)這個(gè)時(shí)候應(yīng)當(dāng)拋出異常 if (timestamp < lastTimestamp) { throw new RuntimeException( String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp)); } //如果是同一時(shí)間生成的,則進(jìn)行毫秒內(nèi)序列 if (lastTimestamp == timestamp) { sequence = (sequence + 1) & sequenceMask; //毫秒內(nèi)序列溢出 if (sequence == 0) { //阻塞到下一個(gè)毫秒,獲得新的時(shí)間戳 timestamp = tilNextMillis(lastTimestamp); } } //時(shí)間戳改變,毫秒內(nèi)序列重置 else { sequence = 0L; } //上次生成ID的時(shí)間截 lastTimestamp = timestamp; //移位并通過(guò)或運(yùn)算拼到一起組成64位的ID return ((timestamp - twepoch) << timestampLeftShift) // | (datacenterId << datacenterIdShift) // | (workerId << workerIdShift) // | sequence; } /** * 阻塞到下一個(gè)毫秒,直到獲得新的時(shí)間戳 * * @param lastTimestamp 上次生成ID的時(shí)間截 * @return 當(dāng)前時(shí)間戳 */ protected long tilNextMillis(long lastTimestamp) { long timestamp = timeGen(); while (timestamp <= lastTimestamp) { timestamp = timeGen(); } return timestamp; } /** * 返回以毫秒為單位的當(dāng)前時(shí)間 * * @return 當(dāng)前時(shí)間(毫秒) */ protected long timeGen() { return System.currentTimeMillis(); } /** * 隨機(jī)id生成,使用雪花算法 * * @return */ public static String getSnowId() { SnowflakeIdWorker sf = new SnowflakeIdWorker(); String id = String.valueOf(sf.nextId()); return id; } //=========================================Test========================================= /** * 測(cè)試 */ public static void main(String[] args) { SnowflakeIdWorker idWorker = new SnowflakeIdWorker(0, 0); for (int i = 0; i < 1000; i++) { long id = idWorker.nextId(); System.out.println(id); } } }
雪花算法強(qiáng)依賴(lài)機(jī)器時(shí)鐘,如果機(jī)器上時(shí)鐘回?fù)?,?huì)導(dǎo)致發(fā)號(hào)重復(fù)。 通常通過(guò)記錄最后使用時(shí)間處理該問(wèn)題。
6、美團(tuán)(Leaf)
由美團(tuán)開(kāi)發(fā),開(kāi)源項(xiàng)目鏈接:https://github.com/Meituan-Dianping/Leaf
Leaf同時(shí)支持號(hào)段模式和snowflake算法模式,可以切換使用。
snowflake模式依賴(lài)于ZooKeeper,不同于原始snowflake算法也主要是在workId的生成上,Leaf中workId是基于ZooKeeper的順序Id來(lái)生成的,每個(gè)應(yīng)用在使用Leaf-snowflake時(shí),啟動(dòng)時(shí)都會(huì)都在Zookeeper中生成一個(gè)順序Id,相當(dāng)于一臺(tái)機(jī)器對(duì)應(yīng)一個(gè)順序節(jié)點(diǎn),也就是一個(gè)workId。
號(hào)段模式是對(duì)直接用數(shù)據(jù)庫(kù)自增ID充當(dāng)分布式ID的一種優(yōu)化,減少對(duì)數(shù)據(jù)庫(kù)的頻率操作。相當(dāng)于從數(shù)據(jù)庫(kù)批量的獲取自增ID,每次從數(shù)據(jù)庫(kù)取出一個(gè)號(hào)段范圍,例如 (1,1000] 代表1000個(gè)ID,業(yè)務(wù)服務(wù)將號(hào)段在本地生成1~1000的自增ID并加載到內(nèi)存。
7、百度(Uidgenerator)
源碼地址:https://github.com/baidu/uid-generator
中文文檔地址:https://github.com/baidu/uid-generator/blob/master/README.zh_cn.md
UidGenerator是百度開(kāi)源的Java語(yǔ)言實(shí)現(xiàn),基于Snowflake算法的唯一ID生成器。它是分布式的,并克服了雪花算法的并發(fā)限制。單個(gè)實(shí)例的QPS能超過(guò)6000000。需要的環(huán)境:JDK8+,MySQL(用于分配WorkerId)。
百度的Uidgenerator對(duì)結(jié)構(gòu)做了部分的調(diào)整,具體如下:
時(shí)間部分只有28位,這就意味著UidGenerator默認(rèn)只能承受8.5年(2^28-1/86400/365),不過(guò)UidGenerator可以適當(dāng)調(diào)整delta seconds、worker node id和sequence占用位數(shù)。
8、滴滴(TinyID)
由滴滴開(kāi)發(fā),開(kāi)源項(xiàng)目鏈接:https://github.com/didi/tinyid
Tinyid是在美團(tuán)(Leaf)的leaf-segment算法基礎(chǔ)上升級(jí)而來(lái),不僅支持了數(shù)據(jù)庫(kù)多主節(jié)點(diǎn)模式,還提供了tinyid-client客戶(hù)端的接入方式,使用起來(lái)更加方便。但和美團(tuán)(Leaf)不同的是,Tinyid只支持號(hào)段一種模式不支持雪花模式。Tinyid提供了兩種調(diào)用方式,一種基于Tinyid-server提供的http方式,另一種Tinyid-client客戶(hù)端方式。
總結(jié)比較
優(yōu)點(diǎn) | 缺點(diǎn) | |
---|---|---|
UUID | 代碼實(shí)現(xiàn)簡(jiǎn)單、沒(méi)有網(wǎng)絡(luò)開(kāi)銷(xiāo),性能好 | 占用空間大、無(wú)序 |
數(shù)據(jù)庫(kù)自增ID | 利用數(shù)據(jù)庫(kù)系統(tǒng)的功能實(shí)現(xiàn),成本小、ID自增有序 | 并發(fā)性能受Mysql限制、強(qiáng)依賴(lài)DB,當(dāng)DB異常時(shí)整個(gè)系統(tǒng)不可用,致命 |
Redis INCR | 性能優(yōu)于數(shù)據(jù)庫(kù)、ID有序 | 解決單點(diǎn)問(wèn)題帶來(lái)的數(shù)據(jù)一致性等問(wèn)題使得復(fù)雜度提高 |
雪花算法 | 不依賴(lài)數(shù)據(jù)庫(kù)等第三方系統(tǒng),性能也是非高、可以根據(jù)自身業(yè)務(wù)特性分配bit位,非常靈活 | 強(qiáng)依賴(lài)機(jī)器時(shí)鐘,如果機(jī)器上時(shí)鐘回?fù)埽瑫?huì)導(dǎo)致發(fā)號(hào)重復(fù)或者服務(wù)會(huì)處于不可用狀態(tài)。 |
號(hào)段模式 | 數(shù)據(jù)庫(kù)的壓力小 | 單點(diǎn)故障ID不連續(xù) |
Leaf、Uidgenerator、TinyID | 高性能、高可用、接入簡(jiǎn)單 | 依賴(lài)第三方組件如ZooKeeper、Mysql |
到此這篇關(guān)于java九種分布式ID解決方案的文章就介紹到這了,更多相關(guān)java 分布式ID內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Springboot視圖解析器ViewResolver使用實(shí)例
這篇文章主要介紹了Springboot視圖解析器ViewResolver使用實(shí)例,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-04-04JDBC實(shí)現(xiàn)Mysql自動(dòng)重連機(jī)制的方法詳解
最近在工作中發(fā)現(xiàn)了一個(gè)問(wèn)題,通過(guò)查找相關(guān)的資料終于解決了,下面這篇文章主要給大家介紹了關(guān)于JDBC實(shí)現(xiàn)Mysql自動(dòng)重連機(jī)制的相關(guān)資料,文中給出多種解決的方法,需要的朋友可以參考借鑒,下面來(lái)一起看看吧。2017-07-07解決java Graphics drawImage 無(wú)法顯示圖片的問(wèn)題
這篇文章主要介紹了解決java Graphics drawImage 無(wú)法顯示圖片的問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-11-11Springboot獲取bean實(shí)例之SpringContextUtil詳解
這篇文章主要介紹了Springboot獲取bean實(shí)例之SpringContextUtil使用,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2025-03-03Java中DTO與Entity拷貝轉(zhuǎn)換的方法小結(jié)
在?Java?開(kāi)發(fā)中,DTO(Data?Transfer?Object)和?Entity(實(shí)體類(lèi))是常見(jiàn)的兩種數(shù)據(jù)模型,本文將介紹幾種常見(jiàn)的工具類(lèi)和自定義方式來(lái)實(shí)現(xiàn)這種轉(zhuǎn)換,感興趣的可以了解下2025-02-02淺談Java虛擬機(jī)對(duì)內(nèi)部鎖的四種優(yōu)化方式
這篇文章主要介紹了淺談Java虛擬機(jī)對(duì)內(nèi)部鎖的四種優(yōu)化方式,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-10-10SSM框架通過(guò)mybatis-generator自動(dòng)生成代碼(推薦)
這篇文章主要介紹了SSM框架通過(guò)mybatis-generator自動(dòng)生成代碼,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2017-11-11關(guān)于RequestMapping注解的作用說(shuō)明
這篇文章主要介紹了關(guān)于RequestMapping注解的作用說(shuō)明,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教。2022-01-01