snowflake

分布式id生成算法的有很多种,Twitter的雪花算法(SnowFlake)就是其中经典的一种。

SnowFlake算法的优点:

生成ID时不依赖于数据库,完全在内存生成,高性能高可用。

容量大,每秒可生成几百万ID。

SnowFlake算法在同一毫秒内最多可以生成多少个全局唯一ID呢?同一毫秒的ID数量 = 1024 * 4096 = 4194304

所有生成的id按时间趋势递增,后续插入数据库的索引树的时候,性能较高。

整个分布式系统内不会产生重复id(因为有datacenterId和workerId来做区分)

SnowFlake算法的缺点:

依赖于系统时钟的一致性。如果某台机器的系统时钟回拨,有可能造成ID冲突,或者ID乱序。

还有,在启动之前,如果这台机器的系统时间回拨过,那么有可能出现ID重复的危险。

问题?

workId 怎么保证唯一?

可以通过分布式缓存来保存机器ID和workId之间的映射关系。启动的时候访问分布式缓存查询当前机器ID对应的workId,如果查询不到则获取一个并保存到分布式缓存中。

可通过Zookeeper管理workId,免去手动频繁修改集群节点,去配置机器ID的麻烦。

lastTimestamp上次生成ID的时间戳,这个是在内存中,系统时钟回退+重启后呢?无法保证

目前好像只能流程上控制系统时钟不回退。

41位 (timestamp - this.twepoch) << this.timestampLeftShift 超过长整型怎么办?

this.twepoch 可以设置当前开始使用系统时的时间,可以保证69年不超

Javascript 无法支持> 53位的数字怎么办?

js Number被表示为双精度浮点数,最大值为 Number.MAX_SAFE_INTEGER = 2^53-1

BigInt 是 JavaScript 中的一个新的原始数字类型,可以用任意精度表示整数。即使超出 Number 的安全整数范围限制,也可以安全地存储和操作大整数。

要创建一个 BigInt,将 n 作为后缀添加到任何整数文字字面量

BigInt 支持大数,那怎么控制这里用 64bits 长整型,左移溢出会出现问题吗?

这里不做处理会出现问题,BigInt 可以用任意精度表示整数

如何处理?暂不处理

此问题本质还是上面的41位时间差问题,69年不超,再长就超了,需要重新设计支持,也可以做溢出提示。

如果想限制为仅64位整数,则必须始终使用强制转换 BigInt.asIntN BigInt.asUintN

只要我们传递 BigInt 超过 64 位整数范围的值(例如,63 位数值 + 1 位符号位),就会发生溢出。

概述

SnowFlake算法生成id的结果是一个64bit大小的整数,它的结构如下图:


1位,不用。二进制中最高位为1的都是负数,但是我们生成的id一般都使用整数,所以这个最高位固定是0

41位,用来记录时间戳(毫秒)。

41位可以表示$2^{41}-1$个数字,

如果只用来表示正整数(计算机中正数包含0),可以表示的数值范围是:0 至 $2^{41}-1$,减1是因为可表示的数值范围是从0开始算的,而不是1。

也就是说41位可以表示$2^{41}-1$个毫秒的值,转化成单位年则是$(2^{41}-1) / (1000 * 60 * 60 * 24 * 365) = 69$年

10位,用来记录工作机器id。

可以部署在$2^{10} = 1024$个节点,包括5位datacenterId和5位workerId

5位(bit)可以表示的最大正整数是$2^{5}-1 = 31$,即可以用0、1、2、3、....31这32个数字,来表示不同的datecenterId或workerId

12位,序列号,用来记录同毫秒内产生的不同id。

12位(bit)可以表示的最大正整数是$2^{12}-1 = 4095$,即可以用0、1、2、3、....4094这4095个数字,来表示同一机器同一时间截(毫秒)内产生的4095个ID序号

由于64bit的整数是long类型,所以SnowFlake算法生成的id就是long来存储的。但这个长度超出 js 最大数范围 Number.MAX_SAFE_INTEGER 了,在js 中实现要使用 BigInt 来表示。

Talk is cheap, show you the code

代码理解

位运算基础

在计算机中,负数的二进制是用补码来表示的。

反码 = 除符号位, 原码其余位取反而得

补码 = 反码 + 1

补码 = (原码 - 1)再取反码

在计算机中无符号数用原码表示, 有符号数用补码表示

补码的意义就是可以拿补码和原码(3的二进制)相加,最终加出一个“溢出的0”

因此-1的二进制应该这样算:

00000000 00000000 00000000 00000001 //原码:1的二进制

11111111 11111111 11111111 11111110 //取反码:1的二进制的反码

11111111 11111111 11111111 11111111 //加1:-1的二进制表示(补码)

用位运算计算n个bit能表示的最大数值

const maxWorkerId = -1n ^ (-1n << 5n)

// 利用位运算计算出5位能表示的最大正整数是多少

-1 左移 5,得结果a :

11111111 11111111 11111111 11111111 // -1的二进制表示(补码)

11111 11111111 11111111 11111111 11100000 // 高位溢出的不要,低位补0

11111111 11111111 11111111 11100000 // 结果a

-1 异或 a :

11111111 11111111 11111111 11111111 // -1的二进制表示(补码)

^ 11111111 11111111 11111111 11100000 // 两个操作数的位中,相同则为0,不同则为1

---------------------------------------------------------------------------

00000000 00000000 00000000 00011111 // 最终结果31

最终结果是31,二进制 00000000 00000000 00000000 00011111 转十进制可以这么算:

2^4 + 2^3 + 2^2 + 2^1 + 2^0 = 16 + 8 + 4 + 2 + 1 = 31

用mask防止溢出

this.sequence = (this.sequence + 1n) & this.sequenceMask;

// 这段代码通过 `位与` 运算保证计算的结果范围始终是 0-4095

用位运算汇总结果

位或运算,同一位只要有一个是1,则结果为1,否则为0。

位运算左移超出的溢出部分扔掉,右侧空位则补0。

return (

((timestamp - this.twepoch) << this.timestampLeftShift) | // 时间差左移22

(this.dataCenterId << this.dataCenterIdShift) | // 数据标识id左移 17

(this.workerId << this.workerIdShift) | // 机器id左移 12

this.sequence

);

--------------------

|

|简化

\|/

--------------------

return (la) |

(lb) |

(lc) |

sequence;

数据示例:

timestamp: 1505914988849

twepoch: 1288834974657

datacenterId: 17

workerId: 25

sequence: 0

二进制过程

1 | 41 | 5 | 5 | 12

0|0001100 10100010 10111110 10001001 01011100 00| | | //la

0| |10001| | //lb

0| | |1 1001| //lc

or 0| | | |‭0000 00000000‬ //sequence

------------------------------------------------------------------------------------------

0|0001100 10100010 10111110 10001001 01011100 00|10001|1 1001|‭0000 00000000‬ //结果:910499571847892992

支持反推数据

反推机器ID、数据中心ID和创建的时间戳

机器ID = id >> workerIdShift & ~(-1n << workerIdBits);

数据中心ID = id >> datacenterIdShift & ~(-1n << datacenterIdBits);

时间戳 = id >> timestampLeftShift & ~(-1n << 41n) + twepoch;

参考:

雪花算法

BigInt

关于限制为仅64位整数,需要特定处理,可以提示数据长度溢出了。

// 在 console 中测试,溢出怎么办,怎么检查出问题了

var aa = 1n;

(aa<<62n).toString(2).padStart(64, 0);

(aa<<65n).toString(2).padStart(64, 0);

(BigInt.asIntN(64, aa<<62n)).toString(2).padStart(64, 0);

(BigInt.asIntN(64, aa<<65n)).toString(2).padStart(64, 0);

const max = 2n ** (64n - 1n) - 1n;

BigInt.asIntN(64, max); // 有符号数

BigInt.asUintN(64, max); // 无符号数

→ 9223372036854775807n

BigInt.asIntN(64, max + 1n);

new BigInt64Array(4)

jest