Snowflake

  雪花算法简单来说是这样一个长整形数值。它64位,8个字节,刚好一个long,在单个节点上是有序的。如图它主要由4部分组成。

1bit:固定为0
  二进制里第一个bit如果是 1,表示负数,但是我们生成的 id都是正数,所以第一个 bit 统一都是 0。
41 bit:时间戳,单位毫秒
  表示的数字多达 2^41 - 1,也就是可以标识 2 ^ 41 - 1 个毫秒值。
10 bit:哪台机器产生的
  前5 个 bit 代表机房 id,后5 个 bit 代表机器 id。
12 bit:自增序列
  同1毫秒内,同一机器,可以产生2 ^ 12 - 1 = 4096个不同的 id。

package com.test;

import org.apache.logging.log4j.util.PropertiesUtil;

import java.io.IOException;
import java.util.Properties;

public class Snowflake {

    // ==============================Fields===========================================
    /** 序列的掩码,12个1,也就是(0B111111111111=0xFFF=4095) */
    private static final long SEQUENCE_MASK = 0xFFF;

    /**系统起始时间,这里取2020-01-01 **/
    private long startTimeStamp = 1577836800000L;

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

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

    // --------------  初始化  -------------
    /** 工作机器 ID(0~31) */
    private static long workerId;

    /** 工作组 ID(0~31) */
    private static long datacenterId;

    static{
        String filename = "application.yml";
        Properties pro = new Properties();
        try {
            pro.load(PropertiesUtil.class.getClassLoader().getResourceAsStream(filename));
        } catch (IOException e) {
            e.printStackTrace();
        }
        // 这种读取配置方式,没法在yml 里面写 snowflake:datacenterId: 2 的格式
        workerId = Long.valueOf(pro.getProperty("workerId"));
        datacenterId = Long.valueOf(pro.getProperty("datacenterId"));

        if (workerId > 31 || workerId < 0) {
            throw new IllegalArgumentException("workId必须在0-31之间,当前="+workerId);
        }
        if (datacenterId > 31 || datacenterId < 0) {
            throw new IllegalArgumentException("datacenterId必须在0-31之间,当前="+datacenterId);
        }
    }


//    /**
//     * 标注为 @Component 类,通过注解读取配置
//     * @param datacenterId 工作组 ID (0~31)
//     * @param workerId     工作机器 ID (0~31)
//     */
//    public Snowflake(@Value("${datacenterId}") long datacenterId, @Value("${workerId}") long workerId) {
//        if (workerId > 31 || workerId < 0) {
//            throw new IllegalArgumentException("workId必须在0-31之间,当前="+workerId);
//        }
//        if (datacenterId > 31 || datacenterId < 0) {
//            throw new IllegalArgumentException("datacenterId必须在0-31之间,当前="+datacenterId);
//        }
//
//        Snowflake.workerId = workerId;
//        Snowflake.datacenterId = datacenterId;
//    }

    /**
     * 加锁,线程安全
     * @return long 类型的 ID
     */
    public synchronized long nextId() {
        long timestamp = currentTime();

        // 如果当前时间小于上一次 ID 生成的时间戳,说明系统时钟回退过这个时候应当抛出异常
        if (timestamp < lastTimestamp) {
            throw new RuntimeException("时钟回退!时间差="+(lastTimestamp - timestamp));
        }

        // 同一毫秒内,序列增加
        if (lastTimestamp == timestamp) {
            //超出阈值。思考下为什么这么运算?
            sequence = (sequence + 1) & SEQUENCE_MASK;
            // 毫秒内序列溢出
            if (sequence == 0) {
                //自旋等待下一毫秒
                while ((timestamp= currentTime()) <= lastTimestamp) {

                }
            }
        } else {
            //已经进入下一毫秒,从0开始计数
            sequence = 0L;
        }

        //赋值为新的时间戳
        lastTimestamp = timestamp;

        //移位拼接
        long id = ((timestamp - startTimeStamp) << 22)
                | (datacenterId << 17)
                | (workerId << 12)
                | sequence;
        return id;
    }


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

//    /**
//     * 转成二进制展示
//     */
//    public static String toBit(long id){
//        String bit = org.apache.commons.lang3.StringUtils.leftPad(Long.toBinaryString(id), 64, "0");
//        return bit.substring(0,1) +
//                " - " +
//                bit.substring(1,42) +
//                " - " +
//                bit.substring(42,52)+
//                " - " +
//                bit.substring(52,64);
//    }

    public static void main(String[] args) {
        Snowflake idWorker = new Snowflake();

        for (int i = 0; i < 10; i++) {
            long id = idWorker.nextId();
            System.out.println(id);
        }
    }
}

  雪花算法 依赖于本地时钟。所以存在时钟回拨问题。Snowflake是一种约定,它把时间戳、工作组 ID、工作机器 ID、自增序列号组合在一起,生成一个 64bits 的整数ID,能够使用 (2^41)/(1000606024365) = 69.7 年,每台机器每毫秒理论最多生成 2^12 个 ID。那么,如何避免时钟回拨?可以参考下美团的实现:

  1. 机器号

  使用Zookeeper持久顺序节点的特性自动对snowflake节点配置wokerID,来保证机器id不重复。

  2. 相对时间

  每3秒上报timestamp。并且上报时,如果发现当前时间戳少于最后一次上报的时间戳,那么会放弃上报。防止在实例重启过程中,由于时钟回拨导致可能产生重复ID的问题。

posted @ 2022-04-12 17:32  吴磊的  阅读(346)  评论(0编辑  收藏  举报
//生成目录索引列表