先看图中雪花算法的结构

Snowflake 雪花算法 原理说明和注意事项_java

 

 

第一段1位,固定0, 69年以后可能会用1,也就是说默认在一个系统中只能用最多69年,如果征用第一位可以使用139年。

 

第二段41位,用时间毫秒数数表示41位大概是69年多,默认表示1971年1月1日到当前时间的毫秒数,有的雪花算法优化支持设定这个起算时间,我们可以把它指定位我们系统立项的时间,这样的好处在于可以使用完整的69年,第一位改成1,还可以用在用70年,一句话,可以用到死。

 

第三段10位,2的10次方,同样1024 个标识符,可用最多支持1024 台节点的分布式器群。1024 多余中小公司来说太多了,有些多雪花算法的包装把它分成2段,workerId和datacenterId一段5位,也就是32个值。分别表示不同的服务和同一个服务的不同集群节点。

 

第四段12位,2的12次方4096个,这个是内部加锁单调递增的。也就是每毫秒最多产出4096个,如果你的业务需要单机平均每毫秒生产的数据量大于4096,那么大概不适合学号算法,或者你改改默认的算法,把第三段用不完的借几位给第4段。如果你的系统只是阶段性的超过没毫秒4096,雪花算法依旧是可以支持的,比如当前毫秒如果不够用了,就自动使用下一个毫秒应该生成的id。我们也可以指定这个向后面的毫秒应该生成的id借最多能接几个毫秒的(注意的是,hutool里面有个bugs)。一般默认不指定,每毫秒4096 以后就重复了,hutool 的默认做法死重复以后就等待到下一毫秒。

 

 

 

 

下面是一个hutool雪花算法的实现:默认使用时间是2010-11-4 9:42:54(DEFAULT_TWEPOCH),而不是1971年1月1日,默认是没毫秒生产id超过4096个以后就重复,但是可以通过设置timeOffset来指定最多向后面借多个毫秒的ID//

package com.lomi.entity;

/**
 * 描述
 *
 * @Author ZHANGYUKUN
 * @Date 2022/6/26
 */
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//


import cn.hutool.core.date.SystemClock;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import java.io.Serializable;
import java.util.Date;

public class Snowflake implements Serializable {
    private static final long serialVersionUID = 1L;
    public static long DEFAULT_TWEPOCH = 1288834974657L;
    public static long DEFAULT_TIME_OFFSET = 2000L;
    private static final long WORKER_ID_BITS = 5L;
    private static final long MAX_WORKER_ID = 31L;
    private static final long DATA_CENTER_ID_BITS = 5L;
    private static final long MAX_DATA_CENTER_ID = 31L;
    private static final long SEQUENCE_BITS = 12L;
    private static final long WORKER_ID_SHIFT = 12L;
    private static final long DATA_CENTER_ID_SHIFT = 17L;
    private static final long TIMESTAMP_LEFT_SHIFT = 22L;
    private static final long SEQUENCE_MASK = 4095L;
    private final long twepoch;
    private final long workerId;
    private final long dataCenterId;
    private final boolean useSystemClock;
    private final long timeOffset;
    private long sequence;
    private long lastTimestamp;

    public Snowflake() {
        this(IdUtil.getWorkerId(IdUtil.getDataCenterId(31L), 31L));
    }

    public Snowflake(long workerId) {
        this(workerId, IdUtil.getDataCenterId(31L));
    }

    public Snowflake(long workerId, long dataCenterId) {
        this(workerId, dataCenterId, false);
    }

    public Snowflake(long workerId, long dataCenterId, boolean isUseSystemClock) {
        this((Date)null, workerId, dataCenterId, isUseSystemClock);
    }

    public Snowflake(Date epochDate, long workerId, long dataCenterId, boolean isUseSystemClock) {
        this(epochDate, workerId, dataCenterId, isUseSystemClock, DEFAULT_TIME_OFFSET);
    }

    public Snowflake(Date epochDate, long workerId, long dataCenterId, boolean isUseSystemClock, long timeOffset) {
        this.sequence = 0L;
        this.lastTimestamp = -1L;
        if (null != epochDate) {
            this.twepoch = epochDate.getTime();
        } else {
            this.twepoch = DEFAULT_TWEPOCH;
        }

        if (workerId <= 31L && workerId >= 0L) {
            if (dataCenterId <= 31L && dataCenterId >= 0L) {
                this.workerId = workerId;
                this.dataCenterId = dataCenterId;
                this.useSystemClock = isUseSystemClock;
                this.timeOffset = timeOffset;
            } else {
                throw new IllegalArgumentException(StrUtil.format("datacenter Id can't be greater than {} or less than 0", new Object[]{31L}));
            }
        } else {
            throw new IllegalArgumentException(StrUtil.format("worker Id can't be greater than {} or less than 0", new Object[]{31L}));
        }
    }

    public long getWorkerId(long id) {
        return id >> 12 & 31L;
    }

    public long getDataCenterId(long id) {
        return id >> 17 & 31L;
    }

    public long getGenerateDateTime(long id) {
        return (id >> 22 & 2199023255551L) + this.twepoch;
    }

    public synchronized long nextId() {
        long timestamp = this.genTime();
        if (timestamp < this.lastTimestamp) {
            if (this.lastTimestamp - timestamp >= this.timeOffset) {
                throw new IllegalStateException(StrUtil.format("Clock moved backwards. Refusing to generate id for {}ms", new Object[]{this.lastTimestamp - timestamp}));
            }

            timestamp = this.lastTimestamp;
        }

        if (timestamp == this.lastTimestamp) {
            long sequence = this.sequence + 1L & 4095L;

            //默认是这么写的,这里是一个应该是一个bug,如果是向下一秒借的情况,不需要等到下一毫秒,直接返回就行了
            if ( sequence == 0L) {
                timestamp = this.tilNextMillis(this.lastTimestamp);
            }
            //修正的写法
           /* if ( sequence == 0L) {
                if( timeOffset == 0 ){
                    timestamp = this.tilNextMillis(this.lastTimestamp);
                }else{
                    timestamp = timestamp+1;
                }
            }*/

            this.sequence = sequence;
        } else {
            this.sequence = 0L;
        }

        this.lastTimestamp = timestamp;
        return timestamp - this.twepoch << 22 | this.dataCenterId << 17 | this.workerId << 12 | this.sequence;
    }

    public String nextIdStr() {
        return Long.toString(this.nextId());
    }

    private long tilNextMillis(long lastTimestamp) {
        long timestamp;
        for(timestamp = this.genTime(); timestamp == lastTimestamp; timestamp = this.genTime()) {
        }

        if (timestamp < lastTimestamp) {
            throw new IllegalStateException(StrUtil.format("Clock moved backwards. Refusing to generate id for {}ms", new Object[]{lastTimestamp - timestamp}));
        } else {
            return timestamp;
        }
    }

    private long genTime() {
        return this.useSystemClock ? SystemClock.now() : System.currentTimeMillis();
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.
  • 76.
  • 77.
  • 78.
  • 79.
  • 80.
  • 81.
  • 82.
  • 83.
  • 84.
  • 85.
  • 86.
  • 87.
  • 88.
  • 89.
  • 90.
  • 91.
  • 92.
  • 93.
  • 94.
  • 95.
  • 96.
  • 97.
  • 98.
  • 99.
  • 100.
  • 101.
  • 102.
  • 103.
  • 104.
  • 105.
  • 106.
  • 107.
  • 108.
  • 109.
  • 110.
  • 111.
  • 112.
  • 113.
  • 114.
  • 115.
  • 116.
  • 117.
  • 118.
  • 119.
  • 120.
  • 121.
  • 122.
  • 123.
  • 124.
  • 125.
  • 126.
  • 127.
  • 128.
  • 129.
  • 130.
  • 131.
  • 132.
  • 133.
  • 134.
  • 135.
  • 136.
  • 137.
  • 138.
  • 139.
  • 140.
  • 141.
  • 142.
  • 143.
  • 144.
  • 145.
  • 146.
  • 147.
  • 148.
  • 149.
  • 150.
  • 151.

  

 //测试例子(测试使用hutool 生产409600个ID ,100毫秒默认生成的最大值,后面 new Snowflake(new Date(), 0, 0, false, 409600000) 最后指的的 偏移毫秒数,我随便指定的,只要大于  100 就行)

public static void main(String[] args) {
        
        Snowflake snowflake = new Snowflake(new Date(), 0, 0, false, 409600000);
        Set<Long> ids = new HashSet<>();

        Long a = System.currentTimeMillis();
        for(int i = 0;i<409600;i++   ){
            snowflake.nextId();
           // ids.add( snowflake.nextId() );
        }
        System.out.println( System.currentTimeMillis()-a );
        System.out.println( ids.size() );



	}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.

 

 

 

 

 

hutool 的写法 用时:

 

Snowflake 雪花算法 原理说明和注意事项_Java_02

 

 

 

 

 

 

修正后的耗时:

Snowflake 雪花算法 原理说明和注意事项_java_03

 

 

 

 

 

 

正确性验证代码:生成 409600 个 id并且去重复,然后得到生成的个数

public static void main(String[] args) {

        Snowflake snowflake = new Snowflake(new Date(), 0, 0, false, 409600000);
        Set<Long> ids = new HashSet<>();

        Long a = System.currentTimeMillis();
        for(int i = 0;i<409600;i++   ){
            //snowflake.nextId();
            ids.add( snowflake.nextId() );
        }
        System.out.println( System.currentTimeMillis()-a );
        System.out.println( ids.size() );



	}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.

 结果:

Snowflake 雪花算法 原理说明和注意事项_Java_04

 

hutool 版本  5.7.22

 

 

 

雪花算法,要保持全局唯一,必须要指定唯一的dataCenterId和 workerId,正常这两个数都是0-31 之间的一个值。
如果我们自己的商用节点,应该依赖注册中心计数器之类的自动设置dataCenterId和 workerId,如果是小集群,固定几台机子手动的为每隔节点指定 dataCenterId和workerId也行(比如读取指定目录下的一个文件)

hutool 里面的 雪花算法能用吗?

hutool里面的没有注册中心,所以不能保证全局唯一的dataCenterId和workId
但是 hutool里面里面的 dataCenterId 是通过物理地址算出来的,然后workId 是通过 dataCenterId+当前进程Id 算出来的.

结论:同一台物理机上 dataCenterId会相等,不同物理机的 dataCenterId 大概率不会相等
同一个Java进程里面 workId 会相等,Java进程重启 workId 会变化 大概率不会相等

所以可以简单的看成hutool随机的指定了dataCenterId和workId重复的概率 1/255 分之一,