`

数据库实现

阅读更多
        Redis 服务器将其所有的数据库都保存在 redisServer 结构的 db 数组中,db 数组中的每项都是一个 redisDb 结构,代表一个数据库。而在服务器内部,客户端当前的目标数据库则都保存在 redisClient 结构的 db 属性中。这三个结构的关键定义如下。
struct redisServer{
    /* ... */
    redisDb *db;        // 用于保存服务器中的所有数据库的数组
    int dbnum;          // 服务器的数据库数量
    /* ... */
};

typedef struct redisClient{
    /* ... */
    redisDb *db;       // 记录客户端当前正在使用的数据库
    /* ... */
} redisClient;

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

        服务器初始化时,会根据 redisServer 结构的 dbnum 属性来决定应该创建多少个数据库,该属性值默认为 16,可通过 database 配置选项来修改。
        每个 Redis 客户端都有自己的目标数据库,以作为客户端执行命令的操作对象。redisClient 结构的 db 属性指针指向 redisServer.db 数组的其中一个元素,被指向的元素就是客户端的目标数据库。默认的 Redis 客户端的目标数据库为 0 号数据库,可以通过 SELECT 命令来切换目标数据库(实则就是修改了 redisClient.db 指针)。要注意的是,在处理多数据库程序时,目前 Redis 并没有可以返回客户端目标数据库的命令,因此在数次切换数据库之后,可能会忘记自己当前所处的数据库,所以为了避免误操作,在执行类似 FLUSHDB 这样危险的命令之前,最好显示地执行一个 SELECT 命令,然后才执行别的命令。
        对于 Redis 中的每个数据库,都是使用一个 redisDb 结构来表示的,其中的 dict 字典指针保存了对应数据库中的所有键值对,该字典也被称为键空间(key space)。键空间和用户所见的数据库是直接对应的:
        (1)键空间的键就是数据库的键,每个键都是一个字符串对象。
        (2)键空间的值就是数据库的值,每个值可以是字符串对象、列表对象、哈希表对象、集合对象和有序集合对象中的任意一种(见Redis 五种对象)。
        因为数据库的键空间是一个字典,所以所有针对数据库的操作,如添加或者删除一个键值对、清空整个数据库的 FLUSHDB 命令、随机返回数据库中某个键的 RANDOMKEY 命令和返回数据库键数量的 DBSIZE 命令等,实际上都是通过对键空间字典进行操作来实现的。
        redisDb 结构的 expires 字典保存了数据库中所有键的过期时间,被称为过期字典。过期字典的键是一个指针,指向键空间中的某个键对象(也就是某个数据库键),过期字典的值是一个 long long 类型的整数,保存了对应键的过期时间——一个毫秒精度的 UNIX 时间戳(PEXPIREAT 命令的执行结果,其他三个用于设置过期时间和生存时间的 EXPIRE、PEXPIRE 和 EXPIREAT 命令,实际上都是使用 PEXPIREAT 命令来实现的。键的过期时间可以通过 PERSIST 命令来移除,而键的剩余生存时间则可以通过 TTL 或者 PTTL 命令来返回)。
        当对数据库进行读写时,服务器不仅会对键空间执行指定的操作,还会执行以下一些额外的维护操作。
        (1)在读取一个键(写操作也要对键进行读取)后,服务器会根据键是否存在来更新服务器的键空间命中(hit)次数或不命中(miss)次数,这两个值可以在 INFO stats 命令的 keyspace_hits 属性和 keyspace_misses 属性中查看。
        (2)在读取一个键后,服务器会更新键的 LRU(最后一次使用)时间,该值可以用于计算键的闲置时间,使用 OBJECT idletime <key> 命令可以查看键 key 的闲置时间。
        (3)如果服务器在读取一个键时发现其已过期,则服务器会先删除该键,然后再执行余下的其他操作。
        (4)如果有客户端使用 WATCH 命令监视了某个键,则服务器在对被监视的键进行修改后,会将其标记为脏(dirty),从而让事务程序注意到这个键已经被修改过。
        (5)服务器每次修改一个键后,都会将脏键计数器的值加 1,这个计数器会触发服务器的持久化以及复制操作。
        (6)如果服务器开启了数据库通知功能,则在对键进行修改后,服务器将按配置发送相应的数据库通知。
        在对过期键进行删除时,有三种不同的删除策略,其中第一种和第三种为主动删除策略,第二种为被动删除策略。
        (1)定时删除:在设置键的过期时间时,创建一个定时器(timer),让定时器在键的过期时间来临时,立即执行删除操作。这种方式对内存是最友好的,但是对 CPU 时间却是最不友好的。一方面,使用定时器可以保证过期键会尽可能地被删除,并释放其所占用的内存。但另一方面,在过期键较多时,删除过期键这一行为可能会占用相当一部分 CPU 时间,这在内存不紧张但 CPU 时间非常紧张(如有大量的命令请求等待服务器处理)的情况下,无疑会对服务器的响应时间和吞吐量造成影响。
        (2)惰性删除:放任键过期不管,但在每次从键空间中获取键时,都检查取得的键是否过期,如果是的话就删除该键,否则就返回该键。这种方式对 CPU 时间来说是最友好的,但对内存却是最不友好的。它虽然可以保证删除过期键的操作只会在非做不可的情况下进行,并且删除的目标仅限于当前处理的键,但如果数据库中有大量的过期键,而这些过期键又恰好没有被访问到的话,那么则可能永远也不会被删除(除非用户手动执行 FLUSHDB 命令),这种情况甚至可以被看作是一种内存泄漏。比如,对于一些和时间有关的数据,如日志,在某个时间点后,对它们的访问就会大大减少,甚至不再访问。如果这类过期数据大量地积压在数据库中,而它们的键所占用的内存也没有释放,那么造成的后果肯定是非常严重的。
        (3)定期删除:每隔一段时间,程序就对数据库进行一次检查,以删除里面的过期键。至于要删除多少过期键,以及要检查多少数据库,则由算法决定。毫无疑问,这种方式是定时删除和惰性删除的一种整合和折中:它通过限制删除操作执行的时长和频率来减少了定时删除操作对 CPU 时间的影响,而通过定期删除过期键,则有效地减少了惰性删除因为过期键而可能带来的内存浪费。但定期删除策略的难点是确定删除操作执行的时长和频率,如果配置不当,则容易使其退化成定时删除或惰性删除策略。因此采用这种策略时,必须要根据服务器的使用情况来合理的设置删除操作的执行时长和频率。
        在 Redis 中,服务器实际使用的是惰性删除和定期删除两种策略,通过配合使用这两种策略,服务器可以很好地在合理使用 CPU 时间和避免浪费内存之间取得平衡。Redis 在使用定期策略时,它会分多次依次遍历服务器中的各个数据库,从各数据库的 expires 字典中随机检查一部分键的过期时间,并删除其中的过期键。
        过期键也会影响到 Redis 服务器中的 RDB 持久化功能、AOF 持久化功能以及复制功能。
        对于 RDB 文件的生成,在执行 SAVE 或者 BGSAVE 命令创建一个新的 RDB 文件时,程序会检查数据库中的键,已过期的键不会保存到新创建的 RDB 文件中。而对于 RDB 文件的载入,如果启动 Redis 服务器时开启了 RDB 功能,则服务器将按照下列两种模式进行载入:
        (1)如果服务器以主服务器模式运行,那么程序会对载入的 RDB 文件中保存的键进行检查,未过期的键会被载入到数据库中,而过期键则会被忽略。因此过期键对载入 RDB 文件的主服务器不会造成影响。
        (2)如果服务器以从服务器模式运行,那么不论载入的 RDB 文件中保存的键过期与否,都会被载入到数据库中。不过由于主服务器在进行数据同步时会清空从服务器数据库,所以一般过期键也不会影响载入 RDB 文件的从服务器。
        对于 AOF 文件的写入,当服务器以 AOF 持久化模式运行时,如果数据库中的某个键已经过期,但还没有被惰性删除或者定期删除,则 AOF 文件不会因为这个过期键而产生任何影响。但当过期键被删除后,程序将会向 AOF 文件追加一条 DEL 命令,来显示地记录该键已被删除。比如,如果客户端试图使用“GET message”命令访问过期的 message 键,则服务器将执行以下三个动作:
        (1)从数据库中删除 message 键。
        (2)追加一条“DEL message”命令到 AOF 文件。
        (3)向该客户端返回空回复。
        对于 AOF 文件的重写类似于 RDB 文件的生成,程序会检查数据库中的键,已过期的键不会被保存到重写后的 AOF 文件中。
        在复制方面,当服务器运行在复制模式下时,从服务器的过期键删除动作完全由主服务器控制:
        (1)主服务器在删除一个过期键后,会显示地向所有从服务器发送一个 DEL 命令,告知从服务器删除这个过期键。
        (2)从服务器在执行客户端发送的读命令时,即使碰到过期键也不会将其删除,而是像处理未过期的键一样来进行处理。从服务器只有在接到主服务器发来的 DEL 命令后,才会删除过期键。
        通过由主服务器来控制从服务器统一地删除过期键,可以保证主从服务器数据的一致性。
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics