redis中存储小数

2023-06-20,

 在做一个活动的需求时,需要往redis中有序的集合中存储一个小数,结果发现取出数据和存储时的数据不一致

zadd test_2017 1.1 tom
(integer)
zrevrange test_2017 - withscores
) "tom"
) "1.1000000000000001" zadd test_2017 1.2 sam
(integer)
zrevrange test_2017 - withscores
) "sam"
) "1.2"
) "tom"
) "1.1000000000000001"

  是不是很奇怪, 存储tom的score 为1.1,结果为 1.1000000000000001,存储 sam的score为1.2,结果就是1.2

1.19999999999999995559 第16位是9,第17位是5, 四舍五入1.2

1.10000000000000008882 第16位是0,  第17位是8,四舍五入1.10000000000000001

但在php中

<?php
echo 1.1;

确实显示1.1,是不是有点奇怪,后来在php.ini中找到到precision这个选项, 指

; The number of significant digits displayed in floating point numbers.
; http://php.net/precision
precision =

因为在php中,小数都是以double形式存储的,那么1.10000000000000008882中第14位为0,第15位也为0,四舍五入,为1.1

解决方法:php在处理时使用bcmul函数,将小数*100,再存入redis,待取出来时,再除以100

看了下redis  zadd的源码,发现 zadd key score name 这个命令,redis利用strtod这个函数 将score 转为double浮点数

/* This generic command implements both ZADD and ZINCRBY. */
void zaddGenericCommand(redisClient *c, int incr) {
static char *nanerr = "resulting score is not a number (NaN)";
robj *key = c->argv[];
robj *ele;
robj *zobj;
robj *curobj;
double score = , *scores, curscore = 0.0;
int j, elements = (c->argc-)/;
int added = ; if (c->argc % ) {
addReply(c,shared.syntaxerr);
return;
} /* Start parsing all the scores, we need to emit any syntax error
* before executing additions to the sorted set, as the command should
* either execute fully or nothing at all. */
scores = zmalloc(sizeof(double)*elements);
for (j = ; j < elements; j++) {
if (getDoubleFromObjectOrReply(c,c->argv[+j*],&scores[j],NULL)
!= REDIS_OK)
{
zfree(scores);
return;
}
} 。。。。
}
int getDoubleFromObjectOrReply(redisClient *c, robj *o, double *target, const char *msg) {
double value;
if (getDoubleFromObject(o, &value) != REDIS_OK) {
if (msg != NULL) {
addReplyError(c,(char*)msg);
} else {
addReplyError(c,"value is not a valid float");
}
return REDIS_ERR;
}
*target = value;
return REDIS_OK;
} int getDoubleFromObject(robj *o, double *target) {
double value;
char *eptr; if (o == NULL) {
value = ;
} else {
redisAssertWithInfo(NULL,o,o->type == REDIS_STRING);
if (o->encoding == REDIS_ENCODING_RAW) {
errno = ;
value = strtod(o->ptr, &eptr);
if (isspace(((char*)o->ptr)[]) || eptr[] != '\0' ||
errno == ERANGE || isnan(value))
return REDIS_ERR;
} else if (o->encoding == REDIS_ENCODING_INT) {
value = (long)o->ptr;
} else {
redisPanic("Unknown string encoding");
}
}
*target = value;
return REDIS_OK;
}

利用strtod写个小程序

#include <stdio.h>
#include <stdlib.h> int main()
{
char str[] = "1.1 This is test";
char *ptr;
double ret; ret = strtod(str, &ptr);
printf("%.51f",ret); return();
}

<!--
p.p1 {margin: 0.0px 0.0px 0.0px 0.0px; font: 17.0px Menlo}
span.s1 {font-variant-ligatures: no-common-ligatures}
-->

结果是 1.100000000000000088817841970012523233890533447265625

double双精度浮点数据的有效位是16位(针对10进制来说),

也就是 printf("%.16f", ret) 那面上面的数据就是 1.1000000000000001, 也就是根据第17位是8,四舍五入

看了下网上关于strtod源码,感觉返回的就是1.1,但赋值给double类型的 ret,才有了上面的值

浮点数和基本类型数据的存储差别比较大,这里不是说存储形式的差别,而是浮点数存放的时候是要经过运算后再转换成整数的4字节或8字节的形式,然后再存放到内存里。因此,只通过16进制数是看不出来和整数有什么差别

在内存中保存小数,使用的是科学计数法

比如 123.456 用十进制科学计数法可以表达为 1.23456 × 102 ,其中 1.23456 为尾数,10 为基数,2 为指数

在 IEEE 标准中,浮点数是将特定长度的连续字节的所有二进制位分割为特定宽度的符号域,指数域和尾数域三个域,其中保存的值分别用于表示给定二进制浮点数中的符号,指数和尾数。这样,通过尾数和可以调节的指数(所以称为"浮点")就可以表达给定的数值了。具体的格式:

符号位  阶码  尾数  长度
float             1          8        23   32
double          1         11   52   64

http://blog.csdn.net/jjj19891128/article/details/22945441

http://www.cnblogs.com/dolphin0520/archive/2011/10/02/2198280.html

那么,我们先来看32位浮点数 的换算:

1. 从浮点数到16进制数

float  var = 5.2f;

就这个浮点数,我们一步一步将它转换为16进制数。

首先,整数部分5,4位二进制表示为:0101。

其次,小数部分0.2,我们应该学了小数转换为二进制的计算方法,那么就是依次乘以2,取整数部分作为二进制数,取小数部分继续乘以2,一直算到小数结果为0为止。那么对0.2进行计算:

0.2*2 = 0.4 * 2 = 0.8 * 2 = 1.6(0.6) * 2 = 1.2(0.2)*2 = 0.4 * 2 = 0.8 * 2 = 1.6(0.6) * 2 = 1.2 ... ...

                0              0            1                     1                  0             0             1                  1   ... ...

因此,这里把0.2的二进制就计算出来了,结果就为:0.00110011... ... 这里的省略号是你没有办法计算完。二进制序列无限循环,没有到达结果为0的那一天。那么此时我们该怎么办?这里就得取到一定的二进制位数后停止计算,然后舍入。我们知道,float是32位,后面尾数的长度只能最大23位。因此,计算结束的时候,整数部分加上小数部分的二进制一共23位二进制。因此5.2的二进制表示就为:

101.00110011001100110011

一共23位。

此时,使用科学计数法表示,结果为:

1.0100110011001100110011 * 22

由于我们规定,使用二进制科学计数法后,小数点左边必须为1(肯定为1嘛,为0的话那不就是0.xxxx*sxxx 了,这样没有什么意义),这里不能为0是有一个很大的好处的,为什么?因为规定为1,这样这个1就不用存储了,我们在从16进制数换算到浮点数的时候加上这个1就是了,因为我们知道这里应该有个1,省略到这个1的目的是为了后面的小数部分能够多表示一位,精度就更高一些了哟。那么省略到小数点前面的1后的结果为:

.01001100110011001100110 * 22

这里后面蓝色的0就是补上的,这里不是随便补的一个0,而是0.2的二进制在这一位上本来就应该为0,如果该为1,我们就得补上一个1.是不是这样多了一位后,实际上我们用23位表示了24位的数据量。有一个位是隐藏了,固定为1的。我们不必记录它。

但是,在对阶或向右规格化时,尾数要向右移位,这样被右移的尾数的低位部分会被丢掉,从而造成一定的误差,因此要进行舍入处理。 常用的舍入方法有两种:一种是“0舍1入”法,即如果右移时被丢掉数位的最高位为0则舍去,为1则将尾数的末位加“1”,另一种是“恒置1”,即只要数位被移掉,就在尾数的末位恒置“1”。

举个例子:

123.456的二进制表示:

123.456的二进制到23位时:111 1011.0111 0100 1011 1100 01...

后面还有依次为01...等低位,由于最高位的1会被隐藏,向后扩展一位如果不做舍入操作则结果为:

1.11 1011 0111 0100 1011 1100 0 * 26

但是经过舍入操作后,由于被舍掉的位的最高位是1,或者“恒置1”法,最后面的0都应该是1。因此最终就应该是:

1.11 1011 0111 0100 1011 1100 1 * 26

在这里需要说明,不管是恒置1,还是0舍1入法,其根本都是为了减小误差。

好了,尾数在这里就计算好了,他就是 01001100110011001100110 

再来看阶数,这里我们知道是2^2次方,那么指数就是2。同样IEEE标准又规定了,因为中间的 阶码在float中是占8位,而这个 阶码又是有符号的(意思就是说,可以有2^-2次方的形式)。

float 类型的 偏置量 Bias = 2k-1 -1 = 28-1 -1 = 127 ,但还要补上刚才因为左移作为小数部分的 2 位(也就是科学技术法的指数),因此偏置量为 127 + 2=129 ,就是 IEEE 浮点数表示标准:

V = (-1)s × M × 2E

E = e - Bias

中的 e ,此前计算 Bias=127 ,刚好验证了 E = 129 - 127 = 2 。

这里的阶码就是12910 ,二进制就是:1000 00012 。

因此,拼接起来后:

1000 0001 01001100110011001100110

| ←   8位 → | | ←------------- 23位 -------------→ |

一共就是31位了,这里还差一位,那就是符号位,我们定义的是5.2,正数。因此这里最高位是0,1表示负数。

而后结果就是:

  0 1000 0001 01001100110011001100110

1位 | ← 8位 → | | ←-------------- 23位 ------------→ |

到这里,我们内存里面的十六进制数产生了,分开来看:

0 100 0000 1 010 0110 0110 0110 0110 0110

    4       0        A        6       6        6        6        6

因此,我们看到的就是0x40A66666, 此就是5.2最终的整数形式。

网上有个例子:这个例子是计算小数点后60位的,因为1.25中的整数1的二进制就是1, 进位位为0,阶码为11位,还剩 64-1-11=52位,所以有效位为52位

<?php
$bin = "";
$int = ;
$base = ;
for ($i = ; $i <= ; $i++) {
$int = $int * ;
if ($int == $base) {
$bin.="";
break;
}
if ($int >$base) {
$bin.="";
$int = $int - $base;
} else {
$bin .= "";
}
} echo $bin;
echo "\n";
echo "现在的长度是".strlen($bin);
echo "\n"; echo"\n"; echo "52位长度的二进制\n";
$bin=substr($bin,,);
echo $bin."\n"; $f = ;
$l = strlen($bin);
for ($i = ; $i < $l; $i++) {
if ($bin[$i] > ) {
$f = $f + pow(, -($i + ));
}
}
echo "反计算后数值\n";
echo number_format($f, );
echo "\n"; echo "1.15本身的30位数据\n";
$f = 1.15;
echo number_format($f, );
echo "\n";

结果

<!--
p.p1 {margin: 0.0px 0.0px 0.0px 0.0px; font: 16.0px Menlo}
p.p2 {margin: 0.0px 0.0px 0.0px 0.0px; font: 16.0px Menlo; min-height: 19.0px}
span.s1 {font-variant-ligatures: no-common-ligatures}
-->

0010011001100110011001100110011001100110011001100110011001100

现在的长度是61

52位长度的二进制

0010011001100110011001100110011001100110011001100110

反计算后数值

1.149999999999999911182158029987

1.15本身的30位数据

1.149999999999999911182158029987

经比较发现,61位的二进制和52的二进制 反向计算后的结果是一样的

<!--
p.p1 {margin: 0.0px 0.0px 0.0px 0.0px; font: 17.0px Menlo}
span.s1 {font-variant-ligatures: no-common-ligatures}
-->

redis中存储小数的相关教程结束。

《redis中存储小数.doc》

下载本文的Word格式文档,以方便收藏与打印。