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

本文涉及的产品
云数据库 Tair(兼容Redis),内存型 2GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
简介: Redis中当存储数据为List集合时,如何控制集合内每个数据元素的生命周期

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

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

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

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

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

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

语法为:

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

在这种情况下,我们只能够给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

image.png

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

存储的结构为:

image.png

代码大致如下吧:

  @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

image.png

如何控制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

image.png

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

zadd set1 1522598879 "one"
zadd set1 1522598969 "two"
zadd set1 1522598979 "three"
#执行查询:
zrevrange set1 0 1

image.png

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

zadd set1 1522598879 "one"
zadd set1 1522598969 "two"
zadd set1 1522598979 "three"
#执行命令: 
zremrangebyscore set1 0 1522598920

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

image.png

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

把过期时间的时间戳作为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来实现:

image.png

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

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

image.png

作者的回复:

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

image.png

在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

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

image.png

关联一个Lua脚本去实现~~

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

image.png

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

EXPIREMEMBER 键名子键 [时间]

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

TTL 键名子键

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

4月24日补充更新

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

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

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

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

大致如下图一样的结构:

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

image.png

使用:从截取时间来判断是否过期,改为了判断key是否存在,存在即获取出来,不存在,即删除List中保存的值(补充:另外我认为可以另外开个异步线程定时去进行删除,如果key不存在,直接不作操作即可)。

利弊皆有

为什么说利弊皆有呢?

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

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

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

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

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

image.png

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

image.png

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

后记

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


相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
目录
相关文章
|
2月前
|
缓存 监控 NoSQL
Redis经典问题:数据不一致
在使用Redis时,缓存与数据库数据不一致会导致应用异常。主要原因包括缓存更新失败、Rehash异常等。解决方案有:重试机制、缩短缓存时间、优化写入策略、建立监控报警、定期验证一致性、采用缓存分层及数据回滚恢复机制。这些措施可确保数据最终一致性,提升应用稳定性和性能。
|
2月前
|
缓存 NoSQL Redis
Redis经典问题:数据并发竞争
数据并发竞争是大流量系统(如火车票系统、微博平台)中常见的问题,可能导致用户体验下降甚至系统崩溃。本文介绍了两种解决方案:1) 加写回操作加互斥锁,查询失败快速返回默认值;2) 保持多个缓存备份,减少并发竞争概率。通过实践案例展示,成功提高了系统的稳定性和性能。
|
2月前
|
存储 消息中间件 监控
Redis Stream:实时数据流的处理与存储
通过上述分析和具体操作示例,您可以更好地理解和应用 Redis Stream,满足各种实时数据处理需求。
104 14
|
3月前
|
缓存 NoSQL PHP
Redis作为PHP缓存解决方案的优势、实现方式及注意事项。Redis凭借其高性能、丰富的数据结构、数据持久化和分布式支持等特点,在提升应用响应速度和处理能力方面表现突出
本文深入探讨了Redis作为PHP缓存解决方案的优势、实现方式及注意事项。Redis凭借其高性能、丰富的数据结构、数据持久化和分布式支持等特点,在提升应用响应速度和处理能力方面表现突出。文章还介绍了Redis在页面缓存、数据缓存和会话缓存等应用场景中的使用,并强调了缓存数据一致性、过期时间设置、容量控制和安全问题的重要性。
69 5
|
3月前
|
缓存 NoSQL 关系型数据库
Redis和Mysql如何保证数据⼀致?
在项目中,为了解决Redis与Mysql的数据一致性问题,我们采用了多种策略:对于低一致性要求的数据,不做特别处理;时效性数据通过设置缓存过期时间来减少不一致风险;高一致性但时效性要求不高的数据,利用MQ异步同步确保最终一致性;而对一致性和时效性都有高要求的数据,则采用分布式事务(如Seata TCC模式)来保障。
86 14
|
3月前
|
存储 NoSQL 算法
Redis分片集群中数据是怎么存储和读取的 ?
Redis集群采用哈希槽分区算法,共有16384个哈希槽,每个槽分配到不同的Redis节点上。数据操作时,通过CRC16算法对key计算并取模,确定其所属的槽和对应的节点,从而实现高效的数据存取。
85 13
|
3月前
|
NoSQL Redis
Redis的数据淘汰策略有哪些 ?
Redis 提供了 8 种数据淘汰策略,分为淘汰易失数据和淘汰全库数据两大类。易失数据淘汰策略包括:volatile-lru、volatile-lfu、volatile-ttl 和 volatile-random;全库数据淘汰策略包括:allkeys-lru、allkeys-lfu 和 allkeys-random。此外,还有 no-eviction 策略,禁止驱逐数据,当内存不足时新写入操作会报错。
329 16
|
3月前
|
存储 NoSQL Redis
Redis的数据过期策略有哪些 ?
Redis 采用两种过期键删除策略:惰性删除和定期删除。惰性删除在读取键时检查是否过期并删除,对 CPU 友好但可能积压大量过期键。定期删除则定时抽样检查并删除过期键,对内存更友好。默认每秒扫描 10 次,每次检查 20 个键,若超过 25% 过期则继续检查,单次最大执行时间 25ms。两者结合使用以平衡性能和资源占用。
78 11
|
3月前
|
监控 NoSQL 测试技术
【赵渝强老师】Redis的AOF数据持久化
Redis 是内存数据库,提供数据持久化功能,支持 RDB 和 AOF 两种方式。AOF 以日志形式记录每个写操作,支持定期重写以压缩文件。默认情况下,AOF 功能关闭,需在 `redis.conf` 中启用。通过 `info` 命令可监控 AOF 状态。AOF 重写功能可有效控制文件大小,避免性能下降。
110 6
|
3月前
|
存储 监控 NoSQL
【赵渝强老师】Redis的RDB数据持久化
Redis 是内存数据库,提供数据持久化功能以防止服务器进程退出导致数据丢失。Redis 支持 RDB 和 AOF 两种持久化方式,其中 RDB 是默认的持久化方式。RDB 通过在指定时间间隔内将内存中的数据快照写入磁盘,确保数据的安全性和恢复能力。RDB 持久化机制包括创建子进程、将数据写入临时文件并替换旧文件等步骤。优点包括适合大规模数据恢复和低数据完整性要求的场景,但也有数据完整性和一致性较低及备份时占用内存的缺点。
157 6