最近项目后端为 Prong 开发了一个基于 snowflake 算法的 Java 分布式 ID 组件,将实体主键从原来的 String 类型的 UUID 修改成了 Long 型的分布式 ID。修改后发现前端显示的 ID 和数据库中的 ID 不一致。例如数据库中存储的是:812782555915911412,显示出来却成了 812782555915911400,后面 2 位变成了 0,精度丢失了:
1 | console.log(812782555915911412); |
原因
这是因为 JavaScript 中数字的精度是有限的,Java 的 Long 类型的数字超出了 JavaScript 的处理范围。JavaScript 内部只有一种数字类型 Number,所有数字都是采用 IEEE 754 标准定义的双精度 64 位格式存储,即使整数也是如此。这就是说,JavaScript 语言的底层根本没有整数,所有数字都是小数(64 位浮点数)。其结构如图:
各位的含义如下:
- 1 位(s) 用来表示符号位,0 表示正数,1 表示负数
- 11 位(e) 用来表示指数部分
- 52 位(f) 表示小数部分(即有效数字)
双精度浮点数(double
)并不是能够精确表示范围内的所有数, 虽然双精度浮点型的范围看上去很大: 。 可以表示的最大整数可以很大,但能够精确表示,使用算数运算的并没有这么大。因为小数部分最大是 52
位,因此 JavaScript 中能精准表示的最大整数是 ,十进制即 9007199254740991
。
1 | console.log(Math.pow(2, 53) - 1); |
JavaScript 有所谓的最大和最小安全值:
1 | console.log(Number.MAX_SAFE_INTEGER); |
安全
意思是说能够 one-by-one
表示的整数,也就是说在范围内,双精度数表示和整数是一对一的,在这个范围以内,所有的整数都有唯一的浮点数表示,这叫做安全整数。
而超过这个范围,会有两个或更多整数的双精度表示是相同的;即超过这个范围,有的整数是无法精确表示的,只能大约(round)到与它相近的浮点数(说到底就是科学计数法)表示,这种情况下叫做不安全整数
,例如:
1 | console.log(Number.MAX_SAFE_INTEGER + 1); // 结果:9007199254740992,精度未丢失 |
而 Java
的 Long
类型的有效位数是 63 位(扣除一位符号位),其最大值为,十进制为 9223372036854775807。
1 | public static void main(String[] args) { |
所以只要 java 传给 JavaScript 的 Long
类型的值超过 9007199254740991,就有可能产生精度丢失,从而导致数据和逻辑出错。
和其他编程语言(如 C 和 Java)不同,JavaScript 不区分整数值和浮点数值,所有数字在 JavaScript 中均用浮点数值表示,所以在进行数字运算的时候要特别注意精度缺失问题。容易造成混淆的是,某些运算只有整数才能完成,此时 JavaScript 会自动把 64 位浮点数,转成 32 位整数,然后再进行运算,由于浮点数不是精确的值,所以涉及小数的比较和运算要特别小心。
解决方法
解决办法就是让 Javascript 把数字当成字符串进行处理。对 Javascript 来说如果不进行运算,数字和字符串处理起来没有什么区别。当然如果需要进行运算,只能采用其他方法,例如 JavaScript 的一些开源库 bignum、bigint 等支持长整型的处理。Java 进行 JSON 处理的时候是能够正确处理 long 型的,只需要将数字转化成字符串就可以了。