MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Redis数据库键空间的存储架构探秘

2024-05-224.3k 阅读

Redis 键空间基础概念

Redis 作为一款高性能的键值对数据库,其核心在于键空间(Key Space)的设计与管理。键空间是 Redis 数据库存储所有键值对的逻辑空间,每个 Redis 数据库都有一个对应的键空间。

键的类型与命名规则

Redis 的键本质上是字符串类型。虽然理论上键可以是任意二进制安全的字符串,长度上限为 512MB,但在实际应用中,为了便于管理和提高可读性,建议遵循一定的命名规范。例如,采用分层命名方式,以冒号(:)分隔不同层级的信息。比如 user:1:name,其中 user 表示这是与用户相关的键,1 是用户 ID,name 表示该键存储的是用户的名字。这样的命名方式使得键空间的结构更加清晰,易于维护。

值的类型多样性

Redis 支持多种数据类型作为值,包括字符串(String)、哈希(Hash)、列表(List)、集合(Set)和有序集合(Sorted Set)。这种丰富的数据类型使得 Redis 能够满足不同场景下的需求。例如,在缓存用户信息场景中,可以使用哈希类型存储用户的多个属性;在实现消息队列时,列表类型就非常适用。

下面通过 Python 代码示例展示如何操作不同类型的值:

import redis

# 连接 Redis 服务器
r = redis.Redis(host='localhost', port=6379, db=0)

# 设置字符串值
r.set('string_key', 'Hello, Redis!')
print(r.get('string_key'))

# 设置哈希值
r.hset('hash_key', 'field1', 'value1')
r.hset('hash_key', 'field2', 'value2')
print(r.hgetall('hash_key'))

# 设置列表值
r.rpush('list_key', 'element1')
r.rpush('list_key', 'element2')
print(r.lrange('list_key', 0, -1))

# 设置集合值
r.sadd('set_key', 'item1')
r.sadd('set_key', 'item2')
print(r.smembers('set_key'))

# 设置有序集合值
r.zadd('sorted_set_key', {'member1': 1,'member2': 2})
print(r.zrange('sorted_set_key', 0, -1, withscores=True))

这段代码通过 redis - py 库连接到本地 Redis 服务器,并分别操作了不同类型的值。

Redis 键空间的数据结构实现

Redis 键空间的高效运行依赖于底层精心设计的数据结构。

字典结构存储键值对

Redis 内部使用字典(dict)结构来存储键空间中的所有键值对。这个字典是 Redis 键空间的核心数据结构,它提供了快速的键查找和插入操作。字典结构在实现上采用了哈希表,通过对键进行哈希计算,快速定位到相应的存储位置。

在 Redis 中,字典结构由 dict 结构体和 dictEntry 结构体组成。dict 结构体包含了哈希表数组、哈希表大小、已使用的哈希表节点数量等信息。dictEntry 结构体则表示哈希表中的一个节点,存储了键值对的具体信息,包括键、值以及指向下一个节点的指针(用于解决哈希冲突)。

以下是简化的 C 语言代码示例,展示字典结构的基本组成:

// 定义 dictEntry 结构体
typedef struct dictEntry {
    void *key;
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    struct dictEntry *next;
} dictEntry;

// 定义 dict 结构体
typedef struct dict {
    dictType *type;
    void *privdata;
    dictht ht[2];
    long rehashidx;
    int iterators;
} dict;

// 定义哈希表结构体
typedef struct dictht {
    dictEntry **table;
    unsigned long size;
    unsigned long sizemask;
    unsigned long used;
} dictht;

虽然 Redis 实际代码更加复杂,但这段简化代码有助于理解其基本结构。

哈希表的扩展与收缩

随着键值对的不断插入和删除,哈希表需要动态调整大小以保持高效性能。当哈希表的负载因子(已使用节点数与哈希表大小的比值)过高(默认达到 1.5)时,Redis 会进行扩展操作,将哈希表大小翻倍,并重新计算所有键值对的哈希值,将其重新分配到新的哈希表中。

相反,当负载因子过低(默认小于 0.1)时,Redis 会进行收缩操作,减小哈希表的大小,释放不必要的内存空间。

在 Redis 中,扩展和收缩操作并不是一次性完成的,而是采用渐进式 rehash 的方式。这样可以避免在操作过程中阻塞主线程,保证 Redis 的高性能。渐进式 rehash 通过 rehashidx 字段来记录当前 rehash 的进度,每次执行写操作或部分读操作时,会逐步将一部分键值对从旧哈希表迁移到新哈希表。

键空间的过期策略

Redis 支持为键设置过期时间,这在很多场景下非常有用,比如缓存数据的自动清理。

过期时间的设置与存储

在 Redis 中,可以使用 EXPIRE 命令为键设置过期时间(以秒为单位),或者使用 PEXPIRE 命令设置过期时间(以毫秒为单位)。当为一个键设置过期时间后,Redis 会在内部维护一个过期字典,这个字典与键空间字典是分离的。过期字典的键与键空间字典的键是相同的,而值则是该键的过期时间戳。

以下是通过命令行设置键过期时间的示例:

redis-cli set mykey "Hello"
redis-cli expire mykey 60  # 设置 mykey 在 60 秒后过期

过期策略实现

Redis 采用了两种过期策略:定期删除和惰性删除。

定期删除:Redis 会定期随机抽取一些键检查是否过期,并删除过期的键。这个定期操作的频率是可以配置的,通过 hz 参数来设置,默认值为 10,表示每秒执行 10 次过期检查。每次检查的键数量是有限的,这样可以避免过多占用 CPU 资源。

惰性删除:当客户端访问一个键时,Redis 会先检查该键是否过期。如果过期,则删除该键,并返回 nil。这种方式可以保证只有被访问的过期键才会被删除,不会主动消耗额外的资源去扫描所有键。

这两种策略相结合,既保证了过期键能及时被删除,又避免了过多的性能开销。

键空间的持久化机制

Redis 提供了两种持久化机制:RDB(Redis Database)和 AOF(Append - Only File),用于将键空间的数据保存到磁盘,以便在重启后恢复数据。

RDB 持久化

RDB 持久化是将 Redis 在某一时刻的键空间数据以快照的形式保存到磁盘上。这个快照文件是一个紧凑的二进制文件,恢复时可以快速加载到内存中。

在 Redis 中,可以通过配置文件中的 save 配置项来设置触发 RDB 持久化的条件。例如,save 900 1 表示如果在 900 秒内至少有 1 个键被修改,就触发一次 RDB 持久化。也可以通过 BGSAVE 命令手动触发 RDB 持久化,该命令会在后台 fork 一个子进程来执行持久化操作,不会阻塞主线程。

RDB 持久化的优点是恢复速度快,因为它是直接将二进制快照文件加载到内存中。缺点是可能会丢失最后一次持久化之后的数据,因为两次持久化之间的数据修改不会实时保存到磁盘。

AOF 持久化

AOF 持久化是将 Redis 执行的写命令以追加的方式保存到 AOF 文件中。当 Redis 重启时,会重新执行 AOF 文件中的命令来恢复键空间的数据。

AOF 持久化的优点是数据完整性更好,因为它记录了每一个写操作。缺点是 AOF 文件可能会变得非常大,因为随着时间推移,会不断追加新的命令。为了解决这个问题,Redis 提供了 AOF 重写机制。AOF 重写会在后台生成一个新的 AOF 文件,这个文件只包含恢复当前键空间数据所需的最少命令。可以通过 BGREWRITEAOF 命令手动触发 AOF 重写,或者通过配置文件中的 auto - aof - rewrite - min - sizeauto - aof - rewrite - percentage 配置项自动触发。

在实际应用中,很多场景会同时开启 RDB 和 AOF 持久化,利用 RDB 的快速恢复和 AOF 的数据完整性优势。

键空间在集群环境下的分布

在 Redis 集群环境中,键空间需要在多个节点之间进行合理分布,以实现高可用性和负载均衡。

哈希槽(Hash Slot)概念

Redis 集群采用哈希槽的方式来分配键空间。Redis 集群有 16384 个哈希槽,每个键通过 CRC16 算法计算出一个 16 位的哈希值,然后对 16384 取模,得到的结果就是该键应该分配到的哈希槽编号。集群中的每个节点负责一部分哈希槽,通过这种方式将键空间均匀地分布在各个节点上。

例如,假设有三个节点 A、B、C,节点 A 负责 0 - 5460 号哈希槽,节点 B 负责 5461 - 10922 号哈希槽,节点 C 负责 10923 - 16383 号哈希槽。当一个键通过哈希计算得到哈希槽编号为 3000 时,该键就会被存储在节点 A 上。

集群节点间的通信与数据迁移

Redis 集群节点之间通过 Gossip 协议进行通信,交换彼此的状态信息,包括节点负责的哈希槽、节点的存活状态等。当集群需要进行数据迁移时,比如增加或删除节点,会通过重新分配哈希槽来实现。

例如,当添加一个新节点 D 时,集群会从其他节点中迁移一部分哈希槽到节点 D 上。迁移过程中,节点会将属于目标哈希槽的键值对发送到新节点,并在本地删除这些键值对。客户端在访问键时,如果键所在的哈希槽已经迁移到其他节点,节点会返回一个 MOVED 错误,告诉客户端应该去哪个节点获取数据。

通过这种方式,Redis 集群能够动态适应节点的变化,保持键空间的合理分布和高可用性。

键空间的优化与调优

为了充分发挥 Redis 键空间的性能,需要进行一些优化和调优操作。

合理使用数据类型

根据实际业务需求选择合适的数据类型非常重要。例如,如果需要存储大量的对象属性,哈希类型比字符串类型更节省内存和提高操作效率。在存储有序数据时,有序集合是更好的选择。通过分析业务场景,选择最优的数据类型可以减少内存占用和提高操作性能。

避免大键

大键(Large Key)指的是占用大量内存空间的键,比如一个非常长的列表或一个包含大量字段的哈希。大键会带来一些性能问题,例如在删除大键时,会阻塞主线程,因为 Redis 是单线程模型,删除操作需要释放大量内存。此外,大键在网络传输时也会占用较多带宽。

为了避免大键,可以将数据进行拆分存储。例如,将一个大的列表拆分成多个小列表,通过合理的命名规则来管理这些小列表。

优化键的命名

虽然 Redis 对键的命名没有严格限制,但遵循良好的命名规范可以提高代码的可读性和维护性。同时,较短的键名在存储和网络传输时也会更高效,因为它们占用的空间更小。

配置参数调优

Redis 提供了许多配置参数,可以根据实际应用场景进行调优。例如,maxmemory 参数用于设置 Redis 实例能够使用的最大内存,当达到这个上限时,可以通过 maxmemory - policy 参数指定内存淘汰策略,如 volatile - lru(在设置了过期时间的键中使用 LRU 算法淘汰键)、allkeys - lru(在所有键中使用 LRU 算法淘汰键)等。

通过合理配置这些参数,可以优化 Redis 键空间的性能,确保其在不同场景下都能高效运行。

综上所述,深入理解 Redis 键空间的存储架构、过期策略、持久化机制以及在集群环境下的分布,对于优化 Redis 应用性能、保证数据完整性和高可用性至关重要。通过合理的使用和调优,可以充分发挥 Redis 作为高性能键值数据库的优势。