一次 BigDecimal 精度“回退”的隐蔽 Bug

反思笔记:一次 BigDecimal 精度“回退”的隐蔽 Bug
一、问题背景

在本地环境运行正常的退款金额判断逻辑,打包成 jar 部署到服务器后出现异常:

refundFee = 56.99

与配置金额 56.98 比较时,compareTo 结果却不成立

当配置改为 56.97 又成立

表面看是 环境差异,实际是 代码隐患被服务器放大。

二、问题根因定位

不是 BigDecimal 不准,而是被我“用错了”

核心错误点:

BigDecimal refundFeeBd = BigDecimal.valueOf(refundFee);

此时 refundFee 已经是 BigDecimal,但:

BigDecimal.valueOf() 只接受 long / double

JVM 实际执行路径是:

BigDecimal
→ refundFee.doubleValue()
→ double(产生二进制误差)
→ BigDecimal.valueOf(double)

👉 一次隐式的 BigDecimal → double → BigDecimal 精度回退

三、为什么这个 Bug 隐蔽且危险

代码语义看起来完全正确

编译不报错

日志不明显

本地可能“刚好没踩雷”

不同 JVM / CPU / JIT 行为差异

问题只在“金额临界点”出现

56.98 / 56.99 这种边界值最容易出问题

发生位置靠近业务判断

一旦出错,直接影响业务通知 / 风控 / 财务逻辑

四、连带发现的二次问题
new BigDecimal(String.format("%.2f", refundFee));

问题本质相同:

String.format 会调用 refundFee.doubleValue()

再次触发精度回退

五、正确认知修正

❌ 错误认知

“我已经在用 BigDecimal 了,应该是安全的”

✅ 正确认知

BigDecimal 只有在「全链路不用 double」时才是安全的

六、最终修正原则(已固化为个人规范)
1️⃣ 金额一旦进入 BigDecimal

❌ 禁止 doubleValue()

❌ 禁止 BigDecimal.valueOf(BigDecimal)

❌ 禁止 String.format

✅ 只允许:

add / subtract / multiply / divide

compareTo

setScale(RoundingMode)

2️⃣ 比较前统一 scale
refundFee = refundFee.setScale(2, RoundingMode.HALF_UP);

3️⃣ 展示与计算彻底分离

计算:BigDecimal

展示:最后一步再格式化

七、这次问题给我的提醒

金融/金额类 Bug 往往不是“逻辑复杂”,而是“细节失守”

真正危险的不是不会用 BigDecimal,
而是 “以为自己已经用对了”。

八、行动项(避免再次发生)

项目中 grep 排查 doubleValue / String.format / valueOf(

金额相关方法统一入参为 BigDecimal

代码评审中:金额逻辑单独检查

给自己:金额代码默认“怀疑自己”

posted @ 2026-01-15 15:34  冷风5997  阅读(1)  评论(0)    收藏  举报