Skip to content

Latest commit

 

History

History
477 lines (241 loc) · 23.4 KB

File metadata and controls

477 lines (241 loc) · 23.4 KB

Redis原理

1.发布订阅模式

1.1 列表的局限

通过队列的 rpush 和 lpop 可以实现消息队列(队尾进队头出),但是消 费者需要不停地调用 lpop 查看 List 中是否有等待处理的消息(比如写一个 while 循环)。 为了减少通信的消耗,可以 sleep()一段时间再消费,但是会有两个问题:

1、如果生产者生产消息的速度远大于消费者消费消息的速度,List 会占用大量的内 存。

2、消息的实时性降低。

list 还提供了一个阻塞的命令:blpop,没有任何元素可以弹出的时候,连接会被阻塞。

基于 list 实现的消息队列,不支持一对多的消息分发。

1.2 发布订阅模式

1.2.1订阅频道

image-20220306181308994

发出去的消息不会被持久化,因为它已经从队列里面移除了, 所以消费者只能收到它开始订阅这个频道之后发布的消息。

image-20220306163537562

1.2.2 按规则订阅频道

支持 ? 和 * 占位符。?代表一个字符,*代表 0 个或者多个字

image-20220306181608159

image-20220306181642540

2.Redis事务

2.1 为什么要用事务

我们知道 Redis 的单个命令是原子性的(比如 get set mget mset),如果涉及到 多个命令的时候,需要把多个命令作为一个不可分割的处理序列,就需要用到事务。

Redis 的事务有两个特点:

1、按进入队列的顺序执行。

2、不会受到其他客户端的请求的影响。

2.2 事务的用法

通过 multi 的命令开启事务。事务不能嵌套,多个 multi 命令效果一样。

multi 执行后,客户端可以继续向服务器发送任意多条命令, 这些命令不会立即被 执行, 而是被放到一个队列中, 当 exec 命令被调用时, 所有队列中的命令才会被执 行。

Redis 的事务涉及到四个命令:multi(开启事务),exec(执行事务),discard (取消事务),watch(监视)

2.3 watch命令

在 Redis 中还提供了一个 watch 命令。

它可以为 Redis 事务提供 CAS 乐观锁行为(Check and Set / Compare and Swap),也就是多个线程更新变量的时候,会跟原值做比较,只有它没有被其他线程修 改的情况下,才更新成新的值。

我们可以用 watch 监视一个或者多个 key,如果开启事务之后,至少有一个被监视 key 键在 exec 执行之前被修改了, 那么整个事务都会被取消(key 提前过期除外)。可 以用 unwatch 取消。

image-20220306183918981

2.4 事务可能遇到的问题

2.4.1 在执行exec之前发生错误

比如:入队的命令存在语法错误,包括参数数量,参数名等等(编译器错误)。

image-20220306184014917

在这种情况下事务会被拒绝执行,也就是队列中所有的命令都不会得到执行。

2.4.2 在执行exec之后发生错误

比如,类型错误,比如对 String 使用了 Hash 的命令,这是一种运行时错误。

image-20220306185223362

最后我们发现 set k1 1 的命令是成功的,也就是在这种发生了运行时异常的情况下, 只有错误的命令没有被执行,但是其他命令没有受到影响。

这个显然不符合我们对原子性的定义,也就是我们没办法用 Redis 的这种事务机制 来实现原子性,保证数据的一致。

3.lua脚本

使用 Lua 脚本来执行 Redis 命令的好处:

1、一次发送多个命令,减少网络开销。

2、Redis 会将整个脚本作为一个整体执行,不会被其他请求打断,保持原子性。

3、对于复杂的组合命令,我们可以放在文件中,可以实现程序之间的命令集复 用。

3.1 在redis中调用Lua脚本

image-20220306185650022

3.2 在Lua脚本中调用Redis命令

image-20220306185750166

3.2.1 设置键值对

image-20220306185818021

3.2.2 在 Redis 中调用 Lua 脚本文件中的命,操作Redis

image-20220306185837551

3.2.3 案例:对 IP 进行限流

image-20220306190310912

image-20220306190326441

3.2.4 缓存 Lua 脚

为什么要缓存

在脚本比较长的情况下,如果每次调用脚本都需要把整个脚本传给 Redis 服务端, 会产生比较大的网络开销。为了解决这个问题,Redis 提供了 EVALSHA 命令,允许开发 者通过脚本内容的 SHA1 摘要来执行脚本。

如何缓存

Redis 在执行 script load 命令时会计算脚本的 SHA1 摘要并记录在脚本缓存中,执 行 EVALSHA 命令时 Redis 会根据提供的摘要从脚本缓存中查找对应的脚本内容,如果 找到了则执行脚本,否则会返回错误:"NOSCRIPT No matching script. Please use EVAL."

image-20220306190511004

自乘案例

image-20220306200859267

3.2.5 脚本超时

为 了防 止 某个 脚本 执 行时 间 过长 导 致 Redis 无 法提 供 服务 , Redis 提 供 了 lua-time-limit 参数限制脚本的最长运行时间,默认为 5 秒钟。

lua-time-limit 5000(redis.conf 配置文件中)

当脚本运行时间超过这一限制后,Redis 将开始接受其他命令但不会执行(以确保脚 本的原子性,因为此时脚本并没有被终止),而是会返回“BUSY”错误。

Redis 提供了一个 script kill 的命令来中止脚本的执行。新开一个客户端:

script kill

image-20220306201058197

shutdown nosave 和 shutdown 的区别在于 shutdown nosave 不会进行持久化 操作,意味着发生在上一次快照后的数据库修改都会丢失。

4.Redis为什么那么快

4.1 Redis 到底有多快?

根据官方的数据,Redis 的 QPS 可以达到 10 万左右

4.2 Redis 为什么这么快?

总结:

1)纯内存结构

2)单线程

3)多路复用

4.2.1 内存

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

第二个,要实现这么高的并发性能,是不是要创建非常多的线程? 恰恰相反,Redis 是单线程的。

4.2.2 单线程

1、没有创建线程、销毁线程带来的消耗

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

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

4.2.3 异步非阻塞

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

4.3 Redis 为什么是单线程的?

FAQ – Redis

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

4.4 单线程为什么这么快?

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

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

image-20220306202024274

这种方式有几个弊端:

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

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

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

image-20220306202104955

address sizes : 40 bits physical, 48 bits virtual

实际的物理内存可能远远小于虚拟内存的大小。

总结:引入虚拟内存,可以提供更大的地址空间,并且地址空间是连续的,使得程 序编写、链接更加简单。并且可以对物理内存进行隔离,不同的进程操作互不影响。还 可以通过把同一块物理内存映射到不同的虚拟地址空间实现内存共享。

4.4.2 用户空间和内核空间

image-20220306202235244

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

内核空间中存放的是内核代码和数据,而进程的用户空间中存放的是用户程序的代 码和数据。不管是内核空间还是用户空间,它们都处于虚拟空间中,都是对物理地址的 映射。

在 Linux 系统中, 内核进程和用户进程所占的虚拟内存比例是 1:3

image-20220306202305476

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

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

4.4.3 进程切换(上下文切换)

image-20220306202426968

image-20220306202443160

4.4.4 进程的阻塞

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

4.4.5 文件描述符 FD

image-20220306202607514

4.4.6 传统 I/O 数据拷贝

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

image-20220306202620777

4.4.7 Blocking I/O

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

image-20220306202930647

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

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

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

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

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

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

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

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

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

image-20220306203123733

多路复用有很多的实现,以 select 为例,当用户进程调用了多路复用器,进程会被 阻塞。内核会监视多路复用器负责的所有 socket,当任何一个 socket 的数据准备好了, 多路复用器就会返回。这时候用户进程再调用 read 操作,把数据从内核缓冲区拷贝到用 户空间。

image-20220306203154748

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

5.内存回收

内存回收主要分为两类,一类是 key 过期,一类是内存使用达到上限(max_memory) 触发内存淘汰。

5.1 过期策略

定期删除+惰性删除,如果内存还是不够,就走内存淘汰策略。

5.1.1 定时过期(主动淘汰)

每个设置过期时间的 key 都需要创建一个定时器,到过期时间就会立即清除。该策 略可以立即清除过期的数据,对内存很友好;但是会占用大量的 CPU 资源去处理过期的 数据,从而影响缓存的响应时间和吞吐量。

5.1.2 惰性过期(被动淘汰)

只有当访问一个 key 时,才会判断该 key 是否已过期,过期则清除。该策略可以最 大化地节省 CPU 资源,却对内存非常不友好。极端情况可能出现大量的过期 key 没有再 次被访问,从而不会被清除,占用大量内存。

5.1.3 定期过期

每隔一定的时间,会扫描一定数量的数据库的 expires 字典中一定数量的 key,并清 除其中已过期的 key。该策略是前两者的一个折中方案。通过调整定时扫描的时间间隔和 每次扫描的限定耗时,可以在不同情况下使得 CPU 和内存资源达到最优的平衡效果

5.2 淘汰策略

Redis 的内存淘汰策略,是指当内存使用达到最大内存极限时,需要使用淘汰算法来 决定清理掉哪些数据,以保证新数据的存入。

5.2.1 最大内存设置

redis.conf 参数配置

# maxmemory <bytes

如果不设置 maxmemory 或者设置为 0,64 位系统不限制内存,32 位系统最多使 用 3GB 内存。

5.2.2 淘汰策略

image-20220306203803612

5.2.3 LRU 淘汰原理

问题:如果基于传统 LRU 算法实现 Redis LRU 会有什么问题?

需要额外的数据结构存储,消耗内存。

Redis LRU 对传统的 LRU 算法进行了改良,

通过随机采样来调整算法的精度。 如果淘汰策略是 LRU,则根据配置的采样值 maxmemory_samples(默认是 5 个), 随机从数据库中选择 m 个 key, 淘汰其中热度最低的 key 对应的缓存数据。所以采样参数m配置的数值越大, 就越能精确的查找到待淘汰的缓存数据,但是也消耗更多的CPU计 算,执行效率降低。

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

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

问题:为什么不获取精确的时间而是放在全局变量中?不会有延迟的问题吗

这样函数 lookupKey 中更新数据的 lru 热度值时,就不用每次调用系统函数 time,可 以提高执行效率。

OK,当对象里面已经有了 LRU 字段的值,就可以评估对象的热度了。

函数 estimateObjectIdleTime 评估指定对象的 lru 热度,思想就是对象的 lru 值和 全局的 server.lruclock 的差值越大(越久没有得到更新), 该对象热度越低。

5.2.4 LFU

image-20220306204312454

6.持久化机制

6.1 RDB

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

6.1.1 RDB 触发

1、自动触发

a)配置规则触发。

redis.conf, SNAPSHOTTING,其中定义了触发把数据保存到磁盘的触发频率。

如果不需要 RDB 方案,注释 save 或者配置成空字符串“”

image-20220306204506558

问题:为什么停止 Redis 服务的时候没有 save,重启数据还在?

RDB 还有两种触发方式:

b)shutdown 触发,保证服务器正常关闭。

c)flushall,RDB 文件是空的,没什么意义(删掉 dump.rdb 演

2、手动触发

a)save

save 在生成快照的时候会阻塞当前 Redis 服务器, Redis 不能处理其他命令。如果 内存中的数据比较多,会造成 Redis 长时间的阻塞。生产环境不建议使用这个命令。

b)bgsave 执行 、bgsave 时,Redis 会在后台异步进行快照操作,快照同时还可以响应客户端请 求。

具体操作是 Redis 进程执行 fork 操作创建子进程(copy-on-write),RDB 持久化 过程由子进程负责,完成后自动结束。它不会记录 fork 之后后续的命令。阻塞只发生在 fork 阶段,一般时间很短。

用 lastsave 命令可以查看最近一次成功生成快照的时间。

6.1.2 RDB 数据的恢复(演示)

1、shutdown 持久化

image-20220306204741233

2、模拟数据丢失

image-20220306204805913

3、通过备份文件恢复数据

image-20220306205038154

image-20220306205048119

6.1.3 RDB 文件的优势和劣势

一、优势

1.RDB 是一个非常紧凑(compact)的文件,它保存了 redis 在某个时间点上的数据 集。这种文件非常适合用于进行备份和灾难恢复。

2.生成 RDB 文件的时候,redis 主进程会 fork()一个子进程来处理所有保存工作,主 进程不需要进行任何磁盘 IO 操作。

3.RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。

二、劣势

1、RDB 方式数据没办法做到实时持久化/秒级持久化。因为 bgsave 每次运行都要 执行 fork 操作创建子进程,频繁执行成本过高。

2、在一定间隔时间做一次备份,所以如果 redis 意外 down 掉的话,就会丢失最后 一次快照之后的所有修改(数据有丢失)。

如果数据相对来说比较重要,希望将损失降到最小,则可以使用 AOF 方式进行持久化。

6.2 AOF

AOF:Redis 默认不开启。AOF 采用日志的形式来记录每个写操作,并追加到文件中。开启后,执行更改 Redis 数据的命令时,就会把命令写入到 AOF 文件中。

Redis 重启时会根据日志文件的内容把写指令从前到后执行一次以完成数据的恢复 工作。

6.2.1 AOF 配置

image-20220306205256896

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

由于操作系统的缓存机制,AOF 数据并没有真正地写入硬盘,而是进入了系统的硬盘缓存。什么时候把缓冲区的内容写入到 AOF 文件?

image-20220306205354390

问题:文件越来越大,怎么办?

由于 AOF 持久化是 Redis 不断将写命令记录到 AOF 文件中,随着 Redis 不断的进 行,AOF 的文件会越来越大,文件越大,占用服务器内存越大以及 AOF 恢复要求时间 越长。

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

可以使用命令 bgrewriteaof 来重写。

AOF 文件重写并不是对原文件进行重新整理,而是直接读取服务器现有的键值对, 然后用一条命令去代替之前记录这个键值对的多条命令,生成一个新的文件后去替换原 来的 AOF。

image-20220306205534631

问题:重写过程中,AOF 文件被更改了怎么办?

image-20220306205612338

image-20220306205623122

6.2.2 AOF 数据恢复

启动即可恢复。

6.2.3 AOF 优势与劣势

优点:

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

缺点:

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

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