计算机浮点数处理带来的问题
业务场景:
- 用户用 10 元 购买三张券,每一个张券可以直接抵一款产品,平均下来一张券的价值为:
10 * 3 = 3.333333333...
- 假设用户购买产品 A 用了两张劵,A 的进价为 2 元。那么 A 的利润为
3.3333333... * 2 - 2 = 4.66666666666...
- 实际应用过程中,券不止可以抵同一款产品,所以服务器没有办法判断某一张券抵了那一款产品,必须要前端(App/Web) 手动填充产品的等价售价
这种无线循环的小数,客户端是没有办法填充的,只能精确到「分」,即 3.33
。这样在利润统计的过程中就会少了
0.00666666666...
,系统运行时间一长,就会导致财务对账对不上。
为了解决 10 * 3 = 3.333333333
这种高精度运算引发的订单无法结算的问题,决定手动进行截断,即:
10 * 3 = [3.3, 3.3, 3.4]
。但实际程序处理时发现 10 - 3.3 - 3.3 = 3.4000000000000004
而不是 3.4
。
本以为这是 Python 语言的问题,因为之前用 C++/Java/C#
这几类语言都没有遇到过这个问题。
经过一番研究后才想明白,这是计算机二进制存储本身的问题,和语言无关。二进制无法精确的存储浮点数,所以浮点数才有「浮点数精度」这样的概念。
比如 0.3
这样的数值,计算机实际存储的可能是 0.3
无限近似的值,近似的能力取决于计算机本身,
而取出来的值到底是多少取决于编程语言的浮点数精度 。因为 Python 的 float 精度高,所以 10 - 3.3 - 3.3 = 3.4000000000000004
。
C++ 的 float 精度是 6 位,自然而然 3.4000000000000004
被取值为 =3.4=,Java/C# 原因类似,所以我们看到"没问题"的假象。
这同时也解释了,为什么我们系统中很多没有除法运算的浮点数也有很长的尾序列。
既然如此,浮点数的带来的误差是没有办法解决的了,我们只能降低误差。我想的解决方案是:
- 对于后端: 所有与钱相关的,只保留两位有效数字,再后面的小数,计算机和语言爱怎么处理怎么处理,我们都不关心。还是按照我们现在的思路: 10 * 3 = [3.33, 3.33, 3.34],对所有实际处理的结果做「分」的四舍五入。
- 对于前端(App/Web): 同样保留两位有效数字(判等的时候也需要考虑),后端传给前端的数值假定为:
3.4000000000000004
,前端按照3.40
来处理就可以了。
0.01
这样的误差是不可接受的,累加成百上千次账目上就表现的很明显了。而 0.0000000000000001
这样的误差是可以接受的,这要累加到门店倒闭估计都不会出问题。