1. lists、sets、hashes、sorted sets 类型,元素、键值对最多2^32-1个

2. 一个字符串类型的值能存储最大容量是512M

3. Redis如何做内存优化

  • 尽可能使用散列表(hashes),散列表(是说散列表里面存储的数少)使用的内存非常小,所以应该尽可能数据模型抽象到一个散列表里面。web系统中有一个用户对象,不要为这个用户的名称,姓氏,邮箱,设置单独的key,而是应该把这个用户的所有信息存储到一张散列表里面。

    4. 可重入性

  • 可重入性是指线程在持有锁的情况下再次请求加锁,如果一个锁支持同一个线程的多次加锁,那么这个锁就是可重入的。比如 Java 语言里有个 ReentrantLock 就是可重入锁。Redis 分布式锁如果要支持可重入,需要对客户端的 set 方法进行包装,使用线程的 Threadlocal 变量存储当前持有锁的计数。

    5. 客户端在处理请求时加锁没加成功

  • 直接抛出异常,通知用户稍后重试;

  • sleep 一会再重试;
  • 将请求转移至延时队列,过一会再试;

    6. Redis 作为消息队列为什么不能保证 100% 的可靠性

  • 是因为redis的发消息,没有持久化。如果消息在发送过程中丢失,那客户端就永远也收不到消息了,redis也没有针对消息丢失做出一些补偿。

  • pop出消息后,list 中就没这个消息了,如果处理消息的程序拿到消息还未处理就挂掉了,那消息就丢失

    7. 单线程的 Redis 为什么这么快?

  • 纯内存操作

  • 单线程操作(它的请求处理整个流程是单线程处理的),避免了频繁的上下文切换,不用考虑锁相关问题消耗。
  • 采用了非阻塞 I/O 多路复用机制
    • Redis可以在单线程中监听多个Socket的请求,在任意一个Socket可读/可写时,Redis去读取客户端请求,在内存中操作对应的数据,然后再写回到Socket中。
  • redis利用队列技术将并发访问变为串行访问,消除了传统数据库串行控制的开销
  • 使用底层模型不同,底层实现方式以及与客户端之间通信的应用协议不一样,Redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求

    8. 非阻塞 I/O 多路复用机制?

  • “多路”指的是多个网络连接,“复用”指的是复用同一个线程。

  • 多路I/O复用模型是利用 select、poll、epoll 可以同时监察多个流的 I/O 事件的能力,在空闲的时候,会把当前线程阻塞掉,当有一个或多个流有 I/O 事件时,就从阻塞态中唤醒,于是程序就会轮询一遍所有的流(epoll 是只轮询那些真正发出了事件的流),并且只依次顺序的处理就绪的流,这种做法就避免了大量的无用操作。
  • redis-client 在操作的时候,会产生具有不同事件类型的 Socket。
  • 保证在监听多个Socket连接的情况下,只针对有活动的Socket采取反应。
  • 在服务端,有一段 I/O 多路复用程序,将其置入队列之中。然后,文件事件分派器,依次去队列中取,转发到不同的事件处理器中。
  • 这个 I/O 多路复用机制,Redis 还提供了 select、epoll、evport、kqueue 等多路复用函数库 (老师检查作业的例子)
  • 在 I/O 多路复用模型中,最重要的函数调用就是 select,该方法的能够同时监控多个文件描述符的可读可写情况,当其中的某些文件描述符可读或者可写时,select 方法就会返回可读以及可写的文件描述符个数。

8.1 为什么 Redis 中要使用 I/O 多路复用这种技术

  • Redis 是跑在单线程中的,所有的操作都是按照顺序线性执行的,但是由于读写操作等待用户输入或输出都是阻塞的,所以 I/O 操作在一般情况下往往不能直接返回,这会导致某一文件的 I/O 阻塞导致整个进程无法对其它客户提供服务,而 I/O 多路复用就是为了解决这个问题而出现的。

    9. 配置内存淘汰策略的

    • 最近最少使用LRU
    • 最不经常使用算法LFU

      10. Redis 和数据库双写一致性问题

    • 首先,采取正确更新策略,先更新数据库,再删缓存。其次,因为可能存在删除缓存失败的问题,提供一个补偿措施即可,例如利用消息队列。

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

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

      12. 缓存穿透解决方案

    • 缘由:数据库和缓存都不存在的数据,查询的时候请求就会打到数据库去

  1. 缓存空值
    1. 如果请求的数据量不大的情况下:为这些key对应的值设置为null 丢到缓存里面去。后面再出现查询这个key 的请求的时候,直接返回null
  2. 请求数据比较大的情况下:提供一个能迅速判断请求是否有效的拦截机制,比如,利用布隆过滤器,内部维护一系列合法有效的 Key。迅速判断出,请求所携带的 Key 是否合法有效。如果不合法,则直接返回。

13. 缓存击穿

  • 缘由:大量的请求同时查询一个 key 时,此时这个key正好失效了,就会导致大量的请求都打到数据库上面去。
  • 解决办法:可以在第一个查询数据的请求上使用一个 互斥锁来锁住它。其他的线程走到这一步拿不到锁就等着,等第一个线程查询到了数据,然后做缓存。后面的线程进来发现已经有缓存了,就直接走缓存。
  • 设置热点数据永远不过期。或者加上互斥锁就能搞定了

    13. 缓存雪崩

  • 缘由:即缓存同一时间大面积的失效,这个时候又来了一波请求,结果请求都怼到数据库上,从而导致数据库连接异常。

  • 使用 Redis 高可用架构:使用 Redis 集群来保证 Redis 服务不会挂掉
  • 缓存时间不一致,给缓存的失效时间,加上一个随机值,避免集体失效
  • 缓存失效或,通过加锁或队列来控制读取数据库的访问的线程数量,比如对某个key值运行一个线程访问数据库,其他线程等待
  • 不同的key,设置不同的过期时间,让环创失效的时间点尽量均匀
  • 做二级缓存,a1失效时候,访问a2,a1失效的时间设置为短期,a2为长期

14. 如何解决 Redis 的并发竞争 Key 问题

  1. 如果对这个 Key 操作,不要求顺序
    • 准备一个分布式锁,大家去抢锁,抢到锁就做 set 操作即可,比较简单
  2. 如果对这个 Key 操作,要求顺序
    • 假设有一个 key1,系统 A 需要将 key1 设置为 valueA,系统 B 需要将 key1 设置为 valueB,系统 C 需要将 key1 设置为 valueC。
    • 期望按照 key1 的 value 值按照 valueA > valueB > valueC 的顺序变化。这种时候我们在数据写入数据库的时候,需要保存一个时间戳。
    • 假设时间戳如下:
    • 系统A key 1 {valueA 3:00}
    • 系统B key 1 {valueB 3:05}
    • 系统C key 1 {valueC 3:103}
    • 假设这会系统 B 先抢到锁,将 key1 设置为{valueB 3:05}。接下来系统 A 抢到锁,发现自己的 valueA 的时间戳早于缓存中的时间戳,那就不做 set 操作了,以此类推。
    • 比如利用队列,将 set 方法变成串行访问也可以

      15. Redis的同步机制

  • Redis可以使用主从同步,从从同步。第一次同步时,主节点做一次bgsave,并同时将后续修改操作记录到内存buffer,待完成后将rdb文件全量同步到复制节点,复制节点接受完成后将rdb镜像加载到内存。加载完成后,再通知主节点将期间修改的操作记录同步到复制节点进行重放就完成了同步过程。

    16. 集群的原理是什么

  • Redis Sentinal着眼于高可用,在master宕机时会自动将slave提升为master,继续提供服务。

  • Redis Cluster着眼于扩展性,在单个redis内存不足时,使用Cluster进行分片存储。

    17. 过期键删除策略

  • 定时删除:在设置键的过期时间的同时,创建一个定时器,让定时器在到达过期时间时,立即执行删除操作(CPU不友好、内存友好)

  • 惰性删除:每次获取键时,才判断是否过期,如果过期就删除;没有过期则返回(CPU友好、内存不友好)。
  • 定期删除:每隔一段时间,就对 Redis 进行一遍检查,删除里面过期的键(折中)。
  • Redis 默认使用惰性删除与定期删除相结合,配合使用。

    18. 安全性与生命期保证

  • 保证高效使用分布式锁至少需要三个属性

    • 安全属性:互斥。在任意给定的时间,只有一个客户端可以持有锁。
    • 生命期属性A:无死锁的。即使锁定了某个资源的客户端发生崩溃或者出现网络分区,其他客户端最终也可以获得一把锁。
    • 生命期属性B:容错的。只要Redis节点中的多数派是可用的,客户端就可以获得/释放锁。

      19. Redis做异步队列、redis如何实现延时队列

  • 一般使用list结构作为队列,rpush生产消息,lpop消费消息。当lpop没有消息的时候,要适当sleep一会再重试。
  • 使用sortedset,拿时间戳作为score,消息内容作为key调用zadd来生产消息,消费者用zrangebyscore指令获取N秒之前的数据轮询进行处理。

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

    • Redis 提供两种持久化机制 RDB(默认) 和 AOF 机制:
    • RDB是Redis默认的持久化方式。按照一定的时间将内存的数据以快照的形式保存到硬盘中,对应产生的数据文件为dump.rdb。通过配置文件中的save参数来定义快照的周期。
    • 优点
      • 只有一个文件 dump.rdb,方便持久化。
      • 容灾性好,一个文件可以保存到安全的磁盘。
      • 性能最大化,fork 子进程来完成写操作,让主进程继续处理命令,所以是 IO 最大化。使用单独子进程来进行持久化,主进程不会进行任何 IO 操作,保证了 redis 的高性能
      • 相对于数据集大时,比 AOF 的启动效率更高。
    • 缺点
      • 数据安全性低。RDB 是间隔一段时间进行持久化,如果持久化之间 redis 发生故障,会发生数据丢失。所以这种方式更适合数据要求不严谨的时候)
    • AOF:持久化

      • AOF(Append-only file)持久化方式:是指所有的命令行记录以 redis 命令请 求协议的格式完全持久化存储)保存为 aof 文件。
      • AOF持久化(即Append Only File持久化),则是将Redis执行的每次写命令记录到单独的日志文件中,当重启Redis会重新将持久化的日志中文件恢复数据。
      • 当两种方式同时开启时,数据恢复Redis会优先选择AOF恢复。
      • 优点
        • 数据安全,aof 持久化可以配置 appendfsync 属性,有 always,每进行一次 命令操作就记录到 aof 文件中一次。
        • 通过 append 模式写文件,即使中途服务器宕机,可以通过 redis-check-aof 工具解决数据一致性问题。
        • AOF 机制的 rewrite 模式。AOF 文件没被 rewrite 之前(文件过大时会对命令 进行合并重写),可以删除其中的某些命令(比如误操作的 flushall))
        • Redis4.0之后有了混合持久化的功能,将bgsave的全量和aof的增量做了融合处理,这样既保证了恢复的效率又兼顾了数据的安全性。
      • 缺点
        • AOF 文件比 RDB 文件大,且恢复速度慢。
        • 数据集大的时候,比 rdb 启动效率低。

          22. integers列表键底层实现

    • integers列表键的底层实现就是一个链表,链表中的每个节点都保存了一个整数值。

      23. 使用Redis和直接使用内存有本质区别么?

    • 直接使用内存,就是所谓的local cache,性能比redis这种remote cache性能要高,因为没有网络开销,但是存的数据量很有限,一般都是结合redis一起使用

      24. Redis主从复制

    • 作用:主从备份,防止节点宕机。使得读写分离,达到负载均衡的目的。

    • 原理:Redis的主从结构可以采用一主多从或者级联结构,Redis主从复制可以根据是否是全量分为全量同步和增量同步。
    • Redis全量复制一般发生在Slave初始化阶段,这时Slave需要将Master上的所有数据都复制一份。具体步骤如下
      • 从服务器连接主服务器,发送SYNC命令;
      • 主服务器接收到SYNC命名后,开始执行BGSAVE命令生成RDB文件并使用缓冲区记录此后执行的所有写命令;
      • 主服务器BGSAVE执行完后,向所有从服务器发送快照文件,并在发送期间继续记录被执行的写命令;
      • 从服务器收到快照文件后丢弃所有旧数据,载入收到的快照;
      • 主服务器快照发送完毕后开始向从服务器发送缓冲区中的写命令;
      • 从服务器完成对快照的载入,开始接收命令请求,并执行来自主服务器缓冲区的写命令;
    • Redis增量复制:指Slave初始化后开始正常工作时主服务器发生的写操作同步到从服务器的过程。 增量复制的过程主要是主服务器每执行一个写命令就会向从服务器发送相同的写命令,从服务器接收并执行收到的写命令。
    • Redis主从同步策略:主从刚刚连接的时候,进行全量同步;全量同步结束后,进行增量同步。当然,如果有需要,slave 在任何时候都可以发起全量同步。redis 策略是,无论如何,首先会尝试进行增量同步,如不成功,要求从机进行全量同步。
    • 注意点:如果多个Slave断线了,需要重启的时候,因为只要Slave启动,就会发送sync请求和主机全量同步,当多个同时出现的时候,可能会导致Master IO剧增宕机。

      26. Redis做异步队列,怎么用的?有什么缺点?

    • 一般使用list结构作为队列,rpush生产消息,lpop消费消息。当lpop没有消息的时候,要适当sleep一会再重试。

    • 缺点:
    • 在消费者下线的情况下,生产的消息会丢失,得使用专业的消息队列如rabbitmq等。 能不能生产一次消费多次呢? 使用pub/sub主题订阅者模式,可以实现1:N的消息队列。

      27. 为什么 Redis 先执行指令再记录aof日志而不是像其它存储引擎一样反过来呢?

    • 为了性能;先写日志再写执行 等于先进行磁盘IO然后在执行命令,太慢了; 先写日志然后执行的一般是支持事务的

      28. Redis 支持多线程主要就是两个原因

    • 可以充分利用服务器 CPU 资源,目前主线程只能利用一个核。

    • 多线程任务可以分摊 Redis 同步 IO 读写负荷。

      29. Redis集群方案什么情况下会导致整个集群不可用?

    • 有A,B,C三个节点的集群,在没有复制模型的情况下,如果节点B失败了,那么整个集群就会以为缺少5501-11000这个范围的槽而不可用。

      30. 说说Redis哈希槽的概念?

    • Redis集群没有使用一致性hash,而是引入了哈希槽的概念,Redis集群有16384个哈希槽,每个key通过CRC16校验后对16384取模来决定放置哪个槽,集群的每个节点负责一部分hash槽。

      31. Redis集群的主从复制模型是怎样的?

    • 为了使在部分节点失败或者大部分节点无法通信的情况下集群仍然可用,所以集群使用了主从复制模型,每个节点都会有N-1个复制品.

      35. Redis有序集合zset底层怎么实现的

    • Redis中的set数据结构底层用的是跳表实现的.跳表是一个随机化的数据结构,实质就是一种可以进行二分查找的有序链表。跳表在原有的有序链表上面增加了多级索引,通过索引来实现快速查找。跳表不仅能提高搜索性能,同时也可以提高插入和删除操作的性能。

      • 跳表是可以实现二分查找的有序链表;
      • 每个元素插入时随机生成它的level;
      • 最低层包含所有的元素;
      • 如果一个元素出现在level(x),那么它肯定出现在x以下的level中
      • 每个索引节点包含两个指针,一个向下,一个向右
      • 跳表查询、插入、删除的时间复杂度为O(log n),与平衡二叉树接近;

        36. 为什么Redis选择使用跳表而不是红黑树来实现有序集合?(O(logN))

    • Redis的有序集合支持的操作:

      • 插入元素
      • 删除元素
      • 查找元素
      • 有序输出所有元素
      • 查找区间内所有元素
      • 前4项红黑树都可以完成,且时间复杂度与跳表一致。但是,最后一项,红黑树的效率就没有跳表高了。 在跳表中,要查找区间的元素,我们只要定位到两个区间端点在最低层级的位置,然后按顺序遍历元素就可以了,非常高效。而红黑树只能定位到端点后,再从首位置开始每次都要查找后继节点,相对来说是比较耗时的。 此外,跳表实现起来很容易且易读,红黑树实现起来相对困难,所以Redis选择使用跳表来实现有序集合。

        37. 跳表的查询过程是怎么样的,查询和插入的时间复杂度

  • 先从第一层查找,不满足就下沉到第二层找,因为每一层都是有序的,写入和插入的时间复杂度都是O(logN)

    38. Redis多线程的实现机制

image.png
简述流程

  • 主线程负责接收建立连接请求,获取 Socket 放入全局等待读处理队列。
  • 主线程处理完读事件之后,通过 RR(Round Robin)将这些连接分配给这些 IO 线程。
  • 主线程阻塞等待 IO 线程读取 Socket 完毕。
  • 主线程通过单线程的方式执行请求命令,请求数据读取并解析完成,但并不执行。
  • 主线程阻塞等待 IO 线程将数据回写 Socket 完毕。
  • 解除绑定,清空等待队列。

image.png

  • 该设计有如下特点:
    • IO 线程要么同时在读 Socket,要么同时在写,不会同时读或写。
    • IO 线程只负责读写 Socket 解析命令,不负责命令处理。

39. 开启多线程后,是否会存在线程并发安全问题?

  • Redis 多线程部分只是用来处理网络数据的读写和协议解析,执行命令仍然是单线程顺序执行。

    40. 多线程优化

  • Redis Server本身是多线程的,除了请求处理流程是单线程处理之外,Redis内部还有其他工作线程在后台执行,它负责异步执行某些比较耗时的任务,例如AOF每秒刷盘、AOF文件重写都是在另一个线程中完成的。

  • 而在Redis 4.0之后,Redis引入了lazyfree的机制,提供了unlinkflushall ayscflushdb async等命令和lazyfree-lazy-evictionlazyfree-lazy-expire等机制来异步释放内存,它主要是为了解决在释放大内存数据导致整个redis阻塞的性能问题。
  • 在删除大key时,释放内存往往都比较耗时,所以Redis提供异步释放内存的方式,让这些耗时的操作放到另一个线程中异步去处理,从而不影响主线程的执行,提高性能。
  • 到了Redis 6.0,Redis又引入了多线程来完成请求数据的协议解析,进一步提升性能。它主要是解决高并发场景下,单线程解析请求数据协议带来的压力。请求数据的协议解析由多线程完成之后,后面的请求处理阶段依旧还是单线程排队处理。

    43 Redis如何做大量数据插入?

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

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

  • 先写数据库,再写缓存,数据库写成功,缓存写失败,缓存使用时,假如读缓存失败,先读数据库,再回写缓存的方式实现

    45 缓存热点key

  • 对缓存查询加锁,如果KEY不存在,就加锁,然后查DB入缓存,然后解锁;其他进程如果发现有锁就等待,然后等解锁后返回数据或者进入DB查询

    46 什么是 RedLock

  • 此种方式比原先的单节点的方法更安全。它可以保证以下特性:

  • 安全特性:互斥访问,即永远只有一个 client 能拿到锁
  • 避免死锁:最终 client 都可能拿到锁,不会出现死锁的情况,即使原本锁住某资源的 client crash 了或者出现了网络分区
  • 容错性:只要大部分 Redis 节点存活就可以正常提供服务

    41. redis 缺点

  • 单线程处理最大的缺点就是,如果前一个请求发生耗时比较久的操作,那么整个Redis就会阻塞住,其他请求也无法进来,直到这个耗时久的操作处理完成并返回,其他请求才能被处理到。

    42. redis 优点

  • 安全性,Redis是二进制安全的,不关心序列化和反序列化(key和value都是byte类型,序列化和反序列化是应用放来控制的)

  • Redis 主要通过网络框架进行访问,而不再是动态库了(用动态链接库实现的内存数据库,使用数据库的应用程序需要和库在一个主机上。如果不在同一主机上,就需要通过网络框架访问了,例如建立socket访问。跟语言有关,比如c++语言实现的就是由c++编写的程序调用,如果用动态链接库实现键值数据库,应用程序可以先实例化动态链接库提供的类,然后调用实例的接口进行数据存取。我还是以SimpleKV举个例子,使用动态链接库方式时,使用方式类似如下:实例化一个db,然后可以进行读写操作。 simplekv::DB *db db.get(key, &value) db.put(key, value)),这也使得 Redis 可以作为一个基础性的网络服务进行访问,扩大了 Redis 的应用范围。

    43. 整数数组和压缩列表在查找时间复杂度方面并没有很大的优势,那为什么 Redis 还会把它们作为底层数据结构呢?

  • 内存利用率,数组和压缩列表都是非常紧凑的数据结构,它比链表占用的内存要更少。Redis是内存数据库,大量数据存到内存中,此时需要做尽可能的优化,提高内存的利用率。

  • 数组对CPU高速缓存支持更友好,所以Redis在设计时,集合数据元素较少情况下,默认采用内存紧凑排列的方式存储,同时利用CPU高速缓存不会降低访问速度。当数据元素超过设定阈值后,避免查询时间复杂度太高,转为哈希和跳表数据结构存储,保证查询效率。

    44 如果数组上是随机访问,对CPU高速缓存友好吗

  • Redis底层的使用数组和压缩链表对数据大小限制在64个字节以下,当大于64个字节会改变存储数据的数据结构,所以随机访问对于CPU高速缓存没啥影响(缓存行 (Cache Line) 便是 CPU Cache 中的最小单位,CPU Cache 由若干缓存行组成,一个缓存行的大小通常是 64 字节(这取决于 CPU),并且它有效地引用主内存中的一块地址。)

    45 跳表的话,zscore时间复杂度是多少

  • zset有个ZSCORE的操作,用于返回单个集合member的分数,它的操作复杂度是O(1),这个hash table保存了集合元素和相应的分数,所以做ZSCORE操作时,直接查这个表就可以,复杂度就降为O(1)了。


    46 redis为啥是后写AOF日志

  • 不会对命令进行语法检查

  • 只记录执行成功的命令
  • 执行完之后再记录,不会阻塞当前的写操作