分布式 ID 生成器的解决方案

设计一个分布式环境下的全局唯一的发号器,或者也称分布式 ID 生成器,需要考虑分布式系统的特点,以及实现全局唯一性、高性能、低延迟等方面的需求。

现在通用的做法是以雪花算法为基础。

一、雪花算法(Snowflake Algorithm)

雪花算法是一种常用于分布式系统生成唯一 ID 的算法,它的核心思想是使用一个 64 位的整数作为全局唯一 ID。

1、雪花算法的组成

分布式 ID 生成器的解决方案_第1张图片

一个雪花算法生成的 64 位 ID 通常包括以下部分

  • 时间戳(Timestamp):通常占位 41bit,用于记录 ID 生成的时间,以毫秒为单位,这个时间戳是从 1970 年开始计算,可以使用 69 年,为了不浪费,在实际项目中,可以用时间的相对值来进行计算
  • 工作机器 ID:占位 10bit,用于标识不同的机器,因为在分布式系统中,每个机器都有唯一的 ID。但如果使用机房 ID & 工作机器 ID,就分别占用 5bit,用于标识不同的机房不同的机器
  • 序列号(Sequence):在同一个毫秒内,如果有多个请求需要生成 ID,序列号用于区分这些请求,通常占位 12bit,可以保证在同一毫秒内生成 4096 个不重复的 ID
2、雪花算法的特点
  • 全局唯一性:确保在分布式系统中生成的每个 ID 都是唯一的
  • 时间戳相关:ID 中包含时间戳信息,可以通用 ID 大致推断出数据的生成时间
  • 有序性:由于包含时间戳,所以生成的 ID 在时间上是有序的
  • 高性能:生成 ID 的操作简单且快速,能够满足高并发的需求
  • 可扩展性:雪花算法本身的设计允许根据需要进行扩展和定制
3、雪花算法的实现步骤

雪花算法的实现通常包含以下步骤:

  • 获取当前的时间戳(毫秒级别)
  • 将时间戳左移一定的位数(如 22 位),为工作机器 ID、机房 ID 的序列号流出空间
  • 将工作机器 ID 和机房 ID 合并后,再左移一定的位数(如 12 位),为序列号留出空间
  • 生成一个序列号,并与前面的部分进行按位或操作,得到最终的 ID
  • 如果在同一毫秒内生成的 ID 数量达到了最大序列号 4096,则需要等待下一毫秒再生成新的 ID
4、雪花算法的具体实现

使用 Java 代码实现雪花 ID 生成器:

public class SnowflakeIdGenerator {
    // 起始时间戳,可以自由设置
    private static final long START_TIMESTAMP = 1609459200000L;

    // 每一部分占用的位数
    private static final long DATA_CENTER_ID_BITS = 5L;
    private static final long MACHINE_ID_BITS = 5L;
    private static final long SEQUENCE_BITS = 12L;

    // 每一部分的最大值
    private static final long MAX_DATA_CENTER_ID = ~(-1L << DATA_CENTER_ID_BITS);
    private static final long MAX_MACHINE_ID = ~(-1L << MACHINE_ID_BITS);
    private final static long MAX_SEQUENCE = -1L ^ (-1L << SEQUENCE_BIT);

    // 每一部分向左的位移
    private static final long MACHINE_ID_SHIFT = SEQUENCE_BITS;
    private static final long DATA_CENTER_ID_SHIFT = SEQUENCE_BITS + MACHINE_ID_BITS;
    private static final long TIMESTAMP_LEFT_SHIFT = SEQUENCE_BITS + MACHINE_ID_BITS + DATA_CENTER_ID_BITS;

    // 数据中心/机房
    private final long dataCenterId;
    // 机器
    private final long machineId;
    // 序列号
    private long sequence = 0L;
    // 上一次时间戳
    private long lastTimestamp = -1L;

    public SnowflakeIdGenerator(long dataCenterId, long machineId) {
        if (dataCenterId > MAX_DATA_CENTER_ID || dataCenterId < 0) {
            throw new IllegalArgumentException("Data center ID can't be greater than " + MAX_DATA_CENTER_ID + " or less than 0");
        }
        if (machineId > MAX_MACHINE_ID || machineId < 0) {
            throw new IllegalArgumentException("Machine ID can't be greater than " + MAX_MACHINE_ID + " or less than 0");
        }
        this.dataCenterId = dataCenterId;
        this.machineId = machineId;
    }

    public synchronized long generateId() {
        long currentTimestamp = System.currentTimeMillis();

        if (currentTimestamp < lastTimestamp) {
            throw new RuntimeException("Clock moved backwards. Refusing to generate ID");
        }

        if (currentTimestamp == lastTimestamp) {
            // 相同毫秒内序列号自增
            sequence = (sequence + 1) & ((1 << SEQUENCE_BITS) - 1);
            // 同一毫秒内序列数已经达到最大
            if (sequence == 0) {
                // Sequence overflow, wait until next millisecond
                currentTimestamp = waitNextMillis(currentTimestamp);
            }
        } else {
            // 不同毫秒内序列号置 0
            sequence = 0L;
        }

        lastTimestamp = currentTimestamp;

        return ((currentTimestamp - START_TIMESTAMP) << TIMESTAMP_LEFT_SHIFT) | // 时间戳部分
               (dataCenterId << DATA_CENTER_ID_SHIFT) |  // 数据中心部分
               (machineId << MACHINE_ID_SHIFT) |		// 机器标识部分
               sequence;								// 序列号部分
    }

    private long waitNextMillis(long currentTimestamp) {
        long timestamp = System.currentTimeMillis();
        while (timestamp <= currentTimestamp) {
            timestamp = System.currentTimeMillis();
        }
        return timestamp;
    }
}

使用 Java 代码示例:

public class SnowflakeIdGeneratorExample {
    public static void main(String[] args) {
        SnowflakeIdGenerator idGenerator = new SnowflakeIdGenerator(1, 1);

        for (int i = 0; i < 10; i++) {
            long id = idGenerator.generateId();
            System.out.println("Generated ID: " + id);
        }
    }
}
5、注意事项
时钟回拨

服务服务器的时钟发生了回拨,也就是时间倒退,可能会导致生成重复的 ID,为了解决这个问题,可以再雪花算法中加入逻辑来检测时钟回拨,并在发生时采取相应的措施,比如等待时钟恢复或者抛出异常。

机器 ID 和机房 ID 的分配

需要有一个中心化的服务来管理和分配机器 ID 和机房 ID,以确保它们的唯一性。

性能优化

可以通过使用更高效的数据结构和算法来提高 ID 生成的性能,比如可以使用缓存来减少频繁获取系统时间戳的开销。

扩展性

在设计时需要考虑未来可能的扩展需求,比如增加更多的数据中心或者机器节点,可以调整个部分所占的位数来实现。

二、其他实现方案

1、UUID(Universally Unique Identifier)

UUID 是一种标准化的全局唯一标识符,通常由 32 个十六进制数字组成,分为五段,比如:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx。UUID 基于时间、机器 MAC 地址、命名空间等因素生成,因此具有极高的全局唯一性。它的生成不依赖于数据库,且生成速度快,适用于分布式系统。但是,UUID 的一个缺点是它相对较长,占用的存储空间较大,不易于人阅读和记忆。而且无序,不适用于需要有序的场景。

2、数据库自增主键

在单数据库系统中,可以利用数据库的自增主键来生成全局唯一的 ID,简单但在分布式系统中,如果多个数据库实例分别生成 ID,就可能导致 ID 冲突,为了解决这个问题,可以通过设置不同的起始值和步长来实现多个数据库实例之间的 ID 分段生成,从而保证全局唯一性。但这种方法需要数据库支持,且在高并发场景下性能可能受限。

3、Redis 生成 ID

Redis 是高性能的内存数据库,可以用来生成全局唯一性的 ID,利用 Redis 的原子操作(INCR or INCRBY)可以很容易地实现 ID 的自增和全局为一些。而且 Redis 还支持分布式环境下的操作,可以通过 Redis 集群来保证高可用性和可扩展性。但要注意,如果 Redis 服务发生故障,可能会影响 ID 生成的可用性。

4、Twitter 的 Snaowflake 改进版

虽然雪花算法本身已经相当成熟和稳定,但在实际应用中仍然可以根据具体需求进行改进和优化。比如,可以调整个部分所占的位数以适应不同的业务场景。可以采用更高效的时间获取方式来减少系统调用开销。可以引入更多的因素,比如业务类型、用户 ID 等来增强 ID 的语义信息。这些改进可以保证全局唯一性的基础上进一步提升 ID 生成的性能和灵活性。

5、其他第三方库或者工具

现在有一些第三方库或者工具提供了全局唯一 ID 的生成功能,比如 Google Protobuf 中的 MessageId、Apache 中的 Commons id 等。这些库或者工具通常提高了丰富的配置选项和扩展接口以满足不同场景下的需求。但在使用这些第三方库或者工具的时候需要了解内部实现原理以及可能存在的限制和问题,这样可以更好地进行集成和维护。

你可能感兴趣的:(技术专项能力,分布式,雪花算法,分布式,ID)