分布式id生成器

概述

网上关于分布式id生成器的文章已经很多了,本文章主要是想介绍下之前设计和开发的两种分布式id生成器。具体背景和其他生成器的优劣不会着重介绍。先简单说下分布式id的需求(载自新美大Leaf方案技术博客,下面会附上链接):

1.全局唯一性:不能出现重复的ID号,既然是唯一标识,这是最基本的要求。

2.趋势递增:在MySQL InnoDB引擎中使用的是聚集索引,由于多数RDBMS使用B-tree的数据结构来存储索引数据,在主键的选择上面我们应该尽量使用有序的主键保证写入性能(避免索引页分裂导致的性能问题)。

3.单调递增:保证下一个ID一定大于上一个ID,例如事务版本号、IM增量消息、排序等特殊需求。

4.信息安全:如果ID是连续的,恶意用户的扒取工作就非常容易做了,直接按照顺序下载指定URL即可;如果是订单号就更危险了,竞对可以直接知道我们一天的单量。所以在一些应用场景下,会需要ID无规则、不规则。

笔者当时接到的需求是订单id和凭证id的生成。凭证id分为两部分:六位取票码和六位验证码(都是字母和数字的结合)。关于其他方案的调研请参考新美大Leaf系统的技术博客,写的比我好。先说下订单id生成器方案,订单id生成器是基于snowflake改造的。

基于snowflake的分布式id生成器

说到分布式id生成器方案,首先大方向上会首先考虑是搞成一个服务还是一个SDK。考虑到如下原因,选择了SDK的方案:

  1. SDK的设计和开发复杂度要低很多,产品都是慢慢迭代的,先搞一版能满足短期内需求的方案。

  2. SDK完全内存生成,没有网络开销,也没有网络调用带来的不稳定性。

再介绍下snowflake。

Snowflake ID有64bits长,由以下三部分组成:

1.第一位为0,不用。

2.timestamp—41bits,精确到ms,那就意味着其可以表示长达(2^41-1)/(1000360024*365)=139.5年,另外使用者可以自己定义一个开始纪元(epoch),然后用(当前时间-开始纪元)算出time,这表示在time这个部分在140年的时间里是不会重复的,官方文档在这里写成了41bits,应该是写错了。另外,这里用time还有一个很重要的原因,就是可以直接更具time进行排序,对于twitter这种更新频繁的应用,时间排序就显得尤为重要了。

3.machine id—10bits,该部分其实由datacenterId和workerId两部分组成,这两部分是在配置文件中指明的。

  • datacenterId,方便搭建多个生成uid的service,并保证uid不重复,比如在datacenter0将机器0,1,2组成了一个生成uid的service,而datacenter1此时也需要一个生成uid的service,从本中心获取uid显然是最快最方便的,那么它可以在自己中心搭建,只要保证datacenterId唯一。如果没有datacenterId,即用10bits,那么在搭建一个新的service前必须知道目前已经在用的id,否则不能保证生成的id唯一,比如搭建的两个uid service中都有machine id为100的机器,如果其server时间相同,那么产生相同id的情况不可避免。

  • workerId是实际server机器的代号,最大到32,同一个datacenter下的workerId是不能重复的。它会被注册到consul上,确保workerId未被其他机器占用,并将host:port值存入,注册成功后就可以对外提供服务了。

4.sequence id —12bits,该id可以表示4096个数字,它是在time相同的情况下,递增该值直到为0,即一个循环结束,此时便只能等到下一个ms到来,一般情况下4096/ms的请求是不太可能出现的,所以足够使用了。

其中考虑点主要是第三部分和第四部分的设计。

第三部分在实践中并没有考虑datacenterId,只需要生成6bit的workId就行(业务规模并没有那么大,63台机器是考虑了三年业务增长量)。workId的生成需要业务方建立一张辅助表t。主要字段:id(unsigned),server_ip。id是数据库的自增id,会用作workId。server_ip是相应work的ip。业务方需要事先建好表t,并初始化63条空数据。当业务方项目启动时需要抢占id。循环执行如下sql:

update table t set server_ip = ip where id=id and server_ip is NULL;

抢占成功id就是当前机器的workid。失败时也就是需要更进技术方案的时候了。

当业务方某台机器停服时会把相应数据行的server_ip更新成null。

第四部分序列号,也就是毫秒内的并发量。 自然而然想到一个数据结构,代码如下:

    static class TMCounter {
        long tm = 0;
        AtomicLong counter = new AtomicLong(0);

        public TMCounter() {
        }

        public TMCounter(long tm) {
            this.tm = tm;
        }
    }

之前开发的时候还不了解LongAdder,之后会有一篇文章说明LongAddder解决了AtomicLong的什么问题。

另外还会有个属性存储上一次请求的TMCounter信息:

    private volatile TMCounter preCounter = new TMCounter();

当一个获取唯一id的请求来时,首先会判断当前时间戳和上一次请求的时间戳的大小关系。

  • 若大于说明是新的时间戳,会new一个新的TMCounter对象。

  • 若等于或者小于(时钟回拨的情况)会算进上一次的并发里。

最后,按照各位的偏移量把四部分拼接在一起,代码如下:

     private TMCounter queryCurCounter() {
        // 计算与起始时间的差值
        long tm = System.currentTimeMillis() - START_TM;
        if (tm > curCounter.tm) {
            lock.lock();
            try {
                if (tm > curCounter.tm) {
                    // 更新计数器
                    curCounter = new TMCounter(tm);
                }
            } finally {
                lock.unlock();
            }
        }
        return curCounter;
    }

    @Override
    public Long generateUniqueId(int workerId) {
        TMCounter tmCounter = queryCurCounter();
        int countValue = tmCounter.counter.addAndGet(1);
        return (tmCounter.tm << (WORKER_ID_BITS + COUNTER_VALUE_BITS)) | (workerId << COUNTER_VALUE_BITS) | countValue;
    }

定时任务预生成方案

凭证id对长度和内容都有要求,snowflake方案就不能满足了。当时设计的方案分为两部分:

生成

  • 生成策略:每天凌晨两点定时生成一定量的凭证id(取票码和验证码建联合唯一索引,随机生成两个字符串插入表中。半夜跑也就不管什么性能了。)以及同步到redis的list结构里。并且相应的数据会考虑到表里数据越多,冲突就会越多。会定期将一个月之前的数据迁移到ES同时删除表里面相应的数据(是不是有点冷热数据隔离的思想)。
  • 生成数量: 第一次是预计业务量的两倍(建议搞成动态配置)。之后生成的数量是业务量的两倍-redis还剩余的量。

取Id

直接从redis的list取并更新数据库相应凭证状态为已使用。

这两种方案都比较简单,方案略显粗糙,都是为了应付业务初期遇到的问题。

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

推荐阅读更多精彩内容