大流量、業務效率?從一個榜單開始

葉小釵發表於2022-05-05

原創不易,求分享、求一鍵三連

業務場景

之前在一家直播團隊做過一段時間的營收部門負責人,榜單是直播平臺最通用的一種玩法,可以彰顯使用者的身份,刺激使用者之間的pk,從而增加平臺的營收,下面介紹幾種榜單常見的玩法。

限時熱門榜

玩法規則大致是每30分鐘,對主播收到打賞值進行排行,其中有2類排行榜,限時熱門總榜和限時熱門分割槽榜,這裡使用自然30分鐘代表每個週期,每天有48個30分鐘,分別有1、2、3代表每天第1、2、3個30分鐘。

大流量、業務效率?從一個榜單開始

歐皇主播榜

玩法規則大致是主播房間內使用者抽到的冰晶城堡數量的排行,頁面上有3個榜單,昨日榜、今日榜、總榜。

大流量、業務效率?從一個榜單開始

直播重營收,營收看活動,活動看打榜,所以這種榜單每個月都會以各種形式出現,我們需要設計一套通用的榜單系統,減輕後續工作量,這是背景。

榜單分析

首先我們對業務進行抽象:

大流量、業務效率?從一個榜單開始

我們抽象出一些關鍵詞:

  • 使用者id(user_id)
  • 主播id(master_id)
  • 投喂(coin)
  • 時間
  • 分割槽

時間有今日、昨日、自然30分鐘。從這些榜單中我們可以抽象出統一的一套規則,榜單型別、榜單維度、榜單物件、榜單積分。

榜單規則

  • 榜單型別

同一種榜單型別代表的是一類榜單,這一類榜單具備同一套邏輯規則,例如限時熱門榜,雖然每30分鐘會有一個榜單,但是這些榜單資料的規則是一致的。

限時熱門分割槽榜和限時熱門榜的規則是不一樣的,熱門分割槽限時榜統計的是分割槽的主播,限時熱門分割槽榜統計的是全區的主播。

需要注意的是,限時熱門分割槽榜和限時熱門榜也可抽象成一類榜單。

  • 榜單維度

同一類榜單可能會有多個榜單,例如限時熱門榜,每個自然30分鐘內都會有一個榜單,每個的榜單都是不同的,或者說是互不影響的。

限時熱門分割槽榜,每個自然30分鐘內都會有一個榜單,這裡自然30分鐘就是一個維度。

限時熱門分割槽榜,每個自然30分鐘內*所有分割槽都會有一個榜單,這裡自然30分鐘和分割槽就是一個維度。

歐皇主播日榜,活動時間內主播房間內每日使用者抽到的冰晶城堡數量的排行,這裡日就是一個維度。

歐皇主播日榜,活動時間內主播房間內使用者抽到的冰晶城堡數量的排行,這裡只有一個榜單資料,維度為空。

  • 榜單物件

榜單物件指的是我們給誰進行排行,這個誰可以是使用者,也可以是主播,也可以是其他,例如限時熱門榜,這個榜單物件就是主播,我們需要給主播進行排行。

  • 榜單物件積分

榜單物件積分比較簡單,就是一個進行排序的值,例如限時熱門榜,使用者消費就是積分。

榜單實現

  • 榜單配置

配置可以放在配置檔案裡面,或者可以通過後臺管理系統進行管理,配置如下:

[[rank]]
rankname = "master_luck_day"  // 榜單型別
title = "歐皇主播日榜" // 榜單名稱,實際業務中沒有使用到,這裡只做一個名稱區分
top = 100 // 榜單最多展示n條,和業務有關
set = 86400 * 2 // redis set的過期時間,見下方說明
string_expire = 86400 // redis item的過期時間,見下方說明
customsort = 1 // 自定義排序規則,代表相同積分,先到的在前,見下方說明
[[rank]]
rankname = "master_luck_total"
title = "歐皇主播總榜" 
top = 100  
set = 86400 * 30 // 假設活動過期時間是30天
string_expire = 86400 
customsort = 2
  • 榜單介面

這裡只展示最常見的3個介面,其它介面請在具體業務場景中新增。

incrScore:增加榜單積分,類似於redis的incr;

請求引數

大流量、業務效率?從一個榜單開始

返回結果

{
    "code": 0,
    "errcode": 0,
    "message": "ok",
    "errmsg": "ok",
    "data": {
        //  成功或失敗,失敗可以重試
        "status": true
    }
}

getScore:獲取榜單分數及榜單排名;

請求引數

大流量、業務效率?從一個榜單開始

返回結果

{
    "code": 0,
    "errcode": 0,
    "message": "ok",
    "errmsg": "ok",
    "data": {
        //  分數
        "score": 0,
        //  排名
        "rank": 0
    }
}

topScore:獲取榜單排名

請求引數

大流量、業務效率?從一個榜單開始

返回結果

{
    "code": 0,
    "errcode": 0,
    "message": "ok",
    "errmsg": "ok",
    "data": {
        //  排名資料
        "data": [
            {
                //  rank_item
                "rank_item": 0,
                //  排名
                "rank": 0,
                //  積分
                "score": 0
            }
        ]
    }
}

榜單表設計

表設計如下,在實際使用中,需要注意分庫分表,索引也根據實際使用到的場景進行新增,這裡只展示唯一索引:

CREATE TABLE `rank` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `rank_name` varchar(30) NOT NULL DEFAULT '0' COMMENT '榜單型別',
  `rank_type` varchar(50) NOT NULL DEFAULT '' COMMENT '榜單維度',
  `rank_item` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '榜單物件',
  `score` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '積分',
  `extra_data` varchar(50) NOT NULL DEFAULT '擴充套件資料',
  `rank` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '排名',
  `custom_sort` varchar(200) NOT NULL DEFAULT '' COMMENT '自定義排序',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_rank_id_rank_type_rank_item` (`rank_id`,`rank_type`,`rank_item`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='通用榜單表'
CREATE TABLE `rank_log` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `rank_name` varchar(30) NOT NULL DEFAULT '0' COMMENT '榜單id',
  `rank_type` varchar(50) NOT NULL DEFAULT '' COMMENT '子榜id',
  `rank_item` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '物件id',
  `msg_id` varchar(150) NOT NULL DEFAULT '' COMMENT '訊息',
  `change_score` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '變化的積分',
  `after_score` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '變化後的積分',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_rank_name_rank_type_rank_item_msg_id` (`rank_name`,`rank_type`,`rank_item`,`msg_id`),
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='榜單更新日誌表'

更新榜單積分時,同時會更新榜單日誌表,通過事務更新,保持資料一致性,通過msg_id保證冪等,介面如果呼叫失敗,可以重試,類似於使用者花錢時,會更新錢包資料同時會記錄流水資料。呼叫incr介面時,會執行下面的sql,這2條sql在同一事務中執行。

insert into rank(rank_name,rank_type,rank_item,score) values(params.rank_name,params.rank_type,params.rank_item,params.score) insert on dumplicate update score = params.score;
insert into rank_log(rank_name,rank_type,rank_item,score,msg_id) values(params.rank_name,params.rank_type,params.rank_item,params.score,params.msg_id);

需要注意的是資料庫會儲存全量排行榜資料。

事務說明

使用事務更新是否有必要?能否直接通過快取做冪等?

確實在一般情況下使用快取做冪等(set key ... nx px),然後輔以日誌查詢就足夠了,使用流水日誌對一致性更好,同時查詢問題更加方便,但是對資料庫的壓力更大,可以根據實際業務場景選用合適的技術方案。

榜單快取設計

在一般業務中,榜單隻需要展示topn的排名資料,例如top10,top100等,並且在有一定體量的公司中,資料庫都不能直接對外,必須在資料庫上層加一層快取。

  • 榜單排名資料

榜單排名資料使用的是zset實現,zset的key為榜單名稱+子榜id, zset的member為物件id,score為榜單積分。更新榜單時,做如下操作:

_rankListKey = "rank:list:%d:%s"
rankListKey := fmt.Sprintf(_rankListKey, params.RankName, params.RankType)
// 下面的redis操作可以使用一些優化手段,例如pipline,此處為示例
redis.zAdd(_rankListKey, score, rank_item) // score代表的是該榜單物件當前的積分
redis.Expire(_rankListKey,  config.set_expire) // config.set_expire為配置set的的過期時間
redis.zrembyscore(_rankListKey,0,last_rank_score - 1) //last_rank_score代表的是第top名的積分,刪除0到最後一名之間的資料,保證資料只有top個

zset的過期時間大於榜單更新最大時間,如下所示:

大流量、業務效率?從一個榜單開始

需要注意的是,zset的member數量是需要限制的,不然可能會有大key和熱key的問題。

  • 榜單積分資料

業務場景中需要展示某個主播具體的有多少積分。榜單排名資料使用的是string實現, key為榜單型別+榜單維度+榜單物件,value為榜單積分。

此處可能會有人會有疑惑,為啥會需要需要榜單積分快取?

  1. zset限制member數量大小;
  2. 業務場景需要展示超過topn的積分,如上第2張圖;
_rankItemKey = "rank:item:%d:%s:%d"
rankItemKey := fmt.Sprintf(_rankItemKey, params.RankName, params.RankType, params.RankItem)
score, err := redis.get(rankItemKey)
if err == redis.ErrNil {
    // 回源資料庫,查詢積分,得到rscore
    redis.set(rankItemKey, rank_item, rscore + params.score, config.string_expire)    // config.string_expire為配置的的過期時間
    err = nil
    return nil
} else if err != nil {
    // 返回錯誤,業務可以重試
    return err
}
redis.incr(rankItemKey, params.rank_item,params.score) 

榜單積分快取資料量會比榜單排名快取多很多,過期時間可以根據redis服務容量進行配置,可以在榜單更新時間內失效。

最後給一個流程圖:

榜單更新流程

大流量、業務效率?從一個榜單開始

榜單實現案例

  1. 限時熱門榜/限時熱門分割槽榜實現

當使用者在直播間消費時,增加榜單資料,引數入下:

大流量、業務效率?從一個榜單開始
  1. 歐皇主播日榜/歐皇主播總榜實現

當使用者在直播間抽獎抽到指定道具時,增加榜單資料,引數如下:

大流量、業務效率?從一個榜單開始

進階場景

近7日榜的實現

主播近七日收到使用者打賞之和的排行,這裡近七日是一個滑動視窗概念,例如20200420代表的是20200414 ~20200420這7日。

  • 業務分析

榜單維度,可以用日期來標識,例如20200420代表的是20200414 ~20200420這7日 榜單物件,主播 榜單積分,主播近7日收到的積分之和

  • 方案1

存在兩種榜單資料,一個是七日的榜單資料(實際使用),一個是每日的榜單資料(輔助使用)。

每日凌晨啟動定時任務將前6日的日榜資料加到近7日的榜單資料中,資料是從資料庫中獲取,獲取的是全量資料,當凌晨使用者投喂時,會實時更新七日榜單的資料,也就是說指令碼積分資料和實時積分資料是同時在跑的,理論上,當指令碼跑完時,資料會是正確的。

這種方案好處是簡單,可以快速實現,壞處需要定時任務,且資料不是平滑更新的,定時任務執行期間資料不準確。

  • 方案2

方案2沒有使用每日的輔助榜單資料,每次更新資料時會同步更新今日的七日榜和後6天的七日榜,例如今天是2022-04-20,如果增加1積分,會同時更新20220420七日榜、20220422七日榜、20220423七日榜、20220424七日榜、20220425七日榜、20220426七日榜。

大流量、業務效率?從一個榜單開始

當到了26日時,主播1的20220426七日榜的積分會為3;當到了27日時,主播1的20220427七日榜的積分會為2;當到了28日時,主播1的20220428七日榜的積分會為1;當到了29日時,主播1的20220429七日榜的積分會為0。

這種方案好處是沒有定時任務,資料是平滑更新的,壞處是介面請求會放大,同時會更新很多條資料,基本無法支援近30天的場景,且業務呼叫較為複雜。

  • 方案3

更新資料時更新今日的七日榜資料,同時更新明天的七日榜資料(如果沒有指令碼相當於是今日的日榜資料),並且記錄每日的資料,每日中午會將前5日每日的資料加到明日的7日榜中。

大流量、業務效率?從一個榜單開始

我們一起看一下20220423七日榜的資料的正確性,20220423七日榜在2022-04-22增加積分1,在2022-04-22中午,將2022-04-17 ~ 2022-04-21這5天日榜的資料共2分加到了20220423七日榜中,在2022-04-23主播1增加1積分增加了積分1,主播積分為4。

這個方案的好處是資料是平滑更新的,可以實現任意時間階段的連續榜單,且呼叫簡單,連續榜的邏輯已是在服務內部實現,壞處是實現較為複雜。

榜單積分相同如何排序?

zset存在一個問題,就是相同積分時,zset會按照member的字典序進行排序,有些業務場景,可能會對相同積分的也需要進行排序,例如相同積分,先到在前。榜單配置中增加有customsort欄位, 1代表按時間正序排序, 2代表按時間倒序排序。

資料庫存在custom_sort欄位,如果按照時間正序排序,為負數的時間戳,如果按照時間倒序排序,為正數的時間戳。

每次更新積分資料後,搜尋資料庫與該物件積分相同的資料(最多top條,根據配置,下面用1000來說明),sql語句為:

select item_id from rank where rank_name = params.rank_name and rank_type = params.rank_type and score = cur_score order by custom_sort desc limit 1000 

然後將score積分加上一個小數,從0.999至0,將相同的資料新增至zset之中,從而實現相同積分排序。

如何實現排名變化趨勢?

有些榜單場景會有主播今日的排名會和逐日昨日的排名進行比較,看是上升、下降還是不變?

大流量、業務效率?從一個榜單開始

例如主播今日投喂榜需要實現排名變化趨勢,可以每天零點執行指令碼,獲取榜單上一個週期的排行資料,也就是昨日的topn排名的主播排行資訊,寫到今日的榜單資料中,並且將昨日排名資料,寫到今日的排行資料中,欄位使用extra_data,當獲取榜單排行時,可以獲取到extra_data資料,當前排名和昨日排名資料進行比較即可得到變化趨勢,若沒有獲取到extra_data資料,即昨日沒有上排行榜,變化趨勢為向上。

這個方案有個小問題,就是不夠平滑,但該功能實時性要求較小,可以忽略。extra_data怎麼使用快取、怎麼平滑展示資料留個大家去思考。

以上就是一個實際業務場景,以及面對這個業務場景時候如何提升開發效率的case。

好了,今天的分享就到這,喜歡的同學可以四連支援:

大流量、業務效率?從一個榜單開始

想要更多交流可以加群討論:

 

相關文章