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的问题。