Redis SortedSet結構score欄位丟失精度問題解決辦法

普通程式設計師發表於2018-10-30

一、問題現象

專案中採用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

查詢看一下結果 

Redis SortedSet結構score欄位丟失精度問題解決辦法

我們發現score值採用科學計數法表示,test3,test4兩個元素的score值顯示是一樣的。

使用score=215857550229364736 執行查詢,結果如下圖 

Redis SortedSet結構score欄位丟失精度問題解決辦法

使用215857550229364736查詢,結果score為215857550229364737的test4也被查出來了

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

Redis SortedSet結構score欄位丟失精度問題解決辦法

這一現象給我們的系統功能帶了困擾,會影響到訊息同步TimeLine的精確性(參看《基於TimeLine模型的訊息同步機制》)。

二、問題原因

查詢相關資料發現Sorted Sets中的Score是double型別,我們的msgid是long型別。問題是long轉換為double時,丟失精度

1、snowflake演算法簡介

訊息ID採用snowflake演算法,採用64位二進位制整數。二進位制具體位數含義如下圖。

Redis SortedSet結構score欄位丟失精度問題解決辦法

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資料的結構如下圖 

Redis SortedSet結構score欄位丟失精度問題解決辦法

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/,如需轉載,請註明出處,否則將追究法律責任。

相關文章