Redis常见面试题
1、基础
简单介绍下redis
- C写的开源高性能非关系型键值对数据库。底层采取epoll读写速度非常快,大多用于缓存,也提供了事务、持久化、集群以及多种数据类型的功能。
你认为Redis有哪些优缺点?
- 优点:
- 读写速度快
- 支持持久化
- 支持事务
- 数据结构丰富
- 支持主从,数据分片
- 缺点:
- 不具备自动恢复功能
- 较难支持在线动态扩容
说说Redis的几种数据类型
- string:字符串、整数或者浮点数
- list:列表可重复
- hash:包含键值对的无序散列表
- set:无序集合不可重复
- zset:有序集合不可重复
列举几个Redis应用场景
- 计数器
- 数据缓存
- 页面缓存
- 消息队列(blpop)
- 分布式锁
- 好友关系(set交并差集)
- 排行榜(zset的score)
Redis和memcached的区别?
- Redis比memcached数据类型丰富。
- Redis支持批量操作,事务操作,持久化,发布订阅,memcached不支持。
Redis线程模型知道吗?
- redis 内部使用文件事件处理器 file event handler,它是单线程的,所以redis才叫做单线程模型。它采用IO****多路复用机制同时监听多个 socket。多个 socket 可能会并发产生不同的操作,每个操作对应不 同的文件事件,但是 IO多路复用程序会监听多个 socket,会将产生事件的 socket 放入队列中排队,事件分派器每次从队列中取出一个 socket,根据 socket 的事件类型交给对应的事件处理器进行处理。
1657692192198
缓存和数据库谁先更新呢?
- 写请求过来,将写请求缓存到缓存队列中,并且开始执行写请求的具体操作(删除缓存中的数据,更新数据库,更新缓存)。
- 如果在更新数据库过程中,又来了个读请求,将读请求再次存入到缓存队列(可以搞n个队列,采用key的hash值进行队列个数取模hash%n,落到对应的队列中,队列需要保证顺序性)中,顺序性保证等待队列前的写请求执行完成,才会执行读请求之前的写请求删除缓存失败,直接返回,此时数据库中的数据是旧值,并且与缓存中的数据是一致的,不会出现缓存一致性 的问题。
- 写请求删除缓存成功,则更新数据库,如果更新数据库失败,则直接返回,写请求结束,此时数据库中的值依旧是旧值,读请求过来后,发现缓存中没有数据,则会直接向数据库中请求,同时将数据写入到缓存中,此时也不会出现数据一致性的问题。
- 更新数据成功之后,再更新缓存,如果此时更新缓存失败,则缓存中没有数据,数据库中是新 值 ,写请求结束,此时读请求还是一样,发现缓存中没有数据,同样会从数据库中读取数据, 并且存入到缓存中,其实这里不管更新缓存成功还是失败,都不会出现数据一致性的问题。
2、持久化
什么是持久化?Redis为什么需要持久化?
- 持久化就是把内存的数据写到磁盘中去,防止服务宕机了内存数据丢失。
Redis有哪几种持久化方式?优缺点是什么?
- rdb(默认) 和aof两种。
- rdb优点:
- 二进制存储节省空间,只有一个dump.rdb文件。
- 灾难恢复较快。
- 性能最大化(主进程处理命令的效率,而不是持久化的效率),采取的是fork()+copyonwrite技术。
- rdb缺点:
- 安全性相对较低,因为rdb是每隔一段时间进行持久化,数据丢失率较高。
- 持久化速度相对aof较低,因为aof直接append追加,rdb是全量。
- aof优点:
- 数据安全,可以配置always,也就是每进行一次命令操作就记录到aof文件中一次。
- 持久化速度较快,每次都只是追加一个语句到文件。
- 带rewrite机制。
- aof缺点:
- AOF 文件比 RDB 文件大,所以灾难性恢复速度慢。
- 会对主进程对外提供请求的效率造成影响,接收请求、处理请求、写aof文件这三步是串行原子执行的。而非异步多线程执行的。Redis单线程!
两种持久化方式如何做选择?
- 如果数据非常敏感,尽量做到不丢失,那么选择aof,反之rdb。
- 如果追求容灾恢复速度,那么建议rdb,因为他是二进制的,文件很小,恢复速度快。
- 如果追求读写速度,那么建议rdb,因为他是fork出来子进程配合copyonwrite技术来持久化,不会 影响主进程的读写。
- Redis4.0后支持混合持久化,也就是rdb+aof,建议开启。
RDB持久化的原理是怎样的?
- fork子进程+copyonwrite技术。
什么是fork?
- fork()是unix和linux这种操作系统的一个api,而不是Redis的api。fork()用于创建一个子进程,注意是子进程,不是子线程。fork()出来的进程共享其父类的内存数据。仅仅是共享fork()出子进程的那一刻的内存数据,后期主进程修改数据对子进程不可见,同理,子进程修改的数据对主进程也不可见。比如:A 进程fork()了一个子进程B,那么A进程就称之为主进程,这时候主进程A和子进程B所指向的内存空间是 同一个,所以他们的数据一致。但是A修改了内存上的一条数据,这时候B是看不到的,A新增一条数据,删除一条数据,B都是看不到的。而且子进程B出问题了,对我主进程A完全没影响,我依然可以对 外提供服务,但是主进程挂了,子进程也必须跟随一起挂。这一点有点像守护线程的概念。Redis正是巧 妙的运用了fork()这个牛逼的api来完成RDB的持久化操作。
Redis中的fork()
- Redis巧妙的运用了fork()。当bgsave执行时,Redis主进程会判断当前是否有fork()出来的子进程,若有则忽略,若没有则会fork()出一个子进程来执行rdb文件持久化的工作,子进程与Redis主进程共享同一份内存空间,所以子进程可以搞他的rdb文件持久化工作,主进程又能继续他的对外提供服务,二者互不影响。我们说了他们之后的修改内存数据对彼此不可见,但是明明指向的都是同一块内存空间,这是咋搞得?肯定不可能是fork()出来子进程后顺带复制了一份数据出来,如果是这样的话比如我有4g内存,那么其实最大有限空间是2g,我要给rdb留出一半空间来,扯淡一样!那他咋做的?采取了copyonwrite技术。
什么是copyonwrite?
主进程fork()子进程之后,内核把主进程中所有的内存页的权限都设为read-only,然后子进程的地址空间指向主进程。这也就是共享了主进程的内存,当其中某个进程写内存时(这里肯定是主进程写,因为子进程只负责rdb文件持久化工作,不参与客户端的请求),CPU硬件检测到内存页是read-only的,于是触发页异常中断(page-fault),陷入内核的一个中断例程。中断例程中,内核就会把触发的异常的页复制一份(这里仅仅复制异常页,也就是所修改的那个数据页,而不是内存中的全部数据),于是主子进 程各自持有独立的一份。
其实就是更改数据的之前进行copy一份更改数据的数据页出来,比如主进程收到了 set k 1 请求(之前k的值是2),然后这同时又有子进程在rdb持久化,那么主进程就会把k这个key的数据页拷贝一份,并且主进程中k这个指针指向新拷贝出来的数据页地址上,然后进行更改值为1的操作,这个主进程k元素地址引用的新拷贝出来的地址,而子进程引用的内存数据k还是修改之前的。
一句话总结copyonwrite
- copyonwritefork()出来的子进程共享主进程的物理空间,当主子进程有内存写入操作时,read-only内存页发生中断,将触发的异常的内存页复制一份(其余的页还是共享主进程的)。
为什么要用copyonwrite?
假设是全量复制,那么内存空间直接减半,浪费资源不说,数据量10g,全量复制这10g的时间也够长的。这谁顶得住?
如果不全量复制,会是怎样?相当于我一边复制,你一边写数据,看着貌似问题不大,其实不然。比如现在Redis里有k1的值是1,k2的值是2,比如bgsave了,这时候rdb写入了k1的值,在写k2的值之前时,有个客户端请求 。
set k1 11 set k2 22
那么持久化进去的是k2 22,但是k1的值还是1,而不是最新的11,所以会造成数据问题,所以采取了copyonwrite技术来保证触发bgsave请求的时候无论你怎么更改,都对我rdb文件的数据持久化不会造成任何影响。
AOF持久化的原理是怎样的?
- 就是每次都在aof文件后面追加命令。他与主进程收到请求、处理请求是串行化的,而非异步并行的。图示如下:
1657693245060 - 所以aof的频率高的话绝逼会对Redis带来性能影响,因为每次都是刷盘操作。跟mysql一样了。Redis每次都是先将命令放到缓冲区,然后根据具体策略(每秒/每条指令/缓冲区满)进行刷盘操作。如果配置的always,那么就是典型阻塞,如果是sec,每秒的话,那么会开一个同步线程去每秒进行刷盘操作,对主线程影响稍小。
AOF有哪几种刷盘策略?
- 其实Redis每次在写入AOF缓冲区之前,他都会调用flushAppendOnlyFile(),判断是否需要将AOF缓冲区的内容写入和同步到AOF文件中。这个决策是由配置文件的三个策略来控制的
- always
- everysec
- no
能同时存在几个fork?为什么?
- 只能同时存在一个,原因如下:
- bgsave命令执行期间,client发送的save/bgsave命令会被服务器拒绝,这么做是因为如果产生多个子进程同时进行rdb持久化的工作的话会产生竞争条件,造成数据问题已经服务器压力也会某些条件下过大。
如何手动进行rdb持久化?
- save
- 同步、阻塞。
- 致命的问题,持久化的时候redis服务阻塞(准确的说会阻塞当前执行save命令的线程,但是redis是单线程的,所以整个服务会阻塞),不能继对外提供请求,GG!数据量小的话肯定影响不大,数据量大呢?每次复制需要1小时,那就相当于停机一小时。
- bgsave
- 异步、非阻塞。
- 采取fork() + copyonwrite的方式,他可以一边进行持久化,一边对外提供读写服务,互不影响,新写的数据对我持久化不会造成数据影响,你持久化的过程中报错或者耗时太久都对我当前对外提供请求的服务不会产生任何影响。持久化完会将新的rdb文件覆盖之前的。
3、过期淘汰策略
Redis的过期key删除策略有哪些?也是expire原理
- 如果假设你设置一个一批key只能存活1个小时,那么接下来1小时后,redis是怎么对这批key进行删除的? 答案是:定期删除+惰性删除
- 定期删除
- 定期删除,指的是redis默认是每隔100ms就随机抽取一些设置了过期时间的key,检查其是否过期,如果过期就删除。假设redis里放了10万个key,都设置了过期时间,你每隔几百毫秒,就检查10万个key,那redis基本上就死了,cpu负载会很高的,消耗在你的检查过期key上了。注意,这里可不是每隔100ms就遍历所有的设置过期时间的key,那样就是一场性能上的灾难。实际上redis是每隔100ms随机抽取一些key来检查和删除的。
- 惰性删除
- 定期删除可能会导致很多过期key到了时间并没有被删除掉,那咋整呢?所以就是惰性删除了。这就是说,在你获取某个key的时候,redis会检查一下 ,这个key如果设置了过期时间那么是否过期 了?如果过期了此时就会删除,不会给你返回任何东西。 并不是key到时间就被删除掉,而是你查询这个key的时候,redis再懒惰的检查一下
- 通过上述两种手段结合起来,保证过期的key一定会被干掉
- 总结
- 很简单,就是说,你的过期key,靠定期删除没有被删除掉,还停留在内存里,占用着你的内存呢,除非你的系统去查一下那个key,才会被redis给删除掉 。
- 但是实际上这还是有问题的,如果定期删除漏掉了很多过期key,然后你也没及时去查,也就没走惰性删除,此时会怎么样?如果大量过期key堆积在内存里,导致redis内存块耗尽了,咋整?
- 答案是:走内存淘汰机制。
4、事务
Redis的事务概念
- Redis的事务并不像Mysql那么灵活,有隔离级别,出问题后还能回滚数据等高级操作。Redis毕竟是非关系型数据库,他目前事务回滚机制是不执行命令,也就是可以采取watch命令模拟乐观锁,进行监听数据,发现数据不是事务开始时候的样子了,那么我这个事务里的命令就不会得到执行。
Redis事务的相关命令
- MULTI:开始事务。
- EXEC:执行事务,也就是说只有EXEC命令执行的时候,这个事务内的语句才会真正的得到执行。
- WATCH:监听数据变化,在开始事务之前执行。
Redis事务支持隔离性吗?
- Redis是单进程的且它保证在执行事务时不会对事务进行中断,事务可以从运行直到执行完所有事务队列中的命令为止。因此,Redis 的事务是总是带有隔离性的。
Redis事务保证原子性吗,支持回滚吗?
- Redis中,单条命令是原子性执行的,但事务不保证原子性,且没有回滚。他目前事务回滚机制是不执行命令,也就是可以采取watch命令模拟乐观锁,进行监听数据,发现数据不是事务开始时候的样子了,那么我这个事务里的命令就不会得到执行。
Redis事务的原理
- watch
- 每个数据库都维护一个字典watched_keys,key为监听的key,value即为监听的客户端对象。所有客户端的修改都会去检查这个字典,如果key一致则会去打开客户端对象的 REDIS_DIRTY_CAS.
- multi 和 exec
- multi命令将客户端标记为“事物状态”,这个客户端后续的普通操作指令都会放入事务队列(客户端会保存一个事务队列),直到执行multi、exec、watch(?)、discard。exec时会服务端去查当前客户端的REDIS_DIRTY_CAS,如果被打开了,则拒绝客户端提交的事务。没打开则遍历客户端事务队列,挨条执行。
5、主从集群
Redis如何实现高可用?
- 采取主从复制,用slave来做从节点,读写分离。主节点挂了的话从节点可以切换为主继续提供工作。哨兵方式的话可以自动故障切换。
主从复制有哪几种常见的方式?
同步阻塞
优点:数据强一致性。(但是会破坏可用性,也就是CAP的A)
缺点:效率低,同步阻塞。
1657696521194 异步非阻塞(Redis默认采取的此种方式,效率高)
优点:效率高,异步非阻塞。
缺点:会丢失数据,满足了CAP的A,舍弃了CAP的C。
1657696554759 同步阻塞MQ(大数据hive采取的就是这种方式,他会保证最终一致性。)
优点:效率相对较高、能保证数据最终一致性。
缺点:没发现啥缺点。非要说缺点那就是有可能取到不一致的数据,因为不是强一致性。为什么Redis不采取这个?因为Redis要高效率,不想融入太多组件(MQ)进来。
1657696588783
主从复制的完整过程
- slave启动,这时候仅仅保存了master的信息,比如master的host和ip,复制流程还未开始。
- slave节点内部有个定时任务,每秒检查是否有新的master节点要连接和复制,若有,则跟master 节点建立socket网络链接。
- slave发送ping命令给master。
- 进行权限认证,若master设置了密码(requirepass),那么slave节点必须发送masteraut的命令过去进行认证。
- master节点第一次执行全量复制,将所有数据发送给slave节点。
- master后续持续写命令,异步复制给slave。
主从复制核心原理
- 当启动一个slave节点的时候他会发送 PSYNC命令给master节点
- 如果slave是第一次连接到master,那么会触发一次 full resynchronization 进行全量复制。开 始 full resynchronization 的时候,master会启动一个后台线程负责生成rdb文件,与此同时还会将客户端收到的所有写命令缓存到内存中。rdb文件生成完成后,master会将这个rdb发送给slave,slave会先把master传来的rdb写入磁盘然后清空自己的旧数据然后 从本地磁盘load到内存中进行同步数据,然后master会将内存中缓存的写命令发送给slave,slave也会同步这些数据。
- 若slave不是第一次连接到master,而是因为某种原因(比如网络故障)断开了进行重新链接的,那么master仅仅会复制给slave断开这段时间缺少的那部分数据,不会全量复制。这个称为断点续传。
怎么判断Slave和Master是不是第一次链接的?
- master节点会在内存中创建一个backlog,然后master和slave都会保存一个replica offset和一个master run id,offset就是保存在backlog中的。 若master和slave的网络链接断开了,那么slave会让master从上次的replica offset开始继续复制。若没找到对应的offset,则进行一次全量复制。
什么是主从复制的断点续传?
- redis2.8开始支持的主从复制断点续传,若主从复制过程中出现了网络故障导致网络链接断开了,那么slave重新连接到master的时候可以接着上次复制的地方继续复制下去,而不是从头开始复制。
怎么实现的断点续传?
- master节点会在内存中创建一个backlog,然后master和slave都会保存一个replica offset和一个master id,offset就是保存在backlog中的。 若master和slave的网络链接断开了,那么slave会让master从上次的replica offset开始继续复制。若没找到对应的offset,则进行一次全量复制。
什么是backlog?
- backlog是一个环形缓冲区,整个master进程中只会存在一个,所有的slave公用,默认大小是1MB,在master给slave复制数据的时候,也会将增量数据在backlog中同步写一份,backlog主要用于做全量复 制中断的时候的增量复制的。
replica offset是干嘛的?
- master和slave都会维护一个offset,master和slave都会不断累加offset,然后slave每秒都上报自己的offset给master,同时master也会保存每个slave的offset,然后slave上报offset给master的时候, master发现offset不一致就能发现数据不一致的情况了。
主从复制过期key处理
- 首先slave是不会过期key的,只会等master过期key,若master过期了一个key或者lru淘汰了一个key,那么master会模拟一条del命令发送给slave。
聊聊sentinel哨兵?
- 有哨兵之前都是手动进行故障转移。sentinel可以将之前纯人工进行故障操作的步骤自动化。哨兵主要包含以下功能:
- 集群监控,负责监控redis的master和slave进程是否正常工作
- 消息通知,若某个redis实例有故障,那么哨兵负责发送消息作为报警通知给管理员
- 故障转移,若master节点挂了,会自动转移到slave节点 上
- 配置中心,若故障转移发生了,会通知客户端新的master地址
- 哨兵也需要单独部署,但是他不负责读写请求,只是个看门狗,负责监控Redis集群和自动故障转移
哨兵之间是怎么发现彼此的 ?
- 是通过PSUBSCRIBE这个Redis内置的发布订阅命令来实现的,他们共同监听 sentinel:hello 这个channel,每隔两秒钟,每个哨兵都会往自己监控的某个master+slaves对应的 sentinel:hellochannel 里发送一个消息,内容是自己的host、ip和runid还有对这个master的监控配置,每个哨兵也 会去监听自己监控的每个master+slaves对应的 sentinel:hello channel ,然后去感知到同样在监听这个master+slaves的其他哨兵的存在,每个哨兵还会跟其他哨兵交换对master的监控配置,互相进行监控配置的同步。
什么是脑裂?如何解决?
脑裂:某个master所在机器突然脱离了正常的网络,跟其他slave机器不能连接,但是实际上master还运行着,此时哨兵可能就会认为master宕机了,然后开启选举,将其他slave切换成了master,这个时候,集群里就会有两个master,也就是所谓的脑裂。
解决方案
# 表示连接到master的最少slave数量 min-replicas-to-write 3 # 表示slave连接到master的最大延迟时间 min-replicas-max-lag 10
按照上面的配置,要求至少3个slave节点,且数据复制和同步的延迟不能超过10秒,否则的话master就 会拒绝写请求,配置了这两个参数之后,如果发生集群脑裂,原先的master节点接收到客户端的写入请求会拒绝,就可以减少数据同步之后的数据丢失(最多丢失10s)。
主从会造成数据不一致吗?如何解决?
会造成,如下两个可能:
- 因为master -> slave的复制是异步的,所以可能有部分数据还没复制到slave,master就宕机了,此时这些部分数据就丢失了。
- 脑裂的情况也会造成数据不一致。此时虽然某个slave被切换成了master,但是可能client还没来得及切换到新的master,还继续写向旧master的数据可能也丢失了,因此旧master再次恢复的时 候,会被作为一个slave挂到新的master上去,自己的数据会清空,重新从新的master复制数据
解决方案
# 表示连接到master的最少slave数量 min-replicas-to-write 3 # 表示slave连接到master的最大延迟时间 min-replicas-max-lag 10
按照上面的配置,要求至少3个slave节点,且数据复制和同步的延迟不能超过10秒,否则的话master就会拒绝写请求,配置了这两个参数之后,如果发生集群脑裂,原先的master节点接收到客户端的写入请求会拒绝,就可以减少数据同步之后的数据丢失(最多丢失10s)。是的,没有根治,只是减少了丢失量。若要当作db来用,需要零丢失的话,可以引入mq当中间件。
什么是主观宕机(sdown)?什么是客观宕机(odown)?
- sdown是主观宕机,就一个哨兵如果自己觉得一个master宕机了,那么就是主观宕机
- odown是客观宕机,如果quorum数量的哨兵都觉得一个master宕机了,那么就是客观宕机
master选举的时候会选择哪个slave?
- 跟master断开连接的时长,如果一个slave跟master断开连接已经超过了 down-after-milliseconds 配置的10倍,那么slave就被认为不适合选举为master
- 按照slave优先级进行排序,slave priority越低,优先级就越高
- 如果slave priority相同,那么看replica offset,哪个slave复制了越多的数据,offset越靠后,优先级就越高
- 如果上面两个条件都相同,那么选择一个run id比较小的那个slave
Redis集群最大节点个数是多少?
- 16384个
Redis集群是读写分离吗?
- cluster默认是不支持slave节点读或者写的,跟我们手动基于哨兵搭建的主从架构不一样的,需要手动带上readonly这个指令,这个时候才能在slave node进行读取。默认的话就是读和写都到master上去执行的,redis cluster模式下就不建议做物理的读写分离了,建议通过master的水平扩容,来横向扩展读写吞吐量,还有支撑更多的海量数据。
Redis集群如何做高可用的?
- Redis集群的高可用并不像哨兵那样需要单独部署哨兵程序,默认也不会像主从复制那样读写分离,Redis集群的slave节点仅仅做数据备份以及高可用,比如现在有5个master,每个master都有1个slave,然后新增了3个slave作为冗余,那么现在有的master就有2个slave了,也就是有的master出现了salve冗余。如果某个master的slave挂了,那么redis cluster会自动迁移一个冗余的slave给那个 master,所以集群部署的时候最好多冗余出几个slave做高可用。
- 或者通过链表方式进行配置:master<-slave<-slave ,这样master废了下一个slave可以顶上,其他slave不需要变动。
Redis集群如何扩一个节点?如何摘一个节点?
- 扩容:
- 启动新Redis实例当作master节点加入老集群
- reshard一些数据过去,也就是把一部分hash slot从一些node上迁移到这个新节点中,让集群16384个slot重新平均分配
- 启动新Redis实例当作slave节点加入老集群(为了高可用)
- 缩容:
- 先用reshard将数据都移除到其他节点
- 确保清空了一个master的hash slot时,redis cluster就会自动将其slave挂载到其他master上去
- 这个时候就只要删除掉master就可以了。
Redis集群Slave是如何进行升级为Master的?
和哨兵的类似的流程。
- 判断节点宕机,如果一个节点认为另外一个节点宕机,那么就是pfail(主观宕机),如果多个节点都认为另外一个节点宕机了,那么就是fail(客观宕机),跟哨兵的原理几乎一样,sdown, odown,在cluster-node-timeout内,某个节点一直没有返回pong,那么就被认为pfail,如果一个节点认为某个节点pfail了,那么会在gossip ping消息中,ping给其他节点,如果超过半数的节点都认为pfail了,那么就会变成fail。
- 从节点过滤,对宕机的master node,从其所有的slave node中,选择一个切换成master node,检查每个slave node与master node断开连接的时间,如果超过了 cluster-node-timeout *cluster-slave-validity-factor ,那么就没有资格切换成master,这个也是跟哨兵是一样的,从节点超时过滤的步骤。
- 从节点选举,每个从节点,都根据自己对master复制数据的offset,来设置一个选举时间,offset越大(复制数据越多)的从节点,选举时间越靠前,优先进行选举,所有的master node开始slave选举投票,给要进行选举的slave进行投票,如果大部分 master node(N/2 + 1) 都投票给了某个从节点,那么选举通过,那个从节点可以切换成master,从节点执行主备切换,从节点切换为主 节点。
6、场景问题
如何保证Redis里都是热点数据?
- redis内存数据集大小上升到一定大小的时候,就会进行数据淘汰策略,热点数据不会被淘汰掉。
Redis内存快用完了会怎样?
- 会进行内存淘汰机制,当Redis达到内存上限时会淘汰掉不活跃的数据。如果还是不够,内存还是满了的情况下Redis的写命令会返回错误信息,读命令还可以正常返回。
如何优化Redis内存?
- 选择合适的数据类型
- 大value选择gzip压缩
- 避免使用大key
- 批处理采取Pipeline(小心配置单次pipeline命令总大小,不要撑爆socket缓冲区)
Redis为什么这么快?
- 纯内存操作,且时间复杂度大多都是O(1),也有特例,比如zset底层跳表是log(n),keys*....等!
- 数据结构底层存储的设计比较节省空间
- 采取单线程避免了不必要的上下文切换。
- 采取I/O多路复用,epoll模型,非阻塞IO。
如何解决Redis的并发竞争key的问题?
- 问题描述:多个系统操作一个Key,本来想将这个Key 的 value依次改为 1,2,3,4. 但是因为并发问题造成乱序,1、3、4、2,最终结果变为2.
- 解决:
- 分布式锁
- 主要是用 setnx 的命令,SET if Not eXists,如果存在则返回失败。
- MQ串行化
如何用Redis实现分布式锁?
- setnx
- setnx简单,但要注意:value要设置为线程或者请求的id等作为加锁者的唯一标识,因为setnx为 了避免死锁是要设置超时时间的,如果线程A加锁之后,操作时间过长导致锁超时,这时线程B就能上锁了,但是就在这时,线程A工作完成,把锁给删了,这样就等于把线程B的锁删了。加唯一标识可以解决这个问题,但这也引出一个问题,就是最后释放删除锁时需要判断唯一标识是否和当前加锁者匹配,但这两步并非原子操作,还是有可能确认匹配之后,锁超时然后线程B上锁,但是线程A又把线程B的锁给干掉了。
Redis集群是前期做还是后期数据量上来了后在做?为什么?
- 建议提前做好数据量预估然后选择合适的Redis集群数,避免后期在新增服务器的时候还需要重新分片。目前Redis集群无法做到动态自动扩容,需要手动执行命令重新reshard。
生产环境部署Redis该如何选择用哪种方式?
- 单机直接pass掉,不考虑,单点故障!
- 如果数据量不大,一般来说你的缓存的总量在10G以内就可以,那么建议按照以下架构去部署redisredis持久化+备份方案+容灾方案+replication(主从+读写分离)+sentinal(哨兵集群,3个节 点,高可用性)
- 可以支撑的数据量在10G以内,可以支撑的写QPS在几万左右,可以支撑的读QPS可以上10万以上 (随你的需求,水平扩容slave节点就可以),可用性在99.99%。
- 如果你的数据量很大,海量数据,那么选择redis cluster方式部署,多master分布式存储数据,水平扩容,支撑更多的数据量,读写QPS分别都达到几十万都没问题,只要扩容master即可,redis cluster,读写分离,支持不太好,readonly才能去slave上读,支撑99.99%可用性,也没问题,slave -> master的主备切换,冗余slave去进一步提升可用性的方案(每个master挂一个slave,但是整个集群再加个3个slave冗余一下)
缓存雪崩
- 我们可以简单的理解为:由于原有缓存失效,新缓存未到期间 (例如:我们设置缓存时采用了相同的过期时间,在同一时刻出现大面积的缓存过期),所有原本应该访问缓存的请求都去查询数据库了,而对数据库CPU和内存造成巨大压力,严重的会造成数据库宕机。从而形成一系列连锁反应,造成整个系统崩溃。
- 解决办法:
- 大多数系统设计者考虑用加锁(最多的解决方案)或者队列的方式保证来保证不会有大量的线程对数据库一次性进行读写,从而避免失效时大量的并发请求落到底层存储系统上。还有一个简单方案就时讲缓存失效时间分散开。
缓存穿透
- 缓存穿透是指用户查询数据,在数据库没有,自然在缓存中也不会有。这样就导致用户查询的时候,在缓存中找不到,每次都要去数据库再查询一遍,然后返回空(相当于进行了两次无用的查询)。这样请求就绕过缓存直接查数据库,这也是经常提的缓存命中率问题。
- 解决办法
- 最常见的则是采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。
- 另外也有一个更为简单粗暴的方法,如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。通过这个直接设置的默认值 存放到缓存,这样第二次到缓冲中获取就有值了,而不会继续访问数据库,这种办法最简单粗暴。
缓存预热
- 缓存预热这个应该是一个比较常见的概念,相信很多小伙伴都应该可以很容易的理解,缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统。这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据!
- 解决
- 思路:
- 直接写个缓存刷新页面,上线时手工操作下;
- 数据量不大,可以在项目启动的时候自动进行加载;
- 定时刷新缓存;
缓存更新
- 除了缓存服务器自带的缓存失效策略之外(Redis默认的有6中策略可供选择),我们还可以根据具体的业务需求进行自定义的缓存淘汰,常见的策略有两种:
- 定时去清理过期的缓存;
- 当有用户请求过来时,再判断这个请求所用到的缓存是否过期,过期的话就去底层系统得到新数据并更新缓存。
- 两者各有优劣,第一种的缺点是维护大量缓存的key是比较麻烦的,第二种的缺点就是每次用户请求过来都要判断缓存失效,逻辑相对比较复杂!具体用哪种方案,大家可以根据自己的应用场景来权衡
什么是缓存击穿?如何解决?
- 某个 key 非常非常热点,访问非常的频繁,高并发访问的情况下,当这个 key在失效(可能expire过期了,也可能LRU淘汰了)的瞬间,大量的请求进来,这时候就击穿了缓存,直接请求到了数据库,一下子来这么多,数据库肯定受不了,这就叫缓存击穿。某个key突然失效,然后这时候高并发来访问这个key,结果缓存里没有,都跑到db了。和缓存雪崩不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。
- 解决方案
- 热点数据永不过期(看业务需求。不过很少有这种场景,一般都是比如搞活动或者大促的时候才会存这些热点数据,活动过后就需要到期,不会设置永不过期。),此方法简单粗暴,不过还看具体需求。
- 分布式锁。来个请求后先锁住,然后去db查,查到后再将数据set到redis里。只有当redis里getKey没拿到数据需要请求db的时候才加锁。不影响Redis里有数据的时候处理高并发请求,也能保证db不会被打垮
什么是缓存降级?如何实现?
- 就是当缓存出故障的时候,别影响主业务系统,让他可以继续对外提供服务。
- 实现方案
- 很简单,在catch里去查db,而不是抛出异常。
- 利用熔断降级组件返回错误码。
什么是热点key?如何安全的缓存热点key?
- 热点key就是频繁被访问的key,为了防止频繁访问数据库,所以需要将这些数据缓存起来,这些数据就称为热点数据。
- 一般采取分布式锁来安全的缓存热点key,也就是对查询缓存加锁,即如果key不存在就加锁,然后查数据库set到缓存,然后解锁。其他进程如果发现有锁就等待,然后等解锁后返回数据或者进入DB查询。
遇到过Redis常见性能问题吗?怎么解决的?
- for循环操作redis,几万个。换成Pipeline分批处理。
- key过大,采取缩写。
- value是个大json,用户信息。去除不必要的字段,并且采取gzip压缩成二进制存储。
- 单节点扛不住并发量,采取集群数据分片。
- 用户签到采取的string类型,换成bitmap。
- 禁用一些耗费性能的命令,比如:keys等。
用Redis如何实现mq?
- 采取list数据类型,rpush生产消息,lpop消费消息,阻塞式的可以用blpop,在没有信息的时候,会一直阻塞,直到信息的到来。
用Redis如何实现延迟队列?
- 采取zset数据类型,使用时间戳做score,然后用zadd来生产消息,消费组使用 zrangbyscore 获取x秒之前的数据做轮询处理。
Redis到底是单线程的还是多线程的?
- redis中io多路复用器模块是单线程执行,事件处理器也是单线程执行,两个线程不一样。所以实际redis应该是单进程多线程,只是不同的模块都用的单线程实现。 两个维度来举例:
- 若是client发送命令到server的话,server处理命令是单线程逐条进行的。
- server内部可以是多线程的,比如aof持久化,假设策略每秒,那就是再单独开启一个线程去执行aof文件持久化操作,这就是多线程了。
五种数据类型的底层存储原理是什么?
- string:int、raw、embstr
- list:ziplist、linkedlist
- hash:ziplist、hashtable
- set:intset、hashtable
- sorted set:ziplist、skiplist+dict
Redis的数据类型及使用场景
- String
- 最常规的 set/get 操作,Value 可以是 String 也可以是数字。一般做一些复杂的计数功能的缓存。
- Hash
- 这里 Value 存放的是结构化的对象,比较方便的就是操作其中的某个字段。我在做单点登录的时 候,就是用这种数据结构存储用户信息,以 CookieId 作为 Key,设置 30 分钟为缓存过期时间,能很好的模拟出类似 Session 的效果。
- List
- 使用 List 的数据结构,可以做简单的消息队列的功能。另外,可以利用 lrange 命令,做基于 Redis的分页功能,性能极佳,用户体验好。
- Set
- 因为 Set 堆放的是一堆不重复值的集合。所以可以做全局去重的功能。我们的系统一般都是集群部署,使用 JVM 自带的 Set 比较麻烦。另外,就是利用交集、并集、差集等操作,可以计算共同喜好,全部的喜好,自己独有的喜好等功能。
- Sorted Set
- Sorted Set 多了一个权重参数 Score,集合中的元素能够按 Score 进行排列。可以做排行榜应用,取 TOP(N) 操作。Sorted Set 可以用来做延时任务。
怎么提高缓存命中率?
- 主要常用的手段有:
- 提前加载数据到缓存中;
- 增加缓存的存储空间,提高缓存的数据;
- 调整缓存的存储数据类型;
- 提升缓存的更新频率。