雪花算法的java实现(不用设置机器id和数据id)

雪花算法的java实现(不用设置机器id和数据id)_第1张图片
Snowflake生成的是Long类型的ID,一个Long类型占8个字节,每个字节占8比特,也就是说一个Long类型占64个比特。

Snowflake ID组成结构:正数位(占1比特)+ 时间戳(占41比特)+ 机器ID(占5比特)+ 数据中心(占5比特)+ 自增值(占12比特),总共64比特组成的一个Long类型。

其实我们通过图片可以看出来,我们如果保证单机唯一,只需要保证前面 时间戳(占41比特)和 自增值(占12比特) 保证唯一,我们就可以以保证单机唯一。而当分布式的情况下,我们需要保证 不同机器上面 机器ID(占5比特)+ 数据中心(占5比特)的唯一保证 不同机器生成 机器ID(占5比特)+ 数据中心(占5比特)来保证唯一。当分布式的时候来保证生成 机器ID(占5比特)+ 数据中心(占5比特)这两个唯一,要么通过redis,zookeeper这类工具,让不同机器 机器id 和数据中心id不同。要么启动的时候将这两参数,来设置到对应参数中。

那么我们有没有一种方法,不用 通过redis,zookeeper这类工具。也不用启动的时候的时候设置这两个值,这个时候不同机器并没有通信 机器id 和 数据中心id,必然会导致不同机器的的id出现相同值的可能性。那我们可以通过ip地址分布尽量缩小这种可能性。这个出现相同的可能性极低,基本可以满足绝大部分任务场景的一种妥协方案。

public class SnowflakeIdWorkerUtils {

    // ==============================Fields===========================================


    /**
     * 机器id所占的位数
     */
    private static final int WORK_LEN = 5;

    /**
     * 数据标识id所占的位数
     */
    private static final int DATA_LEN = 5;

    /**
     * 毫秒内序列的长度
     */
    private static final int SEQ_LEN = 12;

    /**
     * 时间部分所占长度
     */
    private static final int TIME_LEN = 41;

    /**
     * 开始时间截 (2015-01-01)
     */
    private static final long START_TIME = 1420041600000L;


    /**
     * 上次生成iD的时间戳
     */
    private static volatile long LAST_TIME_STAMP = -1L;


    /**
     * 时间部分向左移动的位数22(雪花算法总长度64,最高位是符号位,正数是0,负数是1,所以id一般是正数,最高位是 1)
     */
    private static final int TIME_LEFT_BIT = 64 - 1 - TIME_LEN;


    private static final long DATA_ID = getDataId();

    private static final long WORK_ID = getWorkId();

    /**
     * 数据中心id最大值 31
     */
    private static final int DATA_MAX_NUM = ~(-1 << DATA_LEN);

    /**
     * 数据中心id最大值 31
     */
    private static final int WORK_MAX_NUM = ~(-1 << WORK_LEN);


    /**
     * 机器随机获取数据中中心id的参数 32
     */
    private static final int DATA_RANDOM = DATA_MAX_NUM + 1;


    /**
     * 随机获取的机器id的参数
     */
    private static final int WORK_RANDOM = WORK_MAX_NUM + 1;

    /**
     * 数据中心id左移位数 17
     */
    private static final int DATA_LEFT_BIT = TIME_LEFT_BIT - DATA_LEN;

    /**
     * 机器id左移位数 12
     */
    private static final int WROK_LEFT_BIT = DATA_LEFT_BIT - WORK_LEN;

    /**
     * 上一次毫秒的序列值
     */
    private static volatile long LAST_SEQ = 0L;


    /**
     * 毫秒内序列的最大值 4095
     */
    private static final long SEQ_MAX_NUM = ~(-1 << SEQ_LEN);





    // ==============================Methods==========================================

    /**
     * 根据host name 取余,发生异常就获取0到31之间的随机数
     * @return
     */
    public static int getWorkId() {

        try {
            return getHostId(Inet4Address.getLocalHost().getHostAddress(), WORK_MAX_NUM);
        } catch (UnknownHostException e) {
            return new Random().nextInt(WORK_RANDOM);
        }

    }

    /**
     * 根据host name 取余,发生异常就获取0到31之间的随机数
     * @return
     */
    public static int getDataId() {

        try {
            return getHostId(Inet4Address.getLocalHost().getHostAddress(), DATA_MAX_NUM);
        } catch (UnknownHostException e) {
            return new Random().nextInt(DATA_RANDOM);
        }

    }

    /**
     * 根据host name 取余
     *
     * @return
     */
    private static int getHostId(String s, int max) {
        byte[] bytes = s.getBytes();
        int sums = 0;
        for (byte b : bytes) {
            sums += b;
        }
        return sums % (max + 1);
    }

    /**
     * 获得下一个ID (该方法是线程安全的)
     *
     * @return SnowflakeId
     */
    public static  synchronized long genId() {
        long now = timeGen();

        //如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常
        if (now < LAST_TIME_STAMP) {
            throw new RuntimeException(
                    String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds", LAST_SEQ - now));
        }

        //如果是同一时间生成的,则进行毫秒内序列
        if (now == LAST_TIME_STAMP) {
            LAST_SEQ = (LAST_SEQ + 1) & SEQ_MAX_NUM;
            //毫秒内序列溢出
            if (LAST_SEQ == 0) {
                //阻塞到下一个毫秒,获得新的时间戳
                now = tilNextMillis(LAST_TIME_STAMP);
            }
        }
        //时间戳改变,毫秒内序列重置
        else {
            LAST_SEQ = 0L;
        }

        //上次生成ID的时间截
        LAST_TIME_STAMP = now;

        //移位并通过或运算拼到一起组成64位的ID
        return ((now - START_TIME) << TIME_LEFT_BIT)
                | (DATA_ID << DATA_LEFT_BIT)
                | (WORK_ID << WROK_LEFT_BIT)
                | LAST_SEQ;
    }

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

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

    //==============================Test=============================================

    /**
     * 测试
     */
    public static void main(String[] args) {
        HashSet ids = new HashSet<>(100);
        long startTime = System.currentTimeMillis();
        for (int i = 0; i < 100; i++) {
            long id = genId();
            System.out.println(id);
            ids.add(id);
        }
        long endTime = System.currentTimeMillis();
        System.out.println("共生成id["+ids.size()+"] 个,共花费时间["+(endTime-startTime)+"]");
    }
}

如果我们业务不允许这种极低出现重复id的可能性,我们可以使用。下面三家开源的id生成器来保证唯一。

美团(Leaf)
Leaf由美团开发,github地址:https://github.com/Meituan-Dianping/Leaf
Leaf同时支持号段模式和snowflake算法模式,可以切换使用.

滴滴(Tinyid)

Tinyid由滴滴开发,Github地址:https://github.com/didi/tinyid。
Tinyid是基于号段模式原理实现的与Leaf如出一辙,每个服务获取一个号段(1000,2000]、(2000,3000]、(3000,4000]

百度(uid-generator)
uid-generator是由百度技术部开发,项目GitHub地址 https://github.com/baidu/uid-generator

uid-generator是基于Snowflake算法实现的,与原始的snowflake算法不同在于,uid-generator支持自定义时间戳、工作机器ID和 序列号 等各部分的位数,而且uid-generator中采用用户自定义workId的生成策略。

uid-generator需要与数据库配合使用,需要新增一个WORKER_NODE表。当应用启动时会向数据库表中去插入一条数据,插入成功后返回的自增ID就是该机器的workId数据由host,port组成。

对于uid-generator ID组成结构:

workId,占用了22个bit位,时间占用了28个bit位,序列化占用了13个bit位,需要注意的是,和原始的snowflake不太一样,时间的单位是秒,而不是毫秒,workId也不一样,而且同一应用每次重启就会消费一个workId。

你可能感兴趣的:(工具类)