Redis面试大全
1. 什么是Redis
Redis是由意大利人Salvatore Sanfilippo(网名:antirez)开发的一款内存高速缓存数据库。Redis全称为:Remote Dictionary Server(远程数据服务),该软件使用C语言编写,Redis是一个key-value存储系统,它支持丰富的数据类型,如:string、list、set、zset(sorted set)、hash。
2. Redis特点
- 以内存作为数据存储介质,读写数据的效率极高,远远超过数据库。以设置和获取一个256字节字符串为例,它的读取速度可高达110000次/s,写速度高达81000次/s。
- 支持数据的持久化,可以将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用。
- 支持多种数据类型String、List、Hash、Set、zset
- 支持数据的备份,即master-slave模式的数据备份
3.Redis有哪些数据类型,以及每种数据类型使用的场景
Redis支持多种数据类型,有String、List、Hash、Set、zset
String(字符串)
String类型是二进制安全的,意思是Redis的String可以包含任何数据,比如图片或者序列化的对象等。一个Redis中字符串的value最多可以是512M。一般做一些复杂的计数功能的缓存。
List(列表)
List是按照插入顺序排序的字符串链表。
从元素插入和删除的效率视角来看,如果我们是在链表的两头插入或删除元素,这将会是非常高效的操作,即使链表中已经存储了百万条记录,该操作也可以在常量时间内完成。然而需要说明的是,如果元素插入或删除操作是作用于链表中间,那将会是非常低效的 。
Redis链表经常会被用于消息队列的服务,以完成多程序之间的消息交换。假设一个应用程序正在执行LPUSH操作向链表中添加新的元素,我们通常将这样的程序称之为"生产者(Producer)",而另外一个应用程序正在执行RPOP操作从链表中取出元素,我们称这样的程序为"消费者(Consumer)"。如果此时,消费者程序在取出消息元素后立刻崩溃,由于该消息已经被取出且没有被正常处理,那么我们就可以认为该消息已经丢失,由此可能会导致业务数据丢失,或业务状态的不一致等现象的发生。然而通过使用RPOPLPUSH命令,消费者程序在从主消息队列中取出消息之后再将其插入到备份队列中,直到消费者程序完成正常的处理逻辑后再将该消息从备份队列中删除。同时我们还可以提供一个守护进程,当发现备份队列中的消息过期时,可以重新将其再放回到主消息队列中,以便其它的消费者程序继续处理。
可以利用 lrange 命令,做基于 Redis 的分页功能,性能极佳,用户体验好
Hash(字典)
Hash是一个健值对集合,是一个String类型的key与value的映射表,特别适合用于存储对象。 可用于存储、读取、修改用户属性, Hash 结构可以使你像在数据库中 Update 一个属性一样只修改某一项属性值。
Set(集合)
Set 是一个集合,集合的概念就是一堆不重复值的组合。利用 Redis 提供的 Set 数据结构,可以存储一些集合性的数据。 集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是O(1) 。Redis 非常人性化的为集合提供了求交集、并集、差集等操作,那么就可以非常方便的实现如共同关注、共同喜好、二度好友等功能 。也可以做全局去重的功能。
zset(Sorted Set,有序集合)
和Sets相比,Sorted Sets是将 Set 中的元素增加了一个权重参数 score,使得集合中的元素能够按 score 进行有序排列。可以做排行榜应用,取 TOP N 操作。Sorted Set 可以用来做延时任务。最后一个应用就是可以做范围查找。
4.为什么要用Redis
性能和并发。
性能:将一些耗时比较久,且结果不经常变动的SQL,放到Redis中,这样,请求直接从缓存中读取,使得能够迅速响应。
并发:大并发的情况下,所有的请求直接访问数据库,数据库会出现连接异常。这个时候,就需要使用Redis做一个缓冲操作,让请求先访问到redis,而不是直接访问数据库 。
5.Redis有什么缺点
- 缓存和数据库双写一致性问题
- 缓存击穿问题
- 缓存雪崩问题
- 缓存的并发竞争问题
6.Redis为什么这么快
- 纯内存操作
- 单线程模型,避免了频繁的上下文切换
- 采用非阻塞I/O多路复用机制
什么是非阻塞I/O呢?
阻塞与非阻塞可以简单理解为调用一个IO操作能不能立即得到返回应答,如果不能立即获得返回,需要等待,那就阻塞了;否则就可以理解为非阻塞。
什么是I/O多路复用机制呢?
单个线程,通过记录跟踪每个I/O流(sock)的状态,来同时管理多个I/O流 。
I/O多路复用的优势并不是对于单个连接能处理的更快,而是在于可以在单个线程/进程中处理更多的连接。与多进程和多线程技术相比,I/O多路复用技术的最大优势是系统开销小,系统不必创建进程/线程,也不必维护这些进程/线程,从而大大减小了系统的开销。
7.Redis的过期策略
Redis采用的过期策略是:定期删除+惰性删除策略。
定期删除,Redis 默认每隔100ms 检查,是否有过期的 Key,有过期 Key 则删除。
需要说明的是,Redis 不是每隔100ms 将所有的 Key 检查一次,而是随机抽取进行检查(如果每隔 100ms,全部 Key 进行检查,Redis 岂不是卡死)。
定期删除可以通过:
第一、配置redis.conf 的hz选项,默认为10 (即1秒执行10次,100ms一次,值越大说明刷新频率越快,最Redis性能损耗也越大,建议不要超过100)
第二、配置redis.conf的maxmemory最大值,当已用内存超过maxmemory限定时,就会触发主动清理策略
因此,如果只采用定期删除策略,会导致很多 Key 到时间没有删除。于是,惰性删除派上用场。
也就是说在你获取某个 Key 的时候,Redis 会检查一下,这个 Key 如果设置了过期时间,如果过期了,此时就会删除。
过期策略可以参考:[Redis数据过期策略详解](https://www.cnblogs.com/xuliangxing/p/7151812.html)
但是当某些key没有被定期删除到,也没有获取某些key,内存岂不是也会占用很高,这时候就需要内存淘汰机制了
8.Redis内存淘汰机制
在 redis.conf 中有一行配置
# maxmemory-policy volatile-lru
Redis内存淘汰策略有(触发该策略的机制是 当内存不足以容纳新写入数据时):
- noeviction:谁也不删,直接在写操作时返回错误 。应该没人用吧。
- allkeys-lru:在键空间中,移除最近最少使用的 Key。推荐使用,目前项目在用这种。
- allkeys-random:在键空间中,随机移除某个 Key。应该也没人用吧,你不删最少使用 Key,去随机删。
- volatile-lru:在设置了过期时间的键空间中,移除最近最少使用的 Key。这种情况一般是把 Redis 既当缓存,又做持久化存储的时候才用。不推荐。
- volatile-random:在设置了过期时间的键空间中,随机移除某个 Key。依然不推荐。
- volatile-ttl:在设置了过期时间的键空间中,有更早过期时间的 Key 优先移除。不推荐。
9.Redis和数据库双写一致性问题
首先,采取正确更新策略,先更新数据库,再删缓存。其次,因为可能存在删除缓存失败的问题,提供一个补偿措施即可,例如利用消息队列。
10.如何应对缓存穿透、缓存雪崩、缓存击穿问题
缓存穿透:黑客故意去请求缓存中不存在的数据,导致所有的请求都怼到数据库上,从而数据库连接异常。
解决方法:
- 采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层数据库的查询压力(推荐)
- 如果一个查询返回的数据为空(不管是数 据不存在,还是系统故障),仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟(推荐,简单暴力)
- 利用互斥锁,缓存失效的时候,先去获得锁,得到锁了,再去请求数据库。没得到锁,则休眠一段时间重试
- 采用异步更新策略,无论 Key 是否取到值,都直接返回。Value 值中维护一个缓存失效时间,缓存如果过期,异步起一个线程去读数据库,更新缓存。需要做缓存预热(项目启动前,先加载缓存)操作。
缓存雪崩:当缓存服务器重启或者大量缓存集中在某一个时间段失效,来了一批请求,请求全部到DB,DB瞬时压力过重雪崩。
解决方法:
- 给缓存的失效时间加上一个随机值,避免集体失效
- 使用互斥锁,但是该方案吞吐量明显下降了
- 双缓存,我们有两个缓存,缓存 A 和缓存 B。缓存 A 的失效时间为 20 分钟,缓存 B 不设失效时间。自己做缓存预热操作。然后细分以下几个小点:从缓存 A 读数据库,有则直接返回;A 没有数据,直接从 B 读数据,直接返回,并且异步启动一个更新线程,更新线程同时更新缓存 A 和缓存 B。(推荐)
缓存击穿:对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。这个时候,需要考虑一个问题:缓存被“击穿”的问题,这个和缓存雪崩的区别在于这里针对某一key缓存,前者则是很多key。 缓存在某个时间点过期的时候,恰好在这个时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。
解决方法:
- 使用互斥锁
- "提前"使用互斥锁
- "永不过期"
参考:
11.若Redis中有1亿个key,其中有10w是以某个固定的已知前缀开头,怎么将它们全部找出来
使用keys指令可以扫出指定模式的key列表。如果这个redis正在给线上的业务提供服务,keys指令会导致线程阻塞一段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复。这个时候可以使用scan指令,scan指令可以无阻塞的提取出指定模式的key列表,但是会有一定的重复概率,在客户端做一次去重就可以了,但是整体所花费的时间会比直接用keys指令长。
12.怎么使用Redis做异步队列
一般使用list结构作为队列,rpush生产消息,lpop消费消息。当lpop没有消息的时候,要适当sleep一会再重试。 如果不用sleep,list还有个指令叫blpop,在没有消息的时候,它会阻塞住直到消息到来。
使用pub/sub主题订阅者模式,可以实现1:N的消息队列。 也就是生产一次消费多次。但是使用pub/sub是有缺点的,在消费者下线的情况下,生产的消息会丢失,得使用专业的消息队列如rabbitmq等。
redis如何实现延时队列:使用sortedset,拿时间戳作为score,消息内容作为key,调用zadd来生产消息,消费者用zrangebyscore指令获取N秒之前的数据轮询进行处理。
13.怎么用Redis实现分布式锁
主要是使用了redis 的setnx命令,缓存了锁,reids缓存的key是锁的key,所有的共享, value是锁的到期时间(注意:这里把过期时间放在value了,没有时间上设置其超时时间)。
1.通过setnx尝试设置某个key的值,成功(当前没有这个锁)则返回,成功获得锁
2.锁已经存在则获取锁的到期时间,和当前时间比较,超时的话,则设置新的值
实现方法可以参考:Redis分布式锁实现