Redis 浮点数累计实现
来源:51cto
时间:2023-08-08 13:30:07 181浏览 收藏
小伙伴们对数据库编程感兴趣吗?是否正在学习相关知识点?如果是,那么本文《Redis 浮点数累计实现》,就很适合你,本篇文章讲解的知识点主要包括Redis、数据库。在之后的文章中也会多多分享相关知识点,希望对大家的知识积累有所帮助!
Redis 浮点数累计主要是有两个命令
- INCRBYFLOAT 是 SET 指令的浮点数累计
- HINCRBYFLOAT 是 HASH 类型的浮点数累计
在内部 HINCRBYFLOAT 和 INCRBYFLOAT 自增实现相同。所以我们分析 INCRBYFLOAT 即可。
基本使用
直接使用指令。
INCRBYFLOAT mykey 0.1
INCRBYFLOAT mykey 1.111
INCRBYFLOAT mykey 1.111111
使用 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"
INCRBYFLOAT 可表示范围
按照官方文档的说法 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);
}
源码地址: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
输出结果:
累计结果正确, 整数位: 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.99999999999999999
INCRBYFLOAT 导致精度丢失
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);
在 RedisTemplate 的这个 increment 接受的参数类型就是一个 double 所以会发生精度问题
C 语言长双精度类型
因为 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 之所以没有精度问题,主要还是源于其更大的范围和更高的内在精度。
问题总结
- Redis 浮点数累计操作 INCRBYFLOAT 不适合精度要求比较高的金额计算。
- Redis 浮点数累计操作 INCRBYFLOAT 也不能平替 BigDecimal 计算,如果一定需要存储可以考虑通过 lua 脚本实现 CAS 进行修改,最终存储为 String 类型的一个结果。
- Redis 的浮点数虽然做了比较好的优化,但是没有从根本解决计算精度问题。
参考文档
- https://redis.io/commands/incrbyfloat/。
- https://wiki.c2.com/?BankersRounding。
- https://www.wikihow.com/Round-to-the-Nearest-Tenth。
- https://learn.microsoft.com/zh-cn/cpp/c-language/type-long-double?view=msvc-170。
- https://learn.microsoft.com/zh-cn/cpp/c-runtime-library/reference/strtold-strtold-l-wcstold-wcstold-l?view=msvc-170。
以上就是《Redis 浮点数累计实现》的详细内容,更多关于redis的资料请关注golang学习网公众号!
-
286 收藏
-
235 收藏
-
117 收藏
-
138 收藏
-
185 收藏
-
342 收藏
-
361 收藏
-
159 收藏
-
164 收藏
-
221 收藏
-
156 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 542次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 507次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 497次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 484次学习