需求是在缓存最近一周内用户所有消息列表,考虑用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
请发表评论