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

Redis对象与持久化机制的集成与优化

2022-04-245.0k 阅读

Redis对象

Redis 是一个基于内存的高性能键值存储数据库,它以一种非常高效的方式管理各种数据类型。在 Redis 中,所有的数据都被组织成对象。理解 Redis 对象对于深入掌握 Redis 的工作原理以及如何优化其性能至关重要。

Redis 对象类型

  1. 字符串对象(String Object) 字符串对象是 Redis 中最基本的数据类型。它可以存储字符串、整数或者浮点数。例如,我们可以使用以下命令来设置和获取一个字符串值:
import redis

r = redis.Redis(host='localhost', port=6379, db = 0)
r.set('name', 'John')
value = r.get('name')
print(value.decode('utf-8'))

在 Redis 内部,字符串对象根据其存储的值的类型和长度,会采用不同的编码方式。如果存储的是长度较小的整数,会采用 int 编码,以节省内存空间。如果是普通字符串,长度小于 39 字节时,会采用 embstr 编码,这种编码方式将对象头和字符串内容存储在一块连续的内存空间,减少内存碎片。而当字符串长度超过 39 字节时,会采用 raw 编码。

  1. 列表对象(List Object) 列表对象是一个有序的字符串元素集合。它常被用于实现消息队列等功能。可以使用 lpushrpop 命令来模拟队列操作。
r.lpush('mylist', 'element1')
r.lpush('mylist', 'element2')
element = r.rpop('mylist')
print(element.decode('utf-8'))

Redis 的列表对象在底层有两种编码方式:ziplistlinkedlist。当列表中的元素数量较少且每个元素的长度较短时,会采用 ziplist 编码,它将所有元素紧凑地存储在一块连续的内存空间中,节省内存。而当列表元素数量较多或者元素长度较大时,会转换为 linkedlist 编码,以提高操作效率。

  1. 哈希对象(Hash Object) 哈希对象是一个键值对集合,类似于 Python 中的字典。常用于存储对象的多个属性。
r.hset('user:1', 'name', 'Alice')
r.hset('user:1', 'age', 25)
name = r.hget('user:1', 'name')
print(name.decode('utf-8'))

哈希对象同样有两种编码方式:ziplisthashtable。当哈希对象中的键值对数量较少且键和值的长度都较短时,采用 ziplist 编码。否则,会使用 hashtable 编码,hashtable 编码在查找和插入操作上具有较高的效率。

  1. 集合对象(Set Object) 集合对象是一个无序的字符串元素集合,并且集合中的元素是唯一的。常用于实现去重和交集、并集等集合运算。
r.sadd('myset', 'item1')
r.sadd('myset', 'item2')
members = r.smembers('myset')
for member in members:
    print(member.decode('utf-8'))

集合对象有两种编码方式:intsethashtable。当集合中的元素都是整数且数量较少时,采用 intset 编码,它以有序且紧凑的方式存储整数。当集合元素不满足 intset 的条件时,会使用 hashtable 编码。

  1. 有序集合对象(Sorted Set Object) 有序集合对象类似于集合对象,但每个元素都关联一个分数(score),通过分数来对元素进行排序。常用于排行榜等应用场景。
r.zadd('leaderboard', {'player1': 100, 'player2': 200})
sorted_members = r.zrange('leaderboard', 0, -1, withscores=True)
for member, score in sorted_members:
    print(member.decode('utf-8'), score)

有序集合对象有两种编码方式:ziplistskiplist。当有序集合中的元素数量较少且每个元素的长度较短时,采用 ziplist 编码。当元素数量较多或者元素长度较大时,会使用 skiplist 编码,skiplist 是一种可以在 $O(log n)$ 时间复杂度内完成插入、删除和查找操作的数据结构。

Redis 对象结构

Redis 对象在内存中有着特定的结构。每个 Redis 对象都由一个 redisObject 结构体表示,其定义如下(简化版 C 代码):

typedef struct redisObject {
    unsigned type:4;
    unsigned encoding:4;
    unsigned lru:LRU_BITS;
    int refcount;
    void *ptr;
} robj;
  1. type 字段:表示对象的类型,如字符串、列表、哈希等。通过 type 字段,Redis 可以在执行命令时快速判断对象类型是否与命令匹配。例如,如果执行 hget 命令(用于获取哈希对象中的字段值),Redis 会首先检查对象的 type 是否为哈希类型,如果不是,则返回错误。
  2. encoding 字段:表示对象的编码方式,如前文所述,不同类型的对象在不同条件下会采用不同的编码。通过 encoding 字段,Redis 可以根据具体的编码方式来选择合适的操作函数。例如,对于采用 ziplist 编码的列表对象,其插入、删除操作的函数与采用 linkedlist 编码的列表对象的操作函数是不同的。
  3. lru 字段:用于记录对象的最后一次访问时间,主要用于实现内存淘汰策略。当 Redis 内存不足且启用了某种内存淘汰策略(如 volatile - lru,从设置了过期时间的键中选择最近最少使用的键进行淘汰)时,会根据对象的 lru 字段来判断哪些对象应该被淘汰。
  4. refcount 字段:用于记录对象的引用计数。当一个对象的引用计数为 0 时,意味着没有任何地方引用该对象,Redis 会自动释放该对象所占用的内存。例如,当我们使用 del 命令删除一个键时,该键所对应的 Redis 对象的引用计数会减 1,当引用计数变为 0 时,对象内存被释放。
  5. ptr 字段:指向对象实际的数据存储位置。对于字符串对象,ptr 可能指向 embstr 编码的字符串内存地址,或者 raw 编码的字符串内存地址。对于列表对象,ptr 可能指向 ziplist 或者 linkedlist 的内存地址。

Redis 持久化机制

Redis 作为内存数据库,为了保证数据的可靠性和恢复能力,提供了两种持久化机制:RDB(Redis Database)和 AOF(Append - Only File)。

RDB 持久化

  1. 原理 RDB 持久化是将 Redis 在某一时刻的内存数据快照以二进制的形式保存到磁盘上。当 Redis 启动时,可以通过加载这个快照文件来恢复数据。RDB 持久化的触发方式有两种:手动触发和自动触发。
    • 手动触发:可以使用 SAVE 或者 BGSAVE 命令。SAVE 命令会阻塞 Redis 服务器进程,直到 RDB 文件生成完毕,在这个过程中,Redis 无法处理其他客户端的请求。而 BGSAVE 命令会 fork 一个子进程来进行 RDB 文件的生成,主进程继续处理客户端请求,因此不会阻塞服务器。
    • 自动触发:可以在 Redis 配置文件中设置自动触发的条件。例如,通过配置 save 900 1 表示在 900 秒内如果至少有 1 个键发生了变化,就自动触发 BGSAVE 操作。
  2. RDB 文件结构 RDB 文件包含了一些元数据和实际的数据部分。元数据部分记录了 RDB 文件的版本号等信息。实际数据部分则是按照对象类型依次存储每个键值对。对于字符串对象,会存储其编码方式、长度和内容。对于列表、哈希等复杂对象,会按照其内部结构进行序列化存储。例如,对于采用 ziplist 编码的列表对象,会将 ziplist 的内容完整地写入 RDB 文件。
  3. 优点
    • 恢复速度快:因为 RDB 文件是一个完整的内存快照,在恢复数据时,Redis 只需将 RDB 文件中的数据一次性加载到内存中,相比 AOF 日志重放,速度更快。这使得在一些对恢复速度要求较高的场景下,如缓存重建,RDB 是一个很好的选择。
    • 文件体积小:RDB 文件采用二进制格式存储,并且对数据进行了一定的压缩,相比于 AOF 文件,在存储相同数据的情况下,RDB 文件体积更小,这对于存储设备的空间占用较少,也有利于数据的传输和备份。
  4. 缺点
    • 数据可能丢失:由于 RDB 是按照一定的时间间隔进行快照的,如果在两次快照之间 Redis 发生故障,那么这期间的数据变化将会丢失。例如,设置了 save 900 1,但在第 899 秒时 Redis 崩溃,那么这 899 秒内的数据变化都不会包含在 RDB 文件中。
    • fork 操作开销大BGSAVE 操作需要 fork 一个子进程,在 fork 过程中,操作系统需要为子进程复制父进程的内存页表等资源,这个过程会消耗一定的 CPU 和内存资源。如果 Redis 服务器内存较大,fork 操作的开销会更加明显,可能会对服务器性能产生短暂的影响。

AOF 持久化

  1. 原理 AOF 持久化是将 Redis 执行的写命令以日志的形式追加到文件末尾。当 Redis 启动时,会重新执行 AOF 文件中的命令来恢复数据。AOF 日志的写入方式可以通过配置 appendfsync 参数来控制,有三种可选值:
    • always:每次执行写命令后,都立即将命令写入 AOF 文件并同步到磁盘。这种方式保证了数据的最高安全性,即使 Redis 发生故障,最多只会丢失一条命令的数据。但由于每次写操作都要进行磁盘 I/O,性能开销较大。
    • everysec:每秒将 AOF 缓冲区中的命令写入 AOF 文件并同步到磁盘。这种方式在性能和数据安全性之间取得了较好的平衡,是默认的配置。在大多数情况下,即使 Redis 发生故障,最多只会丢失一秒内的数据。
    • no:由操作系统决定何时将 AOF 缓冲区中的数据写入磁盘。这种方式性能最高,但数据安全性最差,在 Redis 发生故障时,可能会丢失大量的数据。
  2. AOF 文件结构 AOF 文件是一个文本文件,每行记录一条 Redis 写命令。例如,执行 SET key value 命令后,AOF 文件中会追加一行 *3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue\r\n。这里采用了 Redis 协议的格式进行记录,*3 表示命令由 3 个参数组成,$3 表示第一个参数 SET 的长度为 3 个字符,以此类推。
  3. 优点
    • 数据安全性高:通过合理配置 appendfsync 参数,可以在保证一定性能的前提下,最大限度地减少数据丢失。例如,设置 appendfsync everysec,在大多数情况下,只会丢失一秒内的数据,相比 RDB 持久化,数据丢失的风险大大降低。
    • 可读性好:AOF 文件是文本格式,便于人工查看和分析。如果需要对 Redis 中的数据进行调试或者审计,可以直接查看 AOF 文件中的命令记录。
  4. 缺点
    • 文件体积大:由于 AOF 文件记录的是每条写命令,而不是内存数据的快照,随着时间的推移和写操作的增加,AOF 文件会越来越大。这不仅会占用大量的磁盘空间,还会影响数据恢复的速度,因为在恢复数据时需要重新执行 AOF 文件中的所有命令。
    • 恢复速度慢:相比 RDB 直接加载内存快照,AOF 在恢复数据时需要重新执行大量的写命令,这个过程相对较慢。特别是当 AOF 文件非常大时,恢复时间会显著增加。

Redis 对象与持久化机制的集成

  1. RDB 中的对象处理 在生成 RDB 文件时,Redis 会遍历内存中的所有对象,并按照对象的类型和编码方式进行序列化存储。例如,对于字符串对象,会根据其编码是 intembstr 还是 raw,采用不同的方式写入 RDB 文件。如果是 int 编码,会直接将整数值写入文件。对于采用 ziplist 编码的列表对象,会将 ziplist 的结构和元素依次写入文件。在加载 RDB 文件时,Redis 会根据文件中的数据恢复出相应的对象,包括对象的类型、编码和数据内容。
  2. AOF 中的对象处理 AOF 文件记录的是对对象的操作命令。当执行一个写命令,如 SET key value 时,这个命令会被追加到 AOF 文件中。在恢复数据时,Redis 会按照 AOF 文件中的命令顺序依次执行,从而重新构建出内存中的对象。对于复杂对象,如哈希对象的多次 hset 操作,AOF 文件会完整记录每次 hset 命令,在恢复时也会按照记录顺序执行,保证哈希对象的状态与故障前一致。

Redis 对象与持久化机制的优化

  1. 对象优化
    • 合理选择数据类型和编码:根据实际应用场景,选择最合适的数据类型和编码方式。例如,如果存储的是少量的整数集合,可以使用集合对象的 intset 编码,以节省内存。避免不必要的编码转换,因为编码转换可能会带来额外的性能开销。
    • 减少对象的创建和销毁:频繁地创建和销毁 Redis 对象会消耗内存和 CPU 资源。可以通过复用对象来减少这种开销。例如,在使用列表对象实现消息队列时,可以尽量避免每次处理消息都创建新的列表对象,而是复用已有的列表对象。
  2. RDB 优化
    • 调整触发策略:根据应用对数据丢失的容忍程度和服务器性能,合理调整 RDB 自动触发的条件。如果对数据丢失比较敏感,可以缩短自动触发的时间间隔,但要注意这可能会增加 fork 操作的频率,对服务器性能产生一定影响。
    • 优化 fork 性能:为了减少 fork 操作对服务器性能的影响,可以在 Redis 服务器内存使用方面进行优化,如尽量避免 Redis 服务器内存使用达到物理内存上限,减少内存交换。还可以在操作系统层面调整 swappiness 参数,降低内存交换的频率,从而优化 fork 操作的性能。
  3. AOF 优化
    • 合理配置 appendfsync:根据应用对数据安全性和性能的要求,选择合适的 appendfsync 值。如果应用对性能要求较高且能容忍一定的数据丢失,可以选择 everysecno。如果对数据安全性要求极高,选择 always 时要充分考虑其性能开销。
    • AOF 重写:随着 AOF 文件的不断增大,可以使用 AOF 重写机制来压缩 AOF 文件。AOF 重写会在不影响 Redis 正常工作的情况下,对 AOF 文件进行整理,将多条命令合并为一条,去除冗余命令。例如,对于多次对同一个键的 SET 操作,重写后只会保留最后一次 SET 操作的命令。可以通过手动执行 BGREWRITEAOF 命令或者配置自动重写条件(如 auto - aof - rewrite - percent 100auto - aof - rewrite - min - size 64mb,表示当 AOF 文件大小超过上次重写后文件大小的 100% 且文件大小超过 64MB 时,自动触发 AOF 重写)来进行 AOF 文件的优化。

通过对 Redis 对象和持久化机制的深入理解,并进行合理的优化,可以使 Redis 在保证数据可靠性的同时,提供更高的性能和更好的资源利用率。无论是在小型应用还是大规模分布式系统中,这些优化措施都能为系统的稳定运行和高效性能提供有力支持。