栏目说明:“Redis为什么快”记录我对Redis原理的学习过程,以及一些心得体会。也许没有那么”深入“,但一定通俗易懂。个人理解仅供参考。

作者:Kingston 8GB


上期指路:【Redis为什么快】01 内存存储(上)


文章目录


  • 3 Redis数据的过期时间
  • 3.1 常用命令
  • 3.2 过期时间的内存存储
  • 3.3 过期键的删除策略
  • 3.4 过期时间的作用



上期我们提到,Redis数据库结构体定义如下:

typedef struct redisDb {
    int id; //id是数据库序号,为0-15(默认Redis有16个数据库)
    long avg_ttl; //存储的数据库对象的平均ttl(time to live),用于统计
    dict *dict; //存储数据库所有的key-value
    dict *expires; //存储key的过期时间
    dict *blocking_keys;//blpop 存储阻塞key和客户端对象
    dict *ready_keys;//阻塞后push 响应阻塞客户端 存储阻塞后push的key和客户端对象
    dict *watched_keys;//存储watch监控的的key和客户端对象
} redisDb;

其中,dict字典主要存储所有的键值对,而expires字典存放的是所有键值对的过期时间。

今天我们继续聊聊内存存储方式为Redis性能带来的正面影响,从“过期时间”说起。

3 Redis数据的过期时间

Redis为什么要为数据设置过期时间?这是一个迫不得已的做法,更是一种为提升性能而精心设计的做法,后面再说。

3.1 常用命令

Redis对于过期时间的设置,命令如下:

// 存入("key:name", "misterpassion")键值对,并设置过期时间为10秒
127.0.0.1:6379> SET key:name misterpassion EX 10

在SET命令中,EX参数用于指定过期时间,换言之,10秒后当我们GET这个键值对:

// 10秒后访问key:name 
127.0.0.1:6379> GET key:name
(nil)

将查找失败,因为对象过期被回收了

除了EX参数,还可以用PX参数以毫秒为单位设置过期时间:

// 存入("key:name", "misterpassion")键值对,并设置过期时间为10,000毫秒
127.0.0.1:6379> SET key:name misterpassion PX 10000

上述命令和第一条命令等效。

我们可以用TTL(Time To Live)命令查询某个键的剩余寿命:

// 查看key:name的过期时间
127.0.0.1:6379> TTL key:name
(integer) 2

返回值提示我们这个键值对还有2秒过期。

如果没有在SET命令时就设置过期时间,而是想在后期补充设置,可以用EXPIRE和PEXPIRE命令,立即生效:

// 用EXPIRE设置的过期时间单位为秒
127.0.0.1:6379> EXPIRE key:name 15
OK
// 再用PEXPIRE设置一次,单位为毫秒
127.0.0.1:6379> PEXPIRE key:name 15000
OK

这些命令基本就够用了。在新版本的Redis里还引入了一些新的命令,比如能查询过期时间戳。这种命令用处就没有那么大了,因为正常的人类看不懂时间戳。

3.2 过期时间的内存存储

在Redis数据库中,键值对以哈希表的形式存储在内存中,那么某个key的过期时间存在哪里呢?RedisDb维护了另一个哈希表expires,用于存储key的过期时间,如图所示。

redis和内存区别_缓存

expires字典中,key就是key,value存的是这个key的过期时间戳。当我们调用TTL命令,会将当前时间戳和过期时间戳进行计算,返回剩余过期时间。

TTL和GET类命令的时间复杂度相同,都是O(1),因为都是在做哈希表的查询。

注意,由于内存非常宝贵,Redis也不舍得为了存过期时间,而把所有的key再冗余存储一份。因此dict和expire字典里存的key,都是指向真实key内存地址的指针,上面图里画的并不准确。

可见为了优化性能,Redis的设计师真是用心良苦!

redis和内存区别_redis和内存区别_02

头发还不少。

3.3 过期键的删除策略

还有一个很重要的问题,Redis是如何对待过期的数据呢?是立即逐出内存,还是等待一个什么时机呢?

要想实现过期键的立即删除,似乎需要设置一个定时器,或者一个异步任务,或者维护一个消息队列,保证一过期就触发某个事件——这种做法叫“定时删除”。就算不考虑维护成本,这种做法还有两个致命的弊端:

  1. 如果Redis挂掉了,由于定时器没有持久化机制,重启之后定时器可能会消失,导致一些键永不过期,造成内存的泄露,即使重新设置定时器,也需要一些开销。
  2. 如果在某一短暂的时间间隔内很多键集中过期,删除这些键可能让CPU的压力陡增。

Redis没有采用定时删除的策略。

还有一种做法叫“定期删除”,说得直白一点,就是轮询检查,过一定时间我就扫描这些键,发现有过期的我就逐出内存。

这种方法比较柔和,也可以通过随机采样一些键进行过期时间检查,替代全库的扫描,所以效率还是比较高的。

Redis采纳了定期删除这个方案。具体来说,Redis存在一个“周期任务”机制,每次周期任务,Redis都会固定做一些事,这其中就包含了扫描并删除过期键。

这个周期任务的执行频率,默认是10Hz,也就是1秒执行10次周期任务,可以通过INFO命令查看:

127.0.0.1:6379> INFO
redis_version:7.0.0
...
hz:10
...

那每次扫描都是全库扫描吗?不是。Redis只会随机抽取20个key,删除其中过期的。但如果有超过25%(5个)的key过期了,那就会继续抽20个检查,以此循环。

除了“定期删除”,Redis额外引入了“惰性删除”策略。“惰性”的思想很常见,多为节约资源而设计,例如JVM的懒加载机制。

这里Redis的核心思想是,既然我没访问到这个键,那它过没过期、在不在内存,对于使用者而言又有何关系呢?只有当使用者真的访问到了过期数据,这时候再删除,其实都来得及。

这个实现也非常简单,在每次访问某个键的时候,会去查过期字典(类似于TTL命令),过期了,删掉它,返回空值。

但是还剩一个小问题,有些键从来没被访问过,虽然过期了,但会赖在内存不走。这时候定期删除就派上用场了,作为兜底。多执行几次周期任务,这种老赖差不多就出去了。

所以Redis对于过期键的处理,遵循“定期删除+惰性删除”的策略,再一次感叹设计者的巧思!

redis和内存区别_redis和内存区别_03


3.4 过期时间的作用

最后问题来了,为什么说过期时间的引入,有助于提升Redis的性能?

总结了一下,大概可以从这几方面考虑:

  1. 防止OOM:对于长时间不访问的数据,如果我们长时间不清理,就会越堆越多,造成内存的浪费,甚至堆满内存空间,产生OOM(Out Of Memory)。

在32位操作系统中,内存空间只有4GB,并没有太多空间留给Redis作存储,这种情况尤其需要考虑。

当然现在的机子都是64位的了,地址空间可以视为无限大,但我们依然可以通过设置Redis配置文件redis.conf中“maxmemory”参数的值(默认单位为Byte),来人为限制Redis最大占用的内存。

// 限制Redis最大占用的内存为1MBmaxmemory 1024

  1. 便于自动清理:Redis通过设置过期时间,可以实现对不重要的数据的自动清理(定期删除+惰性删除),避免应用程序手动删除过期数据,减轻应用程序的负担。
  2. 保留最新数据:Redis想确保自身只保留最新和有效的数据,而不是过期的数据。这里需要说明,过期的数据不等于非热点数据,而是可能需要优先被清理的数据。

所以,这差不多可以解释Redis的过期时间既是迫不得已、又是精心设计的原因,它的存在对Redis的性能提升而言举足轻重!

下期我们继续讲Redis过期时间的那些巧思和妙用!

下期:【Redis为什么快】03 内存存储(下)