问题
很多系统都会需要生成大量唯一 ID,而唯一 ID 大多数采用 Snowflake 算法。但这个算法有个弊端,就是生成的整型太大了。使用 json 传给客户端时,客户端往往会使用默认 json 库解析,导致数字精确值丢失。就是返回的值与解析的值不一致。
举个例子
var json = '{"largeNumber": 1234567890123456789}';
var obj = JSON.parse(json);
console.log(obj.largeNumber); // 输出:1234567890123456800
这会导致刚接触大整数解析的前后端开发一脸懵逼,并且可能会花很多时间来排查问题和解决问题,甚至改变设计方案。
原因
这就不得不介绍下 JS 的Number 类型了。JavaScript中的Number类型遵循IEEE 754标准,使用64位(8字节)来表示浮点数。64位的分配方式如下:
- 符号位:1位,最高位被用作符号位,0代表正数,1代表负数。
- 指数位:11位,用于存储指数部分,这部分通过偏移量(bias)表示真实的指数值。偏移量是固定的,对于64位的浮点数,偏移量为1023。
- 尾数(或小数)位:52位,用于存储小数部分,实际上是表示1与2之间的小数部分,因为浮点数的表示基于二进制系统。为了提高表示的精度,IEEE 754标准采用了一种称为“隐含的前导1”的技巧,即不直接存储这个1,而是假定第一个数字总是1(除非是特殊的数值,如0或NaN等)。
所以,它只能安全地表示-2^53+1到2^53-1之间的整数。而雪花算法生成的值超过了这个范围。
解决?
那又该如何解决呢?一般常用以下三种方法解决:
- 使用字符串表示:将长整型数值用字符串的形式表示,确保在数据传递过程中不会失去精度。
- 使用BigInt类型:在JavaScript中,有一种新的原始数据类型BigInt,它允许你安全地表示和操作大整数。
- 使用第三方库:例如,json-bigint等第三方库,亦或者增加一些配置。
弊端 OR 痛点?
这三种方法有什么弊端或痛点吗?
-
使用字符串表示:
- 互操作性问题:当你的应用需要与其他系统交互时,如果其他系统期望的是数字而不是字符串,这种方式可能会遇到问题。
- 使用不便:对于需要进行算术运算的场合,字符串形式的数字会非常不便,你需要频繁地在字符串和数字之间转换。
- 性能影响:频繁的类型转换可能对性能有一定影响,尤其是在处理大量数据时。
-
使用BigInt类型:
- 兼容性限制:
BigInt是一个相对较新的JavaScript特性,老版本的浏览器和JavaScript环境可能不支持它。 - 与
Number类型不完全兼容:BigInt和Number之间不能直接进行算术运算,使用不当时可能会引发错误。 - JSON兼容性问题:标准的JSON不支持
BigInt,如果直接将含有BigInt的对象序列化为JSON字符串,可能会遇到问题。
- 兼容性限制:
-
使用第三方库:
- 增加额外的依赖:使用第三方库意味着增加了项目的依赖,这可能会增加项目的复杂性。
- 性能考虑:相比原生的类型处理,第三方库的实现可能会引入额外的性能开销。
- 安全风险:引入第三方库也可能带来安全风险,特别是如果库不是很活跃,可能无法及时修复潜在的安全问题。
- 可移植性问题:依赖特定库可能限制了代码的可移植性,尤其是当涉及到不同运行环境或框架时。
完美解决?
那有没有完美的解决方法呢?
有,那就是生成的全局唯一ID的整型大小不超过2^53-1就好了。是不是很简单?
具体点?
将53位分成三段:
- 第一段长度 32 ~ 43 位,存放(毫)秒级时间戳
- 第二段长度 0 ~ 20 位,存放机器码
- 第三段长度 2 ~ 21 位,存放计数器(递增)
| 名称 | 方案 | 最大支持时间 | 极限速度(N=0) | 说明 |
|---|---|---|---|---|
| ID | Second 32+N+(21-N) | 2106-02-07 14:28:15 | 2,097,151 / s | N = 0 OR (N >1 AND N <20) |
| ID2 | Second 33+N+(20-N) | 2242-03-16 20:56:31 | 1,048,575 / s | N = 0 OR (N >1 AND N <19) |
| ID3 | Millisecond 42+N+(11-N) | 2109-05-15 15:35:11 | 2,047 / ms | N = 0 OR (N >1 AND N <10) |
| ID3 | Millisecond 43+N+(10-N) | 2248-09-26 23:10:22 | 1,023 / ms | N = 0 OR (N >1 AND N <9) |
有没有现成库可以使用?
有啥特性?
核心特性如下:
- 支持秒级和毫秒级ID生成;
- 生成的ID是递增的,不会出现重复;
- 支持分布式系统中的ID生成,可以配置节点;
- ID为53位整型,能够支持JSON整型传输解析,避免超过长度限制导致解析错误;
- 采用无锁技术,生成速度非常快,每秒可以生成超过两百万个不重复且递增的ID;
- 超出每秒生成上限会等待到下一秒,并且计数器重置开始重新计数;
- 在服务器时钟发生回拨时也不会生成重复的ID,支持等待或获取网络时间,默认等待时间为3秒;
- 使用简单,单机无需配置,分布式系统只需设置节点即可;
- ID设计采用三段式结构:时间戳、机器码、计数器。
如何使用?
简单
go get github.com/ace-zhaoy/go-id
三种任君取用
id := goid.GenID()
id2 := goid.GenID2()
id3 := goid.GenID3()
想支持分布式?
设置节点标识及长度
// 节点长度为 10(支持 1023 台机器),当前节点标识是1
goid.GetID().SetNode(1, 10)
单位时间内不想连续递增?
设置序列号起始值以及间隔值
// id末尾的序列号间隔 1024
goid.GetID().SetDelta(1024)
不想固定间隔递增?
设置随机增量
// id末尾的序列号每次在 1 ~ 1024 之间随机增加,范围不宜太大
goid.GetID().SetRandomDelta(1024)
服务器时钟回拨了怎么办?
默认等待 3s,也可设置等待时间以及NTP Server。设置之后,超时会尝试从NTP Server获取最新时间。
goid.GetID().SetMaxBacktrackWait(10 * time.Second)
goid.GetID().SetNTPServer("pool.ntp.org")
分布式机器码咋弄?
提供一个根据机器主机名和ip生成机器码的函数
# 获取长度为 8 位以内的机器码
goid.GenerateMachineCode(8)
不想使用包自带的全局变量?
那就New一个
// 保存好变量
myID := goid.NewID()
id := myID.Generate()