文章目录

  • 一、前言
  • 二、雪花算法snowflake
    • 1、基本定义
    • 2、snowflake的优缺点
  • 三、Java代码实现snowflake
    • 1、组装生成id
    • 2、计算最大值的几种方式
    • 3、反解析ID
    • 4、ID生成器使用方式
  • 四、时钟回拨问题和解决方案讨论
    • 1、时间戳自增彻底解决时钟回拨问题
    • 2、缓存历史序列号缓解时钟回拨问题
    • 3、等待时钟校正
  • 五、要点总结

一、前言

在日趋复杂的分布式系统中,数据量越来越大,数据库分库分表是一贯的垂直水平做法,但是需要一个全局唯一ID标识一条数据或者MQ消息,数据库id自增就显然不能满足要求了。因为场景不同,分布式ID需要满足以下几个条件:

  1. 全局唯一性,不能出现重复的ID。
  2. 趋势递增,在MySQL InnoDB引擎中使用的是聚集索引,由于多数RDBMS使用B-tree的数据结构来存储索引数据,在主键的选择上应该尽量使用有序的主键保证写入性能。
  3. 单调递增,保证下一个ID一定大于上一个ID。例如分布式事务版本号、IM增量消息、排序等特殊需求。
  4. 信息安全,对于特殊业务,如订单等,分布式ID生成应该是无规则的,不能从ID上反解析出流量等敏感信息。

市面上对分布式ID生成大致有几种算法(一些开源项目都是围着这几种算法进行实现和优化):

  1. UUID:因为是本地生成,性能极高,但是生成的ID太长,16字节128位,通常需要字符串类型存储,且无序,所以很多场景不适用,也不适用于作为MySQL数据库的主键和索引(MySql官方建议,主键越短越好;对于InnoDB引擎,索引的无序性可能会引起数据位置频繁变动,严重影响性能)。
  2. 数据库自增ID:每次获取ID都需要DB的IO操作,DB压力大,性能低。数据库宕机对外依赖服务就是毁灭性打击,不过可以部署数据库集群保证高可用。
  3. 数据库号段算法:对数据库自增ID的优化,每次获取一个号段的值。用完之后再去数据库获取新的号段,可以大大减轻数据库的压力。号段越长,性能越高,同时如果数据库宕机,号段没有用完,短时间还可以对外提供服务。(美团的Leaf、滴滴的TinyId)
  4. 雪花算法:Twitter开源的snowflake,以时间戳+机器+递增序列组成,基本趋势递增,且性能很高,因为强依赖机器时钟,所以需要考虑时钟回拨问题,即机器上的时间可能因为校正出现倒退,导致生成的ID重复。(百度的uid-generator、美团的Leaf)

雪花算法和数据库号段算法用的最多,本篇主要对雪花算法原理剖析和解决时钟回拨问题讨论。

二、雪花算法snowflake

1、基本定义

snowflake原理其实很简单,生成一个64bit(long)的全局唯一ID,标准元素以1bit无用符号位+41bit时间戳+10bit机器ID+12bit序列化组成,其中除1bit符号位不可调整外,其他三个标识的bit都可以根据实际情况调整:

  1. 41bit-时间可以表示(1L<<41)/(1000L360024*365)=69年的时间。
  2. 10bit-机器可以表示1024台机器。如果对IDC划分有需求,还可以将10-bit分5-bit给IDC,分5-bit给工作机器。这样就可以表示32个IDC,每个IDC下可以有32台机器。
  3. 12个自增序列号可以表示2^12个ID,理论上snowflake方案的QPS约为409.6w/s。

注:都是从0开始计数。

2、snowflake的优缺点

优点:

  • 毫秒数在高位,自增序列在低位,整个ID都是趋势递增的。
  • 可以不依赖数据库等第三方系统,以服务的方式部署,稳定性更高,生成ID的性能也非常高。
  • 可以根据自身业务特性分配bit位,非常灵活。

缺点:

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

三、Java代码实现snowflake

如下示例,41bit给时间戳,5bit给IDC,5bit给工作机器,12bit给序列号,代码中是写死的,如果某些bit需要动态调整,可在成员属性定义。计算过程需要一些位运算基础。

public class SnowflakeIdGenerator {public static final int TOTAL_BITS = 1 << 6;private static final long SIGN_BITS = 1;private static final long TIME_STAMP_BITS = 41L;private static final long DATA_CENTER_ID_BITS = 5L;private static final long WORKER_ID_BITS = 5L;private static final long SEQUENCE_BITS = 12L;/*** 时间向左位移位数 22位*/private static final long TIMESTAMP_LEFT_SHIFT = WORKER_ID_BITS + DATA_CENTER_ID_BITS + SEQUENCE_BITS;/*** IDC向左位移位数 17位*/private static final long DATA_CENTER_ID_SHIFT = WORKER_ID_BITS + SEQUENCE_BITS;/*** 机器ID 向左位移位数 12位*/private static final long WORKER_ID_SHIFT = SEQUENCE_BITS;/*** 序列掩码,用于限定序列最大值为4095*/private static final long SEQUENCE_MASK =  -1L ^ (-1L << SEQUENCE_BITS);/*** 最大支持机器节点数0~31,一共32个*/private static final long MAX_WORKER_ID = -1L ^ (-1L << WORKER_ID_BITS);/*** 最大支持数据中心节点数0~31,一共32个*/private static final long MAX_DATA_CENTER_ID = -1L ^ (-1L << DATA_CENTER_ID_BITS);/*** 最大时间戳 2199023255551*/private static final long MAX_DELTA_TIMESTAMP = -1L ^ (-1L << TIME_STAMP_BITS);/*** Customer epoch*/private final long twepoch;private final long workerId;private final long dataCenterId;private long sequence = 0L;private long lastTimestamp = -1L;/**** @param workerId 机器ID* @param dataCenterId  IDC ID*/public SnowflakeIdGenerator(long workerId, long dataCenterId) {this(workerId, dataCenterId, null);}/**** @param workerId  机器ID* @param dataCenterId IDC ID* @param epochDate 初始化时间起点*/public SnowflakeIdGenerator(long workerId, long dataCenterId, Date epochDate) {if (workerId > MAX_WORKER_ID || workerId < 0) {throw new IllegalArgumentException("worker Id can't be greater than "+ MAX_WORKER_ID + " or less than 0");}if (dataCenterId > MAX_DATA_CENTER_ID || dataCenterId < 0) {throw new IllegalArgumentException("datacenter Id can't be greater than {" + MAX_DATA_CENTER_ID + "} or less than 0");}this.workerId = workerId;this.dataCenterId = dataCenterId;if (epochDate != null) {this.twepoch = epochDate.getTime();} else {//2010-10-11this.twepoch = 1286726400000L;}}public long genID() throws Exception {try {return nextId();} catch (Exception e) {throw e;}}public long getLastTimestamp() {return lastTimestamp;}/*** 通过移位解析出sequence,sequence有效位为[0,12]* 所以先向左移64-12,然后再像右移64-12,通过两次移位就可以把无效位移除了* @param id* @return*/public long getSequence2(long id) {return (id << (TOTAL_BITS - SEQUENCE_BITS)) >>> (TOTAL_BITS - SEQUENCE_BITS);}/*** 通过移位解析出workerId,workerId有效位为[13,17], 左右两边都有无效位* 先向左移 41+5+1,移除掉41bit-时间,5bit-IDC、1bit-sign,* 然后右移回去41+5+1+12,从而移除掉12bit-序列号* @param id* @return*/public long getWorkerId2(long id) {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*/public long getDataCenterId2(long id) {return (id << (TIME_STAMP_BITS + SIGN_BITS)) >>> (TIME_STAMP_BITS + WORKER_ID_BITS + SEQUENCE_BITS + SIGN_BITS);}/*** 41bit-时间,左边1bit-sign为0,可以忽略,不用左移,所以只需要右移,并加上起始时间twepoch即可。* @param id* @return*/public long getGenerateDateTime2(long id) {return (id >>> (DATA_CENTER_ID_BITS + WORKER_ID_BITS + SEQUENCE_BITS)) + twepoch;}public long getSequence(long id) {return id & ~(-1L << SEQUENCE_BITS);}public long getWorkerId(long id) {return id >> WORKER_ID_SHIFT & ~(-1L << WORKER_ID_BITS);}public long getDataCenterId(long id) {return id >> DATA_CENTER_ID_SHIFT & ~(-1L << DATA_CENTER_ID_BITS);}public long getGenerateDateTime(long id) {return (id >> TIMESTAMP_LEFT_SHIFT & ~(-1L << 41L)) + twepoch;}private synchronized long nextId() throws Exception {long timestamp = timeGen();// 1、出现时钟回拨问题,直接抛异常if (timestamp < lastTimestamp) {long refusedTimes = lastTimestamp - timestamp;// 可自定义异常类throw new UnsupportedOperationException(String.format("Clock moved backwards. Refusing for %d seconds", refusedTimes));}// 2、时间等于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、时间大于lastTimestamp没有发生回拨, sequence 从0开始this.sequence = 0L;}lastTimestamp = timestamp;return allocate(timestamp - this.twepoch);}private long allocate(long deltaSeconds) {return (deltaSeconds << TIMESTAMP_LEFT_SHIFT) | (this.dataCenterId << DATA_CENTER_ID_SHIFT) | (this.workerId << WORKER_ID_SHIFT) | this.sequence;}private long timeGen() {long currentTimestamp = System.currentTimeMillis();// 时间戳超出最大值if (currentTimestamp - twepoch > MAX_DELTA_TIMESTAMP) {throw new UnsupportedOperationException("Timestamp bits is exhausted. Refusing ID generate. Now: " + currentTimestamp);}return currentTimestamp;}private long tilNextMillis(long lastTimestamp) {long timestamp = timeGen();while (timestamp <= lastTimestamp) {timestamp = timeGen();}return timestamp;}/*** 测试* @param args*/public static void main(String[] args) throws Exception {SnowflakeIdGenerator snowflakeIdGenerator = new SnowflakeIdGenerator(1,2);long id = snowflakeIdGenerator.genID();System.out.println("ID=" + id + ", lastTimestamp=" + snowflakeIdGenerator.getLastTimestamp());System.out.println("ID二进制:" + Long.toBinaryString(id));System.out.println("解析ID:");System.out.println("Sequence=" + snowflakeIdGenerator.getSequence(id));System.out.println("WorkerId=" + snowflakeIdGenerator.getWorkerId(id));System.out.println("DataCenterId=" + snowflakeIdGenerator.getDataCenterId(id));System.out.println("GenerateDateTime=" + snowflakeIdGenerator.getGenerateDateTime(id));System.out.println("Sequence2=" + snowflakeIdGenerator.getSequence2(id));System.out.println("WorkerId2=" + snowflakeIdGenerator.getWorkerId2(id));System.out.println("DataCenterId2=" + snowflakeIdGenerator.getDataCenterId2(id));System.out.println("GenerateDateTime2=" + snowflakeIdGenerator.getGenerateDateTime2(id));}}

1、组装生成id

生成id的过程,就是把每一种标识(时间、机器、序列号)移到对应位置,然后相加。

long id = (deltaTime << TIMESTAMP_LEFT_SHIFT) | (this.dataCenterId << DATA_CENTER_ID_SHIFT) | (this.workerId << WORKER_ID_SHIFT) | this.sequence;
  • deltaTime向左移22位(IDC-bit+机器bit+序列号bit)。
  • dataCenterId向左移17位(机器bit+序列号bit)。
  • workerId向左移12位(序列号bit)。
  • sequence不用移。
  • 中间的|以运算规律就相当于+求和(1 | 1 = 1,1 | 0 = 1,0 | 1 = 1,0 | 0 = 0)。

2、计算最大值的几种方式

(1)注意到代码中分别对每个标识的最大值做了计算:

//序列掩码,用于限定序列最大值为4095 ((2^12)-1) ,从0开始算就有4096个序列
private static final long SEQUENCE_MASK =  -1L ^ (-1L << SEQUENCE_BITS);//最大支持机器节点数0~31,一共32个  (2^5)-1
private static final long MAX_WORKER_ID = -1L ^ (-1L << WORKER_ID_BITS);//最大支持数据中心节点数0~31,一共32个   (2^5)-1
private static final long MAX_DATA_CENTER_ID = -1L ^ (-1L << DATA_CENTER_ID_BITS);//最大时间戳 2199023255551   (2^41)-1
private static final long MAX_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)。

  • -1L的二进制为64个1:1111111111111111111111111111111111111111111111111111111111111111
  • -1L左移12位得到:1111111111111111111111111111111111111111111111111111 000000 000000
  • 最后11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 000000 000000^运算得到0000000000000000000000000000000000000000000000000000 111111 111111(前面有52个0),这就得到序列号的最大值(4095)了,也可以说是掩码。

(2)其实有一种更容易理解的计算最大值的方式,比如计算12bit-序列号的最大值,那就是(2^12 -1)呀,但是位运算性能更高,用位运算的方式就是((1 << 12) -1)。1左移12位得到1 0000 0000 0000,减1也是可以得到1111 1111 1111,即4095。

(3)还看到一种计算最大值的方式,继续拿12bit-序列号举例,~(-1L << 12)~不知道怎么计算那就傻了:

-1L先向左移12位得到1111111111111111111111111111111111111111111111111111 000000 000000,然后进行~按位非运算(~ 1 = -2,~ 0 = -1 ,~n = - ( n+1 )),也可以理解为反转,1转为0,0转为1,然后也可以得到0000000000000000000000000000000000000000000000000000 111111 111111

3、反解析ID

(1)通过已经生成的ID解析出时间、机器和序列号:

public long getSequence(long id) {return id & ~(-1L << SEQUENCE_BITS);
}public long getWorkerId(long id) {return id >> WORKER_ID_SHIFT & ~(-1L << WORKER_ID_BITS);
}public long getDataCenterId(long id) {return id >> DATA_CENTER_ID_SHIFT & ~(-1L << DATA_CENTER_ID_BITS);
}public long getGenerateDateTime(long id) {return (id >> TIMESTAMP_LEFT_SHIFT & ~(-1L << 41L)) + twepoch;
}

因为sequence本身就在低位,所以不需要移动,其他机器和时间都是需要将id向右移动,使得自己的有效位置在低位,至于和自己的最大值做&运算,是为了让不属于自己bit的位置无效,即都转为0。

例如:生成的id为1414362783486840832,转为二进制1001110100000110100110111010100111101010001000001000000000000,想解析出workerIdworkerId有效位为[13, 17],那就将id向右移12位,移到低位得到0000000000001001110100000110100110111010100111101010001000001workerId有5-bit,那么除了低位5-bit,其他位置都是无效bit,转为0。000000000000100111010000011010011011101010011110101000100000111111&运算得到1(左边都是0可以省掉)。

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

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

/*** 通过移位解析出sequence,sequence有效位为[0,12]* 所以先向左移64-12,然后再像右移64-12,通过两次移位就可以把无效位移除了* @param id* @return*/
public long getSequence2(long id) {return (id << (TOTAL_BITS - SEQUENCE_BITS)) >>> (TOTAL_BITS - SEQUENCE_BITS);
}
/*** 通过移位解析出workerId,workerId有效位为[13,17], 左右两边都有无效位* 先向左移 41+5+1,移除掉41bit-时间,5bit-IDC、1bit-sign,* 然后右移回去41+5+1+12,从而移除掉12bit-序列号* @param id* @return*/
public long getWorkerId2(long id) {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*/
public long getDataCenterId2(long id) {return (id << (TIME_STAMP_BITS + SIGN_BITS)) >>> (TIME_STAMP_BITS + WORKER_ID_BITS + SEQUENCE_BITS + SIGN_BITS);
}
/*** 41bit-时间,左边1bit-sign为0,可以忽略,不用左移,所以只需要右移,并加上起始时间twepoch即可。* @param id* @return*/
public long getGenerateDateTime2(long id) {return (id >>> (DATA_CENTER_ID_BITS + WORKER_ID_BITS + SEQUENCE_BITS)) + twepoch;
}

4、ID生成器使用方式

主要有两种方式,一种是发号器,一种是本地生成:

  • 发号器,就是把雪花算法ID生成封装成一个服务,部署在多台机器上,由外界请求发号器服务获取ID。这样做的好处,是机器不需要那么多,1024台完全足够了,相对ID的时间戳和序列号的bit就可以调大一些。但是因为需要远程请求获取ID,所以会受到网络波动的影响,性能上肯定是没有直接从本地生成获取高的,同时发号器一旦挂了,很多服务就不能对外提供服务了,所以发号器服务需要高可用,多实例,异地部署和容灾,发号器在发号的时候,也可以发布一段时间的ID,服务本地缓存起来,这样不仅提高性能,不需要每次都去请求发号器,也在一定程度上缓解了发号器故障带来的影响。
  • 本地生成ID,没有网络延迟,性能极高。只能通过机器id来保证生成的ID唯一性,所以需要提供足够多的机器id,每台机器可能部署多个服务,每个服务可能部署在多台机器,都需要分配不同的机器id,并且服务重启了也需要重新分配机器id。这样机器id就有了用后即毁的特点。需要足够多的机器id,就必须缩减时间bit和序列号bit。

可以利用MySql或者zk进行机器id的分配和管理。

四、时钟回拨问题和解决方案讨论

首先看看时钟为什么会发生回拨?机器本地时钟可能会因为各种原因发生不准的情况,网络中提供了NTP服务来做时间校准,做校准的时候就会发生时钟的跳跃或者回拨的问题。

因为雪花算法强依赖机器时钟,所以难以避免受到时钟回拨的影响,有可能产生ID重复。原标准实现代码中是直接抛异常,短暂停止对外服务,这样在实际生产中是无法忍受的。所以要尽量避免时钟回拨带来的影响,解决思路有两个:

  • 不依赖机器时钟驱动,就没时钟回拨的事儿了。即定义一个初始时间戳,在初始时间戳上自增,不跟随机器时钟增加。时间戳何时自增?当序列号增加到最大时,此时时间戳+1,这样完全不会浪费序列号,适合流量较大的场景,如果流量较小,可能出现时间断层滞后。
  • 依然依赖机器时钟,如果时钟回拨范围较小,如几十毫秒,可以等到时间回到正常;如果流量不大,前几百毫秒或者几秒的序列号肯定有剩余,可以将前几百毫秒或者几秒的序列号缓存起来,如果发生时钟回拨,就从缓存中获取序列号自增。

(时钟回拨问题,可通过手动调整电脑上的时钟进行模拟测试。)

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

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

起始时间可以构造器里指定,也可以用默认的,而sequence初始为-1,是为了不想浪费sequence+1=0这一序列号。

sequence = 0排除掉初始sequence=-1 +1 = 0的情况就是sequence超过最大值了,此时时间戳startTimestamp自增。

代码和思路都很简单,就是完全脱离机器时钟,彻底解决了时钟回拨问题。显而易见的优点,每一毫秒4096个序列号([0,4095])没有浪费,同时因为时间自增由程序自己掌控,所以可以利用未来时间,预先生成一些ID放在缓存里,外界从缓存中直接获取ID,快消费完了再生产,这样就形成了永动的生产-消费者模式,获取ID省去了生成的过程,性能也会大大提升。

但是时间戳完全自控,也有很明显的缺点,ID生成的时间,并不是真实的时间,如果流量较小,时间可能会滞后很多。如果对从ID解析出来的时间戳没有什么利用意义,这个缺点也不需要关心。

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

// 记录近2S的毫秒数的sequence的缓存
private int LENGTH = 2000;
// sequence缓存
private long[] sequenceCycle = new long[LENGTH];private synchronized long nextId() throws Exception {long timestamp = timeGen();int index = (int)(timestamp % LENGTH);// 1、出现时钟回拨问题,获取历史序列号自增if (timestamp < lastTimestamp) {long sequence = 0;do {if ((lastTimestamp - timestamp) > LENGTH) {// 可自定义异常、告警等,短暂不能对外提供,故障转移,将请求转发到正常机器。throw new UnsupportedOperationException("The timeback range is too large and exceeds 2000ms caches");}long preSequence = sequenceCycle[index];sequence = (preSequence + 1) & SEQUENCE_MASK;if (sequence == 0) {// 如果取出的历史序列号+1后已经达到超过最大值,// 则重新获取timestamp,重新拿其他位置的缓存timestamp = tilNextMillis(lastTimestamp);index = (int)(timestamp % LENGTH);} else {// 更新缓存sequenceCycle[index] = this.sequence;            return allocate((timestamp - this.twepoch), sequence);}} while (timestamp < lastTimestamp);// 如果在获取缓存的过程中timestamp恢复正常了,就走正常流程}// 2、时间等于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、时间大于lastTimestamp没有发生回拨, sequence 从0开始this.sequence = 0L;}// 缓存sequence + 更新lastTimestampsequenceCycle[index] = this.sequence;lastTimestamp = timestamp;// 生成idreturn allocate(timestamp - this.twepoch);
}

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

  • 若获取的历史sequence+1之后超过了最大值,则重新获取时间戳,重新获取缓存sequence
  • 极端情况下,获取很多次缓存sequence+1都超过了最大值,就会一直循环获取,这样可能会影响性能,所以实际生产中可以限定重新获取次数。
  • 在这个重新获取的过程中,时钟可能恢复正常了,则此时也要退出循环,走正常流程。

3、等待时钟校正

private synchronized  long nextId3() {long timestamp = timeGen();// 1、出现时钟回拨问题,如果回拨幅度不大,等待时钟自己校正if (timestamp < lastTimestamp) {int sleepCntMax = 2;int sleepCnt = 0;do {long sleepTime = lastTimestamp - timestamp;if (sleepCnt > sleepCntMax) {// 可自定义异常类throw new UnsupportedOperationException(String.format("Clock moved backwards. Refusing for %d seconds", sleepTime));}if (sleepTime <= 500) {try {Thread.sleep(sleepTime);} catch (InterruptedException e) {e.printStackTrace();} finally {sleepCnt++;timestamp = tilNextMillis(lastTimestamp);}} else {// 可自定义异常类throw new UnsupportedOperationException(String.format("Clock moved backwards. Refusing for %d seconds", sleepTime));}} while (timestamp < lastTimestamp);}// 2、时间等于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、时间大于lastTimestamp没有发生回拨, sequence 从0开始this.sequence = 0L;}lastTimestamp = timestamp;// 生成idreturn allocate(timestamp - this.twepoch);
}

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

解决时钟回拨问题的方法还有很多,无非就是避免和缓解。每种方式有各自的特点和适用场景,可以两两结合使用,比如时钟回拨幅度小,就休眠校正,回拨幅度大或者出现多次回拨,也不抛异常,获取缓存sequence对外提供服务。也可以当发生时钟回拨时,用备用机器id生成ID等。

五、要点总结

  1. 生成全局唯一的分布式ID的方式有很多,常用的有数据库号段算法和雪花算法,这两个算法的实践,大厂也有开源的项目,如百度的uid-generator、美团的Leaf、滴滴的TinyId等。
  2. 雪花算法的原理很简单,主要由时间戳+机器id+序列号生成64bit的ID,整体趋势递增,且全局唯一,性能也不错。每种组成标识的bit都可以自定义,灵活性很高,如果需要更高的QPS,可以相对的把序列号bit调大一些。
  3. 因为雪花算法强依赖机器时钟,就难以避免时钟回拨问题,解决的方式很多,无非从避免和缓解两个角度出发,常用的方式有,时间戳自增脱离机器时钟依赖,利用缓存序列号,或者等待时钟校正等,各有各的特点,正确利用其优点,才能最大提高性能。
  4. 雪花算法ID生成器的使用方式有两种,一种是远程发号器,需要做到高可用。另一种就是直接本地生成ID,省去了远程请求过程,性能自然也是比远程发号器高的,但是机器id用后即毁,需要分配足够多的机器id。机器id的管理和分配可以利用MySql或者ZK

参考:

  • Leaf——美团点评分布式ID生成系统

  • http://www.cnblogs.com/relucent/p/4955340.html

  • https://segmentfault.com/a/1190000011282426

  • https://www.yuque.com/simonalong/butterfly/tul824

如若文章有错误理解,欢迎批评指正,同时非常期待你的评论、点赞和收藏。

雪花算法snowflake分布式id生成原理详解,以及对解决时钟回拨问题几种方案讨论相关推荐

  1. 雪花算法【分布式ID问题】【刘新宇】

    分布式ID 1 方案选择 UUID UUID是通用唯一识别码(Universally Unique Identifier)的缩写,开放软件基金会(OSF)规范定义了包括网卡MAC地址.时间戳.名字空间 ...

  2. mysql 主键设计 雪花算法_数据库 ID 生成方案:雪花算法

    今天介绍的雪花算法:Snowflake,可以让负责生成分布式 ID 的每台机器在每毫秒内生成不一样的 ID.Snowflake 是 Twitter 开源的分布式 ID 生成算法,它不依赖数据库. 核心 ...

  3. 雪花算法-Java实现-解决时钟回拨的一种方法

    背景: 前不久发生了一次严重的生产事件, 与雪花算法有关,但不是雪花算法的问题 具体问题参考代码main中的注释, 结论如下 序列可以使用69年, 序列的长度变化是这样的, 假设以当前时间为初始化值 ...

  4. java不规则算法_分布式id生成算法 snowflake 详解

    背景 在复杂分布式系统中,往往需要对大量的数据和消息进行唯一标识.如在支付流水号.订单号等,随者业务数据日渐增长,对数据分库分表后需要有一个唯一ID来标识一条数据或消息,数据库的自增ID显然不能满足需 ...

  5. 推特雪花算法,分布式id生成器

    推特雪花算法 分布式id生成器 package util;import java.lang.management.ManagementFactory; import java.net.InetAddr ...

  6. 大型分布式服务器架构原理详解

    引言 一个成熟的大型网站的系统架构并不是开始设计就具备完整的高性能.高可用.安全等特性,它总是随着用户量的增加,业务功能的扩展逐渐演变完善的,在这个过程中,开发模式.技术架构.设计思想也发生了很大的变 ...

  7. Android面试Hash原理详解二

    Hash系列目录 Android面试Hash原理详解一 Android面试Hash原理详解二 Android面试Hash常见算法 Android面试Hash算法案例 Android面试Hash原理详解 ...

  8. Redis实战和核心原理详解(4)Redis存储Key的一种设计实现方式:模式匹配

    注意:此方案仅为演示Redis 的使用,正式生产环境切勿使用! 相关文章: Redis实战和核心原理详解(1)Centos7.0下安装Redis 5.0详细过程和使用常见问题 Redis实战和核心原理 ...

  9. Twitter的分布式雪花算法 SnowFlake 每秒自增生成26个万个可排序的ID (Java版)

    分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的. 有些时候我们希望能使用一种简单一 ...

最新文章

  1. 初学者应该了解的编程陷阱:javascript篇
  2. EASYHOOK逆向寒假生涯(20/100)
  3. Delphi常见各类编译错误信息-中英对照
  4. [BZOJ 5072][Lydsy1710月赛]小A的树
  5. Android Studio :1、连接手机调试(超级详细;附带连接测试录像);2、点击Button按钮,显示Toast中的内容
  6. yiicms php版本,yiicms
  7. p20pro 鸿蒙,后置镜头变液态双摄?华为P50Pro再曝光,搭载鸿蒙OS传感器变1寸
  8. 程序员斗图时最爱用哪些表情包?拿走不谢!
  9. 软件测试基础知识 + 面试理论(超详细)
  10. U盘启动盘制作,金士顿2GU盘量产工…
  11. win10安装steam有损计算机,win10系统steam安装更新失败的解决方法
  12. CHD 5.10 离线安装
  13. 移动端隐藏scroll滚动条::-webkit-scrollbar
  14. 【云图】自有数据的多边形检索(云检索)
  15. 同时设置min-height和max-height后el-scrollbar滚动监听失效?
  16. 七牛云CNAME设置,七牛云绑定域名。
  17. 2021服务器品牌前十大排名
  18. 通过PPG“神话” 论服装电子商务
  19. 医学图像处理入门知识 | 格式DICOM,MHD+RAW | 坐标系 | ITK-SNAP | 重采样
  20. 定型“体验+大数据”模式 大悦城走出“弯路”

热门文章

  1. 澳门大学研究生计算机专业含金量,澳门大学的研究生院怎么样?什么专业比较好?...
  2. 金蝶云进销存与云会计基础算账
  3. unity 应用商店下载的插件位置
  4. Spring Boot探究之旅--启动分析
  5. 常见的深度学习优化算法(概述)
  6. 余生很长,凑活过吧。
  7. mysql最大公约数_什么是最大公约数和最小公倍数?
  8. PHPStorm+Xdebug进行emote Debug时无法进入断点问题排查
  9. 计算机固态硬盘与机械硬盘的区别是什么,怎么辨别电脑里面的cde盘是机械硬盘还是固态硬盘...
  10. python中queue.Queue task_done