框架篇:分布式全局唯一ID
本文轉(zhuǎn)載自微信公眾號(hào)「潛行前行」,作者cscw。轉(zhuǎn)載本文請聯(lián)系潛行前行公眾號(hào)。
前言
每一次HTTP請求,數(shù)據(jù)庫的事務(wù)的執(zhí)行,我們追蹤代碼執(zhí)行的過程中,需要一個(gè)唯一值和這些業(yè)務(wù)操作相關(guān)聯(lián),對(duì)于單機(jī)的系統(tǒng),可以用數(shù)據(jù)庫的自增ID或者時(shí)間戳加一個(gè)在本機(jī)遞增值,即可實(shí)現(xiàn)唯一值。但在分布式,又該如何實(shí)現(xiàn)唯一性的ID
- 分布式ID的特性
 - 數(shù)據(jù)庫自增的ID
 - Redis分布式ID
 - Zookeeper分布式ID
 - 全局唯一UUID的優(yōu)缺點(diǎn)
 - Twitter的雪花算法生成分布式ID
 
分布式ID的特性
- 全局唯一性,必須性
 - 冪等性,如果是根據(jù)某些信息生成,則需要保障冪等性
 - 注意安全性,ID里隱藏一些信息,不能被猜出來,也不能被猜出來 ID 如何生成
 - 趨勢遞增性,在查詢比較時(shí),可以判斷業(yè)務(wù)操作的時(shí)間順序
 
數(shù)據(jù)庫自增的ID
實(shí)現(xiàn)簡單,ID單調(diào)自增,數(shù)值類型查詢速度快,但是單點(diǎn)DB存在宕機(jī)風(fēng)險(xiǎn),無法扛住高并發(fā)場景
- CREATE TABLE FLIGHT_ORDER (
 - id int(11) unsigned NOT NULL auto_increment, #自增ID
 - PRIMARY KEY (id),
 - ) ENGINE=innodb;
 
集群下如何保證數(shù)據(jù)庫ID的唯一性
- 當(dāng)隨著業(yè)務(wù)發(fā)展,服務(wù)拓展到多臺(tái)的大集群時(shí),為了解決單點(diǎn)數(shù)據(jù)庫的壓力,數(shù)據(jù)庫也會(huì)相應(yīng)的變成一個(gè)集群,那如何保證集群下數(shù)據(jù)庫ID的唯一性
 - 每一臺(tái)數(shù)據(jù)庫實(shí)例都設(shè)置一個(gè)起始值和增長步長
 
缺點(diǎn):不利于后續(xù)擴(kuò)容,如果后續(xù)需要擴(kuò)容還需要人工介入修改 起始值和增長步長
Redis 分布式ID
假如系統(tǒng)有億萬的數(shù)據(jù),依靠數(shù)據(jù)庫的自增ID在分表分庫之后,需要人工修改每臺(tái)數(shù)據(jù)庫實(shí)例,擴(kuò)容性差,維護(hù)性不好
基于Redis INCR 命令生成分布式全局唯一ID
- 服務(wù)向redis獲取Id,ID則和數(shù)據(jù)庫解耦,可以解決ID和分表分庫的問題,而且redis比數(shù)據(jù)庫性能更快,可以支撐集群服務(wù)并發(fā)獲取ID的需求
 - redis的INCR命令具備了 INCR AND GET 的原子操作;redis是單進(jìn)程單線程架構(gòu),INCR 命令不會(huì)出現(xiàn) ID 重復(fù)
 
- @Autowired
 - private StringRedisTemplate stringRedisTemplate;
 - private static final String ID_KEY = "id_good_order";
 - public Long incrementId() {
 - return stringRedisTemplate.opsForValue().increment(ID_KEY);
 - }
 
HINCRBY 命令
- 實(shí)際上,為了存儲(chǔ)序列號(hào)的更多相關(guān)信息,可以使用了 Redis 的 Hash 數(shù)據(jù)結(jié)構(gòu),Redis 同樣為 Hash 提供 HINCRBY 命令來實(shí)現(xiàn) “INCR AND GET” 原子操作
 
- //KEY_NAME 是 hash結(jié)構(gòu)對(duì)應(yīng)的Key,FIELD_NAME 是hash結(jié)構(gòu)的字段,INCR_BY_NUMBER是增量值
 - redis 127.0.0.1:6379> HINCRBY KEY_NAME FIELD_NAME INCR_BY_NUMBER
 
宕機(jī)序列號(hào)恢復(fù)問題
- redis是內(nèi)存數(shù)據(jù)庫,在沒有開啟RDB或AOF持久化的情況下,一旦宕機(jī)ID數(shù)據(jù)將會(huì)有丟失。即便開啟了RDB持久化,由于最近一次快照時(shí)間和最新一條 HINCRBY 命令的時(shí)間有可能存在時(shí)間差,宕機(jī)后通過RDB快照恢復(fù)數(shù)據(jù)集會(huì)發(fā)生ID取值重復(fù)的情況
 - redis宕機(jī)序列號(hào)恢復(fù)方案
    
- 利用關(guān)系型數(shù)據(jù)庫來記錄一個(gè)短時(shí)內(nèi) 最大可取序列號(hào) MAX_ID,從redis獲取ID時(shí)只能取小于 MAX_ID 的序列號(hào)
 - 為了計(jì)算最大值,需要一個(gè)定時(shí)任務(wù)定期計(jì)算ID消費(fèi)速度RATE,存于redis。當(dāng)客戶端取得 CUR_ID、RATE 和 MAX_ID,則根據(jù) ID 消費(fèi)速度 RATE 計(jì)算 CUR_ID 是否逼近MAX_ID,如果是則更新數(shù)據(jù)庫的MAX_ID
 
 
Zookeeper 分布式ID
- 利用zookeeper的持久性有序節(jié)點(diǎn),可以實(shí)現(xiàn)自增的分布式ID,而且zookeeper是個(gè)高可用的集群服務(wù),提交成功的消息具有持久性,因此不怕機(jī)器宕機(jī)問題,或者單機(jī)問題
 
- <dependency>
 - <groupId>org.apache.curator</groupId>
 - <artifactId>curator-framework</artifactId>
 - <version>4.2.0</version>
 - </dependency>
 - <dependency>
 - <groupId>org.apache.curator</groupId>
 - <artifactId>curator-recipes</artifactId>
 - <version>4.2.0</version>
 - </dependency>
 
- 示例
 
- RetryPolicy retryPolicy = new ExponentialBackoffRetry(500, 3);
 - CuratorFramework client = CuratorFrameworkFactory.builder()
 - .connectString("localhost:2181")
 - .connectionTimeoutMs(5000)
 - .sessionTimeoutMs(5000)
 - .retryPolicy(retryPolicy)
 - .build();
 - client.start();
 - String sequenceName = "root/sequence/distributedId";
 - DistributedAtomicLong distAtomicLong = new DistributedAtomicLong(client, sequenceName, retryPolicy);
 - //使用DistributedAtomicLong生成自增序列
 - public Long sequence() throws Exception {
 - AtomicValue<Long> sequence = this.distAtomicLong.increment();
 - if (sequence.succeeded()) {
 - return sequence.postValue();
 - } else {
 - return null;
 - }
 - }
 
UUID的優(yōu)缺點(diǎn)
- 基于數(shù)據(jù)庫,redis,zookeeper的分布式ID都高度依賴一個(gè)外部服務(wù),對(duì)于某些場景,假如不存在這些外部服務(wù)又該怎么生成分布式的ID
 - JDK里自帶一個(gè)唯一性的ID的生成器,具有全球唯一性,這就是UUID,不過它是串無意義的字符串,存儲(chǔ)性能差,查詢也很耗時(shí),對(duì)于訂單系統(tǒng),不適合作為唯一ID,常見優(yōu)化方案為「轉(zhuǎn)化為兩個(gè)uint64整數(shù)存儲(chǔ)」或者 「折半存儲(chǔ)」(折半后不能保證唯一性)
 - 但對(duì)于日志系統(tǒng),或只是為了作為數(shù)據(jù)里可以唯一識(shí)別序列號(hào)的關(guān)聯(lián)屬性時(shí),可以用UUID
 
- String uuid = UUID.randomUUID().toString().replaceAll("-","");
 
Twitter 的雪花算法生成分布式ID
- 和UUID一樣,雪花算法并不依賴外部服務(wù)
 - 雪花算法是 Twitter 公司內(nèi)部分布式項(xiàng)目采用的ID生成算法,廣受國內(nèi)公司好評(píng)。不依賴第三方服務(wù),效率高
 
Snowflake ID組成結(jié)構(gòu):正數(shù)位(占1比特)+ 時(shí)間戳(占41比特)+ 機(jī)器ID(占5比特)+ 數(shù)據(jù)中心(占5比特)+ 自增值(占12比特),總共64比特組成的一個(gè)Long類型。
1:第一個(gè)bit位(1bit):Java中l(wèi)ong的最高位是符號(hào)位代表正負(fù),正數(shù)是0,負(fù)數(shù)是1,一般生成ID都為正數(shù),所以默認(rèn)為0。
2:時(shí)間戳部分(41bit):毫秒級(jí)的時(shí)間,不建議存當(dāng)前時(shí)間戳,而是用(當(dāng)前時(shí)間戳 - 固定開始時(shí)間戳)的差值,可以使產(chǎn)生的ID從更小的值開始
3:工作機(jī)器id(10bit):也被叫做workId,這個(gè)可以靈活配置,機(jī)房或者機(jī)器號(hào)組合都可以。
4:序列號(hào)部分(12bit),自增值支持同一毫秒內(nèi)同一個(gè)節(jié)點(diǎn)可以生成4096個(gè)ID
- //Twitter的SnowFlake算法,使用SnowFlake算法生成一個(gè)整數(shù)
 - public class SnowFlakeShortUrl {
 - //起始的時(shí)間戳
 - static long START_TIMESTAMP = 1624698370256L;
 - //每一部分占用的位數(shù)
 - static long SEQUENCE_BIT = 12; //序列號(hào)占用的位數(shù)
 - static long MACHINE_BIT = 5; //機(jī)器標(biāo)識(shí)占用的位數(shù)
 - static long DATA_CENTER_BIT = 5; //數(shù)據(jù)中心占用的位數(shù)
 - //每一部分的最大值
 - static long MAX_SEQUENCE = -1L ^ (-1L << SEQUENCE_BIT);
 - static long MAX_MACHINE_NUM = -1L ^ (-1L << MACHINE_BIT);
 - static long MAX_DATA_CENTER_NUM = -1L ^ (-1L << DATA_CENTER_BIT);
 - //每一部分向左的位移
 - static long MACHINE_LEFT = SEQUENCE_BIT;
 - static long DATA_CENTER_LEFT = SEQUENCE_BIT + MACHINE_BIT;
 - static long TIMESTAMP_LEFT = DATA_CENTER_LEFT + DATA_CENTER_BIT;
 - //dataCenterId + machineId 等于10bit工作機(jī)器ID
 - private long dataCenterId; //數(shù)據(jù)中心
 - private long machineId; //機(jī)器標(biāo)識(shí)
 - private volatile long sequence = 0L; //序列號(hào)
 - private volatile long lastTimeStamp = -1L; //上一次時(shí)間戳
 - private volatile long l currTimeStamp = -1L; //當(dāng)前時(shí)間戳
 - private long getNextMill() {
 - long mill = System.currentTimeMillis();
 - while (mill <= lastTimeStamp) mill = System.currentTimeMillis();
 - return mill;
 - }
 - //根據(jù)指定的數(shù)據(jù)中心ID和機(jī)器標(biāo)志ID生成指定的序列號(hào)
 - public SnowFlakeShortUrl(long dataCenterId, long machineId) {
 - Assert.isTrue(dataCenterId >=0 && dataCenterId <= MAX_DATA_CENTER_NUM,"dataCenterId is illegal!");
 - Assert.isTrue(machineId >= 0 || machineId <= MAX_MACHINE_NUM,"machineId is illegal!");
 - this.dataCenterId = dataCenterId;
 - this.machineId = machineId;
 - }
 - //生成下一個(gè)ID
 - public synchronized long nextId() {
 - currTimeStamp = System.currentTimeMillis();
 - Assert.isTrue(currTimeStamp >= lastTimeStamp,"Clock moved backwards");
 - if (currTimeStamp == lastTimeStamp) {
 - //相同毫秒內(nèi),序列號(hào)自增
 - sequence = (sequence + 1) & MAX_SEQUENCE;
 - if (sequence == 0L) { //同一毫秒的序列數(shù)已經(jīng)達(dá)到最大,獲取下一個(gè)毫秒
 - currTimeStamp = getNextMill();
 - }
 - } else {
 - sequence = 0L; //不同毫秒內(nèi),序列號(hào)置為0
 - }
 - lastTimeStamp = currTimeStamp;
 - return (currTimeStamp - START_TIMESTAMP) << TIMESTAMP_LEFT //時(shí)間戳部分
 - | dataCenterId << DATA_CENTER_LEFT //數(shù)據(jù)中心部分
 - | machineId << MACHINE_LEFT //機(jī)器標(biāo)識(shí)部分
 - | sequence; //序列號(hào)部分
 - }
 - public static void main(String[] args) {
 - SnowFlakeShortUrl snowFlake = new SnowFlakeShortUrl(10, 4);
 - for (int i = 0; i < (1 << 12); i++) {
 - //10進(jìn)制
 - System.out.println(snowFlake.nextId());
 - }
 - }
 - }
 
Reference
[1]github地址:https://github.com/cscsss/learnHome
[2]常見分布式全局唯一ID生成策略及算法的對(duì)比:
https://blog.csdn.net/u010398771/article/details/79765836
[3]基于 Redis 的序列號(hào)服務(wù)(分布式id)的設(shè)計(jì):
https://blog.csdn.net/carryxu123456/article/details/82630029
[4]9種 分布式ID生成方案,讓你一次學(xué)個(gè)夠:
https://segmentfault.com/a/1190000022717820

















 
 
 








 
 
 
 