Redis 面试题

1. Redis 是什么?

Redis 是一种基于内存的数据库,对数据的读写操作都是在内存中完成,因此读写速度非常快,常用于缓存,消息队列、分布式锁等场景

2. Redis 有哪些数据类型?

  • 5 种基础数据结构:String(字符串)、List(列表)、Set(集合)、Hash(哈希)、Zset(有序集合)。
  • 3 种特殊数据结构:HyperLogLogs(基数统计)、Bitmap (位存储)、Geospatial (地理位置)。

String 是最常用的数据类型,String 类型的应用场景:缓存对象、常规计数、分布式锁、共享 session 信息等。

2.1. 缓存对象

使用 String 来缓存对象有两种方式:

  • 直接缓存整个对象的 JSON,命令例子: SET user:1 '{"name":"xiaolin", "age":18}'
  • 采用将 key 进行分离为 user:ID:属性,采用 MSET 存储,用 MGET 获取各属性值,命令例子: MSET user:1:name xiaolin user:1:age 18 user:2:name xiaomei user:2:age 20

2.2. 使用 Redis 实现分布式锁

SET 命令有个 NX 参数可以实现「key不存在才插入」,可以用它来实现分布式锁:

  • 如果 key 不存在,则显示插入成功,可以用来表示加锁成功;
  • 如果 key 存在,则会显示插入失败,可以用来表示加锁失败。

一般而言,还会对分布式锁加上过期时间,分布式锁的命令如下:

SET lock_key unique_value NX PX 10000
  • lock_key 就是 key 键;
  • unique_value 是客户端生成的唯一的标识;
  • NX 代表只在 lock_key 不存在时,才对 lock_key 进行设置操作;
  • PX 10000 表示设置 lock_key 的过期时间为 10s,这是为了避免客户端发生异常而无法释放锁。(也可用 EX,单位为秒)

而解锁的过程就是将 lock_key 键删除,但不能乱删,要保证执行操作的客户端就是加锁的客户端。所以,解锁的时候,我们要先判断锁的 unique_value 是否为加锁客户端,是的话,才将 lock_key 键删除。这里提一点:如何保证解锁的客户端为加锁客户端呢?可以给 key 再加一个随机不重复的 id,这样 key 唯一了,只有加锁的客户端才能解锁。

可以看到,解锁是有两个操作,这时就需要 Lua 脚本来保证解锁的原子性,因为 Redis 在执行 Lua 脚本时,可以以原子性的方式执行,保证了锁释放操作的原子性。

// 释放锁时,先比较 unique_value 是否相等,避免锁的误释放
if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

这样一来,就通过使用 SET 命令和 Lua 脚本在 Redis 单节点上完成了分布式锁的加锁和解锁。

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

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

  1. 高性能:假如用户第一次访问 MySQL 中的某些数据。这个过程会比较慢,因为是从硬盘上读取的。将该用户访问的数据缓存在 Redis 中,这样下一次再访问这些数据的时候就可以直接从缓存中获取了,操作 Redis 缓存就是直接操作内存,所以速度相当快。
  2. 高并发:单台设备的 Redis 的 QPS(Query Per Second,每秒钟处理完请求的次数) 是 MySQL 的 10 倍。Redis 单机的 QPS 能轻松破 10w,而 MySQL 单机的 QPS 很难破 1w。

4. Redis为何这么快?

Redis 内部做了非常多的性能优化,比较重要的有下面 3 点:

  1. Redis 基于内存,内存的访问速度是磁盘的上千倍;
  2. Redis 基于 Reactor 模式设计开发了一套高效的事件处理模型,主要是单线程事件循环和 IO 多路复用;
  3. Redis 内置了多种优化过后的数据结构实现,性能非常高。

5. 开发中常用的 Redis 命令有哪些?

  • set:设置值,比如:set key value
  • get:获取值,比如:get key
  • exists:检查给定 key 是否存在,比如:exists key_name
  • del:删除 key,比如:del key_name
  • expire:为给定 key 设置过期时间,以秒为单位,比如: expire key_name time_in_seconds
  • ttl:以秒为单位,返回给定 key 的剩余生存时间
  • mset:同时 set 多个值,比如:mset key1 value1 key2 value2 .. keyN valueN
  • mget:获取所有(一个或多个)给定 key 的值,比如:mget key1 key2 .. keyN
  • hset:hash 命令,一个 key 对应一个哈希表。比如:hset key field1 value1 field2 value2
  • hget:获取哈希表。比如:hget key
  • hexists:判断哈希表 key 是否存在
  • hdel:删除哈希表 key

6. Redis 如何实现数据不丢失?

Redis 共有三种数据持久化的方式:

  • AOF 日志:Redis 在执行完一条写操作命令后,就会把该命令以追加的方式写入到一个文件里,然后 Redis 重启时,会读取该文件记录的命令,然后逐一执行命令的方式来进行数据恢复。
  • RDB 快照:将某一时刻的内存数据,以二进制的方式写入磁盘;
  • 混合持久化方式:Redis 4.0 新增的方式,集成了 AOF 和 RBD 的优点;

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

7.1 缓存穿透

用户请求的数据,在缓存和数据库中都没有,导致所有该请求都到数据库了,给数据库增大压力。本来缓存就是用来快速响应和缓解数据库压力的。
解决方案:

  1. 加强业务校验:严格控制请求参数范围,控制非法请求。
  2. 缓存不存在的数据。如果查询的数据为空,就缓存一个空字符串对象并返回,并设置一个较短的过期时间,比如 60s 或 30s,防止非法数据不断请求数据库。
  3. 布隆过滤器:布隆过滤器是一种数据结构。对于缓存击穿,我们可以将查询的数据条件都哈希到一个足够大的布隆过滤器中,用户发送的请求会先被布隆过滤器拦截,一定不存在的数据就直接拦截返回了,从而避免下一步对数据库的压力。

7.2 缓存击穿

Redis 中一个热点 key 在失效的同时,大量的请求过来,从而会全部到达数据库,压垮数据库。
解决方案:

  1. 设置热点数据永不过期。对于某个需要频繁获取的信息,缓存在Redis中,并设置其永不过期。当然这种方式比较粗暴,对于某些业务场景是不适合的。

  2. 定时更新。比如这个热点数据的过期时间是1h,那么每到59minutes时,通过定时任务去更新这个热点key,并重新设置其过期时间。

  3. 互斥锁。这是解决缓存穿透比较常用的方法。互斥锁简单来说就是在 Redis 中根据 key 获得的 value 值为空时,先锁上,然后从数据库加载,加载完毕,释放锁。若其他线程也在请求该 key 时,发现获取锁失败,则睡眠一段时间(比如100ms)后重试。

7.3 缓存雪崩

Redis 中缓存的数据大面积同时失效(比如同时过期了),或者 Redis 宕机,从而会导致大量请求直接到数据库,压垮数据库。
解决方案:

  1. 规划过期时间,均匀分布
  2. 数据预热。对局即将到来的请求高峰,提前查一遍库,更新一下缓存。
  3. 保证 Redis 高可用。
posted @ 2023-06-08 15:57  xfcoding  阅读(68)  评论(0编辑  收藏  举报