13:44 收银台崩了,所有人都在查 SQL:根因竟然是 BigDecimal
- URL: https://mp.weixin.qq.com/s/W-OOf7OQ_2b4g1bYVpG7HQ
- Date Saved: 2026-08-14
- Source: WeChat (芋道源码)
- Tags: dev-tools
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 条红线
- 永远用
BigDecimal.valueOf(double),不用new BigDecimal(double) - 除法必须指定精度和舍入模式(
RoundingMode.HALF_UP) - 比较用
compareTo,不用equals(标度不同会 false) - BigDecimal 不可变,运算结果必须接住(
a = a.add(b)) - 金额尽量用分单位 long 存储,展示时再转
构造器选择
- String ✅ 首选
- int/long ✅ 整数场景
- double/float ❌ 禁止
- BigDecimal.valueOf(double) ✅ 内部走 Double.toString 绕开二进制
4 个常见生产坑
- Jackson 序列化偷偷把 BigDecimal 转 Double(开启 USE_BIG_DECIMAL_FOR_FLOATS)
- equals 比较金额(0.00 vs 0 标度不同返回 false)
- 除法漏写精度抛 ArithmeticException
- MySQL DECIMAL(10,2) 精度不够(汇率/税率建议 DECIMAL(19,6) 起步)
核心原则
让 double 在金额传输链上根本不存在 — 从前端/DB/JSON 到后端全程 BigDecimal 或 String。