高并發(fā)無(wú)鎖無(wú)IO等待分布式ID生成方案

A)

網(wǎng)絡(luò)上現(xiàn)在有很多的分布式ID生成算法, 各大廠商也開源了自己的分布式id生成算法. 前段時(shí)間項(xiàng)目里有個(gè)生成唯一id的需求, 思考了一下, 將flick的id生成方案和Twitter的id生成算法結(jié)合到一起, 寫了個(gè)小算法, 也算是站在巨人的肩膀上做了點(diǎn)小東西, lol

B)

原理大致是這樣的, 利用mysql insert來(lái)計(jì)算出集群中某個(gè)節(jié)點(diǎn)處于集群中的位置, 算出serverId, 然后利用雪花算法在該id上生成分布式id.

目前的實(shí)現(xiàn)是采用long來(lái)進(jìn)行存儲(chǔ)的, 因此只能在生成時(shí)間維度, 節(jié)點(diǎn)數(shù)量, 和每毫秒內(nèi)生成的數(shù)量上進(jìn)行調(diào)節(jié), 如果你們可以存儲(chǔ)字符串的話, 那么可以拓展一下該算法, 加大時(shí)間和空間的容量.

C)

算法實(shí)現(xiàn)

/**
 * ID 生成器
 * <p>
 * 整個(gè)ID算法很簡(jiǎn)單,
 * 1. 參考Flickr ID生成算法, 使用MYSQL獲得一個(gè)自增ID, 然后對(duì)ID取模, 算出一個(gè)服務(wù)器ID
 * 2. 參考Twitter的雪花算法, 算出一個(gè)long型ID
 * <p>
 * 該算法保證在30年內(nèi), 6萬(wàn)臺(tái)機(jī)器, 單機(jī)每秒可以產(chǎn)出128, 000個(gè)不重復(fù)ID
 * <p>
 * <p>
 * CREATE TABLE `account_server_id` (
 * `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
 * `stub` char(1) DEFAULT NULL,
 * PRIMARY KEY (`id`),
 * UNIQUE KEY `stub` (`stub`)
 * ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
 * <p>
 * <p>
 * |1, 000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0 |000, 0000, 0000, 0000, 0 |000, 0000           |
 * | |                   時(shí)間戳(40位)                                |   服務(wù)器ID(16位)         | 單個(gè)時(shí)間戳內(nèi)的Id(7位) |
 */
@Service
public class IDGeneratorService implements CommandLineRunner {

    private static final Logger LOG = LoggerFactory.getLogger(IDGeneratorService.class);

    // 時(shí)間戳從哪一年開始計(jì)時(shí)
    private static final int START_YEAR = 2018;

    // 時(shí)間取40位, 保證ID34年內(nèi)不會(huì)重復(fù)
    private static final int timeBitsSize = 40;
    private static final int serverIdBitsSize = 16;
    private static final int countBitsSize = 7;

    private long maxIdPerMill;

    // 時(shí)間開始時(shí)間戳, 相當(dāng)于System.currentTimeMillis()的1970年
    private long startDateTime;
    // 服務(wù)器ID表示位, 在集群中表示一個(gè)節(jié)點(diǎn)
    private long serverIdBits;
    // 單機(jī)中, 某個(gè)時(shí)刻生長(zhǎng)得id
    private long currentID;

    private long maxTime;

    private long lastGenerateTime = System.currentTimeMillis();
    private Object lock = new Object();

    @Resource
    private AccountServerIdMapper accountServerIdMapper;

    public void init() {
        // 1. 計(jì)算出開始生成ID的起始時(shí)間戳
        LocalDateTime start = LocalDateTime.of(START_YEAR, 1, 1, 0, 0);
        startDateTime = start.toInstant(ZoneOffset.of("+8")).toEpochMilli();

        // 2. 算出支持最大年限的時(shí)間
        maxTime = ((Double) Math.pow(2, timeBitsSize)).longValue();

        // 3. 算出每毫秒能產(chǎn)出多少ID
        maxIdPerMill = ((Double) Math.pow(2, countBitsSize)).longValue();

        /**
         * 4. 根據(jù)Mysql自增ID取模, 算出每個(gè)服務(wù)器ID, 在生產(chǎn)環(huán)境中, 應(yīng)該保證服務(wù)器數(shù)量是該值的一半, 如此一來(lái)就可以避免, 服務(wù)器集群整體
         * 重啟時(shí), 不會(huì)拿到與重啟之前的服務(wù)器相同的Id
         * 這個(gè)值的計(jì)算是為了適應(yīng)這種場(chǎng)景, 在服務(wù)器灰度上線的時(shí)候, 有可能是原來(lái)的服務(wù)器還沒有關(guān)閉, 但是新的服務(wù)器已經(jīng)起來(lái)了, 此時(shí)會(huì)有倆套
         * 服務(wù)器同時(shí)在處理業(yè)務(wù)邏輯, 那么它們就有可能拿到一樣的服務(wù)器ID, 從而導(dǎo)致產(chǎn)生一樣的ID號(hào)
         */
        long serverSize = ((Double) Math.pow(2, serverIdBitsSize)).longValue();

        AccountServerId accountServerId = new AccountServerId();
        accountServerIdMapper.nextId(accountServerId);
        long serverId = (int) (accountServerId.getId() % serverSize);

        /**
         * 5. 算出每個(gè)服務(wù)器ID在long類型中的數(shù)據(jù)位置, 然后緩存起來(lái)
         */
        serverIdBits = (serverId << (countBitsSize));

        LOG.info("[ID生成器] 開始時(shí)間:{}, 時(shí)間戳:{} ", new Date(startDateTime), startDateTime);
        LOG.info("[ID生成器] 結(jié)束時(shí)間:{}, 時(shí)間戳:{} ", new Date(startDateTime + maxTime), maxTime);
        LOG.info("[ID生成器] 每毫秒生成最大ID數(shù):{} ", maxIdPerMill);
        LOG.info("[ID生成器] 當(dāng)前serverId: {}, serverIdSize:{}", serverId, serverSize);
        LOG.info("[ID生成器] serverIdBits: {}", Long.toBinaryString(serverIdBits));
    }

    /**
     * 生成一個(gè)64位的GUID
     * <p>
     * 在next()方法中, 沒有使用任何的對(duì)象, 如此一來(lái)就可以減輕GC的壓力.
     *
     * @return
     */
    public long next() {

        synchronized (lock) {
            long curTime = System.currentTimeMillis() - startDateTime;
            if (curTime >= maxTime) {
                LOG.error("[ID生成器] 超過負(fù)載, {}, {}芬失!返回 -1", curTime, maxTime);
                return -1;
            }

            if (lastGenerateTime != curTime) {
                currentID = 0;
            } else {

                if (currentID >= maxIdPerMill) {
                    LOG.error("[ID生成器] 同一毫秒[" + curTime + "]內(nèi)生成" + currentID + "個(gè)ID监氢!返回 -1");
                    return -1;
                }

                ++currentID;
            }

            lastGenerateTime = curTime;
            long gid = (curTime << countBitsSize + serverIdBitsSize) | serverIdBits;
            gid |= currentID;

            return gid;
        }
    }

    public String nextStrId() {
        return String.valueOf(next());
    }

    public long tryNextId() {
        for (int i = 0; i < 1000; i++) {

            long start = System.currentTimeMillis();
            long id = next();
            long diff = System.currentTimeMillis() - start;
            if (diff > 3) {
                String tid = Thread.currentThread().getName();
                LOG.warn("[ID生成器] 線程{} 生成ID: {} 大于3毫秒: {}", tid, id, diff);
            }

            if (id == -1) {
                try {
//                  LOG.error("[ID生成器] 生成ID為-1, 可能超過每毫秒內(nèi)生成最大數(shù)量, 等待1毫秒");
                    TimeUnit.MILLISECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                continue;
            }
            return id;
        }
        return -1;
    }

    public String tryNextStrId() {
        return String.valueOf(tryNextId());
    }

    @Override
    public void run(String... args) throws Exception {
        init();
    }
}

mybatis

@Mapper
public interface AccountServerIdMapper {

    @Insert("REPLACE INTO server_id (stub) VALUES ('a');")
    @SelectKey(statement = "SELECT LAST_INSERT_ID()", keyProperty = "id", before = false, resultType = Long.class)
    Long nextId(AccountServerId accountServerId);

}

SQL

CREATE TABLE `server_id` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `stub` char(1) DEFAULT NULL,
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '創(chuàng)建時(shí)間',
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新時(shí)間',
  PRIMARY KEY (`id`),
  UNIQUE KEY `stub` (`stub`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

測(cè)試

@RunWith(JMockit.class)
public class IDGeneratorUtilTest {

    private static final Logger logger = LoggerFactory.getLogger(IDGeneratorUtilTest.class);

    private static final int MAX_TIMES = 2000000;
    private static final int PRINT_TIMES = 100;

    @Tested
    private IDGeneratorService idGeneratorUtil;

    @Injectable
    private AccountServerIdMapper accountServerIdMapper;

    /**
     * 21026 [main] DEBUG c.f.l.service.IDGeneratorUtilTest - 20506 毫秒內(nèi)生成 2000000 個(gè)ID
     * <p>
     * 單線程的情況下, 在MacBook Pro上是每毫秒鐘生成 97 個(gè)id
     */
    @Test
    public void testOneServerIdGenerate() {
        new Expectations() {
            {
                accountServerIdMapper.nextId((AccountServerId) any);
                result = 2;
            }
        };
        idGeneratorUtil.init();

        Set<Long> ids = new HashSet<>();

        long start = System.currentTimeMillis();

        for (int i = 0; i < MAX_TIMES; i++) {
            long id = idGeneratorUtil.tryNextId();
            if (ids.contains(id)) {
                System.out.println(id);
            }
            ids.add(id);
        }
        logger.debug((System.currentTimeMillis() - start) + " 毫秒內(nèi)生成 " + ids.size() + " 個(gè)ID");
        Assert.assertEquals(ids.size(), MAX_TIMES);

        Object[] idArray = ids.toArray();
        for (int i = 0; i < PRINT_TIMES; i++) {
            logger.debug(idArray[i] + " : " + Long.toBinaryString((Long) idArray[i]));
        }
    }

    /**
     * 207703 [Thread-7] DEBUG c.f.l.service.IDGeneratorUtilTest - 207136 毫秒內(nèi)生成 2000000 個(gè)ID
     * 208031 [Thread-3] DEBUG c.f.l.service.IDGeneratorUtilTest - 207465 毫秒內(nèi)生成 2000000 個(gè)ID
     * 208626 [Thread-10] DEBUG c.f.l.service.IDGeneratorUtilTest - 208059 毫秒內(nèi)生成 2000000 個(gè)ID
     * 208630 [Thread-9] DEBUG c.f.l.service.IDGeneratorUtilTest - 208063 毫秒內(nèi)生成 2000000 個(gè)ID
     * 209153 [Thread-6] DEBUG c.f.l.service.IDGeneratorUtilTest - 208586 毫秒內(nèi)生成 2000000 個(gè)ID
     * 209170 [Thread-5] DEBUG c.f.l.service.IDGeneratorUtilTest - 208603 毫秒內(nèi)生成 2000000 個(gè)ID
     * 209373 [Thread-2] DEBUG c.f.l.service.IDGeneratorUtilTest - 208807 毫秒內(nèi)生成 2000000 個(gè)ID
     * 209412 [Thread-1] DEBUG c.f.l.service.IDGeneratorUtilTest - 208846 毫秒內(nèi)生成 2000000 個(gè)ID
     * 209508 [Thread-4] DEBUG c.f.l.service.IDGeneratorUtilTest - 208941 毫秒內(nèi)生成 2000000 個(gè)ID
     * 209536 [Thread-8] DEBUG c.f.l.service.IDGeneratorUtilTest - 208969 毫秒內(nèi)生成 2000000 個(gè)ID
     * <p>
     * 多線程的情況下, 在MacBook Pro上是每毫秒鐘生成 9 個(gè)id, 可見由于鎖的競(jìng)爭(zhēng), 產(chǎn)生的影響還是非常大的
     */
    @Test
    public void testMutilServerIdGenerate() {
        new Expectations() {
            {
                accountServerIdMapper.nextId((AccountServerId) any);
                result = 2;
            }
        };
        idGeneratorUtil.init();

        Runnable runnable = () -> {
            Set<Long> ids = new HashSet<>();

            long start = System.currentTimeMillis();

            for (int i = 0; i < MAX_TIMES; i++) {
                long id = idGeneratorUtil.tryNextId();
                ids.add(id);
            }
            logger.debug((System.currentTimeMillis() - start) + " 毫秒內(nèi)生成 " + ids.size() + " 個(gè)ID");
            Assert.assertEquals(ids.size(), MAX_TIMES);
        };

        List<Thread> list = new ArrayList<>();
        int cpus = Runtime.getRuntime().availableProcessors() + 2;
        logger.debug("CPU : " + cpus);

        for (int i = 0; i < cpus; i++) {
            Thread thread = new Thread(runnable);
            list.add(thread);
            thread.start();
        }

        for (Thread thread : list) {
            try {
                thread.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末字支,一起剝皮案震驚了整個(gè)濱河市鸦概,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌阎肝,老刑警劉巖挤渔,帶你破解...
    沈念sama閱讀 211,265評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異风题,居然都是意外死亡判导,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,078評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門沛硅,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)眼刃,“玉大人,你說我怎么就攤上這事摇肌±藓欤” “怎么了?”我有些...
    開封第一講書人閱讀 156,852評(píng)論 0 347
  • 文/不壞的土叔 我叫張陵围小,是天一觀的道長(zhǎng)昵骤。 經(jīng)常有香客問我,道長(zhǎng)肯适,這世上最難降的妖魔是什么变秦? 我笑而不...
    開封第一講書人閱讀 56,408評(píng)論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮框舔,結(jié)果婚禮上蹦玫,老公的妹妹穿的比我還像新娘。我一直安慰自己刘绣,他們只是感情好樱溉,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,445評(píng)論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著纬凤,像睡著了一般福贞。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上移斩,一...
    開封第一講書人閱讀 49,772評(píng)論 1 290
  • 那天肚医,我揣著相機(jī)與錄音绢馍,去河邊找鬼向瓷。 笑死,一個(gè)胖子當(dāng)著我的面吹牛舰涌,可吹牛的內(nèi)容都是我干的猖任。 我是一名探鬼主播,決...
    沈念sama閱讀 38,921評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼瓷耙,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼朱躺!你這毒婦竟也來(lái)了刁赖?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,688評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤长搀,失蹤者是張志新(化名)和其女友劉穎宇弛,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體源请,經(jīng)...
    沈念sama閱讀 44,130評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡枪芒,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,467評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了谁尸。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片舅踪。...
    茶點(diǎn)故事閱讀 38,617評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖良蛮,靈堂內(nèi)的尸體忽然破棺而出抽碌,到底是詐尸還是另有隱情,我是刑警寧澤决瞳,帶...
    沈念sama閱讀 34,276評(píng)論 4 329
  • 正文 年R本政府宣布货徙,位于F島的核電站,受9級(jí)特大地震影響皮胡,放射性物質(zhì)發(fā)生泄漏破婆。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,882評(píng)論 3 312
  • 文/蒙蒙 一胸囱、第九天 我趴在偏房一處隱蔽的房頂上張望祷舀。 院中可真熱鬧,春花似錦烹笔、人聲如沸裳扯。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,740評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)饰豺。三九已至,卻和暖如春允蜈,著一層夾襖步出監(jiān)牢的瞬間冤吨,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,967評(píng)論 1 265
  • 我被黑心中介騙來(lái)泰國(guó)打工饶套, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留漩蟆,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,315評(píng)論 2 360
  • 正文 我出身青樓妓蛮,卻偏偏與公主長(zhǎng)得像怠李,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,486評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容

  • 文章轉(zhuǎn)載自公眾號(hào)“達(dá)達(dá)京東到家技術(shù)”捺癞。 背景 在分布式系統(tǒng)中夷蚊,經(jīng)常需要對(duì)大量的數(shù)據(jù)、消息髓介、http 請(qǐng)求等進(jìn)行唯一...
    淡淡的橙子閱讀 5,907評(píng)論 1 41
  • 一惕鼓,題記 所有的業(yè)務(wù)系統(tǒng),都有生成ID的需求唐础,如訂單id呜笑,商品id,文章ID等彻犁。這個(gè)ID會(huì)是數(shù)據(jù)庫(kù)中的唯一主鍵叫胁,在...
    eonhu閱讀 9,035評(píng)論 0 8
  • 轉(zhuǎn)載:細(xì)聊分布式ID生成方法 一、需求緣起 幾乎所有的業(yè)務(wù)系統(tǒng)汞幢,都有生成一個(gè)記錄標(biāo)識(shí)的需求驼鹅,例如: (1)消息標(biāo)識(shí)...
    meng_philip123閱讀 2,559評(píng)論 0 17
  • 我終于結(jié)束了我所有的學(xué)業(yè)仲智,踏上了生活工作的旅途…… 這一年我不免緬懷上學(xué)的日子多么好视搏,懷念一群同齡人無(wú)憂無(wú)慮的日子...
    李秋會(huì)_閱讀 461評(píng)論 4 2
  • 目標(biāo):不停追求(卓)漫仆、不斷翱翔(菲) 雨——臺(tái)風(fēng)“卡努”掸宛,今年臺(tái)風(fēng)好愛廣東啊——之前是“天鴿”考传、“帕卡”、“瑪娃”...
    逆風(fēng)追夢(mèng)人閱讀 161評(píng)論 0 0