Redis SortedSet 中 score 的精度问题

一、问题发现

通过 jedis 往 sortedset 中添加了个成员,并且设定了一个Double 类型的分数时,精度出现了问题
测试代码如下:

@Test
public void zadd(){
    jedis.zadd("test:cli", 13.36, "mb1");
}

如果用 jedis 的 api 来获取 score 的话一切正常

@Test
public void zscore(){
    System.out.println(jedis.zscore("test:cli", "mb1"));
}

输出结果:

13.36

但是如果通过 redis-cli 去查看的时候精度是有问题的:

181.137.128.153:7002>
181.137.128.153:7002> zrange test:cli 0 -1 WITHSCORES
1) “mb1”
2) “13.359999999999999”
181.137.128.153:7002>


二、源码探寻

如果不想看源码,可以跳过这里直接看第四节的结论哈~

1、zadd

先来看看是不是数据插入时导致的精度问题

@Test
public void zadd(){
    jedis.zadd("test:cli", 13.36, "mb1");
}

点击zadd方法

@Override
public Long zadd(final String key, final double score, final String member) {
  return new JedisClusterCommand<Long>(connectionHandler, maxAttempts) {
    @Override
    public Long execute(Jedis connection) {
      return connection.zadd(key, score, member);
    }
  }.run(key);
}

继续点击zadd方法

public Long zadd(final String key, final double score, final String member) {
  checkIsInMultiOrPipeline();
  client.zadd(key, score, member);
  return client.getIntegerReply();
}

继续点击zadd方法

public void zadd(final String key, final double score, final String member) {
  zadd(SafeEncoder.encode(key), score, SafeEncoder.encode(member));
}

继续点击zadd方法

public void zadd(final byte[] key, final double score, final byte[] member) {
  //将数据转成 byte[] 后,再发送给 redis server
  sendCommand(ZADD, key, toByteArray(score), member);
}

这里写图片描述
我们发现插入到 redis 时是没有问题的!!
ok~


2、zscore

接下来我们来查看一下从 redis server 获取数据时是不是有精度问题

@Test
public void zscore(){
    System.out.println(jedis.zscore("test:cli", "mb1"));
}

点击 zscore() 方法

@Override
public Double zscore(final String key, final String member) {
  return new JedisClusterCommand<Double>(connectionHandler, maxAttempts) {
    @Override
    public Double execute(Jedis connection) {
      return connection.zscore(key, member);
    }
  }.run(key);
}

再点击 zscore() 方法

public Double zscore(final String key, final String member) {
  checkIsInMultiOrPipeline();
  client.zscore(key, member);
  //该方法先得到 String 类型的数据
  final String score = client.getBulkReply();
  //然后再转成 Double 类型
  return (score != null ? new Double(score) : null);
}

点击 getBulkReply() 方法

public String getBulkReply() {
  //server 返回的是 byte[]
  final byte[] result = getBinaryBulkReply();
  if (null != result) {
    return SafeEncoder.encode(result);
  } else {
    return null;
  }
}

这里写图片描述

!!!
发现 server 返回给 client 的就是精度有问题的!震惊!


三、再度探究

细心的同学可能会发现,诶,之前用 jedis api 获取时都是没有精度问题的,怎么会出现这种情况呢?
我们可以运行下面的程序看看就知道了:

@Test
public void zdouble(){
      String score = "13.359999999999999";
      System.out.println(new Double(score));
}

输出结果是:

13.36

值是正确但问题是,redis server居然给 client 返回的是精度有问题的!!


于是我猜测是 Redis 内部精度把控有问题。
请看下面的验证(redis-cli):

181.137.128.153:7002> keys *
(empty list or set)
181.137.128.153:7002> zadd test:key 13.36 mb1
(integer) 1
181.137.128.153:7002> zrange test:key 0 -1 WITHSCORES
1) “mb1”
2) “13.359999999999999”
181.137.128.153:7002>
181.137.128.153:7002>
181.137.128.153:7002> zadd test:key 13.35 mb2
(integer) 1
181.137.128.153:7002> zrange test:key 0 -1 WITHSCORES
1) “mb2”
2) “13.35”
3) “mb1”
4) “13.359999999999999”
181.137.128.153:7002>
181.137.128.153:7002> zscore test:key mb1
“13.359999999999999”
181.137.128.153:7002>
181.137.128.153:7002>

发现,就是 Redis 内部精度的问题!!


下面我们来看一下 redis 中自动帮我们累加 score 的 zincrby() 方法会不会也有精度问题:

public static void main(String[] args) throws Exception {
        JedisCluster jedis = JedisClusterUtil.getJedisCluster();
        //每次添加的值
        double addValue = 13.03;
        String key = "test:cli:1";
        String member = "mb1";
        //score设置为 405887.59
        jedis.zadd(key, 405887.59, member);
        /**
         * 对 member 成员不断累加值,累计后获得最新值,如果前后的差值不等于 addValue 则退出
         */
        while (true){
            Double k1 = jedis.zscore(key, member);
            //让程序自动帮我们累加
            jedis.zincrby(key, addValue, member);
            Double k2 = jedis.zscore(key, member);

            /**
             * 如果redis api内部帮我们累加的值不等于 addValue 则退出
             * 注意用 BigDecimal进行操作
             */
            if (cha(k2, k1) != addValue){
                System.out.println("k1 = " + k1);
                System.out.println("k2 = " + k2);
                break;
            }
        }
    }

    /**
     * 求差值
     * 注意,用BigDecimal类来进行double的运算
     * @param d1
     * @param d2
     * @return d1 - d2
     */
    public static Double cha(double d1, double d2){
        Double cha = new BigDecimal(String.valueOf(d1)).subtract(new BigDecimal(String.valueOf(d2))).doubleValue();
        System.out.println("cha = "+cha);
        return cha;
    }
}

输出为:

cha = 13.03000000005
k1 = 405887.59
k2 = 405900.62000000005

发现,如果让程序内部自动帮我们累加 Double,那精度也会出现问题


四、解决办法

  1. 建议 将 Double 类型转换成 Long 类型后再保存到 Redis,然后获取值时再通过 BigDecimal 将值乘以 0.01 : 这样不管是 zadd 或者是 zincrby,都没有精度问题
  2. 如果硬是要用 Double 类型的话,不要用 redis 提供的 increase 方法,有精度问题
  3. 如果不把 Double 转换成 Long 类型的话,那么我们自己用BigDecimal类来操作 score,然后调用 zadd() 方法。


注:上面的代码都是基于 redis 的集群模式来测试的,且 jedis 的版本是2.9.0。关于 jedis 的获取可以查看我的另一篇文章哈~

猜你喜欢

转载自blog.csdn.net/Lin_wj1995/article/details/80046443