跟我一起来学习Redis中的数据库(下)

249 阅读7分钟

这是我参与8月更文挑战的第10天,活动详情查看:8月更文挑战

上文简单的介绍了Redis中的数据库

一、数据库键空间操作及特性

  Redis是一个键值对(key-value pair)数据库服务器,服务器中的每个数据库都由一个redis.h/redisDb结构表示,其中,redisDb结构的dict字典保存了数据库中的所有键值对,我们将这个字典称为键空间(key space):

typedef struct redisDb {
    //....
    // 数据库键空间,保存着数据库中的所有键值对
    dict *dict;
    // ....
} redisDb;

键空间和用户所见的数据库是直接对应的:
1、键空间的键也就是数据库的键,每个键都是一个字符串对象。
2、键空间的值也就是数据库的值,每个值可以是字符串对象、列表对象、哈希表对象、集合对象和有序集合对象中的任意一种Redis对象。

image.png

数据库键空间包含列表对象、哈希对象、字符串对象例子

1、添加新键

  • 添加一个新键值对到数据库,实际上就是将一个新键值对添加到键空间字典里面,其中键为字符串对象,而值则为任意一种类型的Redis对象。 2、删除键
  • 删除数据库中的一个键,实际上就是在键空间里面删除键所对应的键值对对象。 3、更新键
  • 对一个数据库进行更新,实际上就是对键空间里面键所对应的值对象进行更新,根据值对象的类型不同,更新的具体方法也会有所不同。 3、对键取值
  • 对一个数据库进行取值,实际上就是在键空间中取出键所对应的值对象,根据值对象的类型不同,具体的取值方法也会有所不同。   除了上述操作之外,还有很多针对数据库本身的Redis命令,也是通过对键空间进行处理来完成的。比如清空整个数据库的FLUSHDB命令,就是通过删除键空间中所有键值对数量来实现的;用于随机返回数据库中某个键的RANDOMKEY命令,就是通过在键空间中随机返回一个键来实现的。类似的命令还有EXISTS、RENAME、KEYS等。

1.1 读写键空间时的维护操作

  当使用Redis命令对数据库进行读写时,服务器不仅会对键空间执行指定的读写操作,还会执行一些额外的维护操作,其中包含以下:
1、在读取一个键之后(读操作和写操作都要对键进行读取),服务器会根据键是否存在来更新服务器的键空间命中(hit)次数或键空间不命中(miss)次数,这两个值可以在INFO stats命令的keyspace_hits属性和keyspace_misses属性中查看。
2、在读取一个键之后,服务器会更新键的LRU(最后一次使用)时间,这个值可以用于计算键的闲置时间,使用OBJECT idletime <key> 命令可以查看键key的闲置时间。
3、如果服务器在读取一个键时发现该键已经过期,那么服务器会先删除这个过期键,然后才执行余下的其他操作。

二、设置键的生存时间或过期时间

  通过EXPIRE命令或者PEXPIRE命令,客户端可以以秒或者毫秒精度为数据库中的某个键设置生存时间(Time To Live,TTL),在经过指定的秒数或者毫秒数之后,服务器就会自动删除生存时间为0的键。
  客户端也可以通过EXPIREAT命令或PEXPIREAT命令,以秒或者毫秒精度给数据库中的某个键设置过期时间(expire time),过期时间是一个UNIX时间戳,当键的过期时间来临时,服务器就自动删除。
  TTL命令和PTTL命令接受一个带有生存时间或者过期时间的键,返回这个键的剩余生存时间。

2.1 设置过期时间

  Redis有四个不同的命令可以用于设置键的生存时间(键可以存多久)或过期时间:

  • EXPIRE <key> <ttl> 命令用于将键key的生存时间设置ttl秒。
  • PEXPIRE <key> <ttl>命令用于将键key的生存时间设置ttl毫秒。
  • EXPIREAT <key> <timestamp> 命令用于将键key的过期时间设置为timesamp所指定的秒数时间戳。
  • PEXPIREAT <key> <timestamp> 命令用于将键key的过期时间设置为timesamp所指定的毫秒数时间戳。 以上所有命令最后都会被转换PEXPIREAT命令来实现。(有点多态的意思)

image.png

设置生存时间和设置过期时间的命令之间的转换

2.2 保存过期时间

  redisDb结构的expires字典保存了数据库中所有键的过期时间,我们称这个字典为过期字典:

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

以下是PEXPIREAT命令的伪代码定义:

def PERPIREAT(key, expire_time_in_ms):
    # 如果给定的键不存在于键空间,那么不能设置过期时间
    if key not in redisDb.dict
        return 0
    # 在过期字典中关联键和过期时间
    redisDb.expires[key] = expire_time_in_ms
    # 过期时间设置成功
        return 1

2.3 移除过期时间

  PERSIST命令可以移除一个键的过期时间。

以下是PERSIST命令的伪代码定义

def PERSIST(key):
    # 如果给定的键不存在,或者键没有设置过期时间,那么直接返回
    if key not in redisDb.expires:
        return 0
    # 移除过期字典中给定键的键值对关联
    redisDb.expires.remove(key)
    # 过期时间移除成功
        return 1

2.4 计算并返回剩余生存时间

  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.5 过期键的判定

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

  • 检查给定键是否存在于过期字典,如果存在,那么取得键的过期时间。
  • 检查当前UNIX时间戳是否大于键的过期时间:如果是的话,那么键已经过期;否则的话,键未过期。

三、过期键删除策略

  一个键过期了,它什么时候会被删除呢?

这个问题有三种可能的答案,它们分别代表了三种不同的删除策略:

  • 定时删除:在设置键的过期时间的同时,创建一个定时器(timer),让定时器在键的过期时间来临时,立即执行对键的删除操作。
  • 惰性删除:放任键过期不管,但是每次从键空间中获取键时,都检查取得的键是否过期,如果过期的话,就删除该键;如果没有过期,就返回该键。
  • 定期删除:每隔一段时间,程序就对数据库进行一次检查,删除里面的过期键。至于删除多少过期键,以及要检查多少个数据库,则由算法决定。