Redis常见面试题及解答

Redis常见面试题及解答

  • Redis的版本
  • 1 Redis单线程的速度快的原因
  • 2 Redis为什么是单线程
  • 3 Redis的网络模型
  • 4 string(字符串)
    • 4.1 string使用场景
    • 4.2 string常用命令
    • 4.3 string特点及底层实现
  • 5 list(列表)
    • 5.1 list使用场景
    • 5.2 list常用命令
    • 5.3 list特点及底层实现
  • 6 Set(集合)
    • 6.1 Set使用场景
    • 6.2 Set常用命令
    • 6.3 Set特点及底层实现
  • 7 Zset(有序集合)
    • 7.1 Zset使用场景
    • 7.2 Zset常用命令
    • 7.3 Zset特点及底层实现
    • 7.4 跳表中的层高是怎么决定
  • 8 hash(字典)
    • 8.1 hash使用场景
    • 8.2 hash常用命令
    • 8.3 hash特点及底层实现
    • 8.4 Hash存数据时大Key的问题
  • 9 几种数据类型总结
    • 9.1 写入数据的时间复杂度
    • 9.2 ziplist(压缩列表)
    • 9.3 skiplist(跳表)
    • 9.4 hashtable(字典)
  • 10 Redis的AOF和RDB
    • 10.1 Redis缓存宕机如何处理
    • 10.2 RDB机制
      • 10.2.1 手动触发
      • 10.2.2 自动触发
      • 10.2.3 主从复制刚开始时触发
      • 10.2.4 save和bgsave命令的区别
      • 10.2.5 RDB的优缺点
    • 10.3 AOF机制
      • 10.3.1 文件重写原理(针对AOF文件比较大的情况,Redis做的优化)
      • 10.3.2 AOF也有三种触发机制
      • 10.3.3 AOF的优缺点
    • 10.4 RDB和AOF的选择
  • 11 Redis的主从复制
    • 11.1 主从复制介绍
    • 11.2 主从复制的目的及优点
    • 11.3 主从复制的实现过程
      • 11.3.1 全量同步具体步骤
    • 11.4 Redis如何保证主从的一致性
    • 11.5 Redis主从复制为什么不使用AOF?
  • 12 Redis哨兵机制
    • 12.1 哨兵机制的工作原理
    • 12.2 Redis哨兵机制中哨兵节点个数设置原则
    • 12.3 在从节点中选出新的主节点是随机的吗
  • 13 缓存击穿、缓存雪崩、缓存穿透区别及解决方案
    • 13.1 缓存击穿
    • 13.2 缓存雪崩
    • 13.3 缓存穿透
    • 13.4 布隆过滤器
  • 14 使用String和hash存储对象的对比
  • 15 Redis如何保证缓存的一致性
  • 16 Redis怎么实现分布式锁
    • 16.1 加锁/释放锁
    • 16.2 Redlock算法
    • 16.3 分布式锁和单机锁得区别
  • 17 Redis给一个键值对设置过期时间
  • 18 Redis的事务
  • 19 Redis集群方案
  • 20 Redis删除数据的方式
    • 20.1 Redis的key过期之后是立即删除吗?是什么机制?
    • 20.2 delete是阻塞还是非阻塞,或者Redis有没有非阻塞删除的方式
  • 21 Redis限流的几种方式
  • 22 Redis的stream是否用过
  • 23 Redis架构如何设计及优化

Redis的版本

截至当前日期:2023年8月28号,Linux系统下最新版本的Redis是7.2。

Linux系统Redis下载地址:https://redis.io/download

官方没有提供Windows系统的Redis,下载Windows系统的Redis:https://github.com/tporadowski/redis/releases

1 Redis单线程的速度快的原因

以下是来自GPT3.5的回答。

  1. 内存存储Redis是将数据存储在内存中,而内存的读写速度远高于磁盘的读写速度。这使得Redis能够快速地进行数据的读写操作。
  2. 非阻塞I/O操作Redis使用了异步非阻塞的I/O操作模型,通过使用事件循环机制(Event Loop),在单个线程中管理多个客户端的请求。这种模型允许Redis在同一时间处理多个并发请求,从而避免了线程切换的开销。
  3. 高效的数据结构Redis提供了多种高效的数据结构,如字符串、哈希、列表、集合、有序集合等。这些数据结构在存储和操作数据时都经过了优化,使得Redis在特定场景下能够以更少的操作完成更多的任务。
  4. 单线程避免了锁竞争: 多线程程序需要处理锁竞争和线程间同步的问题,而Redis的单线程模型避免了这些复杂性。这使得Redis的代码更容易维护和调试,同时也减少了因为锁竞争而引起的性能问题。
  5. 避免上下文切换: 多线程程序在进行上下文切换时会有一定的开销,而Redis单线程模型避免了这种开销。虽然单线程模型本身也会存在性能瓶颈,但由于Redis大部分操作是基于内存的,因此在绝大多数情况下,单线程模型的优势更大。
  6. 持久化策略的选择Redis提供了多种持久化方式,如RDB快照和AOF日志文件。虽然持久化操作会在某些情况下对性能产生影响,但用户可以根据自己的需求选择适合的持久化策略。

需要注意的是,虽然 Redis单线程模型在许多情况下表现出色,但也存在一些适用场景有限的情况。例如,当需要进行大量的计算密集型操作时,单线程模型可能会受到限制。此外, Redis通过多实例的方式也可以在一定程度上实现并行处理多个请求。

2 Redis为什么是单线程

以下是来自GPT3.5的回答。

  1. 避免锁竞争: 多线程程序通常需要处理锁竞争和线程同步的问题,这会引入复杂性并可能导致性能下降。Redis的单线程模型避免了这些问题,使代码更加简洁,维护起来更容易。
  2. 内存和CPU之间的平衡Redis主要用于高速数据存储和访问,而目前CPU的速度已经远远超过了内存的速度。因此,在大多数情况下,Redis遇到的瓶颈是在内存访问而不是CPU计算上。通过使用单线程模型,Redis能够充分利用CPU的计算能力,而不会受到内存访问速度的限制。
  3. 降低复杂性: 多线程编程会引入线程同步、死锁等问题,这些问题难以调试和管理。Redis的设计目标之一是简单和可靠,选择单线程模型有助于降低系统的复杂性,使其更加稳定和易于维护。
  4. 避免上下文切换开销: 在多线程系统中,线程切换会引入上下文切换的开销,这可能在高并发环境下导致性能下降。Redis的单线程模型避免了这种开销,因为不需要频繁切换线程上下文。
  5. 异步非阻塞操作Redis在单线程内使用异步非阻塞I/O操作模型,可以在单个线程中同时处理多个客户端请求,从而提供了高并发能力。
  6. 适用于特定场景Redis最初设计时是作为一个高性能的内存数据库和缓存系统,适用于数据存储、访问频繁、读写操作为主的场景。在这种场景下,单线程模型可以更好地发挥其优势。

需要注意的是,尽管Redis在很多情况下能够通过单线程模型实现高性能,但在某些特定场景,如大量的计算密集型操作,单线程模型可能会有一些限制。在这种情况下,可以考虑将计算部分从Redis中分离出来,或者使用Redis的多实例来实现并行处理。

3 Redis的网络模型

以下是来自GPT3.5的回答。
Redis的网络模型是基于事件驱动的、单线程的异步网络I/O多路复用模型。以下是Redis的网络模型的主要特点:

  1. 事件驱动架构Redis使用事件驱动架构来处理客户端请求和其他事件。这种架构通过一个事件循环(Event Loop)来监听并处理不同类型的事件,如新连接、数据读取、数据写入等。
  2. 单线程模型Redis主要运行在一个单独的线程中,负责处理所有的客户端请求和事件。这个线程不会被I/O操作阻塞,而是通过非阻塞方式处理多个客户端的请求。
  3. 多路复用(Multiplexing)Redis使用多路复用技术来同时监听多个套接字(sockets)。这种技术使得Redis能够在单个线程中同时处理多个客户端的连接和I/O操作。
  4. 文件事件和时间事件Redis的事件循环监听两种类型的事件。文件事件用于处理套接字的I/O操作,例如连接、读取和写入。时间事件用于执行定时任务,例如定期执行某些操作。
  5. 非阻塞I/ORedis的网络模型是非阻塞的,这意味着当一个客户端的请求需要进行I/O操作时,Redis不会阻塞整个线程,而是会继续处理其他客户端的请求,直到该I/O操作完成。
  6. 缓冲区Redis使用缓冲区来存储客户端发送的数据和要返回给客户端的数据。这种缓冲区机制有助于提高性能,避免频繁的数据复制操作。

通过这种事件驱动的网络模型,Redis能够在单线程下实现高并发和低延迟的数据处理能力。这种设计使得Redis在处理高速数据存储和访问时表现出色,同时减少了多线程带来的复杂性和线程同步的问题。然而,需要注意的是,Redis的单线程模型在某些特定的场景下可能会受到计算密集型任务的限制。

4 string(字符串)

RedisKey-Value的数据形式,所以Key一般都是string类型,主要是Value分成5种数据类型。

4.1 string使用场景

  1. 单值缓存: 最常见的使用场景之一是将数据缓存在Redis中,以加速数据访问。例如,可以将数据库查询结果、计算结果或其他频繁访问的数据存储在Redis中,以避免频繁查询数据库或执行复杂计算。
  2. 计数器string类型可以用来实现计数器,例如用于统计网站的页面访问次数、用户点击次数等。通过使用Redis的原子性操作(如INCRDECR命令),可以实现高效的计数功能。
  3. 分布式锁: 可以使用string类型来实现分布式锁。通过在Redis中设置一个特定的键作为锁,并使用带有NX参数的SET命令来尝试获取锁,可以确保只有一个客户端能够获得锁。
  4. 会话管理: 在Web应用中,可以使用string类型来存储用户会话数据,例如用户登录信息、购物车内容等。这可以帮助实现快速的会话管理。
  5. 消息队列string类型可以用于实现简单的消息队列,通过将消息序列化为字符串并使用LPUSHRPUSH命令将其添加到列表中,然后使用BRPOP等命令进行消息的出队操作。
  6. 数据序列化Redis中的string类型可以存储任何类型的数据,包括序列化的对象。这在某些情况下可以用来存储应用程序状态或配置信息。
  7. 位图Redis提供了一些位操作命令,可以将string类型用作位图数据结构。这在一些应用场景中,如统计用户在线状态、活跃用户等,非常有用。
  8. 缓存验证: 有时,需要将缓存与后端数据同步,以确保缓存数据的一致性。可以将数据存储为string类型,并在存储时添加一个版本号或时间戳。应用程序在使用缓存数据之前,可以检查版本号或时间戳,以验证缓存的有效性。

4.2 string常用命令

  1. SET key value [EX seconds] [PX milliseconds] [NX|XX]: 设置键的值。可以选择设置键的过期时间,以及设置条件(NX表示只在键不存在时设置,XX表示只在键已经存在时设置)。
  2. GET key: 获取键的值。
  3. DEL key [key ...]: 删除一个或多个键。
  4. INCR key: 将键的值加一,如果键不存在则创建并设置值为1。
  5. DECR key: 将键的值减一,如果键不存在则创建并设置值为-1。
  6. INCRBY key increment: 将键的值增加指定的整数。
  7. DECRBY key decrement: 将键的值减少指定的整数。
  8. APPEND key value: 在键的值后追加字符串。
  9. STRLEN key: 返回键的值的长度。
  10. GETRANGE key start end: 返回键的值从startend的子字符串。
  11. SETRANGE key offset value: 用指定字符串替换键值从指定偏移量开始的部分。
  12. MSET key value [key value ...]: 设置多个键的值。
  13. MGET key [key ...]: 获取多个键的值。
  14. SETNX key value: 将键的值设置为指定的值,仅当键不存在时。
  15. GETSET key value: 设置键的值,并返回之前的值。
  16. MSETNX key value [key value ...]: 同时设置多个键的值,仅当所有键都不存在时。
  17. STRLEN key: 返回键的值的长度。
  18. SETEX key seconds value: 设置键的值,并指定过期时间(以秒为单位)。
  19. PSETEX key milliseconds value: 设置键的值,并指定过期时间(以毫秒为单位)。
  20. SETBIT key offset value: 设置或清除键的值在指定偏移量上的位。
  21. GETBIT key offset: 获取键的值在指定偏移量上的位。

4.3 string特点及底层实现

Redisstring类型是最基本的数据类型之一,用于存储字符串数据。它具有以下特点:

  1. 存储任意数据: 虽然称为string,但实际上可以存储任何类型的字符串数据,包括文本、二进制数据等。
  2. 简单的键值对结构string类型采用键值对的方式存储数据,每个键都对应一个字符串值。
  3. 高效的操作Redisstring类型支持丰富的操作,如获取、设置、追加、增加、减少等,这些操作都能在常数时间内完成,具有高效性能。
  4. 自动内存管理Redis会自动进行内存管理,根据数据的大小和情况,选择性地使用不同的内存优化策略,如使用RAW编码、INT编码等。
  5. 高速缓存功能string类型是存储缓存数据的常见选择。通过将数据存储在string类型中,可以在不需要时轻松地清除、替换或更新缓存。
  6. 过期时间string类型可以设置过期时间,用于实现数据的自动过期和失效。过期时间到达后,数据会被自动删除。

string类型底层实现:
Redisstring类型的底层数据实际上使用一个叫做SDSSimple Dynamic String)的数据结构来进行存储。SDS是一种可动态调整大小的字符串表示,它允许字符串的长度根据实际存储的内容进行变化,从而避免了频繁的内存重新分配操作。SDS还会记录字符串的长度、可用空间以及字符数组。

SDS的结构:

struct sdshdr {
    int len;         // 字符串的已使用长度
    int free;        // 未使用的空间长度
    char buf[];      // 字节数组,存储实际数据
};

SDS的结构中,len表示已使用的字符串长度,free表示未使用的空间长度,而buf是一个字符数组,存储实际的字符串数据。SDS可以根据字符串的长度进行动态扩展和收缩,从而在保证高效内存使用的同时,提供了快速的字符串操作。

SDS的特点:

  1. 动态性SDS可以根据字符串的长度动态地调整内存的大小,避免了固定大小带来的内存浪费问题。
  2. 二进制安全SDS可以存储任意二进制数据,因此在Redis中,字符串不仅仅限于存储文本数据,还可以存储图片、音频等任何二进制数据。
  3. 惰性空间释放: 当SDS的字符串被修改时,不会立即释放被修改前的内存,而是在必要的时候才进行释放,这可以避免频繁的内存分配和释放操作。
  4. 预分配空间SDS会根据字符串的长度预分配额外的空间,以减少频繁的内存重新分配。
  5. 快速获取长度: 由于SDS在内部维护了字符串的长度信息,所以可以在O(1)的时间复杂度内获取字符串的长度。
  6. 常数时间复杂度操作SDS支持在字符串的末尾追加、删除和修改字符,这些操作的时间复杂度都是O(1)

  • 当存储短字符串时,Redis可能会使用int编码来存储,这会将字符串转换为整数,节省内存空间。而在需要的情况下,Redis还会使用embstr编码,将短字符串直接嵌入到数据对象中,避免了分配和释放额外的内存。

  • 对于长字符串,Redis会使用raw编码,这是一种典型的字符串存储方式,其格式包括字符串长度和字符数组。在SDS中,字符数组的大小会比实际的字符串长度大,以便在需要进行追加操作时,不必频繁重新分配内存。

5 list(列表)

5.1 list使用场景

Redislist类型是一个双向链表数据结构,它支持在列表的两端执行插入、删除、获取操作。以下是一些适合使用Redislist类型的场景:

  1. 消息队列Redislist可以用作简单的消息队列。生产者可以使用LPUSH命令将消息添加到列表的头部,消费者可以使用BRPOPBLPOP命令从列表的尾部阻塞式地获取消息。这种方式实现了一种基本的发布-订阅模式。
  2. 任务队列: 类似于消息队列,list也可以用于构建任务队列。生产者将需要执行的任务放入列表,消费者从列表中获取任务并执行。
  3. 实时数据流: 可以将实时产生的数据放入list中,供客户端按需获取。这在实时分析、监控等场景中非常有用。
  4. 活动日志: 将用户的活动日志按时间顺序存储在list中,以便后续分析和查看用户的活动历史。
  5. 历史记录: 用于存储应用程序的历史操作记录,比如聊天应用中的消息历史记录。
  6. 排行榜: 可以将用户分数和ID存储在list中,用于实现排行榜功能。通过使用ZADD命令,还可以将成员按照分数有序地存储。
  7. 数据缓存: 通过list存储一些热门数据,例如近期热门文章、商品等。可以使用LPUSHRPUSHLPOP等命令来更新缓存数据。
  8. 任务调度: 将需要定时执行的任务存储在list中,并使用BLPOP命令在合适的时间获取并执行这些任务。
  9. 聊天应用list可以用于存储聊天消息。通过LPUSH将新消息添加到聊天历史中,然后使用LRANGE获取聊天记录。

需要注意的是,虽然list是一个有用的数据类型,但在处理大型列表时,需要考虑性能和内存的问题。过长的链表可能会影响操作的速度和内存使用。此外,如果需要更复杂的数据结构或功能,可能需要考虑使用其他数据类型,如hashset

5.2 list常用命令

  1. LPUSH key value [value ...]: 将一个或多个值插入到列表的左侧(头部)。
  2. RPUSH key value [value ...]: 将一个或多个值插入到列表的右侧(尾部)。
  3. LPOP key: 移除并返回列表的左侧(头部)的值。
  4. RPOP key: 移除并返回列表的右侧(尾部)的值。
  5. LINDEX key index: 返回列表中指定索引位置的元素。
  6. LLEN key: 返回列表的长度。
  7. LRANGE key start stop: 返回列表中指定范围的元素。范围是闭区间,包括startstop位置的元素。
  8. LTRIM key start stop: 对列表进行修剪,只保留指定范围内的元素。范围是闭区间。
  9. LINSERT key BEFORE|AFTER pivot value: 在列表中找到指定值pivot,然后在其前面或后面插入新的值。
  10. LREM key count value: 从列表中删除指定数量的与值匹配的元素。
  11. BLPOP key [key ...] timeout: 阻塞式地从列表的左侧弹出一个或多个元素,如果列表为空则阻塞等待,直到有元素可弹出或超时。
  12. BRPOP key [key ...] timeout: 类似于BLPOP,但从列表的右侧弹出元素。
  13. RPOPLPUSH source destination: 弹出source列表的最后一个元素,并将它添加到destination列表的头部。
  14. LSET key index value: 将列表中指定索引位置的元素设置为新值。
  15. LREM key count value: 从列表中删除指定数量的与值匹配的元素。
  16. RPOPLPUSH source destination: 弹出source列表的最后一个元素,并将它添加到destination列表的头部。

这些是一些常用于处理Redislist类型的命令。根据具体的应用场景和需求,可以选择适合的命令来操作和管理列表数据。

5.3 list特点及底层实现

Redislist类型是一个双向链表数据结构,用于存储有序的、可重复的元素列表。每个元素都可以在列表的头部或尾部进行插入、删除和访问操作。以下是list类型的特点:

  1. 有序存储list类型中的元素是有序的,每个元素都有一个相对位置。可以根据插入顺序来访问和操作元素。
  2. 可重复元素list类型允许存储相同的元素,即使是重复的元素也会保留。
  3. 双向链表list类型实际上是一个双向链表,支持在列表的头部和尾部进行插入、删除操作。这使得头部和尾部操作的时间复杂度都为O(1)
  4. 支持索引访问: 可以通过索引位置来访问list中的元素,这使得可以在常数时间内访问任意位置的元素。
  5. 支持范围查询: 可以通过索引范围来获取一段连续的元素子列表。
  6. 用于队列和栈list类型既可以作为队列使用(先进先出),也可以作为栈使用(先进后出)。

list类型底层实现:
Redislist是一种基本的数据结构,它是一个有序的字符串元素集合。list可以包含重复的元素,而且可以在list的两端进行插入和删除操作。在底层,Redislist是通过双向链表(doubly linked list)压缩列表(ziplist)两种数据结构来实现的。

  1. 压缩列表(ziplist)
  • list中的元素数量比较少且元素较小时,Redis会使用压缩列表作为底层数据结构,这是一种紧凑的连续内存结构。
  • 压缩列表通过将多个元素紧密地存储在一块内存中,减少了指针等开销,从而节省了内存空间。
  • 它适用于类似于字符串、整数等较小数据类型的元素。
  • 压缩列表支持O(1)时间复杂度的尾部插入和弹出操作,但在中间插入或删除元素时可能会引发内存的重新分配和复制,因此效率较低。
  1. 双向链表(doubly linked list)
  • list的元素数量较多或元素较大时,Redis会选择使用双向链表作为底层数据结构,以便更好地处理插入、删除等操作。
  • 双向链表通过指针将元素连接起来,支持快速的插入和删除操作,但相比压缩列表会占用更多的内存空间。
  • 它适用于任何大小的元素,尤其是在需要频繁地在中间插入或删除元素的情况下。
  1. 双向链表(doubly linked list)压缩列表(ziplist)的切换
  • Redis会根据list的当前大小和操作模式来自动选择使用压缩列表或双向链表。
  • list的元素数量增加时,Redis可能会将压缩列表升级为双向链表,以便更好地处理大量元素。
  • 反之,当list的元素数量减少时,Redis可能会将双向链表降级为压缩列表,以节省内存。

总结:
Redislist底层实现原理涉及了压缩列表和双向链表这两种数据结构,根据元素数量和大小的不同,选择合适的数据结构来平衡性能和内存使用。这种灵活性使得Redislist在不同场景下都能够高效地存储和处理数据。

6 Set(集合)

6.1 Set使用场景

Redisset类型是一个无序的、不重复的数据集合。它可以存储多个不同的元素,但不允许重复。以下是一些适合使用Redisset类型的场景:

  1. 标签和关键字存储: 适用于存储文章、商品、图片等对象的标签、关键字或标识。每个对象可以对应一个set,其中存储与该对象相关的标签。
  2. 好友关系: 可以使用set来存储用户之间的好友关系。每个用户对应一个set,其中存储与该用户关联的好友。
  3. 共同兴趣爱好: 类似于好友关系,可以使用set存储用户的兴趣爱好。比较两个用户的兴趣爱好集合,可以找到共同的兴趣爱好。
  4. 唯一值存储set中不允许重复的特性使得它适合用来存储唯一值,例如在线用户、IP地址、手机号等。
  5. 投票系统: 用于存储用户的投票选项,确保每个用户只能投一次票。
  6. 集合操作Redis提供了一系列操作set的命令,如求交集、并集、差集等。这些操作对于数据分析和数据处理非常有用。
  7. 排他性操作: 可以使用set实现排他性操作,例如某个资源是否被占用,以及哪些用户正在使用它。
  8. 用户在线状态: 将在线用户的ID存储在一个set中,通过SADDSREM命令来维护用户的在线状态。
  9. 黑名单和白名单: 可以将需要屏蔽或允许的对象(用户、IP等)存储在set中,用于实现黑名单或白名单功能。

需要注意的是,虽然set类型适合存储不重复的数据,但它不适合存储有序数据。如果需要有序性,可以考虑使用zset(有序集合)类型。在选择数据类型时,需要根据具体的需求和场景来做出决策。

6.2 Set常用命令

  1. SADD key member [member ...]: 将一个或多个成员添加到集合中。
  2. SREM key member [member ...]: 从集合中移除一个或多个成员。
  3. SISMEMBER key member: 检查成员是否存在于集合中,返回布尔值。
  4. SCARD key: 返回集合中成员的数量。
  5. SMEMBERS key: 返回集合中所有成员。
  6. SRANDMEMBER key [count]: 随机返回集合中一个或多个成员,如果指定了count参数,则返回不重复的成员。
  7. SPOP key [count]: 随机移除并返回集合中一个或多个成员。
  8. SDIFF key [key ...]: 返回给定集合之间的差集。
  9. SINTER key [key ...]: 返回给定集合的交集。
  10. SUNION key [key ...]: 返回给定集合的并集。
  11. SDIFFSTORE destination key [key ...]: 将给定集合之间的差集存储到另一个集合中。
  12. SINTERSTORE destination key [key ...]: 将给定集合的交集存储到另一个集合中。
  13. SUNIONSTORE destination key [key ...]: 将给定集合的并集存储到另一个集合中。
  14. SMOVE source destination member: 将成员从一个集合移动到另一个集合。
  15. SPOP key [count]: 随机移除并返回集合中一个或多个成员。

6.3 Set特点及底层实现

Redisset类型是一种无序的、不重复的数据集合。它具有以下特点:

  1. 无序性set类型中的元素是无序的,没有特定的排列顺序。
  2. 不重复性set类型保证其中不会存在重复的元素,每个元素只能出现一次。
  3. 高效的成员判定set类型支持高效地判断一个成员是否存在,操作的时间复杂度为O(1)
  4. 集合操作set类型支持集合操作,如求交集、并集、差集等,这些操作对于数据处理和分析非常有用。
  5. 适合存储唯一值:由于不允许重复元素,set类型适合用来存储唯一的标识、ID、用户名等数据。
  6. 无索引set类型不支持通过索引来访问元素,只能通过成员判定操作来检查元素是否存在。
  7. 自动内存管理Redis会根据集合中元素的数量,自动选择使用intset(整数集合)或hashtable(哈希表)来存储数据,以便节省内存。

Set类型底层实现:
Redisset类型的底层实现有两种方式:intsethashtable

  1. intset(整数集合):当set中的所有元素都可以表示为整数时,Redis会使用intset来存储数据。intset是一种紧凑的、有序的数据结构,使用连续的内存块来存储整数数据。它可以节省内存,同时支持高效的成员判定操作。
  2. hashtable(哈希表):当set中的元素不能全都表示为整数,或者元素数量较大时,Redis会使用 hashtable来存储数据。 hashtable是一种常见的散列数据结构,可以用来存储非整数类型的元素。虽然会占用更多的内存,但它提供了更大的灵活性。

总结:
Redisset类型在底层使用intsethashtable这两种不同的数据结构来存储数据,以便根据元素类型和数量来选择合适的存储方式,以达到更好的内存效率和性能。

7 Zset(有序集合)

7.1 Zset使用场景

Redis的有序集合(Zset)是一种特殊的集合数据类型,它与普通集合相比,每个元素都有一个关联的分数(score),并且元素按照分数的顺序排列。以下是一些适合使用RedisZset类型的场景:

  1. 排行榜和计分系统Zset可以用于实现排行榜,例如游戏中的玩家积分排名、音乐应用中的热门歌曲排名等。每个元素表示一个用户或歌曲,分数表示用户的积分或歌曲的播放次数。
  2. 实时热门数据: 用于存储实时热门数据,例如热门文章、热门商品等。根据分数来判断热门程度,可以轻松地获取热门数据。
  3. 社交网络关注和粉丝关系: 可以使用Zset存储用户的关注关系,每个元素表示一个用户,分数表示关注时间。还可以使用Zset存储用户的粉丝关系,分数表示粉丝关注时间。
  4. 范围查询Zset支持根据分数范围进行查询,这对于获取一定范围内的数据非常有用。例如,在时间轴上查询一段时间内的数据。
  5. 定时任务调度: 可以使用Zset来实现定时任务的调度。将任务的执行时间作为分数,任务内容作为成员,通过定时地获取分数小于当前时间的成员来执行任务。
  6. 带有权重的投票系统Zset可以用于实现带有权重的投票系统,每个元素表示一个投票项,分数表示该投票项的权重。可以用来进行复杂的投票计算。
  7. 基于距离的地理位置查询: 如果使用Zset的分数表示地理位置的坐标,可以实现基于距离的地理位置查询,比如查找附近的商店、用户等。
  8. 带有过期时间的数据Zset的分数可以表示数据的过期时间,结合定时任务,可以实现一些带有过期时间的数据清理操作。

需要注意的是,虽然Zset提供了有序性和分数的特性,但由于其内部实现是基于跳跃表和散列表,所以在性能方面要综合考虑。在选择数据类型时,需要根据具体的需求和场景来做出决策。

7.2 Zset常用命令

  1. ZADD key score member [score member ...]: 将一个或多个成员添加到有序集合中,每个成员关联一个分数。
  2. ZREM key member [member ...]: 从有序集合中移除一个或多个成员。
  3. ZSCORE key member: 获取成员在有序集合中的分数。
  4. ZINCRBY key increment member: 增加成员的分数,可以指定增量。
  5. ZCARD key: 获取有序集合中成员的数量。
  6. ZRANK key member: 获取成员在有序集合中的排名(从小到大)。
  7. ZREVRANK key member: 获取成员在有序集合中的排名(从大到小)。
  8. ZRANGE key start stop [WITHSCORES]: 返回有序集合中指定范围的成员。通过WITHSCORES选项,可以同时返回成员的分数。
  9. ZREVRANGE key start stop [WITHSCORES]: 类似于ZRANGE,但是结果从大到小排列。
  10. ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count]: 根据分数范围返回成员。可以通过WITHSCORES选项返回分数,通过LIMIT选项进行分页。
  11. ZREVRANGEBYSCORE key max min [WITHSCORES] [LIMIT offset count]: 类似于ZRANGEBYSCORE,但是结果从大到小排列。
  12. ZCOUNT key min max: 计算有序集合中分数在给定范围内的成员数量。
  13. ZREMRANGEBYRANK key start stop: 移除有序集合中指定排名范围内的成员。
  14. ZREMRANGEBYSCORE key min max: 移除有序集合中分数在给定范围内的成员。
  15. ZINTERSTORE destination numkeys key [key ...]: 计算给定的一个或多个有序集合的交集,并将结果存储在新的有序集合中。
  16. ZUNIONSTORE destination numkeys key [key ...]: 计算给定的一个或多个有序集合的并集,并将结果存储在新的有序集合中。

7.3 Zset特点及底层实现

Redis的有序集合(Zset)是一种有序的、不重复的数据集合,每个元素都有一个与之关联的分数(score),用于排序。以下是有序集合的特点:

  1. 有序性:有序集合中的元素是按照分数进行排序的,可以根据分数从小到大或从大到小进行遍历。
  2. 不重复性:有序集合中不允许有重复的元素,每个元素是唯一的。
  3. 支持范围查询:可以根据分数范围来获取一段分数内的元素。
  4. 支持排名:可以获取元素在有序集合中的排名,从小到大或从大到小。
  5. 高效的插入和删除:有序集合支持高效地插入和删除元素,时间复杂度为O(log N)N为元素数量。
  6. 适合用于排行榜:有序集合适用于实现排行榜,例如用户积分排名、热门内容排名等。

Zset类型底层实现原理:
RedisZset底层实现实际上使用了两种数据结构来进行存储和索引:压缩列表(ziplist)跳跃表(skiplist)

  1. 压缩列表(ziplist):当有序集合中的成员数量较少,且成员的值较短时,Redis使用压缩列表作为底层存储结构。压缩列表是一种紧凑的、高效的数据结构,适用于存储较小的数据。
  2. 跳跃表(skiplist):当有序集合中的成员数量较多,或者成员的值较长时,Redis会使用跳跃表作为底层存储结构。跳跃表是一种有序数据结构,支持快速的插入、删除和范围查找操作,能够在较大数据量下保持良好的性能。

这两种底层存储结构的选择是基于成员数量和成员值的大小来决定的,以便在不同场景下实现最佳的性能和内存占用。由于压缩列表适用于存储较小的数据,而跳跃表适用于存储较大的数据,因此Redis会根据具体情况来做出选择。

7.4 跳表中的层高是怎么决定

参考1:Redis的数据结构之跳表
层高
节点的层高最小值为1,最大值是ZSKIPLIST_MAXLEVEL,Redis中节点层高的值为32。

zslRandomLevel函数
每次创建一个新跳跃表节点的时候,程序都会根据幂次定律(zslRandomLevel,越大的数出现的概率越小)随机生成一个介于1和32之间的值作为level数组的大小,这个大小就是层的“高度”。 节点层高确定之后便不会在修改。

8 hash(字典)

8.1 hash使用场景

Redishash类型是一种用于存储字段和值之间映射关系的数据结构。每个hash可以存储多个字段和相应的值,类似于一个键值对的集合。以下是一些适合使用Redishash类型的场景:

  1. 存储对象属性:适用于存储对象的属性。每个hash可以表示一个对象,字段表示对象的属性名称,值表示属性的值。例如,存储用户对象的属性。
  2. 缓存:当需要缓存一个复杂的数据结构时,hash类型可以用来存储缓存数据。每个hash表示一个缓存项,字段表示缓存项的唯一标识,值表示缓存数据。
  3. 实时统计:适用于存储实时统计数据。例如,存储网站的页面浏览次数、用户在线时长等信息,每个hash表示一个统计项,字段表示统计项的名称,值表示统计值。
  4. 配置存储:可以将应用程序的配置信息存储在hash中,字段表示配置项的名称,值表示配置项的值。
  5. 存储嵌套数据hash类型可以用于存储嵌套的数据结构,类似于JSON。例如,可以存储文章的标题、内容、作者等信息。
  6. 存储用户购物车:适用于存储用户的购物车信息。每个hash表示一个用户的购物车,字段表示商品的ID,值表示商品的数量。
  7. 存储表单数据: 当需要存储表单数据时,hash类型可以用来存储表单字段和对应的值。每个hash表示一个表单项,字段表示表单字段的名称,值表示表单字段的值。
  8. 存储计数器hash类型可以用于存储计数器。每个hash表示一个计数项,字段表示计数项的名称,值表示计数值。

8.2 hash常用命令

  1. HSET key field value: 设置指定hash中字段的值。
  2. HGET key field: 获取指定hash中字段的值。
  3. HDEL key field [field ...]: 删除指定hash中的一个或多个字段。
  4. HEXISTS key field: 检查指定hash中是否存在指定的字段。
  5. HLEN key: 获取指定hash中字段的数量。
  6. HKEYS key: 获取指定hash中所有字段的列表。
  7. HVALS key: 获取指定hash中所有字段的值的列表。
  8. HMSET key field value [field value ...]: 设置指定hash中多个字段的值。
  9. HMGET key field [field ...]: 获取指定hash中多个字段的值。
  10. HINCRBY key field increment: 将指定hash中字段的值增加指定的整数。
  11. HINCRBYFLOAT key field increment: 将指定hash中字段的值增加指定的浮点数。
  12. HSETNX key field value: 当指定hash字段不存在时,设置字段的值。
  13. HGETALL key: 获取指定hash中所有字段和值的列表,返回一个关联数组。
  14. HSCAN key cursor [MATCH pattern] [COUNT count]: 对hash进行增量式迭代,返回匹配给定模式的元素。

8.3 hash特点及底层实现

Redishash类型是一种用于存储字段和值之间映射关系的数据结构。每个hash可以存储多个字段和相应的值,类似于一个键值对的集合。以下是hash类型的特点:

  1. 字段-值映射hash类型用于建立字段和值之间的映射关系,类似于一个小型的散列表。
  2. 灵活的字段数hash类型中可以存储多个字段,每个字段都有一个对应的值,不同hash对象之间的字段数可以不同。
  3. 高效的查找和修改:由于采用了散列表的结构,hash类型支持在常数时间内进行字段的查找、修改和删除操作。
  4. 适合存储对象属性hash类型适合存储对象的属性,每个字段表示对象的一个属性,值表示属性的值。
  5. 适合存储小数据集:当存储的字段数较少时,hash类型比使用多个hash类型更经济、更高效。

hash类型底层实现原理:
Redishash类型底层是使用了两种不同的数据结构来实现:ziplist(压缩列表)和hashtable(哈希表)。

  1. ziplist(压缩列表):当hash类型的字段数量较少且字段名和值都是短字符串时,Redis会使用ziplist来存储数据。ziplist是一种紧凑的、有序的数据结构,将字段和值按照一定的方式进行打包。ziplist适用于小型的hash,它可以节省内存。
  2. hashtable(哈希表):当hash类型的字段数量较多或字段名和值是长字符串时,Redis会使用hashtable来存储数据。hashtable是一种典型的散列数据结构,用于支持更大规模的hash对象。

通过根据hash对象的大小和特点选择合适的底层数据结构,Redis在实现hash类型时可以充分发挥不同数据结构的优势,以提供高效的性能和内存使用。

8.4 Hash存数据时大Key的问题

Redis中的"大Key"问题是指存储在Redis中的单个键(Key)所关联的数据非常大,导致一些性能和管理方面的问题。这种情况可能会对Redis的性能、内存使用和数据管理产生负面影响。以下是大Key问题可能带来的一些问题和解决方法:

产生原因:

  1. 内存消耗: 大Key会占用大量的内存空间,如果大量的内存被用于存储少量的大Key,会导致内存资源的浪费。
  2. 影响性能: 对于大Key的读写操作可能会影响Redis的性能,因为读写一个大Key可能会耗费更多的时间。
  3. 数据迁移问题: 在Redis的集群环境中,数据可能会在节点之间进行迁移。如果大Key的迁移频繁发生,会增加网络开销和延迟。

解决方法:

  1. 分割大Redis: 将一个大Redis拆分成多个小Redis,每个小Redis存储其中的一部分数据。例如,如果要存储一个大的JSON对象,可以将其拆分为多个字段存储在不同的小Key中。
  2. 压缩数据: 对于存储在大Key中的数据,可以考虑对数据进行压缩,以减少内存占用。
  3. 分片: 如果大Key无法避免,可以考虑将数据分散存储在多个小Key中,以减少单个Key的大小。
  4. 使用数据结构: 根据数据的特点,选择适当的数据结构来存储,例如将大数据拆分成多个小的有序集合(Zset)或列表(List)来存储。
  5. 设置合适的过期时间: 如果大Key是临时性的数据,可以设置适当的过期时间,确保数据不会长时间占用内存。
  6. 定期清理: 对于不再需要的大Key,应该及时进行删除操作,以释放内存。

总结:
避免大Key问题可以通过合理的数据设计、数据拆分、数据压缩以及定期的数据管理来实现。在设计数据存储方案时,需要综合考虑数据的大小、访问频率和生命周期等因素,以便有效地管理数据并确保Redis的性能和资源利用率。

9 几种数据类型总结

9.1 写入数据的时间复杂度

  1. String(字符串):写入和更新字符串数据的时间复杂度都是常数时间O(1)
  2. List(列表):在列表的头部或尾部添加元素的时间复杂度是常数时间O(1)。在其他位置插入操作的时间复杂度是线性的,取决于列表的长度,所以是O(N)
  3. Set(集合):平均情况下,添加元素到集合的时间复杂度是常数时间O(1)。但是,最坏情况下可能是O(n),其中n是集合中元素的数量,这是因为Redis在集合元素数量增加时会进行扩容。
  4. Sorted Set(有序集合):平均情况下,添加和删除元素的时间复杂度是常数时间O(1)。但是在移除并返回分数最低的成员时是O(log N)
  5. Hash(哈希):平均情况下,添加或更新字段到哈希的时间复杂度是常数时间O(1)。但是,在最坏情况下,也就是一次性添加大量的字段到hash时是O(n),其中n是添加到哈希中字段的数量,这是因为Redis在哈希字段数量增加时会进行扩容。

需要注意的是,上述时间复杂度都是平均情况下的估算,实际的性能可能会受到多种因素的影响,如数据大小、服务器负载、内存管理策略等。在实际使用中,可以根据具体的应用场景和需求,选择合适的数据类型来存储数据,并综合考虑数据的读写性能。


参考1:redis数据类型(5种)和底层实现
参考2:redis数据类型底层实现

9.2 ziplist(压缩列表)

list列表底层也有使用压缩列表的数据结构,它是通过紧密相连的节点来存储元素,来达到节约内存的目的,有序集合和hash对象使用压缩列表实现的数据构造类似,都是相邻两个节点来存储一个元素的信息,只不过hash存储的是键值对,相邻两个节点分别存储hash对象的键和值,保存键的节点在前, 保存值的节点在后,两两一组。而zset存储的是元素值和排序用的double分数,数据结构图如下:
Redis常见面试题及解答_第1张图片

9.3 skiplist(跳表)

跳表全称为跳跃列表,是一个允许快速查询,插入和删除的有序数据链表。跳跃列表的平均查找和插入时间复杂度都是O(logn)。快速查询是通过维护一个多层次的链,通过有限次范围查找来实现的,它的效率和红黑树不相上下,但是实现原理相对于红黑树来说简单很多。

跳跃表节点定义:

typedef struct zskiplistNode {
     //成员对象
     robj *robj;
    //分值
    double score;
    //后退指针
    struct zskiplistNode *backward;
    //层级信息数组,用来实现跳跃,对应于下图的L1、L2、L3....每个节点所拥有的层级信息是随机的
    struct zskiplistLevel {
        /* 对应level的下一个节点 */
        struct zskiplistNode *forward;
        /* 从当前节点到下一个节点的跨度 */
        unsigned int span;
      } level[];
} zskiplistNode

跳表定义:

typedef struct zskiplist {
    
    //跳跃表的头结点和尾节点,通过这两字段+zskiplistNode的backward和zskiplistLevel的forward可以实现正向和反向遍历
    struct zskiplistNode *header, *tail;
    //当前跳跃表的长度,保留这个字段的主要目的是可以以O(1)复杂度内获取跳跃表的长度
    unsigned long length; 
    /* 跳跃表的节点中level的最大值。但是不包括头结点,头结点包含所有的层级信息---ZSKIPLIST_MAXLEVEL = 32。level的值随着跳跃表中节点的插入和删除随时动态调整 */     
    int level;
} zskiplist;

结构图如下:
Redis常见面试题及解答_第2张图片
Redis使用跳表存储集合元素和分值的同时,还使用字典来保持元素和分值的映射关系,利用字典的特性使zscord()的实现只有O(1)复杂度。其定义如下:

typedef struct zset {
    //数据字典:用来存储元素和分数的对应关系
    dict *dict;
    //跳表:用来存储集合元素及元素直接的跳跃关系
    zskiplist *zsl;
} zset;

9.4 hashtable(字典)

Redis中的hashtableJava中的HashMap类似,都是通过"数组+链表"的实现方式解决部分的哈希冲突。

字典的定义

typedef struct dict {
    //类型特定函数,可以根据所存数据类型的不同,指向不同的数据操作实现
    dicType **type;
    /私有数据,配合type使用
    void *privdate;
    //hash表
    dictht ht[2];
    //rehash索引,当不进行rehash时值伟-1
    int trehashidx;
       
}dict;

hash表:

typedef struct dictht {
    //哈希表数组
    dicEntry **table;
    //哈希表大小
    unsigned long size;
    //已有节点数量
    unsigned long used;
       
}dictht;

hash表节点:

typedef struct dicEntry {
    //键
    void *key
    //值
    union{
        void *val;
    }v;
    
    //下一个节点指向
    struct dicEntry *next;
       
}dicEntry;

RedisHash的字典的结构图:
Redis常见面试题及解答_第3张图片

10 Redis的AOF和RDB

10.1 Redis缓存宕机如何处理

  1. 监控和警报系统:设置监控和警报系统,以便在Redis出现故障时及时收到通知。这可以帮助快速采取行动,减少停机时间。
  2. 备份和恢复:定期备份Redis数据是一种好的实践。如果发生宕机,可以使用备份数据来恢复Redis的状态。确保备份是定期进行的,并测试备份的可恢复性。
  3. 高可用架构:考虑使用Redis的高可用架构,如主从复制或Redis集群。主从复制允许在主Redis实例宕机时使用从实例继续提供服务。Redis集群则可以将数据分布在多个节点上,提高可用性和性能
  4. 持久化策略Redis提供了两种主要的持久化方式,分别是RDB快照和AOF日志。通过配置适当的持久化策略,可以在宕机发生时最大程度地减少数据丢失。
  5. 故障转移:如果使用主从复制,当主节点宕机时,可以手动或自动地将从节点提升为新的主节点。这将允许继续提供服务,同时可以修复原始主节点。
  6. 负载均衡:如果使用Redis集群,可以通过负载均衡将请求分发到不同的Redis节点上。这有助于减轻单个节点宕机的影响。
  7. 监控和分析:使用监控工具来实时监测Redis的性能和健康状况。这将能够预测问题并采取措施,以防止宕机。
  8. 容错处理:在应用程序代码中实现适当的容错处理。如果Redis缓存不可用,应用程序应该能够从备用数据源获取数据或以某种方式继续运行,而不会导致严重故障。

10.2 RDB机制

RDB(Redis Database Backup)Redis默认的持久化方式,它是在指定的时间间隔内把数据以快照的形式保存在磁盘上,实际是写入到二进制文件中,默认的文件名为dump.rdb

  • 可以在配置文件中设置触发RDB快照的条件,例如每隔一段时间或在达到一定的写入操作次数后。
  • 当触发条件满足时,Redisfork一个子进程。这个子进程负责生成RDB文件,这个RDB文件是一个二进制文件,包含了当前内存中的数据快照。
  • 在子进程生成RDB文件的过程中,Redis主进程仍然继续处理命令请求。
  • 一旦RDB文件生成完毕,Redis会用新的RDB文件覆盖旧的RDB文件,以实现数据持久化。
  • Redis重启时,它会尝试加载最新的RDB文件,将数据恢复到内存中。

在安装了Redis之后,所有的配置都是在redis.conf文件中,里面保存了RDBAOF两种持久化机制的各种配置。

RDB机制的触发时机有两个:手动触发、自动触发。

10.2.1 手动触发

手动触发是通过向Redis发送特定的命令来手动触发RDB快照生成,比如使用savebgsave命令。

但是使用save命令会阻塞当前Redis服务器,因为它会在生成RDB文件时暂停处理其他命令请求,直到RDB过程完成为止。如果存在旧的RDB文件,一旦新的RDB文件生成完毕,Redis会用该文件覆盖旧的RDB文件,以实现数据持久化。

连接Redis服务器的客户端可能都是几万或者是几十万,这种方式会有性能问题,不推荐使用。

10.2.2 自动触发

自动触发是通过配置文件来实现的。在redis.conf配置文件中,里面有触发save命令的配置,可以去设置。

比如“save m n”。表示m秒内数据集存在n次修改时,自动触发RDB快照。
默认如下配置:

  • save 900 1:表示900秒内如果至少有1个key的值变化;
  • save 300 10:表示300秒内如果至少有10个key的值变化;
  • save 60 10000:表示60秒内如果至少有10000个key的值变化;

可以同时配置多个。如果不需要自动持久化,可以注释掉所有的save行来停用保存功能。

其他参数:

  • stop-writes-on-bgsave-error:默认值为yes。当启用了RDB且最后一次后台保存数据失败,Redis是否停止接收数据。这会让用户意识到数据没有正确持久化到磁盘上,否则没有人会注意到灾难(disaster)发生了。如果Redis重启了,那么又可以重新开始接收数据了。
  • rdbcompression:默认值是yes。对于存储到磁盘中的快照,可以设置是否进行压缩存储。
  • rdbchecksum:默认值是yes。在存储快照后,我们还可以让Redis使用CRC64算法来进行数据校验,但是这样做会增加大约10%的性能消耗,如果希望获取到最大的性能提升,可以关闭此功能。
  • dbfilename:设置快照的文件名,默认是dump.rdb
  • dir:设置快照文件的存放路径,这个配置项一定是个目录,而不能是文件名。

10.2.3 主从复制刚开始时触发

Redis的从节点与主节点进行初次同步(复制)时,主节点会生成一个RDB快照,并将该快照发送给从节点。这有助于加速从节点的初始化同步过程。

10.2.4 save和bgsave命令的区别

  1. save:命令会阻塞当前Redis服务器,因为它会在生成RDB文件时暂停处理其他命令请求,直到RDB过程完成为止。如果存在旧的RDB文件,一旦新的RDB文件生成完毕,Redis会用该文件覆盖旧的RDB文件,以实现数据持久化。
  2. bgsave:执行该命令时,Redis会在后台异步进行快照操作,快照同时还可以响应客户端请求。
    具体操作是Redis进程会执行fork操作创建子进程,RDB持久化过程由子进程负责,完成后自动结束。阻塞只发生在fork阶段,一般时间很短。基本上Redis内部所有的RDB操作都是采用bgsave命令。

10.2.5 RDB的优缺点

优点:

  1. 紧凑的文件格式RDB文件是一个紧凑的二进制文件,包含了整个Redis数据库的数据快照。这使得RDB文件适用于备份和迁移数据,以及在恢复时占用较小的存储空间。
  2. 恢复速度快:由于RDB文件是一个完整的数据快照,恢复速度通常比AOF日志文件要快。这对于在灾难恢复情况下尤为重要。
  3. 适合冷备份RDB适合用于生成冷备份,可以在需要备份时手动触发RDB生成。这有助于在数据库状态稳定的情况下进行备份,以避免备份过程中的数据变动。
  4. 性能较低的环境下更友好RDB生成过程可以在Redis子进程中进行,这有助于减轻主进程的负担,使其在高负载或性能较低的环境下更加稳定。

缺点:

  1. 数据丢失风险:由于RDB是基于时间或写入操作次数来触发的,如果在最近一次RDB生成和Redis宕机之间发生了数据写入,那么这部分数据将会丢失。
  2. 不适合持续更新RDB适用于周期性的数据快照,而不适合需要实时持续更新的应用。由于生成RDB文件会导致Redis主进程在一段时间内阻塞,这可能会影响应用程序的性能。
  3. RDB适应性差RDB文件仅包含数据快照,如果Redis在上次RDB生成和宕机之间崩溃,那么AOF日志文件将会是更可靠的数据恢复来源。
  4. 文件变大:随着时间的推移,RDB文件会变得相对较大,因为它包含了所有数据的快照。这会影响备份和恢复的速度。

10.3 AOF机制

AOF(Append-Only File)Redis的另一种持久化机制,因为RDB全量备份是比较耗时的,所以Redis提供一种更加高效的方式AOF,工作机制就是Redis会将每一个收到的写命令都通过write函数追加到日志文件中,以实现数据的持久化。AOF机制相对于RDB机制,更加持久,允许在Redis重启时通过重新执行写入操作来恢复数据。

10.3.1 文件重写原理(针对AOF文件比较大的情况,Redis做的优化)

AOF的方式也同时带来了另一个问题。持久化文件会变的越来越大。为了压缩AOF的持久化文件。Redis提供了bgrewriteaof命令。将内存中的数据以命令的方式保存到临时文件中,同时会fork出一条新进程来将文件重写。

具体优化:

  1. AOF重写Redis引入了AOF重写机制,它允许在不中断服务的情况下创建一个新的AOF文件,其中只包含从数据库重启后的写入操作。这可以通过运行一个后台进程来完成,该进程分析现有AOF文件,然后根据数据库的当前状态重写新的AOF文件。这样可以减小AOF文件的大小并且不会影响性能。
  2. AOF重写触发: 可以通过配置auto-aof-rewrite-percentageauto-aof-rewrite-min-size参数来触发AOF重写。当AOF文件的大小达到auto-aof-rewrite-min-size并且文件增长量超过了当前文件大小的auto-aof-rewrite-percentage时,Redis会自动触发AOF重写。
  3. AOF重写后合并: 在AOF重写过程中,如果发现有多条命令对同一个键执行了多次操作,那么只保留最终状态的写入操作,从而避免重复写入导致的数据冗余。
  4. 压缩指令: 在AOF文件中,Redis可以使用一些优化技术来减小指令的存储空间,从而降低AOF文件的大小。

10.3.2 AOF也有三种触发机制

  1. always(始终同步)Redis会将每次执行的操作命令追加到AOF文件中,从而确保每次操作都被持久化到磁盘。这是最高级别的数据保护,但也会产生显著的性能开销,因为每个操作都需要同步到磁盘
    在配置文件中配置方式:appendfsync always
  2. everysec(每秒同步)Redis会每秒执行一次,将已缓冲的操作命令同步追加到AOF文件。这种方式在性能和持久性之间取得了平衡,一般来说,数据丢失的风险较小。
    在配置文件中配置方式:appendfsync everysec
  3. no(异步)Redis将操作命令追加到AOF文件,但不会主动将数据同步到磁盘,而是由操作系统来处理同步。这提供了最高的性能,但也导致了数据丢失的风险,因为操作命令可能会在写入到磁盘之前存在于内存中。
    在配置文件中配置方式:appendfsync no
  4. Redis还支持自定义的appendfsync值,可以在配置文件中指定一个时间间隔,例如appendfsync 100,表示每隔100毫秒将缓冲的写入操作同步到磁盘。

选择适当的AOF触发机制取决于应用需求,需要根据性能和数据保护之间的权衡来进行选择。通常,everysec是一种常见的配置,因为它在大多数情况下可以提供良好的性能和可靠的持久性。 如果对性能要求非常高,并且可以容忍一些数据丢失风险,那么可以考虑使用no触发机制。

10.3.3 AOF的优缺点

优点:

  1. 高持久性AOF记录了每个写入操作,因此相对于RDB持久化,数据恢复的能力更强,数据丢失的风险更低。
  2. 数据重放AOF文件可以被重新执行,使得在灾难恢复和故障转移的情况下更容易恢复数据。
  3. 可读性AOF文件是一个文本文件,可以查看其内容,以了解Redis的历史写入操作。
  4. 部分持久性: 根据appendfsync的设置,可以在性能和持久性之间做出权衡。
  5. 灵活性AOF支持多种同步选项,可以根据应用需求调整性能和数据保护级别。

缺点:

  1. 文件大小: 随着时间的推移,AOF文件可能会变得很大,影响备份、迁移和恢复的速度。
  2. 写入延迟: 在everysecno模式下,数据可能会在一段时间内只存在于内存中,未持久化到磁盘,可能会导致数据丢失。
  3. 性能开销: 在较高的appendfsync设置下,AOF持久化会对性能产生一定影响。
  4. 文件碎片AOF文件可能会出现碎片问题,特别是在文件大小频繁增加和重写时,这可能会影响磁盘空间的利用率。
  5. 恢复速度: 在某些情况下,使用AOF恢复数据可能比使用RDB恢复数据更慢,因为AOF文件中的写入操作需要重新执行。

总结:
AOF持久化适用于需要高持久性和较小数据丢失风险的情况。如果更关注性能和较短的恢复时间,可以考虑在适当情况下使用RDB持久化。在实际应用中,可以根据业务需求和性能要求来选择适合的持久化机制或结合使用两种机制以达到平衡。

10.4 RDB和AOF的选择

选择的话,两者加一起才更好。因为两个持久化机制明白了,剩下的就是看自己的需求了,需求不同选择的也不一定,但是通常都是结合使用。有一张图可供总结:

RDB和AOF对比图:
Redis常见面试题及解答_第4张图片

11 Redis的主从复制

参考1:https://www.cnblogs.com/FatalFlower/p/15952843.html

11.1 主从复制介绍

Redis的主从复制允许将一个Redis服务器(主节点)的数据复制到多个其他Redis服务器(从节点)上,以实现数据的分发、备份和高可用性。主从复制在许多场景下都非常有用,例如高可用、提高读取性能、数据备份和灾难恢复。

Redis主从复制的一般工作原理和一些关键概念:

  1. 主节点(Master):主节点是数据源,它负责接收客户端的写入操作,并将这些操作应用于自身的数据集。主节点将自己的数据变更记录在AOF日志或RDB文件中,然后将这些变更通过网络发送给从节点。
  2. 从节点(Slave):从节点是主节点的复制品,它通过连接到主节点并接收主节点发送的写入操作变更,来保持自身的数据与主节点同步。从节点不能进行写入操作,只能处理读取请求,从而分担主节点的读取压力。
  3. 数据同步:从节点在初始连接时会执行全量复制(全量同步),获取主节点上的所有数据。之后,从节点会持续地从主节点获取增量变更(增量同步),以保持自身数据的一致性。
  4. 复制的类型
    • 全量复制(Full Resynchronization):在初始连接或数据丢失的情况下,从节点会进行全量复制,获取主节点上的所有数据。
    • 增量复制(Incremental Replication):在全量复制之后,从节点会持续接收主节点的写入操作,保持数据的同步。
  5. 自动重连:如果从节点与主节点的连接中断,从节点会自动尝试重新连接,并从最近的数据点开始增量复制,以保持数据的一致性。
  6. 读写分离:由于从节点能够处理读取请求,主从复制也可以用于实现读写分离架构,将读请求分发到从节点,从而减轻主节点的负载。
  7. 高可用性:如果主节点发生故障,可以将其中一个从节点提升为新的主节点,实现故障转移,以确保服务的可用性。

总结:
主从复制为Redis提供了高可用性、数据备份、负载均衡等优势。然而,需要注意的是,主从复制并不适用于所有场景,因为从节点仅能提供与主节点相同的数据,不能实现跨主节点的数据查询。

11.2 主从复制的目的及优点

主从复制(Master-Slave Replication)是Redis中一种重要的数据复制机制,其目的是将一个Redis服务器(主节点)的数据实时复制到其他多个Redis服务器(从节点),以实现数据的分发、备份、高可用性和负载均衡等目标。主从复制的优点如下:

  1. 高可用性:主从复制可以提高系统的可用性。当主节点出现故障时,可以将一个从节点升级为新的主节点,使系统能够继续提供服务,减少停机时间。
  2. 数据备份:从节点保存了主节点的数据的副本,因此可以在数据丢失、破坏或误删除时用作备份。这对于数据的恢复和业务连续性非常重要。
  3. 读写分离:从节点可以用于处理读取请求,从而分担主节点的负载。这有助于提高主节点的写入性能,并实现负载均衡。
  4. 数据分发:主从复制可以将数据在多个节点之间分发,这在分布式系统中非常有用。从而,各个节点可以提供就近的服务,减少网络延迟。
  5. 灾难恢复:如果主节点遭受硬件故障、数据损坏或其他灾难,从节点可以用作快速的灾难恢复手段。
  6. 升级和维护:当需要对主节点进行升级、维护或配置更改时,可以先将其降级为从节点,然后升级从节点。这样可以避免服务中断。
  7. 实时数据复制:主从复制可以实现实时的数据同步,从而保持从节点的数据与主节点的数据一致。
  8. 实验和分析:可以使用从节点来进行实验、分析和性能测试,而不影响主节点的生产环境。

虽然主从复制具有许多优点,但也需要注意它的一些限制。比如:从节点的数据与主节点的数据相同,不能进行跨主节点的数据查询。 此外,主节点出现故障时,进行故障转移可能需要一些手动操作,具体取决于配置和部署。 综合考虑业务需求和系统架构,决定是否采用主从复制以及如何配置和管理它。

11.3 主从复制的实现过程

Redis的主从复制分为以下几个阶段:

  1. 连接阶段:从节点首先与主节点建立连接。从节点发送一个SYNC命令给主节点,表示它希望进行全量同步。
  2. 全量同步阶段:在连接建立后,主节点开始执行全量同步过程。
  3. 增量同步阶段: 一旦全量同步完成,从节点进入增量同步阶段。在此阶段,主节点会持续地将写入操作发送给从节点,从节点接收并执行这些操作,以保持数据的实时一致性。
  4. 持续同步和保持连接: 从节点持续地向主节点发送心跳信号,以保持连接活跃。如果连接断开,从节点会尝试重新连接,然后继续进行增量同步。
  5. 故障转移阶段(可选): 如果主节点发生故障,从节点可以被提升为新的主节点,以实现故障转移和高可用性。

11.3.1 全量同步具体步骤

Redis主从复制中,全量同步(Full Resynchronization)是在从节点初始连接到主节点时所执行的过程,用于将主节点上的数据完整地传输到从节点,以确保从节点的数据与主节点的数据一致。以下是Redis主从复制中全量同步的具体步骤:

  1. 从节点连接到主节点: 当从节点启动或重新连接时,它会发送一个SYNC命令给主节点,表示它希望进行全量同步。
  2. 主节点接受SYNC命令: 主节点收到从节点的SYNC命令后,会准备进行全量同步。
  3. 主节点执行BGSAVE(可选): 主节点在开始全量同步之前,可以执行一个后台持久化操作,即执行BGSAVE命令生成RDB快照文件。这是为了确保在全量同步期间主节点的数据不会发生大的变化。
  4. 生成RDB文件: 如果主节点执行了BGSAVE,它会生成一个RDB快照文件,其中包含了当前主节点的数据。
  5. 发送RDB文件和AOF偏移量: 主节点将生成的RDB文件发送给从节点,并发送当前的AOF偏移量,表示数据变更在AOF日志中的位置。
  6. 从节点载入RDB文件: 从节点接收到RDB文件后,会载入该文件,将其中的数据加载到自己的内存中。
  7. 发送PSYNC命令: 从节点完成RDB文件的载入后,会向主节点发送PSYNC命令,携带上次复制的偏移量信息,以告知主节点从哪个位置开始进行增量同步。
  8. 主节点进行增量同步: 主节点接收到从节点的PSYNC命令后,根据携带的偏移量信息,开始将从该位置开始的写入操作发送给从节点。
  9. 从节点应用增量操作: 从节点接收并应用主节点发送的增量操作,以使从节点的数据与主节点的数据保持一致。
  10. 完成同步: 一旦从节点赶上主节点的数据,全量同步完成。从节点现在是主节点的复制品,并可以处理读取请求。

需要注意的是,全量同步是一个初始过程,仅在从节点刚连接到主节点时执行一次。之后,主节点会持续地将增量写入操作发送给从节点,以保持数据的同步。全量同步的过程确保了从节点的数据与主节点的数据一致性,并为之后的增量同步奠定了基础。

11.4 Redis如何保证主从的一致性

  1. 全量同步:在主从复制刚开始时,从节点会进行全量同步。主节点会将自己的整个数据集发送给从节点,确保从节点的数据与主节点一致。
  2. 增量同步:全量同步完成后,主节点会将写入操作记录在AOF日志中,并将这些操作发送给从节点。从节点会持续地接收并执行这些写入操作,以保持数据一致性。
  3. 命令复制:从节点接收到主节点的写入操作后,会将这些操作执行到自己的数据集中。这意味着从节点上的数据会按照主节点的操作顺序进行更新,从而保持一致性。
  4. 复制偏移量:主节点会记录每个写入操作的偏移量,表示该操作在AOF日志中的位置。从节点会定期向主节点发送自己的复制偏移量,以确保从正确的位置进行增量同步。
  5. 断点续传:如果从节点与主节点的连接中断,从节点会尝试重新连接并从断点位置继续同步。这避免了重复同步已经应用的数据。
  6. 异步复制:主从复制默认是异步的,主节点不会等待从节点应用写入操作。这可能导致从节点的数据略有滞后,但不影响一致性。

需要注意的是,尽管Redis通过上述机制来维护主从数据的一致性,但在某些情况下,由于网络延迟、故障等原因,从节点的数据可能会稍微滞后于主节点。因此,在主从复制中,不同节点之间的数据同步是一个近似一致性的概念,而不是强一致性。如果需要更严格的一致性,可以考虑使用Redis的哨兵机制或者集群模式。

11.5 Redis主从复制为什么不使用AOF?

主从复制中的AOF(Append-Only File)持久化和主节点的AOF持久化是两个不同的概念。在主从复制中,主节点的AOF持久化仍然可以被启用,但在从节点上通常会禁用AOF持久化。

为什么在从节点上不使用AOF持久化:

  1. 数据同步的方式:主从复制的主要目的是将主节点的数据同步到从节点,以实现数据的分发、备份和高可用性。从节点通过执行与主节点一样的写入操作来实现数据同步,因此不需要AOF文件来记录写入操作。
  2. 数据保护和持久化:从节点的数据是通过执行主节点的写入操作来同步的,而不是通过AOF文件。从节点并不直接处理客户端写入请求,因此不需要保留AOF文件来确保数据的持久性和恢复能力。
  3. 性能和延迟:从节点的主要任务是处理读取请求,它需要尽可能快速地响应这些请求。启用AOF会增加写入操作的开销,可能会影响从节点的读取性能,并且可能引入额外的写入延迟。
  4. 节省空间:从节点的AOF文件不会被用于数据恢复,因为它的数据是通过复制而来。因此,禁用从节点的AOF可以节省存储空间。
  5. 简化配置和管理:禁用从节点的AOF可以简化配置和管理,减少潜在的错误和混淆。

虽然在从节点上禁用AOF持久化是常见的做法,但这不是硬性规定。如果应用场景需要从节点也进行AOF持久化,仍然可以在从节点上启用AOF。然而,在主从复制的情况下,AOF文件在从节点上通常不会被使用,因为数据同步是通过主节点发送写入操作来实现的。

12 Redis哨兵机制

Redis哨兵(Sentinel)是用于监控和管理Redis主从复制和高可用性的系统。它可以自动检测主节点故障,进行故障转移,并重新配置从节点以提供高可用性的服务。

哨兵机制是Redis高可用性架构的一部分,允许在主节点发生故障时实现自动故障转移,以确保服务的连续性。

哨兵节点本质上也是一个Redis节点,但是和主节点和从节点不同,哨兵节点只是监视主节点和从节点,并不执行相关的业务操作。

12.1 哨兵机制的工作原理

  1. 故障检测:哨兵会定期向Redis的主节点和从节点发送心跳检测,以监测它们的状态。如果一个节点不再响应心跳,哨兵首先会将其标记为"主观下线"。
  2. 选举新主节点: 当主节点被标记为"主观下线"后,哨兵会通过SENTINEL is-masterdown-by-addr指令获取其它哨兵节点对于当前主节点的判断情况,如果哨兵节点对于当前主节点的主观下线判断数量超过了在配置文件中定义的票数,那么该主节点就被判定为“ODOWN”(客观下线)。然后与其他哨兵进行投票,以选择一个从节点升级为新的主节点。
  3. 自动故障转移: 一旦新主节点被选出,哨兵会执行自动故障转移过程。主要为以下步骤:
    • 哨兵会向其他从节点发送命令,让它们将复制目标从旧主节点切换到新主节点。
    • 哨兵会更新客户端的配置,将连接指向新的主节点。
    • 哨兵会更新所有相关节点的配置,以确保节点间的通信正确地指向新主节点。
  4. 数据同步: 一旦新主节点选举出来,从节点会向新主节点进行全量同步,以获取丢失的数据并保持一致性。
  5. 故障恢复: 当故障转移完成后,整个集群会恢复正常运行。新的主节点将负责处理客户端的写入请求,而从节点将继续处理读取请求。
  6. 监控和报警: 哨兵会持续监控集群的状态,如内存使用情况、连接数等,它还可以配置报警,在发生故障转移或其他问题时触发报警,以便在出现问题时通知管理员。
  7. 多哨兵支持: 在大规模的系统中,可以部署多个哨兵实例来提高监控和决策的可靠性。

由于使用单个的哨兵来监视Redis集群的节点不是完全可靠的,因为哨兵节点也有可能会出现故障,所以一般情况下会使用多个哨兵节点来监视整个Redis集群,如下图所示:
Redis常见面试题及解答_第5张图片
当存在多个哨兵节点时,在Redis哨兵机制中,对于节点的下线也有区分:

  1. 主观下线(Subjectively Down,即 SDOWN):指单个哨兵节点对集群中的节点作出主观下线判断。
  2. 客观下线(Objectively Down,即 ODOWN):指多个哨兵节点对集群中的节点作出主观下线判断,并且通过SENTINEL is-master-down-by-addr命令互相交流之后,作出Redis节点下线的判断。一个哨兵节点可以通过向另一个哨兵节点发送SENTINEL is-master-down-by-addr命令来询问对方是否认为给定的节点已经下线。

需要注意的是,哨兵机制的目标是实现集群的高可用性,确保在主节点故障时仍然能够提供服务。但哨兵机制并不是完美的,因此在使用过程中需要正确配置和管理,以确保其能够正常工作。另外,考虑使用Redis的集群模式也是实现高可用性的一种选择。

12.2 Redis哨兵机制中哨兵节点个数设置原则

  1. 奇数个节点:哨兵节点的数量通常选择奇数个,以确保在进行选举和决策时能够达成多数共识。奇数个节点能够更好地处理节点故障、拆分等问题,避免出现僵局情况。
  2. 至少三个节点:最少应该配置三个哨兵节点,以确保能够实现故障检测和自动故障转移。两个节点可能会导致选举过程中出现不确定性。
  3. 节点分布:哨兵节点应该分布在不同的物理机器或虚拟机上,以确保即使一部分节点出现故障,仍然能够保持足够的监控和故障检测能力。
  4. 偶数节点不推荐:避免使用偶数个哨兵节点,因为在故障情况下可能出现投票平局,导致选举无法达成多数共识。
  5. 多于三个节点:在生产环境中,通常建议配置多于三个哨兵节点,例如五个或七个。多个哨兵节点可以提高监控的可靠性,减少误判和误操作。
  6. 考虑资源和维护:哨兵节点也会消耗资源,包括内存和网络带宽。在设置节点数量时,考虑可用的资源和维护成本。

总结:
设置哨兵节点的数量需要权衡可靠性、资源消耗和管理复杂性。选择适当的奇数个哨兵节点,并确保它们分布在不同的位置,以实现高可用性、故障检测和自动故障转移的目标。

12.3 在从节点中选出新的主节点是随机的吗

不是,哨兵在进行主节点故障转移时,选出新的主节点是经过一定的投票和共识过程的,以确保选出的节点是合适的、可用的,并且能够保持数据一致性。

在投票过程中,哨兵会考虑以下因素:

  1. 健康状态:哨兵会选择一个被标记为"主观上线"(即正常状态)的从节点作为新的主节点。被标记为"主观下线"的节点不会参与选举。
  2. 复制偏移量:哨兵会考虑每个从节点的复制偏移量,即从节点在AOF日志中的位置。复制偏移量较大的从节点可能更适合被选为新主节点,以确保数据的一致性。
  3. 投票数:哨兵会向其他哨兵发送投票请求,然后会根据收到的投票数来决定是否选举某个从节点为新主节点。
  4. 优先级:在一些情况下,可以为从节点配置不同的优先级。优先级高的从节点可能更有可能被选为新的主节点。
  5. 其他因素:哨兵还可以考虑其他因素,如节点的配置、性能等。

13 缓存击穿、缓存雪崩、缓存穿透区别及解决方案

参考1:Redis的三大缓存异常原因分析和解决方案

13.1 缓存击穿

Redis的缓存击穿是一种性能问题,通常发生在具有高并发访问的系统中。它的产生原因是在缓存中存储了某个热点数据,但在某一时刻,大量并发请求同时访问该热点数据,而此时该热点数据的缓存数据刚好过期或被删除,导致每个请求都需要重新查询数据库或重新生成数据,从而导致数据库负载剧增,系统性能急剧下降。

  1. 产生原因:缓存击穿的情况,经常是发生在热点数据过期失效的情况。
  2. 解决方案
    • 对于访问很频繁的热点数据,就不需要设置过期时间了。这样对热点数据的访问可以直接在缓存中进行处理,Redis的数万级别的高吞吐量可以很好的应对大量的并发请求。
    • 使用布隆过滤器(Bloom Filter):布隆过滤器可以用来判断某个数据是否存在于缓存中,可以在查询缓存之前快速排除不存在于缓存中的数据,减少对数据库的请求。
    • 使用缓存预热(Cache Warming):在系统启动或数据更新之前,可以通过预先加载常用数据到缓存中,以减少冷启动时的缓存击穿风险。

13.2 缓存雪崩

Redis的缓存雪崩通常发生在大规模缓存中。它指在某一时刻,大量的缓存数据同时失效或被清除,导致大量请求同时落到数据库上,从而引起数据库负载急剧增加,甚至引发系统崩溃。

  1. 产生原因:产生原因一般有两种情况:

    • 缓存中有大量的数据同时过期,导致请求无法得到处理。当多个缓存键的失效时间(过期时间)设置得非常接近时,它们可能在同一时刻失效,导致大量请求同时落到数据库上。
    • Redis缓存实例发生故障,宕机了,导致大量请求积压到数据库。一般来说,一个Redis实例可以支持数万级别的请求处理吞吐量,而单个数据库可能只能支持数千级别的请求处理吞吐量,它们两个的处理能力可能相差了近十倍。由于缓存雪崩,Redis缓存失效,所以,数据库就可能要承受近十倍的请求压力,从而因为压力过大而崩溃。
  2. 解决方案
    针对缓存中有大量的数据同时过期的情况,可以提供两种解决方案。

    • 随机失效时间:给数据的到期时间增加一个较小的随机数,避免给大量的数据设置相同的过期的时间。
    • 使用缓存预热(Cache Warming):在系统启动或数据更新之前,可以通过预先加载常用数据到缓存中,以减少冷启动时的缓存击穿风险。
    • 服务降级:当发生缓存雪崩时,针对不同的数据采取不同的处理方式。
      非核心数据:暂时停止从缓存中查询这些数据,而是直接返回预定义信息、空值或者是错误信息。
      核心数据:缓存数据丢失时通过数据库读取。
      使用服务降级的方式,只有部分的数据请求会被发送到数据库,则数据库的压力就没有那么大了。
    • 监控和报警:实施监控系统,定期检查缓存的状态,当发现缓存出现异常或过期集中时,及时发出警报并采取措施,以防止缓存雪崩。

针对Redis缓存实例发生故障宕机的情况,同样也有两点建议。

  • 在业务系统中实现服务熔断或者请求限流机制:
    服务熔断:在发生缓存雪崩时,为了防止引发连锁的数据库雪崩,甚至是整个系统的崩溃,可以暂停业务应用对缓存系统的接口访问。
    请求限流:在请求入口前端只允许每秒进入系统的请求数为1000个,再多的请求就会在入口前端被直接拒绝服务。
  • 提前预防:通过主从节点的方式构建Redis缓存高可靠集群。如果Redis缓存的主节点故障宕机了,从节点还可以切换成为主节点,继续提供缓存服务,避免了由于缓存实例宕机而导致的缓存雪崩问题。

13.3 缓存穿透

缓存穿透指的是要访问的数据既不在Redis中,也不在数据库中,导致请求访问缓存缓缺失。这样一来应用无法从数据库中读取写入缓存,缓存成了摆设,同时给数据库和缓存都带来巨大的压力。

  1. 产生原因
    • 业务层误操作:缓存中的数据和数据库中的数据被误删除了,所以缓存中和数据库中都没有数据。
    • 恶意攻击:恶意用户或攻击者可能会故意发送查询不存在的数据的请求,以耗尽系统资源或尝试破坏应用程序。
  2. 解决方案
    • 缓存空值或者缺省值:一旦发生缓存穿透,可针对查询的数据在Redis缓存中设置一个短期的空值或者缺省值,当应用发送后续请求进行查询的时候就可以从Redis中读取到空值或者缺省值返回,避免大量请求数据库。
    • 使用布隆过滤器快速判断数据是否存在,避免从数据库中查询数据,减轻数据库压力:通过查询布隆过滤器快速判断数据是否存在,如果不存在就不需要再去数据库中查询了。
    • 在请求入口的前端进行请求检测:缓存穿透很大一部分原因是有大量的恶意请求访问不存在的数据,所以对业务系统接收到的请求进行合法性检测,把恶意的请求直接过滤掉,不让它们访问后端缓存和数据库。
    • 使用缓存预热(Cache Warming):在系统启动或数据更新之前,可以通过预先加载常用数据到缓存中,以减少冷启动时的缓存击穿风险。
    • 监控和报警:实施监控系统,定期检查缓存的状态,当发现缓存出现异常或过期集中时,及时发出警报并采取措施,以防止缓存穿透。

跟缓存雪崩、缓存击穿这两类问题相比,缓存穿透的影响更大一些。从预防的角度来说,我们需要避免误删除数据库和缓存中的数据;从应对角度来说,我们可以在业务系统中使用缓存空值或缺省值、使用布隆过滤器,以及进行恶意请求检测等方法。

13.4 布隆过滤器

布隆过滤器(Bloom Filter)是一种用于高效判断一个元素是否属于一个集合的数据结构,它可以快速检查一个元素是否可能在集合中,但具有一定的概率性和容错性。布隆过滤器在一些应用中能够有效地减少不必要的查询,节省计算资源。

优点:

  1. 空间效率高:布隆过滤器使用位数组,相对于存储实际元素,它的空间消耗很小。
  2. 查询效率高:布隆过滤器的查询操作非常快速,只需计算哈希并检查位值。
  3. 支持大规模数据集:布隆过滤器适用于处理大规模的数据集,因为它的空间和时间复杂度都与数据集大小无关。
  4. 保密性强:因为布隆过滤器不存储元素本身。

缺点:

  1. 存在误判:布隆过滤器可以产生误判,即它可能认为元素在集合中但实际上不在,但不会产生漏判(如果它认为元素不在集合中,那么一定不在)。
  2. 无法删除元素:布隆过滤器中一旦将位设置为1,就无法删除元素。如果需要删除元素,通常需要使用其他技巧,如定时重建布隆过滤器。
  3. 无法获取元素本身。

工作原理: 布隆过滤器的核心是一个二进制向量数组和一组哈希函数。

  1. 初始化:创建一个长度为m的位数组,所有位都初始化为0。
  2. 添加元素:将待添加的元素通过多个哈希函数映射为位数组中的多个位置,并将这些位置的对应位设置为1。
  3. 查询元素:对待查询的元素使用相同的哈希函数,检查对应位置的位值。如果所有位置的位值都为1,说明元素可能存在于集合中;如果有任何一个位置的位值为0,说明元素一定不存在于集合中。

使用场景:

  1. 解决Redis缓存穿透问题。
  2. 做邮件的黑名单过滤。
  3. 对爬虫网址做过滤,爬过的不在爬。
  4. 解决新闻推荐过的不再推荐。
  5. HBase/RocksDB/LevelDB等数据库内置布隆过滤器,用于判断数据是否存在,可以减少数据库的IO请求。

Redis集成布隆过滤器:
Redis可以集成布隆过滤器,版本推荐6.x,最低4.x版本,下载安装插件后在redis.config配置文件中加入redisbloom.so文件的地址并重启。(若是redis集群则每个配置文件中都需要加入redisbloom.so文件的地址)。

主要指令:

  1. bf.add:添加一个元素。
  2. bf.exists:判断一个元素是否存在。
  3. bf.madd:添加多个元素。
  4. bf.mexists:判断多个元素是否存在。

14 使用String和hash存储对象的对比

使用字符串(String):

  1. 简单数据:字符串适用于存储简单的键值对数据,例如配置信息、计数器等。
  2. 性能:字符串的读写操作非常高效,适用于对单个字段进行频繁的读写操作。
  3. 扩展性:当需要存储多个对象的不同字段时,可以使用多个字符串键,每个键存储一个字段,这种方式较为简单。
  4. 内存效率:对于小对象,字符串通常比散列更内存效率高,因为它不需要存储字段名。

使用哈希(Hash):

  1. 结构化数据:哈希适用于存储结构化的数据对象,其中包含多个字段,每个字段都有自己的名称和值。
  2. 高效存储:哈希可以存储多个字段,每个字段都可以单独进行读写操作,这使得它适用于需要频繁操作对象的不同部分的情况。
  3. 字段命名:哈希允许为每个字段指定一个名称,这有助于更好地组织和理解存储的数据。
  4. 查询和更新:如果只需要读取或更新散列中的一个或少数几个字段,而不是整个对象,哈希更为高效。
  5. 节省内存:对于大量具有相同字段的对象,使用哈希可以节省内存,因为字段名只存储一次。

15 Redis如何保证缓存的一致性

参考:Redis之缓存一致性

16 Redis怎么实现分布式锁

两种方式:

  • 加锁/释放锁;
  • Redlock算法;

16.1 加锁/释放锁

  1. 加锁:在需要获取锁的客户端向Redis发送一个SET命令,尝试在Redis中设置一个特定的键(锁键)并赋予一个唯一的值,通常是客户端的标识符(如UUID)。
SET lock_key unique_value NX PX 30000
  • lock_key:锁的名称,可以是任何唯一的标识符。
  • unique_value:是唯一的值,通常由客户端生成,用于标识这个锁的持有者。
  • NX:表示仅当lock_key不存在时才设置成功,即获取锁的操作是原子的。
  • PX 30000:表示设置锁的超时时间为30秒,以防止锁被永久占用。
  1. 释放锁: 在需要释放锁的客户端可以使用DEL命令来删除锁键,确保只有锁的持有者可以释放锁。
DEL lock_key

可以在释放锁时检查锁的持有者是否是当前客户端,以确保不会释放其他客户端持有的锁。

这种基本的分布式锁实现有一些限制和注意事项:

  • 获取锁的超时时间需要谨慎设置,过短可能导致竞争激烈,过长可能导致客户端长时间等待。
  • 如果锁的持有者执行时间较长,需要确保锁不会在执行期间自动过期,可以通过不断延长锁的超时时间来实现。
  • 释放锁时需要谨慎,确保只有锁的持有者可以释放锁,以避免误操作。

16.2 Redlock算法

Redlock是一种用于分布式系统中的分布式锁算法,它是由Redis的作者AntirezRedis官方文档中提出的一种思路。虽然Redlock提供了一种可行的分布式锁实现方法,但它仍然有一些局限性,需要谨慎使用。以下是使用Redlock算法在Redis中实现分布式锁的基本步骤:

  1. 选择锁服务器:在分布式环境中,选择多个独立的Redis节点,通常是奇数个(例如3、5、7个)。这些节点可以是Redis的主节点或者具有持久性和同步机制的Redis哨兵节点,作为锁服务器。
  2. 获取当前时间戳:所有客户端需要获取一个相同的当前时间戳,可以使用Unix时间戳或者其他合适的时间单位。
  3. 尝试获取锁:客户端使用相同的key和具有唯一性的value(例如UUID)获取锁,在每个Redis实例上请求获取锁,使用SET命令并设置锁的超时时间。
SET lock_key unique_value NX PX 30000
  • lock_key:锁的名称,可以是任何唯一的标识符。
  • unique_value:是唯一的值,通常由客户端生成,用于标识这个锁的持有者。
  • NX:表示仅当lock_key不存在时才设置成功,即获取锁的操作是原子的。
  • PX 30000:表示设置锁的超时时间为30秒,以防止锁被永久占用。
  1. 计算获取锁的时间:如果在大多数节点上成功获取锁,客户端可以计算获取锁的时间(即当前时间戳减去第二步中获取的时间戳)。key的真正有效时间等于有效时间减去获取锁所使用的时间。
  2. 判断锁是否有效:客户端需要判断获取锁的时间是否小于锁的有效时间(例如锁的超时时间的一半),当且仅当从大多数(N/2+1)的Redis节点都取到锁,并且使用的时间小于锁失效时间时,锁才算获取成功。
  3. 释放锁:当客户端完成任务后,可以使用DEL命令在所有节点上根据key请求ID删除锁。
  4. 处理锁失效:如果因为某些原因,获取锁失败(没有在至少N/2+1个Redis实例取到锁或者取锁时间已经超过了有效时间),客户端应该在所有的Redis实例上进行解锁(即便某些Redis实例根本就没有加锁成功,防止某些节点获取到锁但是客户端没有得到响应而导致接下来的一段时间不能被重新获取锁)。

需要注意的是,Redlock算法并不是绝对可靠的,它对网络分区等异常情况可能不够健壮。在使用Redlock时,建议谨慎考虑以下因素:

  1. 在网络分区(网络隔离)发生时,可能导致部分节点无法通信,进而可能导致锁的失效。
  2. Redlock的性能相对较低,因为它需要在多个节点上进行多次尝试。

16.3 分布式锁和单机锁得区别

参考1:分布式锁:概述篇

单机锁:解决的是进程内线程间的问题;
分布式锁:则是解决的进程间的一个问题;

17 Redis给一个键值对设置过期时间

参考1:Redis Setex 命令

Redissetex命令为指定的key设置值及其过期时间。如果key已经存在, setex命令将会替换旧的值。

SETNX key value只有在key不存在时设置key的值。

示例:

redis 127.0.0.1:6379> SETEX KEY_NAME TIMEOUT VALUE

18 Redis的事务

Redis支持事务,事务允许将多个命令打包成一个单一的执行单元,要么全部执行成功,要么全部执行失败,保持了原子性。Redis使用MULTIEXECDISCARDWATCH命令来实现事务。以下是关于Redis事务的一些重要信息:

  1. 开启事务:通过MULTI命令来开启一个事务。一旦开启事务,后续的命令都会被放入事务队列中,但不会立即执行。
MULTI
  1. 添加命令到事务队列:在事务开启后,所有后续的命令都会被添加到事务队列中,但不会立即执行。
SET key1 value1
SET key2 value2
  1. 执行事务:通过EXEC命令来执行事务中的所有命令。一旦执行EXECRedis会按照事务队列中的顺序执行所有命令。
EXEC

如果事务中的任何命令出现了错误,Redis将执行失败,并返回一个包含错误信息的数组。否则,事务将被成功执行,返回每个命令的结果。
4. 取消事务:如果在执行事务之前需要取消事务,可以使用DISCARD命令。DISCARD命令会清空事务队列中的所有命令。

DISCARD
  1. 事务中的错误处理:如果事务中的某个命令执行失败,不会影响其他命令的执行,Redis会继续执行后续的命令。因此,需要在客户端代码中检查每个命令的执行结果,以判断是否有错误。
  2. 事务的原子性Redis事务是原子性的,意味着在事务执行期间,其他客户端无法插入命令,也无法读取事务中的未执行命令。
  3. WATCH命令Redis还提供了WATCH命令,它用于监视一个或多个键。如果在事务执行前被监视的键发生了变化,事务将被取消。这可以用于实现乐观锁的机制。

Redis事务非常适合一次性执行多个命令,从而确保这些命令的原子性。然而,需要注意的是,Redis事务不同于传统数据库的事务,它不提供隔离性,不支持回滚,且在一些情况下可能表现出预期之外的行为。因此,在使用Redis事务时,需要充分了解其特性和限制,并根据具体需求进行设计和使用。

19 Redis集群方案

参考1:Redis:集群方案

常见的Redis集群方案:

  1. 主从复制;
  2. 哨兵模式;
  3. 官网的Redis Cluster

20 Redis删除数据的方式

20.1 Redis的key过期之后是立即删除吗?是什么机制?

不是,Redis中的键过期后并不会立即被立刻删除。过期键的删除是通过Redis的定期任务来进行的,具体的删除时间取决于Redis的策略和配置。

Redis使用两种主要策略来管理过期键的删除:

  1. 定期删除Redis会定期检查一定数量的键,看它们是否过期,然后删除过期的键。这个检查过程不是立即发生,而是由Redis配置中的hz参数控制的,表示每秒执行检查的次数。默认情况下,hz设置为10,即每秒检查10次。
  2. 惰性删除:当客户端尝试访问一个键时,Redis会首先检查该键是否过期。如果键过期了,Redis会删除它,然后返回一个不存在的值。这意味着过期键只有在访问时才会被删除。

总的来说,Redis的过期键并不是立即删除的,而是在一定的时间内(取决于hz参数和键的访问频率)被定期或惰性删除。这种策略可以有效减少删除操作的开销,同时保证了Redis的高性能。

需要注意的是,过期键的删除是基于LRU(最近最少使用)算法实现的,因此在某些情况下可能会有一些不确定性,例如可能出现一些已过期但尚未删除的键。但总体来说,Redis会尽力确保过期键的及时删除。如果需要精确控制键的生命周期,可以使用EXPIREPEXPIRE命令来设置键的过期时间。

20.2 delete是阻塞还是非阻塞,或者Redis有没有非阻塞删除的方式

Redis的删除操作通常是非阻塞的,当执行删除命令时,Redis会立即返回,并在后台异步地执行删除操作,而不会阻塞其他操作的执行。

这意味着即使执行了删除操作,Redis仍然可以同时响应其他读取和写入请求,不会因为删除操作而停止响应其他命令。这种非阻塞的特性有助于保持Redis的高吞吐量和低延迟特性。

需要注意的是,虽然Redis的删除操作是非阻塞的,但在删除大量数据时,删除操作可能会占用一定的CPUI/O资源,从而对其他操作产生一定的影响。因此,在大规模删除操作时,需要谨慎考虑对系统性能的影响,可以采用分批次删除或者在低负载时段进行删除操作,以减轻影响。

此外,如果需要在删除某个键后立即获取该键的值,可以使用DEL命令删除键,并立即访问该键,Redis会在删除后返回"nil"值。这可以用于原子地删除并获取键的值,但请注意,如果键不存在或已经过期,它也会返回"nil"值。

21 Redis限流的几种方式

  1. 基于Redis的setnx的操作
    比如我们需要在10秒内限定20个请求,那么我们在setnx的时候可以设置过期时间10,当请求的setnx数量达到20时候即达到了限流效果。代码比较简单就不做展示了。

这种做法的弊端有很多弊端,比如当统计1-10秒的时候,无法统计2-11秒之内,如果需要统计N秒内的M个请求,那么我们的Redis中需要保持N个key等等问题。

  1. 基于Redis的数据结构zset
    其实限流涉及的最主要的就是滑动窗口,上面也提到1-10怎么变成2-11。其实也就是起始值和末端值都各+1即可。

可以将请求构造成一个zset数组,当每一次请求进来的时候,value保持唯一,可以用UUID或者时间戳,而score可以用当前时间戳表示,因为score我们可以用来计算当前时间戳之内有多少的请求数量。而zset数据结构也提供了zrange方法让我们可以很轻易的获取到2个时间戳内有多少请求。

通过上述代码可以做到滑动窗口的效果,并且能保证每N秒内至多M个请求,缺点就是zset的数据结构会越来越大。实现方式相对也是比较简单的。

  1. 基于Redis的令牌桶算法
    提令牌桶算法涉及到输入速率和输出速率,当输出速率大于输入速率,那么就是超出流量限制了。也就是说我们每访问一次请求的时候,可以从Redis中获取一个令牌,如果拿到令牌了,那就说明没超出限制,而如果拿不到,则结果相反。

    根据这个思想,可以结合Redislist数据结构很轻易的做到这样的代码,只是简单实现依靠listleftPop来获取令牌。

22 Redis的stream是否用过

Redis StreamsRedis 5.0引入的新数据结构,用于处理有序流式数据。它们提供了类似于消息队列的功能,但有更多的功能,允许轻松地添加、读取和处理事件流。Streams主要用于日志处理、消息传递和事件驱动的应用程序中。

以下是有关Redis Streams的关键概念和操作:

  1. StreamStream是一个有序的、不断增长的事件序列,它包含了一个或多个条目(entry)。每个条目都有一个唯一的ID来标识,可以是整数或时间戳。Stream的特点是可以保留和查询历史数据。
  2. EntryStream中的每个事件都称为一个entry,它包含了一个关键字(field)和一个值(value)。通常,值是一个包含了事件数据的JSON对象。
  3. 消费者(Consumer):消费者是指应用程序或客户端,它可以订阅一个或多个Stream,以便接收事件。Redis允许多个消费者订阅相同的Stream,并可以独立读取事件。
  4. 消费者组(Consumer Group):消费者组是一组消费者的集合,用于协作处理事件。它可以确保在多个消费者之间平衡事件的处理负载。
  5. XADDXADD命令用于向Stream添加新的条目,它接收一个Stream名称、一个条目ID和一个包含字段值对的数据。条目ID可以是自动生成的或者用户指定的。
  6. XREADXREAD命令用于从Stream读取事件。可以根据条件读取事件,例如,读取未读取的事件、读取指定数量的事件等。
  7. XGROUPXGROUP命令用于创建和管理消费者组,以及将消费者加入组中。
  8. XACK 和 XDELXACK用于确认消费者已经成功处理了一个或多个事件,而XDEL用于删除事件。
  9. XPENDINGXPENDING用于查询未处理的事件以及与事件相关的信息,如消费者信息、未处理事件的数量等。

Redis Streams是一个强大的数据结构,可用于实现各种实时数据处理任务。它允许应用程序通过事件流进行通信,并且可以在流中保留历史数据以进行后续分析和查询。Streams还支持多个消费者之间的负载均衡,以确保事件能够高效地处理。

23 Redis架构如何设计及优化

设计和优化Redis架构通常依赖于应用程序的需求和负载情况。Redis是一个高性能的键值存储系统,可以用于多种用途,包括缓存、会话存储、计数器等。以下是设计和优化Redis架构的一些常见考虑因素和策略:

  1. 数据模型
    • 确定数据的存储结构,例如使用字符串、列表、集合、哈希或有序集合等数据类型。
    • 考虑如何组织和命名键,以便于查询和管理。
  2. 数据分区
    • 对于大规模负载,考虑使用Redis的分片机制将数据分布到多个Redis实例中,以减轻单个实例的负载。
    • 使用一致性哈希算法来确定数据应存储在哪个分片上。
  3. 数据持久化
    • 根据可接受的数据丢失程度,选择适当的持久化方式,如快照(RDB)和日志(AOF)。
    • 配置持久化的频率,以平衡性能和数据安全性。
  4. 内存优化
    • 监控Redis的内存使用情况,确保不会超出可用内存。
    • 使用Redis内置的LRU算法或手动删除过期数据来管理内存。
  5. 集群和高可用性
    • 对于关键应用,使用Redis高可用性解决方案,如主从复制、哨兵或Redis集群。
    • 考虑数据备份和恢复策略,以应对故障。
  6. 访问控制和安全性
    • 实现适当的访问控制,限制对Redis实例的访问。
    • 使用密码认证或者更高级的安全机制来保护数据。
  7. 性能优化
    • 针对特定的使用情况和负载特性,优化Redis的配置参数,如最大连接数、超时设置、并发客户端数等。
    • 使用Redis的事务和管道功能,减少客户端与服务器之间的通信开销。
  8. 缓存策略
    • 为缓存数据定义合适的过期时间,以确保数据不会无限期占用内存。
    • 使用LRULFU算法来淘汰不常用的缓存数据。
  9. 监控和性能分析
    • 使用监控工具来实时监视Redis的性能和健康状态。
    • 使用性能分析工具来识别性能瓶颈和优化机会。
  10. 版本升级
    • 定期升级Redis版本,以获取性能改进、安全性修复和新特性。在升级之前,确保进行充分的测试,以防止应用程序出现不兼容问题。
  11. 客户端优化
    • 优化客户端代码,避免不必要的Redis查询和连接开销。
    • 使用连接池和重用连接,以减少连接的创建和销毁开销。
  12. 分布式缓存
    • 如果在应用程序中使用Redis作为分布式缓存,请考虑如何处理缓存失效、热点数据和缓存预热等问题。
  13. 容量规划
    • 根据预期的数据量和负载,规划足够的硬件资源,包括内存、CPU和存储。

总结:
Redis的设计和优化应该是根据具体的应用需求和负载情况来进行的。不同的应用可能需要不同的策略和配置。定期监控和性能调优是保持Redis高性能和可用性的关键。同时,随着Redis和应用的发展,也需要不断地进行容量规划和架构调整。

你可能感兴趣的:(面试,redis)