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。

相关推荐
guodingdingh1 小时前
软件开发工作问题总结0718
java·开发语言·数据库
咖啡八杯3 小时前
GoF设计模式——解释器模式
java·后端·spring·设计模式
优橙教育3 小时前
5G网优培训 vs Java开发:转行选哪个?
java·开发语言·5g
糖果店的幽灵3 小时前
【DeepAgents 从入门到精通】Context Management 上下文管理
java·人工智能·后端·spring·中间件·langgraph·deepagents
腻害兔4 小时前
【若依项目-产品经理视角】RuoYi-Vue-Pro 源码拆解:字典、短信、邮件、通知——后台系统的“基础设施四件套“!
java·前端·vue.js·产品经理·ai编程
碎光拾影5 小时前
ARM交叉工具链各工具作用及IMX6ULL平台LED+蜂鸣器裸机程序实现
java·开发语言·数据库
Miao121316 小时前
微服务 API 测试实践:海外某民宿平台如何构建模式驱动测试基础设施
java·开发语言
腻害兔6 小时前
【若依项目-产品经理视角】RuoYi-Vue-Pro 源码拆解:支付模块 yudao-module-pay,一个让产品经理都看懂的支付中台设计
java·前端·vue.js·产品经理·ai编程
CRMEB系统商城7 小时前
开源自建还是SaaS订阅?算一笔3年经济账
java·大数据·开发语言·开源
Leo.yuan7 小时前
数据仓库建设怎么做?从源数据到分析报表全流程讲清
大数据·分布式·spark