掃二維碼與項目經(jīng)理溝通
我們在微信上24小時期待你的聲音
解答本文疑問/技術(shù)咨詢/運營咨詢/技術(shù)建議/互聯(lián)網(wǎng)交流
從技術(shù)的角度來說,技術(shù)方案的選型都是受限于實際的業(yè)務(wù)場景,都以解決實際業(yè)務(wù)場景為目標。

在我們的實際業(yè)務(wù)場景中,需要以游戲的維度收集和上報行為數(shù)據(jù),考慮數(shù)據(jù)的量級,執(zhí)行盡最大努力交付且允許數(shù)據(jù)的部分丟棄。
數(shù)據(jù)上報支持游戲的維度的批量上報,支持同一款游戲128個行為進行批量上報。
數(shù)據(jù)上報需要時效控制,上報的數(shù)據(jù)必須是上報時刻的前3分鐘的數(shù)據(jù)。
整體數(shù)據(jù)的業(yè)務(wù)形態(tài)如下圖所示:
從業(yè)務(wù)的角度來說包含數(shù)據(jù)的收集和數(shù)據(jù)的上報,我們把數(shù)據(jù)的收集比作生產(chǎn)者,數(shù)據(jù)的上報比作消費者,是一個典型的生產(chǎn)消費模型。
生產(chǎn)消費模型在JVM進程內(nèi)部通過隊列+鎖或者無鎖的Disruptor來實現(xiàn),在跨進程場景下通過MQ(RocketMQ/kafka)進行處理解耦。
但是細化到具體業(yè)務(wù)場景來看,消息的消費有諸多限制,包括: 游戲維度的批量行為上報,行為上報的時效限制,細化到各個技術(shù)方案選型進行對比。
使用RocketMQ 或者Kafaka等消息隊列來存儲上報的消息,但是消費側(cè)需要考慮在業(yè)務(wù)進程中按照游戲維度進行聚合,其中技術(shù)細節(jié)涉及按照游戲維度進行拆分,在滿足消息時效性和批量性的前提下觸發(fā)上報。在這種方案下消息中間件扮演的角色本質(zhì)上消息的中轉(zhuǎn)站, 沒有解決任何業(yè)務(wù)場景中提及的游戲維度拆分、批量性和時效性。
在方案一的基礎(chǔ)上,尋求一種技術(shù)方案來解決游戲維度的 消息分組、批量消費 、時效性 。通過Redis的list結(jié)構(gòu)來實現(xiàn)隊列(進一步要求實現(xiàn)定長隊列)來解決游戲維度的消息分組;通過Redis的list支持的Lrange來實現(xiàn)批量消費;通過業(yè)務(wù)側(cè)的多線程來解決時效問題,針對高頻的游戲使用單獨的線程池進行處理,上述兩個手段能夠保證消費速度大于生產(chǎn)速度。
對比兩種方案后決定使用Redis的實現(xiàn)了一個偽消息中間件:
整體的技術(shù)方案如下圖所示:
步驟一:游戲維度的某行為數(shù)據(jù)PUSH到游戲維度的隊列當中。
步驟二:判斷游戲是否在游戲的集合Set中,如果在就直接返回,如果不在進行步驟三。
步驟三:往游戲列表中PUSH游戲。
步驟一:從游戲?qū)ο蟮牧斜碇醒h(huán)取出一款游戲。
步驟二:通過步驟一獲取的游戲?qū)ο笕ピ撚螒驅(qū)ο蟮男袨閿?shù)據(jù)隊列中批量獲取數(shù)據(jù)處理。
在Redis的支持命令中,在List和Set的基礎(chǔ)命令,結(jié)合Lua腳本來實現(xiàn)整個技術(shù)方案。
消息數(shù)據(jù)層面,通過單獨的List循環(huán)維護待消費的游戲維度的數(shù)據(jù),每個游戲維度使用定長的List來保存消息。
消息生產(chǎn)過程中,通過結(jié)合List的llen+lpop+rpush來實現(xiàn)游戲維度的定長隊列,保證隊列的長度可控。
消息消費過程中,通過結(jié)合List的lrange+ltrim來實現(xiàn)游戲維度的消息的批量消費。
在整個執(zhí)行的復(fù)雜度層面,需要保證時間復(fù)雜度在0(N)常量維度,保證時間可控。
EVAL script numkeys key [key ...] arg [arg ...]
時間復(fù)雜度:取決于腳本本身的執(zhí)行的時間復(fù)雜度。
> eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second
1) "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解釋器去運行所有命令,我們可以保證,腳本的執(zhí)行是原子性的。作用就類似于加了MULTI/EXEC。
LLEN key
計算List的長度
時間復(fù)雜度:O(1)。
LPOP key [count]
從List的左側(cè)移除元素
時間復(fù)雜度:O(N),N為移除元素的個數(shù)。
RPUSH key element [element ...]
從List的右側(cè)保存元素
時間復(fù)雜度:O(N),N為保存元素的個數(shù)。
LRANGE key start end
時間復(fù)雜度:O(S+N), S為偏移量start, N為指定區(qū)間內(nèi)元素的數(shù)量。
下標(index)參數(shù) start 和 stop 都以 0 為底,也就是說,以 0 表示列表的第一個元素,以 1 表示列表的第二個元素,以此類推。
你也可以使用負數(shù)下標,以 -1 表示列表的最后一個元素, -2 表示列表的倒數(shù)第二個元素,以此類推。
LTRIM key start stop
時間復(fù)雜度:O(N) where N is the number of elements to be removed by the operation.
修剪(trim)一個已存在的 list,這樣 list 就會只包含指定范圍的指定元素。
SADD key member [member ...]
往Set集合添加數(shù)據(jù)。
時間復(fù)雜度:O(1)。
SISMEMBER key member
判斷Set集合是否存在元素。
時間復(fù)雜度:O(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";
執(zhí)行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));
執(zhí)行效果
實現(xiàn)固長隊列的數(shù)據(jù)存儲并設(shè)置過期時間
定義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";
執(zhí)行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));
執(zhí)行效果
取固定數(shù)量的對象,然后保留隊列的剩余的消息對象。
在描述完方案的原理和實現(xiàn)細節(jié)之后,進一步對適用的業(yè)務(wù)場景進行下總結(jié)。整體方案是基于redis的基本數(shù)據(jù)結(jié)構(gòu)構(gòu)建一個偽消息隊列,用以解決 消息的單個生產(chǎn)批量消費 的場景,通過多key形式實現(xiàn)消息隊列的多Topic模式,重要的是能夠借助于redis的原生能力在O(N)的時間復(fù)雜度完成批量消費。另外該方案也可以降級作為實現(xiàn)先進先出定長的日志隊列。
本文主要探索在特定業(yè)務(wù)場景下通過Redis的原生命令實現(xiàn)類MQ的功能,創(chuàng)新式的通過Lua腳本組合Redis的List的基礎(chǔ)命令,實現(xiàn)了消息的分組,消息的定長隊列,消息的批量消費功能;整體解決方案在線上環(huán)境落地并平穩(wěn)運行,為特定場景提供了一種通用的解決方案。

我們在微信上24小時期待你的聲音
解答本文疑問/技術(shù)咨詢/運營咨詢/技術(shù)建議/互聯(lián)網(wǎng)交流