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

Redis单机数据库服务器数据库布局解析

2021-05-295.4k 阅读

Redis 单机数据库基础概念

Redis 是一个开源的,基于内存的数据结构存储系统,它可以用作数据库、缓存和消息中间件。在单机环境下,Redis 服务器以一种高效且独特的方式组织和管理数据,这种组织方式被称为数据库布局。理解 Redis 的数据库布局对于开发人员优化数据存储和访问,提升应用性能至关重要。

Redis 单机数据库的结构特点

Redis 单机数据库采用了一种简单的键值对(Key - Value)存储模型。每个键都是唯一的,通过键可以快速定位到对应的值。值得注意的是,Redis 中的值并非仅仅是简单的字符串,它支持多种数据结构,如字符串(String)、哈希(Hash)、列表(List)、集合(Set)和有序集合(Sorted Set)等。这使得 Redis 在存储和处理不同类型的数据时具有很高的灵活性。

例如,使用字符串数据结构存储用户信息:

import redis

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

# 设置键值对
r.set('user:1:name', 'John')
r.set('user:1:age', '30')

# 获取值
name = r.get('user:1:name')
age = r.get('user:1:age')

print(name.decode('utf - 8'))
print(age.decode('utf - 8'))

在上述 Python 代码中,通过 redis - py 库连接到本地 Redis 服务器,并使用 set 方法设置字符串类型的键值对,使用 get 方法获取对应的值。

数据库编号与选择

Redis 单机服务器默认提供 16 个数据库,编号从 0 到 15 。客户端可以通过 SELECT 命令选择要操作的数据库。不同数据库之间的数据是相互隔离的,这在一些场景下可以方便地进行数据的逻辑分组。例如,在开发测试环境中,可以将测试数据存放在不同的数据库编号中,避免与生产数据混淆。

以下是使用 Redis 命令行工具选择数据库的示例:

redis - cli
127.0.0.1:6379> SELECT 2
OK
127.0.0.1:6379[2]> SET test_key test_value
OK
127.0.0.1:6379[2]> GET test_key
"test_value"

在这个示例中,首先通过 redis - cli 进入 Redis 命令行界面,然后使用 SELECT 2 选择了编号为 2 的数据库,并在该数据库中设置和获取了一个键值对。

字符串类型数据的存储布局

字符串(String)是 Redis 中最基本的数据类型。在 Redis 内部,字符串的存储布局相对简单,但也有一些值得深入探讨的细节。

简单动态字符串(SDS)

Redis 使用简单动态字符串(Simple Dynamic String,SDS)来存储字符串值。SDS 是 Redis 自定义的一种数据结构,与传统的 C 语言字符串相比,它具有以下优势:

  1. 获取长度的时间复杂度:C 语言字符串获取长度需要遍历整个字符串,时间复杂度为 O(n),而 SDS 通过在结构中记录长度信息,获取长度的时间复杂度为 O(1)。
  2. 内存分配:SDS 在进行字符串修改操作时,会预分配一定的空间,减少频繁的内存分配和释放,提高性能。

SDS 的结构定义大致如下(简化版):

struct sdshdr {
    int len;        // 已使用的长度
    int free;       // 未使用的长度
    char buf[];     // 存储字符串内容的数组
};

例如,当我们执行 SET key "hello" 命令时,Redis 会创建一个 SDS 结构来存储 "hello" 这个字符串,len 为 5,free 根据预分配策略可能为 0 或者其他值(如果有预分配)。

整数的优化存储

当 Redis 存储的字符串值是整数时,Redis 会进行优化存储。如果该整数在一定范围内(例如 64 位系统下,范围为 -9223372036854775808 到 9223372036854775807),Redis 会直接将该整数存储在字符串对象中,而不是像普通字符串一样以字符数组的形式存储。这样做可以节省内存空间,并且在进行一些数值操作(如 INCRDECR)时,无需进行字符串到数值的转换,提高了操作效率。

以下是一个使用 INCR 命令对存储的整数进行自增的示例:

redis - cli
127.0.0.1:6379> SET counter 10
OK
127.0.0.1:6379> INCR counter
(integer) 11
127.0.0.1:6379> GET counter
"11"

在这个示例中,先设置了一个整数值 10,然后使用 INCR 命令进行自增操作,Redis 能够高效地处理这种整数操作。

哈希类型数据的存储布局

哈希(Hash)类型在 Redis 中用于存储字段和值的映射关系,类似于编程语言中的字典(Dictionary)。哈希类型适用于存储对象的属性,例如用户的多个属性(姓名、年龄、地址等)可以存储在一个哈希对象中。

哈希对象的内部结构

Redis 哈希对象在内部有两种不同的存储方式,取决于哈希对象的大小和字段数量。

  1. 压缩列表(ziplist):当哈希对象的字段数量较少且每个字段和值的长度较短时,Redis 会使用压缩列表来存储哈希对象。压缩列表是一种紧凑的、连续内存的数据结构,它将多个元素紧凑地存储在一起,减少内存碎片。每个元素由一个头信息和实际数据组成,头信息记录了前一个元素的长度和当前元素的长度等信息,以便在遍历和插入删除元素时能够高效操作。
  2. 哈希表(hashtable):当哈希对象的字段数量较多或者字段和值的长度较长时,Redis 会使用哈希表来存储哈希对象。哈希表是一种基于哈希算法的数据结构,通过哈希函数将键映射到一个桶(bucket)中,然后在桶中通过链表解决哈希冲突。这种结构能够提供快速的查找、插入和删除操作,时间复杂度平均为 O(1)。

以下是使用 Python 操作 Redis 哈希类型的示例:

import redis

r = redis.Redis(host='localhost', port=6379, db=0)

# 设置哈希字段
r.hset('user:1', 'name', 'John')
r.hset('user:1', 'age', '30')
r.hset('user:1', 'address', '123 Main St')

# 获取哈希所有字段和值
user_info = r.hgetall('user:1')

for key, value in user_info.items():
    print(key.decode('utf - 8'), value.decode('utf - 8'))

在上述代码中,使用 hset 方法设置哈希对象 user:1 的字段和值,使用 hgetall 方法获取所有字段和值。

哈希表的扩展与收缩

随着哈希对象中元素的增加或减少,Redis 的哈希表会进行动态的扩展和收缩。当哈希表的负载因子(已使用的桶数量与总桶数量的比值)超过一定阈值(默认是 1)时,哈希表会进行扩展,即增加桶的数量,重新计算元素的哈希值并将元素重新分配到新的桶中。相反,当负载因子低于一定阈值(默认是 0.1)时,哈希表会进行收缩,减少桶的数量,同样重新分配元素。这种动态调整机制确保了哈希表在不同数据量下都能保持较好的性能。

列表类型数据的存储布局

列表(List)类型在 Redis 中用于存储一个有序的字符串元素集合。列表非常适合实现消息队列、最新消息的展示等场景。

列表对象的编码方式

Redis 列表对象同样有两种编码方式:

  1. 压缩列表(ziplist):与哈希对象类似,当列表对象包含的元素数量较少且每个元素的长度较短时,Redis 会使用压缩列表来存储列表。这种方式通过紧凑的内存布局节省空间,并且在遍历列表时能够高效地获取元素。
  2. 双向链表(linkedlist):当列表对象的元素数量较多或者元素长度较长时,Redis 会使用双向链表来存储列表。双向链表允许在 O(1) 的时间复杂度内进行头部和尾部的插入和删除操作,非常适合实现队列和栈的操作。

以下是使用 Redis 命令行操作列表类型的示例:

redis - cli
127.0.0.1:6379> RPUSH mylist "apple" "banana" "cherry"
(integer) 3
127.0.0.1:6379> LRANGE mylist 0 -1
1) "apple"
2) "banana"
3) "cherry"
127.0.0.1:6379> LPOP mylist
"apple"
127.0.0.1:6379> LRANGE mylist 0 -1
1) "banana"
2) "cherry"

在这个示例中,首先使用 RPUSH 命令从列表尾部插入元素,然后使用 LRANGE 命令获取列表所有元素,最后使用 LPOP 命令从列表头部删除元素。

快速列表(quicklist)

在 Redis 3.2 版本之后,引入了快速列表(quicklist)。快速列表结合了双向链表和压缩列表的优点,它是一个双向链表,每个节点是一个压缩列表。这样既可以在需要时高效地进行头部和尾部的操作,又可以在存储大量小元素时节省内存空间。快速列表的节点大小可以通过配置参数进行调整,以适应不同的应用场景。

集合类型数据的存储布局

集合(Set)类型在 Redis 中用于存储一组无序的、唯一的字符串元素。集合类型常用于实现标签系统、去重等功能。

集合对象的编码方式

Redis 集合对象有两种编码方式:

  1. 整数集合(intset):当集合中的所有元素都是整数且元素数量较少时,Redis 会使用整数集合来存储集合。整数集合是一种紧凑的、有序的存储结构,它根据元素的类型动态调整自身的存储类型(例如,当所有元素都在 int16_t 范围内时,使用 int16_t 类型存储,随着元素值范围的增大,会动态升级为 int32_tint64_t),以节省内存空间。
  2. 哈希表(hashtable):当集合中的元素不是整数或者元素数量较多时,Redis 会使用哈希表来存储集合。哈希表通过哈希函数将元素映射到桶中,利用哈希表的特性保证元素的唯一性和快速的查找、插入和删除操作。

以下是使用 Python 操作 Redis 集合类型的示例:

import redis

r = redis.Redis(host='localhost', port=6379, db=0)

# 添加元素到集合
r.sadd('fruits', 'apple', 'banana', 'cherry')

# 获取集合所有元素
fruits = r.smembers('fruits')

for fruit in fruits:
    print(fruit.decode('utf - 8'))

在上述代码中,使用 sadd 方法向集合 fruits 中添加元素,使用 smembers 方法获取集合所有元素。

集合的交集、并集和差集运算

Redis 提供了丰富的命令来进行集合之间的交集、并集和差集运算。这些运算基于集合的存储结构实现,利用哈希表或整数集合的特性能够高效地完成。例如,计算两个集合的交集时,如果两个集合都使用哈希表存储,Redis 可以通过遍历其中一个哈希表,在另一个哈希表中查找对应的元素,快速得出交集结果。

以下是使用 Redis 命令行进行集合运算的示例:

redis - cli
127.0.0.1:6379> SADD set1 "a" "b" "c"
(integer) 3
127.0.0.1:6379> SADD set2 "b" "c" "d"
(integer) 3
127.0.0.1:6379> SINTER set1 set2
1) "b"
2) "c"
127.0.0.1:6379> SUNION set1 set2
1) "a"
2) "b"
3) "c"
4) "d"
127.0.0.1:6379> SDIFF set1 set2
1) "a"

在这个示例中,分别使用 SADD 命令创建了两个集合 set1set2,然后使用 SINTER 命令计算交集,SUNION 命令计算并集,SDIFF 命令计算差集。

有序集合类型数据的存储布局

有序集合(Sorted Set)类型在 Redis 中用于存储一组有序的、唯一的字符串元素,每个元素都关联一个分数(score),通过分数来进行排序。有序集合常用于排行榜等场景。

有序集合对象的内部结构

Redis 有序集合对象由两种数据结构组成:

  1. 跳跃表(skiplist):跳跃表是一种有序的数据结构,它通过在每个节点上增加多层指针,使得在查找元素时可以跳过一些节点,提高查找效率。跳跃表的平均查找时间复杂度为 O(log n),与平衡树类似,但实现相对简单。在有序集合中,跳跃表用于按照分数对元素进行排序,方便进行范围查询等操作。
  2. 哈希表(hashtable):哈希表用于存储元素到分数的映射关系,这样可以在 O(1) 的时间复杂度内根据元素获取其分数,同时保证元素的唯一性。

以下是使用 Redis 命令行操作有序集合类型的示例:

redis - cli
127.0.0.1:6379> ZADD scores 85 "Alice" 90 "Bob" 95 "Charlie"
(integer) 3
127.0.0.1:6379> ZRANGE scores 0 -1 WITHSCORES
1) "Alice"
2) "85"
3) "Bob"
4) "90"
5) "Charlie"
6) "95"
127.0.0.1:6379> ZRANK scores "Bob"
(integer) 1

在这个示例中,使用 ZADD 命令向有序集合 scores 中添加元素和对应的分数,使用 ZRANGE 命令获取有序集合所有元素及其分数,使用 ZRANK 命令获取指定元素的排名。

跳跃表的构建与操作

跳跃表在插入元素时,会随机生成节点的层数,层数越高的节点在跳跃表中出现的概率越低。这样可以保证跳跃表在插入和删除元素时,仍然能够保持较好的性能。在进行范围查询时,跳跃表可以根据分数范围快速定位到对应的节点,然后通过链表指针遍历范围内的元素。例如,要获取分数在 90 到 100 之间的元素,跳跃表可以快速定位到分数为 90 的节点,然后依次遍历后续节点,直到分数超过 100 为止。

键空间与过期策略

在 Redis 单机数据库中,键空间(Keyspace)是存储所有键值对的地方。同时,Redis 支持为键设置过期时间,当键过期时,Redis 会采用一定的策略来处理这些过期键。

键空间的结构

键空间本质上是一个哈希表,其中键是 Redis 中的键,值是一个包含了值对象(如字符串、哈希等)和其他元数据(如过期时间、访问时间等)的结构体。通过这种哈希表结构,Redis 能够快速地根据键找到对应的键值对。

过期策略

Redis 采用了两种过期策略:

  1. 定期删除:Redis 会定期随机抽取一些设置了过期时间的键进行检查,如果发现键已过期,则删除该键。这种策略不会占用太多 CPU 时间,但可能会导致一些过期键不能及时被删除。
  2. 惰性删除:当客户端访问一个键时,如果发现该键已过期,则删除该键并返回空值。这种策略只有在访问过期键时才会进行删除操作,不会主动占用 CPU 资源,但可能会导致过期键在内存中停留一段时间。

为了平衡内存占用和 CPU 负载,Redis 结合了定期删除和惰性删除两种策略,尽可能在保证性能的同时,及时清理过期键,释放内存空间。

以下是使用 Python 设置键的过期时间的示例:

import redis

r = redis.Redis(host='localhost', port=6379, db=0)

# 设置键值对并设置过期时间为 60 秒
r.setex('temp_key', 60, 'temp_value')

在上述代码中,使用 setex 方法设置了一个键值对,并设置该键的过期时间为 60 秒。

内存管理与优化

Redis 作为基于内存的数据库,内存管理对于其性能和稳定性至关重要。

Redis 的内存分配器

Redis 默认使用 jemalloc 作为内存分配器。jemalloc 是一个高效的内存分配库,它针对多线程环境进行了优化,能够减少内存碎片,提高内存分配和释放的效率。在 Redis 中,不同的数据结构(如 SDS、哈希表、跳跃表等)都依赖于 jemalloc 来分配和管理内存。

内存优化策略

  1. 数据结构选择:根据实际应用场景,选择合适的数据结构可以显著减少内存占用。例如,对于存储少量字段的对象,使用哈希类型的压缩列表编码方式比使用哈希表编码方式更节省内存。
  2. 键值对设计:合理设计键值对的命名和结构,避免过长的键名和不必要的冗余数据。同时,尽量将相关的数据存储在同一个数据结构中,减少键的数量,从而降低键空间的内存占用。
  3. 过期策略优化:合理设置键的过期时间,及时释放不再使用的内存。通过调整定期删除和惰性删除的参数,平衡 CPU 负载和内存释放的及时性。

例如,在存储大量用户信息时,如果每个用户只有少量的属性(如姓名、年龄),可以考虑使用哈希类型,并确保哈希对象的大小在压缩列表的适用范围内,以节省内存。

持久化机制对数据库布局的影响

Redis 提供了两种持久化机制:RDB(Redis Database)和 AOF(Append - Only File),这两种机制会对 Redis 单机数据库的布局产生一定的影响。

RDB 持久化

RDB 持久化是将 Redis 在内存中的数据以快照的形式保存到磁盘上。在进行 RDB 持久化时,Redis 会遍历当前数据库的键空间,将所有的键值对按照一定的格式写入到 RDB 文件中。对于不同的数据结构,会采用不同的方式进行序列化。例如,对于哈希对象,会将字段和值依次序列化;对于列表对象,会将元素依次序列化。

当 Redis 启动时,如果存在 RDB 文件,会读取 RDB 文件并将其中的数据重新加载到内存中,恢复数据库的布局。RDB 持久化的优点是恢复速度快,因为它是直接将内存数据的快照加载到内存中。缺点是可能会丢失最近一次持久化之后的数据,因为 RDB 持久化是定期进行的。

AOF 持久化

AOF 持久化是将 Redis 执行的写命令以追加的方式记录到 AOF 文件中。当 Redis 启动时,会重新执行 AOF 文件中的命令,从而重建数据库布局。AOF 文件记录的是命令的执行顺序,而不是数据的最终状态。例如,如果对一个哈希对象进行多次 HSET 操作,AOF 文件会记录每一次 HSET 命令。

AOF 持久化的优点是数据的完整性更高,因为它几乎可以记录每一次写操作。缺点是 AOF 文件可能会变得很大,并且在恢复数据时需要重新执行所有的命令,速度相对较慢。为了解决 AOF 文件过大的问题,Redis 提供了 AOF 重写机制,它会根据当前数据库的状态生成一个优化后的 AOF 文件,减少文件大小。

在实际应用中,通常会结合 RDB 和 AOF 两种持久化机制,利用 RDB 的快速恢复和 AOF 的数据完整性优势,确保 Redis 单机数据库的数据安全和高效恢复。

通过深入了解 Redis 单机数据库的布局,包括数据结构的存储方式、键空间管理、内存优化和持久化机制等方面,开发人员可以更好地利用 Redis 的特性,优化应用程序的性能,提高系统的稳定性和可靠性。无论是在缓存、数据存储还是消息队列等场景中,对 Redis 数据库布局的理解都是至关重要的。在实际开发中,需要根据具体的业务需求,灵活运用 Redis 的各种功能,选择合适的配置和策略,以充分发挥 Redis 的优势。