一、雪花算法介绍 雪花算法(Snowflake Algorithm)是由 Twitter 公司开发的一种分布式 ID 生成算法,用于在分布式系统环境中高效地生成全局唯一 ID。这个算法生成的 ID 是一个 64 位的整数,结构如下:
符号位(1 bit) :最高位是符号位,始终为 0,表示这是一个正整数。
时间戳(41 bits) :接下来的 41 位是毫秒级时间戳,从一个自定义的起始时间点(通常是某个特定的时间,比如 2020-01-01 00:00:00)开始计算的偏移量。这可以确保不同机器上的 ID 在时间上是有序的,并且能够支持大约 69 年的时间跨度。
机器标识(10 bits) :再接下来的 10 位是机器标识,可以用来区分不同的工作机器。这部分可以进一步划分为数据中心 ID 和机器 ID,例如用 5 位表示数据中心,用 5 位表示同一数据中心内的机器 ID。
序列号(12 bits) :最后的 12 位是序列号,在同一个毫秒内,同一台机器上产生的不同 ID 会通过原子递增的方式分配不同的序列号。这使得即使在同一毫秒内也可以生成多个唯一的 ID。
这种设计的优点在于它可以在分布式的环境下快速生成不重复的 ID,并且这些 ID 在整体上有一定的顺序性,这对于某些数据库索引是有利的。此外,由于使用了时间戳,所以生成的 ID 也隐含了时间信息,有助于调试和故障排查。
1、优点
全局唯一 :雪花算法生成的ID是全局唯一的,可以用于分布式系统中的数据分片和数据合并,避免了 ID 冲突的问题。
时间有序 :雪花算法生成的 ID 中包含了时间戳信息,可以根据 ID 的大小推算出生成的时间,方便进行数据排序和查询。
高性能 :雪花算法生成 ID 的速度很快,可以满足高并发的场景需求。
可扩展性 :雪花算法的 数据结构相对简单,易于扩展和修改。
2、缺点
依赖于系统时钟 :雪花算法生成 ID 的过程中依赖于系统时钟,如果系统时钟发生回拨,可能会导致生成的 ID 出现重复。
长度固定 :雪花算法生成的 ID 长度固定为 64 位,可能会导致存储和传输成本较高。
不支持分布式计算 :雪花算法生成 ID 的过程是单线程的,不能支持分布式计算。
二、源码 这里的源码看的是 hutool 中封装的雪花算法。
其中有几个重要的属性:
1 2 3 4 5 6 7 8 9 private long sequence = 0L ;private long lastTimestamp = -1L ;private final long randomSequenceLimit;private final long timeOffset;private final long epoch;
epoch
表示初始化时间点。雪花算法是基于时间的,64 bit 位中有 41 位来记录毫秒值,该值表示的是生成 ID 的时间点与 epoch
属性的差值,41 位毫秒值,2 ^ 41 = 2,199,023,255,552
,换算成年份大约 69 年左右,所以这个时间需要自定义一下,尽量靠近系统初次上线前的时间。因为是差值,后续如果修改这个 epoch
默认开始时间,会造成生成 ID 重复。
sequence
为自增序号
lastTimestamp
为最后一次生成 ID 的时间
randomSequenceLimit
用于限制生成 ID 的随机范围
timeOffset
允许的时钟回拨毫秒数
1、机器标识如何生成 有 10 位需要用来作为机器标识,10 个 bit 位,最多可以区分 1024 个 Java 进程实例。在这 10 位又分为数据中心 ID 和机器 ID。
根据机器的 MAC 地址来生成 dataCenter Id
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 public static long getDataCenterId (long maxDatacenterId) { Assert.isTrue(maxDatacenterId > 0 , "maxDatacenterId must be > 0" ); if (maxDatacenterId == Long.MAX_VALUE) { maxDatacenterId -= 1 ; } long id = 1L ; byte [] mac = null ; try { mac = NetUtil.getLocalHardwareAddress(); } catch (UtilException ignore) { } if (mac != null ) { id = ((0x000000FF & (long ) mac[mac.length - 2 ]) | (0x0000FF00 & (((long ) mac[mac.length - 1 ]) << 8 ))) >> 6 ; id = id % (maxDatacenterId + 1 ); } return id; }
根据 Java 进程的 PID 来生成 worker ID
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public static long getWorkerId (long datacenterId, long maxWorkerId) { final StringBuilder mpid = new StringBuilder (); mpid.append(datacenterId); try { mpid.append(RuntimeUtil.getPid()); } catch (UtilException igonre) { } return (mpid.toString().hashCode() & 0xffff ) % (maxWorkerId + 1 ); }
可以根据实际需要修改这 10 位中,dataCenter Id 和 worker Id 所占用的位数
程序启动时,使用常量类来存储当前 Java 进程实例的 dataCenter Id 和 Worker Id
1 2 3 4 5 6 7 8 9 10 11 12 13 public class IdConstants { public static final long DEFAULT_DATACENTER_ID = IdUtils.getDataCenterId(Snowflake.MAX_DATA_CENTER_ID); public static final long DEFAULT_WORKER_ID = IdUtils.getWorkerId(DEFAULT_DATACENTER_ID, Snowflake.MAX_WORKER_ID); }
2、ID 生成 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 public synchronized long nextId () { long timestamp = genTime(); if (timestamp < this .lastTimestamp) { if (this .lastTimestamp - timestamp < timeOffset) { timestamp = lastTimestamp; } else { throw new IllegalStateException (StrUtil.format("Clock moved backwards. Refusing to generate id for {} ms" , lastTimestamp - timestamp)); } } if (timestamp == this .lastTimestamp) { final long sequence = (this .sequence + 1 ) & SEQUENCE_MASK; if (sequence == 0 ) { timestamp = this .tilNextMillis(lastTimestamp); } this .sequence = sequence; } else { if (randomSequenceLimit > 1 ) { sequence = RandomUtil.randomLong(randomSequenceLimit); } else { sequence = 0L ; } } lastTimestamp = timestamp; return ((timestamp - epoch) << TIMESTAMP_LEFT_SHIFT) | (dataCenterId << DATA_CENTER_ID_SHIFT) | (workerId << WORKER_ID_SHIFT) | sequence; }
该方法用于生成新的 ID,在生成 ID 的时间是否与上一次生成时间处于同一毫秒时,需要执行的逻辑不一样,并且会限制同一毫秒,同一 Java 进程中生成的 ID 数量为 4096 个 ID,如果超过这个数量,将会等待到下一毫秒去生成。
另外,该实现中还处理了正在低调用量时,每一毫秒生成的 ID 很少,生成的 ID 序号都为 0,导致生成的 ID 都为偶数的问题。
3、根据 ID 获取各个组成部分 3.1 根据 ID 获取 worker ID 1 2 3 4 5 6 7 8 9 10 11 12 13 14 public long getWorkerId (long id) { return id >> WORKER_ID_SHIFT & ~(-1L << WORKER_ID_BITS); }
3.2 根据 ID 获取 dataCenter ID 1 2 3 4 5 6 7 8 9 public long getDataCenterId (long id) { return id >> DATA_CENTER_ID_SHIFT & ~(-1L << DATA_CENTER_ID_BITS); }
3.3 根据 ID 获取生成时间 1 2 3 4 5 6 7 8 9 public long getGenerateDateTime (long id) { return (id >> TIMESTAMP_LEFT_SHIFT & ~(-1L << 41L )) + epoch; }
4、IdUtils 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 public class IdUtils { private static final LocalDateTime LOCAL_DATE_TIME = LocalDateTime.of(2024 , 12 , 2 , 12 , 25 , 43 ); public static Snowflake getSnowflake () { return Singleton.get(Snowflake.class); } public static Snowflake getSnowflake (long workerId) { return Singleton.get(Snowflake.class, workerId); } public static Snowflake getSnowflake (long workerId, long datacenterId) { return Singleton.get(Snowflake.class, workerId, datacenterId); } public static Snowflake getSnowflake (LocalDateTime epochDate, long workerId, long dataCenterId, boolean isUseSystemClock) { return Singleton.get(Snowflake.class, epochDate, workerId, dataCenterId, isUseSystemClock); } public static long nextId () { return getSnowflake(LOCAL_DATE_TIME, IdConstants.DEFAULT_WORKER_ID, IdConstants.DEFAULT_DATACENTER_ID, true ).nextId(); } public static String nextIdStr () { return getSnowflake(LOCAL_DATE_TIME, IdConstants.DEFAULT_WORKER_ID, IdConstants.DEFAULT_DATACENTER_ID, true ).nextIdStr(); } }
需要向数据库中插入数据时,使用 IdUtils.nextId
或 IdUtils.nextIdStr
方法来生成 ID,方法中使用了自定义的计算差值开始时间。
三、其他问题 1、为什么不用数据库自增主键 之前有一个项目,使用的是主主复制,北中心和南中心使用各自的数据库,数据在北中心和南中心之间进行主主复制,当时创建序列时,还要特意在首位区分开南中心和北中心,比如,南中心以 1 开头,北中心以 2 开头。
雪花 ID 是有顺序的,对于索引来说,有序会使查询效率更高,而使用 UUID 是无序的,比较不适合作为主键。
2、关于 System.currentTimeMillis()
性能 柠檬夕桐/sequence - 码云 - 开源中国
3、雪花算法时钟回拨问题 雪花算法依赖系统时钟,雪花算法生成 ID 的组成部分中的时间戳是计算的生成 ID 的时间到设定好的起始时间的差值,当时钟回拨发生时,再次生成时这个计算的差值可能会再次出现,就有可能会生成重复的 ID,这将会破坏系统一致性和稳定性。
另外就算不产生重复的 ID,但是发生时钟回拨后,新生成的 ID 可能比回拨前已经生成的 ID 小,即全局递增性质被破坏。如果有业务依赖 ID 的递增性质,可能会发生问题。
出现时钟回拨的原因:
NTP 时间同步异常。NTP(网络时间协议)在同步时间时可能会导致时间回拨。
手动调整时间。
虚拟环境出现问题。比如虚拟机挂起后恢复时,系统时钟可能回拨。
解决时钟回拨问题,有如下几个常见方法:
直接抛出异常。
延迟等待。
切换机器。在分布式系统中,会部署多机器多实例,当某个节点发生回拨时,可以切换到其他正常的节点来生成 ID。雪花 ID 中有 10 位是机器标识,通过操作使雪花 ID 的机器标识为之前一直未使用的机器标识。
追赶时钟。
使用备用时间戳。记录最后一次生成 ID 的时间,当发生时钟回拨后,判断生成时间是否小于记录的最后一次生成 ID 的时间,如果小于则使用最后记录的时间来生成 ID。
相关链接 柠檬夕桐/sequence - 码云 - 开源中国
百度 UidGenerator 和美团 Leaf | 死磕 Java
百度 UidGenerator | 死磕 Java
OB links #Java #后端