一个通过mongo shell插入整数的问题

coding Quarterback 174℃ 0评论

本文主要讨论这几个问题:

  1. 问题描述
  2. 原因分析
  3. 问题验证
  4. 解决方法
  5. 扩展

这是中文社区Q群网友问到的一个问题,很简单基础但也很容易迷惑人。

1. 问题描述

通过mongo shell按照下图所示的方式插入一个超大的长整型整数(准确的说,应该是一个超过2^53 – 1的数时,例如123456789111111111)。但是随后查询发现,查询出的值和插入时的值并不相等(如图,插入123456789111111111,查询时却变为123456789111111104)。

97224546

2. 原因分析

初看这个问题,很容易被迷惑,关注点很容易错误的集中在NumberLong函数上,首先想到的是不是NumberLong这个构造长整形的方法实现有问题。

首先说一下,为什么要使用NumberLong函数?

因为mongo shell实际上是一个js环境,shell中的命令是通过一个JS引擎解释执行的。在js中,数值常量是双精度浮点数(double)类型。因为我们想要通过mongo shell插入一个长整型的整数,因此在上述插入过程中,我们需要通过NumberLong函数显示的将double型的字面常量123456789111111111封装为一个长整形,从查出的结果来看,类型转换是正确的。

NumberLong函数应该是没有问题的,那么问题出在哪?答案是双精度浮点数本身

其实,这个问题跟是否在mongo shell中操作,并没有关系。问题的本质,在于双精度浮点数本身。

我们知道,在计算机中,通常用64bit来表示一个双精度浮点数(IEEE, 后面解释),而浮点数只能提供对实数的一个近似,因为任意相邻两个整数之间都有无穷多个实数。显然用64bit,只能表示最多2^64个数字。因此,浮点数有所谓的精度问题,它只能精确表示有效位数的数值,超过有效位数就存在丢失精度的问题。在此,我们可以猜测,上述问题就是因为123456789111111111这个数已经超出了双精度浮点数能表示的最大有效精度位数,在传递给NumberLong进行转换之前就已经丢失了精度,因此最终查出的结果和插入的值不一致。

要理解这个问题,我们需要简单解释下浮点数格式。在浮点数出现前,一种表示实数的格式称为定点格式,即使用固定的位数表示整数,固定的位数表示小数。比如16位的无符号定点格式,我们使用8位作为小数部分,8位作为整数部分。整数部分可以表示的范围是0 ~255,而小数部可以表示的范围是2^-8 到 1之间的小数。定点格式虽然表示简单,但存在一个问题就是不能有效使用所有bit位。比如我们要表示0.2,只需要1位来表示小数值,其余7位实际上就浪费了,这浪费的7位如果用来表示整数部分,显然就可以提高整数部分的表示范围。

为了解决上述问题,出现了浮点数格式。我们现在所用的IEEE浮点数格式,分为单精度(就是我们熟知的float,32位表示),双精度(double,64位),扩展精度(我们一般很少用到,80位)。为了表示实数,浮点数格式使用一些位来表示尾数(尾数位决定了表示精度),一部分位来表示阶码(也称作指数,指数位决定了浮点数能表达的绝对值最小和最大值的范围)。

这里我们主要讲双精度格式(double),实际上,double的64位是如下进行划分的:

1bit(符号位) 11bits(指数位) 52bits(尾数位)

因为我们猜测最要是double类型丢失精度的原因,尾数决定精度,所以我们关注尾数部分,2^52 = 4503599627370496, 2^53 – 1 = 9007199254740991。一共16位,因此double的精度为15~16位(因为最多只能精确表示到9007199254740991,16位还剩下一些数字会因为超出尾数表示范围而可能丢失精度)。

3. 问题验证

前面我们说到超过尾数部分能表示的最大值后就可能会出现精度丢失,下面我们进行一些简单的验证:

183210109

我们发现从9007199254740993开始就出现精度丢失问题了。为什么不是9007199254740992?注意,我们说的是可能会出现丢失精度,并不是说9007199254740991之后所有数都不能精确表示。因为结合指数部分,这之后的有些数字还是可以表示的,比如9007199254740992 = 4503599627370496 ^ 2。而9007199254740993显然不能通过尾数结合指数表示,所以发生精度丢失,只能是接近的近似值。

4. 解决方法

针对我们这个问题,解决方法很简单,而且也是NumberLong的正确用法,即以整数字符串作为参数,而不是默认为双精度浮点数的数字字面量。

183840125

5. 扩展

在其他地方,比如Java等语言,或者mysql等数据库中,为了避免浮点数丢失精度的问题,我们应该考虑使用BigDecimal或者将小数转为整数的方式,来避免精度丢失的问题。

/* 本文属于原创文章,转载请注明作者和出处 quarterback.cn,请勿用于任何商业用途 */



喜欢 (0)or分享 (0)
发表我的评论
取消评论
表情

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址