雪花算法生成id长度可控吗_分布式ID

本文详细介绍了分布式ID的概念及其重要性,分析了UUID、数据库自增ID、号段模式、Redis、雪花算法(SnowFlake)等多种分布式ID生成方式的优缺点。其中,雪花算法因其有序递增、本地生成的特点受到好评,但也存在时钟依赖问题。此外,还提到了美团的Leaf、百度的Uidgenerator和滴滴的TinyID等解决方案,以及它们在解决时钟回拨、性能和扩展性方面的策略。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

本文主要围绕是什么、为什么、怎么做三个方面来介绍分布式ID。


是什么?

数据库中的每条数据库通常需要唯一ID来标识,传统方式采用自增ID即可满足要求,但在分布式系统中往往需要对数据进行分库分表,这时候就需要一个全局唯一的ID来标识数据,这个全局唯一的ID就叫做分布式ID。

为什么?

分布式ID是分布式系统的全局唯一ID,分布式ID生成的好坏将直接影响到整个系统的性能。分布式ID需要满足如下条件:

  • 全局唯一:必须保证ID全局性唯一

  • 高性能:延时低、响应快,否则反倒可能成为性能瓶颈

  • 高可用:作为数据的唯一标识,必须保持高可用

  • 趋势递增:数值类型最优,能够趋势递增

  • 易接入:在系统设计和实现上要尽可能的简单,开箱即可

怎么做?

本文主要分析以下9种分布式ID生成方式以及优缺点:

  • UUID

  • 数据库自增ID

  • 数据库多主模式

  • 号段模式

  • Redis

  • 雪花算法(SnowFlake)

  • 美团(Leaf)

  • 百度 (Uidgenerator)

  • 滴滴出品(TinyID)

 UUID 

根据UUID生成分布式ID最容易想到的,UUID存在多个版本。

  • UUID Version 1:基于时间的UUID

    基于时间的UUID通过计算当前时间戳、随机数和机器MAC地址得到。由于在算法中使用了MAC地址,这个版本的UUID可以保证在全球范围的唯一性。但与此同时,使用MAC地址会带来安全性问题,这就是这个版本UUID受到批评的地方。如果应用只是在局域网中使用,也可以使用退化的算法,以IP地址来代替MAC地址。

  • UUID Version 2:DCE安全的UUID

    DCE(Distributed Computing Environment)安全的UUID和基于时间的UUID算法相同,但会把时间戳的前4位置换为POSIX的UID或GID。这个版本的UUID在实际中较少用到。

  • UUID Version 3:基于名字的UUID(MD5)

    基于名字的UUID通过计算名字和名字空间的MD5散列值得到。这个版本的UUID保证了:相同名字空间中不同名字生成的UUID的唯一性;不同名字空间中的UUID的唯一性;相同名字空间中相同名字的UUID重复生成是相同的。

  • UUID Version 4:随机UUID

    根据随机数,或者伪随机数生成UUID。这种UUID产生重复的概率是可以计算出来的,但随机的东西就像是买彩票:你指望它发财是不可能的,但狗屎运通常会在不经意中到来。

  • UUID Version 5:基于名字的UUID(SHA1)

    和版本3的UUID算法类似,只是散列值计算使用SHA1(Secure Hash Algorithm 1)算法。

其中:

    Version 1/2适合应用于分布式计算环境下,具有高度的唯一性;

    Version 3/5适合于一定范围内名字唯一,且需要或可能会重复生成UUID的环境下;

    至于Version 4,个人的建议是最好不用(虽然它是最简单最方便的)。

优点:

    本机生成,无性能问题。V1版本理论上全球唯一。

缺点:

    ID无序;字符串;长度过长;基于MAC生成可能造成MAC泄漏。

    (对于数据库主键,最好的应该有序,短小,数值类型。尤其是无序会导致数据库聚簇索引结构频繁变动)

 数据库自增ID 

基于数据库的auto_increment自增ID完全可以充当分布式ID,具体实现:需要一个单独的MySQL实例用来生成ID,建表结构如下:‍

CREATE TABLE SEQUENCE_ID (      id bigint(20) unsigned NOT NULL auto_increment,       value char(10) NOT NULL default '',      PRIMARY KEY (id)) ENGINE=MyISAM;

当我们需要一个ID的时候,向表中插入一条记录返回主键ID,但这种方式有一个比较致命的缺点,访问量激增时MySQL本身就是系统的瓶颈,用它来实现分布式服务风险比较大,不推荐!

优点:

    实现简单,ID单调自增,数值类型查询速度快。

缺点:

    DB单点存在宕机风险,高并发场景下DB本身将成为性能瓶颈。

 数据库多实例自增ID 

这个方案就是为了解决MySQL单点问题,在auto_increment基本上面,设置step步长。

772519888b06ac71691532064fe43fd4.png

从上图可以看出,水平扩展的数据库集群,有利于解决数据库单点问题,同时为了ID生成特性,将自增步长按照机器数量来设置。

优点:

    解决了单点问题。

缺点:

    步长确定后无法扩容;高并发场景下DB本身仍将成为性能瓶颈。

 号段模式 

号段模式是主流分布式ID生成方式之一,号段模式可以理解为从数据库批量的获取自增ID,每次从数据库取出一个号段范围,例如 (1,1000] 代表1000个ID,具体的业务服务将本号段,生成1~1000的自增ID并加载到内存。表结构如下:

CREATE TABLE id_generator (    id int(10) NOT NULL,    max_id bigint(20) NOT NULL COMMENT '当前最大id',    step int(20) NOT NULL COMMENT '号段的布长',    biz_typeint(20) NOT NULL COMMENT '业务类型',    version int(20) NOT NULL COMMENT '版本号',    PRIMARY KEY (`id`))

    biz_type :代表不同业务类型

    max_id :当前最大的可用id

    step :代表号段的长度

    version :乐观锁,每次都更新version,保证并发时数据的正确性

当一批号段ID用完,再次向数据库申请新号段,对max_id字段做一次update操作,update max_id= max_id + step,update成功则说明新号段获取成功,新的号段范围是(max_id ,max_id +step]。

update id_generator set max_id = #{max_id+step}, version = version + 1 where version = #{version} and biz_type = XXX

由于多业务端可能同时操作,所以采用版本号version乐观锁方式更新,这种分布式ID生成方式不强依赖于数据库,不会频繁的访问数据库,对数据库的压力小很多。

 Redis 

利用Redis的原子性操作自增,一般算法为:时间戳+ 自增Id, 补0 

优点:

    有序递增,可读性强。

缺点:

    占用带宽,每次都要向Redis请求。

注意:

        使用Redis需要考虑持久化问题,Redis有两种持久化方式RDB和AOF。

  • RDB会定时打一个快照进行持久化,假如连续自增但redis没及时持久化,而这会Redis挂掉了,重启Redis后会出现ID重复的情况。

  • AOF会对每条写命令进行持久化,即使Redis挂掉了也不会出现ID重复的情况,但由于incr命令的特殊性,会导致Redis重启恢复的数据时间过长。

 雪花算法(SnowFlake)

雪花算法(Snowflake)是Twitter公司内部分布式项目采用的ID生成算法,开源后广受国内大厂的好评,在该算法影响下各大公司相继开发出各具特色的分布式生成器。

a700e114d73301c47c732b727e62b808.png

如上图所示:

① 1 bit:是不用的,为啥呢?

    因为二进制里第一个 bit 为如果是 1,那么都是负数,但是我们生成的 ID 都是正数,所以第一个 bit 统一都是 0。

② 41 bit:表示的是时间戳,单位是毫秒。

    41 bit 可以表示的数字多达 2^41 - 1,可以标识 2 ^ 41 - 1 个毫秒值,换算成年就是表示 69 年的时间。

③ 10 bit:记录工作机器 ID,代表的是这个服务最多可以部署在 2^10 台机器上,也就是 1024 台机器。

    10 bit 里 5 个 bit 代表机房 id,5 个 bit 代表机器 ID。意思就是最多代表 2 ^ 5 个机房(32 个机房),每个机房里可以代表 2 ^ 5 个机器(32 台机器)。

④12 bit:这个是用来记录同一个毫秒内产生的不同 ID。

    12 bit 可以代表的最大正整数是 2 ^ 12 - 1 = 4096,也就是说可以用这个 12 bit 代表的数字来区分同一个毫秒内的 4096 个不同的 ID。理论上snowflake方案的QPS约为409.6w/s,这种分配方式可以保证在任何一个IDC的任何一台机器在任意毫秒内生成的ID都是不同的。

根据这个算法,只需要将这个算法用Java语言实现出来,封装为一个工具方法,那么各个业务应用可以直接使用该工具方法来获取分布式ID,只需保证每个业务应用有自己的工作机器id即可,而不需要单独去搭建一个获取分布式ID的应用。

优点:

    毫秒数在高位,自增序列在低位,ID趋势递增。

    不依赖第三方应用,本地生成,生成性能高。

    可以根据自身业务特性分配bit位,非常灵活。

缺点:

    强依赖机器时钟,时钟回拨可能会导致ID重复。

 美团(Leaf)

Leaf由美团开发,Leaf同时支持号段模式和Snowflake算法模式,可以切换使用。

Leaf-segment方案:

  • 通过proxy server批量获取分布式ID,每次获取一个segment号段,用完之后再去数据库获取新的号段,大大的减轻数据库的压力;

  • 各个业务不同的发号需求用biz_tag字段来区分,每个biz-tag的ID获取相互隔离,互不影响。如果以后有性能需求需要对数据库扩容,只需要对biz_tag分库分表就行。

建表语句如下:

DROP TABLE IF EXISTS `leaf_alloc`;CREATE TABLE `leaf_alloc` (  `biz_tag` varchar(128)  NOT NULL DEFAULT '' COMMENT '业务key,用来区分业务',  `max_id` bigint(20) NOT NULL DEFAULT '1' COMMENT '当前已经分配了的最大id',  `step` int(11) NOT NULL COMMENT '初始步长,也是动态调整的最小步长',  `description` varchar(256)  DEFAULT NULL COMMENT '业务key的描述',  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '数据库维护的更新时间',  PRIMARY KEY (`biz_tag`)) ENGINE=InnoDB;

大致架构如下图所示:

46ae3db970ab4aee7a666ed72dda1ce4.png

test_tag在第一台Leaf机器上是1~1000的号段,当这个号段用完时,会去加载另一个长度为step=1000的号段,假设另外两台号段都没有更新,这个时候第一台机器新加载的号段就应该是3001~4000。同时数据库对应的biz_tag这条数据的max_id会从3000被更新成4000,更新号段的SQL语句如下:

Begin    UPDATEtableSETmax_id=max_id+step WHEREbiz_tag=xxx    SELECTtag, max_id, step FROMtableWHEREbiz_tag=xxxCommit

优点:

    支持线性扩展,性能能够支撑大多数业务场景。

    ID是趋势递增的8byte的64位数字。

    容灾性高:即使DB宕机,短时间内Leaf仍能正常对外提供服务。

    自定义max_id大小,方便业务以原有ID迁移过来。

缺点:

    ID不够随机,能够泄露发号数量的信息,不太安全。

    TP999更新号段,数据库IO可能导致用户线程阻塞。

    DB宕机会造成整个系统不可用。

双buffer

针对TP999采用双buffer的方式,Leaf服务内部有两个号段缓存区segment。当前号段已下发10%时,如果下一个号段未更新,则另启一个更新线程去更新下一个号段。当前号段全部下发完后,如果下个号段准备好了则切换到下个号段为当前segment接着下发,循环往复。

主要特性如下:

  • 每个biz-tag都有消费速度监控,通常推荐segment长度设置为服务高峰期发号QPS的600倍(10分钟),这样即使DB宕机,Leaf仍能持续发号10-20分钟不受影响;

  • 每次请求来临时都会判断下个号段的状态,从而更新此号段,所以偶尔的网络抖动不会影响下个号段的更新。

具体实现如下图所示:

d3feec41f44a8880ab6878e70064759a.png

Leaf-Snowflake方案:

鉴于Leaf-segment方案不适用于美团的订单号这种场景(Leaf-segment方案可以生成趋势递增的ID,同时ID号是可计算的,很容易被猜出美团每日的订单量这种商业秘密),所以Leaf-Snowflake方案就应运而生了。

4cadd5d75f99299d914b04ee83e6748c.png

Leaf-Snowflake方案完全沿用Snowflake方案的bit位设计。对于workerID的分配,当服务集群数量较小的情况下,完全可以手动配置。Leaf服务规模较大,动手配置成本太高。所以Leaf-Snowflake使用Zookeeper持久顺序节点的特性自动对Snowflake节点配置wokerID。Leaf-snowflake是按照下面几个步骤启动的:

  • 启动Leaf-snowflake服务,连接Zookeeper,在leaf_forever父节点下检查自己是否已经注册过(是否有该顺序子节点)。

  • 如果有注册过直接取回自己的workerID(zk顺序节点生成的int类型ID号),启动服务。

  • 如果没有注册过,就在该父节点下面创建一个持久顺序节点,创建成功后取回顺序号当做自己的workerID号,启动服务。

8673b4e2d646ca95a0c2d928d3210151.png

弱依赖ZooKeeper

除了每次会去ZooKeeper拿数据以外,也会在本机文件系统上缓存一个workerID文件。当ZooKeeper出现问题,恰好机器出现问题需要重启时,能保证服务能够正常启动,这样做到对第三方组件的弱依赖。

解决时钟问题

因为这种方案依赖时间,如果机器的时钟发生了回拨,那么就会有可能生成重复的ID号,需要解决时钟回退的问题。

cefa87ec21a29c478a95d4c122b6cea1.png

参见上图整个启动流程图,服务启动时首先检查自己是否写过ZooKeeper leaf_forever节点:

  • 若写过,则用自身系统时间与leaf_forever/${self}节点记录时间做比较,若小于leaf_forever/${self}时间则认为机器时间发生了大步长回拨,服务启动失败并报警。

  • 若未写过,证明是新服务节点,直接创建持久节点leaf_forever/${self}并写入自身系统时间,接下来综合对比其余Leaf节点的系统时间来判断自身系统时间是否准确,具体做法是取leaf_temporary下的所有临时节点(所有运行中的Leaf-snowflake节点)的服务IP:Port,然后通过RPC请求得到所有节点的系统时间,计算sum(time)/nodeSize。

  • 若abs( 系统时间-sum(time)/nodeSize ) < 阈值,认为当前系统时间准确,正常启动服务,同时写临时节点leaf_temporary/${self} 维持租约。

  • 否则认为本机系统时间发生大步长偏移,启动失败并报警。

  • 每隔一段时间(3s)上报自身系统时间写入leaf_forever/${self}。

由于强依赖时钟,对时间的要求比较敏感,在机器工作时NTP同步也会造成秒级别的回退,建议可以直接关闭NTP同步。要么在时钟回拨的时候直接不提供服务直接返回ERROR_CODE,等时钟追上即可。或者做一层重试,然后上报报警系统,更或者是发现有时钟回拨之后自动摘除本身节点并报警,如下:

//发生了回拨,此刻时间小于上次发号时间 if (timestamp < lastTimestamp) {            long offset = lastTimestamp - timestamp;            if (offset <= 5) {                try {                  //时间偏差大小小于5ms,则等待两倍时间                    wait(offset << 1);//wait                    timestamp = timeGen();                    if (timestamp < lastTimestamp) {                       //还是小于,抛异常并上报                        throwClockBackwardsEx(timestamp);                      }                    } catch (InterruptedException e) {                     throw  e;                }            } else {                //throw                throwClockBackwardsEx(timestamp);            }        } //分配ID

 百度(Uidgenerator)

uid-generator是百度技术部基于Snowflake算法开发的,与原始的Snowflake算法不同在于,uid-generator支持自定义时间戳、工作机器ID和序列号等各部分的位数。

fb72a0151700f1ae38d17c336e74e771.png

由上图可知,uid-generator的时间部分长度只有28位,这就意味着uid-generator默认只能承受8.5年(2^28-1/86400/365)。当然可以根据业务的需求,uid-generator可以适当调整delta seconds、worker node id和sequence占用位数。

接下来分析百度uid-generator的实现。需要说明的是uid-generator有两种方式提供:DefaultUidGenerator和CachedUidGenerator。

DefaultUidGenerator

  • delta seconds

    这个值是指当前时间与epoch时间的时间差,且单位为秒。epoch时间就是指集成uid-generator生成分布式ID服务第一次上线的时间,可配置。

  • worker id

搭建uid-generator的话,需要创建一个表:

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;

        uid-generator会在实例启动时,往WORKER_NODE表中插入一行数据,得到的id值就是workerId的值。由于workerId默认22位,限制集成uid-generator生成分布式ID的所有实例重启次数不允许超过4194303次(即2^22-1)。

  • sequence

    sequence是DefaultUidGenerator的核心,它对时钟回拨的处理比较简单粗暴,具体实现如下:

// synchronized保证线程安全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);    }    // 如果当前时间和上一次是同一秒时间,那么sequence自增    if (currentSecond == lastSecond) {        sequence = (sequence + 1) & bitsAllocator.getMaxSequence();        // 如果同一秒内自增值超过2^13-1,那么就会自旋等待下一秒        if (sequence == 0) {            currentSecond = getNextSecond(lastSecond);        }    } else {        // 如果是新的一秒,那么sequence重新从0开始        sequence = 0L;    }    lastSecond = currentSecond;    return bitsAllocator      .allocate(currentSecond - epochSeconds, workerId, sequence);}

CachedUidGenerator

CachedUidGenerator是 uid-generator的重要改进实现。它的核心利用了RingBuffer,如下图所示,它本质上是一个数组,数组中每个项被称为slot。uid-generator设计了两个RingBuffer,一个保存唯一ID,一个保存flag。RingBuffer的尺寸是2^n,n必须是正整数:

6163adbe184761aaaf1a581f30cfcbde.png

  • RingBuffer Of UID

    保存唯一ID的RingBuffer,它有两个指针,Tail指针和Cursor指针。Tail指针表示最后一个生成的唯一ID。如果这个指针追上了Cursor指针,意味着RingBuffer已经满了。这时候,不允许再继续生成ID了。Cursor指针表示最后一个已经给消费的唯一ID。如果Cursor指针追上了Tail指针,意味着RingBuffer已经空了。这时候,不允许再继续获取ID了。

  • RingBuffer Of Flag

    这个RingBuffer的每个slot的值都是0或者1,0是CAN_PUT_FLAG的标志位,1是CAN_TAKE_FLAG的标识位。每个slot的状态要么是CAN_PUT,要么是CAN_TAKE。以某个slot的值为例,初始值为0,即CAN_PUT。接下来会初始化填满这个RingBuffer,这时候这个slot的值就是1,即CAN_TAKE。等获取分布式ID时取到这个slot的值后,这个slot的值又变为0,以此类推。

CachedUidGenerator在初始化RingBuffer,会根据boostPower的值确定RingBuffer的size。RingBuffer参数paddingFactor默认值是50,意指当RingBuffer中剩余可用ID数量少于50%的时候,将启动一个异步线程往RingBuffer中填充新的唯一ID,直到填满为止。

在满足填充新的唯一ID条件时,CachedUidGenerator通过时间值递增得到新的时间值,而不是System.currentTimeMillis()这种通过系统时钟的方式获取,从而脱离了对服务器时间的依赖,也就不会有时钟回拨的问题。而lastSecond是AtomicLong类型,所以线程也是安全的。

 滴滴出品(TinyID)

Tinyid是用Java开发的一款分布式ID生成系统,基于数据库号段模式实现,关于这个算法与美团Leaf-segment如出一辙。Tinyid提供了java-client(sdk)使id生成本地化,获得了更好的性能与可用性。

TinyID的特性:

    http方式访问,性能取决于http server的能力,网络传输速度。

    java-client方式,本地生成,号段长度(step)越长,QPS越大。

总结

本文对分布式ID做了简要介绍,旨在指明一个学习方向,每种生成方式各有它的优缺点,具体如何使用还要看具体的业务需求。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值