BigDecimal使用不當(dāng),造成P0事故!
往期熱門文章:
1、Spring Boot 啟動(dòng)時(shí)自動(dòng)執(zhí)行代碼的幾種方式,還有誰不會(huì)??
文章來源:https://c1n.cn/MSqAy
背景
事故
分析
總結(jié)
工具分享
背景
我們?cè)谑褂媒痤~計(jì)算或者展示金額的時(shí)候經(jīng)常會(huì)使用 BigDecimal,也是涉及金額時(shí)非常推薦的一個(gè)類型。
BigDecimal 自身也提供了很多構(gòu)造器方法,這些構(gòu)造器方法使用不當(dāng)可能會(huì)造成不必要的麻煩甚至是金額損失,從而引起事故資損。
事故
接下來我們看下收銀臺(tái)出的一起事故。
| 問題描述
收銀臺(tái)計(jì)算商品金額報(bào)錯(cuò),導(dǎo)致訂單無法支付。
| 事故級(jí)別
P0
| 事故過程
如下:
13:44,接到報(bào)警,訂單支付失敗,支付可用率降至 60%
13:50,迅速回滾上線代碼,恢復(fù)正常
14:20,review 代碼,預(yù)發(fā)布驗(yàn)證發(fā)現(xiàn)問題點(diǎn)
14:58,修改問題代碼上線,線上恢復(fù)
| 故障原因
BigDecimal 在金額計(jì)算中丟失精度。
原因分析
首先我們先用一段代碼復(fù)現(xiàn)問題根源,如下所示:
public?static?void?main(String[]?args)?{
????BigDecimal?bigDecimal=new?BigDecimal(88);
????System.out.println(bigDecimal);
????bigDecimal=new?BigDecimal("8.8");
????System.out.println(bigDecimal);
????bigDecimal=new?BigDecimal(8.8);
????System.out.println(bigDecimal);
}
執(zhí)行結(jié)果如下:

通過測(cè)試發(fā)現(xiàn),當(dāng)使用 double 或者 float 這些浮點(diǎn)數(shù)據(jù)類型時(shí),會(huì)丟失精度,String、int 則不會(huì),這是為什么呢?
我們點(diǎn)開構(gòu)造器方法看下源碼:
public?static?long?doubleToLongBits(double?value)?{
????long?result?=?doubleToRawLongBits(value);
????//?Check?for?NaN?based?on?values?of?bit?fields,?maximum
????//?exponent?and?nonzero?significand.
????if?(?((result?&?DoubleConsts.EXP_BIT_MASK)?==
??????????DoubleConsts.EXP_BIT_MASK)?&&
?????????(result?&?DoubleConsts.SIGNIF_BIT_MASK)?!=?0L)
????????result?=?0x7ff8000000000000L;
????return?result;
}
問題就處在 doubleToRawLongBits 這個(gè)方法上,在 jdk 中 double 類(float 與 int 對(duì)應(yīng))中提供了 double 與 long 轉(zhuǎn)換,doubleToRawLongBits 就是將 double 轉(zhuǎn)換為 long,這個(gè)方法是原始方法(底層不是 java 實(shí)現(xiàn),是 c++ 實(shí)現(xiàn)的)。
double 之所以會(huì)出問題,是因?yàn)樾?shù)點(diǎn)轉(zhuǎn)二進(jìn)制丟失精度。

BigDecimal 在處理的時(shí)候把十進(jìn)制小數(shù)擴(kuò)大 N 倍讓它在整數(shù)上進(jìn)行計(jì)算,并保留相應(yīng)的精度信息。
①float 和 double 類型,主要是為了科學(xué)計(jì)算和工程計(jì)算而設(shè)計(jì)的,之所以執(zhí)行二進(jìn)制浮點(diǎn)運(yùn)算,是為了在廣泛的數(shù)值范圍上提供較為精確的快速近和計(jì)算。
②并沒有提供完全精確的結(jié)果,所以不應(yīng)該被用于精確的結(jié)果的場(chǎng)合。
③當(dāng)浮點(diǎn)數(shù)達(dá)到一定大的數(shù),就會(huì)自動(dòng)使用科學(xué)計(jì)數(shù)法,這樣的表示只是近似真實(shí)數(shù)而不等于真實(shí)數(shù)。
④當(dāng)十進(jìn)制小數(shù)位轉(zhuǎn)換二進(jìn)制的時(shí)候也會(huì)出現(xiàn)無限循環(huán)或者超過浮點(diǎn)數(shù)尾數(shù)的長(zhǎng)度。
總結(jié)
所以,在涉及到精度計(jì)算的過程中,我們盡量使用 String 類型來進(jìn)行轉(zhuǎn)換。
正確用法如下:
BigDecimal?bigDecimal2=new?BigDecimal("8.8");
BigDecimal?bigDecimal3=new?BigDecimal("8.812");
System.out.println(?bigDecimal2.compareTo(bigDecimal3));
System.out.println(?bigDecimal2.add(bigDecimal3));
BigDecimal 創(chuàng)建出來的是對(duì)象,我們不能用傳統(tǒng)的加減乘除對(duì)其進(jìn)行運(yùn)算,必須使用他的方法,在我們數(shù)據(jù)庫存儲(chǔ)里,如果我們使用的是 double 或者 float 類型,需要進(jìn)行來回的轉(zhuǎn)換后進(jìn)行計(jì)算,非常不方便。
工具分享
所以在這里整理出一個(gè) util 類供大家使用:
import?java.math.BigDecimal;
/**
?*?@Author?shuaige
?*?@Date?2022/4/17
?*?@Version?1.0
?**/
public?class?BigDecimalUtils?{
????public?static?BigDecimal?doubleAdd(double?v1,?double?v2)?{
????????BigDecimal?b1?=?new?BigDecimal(Double.toString(v1));
????????BigDecimal?b2?=?new?BigDecimal(Double.toString(v2));
????????return?b1.add(b2);
????}
????public?static?BigDecimal?floatAdd(float?v1,?float?v2)?{
????????BigDecimal?b1?=?new?BigDecimal(Float.toString(v1));
????????BigDecimal?b2?=?new?BigDecimal(Float.toString(v2));
????????return?b1.add(b2);
????}
????public?static?BigDecimal?doubleSub(double?v1,?double?v2)?{
????????BigDecimal?b1?=?new?BigDecimal(Double.toString(v1));
????????BigDecimal?b2?=?new?BigDecimal(Double.toString(v2));
????????return?b1.subtract(b2);
????}
????public?static?BigDecimal?floatSub(float?v1,?float?v2)?{
????????BigDecimal?b1?=?new?BigDecimal(Float.toString(v1));
????????BigDecimal?b2?=?new?BigDecimal(Float.toString(v2));
????????return?b1.subtract(b2);
????}
????public?static?BigDecimal?doubleMul(double?v1,?double?v2)?{
????????BigDecimal?b1?=?new?BigDecimal(Double.toString(v1));
????????BigDecimal?b2?=?new?BigDecimal(Double.toString(v2));
????????return?b1.multiply(b2);
????}
????public?static?BigDecimal?floatMul(float?v1,?float?v2)?{
????????BigDecimal?b1?=?new?BigDecimal(Float.toString(v1));
????????BigDecimal?b2?=?new?BigDecimal(Float.toString(v2));
????????return?b1.multiply(b2);
????}
????public?static?BigDecimal?doubleDiv(double?v1,?double?v2)?{
????????BigDecimal?b1?=?new?BigDecimal(Double.toString(v1));
????????BigDecimal?b2?=?new?BigDecimal(Double.toString(v2));
????????//?保留小數(shù)點(diǎn)后兩位?ROUND_HALF_UP?=?四舍五入
????????return?b1.divide(b2,?2,?BigDecimal.ROUND_HALF_UP);
????}
????public?static?BigDecimal?floatDiv(float?v1,?float?v2)?{
????????BigDecimal?b1?=?new?BigDecimal(Float.toString(v1));
????????BigDecimal?b2?=?new?BigDecimal(Float.toString(v2));
????????//?保留小數(shù)點(diǎn)后兩位?ROUND_HALF_UP?=?四舍五入
????????return?b1.divide(b2,?2,?BigDecimal.ROUND_HALF_UP);
????}
????/**
?????*?比較v1?v2大小
?????*?@param?v1
?????*?@param?v2
?????*?@return?v1>v2?return?1??v1=v2?return?0?v1?????*/
????public?static?int?doubleCompareTo(double?v1,?double?v2)?{
????????BigDecimal?b1?=?new?BigDecimal(Double.toString(v1));
????????BigDecimal?b2?=?new?BigDecimal(Double.toString(v2));
????????return??b1.compareTo(b2);
????}
????public?static?int?floatCompareTo(float?v1,?float?v2)?{
????????BigDecimal?b1?=?new?BigDecimal(Float.toString(v1));
????????BigDecimal?b2?=?new?BigDecimal(Float.toString(v2));
????????return??b1.compareTo(b2);
????}
}
往期熱門文章:
2、超越 Xshell!號(hào)稱下一代 Terminal 終端神器,用完愛不釋手!
7、IDEA公司再發(fā)新神器!超越 VS Code 騷操作!
8、我懷疑這是 IDEA 的 BUG,但是我翻遍全網(wǎng)沒找到證據(jù)!
9、Spring MVC 中的 Controller 是線程安全的嗎?
10、Gitee 倒下了???
