文章目錄
0. 背景
近日由於訂單量+機器數量增加,導致原來使用時間戳生成唯一訂單號的方法行不通了,出現很多主鍵衝突
並且由於主鍵衝突導致的事務死鎖的機率也隨之加大,進而需要補的數據越來越多!因此急需一個全局唯一ID生成方式。
但由於現在單體框架還在拆解過程中,新的生成方式需要兼容單體應用(多臺集羣)
最好能用最簡單的方式先解決,待以後服務拆解出來後考慮可用性擴展性。
1. 調研
大概有以下幾個選擇,優先級由高到低:
- snowflake
- 數據庫唯一ID主鍵約束
- redis 原子遞增
- UUID
- leaf
- uid-generator
優先級 snowflake 主要是有以下理由:
- snowflake 現階段用一個工具類就能直接上手使用
- 現在分庫分表也使用了 snowflake,以後可以做統一的唯一ID服務
- 很多分佈式ID中間件都基於 snowflake,以後也方便擴展,而不會出現由於算法不同,新算法生成的序列號可能被以前已經生成過
2. 實際使用
- 開始考慮使用IP的最後一位作爲 workid,因爲已經和運維確認過,現在線上機器是最後是連號,不會重複,但是因爲默認 workid 最大是 32,就沒有將 datacenterid 都改爲 workid,因爲我發現我們 hostname 是帶編號的,而且是遞增的規律:xxservice001,最後三位是序號,因此直接取最後三位數字,作爲 workid。
snowflake 默認使用
1+41+10+12
ID組裝模式,其中 10 bit 是 datacenter + workid
2.1 時鐘回撥問題
關於時鐘回撥,一般解決方法有:
- 機器之間定時同步時間
- 通過請求其它機器獲取平均時間戳對比(Leaf)
- 在閾值內不斷獲取當前時間直到大於上次申請時間
- 使用第三方存儲號段來解決(我個人傾向號段方案)
由於正面臨拆分,以後會有專門的 id 生成服務,因此現在並沒有在單體服務上做時鐘回撥的解決,我選擇最簡單的 hutool 工具引用即可,但是它仍可能有一定機率有時鐘回撥。
現階段運行了兩週,還沒發現這個錯誤。
3. 其它ID算法簡析
稍微系統的學習一下業內的分佈式ID生成方案
3.1 uid-generator
以中間件的形式提供服務,基於 snowflake 的優化版,使用雙 buffer 環(我個人理解爲生產者和消費者模式),一個 buffer 生產號段,一個 buffer 消費號段。
snowflake 各個分配可自定義:
- 1bit:第一位不用,用於標誌 0 ,表示正數
- 28bits:每個機器當前時間,單位秒
- 22bits:機器數量
- 13bits:每秒的併發數
- 默認是秒級併發,常規的
snowflake
算法是基於毫秒級(41 bits),但是沒有必要這麼精確,每臺機器每秒併發最多8192夠用了。 - 接着是環形數組,通俗來講就是生產者和消費者模型,一個不能生產過快,一個不能消費過快。同時該模型又有擴容機制,即生產者到一定量後擴容。生產者每次生產者的數量。由於是週期性填充生產並緩存,因此可以接受短時間內第三方存儲服務不可用。
- 默認支持通過 mysql 存儲機器申請情況,這可以用其它來代替,例如 zk,但是這需要我依賴的需要高可用
由此可見, 不管如何配置, CachedUidGenerator總能提供600萬/s的穩定吞吐量, 只是使用年限會有所減少. 這真的是太棒了
https://github.com/baidu/uid-generator/blob/master/README.zh_cn.md
3.2 leaf
也是以中間件的形式提供服務。
目前Leaf的性能在4C8G的機器上QPS能壓測到近5w/s,TP999 1ms,已經能夠滿足大部分的業務的需求
有兩種生產號段方式:Leaf-segment
、Leaf-snowflake
3.2.1 Leaf-segment
基於 mysql 數據庫的號段模式,不是每一次都訪問數據庫,而是在數據庫記錄哪臺機器分配了哪個號段區間。
在每次消費時,當前機器訪問 Leaf 時,Leaf 會得到該機器是否已經發了號段,並判斷下一個號段的狀態,接着從數據庫中更新號段信息,並將號段到載入內存中,防止出現當前號段消費完後,加載下一個號段時的卡頓。
由此得知可以短暫的容忍 mysql 一段時間內不可用。
3.2.2 Leaf-snowflake
標準的 1+41+10+12
ID組裝模式,默認使用 zk 做 leaf 集羣多機房部署以達到高可用,主要是利用臨時節點,記錄當前正在訪問 leaf 的消費端。
解決了時鐘回撥問題:3s一次上傳 leaf 機器節點當前的時間,並進行 RPC 調用其它機器的時間,得到平均時間後,有個閾值(考慮網絡延遲)進而通過判斷當前機器時間與平均時間,來解決時鐘回撥的問題。
3.3 UUID
唯一,但是無序,且長度太長,一般來說,數據庫存都是 bigint(20) 即 64 bit,對應在 java 中使用 long,也是 64bit,而 uuid 是 128 bit,且是字母數字組合,需要 char(128),空間浪費很嚴重
3.4 redis
基於 Redis 全局遞增,步長設置。
使用 lua 腳本結合 snowflake 來實現
例如有三臺業務機器,兩臺 redis 作爲id生成機器:1+41+10+12
模式,其中的 workid 使用 redis 集羣中的機器編號,這樣就會請求的每個 redis 獲取的 redis 都不會重複,但是仍舊有時鐘回撥的問題。
3.5 idx_mysql_id
基於 mysql 的主鍵,強依賴mysql,而且性能低,適合前期使用,併發量大時,不僅每次要訪問數據庫,而且還要做保證數據庫的高可用
3.6 snowflake
基本上現在主流的都會或多或少參考該算法
一共64bit,
- 1bit默認爲0,表示正數
- 41bit作爲時間戳毫秒值
- 10bit作爲工作機器
- 12bit作爲每個機器每毫秒下最多能支持多少個併發請求生成號
cn.hutool.core.lang.Snowflake
// 獲取當前時間,hutool 內部有個定時任務刷時間
long timestamp = genTime();
// 將當前時間與上次申請時間對比,判斷是否出現時鐘回撥
if (timestamp < lastTimestamp) {
//如果服務器時間有問題(時鐘回撥) 報錯。
throw new IllegalStateException(StrUtil.format("Clock moved backwards. Refusing to generate id for {}ms", lastTimestamp - timestamp));
}
// 微秒下的併發走這裏
if (lastTimestamp == timestamp) {
// 微秒下進行序號 +1,默認最多支持微秒 8192 併發量
sequence = (sequence + 1) & sequenceMask;
if (sequence == 0) {
timestamp = tilNextMillis(lastTimestamp);
}
} else {
// 併發數重置
sequence = 0L;
}
// 重置上一次時間
lastTimestamp = timestamp;
4. 個人認爲好的解決方案
4.1 號段
記錄請求的機器ip+port(能唯一定位該機器的標識),然後分配號段,例如1000~3000,另一個機器過來請求,分配另一個號段,例如 4000~9000,這樣。主要是依賴第三方存儲號段分配情況,例如使用數據庫,Redis,Zookeeper 都可以。沒有
4.2 算法生成
就是 snowflake,不同在於根據業務的不同,可以對其中的 bit 做不同的分配,例如毫秒位改爲秒位,這樣多了三個位可以放給機器位,適合併發量不大,但是機器多的場景,或者機器位給最後的併發請求量位,適合併發量大,機器數量比 1024 少的場景。
而且這個也可以進化爲號段,但是會有很多永遠都不會用到的號段,需要有個重複利用的方案。但是這解決了普通號段模式的訂單有序性問題。
5. 參考
分佈是唯一ID系列5篇:https://www.cnblogs.com/itqiankun/p/11350857.html
美團分佈式ID系列2篇:https://tech.meituan.com/2019/03/07/open-source-project-leaf.html
多key的預備:https://mp.weixin.qq.com/s/PCzRAZa9n4aJwHOX-kAhtA?