分散式全域性ID生成器原理剖析及非常齊全開源方案應用示例
為何需要分散式ID生成器
**本人部落格網站 **IT小神 www.itxiaoshen.com
**拿我們系統常用Mysql資料庫來說,在之前的單體架構基本是單庫結構,每個業務表的ID一般從1增,通過 **AUTO_INCREMENT=1
設定自增起始值,隨著系統(比如網際網路電商、外賣)使用者資料日漸增長,單庫效能無法滿足業務系統,在這之後我們會使用基於主從同步的讀寫分離,但當用戶量規模連主從模式都無法應對時,我們會採用分庫分表(當然現在還有其他解決方案比如分散式關係型資料庫如TiDB)的方案,這樣對資料分庫分表後需要有一個唯一 ID 來標識一條資料或訊息,資料庫的自增 ID 顯然不能滿足需求,在複雜分散式系統中,往往還有很多場景需要對大量的資料和訊息進行唯一標識,這就迫使我們需要用到分散式系統中全域性ID生成器。
我們本篇文章只是介紹一些常用實現方案,而大部分的開源分散式ID生成器基本都是基於號段模式和雪花演算法為基礎,可以根據不同業務場景需要選擇,不做詳細說明
分散式ID滿足要求
- 全域性唯一:需要是唯一標識,不能出現重複的 ID 號,這是最基本的要求。
- 高效能:高QPS、低延遲、否則反倒會成為系統瓶頸
- 高可用性:可用性接近 5 個 9
- 資訊保安:如果 ID 是連續的那對於惡意使用者爬蟲採用順序爬取指定 URL爬取資訊就非常容易完成;如果是作為訂單號就更危險了,可以直接知道一天的單量,所以在一些應用場景下會需要 ID 無規則、不規則的要求
- 趨勢遞增:在 MySQL InnoDB 引擎中使用的是聚集索引,採用B+ Tree的資料結構來儲存索引資料,在主鍵的選擇上我們應該儘量使用有序的編號保證寫入效能
- 單調遞增:保證下一個 ID 一定大於上一個 ID,例如事務版本號、IM 增量訊息、排序等特殊需求。
常用解決方案
UUID
全域性ID在Java中們可以簡單使用來UUID生成,輸出的41c9b76fc5ac4265939cd5b27bdacdf1這種結果的字串資料,可以看生成的是36位長度的16進位制的字串,然後將中劃線-替換為空字串**
public static void main(String[] args) { String uuid = UUID.randomUUID().toString().replaceAll("-",""); System.out.println(uuid); }
優點
- 優點UUID設計上固然是可以滿足全域性唯一的要求
缺點
- UUID太長且無序,在網際網路大部分企業中都是使用Mysql資料庫,且有些業務場景需要使用到事務因此底層儲存引擎採用的是Innodb,這就導致B+ Tree索引的分裂,儲存和索引的效能差,並不適合在Innodb作為主鍵,自增ID比較適合作為Innodb主鍵
資料庫自增ID
這樣方式就是單獨使用一個數據庫來生成ID,業務程式通過這個資料庫獲取ID,表結構可以簡單設計如下,--然後再通過事務通過插入等操作資料觸發ID自增,這個資料庫層級效能比較高,你也可以採用表級別插入返回資料的主鍵
CREATE DATABASE `SEQ_ID`;
CREATE TABLE SEQID.SEQUENCE_ID (
id bigint(20) unsigned NOT NULL auto_increment,
id_value char(10) NOT NULL default '',
PRIMARY KEY (id),
UNIQUE KEY id_value(id_value)
) ENGINE=MyISAM;
begin
replace into SEQUENCE_ID(id_value) values('xxx');
SELECT LAST_INSERT_ID();
commit;
end
優點
- 簡單、ID自增
缺點
- DB單點故障
- Mysql併發不好,無法抗住高併發
資料庫叢集模式
上面單個數據庫有弊端,那麼可以採用資料庫叢集,資料庫叢集常用主從和主主,我們使用主主模式,每個資料庫通過設定不同起始值和相同自增步長來實現,比如三臺mysql主主模式,mysql1從1開始自增步長為3,序號1、4、7...,mysql2從2開始自增步長為3,序號2、5、8...,mysql3從3開始自增步長為3,序號3、6、9....,每個業務系統可以通過這三臺中獲取到ID
set @@auto_increment_offset = 1; -- mysql1起始值
set @@auto_increment_increment = 3; -- mysql1自增步長
set @@auto_increment_offset = 2; -- mysql2起始值
set @@auto_increment_increment = 3; -- mysql2自增步長
set @@auto_increment_offset = 3; -- mysql3起始值
set @@auto_increment_increment = 3; -- mysql3自增步長
優點
- 解決DB單點問題
缺點
- 不利於擴容,如果需要進行MySQL擴容增加節點還是比較麻煩,可能還需要停機擴容
號段模式
號段模式幾乎是目前所有開源分散式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,保證併發時資料的正確性
每次申請一個號段,通過樂觀鎖的機制對 max_id
欄位做一次 update
操作,update成功則說明新號段獲取成功,新的號段範圍是 (max_id ,max_id +step]
update id_generator set max_id = #{max_id+step}, version = version + 1 where version = # {version} and biz_type = XXX
Redis實現
Redis
也同樣可以實現,原理就是利用 redis
**的 **incr
命令實現ID的原子性自增,redis持久化也支援基於每條命令持久化方式,且redis自身有高可用叢集模式
192.168.3.117:6379> set seq_id 1 // 初始化自增ID為1
OK
192.168.3.117:6379> incr seq_id // 增加1,並返回遞增後的數值
(integer) 2
雪花演算法(SnowFlake)
雪花演算法(Snowflake)是twitter公司內部分散式專案採用的ID生成演算法,開源後廣受國內大廠的好評,在該演算法影響下各大公司相繼開發出各具特色的分散式生成器。SnowFlake演算法用來生成64位的ID,剛好可以用long整型儲存,能夠用於分散式系統中生產唯一的ID, 並且生成的ID有序
Snowflake
生成的是Long型別的ID,一個Long型別佔8個位元組,每個位元組佔8位元,也就是說一個Long型別佔64個位元。
Snowflake ID組成結構:正數位
(佔1位元)+ 時間戳
(佔41位元)+ 機器ID
(佔5位元)+ 資料中心
(佔5位元)+ 自增值
(佔12位元),總共64位元組成的一個Long型別。
- 第一個bit位(1bit):Java中long的最高位是符號位代表正負,正數是0,負數是1,一般生成ID都為正數,所以預設為0。
- 時間戳部分(41bit):毫秒級的時間,不建議存當前時間戳,而是用(當前時間戳 - 固定開始時間戳)的差值,可以使產生的ID從更小的值開始;41位的時間戳可以使用69年,(1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69年
- 工作機器id(10bit):也被叫做
workId
,這個可以靈活配置,機房或者機器號組合都可以。 - 序列號部分(12bit),自增值支援同一毫秒內同一個節點可以生成4096個ID
雪花演算法比較依賴於時間,會出現時鐘回撥的問題,所以儘量保證時間同步,大部分開源分散式ID生成器大都有優化解決時鐘回撥的問題
下面是基於Twitter的雪花演算法SnowFlake,使用Java語言實現,封裝成工具方法,各個業務應用可以直接使用該工具方法來獲取分散式ID,只需保證每個業務應用有自己的工作機器id即可,而不需要單獨去搭建一個獲取分散式ID的應用
0 - 41位時間戳 - 5位資料中心標識 - 5位機器標識 - 12位序列號
5位資料中心標識跟5位機器標識這樣的分配僅僅是當前實現中分配的,如果業務有其實的需要,可以按其它的分配比例分配,如10位機器標識,不需要資料中心標識。
/**
* twitter的snowflake演算法 -- java實現
*
* @author beyond
* @date 2016/11/26
*/
public class SnowFlake {
/**
* 起始的時間戳
*/
private final static long START_STMP = 1480166465631L;
/**
* 每一部分佔用的位數
*/
private final static long SEQUENCE_BIT = 12; //序列號佔用的位數
private final static long MACHINE_BIT = 5; //機器標識佔用的位數
private final static long DATACENTER_BIT = 5;//資料中心佔用的位數
/**
* 每一部分的最大值
*/
private final static long MAX_DATACENTER_NUM = -1L ^ (-1L << DATACENTER_BIT);
private final static long MAX_MACHINE_NUM = -1L ^ (-1L << MACHINE_BIT);
private final static long MAX_SEQUENCE = -1L ^ (-1L << SEQUENCE_BIT);
/**
* 每一部分向左的位移
*/
private final static long MACHINE_LEFT = SEQUENCE_BIT;
private final static long DATACENTER_LEFT = SEQUENCE_BIT + MACHINE_BIT;
private final static long TIMESTMP_LEFT = DATACENTER_LEFT + DATACENTER_BIT;
private long datacenterId; //資料中心
private long machineId; //機器標識
private long sequence = 0L; //序列號
private long lastStmp = -1L;//上一次時間戳
public SnowFlake(long datacenterId, long machineId) {
if (datacenterId > MAX_DATACENTER_NUM || datacenterId < 0) {
throw new IllegalArgumentException("datacenterId can't be greater than MAX_DATACENTER_NUM or less than 0");
}
if (machineId > MAX_MACHINE_NUM || machineId < 0) {
throw new IllegalArgumentException("machineId can't be greater than MAX_MACHINE_NUM or less than 0");
}
this.datacenterId = datacenterId;
this.machineId = machineId;
}
/**
* 產生下一個ID
*
* @return
*/
public synchronized long nextId() {
long currStmp = getNewstmp();
if (currStmp < lastStmp) {
throw new RuntimeException("Clock moved backwards. Refusing to generate id");
}
if (currStmp == lastStmp) {
//相同毫秒內,序列號自增
sequence = (sequence + 1) & MAX_SEQUENCE;
//同一毫秒的序列數已經達到最大
if (sequence == 0L) {
currStmp = getNextMill();
}
} else {
//不同毫秒內,序列號置為0
sequence = 0L;
}
lastStmp = currStmp;
return (currStmp - START_STMP) << TIMESTMP_LEFT //時間戳部分
| datacenterId << DATACENTER_LEFT //資料中心部分
| machineId << MACHINE_LEFT //機器標識部分
| sequence; //序列號部分
}
private long getNextMill() {
long mill = getNewstmp();
while (mill <= lastStmp) {
mill = getNewstmp();
}
return mill;
}
private long getNewstmp() {
return System.currentTimeMillis();
}
public static void main(String[] args) {
SnowFlake snowFlake = new SnowFlake(2, 3);
for (int i = 0; i < (1 << 12); i++) {
System.out.println(snowFlake.nextId());
}
}
}
百度 (Uidgenerator)
概述
官方GitHub地址** **https://github.com/baidu/uid-generator
UidGenerator是Java實現的, 基於Snowflake演算法的唯一ID生成器。UidGenerator以元件形式工作在應用專案中, 支援自定義workerId位數和初始化策略, 從而適用於docker等虛擬化環境下例項自動重啟、漂移等場景。 在實現上, UidGenerator通過借用未來時間來解決sequence天然存在的併發限制; 採用RingBuffer來快取已生成的UID, 並行化UID的生產和消費, 同時對CacheLine補齊,避免了由RingBuffer帶來的硬體級「偽共享」問題. 最終單機QPS可達600萬。
依賴版本:Java8及以上版本, MySQL(內建WorkerID分配器, 啟動階段通過DB進行分配; 如自定義實現, 則DB非必選依賴)
[](https://github.com/baidu/uid-generator/blob/master/doc/snowflake.png)
Snowflake演算法描述:指定機器 & 同一時刻 & 某一併發序列,是唯一的。據此可生成一個64 bits的唯一ID(long)。預設採用上圖位元組分配方式:
- sign(1bit)****固定1bit符號標識,即生成的UID為正數。
- delta seconds (28 bits)****當前時間,相對於時間基點"2016-05-20"的增量值,單位:秒,而不是毫秒,最多可支援約8.7年
- worker id (22 bits)****機器id,最多可支援約420w次機器啟動。內建實現為在啟動時由資料庫分配,預設分配策略為用後即棄,後續可提供複用策略,同一應用每次重啟就會消費一個workId
- sequence (13 bits)**
**每秒下的併發序列,13 bits可支援每秒8192個併發。
UidGenerator
是基於 Snowflake
演算法實現的,與原始的 snowflake
演算法不同在於,UidGenerator
支援自 定義時間戳
、工作機器ID
和 序列號
等各部分的位數,而且 UidGenerator
中採用使用者自定義 workId
的生成策略。
UidGenerator
需要與資料庫配合使用,需要新增一個 WORKER_NODE
表。當應用啟動時會向資料庫表中去插入一條資料,插入成功後返回的自增ID就是該機器的 workId
資料由host,port組成。
提供了兩種生成器: DefaultUidGenerator、CachedUidGenerator,如對UID生成效能有要求則使用CachedUidGenerator。
CachedUidGenerator
RingBuffer環形陣列,陣列每個元素成為一個slot。RingBuffer容量,預設為Snowflake演算法中sequence最大值,且為2^N。可通過 boostPower
配置進行擴容,以提高RingBuffer 讀寫吞吐量。
Tail指標、Cursor指標用於環形陣列上讀寫slot:
- Tail指標****表示Producer生產的最大序號(此序號從0開始,持續遞增)。Tail不能超過Cursor,即生產者不能覆蓋未消費的slot。當Tail已趕上curosr,此時可通過
rejectedPutBufferHandler
指定PutRejectPolicy - Cursor指標**
**表示Consumer消費到的最小序號(序號序列與Producer序列相同)。Cursor不能超過Tail,即不能消費未生產的slot。當Cursor已趕上tail,此時可通過rejectedTakeBufferHandler
指定TakeRejectPolicy
CachedUidGenerator採用了雙RingBuffer,Uid-RingBuffer用於儲存Uid、Flag-RingBuffer用於儲存Uid狀態(是否可填充、是否可消費)
由於陣列元素在記憶體中是連續分配的,可最大程度利用CPU cache以提升效能。但同時會帶來「偽共享」FalseSharing問題,為此在Tail、Cursor指標、Flag-RingBuffer中採用了CacheLine 補齊方式。
RingBuffer填充時機
- 初始化預填充****RingBuffer初始化時,預先填充滿整個RingBuffer.
- 即時填充****Take消費時,即時檢查剩餘可用slot量(
tail
** -cursor
),如小於設定閾值,則補全空閒slots。閾值可通過paddingFactor
來進行配置,請參考Quick Start中CachedUidGenerator配置** - 週期填充**
**通過Schedule執行緒,定時補全空閒slots。可通過scheduleInterval
配置,以應用定時填充功能,並指定Schedule時間間隔
簡單使用
官方原始碼匯入idea
建立資料庫和匯入表WORKER_NODE.sql
建立一個SpringBoot啟動類,在application-dev.yml檔案配置資料庫資訊,啟動類配置Mybatis掃描com.baidu.fsg.uid的mapper檔案註解,建立一個UidControoler提供一個獲取單個uid的介面,啟動SpringBoot程式
訪問提供介面地址:http://localhost:8080/uid/snowflake** ,返回uid結果,每次重新整理+1**
資料庫表WORKER_NODE當我們每次啟動程式會重新生成新的記錄
美團(Leaf)
概述
官方GitHub地址** **https://github.com/Meituan-Dianping/Leaf
There are no two identical leaves in the world. 世界上沒有兩片完全相同的樹葉。
— 萊布尼茨
Leaf 最早期需求是各個業務線的訂單ID生成需求。在美團早期,有的業務直接通過DB自增的方式生成ID,有的業務通過redis快取來生成ID,也有的業務直接用UUID這種方式來生成ID。以上的方式各自有各自的問題,因此我們決定實現一套分散式ID生成服務來滿足需求。
目前Leaf覆蓋了美團點評公司內部金融、餐飲、外賣、酒店旅遊、貓眼電影等眾多業務線。在4C8G VM基礎上,通過公司RPC方式呼叫,QPS壓測結果近5w/s,TP999 1ms
當然,為了追求更高的效能,需要通過RPC Server來部署Leaf 服務,那僅需要引入leaf-core的包,把生成ID的API封裝到指定的RPC框架中即可。
Leaf Server 是一個spring boot的程式,提供HTTP服務來獲取ID。
Leaf 提供兩種生成的ID的方式(號段模式和snowflake模式),你可以同時開啟兩種方式,也可以指定開啟某種方式(預設兩種方式為關閉狀態)
配置
Leaf Server的配置都在leaf-server/src/main/resources/leaf.properties中
配置項 | 含義 | 預設值 |
---|---|---|
leaf.name | leaf 服務名 | |
leaf.segment.enable | 是否開啟號段模式 | false |
leaf.jdbc.url | mysql 庫地址 | |
leaf.jdbc.username | mysql 使用者名稱 | |
leaf.jdbc.password | mysql 密碼 | |
leaf.snowflake.enable | 是否開啟snowflake模式 | false |
leaf.snowflake.zk.address | snowflake模式下的zk地址 | |
leaf.snowflake.port | snowflake模式下的服務註冊埠 |
- 號段模式
- 如果使用號段模式,需要建立DB表,並配置leaf.jdbc.url, leaf.jdbc.username, leaf.jdbc.password
- 如果不想使用該模式配置leaf.segment.enable=false即可。
- Snowflake模式
- 演算法取自twitter開源的snowflake演算法。
- 如果不想使用該模式配置leaf.snowflake.enable=false即可。
- 配置zookeeper地址
- 在leaf.properties中配置leaf.snowflake.zk.address,配置leaf 服務監聽的埠leaf.snowflake.port。
簡單使用
- 建立資料庫,通過原始碼根目錄下的scripts的leaf_alloc.sql匯入資料庫表leaf_alloc
- 初始化資料,設定步長為2000,每次重啟重新獲取為下一個號段起始值
INSERT INTO leaf_alloc(biz_tag, max_id, step, DESCRIPTION) VALUES('itxs', 1, 2000, 'Test leaf Segment Mode Get Id')
配置application.properties中的資料庫資訊,將leaf.segment.enable設定為true或者註釋;配置zookeeper資訊,leaf.snowflake.enable設定為true或者註釋;啟動leaf-server Spring Boot啟動類
訪問號段模式http介面地址:http://localhost:8080/api/segment/get/itxs
訪問雪花演算法的http介面地址:http://localhost:8080/api/snowflake/get/test
訪問監控頁面地址:http://localhost:8080/cache
我們再使用上一小節的工程專案先簡單通過將leaf的core模組原始碼工程引入,使用號段模式,通過@Autowired SegmentIDGenImpl主動注入leaf號段模式實現類,並完成http getSegment測試介面的controller
package com.itxs.uiddemo.controller;
import javax.annotation.Resource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.baidu.fsg.uid.UidGenerator;
import com.sankuai.inf.leaf.Result;
import com.sankuai.inf.leaf.segment.SegmentIDGenImpl;
@RestController
@RequestMapping(value="/uid")
public class UidController {
@Resource(name = "cachedUidGenerator")
private UidGenerator cachedUidGenerator;
@Autowired
private SegmentIDGenImpl idGen;
@GetMapping("/snowflake")
public String snowflake() {
return String.valueOf(this.cachedUidGenerator.getUID());
}
@GetMapping(value = "/segment/{key}")
public Result<Long> getSegment(@PathVariable("key") String key) throws Exception {
return this.idGen.get(key);
}
}
啟動Spring Boot程式,訪問http://localhost:8080/uid/segment/itxs,返回data欄位就是uid值,每次重新整理+1
重新啟動後,再次訪問http://localhost:8080/uid/segment/itxs,返回data欄位1001,也即是新的號段的起始值,資料庫的maxid也變為1001
當然也可以採用Spring Boot Startser方式使用,官網也有相關的說明
我們自己下載leaf-starter 整合Spring Boot 製作啟動器starter原始碼進行編譯
編譯好leaf-boot-starter後我們新建一個Spring Boot demo工程,由於原來封裝是基於Spring Boot早期的版本,高版本不相容,所以用早期版本,由於leaf-boot-starter裡面使用zookeeper的客戶端curator,我們直接執行是出現curator的某些類找不到,因此我們簡單就直接在工程加入curator-framework和curator-recipes的依賴。
pom檔案
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.itxs</groupId>
<artifactId>leaf-spring-boot-demo</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<artifactId>spring-boot-starter-parent</artifactId>
<groupId>org.springframework.boot</groupId>
<version>2.0.3.RELEASE</version>
</parent>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.sankuai.inf.leaf</groupId>
<artifactId>leaf-boot-starter</artifactId>
<version>1.0.1-RELEASE</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
<version>5.2.0</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>5.2.0</version>
</dependency>
</dependencies>
</project>
在class path也即是resource根目錄下新建leaf.properties檔案,同時開啟號段模式和雪花演算法,配置資訊如下
leaf.name=com.sankuai.leaf.opensource.test
leaf.segment.enable=true
leaf.segment.url=jdbc:mysql://192.168.3.117:3306/leaf?autoReconnect=true&useUnicode=true&characterEncoding=UTF-8
leaf.segment.username=leaf
leaf.segment.password=leaf123
leaf.snowflake.enable=true
leaf.snowflake.address=192.168.3.117
leaf.snowflake.port=2181
新建一個controller用於測試,提供號段和雪花演算法測試介面
package com.itxs.controller;
import com.sankuai.inf.leaf.common.Result;
import com.sankuai.inf.leaf.service.SegmentService;
import com.sankuai.inf.leaf.service.SnowflakeService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping(value="/uid")
public class LeafUidController {
@Autowired
private SegmentService segmentService;
@Autowired
private SnowflakeService snowflakeService;
@GetMapping("/snowflake")
public String snowflake() {
return String.valueOf(this.snowflakeService.getId("test"));
}
@GetMapping(value = "/segment/{key}")
public Result getSegment(@PathVariable("key") String key) throws Exception {
return this.segmentService.getId(key);
}
}
新建Spring Boot啟動類,在啟動類上標註@EnableLeafServer開啟LeafServer的註解,啟動Spring Boot程式,預設是使用8080埠
package com.itxs;
import com.sankuai.inf.leaf.plugin.annotation.EnableLeafServer;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@EnableLeafServer
public class LeafApplication {
public static void main(String[] args) {
SpringApplication.run(LeafApplication.class,args);
}
}
訪問號段uid獲取介面:http://localhost:8080/uid/segment/itxs,放回id結果如下
訪問雪花演算法uid獲取介面:http://localhost:8080/uid/snowflake,返回id結果如下
滴滴(TinyID)
概述
官方GitHub地址** **https://github.com/didi/tinyid/
Tinyid是用Java開發的一款分散式id生成系統,基於資料庫號段演算法實現,關於這個演算法可以參考美團leaf或者tinyid原理介紹。Tinyid擴充套件了leaf-segment演算法,支援了多db(master),同時提供了java-client(sdk)使id生成本地化,獲得了更好的效能與可用性。Tinyid在滴滴客服部門使用,均通過tinyid-client方式接入,每天生成億級別的id。
- 效能
- http方式訪問,效能取決於http server的能力,網路傳輸速度
- java-client方式,id為本地生成,號段長度(step)越長,qps越大,如果將號段設定足夠大,則qps可達1000w+
- 可用性
- 依賴db,當db不可用時,因為server有快取,所以還可以使用一段時間,如果配置了多個db,則只要有1個db存活,則服務可用
- 使用tiny-client,只要server有一臺存活,則理論上可用,server全掛,因為client有快取,也可以繼續使用一段時間
- 特性
- 全域性唯一的long型id
- 趨勢遞增的id,即不保證下一個id一定比上一個大
- 非連續性
- 提供http和java client方式接入
- 支援批量獲取id
- 支援生成1,3,5,7,9...序列的id
- 支援多個db的配置,無單點
適用場景:只關心id是數字,趨勢遞增的系統,可以容忍id不連續,有浪費的場景**
**不適用場景:類似訂單id的業務(因為生成的id大部分是連續的,容易被掃庫、或者測算出訂單量)
推薦使用方式
- tinyid-server推薦部署到多個機房的多臺機器
- 多機房部署可用性更高,http方式訪問需使用方考慮延遲問題
- 推薦使用tinyid-client來獲取id,好處如下:
- id為本地生成(呼叫AtomicLong.addAndGet方法),效能大大增加
- client對server訪問變的低頻,減輕了server的壓力
- 因為低頻,即便client使用方和server不在一個機房,也無須擔心延遲
- 即便所有server掛掉,因為client預載入了號段,依然可以繼續使用一段時間 注:使用tinyid-client方式,如果client機器較多頻繁重啟,可能會浪費較多的id,這時可以考慮使用http方式
- 推薦db配置兩個或更多:
- db配置多個時,只要有1個db存活,則服務可用 多db配置,如配置了兩個db,則每次新增業務需在兩個db中都寫入相關資料
原理和架構
- tinyid是基於資料庫發號演算法實現的,簡單來說是資料庫中儲存了可用的id號段,tinyid會將可用號段載入到記憶體中,之後生成id會直接記憶體中產生。
- 可用號段在第一次獲取id時載入,如當前號段使用達到一定量時,會非同步載入下一可用號段,保證記憶體中始終有可用號段。
- (如可用號段1-1000被載入到記憶體,則獲取id時,會從1開始遞增獲取,當使用到一定百分比時,如20%(預設),即200時,會非同步載入下一可用號段到記憶體,假設新載入的號段是1001-2000,則此時記憶體中可用號段為200-1000,1001~2000),當id遞增到1000時,當前號段使用完畢,下一號段會替換為當前號段。依次類推。
- nextId和getNextSegmentId是tinyid-server對外提供的兩個http介面
- nextId是獲取下一個id,當呼叫nextId時,會傳入bizType,每個bizType的id資料是隔離的,生成id會使用該bizType型別生成的IdGenerator。
- getNextSegmentId是獲取下一個可用號段,tinyid-client會通過此介面來獲取可用號段
- IdGenerator是id生成的介面
- IdGeneratorFactory是生產具體IdGenerator的工廠,每個biz_type生成一個IdGenerator例項。通過工廠,我們可以隨時在db中新增biz_type,而不用重啟服務
- IdGeneratorFactory實際上有兩個子類IdGeneratorFactoryServer和IdGeneratorFactoryClient,區別在於,getNextSegmentId的不同,一個是DbGet,一個是HttpGet
- CachedIdGenerator則是具體的id生成器物件,持有currentSegmentId和nextSegmentId物件,負責nextId的核心流程。nextId最終通過AtomicLong.andAndGet(delta)方法產生。
簡單使用
- 建立表
- 匯入原始碼根目錄下面tinyid/tinyid-server/db.sql的資料庫指令碼,兩張表一張儲存每個業務型別的token授權資訊,一張儲存業務型別ID的號段模式起始值和步長,通過version也即是資料庫樂觀鎖實現原子操作。
cd tinyid/tinyid-server/ && create table with db.sql (mysql)
- 配置db
cd tinyid-server/src/main/resources/offline
vi application.properties
datasource.tinyid.names=primary
datasource.tinyid.primary.driver-class-name=com.mysql.jdbc.Driver
datasource.tinyid.primary.url=jdbc:mysql://ip:port/databaseName?autoReconnect=true&useUnicode=true&characterEncoding=UTF-8
datasource.tinyid.primary.username=root
datasource.tinyid.primary.password=123456
- 啟動tinyid-server
- 將原始碼放在一個linux主機上,當然得有Jdk和Maven環境,在tinyid-server目錄下執行指令碼編譯並啟動編譯好的jar包.並啟動tinyid-server程式
cd tinyid-server/
sh build.sh offline
java -jar output/tinyid-server-xxx.jar
或者將tinyid原始碼匯入idea中,同樣配置db,然後啟動tinyid-server
通過初始化sql指令碼中的授權碼和biz_type,訪問本地的RestApi介面測試,結果如下
接下來我們使用基於java客戶端的方式,這也是官方推薦的,效能最好,我們這裡就直接使用客戶端原始碼工程的測試程式碼
- 匯入Maven dependency
<dependency>
<groupId>com.xiaoju.uemc.tinyid</groupId>
<artifactId>tinyid-client</artifactId>
<version>${tinyid.version}</version>
</dependency>
- 配置客戶端資訊tinyid_client.properties
tinyid.server=localhost:9999
tinyid.token=0f673adf80504e2eaa552f5d791b644c
#(tinyid.server=localhost:9999/gateway,ip2:port2/prefix,...)
- 編寫程式碼,test為業務型別
Long id = TinyId.nextId("test");
List<Long> ids = TinyId.nextId("test", 10);
我們再看資料庫表的資訊,發現max_id已經變為200001,也即是每個客戶端通過步長申請號段放在記憶體中,然後更新資料庫表為下一次申請id段的起始值
看到這裡,以後如果遇到需要使用分散式ID的場景,你會選擇和使用了嗎?