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命令基本没有改进的空间,只有想办法减少操作的元素个数
每次把队列中所有元素取出来作为候选集是完全没必要的
队列中的元素是按照到来的时间单调排列的,只有一个小范围内的元素是可能过期的。 比如如果更新间隔是100ms,那我每次只选取120ms(比100ms稍大的值)的请求数作为候选集来删除 可以大大减少每次要操作的元素个数
异步操作
不管是向队列中加入元素,还是删除过期元素,都没有必要使用同步操作等待redis server返回结果
big key
这个场景中的big key就是这个队列了 那么实际上也完全没必要使用一个大的queue 可以根据时间戳拆成多个小的queue来操作