1 在项目中缓存是如何使用的?缓存如果使用不当会造成什么后果?
这个问题几乎是互联网公司必问的,是基础中的基础。
- 如何使用缓存?
这个问题最好是根据自己的业务场景来答,没有的话需要硬加一个场景。TODO -
缓存的用途?
(1) 高性能
对于一些变动不频繁的数据,但是读请求比较频繁的数据,这种情况下使用缓存是经典的场景。
(2) 高并发
mysql本身对高并发支持的不怎么好,QPS到了2000基本上就很危险了,这种东西本身就就是内存数据库的天下。
(3) 缓存的不良后果
1 缓存与数据库双写不一致
2 缓存雪崩
3 缓存穿透
4 缓存并发竞争
2 redis和memcached有什么区别?redis的线程模型是什么?为什么单线程的redis比多线程的memcached效率要高得多(为什么redis是单线程的但是还可以支撑高并发)?
(1)memcached就说没用过吧,那就直接说redis的特点吧。
1 支持丰富的数据操作
2 原生就支持集群模式,高可用哨兵模式
(2) redis 线程模型
redis 线程模型中有两个重要的角色,IO多路复用epoll模型(Multiplexing epoll model下面简称mem)和文件事件分派器(File event handler下面简称 feh)。
mem负责监听客户端socket请求,有请求到达时就将socket请求压入到队列中。feh负责请求分派执行,feh根据每个socket当前产生的事件,来选择对应的事件处理器来处理。这里因为feh是单线程的,redis才叫做单线程的模型。
(3)为啥redis单线程模型也能效率这么高?
1 纯内存操作
2 核心是基于非阻塞的IO多路复用机制
3 单线程反而避免了多线程的频繁上下文切换问题
3 redis都有哪些数据类型?分别在哪些场景下使用比较合适?
这个问题的目的是 看看你到底有没有全面的了解redis有哪些功能,一般怎么来用,啥场景用什么,就怕你别就会最简单的kv操作,另外看看你在实际项目里都怎么玩儿过redis。这个回答不好,面试基本崩了。
(1) string
这是最基本的类型了,没啥可说的,就是普通的set和get,做简单的kv缓存
(2) hash
这个是类似map的一种结构,这个一般就是可以将结构化的数据,比如一个对象(前提是这个对象没嵌套其他的对象)给缓存在redis里,然后每次读写缓存的时候,可以就操作hash里的某个字段。也可以看做数据库的一条记录,但是有两个地方不同,第一 数据库是可以关联查询的,这个不行。第二 数据库可能会要求某个字段不能为空等限制 , 这个没有
(3) list
有序列表
这个是可以玩儿出很多花样的,比如可以通过list存储一些列表型的数据结构,类似粉丝列表了、文章的评论列表了之类的东西。
比如可以通过lrange命令,就是从某个元素开始读取多少个元素,可以基于list实现分页查询,这个很棒的一个功能,基于redis实现简单的高性能分页,可以做类似微博那种下拉不断分页的东西,性能高,就一页一页走。
比如可以搞个简单的消息队列,从list头怼进去,从list尾巴那里弄出来
(4) set
无序集合,自动去重
直接基于set将系统里需要去重的数据扔进去,自动就给去重了,如果你需要对一些数据进行快速的全局去重,你当然也可以基于jvm内存里的HashSet进行去重,但是如果你的某个系统部署在多台机器上呢?得基于redis进行全局的set去重。但是redis的去重策略呢?这个无法做复杂规则的去重。另外 可以基于set玩儿交集、并集、差集的操作。
(5) sorted set
排序的set,去重但是可以排序,写进去的时候给一个分数,自动根据分数排序,这个可以玩儿很多的花样,最大的特点是有个分数可以自定义排序规则,比如说你要是想根据时间对数据排序,那么可以写入进去的时候用某个时间作为分数,人家自动给你按照时间排序了
4 redis的过期策略都有哪些?内存淘汰机制都有哪些?手写一下LRU代码实现?
(1) redis里写的数据怎么没了?
redis是缓存,不是数据库。一台机器上可能redis所有的空间最多只有10G,那么数据越来越多了怎么办?比如我们往redis上放了20G的数据,这肯定是会有数据丢失的,redis自己会干掉那些不常用的数据。另一种数据消失的方式是用户给数据设置过期时间 setEx 或者 或者 set key value time ,set进去后过了time时间后数据就失效了。
(2) 数据明明都过期了,怎么还占用着内存?
比如10G容量的redis放入5G的数据,内存占用50%。 设置这5G数据的时候 均设置了一个小时的有效期,但是过了一个小时候内存占用依然是50%,这是怎么回事呢?
答案是:定期删除+惰性删除
定期删除:指的是redis默认是每隔100ms就随机抽取一些设置了过期时间的key,检查其是否过期,如果过期就删除。这里是随机抽取一些key来检查,不可能检查所有key,那样会导致机器负载过高。
单单是定期删除可能是不能解决问题的,因为会有很多数据没有被检查,这时候就需要惰性删除了。惰性删除指的是:在你获取某个key的时候,redis会检查一下 ,这个key如果设置了过期时间那么是否过期了?如果过期了此时就会删除,不会给你返回任何东西。
经过上面的定期删除+惰性删除处理后,redis依然存在问题,当大量数据设置过期时间,但是又没去查数据,也就是没触发惰性删除,这种情况内存依然会耗尽。这个时候 redis本身会有一个内存淘汰策略
如果redis的内存占用过多的时候,此时会进行内存淘汰,有以下内存淘汰策略
- noeviction: 当内存不足以容纳新写入数据时,新写入操作会报错,这个一般没人用吧,实在是太恶心了。
- allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 key(这个是最常用的)。
- allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个 key,这个一般没人用吧,为啥要随机,肯定是把最近最少使用的 key 给干掉啊。
- volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的 key(这个一般不太合适)。
- volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个 key。
- volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的 key 优先移除。
class LRUCache<K, V> extends LinkedHashMap<K, V> {
private final int CACHE_SIZE;
/**
* 传递进来最多能缓存多少数据
*
* @param cacheSize 缓存大小
*/
public LRUCache(int cacheSize) {
// true 表示让 linkedHashMap 按照访问顺序来进行排序,最近访问的放在头部,最老访问的放在尾部。
super((int) Math.ceil(cacheSize / 0.75) + 1, 0.75f, true);
CACHE_SIZE = cacheSize;
}
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
// 当 map中的数据量大于指定的缓存个数的时候,就自动删除最老的数据。
return size() > CACHE_SIZE;
}
}
5 如何保证Redis的高并发和高可用?redis的主从复制原理能介绍一下么?redis的哨兵原理能介绍一下么?
单机的redis是有性能瓶颈的,我们想提高redis的负载能力最直接的方法就是读写分离,这样一般就能保证大部分的项目。通过水平扩展,redis可以轻松达到几十万的QPS能力。
(1) redis replication redis主从复制的基本原理
1 redis 主从复制的一些基本知识
- redis采用异步方式复制数据到slave节点,不过redis 2.8开始,slave node会周期性地确认自己每次复制的数据量
- 一个master node是可以配置多个slave node的,一个slave只能有一个master node
- slave node也可以连接其他的slave node
- slave node做复制的时候,是不会block master node的正常工作的,也不会阻塞自己的查询服务,它会用旧的数据集来提供服务; 但是复制完成的时候,需要删除旧数据集,加载新数据集,这个时候就会暂停对外服务了
- slave node主要用来进行横向扩容,做读写分离,扩容的slave node可以提高读的吞吐量
2、master持久化对于主从架构的安全保障的意义
如果采用了主从架构,那么建议必须开启master node的持久化!
如果你关掉master的持久化,不建议用slave node作为master node的数据热备,因为那样的话,可能在master宕机重启(RDB和AOF都关闭了的情况下)的时候数据是空的,然后可能一经过复制,salve node数据也丢了。所以master节点,必须要使用持久化机制。另外,master的各种备份方案,要不要做,万一说本地的所有文件丢失了; 从备份中挑选一份rdb去恢复master; 这样才能确保master启动的时候,是有数据的。即使采用了后续讲解的高可用机制,slave node可以自动接管master node,但是也可能sentinal还没有检测到master failure,master node就自动重启了,还是可能导致上面的所有slave node数据清空故障,所以master节点,必须要使用持久化机制,最终我觉得在sentinal高可用模式下,不管是主是从都要开启持久化机制,因为从可能会变成主。
3 主从复制的核心原理
当启动一个slave node的时候,它会发送一个PSYNC命令给master node,如果这是slave node重新连接master node,那么master node仅仅会复制给slave部分缺少的数据; 否则如果是slave node第一次连接master node,那么会触发一次full resynchronization(全量复制),开始full resynchronization的时候,master会启动一个后台线程,开始生成一份RDB快照文件,同时还会将从客户端收到的所有写命令缓存在内存中。RDB文件生成完毕之后,master会将这个RDB发送给slave,slave会先写入本地磁盘,然后再从本地磁盘加载到内存中。然后master会将内存中缓存的写命令发送给slave,slave也会同步这些数据。
slave node如果跟master node有网络故障,断开了连接,会自动重连。master如果发现有多个slave node都来重新连接,仅仅会启动一个rdb save操作,用一份数据服务所有slave node。
4、主从复制的断点续传
从redis 2.8开始,就支持主从复制的断点续传,如果主从复制过程中,网络连接断掉了,那么可以接着上次复制的地方,继续复制下去,而不是从头开始复制一份
master node会在内存中常见一个backlog,master和slave都会保存一个replica offset还有一个master id,offset就是保存在backlog中的。如果master和slave网络连接断掉了,slave会让master从上次的replica offset开始继续复制,但是如果没有找到对应的offset,那么就会执行一次resynchronization。
5 无磁盘化复制
master在内存中直接创建rdb,然后发送给slave,不会在自己本地落地磁盘了
repl-diskless-sync
repl-diskless-sync-delay,等待一定时长再开始复制,因为要等更多slave重新连接过来
6 过期key处理
slave不会过期key,只会等待master过期key。如果master过期了一个key,或者通过LRU淘汰了一个key,那么会模拟一条del命令发送给slave。
7 redis 主从复制完整流程
- slave node启动,仅仅保存master node的信息,包括master node的host和ip,但是复制流程没开始master host和ip是从哪儿来的,redis.conf里面的slaveof配置的
- slave node内部有个定时任务,每秒检查是否有新的master node要连接和复制,如果发现,就跟master node建立socket网络连接
- slave node发送ping命令给master node
- 口令认证,如果master设置了requirepass,那么salve node必须发送masterauth的口令过去进行认证
- master node第一次执行全量复制,将所有数据发给slave node
-
master node后续持续将写命令,异步复制给slave node
8 数据同步相关的核心机制
- master和slave都会维护一个offset
master会在自身不断累加offset,slave也会在自身不断累加offset,slave每秒都会上报自己的offset给master,同时master也会保存每个slave的offset,这个倒不是说特定就用在全量复制的,主要是master和slave都要知道各自的数据的offset,才能知道互相之间的数据不一致的情况。 - backlog
master node有一个backlog,默认是1MB大小,master node给slave node复制数据时,也会将数据在backlog中同步写一份,backlog主要是用来做全量复制中断候的增量复制的 -
master run id
info server命令,可以看到master run id,如果根据host+ip定位master node,是不靠谱的,如果master node重启或者数据出现了变化,那么slave node应该根据不同的run id区分,run id不同就做全量复制,如果需要不更改run id重启redis,可以使用redis-cli debug reload命令。
- psync
从节点使用psync从master node进行复制,psync runid offset ,master node会根据自身的情况返回响应信息,可能是FULLRESYNC runid offset触发全量复制,可能是CONTINUE触发增量复制 - 全量复制
(1)master执行bgsave,在本地生成一份rdb快照文件
(2)master node将rdb快照文件发送给salve node,如果rdb复制时间超过60秒(repl-timeout),那么slave node就会认为复制失败,可以适当调节大这个参数
(3)对于千兆网卡的机器,一般每秒传输100MB,6G文件,很可能超过60s
(4)master node在生成rdb时,会将所有新的写命令缓存在内存中,在salve node保存了rdb之后,再将新的写命令复制给salve node
(5)client-output-buffer-limit slave 256MB 64MB 60,如果在复制期间,内存缓冲区持续消耗超过64MB,或者一次性超过256MB,那么停止复制,复制失败
(6)slave node接收到rdb之后,清空自己的旧数据,然后重新加载rdb到自己的内存中,同时基于旧的数据版本对外提供服务
(7)如果slave node开启了AOF,那么会立即执行BGREWRITEAOF,重写AOF - 增量复制
(1)如果全量复制过程中,master-slave网络连接断掉,那么salve重新连接master时,会触发增量复制
(2)master直接从自己的backlog中获取部分丢失的数据,发送给slave node,默认backlog就是1MB
(3)msater就是根据slave发送的psync中的offset来从backlog中获取数据的 - heartbeat
master默认每隔10秒发送一次heartbeat,salve node每隔1秒发送一个heartbeat
redis哨兵架构的相关基础知识
(1)哨兵介绍
1 集群监控,负责监控redis master和slave进程是否正常工作
2 消息通知,如果某个redis实例有故障,那么哨兵负责发送消息作为报警通知给管理员
3 故障转移,如果master node挂掉了,会自动转移到slave node上
4 配置中心,如果故障转移发生了,通知client客户端新的master地址
哨兵本身也是分布式的,作为一个哨兵集群去运行,互相协同工作
1 故障转移时,判断一个master node是宕机了,需要大部分的哨兵都同意才行,涉及到了分布式选举的问题
2即使部分哨兵节点挂掉了,哨兵集群还是能正常工作的,因为如果一个作为高可用机制重要组成部分的故障转移系统本身是单点的,那就很坑爹了
(2) 为什么redis哨兵集群必须至少3个节点? https://blog.csdn.net/b644ROfP20z37485O35M/article/details/102965597
(3) redis哨兵主备切换的数据丢失问题:异步复制、集群脑裂
1 异步复制导致的数据丢失
因为master -> slave的复制是异步的,所以可能有部分数据还没复制到slave,master就宕机了,此时这些部分数据就丢失了
2 脑裂导致的数据丢失
脑裂,也就是说,某个master所在机器突然脱离了正常的网络,跟其他slave机器不能连接,但是实际上master还运行着,此时哨兵可能就会认为master宕机了,然后开启选举,将其他slave切换成了master,这个时候,集群里就会有两个master,也就是所谓的脑裂。此时虽然某个slave被切换成了master,但是可能client还没来得及切换到新的master,还继续写向旧master的数据可能也丢失了,因此旧master再次恢复的时候,会被作为一个slave挂到新的master上去,自己的数据会清空,重新从新的master复制数据
解决异步复制和脑裂导致的数据丢失
min-slaves-to-write 1
min-slaves-max-lag 10
上面的配置信息意思是: 要求至少有1个slave,数据复制和同步的延迟不能超过10秒,如果说一旦所有的slave,数据复制和同步的延迟都超过了10秒钟,那么这个时候,master就不会再接收任何请求了。
有了min-slaves-max-lag这个配置,就可以确保说,一旦slave复制数据和ack延时太长,就认为可能master宕机后损失的数据太多了,那么就拒绝写请求,这样可以把master宕机时由于部分数据未同步到slave导致的数据丢失降低的可控范围内
减少脑裂的数据丢失
如果一个master出现了脑裂,跟其他slave丢了连接,那么上面两个配置可以确保说,如果不能继续给指定数量的slave发送数据,而且slave超过10秒没有给自己ack消息,那么就直接拒绝客户端的写请求,这样脑裂后的旧master就不会接受client的新数据,也就避免了数据丢失。上面的配置就确保了,如果跟任何一个slave丢了连接,在10秒后发现没有slave给自己ack,那么就拒绝新的写请求,因此在脑裂场景下,最多就丢失10秒的数据。
(4)redis哨兵实现故障转移(主备切换)
1 主观下线与客观下线切换
主观宕机简称sdown,就一个哨兵如果自己觉得一个master宕机了,那么就是主观宕机
客观宕机简称odown,如果quorum(quorum是有计算公式的)数量的哨兵都觉得一个master宕机了,那么就是客观宕机
sdown达成的条件很简单,如果一个哨兵ping一个master,超过了is-master-down-after-milliseconds指定的毫秒数之后,就主观认为master宕机
sdown到odown转换的条件很简单,如果一个哨兵在指定时间内,收到了quorum指定数量的其他哨兵也认为那个master是sdown了,那么就认为是odown了,客观认为master宕机
2 哨兵集群之间的自动发现机制
哨兵互相之间的发现,是通过redis的pub/sub系统实现的,每个哨兵都会往sentinel:hello这个channel里发送一个消息,这时候所有其他哨兵都可以消费到这个消息,并感知到其他的哨兵的存在。
3 slave配置的自动纠正
哨兵会负责自动纠正slave的一些配置,比如slave如果要成为潜在的master候选人,哨兵会确保slave在复制现有master的数据; 如果slave连接到了一个错误的master上,比如故障转移之后,那么哨兵会确保它们连接到正确的master上
4 slave->master选举算法
如果一个master被认为odown了,而且majority哨兵都允许了主备切换,那么某个哨兵就会执行主备切换操作,此时首先要选举一个slave来切换为master节点
如果一个slave跟master断开连接已经超过了down-after-milliseconds的10倍,外加master宕机的时长,那么slave就被认为不适合选举为master,公式为:(down-after-milliseconds * 10) + milliseconds_since_master_is_in_SDOWN_state
接下来会对slave进行排序
(1)按照slave优先级进行排序,slave priority越低,优先级就越高
(2)如果slave priority相同,那么看replica offset,哪个slave复制了越多的数据,offset越靠后,优先级就越高
(3)如果上面两个条件都相同,那么选择一个run id比较小的那个slave
5 quorum和majority
每次一个哨兵要做主备切换,首先需要quorum数量的哨兵认为odown,然后选举出一个哨兵来做切换,这个哨兵还得得到majority个哨兵的授权,才能正式执行切换
如果quorum < majority,比如5个哨兵,majority就是3,quorum设置为2,那么就3个哨兵授权就可以执行切换
但是如果quorum >= majority,那么必须quorum数量的哨兵都授权,比如5个哨兵,quorum是5,那么必须5个哨兵都同意授权,才能执行切换
6 configuraiton传播
哨兵完成切换之后,会在自己本地更新生成最新的master配置,然后同步给其他的哨兵,就是通过之前说的pub/sub消息机制