Redis过期功能介绍

Posted by Sky丶Memory on September 28, 2018

Redis作为一款NoSQL数据库产品,其自身提供了自动过期功能来清理失效数据,这可以大大简化开发人员的工作,使其更关注于业务层的逻辑,这篇文章从使用、自动清理策略、对持久化、复制的影响三个方面来介绍Redis过期功能。

使用

Redis默认提供了5种方式设置生存或过期时间,其基本语法大体如下:

  • EXPIRE key seconds: 将键key的生存时间设置为seconds秒

  • EXPIREAT key timestamp: 将键key的过期时间设置为timestamp所指定的秒数时间戳

  • PEXPIRE key milliseconds: 将键key的生存时间设置为milliseconds毫秒

  • PEXPIREAT key milliseconds-timestamp:将键key的过期时间设置为milliseconds-timestamp所指定的毫秒时间戳

  • SET key value [expiration EX seconds | PX milliseconds]: 将键key的生存时间设置为seconds秒或milliseconds毫秒

简化一下上述方法,可以发现设置过期或生存时间无非相对和绝对两种时间方式,绝对时间戳可通过time命令获取Redis服务器当前时间戳,再根据具体的业务逻辑算出对应的时间戳即可。

Redis虽然提供了5种设置时间的方式,其内部设置方式却通过PEXPIREAT实现,其他的命令皆可间接转换为PEXPIREAT,具体转换逻辑伪代码如下所示:

def EXPIRE(key,ttl_in_sec):

    #将TTL从秒转换成毫秒
    ttl_in_ms = sec_to_ms(ttl_in_sec)

    PEXPIRE(key, ttl_in_ms)

def PEXPIRE(key,ttl_in_ms):

    #获取以毫秒计算的当前UNIX时间戳
    now_ms = get_current_unix_timestamp_in_ms()

    #当前时间加上TTL,得出毫秒格式的键过期时间
    PEXPIREAT(key,now_ms+ttl_in_ms)

def EXPIREAT(key,expire_time_in_sec):

    # 将过期时间从秒转换为毫秒
    expire_time_in_ms = sec_to_ms(expire_time_in_sec)

    PEXPIREAT(key, expire_time_in_ms)

键设置过期时间后,可通过TTL或PTTL查看键的剩余生存时间:

redis> set name SkyMemory
OK

redis> expire name 20
(integer) 1

redis> ttl name
(integer) 15

# 20s后
redis> ttl name 
(integer) -2

同样地,可以通过PERSIST命令显示删除键的过期时间:

redis> set name skymemory ex 120
OK

redis> ttl name
(integer) 118

redis> PERSIST name
(integer) 1

redis> ttl name
(integer) -1

过期删除策略

通过指定键过期时间后,Redis默认会自动清理失效的键,那Redis具体的清理策略是什么呢?

Redis内部记录键过期时间格式为绝对Unix时间戳,精确到毫秒,对于过期键的删除策略,Redis内部有2种形式:惰性删除和定时删除

惰性删除,又称被动删除,基本思路大概就是将对键的删除操作延迟到对键的访问,也就是每次对键进行操作时,判断键是否过期:

这种方式对CPU来说是友好的,尽量不占用CPU资源,但对内存来说,又是不友好的,因为键的删除依赖客户端的访问, 如果客户端设置了大量键的过期时间,在过期时间到达后,并没有再访问Redis服务器,这些键会一直占用内存,最终导致Redis服务器内存资源不够, 为了避免这种情况的发生,Redis又额外的增加了定时删除的策略来尽量保证不会出现这种情况。

定时删除,又称主动删除,基本思路大概就是服务器定期(默认1S运行10次)运行删除过期键的任务来清理失效键,整个过程的伪代码描述如下:

# 默认每次检查的数据库数量
DEFAULT_DB_NUMBERS = 16

# 默认每个数据库检查的键数量
DEFAULT_KEY_NUMBERS = 20

# 全局变量,记录检查进度
current_db = 0

def activeExpireCycle():

    # 初始化要检查的数据库数量
    # 如果服务器的数据库数量比 DEFAULT_DB_NUMBERS 要小
    # 那么以服务器的数据库数量为准
    if server.dbnum < DEFAULT_DB_NUMBERS:
        db_numbers = server.dbnum
    else:
        db_numbers = DEFAULT_DB_NUMBERS

    # 遍历各个数据库
    for i in range(db_numbers):

        # 如果current_db的值等于服务器的数据库数量
        # 这表示检查程序已经遍历了服务器的所有数据库一次
        # 将current_db重置为0,开始新的一轮遍历
        if current_db == server.dbnum:
            current_db = 0

        # 获取当前要处理的数据库
        redisDb = server.db[current_db]

        # 将数据库索引增1,指向下一个要处理的数据库
        current_db += 1

        # 检查数据库键
        for j in range(DEFAULT_KEY_NUMBERS):

            # 如果数据库中没有一个键带有过期时间,那么跳过这个数据库
            if redisDb.expires.size() == 0: break

            # 随机获取一个带有过期时间的键
            key_with_ttl = redisDb.expires.get_random_key()

            # 检查键是否过期,如果过期就删除它
            if is_expired(key_with_ttl):
                delete_key(key_with_ttl)

            # 已达到时间上限,停止处理
            if reach_time_limit(): return

持久化、复制影响

Redis默认提供了RDB和AOF两种持久化方式,过期策略对它们处理方式也不尽同。

对于RDB持久化方式来说,生成RDB文件时,Redis默认会自动过滤过期键;而载入RDB文件时,又分两种情况: 以主服务器启动,Redis在载入过程中会过滤过期键,以从服务器启动,不论键是否过期,都会被载入到数据库。 不过,因为主从服务器在进行数据同步的时候,从服务器的数据库就会被清空,所以一般来讲,过期键对载入RDB文件的从服务器也不会造成影响。

对于AOF、复制来说,Redis处理方式基本一样,当删除一个过期键之后,显示追加一条DEL命令到AOF文件中, 同时也会显式地向所有从服务器发送一个DEL命令,告知从服务器删除这个过期键。从服务器只有在接到主服务器发来的DEL命令之后,才会删除过期键。

参考

Redis设计与实现

Redis Document