玖叶教程网

前端编程开发入门

含泪整理Redis相关面试题大全

1、什么是redis?redis有哪些优缺点?

redis是一个C语言编写的开源的高性能NOSQL键值对数据库吗,支持5种数据类型:字符串,列表,集合,散列表,有序集合。

与传统数据库不一样,Redis数据存储在内存中,读写速度非常快,redis被广泛应用于缓存,每秒可处理超过10w次的读写操作。

优点
    读写性能优异,读11w次/s, 写8w次/s

    支持事务,redis所有操作都是原子性的,还支持几个操作合并也是原子性的

2、Redis有哪些应用场景?

1、DB缓存,减轻服务器压力

2、提高系统响应

3、做Session分离

4、做分布式锁 使用setNX

5、做乐观锁 Redis的watch+incr

3、为什么要用redis而不用map/guava?

本地缓存和分布式缓存的原因

4、说一说缓存的读写模式?

1、Cache Aside Pattern(旁路缓存,常用)

是最经典的缓存+数据库读写模式。先读缓存,没有则读数据库,去除数据放缓存,同时返回响应。更新的时候,先更新数据库,再删除缓存。

为什么 是删除缓存,不是更新缓存呢?

    1、缓存的值是一个结构,hash,list,更新需要遍历

    2、懒加载,使用的时候才更新缓存(亦可以异步填充)

高并发脏读的三种情况
    1、先更新数据库,再更新缓存

    2、先删除缓存,再更新数据库

    3、先更新数据库,再删除缓存(推荐)

2、Read/Write Thread Pattern

应用只操作缓存,缓存操作数据库

ReadThread:应用程序读缓存,缓存没有,由缓存回源数据库,并写入缓存

WriteThread:应用程序写缓存,缓存写数据库,比较复杂

3、Write Behind Cachiing Parttern

应用程序只更新缓存,缓存通过异步将数据批量更新到数据库,不能实时同步,甚至会丢数据

5、Redis为什么是单线程,高并发这么快是为什么呢?

1、高并发快的原因
    1、redis是基于内存的,内存的读写速度非常快

    2、redis是单线程的,省去了很多上下文切换线程的时间

    3、redis使用IO多路复用技术(epoll),可以处理并发的连接。多路是指多个网络连接,复用是复用同一个线程。可以让单线程高效处理多个连接请求

    4、数据结构为key-value,读取数据快。比如还有压缩表,跳跃表等加快数据读取

    5、redis有使用自己的事件分离器,效率比较高,内部采用非阻塞方式,吞吐能力比较大

2、为什么是单线程
    因为redis是基于内存,CPU不是redis瓶颈,瓶颈可能是机器内存或者网络带宽,既然单线程容易实现且CPU不能成为瓶颈,就顺利采用单线程方式

3、单线程的优势
    代码清晰,逻辑简单,不用考虑各种锁问题,不需要上下文切换导致消耗CPU

6、说一说Redis有哪些数据类型?

1、String类型
    可存储字符串,整数,浮点型,数字可以自增自减
    
2、List类型
    是一个双向列表,可以从两端压入或弹出,存储一些列表的数据结构
    
3、SET类型(无序集合)
    用于一些不重复并且不需要顺序的数据结构
    
4、Hash类型(散列表)

5、ZSET类型(有序集合)

7、说一说Redis的RDB持久化和AOF持久化?

7.1、简单介绍

1、RDB持久化:可以指定的时间间隔能对数据进行快照存储,然后写入内存,以便在REDIS重启时,可以通过RDB还原数据库

2、AOF持久化:记录每次对服务器写的操作命令(RESP),当服务器重启时会重新执行这些命令来恢复数据,AOF每次写操作以append方式追加在文件末尾,redis还能在后台对AOF重写,使得AOF达到瘦身的效果

3、如果redis开启了AOF,优先使用AOF,只有AOF关闭,才会使用RDB

7.2、RDB持久化

7.2.1、RDB文件格式(可用winhex打开)

RDB文件是一个经过压缩的二进制文件(默认名:dump.rdb),由多个部分组成,RDB格式为: “REDIS”| RDB_VERSION | AUX_FIELD_KEY_VALUE_PAIRS | DB_NUM | DB_DICT_SIZE | EXPIRE_DICT_SIZE | KEY_VALUE_PAIRS | EOF | CHECK_NUM

1、头部5字节固定为“REDIS”字符串
2、4个字节的RDB版本号(不是REDIS的版本号),当前为9,填充为0009
3、辅助字段,以KEY_VALUE形式,比如:redis-ver(redis版本), redis-bits(64/32),ctime(当期时间戳),used-mem(使用内存)
4、存储数据库号码
5、字典大小
6、过期Key
7、主要数据,以key-value形式存储
8、结果标志
9、校验和,就是看文件是否损坏或被修改

7.2.2、RDB触发方式

1、配置参数定期执行
    save "" # 不使用RDB存储, 不能主从
    save 900 1  # 表示15分钟至少有1个键被更改则进行快照
    save 300 10 # 表示5分钟至少有10个键被更改则进行快照
    save 60 10000   # 表示1分钟至少有10000个键被更改则进行快照
2、命令显式触发
    bgsave

7.2.3、RDB执行流程

1、流程图

2、流程说明

1、比如使用bgsave触发,Redis父进程首先判断,当前是否执行save,或者bgsave/bgwriteaof(aof重写命令)的紫禁城,如果在执行则bgsave命令直接返回

2、父进程执行fork创建子进程,这个过程父进程是阻塞的,redis不能执行来自客户端的命令

3、父进程fork后,bgsave命令返回“background saving started”,并可以响应其他命令

4、子进程创建RDB文件,根据父进程内存快照生成临时快照文件,完成后对原有文件进行原子替换(RDB始终完整)

5、子进程发送信号给父进程表示完成,父进程更新统计信息

7.3、AOF持久化

7.3.1、AOF原理

AOF存储的是redis命令,同步命令到AOF文件整个分为三个阶段:命令传播,缓存追加,文件保存和写入

1、命令传播:通过网络将协议文本发送给Redis,服务器接受后,根据协议内容,选择适当的函数,将参数从字符串文本转换为stringObject,然后命令参数传播到AOF程序

2、缓存追加:AOF根据命令以及命令参数,将命令从字符串转换为原来的协议文本,然后追加到redis.h/redisServer的aof_buf末尾

3、文件写入和保存:服务器会调用flushAppendOnlyFile函数,条件是WRITE时将aof_buf中的缓存写入AOF文件,条件是SAVE时,调用fsync或fdatasync,将AOF保存到磁盘

7.3.2、AOF保存模式

1、AOF_FSYNC_NO:不保存
    每次调用flushAppendOnlyFile, WRITE会被执行,SAVE会被略过
    
2、AOF_FSYNC_EVERTSEC:每秒钟保存一次(默认)
    这种模式下,SAVE原则上每隔1s钟就会执行一次,因为SAVE是后台子线程,所以不会引起服务器主进程阻塞
    
3、AOF_FSYNC_ALWAYS:每执行一个命令保存一次(不推荐)
    每执行一个命令之后,WRITE和SAVE都会被执行。SAVE是由主进程执行,主进程被阻塞,不能接受请求

7.3.3、AOF重写

AOF记录数据越多,体积就会越大,需要重写瘦身。Redis可以自动在后台队AOF进行重写,重写后包含回复当前数据集所需要的最小命令集合

子进程重写期间,主进程还需要继续处理命令,新的命令可能对现有数据进行修改,Redis就增加了AOF重写缓存。

fork出子进程后,redis主进程接受命令后,除了将写命令追加到现有的AOF,还会追加到AOF重写缓存中。子进程重写完毕后,会通知主进程,会把AOF重写缓存的内容全部写入新的AOF中

7.3.4、AOF配置(命令bgwriteaof)

appendonly  yes # 开启aof   

auto-aof-rewrite-percentage 100 # 表示aof超过上一次aof的百分之多少会进行重写

auto-aof-rewrite-min-size 64mb # 限制允许重写的aof文件大小,也就是小于64mb时,不需要优化

7.3.5、混合持久化

redis4.0开始支持rdb和aof混合持久化,rdb头+aof身体--》appendonly.aof

开启混合持久化  aof-use-rdb-preamble yes

7.4、持久化方式总结与抉择

7.4.1、RDB和AOF对比

1、RDB是某个时刻的快照数据,使用二进制存储,AOF存操作命令,采用文本存储(混合)

2、RDB性能高,AOF性能比较低

3、RDB会丢失最后一次快照以后更改的所有数据,AOF设置每秒保存一次,则最多丢2秒的数据

4、Redis主服务器模式运行,RDB不会保存过期key,RDB以从服务器运行,会保存过期key,主从同步时,再清空过期key。AOF写入时,过期key会追加一条del命令,执行aof重写时,会忽略过期key和del命令

7.4.2、如何选择

内存数据库:rdb+aof  数据不容易丢失

缓存服务器:rdb  高性能, 不建议只使用aof(性能差)

追求高性能:可以都不开,redis宕机,从数据源恢复

字典库:可以选择不驱逐,保证数据完整性

8、Redis过期键删除有哪些策略?

redis性能高,官方表示读11w次/s,写81000次/秒,长期使用key不断增加,redis作为缓存,物理内存也会满,内存与磁盘交换虚拟内存,频繁IO导致性能急剧下降.

maxmemory
    默认不设置,一般设置物理内存3/4,趋近maxmemory后,会通过缓存淘汰策略,从内存中删除

expire数据结构
    expire命令在到达过期时间后回自动删除key。
删除策略
    定时删除:设置过期时间时,创建一个定时器,,让定时器在过期时间来临时立即执行删除,需要创建定时器,耗费CPU,不推荐

    惰性删除: 在key被访问时如果发现它已经失效,那么就删除.调用expireIfNeeded函数,读取之前检查一下有没有失效,失效则删除

    主动删除:  在redis.conf配置主动删除策略,默认是no-enviction(不删除)

        1、allkeys-lru:在不确定时一般采用LRU

        2、volatile-lru:比allkeys-lru性能差,存过期时间

        3、allkeys-random:随机淘汰,希望请求平均分布时可以选择

        4、volatile-ttl 自己控制

        5、no-enviction 禁止驱逐(如字典表)

9、谈一谈REDIS中的事务?

9.1、事务命令

multi:用于标记事务开始,redis会将后续命令逐个放入队列中,然后使用exec原子化的执行这个命令队列

exec:执行命令队列

discard:清除命令队列

watch:监视 key

unwatch:清除监视key

9.2、事务机制

1、事务执行:在redisClient中,有flags属性,用来标识是否在事务中  flags=REDIS_MULTI

2、命令入队:RedisClient将命令存放在事务队列中(multi, exec,discard,watch除外)

3、事务队列:multiCmd *commands 用于存放命令

4、执行事务:RedisClient向服务端发送exec命令,RedisServer遍历队列,最后将执行结果一次性返回.如果某条命令发生错误,Redisclient将flags置为REDIS_DIRTY_EXEC,exec命令将会失败返回

9.3、watch执行

使用watch监视数据库键,redisDb有一个watch_keys字典,key是某个被监视的数据的key,值是一个链表,记录了所有监视这个key的客户端

监视机制触发:数据修改后,监视这个数据的客户端的flags置为REDIS_DIRTY_CAS

事务执行:redisclient向服务器发送exec,服务端判断Redisclient的flags,如果是REDIS_DIRTY_CAS,则清空事务队列

9.4、Redis的弱事务性

1、redis语法错误,整个事务的命令在队列里都清除

2、redis运行错误:在队列里正确的命令可以执行且不支持回滚,因为大多数事务失败都是语法错误或者类型错误,一般开发阶段可预见,redis为了性能就忽略了事务

9.5、lua脚本可以保证事务原子性

1、lua命令
    1、eval script numkeys key [key ...] arg [arg ... ]   
        比如:eval"return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second
        
    2、redis.call和reids.pcall    
        比如:eval"return redis.call('set',KEYS[1],ARGV[1])"1 n1 zhaoyun
        
    3、evalsha script load "return redis.call('set',KEYS[1],ARGV[1])" 
        会返回一个sha的编码,执行evalsha + 编码即可执行脚本
    
    4、直接编写lua脚本,使用redis-client --eval 指定 脚本和参数
    
2、利用Redis整合Lua,主要是为了性能以及事务的原子性。因为redis帮我们提供的事务功能太差。

3、lua脚本复制分为脚本传播模式和命令传播模式
    1、脚本传播,脚本包含时间,内部状态、随机函数等不能出现
    
    2、命令传播,所有写命令用事务包裹,复制到AOF文件及服务器。
    
    3、redis.replicate_commands()

9.6、管道,事务,脚本的区别

管道无原子性,命令是独立的,脚本事务性强于事务,脚本执行期间,客户端其他脚本或事务无法执行,所以脚本时间应该尽量短

10、REDIS哨兵、复制、集群的设计原理,以及区别?

10.1、主从复制原理

redis为了单点数据库问题,会把数据复制多个副本到其他节点,以实现redis高可用,数据冗余备份

1、从数据库向主数据库发送sync命令,主数据库接受后创建一个rdb快照文件

2、主数据库发送rdb文件给从服务器,从服务器接受并载入该文件

3、主服务器将同步阶段接收到的新命令写入缓冲区,然后将缓冲区所有的写命令发送给从服务器执行

4、处理完之后,主数据库每写一个命令,就会发送给从服务器

注意:2.8之后,会根据从服务器断开之前最新命令的偏移量进行增量同步。2.8之后使用psync发送同步命令,并且带上runid和偏移量offerset,主服务器会判断是否是第一次同步,是的话走全量同步,不是的话根据偏移量进行增量同步

10.2、主从复制存在的问题?

主服务器挂了,从服务器可读不可写,无法实现自动化故障转移

10.3、使用哨兵机制实现自动化故障转移

10.3.1、主要功能

1、集群监控,负责监控redis master和slave进程是否正常工作

2、消息通知:如果某个redis有故障,哨兵负责发送消息作为报警通知给管理员

3、故障转移:如果master挂了,会自动转移到slave上

4、配置中心:如果故障转移发生了,通知客户端新的master地址

10.3.2、redis哨兵高可用

redis简历多个哨兵,共同监控数据节点的运行

哨兵之间互相通信,交换对主从节点的监控情况

每隔1s每个哨兵会向整个集群方ping命令做心跳检测

10.3.3、哨兵中的主观下线和客观下线

主观下线:一个哨兵发现ping主节点时没有响应,主观认为down掉了

客观下线:首先发现主节点下线的哨兵将信息发送给其他哨兵,他们也进行ping操作,多个哨兵交换主观判断结果,超过半数以上的哨兵认为主挂了,才判断主节点那客观下线了。

投票选举:那个节点最先判断主节点下线,就发起投票机制(raft算法),最终投为主节点的哨兵节点完成主从自动切换过程

10.4、集群

为了解决redis单机容量有限问题,将数据进行分片集群处理,存储到多台服务器,内存不受限与单机

10.4.1、edis Cluster采用去中心化模式,使用gossip协议

meet:sender向receiver发出,请求receiver加入sender集群

ping:节点检测其他节点是否在线

pong:receiver收到meet或ping后回复,在Failover后,新的master也会广播pong

fail:节点A判断B下线后,A节点广播B的fail消息,其他节点收到后标记B下线

publish:节点A收到publish,节点A执行该命令,并广播集群,收到广播的节点执行相同的命令

10.4.2、slot(hash槽)

rediscluster把所有的物理节点映射到[0-16384]个slot上,采用平均和连续分配方式。

采用crc32算法计算hash槽。为什么是16384个槽,因为作者认为1000台redis之后会出现问题,16384个槽也够用了

10.4.3、集群搭建

开启集群

cluster-enabled yes

创建集群:

redis-client --cluster ip1:port1 ipn:portn --cluster-replicas 1  #【指定副本数量,也就是每个几点的副本节点数量】

集群信息可以在node.conf中 迁移:新的master节点加入后者删除,需要进行槽和槽数据的迁移

1、节点B发送状态变更状态命令,将B的slot状态置为importing

2、向节点A发送变更命令,将A的slot状态置为migrating

3、向A发送migrate命令,告知A将要迁移的slot对应的key迁移到B

4、所有key迁移完后,cluster setslot重新设置槽位

扩容:

./redis-cli --cluster add-node 192.168.127.128:7007   192.168.127.128:7001
                               # 新加入者               (集群发起者)

重新分槽

./redis-cli --cluster reshard 192.168.127.128:7007

添加从节点

./redis-cli --cluster add-node 192.168.127.128:7008 192.168.127.128:7007 --cluster-slave --cluster-master-id 6ff20bf463c954e977b213f0e36f3efc02bd53d6

11、Redis并发竞争key有什么解决方案?

并发竞争:redis多个client同时set key引起的并发问题, 如何解决并发竞争key问题?

11.1、使用分布式锁

1、整体技术方案:主要是准备一个分布式锁,大家去抢锁,抢到锁就做set操作

2、为什么分布式锁:因为传统加锁,只适合单点。不管事zookeeper或redis实现,基本原理都是用一个状态值表示锁,对锁的占用和释放通过状态值来标识

3、分布式锁的要求:互斥,无死锁,容错

4、分布式锁的实现,数据库,redis(setnx),zookeeper(临时节点)

11.2、使用消息队列

并发过大的情况,可以以通过消息队列处理,把并行读写串行化,把redis set操作放在队列里,必须一个一个的执行,这也是高并发的一种通用解决方案

12、REDIS如何实现分布式锁?

12.1、使用setnx

1、获取锁
    使用redis.set(lockKey, requestId, "NX", "PX", expireTime)  # 操作是原子性,并发不会有问题
    使用result  = redis.setnx(lockKey, requestId)   ,if(result == 1){ jedis.expire(lockKey, expireTime) } # 并发会有问题
2、释放锁
    使用del 先查询requestId是否等于查询结果,是的话就删除lockKey,也就是释放锁  # 存在并发问题
    使用redis+lua脚本释放锁(推荐)
        将操作封装在lua脚本中 ,因为lua脚本具有原子性
3、存在问题
    单机无法保证高可用。主从无法保证强一致性,主机宕机会造成锁的重复获取。无法续租,超过过期时间后,不能继续使用

12.2、使用使用redission

代码实现

实现原理

如果客户端面对的是一个redis集群,根据hash节点随机选择一台,发送lua脚本到redis服务器

获取锁原理
    锁互斥机制:首先判断锁是否存在,如果已经有了,判断锁的ID是不是自己,此时返回锁的剩余生存时间
    自动延时机制:获取锁后,回启东一个watch dog,是一个后台线程,每隔10s检查一下,如果还持有锁,不断延长key的生存时间
    可重入锁机制:某个客户端获取锁后,再次获取,使用incrby 进行累加1

释放锁原理
    每次对锁的加所次数-1,为0后调用del命令删除key

13、谈一谈你对REDIS缓存穿透,缓存击穿,缓存雪崩的理解?

13.1、缓存穿透

key对应的数据在数据源中不存在,每次针对key的请求从缓存获取不到,请求会到数据源,从而可能压垮数据源

解决:
    1、布隆过滤器:将所有可能存在的数据hash到一个足够大的bitmap,一个一定不存在的数据会被这个bitmap拦截,从而避免底层查询压力
    2、查询空值,我们将空值缓存,但是过期时间很短,最长不超过5分钟

13.2、缓存击穿

key对应的数据存在,但是在redis中过期,若此时有大量的并发请求过来,这些请求发现缓存过期会从DB中获取并回设到缓存,这时候可能瞬间把DB压垮

解决:
    使用互斥锁:在缓存失效的时候,不立即去load db,而是使用互斥锁保护DB资源,只有一个线程去获取DB数据,然后回填到缓存,其他线程排队等待回设完毕或充缓存中获取

13.3、缓存雪崩

当服务器重启或大量缓存集中在某个时间段失效,  这也会给后端带来极大的压力

解决:
    1、加锁或者使用队列串行化读写,真正高并发很少使用,性能低

    2、将缓存失效时间分散开,缓存同时失效的重复率会降低

14、REDIS缓存和MYSQL数据一致性的解决方案?

读取缓存一般没啥问题,但是涉及到数据更新,就容易出现redis和mysql数据不一致问题

1、如果删除redis缓存,还没来得及写入mysql,此时一个线程俩来读取,缓存为空,则去数据库读取然后写缓存,此缓存中为脏数据

2、如果先写库数据库,在删除缓存前,数据库宕机了,没有删掉缓存,此时数据也不一致了

解决方案

14.1、延时双删

先删缓存,再写数据库,休眠一段时间,再次删除缓存(休眠时间根据业务定)

设置缓存过期时间,可以保证最终一致性

弊端:结合双删策略+缓存超时时间,最差就是在超时时间内数据不一致,而且又增加了写请求的耗时

14.2、异步更新缓存

mysql binlog增量订阅消费+消息队列+增量数据更新到redis

1、读redis:热数据基本在redis

2、写mysql:增删改都操作mysql

3、更新redis,mysql操作Binlog,来更新redis
    1、将数据全量写入redis
    2、增量:实时更新

读取Binlog后,利用消息队列推送更新各台redis缓存数据。可以结合阿里的canal,实现对Binlog的订阅

15、热点数据和冷数据?

热数据就是经常会被访问的数据,一般位于redis,命中率尽量要高

冷数据是指不经常被访问的数据,一般位于DB中

冷热数据交换:可以使用maxmemory+allkeys-lru

冷热交换比例:热20w,冷200w

16、什么是缓存热点Key?

就是热key突然过期,然后又大量的并发请求过来,可能会将缓存击穿,直达数据库,导致DB压力过大甚至压垮

17、假如Redis有1亿个key,有10w个key以固定前缀开头,如何查询?

redis是单线程,keys会阻塞,线上服务会停顿。

可以使用scan命令,可以无阻塞的提取指定模式的key列表,但是会有一定的重复概率,在客户端做一次去重即可,整体时间可能比keys长



发表评论:

控制面板
您好,欢迎到访网站!
  查看权限
网站分类
最新留言