1. Redis 的数据结构都有哪些?

Redis 支持五种数据类型,其底层实现的编码数据结构有 8 种:

  • SDS – simple synamic string – 支持自动动态扩容的字节数组
  • list – 平平无奇的链表
  • dict – 使用双哈希表实现的, 支持平滑扩容的字典
  • zskiplist – 附加了后向指针的跳跃表
  • intset – 用于存储整数数值集合的自有结构
  • ziplist – 一种实现上类似于 TLV, 但比 TLV 复杂的, 用于存储任意数据的有 序序列的数据结构
  • quicklist – 一种以 ziplist 作为结点的双链表结构
  • zipmap – 一种用于在小规模场合使用的轻量级字典结构

string 底层是 SDS 内部编码 int,emstr,raw hash 是 ziplist 与 hashtable list 采取的双端链表 linklist,压缩列表 ziplist set 是 hashtable 和 inset zset 是 ziplist 和 skiplist

2. Redis 没有使用多线程?为什么不使用多线程?

虽然说 Redis 是单线程模型,但是,实际上,Redis 在 4.0 之后的版本中就已经加入了对多线程的支持。

不过,Redis 4.0 增加的多线程主要是针对一些大键值对的删除操作的命令,使用这些命令就会使用主处理之外的其他线程来“异步处理”。

大体上来说,Redis 6.0 之前主要还是单线程处理。

那,Redis6.0 之前 为什么不使用多线程?

我觉得主要原因有下面 3 个:

  • 单线程编程容易并且更容易维护。
  • Redis 的性能瓶颈不在 CPU ,主要在内存和网络。
  • 多线程就会存在死锁、线程上下文切换等问题,甚至会影响性能。

3. Redis6.0 之后为何引入了多线程?

Redis6.0 引入多线程主要是为了提高网络 IO 读写性能,因为这个算是 Redis 中的一个性能瓶颈(Redis 的瓶颈主要受限于内存和网络)。

虽然,Redis6.0 引入了多线程,但是 Redis 的多线程只是在网络数据的读写这类耗时操作上使用了,执行命令仍然是单线程顺序执行。因此,你也不需要担心线程安全问题。

Redis6.0 的多线程默认是禁用的,只使用主线程。如需开启需要修改 redis 配置文件 redis.conf :

io-threads-do-reads yes

 

开启多线程后,还需要设置线程数,否则是不生效的。同样需要修改 redis 配置文件 redis.conf :

io-threads 4 #官网建议4核的机器建议设置为2或3个线程,8核的建议设置为6个线程

4. Redis 的事务性

Redis 通过 MULTI、EXEC、WATCH 等命令来实现事务(transaction)功能。事 务提供了一种将多个命令请求打包,然后一次性、按顺序地执行多个命令的机 制,并且在事务执行期间,服务器不会中断事务而改去执行其他客户端的命令 请求,它会将事务中的所有命令都执行完毕后,然后才去处理其他客户端的命 令请求。 事务的生命周期为:

  • 事务开始:使用 MULTI 开启一个事务(自己项目中,采用 jedis.multi() 来返回一个事务 Transaction,后续可以在此事务上进行操作)
  • 命令入队:在开启事务的时候,每次操作的命令将会被插入到一个队列中,同时这个命令并不会被真的执行。
  • 事务执行:EXEC 命令进行提交事务

Redis 事务具有的性质:

  • 单独的隔离操作:事务中所有命令都会被序列化、按顺序执行,在执行过程中不会被其他客户端发送来的命令打断。
  • 没有隔离级别的概念:队列中的命令在事务没有被提交之前不会被实际执行。

在 Redis 中,事务总是具有原子性、一致性和隔离性。当 Redis 运行在某种特定的持久化模式(开启 AOF 和 RDB 服务)下时,事务也具有持久性。

着重讲一下原子性。

对于 Redis 的事务功能来说,事务队列中的命令要么就全部执行,要不就一个都不执行。从这点来说,事务具有原子性。但这个执行失败的条件是指命令入队出错(比如命令不存在,格式不正确等情况)而被服务器拒绝执行,而不是命令实际执行时出错。

Redis 的事务与传统的关系式数据库事务的最大区别在于,Redis 不支持事务回滚机制(rollback),即使事务队列中的某个命令在执行期间出现了错误,整个事务也会继续执行下去,直到将事务队列中的所有命令都执行完毕为止。 一定注意,Redis 的事务性与常见的关系式数据库有些不同(尤其原子性)。

5. 当前 Redis cluster 集群有哪些方式,各自优缺点,场景?

Redis 集群是 Redis 提供的分布式数据库方案,集群通过分片来进行数据共享, 并提供复制和故障转移功能。

Redis 集群使用数据分片(sharding)而非一致性哈希(consistency hashing) 来实现:一个 Redis 集群包含 16384 个哈希槽(hash slot),数据库中的每个键 都属于这个 16384 个哈希槽的其中一个,集群使用公式 CRC16(key) % 16384 来计算键 key 属于哪个槽,其中 CRC16(key)语句用于计算键 key 的 CRC16 校 验和。

  • 数据共享:Redis 提供多个节点实例间的数据共享,也就是 Redis A、B、 C、D 彼此之间的数据是同步的,同样彼此之间也可以通信,而对于客户 端操作的 keys 是由 Redis 系统自行分配到各个节点中。
  • 主从复制:Redis 的多个实例间通信时,一旦其中一个节点故障,那么 Redis 集群就不能继续正常工作了,所以需要一种复制机制(Master- Slave)机制,做到一旦节点 A 故障了,那么其从节点 A1 和 A2 就可以 接管并继续提供与 A 同样的工作服务。
  • 哈希槽值:Redis 集群中使用哈希槽来存储客户端的 keys,而在 Redis 中,目前存有 16384(2 的 14 次方)个哈希槽,它们被全部分配给所有 的节点。

6. Redis 适合于哪些场景?

  • Session 共享(单点登陆)
  • 页面缓存
  • 队列(比如项目中用到的异步队列)
  • 排行榜/计数器
  • 发布/订阅(实现消息流)

7. Redis 持久化的机制,AOF 和 RDB 的区别

Redis 提供两种方式进行持久化,一种是 RDB 持久化(会按照配置的指定时间 将内存中的数据快照到磁盘中,创建一个 dump.rdb 文件,Redis 启动时再恢复 到内存中),另一种是 AOF 持久化(以日志的形式记录每个写操作(读操作不 记录),只需追加文件但不可以改写文件,Redis 启动时会根据日志从头到尾 全部执行一遍以完成数据的恢复工作)。

两者的区别在于:

  • RDB 持久化是指在指定的时间间隔内将内存中的数据集快照写入磁盘, 实际操作过程是 fork 一个子进程,先将数据集写入临时文件,写入成功 后,再替换之前的文件,用二进制压缩存储。
  • AOF 持久化以日志的形式记录服务器所处理的每一个写、删除操作,查 询操作不会记录,以文本的方式记录,可以打开文件看到详细的操作记 录。

默认情况下,Redis 选用的是快照 RDB 的持久化方式,将内存 中的数据以快照的方式写入二进制文件中,默认的文件名为 dump.rdb。

RDB 方式不能完全保证数据持久化,因为是定时保存,所以当 redis 服务 down 掉,就会丢失一部分数据,而且数据量大,写操作多的情况下,会引起大量的 磁盘 IO 操作,会影响性能。所以,当 RDB 和 AOF 方式都开启的情况下,服务 器会优先使用 AOF 文件来还原数据库状态,当然,AOF 恢复数据速度要慢一些。

还一点需要注意,服务器在载入 RDB 文件期间,会一直处于阻塞状态,直到载 入工作完成为止。

8. Redis 优化操作

  • 使用简短的 key。
  • 大的数据压缩后再存入 value。
  • 设置 key 有效期。
  • 选择回收策略。当 Redis 的实例空间被填满了之后,将会尝试回收一部 分 key。在 Redis 中,允许用户设置最大使用内存大小 server.maxmemory,当 Redis 内存数据集大小上升到一定大小的时候, 就会施行数据淘汰策略,有很多不同的回收策略。
  • 在服务器端使用 Lua 脚本。

9. Redis 的主从复制机制原理

主从的意义:

  • redis 要达到高可用、高并发,只有单个 redis 是不够的,单个 redis 也 就只能支持几万的 QPS,所以必须以集群的形式提供服务,而集群中又 以多个主从组成。
  • 主从是以多个 redis 集合在一起,以一个 master 多个 slave 为模式对外 提供服务,master 主要以写为主,slave 提供读,即是读写分离的情况, 以读多写少为准,如果写比较多的情况一般就以异步的形式提供服务

主从复制功能分为两个阶段:

  • 同步操作:将从服务器的数据库状态更新至主服务器当前所处的数据库 状态。
  • 命令传播:用于在主服务器的数据库状态被修改,导致主从服务器的数 据库状态出现不一致时,让主从服务器的数据库重新回到一致状态。

Redis2.8 版本之前同步操作采用 SYNC 命令,只有全量同步,效率比较低。2.8 版本之后使用 PSYNC 命令代替 SYNC 命令来执行复制时的同步操作,自行判断 是全量同步还是增量同步(通过复制偏移量、复制积压缓冲区(其实就是一个 FIFO 的队列)和服务器运行 ID 三个部分来实现),效率较高。

在命令传播阶段,从服务器默认会以每秒一次的频率,向主服务器发送命令: REPLCONF ACK <replication_offset>; 其中 replication_offset 是从服务器当前 的复制偏移量。

心跳检测对主从服务器有三个作用:

  • 检测主从服务器的网络连接状态。
  • 辅助实现 min-slaves 选项。
  • 检测命令丢失,通过对比主从服务器的复制偏移量。

10. Redis 的线程模型是什么?

Redis 基于 Rector 模型开发了自己的文件事件处理器(file event handler):

  • 文件事件处理器使用 I/O 多路复用程序来同时监听多个套接字,并根据 套接字目前执行的任务来为套接字关联不同的事件处理器。
  • 当被监听的套接字准备好执行连接应答(accept)、读取(read)、写 入(write)、关闭(close)等操作时,与操作相对应的文件事件就会产 生,这时文件事件处理器就会调用套接字之前关联好的事件处理器来处 理这些事件。

虽然文件事件处理器以单线程方式运行,但通过与 I/O 多路复用程序来监听多 个套接字,文件事件处理器既实现了高性能的网络通信模型, 又可以很好地与 redis 服务器中其他同样以单线程方式运行的模块进行对接, 这保持了 Redis 内 部单线程设计的简单性。

11. Redis 中 set 和 zset 的区别

set 是 Redis 下的无序集合对象,是通过 intset 或者 hashtable 编码实现。

zset 是 Redis 下的有序集合对象,是通过 ziplist 或者 skiplist 编码实现。

ziplist 编码的有序集合对象使用压缩列表作为底层实现,每个集合元素使用两 个紧挨在一起的压缩列表节点来保存,第一个节点保存元素的成员,第二个节 点则保存元素的分值。对于 skiplist 编码实现,它同时内部包含一个字典和跳跃 表,程序都可以用 O(log(N))的复杂度往集合中添加成员,并可以用 O(1)的复杂 度查找给定成员的分值

12. Redis 分布式锁的实现方式

第一种,使用 redis 的 watch 命令进行实现。

watch 指令在 redis 事物中提供了 CAS 的行为。为了检测被 watch 的 keys 在是 否有多个 clients 同时改变引起冲突,这些 keys 将会被监控。如果至少有一个 被监控的 key 在执行 exec 命令前被其他客户端修改,整个事务将会回滚,不执 行任何动作,从而保证原子性操作,并且执行 exec 会得到 null 的回复。

具体工作机制:watch 命令会监视给定的每一个 key,当 exec 时如果监视的任 一个 key 自从调用 watch 后发生过变化,则整个事务会回滚,不执行任何动作。 注意 watch 的 key 是对整个连接有效的,事务也一样。如果连接断开,监视和 事务都会被自动清除.

第二种,使用 redis 的 setnx 命令进行实现。

先看一下这个相关的命令。

SETNX key value

如果 key 不存在,就设置 key 对应字符串 value。在这种情况下,该命令和 SET 一样。当 key 已经存在时,就不做任何操作。SETNX 是”SET if Not eXists”。

expire KEY seconds

设置 key 的过期时间。如果 key 已过期,将会被自动删除。

del KEY

删除 key

由于当某个 key 不存在的时候,SETNX 才会设置该 key。且由于 Redis 采用单 进行单线程模型,所以,不需要担心并发问题。那么,就可以利用 SETNX 的特 性维护一个 key,存在的时候,即锁被某个线程持有;不存在的时候,没有线 程持有锁。

并且还可以设置 key 的过期时间当作锁的超时时间,释放锁就直接可以将 key 删除即可。

13. Redis 遇到的问题和缺点。

  • Redis 不具备自动容错和恢复功能,主机从机的宕机都会导致前端部分 读写请求失败,需要等待机器重启或者切换数据库才能恢复。
  • Redis 主从复制过程中,第一个步骤是同步,需要采用全量复制,复制 过程中主机会 fork 出一个子进程对内存做一份快照,并将子进程的内存 快照保存为文件发送给从机,这一过程需要确保主机有足够多的空余内 存。若快照文件较大,对集群的服务能力会产生较大的影响。
  • Redis 作为缓存的话,还会出现缓存和数据库双写一致性的问题。

14. Redis 各个数据类型的使用场景。

Redis 支持物种数据类型:string(字符串)、hash(哈希)、list(列表)、 set(集合)及 zset(有序集合)。

  • String是简单的 key-value 类型,value 其实不仅可以是 String,也 可以是数字。常规 key-value 缓存应用;常规计数:微博数,粉丝数等。
  • hash是一个 string 类型的 field 和 value 的映射表,hash 特别适合 用于存储对象。存储部分变更的信息,比如说用户信息等。
  • list是一个链表。可以轻松实现最新消息排行榜等功能。另外一个 应用就是可以实现一个消息队列。可以利用 List 的 PUSH 操作,将任务 存在 List 中,然后工作线程再用 POP 操作将任务取出进行执行。也可以 通过 zset 构建有优先级的队列系统。此外,还可以将 redis 用作日志收 集器,实际上还是一个队列,多个端点将日志信息写入 redis,然后一个 worker 统一将所有日志写到磁盘。
  • set是一个没有重复值得集合。可以存储一些集合性的数据。在微 博应用中,可以将一个用户所有的关注人存在一个集合中,将其所有粉 丝存在一个集合中。Redis 还为集合提供了求交集、并集、差集等操作, 可以非常方便的实现如共同关注、共同喜好等功能。
  • zset相比 set,zset 增加了一个权重参数 score,使得集合中的元素 能够按 score 进行有序排列。比如一个存储全班同学成绩的 sorted set, 其集合 value 可以是同学的学号,而 score 就可以是其考试得分,这样 在数据插入集合的时候,就已经进行了天然的排序。也可以利用 zset 设 计带有优先级的队列。另外,还可以做排行榜应用,取 TOP N 操作。

15. Redis 是如何判断数据是否过期的呢?

Redis 通过一个叫做过期字典(可以看作是 hash 表)来保存数据过期的时间。过期字典的键指向 Redis 数据库中的某个 key(键),过期字典的值是一个 long long 类型的整数,这个整数保存了 key 所指向的数据库键的过期时间(毫秒精度的 UNIX 时间戳)。

过期字典是存储在 redisDb 这个结构里的:

typedef struct redisDb {
    ...

    dict *dict;     //数据库键空间,保存着数据库中所有键值对
    dict *expires   // 过期字典,保存着键的过期时间
    ...
} redisDb;

16. Redis 数据淘汰策略

Redis 提供了五种数据淘汰策略:

  • volatile-lru:使用 LRU 算法进行数据淘汰(淘汰上次使用时间最早的, 且使用次数最少的 key),只淘汰设定了有效期的。
  • keyallkeys-lru:使用 LRU 算法进行数据淘汰,所有的 key 都可以被淘汰。
  • volatile-random:随机淘汰数据,只淘汰设定了有效期的 key。
  • allkeys-random:随机淘汰数据,所有的 key 都可以被淘汰。
  • volatile-ttl:淘汰剩余有效期最短的 key。

此外,如果不设置如上策略的话,还有一种 noeviction 策略,当内存限制达到, 谁也不删除,返回错误。

17. Redis 的缓存雪崩

缓存雪崩,简单的理解就是:由于原有缓存失效(或者数据未加载到缓存中), 新缓存未到期间(缓存正常从 Redis 中获取),所有原本应该访问缓存的请求 都去查询数据库了,而对数据库 CPU 和内存造成巨大压力,严重的会造成数据 库宕机,造成系统的崩溃。对此,基本的解决思路有:

  • 考虑采用加锁或者队列的方式保证不会同时有大量的线程对数据库一次 性进行读写,避免缓存失效时对数据库造成太大的压力,虽然能够一定 的程度上缓解了数据库的压力,但是也降低了系统的吞吐量。
  • 分析用户的行为,不同的 key 设置不同的过期时间,尽量让缓存失效的 时间均匀分布。
  • 做二级缓存,或者双缓存策略。A1 为原始缓存,A2 为拷贝缓存,A1 失 效时,可以访问 A2,A1 缓存失效时间设置为短期,A2 设置为长期。

具体方法如下:

  • 事发前:实现 Redis 的高可用(主从架构+Sentinel 或者 Redis Cluster), 尽量避免 Redis 挂掉这种情况发生。
  • 事发中:万一 Redis 真的挂了,我们可以设置本地缓存(ehcache)+限流 (hystrix),尽量避免我们的数据库被干掉(起码能保证我们的服务还是能 正常工作的)。
  • 事发后:redis 持久化,重启后自动从磁盘上加载数据,快速恢复缓存数据。
  1. Redis 的缓存穿透

缓存穿透是指用户查询数据时,在数据库中没有,自然在缓存中也不会有。这 样就导致用户查询的时候,在缓存中找不到,每次都要去数据库中查询。查询 一个必然不存在的数据。比如文章表,查询一个不存在的 id,每次都会访问 DB, 如果有人恶意破坏,很可能直接对 DB 造成影响。对此,基本的解决思路有:

  • 如果查询数据库为空,直接设置一个默认值存放到缓存中,这样第二次 缓存中获取就有值了,而不会继续访问数据库,这种方法最简单粗暴。
  • 根据缓存数据 key 的规则进行过滤,比如说缓存 Key 为 mac 地址。这就 要求 key 必须有以顶的规则,这种方法可以缓解一部分的压力,但是无 法根治。
  • 采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的 BitSet 中, 不存在的数据将会被拦截掉,从而避免了对底层存储系统的查询压力。 对于布隆过滤器,可以用 BitSet 来构建。

具体方法如下:

由于请求的参数是不合法的(每次都请求不存在的参数),于是我们可以使用布隆 过滤器(BloomFilter)或者压缩 filter 提前拦截,不合法就不让这个请求到数据库层。

当我们从数据库找不到的时候,我们也将这个空对象设置到缓存里边去。下次 再请求的时候,就可以从缓存里边获取了。这种情况我们一般会将空对象设置 一个较短的过期时间。

关于更新

《Java技术指北面试宝典》预计一个月左右会有一次更新和完善。

大家可以在我的公众号【Java技术指北】后台回复【面试宝典最新版】即可获取最新版!如果觉得内容不错的话,欢迎转发和分享。


小白学堂 » Redis 面试大全

就聊挣钱,一个带着你做副业的社群。

立即查看 了解详情