雪花算法详解(原理优缺点及代码实现)

雪花算法详解(原理优缺点及代码实现)-mikechen

雪花算法简介

雪花算法,英文名为snowflake,翻译过来就是是雪花,所以叫雪花算法。

在大自然雪花形成过程中,会形成不同的结构分支,所以说不存在两片完全一样的雪花,表示生成的id如雪花般独一无二。

雪花算法详解(原理优缺点及代码实现)-mikechen

雪花算法,它最早是twitter内部使用的分布式环境下的唯一分布式ID生成算法。

 

雪花算法的优缺点

雪花算法,它至少有如下4个优点:

1.系统环境ID不重复

能满足高并发分布式系统环境ID不重复,比如大家熟知的分布式场景下的数据库表的ID生成。

 

2.生成效率极高

在高并发,以及分布式环境下,除了生成不重复 id,每秒可生成百万个不重复 id,生成效率极高。

 

3.保证基本有序递增

基于时间戳,可以保证基本有序递增,很多业务场景都有这个需求。

 

4.不依赖第三方库

不依赖第三方的库,或者中间件,算法简单,在内存中进行。

 

雪花算法,有一个比较大的缺点:

依赖服务器时间,服务器时钟回拨时可能会生成重复 id。

 

雪花算法原理

详细的雪花算法构造如下图所示:

雪花算法详解(原理优缺点及代码实现)-mikechen

雪花算法的原理:就是生成一个的 64 位的 long 类型的唯一 id,主要分为如下4个部分组成:

1)1位保留 (基本不用)

1位标识:由于long基本类型在Java中是带符号的,最高位是符号位,正数是0,负数是1,所以id一般是正数,最高位是0,所以这第一位都是0。

 

2)41位时间戳 

接下来 41 位存储毫秒级时间戳,41位可以表示2^41-1个毫秒的值,转化成单位年则是:(2^41−1)/(1000∗60∗60∗24∗365)=69年 。

41位时间戳 :也就是说这个时间戳可以使用69年不重复,大概可以使用 69 年。

注意:41位时间截不是存储当前时间的时间截,而是存储时间截的差值“当前时间截 – 开始时间截”得到的值。

这里的的开始时间截,一般是我们的id生成器开始使用的时间,由我们程序来指定的,一般设置好后就不要去改变了,切记!!!

因为,雪花算法有如下缺点:依赖服务器时间,服务器时钟回拨时可能会生成重复 id

 

3)10位机器

10位的数据机器位,可以部署在1024个节点,包括5位datacenterId和5位workerId,最多可以部署 2^10=1024 台机器。

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

 

4) 12bit序列号

用来记录同毫秒内产生的不同id,12位的计数顺序号支持每个节点每毫秒(同一机器,同一时间截)产生4096个ID序号。

理论上雪花算法方案的QPS约为409.6w/s,这种分配方式可以保证在任何一个IDC的任何一台机器在任意毫秒内生成的ID都是不同的。

 

雪花算法代码实现

public class SnowflakeIdWorker {
    /** 开始时间截 (这个用自己业务系统上线的时间) */
    private final long twepoch = 1575365018000L;

    /** 机器id所占的位数 */
    private final long workerIdBits = 10L;

    /** 支持的最大机器id,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数) */
    private final long maxWorkerId = -1L ^ (-1L << workerIdBits);
    
    /** 序列在id中占的位数 */
    private final long sequenceBits = 12L;

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

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

    /** 生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095) */
    private final long sequenceMask = -1L ^ (-1L << sequenceBits);

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

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

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

    //==============================Constructors=====================================
    /**
     * 构造函数
     * @param workerId 工作ID (0~1024)
     */
    public SnowflakeIdWorker(long workerId) {
        if (workerId > maxWorkerId || workerId < 0) {
            throw new IllegalArgumentException(String.format("workerId can't be greater than %d or less than 0", maxWorkerId));
        }
        this.workerId = workerId;
    }

    // ==============================Methods==========================================
    /**
     * 获得下一个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;
            //毫秒内序列溢出
            if (sequence == 0) {
                //阻塞到下一个毫秒,获得新的时间戳
                timestamp = tilNextMillis(lastTimestamp);
            }
        }
        //时间戳改变,毫秒内序列重置
        else {
            sequence = 0L;
        }

        //上次生成ID的时间截
        lastTimestamp = timestamp;

        //移位并通过或运算拼到一起组成64位的ID
        return ((timestamp - twepoch) << timestampLeftShift) //
                | (workerId << workerIdShift) //
                | 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();
    }
}

 

陈睿mikechen

10年+大厂架构经验,资深技术专家,就职于阿里巴巴、淘宝、百度等一线互联网大厂。

关注「mikechen」公众号,获取更多技术干货!

后台回复面试即可获取《史上最全阿里Java面试题总结》,后台回复架构,即可获取《阿里架构师进阶专题全部合集

评论交流
    说说你的看法