前段时间,我们跟一家银行合作,开发了一款适合我们的支付系统。最近和解了,发现有些订单总是错位。银行的订单金额和对账金额总是少1分钱。这就奇怪了,这种异常的订单多了,会亏不少钱。在涉及金钱的金融世界里,这是一个非常严肃的问题。我们跟银行核对了一下,找到了问题的原因,也就是我今天要讲的技术性的东西:双精度的损失。1问题复现举个简单的例子doubleresult=1.0-0.9;这段代码的结果是什么?0.1?如果你执行代码,你会分分钟被打脸。doubleprecisionlossproblem2背后的原理,无论是本文中我们提到的double还是float,都是浮点数。在计算机科学中,浮点数(英文:floatingpoint,缩写为FP)是一个实数的近似值的数值表示,用有效数字(即尾数)加上一个幂表示,通常乘以某个基数获得的整数子指数。用这种表示法表示的值称为浮点数。计算机使用浮点运算的主要原因是计算机使用二进制运算。例如:4÷2=2、4=100(二进制)、2=010(二进制)。在二进制中除以2相当于后退一位。那么1.0÷2=0.5=0.1(二进制)就是1/2,以此类推。二进制0.01(二进制)是十进制1/(2^2)=1/4=0.25。上面看到的1、0.5、0.25都是可以转成二进制的小数,比如十进制的0.1,用二进制是不能准确表示的。因此,只能使用近似值。例如,让我们尝试将十进制的0.1转换为二进制,步骤如下:0.1*2=0.2...0-整数部分为“0”。整数部分“0”清零后为“0”,再用“0.2”计算。0.2*2=0.4...0——整数部分为“0”。整数部分“0”清零后,用“0.4”继续计算。0.4*2=0.8...0——整数部分为“0”。整数部分“0”清零后,用“0.8”继续计算。0.8*2=1.6...1——整数部分为“1”。整数部分“1”清“0”,再用“0.6”计算。0.6*2=1.2...1——整数部分为“1”。整数部分“1”清“0”,再用“0.2”计算。0.2*2=0.4...0——整数部分为“0”。整数部分“0”清零后,用“0.4”继续计算。0.4*2=0.8...0——整数部分为“0”。整数部分“0”清零后,用“0.8”继续计算。0.8*2=1.6...1——整数部分为“1”。整数部分“1”清“0”,再用“0.6”计算。0.6*2=1.2...1——整数部分为“1”。整数部分“1”清“0”,再用“0.2”计算。0.2*2=0.4...0——整数部分为“0”。整数部分“0”清零后,用“0.4”继续计算。0.4*2=0.8...0——整数部分为“0”。整数部分“0”清零后为“0”,再用“0.2”计算。0.8*2=1.6...1——整数部分为“1”。整数部分“1”清“0”,再用“0.2”计算。...可以发现,这个过程是无穷无尽的,除了一个***循环小数:二进制0.0001100110011...那么,这个绝对的不循环小数在计算机中怎么表示呢?只能根据不同的精度来考虑因式分解的位数不同。我们知道float是单精度,double是双精度。不同的精度意味着保留的有效位数不同。保留的位数越多,精度越高。所以浮点数在Java中是无法准确表示的,因为大部分浮点数转换成二进制都是非循环小数,只能通过保精度来近似。里面其实有明确的规定,就是decimal类型是禁止用float或者double来表示的。(虽然这是Mysql相关的规则,但是Java代码也是适用的。)3问题扩展现在我们基本知道双精度的问题是什么了。在实际的订单交易过程中,出现该问题的场景较多的是金额单位元转分。银行给你的单位是元,你自己算的是分;前端输入为元,计算为分等。例如:用户下单64.6元,你需要换算成份。如果直接除以100,你会发现计算出来的分数总是6459,少了1分钱。金额丢失1分问题4解决方案1)使用BigDecimal为了解决这个浮点小数进度丢失的问题,java提供了一种计算方法BigDecimal。这对BigDecimal来说足够了吗?不,这可以解决大部分问题,如果其他系统或语言不支持BigDecimal。当我们无法解决这个问题的时候,我们需要做的就是想办法避免这个问题的影响。2)在points中,使用Long作为数据结构进行存储。目前,我们的一些核心系统在金额传输的过程和存储过程中,仍然以元为单位存储浮点数。导致在计算10元以下订单的利率时,无法计算清楚,让我们的业务服务在处理这些问题时很头疼。整数和整数的计算不存在这些精度损失问题。Long(9223372036854775807)的取值范围是完全够用的。3)如果有无穷无尽的分裂怎么办?分裂总是出现不可分割的情况怎么办?一个叫netting的词是什么意思?举个简单的例子。如果你现在需要把10元分成3分,如果你把10除以3,你会发现它是3.33333无穷大的3。这些数字根本不可能精确地存储在程序或数据库中。简单的理解就是当整除不完或者需要去掉小数点时,将前面的n-1笔(这里n=3)向上取整。***一笔为底线(总金额减去前n-1笔交易的总和)。这样可以保证总量不会丢失。我们这里具体的应用场景是用户使用代金券,然后有部分退款,还有计算可退本金的问题。代金券的处理又是一篇大文章了,这里稍后再介绍。5总结Long可以在没有浮点存储的情况下使用。前后端之间传递金额(元)时,请使用字符串,不要使用浮点数。请使用BigDecimal进行浮点运算。如果实在分不出来,可以考虑用网的方式解决。双精度不是坑,而是一个容易被忽略的巨大坑。希望这个小经验对大家有所帮助,慎重对待。>-<关于银行的1分差,等银行修好,调整历史订单,哈哈哈哈。
