浮点计算中的陷阱

最近在公司的支付业务中添加了新的功能,突然发现支付的时候概率性出错,当输入小数时,常常会出现支付失败,因此一步一步地查看代码,发现最后的金额不对,出现了0.00000005这种情况,通常这种情况都是由于浮点计算精度丢失所造成的。

其实之前做业务查看数据库的时候已经发现有些苗头不对,在数据库中保存的金额的数据居然是保留两位小数的浮点数,数据类型是DECIMAL。建数据结构表的人应该是意识到浮点计算会精度丢失所以采用DECIMAL,但仅仅是有这个意识,却不知道什么场景下会精度丢失,DECIMAL结构是保存任意精度的数值,例如100.0000000000000000000000000000000001这种,因为在mysql内部DECIMAL类型存的是字符串,所以可以保存任意精度,但却不能保证在计算的时候保证精度。

例如20-19.99应该是等于0.01,但是输入PHP得到的是

> php -r 'echo 20-19.99;'
> 0.010000000000002

任何语言都有这样的问题。换成Javascript就是:0.010000000000001563。

有一些精度丢失是可以容忍的,例如买基金收取1.5%的手续费,用户支付了1000元,那么买基金的部分就是:1000/(1+0.015),四舍五入之后就是985.22,因为用户也知道它是一个除不尽的数。但是有一些却不能容忍,例如20-19.99,那么它应该就是精确的1分钱,多1分少1分都是不应该的。

那么如何来避免这种情况,涉及到金额的话,可以将单位保存为分,即数值乘以100,因为人民币最小的单位就是分,但是按照数学误差来说最好保留到厘,即乘以1000,因为最后一位是误差位,是不准确的,所以分是精确的。

如果设计到的不是金额,而是其他的统计数据啥的,需要高精度的计算,可以使用BCMath,或者GMP。BCMath扩展基本都会装,而且满足大部分的需求。而GMP提供了更多的函数。