一次 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
代码评审中:金额逻辑单独检查
给自己:金额代码默认“怀疑自己”

浙公网安备 33010602011771号