在电商或者金融相关的场景中,商品价格等数据都会涉及到小数的表示或者计算,如果使用编程语言内置的浮点数类型,会有精度丢失的风险。在应用领域,decimal
类型应运而生,MySQL 数据库中内置支持 decimal
数据类型,而程序设计上,一般编程语言都会有标准库或者第三方库对 decimal
类型提供实现。本文快速展示下如何实现全链路对 decimal
类型数据的读取处理,而不用担心会丢失数据的精度。
在 MySQL 层,decimal
类型的值使用二进制表示,其大致转换过程是:
1234567890.1234
,分为 1234567890
和 1234
;1234567890
将分为 1
和 234567890
;1
即 0b00000001
,而 234567890
则对应 0x0D-FB-38-D2
;0x04D2
;0x81 0D FB 38 D2 04 D2
,也就是使用了 7 个字节来表示这个数字。Bonus: 如果是小数,比如 -1234567890.1234
,则只需要将上面第 5 步的所有位置反即可,也就是 0x7E F2 04 C7 2D FB 2D
MySQL 通过设计巧妙的可变长度的二进制转换,实现了对严格要求精度的小数的表示。
存储在 MySQL 底层存储上的 decimal,我们知道是二进制了之后,也就对精度问题的持久化存储放心了,但是,又带来两个问题:
答案很简单:纯文本。
通过对 MySQL 连接进行抓包,可以确认这一点,截图是通过 Wireshark 抓包 MySQL 服务器返回的 decimal 数据:
因为使用了纯文本传输数据,所以不用担心小数在传输过程中会有精度问题
在我的应用程序里,我使用了 golang 来开发程序,依赖了 shopspring/decimal 包来处理 decimal 类型,而且它同时实现了 sql.Scanner 接口,也就意味着我可以直接用它完成对数据库查询返回数据的反序列化。比如我的代码里:
// Order ...
type Order struct {
OrderNo string
PurchaseAmount decimal.Decimal
Status uint8
}
order := new(Order)
db.Where("order_no = ?", orderNo).First(order).Error
不需要额外的逻辑,PurchaseAmount
能够精确地反序列化 decimal 类型的数据。
尽管如此,我还是看了下 shopspring/decimal 包里对 Scanner
接口的实现,以确认它确实是安全的:
首先,我在源码处加了两行代码,以方便我确认底层数据的类型,确认反序列前,是一个字节序列:
之后,我跟踪了代码的执行,可以看到 decimal 包按照字符串的方式对数据直接进行了反序列化:
考虑到整数的溢出以及浮点数精度损失风险,我在对外服务的协议规范上,也都统一使用字符串类型。
message Order {
string order_no = 1;
string purchase_amount = 2;
status int32 = 3;
}
shopspring/decimal
实现 decimal 类型数据的处理;shopspring/decimal
底层使用了科学计数法表示 decimal,但是本文就不展开了;