Redis键的生存时间和过期时间

本文详细介绍了Redis中键过期的设置与管理方法,包括如何使用expire和persist命令控制键的有效期,以及Redis如何处理过期键的不同策略。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、设置生存时间

        Redis作为内存数据库,和memcached一样提供了设置键的生存时间和过期时间的功能。通过expire命令或者pexpire命令实现秒级或者毫秒级的生存时间的设置功能:

  • EXPIRE <KEY> <TTL> : 将键的生存时间设为 ttl 秒
  • PEXPIRE <KEY> <TTL> :将键的生存时间设为 ttl 毫秒
  • EXPIREAT <KEY> <timestamp> :将键的过期时间设为 timestamp 所指定的秒数时间戳
  • PEXPIREAT <KEY> <timestamp>: 将键的过期时间设为 timestamp 所指定的毫秒数时间戳.

虽然有多种不同单位和不同形式的设置命令,但实际上EXPIRE、PEXPIRE、EXPIREAT、PEXPIREAT四个命令内部都是调用同一个函数实现,在db.c 文件中,四个命令对应的函数如下:

//expire命令
void expireCommand(redisClient *c) {
    expireGenericCommand(c,mstime(),UNIT_SECONDS);
}
//expiread命令
void expireatCommand(redisClient *c) {
    expireGenericCommand(c,0,UNIT_SECONDS);
}
//pexpire命令
void pexpireCommand(redisClient *c) {
    expireGenericCommand(c,mstime(),UNIT_MILLISECONDS);
}
//pexpireat命令
void pexpireatCommand(redisClient *c) {
    expireGenericCommand(c,0,UNIT_MILLISECONDS);
}
void expireGenericCommand(redisClient *c, long long basetime, int unit) {
    robj *key = c->argv[1], *param = c->argv[2];
    long long when; /* unix time in milliseconds when the key will expire. */
    // 取出 when 参数
    if (getLongLongFromObjectOrReply(c, param, &when, NULL) != REDIS_OK)
        return;
    // 如果传入的过期时间是以秒为单位的,那么将它转换为毫秒
    if (unit == UNIT_SECONDS) when *= 1000;
    when += basetime;
    /* No key, return zero. */
    // 取出键
    if (lookupKeyRead(c->db,key) == NULL) {
        addReply(c,shared.czero);
        return;
    }
    /* EXPIRE with negative TTL, or EXPIREAT with a timestamp into the past
     * should never be executed as a DEL when load the AOF or in the context
     * of a slave instance.
     * 在载入数据时,或者服务器为附属节点时,
     * 即使 EXPIRE 的 TTL 为负数,或者 EXPIREAT 提供的时间戳已经过期,
     * 服务器也不会主动删除这个键,而是等待主节点发来显式的 DEL 命令。
     * Instead we take the other branch of the IF statement setting an expire
     * (possibly in the past) and wait for an explicit DEL from the master. 
     * 程序会继续将(一个可能已经过期的 TTL)设置为键的过期时间,
     * 并且等待主节点发来 DEL 命令。
     */
    if (when <= mstime() && !server.loading && !server.masterhost) {
        // when 提供的时间已经过期,服务器为主节点,并且没在载入数据
        robj *aux;
        redisAssertWithInfo(c,key,dbDelete(c->db,key));
        server.dirty++;
        /* Replicate/AOF this as an explicit DEL. */
        // 传播 DEL 命令
        aux = createStringObject("DEL",3);
        rewriteClientCommandVector(c,2,aux,key);
        decrRefCount(aux);
        signalModifiedKey(c->db,key);
        notifyKeyspaceEvent(REDIS_NOTIFY_GENERIC,"del",key,c->db->id);
        addReply(c, shared.cone);
        return;
    } else {
        // 设置键的过期时间
        // 如果服务器为附属节点,或者服务器正在载入,
        // 那么这个 when 有可能已经过期的
        setExpire(c->db,key,when);
        addReply(c,shared.cone);
        signalModifiedKey(c->db,key);
        notifyKeyspaceEvent(REDIS_NOTIFY_GENERIC,"expire",key,c->db->id);
        server.dirty++;
        return;
    }
}

        可以看到以上四条命令最终都调用expireGenericCommand函数执行。在redis.h/redisDb的结构的expires字典保存了数据库中所有键的过期时间,这个字典称为过期字典。

  • 过期字典是一个指针,指向键空间的某个键对象
  • 过期字典的值是一个 long long 类型的整数,这个整数保存了键所指向的数据库键的过期时间–一个毫秒级的 UNIX 时间戳
typedef struct redisDb {
    // 数据库键空间,保存着数据库中的所有键值对
    dict *dict;
    // 键的过期时间,字典的键为键,字典的值为过期事件 UNIX 时间戳
    dict *expires;
    //...
} redisDb;

因此,上一节数据库键空间的基础上加上过期时间:


其键空间变更为以下状态(在实际代码中,键空间的键和过期时间的键都指向同一个键对象):


一旦键过了过期时间,再通过Redis数据库取该键值对就为空:


二、移除过期时间

        persist命令就是pexpireat命令的反操作:persist命令在过期字典中查找给定的键,并解除键值在过期字典中的关联。如果对上述键空间执行:persist message、persist book,其键空间变更为如下状态:


三、计算并返回键的剩余时间

ttl或者pttl命令可以返回秒级或者毫秒级别的键的剩余生存时间。

//ttl命令
void ttlCommand(redisClient *c) {
    ttlGenericCommand(c, 0);
}
//pttl命令
void pttlCommand(redisClient *c) {
    ttlGenericCommand(c, 1);
}
/*
 * 返回键的剩余生存时间。
 * output_ms 指定返回值的格式:
 *  - 为 1 时,返回毫秒
 *  - 为 0 时,返回秒
 */
void ttlGenericCommand(redisClient *c, int output_ms) {
    long long expire, ttl = -1;

    /* If the key does not exist at all, return -2 */
    // 取出键
    if (lookupKeyRead(c->db,c->argv[1]) == NULL) {
        addReplyLongLong(c,-2);
        return;
    }
    /* The key exists. Return -1 if it has no expire, or the actual
     * TTL value otherwise. */
    // 取出过期时间
    expire = getExpire(c->db,c->argv[1]);
    if (expire != -1) {
        // 计算剩余生存时间
        ttl = expire-mstime();
        if (ttl < 0) ttl = 0;
    }
    if (ttl == -1) {
        // 键是持久的
        addReplyLongLong(c,-1);
    } else {
        // 返回 TTL 
        // (ttl+500)/1000 计算的是渐近秒数
        addReplyLongLong(c,output_ms ? ttl : ((ttl+500)/1000));
    }
}

四、过期键的删除策略

通过过期字典,程序可以通过以下步骤检查一个键是否过期:

(1)检查给定键是否存在于过期字典中,如果存在则取出其过期时间;

(2)检查当前UNIX时间戳是否大于键的过期时间,如果是则键过期执行过期删除;

对于过期键的删除,有三种策略:

(1)定时删除:在设置键的过期时间时,创建一个定时事件,当过期时间到达,由事务处理器自动执行过期键删除的操作。定时删除策略的优点是对内存友好缺点是对CPU不友好,查找一个时间事件的时间复杂度为O(N),所以不能高效处理大量时间事件。

(2)惰性删除:每次从键空间获取键时,检查键是否过期,如果过期则删除,否则返回对应的值。惰性删除策略的优点是对CPU友好缺点是对内存不友好

(3)定期删除:每过一段时间,对数据库检查一次,删除里面的过期键。整合了定时删除和惰性删除的策略进行一定程度的折中。

在db.c/expireIfNeeded函数中可以看到所有命令在读取或写入数据库之前,都会调用expireIfNeeded对键进行检查:

int expireIfNeeded(redisDb *db, robj *key) {
    mstime_t when = getExpire(db,key);
    mstime_t now;
    if (when < 0) return 0; /* 没有设置过期时间 */
    /* 如果服务器正在加载数据,稍后再处理 */
    if (server.loading) return 0;
    ...
    /* 没有过期 */
    if (now <= when) return 0;
    /* 删除键和过期时间 */
    server.stat_expiredkeys++;
    /*将删除命令传播到 AOF 文件和附属节点*/
    propagateExpire(db,key);
    notifyKeyspaceEvent(REDIS_NOTIFY_EXPIRED,
        "expired",key,db->id);
    return dbDelete(db,key);
}

参考文献
1、http://www.redis.net.cn/tutorial/3506.html
2、《Redis设计与实现》第二版---黄健宏
3、https://github.com/xingzhexiaozhu/redis-3.0-annotated
4、http://www.yiibai.com/redis/redis_strings.html

<think>首先,用户的问题是:“我想设置Redis过期时间的方法 请问Redis 设置过期时间的方法”。这翻译成英文是:“I want to know the method to set the expiration time for Redis keys. Please tell me the method to set the expiration time for Redis keys.” 根据系统级指令,我需要遵循以下规则: 1. 所有行内数学表达式使用$...$格式。 2. 独立公式使用$$...$$格式并单独成段。 3. LaTeX语法正确。 4. 使用中文回答。 5. 生成相关问题。 6. 回答中引用的段落末尾自然地添加引用标识,例如[^1]。 用户可见层指令: - 回答结构清晰,帮助用户逐步解决问题。 - 尽量保证回答真实可靠。 - 参考站内引用:引用[1]到[5]提供了关于Redis设置过期时间的相关信息。 参考引用内容: - 引用[1]:介绍了Redis设置生存时间过期时间的四个命令:EXPIRE, PEXPIRE, EXPIREAT, PEXPIREAT。 - 引用[2]:提到了Redis的lazy expiration策略。 - 引用[3]:详细说明了设置过期时间的注意事项,如不存在时的返回值、负值过期时间的效果、PERSIST命令的使用、以及字符串类型使用SET命令会清除过期时间。 - 引用[4]:展示了在Spring Data Redis中使用RedisTemplate设置过期时间的方法。 - 引用[5]:给出了Redis命令行中设置过期时间的例子。 用户的问题是关于设置Redis过期时间的方法,所以我需要基于这些引用提供清晰的回答。 回答结构: 1. **介绍Redis设置过期时间的基本方法**:使用命令如EXPIRE、PEXPIRE等。 2. **详细解释每个命令**:基于引用内容。 3. **注意事项**:如引用[3]中提到的陷阱。 4. **编程示例**:如果适用,展示如何在代码中实现,如使用RedisTemplate。 5. **总结**:简要概述关点。 6. **生成相关问题**:在末尾添加相关问题。 由于用户的问题是用中文问的,我必须用中文回答。 生成相关问题:指令要求生成相关问题,我需要基于主题创建2-3个问题。 最终回答格式: - 使用清晰的结构,如标题、步骤列表。 - 自然地添加引用标识,例如在解释某个点时引用相关部分。 大纲: 1. **开头**:直接回答问题,解释Redis设置过期时间的重要性。 2. **方法**:列出并描述设置过期时间的命令。 - EXPIRE:设置秒级生存时间。 - PEXPIRE:设置毫秒级生存时间。 - EXPIREAT:设置秒级时间过期时间。 - PEXPIREAT:设置毫秒级时间过期时间。 3. **注意事项**:基于引用[3],讨论常见问题。 4. **编程示例**:展示在Spring Boot中使用RedisTemplate的代码示例。 5. **总结**:重申关点。 6. **相关问题**:生成2-3个相关问题。 生成相关问题示例: 1. Redis如何删除过期? 2. 设置过期时间时有哪些常见错误需要避免? 3. 如何在Redis中检查的剩余生存时间?</think>### Redis 设置过期时间的方法 在 Redis 中,设置(key)的过期时间是一种常见操作,用于自动删除以节省内存资源。Redis 提供了多种命令来设置生存时间(TTL)或过期时间戳,支持秒级毫秒级精度。以下我将逐步解释具体方法、注意事项编程实现,确保回答真实可靠,基于 Redis 官方文档常见实践[^1][^3][^5]。 #### 1. **基本命令设置过期时间** Redis 有四个核心命令用于设置过期时间。这些命令可以直接在 Redis 命令行或客户端中使用: - **EXPIRE key seconds**:设置生存时间为指定秒数。例如,设置 `name` 在 10 秒后过期: ``` 127.0.0.1:6379> SET name "redis" OK 127.0.0.1:6379> EXPIRE name 10 (integer) 1 # 返回 1 表示成功 ``` - **PEXPIRE key milliseconds**:设置生存时间为指定毫秒数。例如,设置 `age` 在 10000 毫秒(10 秒)后过期: ``` 127.0.0.1:6379> SET age 30 OK 127.0.0.1:6379> PEXPIRE age 10000 (integer) 1 ``` - **EXPIREAT key timestamp**:设置过期时间为 Unix 时间戳(秒级)。例如,设置 `session` 在 2025 年 1 月 1 日 00:00:00 过期: ``` 127.0.0.1:6379> SET session "data" OK 127.0.0.1:6379> EXPIREAT session 1735660800 # 时间戳值 (integer) 1 ``` - **PEXPIREAT key timestamp**:设置过期时间为 Unix 时间戳(毫秒级)。例如,设置 `token` 在毫秒级时间戳过期: ``` 127.0.0.1:6379> SET token "abc123" OK 127.0.0.1:6379> PEXPIREAT token 1735660800000 (integer) 1 ``` 这些命令的返回值:`1` 表示成功设置,`0` 表示不存在或设置失败[^1][^3][^5]。 #### 2. **注意事项常见问题** 在设置过期时间时,需注意以下陷阱,避免数据意外丢失: - **不存在时返回 0**:如果尝试对不存在的设置过期时间Redis 会返回 `0`,不会创建新。 ``` 127.0.0.1:6379> EXPIRE not_exist_key 30 (integer) 0 ``` - **负值过期时间会立即删除**:如果设置负数秒数或毫秒数,会被立刻删除,相当于 `DEL` 命令。 ``` 127.0.0.1:6379> SET hello "world" OK 127.0.0.1:6379> EXPIRE hello -2 (integer) 1 127.0.0.1:6379> GET hello (nil) # 已被删除 ``` - **字符串类型的特殊行为**:对字符串使用 `SET` 命令会清除其过期时间。例如,先设置过期时间,再 `SET` 新值,过期时间失效: ``` 127.0.0.1:6379> EXPIRE hello 50 (integer) 1 127.0.0.1:6379> TTL hello # 检查剩余时间 (integer) 46 127.0.0.1:6379> SET hello "new_value" OK 127.0.0.1:6379> TTL hello (integer) -1 # 过期时间被清除,永久存在 ``` - **清除过期时间**:使用 `PERSIST` 命令可以移除过期时间,使其永久有效。 ``` 127.0.0.1:6379> EXPIRE key 50 (integer) 1 127.0.0.1:6379> PERSIST key (integer) 1 127.0.0.1:6379> TTL key (integer) -1 ``` Redis 采用 lazy expiration 策略:过期不会立即删除,而是在访问时检查并清理[^2][^3]。 #### 3. **在编程中设置过期时间(以 Spring Boot 为例)** 如果您使用 Java Spring Data Redis,可以通过 `RedisTemplate` 方便地设置过期时间。以下是一个完整示例: ```java import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; import java.util.concurrent.TimeUnit; import javax.annotation.Resource; @Service public class RedisService { @Resource private RedisTemplate<String, Object> redisTemplate; // 设置值对并指定过期时间(秒) public void setWithExpire(String key, Object value, long timeoutSeconds) { redisTemplate.opsForValue().set(key, value, timeoutSeconds, TimeUnit.SECONDS); } // 为现有设置过期时间 public void setExpire(String key, long timeout, TimeUnit unit) { redisTemplate.expire(key, timeout, unit); } // 获取的剩余生存时间(秒) public Long getRemainingTime(String key) { return redisTemplate.getExpire(key, TimeUnit.SECONDS); } } ``` - 说明:`setWithExpire` 方法在设置值的同时指定过期时间;`setExpire` 可为已存在的添加过期时间;`getRemainingTime` 返回剩余秒数(-1 表示永久,-2 表示不存在)[^4]。 #### 4. **总结** - **核心方法**:使用 `EXPIRE`、`PEXPIRE`、`EXPIREAT` 或 `PEXPIREAT` 命令设置过期时间,支持秒级或毫秒级精度。 - **最佳实践**:始终检查命令返回值(1 成功,0 失败);避免对字符串重复使用 `SET`,以免清除过期时间;定期用 `TTL` 或 `PTTL` 命令监控剩余时间。 - **内部机制**:Redis 最终统一使用 `PEXPIREAT` 处理所有过期设置,并依赖 lazy expiration 在访问时清理[^1][^3]。 如果您需要更深入的性能调优或具体场景示例,请参考 Redis 官方文档或相关教程。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值