Chapter 10.利用Redis Zset實(shí)現(xiàn)雙維度排行榜

歡迎來到「我是真的狗雜談世界」,關(guān)注不迷路

背景


最近需要將遇到的幾個(gè)排行需求點(diǎn)抽出來做一個(gè)獨(dú)立的通用排行組件,整理記錄一下古涧。

核心需求


  • 能獲得連續(xù)的部分的榜單:比如前100名矮烹、第300~400名的用戶以及分值
  • 能獲得任意單用戶信息:比如用戶A的分值與當(dāng)前排名
  • 能操作榜單單用戶分值:比如為用戶A增加3分
  • 上述功能要求實(shí)時(shí)
  • 分值相同時(shí)图焰,要求按照達(dá)到分值時(shí)間的先后排序盐碱,先達(dá)到分值的用戶排在前面
  • 分值是整數(shù)(或類似金額這種有限位小數(shù)榛了,也可以看為整數(shù))矿咕,業(yè)務(wù)上一般高精度小數(shù)無場(chǎng)景意義

思考過程


說白了就是排序問題抢肛,相關(guān)的算法和結(jié)構(gòu)有很多了,可問題是總不能自己從零開始實(shí)現(xiàn)吧(也不是不可以)~~

那么在現(xiàn)有的存儲(chǔ)介質(zhì)上很容易想到Redis的ZSet數(shù)據(jù)類型(MySQL不是今天的主題碳柱,假裝想不到咳咳)

Redis ZSet


底層結(jié)構(gòu)與操作過程

ZSet數(shù)據(jù)類型的主要數(shù)據(jù)結(jié)構(gòu)是跳表捡絮,具有多層級(jí)結(jié)構(gòu)(因此對(duì)內(nèi)存要求稍稍高一點(diǎn)),具體的結(jié)構(gòu)和操作過程在「Tool 1.Redis搗騰系列」中繼續(xù)搗騰莲镣。

相關(guān)功能情況

現(xiàn)在只關(guān)注上述需求用到的幾個(gè)操作是否支持以及性能開銷情況:

  • ZADD/ZINCRBY: O(log(N))
  • ZSCORE: O(1)
  • ZCARD: O(1)
  • ZRANGE/ZREVRANGE: O(log(N)+M)福稳;N為有序集的基數(shù),而M為結(jié)果集的基數(shù)
  • ZRANK/ZREVRANK: O(log(N))

O(log(N))的時(shí)間復(fù)雜度理論上完全可以扛住上億數(shù)據(jù)的并發(fā)查詢(當(dāng)然并不建議真的在生產(chǎn)環(huán)境這么干)瑞侮。

O(log(N)+M)需要特別注意的圆,M是線性增長(zhǎng)的,需要嚴(yán)格控制上限

一些特點(diǎn)

ZSet在使用上是member->score結(jié)構(gòu):

  • member: 被排序的標(biāo)識(shí)半火,也是默認(rèn)的第二排序維度(score相同時(shí)越妈,redis以member的字典序排列)
  • score: 被排序的分值,存儲(chǔ)類型是double

雙維度問題


如果直接按照上述用法進(jìn)行使用钮糖,那么只有第一排序維度score是我需要的梅掠,雖然有第二排序維度member,而需要的第二排序維度是時(shí)間。那怎么辦呢瓤檐?

思考技巧

我常常用來解決問題的思考技巧有:

  • 加:加入其他模塊赂韵、組件來滿足需求
  • 借:借助現(xiàn)有的但不滿足需求的模塊、組件利用轉(zhuǎn)換來滿足需求
  • 合:將幾個(gè)模塊挠蛉、組件合并起來實(shí)現(xiàn)一個(gè)需求祭示,跟拆可以互相轉(zhuǎn)換只是視角層次不同
  • 拆:將一個(gè)模塊、組件拆分為多個(gè)谴古,以滿足需求质涛,跟合可以互相轉(zhuǎn)換只是視角層次不同

實(shí)現(xiàn)思路

思路1:加結(jié)構(gòu)

粗暴一點(diǎn):一個(gè)ZSet只有一個(gè)score滿足需求,那就同時(shí)維護(hù)兩個(gè)ZSet掰担,一個(gè)存分值一個(gè)存時(shí)間


  • 優(yōu)點(diǎn):從方案上說實(shí)現(xiàn)比較簡(jiǎn)單
  • 缺點(diǎn):具體實(shí)現(xiàn)上需要同時(shí)維護(hù)兩個(gè)ZSet汇陆,需要的空間略大;且對(duì)并發(fā)控制粒度要求大(讀寫鎖+鎖范圍是操作兩個(gè)ZSet期間)

思路2:借member

前面提到member是默認(rèn)的第二排序維度带饱,但直接使用是無法滿足時(shí)間排序的需求的毡代,那如果member的內(nèi)容中涵蓋了時(shí)間呢?


比如原始數(shù)據(jù)是這樣:

user=user1勺疼;score=100教寂;time=2022-12-15 13:30:30
user=user2;score=100执庐;time=2022-12-15 13:30:35

而存儲(chǔ)時(shí)將time+user作為member:

member=2022-12-15 13:30:30_user1酪耕;score=100
member=2022-12-15 13:30:35_user2;score=100

這樣就借助了member來實(shí)現(xiàn)了時(shí)間維度的排序轨淌。


  • 優(yōu)點(diǎn): 從方案上說實(shí)現(xiàn)也比較簡(jiǎn)單
  • 缺點(diǎn):在維護(hù)上仍舊需要另一個(gè)結(jié)構(gòu)來輔助映射用戶當(dāng)前時(shí)間(比如hash)迂烁,否則獲取用戶排行信息的時(shí)候無法確定member的值;同時(shí)每次修改用戶分值時(shí)必須控制并發(fā)和原子操作刪除原member和添加新member

思路3:拆/合score

前面還提到score的存儲(chǔ)類型是double递鹉,也就是說有很多位(不管二進(jìn)制位也好盟步、十進(jìn)制位也好),而數(shù)字的比較是高位相等時(shí)以低位來決定結(jié)果梳虽,這不是跟多維度排序的需求一樣嗎址芯?那如果我把這么多位拆成兩部分分別代表分值和時(shí)間,存儲(chǔ)時(shí)計(jì)算合并呢窜觉?


比如(以十進(jìn)制位舉例)原始數(shù)據(jù)是這樣:

user=user1谷炸;score=100;time=3092
user=user2禀挫;score=100旬陡;time=3093

而存儲(chǔ)時(shí)將score+time作為score(當(dāng)然這樣時(shí)間維度是順序反了,用一個(gè)大數(shù)去減就可以把順序帶回來):

member=user1语婴;score=1003092
member=user2描孟;score=1003093

這樣就將一個(gè)score拆成了多個(gè)存儲(chǔ)(或者說將多個(gè)存儲(chǔ)合存在一個(gè)score中)了驶睦。


  • 優(yōu)點(diǎn):無需維護(hù)額外的結(jié)構(gòu),空間開銷少匿醒;且只需要控制寫鎖场航,不會(huì)干擾讀并發(fā)
  • 缺點(diǎn):score的計(jì)算相對(duì)復(fù)雜一點(diǎn),且縮小了分值和時(shí)間的取值范圍廉羔;另外score的可讀性不那么好(取值范圍和可讀性在實(shí)踐中進(jìn)行了優(yōu)化)

實(shí)踐過程


細(xì)化方案


方案1:以十進(jìn)制對(duì)整數(shù)位進(jìn)行劃分

  • Redis ZSet的score值在超過254后存儲(chǔ)和計(jì)算會(huì)出現(xiàn)問題溉痢,保險(xiǎn)起見,采用253最為最大整數(shù):900719 9254740992
  • 秒級(jí)時(shí)間戳需要10位憋他,因此低10位由秒級(jí)時(shí)間戳填充
  • 高6位則可以由分值填充孩饼,但分值最大為900718

  • 優(yōu)點(diǎn):可讀性較高
  • 缺點(diǎn):分值范圍小

方案2:以二進(jìn)制對(duì)整數(shù)位進(jìn)行劃分

  • 仍舊是2^53作為最大整數(shù)
  • 采用低32作為時(shí)間戳(可表示到2106-02-07 14:28:16)
  • 剩余高21位作為分值(可表示到2097152)

  • 優(yōu)點(diǎn):分值范圍較方案1變大了一些(如果壓縮時(shí)間戳位數(shù),還可以再變大一倍)
  • 缺點(diǎn):可讀性更差了(想象一下兩個(gè)數(shù)轉(zhuǎn)換成二進(jìn)制后再組合成一個(gè)新的數(shù))

方案3:利用double的浮點(diǎn)計(jì)算

  • double的有效位可以保證在15位以上
  • 將分值作為score的整數(shù)部分
  • 將時(shí)間戳逆向后作為score的小數(shù)部分

  • 優(yōu)點(diǎn):可讀性較高竹挡;利用浮點(diǎn)數(shù)特點(diǎn)镀娶,分值取值范圍可以很大(起碼2^53沒有問題了)
  • 缺點(diǎn):也是因?yàn)楦↑c(diǎn)數(shù)特點(diǎn),隨著分值(整數(shù)部分)的逐漸增大揪罕,時(shí)間戳(小數(shù)部分)精度逐漸變小

選擇


最后我選擇了方案3梯码,因?yàn)榭紤]到有分值大小和時(shí)間精度高低兩個(gè)維度的指標(biāo),方案3在不同場(chǎng)景下的匹配度如下:

分值小 分值大
時(shí)間精度高 匹配度高 匹配度低
時(shí)間精度低 匹配度高 匹配度高

根據(jù)上表好啰,業(yè)務(wù)業(yè)務(wù)需要:

  • 對(duì)于分值上限腥绦(百萬級(jí)別以下)的業(yè)務(wù)場(chǎng)景,方案3可以保障時(shí)間高精度
  • 對(duì)于分值上限高(千萬級(jí)別以上)的業(yè)務(wù)場(chǎng)景坎怪,分值往往是從小累計(jì)到大的且在小分值時(shí)競(jìng)爭(zhēng)激烈(容易出現(xiàn)同分多,達(dá)到同分的時(shí)間間隔小的情況)大分值時(shí)則不激烈廓握,采用方案3可以在分值較小時(shí)仍舊保障時(shí)間高精度搅窿,分值大時(shí)一般對(duì)時(shí)間精度要求也低了
  • 另外還可以根據(jù)業(yè)務(wù)來收縮時(shí)間戳(小數(shù)部分)的范圍來擴(kuò)大秒級(jí)時(shí)間精度下的分值(整數(shù)部分)范圍

DEMO

// lock acquire

// 計(jì)算分值部分
$nowScore = $redis->zScore($key, $member);
$setScore = $addScore + intval($nowScore);

// 計(jì)算時(shí)間戳部分(可做到百萬級(jí)別分值仍舊保持秒級(jí)時(shí)間戳精度,還可以繼續(xù)優(yōu)化這塊提升分值上限)
$maxTime = 4102415999; // 2099-12-31 23:59:59
$nowTime = $maxTime - time();
$timeScore = bcdiv($nowTime, $maxTime, 10);
$finalScore = bcadd($setScore, $timeScore, 10);

// 設(shè)置
$redis->zAdd($key, $finalScore, $member);

// lock release
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末隙券,一起剝皮案震驚了整個(gè)濱河市男应,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌娱仔,老刑警劉巖沐飘,帶你破解...
    沈念sama閱讀 222,183評(píng)論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異牲迫,居然都是意外死亡耐朴,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,850評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門盹憎,熙熙樓的掌柜王于貴愁眉苦臉地迎上來筛峭,“玉大人,你說我怎么就攤上這事陪每∮跋” “怎么了镰吵?”我有些...
    開封第一講書人閱讀 168,766評(píng)論 0 361
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)挂签。 經(jīng)常有香客問我疤祭,道長(zhǎng),這世上最難降的妖魔是什么饵婆? 我笑而不...
    開封第一講書人閱讀 59,854評(píng)論 1 299
  • 正文 為了忘掉前任勺馆,我火速辦了婚禮,結(jié)果婚禮上啦辐,老公的妹妹穿的比我還像新娘谓传。我一直安慰自己,他們只是感情好芹关,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,871評(píng)論 6 398
  • 文/花漫 我一把揭開白布续挟。 她就那樣靜靜地躺著,像睡著了一般侥衬。 火紅的嫁衣襯著肌膚如雪诗祸。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,457評(píng)論 1 311
  • 那天轴总,我揣著相機(jī)與錄音直颅,去河邊找鬼。 笑死怀樟,一個(gè)胖子當(dāng)著我的面吹牛功偿,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播往堡,決...
    沈念sama閱讀 40,999評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼械荷,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了虑灰?” 一聲冷哼從身側(cè)響起吨瞎,我...
    開封第一講書人閱讀 39,914評(píng)論 0 277
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎穆咐,沒想到半個(gè)月后颤诀,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,465評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡对湃,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,543評(píng)論 3 342
  • 正文 我和宋清朗相戀三年崖叫,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片拍柒。...
    茶點(diǎn)故事閱讀 40,675評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡归露,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出斤儿,到底是詐尸還是另有隱情剧包,我是刑警寧澤恐锦,帶...
    沈念sama閱讀 36,354評(píng)論 5 351
  • 正文 年R本政府宣布,位于F島的核電站疆液,受9級(jí)特大地震影響一铅,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜堕油,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,029評(píng)論 3 335
  • 文/蒙蒙 一潘飘、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧掉缺,春花似錦卜录、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,514評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至搜囱,卻和暖如春丑瞧,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背蜀肘。 一陣腳步聲響...
    開封第一講書人閱讀 33,616評(píng)論 1 274
  • 我被黑心中介騙來泰國(guó)打工绊汹, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人扮宠。 一個(gè)月前我還...
    沈念sama閱讀 49,091評(píng)論 3 378
  • 正文 我出身青樓西乖,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親坛增。 傳聞我的和親對(duì)象是個(gè)殘疾皇子摇锋,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,685評(píng)論 2 360

推薦閱讀更多精彩內(nèi)容