由闲鱼的转账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数据进行上面操作后并不会影响其原始数据的值,下面的操作中最终会存在三个不一样的值,原始的数据bigDecimal1
、bigDecimal2
计算后的结果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数据类型
个人水平有限,上面的内容可能存在没有描述清楚或者错误的地方,假如开发同学发现了,请及时告知,我会第一时间修改相关内容。假如我的这篇内容对你有任何帮助的话,麻烦给我点一个赞。你的点赞就是我前进的动力。