雪花算法教程以及改进(java)
雪花算法概述
雪花算法(Snowflake Algorithm)是一种用于生成分布式系统中全局唯一ID的算法。这些ID通常是64位的整数,由一系列位段组成,每个位段都有其特定的含义和作用。雪花算法的设计目标是在不依赖集中式ID发号服务的情况下,实现高可用性和高并发性的ID生成。
-
雪花算法的基本原理
雪花算法的核心思想是将一个64位的长整数(在大多数编程语言中称为long
类型)用作ID,这个长整数被分割成多个部分,每个部分代表不同的信息。通过这种方式,算法能够在不同的机器上独立生成ID,而不会产生冲突。 -
雪花算法的组成部分
-
-
一个典型的64位雪花ID通常由以下几部分组成:
- 时间戳 - 这是雪花算法中最重要的部分,通常占用41位。时间戳部分记录了ID生成的时间,通常是相对于某个自定义的“纪元”时间的偏移量。这个时间戳保证了ID的唯一性和时间有序性。
- 机器标识 - 也称为工作机器ID,通常占用10位,用于标识生成ID的机器。每台机器都有一个唯一的ID,以确保在同一系统内不同机器生成的ID互不冲突。
- 序列号 - 这部分通常占用12位,用于在同一毫秒内生成多个ID。序列号在同一毫秒内从0开始递增,当达到最大值后(例如4095)会回绕到0。如果在同一毫秒内序列号已经增长到最大值,算法将等待直到下一毫秒继续生成ID。
通过以上组成部分的组合,雪花算法能够确保即使在高并发的环境下,也能快速生成全局唯一的ID。这些ID不仅在全局范围内是唯一的,而且还大致反映了生成顺序,这对于需要按时间排序的场景非常有用。
- 如何生成分布式唯一ID
当生成ID的请求到来时,雪花算法会按照以下步骤生成ID:
- 获取当前时间戳,与自定义纪元时间相减,得到时间戳差值。
- 获取数据中心标识和机器标识。
- 如果请求在同一毫秒内到达,则递增序列号;如果是新的毫秒,则重置序列号为0。
- 将时间戳差值、数据中心标识、机器标识和序列号拼接起来,生成最终的ID。
雪花算法的实现
雪花算法的实现严重依赖时间,因此如果发现时间回退需要抛出异常。
package com.example.springbootmybatis.controller;
/**
* Description :
* Created by Qian
* Date :2024/5/16
*/
public class SnowflakeIdWorker{
/** 开始时间截 (这个用自己业务系统上线的时间) */
private final long twepoch = 1575365018000L;
/** 机器id所占的位数 */
private final long workerIdBits = 10L;
/** 支持的最大机器id,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数) */
private final long maxWorkerId = -1L ^ (-1L << workerIdBits);
/** 序列在id中占的位数 */
private final long sequenceBits = 12L;
/** 机器ID向左移12位 */
private final long workerIdShift = sequenceBits;
/** 时间截向左移22位(10+12) */
private final long timestampLeftShift = sequenceBits + workerIdBits;
/** 生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095) */
private final long sequenceMask = -1L ^ (-1L << sequenceBits);
/** 工作机器ID(0~1024) */
private long workerId;
/** 毫秒内序列(0~4095) */
private long sequence = 0L;
/** 上次生成ID的时间截 */
private long lastTimestamp = -1L;
//==============================Constructors=====================================
/**
* 构造函数
* @param workerId 工作ID (0~1024)
*/
public SnowMaker(long workerId) {
if (workerId > maxWorkerId || workerId < 0) {
throw new IllegalArgumentException(String.format("workerId can't be greater than %d or less than 0", maxWorkerId));
}
this.workerId = workerId;
}
// ==============================Methods==========================================
/**
* 获得下一个ID (该方法是线程安全的)
* @return SnowflakeId
*/
public synchronized long nextId() {
long timestamp = timeGen();
//如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常
if (timestamp < lastTimestamp) {
throw new RuntimeException(
String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
}
//如果是同一时间生成的,则进行毫秒内序列
if (lastTimestamp == timestamp) {
sequence = (sequence + 1) & sequenceMask;
//毫秒内序列溢出
if (sequence == 0) {
//阻塞到下一个毫秒,获得新的时间戳
timestamp = tilNextMillis(lastTimestamp);
}
}
//时间戳改变,毫秒内序列重置
else {
sequence = 0L;
}
//上次生成ID的时间截
lastTimestamp = timestamp;
//移位并通过或运算拼到一起组成64位的ID
return ((timestamp - twepoch) << timestampLeftShift) //
| (workerId << workerIdShift) //
| sequence;
}
/**
* 阻塞到下一个毫秒,直到获得新的时间戳
* @param lastTimestamp 上次生成ID的时间截
* @return 当前时间戳
*/
protected long tilNextMillis(long lastTimestamp) {
long timestamp = timeGen();
while (timestamp <= lastTimestamp) {
timestamp = timeGen();
}
return timestamp;
}
/**
* 返回以毫秒为单位的当前时间
* @return 当前时间(毫秒)
*/
protected long timeGen() {
return System.currentTimeMillis();
}
}
测试
class SnowMakerTest {
@Test
void tilNextMillis() {
SnowflakeIdWorker snowMaker = new SnowflakeIdWorker(0);
for (int i = 0; i < 100; i++) {
long id = snowMaker.nextId();
System.out.println(id);
}
}
}
雪花算法的优点
高效率的ID生成
雪花算法能够在单个节点上每秒生成数百万个ID,这得益于其简单的数学运算。算法主要依赖于位运算来构造ID,这些操作在现代CPU上非常快速。因此,雪花算法可以满足高并发场景下对ID生成的需求,而不会成为系统的瓶颈。
无需依赖数据库
与基于数据库的自增ID或其他依赖数据库的ID生成策略不同,雪花算法不需要数据库的支持。这意味着它可以减少对数据库的访问压力,避免了网络延迟和数据库性能可能带来的影响。此外,由于不依赖于数据库,雪花算法也减少了系统的复杂性,并提高了系统的可用性。
单调递增
雪花算法生成的ID具有单调递增的特性,这是因为ID的最高位是基于时间戳的,而时间戳是随着时间单调递增的。这个特性对于需要按时间顺序排序记录的系统非常有用,因为它可以保证后生成的ID在数值上总是大于先生成的ID。这样,即使在不同的数据库或存储系统中,只要按照ID排序,就能大致反映出记录的创建顺序。
在分布式数据库或者需要全局排序的场景中,这个特性尤其重要。例如,在分布式日志系统中,通过ID的单调递增特性可以快速定位和检索日志条目。在电商平台的订单系统中,单调递增的ID可以帮助维护订单生成的顺序。
可扩展性和可配置性
雪花算法的设计允许通过配置数据中心标识和机器标识来扩展系统。这使得算法可以适应不同规模的系统,从小型项目到大型企业级应用。如果需要更多的数据中心或机器,可以通过调整相应的位数来分配更多的ID空间。此外,雪花算法的时间戳起始点(纪元时间)是可配置的,这为系统的迁移和升级提供了灵活性。
雪花算法改进
采用时间戳+系列号的方式更加简便,面对大多数业务来说足够使用
public class CustomSnowflake {
// 起始时间戳 (2023-01-01 00:00:00)
private final long twepoch = 1672444800000L;
// 时间戳的位数
private final long timestampBits = 31L;
// 序列号的位数
private final long sequenceBits = 32L;
// 最大时间戳值
private final long maxTimestamp = ~(-1L << timestampBits);
// 序列号掩码
private final long sequenceMask = ~(-1L << sequenceBits);
// 上次生成ID的时间戳
private long lastTimestamp = -1L;
// 序列号
private long sequence = 0L;
// 线程安全的锁对象
private final Object lock = new Object();
// 生成唯一ID
public long nextId() {
synchronized (lock) {
long timestamp = timeGen();
// 如果当前时间小于上次生成ID的时间戳,抛出异常
if (timestamp < lastTimestamp) {
throw new RuntimeException(
String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
}
// 如果在相同的毫秒内生成ID,则递增序列号
if (lastTimestamp == timestamp) {
sequence = (sequence + 1) & sequenceMask;
// 如果序列号溢出,则阻塞直到下一毫秒
if (sequence == 0) {
timestamp = tilNextMillis(lastTimestamp);
}
} else {
// 如果是新的毫秒,重置序列号
sequence = 0L;
}
lastTimestamp = timestamp;
// 生成ID
return ((timestamp - twepoch) << sequenceBits) | sequence;
}
}
// 阻塞直到下一毫秒
protected long tilNextMillis(long lastTimestamp) {
long timestamp = timeGen();
while (timestamp <= lastTimestamp) {
timestamp = timeGen();
}
return timestamp;
}
// 获取当前时间戳(毫秒)
protected long timeGen() {
return System.currentTimeMillis();
}
// 测试方法
public static void main(String[] args) {
CustomSnowflake customSnowflake = new CustomSnowflake();
for (int i = 0; i < 10; i++) {
long id = customSnowflake.nextId();
System.out.println(id);
}
}
}
输出: