雪花算法

雪花算法

世界上没有完全相同的两朵雪花

1. 初识雪花算法

1.1 雪花算法

snowflake中文的意思是 雪花,雪片,所以翻译成雪花算法。它最早是twitter内部使用的分布式环境下的唯一ID生成算法。在2014年开源。开源的版本由scala编写。

1.2 雪花算法的特点

  • 能满足高并发分布式系统环境下ID不重复
  • 基于时间戳,可以保证基本有序递增(有些业务场景对这个又要求)
  • 不依赖第三方的库或者中间件
  • 生成效率极高

1.3 雪花算法解决的问题

我们都知道每一种算法都是解决现实世界上的实际问题的,那么雪花算法解决了什么问题呢?

其主要解决的问题就在高并发高可用环境下生成唯一id的算法。如我们在双十一的时候订单号的生成。

生成唯一id ? ? ?

这时你可能就有疑问了,我们在数据库中生成唯一 id 的方法也是有的,为什么还要再使用雪花算法呢 ? ?

回想一下我学到过生成唯一 id 的方法 :

  1. ​ 数据库中的主键自增.
  2. ​ java中的 uuid

为什么不用他们呢 ??

1.4 uuid 主键自增的问题

UUID 是的一种生成唯一 ID 方法,那么他是真的唯一吗 ? ?

在 这里我们就要说到 UUID 生成的策略 :

  • Mac地址+时间戳+自增序列

前两个都可以修改,自增序列在不同机器本身就无法保证唯一。所以他不是唯一的 可能会生成相同的 。

且 UUID 可读性很差,没有顺序 。

主键自增呢 ?

  • 如果你是使用 MySql 的话,自能是主键自增,其他是不能自增 。
  • MySql 的并发支持 400左右
  • 安全性差

这时也会有人说 业务需求是可以是主键 且并发 MySql 做集群 ,这个也是会有很大的问题的,在这里就不细说了 。

简而言之 :雪花算是可以在高并发分布式系统环境下生成不重复的ID

2. 雪花算法的实现

实现雪花算法也是十分简单的 。

2.1 雪花算法生成策略

算法产生的是一个long型 64 比特位的值,第一位未使用 。

在这里插入图片描述

  • bit

    不用,因为二进制中最高位是符号位,1表示负数,0表示正数。生成的id一般都是用整数,所以最高位固定为0。

  • **41bit-时间戳 ** 用来记录时间戳,毫秒单位 .

    • 41位可以表示[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传个数字
    • 如果只用来表示正整数(计算机中正数包含0),可以表示的数值范围是:0 至 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传,减1是因为可表示的数值范围是从0开始算的,而不是1。
    • 也就是说41位可以表示[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传个毫秒的值,转化成单位年则是

    所以说雪花算法可以用 69 年

  • 10bit-工作机器id,用来记录工作机器id。

    • 可以部署在个节点,包括5位datacenterId和5位workerId
    • 5位(bit)可以表示的最大正整数是[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8YLWYhXe-1617321544097)(https://math.jianshu.com/math?formula=2%5E%7B5%7D-1%20%3D%2031)],即可以用0、1、2、3、…31这32个数字,来表示不同的datecenterId或workerId
  • 12bit-序列号,序列号,用来记录同毫秒内产生的不同id。

    • 12位(bit)可以表示的最大正整数是,即可以用0、1、2、3、…4094这4095个数字,来表示同一机器同一时间截(毫秒)内产生的4095个ID序号。

2.2 代码实现

java

  • 第一步 :定义一个起始的时间时间戳

    	private final static long START_STMP = 1480166465631L;
    
  • 第二步 : 我们上面说到要用到工作机器 id 与 序列号所以我们把他俩也定义出来

        private long workerId;    //工作id
        private long datacenterId;   //数据id
        //12位的序列号
        private long sequence;
    
  • 第三步 :规定其长度

        //长度为5位
        private long workerIdBits = 5L;
        private long datacenterIdBits = 5L;
        //最大值
        private long maxWorkerId = -1L ^ (-1L << workerIdBits);
        private long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
        //序列号id长度
        private long sequenceBits = 12L;
        //序列号最大值
        private long sequenceMask = -1L ^ (-1L << sequenceBits);
       
    
  • 第四步 : 规定偏移位数

        //工作id需要左移的位数,12位
        private long workerIdShift = sequenceBits;
       //数据id需要左移位数 12+5=17位
        private long datacenterIdShift = sequenceBits + workerIdBits;
        //时间戳需要左移位数 12+5+5=22位
        private long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
    
  • 第五步 :上次时间戳

       //上次时间戳,初始值为负数
        private long lastTimestamp = -1L;
    
  • 第六步 :给定获取时间戳的方法

       public long getWorkerId(){
            return workerId;
        }
    
        public long getDatacenterId(){
            return datacenterId;
        }
    
        public long getTimestamp(){
            return System.currentTimeMillis();
        }
    
  • 第七步 : 构造方法

        public MySnowFlake(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 timeGen(){
            return System.currentTimeMillis();
        }
    
        //获取时间戳,并与上次时间戳比较
        private long tilNextMillis(long lastTimestamp) {
            long timestamp = timeGen();
            while (timestamp <= lastTimestamp) {
                timestamp = timeGen();
            }
            return timestamp;
        }
    
    
  • 第九步 :核心 下一个ID生成

     //下一个ID生成算法
        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));
            }
    
            //获取当前时间戳如果等于上次时间戳(同一毫秒内),则在序列号加一;否则序列号赋值为0,从0开始。
            if (lastTimestamp == timestamp) {
                sequence = (sequence + 1) & sequenceMask;
                if (sequence == 0) {
                    timestamp = tilNextMillis(lastTimestamp);
                }
            } else {
                sequence = 0;
            }
            
            //将上次时间戳值刷新
            lastTimestamp = timestamp;
    
            /**
              * 返回结果:
              * (timestamp - twepoch) << timestampLeftShift) 表示将时间戳减去初始时间戳,再左移相应位数
              * (datacenterId << datacenterIdShift) 表示将数据id左移相应位数
              * (workerId << workerIdShift) 表示将工作id左移相应位数
              * | 是按位或运算符,例如:x | y,只有当x,y都为0的时候结果才为0,其它情况结果都为1。
              * 因为个部分只有相应位上的值有意义,其它位上都是0,所以将各部分的值进行 | 运算就能得到最终拼接好的id
            */
            return ((timestamp - twepoch) << timestampLeftShift) |
                    (datacenterId << datacenterIdShift) |
                    (workerId << workerIdShift) |
                    sequence;
        }
    

总的代码

package cn.guoke.xuehua;

/**
 * @Desc 雪花算法
 */
public class MySnowFlake {

    /**
     * 起始的时间戳
     */
    private final static long START_STMP = 1480166465631L;

    private long workerId;    //工作id
    private long datacenterId;   //数据id
    //12位的序列号
    private long sequence;

    //长度为5位
    private long workerIdBits = 5L;
    private long datacenterIdBits = 5L;
    //最大值
    private long maxWorkerId = -1L ^ (-1L << workerIdBits);
    private long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
    //序列号id长度
    private long sequenceBits = 12L;
    //序列号最大值
    private long sequenceMask = -1L ^ (-1L << sequenceBits);

    //工作id需要左移的位数,12位
    private long workerIdShift = sequenceBits;
    //数据id需要左移位数 12+5=17位
    private long datacenterIdShift = sequenceBits + workerIdBits;
    //时间戳需要左移位数 12+5+5=22位
    private long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;


    //上次时间戳,初始值为负数
    private long lastTimestamp = -1L;

    public long getWorkerId(){
        return workerId;
    }

    public long getDatacenterId(){
        return datacenterId;
    }

    public long getTimestamp(){
        return System.currentTimeMillis();
    }


    public MySnowFlake(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;
    }



    //下一个ID生成算法
    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));
        }

        //获取当前时间戳如果等于上次时间戳(同一毫秒内),则在序列号加一;否则序列号赋值为0,从0开始。
        if (lastTimestamp == timestamp) {
            sequence = (sequence + 1) & sequenceMask;
            if (sequence == 0) {
                timestamp = tilNextMillis(lastTimestamp);
            }
        } else {
            sequence = 0;
        }

        //将上次时间戳值刷新
        lastTimestamp = timestamp;

        /**
         * 返回结果:
         * (timestamp - twepoch) << timestampLeftShift) 表示将时间戳减去初始时间戳,再左移相应位数
         * (datacenterId << datacenterIdShift) 表示将数据id左移相应位数
         * (workerId << workerIdShift) 表示将工作id左移相应位数
         * | 是按位或运算符,例如:x | y,只有当x,y都为0的时候结果才为0,其它情况结果都为1。
         * 因为个部分只有相应位上的值有意义,其它位上都是0,所以将各部分的值进行 | 运算就能得到最终拼接好的id
         */
        return ((timestamp - START_STMP) << 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();
    }


}

2.3 最大值生成的理解

 private long sequenceMask = -1L ^ (-1L << sequenceBits);

首先我们要知道负数在计算机里是以补码的形式表达的,而补码是负数的绝对值的原码,再取得反码,然后再加1得到。

-1取绝对值是1,1的二进制表示,也就是原码是:

00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001

然后取反操作,也就是1变0; 0变1,得到:

11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111110

然后加1,得到:

11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111

OK,这就是-1在计算机中的表示了,然后我们来看看(-1L << sequenceBits),这个很简单,直接左移12个比特位即可,得到:

11111111 11111111 11111111 11111111 11111111 11111111 11110000 00000000

上面两个异或,得到:

00000000 00000000 00000000 00000000 00000000 00000000 00001111 11111111

也就是4095。

3. 雪花算法 补充(时钟回拨问题)

3.1 snowflake算法的时钟回拨问题如何解决

11111111 11111111


OK,这就是-1在计算机中的表示了,然后我们来看看(-1L << sequenceBits),这个很简单,直接左移12个比特位即可,得到:

```javascript
11111111 11111111 11111111 11111111 11111111 11111111 11110000 00000000

上面两个异或,得到:

00000000 00000000 00000000 00000000 00000000 00000000 00001111 11111111

也就是4095。

3. 雪花算法 补充(时钟回拨问题)

3.1 snowflake算法的时钟回拨问题如何解决

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值