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

71次阅读
没有评论

共计 2563 个字符,预计需要花费 7 分钟才能阅读完成。

行业资讯    
数据库    
Redis SortedSet 结构 score 字段丢失精度问题解决办法是什么

这期内容当中丸趣 TV 小编将会给大家带来有关 Redis SortedSet 结构 score 字段丢失精度问题解决办法是什么,文章内容丰富且以专业的角度为大家分析和叙述,阅读完这篇文章希望大家可以有所收获。

一、问题现象

项目中采用 Redis SortedSet 存储用户的离线消息,score 值存储的 msgid(消息 ID)。msgid 采用 snowflake 算法生成,按照时间有序。

生成的 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 丢失精度的问题。

上述就是丸趣 TV 小编为大家分享的 Redis SortedSet 结构 score 字段丢失精度问题解决办法是什么了,如果刚好有类似的疑惑,不妨参照上述分析进行理解。如果想知道更多相关知识,欢迎关注丸趣 TV 行业资讯频道。

正文完
 
丸趣
版权声明:本站原创文章,由 丸趣 2023-07-17发表,共计2563字。
转载说明:除特殊说明外本站除技术相关以外文章皆由网络搜集发布,转载请注明出处。
评论(没有评论)