基于数据库的分布式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<String> getNextEntityIds(String bizCode, Integer reqCount) {
        lock.lock();
        try {
            List<String> 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唯一性的测试代码:



©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容