美文网首页
ID生成算法

ID生成算法

作者: 剑客kb | 来源:发表于2019-05-31 19:43 被阅读0次

    snowflake

    源码地址
    由于源码是scala编写的,翻译成java

    public class IdWorker{
    
        private long workerId;
        private long datacenterId;
        private long sequence;
    
        public IdWorker(long workerId, long datacenterId, long sequence){
            // sanity check for workerId
            if (workerId > maxWorkerId || workerId < 0) {
                throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0",maxWorkerId));
            }
            if (datacenterId > maxDatacenterId || datacenterId < 0) {
                throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0",maxDatacenterId));
            }
            System.out.printf("worker starting. timestamp left shift %d, datacenter id bits %d, worker id bits %d, sequence bits %d, workerid %d",
                    timestampLeftShift, datacenterIdBits, workerIdBits, sequenceBits, workerId);
    
            this.workerId = workerId;
            this.datacenterId = datacenterId;
            this.sequence = sequence;
        }
    
        private long twepoch = 1288834974657L;
    
        private long workerIdBits = 5L;
        private long datacenterIdBits = 5L;
        private long maxWorkerId = -1L ^ (-1L << workerIdBits);
        private long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
        private long sequenceBits = 12L;
    
        private long workerIdShift = sequenceBits;
        private long datacenterIdShift = sequenceBits + workerIdBits;
        private long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
        private long sequenceMask = -1L ^ (-1L << sequenceBits);
    
        private long lastTimestamp = -1L;
    
        public long getWorkerId(){
            return workerId;
        }
    
        public long getDatacenterId(){
            return datacenterId;
        }
    
        public long getTimestamp(){
            return System.currentTimeMillis();
        }
    
        public synchronized long nextId() {
            long timestamp = timeGen();
    
            if (timestamp < lastTimestamp) {
                System.err.printf("clock is moving backwards.  Rejecting requests until %d.", lastTimestamp);
                throw new RuntimeException(String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds",
                        lastTimestamp - timestamp));
            }
    
            if (lastTimestamp == timestamp) {
                sequence = (sequence + 1) & sequenceMask;
                if (sequence == 0) {
                    timestamp = tilNextMillis(lastTimestamp);
                }
            } else {
                sequence = 0;
            }
    
            lastTimestamp = timestamp;
            return ((timestamp - twepoch) << timestampLeftShift) |
                    (datacenterId << datacenterIdShift) |
                    (workerId << workerIdShift) |
                    sequence;
        }
    
        private long tilNextMillis(long lastTimestamp) {
            long timestamp = timeGen();
            while (timestamp <= lastTimestamp) {
                timestamp = timeGen();
            }
            return timestamp;
        }
    
        private long timeGen(){
            return System.currentTimeMillis();
        }
    
        //---------------测试---------------
        public static void main(String[] args) {
            IdWorker worker = new IdWorker(1,1,1);
            for (int i = 0; i < 30; i++) {
                System.out.println(worker.nextId());
            }
        }
    
    }
    

    snowflake 64bit组成


    41位的时间戳能够用到约69年。假设时间从2010年开始的话,可以用到2079年。10bit工作机器可以支持1024个实例部署,12bit序列号代表每ms最多能够产生4096个seq,所以snowflake理论上的QPS大约为400w/s。
    优点:

    • 整个ID趋势递增
    • 不依赖第三方系统,性能好
      缺点:强依赖机器时钟,如果机器上时钟回拨,会导致发号重复或者服务会处于不可用状态。

    美团Leaf改进的Snowflake

    源码地址

    流程图
    参见上图整个启动流程图,服务启动时首先检查自己是否写过ZooKeeper leaf_forever节点:
    1、若写过,则用自身系统时间与leaf_forever/self节点记录时间做比较,若小于leaf_forever/self时间则认为机器时间发生了大步长回拨,服务启动失败并报警。
    2、若未写过,证明是新服务节点,直接创建持久节点leaf_forever/self并写入自身系统时间,接下来综合对比其余Leaf节点的系统时间来判断自身系统时间是否准确,具体做法是取leaf_temporary下的所有临时节点(所有运行中的Leaf-snowflake节点)的服务IP:Port,然后通过RPC请求得到所有节点的系统时间,计算sum(time)/nodeSize。
    3、若abs( 系统时间-sum(time)/nodeSize ) < 阈值,认为当前系统时间准确,正常启动服务,同时写临时节点leaf_temporary/{self}。
    由于强依赖时钟,对时间的要求比较敏感,在机器工作时NTP同步也会造成秒级别的回退,建议可以直接关闭NTP同步。要么在时钟回拨的时候直接不提供服务直接返回ERROR_CODE,等时钟追上即可。或者做一层重试,然后上报报警系统,更或者是发现有时钟回拨之后自动摘除本身节点并报警,如下:
    //发生了回拨,此刻时间小于上次发号时间
     if (timestamp < lastTimestamp) {
                  
                long offset = lastTimestamp - timestamp;
                if (offset <= 5) {
                    try {
                        //时间偏差大小小于5ms,则等待两倍时间
                        wait(offset << 1);//wait
                        timestamp = timeGen();
                        if (timestamp < lastTimestamp) {
                           //还是小于,抛异常并上报
                            throwClockBackwardsEx(timestamp);
                          }    
                    } catch (InterruptedException e) {  
                       throw  e;
                    }
                } else {
                    //throw
                    throwClockBackwardsEx(timestamp);
                }
            }
     //分配ID   
    

    同时,Leaf服务规模较大,动手配置成本太高。所以使用Zookeeper持久顺序节点的特性自动对snowflake节点配置wokerID。Leaf-snowflake是按照下面几个步骤启动的:
    1、启动Leaf-snowflake服务,连接Zookeeper,在leaf_forever父节点下检查自己是否已经注册过(是否有该顺序子节点)。
    2、如果有注册过直接取回自己的workerID(zk顺序节点生成的int类型ID号),启动服务。
    3、如果没有注册过,就在该父节点下面创建一个持久顺序节点,创建成功后取回顺序号当做自己的workerID号,启动服务。

    美团Leaf 号段模式

    每次从数据库获取一个segment(step决定大小)号段的值,Segment结构如下,用完之后再去数据库获取。为了防止表过大,可以对biz_tag水平分库分表。

    public class Segment {
        private AtomicLong value = new AtomicLong(0);
        private volatile long max;
        private volatile int step;
        private SegmentBuffer buffer;
    }
    
    表字段
    CREATE TABLE `leaf_alloc` (
      `biz_tag` varchar(128)  NOT NULL DEFAULT '',
      `max_id` bigint(20) NOT NULL DEFAULT '1',
      `step` int(11) NOT NULL,
      `description` varchar(256)  DEFAULT NULL,
      `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
      PRIMARY KEY (`biz_tag`)
    ) ENGINE=InnoDB;
    

    缺点:
    1、ID号码不够随机,能够泄露发号数量的信息,不太安全。
    2、TP999数据波动大,当号段使用完之后还是会hang在更新数据库的I/O上,tg999数据会出现偶尔的尖刺。
    3、DB宕机会造成整个系统不可用。
    针对第二点,leaf做法:当号段消费到某个点时就异步的把下一个号段加载到内存中。而不需要等到号段用尽的时候才去更新号段。这样做就可以很大程度上的降低系统的TP999指标。
    采用双buffer的方式,Leaf服务内部有两个号段缓存区segment。当前号段已下发10%时,如果下一个号段未更新,则另启一个更新线程去更新下一个号段。当前号段全部下发完后,如果下个号段准备好了则切换到下个号段为当前segment接着下发,循环往复。

    • 每个biz-tag都有消费速度监控,通常推荐segment长度设置为服务高峰期发号QPS的600倍(10分钟),这样即使DB宕机,Leaf仍能持续发号10-20分钟不受影响。
    • 每次请求来临时都会判断下个号段的状态,从而更新此号段,所以偶尔的网络抖动不会影响下个号段的更新

    参考:https://segmentfault.com/a/1190000011282426
    https://tech.meituan.com/2017/04/21/mt-leaf.html

    相关文章

      网友评论

          本文标题:ID生成算法

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