Java生成隨機的字符串uuid & 數據庫自增主鍵 & redis的id生成策略 & 雪花算法 & 百度的UidGenerator算法
一、分布式ID的業務需求
在復雜的分布式系統中,往往需要對大量的數據和消息進行唯一標識。能夠生成全局唯一ID的系統是非常必要的。
二、生成id的硬性要求
- 全局唯一:不能出現重復的id號,既然是唯一標識,這是最基本的要求。
- 趨勢遞增:在mysql的innoDB引擎中使用的是聚集索引,由於多數RDBMS使用BTree的數據結構來存儲索引數據。因此在主鍵的選擇上我們應該盡量使用有序的主鍵保證寫入性能。
- 單調遞增:保證下一個ID一定大於上一個ID,例如事務版本號,IM增量消息、排序等特殊需求。
- 信息安全:如果id是連續的,惡意獲取用戶工作就非常容易做了,直接按照順序下載指定的URL即可;如果是訂單號就更危險了,競爭對手可以直接知道我們一天的單量,所以在一些應用場景下,需要無規則的id。
- 含時間戳:這樣能夠在開發中快速了解分布式id的生成時間。
三、id生成系統的可用性要求
- 高可用:發一個獲取分布式id的請求,服務器就可以保證99.99%的情況下給我創建一個唯一的分布式id。
- 低延遲:發一個獲取分布式id的請求,服務器響應速度要快。
- 高QPS:假如並發10萬個創建分布式id請求,服務器要頂得住並能成功創建10萬個唯一的分布式id。
四、Java生成隨機的字符串uuid
uuid性能非常高,本地生成,沒有網絡消耗,如果只考慮唯一性UUID是OK的,但是入數據庫的性能較差。
為什么無序的uuid會導致數據庫性能變差?
- 無序:無法預測它的生成順序,不能生成遞增有序的數字。首先分布式id一般都會作為主鍵,uuid太長,占用存儲空間比較大,如果是海量數據庫,就需要考慮存儲量的問題。
- uuid往往是使用字符串存儲:查詢的效率比較低。傳輸數據量大,且不可讀。
- 索引,B+樹索引的分裂:既然分布式id是主鍵,主鍵是包括索引的,然后mysql的索引是通過b+樹來實現的,因為uuid數據是無序的,每一次新的uuid數據的插入,為了查詢的優化,都會對索引“底層的B+樹進行修改,這一點很不好。插入完全無序,不但會導致一些中間節點產生分裂,也會白白創造出很多不飽和的節點,這樣大大降低了數據庫插入的性能。
五、數據庫自增主鍵
(1)數據庫自增主鍵原理是:基於數據庫自增id和MySQL數據庫的replace into 實現的,這里的replace into 跟insert功能類似,不同點在於replace into首先嘗試把數據插入列表中,如果發現表中已經有此行數據(根據主鍵或唯一索引判斷)則先刪除再插入,否則直接插入新數據。
(2)不適合做分布式id:系統水平擴展比較困難如果是單機的話就OK,如果是分布式多台機子的話就很難實現了。數據庫壓力很大,每次獲取id都需要讀取一次數據庫,性能低。
六、redis的id生成策略
redis是單線程的天生保證原子性,可以使用原子操作INCR和NCRBY來實現。
但是缺點同上面的mysql一樣,不利於平行擴容。同時key一定要設置有效期,不過可以使用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
七、雪花算法
使用一個 64 bit 的 long 型的數字作為全局唯一 ID。
1)第一個部分,是 1 個 bit:0,這個是無意義的;二進制里第一個bit如果是1,那么都是負數,但是我們生成的id都是正數,所以第一個Bit統一都是0。1位sign標識位;
2)第二個部分,是 41 個 bit:表示的是時間戳;單位是毫秒。
3)第三個部分,是 5 個 bit:表示的是機房 ID,10001;5位數據中心id
4)第四個部分,是 5 個 bit:表示的是機器 ID,1 1001;5位工作機器id
5)第五個部分,是 12 個 bit:表示的序號,就是某個機房某台機器上這一毫秒內同時生成的 ID 的序號,0000 00000000。記錄同一個毫秒內產生的不同id。12位自增序列
對於分布式系統來說雪花算法的優缺點:
(1)優點:
- 毫秒數在高位,自增序列在低位,整個id都是趨勢遞增的;
- 不依賴數據庫等第三方系統,以服務的方式部署,穩定性更高,生成id的性能也是非常高的;
- 可以根據自身業務特性分配bit位,非常靈活。
(2)缺點:強依賴機器時鍾,如果機器上時鍾回撥,會導致發號重復或者服務會處於不可用狀態。
雪花算法生成唯一id的demo示例:
導入依賴:
<!--hutool 測試雪花算法--> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-captcha</artifactId> <version>5.2.0</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.16.18</version> </dependency> <!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-api --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.7.25</version> </dependency>
import cn.hutool.core.lang.Snowflake; import cn.hutool.core.net.NetUtil; import cn.hutool.core.util.IdUtil; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; /** * 雪花算法生成UUID,測試demo */ @Slf4j @Component public class IdGeneratorSnowFlake { private long workerId = 0; private long datacenterId = 1; private Snowflake snowflake = IdUtil.createSnowflake(workerId,datacenterId); public void init() { try{ workerId = NetUtil.ipv4ToLong(NetUtil.getLocalhostStr()); log.info("當前機器得workerId:{}",workerId); } catch (Exception e) { log.info("當前機器的workerId獲取失敗",e); workerId = NetUtil.getLocalhostStr().hashCode(); log.info("當前機器workId:{}",workerId); } } public synchronized long snowflakeId() { return snowflake.nextId(); } public synchronized long snowflakeId (long workerId,long datacenterId) { snowflake = IdUtil.createSnowflake(workerId,datacenterId); return snowflake.nextId(); } public static void main(String[] args) { //1440603845439913984 //1440603918894759936 System.out.println(new IdGeneratorSnowFlake().snowflakeId()); } }
八、UidGenerator算法
UidGenerator算法是對雪花算法的改進。
UidGenerator的組成:sign(1bit)+ delta seconds (28bits) + worker node id (22bits) + sequence (13bits)
UidGenerator能保證“指定機器&同一時刻&某一並發序列”,是唯一,並據此生成一個64bits的唯一id(long),且默認采用上圖字節分配方式。
UidGenerator與原版的雪花算法不同,UidGenerator還支持自定義時間戳、工作機器id和序列號等各部位的位數,以應用於不同場景。
- 1)sign(1bit):固定1bit符號標識,即生成的UID為正數。
- 2)delta seconds (28 bits):當前時間,相對於時間基點"2016-05-20"的增量值,單位:秒,最多可支持約8.7年(注意:(a)這里的單位是秒,而不是毫秒! (b)注意這里的用詞,是“最多”可支持8.7年)。
- 3)worker id (22 bits):機器id,最多可支持約420w次機器啟動。內置實現為在啟動時由數據庫分配,默認分配策略為用后即棄,后續可提供復用策略。
- 4)sequence (13 bits):每秒下的並發序列,13 bits可支持每秒8192個並發(注意下這個地方,默認支持qps最大為8192個)。
UidGenerator的兩種實現方式:
(1)DefaultUidGenerator
通過DefaultUidGenerator 實現,對時鍾回撥的處理比較簡單粗暴,另外如果使用DefaultUidGenerator 方式生成分布式id,一定要根據你的業務的情況和特點,調整各個字段占用的位數。
(2)CachedUidGenerator
CachedUidGenerator是在DefaultUidGenerator 的基礎上進行了改進,它的核心是利用了RingBuffer,本質上是一個數組,數組中每個項被稱為slot,CachedUidGenerator設計了兩個RingBuffer,一個保存唯一id,一個保存flag,RingBuffer的尺寸是2^n,n必須是正整數。
CachedUidGenerator主要通過采取如下一些措施和方案規避了時鍾回撥的問題和增強唯一性:
- 自增列:CachedUidGenerator的workerid在實例每次重啟時初始化,且就是數據庫的自增id,從而完美的實現每個實例獲取到的workerid不會有任何沖突;
- RingBuffer:CachedUidGenerator不再在每次取ID時都實時計算分布式ID,而是利用RingBuffer數據結構預先生成若干個分布式ID並保存;
- 時間遞增:傳統的SnowFlake算法實現都是通過System.currentTimeMillis()來獲取時間並與上一次時間進行比較,這樣的實現嚴重依賴服務器的時間。而CachedUidGenerator的時間類型是AtomicLong,且通過incrementAndGet()方法獲取下一次的時間,從而脫離了對服務器時間的依賴,也就不會有時鍾回撥的問題(這種做法也有一個小問題,即分布式ID中的時間信息可能並不是這個ID真正產生的時間點,例如:獲取的某分布式ID的值為3200169789968523265,它的反解析結果為{"timestamp":"2019-05-02 23:26:39","workerId":"21","sequence":"1"},但是這個ID可能並不是在"2019-05-02 23:26:39"這個時間產生的)。
CachedUidGenerator通過緩存的方式預先生成一批唯一ID列表,可以解決唯一ID獲取時候的耗時。但這種方式也有不好點,一方面需要耗費內存來緩存這部分數據,另外如果訪問量不大的情況下,提前生成的UID中的時間戳可能是很早之前的。而對於大部分的場景來說,DefaultUidGenerator 就可以滿足相關的需求了,沒必要來湊CachedUidGenerator這個熱鬧。
CachedUidGenerator測試:
不管如何配置, CachedUidGenerator總能提供600萬/s的穩定吞吐量,只是使用年限會有所減少。
代碼地址:https://github.com/baidu/uid-generator
項目里面的配置文件的配置如下:
mysql.properties
#datasource db info mysql.driver=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://127.0.0.1:3306/mysql?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC jdbc.username=*** jdbc.password=*** jdbc.maxActive=2 #datasource base datasource.defaultAutoCommit=true datasource.initialSize=2 datasource.minIdle=0 datasource.maxWait=5000 datasource.testWhileIdle=true datasource.testOnBorrow=true datasource.testOnReturn=false datasource.validationQuery=SELECT 1 FROM DUAL datasource.timeBetweenEvictionRunsMillis=30000 datasource.minEvictableIdleTimeMillis=60000 datasource.logAbandoned=true datasource.removeAbandoned=true datasource.removeAbandonedTimeout=120 datasource.filters=stat