分布式主鍵

每個(gè)公司的業(yè)務(wù)場(chǎng)景不同,生成唯一ID、序列號(hào)或者單號(hào)的方案也都不一樣,這邊簡(jiǎn)單列舉一些常見的方案:

一、 數(shù)據(jù)庫(kù)自增主鍵

優(yōu)點(diǎn):
1)簡(jiǎn)單,代碼方便,性能可以接受。
2)數(shù)字ID天然排序,對(duì)分頁(yè)或者需要排序的結(jié)果很有幫助。

缺點(diǎn):
1)不同數(shù)據(jù)庫(kù)語(yǔ)法和實(shí)現(xiàn)不同,數(shù)據(jù)庫(kù)遷移的時(shí)候或多數(shù)據(jù)庫(kù)版本支持的時(shí)候需要處理。
2)在單個(gè)數(shù)據(jù)庫(kù)或讀寫分離或一主多從的情況下,只有一個(gè)主庫(kù)可以生成。有單點(diǎn)故障的風(fēng)險(xiǎn)。
3)在性能達(dá)不到要求的情況下,比較難于擴(kuò)展。
4)如果遇見多個(gè)系統(tǒng)需要合并或者涉及到數(shù)據(jù)遷移會(huì)相當(dāng)痛苦。
5)分表分庫(kù)的時(shí)候會(huì)有麻煩。

優(yōu)化方案:
針對(duì)主庫(kù)單點(diǎn),如果有多個(gè)Master庫(kù),則每個(gè)Master庫(kù)設(shè)置的起始數(shù)字不一樣,步長(zhǎng)一樣,可以是Master的個(gè)數(shù)。比如:Master1 生成的是 1、4、7、10,Master2生成的是2、5、8、11,Master3生成的是 3、6、9、12。這樣就可以有效生成集群中的唯一ID,也可以大大降低ID生成數(shù)據(jù)庫(kù)操作的負(fù)載。

二、UUID

利用java.util.UUID類生成

優(yōu)點(diǎn):
1)簡(jiǎn)單,代碼方便。
2)生成ID性能非常好,基本不會(huì)有性能問(wèn)題。
3)全球唯一,理論上不會(huì)重復(fù),在遇見數(shù)據(jù)遷移,系統(tǒng)數(shù)據(jù)合并,或者數(shù)據(jù)庫(kù)變更等情況下,可以從容應(yīng)對(duì)。

缺點(diǎn):
1)沒有排序,無(wú)法保證趨勢(shì)遞增。
2)UUID往往是使用字符串存儲(chǔ),查詢的效率比較低。
3)存儲(chǔ)空間比較大,如果是海量數(shù)據(jù)庫(kù),就需要考慮存儲(chǔ)量的問(wèn)題。
4)傳輸數(shù)據(jù)量大
5)不可讀。

三、數(shù)據(jù)庫(kù)或者Redis生成ID

當(dāng)使用數(shù)據(jù)庫(kù)來(lái)生成ID性能不夠要求的時(shí)候,我們可以嘗試使用Redis來(lái)生成ID。
數(shù)據(jù)庫(kù)方式實(shí)際上也就是建一張表保存當(dāng)前值以及步長(zhǎng),每次去更新獲取。

這主要依賴于Redis是單線程的,所以也可以用生成全局唯一的ID。可以用Redis的原子操作 INCR和INCRBY來(lái)實(shí)現(xiàn)。

可以使用Redis集群來(lái)獲取更高的吞吐量。假如一個(gè)集群中有5臺(tái)Redis??梢猿跏蓟颗_(tái)Redis的值分別是1,2,3,4,5,然后步長(zhǎng)都是5。各個(gè)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

這個(gè),隨便負(fù)載到哪個(gè)機(jī)確定好,未來(lái)很難做修改。但是3-5臺(tái)服務(wù)器基本能夠滿足器上,都可以獲得不同的ID。但是步長(zhǎng)和初始值一定需要事先需要了。使用Redis集群也可以防止單點(diǎn)故障的問(wèn)題。

另外,比較適合使用Redis來(lái)生成每天從0開始的流水號(hào)。比如訂單號(hào)=日期+當(dāng)日自增長(zhǎng)號(hào)。可以每天在Redis中生成一個(gè)Key,使用INCR進(jìn)行累加。

優(yōu)點(diǎn):
1)不依賴于數(shù)據(jù)庫(kù),靈活方便,且性能優(yōu)于數(shù)據(jù)庫(kù)。
2)數(shù)字ID天然排序,對(duì)分頁(yè)或者需要排序的結(jié)果很有幫助。
缺點(diǎn):
1)如果系統(tǒng)中沒有Redis,還需要引入新的組件,增加系統(tǒng)復(fù)雜度。
2)需要編碼和配置的工作量比較大。

四、時(shí)間戳 業(yè)務(wù)字段 隨機(jī)數(shù)組合

比如時(shí)間戳+用戶ID+隨機(jī)數(shù)就是一個(gè)很好的例子

訂單命名的幾種規(guī)則:

1、不重復(fù)。
唯一性
2、安全性。
你的訂單編號(hào)不能透露你公司的真實(shí)運(yùn)營(yíng)信息,比如你的訂單就是流水號(hào)的話,那么別人就可以從訂單號(hào)推測(cè)出你公司的整體運(yùn)營(yíng)概括了。所以訂單編碼必須是除了你們公司少部分人外,其他人基本看不懂的。參考京東和淘寶的編碼規(guī)則,基本別人是搞不清是什么意思的。
其實(shí)最好的防泄漏編碼規(guī)則就是在編碼中不要加入任何和公司運(yùn)營(yíng)的數(shù)據(jù)。
3、不能使用大規(guī)模隨機(jī)碼。
很多人分析訂單編碼規(guī)則的時(shí)候,第一個(gè)念頭肯定是不重復(fù)唯一性,那么第二個(gè)念頭可能就是安全性,那么同時(shí)滿足前兩者的第三個(gè)念頭就是隨機(jī)碼了。因?yàn)榇笠?guī)模的隨機(jī)碼隨機(jī)生成,因?yàn)楸旧砭蜎]有意義所以無(wú)所謂泄密了。但是事實(shí)上這種編碼規(guī)則在實(shí)現(xiàn)上會(huì)有很大問(wèn)題的。
隨機(jī)碼滿足第二點(diǎn)安全性要求,為了滿足第一點(diǎn)不重復(fù)特性,那就得在生成隨機(jī)碼的時(shí)候?qū)Ρ葰v史數(shù)據(jù)是否有重復(fù),如果你的訂單數(shù)量到達(dá)了十萬(wàn)次,你每次生成訂單編碼時(shí)就得對(duì)比十萬(wàn)條歷史數(shù)據(jù),你可想而知會(huì)造成什么巨大問(wèn)題。
但是難道隨機(jī)碼就不能在編碼中使用了嗎?小規(guī)模的隨機(jī)碼是可以使用的,比如2~3位,這種隨機(jī)碼一般都是和流水號(hào)等結(jié)合使用,主要作用是為了隱藏流水號(hào)的真實(shí)數(shù)據(jù)而進(jìn)行使用的。
4、防止并發(fā)。
這條規(guī)則主要針對(duì)編碼中有時(shí)間的設(shè)定。
5、控制位數(shù)。
這點(diǎn)很好理解,訂單號(hào)的作用就是便于查詢。
一般正常使用場(chǎng)景應(yīng)該是訂單出異狀或者退貨的時(shí)候,用戶將訂單號(hào)報(bào)給客服,由客服進(jìn)行查詢。
所以一般在10~15位為好。
京東10位,淘寶15位。

五、雪花算法

image.png

使用雪花算法生成的主鍵,二進(jìn)制表示形式包含4部分,從高位到低位分表為:1bit符號(hào)位、41bit時(shí)間戳位、10bit工作進(jìn)程位以及12bit序列號(hào)位。

符號(hào)位(1bit)
預(yù)留的符號(hào)位,恒為零。

時(shí)間戳位(41bit)
41位的時(shí)間戳可以容納的毫秒數(shù)是2的41次冪,一年所使用的毫秒數(shù)是:365 * 24 * 60 * 60 * 1000。通過(guò)計(jì)算可知:

Math.pow(2, 41) / (365 * 24 * 60 * 60 * 1000L);
結(jié)果約等于69.73年。ShardingSphere的雪花算法的時(shí)間紀(jì)元從2016年11月1日零點(diǎn)開始,可以使用到2086年,相信能滿足絕大部分系統(tǒng)的要求。

工作進(jìn)程位(10bit)
該標(biāo)志在Java進(jìn)程內(nèi)是唯一的,如果是分布式應(yīng)用部署應(yīng)保證每個(gè)工作進(jìn)程的id是不同的。該值默認(rèn)為0,可通過(guò)屬性設(shè)置。

序列號(hào)位(12bit)
該序列是用來(lái)在同一個(gè)毫秒內(nèi)生成不同的ID。如果在這個(gè)毫秒內(nèi)生成的數(shù)量超過(guò)4096(2的12次冪),那么生成器會(huì)等待到下個(gè)毫秒繼續(xù)生成。

問(wèn)題:
時(shí)間回?fù)軉?wèn)題:由于機(jī)器的時(shí)間是動(dòng)態(tài)的調(diào)整的,有可能會(huì)出現(xiàn)時(shí)間跑到之前幾毫秒,如果這個(gè)時(shí)候獲取到了這種時(shí)間,則會(huì)出現(xiàn)數(shù)據(jù)重復(fù)
機(jī)器id分配及回收問(wèn)題:目前機(jī)器id需要每臺(tái)機(jī)器不一樣,這樣的方式分配需要有方案進(jìn)行處理,同時(shí)也要考慮,如果改機(jī)器宕機(jī)了,對(duì)應(yīng)的workerId分配后的回收問(wèn)題
機(jī)器id上限:機(jī)器id是固定的bit,那么也就是對(duì)應(yīng)的機(jī)器個(gè)數(shù)是有上限的,在有些業(yè)務(wù)場(chǎng)景下,需要所有機(jī)器共享同一個(gè)業(yè)務(wù)空間,那么10bit表示的1024臺(tái)機(jī)器是不夠的。

時(shí)鐘回?fù)?br> 服務(wù)器時(shí)鐘回?fù)軙?huì)導(dǎo)致產(chǎn)生重復(fù)序列,因此默認(rèn)分布式主鍵生成器提供了一個(gè)最大容忍的時(shí)鐘回?fù)芎撩霐?shù)。 如果時(shí)鐘回?fù)艿臅r(shí)間超過(guò)最大容忍的毫秒數(shù)閾值,則程序報(bào)錯(cuò);如果在可容忍的范圍內(nèi),默認(rèn)分布式主鍵生成器會(huì)等待時(shí)鐘同步到最后一次主鍵生成的時(shí)間后再繼續(xù)工作。 最大容忍的時(shí)鐘回?fù)芎撩霐?shù)的默認(rèn)值為0,可通過(guò)屬性設(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)識(shí),由于long基本類型在Java中是帶符號(hào)的,最高位是符號(hào)位,正數(shù)是0,負(fù)數(shù)是1,所以id一般是正數(shù),最高位是0<br>
 * 41位時(shí)間截(毫秒級(jí)),注意,41位時(shí)間截不是存儲(chǔ)當(dāng)前時(shí)間的時(shí)間截,而是存儲(chǔ)時(shí)間截的差值(當(dāng)前時(shí)間截 - 開始時(shí)間截)
 * 得到的值),這里的的開始時(shí)間截,一般是我們的id生成器開始使用的時(shí)間,由我們程序來(lái)指定的(如下下面程序IdWorker類的startTime屬性)。41位的時(shí)間截,可以使用69年,年T = (1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69<br>
 * 10位的數(shù)據(jù)機(jī)器位,可以部署在1024個(gè)節(jié)點(diǎn),包括5位datacenterId和5位workerId<br>
 * 12位序列,毫秒內(nèi)的計(jì)數(shù),12位的計(jì)數(shù)順序號(hào)支持每個(gè)節(jié)點(diǎn)每毫秒(同一機(jī)器,同一時(shí)間截)產(chǎn)生4096個(gè)ID序號(hào)<br>
 * 加起來(lái)剛好64位,為一個(gè)Long型。<br>
 * SnowFlake的優(yōu)點(diǎn)是,整體上按照時(shí)間自增排序,并且整個(gè)分布式系統(tǒng)內(nèi)不會(huì)產(chǎn)生ID碰撞(由數(shù)據(jù)中心ID和機(jī)器ID作區(qū)分),并且效率較高,經(jīng)測(cè)試,SnowFlake每秒能夠產(chǎn)生26萬(wàn)ID左右。
 */

    // ==============================Fields===========================================
    /**
     * 開始時(shí)間截 (2018-07-03)
     */

    private final long twepoch = 1530607760000L;

    /**
     * 機(jī)器id所占的位數(shù)
     */
    private final long workerIdBits = 5L;

    /**
     * 數(shù)據(jù)標(biāo)識(shí)id所占的位數(shù)
     */
    private final long datacenterIdBits = 5L;

    /**
     * 支持的最大機(jī)器id,結(jié)果是31 (這個(gè)移位算法可以很快的計(jì)算出幾位二進(jìn)制數(shù)所能表示的最大十進(jìn)制數(shù))
     */
    private final long maxWorkerId = -1L ^ (-1L << workerIdBits);

    /**
     * 支持的最大數(shù)據(jù)標(biāo)識(shí)id,結(jié)果是31
     */
    private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);

    /**
     * 序列在id中占的位數(shù)
     */
    private final long sequenceBits = 12L;

    /**
     * 機(jī)器ID向左移12位
     */
    private final long workerIdShift = sequenceBits;

    /**
     * 數(shù)據(jù)標(biāo)識(shí)id向左移17位(12+5)
     */
    private final long datacenterIdShift = sequenceBits + workerIdBits;

    /**
     * 時(shí)間截向左移22位(5+5+12)
     */
    private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;

    /**
     * 生成序列的掩碼,這里為4095 (0b111111111111=0xfff=4095)
     */
    private final long sequenceMask = -1L ^ (-1L << sequenceBits);

    /**
     * 工作機(jī)器ID(0~31)
     */
    private long workerId;

    /**
     * 數(shù)據(jù)中心ID(0~31)
     */
    private long datacenterId;

    /**
     * 毫秒內(nèi)序列(0~4095)
     */
    private long sequence = 0L;

    /**
     * 上次生成ID的時(shí)間截
     */
    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==========================================

    /**
     * 獲得下一個(gè)ID (該方法是線程安全的)
     *
     * @return SnowflakeId
     */
    public synchronized long nextId() {
        long timestamp = timeGen();

        //如果當(dāng)前時(shí)間小于上一次ID生成的時(shí)間戳,說(shuō)明系統(tǒng)時(shí)鐘回退過(guò)這個(gè)時(shí)候應(yīng)當(dāng)拋出異常
        if (timestamp < lastTimestamp) {
            throw new RuntimeException(
                    String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
        }

        //如果是同一時(shí)間生成的,則進(jìn)行毫秒內(nèi)序列
        if (lastTimestamp == timestamp) {
            sequence = (sequence + 1) & sequenceMask;
            //毫秒內(nèi)序列溢出
            if (sequence == 0) {
                //阻塞到下一個(gè)毫秒,獲得新的時(shí)間戳
                timestamp = tilNextMillis(lastTimestamp);
            }
        }
        //時(shí)間戳改變,毫秒內(nèi)序列重置
        else {
            sequence = 0L;
        }

        //上次生成ID的時(shí)間截
        lastTimestamp = timestamp;

        //移位并通過(guò)或運(yùn)算拼到一起組成64位的ID
        return (((timestamp - twepoch) << timestampLeftShift) //
                | (datacenterId << datacenterIdShift) //
                | (workerId << workerIdShift) //
                | sequence);
    }

    /**
     * 阻塞到下一個(gè)毫秒,直到獲得新的時(shí)間戳
     *
     * @param lastTimestamp 上次生成ID的時(shí)間截
     * @return 當(dāng)前時(shí)間戳
     */
    protected long tilNextMillis(long lastTimestamp) {
        long timestamp = timeGen();
        while (timestamp <= lastTimestamp) {
            timestamp = timeGen();
        }
        return timestamp;
    }

    /**
     * 返回以毫秒為單位的當(dāng)前時(shí)間
     *
     * @return 當(dāng)前時(shí)間(毫秒)
     */
    protected long timeGen() {
        return System.currentTimeMillis();
    }

    //==============================Test=============================================

    /**
     * 測(cè)試
     */
    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——美團(tuán)點(diǎn)評(píng)分布式ID生成系統(tǒng)

七、案例

1、秒級(jí)別時(shí)間+系統(tǒng)標(biāo)識(shí)+業(yè)務(wù)標(biāo)識(shí)+版本號(hào)+固定位數(shù)的隨機(jī)數(shù)

2、秒級(jí)別時(shí)間+系統(tǒng)標(biāo)識(shí)+業(yè)務(wù)標(biāo)識(shí)+版本號(hào)+固定位數(shù)數(shù)據(jù)庫(kù)按步長(zhǎng)取數(shù)(比如取右邊六位)

3、秒或毫秒時(shí)間+用戶ID+固定位數(shù)的隨機(jī)數(shù)
一個(gè)用戶正常情況下不可能同時(shí)生成兩筆以上訂單

第2、3種方法正常來(lái)說(shuō)是絕對(duì)安全的,第1種隨機(jī)數(shù)方法最好還是數(shù)據(jù)庫(kù)加一個(gè)唯一索引,如果重復(fù)的話就重新生成一次即可。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容