浅谈Redis之数据类型、过期删除、持久化

这里对Redis的数据类型、过期删除、持久化等方面进行介绍

abstract.png

数据类型

String 字符串

Redis的object encoding命令可以返回Redis对象所使用的编码。进而可帮助我们了解Redis某种数据类型的内部实现方式。具体地,对于String字符串类型的数据内部的实现方式有以下几种情况

  • 值为整数,且可以用long类型来表示,则该值可以直接用long表示。相应object encoding命令的编码值为int
  • 值为字符串,且长度大于指定长度,则该值会使用简单动态字符串SDS表示。相应object encoding命令的编码值为raw
  • 值为字符串,且长度不大于指定长度,则该值会使用embstr编码的简单动态字符串SDS进行表示。相应object encoding命令的编码值为embstr

测试结果,如下所示

figure 1.jpeg

List 列表

在Redis 3.2之前对于List类型的数据而言,其内部有两种实现方式

  • 列表中元素数量小于一定数量 且 各元素字符串的长度均小于一定长度,其内部会使用linkedlist双向链表进行实现
  • 对于不满足上述条件的情形,其内部会使用ziplist压缩链表进行实现

而从Redis 3.2开始,对于List类型的数据则统一通过quickList进行实现,测试结果,如下所示

figure 2.jpeg

Hash 哈希

对于Hash哈希类型的数据内部的实现方式有以下几种情况

  • 哈希表中键值对小于一定数量 且 哈希表中所有键、值的字符串长度均小于一定长度,其内部会使用ziplist压缩链表进行实现
  • 对于不满足上述条件的情形,其内部会使用dict字典进行实现。相应object encoding命令的编码值为hashtable

测试结果,如下所示

figure 3.jpeg

Set 集合

对于Set集合类型的数据内部的实现方式有以下几种情况

  • 集合中元素小于一定数量 且 所有元素均为整数,其内部会使用intset整数集合进行实现。其中intset整数集合本质上是整型数组
  • 对于不满足上述条件的情形,其内部会使用dict字典进行实现。相应object encoding命令的编码值为hashtable

测试结果,如下所示

figure 4.jpeg

Zset 有序集合

对于Zset集合类型的数据内部的实现方式有以下几种情况

  • 集合中元素小于一定数量 且 各元素字符串的长度均小于一定长度,其内部会使用ziplist压缩列表进行实现
  • 对于不满足上述条件的情形,其内部会使用skiplist跳表进行实现

测试结果,如下所示

figure 5.jpeg

值得一提的是当通过skiplist跳表实现Zset时,其内部还会同时使用dict字典。以便在根据元素查询分数时,可以实现常数时间复杂度;而无需通过遍历跳表来获取

内部数据结构

SDS 简单动态字符串

在C中只能通过字符数组来表示字符串,此举显然无法满足Redis对字符串在功能、效率、安全等方面的需求。故其内部设计了一种名为SDS简单动态字符串的数据结构以用于存储字符串。简单来说相比较于C字符串而言,SDS具有以下几个方面的优点

  1. 避免缓冲区溢出
  2. 减少修改字符串带来的内存重分配次数。一方面,通过预分配内存空间来应对SDS的字符串的未来增长;另一方面,通过内存空间的惰性释放应对SDS的字符串的缩短操作
  3. 二进制安全:在C的字符数组中,对于空字符(即\0)会视为字符串的结尾。故其不能保存类似于图像、音频等的二进制数据。而在SDS由于采用了单独的属性字段表示SDS的字符串长度,故使得其可以保存二进制数据
  4. 由于SDS采用了单独的属性字段表示SDS的字符串长度,故可以以常数时间复杂度获取SDS的字符串长度

linkedlist 双向链表

Redis的双向链表存在如下特点

  1. 内置了head表头、tail表尾字段,可以快速定位到链表的表头、表尾
  2. 表头所指向的节点的前驱指针、表尾所指向的节点的后继指针 均指向 NULL,故在该双向链表中是不存在环的
  3. 内置了链表长度计数器字段,故可以以常数时间复杂度获取链表中元素的数量

dict 字典

在Redis的dict字典是通过哈希表(注意,此处的哈希表不是指Redis对外所提供的Hash哈希数据类型)作为底层实现的。其中每个字典内部实际上是带有两个哈希表的,第二个哈希表仅在rehash重哈希时用于临时存储键值对。值得一提的是,rehash重哈希过程并不是一次性、集中式地完成,而是分多次、渐进式地完成。以避免键值对数量过多、庞大的计算量对Redis 服务端性能造成影响。这样在rehash重哈希完成之前,如果对字典进行查询、删除等操作,就必须同时使用两个哈希表。例如对于查询操作而言,当第一个哈希表中不存在相应键时,就需要去第二个哈希表进行查找。而对于哈希冲突,则采用链地址法。这样同一个索引下的多个键值对会形成一个单向链表

skiplist 跳表

Redis中的skiplist跳表是Zset有序集合的实现方式之一。不同于一般的跳表,为了方便Zset计算元素排名Rank的需求。其在跳表节点的每一层中提供了一个跨度的属性(即level[i].span属性),以用于表示在当前层中从前一个跳表节点到当前跳表节点所经过的节点数。这样在查找某个元素的过程中,只需将沿途访问过的所有层的跨度累计起来。就可以得到目标元素在跳表中的排名

ziplist 压缩列表

ziplist压缩列表是Redis为了节约内存而开发的由连续内存组成的顺序型数据结构。由于列表中各节点所使用的内存是连续分配的,故在列表头部的内存空间只需记录表尾距离列表的偏移量即可确定表尾的位置;同理,在每个列表节点中只需记录前一个节点所使用的字节数,即可快速定位的前一个节点。换言之,压缩列表也是双向的。同样由于内存分配是连续的,故新增、修改、删除节点会引发列表的连锁更新

过期删除机制

实现原理

客户端无论通过何种命令(EXPIRE、PEXPIRE、EXPIREAT、PEXPIREAT等命令)设置Key的TTL,Redis服务端最终都会将其转化为绝对等到期时间的毫秒级Unix时间戳进行存储,即最终都是通过PEXPIREAT命令实现的。例如当前时间为2022年4月17日零时,那么我们设置Key的生存时间为10天。则该Redis服务端会将2022年4月27日零时所对应的毫秒级Unix时间戳存储、记录下来

至于Redis对过期Key的删除会同时采用两种策略:

  • 惰性删除:每次执行Redis命令前都会对Key进行检查,判断Key是否存在、是否过期。如果Key存在 且 Key已过期,则会删除该Key;如果Key存在 且 未过期,则继续执行命令
  • 定期删除:Redis内部会有一个定时任务,每次执行任务时会在指定时间范围内对一定数量的数据库中随机抽取一定数量的设置了过期时间的Key进行检查。当检查发现某个设置了过期时间的Key已经过期则会进行删除

从Redis 服务端的角度来看,惰性删除对于Redis服务端来说是被动删除策略,定期删除对于Redis服务端来说是主动删除策略。之所以同时采用两种策略。是因为仅仅使用前者的话,一旦Key过期后,如果客户端没有再次访问该Key、发起对该Key的命令,则该Key将永远无法被删除。长久以往会大大浪费Redis的内存空间,严重时甚至可以视为内存泄露。故其还需要定期删除策略对其进行兜底;而仅仅使用后者的话,为了避免定期任务在删除过期Key占用过多的CPU时间,而导致影响Redis服务器对客户端命令的响应时间、吞吐量。故定期任务每次只会删除一定数量的部分过期Key。一旦某个过期Key没有来得及被定期任务删除、而恰好又被客户端再一次访问时,即会导致客户端产生该Key未过期的错觉。故其还需要惰性删除策略对其进行兜底

RDB对过期Key处理策略

1. 生成RDB文件阶段

在通过save、bgsave命令创建一个新的RDB文件时,其会对数据库中的Key进行检查。以保证过期Key不会被保存到新创建的REB文件中

2. 载入RDB文件阶段

在Redis启动过程中如果开启了RDB功能,则其会对RDB文件进行载入。具体地:

  • 如果Redis不是以Slave身份运行,则在载入过程中其会对RDB文件中的Key进行检查。以保证未过期的Key会被载入,而过期Key则会被直接忽略、不会载入到数据库中
  • 如果Redis是以Slave身份运行,则对于RDB文件中的所有Key。无论是否过期,都会被载入到数据库中。但因为Master、Slave在进行同步时,从库会被清空,故一般来说,过期Key对载入RDB文件的Slave而言也不会产生什么影响

AOF对过期KEY处理策略

1. AOF文件追加、写入、同步

当Redis以AOF方式进行持久化时,在新增Key时会向AOF文件追加一条新增命令。当Key过期后通过惰性删除、定期删除机制被删除后,其会再次向AOF追加一条DEL删除命令。即显式地记录该过期Key已被删除

2. AOF重写

在进行AOF重写过程中,其与生成RDB文件类似。同样会对数据库中的Key进行检查。以保证过期Key不会被保存到重写后的AOF文件中

Replication对过期KEY处理策略

当Redis运行在复制模式下,Slave中过期Key的删除是由Master进行控制的。以此保证Master-Slave 数据的一致性。具体地

  1. Master在删除一个过期Key后,会显式向所有Slave发送一个DEL命令。以用于告知Slave删除这个过期Key
  2. Slave在接收到Master发送的DEL命令前,即使Key过期了Slave也不会将其删除。换言之,此时客户端如果访问该过期Key,依然可以正常获取相应的数据,就像这个Key还未过期一样
  3. Slave在接收到Master发送的DEL命令后,Slave才会将相应的过期Key删除

持久化

RDB

1. 创建RDB文件

RDB(Redis DataBase)方式的持久化实际上是将在某个时间点的数据库快照保存在名为dump.rdb的文件当中。可通过save、bgsave命令手动显式地创建RDB文件。区别在于前者会阻塞Redis服务端进程,直到RDB文件创建完成为止。在此之前服务端将无法处理客户端的命令、请求;而后者则会创建一个子进程负责创建RDB文件,此举可以保证服务端能够继续处理客户端的命令、请求

此外Redis支持在服务端配置文件中通过添加save配置项,来设置任意个创建RDB文件的条件。只要其中任意一个条件满足,服务端即可自动执行bgsave命令。配置示例如下所示

1
2
3
4
5
6
# 在900秒内 对数据库进行了至少1次的修改
save 900 1
# 在300秒内 对数据库进行了至少10次的修改
save 300 10
# 在60秒内 对数据库进行了至少10000次的修改
save 60 1000

对于RDB文件,可以通过Linux命令od进行分析。效果如下所示,其中-c、-x分别表示使用ASCII、十六进制格式显示文件内容

figure 6.jpeg

2. 载入RDB文件

RDB文件的载入是在Redis服务端启动时自动完成的,故Redis未提供相应命令。服务端在启动过程中只要检测到RDB文件存在,则其就会自动载入RDB文件。但值得一提的是,由于RDB文件相比较于下面介绍的AOF文件而言,其更新频率低。故当Redis服务端开启了AOF持久化,则服务端会优先使用AOF文件来还原数据库状态。只有AOF持久化在关闭的条件下,服务端才会使用RDB文件来还原数据库

AOF

AOF(Append Of File)方式的持久化则是通过记录Redis服务端执行的写命令来实现的。是否启用AOF持久化可通过设置服务端配置文件的appendonly配置项进行控制,该配置项默认为no,即不启用AOF持久化。如果为yes则表示启用AOF持久化

1. AOF文件追加、写入、同步

在AOF持久化的基本原理,包括三个步骤:追加命令、文件写入、文件同步。当开启AOF持久化后,服务端在执行完一个写命令后,会以指定格式将被执行的命令追加写入Redis的AOF缓冲区。然后将AOF缓冲区写入、同步至AOF文件当中。这里之所以将写入、同步视为两个步骤。是因为现代操作系统为了提高写入效率,把数据从内存写到硬盘分为两个步骤:先将内存数据写入内存缓冲区,待内存缓冲区满了 或 达到时限后,再真正将内存缓冲区的数据写入磁盘。此举虽然提高了效率,但也带来一定的风险。因为如果内存缓冲区的数据还未来得及写入磁盘,计算机发生宕机。即会出现数据丢失的风险

为此Redis服务端的配置文件提供了一个appendfsync参数,用于控制文件写入、文件同步的时机

  • always:将AOF缓冲区的所有内容写入并同步到AOF文件。该配置显然效率最低、但安全性高
  • everysec:将AOF缓冲区的所有内容写入到AOF文件;如果距离上次文件同步超过1秒,则会再次进行同步。其是appendfsync配置项的默认值。因为效率、安全各方面表现比较均衡。即使Redis发生意外宕机,也只会丢失1秒钟的命令数据
  • no:将AOF缓冲区的所有内容写入到AOF文件,但不对AOF文件进行同步。何时同步交由操作系统控制、决定。显然安全性最差

2. AOF重写

随着Redis运行时间越来越长,AOF文件存在文件体积膨胀的问题。比如我们先每次向list添加一个随机数、然后添加100次后,再每次随机从list删除一个数,直到list中只剩一个元素为止。那么AOF文件中为了记录这个list的数据状态,就已经记录、写入了199条命令。而实际上list中却只有一个元素。为此Redis中引入了AOF rewrite重写机制,以重新生成一个新的AOF文件以替代原来较大的AOF文件。重写后的AOF文件由于不存在冗余命令,故文件体积将会大幅缩减

AOF重写机制的实现也很简单,其会直接读取Redis数据库中的键值对来实现,而无需对原AOF文件进行读取、分析操作。例如对于上面的例子,只需 向该list添加最后仅剩的一个元素 这1条命令即可表示。同时为避免在AOF重写过程中阻塞Redis服务端进程、进而无法处理客户端的命令、请求,其会创建一个子进程用于进行AOF重写。以保证服务端能够继续处理客户端的命令、请求

由于在AOF重写期间,Redis服务端接收到客户端新的命令可能会对现有数据库状态进行修改,进而使得Redis最新的数据库状态与重写后的AOF文件不一致。为了解决这一问题,Redis设置了一个AOF重写缓冲区。在AOF重写期间,当服务端进程在执行完客户端发送的写命令后,会分别将该命令追加到AOF缓冲区、AOF重写缓冲区。这样当子进程完成AOF重写工作后,再把AOF重写缓冲区中的内容追加、写入到AOF重写文件中。最后对重写AOF文件进行改名,覆盖替换掉原来的AOF文件。这样整个AOF重写就完成了

3. 载入AOF文件

由于AOF文件中包含了恢复数据库所需的全部写命令,故服务端只需读入并重新执行一遍AOF文件中的写命令即可。值得一提的是,Redis服务端在载入AOF文件时会创建一个不带网络连接的伪客户端,以用于发起调用写命令的请求。当AOF文件载入完毕后,会关闭这个伪客户端

参考文献

  1. Redis设计与实现 黄健宏著
0%