天天看點

阿裡P8級架構師怎麼處理電商業務中的數值計算的精度/舍入/溢出問題?(上)1 電腦的災難:10%+10%到底等于幾?2 滿目瘡痍的Double3 救世的BigDecimal

1 電腦的災難:10%+10%到底等于幾?

  • 我們人類以為是 0.2,可是打開手機電腦試試呢?
  • 阿裡P8級架構師怎麼處理電商業務中的數值計算的精度/舍入/溢出問題?(上)1 電腦的災難:10%+10%到底等于幾?2 滿目瘡痍的Double3 救世的BigDecimal

解密

國外計算程式使用的單步計算法。于是,

a+b%

表示

a*(1+b%)

。是以,手機電腦實際上在計算

10%*(1+10%)= 0.11

再通俗點一句話說清運算原理。以8+10%為例,為什麼=8.8而不是8.1?一起讀:8元錢,加上10%的小費,一共是8.8元。

最早的電子電腦并沒有%,是後來加的。作為後續改進,它一定解決了計算場景中的常用痛點,而絕不是腦殘。我推測很可能是西方人計算折扣、小費、利息等常見場景。

2 滿目瘡痍的Double

  • 浮點數四則運算
  • 阿裡P8級架構師怎麼處理電商業務中的數值計算的精度/舍入/溢出問題?(上)1 電腦的災難:10%+10%到底等于幾?2 滿目瘡痍的Double3 救世的BigDecimal
  • 結果
  • 阿裡P8級架構師怎麼處理電商業務中的數值計算的精度/舍入/溢出問題?(上)1 電腦的災難:10%+10%到底等于幾?2 滿目瘡痍的Double3 救世的BigDecimal
  • 由于計算機内部是以二進制存儲數值的,浮點數亦是。Java采用IEEE 754标準實作浮點數的表達和運算。比如,0.1的二進制表示為0.0 0011 0011 0011… (0011 無限循環),再轉換為十進制就是0.1000000000000000055511151231257827021181583404541015625。計算機無法精确表示0.1,是以浮點數計算造成精度損失。

你可能覺得像0.1,其十進制和二進制間轉換後相差很小,不會對計算産生什麼嚴重影響。但積土成山,大量使用double作大量金錢計算,最終損失精度就是大量資金出入了。

一位“黑客”利用銀行漏洞從PayPal、Google Checkout和其它線上支付公司竊取了5萬多美元,每次隻偷幾美分。他所利用的漏洞是:銀行在開戶後一般會向帳号發送小額錢去驗證帳戶是否有效,數額一般在幾美分到幾美元左右。Google Checkout和Paypal也使用相同的方法去檢驗與線上帳号捆綁的信用卡和借記卡帳号。 用一個自動腳本開了58,000個帳号,收集了數以千計的超小額費用,彙入到幾個個人銀行賬戶中去。從Google Checkout服務騙到了$8,000以上的現金。銀行注意到了這種奇怪的現金流動,和他取得聯系,Largent解釋他仔細閱讀過相關服務條款,相信 自己沒做錯事,聲稱需要錢去償還債務。但Largent使用了假名,包括卡通人物的名字,假的位址和社會保障号碼,是以了違反了郵件、銀行和電信欺騙法律。别在中國嘗試,這要判無期徒刑。

3 救世的BigDecimal

我們知道BigDecimal,在浮點數精确表達和運算的場景,一定要使用。不過,在使用BigDecimal時有幾個坑需要避開。

  • BigDecimal之前的四則運算
  • 阿裡P8級架構師怎麼處理電商業務中的數值計算的精度/舍入/溢出問題?(上)1 電腦的災難:10%+10%到底等于幾?2 滿目瘡痍的Double3 救世的BigDecimal
  • 輸出
  • 阿裡P8級架構師怎麼處理電商業務中的數值計算的精度/舍入/溢出問題?(上)1 電腦的災難:10%+10%到底等于幾?2 滿目瘡痍的Double3 救世的BigDecimal
  • 運算結果還是不精确,隻不過是精度高了。

3.1 BigDecimal表示/計算浮點數且使用字元串構造器

阿裡P8級架構師怎麼處理電商業務中的數值計算的精度/舍入/溢出問題?(上)1 電腦的災難:10%+10%到底等于幾?2 滿目瘡痍的Double3 救世的BigDecimal

完美輸出

阿裡P8級架構師怎麼處理電商業務中的數值計算的精度/舍入/溢出問題?(上)1 電腦的災難:10%+10%到底等于幾?2 滿目瘡痍的Double3 救世的BigDecimal

無法調用BigDecimal傳入Double的構造器,但手頭隻有一個Double,如何轉換為精确表達的BigDecimal?

  • Double.toString

    把double轉換為字元串可行嗎?
  • 阿裡P8級架構師怎麼處理電商業務中的數值計算的精度/舍入/溢出問題?(上)1 電腦的災難:10%+10%到底等于幾?2 滿目瘡痍的Double3 救世的BigDecimal

401.5000。

與上面字元串初始化100和4.015相乘得到的結果401.500相比,這裡為什麼多了1個0?

BigDecimal有

scale 小數點右邊的位數

precision 精度,即有效數字的長度

new BigDecimal(Double.toString(100))得到的BigDecimal的scale=1、precision=4;而

new BigDecimal(“100”)得到的BigDecimal的scale=0、precision=3。

BigDecimal乘法操作,傳回值的scale是兩個數的scale相加。是以,初始化100的兩種不同方式,導緻最後結果的scale分别是4和3:

private static void testScale() {
    BigDecimal bigDecimal1 = new BigDecimal("100");
    BigDecimal bigDecimal2 = new BigDecimal(String.valueOf(100d));
    BigDecimal bigDecimal3 = new BigDecimal(String.valueOf(100));
    BigDecimal bigDecimal4 = BigDecimal.valueOf(100d);
    BigDecimal bigDecimal5 = new BigDecimal(Double.toString(100));

    print(bigDecimal1); //scale 0 precision 3 result 401.500
    print(bigDecimal2); //scale 1 precision 4 result 401.5000
    print(bigDecimal3); //scale 0 precision 3 result 401.500
    print(bigDecimal4); //scale 1 precision 4 result 401.5000
    print(bigDecimal5); //scale 1 precision 4 result 401.5000
}

private static void print(BigDecimal bigDecimal) {
    log.info("scale {} precision {} result {}", bigDecimal.scale(), bigDecimal.precision(), bigDecimal.multiply(new BigDecimal("4.015")));
}