Redis原理篇

掌握 Redis 的底层原理,包括单线程工作机制、内存回收、持久化

Redis 为什么这么快?

考虑以下几个问题

  • Redis到底有多快?
  • Redis为什么这么快?
  • Redis为什么是单线程的?
  • 单线程为什么这么快?

Redis到底有多快?

https://redis.io/topics/benchmarks

机器配置

image-20200226231728618

普通命令

image-20200226231245939

Pipeline

image-20200226231323246

根据官方的数据,Redis 的 QPS 可以达到 10 万左右(每秒请求数)。

Redis为什么这么快?

  • 纯内存结构
  • 单线程
  • 多路复用线程模型

内存

​ KV 结构的内存数据库,时间复杂度 O(1)。

单线程

单线程有什么好处呢?
1、没有创建线程、销毁线程带来的消耗

2、避免了上线文切换导致的 CPU 消耗

3、避免了线程之间带来的竞争问题,例如加锁释放锁死锁等等

异步非阻塞

异步非阻塞 I/O,多路复用处理并发连接。

Redis为什么是单线程的?

https://redis.io/topics/faq#redis-is-single-threaded-how-can-i-exploit-multiple-cpu--cores

因为单线程已经够用了,CPU 不是 redis 的瓶颈。Redis 的瓶颈最有可能是机器内存 或者网络带宽。既然单线程容易实现,而且 CPU 不会成为瓶颈,那就顺理成章地采用单 线程的方案了。

单线程为什么这么快?

虚拟存储器(虚拟内存 Vitual Memory)

名词解释:主存:内存;辅存:磁盘(硬盘)

计算机主存(内存)可看作一个由 M 个连续的字节大小的单元组成的数组,每个字 节有一个唯一的地址,这个地址叫做物理地址(PA)。早期的计算机中,如果 CPU 需要 内存,使用物理寻址,直接访问主存储器。

image-20200226232340065

这种方式有几个弊端:

1、在多用户多任务操作系统中,所有的进程共享主存,如果每个进程都独占一块物 理地址空间,主存很快就会被用完。我们希望在不同的时刻,不同的进程可以共用同一 块物理地址空间。

2、如果所有进程都是直接访问物理内存,那么一个进程就可以修改其他进程的内存 数据,导致物理地址空间被破坏,程序运行就会出现异常。

为了解决这些问题,我们就想了一个办法,在 CPU 和主存之间增加一个中间层。CPU 不再使用物理地址访问,而是访问一个虚拟地址,由这个中间层把地址转换成物理地址, 最终获得数据。这个中间层就叫做虚拟存储器(Virtual Memory)。

image-20200226232511628

用户空间和内核空间

为了避免用户进程直接操作内核,保证内核安全,操作系统将虚拟内存划分为两部 分,一部分是内核空间(Kernel-space)/ˈkɜːnl /,一部分是用户空间(User-space)

image-20200301110328173

​ 内核是操作系统的核心,独立于普通的应用程序,可以访问受保护的内存空间,也 有访问底层硬件设备的权限。

​ 内核空间中存放的是内核代码和数据,而进程的用户空间中存放的是用户程序的代码和数据。不管是内核空间还是用户空间,它们都处于虚拟空间中,都是对物理地址的 映射。在 Linux 系统中, 内核进程和用户进程所占的虚拟内存比例是 1:3。

当进程运行在内核空间时就处于内核态,而进程运行在用户空间时则处于用户态。

​ 进程在内核空间以执行任意命令,调用系统的一切资源;在用户空间只能执行简单 的运算,不能直接调用系统资源,必须通过系统接口(又称 system call),才能向内核 发出指令。

top 命令:us 代表 CPU 消耗在 User space 的时间百分比; sy 代表 CPU 消耗在 Kernel space 的时间百分比。

进程切换(上下文切换)

​ 多任务操作系统是怎么实现运行远大于 CPU 数量的任务个数的?当然,这些任务实 际上并不是真的在同时运行,而是因为系统通过时间片分片算法,在很短的时间内,将 CPU 轮流分配给它们,造成多任务同时运行的错觉。

​ 为了控制进程的执行,内核必须有能力挂起正在 CPU 上运行的进程,并恢复以前挂 起的某个进程的执行。这种行为被称为进程切换。

什么叫上下文?

​ 在每个任务运行前,CPU 都需要知道任务从哪里加载、又从哪里开始运行,也就是 说,需要系统事先帮它设置好 CPU 寄存器和程序计数器(ProgramCounter),这个叫做 CPU 的上下文。

​ 而这些保存下来的上下文,会存储在系统内核中,并在任务重新调度执行时再次加 载进来。这样就能保证任务原来的状态不受影响,让任务看起来还是连续运行。

在切换上下文的时候,需要完成一系列的工作,这是一个很消耗资源的操作。

进程的阻塞

正在运行的进程由于提出系统服务请求(如 I/O 操作),但因为某种原因未得到操 作系统的立即响应,该进程只能把自己变成阻塞状态,等待相应的事件出现后才被唤醒。 进程在阻塞状态不占用 CPU 资源。

文件描述符 FD

Linux 系统将所有设备都当作文件来处理,而 Linux 用文件描述符来标识每个文件 对象。

文件描述符(File Descriptor)是内核为了高效管理已被打开的文件所创建的索引, 用于指向被打开的文件,所有执行 I/O 操作的系统调用都通过文件描述符;文件描述符 是一个简单的非负整数,用以表明每个被进程打开的文件。

Linux 系统里面有三个标准文件描述符。 0:标准输入(键盘);1:标准输出(显示器);2:标准错误输出(显示器)。

传统 I/O 数据拷贝

当应用程序执行 read 系统调用读取文件描述符(FD)的时候,如果这块数据已经 存在于用户进程的页内存中,就直接从内存中读取数据。如果数据不存在,则先将数据 从磁盘加载数据到内核缓冲区中,再从内核缓冲区拷贝到用户进程的页内存中。(两次 拷贝,两次 user 和 kernel 的上下文切换)。

image-20200301110742887

Blocking I/O

当使用 read 或 write 对某个文件描述符进行过读写时,如果当前 FD 不可读,系统 就不会对其他的操作做出响应。从设备复制数据到内核缓冲区是阻塞的,从内核缓冲区 拷贝到用户空间,也是阻塞的,直到 copy complete,内核返回结果,用户进程才解除 block 的状态。

为了解决阻塞的问题,我们有几个思路。

1、在服务端创建多个线程或者使用线程池,但是在高并发的情况下需要的线程会很 多,系统无法承受,而且创建和释放线程都需要消耗资源。

2、由请求方定期轮询,在数据准备完毕后再从内核缓存缓冲区复制数据到用户空间 (非阻塞式 I/O),这种方式会存在一定的延迟。

能不能用一个线程处理多个客户端请求?

I/O 多路复用(I/O Multiplexing)

I/O 指的是网络 I/O。

多路指的是多个 TCP 连接(Socket 或 Channel)。

复用指的是复用一个或多个线程。

它的基本原理就是不再由应用程序自己监视连接,而是由内核替应用程序监视文件描述符。

客户端在操作的时候,会产生具有不同事件类型的 socket。在服务端,I/O 多路复 用程序(I/O Multiplexing Module)会把消息放入队列中,然后通过文件事件分派器(File event Dispatcher),转发到不同的事件处理器中。

image-20200301110943552

多路复用有很多的实现,以 select 为例,当用户进程调用了多路复用器,进程会被阻塞。内核会监视多路复用器负责的所有 socket,当任何一个 socket 的数据准备好了, 多路复用器就会返回。

这时候用户进程再调用 read 操作,把数据从内核缓冲区拷贝到用户空间

image-20200301111059974

所以,I/O 多路复用的特点是通过一种机制一个进程能同时等待多个文件描述符, 而这些文件描述符(套接字描述符)其中的任意一个进入读就绪(readable)状态,select() 函数就可以返回。

Redis 的多路复用, 提供了 select, epoll, evport, kqueue 几种选择,在编译的时 候来选择一种。

evport 是 Solaris 系统内核提供支持的;
epoll 是 LINUX 系统内核提供支持的;
kqueue 是 Mac 系统提供支持的;
select 是 POSIX 提供的,一般的操作系统都有支撑(保底方案);

源码 ae_epoll.c、ae_select.c、ae_kqueue.c、ae_evport.c

内存回收

Reids 所有的数据都是存储在内存中的,在某些情况下需要对占用的内存空间进行回 收。内存回收主要分为两类,一类是 key 过期,一类是内存使用达到上限(max_memory) 触发内存淘汰。

过期策略

定时过期(主动淘汰)

惰性过期(被动淘汰)

定期过期

Redis 中同时使用了惰性过期和定期过期两种过期策略。

淘汰策略

最大内存设置

淘汰策略

LRU

LFU

random

LRU 淘汰原理

如果淘汰策略是 LRU,则根据配置的采样值 maxmemory_samples(默认是 5 个),随机从数据库中选择 m 个 key, 淘汰其中热度最低的 key 对应的缓存数据。

为什么不用常规的哈希表+双向链表的方式实现?需要额外的数据结构,消耗资源。 而 Redis LRU 算法在 sample 为 10 的情况下,已经能接近传统 LRU 算法了。

如何找出热度最低的数据?

Redis 中所有对象结构都有一个 lru 字段, 且使用了 unsigned 的低 24 位,这个字段 用来记录对象的热度。对象被创建时会记录 lru 值。在被访问的时候也会更新 lru 的值。 但是不是获取系统当前的时间戳,而是设置为全局变量 server.lruclock 的值

https://redis.io/topics/lru-cache

LFU

持久化机制

RDB

RDB 是 Redis 默认的持久化方案。当满足一定条件的时候,会把当前内存中的数 据写入磁盘,生成一个快照文件 dump.rdb。Redis 重启会通过加载 dump.rdb 文件恢 复数据。

RDB 触发

RDB 数据的恢复(演示)

RDB 文件的优势和劣势

一、优势
1.RDB 是一个非常紧凑(compact)的文件,它保存了 redis 在某个时间点上的数据

集。这种文件非常适合用于进行备份和灾难恢复。
2.生成 RDB 文件的时候,redis 主进程会 fork()一个子进程来处理所有保存工作,主进程不需要进行任何磁盘 IO 操作。
3.RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。
二、劣势
1、RDB 方式数据没办法做到实时持久化/秒级持久化。因为 bgsave 每次运行都要执行 fork 操作创建子进程,频繁执行成本过高。

2、在一定间隔时间做一次备份,所以如果 redis 意外 down 掉的话,就会丢失最后一次快照之后的所有修改(数据有丢失)。 如果数据相对来说比较重要,希望将损失降到最小,则可以使用 AOF 方式进行持久化。

AOF

Append Only File
AOF:Redis 默认不开启。AOF 采用日志的形式来记录每个写操作,并追加到文件中。开启后,执行更改 Redis 数据的命令时,就会把命令写入到 AOF 文件中。
Redis 重启时会根据日志文件的内容把写指令从前到后执行一次以完成数据的恢复工作。

那么对于 AOF 和 RDB 两种持久化方式,我们应该如何选择呢?

如果可以忍受一小段时间内数据的丢失,毫无疑问使用 RDB 是最好的,定时生成 RDB 快照(snapshot)非常便于进行数据库备份, 并且 RDB 恢复数据集的速度也要 比 AOF 恢复的速度要快。

否则就使用 AOF 重写。但是一般情况下建议不要单独使用某一种持久化机制,而是应该两种一起用,在这种情况下,当 redis 重启的时候会优先载入 AOF 文件来恢复原始 的数据,因为在通常情况下 AOF 文件保存的数据集要比 RDB 文件保存的数据集要完整。

AOF 配置

参数 说明
appendonly Redis 默认只开启 RDB 持久化,开启 AOF 需要修改为 yes
appendfilename “appendonly.aof” 路径也是通过 dir 参数配置 config get dir

数据都是实时持久化到磁盘吗?

AOF 持久化策略(硬盘缓存到磁盘),默认 everysec
 no表示不执行fsync,由操作系统保证数据同步到磁盘,速度最快,但是不太安全;  always表示每次写入都执行fsync,以保证数据同步到磁盘,效率很低;
 everysec 表示每秒执行一次 fsync,可能会导致丢失这 1s 数据。通常选择 everysec ,兼顾安全性和效率。

文件越来越大,怎么办?

为了解决这个问题,Redis 新增了重写机制,当 AOF 文件的大小超过所设定的阈值 时,Redis 就会启动 AOF 文件的内容压缩,只保留可以恢复数据的最小指令集

AOF 优势与劣势

优点:

1、AOF 持久化的方法提供了多种的同步频率,即使使用默认的同步频率每秒同步 一次,Redis 最多也就丢失 1 秒的数据而已。

缺点:

1、对于具有相同数据的的 Redis,AOF 文件通常会比 RDF 文件体积更大(RDB 存的是数据快照)。

2、虽然 AOF 提供了多种同步的频率,默认情况下,每秒同步一次的频率也具有较 高的性能。在高并发的情况下,RDB 比 AOF 具好更好的性能保证。

那么对于 AOF 和 RDB 两种持久化方式,我们应该如何选择呢?

如果可以忍受一小段时间内数据的丢失,毫无疑问使用 RDB 是最好的,定时生成 RDB 快照(snapshot)非常便于进行数据库备份, 并且 RDB 恢复数据集的速度也要 比 AOF 恢复的速度要快。

否则就使用 AOF 重写。但是一般情况下建议不要单独使用某一种持久化机制,而 是应该两种一起用,在这种情况下,当 redis 重启的时候会优先载入 AOF 文件来恢复原始 的数据,因为在通常情况下 AOF 文件保存的数据集要比 RDB 文件保存的数据集要完整

重写的时候会写成RDB后面通过AOF的方式追加


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 951488791@qq.com

文章标题:Redis原理篇

字数:4k

本文作者:zhengyumin

发布时间:2020-02-28, 18:52:35

最后更新:2020-03-25, 18:36:03

原始链接:http://zyumin.github.io/2020/02/28/Redis%E5%8E%9F%E7%90%86%E7%AF%87/

版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。