Mongoose v5.11.11数值增量精度异常问题咨询(Node.js v12.18.3环境)
Mongoose $inc 操作导致浮点数精度异常的原因与解决方法
这个问题我之前做金融类项目时也踩过坑,本质是二进制浮点数的精度限制在搞鬼,再加上Mongoose的setter逻辑和MongoDB原子操作的特性冲突,才出现了这种不符合预期的结果。咱们一步步拆解原因,再给可行的解决方案。
原因分析
JavaScript里的Number类型是基于IEEE 754标准的双精度浮点数,这种存储方式无法精确表示所有十进制小数——比如0.29和8.01在二进制里都是无限循环的近似值。
而你的代码里存在两个关键问题:
- Schema中的
set方法只在初始赋值、直接修改字段值时生效,$inc是MongoDB的原子更新操作,会直接绕过Mongoose的setter逻辑,直接对数据库里存储的二进制浮点数做运算; - 两个不精确的浮点数相加后,近似值的误差被放大,最终就出现了
8.299999999999999这种看起来“奇怪”的结果。
解决方法
这里有三个靠谱的方案,你可以根据业务场景选择:
1. 改用整数存储(最推荐)
这是金融类系统的通用做法:把金额类数值放大100倍,以“分”为单位存储整数,彻底规避浮点数精度问题。修改代码如下:
// 更新后的Schema const Schema = new Schema({ total: { type: Number, default: 801, // 8.01元 → 801分 } }) // 控制器函数:传入的addValue需转成“分”单位 let myController = (id, addValue) => { const addInCents = Math.round(addValue * 100); // 0.29元 → 29分 return model.findByIdAndUpdate(id, { $inc: { total: addInCents } }) }
读取数据时再除以100转成“元”即可,完全不会有精度丢失的问题。
2. 绕开原子操作,触发Mongoose的setter逻辑
如果不想修改存储结构,可以先查询文档,再通过修改字段值触发setter的四舍五入,最后保存:
let myController = async (id, addValue) => { const doc = await model.findById(id); doc.total += addValue; // 这里会触发set方法的Math.round(v*100)/100 return doc.save(); }
⚠️ 注意:这种方式不是原子操作,如果有并发更新的场景,可能会出现数据覆盖问题,需要结合事务或者版本锁(比如versionKey)来处理。
3. 使用MongoDB的Decimal128类型
Mongoose支持MongoDB原生的Decimal128类型,它能精确表示十进制小数。修改代码如下:
const { Decimal128 } = require('mongoose').Types; // 更新后的Schema const Schema = new Schema({ total: { type: Decimal128, default: Decimal128.fromString('8.01'), set: v => Decimal128.fromString((Math.round(Number(v)*100)/100).toString()) } }) // 控制器函数:传入的addValue需转成Decimal128类型 let myController = (id, addValue) => { const incValue = Decimal128.fromString(addValue.toString()); return model.findByIdAndUpdate(id, { $inc: { total: incValue } }) }
这种方式能精确存储,但查询返回的是Decimal128对象,需要调用.toString()或转成数字后使用,适合对精度要求极高且无法修改整数存储的场景。
内容的提问来源于stack exchange,提问作者Sergey Ivanov
相关产品推荐
相关产品推荐

