谈谈id那些事(四)——雪花 ID(Snowflake ID)

你好,我是吴计可师
点击下方👇关注公众号,带你一起复习后端技术,看看面试考点,补充积累技术知识,每天都为面试准备积累

雪花 ID(Snowflake ID) 是一种分布式全局唯一 ID 生成算法,由 Twitter 提出,专为分布式系统设计。其设计目标是高效生成全局唯一的、时间有序的 ID,适合高并发的分布式环境。


01
雪花 ID 的结构


一个典型的雪花 ID 是一个 64 位的长整型数(long 类型),分为以下几个部分:

位数含义描述
1 位符号位恒为 0,正数。
41 位时间戳从某一特定时间开始的毫秒数,可使用约 69 年。
10 位工作机器 ID(Worker ID)标识生成该 ID 的机器,最多支持 1024 个节点(5 位数据中心 ID + 5 位机器 ID)。
12 位序列号同一毫秒内生成的序列号,最多支持每毫秒生成 4096 个 ID。


示意图

| 0 | 41 位时间戳 | 5 位数据中心 ID | 5 位机器 ID | 12 位序列号 |
  • 符号位(1 位):

    • 固定为 0,保证生成的 ID 是正数。

  • 时间戳(41 位):

    • 从一个自定义的起始时间(Epoch)开始的毫秒数。

    • 41 位可以表示约 69 年(241/(1000×60×60×24×365)241/(1000×60×60×24×365))。

  • 工作机器 ID(10 位):

    • 分为 5 位数据中心 ID 和 5 位机器 ID,最多支持 1024 个节点。

  • 序列号(12 位):

    • 用于在同一毫秒内生成多个 ID。

    • 每毫秒最多生成 4096 个 ID(212212)。

02
雪花 ID 的特点


优点

  • 全局唯一性:

    • 通过时间戳、机器 ID 和序列号的组合,确保 ID 唯一。

  • 高性能:

    • 本地生成,不依赖锁或数据库,单节点每秒可生成数百万个 ID。

  • 趋势递增:

    • ID 的时间戳位决定了它按时间递增,有利于数据库索引。

缺点

  • 强依赖时钟:

    • 如果机器时间不准确(如回拨),可能会导致重复 ID。

  • 占用空间:

    • ID 的长度较长(64 位),相比自增 ID,占用更多存储空间。

03
雪花 ID 的实现


以下是一个简单的 Java 实现:

代码实现

public class SnowflakeIdGenerator {    // 起始时间戳(Epoch),建议固定为项目启动时间    private static final long EPOCH = 1609459200000L; // 2021-01-01 00:00:00    // 各部分位数    private static final long WORKER_ID_BITS = 5L; // 机器 ID 位数    private static final long DATA_CENTER_ID_BITS = 5L; // 数据中心 ID 位数    private static final long SEQUENCE_BITS = 12L; // 序列号位数    // 最大值    private static final long MAX_WORKER_ID = ~(-1L << WORKER_ID_BITS); // 31    private static final long MAX_DATA_CENTER_ID = ~(-1L << DATA_CENTER_ID_BITS); // 31    // 位移    private static final long WORKER_ID_SHIFT = SEQUENCE_BITS;    private static final long DATA_CENTER_ID_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS;    private static final long TIMESTAMP_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS + DATA_CENTER_ID_BITS;    // 序列号掩码    private static final long SEQUENCE_MASK = ~(-1L << SEQUENCE_BITS); // 4095    private long workerId; // 机器 ID    private long dataCenterId; // 数据中心 ID    private long sequence = 0L; // 当前序列号    private long lastTimestamp = -1L; // 上一次生成 ID 的时间戳    // 构造器    public SnowflakeIdGenerator(long workerId, long dataCenterId) {        if (workerId > MAX_WORKER_ID || workerId < 0) {            throw new IllegalArgumentException("Worker ID out of range");        }        if (dataCenterId > MAX_DATA_CENTER_ID || dataCenterId < 0) {            throw new IllegalArgumentException("Data Center ID out of range");        }        this.workerId = workerId;        this.dataCenterId = dataCenterId;    }    // 生成 ID    public synchronized long nextId() {        long timestamp = currentTime();        // 如果当前时间小于上次时间,说明时钟回拨        if (timestamp < lastTimestamp) {            throw new RuntimeException("Clock moved backwards. Refusing to generate ID");        }        // 如果是同一毫秒内,增加序列号        if (timestamp == lastTimestamp) {            sequence = (sequence + 1) & SEQUENCE_MASK;            // 序列号用尽            if (sequence == 0) {                timestamp = waitNextMillis(lastTimestamp); // 等到下一毫秒            }        } else {            sequence = 0L; // 不同毫秒,重置序列号        }        lastTimestamp = timestamp;        // 生成 ID        return ((timestamp - EPOCH) << TIMESTAMP_SHIFT) // 时间戳部分                | (dataCenterId << DATA_CENTER_ID_SHIFT) // 数据中心部分                | (workerId << WORKER_ID_SHIFT) // 机器 ID 部分                | sequence; // 序列号部分    }    // 等待下一毫秒    private long waitNextMillis(long lastTimestamp) {        long timestamp = currentTime();        while (timestamp <= lastTimestamp) {            timestamp = currentTime();        }        return timestamp;    }    // 获取当前时间    private long currentTime() {        return System.currentTimeMillis();    }    // 测试    public static void main(String[] args) {        SnowflakeIdGenerator generator = new SnowflakeIdGenerator(1, 1);        for (int i = 0; i < 10; i++) {            System.out.println(generator.nextId());        }    }}


04
雪花 ID 的应用场景


  • 分布式系统中的唯一标识符:微服务架构中,订单号、用户 ID 等唯一标识。

  • 分布式缓存:用于生成缓存键,避免冲突。


  • 数据库主键:比如 MySQL 的 bigint 主键字段,配合雪花 ID 可以高效支持高并发插入。

05
雪花 ID 的扩展与优化


解决时间回拨问题:

  • 检测到时间回拨时,可以引入一定的等待机制,或者记录时间戳偏移。

  • 使用 NTP 服务校准时间。

调整结构:

  • 缩减时间戳位数,增加机器 ID 位数,用于更多节点场景。

  • 增加序列号位数,适应极高并发场景。

  • 增加或减少各部分位数,以适应特定业务场景。例如:

分布式改进:

  • 雪花 ID 算法可以部署在多个节点上,利用 ZooKeeper 或 Etcd 分配唯一的工作机器 ID 和数据中心 ID。

06
直击面试


6.1 什么是雪花算法(Snowflake)?

问题解析:这个问题主要考察你对雪花算法的基础理解,尤其是它如何工作和为什么被广泛使用。

参考答案:雪花算法是由 Twitter 提出的分布式 ID 生成算法,用于在分布式系统中生成全局唯一且有序的 ID。雪花算法生成的 ID 通常为 64 位二进制数,其中包含以下几个部分:

  • 1 位符号位:通常为 0(表示正数)。

  • 41 位时间戳:表示从某个固定时间(通常是纪元时间,如 1970 年 1 月 1 日)开始的毫秒数,能够支持 69 年的时间跨度。

  • 10 位机器 ID:可以表示 1024 个不同的节点或机器。

  • 12 位序列号:用于同一毫秒内生成多个 ID,最多每毫秒能生成 4096 个唯一 ID。

雪花算法的核心是基于时间戳生成递增的 ID,能够在分布式系统中生成唯一且有序的 ID。


6.2 雪花算法生成的 ID 为什么是唯一的?

问题解析:考察你对雪花算法设计原理的理解,特别是如何确保全局唯一性。

参考答案:雪花算法能够生成唯一的 ID 主要基于以下几个因素:

  • 时间戳:时间戳是基于毫秒的,可以确保同一时刻生成的 ID 不会重复。时间戳的位数足够长(41 位),可以支持大约 69 年的 ID 生成。

  • 机器 ID:每个机器(节点)都有一个唯一的机器 ID。雪花算法将机器 ID 分配给不同的机器,确保每台机器生成的 ID 不会重复。

  • 序列号:每个机器在同一毫秒内可以生成多个 ID,最多支持每毫秒生成 4096 个 ID。每次生成 ID 时,序列号自增,确保同一毫秒内的多个 ID 不会冲突。

通过时间戳、机器 ID 和序列号的组合,雪花算法可以保证生成的 ID 在分布式环境中是唯一的。


6.3 雪花算法生成 ID 是如何保证有序的?

问题解析:这个问题考察你对雪花算法在生成 ID 时如何保持顺序性的理解。

参考答案:雪花算法保证生成的 ID 有序主要是通过时间戳来实现的。由于雪花算法的 ID 由时间戳和自增序列号组成,其中时间戳部分是按毫秒递增的。因此,生成的 ID 会随着时间的推移而递增。

  • 时间戳递增:由于时间戳是基于毫秒的,每次生成的 ID 时间戳部分会递增,确保生成的 ID 按时间顺序排列。

  • 序列号递增:在同一毫秒内,雪花算法通过自增的序列号来区分不同的 ID,因此,同一毫秒内生成的 ID 也能按顺序排列。

总之,雪花算法通过时间戳的递增以及序列号的自增来保证 ID 的有序性。


6.4 雪花算法能支持多少个机器节点并发生成 ID?

问题解析:这个问题考察你对雪花算法中机器 ID 和节点数量的理解。

参考答案:雪花算法中,机器 ID 部分使用了 10 位 来表示,因此可以支持 2^10 = 1024 个不同的机器节点或工作进程。在一个集群中,如果有 1024 个机器节点,雪花算法能够确保每个节点生成的 ID 唯一。

如果节点数超过 1024 个,可以通过扩展机器 ID 的位数来增加可支持的机器节点数,但这需要修改雪花算法的位数划分。


6.5 雪花算法的时钟回拨问题如何解决?

问题解析:这个问题考察你对雪花算法中的时钟回拨问题的理解,以及如何解决该问题。

参考答案:时钟回拨是指机器的系统时间发生回退,例如由手动调整时间或系统故障导致。在雪花算法中,时钟回拨会导致生成的 ID 不连续,甚至重复。

为了避免这个问题,雪花算法使用了以下几种策略:

  • 监控时钟回拨:雪花算法会监控系统时间是否发生回拨。如果检测到回拨,算法会停止生成新的 ID,直到系统时间恢复正常。

  • 等待机制:如果检测到时钟回拨,雪花算法会等待一段时间,直到系统时间回到一个安全的状态。这样可以避免生成重复的 ID。

  • 回退机制:如果时钟回拨,雪花算法可以回退到最近的合法时间戳,继续生成 ID,确保 ID 的递增性。

通过这些机制,雪花算法能够有效避免时钟回拨带来的问题。


6.6 雪花算法生成的 ID 为什么是 64 位?

问题解析:考察你对雪花算法位数划分的理解,尤其是为什么选择 64 位来表示 ID。

参考答案:雪花算法生成的 ID 是 64 位的,64 位的设计是为了满足以下需求:

  • 时间戳部分(41 位):用于表示生成 ID 的时间,精度到毫秒。41 位的时间戳可以表示约 69 年的时间跨度,足够支持大规模系统的需求。

  • 机器 ID 部分(10 位):用于表示生成 ID 的机器节点或数据中心的标识符。10 位可以支持最多 1024 个机器节点或数据中心。

  • 序列号部分(12 位):用于表示同一毫秒内生成的多个 ID。12 位的序列号可以在同一毫秒内生成最多 4096 个唯一 ID。

64 位的设计既能保证 ID 的唯一性,又能够支持高并发和大规模的分布式系统。


6.7 雪花算法与 Redis 自增 ID 相比有什么优势和劣势?

问题解析:这个问题考察你对雪花算法与 Redis 自增 ID 的对比理解。

参考答案:雪花算法和 Redis 自增 ID 各自有优缺点,适用于不同的场景:

  • 雪花算法的优势:

    • 分布式生成:雪花算法可以在多个机器节点上并行生成全局唯一的 ID,避免了单点故障和性能瓶颈。

    • 高效生成:雪花算法生成 ID 非常高效,可以支持高并发的场景。

    • 有序性:生成的 ID 基于时间戳递增,因此具有顺序性,适合需要排序的场景。

    • 无依赖性:不依赖于外部服务,如 Redis,不会成为单点故障。

  • Redis 自增 ID 的优势:

    • 简单易用:通过 Redis 的 INCR 命令可以简单地生成自增 ID,使用起来非常方便。

    • 支持高并发:Redis 的单线程模型非常适合高并发的场景。

  • 雪花算法的劣势:

    • 机器配置复杂:需要提前配置机器 ID 和数据中心 ID,管理起来较为复杂。

    • 时钟回拨问题:雪花算法受系统时钟的影响,时钟回拨可能导致 ID 冲突。

  • Redis 自增 ID 的劣势:

    • 单点故障:如果 Redis 实例宕机且没有持久化机制,可能导致已生成的 ID 丢失。

    • 性能瓶颈:高并发场景下,Redis 可能成为性能瓶颈,尤其是在单个节点上进行 ID 生成时。

6.8 雪花算法如何支持秒级别的 ID 生成?

问题解析:考察你对雪花算法的扩展性和优化的理解,尤其是在生成 ID 时的时间精度。

参考答案:雪花算法默认是基于毫秒级别的时间戳来生成 ID,精度是 1 毫秒。如果需要生成秒级别的 ID,可以通过以下几种方式:

  • 降低时间戳精度:可以将时间戳的精度降低到秒级(例如将 41 位的时间戳中的低 10 位调整为 0),从而生成秒级别的 ID。

  • 增加序列号的位数:通过增加序列号部分的位数,可以在每秒内生成更多的 ID




扫码关注

一起积累后端知识
不积跬步,无以至千里
不积小流,无以成江海



喜欢此内容的人还喜欢

谈谈id那些事(三)——阿里巴巴的 TDDL的ID生成


谈谈id那些事(二)——Redis 自增 ID


谈谈id那些事(一)——数据库的自增ID