小型分佈式微服務Long型ID生成器

在微服務開發中,ID生成一直是一個十分頭疼的問題,網上常見的方式有三種:

  1. 基於數據庫的主鍵自增
  2. 基於UUID
  3. 雪花算法

對於第1種基於數據庫主鍵自增的,當數據量比較少時無所謂,但是當數據量很大需要進行分表分庫是擴展時,對於ID的處理就會出現各種麻煩

對於第2種基於UUID的主鍵,理論上不會出現問題,但是由於UUID是字符串形式,在Mysql數據庫中對於字符串的查詢沒有數值型的效率高

對於第3種基於雪花算法的ID生成器,這種方式也是比較主流的,各大廠的ID生成器也是在此基礎之上進行優化改造後實現的。但是由於雪花算法生成的ID是基於64位Long型數值產生的,傳遞給前端時會出現精度丟失的問題。

本文介紹的方式是在雪花算法的基礎之上對ID長度進行調整後生成的, *適用於TPS在100W左右的應用。爲了保證返回給前端的ID不會出現溢出,返回給前端的數值不超過9007199254740991,即二進制的53位全1數值,其中使用前40位用作時間戳位,能夠保證在35年不會重複,3位作爲機器位,即最多允許部署8臺機器,對於小型服務基本夠用,後10位作爲序號位,允許每毫秒最多產生1024個ID。

由於是適用於小型企業的,故不設置數據中心節點。可以自己定製註冊中心進行工作機器註冊。


import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;

/**
 * 適用於TPS在100W左右的應用
 * 爲了保證返回給前端的ID不會出現溢出,返回給前端的整型不能超過15位
 * 後端限制最高返回值是53位二進制的全1
 * 使用40位用作時間戳位,能夠保證在35年不會重複
 * 3位作爲機器位,即最多允許部署8臺機器,對於小型服務基本夠用
 * 10位作爲序號位,允許每毫秒最多產生1024個ID
 *
 * @author: LiuMeng
 * @date: 2019/11/19
 * TODO:
 */
@Slf4j
public class Sequence {

    /**
     * 時間起始標記點,作爲基準,一般取系統的最近時間(一旦確定不能變動)
     * 2019-11-20 00:00:00
     */
    private final long edenPoint = 1574179200000L;
    private final long workerIdBits = 3L;
    private final long maxWorkerId = -1L ^ (-1L << workerIdBits);
    private final long sequenceBits = 10L;
    private final long workerIdShift = sequenceBits;
    private final long timestampLeftShift = sequenceBits + workerIdBits;
    private final long sequenceMask = -1L ^ (-1L << sequenceBits);
    private final long workerId;
    private final long idBits = 53;
    private final long maxLimit = -1L ^ (-1L << idBits);
    private long sequence = 0L;
    private long lastTimestamp = -1L;
    private final ISequenceAdapter sequenceAdapter;
    private static Sequence instance;

    /**
     * 有參構造器
     */
    private Sequence(ISequenceAdapter sequenceAdapter) {
        this.sequenceAdapter = sequenceAdapter;
        long nextWorkId = this.sequenceAdapter.getNextWorkId();
        if (nextWorkId > maxWorkerId) {
            throw new IllegalStateException("The work Id has reached the maximum");
        }
        this.workerId = nextWorkId;
        this.startSequenceIdRegisterScheduled();
        instance = this;
    }

    /**
     * 獲取全局對象
     *
     * @return
     */
    public static Sequence getInstance(ISequenceAdapter sequenceAdapter) {
        if (null == instance) {
            synchronized (Sequence.class) {
                if (null == instance) {
                    instance = new Sequence(sequenceAdapter);
                }
            }
        }
        return instance;
    }

    /**
     * 獲取全局對象
     *
     * @return
     */
    public static Sequence getInstance() throws SequenceInstanceException {
        if (null == instance) {
            throw new SequenceInstanceException("the sequence is not instance, you can use [getInstance(ISequenceAdapter)] to get or init.");
        }
        return instance;
    }

    public synchronized long nextId() {
        long timestamp = timeGen();
        if (timestamp < lastTimestamp) {
            long offset = lastTimestamp - timestamp;
            if (offset <= 5) {
                try {
                    wait(offset << 1);
                    timestamp = timeGen();
                    if (timestamp < lastTimestamp) {
                        throw new RuntimeException(String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds", offset));
                    }
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            } else {
                throw new RuntimeException(String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds", offset));
            }
        }
        if (lastTimestamp == timestamp) {
            sequence = (sequence + 1) & sequenceMask;
            if (sequence == 0) {
                timestamp = tilNextMillis(lastTimestamp);
            }
        } else {
            sequence = ThreadLocalRandom.current().nextLong(1, 3);
        }
        lastTimestamp = timestamp;
        // 時間戳部分 | 機器標識部分 | 序列號部分
        return (((timestamp - edenPoint) << timestampLeftShift)
                | (workerId << workerIdShift)
                | sequence) & maxLimit;
    }

    /**
     * 同一毫秒內Id數量達到最大,等待下一毫秒
     *
     * @param lastTimestamp
     * @return
     */
    private long tilNextMillis(long lastTimestamp) {
        long timestamp = timeGen();
        //自旋獲取下毫秒
        while (timestamp <= lastTimestamp) {
            timestamp = timeGen();
        }
        return timestamp;
    }

    private long timeGen() {
        return sequenceAdapter.getTimestamp();
    }

    private void startSequenceIdRegisterScheduled() {
        ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(runnable -> {
            Thread thread = new Thread(runnable, "Sequence-Register-Schedule");
            thread.setDaemon(true);
            return thread;
        });
        scheduler.scheduleAtFixedRate(() -> this.sequenceAdapter.register(this.workerId), 1, 1, TimeUnit.MINUTES);
    }
}

/**
 * @author: LiuMeng
 * @date: 2019/11/20
 * TODO:
 */
public interface ISequenceAdapter {

    /**
     * 註冊服務的工作ID,每分鐘一次
     * @param workId
     */
    void register(long workId);

    /**
     * 獲取服務的下一個工作Id
     */
    long getNextWorkId();

    /**
     * 獲取當前的時間戳
     * @return
     */
    long getTimestamp();
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章