之前在一家直播團(tuán)隊(duì)做過(guò)一段時(shí)間的營(yíng)收部門(mén)負(fù)責(zé)人,榜單是直播平臺(tái)最通用的一種玩法,可以彰顯用戶(hù)的身份,刺激用戶(hù)之間的pk,從而增加平臺(tái)的營(yíng)收,下面介紹幾種榜單常見(jiàn)的玩法。
限時(shí)熱門(mén)榜玩法規(guī)則大致是每30分鐘,對(duì)主播收到打賞值進(jìn)行排行,其中有2類(lèi)排行榜,限時(shí)熱門(mén)總榜和限時(shí)熱門(mén)分區(qū)榜,這里使用自然30分鐘代表每個(gè)周期,每天有48個(gè)30分鐘,分別有1、2、3代表每天第1、2、3個(gè)30分鐘。
歐皇主播榜玩法規(guī)則大致是主播房間內(nèi)用戶(hù)抽到的冰晶城堡數(shù)量的排行,頁(yè)面上有3個(gè)榜單,昨日榜、今日榜、總榜。
直播重營(yíng)收,營(yíng)收看活動(dòng),活動(dòng)看打榜,所以這種榜單每個(gè)月都會(huì)以各種形式出現(xiàn),我們需要設(shè)計(jì)一套通用的榜單系統(tǒng),減輕后續(xù)工作量,這是背景。
榜單分析首先我們對(duì)業(yè)務(wù)進(jìn)行抽象:
我們抽象出一些關(guān)鍵詞:
用戶(hù)id(user_id)主播id(master_id)投喂(coin)時(shí)間分區(qū)時(shí)間有今日、昨日、自然30分鐘。從這些榜單中我們可以抽象出統(tǒng)一的一套規(guī)則,榜單類(lèi)型、榜單維度、榜單對(duì)象、榜單積分。
榜單規(guī)則榜單類(lèi)型同一種榜單類(lèi)型代表的是一類(lèi)榜單,這一類(lèi)榜單具備同一套邏輯規(guī)則,例如限時(shí)熱門(mén)榜,雖然每30分鐘會(huì)有一個(gè)榜單,但是這些榜單數(shù)據(jù)的規(guī)則是一致的。
限時(shí)熱門(mén)分區(qū)榜和限時(shí)熱門(mén)榜的規(guī)則是不一樣的,熱門(mén)分區(qū)限時(shí)榜統(tǒng)計(jì)的是分區(qū)的主播,限時(shí)熱門(mén)分區(qū)榜統(tǒng)計(jì)的是全區(qū)的主播。
需要注意的是,限時(shí)熱門(mén)分區(qū)榜和限時(shí)熱門(mén)榜也可抽象成一類(lèi)榜單。
榜單維度同一類(lèi)榜單可能會(huì)有多個(gè)榜單,例如限時(shí)熱門(mén)榜,每個(gè)自然30分鐘內(nèi)都會(huì)有一個(gè)榜單,每個(gè)的榜單都是不同的,或者說(shuō)是互不影響的。
限時(shí)熱門(mén)分區(qū)榜,每個(gè)自然30分鐘內(nèi)都會(huì)有一個(gè)榜單,這里自然30分鐘就是一個(gè)維度。
限時(shí)熱門(mén)分區(qū)榜,每個(gè)自然30分鐘內(nèi)*所有分區(qū)都會(huì)有一個(gè)榜單,這里自然30分鐘和分區(qū)就是一個(gè)維度。
歐皇主播日榜,活動(dòng)時(shí)間內(nèi)主播房間內(nèi)每日用戶(hù)抽到的冰晶城堡數(shù)量的排行,這里日就是一個(gè)維度。
歐皇主播日榜,活動(dòng)時(shí)間內(nèi)主播房間內(nèi)用戶(hù)抽到的冰晶城堡數(shù)量的排行,這里只有一個(gè)榜單數(shù)據(jù),維度為空。
榜單對(duì)象榜單對(duì)象指的是我們給誰(shuí)進(jìn)行排行,這個(gè)誰(shuí)可以是用戶(hù),也可以是主播,也可以是其他,例如限時(shí)熱門(mén)榜,這個(gè)榜單對(duì)象就是主播,我們需要給主播進(jìn)行排行。
榜單對(duì)象積分榜單對(duì)象積分比較簡(jiǎn)單,就是一個(gè)進(jìn)行排序的值,例如限時(shí)熱門(mén)榜,用戶(hù)消費(fèi)就是積分。
榜單實(shí)現(xiàn)榜單配置配置可以放在配置文件里面,或者可以通過(guò)后臺(tái)管理系統(tǒng)進(jìn)行管理,配置如下:
[[rank]]rankname = "master_luck_day" // 榜單類(lèi)型title = "歐皇主播日榜" // 榜單名稱(chēng),實(shí)際業(yè)務(wù)中沒(méi)有使用到,這里只做一個(gè)名稱(chēng)區(qū)分top = 100 // 榜單最多展示n條,和業(yè)務(wù)有關(guān)set = 86400 * 2 // redis set的過(guò)期時(shí)間,見(jiàn)下方說(shuō)明string_expire = 86400 // redis item的過(guò)期時(shí)間,見(jiàn)下方說(shuō)明customsort = 1 // 自定義排序規(guī)則,代表相同積分,先到的在前,見(jiàn)下方說(shuō)明[[rank]]rankname = "master_luck_total"title = "歐皇主播總榜" top = 100 set = 86400 * 30 // 假設(shè)活動(dòng)過(guò)期時(shí)間是30天string_expire = 86400 customsort = 2榜單接口這里只展示最常見(jiàn)的3個(gè)接口,其它接口請(qǐng)?jiān)诰唧w業(yè)務(wù)場(chǎng)景中添加。
incrScore:增加榜單積分,類(lèi)似于redis的incr;
請(qǐng)求參數(shù)
返回結(jié)果
{ "code": 0, "errcode": 0, "message": "ok", "errmsg": "ok", "data": { // 成功或失敗,失敗可以重試 "status": true }}getScore:獲取榜單分?jǐn)?shù)及榜單排名;
請(qǐng)求參數(shù)
返回結(jié)果
{ "code": 0, "errcode": 0, "message": "ok", "errmsg": "ok", "data": { // 分?jǐn)?shù) "score": 0, // 排名 "rank": 0 }}topScore:獲取榜單排名
請(qǐng)求參數(shù)
返回結(jié)果
{ "code": 0, "errcode": 0, "message": "ok", "errmsg": "ok", "data": { // 排名數(shù)據(jù) "data": [ { // rank_item "rank_item": 0, // 排名 "rank": 0, // 積分 "score": 0 } ] }}榜單表設(shè)計(jì)表設(shè)計(jì)如下,在實(shí)際使用中,需要注意分庫(kù)分表,索引也根據(jù)實(shí)際使用到的場(chǎng)景進(jìn)行添加,這里只展示唯一索引:
CREATE TABLE `rank` ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `rank_name` varchar(30) NOT NULL DEFAULT '0' COMMENT '榜單類(lèi)型', `rank_type` varchar(50) NOT NULL DEFAULT '' COMMENT '榜單維度', `rank_item` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '榜單對(duì)象', `score` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '積分', `extra_data` varchar(50) NOT NULL DEFAULT '擴(kuò)展數(shù)據(jù)', `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 '對(duì)象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='榜單更新日志表'更新榜單積分時(shí),同時(shí)會(huì)更新榜單日志表,通過(guò)事務(wù)更新,保持?jǐn)?shù)據(jù)一致性,通過(guò)msg_id保證冪等,接口如果調(diào)用失敗,可以重試,類(lèi)似于用戶(hù)花錢(qián)時(shí),會(huì)更新錢(qián)包數(shù)據(jù)同時(shí)會(huì)記錄流水?dāng)?shù)據(jù)。調(diào)用incr接口時(shí),會(huì)執(zhí)行下面的sql,這2條sql在同一事務(wù)中執(zhí)行。
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);需要注意的是數(shù)據(jù)庫(kù)會(huì)保存全量排行榜數(shù)據(jù)。
事務(wù)說(shuō)明使用事務(wù)更新是否有必要?能否直接通過(guò)緩存做冪等?
確實(shí)在一般情況下使用緩存做冪等(set key ... nx px),然后輔以日志查詢(xún)就足夠了,使用流水日志對(duì)一致性更好,同時(shí)查詢(xún)問(wèn)題更加方便,但是對(duì)數(shù)據(jù)庫(kù)的壓力更大,可以根據(jù)實(shí)際業(yè)務(wù)場(chǎng)景選用合適的技術(shù)方案。
榜單緩存設(shè)計(jì)在一般業(yè)務(wù)中,榜單只需要展示topn的排名數(shù)據(jù),例如top10,top100等,并且在有一定體量的公司中,數(shù)據(jù)庫(kù)都不能直接對(duì)外,必須在數(shù)據(jù)庫(kù)上層加一層緩存。
榜單排名數(shù)據(jù)榜單排名數(shù)據(jù)使用的是zset實(shí)現(xiàn),zset的key為榜單名稱(chēng)+子榜id, zset的member為對(duì)象id,score為榜單積分。更新榜單時(shí),做如下操作:
_rankListKey = "rank:list:%d:%s"rankListKey := fmt.Sprintf(_rankListKey, params.RankName, params.RankType)// 下面的redis操作可以使用一些優(yōu)化手段,例如pipline,此處為示例redis.zAdd(_rankListKey, score, rank_item) // score代表的是該榜單對(duì)象當(dāng)前的積分redis.Expire(_rankListKey, config.set_expire) // config.set_expire為配置set的的過(guò)期時(shí)間redis.zrembyscore(_rankListKey,0,last_rank_score - 1) //last_rank_score代表的是第top名的積分,刪除0到最后一名之間的數(shù)據(jù),保證數(shù)據(jù)只有top個(gè)zset的過(guò)期時(shí)間大于榜單更新最大時(shí)間,如下所示:
需要注意的是,zset的member數(shù)量是需要限制的,不然可能會(huì)有大key和熱key的問(wèn)題。
榜單積分?jǐn)?shù)據(jù)業(yè)務(wù)場(chǎng)景中需要展示某個(gè)主播具體的有多少積分。榜單排名數(shù)據(jù)使用的是string實(shí)現(xiàn), key為榜單類(lèi)型+榜單維度+榜單對(duì)象,value為榜單積分。
此處可能會(huì)有人會(huì)有疑惑,為啥會(huì)需要需要榜單積分緩存?
zset限制member數(shù)量大小;業(yè)務(wù)場(chǎng)景需要展示超過(guò)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 { // 回源數(shù)據(jù)庫(kù),查詢(xún)積分,得到rscore redis.set(rankItemKey, rank_item, rscore + params.score, config.string_expire) // config.string_expire為配置的的過(guò)期時(shí)間 err = nil return nil} else if err != nil { // 返回錯(cuò)誤,業(yè)務(wù)可以重試 return err}redis.incr(rankItemKey, params.rank_item,params.score)榜單積分緩存數(shù)據(jù)量會(huì)比榜單排名緩存多很多,過(guò)期時(shí)間可以根據(jù)redis服務(wù)容量進(jìn)行配置,可以在榜單更新時(shí)間內(nèi)失效。
最后給一個(gè)流程圖:
榜單更新流程榜單實(shí)現(xiàn)案例限時(shí)熱門(mén)榜/限時(shí)熱門(mén)分區(qū)榜實(shí)現(xiàn)當(dāng)用戶(hù)在直播間消費(fèi)時(shí),增加榜單數(shù)據(jù),參數(shù)入下:
歐皇主播日榜/歐皇主播總榜實(shí)現(xiàn)當(dāng)用戶(hù)在直播間抽獎(jiǎng)抽到指定道具時(shí),增加榜單數(shù)據(jù),參數(shù)如下:
進(jìn)階場(chǎng)景近7日榜的實(shí)現(xiàn)主播近七日收到用戶(hù)打賞之和的排行,這里近七日是一個(gè)滑動(dòng)窗口概念,例如20200420代表的是20200414 ~20200420這7日。
業(yè)務(wù)分析榜單維度,可以用日期來(lái)標(biāo)識(shí),例如20200420代表的是20200414 ~20200420這7日 榜單對(duì)象,主播 榜單積分,主播近7日收到的積分之和
方案1存在兩種榜單數(shù)據(jù),一個(gè)是七日的榜單數(shù)據(jù)(實(shí)際使用),一個(gè)是每日的榜單數(shù)據(jù)(輔助使用)。
每日凌晨啟動(dòng)定時(shí)任務(wù)將前6日的日榜數(shù)據(jù)加到近7日的榜單數(shù)據(jù)中,數(shù)據(jù)是從數(shù)據(jù)庫(kù)中獲取,獲取的是全量數(shù)據(jù),當(dāng)凌晨用戶(hù)投喂時(shí),會(huì)實(shí)時(shí)更新七日榜單的數(shù)據(jù),也就是說(shuō)腳本積分?jǐn)?shù)據(jù)和實(shí)時(shí)積分?jǐn)?shù)據(jù)是同時(shí)在跑的,理論上,當(dāng)腳本跑完時(shí),數(shù)據(jù)會(huì)是正確的。
這種方案好處是簡(jiǎn)單,可以快速實(shí)現(xiàn),壞處需要定時(shí)任務(wù),且數(shù)據(jù)不是平滑更新的,定時(shí)任務(wù)執(zhí)行期間數(shù)據(jù)不準(zhǔn)確。
方案2方案2沒(méi)有使用每日的輔助榜單數(shù)據(jù),每次更新數(shù)據(jù)時(shí)會(huì)同步更新今日的七日榜和后6天的七日榜,例如今天是2022-04-20,如果增加1積分,會(huì)同時(shí)更新20220420七日榜、20220422七日榜、20220423七日榜、20220424七日榜、20220425七日榜、20220426七日榜。
當(dāng)?shù)搅?6日時(shí),主播1的20220426七日榜的積分會(huì)為3;當(dāng)?shù)搅?7日時(shí),主播1的20220427七日榜的積分會(huì)為2;當(dāng)?shù)搅?8日時(shí),主播1的20220428七日榜的積分會(huì)為1;當(dāng)?shù)搅?9日時(shí),主播1的20220429七日榜的積分會(huì)為0。
這種方案好處是沒(méi)有定時(shí)任務(wù),數(shù)據(jù)是平滑更新的,壞處是接口請(qǐng)求會(huì)放大,同時(shí)會(huì)更新很多條數(shù)據(jù),基本無(wú)法支持近30天的場(chǎng)景,且業(yè)務(wù)調(diào)用較為復(fù)雜。
方案3更新數(shù)據(jù)時(shí)更新今日的七日榜數(shù)據(jù),同時(shí)更新明天的七日榜數(shù)據(jù)(如果沒(méi)有腳本相當(dāng)于是今日的日榜數(shù)據(jù)),并且記錄每日的數(shù)據(jù),每日中午會(huì)將前5日每日的數(shù)據(jù)加到明日的7日榜中。
我們一起看一下20220423七日榜的數(shù)據(jù)的正確性,20220423七日榜在2022-04-22增加積分1,在2022-04-22中午,將2022-04-17 ~ 2022-04-21這5天日榜的數(shù)據(jù)共2分加到了20220423七日榜中,在2022-04-23主播1增加1積分增加了積分1,主播積分為4。
這個(gè)方案的好處是數(shù)據(jù)是平滑更新的,可以實(shí)現(xiàn)任意時(shí)間階段的連續(xù)榜單,且調(diào)用簡(jiǎn)單,連續(xù)榜的邏輯已是在服務(wù)內(nèi)部實(shí)現(xiàn),壞處是實(shí)現(xiàn)較為復(fù)雜。
榜單積分相同如何排序?zset存在一個(gè)問(wèn)題,就是相同積分時(shí),zset會(huì)按照member的字典序進(jìn)行排序,有些業(yè)務(wù)場(chǎng)景,可能會(huì)對(duì)相同積分的也需要進(jìn)行排序,例如相同積分,先到在前。榜單配置中增加有customsort字段, 1代表按時(shí)間正序排序, 2代表按時(shí)間倒序排序。
數(shù)據(jù)庫(kù)存在custom_sort字段,如果按照時(shí)間正序排序,為負(fù)數(shù)的時(shí)間戳,如果按照時(shí)間倒序排序,為正數(shù)的時(shí)間戳。
每次更新積分?jǐn)?shù)據(jù)后,搜索數(shù)據(jù)庫(kù)與該對(duì)象積分相同的數(shù)據(jù)(最多top條,根據(jù)配置,下面用1000來(lái)說(shuō)明),sql語(yǔ)句為:
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積分加上一個(gè)小數(shù),從0.999至0,將相同的數(shù)據(jù)添加至zset之中,從而實(shí)現(xiàn)相同積分排序。
如何實(shí)現(xiàn)排名變化趨勢(shì)?有些榜單場(chǎng)景會(huì)有主播今日的排名會(huì)和逐日昨日的排名進(jìn)行比較,看是上升、下降還是不變?
例如主播今日投喂榜需要實(shí)現(xiàn)排名變化趨勢(shì),可以每天零點(diǎn)執(zhí)行腳本,獲取榜單上一個(gè)周期的排行數(shù)據(jù),也就是昨日的topn排名的主播排行信息,寫(xiě)到今日的榜單數(shù)據(jù)中,并且將昨日排名數(shù)據(jù),寫(xiě)到今日的排行數(shù)據(jù)中,字段使用extra_data,當(dāng)獲取榜單排行時(shí),可以獲取到extra_data數(shù)據(jù),當(dāng)前排名和昨日排名數(shù)據(jù)進(jìn)行比較即可得到變化趨勢(shì),若沒(méi)有獲取到extra_data數(shù)據(jù),即昨日沒(méi)有上排行榜,變化趨勢(shì)為向上。
這個(gè)方案有個(gè)小問(wèn)題,就是不夠平滑,但該功能實(shí)時(shí)性要求較小,可以忽略。extra_data怎么使用緩存、怎么平滑展示數(shù)據(jù)留個(gè)大家去思考。
以上就是一個(gè)實(shí)際業(yè)務(wù)場(chǎng)景,以及面對(duì)這個(gè)業(yè)務(wù)場(chǎng)景時(shí)候如何提升開(kāi)發(fā)效率的case。
好了,今天的分享就到這,喜歡的同學(xué)可以四連支持:
本人花費(fèi)2個(gè)月時(shí)間,整理了一套JAVA開(kāi)發(fā)技術(shù)資料,內(nèi)容涵蓋java基礎(chǔ),分布式、微服務(wù)等主流技術(shù)資料,包含大廠面經(jīng),學(xué)習(xí)筆記、源碼講義、項(xiàng)目實(shí)戰(zhàn)、講解視頻。
java面試資料
希望可以幫助一些想通過(guò)自學(xué)提升能力的朋友,領(lǐng)取資料,掃碼關(guān)注一下
記得轉(zhuǎn)發(fā)+關(guān)注+私信
私信回復(fù)【2022面試資料】
領(lǐng)取更多學(xué)習(xí)資料
掃描二維碼推送至手機(jī)訪問(wèn)。
版權(quán)聲明:本文由信途科技轉(zhuǎn)載于網(wǎng)絡(luò),如有侵權(quán)聯(lián)系站長(zhǎng)刪除。
轉(zhuǎn)載請(qǐng)注明出處http://macbookprostickers.com/xintu/73055.html