雪花算法生成主键

在分布式系统中,为了保证数据的主键全局唯一且自增,可以使用Twitter的雪花算法(SnowFlake),它可按时间趋势递增.

1)算法原理

其算法生成的ID是一个64bit大小的整数,换成long类型是19位,它的结构如下图

 从左向右进行说明:

1)第1位(1bit)表示符号位。由于一般id都是正数,故此位是0;

2)第2-42位(41bit)表示时间戳,记录的是毫秒级的时间戳,大概可以使用69年;

3)第43-53位(10bit)表示工作机器的id(5位datacenterId+5位workerId),每台机器都有单独的机器ID,不会重复;

4)第54-64位(12bit)表示序列化号,记录同毫秒内产生的不同id

2)代码实现

package com.zys.example.util;

import java.security.SecureRandom;

public class SnowflakeUtil {
    //初始时间戳
    private static final long EPOCH_STAMP = 1262275200000L;
    //序列号id长度
    private static final long SEQUENCE_BIT = 12L;
    //数据id长度
    private static final long MACHINE_BIT = 5L;
    //工作id长度
    private static final long DATA_CENTER_BIT = 5L;
    //序列号最大值
    private static final long MAX_SEQUENCE_NUM = -1L ^ (-1L << SEQUENCE_BIT);
    //工作id需要左移的位数
    private static final long MACHINE_LEFT = SEQUENCE_BIT;
    //数据id需要左移位数
    private static final long DATA_CENTER_LEFT = SEQUENCE_BIT + MACHINE_BIT;
    //时间戳需要左移位数
    private static final long TIMESTAMP_LEFT = SEQUENCE_BIT + MACHINE_BIT + DATA_CENTER_BIT;
    //上次时间戳,初始值为负数
    private static long lastTimestamp = -1L;
    //机器id默认值
    private static final long MACHINE_ID = 1l;
    //数据id默认值
    private static final long DATACENTER_ID = 1l;
    //序列号默认值
    private static long sequence = 0L;

    //异步获取下一个值
    private static synchronized long getNextValue(Long machineId, long dataCenterId) throws Exception {
        String os = System.getProperty("os.name");
        SecureRandom secureRandom;
        if (os.toLowerCase().startsWith("win")) {
            // windows机器用
            secureRandom = SecureRandom.getInstanceStrong();
        } else {
            // linux机器用
            secureRandom = SecureRandom.getInstance("NativePRNGNonBlocking");
        }
        long currentTimeMillis = currentTimeMillis();
        //获取当前时间戳,如果当前时间戳小于上次时间戳,则时间戳获取出现异常
        if (currentTimeMillis < lastTimestamp) {
            throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", (lastTimestamp - currentTimeMillis)));
        }
        //如果等于上次时间戳(同一毫秒内),则在序列号加一;否则序列号赋值为0,从0开始
        if (currentTimeMillis == lastTimestamp) {
            sequence = (sequence + 1) & MAX_SEQUENCE_NUM;
            if (sequence == 0) {
                sequence = secureRandom.nextInt(Long.valueOf(SEQUENCE_BIT).intValue());
                currentTimeMillis = tilNextMillis(lastTimestamp);
            }
        } else {
            sequence = secureRandom.nextInt(Long.valueOf(SEQUENCE_BIT).intValue());
        }
        lastTimestamp = currentTimeMillis;
        long nextId = ((currentTimeMillis - EPOCH_STAMP) << TIMESTAMP_LEFT)
                | (dataCenterId << DATA_CENTER_LEFT)
                | (machineId << MACHINE_LEFT)
                | sequence;

        return nextId;
    }

    //获取时间戳,并与上次时间戳比较
    private static long tilNextMillis(long lastTimestamp) {
        long currentTimeMillis = currentTimeMillis();
        while (currentTimeMillis <= lastTimestamp) {
            currentTimeMillis = currentTimeMillis();
        }
        return currentTimeMillis;
    }

    //获取系统时间戳
    private static long currentTimeMillis() {
        return System.currentTimeMillis();
    }

    public static synchronized long nextValue() throws Exception {
        return getNextValue(MACHINE_ID, DATACENTER_ID);
    }

    public static synchronized long nextValue(long machineId) throws Exception {
        return getNextValue(machineId, DATACENTER_ID);
    }

    public static synchronized long nextValue(long machineId, long dataCenterId) throws Exception {
        return getNextValue(machineId, dataCenterId);
    }


}

3)代码调用

public static void main(String[] args) throws Exception {
     SnowflakeUtil.nextValue();
//        SnowflakeUtil.nextValue(1);
//        SnowflakeUtil.nextValue(1, 1);
 }

以上三种方法均可,一般使用第一种即可。

4)注意实现

由于其生成的long类型19位,在与前端交互时精度会丢失,也有解决办法

 

posted @ 2021-08-20 21:24  钟小嘿  阅读(670)  评论(0编辑  收藏  举报