京东面试:如何设计600Wqps高并发ID?如何解决时钟回拨问题?

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
云数据库 RDS MySQL,高可用系列 2核4GB
简介: 资深架构师尼恩在其读者交流群中分享了关于分布式ID系统的设计与实现,特别是针对高并发场景下的解决方案。他强调了分布式ID系统在高并发核心组件中的重要性,并详细介绍了百度的UidGenerator,这是一个基于Snowflake算法改进的Java实现,旨在解决分布式系统中的唯一ID生成问题。UidGenerator通过自定义workerId位数和初始化策略,支持虚拟化环境下的实例自动重启和漂移,其单机QPS可达600万。此外尼恩的技术分享不仅有助于提升面试表现,还能帮助开发者在实际项目中应对高并发挑战。

尼恩说在前面

在40岁老架构师 尼恩的读者交流群(50+)中,最近有小伙伴拿到了一线互联网企业如得物、阿里、滴滴、极兔、有赞、shein 希音、shopee、百度、网易的面试资格,遇到很多很重要的面试题:

一个分布式 、高并发 的 ID系统,如何实现?

ID生产,如何实现100万 级别 的高并发?

前几天 小伙伴面试 京东,遇到了这个问题。但是由于 没有回答好,导致面试挂了。

小伙伴面试完了之后,来求助尼恩。

首先,分布式ID很重要,在所有的高并发核心组件中,分布式ID 是核心中的核心、重点中的重点 。

分布式ID 组件,是整个系统 黄金链路上的关键组件、黄金组件

如果分布式ID 组件出现问题,整个黄金链路上关键动作都无法执行,这就会带来一场灾难,一定是P0级大灾难。

那么,遇到分布式ID 组件这个问题,该如何才能回答得很漂亮,才能 让面试官刮目相看、口水直流。

所以,尼恩给大家做一下系统化、体系化的梳理,使得大家内力猛增,可以充分展示一下大家雄厚的 “技术肌肉”,让面试官爱到 “不能自已、口水直流”,然后实现”offer直提”。

当然,这道面试题,以及参考答案,也会收入咱们的 《尼恩Java面试宝典》V145版本PDF集群,供后面的小伙伴参考,提升大家的 3高 架构、设计、开发水平。

最新《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》的PDF,请关注本公众号【技术自由圈】获取,后台回复:领电子书

站上巨人肩膀上学习

高并发 的 ID有两种,一种是叶段id,一种是雪花id。关于叶段id的学习,具体请参见尼恩的视频:

顶奢、优质项目:1000W级qps ID组件 架构与实操

尼恩提示,在这里不展开 叶段id,仅 介绍另一个种高并发:雪花id。

而 高并发、分布式雪花算法的代表作: 非 百度的 UidGenerator 莫属。

UidGenerator是Java实现的, 基于Snowflake算法的唯一ID生成器。百度的UidGenerator用来生成全局的唯一ID,是雪花算法的改进版。

UidGenerator以组件形式工作在应用项目中, 支持自定义workerId位数和初始化策略,适用于docker等虚拟化环境下实例自动重启、漂移等场景。

githubhttps://github.com/baidu/uid-generator

在实现上, UidGenerator通过借用未来时间来解决sequence天然存在的并发限制;

采用RingBuffer来缓存已生成的UID, 并行化UID的生产和消费, 同时对CacheLine补齐,避免了由RingBuffer带来的硬件级「伪共享」问题.

结果是,最终单机QPS可达600万

UidGenerator 依赖版本:

  • Java8及以上版本,
  • MySQL(内置WorkerID分配器, 启动阶段通过DB进行分配; 如自定义实现, 则DB非必选依赖)

Snowflake的二进制位段 设计

Snowflake是Twitter开源的一种分布式ID生成算法,用于在分布式系统中生成全局唯一的ID。

它的设计目标是高性能、低延迟和趋势递增的ID生成。

Snowflake的二进制位段 设计,大致为4段 (如果不包括前面的符号位,就是3段)

在这里插入图片描述

Snowflake生成的ID是一个64位的整数,由以下部分组成:

  1. sign(1bit) 固定1bit符号标识,即生成的UID为正数。
  1. 时间戳(41位):使用41位存储毫秒级的时间戳,表示自定义的起始时间(Epoch)到生成ID的时间之间的毫秒数。
41bit-时间可以表示(1L<<41)/(1000L*3600*24*365)=69年的时间。
  1. 节点ID(10位):用于标识不同的节点或机器。在分布式系统中,每个节点应具有唯一的节点ID。
10bit-机器可以表示1024台机器。
如果对IDC划分有需求,还可以将10-bit分5-bit给IDC,分5-bit给工作机器。
这样就可以表示32个IDC,每个IDC下可以有32台机器。
  1. 序列号(12位):在同一毫秒内生成的序列号。如果在同一毫秒内生成的ID数量超过了12位能够表示的范围,那么会等待下一毫秒再生成ID。
12个自增序列号可以表示2^12个ID,理论上snowflake方案的QPS约为409.6w/s。

Snowflake生成的ID具有趋势递增的特点,因为高位部分是基于时间戳生成的。

Snowflake算法描述:指定机器 & 同一时刻 & 某一并发序列,是唯一的。

据此可生成一个64 bits的唯一ID(long)。

UidGenerator 的二进制位段 设计

百度的 UidGenerator 依然是以划分命名空间的方式将 64-bit位分割成多个部分,默认划分方式有别于雪花算法:它默认是由 1-28-22-13 的格式进行划分,可调整各个字段占用的位数。
UidGenerator 的二进制位段 设计,为4段(如果不包括前面的符号位,就是3段)

在这里插入图片描述

  • 第1位:标志位
    • 仍然占用1bit,其值始终是0:即生成的UID为 正数,不是负数
  • 第2位开始的28bit:时间戳(当前时间,相对于epoch时间的增量值)
    • 可表示2^28个数,不再是以毫秒为单位,而是以秒为单位,可用(1L<<28)/ (360024365) ≈ 8.51 年(最多可用8.7年,后边讲解)。
    • epoch时间:指使用 UidGenerator生成分布式ID服务,第一次上线的时间,epoch可配置,默认的epoch时间是2016-09-20,不配置的话,会浪费好几年的可用时间。
  • 第29位开始的22bit:中间的 workId (数据中心+工作机器,可以其他组成方式)
    • 可表示 2^22 = 4194304个工作ID(最多可支持约420w次机器启动)。
    • 内置实现:在启动时由数据库分配;默认分配策略:用后即弃;后续可提供复用策略。
  • 最后的13-bit位:并发序列(自增)
    • 表示每秒的并发数量,默认为2
    • ^13 = 8192个并发(即:默认qps为8192)。

43岁老架构师尼恩提示,UidGenerator 的二进制位段 ,每一段的长度是可以变化的,可以配置的。

以上UidGenerator 的二进制位段 设计 参数,可均 通过Spring进行自定义

UidGeneratorQuick Start

这里介绍如何在基于Spring的项目中使用UidGenerator, 具体流程如下:

步骤1: 安装依赖

先下载Java8, MySQLMaven

设置环境变量

maven无须安装, 设置好MAVEN_HOME即可. 可像下述脚本这样设置JAVA_HOME和MAVEN_HOME, 如已设置请忽略.

export MAVEN_HOME=/xxx/xxx/software/maven/apache-maven-3.3.9
export PATH=$MAVEN_HOME/bin:$PATH
JAVA_HOME="/Library/Java/JavaVirtualMachines/jdk1.8.0_91.jdk/Contents/Home";
export JAVA_HOME;

步骤2: 创建表WORKER_NODE

运行sql脚本以导入表WORKER_NODE, 脚本如下:

DROP DATABASE IF EXISTS `xxxx`;
CREATE DATABASE `xxxx` ;
use `xxxx`;
DROP TABLE IF EXISTS WORKER_NODE;
CREATE TABLE WORKER_NODE
(
ID BIGINT NOT NULL AUTO_INCREMENT COMMENT 'auto increment id',
HOST_NAME VARCHAR(64) NOT NULL COMMENT 'host name',
PORT VARCHAR(64) NOT NULL COMMENT 'port',
TYPE INT NOT NULL COMMENT 'node type: ACTUAL or CONTAINER',
LAUNCH_DATE DATE NOT NULL COMMENT 'launch date',
MODIFIED TIMESTAMP NOT NULL COMMENT 'modified time',
CREATED TIMESTAMP NOT NULL COMMENT 'created time',
PRIMARY KEY(ID)
)
 COMMENT='DB WorkerID Assigner for UID Generator',ENGINE = INNODB;

修改mysql.properties配置中, jdbc.url, jdbc.username和jdbc.password, 确保库地址, 名称, 端口号, 用户名和密码正确.

步骤3: 修改Spring配置

提供了两种生成器: DefaultUidGeneratorCachedUidGenerator

如对UID生成性能有要求, 请使用CachedUidGenerator

对应Spring配置分别为: https://github.com/baidu/uid-generator/blob/master/default-uid-spring.xmlcached-uid-spring.xml

官方给的DefaultUidGenerator配置
<!-- DefaultUidGenerator -->
<bean id="defaultUidGenerator" class="com.baidu.fsg.uid.impl.DefaultUidGenerator" lazy-init="false">
    <property name="workerIdAssigner" ref="disposableWorkerIdAssigner"/>

    <!-- Specified bits & epoch as your demand. No specified the default value will be used -->
    <property name="timeBits" value="29"/>
    <property name="workerBits" value="21"/>
    <property name="seqBits" value="13"/>
    <property name="epochStr" value="2016-09-20"/>
</bean>

<!-- 用完即弃的WorkerIdAssigner,依赖DB操作 -->
<bean id="disposableWorkerIdAssigner" class="com.baidu.fsg.uid.worker.DisposableWorkerIdAssigner" />
官方给的CachedUidGenerator配置
<!-- CachedUidGenerator -->
<bean id="cachedUidGenerator" class="com.baidu.fsg.uid.impl.CachedUidGenerator">
    <property name="workerIdAssigner" ref="disposableWorkerIdAssigner" />

    <!-- 以下为可选配置, 如未指定将采用默认值 -->
    <!-- Specified bits & epoch as your demand. No specified the default value will be used -->
    <property name="timeBits" value="29"/>
    <property name="workerBits" value="21"/>
    <property name="seqBits" value="13"/>
    <property name="epochStr" value="2016-09-20"/>

    <!-- RingBuffer size扩容参数, 可提高UID生成的吞吐量. -->
    <!-- 默认:3, 原bufferSize=8192, 扩容后bufferSize= 8192 << 3 = 65536 -->
    <property name="boostPower" value="3"></property>

    <!-- 指定何时向RingBuffer中填充UID, 取值为百分比(0, 100), 默认为50 -->
    <!-- 举例: bufferSize=1024, paddingFactor=50 -> threshold=1024 * 50 / 100 = 512. -->
    <!-- 当环上可用UID数量 < 512时, 将自动对RingBuffer进行填充补全 -->
    <property name="paddingFactor" value="50"></property>

    <!-- 另外一种RingBuffer填充时机, 在Schedule线程中, 周期性检查填充 -->
    <!-- 默认:不配置此项, 即不实用Schedule线程. 如需使用, 请指定Schedule线程时间间隔, 单位:秒 -->
    <property name="scheduleInterval" value="60"></property>

    <!-- 拒绝策略: 当环已满, 无法继续填充时 -->
    <!-- 默认无需指定, 将丢弃Put操作, 仅日志记录. 如有特殊需求, 请实现RejectedPutBufferHandler接口(支持Lambda表达式) -->
    <property name="rejectedPutBufferHandler" ref="XxxxYourPutRejectPolicy"></property>

    <!-- 拒绝策略: 当环已空, 无法继续获取时 -->
    <!-- 默认无需指定, 将记录日志, 并抛出UidGenerateException异常. 如有特殊需求, 请实现RejectedTakeBufferHandler接口(支持Lambda表达式) -->
    <property name="rejectedTakeBufferHandler" ref="XxxxYourTakeRejectPolicy"></property>

</bean>

<!-- 用完即弃的WorkerIdAssigner, 依赖DB操作 -->
<bean id="disposableWorkerIdAssigner" class="com.baidu.fsg.uid.worker.DisposableWorkerIdAssigner" />
官方给的Mybatis配置

mybatis-spring.xml配置说明如下:

<!-- Spring annotation扫描 -->
<context:component-scan base-package="com.baidu.fsg.uid" />

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
    <property name="dataSource" ref="dataSource" />
    <property name="mapperLocations" value="classpath:/META-INF/mybatis/mapper/M_WORKER*.xml" />
</bean>

<!-- 事务相关配置 -->
<tx:annotation-driven transaction-manager="transactionManager" order="1" />

<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource" />
</bean>

<!-- Mybatis Mapper扫描 -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
    <property name="annotationClass" value="org.springframework.stereotype.Repository" />
    <property name="basePackage" value="com.baidu.fsg.uid.worker.dao" />
    <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" />
</bean>

<!-- 数据源配置 -->
<bean id="dataSource" parent="abstractDataSource">
    <property name="driverClassName" value="${mysql.driver}" />
    <property name="maxActive" value="${jdbc.maxActive}" />
    <property name="url" value="${jdbc.url}" />
    <property name="username" value="${jdbc.username}" />
    <property name="password" value="${jdbc.password}" />
</bean>

<bean id="abstractDataSource" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close">
    <property name="filters" value="${datasource.filters}" />
    <property name="defaultAutoCommit" value="${datasource.defaultAutoCommit}" />
    <property name="initialSize" value="${datasource.initialSize}" />
    <property name="minIdle" value="${datasource.minIdle}" />
    <property name="maxWait" value="${datasource.maxWait}" />
    <property name="testWhileIdle" value="${datasource.testWhileIdle}" />
    <property name="testOnBorrow" value="${datasource.testOnBorrow}" />
    <property name="testOnReturn" value="${datasource.testOnReturn}" />
    <property name="validationQuery" value="${datasource.validationQuery}" />
    <property name="timeBetweenEvictionRunsMillis" value="${datasource.timeBetweenEvictionRunsMillis}" />
    <property name="minEvictableIdleTimeMillis" value="${datasource.minEvictableIdleTimeMillis}" />
    <property name="logAbandoned" value="${datasource.logAbandoned}" />
    <property name="removeAbandoned" value="${datasource.removeAbandoned}" />
    <property name="removeAbandonedTimeout" value="${datasource.removeAbandonedTimeout}" />
</bean>

<bean id="batchSqlSession" class="org.mybatis.spring.SqlSessionTemplate">
    <constructor-arg index="0" ref="sqlSessionFactory" />
    <constructor-arg index="1" value="BATCH" />
</bean>

步骤4: 运行示例单测

运行单测CachedUidGeneratorTest, 展示UID生成、解析等功能

@Resource
private UidGenerator uidGenerator;

@Test
public void testSerialGenerate() {
   
    // Generate UID
    long uid = uidGenerator.getUID();

    // Parse UID into [Timestamp, WorkerId, Sequence]
    // {"UID":"180363646902239241","parsed":{    "timestamp":"2017-01-19 12:15:46",    "workerId":"4",    "sequence":"9"        }}
    System.out.println(uidGenerator.parseUID(uid));

}

DefaultUidGenerator 的底层原理 和核心源码 学习

DefaultUidGenerator的代码很简单,尼恩带着大家,围绕三段进行介绍。

前面讲到,UidGenerator 的二进制位段 设计,为3段(不包括前面的符号位,就是3段),

在这里插入图片描述

第一位段:delta seconds

这个值是指当前时间与epoch时间的时间差,且单位为秒。

epoch时间就是指集成UidGenerator生成ID服务第一次上线的时间,可配置,也一定要根据你的上线时间进行配置,因为默认的epoch时间可是2016-09-20,不配置的话,会浪费好几年的可用时间。

第二位段:workerId

UidGenerator是如何给worker id赋值的?搭建UidGenerator的话,需要创建一个表

DROP TABLE IF EXISTS WORKER_NODE;
CREATE TABLE WORKER_NODE(
  ID BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY ,
  HOST_NAME VARCHAR(64) NOT NULL COMMENT 'host name',
  PORT VARCHAR(64) NOT NULL COMMENT 'port',
  TYPE INT NOT NULL COMMENT 'node type: ACTUAL or CONTAINER',
  LAUNCH_DATE DATE NOT NULL COMMENT 'launch date',
  MODIFIED DATETIME NOT NULL COMMENT 'modified time',
  CREATED DATEIMTE NOT NULL COMMENT 'created time'
)COMMENT='DB WorkerID Assigner for UID Generator',ENGINE = INNODB;

UidGenerator会在集成用它生成分布式ID的实例启动的时候,往这个表中插入一行数据,得到的id值就是准备赋给workerId的值。

由于workerId默认22位,那么,集成UidGenerator生成分布式ID的所有实例重启次数是不允许超过4194303次(即2^22-1),否则会抛出异常。

生成workerId的核心代码来自 DisposableWorkerIdAssigner.java中,当然,你也可以实 WorkerIdAssigner.java 接口,自定义生成workerId。

第三位段:sequence (秒内的序号)

核心代码如下,几个实现的关键点:

  • synchronized保证线程安全;
  • 如果时间有任何的回拨,那么直接抛出异常;
  • 如果当前时间和上一次是同一秒时间,那么sequence自增。如果同一秒内自增值超过2^13-1,那么就会自旋等待下一秒(getNextSecond);
  • 如果是新的一秒,那么sequence重新从0开始;
protected synchronized long nextId() {
   
    long currentSecond = getCurrentSecond();
    if (currentSecond < lastSecond) {
   
        long refusedSeconds = lastSecond - currentSecond;
        throw new UidGenerateException("Clock moved backwards. Refusing for %d seconds", refusedSeconds);
    }
    if (currentSecond == lastSecond) {
   
        sequence = (sequence + 1) & bitsAllocator.getMaxSequence();
        if (sequence == 0) {
   
            currentSecond = getNextSecond(lastSecond);
        }
    } else {
   
        sequence = 0L;
    }
    lastSecond = currentSecond;
    return bitsAllocator.allocate(currentSecond - epochSeconds, workerId, sequence);
}

CachedUidGenerator 的底层原理 和核心源码 学习

UidGenerator 提供了两种生成唯一ID方式,分别是 DefaultUidGenerator 和 CachedUidGenerator,官方建议如果有性能考虑的话使用 CachedUidGenerator 方式实现.

CachedUidGenerator 是性能较高的 生成方式,采用的是预生产id的方式:

  • 使用 RingBuffer 缓存预生产 id。数组每个元素成为一个slot。

  • RingBuffer容量,默认为Snowflake算法中sequence最大值(2^13 = 8192)。可通过 boostPower 配置进行扩容,以提高 RingBuffer 读写吞吐量。

CachedUidGenerator实现原理

因为delta seconds部分是以秒为单位的,所以1个worker 1秒内最多生成的id书为8192个(2的13次方)。从上可知,支持的最大qps为8192,所以,需要通过缓存id来提高吞吐量。

本质上,CachedUidGenerator 通过预生产+缓冲的方式,借用未来时间来解决sequence天然存在的并发限制

  • CachedUidGenerator 采用RingBuffer (或者环形队列)来缓存已生成的UID,

  • CachedUidGenerator 把 ID的生产 和 ID消费 并行化 ,

  • CachedUidGenerator 对CacheLine补齐,避免了由RingBuffer带来的硬件级「伪共享」问题.

通过优化,CachedUidGenerator 最终单机QPS可达600万。

为什么,叫借助未来时间?

因为每秒最多生成8192个id,当1秒获取id数多于8192时,RingBuffer中的id很快消耗完毕,在填充RingBuffer时,生成的id的delta seconds 部分只能使用未来的时间。

CachedUidGenerator的环形队列设计

CachedUidGenerator 采用RingBuffer来缓存预生产的ID, 实现了并行化ID的生产和消费

使用RingBuffer缓存生成的id。RingBuffer是个环形数组,默认大小为8192个,里面缓存着生成的id。

在这里插入图片描述

  1. 获取id
    1. Consumer 会从ringbuffer中拿一个id,
    2. 支持 Consumer 并发获取
  2. 填充id
    1. RingBuffer填充时机 为3种: 初始化预填充、即时填充、定时填充
    2. 初始化预填充:程序启动时,将RingBuffer填充满,缓存着8192个id
    3. 即时填充: 调用getUID()获取id时,检测到RingBuffer中的剩余id个数小于总个数的50%,将RingBuffer填充满,使其缓存8192个id
    4. 定时填充: 可配置是否使用以及定时任务的周期, 通过Schedule线程,定时补全空闲slots。可通过scheduleInterval配置,以应用定时填充功能,并指定Schedule时间间隔。
  3. Tail指针、Cursor指针用于环形数组上读写slot.

啰嗦一下,RingBuffer填充时机

  • 初始化预填充
    RingBuffer初始化时,预先填充满整个RingBuffer.
  • 即时填充
    Take消费时,即时检查剩余可用slot量(tail- cursor),如小于设定阈值,则补全空闲slots。阈值可通过paddingFactor来进行配置,请参考Quick Start中CachedUidGenerator配置
  • 定时填充
    通过Schedule线程,定时补全空闲slots。可通过scheduleInterval配置,以应用定时填充功能,并指定Schedule时间间隔

CachedUidGenerator双RingBuffer设计

CachedUidGenerator采用了双RingBuffer:

  • Uid-RingBuffer
  • Flag-RingBuffer

Uid-RingBuffer用于存储Uid、Flag-RingBuffer用于存储Uid状态(是否可填充、是否可消费)

在这里插入图片描述

Tail指针、Cursor指针用于环形数组上读写slot:

  • Tail指针 (Producer 生产指针)
    表示Producer生产的最大序号(此序号从0开始,持续递增)。Tail不能超过Cursor,即生产者不能覆盖未消费的slot。当Tail已赶上curosr,此时可通过rejectedPutBufferHandler指定PutRejectPolicy
  • Cursor指针 (Consumer 消费指针)
    表示Consumer消费到的最小序号(序号序列与Producer序列相同)。Cursor不能超过Tail,即不能消费未生产的slot。当Cursor已赶上tail,此时可通过rejectedTakeBufferHandler指定TakeRejectPolicy

RingBuffer环形数组,数组每个元素成为一个slot。RingBuffer容量,默认为Snowflake算法中sequence最大值,且为2^N。

可通过boostPower配置进行扩容,以提高RingBuffe 读写吞吐量。

CachedUidGenerator 通过CacheLine补齐解决伪共享

由于数组元素在内存中是连续分配的,可最大程度利用CPU cache以提升性能。

但同时会带来「伪共享」FalseSharing问题,为此在Tail、Cursor指针、Flag-RingBuffer中采用了CacheLine
补齐方式。

在这里插入图片描述

如何解决 Snowflake 的时钟回拨问题?

什么是Snowflake 时间回拨问题?

  • 雪花算法通过时间来即将作为id的区分标准之一,对于同一台id生成机器,它通过时间和序号保证id不重复
  • 当机器出现问题,时间可能回到之前,此时,时间就不能区分
  • 又或者因为闰秒的出现,导致时间回拨

如何解决

方法1 直接抛出异常

  • 不管3X7==21,直接抛出异常
  • 将问题交给人工解决
  • 这种方法也是原始的雪花算法,百度的uid-generator采用的
  • 太过简单,显然不好

方法2 延迟等待

  • 这种时间回拨(回跳)或许只出现一次,也许只是机器出现了小问题,所以产生
  • 对于这种场景,没有必要抛出异常,中断业务
  • 此时,将当前线程阻塞3ms,之后再获取时间,看时间是否比上一次请求的时间大
  • 如果大了,说明恢复正常了,则不用管
  • 如果还小,说明真出问题了,则抛出异常,呼唤程序员处理
  • 实际应用项目: 美团的leaf, 用如果时间差在5ms内,则等待 时间差<<1, 然后再判断

方法3 备用机

  • 当前机器出现问题,则换一台机器
  • 通过高可用来解决该问题

方法4 采用之前最大时间

  • 本身得出时间回拨结论就是通过当前时间和上次最后(大)的时间进行比较
  • 那么此时可以采用上次最大时间的最大序号之后的序号来进行继续使用
  • 从而保证了唯一性
方法5 追赶时间
  • 可以采取这样的暴力思路,因为当前的时间回拨了,比之前的时间慢
  • 那么我们便加速追赶时间
  • 首先,不返回id
  • 然后将我们的seq增加比如1024个,然后判断是否回拨,如果不是,再加1024
  • 当seq超过了12位的maxSeq时,按照雪花算法的逻辑,时间便会进位,借用下个时间的seq
  • 此时就实现了时间的加速
  • 经过若干个加速,则可以实现时间正常

UidGenerator 如何解决了时钟回拨问题

百度开源的 UidGenerator 是基于Java语言实现的唯一ID生成器,是在雪花算法 snowflake 的基础上做了一些改进(解决了时钟回拨问题)

总体来说,UidGenerator 通过两个位段自增 彻底 解决了时钟回拨问题

第一:workerId位段自增

UidGenerator的workerId在实例每次重启时初始化,workerId 属于数据库 自增ID,

由于 workerId 不断增长,所以不同的工作节点,不会有任何workerId冲突。

第二:delta seconds时间位段递增:

传统的雪花算法实现都是通过System.currentTimeMillis()来获取时间, 并与上一次时间进行比较,严重依赖服务器的时间,而服务器的时间容易造成时钟回拨,从而产生时间冲突。

而UidGenerator的时间类型是AtomicLong,且通过incrementAndGet()方法获取下一次的时间,从而脱离了对服务器时间的依赖,也就不会有时钟回拨的问题

关于雪花id、叶段id的学习,具体请参考 43岁资深老架构师尼恩的视频

顶奢、优质项目:1000W级qps ID组件 架构与实操

说在最后:有问题找老架构取经‍

通过对分布式ID的深度解答,可以充分展示一下大家雄厚的 “技术肌肉”,让面试官爱到 “不能自已、口水直流”,然后实现”offer直提”。

在面试之前,建议大家系统化的刷一波 5000页《尼恩Java面试宝典PDF》,里边有大量的大厂真题、面试难题、架构难题。

很多小伙伴刷完后, 吊打面试官, 大厂横着走。

在刷题过程中,如果有啥问题,大家可以来 找 40岁老架构师尼恩交流。

另外,如果没有面试机会,可以找尼恩来改简历、做帮扶。

遇到职业难题,找老架构取经, 可以省去太多的折腾,省去太多的弯路。

尼恩指导了大量的小伙伴上岸,前段时间,刚指导一个40岁+被裁小伙伴,拿到了一个年薪100W的offer。

狠狠卷,实现 “offer自由” 很容易的, 前段时间一个武汉的跟着尼恩卷了2年的小伙伴, 在极度严寒/痛苦被裁的环境下, offer拿到手软, 实现真正的 “offer自由” 。

尼恩技术圣经系列PDF

……完整版尼恩技术圣经PDF集群,请找尼恩领取

《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》PDF,请到下面公号【技术自由圈】取↓↓↓

相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
13天前
|
存储 消息中间件 NoSQL
每日大厂面试题大汇总 —— 今日的是“京东-后端开发-一面”
文章汇总了京东后端开发一面的面试题目,包括ArrayList与LinkedList的区别、HashMap的数据结构和操作、线程安全问题、线程池参数、MySQL存储引擎、Redis性能和线程模型、分布式锁处理、HTTP与HTTPS、Kafka等方面的问题。
57 0
|
2月前
|
算法 Go
[go 面试] 雪花算法与分布式ID生成
[go 面试] 雪花算法与分布式ID生成
|
1天前
|
SQL 关系型数据库 MySQL
京东面试:什么情况下 mysql RR不能解决幻读? RR隔离mysql如何实现?
老架构师尼恩在其读者交流群中分享了关于MySQL事务隔离级别的深入解析,特别针对RR级隔离如何解决幻读问题进行了详细讨论。文章不仅解释了ACID中的隔离性概念,还列举了四种事务隔离级别(未提交读、提交读、可重复读、串行读)的特点及应用场景。尼恩通过具体的例子和图表,清晰地展示了不同隔离级别下的并发事务问题(脏读、不可重复读、幻读)及其解决方案,特别是RR级隔离下的MVCC机制如何通过快照读和当前读来防止幻读。此外,尼恩还提供了相关面试题的解答技巧和参考资料,帮助读者更好地准备技术面试。更多详细内容和实战案例可在《尼恩Java面试宝典》中找到。
|
1天前
|
缓存 NoSQL Java
秒杀圣经:10Wqps高并发秒杀,16大架构杀招,帮你秒变架构师 (1)
高并发下,如何设计秒杀系统?这是一个高频面试题。40岁老架构师尼恩的读者交流群中,近期有小伙伴在面试Shopee时遇到了这个问题,未能很好地回答,导致面试失败。为此,尼恩进行了系统化、体系化的梳理,帮助大家提升“技术肌肉”,让面试官刮目相看。秒杀系统设计涉及16个架构要点,涵盖业务架构、流量架构、异步架构、分层架构、缓存架构、库存扣减、MQ异步处理、限流、熔断、降级、存储架构等多个方面。掌握这些要点,可以有效应对高并发场景下的秒杀系统设计挑战。
秒杀圣经:10Wqps高并发秒杀,16大架构杀招,帮你秒变架构师 (1)
|
9天前
|
算法 Java 数据中心
探讨面试常见问题雪花算法、时钟回拨问题,java中优雅的实现方式
【10月更文挑战第2天】在大数据量系统中,分布式ID生成是一个关键问题。为了保证在分布式环境下生成的ID唯一、有序且高效,业界提出了多种解决方案,其中雪花算法(Snowflake Algorithm)是一种广泛应用的分布式ID生成算法。本文将详细介绍雪花算法的原理、实现及其处理时钟回拨问题的方法,并提供Java代码示例。
27 2
|
6天前
|
缓存 关系型数据库 API
京东面试题:ElasticSearch深度分页解决方案!
京东面试题:ElasticSearch深度分页解决方案!
|
2月前
|
算法 关系型数据库 MySQL
技术分享:600W QPS高并发ID设计与时钟回拨解决方案
【8月更文挑战第26天】在大型分布式系统中,高并发ID生成和时钟同步是两个至关重要的技术挑战。随着业务量的快速增长,如美团点评的金融、支付、餐饮等业务场景,每秒需要处理数百万级别的请求,这就对ID的生成效率和唯一性提出了极高要求。同时,时钟回拨问题也时常困扰着系统管理员,影响数据一致性和系统稳定性。本文将围绕这两个主题,分享一些工作学习中的技术干货。
51 1
|
2月前
|
消息中间件 算法 前端开发
京东面试:说说CMS工作原理?
京东面试:说说CMS工作原理?
39 2
|
3月前
|
消息中间件 编解码 网络协议
京东面试 rockmq是推消息还是拉消息?他的消息模型是啥?
RocketMQ采用拉模式结合长轮询模拟推效果,减少延迟并优化资源使用。在长轮询中,服务器在无消息时保持请求开放,待有新消息时立即响应,提升实时性。利用Netty的TCP连接和异步处理,RocketMQ构建高效通信协议,适应不同吞吐量和实时性需求场景,兼顾控制与实时响应。
41 0
京东面试 rockmq是推消息还是拉消息?他的消息模型是啥?
|
3月前
|
监控 网络协议 Java
Java面试题:解释Java NIO与BIO的区别,以及NIO的优势和应用场景。如何在高并发应用中实现NIO?
Java面试题:解释Java NIO与BIO的区别,以及NIO的优势和应用场景。如何在高并发应用中实现NIO?
58 0