想知道誰是你的最佳用戶?基於Redis實現排行榜周期榜與最近N期榜

来源:https://www.cnblogs.com/qcloud1001/archive/2018/12/13/10115588.html
-Advertisement-
Play Games

本文由雲+社區發表 前言 業務已基於Redis實現了一個高可用的排行榜服務,長期以來相安無事。有一天,產品說:我要一個按周排名的排行榜,以反映本周內用戶的活躍情況。於是周榜(按周重置更新的榜單)誕生了。為了滿足產品多變的需求,我們一併實現了小時榜、日榜、周榜、月榜幾種周期榜。本以為可長治久安了,又有 ...


本文由雲+社區發表

前言

業務已基於Redis實現了一個高可用的排行榜服務,長期以來相安無事。有一天,產品說:我要一個按周排名的排行榜,以反映本周內用戶的活躍情況。於是周榜(按周重置更新的榜單)誕生了。為了滿足產品多變的需求,我們一併實現了小時榜、日榜、周榜、月榜幾種周期榜。本以為可長治久安了,又有一天,產品體驗業務後說:我想要一個最近7天榜,反映最近一段時間的用戶活躍情況,不想讓歷史的高分用戶長期占據榜首,可否?於是,滾動榜(最近N期榜)的需求誕生了。

周期榜

周期榜實現還是很容易的,給每個周期算出一個序號,作為榜單名尾碼,進入新的周期自然切換讀寫新榜單,平滑過度。以日榜為例,根據時間戳ts計算每日序號s=ts/86400,以日序號s作為尾碼即可實現零點後自動讀寫新日榜。小時榜與此雷同,不再贅述。

對於周榜,可以選定某一個周一(或周日,看需求)的時間戳為基準,計算基準到當前經過的周數為周序號,以此作為榜單尾碼。

對於月榜,稍有不同,因為月份天數不固定,所以不能按照上述方法計算。但我們可以根據時間戳取得年、月信息,以年月做標誌(如201810)尾碼,即可實現月榜。

滾動榜

方案探討

滾動榜需要考慮多個周期榜數據的聚合與自動迭代更新,實現起來就沒那麼容易了。下麵分析幾個方案。

方案1:每日一個滾動榜,當日離線補齊數據

還以日榜為例,最近N天榜就是把前N-1天到當天的每一個日榜榜單累加即可,比如最近7天榜,就是前6天到當天的每一個日榜中相同元素數據累加。因此,最直觀的一個方案是:首先記錄每天的排行榜R,那麼第i天的最近N天榜Si=∑N−1n=0Ri−n,其中,Ri−x表示第i天的前x天的日榜。實現上,可以每日生成一個滾動榜S和當天日榜R,加分時同時寫入S和R,每日零點後跑工具將前N-1天數據累加寫入當日滾動榜S。

這個方案的優點是直觀,實現簡單。但缺點也很明顯,一是每日一個滾動榜,消耗記憶體較多;二是數據更新不實時,需要等待離線作業完成累加後S中的數據才完全正確;三是時間複雜度高,7天榜還好,只需要讀過去6天數據,如果是100天榜,該方案需要讀過去99天榜,顯然不可接受。

方案2:全局一個滾動榜,當日離線補齊數據

基於方案1,如果業務無需查詢歷史的S,可以只使用全局一個S,無需每日創建一個Si。加分操作還是同時加當日的Ri和全局唯一的S,但每日零點的離線作業改為從S中減去Ri−(N−1)的數據(即將最早一天的數據淘汰,從而實現S的計數滾動)。

此方案減少了記憶體使用,同時離線任務每次只需讀取一個日榜做減法,時間複雜度為O(1);但仍需要離線作業完成才能保證數據正確性,還是無法做到平滑過渡。

方案3:每日一個滾動榜,實時更新

要做到每日零點後榜單實時生效,而不需要等待離線作業的完成,一種方案是預寫未來的榜單。不難得出,當日分數會計入往後N-1天的滾動榜中。因此,可以寫當天的滾動榜Si的同時,寫往後N-1天的榜單Si+1到Si+N−1。

該方案不僅能脫離離線作業做到實時更新,且可以省略每天的日榜。但缺點也不難看出,對於7天滾動榜,每次寫操作需要更新7個榜單,寫入量小時還勉強能接受,如果寫操作量大或者需要的是30天、60天滾動榜,此方案可行性幾乎為零。

方案4:實時更新,常數次寫操作

有不有辦法做到既能實時更新,寫榜數量也不隨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)這部分數據,那麼在零點進入i+1天後,Ri+1自然就是可用狀態了。

以3天滾動榜為例,次日滾動榜初始態為當日滾動榜減去n-2天的日榜數據。
     +-------------------------------------------+
     |                                           |
+----+---+   +--------+   +--------+             |
|        |   |        |   |        |             |
| R(i-2) |   | R(i-1) |   |  R(i)  |             |
|        |   |        |   |        |             |
+----+---+   +----+---+   +---+----+             |
     |            |           |                  |
     |            |           |                  |
     |            |           |                  |
     |            |           v+                 v-
     |            |
     |            |    +  +--------+        +--------+
     |            +-----> |        |     +  |        |
     |                 +  |  S(i)  | +---+> | S(i+1) |
     +-----------------+> |        |        |        |
                          +--------+        +--------+

那麼,如何提前準備好Si−Ri−(N−1)這部分數據呢?可以如下處理:

  • 對一個元素加分時,加當日周期榜Ri、滾動榜Si;還需根據其在今日滾動榜中的分數s、及n-1天日榜中的分數r,計算出其在明日滾動榜中的初始分數s-r寫入明日滾動榜中;即3個寫操作;
  • 如果一個元素在當日沒有任何加分操作,那麼不會觸發寫入初始分數操作,所以還需要一個離線工具補齊。與方案1、2不同的是,該離線工具可提前一天運行,即當日運行離線工具補齊次日的滾動榜數據即可。

簡而言之:第一步是運行離線工具生成次日的滾動榜;第二步是在寫操作時同時更新次日的滾動榜。

該方案也是每日一個滾動榜。相對方案3而言,是空間換時間。如果空間不足且無保留歷史的需求,可在離線工具中清理歷史數據。

                                +--------------+
                                |              |
                                |   AddScore   |
                                |              |
                                +-+----+-----+-+
                                  |    |     |
                                  v    |     |
+--------+   +--------+   +-------++   |     |
|        |   |        |   |        |   |     |
| R(i-2) |   | R(i-1) |   |  R(i)  |   |     |
|        |   |        |   |        |   |     |
+--------+   +--------+   +--------+   |     |
                                       |     v
                          +--------+   |    ++-------+
                          |        |   |    |        |
                          |  S(i)  +<--+    | S(i+1) |
                          |        |        |        |
                          +--------+        +----+---+
                                                 ^
                                                 |
                                                 |
                                          +------+-----+
                                          |            |
                                          |    Tool    |
                                          |            |
                                          +------------+

方案4的實現

以下是實現參考。此處僅列出核心的lua腳本。Redis命令調用腳本的參數定義為:

eval script 4 當日日榜key 當日滾動榜key 即將淘汰的日榜key 明日滾動榜key 榜單元素名 加分數

lua腳本script如下:

--加今日日榜分數
redis.call('ZINCRBY', KEYS[1], ARGV[2], ARGV[1])

--加今日滾動榜分數
local rs = redis.call('ZINCRBY', KEYS[2], ARGV[2], ARGV[1])
local curRoundScore = 0
if (rs) then
    curRoundScore = tonumber(rs)
end

--取即將淘汰的日榜分數
rs = redis.call('ZSCORE', KEYS[3], ARGV[1])
local oldCycleScore = 0
if (rs) then
    oldCycleScore = tonumber(rs)
end

--計算次日滾動榜初始分數
local nextRoundScore = curRoundScore - oldCycleScore
if nextRoundScore < 0 then
    nextRoundScore = 0
end

--設置次日滾動榜分數
redis.call('ZADD', KEYS[4], nextRoundScore, ARGV[1])

--返回今日分數
rs = redis.call('ZREVRANK', KEYS[2], ARGV[1])
return {curRoundScore, rs}

關於榜單key計算準確度的探討 我們的業務是在排行榜接入層邏輯中計算榜單尾碼的,這種方案對邏輯層多台機器的時間一致性要求較高,如果邏輯層伺服器時鐘不一致,可能在時間切換點上出現不同機器讀寫不同榜單的問題。如果業務對時間精確度要求嚴格,可以考慮通過lua腳步在redis端計算尾碼。

.

關於記憶體容量限制的探討 基於ZSet實現的排行榜,每個元素約需要100位元組記憶體。如果榜單長度為1000萬,則每個榜單約需要1G記憶體。滾動榜的計算需要每日保留一個日榜,如果滾動周期較長,則可能單機記憶體容量不足以容納所有需要的榜單。 考慮到歷史日榜數據是不會變更的,因此不在lua腳本中讀取歷史日榜數據也無一致性問題。故可以將榜單打散到多個Redis實例,在接入層做邏輯讀取歷史日榜的分數,再以參數形式傳入給lua腳本處理。

總結

在榜單長度不大且併發量不高的場景下,使用關係資料庫+Cache的方案實現排行榜有更高的靈活性。而在海量數據與高併發的場景下,Redis是一個更好的選擇。本文基於Redis實現的滾動榜,不論滾動周期多長,都只需要常數(3)次數的寫操作,有較好的性能和可擴展性。且通過離線+線上的雙預生成機制,確保了榜單實時生效,可用性較強。

此文已由作者授權騰訊雲+社區發佈



您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • lsyncd 是一個支持實時、雙向、多機器的多模式文件同步工具。 ...
  • The VMware Authorization Service is not running。 原因 虛擬機服務沒有開啟 解決方法 ...
  • VM下載 VM是一款收費軟體,要找有密鑰的下載。 我的網盤 > 軟體 > 常用電腦工具 > VM VM安裝 參考鏈接中的安裝步驟 http://blog.java1234.com/blog/articles/290.html ...
  • 2)doPost和doGet的區別?(視頻下載) (全部書籍) 馬克-to-win:1)當用戶在瀏覽器地址欄輸入URL,2)點擊Web頁面 中的鏈接3)提交沒有指定METHOD的表單,4)或指定了METHOD=“GET”時,瀏覽器所發出的請求是GET請求。METHOD=“POST”的 表單所發出的請 ...
  • "https://stackoverflow.com/questions/4424193/what happens to mutex when the thread which acquired it exits?noredirect=1&lq=1" 解釋當一個lock了mutex的線程退出了,卻沒 ...
  • 設置(CentOS 6 vs CentOS 7)系統常用配置 ysvinit vs Upstart vs Systemd) 常見設置: 字元集CentOS 6方法:/etc/sysconfig/i18n中的LANG=CentOS 7方法1:localectl set-locale LANG=方法2 ...
  • #!/bin/bash #set -x #date: 2018-12-13 #Description: 一鍵安裝LNMP環境 or LAMP 環境 #Version: 0.4 #Author: simon #定義命令搜索路徑 PATH=/bin:/sbin:/usr/bin:/usr/sbin:/u... ...
  • 1 server { 2 listen 8080; 3 server_name localhost; 4 5 #charset koi8-r; 6 charset utf-8; 7 8 #access_log logs/host.access.log main; 9 10 location / { ...
一周排行
    -Advertisement-
    Play Games
  • 示例項目結構 在 Visual Studio 中創建一個 WinForms 應用程式後,項目結構如下所示: MyWinFormsApp/ │ ├───Properties/ │ └───Settings.settings │ ├───bin/ │ ├───Debug/ │ └───Release/ ...
  • [STAThread] 特性用於需要與 COM 組件交互的應用程式,尤其是依賴單線程模型(如 Windows Forms 應用程式)的組件。在 STA 模式下,線程擁有自己的消息迴圈,這對於處理用戶界面和某些 COM 組件是必要的。 [STAThread] static void Main(stri ...
  • 在WinForm中使用全局異常捕獲處理 在WinForm應用程式中,全局異常捕獲是確保程式穩定性的關鍵。通過在Program類的Main方法中設置全局異常處理,可以有效地捕獲並處理未預見的異常,從而避免程式崩潰。 註冊全局異常事件 [STAThread] static void Main() { / ...
  • 前言 給大家推薦一款開源的 Winform 控制項庫,可以幫助我們開發更加美觀、漂亮的 WinForm 界面。 項目介紹 SunnyUI.NET 是一個基於 .NET Framework 4.0+、.NET 6、.NET 7 和 .NET 8 的 WinForm 開源控制項庫,同時也提供了工具類庫、擴展 ...
  • 說明 該文章是屬於OverallAuth2.0系列文章,每周更新一篇該系列文章(從0到1完成系統開發)。 該系統文章,我會儘量說的非常詳細,做到不管新手、老手都能看懂。 說明:OverallAuth2.0 是一個簡單、易懂、功能強大的許可權+可視化流程管理系統。 有興趣的朋友,請關註我吧(*^▽^*) ...
  • 一、下載安裝 1.下載git 必須先下載並安裝git,再TortoiseGit下載安裝 git安裝參考教程:https://blog.csdn.net/mukes/article/details/115693833 2.TortoiseGit下載與安裝 TortoiseGit,Git客戶端,32/6 ...
  • 前言 在項目開發過程中,理解數據結構和演算法如同掌握蓋房子的秘訣。演算法不僅能幫助我們編寫高效、優質的代碼,還能解決項目中遇到的各種難題。 給大家推薦一個支持C#的開源免費、新手友好的數據結構與演算法入門教程:Hello演算法。 項目介紹 《Hello Algo》是一本開源免費、新手友好的數據結構與演算法入門 ...
  • 1.生成單個Proto.bat內容 @rem Copyright 2016, Google Inc. @rem All rights reserved. @rem @rem Redistribution and use in source and binary forms, with or with ...
  • 一:背景 1. 講故事 前段時間有位朋友找到我,說他的窗體程式在客戶這邊出現了卡死,讓我幫忙看下怎麼回事?dump也生成了,既然有dump了那就上 windbg 分析吧。 二:WinDbg 分析 1. 為什麼會卡死 窗體程式的卡死,入口門檻很低,後續往下分析就不一定了,不管怎麼說先用 !clrsta ...
  • 前言 人工智慧時代,人臉識別技術已成為安全驗證、身份識別和用戶交互的關鍵工具。 給大家推薦一款.NET 開源提供了強大的人臉識別 API,工具不僅易於集成,還具備高效處理能力。 本文將介紹一款如何利用這些API,為我們的項目添加智能識別的亮點。 項目介紹 GitHub 上擁有 1.2k 星標的 C# ...