likes
comments
collection
share

在 Amazon DocumentDB 里处理 Decimal128类型数据的解决方案

作者站长头像
站长
· 阅读数 3

一道简单的数学题

在开始今天的内容之前,我们先计算一道简单的数学题。0.1 X 0.2 =?我相信很多人都笑了,0.02,这是一个孩童都可以回答得出的答案。我们用这道数学题问一下计算机,看看结果又是怎样。

欢迎第一位选手 Java 入场

Java Code
class Main {
 public static void main(String[] args) {
   System.out.println(0.1 * 0.2);
 }
}

计算机给出了答案:

0.020000000000000004

怎么样,是不是手心开始出汗了!我们再欢迎第二位选手 Node.Js 入场:

Node.Js Code:
> 0.1
0.1

> 0.2
0.2
> 0.1 * 0.2
0.020000000000000004

还是0.020000000000000004。难道是乘法不行?那我们换加减法!

有请最后一位选手 Golang 入场:

Golang Code:
a := 1024.1
b := a * 100
fmt.Println(b)

102409.99999999999

c := 2.6
fmt.Println(a - c)

1021.4999999999999

这是 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 的原理。

在 Amazon DocumentDB 里处理 Decimal128类型数据的解决方案

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 的离线迁移。

Code 部分:
##MongoShell Statement,于 MongoDB 执行
##切换至 poc 数据库
use poc;

##创建 origin 数据表并插入两条测试数据,value 字段为 Decimal128
db.origin.insertMany( [
{"_id": 1,  "item": "Byte", "value": Decimal128("1.333333") },

{ "_id": 2, "item": "Bit", "value": Decimal128("2.666666")  }
] )

##结果返回为插入成功
{ acknowledged: true, insertedIds: { '0': 1, '1': 2 } }

##验证一下数据是否存在
db.origin.find();
##返回结果确认数据创建成功;
[
  { _id: 1, item: 'Byte', value: Decimal128("1.333333") },
  { _id: 2, item: 'Bit', value: Decimal128("2.666666") }
]

##转换开始,将 value 字段的 Decimal128格式转换为字符串 String 并另存新字段/列,取名为 newvalue,并将聚合之后的新表输出保存为 poc 数据库下以 newtable 为名的新表

db.getSiblingDB("poc").origin.aggregate( [
{
$addFields: {
newvalue: { $toString: "$value" }
}
},
{ $out : "newtable" }
] )

##确认一下输出是否成功,在看到原始表 origin 之外,增加了一张新表 newtable
show tables;

##得到结果
newtable
origin

##查看一下转换之后新表 newtabl e里面的数据
db.newtable.find();

##结果返回可以看出除了原始表 origin 里的_id,item,value 三个字段之外,新增了一个字段 newvalue,其值与原始 decimal128格式的 value 字段,数值相等,且为字符串 String

[
  {
    _id: 1,
    item: 'Byte',
    value: Decimal128("1.333333"),
    newvalue: '1.333333'
  },
  {
    _id: 2,
    item: 'Bit',
    value: Decimal128("2.666666"),
    newvalue: '2.666666'
  }
]

#经过比对后,数据无误,我们删除原始 decimal128格式的 value 字段
db.newtable.updateMany(
{ "_id": { $gt: 0 } },

{ $unset: { value : "" } 
}
)

##并将 string 格式的 newvalue 重命名为 value
db.newtable.updateMany(
{ "_id": { $gt: 0 } },

{ $rename: { "newvalue": "value"}
 }
)

##返回两条数据修改完成,本段代码为控制台返回,无需执行
{
  acknowledged: true,
  insertedId: null,
  matchedCount: 2,
  modifiedCount: 2,
  upsertedCount: 0
}

##我们确认一下数据
db.newtable.find();

##返回数据中 value 字段已经是 string 格式,本段代码为控制台返回,无需执行
[
  { _id: 1, item: 'Byte', value: '1.333333' },
  { _id: 2, item: 'Bit', value: '2.666666' }
]
##使用 Mongo Shell 原生客户端登录 Amazon DocumentDB
##Bash Statement,其中 YOUR_DOCUMENTDB_ENDPOINT 请使用您环境的##DocumentDB 终端节点地址替换,YOUR_USER_NAME 请使用您环境的 DocumentDB 用户##替换,本操作使用了 DocumentDB 自定义参数组并关闭了 TLS,在您生产环境,建议保留##TLS 处于启用状态
mongosh --host YOUR_DOCUMENTDB_ENDPOINT -u YOUR_USER_NAME -p

##输入用户密码登陆
Enter password: *************

##DocumentDB Statement
##切换至 poc 数据库
Use poc;

##查看数据表
show tables;

##返回为空,当前我们的数据库中没有数据表存在;

MongoDB 向 DocumentDB 迁移

除了可以使用 MongoDB 原生的 mongodump/mongorestore 进行数据的迁移,我们还可以使用 Amazon Data Migration Service(DMS)以 MongoDB 为数据源,以 Amazon DocumentDB 为数据目标,进行数据迁移,本例采用后者

1.通过控制台找到 DMS 服务,并点选进入 DMS 控制台

在 Amazon DocumentDB 里处理 Decimal128类型数据的解决方案

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

在 Amazon DocumentDB 里处理 Decimal128类型数据的解决方案

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

在 Amazon DocumentDB 里处理 Decimal128类型数据的解决方案

4.点击【创建子网组】,完成子网创建

5.创建复制实例

在 Amazon DocumentDB 里处理 Decimal128类型数据的解决方案

在 Amazon DocumentDB 里处理 Decimal128类型数据的解决方案

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

在 Amazon DocumentDB 里处理 Decimal128类型数据的解决方案

7.创建终端节点

在 Amazon DocumentDB 里处理 Decimal128类型数据的解决方案

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

在 Amazon DocumentDB 里处理 Decimal128类型数据的解决方案

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

在 Amazon DocumentDB 里处理 Decimal128类型数据的解决方案

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

在 Amazon DocumentDB 里处理 Decimal128类型数据的解决方案

在 Amazon DocumentDB 里处理 Decimal128类型数据的解决方案

7.4 使用 Secret Manager 来管理 DocumentDB 的账号信息(可选)

详情可以阅读另一篇专题 blog,请点击这里

  1. 创建迁移任务

在 Amazon DocumentDB 里处理 Decimal128类型数据的解决方案

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

在 Amazon DocumentDB 里处理 Decimal128类型数据的解决方案 8.2 在表映像部分,我们创建一个选择规则,对 poc 数据库下的newtable 数据表做选中,然后点选创建任务。

在 Amazon DocumentDB 里处理 Decimal128类型数据的解决方案

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

在 Amazon DocumentDB 里处理 Decimal128类型数据的解决方案

至此存量数据已经通过本方案结合DMS全部迁移至 DocumentDB 下,并且完成了 Decimal128向 string 数据格式的转换。我们来做一个验证。

##登陆到 DocumentDB
##DocumentDB Statement
mongosh --host YOUR_DOCUMENTDB_ENDPOINT -u YOUR_USER_NAME -p

##输入用户密码登陆
Enter password: *************

##切换至 poc 数据库
use poc;

##查看数据表
show tables;

##数据表已经由 DMS 同步到了 DocumentDB
newtable

##验证一下数据
db.newtable.find();

##结果返回符合我们预期
[
  { _id: 1, item: 'Byte', value: '1.333333' },
  { _id: 2, item: 'Bit', value: '2.666666' }
]

将 Decimal128转换为 Java BigDecimal

通过之前的解决方案,我们已经成功的把 Decimal128转换成为 String 存储在数据库中,实现了精度的保留,但是 string 格式保存的数值无法参与计算,我们应该如何解决这个难题?

在 Java 语言中,Decimal128并不能被直接使用,需要专为 BigDecimal 之后,再进行各类处理与运算。我们知道 Decimal128是基于 String 的一种延展,那 String 能否按照这个思路进行处理呢?

答案是可以的,我们可以借助 Java 的一个公共类 BigDecimal 实现我们的需求。以下为 Java 的示例代码,展示我们如何利用这个公共类,进行格式的双向转换,可供参考。

##Java Code
##Transfer String to Java BigDecimal
##引用 BigDecimal 公共类
Import java.math.BigDecimal;
##定义公共类 String2BD
Public class String2BD{
	public static void main(String[ ] args){
	String inputstring = “12.3456;
	BigDecimal bd = new BigDecimal(inputstring);
	System.out.printIn(bd);
}
}

将输入字符串“12.3456“转换得到数字12.3456,可用于从数据库中读取字符串格式数据后转换为 Java 的 BigDecimal 格式。

##Java Code
##Transfer Java BigDecimal to String
##引用 BigDecimal 公共类
Import java.math.BigDecimal;
##定义公共类 BD2String
Public class BD2String{
	BigDecimal inputbd = new BigDecimal(65.4321)
	String outputstring = inputbd.toString();
	System.out.println(outputstring);
}

将 BigDecimal 格式65.4321转换得到字符串“65.4321“,可将结果以字符串格式存回数据库。

总结

用本方案使用 String 替代了 Decimal128,完成了存量数据的迁移,对于新增数据,在保证效率的前提下,通过 Java 的 BigDecimal 公共类实现 String 与 BigDecimal 的双向转换,解决了 DocumentDB 中需要使用 Decimal128格式的需求。DocumentDB 新功能持续发布中,敬请关注。

参考链接:

1.快速理解 Decimal

www.splashlearn.com/math-vocabu…

2.使用 Secret Manager 来管理 DMS Endpoints

aws.amazon.com/blogs/datab…

3.Java Public Class BigDecimal from Oracle

docs.oracle.com/javase/8/do…

本篇作者

在 Amazon DocumentDB 里处理 Decimal128类型数据的解决方案

付晓明

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