json 解析长整型(大整数),数字精确值丢失?何不来试试 Mini Snowflake

502 阅读6分钟

问题

很多系统都会需要生成大量唯一 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. 符号位:1位,最高位被用作符号位,0代表正数,1代表负数。
  2. 指数位:11位,用于存储指数部分,这部分通过偏移量(bias)表示真实的指数值。偏移量是固定的,对于64位的浮点数,偏移量为1023。
  3. 尾数(或小数)位:52位,用于存储小数部分,实际上是表示1与2之间的小数部分,因为浮点数的表示基于二进制系统。为了提高表示的精度,IEEE 754标准采用了一种称为“隐含的前导1”的技巧,即不直接存储这个1,而是假定第一个数字总是1(除非是特殊的数值,如0或NaN等)。

所以,它只能安全地表示-2^53+1到2^53-1之间的整数。而雪花算法生成的值超过了这个范围。

解决?

那又该如何解决呢?一般常用以下三种方法解决:

  1. 使用字符串表示:将长整型数值用字符串的形式表示,确保在数据传递过程中不会失去精度。
  2. 使用BigInt类型:在JavaScript中,有一种新的原始数据类型BigInt,它允许你安全地表示和操作大整数。
  3. 使用第三方库:例如,json-bigint等第三方库,亦或者增加一些配置。

弊端 OR 痛点?

这三种方法有什么弊端或痛点吗?

  1. 使用字符串表示:

    • 互操作性问题:当你的应用需要与其他系统交互时,如果其他系统期望的是数字而不是字符串,这种方式可能会遇到问题。
    • 使用不便:对于需要进行算术运算的场合,字符串形式的数字会非常不便,你需要频繁地在字符串和数字之间转换。
    • 性能影响:频繁的类型转换可能对性能有一定影响,尤其是在处理大量数据时。
  2. 使用BigInt类型:

    • 兼容性限制BigInt是一个相对较新的JavaScript特性,老版本的浏览器和JavaScript环境可能不支持它。
    • Number类型不完全兼容BigIntNumber之间不能直接进行算术运算,使用不当时可能会引发错误。
    • JSON兼容性问题:标准的JSON不支持BigInt,如果直接将含有BigInt的对象序列化为JSON字符串,可能会遇到问题。
  3. 使用第三方库:

    • 增加额外的依赖:使用第三方库意味着增加了项目的依赖,这可能会增加项目的复杂性。
    • 性能考虑:相比原生的类型处理,第三方库的实现可能会引入额外的性能开销。
    • 安全风险:引入第三方库也可能带来安全风险,特别是如果库不是很活跃,可能无法及时修复潜在的安全问题。
    • 可移植性问题:依赖特定库可能限制了代码的可移植性,尤其是当涉及到不同运行环境或框架时。

完美解决?

那有没有完美的解决方法呢?

有,那就是生成的全局唯一ID的整型大小不超过2^53-1就好了。是不是很简单?

具体点?

将53位分成三段:

  1. 第一段长度 32 ~ 43 位,存放(毫)秒级时间戳
  2. 第二段长度 0 ~ 20 位,存放机器码
  3. 第三段长度 2 ~ 21 位,存放计数器(递增)

image.png

名称方案最大支持时间极限速度(N=0)说明
IDSecond 32+N+(21-N)2106-02-07 14:28:152,097,151 / sN = 0 OR (N >1 AND N <20)
ID2Second 33+N+(20-N)2242-03-16 20:56:311,048,575 / sN = 0 OR (N >1 AND N <19)
ID3Millisecond 42+N+(11-N)2109-05-15 15:35:112,047 / msN = 0 OR (N >1 AND N <10)
ID3Millisecond 43+N+(10-N)2248-09-26 23:10:221,023 / msN = 0 OR (N >1 AND N <9)

有没有现成库可以使用?

有,github.com/ace-zhaoy/g…

有啥特性?

核心特性如下:

  • 支持秒级和毫秒级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()