BigDecimal使用不当,造成P0事故!

Posted Hollis Chuang

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了BigDecimal使用不当,造成P0事故!相关的知识,希望对你有一定的参考价值。

Hollis的新书限时折扣中,一本深入讲解Java基础的干货笔记!

来源:juejin.cn/post/7087404273503305736

# 背景

我们在使用金额计算或者展示金额的时候经常会使用BigDecimal,也是涉及金额时非常推荐的一个类型。

BigDecimal自身也提供了很多构造器方法,这些构造器方法使用不当可能会造成不必要的麻烦甚至是金额损失,从而引起事故资损。

接下来我们看下收银台出的一起事故。

【问题描述】

收银台计算商品金额报错,导致订单无法支付。

【事故级别】

P0

【过程】

  • 13:44 接到报警,订单支付失败,支付可用率降至60%

  • 13:50 迅速回滚上线代码,恢复正常;

  • 14:20 review代码,预发布验证发现问题点

  • 14:58 修改问题代码上线,线上恢复

【故障原因】 BigDecimal在金额计算中丢失精度

# 原因分析

首先我们先用一段代码复现问题根源,如下所示:

public static void main(String[] args) 
    BigDecimal bigDecimal=new BigDecimal(88);
    System.out.println(bigDecimal);
    bigDecimal=new BigDecimal("8.8");
    System.out.println(bigDecimal);
    bigDecimal=new BigDecimal(8.8);
    System.out.println(bigDecimal);

执行结果如下:

通过测试发现,当使用double或者float这些浮点数据类型时,会丢失精度,String、int则不会;这是为什么呢?

我们点开构造器方法看下源码:

public static long doubleToLongBits(double value) 
    long result = doubleToRawLongBits(value);
    // Check for NaN based on values of bit fields, maximum
    // exponent and nonzero significand.
    if ( ((result & DoubleConsts.EXP_BIT_MASK) ==
          DoubleConsts.EXP_BIT_MASK) &&
         (result & DoubleConsts.SIGNIF_BIT_MASK) != 0L)
        result = 0x7ff8000000000000L;
    return result;

问题就处在 doubleToRawLongBits 这个方法上,在jdk中double类(float与int对应)中提供了double与long转换,doubleToRawLongBits就是将double转换为long,这个方法是原始方法(底层不是java实现,是c++实现的)。

double之所以会出问题,是因为小数点转二进制丢失精度。BigDecimal在处理的时候把十进制小数扩大N倍让它在整数上进行计算,并保留相应的精度信息

  1. float和double类型,主要是为了科学计算和工程计算而设计的,之所以执行二进制浮点运算,是为了在广泛的数值范围上提供较为精确的快速近和计算。

  2. 并没有提供完全精确的结果,所以不应该被用于精确的结果的场合。

  3. 当浮点数达到一定大的数,就会自动使用科学计数法,这样的表示只是近似真实数而不等于真实数。

  4. 当十进制小数位转换二进制的时候也会出现无限循环或者超过浮点数尾数的长度。

# 总结

所以,在涉及到精度计算的过程中,我们尽量使用String类型来进行转换,正确用法如下:

BigDecimal bigDecimal2=new BigDecimal("8.8");
BigDecimal bigDecimal3=new BigDecimal("8.812");
System.out.println( bigDecimal2.compareTo(bigDecimal3));
System.out.println( bigDecimal2.add(bigDecimal3));

BigDecimal创建出来的是对象,我们不能用传统的加减乘除对其进行运算,必须使用他的方法,在我们数据库存储里,如果我们使用的是double或者float类型,需要进行来回的转换后进行计算,非常不方便。

# 工具分享

所以在这里整理出一个util类供大家使用。

import java.math.BigDecimal;


/**
 * @Author shuaige
 * @Date 2022/4/17
 * @Version 1.0
 **/
public class BigDecimalUtils 
    public static BigDecimal doubleAdd(double v1, double v2) 
        BigDecimal b1 = new BigDecimal(Double.toString(v1));
        BigDecimal b2 = new BigDecimal(Double.toString(v2));
        return b1.add(b2);
    
    public static BigDecimal floatAdd(float v1, float v2) 
        BigDecimal b1 = new BigDecimal(Float.toString(v1));
        BigDecimal b2 = new BigDecimal(Float.toString(v2));
        return b1.add(b2);
    
    public static BigDecimal doubleSub(double v1, double v2) 
        BigDecimal b1 = new BigDecimal(Double.toString(v1));
        BigDecimal b2 = new BigDecimal(Double.toString(v2));
        return b1.subtract(b2);
    
    public static BigDecimal floatSub(float v1, float v2) 
        BigDecimal b1 = new BigDecimal(Float.toString(v1));
        BigDecimal b2 = new BigDecimal(Float.toString(v2));
        return b1.subtract(b2);
    




    public static BigDecimal doubleMul(double v1, double v2) 
        BigDecimal b1 = new BigDecimal(Double.toString(v1));
        BigDecimal b2 = new BigDecimal(Double.toString(v2));
        return b1.multiply(b2);
    
    public static BigDecimal floatMul(float v1, float v2) 
        BigDecimal b1 = new BigDecimal(Float.toString(v1));
        BigDecimal b2 = new BigDecimal(Float.toString(v2));
        return b1.multiply(b2);
    




    public static BigDecimal doubleDiv(double v1, double v2) 
        BigDecimal b1 = new BigDecimal(Double.toString(v1));
        BigDecimal b2 = new BigDecimal(Double.toString(v2));
        // 保留小数点后两位 ROUND_HALF_UP = 四舍五入
        return b1.divide(b2, 2, BigDecimal.ROUND_HALF_UP);
    
    public static BigDecimal floatDiv(float v1, float v2) 
        BigDecimal b1 = new BigDecimal(Float.toString(v1));
        BigDecimal b2 = new BigDecimal(Float.toString(v2));
        // 保留小数点后两位 ROUND_HALF_UP = 四舍五入
        return b1.divide(b2, 2, BigDecimal.ROUND_HALF_UP);
    
    /**
     * 比较v1 v2大小
     * @param v1
     * @param v2
     * @return v1>v2 return 1  v1=v2 return 0 v1<v2 return -1
     */
    public static int doubleCompareTo(double v1, double v2) 
        BigDecimal b1 = new BigDecimal(Double.toString(v1));
        BigDecimal b2 = new BigDecimal(Double.toString(v2));
        return  b1.compareTo(b2);
    
    public static int floatCompareTo(float v1, float v2) 
        BigDecimal b1 = new BigDecimal(Float.toString(v1));
        BigDecimal b2 = new BigDecimal(Float.toString(v2));
        return  b1.compareTo(b2);
    

我的新书《深入理解Java核心技术》已经上市了,上市后一直蝉联京东畅销榜中,目前正在6折优惠中,想要入手的朋友千万不要错过哦~长按二维码即可购买~

长按扫码享受6折优惠

往期推荐

一文搞明白分布式事务解决方案!真的 so easy!


祖传代码如何优化性能?


搜狐员工遭遇“工资补助”诈骗?网友都蚌埠住了


有道无术,术可成;有术无道,止于术

欢迎大家关注Java之道公众号

好文章,我在看❤️

以上是关于BigDecimal使用不当,造成P0事故!的主要内容,如果未能解决你的问题,请参考以下文章

BigDecimal使用不当,造成P0事故!

BigDecimal使用不当,造成P0事故!

BigDecimal使用不当,造成P0事故!

P0级重大事故:Redis 分布式锁使用不当,超卖了100瓶飞天茅台!

记一次分布式锁使用不当引发生产事故.....

Redis 锁使用不当,超卖了100瓶茅台!某程序员...