来源:https://docs.keydb.dev/blog/2021/06/08/blog-post/
在Redis社区有一个很早就提出来的功能:让SET和HASH数据类型的子成员有自己的过期时间。Redis没有接受该需求,可能是考虑到在没有内置命令的情况下实现该功能比较困难,但KeyDB(Redis的一个分支)专注于提供易于使用的高性能Redis,所以实现了该功能。
KeyDB添加子key过期的最初尝试是对每个expire都添加一个用于子key过期的vector,但这会导致某些性能问题。我们接下来将了解这些问题的原因,以及我们如何使用更复杂的数据结构,如哈希表,来解决这些问题。
EXPIREMEMBER命令
KeyDB通过新命令EXPIREMEMBER实现子key过期。该命令的参数是key, 子key及其过期时间,在过期时间到达后将子key从key中删除,其他的子key保持不变。以下是Hash数据类型的例子:
Keydb-cli> HSET myhash f1 v1 f2 v2 f3 v3
(integer)3
Keydb-cli> EXPIREMEMBER myhash f3 10
(integer)1
Keydb-cli> TTL myhash f3
(integer)10
10秒后
Keydb-cli> HGET myhash f1
“v1”
Keydb-cli> HGET myhash f2
“v2”
Keydb-cli> HGET myhash f3
(nil)
在上面的例子中,我们将成员f3设置为10秒后到期,10秒后它就被删除了,但其他成员都还在。
KeyDB如何实现EXPEMEMBER
KeyDB以两种不同的方式过期key和子key: 被动过期和主动过期。每当访问key或子key时,被动过期就会触发:检查该key或子key是否过期并将其删除。主动过期是由KeyDB在运行期间定期执行,KeyDB会遍历即将过期的key和子key,并删除已经过期的key和子key。
在实现EXPEMEMBER命令时,一个重要的考虑因素是如何存储key和子key的过期时间。存储方式可以让主动过期算法快速遍历最有可能已经到期的key和子key,也可以让被动过期快速找到相关的key和子key,同时对KeyDB内存使用的影响降至最低。KeyDB使用按过期时间排序的vector,但这会导致被动过期的查找期间线性增加,从而对其他操作产生巨大影响。为了解决这个问题,我们考虑使用hash表,它提供了恒定时间的查找。以下是使用sorted vector和hash表的优缺点。
Sorted vector的优点:
- 主动过期算法可以轻松遍历即将过期的key和子key,使KeyDB能够确保快速删除不经常访问的过期key和子key。
- Vector是一种内存高效的数据结构,能最小化存储过期时间所需的内存量。
Sorted vector的缺点:
- 添加新的过期时间和修改现有的过期时间需要O(log(n))的搜索,以确保将其放到vector中正确的位置,这会导致过期命令随着即将过期key和子key数量的增加而减慢。
- 被动过期需要查找特定key的过期时间,在按过期时间排序的vector中,这需要O(n)的搜索,这会导致在访问有大量即将过期子key的key时大幅减慢。
Hash表的优点:
- 添加新的过期时间和修改现有的过期时间可以在O(1)的时间内快速完成。
- 被动过期需要查找特定key的过期时间,也可以在O(1)的时间内快速完成。
Hash表的缺点:
- Hash表增加了在主动到期算法中遍历即将过期key和子key的复杂度,从而增加了删除不经常访问的过期key和子key的时间。
- Hash表的内存效率不是很高,通常在负载因子约为2/3的情况下达到最佳性能。在存储等量过期信息的条件下,hash表的内存使用量比sorted vetor多50%。
下面的图表比较了有1000000条记录的hash数据结构(其中n个子key即将过期)分别用sorted vector和hash表实现子key过期时的性能(operations/second)。基于图表中的对比,我们选择了使用hash表来存储过期信息。
为EXPIREMEMBER命令定制的hash表
如前文所分析,使用hash表的主要缺点是增加了在主动到期算法中遍历即将过期key和子key的复杂度。所以我们对用于EXPIREMEMBER命令的hash表做了如下定制:
- 按到期时间对hash表中的单个bucket进行排序,每个bucket都有固定的最大记录数,因此保持它们排序的成本为O(1)。
- 创建一个智能iterator,该iterator按顺序访问上述bucket。这样可以尽量减少在遍历hash表时碰到没有过期的key和子key。
综上所述,使用hash表可以明显地提高被动过期的性能,并通过定制的hash表尽量减少对主动过期算法的性能影响,从而快速查找过期的key和子key。我们最终获得了最佳实践,使KeyDB在不影响整体性能的情况下实现了EXPIREMEMBER命令,支持了子key过期。