系統唯一ID是我們在設計一個系統的時候常常會遇見的問題,也常常為這個問題而糾結。產生ID的方法有很多,適應不同的場景、需求以及效能要求。所以有些比較複雜的系統會有多個ID產生的策略。以下就介紹一些常見的ID產生策略。
一、資料庫自增長序列或欄位
最常見的方式。利用資料庫,全資料庫唯一。
優點:
簡單,程式碼方便,效能可以接受。
數字ID自然排序,對分頁或需要排序的結果很有幫助。
缺點:
#不同資料庫語法和實作不同,資料庫遷移的時候或多資料庫版本支援的時候需要處理。
在單一資料庫或讀寫分離或一主多從的情況下,只有一個主庫可以生成。有單點故障的風險。
在效能無法達到要求的情況下,比較難於擴充。
如果遇見多個系統需要合併或涉及資料遷移會相當痛苦。
分錶分庫的時候會有麻煩。
優化方案:
#針對主函式庫單點,如果有多個Master函式庫,則每個Master庫設定的起始數字不一樣,步長一樣,可以是Master的個數。例如:Master1 生成的是 1,4,7,10,Master2生成的是2,5,8,11 Master3生成的是 3,6,9,12。這樣就可以有效產生叢集中的唯一ID,也可以大幅降低ID產生資料庫操作的負載。
二、UUID 常見的方式。
可以利用資料庫也可以利用程式生成,一般來說全球唯一。
優點:
簡單,程式碼方便。
產生ID效能非常好,基本上不會有效能問題。
全球唯一,在遇見資料遷移,系統資料合併,或資料庫變更等情況下,可以從容應對。
缺點:
#沒有排序,無法保證趨勢遞增。
UUID往往是使用字串存儲,查詢的效率比較低。
。儲存空間比較大,如果是海量資料庫,就需要考慮儲存量的問題。
#傳輸資料量大
#不讀取。
三、Redis產生ID
當使用資料庫來產生ID效能不夠要求的時候,我們可以嘗試使用Redis來產生ID。這主要依賴Redis是單線程的,所以也可以用產生全域唯一的ID。可以用Redis的原子操作 INCR和INCRBY來實現。
可以使用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
這個,隨便負載到哪個機確定好,未來很難做修改。但是3-5台伺服器基本上能夠滿足器上,都可以獲得不同的ID。但是步長和初始值一定需要事先需要了。使用Redis叢集也可以方式單點故障的問題。
另外,比較適合使用Redis來產生每天從0開始的流水號。例如訂單號碼=日期 當日自增長號。可以每天在Redis中產生一個Key,使用INCR進行累積。
優點:
不依賴資料庫,靈活方便,且效能優於資料庫。
數字ID自然排序,對分頁或需要排序的結果很有幫助。
缺點:
#如果系統中沒有Redis,還需要引進新的元件,增加系統複雜度。
需要編碼和設定的工作量比較大。
四、Twitter的snowflake演算法
snowflake是Twitter開源的分散式ID產生演算法,結果是一個long類型的ID。其核心思想是:使用41bit作為毫秒數,10bit作為機器的ID(5個bit是資料中心,5個bit的機器ID),12bit作為毫秒內的流水號(意味著每個節點在每毫秒可以產生4096 個ID),最後還有一個符號位,永遠是0。具體實現的程式碼可以參看:https://github.com/twitter/snowflake
public class IdWorker { // ==============================Fields=========================================== /** 开始时间截 (2015-01-01) */ private final long twepoch = 1420041600000L; /** 机器id所占的位数 */ private final long workerIdBits = 5L; /** 数据标识id所占的位数 */ private final long datacenterIdBits = 5L; /** 支持的最大机器id,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数) */ private final long maxWorkerId = -1L ^ (-1L << workerIdBits); /** 支持的最大数据标识id,结果是31 */ private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits); /** 序列在id中占的位数 */ private final long sequenceBits = 12L; /** 机器ID向左移12位 */ private final long workerIdShift = sequenceBits; /** 数据标识id向左移17位(12+5) */ private final long datacenterIdShift = sequenceBits + workerIdBits; /** 时间截向左移22位(5+5+12) */ private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits; /** 生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095) */ private final long sequenceMask = -1L ^ (-1L << sequenceBits); /** 工作机器ID(0~31) */ private long workerId; /** 数据中心ID(0~31) */ private long datacenterId; /** 毫秒内序列(0~4095) */ private long sequence = 0L; /** 上次生成ID的时间截 */ private long lastTimestamp = -1L; //==============================Constructors===================================== /** * 构造函数 * @param workerId 工作ID (0~31) * @param datacenterId 数据中心ID (0~31) */ public IdWorker(long workerId, long datacenterId) { if (workerId > maxWorkerId || workerId < 0) { throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId)); } if (datacenterId > maxDatacenterId || datacenterId < 0) { throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId)); } this.workerId = workerId; this.datacenterId = datacenterId; } // ==============================Methods========================================== /** * 获得下一个ID (该方法是线程安全的) * @return SnowflakeId */ public synchronized long nextId() { long timestamp = timeGen(); //如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常 if (timestamp < lastTimestamp) { throw new RuntimeException( String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp)); } //如果是同一时间生成的,则进行毫秒内序列 if (lastTimestamp == timestamp) { sequence = (sequence + 1) & sequenceMask; //毫秒内序列溢出 if (sequence == 0) { //阻塞到下一个毫秒,获得新的时间戳 timestamp = tilNextMillis(lastTimestamp); } } //时间戳改变,毫秒内序列重置 else { sequence = 0L; } //上次生成ID的时间截 lastTimestamp = timestamp; //移位并通过或运算拼到一起组成64位的ID return ((timestamp - twepoch) << timestampLeftShift) // | (datacenterId << datacenterIdShift) // | (workerId << workerIdShift) // | sequence; } /** * 阻塞到下一个毫秒,直到获得新的时间戳 * @param lastTimestamp 上次生成ID的时间截 * @return 当前时间戳 */ protected long tilNextMillis(long lastTimestamp) { long timestamp = timeGen(); while (timestamp <= lastTimestamp) { timestamp = timeGen(); } return timestamp; } /** * 返回以毫秒为单位的当前时间 * @return 当前时间(毫秒) */ protected long timeGen() { return System.currentTimeMillis(); } //==============================Test============================================= /** 测试 */ public static void main(String[] args) { IdWorker idWorker = new IdWorker(0, 0); for (int i = 0; i < 1000; i++) { long id = idWorker.nextId(); System.out.println(Long.toBinaryString(id)); System.out.println(id); } }}
snowflake演算法可以根據自身專案的需要進行一定的修改。例如估算未來的資料中心個數,每個資料中心的機器數以及統一毫秒可以能的並發數來調整在演算法中所需的bit數。
優點:
不依賴資料庫,靈活方便,且效能優於資料庫。
ID依照時間在單機上是遞增的。
缺點:
#在單機上是遞增的,但由於涉及分散式環境,每台機器上的時鐘不可能完全同步,也許有時候也會出現不是全域遞增的狀況。
五、利用zookeeper產生唯一ID
zookeeper主要透過其znode資料版本來產生序號,可以產生32位元和64位元的資料版本號,客戶端可以使用這個版本號來作為唯一的序號。
很少會使用zookeeper來產生唯一ID。主要是由於需要依賴zookeeper,並且是多步驟呼叫API,如果在競爭較大的情況下,需要考慮使用分散式鎖定。因此,性能在高並發的分散式環境下,也不甚理想。
六、MongoDB的ObjectId
MongoDB的ObjectId和snowflake演算法類似。它設計成輕量的,不同的機器都能用全域唯一的同種方法方便地產生它。 MongoDB 從一開始就設計用來作為分散式資料庫,處理多個節點是一個核心需求。使其在分片環境中要容易產生得多。其格式如下: [src/main/resources/objectId.png] 這裡寫圖片描述:
前4 個位元組是從標準紀元開始的時間戳,單位為秒。時間戳,與隨後的5 個位元組組合起來,提供了秒級別的唯一性。由於時間戳在前,這意味著ObjectId 大致上會按照插入的順序排列。這對於某些方面很有用,例如將其作為索引提高效率。這4 個位元組也隱含了文件建立的時間。絕大多數客戶端類別庫都會公開一個方法從ObjectId 取得這個資訊。接下來的3 位元組是所在主機的唯一識別碼。通常是機器主機名稱的雜湊值。這樣就可以確保不同主機產生不同的ObjectId,不會產生衝突。為了確保在同一台機器上並發的多個進程產生的ObjectId 是唯一的,接下來的兩個位元組來自產生ObjectId 的進程識別碼(PID)。前9 位元組保證了同一秒鐘不同機器不同進程產生的ObjectId 是唯一的。後3 位元組就是一個自動增加的計數器,確保相同進程同一秒產生的ObjectId 也是不一樣的。同一秒鐘最多允許每個進程擁有2563(16 777 216)個不同的ObjectId。
相關推薦:
以上是分散式系統唯一ID產生方案匯總的詳細內容。更多資訊請關注PHP中文網其他相關文章!