如何在Redis中“过期”HSET子键?

188

我需要过期所有在Redis哈希表中超过1个月的键。

13个回答

177

这是不可能的, 为了保持Redis的简洁

Redis 的创始人 Antirez 曾说:

嗨,这是不可能的。你可以使用一个不同的顶级键来存储特定的字段,或者将另一个带有过期时间的字段与该字段一起存储,获取两个字段,并让应用程序根据当前时间确定它是否仍然有效。


27
这就是为什么 Redis 是一款如此棒的软件。他们知道如何保持简单明了。 - tObi
7
在我看来,当我发现Redis中缺少我需要的某些功能时,这种情况发生得太频繁了。 - Oleg Yablokov
15
我不同意。每个人都会以各种有缺陷且效率较低的方式实现相同的功能。 - Trevor Hickey
@supr,你能否解释一下为什么这个有效吗? - Liran
@tObi,我想要实现一个简单的队列,在其中我可以增加(INCR)一条消息ID,然后保存带有某些TTL的消息。由于在Lua脚本中动态访问键时会破坏群集,所以我无法编写Lua脚本。我不能过期哈希元素,因为太过简单了。那么下一步该怎么做?自定义分片+Lua脚本?使用随机的第三方插件,并希望它们已经准备好生产环境了吗? - Anish Ramaswamy
显示剩余2条评论

39

Redis不支持在除了顶级键之外的哈希上设置TTL,否则将使整个哈希过期。如果您正在使用分片集群,则可以使用另一种方法。这种方法可能并非在所有场景中都有用,并且性能特征可能与预期有所不同。但仍值得提及:

当存在哈希时,其结构基本上如下:

hash_top_key
  - child_key_1 -> some_value
  - child_key_2 -> some_value
  ...
  - child_key_n -> some_value

因为我们希望将TTL添加到子键中,所以可以将它们移动到顶级键。主要问题是现在键应该是hash_top_key和子键的组合:

{hash_top_key}child_key_1 -> some_value
{hash_top_key}child_key_2 -> some_value
...
{hash_top_key}child_key_n -> some_value

我们有意使用 {} 符号。这样可以让所有这些键落在同一个 哈希槽 中。您可以在这里阅读更多信息:https://redis.io/topics/cluster-tutorial

现在,如果我们想执行哈希操作,我们可以执行以下操作:

HDEL hash_top_key child_key_1 => DEL {hash_top_key}child_key_1

HGET hash_top_key child_key_1 => GET {hash_top_key}child_key_1

HSET hash_top_key child_key_1 some_value => SET {hash_top_key}child_key_1 some_value [some_TTL]

HGETALL hash_top_key => 
  keyslot = CLUSTER KEYSLOT {hash_top_key}
  keys = CLUSTER GETKEYSINSLOT keyslot n
  MGET keys

这里最有趣的是HGETALL。首先,我们获取所有子密钥的hash slot。然后,我们获取该特定hash slot的密钥,最后检索值。在此过程中,我们需要小心,因为可能会有多于n个密钥与相同的hash slot 相关,而且还可能有我们不感兴趣但具有相同hash slot 的密钥。我们实际上可以编写一个Lua脚本来通过执行EVALEVALSHA命令在服务器上执行这些步骤。同样,您需要考虑此方法对您特定情况的性能影响。

更多参考文献:


3
这种方法使用的内存比简单的带过期时间的键更多。 - VasileM
我可以将顶级键及其所有子键设置为过期,但是找不到如何使整个哈希表及其所有键过期的方法。 - mjs
@mjs,你可以直接设置它的过期时间。EXPIRE myHashKey 10 可以让你的哈希在10秒后过期。https://redis.io/commands/expire - jonlink
@jonlink 看起来与设置键相关,而不是属于哈希的键。编辑。我看到那正是我想要的 :p ... 我认为问题可能是当您向哈希添加内容时,ttl没有更新? - mjs
这会将所有的键保存到集群中的同一个分片吗? - jsnelgro
这会将所有的密钥保存在集群中的同一个分片上吗? - jsnelgro

22

这在KeyDB中是可以实现的,它是Redis的一个分支。由于它是一个分支,它完全兼容Redis,并可作为一种替代品使用。

只需使用EXPIREMEMBER命令即可。它适用于集合、哈希和排序集。

EXPIREMEMBER keyname subkey [time]

您还可以使用TTL和PTTL查看过期时间

TTL keyname subkey

更多文档请参见:https://docs.keydb.dev/docs/commands/#expiremember


15

您可以在redis中使用排序集合(Sorted Set)来创建一个以时间戳为分数的TTL容器。 例如,每当您将事件字符串插入集合时,可以将其分数设置为事件时间。 因此,您可以通过调用zrangebyscore "your set name" min-time max-time获取任何时间窗口的数据。

此外,我们可以使用zremrangebyscore "your set name" min-time max-time进行过期处理,以删除旧事件。

唯一的缺点是您需要从外部进程执行清理操作来维护集合的大小。


5

我们在此讨论的问题是相同的。

我们有一个 Redis 哈希表,其中包含一个键和哈希条目(名称/值对),我们需要在每个哈希条目中保持单独的过期时间。

我们通过添加 n 字节的前缀数据来实现这一点,包含编码过期信息,当我们写入哈希条目值时,我们还设置键在所写入的值所包含的时间到期。

然后,在读取时,我们解码前缀并检查是否过期。这是额外的开销,但是读取仍然是 O(n),并且整个键将在最后一个哈希条目过期时过期。


5
我想到的解决方案是:
假设我想让数据在3分钟后过期: 我将数据保存在3个字段0,1,2中, 然后对当前时间的分钟数进行模运算% 3。
例如,如果模等于0, 那么我只使用1、2和0,删除1; 然后它变成1,所以我使用2和0并删除1。
我没有使用它,也没有检查它,但我只是想告诉你这是可能的。

4
如果您的使用场景是在Redis中缓存值,并且可以容忍陈旧的值,但希望偶尔刷新它们以避免太过陈旧,一个糟糕的解决方法就是在字段值中包含一个时间戳,并在访问该值时处理过期。
这样可以让您继续正常使用Redis哈希,而无需担心其他方法可能出现的任何复杂问题。唯一的代价是客户端端需要一些额外的逻辑和解析。虽然这不是完美的解决方案,但这是我通常采用的方法,因为我没有因其他原因而需要TTL,并且通常需要对缓存的值进行额外的解析。
因此,基本上就像这样:
在Redis中:
hash_name
- field_1: "2021-01-15;123"
- field_2: "2021-01-20;125"
- field_2: "2021-02-01;127"

你的(伪)代码:

您的(伪)代码:

val = redis.hget(hash_name, field_1)
timestamp = val.substring(0, val.index_of(";"))

if now() > timestamp:
  new_val = get_updated_value()
  new_timestamp = now() + EXPIRY_LENGTH
  redis.hset(hash_name, field_1, new_timestamp + ";" + new_val)
  val = new_val
else:
  val = val.substring(val.index_of(";"))

// proceed to use val

我认为最大的注意事项是,您永远不会删除字段,因此哈希表可能会变得非常大。不确定是否有优雅的解决方案 - 如果感觉太大,我通常会偶尔删除哈希表。也许您可以在某个地方跟踪您存储的所有内容,并定期删除它们(尽管在那时,您可能会使用该机制手动到期字段...)。


4

有一个Redisson的Java框架,它实现了具有entry TTL支持的哈希Map对象。它在底层使用hmapzset Redis对象。使用示例:

RMapCache<Integer, String> map = redisson.getMapCache('map');
map.put(1, 30, TimeUnit.DAYS); // this entry expires in 30 days

这种方法非常有用。

但是你如何创建一个地图呢?因为我找不到任何教程或创建/设置方法。 - FaNaT
@ZoltánNémeth 在 Redis 中,当第一个值被插入时,映射会自动创建。 - Nikita Koksharov

3

关于NodeJS实现,我已经在保存在HASH中的对象中添加了自定义的expiryTime字段。然后,在特定时间段之后,我使用以下代码清除过期的HASH条目:

client.hgetall(HASH_NAME, function(err, reply) {
    if (reply) {
        Object.keys(reply).forEach(key => {
            if (reply[key] && JSON.parse(reply[key]).expiryTime < (new Date).getTime()) {
                client.hdel(HASH_NAME, key);
            }
        })
    }
});

你可以使用 Array.filter 来创建一个需要从哈希表中删除的 keys 数组,然后将其作为参数传递给 client.hdel(HASH_NAME, ...keys) 以进行单次调用,从而使其更加高效。 - doublesharp
const keys = Object.keys(reply).filter(key => reply[key] && JSON.parse(reply[key]).expiryTime < Date.now()); client.hdel(HASH_NAME, ...keys); - doublesharp

2

您可以通过在存储键时添加前缀或命名空间来以不同的方式在Redis中存储键/值,例如“hset_”

  • 获取键/值GET hset_key等同于HGET hset key

  • 添加键/值SET hset_key value等同于HSET hset key

  • 获取所有键KEYS hset_*等同于HGETALL hset

  • 获取所有值应该在两个操作中完成,首先获取所有键KEYS hset_*然后获取每个键的值

  • 使用TTL或过期时间添加键/值是问题的主题:

 SET hset_key value
 EXPIRE hset_key

注意: KEYS 会在整个数据库中查找匹配的键,这可能会影响性能,特别是当你的数据库很大时。而SCAN 0 MATCH hset_* 可能会更好,只要它不会阻塞服务器,但对于大型数据库性能仍然是一个问题。

如果您要过期一些键,并且它们是一小部分键,您可以创建一个新的数据库来单独存储这些键。

感谢 @DanFarrell 指出与KEYS相关的性能问题。


2
请注意,这是性能特征的重大变化。 - erik258
2
请将KEYS视为一种仅在生产环境中极度小心使用的命令。HGETALL对于集合中的事物数量是O(n),而KEYS则是针对数据库中事物数量的。 - erik258
1
另外,如果这些键是小集合,请将它们分开存储到不同的数据库中。谢谢@DanFarrell - Muhammad Soliman
1
扫描也是O(n),这将HGETALL从O(哈希表大小)变为O(整个Redis数据库的大小)。 - Binyamin
1
如果你有几十万个键,那么你会发现使用SCAN命令,虽然不会长时间阻塞其他调用,但会导致REDIS CPU占用率达到100%。 - dan carter
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接