Redis
一、基础概念篇
1、介绍一下Redis是什么?
据说有一名意大利程序员,在 2004 年到 2006 年间主要做嵌入式工作,之后接触了 Web,2007 年和朋友共同创建了一个网站,并为了解决这个网站的负载问题(为了避免 MySQL 的低性能),于是亲自定做一个数据库,并于 2009 年开发完成,这个就是 Redis。这个意大利程序员就是 Salvatore Sanfilippo 江湖人称 Redis 之父,大家更习惯称呼他 Antirez。
Redis(Remote Dictionary Server ),即远程字典服务,它的主要特点是使用C语言编写、开源、支持网络访问、可基于内存亦可基于磁盘的持久化、Key-Value类型数据库、提供多种语言的API。Redis属于非关系型数据库中的一种解决方案,目前也是业界主流的缓存解决方案组件。
数百万开发人员在使用Redis用作数据库、缓存、流式处理引擎和消息代理。
2、介绍一下Redis的功能模块架构
网络访问框架
通过网络框架以 Socket 通信的形式对外提供键值对操作,包括socket服务和协议解析。客户端发送命令时,命令会被封装成网络请求传输给redis。
操作模块
主要对各种数据进行操作,如get 、put 、delete 、scan操作等。
索引模块
索引模块主要目的是为了通过key值快速定位value值,从而进行操作。 redis使用的索引模块为哈希表。redis存储内存的高性能随机访问特性可以很好地与哈希表 O(1) 的操作复杂度相匹配。
存储模块
主要完成保存数据的工作,存储数据模型为 key-value形式,value支持丰富的数据类型。包括字符串,列表 ,哈希表,集合等。不同的数据类型能支持不同的业务需求。
其中的持久化模块主要对数据进行持久化,当系统重启时,能够快速恢复服务。redis的持久化策略分为:日志(AOF)和快照(RDB)两种方式,新版本增加了混合模式。
高可用模块
主从复制:主从架构中用到(一个Master至少一个slave),master -> slave 数据复制
哨兵:主从架构实现高可用(一个Master至少一个slave),在master故障的时候,快速将slave切换成master,实现快速的灾难恢复,实现高可用性;
高扩展模块
Redis Cluster(分片集群)是Redis提供的分布式高可扩展解决方案。
切片集群,也叫分片集群(集群分片),就是指启动多个 Redis 实例组成一个集群,然后按照一定的规则,把收到的数据划分成多份,每一份用一个实例来保存。
其它模块
还有一些其他模块,例如:数据压缩、过期机制、数据淘汰策略、主从复制、集群化、高可用等功能,另外还可以增加统计模块、通知模块、调试模块、元数据查询等辅助功能。
3、说说Redis的部署方式
截至目前,Redis支持四种部署架构,分别是单机、主从、哨兵、集群。
单机
单机模式是最原始的模式,非常简单,就是安装运行一个Redis实例,然后业务项目调用即可。
单机有宕机的风险,可用性保证差。一些非常简单的应用,并非必须保证高可用的情况下完全可以使用该模式。
单机 Redis 能够承载的 QPS(每秒查询速率)取决于业务操作的复杂性。假如是简单的 key value 查询那性能就会很高,单机能支持10W+的QPS。如果是Lua 脚本,则性能会差。
主从
我们可以同时部署多个Redis,把同时接收读/写操作的节点称为Master(主节点), 接收读操作和数据同步的节点称为Slave(从节点)。
只要主从节点之间的网络连接正常,主节点就会将写入自己的数据同步更新给从节点,从而保证主从节点的数据一致性。
主从架构比较适合读高并发场景,当QPS增加时,水平扩展从节点即可。
主从架构存在的问题是:高可用性不够,当主节点宕机,需要在众多从节点中选一个作为新的主节点,同时需要修改客户端保存的主节点信息并重启客户端,还需要通知所有的从节点去复制新的主节点数据,从而保证服务的高可用性。整个切换过程需要人工干预,而这个过程很明显会造成服务的短暂不可用。
哨兵
Redis2.8支持了哨兵架构,它主要解决了主从架构中存在的高可用性问题,在主从架构的基础上,哨兵架构实现了自动化故障检测和恢复机制,全过程无需人工干预。
如上图所示,哨兵架构由两部分集群组成,哨兵节点集群和数据节点集群:
哨兵节点集群
该集群中的节点不存储数据,是特殊的redis节点,主要完成监控、提醒、自动故障转移这三大功能。
1)监控(Monitoring):哨兵节点会不断地发送ping消息检测数据节点是否正常;
2)提醒(Notification):当监控到某个数据节点有问题时, 哨兵可以通过 API 向管理员或者其他应用程序发送通知
3)自动故障迁移(Automatic failover):当一个主数据节点不能正常工作时, 哨兵会开始一次自动故障迁移操作,将该主节点下线,选举一个从数据节点升级为主节点(这里也就是将主从架构中的人工干预过程自动化),同时通知客户端更新主从地址信息
数据节点集群
该集群中的节点分为主从模式,都存储业务数据,这块其实就是之前的主从架构模式部分
哨兵模式部署成本高,需要单独维护一套哨兵集群,且它依旧没有解决主数据节点的写压力,主节点写的能力和存储能力依旧受限单机限制。另外,这种模式下的动态扩容变得更加复杂。
集群
Redis 3.0 版本正式推出 Redis Cluster 集群模式,有效地解决了 Redis 分布式方面的需求。Redis Cluster 集群模式具有高可用、可扩展性、分布式、容错等特性。
如上图所示,Redis的集群模式采用的是无中心多节点结构,节点数量至少为 6 个才能保证组成完整高可用的集群,其中三个为主节点,三个为从节点。三个主节点会分配槽,处理客户端的命令请求,而从节点可用在主节点故障后,顶替主节点。
每个节点都可以保存部分数据和整个集群状态,每个节点都和其他所有节点连接(采用 Gossip 协议进行通信,交换维护节点元数据信息),主从节点之间会进行数据复制。
4、说说Redis的基本数据类型有哪些?
redis中支持的数据类型主要分为三大类:五大基本数据类型、三大扩展数据类型、自定义数据类型:
Redis 所有的数据结构都是以唯一的key 字符串作为名称,然后通过这个唯一 key 值来获取相应的 value 数据。不同类型的数据结构的差异就在于 value 的结构不一样。
5、Redis为什么这么快?
从几个方面展开回答:
基于纯内存操作
CPU不是 Redis性能瓶颈,,Redis的瓶颈是机器内存和网络带宽。
使用IO多路复用模型,非阻塞IO
一句话描述 IO 多路复用在 Redis 中的应用:Redis 将所有产生事件的套接字都放到一个队列里面,以有序、同步、每次一个套接字的方式向文件事件分派器传送套接字,文件事件分派器根据套接字对应的事件选择响应的处理器进行处理,从而实现了高效的网络请求。
高效的数据结构,对数据的操作比较简单
6、介绍Redis的内存清理策略
redis对于已过期的key,有下面两种清理策略:
定期删除
redis 会将每个设置了过期时间的 key 放入到一个独立的字典中,以后会从字典中根据某种策略抽取一些key检查是否到期,已到期就删除。常用策略有no-envicition、allkeys-random、allkeys-lru、volatile-random、volatile-ttl、volatile-lru、volatile-lfu、allkeys-lfu
算法
惰性删除
惰性策略就是在客户端访问这个key的时候,redis对key的过期时间进行检查,如果过期了就立即删除。
7、说说Redis的持久化机制
Redis 提供了两种持久化的方式,分别是RDB(Redis DataBase)和AOF(Append Only File)。
RDB:定期将 redis 某一时刻的数据持久化到磁盘中,是一种快照式的持久化方法。
AOF:将 redis 执行过的所有写指令记录到文件中,在下次 redis 重新启动时,只要把这些写指令按先后顺序再重复执行一遍,就可以实现数据恢复了。
官方的建议是两种持久化方式同时使用,这样可以提供更可靠的持久化方案。在这种情况下,如果 redis 重启的话,则会优先采用 AOF 方式来进行数据恢复,这是因为 AOF 方式的数据恢复完整度更高。
Redis 4.0 开始支持 RDB 和 AOF 的混合持久化(默认关闭,可以通过配置项 aof-use-rdb-preamble 开启)。
8、Redis支持事务吗?
说到事务,大家可能最先想到的就是关系型数据库中的事务管理。Redis也支持事务,和关系型数据库的事务有类似的特点:
- 隔离性:事务是一个单独的隔离操作,事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
- 原子性:事务是一个原子操作,事务中的命令要么全部被执行,要么全部都不执行。
但是redis的事务和关系型数据库的事务有一个最大的区别:redis事务不会回滚,即使事务中有某条/某些命令执行失败了, 事务队列中的其他命令仍然会继续执行完毕。
MULTI 、 EXEC 、 DISCARD 和 WATCH 是 Redis 事务相关的命令。MULTI负责开启事务,EXEC负责执行事务中的命令,DISCARD负责清空事务中的命令队列并放弃执行当前事务,WATCH来监控某个键是否被修改。
9、介绍下Redis的线程模型?
Redis 内部使用文件事件处理器 file event handler
,这个文件事件处理器是单线程的,所以 Redis 才叫做单线程的模型。它采用 IO 多路复用机制同时监听多个 Socket,根据 Socket 上的事件来选择对应的事件处理器进行处理。
文件事件处理器的结构包含 4 个部分:
- 多个 Socket
- IO 多路复用程序
- 文件事件分派器
- 事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)
多个 Socket 可能会并发产生不同的操作,每个操作对应不同的文件事件,但是 IO 多路复用程序会监听多个 Socket,会将 Socket 产生的事件放入队列中排队,事件分派器每次从队列中取出一个事件,把该事件交给对应的事件处理器进行处理。
10、谈谈布隆过滤器的理解
布隆过滤器(英语:Bloom Filter)是1970年由一个叫布隆的小伙子提出的。它实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。
布隆过滤器的原理是,当一个元素被加入集合时,通过K个散列函数将这个元素映射成一个位数组中的K个点,把它们置为1。检索时,我们只要看看这些点是不是都是1就(大约)知道集合中有没有它了:如果这些点有任何一个0,则被检元素一定不在;如果都是1,则被检元素很可能在。这就是布隆过滤器的基本思想。
bloom filter之所以能做到在时间和空间上的效率比较高,是因为牺牲了判断的准确率、删除的便利性,主要缺点是:
- 存在误判,可能要查到的元素并没有在容器中,但是hash之后得到的k个位置上值都是1。如果bloom filter中存储的是黑名单,那么可以通过建立一个白名单来存储可能会误判的元素。
- 删除困难。一个放入容器的元素映射到bit数组的k个位置上是1,删除的时候不能简单的直接置为0,可能会影响其他元素的判断。
在使用bloom filter时,绕不过的两点是预估数据量n以及期望的误判率fpp,
在实现bloom filter时,绕不过的两点就是hash函数的选取以及bit数组的大小。
二、数据结构篇
1、谈谈SDS(简单动态字符串)
在Redis中,string类型的底层数据结构是sds
数据结构:
特点总结(和C语言的字符串做对比):
O(1)复杂度获取字符串长度:数据结构中的len字段保存了字符串长度,直接获取即可。而C语言要遍历整个字符串char数组,是O(n)
二进制安全:支持图像、音频、视频等任何二进制数据写入,而C语言只能支持符合ASCII编码的文本写入
不会发生缓冲区溢出:空间预分配和惰性清理机制
2、说一下Redis中的压缩列表
Redis 的 hash 数据类型的底层实现之一是压缩列表。(当数据量大时,hash 数据类型的另外一个底层实现是哈希表)
压缩列表是 Redis 为了节约内存而开发的,它是由连续内存块组成的顺序型数据结构,有点类似于数组。
压缩列表在表头有三个字段:
- *zlbytes*,记录整个压缩列表占用堆内存字节数;
- *zltail*,记录压缩列表「尾部」节点距离起始地址由多少字节,也就是列表尾的偏移量;
- *zllen*,记录压缩列表包含的节点数量;
- *zlend*,标记压缩列表的结束点,特殊值 OxFF(十进制255)
在压缩列表中,要查找定位第一个元素和最后一个元素,可以通过表头三个字段的长度直接定位,复杂度是 O(1)。而查找其他元素时,就只能逐个查找,此时的复杂度就是 O(N) 了。
压缩列表节点中包含三部分内容:
- *prevlen*,记录了前一个节点的长度;
- *encoding*,记录了当前节点实际数据的类型以及长度;
- *data*,记录了当前节点的实际数据;
压缩列表存在的问题是:连锁更新。
连锁更新一旦发生,就会导致压缩列表 占用的内存空间要多次重新分配,这就会直接影响到压缩列表的访问性能。
压缩列表只适用于保存的节点数量不多的场景,只要节点数量足够小,即使发生连锁更新,也是能接受的。
3、说一下哈希表的理解
哈希表是一种保存键值对(key-value)的数据结构。当一个哈希键包含的 key-value 比较多,或者 key-value 中元素都是比较长多字符串时,Redis 就会使用哈希表作为哈希键的底层实现。
优点:以 O(1) 的复杂度快速查询数据。
缺点:在哈希表大小固定的情况下,随着数据不断增多,那么哈希冲突的可能性也会越高。
Redis 采用了链式哈希的方式解决哈希表中的哈希冲突。实现的方式就是每个哈希表节点都有一个 next 指针,多个哈希表节点可以用 next 指针构成一个单项链表,被分配到同一个哈希桶上的多个节点可以用这个单项链表连接起来,这样就解决了哈希冲突。
但是链式哈希也会带来新的问题,当冲突越来越多,链表的长度会越来越长,查询性能O(N)则性能会越来越差。 因此Redis此时会采用rehash(重新hash)的方式对哈希表的大小进行扩展。
rehash
Redis 使用了两个全局哈希表进行 rehash。
这个过程分为三步:
- 给「哈希表 2」 分配空间,一般会比「哈希表 1」 大 2 倍;
- 将「哈希表 1 」的数据迁移到「哈希表 2」 中;
- 迁移完成后,「哈希表 1 」的空间会被释放,并把「哈希表 2」 设置为「哈希表 1」,然后在「哈希表 2」 新创建一个空白的哈希表,为下次 rehash 做准备。
但是第二步很有问题,如果「哈希表 1 」的数据量非常大,那么在迁移至「哈希表 2 」的时候,因为会涉及大量的数据拷贝,此时可能会对 Redis 造成阻塞,无法服务其他请求。
因此Redis又增加了渐进式hash的操作。
渐进式 rehash 步骤如下:
- 给「哈希表 2」 分配空间;
- 在 rehash 进行期间,每次哈希表元素进行新增、删除、查找或者更新操作时,Redis 除了会执行对应的操作之外,还会顺序将「哈希表 1 」中索引位置上的所有 key-value 迁移到「哈希表 2」 上;
- 随着客户端发起的哈希表操作请求数量越多,最终会把「哈希表 1 」的所有 key-value 迁移到「哈希表 2」,从而完成 rehash 操作。
在进行渐进式 rehash 的过程中,会有两个哈希表,所以在渐进式 rehash 进行期间,哈希表元素的删除、查找、更新等操作都会在这两个哈希表进行。新增一个 key-value 时,会被保存到「哈希表 2 」里面,而「哈希表 1」 则不再进行任何添加操作,这样保证了「哈希表 1 」的 key-value 数量只会减少,随着 rehash 操作的完成,最终「哈希表 1 」就会变成空表。
rehash 触发条件
触发 rehash 操作的条件,主要有两个:
- 当负载因子大于等于 1 ,并且 Redis 没有在执行 bgsave 命令或者 bgrewiteaof 命令,也就是没有执行 RDB 快照或没有进行 AOF 重写的时候,就会进行 rehash 操作。
- 当负载因子大于等于 5 时,此时说明哈希冲突非常严重了,不管有没有有在执行 RDB 快照或 AOF 重写,都会强制进行 rehash 操作。
负载因子=哈希表已保存节点数量/哈希表大小
4、说一下跳跃表的理解
举个形象的例子:跳跃表就类似于这样的机制,最下面一层所有的元素都会串起来,都是员工,然后每隔几个元素就会挑选出一个代表,再把这几个代表使用另外一级指针串起来。然后再在这些代表里面挑出二级代表,再串起来。最终形成了一个金字塔的结构。
zset的底层实现就是跳跃表。
三、应用场景篇
1、Redis在高并发场景下可能会出现什么问题?
解释缓存穿透、缓存击穿、缓存雪崩
缓存穿透
一句话概括:穿过 Redis 和数据库
展开说说:
下面这段逻辑我们应该经常遇到:先去 Redis 中查找某数据,Redis 中查不到就去 DB 中查,DB 中查到后回写一份数据到 Redis 中。
上面这段逻辑正常情况下问题并不大,但是如果是在高并发场景下或者某个用户恶意重复请求某个不存在的数据A,那么每次请求都会直接穿过Redis组件并触达到 DB 上,严重时会导致数据库连接资源耗尽然后宕机。
因此,为了预防上述情况, 当Redis 和数据库中都没有我们想要的数据时,就需要考虑缓存穿透的问题了。
常见缓存穿透的解决方案:
- 缓存空值
- 请求合法性校验
- 布隆过滤器
缓存击穿
一句话概括:大家都盯着一个key往死里整。
*展开说说:
当某个业务是高并发场景时,大量请求同时请求同一个数据,而此时Redis 中该数据在此刻正好过期了,那么无数的请求则直接打到了后面的数据库 上,那么DB由于连接资源有限,被瞬间耗尽 ,后果不用想,DB肯定会挂。
常见缓存击穿的解决方案:
- 使用互斥锁
- 热点key永不过期
- 后台续命(后台开一个定时任务,专门主动更新即将过期的数据。相当于让key永不过期)
缓存雪崩
一句话概括:Redis挂了,或者数据都同时过期
展开说说:
在某个时刻 Redis 服务实例挂了,或者Redis中的热点 key 都过期(失效)了。如果集群中的热点 key 在某一时刻同时失效了的话,同时海量的请求都将直接打到 DB 上,DB 就很可能会被击垮。
常见缓存雪崩的解决方案:
热点key永不过期
key的 失效时间加上随机数
限流器+本地缓存
2、说说热Key问题和解决方案
答案来源:如何找出优化大Key与热Key,产生的原因和问题_云数据库 Redis-阿里云帮助中心 (aliyun.com)
一句话概括:请求到的分片过于集中,超过单台Server的性能极限。
展开说说:
在服务端读数据进行访问时,往往会对数据进行分片切分(集群方案)。此过程中会在某一主机Server上对相应的Key进行访问,当访问超过Server极限时,就会导致热点 Key 问题的产生。
通常以其接收到的Key被请求频率来判定是否是热key,例如:
- QPS集中在特定的Key:Redis实例的总QPS(每秒查询率)为10,000,而其中一个Key的每秒访问量达到了7,000。
- 带宽使用率集中在特定的Key:对一个拥有上千个成员且总大小为1 MB的HASH Key每秒发送大量的HGETALL操作请求。
- CPU使用时间占比集中在特定的Key:对一个拥有数万个成员的Key(ZSET类型)每秒发送大量的ZRANGE操作请求。
带来的问题:
- 占用大量的CPU资源,影响其他请求并导致整体性能降低。
- 集群架构下,产生访问倾斜,即某个数据分片被大量访问,而其他数据分片处于空闲状态,可能引起该数据分片的连接数被耗尽,新的连接建立请求被拒绝等问题。
- 在抢购或秒杀场景下,可能因商品对应库存Key的请求量过大,超出Redis处理能力造成超卖。
- 热Key的请求压力数量超出Redis的承受能力易造成缓存击穿,即大量请求将被直接指向后端的存储层,导致存储访问量激增甚至宕机,从而影响其他业务。
常见热key问题解决方案:
- 服务端增加本地缓存
- 热点key打撒到其它分片节点
- 使用读写分离架构
3、说说大Key问题和解决方案
答案来源:如何找出优化大Key与热Key,产生的原因和问题_云数据库 Redis-阿里云帮助中心 (aliyun.com)
一句话概括:单个简单的key存储的value很大
展开说说:
业务场景中经常会有各种大key的情况, 比如: 1)单个简单的key存储的value很大(例如排行榜信息,key是固定的,value排行榜几十万的数据) 2)hash、set、zset、list中存储过多的元素(以万为单位)
通常以Key的大小和Key中成员的数量来综合判定是否是大key,例如:
- Key本身的数据量过大:一个String类型的Key,它的值为5 MB。
- Key中的成员数过多:一个ZSET类型的Key,它的成员数量为10,000个。
- Key中成员的数据量过大:一个Hash类型的Key,它的成员数量虽然只有1,000个但这些成员的Value(值)总大小为100 MB。
带来的问题:
- 客户端执行命令的时长变慢。
- Redis内存达到maxmemory参数定义的上限引发操作阻塞或重要的Key被逐出,甚至引发内存溢出(Out Of Memory)。
- 集群架构下,某个数据分片的内存使用率远超其他数据分片,无法使数据分片的内存资源达到均衡。
- 对大Key执行读请求,会使Redis实例的带宽使用率被占满,导致自身服务变慢,同时易波及相关的服务。
- 对大Key执行删除操作,易造成主库较长时间的阻塞,进而可能引发同步中断或主从切换
常见大key问题解决方案:
- 拆分大key为多个小key,使用multiGet获取值
- 定期对过期的大key进行清理
- 监控Redis的内存水位,提前预防问题
4、用过Redis的分布式锁吗?
Redis实现分布式锁有多种方案:
方案一:SETNX + EXPIRE
EXPIRE操作可能失败导致锁永远得不到释放
方案二:SETNX + value值是(系统时间+过期时间)
依赖每个客户端的时间设置,必须要求分布式环境下,每个客户端的时间必须同步。
当前客户端获得的锁可能被别的客户端释放/解锁。
当前客户端锁的过期时间,可能被别的客户端覆盖。
方案三:使用Lua脚本(包含SETNX + EXPIRE两条指令)
方案四:SET的扩展命令(SET EX PX NX)
方案五:SET EX PX NX + 校验唯一随机值,再释放锁
方案六: 开源框架~Redisson
方案七:多机实现的分布式锁Redlock
5、如何保证Redis和数据库的双写一致性
首先要坚持一个观点:没有任何技术方案可以保证Redis和数据库的实时双写一致,都只能做到最终一致性。
保证双写一致性,可选的方案有:更新数据库 + 更新缓存、更新数据库 + 删除缓存
更新数据库 + 更新缓存方案
在并发情况下无法保证缓存和数据一致性,且存在缓存资源浪费和机器性能浪费的情况发生
更新数据库 + 删除缓存方案
先删除缓存,再更新数据库在并发情况下依旧有数据不一致问题,解决方案是延迟双删,但这个延迟时间很难评估,所以推荐用先更新数据库,再删除缓存的方案; 在先更新数据库,再删除缓存方案下,为了保证两步都成功执行,需配合消息队列或订阅变更日志的方案来做,本质是通过重试的方式保证数据一致性
保证数据库和缓存一致性,推荐采用「先更新数据库,再删除缓存」方案,并配合「消息队列」或「订阅变更日志」的方式来做。
四、更多面试题
前面列出的面试题是小郭目前熟知的知识,还比较少。
下面这个链接里是是某位大佬整理的非常全的面试题,小郭已经收藏慢慢看了,推荐大家也可以收藏一下。
推荐大佬的面试宝库:JavaFamily/docs/all/缓存/redis at master · AobingJava/JavaFamily (github.com)