雪花算法SnowFlake全方位详细解读,结合位运算的使用解读

SnowFlake算法是一种由Twitter开源的分布式ID生成策略,通过划分64位ID空间,包括时间戳、机器标识和序列号。本文详细解析了其Java实现,分析了位运算如异或、左移和与操作在算法中的应用,以及如何处理并发和防止ID重复。此外,还讨论了算法的优缺点,如依赖时钟可能导致的问题。
摘要由CSDN通过智能技术生成

Snowflake,雪花算法是由Twitter开源的分布式ID生成算法,以划分命名空间的方式将 64-bit位分割成多个部分,每个部分代表不同的含义。而 Java中64bit的整数是Long类型,所以在 Java 中 SnowFlake 算法生成的 ID 就是 long 来存储的。

位运算异或( ^ ) ,左移( << ) ,与( & ),或( | )

java中基本类型占用字节数 (整型)

 

  • 第1位:占用1bit,第一位为符号位,不使用。

  • 第1部分:41位的时间戳,41-bit位可表示2^41个数,每个数代表毫秒,那么雪花算法可用的时间年限是(2^41)/(1000*60*60*24*365)=69 年的时间。

  • 第2部分:10-bit位可表示机器数,即2^10 = 1024台机器,通常不会部署这么多台机器,(细分两部分5-bit(数据),5-bit(2^5)=32台机器)也可划分多个部分,

  • 第3部分:12-bit位是自增序列,可表示2^12 = 4096个数。觉得一毫秒个数不够用也可以调大点

 41位时间戳是固定的,时间戳转二进制的长度是41位,后面两个部分都可以灵活调正,只要注意后面位运算的位数就行.

 

一、SnowFlake代码


import org.springframework.util.Assert;

public class IdWorker {

    /**
     * 这两个参数可以读取配置文件
     * 这里默认写死
     *
     * @param workerId 机器标识
     * @param datacenterId 数据标识
     */
    private static SnowflakeIdWorker worker = new SnowflakeIdWorker(0, 0);

    public static long id() {
        Assert.notNull(worker, "SnowflakeIdWorker未配置!");
        return worker.nextId();
    }

    /**
     * Twitter的分布式自增ID算法snowflake
     */
    public static class SnowflakeIdWorker {
        /**
         * 第1部分(41位)
         * 开始时间截 (2022-04-01)
         */
        private final long startTime = 1648742400000L;

        /**
         * 第2部分(10位)
         * 机器id所占的位数
         */
        private final long workerIdBits = 5L;
        /**
         * 数据标识id所占的位数
         */
        private final long datacenterIdBits = 5L;

        /**
         * 第3部分(12位)
         * 序列在id中占的位数
         */
        private final long sequenceBits = 12L;

        /**
         * -1L ^ (-1L << 5) = 31
         * 支持的最大机器id,结果是31
         */
        private final long maxWorkerId = -1L ^ (-1L << workerIdBits);

        /**
         * -1L ^ (-1L << 5) = 31
         * 支持的最大数据标识id,结果是31
         */
        private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);

        /**
         * -1L ^ (-1L << 12) = 4095
         * 自增长最大值4095,0开始
         */
        private final long sequenceMask = -1L ^ (-1L << sequenceBits);

        /**
         * 时间截向左移22位(5+5+12)
         */
        private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;

        /**
         * 数据标识id向左移17位(12+5)
         */
        private final long datacenterIdShift = sequenceBits + workerIdBits;

        /**
         * 机器ID向左移12位
         */
        private final long workerIdShift = sequenceBits;


        /**
         * 工作机器ID(0~31)
         */
        private long workerId;

        /**
         * 数据中心ID(0~31)
         */
        private long datacenterId;

        /**
         * 1毫秒内序号(0~4095)
         */
        private long sequence = 0L;

        /**
         * 上次生成ID的时间截
         */
        private long lastTimestamp = -1L;

        /**
         * 构造函数
         *
         * @param workerId     工作ID (0~31)
         * @param datacenterId 数据中心ID (0~31)
         */
        public SnowflakeIdWorker(long workerId, long datacenterId) {
            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));
            }
            this.workerId = workerId;
            this.datacenterId = datacenterId;
        }

        /**
         * 获得下一个ID (该方法是线程安全的)
         *
         * @return SnowflakeId
         */
        public synchronized long nextId() {
            long timestamp = timeGen();
            // 如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常
            if (timestamp < 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;
                // 毫秒内序列溢出
                //sequence == 0 ,就是1毫秒用完了4096个数
                if (sequence == 0) {
                    // 阻塞到下一个毫秒,获得新的时间戳
                    timestamp = tilNextMillis(lastTimestamp);
                }
            }
            // 时间戳改变,毫秒内序列重置
            else {
                sequence = 0L;
            }
            // 上次生成ID的时间截
            lastTimestamp = timestamp;

            // 移位并通过或运算拼到一起组成64位的ID
            return ((timestamp - startTime) << timestampLeftShift) // 时间戳左移22位
                    | (datacenterId << datacenterIdShift) //数据标识左移17位
                    | (workerId << workerIdShift) //机器id标识左移12位
                    | sequence;
        }

        /**
         * 阻塞到下一个毫秒,直到获得新的时间戳
         *
         * @param lastTimestamp 上次生成ID的时间截
         * @return 当前时间戳
         */
        protected long tilNextMillis(long lastTimestamp) {
            long timestamp = timeGen();
            while (timestamp <= lastTimestamp) {
                timestamp = timeGen();
            }
            return timestamp;
        }

        /**
         * 返回以毫秒为单位的当前时间
         *
         * @return 当前时间(毫秒)
         */
        protected long timeGen() {
            return System.currentTimeMillis();
        }

    }

二、SnowFlake代码分析,结合位运算分析

1. 异或( ^ ) ,左移( << ) 

/**
         * -1L ^ (-1L << 5) = 31
         * 支持的最大机器id,结果是31
         */
        private final long maxWorkerId = -1L ^ (-1L << workerIdBits);

        /**
         * -1L ^ (-1L << 5) = 31
         * 支持的最大数据标识id,结果是31
         */
        private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
        
        /**
         * -1L ^ (-1L << 12) = 4095
         * 自增长最大个数4095,0开始
         */
        private final long sequenceMask = -1L ^ (-1L << sequenceBits);

这里使用到了 异或( ^ ) ,左移( << )  这两个位运算

  • 异或( ^ )  两个数相同,结果为0,不相同则为1
  • 左移( << )  二进制向左移多少位,低位补0

计算机中负数的二进制是用补码来表示的。

  • 补码 = 反码 + 1

例如 :1L的二进制

0000000000000000000000000000000000000000000000000000000000000001

反码

1111111111111111111111111111111111111111111111111111111111111110

补码 =反码 + 1 (-1L的二进制)

1111111111111111111111111111111111111111111111111111111111111111

-1L << 5 ,向左移动5位,低位补0
               11111111   11111111   11111111   11111111   // -1补码
       11111   11111111   11111111   11111111   11100000   //左移5位
---------------------------------------------------------------------
               11111111   11111111   11111111   11100000   //  高位溢出舍弃 
-1L ^ (-1L << 5) :异或( ^ )  两个数相同,结果为0,不相同则为1
               11111111   11111111   11111111   11111111   // -1补码
    ^          11111111   11111111   11111111   11100000   //左移5位的结果
---------------------------------------------------------------------
               00000000   00000000   00000000   00011111

00000000 00000000 00000000 00011111

就是16+8+4+2+1 = 31

也就是 2的5次方-1 = 31

该写法是利用位运算计算出5位能表示的最大正整数是多少,从0开始算。所以可以配置32台机器。

2. 与( & )

            // 如果是同一时间生成的,则进行毫秒内序列
            if (lastTimestamp == timestamp) {
                sequence = (sequence + 1) & sequenceMask;
                // 毫秒内序列溢出
                //sequence == 0 ,就是1毫秒用完了4096个数
                if (sequence == 0) {
                    // 阻塞到下一个毫秒,获得新的时间戳
                    timestamp = tilNextMillis(lastTimestamp);
                }
            }

这里使用到了 与( & ) 位运算

  • 两个数都为1,结果为1,否则为0

sequence 一毫秒内开始从0开始, sequenceMask为最大值4095。

这个方法里sequence == 0 为什么要等到下一毫秒来重置sequence的值

               00000000   00000000   00000000   00000001   // 1
    &          00000000   00000000   00001111   11111111   //4095
---------------------------------------------------------------------
               00000000   00000000   00000000   00000001   //1
               00000000   00000000   00000000   00000010   // 2
    &          00000000   00000000   00001111   11111111   //4095
---------------------------------------------------------------------
               00000000   00000000   00000000   00000010   // 2

  ......

               00000000   00000000   00001111   11111111   //4095
    &          00000000   00000000   00001111   11111111   //4095
---------------------------------------------------------------------
               00000000   00000000   00001111   11111111   //4095
               00000000   00000000   00010000   00000000   //4096
    &          00000000   00000000   00001111   11111111   //4095
---------------------------------------------------------------------
               00000000   00000000   00000000   00000000   //0

可以看出来到了4096之前,计算出来的结果都是等于本身,到了4096计算结果为0,所以sequence == 0 就是说从0开始,到4095,4096个数已经用完了。实际场景不够用可以调大位数。

3.或( | )

            // 移位并通过或运算拼到一起组成64位的ID
            return ((timestamp - startTime) << timestampLeftShift) // 时间戳左移22位
                    | (datacenterId << datacenterIdShift) //数据标识左移17位
                    | (workerId << workerIdShift) //机器id标识左移12位
                    | sequence;

这里使用到了 与( | ), 左移( << )  这两个位运算

  • 与( | ) 两个数只要一个是1,结果为1,否则为0

问题:这是最后生成id的算法,为什么不同的部分要左移不同的位数呢。

模拟下数据

    public static void main(String[] args) {

        //第一部分时间戳
        Long a = 1648742400000L;
        String aa = Long.toBinaryString(a);
        System.out.println("时间戳位数" + aa.length());
        while (aa.length() < 64) {
            aa = "0" + aa;
        }
        System.out.println("//时间戳------------------------------------//");
        System.out.println(aa);


        //第二部分数据区分
        Long b = 5L;
        String bb = Long.toBinaryString(b);
        while (bb.length() < 64) {
            bb = "0" + bb;
        }
        System.out.println("//数据标识------------------------------------//");
        System.out.println(bb);

        //机器区分
        Long c = 6L;
        String cc = Long.toBinaryString(c);
        while (cc.length() < 64) {
            cc = "0" + cc;
        }
        System.out.println("//机器标识------------------------------------//");
        System.out.println(cc);
        

        //第三部分递增数
        Long d = 1L;
        String dd = Long.toBinaryString(d);
        while (dd.length() < 64) {
            dd = "0" + dd;
        }
        System.out.println("//自增数------------------------------------//");
        System.out.println(dd);

    }
时间戳位数41
//时间戳------------------------------------//
0000000000000000000000010111111111100000101101001000000000000000
//数据标识------------------------------------//
0000000000000000000000000000000000000000000000000000000000000101
//机器标识------------------------------------//
0000000000000000000000000000000000000000000000000000000000000110
//自增数------------------------------------//
0000000000000000000000000000000000000000000000000000000000000001

进行位移计算后

    public static void main(String[] args) {

        //第一部分时间戳
        Long a = 1648742400000L;
        String aa = Long.toBinaryString(a<<22);
        while (aa.length() < 64) {
            aa = "0" + aa;
        }
        System.out.println("//位移22位后时间戳------------------------------------//");
        System.out.println(aa);


        //第二部分数据区分
        Long b = 5L;
        String bb = Long.toBinaryString(b<<17);
        while (bb.length() < 64) {
            bb = "0" + bb;
        }
        System.out.println("//位移17位后数据标识------------------------------------//");
        System.out.println(bb);

        //机器区分
        Long c = 6L;
        String cc = Long.toBinaryString(c<<12);
        while (cc.length() < 64) {
            cc = "0" + cc;
        }
        System.out.println("//位移12位后机器标识------------------------------------//");
        System.out.println(cc);


        //第三部分递增数
        Long d = 1L;
        String dd = Long.toBinaryString(d);
        while (dd.length() < 64) {
            dd = "0" + dd;
        }
        System.out.println("//自增数(最后一个部分不用位移)------------------------------------//");
        System.out.println(dd);

    }
//位移22位后时间戳------------------------------------//
0101111111111000001011010010000000000000000000000000000000000000
//位移17位后数据标识------------------------------------//
0000000000000000000000000000000000000000000010100000000000000000
//位移12位后机器标识------------------------------------//
0000000000000000000000000000000000000000000000000110000000000000
//自增数(最后一个部分不用位移)------------------------------------//
0000000000000000000000000000000000000000000000000000000000000001

最后使用位移后的数据进行 与(|)计算合并。两个数只要一个是1,结果为1,否则为0

         0101111111111000001011010010000000000000000000000000000000000000  //时间戳
    |    0000000000000000000000000000000000000000000010100000000000000000  //数据标识
    |    0000000000000000000000000000000000000000000000000110000000000000  //机器标识
    |    0000000000000000000000000000000000000000000000000000000000000001  //数据标识
---------------------------------------------------------------------
         0101111111111000001011010010000000000000000010100110000000000001  //64位

最后可以看出,对应每个部分之间的数据就是互不影响,放在了各自对应的位数范围内,把每个部分的数据合并起来。

时间戳左移22位,因为后面有 5(数据标识)+5(机器标识)+12(自增数)=22位

数据标识左移17位,后面还需要 5(机器标识)+12(自增数)=17位

机器标识左移12位,最后留下的位数给自增数。

总结

优点:雪花算法提供了一个很好的设计思想,雪花算法生成的ID是趋势递增,不依赖数据库等第三方系统,生成ID的性能也是非常高的,而且可以根据自身业务特性分配bit位,非常灵活

缺点:雪花算法强依赖机器时钟,如果机器上时钟回拨,会导致发号重复。如果恰巧回退前生成过一些ID,而时间回退后,生成的ID就有可能重复

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值