Redis高频面试题完整版

发表于 2年以前  | 总阅读数:752 次

大家好, 今天分享路人张大佬整理的Redis高频面试题,大约两万多字,点击上面的公众号后台回复“面试手册”可以获取PDF版

文章目录:

  • Redis概述

  • 什么是Redis?

  • Redis的优缺点?

  • Redis为什么常常用做缓存?相比于guava有什么优势?

  • Redis和Memcached的区别与共同点?

  • Redis是单线程还是多线程?Redis为什么这么快?

  • Redis6.0之后为什么引入了多线程?

  • Redis的数据类型有哪些?

  • Redis的数据结构有哪些?

  • Redis的应用场景有哪些?

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

  • 过期键的删除策略

  • 键的过期删除策略

  • Redis的内存淘汰机制是什么样的?

  • Redis的持久化

  • 什么是Redis的持久化?

  • Redis常见的持久化机制有哪些?有什么有优缺点?

  • Redis的事务

  • 什么是Redis的事务

  • Redis事务的相关命令

  • Redis事务执行的三个阶段

  • Redis事务的特性

  • Redis事务为什么不支持回滚?

  • Redis的集群、主从、哨兵

  • Redis集群的实现方案有哪些?

  • Redis主从架构中数据丢失吗

  • 如何解决主从架构数据丢失问题?

  • Redis集群的主从复制过程是什么样的?

  • Redis是如何保证主从服务器一致处于连接状态以及命令是否丢失?

  • 因为网络原因在主从复制过程中停止复制会怎么样?

  • 了解Redis哈希槽吗?

  • Redi集群最大的节点个数是多少?为什么?

  • Redis集群是如何选择数据库的?

  • Redis高可用方案如何实现?

  • Redis的分区

  • Redis的分区作用是什么?

  • Redis分区有哪些实现方案?

  • Redis分区的缺点?

  • Redis的分布式问题

  • 什么是分布式锁?

  • 分布式锁具有哪些特性?

  • 分布式锁的实现方法?

  • Redis如何实现分布式锁?

  • Redis并发竞争key问题应该如何解决?

  • 什么是RedLock

  • Redis的缓存问题

  • 说下什么是缓存雪崩、缓存穿透、缓存击穿,及它们的解决方案

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

  • Redis其他高频面试题

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

  • Redis如何实现大量数据插入?

  • 如何通过Redis实现异步队列?

  • 如何通过Redis实现延时队列?

  • Redis回收使用什么算法?

  • Redis 里面有1亿个 key,其中有 10 个 key 是包含 java,如何将它们全部找出来?

  • 生产环境中的Redis是如何部署的

Redis概述

什么是Redis?

Redis是一个高性能的非关系型的键值对数据库,使用C编写实现的。与传统的数据库不同的是Redis是存在内存中的,所以读写速度非常快,每秒可以处理超过10万次的读写操作,这也是Redis常常被用作缓存的原因。

Redis的优缺点?

优点:

  • 读写性能好,读的速度可达110000次/s,写的速度可达81000次/s。
  • 支持数据持久化,有AOF和RDB两中持久化方式
  • 数据结构丰富,支持String、List、Set、Hash等结构
  • 支持事务,Redis所有的操作都是原子性的,并且还支持几个操作合并后的原子性执行,原子性指操作要么成功执行,要么失败不执行,不会执行一部分。
  • 支持主从复制,主机可以自动将数据同步到从机,进行读写分离。

缺点:

  • 因为Redis是将数据存到内存中的,所以会受到内存大小的限制,不能用作海量数据的读写
  • Redis不具备自动容错和恢复功能,主机或从机宕机会导致前端部分读写请求失败,需要重启机器或者手动切换前端的IP才能切换

Redis为什么常常用做缓存?相比于guava有什么优势?

缓存的定义是访问速度比一般随机存取存储器快的一种高速存储器,而因为Redis是基于内存提供了高性能的数据存取功能,其比较显著的优势就是非常地快。

缓存可以分为本地缓存或者分布式缓存,比较常用的guava缓存就是一种本地缓存,其主要特点是轻量并且快速,生命周期随着JVM的销毁而结束,缺点是在多实例的情况下,每个实例都要自己保存一份缓存,这样会导致缓存的一致性出现问题。

Redis则是分布式缓存,在多实例情况下,每个实例都共享一份缓存数据,缓存具备一致性。缺点是要保持Redis的高可用整体架构会比较复杂。

Redis和Memcached的区别与共同点?

相同点:

  • 两者的读写性能都比较高
  • 都是基于内存的数据库,通常被当作缓存使用
  • 都有过期策略
  • 都是基于C语言实现

不同点:

不同点 Redis Memcached
是否支持复制 支持主从复制 不支持复制
key长度 长度最大为 2GB 长度最多为 250 个字节
数据类型 不仅支持key-value类型的数据,还支持hash、list、set、zset等数据等数据类型的数据 仅支持key-value类型的数据
数据持久化 支持数据持久化,可以将数据保存到磁盘 不支持数据持久化
网络IO模型 单线程的多路 IO 复用模型 多线程的非阻塞IO模式
集群 原生支持cluster 模式集群 无原生

Redis是单线程还是多线程?Redis为什么这么快?

Redis6.0之前是单线程的,为什么Redis6.0之前采用单线程而不采用多线程呢?

简单来说,就是Redis官方认为没必要,单线程的Redis的瓶颈通常在CPU的IO,而在使用Redis时几乎不存在CPU成为瓶颈的情况。使用Redis主要的瓶颈在内存和网络,并且使用单线程也存在一些优点,比如系统的复杂度较低,可为维护性较高,避免了并发读写所带来的一系列问题。

Redis为什么这么快主要有以下几个原因:

  • 运行在内存中
  • 数据结构简单
  • 使用多路IO复用技术
  • 单线程实现,单线程避免了线程切换、锁等造成的性能开销。

Redis6.0之后为什么引入了多线程?

前面说了那么多Redis使用单线程的原因,但从Redis6.0后开始支持多线程了,简直打脸有点快。那么为什么较新的Redis版本又开始支持多线程了呢?

前面也说了Redis的瓶颈在内存和网络,Redis6.0引入多线程主要是为了解决网路IO读写这个瓶颈,执行命令还是单线程执行的,所以也不存在线程安全问题。

Redis6.0默认是否开启了多线程呢?

默认是没有开启的,如需开启,需要修改配置文件redis.conf:io-threads-do-reads no,no改为yes

Redis的数据类型有哪些?

Redis的常见的数据类型有String、Hash、Set、List、ZSet。还有三种不那么常见的数据类型:Bitmap、HyperLogLog、Geospatial

数据类型 可以存储的值 可进行的操作 应用场景
STRING 字符串、整数、浮点数 对整数或浮点数可以进行自增、自减操作 对字符串操作 键值对缓存及常规计数: 微博数, 粉丝数
LIST 列表(内部使用双向列表实现) 向列表两端添加元素,或者获得列表的某一个片段 存储文章ID列表、存储评论列表等
SET 无序集合(内部使用值为空的散列表) 增加/删除元素、获取集合中元素、取交集并集等等 共同好友、共同关注等
ZSET 有序集合(内部使用散列表和跳表) 添加、获取、删除元素 根据分值范围或者成员来获取元素 计算一个键的排名 去重、获取排名前几的用户
HASH 包含键值对的无序散列表 添加、获取、移除单个键值对 获取所有键值对 检查某个键是否存在 常用于存储对象

Bitmap:位图,是一个以位为单位的数组,数组中只能存储1或0,数组的下标在Bitmap中叫做偏移量。Bitmap实现统计功能,更省空间。面试中常问的布隆过滤器就有用到这种数据结构,布隆过滤器可以判断出哪些数据一定不在数据库中,所以常被用来解决Redis缓存穿透问题。

Hyperloglog:HyperLogLog 是一种用于统计基数的数据集合类型,每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基数。HyperLogLog 的优点是,在输入元素的数量或者体积非常大时,计算基数所需的空间总是固定 的、并且是很小的。缺点是 HyperLogLog 的统计规则是基于概率完成的,所以它给出的统计结果是有一定误差的,标准误算率是 0.81%。常见的应用场景:统计网站的UV

Geospatial:主要用于存储地理位置信息,常用于定位附近的人,打车距离的计算等。

Redis的数据结构有哪些?

很多人都会把数据结构和数据类型混为一谈,包括很多面试官问的时候也没有刻意区分这两个。Redis的数据结构比较多,篇幅有限,这里只重点介绍面试常问的跳跃表。

Redis的数据结构有简单动态字符串链表字典跳跃表整数集合压缩列表等。

简单动态字符串:大家都知道,Redis的底层是用C语言编写,但Redis并没有直接使用C语言传统的字符串表示,而是构建了一种名为简单动态字符串的抽象类型。

链表:链表提供了高效的节点重排能力,以及顺序性的节点访问方式,并且可以通过增删节点来灵活地调整链表的长度。链表是列表的底层实现之一

字典:字典,又称为符号表(symbol table)、关联数组(associativearray)或映射(map),是一种用于保存键值对(key-value pair)的抽象数据结构。字典在Redis中的应用相当广泛,比如Redis的数据库就是使用字典来作为底层实现的,对数据库的增、删、查、改操作也是构建在对字典的操作之上的。

整数集合: 整数集合(intset)是集合键的底层实现之一,当一个集合只包含整数值元素,并且这个集合的元素数量不多时,Redis就会使用整数集合作为集合键的底层实现。

压缩列表(ziplist):压缩列表是Redis为了节约内存而开发的,是由一系列特殊编码的连续内存块组成的顺序型(sequential)数据结构。

对象:可能看到这里,很多人在想Redis的数据结构和数据类型的区别,其实上面介绍的是Redis的底层数据结构,但Redis并没有直接使用这些数据结构来实现键值对数据库,而是基于这些数据结构创建了一个对象系统,这个系统包含字符串对象、列表对象、哈希对象、集合对象有序集合对象这五种类型的对象,每种对象都用到了至少一种我们前面所介绍的数据结构,是不是这就和前面对上了。

看到这里很多人会好奇,为什么不直接使用这些底层数据结构,而是要创建对象系统。对象系统主要有以下优点:

  • 通过这五种不同类型的对象,Redis可以在执行命令之前,根据对象的类型来判断一个对象是否可以执行给定的命令。
  • 我们可以针对不同的使用场景,为对象设置多种不同的数据结构实现,从而优化对象在不同场景下的使用效率。
  • 实现了基于引用计数技术的内存回收机制,当程序不再使用某个对象的时候,这个对象所占用的内存就会被自动释放,了解Java虚拟机的垃圾回收机制看到这里是不是很熟悉。
  • edis还通过引用计数技术实现了对象共享机制,这一机制可以在适当的条件下,通过让多个数据库键共享同一个对象来节约内存。

对象这部分占了比较大的篇幅,其实面试中问的也不多,但为了更方便理解,介绍地多些。顺便看下这些底层数据结构和对象系统的对应关系。

最后介绍下面试中常问的跳跃表。

跳跃表(skiplist):跳跃表(skiplist)是一种有序数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。跳跃表支持平均O(logN)、最坏O(N)复杂度的节点查找,还可以通过顺序性操作来批量处理节点。跳跃表作是序集合键的底层实现之一

和链表、字典等数据结构被广泛地应用在Redis内部不同,Redis只在两个地方用到了跳跃表,一个是实现有序集合键,另一个是在集群节**点中用作内部数据结构**,除此之外,跳跃表在Redis里面没有其他用途。

跳跃表本质上采用的是一种空间换时间的策略,是一种可以可以进行二分查找有序链表,跳表在原有的有序链表上增加了多级索引,通过索引来实现快速查询。跳表不仅能提高搜索性能,同时也可以提高插入和删除操作的性能。

这是一个原始的有序列表,时间复杂度为O(n)。

为了提高查找效率,可以对链表建立一级索引,如下图,在之前找到11这个元素需要遍历6个节点,现在需要5个。链表越长,效率提升越明显。

为了继续提高查找效率可以继续增加索引

对于理想的跳表,每向上一层索引节点数量都是下一层的1/2,跳表的**时间复杂度为o(logn),空间复杂度为o(n)**,虽然是空间换时间的策略,这里举例存储的只是数字,如果是存储比较大的对象,浪费的空间就不值得一提了,因为索引结点只需要存储关键值和几个指针,并不需要存储对象。

跳表相比于红黑树的优点(redis为什么用跳表不同红黑树):

  • 内存占用更少,自定义参数化决定使用多少内存
  • 查询性能至少不比红黑树差
  • 简单更容易实现和维护

上面这三个优点是我在一篇博客中看到,这个问题redis作者本人也回应过。我这蹩脚的英文水平就不翻译了,以免跑偏了。

  • They are not very memory intensive. It's up to you basically. Changing parameters about the probability of a node to have a given number of levels will make then less memory intensive than btrees.
  • A sorted set is often target of many ZRANGE or ZREVRANGE operations, that is, traversing the skip list as a linked list. With this operation the cache locality of skip lists is at least as good as with other kind of balanced trees.
  • They are simpler to implement, debug, and so forth. For instance thanks to the skip list simplicity I received a patch (already in Redis master) with augmented skip lists implementing ZRANK in O(log(N)). It required little changes to the code.

最后,说下Redis中的跳跃表和普通的跳跃表有什么区别?

  • Redis中的跳跃表分数(score)允许重复,即跳跃表的key允许重复,如果分数重复,还需要根据数据内容来进字典排序。普通的跳跃表是不支持的。
  • 第1层链表不是一个单向链表,而是一个双向链表。这是为了方便以倒序方式获取一个范围内的元素。
  • 在Redis的跳跃表中可以很方便地计算出每个元素的排名。

篇幅有限,关于跳表的实现细节就不过多介绍了,有兴趣的同学可以自行了解,本小节部分内容参考《Redis设计与实现》

Redis的应用场景有哪些?

  • 缓存:Redis基于内存,读写速度非常快,并且有键过期功能和键淘汰策略,可以作为缓存使用。
  • 排行榜:Redis提供的有序集合可以很方便地实现排行榜。
  • 分布式锁:Redis的setnx功能来实现分布式的锁。
  • 社交功能:实现共同好友、共同关注等
  • 计数器:通过String进行自增自减实现计数功能
  • 消息队列:Redis提供了发布、订阅、阻塞队列等功能,可以实现一个简单的消息队列。

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

可以在一个服务器上部署多个Redis实例,把他们当作不同的服务器使用。

过期键的删除策略

键的过期删除策略

常见的过期删除策略是惰性删除定期删除定时删除

  • 惰性删除:只有访问这个键时才会检查它是否过期,如果过期则清除。优点:最大化地节约CPU资源。缺点:如果大量过期键没有被访问,会一直占用大量内存。
  • 定时删除:为每个设置过期时间的key都创造一个定时器,到了过期时间就清除。优点:该策略可以立即清除过期的键。缺点:会占用大量的CPU资源去处理过期的数据。
  • 定期删除:每隔一段时间就对一些键进行检查,删除其中过期的键。该策略是惰性删除和定时删除的一个折中,既避免了占用大量CPU资源又避免了出现大量过期键不被清除占用内存的情况。

Redis中同时使用了惰性删除定期删除两种。

Redis的内存淘汰机制是什么样的?

Redis是基于内存的,所以容量肯定是有限的,有效的内存淘汰机制对Redis是非常重要的。

当存入的数据超过Redis最大允许内存后,会触发Redis的内存淘汰策略。在Redis4.0前一共有6种淘汰策略。

  • volatile-lru:当Redis内存不足时,会在设置了过期时间的键中使用LRU算法移除那些最少使用的键。(注:在面试中,手写LRU算法也是个高频题,使用双向链表和哈希表作为数据结构)
  • volatile-ttl:从设置了过期时间的键中移除将要过期的
  • volatile-random:从设置了过期时间的键中随机淘汰一些
  • allkeys-lru:当内存空间不足时,根据LRU算法移除一些键
  • allkeys-random:当内存空间不足时,随机移除某些键
  • noeviction:当内存空间不足时,新的写入操作会报错

前三个是在设置了过期时间的键的空间进行移除,后三个是在全局的空间进行移除

在Redis4.0后可以增加两个

  • volatile-lfu:从设置过期时间的键中移除一些最不经常使用的键(LFU算法:Least Frequently Used))
  • allkeys-lfu:当内存不足时,从所有的键中移除一些最不经常使用的键

这两个也是一个是在设置了过期时间的键的空间,一个是在全局空间。

Redis的持久化

什么是Redis的持久化?

因为Redis是基于内存的,为了让防止一些意外情况导致数据丢失,需要将数据持久化到磁盘上。

Redis常见的持久化机制有哪些?有什么有优缺点?

Redis提供了两种不同的持久化方式,一种是RDB,一种是AOF

RDB

RDB是redis默认的持久化方式,按照一定的时间间隔将内存的数据以快照的形式保存到硬盘,恢复时是将快照读取到内存中。RDB持久化实际操作过程是fork一个子进程,先将数据集写入临时文件,写入成功后,再替换之前的文件,用二进制压缩存储。如下图

优点:

  • 适合对大规模的数据恢复,比AOF的启动效率高
  • 只有一个文件 dump.rdb,方便持久化
  • 性能最大化,在开始持久化时,它唯一需要做的只是fork出子进程,之后再由子进程完成这些持久化的工作,这样就可以极大的避免服务进程执行IO操作了。

缺点:

  • 数据安全性低,在一定间隔时间内做一次备份,如果Redis突然宕机,会丢失最后一次快照的修改
  • 由于RDB是通过fork子进程来协助完成数据持久化工作的,因此当数据集较大时,可能会导致整个服务器停止服务几百毫秒,甚至是1秒钟。

AOF

AOF持久化以日志的形式记录服务器所处理的每一个写、删除操作,查询操作不会记录,以文本的方式记录,可以打开文件看到详细的操作记录。

优点:

  • 具备更高的安全性,Redis提供了3种同步策略,分别是每秒同步、每修改同步和不同步。相比RDB突然宕机丢失的数据会更少,每秒同步会丢失一秒种的数据,每修改同步会不会丢失数据。
  • 由于该机制对日志文件的写入操作采用的是append模式,因此在写入过程中即使出现宕机现象,也不会破坏日志文件中已经存在的内容。
  • AOF包含一个格式清晰、易于理解的日志文件用于记录所有的修改操作,可以通过该文件完成数据的重建。

缺点:

  • 对于相同数量的数据集而言,AOF文件通常要大于RDB文件。RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。
  • 根据AOF选择同步策略的不同,效率也不同,但AOF在运行效率上往往会慢于RDB。

Redis的事务

什么是Redis的事务

Redis的事务是一个单独的隔离操作,事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断,所以Redis事务是在一个队列中,一次性、顺序性、排他性地执行一系列命令。

Redis 事务的主要作用就是串联多个命令防止别的命令插队

Redis事务的相关命令

Redis事务相关的命令主要有以下几种:

  • DISCARD:命令取消事务,放弃执行事务队列内的所有命令,恢复连接为非 (transaction) 模式,如果正在使用 WATCH 命令监视某个(或某些) key,那么取消所有监视,等同于执行命令 UNWATCH 。
  • EXEC:执行事务队列内的所有命令。
  • MULTI:用于标记一个事务块的开始。
  • UNWATCH:用于取消 WATCH命令对所有 key 的监视。如果已经执行过了EXEC或DISCARD命令,则无需再执行UNWATCH命令,因为执行EXEC命令时,开始执行事务,WATCH命令也会生效,而 DISCARD命令在取消事务的同时也会取消所有对 key 的监视,所以不需要再执行UNWATCH命令了
  • WATCH:用于标记要监视的key,以便有条件地执行事务,WATCH命令可以监控一个或多个键,一旦其中有一个键被修改(或删除),之后的事务就不会执行。

Redis事务执行的三个阶段

  1. 开始事务(MULTI)
  2. 命令入列
  3. 执行事务(EXEC)

Redis事务的特性

  • Redis事务不保证原子性,单条的Redis命令是原子性的,但事务不能保证原子性。
  • Redis事务是有隔离性的,但没有隔离级别,事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。(顺序性、排他性)
  • Redis事务不支持回滚,Redis执行过程中的命令执行失败,其他命令仍然可以执行。(一次性)

Redis事务为什么不支持回滚?

在Redis的事务中,命令允许失败,但是Redis会继续执行其它的命令而不是回滚所有命令,是不支持回滚的。

主要原因有以下两点:

  • Redis 命令只在两种情况失败:

  • 语法错误的时候才失败(在命令输入的时候不检查语法)。

  • 要执行的key数据类型不匹配:这种错误实际上是编程错误,这应该在开发阶段被测试出来,而不是生产上。

  • 因为不需要回滚,所以Redis内部实现简单并高效。(在Redis为什么是单线程而不是多线程也用了这个思想,实现简单并且高效)

Redis的集群、主从、哨兵

Redis集群的实现方案有哪些?

在说Redis集群前,先说下为什么要使用Redis集群,Redis单机版主要有以下几个缺点:

  • 不能保证数据的可靠性,服务部署在一台服务器上,一旦服务器宕机服务就不可用。
  • 性能瓶颈,内存容量有限,处理能力有限

Redis集群就是为了解决Redis单机版的一些问题,Redis集群主要有以下几种方案

  • Redis 主从模式
  • Redis 哨兵模式
  • Redis 自研
  • Redis Clustert

下面对这几种方案进行简单地介绍:

Redis主从模式

Redis单机版通过RDB或AOF持久化机制将数据持久化到硬盘上,但数据都存储在一台服务器上,并且读写都在同一服务器(读写不分离),如果硬盘出现问题,则会导致数据不可用,为了避免这种问题,Redis提供了复制功能,在master数据库中的数据更新后,自动将更新的数据同步到slave数据库上,这就是主从模式的Redis集群,如下图

主从模式解决了Redis单机版存在的问题,但其本身也不是完美的,主要优缺点如下:

优点:

  • 高可靠性,在master数据库出现故障后,可以切换到slave数据库
  • 读写分离,slave库可以扩展master库节点的读能力,有效应对大并发量的读操作

缺点:

  • 不具备自动容错和恢复能力,主节点故障,从节点需要手动升为主节点,可用性较低

Redis 哨兵模式

为了解决主从模式的Redis集群不具备自动容错和恢复能力的问题,Redis从2.6版本开始提供哨兵模式

哨兵模式的核心还是主从复制,不过相比于主从模式,多了一个竞选机制(多了一个哨兵集群),从所有从节点中竞选出主节点,如下图

从上图中可以看出,哨兵模式相比于主从模式,主要多了一个哨兵集群,哨兵集群的主要作用如下:

  • 监控所有服务器是否正常运行:通过发送命令返回监控服务器的运行状态,处理监控主服务器、从服务器外,哨兵之间也相互监控
  • 故障切换:当哨兵监测到master宕机,会自动将slave切换成master,然后通过发布订阅模式通知其他的从服务器,修改配置文件,让它们切换master。同时那台有问题的旧主也会变为新主的从,也就是说当旧的主即使恢复时,并不会恢复原来的主身份,而是作为新主的一个从。

哨兵模式的优缺点:

优点:

  • 哨兵模式是基于主从模式的,解决可主从模式中master故障不可以自动切换故障的问题。

缺点:

  • 浪费资源,集群里所有节点保存的都是全量数据,数据量过大时,主从同步会严重影响性能
  • Redis主机宕机后,投票选举结束之前,谁也不知道主机和从机是谁,此时Redis也会开启保护机制,禁止写操作,直到选举出了新的Redis主机。
  • 只有一个master库执行写请求,写操作会单机性能瓶颈影响

Redis 自研

哨兵模式虽然解决了主从模式存在的一些问题,但其本身也存在一些弊端,比如数据在每个Redis实例中都是全量存储,极大地浪费了资源,为了解决这个问题,Redis提供了Redis Cluster,实现了数据分片存储,但Redis提供Redis Cluster之前,很多公司为了解决哨兵模式存在的问题,分别自行研发Redis集群方案。

客户端分片

客户端分片是把分片的逻辑放在Redis客户端实现,通过Redis客户端预先定义好的路由规则(使用哈希算法),把对Key的访问转发到不同的Redis实例中,查询数据时把返回结果汇集。如下图

客户端分片的优缺点:

优点:Redis实例彼此独立,相互无关联,每个Redis实例像单服务器一样运行,非常容易线性扩展,系统的灵活性很强。

缺点:

  • 客户端sharding不支持动态增删节点。服务端Redis实例群拓扑结构有变化时,每个客户端都需要更新调整。
  • 运维成本比较高,集群的数据出了任何问题都需要运维人员和开发人员一起合作,减缓了解决问题的速度,增加了跨部门沟通的成本。
  • 在不同的客户端程序中,维护相同的路由分片逻辑成本巨大。比如:java项目、PHP项目里共用一套Redis集群,路由分片逻辑分别需要写两套一样的逻辑,以后维护也是两套。
代理分片

客户端分片的最大问题就是服务端Redis实例群拓扑结构有变化时,每个客户端都需要更新调整。

为了解决这个问题,代理分片出现了,代理分片将客户端分片模块单独分了出来,作为Redis客户端和服务端的桥梁,如下图

代理模式的优点:解决了服务端Redis实例群拓扑结构有变化时,每个客户端都需要更新调整的问题。缺点是由于Redis客户端的每个请求都经过代理才能到达Redis服务器,这个过程中会产生性能损失。

常见的代理分片有Twitter开源的Redis代理Twemproxy和豌豆荚自主研发的Codis

Redis Cluster

前面介绍了为了解决哨兵模式的问题,各大企业提出了一些数据分片存储的方案,在Redis3.0中,Redis也提供了响应的解决方案,就是Redist Cluster。

Redis Cluster是一种服务端Sharding技术,Redis Cluster并没有使用一致性hash,而是采用slot(槽)的概念,一共分成16384个槽。将请求发送到任意节点,接收到请求的节点会将查询请求发送到正确的节点上执行。

什么是Redis哈希槽呢?本来不想详细介绍这个的,但面试确实经常问,还是简单说一下,

在介绍slot前,要先介绍下一致性哈希(客户端分片经常会用的哈希算法),那这个一致性哈希有什么用呢?其实就是用来保证节点负载均衡的,那么多主节点,到底要把数据存到哪个主节点上呢?就可以通过一致性哈希算法要把数据存到哪个节点上。

一致性哈希

下面详细说下一致性哈希算法,首先就是对key计算出一个hash值,然后对2^32取模,也就是值的范围在[0, 2^32 -1],一致性哈希将其范围抽象成了一个圆环,使用CRC16算法计算出来的哈希值会落到圆环上的某个地方。

现在将Redis实例也分布在圆环上,如下图(图片来源于网络)

假设A、B、C三个Redis实例按照如图所示的位置分布在圆环上,通过上述介绍的方法计算出key的hash值,发现其落在了位置E,按照顺时针,这个key值应该分配到Redis实例A上。

如果此时Redis实例A挂了,会继续按照顺时针的方向,之前计算出在E位置的key会被分配到RedisB,而其他Redis实例则不受影响。

但一致性哈希也不是完美的,主要存在以下问题:当Redis实例节点较少时,节点变化对整个哈希环中的数据影响较大,容易出现部分节点数据过多,部分节点数据过少的问题,出现数据倾斜的情况,如下图(图片来源于网络),数据落在A节点和B节点的概率远大于B节点

为了解决这种问题,可以对一致性哈希算法引入虚拟节点(A#1,B#1,C#1),如下图(图片来源于网络),

那这些虚拟节点有什么用呢?每个虚拟节点都会映射到真实节点,例如,计算出key的hash值后落入到了位置D,按照顺时针顺序,应该落到节点C#1这个虚拟节点上,因为虚拟节点会映射到真实节点,所以数据最终存储到节点C。

虚拟槽

在Redis Cluster中并没有使用一致性哈希,而引进了虚拟槽。虚拟槽的原理和一致性哈希很像,Redis Cluster一共有2^14(16384)个槽,所有的master节点都会有一个槽范围比如0~1000,槽数是可以迁移的。master节点的slave节点不分配槽,只拥有读权限,其实虚拟槽也可以看成一致性哈希中的虚拟节点。

虚拟槽和一致性哈希算法的实现上也很像,先通过CRC16算法计算出key的hash值,然后对16384取模,得到对应的槽位,根据槽找到对应的节点,如下图。

使用虚拟槽的好处:

  • 更加方便地添加和移除节点,增加节点时,只需要把其他节点的某些哈希槽挪到新节点就可以了,当需要移除节点时,只需要把移除节点上的哈希槽挪到其他节点就行了,不需要停掉Redis任何一个节点的服务(采用一致性哈希需要增加和移除节点需要rehash
Redis Cluster 结构

上面介绍了Redis Cluster如何将数据分配到合适的节点,下面来介绍下Redis Cluster结构,简单来说,Redis Cluster可以看成多个主从架构组合在一起,如下图

上图看起来比较乱,其实很好理解,上图中一个Redis Cluster有两组节点组成(官方推荐,一般至少三主三从六个节点,画多个节点看起来太乱,所以上图只画了两个主节点),每组节点可以看成一个主从模式,并且每组节点负责的slot不同(假设有4个slot,A组节点负责第1个和第二个slot,B组节点负责第3个和第4个,其中master节点负责写,slave节点负责读)

上图中共有三种线

主备复制的线很好理解,就和主从模式一样,在master库中的数据更新后,自动将更新的数据同步到slave库上

对外服务的线也很好理解,就是外部在对Redis进行读取操作,访问master进行写操作,访问slave进行读操作

Redis Bus的作用相对复杂些,这里简单说下,

首先要知道Redis Cluster是一个去中心化的架构,不存在统一的配置中心,Redis Cluster中的每个节点都保存了集群的配置信息,在Redis Cluster中,这个配置信息通过Redis Cluster Bus进行交互,并最后达成一致性。

配置信息的一致性主要依靠PING/PONG,每个节点向其他节点频繁的周期性的发送PING/PONG消息。对于大规模的集群,如果每次PING/PONG 都携带着所有节点的信息,则网络开销会很大。此时Redis Cluster 在每次PING/PONG,只包含了随机的一部分节点信息。由于交互比较频繁,短时间的几次交互之后,集群的状态也会达成一致。

当Cluster 结构不发生变化时,各个节点通过gossip 协议(Redis Cluster各个节点之间交换数据、通信所采用的一种协议)在几轮交互之后,便可以得知Cluster的结构信息,达到一致性的状态。但是当集群结构发生变化时(故障转移/分片迁移等),优先得知变更的节点会将自己的最新信息扩散到Cluster,并最终达到一致。

其实,上面说了半天也不太容易理解,简单来说Redis Bus是用于节点之间的信息交路,交互的信息有以下几个:

  • 数据分片(slot)和节点的对应关系(对应上图中的slot1和slot2在masterA节点上,就是要知道哪个slot在哪个节点上)
  • 集群中每个节点可用状态(不断向其他节点发消息看看你挂了没)
  • 集群结构发生变更时,通过一定的协议对配置信息达成一致。数据分片的迁移、主备切换、单点master的发现和其发生主备关系变更等,都会导致集群结构变化。(发现有的节点挂了或者有新的节点加进来了,赶紧和其他节点同步信息)

Redis主从架构中数据丢失吗

Redis主从架构丢失主要有两种情况

  • 异步复制同步丢失
  • 集群产生脑裂数据丢失

下面分别简单介绍下这两种情况:

异步复制同步丢失:

Redis主节点和从节点之间的复制是异步的,当主节点的数据未完全复制到从节点时就发生宕机了,master内存中的数据会丢失。

如果主节点开启持久化是否可以解决这个问题呢?

答案是否定的,在master 发生宕机后,sentinel集群检测到主节点发生故障,重新选举新的主节点,如果旧的主节点在故障恢复后重启,那么此时它需要同步新主节点的数据,此时新的主节点的数据是空的(假设这段时间中没有数据写入)。那么旧主机点中的数据就会被刷新掉,此时数据还是会丢失。

集群产生脑裂:

简单来说,集群脑裂是指一个集群中有多个主节点,像一个人有两个大脑,到底听谁的呢?

例如,由于网络原因,集群出现了分区,master与slave节点之间断开了联系,哨兵检测后认为主节点故障,重新选举从节点为主节点,但主节点可能并没有发生故障。此时客户端依然在旧的主节点上写数据,而新的主节点中没有数据,在发现这个问题之后,旧的主节点会被降为slave,并且开始同步新的master数据,那么之前的写入旧的主节点的数据被刷新掉,大量数据丢失。

如何解决主从架构数据丢失问题?

在Redis的配置文件中,有两个参数如下:

min-slaves-to-write 1
min-slaves-max-lag 10

其中,min-slaves-to-write默认情况下是0,min-slaves-max-lag默认情况下是10。

上述参数表示至少有1个salve的与master的同步复制延迟不能超过10s,一旦所有的slave复制和同步的延迟达到了10s,那么此时master就不会接受任何请求。

通过降低min-slaves-max-lag参数的值,可以避免在发生故障时大量的数据丢失,一旦发现延迟超过了该值就不会往master中写入数据。

这种解决数据丢失的方法是降低系统的可用性来实现的。

Redis集群的主从复制过程是什么样的?

主从复制主要有以下几步:

  1. 设置服务器的地址和端口号
  2. 建立套接字连接(建立主从服务器之间的连接)
  3. 发送PING命令(检验套接字是否可用)
  4. 身份验证
  5. 同步(从主库向从库同步数据,分为全量复制和部分复制)
  6. 命令传播(经过上面同步操作,此时主从的数据库状态其实已经一致了,许主服务器马上就接受到了新的写命令,执行完该命令后,主从的数据库状态又不一致。数据同步阶段完成后,主从节点进入命令传播阶段;在这个阶段主节点将自己执行的写命令发送给从节点,从节点接收命令并执行,从而保证主从节点数据的一致性)

简单解释下全量复制和部分复制:

在Redis2.8以前,从节点向主节点发送sync命令请求同步数据,此时的同步方式是全量复制;在Redis2.8及以后,从节点可以发送psync命令请求同步数据,此时根据主从节点当前状态的不同,同步方式可能是全量复制或部分复制。

  • 全量复制:用于初次复制或其他无法进行部分复制的情况,将主节点中的所有数据都发送给从节点,是一个非常重型的操作。
  • 部分复制:用于网络中断等情况后的复制,只将中断期间主节点执行的写命令发送给从节点,与全量复制相比更加高效。需要注意的是,如果网络中断时间过长,导致主节点没有能够完整地保存中断期间执行的写命令,则无法进行部分复制,仍使用全量复制。

Redis是如何保证主从服务器一致处于连接状态以及命令是否丢失?

命令传播阶段,从服务器会利用心跳检测机制定时的向主服务发送消息。

因为网络原因在主从复制过程中停止复制会怎么样?

如果出现网络问题断开,会自动重连,并且支持断点续传,接着上次复制的地方继续复制,而不是重新复制一份。

下面说下其中的实现细节,首先需要了解replication bufferreplication backlog

replication buffer:主库连接的每一个从库的对应一个 replication buffer,主库执行完每一个操作命令后,会将命令分别写入每一个从库所对应的 replication buffer

replication backlog:replication backlog 是一个环形区域,大小可以通过 repl-backlog-size参数设置,并且和 replication buffer不同的是,一个主库中只有一个 replication backlog。

主库会通过 master_repl_offset 记录写入的位置,从库会通过 slave_repl_offset 记录自己读取的位置,这里的位置也叫做偏移量。

刚开始复制数据的时候,上述两者的值相同,且都位于初始位置。每当主库向 replication backlog 写入一个操作,master_repl_offset 就会增加 1,从库复制完操作命令后,slave_repl_offset 也会增加 1。

正常情况下,slave_repl_offset 会跟着 master_repl_offset 变化,两者保持一个小的差距,或者相等。

如果主从库之间的网络连接中断,从库便无法继续复制主库的操作命令,但是主库依然会向 replication backlog 中写入操作命令。

当网络恢复之后,从库会继续向主库请求同步数据,主库通过slave_repl_offset知道从库复制操作命令的位置。这个时候,主库只需要把 master_repl_offsetslave_repl_offset 之间的操作命令同步给从库就可以了。

但是前面提到 replication backlog 是一个环形结构,如果网络中断的时间过长,随着主库不断向其中写入操作命令,master_repl_offset 不断增大,就会有从库没有复制的操作命令被覆盖。如果出现这种情况,就需要重新进行全量复制了。

为了避免全量复制的情况,可以通过修改 repl-backlog-size 参数的值,为 replication backlog 设置合适的大小。

这个值需要结合实际情况来设置,具体思路是:从命令在主库中产生到从库复制完成所需要的时间为 t,每秒钟产生的命令的数量为 c,命令的大小为 s,这个值不能低于他们的乘积。考虑到突发的网络压力以及系统运行过程中可能出现的阻塞等情况,应该再将这个值乘以 2 或者更多。

此处参考博客:https://juejin.cn/post/7017827613544546335

了解Redis哈希槽吗?

见Redis Cluster处

Redi集群最大的节点个数是多少?为什么?

16384个

Redis作者的回答:

  • Normal heartbeat packets carry the full configuration of a node, that can be replaced in an idempotent way with the old in order to update an old config. This means they contain the slots configuration for a node, in raw form, that uses 2k of space with16k slots, but would use a prohibitive 8k of space using 65k slots.
  • At the same time it is unlikely that Redis Cluster would scale to more than 1000 mater nodes because of other design tradeoffs.

上面主要说了两点,

在redis节点发送心跳包时需要把所有的槽放到这个心跳包里,以便让节点知道当前集群信息,16384=16k,在发送心跳包时使用char进行bitmap压缩后是2k(2 * 8 (8 bit) * 1024(1k) = 16K),也就是说使用2k的空间创建了16k的槽数。虽然使用CRC16算法最多可以分配65535(2^16-1)个槽位,65535=65k,压缩后就是8k(8 * 8 (8 bit) * 1024(1k) =65K),也就是说需要8k的心跳包,作者认为这样做不太值得

由于其他设计折衷,一般情况下一个redis集群不会有超过1000个master节点

Redis集群是如何选择数据库的?

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

Redis高可用方案如何实现?

常见的Redis高可用方案有以下几种:

  • 数据持久化
  • 主从模式
  • Redis 哨兵模式
  • Redis 集群(自研及Redis Cluster)

Redis的分区

Redis的分区作用是什么?

  • 扩展数据库容量,可以利用多台机器的内存构建更大的数据库
  • 扩展计算能力,分区可以在多核和多计算机之间弹性扩展计算能力,在多计算机和网络适配器之间弹性扩展网络带宽

Redis分区有哪些实现方案?

在介绍Redis集群的实现方案时已经介绍过了客户端分区代理分区,常见的Redis分区方案主要有以下三种:

  • 客户端分区:客户端决定数据被存到哪个Redis节点或者从哪个节点读取
  • 代理分区:客户端将请求发送到代理,而不是直接发送到Redis节点,代理根据分区策略将请求发送到Redis节点上
  • 查询路由:客户端随机请求任意一个Redis节点,这个Redis节点将请求转发到正确的Redis节点。Redis Cluster实现了一种混合形式的查询路由,并不是直接将请求从一个Redis节点转发到另一个Redis节点,而是在客户端的帮助下直接重定向到正确的redis节点

Redis分区的缺点?

  • 不支持多个键的操作,例如不能操作映射在两个Redis实例上的两个集合的交叉集。(其实可以做到这一点,但是需要间接的解决)
  • Redis不支持多个键的事务
  • Redis是以键来分区,因此不能使用单个大键对数据集进行分片,例如一个非常大的有序集
  • 数据的处理会变得复杂,比如你必须处理多个RDB和AOF文件,在多个实例和主机之间持久化你的数据
  • 添加和删除节点也会变得复杂,例如通过在运行时添加和删除节点,Redis集群通常支持透明地再均衡数据,但是其他系统像客户端分区或者代理分区的特性就不支持该特性。不过Pre-sharding(预分片)可以在这方面提供帮助。

Redis的分布式问题

什么是分布式锁?

相信大家对程序中的锁并不陌生,无论是在并发编程或者Java虚拟机都有学到过。

锁在程序中的作用主要是同步,就是保证共享资源在同一时刻只能被同一个线程访问。

分布式锁则是为了保证在分布式场景下,共享资源在同一时刻只能被同一个线程访问,或者说是用来控制分布式系统之间同步访问共享资源。举个简单例子,如下图

从上图可以看出,变量A在三个服务器中都有涉及到,如果不对其进行控制的话,三个服务器中的变量A很难做到同步,解决这个问题的方法就是分布式锁。

分布式锁具有哪些特性?

  • 互斥性:在任意时刻,同一条数据只能被一台机器上的一个线程执行
  • 高可用性:当部分节点宕机后,客户端仍可以正常地获取锁和释放锁
  • 独占性:加锁和解锁必须同一台服务器执行,不能在一个服务器上加锁,在另一个服务器上释放锁
  • 防锁超时:如果客户端没有主动释放锁,服务器会在一定时间后自动释放锁, 防止客户端宕机或者网络异常导致宕机

分布式锁的实现方法?

基本思路就是要在整个系统中提供一个全局、唯一的获取锁的“东西”,然后每个系统在需要加锁时,都去问这个“东西”拿到一把锁,这样不同的系统拿到的就可以认为是同一把锁。

常见的分布式锁实现方案有三种:

基于关系型数据库

优点:直接借助数据库容易理解

缺点:在使用关系型数据库实现分布式锁的过程中会出现各种问题,例如数据库单点问题和可重入问题,并且在解决过程中会使得整个方案越来越复杂

基于Redis

优点:性能好,实现起来较为方便

缺点

  • key的过期时间设置难以确定,如何设置的失效时间太短,方法没等执行完,锁就自动释放了,那么就会产生并发问题。如果设置的时间太长,其他获取锁的线程就可能要平白的多等一段时间。
  • Redis的集群部署虽然能解决单点问题,但是并不是强一致性的,锁的不够健壮

基于zookeeper

优点:有效地解决单点问题,不可重入问题,非阻塞问题以及锁无法释放的问题,实现起来较为简单。

缺点:性能上不如使用缓存实现分布式锁

三种方案的对比

方案 复杂度 性能 可靠性 学习成本
基于关系型数据库
基于Redis
基于zookeeper

Redis如何实现分布式锁?

前面讲过了分布式锁的特性,其实实现分布式锁就是围绕着这些展开的

Redis实现分布式锁的主要命令:SETNX,该命令的作用是当key不存在时设置key的值,当Key存在时,什么都不做。

先来看最简单的实现方式,如下图

从上图可以看到主要两个关键步骤,加锁和解锁

但是这个简陋的分布式锁存在很多问题,并不能满足上述介绍的分布式锁的特性,

比如,当线程1执行到上图中执行业务这步时,业务代码突然出现异常了,无法进行删除锁这一步,那就完犊子了,死锁了,其他线程也无法获取到锁了(因为SETNX的特性)。

改进方案1

那咋整呢?

一提到异常,有人想起了try-catch-finally了,把删除锁的操作放到finally代码块中,就算出现异常,也是能正常释放锁的,执行业务出现异常这个问题确实解决了。但这玩意并不靠谱,如果Redis在执行业务这步宕机了呢,finally代码块也不会执行了。

改进方案2

其实这个问题很好解决,只需给锁设置一个过期时间就可以了,对key设置过期时间在Redis中是常规操作了。就是这个命令SET key value [EX seconds][PX milliseconds] [NX|XX]

  • EX second: 设置键的过期时间为second秒;
  • PX millisecond:设置键的过期时间为millisecond毫秒;
  • NX:只在键不存在时,才对键进行设置操作;
  • XX:只在键已经存在时,才对键进行设置操作;
  • SET操作完成时,返回OK,否则返回nil。

那先现在这个方案就完美了吗?显然没有

例如,线程1获取到了锁,并设置了有效时间10秒,但线程1在执行业务时超过了10秒,锁到期自动释放了,在锁释放后,线程2又获取了锁,在线程2执行业务时,线程1执行完了,随后执行了删除锁这一步,但是线程1的锁早就到期自动释放了,他删除的是线程2的锁!!!

上面这个例子说的有点乱,突然想到一个现实生活中的例子,把Redis比作一个厕所,张三去上厕所,关上了门(获取锁,并设置了10秒),上到一半(10秒到了,门自动开了),这时李四进去了,关上了门(获取锁,并设置了10秒),张三上完了厕所,把门打开了走了(执行了删除锁操作)

上面这个荒诞的例子说明了方案2有两处不合理的地方,第一是张三厕所没上完,李四怎么能进去呢?他们上厕所产生了冲突,第二是张三上完厕所怎么能打开李四的门呢(分布式锁的特性,加锁和解锁必须同一台服务器执行)?

改进方案3

其实看起来方案2的问题很容易解决,只需要把锁的过期时间设置的非常长,就可以避免这两个问题,但是这样并不可行,因为这样相当于回到最简陋的方案(会导致李四一直上不到厕所)。

那如何能让李四上到厕所,还不会让自己锁的门被张三打开门呢?

很简单,为锁加一个标识,例如生成一个UUID,作为锁的标识,每个线程获取锁时都会生成一个不同的UUID作为锁的标识,在进行删除锁时会进行判断,锁的标识和自己生成UUID相等时才进行删除操作,这样就避免线程1释放了线程2的锁。(相当于自己上自己的锁,不要计较为什么张三在李四上厕所时不需要李四的钥匙就能离开厕所这种事,上厕所和分布式锁逻辑并不完全相同,只是简单类比)

那怎么解决李四未等张三上完厕所就进厕所呢?(如何确定锁的过期时间)

可以在加锁时,先设置一个预估的过期时间,然后开启一个守护线程,定时去检测这个锁的失效时间,如果锁快要过期了,操作共享资源还未完成,那么就自动对锁进行续期,重新设置过期时间。

好了,张三和李四上厕所的解决了。

那方案3就没有其他问题了吗?其实还是有的,比如目前的分布式锁还不具备可重入性(同一线程可以重复获取锁,解决线程需要多次进入锁内执行任务的问题)

改进方案4

参考其他重入锁的实现,可以通过对锁进行重入计数,加锁时加 1,解锁时减 1,当计数归 0 时才能释放锁。

那现在方案就没有问题了吗,其实还有

比如,线程1获取了锁,线程2没有获取到锁,那么线程2怎么知道线程1啥时候释放了锁,进而再去获取锁呢?

改进方案5

方案4中问题的解决方案,一般以下两种解决方案:

  • 可以通过客户端轮询的方式,就是线程2过一会就来看看是不是能获取锁了。这种方式比较消耗服务器资源,当并发量比较大时,会影响服务器的效率。
  • 通过Redis的发布订阅功能,当获取锁失败时,订阅锁释放消息,获取锁成功后释放时,发送锁释放消息。

那现在这个方案完美了吗?也还没有

目前讨论的都是redis是单节点的情况,如果这个节点挂了,那么所有的客户端都获取不到锁了

改进方案6

为了实现多节点Redis的分布式锁,Redis的作者提出了RedLock算法。

这是RedLock算法官网的地址,https://redis.io/topics/distlock,英文好的建议直接看官方文档,毕竟我这四六级飘过的人也可能理解的不准确,下面的内容主要是参考官网内容。

首先介绍保证分布式锁的有效性和安全性的要求:
  • 互斥性:在任何给定时刻,只有一个客户端可以持有一个锁
  • 释放死锁:即使锁定资源的客户端崩溃或被分区,也可以释放锁
  • 容错性:只要大多数Redis节点都在运行,客户端就能够获取和释放锁。
为什么基于故障转移实现的Redis分布式锁还不够用?

官网中举了一个例子:

客户端A获得主服务器上的锁,然后主服务器向从服务器复制数据的过程中崩了,导致数据没有复制到从数据库中,这时会在从服务器中选出来一个升级为主服务器,但新的主服务器中并没有客户端A设置的锁。所以客户端B也可以获取到锁,违背了上面说的互斥性

这就解释为什么需要RedLock算法

RedLock算法

假设有5个完全独立的Redis服务器,多节点Redis实现的RedLock算法如下

  • 获取当前时间戳
  • 客户端尝试在5个实例中按顺序获取锁,在所有实例中使用相同的键名和随机值。当在每个实例中设置锁时,需要将锁的获取时间设置为比锁过期短很多。例如,如果锁自动释放时间为10秒,则锁的获取时间在5-50毫秒。这是为了不要过长时间等待已经关闭的Redis实例,如果一个Redis实例不可用,我们应该尽快尝试获取下一个Redis实例的锁。
  • 客户端通过从当前时间中减去步骤1中获得的时间戳,计算出获取锁所需的时间。当且仅当客户端能够在大多数实例(至少3个)中获得锁,并且花费在获取锁的总时间小于锁的有效性时间时,该锁被认为已经获得。
  • 如果获得了锁,锁真正的有效时间为锁初始设置的有效时间(过期时间)减去第三步的时间,例如,锁初始有限时间为5s,获取锁花了0.5s,则锁真正的有效时间为4.5s(忽略了时钟漂移,时间漂移指指两个电脑间时间流速基本相同的情况下,两个电脑(或两个进程间)时间的差值)
  • 如果客户端由于某些原因无法获得锁(要么无法锁定N/2+1个Redis实例,要么有锁的效性时间为负数),客户端将尝试解锁所有Redis实例(即使是它认为无法锁定的Redis实例)。
RedLock算法是异步的吗?

可以看成同步算法,虽然没有跨进程的同步时钟,但每个进程(多个电脑)的本地时间仍然大致以相同的速度流动,与锁的自动释放时间相比,误差较小,将其忽略的话,则可以看成同步算法。

RedLock失败重试

当客户端无法获取到锁时,应该在随机时间后重试,并且理想的客户端应该并发地将所有命令用时发给所有Redis实例。对于已经获取锁的客户端要在完成任务后及时释放锁,这样其他客户端就不需要等锁自动过期后在获取。如果在获取锁后,在主动释放锁前无法连接到Redis实例,就只能等锁自动失效了。

释放锁

释放锁很简单,只要释放所有实例中的锁,不需要考虑是否释放成功(释放时会判断这个锁的value值是不是自己设置的,避免释放其他客户端设置的锁)

RedLock的 Safety arguments
  • 假设客户端可以获取到大多数Redis实例,并且所有Redis实例具有相同的key和过期时间,但不同的Redis实例的key是不同的时间设置的(获取锁的时间不可能完全一致),所以过期时间也不同,假设获取第一个Redis实例的锁的时间为T1,最后一个为T2,则客户端获得锁的最小有效时间为key的有效时间-(T2-T1)-时钟漂移。
  • 为什么需要获取一半以上的Redis实例的锁才算获取到锁成功呢?因为如果获取不到一半也算成功的话会导致多个客户端同时获取到锁,违背了互斥性
  • 一个客户端锁定大多数Redis实例所需的时间大于或者接近锁的过期时间时,会认为锁无效,并解锁所有Redis实例
RedLock崩溃的相关解决方法

场景:客户端A在成功获取锁后,如果所有Redis重启,这时客户端B就可以再次获取到锁,违背了互斥性

解决方法:开启AOF持久化,可以解决这个问题,但是AOF同步到磁盘上的方式默认是每秒一次,如果1秒内断电,会导致1秒内的数据丢失,如果客户端是在这1秒内获得的锁,立即重启可能会导致锁的互斥性失效,解决方法是每次Redis无论因为什么原因停掉都要等key的过期时间到了在重启(延迟重启),这么做的缺点就是在等待重启这段时间内Redis处于关闭的状态。

最后,上面的方案6还有其他问题吗?其实还是有的,不过还有一种更适合Java的强大方案是Redisson,有兴趣的小伙伴可以去了解下

Redis并发竞争key问题应该如何解决?

Redis并发竞争key就是多个客户端操作一个key,可能会导致数据出现问题,主要有以下几种解决办法:

  • 乐观锁watch 命令可以方便的实现乐观锁。watch 命令会监视给定的每一个key,当 exec 时如果监视的任一个key自从调用watch后发生过变化,则整个事务会回滚,不执行任何动作。不能在分片集群中使用
  • 分布式锁,适合分布式场景
  • 时间戳,适合有序场景,比如A想把key设置为1,B想把key设置为2,C想把key设置为3,对每个操作加上时间戳,写入前先比较自己的时间戳是不是早于现有记录的时间戳,如果早于,就不写入了
  • 消息队列,串行化处理

什么是RedLock

见上文Redis实现分布式锁的方案6

Redis的缓存问题

说下什么是缓存雪崩、缓存穿透、缓存击穿,及它们的解决方案

这是一个非常高频的面试题,也非常容易掌握,比较麻烦的是总是分不清这三个哪个是哪个

下图是一个正常的系统架构图,其中缓存的作用是减轻数据库的压力,提升系统的性能,无论是缓存雪崩、缓存击穿还是缓存穿透都是缓存失效了导致数据库压力过大。

缓存雪崩

什么是缓存雪崩?

缓存雪崩是指在某一个时刻出现大规模的缓存失效的情况,大量的请求直接打在数据库上面,可能会导致数据库宕机,如果这时重启数据库并不能解决根本问题,会再次造成缓存雪崩。

为什么会造成缓存雪崩?

一般来说,造成缓存雪崩主要有两种可能

  • Redis宕机了
  • 很多key采取了相同的过期时间
如何解决缓存雪崩?
  • 为避免Redis宕机造成缓存雪崩,可以搭建Redis集群
  • 尽量不要设置相同的过期时间,例如可以在原有的过期时间加上随机数
  • 服务降级,当流量到达一定的阈值时,就直接返回“系统繁忙”之类的提示,防止过多的请求打在数据库上,这样虽然难用,但至少可以使用,避免直接把数据库搞挂

缓存击穿

什么是缓存击穿?

缓存雪崩是大规模的key失效,而缓存击穿是一个热点的Key,有大并发集中对其进行访问,突然间这个Key失效了,导致大并发全部打在数据库上,导致数据库压力剧增,这种现象就叫做缓存击穿。

比较经典的例子是商品秒杀时,大量的用户在抢某个商品时,商品的key突然过期失效了,所有请求都到数据库上了。

如何解决缓存击穿
  • 虑热点key不设置过期时间,避免key过期失效
  • 加锁,如果缓存失效的情况,只有拿到锁才可以查询数据库,降低了在同一时刻打在数据库上的请求,防止数据库宕机,不过这样会导致系统的性能变差。

缓存穿透

什么是缓存穿透

缓存穿透是指用户的请求没有经过缓存而直接请求到数据库上了,比如用户请求的key在Redis中不存在,或者用户恶意伪造大量不存在的key进行请求,都可以绕过缓存,导致数据库压力太大挂掉。

如何解决缓存穿透
  • 参数校验,例如可以对用户id进行校验,直接拦截不合法的用户的请求
  • 布隆过滤器,布隆过滤器可以判断这个key在不在数据库中,特点是如果判断这个key不在数据库中,那么这个key一定不在数据库中,如果判断这个key在数据库中,也不能保证这个key一定在数据库中。就是会有少数的漏网之鱼,造成这种现象的原因是因为布隆过滤器中使用了hash算法,对key进行hash时,不同的key的hash值一定不同,但相同的hash的值不能说明这两个key相同。下面简单介绍下布隆过滤器,这个面试也常问。

布隆过滤器底层使用bit数组存储数据,该数组中的元素默认值是0。

布隆过滤器第一次初始化的时候,会把数据库中所有已存在的key,经过一系列的hash算法计算,算出每个key的位置,并将该位置的值置为1,为了减少哈希冲突的影响,可以对每个key进行多次hash计算,如下图

现在,用户所有的请求都要经过布隆过滤器过滤一遍,如果只有用户请求的key的hash值都是1才可以通过,否则直接拦截,如下图

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

这是面试的高频题,需要好好掌握,这个问题是没有最优解的,只能数据一致性和性能之间找到一个最适合业务的平衡点

首先先来了解下一致性,在分布式系统中,一致性是指多副本问题中的数据一致性。一致性可以分为强一致性、弱一致性和最终一致性

  • 强一致性:当更新操作完成之后,任何多个后续进程或者线程的访问都会返回最新的更新过的值。强一致性对用户比较友好,但对系统性能影响比较大。
  • 弱一致性:系统并不保证后续进程或者线程的访问都会返回最新的更新过的值。
  • 最终一致性:也是弱一致性的一种特殊形式,系统保证在没有后续更新的前提下,系统最终返回上一次更新操作的值。

大多数系统都是采用的最终一致性,最终一致性是指系统中所有的副本经过一段时间的异步同步之后,最终能够达到一个一致性的状态,也就是说在数据的一致性上存在一个短暂的延迟。

如果想保证缓存和数据库的数据一致性,最简单的想法就是同时更新数据库和缓存,但是这实现起来并不现实,常见的方案主要有以下几种:

  • 先更新数据库,后更新缓存
  • 先更新缓存,后更新数据库
  • 先更新数据库,后删除缓存
  • 先删除缓存,后更新数据库

乍一看,感觉第一种方案就可以实现缓存和数据库一致性,其实不然,更新缓存是个坑,一般不会有更新缓存的操作。因为很多时候缓存中存的值不是直接从数据库直接取出来放到缓存中的,而是经过一系列计算得到的缓存值,如果数据库写操作频繁,缓存也会频繁更改,所以更新缓存代价是比较大的,并且更改后的缓存也不一定会被访问就又要重新更改了,这样做无意义的性能消耗太大了。下面介绍删除缓存的方案

先更新数据库,后删除缓存

这种方案也存在一个问题,如果更新数据库成功了,删除缓存时没有成功,那么后面每次读取缓存时都是错误的数据。

解决这个问题的办法是删除重试机制,常见的方案有利用消息队列和数据库的日志

利用消息队列实现删除重试机制,如下图

步骤在图中写的已经比较清除了,这里简单说下为什么使用消息队列,消息队列可以保证写到队列中的消息在成功消费之前不会消失,并且在第4步中获取消息时只有消费成功才会删除消息,否则会继续投递消息给应用程序,符合消息重试的要求。

但这个方案也有一些缺点,比如系统复杂度高,对业务代码入侵严重,这时可以采用订阅数据库日志的方法删除缓存。如下图

先删除缓存,后更新数据库

这种方案也存在一些问题,比如在并发环境下,有两个请求A和B,A是更新操作,B是查询操作

  1. 假设A请求先执行,会先删除缓存中的数据,然后去更新数据库
  2. B请求查询缓存发现为空,会去查询数据库,并把这个值放到缓存中
  3. 在B查询数据库时A还没有完全更新成功,所以B查询并放到缓存中的是旧的值,并且以后每次查询缓存中的值都是错误的旧值

这种情况的解决方法通常是采用延迟双删,就是为保证A操作已经完成,最后再删除一次缓存

逻辑很简单,删除缓存后,休眠一会儿再删除一次缓存,虽然逻辑看起来简单,但实现起来并不容易,问题就出在延迟时间设置多少合适,延迟时间一般大于B操作读取数据库+写入缓存的时间,这个只能是估算,一般可以考虑读业务逻辑数据的耗时 + 几百毫秒。

在实际应用中,还是先更新数据库后删除缓存这种方案用的多些。

需要注意的是,无论哪种方案,如果数据库采取读写分离+主从复制延迟的话,即使采用先更新数据库后删除缓存也会出现类似先删除缓存后更新数据库中出现的问题,举个例子

  1. A操作更新主库后,删除了缓存
  2. B操作查询缓存没有查到数据,查询从库拿到旧值
  3. 主库将新值同步到从库
  4. B操作将拿到的旧值写入缓存

这就造成了缓存中的是旧值,数据库中的是新值,解决方法还是上面说的延迟双删,延迟时间要大于主从复制的时间

Redis其他高频面试题

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

一个字符串类型键允许存储的数据的最大容量是512MB

Redis如何实现大量数据插入?

这个问题在Redis的官方文档给出了答案,从Redis 2.6开始redis-cli支持一种新的被称之为pipe mode的新模式用于执行大量数据插入工作。具体可以看官网的详细解释,这里就不再复制粘贴了:https://www.redis.com.cn/topics/mass-insert.html

如何通过Redis实现异步队列?

主要有两种方式

第一种是使用List作为队列,通过RPUSH生产消息, LPOP消费消息

存在的问题:如果队列是空的,客户端会不停的pop,陷入死循环

解决方法

  • 当lpop没有消息时,可以使用sleep机制先休眠一段时间,然后再检查有没有消息。
  • 可以使用blpop命令,在队列没有数据的时候,会立即进入休眠状态,一旦数据到来,则立刻醒过来。这种做法的缺点是只能提供一个消费者消费

第二种方法是pub/sub主题订阅模式,发送者(pub)发送消息,订阅者(sub)接收消息

存在的问题:消息的发布是无状态的,无法保证到达,如果订阅者在发送者发布消息时掉线,之后上线也无法接收发布者发送的消息

解决方法:使用消息队列

如何通过Redis实现延时队列?

先说下延时队列的使用场景:

  • 常见的微信红包场景,A给B发红包,B没有收,1天后钱会退回原账户
  • 电商的订单支付场景,订单在半小时内未支付会自动取消

上述场景可以通过定时任务采用数据库/非关系型数据库轮询方案或延迟队列,现主要介绍下Redis实现的延迟队列

可以通过Redis的zset命令实现延迟队列,ZSET是Redis的有序集合,通过zadd score1 value1命令向内存中生产消息,并利用设置好的时间戳作为score进行排序,然后通过zrangebysocre 查询符合条件的所有待处理的任务,循环执行,也可以zrangebyscore key min max withscores limit 0 1 查询最早的一条任务,来进行消费,如下图(画的第二种,好画点)

Redis回收使用什么算法?

LRU算法引用计数法

LRU算法很常见,在学习操作系统时也经常看到,淘汰最长时间没有被使用的对象,LRU算法在手撕代码环节也经常出现,要提前背熟

引用计数法在学习JVM中也见过的,对于创建的每一个对象都有一个与之关联的计数器,这个计数器记录着该对象被使用的次数,当对象被一个新程序使用时,它的引用计数值会被增1,当对象不再被一个程序使用时,它的引用计数值会被减1,垃圾收集器在进行垃圾回收时,对扫描到的每一个对象判断一下计数器是否等于0,若等于0,就会释放该对象占用的内存空间,简单来说就是淘汰使用次数最少的对象(LFU算法)

Redis 里面有1亿个 key,其中有 10 个 key 是包含 java,如何将它们全部找出来?

可以使用Redis的KEYS命令,用于查找所有匹配给定模式 pattern 的 key ,虽然时间复杂度为O(n),但常量时间相当小。

注意: 生产环境使用 KEYS命令需要非常小心,在大的数据库上执行命令会影响性能,KEYS指令会导致线程阻塞一段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复。这个命令适合用来调试和特殊操作,像改变键空间布局。

不要在你的代码中使用 KEYS 。如果你需要一个寻找键空间中的key子集,考虑使用 SCAN 或 sets。

生产环境中的Redis是如何部署的

这个按自己得情况说就行了,但得提前想好怎么说,避免忘了

本文由哈喽比特于2年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/CQhc3QcUzLtgjZ0SxzR-Jg

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:1年以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:1年以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:1年以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:1年以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:1年以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:1年以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:1年以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:1年以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:1年以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:1年以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:1年以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:1年以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:1年以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:1年以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:1年以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:1年以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:1年以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:1年以前  |  398次阅读  |  详细内容 »
 相关文章
Android插件化方案 5年以前  |  237267次阅读
vscode超好用的代码书签插件Bookmarks 2年以前  |  8108次阅读
 目录