在实际开发中经常会遇到一些有时效的数据,比如,限时优惠活动,缓存或验证码等,过了一定时间就需要删除这些数据。在关系性数据库中一般需要额外的一个字段记录到期时间,然后定期检测删除过期数据。在 Redis 中提供了键的过期时间这个功能来解决这个问题。通过这个功能,可以让特定的键在指定的时间之后自动删除,而不需要手动执行删除操作。
Redis 有四个不同的命令可以用于设置键的生存时间(键可以存在多久)或过期时间(键什么时候会被删除):
虽然有多种不同单位和不同形式的设置命令,但实际上 EXPIRE、PEXPIRE、EXPIREAT 三个命令都是使用 PEXPIREAT 命令来实现的:
(1) EXPIRE 命令转换成 PEXPIRE 命令:
def EXPIRE(key, ttl_in_sec):
# 将TTL从秒转换成毫秒
ttl_in_ms = sec_to_ms(ttl_in_sec)
PEXPIRE(key,ttl_in_ms)
(2) PEXPIRE 命令转换成 PEXPIREAT 命令:
def PEXPIRE(key, ttl_in_ms):
# 获取以毫秒计算的当前UNIX时间戳
now_ms = get_current_unix_timestamp_in_ms()
# 当前时间加上TTL,得出毫秒格式的键过期时间
PEXPIREAT(key, now_ms + ttl_in_ms)
(3) EXPIREAT 命令转换成 PEXPIREAT 命令:
def EXPIREAT(key, expire_time_in_sec):
# 将过期时间从秒转换为毫秒
expire_time_in_ms = sec_to_ms(expire_time_in_sec)
PEXPIREAT(key, expire_time_in_ms)
通过 EXPIRE 命令或者 PEXPIRE 命令,可以以秒级或者毫秒级为某个键设置生存时间(TimeToLive,TTL),在经过指定的秒数或者毫秒数之后,服务器就会自动删除这个键:
127.0.0.1:6379> set k1 v1
OK
127.0.0.1:6379> expire k1 5 // 设置生存时间为5s,即5s后过期
(integer) 1
127.0.0.1:6379> get k1 // 5s 之内执行
"v1"
127.0.0.1:6379> get k1 // 5s 之外执行
(nil)
127.0.0.1:6379> set k1 v1
OK
127.0.0.1:6379> pexpire k1 5000 // 设置生存时间为5000ms,即5000ms后过期
(integer) 1
127.0.0.1:6379> get k1 // 5000ms 之内执行
"v1"
127.0.0.1:6379> get k1 // 5000ms 之外执行
(nil)
SETEX 命令可以在设置一个字符串键的同时为键设置过期时间,其原理与 EXPIRE 命令设置过期时间的原理是完全一样的。
EXPIREAT 命令或 PEXPIREAT 命令,可以以秒级或者毫秒级为某个键设置过期时间(expire time)。过期时间是一个 UNIX 时间戳,当键的过期时间来临时,服务器就会自动从数据库中删除这个键:
127.0.0.1:6379> set k1 v1
OK
127.0.0.1:6379> expireat k1 1636988400 // 设置过期时间戳(秒)为 1636988400,即到达 2021-11-15 23:00:00 后过期
(integer) 1
127.0.0.1:6379> get k1 // 设置时间点之前执行
"v1"
127.0.0.1:6379> get k1 // 设置时间点之后执行
(nil)
127.0.0.1:6379> set k1 v1
OK
127.0.0.1:6379> pexpireat k1 1636988580000 // 设置过期时间戳(毫秒)为 1636988580000,即到达 2021-11-15 23:03:00 后过期
(integer) 1
127.0.0.1:6379> get k1
"v1"
127.0.0.1:6379> get k1
(nil)
redisDb 结构的 expires 字典保存了数据库中所有键的过期时间,这个字典称为过期字典:
typedef struct redisDb {
// ...
// 数据库键空间,保存着数据库中的所有键值对
dict *dict;
// 过期字典,保存着键的过期时间
dict *expires;
// ...
} redisDb;
下图展示了一个带有过期字典的数据库例子,在这个例子中,键空间保存了数据库中的所有键值对(在这展示了两个键值对):
而过期字典则保存了数据库键的过期时间,在这保存了两个键值对:
为了展示方便,上图的键空间和过期字典中重复出现了两次 a 键对象 和 b 键对象。在实际中,键空间的键和过期字典的键都指向同一个键对象,所以不会出现任何重复对象,也不会浪费任何空间。
当客户端执行 PEXPIREAT 命令(或者其他三个会转换成 PEXPIREAT 命令的命令)为一个键设置过期时间时,服务器会在数据库的过期字典中关联给定的键和过期时间。举个例子,如果在服务器执行以下命令之后:
127.0.0.1:6379> set c c-v
OK
127.0.0.1:6379> pexpireat c 1637077800000 // 设置过期时间戳(毫秒)为 1637077800000,即到达 2021-11-16 23:50:00 后过期
(integer) 0
127.0.0.1:6379> get c
"c-v"
过期字典将新增一个键值对,其中键为 c 键对象,而值则为 1637077800000(2021-11-16 23:50:00),如下图所示。
以下是 PEXPIREAT 命令的伪代码定义:
def PEXPIREAT(key, expire_time_in_ms):
# 如果给定的键不存在于键空间,那么不能设置过期时间
if key not in redisDb.dict:
return 0
# 在过期字典中关联键和过期时间
redisDb.expires[key] = expire_time_in_ms
# 过期时间设置成功
return 1
如果想知道一个键还有多久时间会被删除,可以使用 TTL 或者 PTTL 命令,TTL 命令以秒为单位返回键的剩余生存时间,而 PTTL 命令则以毫秒为单位返回键的剩余生存时间:
127.0.0.1:6379> set k1 v1
OK
127.0.0.1:6379> expire k1 30 // 设置生存时间为30s,即30s后过期
(integer) 1
127.0.0.1:6379> ttl k1 // 使用 ttl 命令查看 k1 的过期时间
(integer) 27
127.0.0.1:6379> pttl k1 // 使用 pttl 命令查看 k1 的过期时间
(integer) 20807
127.0.0.1:6379> get k1 // 30s 之内执行
"v1"
127.0.0.1:6379> get k1 // 30s 之外执行
(nil)
127.0.0.1:6379> ttl k1
(integer) -2
随着时间的流逝,k1 键的生存时间逐渐减少,30 秒后会被删除。当键不存在时 TTL(PTTL) 命令会返回 -2。
TTL 和 PTTL 两个命令都是通过计算键的过期时间和当前时间之间的差来实现的,以下是这两个命令的伪代码实现:
def PTTL(key):
# 键不存在于数据库
if key not in redisDb.dict:
return -2
# 尝试取得键的过期时间
# 如果键没有设置过期时间,那么 expire_time_in_ms 将为 None
expire_time_in_ms = redisDb.expires.get(key)
# 键没有设置过期时间
if expire_time_in_ms is None:
return -1
# 获得当前时间
now_ms = get_current_unix_timestamp_in_ms()
# 过期时间减去当前时间,得出的差就是键的剩余生存时间
return (expire_time_in_ms - now_ms)
def TTL(key):
# 获取以毫秒为单位的剩余生存时间
ttl_in_ms = PTTL(key)
if ttl_in_ms < 0:
# 处理返回值为-2和-1的情况
return ttl_in_ms
else:
# 将毫秒转换为秒
return ms_to_sec(ttl_in_ms)
当键不存在时,返回 -2;当键存在但没有设置剩余生存时间时,返回 -1;否则,以秒为单位,返回 key 的剩余生存时间。在 Redis 2.8 以前,当键不存在,或者键没有设置剩余生存时间时,命令都返回 -1。
举个例子,对于一个过期时间为 1637077800000(2021-11-16 23:50:00)的键 c 来说,如果当前时间为 1637077200000(2021-11-16 23:40:00):
如果想取消键的过期时间,可以使用 PERSIST 命令,如下所示:
127.0.0.1:6379> EXPIRE k 50
(integer) 1
127.0.0.1:6379> GET k
"v"
127.0.0.1:6379> PERSIST k
(integer) 1
127.0.0.1:6379> TTL k
(integer) -1
PERSIST 命令就是 PEXPIREAT 命令的反操作:在过期字典中查找给定的键,然后解除键和值(过期时间)在过期字典中的关联。如果数据库当前的状态如上图所示,那么当服务器执行以下命令之后:
127.0.0.1:6379> PERSIST b
(integer) 1
那么数据库的状态将更新为如下图所示:
可以看到,当 PERSIST 命令行之后,过期字典中原来的 b 键值对消失了,这代表数据库键 b 的过期时间已经被移除。以下是 PERSIST 命令的伪代码定义:
def PERSIST(key):
# 如果键不存在,或者键没有设置过期时间,那么直接返回
if key not in redisDb.expires:
return 0
# 移除过期字典中给定键的键值对关联
redisDb.expires.remove(key)
# 键的过期时间移除成功
return 1
从上我们可以知道如果过期时间被成功的清除则返回 1,否则返回 0。
在使用 DEL、SET、GETSET 等会覆盖键对应值的命令时,如果操作一个设置了过期时间的键,则对应键的过期时间会被清除:
127.0.0.1:6379> set k1 v1
OK
127.0.0.1:6379> expire k1 30 // 设置生存时间为30s,即30s后过期
(integer) 1
127.0.0.1:6379> get k1 // 30s 之内执行
"v1"
127.0.0.1:6379> set k1 v2 // 30s 之内执行 重新赋值会清除过期时间
OK
127.0.0.1:6379> get k1 // 30s 之内执行
"v2"
127.0.0.1:6379> ttl k1 // 30s 之内执行
(integer) -1
127.0.0.1:6379> get k1 // 30s 之外执行
"v2"
在使用 INCR、LPUSH、HSET 等只是修改键的值,而不是覆盖整个值的命令时,不会清除键的过期时间:
127.0.0.1:6379> set k1 1
OK
127.0.0.1:6379> expire k1 30 // 设置生存时间为30s,即30s后过期
(integer) 1
127.0.0.1:6379> get k1 // 30s 之内执行
"1"
127.0.0.1:6379> ttl k1 // 查看过期时间
(integer) 19
127.0.0.1:6379> incr k1 // 自增1
(integer) 2
127.0.0.1:6379> ttl k1 // 过期时间没有被清除
(integer) 8
使用 RENAME 命令会转移老键的过期时间到新键上。在使用例如:RENAME k1 k2 命令将 k1 重命名为 k2,不管 k2 有没有设置过期时间,新的键 k2 将会继承 k1 的所有特性:
127.0.0.1:6379> set k1 v1 ex 120 // 设置生存时间为120s,即120s后过期
OK
127.0.0.1:6379> set k2 v2 ex 60 // 设置生存时间为60s,即60s后过期
OK
127.0.0.1:6379> ttl k1
(integer) 113
127.0.0.1:6379> ttl k2
(integer) 53
127.0.0.1:6379> rename k1 k2 // k1 重命名 k2
OK
127.0.0.1:6379> ttl k2
(integer) 103
参考: