在 Amazon DocumentDB 里处理 Decimal128 类型数据的解决方案
一道简单的数学题
在开始今天的内容之前,我们先计算一道简单的数学题。0.1 X 0.2 =?我相信很多人都笑了,0.02,这是一个孩童都可以回答得出的答案。我们用这道数学题问一下计算机,看看结果又是怎样。
亚马逊云科技开发者社区为开发者们提供全球的开发技术资源。这里有技术文档、开发案例、技术专栏、培训视频、活动与竞赛等。帮助中国开发者对接世界最前沿技术,观点,和项目,并将中国优秀开发者或技术推荐给全球云社区。如果你还没有关注/收藏,看到这里请一定不要匆匆划过,点这里让它成为你的技术宝库!
欢迎第一位选手 Java 入场
计算机给出了答案:
0.020000000000000004
怎么样,是不是手心开始出汗了!我们再欢迎第二位选手 Node.Js 入场:
还是 0.020000000000000004。难道是乘法不行?那我们换加减法!
有请最后一位选手 Golang 入场:
这是 Java 亦或是 Golang 的问题吗?当我们继续在 Python,Ruby 等主流语言上得到相同的结果时,是否会让你感觉世界观遭到了颠覆?
不用怀疑自己,错的是计算机。为什么这么简单的数学题,强大如 Intel/AMD/Graviton 的 CPU 却不能给出正确答案呢?
我们来看下真正的原因。其实是因为在十进制的数学体系中,二进制浮点类型并不适合用来表现或者描述数据本身。譬如 0.1 这个数字,如果使用二进制浮点类型来描述它时,它会被表现为 0.0001100110011001101,这导致了很多数值在计算中会产生精度丢失或者结果偏差。
当然,这在我们的日常生活中,并不会带来太大的问题。譬如天气预报中的温度与湿度指标,数值仅用作体感的参考,35.79999992 摄氏度并不会让你感觉比 36 摄氏度更凉爽或者比 35.5 摄氏度更酷热;您在超市购物时,收银员也不会非要让你支付 12.133333 元相比 12 元多出来的 0.133333 元,但是在一些高精度计算的场景中,数值精度的丢失,会对最终的结果产生严重甚至完全相反的结果。那我们应该如何在保留数值精度的前提下,对数值进行计算呢?
Decimal 数据格式
与我们常见的 Float,Double 等近似保存的数据类型不同,Decimal 保存了精确的原始数值。可以说 Decimal 专门为十进制数学体系设计,弥补了二进制转述小数部分的缺憾,我们通过一张示意图来理解 decimal 的原理。

MongoDB 中的 Decimal
作为广泛使用的文档型数据库,MongoDB 也受到数值精度问题的困扰。为了能够实现高精度数值的存储与还原,decimal128 应运而生,可以在特别微小数值的保存场景上,提供技术层面的支持。
亚马逊云科技推出了托管的兼容 MongoDB 的云原生文档数据库 Amazon DocumentDB,依托计算与存储分离的架构,在很多不同的场景下,帮助客户实现了集群快速扩容,自动流式备份,计算层扩缩容,存储层自动扩容等诸多云原生数据库的功能,简化了数据库运维工作与提高了工作效率。不过截至到 2022 年 7 月,DocumentDB 暂不支持 Decimal128 格式的数据,该如何解决这个问题呢?
通过现象看本质,大家都是”String”
数字 与小数,本身也属于字符的一种,所以 Decimal 本身也是基于字符格式的一种延展。Decimal128(14.999999)与 Decimal(’14.999999’)存在什么本质上的不同,留给各位技术小伙伴们思考了。下面我们通过一个解决方案来解决 DocumentDB 与 Decimal128 的兼容问题。大家一起来吧!
本方案描述了如何短暂停机,将 Decimal128 数据格式转换为 String 的步骤,这解决了存量数据的格式转换问题,并通过 Amazon Data Migration Service 实现了 MongoDB 向 DocumentDB 的离线迁移。
MongoDB 向 DocumentDB 迁移
除了可以使用 MongoDB 原生的 mongodump/mongorestore 进行数据的迁移,我们还可以使用 Amazon Data Migration Service(DMS)以 MongoDB 为数据源,以 Amazon DocumentDB 为数据目标,进行数据迁移,本例采用后者
1.通过控制台找到 DMS 服务,并点选进入 DMS 控制台

2.点击左侧菜单栏的【子网组】,然后点击右上角的【创建子网组】

3.创建一个自定义子网组。如果您的环境是 MongoDB 与 DocumentDB 之间,存在有专线或者 VPN 构建的私有网络环境,您可以如图所示创建一个位于私有子网的自定义子网组,否则,请创建一个位于公有子网的自定义子网组。

4.点击【创建子网组】,完成子网创建
5.创建复制实例


如果您的环境是 MongoDB 与 DocumentDB 之间,存在有专线或者 VPN 构建的私有网络环境,您可以如图所示反选【公开访问】功能,否则,请勾选【公开访问】功能。

7.创建终端节点

7.1 创建以 MongoDB 为引擎的源终端节点

7.2 按照您的实际情况替换红框内容

7.3 创建以 Amazon DocumentDB 为目标的目标终端节点


7.4 使用 Secret Manager 来管理 DocumentDB 的账号信息(可选)
详情可以阅读另一篇专题 blog,请点击这里
创建迁移任务

8.1 使用我们之前创建的复制节点,源终端节点,目标终端节点创建一个迁移任务

8.2 在表映像部分,我们创建一个选择规则,对 poc 数据库下的 newtable 数据表做选中,然后点选创建任务。

8.3 等待迁移任务加载完成,进度到达 100%

至此存量数据已经通过本方案结合 DMS 全部迁移至 DocumentDB 下,并且完成了 Decimal128 向 string 数据格式的转换。我们来做一个验证。
将 Decimal128 转换为 Java BigDecimal
通过之前的解决方案,我们已经成功的把 Decimal128 转换成为 String 存储在数据库中,实现了精度的保留,但是 string 格式保存的数值无法参与计算,我们应该如何解决这个难题?
在 Java 语言中,Decimal128 并不能被直接使用,需要专为 BigDecimal 之后,再进行各类处理与运算。我们知道 Decimal128 是基于 String 的一种延展,那 String 能否按照这个思路进行处理呢?
答案是可以的,我们可以借助 Java 的一个公共类 BigDecimal 实现我们的需求。以下为 Java 的示例代码,展示我们如何利用这个公共类,进行格式的双向转换,可供参考。
将输入字符串“12.3456“转换得到数字 12.3456,可用于从数据库中读取字符串格式数据后转换为 Java 的 BigDecimal 格式。
将 BigDecimal 格式 65.4321 转换得到字符串“65.4321“,可将结果以字符串格式存回数据库。
总结
用本方案使用 String 替代了 Decimal128,完成了存量数据的迁移,对于新增数据,在保证效率的前提下,通过 Java 的 BigDecimal 公共类实现 String 与 BigDecimal 的双向转换,解决了 DocumentDB 中需要使用 Decimal128 格式的需求。DocumentDB 新功能持续发布中,敬请关注。
参考链接:
1.快速理解 Decimal
https://www.splashlearn.com/math-vocabulary/decimals/decimal?trk=cndc-detail
2.使用 Secret Manager 来管理 DMS Endpoints
3.Java Public Class BigDecimal from Oracle
https://docs.oracle.com/javase/8/docs/api/java/math/BigDecimal.html?trk=cndc-detail
本篇作者

付晓明
亚马逊云解决方案架构师,负责云计算解决方案的咨询与架构设计,同时致力于数据库,边缘计算方面的研究和推广。在加入亚马逊云科技之前曾在金融行业 IT 部门负责互联网券商架构的设计,对分布式,高并发,中间件等具有丰富经验。
评论