Administrator
发布于 2025-09-15 / 9 阅读
5

Redis 过期键重复设置的底层逻辑:从现象到源码的深度解析

在 Redis 日常使用中,我们常会遇到这样的场景:给一个键设置了过期时间后,又在过期前重复设置该键(且不指定过期时间)。这种操作会产生什么效果?是保留原过期时间,还是清除过期时间?本文将从实际现象出发,结合 Redis 源码,彻底揭开这一行为的底层逻辑。

一、直观现象:重复设置键会清除过期时间

先通过一组实验验证具体行为:

# 1. 设置键并指定过期时间(100秒)
127.0.0.1:6379> SET key "value" EX 100
OK

# 2. 查看剩余过期时间(约99秒)
127.0.0.1:6379> TTL key
(integer) 99

# 3. 过期前重复设置键(不指定过期时间)
127.0.0.1:6379> SET key "new value"
OK

# 4. 再次查看过期时间(返回-1,表示永不过期)
127.0.0.1:6379> TTL key
(integer) -1

结论:当一个键已存在过期时间时,若在过期前用 SET 命令(不指定过期时间)重复设置,原过期时间会被清除,键变为 “永不过期”。

二、源码解析:过期时间的存储与清除机制

要理解这一现象,需从 Redis 中键的存储结构、过期时间的管理方式,以及 SET 命令的执行逻辑三方面分析。

1. 键的存储与过期时间的单独管理

Redis 中,所有键值对存储在数据库结构体 redisDb 中,其中:

  • dict *dict:存储键值对的哈希表(键为字符串,值为 redisObject)。

  • dict *expires:单独存储键的过期时间(键为字符串,值为过期时间戳,单位为毫秒)。

// server.h 中 redisDb 结构体定义
typedef struct redisDb {
    dict *dict;                 // 键值对哈希表
    dict *expires;              // 过期时间哈希表
    // ... 其他字段(如过期键淘汰相关)
} redisDb;


关键点:过期时间与键值对是 “分离存储” 的。一个键是否过期,取决于 expires 哈希表中是否存在对应的时间戳。

2. SET 命令的执行逻辑:会清除过期时间

SET 命令的核心处理函数是 setCommand(定义在 t_string.c 中),其关键逻辑包括:

  • 检查键是否已存在。

  • 存储新的键值对(覆盖原有值)。

  • 清除该键在 expires 哈希表中的记录(若存在)


关键函数 dbDeleteExpire:从 expires 哈希表中删除键的过期时间,实现如下:

// db.c 中 dbDeleteExpire 函数
void dbDeleteExpire(redisDb *db, robj *key) {
    // 从 expires 哈希表中删除键,返回是否删除成功
    if (dictDelete(db->expires, key) == DICT_OK) {
        // 若键存在过期时间,则更新数据库的过期键数量统计
        server.stat_expired_keys--;
    }
}

3. 为什么重复设置会清除过期时间?

结合上述源码可知:

  • 当执行 SET key value(不指定过期时间)时,expire 变量为 false,触发 dbDeleteExpire 调用。

  • dbDeleteExpire 会从 expires 哈希表中删除该键的记录,导致键失去过期时间(即变为永不过期)。

即使键原本存在过期时间,只要重复设置时未指定新的过期时间,Redis 就会默认清除原有过期配置。

三、扩展:哪些命令会清除过期时间?

除了 SET 命令,其他覆盖式修改键值的命令(如 GETSETSETNX 不带过期时间时)也会清除过期时间,核心原因是它们都会调用 dbDeleteExpire 函数。

例如 GETSET 命令(获取旧值并设置新值):

非覆盖式命令(如 APPENDINCR)仅修改值的内容,不会清除过期时间:

bash

# 实验:APPEND 命令不影响过期时间
127.0.0.1:6379> SET key "a" EX 100
OK
127.0.0.1:6379> TTL key
(integer) 98
127.0.0.1:6379> APPEND key "b"  # 追加内容(非覆盖)
(integer) 2
127.0.0.1:6379> TTL key        # 过期时间仍存在
(integer) 95


原因APPEND 等命令仅修改 redisObject 的值,不会调用 dbDeleteExpire,因此 expires 哈希表中的过期时间记录保持不变。

四、底层设计思考:为什么要这样实现?

Redis 设计 “重复设置键会清除过期时间” 的行为,本质是遵循 “显式操作优先” 原则:

  • 当用户未指定过期时间时,Redis 认为 “覆盖设置键” 是一个新的操作,应默认使用键的 “默认状态”(永不过期)。

  • 若需保留过期时间,用户需显式指定(如 SET key value EX 剩余秒数),避免隐式行为导致意外。

这种设计减少了歧义,让用户能更精确地控制键的生命周期。

总结

当一个带有过期时间的 Redis 键被重复设置(不指定过期时间)时:

  1. 键的旧值会被新值覆盖。

  2. Redis 会从 expires 哈希表中删除该键的过期时间记录。

  3. 最终结果:键变为永不过期。

这一行为的底层逻辑由 setCommand 函数中的 dbDeleteExpire 调用实现,体现了 Redis 对 “显式操作” 和 “用户意图” 的尊重。在实际开发中,需注意这一特性,避免因误操作导致键长期留存占用内存