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

  1. zadd test_2017 1.1 tom
  2. (integer)
  3. zrevrange test_2017 - withscores
  4. ) "tom"
  5. ) "1.1000000000000001"
  6.  
  7. zadd test_2017 1.2 sam
  8. (integer)
  9. zrevrange test_2017 - withscores
  10. ) "sam"
  11. ) "1.2"
  12. ) "tom"
  13. ) "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中

  1. <?php
  2. echo 1.1;

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

  1. ; The number of significant digits displayed in floating point numbers.
  2. ; http://php.net/precision
  3. 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浮点数

  1. /* This generic command implements both ZADD and ZINCRBY. */
  2. void zaddGenericCommand(redisClient *c, int incr) {
  3. static char *nanerr = "resulting score is not a number (NaN)";
  4. robj *key = c->argv[];
  5. robj *ele;
  6. robj *zobj;
  7. robj *curobj;
  8. double score = , *scores, curscore = 0.0;
  9. int j, elements = (c->argc-)/;
  10. int added = ;
  11.  
  12. if (c->argc % ) {
  13. addReply(c,shared.syntaxerr);
  14. return;
  15. }
  16.  
  17. /* Start parsing all the scores, we need to emit any syntax error
  18. * before executing additions to the sorted set, as the command should
  19. * either execute fully or nothing at all. */
  20. scores = zmalloc(sizeof(double)*elements);
  21. for (j = ; j < elements; j++) {
  22. if (getDoubleFromObjectOrReply(c,c->argv[+j*],&scores[j],NULL)
  23. != REDIS_OK)
  24. {
  25. zfree(scores);
  26. return;
  27. }
  28. }
  29.  
  30. 。。。。
  31. }
  1. int getDoubleFromObjectOrReply(redisClient *c, robj *o, double *target, const char *msg) {
  2. double value;
  3. if (getDoubleFromObject(o, &value) != REDIS_OK) {
  4. if (msg != NULL) {
  5. addReplyError(c,(char*)msg);
  6. } else {
  7. addReplyError(c,"value is not a valid float");
  8. }
  9. return REDIS_ERR;
  10. }
  11. *target = value;
  12. return REDIS_OK;
  13. }
  14.  
  15. int getDoubleFromObject(robj *o, double *target) {
  16. double value;
  17. char *eptr;
  18.  
  19. if (o == NULL) {
  20. value = ;
  21. } else {
  22. redisAssertWithInfo(NULL,o,o->type == REDIS_STRING);
  23. if (o->encoding == REDIS_ENCODING_RAW) {
  24. errno = ;
  25. value = strtod(o->ptr, &eptr);
  26. if (isspace(((char*)o->ptr)[]) || eptr[] != '\0' ||
  27. errno == ERANGE || isnan(value))
  28. return REDIS_ERR;
  29. } else if (o->encoding == REDIS_ENCODING_INT) {
  30. value = (long)o->ptr;
  31. } else {
  32. redisPanic("Unknown string encoding");
  33. }
  34. }
  35. *target = value;
  36. return REDIS_OK;
  37. }

利用strtod写个小程序

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3.  
  4. int main()
  5. {
  6. char str[] = "1.1 This is test";
  7. char *ptr;
  8. double ret;
  9.  
  10. ret = strtod(str, &ptr);
  11. printf("%.51f",ret);
  12.  
  13. return();
  14. }

结果是 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位

  1. <?php
  2. $bin = "";
  3. $int = ;
  4. $base = ;
  5. for ($i = ; $i <= ; $i++) {
  6. $int = $int * ;
  7. if ($int == $base) {
  8. $bin.="";
  9. break;
  10. }
  11. if ($int >$base) {
  12. $bin.="";
  13. $int = $int - $base;
  14. } else {
  15. $bin .= "";
  16. }
  17. }
  18.  
  19. echo $bin;
  20. echo "\n";
  21. echo "现在的长度是".strlen($bin);
  22. echo "\n";
  23.  
  24. echo"\n";
  25.  
  26. echo "52位长度的二进制\n";
  27. $bin=substr($bin,,);
  28. echo $bin."\n";
  29.  
  30. $f = ;
  31. $l = strlen($bin);
  32. for ($i = ; $i < $l; $i++) {
  33. if ($bin[$i] > ) {
  34. $f = $f + pow(, -($i + ));
  35. }
  36. }
  37. echo "反计算后数值\n";
  38. echo number_format($f, );
  39. echo "\n";
  40.  
  41. echo "1.15本身的30位数据\n";
  42. $f = 1.15;
  43. echo number_format($f, );
  44. echo "\n";

结果

0010011001100110011001100110011001100110011001100110011001100

现在的长度是61

52位长度的二进制

0010011001100110011001100110011001100110011001100110

反计算后数值

1.149999999999999911182158029987

1.15本身的30位数据

1.149999999999999911182158029987

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

redis中存储小数的更多相关文章

  1. 往redis中存储数据是利用pipeline方法

    在redis中保存数据时,保存和设置有效时间是分开写的话,如果中间出现的异常,这会导致数据永久有效,因此就可以采用pipeline方法. # 创建redis管道对象,可以一次执行多个语句 pipeli ...

  2. flask-session 在redis中存储session

    依赖: Flask Flask-Session redis import os from flask import Flask, session, request from flask_session ...

  3. Redis中存储对象区别

    1.最常用的是String结构,key和value都是字符串类型: 2.哈希:比较是用于对对象的操作: 3.List:按照插入数据顺序保存,value是可以重复的,底层是双向链表: 4.集合:是Str ...

  4. Redis中的value包含中文显示的问题?

    linux 系统 redis不识别中文  如何显示中文 在Redis中存储的value值是中文“马拉斯加”Shell下get获取后展示的结果为:\xc2\xed\xc0\xad\xcb\xb9\xbc ...

  5. redis 中文存储乱码问题

    在redis 中存储中文,读取会出现乱码(其实不是乱码,只是不是我们存的中文显示) redis> set test "我们" OK redis> get test &q ...

  6. Redis数据存储结构之String

    前言: 在Redis使用中,我们最常使用的操作是set key value,或 get key value .这里面包含了redis最基本的数据类型:String,字符串类型是redis中最基本的类型 ...

  7. Infinispan 8 中新的 Redis 缓存存储实现

    转载于:http://www.itxuexiwang.com/a/shujukujishu/redis/2016/0216/147.html nfinispan 8 包含了一个新的在 Redis k/ ...

  8. flink04 -----1 kafkaSource 2. kafkaSource的偏移量的存储位置 3 将kafka中的数据写入redis中去 4 将kafka中的数据写入mysql中去

    1. kafkaSource 见官方文档 2. kafkaSource的偏移量的存储位置 默认存在kafka的特殊topic中,但也可以设置参数让其不存在kafka的特殊topic中   3   将k ...

  9. 讨论两种Redis中Token的存储方式

    摘要:本文讨论一个问题:存储token时,token与对应用户id谁来作为key? 问题起源问题起源于要给公司的后台管理系统添加权限管理,选用的是开源框架shiro,而原本系统上是采用token做了登 ...

随机推荐

  1. 如果习惯VisualStudio,可以如下实现.Shader文件的语法高亮。

    如果习惯VisualStudio,可以如下实现.Shader文件的语法高亮. 下载作者donaldwu自己添加的关键词文件usertype.dat.其包括了Unity ShaderLab的部分关键字, ...

  2. cs4.1 编译与安装

    cs4.1编译报 https://issues.apache.org/jira/browse/CLOUDSTACK-2913 cs4.1安装报

  3. Solidity oraclize解析Json格式数据

    solidity虽然不能解析json数据但是oraclize_query可以直接处理: pragma solidity ^; import "github.com/oraclize/ethe ...

  4. centos 命令行和图形桌面模式的切换

    1.安装系统时建议安装图形界面,毕竟图形桌面下安装程序,比较方便 2.系统部署完成后可以切换到命令行界面:打开一个SHELL窗口运行 init 3 即可进入命令行界面.恢复图形用init 5 3.进入 ...

  5. 基于weui的一个小插件

    移动端项目当中大量的使用了weui,为了减少工作量,方便修改,自己写了个小插件,暂时只有toast和dialog部分,可能会更新actionSheet等其他部分 更新一个手机端预览的二维码,就直接放项 ...

  6. cookie用法小结 cookie.setPath 跨域共享

    1. JSP中Cookie的读写 Cookie的本质是一个键值对,当浏览器访问web服务器的时候写入在客户端机器上,里面记录一些信息.Cookie还有一些附加信息,比如域名.有效时间.注释等等. 下面 ...

  7. CMDB和运维自动化

    IT运维,指的是对已经搭建好的网络,软件,硬件进行维护.运维领域也是有细分的,有硬件运维和软件运维 硬件运维主要包括对基础设施的运维,比如机房的设备,主机的硬盘,内存这些物理设备的维护 软件运维主要包 ...

  8. Yii2在Form中处理短信验证码的Validator,耦合度最低的短信验证码验证方式

    短信验证码在目前大多数web应用中都会有,本文介绍一个基于Yii2 Validator方式的验证码验证方式. 在其他文章中看到的方式大多比较难做到一次封装,多次重用. 使用此方式的好处自然不用多说,V ...

  9. jQuary总结3: jQuery语法1

    1.jQuery样式操作 1.1 设置或者修改样式,操作的是style属性. 单样式语法: jQuery对象.css('属性名', '属性值') //html <div id="box ...

  10. asp.net WebAPI 问题 iisnode默认不支持PUT和DELETE的解决

    因为iisnode的自动重启服务器方便,一直用的它来作为开发中的node服务器,今天一个delete命令过去,得到一个405(?好像是)错误,让我很郁闷. 用原生的node试一下,是完美支持的,本来打 ...