每個公司的業(yè)務(wù)場景不同,生成唯一ID、序列號或者單號的方案也都不一樣,這邊簡單列舉一些常見的方案:
一、 數(shù)據(jù)庫自增主鍵
優(yōu)點:
1)簡單,代碼方便,性能可以接受。
2)數(shù)字ID天然排序,對分頁或者需要排序的結(jié)果很有幫助。
缺點:
1)不同數(shù)據(jù)庫語法和實現(xiàn)不同,數(shù)據(jù)庫遷移的時候或多數(shù)據(jù)庫版本支持的時候需要處理。
2)在單個數(shù)據(jù)庫或讀寫分離或一主多從的情況下,只有一個主庫可以生成。有單點故障的風(fēng)險。
3)在性能達不到要求的情況下,比較難于擴展。
4)如果遇見多個系統(tǒng)需要合并或者涉及到數(shù)據(jù)遷移會相當(dāng)痛苦。
5)分表分庫的時候會有麻煩。
優(yōu)化方案:
針對主庫單點,如果有多個Master庫,則每個Master庫設(shè)置的起始數(shù)字不一樣,步長一樣,可以是Master的個數(shù)。比如:Master1 生成的是 1、4、7、10,Master2生成的是2、5、8、11,Master3生成的是 3、6、9、12。這樣就可以有效生成集群中的唯一ID,也可以大大降低ID生成數(shù)據(jù)庫操作的負載。
二、UUID
利用java.util.UUID類生成
優(yōu)點:
1)簡單,代碼方便。
2)生成ID性能非常好,基本不會有性能問題。
3)全球唯一,理論上不會重復(fù),在遇見數(shù)據(jù)遷移,系統(tǒng)數(shù)據(jù)合并,或者數(shù)據(jù)庫變更等情況下,可以從容應(yīng)對。
缺點:
1)沒有排序,無法保證趨勢遞增。
2)UUID往往是使用字符串存儲,查詢的效率比較低。
3)存儲空間比較大,如果是海量數(shù)據(jù)庫,就需要考慮存儲量的問題。
4)傳輸數(shù)據(jù)量大
5)不可讀。
三、數(shù)據(jù)庫或者Redis生成ID
當(dāng)使用數(shù)據(jù)庫來生成ID性能不夠要求的時候,我們可以嘗試使用Redis來生成ID。
數(shù)據(jù)庫方式實際上也就是建一張表保存當(dāng)前值以及步長,每次去更新獲取。
這主要依賴于Redis是單線程的,所以也可以用生成全局唯一的ID??梢杂肦edis的原子操作 INCR和INCRBY來實現(xiàn)。
可以使用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臺服務(wù)器基本能夠滿足器上,都可以獲得不同的ID。但是步長和初始值一定需要事先需要了。使用Redis集群也可以防止單點故障的問題。
另外,比較適合使用Redis來生成每天從0開始的流水號。比如訂單號=日期+當(dāng)日自增長號。可以每天在Redis中生成一個Key,使用INCR進行累加。
優(yōu)點:
1)不依賴于數(shù)據(jù)庫,靈活方便,且性能優(yōu)于數(shù)據(jù)庫。
2)數(shù)字ID天然排序,對分頁或者需要排序的結(jié)果很有幫助。
缺點:
1)如果系統(tǒng)中沒有Redis,還需要引入新的組件,增加系統(tǒng)復(fù)雜度。
2)需要編碼和配置的工作量比較大。
四、時間戳 業(yè)務(wù)字段 隨機數(shù)組合
比如時間戳+用戶ID+隨機數(shù)就是一個很好的例子
訂單命名的幾種規(guī)則:
1、不重復(fù)。
唯一性
2、安全性。
你的訂單編號不能透露你公司的真實運營信息,比如你的訂單就是流水號的話,那么別人就可以從訂單號推測出你公司的整體運營概括了。所以訂單編碼必須是除了你們公司少部分人外,其他人基本看不懂的。參考京東和淘寶的編碼規(guī)則,基本別人是搞不清是什么意思的。
其實最好的防泄漏編碼規(guī)則就是在編碼中不要加入任何和公司運營的數(shù)據(jù)。
3、不能使用大規(guī)模隨機碼。
很多人分析訂單編碼規(guī)則的時候,第一個念頭肯定是不重復(fù)唯一性,那么第二個念頭可能就是安全性,那么同時滿足前兩者的第三個念頭就是隨機碼了。因為大規(guī)模的隨機碼隨機生成,因為本身就沒有意義所以無所謂泄密了。但是事實上這種編碼規(guī)則在實現(xiàn)上會有很大問題的。
隨機碼滿足第二點安全性要求,為了滿足第一點不重復(fù)特性,那就得在生成隨機碼的時候?qū)Ρ葰v史數(shù)據(jù)是否有重復(fù),如果你的訂單數(shù)量到達了十萬次,你每次生成訂單編碼時就得對比十萬條歷史數(shù)據(jù),你可想而知會造成什么巨大問題。
但是難道隨機碼就不能在編碼中使用了嗎?小規(guī)模的隨機碼是可以使用的,比如2~3位,這種隨機碼一般都是和流水號等結(jié)合使用,主要作用是為了隱藏流水號的真實數(shù)據(jù)而進行使用的。
4、防止并發(fā)。
這條規(guī)則主要針對編碼中有時間的設(shè)定。
5、控制位數(shù)。
這點很好理解,訂單號的作用就是便于查詢。
一般正常使用場景應(yīng)該是訂單出異狀或者退貨的時候,用戶將訂單號報給客服,由客服進行查詢。
所以一般在10~15位為好。
京東10位,淘寶15位。
五、雪花算法

使用雪花算法生成的主鍵,二進制表示形式包含4部分,從高位到低位分表為:1bit符號位、41bit時間戳位、10bit工作進程位以及12bit序列號位。
符號位(1bit)
預(yù)留的符號位,恒為零。
時間戳位(41bit)
41位的時間戳可以容納的毫秒數(shù)是2的41次冪,一年所使用的毫秒數(shù)是:365 * 24 * 60 * 60 * 1000。通過計算可知:
Math.pow(2, 41) / (365 * 24 * 60 * 60 * 1000L);
結(jié)果約等于69.73年。ShardingSphere的雪花算法的時間紀(jì)元從2016年11月1日零點開始,可以使用到2086年,相信能滿足絕大部分系統(tǒng)的要求。
工作進程位(10bit)
該標(biāo)志在Java進程內(nèi)是唯一的,如果是分布式應(yīng)用部署應(yīng)保證每個工作進程的id是不同的。該值默認為0,可通過屬性設(shè)置。
序列號位(12bit)
該序列是用來在同一個毫秒內(nèi)生成不同的ID。如果在這個毫秒內(nèi)生成的數(shù)量超過4096(2的12次冪),那么生成器會等待到下個毫秒繼續(xù)生成。
問題:
時間回撥問題:由于機器的時間是動態(tài)的調(diào)整的,有可能會出現(xiàn)時間跑到之前幾毫秒,如果這個時候獲取到了這種時間,則會出現(xiàn)數(shù)據(jù)重復(fù)
機器id分配及回收問題:目前機器id需要每臺機器不一樣,這樣的方式分配需要有方案進行處理,同時也要考慮,如果改機器宕機了,對應(yīng)的workerId分配后的回收問題
機器id上限:機器id是固定的bit,那么也就是對應(yīng)的機器個數(shù)是有上限的,在有些業(yè)務(wù)場景下,需要所有機器共享同一個業(yè)務(wù)空間,那么10bit表示的1024臺機器是不夠的。
時鐘回撥
服務(wù)器時鐘回撥會導(dǎo)致產(chǎn)生重復(fù)序列,因此默認分布式主鍵生成器提供了一個最大容忍的時鐘回撥毫秒數(shù)。 如果時鐘回撥的時間超過最大容忍的毫秒數(shù)閾值,則程序報錯;如果在可容忍的范圍內(nèi),默認分布式主鍵生成器會等待時鐘同步到最后一次主鍵生成的時間后再繼續(xù)工作。 最大容忍的時鐘回撥毫秒數(shù)的默認值為0,可通過屬性設(shè)置。
/**
* @Author
* @Date 17/07/2019
**/
public class SnowFlakeGenerator {
/**
* Twitter_Snowflake<br>
* SnowFlake的結(jié)構(gòu)如下(每部分用-分開):<br>
* 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000 <br>
* 1位標(biāo)識,由于long基本類型在Java中是帶符號的,最高位是符號位,正數(shù)是0,負數(shù)是1,所以id一般是正數(shù),最高位是0<br>
* 41位時間截(毫秒級),注意,41位時間截不是存儲當(dāng)前時間的時間截,而是存儲時間截的差值(當(dāng)前時間截 - 開始時間截)
* 得到的值),這里的的開始時間截,一般是我們的id生成器開始使用的時間,由我們程序來指定的(如下下面程序IdWorker類的startTime屬性)。41位的時間截,可以使用69年,年T = (1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69<br>
* 10位的數(shù)據(jù)機器位,可以部署在1024個節(jié)點,包括5位datacenterId和5位workerId<br>
* 12位序列,毫秒內(nèi)的計數(shù),12位的計數(shù)順序號支持每個節(jié)點每毫秒(同一機器,同一時間截)產(chǎn)生4096個ID序號<br>
* 加起來剛好64位,為一個Long型。<br>
* SnowFlake的優(yōu)點是,整體上按照時間自增排序,并且整個分布式系統(tǒng)內(nèi)不會產(chǎn)生ID碰撞(由數(shù)據(jù)中心ID和機器ID作區(qū)分),并且效率較高,經(jīng)測試,SnowFlake每秒能夠產(chǎn)生26萬ID左右。
*/
// ==============================Fields===========================================
/**
* 開始時間截 (2018-07-03)
*/
private final long twepoch = 1530607760000L;
/**
* 機器id所占的位數(shù)
*/
private final long workerIdBits = 5L;
/**
* 數(shù)據(jù)標(biāo)識id所占的位數(shù)
*/
private final long datacenterIdBits = 5L;
/**
* 支持的最大機器id,結(jié)果是31 (這個移位算法可以很快的計算出幾位二進制數(shù)所能表示的最大十進制數(shù))
*/
private final long maxWorkerId = -1L ^ (-1L << workerIdBits);
/**
* 支持的最大數(shù)據(jù)標(biāo)識id,結(jié)果是31
*/
private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
/**
* 序列在id中占的位數(shù)
*/
private final long sequenceBits = 12L;
/**
* 機器ID向左移12位
*/
private final long workerIdShift = sequenceBits;
/**
* 數(shù)據(jù)標(biāo)識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;
/**
* 數(shù)據(jù)中心ID(0~31)
*/
private long datacenterId;
/**
* 毫秒內(nèi)序列(0~4095)
*/
private long sequence = 0L;
/**
* 上次生成ID的時間截
*/
private long lastTimestamp = -1L;
//==============================Constructors=====================================
/**
* 構(gòu)造函數(shù)
*
* @param workerId 工作ID (0~31)
* @param datacenterId 數(shù)據(jù)中心ID (0~31)
*/
public SnowFlakeGenerator(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();
//如果當(dāng)前時間小于上一次ID生成的時間戳,說明系統(tǒng)時鐘回退過這個時候應(yīng)當(dāng)拋出異常
if (timestamp < lastTimestamp) {
throw new RuntimeException(
String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
}
//如果是同一時間生成的,則進行毫秒內(nèi)序列
if (lastTimestamp == timestamp) {
sequence = (sequence + 1) & sequenceMask;
//毫秒內(nèi)序列溢出
if (sequence == 0) {
//阻塞到下一個毫秒,獲得新的時間戳
timestamp = tilNextMillis(lastTimestamp);
}
}
//時間戳改變,毫秒內(nèi)序列重置
else {
sequence = 0L;
}
//上次生成ID的時間截
lastTimestamp = timestamp;
//移位并通過或運算拼到一起組成64位的ID
return (((timestamp - twepoch) << timestampLeftShift) //
| (datacenterId << datacenterIdShift) //
| (workerId << workerIdShift) //
| sequence);
}
/**
* 阻塞到下一個毫秒,直到獲得新的時間戳
*
* @param lastTimestamp 上次生成ID的時間截
* @return 當(dāng)前時間戳
*/
protected long tilNextMillis(long lastTimestamp) {
long timestamp = timeGen();
while (timestamp <= lastTimestamp) {
timestamp = timeGen();
}
return timestamp;
}
/**
* 返回以毫秒為單位的當(dāng)前時間
*
* @return 當(dāng)前時間(毫秒)
*/
protected long timeGen() {
return System.currentTimeMillis();
}
//==============================Test=============================================
/**
* 測試
*/
public static void main(String[] args) {
SnowFlakeGenerator idWorker = new SnowFlakeGenerator(0, 0);
for (int i = 0; i < 1000; i++) {
long id = idWorker.nextId();
//System.out.println(Long.toBinaryString(id));
System.out.println(id);
}
}
}
六、LEAF
Leaf——美團點評分布式ID生成系統(tǒng)
七、案例
1、秒級別時間+系統(tǒng)標(biāo)識+業(yè)務(wù)標(biāo)識+版本號+固定位數(shù)的隨機數(shù)
2、秒級別時間+系統(tǒng)標(biāo)識+業(yè)務(wù)標(biāo)識+版本號+固定位數(shù)數(shù)據(jù)庫按步長取數(shù)(比如取右邊六位)
3、秒或毫秒時間+用戶ID+固定位數(shù)的隨機數(shù)
一個用戶正常情況下不可能同時生成兩筆以上訂單
第2、3種方法正常來說是絕對安全的,第1種隨機數(shù)方法最好還是數(shù)據(jù)庫加一個唯一索引,如果重復(fù)的話就重新生成一次即可。