欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Java系列之雪花算法和原理

程序员文章站 2022-05-03 15:23:29
...

SnowFlake 算法:是 Twitter 开源的分布式 id 生成算法。
核心思想:使用一个 64 bit 的 long 型的数字作为全局唯一 id。
首先了解一下雪花ID的结构:从网上盗用一张;
Java系列之雪花算法和原理
针对上面各个部分做简单说明:

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

  2. 41bit-时间戳,用来记录时间戳,毫秒级。

  • 41位可以表示241-1个数字,
  • 如果只用来表示正整数(计算机中正数包含0),可以表示的数值范围是:0 至 241- 1;减1是因为可表示的数值范围是从0开始算的,而不是1。
  • 也就是说41位可以表示241-1个毫秒的值,转化成单位年则是(241-1) / (1000 * 60 * 60 * 24 * 365)=69年
  1. 10bit-工作机器id,用来记录工作机器id。代表的是这个服务最多可以部署在 210 台机器上,也就是 1024 台机器。
  • 可以部署在个节点,包括5位datacenterId和5位workerId;
  • 10 bit 里 5 个 bit 代表机房 id,5 个 bit 代表机器 id。意思就是最多代表 2 5 个机房(32 个机房),每个机房里可以代表 2 5 个机器(32 台机器),也可以根据自己公司的实际情况确定
  • 5位(bit)可以表示的最大正整数是,即可以用0、1、2、3、…31这32个数字,来表示不同的datecenterId或workerId;
  1. 12bit-***,***,用来记录同毫秒内产生的不同id。
  • 12位(bit)可以表示的最大正整数是212 - 1 ,即可以用0、1、2、3、…4094这4095个数字,来表示同一机器同一时间截(毫秒)内产生的4095个ID序号。

  • 对于分布式中雪花ID的应用理解:
    SnowFlake算法生成的ID大致上是按照时间递增的,用在分布式系统中时,需要注意数据中心标识和机器标识必须唯一,这样就能保证每个节点生成的ID都是唯一的。或许我们不一定都需要像上面那样使用5位作为数据中心标识,5位作为机器标识,可以根据我们业务的需要,灵活分配节点部分,如:若不需要数据中心,完全可以使用全部10位作为机器标识;若数据中心不多,也可以只使用3位作为数据中心,7位作为机器标识
    snowflake生成的ID整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞(由datacenter和workerId作区分),并且效率较高。据说:snowflake每秒能够产生26万个ID。

综上,简单举例说明一下:

0 - 0001100101 0001010111 1101000100 1010111000 0 - 10001 - 11001 - 000000000000

第一个部分,是 1 个 bit:0,这个是无意义的。
第二个部分是 41 个 bit:表示的是时间戳。
第三个部分是 5 个 bit:表示的是机房 id,10001。
第四个部分是 5 个 bit:表示的是机器 id,11001。
第五个部分是 12 个 bit:表示的序号,就是某个机房某台机器上这一毫秒内同时生成的 id 的序号,000000000000。

源码实现

package com.qxy.utils;

/**
 * @author wx
 * */
public final class SnowFlake {
        // 起始的时间戳
        private final static long START_STMP = 1577808000000L; //2020-01-01
        // 每一部分占用的位数,就三个
        private final static long SEQUENCE_BIT = 12; //***占用的位数
        private final static long MACHINE_BIT = 5; //机器标识占用的位数
        private final static long DATACENTER_BIT = 5; //数据中心占用的位数
        // 每一部分最大值
        private final static long MAX_DATACENTER_NUM = ~(-1L << DATACENTER_BIT);
        private final static long MAX_MACHINE_NUM = ~(-1L << MACHINE_BIT);
        private final static long MAX_SEQUENCE = ~(-1L << SEQUENCE_BIT);
        // 每一部分向左的位移
        private final static long MACHINE_LEFT = SEQUENCE_BIT;
        private final static long DATACENTER_LEFT = SEQUENCE_BIT + MACHINE_BIT;
        private final static long TIMESTMP_LEFT = DATACENTER_LEFT + DATACENTER_BIT;
        private long datacenterId; //数据中心
        private long machineId; //机器标识
        private long sequence = 0L; //***
        private long lastStmp = -1L; //上一次时间戳

        public SnowFlake(long datacenterId, long machineId) {
            if (datacenterId > MAX_DATACENTER_NUM || datacenterId < 0) {
                throw new IllegalArgumentException("datacenterId can't be greater than MAX_DATACENTER_NUM or less than 0");
            }
            if (machineId > MAX_MACHINE_NUM || machineId < 0) {
                throw new IllegalArgumentException("machineId can't be greater than MAX_MACHINE_NUM or less than 0");
            }
            this.datacenterId = datacenterId;
            this.machineId = machineId;
        }

        //产生下一个ID
        public synchronized long nextId() {
            long currStmp = timeGen();
            if (currStmp < lastStmp) {
                throw new RuntimeException("Clock moved backwards.  Refusing to generate id");
            }

            if (currStmp == lastStmp) {
                //if条件里表示当前调用和上一次调用落在了相同毫秒内,只能通过第三部分,***自增来判断为唯一,所以+1.
                sequence = (sequence + 1) & MAX_SEQUENCE;
                //同一毫秒的序列数已经达到最大,只能等待下一个毫秒
                if (sequence == 0L) {
                    currStmp = getNextMill();
                }
            } else {
                //不同毫秒内,***置为0
                //执行到这个分支的前提是currTimestamp > lastTimestamp,说明本次调用跟上次调用对比,已经不再同一个毫秒内了,这个时候序号可以重新回置0了。
                sequence = 0L;
            }

            lastStmp = currStmp;
            //就是用相对毫秒数、机器ID和自增序号拼接
            return (currStmp - START_STMP) << TIMESTMP_LEFT //时间戳部分
                    | datacenterId << DATACENTER_LEFT       //数据中心部分
                    | machineId << MACHINE_LEFT             //机器标识部分
                    | sequence;                             //***部分
        }

        private long getNextMill() {
            long mill = timeGen();
            while (mill <= lastStmp) {
                mill = timeGen();
            }
            return mill;
        }

        private long timeGen() {
            return System.currentTimeMillis();
        }

    public static void main(String[] args) {
        SnowFlake snowFlake = new SnowFlake(2, 3);
        for (int i =0; i < 1000; i ++) {
            System.out.println(snowFlake.nextId());
        }
    }

}

总结:
SnowFlake 算法系统:首先从配置文件中读取自己所在的机房和机器,比如机房 id = 17,机器 id = 12。接着 SnowFlake 算法系统接收到这个请求之后,用二进制位运算的方式生成一个 64 bit 的 long 型 id,64 个 bit 中的第一个 bit 是无意义的。
接着 41 个 bit,就可以用当前时间戳(单位到毫秒),然后接着 5 个 bit 设置上这个机房 id,还有 5 个 bit 设置上机器 id。
最后再判断一下,当前这台机房的这台机器上这一毫秒内,这是第几个请求,给这次生成 id 的请求累加一个序号,作为最后的 12 个 bit。