什么是雪花算法

雪花算法(Snowflake)是一种生成分布式全局唯一ID的算法,生成的ID称为Snowflake IDs或snowflakes。这种算法由Twitter创建,并用于推文的ID。Discord和Instagram等其他公司采用了修改后的版本。

一个Snowflake ID有64位元。前41位是时间戳,表示了自选定的时期以来的毫秒数。 接下来的10位代表计算机ID,防止冲突。 其余12位代表每台机器上生成ID的序列号,这允许在同一毫秒内创建多个Snowflake ID。SnowflakeID基于时间生成,故可以按时间排序。 此外,一个ID的生成时间可以由其自身推断出来,反之亦然。该特性可以用于按时间筛选ID,以及与之联系的对象。

上述解释来源于wiki百科。

雪花算法实现思路

从上面的解释中可以知道,雪花算法依靠时间戳,序列号等来保证唯一性。

这里我们使用java中long类型64位来当做唯一id,然后看下这64是怎么分配的。

1

41

12

最高位为符号位

时间戳

序列号

按照上面这种分配方式使用了54位,还剩了10位没使用。而且我们还发现了一个问题
Q:按照这种方式,如果部署多台不是会重复吗?
那么按照思路,我们只需要将剩下的10位当做部署的机器码,那么这个问题就可以解决了,当然有一些还需要区分下应用类型。

1

41

5

5

12

最高位为符号位

时间戳

workspace

机器码

序列号

这样就保证了多台部署问题和不同应用有一个区分度(根据workspace来区分),到这数据位数的含义就分配完毕了,下面就是生成的流程了

  1. 获取当前时间timestamp和上一次获取id的事件lastTimestamp
  2. 判断时间
  1. 如果timestamp < lastTimestamp,那么抛出错误(因为后续时间必定大于之前的,不然无法保证id不重复,而出现这种情况可能是时钟回拨)
  2. 如果timestamp==lastTimeStamp,那么自增序列sequence+1,然后与其上限值比较
  1. 如果没超过上限值,那么继续向下执行
  2. 如果超过了上限值,自旋获取时间,直到获得下一个时间,然后设置sequence为0
  1. 如果timestamp>lastTimestamp,这种情况是最安全的,那么直接设置sequence为0
  2. 更新lastTimestamp
  3. 返回按位组装的long值

雪花算法的java实现

/**
 * @Author:TangFenQi
 * @Date:2021/11/4 11:43
 */
public class IdWorker {

    private static final int TIMESTAMP_BIT=41;//时间戳位数
    private static final int WORKSPACE_BIT=5;//工作空间位数
    private static final int MACHINE_BIT=5;//机器码位数
    private static final int SEQUENCE_BIT=12;//自增序列位数

    private static final int WORKSPACE_MASK=(1<<WORKSPACE_BIT)-1;//工作空间上界
    private static final int MACHINE_MASK=(1<<MACHINE_BIT)-1;//机器码上界
    private static final int SEQUENCE_MASK=(1<<SEQUENCE_BIT)-1; //自增寻列上界

    private static long lastTimestamp=-1; //记录上一次生成id时的时间戳
    private static long sequence=0;

    public static synchronized long nextId(int workspace,int machineId){
        Assert.isTrue(workspace>=0,String.format("workspace only be positive ! workspace[%n]",workspace));
        Assert.isTrue(workspace<=WORKSPACE_MASK,String.format("workspace number out of bound ! workspace:[%n],limit:[%n]",workspace,WORKSPACE_MASK));
        Assert.isTrue(machineId>=0,String.format("workspace only be positive ! machineId[%n]",workspace));
        Assert.isTrue(machineId<=MACHINE_MASK,String.format("workspace number out of bound ! machineId:[%n],limit:[%n]",workspace,MACHINE_MASK));
        //获取当前时间戳
        long timestamp = System.currentTimeMillis();
        if(timestamp<lastTimestamp){
            //比较是否大于上一次时间(防止时钟回拨导致生成重复ID)
            throw new IllegalArgumentException(String.format("lastTimestamp later then current time! pls check whether turn the clock?? current timestamp [%s] ,last timestamp [%s] ",timestamp,lastTimestamp));
        }else if(timestamp==lastTimestamp){
            //是否与上一次时间戳相等(需要分情况,可能出现向后借时间的问题)
            sequence=(sequence+1)&SEQUENCE_MASK;
            //如果越界
            if(sequence==0){
                timestamp=getNextTime(timestamp);
            }
        }else {
            //是否大于上一次时间戳(安全)
            sequence=0;
        }

        lastTimestamp=timestamp;
        return timestamp<<(WORKSPACE_BIT+MACHINE_BIT+SEQUENCE_BIT)|
                workspace<<(MACHINE_BIT+SEQUENCE_BIT)|
                machineId<<(SEQUENCE_BIT)|
                sequence;
    }

    private static long getNextTime(long currentTimestamp){
        long newTimestamp=System.currentTimeMillis();
        while (newTimestamp<=currentTimestamp){
            newTimestamp=System.currentTimeMillis();
        }
        return newTimestamp;
    }