基于数据库的分布式ID生成器

背景

随着互联网的高速发展,企业系统采集的数据呈指数式上升,集成的业务也越来越多。于是,以分布式微服务为基础的项目架构逐渐走进人们的视野,在分布式项目中,对大量的数据、消息、http请求等要有唯一标识这时,传统的数据库自增主键已经不能满我们的需求。

  • 全局唯一:不能出现重复ID。
  • 高可用:ID生成系统是基础系统,被许多关键系统调用,一旦宕机,会造成严重影响。

以上两个特性,是分布式ID最基本的要求。

基于数据库的分布式ID

除了具备全局唯一性、高可用的特点外,还具备轻量级,不依赖第三方;高性能,与内存结合使用,减少了与数据库的交互;ID规则灵活,可扩展。

数据库:

核心代码:
public class EntityIdHandler {

    private final ReentrantLock lock = new ReentrantLock();
    private static final int RETRY_COUNT = 5;
    /**最后生成的id*/
    private String lastGeneratedId = "";
    /**主键,单据类型*/
    private String idCode = "";
    /**固定前缀*/
    private String fixPrefix = "";
    /**是否包含业务前缀*/
    private boolean includeBizPrefix = false;
    /**日期前缀*/
    private String datePrefix = "";
    /**日期格式*/
    private String datePattern = "";
    /**数字部分位数*/
    private int numDigit = 0;
    /**最大键值*/
    private long maxValue = 0;
    /**下一个键值*/
    private long nextValule = 0;

    private EntityIdConfService entityIdConfService;

    public EntityIdHandler(String idCode, EntityIdConfService entityIdConfService) {
        this.idCode = idCode.trim().toLowerCase();
        this.entityIdConfService = entityIdConfService;
        retrieveFromDB(RETRY_COUNT);
    }

    /**
     * 更新数据库,刷新内存池。数据库中要记录两个值:nextValue,datePrefix,其中datePrefix只是表示当前的,并不是与nextValue对应,nextValue是未来使用值。
     * 数据库要记录最后一次被更新时对应的日期前缀,以便当服务崩溃重启时,用它来和当前日期比较,判断数据库的nextValue是否回归poolSize+1。
     * 如果是日期前缀变更,更新数据库nextValue为pooSize+1。数据库中管理的只是当前日期前缀段内的递增数量;
     * 每次服务重启,要查询数据库中的日期前缀是否已过时,如果已过时,则准备更新数据库日期为当前。
     * 如果是第一次初始化即系统第一次投入使用,置数据库NextBatchStartValue值为1,如果不是第一次启动,置数据库nextBatchStartValue=NextBatchStartValue+poolSize,
     * 因此也有可能最多浪费一个poolsize的id,作为内存未和数据库及时通信的损失。
     */
    private void retrieveFromDB(int retryCount) {
        boolean success = false;
        if (retryCount == 0) {return;}
        try {
            EntityIdConf entityIdConf = entityIdConfService.selectById(idCode);
            String tempDatePrefix = getTempDatePrefix(entityIdConf.getDatePattern());
            //如果日期前缀发生变更
            if (!tempDatePrefix.trim().equalsIgnoreCase("") &&
                    !tempDatePrefix.equalsIgnoreCase(entityIdConf.getDatePrefix())) {
                entityIdConf.setDatePrefix(tempDatePrefix);
                entityIdConf.setNextBatchStartValue(entityIdConf.getPoolSize() + 1L);
            } else {
                //如果日期前缀没有变化,则增加步长,(在服务重启时这将损失id资源)
                entityIdConf.setNextBatchStartValue(entityIdConf.getNextBatchStartValue() + entityIdConf.getPoolSize());
            }
            entityIdConf.setLastGeneratedId(this.lastGeneratedId);
            //先更新数据库,再更新内存。
            if (entityIdConfService.updateById(entityIdConf)) {
                updateMemoryFieldFromDB(entityIdConf);
            }
            success = true;
        } catch (Exception e) {
            e.printStackTrace();
        }

        if (!success) {
            // Call this method again, but sleep briefly to try to avoid thread contention.
            try {
                Thread.sleep(75);
            } catch (InterruptedException ie) {
                ie.printStackTrace();
            }
            retrieveFromDB(retryCount - 1);
        }
    }

    /**
     * 根据数据库更新内存值
     */
    private void updateMemoryFieldFromDB(EntityIdConf entityIdConf) {
        if (entityIdConf != null) {
            this.fixPrefix = entityIdConf.getFixPrefix() == null ? "".trim() : entityIdConf.getFixPrefix().trim();
            this.numDigit = entityIdConf.getNumDigit();
            this.includeBizPrefix = entityIdConf.getIncludeBizPrefix() == null ? Boolean.FALSE : entityIdConf.getIncludeBizPrefix();
            this.datePattern = entityIdConf.getDatePattern() == null ? "".trim() : entityIdConf.getDatePattern().trim();
            this.datePrefix = entityIdConf.getDatePrefix() == null ? "" : entityIdConf.getDatePrefix().trim();
            this.maxValue = entityIdConf.getNextBatchStartValue() - 1L;
            this.nextValule = entityIdConf.getNextBatchStartValue() - entityIdConf.getPoolSize();
        }
    }

    /**
     * 获取单个ID
     * @return
     */
    public String getNextEntityId(String bizCode) {
        lock.lock();
        try {
            String stringValue = getNextStringValue();
            String nextEntityId = this.fixPrefix.trim()
                    + (this.includeBizPrefix ? bizCode : "")
                    + this.datePrefix.trim()
                    + stringValue;
            this.lastGeneratedId = nextEntityId;
            return nextEntityId;
        } finally {
            lock.unlock();
        }
    }

    /**
     * 批量获取ID
     * @return
     */
    public List getNextEntityIds(String bizCode, Integer reqCount) {
        lock.lock();
        try {
            List entityIds = new ArrayList<>();
            for (int i = 0; i < reqCount; i++) {
                entityIds.add(getNextEntityId(bizCode));
            }
            return entityIds;
        } finally {
            lock.unlock();
        }
    }


    public String getNextStringValue() {
        //如果日期前缀变化
        String tempDatePrefix = getTempDatePrefix(this.datePattern);
        if (!tempDatePrefix.trim().equalsIgnoreCase("") &&
                !tempDatePrefix.equalsIgnoreCase(this.datePrefix)) {
            retrieveFromDB(RETRY_COUNT);
        }
        //如果内存id资源耗光,需要同步数据库
        if (nextValule > maxValue) {
            retrieveFromDB(RETRY_COUNT);
        }
        //内存id正常增长
        if (nextValule <= maxValue) {
            nextValule += nextValule;
        } else {
            nextValule = -1L;
        }
        String nextVal = Long.toString(nextValule);
        return StringUtils.leftPad(nextVal, this.numDigit, "0");
    }

    private String getTempDatePrefix(String datePattern) {
        if (datePattern.trim().equalsIgnoreCase("")) {return "";}
        LocalDate localDate = LocalDate.now();
        return localDate.format(DateTimeFormatter.ofPattern(datePattern));
    }

}

以上是实体类ID生成器的主要核心,原理是:开始获取下一个ID -> 判断日期是否变化 -> 是,将内存的下一个值(nextBatchStartValue)置为1,重新开始计数,更新内存,更新数据库 -> 否,判断下一个值是否超出内存最大键值 -> 是,更新nextBatchStartValue+=poolSize,同步数据库 -> 否,正常增长。

以下是测试单机qps和多线程环境下ID唯一性的测试代码:



你可能感兴趣的:(基于数据库的分布式ID生成器)