原文链接:https://mp.weixin.qq.com/s?__biz=MzU2NTc0MDMyNg==&mid=2247483823&idx=1&sn=e4ded94ac60fdcdc07103a93ea04a501&chksm=fcb657e6cbc1def0aae2c0b72f0cd31f4b7b602d00fe677cce810a0ba6a38cc47433703da077&mpshare=1&scene=23&srcid=0724qcB3Fe3tyGx9r1sPh1oZ&sharer_sharetime=1595582644044&sharer_shareid=f11d608890e113ccd16c1f9281c7c800#rd
前言:
Redis在面试中必问,以下是我整理的高级面试题,如有更好的面试题大家给我留言,答案有不对的大家也可以给我留言。
面试题:
一.redis 是什么?都有哪些使用场景?
redis是一个高性能的key-value数据库。支持数据的数据持久化,支持list,set,hash等数据结构,支持高可用的主从模式。
使用场景:
1.削峰:常见的秒杀活动中并发请求量会非常大,为了防止我们DB宕机,需要通过中间件进行削峰限流。很简单一个应用方法,前端接受10000/s的并发请求,而后端接口只能处理100/s并发请求,那么将前端的并发请求扔到redis队列中缓存起来,最大不超过100个。
2.热数据:一些系统频繁请求的数据存放到redis中减少DB压力。举个例子:每个系统中都会有字典数据,这些数据不会经常变但是访问特别频繁,将这些数据存放到redis中加快访问速度减少DB压力。
3.计数器:通过redis提供的递增和递减命令实现计数器功能。举个例子:我们要对某个用户的某个操作进行统计次数,把用户:操作作为key,每次用户操作功能递增。
4.分布式锁:分布式服务中需要对某个资源进行保护,保证不会产生并发,通过redis的分布式锁实现。
二.redis 有哪些功能?
1.数据缓存
2.数据持久化:redis根据设定规则会把缓存数据持久化到文件中,防止意外重启丢失数据造成缓存雪崩。
3.哨兵,复制:高可用,主从同步,主库挂了哨兵自动切换备库。
4.集群:为什么需要集群,有人说如果是IO或者CPU资源不够我们可以在主从的基础上进行读写分离,是可以但是两者内存数是固定的,并不能横向扩展,因为主从数据都会全部复制。那么这时候就需要集群,一个集群挂载N个主从,每一个主从通过规则存储一部分数据,支持横向扩展。
三.redis 为什么是单线程的?
引用:https://www.php.cn/redis/424484.html
Redis为什么是单线程的?
因为Redis是基于内存的操作,CPU不是Redis的瓶颈,Redis的瓶颈最有可能是机器内存的大小或者网络带宽。既然单线程容易实现,而且CPU不会成为瓶颈,那就顺理成章地采用单线程的方案了。
详细原因:
1、不需要各种锁的性能消耗
Redis的数据结构并不全是简单的Key-Value,还有list,hash等复杂的结构,这些结构有可能会进行很细粒度的操作,比如在很长的列表后面添加一个元素,在hash当中添加或者删除一个对象。这些操作可能就需要加非常多的锁,导致的结果是同步开销大大增加。
总之,在单线程的情况下,就不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗。
2、单线程多进程集群方案
单线程的威力实际上非常强大,每核心效率也非常高,多线程自然是可以比单线程有更高的性能上限,但是在今天的计算环境中,即使是单机多线程的上限也往往不能满足需要了,需要进一步摸索的是多服务器集群化的方案,这些方案中多线程的技术照样是用不上的。
3、CPU消耗
采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU。但是如果CPU成为Redis瓶颈,或者不想让服务器其他CUP核闲置,那怎么办?
可以考虑多起几个Redis进程,Redis是key-value数据库,不是关系数据库,数据之间没有约束。只要客户端分清哪些key放在哪个Redis进程上就可以了。
Redis单线程的优劣势:
1、单进程单线程优势
代码更清晰,处理逻辑更简单,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗,不存在多进程或者多线程导致的切换而消耗CPU
2、单进程单线程弊端
无法发挥多核CPU性能,不过可以通过在单机开多个Redis实例来完善;
四.什么是缓存穿透?怎么解决?
缓存穿透:缓存和DB都没有此key数据,大量并发请求获取key对应的value,经过缓存后依然访问DB,DB压力大很容易宕机。
解放方案:
1.数据key校验拦截,如果key是自增主键,有人偏偏发送获取 id = -1的大并发请求,需要对id <=0的请求拦截。
2.当查询数据库没有数据默认给缓存value设置 null 过期时间为5s或者2s具体看场景。
这里多充点redis经常遇到的问题:
缓存击穿:
缓存中某热数据过期,数据库中有数据。当此刻大量并发请求获取此热数据那么压力全部落到DB上很可能导致DB宕机。
解决办法:
1.此热数据永不过期
2.对此热数据key加同步锁,没有获取锁的自旋,防止都去访问DB更新缓存。
缓存雪崩:
缓存数据在相近时间大量过期,导致并发请求在某一时刻全部落到DB,导致DB宕机。
解决方案:
1.过期时间随机生成
2.热点数据永不过期
3.数据库集群化,分库分表减少热点库压力
五.redis 支持的数据类型有哪些?
Key-value,String,Hash,List,Set,Sorted sets,HyperLogLog,Bitmaps
六.redis 支持的 java 客户端都有哪些?
Aredis,java-redis-client,JDBC-Redis,Jedipus,Jedis,JRedis,lettuce,redis-protocol,RedisClient,Redisson,RJC,vertx-redis-client,viredis。
我们常用的是jedis,lettuce,redisson。我们集成spring的项目操作redis一般都会使用spring-data-redis作为redis客户端,其实他只是对jedis客户端进行了封装。
七.jedis和redisson有哪些区别?
大家留言答案,我会整理然后补充
八.怎么保证缓存和数据库数据的一致性?
缓存与数据库一致性的问题是我们经常遇到的,也是面试官必问的问题,我们仔细分析一下场景:
1. 读取缓存,如果缓存未匹配数据则读取数据库数据进行更新;更新数据时先更新数据后删除缓存。当更新数据后删除缓存失败了就会引起缓存与数据库数据不一致。
2.读取缓存,如果缓存未匹配数据则读取数据库数据进行更新;更新数据时先删除缓存在更新数据。
恰似看着没有问题,但是当遇到高并发的情况下也会造成数据不一致,在删除缓存后更新数据前,又来了读取缓存的请求,从数据库读取到修改前的数据且更新到缓存中!!!
3.为了兼容高并发场景我们对第2种方式进行升级改造。
我们用一个先进先出的队列来控制:读取更新和DB更新操作。每一次的DB更新操作和读取更新操作都要先存放到队列中,等待消费者消费。为什么要这么做呢,其实我们只是为了把读取更新和DB更新串行化,使他们有顺序,这样无论任何的高并发场景都可以满足。当然我们需要优化一些东西,在读取更新操作逻辑中,如果发现key已在缓存中存在,那么我们就没有必要做重复更新操作,因为读取更新的前提是缓存种没有key对应的value。
九.redis 持久化有几种方式?
引用:https://www.php.cn/redis/421586.html
Redis的所有数据都是保存在内存中,然后不定期的通过异步方式保存到磁盘上(这称为“半持久化模式”);也可以把每一次数据变化都写入到一个append only file(aof)里面(这称为“全持久化模式”)。
由于Redis的数据都存放在内存中,如果没有配置持久化,redis重启后数据就全丢失了,于是需要开启redis的持久化功能,将数据保存到磁盘上,当redis重启后,可以从磁盘中恢复数据。
redis提供两种方式进行持久化,一种是RDB持久化(原理是将Reids在内存中的数据库记录定时dump到磁盘上的RDB持久化),另外一种是AOF(append only file)持久化(原理是将Reids的操作日志以追加的方式写入文件)。
那么这两种持久化方式有什么区别呢,改如何选择呢?
RDB持久化是指在指定的时间间隔内将内存中的数据集快照写入磁盘,实际操作过程是fork一个子进程,先将数据集写入临时文件,写入成功后,再替换之前的文件,用二进制压缩存储。
AOF持久化以日志的形式记录服务器所处理的每一个写、删除操作,查询操作不会记录,以文本的方式记录,可以打开文件看到详细的操作记录。
二者优缺点
RDB存在哪些优势呢?
1). 一旦采用该方式,那么你的整个Redis数据库将只包含一个文件,这对于文件备份而言是非常完美的。比如,你可能打算每个小时归档一次最近24小时的数据,同时还要每天归档一次最近30天的数据。通过这样的备份策略,一旦系统出现灾难性故障,我们可以非常容易的进行恢复。
2). 对于灾难恢复而言,RDB是非常不错的选择。因为我们可以非常轻松的将一个单独的文件压缩后再转移到其它存储介质上。
3). 性能最大化。对于Redis的服务进程而言,在开始持久化时,它唯一需要做的只是fork出子进程,之后再由子进程完成这些持久化的工作,这样就可以极大的避免服务进程执行IO操作了。
4). 相比于AOF机制,如果数据集很大,RDB的启动效率会更高。
RDB又存在哪些劣势呢?
1). 如果你想保证数据的高可用性,即最大限度的避免数据丢失,那么RDB将不是一个很好的选择。因为系统一旦在定时持久化之前出现宕机现象,此前没有来得及写入磁盘的数据都将丢失。
2). 由于RDB是通过fork子进程来协助完成数据持久化工作的,因此,如果当数据集较大时,可能会导致整个服务器停止服务几百毫秒,甚至是1秒钟。
AOF的优势有哪些呢?
1). 该机制可以带来更高的数据安全性,即数据持久性。Redis中提供了3中同步策略,即每秒同步、每修改同步和不同步。事实上,每秒同步也是异步完成的,其效率也是非常高的,所差的是一旦系统出现宕机现象,那么这一秒钟之内修改的数据将会丢失。而每修改同步,我们可以将其视为同步持久化,即每次发生的数据变化都会被立即记录到磁盘中。可以预见,这种方式在效率上是最低的。至于无同步,无需多言,我想大家都能正确的理解它。
2). 由于该机制对日志文件的写入操作采用的是append模式,因此在写入过程中即使出现宕机现象,也不会破坏日志文件中已经存在的内容。然而如果我们本次操作只是写入了一半数据就出现了系统崩溃问题,不用担心,在Redis下一次启动之前,我们可以通过redis-check-aof工具来帮助我们解决数据一致性的问题。
3). 如果日志过大,Redis可以自动启用rewrite机制。即Redis以append模式不断的将修改数据写入到老的磁盘文件中,同时Redis还会创建一个新的文件用于记录此期间有哪些修改命令被执行。因此在进行rewrite切换时可以更好的保证数据安全性。
4). AOF包含一个格式清晰、易于理解的日志文件用于记录所有的修改操作。事实上,我们也可以通过该文件完成数据的重建。
AOF的劣势有哪些呢?
1). 对于相同数量的数据集而言,AOF文件通常要大于RDB文件。RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。
2). 根据同步策略的不同,AOF在运行效率上往往会慢于RDB。总之,每秒同步策略的效率是比较高的,同步禁用策略的效率和RDB一样高效。
二者选择的标准,就是看系统是愿意牺牲一些性能,换取更高的缓存一致性(aof),还是愿意写操作频繁的时候,不启用备份来换取更高的性能,待手动运行save的时候,再做备份(rdb)。rdb这个就更有些 eventually consistent的意思了。
十.redis 怎么实现分布式锁?
分布式锁构成的三个概念:
1.安全属性(Safetyproperty): 独享(相互排斥)。在任意一个时刻,只有一个客户端持有锁。
2.活性A(Livenessproperty A): 无死锁。即便持有锁的客户端崩溃(crashed)或者网络被分裂(gets partitioned),锁仍然可以被获取。
3.活性B(Livenessproperty B): 容错。 只要大部分Redis节点都活着,客户端就可以获取和释放锁.
两个大的场景,单机获取分布式锁和集群获取分布式锁,集群也包括主从模式。
单机版获取锁:
通过 SET resource_name my_random_value NX PX 30000命令获取锁。说一下该SET的参数,依次是:key,value,NX代表仅在key不存在的情况下才能执行成功,PX是过期时间单位,30000具体时间。key必须唯一,value是为了保障开锁者才有权解锁。
单机版释放锁:
通过lua脚本实现,有人说为什么不用del或者判断value值之后再进行del呢,原子性问题,单纯的del可能删除别人获取的锁,判断value后再del也会造成删除别人锁的问题,当A执行完if判断后再del之前,A获取的锁已过期,且这时B获取了此锁,这时A接着执行del是不是删除了B的锁。脚本如下:
if redis.call("get",KEYS[1]) == ARGV[1]then
returnredis.call("del",KEYS[1])
else
return 0
end
注意:如果你项目中还用setnx实现分布式锁那么注意了这是一个定时炸弹。setnx本意就是SET IF NO EXIST,只能SET值不能设置过期时间,然后你需要通过expire设置过期时间,哦炸了,这两个操作并不是原子性的,一单不能设置过期时间,锁可就是永久了,其他坑自己总结下。
现在咱们说集群或者主从获取分布式锁的问题,如果还向上述方式获取锁,那么在主库同步数据到从库之前挂掉了,哨兵发现主动切换备库为主库,这时锁已失效,其他客户端依然可以重复的获取此锁。怎么解决呢,Redlock算法,这个算法迄今为止争议不断有兴趣的可以去redis官网查看一下,下面摘录官方通过Redlock算法获取锁的描述:
在Redis的分布式环境中,我们假设有N个Redismaster。这些节点完全互相独立,不存在主从复制或者其他集群协调机制。之前我们已经描述了在Redis单实例下怎么安全地获取和释放锁。我们确保将在每(N)个实例上使用此方法获取和释放锁。在这个样例中,我们假设有5个Redismaster节点,这是一个比较合理的设置,所以我们需要在5台机器上面或者5台虚拟机上面运行这些实例,这样保证他们不会同时都宕掉。
为了取到锁,客户端应该执行以下操作:
1. 获取当前Unix时间,以毫秒为单位。
2.依次尝试从N个实例,使用相同的key和随机值获取锁。在步骤2,当向Redis设置锁时,客户端应该设置一个网络连接和响应超时时间,这个超时时间应该小于锁的失效时间。例如你的锁自动失效时间为10秒,则超时时间应该在5-50毫秒之间。这样可以避免服务器端Redis已经挂掉的情况下,客户端还在死死地等待响应结果。如果服务器端没有在规定时间内响应,客户端应该尽快尝试另外一个Redis实例。
3.客户端使用当前时间减去开始获取锁时间(步骤1记录的时间)就得到获取锁使用的时间。当且仅当从大多数(这里是3个节点)的Redis节点都取到锁,并且使用的时间小于锁失效时间时,锁才算获取成功。
4.如果取到了锁,key的真正有效时间等于有效时间减去获取锁所使用的时间(步骤3计算的结果)。
5.如果因为某些原因,获取锁失败(没有在至少N/2+1个Redis实例取到锁或者取锁时间已经超过了有效时间),客户端应该在所有的Redis实例上进行解锁(即便某些Redis实例根本就没有加锁成功)。
释放锁:
释放锁比较简单,向所有的Redis实例发送释放锁命令即可,不用关心之前有没有从Redis实例成功获取到锁(lua脚本)
十一.redis 分布式锁有什么缺陷?
单机版分布式锁,redis不是高可用,锁随时会失效。
集群版通过Redlock算法获取的分布式锁:
1.脑裂
集群状态下我们需要向所有redis服务发送SET命令。假如有A,B,C,D,E 5个redis集群,当客户端1需要获取key=2的分布式锁,只有A,B,C成功SET,D和E服务SET失败。同时客户端2也要获取key=2的分布式锁,D,E成功响应。此时客户端1和客户端2分别持有5个集群中部分SET成功实例,都在等待对方释放,这样我们可以称为死锁或者脑裂。
为了防止脑裂问题,第一:客户端尽可能的并发发送SET请求到所有redis实例,获取redis实例锁花费的时间越低出现脑裂的几率就越小。第二:如果客户端获取redis集群实例锁大部分都失败了,应该尽快释放分布式锁,防止和其他客户端出现冲突。
2.安全争议
很多大神都会讨论这个算法是否安全的问题,这里引用redis官网的叙述:
这个算法安全么?我们可以从不同的场景讨论一下。
让我们假设客户端从大多数Redis实例取到了锁。所有的实例都包含同样的key,并且key的有效时间也一样。然而,key肯定是在不同的时间被设置上的,所以key的失效时间也不是精确的相同。我们假设第一个设置的key时间是T1(开始向第一个server发送命令前时间),最后一个设置的key时间是T2(得到最后一台server的答复后的时间),我们可以确认,第一个server的key至少会存活 MIN_VALIDITY=TTL-(T2-T1)-CLOCK_DRIFT。所有其他的key的存活时间,都会比这个key时间晚,所以可以肯定,所有key的失效时间至少是MIN_VALIDITY。
当大部分实例的key被设置后,其他的客户端将不能再取到锁,因为至少N/2+1个实例已经存在key。所以,如果一个锁被(客户端)获取后,客户端自己也不能再次申请到锁(违反互相排斥属性)。
然而我们也想确保,当多个客户端同时抢夺一个锁时不能两个都成功。
如果客户端在获取到大多数redis实例锁,使用的时间接近或者已经大于失效时间,客户端将认为锁是失效的锁,并且将释放掉已经获取到的锁,所以我们只需要在有效时间范围内获取到大部分锁这种情况。在上面已经讨论过有争议的地方,在MIN_VALIDITY时间内,将没有客户端再次取得锁。所以只有一种情况,多个客户端会在相同时间取得N/2+1实例的锁,那就是取得锁的时间大于失效时间(TTL time),这样取到的锁也是无效的.
十二.redis 如何做内存优化?
这里先叙述一下我们编程中常用到的优化方式:
1.尽可能使用位级别或者字级别的操作:GETRANGE, SETRANGE, GETBIT 和 SETBIT。举例:需要在缓存中存储用户是男是女,我们应该用1表示男,2表示女每次只占用1个字节。
2.尽可能使用散列表:小散列表(是说散列表里面存储的数少)使用的内存非常小,所以你应该尽可能的将你的数据模型抽象到一个散列表里面。比如你的web系统中有一个用户对象,不要为这个用户的名称,姓氏,邮箱,密码设置单独的key,而是应该把这个用户的所有信息存储到一张散列表里面.
在叙述下redis安装方面优化方式:
引用:http://www.redis.cn/topics/memory-optimization.html
1.小的聚合类型数据的特殊编码处理
Redis2.2版本及以后,存储集合数据的时候会采用内存压缩技术,以使用更少的内存存储更多的数据。如Hashes,Lists,Sets和SortedSets,当这些集合中的所有数都小于一个给定的元素,并且集合中元素数量小于某个值时,存储的数据会被以一种非常节省内存的方式进行编码,使用这种编码理论上至少会节省10倍以上内存(平均节省5倍以上内存)。并且这种编码技术对用户和redisapi透明。因为使用这种编码是用CPU换内存,所以我们提供了更改阈值的方法,只需在redis.conf里面进行修改即可.
hash-max-zipmap-entries 64(2.6以上使用hash-max-ziplist-entries)
hash-max-zipmap-value 512 (2.6以上使用hash-max-ziplist-value)
list-max-ziplist-entries 512
list-max-ziplist-value 64
zset-max-ziplist-entries 128
zset-max-ziplist-value 64
set-max-intset-entries 512
(集合中)如果某个值超过了配置文件中设置的最大值,redis将自动把把它(集合)转换为正常的散列表。这种操作对于比较小的数值是非常快的,但是,如果你为了使用这种编码技术而把配置进行了更改,你最好做一下基准测试(和正常的不采用编码做一下对比).
2.使用32位的redis
使用32位的redis,对于每一个key,将使用更少的内存,因为32位程序,指针占用的字节数更少。但是32的redis整个实例使用的内存将被限制在4G以下。使用make 32bit命令编译生成32位的redis。RDB和AOF文件是不区分32位和64位的(包括字节顺序),所以你可以使用64位的reidis恢复32位的RDB备份文件,相反亦然.
3.内存分配:
为了存储用户数据,当设置了maxmemory后Redis会分配几乎和maxmemory一样大的内存(然而也有可能还会有其他方面的一些内存分配).
精确的值可以在配置文件中设置,或者在启动后通过 CONFIG SET 命令设置(see Usingmemory as an LRU cache for more info). Redis内存管理方面,你需要注意以下几点:
当某些缓存被删除后Redis并不是总是立即将内存归还给操作系统。这并不是redis所特有的,而是函数malloc()的特性。例如你缓存了5G的数据,然后删除了2G数据,从操作系统看,redis可能仍然占用了5G的内存(这个内存叫RSS,后面会用到这个概念),即使redis已经明确声明只使用了3G的空间。这是因为redis使用的底层内存分配器不会这么简单的就把内存归还给操作系统,可能是因为已经删除的key和没有删除的key在同一个页面(page),这样就不能把完整的一页归还给操作系统.
上面的一点意味着,你应该基于你可能会用到的 最大内存来指定redis的最大内存。如果你的程序时不时的需要10G内存,即便在大多数情况是使用5G内存,你也需要指定最大内存为10G.
内存分配器是智能的,可以复用用户已经释放的内存。所以当使用的内存从5G降低到3G时,你可以重新添加更多的key,而不需要再向操作系统申请内存。分配器将复用之前已经释放的2G内存.
因为这些,当redis的peak内存非常高于平时的内存使用时,碎片所占可用内存的比例就会波动很大。当前使用的内存除以实际使用的物理内存(RSS)就是fragmentation;因为RSS就是peakmemory,所以当大部分key被释放的时候,此时内存的mem_used / RSS就比较高.
如果 maxmemory没有设置,redis就会一直向OS申请内存,直到OS的所有内存都被使用完。所以通常建议设置上redis的内存限制。或许你也想设置maxmemory-policy 的值为 noeviction(在redis的某些老版本默认 并 不是这样)
设置了maxmemory后,当redis的内存达到内存限制后,再向redis发送写指令,会返回一个内存耗尽的错误。错误通常会触发一个应用程序错误,但是不会导致整台机器宕掉.
十三.redis 淘汰策略有哪些?
引用:http://www.redis.cn/topics/lru-cache.html
以下的策略是可用的:
noeviction:返回错误当内存限制达到并且客户端尝试执行会让更多内存被使用的命令(大部分的写入指令,但DEL和几个例外)
allkeys-lru: 尝试回收最少使用的键(LRU),使得新添加的数据有空间存放。
volatile-lru:尝试回收最少使用的键(LRU),但仅限于在过期集合的键,使得新添加的数据有空间存放。
allkeys-random: 回收随机的键使得新添加的数据有空间存放。
volatile-random: 回收随机的键使得新添加的数据有空间存放,但仅限于在过期集合的键。
volatile-ttl:回收在过期集合的键,并且优先回收存活时间(TTL)较短的键,使得新添加的数据有空间存放。
如果没有键满足回收的前提条件的话,策略volatile-lru, volatile-random以及volatile-ttl就和noeviction 差不多了。
选择正确的回收策略是非常重要的,这取决于你的应用的访问模式,不过你可以在运行时进行相关的策略调整,并且监控缓存命中率和没命中的次数,通过RedisINFO命令输出以便调优。
一般的经验规则:
使用allkeys-lru策略:当你希望你的请求符合一个幂定律分布,也就是说,你希望部分的子集元素将比其它其它元素被访问的更多。如果你不确定选择什么,这是个很好的选择。.
使用allkeys-random:如果你是循环访问,所有的键被连续的扫描,或者你希望请求分布正常(所有元素被访问的概率都差不多)。
使用volatile-ttl:如果你想要通过创建缓存对象时设置TTL值,来决定哪些对象应该被过期。
allkeys-lru 和 volatile-random策略对于当你想要单一的实例实现缓存及持久化一些键时很有用。不过一般运行两个实例是解决这个问题的更好方法。
为了键设置过期时间也是需要消耗内存的,所以使用allkeys-lru这种策略更加高效,因为没有必要为键取设置过期时间当内存有压力时。
十四.redis 常见的性能问题有哪些?该如何解决?
这个大家去redis官网看下主从复制,持久化,高可用等特性安装设置,这东西得靠积累,如果你没有搭建过redis集群是体会不到的,即便背过答案也无济于事。http://www.redis.cn/documentation.html