⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 juejin.cn/post/7399985723673837577 「五阳」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注微信公众号:【芋道源码】有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言都将得到认真回复。甚至不知道如何读源码也可以请教噢。
  4. 新的源码解析文章实时收到通知。每周更新一篇左右。
  5. 认真的源码交流微信群。

前些日子,测试提过来一个 bug,说下单价格应该是 2.01,但是在订单详情中展示了 2.00 元。我头嗡的一下子,艹,不会是因为 double 的精度问题吧~

果不其然,经过排查代码,最终定位原因订单详情展示金额时,使用 double 进行了金额转换,导致金额不准。

我马上排查核心购买和售后链路,发现涉及资金交易的地方没有问题,只有这一处问题,要不然这一口大锅非得扣我身上。

为什么 2.01 变成了 2.0

2.01 等小数 在计算机中按照2进制补码存储时,存在除不尽,精度丢失的问题。例如 2.01 的补码为 000000010.009999999999999787。正如十进制场景存在 1/3 等无限小数问题,二进制场景也存在无限小数,所以一定会存在精度问题。

什么场景小数转换存在问题

for (int money = 0; money < 10000; money++) {
String valueYuan = String.format("%.2f", money * 1.0 / 100);

int value = (int) (Double.valueOf(valueYuan) * 100);
if (value != money) {
System.out.println(String.format("原值: %s, 现值:%s", money, value));
}
}

如上代码中,先将数字 除以 100,转为元, 精度为2位,然后将double 乘以100,转为int。在除以、乘以两个操作后,精度出现丢失。

我把1-10000 的范围测试一遍,共有573个数字出现精度转换错误。这个概率已经相当大了。

如何转换金额更安全?

Java 提供了 BigDecimcal 专门处理精度更高的浮点数。简单封装一下代码,元使用 String 表示,分使用 int 表示。提供两个方法实现 元和分的 互转。

public static String change2Yuan(int money) {
BigDecimal base = BigDecimal.valueOf(money);
BigDecimal yuanBase = base.divide(new BigDecimal(100));
return yuanBase.setScale(2, BigDecimal.ROUND_HALF_UP).toString();
}

public static int change2Fen(String money) {
BigDecimal base = new BigDecimal(money);

BigDecimal fenBase = base.multiply(new BigDecimal(100));
return fenBase.setScale(0, BigDecimal.ROUND_HALF_UP).intValue();
}

测试

测试 0-1 亿 的金额转换逻辑,均成功转换,不存在精度丢失。

int error = 0;
long time = System.currentTimeMillis();
for (int money = 0; money < 100000000; money++) {
String valueYuan = change2Yuan(money);

int value = change2Fen(valueYuan);
if (value != money) {
error++;
}
}
System.out.println(String.format("时间:%s", (System.currentTimeMillis() - time)));
System.out.println(error);

性能测试

网上很多人说使用 BigDecimcal 存在性能影响,但是我测试性能还是不错的。可能首次耗时略高,大约2ms

总结

涉及金额转换的 地方,一定要小心处理,防止出现精度丢失问题。可以使用代码审查工具,查看代码中是否存在使用 double 进行金额转换的代码, 同时提供金额转换工具类。

文章目录
  1. 1. 为什么 2.01 变成了 2.0
  2. 2. 什么场景小数转换存在问题
  3. 3. 如何转换金额更安全?
  4. 4. 测试
  5. 5. 性能测试
    1. 5.0.1. 总结