ITPub博客

首页 > 数据库 > 数据库开发技术 > Redis SortedSet结构score字段丢失精度问题解决办法

Redis SortedSet结构score字段丢失精度问题解决办法

原创 数据库开发技术 作者:普通程序员 时间:2018-10-30 15:58:36 0 删除 编辑

一、问题现象

项目中采用Redis SortedSet存储用户的离线消息,score值存储的msgid(消息ID)。msgid采用snowflake算法生成,按照时间有序。(参看《一个海量在线用户即时通讯系统(IM)的完整设计》)

生成的msgid有18位十进制整数,例如 215857550229364736

我们发现数值很接近的msgid,在redis中无法通过score进行区分。

举个列子,在redis中tzset结构里存入如下几条数据  

ZADD tzset 215857497028812800 test1

ZADD tzset 215857540511162369 test2

ZADD tzset 215857550229364736 test3

ZADD tzset 215857550229364737 test4

查询看一下结果 

我们发现score值采用科学计数法表示,test3,test4两个元素的score值显示是一样的。

使用score=215857550229364736 执行查询,结果如下图 

使用215857550229364736查询,结果score为215857550229364737的test4也被查出来了

用215857550229364739去查,竟然也能查出来 

这一现象给我们的系统功能带了困扰,会影响到消息同步TimeLine的精确性(参看《基于TimeLine模型的消息同步机制》)。

二、问题原因

查询相关资料发现Sorted Sets中的Score是double类型,我们的msgid是long类型。问题是long转换为double时,丢失精度

1、snowflake算法简介

消息ID采用snowflake算法,采用64位二进制整数。二进制具体位数含义如下图。

1位,不用。二进制中最高位为1的都是负数,但是我们生成的id都使用正数,所以这个最高位固定是0

41位,用来记录时间戳(毫秒)。

如果只用来表示正整数(计算机中正数包含0),可以表示的数值范围是:0 至 241−1,减1是因为可表示的数值范围是从0开始算的,而不是1。

也就是说41位可以表示241−1个毫秒的值,转化成单位年则是(241−1)/(1000∗60∗60∗24∗365)=69年

10位,用来记录工作机器id。

可以部署在1024个节点,包括5位datacenterId和5位workerId

12位,序列号,用来记录同毫秒内产生的不同id。

12位(bit)可以表示的最大正整数是4095,即可以用0、1、2、3、....4095这4096个数字,来表示同一机器同一时间截(毫秒)内产生的4096个ID序号

2、doublel数据结构

double数据的结构如下图 

3、问题定位

63bit(去掉符号位)的数转换为52bit的数,从某一位开始进行了四舍五入,导致精度下降。所以215857550229364736、215857550229364737、215857550229364739三个数据被转换为double类型后,计算机认为是相同的数

三、解决办法

问题找到了,怎么解决呢?

id生成策略要保证整个系统生命周期类所有ID唯一,设计一个52bit的ID生成器保证ID唯一难度较大。

Redis的score数据类型更是修改不了

用52bit来表示63bit的数据一定会丢失信息,长整型long默认转换为double的方式丢失的信息会影响到业务,能不能结合业务特点自定义一种转换(映射)方式,答案是肯定的。

有以下几种想法

1、因为Redis缓存的消息最多保存15天(假设)或者最多保存多少条。能不能截去41位时间戳的部分高位,确保Redis缓存时间周期内时间戳长度够用就行呢?计算了一下长度 log(15*24*60*60*1000)=30.2,大约30位二进制数即可在现有规则下表示15天时间。所以将41位时间戳的前11位屏蔽掉,可以节约11位二进制信息。这样63bit刚好能用52bit来表示。

然而这个方式有个致命问题,当15天时间周期到了后,时间戳会变得特别小(新的周期),这导致上一个周期后边的数据Score值大于新周期。消息顺序混乱了,会导致拉离线丢消息,这不能接受!

2、去掉10bit工作机id和序列号的最高位bit。

去掉这11bit,不会对消息的顺序造成影响,但是可能造成score数值冲突(相同)。分析一下score冲突的可能性。

(1)12bit序列号能表示4096个数。去掉最高位,能表示2048个数。所以单个msgid生成节点(dispatch模块)每毫秒,每个用户要超过2048条消息,才可能出现score重复。这个基本不可能发生。

(2)去掉10bit工作机id号,需要同一毫秒,同一用户在不同的dispatch节点都接收到消息,score才可能冲突。即使出现这种情况,由于12位序列号我们做了模128的随机分布(解决分库问题),即使出现同一毫秒不同disptch生成同一用户msgid的情况下,score冲突的概率还要除以 128*128。这个概率非常低

(3)即使出现了score冲突(两条消息有相同score),最多造成拉取离线消息多拉取相同score的消息(本来一次拉取10条离线,结果可能拉到11条),对业务也没有影响

因此采用去掉10bit工作机id和序列号的最高位bit将63bit(不含符号位)的msgid转换成52bit的score对业务上没有影响。同时解决了redis sorted set丢失精度的问题。 

因此采用去掉10bit工作机id和序列号的最高位bit将63bit(不含符号位)的msgid转换成52bit的score对业务上没有影响。同时解决了redis sorted set丢失精度的问题。

来自 “ ITPUB博客 ” ,链接:http://blog.itpub.net/31556438/viewspace-2218088/,如需转载,请注明出处,否则将追究法律责任。

请登录后发表评论 登录
全部评论

注册时间:2018-09-27

  • 博文量
    22
  • 访问量
    62402