需求是在緩存最近一周內用戶所有消息列表,考慮用Redis 存儲;為每個存儲一個獨立Sorted Set,value 為消息體,Score 為MessageId,用以實現增量消息同步。
問題就來了:Sorted Set 怎么清理?
-設計內存容量只允許放一周內最新的,太久了緩存意義不大,太浪費。
-再者存在百萬級/s群發請求,不允許寫入時觸發清理。
理想模型:如果使用磁盤則使用MyIsam堆表,數據按照順序寫入,再建立以uid為索引,刪除卻是完全順序的。內存里面的話Hash 表 + RB 樹兩個維度索引,RB樹可按照時間順序清理。
解決方案:
- 寫入第一條時,設置一周過期時間
判斷是否第一條:zadd key 0 0 value score 返回2 說明第一條,1 不是第一條,只是多一條0的數據
- 用戶每天第一次登陸,觸發一次清理
清理需要遍歷Sorted Set上,消息一般不小,浪費io流量了,所以考慮采用lua 腳本實現。
- 這樣保證,通過pipeline只是高並發寫入,同時保證活躍用戶一周內消息都在內存(不活躍不保證),清理簡單
清理腳本如下:
local ltime = 0 local dels = 0 local lefts = 0 local list = redis.call("ZRANGE", KEYS[1], 1, -1) if(list[1] == nil) then return {-1, 0} end for _,v in ipairs(list) do if lefts == 0 then ltime = struct.unpack('<i', v) if ltime < tonumber(KEYS[3]) then dels = dels + 1 else lefts = lefts + 1 end else lefts = lefts + 1 end end if lefts > tonumber(KEYS[2]) then dels = dels + (lefts - tonumber(KEYS[2])) lefts = tonumber(KEYS[2]) end if lefts == 0 then ltime = 0 redis.call("DEL", KEYS[1]) elseif dels > 0 then redis.call("ZREMRANGEBYRANK", KEYS[1], 1, dels) end return {dels, ltime}
寫入的消息前4byte 為little-endian 的UnixTime,Redis lua 支持struct,很簡單解析出(當然也支持cjson,但速度要差一些).
清理過期數據,並返回最后一條寫入的時間,應用根據返回時間適當延長過期時間。
這里因為考慮每個人消息一般不會太多,所以全部遍歷,多的話可考慮分部分遍歷,如10條10條來,最新的就不會被不必要的取出來了,怎么說遍歷大Set還是較慢的。
clear_msgs(Uid, MaxLen, ExpireSec) -> ToExpires = utime() - ExpireSec, {ok, [Dels, LTime]} = eredis:q(pooler(Uid), [<<"EVAL">>, clear_script(), <<"3">>, ?KEY_LIST(Uid), MaxLen, ToExpires]), {ok, binary_to_integer(Dels), binary_to_integer(LTime)}.
性能:此段腳本在我機器上速度2.5w/s(列表長度10), 相比get 7w/s。速度很快,也節省網絡流量。
script load
此段腳本有700多字節,每次執行會帶來不少網絡流量;但對性能影響較小,內部對於eval 會先sha1 腳本,從緩存獲取生成好的lua 方法執行。
當然最好使用script load,節省腳本傳輸、腳本的sha1計算,就行存儲過程一樣執行。
luajit:
github討論過 ,redis lua,相比nginx_lua 更像數據庫存儲過程,提供事務性的多個相關性操作,是否使用jit區別不大;
支持的庫也很有限base、table、string、math、debug、cjson、struct、cmsgpack,能夠做的事情不多,也盡量別把太多邏輯用lua寫。
redis.log 方法:
調試大段的lua腳本,這個方法還是挺管用的。
相關參考:
官方說明:http://oldblog.antirez.com/post/scripting-branch-released.html
源碼分析:http://blog.nosqlfan.com/html/4099.html