在开测项目服务端的开发中,redis 的 zset 是常用的数据结构。因为它元素不重复且每个元素都有一个分数的特点,经常作为有序队列和元素排序来使用,排序的方式自然是通过每个元素的 score 的大小。

一、score 的数据类型

在计算机中,字符都是可以比较大小的,那么 score 的数据类型是不是只要是字符型就可以的呢?答案是否定的。如下图,当我们使用非数字类型的时候会提示 “(error) value is not a valid float” 的错误信息。同时这个错误信息告诉我们,score 类型是浮点型的数据。

既然是浮点型的数据都可以,那么我们使用 int,long,float,double 等数字类型,当然都是可以的。但是浮点型数据自然都是有精度的,且会影响我们的业务开发。
Ps: 在相关的参考资料中有说是 double 类型的,有说是数字转成字符串类型存到 redis 中,查询时返回是字符串类型,从结果上观察都是成立的。但是在 Jedis 驱动中入参和出参使用的都是 double 数据类型。

二、java 的数据精度与 redis 的数据精度

先看一个有趣的例子,如下图,使用 java 代码,声明两个 double 的变量 a 和 b,值都是 27 个数字,a 的末位数字比 b 的末位数字大 1,其他位置的数字相同,a 是大于 b 的,但是代码输出的结果是 a 等于 b,a 和 b 也变成了 18 个有效数字的科学计数。

除了上面的问题,在项目中用到 double 类型数据四则运算时,经常出现精度丢失的问题,总是在一个正确的结果左右偏 0.0000**1。

其输出结果如下:

在商业型软件中,这种精度问题,会有很大的影响。在《Effective Java》这本书提供了解决方法,float 和 double 只能用来做科学计算或者是工程计算,在商业计算中我们要用 java.math.BigDecimal。但是在 Jedis 驱动中,使用的是 double 作为形参的,我们执行像 zset 中插入 3.14 的操作,代码如下。

我们通过 redis-cli 去查看的时候,结果显示的是 3.1400000000000001,但是 java 读取出来的时候显示的是 3.14。

通过 debug,我们发现,并不是数据插入时导致的精度问题,而是从 redis 中读取的时候出现的问题,由此我们可以猜测是 redis 自身的浮点型精度的问题。

我们可以通过 redis-cli 来验证一下:

果然如前面猜测的一样,redis 自身的浮点型精度是有问题的,并且做 zincrby 的时候也是有精度问题的。

三、开测中使用的一个例子

笔者在项目中,目前没有遇到因为精度而导致的问题,只是在开测的服务端开发中,会有使用时间戳作为 score 来进行任务的排序,时间戳由 14 位数字组成的长整型,向上转型成 double,存入 redis 中会变成成科学计数法,为了避免精度丢失的问题,才进行了相关实验。
经过实验,当 zset 中的长整型数字位数超过 17 位的时候才会出现精度丢失的问题,如下图。

此结果与 java 的 double 类型保持一致,所以使用时间戳作为分数不会造成精度丢失的问题,同时,我们应该避免使用超过 17 位的数字作为分数。如果不得不使用,笔者认为一般是数据量特别大、使用 uuid,或者是精度要求比较高,这些情况可以通过一些算法做预处理,只要达到预期的效果就行。

四、小结

本文对 redis 的 zset 的 score 的数据精度问题做了简单的分析和总结,希望能在以后的开发中,能够避免因为 score 精度而引起业务问题。


↙↙↙阅读原文可查看相关链接,并与作者交流