java千万用户实现实时排名_想知道谁是你的最佳用户?基于Redis实现排行榜周期榜与最近N期榜...
本文由云+社區(qū)發(fā)表
前言
業(yè)務(wù)已基于Redis實現(xiàn)了一個高可用的排行榜服務(wù),長期以來相安無事。有一天,產(chǎn)品說:我要一個按周排名的排行榜,以反映本周內(nèi)用戶的活躍情況。于是周榜(按周重置更新的榜單)誕生了。為了滿足產(chǎn)品多變的需求,我們一并實現(xiàn)了小時榜、日榜、周榜、月榜幾種周期榜。本以為可長治久安了,又有一天,產(chǎn)品體驗業(yè)務(wù)后說:我想要一個最近7天榜,反映最近一段時間的用戶活躍情況,不想讓歷史的高分用戶長期占據(jù)榜首,可否?于是,滾動榜(最近N期榜)的需求誕生了。
周期榜
周期榜實現(xiàn)還是很容易的,給每個周期算出一個序號,作為榜單名后綴,進入新的周期自然切換讀寫新榜單,平滑過度。以日榜為例,根據(jù)時間戳ts計算每日序號s=ts/86400,以日序號s作為后綴即可實現(xiàn)零點后自動讀寫新日榜。小時榜與此雷同,不再贅述。
對于周榜,可以選定某一個周一(或周日,看需求)的時間戳為基準,計算基準到當(dāng)前經(jīng)過的周數(shù)為周序號,以此作為榜單后綴。
對于月榜,稍有不同,因為月份天數(shù)不固定,所以不能按照上述方法計算。但我們可以根據(jù)時間戳取得年、月信息,以年月做標志(如201810)后綴,即可實現(xiàn)月榜。
滾動榜
方案探討
滾動榜需要考慮多個周期榜數(shù)據(jù)的聚合與自動迭代更新,實現(xiàn)起來就沒那么容易了。下面分析幾個方案。
方案1:每日一個滾動榜,當(dāng)日離線補齊數(shù)據(jù)
還以日榜為例,最近N天榜就是把前N-1天到當(dāng)天的每一個日榜榜單累加即可,比如最近7天榜,就是前6天到當(dāng)天的每一個日榜中相同元素數(shù)據(jù)累加。因此,最直觀的一個方案是:首先記錄每天的排行榜R,那么第i天的最近N天榜Si=∑N?1n=0Ri?n,其中,Ri?x表示第i天的前x天的日榜。實現(xiàn)上,可以每日生成一個滾動榜S和當(dāng)天日榜R,加分時同時寫入S和R,每日零點后跑工具將前N-1天數(shù)據(jù)累加寫入當(dāng)日滾動榜S。
這個方案的優(yōu)點是直觀,實現(xiàn)簡單。但缺點也很明顯,一是每日一個滾動榜,消耗內(nèi)存較多;二是數(shù)據(jù)更新不實時,需要等待離線作業(yè)完成累加后S中的數(shù)據(jù)才完全正確;三是時間復(fù)雜度高,7天榜還好,只需要讀過去6天數(shù)據(jù),如果是100天榜,該方案需要讀過去99天榜,顯然不可接受。
方案2:全局一個滾動榜,當(dāng)日離線補齊數(shù)據(jù)
基于方案1,如果業(yè)務(wù)無需查詢歷史的S,可以只使用全局一個S,無需每日創(chuàng)建一個Si。加分操作還是同時加當(dāng)日的Ri和全局唯一的S,但每日零點的離線作業(yè)改為從S中減去Ri?(N?1)的數(shù)據(jù)(即將最早一天的數(shù)據(jù)淘汰,從而實現(xiàn)S的計數(shù)滾動)。
此方案減少了內(nèi)存使用,同時離線任務(wù)每次只需讀取一個日榜做減法,時間復(fù)雜度為O(1);但仍需要離線作業(yè)完成才能保證數(shù)據(jù)正確性,還是無法做到平滑過渡。
方案3:每日一個滾動榜,實時更新
要做到每日零點后榜單實時生效,而不需要等待離線作業(yè)的完成,一種方案是預(yù)寫未來的榜單。不難得出,當(dāng)日分數(shù)會計入往后N-1天的滾動榜中。因此,可以寫當(dāng)天的滾動榜Si的同時,寫往后N-1天的榜單Si+1到Si+N?1。
該方案不僅能脫離離線作業(yè)做到實時更新,且可以省略每天的日榜。但缺點也不難看出,對于7天滾動榜,每次寫操作需要更新7個榜單,寫入量小時還勉強能接受,如果寫操作量大或者需要的是30天、60天滾動榜,此方案可行性幾乎為零。
方案4:實時更新,常數(shù)次寫操作
有不有辦法做到既能實時更新,寫榜數(shù)量也不隨N的增加而增加呢?不難看出,第i天滾動榜Si=∑N?1n=0Ri?n,而第i+1天的滾動榜Si+1=∑N?1n=0R(i+1)?n=∑N?2n=0Ri?n+Ri+1。顯然,Si+1=Si?Ri?(N?1)+Ri+1。由于Ri+1在剛達到零點時必然為空且可以在次日實時加到Si+1上,因此如果我們能提前準備好Si?Ri?(N?1)這部分數(shù)據(jù),那么在零點進入i+1天后,Ri+1自然就是可用狀態(tài)了。
以3天滾動榜為例,次日滾動榜初始態(tài)為當(dāng)日滾動榜減去n-2天的日榜數(shù)據(jù)。
+-------------------------------------------+
| |
+----+---+ +--------+ +--------+ |
| | | | | | |
| R(i-2) | | R(i-1) | | R(i) | |
| | | | | | |
+----+---+ +----+---+ +---+----+ |
| | | |
| | | |
| | | |
| | v+ v-
| |
| | + +--------+ +--------+
| +-----> | | + | |
| + | S(i) | +---+> | S(i+1) |
+-----------------+> | | | |
+--------+ +--------+
那么,如何提前準備好Si?Ri?(N?1)這部分數(shù)據(jù)呢?可以如下處理:
對一個元素加分時,加當(dāng)日周期榜Ri、滾動榜Si;還需根據(jù)其在今日滾動榜中的分數(shù)s、及n-1天日榜中的分數(shù)r,計算出其在明日滾動榜中的初始分數(shù)s-r寫入明日滾動榜中;即3個寫操作;
如果一個元素在當(dāng)日沒有任何加分操作,那么不會觸發(fā)寫入初始分數(shù)操作,所以還需要一個離線工具補齊。與方案1、2不同的是,該離線工具可提前一天運行,即當(dāng)日運行離線工具補齊次日的滾動榜數(shù)據(jù)即可。
簡而言之:第一步是運行離線工具生成次日的滾動榜;第二步是在寫操作時同時更新次日的滾動榜。
該方案也是每日一個滾動榜。相對方案3而言,是空間換時間。如果空間不足且無保留歷史的需求,可在離線工具中清理歷史數(shù)據(jù)。
+--------------+
| |
| AddScore |
| |
+-+----+-----+-+
| | |
v | |
+--------+ +--------+ +-------++ | |
| | | | | | | |
| R(i-2) | | R(i-1) | | R(i) | | |
| | | | | | | |
+--------+ +--------+ +--------+ | |
| v
+--------+ | ++-------+
| | | | |
| S(i) +
| | | |
+--------+ +----+---+
^
|
|
+------+-----+
| |
| Tool |
| |
+------------+
方案4的實現(xiàn)
以下是實現(xiàn)參考。此處僅列出核心的lua腳本。Redis命令調(diào)用腳本的參數(shù)定義為:
eval script 4 當(dāng)日日榜key 當(dāng)日滾動榜key 即將淘汰的日榜key 明日滾動榜key 榜單元素名 加分數(shù)
lua腳本script如下:
--加今日日榜分數(shù)
redis.call('ZINCRBY', KEYS[1], ARGV[2], ARGV[1])
--加今日滾動榜分數(shù)
local rs = redis.call('ZINCRBY', KEYS[2], ARGV[2], ARGV[1])
local curRoundScore = 0
if (rs) then
curRoundScore = tonumber(rs)
end
--取即將淘汰的日榜分數(shù)
rs = redis.call('ZSCORE', KEYS[3], ARGV[1])
local oldCycleScore = 0
if (rs) then
oldCycleScore = tonumber(rs)
end
--計算次日滾動榜初始分數(shù)
local nextRoundScore = curRoundScore - oldCycleScore
if nextRoundScore < 0 then
nextRoundScore = 0
end
--設(shè)置次日滾動榜分數(shù)
redis.call('ZADD', KEYS[4], nextRoundScore, ARGV[1])
--返回今日分數(shù)
rs = redis.call('ZREVRANK', KEYS[2], ARGV[1])
return {curRoundScore, rs}
關(guān)于榜單key計算準確度的探討 我們的業(yè)務(wù)是在排行榜接入層邏輯中計算榜單后綴的,這種方案對邏輯層多臺機器的時間一致性要求較高,如果邏輯層服務(wù)器時鐘不一致,可能在時間切換點上出現(xiàn)不同機器讀寫不同榜單的問題。如果業(yè)務(wù)對時間精確度要求嚴格,可以考慮通過lua腳步在redis端計算后綴。
.
關(guān)于內(nèi)存容量限制的探討 基于ZSet實現(xiàn)的排行榜,每個元素約需要100字節(jié)內(nèi)存。如果榜單長度為1000萬,則每個榜單約需要1G內(nèi)存。滾動榜的計算需要每日保留一個日榜,如果滾動周期較長,則可能單機內(nèi)存容量不足以容納所有需要的榜單。 考慮到歷史日榜數(shù)據(jù)是不會變更的,因此不在lua腳本中讀取歷史日榜數(shù)據(jù)也無一致性問題。故可以將榜單打散到多個Redis實例,在接入層做邏輯讀取歷史日榜的分數(shù),再以參數(shù)形式傳入給lua腳本處理。
總結(jié)
在榜單長度不大且并發(fā)量不高的場景下,使用關(guān)系數(shù)據(jù)庫+Cache的方案實現(xiàn)排行榜有更高的靈活性。而在海量數(shù)據(jù)與高并發(fā)的場景下,Redis是一個更好的選擇。本文基于Redis實現(xiàn)的滾動榜,不論滾動周期多長,都只需要常數(shù)(3)次數(shù)的寫操作,有較好的性能和可擴展性。且通過離線+在線的雙預(yù)生成機制,確保了榜單實時生效,可用性較強。
此文已由作者授權(quán)騰訊云+社區(qū)發(fā)布
總結(jié)
以上是生活随笔為你收集整理的java千万用户实现实时排名_想知道谁是你的最佳用户?基于Redis实现排行榜周期榜与最近N期榜...的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 信用卡转账算不算消费 你可说了不算
- 下一篇: sql2000 mysql 兼容_SQL