ICode9

精准搜索请尝试: 精确搜索
首页 > 编程语言> 文章详细

分布式唯一ID(二)--基于Snowflake算法生成唯一ID

2022-03-01 01:03:00  阅读:204  来源: 互联网

标签:-- long Snowflake 回拨 毫秒 生成 ID 时钟


目录

一、背景:

SnowFlake算法生成id的结果是一个64bit大小的整数,它的结构如下图:

1位:不使用,二进制中最高位为1的都是负数,ID一般都使用正整数,所以这个最高位固定是0。

41位:用来记录时间戳(毫秒)。可以表示2 ^ 41 - 1个数字,转化成单位年则是(2 ^ 41 - 1) / (1000 * 60 * 60 * 24 * 365) = 69年。

10位:用来记录工作机器id。可以部署在2 ^ 10 = 1024个节点,包括5位datacenterId和5位workerId。

5位可以表示的最大正整数是2 ^ 5 - 1 = 31,即可以用0、1、2、3、....31这32个数字,来表示不同的datecenterId或workerId。

12位:序列号,用来记录同毫秒内产生的不同ID。可以用0、1、2、3、....4095这4096个数字,来表示同一机器同一时间截(毫秒)内产生的4096个ID序号

由于在Java中64bit的整数是long类型,所以在Java中SnowFlake算法生成的id就是long来存储的。

二、特点:

  • 所有生成的id按时间趋势递增
  • 整个分布式系统内不会产生重复ID(因为有datacenterId和workerId来做区分)

三、代码实现:

    public class IdWorker {
    
        private long workerId;
        private long datacenterId;
        private long sequence;
    
        public IdWorker(long workerId, long datacenterId, long sequence) {
            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));
            }
            log.info("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) {
                log.error("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());
            }
        }
    
    }

由于唯一ID生成服务肯定不会只有一个节点,所以只能保证唯一ID是局部递增的,无法保证全局递增。

四、多机房部署:

  • 如果并发特别高,以及高可用等问题,可以采用多机房部署。
  • 将机器ID拆分为机房ID+机器ID,分别占用5位。
  • 可以在maven prod profile配置机房ID。

五、时钟回拨:

从Snowflake算法的生成规则中可以看出,和时间戳强依赖,可能出现时钟回拨。

1、举个栗子:

2020-12-12 12:12:12,生成分布式唯一ID。
2020-12-12 12:12:11,此时机器时钟回拨到前面的时间,就会出现ID重复的问题。

2、解决方案:

2.1. 关闭时钟同步:

关闭时钟同步,避免产生时钟同步问题。这个不太现实,因为强依赖时间的系统,一般都得做时钟同步,避免时间严重错误。

2.2. 记录上一次生成ID的时间:

记录上一次生成ID的时间,如果发现本次生成ID的时间戳小于上次的时间戳,说明时钟回拨了。

如果回拨时间在500ms以内,hang住请求,等待对应的回拨时间。等待时间结束之后,当前时间戳比上一次生成ID的时间戳要大了。

对于业务方而言,仅仅个别的时钟回拨情况之下,500ms,还在接受范围之内,请求一般情况下只要50ms。

如果时钟回拨时间在500ms - 5s,此时可以返回一个异常状态 + 异常持续时间给客户端,可以通知自行进行重试。

可以封装在唯一ID生成服务的客户端,客户端里封装自动重试机制,发现某台服务器返回的响应短时间内没法提供服务,自动就去请求其他机器上的服务获取唯一ID。

2.3. 暂停对外提供服务:

如果发现时钟回拨的太多,比如超过了1分钟,直接进行报警。
同时不再对外提供服务,从集群中下线。

2.4. 内存中维护最近几秒生成的ID:

要在内存里维护最近几秒内每一毫秒生成的ID值的最大值,时钟回拨一般是几十毫秒到几百毫秒,很少会超过秒的,所以保存最近几秒的就行了。
然后如果发生了时钟回拨,此时就看回拨到了哪一毫秒,因为时间戳是毫秒级的,接着就看那一毫秒。
从那一毫秒生产过的ID序号往后继续生成就可以了,后续每一毫秒都是依次类推,就可以完美避免重复问题,还不用等待。

2.5. 兜底:
  • 比如你保留最近10s内每一毫秒生成的ID,那么万一时钟回拨碰巧超过了10s呢?
  • 这种概率很低,可以跟二三两个方案结合,设置几个阈值。
  • 比如说,你保留最近10s的ID,回拨10s内都可以保证不重复,不停顿;
  • 如果超过10s,在60s内,可以有一个等待的过程,让他时间前进到你之前保留过的10s范围内去;
  • 如果回拨超过了60s,直接下线。

3、问题:

  • 如果服务重启,上一次生成唯一ID的时间戳没了,最近1s内每一毫秒的最大ID序号也没了。
  • 重启之后,出现了时间回拨,发现不了时间回拨问题,其次也没有办法继续按照之前的思路去生成不重复的唯一ID了。

4、解决方案:

4.1. 方案一:

将之前的数据持久化到磁盘中,启动的时候重新加载到内存中。

4.2. 方案二:

每次唯一ID生成服务启动会从ZK中获取最新的机器ID,删除自己之前的机器ID。

标签:--,long,Snowflake,回拨,毫秒,生成,ID,时钟
来源: https://www.cnblogs.com/huigelaile/p/15948349.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有