分布式系統(tǒng)唯一性ID生成策略思考

使用分布式系統(tǒng)架構(gòu)面臨全局ID的生成策略的抉擇,本文描述了筆者所構(gòu)想的一個(gè)優(yōu)化方案,歡迎拍磚!

多種ID生成方式


1. UUID

算法的核心思想是結(jié)合機(jī)器的網(wǎng)卡、當(dāng)?shù)貢r(shí)間、一個(gè)隨記數(shù)來(lái)生成UUID。

  • 優(yōu)點(diǎn):本地生成,性能好,沒(méi)有高可用風(fēng)險(xiǎn);
  • 缺點(diǎn):長(zhǎng)度過(guò)長(zhǎng),且無(wú)序

2. 數(shù)據(jù)庫(kù)sequence

使用數(shù)據(jù)庫(kù)的id自增策略,如MySQL的auto_increment。并且可以使用兩臺(tái)數(shù)據(jù)庫(kù)分別設(shè)置不同步長(zhǎng),生成不重復(fù)ID的策略來(lái)實(shí)現(xiàn)高可用。

  • 優(yōu)點(diǎn):數(shù)據(jù)庫(kù)生成的ID絕對(duì)有序,高可用實(shí)現(xiàn)方式簡(jiǎn)單。
  • 缺點(diǎn):需要獨(dú)立部署數(shù)據(jù)庫(kù)實(shí)例,成本高,有性能瓶頸

3.雪花算法

twitter生成全局ID生成器的算法策略。

簡(jiǎn)單來(lái)說(shuō):就是把64的Long型數(shù)據(jù)由以下幾個(gè)部分組成:
符號(hào)位(1位)-時(shí)間戳(41位)-數(shù)據(jù)中心標(biāo)識(shí)(5位)-ID生成器實(shí)例標(biāo)識(shí)(5位)-序列號(hào)(12位)
通過(guò)部署多個(gè)ID生成器,位各個(gè)業(yè)務(wù)系統(tǒng)生成全局唯一的Long型ID。

  • 優(yōu)點(diǎn):生成Long型易操作,有序
  • 缺點(diǎn):需要獨(dú)立部署id生成器,增加維護(hù)成本

4.MongoDB ObjectId

生成策略類似雪花算法。

時(shí)間戳+機(jī)器ID+進(jìn)程ID+序列號(hào)=>ObjectId對(duì)象

  • 優(yōu)點(diǎn):本地生成,有序,成本低
  • 缺點(diǎn):使用機(jī)器ID和進(jìn)程ID,64位Long無(wú)法存儲(chǔ),只能生成特殊ObjectId對(duì)象。

總結(jié)對(duì)比

方式 優(yōu)點(diǎn) 缺點(diǎn)
UUID 本地生成,無(wú)中心,無(wú)性能瓶頸 無(wú)序,過(guò)長(zhǎng)
MongoDB ObjectId 本地生成,含時(shí)間戳,有序 過(guò)長(zhǎng)
數(shù)據(jù)庫(kù)sequence 有序 中心生成,獨(dú)立部署數(shù)據(jù)庫(kù)
雪花算法 有序,Long型 中心生成,獨(dú)立部署ID生成器

大致的總結(jié)優(yōu)略點(diǎn)如下:

方式 優(yōu)點(diǎn) 缺點(diǎn)
UUID 本地生成,無(wú)中心,無(wú)性能瓶頸 無(wú)序,過(guò)長(zhǎng)
MongoDB ObjectId 本地生成,含時(shí)間戳,有序 過(guò)長(zhǎng)
數(shù)據(jù)庫(kù)sequence 有序 中心生成,獨(dú)立部署數(shù)據(jù)庫(kù)
雪花算法 有序,Long型 中心生成,獨(dú)立部署ID生成器

想要的

看了以上,我想要的ID生成策略是怎樣的呢?
64位易操作存儲(chǔ),按時(shí)間有序,無(wú)中心本地生成。
好吧,其實(shí)本文也沒(méi)有完全實(shí)現(xiàn)以上需求,如果哪位小伙伴有更好方案歡迎回復(fù)分享?。?!
本文只是基于對(duì)以上幾種方案的認(rèn)識(shí),稍加改進(jìn),盡可能的滿足需求!

來(lái)吧


我的想法:

使用Long型,不可避免參考雪花算法的實(shí)現(xiàn),但是要實(shí)現(xiàn)本地化生成,要參考ObjectId的生成策略,使用類似機(jī)器ID,進(jìn)程ID來(lái)保證唯一性。

如何解決使用機(jī)器ID,進(jìn)程ID時(shí)導(dǎo)致ID過(guò)長(zhǎng)的問(wèn)題?

解決方式:放棄使用機(jī)器ID,進(jìn)程ID,使用serverId標(biāo)識(shí)服務(wù),使用instanceId標(biāo)識(shí)服務(wù)進(jìn)程,但是。。。沒(méi)辦法,需要一個(gè)中心來(lái)進(jìn)行注冊(cè),保證唯一性,本例中使用Redis(不限于redis,database,memcached都可)。

  • 相對(duì)于使用獨(dú)立部署的ID生成器,我想Redis之類的緩存集群是各個(gè)分布式系統(tǒng)架構(gòu)中都會(huì)存在的,這樣可以顯著降低架構(gòu)復(fù)雜度,降低成本。
  • 對(duì)redis的依賴較低,可以說(shuō)只需要啟動(dòng)的時(shí)候訪問(wèn)redis即可,后續(xù)本地生成ID。
  • 另外serverId是固定不變的,是可以預(yù)先分配好的,比如會(huì)員中心微服務(wù)的serverId分配為10,這是固定不變的。

大致實(shí)現(xiàn)

類似雪花算法的參數(shù)

public class IdGenerator {

    // 時(shí)間基線  2016/1/1
    private final long timeBaseLine = 1454315864414L;

    // 服務(wù)編號(hào)
    private volatile long serverId = -1;

    //服務(wù)實(shí)例編號(hào)
    private volatile long instanceId = -1;
    
    private static final long serverIdBits = 7;
    private static final long instanceIdBits = 10;
    private static final long sequenceBits = 5;
    ...
}
  • serverIdBits = 7,最多支持128個(gè)服務(wù)。
  • instanceIdBits = 10, 每個(gè)服務(wù)支持1024個(gè)實(shí)例。
  • sequenceBits = 5,每毫秒可生成的id數(shù)量32個(gè)(由于本地生成,且時(shí)間戳精確到毫秒,同一毫秒內(nèi)的ID沖突不會(huì)像中心那么嚴(yán)重。)

簡(jiǎn)單流程

服務(wù)A請(qǐng)求redis分配instanceId

主要方法:

 /**
     * 應(yīng)用啟動(dòng)完成后調(diào)用init
     *
     * @param serverId
     */
    public synchronized void init(long serverId) {
        this.serverId = serverId;
        if (!inited) {
            inited = true;
            Jedis jedis = new Jedis("localhost", 6379);
            ScheduledExecutorService scheduledService = Executors.newScheduledThreadPool(1);
            RegisterIdCreatorInstanceTask registerIdCreatorInstanceTask = new RegisterIdCreatorInstanceTask(jedis);
            // 定義定時(shí)任務(wù),定期調(diào)用redis注冊(cè),續(xù)約instanceId
            scheduledService.scheduleWithFixedDelay(registerIdCreatorInstanceTask, 0, RegisterIdCreatorInstanceTask.INTERVAL_SECONDS, TimeUnit.SECONDS);
        } else {
            System.out.println("已經(jīng)初始化!");
        }
    }
    
    /**
     * 注冊(cè)id生成器實(shí)例
     */
    private class RegisterIdCreatorInstanceTask implements Runnable {
        private Logger logger = Logger.getLogger(RegisterIdCreatorInstanceTask.class.getCanonicalName());

        public static final int INTERVAL_SECONDS = 30;

        private Jedis jedis;

        private RegisterIdCreatorInstanceTask(Jedis jedis) {
            this.jedis = jedis;
        }

        public void run() {

            try {

                long srvId = idGenerator.getServerId();
                long currentInstanceId = idGenerator.getInstanceId();

                String prefixKey = ID_CREATOR_KEY + KEY_SEP + srvId + KEY_SEP;

                if (currentInstanceId < 0) {
                    //注冊(cè)
                    registerInstanceIdWithIpv4();
                } else {
                    //續(xù)約
                    String result = jedis.set(prefixKey + currentInstanceId, srvId + KEY_SEP + currentInstanceId, "XX", "EX", INTERVAL_SECONDS * 3);
                    if (!"OK".equals(result)) {
                        logger.warning("服務(wù)[" + srvId + "]ID生成器:" + currentInstanceId + "續(xù)約失敗,等待重新注冊(cè)");
                        registerInstanceIdWithIpv4();
                    } else {
                        logger.info("服務(wù)[" + srvId + "]ID生成器:" + currentInstanceId + "續(xù)約成功");
                    }

                }

            } catch (JedisException e) {
                logger.severe("Redis 出現(xiàn)異常!");
                e.printStackTrace();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                if (idGenerator.getInstanceId() < 0) {
                    idGenerator.setInited(false);
                }
                if (jedis != null) {
                    jedis.close();
                }
            }
        }

        private int registerInstanceIdWithIpv4() {
            long ip4Value = getIp4LongValue();
            // Redis key 格式:key->val , ID_CREATOR:serverId:instanceId -> serverId:instanceId
            String prefixKey = ID_CREATOR_KEY + KEY_SEP + serverId + KEY_SEP;

            // 需要使用java8
            int regInstanceId = registerInstanceId((int) (ip4Value % (maxInstanceId + 1)), (int) maxInstanceId, (v) -> {
                String res = jedis.set(prefixKey + v, serverId + KEY_SEP + v, "NX", "EX", INTERVAL_SECONDS * 3);
                return "OK".equals(res) ? v : -1;
            });

            idGenerator.setInstanceId(regInstanceId);
            idGenerator.setInited(true);

            logger.info("服務(wù)[" + serverId + "]注冊(cè)了一個(gè)ID生成器:" + regInstanceId);
            return regInstanceId;
        }


        /**
         * 注冊(cè)instance,成功就返回
         *
         * @param basePoint
         * @param max
         * @param action
         * @return
         */
        public int registerInstanceId(int basePoint, int max, Function<Integer, Integer> action) {
            int result;
            for (int i = basePoint; i <= max; i++) {
                result = action.apply(i);
                if (result > -1) {
                    return result;
                }
            }

            for (int i = 0; i < basePoint; i++) {
                result = action.apply(i);
                if (result > -1) {
                    return result;
                }
            }
            return 0;
        }

        /**
         * IPV4地址轉(zhuǎn)Long
         *
         * @return
         */
        private long getIp4LongValue() {
            try {
                InetAddress inetAddress = Inet4Address.getLocalHost();
                byte[] ip = inetAddress.getAddress();

                return Math.abs(((0L | ip[0]) << 24)
                        | ((0L | ip[1]) << 16)
                        | ((0L | ip[2]) << 8)
                        | (0L | ip[3]));

            } catch (Exception ex) {
                ex.printStackTrace();
                return 0;
            }
        }
    }
    
/**
     * 獲取ID
     *
     * @return
     */
    public long getId() {
        long id = nextId();
        return id;
    }

    private synchronized long nextId() {
        if (serverId < 0 || instanceId < 0) {
            throw new IllegalArgumentException("目前不能生成唯一性ID,serverId:[" + serverId + "],instanceId:[" + instanceId + "]!");
        }

        long timestamp = currentTime();
        if (timestamp < lastTimestamp) {
            throw new IllegalStateException("Err clock");
        }
        sequence = (sequence + 1) & maxSequence;
        if (lastTimestamp == timestamp) {
            if (sequence == 0) {
                timestamp = tilNextMillis(lastTimestamp);
            }
        }
        lastTimestamp = timestamp;

        long id = ((timestamp - timeBaseLine) << timeBitsShift)
                | (serverId << serverIdBitsShift)
                | (instanceId << instanceIdBitsShift)
                | sequence;
        return id;
    }

如有問(wèn)題歡迎指正??!

如有其他方案,歡迎回復(fù)告知,謝謝?。?!

源碼 https://github.com/darren-fu/IdGenerator

最后編輯于
?著作權(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)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Entrance 數(shù)據(jù)在分片時(shí),典型的是分庫(kù)分表,就有一個(gè)全局ID生成的問(wèn)題。 單純的生成全局ID并不是什么難題,...
    天下無(wú)敵強(qiáng)閱讀 2,313評(píng)論 2 5
  • Spring Cloud為開(kāi)發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見(jiàn)模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,545評(píng)論 19 139
  • 一,題記 所有的業(yè)務(wù)系統(tǒng),都有生成ID的需求,如訂單id,商品id,文章ID等。這個(gè)ID會(huì)是數(shù)據(jù)庫(kù)中的唯一主鍵,在...
    架構(gòu)師小秘圈閱讀 4,139評(píng)論 1 18
  • 最近在proxy的分庫(kù)分表,需要給表中的主字段產(chǎn)生一個(gè)全局唯一ID,考慮到后期DBA會(huì)拿這個(gè)ID做索引的,所以產(chǎn)生...
    luomoxyz閱讀 3,378評(píng)論 0 4
  • 1、當(dāng)兵后悔兩年,不當(dāng)兵后悔一輩子! 吾思:英雄需要用簡(jiǎn)短有力的言辭來(lái)激勵(lì)和體現(xiàn),男子漢一點(diǎn),做熱血的事業(yè),亦可抱...
    LO晨歆VE閱讀 433評(píng)論 0 1

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