赞
踩
既然是缓存,就会涉及过期时间以及过期后清理回收内存的过程;本篇主要讲一下redis的key过期时间相关的命令、注意事项、回收策略;
注意:实际上,redis的内存回收触发有两种情况,上面说的是一种,也就是我们设置了过期的对象到期的时候触发的到期释放的内存回收,还有一种是内存使用达到maxmemory上限时候触发的溢出回收。本篇主要讲前者,后者可以参考《Redis——内存消耗及回收》。
生存时间:(Time To Live, TTL),经过指定的秒/毫秒之后,服务器自动删除TTL为0的key
过期时间:(expire time),时间戳,表示一个具体时间点,到这个时间点后,服务器会删除key
(1)设置生存时间TTL
- EXPIRE key ttl #设置ttl,单位s
- PEXPIRE key ttl #设置ttl,单位ms
可以对一个已经带有生存时间的 key 执行 EXPIRE 命令,新指定的生存时间会取代旧的生存时间。
为给定 key 设置生存时间,当 key 过期时(生存时间为 0 ),它会被自动删除。
在 Redis 中,带有生存时间的 key 被称为『易失的』(volatile)。
生存时间可以通过使用 DEL 命令来删除整个 key 来移除,或者被 SET 和 GETSET 命令覆写(overwrite),这意味着,如果一个命令只是修改(alter)一个带生存时间的 key 的值而不是用一个新的 key 值来代替(replace)它的话,那么生存时间不会被改变。
比如说,对一个 key 执行 INCR 命令,对一个列表进行 LPUSH 命令,或者对一个哈希表执行 HSET 命令,这类操作都不会修改 key 本身的生存时间。
另一方面,如果使用 RENAME 对一个 key 进行改名,那么改名后的 key 的生存时间和改名前一样。
RENAME 命令的另一种可能是,尝试将一个带生存时间的 key 改名成另一个带生存时间的 another_key ,这时旧的 another_key (以及它的生存时间)会被删除,然后旧的 key 会改名为 another_key ,因此,新的 another_key 的生存时间也和原本的 key 一样。
使用 PERSIST 命令可以在不删除 key 的情况下,移除 key 的生存时间,让 key 重新成为一个『持久的』(persistent) key 。
这个命令和 EXPIRE 命令的作用类似,但是它以毫秒为单位设置 key 的生存时间,而不像 EXPIRE 命令那样,以秒为单位。
返回值:
设置成功返回 1 。
当 key 不存在或者不能为 key 设置生存时间时(比如在低于 2.1.3 版本的 Redis 中你尝试更新 key 的生存时间),返回 0 。
示例:
- redis> SET cache_page "www.AA.com"
- OK
-
- redis> EXPIRE cache_page 30 # 设置过期时间为 30 秒
- (integer) 1
-
- redis> TTL cache_page # 查看剩余生存时间
- (integer) 23
-
- redis> EXPIRE cache_page 30000 # 更新过期时间
- (integer) 1
-
- redis> TTL cache_page
- (integer) 29996
(2)设置过期时间 (指定过期的时间节点)
- EXPIREAT key timestamp #设置expire time,s
- PEXPIREAT key timestamp #设置exprie time,ms
EXPIREAT 的作用和 EXPIRE 类似,都用于为 key 设置生存时间。
不同在于 EXPIREAT 命令接受的时间参数是 UNIX 时间戳(unix timestamp)。
这个命令和 EXPIREAT 命令类似,但它以毫秒为单位设置 key 的过期 unix 时间戳。
过期时间的精确度
在 Redis 2.4 版本中,过期时间的延迟在 1 秒钟之内 —— 也即是,就算 key 已经过期,但它还是可能在过期之后一秒钟之内被访问到,而在新的 Redis 2.6 版本中,延迟被降低到 1 毫秒之内。
以上4种命令虽然各有不同,但是其底层都是使用 PEXPIREAT 实现的!
(3)删除和更新
PERSIST key #移除生存时间
移除给定 key 的生存时间,将这个 key 从『易失的』(带生存时间 key )转换成『持久的』(一个不带生存时间、永不过期的 key )。
DLE 命令可以删除key,也会删除其生存时间
SET 和 GETSET 命令也可以覆写生存时间
(4)查看剩余存活时间
- TTL key #计算key的剩余生存时间,s
- PTTL key #计算key的剩余生存时间,ms
以秒为单位,返回给定 key 的剩余生存时间(TTL, time to live)。
这个命令类似于 TTL 命令,但它以毫秒为单位返回 key 的剩余生存时间。
返回值:
当 key 不存在时,返回 -2 。
当 key 存在但没有设置剩余生存时间时,返回 -1 。
否则,以秒为单位,返回 key 的剩余生存时间。
redisDb结果的expires字典中保存了数据库中的所有key的过期时间,redisDb的声明如下:
- /* Redis database representation. There are multiple databases identified
- * by integers from 0 (the default database) up to the max configured
- * database. The database number is the 'id' field in the structure. */
- //每个数据库都是一个redisDb,id为数据库编号
- typedef struct redisDb {
- dict *dict; //键空间,保存了数据中所有键值对
- dict *expires; //过期字典,保存了数据库中所有键的过期时间
- dict *blocking_keys;
- dict *ready_keys;
- dict *watched_keys;
- struct evictionPoolEntry *eviction_pool;
- int id; /* Database ID */
- long long avg_ttl; /* Average TTL, just for stats */
- } redisDb;
expires 的键是一个指针,指向某个键对象,值是一个 long long 类型整数,保存了过期时间,是一个毫秒精度的UNIX时间戳
可见,过期时间的保存是使用key来作为关联的,所以操作用,修改key均可以修改过期时间,而只修改key的value,是不是改变其过期时间的;
如何计算过期时间?
底层的处理方式也很简单,获取key的生存时间戳,减去当前时间戳即可;
如果键不存在,则返回-2;
如果键没有设置过期时间,则返回-1;
同样可以使用此方法判断key是否过期,TTL/PTTL 结果小于0,则表示过去,大于0,则表示未过期;
有哪些过期删除策略?
定时删除:设置键的过期时间的同时,设置一个定时器,来删除键
惰性删除:放任过期键不管,每次从键空间取值时,检查是否过期,以决定是否删除;
定期删除:每隔一段时间,进行一次数据库检查,删除里面的过期键,至于,要删除多少过期键,以及要检查多少数据库,由算法决定;
(1)定时删除
定时删除是指在设置键的过期时间的同时,创建一个定时器,让定时器在键的过期时间来临时,立即执行对键的删除操作。
定时删除策略对内存是最友好的:通过使用定时器,定时删除策略可以保证过期键会尽可能快的被删除,并释放过期键所占用的内存。
定时删除策略的缺点是,他对CPU时间是最不友好的:再过期键比较多的情况下,删除过期键这一行为可能会占用相当一部分CPU时间。
除此之外,创建一个定时器需要用到Redis服务器中的时间事件。而当前时间事件的实现方式----无序链表,查找一个事件的时间复杂度为O(N)----并不能高效地处理大量时间事件。
(2)惰性删除
惰性删除是指放任键过期不管,但是每次从键空间中获取键时,都检查取得的键是否过期,如果过期的话就删除该键,如果没有过期就返回该键。
惰性删除策略对CPU时间来说是最友好的,但对内存是最不友好的。如果数据库中有非常多的过期键,而这些过期键又恰好没有被访问到的话,那么他们也许永远也不会被删除。
Redis 的惰性删除策略由 db.c/expireIfNeeded 函数实现,所有读写Redis的命令在执行之前都会调用expireIfNeeded 函数
(3)定期删除
定期删除是指每隔一段时间,程序就对数据库进行一次检查,删除里面的过期键。
定期删除策略是前两种策略的一种整合和折中:
定期删除策略的难点是确定删除操作执行的时长和频率:
那你有没有想过一个问题,Redis里面如果有大量的key,怎样才能高效的找出过期的key并将其删除呢,难道是遍历每一个key吗?假如同一时期过期的key非常多,Redis会不会因为一直处理过期事件,而导致读写指令的卡顿。
这里说明一下,Redis是单线程的,所以一些耗时的操作会导致Redis卡顿,比如当Redis数据量特别大的时候,使用keys * 命令列出所有的key。
Redis所有的键都可以设置过期属性,内部保存在过期字典中。由于进程内保存大量的键,维护每个键精准的过期删除机制会导致消耗大量的 CPU,对于单线程的Redis来说成本过高。
因此——Redis服务器实际使用的是惰性删除和定期删除两种策略:通过配合使用这两种删除策略,服务器可以很好的在合理使用CPU时间和避免浪费内存空间之间取得平衡。
惰性删除:顾名思义,指的是不主动删除,当用户访问已经过期的对象的时候才删除种方式看似很完美,在访问的时候检查key的过期时间,最大的优点是节省cpu的开销,不用另外的内存和TTL链表来维护删除信息。但是如果一个key已经过期了,如果长时间没有被访问,那么这个key就会一直存留在内存之中,严重消耗了内存资源。
定时任务删除:为了弥补第一种方式的缺点,redis内部还维护了一个定时任务,默认每秒运行10次。定时任务中删除过期逻辑采用了自适应算法,使用快、慢两种速率模式回收键。
定期删除:Redis会将所有设置了过期时间的key放入一个字典中,然后每隔一段时间从字典中随机一些key检查过期时间并删除已过期的key。
Redis默认每秒进行10次过期扫描:
同时,为了保证不出现循环过度的情况,Redis还设置了扫描的时间上限,默认不会超过25ms。
图示:
流程说明:
1. 定时任务在每个数据库空间随机检查20个键,当发现过期时删除对应的键。
2. 如果超过检查数25%的键过期,循环执行回收逻辑直到不足25%或 运行超时为止,慢模式下超时时间为25毫秒。
3. 如果之前回收键逻辑超时,则在Redis触发内部事件之前再次以快模 式运行回收过期键任务,快模式下超时时间为1毫秒且2秒内只能运行1次。
4. 快慢两种模式内部删除逻辑相同,只是执行的超时时间不同。
定期删除策略的实现
过期键的定期删除策略由函数redis.c/activeExpireCycle实现,每当Redis服务器周期性操作redis.c/serverCron函数执行时,activeExpireCycle函数就会被调用,它在规定的时间内分多次遍历服务器中的各个数据库,从数据库的expires字典中随机检查一部分键的过期时间,并删除其中的过期键。
(1)DEL/SET/GETSET等命令会清除过期时间
在使用DEL、SET、GETSET等会覆盖key对应value的命令操作一个设置了过期时间的key的时候,会导致对应的key的过期时间被清除。
- //设置mykey的过期时间为300s
- 127.0.0.1:6379> set mykey hello ex 300
- OK
- //查看过期时间
- 127.0.0.1:6379> ttl mykey
- (integer) 294
- //使用set命令覆盖mykey的内容
- 127.0.0.1:6379> set mykey olleh
- OK
- //过期时间被清除
- 127.0.0.1:6379> ttl mykey
- (integer) -1
(2)INCR/LPUSH/HSET等命令则不会清除过期时间
而在使用INCR/LPUSH/HSET这种只是修改一个key的value,而不是覆盖整个value的命令,则不会清除key的过期时间。
INCR:
- //设置incr_key的过期时间为300s
- 127.0.0.1:6379> set incr_key 1 ex 300
- OK
- 127.0.0.1:6379> ttl incr_key
- (integer) 291
- //进行自增操作
- 127.0.0.1:6379> incr incr_key
- (integer) 2
- 127.0.0.1:6379> get incr_key
- "2"
- //查询过期时间,发现过期时间没有被清除
- 127.0.0.1:6379> ttl incr_key
- (integer) 277
LPUSH:
- //新增一个list类型的key,并添加一个为1的值
- 127.0.0.1:6379> LPUSH list 1
- (integer) 1
- //为list设置300s的过期时间
- 127.0.0.1:6379> expire list 300
- (integer) 1
- //查看过期时间
- 127.0.0.1:6379> ttl list
- (integer) 292
- //往list里面添加值2
- 127.0.0.1:6379> lpush list 2
- (integer) 2
- //查看list的所有值
- 127.0.0.1:6379> lrange list 0 1
- 1) "2"
- 2) "1"
- //能看到往list里面添加值并没有使过期时间清除
- 127.0.0.1:6379> ttl list
- (integer) 252
(3)PERSIST命令会清除过期时间
当使用PERSIST命令将一个设置了过期时间的key转变成一个持久化的key的时候,也会清除过期时间。
- 127.0.0.1:6379> set persist_key haha ex 300
- OK
- 127.0.0.1:6379> ttl persist_key
- (integer) 296
- //将key变为持久化的
- 127.0.0.1:6379> persist persist_key
- (integer) 1
- //过期时间被清除
- 127.0.0.1:6379> ttl persist_key
- (integer) -1
(4)使用RENAME命令,老key的过期时间将会转到新key上
在使用例如:RENAME KEY_A KEY_B命令将KEY_A重命名为KEY_B,不管KEY_B有没有设置过期时间,新的key KEY_B将会继承KEY_A的所有特性。
- //设置key_a的过期时间为300s
- 127.0.0.1:6379> set key_a value_a ex 300
- OK
- //设置key_b的过期时间为600s
- 127.0.0.1:6379> set key_b value_b ex 600
- OK
- 127.0.0.1:6379> ttl key_a
- (integer) 279
- 127.0.0.1:6379> ttl key_b
- (integer) 591
- //将key_a重命名为key_b
- 127.0.0.1:6379> rename key_a key_b
- OK
- //新的key_b继承了key_a的过期时间
- 127.0.0.1:6379> ttl key_b
- (integer) 248
这里篇幅有限,我就不一一将key_a重命名到key_b的各个情况列出来,大家可以在自己电脑上试一下key_a设置了过期时间,key_b没设置过期时间这种情况。
(5)使用EXPIRE/PEXPIRE设置的过期时间为负数或者使用EXPIREAT/PEXPIREAT设置过期时间戳为过去的时间会导致key被删除
EXPIRE:
- 127.0.0.1:6379> set key_1 value_1
- OK
- 127.0.0.1:6379> get key_1
- "value_1"
- //设置过期时间为-1
- 127.0.0.1:6379> expire key_1 -1
- (integer) 1
- //发现key被删除
- 127.0.0.1:6379> get key_1
- (nil)
EXPIREAT:
- 127.0.0.1:6379> set key_2 value_2
- OK
- 127.0.0.1:6379> get key_2
- "value_2"
- //设置的时间戳为过去的时间
- 127.0.0.1:6379> expireat key_2 10000
- (integer) 1
- //key被删除
- 127.0.0.1:6379> get key_2
- (nil)
6、EXPIRE命令可以更新过期时间
对一个已经设置了过期时间的key使用expire命令,可以更新其过期时间。
- //设置key_1的过期时间为100s
- 127.0.0.1:6379> set key_1 value_1 ex 100
- OK
- 127.0.0.1:6379> ttl key_1
- (integer) 95
- 更新key_1的过期时间为300s
- 127.0.0.1:6379> expire key_1 300
- (integer) 1
- 127.0.0.1:6379> ttl key_1
- (integer) 295
在Redis2.1.3以下的版本中,使用expire命令更新一个已经设置了过期时间的key的过期时间会失败。并且对一个设置了过期时间的key使用LPUSH/HSET等命令修改其value的时候,会导致Redis删除该key。
参考:
在Redis中设置了过期时间的Key,需要注意哪些问题? - SegmentFault 思否
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。