由闲鱼的转账BUG引起的思考

想写这一篇文章主要是前两天看到了一个闲鱼的BUG,而且自己也复现了这个问题。在使用闲鱼的时候当我向好友转账2.1的时候,最终支付宝显示的却只有2.09(前天出现的问题,目前闲鱼已经修复了这个问题)。作为一个消费者这只是一个BUG,但是作为一个JAVA开发,就让我思考到假如这个金额的数据是需要服务端进行处理?JAVA要如何处理这些数据。

在这里插入图片描述

我的一分钱呢??????在这里插入图片描述

在这里插入图片描述


这从未设想的问题啊,所以这里就整理下对于服务端 的开发,对于金额的处理、储存和传输应该如何操作

JAVA对金额数据的处理

错误的付款金额

在设计商品的数据结构时候,我们可能尝试将商品的价格设置为float或者double类型,而购买数量因为产品不同可能被设计为int或者long。当需要我们计算总价的时候,如果我们直接将单价*购买数量就会出现下面的情况:

    public static void main(String[] args) {
        float a = 72.49f;
        System.out.println("商品a单价:" + a);
        int n = 10;
        System.out.println("购买a数量:" + n);
        System.out.println("商品a总价:" + a*n);

        double d1 = 0.58D;
        long n1 = 100L;
        System.out.println("商品b单价:" + d1);
        System.out.println("购买b数量:" + n1);
        System.out.println("商品b总价:" + d1*n1);
    }

上面代码看起来就是很简单的乘法运算,但是投入到生产中会出现很大的问题,它会得到下面的结果

商品a单价:72.49
购买a数量:10
商品a总价:724.89996
商品b单价:0.58
购买b数量:100
商品b总价:57.99999999999999

可以看到本来应该支付724.9元的订单只需要支付724.89。而本来需要58块的订单缺只需要57.99。无论是float还是double都出现了金额缺失的情况。

使用BigDecimal进行金额计算

因为float和double存在精度丢失问题所以在进行数字的精确计算的时候,我们需要通过BigDecmal来进行精确计算。

将数字转换为BigDecimal

BigDecimal提供了相当多的构造方法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NeOuTEcX-1593868618419)(BCC0E85C61C04C38A0C7D2A4C3AD8F5F)]

上面方法虽然多但是我们常用的构造方法就是下面几种,通过下面的方法来将String、int和long类型的数据转换为BigDecimal

    // BigDecimal(int) 创建一个具有参数所指定整数值的对象
    BigDecimal num1 = new BigDecimal(10);
    // BigDecimal(long) 创建一个具有参数所指定长整数值的对象。
    BigDecimal num2 = new BigDecimal(1000000L);
    //BigDecimal(String) 创建一个具有参数所指定以字符串表示的数值的对象
    BigDecimal num12 = new BigDecimal("0.005");

对于double和float的特殊处理

如果我们将double或者float数据使用上面方式获取BigDecimal则会得到下面这种错误结果

// 输出:72.48999786376953125
        float a = 72.49f;
        BigDecimal num3 = new BigDecimal(a);
        System.out.println(num3);
// 输出:0.57999999999999996003197111349436454474925994873046875
        double d1 = 0.58D;
        BigDecimal num4 = new BigDecimal(d1);
        System.out.println(num4);

有些文章中介绍可以使用其静态方法BigDecimal.valueOf(d1),但是此方法面对float的数据类型依旧无法准确输出内容。所以对于float我们最好将其转换为String后进行处理

        float a = 72.49f;
        String s = String.valueOf(a);
        System.out.println(s);
对BigDecimal 数据进行操作

BigDecimal提供了一系列的方法让我们更加精确的对数据进行处理

方法 作用 例子 解释
add 加法 num1.add(num1和num2相加) num1和num2相加
subtract 减法 num1.subtract(num2) num1减去num2
multiply 乘法 num1.multiply(num2) num1乘 num2
divide 除法 num2.divide(num1,2,BigDecimal.ROUND_HALF_UP) num2 除以 num1,并且保留两位小数
divideToIntegralValue 除法并获取其整数部分 num2.divideToIntegralValue(num1) num2 除以 num1,并获取其整数部分
compareTo 比较大小 num1.compareTo(num2) num1和num2比大小,如果num1小于num2则返回-1,相等则返回0,大于则返回1
abs 绝对值 num3.abs() 返回num3的绝对值

特别需要注意!进行相关操作后并不会作用到原始数据上

在BigDecimal数据进行上面操作后并不会影响其原始数据的值,下面的操作中最终会存在三个不一样的值,原始的数据bigDecimal1bigDecimal2计算后的结果add

        BigDecimal add = bigDecimal1.add(bigDecimal2);
        System.out.println(add);
        System.out.println(bigDecimal1);
        System.out.println(bigDecimal2);

除法四舍五入操作

除法操作时调用的方法

public BigDecimal divide(BigDecimal divisor, int scale, int roundingMode);
public BigDecimal divide(BigDecimal divisor, int scale, RoundingMode roundingMode);

其最后一个参数用来确定小数的舍入的策略。

数字参数 枚举参数 作用
BigDecimal.ROUND_UP RoundingMode.UP 被舍弃的小数位如果不是0,则舍弃部分前面的数字+1
BigDecimal.ROUND_DOWN RoundingMode.DOWN 不会对舍弃部分前面的数字+1
BigDecimal.ROUND_CEILING RoundingMode.CEILING 如果结果是正数则使用RoundingMode.UP规则;如果结果是负数则使用RoundingMode.DOWN规则
BigDecimal.ROUND_FLOOR RoundingMode.FLOOR 使用和RoundingMode.CEILING相反的策略
BigDecimal.ROUND_HALF_UP RoundingMode.HALF_UP 可以理解为四舍五入
BigDecimal.ROUND_HALF_DOWN RoundingMode.HALF_DOWN 舍弃部分 > 0.5,则舍入行为同 RoundingMode.UP;否则舍入行为同RoundingMode.DOWN
BigDecimal.ROUND_HALF_EVEN RoundingMode.HALF_EVEN 如果距离相邻的数字相等,则向相邻的偶数舍入,如果不相等,则如果舍弃部分左边的数字为奇数,则舍入行为同RoundingMode.HALF_UP;如果为偶数,则舍入行为同RoundingMode.HALF_DOWN
BigDecimal.ROUND_UNNECESSARY RoundingMode.UNNECESSARY 判断是否精确操作,如果需要进行舍入操作则抛出异常

金额类数据如何保存(MySQL)

在金额类数据处理完后,我们需要保存到数据库中,而对于这些交易数据,根据每个系统涉及交易的规模和业务不同,目前有三种选择(实际上只写了两种,网上有人介绍使用String或者说varchar,说实话我是不喜欢将金额存储为字符串)。

decimal

使用decimal在数据库中可以非常精确的表示一个数据的值,而一般保存交易金额我们可以将其类型设置为decimal(M,S),M表示整数和小数部分的总长度,S表示其中小数部分的位数,对于日常交易过程中我们所使用的的最小单位是分,也就是0.01元,所以可以设置为2;

long

有些设计中,将数据库中金额的单位认为是。对于这种设计对于金额的数据类型可以设置为long,此时对于值为100的数据,会被认定为1元,而不是100元。

金额类数据如何传输

关于在通过跨服务跨系统进行金额数据传输的时候,数据类型如何确定,可以直接参照支付宝SDK上的要求使用String数据类型


个人水平有限,上面的内容可能存在没有描述清楚或者错误的地方,假如开发同学发现了,请及时告知,我会第一时间修改相关内容。假如我的这篇内容对你有任何帮助的话,麻烦给我点一个赞。你的点赞就是我前进的动力。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐