WeChat (芋道源码)
dev-tools
Original source

13:44 收银台崩了,所有人都在查 SQL:根因竟然是 BigDecimal

Summary

一次 P0 事故复盘:支付可用率从 99% 掉到 60%,根因是 new BigDecimal(8.8) 导致金额精度丢失。

事故时间线

  • 13:44 告警,支付失败
  • 13:50 回滚,可用率回升
  • 14:20 定位到 new BigDecimal(8.8)
  • 14:58 修复上线,共 74 分钟

根因

new BigDecimal(double) 拿到的是 IEEE 754 截断后的二进制值,8.8 变成 8.800000000000000710…。金额计算中尾巴被放大累加,导致支付校验失败。

5 条红线

  1. 永远用 BigDecimal.valueOf(double),不用 new BigDecimal(double)
  2. 除法必须指定精度和舍入模式(RoundingMode.HALF_UP)
  3. 比较用 compareTo,不用 equals(标度不同会 false)
  4. BigDecimal 不可变,运算结果必须接住(a = a.add(b))
  5. 金额尽量用分单位 long 存储,展示时再转

构造器选择

  • String ✅ 首选
  • int/long ✅ 整数场景
  • double/float ❌ 禁止
  • BigDecimal.valueOf(double) ✅ 内部走 Double.toString 绕开二进制

4 个常见生产坑

  1. Jackson 序列化偷偷把 BigDecimal 转 Double(开启 USE_BIG_DECIMAL_FOR_FLOATS)
  2. equals 比较金额(0.00 vs 0 标度不同返回 false)
  3. 除法漏写精度抛 ArithmeticException
  4. MySQL DECIMAL(10,2) 精度不够(汇率/税率建议 DECIMAL(19,6) 起步)

核心原则

让 double 在金额传输链上根本不存在 — 从前端/DB/JSON 到后端全程 BigDecimal 或 String。