重新思考 Redis EXPIRE 命令
本文讨论了我们对 EXPIRE 命令及其工作方式所做的几项更改。我们通过 EXPIREMEMBER 命令增加了使集合中的单个成员过期的功能,并且还使主动删除能够以近乎实时的方式运行,这对于那些在生产中严重依赖使用过期功能的用户来说具有巨大优势。在处理 EXPIRE 命令的整个过程中,我们实际上通过这些改进节省了 5-10% 的内存。
这项工作是在 KeyDB 中完成的,它是一个开源、高性能的 Redis 分支,我们能够实现一些可能永远不会成为 Redis 一部分的功能。诸如多线程(性能提升5倍)和主主复制等功能减少了分片(最多减少10倍),使一系列用户受益,特别是那些重视生产设置简单性的用户。
例如,添加使集合中单个成员过期的功能一直是一个被高度要求的功能,但 Redis 的创建者从根本上不想添加它(参见:Github issue#135)。
这个选项已经被请求多年,甚至最近也有,需要复杂的变通方法来实现这个需求。我们将在本博客后面讨论当前的方法/变通方法以及新的 EXPIREMEMBER 功能。
KeyDB 是一个支持让生活更轻松功能的数据库,我们相信通过正确的方法,您不必在性能上妥协。我们也正在我们的开源项目上积极寻求反馈和请求。
#
改变 EXPIRE 命令的工作方式本节讨论我们如何能够更新 EXPIRE 功能,以 O(N) 时间复杂度和确定性算法实际移除过期的键。您可能不认为这很重要,但如果您严重依赖 EXPIRE 功能,这可能意味着可观的内存节省。
EXPIRE 命令广泛应用于跟踪用户会话的应用程序中,以及临时数据很重要但同时需要控制内存消耗的应用程序中。
EXPIRE 命令使用被动和主动删除。被动删除意味着如果我们调用一个已经过期的键,它会在那个时候被删除。然而,有些键可能永远不会再被调用,这就是主动删除发挥作用的地方。Redis 使用随机方法,每秒十次搜索 20 个带有过期时间的随机键。这是一种概率性算法,而 KeyDB 使用的是确定性算法。
对于使用少量过期键的用户来说,过去的这种方法是没问题的。但如果您严重依赖 EXPIRE 呢?如果您设置的过期时间间隔差异巨大呢?好吧,让我们举几个例子,看看键在过期后可能会在内存中保留多长时间。
通过主动删除来移除过期键所需的时间在 Redis 中是非线性的,并且由于随机算法,当活跃过期键与已过期键的比例越高时,这个时间会变得更长。而使用 KeyDB 的确定性算法,我们能够以近乎线性的关系来处理过期。在上面图表的场景中,KeyDB 的键在不到1秒内就被移除了。
为了更好地理解 KeyDB 是如何优化该功能的,让我们看下面两个场景,这两个场景中有足够多的键,可以注意到 KeyDB 主动删除中的延迟。在第一个图表中,我们运行一个测试,其中有 1000 万个具有相同过期时间(相同的 Unix 时间戳)的键。当它们过期时,Redis 和 KeyDB 都会在合理的时间内将它们移除(CPU 使用率也相同)。两种方法之间的巨大差异在于,当存在大量过期键,但其中只有一小部分是已过期的时候。
在第二个图表中,您可以看到,在有 2000 万个过期键的情况下,如果 1000 万个键同时过期(TTL<=0),KeyDB 会以线性的方式在很短的时间内将它们全部移除。而 Redis 只移除了很小一部分(并且这已经是曲线最陡峭的部分——参考上面的模型)。
现在,如果您严重依赖 EXPIRE 并不断添加带有过期时间的新键,您理论上计算的内存使用量可能与实际的内存消耗有很大差异。能够近乎实时地移除过期键,确保您的数据库内存不会出现巨大的降级延迟,并且您不会积累可能滞留数天的已过期键。
为什么 Redis 的 EXPIRE 不是这样做的?您可能会想‘这一定很耗费资源,或者消耗更多内存’。在对这个命令进行基准测试时,Redis 和 KeyDB 使用的 CPU 强度是相同的,但请记住 KeyDB 是多线程的,如果一个节点启用了超过1个线程,它可以获得高达5倍的性能。关于内存,我们很高兴地说,对 EXPIRE 功能的改进实际上节省了内存!如果您测量一组带有过期时间的相同键的内存消耗,KeyDB 的内存使用量比在 Redis 上使用的同一组键大约少 10%!所以,您不仅能立即移除过期键以优化内存使用,还能在仍在排队的过期键上额外节省 10% 的内存!对于重度 EXPIRE 用户来说,这个功能非常有益,更不用说 INFO KEYSPACE 现在也会包含准确的信息。如果适用,这可以提高您的指标的准确性。
#
EXPIREMEMBER:使集合中的单个成员过期:使用外部工具来最小化内存消耗、复杂性或规模总是最好的吗?更多地考虑代码库而不是使用它的应用程序会更好吗?不总是这样。作为用户,您应该有选择权来权衡您所看重的利弊。KeyDB 有着不同的理念,它提倡简单性并力求减少活动部件……而且我们仍然优先考虑性能!
让我们先看看 KeyDB 的新 EXPIREMEMBER 命令,然后比较人们为了实现这一目标而不得不采用的替代方案。
通过 EXPIREMEMBER,您可以简单地选择让集合中的特定成员过期。TTL、PTTL 已为此更新,并且为了保持一致性,还添加了 EXPIREMEMBERAT。
在上面的例子中,我们设置成员 member3 在 10 秒后过期。10 秒后它就消失了,而其他成员仍然存在。
过去的变通方法为了实现这个功能的需求包括(简化描述):- 将集合元素与时间戳一起存储——即存储“foo:unix-timestamp”。
- 使用一个有序集合(为您拥有的所有集合使用一个全局的有序集合),其中分数为 Unix 时间。
对于这两种情况,您都需要自己想办法检测 Unix 时间戳何时被超过并移除键。人们使用 KEYS 命令来搜索它们然后移除,其他人则使用 ZRANGEBYSCORE 来最小化搜索中的 CPU 周期。然后,您必须找到自己的方法来删除它们。决定您将多频繁地运行这些查询,并设置一个 cron 作业来执行此操作,或使用其他一些客户端方法。仅使用 Unix 时间戳,您必须确保服务器时钟与客户端同步以避免不准确,并且您不能依赖服务器通过指定 TTL 为您完成此操作。
设置额外的活动部件来控制这样一个功能会增加系统的复杂性、设置和维护,如果这些部件出现故障,可能会导致数据库大小激增。键的过期是数据库已经内置的功能,应该延续到集合的成员上,而不是通过变通方法。
像 EXPIREMEMBER 这样的内置命令所带来的简单性和选择性,对我们来说似乎是一个不假思索就应该添加的选项。关于“它太耗内存和复杂”,嗯,上面的选项也不一定被认为是“一个简单的解决方案”。关于内存消耗,EXPIRE 和 EXPIREMEMBER 比 Redis 的 EXPIRE 使用更少的内存,但是如果您已经在使用上述更复杂的方法之一,它可能节省也可能不节省内存。但这种权衡值得吗?
- 由于 KeyDB 在带有 EXPIRE 的键上节省了内存,如果您比较 Redis 中设置了 EXPIRE 的 'x' 个键,与使用 EXPIREMEMBER 命令为集合成员设置的等量键,实际上使用 EXPIREMEMBER 可以节省 5% 的内存。
- 与变通方法相比,一个为每个成员附加了 Unix 时间戳的集合,比一个使用 EXPIREMEMBER 为每个成员设置了过期时间的集合少占用 33% 的内存。因此,在这种情况下以及有序集合的情况下,变通方法可以说可以节省一些内存,但代价是需要对数据库进行外部的变通处理。
能够精简您的数据集并确保立即移除过期的标签,对我们来说,其重要性超过任何变通方法。考虑到 EXPIRE 功能本身带来的内存节省,我们认为这些改变是对项目的一个很好的改进。
#
来了解我们如果您喜欢这些功能,想要尝试一下,或者想了解更多,您可以在这里查看 KeyDB 的开源项目。
您可以在这里查看我们的 Docker 镜像,如果您想查看刚刚发布的其他功能,例如对 OBJECT LASTMODIFIED 的更新,以及向 BITOP 添加 LSHIFT 和 RSHIFT,请单击这里。另一个受欢迎的功能是 KeyDB 的主主复制功能,您可以在这里查看其概述。要查看 KeyDB 的其余命令,或有关如何使用这些命令的更多详细信息,您可以在这里找到它们。