Redis常见面试问题

Redis是一种运行在内存当中的非关系型数据库,使用c语言编写,由于他的高效读写能力,Redis通常用作缓存,分布式锁,分布式Session等场景

Redis原生还支持事务,持久化,集群方案等等。

在Redis之前,Memcached也经常拿来做缓存,只不过后来Memcached被淘汰了,两者的简单比较

相同点:

  • 都是基于内存的数据库,一般都来作为缓存组件

  • 两者的性能都非常优秀

不同点:

  • Redis支持更加丰富的数据类型,除了Memcached仅仅支持的KV外,还支持list、set、zset、hash等数据结构

  • Redis具有持久化特性,因此具有灾难恢复机制,Memcached没有(可用性)

  • Redis可以将硬盘作为扩展存储当内存用完的时候使用,而Memcached会直接报错(扩展性)

  • Redis原生支持集群方案,而Memcached需要在客户端内实现集群部署逻辑(可扩展性)

  • Redis在6.0之前是单线程的多路IO复用模型,而Memcached是多线程,非阻塞的IO复用模型

数据缓存的一个处理流程:

用户查询Redis是否存在数据,如果存在直接返回

如果不存在,则访问数据库,如果数据库不存在则返回null

如果数据库存在,则更新Redis缓存并返回数据结果

经过这个流程的处理,系统并发量将不会直接被MySQL的并发上限所限制,增大了系统的吞吐量和响应时间。(高性能和高并发的角度)

Redis常见五大数据结构:

string数据结构,简单的kv类型,可以用于计数的场景,可以计算用户访问次数,文章访问量等等

是动态字符串,内部存储类似于Java当中的ArrayList,使用数组来完成空间的预分配,如果长度是小于1M(220)的时候扩容是直接翻倍的,如果是大于1M(220)时候扩容会增加1M空间,最大长度限制为512M

127.0.0.1:6379> set number 1
OK
127.0.0.1:6379> incr number
(integer) 2
127.0.0.1:6379> get number
"2"
127.0.0.1:6379> decr number
(integer) 1
127.0.0.1:6379> get number
"1"

list数据结构,类似于Java当中的LinkedList,双向链表实现,插入删除非常快,Redis这种数据结构常被作为内存队列来进行使用

127.0.0.1:6379> rpush myList v1
(integer) 1
127.0.0.1:6379> rpush myList v2 v3
(integer) 3
127.0.0.1:6379> lpop myList
"v1"
127.0.0.1:6379> rpop myList
"v3"

hash:和HashMap类似,内部使用数组+链表实现,特别适合用于存储对象

但是在使用过程当中是保证了他的可用性的,这点体现在了他的Rehash过程当中,也是和HashMap类似具有扩容特性,一旦扩容就会进行rehash,Redis为了保证rehash过程期间hash的可用性,是采用了渐进式rehash的,如需要将1扩容成2,创建一个大小为2的数组,进行数据迁移,前面的数据查询依然使用原来的字典,在复制过程当中只有全部完成了,才会使用新字典去代替原字典,保证流程不会阻塞

127.0.0.1:6379> hset userInfo name LuckyCurve age 21
(integer) 2
127.0.0.1:6379> hget userInfo name
"LuckyCurve"
127.0.0.1:6379> hget userinfo age
(nil)
127.0.0.1:6379> hget userInfo age
"21"
127.0.0.1:6379> hset userInfo age 22
(integer) 0
127.0.0.1:6379> hget userInfo age
"22"

set:类似于Java中的HashSet,具有唯一性的特性,也是使用哈希这种存储结构来保证元素的唯一性的,之所以可以保证唯一性,是减少对象间equals方法调用次数,并且支持集合的交并补运算

127.0.0.1:6379> sadd mySet1 v1 v2
(integer) 2
127.0.0.1:6379> sadd mySet2 v2 v3
(integer) 2
127.0.0.1:6379> sinterstore mySet3 mySet1 mySet2
(integer) 1
127.0.0.1:6379> smembers mySet3
1) "v2"

zset:有序集合,类似于HashMap具有了TreeMap的排序功能一样,适用于对某些场景需要根据对象字段进行排序的情况文件事件,底层实现是跳表。

增加了一列score来完成评分

跳表就是一个多层级的链表,高层级的链表是低层级链表的子集,构建这个层级链表的规则是将每两个节点提取一个节点出来做索引,将这些索引节点再构成一个链表,不断重复这个动作,直到当前层的节点个数为两个

删除数据的操作比较简单,只需要删除多层级的链表当中的每个元素就行了,但是插入时候可能导致两个索引节点之间的数据过于庞大,有一个随机策略来避免跳表退化

之所以选用跳表而不选择红黑树取决于以下两方面:

  1. 范围查询时候跳表效率高

  2. 插入删除较之于红黑树的涂色简单的多

Redis是基于Reactor模型(反映模型模型,基于事件驱动)设计出来的,和Netty类似,Reactor好像成为了NIO开发的基石了,Redis的核心为文件事件处理器,因为文件事件处理器是单线程方式运行的,因此我们说Redis是单线程的。

这里的文件事件处理器非常类似于NIO当中的Selector组件,Selector是实现了对大量Channel的管理,这里的文件事件管理器同时监听大量的Socket,来并发处理大量请求

体现Redis具有反应式编程的特性在于:Redis是等待套接字执行请求,发送事件给文件事务管理器,文件事务管理器通过调用不同的事件处理器来完成响应

Redis之所以不愿意使用多线程,一直保持单线程到6.0是因为单线程容易维护,且不存在性能瓶颈,同时避免了上下文切换和锁竞争的消耗

Redis6.0之后引入多线程仅仅是针对较大的网络数据的读写上了,核心还是单线程

Redis可以设置过期时间来保证一些长时间没有被使用到的数据被清除,Redis的过期时间除了可以缓解内存压力,我们还可以利用过期这种特性来完成特定业务逻辑,如短信验证码60s之内有效

是使用一个hash数据结构【过期字典】来维持的过期时间,key指向一个数据,value存储的是key所指对象的过期时间

Redis过期数据的删除策略:

  • 惰性删除:只有在取出key的时候才对数据进行过期检查,对CPU友好,对内存不利【空间换时间】

  • 定期删除:每隔一段时间根据过期字典删除一批数据,尽可能少影响CPU,对内存有利【时间换空间】

Redis采取的是定期删除和惰性删除结合的策略,

【问题】:如果此时key比较多,惰性删除和定期删除可能漏掉大量的key,就需要使用到内存淘汰机制了。

内存淘汰机制不仅可以解决过期删除策略导致的key堆积问题主动淘汰一些key,还可以对热点数据进行筛选并保留,存在以下几种策略:

volatile-LRU:从过期字典中挑选最近使用最少的数据淘汰

volatile-TTL:从过期字典中挑选将要过期的数据进行淘汰

Randon:从过期字典中随机挑选数据进行淘汰

allkeys-lru:当内存不足时候,从过期字典中移除最少使用的数据【最常用】

allkeys-random:从数据集当中挑选任意数据淘汰

no-evictiion:禁止驱逐数据(用的非常少了)

Redis持久化机制

Redis有两种持久化方案,快照和只追加文件

快照相当于是某个时间点上内存数据的一个副本,可以将快照放在磁盘上等待下次的数据恢复,也可以直接将快照传给其他的数据库来实现Redis的主从复制结构,快照就是Redis默认的持久化方案,不要感觉奇怪,因为Redis数据变换是非常频繁的,直接使用快照反而有时候有优势,但是快照方式容易漏数据

另一种持久化方案是只追加文件,类似于MySQL的redo log,直接在磁盘上记录更改Redis数据的命令。

4.0之后支持两个混合,但是默认依旧还是快照方式,混合之后就相当于基于某个版本的数据然后记录修改指令。

快照实现方式是fork一个子进程,这时候这两个进程所占有的空间其实是指向同一片内存区玉的(操作系统优化 CopyOnWrite),但是一旦来了写入请求后,这时候就会完成数据复制,然后将数据域当中的内容直接形成一个新的RDB文件,数据是fork时候的数据来源,只有在生成了新的RDB文件之后才会完成新RDB到原来RDB的替换操作,保证数据一致性。

1GB的文件加载到内存当中来花费20s~30s的时间。

Redis提供数据保障容灾的几种方式:

  • 哨兵模式(类似于非分布式场景下的容灾处理):

哨兵可以理解成一个独立的进程,来保证当Master宕机了之后可以将Slave节点切换为Master节点,并且通过发布订阅模型来将这个变换操作通知到其他的Slave节点更改配置文件

  • Redis Cluster(分布式场景,因为有很多限制,主要是单机QPS支持不上去,即便通过硬件扩容,可是内存瓶颈、网卡瓶颈都是客观存在的问题)

核心思想就是将数据分摊到不同的机器上面去,几种数据分布方式:

  1. 顺序分布:如前多少条数据往其中一个机器当中存储,当这个机器存储容量达到阈值时候、下一段数据又往下一个机器上存储了,更加常见的是关系型数据库的分库分表设计当中

  2. 哈希分布:可以进行进一步划分

    1. 节点取余分区,hash(key) % node 根据结果值来决定将当前数据存储在那一台机器上面

      存在一个很大的问题,当增加机器时候,会导致其中存储的数据都需要重新进行哈希运算来重新分区

      建议做法是成倍地增加机器来减少数据迁移量,如从3台增加到6台,其中只有50%的数据会发生迁移

      以上故障可以人为避免,但是模拟另一种情况:其中一台服务器宕机,这时候整个缓存就不可用了,因为服务器数量变化,hash规则改变,所有缓存需要重新rehash,导致缓存雪崩问题

    2. 一致性哈希分区:当前服务器IP地址对232进行取余,将服务器打到这个hash环上面来,此时新来了数据之后,同样的使用哈希运算然后对232取余数,打到这个hash环上面来,数据会顺着这个环来找到下一个节点,这个节点负责对这个数据的存储

      分配到token环上的数据,会通过向后嗅探的方式来完成存储位置的寻找,并且将数据存储到对应的机器上面去,即使其中一台服务器发生问题以后,也不会导致整个集群的不可用 存在的问题:可能主机存放不均,因为主机在环上的地址也是通过hash算出来的,可能出现这种情况:

      也就是hash环的偏斜,导致缓存极度不均匀的分布在各节点上

    解决方法:使用虚拟节点,在hash环当中设置多个虚拟节点,通常都是32个甚至更多,我们只需要维护虚拟节点到实际节点的映射即可完成数据的存储。

  3. 虚拟槽分区 共有16384个槽,每个节点都会负责一个槽范围,当数据进来之后,会完成hash运算并对16384进行取余,然后看是在那个节点负责的机器上,然后去对应的机器上完成数据的读取或写入即可

    当节点挂掉之后,会通知其他节点完成对槽的平分,这时候只需要进行少量的数据交换即可

    这里的Redis Cluster也就是根据这里的虚拟槽分区来完成的,多台Master节点负责一个槽范围,并且每个节点都是Master-Slave的结构设计的

https://www.zsythink.net/archives/1182

Redis的事务,Redis的事务不同于传统的关系型数据库具有ACID特性,不支持事务回滚,因此不支持原子性和持久性,主要是为了性能考虑。Redis事务仅仅是提供命令打包功能,按顺序执行打包的所有操作,并且不会被中途打断。

使用Redis的两个常见问题:缓存穿透和缓存雪崩

缓存穿透:大量key不存在于Redis中,请求直接打到MySQL上了

布隆过滤器的核心就是一个byte数组,并且配合一系列的哈希函数进行扰动,对每个存在于MySQL中的元素,都会进行全部的哈希运算,得到多个哈希值,并将多个算出的哈希值置一。在查找的时候,也是同样的逻辑,将该元素进行哈希运算得到多个哈希值,判断这些哈希值对应的下标是否都为1,如果都为1则可能存在(主要存在一个哈希冲突,可以通过增大数组的容量和选择更优秀的哈希算法来解决),如果其中一个不为1则肯定不存在。

布隆过滤器的删除过程则比较复杂了。

缓存雪崩:主要指的是缓存数据大面积失效,导致请求直接打到了数据库上来,很可能导致数据库直接宕机

造成缓存大面积失效的原因可能是:Redis服务直接不可用、热点缓存数据突然失效

第一种解决方法:部署Redis集群,避免Redis服务宕机、限流,避免请求直接全部打到Redis上来

第二种解决方法:增加Redis所占内存,让内存中存取更多数据,从而导致缓存失效的比率降低。2、设置过期策略,设置成永不失效,防止一部分热点数据被丢弃

看到了一个缓存雪崩非常好的例子,假如我抢购页面是零点开始,而我现在在中午十二点缓存了数据,过期时间定为了12小时,刚好抢购时候缓存大面积失效,直接打到数据库上去,这就是缓存雪崩的一种案例。

解决方法:设置随机的过期时间,避免同一时间点缓存大面积失效

保证数据的一致性

无法做到短时间内的数据一致性,但是可以保证整体的数据一致性,即引入缓存的代价就是带来了短时间的数据不一致。

主要的解决策略为:更新DB的时候删除缓存,因为DB更新具有原子性,不用管,如果DB成功了但是cache删除失败了,建议定一个重试次数去一直重试。

通常,我们将保证数据一致性的策略叫做缓存读写策略,主要包括三种:

  • 旁路缓存模式

适合读多写少的场景,也是最常用到的,读操作与上面说的一样,写操作则是先写DB,然后在删除cache。

为什么要先删除DB再删除cache呢?主要是因为cache的读写速度会比DB快的多,让DB先执行,cache后执行可以最大可能的减少数据不一致的时间。当然该方案只能减少数据不一致时候的时间,无法完全避免

缺陷:

1、首次请求的时候一定是不命中数据的

解决方法:预先加载一部分热数据进行预热

2、Redis和MySQL不具备强一致性

某个时间点仍然会存在Redis和MySQL数据不一致的问题,解决方法:使用分布式锁

  • 读写穿透

运用的比较少,让应用直接和Redis打交道,Redis自身去做将数据写入DB的工作,这是非常少见的,因为Redis不支持将数据写入到DB当中

操作逻辑为:

读操作:从cache中获取数据,获取到了直接返回,如果获取不到,从DB加载到cache中,然后再由cache返回

写操作:先查cache,如果不存在则直接更新DB,如果存在则更新cache,然后cache自己去更新DB

缺陷:

1、首次请求数据必然不会命中缓存

  • 异步缓存写入

感觉使用的更少了,是在读写穿透的基础上进行了改变,在数据更新的时候如果数据存储在cache中,读写穿透是cache同步调用DB去进行数据更新,而异步缓存写入则是只更新缓存,不直接更新DB,而是改为异步批量的方式更新DB、

可以看出写性能是非常高的,直接将数据改变完全写入到内存当中,适用于数据经常变化但是对数据一致性要求没那么高的场景。

缺陷:

1、首次访问数据必然不会出现在缓存

2、数据一致性可能比较差

Redis集群部署方式:

1、主从哨兵模式(这个是为了)

最后更新于