您的位置:  首頁 > 技術 > 中間件 > 正文

Redis 定長隊列的探索和實踐

2022-08-22 10:39 https://my.oschina.net/vivotech/blog/5562479 vivo互聯網技術 次閱讀 條評論

vivo 互聯網服務器團隊 - Wang Zhi


一、業務背景


從技術的角度來說,技術方案的選型都是受限于實際的業務場景,都以解決實際業務場景為目標。


在我們的實際業務場景中,需要以游戲的維度收集和上報行為數據,考慮數據的量級,執行盡最大努力交付且允許數據的部分丟棄。


數據上報支持游戲的維度的批量上報,支持同一款游戲128個行為進行批量上報。


數據上報需要時效控制,上報的數據必須是上報時刻的前3分鐘的數據。


整體數據的業務形態如下圖所示:




二、技術選型


從業務的角度來說包含數據的收集和數據的上報,我們把數據的收集比作生產者,數據的上報比作消費者,是一個典型的生產消費模型。


生產消費模型在JVM進程內部通過隊列+鎖或者無鎖的Disruptor來實現,在跨進程場景下通過MQ(RocketMQ/kafka)進行處理解耦。


但是細化到具體業務場景來看,消息的消費有諸多限制,包括:游戲維度的批量行為上報,行為上報的時效限制,細化到各個技術方案選型進行對比。


方案一

使用RocketMQ 或者Kafaka等消息隊列來存儲上報的消息,但是消費側需要考慮在業務進程中按照游戲維度進行聚合,其中技術細節涉及按照游戲維度進行拆分,在滿足消息時效性和批量性的前提下觸發上報。在這種方案下消息中間件扮演的角色本質上消息的中轉站,沒有解決任何業務場景中提及的游戲維度拆分、批量性和時效性。


方案二

在方案一的基礎上,尋求一種技術方案來解決游戲維度的消息分組、批量消費 、時效性。通過Redis的list結構來實現隊列(進一步要求實現定長隊列)來解決游戲維度的消息分組;通過Redis的list支持的Lrange來實現批量消費;通過業務側的多線程來解決時效問題,針對高頻的游戲使用單獨的線程池進行處理,上述兩個手段能夠保證消費速度大于生產速度。


方案對比

對比兩種方案后決定使用Redis的實現了一個偽消息中間件:

  1. 通過List對象實現定長隊列來保存游戲維度的行為消息(以游戲作為key的List對象來保存用戶行為);

  2. 通過List來保存所有的存在行為數據的游戲列表;

  3. 通過Set來進行去重判斷來保證2中的List對象的唯一性。


整體的技術方案如下圖所示:



生產過程

步驟一:游戲維度的某行為數據PUSH到游戲維度的隊列當中。

步驟二:判斷游戲是否在游戲的集合Set中,如果在就直接返回,如果不在進行步驟三。

步驟三:往游戲列表中PUSH游戲。

消費過程

步驟一:從游戲對象的列表中循環取出一款游戲。

步驟二:通過步驟一獲取的游戲對象去該游戲對象的行為數據隊列中批量獲取數據處理。


三、技術原理


在Redis的支持命令中,在List和Set的基礎命令,結合Lua腳本來實現整個技術方案。


消息數據層面,通過單獨的List循環維護待消費的游戲維度的數據,每個游戲維度使用定長的List來保存消息。


消息生產過程中,通過結合List的llen+lpop+rpush來實現游戲維度的定長隊列,保證隊列的長度可控。


消息消費過程中,通過結合List的lrange+ltrim來實現游戲維度的消息的批量消費。


在整個執行的復雜度層面,需要保證時間復雜度在0(N)常量維度,保證時間可控。


3.1 Lua 腳本

EVAL?script?numkeys?key?[key?...]?arg?[arg?...]????時間復雜度:取決于腳本本身的執行的時間復雜度。?>?eval?"return?{KEYS[1],KEYS[2],ARGV[1],ARGV[2]}"?2?key1?key2?first?second1)?"key1"2)?"key2"3)?"first"4)?"second"?Redis?uses?the?same?Lua?interpreter?to?run?all?the?commands.Also?Redis?guarantees?that?a?script?is?executed?in?an?atomic?way:no?other?script?or?Redis?command?will?be?executed?while?a?script?is?being?executed.This?semantic?is?similar?to?the?one?of?MULTI?/?EXEC.From?the?point?of?view?of?all?the?other?clients?the?effects?of?a?script?are?either?still?not?visible?or?already?completed.
? ?

Redis采用相同的Lua解釋器去運行所有命令,我們可以保證,腳本的執行是原子性的。作用就類似于加了MULTI/EXEC。

  • Lua 腳本內多個命令以原子性的方式執行,保證了命令執行的線程安全。

  • Lua 腳本結合List命令實現定長隊列,實現批量消費。

  • Lua 腳本僅支持單個key的操作,不支持多key的操作。


3.2 List 對象

LLEN?key????計算List的長度????時間復雜度:O(1)。?LPOP?key?[count]????從List的左側移除元素????時間復雜度:O(N),N為移除元素的個數。?RPUSH?key?element?[element?...]????從List的右側保存元素????時間復雜度:O(N),N為保存元素的個數。
? ? ? ? ? ? ? ?
  • List的基礎命令包括計算List的長度,移除數據,添加數據,整體命令的復雜度都在O(N)的常量時間。

  • 整合上述三個命令,我們能保證實現固定長度的隊列,通過判斷隊列長度是否達到定長結合新增隊列元素和移除隊列元素來完成。


LRANGE?key?start?end????時間復雜度:O(S+N),?S為偏移量start,?N為指定區間內元素的數量。??????下標(index)參數?start?和?stop?都以?0?為底,也就是說,以?0?表示列表的第一個元素,以?1?表示列表的第二個元素,以此類推。????你也可以使用負數下標,以?-1?表示列表的最后一個元素,?-2?表示列表的倒數第二個元素,以此類推。?LTRIM?key?start?stop????時間復雜度:O(N)?where?N?is?the?number?of?elements?to?be?removed?by?the?operation.?????修剪(trim)一個已存在的?list,這樣?list?就會只包含指定范圍的指定元素。
? ? ? ? ? ? ? ?
  • List的基礎命令包括批量返回數據和裁剪數據,整體命令的復雜度都在O(N)的常量時間。

  • 整合上述兩個命令,我們能夠批量消費數據并移除隊列數據,通過LRANGE批量返回數據并通過LTRIM保留剩余數據。


3.3 Set 對象

SADD?key?member?[member?...]????往Set集合添加數據。????時間復雜度:O(1)。?????SISMEMBER?key?member????判斷Set集合是否存在元素。????時間復雜度:O(1)。
? ? ? ? ? ? ? ?
  • 通過Set集合來保證數據的唯一性,且時間復雜度可控。


四、技術應用


4.1 生產消息

定義LUA腳本???????CACHE_NPPA_EVENT_LUA?=????????"local?retVal?=?0?"?+????????"local?key?=?KEYS[1]?"?+????????"local?num?=?tonumber(ARGV[1])?"?+????????"local?val?=?ARGV[2]?"?+????????"local?expire?=?tonumber(ARGV[3])?"?+????????"if?(redis.call('llen',?key)?<?num)?then?redis.call('rpush',?key,?val)?"?+????????"else?redis.call('lpop',?key)?redis.call('rpush',?key,?val)?retVal?=?1?end?"?+????????"redis.call('expire',?key,?expire)?return?retVal";??執行LUA腳本????String?data?=?JSON.toJSONString(nppaBehavior);????Long?retVal?=?(Long)jedisClusterTemplate.eval(CACHE_NPPA_EVENT_LUA,?1,?NPPA_PREFIX?+?nppaBehavior.getGamePackage(),?String.valueOf(MAX_GAME_EVENT_PER_GAME),?data,?String.valueOf(NPPA_TTL_MINUTE?*?60));?執行效果????實現固長隊列的數據存儲并設置過期時間
? ?


  • 通過整合llen+rpush+lpop三個命令實現定長隊列。

  • 通過lua腳本保證上述命令的原子性執行。



  • 整體的執行流程如上圖所示,核心理念通過lua腳本的原子性保證了隊列長度計算(llen)、隊列數據移除(lpop)、隊列數據保存(rpush)的原子性執行。


4.2 消費消息

定義LUA腳本????QUERY_NPPA_EVENT_LUA?=????????"local?data?=?{}?"?+????????"local?key?=?KEYS[1]?"?+????????"local?num?=?tonumber(ARGV[1])?"?+????????"data?=?redis.call('lrange',?key,?0,?num)?redis.call('ltrim',?key,?num+1,?-1)?return?data";?執行LUA腳本????Integer?batchSize?=?NppaConfigUtils.getInteger("nppa.report.batch.size",?1);????Object?result?=?jedisClusterTemplate.eval(QUERY_NPPA_EVENT_LUA,?1,NPPA_PREFIX?+?gamePackage,?String.valueOf(batchSize));?執行效果????取固定數量的對象,然后保留隊列的剩余的消息對象。
? ?
  • 通過整合lrange+ltrim兩個命令實現消息的批量消費。

  • 通過lua腳本保證上述命令的原子性執行。



  • 整體的執行流程如上圖所示,核心理念通過lua腳本的原子性保證了數據獲取(Lrange)和數據裁剪(Ltrim)的原子性執行。

  • 整體的消費流程選擇pull模式,通過多線程循環輪詢可消費的隊列進行消費。與借助于redis的pub/sub的通知機制實現消費流程的push模式相比,pull模式成本更低效果更佳。


4.3 注意事項


  • Redis集群模式下,執行Lua腳本建議傳單key,多key會報重定向錯誤。

  • 在不同的Redis版本下,Lua腳本針對null的返回值處理不同,參考官方文檔。

  • 消費者的消費過程中通過循環遍歷游戲列表,然后根據游戲去獲取對應的消息對象,但是不同的游戲對應的熱度不同,所以在消費端我們通過配置的方式為熱門游戲單獨開啟消費線程進行消費,相當于針對不同游戲配置不同優先級的消費者。


五、線上效果





  • 生產和消費的QPS約為1w qps左右,整體上報QPS通過批量上報后會遠低于生產的消息生產和消費的QPS。

  • 整體數據的使用游戲包名作為key進行存儲,性能上不存在熱點的問題。


六、適用場景


在描述完方案的原理和實現細節之后,進一步對適用的業務場景進行下總結。整體方案是基于redis的基本數據結構構建一個偽消息隊列,用以解決消息的單個生產批量消費的場景,通過多key形式實現消息隊列的多Topic模式,重要的是能夠借助于redis的原生能力在O(N)的時間復雜度完成批量消費。另外該方案也可以降級作為實現先進先出定長的日志隊列。


七、總結


本文主要探索在特定業務場景下通過Redis的原生命令實現類MQ的功能,創新式的通過Lua腳本組合Redis的List的基礎命令,實現了消息的分組,消息的定長隊列,消息的批量消費功能;整體解決方案在線上環境落地并平穩運行,為特定場景提供了一種通用的解決方案。



END


猜你喜歡

? ? ? ? ? ? ? ?


本文分享自微信公眾號 - vivo互聯網技術(vivoVMIC)。
如有侵權,請聯系 support@oschina.cn 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

展開閱讀全文 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?
  • 0
    感動
  • 0
    路過
  • 0
    高興
  • 0
    難過
  • 0
    搞笑
  • 0
    無聊
  • 0
    憤怒
  • 0
    同情
熱度排行
友情鏈接
18禁高潮出水呻吟娇喘mp3,日本熟妇乱人伦A片免费高清,成人午夜精品无码区,狠狠色噜噜色狠狠狠综合久久,麻豆一区二区99久久久久,年轻的妈妈4,少妇被又大又粗又爽毛片,护士张开腿让我爽了一夜,男男互攻互受h啪肉np文,你好神枪手电视剧免费观看啊,97人妻一区二区精品免费,久久久婷婷五月亚洲97号色,freegaysexvideos男男中国,国产精品国产三级国av麻豆,国产精品又黄又爽又色无遮挡网站,亚洲av无码一区二区三区网站,亚洲国产精品久久久久蜜桃,国产真人无码作爱视频免费,国产成人精品亚洲一区二区三区,亚洲欧洲日产最新,老司机带带我精彩免费,国产成人久久精品激情,日本最新av免费一区二区三区,边摸边吃奶又黄又激烈视频
<蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <文本链> <文本链> <文本链> <文本链> <文本链> <文本链>