面试 Redis
Kiml Lv5
  • 前言
    ❗表示必掌握,❔表示基本不会问

  • 更新

1
24-06-24 初始记录

什么是 Redis

Redis(Remote Dictionary Server) 是一个使用 C 语言编写的,开源的(BSD 许可)高性能非关系型(NoSQL)的键值对数据库。

Redis 可以存储键和五种不同类型的值之间的映射。键的类型只能为字符串,值支持五种数据类型:字符串、列表、集合、散列表、有序集合。

什么是 NoSQL?

NoSQL 的全称是 Not-Only SQL,指的是非关系型数据库,它是关系型数据库的补充,没有表与表之间的关系,主要用于海量数据的处理问题。

除了 Redis,你还了解其它的 NoSQL 吗?

nosql 数据库有很多,比如 HBase、MongoDB、Memcached 等,Redis 相比于其他的 nosql 而言,效率高,数据结构比较丰富。

  • MongoDB

    • 高性能、无模式的文档型数据库 (一个文档相当于关系数据库中的一条记录,格式是 xml 或者 json 等),支持二级索引,非常适合文档化格式的存储及查询。MongoDB 的官方定位是通用数据库,确实和 MySQL 有些像,现在也很流行,但它还是有事务、join 等短板,在事务、复杂查询应用下无法取代关系型数据库。但 MongoDB 更注重庞大数据的存储和操作,但不适合用于临时存储的缓存,如果将数据做缓存使用,还是 Redis 性能更高。
  • Redis

    • 内存型 Key/Value 系统,读写性能非常好,支持操作原子性,很适合用来做高速缓存。
  • HBase

    • 存储容量大,一个表可以容纳上亿行、上百万列,可应对超大数据量要求扩展简单的需求。

Redis 有哪些优缺点?

优点

  • 读写性能优异。Redis 能读的速度是 110000 次/s,写的速度是 81000 次/s。

  • 支持数据持久化。支持 AOF 和 RDB 两种持久化方式。

  • 支持事务。Redis 的所有操作都是原子性的,同时 Redis 还支持对几个操作合并后的原子性执行。

  • 数据结构丰富。除了支持 string 类型的 value 外还支持 hash、set、zset、list 等数据结构。

  • 支持主从复制。主机会自动将数据同步到从机,可以进行读写分离。

缺点

  • 数据库容量受到物理内存的限制,不能用作海量数据的高性能读写。因此 Redis 适合的场景主要局限在较小数据量的高性能操作和运算上。

  • Redis 不具备自动容错和恢复功能。主机从机的宕机都会导致前端部分读写请求失败,需要等待机器重启或者手动切换前端的 IP 才能恢复。

  • 数据不同步。主机宕机,宕机前有部分数据未能及时同步到从机,切换 IP 后还会引入数据不一致的问题,降低了系统的可用性。

  • Redis 较难支持在线扩容,在集群容量达到上限时在线扩容会变得很复杂。为避免这一问题,运维人员在系统上线时必须确保有足够的空间,这对资源造成了很大的浪费。

为什么要用 Redis/为什么要用缓存?

高性能
假如用户第一次访问数据库中的某些数据。这个过程会比较慢,因为是从硬盘上读取的。将该用户访问的数据存在数缓缓存中,这样下一次再访问这些数据的时候就可以直接从缓存中获取了。操作缓存就是直接操作内存,所以速度相当快。如果数据库中的对应数据改变的之后,同步改变缓存中相应的数据即可!
高并发
直接操作缓存能够承受的请求是远远大于直接访问数据库的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。

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

缓存分为本地缓存和分布式缓存。以 Java 为例,使用自带的 map 或者 guava 实现的是本地缓存,最主要的特点是轻量以及快速,生命周期随着 jvm 的销毁而结束,并且在多实例的情况下,每个实例都需要各自保存一份缓存,缓存不具有一致性。

使用 redis 或 memcached 之类的称为分布式缓存,在多实例的情况下,各实例共用一份缓存数据,缓存具有一致性。缺点是需要保持 redis 或 memcached 服务的高可用,整个程序架构上较为复杂。

Redis 与 Memcached 的区别是什么?为什么不选 Memcached?

  1. memcached 所有的值均是简单的字符串,且只支持 k/v 类型,redis 作为其替代者,支持更为丰富的数据类型。

  2. redis 可以持久化其数据,memcached 只用做缓存提升性能,不能做持久化。

  3. memcached 存储数据有限制:1M 【大于 1M,认为就行分割】(内存碎片)

  4. memcached 集群数据没有复制和同步机制(崩溃不会影响程序,会从数据库中取数据)。

  5. memcached 内存不能及时回收,它只有 LRU(算法) 这一种方式,而 redis 有多种内存回收方式。

Redis 为什么这么快?

  1. 完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。数据存在内存中,类似于 HashMap,HashMap 的优势就是查找和操作的时间复杂度都是 O(1);

  2. 数据结构简单,对数据操作也简单,Redis 中的数据结构是专门进行设计的;

  3. 采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;

  4. 使用多路 I/O 复用模型,非阻塞 IO;

  5. 使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis 直接自己构建了 VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求;

Redis 的主要缺点是什么?

  1. 内链:[[面试-08 Redis#在工作中,如果数据库修改了数据,那就跟 redis 数据不同步了,此时该办?(缓存双写一致性问题)|缓存双写一致性问题]]
    外链:缓存双写一致性问题

  2. 内链:[[面试-08 Redis#缓存雪崩|缓存雪崩问题]]
    外链:缓存雪崩问题

  3. 内链:[[面试-08 Redis#缓存穿透|缓存穿透问题]]
    外链:缓存穿透问题

  4. 内链:[[面试-08 Redis#那怎么解决缓存的并发竞争问题?比如多个子系统去set一个key但最后执行顺序和我们期望顺序不一样。|缓存的并发竞争问题]]
    外链:缓存的并发竞争问题

数据类型

Redis 的数据类型有哪些?它的数据操作是怎么样的?

  • 数据类型

    • string、hash、list(有序、可重复)、set(无序、不可重复)、zset(不可重复,基于 score 实现排序)
  • 数据操作

    • Redis 中的数据存储方式是 key-value 形式
    • Redis 不能保存 JAVA 对象,需要转为 JSON 对象后存入 Redis
数据类型 可以存储的值 操作 应用场景
String 字符串、整数或浮点数 1.对整个字符串或者字符串的其中一部分执行操作 2.对整数和浮点数执行自增或自减操作 做简单的键值对缓存
List 列表 1.从两端压入或弹出元素 2.对单个或多个元素进行修剪,只保留一个范围内的元素 存储一些列表型的数据结构,类似粉丝列表、文章的评论列表之类的数据
Set 无序集合 1.添加、获取、移除单个元素 2.检查一个元素是否在集合中 3.计算交集、并集、差集 从集合里面随机获取元素 交集、并集、差集的操作,比如两人的粉丝列表做交集
Hash 包含键值对的无序散列表 1.添加、获取、移除单个键值对 2.获取所有键值对 3.检查某个键是否存在 结构化的数据,比如一个对象
ZSet 有序集合 1.添加、获取、移除元素 2.根据分值范围或成员来获取元素 3.计算一个键的排名 去重,但可以排序,如获取前几名用户

Redis 的应用场景

  1. 计数器

  • 可以对 String 进行自增自减运算,从而实现计数器功能。Redis 这种内存型数据库的读写性能非常高,很适合存储频繁读写的计数量。

  1. 缓存

  • 将热点数据放到内存中,设置内存的最大使用量以及淘汰策略来保证缓存的命中率。

  1. 会话缓存

  • 可以使用 Redis 来统一存储多台应用服务器的会话信息。当应用服务器不再存储用户的会话信息,也就不再具有状态,一个用户可以请求任意一个应用服务器,从而更容易实现高可用性以及可伸缩性。

  1. 全页缓存(FPC)

  • 除基本的会话 token 之外,Redis 还提供很简便的 FPC 平台。以 Magento 为例,Magento 提供一个插件来使用 Redis 作为全页缓存后端。此外,对 WordPress 的用户来说,Pantheon 有一个非常好的插件 wp-redis,这个插件能帮助你以最快速度加载你曾浏览过的页面。

  1. 查找表

  • 例如 DNS 记录就很适合使用 Redis 进行存储。查找表和缓存类似,也是利用了 Redis 快速的查找特性。但是查找表的内容不能失效,而缓存的内容可以失效,因为缓存不作为可靠的数据来源。

  1. 消息队列 (发布/订阅功能)

  • List 是一个双向链表,可以通过 lpush 和 rpop 写入和读取消息。不过最好使用 Kafka、RabbitMQ 等消息中间件。

  1. 分布式锁实现

  • 在分布式场景下,无法使用单机环境下的锁来对多个节点上的进程进行同步。可以使用 Redis 自带的 SETNX 命令实现分布式锁,除此之外,还可以使用官方提供的 RedLock 分布式锁实现。

  1. 其它

  • Set 可以实现交集、并集等操作,从而实现共同好友等功能。ZSet 可以实现有序性操作,从而实现排行榜等功能。

Redis 应用场景(根据类型)

  • String

    • 适合最简单的 k-v 存储,类似于 memcached 的存储结构,短信验证码,配置信息等,就用这种类型来存储。
  • Hash

    • 一般 key 为 ID 或者唯一标示,value 对应的就是详情了。如商品详情,个人信息详情,新闻详情等。
  • List

    • 因为 list 是有序的,比较适合存储一些有序且数据相对固定的数据。如省市区表、字典表等。因为 list 是有序的,适合根据写入的时间来排序,如:最新的***,消息队列等。
  • Set

    • 可以简单的理解为 ID-List 的模式,如微博中一个人有哪些好友,set 最牛的地方在于,可以对两个 set 提供交集、并集、差集操作。例如:查找两个人共同的好友等。
  • ZSet

    • 自动会根据 score 的值进行排序。比较适合类似于 top 10 等不根据插入的时间来排序的数据。

Redis 持久化

什么是 Redis 持久化?

  • 持久化就是把内存的数据写到磁盘中去,防止服务宕机了内存数据丢失。

Redis 的持久化机制是什么?各自的优缺点?

RDB

  • 简述

    • RDB 是二进制快照形式,数据体量小,保存效率高,但丢失风险也较大,因为它是定时定量更改才会自动持久化,无法实时存储,如果在快照之前丢失,则无法找回。
  • 优点

    1. 只有一个文件 dump.rdb,方便持久化。
    2. 容灾性好,一个文件可以保存到安全的磁盘。
    3. 性能最大化,fork 子进程来完成写操作,让主进程继续处理命令,所以是 IO 最大化。使用单独子进程来进行持久化,主进程不会进行任何 IO 操作,保证了 redis 的高性能
    4. 相对于数据集大时,比 AOF 的启动效率更高。
  • 缺点

    1. 数据安全性低。RDB 是间隔一段时间进行持久化,如果持久化之间 redis 发生故障,会发生数据丢失。所以这种方式更适合数据要求不严谨的时候)

AOF

  • 简述

    • AOF 是过程命令形式,数据体量大 (可以用 AOF 重写解决该问题),效率低于 RDB,记录每个操作,存储格式也更复杂,但数据相对完整 (最快可以每秒同步一次),且弥补了 RDB 不能实时存储的缺点。
  • 优点

    1. 数据安全,AOF 持久化可以配置 appendfsync 属性,有 always,每进行一次命令操作就记录到 AOF 文件中一次。
    2. 通过 append 模式写文件,即使中途服务器宕机,可以通过 redis-check-aof 工具解决数据一致性问题。
    3. AOF 机制的 rewrite 模式。AOF 文件没被 rewrite 之前(文件过大时会对命令进行合并重写),可以删除其中的某些命令(比如误操作的 flushall))
  • 缺点

    1. AOF 文件比 RDB 文件大,且恢复速度慢。
    2. 数据集大的时候,比 rdb 启动效率低。
AOF 数据体量大,那有什么办法可以解决这个问题吗?
  • Redis 中引入了 AOF 重写机制可以压缩文件体积——执行压缩命令即可。

  • AOF 重写可以降低磁盘占用量,也能提高数据恢复效率,它会对同一数据的多条写命令合并为一条写命令,且为了防止数据量过大造成缓冲区溢出,每条指令最多为 64 个元素。

  • AOF 重写其实是一个同步开启的子进程,Redis 执行指令时,子进程也会开启重写,主进程会将写入的数据同步到子进程,子进程则开始重写 AOF 文件,写完后再返回给主进程,完成 AOF 重写。

选用

  • AOF 文件比 RDB 更新频率高,优先使用 AOF 还原数据。

  • AOF 比 RDB 更安全也更大

  • RDB 性能比 AOF 好

  • 如果两个都配了优先加载 AOF

  • 如果对数据非常敏感用 AOF,如果追求大数据集的恢复速度选 RDB。

Redis 持久化数据和缓存怎么做扩容?

  • 如果 Redis 被当做缓存使用,使用一致性哈希实现动态扩容缩容。

  • 如果 Redis 被当做一个持久化存储使用,必须使用固定的 keys-to-nodes 映射关系,节点的数量一旦确定不能变化。否则的话 (即 Redis 节点需要动态变化的情况),必须使用可以在运行时进行数据再平衡的一套系统,而当前只有 Redis 集群可以做到这样。

数据删除

Redis 是怎么进行数据删除的?过期删除策略?

三种过期策略

定时删除

创建一个定时器,当 key 设置过期时间,且过期时间到达时,由定时器任务立即执行对键的删除操作。

这样的好处是及时快速释放内存,坏处时,消耗过多的处理器性能。

惰性删除

定时删除策略中,从删除方法来看,必然会导致有 key 过期了但未从 redis 中删除的情况。

面对这种情况,redis 在操作一个 key 时,会先判断这个值是否过期,若已过期,则删除该 key;若未过期,则进行后续操作。

定期删除(常用)
  • Redis 默认会每秒进行十次过期扫描,过期扫描不会遍历过期字典中所有的 key,而是采用了一种简单的贪心策略。

    1. 从过期字典中随机 20 个 key;
    2. 删除这 20 个 key 中已经过期的 key;
    3. 如果过期的 key 比率超过 1/4,那就重复步骤 1;
    4. 同时,为了保证过期扫描不会出现循环过度,导致线程卡死现象,算法还增加了扫描时间的上限,默认不会超过 25ms。
    5. 如果某一时刻,有大量 key 同时过期,Redis 会持续扫描过期字典,造成客户端响应卡顿,因此设置过期时间时,就尽量避免这个问题,在设置过期时间时,可以给过期时间设置一个随机范围,避免同一时刻过期。
      (expires 字典会保存所有设置了过期时间的 key 的过期时间数据,其中,key 是指向键空间中的某个键的指针,value 是该键的毫秒精度的 UNIX 时间戳表示的过期时间。键空间是指该 Redis 集群中保存的所有键。)
      Redis 中同时使用了惰性过期和定期过期两种过期策略。

怎么判断一个 Key 还有多少时间

  • TTL 命令(命令行)

    • 正数 -> 剩余时间
    • -1 -> 永久 Key
    • -2 -> Key 已经过期或者不存在

如何配置定期删除执行时间间隔?

redis 的定时任务默认是 10s 执行一次,如果要修改这个值,可以在 redis.conf 中修改 hz 的值。

redis.conf 中,hz 默认设为 10,提高它的值将会占用更多的 cpu,当然相应的 redis 将会更快的处理同时到期的许多 key,以及更精确的去处理超时。

hz 的取值范围是 1~500,通常不建议超过 100,只有在请求延时非常低的情况下可以将值提升到 100。

单线程的 redis,如何知道要运行定时任务

Redis 是单线程的,线程不但要处理定时任务,还要处理客户端请求,线程不能阻塞在定时任务或处理客户端请求上,那么,redis 是如何知道何时该运行定时任务的呢?

Redis 的定时任务会记录在一个称为最小堆的数据结构中。在这个堆中,最快要执行的任务排在堆的最上方。在每个循环周期,Redis 都会将最小堆里面已经到点的任务立即进行处理。处理完毕后,将最快要执行的任务还需要的时间记录下来,这个时间就是接下来处理客户端请求的最大时长,若达到了该时长,则暂时不处理客户端请求而去运行定时任务。

Redis key 的过期时间和永久有效分别怎么设置?

EXPIRE 和 PERSIST 命令。

内存淘汰

Redis 的内存淘汰策略有哪些?

Redis 的内存淘汰策略是指在 Redis 的用于缓存的内存不足时,怎么处理需要新写入且需要申请额外空间的数据。

操作的数据集 内存淘汰策略 说明
设置过期时间的键空间选择性移除(可能会过期的数据集 server.db[i].expires) volatile-lru 淘汰最近一次使用时间距现在最久的数据(Least Recently Used)最常用
volatile-lfu 淘汰使用次数最少的数据(Least Frequently Used)
volatile-ttl 淘汰过期时间最近的数据
volatile-random 淘汰随机数据
全局的键空间选择性移除(所有数据集) allkeys-lru 淘汰最近一次使用时间距现在最久的数据
allkeys-lfu 淘汰使用次数最少的数据
allkeys-random 淘汰随机数据
放弃数据驱逐 no-enviction 禁止驱逐数据(Redis4.0 中默认策略),会引发错误 OOM(Out Of Memory)

总结
Redis 的内存淘汰策略的选取并不会影响过期的 key 的处理。内存淘汰策略用于处理内存不足时需要申请额外空间的数据;过期策略用于处理过期的缓存数据。

MySQL 里有 2000w 数据,Redis 中只存 20w 的数据,如何保证 Redis 中的数据都是热点数据?

Redis 内存数据集大小上升到一定大小的时候,就会施行数据淘汰策略 (上面的 allkeys-lru,移除最近最少使用的 key)。

Redis 主要消耗什么物理资源?

内存。

Redis 的内存用完了会发生什么?

如果达到设置的上限,Redis 的写命令会返回错误信息(但是读命令还可以正常返回。)或者你可以配置内存淘汰机制,当 Redis 达到内存上限时会冲刷掉旧的内容。

Redis 如何做内存优化?

可以好好利用 Hash,list,sorted set,set 等集合类型数据,因为通常情况下很多小的 Key-Value 可以用更紧凑的方式存放到一起。尽可能使用散列表(hashes),散列表(是说散列表里面存储的数少)使用的内存非常小,所以你应该尽可能的将你的数据模型抽象到一个散列表里面。比如你的 web 系统中有一个用户对象,不要为这个用户的名称,姓氏,邮箱,密码设置单独的 key,而是应该把这个用户的所有信息存储到一张散列表里面

集群方案

说一说 Redis 的主从复制

对于大型网站来说,每秒需要读取的数据远远超过单台 Redis 服务所能承受的压力,而写入的数据相对来说较少,这时候用 Redis 的主从复制模式可以很好的提升同一个内容的读取速度。

Redis 主从复制模式其实是一个读写分离模型,将主服务器用来处理写操作,从服务器只提供读操作,不过主从复制是所有内容完全一致的,而不是分库分表的,严格来说不算集群。

主从复制模式最大的问题是只有一个主节点,没有实现高可用,所以还需要哨兵机制来实现高可用。

主从同步过程

当启动一个 slave node 的时候,它会发送一个 PSYNC <runid> <offset> 命令给 master node。

如果这是 slave node 初次连接到 master node,那么会触发一次 full resynchronization 全量复制(接收命令判断 runid 是否匹配,判定 offset 是否在复制缓冲区中)。此时 master 会启动一个后台线程,开始生成一份 RDB 快照文件,

  • 同时还会将从客户端 client 新收到的所有写命令缓存在 replication buffer(redis server 会为每一个连接到自己的客户端创建一个 replication buffer,用来缓存主库执行的命令)中。RDB 文件生成完毕后, master 会将这个 RDB 发送给 slave,slave 会先写入本地磁盘,然后再从本地磁盘加载到内存中,过程中基于旧的数据对外提供服务。

    • 如果 replication buffer 写满了(client-output-buffer-limit),无论客户端是普通客户端还是从库,只能断开跟这个客户端的连接了。这样从库全量同步失败,只能再次尝试全量同步。
  • 接着 master 会将内存中缓存的写命令发送到 slave,slave 也会同步这些数据。

    • 如果 slave node 开启了 AOF,那么立即执行重写 AOF

slave node 如果跟 master node 有网络故障,断开了连接,会自动重连,连接之后 master node 仅会复制给 slave 部分缺少的数据。(主库的写命令,除了传给从库后,还会写入 repl_backlog_buffer

  • 如果从库给的 runid 和 offset 都是对的,那么主服务器就会从上次同步的 offset 位置开始进行增量同步

  • 如果 offset 已经小于了主的复制缓冲区中最小的那个偏移量,就会进行全量复制

  • 切换了主服务器也会进行全量复制

image

什么是 Redis 哨兵?

sentinel(哨兵)是用于监控 Redis 集群中 Master 状态的工具,其本质就是一个独立运行的进程,是 Redis 的高可用解决方案。

sentinel 可以监视一个或者多个 Redis master 服务,以及这些 master 服务的所有从服务;当某个 master 服务下线时,自动将该 master 下的某个从服务升级为 master 服务替代已下线的 master 服务继续处理请求,并且其余从节点开始从新的主节点复制数据。(它会直接修改配置文件,来实现修改主服务器)

在 Redis 安装完成后,会有一个 redis-sentinel 的文件,这就是启动 sentinel 的脚本文件,同时还有一个 sentinel.conf 文件,这个是 sentinel 的配置文件。

但哨兵也可能会挂,所以需要对哨兵也实现高可用(集群)。

哨兵的核心知识

哨兵至少需要 3 个实例,来保证自己的健壮性。

哨兵 + Redis 主从的部署架构,是不保证数据零丢失的,只能保证 Redis 集群的高可用性。

对于哨兵 + Redis 主从这种复杂的部署架构,尽量在测试环境和生产环境,都进行充足的测试和演练。

为什么要搭建 Redis 集群?

Redis 有内置集群 cluster,Redis cluster 是无中心节点的集群架构,本身就是去中心化的,也就是说,每个节点都保存各自的数据和整个集群的状态。每个节点都和其他所有节点连接,而且这些连接保持活跃,这样就保证了我们只需要连接集群中的任意一个节点,就可以获取到其他节点的数据。

cluster 集群中内置了哨兵机制,不需要再额外启动哨兵程序。

Redis cluster 为了保证数据的高可用,加入了主从模式,主节点只用来存,从节点只用来取,如果主节点挂掉了,就会在从节点中选取一个来充当主节点。当然,如果通过集群命令,还可以实现在任何一个节点写入。

Redis 集群工作流程
  1. 通过哈希的方式,将数据分片,每个节点均分存储一定哈希槽 (哈希值) 区间的数据,默认分配了 16384 个槽位

  2. 每份数据分片会存储在多个互为主从的多节点上

  3. 数据写入先写主节点,再同步到从节点 (支持配置为阻塞同步)

  4. 同一分片多个节点间的数据不保持一致性

  5. 读取数据时,当客户端操作的 key 没有分配在该节点上时,redis 会返回转向指令,指向正确的节点

  6. 扩容时时需要需要把旧节点的数据迁移一部分到新节点

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

16379 端口号是用来进行节点间通信的,也就是 cluster bus 的东西,cluster bus 的通信,用来进行故障检测、配置更新、故障转移授权。cluster bus 用了另外一种二进制的协议,gossip 协议,用于节点间进行高效的数据交换,占用更少的网络带宽和处理时间。

你知道哪些分布式寻址法?

hash 算法(大量缓存重建)

一致性 hash 算法(自动缓存迁移)+ 虚拟节点(自动负载均衡)

redis cluster 的 hash slot (槽)算法

你了解一致性哈希吗?

简单来说,一致性 Hash 算法将整个哈希值空间组织成一个虚拟的圆环,整个空间圆按顺时针方向布局,正上方点代表 0,也代表 2^32-1,即一共 2^32 个点组成的圆环称为 Hash 环。

一致性哈希算法可用于 Redis 的集群搭建,当然这种用法已经过时了,现在可以用 Redis cluster 的哈希槽来替代一致性哈希算法,实现集群搭建。

使用一致性哈希算法时,多个服务器也会落在这个 Hash 环上,并且能确定各自的位置,这样用户访问的时候,会根据用户 IP 计算出哈希值,对应到哈希环上,并顺时针行走,遇到的第一台服务器就是该用户被分配到的服务器。

但是一致性哈希会有数据倾斜的问题,也就是可能多台服务器聚集在相近的地方,这时就需要将服务器多构建几个虚拟节点,来分散到 Hash 环上的各个位置,来使数据分配的服务器较为均衡,设置的虚拟节点约多,数据分配的越是相对均匀。

Redis 集群是同步复制还是异步复制?是否会有写操作丢失情况?为什么?

异步复制。

Redis 主节点与从节点之间的数据复制是异步复制的,当客户端发送写请求给 master 节点的时候,客户端会直接返回 OK,然后主节点同步到各个从节点中。

如果主节点没来得及同步给从节点时发生宕机,那么主节点内存中的数据就会丢失。

如果主节点中开启持久化,能不能保证数据不丢失呢?

不能。

主节点宕机后(脑裂也会有相同的数据丢失问题),会自动启动哨兵机制,重新选举新的主节点,如果这时候旧的主节点恢复故障重启了,它就会去同步新的主节点数据,而这时新的主节点并没有同步那之前丢失的数据,旧的主节点的上的数据则会在同步新主节点上数据时,刷新掉,此时数据还是会丢失。

怎么才能保证数据不丢失?或者如保证尽量少的数据丢失?

没办法完全保证数据不丢失。但是可以通过设置主从节点间的同步复制延迟时间来尽量少的数据丢失。

比如,在 Redis 集群中,在 redis 配置文件中修改同步复制延迟的时间不超过 10s,一但延迟超过这个时间,说明主节点可能出了问题(不一定是宕机),那么配置生效,主节点只能读,不能再写入。

  • min-slaves-to-write 1

  • min-slaves-max-lag 10

  • 要求至少有 1 个 slave,数据复制和同步的延迟不能超过 10 秒(这样脑裂最多就丢失 10s 数据)

同时在客户端做降级处理,把数据写到本地磁盘。

什么是 Redis 脑裂?会存在什么问题?

一个集群中的 master 恰好网络故障,导致与 sentinal 联系不上了,sentinal 把另一个 slave 提升为了 master。此时就存在两个 master 了。

当我们发现的时候,停止掉其中的一个 master,手动切换成 slave,当它连接到提升后的 master 的时候,会开始同步数据,那么自己脑裂期间接收的写数据就被丢失了。

Redis 集群如何选择数据库?

Redis 集群目前无法做数据库选择,默认在 0 数据库。

Redis 集群最大节点个数是多少?

16384 个

生产环境中的 redis 是怎么部署的?

redis cluster,10 台机器,5 台机器部署了 redis 主实例,另外 5 台机器部署了 redis 的从实例,每个主实例挂了一个从实例,5 个节点对外提供读写服务,每个节点的读写高峰 qps 可能可以达到每秒 5 万,5 台机器最多是 25 万读写请求/s。

  • 机器是什么配置?32G 内存 + 8 核 CPU + 1T 磁盘,但是分配给 redis 进程的是 10g 内存,一般线上生产环境,redis 的内存尽量不要超过 10g,超过 10g 可能会有问题。

  • 5 台机器对外提供读写,一共有 50g 内存。

  • 因为每个主实例都挂了一个从实例,所以是高可用的,任何一个主实例宕机,都会自动故障迁移,redis 从实例会自动变成主实例继续提供读写服务。

  • 你往内存里写的是什么数据?每条数据的大小是多少?商品数据,每条数据是 10kb。100 条数据是 1mb,10 万条数据是 1g。常驻内存的是 200 万条商品数据,占用内存是 20g,仅仅不到总内存的 50%。目前高峰期每秒就是 3500 左右的请求量。其实大型的公司,会有基础架构的 team 负责缓存集群的运维。

缓存异常

缓存预热

系统上线后,将相关的缓存数据直接加载到缓存系统。这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题。用户直接查询事先被预热的缓存数据。

解决方法

  • 自动:使用监听器(web/Spring boot)监听某个事件(项目启动/容器的创建),在事件触发时查询数据库,把一些热门数据提前缓存到 Redis 中

  • 手动:使用单元测试脚本 动态的往 Redis 中进行数据的添加

缓存雪崩

缓存同一时间大面积的失效,所以,后面的请求都会落到数据库上,造成数据库短时间内承受大量请求而崩掉。

解决方法

  • 设置缓存的失效时间尽量错开(比如 30 天 + 随机数 (s)),热门数据时间长点,不热门的时间短点。

  • 设置多级缓存,Nginx 缓存 +redis 缓存 +ehcache 缓存……每一个缓存都是一个集群,相同的数据会在多种服务器进行缓存,可以 100% 解决缓存雪崩。

  • 侧面解决:优化数据库,提升效率,使用页面静态技术(多级缓存)代替从 redis 中取值。

  • 限流/降级

  • 超热数据使用永久 Key(定期维护)

缓存穿透

就是指反复查询一个数据库一定不存在的数据,导致数据库压力过大,这种情况一般只有恶意攻击才会出现。

解决方法

  • 接口层增加校验,如用户鉴权校验,id 做基础校验,id<=0 的直接拦截

  • 从缓存取不到的数据,在数据库中也没有取到,这时也可以将 key-value 对写为 key-null,缓存有效时间可以设置短点,如 30 秒(设置太长会导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个 id 暴力攻击

  • 采用布隆过滤器(在海量数据中判断一个数据是否存在),将所有可能存在的数据哈希到一个足够大的 bitmap 中,一个一定不存在的数据会被这个 bitmap 拦截掉,从而避免了对底层存储系统的查询压力。根据 Hash 判断,如果数据不存在,那么一定不存在。

  • 加密参数,符合加密规则的参数才会被接收

缓存击穿

是指一个热点 key 的 redis 缓存失效,导致大量请求瞬间集中到数据库。和缓存雪崩不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。

解决方法

  • 多级缓存

  • 延长热点数据有效期。

  • 限流/降级

缓存降级

当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,仍然需要保证服务还是可用的,即使是有损服务。系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级。

热点数据和冷数据

  • 热点数据,缓存才有价值

    • 某 IM 产品,生日祝福模块,当天的寿星列表,缓存以后可能读取数十万次。某导航产品,我们将导航信息,缓存以后可能读取数百万次。
  • 对于冷数据而言,大部分数据可能还没有再次访问到就已经被挤出内存,不仅占用内存,而且价值不大。

  • 频繁修改的数据,看情况考虑使用缓存

    • 这个读取接口对数据库的压力很大,但是又是热点数据,这个时候就需要考虑通过缓存手段,减少数据库的压力,比如我们的某助手产品的,点赞数,收藏数,分享数等是非常典型的热点数据,但是又不断变化,此时就需要将数据同步保存到 Redis 缓存,减少数据库压力。

线程模型

为什么 Redis 是单线程的?

Redis 的底层主要是三大部分:IO 多路复用程序 + 队列 + 文件事件分派器。而文件事件分派器就是用来分派执行任务的,且它是单线程的,所以说 Redis 也是单线程的。

此外,Redis 底层是基于 C 语言所写,且是基于内存运行,所以他的 IO 速度很快,相对来说主要要占用的是 CPU 的资源,如果使用多线程反而会因为线程的切换增大开销,降低效率。对于不是处理海量数据,多用于缓存使用的 Redis 来说,使用单线程是效率最优的。

Redis 是单线程的,为什么还需要用它实现分布式锁?

Redis 单线程与分布式锁没有关系。Redis 单线程只能保证在 Redis 中的读写是按一定顺序执行(Redis 不保证原子性,也不会回滚,某条命令运行失败也会继续执行),但在分布式中,是多台机器间的多进程调用,为了保证执行过程中,不被其他进程抢断执行,需要有一个第三方组件来实现分布式锁。即使数据就在 Redis 中,也无法保证数据在进行其他服务处理的时候,被其他进程抢走。

Redis 是单线程的,如何提高多核 CPU 的利用率?

可以在同一个服务器部署多个 Redis 的实例,并把他们当作不同的服务器来使用,在某些时候,无论如何一个服务器是不够的, 所以,如果你想使用多个 CPU,你可以考虑一下分区。

Redis 分区

为什么要做 Redis 分区

分区可以让 Redis 管理更大的内存,Redis 将可以使用所有机器的内存,提升内存和计算能力。

怎么实现 Redis 分区?

Cluster 集群就很好的实现了 Redis 分区。客户端随机地请求任意一个 Redis 实例,然后由 Redis 将请求转发给正确的 Redis 节点。Redis Cluster 实现了一种混合形式的查询路由,但并不是直接将请求从一个 Redis 节点转发到另一个 Redis 节点,而是在客户端的帮助下直接 redirected 到正确的 Redis 节点。

Redis 分区的缺点是什么?

涉及多个 key 的操作时可能会比较麻烦一点:比如两个集合 key 在不同的机子上,那就不能直接在 Redis 中做交集。

Redis 分区和集群有什么区别?

分区是逻辑概念,即一个 master 可以分为一个 master+n 个 slave。

集群是系统结构,集群是分区概念的一种实现方式。

分布式问题

那怎么解决缓存的并发竞争问题?比如多个子系统去 set 一个 key 但最后执行顺序和我们期望顺序不一样

可以采用分布式锁(Zookeeper 和 Redis 都可以实现分布式锁)的方案。(如果不存在 Redis 的并发竞争 Key 问题,不要使用分布式锁,这样会影响性能)

基于 Zookeeper 临时有序节点可以实现的分布式锁。大致思想为:每个客户端对某个方法加锁时,在 Zookeeper 上的与该方法对应的指定节点的目录下,生成一个唯一的瞬时有序节点。 判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个。 当释放锁的时候,只需将这个瞬时节点删除即可。同时,其可以避免服务宕机导致的锁无法释放,而产生的死锁问题。完成业务流程后,删除对应的子节点释放锁。

在实践中,当然是从以可靠性为主。所以首推 Zookeeper。

说一说 Redis 分布式锁

首先,分布式锁是一种思想,是为了解决多机器多进程之间的同步问题,需要引入第三方来做锁处理。

Redis 分布式锁的加锁,其实就是给 Key 键设置一个值(SET lock_key random_value NX PX 5000,NX 表示键不存在时才设置),其他进程执行前会判断 Redis 中这个 Key 是否有值,如果发现这个 Key 有值了,就说明已有其他进程在执行,则循环等待,超时则获取失败。

解锁就是将 Key 键删除,为了保证解锁的原子性操作,用 Redis 自带的 LUA 脚本完成操作。

Redis 做为分布式锁的优点是性能高,缺点是不可靠,比如在 Redis 集群架构中,一旦主节点宕机,新的主节点会给新进程也加锁。

但不论如何,分布式锁一定会带来性能消耗问题,且实际中使用查询的比例远大于写的比例,除非是重要的敏感数据,不然为了更高的性能,一般会选择舍弃使用分布式锁。

这里为什么要用 Lua 脚本来完成解锁操作?

Redis 只能保证单个指令进入 Redis 是单线程原子性的,但不能保证多个命令之间的顺序是原子性的,但是 Lua 指令可以将这个命令绑定,相当于只有一个执行命令,保证它们是顺序执行的。

为什么 Redis 分布式解锁要保证原子性?

主要是怕误将其他客户端的锁解开。

比如客户端 A 加锁,一段时间之后客户端 A 解锁,在进入 unlock 后执行 jedis.del() 之前,锁突然过期了,此时客户端 B 尝试加锁成功,然后客户端 A 再执行 del() 方法,这时候客户端 A 已经执行过 Random_Value 的判断,虽然和客户端 B 的不一样,但我们总不能每一行代码都做一个值判断吧,这时候客户端 A 就会将客户端 B 的锁给解除了。

Redis 分布式锁应该注意什么?

加锁的时候,应考虑到执行一半宕机或故障导致没能执行到解锁的命令,产生死锁,所以需要给定一个过期时间,防止死锁。

解锁要保证原子一致性。

Redis 的解锁就是把 key 删除即可,但是删除的时候不能随便删,比如线程 A 不能删除线程 B 的 key,这个时候 value 就起到作用了,random_value 我们设置为随机值,每一个线程都生成一个随机值作为 random_value,删除 key 的时候先判断随机值是否和本线程的一致,一致的才可以删除。

Redis 事务

Redis 事务的概念

Redis 事务的本质是通过 MULTI、EXEC、WATCH 等一组命令的集合。事务支持一次执行多个命令,一个事务中所有命令都会被序列化。在事务执行过程,会按照顺序串行化执行队列中的命令,其他客户端提交的命令请求不会插入到事务执行命令序列中。

总结说:Redis 事务就是一次性、顺序性、排他性的执行一个队列中的一系列命令。

Redis 事务的三个阶段

  1. 事务开始 MULTI

  2. 命令入队

  3. 事务执行 EXEC

事务执行过程中,如果服务端收到有 EXEC、DISCARD、WATCH、MULTI 之外的请求,将会把请求放入队列中排队

Redis 事务相关问题

Redis 事务功能是通过 MULTI、EXEC、DISCARD 和 WATCH 四个原语实现的。(Redis 事务很鸡肋)

  1. Redis 会将一个事务中的所有命令序列化,然后按顺序执行。

  2. Redis 不支持回滚,“Redis 在事务失败时不进行回滚,而是继续执行余下的命令”, 所以 Redis 的内部可以保持简单且快速。

  3. 如果在一个事务中的命令出现错误,那么所有的命令都不会执行(就是除非你的命令写错了,导致所有命令不执行);

  4. 如果在一个事务中出现运行错误,那么正确的命令会被执行。

WATCH 命令是一个乐观锁,可以为 Redis 事务提供 check-and-set (CAS)行为。 可以监控一个或多个键,一旦其中有一个键被修改(或删除),之后的事务就不会执行,监控一直持续到 EXEC 命令。

MULTI 命令用于开启一个事务,它总是返回 OK。 MULTI 执行之后,客户端可以继续向服务器发送任意多条命令,这些命令不会立即被执行,而是被放到一个队列中,当 EXEC 命令被调用时,所有队列中的命令才会被执行。

EXEC:执行所有事务块内的命令。返回事务块内所有命令的返回值,按命令执行的先后顺序排列。 当操作被打断时,返回空值 nil 。

通过调用 DISCARD,客户端可以清空事务队列,并放弃执行事务, 并且客户端会从事务状态中退出。

UNWATCH 命令可以取消 watch 对所有 key 的监控。

Redis 事务保证原子性吗?

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

Redis 事务保证隔离性吗?

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

场景

在工作中,如果数据库修改了数据,那就跟 Redis 数据不同步了,此时该办?(缓存双写一致性问题)

第一种是硬编码方式;

  • 让服务的更新和查询分先后进行执行,服务将数据库更新后,再查返更新后的数据,把更新后的数据再更新到 Redis。如果有多个服务同时调用,就用 MQ 获取 Mysql 的 binlog 中的修改顺序,依次发送消息给 redis,MySQL 的顺序记录就是先后更改的执行顺序,MQ 根据这个顺序即可保证先后的更新顺序,让 Redis 的更新顺序不出问题。

第二种是 Redis 缓存二次开发解决(MyBatisPlus+Redis 整合:餐饮项目中使用到)。

  • 用 Mybatis 二级缓存对外开放的 cache 接口进行二次开发,Cache 支持 SPI 机制,通过注解来调动相关配置类,通过实现 Mybatis 二级缓存接口 Cache 集成 Redis 缓存,可以自动更新 MySQL 数据到 Redis 缓存中。

  • 为了防止在读写的过程中出现高并发问题,还需要加上 ReentrantReadWriteLock 读写锁,ReentrantLock 只能进入一个线程,ReentrantReadWriteLock 允许多个线程访问,提高高并发,但只支持多个线程读,不支持同时读写或写写。

  • 但是以上方式也不能完全解决同步问题,仍存在分布式的高并发问题,如果需要完全保证一致性,就需要使用分布式锁,对每个事务上锁。但这样对系统资源消耗过大,得不偿失,毕竟读取的量是远大于增删改的量的。

假如 Redis 里面有 1 亿个 key,其中有 10W 个 key 是以某个固定的已知前缀开头的,如何将它们全部找出来?

使用 keys 指令可以扫出指定模式的 key 列表。

使用 SCAN+MATCH 参数匹配前缀命令获取所有 key(会重复,需要进行去重处理)

1
2
3
redis 127.0.0.1:6379> KEYS PATTERN

SCAN 0MATCH "prefix:*"

匹配模式:

  • h?llo 匹配 hellohallo 和 hxllo
  • h*llo 匹配 hllo 和 heeeello
  • h[ae]llo 匹配 hello and hallo, 不匹配 hillo
  • h[^e]llo 匹配 hallohbllo, … 不匹配 hello
  • h[a-b]llo 匹配 hallo 和 hbllo

以上是原先的答案,但是参考博客 (https://cloud.tencent.com/developer/article/2408542),这样回答反而是错误的。

根据上篇博客的内容,具体的测试内容不放出了。总之这题如果面试官只是要考查你 KEYS 命令和 SCAN 命令的区别,并且想要看看你知不知道 KEYS 命令的阻塞问题,那么你回答 SCAN 就已经过了。而实际中,如果真的有经验,你就会发现 SCAN 的能力阈值是在那里的。于是你需要继续反问面试官,是否有时间要求。

自己进行了一下验证,如果直接一次 scan 一千万的记录 耗时为: 10.15 秒。

理论上 scan 一个键值对的时间为 1 微秒左右。

如果 Redis 里面有 1000 万个 key 的话 60 台服务器如果同时进行一次所有的 scan 那么搞不好至少会有在 运行期间内产生总计 600S 的延迟时间.

Redis redis 正给线上的业务提供服务,那使用 keys 指令会有什么问题?

由于 Redis 是单线程的,而 keys 是遍历查询,每个数据都会遍历一次,所以 keys 指令去查找大量数据会导致线程阻塞一段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复。

可替代方式是,可以使用 scan 指令,scan 指令可以无阻塞的提取出指定模式的 key 列表,但是会有一定的重复概率,在客户端做一次去重就可以了,但是整体所花费的时间会比直接用 keys 指令长。

但这里要注意的是,scan 指令有一个坑,不会自动释放连接,需要手动释放连接。// todo 这个需要查找 具体的解决方案我有做笔记

Scan 为什么可以不阻塞提取 key 列表?为什么会有重复问题?

Scan 命令其实是迭代器方式对数据进行遍历获取,但它是分次进行的,可以在中途返回查到的数据,做到不阻塞,且因为 Redis 底层是 Hash 字典,会有扩容和缩容问题,所以 scan 每次都会往前退一定数据开始继续遍历,所以会有重复问题。(具体见笔记)。

使用 Redis 做过异步队列吗,是如何实现的?

使用 list 类型保存数据信息,rpush 生产消息,lpop 消费消息,当 lpop 没有消息时,可以 sleep 一段时间,然后再检查有没有信息,如果不想 sleep 的话,可以使用 blpop, 在没有信息的时候,会一直阻塞,直到信息的到来。Redis 可以通过 pub/sub 主题订阅模式实现一个生产者,多个消费者,当然也存在一定的缺点,当消费者下线时,生产的消息会丢失。

Redis 如何实现延时队列?

使用 sortedset,使用时间戳做 score,消息内容作为 key,调用 zadd 来生产消息,消费者使用 zrangbyscore 获取 n 秒之前的数据做轮询处理。

Redis 回收进程如何工作的?

  1. 一个客户端运行了新的命令,添加了新的数据。

  2. Redis 检查内存使用情况,如果大于 maxmemory 的限制, 则根据设定好的策略进行回收。

  3. 一个新的命令被执行,等等。

  4. 所以我们不断地穿越内存限制的边界,通过不断达到边界然后不断地回收回到边界以下。

  5. 如果一个命令的结果导致大量内存被使用(例如很大的集合的交集保存到一个新的键),不用多久内存限制就会被这个内存使用量超越。

Redis 回收使用的是什么算法?

LRU 算法(将最近最久未使用的页面予以淘汰。)

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 官方为什么不提供 Windows 版本?

因为目前 Linux 版本已经相当稳定,而且用户量很大,无需开发 windows 版本,反而会带来兼容性等问题。

一个字符串类型的值能存储最大容量是多少?

512M

Redis 如何做大量数据插入?

Redis2.6 开始 redis-cli 支持一种新的被称之为 pipe mode 的新模式用于执行大量数据插入工作。

 评论
评论插件加载失败
正在加载评论插件
由 Hexo 驱动 & 主题 Keep
访客数 访问量