
Redis 浮点数累计主要是点数有两个命令
INCRBYFLOAT 是 SET 指令的浮点数累计HINCRBYFLOAT 是 HASH 类型的浮点数累计在内部 HINCRBYFLOAT 和 INCRBYFLOAT 自增实现相同。所以我们分析 INCRBYFLOAT 即可。累计
直接使用指令。实现
复制INCRBYFLOAT mykey 0.1 INCRBYFLOAT mykey 1.111 INCRBYFLOAT mykey 1.1111111.2.3.使用 lua 脚本的点数方式,因为 redis 可以通过 lua 脚本来保证操作的累计原子性,所以当我们同时操作多个 key 的实现时候一般使用 lua 脚本的方式。
复制eval "return redis.call(INCRBYFLOAT,点数 KEYS[1], ARGV[1])" 1 mykey1 "1.11" eval "return redis.call(INCRBYFLOAT, KEYS[1], ARGV[1])" 1 mykey1 "1.11111" eval "return redis.call(INCRBYFLOAT, KEYS[1], ARGV[1])" 1 mykey1 "1.11111"1.2.3.按照官方文档的说法 INCRBYFLOAT 可以表示小数位 17 位。比如按照 jedis 的累计 api 来说,我们能够使用的实现就是在 double 的精度范围内,也就是点数 15-16位。这里我也看了 redis 的累计源码,他在底层实现是实现通过 c 语言的 long double 类型来进行计算的。
复制void incrbyfloatCommand(client *c) { long double incr,点数 value; robj *o, *new; o = lookupKeyWrite(c->db,c->argv[1]); if (checkType(c,o,OBJ_STRING)) return; if (getLongDoubleFromObjectOrReply(c,o,&value,NULL) != C_OK || getLongDoubleFromObjectOrReply(c,c->argv[2],&incr,NULL) != C_OK) return; value += incr; if (isnan(value) || isinf(value)) { addReplyError(c,"increment would produce NaN or Infinity"); return; } new = createStringObjectFromLongDouble(value,1); if (o) dbReplaceValue(c->db,c->argv[1],new); else dbAdd(c->db,c->argv[1],new); signalModifiedKey(c,c->db,c->argv[1]); notifyKeyspaceEvent(NOTIFY_STRING,"incrbyfloat",c->argv[1],c->db->id); server.dirty++; addReplyBulk(c,new); /* Always replicate INCRBYFLOAT as a SET command with the final value * in order to make sure that differences in float precision or formatting * will not create differences in replicas or after an AOF restart. */ rewriteClientCommandArgument(c,0,shared.set); rewriteClientCommandArgument(c,2,new); rewriteClientCommandArgument(c,3,shared.keepttl); }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.源码地址:https://github.com/redis/redis/blob/unstable/src/t_string.c long double 是 c 语言的长双精度浮点型,亿华云在 x86 的累计 64 位操作系统上占通常占用 16 字节(128 位),相较于 8 字节的实现 double 类型具有更大的范围和更高的精度。(这部分来源于 chatgpt) 因为 redis 采用的 long double 类型来做浮点数计算, 所以 redis 就可以保证到小数点后 17 位的精度。 整数位也可以表示 17 位 redis 的浮点数计算通常情况下会丢失精度吗? 通常情况下是不会的,但是不能保证一定不会。
测试代码如下:
复制public class RedisIncrByFloatTest { public static void main(String[] args) { Jedis jedis = new Jedis("127.0.0.1", 6379); BigDecimal decimalIncr = java.math.BigDecimal.ZERO; String key = "IncrFloat:Digit100"; //测试精度 test_accuracy(jedis, decimalIncr, key); //测试正浮点数最大值 test_max_positive_float(jedis, decimalIncr, key); jedis.disconnect(); jedis.close(); } private static void test_max_positive_float(Jedis jedis, BigDecimal decimalIncr, String key) { jedis.del(key); String value = "99999999999999999.00000000000000003"; List<String> evalKeys = Collections.singletonList(key); List<String> evalArgs = Collections.singletonList(value); String luaStr = "redis.call(INCRBYFLOAT, KEYS[1], ARGV[1]) return redis.call(GET, KEYS[1])"; Object result = jedis.eval(luaStr, evalKeys, evalArgs); decimalIncr = decimalIncr.add(new BigDecimal(value)); BigDecimal redisIncr = new BigDecimal(String.valueOf(result)); value = "0.99999999999999996"; evalKeys = Collections.singletonList(key); evalArgs = Collections.singletonList(value); luaStr = "redis.call(INCRBYFLOAT, KEYS[1], ARGV[1]) return redis.call(GET, KEYS[1])"; result = jedis.eval(luaStr, evalKeys, evalArgs); decimalIncr = decimalIncr.add(new BigDecimal(value)); redisIncr = new BigDecimal(String.valueOf(result)); boolean eq = comparteNumber(redisIncr, decimalIncr); if (eq) { System.out.println("累计结果正确, 整数位: " + 17 + "位, 结果期望值: decimalIncr " + decimalIncr.toPlainString() + ", 目标值(redis):" + redisIncr.toPlainString()); } else { System.out.println("累计结果不正确, 整数位: " + 17 + "位, 期望值: decimalIncr " + decimalIncr.toPlainString() + ", 目标值(redis):" + redisIncr.toPlainString()); } } private static void test_accuracy(Jedis jedis, BigDecimal decimalIncr, String key) { jedis.del(key); for (int i = 16; i < 30; i++) { String value = createValue(i); final List<String> evalKeys = Collections.singletonList(key); final List<String> evalArgs = Collections.singletonList(value); String luaStr = "redis.call(INCRBYFLOAT, KEYS[1], ARGV[1]) return redis.call(GET, KEYS[1])"; Object result = jedis.eval(luaStr, evalKeys, evalArgs); decimalIncr = decimalIncr.add(new BigDecimal(value)); BigDecimal redisIncr = new BigDecimal(String.valueOf(result)); boolean eq = comparteNumber(redisIncr, decimalIncr); if (eq) { System.out.println("累计结果正确, 整数位: " + i + "位, 结果期望值: decimalIncr " + decimalIncr.toPlainString() + ", 目标值(redis):" + redisIncr.toPlainString()); } else { System.out.println("累计结果不正确, 整数位: " + i + "位, 期望值: decimalIncr " + decimalIncr.toPlainString() + ", 目标值(redis):" + redisIncr.toPlainString()); break; } } } private static String createValue(int i) { String result = "9" + "0".repeat(Math.max(0, i - 1)); return result + ".00000000000000003"; } private static boolean comparteNumber(BigDecimal redisIncr, BigDecimal decimalIncr) { return decimalIncr.compareTo(redisIncr) == 0; } }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.40.41.42.43.44.45.46.47.48.49.50.51.52.53.54.55.56.57.58.59.60.61.62.63.64.65.66.67.68.69.70.71.72.73.74.75.输出结果:
复制累计结果正确, 整数位: 16位, 结果期望值: decimalIncr 9000000000000000.00000000000000003, 目标值(redis):9000000000000000.00000000000000003 累计结果正确, 整数位: 17位, 结果期望值: decimalIncr 99000000000000000.00000000000000006, 目标值(redis):99000000000000000.00000000000000006 累计结果不正确, 整数位: 18位, 期望值: decimalIncr 999000000000000000.00000000000000009, 目标值(redis):999000000000000000 累计结果正确, 整数位: 17位, 结果期望值: decimalIncr 99999999999999999.99999999999999999, 目标值(redis):99999999999999999.999999999999999991.2.3.4.INCRBYFLOAT 导致精度丢失有两种情况:
累计的范围值超过 INCRBYFLOAT 所能表示的最大精度范围,在 double 范围内。INCRBYFLOAT 底层计算是通过long double 来计算的在 C语言中 long double占用128 位,其范围为: 最小值: ±5.4×10^-4951 最大值: ±1.1×10^4932 能表示的有效数字在34~35位之间。
我们使用类似 jedis 的 api 提供的是 double 类型的参数,可能在调用之前,参数转换的过程就发生了精度问题。比如 复制StringRedisTemplate template = new StringRedisTemplate(); template.opsForValue().increment("v1", 1.3D);1.2.在 RedisTemplate 的云服务器这个 increment 接受的参数类型就是一个 double 所以会发生精度问题
因为 redis 底层采用的是long double 计算,所以这个问题转化为长双精度(long double)为什么没有精度问题? 这是因为 long double 具有更大的范围和更高的精度。long double 的范围和精度高于 double 类型:
范围更大:long double 可以表示更大和更小的数字精度更高:long double 可以表示的有效数字多于 double 类型这意味着,对于同样的浮点计算,long double 具有更少的舍入误差。具体来说,几点原因造成 long double 没有精度问题:
long double 使用更多的bit位来表示浮点数。long double 使用四舍五入(rounding to nearest)而不是银行家舍入(bankers rounding),导致更少的误差累加。许多编译器及 CPU 针对 long double 具有优化, 会生成精度更高的机器码来执行 long double 计算。long double 内部采用更大的指数域, 能更准确地表示相同范围内的数字。综上,long double 的更广范围和更高精度,让它在相同的浮点计算中具有更少的舍入误差。这也就解释了为什么 long double 没有明显的精度问题,因为它天生就是为了提供更高精度而设计的。网站模板相比之下,double 使用的位数相对有限,即使采用折中舍入法,在一些场景下它的误差也可能累加显著。所以总的来说,long double 之所以没有精度问题,主要还是源于其更大的范围和更高的内在精度。
相关文章: