Redis面试题
Redis面试题
面试官:我看你做的项目中,都用到了Redis,你在最近的项目中那些场景用了Redis?
候选人:一定要结合项目,1. 为了验证项目场景的真实性;2. 为了作为深入发问的切入点;
- 缓存(缓存三兄弟:穿透、击穿、雪崩;双写一致性、持久化、数据过期策略、数据淘汰策略)
- 分布式锁(setnx、redisson)
- 消息队列、延迟队列 (何种数据类型)
缓存穿透、击穿、雪崩
面试官:什么是缓存穿透 ? 怎么解决 ?
候选人:
缓存穿透是指查询一个一定不存在的数据,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到 DB 去查询,可能导致 DB 挂掉。这种情况大概率是遭到了攻击或者key大面积的Key过期。
解决方案的话,可以在缓存层添加一个布隆过滤器来解决它,通过布隆过滤器可以迅速的判断数据到底在不在缓存层中,从而可以防止大量的对后端数据库的无效访问,降低DB的压力。
面试官:好的,你能介绍一下布隆过滤器吗?
候选人:
布隆过滤器主要是用于检索一个元素是否在一个集合中。我们当时使用的是redisson实现的布隆过滤器。
布隆过滤器的设计思想是用一个bitmap和三个哈希函数来实现的。它的底层主要是先去初始化一个bit数组,里面存放的二进制0或1。在一开始都是0,当一个key来了之后经过3次hash计算,模于数组长度找到数据的下标然后把数组中原来的0改为1,这样的话,三个数组的位置就能标明一个key的存在。查找的过程也是一样的。
当然是有缺点的,布隆过滤器有可能会产生一定的误判(哈希冲突),我们一般可以设置这个误判率,大概不会超过5%,其实这个误判是必然存在的,要不就得增加数组的长度,其实已经算是很划算了,5%以内的误判率一般的项目也能接受,不至于高并发下压倒数据库。
面试官:什么是缓存击穿 ? 怎么解决 ?
候选人:
缓存击穿的意思是对于设置了过期时间的key,缓存在某个时间点过期的时候,恰好这时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端 DB 加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把 DB 压垮。
解决方案有三种方式:
第一对于高热点数据,永远不设置过期时间
第二可以使用分布式锁:当缓存失效时,不立即去load db,先使用如 Redis 的 setnx 去设置一个互斥锁(保证此时只允许只有一个线程对数据库进行访问),当操作成功返回时再进行 load db的操作并回设缓存,否则重试get缓存的方法
第三种方案可以设置当前key逻辑过期,大概是思路如下:
①:在设置key的时候,设置一个过期时间字段一块存入缓存中,不给当前key设置过期时间
例如:{"name": "sss", "age": 12, "expire": 12312344}
②:当查询的时候,从redis取出数据后判断时间是否过期
③:如果过期则开通另外一个线程进行数据同步(保证不会阻塞用户线程),当前线程正常返回数据,返回的数据时过期的数据
当然两种方案各有利弊:
如果选择数据的强一致性,建议使用分布式锁的方案,性能上可能没那么高,锁需要等,也有可能产生死锁的问题
如果选择key的逻辑删除,则优先考虑的高可用性,性能比较高,但是数据同步这块做不到强一致
具体的业务场景选取二者之一即可,类似于转账场景需要强一致性,用户体验优先的情况下首先考虑高可用性。
面试官:什么是缓存雪崩 ? 怎么解决 ?
候选人:
缓存雪崩有两种导致的方式,第一种缓存雪崩意思是设置缓存时采用了相同的过期时间,导致大量缓存在某一时刻同时失效,请求全部转发到DB,DB 瞬时压力过重雪崩。第二种缓存雪崩是因为Redis集群挂掉导致的。
与缓存击穿的区别:雪崩是很多key,击穿是某一个key缓存。
- 解决方案主要是可以将缓存失效时间分散开,比如可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
- 当由于Redis集群挂掉导致的缓存雪崩时,这种情况下可以通过搭建Redis集群来防止,如哨兵模式,集群模式。
- 穿透、击穿、雪崩问题都可以通过限流保底解决。
双写一致性
面试官:redis做为缓存,mysql的数据如何与redis进行同步呢?(双写一致性)
先介绍自己的业务背景,抛出这个饵。(一致性要求高、允许延迟一致是两种不同的解决方案,所以要针对于项目来说)
双写一致性? 当修改了数据库的数据也要更新缓存的数据,缓存和数据库的数据要保持一致性
共享锁:读锁,加锁之后,其他线程可以共享读操作,但是不能写
排他锁:独占锁,加锁之后阻塞其他线程的读写操作
候选人:我最近做的这个项目xxx,里面有xxxx(根据自己的简历上写)的功能,需要让数据库与redis高度保持一致,因为要求时效性比较高,我们当时采用的读写锁保证的强一致性。
我们采用的是redisson实现的读写锁,在读的时候添加共享锁,可以保证读读不互斥,读写互斥。当我们更新数据的时候,添加排他锁,它是读写,读读都互斥,这样就能保证在写数据的同时是不会让其他线程读数据的,避免了脏数据。这里面需要注意的是读方法和写方法上需要使用同一把锁才行。
面试官:那这个排他锁是如何保证读写、读读互斥的呢?
候选人:其实排他锁底层使用也是setnx,保证了同时只能有一个线程操作锁住的方法
面试官:你听说过延时双删吗?为什么不用它呢?
候选人:延迟双删是通过两次删除缓存操作,完成Redis和DB的数据一致性,但是是弱一致性并且仍然可能存在脏数据。如果是写操作,我们先把缓存中的数据删除,然后更新数据库,最后再延时删除缓存中的数据,其中这个延时多久不太好确定,在延时的过程中可能会出现脏数据,并不能保证强一致性,所以没有采用它,延时只是尽可能的降低了脏数据的风险,而并不能完全排除脏数据。
面试官:redis做为缓存,mysql的数据如何与redis进行同步呢?(双写一致性)
Binlog:二进制日志,记录了所有的DDL语句和DML语句,但是不包括数据查询语句。
候选人:嗯!就说我最近做的这个项目,里面有xxxx(根据自己的简历上写)的功能,数据同步可以有一定的延时(符合大部分业务)
方法一:我们当时采用的阿里的canal组件实现数据同步,canal使用mysql的主从实现最终一致性:不需要更改业务代码,业务代码0侵入,部署一个canal服务。canal服务把自己伪装成mysql的一个从节点,当mysql数据更新以后,canal会读取binlog数据,然后在通过canal的客户端获取到数据,更新缓存即可。
方法二:可以使用异步通知的方式保证数据的最终一致性,主要依赖于MQ的可靠性。修改MySQL数据后,发送消息到MQ服务,其中缓存服务监听MQ,发现MySQL数据发生了更改就删除Redis中的这条数据的缓存
Redis的数据持久化
面试官:redis做为缓存,数据的持久化是怎么做的?
候选人:在Redis中提供了两种数据持久化的方式:1、RDB 2、AOF
RDB的数据持久化策略:将内存数据保存在RDB二进制文件中,在恢复数据时直接将数据copy到内存中。其中对于RDB的数据持久化有两种备份方案:
可以通过人工备份:
save
或者指令bgsave
来生成Redis持久化文件。可以使用自动触发策略备份,需要在RedisConfig配置文件中添加Save配置项。如下所示,在900秒内至少有一个key被修改,则执行bgsave。
save 900 1
对于AOF的数据持久化策略:AOF是将每一次执行的写命令记录下来,在恢复数据时全部重新执行一遍。
AOF持久化策略默认时关闭的,需要在RedisConfig中将其配置开启:appendonly: yes;appendonly: "appendonly.aof";分别指定开启AOF与指定AOF文件名称
AOF的刷盘策略?三种刷盘策略:1. 每执行一次写命令,立即记录到AOF文件中,对性能损耗过大;2.每秒执行一次刷盘策略,性能适中,最多丢失一秒数据;3. 写命令执行完先放到缓冲区中,等待操作系统的指示执行刷盘策略,性能很高但是容易丢失大量的数据。三种策略也同样在RedisConfig 文件中配置。
文件特点:AOF文件会比RDB文件大的多,而且AOF会记录对同一个KEy的多次写操作,但是只有最后一个写操作才是有效的;可以执行bgrewtiteof指令,让AOF文件执行重构,用最少的指令行达到相同的结果。也可以通过配置指定自动触发条件,例如配置:与上次AOF相比文件增长超过的百分比?AOF文件体积最小多大以上才出发重写?
面试官:RDB的执行原理是什么?
候选人:bgsave在开始时会fork主进程得到子进程,其中fork过程会阻塞,但是阻塞是nm级别的。此时子进程可共享主进程的内存数据,此时的copy过程直接将主进程的页表copy一份给子进程,这样速度就很快了,有了相同的页表,此时主进程和子进程二者拥有了相同的物理内存映射关系,子进程也可读到主进程相同的数据了,并写入RDB文件中(针对整个内存做快照)。
其他进程不会被阻塞,为了防止写rdb时其他进程过来读、修改内存数据导致脏数据的问题,fork底层采用copy-on-write技术,其中当主进程执行读操作时,访问共享内存;当主进程执行些操作时,会copy一份数据,执行写操作,并且会改变页表中指针指向的物理内存地址为copy后的地址。
面试官:这两种方式,哪种恢复的比较快呢?
候选人:RDB因为是二进制文件,并且RDB文件会压缩,刷盘策略没有AOF那么频繁,所以在保存的时候体积也是比较小的,它恢复的比较快,但是它有可能会丢数据,我们通常在项目中也会使用AOF来恢复数据,虽然AOF恢复的速度慢一些,但是它丢数据的风险要小很多,在AOF文件中可以设置刷盘策略,我们当时设置的就是每秒批量写入一次命令
Redis 的数据过期策略
面试官:Redis的数据过期策略有哪些 ?
候选人:
在redis中提供了两种数据过期删除策略
第一种是惰性删除,在设置该key过期时间后,我们不去管它,当需要该key时,我们在检查其是否过期,如果过期,我们就删掉它,反之返回该key。这种方式对CPU比较友好,只有在使用的时候才会去检查这个key是否过期。但是对内存不太友好,如果有大量的key过期并且没有被访问,则大量的内存不会被释放,浪费内存。
第二种是定期删除,就是说每隔一段时间,我们就对一些key进行检查,删除里面过期的key。
定期清理的两种模式:
- SLOW模式是定时任务,执行频率默认为10hz,每次不超过25ms,以通过修改配置文件redis.conf 的 hz 选项来调整这个次数
- FAST模式执行频率不固定,每次事件循环会尝试执行,但两次间隔不低于2ms,每次耗时不超过1ms
Redis的过期删除策略:惰性删除 + 定期删除两种策略进行配合使用。更大程度的删除过期的Key。
面试官: 定期删除的优缺点是什么?
候选人:优点是定期删除可以通过限制删除执行的时常和频率来减少删除操作对cpu的影响,另外定期删除也能有效的释放过期键占用的内存。
缺点是难以确定删除执行的时常和频率。
Redis的数据删除策略
面试官:Redis的数据淘汰策略有哪些 ?
数据淘汰指的是当Redis的内存不够用时,此时向Redis中添加新的Key,那么Redis就会按照某一种规则将内存中的数据删除掉,这种删除数据的规则指的就是内存淘汰策略。
和数据过期完全就是两个完全不同的概念!
数据淘汰策略有很多种,没必要都记住,但是一定要注意记住两个关键性的概念:LRU、LFU
候选人:
嗯,这个在redis中提供了很多种,默认是noeviction,不删除任何数据,内部不足直接报错
是可以在redis的配置文件中进行设置的,里面有两个非常重要的概念,一个是LRU,另外一个是LFU
LRU的意思就是最少最近使用,用当前时间减去最后一次访问时间,这个值越大则淘汰优先级越高。
LFU的意思是最少频率使用。会统计每个key的访问频率,值越小淘汰优先级越高
我们在项目设置的allkeys-lru,挑选最近最少使用的数据淘汰,把一些经常访问的key留在redis中
Redis的数据淘汰策略
面试官:谈一下Redis的数据淘汰策略的使用建议?
这种问题大多数情况以场景题的形式出现,可以结合实际的场景需求选取合适的解决方案!
候选者:
- 优先使用allkeys-lru策略,充分利用LRU算法的优势,把最近最常使用的数据留在缓存中,如果业务中有明显的冷热数据的划分,建议使用这种情况。
- 如果业务中Redis中存储的数据之间的访问频率差别不大,并且没有明确的冷热数据的区分,建议使用allkeys-random,随机选择淘汰。
- 如果业务中有置顶的需求,那么可以使用 volatile-lru,并且置顶数据不设置过期时间,这些数据就永远不会被淘汰,只会淘汰那些设置了过期时间的数据。
- 如果业务中有段时间内的高频访问需求,可以考虑使用allkeys-lfu、volatile-lfu策略。
面试官:数据库有1000万数据 ,Redis只能缓存20w数据, 如何保证Redis中的数据都是热点数据 ?
考察面试者能否根据具体的业务场景选择合适的Redis的数据淘汰策略。
候选人:
可以考虑一下实际业务中对热点数据的界定。
如果热点数据指的是实效性很强的数据,短时间内才有意义的数据,可以使用 allkeys-lru (挑选最近最少使用的数据淘汰)淘汰策略,那留下来的都是经常访问的热点数据
如果热点数据指的是访问频率很高的数据,低频的数据不具备热点性质,则可以考虑使用 allkeys-lfu来剔除掉低频访问的数据,只保留高频访问的数据。
面试官:Redis的内存用完了会发生什么?
考察Redis内存淘汰策略概念
候选人:
嗯~,这个要看redis的数据淘汰策略是什么,如果是默认的配置,redis内存用完以后则直接报错。我们项目中的场景xxx当时设置的 allkeys-lru 策略。把最近最常访问的数据留在缓存中。
Redis分布式锁
这里需要结合项目中的具体使用来回答:
Redis分布式锁的应用场景:集群情况下的定时任务、抢单、幂等性场景。
面试官: 你在那些场景下使用到了Redis的分布式锁?
候选人: 之前做了个小Demo,并结合Jemter压测模拟一下高并发场景下可能出现的超买超卖问题:
显而易见,如果只是按照正常的处理逻辑进行处理,只是没有并发的情况下执行正确的代码逻辑,不能保证高并发场景下的数据安全性问题,必定会出现超买超卖问题。解决线程安全问题,考虑加锁解决。
先考虑加Java中提供的Synchronized锁:把原有的业务逻辑用synchronized代码块包了一下:
每次一个线程获取到互斥锁,其他的线程就无法进入业务逻辑代码块中执行代码,必须要等待线程释放掉互斥锁,并且成功抢占到这个锁后才能保证这个线程开始扣减库存的业务逻辑(见上图左)。
上面的代码在我本机中是压测没有任何问题,我又在三个虚拟机的tomcat中分别部署了这个小Demo的jar包,配置Nginx做负载均衡,如下图所示:
8080和8081两个虚拟机都能在各自的电脑中获取到库存,这就意味着synchrinized锁在这里失效了,因为这里synchronized锁是Java的本地锁,他只能保证落在同一台JVM下的请求之间服务互斥,但是每一台虚拟机就对应一个单独的JVM,synchronized并不会垮JVM级别去实现互斥,所以这里同样出现了线程安全问题。所以在集群的情况下不能使用本地锁,需要使用分布式锁,如下图所示:
面试官:Redis分布式锁如何实现 ?
候选人:我在上面的Demo中使用两种方式分别实现分布式锁,第一种是直接使用原生的setnx指令实现,第二种是使用Redisson库实现
第一种方式设置代码:set lock value NX EX 10
(正确做法)注意事项:
- 保证指令一次执行完,保证指令的原子性,不能把setnx和expire分开执行
- 一定要保证设置过期时间,防止加锁服务器宕机,锁永远不会被释放,造成死锁
- 一定要设置合理的续期策略,防止业务代码执行期间锁被释放掉
- 一定要设置value为唯一的字符串,这里可以使用UUID简单的模拟唯一字符串的场景
假如说分开两步执行,没有保证指令的原子性:setnx + expire (错误方式!)
public boolean tryLock(String key,String requset,int timeout) {
Long result = jedis.setnx(key, requset);
// result = 1时,设置成功,否则设置失败
if (result == 1L) {
return jedis.expire(key, timeout) == 1L;
} else {
return false;
}
}
如果在执行完setnx后、expire之前时服务器宕机,可能面临锁永远不会过期从而导致死锁的问题,一个优化措施是使用Lua脚本实现指令的原子性。
原子性:一条指令要么全部执行成功,要么全部执行失败,不会存在部分成功的情况。
使用Lua的优化版本(不是太完美):
public boolean tryLock_with_lua(String key, String UniqueId, int seconds) {
String lua_scripts = "if redis.call('setnx',KEYS[1],ARGV[1]) == 1 then" +
"redis.call('expire',KEYS[1],ARGV[2]) return 1 else return 0 end";
List<String> keys = new ArrayList<>();
List<String> values = new ArrayList<>();
keys.add(key);
values.add(UniqueId);
values.add(String.valueOf(seconds));
Object result = jedis.eval(lua_scripts, keys, values);
//判断是否成功
return result.equals(1L);
}
正确的方式:set lock value NX EX 10
public boolean tryLock_with_set(String key, String UniqueId, int seconds) {
return "OK".equals(jedis.set(key, UniqueId, "NX", "EX", seconds));
}
面试官: 刚才你提到了要使用唯一的value?为什么要使用唯一的value?不使用有什么后果?
候选人: 必须要保证在加锁时当前的value为全局唯一,这是当前线程加锁的唯一标识,唯一代表了当前加锁的线程,防止锁被其他线程释放,例如:
- 当前线程A加锁,获取锁成功
- 当前线程执行业务逻辑代码过长,导致锁被释放掉
- 线程B加锁,获取到对应同一资源的锁
- 线程A业务逻辑代码执行成功,释放锁(此时释放的时线程B加上的锁!)
所以在释放锁时,需要对value进行校验,判断即将释放的锁是否为当前线程加上的,锁的释放这里使用Lua脚本实现,保证指令的原子性
public boolean releaseLock_with_lua(String key,String value) {
String luaScript = "if redis.call('get',KEYS[1]) == ARGV[1] then " +
"return redis.call('del',KEYS[1]) else return 0 end";
return jedis.eval(luaScript, Collections.singletonList(key), Collections.singletonList(value)).equals(1L);
}
面试官: 刚才提到的加锁指令:set lock value NX EX 10
在集群模式下,如果master节点加锁成功但是还没有同步到slave节点时,此时master节点发生故障,选举机制slave1重新做为master节点,此时线程B又进行加锁成功,那么就存在了两个线程同时加锁成功的情况,请问你如何解决这种问题?
候选者: 的确存在这种问题,毕竟上述的加锁过程都是只针对Redis集群中的一个节点的加锁策略,可能会导致锁丢失。可以在分布式环境下使用Redlock解决这个问题。这里考虑使用Redission对RedLock做的封装的解决方式。
面试官:好的,那你如何控制Redis实现分布式锁有效时长呢?
候选人:Redis的setnx指令不好控制这个问题,我们当时采用的redis的一个框架redisson实现的。
在redisson中需要手动加锁,并且可以控制锁的失效时间和等待时间,当锁住的一个业务还没有执行完成的时候,在redisson中引入了一个看门狗机制,就是说每隔一段时间就检查当前业务是否还持有锁,如果持有就增加加锁的持有时间,当业务执行完成之后需要使用释放锁就可以了。
还有一个好处就是,在高并发下,一个业务有可能会执行很快,先客户1持有锁的时候,客户2来了以后并不会马上拒绝,它会自旋不断尝试获取锁,如果客户1释放之后,客户2就可以马上持有锁,性能也得到了提升。
面试官:好的,redisson实现的分布式锁是可重入的吗?
候选人:嗯,是可以重入的。这样做是为了避免死锁的产生。这个重入其实在内部就是判断是否是当前线程持有的锁,如果是当前线程持有的锁就会计数,如果释放锁就会在计算上减一。在存储数据的时候采用的hash结构,大key可以按照自己的业务进行定制,其中小key是当前线程的唯一标识,value是当前线程重入的次数
面试官:redisson实现的分布式锁能解决主从一致性的问题吗
候选人:这个是不能的,比如,当线程1加锁成功后,master节点数据会异步复制到slave节点,此时当前持有Redis锁的master节点宕机,slave节点被提升为新的master节点,假如现在来了一个线程2,再次加 锁,会在新的master节点上加锁成功,这个时候就会出现两个节点同时持有一把锁的问题。
我们可以利用redisson提供的红锁来解决这个问题,它的主要作用是,不能只在一个redis实例上创建锁,应该是在多个redis实例上创建锁,并且要求在大多数redis节点上都成功创建锁,红锁中要求是redis的节点数量要过半。这样就能避免线程1加锁成功后master节点宕机导致线程2成功加锁到新的master节点上的问题了。
但是,如果使用了红锁,因为需要同时在多个节点上都添加锁,性能就变的很低了,并且运维维护成本也非常高,所以,我们一般在项目中也不会直接使用红锁,并且官方也暂时废弃了这个红锁
面试官:好的,如果业务非要保证数据的强一致性,这个该怎么解决呢?
候选人: 嗯~,redis本身就是支持高可用的,做到强一致性,就非常影响性能,所以,如果有强一致性要求高的业务,建议使用zookeeper实现的分布式锁,它是可以保证强一致性的。
面试官:Redis集群有哪些方案, 知道嘛 ?
候选人:嗯~~,在Redis中提供的集群方案总共有三种:主从复制、哨兵模式、Redis分片集群
Redis主动复制、主从同步流程
Redis搭建主从架构的集群模式,实现读写分离:主节点负责处理写操作,从节点负责处理读操作,需要将主节点写入的数据同步到从节点。
面试官:那你来介绍一下主从同步
候选人:单节点Redis的并发能力是有上限的,要进一步提高Redis的并发能力,可以搭建主从集群,实现读写分离。一般都是一主多从,主节点负责写数据,从节点负责读数据,主节点写入数据之后,需要把数据同步到从节点中
面试官:能说一下,主从同步数据的流程
Replication ID:简称replid,是数据集的标记,主从节点的id一致说明是同一份数据集,每一个Master都有唯一的replid,从节点需要继承master节点的replid
offset:偏移量,随着记录在repl_baklog中的数据越来越多,offset的值也随着增大,当slave节点完成同步时,也会记录当前同步的offset,如果当前的slave的offset小于master的offset,说明slave的数据落后于master,就需要进行更新(同步)
候选人:主从同步分为了两个阶段,一个是全量同步,一个是增量同步
全量同步是指从节点第一次与主节点建立连接的时候使用全量同步,流程是这样的:
从节点请求主节点同步数据,其中从节点会携带自己的replication id和offset偏移量。
主节点判断是否是第一次请求,主要判断的依据就是,主节点与从节点是否是同一个replication id,如果不是,就说明是第一次同步,那主节点就会把自己的数据版本信息(replication id和offset)发送给从节点,让从节点与主节点的信息保持一致。
在同时主节点会执行bgsave,生成rdb文件后,发送给从节点去执行,从节点先把自己的数据清空,然后执行主节点发送过来的rdb文件,这样就保持了一致
如果主节点在rdb生成执行期间,依然有请求到了主节点,而主节点会以命令的方式记录到缓冲区,缓冲区是一个日志文件:repl_baklog,最后把这个日志文件发送给从节点,这样就能保证主节点与从节点完全一致了。后期再同步数据的时候(第一次同步数据以后的每次同步过程),都是依赖于这个日志文件,而不会生成RDB文件去同步了,RDB文件只会在第一次同步过程中才会发送给从节点。这个就是全量同步。
增量同步指的是,当从节点服务重启之后,数据就不一致了,所以这个时候,从节点会请求主节点同步数据:
- 从节点发送数据同步的请求,并且携带自己的offset值、从节点的replid给主节点
- 主节点还是判断不是第一次请求,如果不是第一次请求,就根据节点的offset值,然后主节点从命令日志(repl_baklog)中获取offset值之后的数据,发送给从节点进行数据同步
Redis的高可用:哨兵、集群脑裂
哨兵模式:Redis提供了哨兵机制,来实现集群的自动故障恢复,哨兵可进行监控、故障恢复、通知功能。Sentinel可以监控节点的运行情况,不断的监控master、slave是否按照预期运行。发生故障了可以主动的进行master节点的选取或者slave节点的删除。
服务监控状态:
Sentinel基于心跳机制监控集群的状态,每个一秒向集群中的每一个实例发送一个ping命令:
- 主观下线:如果某个sentinel在规定的时间之内没有收到集群中实例的响应,就认为这个节点主观下线了
- 客观下线:如果超过部署的总sentinel半数个sentinel都未收到集群中实例的响应,说明这个节点客观下线了
哨兵选主规则:
- 判断与主节点断开的事件长短,如果超过了指定的值,就排除这个节点
- 判断从节点的slave-priority值,这个值越小优先级越高
- 如果priority的值相同,则判断slave节点的offset值,越大优先级越高
- 如果前三个方式都无法决出胜负,则判断slave节点运行id大小,越小优先级越高,就会优先被选中为主节点
哨兵模式的脑裂问题?
- 什么是脑裂问题?脑裂问题指的是主节点没有挂,但是哨兵sentinel无法监听到主节点,从而在从节点中选举出一个新的主节点,此时就会同时存在两个主节点,这就是集群的脑裂问题
- 脑裂问题发生的原因?slave与master节点存在于不同的网络分区时,导致sentinel无法监听到master节点,从而导致脑裂问题。
- 脑裂可能产生的影响?脑裂后,可能导致部分的数据丢失,此时sentinel无法监听到主节点,但是客户端仍然可以连接到master进行写入数据,此时如果网络恢复,会把原来的主节点强制降级成为slave,然后从新的master中同步数据(清空自己的数据),则此时后面客户端写入的数据丢失!
- 脑裂问题Redis中是怎么解决的?Redis通过两个配置参数解决这个脑裂问题:
- 最少的slave节点?指定这个参数可以保证:当从节点的数量小于这个值时,无法写入数据
- 数据复制和同步的最长时间?指定这个参数可以保证:主从节点进行数据同步的最长时间?超过了就算连接超时。
面试官:怎么保证Redis的高并发高可用
候选人:首先可以搭建主从集群,再加上使用redis中的哨兵模式,哨兵模式可以实现主从集群的自动故障恢复,里面就包含了对主从服务的监控、自动故障恢复、通知;如果master故障,Sentinel会将一个slave提升为master。当故障实例恢复后也以新的master为主;同时Sentinel也充当Redis客户端的服务发现来源,当集群发生故障转移时,会将最新信息推送给Redis的客户端,所以一般项目都会采用哨兵的模式来保证redis的高并发高可用
面试官:你们使用redis是单点还是集群,哪种集群
候选人:我们当时使用的是主从(1主1从)加哨兵,一般情况下,Redis单节点能提供大概8万左右的写并发,10万左右的读并发操作,一般的项目很难达到这个需求,也绝对够用了,所以没有选择更复杂的架构。一般单节点不超过10G内存,如果Redis内存不足则可以给不同服务分配独立的Redis主从节点(搭建多套Redis的集群模式,拆分服务,一个服务一个Redis主从架构)。尽量不做分片集群。因为集群维护起来比较麻烦,并且集群之间的心跳检测和数据通信会消耗大量的网络带宽,也没有办法使用lua脚本和事务
面试官:redis集群脑裂,该怎么解决呢?
候选人:这个在项目很少见,不过脑裂的问题是这样的,我们现在用的是Redis的哨兵模式集群的:
有的时候由于网络等原因可能会出现脑裂的情况,就是说,由于redis master节点和redis salve节点和sentinel处于不同的网络分区,使得sentinel没有能够心跳感知到master,所以通过选举的方式提升了一个salve为master,这样就存在了两个master,就像大脑分裂了一样,这样会导致客户端还在old master那里写入数据,新节点无法同步数据,当网络恢复后,sentinel会将old master降为salve,这时再从新master同步数据,这会导致old master中的大量数据丢失。
关于解决的话,我记得在redis的配置中可以设置:第一可以设置最少的salve节点个数,比如设置至少要有一个从节点才能同步数据,第二个可以设置主从数据复制和同步的延迟时间,达不到要求就拒绝请求,就可以避免大量的数据丢失
Redis的分片集群结构
主从和哨兵可以解决高可用、高并发读的问题,但是还有两个问题没有解决:
- 海量数据的存储问题
- 高并发写问题
请问是如何解决上述问题?
因为分片集群架构是设置了多个Master节点,并且每个Master存储的数据相互独立,那么数据存储的能力就是原来的倍数、并且高并发写的能力也是原来的倍数
分片集群架构
- 集群中有多个master,每个master保存不同的数据(多个master,解决了高并发的写能力)
- 每个master都可以有多个slave节点(多个slave,解决了高并发的读问题)
- master之间通过心跳机制互相监听彼此的健康状态(彼此之间监听代替了哨兵的作用)
- 不需要操心到底访问哪个master读写数据,随意那个都可以,被访问的master会通过hash计算自动路由到正确的master节点进行数据的读取、写入。
分片集群架构如图所示:
路由导航算法:
面试官:redis的分片集群有什么作用
候选人:分片集群主要解决的是,海量数据存储的问题,集群中有多个master,每个master保存不同数据,并且还可以给每个master设置多个slave节点,就可以继续增大集群的高并发能力。同时每个master之间通过ping监测彼此健康状态,就类似于哨兵模式了。当客户端请求可以访问集群任意节点,最终都会被转发到正确节点
面试官:Redis分片集群中数据是怎么存储和读取的?
候选人:Redis 分片集群引入了哈希槽的概念,有 16384 个哈希槽,集群中每个主节点绑定了一定范围的哈希槽范围, key通过计算 CRC16 值,对值进行 16384 取模来决定放置哪个槽,通过槽找到对应的节点进行存储。取值操作同理。
Redis是单线程的,为什么还执行这么快?
面试官:Redis是单线程的,但是为什么还那么快?
候选人:
- Redis完全基于内存的,C语言编写
- 采用单线程,避免不必要的上下文切换可竞争条件
- 使用多路I/O复用模型,非阻塞IO
例如:bgsave 和 bgrewriteaof 都是在后台执行操作,不影响主线程的正常使用,不会产生阻塞
IO模型及其IO多路复用模型
Redis是纯内存操作,执行速度非常快,他的性能瓶颈不是执行速度而是网络延迟,IO多路复用模型主要是实现了高效的网络请求。
- 用户空间和内核空间
- 常见的IO模型
- 阻塞IO
- 非阻塞IO
- IO多路复用
- Redis网络模型
- Linux系统中,一个进程的内存使用情况被划分为两个部分:内核空间、用户空间
- 用户空间只能执行受限的命令,权限较低,要想调用系统资源,必须通过调用内核空间提供的接口完成
- 内核空间可以执行特权命令,调用一切系统资源
Linux为了提高IO效率,会在用户空间和内核空间都添加上缓冲区:
- 写数据时,要把用户缓冲数据拷贝到内核缓冲区,然后写入网卡设备
- 读数据时,要把内核缓冲区的数据拷贝到用户缓冲区
两个导致IO瓶颈的原因:
- 在读数据时,如果没有数据拷贝过来,就一直在等待内核缓冲区内的数据,数据等待过程耗时
- 数据在内核缓冲区、用户缓冲区之间的来回流转拷贝,耗时
所以IO优化的方向就是从这两点下手!
阻塞IO
阻塞IO模型中,用户进程在“数据拷贝”、“等待数据”过程处于阻塞状态
非阻塞IO
非阻塞IO在第一个等待数据的阶段看起来是没有被阻塞,但是实际上和阻塞了是等效的,她不断的在循环询问,循环询问,处于一个无用功,浪费资源,可能会导致CPU飙高。
IO多路复用
IO多路复用的三种实现方式
这三种实现方式,前两种是:只要是监听的任何一个socket有响应了,就去便利所有的socket,效率明显低,没有目的的遍历;
第三种epoll模式:当socket就绪了,还会告诉用户进程到底是哪些socket就绪了,就可以完美的解决无用的便利。
Redis网络模型
面试官:能解释一下I/O多路复用模型?
候选人:I/O多路复用是指利用单个线程来同时监听多个Socket ,并在某个Socket可读、可写时得到通知,从而避免无效的等待,充分利用CPU资源。目前的I/O多路复用都是采用的epoll模式实现,它会在通知用户进程Socket就绪的同时,把已就绪的Socket写入用户空间,不需要挨个遍历Socket来判断是否就绪,提升了性能。
其中Redis的网络模型就是使用I/O多路复用结合事件的处理器来应对多个Socket请求,比如,提供了连接应答处理器、命令回复处理器,命令请求处理器;
在Redis6.0之后,为了提升更好的性能,在命令回复处理器使用了多线程来处理回复事件,在命令请求处理器中,将命令的转换使用了多线程,增加命令转换速度,在命令执行的时候,依然是单线程