Accuracy of score in Redis SortedSet

1. Problem discovery

When a member is added to the sortedset through jedis, and a score of Double type is set, there is a problem with the accuracy. The
test code is as follows:

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

If you use jedis's api to get the score, everything works fine

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

Output result:

13.36

But if you look at it through redis-cli, the accuracy is problematic:

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>


Second, source code exploration

If you don't want to look at the source code, you can skip here and go directly to the conclusion of Section 4~

1、zadd

Let's first see if it is a precision problem caused by data insertion

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

Click on the zadd method

@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);
}

Continue to click on the zadd method

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

Continue to click on the zadd method

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

Continue to click on the zadd method

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

write picture description here
We found no problem inserting into redis! !
ok~


2、zscore

Next, let's see if there is an accuracy problem when getting data from the redis server

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

Click on the zscore() method

@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);
}

Then click on the zscore() method

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);
}

Click on the getBulkReply() method

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

write picture description here

! ! !
It is found that what the server returns to the client has a problem with the accuracy! Shock!


3. Re-exploration

Careful students may find that, hey, there was no accuracy problem when using jedis api to get it before. How could this happen?
We can find out by running the following program:

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

The output is:

13.36

The value is correct but the problem is that the redis server actually returns the client with an accuracy problem! !


So I guess that there is a problem with the internal precision control of Redis.
See the verification below (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>

It is found that it is the problem of the internal precision of Redis! !


Let's take a look at whether the zincrby() method in redis that automatically accumulates scores for us will also have accuracy problems:

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;
    }
}

The output is:

cha = 13.03000000005
k1 = 405887.59
k2 = 405900.62000000005

It is found that if we let the program automatically accumulate Double for us, then the accuracy will also have problems.


Fourth, the solution

  1. It is recommended to convert the Double type to Long type and then save it to Redis, and then multiply the value by 0.01 through BigDecimal when getting the value: this way, whether it is zadd or zincrby, there is no precision problem
  2. If you insist on using the Double type, don't use the increase method provided by redis, there are precision problems
  3. If we don't convert Double to Long type, then we use the BigDecimal class to operate the score, and then call the zadd() method.


Note: The above code is tested based on the cluster mode of redis, and the version of jedis is 2.9.0. About the acquisition of jedis, you can check my other article ~

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=324797292&siteId=291194637