文章目录

  • Redis 实现原理简介
  • 对象系统
  • 对象系统
  • 对象关系图
  • 对象的实现
  • 对象的实现
  • 数据结构
  • 对象与数据结构之间的关系
  • 底层数据结构介绍
  • 压缩列表
  • 整数集合
  • SDS
  • 双向链表
  • 字典
  • 跳跃表
  • 数据库实现
  • Redis 数据库的实现方法
  • 数据库示例
  • 记录过期时间
  • 带有过期时间的数据库示例
  • 持久化实现
  • RDB 持久化实现原理
  • AOF 持久化实现原理
  • AOF 文件重写的实现原理
  • 命令处理模型
  • 命令处理模型
  • 复习
  • 本节重点
  • 了解更多关于 Redis 的底层实现知识


Redis 实现原理简介

单机服务器的内部构造及原理介 绍

对象系统

Redis 数据的构建方法

对象系统

Redis 是一个键值对(key-value pair)数据库,数据库中的每个键都有与之对应的值,并且这些键和值都是对象(object),其中:

  • 键总是一个字符串对象(String Object)。
  • 而值则可以是字符串对象、列表对象(List Object)、散列对象(Hash Object)、集合对象(Set Object)或者有序集合对象(Sorted Set Object / Zset Object)中的任意一个,这五种对象分别对应我们在 Redis 数据库中操作的字符串键值对(SET、BITOP、PFADD)、列表键值对(RPUSH、LRANGE)、散列键值对(HADD、HLEN)、集合键值对(SADD、SMEMBERS)和有序集合键值对(ZADD、ZRANGE)。

举个例子,执行命令 SET msg “hello world” 会让数据库关联起一个新的键值对,其中键是一个包含内容 “msg” 的字符串对象,而值则是一个包含内容 “hello world” 的字符串对象;

另一方面,执行命令 SADD fruits “apple” “banana” “cherry” 则会创建一个键为字符串对象,值为集 合对象的键值对。

对象关系图

redission 原理 redis工作原理详解_redis

对象的实现

Redis 实现不同类型对象的方法

对象的实现

Redis 中的每种对象都有与之相对应的底层数据结构,并且为了让对象在各种不同的应用场景下都有优
秀的性能表现,Redis 为每种对象都提供了两种类型的数据结构实现:

  • 第一类是为了进行性能优化而特制的编码数据结构(encoded data structure),这些数据结构主要使用“以 CPU 换内存的方式”来节约内存。编码数据结构主要在对象包含的值数量比较少、或者值的体积比较小时使用:比如在字符串对象包含的字符串比较短时,集合只包含三五个小元素 时,又或者列表只包含十来个 项时,这些情况都可以使用编码数据结构。
  • 另一类是普通数据结构,也即是我们在书上、论文上看见的数据结构,比如双向链表、字典、跳跃表等等。比起编码数据结构,这些普通的数据结构需要消耗更多内存,但是能 够提供更强大的功能,普通数据结构主要在对象包含的值比较多、或者值的体积比较大时使用。

在创建新对象时,Redis 会优先使用编码数据结构来表示对象,并在有需要时,自动将对象的表示方式转换为普通数据结构。

数据结构

编码数据结构:

  • 压缩列表(zip list)
  • 整数集合(int set)

普通数据结构:

  • 简单动态字符串(SDS, simple dynamic string)
  • 双向链表 - 字典
  • 跳跃表

另外需要注意的是,节约内存并不是编码数据结构所特有的 —— 普通数据结构也有相应的内存优化模式,比如 SDS 就有两种或以上的表示方式,不同的表示可以 让同一对象在不同的使用场景下也尽可能地保持高效。

对象与数据结构之间的关系

redission 原理 redis工作原理详解_redission 原理_02

底层数据结构介绍

压缩列表、整数集合、SDS、双向列表、字典、跳跃表

压缩列表

压缩列表具有以下特点:

  • 压缩列表包含的项都是有序的,列表的两端分 别为表头和表尾。
  • 每个项可以储存一个字符串、整数或者浮点数。
  • 可以从表头开始或者从表尾开始遍 历整个压缩列表,复杂度为 O(N) 。
  • 定位压缩列表中指定索引上的项,复杂度为 O(N) 。
  • 使用压缩列表来储存值消耗的内存比使用双向 链表来储存值消耗的内存要少。
整数集合

整数集合具有以下特点:

  • 集合元素只能是整数(最大 为 64 位),并且集合中不会出 现重复的元素。
  • 集合的底层使用有序的整数数组来表示。
  • 数组的类型会随着新添加元素的 类型而改变:举个例子,如果集合中位长度最大的元素可以使用16 位整数来保存,那么数 组的类型就是
    int16_t ,而如果集合中位长度最大的元素可以使用 32 位整数来保存的话,那么数组的类型就是 int32_t ,诸如此类。
  • 数组的类型只会自动增大,但不会减小。
SDS

Redis 使用 SDS (simple dynamic string)而不是 C 语言的字符串格式(以空字符 为结尾的字符数组)来作为 Redis 的默认字符串表示,SDS 具有以下特点:

  • 可以储存位数组(实现 BITOP 和 HyperLogLog)、字符串、整数和浮点数,其中超 过 64 位的整数和超过 IEEE 754 标准的浮点数使用字符串来表示。
  • 具有 int 、 embstr 和 raw 三种表示形式可选,其中 int 表示用于储存小于等于 64 位的整数,embstr 用来储存比较短的位数组和字符串,而其他格式的 值则由 raw 格式储存。
  • 比起 C 语言的字符串格式, SDS 具有以下四个优点: 1)常数复杂度获取长度值; 2)不会引起缓冲区溢出; 3)通过预分配和惰性释放两种策略来减少内存重分配的 执行次数; 4)可以储存二进制位。
双向链表

Redis 的双向链表实现具有以下特性:

  • 双向、无环、带有表头和表尾指针。
  • 一个链表包含多个项,每个项都是一个字符串对象,换句话来说,一个列表对象可以包含多个字符串对象。
  • 可以从表头或者表尾遍历整个链表,复杂度为 O(N) 。
  • 定位特定索引上的项,复杂度为 O(N) 。
  • 链表带有长度记录属性,获取链表的当前长度的复杂度为 O(1) 。
字典

Redis 的字典实现具有以下特性:

  • 查找、添加、删除键值对的复杂度为 O(1) ,键和值都是字符串对象。
  • 使用散列表(hash table)为底层实现,使用链地址法(separate chaining)来解决键冲突。
  • Redis 会在不同的地方使用不同的散列算法,其中最常用的是 MurmurHash2 算法。
  • 在键值对数量大增或者大减的时候会对散列表进行重新散列(rehash),并且这个 rehash 是渐进式、分多次进行的,不会在短时间内耗费大量 CPU 时间,㐀成服务器阻塞。
跳跃表

Redis 的跳跃表实现具有以下特点:

  • 支持平均 O(log N) 最坏 O(N) 复杂度的节点查找操作,并且可以通过执行范围性(range)操作来批量地获取有序的节点。
  • 跳跃表节点除了实现跳跃表所需的层(level)之外,还具有 score 属性和 obj 属性:前者是一个浮点数,用于记录成员的分值;而后者则是一个字符串对象,用来记录成员本身。
  • 和字典一起构成 ZSET 结构,用于实现 Redis 的有序集合结构:其中字典用于快㏿ 获取元素的分值(比如实现 ZSCORE 命令),以及判断元素是否存在;而跳跃表则用于执行范围操作(比如实现 ZRANGE 命令)。

数据库实现

Redis 储存键值对的方式

Redis 数据库的实现方法

在 Redis 里面,每个数据库都是一个字典,该字典的键和值都是我们之前提到的对象,其中:

  • 字典的键总是一个字符串对象,它储存了用户为键设置的键名。
  • 字典的值则可以是字符串对象、列表对象、散列对象、集合对象或者有序集合对象的其中一个。

因为数据库就是字典,所以针对数据库的操作都是基于字典操作来 实现的:

  • 比如说,使用 DEL 命令删除一个数据库键,就是删除数据库对应的字典的键值对。
  • 又比如说,使用 FLUSHDB 清空数据库,就是清空数据库对应的字典。
  • 诸如此类。

数据库示例

redission 原理 redis工作原理详解_数据结构_03

记录过期时间

为了记录数据库键的过期时间,Redis 为每个数据库创建了另一个字典,专门使用这个字典来记录键的 过期时间,其中:

  • 字典的键指向数据库键对象,也即是带有过期时间的那个键(数据库字典和储存过期时间的字典通过指针使用同一个键对象,不会㐀成任何资源浪费)。
  • 键的值则是一个毫秒格式的 UNIX 时间戳,记录了键到期的时间。
带有过期时间的数据库示例

redission 原理 redis工作原理详解_redission 原理_04

持久化实现

RDB 持久化和 AOF 持久化的实现原理

RDB 持久化实现原理

Redis 会遍历服务器中的所有数据库,访问数据库中的所有键值对,并根据键值对的类型,将这些键值对以及它们的过期时间写入到 RDB 文件里面。

redission 原理 redis工作原理详解_数据库_05

AOF 持久化实现原理

AOF 持久化功能在每次执行命令之后就将协议格式的命令写入到 AOF 缓冲区,然后服务器再定期将缓冲区的内容写入到 AOF 文件,还原数据时只要重新执行 AOF 文件里面的命令即可。

redission 原理 redis工作原理详解_redis_06

AOF 文件重写的实现原理

无须对现有的 AOF 文件进行处理,直接根据数据库目前的状态来生成新的 AOF 文件。

redission 原理 redis工作原理详解_数据结构_07


SELECT

RPUSH alphabet …

HMSET book …

SET message …

命令处理模型

Redis 处理命令请求的方法与模式

命令处理模型

Redis 服务器使用 Reactor 模式来连接多个客户端并处理命令请求,其中:

  • 客户端发送的命令请求会被放到一个有序的 队列里面。
  • 服务器使用单线程方式来执行命令 —— 服务器每次从队列里面取出一个请求并处理它,只有在当前的命令请求处理完毕之后,服务器才会去处理下一个命令请求。
  • 单线程的命令处理方式使得针对服务器以及数据库的操作都不需要加锁,好处是极大地方便了功能的实现,减少了代码出错的可能性;而坏处则是不能最大化地使用硬件的多 线程能力。

复习

本节重点

Redis 数据库中的键和值都是对象,其中键总是一个字符串对象,而值则可以是多种类型对象的其中一个。

Redis 为每种对象都设置了两种或以上的表示方式,使得 对象可以在不同的应用场景中都有最好的性能表现。

Redis 的数据库就是一个字典,数据库操作都是通过字典操作来实现的;除此之外,Redis 还使用了另一个字典来专门记录键值对的过期时间。

RDB 持久化通过遍历数据库并将键值对写入到文件来实现,而 AOF 持久化则通过记录服务器执行过的命令请求来实现。

Redis 服务器使用 Reactor 模式来处理客户端的命令请求。

了解更多关于 Redis 的底层实现知识

因为 Redis 的底层实现本身是一个庞大的课题,所以这一节课只是简单地介绍了 Redis 服务器关于单机数据库方面的实现知 识。

至于Redis 服务器提供的其他功能(比如事 务、发布与订阅、Lua 脚本)以及 Redis 提供的多机功能(比如复制、 Sentinel 和集群)的介绍则没有涉及。