美文网首页分布式技术
分布式id生成器

分布式id生成器

作者: tuser | 来源:发表于2019-04-04 13:32 被阅读0次

概述

网上关于分布式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取并更新数据库相应凭证状态为已使用。

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

相关文章

网友评论

    本文标题:分布式id生成器

    本文链接:https://www.haomeiwen.com/subject/fdefiqtx.html