在單機場景下,全局唯一的ID可以使用資料庫的自增功能,分布式系統一般是高并發場景,那自然不适合。
一、背景
分布式系統中我們會對一些資料量大的業務進行分拆,如:使用者表,訂單表。因為資料量巨大一張表無法承接,就會對其進行分庫分表。
但一旦涉及到分庫分表,就會引申出分布式系統中唯一主鍵
ID
的生成問題。
1.1 唯一ID的特性
- 整個系統
唯一;ID
- ID是數字類型,而且是趨勢遞增;
- ID簡短,查詢效率快。
1.2 遞增與趨勢遞增
遞增 | 趨勢遞增 |
---|---|
第一次生成的ID為12,下一次生成的ID是13,再下一次生成的ID是14。 | 什麼是?如:在一段時間内,生成的ID是遞增的趨勢。如:再一段時間内生成的ID在【0,1000】之間,過段時間生成的ID在【1000,2000】之間。但在【0-1000】區間内的時候,ID生成有可能第一次是12,第二次是10,第三次是14。 |
二、方案
2.1 UUID
UUID
全稱:
Universally Unique Identifier
。标準型式包含32個16進制數字,以連字号分為五段,形式為
8-4-4-4-12
的36個字元,示例:
9628f6e9-70ca-45aa-9f7c-77afe0d26e05
。
- 優點:
- 代碼實作簡單;
- 本機生成,沒有性能問題;
- 因為是全球唯一的
,是以遷移資料容易。ID
- 缺點:
- 每次生成的
是無序的,無法保證趨勢遞增;ID
-
的字元串存儲,查詢效率慢;UUID
- 存儲空間大;
-
本身無業務含義,不可讀。ID
- 應用場景:
- 類似生成token令牌的場景;
- 不适用一些要求有趨勢遞增的ID場景,不适合作為高性能需求的場景下的資料庫主鍵。
也有線上生成的網站,如果你的項目上用到了
UUID
,可以用來生成臨時的測試資料。https://www.uuidgenerator.net/
UUID
2.2 MySQL主鍵自增
利用了
MySQL
的主鍵自增
auto_increment
,預設每次
ID
加
1
- 數字化,
遞增;ID
- 查詢效率高;
- 具有一定的業務可讀。
- 存在單點問題,如果
挂了,就沒法生成MySQL
了;ID
- 資料庫壓力大,高并發抗不住。
2.3 MySQL多執行個體主鍵自增
這個方案就是解決
MySQL
的單點問題,在
auto_increment
基本上面,設定
step
步長
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiIn5GcuMDNzUDMyATM10CO1MDMykDNzIDNwkDM5EDMy0yM2MDO2cTMvwVOwkTMwIzLcNjNzgjN3EzLcd2bsJ2Lc12bj5ycn9Gbi52YugTMwIzZtl2Lc9CX6MHc0RHaiojIsJye.png)
如上,每台的初始值分别為
1
,
2
3
...
N
,步長為
N
(這個案例步長為
4
)
- 優點:解決了單點問題;
- 缺點:一旦把步長定好後,就無法擴容;而且單個資料庫的壓力大,資料庫自身性能無法滿足高并發。
- 應用場景:資料不需要擴容的場景。
2.4 基于Redis實作
- 單機:
的Redis
函數在單機上是原子操作,可以保證唯一且遞增。incr
- 叢集:單機
可能無法支撐高并發。叢集情況下,可以使用步長的方式。比如有5個Redis
節點組成的叢集,它們生成的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
- 優點:有序遞增,可讀性強。
- 缺點:占用帶寬,每次要向
進行請求。Redis
三、優化方案
3.1、改造資料庫主鍵自增
資料庫的自增主鍵的特性,可以實作分布式ID,适合做userId,正好符合如何永不遷移資料和避免熱點? 但這個方案有嚴重的問題:
- 一旦步長定下來,不容易擴容;
- 資料庫壓力山大。
- 為什麼壓力大?
因為我們每次擷取ID的時候,都要去資料庫請求一次。那我們可以不可以不要每次去取?
可以請求資料庫得到ID的時候,可設計成獲得的ID是一個ID區間段。
- 上圖
規則表含義:ID
-
表示為主鍵,無業務含義;id
-
為了表示業務,因為整體系統中會有很多業務需要生成biz_tag
,這樣可以共用一張表維護;ID
-
表示現在整體系統中已經配置設定的最大max_id
;ID
-
描述;desc
-
表示每次取的update_time
時間;ID
- 整體流程:
- 【使用者服務】在注冊一個使用者時,需要一個使用者
;會請求【生成ID
服務(是獨立的應用)】的接口;ID
- 【生成
服務】會去查詢資料庫,找到ID
user_tag
,現在的id
為 ,max_id
step=1000
-
服務】把ID
和max_id
傳回給【使用者服務】;并且把step
更新為max_id
,即更新為max_id = max_id + step
1000
- 【使用者服務】獲得
max_id=0
step=1000
- 這個使用者服務可以用
區間的ID=【max_id + 1,max_id+step】
,即為ID
【1,1000】
- 【使用者服務】會把這個區間儲存到
中;jvm
- 【使用者服務】需要用到
的時候,在區間ID
中依次擷取【1,1000】
,可采用ID
中的AtomicLong
方法;getAndIncrement
- 如果把區間的值用完了,再去請求【生産
服務】接口,擷取到ID
max_id
,即可以用1000
【max_id + 1,max_id+step】
ID
【1001,2000】
- 該方案就非常完美的解決了資料庫自增的問題,而且可以自行定義
的起點,和max_id
步長,非常友善擴容;step
- 也解決了資料庫壓力的問題,因為在一段區間内,是在
記憶體中擷取的,而不需要每次請求資料庫。即使資料庫當機了,系統也不受影響,jvm
還能維持一段時間。ID
3.2 競争問題
以上方案中,如果是多個使用者服務,同時擷取
ID
,同時去請求【ID服務】,在擷取
max_id
的時候會存在并發問題。如:
使用者服務,取到的
A
;使用者服務
max_id=1000
取到的也是
B
,那就出現了問題,
max_id=1000
重複了。
ID
解決方案是:加分布式鎖,保證同一時刻隻有一個使用者服務擷取
max_id
3.3 突發阻塞問題
因為競争問題,所有隻有一個使用者服務去操作資料庫,其他二個會被阻塞。出現的現象就是一會兒突然系統耗時變長,怎麼去解決?
- 雙
方案buffer
流程如下:
- 目前擷取
在ID
中,每次擷取buffer1
ID
中擷取;buffer1
- 當
buffer1
已經使用到了ID
,也就是達到區間的100
10%
- 達到了
,先判斷10%
中有沒有去擷取過,如果沒有就立即發起請求擷取buffer2
線程,此線程把擷取到的ID
,設定到ID
buffer2
- 如果
用完了,會自動切換到buffer1
buffer2
-
用到buffer2
了,也會啟動線程再次擷取,設定到10%
buffer1
- 依次往返。
3.4 總結
-
的方案就達到了業務場景用的buffer
,都是在ID
記憶體中獲得的,從此不需要到資料庫中擷取了,資料庫當機時長長點兒也沒太大影響了。jvm
- 因為會有一個線程,會觀察什麼時候去自動擷取。兩個
之間自行切換使用,就解決了突發阻塞的問題。buffer
四、其他方式
還有一些其他的
ID
生成方案,比如:
- 滴滴:時間+起點編号+車牌号;
- 淘寶訂單:時間戳+使用者
ID
- 其他電商:時間戳+下單管道+使用者
,有的會加上訂單第一個商品的ID
ID
-
MongoDB
:通過ID
共12個位元組,時間+機器碼+pid+inc
的方式最終辨別成一個24長度的十六進制字元。4+3+2+3