likes
comments
collection
share

Redis中当存储数据为List集合时,如何控制集合内每个数据元素的生命周期

作者站长头像
站长
· 阅读数 10

本文正在参加「金石计划」

你最近还好吗,非常不好!!!!!

最近看到的一个面试题,题目就是:“Redis中当存储数据为List集合时,如何控制集合内每个数据元素的生命周期。

坦白说,看到的片刻我是有点懵的。

本篇文章只是记录下自己的解题思路(伪代码)和所找到的相关资料信息,并无详细代码实现。

如何控制List集合内每个元素的生命周期

首先我们都知道,在Redis中,对于List集合,只能够设置top-level-key的过期时间:

语法为:

127.0.0.1:6379>  LPUSH runoobkey redis
(integer) 1
127.0.0.1:6379> EXPIRE runoobkey 150
(integer) 1
127.0.0.1:6379> TTL runoobkey
(integer) 140

在这种情况下,我们只能够给List这个top-key设置过期时间,而无法给 List 集合中的每个元素设置过期时间。

1.1、在每个元素值添加过期时间

我当时想到的最简单的方式,就是在应用程序中控制 List 集合中的元素的过期时间,在每个元素值前添加过期时间(假如过期时间是1个小时,就在值的前缀上添加上一个小时后的时间戳)。

#之前List元素集合的存储方式
127.0.0.1:6379>  LPUSH runoobkey redis
#就是在值的前缀上,添加一个过期时间
127.0.0.1:6379>  LPUSH runoobkey 1682013566_redis

这样存储后,应用程序在取出来使用时也多了一步判断的操作。

#1、取出来

#2、截取获得过期时间

#3、和现在时间进行比较
## 3.1、小于则查询数据库(或执行其他操作)
## 3.2、大于当前时间,则直接使用

这是取的逻辑,但如何删除过期数据,就又成了另一个问题。

1.2、如何删除过期数据

因为在当前情况下,redis 本身是无法自动过期List集合下的每个元素的,那么这一步操作就必须由我们自己来完成了。

取到数据之后,如果是过期数据,就直接将其删除;

如果是未过期的,就正常使用即可。

1.3、之后的思考

我最开始想的就是如何删除 list 中过期的元素,没有想怎么去直接删除整个list集合。

如果当前 List 集合中的元素没有过期,那么当前 List 肯定是不会过期的,即当插入元素或更新元素时,List此时就可以重新更新过期时间。

如果长期不用插入元素或者是更新元素的话,那么就可以直接删除List集合啦。

当List集合所存储的数据是查询多,更新操作少的话,那么我认为可以在每一次更新List集合时,同时为List集合更新过期时间。

在网上也查了其他的资料,Redis 对于 List 集合,想要去控制List集合内每个数据元素的生命周期,并没有太好的方式。

即使是 Redis 作者 Quoth Antirez 的解答也是这般:

'Hi, it is not possible, either use a different top-level key for that specific field, or store along with the filed another field with an expire time, fetch both, and let the application understand if it is still valid or not based on current time.

原链接🔗:stack overflow: How to "EXPIRE" the "HSET" child key in redis?

后续关于Hash和Set集合的讨论也总结于此。

如何控制Hash集合中的每个元素的生命周期

给元素添加时间戳

最简单的方式还是上面那种处理方式,只不过Hash的存储结构会更特殊一些。

如果是下图这种简单结构的就是和上面方式一模一样,在field的value上加上时间戳前缀,取出来的时候再进行判断处理。

时间戳_+value

Redis中当存储数据为List集合时,如何控制集合内每个数据元素的生命周期

如果是一个field的value存储的json数据对象的话,那么就需要在JavaBean对象中加上一个过期时间的字段才可以。

存储的结构为:

Redis中当存储数据为List集合时,如何控制集合内每个数据元素的生命周期

代码大致如下吧:

  @Test
  public void test7() {
    HashOperations<String, Object, Object> opsForHash = stringRedisTemplate.opsForHash();
    //存:
    MyUser myUser = new MyUser();
    myUser.setUsername("宁在春");
    myUser.setPassword("123456");
    myUser.setExpireDate(System.currentTimeMillis()+1500L);
    opsForHash.put("hash:key","username", JSON.toJSONString(myUser));

    // 取
    JSONObject username = JSON.parseObject(opsForHash.get("hash:key", "username").toString());
    MyUser myUser1 = username.toJavaObject(MyUser.class);
    //在使用前需要判断是否过期
    Long expireDate = myUser1.getExpireDate();
    if(expireDate>System.currentTimeMillis()){
      System.out.println("输出未过期的数据===>"+myUser1);
    }

    // 展示某个key下所有的 hashKey
    Set<Object> keys = opsForHash.keys("hash:key");
    keys.forEach(System.out::println);
    // 展示某个key下所有 hashKey 对应 Value值
    List<Object> hashKeyValues = opsForHash.values("hash:key");
    hashKeyValues.forEach(System.out::println);
  }

当然方式还是一样的。

删除操作我这里忘记写了,但其实也就是在取之后,判断是未过期的数据就正常使用,过期的直接删除就好了。

Redisson 框架实现

另外一种则是使用Redisson 来进行实现:Distributed collections

Redis中当存储数据为List集合时,如何控制集合内每个数据元素的生命周期

如何控制Set集合中的每个元素的生命周期

Redis有一种数据结构是Sorted Set,有序集合,它的实现是hash table(element->score, 用于实现zscore及判断element是否在集合内)和skiplist(score->element,按score排序)的混合体。

它有下面几种特性:

  1. 元素唯一
  2. 每个元素拥有一个score
  3. 所有元素依据score进行有序排列
  4. 可通过score来进行查询

我们可以借助这些特性来让集合中的元素拥有时间维度。

每当add一个元素时,把当前时间的 unix timestamp 作为score设置到这个元素上,这样sorted set会根据这个timestamp将元素排序存储。

另一种就是将过期时间作为score设置到元素上,然后定期或者每次使用前调用一个过期函数来删除过期的数据。

把当前时间的时间戳作为score设置到集合元素上

场景一:当我们查询最近1分钟内有更新的元素时,可以使用命令 zrangebyscore key min max来获取。例如:

zadd set1 1522598879 "one"

zadd set1 1522598969 "two"

zadd set1 1522598979 "three"

#执行查询:
zrangebyscore set1 1522598920 1522598980

Redis中当存储数据为List集合时,如何控制集合内每个数据元素的生命周期

场景二:当我们查询最新更新的2个元素,可以使用 zrevrange key start stop来获取。例如:

zadd set1 1522598879 "one"

zadd set1 1522598969 "two"

zadd set1 1522598979 "three"

#执行查询:
zrevrange set1 0 1

Redis中当存储数据为List集合时,如何控制集合内每个数据元素的生命周期

场景三:当我们需要删除最近1分钟没有过更新的元素,可以使用 zremrangebyscore key min max 来删除过期元素。例如:

zadd set1 1522598879 "one"

zadd set1 1522598969 "two"

zadd set1 1522598979 "three"

#执行命令: 
zremrangebyscore set1 0 1522598920

执行结果:删除了元素"one"

Redis中当存储数据为List集合时,如何控制集合内每个数据元素的生命周期

一般来讲,我们会启动一个后台任务来不断进行过期元素的删除操作,任务的重复执行间隔可以视业务对过期数据的容忍度。如果容忍度较高,可以设置时间久一点,相反可以设置时间短一些。

把过期时间的时间戳作为score设置到集合元素上

这一点的答案也是来自于:stack overflow:Redis: To set timeout for a key value pair in Set

使用排序集合 (ZSET),将每个成员的分数设置为其到期时间。这种类型的工作流可以使用例如 Lua 脚本来实现。

要添加成员的伪代码:

redis.call('zadd', KEYS[1], os.time()+ARGV[1], ARGV[2])

要真正使集合中的元素“过期”,需要在它们的时间结束后将其删除。

我们可以通过实施扫描列表或访问列表,定期或每次操作前来做到这一点。例如,以下 Lua 脚本可用于使成员过期(伪代码):

redis.call('zremrangebyscore', KEYS[1], '-inf', os.time())

作者还补充了一段python的伪代码实现:

import time
import redis

def add(r, key, ttl, member):
    r.zadd(key, member, int(time.time()+ttl))

def expire(r, key):
    r.zremrangebyscore(key, '-inf', int(time.time()))

...

r = redis.Redis()
add(r, 'a', 1, 60)
add(r, 'a', 2, 120)

# periodically or before every operation do
expire(r, 'a')

Redisson框架实现

另外同样也可以使用Redisson来实现:

Redis中当存储数据为List集合时,如何控制集合内每个数据元素的生命周期

后续及其他已实现的解决方案

其实关于如何实现针对集合元素的过期的功能,其实早就在2011年时,就有小伙伴在github上提出相关issue啦:Feature Request: Add ability to expire members of a set #135

Redis中当存储数据为List集合时,如何控制集合内每个数据元素的生命周期

作者的回复:

Hello, unfortunately it is not in our plans! Sorry.

The one sentence rationale is: too complex, too memory consuming, more CPU needed. In general the Redis model is: at level of key, many features: timeout, migration, sharding (Redis Cluster). At value level no fancy stuff.

您好,很遗憾,这不在我们的计划中!对不起。

一句话理由是:太复杂,太耗内存,需要更多CPU。 总的来说Redis模型是:at level of key,很多特性:timeout,migration,sharding(Redis Cluster)。在价值层面没有花哨的东西。

因此 Redis 官方一直都没有实现此功能。

但对于 Sorted Set 结构的强化, 我有看到相关的issue

Feature Request: ZRANDMEMBER #6323

Redis中当存储数据为List集合时,如何控制集合内每个数据元素的生命周期

在2021年时,官方最后以这句回复关闭了这个issue:

It looks like we didn't implement the exact signature. The general idea is implemented though, so it's probably reasonable to close this. We can re-evaluate if someone wants this in the future.

看起来我们没有实现的确定特征。不过总体思路已经实现。因此关闭它可能是合理的。我们可以重新评估将来是否有人想要这个。

后续我没有追溯到相关信息啦,所以目前而言,关于如何控制集合元素的过期功能,都是个人在实现,并没有一个官方的标准和例子。不过理念目前都还比较相似。

Alibaba重写Hash和Zset模块

其实关于如何去控制Hash和set数据结构中的元素过期时间,alibaba 对于 Redis 这两种数据结构模块进行了重写,实现了控制元素级别的过期时间。

TairHash

TairZset

大家感兴趣可以去自行查阅一番~

关于 Redis/keyDB

我在 Redis 官方中fork出来的 Redis/KeyDB 分支中,有看到不同的实现想法,并且他们也在实现,我在查阅相关issue时,有小伙伴提出来了

Feature Request, Expire members of a set #51

Keyspace Event For Expired Values #85

这里我看到的实现方法为:

Redis中当存储数据为List集合时,如何控制集合内每个数据元素的生命周期

Redis中当存储数据为List集合时,如何控制集合内每个数据元素的生命周期 关联一个Lua脚本去实现~~

然后我在查阅官网时,貌似是已经实现了:

Redis中当存储数据为List集合时,如何控制集合内每个数据元素的生命周期

只需使用 EXPIREMEMBER 命令。它适用于集合、散列和排序集合。

EXPIREMEMBER 键名子键 [时间]

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

TTL 键名子键

更多文档可在此处获得:https ://docs.keydb.dev/docs/commands/#expiremember

4月24日补充更新

柴佬看完文章给我来了一个新的思路,十分简单,但我在考虑这个问题的时候,真的没往这个方面去考虑捏,晚上在思考了一番后,更新了文章

柴佬:老柴不加班,以后会开一家名叫《柴佬私房菜》的柴佬,hhh~~

关于如何控制List集合内的每个元素的生命周期,当时想的不够多,一些问题也没充分考虑(当然现在也米有,哈哈,没实际业务场景,写起来还是很虚),现在再回过头来看,是真觉得水啦,哈哈

利弊皆有,不过多点思考总归是好的~~

大致如下图一样的结构:

思路本身是十分简单的,就是将直接存于List中的值,抽取出来单独进行存取,再将对应的key放到List集合中;

Redis中当存储数据为List集合时,如何控制集合内每个数据元素的生命周期 使用:从截取时间来判断是否过期,改为了判断key是否存在,存在即获取出来,不存在,即删除List中保存的值(补充:另外我认为可以另外开个异步线程定时去进行删除,如果key不存在,直接不作操作即可)。

利弊皆有

为什么说利弊皆有呢?

缺点很容易看出来,明显是更浪费内存的;另外就是在存储的时候多了一步与Redis的网络交互,不过架构比起之前是更复杂些的,因为多套了层娃~

优点也很容易看出来啦,不需要自己直接去控制集合内元素的过期操作

可行性还是蛮高的,不过我现在都是浮于表面在思考哈,因为我也没遇到实际业务场景哈,都只是简单的提供思路~

Redis中List结构的最大数据容量

在聊天中还谈到了List结构的最大数据容量~(坦白说,我给忘记啦,哈哈)

Redis中当存储数据为List集合时,如何控制集合内每个数据元素的生命周期

然后我在Redis的官网中翻阅了一下:

Redis中当存储数据为List集合时,如何控制集合内每个数据元素的生命周期 Redis中List、Set、Hash、Sorted Sets的数据最大容量都是2^32 - 1 (4,294,967,295)个;String 类型最大存储数据长度为 512 MB

参考及相关链接

redis集合数据过期_如何给redis集合中的元素设置过期时间

Redis/keyDB

Feature Request, Expire members of a set #51

Keyspace Event For Expired Values #85

stack overflow: How to "EXPIRE" the "HSET" child key in redis?

TairHash

TairZset

后记

当时看到这个题目时,其实说的就是考验候选人的研究能力和思考问题的能力,并非说什么一定有个什么正确答案的,只是说哪种方案会更优。

写于 2023 年 4 月 21 日凌晨,作者:宁在春

转载自:https://juejin.cn/post/7224344504776851517
评论
请登录