【項(xiàng)目實(shí)踐】商業(yè)計算怎樣才能保證精度不丟失

商業(yè)計算.png

以項(xiàng)目驅(qū)動學(xué)習(xí)熄云,以實(shí)踐檢驗(yàn)真知

前言

很多系統(tǒng)都有「處理金額」的需求膨更,比如電商系統(tǒng)、財務(wù)系統(tǒng)缴允、收銀系統(tǒng)荚守,等等。只要和錢扯上關(guān)系练般,就不得不打起十二萬分精神來對待矗漾,一分一毫都不能出錯,否則對系統(tǒng)和用戶來說都是災(zāi)難薄料。

保證金額的準(zhǔn)確性主要有兩個方面:溢出精度敞贡。溢出是指存儲數(shù)據(jù)的空間得充足,不能金額較大就存儲不下了摄职。精度是指計算金額時不能有偏差誊役,多一點(diǎn)少一點(diǎn)都不行。

溢出問題大家都知道如何解決谷市,選擇位數(shù)長的數(shù)值類型即可蛔垢,即不用 floatdouble 。而精度問題迫悠,double 就無法解決了啦桌,因?yàn)楦↑c(diǎn)數(shù)會導(dǎo)致精度丟失。

我們來直觀感受一下精度丟失:

double money = 1.0 - 0.9;

這個運(yùn)算結(jié)果誰都知道該為 0.1及皂,然而實(shí)際結(jié)果卻是 0.09999999999999998。出現(xiàn)這個現(xiàn)象是因?yàn)橛嬎銠C(jī)底層是二進(jìn)制運(yùn)算且改,而二進(jìn)制并不能精準(zhǔn)表示十進(jìn)制小數(shù)验烧。所以在商業(yè)計算等精確計算中要使用其他數(shù)據(jù)類型來保證精度不丟失,一定不要使用浮點(diǎn)數(shù)又跛。

本螃蟹接下來會詳細(xì)講解在實(shí)際開發(fā)中到底該怎樣進(jìn)行商業(yè)計算碍拆,并將所有代碼和 SQL 語句放在了 Github 上,克隆下來即可運(yùn)行慨蓝。

解決方案

有兩種數(shù)據(jù)類型可以滿足商業(yè)計算的需求感混,第一個自然是專為商業(yè)計算而設(shè)計的 Decimal 類型,第二個則是定長整數(shù)礼烈。

Decimal

關(guān)于數(shù)據(jù)類型的選擇弧满,一要考慮數(shù)據(jù)庫梢杭,二要考慮編程語言牵咙。即數(shù)據(jù)庫中用什么類型來存儲數(shù)據(jù)瘟斜,代碼中用什么類型來處理數(shù)據(jù)

數(shù)據(jù)庫層面自然是用 decimal 類型装处,因?yàn)樵擃愋筒淮嬖诰葥p失的情況,用它來進(jìn)行商業(yè)計算再合適不過彭沼。

將字段定義為 decimal 的語法為 decimal(M,N)殊鞭,M 代表存儲多少位,N 代表小數(shù)存儲多少位数冬。假設(shè) decimal(20,2)节槐,則代表一共存儲 20 位數(shù)值,其中小數(shù)占 2 位拐纱。

我們新建一張用戶表铜异,字段很簡單就兩個,主鍵和余額:

balance.png

這里小數(shù)位置保留 2 點(diǎn)戳玫,代表金額只存儲到熙掺,實(shí)際項(xiàng)目中存儲到什么單位得根據(jù)業(yè)務(wù)需求來定,都是可以的咕宿。

數(shù)據(jù)庫層面搞定了咱們來看代碼層面币绩,在 Java 中對應(yīng)數(shù)據(jù)庫 decimal 的是 java.math.BigDecimal類型,它自然也能保證精度完全準(zhǔn)確府阀。

要創(chuàng)建BigDecimal主要有三種方法:

BigDecimal d1 = new BigDecimal(0.1); // BigDecimal(double val)
BigDecimal d2 = new BigDecimal("0.1"); // BigDecimal(String val)
BigDecimal d3 = BigDecimal.valueOf(0.1); // static BigDecimal valueOf(double val)

前面兩個是構(gòu)造函數(shù)缆镣,后面一個是靜態(tài)方法。這三種方法都非常方便试浙,但第一種方法禁止使用董瞻!看一下這三個對象各自的打印結(jié)果就知道為什么了:

d1: 0.1000000000000000055511151231257827021181583404541015625
d2: 0.1
d3: 0.1

第一種方法通過構(gòu)造函數(shù)傳入 double 類型的參數(shù)并不能精確地獲取到值,若想正確的創(chuàng)建 BigDecimal田巴,要么將 double 轉(zhuǎn)換為字符串然后調(diào)用構(gòu)造方法钠糊,要么直接調(diào)用靜態(tài)方法。事實(shí)上壹哺,靜態(tài)方法內(nèi)部也是將 double 轉(zhuǎn)換為字符串然后調(diào)用的構(gòu)造方法:

static.png

如果是從數(shù)據(jù)庫中查詢出小數(shù)值抄伍,或者前端傳遞過來小數(shù)值,數(shù)據(jù)會準(zhǔn)確映射成 BigDecimal 對象管宵,這一點(diǎn)我們不用操心截珍。

說完創(chuàng)建,接下來就要說最重要的數(shù)值運(yùn)算箩朴。運(yùn)算無非就是加減乘除岗喉,這些 BigDecimal 都提供了對應(yīng)的方法:

BigDecimal add(BigDecimal); // 加
BigDecimal subtract(BigDecimal); // 減
BigDecimal multiply(BigDecimal); // 乘
BigDecimal divide(BigDecimal); // 除

BigDecimal 是不可變對象,意思就是這些操作都不會改變原有對象的值炸庞,方法執(zhí)行完畢只會返回一個新的對象钱床。若要運(yùn)算后更新原有值,只能重新賦值:

d1 = d1.subtract(d2);

口說無憑燕雁,我們來驗(yàn)證一下精度是否會丟失 :

BigDecimal d1 = new BigDecimal("1.0");
BigDecimal d2 = new BigDecimal("0.9");
System.out.println(d1.subtract(d2));

輸出結(jié)果毫無疑問為 0.1诞丽。

代碼方面已經(jīng)能保證精度不會丟失鲸拥,但數(shù)學(xué)方面除法可能會出現(xiàn)除不盡的情況。比如我們運(yùn)算 10 除以 3僧免,會拋出如下異常:

ArithmeticException.png

為了解決除不盡后導(dǎo)致的無窮小數(shù)問題刑赶,我們需要人為去控制小數(shù)的精度。除法運(yùn)算還有一個方法就是用來控制精度的:

BigDecimal divide(BigDecimal divisor, int scale, int roundingMode)

scale 參數(shù)表示運(yùn)算后保留幾位小數(shù)懂衩,roundingMode 參數(shù)表示計算小數(shù)的方式撞叨。

BigDecimal d1 = new BigDecimal("1.0");
BigDecimal d2 = new BigDecimal("3");
System.out.println(d1.divide(d2, 2, RoundingMode.DOWN)); // 小數(shù)精度為2,多余小數(shù)直接舍去浊洞。輸出結(jié)果為0.33

RoundingMode 枚舉能夠方便地指定小數(shù)運(yùn)算方式牵敷,除了直接舍去,還有四舍五入法希、向上取整等多種方式枷餐,根據(jù)具體業(yè)務(wù)需求指定即可。

注意苫亦,小數(shù)精度盡量在代碼中控制毛肋,不要通過數(shù)據(jù)庫來控制。數(shù)據(jù)庫中默認(rèn)采用四舍五入的方式保留小數(shù)精度屋剑。

比如數(shù)據(jù)庫中設(shè)置的小數(shù)精度為2润匙,我存入 0.335,那么最終存儲的值就會變?yōu)?0.34唉匾。

我們已經(jīng)知道如何創(chuàng)建和運(yùn)算 BigDecimal 對象孕讳,只剩下最后一個操作:比較。因?yàn)槠洳皇腔緮?shù)據(jù)類型巍膘,用雙等號 == 肯定是不行的厂财,那我們來試試用 equals比較:

BigDecimal d1 = new BigDecimal("0.33");
BigDecimal d2 = new BigDecimal("0.3300");
System.out.println(d1.equals(d2)); // false

輸出結(jié)果為 false,因?yàn)?BigDecimalequals 方法不光會比較值峡懈,還會比較精度蟀苛,就算值一樣但精度不一樣結(jié)果也是 false。若想判斷值是否一樣逮诲,需要使用int compareTo(BigDecimal val)方法:

BigDecimal d1 = new BigDecimal("0.33");
BigDecimal d2 = new BigDecimal("0.3300");
System.out.println(d1.compareTo(d2) == 0); // true

d1 大于 d2,返回 1幽告;

d1 小于 d2梅鹦,返回 -1

兩值相等冗锁,返回 0齐唆。

BigDecimal 的用法就介紹到這,我們接下來看第二種解決方案冻河。

定長整數(shù)

定長整數(shù)箍邮,顧名思義就是固定(小數(shù))長度的整數(shù)茉帅。它只是一個概念,并不是新的數(shù)據(jù)類型锭弊,我們使用的還是普通的整數(shù)堪澎。

金額好像理所應(yīng)當(dāng)有小數(shù),但稍加思考便會發(fā)覺小數(shù)并非是必須的味滞。之前我們演示的金額單位是樱蛤,1.55 就是一元五角五分。那如果我們單位是剑鞍,一元五角五分的值就會變成 15.5昨凡。如果再將單位縮小到,值就為 155蚁署。沒錯便脊,只要達(dá)到最小單位,小數(shù)完全可以省略光戈!這個最小單位根據(jù)業(yè)務(wù)需求來定哪痰,比如系統(tǒng)要求精確到,那么值就是1550田度。當(dāng)然妒御,一般精確到分就可以了,咱們接下來演示單位都是分镇饺。

咱們現(xiàn)在新建一個字段乎莉,類型為 bigint,單位為分:

otherBalance.png

代碼中對應(yīng)的數(shù)據(jù)類型自然是 Long奸笤⊥锟校基本類型的數(shù)值運(yùn)算我們是再熟悉不過的了,直接使用運(yùn)算操作符即可:

long d1 = 10000L; // 100元
d1 += 500L; // 加五元
d1 -= 500L; // 減五元

加和減沒什么好說的监右,乘和除可能會出現(xiàn)小數(shù)的情況边灭,比如某個商品打八折,運(yùn)算就是乘以 0.8

long d1 = 2366L; // 23.66元
double result = d1 * 0.8; // 打八折健盒,運(yùn)算后結(jié)果為1892.8
d1 = (long)result; // 轉(zhuǎn)換為整數(shù)绒瘦,舍去所有小數(shù),值為1892扣癣。即18.92元

進(jìn)行小數(shù)運(yùn)算惰帽,類型自然而然就會變?yōu)楦↑c(diǎn)數(shù),所以我們還要將浮點(diǎn)數(shù)轉(zhuǎn)換為整數(shù)父虑。

強(qiáng)轉(zhuǎn)會將所有小數(shù)舍去该酗,這個舍去并不代表精度丟失。業(yè)務(wù)要求最小單位是什么,就只保留什么呜魄,低于分的單位我們壓根沒必要保存悔叽。這一點(diǎn)和 BigDecimal 是一致的,如果系統(tǒng)中只需要到分爵嗅,那小數(shù)精度就為 2娇澎, 剩余的小數(shù)都舍去。

不過有些業(yè)務(wù)計算可能要求四舍五入等其他操作操骡,這一點(diǎn)我們可以通過 Math類來完成:

long d1 = 2366L; // 23.66元
double result = d1 * 0.8; // 運(yùn)算后結(jié)果為1892.8
d1 = (long)result; // 強(qiáng)轉(zhuǎn)舍去所有小數(shù)九火,值為1892
d1 = (long)Math.ceil(result); // 向上取整,值為1893
d1 = (long)Math.round(result); // 四舍五入册招,值為1893
...

再來看除法運(yùn)算岔激。當(dāng)整數(shù)除以整數(shù)時,會自動舍去所有小數(shù):

long d1 = 2366L;
long result = d1 / 3; // 正確的值本應(yīng)該為788.6666666666666是掰,舍去所有小數(shù)虑鼎,最終值為788

如果要進(jìn)行四舍五入等其他小數(shù)操作,則運(yùn)算時先進(jìn)行浮點(diǎn)數(shù)運(yùn)算键痛,然后再轉(zhuǎn)換成整數(shù):

long d1 = 2366L;
double result = d1 / 3.0; // 注意炫彩,這里除以不是 3,而是 3.0 浮點(diǎn)數(shù)
d1 = (long)Math.round(result); // 四射勿入絮短,最終值為789江兢,即7.89元

雖說數(shù)據(jù)庫存儲和代碼運(yùn)算都是整數(shù),但前端顯示時若還是以為單位就對用戶不太友好了丁频。所以后端將值傳遞給前端后杉允,前端需要自行將值除以 100,以為單位展示給用戶席里。然后前端傳值給后端時叔磷,還是以約定好的整數(shù)傳遞。

unit.png

收尾

關(guān)于金額處理就講解完畢了奖磁。我們學(xué)會了兩個商業(yè)計算方案:

  • Decimal 類型
  • 定長整數(shù)

其實(shí)商業(yè)計算并沒有什么技術(shù)難度改基,但如果沒有正確處理則會導(dǎo)致難以估量的損失,畢竟和錢相關(guān)的事都不是小事咖为。

本文為了方便大家理解秕狰,所以省略了前后端聯(lián)調(diào)以及數(shù)據(jù)庫操作的內(nèi)容。但既然是項(xiàng)目實(shí)踐躁染,那就得有一個完整項(xiàng)目封恰,所以本螃蟹基于 Spring Boot 搭建了一個完整的 Web 項(xiàng)目,數(shù)據(jù)庫操作和接口都已寫好褐啡,SQL 語句也有,將 Github 倉庫克隆下來即可感受在真實(shí)項(xiàng)目中如何運(yùn)用的本文知識鳖昌。倉庫中還有許多其他項(xiàng)目實(shí)踐备畦,涵蓋各個業(yè)務(wù)各個功能低飒,其中一些模塊的質(zhì)量甚至可以單開一個倉庫,讓你再也不用尋找各個框架 Demo 和腳手架懂盐。歡迎 star褥赊,螃蟹會更新更多項(xiàng)目實(shí)踐的!

我是「RudeCrab」莉恼,一只粗魯?shù)捏π钒韬恚非蠛唵未直┑刂v解技術(shù)。

關(guān)注「RudeCrab」微信公眾號俐银,和螃蟹一起橫行霸道尿背。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市捶惜,隨后出現(xiàn)的幾起案子田藐,更是在濱河造成了極大的恐慌,老刑警劉巖吱七,帶你破解...
    沈念sama閱讀 219,188評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件汽久,死亡現(xiàn)場離奇詭異,居然都是意外死亡踊餐,警方通過查閱死者的電腦和手機(jī)景醇,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,464評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來吝岭,“玉大人三痰,你說我怎么就攤上這事〔缘” “怎么了酒觅?”我有些...
    開封第一講書人閱讀 165,562評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長微峰。 經(jīng)常有香客問我舷丹,道長,這世上最難降的妖魔是什么蜓肆? 我笑而不...
    開封第一講書人閱讀 58,893評論 1 295
  • 正文 為了忘掉前任颜凯,我火速辦了婚禮,結(jié)果婚禮上仗扬,老公的妹妹穿的比我還像新娘症概。我一直安慰自己,他們只是感情好早芭,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,917評論 6 392
  • 文/花漫 我一把揭開白布彼城。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪募壕。 梳的紋絲不亂的頭發(fā)上调炬,一...
    開封第一講書人閱讀 51,708評論 1 305
  • 那天,我揣著相機(jī)與錄音舱馅,去河邊找鬼缰泡。 笑死,一個胖子當(dāng)著我的面吹牛代嗤,可吹牛的內(nèi)容都是我干的棘钞。 我是一名探鬼主播,決...
    沈念sama閱讀 40,430評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼干毅,長吁一口氣:“原來是場噩夢啊……” “哼宜猜!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起溶锭,我...
    開封第一講書人閱讀 39,342評論 0 276
  • 序言:老撾萬榮一對情侶失蹤宝恶,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后趴捅,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體垫毙,經(jīng)...
    沈念sama閱讀 45,801評論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,976評論 3 337
  • 正文 我和宋清朗相戀三年拱绑,在試婚紗的時候發(fā)現(xiàn)自己被綠了综芥。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,115評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡猎拨,死狀恐怖膀藐,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情红省,我是刑警寧澤额各,帶...
    沈念sama閱讀 35,804評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站吧恃,受9級特大地震影響虾啦,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜痕寓,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,458評論 3 331
  • 文/蒙蒙 一傲醉、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧呻率,春花似錦硬毕、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,008評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽逻悠。三九已至,卻和暖如春韭脊,著一層夾襖步出監(jiān)牢的瞬間蹂风,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,135評論 1 272
  • 我被黑心中介騙來泰國打工乾蓬, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人慎恒。 一個月前我還...
    沈念sama閱讀 48,365評論 3 373
  • 正文 我出身青樓任内,卻偏偏與公主長得像,于是被迫代替她去往敵國和親融柬。 傳聞我的和親對象是個殘疾皇子死嗦,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,055評論 2 355

推薦閱讀更多精彩內(nèi)容