redis学习笔记

Overview

最近在做一个智能算力的项目,其中需要用到redis维护某个全局的时间窗口

资料

复杂数据结构的表示方式

学习过程中唯一让我迷惑的是一个嵌套的数据结构要如何表示 比如我想表示一个key为string, value为List的Hash 按照c++的语法,也就是一个

1std::unordered_map<std::string,std::vector<int>>

但是我发现,在redis中我无法设置value的数据类型

后来发现,redis这种kv存储其实所有的key是在同一个空间的,不存在嵌套关系

嵌套关系是根据相同的string名称来间接进行的 也就是说; hash的value是一个string, 这个string是list的key名称

hash: hash_key_name-> value_key_name list: value_key_name ->list

原子性

redis单个命令是原子操作,但是有的时候需要执行多个命令完成一个操作 比如典型的 Read-Modify-Write 操作,这种操作为了保证原子性通常有两种办法 其一是使用incr/hincrby 等命令,把多个命令合成一个 其二是使用lua script,用以实现更复杂的逻辑

性能优化

以我遇到的具体问题举例说明吧

有一个队列,队列中的元素是不断变化的,想知道这个队列中所有元素的和。 队列元素的增加: 当一个请求到来时,就往队列中添加元素。 可能同时到来上百个请求 队列元素的删除: 按照时间删除,当请求到来超过一定时间后,自动删除。 说白了就是想维护一个时间窗口,并查询窗口中元素之和。

最初考虑过使用redis expire命令来实现自动过期的策略,但是由于expire的key没办法直接把sum也对应删除掉,因此没有选择这个方案。

于是转而考虑使用一个list来维护请求队列,从右边加入元 素,左边删除元素。 维护窗口的时候按照时间戳进行删除

当请求量增大时,发现耗时逐渐变慢,于是开始分析可能的性能瓶颈

使用evalsha替代eval

Lua script 每次会把脚本发送到server执行 script没有变化,因此每次传送的开销也是没必要的

使用evalsha替代eval

O(n)复杂度的操作

目前的实现中有三个O(n)复杂度的操作,分别是取队列中元素、删除其中过期的元素的key、在队列中清除这些元素

代码如下

 1
 2         local queue_name = redis.call("hget",KEYS[1],"queue")
 3         local sum_value = redis.call("hget",KEYS[1],"sum_value")  
 4         local sum_quota = redis.call("hget",KEYS[1],"sum_quota") 
 5         local queue = redis.call("lrange",queue_name,0,-1) 
 6         local cnt = 0 
 7         local need_deleted = {}  
 8         for i=1,#queue do 
 9           local time_stamp = redis.call("hget",queue[i],"timestamp") 
10           if (ARGV[2]-time_stamp<=tonumber(ARGV[1])) then break end  
11           cnt = i 
12           local quota = redis.call("hget", queue[i],"quota")  
13           local value = redis.call("hget",queue[i],"value") 
14           sum_value = sum_value-value  
15           local quota = redis.call("hget",queue[i],"quota") 
16           need_deleted[i] = queue[i]
17           sum_quota = sum_quota - quota  
18          end  
19          redis.call("del",unpack(need_deleted))
20          redis.call("hmset",KEYS[1],"sum_quota",sum_quota,"sum_value",sum_value) 
21          redis.call("ltrim",queue_name,cnt,-1) 
22          return cnt

其中删除操作考虑过使用unlink替代del,发现反而更慢了 参考 Is the UNLINK command always better than DEL command? 原因需要进一步确认

但是另外的lrange和ltrim命令基本没有改进的空间,只有想办法减少操作的元素个数

Info

每次把队列中所有元素取出来作为候选集是完全没必要的

队列中的元素是按照到来的时间单调排列的,只有一个小范围内的元素是可能过期的。 比如如果更新间隔是100ms,那我每次只选取120ms(比100ms稍大的值)的请求数作为候选集来删除 可以大大减少每次要操作的元素个数

异步操作

不管是向队列中加入元素,还是删除过期元素,都没有必要使用同步操作等待redis server返回结果

big key

这个场景中的big key就是这个队列了 那么实际上也完全没必要使用一个大的queue 可以根据时间戳拆成多个小的queue来操作