大家好, 今天分享路人张大佬整理的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是一个高性能的非关系型的键值对数据库,使用C编写实现的。与传统的数据库不同的是Redis是存在内存中的,所以读写速度非常快,每秒可以处理超过10万次的读写操作,这也是Redis常常被用作缓存的原因。
优点:
缺点:
缓存的定义是访问速度比一般随机存取存储器快的一种高速存储器,而因为Redis是基于内存提供了高性能的数据存取功能,其比较显著的优势就是非常地快。
缓存可以分为本地缓存或者分布式缓存,比较常用的guava缓存就是一种本地缓存,其主要特点是轻量并且快速,生命周期随着JVM的销毁而结束,缺点是在多实例的情况下,每个实例都要自己保存一份缓存,这样会导致缓存的一致性出现问题。
Redis则是分布式缓存,在多实例情况下,每个实例都共享一份缓存数据,缓存具备一致性。缺点是要保持Redis的高可用整体架构会比较复杂。
相同点:
不同点:
不同点 | Redis | Memcached |
---|---|---|
是否支持复制 | 支持主从复制 | 不支持复制 |
key长度 | 长度最大为 2GB | 长度最多为 250 个字节 |
数据类型 | 不仅支持key-value类型的数据,还支持hash、list、set、zset等数据等数据类型的数据 | 仅支持key-value类型的数据 |
数据持久化 | 支持数据持久化,可以将数据保存到磁盘 | 不支持数据持久化 |
网络IO模型 | 单线程的多路 IO 复用模型 | 多线程的非阻塞IO模式 |
集群 | 原生支持cluster 模式集群 | 无原生 |
Redis6.0之前是单线程的,为什么Redis6.0之前采用单线程而不采用多线程呢?
简单来说,就是Redis官方认为没必要,单线程的Redis的瓶颈通常在CPU的IO,而在使用Redis时几乎不存在CPU成为瓶颈的情况。使用Redis主要的瓶颈在内存和网络,并且使用单线程也存在一些优点,比如系统的复杂度较低,可为维护性较高,避免了并发读写所带来的一系列问题。
Redis为什么这么快主要有以下几个原因:
前面说了那么多Redis使用单线程的原因,但从Redis6.0后开始支持多线程了,简直打脸有点快。那么为什么较新的Redis版本又开始支持多线程了呢?
前面也说了Redis的瓶颈在内存和网络,Redis6.0引入多线程主要是为了解决网路IO读写这个瓶颈,执行命令还是单线程执行的,所以也不存在线程安全问题。
Redis6.0默认是否开启了多线程呢?
默认是没有开启的,如需开启,需要修改配置文件redis.conf:io-threads-do-reads no,no改为yes
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的底层是用C语言编写,但Redis并没有直接使用C语言传统的字符串表示,而是构建了一种名为简单动态字符串的抽象类型。
链表:链表提供了高效的节点重排能力,以及顺序性的节点访问方式,并且可以通过增删节点来灵活地调整链表的长度。链表是列表的底层实现之一。
字典:字典,又称为符号表(symbol table)、关联数组(associativearray)或映射(map),是一种用于保存键值对(key-value pair)的抽象数据结构。字典在Redis中的应用相当广泛,比如Redis的数据库就是使用字典来作为底层实现的,对数据库的增、删、查、改操作也是构建在对字典的操作之上的。
整数集合: 整数集合(intset)是集合键的底层实现之一,当一个集合只包含整数值元素,并且这个集合的元素数量不多时,Redis就会使用整数集合作为集合键的底层实现。
压缩列表(ziplist):压缩列表是Redis为了节约内存而开发的,是由一系列特殊编码的连续内存块组成的顺序型(sequential)数据结构。
对象:可能看到这里,很多人在想Redis的数据结构和数据类型的区别,其实上面介绍的是Redis的底层数据结构,但Redis并没有直接使用这些数据结构来实现键值对数据库,而是基于这些数据结构创建了一个对象系统,这个系统包含字符串对象、列表对象、哈希对象、集合对象和有序集合对象这五种类型的对象,每种对象都用到了至少一种我们前面所介绍的数据结构,是不是这就和前面对上了。
看到这里很多人会好奇,为什么不直接使用这些底层数据结构,而是要创建对象系统。对象系统主要有以下优点:
对象这部分占了比较大的篇幅,其实面试中问的也不多,但为了更方便理解,介绍地多些。顺便看下这些底层数据结构和对象系统的对应关系。
最后介绍下面试中常问的跳跃表。
跳跃表(skiplist):跳跃表(skiplist)是一种有序数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。跳跃表支持平均O(logN)、最坏O(N)复杂度的节点查找,还可以通过顺序性操作来批量处理节点。跳跃表作是序集合键的底层实现之一。
和链表、字典等数据结构被广泛地应用在Redis内部不同,Redis只在两个地方用到了跳跃表,一个是实现有序集合键,另一个是在集群节**点中用作内部数据结构**,除此之外,跳跃表在Redis里面没有其他用途。
跳跃表本质上采用的是一种空间换时间的策略,是一种可以可以进行二分查找的有序链表,跳表在原有的有序链表上增加了多级索引,通过索引来实现快速查询。跳表不仅能提高搜索性能,同时也可以提高插入和删除操作的性能。
这是一个原始的有序列表,时间复杂度为O(n)。
为了提高查找效率,可以对链表建立一级索引,如下图,在之前找到11这个元素需要遍历6个节点,现在需要5个。链表越长,效率提升越明显。
为了继续提高查找效率可以继续增加索引
对于理想的跳表,每向上一层索引节点数量都是下一层的1/2,跳表的**时间复杂度为o(logn),空间复杂度为o(n)**,虽然是空间换时间的策略,这里举例存储的只是数字,如果是存储比较大的对象,浪费的空间就不值得一提了,因为索引结点只需要存储关键值和几个指针,并不需要存储对象。
跳表相比于红黑树的优点(redis为什么用跳表不同红黑树):
上面这三个优点是我在一篇博客中看到,这个问题redis作者本人也回应过。我这蹩脚的英文水平就不翻译了,以免跑偏了。
最后,说下Redis中的跳跃表和普通的跳跃表有什么区别?
篇幅有限,关于跳表的实现细节就不过多介绍了,有兴趣的同学可以自行了解,本小节部分内容参考《Redis设计与实现》
可以在一个服务器上部署多个Redis实例,把他们当作不同的服务器使用。
常见的过期删除策略是惰性删除、定期删除、定时删除。
Redis中同时使用了惰性删除和定期删除两种。
Redis是基于内存的,所以容量肯定是有限的,有效的内存淘汰机制对Redis是非常重要的。
当存入的数据超过Redis最大允许内存后,会触发Redis的内存淘汰策略。在Redis4.0前一共有6种淘汰策略。
前三个是在设置了过期时间的键的空间进行移除,后三个是在全局的空间进行移除
在Redis4.0后可以增加两个
这两个也是一个是在设置了过期时间的键的空间,一个是在全局空间。
因为Redis是基于内存的,为了让防止一些意外情况导致数据丢失,需要将数据持久化到磁盘上。
Redis提供了两种不同的持久化方式,一种是RDB,一种是AOF。
RDB
RDB是redis默认的持久化方式,按照一定的时间间隔将内存的数据以快照的形式保存到硬盘,恢复时是将快照读取到内存中。RDB持久化实际操作过程是fork一个子进程,先将数据集写入临时文件,写入成功后,再替换之前的文件,用二进制压缩存储。如下图
优点:
缺点:
AOF
AOF持久化以日志的形式记录服务器所处理的每一个写、删除操作,查询操作不会记录,以文本的方式记录,可以打开文件看到详细的操作记录。
优点:
缺点:
Redis的事务是一个单独的隔离操作,事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断,所以Redis事务是在一个队列中,一次性、顺序性、排他性地执行一系列命令。
Redis 事务的主要作用就是串联多个命令防止别的命令插队。
Redis事务相关的命令主要有以下几种:
在Redis的事务中,命令允许失败,但是Redis会继续执行其它的命令而不是回滚所有命令,是不支持回滚的。
主要原因有以下两点:
Redis 命令只在两种情况失败:
语法错误的时候才失败(在命令输入的时候不检查语法)。
要执行的key数据类型不匹配:这种错误实际上是编程错误,这应该在开发阶段被测试出来,而不是生产上。
因为不需要回滚,所以Redis内部实现简单并高效。(在Redis为什么是单线程而不是多线程也用了这个思想,实现简单并且高效)
在说Redis集群前,先说下为什么要使用Redis集群,Redis单机版主要有以下几个缺点:
Redis集群就是为了解决Redis单机版的一些问题,Redis集群主要有以下几种方案
下面对这几种方案进行简单地介绍:
Redis单机版通过RDB或AOF持久化机制将数据持久化到硬盘上,但数据都存储在一台服务器上,并且读写都在同一服务器(读写不分离),如果硬盘出现问题,则会导致数据不可用,为了避免这种问题,Redis提供了复制功能,在master数据库中的数据更新后,自动将更新的数据同步到slave数据库上,这就是主从模式的Redis集群,如下图
主从模式解决了Redis单机版存在的问题,但其本身也不是完美的,主要优缺点如下:
优点:
缺点:
为了解决主从模式的Redis集群不具备自动容错和恢复能力的问题,Redis从2.6版本开始提供哨兵模式
哨兵模式的核心还是主从复制,不过相比于主从模式,多了一个竞选机制(多了一个哨兵集群),从所有从节点中竞选出主节点,如下图
从上图中可以看出,哨兵模式相比于主从模式,主要多了一个哨兵集群,哨兵集群的主要作用如下:
哨兵模式的优缺点:
优点:
缺点:
哨兵模式虽然解决了主从模式存在的一些问题,但其本身也存在一些弊端,比如数据在每个Redis实例中都是全量存储,极大地浪费了资源,为了解决这个问题,Redis提供了Redis Cluster,实现了数据分片存储,但Redis提供Redis Cluster之前,很多公司为了解决哨兵模式存在的问题,分别自行研发Redis集群方案。
客户端分片是把分片的逻辑放在Redis客户端实现,通过Redis客户端预先定义好的路由规则(使用哈希算法),把对Key的访问转发到不同的Redis实例中,查询数据时把返回结果汇集。如下图
客户端分片的优缺点:
优点:Redis实例彼此独立,相互无关联,每个Redis实例像单服务器一样运行,非常容易线性扩展,系统的灵活性很强。
缺点:
客户端分片的最大问题就是服务端Redis实例群拓扑结构有变化时,每个客户端都需要更新调整。
为了解决这个问题,代理分片出现了,代理分片将客户端分片模块单独分了出来,作为Redis客户端和服务端的桥梁,如下图
代理模式的优点:解决了服务端Redis实例群拓扑结构有变化时,每个客户端都需要更新调整的问题。缺点是由于Redis客户端的每个请求都经过代理才能到达Redis服务器,这个过程中会产生性能损失。
常见的代理分片有Twitter开源的Redis代理Twemproxy和豌豆荚自主研发的Codis
前面介绍了为了解决哨兵模式的问题,各大企业提出了一些数据分片存储的方案,在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 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是用于节点之间的信息交路,交互的信息有以下几个:
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中写入数据。
这种解决数据丢失的方法是降低系统的可用性来实现的。
主从复制主要有以下几步:
简单解释下全量复制和部分复制:
在Redis2.8以前,从节点向主节点发送sync命令请求同步数据,此时的同步方式是全量复制;在Redis2.8及以后,从节点可以发送psync命令请求同步数据,此时根据主从节点当前状态的不同,同步方式可能是全量复制或部分复制。
命令传播阶段,从服务器会利用心跳检测机制定时的向主服务发送消息。
如果出现网络问题断开,会自动重连,并且支持断点续传,接着上次复制的地方继续复制,而不是重新复制一份。
下面说下其中的实现细节,首先需要了解replication buffer和replication 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_offset
和 slave_repl_offset
之间的操作命令同步给从库就可以了。
但是前面提到 replication backlog 是一个环形结构,如果网络中断的时间过长,随着主库不断向其中写入操作命令,master_repl_offset
不断增大,就会有从库没有复制的操作命令被覆盖。如果出现这种情况,就需要重新进行全量复制了。
为了避免全量复制的情况,可以通过修改 repl-backlog-size
参数的值,为 replication backlog 设置合适的大小。
这个值需要结合实际情况来设置,具体思路是:从命令在主库中产生到从库复制完成所需要的时间为 t,每秒钟产生的命令的数量为 c,命令的大小为 s,这个值不能低于他们的乘积。考虑到突发的网络压力以及系统运行过程中可能出现的阻塞等情况,应该再将这个值乘以 2 或者更多。
此处参考博客:https://juejin.cn/post/7017827613544546335
见Redis Cluster处
16384个
Redis作者的回答:
上面主要说了两点,
在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 集群目前无法做数据库选择,默认在 0 数据库。
常见的Redis高可用方案有以下几种:
在介绍Redis集群的实现方案时已经介绍过了客户端分区和代理分区,常见的Redis分区方案主要有以下三种:
相信大家对程序中的锁并不陌生,无论是在并发编程或者Java虚拟机都有学到过。
锁在程序中的作用主要是同步,就是保证共享资源在同一时刻只能被同一个线程访问。
分布式锁则是为了保证在分布式场景下,共享资源在同一时刻只能被同一个线程访问,或者说是用来控制分布式系统之间同步访问共享资源。举个简单例子,如下图
从上图可以看出,变量A在三个服务器中都有涉及到,如果不对其进行控制的话,三个服务器中的变量A很难做到同步,解决这个问题的方法就是分布式锁。
基本思路就是要在整个系统中提供一个全局、唯一的获取锁的“东西”,然后每个系统在需要加锁时,都去问这个“东西”拿到一把锁,这样不同的系统拿到的就可以认为是同一把锁。
常见的分布式锁实现方案有三种:
基于关系型数据库:
优点:直接借助数据库容易理解
缺点:在使用关系型数据库实现分布式锁的过程中会出现各种问题,例如数据库单点问题和可重入问题,并且在解决过程中会使得整个方案越来越复杂
基于Redis:
优点:性能好,实现起来较为方便
缺点:
基于zookeeper:
优点:有效地解决单点问题,不可重入问题,非阻塞问题以及锁无法释放的问题,实现起来较为简单。
缺点:性能上不如使用缓存实现分布式锁
三种方案的对比
方案 | 复杂度 | 性能 | 可靠性 | 学习成本 |
---|---|---|---|---|
基于关系型数据库 | 低 | 低 | 低 | 低 |
基于Redis | 中 | 高 | 中 | 中 |
基于zookeeper | 高 | 中 | 高 | 高 |
前面讲过了分布式锁的特性,其实实现分布式锁就是围绕着这些展开的
Redis实现分布式锁的主要命令:SETNX,该命令的作用是当key不存在时设置key的值,当Key存在时,什么都不做。
先来看最简单的实现方式,如下图
从上图可以看到主要两个关键步骤,加锁和解锁。
但是这个简陋的分布式锁存在很多问题,并不能满足上述介绍的分布式锁的特性,
比如,当线程1执行到上图中执行业务这步时,业务代码突然出现异常了,无法进行删除锁这一步,那就完犊子了,死锁了,其他线程也无法获取到锁了(因为SETNX的特性)。
那咋整呢?
一提到异常,有人想起了try-catch-finally了,把删除锁的操作放到finally代码块中,就算出现异常,也是能正常释放锁的,执行业务出现异常这个问题确实解决了。但这玩意并不靠谱,如果Redis在执行业务这步宕机了呢,finally代码块也不会执行了。
其实这个问题很好解决,只需给锁设置一个过期时间就可以了,对key设置过期时间在Redis中是常规操作了。就是这个命令SET key value [EX seconds][PX milliseconds] [NX|XX]
那先现在这个方案就完美了吗?显然没有
例如,线程1获取到了锁,并设置了有效时间10秒,但线程1在执行业务时超过了10秒,锁到期自动释放了,在锁释放后,线程2又获取了锁,在线程2执行业务时,线程1执行完了,随后执行了删除锁这一步,但是线程1的锁早就到期自动释放了,他删除的是线程2的锁!!!
上面这个例子说的有点乱,突然想到一个现实生活中的例子,把Redis比作一个厕所,张三去上厕所,关上了门(获取锁,并设置了10秒),上到一半(10秒到了,门自动开了),这时李四进去了,关上了门(获取锁,并设置了10秒),张三上完了厕所,把门打开了走了(执行了删除锁操作)
上面这个荒诞的例子说明了方案2有两处不合理的地方,第一是张三厕所没上完,李四怎么能进去呢?他们上厕所产生了冲突,第二是张三上完厕所怎么能打开李四的门呢(分布式锁的特性,加锁和解锁必须同一台服务器执行)?
其实看起来方案2的问题很容易解决,只需要把锁的过期时间设置的非常长,就可以避免这两个问题,但是这样并不可行,因为这样相当于回到最简陋的方案(会导致李四一直上不到厕所)。
那如何能让李四上到厕所,还不会让自己锁的门被张三打开门呢?
很简单,为锁加一个标识,例如生成一个UUID,作为锁的标识,每个线程获取锁时都会生成一个不同的UUID作为锁的标识,在进行删除锁时会进行判断,锁的标识和自己生成UUID相等时才进行删除操作,这样就避免线程1释放了线程2的锁。(相当于自己上自己的锁,不要计较为什么张三在李四上厕所时不需要李四的钥匙就能离开厕所这种事,上厕所和分布式锁逻辑并不完全相同,只是简单类比)
那怎么解决李四未等张三上完厕所就进厕所呢?(如何确定锁的过期时间)
可以在加锁时,先设置一个预估的过期时间,然后开启一个守护线程,定时去检测这个锁的失效时间,如果锁快要过期了,操作共享资源还未完成,那么就自动对锁进行续期,重新设置过期时间。
好了,张三和李四上厕所的解决了。
那方案3就没有其他问题了吗?其实还是有的,比如目前的分布式锁还不具备可重入性(同一线程可以重复获取锁,解决线程需要多次进入锁内执行任务的问题)
参考其他重入锁的实现,可以通过对锁进行重入计数,加锁时加 1,解锁时减 1,当计数归 0 时才能释放锁。
那现在方案就没有问题了吗,其实还有
比如,线程1获取了锁,线程2没有获取到锁,那么线程2怎么知道线程1啥时候释放了锁,进而再去获取锁呢?
方案4中问题的解决方案,一般以下两种解决方案:
那现在这个方案完美了吗?也还没有
目前讨论的都是redis是单节点的情况,如果这个节点挂了,那么所有的客户端都获取不到锁了
为了实现多节点Redis的分布式锁,Redis的作者提出了RedLock算法。
这是RedLock算法官网的地址,https://redis.io/topics/distlock,英文好的建议直接看官方文档,毕竟我这四六级飘过的人也可能理解的不准确,下面的内容主要是参考官网内容。
官网中举了一个例子:
客户端A获得主服务器上的锁,然后主服务器向从服务器复制数据的过程中崩了,导致数据没有复制到从数据库中,这时会在从服务器中选出来一个升级为主服务器,但新的主服务器中并没有客户端A设置的锁。所以客户端B也可以获取到锁,违背了上面说的互斥性
这就解释为什么需要RedLock算法
假设有5个完全独立的Redis服务器,多节点Redis实现的RedLock算法如下
可以看成同步算法,虽然没有跨进程的同步时钟,但每个进程(多个电脑)的本地时间仍然大致以相同的速度流动,与锁的自动释放时间相比,误差较小,将其忽略的话,则可以看成同步算法。
当客户端无法获取到锁时,应该在随机时间后重试,并且理想的客户端应该并发地将所有命令用时发给所有Redis实例。对于已经获取锁的客户端要在完成任务后及时释放锁,这样其他客户端就不需要等锁自动过期后在获取。如果在获取锁后,在主动释放锁前无法连接到Redis实例,就只能等锁自动失效了。
释放锁很简单,只要释放所有实例中的锁,不需要考虑是否释放成功(释放时会判断这个锁的value值是不是自己设置的,避免释放其他客户端设置的锁)
场景:客户端A在成功获取锁后,如果所有Redis重启,这时客户端B就可以再次获取到锁,违背了互斥性
解决方法:开启AOF持久化,可以解决这个问题,但是AOF同步到磁盘上的方式默认是每秒一次,如果1秒内断电,会导致1秒内的数据丢失,如果客户端是在这1秒内获得的锁,立即重启可能会导致锁的互斥性失效,解决方法是每次Redis无论因为什么原因停掉都要等key的过期时间到了在重启(延迟重启),这么做的缺点就是在等待重启这段时间内Redis处于关闭的状态。
最后,上面的方案6还有其他问题吗?其实还是有的,不过还有一种更适合Java的强大方案是Redisson,有兴趣的小伙伴可以去了解下
Redis并发竞争key就是多个客户端操作一个key,可能会导致数据出现问题,主要有以下几种解决办法:
watch
命令可以方便的实现乐观锁。watch
命令会监视给定的每一个key,当 exec
时如果监视的任一个key自从调用watch后发生过变化,则整个事务会回滚,不执行任何动作。不能在分片集群中使用见上文Redis实现分布式锁的方案6
这是一个非常高频的面试题,也非常容易掌握,比较麻烦的是总是分不清这三个哪个是哪个
下图是一个正常的系统架构图,其中缓存的作用是减轻数据库的压力,提升系统的性能,无论是缓存雪崩、缓存击穿还是缓存穿透都是缓存失效了导致数据库压力过大。
缓存雪崩是指在某一个时刻出现大规模的缓存失效的情况,大量的请求直接打在数据库上面,可能会导致数据库宕机,如果这时重启数据库并不能解决根本问题,会再次造成缓存雪崩。
一般来说,造成缓存雪崩主要有两种可能
缓存雪崩是大规模的key失效,而缓存击穿是一个热点的Key,有大并发集中对其进行访问,突然间这个Key失效了,导致大并发全部打在数据库上,导致数据库压力剧增,这种现象就叫做缓存击穿。
比较经典的例子是商品秒杀时,大量的用户在抢某个商品时,商品的key突然过期失效了,所有请求都到数据库上了。
缓存穿透是指用户的请求没有经过缓存而直接请求到数据库上了,比如用户请求的key在Redis中不存在,或者用户恶意伪造大量不存在的key进行请求,都可以绕过缓存,导致数据库压力太大挂掉。
布隆过滤器底层使用bit数组存储数据,该数组中的元素默认值是0。
布隆过滤器第一次初始化的时候,会把数据库中所有已存在的key,经过一系列的hash算法计算,算出每个key的位置,并将该位置的值置为1,为了减少哈希冲突的影响,可以对每个key进行多次hash计算,如下图
现在,用户所有的请求都要经过布隆过滤器过滤一遍,如果只有用户请求的key的hash值都是1才可以通过,否则直接拦截,如下图
这是面试的高频题,需要好好掌握,这个问题是没有最优解的,只能数据一致性和性能之间找到一个最适合业务的平衡点
首先先来了解下一致性,在分布式系统中,一致性是指多副本问题中的数据一致性。一致性可以分为强一致性、弱一致性和最终一致性
大多数系统都是采用的最终一致性,最终一致性是指系统中所有的副本经过一段时间的异步同步之后,最终能够达到一个一致性的状态,也就是说在数据的一致性上存在一个短暂的延迟。
如果想保证缓存和数据库的数据一致性,最简单的想法就是同时更新数据库和缓存,但是这实现起来并不现实,常见的方案主要有以下几种:
乍一看,感觉第一种方案就可以实现缓存和数据库一致性,其实不然,更新缓存是个坑,一般不会有更新缓存的操作。因为很多时候缓存中存的值不是直接从数据库直接取出来放到缓存中的,而是经过一系列计算得到的缓存值,如果数据库写操作频繁,缓存也会频繁更改,所以更新缓存代价是比较大的,并且更改后的缓存也不一定会被访问就又要重新更改了,这样做无意义的性能消耗太大了。下面介绍删除缓存的方案
这种方案也存在一个问题,如果更新数据库成功了,删除缓存时没有成功,那么后面每次读取缓存时都是错误的数据。
解决这个问题的办法是删除重试机制,常见的方案有利用消息队列和数据库的日志
利用消息队列实现删除重试机制,如下图
步骤在图中写的已经比较清除了,这里简单说下为什么使用消息队列,消息队列可以保证写到队列中的消息在成功消费之前不会消失,并且在第4步中获取消息时只有消费成功才会删除消息,否则会继续投递消息给应用程序,符合消息重试的要求。
但这个方案也有一些缺点,比如系统复杂度高,对业务代码入侵严重,这时可以采用订阅数据库日志的方法删除缓存。如下图
这种方案也存在一些问题,比如在并发环境下,有两个请求A和B,A是更新操作,B是查询操作
这种情况的解决方法通常是采用延迟双删,就是为保证A操作已经完成,最后再删除一次缓存
逻辑很简单,删除缓存后,休眠一会儿再删除一次缓存,虽然逻辑看起来简单,但实现起来并不容易,问题就出在延迟时间设置多少合适,延迟时间一般大于B操作读取数据库+写入缓存的时间,这个只能是估算,一般可以考虑读业务逻辑数据的耗时 + 几百毫秒。
在实际应用中,还是先更新数据库后删除缓存这种方案用的多些。
需要注意的是,无论哪种方案,如果数据库采取读写分离+主从复制延迟的话,即使采用先更新数据库后删除缓存也会出现类似先删除缓存后更新数据库中出现的问题,举个例子
这就造成了缓存中的是旧值,数据库中的是新值,解决方法还是上面说的延迟双删,延迟时间要大于主从复制的时间
一个字符串类型键允许存储的数据的最大容量是512MB
这个问题在Redis的官方文档给出了答案,从Redis 2.6开始redis-cli
支持一种新的被称之为pipe mode的新模式用于执行大量数据插入工作。具体可以看官网的详细解释,这里就不再复制粘贴了:https://www.redis.com.cn/topics/mass-insert.html
主要有两种方式
第一种是使用List作为队列,通过RPUSH生产消息, LPOP消费消息
存在的问题:如果队列是空的,客户端会不停的pop,陷入死循环
解决方法:
第二种方法是pub/sub主题订阅模式,发送者(pub)发送消息,订阅者(sub)接收消息
存在的问题:消息的发布是无状态的,无法保证到达,如果订阅者在发送者发布消息时掉线,之后上线也无法接收发布者发送的消息
解决方法:使用消息队列
先说下延时队列的使用场景:
上述场景可以通过定时任务采用数据库/非关系型数据库轮询方案或延迟队列,现主要介绍下Redis实现的延迟队列
可以通过Redis的zset命令实现延迟队列,ZSET是Redis的有序集合,通过zadd score1 value1
命令向内存中生产消息,并利用设置好的时间戳作为score进行排序,然后通过zrangebysocre 查询符合条件的所有待处理的任务,循环执行,也可以zrangebyscore key min max withscores limit 0 1
查询最早的一条任务,来进行消费,如下图(画的第二种,好画点)
LRU算法和引用计数法
LRU算法很常见,在学习操作系统时也经常看到,淘汰最长时间没有被使用的对象,LRU算法在手撕代码环节也经常出现,要提前背熟
引用计数法在学习JVM中也见过的,对于创建的每一个对象都有一个与之关联的计数器,这个计数器记录着该对象被使用的次数,当对象被一个新程序使用时,它的引用计数值会被增1,当对象不再被一个程序使用时,它的引用计数值会被减1,垃圾收集器在进行垃圾回收时,对扫描到的每一个对象判断一下计数器是否等于0,若等于0,就会释放该对象占用的内存空间,简单来说就是淘汰使用次数最少的对象(LFU算法)
可以使用Redis的KEYS命令,用于查找所有匹配给定模式 pattern 的 key ,虽然时间复杂度为O(n),但常量时间相当小。
注意: 生产环境使用 KEYS命令需要非常小心,在大的数据库上执行命令会影响性能,KEYS指令会导致线程阻塞一段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复。这个命令适合用来调试和特殊操作,像改变键空间布局。
不要在你的代码中使用 KEYS 。如果你需要一个寻找键空间中的key子集,考虑使用 SCAN 或 sets。
这个按自己得情况说就行了,但得提前想好怎么说,避免忘了
Copyright© 2013-2020
All Rights Reserved 京ICP备2023019179号-8