Redis SortedSet 中 score 的精度问题

本文探讨了使用Jedis向Redis的Sorted Set中添加带有Double类型分数的成员时出现的精度问题。通过对源码的深入分析,发现了Redis内部处理Double类型数据时存在的精度丢失问题,并提供了相应的解决方案。

一、问题发现

通过 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 的获取可以查看我的另一篇文章哈~

### Redis Sorted Set 数据结构 Sorted Set 是 Redis 中一种有序集合数据结构,其中每个成员都是唯一的字符串元素,并关联一个分数(score),用于确定该成员在集合中的顺序[^1]。当多个成员具有相同的分数时,则按照字典序排列。 #### 特点 - 成员唯一性:不允许重复的成员存在。 - 排序功能:基于 score 对成员自动排序。 - 双向索引:支持按分值范围查询以及按排名获取成员。 ### 使用方法 为了操作 Sorted Set,在命令前缀通常会带有 `Z` 字母来表示这是一个针对 ZSet 的指令集。下面列举了一些常用的命令: - **添加/更新成员** 添加新成员到 sorted set 或者更新已存在的成员分数: ```bash ZADD key [NX|XX] [CH] [INCR] score member [[score] member ...] ``` - **删除成员** 移除指定的一个或多个成员: ```bash ZREM key member [member ...] ``` - **获取成员数量** 获取当前 sorted set 中含有的成员总数: ```bash ZCARD key ``` - **计算给定区间内成员的数量** 计算某个分数区间的成员数: ```bash ZCOUNT key min max ``` - **返回指定位置上的成员及其分数** 返回排名位于 start 和 stop 范围内的所有成员(带分数): ```bash ZRANGE key start stop [WITHSCORES] ``` - **移除并弹出最小得分项** 弹出并返回拥有最低分数的那个成员: ```bash ZPOPMIN key [count] ``` - **移除并弹出最大得分项** 弹出并返回拥有最高分数的那个成员: ```bash ZPOPMAX key [count] ``` ### 示例代码 这里给出一段 Python 代码示例展示如何利用 redis-py 库与 Redis 进行交互,完成对 Sorted Set 基本的操作: ```python import redis # 创建连接对象 r = redis.Redis(host='localhost', port=6379, db=0) # 向名为 'myzset' 的 sorted set 插入三个不同分数的成员 r.zadd('myzset', {'one': 1}) r.zadd('myzset', {'two': 2}) r.zadd('myzset', {'three': 3}) # 查询 myzset 所有成员及对应分数 print(r.zrange('myzset', 0, -1, withscores=True)) # 删除成员 two 并验证其已被成功移除 r.zrem('myzset', 'two') if not r.exists('two'): print("Member 'two' has been removed.") # 统计剩余成员数目 print(f"Current size of the zset is {r.zcard('myzset')}") ```
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值