redis学习路线

待更新…

一、nosql讲解

1. 为什么要用nosql?

用户的个人信息,社交网络,地理位置,自己产生的数据,日志等等爆发式增长!传统的关系型数据库已无法满足这些数据处理的要求,这时我们就需要使用NoSQL数据库,它可以很好的处理上述的情况!

2. 什么是nosql?

这里有两个概念:关系型数据库和菲关系型数据库

  • 关系型数据库:列+行,同一个表下数据的结构是一样的。
  • 非关系型数据库:数据存储没有固定的格式,并且可以进行横向扩展。

在这里插入图片描述

3. nosql特点

1. 可扩展性: NoSQL数据库通常比关系型数据库更易于扩展。它们可以轻松地水平扩展,只需添加更多的服务器即可提高性能。这使得NoSQL数据库非常适合处理大规模数据高并发访问

2. 灵活性: NoSQL数据库支持多种数据模型。包括文档、键值对、图形和列式存储。这使得它们能够存储各种类型的数据

3. 高性能: NoSQL数据库通常比关系型数据库更快,因为它们不需要进行复杂的连接和事务处理。这使得它们非常适合需要快速响应的应用程序。

4. 高可用性: NoSQL数据库通常具有更高的可用性,因为它们可以容忍单个节点的故障。这使得它们非常适合需要持续运行的应用程序。

4. 传统的 RDBMS(关系型) 和 NoSQL

【传统的 RDBMS(关系型数据库)】:

  • 结构化组织
  • SQL
  • 数据和关系都存在单独的表中 :行+列
  • 操作,数据定义语言
  • 严格的一致性
  • 基础的事务操作

【Nosql】:

  • 不仅仅是数据
  • 没有固定的查询语言
  • 键值对存储,列存储,文档存储,图形数据库(社交关系)
  • 最终一致性
  • CAP定理和BASE
  • 高性能,高可用,高扩展

5. 阿里巴巴架构演进

在这里插入图片描述
参考链接:阿里巴巴实践分析理解数据架构演进

6. nosql四大分类

在这里插入图片描述


参考链接:Sql Or NoSql,看完这一篇你就懂了

7.CAP原则

CAP是Consistency(一致性),Availability(可用性),Partition tolerance(分区容错性)的缩写。
在这里插入图片描述
在一个分布式系统(指互相连接并共享数据的节点的集合)中,当涉及读写操作时,只能保证一致性(Consistence)、可用性(Availability)、分区容错性(Partition Tolerance)三者中的两个,另外一个必须被牺牲。

  1. 一致性(Consistency)

A read is guaranteed to return the most recent write for a given client.

对某个指定的客户端来说,读操作保证能够返回最新的写操作结果。

  1. 可用性(Availability)

A non-failing node will return a reasonable response within a reasonable amount of time (no error or timeout).

非故障的节点在合理的时间内返回合理的响应(不是错误和超时的响应)。

  1. 分区容忍性(Partition Tolerance)

System continues to work despite message loss or partial failure.

当出现网络分区后,系统能够继续“履行职责”。

8. BASE

BASE 是一个缩写,代表 Basically Available, Soft state, Eventually consistent。它描述了 Redis 的一些关键特性:

  • Basically Available: 基本可用,即使出现故障,也能保持大部分数据的可用性。
  • Soft state: 软状态,数据可能不是完全一致的,但最终会一致。
  • Eventually consistent: 最终一致性,数据最终会一致,但可能需要一些时间。

二、Redis入门

1. Redis是什么

Redis全称为:Remote Dictionary Server(远程数据服务),Redis是一种支持key-value等多种数据结构的存储系统。可用于缓存事件发布或订阅高速队列等场景。提供String(字符串)、Hash(哈希)、 List (列表)、Set(集合)、Zset(有序集合)直接存取,基于内存可持久化

特点1:丰富的数据类型

我们知道很多数据库只能处理一种数据结构:

传统SQL数据库处理二维关系数据
MemCached数据库,键和值都是字符串
文档数据库(MongoDB)是由Json/Bson组成的文档。
当然不是他们这些数据库不好,而是一旦数据库提供数据结构不适合去做某件事情的话,程序写起来就非常麻烦和不自然。

Redis虽然也是键值对数据库,但是和Memcached不同的是:Redis的值不仅可以是字符串,它还可以是其他五种数据结构中的任意一种。

特点2:内存存储

数据库有两种:一种是硬盘数据库,一种是内存数据库。

硬盘数据库是把值存储在硬盘上,在内存中就存储一下索引,当硬盘数据库想访问硬盘的值时,它先在内存里找到索引,然后再找值。问题在于,在读取和写入硬盘的时候,如果读写比较多的时候,它会把硬盘的IO功能堵死。

内存存储是讲所有的数据都存储在内存里面,数据读取和写入速度非常快。

特点3:持久化功能

将数据存储在内存里面的数据保存到硬盘中,保证数据安全,方便进行数据备份和恢复。

2. Redis 和 Memcached 有什么区别?

很多人都说用 Redis 作为缓存,但是 Memcached 也是基于内存的数据库,为什么不选择它作为缓存呢?要解答这个问题,我们就要弄清楚 Redis 和 Memcached 的区别。

Redis 与 Memcached 共同点:

  • 都是基于内存的数据库,一般都用来当做缓存使用。
  • 都有过期策略。
  • 两者的性能都非常高。

Redis 与 Memcached 区别:

  • Redis 支持的数据类型更丰富(String、Hash、List、Set、ZSet),而 Memcached 只支持最简单的 key-value 数据类型;
  • Redis 支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用,而 Memcached 没有持久化功能,数据全部存在内存之中,Memcached 重启或者挂掉后,数据就没了;
  • Redis 原生支持集群模式,Memcached 没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据;
  • Redis 支持发布订阅模型Lua 脚本事务等功能,而 Memcached 不支持

3. 为什么用 Redis 作为 MySQL 的缓存?

主要是因为 Redis 具备高性能高并发两种特性。

  1. Redis 具备高性能:Redis 是一个内存数据库,相比于磁盘上的 MySQL 数据库,Redis 可以提供更快的读取和写入速度。通过将常用的数据存储在 Redis 中,并且将读取请求重定向到 Redis,可以大大减少对 MySQL 的负载,提升系统的整体性能。

![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/c93f9b094b3f410fa0ffcaa6158bec22.png

  1. 数据结构灵活:Redis 提供了丰富的数据结构,如字符串、哈希、列表、集合和有序集合等。这些数据结构可以更好地满足不同应用场景下的需求。而 MySQL 是一个关系型数据库,数据结构相对较为固定。通过使用 Redis,可以灵活地存储和操作数据,提高开发效率和系统的灵活性。

  2. 缓存生命周期管理:Redis 提供了灵活的过期时间设置和缓存淘汰策略,可以根据业务需求对缓存进行生命周期管理。这样可以避免无效或过期的缓存数据对系统性能和数据一致性造成影响。

  3. 分布式缓存支持:Redis 支持分布式缓存,可以将缓存数据存储在多个 Redis 节点上,提高系统的可扩展性和容错性。同时,Redis 还提供了发布订阅机制,可以实现实时数据同步和消息传递。

  4. 高并发:直接操作缓存能够承受的请求是远远大于直接访问数据库的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库在这里插入图片描述

需要注意的是,Redis 并不是一定适合所有的场景,对于一些对数据一致性要求较高的应用或者需要复杂查询功能的应用,使用 Redis 作为缓存可能不太合适。此外,使用 Redis 作为 MySQL 缓存也需要考虑系统的复杂性和维护成本。

4. redis数据结构

Redis 提供了丰富的数据类型,常见的有五种数据类型:String(字符串),Hash(哈希),List(列表),Set(集合)、Zset(有序集合)。这 5 种数据类型是直接提供给用户使用的,是数据的保存形式,其底层实现主要依赖这 8 种数据结构:简单动态字符串(SDS)、LinkedList(双向链表)、Dict(哈希表/字典)、SkipList(跳跃表)、Intset(整数集合)、ZipList(压缩列表)、QuickList(快速列表)。Redis 5 种基本数据类型对应的底层数据结构实现如下表所示:
在这里插入图片描述
Redis 5 种基本数据类型详解
在这里插入图片描述

4.1 String(字符串)

String 是 Redis 中最简单同时也是最常用的一个数据类型。String 是一种二进制安全的数据类型,可以用来存储任何类型的数据比如字符串、整数、浮点数、图片(图片的 base64 编码或者解码或者图片的路径)、序列化后的对象。
在这里插入图片描述

4.2 List(列表)

Redis 的 List 的实现为一个 双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销。
在这里插入图片描述

4.3 Hash(哈希)

Redis 中的 Hash 是一个 String 类型的 field-value(键值对) 的映射表,特别适合用于存储对象,后续操作的时候,你可以直接修改这个对象中的某些字段的值。Hash 类似于 JDK1.8 前的 HashMap,内部实现也差不多(数组 + 链表)。不过,Redis 的 Hash 做了更多优化。
在这里插入图片描述

4.4 Set(集合)

Redis 中的 Set 类型是一种无序集合,集合中的元素没有先后顺序但都唯一,有点类似于 Java 中的 HashSet 。当你需要存储一个列表数据,又不希望出现重复数据时,Set 是一个很好的选择,并且 Set 提供了判断某个元素是否在一个 Set 集合内的重要接口,这个也是 List 所不能提供的。你可以基于 Set 轻易实现交集、并集、差集的操作,比如你可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合。这样的话,Set 可以非常方便的实现如共同关注、共同粉丝、共同喜好等功能。这个过程也就是求交集的过程。
在这里插入图片描述

4.5 Sorted Set(有序集合)

Sorted Set 类似于 Set,但和 Set 相比,Sorted Set 增加了一个权重参数 score,使得集合中的元素能够按 score 进行有序排列,还可以通过 score 的范围来获取元素的列表。有点像是 Java 中 HashMap 和 TreeSet 的结合体。
在这里插入图片描述

5. redis线程

5.1 讲解一下Redis的线程模型?

Redis 单线程指的是「接收客户端请求->解析请求 ->进行数据读写等操作->发送数据给客户端」这个过程是由一个线程(主线程)来完成的,这也是我们常说 Redis 是单线程的原因。

5.2 为什么单线程速度快?

  • 纯内存操作
  • 核心是基于非阻塞的 IO 多路复用机制
  • 单线程反而避免了多线程的频繁上下文切换问题

5.3 Redis 为什么早期选择单线程?

在这里插入图片描述

官方 FAQ 表示,因为 Redis 是基于内存的操作,CPU 成为 Redis 的瓶颈的情况很少见,Redis 的瓶颈最有可能是内存的大小或者网络限制。

如果想要最大程度利用 CPU,可以在一台机器上启动多个 Redis 实例

5.4 Redis6.0 使用多线程是怎么回事?

Redis6.0 的多线程是用多线程来处理数据的读写协议解析,但是 Redis执行命令还是单线程的。

这样做的⽬的是因为 Redis 的性能瓶颈在于⽹络 IO ⽽⾮ CPU,使⽤多线程能提升 IO 读写的效率,从⽽整体提⾼ Redis 的性能。

在这里插入图片描述
在这里插入图片描述

6. Redis持久化

6.1 什么是持久化?

持久化就是把内存的数据写到磁盘中去,防止服务宕机了内存数据丢失。
在这里插入图片描述

6.2 持久化方式

redis持久化aof和rdb区别 redis中rdb持久化和aof持久化
Redis持久化之RDB与AOF 的区别

1. AOF 日志

以日志的形式记录服务器所处理的每一个写、删除操作,查询操作不会记录,以文本的方式记录,可以打开文件看到详细的操作记录。

AOF持久化是通过AOF日志来实现的,而AOF日志是写后日志,即redis先执行命令,然后将数据写入内存中,最后才写入AOF日志中,如下图:
在这里插入图片描述
1. 从Redis AOF 操作过程,我们知道Redis 是先执行写操作命令后,才将该命令记录到 AOF 日志里的,那么它为什么这么设计呢?

  1. 避免额外的检查开销:如果先记录日志再执行命令,那么记录日志的时候,就没有检查命令的正确性,一旦出现错误语法,在AOF日志也会记录下面。那么当需要通过AOF日志恢复数据时,也可能出错

  2. 避免对当前命令的阻塞:而且先执行命令再记录日志,还可以避免操作日志对当前命令的阻塞

2. AOF的风险
因为执行写操作命令和记录日志是两个过程,当我们刚执行完一个写操作命令,但还没有来得及保存AOF文件时就宕机了,那么就会出现数据丢失的风险。另外我们前面也说了它不会阻塞当前命令,但如果下一个写入操作到来之前,该命令还没有写入完日志,就会阻塞,所以它会有阻塞下一次操作的风险。因为执行写操作命令和记录日志这两个过程都是由主进程完成的,所以它们是同步操作的。如下图:
在这里插入图片描述

3. AOF 日志写回硬盘的三大策略
在这里插入图片描述

4. AOF 重写机制
我们知道AOF 是通过文件的方式记录下所有写命令,但是随着写操作越来越多,文件也会越来越大。当AOF 日志文件过大就会带来性能问题,比如再次写入指令的话效率也会变低。另外如果发生宕机,需要读 AOF 文件的内容以恢复数据,如果文件过大,整个恢复的过程就会很慢。所以我们会给AOF 文件的大小设定的阈值,当超过该阈值时候,就会启用 AOF 重写机制,来压缩 AOF 文件,从而避免 AOF 文件越写越大。

AOF 重写就是根据所有的键值对创建一个新的 AOF 文件,可以减少大量的文件空间,减少的原因是:AOF 对于命令的添加是追加的方式,逐一记录命令,但有可能存在某个键值被反复更改,产生了一些冗余数据,这样在重写的时候就可以过滤掉这些指令,从而更新当前的最新状态。

AOF 重写为什么要创建一个新的 AOF 文件,是因为如果 AOF 重写过程中失败了,现有的 AOF 文件就会造成污染,可能无法用于恢复使用。
5.

2. RDB 快照

在指定的时间间隔内将内存中的数据集快照写入磁盘,实际操作过程是fork一个子进程,先将数据集写入临时文件,写入成功后,再替换之前的文件,用二进制压缩存储。

AOF 文件的内容是操作命令

RDB 文件的内容是二进制数据,即实际的某一个瞬间的内存数据

在这里插入图片描述
1. 生成RDB文件的两个命令
分别是 save bgsave,他们的区别就在于是否在「主线程」里执行:

save:在主线程中执行,会导致主线程阻塞;
bgsave:会创建一个子进程,该进程专门用于写入 RDB 文件,可以避免主线程的阻塞,也是默认的方式。

2. 执行快照期间的读写操作

  • bgsave 子进程是由主线程 fork 出来的,可以共享主线程的所有内存数据;
  • bgsave 子进程运行后,开始读取主线程的内存数据,并把它们写入 RDB 文件中;
  • 如果主线程对这些数据都是读操作,那么主线程和 bgsave 子进程互不影响;
  • 如果主线程需要修改一块数据,那么这块数据会被复制一份,生成数据的副本,然后,bgsave子进程会把这个副本数据写入RDB文件,而在这个过程中,主线程仍然可以直接修改原来的数据。
    在这里插入图片描述

3. 混合持久化方式

尽管 RDB 比 AOF 的数据恢复速度快,但是快照的频率不好把握:

  • 如果频率太低,两次快照间一旦服务器发生宕机,就可能会比较多的数据丢失;
  • 如果频率太高,频繁写入磁盘和创建子进程会带来额外的性能开销。

RDB 持久化的优点是恢复大数据集的速度比较快,但是可能会丢失最后一次快照以后的数据。

AOF 持久化的优点是数据的完整性比较高,通常只会丢失一秒的数据,但是对于大数据集,AOF 文件可能会比较大,恢复的速度比较慢。

那有没有什么方法不仅有 RDB 恢复速度快的优点和,又有 AOF 丢失数据少的优点呢?

当然有,那就是将 RDB 和 AOF 合体使用,这个方法是在 Redis 4.0 提出的,该方法叫混合使用 AOF 日志和内存快照,也叫混合持久化。

如果想要开启混合持久化功能,可以在 Redis 配置文件将下面这个配置项设置成 yes:

aof-use-rdb-preamble yes

混合持久化工作在 AOF 日志重写过程

混合持久化模式会在 AOF 重写的时候同时生成一份 RDB 快照,然后将这份快照作为 AOF 文件的一部分,最后再附加新的写入命令。

也就是说,使用了混合持久化,AOF 文件的前半部分是 RDB 格式的全量数据,后半部分是 AOF 格式的增量数据。
在这里插入图片描述

这样的好处在于,重启 Redis 加载数据的时候,由于前半部分是 RDB 内容,这样加载的时候速度会很快

加载完 RDB 的内容后,才会加载后半部分的 AOF 内容,这里的内容是 Redis 后台子进程重写 AOF 期间,主线程处理的操作命令,可以使得数据更少的丢失

6.3 Redis数据如何恢复?

当 Redis 发生了故障,可以从 RDB 或者 AOF 中恢复数据。

恢复的过程也很简单,把 RDB 或者 AOF 文件拷贝到 Redis 的数据目录下,如果使用 AOF 恢复,配置文件开启 AOF,然后启动 redis-server 即可
在这里插入图片描述

Redis 启动时加载数据的流程:

  1. AOF 持久化开启且存在 AOF 文件时,优先加载 AOF 文件。
  2. AOF 关闭或者 AOF 文件不存在时,加载 RDB 文件。
  3. 加载 AOF/RDB 文件成功后,Redis 启动成功。
  4. AOF/RDB 文件存在错误时,Redis 启动失败并打印错误信息。

6.4 如何设置持久化模式

可以通过编辑 Redis 的配置文件 redis.conf 来进行设置,或者在运行时通过 Redis 命令行动态调整。

RDB 持久化通过在配置文件中设置快照(snapshotting)规则来启用。这些规则定义了在多少秒内如果有多少个键被修改,则自动执行一次持久化操作。

save 900 1      # 如果至少有1个键被修改,900秒后自动保存一次
save 300 10     # 如果至少有10个键被修改,300秒后自动保存一次
save 60 10000   # 如果至少有10000个键被修改,60秒后自动保存一次

AOF 持久化是通过在配置文件中设置 appendonly 参数为 yes 来启用的

appendonly yes

此外,还可以配置 AOF 文件的写入频率,这是通过 appendfsync 设置的:

appendfsync always    # 每次写入数据都同步,保证数据不丢失,但性能较低
appendfsync everysec  # 每秒同步一次,折衷方案
appendfsync no        # 由操作系统决定何时同步,性能最好,但数据安全性最低

为了优化 AOF 文件的大小,Redis 允许自动或手动重写 AOF 文件。可以在配置文件中设置重写的触发条件:

auto-aof-rewrite-percentage 100  # 增长到原大小的100%时触发重写
auto-aof-rewrite-min-size 64mb   # AOF 文件至少达到64MB时才考虑重写

手动执行 AOF 重写的命令是:

redis-cli bgrewriteaof

如果决定同时使用 RDB 和 AOF,可以在配置文件中同时启用两者。

save 900 1
appendonly yes

还可以在运行时动态更改:

redis-cli config set save "900 1 300 10 60 10000"
redis-cli config set appendonly yes
redis-cli config set appendfsync everysec

三、高可用

Redis 中常见的集群部署方案
Redis 除了单机部署外,还可以通过主从复制哨兵模式切片集群来实现高可用。

  1. 主从复制(Master-Slave Replication):允许一个 Redis 服务器(主节点)将数据复制到一个或多个 Redis 服务器(从节点)。这种方式可以实现读写分离,适合读多写少的场景。

  2. 哨兵模式(Sentinel):用于监控主节点和从节点的状态,实现自动故障转移和系统消息通知。如果主节点发生故障,哨兵可以自动将一个从节点升级为新的主节点,保证系统的可用性。

  3. 切片集群(Cluster):Redis 集群通过分片的方式存储数据,每个节点存储数据的一部分,用户请求可以并行处理。集群模式支持自动分区、故障转移,并且可以在不停机的情况下进行节点增加或删除。

1. 主从集群模式

主从集群,主从库之间采用的是读写分离

  • 主库:所有的写操作都在主库发生,然后主库同步数据到从库,同时也可以进行读操作;

  • 从库:只负责读操作;
    在这里插入图片描述
    主库需要复制数据到从库,主从双方的数据库需要保存相同的数据,将这种情况称为”数据库状态一致”

从服务器首次加入主服务器中发生的是全量同步

  • 服务器的运行ID(run ID):每个 Redis 服务器在运行期间都有自己的run ID,run ID在服务器启动的时候自动生成。
    从服务器会记录主服务器的run ID,这样如果发生断网重连,就能判断新连接上的主服务器是不是上次的那一个,这样来决定是否进行数据部分重传还是完整重新同步
  • 复制偏移量 offset:主服务器和从服务器都会维护一个复制偏移量
    主服务器每次向从服务器中传递 N 个字节的时候,会将自己的复制偏移量加上 N。
    从服务器中收到主服务器的 N 个字节的数据,就会将自己额复制偏移量加上 N。
    通过主从服务器的偏移量对比可以很清楚的知道主从服务器的数据是否处于一致。

1.1 全量同步

在这里插入图片描述

  1. 从服务器连接到主服务器,然后发送 psync 到主服务器,因为第一次复制,不知道主库run ID,所以run ID为?;

  2. 主服务器接收到同步的响应,回复从服务器自己的run ID和复制进行进度 offset;

  3. 主服务器开始同步所有数据到从库中,同步依赖 RDB 文件,主库会通过 bgsave 命令,生成 RDB 文件,然后将 RDB 文件传送到从库中;

  4. 从库收到 RDB 文件,清除自己的数据,然后载入 RDB 文件;

  5. 主库在同步的过程中不会被阻塞,仍然能接收到命令,但是新的命令是不能同步到从库的,所以主库会在内存中用专门的 replication buffer,记录 RDB 文件生成后收到的所有写操作,然后在 RDB 文件,同步完成之后,再将replication buffer中的命令发送到从库中,这样就保证了从库的数据同步。

1.2 增量同步

如果主从服务器之间发生了网络闪断,从从服务将会丢失一部分同步的命令。

Redis 主库接收到写操作的命令,首先会写入replication buffer(主要用于主从数据传输的数据缓冲),同时也会把这些操作命令也写入repl_backlog_buffer这个缓冲区。
在这里插入图片描述

这里可能有点疑惑,已经有了replication buffer为什么还多余引入一个repl_backlog_buffer呢?

  • repl_backlog_buffer是一个「环形」缓冲区,用于主从服务器断连后,从中找到差异的数据;repl_backlog_buffer是一个环形缓冲区,主库会记录自己写到的位置,从库则会记录自己已经读到的位置。

  • replication buffer用于主节点与各个从节点间,数据的批量交互。主节点为各个从节点分别创建一个缓冲区,由于各个从节点的处理能力差异,各个缓冲区数据可能不同。
    - replication offset,标记上面那个缓冲区的同步进度,主从服务器都有各自的偏移量,主服务器使用 master_repl_offset 来记录自己「写」到的位置,从服务器使用 slave_repl_offset 来记录自己「读」到的位置。

1.3 为什么会出现主从数据不一致?

主从节点间的命令复制是异步进行的,所以无法实现强一致性保证

1.4 如何应对主从数据不一致

比如这里从服务器1,刚刚由于网络原因断连了一会,然后又恢复了连接,这时候,可能缺失了一段时间的命令同步,repl_backlog_buffer的增量同步机制就登场了。
repl_backlog_buffer会根据主服务器的master_repl_offset和从服务器slave_repl_offset,计算出两者命令之间的差距,之后把差距同步给replication buffer,然后发送到从服务器中。

1.5 主从切换如何减少数据丢失?

redis主从丢数据 redis 主从切换 数据丢失
主从切换过程中,产生数据丢失的情况有两种:

1. 异步复制同步丢失:因为 master -> slave的复制都是异步的,所以有可能出现master内存中的部分数据来不及复制到slave上,master就宕机了,随后通过哨兵执行主备切换,导致这部分数据丢失。
在这里插入图片描述

2. 集群产生脑裂数据丢失

脑裂:某个master所在的机器突然脱离了正常的网络,跟其他slave节点不能连接。但实际上master是正常运行的。但哨兵就有可能认为master宕机了,然后开始从剩下的哨兵中选举出一个哨兵执行故障转移,将salve切换成master

此时集群中就会出现两个master,也就是所谓的脑裂,此时虽然某个slave被切换成了master,但是可能client还没来得及切换到新的master,还继续往旧的master中写数据。因此旧的master再次恢复的时候,会被作为一个slave挂到新的master上去,自己的数据就会被清空,重新从master上面复制数据,导致后面那部分往旧master写入的数据就丢失了。

在这里插入图片描述

# 要求至少一个salve,完成数据同步,才认为数据写入成功
min-slaves-to-write 1

# 配置复制和不同延迟不能超过10秒
min-slaves-max-lag 10

2. 哨兵极致

对于主从集群模式,如果从库发生了故障,还有主库和其它的从库可以接收请求,但是如果主库挂了,就不能进行正常的数据写入,同时数据同步也不能正常的进行了,当然这种情况,我们需要想办法避免,于是就引入了下面的哨兵机制。

2.1 什么是哨兵机制

哨兵节点是特殊的 Redis 服务,不提供读写,主要来监控 Redis 中的实例节点,如果监控服务的主服务器下线了,会从所属的从服务器中重新选出一个主服务器,代替原来的主服务器提供服务。
在这里插入图片描述

核心功能就是:监控选主通知
在这里插入图片描述

  • 监控:哨兵机制,会周期性的给所有主服务器发出 PING 命令,检测它们是否仍然在线运行,如果在规定的时间内响应了 PING 通知则认为,仍在线运行;如果没有及时回复,则认为服务已经下线了,就会进行切换主库的动作。这个「规定的时间」是配置项 down-after-milliseconds 参数设定的,单位是毫秒。

  • 选主:当主库挂掉的时候,会从从库中按照既定的规则选出一个新的的主库

  • 通知:当一个主库被新选出来,会通知其他从库,进行连接,然后进行数据的复制。当客户端试图连接失效的主库时,集群也会向客户端返回新主库的地址,使得集群可以使用新的主库。

1. 如何保证选主的准确性?

  • 主观下线:是因为有可能「主节点」其实并没有故障,可能只是因为主节点的系统压力比较大或者网络发送了拥塞,导致主节点没有在规定时间内响应哨兵的 PING 命令。
  • 客观下线:当一个哨兵判断主节点为「主观下线」后,就会向其他哨兵发起命令,其他哨兵收到这个命令后,就会根据自身和主节点的网络状况,做出赞成投票或者拒绝投票的响应。

为了减少误判的情况,哨兵在部署的时候不会只部署一个节点,而是用多个节点部署成哨兵集群最少需要三台机器来部署哨兵集群),通过多个哨兵节点一起判断,就可以就可以避免单个哨兵因为自身网络状况不好,而误判主节点下线的情况、

在这里插入图片描述
2. 哨兵候选者如何选举成为 Leader?

候选者会向其他哨兵发送命令,表明希望成为 Leader 来执行主从切换,并让所有其他哨兵对它进行投票。

每个哨兵只有一次投票机会,如果用完后就不能参与投票了,可以投给自己或投给别人,但是只有候选者才能把票投给自己。

那么在投票过程中,任何一个「候选者」,要满足两个条件:

  • 拿到半数以上的赞成票
  • 拿到的票数同时还需要大于等于哨兵配置文件中的 quorum 值

举个例子,假设哨兵节点有 3 个,quorum 设置为 2,那么任何一个想成为 Leader 的哨兵只要拿到 2 张赞成票,就可以选举成功了。如果没有满足条件,就需要重新进行选举。

这时候有的同学就会问了,如果某个时间点,刚好有两个哨兵节点判断到主节点为客观下线,那这时不就有两个候选者了?这时该如何决定谁是 Leader 呢?

每位候选者都会先给自己投一票,然后向其他哨兵发起投票请求。如果投票者先收到「候选者 A」的投票请求,就会先投票给它,如果投票者用完投票机会后,收到「候选者 B」的投票请求后,就会拒绝投票。这时,候选者 A 先满足了上面的那两个条件,所以「候选者 A」就会被选举为 Leader。

3. 主从故障转移的过程是怎样的?

在哨兵集群中通过投票的方式,选举出了哨兵 leader 后,就可以进行主从故障转移的过程了,如下图:
在这里插入图片描述
主从故障转移操作包含以下四个步骤:

第一步:在已下线主节点(旧主节点)属下的所有「从节点」里面,挑出一个从节点,并将其转换为主节点。
第二步:让已下线主节点属下的所有「从节点」修改复制目标,修为复制「新主节点」;
第三步:将新主节点的 IP 地址和信息,通过「发布者/订阅者机制」通知给客户端;
第四步:继续监视旧主节点,当这个旧主节点重新上线时,将它设置为新主节点的从节点;

步骤一:选出新主节点

我们首先要把网络状态不好的从节点给过滤掉。首先把已经下线的从节点过滤掉,然后把以往网络连接状态不好的从节点也给过滤掉。

怎么判断从节点之前的网络连接状态不好呢?

Redis 有个叫 down-after-milliseconds * 10 配置项,其 down-after-milliseconds 是主从节点断连的最大连接超时时间。如果在 down-after-milliseconds 毫秒内,主从节点都没有通过网络联系上,我们就可以认为主从节点断连了。如果发生断连的次数超过了 10 次,就说明这个从节点的网络状况不好,不适合作为新主节点。

至此,我们就把网络状态不好的从节点过滤掉了,接下来要对所有从节点进行三轮考察:优先级复制进度ID 号。在进行每一轮考察的时候,哪个从节点优先胜出,就选择其作为新主节点。

  • 第一轮考察:哨兵首先会根据从节点的优先级来进行排序,优先级越小排名越靠前,
  • 第二轮考察:如果优先级相同,则查看复制的下标,哪个从「主节点」接收的复制数据多,哪个就靠前。
  • 第三轮考察:如果优先级和下标都相同,就选择从节点 ID 较小的那个。

在这里插入图片描述

在选举出从节点后,哨兵 leader 向被选中的从节点发送 SLAVEOF no one 命令,让这个从节点解除从节点的身份,将其变为新主节点。

步骤二:将从节点指向新主节点
在这里插入图片描述

在发送 SLAVEOF no one 命令之后,哨兵 leader 会以每秒一次的频率向被升级的从节点发送 INFO 命令(没进行故障转移之前,INFO 命令的频率是每十秒一次),并观察命令回复中的角色信息,当被升级节点的角色信息从原来的 slave 变为 master 时,哨兵 leader 就知道被选中的从节点已经顺利升级为主节点了。
如下图,选中的从节点 server2 升级成了新主节点:

在这里插入图片描述
步骤三:通知客户的主节点已更换

经过前面一系列的操作后,哨兵集群终于完成主从切换的工作,那么新主节点的信息要如何通知给客户端呢?

这主要通过 Redis 的发布者/订阅者机制来实现的。每个哨兵节点提供发布者/订阅者机制,客户端可以从哨兵订阅消息。

步骤四:将旧主节点变为从节点

故障转移操作最后要做的是,继续监视旧主节点,当旧主节点重新上线时,哨兵集群就会向它发送 SLAVEOF 命令,让它成为新主节点的从节点,如下图:
在这里插入图片描述

3. 切片集群

当 Redis 缓存数据量大到一台服务器无法缓存时,就需要使用 Redis 切片集群(Redis Cluster )方案,它将数据分布在不同的服务器上,以此来降低系统对单主节点的依赖,从而提高 Redis 服务的读写性能。

四、缓存设计

1. 什么是缓存击穿、缓存穿透、缓存雪崩?

Redis 缓存击穿(失效)、缓存穿透、缓存雪崩怎么解决?
在这里插入图片描述

1.1 缓存穿透

缓存穿透:意味着有特殊请求在查询一个不存在的数据,即数据不存在于Redis 不存在于数据库

导致每次请求都会穿透到数据库,缓存成了摆设,对数据库产生很大压力从而影响正常服务。

在这里插入图片描述
解决方案

  • 缓存空值:当请求的数据不存在 Redis 也不存在数据库的时候,设置一个缺省值(比如:None)。当后续再次进行查询则直接返回空值或者缺省值。
  • 布隆过滤器:在数据写入数据库的同时将这个 ID 同步到到布隆过滤器中,当请求的 id 不存在布隆过滤器中则说明该请求查询的数据一定没有在数据库中保存,就不要去数据库查询了。

1.2 缓存击穿

高并发流量,访问的这个数据是热点数据,请求的数据在 DB 中存在,但是 Redis 存的那一份已经过期,后端需要从 DB 从加载数据并写到 Redis。

关键字:单一热点数据高并发数据失效

但是由于高并发,可能会把 DB 压垮,导致服务不可用。如下图所示:
在这里插入图片描述
解决方案

1. 过期时间 + 随机值

对于热点数据,我们不设置过期时间,这样就可以把请求都放在缓存中处理,充分把 Redis 高吞吐量性能利用起来。

或者过期时间再加一个随机值

设计缓存的过期时间时,使用公式:过期时间=baes 时间+随机时间。

即相同业务数据写缓存时,在基础过期时间之上,再加一个随机的过期时间,让数据在未来一段时间内慢慢过期避免瞬时全部过期,对 DB 造成过大压力。

2. 预热
预先把热门数据提前存入 Redis 中,并设热门数据的过期时间超大值。

3. 使用锁
当发现缓存失效的时候,不是立即从数据库加载数据。

而是先获取分布式锁,获取锁成功才执行数据库查询和写数据到缓存的操作,获取锁失败,则说明当前有线程在执行数据库查询操作,当前线程睡眠一段时间在重试。

这样只让一个请求去数据库读取数据。

1.3 缓存雪崩

缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,请求直接落到数据库上,引起数据库压力过大甚至宕机。

和缓存击穿不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。

出现该原因主要有两种:

  • 大量热点数据同时过期,导致大量请求需要查询数据库并写到缓存;
  • Redis 故障宕机,缓存系统异常。

解决方案
1. 过期时间添加随机值

要避免给大量的数据设置一样的过期时间,过期时间 = baes 时间+ 随机时间(较小的随机数,比如随机增加 1~5 分钟)。

这样一来,就不会导致同一时刻热点数据全部失效,同时过期时间差别也不会太大,既保证了相近时间失效,又能满足业务需求。

2. 接口限流

当访问的不是核心数据的时候,在查询的方法上加上接口限流保护。比如设置 10000 req/s。

如果访问的是核心数据接口,缓存不允许从数据库中查询并设置到缓存中。

这样的话,只有部分请求会发送到数据库,减少了压力。

限流,就是指,我们在业务系统的请求入口前端控制每秒进入系统的请求数,避免过多的请求被发送到数据库。

如下图所示:
在这里插入图片描述

2. 如何保证缓存和数据库的数据⼀致性?

如何保证缓存与数据库双写时的数据一致性

2.1 更新数据库–>更新缓存

举个例子,比如「请求 A 」和「请求 B 」两个请求,同时更新「同一条」数据,则可能出现这样的顺序:
在这里插入图片描述
A 请求先将数据库的数据更新为 1,然后在更新缓存前,请求 B 将数据库的数据更新为 2,紧接着也把缓存更新为 2,然后 A 请求更新缓存为 1。

此时,数据库中的数据是 2,而缓存中的数据却是 1,出现了缓存和数据库中的数据不一致的现象

2.2 更新缓存–>更新数据库

假设「请求 A 」和「请求 B 」两个请求,同时更新「同一条」数据,则可能出现这样的顺序:
在这里插入图片描述
请求先将缓存的数据更新为 1,然后在更新数据库前,B 请求来了, 将缓存的数据更新为 2,紧接着把数据库更新为 2,然后 A 请求将数据库的数据更新为 1。

此时,数据库中的数据是 1,而缓存中的数据却是 2,出现了缓存和数据库中的数据不一致的现象。

所以,无论是「先更新数据库,再更新缓存」,还是「先更新缓存,再更新数据库」,这两个方案都存在并发问题,当两个请求并发更新同一条数据的时候,可能会出现缓存和数据库中的数据不一致的现象。

2.3 更新数据库–>删除缓存

在这里插入图片描述

这一种情况也会出现问题,比如更新数据库成功了,但是在删除缓存的阶段出错了没有删除成功,那么此时再读取缓存的时候每次都是错误的数据了。

对 Mysql 数据库更新操作后再 binlog 日志中我们都能够找到相应的操作,那么我们可以订阅 Mysql 数据库的 binlog 日志对缓存进行操作。
在这里插入图片描述

2.4 删除缓存–> 更新数据库

在这里插入图片描述
此时来了两个请求,请求 A(更新操作) 和请求 B(查询操作)

  • 请求 A 会先删除 Redis 中的数据,然后去数据库进行更新操作
  • 此时请求 B 看到 Redis 中的数据时空的,会去数据库中查询该值,补录到 Redis 中
  • 但是此时请求 A 并没有更新成功,或者事务还未提交

针对「先删除缓存,再更新数据库」方案在「读 + 写」并发请求而造成缓存不一致的解决办法是「延迟双删」。
延迟双删实现的伪代码如下:

#删除缓存
redis.delKey(X)
#更新数据库
db.update(X)
#睡眠
Thread.sleep(N)
#再删除缓存
redis.delKey(X)

是为了避免数据库还没有更新完毕,请求就直接从数据库取数据(此时是旧的数据),并存入缓存。导致后边的请求还是从缓存中获得旧的数据。

但是上述的保证事务提交完以后再进行删除缓存还有一个问题,就是如果你使用的是 Mysql 的读写分离的架构的话,那么其实主从同步之间也会有时间差。
在这里插入图片描述
此时来了两个请求,请求 A(更新操作) 和请求 B(查询操作)

  • 请求 A 更新操作,删除了 Redis
  • 请求主库进行更新操作,主库与从库进行同步数据的操作
  • 请 B 查询操作,发现 Redis 中没有数据
  • 去从库中拿去数据
  • 此时同步数据还未完成,拿到的数据是旧数据

此时的解决办法就是如果是对 Redis 进行填充数据的查询数据库操作,那么就强制将其指向主库进行查询。

在这里插入图片描述

五、Redis过期删除

1. 过期删除策略是什么?

Redis 是可以对 key 设置过期时间的,因此需要有相应的机制将已过期的键值对删除,而做这个工作的就是过期键值删除策略

每当我们对一个 key 设置了过期时间时,Redis 会把该 key 带上过期时间存储到一个过期字典(expires dict)中,也就是说「过期字典」保存了数据库中所有 key 的过期时间

过期字典存储在 redisDb 结构中,如下:

typedef struct redisDb {
    dict *dict;    /* 数据库键空间,存放着所有的键值对 */
    dict *expires; /* 键的过期时间 */
    ....
} redisDb;

过期字典数据结构结构如下:

  • 过期字典的 key 是一个指针,指向某个键对象;
  • 过期字典的 value 是一个 long long 类型的整数,这个整数保存了 key 的过期时间;

过期字典的数据结构如下图所示:
在这里插入图片描述

当我们查询一个 key 时,Redis 首先检查该 key 是否存在于过期字典中:

  • 如果不在,则正常读取键值;
  • 如果存在,则会获取该 key 的过期时间,然后与当前系统时间进行比对,如果比系统时间大,那就没有过期,否则判定该 key 已过期。

在这里插入图片描述

2. 什么是惰性删除?

惰性删除策略的做法是,不主动删除过期键,每次从数据库访问 key 时,都检测 key 是否过期,如果过期则删除该 key

惰性删除的流程图如下:

在这里插入图片描述
惰性删除策略的优点

  • 因为每次访问时,才会检查 key 是否过期,所以此策略只会使用很少的系统资源,因此,惰性删除策略对 CPU 时间最友好

惰性删除策略的缺点

  • 如果一个 key 已经过期,而这个 key 又仍然保留在数据库中,那么只要这个过期 key 一直没有被访问,它所占用的内存就不会释放,造成了一定的内存空间浪费。所以,惰性删除策略对内存不友好

3. 什么是定期删除策略?

定期删除策略的做法是,每隔一段时间「随机」从数据库中取出一定数量的 key 进行检查,并删除其中的过期key

Redis 的定期删除的流程:

  1. 从过期字典中随机抽取 20 个 key;
  2. 检查这 20 个 key 是否过期,并删除已过期的 key;
  3. 如果本轮检查的已过期 key 的数量,超过 5 个(20/4),也就是「已过期 key 的数量」占比「随机抽取 key 的数量」大于 25%,则继续重复步骤 1;如果已过期的 key 比例小于 25%,则停止继续删除过期 key,然后等待下一轮再检查。
  4. 可以看到,定期删除是一个循环的流程。那 Redis 为了保证定期删除不会出现循环过度,导致线程卡死现象,为此增加了定期删除循环流程的时间上限,默认不会超过 25ms。

定期删除的流程如下:

在这里插入图片描述
定期删除函数的运行频率,在Redis2.6版本中,规定每秒运行10次,大概100ms运行一次。在Redis2.8版本后,可以通过修改配置文件redis.conf 的 hz 选项来调整这个次数。

定期删除策略的优点

  • 通过限制删除操作执行的时长和频率,来减少删除操作对 CPU 的影响,同时也能删除一部分过期的数据减少了过期键对空间的无效占用

定期删除策略的缺点

  • 难以确定删除操作执行的时长和频率。如果执行的太频繁,就会对 CPU 不友好;如果执行的太少,那又和惰性删除一样了,过期 key 占用的内存不会及时得到释放。
    可以看到,惰性删除策略和定期删除策略都有各自的优点,所以 Re

4. Redis过期删除策略

在这里插入图片描述

Redis 选择「惰性删除+定期删除」这两种策略配和使用,以求在合理使用 CPU 时间和避免内存浪费之间取得平衡。

在这里插入图片描述

在这里插入图片描述

5. 如何设置过期时间

先说一下对 key 设置过期时间的命令。 设置 key 过期时间的命令一共有 4 个:

  • expire <key> <n>:设置 key 在 n 秒后过期,比如 expire key 100 表示设置 key 在 100 秒后过期;
  • pexpire <key> <n>:设置 key 在 n 毫秒后过期,比如 pexpire key2 100000 表示设置 key2 在 100000 毫秒(100 秒)后过期。
  • expireat <key> <n>:设置 key 在某个时间戳(精确到秒)之后过期,比如 expireat key3 1655654400 表示 key3 在时间戳 1655654400 后过期(精确到秒);
  • pexpireat <key> <n>:设置 key 在某个时间戳(精确到毫秒)之后过期,比如 pexpireat key4 1655654400000 表示 key4 在时间戳 1655654400000 后过期(精确到毫秒)

当然,在设置字符串时,也可以同时对 key 设置过期时间,共有 3 种命令:

  • set <key> <value> ex <n> :设置键值对的时候,同时指定过期时间(精确到秒);
  • set <key> <value> px <n> :设置键值对的时候,同时指定过期时间(精确到毫秒);
  • setex <key> <n> <valule> :设置键值对的时候,同时指定过期时间(精确到秒)。
    如果你想查看某个 key 剩余的存活时间,可以使用 TTL <key> 命令。
# 设置键值对的时候,同时指定过期时间位 60 秒
> setex key1 60 value1
OK

# 查看 key1 过期时间还剩多少
> ttl key1
(integer) 56
> ttl key1
(integer) 52

如果突然反悔,取消 key 的过期时间,则可以使用 PERSIST 命令。

# 取消 key1 的过期时间
> persist key1
(integer) 1

# 使用完 persist 命令之后,
# 查下 key1 的存活时间结果是 -1,表明 key1 永不过期 
> ttl key1 
(integer) -1

六、Redis内存淘汰

1. 内存淘汰策略是什么

前面说的过期删除策略,是删除已过期的 key,而当 Redis 的运行内存已经超过 Redis 设置的最大内存之后,则会使用内存淘汰策略删除符合条件的 key,以此来保障 Redis 高效的运行。

2. 如何设置 Redis 最大运行内存?

在配置文件 redis.conf 中,可以通过参数 maxmemory <bytes> 来设定最大运行内存,只有在 Redis 的运行内存达到了我们设置的最大运行内存,才会触发内存淘汰策略。

# 客户端命令方式配置和查看内存大小
127.0.0.1:6379> config get maxmemory
"maxmemory"
"0"
127.0.0.1:6379> config set maxmemory 100mb
OK
127.0.0.1:6379> config get maxmemory
"maxmemory"
"104857600"

#通过redis.conf 配置文件配置
127.0.0.1:6379> info
# Server
#...
# 配置文件路径
config_file:/opt/homebrew/etc/redis.conf
#...


# 修改内存大小
> vim /opt/homebrew/etc/redis.conf
############################## MEMORY MANAGEMENT ################################

# Set a memory usage limit to the specified amount of bytes.
# When the memory limit is reached Redis will try to remove keys
# according to the eviction policy selected (see maxmemory-policy).
#
#...
maxmemory 100mb
#...

注:若maxmemory=0则表示不做内存限制,但是对于windows系统来说,32位系统默认可使用空间是3G,因为整个系统内存是4G,需要留1G给系统运行。且淘汰策略会自动设置为noeviction,即不开启淘汰策略,当使用空间达到3G的时候,新的内存请求会报错。

3. Redis 内存淘汰策略有哪些?

在这里插入图片描述

Redis 内存淘汰策略共有八种,这八种策略大体分为「不进行数据淘汰」「进行数据淘汰」两类策略。

1、不进行数据淘汰的策略

  • noeviction(Redis3.0之后,默认的内存淘汰策略) :它表示当运行内存超过最大设置内存时,不淘汰任何数据,这时如果有新的数据写入,会报错通知禁止写入,不淘汰任何数据,但是如果没用数据写入的话,只是单纯的查询或者删除操作的话,还是可以正常工作。

2、进行数据淘汰的策略

针对「进行数据淘汰」这一类策略,又可以细分为「在设置了过期时间的数据中进行淘汰」「在所有数据范围内进行淘汰」这两类策略。

【在设置了过期时间的数据中进行淘汰】

  • volatile-random:随机淘汰设置了过期时间的任意键值
  • volatile-ttl:优先淘汰更早过期的键值。
  • volatile-lru( Least Recently Used):淘汰所有设置了过期时间的键值中,最久未使用的键值;
  • volatile-lfu(Least Frequently Used):淘汰所有设置了过期时间的键值中,最少使用的键值;

【在所有数据范围内进行淘汰】

  • allkeys-random:随机淘汰任意键值;
  • allkeys-lru:淘汰整个键值中最久未使用的键值;
  • allkeys-lfu:淘汰整个键值中最少使用的键值。

如何查看当前 Redis 使用的内存淘汰策略?

可以使用 config get maxmemory-policy 命令,来查看当前 Redis 的内存淘汰策略,命令如下:

127.0.0.1:6379> config get maxmemory-policy
1) "maxmemory-policy"
2) "noeviction"

可以看出,当前 Redis 使用的是 noeviction 类型的内存淘汰策略,它是 Redis 3.0 之后默认使用的内存淘汰策略,表示当运行内存超过最大设置内存时,不淘汰任何数据,但新增操作会报错

如何修改 Redis 内存淘汰策略?

设置内存淘汰策略有两种方法:

  • 方式一:通过config set maxmemory-policy <策略>命令设置。它的优点是设置之后立即生效,不需要重启 Redis 服务,缺点是重启 Redis 之后,设置就会失效
  • 方式二:通过修改 Redis 配置文件修改,设置maxmemory-policy <策略>它的优点是重启 Redis 服务后配置不会丢失,缺点是必须重启 Redis 服务,设置才能生效

4. LRU 算法和 LFU 算法有什么区别?

谈谈缓存淘汰的LRU和LFU算法
LFU 内存淘汰算法是 Redis 4.0 之后新增内存淘汰策略,那为什么要新增这个算法?那肯定是为了解决 LRU 算法的问题。

接下来,就看看这两个算法有什么区别?Redis 又是如何实现这两个算法的?

4.1 LRU

什么是 LRU 算法?

LRU 全称是 Least Recently Used 翻译为最近最久未使用算法, LRU是淘汰最长时间没有被使用的页面

传统 LRU 算法的实现是基于「链表」结构,链表中的元素按照操作顺序从前往后排列,最新操作的键会被移动到表头,当需要内存淘汰时,只需要删除链表尾部的元素即可,因为链表尾部的元素就代表最久未被使用的元素。
在这里插入图片描述
实现方法为双向链表+哈希表
在这里插入图片描述
LRU 缓存机制可以通过哈希表辅以双向链表实现,我们用一个哈希表和一个双向链表维护所有在缓存中的键值对。

  • 双向链表按照被使用的顺序存储了这些键值对,靠近头部的键值对是最近使用的,而靠近尾部的键值对是最久未使用的

  • 哈希表即为普通的哈希映射(HashMap),通过缓存数据的键映射到其在双向链表中的位置。

这样以来,我们首先使用哈希表进行定位,找出缓存项在双向链表中的位置,随后将其移动到双向链表的头部,即可在O(1) 的时间内完成 get 或者 put 操作。具体的方法如下:

对于 get 操作,首先判断 key 是否存在:

  • 如果 key 不存在,则返回 −1;

  • 如果 key 存在,则 key 对应的节点是最近被使用的节点。通过哈希表定位到该节点在双向链表中的位置,并将其移动到双向链表的头部,最后返回该节点的值。

对于 put 操作,首先判断 key 是否存在:

  • ① 如果 key 不存在,使用 key 和 value 创建一个新的节点,在双向链表的头部添加该节点,② 并将 key 和该节点添加进哈希表中。③ 然后判断双向链表的节点数是否超出容量,如果超出容量,则删除双向链表的尾部节点,并删除哈希表中对应的项;

  • 如果 key 存在,则与 get 操作类似,先通过哈希表定位,再将对应的节点的值更新为 value,并将该节点移到双向链表的头部。

上述各项操作中,访问哈希表的时间复杂度为O(1),在双向链表的头部添加节点、在双向链表的尾部删除节点的复杂度也为O(1)。而将一个节点移到双向链表的头部,可以分成「删除该节点」和「在双向链表的头部添加节点」两步操作,都可以在 O(1) 时间内完成。

为什么不适用单列表

使用单链表的话,可以实现头部快速插入新节点,尾部快速删除旧节点,时间复杂度都是O(1)。
但是对于·中间节点·,比如我需要节点1的值由2更新为4,这时候除了更新值,还需要将其移动到最前面,而对于单链表,它只知道下一个元素 ,并不知道上一个元素,为了得到上一个元素,它必须遍历一次链表才知道,时间复杂度为O(n),这就是为什么要用双向链表的原因。

在这里插入图片描述

Redis 并没有使用这样的方式实现 LRU 算法,因为传统的 LRU 算法存在两个问题:

  • 需要用链表管理所有的缓存数据,这会带来额外的空间开销
  • 当有数据被访问时,需要在链表上把该数据移动到头端,如果有大量数据被访问,就会带来很多链表移动操作,会很耗时,进而会降低 Redis 缓存性能。

Redis 是如何实现 LRU 算法的?

Redis 实现的是一种近似 LRU 算法,目的是为了更好的节约内存,它的实现方式是在 Redis 的对象结构体中添加一个额外的字段,用于记录此数据的最后一次访问时间

当 Redis 进行内存淘汰时,会使用随机采样的方式来淘汰数据,它是随机取 5 个值(此值可配置),然后淘汰最久没有使用的那个

Redis 实现的 LRU 算法的优点:

  • 不用为所有的数据维护一个大链表,节省了空间占用;
  • 不用在每次数据访问时都移动链表项,提升了缓存的性能;

但是 LRU 算法有一个问题,无法解决缓存污染问题,比如应用一次读取了大量的数据,而这些数据只会被读取这一次,那么这些数据会留存在 Redis 缓存中很长一段时间,造成缓存污染。

因此,在 Redis 4.0 之后引入了 LFU 算法来解决这个问题

4.2 LFU

什么是 LFU 算法?

LFU算法,全称Least frequently used,即最不经常使用。LFU算法的思想是一定时期内被访问次数最少的节点,在将来被访问到的几率也是最小的

由此可以看到,LFU强调的是访问次数,而LRU强调的是访问时间

Redis 是如何实现 LFU 算法的?

LFU有两种实现方式,一是哈希表+平衡二叉树,二是双哈希表,下面以双哈希表为例,说明LFU具体的步骤:
leetcode LFU实现思路

我们定义两个哈希表,第一个 freq_table 以频率 freq 为索引,每个索引存放一个双向链表,这个链表里存放所有使用频率为 freq 的缓存,缓存里存放三个信息,分别为键 key,值 value,以及使用频率 freq。第二个 key_table 以键值 key 为索引,每个索引存放对应缓存在 freq_table 中链表里的内存地址,这样我们就能利用两个哈希表来使得两个操作的时间复杂度均为 O(1)。同时需要记录一个当前缓存最少使用的频率 minFreq,这是为了删除操作服务的。
在这里插入图片描述

在这里插入图片描述

对于 get(key) 操作,我们能通过索引 key 在 key_table 中找到缓存在 freq_table 中的链表的内存地址,如果不存在直接返回 -1,否则我们能获取到对应缓存的相关信息,这样我们就能知道缓存的键值还有使用频率,直接返回 key 对应的值即可。

但是我们注意到 get 操作后这个缓存的使用频率加一了,所以我们需要更新缓存在哈希表 freq_table 中的位置。已知这个缓存的键 key,值 value,以及使用频率 freq,那么该缓存应该存放到 freq_table 中 freq + 1 索引下的链表中。所以我们在当前链表中 O(1)删除该缓存对应的节点,根据情况更新 minFreq 值,然后将其O(1)插入到 freq + 1 索引下的链表头完成更新。这其中的操作复杂度均为 O(1)。你可能会疑惑更新的时候为什么是插入到链表头,这其实是为了保证缓存在当前链表中从链表头到链表尾的插入时间是有序的,为下面的删除操作服务

get的步骤:获取数据—>更新freq_table位置—>更新minFreq

对于 put(key, value) 操作,我们先通过索引 key在 key_table 中查看是否有对应的缓存

  • ①如果有的话,其实操作等价于 get(key) 操作,唯一的区别就是我们需要将当前的缓存里的值更新为 value。
  • ②如果没有的话,相当于是新加入的缓存,如果缓存已经到达容量,需要先删除最近最少使用的缓存,再进行插入。

先考虑插入,由于是新插入的,所以缓存的使用频率一定是 1,所以我们将缓存的信息插入到 freq_table 中 1 索引下的列表头即可,同时更新 key_table[key] 的信息,以及更新 minFreq = 1。

put的步骤

  1. 存在key:等价于get—> 更新缓存的value
  2. 不存在key:新加入key —> 如果缓存已经到达容量,需要先删除最近最少使用的缓存

那么剩下的就是删除操作了,由于我们实时维护了 minFreq,所以我们能够知道freq_table里目前最少使用频率的索引,同时因为我们保证了链表中从链表头到链表尾的插入时间是有序的,所以 freq_table[minFreq] 的链表中链表尾的节点即为使用频率最小且插入时间最早的节点,我们删除它同时根据情况更新 minFreq ,整个时间复杂度均为 O(1)。

delete的步骤:删除freq_table[minFreq] —> 更新 minFreq

如下图展示了样例的全部操作过程:

put key=1,未存在的key
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
put key = 3,删除最少使用的缓存
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  • 11
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值