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

  

版权声明:本文为lulu原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/lulu/p/4050176.html