Redis面试题
一、Redis缓存
1、缓存预热、缓存穿透、缓存雪崩、缓存击穿
-
缓存预热: 系统上线后,提前将相关数据加载到缓存中,避免用户先查库,然后再查缓存。
-
缓存穿透: 指缓存和数据库中都没有的数据,导致所有的请求都落在数据库上,造成数据库短时间内承受大量请求而崩掉。
解决方案:
1)缓存空对象:如果一个查询返回的数据为空(不管是数据是否不存在),我们仍然把这个空结果(null)进行缓存,设置空结果的过期时间会很短,最长不超过五分钟。
2)布隆过滤器:是指在客户端和redis之间又加了一层布隆过滤器,将所有可能存在的数据哈希到一个足够大的 bitmap 中,一个一定不存在的数据会被这个 bitmap 拦截掉,从而避免了对底层存储查询压力,但不一定是准确的,返回存在却不一定是存在。
Redisson实现的布隆过滤器:底层主要是先去初始化一个比较大的数组,里面存放的是二进制0或1,在一开始全是0,当存储一个key时,经过3次hash计算,模于数组长度找到数据的下标后把数组中原来的0改为1,这样就能通过三个数组的位置标明一个key的存在。
缺点:存在一定的误判率,一般可以设置这个误判率,5%以内一般都能接受。 -
缓存雪崩: 指缓存同一时间大面积的失效,所以,后面的请求都会落到数据库上,造成数据库短时间内承受大量请求而崩掉。
解决方案:
1)给不同key的TTL(过期时间)添加随机值,将失效时间分散开来。
2)缓存预热:指系统上线后,将相关的缓存数据直接加载到缓存系统。这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题 -
缓存击穿: 缓存击穿问题也叫热点key问题,就是被一个高并发访问并且缓存重建业务较复杂的key突然失效了,无数请求访问会在瞬间给数据库带来巨大的冲击。
解决方案:
1)互斥锁:用锁的方式只让一个线程来重建缓存数据,其他线程等待缓存构建。适用于强一致性,性能没那么高
2)逻辑过期:设置热点key永不过期,一个线程来获取互斥锁开启写入线程,其他线程获取互斥锁失败,则获取缓存中的旧数据。适用于不严格要求数据一致性。
2、redis作为缓存,MySQL的数据如何与缓存进行同步(保证双写一致性)
-
采用redisson实现的读写锁来保证强一致性:
在读的时候添加 共享锁,可以保证读读不互斥,读写互斥。更新数据时,使用 排他锁,读写、读读都互斥,这样就能保证在写数据的同时是不会让其他线程读数据的,避免了脏数据。需注意要将读方法和写方法上的锁使用同一把锁。
排他锁底层使用的是setnx,保证了同时只能有一个线程操作锁住的方法。
延迟双删: 如果是写操作,先把缓存中的数据删除,然后更新数据库,最后再延时删除缓存中的数据,其中这个延时多久不太好确定,在延时的过程中可能出现脏数据,并不能保证强一致性,所以没有采用它。 -
数据同步允许有一定的延迟:
采用阿里的canal组件实现数据同步,不需要更改业务代码,部署一个canal服务。canal服务把自己伪装成mysql的一个从节点,当mysql数据更新以后,canal会读取binlog数据,然后在通过canal的客户端获取到数据,更新缓存即可。
3、redis作为缓存,如何持久化数据?
- redis中提供了两种数据持久化的方式:RDB、AOF。
- RDB是一个快照文件,他是把redis内存存储的数据写到磁盘上,当redis实例宕机恢复数据的时候,方便从RDB的快照文件中恢复数据。
- AOF的含义是追加文件,当redis操作写命令的时候,都会存储这个文件中,当redis实例宕机恢复数据的时候,会从这个文件中再次执行一遍命令来恢复数据。
- 两种方式中,RDB恢复的速度比较快。因为是二进制文件,在保存的时候体积也比较小,所以恢复的比较快,但是它有可能会丢数据,通常项目中也会使用AOF来恢复数据,虽然AOF恢复数据比较慢,但是数据安全要高些,在AOF文件中可以设置刷盘策略,如可设置每秒批量写入一次命令。
4、redis的key过期后会立即删除吗?(redis的数据过期策略)
- Redis对数据设置数据的有效时间,数据过期以后,就需要将数据从内存中删除掉,可以按照不同的规则进行删除,这种删除规则就被称之为数据的过期删除策略。
5、假如缓存过多,内存有限,被占满了怎么办?(redis的数据淘汰策略)
- 当Redis中的内存不够用时,此时在向Redis中添加新的key,那么Redis就会按照某一种规则将内存中的数据删除掉,这种数据的删除规则被称之为内存的淘汰策略。
- 在redis这提供了8种不同的策略,默认使用的是noeviction,不删除任何数据,内存不足直接报错。
- 数据库有1000万数据,Redis只能缓存20w数据,如何保证Redis中的数据都是热点数据?
使用allkeys-lru策略,挑选最近最少使用的数据淘汰,留下的都是经常访问的热的数据。 - Redis的内存用完了会发生什么,主要看数据淘汰策略是什么,如默认使用的是noeviction,会直接报错。
6、
二、Redis分布式锁
1、Redis分布式锁如何使用?(使用场景)
-
举例:
抢券功能:如果是单体项目,直接用synchronized(this)同步代码块就能解决并发问题。但如果是服务集群部署,将同一份代码部署在多台机器中,则不能再使用同步锁,此时则可以使用redis的分布式锁来解决。 -
分布式锁所要解决的问题的本质是:能够对分布在多台机器中的线程对共享资源的互斥访问。
-
Redis实现分布式锁主要利用redis的 setnx 命令。setnx是SET if not exists(如果不存在,则SET)的简写。
// 添加锁 NX是互斥,EX是设置超时时间
// 注意不能将该条命令分成两条命令实现,因其两条命令不能保证原子性
SET lock value NX EX 10
// 释放锁 删除
DEL key
2、Redis分布式锁如何合理的控制锁的有效时长?
-
在获取Redis分布式锁锁成功后执行业务,如果执行业务的时间超时过期 或 在执行业务的途中服务宕机了,此时Redis会自动释放锁,从而可能造成数据的不一致性。
-
解决方法:
1)根据业务执行时间预估,但一般都可能无法预期,所以不建议使用
2)给锁续期,再创建一个新的线程来监控业务执行了多久,如果监控到业务执行的时间比较长,就增加线程持有锁的时长,redisson实现的分布式锁已经实现了该功能。
3、Redisson实现的分布式锁执行流程(实现原理)
-
Redisson实现的分布式锁也是基于setnx 命令实现的,只是对其做了很多增强和优化。
-
一个线程获取到分布式锁之后,就可以操作redis数据库了,不同的是Redisson会另开一个线程,类似看门狗一样,用来监控这个持有锁的线程,一般是每隔(releaseTime过期时间 / 3)的时间给持有锁的线程做一次续期。假设过期时间为30秒,则每10秒的时间都会给持有锁的线程续期,每次续期时间都为30秒。
-
当业务完成以后,手动释放锁,同时需要通知这个监控线程锁已被删除了,则不需要再做监听了。
-
若线程一开始获取分布式锁失败了,并不会直接中断失败,而是运用了while循环来不断的尝试获取锁,设置了一定的循环次数去获取锁,如果超过了这个次数就直接失败了。(重试机制)
-
所有的加锁、释放锁、设置过期时间等操作都是基于lua脚本完成的。lua脚本的作用就是能保证基于redis实现的多行代码中的原子性。
public void redisLock() throws InterruptedException {
// 获取锁(重入锁),设置执行锁的名称
RLock lock = redissonClient.getLock("lock1");
// 尝试获取锁,参数分别是:分布式锁的最大等待时间 、 锁自动释放时间 、 时间单位
// 第二个参数可不填,如果传入了第二个参数,代表可自己能确定业务的执行时间,则不会再另起一个线程做监控。
// boolean isLock = lock.tryLock(10, 30, TimeUnit.SECONDS)
boolean isLock = lock.tryLock(10, TimeUnit.SECONDS);
// 判断是否获取成功
if(isLock) {
try {
System.out.println(""执行业务);
} finally {
// 释放锁
lock.unlock();
}
}
}
4、Redisson实现的分布式锁是否是可重入锁?(可重入锁)
-
redis实现的分布式锁是不可重入的,但Redisson实现的分布式锁是可重入的,与Java中的ReentranLock是一样的,都是判断是否是同一个线程来判断是否可重入,如果是同一个线程则可重入。
-
当业务比较复杂时,锁粒度比较细时 会用到重入锁的,可以避免多个锁当中产生死锁问题。
-
Redisson中的实现
1)利用hash结构记录线程id和重入次数
2)获取锁之后在原有的重入次数中加1,释放锁成功时再直接减1,直到重入次数为0时才可以将该锁的信息删除。
5、Redisson实现的分布式锁能否解决主从数据的一致性?(不能)
-
假如redis的集群架构有一个主节点(主库),用于写数据;两个从节点(从库),用于读数据。当主节点发生了写数据时,则需要将数据同步到两个从节点中。当Java应用获取锁往主库中写入数据后,还没来得及将数据同步到从库中,主库就宕机了(挂了),则此时就出现了主从数据不一致的情况。主库宕机后,redis使用的哨兵模式,则会从两个从库中选择一个作为主库,此时还能去获取锁,则出现了两个线程拥有同一把锁的情况,导致脏数据。
-
RedLock(红锁):不能只在一个redis实例上创建锁,应该是在多个实例上创建(n / 2 + 1)个,避免在一个redis实例上加锁。但使用红锁,会导致业务实现比较复杂、性能差、运维繁琐。
-
Redisson实现的分布式锁不能解决主从数据的一致性,但是可以使用redisson提供的红锁来解决,红锁的性能太低,如果业务中硬性要求要保证数据的强一致性,建议采用zookeeper实现的分布式锁。
-
Redis 主要保证的是高可用性,zookeeper才可以保证数据的强一致性。
6、Redis分布式锁
三、Redis集群
1、Redis集群有哪些方案?
- Redis中提供的集群方案有三种:
1)主从复制:单节点redis的并发能力是有上限的,要进一步提高redis的并发能力,就需要搭建主从集群,实现读写分离。
2)哨兵模式:sentinel,中文名是哨兵。哨兵是 redis 集群机构中非常重要的一个组件,用来实现主从集群自动故障恢复,可保证redis的高并发高可用。
3)分片集群:可解决海量数据存储问题、高并发写问题,
2、主从数据同步流程(主从复制原理)
-
主从同步:单节点redis的并发能力是有上限的,要进一步提高redis的并发能力,就需要搭建主从集群,实现读写分离,一般是一主多从,主节点负责写数据,从节点负责读数据,主节点写入数据之后,需要把数据同步到从节点中。
-
主从同步分为两个阶段:全量同步、增量同步。
-
主从全量同步:指从节点第一次与主节点建立连接时使用的同步,即全量同步。
1)从节点请求主节点同步数据,其中从节点会携带自己的replication id和offset偏移量。
replication id:数据集的标记,id一致则说明是同一数据集。每一个master都有唯一的id,slave则会继承master节点的id。
offset:偏移量,随着记录在repl_baklog中的数据增多而逐渐增大,slave完成同步时也会记录当前同步的offset,如果slave的offset小于master的offset,说明slave数据落后于master,需要更新。
2)主节点判断是否第一次请求,主要判断的依据是,主节点和从节点是否是同一个replication id,如果不是,就说明是第一次同步,那主节点就会把自己的replication id和offset偏移量发送给从节点,让从节点与主节点的信息保持一致。
3)同时主节点会执行bgsave,生成rdb文件后,发送给从节点去执行,从节点先把自己的数据清空,然后执行主节点发送过来的rdb文件,这样就保持了一致。
4)如果在rdb生成执行期间,依然有请求到了主节点,主节点会以命令的方式记录到缓冲区,缓冲区是一个日志文件
5)最后把这个日志文件发送给从节点,这样就能保证主节点和从节点完全一致了,后期再同步数据时,都是依赖这个日志文件 -
主从增量同步:指当从节点服务重启之后,数据不一致时的同步
1)从节点会请求主节点同步数据,主节点还是判断是不是第一次请求,不是第一次就获取从节点的offset值
2)然后主节点从命令日志中获取offset值之后的数据,发送给从节点进行数据同步。
3、哨兵模式(保证redis的高并发高可用)
-
哨兵模式:实现了主从集群的自动故障恢复(监控、自动故障恢复、通知),如果master故障,sentinel(哨兵)会将一个slave提升为master。当故障实例恢复后也会以新的master为主,同时sentinel(哨兵)也充当redis客户端的服务发现来源,当集群发生故障转移时,会将最新信息推送给redis的客户端,即一般项目采用哨兵模式可保证redis的高并发高可用。
-
哨兵的作用:
1)集群监控:负责监控 redis master 和 slave 进程是否正常工作。sentinel(哨兵)会不断的检查master和slave是否按预期工作。
2)自动故障恢复:如果 master node 故障挂掉了,会自动转移到 slave node 上,将一个slave提升为master,当故障实例恢复后也以新的master为主。
3)消息通知:如果某个 redis 实例有故障,那么sentinel哨兵负责发送消息作为报警通知给管理员,sentinel充当redis客户端的服务发现来源,会将最新信息推送给redis的客户端。 -
服务状态监控:sentinel基于心跳机制监测服务状态,每隔1s向集群的每个实例发送ping命令
1)主观下线:如果某sentinel节点发现某实例未在规定时间响应,则认为该实例主观下线。
2)客观下线:如果超过指定数量quorum的sentinel都认为该实例已经下线,则该实例客观下线,quorum值最好超过sentinel实例数量的一半。 -
哨兵选主规则:
1)首先判断主与从节点断开时间长短,如果超过指定值就排该从节点
2)然后判断slave-priority值,越小优先级越高
3)如果slave-priority一样,则判断slave节点的offset值,越大优先级越高
4)最后判断slave节点的运行id大小,越小优先级越高。
4、Redis集群脑裂,该怎么解决?
-
集群脑裂:指使用哨兵模式集群时,由于主节点master和从节点和sentinel(哨兵)处于不同的网络分区,使得sentinel没有心跳感知到主节点master,所以通过选举的方式提升了一个从节点slave为主节点master,这样就存在两个主节点,就像大脑分裂了一样,会导致客户端还在old master那里写入数据,新节点无法同步数据,当网络恢复后,sentinel(哨兵)会将老的主节点降为slave从节点,这时再从新的主节点同步数据,这会导致老的主节点中的大量数据丢失。
-
解决办法:可以修改redis的配置,可设置最少的slave节点个数,比如设置至少要有一个从节点才能同步数据;可设置主从数据复制和同步的延迟时间,达不到要求就拒绝请求,可避免大量数据丢失。
min-replicas-to-write 1 表示最少的slave节点为1个
min-replicas-max-lag 5 表示数据复制和同步的延迟不能超过5秒
5、redis的分片集群有什么作用?
- 分片集群主要解决海量数据存储的问题,集群中有多个master,每个master保存不同数据
- 每个master都可以设置多个slave节点,就可以继续增大集群的高并发能力。
- 同时每个master之间通过ping监测彼此健康状态,就类似于哨兵模式了。
- 当客户请求可以访问集群任意节点,最终都会被转发到正确节点。
6、redis分片集群中数据是怎么存储和读取的?
- redis分片集群引入了哈希槽(slot)的概念,redis集群有16384个哈希槽,集群中每个主节点绑定了一定范围的哈希槽范围,key通过CRC16校验后对16384取模来决定哪个槽,通过槽找到对应的节点进行存储。
7、Redis是单线程,为什么还那么快?
- Redis是纯内存操作,执行速度非常快
- 采用单线程,避免不必要的上下文切换可竞争条件,多线程还要考虑线程安全问题
- 使用I/O多路复用模型,非阻塞IO
- 例如 bgsave和bgrewriteaof 都是在后台执行操作,不影响主线程的正常使用,不会产生阻塞
8、IO多路复用模型
-
redis是纯内存操作,执行速度非常快,它的性能瓶颈是网络延迟而不是执行速度,I/O多路复用模型主要就是实现了高效的网络请求。
-
Linux系统中一个进程使用的内存情况划分部分:用户空间和内核空间
用户空间只能执行受限的命令(Ring3),而且不能直接调用系统资源,必须通过内核提供的接口来访问。
内核空间可以执行特权命令(Ring0),调用一切系统资源。 -
常见的IO模型
1)阻塞IO(Blocking IO)
2)非阻塞IO(Nonblocking IO)
3)IO多路复用(IO Multiplexing) -
redis网络模型
-
Linux系统为了提高IO效率,会在用户空间和内核空间都加入缓冲区
写数据时,要把用户缓冲数据拷贝到内核缓冲区,然后写入设备
读数据时,要从设备读取数据到内核缓冲区,然后拷贝到用户缓冲区