关于redis:吃透Redis面试八股文

42次阅读

共计 18122 个字符,预计需要花费 46 分钟才能阅读完成。

Redis 连环 40 问,相对够全!

Redis 是什么?

Redis(Remote Dictionary Server)是一个应用 C 语言编写的,高性能非关系型的键值对数据库。与传统数据库不同的是,Redis 的数据是存在内存中的,所以读写速度十分快,被广泛应用于缓存方向。Redis 能够将数据写入磁盘中,保障了数据的平安不失落,而且 Redis 的操作是原子性的。

Redis 优缺点?

长处

  1. 基于内存操作,内存读写速度快。
  2. 反对多种数据类型,包含 String、Hash、List、Set、ZSet 等。
  3. 反对长久化。Redis 反对 RDB 和 AOF 两种长久化机制,长久化性能能够无效地防止数据失落问题。
  4. 反对事务。Redis 的所有操作都是原子性的,同时 Redis 还反对对几个操作合并后的原子性执行。
  5. 反对主从复制。主节点会主动将数据同步到从节点,能够进行读写拆散。
  6. Redis 命令的解决是 单线程 的。Redis6.0 引入了多线程,须要留神的是,多线程用于解决网络数据的读写和协定解析,Redis 命令执行还是单线程的。

毛病

  1. 对结构化查问的反对比拟差。
  2. 数据库容量受到物理内存的限度,不适宜用作海量数据的高性能读写,因而 Redis 适宜的场景次要局限在较小数据量的操作。
  3. Redis 较难反对在线扩容,在集群容量达到下限时在线扩容会变得很简单。

Redis 为什么这么快?

  • 基于内存:Redis 是应用内存存储,没有磁盘 IO 上的开销。数据存在内存中,读写速度快。
  • IO 多路复用模型:Redis 采纳 IO 多路复用技术。Redis 应用单线程来轮询描述符,将数据库的操作都转换成了事件,不在网络 I / O 上节约过多的工夫。
  • 高效的数据结构:Redis 每种数据类型底层都做了优化,目标就是为了谋求更快的速度。

本文曾经收录到 Github 仓库,该仓库蕴含 计算机根底、Java 根底、多线程、JVM、数据库、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分布式、微服务、设计模式、架构、校招社招分享 等外围知识点,欢送 star~

Github 地址

如果拜访不了 Github,能够拜访 gitee 地址。

gitee 地址

既然 Redis 那么快,为什么不必它做主数据库,只用它做缓存?

尽管 Redis 十分快,但它也有一些局限性,不能齐全代替主数据库。有以下起因:

事务处理:Redis 只反对简略的事务处理,对于简单的事务无能为力,比方跨多个键的事务处理。

数据长久化:Redis 是内存数据库,数据存储在内存中,如果服务器解体或断电,数据可能失落。尽管 Redis 提供了数据长久化机制,但有一些限度。

数据处理:Redis 只反对一些简略的数据结构,比方字符串、列表、哈希表等。如果须要解决简单的数据结构,比方关系型数据库中的表,那么 Redis 可能不是一个好的抉择。

数据安全:Redis 没有提供像主数据库那样的平安机制,比方用户认证、访问控制等等。

因而,尽管 Redis 十分快,但它还有一些限度,不能齐全代替主数据库。所以,应用 Redis 作为缓存是一种很好的形式,能够进步应用程序的性能,并缩小数据库的负载。

讲讲 Redis 的线程模型?

Redis 基于 Reactor 模式开发了网络事件处理器,这个处理器被称为文件事件处理器。它的组成构造为 4 局部:多个套接字、IO 多路复用程序、文件事件分派器、事件处理器。因为文件事件分派器队列的生产是单线程的,所以 Redis 才叫单线程模型。

  • 文件事件处理器应用 I / O 多路复用(multiplexing)程序来同时监听多个套接字,并依据套接字目前执行的工作来为套接字关联不同的事件处理器。
  • 当被监听的套接字筹备好执行连贯 accept、read、write、close 等操作时,与操作绝对应的文件事件就会产生,这时文件事件处理器就会调用套接字之前关联好的事件处理器来解决这些事件。

尽管文件事件处理器以单线程形式运行,但通过应用 I/O 多路复用程序来监听多个套接字,文件事件处理器既实现了高性能的网络通信模型,又能够很好地与 redis 服务器中其余同样以单线程形式运行的模块进行对接,这放弃了 Redis 外部单线程设计的简略性。

Redis 利用场景有哪些?

  1. 缓存热点数据,缓解数据库的压力。
  2. 利用 Redis 原子性的自增操作,能够实现 计数器 的性能,比方统计用户点赞数、用户拜访数等。
  3. 分布式锁。在分布式场景下,无奈应用单机环境下的锁来对多个节点上的过程进行同步。能够应用 Redis 自带的 SETNX 命令实现分布式锁,除此之外,还能够应用官网提供的 RedLock 分布式锁实现。
  4. 简略的音讯队列,能够应用 Redis 本身的公布 / 订阅模式或者 List 来实现简略的音讯队列,实现异步操作。
  5. 限速器,可用于限度某个用户拜访某个接口的频率,比方秒杀场景用于避免用户疾速点击带来不必要的压力。
  6. 好友关系,利用汇合的一些命令,比方交加、并集、差集等,实现独特好友、共同爱好之类的性能。

Memcached 和 Redis 的区别?

  1. MemCached 数据结构繁多,仅用来缓存数据,而 Redis 反对多种数据类型
  2. MemCached 不反对数据长久化,重启后数据会隐没。Redis 反对数据长久化
  3. Redis 提供主从同步机制和 cluster 集群部署能力,可能提供高可用服务。Memcached 没有提供原生的集群模式,须要依附客户端实现往集群中分片写入数据。
  4. Redis 的速度比 Memcached 快很多。
  5. Redis 应用 单线程的多路 IO 复用模型 ,Memcached 应用多线程的非阻塞 IO 模型。(Redis6.0 引入了多线程 IO, 用来解决网络数据的读写和协定解析,然而命令的执行依然是单线程)
  6. value 值大小不同:Redis 最大能够达到 512M;memcache 只有 1mb。

最初给大家分享一个 Github 仓库,下面有大彬整顿的 300 多本经典的计算机书籍 PDF,包含 C 语言、C++、Java、Python、前端、数据库、操作系统、计算机网络、数据结构和算法、机器学习、编程人生 等,能够 star 一下,下次找书间接在下面搜寻,仓库继续更新中~

Github 地址

为什么要用 Redis 而不必 map/guava 做缓存?

应用自带的 map 或者 guava 实现的是 本地缓存,最次要的特点是轻量以及疾速,生命周期随着 jvm 的销毁而完结,并且在多实例的状况下,每个实例都须要各自保留一份缓存,缓存不具备一致性。

应用 redis 或 memcached 之类的称为 分布式缓存,在多实例的状况下,各实例共用一份缓存数据,缓存具备一致性。

Redis 数据类型有哪些?

根本数据类型

1、String:最罕用的一种数据类型,String 类型的值能够是字符串、数字或者二进制,但值最大不能超过 512MB。

2、Hash:Hash 是一个键值对汇合。

3、Set:无序去重的汇合。Set 提供了交加、并集等办法,对于实现独特好友、独特关注等性能特地不便。

4、List:有序可反复的汇合,底层是依赖双向链表实现的。

5、SortedSet:有序 Set。外部保护了一个 score 的参数来实现。实用于排行榜和带权重的音讯队列等场景。

非凡的数据类型

1、Bitmap:位图,能够认为是一个以位为单位数组,数组中的每个单元只能存 0 或者 1,数组的下标在 Bitmap 中叫做偏移量。Bitmap 的长度与汇合中元素个数无关,而是与基数的下限无关。

2、Hyperloglog。HyperLogLog 是用来做基数统计的算法,其长处是,在输出元素的数量或者体积十分十分大时,计算基数所需的空间总是固定的、并且是很小的。典型的应用场景是统计独立访客。

3、Geospatial:次要用于存储地理位置信息,并对存储的信息进行操作,实用场景如定位、左近的人等。

SortedSet 和 List 异同点?

相同点

  1. 都是有序的;
  2. 都能够取得某个范畴内的元素。

不同点:

  1. 列表基于链表实现,获取两端元素速度快,拜访两头元素速度慢;
  2. 有序汇合基于散列表和跳跃表实现,拜访两头元素工夫复杂度是 OlogN;
  3. 列表不能简略的调整某个元素的地位,有序列表能够(更改元素的分数);
  4. 有序汇合更耗内存。

Redis 的内存用完了会怎么?

如果达到设置的下限,Redis 的写命令会返回错误信息(然而读命令还能够失常返回)。

也能够配置内存淘汰机制,当 Redis 达到内存下限时会冲刷掉旧的内容。

Redis 如何做内存优化?

能够好好利用 Hash,list,sorted set,set 等汇合类型数据,因为通常状况下很多小的 Key-Value 能够用更紧凑的形式寄存到一起。尽可能应用散列表(hashes),散列表(是说散列表外面存储的数少)应用的内存十分小,所以你应该尽可能的将你的数据模型形象到一个散列表外面。比方你的 web 零碎中有一个用户对象,不要为这个用户的名称,姓氏,邮箱,明码设置独自的 key,而是应该把这个用户的所有信息存储到一张散列表外面。

keys 命令存在的问题?

redis 的单线程的。keys 指令会导致线程阻塞一段时间,直到执行结束,服务能力复原。scan 采纳渐进式遍历的形式来解决 keys 命令可能带来的阻塞问题,每次 scan 命令的工夫复杂度是O(1),然而要真正实现 keys 的性能,须要执行屡次 scan。

scan 的毛病:在 scan 的过程中如果有键的变动(减少、删除、批改),遍历过程可能会有以下问题:新增的键可能没有遍历到,遍历出了反复的键等状况,也就是说 scan 并不能保障残缺的遍历进去所有的键。

Redis 事务

事务的原理是将一个事务范畴内的若干命令发送给 Redis,而后再让 Redis 顺次执行这些命令。

事务的生命周期:

  1. 应用 MULTI 开启一个事务
  2. 在开启事务的时候,每次操作的命令将会被插入到一个队列中,同时这个命令并不会被真的执行
  3. EXEC 命令进行提交事务

一个事务范畴内某个命令出错不会影响其余命令的执行,不保障原子性:

127.0.0.1:6379> multi
OK
127.0.0.1:6379> set a 1
QUEUED
127.0.0.1:6379> set b 1 2
QUEUED
127.0.0.1:6379> set c 3
QUEUED
127.0.0.1:6379> exec
1) OK
2) (error) ERR syntax error
3) OK

WATCH 命令

WATCH命令能够监控一个或多个键,一旦其中有一个键被批改,之后的事务就不会执行(相似于乐观锁)。执行 EXEC 命令之后,就会主动勾销监控。

127.0.0.1:6379> watch name
OK
127.0.0.1:6379> set name 1
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set name 2
QUEUED
127.0.0.1:6379> set gender 1
QUEUED
127.0.0.1:6379> exec
(nil)
127.0.0.1:6379> get gender
(nil)

比方下面的代码中:

  1. watch name开启了对 name 这个 key 的监控
  2. 批改 name 的值
  3. 开启事务 a
  4. 在事务 a 中设置了 namegender的值
  5. 应用 EXEC 命令进提交事务
  6. 应用命令 get gender 发现不存在,即事务 a 没有执行

应用 UNWATCH 能够勾销 WATCH 命令对 key 的监控,所有监控锁将会被勾销。

Redis 事务反对隔离性吗?

Redis 是单过程程序,并且它保障在执行事务时,不会对事务进行中断,事务能够运行直到执行完所有事务队列中的命令为止。因而,Redis 的事务是总是带有隔离性的。

Redis 事务保障原子性吗,反对回滚吗?

Redis 单条命令是原子性执行的,但事务不保障原子性,且没有回滚。事务中任意命令执行失败,其余的命令仍会被执行。

长久化机制

长久化就是把 内存的数据写到磁盘中,避免服务宕机导致内存数据失落。

Redis 反对两种形式的长久化,一种是 RDB 的形式,一种是 AOF 的形式。前者会依据指定的规定定时将内存中的数据存储在硬盘上 ,而 后者在每次执行完命令后将命令记录下来。个别将两者联合应用。

RDB 形式

RDB是 Redis 默认的长久化计划。RDB 长久化时会将内存中的数据写入到磁盘中,在指定目录下生成一个 dump.rdb 文件。Redis 重启会加载 dump.rdb 文件复原数据。

bgsave是支流的触发 RDB 长久化的形式,执行过程如下:

  • 执行 BGSAVE 命令
  • Redis 父过程判断以后 是否存在正在执行的子过程 ,如果存在,BGSAVE 命令间接返回。
  • 父过程执行 fork 操作 创立子过程,fork 操作过程中父过程会阻塞。
  • 父过程 fork 实现后,父过程持续接管并解决客户端的申请 ,而 子过程开始将内存中的数据写进硬盘的临时文件
  • 当子过程写完所有数据后会 用该临时文件替换旧的 RDB 文件

Redis 启动时会读取 RDB 快照文件,将数据从硬盘载入内存。通过 RDB 形式的长久化,一旦 Redis 异样退出,就会失落最近一次长久化当前更改的数据。

触发 RDB 长久化的形式:

  1. 手动触发 :用户执行SAVEBGSAVE命令。SAVE命令执行快照的过程会阻塞所有客户端的申请,应防止在生产环境应用此命令。BGSAVE命令能够在后盾异步进行快照操作,快照的同时服务器还能够持续响应客户端的申请,因而须要手动执行快照时举荐应用 BGSAVE 命令。
  2. 被动触发

    • 依据配置规定进行主动快照,如SAVE 100 10,100 秒内至多有 10 个键被批改则进行快照。
    • 如果从节点执行全量复制操作,主节点会主动执行 BGSAVE 生成 RDB 文件并发送给从节点。
    • 默认状况下执行 shutdown 命令时,如果没有开启 AOF 长久化性能则主动执行·BGSAVE·。

长处

  1. Redis 加载 RDB 复原数据远远快于 AOF 的形式
  2. 应用独自子过程来进行长久化,主过程不会进行任何 IO 操作,保障了 Redis 的高性能

毛病

  1. RDB 形式数据无奈做到实时长久化 。因为BGSAVE 每次运行都要执行 fork 操作创立子过程,属于重量级操作,频繁执行老本比拟高。
  2. RDB 文件应用特定二进制格局保留,Redis 版本升级过程中有多个格局的 RDB 版本,存在老版本 Redis 无奈兼容新版 RDB 格局的问题

AOF 形式

AOF(append only file)长久化:以独立日志的形式记录每次写命令,Redis 重启时会从新执行 AOF 文件中的命令达到复原数据的目标。AOF 的次要作用是 解决了数据长久化的实时性,AOF 是 Redis 长久化的支流形式。

默认状况下 Redis 没有开启 AOF 形式的长久化,能够通过 appendonly 参数启用:appendonly yes。开启 AOF 形式长久化后每执行一条写命令,Redis 就会将该命令写进 aof_buf 缓冲区,AOF 缓冲区依据对应的策略向硬盘做同步操作。

默认状况下零碎 每 30 秒 会执行一次同步操作。为了避免缓冲区数据失落,能够在 Redis 写入 AOF 文件后被动要求零碎将缓冲区数据同步到硬盘上。能够通过 appendfsync 参数设置同步的机会。

appendfsync always // 每次写入 aof 文件都会执行同步,最平安最慢,不倡议配置
appendfsync everysec  // 既保证性能也保障平安,倡议配置
appendfsync no // 由操作系统决定何时进行同步操作

接下来看一下 AOF 长久化执行流程:

  1. 所有的写入命令会追加到 AOP 缓冲区中。
  2. AOF 缓冲区依据对应的策略向硬盘同步。
  3. 随着 AOF 文件越来越大,须要定期对 AOF 文件进行重写,达到压缩文件体积的目标。AOF 文件重写是把 Redis 过程内的数据转化为写命令同步到新 AOF 文件的过程。
  4. 当 Redis 服务器重启时,能够加载 AOF 文件进行数据恢复。

长处

  1. AOF 能够更好的爱护数据不失落,能够配置 AOF 每秒执行一次 fsync 操作,如果 Redis 过程挂掉,最多失落 1 秒的数据。
  2. AOF 以 append-only 的模式写入,所以没有磁盘寻址的开销,写入性能十分高。

毛病

  1. 对于同一份文件 AOF 文件比 RDB 数据快照要大。
  2. 数据恢复比较慢。

RDB 和 AOF 如何抉择?

通常来说,应该同时应用两种长久化计划,以保障数据安全。

  • 如果数据不敏感,且能够从其余中央从新生成,能够敞开长久化。
  • 如果数据比拟重要,且可能接受几分钟的数据失落,比方缓存等,只须要应用 RDB 即可。
  • 如果是用做内存数据,要应用 Redis 的长久化,倡议是 RDB 和 AOF 都开启。
  • 如果只用 AOF,优先应用 everysec 的配置抉择,因为它在可靠性和性能之间取了一个均衡。

当 RDB 与 AOF 两种形式都开启时,Redis 会优先应用 AOF 复原数据,因为 AOF 保留的文件比 RDB 文件更残缺。

Redis 有哪些部署计划?

单机版:单机部署,单机 redis 可能承载的 QPS 大略就在上万到几万不等。这种部署形式很少应用。存在的问题:1、内存容量无限 2、解决能力无限 3、无奈高可用。

主从模式:一主多从,主负责写,并且将数据复制到其它的 slave 节点,从节点负责读。所有的读申请全副走从节点。这样也能够很轻松实现程度扩容,撑持读高并发。master 节点挂掉后,须要手动指定新的 master,可用性不高,根本不必。

哨兵模式:主从复制存在不能主动故障转移、达不到高可用的问题。哨兵模式解决了这些问题。通过哨兵机制能够主动切换主从节点。master 节点挂掉后,哨兵过程会被动选举新的 master,可用性高,然而每个节点存储的数据是一样的,节约内存空间。数据量不是很多,集群规模不是很大,须要主动容错容灾的时候应用。

Redis cluster:服务端分片技术,3.0 版本开始正式提供。Redis Cluster 并没有应用一致性 hash,而是采纳 slot(槽)的概念,一共分成 16384 个槽。将申请发送到任意节点,接管到申请的节点会将查问申请发送到正确的节点上执行。次要是针对海量数据 + 高并发 + 高可用的场景,如果是海量数据,如果你的数据量很大,那么倡议就用 Redis cluster,所有主节点的容量总和就是 Redis cluster 可缓存的数据容量。

主从架构

单机的 redis,可能承载的 QPS 大略就在上万到几万不等。对于缓存来说,个别都是用来撑持读高并发的。因而架构做成主从 (master-slave) 架构,一主多从,主负责写,并且将数据复制到其它的 slave 节点,从节点负责读。所有的读申请全副走从节点。这样也能够很轻松实现程度扩容,撑持读高并发。

Redis 的复制性能是反对多个数据库之间的数据同步。主数据库能够进行读写操作,当主数据库的数据发生变化时会主动将数据同步到从数据库。从数据库个别是只读的,它会接管主数据库同步过去的数据。一个主数据库能够有多个从数据库,而一个从数据库只能有一个主数据库。

主从复制的原理?

  1. 当启动一个从节点时,它会发送一个 PSYNC 命令给主节点;
  2. 如果是从节点首次连贯到主节点,那么会触发一次全量复制。此时主节点会启动一个后盾线程,开始生成一份 RDB 快照文件;
  3. 同时还会将从客户端 client 新收到的所有写命令缓存在内存中。RDB 文件生成结束后,主节点会将 RDB 文件发送给从节点,从节点会先将 RDB 文件 写入本地磁盘,而后再从本地磁盘加载到内存中
  4. 接着主节点会将内存中缓存的写命令发送到从节点,从节点同步这些数据;
  5. 如果从节点跟主节点之间网络呈现故障,连贯断开了,会主动重连,连贯之后主节点仅会将局部缺失的数据同步给从节点。

哨兵 Sentinel

主从复制存在不能主动故障转移、达不到高可用的问题。哨兵模式解决了这些问题。通过哨兵机制能够主动切换主从节点。

客户端连贯 Redis 的时候,先连贯哨兵,哨兵会通知客户端 Redis 主节点的地址,而后客户端连贯上 Redis 并进行后续的操作。当主节点宕机的时候,哨兵监测到主节点宕机,会从新推选出某个体现良好的从节点成为新的主节点,而后通过公布订阅模式告诉其余的从服务器,让它们切换主机。

工作原理

  • 每个 Sentinel 以每秒钟一次的频率向它所晓得的 MasterSlave 以及其余 Sentinel 实例发送一个 PING命令。
  • 如果一个实例间隔最初一次无效回复 PING 命令的工夫超过指定值,则这个实例会被 Sentine 标记为主观下线。
  • 如果一个 Master 被标记为主观下线,则正在监督这个 Master 的所有 Sentinel 要以每秒一次的频率确认 Master 是否真正进入主观下线状态。
  • 当有足够数量的 Sentinel(大于等于配置文件指定值)在指定的工夫范畴内确认 Master 确实进入了主观下线状态,则 Master 会被标记为主观下线。若没有足够数量的 Sentinel 批准 Master 曾经下线,Master 的主观下线状态就会被解除。若 Master从新向 SentinelPING 命令返回无效回复,Master 的主观下线状态就会被移除。
  • 哨兵节点会选举出哨兵 leader,负责故障转移的工作。
  • 哨兵 leader 会推选出某个体现良好的从节点成为新的主节点,而后告诉其余从节点更新主节点信息。

Redis cluster

哨兵模式解决了主从复制不能主动故障转移、达不到高可用的问题,但还是存在主节点的写能力、容量受限于单机配置的问题。而 cluster 模式实现了 Redis 的分布式存储,每个节点存储不同的内容,解决主节点的写能力、容量受限于单机配置的问题。

Redis cluster 集群节点最小配置 6 个节点以上(3 主 3 从),其中主节点提供读写操作,从节点作为备用节点,不提供申请,只作为故障转移应用。

Redis cluster 采纳 虚构槽分区,所有的键依据哈希函数映射到 0~16383 个整数槽内,每个节点负责保护一部分槽以及槽所映射的键值数据。

工作原理:

  1. 通过哈希的形式,将数据分片,每个节点均分存储肯定哈希槽 (哈希值) 区间的数据,默认调配了 16384 个槽位
  2. 每份数据分片会存储在多个互为主从的多节点上
  3. 数据写入先写主节点,再同步到从节点(反对配置为阻塞同步)
  4. 同一分片多个节点间的数据不放弃一致性
  5. 读取数据时,当客户端操作的 key 没有调配在该节点上时,redis 会返回转向指令,指向正确的节点
  6. 扩容时时须要须要把旧节点的数据迁徙一部分到新节点

在 redis cluster 架构下,每个 redis 要放开两个端口号,比方一个是 6379,另外一个就是 加 1w 的端口号,比方 16379。

16379 端口号是用来进行节点间通信的,也就是 cluster bus 的货色,cluster bus 的通信,用来进行故障检测、配置更新、故障转移受权。cluster bus 用了另外一种二进制的协定,gossip 协定,用于节点间进行高效的数据交换,占用更少的网络带宽和解决工夫。

长处:

  • 无核心架构,反对动静扩 容;
  • 数据依照 slot 存储散布在多个节点,节点间数据共享,可动静调整数据分布
  • 高可用性 。局部节点不可用时,集群仍可用。集群模式可能实现主动故障转移(failover),节点之间通过gossip 协定替换状态信息,用投票机制实现 SlaveMaster的角色转换。

毛病:

  • 不反对批量操作(pipeline)。
  • 数据通过异步复制,不保证数据的强一致性
  • 事务操作反对无限 ,只反对多key 在同一节点上的事务操作,当多个 key 散布于不同的节点上时无奈应用事务性能。
  • key作为数据分区的最小粒度,不能将一个很大的键值对象如 hashlist 等映射到不同的节点。
  • 不反对多数据库空间,单机下的 Redis 能够反对到 16 个数据库,集群模式下只能应用 1 个数据库空间。
  • 只能应用 0 号数据库。

哈希分区算法有哪些?

节点取余分区。应用特定的数据,如 Redis 的键或用户 ID,对节点数量 N 取余:hash(key)%N 计算出哈希值,用来决定数据映射到哪一个节点上。
长处是简略性。扩容时通常采纳翻倍扩容,防止数据映射全副被打乱导致全量迁徙的状况。

一致性哈希分区。为零碎中每个节点调配一个 token,范畴个别在 0~232,这些 token 形成一个哈希环。数据读写执行节点查找操作时,先依据 key 计算 hash 值,而后顺时针找到第一个大于等于该哈希值的 token 节点。
这种形式相比节点取余最大的益处在于退出和删除节点只影响哈希环中相邻的节点,对其余节点无影响。

虚构槽分区,所有的键依据哈希函数映射到 0~16383 整数槽内,计算公式:slot=CRC16(key)&16383。每一个节点负责保护一部分槽以及槽所映射的键值数据。Redis Cluser 采纳虚构槽分区算法。

过期键的删除策略?

1、被动删除。在拜访 key 时,如果发现 key 曾经过期,那么会将 key 删除。

2、被动删除。定时清理 key,每次清理会顺次遍历所有 DB,从 db 随机取出 20 个 key,如果过期就删除,如果其中有 5 个 key 过期,那么就持续对这个 db 进行清理,否则开始清理下一个 db。

3、内存不够时清理。Redis 有最大内存的限度,通过 maxmemory 参数能够设置最大内存,当应用的内存超过了设置的最大内存,就要进行内存开释,在进行内存开释的时候,会依照配置的淘汰策略清理内存。

内存淘汰策略有哪些?

当 Redis 的内存超过最大容许的内存之后,Redis 会触发内存淘汰策略,删除一些不罕用的数据,以保障 Redis 服务器失常运行。

Redisv4.0 前提供 6 种数据淘汰策略

  • volatile-lru:LRU(Least Recently Used),最近应用。利用 LRU 算法移除设置了过期工夫的 key
  • allkeys-lru:当内存不足以包容新写入数据时,从数据集中移除最近起码应用的 key
  • volatile-ttl:从已设置过期工夫的数据集中筛选将要过期的数据淘汰
  • volatile-random:从已设置过期工夫的数据集中任意抉择数据淘汰
  • allkeys-random:从数据集中任意抉择数据淘汰
  • no-eviction:禁止删除数据,当内存不足以包容新写入数据时,新写入操作会报错

Redisv4.0 后减少以下两种

  • volatile-lfu:LFU,Least Frequently Used,起码应用,从已设置过期工夫的数据集中筛选最不常常应用的数据淘汰。
  • allkeys-lfu:当内存不足以包容新写入数据时,从数据集中移除最不常常应用的 key。

内存淘汰策略能够通过配置文件来批改,相应的配置项是maxmemory-policy,默认配置是noeviction

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

1、先删除缓存再更新数据库

进行更新操作时,先删除缓存,而后更新数据库,后续的申请再次读取时,会从数据库读取后再将新数据更新到缓存。

存在的问题:删除缓存数据之后,更新数据库实现之前,这个时间段内如果有新的读申请过去,就会从数据库读取旧数据从新写到缓存中,再次造成不统一,并且后续读的都是旧数据。

2、先更新数据库再删除缓存

进行更新操作时,先更新 MySQL,胜利之后,删除缓存,后续读取申请时再将新数据回写缓存。

存在的问题:更新 MySQL 和删除缓存这段时间内,申请读取的还是缓存的旧数据,不过等数据库更新实现,就会复原统一,影响绝对比拟小。

3、异步更新缓存

数据库的更新操作实现后不间接操作缓存,而是把这个操作命令封装成音讯扔到音讯队列中,而后由 Redis 本人去生产更新数据,音讯队列能够保证数据操作程序一致性,确保缓存零碎的数据失常。

以上几个计划都不完满,须要依据业务需要,评估哪种计划影响较小,而后抉择相应的计划。

缓存常见问题

缓存穿透

缓存穿透是指查问一个 不存在的数据,因为缓存是不命中时被动写的,如果从 DB 查不到数据则不写入缓存,这将导致这个不存在的数据每次申请都要到 DB 去查问,失去了缓存的意义。在流量大时,可能 DB 就挂掉了。

怎么解决?

  1. 缓存空值,不会查数据库。
  2. 采纳 布隆过滤器 ,将所有可能存在的数据哈希到一个足够大的bitmap 中,查问不存在的数据会被这个 bitmap 拦挡掉,从而防止了对 DB 的查问压力。

布隆过滤器的原理:当一个元素被退出汇合时,通过 K 个哈希函数将这个元素映射成一个位数组中的 K 个点,把它们置为 1。查问时,将元素通过哈希函数映射之后会失去 k 个点,如果这些点有任何一个 0,则被检元素肯定不在,间接返回;如果都是 1,则查问元素很可能存在,就会去查问 Redis 和数据库。

布隆过滤器个别用于在大数据量的汇合中断定某元素是否存在。

缓存雪崩

缓存雪崩是指在咱们设置缓存时采纳了雷同的过期工夫,导致缓存在某一时刻同时生效,申请全副转发到 DB,DB 刹时压力过重挂掉。

解决办法:

  1. 在原有的生效工夫根底上 减少一个随机值,使得过期工夫扩散一些。这样每一个缓存的过期工夫的反复率就会升高,就很难引发个体生效的事件。
  2. 加锁排队能够起到缓冲的作用 ,避免大量的申请同时操作数据库,但它的毛病是 减少了零碎的响应工夫 升高了零碎的吞吐量,就义了一部分用户体验。当缓存未查问到时,对要申请的 key 进行加锁,只容许一个线程去数据库中查,其余线程等待排队。
  3. 设置二级缓存。二级缓存指的是除了 Redis 自身的缓存,再设置一层缓存,当 Redis 生效之后,先去查问二级缓存。例如能够设置一个本地缓存,在 Redis 缓存生效的时候先去查问本地缓存而非查询数据库。

缓存击穿

缓存击穿:大量的申请同时查问一个 key 时,此时这个 key 正好生效了,就会导致大量的申请都落到数据库。缓存击穿是查问缓存中生效的 key,而缓存穿透是查问不存在的 key。

解决办法:

1、加互斥锁。在并发的多个申请中,只有第一个申请线程能拿到锁并执行数据库查问操作,其余的线程拿不到锁就阻塞等着,等到第一个线程将数据写入缓存后,间接走缓存。能够应用 Redis 分布式锁实现,代码如下:

public String get(String key) {String value = redis.get(key);
    if (value == null) { // 缓存值过期
        String unique_key = systemId + ":" + key;
        // 设置 30s 的超时
        if (redis.set(unique_key, 1, 'NX', 'PX', 30000) == 1) {  // 设置胜利
            value = db.get(key);
            redis.set(key, value, expire_secs);
            redis.del(unique_key);
        } else {  // 其余线程曾经到数据库取值并回写到缓存了,能够重试获取缓存值
            sleep(50);
            get(key);  // 重试
        }
    } else {return value;}
}

2、热点数据不过期。间接将缓存设置为不过期,而后由定时工作去异步加载数据,更新缓存。这种形式实用于比拟极其的场景,例如流量特地特地大的场景,应用时须要思考业务能承受数据不统一的工夫,还有就是异常情况的解决,保障缓存能够定时刷新。

缓存预热

缓存预热就是零碎上线后,将相干的缓存数据间接加载到缓存零碎。这样就能够防止在用户申请的时候,先查询数据库,而后再将数据缓存的问题!用户间接查问当时被预热的缓存数据!

解决方案:

  1. 间接写个缓存刷新页面,上线时手工操作一下;
  2. 数据量不大,能够在我的项目启动的时候主动进行加载;
  3. 定时刷新缓存;

缓存降级

当访问量剧增、服务呈现问题(如响应工夫慢或不响应)或非核心服务影响到外围流程的性能时,依然须要保障服务还是可用的,即便是有损服务。零碎能够依据一些要害数据进行主动降级,也能够配置开关实现人工降级。

缓存降级的最终目标是保障外围服务可用,即便是有损的。而且有些服务是无奈降级的(如退出购物车、结算)。

在进行降级之前要对系统进行梳理,看看零碎是不是能够丢卒保帅;从而梳理出哪些必须誓死爱护,哪些可降级;比方能够参考日志级别设置预案:

  1. 个别:比方有些服务偶然因为网络抖动或者服务正在上线而超时,能够主动降级;
  2. 正告:有些服务在一段时间内成功率有稳定(如在 95~100% 之间),能够主动降级或人工降级,并发送告警;
  3. 谬误:比方可用率低于 90%,或者数据库连接池被打爆了,或者访问量忽然猛增到零碎能接受的最大阀值,此时能够依据状况主动降级或者人工降级;
  4. 严重错误:比方因为非凡起因数据谬误了,此时须要紧急人工降级。

服务降级的目标,是为了避免 Redis 服务故障,导致数据库跟着一起产生雪崩问题。因而,对于不重要的缓存数据,能够采取服务降级策略,例如一个比拟常见的做法就是,Redis 呈现问题,不去数据库查问,而是间接返回默认值给用户。

Redis 怎么实现音讯队列?

应用 list 类型保留数据信息,rpush 生产音讯,lpop 生产音讯,当 lpop 没有音讯时,能够 sleep 一段时间,而后再查看有没有信息,如果不想 sleep 的话,能够应用 blpop, 在没有信息的时候,会始终阻塞,直到信息的到来。

BLPOP queue 0  // 0 示意不限度等待时间

BLPOP 和 LPOP 命令类似,惟一的区别就是当列表没有元素时 BLPOP 命令会始终阻塞连贯,直到有新元素退出。

redis 能够通过 pub/sub主题订阅模式 实现一个生产者,多个消费者,当然也存在肯定的毛病,当消费者下线时,生产的音讯会失落。

PUBLISH channel1 hi
SUBSCRIBE channel1
UNSUBSCRIBE channel1 // 退订通过 SUBSCRIBE 命令订阅的频道。

PSUBSCRIBE channel?* 依照规定订阅。
PUNSUBSCRIBE channel?* 退订通过 PSUBSCRIBE 命令依照某种规定订阅的频道。其中订阅规定要进行严格的字符串匹配,PUNSUBSCRIBE *无奈退订 channel?* 规定。

Redis 怎么实现延时队列

应用 sortedset,拿工夫戳作为 score,音讯内容作为 key,调用 zadd 来生产音讯,消费者用 zrangebyscore 指令获取 N 秒之前的数据轮询进行解决。

pipeline 的作用?

redis 客户端执行一条命令分 4 个过程:发送命令、命令排队、命令执行、返回后果。应用 pipeline 能够批量申请,批量返回后果,执行速度比逐条执行要快。

应用 pipeline 组装的命令个数不能太多,不然数据量过大,减少客户端的等待时间,还可能造成网络阻塞,能够将大量命令的拆分多个小的 pipeline 命令实现。

原生批命令(mset 和 mget)与 pipeline 比照:

  1. 原生批命令是原子性,pipeline 非原子性 。pipeline 命令中途异样退出,之前执行胜利的命令 不会回滚
  2. 原生批命令只有一个命令,但 pipeline 反对多命令

LUA 脚本

Redis 通过 LUA 脚本创立具备原子性的命令:当 lua 脚本命令正在运行的时候,不会有其余脚本或 Redis 命令被执行,实现组合命令的原子操作。

在 Redis 中执行 Lua 脚本有两种办法:evalevalshaeval 命令应用内置的 Lua 解释器,对 Lua 脚本进行求值。

// 第一个参数是 lua 脚本,第二个参数是键名参数个数,剩下的是键名参数和附加参数
> eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second
1) "key1"
2) "key2"
3) "first"
4) "second"

lua 脚本作用

1、Lua 脚本在 Redis 中是原子执行的,执行过程两头不会插入其余命令。

2、Lua 脚本能够将多条命令一次性打包,无效地缩小网络开销。

利用场景

举例:限度接口拜访频率。

在 Redis 保护一个接口拜访次数的键值对,key是接口名称,value是拜访次数。每次拜访接口时,会执行以下操作:

  • 通过 aop 拦挡接口的申请,对接口申请进行计数,每次进来一个申请,相应的接口拜访次数 count 加 1,存入 redis。
  • 如果是第一次申请,则会设置 count=1,并设置过期工夫。因为这里set()expire()组合操作不是原子操作,所以引入 lua 脚本,实现原子操作,防止并发拜访问题。
  • 如果给定工夫范畴内超过最大拜访次数,则会抛出异样。
private String buildLuaScript() {
    return "local c" +
        "\nc = redis.call('get',KEYS[1])" +
        "\nif c and tonumber(c) > tonumber(ARGV[1]) then" +
        "\nreturn c;" +
        "\nend" +
        "\nc = redis.call('incr',KEYS[1])" +
        "\nif tonumber(c) == 1 then" +
        "\nredis.call('expire',KEYS[1],ARGV[2])" +
        "\nend" +
        "\nreturn c;";
}

String luaScript = buildLuaScript();
RedisScript<Number> redisScript = new DefaultRedisScript<>(luaScript, Number.class);
Number count = redisTemplate.execute(redisScript, keys, limit.count(), limit.period());

PS:这种接口限流的实现形式比较简单,问题也比拟多,个别不会应用,接口限流用的比拟多的是令牌桶算法和漏桶算法。

什么是 RedLock?

Redis 官网站提出了一种权威的基于 Redis 实现分布式锁的形式名叫 Redlock,此种形式比原先的单节点的办法更平安。它能够保障以下个性:

  1. 平安个性:互斥拜访,即永远只有一个 client 能拿到锁
  2. 防止死锁:最终 client 都可能拿到锁,不会呈现死锁的状况,即便本来锁住某资源的 client 挂掉了
  3. 容错性:只有大部分 Redis 节点存活就能够失常提供服务

Redis 大 key 怎么解决?

通常咱们会将含有较大数据或含有大量成员、列表数的 Key 称之为大 Key。

以下是对各个数据类型大 key 的形容:

  • value 是 STRING 类型,它的值超过 5MB
  • value 是 ZSET、Hash、List、Set 等汇合类型时,它的成员数量超过 1w 个

上述的定义并不相对,次要是依据 value 的成员数量和大小来确定,依据业务场景确定规范。

怎么解决:

  1. 当 vaule 是 string 时,能够应用序列化、压缩算法将 key 的大小管制在正当范畴内,然而序列化和反序列化都会带来更多工夫上的耗费。或者将 key 进行拆分,一个大 key 分为不同的局部,记录每个局部的 key,应用 multiget 等操作实现事务读取。
  2. 当 value 是 list/set 等汇合类型时,依据预估的数据规模来进行分片,不同的元素计算后分到不同的片。

Redis 常见性能问题和解决方案?

  1. Master 最好不要做任何长久化工作,包含内存快照和 AOF 日志文件,特地是不要启用内存快照做长久化。
  2. 如果数据比拟要害,某个 Slave 开启 AOF 备份数据,策略为每秒同步一次。
  3. 为了主从复制的速度和连贯的稳定性,Slave 和 Master 最好在同一个局域网内。
  4. 尽量避免在压力较大的主库上减少从库
  5. Master 调用 BGREWRITEAOF 重写 AOF 文件,AOF 在重写的时候会占大量的 CPU 和内存资源,导致服务 load 过高,呈现短暂服务暂停景象。
  6. 为了 Master 的稳定性,主从复制不要用图状构造,用单向链表构造更稳固,即主从关系为:Master<–Slave1<–Slave2<–Slave3…,这样的构造也不便解决单点故障问题,实现 Slave 对 Master 的替换,也即,如果 Master 挂了,能够立马启用 Slave1 做 Master,其余不变。

说说为什么 Redis 过期了为什么内存没开释?

第一种状况,可能是笼罩之前的 key,导致 key 过期工夫产生了扭转。

当一个 key 在 Redis 中曾经存在了,然而因为一些误操作使得 key 过期工夫产生了扭转,从而导致这个 key 在应该过期的工夫内并没有过期,从而造成内存的占用。

第二种状况是,Redis 过期 key 的解决策略导致内存没开释。

个别 Redis 对过期 key 的解决策略有两种:惰性删除和定时删除。

先说惰性删除的状况

当一个 key 曾经确定设置了 xx 秒过期同时两头也没有批改它,xx 秒之后它的确曾经过期了,然而惰性删除的策略它并不会马上删除这个 key,而是当再次读写这个 key 时它才会去查看是否过期,如果过期了就会删除这个 key。也就是说,惰性删除策略下,就算 key 过期了,也不会立即开释内容,要等到下一次读写这个 key 才会删除 key。

而定时删除会在肯定工夫内被动淘汰一部分曾经过期的数据,默认的工夫是每 100ms 过期一次。因为定时删除策略每次只会淘汰一部分过期 key,而不是所有的过期 key,如果 redis 中数据比拟多的话要是一次性全量删除对服务器的压力比拟大,每一次只挑一批进行删除,所以很可能呈现局部曾经过期的 key 并没有及时的被清理掉,从而导致内存没有即时被开释。

Redis 忽然变慢,有哪些起因?

  1. 存在 bigkey。如果 Redis 实例中存储了 bigkey,那么在淘汰删除 bigkey 开释内存时,也会耗时比拟久。应该防止存储 bigkey,升高开释内存的耗时。
  2. 如果 Redis 实例 设置了内存下限 maxmemory,有可能导致 Redis 变慢。当 Redis 内存达到 maxmemory 后,每次写入新的数据之前,Redis 必须先从实例中踢出一部分数据,让整个实例的内存维持在 maxmemory 之下,而后能力把新数据写进来。
  3. 开启了内存大页。当 Redis 在执行后盾 RDB 和 AOF rewrite 时,采纳 fork 子过程的形式来解决。但主过程 fork 子过程后,此时的主过程仍旧是能够接管写申请的,而进来的写申请,会采纳 Copy On Write(写时复制)的形式操作内存数据。

    什么是写时复制?

    这样做的益处是,父过程有任何写操作,并不会影响子过程的数据长久化。

    不过,主过程在拷贝内存数据时,会波及到新内存的申请,如果此时操作系统开启了内存大页,那么在此期间,客户端即使只批改 10B 的数据,Redis 在申请内存时也会以 2MB 为单位向操作系统申请,申请内存的耗时变长,进而导致每个写申请的提早减少,影响到 Redis 性能。

    解决方案就是敞开内存大页机制。

  4. 应用了 Swap。操作系统为了缓解内存不足对应用程序的影响,容许把一部分内存中的数据换到磁盘上,以达到应用程序对内存应用的缓冲,这些内存数据被换到磁盘上的区域,就是 Swap。当内存中的数据被换到磁盘上后,Redis 再拜访这些数据时,就须要从磁盘上读取,拜访磁盘的速度要比拜访内存慢几百倍。尤其是针对 Redis 这种对性能要求极高、性能极其敏感的数据库来说,这个操作延时是无奈承受的。解决方案就是减少机器的内存,让 Redis 有足够的内存能够应用。或者整顿内存空间,开释出足够的内存供 Redis 应用
  5. 网络带宽过载。网络带宽过载的状况下,服务器在 TCP 层和网络层就会呈现数据包发送提早、丢包等状况。Redis 的高性能,除了操作内存之外,就在于网络 IO 了,如果网络 IO 存在瓶颈,那么也会重大影响 Redis 的性能。解决方案:1、及时确认占满网络带宽 Redis 实例,如果属于失常的业务拜访,那就须要及时扩容或迁徙实例了,防止因为这个实例流量过大,影响这个机器的其余实例。2、运维层面,须要对 Redis 机器的各项指标减少监控,包含网络流量,在网络流量达到肯定阈值时提前报警,及时确认和扩容。
  6. 频繁短连贯。频繁的短连贯会导致 Redis 大量工夫消耗在连贯的建设和开释上,TCP 的三次握手和四次挥手同样也会减少拜访提早。利用应该应用长连贯操作 Redis,防止频繁的短连贯。

为什么 Redis 集群的最大槽数是 16384 个?

Redis Cluster 采纳数据分片机制,定义了 16384 个 Slot 槽位,集群中的每个 Redis 实例负责保护一部分槽以及槽所映射的键值数据。

Redis 每个节点之间会定期发送 ping/pong 音讯(心跳包蕴含了其余节点的数据),用于替换数据信息。

Redis 集群的节点会依照以下规定发 ping 音讯:

  • (1)每秒会随机选取 5 个节点,找出最久没有通信的节点发送 ping 音讯
  • (2)每 100 毫秒都会扫描本地节点列表,如果发现节点最近一次承受 pong 音讯的工夫大于 cluster-node-timeout/2 则立即发送 ping 音讯

心跳包的音讯头外面有个 myslots 的 char 数组,是一个 bitmap,每一个位代表一个槽,如果该位为 1,示意这个槽是属于这个节点的。

接下来,解答为什么 Redis 集群的最大槽数是 16384 个,而不是 65536 个。

1、如果采纳 16384 个插槽,那么心跳包的音讯头占用空间 2KB(16384/8);如果采纳 65536 个插槽,那么心跳包的音讯头占用空间 8KB (65536/8)。可见采纳 65536 个插槽,发送心跳信息的音讯头达 8k,比拟节约带宽

2、个别状况下一个 Redis 集群 不会有超过 1000 个 master 节点,太多可能导致网络拥挤。

3、哈希槽是通过一张 bitmap 的模式来保留的,在传输过程中,会对 bitmap 进行压缩。bitmap 的填充率越低,压缩率 越高。其中 bitmap 填充率 = slots / N (N 示意节点数)。所以,插槽数越低,填充率会升高,压缩率会进步。

正文完
 0