一、问题发现
通过 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,那精度也会出现问题
四、解决办法
- 建议 将 Double 类型转换成 Long 类型后再保存到 Redis,然后获取值时再通过 BigDecimal 将值乘以 0.01 : 这样不管是 zadd 或者是 zincrby,都没有精度问题
- 如果硬是要用 Double 类型的话,不要用 redis 提供的 increase 方法,有精度问题
- 如果不把 Double 转换成 Long 类型的话,那么我们自己用BigDecimal类来操作 score,然后调用 zadd() 方法。
注:上面的代码都是基于 redis 的集群模式来测试的,且 jedis 的版本是2.9.0。关于 jedis 的获取可以查看我的另一篇文章哈~