Java面试题:分布式ID时钟回拨怎么处理?序列号耗尽了怎么办?

欢迎来到啾啾的博客🐱。

记录学习点滴。分享工作思考和实用技巧,偶尔也分享一些杂谈💬。

有很多很多不足的地方,欢迎评论交流,感谢您的阅读和评论😄。

目录

  • [1 分布式ID](#1 分布式ID)
  • [2 问题](#2 问题)
    • [2.1 时钟回拨](#2.1 时钟回拨)
      • [2.1.1 毫秒级时钟回拨则进行等待](#2.1.1 毫秒级时钟回拨则进行等待)
      • [2.1.2 引入时钟回拨位](#2.1.2 引入时钟回拨位)
      • [2.1.3 基于外部](#2.1.3 基于外部)
      • [2.1.4 混合时间戳](#2.1.4 混合时间戳)
    • [2.2 序列号耗尽](#2.2 序列号耗尽)
      • [2.2.1 等待时间戳](#2.2.1 等待时间戳)
      • [2.2.2 调整结构,增加序列号位数](#2.2.2 调整结构,增加序列号位数)
      • [2.2.3 多ID生成器](#2.2.3 多ID生成器)

现在典型的八股都是凝练了问题,背后有着对问题的认知和解决思路。

一直想写一个八股系列,从问题到原理。从这篇开始吧。

面试题:分布式ID时钟回拨怎么处理?序列号耗尽了怎么办?

1 分布式ID

分布式ID均会有一些需求。

高性能:有序

可维护:包含时间

可用:唯一

结构经常是多段结构,以UUIDv7为例

复制代码
[ 48位毫秒时间戳 ] [ 4位版本号'7' ] [ 76位随机数 ]

基于此,经常面临的问题如下。

2 问题

2.1 时钟回拨

2.1.1 毫秒级时钟回拨则进行等待

毫秒级时钟回拨,且对延迟不敏感场景。让ID生成器等待系统时钟追赶上上一次时间。

java 复制代码
long generateId() {
    long currentTimestamp = getCurrentTimeMillis();
    while (currentTimestamp < lastTimestamp) {
        // 检测到时钟回拨,等待
        Thread.sleep(1);
        currentTimestamp = getCurrentTimeMillis();
    }
    // 继续生成ID
    if (currentTimestamp == lastTimestamp) {
        sequence = (sequence + 1) & 4095; // 序列号递增
        if (sequence == 0) {
            // 序列号用尽,等待下一毫秒
            currentTimestamp = waitNextMillis(currentTimestamp);
        }
    } else {
        sequence = 0; // 新毫秒,重置序列号
    }
    lastTimestamp = currentTimestamp;
    return (currentTimestamp << 22) | (machineId << 12) | sequence;
}

2.1.2 引入时钟回拨位

引入时钟回拨位。在ID结构中预留几位作为"时钟回拨位"(rollback bits),当检测到时钟回拨时,递增回拨位以区分ID,避免重复。

2.1.3 基于外部

即给予外部服务的时钟,避免依赖本地时钟,如使用Redis生成递增ID。

  • 在Redis中每天生成一个Key(如date:20250615),通过INCR操作生成递增序列号。
  • ID格式:日期 + 机器ID + Redis递增序列号。

2.1.4 混合时间戳

结合物理时钟和逻辑时钟,生成混合时间戳,确保即使发生回拨,ID仍保持唯一性和递增性。

使用Google的TrueTime或类似机制,维护一个时间区间([min, max]),确保时间戳在安全范围内。

检测到回拨时,使用逻辑计数器递增,确保ID唯一。

2.2 序列号耗尽

在分布式ID生成中(如Snowflake算法),序列号耗尽是指在同一时间戳(通常是毫秒级)内,序列号部分达到了最大值(例如,12位序列号的最大值是4095),无法继续生成新的唯一ID。这是一个常见问题,尤其在高并发场景下,同一毫秒内可能需要生成大量ID。

2.2.1 等待时间戳

毫秒级的方案等待下一毫秒,纳秒级的方案等待下一纳秒。

java 复制代码
long generateId() {
    long currentTimestamp = getCurrentTimeMillis();
    if (currentTimestamp < lastTimestamp) {
        // 处理时钟回拨(参考上一回答)
        handleClockRollback();
    }
    if (currentTimestamp == lastTimestamp) {
        sequence = (sequence + 1) & 4095; // 递增序列号
        if (sequence == 0) {
            // 序列号耗尽,等待下一毫秒
            currentTimestamp = waitNextMillis(currentTimestamp);
        }
    } else {
        sequence = 0; // 新时间戳,重置序列号
    }
    lastTimestamp = currentTimestamp;
    return (currentTimestamp << 22) | (machineId << 12) | sequence;
}

long waitNextMillis(long lastTimestamp) {
    long current = getCurrentTimeMillis();
    while (current <= lastTimestamp) {
        current = getCurrentTimeMillis();
    }
    return current;
}

2.2.2 调整结构,增加序列号位数

通过调整ID结构,增加序列号的位数(例如从12位增加到14位),从而支持每毫秒生成更多ID。

2.2.3 多ID生成器

通过引入多个ID生成器实例或分片,分散ID生成压力,降低单个实例的序列号耗尽概率。

比如雪花ID的Snowflake,可以为每个节点分配多个机器ID(例如,节点A使用机器ID 1-4,节点B使用5-8)。

当序列号耗尽时,切换到另一个实例生成ID。

相关推荐
虾条_花吹雪1 分钟前
2、Connecting to Kafka
分布式·ai·kafka
考虑考虑35 分钟前
JDK9中的dropWhile
java·后端·java ee
想躺平的咸鱼干43 分钟前
Volatile解决指令重排和单例模式
java·开发语言·单例模式·线程·并发编程
hqxstudying1 小时前
java依赖注入方法
java·spring·log4j·ioc·依赖
·云扬·1 小时前
【Java源码阅读系列37】深度解读Java BufferedReader 源码
java·开发语言
Edingbrugh.南空2 小时前
Hadoop高可用集群搭建
大数据·hadoop·分布式
Bug退退退1232 小时前
RabbitMQ 高级特性之重试机制
java·分布式·spring·rabbitmq
小皮侠2 小时前
nginx的使用
java·运维·服务器·前端·git·nginx·github
Zz_waiting.3 小时前
Javaweb - 10.4 ServletConfig 和 ServletContext
java·开发语言·前端·servlet·servletconfig·servletcontext·域对象
全栈凯哥3 小时前
02.SpringBoot常用Utils工具类详解
java·spring boot·后端