運(yùn)營活動需要實(shí)現(xiàn)一個(gè)投票打榜的功能:用戶通過做任務(wù)獲取票,獲得票之后點(diǎn)擊頁面的“投票”按鈕,給主播加票撮执;打榜頁面會根據(jù)主播的得票數(shù)對主播進(jìn)行排序,展示榜單前N名舷丹。
一抒钱、榜單存儲設(shè)計(jì)
方案1:zset
對于上述榜單功能的設(shè)計(jì),最簡單的方式就是利用redis的zset結(jié)構(gòu)存儲榜單的排名颜凯,key為本次榜單的標(biāo)識符谋币,member存儲主播的id,score存儲主播的票數(shù)症概。但我們都知道zset會有個(gè)存儲的數(shù)量限制蕾额,推薦的最大值是5000,如果說榜單排序的主播個(gè)數(shù)超過上述限制彼城,那此時(shí)需要設(shè)計(jì)更為復(fù)雜的方案诅蝶。
方案2:zset+k-v
如果參與榜單排名的主播數(shù)過多退个,只用zset存儲容易導(dǎo)致大key問題,這時(shí)候需要搭配redis k-v存儲所有主播的票數(shù)调炬。具體解決方案如下:
1. 榜單只存儲topN個(gè)主播的票數(shù)语盈,比如榜單頁面需要展示top100的主播票數(shù),N=100即可缰泡;
2. 所有主播的票數(shù)單獨(dú)存儲刀荒,采用redis k-v存儲票數(shù)。key為主播id棘钞,value代表主播的票數(shù)缠借。
我們本次的功能因?yàn)橹鞑?shù)較多,因此采用方案2實(shí)現(xiàn)武翎。
二烈炭、榜單更新
榜單更新方式
方案1:同步更新
同步更新是最簡單的實(shí)現(xiàn)方式,在用戶調(diào)用投票接口時(shí)直接更新主播的票數(shù)和榜單的排名宝恶,但這個(gè)方案直接訪問redis符隙,只適用于活動流量較小的情況。從穩(wěn)定性的角度考慮垫毙,建議走下面的異步更新方案霹疫。
方案2:異步更新
考慮到活動過程中,容易出現(xiàn)突發(fā)流量综芥,同步更新的方式從穩(wěn)定性的考慮角度不建議丽蝎,因此采用異步更新的方式來實(shí)現(xiàn)。
將用戶投票的消息異步發(fā)送kafka膀藐,在consumer內(nèi)異步處理給主播投票的消息屠阻,給主播加票,同時(shí)更新榜單排名额各。
三国觉、數(shù)據(jù)一致性
榜單更新邏輯
基于zset+kv的實(shí)現(xiàn)方式,用戶給主播投票虾啦,我們需要更新兩個(gè)地方的數(shù)據(jù)
1. 更新主播在kv里的票數(shù)
2. 更新主播在zset的票數(shù)
上述的更新過程是兩個(gè)操作麻诀,于是在實(shí)際更新榜單的過程中,可能會導(dǎo)致一些特殊case的出現(xiàn)傲醉,下面我們來分case具體分析下解決方案蝇闭。
case1:并發(fā)更新導(dǎo)致的數(shù)據(jù)不一致
假設(shè)有兩個(gè)用戶A、B同時(shí)對主播進(jìn)行投票+10票硬毕,主播的原始票數(shù)是10呻引;兩個(gè)操作的順序有可能會導(dǎo)致數(shù)據(jù)不一致
由圖中可以看到,解決并發(fā)更新最簡單的方式是將上述兩步操作用lua腳本封裝成原子操作昭殉,但是目前公司的redis使用規(guī)范不提供使用lua腳本苞七,因?yàn)槟_本復(fù)雜度不可控藐守,容易造成一些難以定位原因的redis故障挪丢,我們嘗試想一些別的方法蹂风。
主播更新票數(shù)主要分為兩種情況,用戶上榜時(shí)直接更新榜單內(nèi)的票數(shù)乾蓬,用戶不上榜時(shí)更新kv數(shù)據(jù)惠啄,再用新的票數(shù)去更新榜單數(shù)據(jù)。于是最后的方案定位以下四步:
1任内、先假設(shè)主播在榜上撵渡,使用zIncrByParams().xx()指令直接更新榜單上的票數(shù);如果成功死嗦,直接走步驟4趋距;失敗走步驟2;
2越除、失敗說明主播不在榜上节腐,查詢主播當(dāng)前票數(shù)+delta之后,使用zIncrByParams().nx()指令更新榜單上的票數(shù)摘盆;如果更新成功翼雀,說明此時(shí)沒有別人更新,走步驟4孩擂;如果更新失敗狼渊,說明此時(shí)有別人先一步讓主播上榜,走步驟3类垦;
3狈邑、步驟2失敗,說明此時(shí)主播已經(jīng)被更新到榜上了蚤认;重復(fù)步驟1米苹,使用zIncrByParams().xx()指令直接更新榜單上的票數(shù)。
4烙懦、更新kv內(nèi)的主播票數(shù)驱入,zincrBy即可。
這個(gè)方案保證了主播在榜上更新票數(shù)的原子性氯析,對于主播不在榜上的情況對票數(shù)更新加分布式鎖亏较,保證同時(shí)只有一個(gè)線程更新票數(shù),能解決大多數(shù)情況下并發(fā)更新導(dǎo)致的數(shù)據(jù)不一致情況掩缓。
case2: 距離上一名-1
除了上述并發(fā)更新的問題雪情,主播票數(shù)存儲在兩個(gè)地方,如何保證獲取到的主播票數(shù)一致性是一個(gè)問題你辣。這里踩了另外一個(gè)坑巡通。
筆者在展示榜單排序時(shí)尘执,以zset的排序?yàn)闇?zhǔn)獲取主播的排序,但展示票數(shù)時(shí)請求了k-v內(nèi)單獨(dú)存儲的票數(shù)宴凉。上述的實(shí)現(xiàn)在實(shí)際過程中誊锭,出現(xiàn)了一個(gè)意外的case:距離上一名的票數(shù)計(jì)算出來是-1。初始以為是redis更新k-v和更新zset時(shí)有失敗的情況弥锄,但后續(xù)實(shí)際增加監(jiān)控發(fā)現(xiàn)redis的可用性還是很高的丧靡,基本不存在這種情況。
在此假設(shè)基礎(chǔ)上籽暇,最有可能的情況是在更新k-v和更新redis的中間請求了數(shù)據(jù)温治,導(dǎo)致兩部分?jǐn)?shù)據(jù)一致,出現(xiàn)了距離上一名票數(shù)-1的極端case戒悠。
最終的實(shí)現(xiàn)方案是熬荆,榜單頁面展示時(shí)上榜主播的票數(shù)以zset內(nèi)存儲的為準(zhǔn),保證排名和票數(shù)獲取數(shù)據(jù)來源的一致性绸狐,單獨(dú)查詢主播票數(shù)時(shí)以k-v內(nèi)存儲的票數(shù)為準(zhǔn)卤恳,即可避免上述問題的出現(xiàn)。
四六孵、風(fēng)控和安全
榜單漏出的數(shù)據(jù)除了上述問題之外纬黎,還需要考慮數(shù)據(jù)的安全性,比如上榜主播昵稱是否滿足基本的風(fēng)控要求劫窒,需要具有緊急下榜的能力本今。
五、榜單更新的實(shí)時(shí)性
榜單更新我們采用的是異步操作主巍,那么前端頁面展示部分冠息,如何去保證榜單頁面更新的實(shí)時(shí)性呢?
初始以為直接前端mock票數(shù)即可孕索,后來發(fā)現(xiàn)榜單頁面的更新逛艰,除了更新票數(shù)之外,還涉及到排名部分的更新搞旭,如何保證榜單頁面實(shí)時(shí)展示給用戶是個(gè)亟待解決的問題散怖。
我們此處采用的是和前端配合的形式,后端接口榜單整個(gè)異步操作的延時(shí)基本可以保證在1秒以內(nèi)肄渗,前端用戶投票之后延遲1秒去請求接口镇眷,1秒之后請求的接口數(shù)據(jù)基本為新數(shù)據(jù),這個(gè)過程中給用戶的體驗(yàn)基本是實(shí)時(shí)更新了榜單頁面翎嫡。
六欠动、定榜
榜單的話一般會涉及到一個(gè)定榜操作,對于榜單topN的數(shù)據(jù)會發(fā)放一些獎(jiǎng)勵(lì)等等,因此主播和觀眾對于榜單定榜的結(jié)果較為關(guān)注具伍,定榜的實(shí)時(shí)性也是一個(gè)較為重要的問題翅雏。
針對上述投票打榜的活動,投票接口我們會限制只有活動時(shí)間內(nèi)可投票人芽。但因?yàn)槲覀冞x擇的方案是異步更新榜單排序望几,用戶的投票消息會延遲幾秒后處理,所以我們的定榜時(shí)間要延遲于活動的結(jié)束時(shí)間啼肩。那如果消息延遲過大怎么辦呢橄妆,遲遲不頂榜衙伶?
最簡單的方式是設(shè)定一個(gè)最大延時(shí)定榜時(shí)間祈坠,異步處理consumer內(nèi)增加系統(tǒng)時(shí)間判斷,如果系統(tǒng)時(shí)間大于最大延時(shí)定榜時(shí)間的話矢劲,其他的投票消息不處理赦拘,即可達(dá)到定榜的結(jié)果。