欢迎光临
我们一直在努力

金额存储: 为什么不能用int,不能用float,必须用decimal?

🌱 问题的本质

我们需要在二进制计算机中精确表示十进制金额。

这是一个根本性的矛盾:

  • 业务需求:金额是十进制的(0.01元、0.10元)
  • 计算机底层:所有数据都是二进制的(0、1)
  • IEEE 754标准:浮点数用二进制表示,无法精确表示大部分十进制小数

# 这不是bug,这是数学必然
0.1 + 0.2 # 结果:0.30000000000000004

# 为什么?因为0.1的二进制表示是无限循环的:
# 0.1 (十进制) = 0.0001100110011001100… (二进制,无限循环)

类比:就像试图用分数1/3精确表示0.333…一样,这是逻辑上不可能的。


⚖️ 三种方案的Trade-off

方案1: FLOAT/DOUBLE(浮点数)

amount FLOAT — 存储1.99元

维度评价
✅ 性能 极快(CPU硬件支持)
✅ 存储 4字节(float)/8字节(double)
❌ 精度 误差累积,100万次0.01累加误差0.0000002元
❌ 比较 0.1 + 0.2 == 0.3 返回false
❌ 合规 不符合金融监管要求

致命场景:

# 电商对账
orders = [0.01] * 1000000 # 100万笔0.01元订单
total = sum(orders) # 10000.000000198元
bank_total = 10000.00 # 银行到账
# 对账失败!差异0.000000198元

结论:❌ 永远不要用于金额存储


方案2: INT(整数,以"分"为单位)

amount_cents INT — 存储199表示1.99元

维度评价
✅ 性能 最快(整数运算)
✅ 精度 绝对精确(无舍入误差)
✅ 存储 4字节(最紧凑)
✅ 索引 最快
❌ 语义 需手动管理单位(分↔元)
⚠️ 精度限制 只能到"分",无法表示0.0597元

Trade-off分析:

适用场景:
✅ 国内电商(只需要"分"精度)
✅ 高频交易(性能优先)
✅ 区块链(以太坊用wei,比特币用satoshi)

不适用场景:
❌ 费率计算(1.99 × 3% = 0.0597元 = 5.97分,不是整数!)
❌ 汇率换算(199日元 = 8.77元,需要更高精度)
❌ 加密货币(BTC精确到8位小数)

微信支付为什么用int?

  • 历史原因:早期系统设计
  • 性能考虑:每秒数百万笔交易
  • 场景限制:只处理人民币"分"精度
  • 强制规范:让开发者显式处理单位转换

  • 方案3: DECIMAL(定点数)

    amount DECIMAL(19,4) — 存储1.9900元,精确到0.0001元(厘)

    维度评价
    ✅ 精度 完全精确(十进制表示)
    ✅ 语义 清晰(一眼看出是"元")
    ✅ 灵活 支持任意精度(DECIMAL(19,8))
    ❌ 性能 比int慢3-10倍(无CPU硬件加速)
    ❌ 存储 5-9字节(比int大)

    Trade-off分析:

    性能代价:
    – INSERT:比int慢40%
    – SUM聚合:比int慢189%
    – 索引查询:比int慢102%

    但在大多数场景下,这个代价是可以接受的(毫秒级差异)

    DECIMAL的精度选择:

    业务场景推荐精度理由
    国内电商 DECIMAL(19,2) 精确到分,最大支持999999999999999.99元
    支付平台 DECIMAL(19,4) 支持费率计算(0.0597元)
    跨境支付 DECIMAL(19,8) 支持汇率换算(8位小数)
    加密货币 DECIMAL(30,18) BTC精确到8位,ETH精确到18位

    🚨 常见反模式

    反模式1: DECIMAL存"分"

    — ❌ 错误:用DECIMAL存"分"
    amount DECIMAL(10,2) — 存3505.00表示35.05元(单位:分)

    问题:
    1. 语义混乱:到底是3505.00元还是35.05元?
    2. 性能浪费:小数部分(.00)完全无用
    3. 两头不靠:既没有int的性能,也浪费了decimal的小数能力

    正确做法:
    ✅ 要么:amount_cents INT3505(性能优先)
    ✅ 要么:amount DECIMAL(10,2)35.05(语义优先)

    为什么会出现这种设计? 典型的"货物崇拜编程":只学了微信支付"存分"的形式,没理解"用int"的本质。


    反模式2: 高精度场景用INT

    # 场景:1.99元商品收取3%手续费
    fee = 1.99 * 0.03 # 0.0597元

    # ❌ 用int存储
    fee_cents = 6 # 四舍五入为6分,损失0.03分
    # 100万笔交易累积误差:0.0003元 × 1000000 = 300元!

    # ✅ 正确方案
    amount DECIMAL(19,4) 0.0597元(精确)

    关键理解:

    • 不是"分"不够用
    • 而是需要更高精度的"元"
    • DECIMAL(19,4)能精确表示0.0597元
    • 不要用DECIMAL存"5.97分"(语义混乱)

    🌍 业界实践与决策逻辑

    Stripe的演进

    2011-2015(v1 API):
    {
    "amount": 1999 // 只返回整数cents
    }

    2016-至今(v2 API):
    {
    "amount": 1999, // 向后兼容
    "amount_decimal": "19.99" // 新增高精度字段
    }

    Stripe的Trade-off:

    • 保持向后兼容(老客户端用amount)
    • 提供高精度支持(新客户端用amount_decimal)
    • 双字段策略:让开发者自己选择

    区块链的极致方案

    // 以太坊智能合约
    uint256 balance = 1 ether; // 实际存储:1000000000000000000 wei

    为什么必须用大整数?

  • 共识一致性:不同节点的浮点实现可能不同
  • 不可篡改:整数运算无舍入误差
  • Gas优化:整数运算比浮点便宜数百倍

  • 🎯 决策树

    你的项目类型?

    ├─ 高并发支付系统(QPS > 5000)
    │ └─ ✅ 数据库:INT(分) + 业务层:Decimal
    │ 理由:性能差异在高并发下被放大

    ├─ 普通电商/SaaS(QPS < 1000)
    │ ├─ 只需要"分"精度?
    │ │ └─ ✅ DECIMAL(19,2) 或 INT(分)
    │ │
    │ └─ 涉及费率/汇率?
    │ └─ ✅ DECIMAL(19,4) 或 DECIMAL(19,8)

    ├─ 跨境支付/多币种
    │ └─ ✅ DECIMAL(19,8) + 币种配置表

    ├─ 加密货币交易所
    │ └─ ✅ DECIMAL(30,18) 或 BIGINT(最小单位)

    └─ 区块链/金融核心系统
    └─ ✅ BIGINT(最小单位) 不接受妥协


    🌐 API设计的Trade-off

    问题:数据库用DECIMAL(19,4),API该返回什么格式?

    方案优势劣势适用场景
    整数cents 性能最好 无法表示0.0597元 仅限"分"精度
    浮点数 直观 JavaScript精度丢失 ❌ 永远不推荐
    字符串 完全精确 慢14%(可忽略) ✅ 现代最佳实践

    JavaScript的陷阱

    // ❌ API返回浮点数
    {"amount": 0.1}
    // JavaScript解析:0.10000000000000001

    // ✅ API返回字符串
    {"amount": "0.10"}
    // 前端用Decimal.js解析,完全精确

    推荐:字符串 + 元数据

    {
    "amount": {
    "value": "1.99", // 主字段:字符串
    "currency": "CNY",
    "display": "¥1.99" // 前端直接显示
    },
    "fee": {
    "value": "0.0597", // 高精度字符串
    "display": "¥0.06" // 四舍五入显示
    }
    }

    全链路数据流:

    用户输入 → Decimal.js验证 → JSON字符串传输 → 后端Decimal解析
    → 数据库DECIMAL存储 → 后端Decimal读取 → JSON字符串返回
    → 前端Decimal.js解析 → 显示


    📋 核心原则总结

    一句话本质

    金额不是数值,是离散的计数单位。必须用十进制精确表示,否则累积误差会破坏会计平衡。

    10条军规

  • 永远不要用float/double存储金额
  • Decimal构造必须用字符串:Decimal("0.1") 而非 Decimal(0.1)
  • 数据库精度必须≥业务需求:费率计算用DECIMAL(19,4),汇率用DECIMAL(19,8)
  • 单位必须明确:要么存"元",要么存"分",不要存"分的小数"
  • 除法必须显式指定舍入模式:amount / 3 需要定义如何处理余数
  • API传输用字符串:前后端双向都用JSON字符串,不用数字类型
  • 前端用Decimal.js处理:JavaScript的number类型不能参与金额计算
  • int存储法适合高性能场景:但需在ORM层自动转换
  • 监管要求:人民币精确到分,误差≤0.01元,必须可审计
  • 单元测试必须覆盖边界值:0.01元、最大值、精度测试
  • Trade-off速查表

    需求INT方案DECIMAL方案
    性能 ★★★★★ 最快 ★★★☆☆ 慢3-10倍
    精度 ★★★★☆ 到"分" ★★★★★ 任意精度
    语义 ★★★☆☆ 需转换 ★★★★★ 清晰
    存储 ★★★★★ 4字节 ★★★☆☆ 5-9字节
    扩展性 ★★☆☆☆ 固定精度 ★★★★★ 灵活
    复杂度 ★★★☆☆ 需封装 ★★★★☆ 开箱即用

    选择建议:

    • 高并发(QPS > 5000)→ INT + ORM封装
    • 普通场景(QPS < 1000)→ DECIMAL直接存"元"
    • 高精度需求(费率/汇率)→ DECIMAL(19,4~8)
    • 区块链/核心系统 → BIGINT(最小单位)

    📚 参考资料

    核心标准

    • IEEE 754-2019 Standard – 浮点数国际标准
    • ISO 4217 – 货币代码标准

    最佳实践

    • Stripe API – Amount Decimal
    • PayPal API – Amount Object
    • Martin Fowler: Money Pattern

    延伸阅读

    • What Every Computer Scientist Should Know About Floating-Point
    • The Floating-Point Guide
    赞(0)
    未经允许不得转载:171主机测评 » 金额存储: 为什么不能用int,不能用float,必须用decimal?
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址