当前位置:首页 > 问答 > 正文

ORA报错说啥NaN当浮点数用,搞得程序崩了,远程帮忙修复解决问题

用户那边打电话过来,火急火燎的,说系统突然就“崩了”,页面上冒出来一堆看不懂的英文错误,我让他别慌,把错误信息完整地截图发过来,截图传过来一看,数据库日志里赫然躺着一行刺眼的ORA错误码,后面跟着一句描述,大意是:“遇到了一个无效的数字:NaN被用在了一个期望是浮点数的地方”。

“NaN”,这仨字母我太熟悉了,在程序员眼里,它基本就等于“麻烦来了”,它不是“不是一个数字”的字面意思那么简单,它更像是一个幽灵值,一个计算彻底失控后留下的烂摊子,你让零去除以零,或者对一个负数开平方根,再或者进行了一些无穷大减无穷大的诡异操作,系统算不下去了,又不能直接报错中断,就干脆给你返回这么一个“NaN”,意思是“这玩意儿我算不出来,是个非数字,你看着办吧”。

问题就在于,程序很多时候“不会办”,它老老实实地把这个NaN当成一个正常的数字,继续参与后续的运算、比较,甚至试图把它塞进数据库里,Oracle数据库是个严谨的家伙,它的浮点数字段(比如BINARY_FLOAT, BINARY_DOUBLE,甚至是NUMBER的某些情况)可容不下NaN这种“异类”,当程序试图把NaN写入字段时,数据库就会坚决地拒绝,抛出我们看到的这个ORA错误,程序流程被打断,可不就“崩了”嘛。

远程连上他们的测试环境,我让用户先别动生产系统,复现问题的步骤倒是很简单,用户描述说就是在处理一批计算数据,特别是涉及到一些比率和百分比运算的时候,页面就卡死然后报错,我顺着这个线索,去查负责核心计算的Java代码,果然,在一个计算“增长率”的方法里,我发现了疑点,代码大概是这样的:double growthRate = (currentValue - lastValue) / lastValue; 看起来很标准对不对?但这里埋了个大雷,如果lastValue是0怎么办?零不能做除数,这在数学上是铁律,在Java里,0做除数不会直接导致程序崩溃(不像整数除零会抛异常),但对于浮点数运算,它会得到一个特殊的结果:Double.NaN

问题就出在这里!当上一期的值lastValue为0,而本期值currentValue不为0时,这个计算就会产生NaN,程序后续又毫无防备地将这个NaN直接用于更新数据库的语句中,UPDATE some_table SET growth_rate = ? WHERE ...,这个问号参数被塞进去的就是NaN,数据库接到这个指令,一看:“嚯,你想往我的数字字段里塞个不是数字的东西?没门!” ORA错误抛出,事务回滚,整个操作失败。

找到根因,修复起来思路就清晰了,核心原则就是:在NaN产生之前就拦截它,或者产生之后立即处理它,绝不能让它溜进数据库。

ORA报错说啥NaN当浮点数用,搞得程序崩了,远程帮忙修复解决问题

我采取了几个步骤:

第一,源头防御,堵住产生NaN的漏洞。 我修改了那个计算增长率的代码,增加了对除数的判断,改完之后大概是这个样子: if (lastValue == 0) { // 如果上期值为0,本期有值,我们认为增长率为一个特定的值,比如100%表示从无到有,或者设置为null growthRate = (currentValue > 0) ? 1.0 : 0.0; // 这里根据业务逻辑来定,也可以设置为其他标志值 } else { growthRate = (currentValue - lastValue) / lastValue;

这样,就从根儿上避免了因除零而产生的NaN。

ORA报错说啥NaN当浮点数用,搞得程序崩了,远程帮忙修复解决问题

第二,增加第二道防线,进行数据清洗。 在将计算结果最终设置到数据库更新语句的参数之前,再加一层校验,我写了一个简单的工具方法: public static Double sanitizeDouble(Double value) { if (value == null || Double.isNaN(value) || Double.isInfinite(value)) { return null; // 或者返回一个默认值,如0.0,但需要业务确认 return value;

然后在使用 growthRate 的地方,这样调用:preparedStatement.setDouble(1, sanitizeDouble(growthRate));,这样,即使其他地方不可预知地产生了NaN,在入库前也会被捕获并处理成NULL(数据库字段需要允许为NULL),或者一个安全的默认值,避免了程序的崩溃。

第三,检查和修正历史脏数据。 我让用户在数据库里执行了一个查询,查找那些已经是NaN的记录,在Oracle里,对于BINARY_FLOAT/BINARY_DOUBLE类型,可以用IS NAN来查询,比如SELECT * FROM some_table WHERE growth_rate IS NAN;,果然,查出了一些历史遗留的“地雷”,对于这些数据,根据业务意义进行了批量更新,要么修正为正确的计算值,要么设置为NULL,清除了隐患。

修复代码后,我在测试环境让用户重新跑了一遍之前出错的操作流程,页面流畅地执行完毕,数据库成功更新,再也没有讨厌的ORA错误弹出来,用户那边松了口气,连声道谢。

总结这次远程救援,关键点就两个:一是要立刻明白NaN的含义和它产生的常见场景(特别是除零);二是处理思路要清晰,优先在代码逻辑层面防止NaN的产生,其次要做好防御性编程,对可能不干净的数据进行清洗,确保送进数据库的数据是“干净”的,这种问题看似是数据库报错,但病根儿八成在应用程序的代码逻辑里。