遇到的問題:
目前流行的直播行業(yè)中悯森,產(chǎn)品會鼓勵用戶多與主播互動宋舷,而互動火熱的體現(xiàn),則是通過上圖最右方的人氣值體現(xiàn)瓢姻。
目前常規(guī)的互動為聊天祝蝠,送禮,點贊幻碱,關(guān)注绎狭,宋星星,分享褥傍。其中聊天儡嘶,送禮,點贊恍风,都是頻率較高的互動蹦狂。
系統(tǒng)要根據(jù)這些互動計算圖右側(cè)的人氣值,并返回給用戶邻耕,而且人氣值需要持久處理鸥咖。
筆者所遇到的問題是,實時計算互動兄世,并存儲時啼辣,能否降低寫庫的次數(shù),以及推送IM的次數(shù)御滩。
如果當(dāng)前房間十分火熱鸥拧,
聊天tps500 ?送禮tps300 點贊tps500 我不希望我每秒會為了更新人氣,更新1000多次削解,并且IM推送上行1000多次富弦,IM下行是給房間里的
每一個人推送,那么次數(shù)更是不可設(shè)想氛驮。
如何減輕庫的寫壓力腕柜,以及IM服務(wù)的壓力,是這次要解決的問題矫废。
總體圖:
上圖是該業(yè)務(wù)的總體結(jié)構(gòu)圖盏缤,每個業(yè)務(wù)有自己的業(yè)務(wù)消息總線,如發(fā)言蓖扑,點贊等唉铜,消息中間件采用kafka,比如我現(xiàn)在是需要接受消息更新人氣律杠,用戶經(jīng)驗等潭流,所以我定義了一個新的消費組community-esb竞惋,消費組收到各業(yè)務(wù)線的消息后,通過調(diào)用人氣服務(wù)灰嫉,以及經(jīng)驗服務(wù)來更新主播的人氣值拆宛,互動發(fā)生源用戶的經(jīng)驗值。消息body基本都是這樣的熬甫,比如聊天:
{userId:<userId>,targetUserId<targetUserId>,roomId<roomId>,message<message>}
消費者會根據(jù)消息體內(nèi)的詳細(xì)內(nèi)容進(jìn)行加人氣處理胰挑,比如送禮會看禮物個數(shù)蔓罚,點贊會看點贊次數(shù)等椿肩,這里不再贅述。
上述規(guī)劃圖中豺谈,這個部分是要解決的問題
起初想到的三種解決方式:
A,
//增加人氣值郑象,用戶進(jìn)入房間獲取,或推送都從這里獲取
Long total=Increase(AnchorPopularKey,value);
//獲取鎖
String timeStamp = getAnchorLockKey(anchor)? 1
如果鎖存在茬末,并且未失效厂榛,則直接返回
If(value!=null && System.currentTime-value<15s){Return;}
//這里有并發(fā)問題
//鎖不存在,則設(shè)置鎖丽惭,并更新db 推送IM
setAnchorLockKey(anchor,random(3,12)s)
Insert DB total
sendIM total
A方式的大致思路為:緩存中存儲主播的總?cè)藲庵祷髂蹋瑯I(yè)務(wù)需要讀取時也從這里讀取,人氣服務(wù)計算好需要增加的人氣值時责掏,
直接調(diào)用redis increase操作柜砾,那么什么時候進(jìn)行落地呢?設(shè)置另外一個ttl key换衬,失效時間為3~10秒痰驱,當(dāng)key失效時,則進(jìn)行落地操作瞳浦,
以及推送IM操作担映,如果希望前端更新快,則時間可設(shè)置短一些叫潦。
優(yōu)點:這種方式網(wǎng)絡(luò)開銷相對較少蝇完,策略驗證是否需要落地只用了一次網(wǎng)絡(luò)IO
缺點:
? ? ? ? 1,因為redis存的是人氣總值矗蕊,而需要保證redis里的總值與數(shù)據(jù)庫總值完全相同較難短蜕。而且最后落地時,是將redis的總值
? ? ? ? ? ? ? 直接做覆蓋操作拔妥,很有可能總值之前是100忿危,覆蓋后變98
? ? ? ? ?2,最后的落地操作没龙,以及推送IM操作铺厨,存在并發(fā)問題缎玫,因為為了達(dá)到較大吞吐量,增加消費消息效率解滓,
? ? ? ? ? ? ? ?消費組的進(jìn)程數(shù)與kafka設(shè)置的patition數(shù)量相同赃磨,當(dāng)多個進(jìn)程同時消費時,可能會同時進(jìn)行落地操作洼裤,
? ? ? ? ? ? ? ?以及推送IM操作邻辉,比較浪費。
B腮鞍,
//增加人氣值值骇,//增加人氣值,用戶進(jìn)入房間獲取移国,或推送都從這里獲取
Long total=Increase(AnchorPopularKey,value);
//獲取鎖
Boolean set = setIfAbsent(anchorLock2(anchor),System.currentTime);? 1
If(set){
//獲取到了鎖吱瘩,入庫,推IM
Insert DB total;
sendIM total;
重新設(shè)置鎖(3~12秒)失效
setExpire(anchorLock2(anchor),random(3,12)s);
}else{
//獲取鎖的設(shè)置時間
timeStamp = get(anchorLock2(anchor));? 2
//如果鎖設(shè)置時間已過15秒迹缀,說明鎖已經(jīng)失效使碾,之前設(shè)置失效失敗,刪除鎖
if(System.currentTime-timeStamp>15s){
delete(anchorLock2);
}
}
方式B算是方式A的改進(jìn)祝懂,為了解決并發(fā)數(shù)據(jù)落地的問題票摇,不過這種方式,增加了一個分布式鎖砚蓬,多了一次網(wǎng)絡(luò)IO消耗矢门。
C,
TASK 60秒一次怜械。颅和。÷圃剩或10秒一次
for(Room room:ShowingRoomList){
//獲取緩存中的人氣值
Popular p = popularService.get(room);
//上次推送至今峡扩,房間人氣值有變化,給予推送更新
if(redis.get(room)!=p.getPopular()){
insertDb;
sendIm
redis.set(room)
}
}
C方式與A B方式完全不同障本,C是通過另起一個進(jìn)程跑task教届,然后異步將人氣值落地,以及推送IM驾霜,但是
還是需要消費者做redis.increase操作案训,只是消費者不再負(fù)責(zé)數(shù)據(jù)落地,以及推送了粪糙。
結(jié)論:
1强霎,起初設(shè)置一個緩存總?cè)藲庵凳菫榱隧槺憧棺x壓力,不過上述幾種方式經(jīng)過反復(fù)推演蓉冈,首先難以解決的問題就是城舞,如果你在緩存中存儲一個總值轩触,那么你用數(shù)據(jù)庫覆蓋緩存可以,如果用緩存覆蓋數(shù)據(jù)庫家夺,將十分危險脱柱,而且難以保證準(zhǔn)確性。
2拉馋,A,B方式均存在很多不必要的網(wǎng)絡(luò)開銷或數(shù)據(jù)庫開銷榨为,需要在兩者之間做均衡。C方式另起進(jìn)程煌茴,使得業(yè)務(wù)不聚合随闺,消費組不能自己
完成任務(wù),需要依賴另一個進(jìn)程景馁。而且板壮,單進(jìn)程存在單點問題逗鸣。不過C方式相對來講合住,更可控些,因為單進(jìn)程撒璧,所以不存在過多的數(shù)據(jù)庫開銷透葛,以及網(wǎng)絡(luò)開銷。
最后決定卿樱,棄用緩存存人氣總值的方式僚害,通過增量更新人氣總值。
設(shè)計圖如下:
代碼如下:
這種方式的思路是繁调,將一分鐘時間分為30個時間片萨蚕,線程將根據(jù)當(dāng)前系統(tǒng)時間,將人氣值累加存儲到當(dāng)前時間片內(nèi)蹄胰,
落地時則是存儲上一個時間片的累加值岳遥,并且為增量更新。這樣可控制一個房間內(nèi)的落地以及推送IM頻率裕寨,最多為30次浩蓉。
通過redis的原子操作,選取leader宾袜,來進(jìn)行落地捻艳,推送,失效操作庆猫,防止并發(fā)問題认轨。同時,當(dāng)房間內(nèi)互動頻率不高時月培,
則實時落地推送嘁字,增強用戶體驗昨稼。
當(dāng)然,我們現(xiàn)在的業(yè)務(wù)量還不大~服務(wù)上線后監(jiān)控圖如下:
由圖可見拳锚,互動產(chǎn)生累加人氣操作tps在晚上比較高假栓,可以達(dá)到90左右,而那時霍掺,實時落地的tps為2.5匾荆。也就是說房間內(nèi)就算互動再頻繁,
右上角的人氣變化大約是2秒一變杆烁。