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

最后

给大家送一个小福利

附高清脑图,高清知识点讲解教程,以及一些面试真题及答案解析。送给需要的提升技术、准备面试跳槽、自身职业规划迷茫的朋友们。

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

  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、基本定义

全网都在用这个图-yyds

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

  1. 41bit-时间可以表示(1L<<41)/(1000L_3600_24*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-11

this.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 + 1

if (timestamp == lastTimestamp) {

sequence = (sequence + 1) & SEQUENCE_MASK;

// Exceed the max sequence, we wait the next second to generate id

if (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));

}

}

雪花算法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)了,也可以说是掩码。

Java运算符优先级列表

(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

*/

最后

光给面试题不给答案不是我的风格。这里面的面试题也只是凤毛麟角,还有答案的话会极大的增加文章的篇幅,减少文章的可读性

Java面试宝典2021版

最常见Java面试题解析(2021最新版)

2021企业Java面试题精选

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

ce`

/**

  • 通过移位解析出sequence,sequence有效位为[0,12]

  • 所以先向左移64-12,然后再像右移64-12,通过两次移位就可以把无效位移除了

  • @param id

  • @return

*/

最后

光给面试题不给答案不是我的风格。这里面的面试题也只是凤毛麟角,还有答案的话会极大的增加文章的篇幅,减少文章的可读性

Java面试宝典2021版

[外链图片转存中…(img-KsgHU5uL-1715716600509)]

[外链图片转存中…(img-8RQuJzX7-1715716600510)]

最常见Java面试题解析(2021最新版)

[外链图片转存中…(img-zpQByY21-1715716600510)]

[外链图片转存中…(img-9pbuy3vQ-1715716600510)]

2021企业Java面试题精选

[外链图片转存中…(img-wqEnQkxP-1715716600511)]

[外链图片转存中…(img-tPvtJFQB-1715716600512)]

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

  • 17
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
雪花算法是种生成分布式ID算法,它可以生成一个64位的ID,其中包含了时间戳、数据中心ID和机器ID等信息。下面是雪花算法生成分布式ID的软件设计模型: 1. 定义一个Snowflake类,该类包含以下属性: - datacenter_id: 数据中心ID,占5位,取值围为0~31。 - worker_id: 机器ID,占5位,取值范围为0~31。 - sequence: 序列号,占12位,取值范围为0~4095。 - last_timestamp: 上一次生成ID的时间戳。 2. 实现Snowflake类的构造函数,初始化datacenter_id和worker_id属性。 3. 实现一个next_id方法,该方法用于生成下一个ID。具体实现如下: - 获取当前时间戳,单位为毫秒。 - 如果当前时间戳小于上一次生成ID的时间戳,则说明系统时钟退过,抛出异常。 - 如果当前时间戳等于上一次生成ID的时间戳,则将序列号加1。 - 如果当前时间戳大于上一次生成ID的时间戳,则将序列号重置为0,并将last_timestamp属性更新为当前时间戳。 - 将datacenter_id、worker_id、时间戳和序列号按照一定的位数组合成一个64位的ID。 - 返生成ID。 4. 在分布式系统中,每个节点都需要创建一个Snowflake实例,并指定不同的datacenter_id和worker_id。每个节点生成ID都是唯一的,且具有时间顺序。 下面是一个Python实现的雪花算法生成分布式ID的代码示例: ```python import time class Snowflake: def __init__(self, datacenter_id, worker_id): self.datacenter_id = datacenter_id self.worker_id = worker_id self.sequence = 0 self.last_timestamp = -1 def next_id(self): timestamp = int(time.time() * 1000) if timestamp < self.last_timestamp: raise Exception("Clock moved backwards. Refusing to generate id") if timestamp == self.last_timestamp: self.sequence = (self.sequence + 1) & 4095 if self.sequence == 0: timestamp = self.wait_next_millis(self.last_timestamp) else: self.sequence = 0 self.last_timestamp = timestamp return ((timestamp - 1288834974657) << 22) | (self.datacenter_id << 17) | (self.worker_id << 12) | self.sequence def wait_next_millis(self, last_timestamp): timestamp = int(time.time() * 1000) while timestamp <= last_timestamp: timestamp = int(time.time() * 1000) return timestamp ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值