【钱处理】商业计算怎样才能保证精度不丢失

news2024/10/7 12:22:37

以项目驱动学习,以实践检验真知

前言

很多系统都有「处理金额」的需求,比如电商系统、财务系统、收银系统,等等。只要和钱扯上关系,就不得不打起十二万分精神来对待,一分一毫都不能出错,否则对系统和用户来说都是灾难。

保证金额的准确性主要有两个方面:溢出精度。溢出是指存储数据的空间得充足,不能金额较大就存储不下了。精度是指计算金额时不能有偏差,多一点少一点都不行。

溢出问题大家都知道如何解决,选择位数长的数值类型即可,即不用 float 用 double 。而精度问题,double 就无法解决了,因为浮点数会导致精度丢失。

我们来直观感受一下精度丢失:

double money = 1.0 - 0.9;

这个运算结果谁都知道该为 0.1,然而实际结果却是 0.09999999999999998。出现这个现象是因为计算机底层是二进制运算,而二进制并不能精准表示十进制小数。所以在商业计算等精确计算中要使用其他数据类型来保证精度不丢失,一定不要使用浮点数。

本螃蟹接下来会详细讲解在实际开发中到底该怎样进行商业计算,并将所有代码和 SQL 语句放在了 Github 上(文末有地址),克隆下来即可运行。

解决方案

有两种数据类型可以满足商业计算的需求,第一个自然是专为商业计算而设计的 Decimal 类型,第二个则是定长整数

Decimal

关于数据类型的选择,一要考虑数据库,二要考虑编程语言。即数据库中用什么类型来存储数据,代码中用什么类型来处理数据

数据库层面自然是用 decimal 类型,因为该类型不存在精度损失的情况,用它来进行商业计算再合适不过。

将字段定义为 decimal 的语法为 decimal(M,N)M 代表存储多少位,N 代表小数存储多少位。假设 decimal(20,2),则代表一共存储 20 位数值,其中小数占 2 位。

我们新建一张用户表,字段很简单就两个,主键和余额:

这里小数位置保留 2 点,代表金额只存储到,实际项目中存储到什么单位得根据业务需求来定,都是可以的。

数据库层面搞定了咱们来看代码层面,在 Java 中对应数据库 decimal 的是 java.math.BigDecimal类型,它自然也能保证精度完全准确。

要创建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)

前面两个是构造函数,后面一个是静态方法。这三种方法都非常方便,但第一种方法禁止使用!看一下这三个对象各自的打印结果就知道为什么了:

d1: 0.1000000000000000055511151231257827021181583404541015625
d2: 0.1
d3: 0.1

第一种方法通过构造函数传入 double 类型的参数并不能精确地获取到值,若想正确的创建 BigDecimal,要么将 double 转换为字符串然后调用构造方法,要么直接调用静态方法。事实上,静态方法内部也是将 double 转换为字符串然后调用的构造方法:

如果是从数据库中查询出小数值,或者前端传递过来小数值,数据会准确映射成 BigDecimal 对象,这一点我们不用操心。

说完创建,接下来就要说最重要的数值运算。运算无非就是加减乘除,这些 BigDecimal 都提供了对应的方法:

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

BigDecimal 是不可变对象,意思就是这些操作都不会改变原有对象的值,方法执行完毕只会返回一个新的对象。若要运算后更新原有值,只能重新赋值:

d1 = d1.subtract(d2);

口说无凭,我们来验证一下精度是否会丢失 :

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

输出结果毫无疑问为  0.1

代码方面已经能保证精度不会丢失,但数学方面除法可能会出现除不尽的情况。比如我们运算 10 除以 3,会抛出如下异常:


为了解决除不尽后导致的无穷小数问题,我们需要人为去控制小数的精度。除法运算还有一个方法就是用来控制精度的:

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

scale 参数表示运算后保留几位小数,roundingMode 参数表示计算小数的方式。

BigDecimal d1 = new BigDecimal("1.0");
BigDecimal d2 = new BigDecimal("3");
System.out.println(d1.divide(d2, 2, RoundingMode.DOWN)); // 小数精度为2,多余小数直接舍去。输出结果为0.33

用 RoundingMode 枚举能够方便地指定小数运算方式,除了直接舍去,还有四舍五入、向上取整等多种方式,根据具体业务需求指定即可。

注意,小数精度尽量在代码中控制,不要通过数据库来控制。数据库中默认采用四舍五入的方式保留小数精度。

比如数据库中设置的小数精度为 2,我存入 0.335,那么最终存储的值就会变为 0.34

我们已经知道如何创建和运算 BigDecimal 对象,只剩下最后一个操作:比较。因为其不是基本数据类型,用双等号 == 肯定是不行的,那我们来试试用 equals比较:

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

输出结果为 false,因为 BigDecimal 的 equals 方法不光会比较值,还会比较精度,就算值一样但精度不一样结果也是 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 的用法就介绍到这,我们接下来看第二种解决方案。

定长整数

定长整数,顾名思义就是固定(小数)长度的整数。它只是一个概念,并不是新的数据类型,我们使用的还是普通的整数。

金额好像理所应当有小数,但稍加思考便会发觉小数并非是必须的。之前我们演示的金额单位是1.55 就是一元五角五分。那如果我们单位是,一元五角五分的值就会变成 15.5。如果再将单位缩小到,值就为 155。没错,只要达到最小单位,小数完全可以省略!这个最小单位根据业务需求来定,比如系统要求精确到,那么值就是1550。当然,一般精确到分就可以了,咱们接下来演示单位都是分。

咱们现在新建一个字段,类型为 bigint,单位为分:

代码中对应的数据类型自然是 Long。基本类型的数值运算我们是再熟悉不过的了,直接使用运算操作符即可:

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

加和减没什么好说的,乘和除可能会出现小数的情况,比如某个商品打八折,运算就是乘以 0.8

long d1 = 2366L; // 23.66元
double result = d1 * 0.8; // 打八折,运算后结果为1892.8
d1 = (long)result; // 转换为整数,舍去所有小数,值为1892。即18.92元

进行小数运算,类型自然而然就会变为浮点数,所以我们还要将浮点数转换为整数。

强转会将所有小数舍去,这个舍去并不代表精度丢失。业务要求最小单位是什么,就只保留什么,低于分的单位我们压根没必要保存。这一点和 BigDecimal 是一致的,如果系统中只需要到分,那小数精度就为 2, 剩余的小数都舍去。

不过有些业务计算可能要求四舍五入等其他操作,这一点我们可以通过 Math类来完成:

long d1 = 2366L; // 23.66元
double result = d1 * 0.8; // 运算后结果为1892.8
d1 = (long)result; // 强转舍去所有小数,值为1892
d1 = (long)Math.ceil(result); // 向上取整,值为1893
d1 = (long)Math.round(result); // 四舍五入,值为1893
...

再来看除法运算。当整数除以整数时,会自动舍去所有小数:

long d1 = 2366L;
long result = d1 / 3; // 正确的值本应该为788.6666666666666,舍去所有小数,最终值为788

如果要进行四舍五入等其他小数操作,则运算时先进行浮点数运算,然后再转换成整数:

long d1 = 2366L;
double result = d1 / 3.0; // 注意,这里除以不是 3,而是 3.0 浮点数
d1 = (long)Math.round(result); // 四射勿入,最终值为789,即7.89元

虽说数据库存储和代码运算都是整数,但前端显示时若还是以为单位就对用户不太友好了。所以后端将值传递给前端后,前端需要自行将值除以 100,以为单位展示给用户。然后前端传值给后端时,还是以约定好的整数传递。

收尾

关于金额处理就讲解完毕了。我们学会了两个商业计算方案:

  • Decimal 类型

  • 定长整数

其实商业计算并没有什么技术难度,但如果没有正确处理则会导致难以估量的损失,毕竟和钱相关的事都不是小事。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/720430.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

集群 第一章

目录 1.群集的含义 2.群集分类 3.群集架构 4.负载调度工作模式 5.lvs 虚拟服务器 6.nat 模式 lvs 负载均衡群集部署 7.总结 1.群集的含义 由多台主机构成,但对外只表现为一个整体,只提供一个访问入口(域名与IP地址)&#…

建筑师们,你们该把三维模型弄到PPT里做汇报了!

➤如何实现项目汇报效率比传统的PPT高,同时汇报效果更直观? ➤如何摆脱方案汇报只能向客户交付数据,安装专业软件查看项目成果? ➤如何将无人机航测数据、CAD图纸、BIM设计成果进行融合,挖掘出更深层次的应用&#x…

mysql体系结构及安装部署mysql

目录 1.体系结构 2.安装mysql 1.yum 源安装 2. 第二种安装方式-通用二进制方式 3.mysql改密方式 第一种,知道密码的情况下 第二种,不知道密码 1.体系结构 MySQL server连接层连接池(缓冲池)SQL层系统管理和控制工具SQL…

亿级日活业务稳如磐石 华为云发布性能测试服务CodeArts PerfTest

HDC期间可参与华为云PaaS生态抽奖活动,活动链接在文末 计算机软件作为人类逻辑智慧的伟大结晶之一,已经渗透到了人类社会的各个角落。早期的计算机发展对硬件有很强的依赖性,只有少数的个人或者机构才能拥有软件这种“奢侈品”。但随着软件行…

了解和搭建zabbix 6.0(来日方长)

前言: 作为一个运维,需要会使用监控系统查看服务器系统性能、应用服务状态和网站流量指标等,利用监控系统的数据去了解网站上线发布的结果和健康状态。 利用一个优秀的监控软件,我们可以: 通过一个友好的界面进行浏览整个网站所有…

OpenAI呼吁超级智能需要被监管

ChatGPT的创建者OpenAI发出了对超级智能监管的发人深省的呼吁,并与核能的监管进行了类比。在最近的一篇博客文章中,OpenAI强调了人工智能快速发展可能产生的影响以及这个不断变化的领域治理的紧迫性。该公司表示,预计人工智能系统将在未来十年…

你一定要用这个API管理工具,看完你就知道为什么了

以下是经常发生在程序员之间的对话: 小张:你知道为什么程序员不喜欢写文档? 小王:因为代码就是最好的文档啊!谁还需要写那些冗长的说明呢? 小张:那你知道为什么程序员也不喜欢别人不写文档吗…

JAVA POI的excel中包含图片进行读取保存,单张图片,多张图片

JAVA POI的excel中包含图片进行读取保存,单张图片,多张图片 ---------------------------------------------效果---------------------------------------------------------- 1.单张图片 2.多张图片 import java.io.FileInputStream; import java.io.FileOutputStream;…

途乐证券|有色金属板块崛起涨超2%,云南锗业两连板

周三(7月5日),A股三大股指震荡整理。截至上午收盘,上证指数跌幅达0.51%,报3228.68点;深证成指和创业板指跌幅分别为0.53%和0.59%;沪深两市合计成交额5310.1.6亿元,总体来看,两市个股跌多涨少。 …

机器学习25:《数据准备和特征工程-III》采样和分隔

目录 1.采样和分割数据 1.1 抽样简介 1.2 过滤 PII(个人身份信息) 2.数据不平衡 2.1 下采样和增加权重 3.数据分割示例 3.1 随机分割可能不是最好的方法 4.分割数据 5.随机化 5.1 实际考虑 5.2 散列的注意事项 6.参考文献 1.采样和分割数据-…

2023,中国电商重回元老时代

中国的历史上不缺“太上皇”,但“太上皇”再度站到台前的很少。公元1457年,被囚禁在南宫的“太上皇”朱祁镇复位,上演了中国历史上少见的南宫复辟。而危机时刻被推举为皇帝的朱祁钰,后来的庙号是代宗,阴阳怪气十足。 …

php://input文件包含

实验目的 通过本实验,了解php封装伪协议,掌握php://input文件包含的用法 实验环境 操作机:kali 靶机:Windows 实验地址:http://靶机ip/exp/include2/input/input2/ 工具:burpsuite 用户名&#xff1a…

好用的屏幕录制工具--Bandicam(班迪录屏)

最近准备录个电脑的音频,找了好多个软件,除了收费就是功能有限,经过好一番折腾试用,发现了2个介面友好功能强大的录屏、录音软件: ① Bandicam(班迪录屏)(强烈推荐) ② 数据蛙录屏 Bandicam(班迪…

【PC】CPU与GPU

文章目录 CPU与主板CPU是什么主板是什么功能 GPU与显卡GPU是什么显卡是什么功能 CPU与GPU的关系 ALU: 算术单元(Arithmetic Unit):算术单元执行基本的算术运算,如加法、减法、乘法和除法。它能够对整数、浮点数和定点数…

适合成长型企业的4个 CRM 工作流程

如果你在繁琐的任务上花费太多的时间并难以让你的业务井井有条,CRM工作流程自动化可能会解决你的问题。 CRM(客户关系管理系统)是自动化工作流程最有效的工具之一,因为它可以帮助你从一个地方完成关键工作流程。CRM工作流程使你能…

软件测试技能,JMeter压力测试教程,获取post请求x-www-form-urlencoded格式的数据(二十四)

一、前言 post请求的参数有一些是json格式,也有一些是x-www-form-urlencoded格式,前面讲签名的时候获取到post请求的是json格式 本篇继续讲x-www-form-urlencoded格式的请求body如何获取到 二、x-www-form-urlencoded 在请求头部添加Content-Type类型…

CodeForces..移位密码器.[简单].[字符比较]

题目描述: 题目解读: 对字符串 a 进行加密后得到j加密字符串 s 。 加密规则为: 在字符串 a 的每个字符之后,添加任意(可能为零)数量的小写字母,与字符本身不同。 在每次这样的添加之后,我们将原字符添加…

【来不及刷题之】42、括号生成(递归)

常规的方法是用回溯来写这个题,但是回溯理解起来实在是有一点困难,下面这个思路是直接用递归来生成,首先要明确的是,在已经生成的字符串中,左括号的数量一定要大于等于右括号的数量,否则就不合法&#xff0…

ADSCOPE加入中国广告协会!

近日,经协会批准,上海倍孜网络技术有限公司正式加入中国广告协会,成为会员单位。上海倍孜将在中广协的组织和引导下,依托自身在行业深耕多年的优势,为中国数字营销领域贡献力量。 中国广告协会(中广协&…

看完就会,从抓包到接口测试的全过程解析

一、为什么抓包 从功能测试角度 通过抓包查看隐藏字段 Web 表单中会有很多隐藏的字段,这些隐藏字段一般都有一些特殊的用途,比如收集用户的数据,预防 CRSF 攻击,防网络爬虫,以及一些其他用途。这些隐藏字段在界面上…