美团的Leaf以及滴滴的TinyId等

发布时间:2025-06-24 18:00:34  作者:北方职教升学中心  阅读量:870


雪花算法提供了高度的灵活性,允许根据实际需求自定义各个组成部分的bit位数,若需提升QPS(每秒查询率),可适当增加序列号所占的bit位数。美团的Leaf以及滴滴的TinyId等。

这样,外部系统可以直接从缓存中获取ID,而无需等待ID的生成过程,从而大大提高了性能。41位的时间截,可以使用69年,年T = (1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69<br> * 10位的数据机器位,可以部署在1024个节点,包括5位datacenterId和5位workerId<br> * 12位序列,毫秒内的计数,12位的计数顺序号支持每个节点每毫秒(同一机器,同一时间截)产生4096个ID序号<br> * 加起来刚好64位,为一个Long型。该时间戳用于与twepoch(一个预设的常量值,表示时间起点)相减,从而得到一个相对时间差,这个相对时间差与序列号结合使用,通过allocate()方法生成最终的唯一ID。

  • dataCenterId向左移17位(机器bit+序列号bit)。
  • workerId向左移12位(序列号bit)。
  • 在这个重新获取的过程中,时钟可能恢复正常了,则此时也要退出循环,走正常流程。

    3、序列号)移到对应位置,然后相加,如下:

    longid =(deltaTime <<TIMESTAMP_LEFT_SHIFT)|(this.dataCenterId <<DATA_CENTER_ID_SHIFT)|(this.workerId <<WORKER_ID_SHIFT)|this.sequence;
    • deltaTime向左移22位(IDC-bit+机器bit+序列号bit)。

    Java代码实现snowflake

    1、考虑时间戳回拨

    /** * Twitter_Snowflake<br> * SnowFlake的结构如下(每部分用-分开):<br> * 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000 <br> * 1位标识,由于long基本类型在Java中是带符号的,最高位是符号位,正数是0,负数是1,所以id一般是正数,最高位是0<br> * 41位时间截(毫秒级),注意,41位时间截不是存储当前时间的时间截,而是存储时间截的差值(当前时间截 - 开始时间截) * 得到的值),这里的的开始时间截,一般是的id生成器开始使用的时间,由程序来指定的(如下下面程序IdWorker类的startTime属性)。

    同时,由于时间戳的自增是由程序自己控制的,因此可以充分利用未来时间,预先生成一些ID并存储在缓存中。清除或翻转二进制位。*常见的方法包括使时间戳自增以摆脱对机器时钟的依赖、

    2、利用缓存来存储序列号,或等待时钟自行校正等。通过每次获取一个号段的值,可以大大减少与数据库的交互次数,从而显著降低数据库的压力。

    snowflake优缺点

    优点:

    • 毫秒数在高位,自增序列在低位,整个ID都是趋势递增的。尽管雪花算法具有诸多优点,但它对机器时钟的强依赖性也带来了时钟回拨问题,为应对这一问题,开发者们提出了多种解决方案,主要分为避免和缓解两大类。

      *然而,这种方法也存在一个明显的缺点,即生成的ID中的时间戳并不是真实的时间。41位的时间戳、机器和序列号:

      publiclonggetSequence(longid){returnid &~(-1L<<SEQUENCE_BITS);}publiclonggetWorkerId(longid){returnid >>WORKER_ID_SHIFT&~(-1L<<WORKER_ID_BITS);}publiclonggetDataCenterId(longid){returnid >>DATA_CENTER_ID_SHIFT&~(-1L<<DATA_CENTER_ID_BITS);}publiclonggetGenerateDateTime(longid){return(id >>TIMESTAMP_LEFT_SHIFT&~(-1L<<41L))+twepoch;}

      因为sequence本身就在低位,所以不需要移动,其他机器和时间都是需要将id向右移动,使得自己的有效位置在低位,至于和自己的最大值做&运算,是为了让不属于自己bit的位置无效,即都转为0。此外,如果流量不大,可以考虑缓存前几百毫秒或几秒的序列号。

      比如回拨时长小于500ms,那就睡眠500ms,等时间恢复到正常,如果这个过程中又发生了时钟回拨,不可能一直等它校正,实际生产中可限定校正的次数,超过最大校正次数,那就抛异常吧,这属于极端情况。

      运算符<<

      运算符“<<”表示左移运算,也被称为位移左移运算符或左移位运算符。

    • 极端情况下,获取很多次缓存sequence+1都超过了最大值,就会一直循环获取,这样可能会影响性能,所以实际生产中可以限定重新获取次数。逻辑实现流程

      外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

      组装生成id

      生成id的过程,就是把每一种标识(时间、在每次生成ID时,sequence会自增,并通过与SEQUENCE_MASK进行位与操作来确保其在有效范围内循环。以下是两种可能的解决思路:

      1、

      除了整数类型,按位异或运算符也可以应用于布尔类型。出现时钟回拨问题,如果回拨幅度不大,等待时钟自己校正if(timestamp <lastTimestamp){intsleepCntMax =2;intsleepCnt =0;do{longsleepTime =lastTimestamp -timestamp;if(sleepCnt >sleepCntMax){// 可自定义异常类thrownewUnsupportedOperationException(String.format("Clock moved backwards. Refusing for %d seconds",sleepTime));}if(sleepTime <=500){try{Thread.sleep(sleepTime);}catch(InterruptedExceptione){e.printStackTrace();}finally{sleepCnt++;timestamp =tilNextMillis(lastTimestamp);}}else{// 可自定义异常类thrownewUnsupportedOperationException(String.format("Clock moved backwards. Refusing for %d seconds",sleepTime));}}while(timestamp <lastTimestamp);}// 2、因此,需要探索有效的解决方案来最小化时钟回拨带来的影响。当时钟回拨发生时,可以从缓存中获取并自增序列号,从而有效地应对时钟回拨问题。但是,为了确保生成的ID具有全局唯一性,需要依赖机器ID来进行区分。

      3、

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

      雪花算法snowflake

      snowflake定义

      外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

      Snowflake算法的原理相对直观,它负责生成一个64位(long型)的全局唯一ID,这个ID的构成包括:1位无用的符号位、*如果系统的流量较小,那么时间戳可能会滞后很多。

      按位异或运算符“^”通常用于处理整数的位级操作,当对两个整数进行异或运算时,它们的每一个二进制位都会根据异或规则进行运算,生成一个新的整数作为结果。

      由于各种潜在因素,机器的本地时钟可能会变得不准确。 */publicclassSnowflakeIdWorkerV1{// ==============================Fields===========================================/** 开始时间截 (2015-01-01) */privatefinallongtwepoch =1420041600000L;/** 机器id所占的位数 */privatefinallongworkerIdBits =5L;/** 数据标识id所占的位数 */privatefinallongdatacenterIdBits =5L;/** 支持的最大机器id,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数) */privatefinallongmaxWorkerId =-1L^(-1L<<workerIdBits);/** 支持的最大数据标识id,结果是31 */privatefinallongmaxDatacenterId =-1L^(-1L<<datacenterIdBits);/** 序列在id中占的位数 */privatefinallongsequenceBits =12L;/** 机器ID向左移12位 */privatefinallongworkerIdShift =sequenceBits;/** 数据标识id向左移17位(12+5) */privatefinallongdatacenterIdShift =sequenceBits +workerIdBits;/** 时间截向左移22位(5+5+12) */privatefinallongtimestampLeftShift =sequenceBits +workerIdBits +datacenterIdBits;/** 生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095) */privatefinallongsequenceMask =-1L^(-1L<<sequenceBits);/** 工作机器ID(0~31) */privatelongworkerId;/** 数据中心ID(0~31) */privatelongdatacenterId;/** 毫秒内序列(0~4095) */privatelongsequence =0L;/** 上次生成ID的时间截 */privatelonglastTimestamp =-1L;//==============================Constructors=====================================/** * 构造函数 * @param workerId 工作ID (0~31) * @param datacenterId 数据中心ID (0~31) */publicSnowflakeIdWorkerV1(longworkerId,longdatacenterId){if(workerId >maxWorkerId ||workerId <0){thrownewIllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0",maxWorkerId));}if(datacenterId >maxDatacenterId ||datacenterId <0){thrownewIllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0",maxDatacenterId));}this.workerId =workerId;this.datacenterId =datacenterId;}// ==============================Methods==========================================/** * 获得下一个ID (该方法是线程安全的) * @return SnowflakeId */publicsynchronizedlongnextId(){longtimestamp =timeGen();//如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常if(timestamp <lastTimestamp){thrownewRuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds",lastTimestamp -timestamp));}//如果是同一时间生成的,则进行毫秒内序列if(lastTimestamp ==timestamp){sequence =(sequence +1)&sequenceMask;//毫秒内序列溢出if(sequence ==0){//阻塞到下一个毫秒,获得新的时间戳timestamp =tilNextMillis(lastTimestamp);}}//时间戳改变,毫秒内序列重置else{sequence =0L;}//上次生成ID的时间截lastTimestamp =timestamp;//移位并通过或运算拼到一起组成64位的IDreturn((timestamp -twepoch)<<timestampLeftShift)//|(datacenterId <<datacenterIdShift)//|(workerId <<workerIdShift)//|sequence;}/** * 阻塞到下一个毫秒,直到获得新的时间戳 * @param lastTimestamp 上次生成ID的时间截 * @return 当前时间戳 */protectedlongtilNextMillis(longlastTimestamp){longtimestamp =timeGen();while(timestamp <=lastTimestamp){timestamp =timeGen();}returntimestamp;}/** * 返回以毫秒为单位的当前时间 * @return 当前时间(毫秒) */protectedlongtimeGen(){returnSystem.currentTimeMillis();}//==============================Test=============================================/** 测试 */publicstaticvoidmain(String[]args){SnowflakeIdWorkerV1idWorker =newSnowflakeIdWorkerV1(0,0);for(inti =0;i <1000;i++){longid =idWorker.nextId();System.out.println(Long.toBinaryString(id));System.out.println(id);}}}

    2、

    因此,如果对从ID中解析出来的时间戳有特定的利用需求(例如用于记录事件发生的时间),那么这个缺点可能会成为问题。MySQL官方建议主键应尽可能短,而对于InnoDB引擎来说,索引的无序性可能会导致数据位置频繁变动,从而严重影响性能。

    雪花算法由于其强烈依赖于机器时钟,因此难以避免受到时钟回拨的影响,这可能导致生成重复的ID。<br> * SnowFlake的优点是,整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞(由数据中心ID和机器ID作区分),并且效率较高,经测试,SnowFlake每秒能够产生26万ID左右。

    时间戳自增彻底解决时钟回拨问题

    privatelongsequence =-1L;privatelongstartTimestamp =1623947387000L;privatesynchronizedlongnextId2(){longsequenceTmp =sequence;sequence =(sequence +1)&SEQUENCE_MASK;// sequence =0 有可能是初始+1=0,也可能是超过了最大值等于0// 所以把 初始+1=0排除掉if(sequence ==0&&sequenceTmp >=0){// sequence自增到最大了,时间戳自增1startTimestamp +=1;}// 生成idreturnallocate(startTimestamp -twepoch);}

    这段Java代码定义了一个用于生成唯一ID的方法nextId2(),该方法不依赖于机器时钟,从而彻底解决了时钟回拨问题。例如,如果你有一个整数8(在二进制中表示为1000),将其左移两位,结果将是32(在二进制中表示为100000),这是因为8乘以2的2次方等于32。41位的时间截,可以使用69年,年T = * (1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69<br> * 10位的数据机器位,可以部署在1024个节点,包括5位datacenterId和5位workerId<br> * 12位序列,毫秒内的计数,12位的计数顺序号支持每个节点每毫秒(同一机器,同一时间截)产生4096个ID序号<br> * 加起来刚好64位,为一个Long型。

    但是,由于ID的获取涉及到远程请求,因此会受到网络波动的影响,其性能自然无法与直接从本地生成相比,同时,发号器服务的稳定性至关重要,一旦服务出现故障,将影响到许多依赖其的外部服务。000000000000100111010000011010011011101010011110101000100000111111&运算得到1(左边都是0可以省掉)。<br> * SnowFlake的优点是,整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞(由数据中心ID和机器ID作区分),并且效率较高,经测试,SnowFlake每秒能够产生26万ID左右。不考虑时间戳回拨

    /** * Twitter_Snowflake<br> * SnowFlake的结构如下(每部分用-分开):<br> * 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000 <br> * 1位标识,由于long基本类型在Java中是带符号的,最高位是符号位,正数是0,负数是1,所以id一般是正数,最高位是0<br> * 41位时间截(毫秒级),注意,41位时间截不是存储当前时间的时间截,而是存储时间截的差值(当前时间截 - 开始时间截) * 得到的值),这里的的开始时间截,一般是的id生成器开始使用的时间,由程序来指定的(如下下面程序IdWorker类的startTime属性)。

    美团的Leaf和滴滴的TinyId就是采用这种算法的典型例子。然而,这种处理方式在实际生产环境中是不可接受的。

    2、

    2、

    3、

    inta =15;// 二进制表示为 0000 1111  intb =8;// 二进制表示为 0000 1000  intc =a ^b;// 进行按位异或运算  System.out.println("Result: "+c);// 输出结果为 7(二进制表示为 0000 0111)

    在这个示例中,整数a和b的二进制表示分别为0000 1111和0000 1000,对它们进行按位异或运算后,得到的结果为0000 0111,即十进制数7。10位机器ID:可以唯一标识最多1024台机器,如果需要对互联网数据中心(IDC)进行划分,可以将这10位拆分为两部分,例如各5位,这样,系统就能够表示最多32个IDC,且每个IDC下可以容纳32台机器。

    尽管可以通过部署数据库集群来提高可用性,但问题并未从根本上解决。

    4、thrownewUnsupportedOperationException("The timeback range is too large and exceeds 2000ms caches");}longpreSequence =sequenceCycle[index];sequence =(preSequence +1)&SEQUENCE_MASK;if(sequence ==0){// 如果取出的历史序列号+1后已经达到超过最大值,// 则重新获取timestamp,重新拿其他位置的缓存timestamp =tilNextMillis(lastTimestamp);index =(int)(timestamp %LENGTH);}else{// 更新缓存sequenceCycle[index]=this.sequence;returnallocate((timestamp -this.twepoch),sequence);}}while(timestamp <lastTimestamp);// 如果在获取缓存的过程中timestamp恢复正常了,就走正常流程}// 2、

    2、本地生成方案

    在此方案中,ID是在本地直接生成的,因此不存在网络延迟问题,性能极高。雪花算法以其简单高效而著称,其核心在于结合时间戳、然而,由于它强烈依赖于机器时钟,因此需要考虑时钟回拨问题。

    1. 若获取的历史sequence+1之后超过了最大值,则重新获取时间戳,重新获取缓存sequence。

      小结

      解决时钟回拨问题的方法还有很多,无非就是避免和缓解。UDDI之间的关系

      Spring揭秘:@import注解应用场景及实现原理!

      Java并发基础:原子类之AtomicMarkableReference全面解析!

      Java并发基础:concurrent Flow API全面解析

      Java并发基础:CopyOnWriteArraySet全面解析

  • 每种方式有各自的特点和适用场景,可以两两结合使用,比如时钟回拨幅度小,就休眠校正,回拨幅度大或者出现多次回拨,也不抛异常,获取缓存sequence对外提供服务,也可以当发生时钟回拨时,用备用机器id生成ID等。单调递增,在特定场景下,如分布式事务的版本控制、全局唯一性,所生成的ID必须在全局范围内是独一无二的,以确保数据的精准标识,避免任何可能的ID冲突。

    运算符相关概念

    运算符&

    "&"表示按位与操作,它用于操作整数类型的位模式,并将每个操作数中的每一位进行与操作,它会将两个整数的二进制表示进行按位与运算,只有当两个数的相应位都为1时,结果的相应位才为1,否则为0。因此每台机器以及机器上部署的每个服务都需要分配不同的机器ID。但如果时间戳的利用意义不大,那么这个缺点也就可以忽略不计了。*每种方法都有其独特之处,只有正确利用它们的优点,才能最大限度地提升系统性能。时间等于lastTimestamp,取当前的sequence + 1if(timestamp ==lastTimestamp){sequence =(sequence +1)&SEQUENCE_MASK;// Exceed the max sequence, we wait the next second to generate idif(sequence ==0){timestamp =tilNextMillis(lastTimestamp);index =(int)(timestamp %LENGTH);}}else{// 3、

    等待时钟校正

    privatesynchronizedlongnextId3(){longtimestamp =timeGen();// 1、即时通讯的增量消息传递或数据排序等,需要确保每个新生成的ID都严格大于前一个ID,从而实现数据的线性顺序增长。

    这种方法的优点在于,它完全摆脱了机器时钟的限制,因此不会受到时钟回拨问题的影响。时间大于lastTimestamp没有发生回拨, sequence 从0开始this.sequence =0L;}lastTimestamp =timestamp;// 生成idreturnallocate(timestamp -this.twepoch);}

    等待时钟自己校正来解决时钟回拨问题,适用于回拨幅度小的场景。

    分布式ID常见的算法

    在分布ID生成的领域,目前市面上主要流行几种算法,这些算法也是众多开源项目进行优化和实现的基础:

    1、

    inta =60;// 60 = 0011 1100  intb =13;// 13 = 0000 1101  intc =a |b;// c will be 61 = 0011 1101

    运算符^

    运算符“^”表示按位异或(XOR)操作,它是一个二元运算符,用于对两个整数的二进制位进行异或运算,异或运算的规则是:如果两个相应的二进制位相同,则结果为0;如果两个相应的二进制位不同,则结果为1。

    例如:生成的id为1414362783486840832,转为二进制1001110100000110100110111010100111101010001000001000000000000,想解析出workerIdworkerId有效位为[13, 17],那就将id向右移12位,移到低位得到0000000000001001110100000110100110111010100111101010001000001workerId5-bit,那么除了低位5-bit,其他位置都是无效bit,转为0。时间大于lastTimestamp没有发生回拨, sequence 从0开始this.sequence =0L;}// 缓存sequence + 更新lastTimestampsequenceCycle[index]=this.sequence;lastTimestamp =timestamp;// 生成idreturnallocate(timestamp -this.twepoch);}

    这里缓存了2000ms的序列号,如果发生时钟回拨,且回拨范围在2000ms内,就从缓存中取序列号自增,超过2000ms回拨,就抛异常,故障转移,将请求分配到正常机器。

    缓存历史序列号缓解时钟回拨问题

    // 记录近2S的毫秒数的sequence的缓存privateintLENGTH=2000;// sequence缓存privatelong[]sequenceCycle =newlong[LENGTH];privatesynchronizedlongnextId()throwsException{longtimestamp =timeGen();intindex =(int)(timestamp %LENGTH);// 1、
  • 可以不依赖数据库等第三方系统,以服务的方式部署,稳定性更高,生成ID的性能也非常高。
  • 最后11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 000000 000000^运算得到0000000000000000000000000000000000000000000000000000 111111 111111(前面有52个0),这就得到序列号的最大值(4095)了,也可以说是掩码。雪花算法,Twitter开源的snowflake算法,以时间戳、

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    END!
    END!
    END!

    往期回顾

    精品文章

    深度思考:总结SOA、为了校准这些时间偏差,网络提供了NTP(网络时间协议)服务。41位时间戳:这部分能够表示的时间跨度为(1L<<41)/(1000L*3600*24*365),即大约69年。然而,在进行时间校准时,时钟的跳跃或回拨问题可能会随之出现。

    4、如果数据库发生宕机,对依赖其服务的外部系统将是毁灭性的打击。为了确保发号器的高可用性,需要采取多实例部署、这种方法在保持对机器时钟依赖的同时,降低了时钟回拨对系统的影响。

    当你对一个整数应用“<<”运算符并指定一个位移量时,该整数的二进制形式中的所有位都会向左移动指定的位数,在移动过程中,最左边的位会被丢弃,而右边空出的位则用0来填充。

    要点总结

    1、这种运算符通常用于低级位操作,如设置、

    3、

    传统的垂直与水平分库分表策略虽然能有效应对数据存储和扩展的问题,但却引出了一个新的需求:需要一个全局唯一标识符(ID)来标定每条数据或消息队列(MQ)中的消息。12位自增序列号:用于在同一毫秒内生成多个ID,最多可以表示2^12个不同的ID,因此,理论上,Snowflake算法能够达到的每秒查询率(QPS)约为409.6万。当sequence自增到最大值并回绕到0时,如果之前的sequenceTmp(即sequence自增前的值)是非负的,那么说明不是初始情况下的自增,此时会将startTimestamp时间戳自增1,以确保生成的ID的唯一性。

    intoriginalValue =8;intshiftedValue =originalValue <<2;// 结果是32

    时钟回拨问题及其解决方案探讨

    在深入了解时钟回拨问题之前,首先需要探讨为什么机器的本地时钟会出现回拨现象。异地容灾等策略。

    inta =60;// 60 = 0011 1100  intb =13;// 13 = 0000 1101  intc =a &b;// c will be 12 = 0000 1100

    运算符|

    "|"表示按位或操作,它用于操作整数类型的位模式,并将每个操作数中的每一位进行或操作,它会将两个整数的二进制表示进行按位或运算,只要两个数的相应位中有一个为1,结果的相应位就为1,否则为0。*远程发号器需要确保高可用性,以满足分布式系统中的需求;而本地直接生成ID则省去了远程请求的开销,因此在性能上更具优势。机器ID和序列号来生成64位的唯一ID,该算法生成的ID整体上呈递增趋势,确保了全局唯一性,同时性能表现优异。

    在原标准实现中,当时钟回拨发生时,系统会直接抛出异常并短暂停止对外服务。这种方法能够充分利用序列号,特别适用于流量较大的场景。趋势递增,鉴于多数关系型数据库管理系统(RDBMS)MySQL的InnoDB引擎使用B树结构来存储索引数据,并依赖聚集索引进行优化,最好择有序的ID以保证高效的写入性能,因此生成的ID应具备整体递增的趋势。

    此外,发号器在设计时还可以考虑一次性发布一段时间内的ID供服务本地缓存,这样既能提升性能,减少对发号器的频繁请求,也能在一定程度上降低发号器故障所带来的风险。10位的机器ID以及12位的序列号,除了固定的1位符号位之外,其余的三个部分都可以根据实际需求进行调整:

    1、

    2、

    4、机器、*然而,本地生成方式也带来了机器ID管理的挑战,因为一旦某个机器ID被使用,它就不能再被其他机器重复使用。号段越长,性能越高。即当机器上的时间因校正而发生倒退时,可能会导致生成的ID重复。

  • 反解析ID

    (1)通过已经生成的ID解析出时间、

    此外,UUID是无序的,这使得它在许多场景中并不适用,尤其是在作为MySQL数据库的主键和索引时。UUID方式,UUID由于是在本地生成,因此具有极高的性能,然而,它生成的ID长度较长,达到16字节128位,通常需要使用字符串类型进行存储。

    1、 */publicclassSnowflakeIdWorkerV2{// ==============================Fields===========================================/** 开始时间截 (2015-01-01) */privatefinallongtwepoch =1420041600000L;/** 机器id所占的位数 */privatefinallongworkerIdBits =5L;/** 数据标识id所占的位数 */privatefinallongdatacenterIdBits =5L;/** 支持的最大机器id,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数) */privatefinallongmaxWorkerId =-1L^(-1L<<workerIdBits);/** 支持的最大数据标识id,结果是31 */privatefinallongmaxDatacenterId =-1L^(-1L<<datacenterIdBits);/** 序列在id中占的位数 */privatefinallongsequenceBits =12L;/** 机器ID向左移12位 */privatefinallongworkerIdShift =sequenceBits;/** 数据标识id向左移17位(12+5) */privatefinallongdatacenterIdShift =sequenceBits +workerIdBits;/** 时间截向左移22位(5+5+12) */privatefinallongtimestampLeftShift =sequenceBits +workerIdBits +datacenterIdBits;/** 生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095) */privatefinallongsequenceMask =-1L^(-1L<<sequenceBits);/** 工作机器ID(0~31) */privatelongworkerId;/** 数据中心ID(0~31) */privatelongdatacenterId;/** 毫秒内序列(0~4095) */privatelongsequence =0L;/** 上次生成ID的时间截 */privatelonglastTimestamp =-1L;privatelongstartTimestamp =-1L;// ==============================Constructors=====================================/** * 构造函数 * * @param workerId 工作ID (0~31) * @param datacenterId 数据中心ID (0~31) */publicSnowflakeIdWorkerV2(longworkerId,longdatacenterId,longstartTimestamp){if(workerId >maxWorkerId ||workerId <0){thrownewIllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0",maxWorkerId));}if(datacenterId >maxDatacenterId ||datacenterId <0){thrownewIllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0",maxDatacenterId));}this.workerId =workerId;this.datacenterId =datacenterId;this.startTimestamp =startTimestamp;}// ==============================Methods==========================================/** * 获得下一个ID (该方法是线程安全的) * * @return SnowflakeId */publicsynchronizedlongnextId(){longsequenceTmp =sequence;sequence =(sequence +1)&sequenceMask;if(sequence ==0&&sequenceTmp >=0){// sequence自增到最大了,时间戳自增1startTimestamp +=1;}// 移位并通过或运算拼到一起组成64位的IDreturn((startTimestamp -twepoch)<<timestampLeftShift)//|(datacenterId <<datacenterIdShift)//|(workerId <<workerIdShift)//|sequence;}/** * 返回以毫秒为单位的当前时间 * * @return 当前时间(毫秒) */protectedstaticlongtimeGen(){returnSystem.currentTimeMillis();}// ==============================Test=============================================/** 测试 */publicstaticvoidmain(String[]args){SnowflakeIdWorkerV2idWorker =newSnowflakeIdWorkerV2(0,0,timeGen());for(inti =0;i <1000;i++){longid =idWorker.nextId();System.out.println(Long.toBinaryString(id));System.out.println(id);}}}

    3、

    • -1L的二进制为64个1:1111111111111111111111111111111111111111111111111111111111111111

    缺点:

    • 强依赖机器时钟,如果机器上时钟回拨,会导致发号重复或者服务处于不可用状态。

      前言

      分布式ID的基本特性

      在分布式系统的复杂环境下,数据量的持续激增对数据库架构提出了新的挑战。

      此外,该方法还确保了每一毫秒内可以生成多达4096个序列号(范围从0到4095),且没有浪费。为了应对这一挑战,开发者们通常利用MySQL或ZooKeeper等工具来进行机器ID的管理和分配。

    计算最大值的几种方式

    代码中分别对每个标识的最大值做了计算:

    //序列掩码,用于限定序列最大值为4095 ((2^12)-1) ,从0开始算就有4096个序列privatestaticfinallongSEQUENCE_MASK=-1L^(-1L<<SEQUENCE_BITS);//最大支持机器节点数0~31,一共32个  (2^5)-1privatestaticfinallongMAX_WORKER_ID=-1L^(-1L<<WORKER_ID_BITS);//最大支持数据中心节点数0~31,一共32个   (2^5)-1privatestaticfinallongMAX_DATA_CENTER_ID=-1L^(-1L<<DATA_CENTER_ID_BITS);//最大时间戳 2199023255551   (2^41)-1privatestaticfinallongMAX_DELTA_TIMESTAMP=-1L^(-1L<<TIME_STAMP_BITS);

    上方的计算就是利用二进制的运算逻辑,拿-1L ^ (-1L << SEQUENCE_BITS)举例:

    先看看从哪个方向开始计算:-1L ^ (-1L << 12)-1L和(-1L <<12)做^按位异或运算(1 ^ 1 = 0,1 ^ 0 = 1,0 ^ 1 = 1,0 ^ 0 = 0)

  • sequence不用移,中间的|以运算规律就相当于+求和(1 | 1 = 1,1 | 0 = 1,0 | 1 = 1,0 | 0 = 0)。雪花算法ID生成器的使用方式灵活多样,其中远程发号器和本地直接生成ID是两种主要方式。WSDL、代码中包含两个关键变量:sequencestartTimestamp

    2、这个运算符用于将整数的二进制表示向左移动指定的位数。REST、

    在这种情况下,依赖数据库自身的ID自增功能显然已经无法满足需求,因此,必须设计出符合以下要求的分布式ID生成策略:

    1、然而,在流量较小的情况下,可能会出现时间断层或滞后的现象。发号器方案

    该方案将雪花算法ID的生成逻辑封装成一个独立的服务,并部署在多个服务器上,外部系统通过请求发号器服务来获取ID,此方案的优势在于,无需部署大量的服务器,仅需确保服务器数量满足需求即可,默认的1024台已经足够。1bit-sign, * 然后右移回去41+5+1+12,从而移除掉12bit-序列号 * @param id * @return */publiclonggetWorkerId2(longid){return(id <<(TIME_STAMP_BITS+DATA_CENTER_ID_BITS+SIGN_BITS))>>>(TIME_STAMP_BITS+DATA_CENTER_ID_BITS+SEQUENCE_BITS+SIGN_BITS);}/** * 通过移位解析出IDC_ID,dataCenterId有效位为[18,23],左边两边都有无效位 * 先左移41+1,移除掉41bit-时间和1bit-sign * 然后右移回去41+1+5+12,移除掉右边的5bit-workerId和12bit-序列号 * @param id * @return */publiclonggetDataCenterId2(longid){return(id <<(TIME_STAMP_BITS+SIGN_BITS))>>>(TIME_STAMP_BITS+WORKER_ID_BITS+SEQUENCE_BITS+SIGN_BITS);}/** * 41bit-时间,左边1bit-sign为0,可以忽略,不用左移,所以只需要右移,并加上起始时间twepoch即可。在分布式系统中生成全局唯一的ID至关重要,目前,业界广泛采用的方案包括数据库号段算法和雪花算法,这些算法不仅在大厂中得到了广泛应用,还催生了诸多开源项目,例如百度的uid-generator、

    (2)不过还有一种解析的思路更易于理解,就是运用两次移位运算,把无效位置移除:

    1bit-sign + 41bit-time + 5bit-IDC + 5bit-workerId + 12bit-sequence

    /** * 通过移位解析出sequence,sequence有效位为[0,12] * 所以先向左移64-12,然后再像右移64-12,通过两次移位就可以把无效位移除了 * @param id * @return */publiclonggetSequence2(longid){return(id <<(TOTAL_BITS-SEQUENCE_BITS))>>>(TOTAL_BITS-SEQUENCE_BITS);}/** * 通过移位解析出workerId,workerId有效位为[13,17], 左右两边都有无效位 * 先向左移 41+5+1,移除掉41bit-时间,5bit-IDC、

    左移运算的效果等同于将原始数值乘以2的位移量次方,这是因为每向左移动一位,就相当于将原始数值乘以2(在二进制中,左移一位等同于将数值翻倍)。机器标识和递增序列为基础生成ID。

    此外,服务重启后也需要重新分配新的机器ID,这种特性使得机器ID具有“用后即弃”的性质,为了满足对大量机器ID的需求,不得不减少时间戳和序列号的位数,这样做虽然会牺牲一部分ID的丰富性,但却是保证本地生成方案可行性的必要妥协。那么,何时自增这个时间戳呢?一个合理的策略是,当序列号增加到最大值时,将时间戳加1。

    目前,百度的uid-generator和美团的Leaf也都在使用或优化这种算法。当应用于两个布尔值时,如果两个值不同,则结果为true;如果两个值相同,则结果为false。出现时钟回拨问题,获取历史序列号自增if(timestamp <lastTimestamp){longsequence =0;do{if((lastTimestamp -timestamp)>LENGTH){// 可自定义异常、信息安全,针对某些敏感业务,如订单处理等,生成的分布式ID必须是无规则的,以防止外部通过解析ID获取到流量信息或其他商业机密,这就需要ID生成策略在保持唯一性和递增性的同时,还要具备足够的信息隐匿性。 * @param id * @return */publiclonggetGenerateDateTime2(longid){return(id >>>(DATA_CENTER_ID_BITS+WORKER_ID_BITS+SEQUENCE_BITS))+twepoch;}

    ID生成器使用方式

    在分布式ID生成的策略中,通常采用两种方式:一是基于发号器的方案,二是本地生成方案。这种算法生成的ID基本呈趋势递增,且性能很高。摆脱对机器时钟的依赖:可以定义一个初始时间戳,并在需要时自增该时间戳,而不是跟随机器时钟的增加。数据库号段算法,这是对数据库自增ID的一种优化方法。

    sequence初始化为-1,这是为了避免浪费sequence+1=0这一序列号。SOAP、数据库自增ID方式,这种方法的缺点是每次获取ID都需要进行数据库IO操作,给数据库带来较大压力,且性能较低。告警等,短暂不能对外提供,故障转移,将请求转发到正常机器。即使数据库发生宕机,只要号段未用完,系统仍能在短时间内继续提供服务。

  • -1L左移12位得到:1111111111111111111111111111111111111111111111111111 000000 000000

    startTimestamp是一个初始时间戳,可以在构造器中指定,也可以使用默认值。

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    雪花算法snowflake是一种优秀的分布式ID生成方案,其优点突出:它能生成全局唯一且递增的ID,确保了数据的一致性和准确性;同时,该算法灵活性强,可自定义各部分bit位,满足不同业务场景的需求;此外,雪花算法生成ID的速度快,效率高,能有效应对高并发场景,是分布式系统中不可或缺的组件。优化对机器时钟的依赖:如果仍然选择依赖机器时钟,那么对于小范围的时钟回拨(例如几十毫秒),可以选择等待时钟恢复正常。时间等于lastTimestamp,取当前的sequence + 1if(timestamp ==lastTimestamp){sequence =(sequence +1)&SEQUENCE_MASK;// Exceed the max sequence, we wait the next second to generate idif(sequence ==0){timestamp =tilNextMillis(lastTimestamp);}}else{// 3、在布尔上下文中,运算符“^”表示逻辑异或操作。