Redis进阶--常见问题整理

一、在项目中缓存是如何使用的?缓存如果使用不当会造成什么后果?

1)项目中缓存是如何使用

看具体项目的应用场景,比如说:我的项目是一个和车辆上报的信息有关的项目,车的数量很多,上报很频繁,但是上报数据到项目时,需要拿到该车辆的信息(最大限速、规定的活动范围等等),来进行一些是否超速,是否越界的判断。这时候,如果从数据库中取,那么给数据库的压力会非常大,所以用缓存来缓解数据库压力。

2)为啥在项目里要用缓存呢?

用缓存,主要是俩用途,高性能和高并发

1)高性能

在这里插入图片描述
假设这么个场景,你有个操作,一个请求过来,吭哧吭哧你各种乱七八糟操作mysql,半天查出来一个结果,耗时600ms。但是这个结果可能接下来几个小时都不会变了,或者变了也可以不用立即反馈给用户。那么此时咋办?
缓存啊,折腾600ms查出来的结果,扔缓存里,一个key对应一个value,下次再有人查,别走mysql折腾600ms了。直接从缓存里,通过一个key查出来一个value,2ms搞定。性能提升300倍。

这就是所谓的高性能。

就是把你一些复杂操作耗时查出来的结果,如果确定后面不咋变了,然后但是马上还有很多读请求,那么直接结果放缓存,后面直接读缓存就好了。

2)高并发

在这里插入图片描述
mysql这么重的数据库,压根儿设计不是让你玩儿高并发的,虽然也可以玩儿,但是天然支持不好。mysql单机支撑到2000qps也开始容易报警了。

所以要是你有个系统,高峰期一秒钟过来的请求有1万,那一个mysql单机绝对会死掉。你这个时候就只能上缓存,把很多数据放缓存,别放mysql。缓存功能简单,说白了就是key-value式操作,单机支撑的并发量轻松一秒几万十几万,支撑高并发so easy。单机承载并发量是mysql单机的几十倍。

3)用了缓存之后会有啥不良的后果?

常见的缓存问题有:

1)缓存与数据库双写不一致
2)缓存雪崩
3)缓存穿透
4)缓存并发竞争

二、redis和memcached有什么区别?

1)Redis支持服务器端的数据操作:Redis相比Memcached来说,拥有更多的数据结构和并支持更丰富的数据操作,通常在Memcached里,你需要将数据拿到客户端来进行类似的修改再set回去。这大大增加了网络IO的次数和数据体积。在Redis中,这些复杂的操作通常和一般的GET/SET一样高效。所以,如果需要缓存能够支持更复杂的结构和操作,那么Redis会是不错的选择。
2)集群模式:memcached没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据;但是redis目前是原生支持cluster模式的,redis官方就是支持redis cluster集群模式的,比memcached来说要更好

三、redis的线程模型

在这里插入图片描述

1)文件事件处理器(上图总称)

redis基于反应器(reactor)模式开发了网络事件处理器,这个处理器叫做文件事件处理器(file event handler)。这个文件事件处理器,是单线程的,redis才叫做单线程的模型,采用IO多路复用机制同时监听多个socket,文件事件分派器根据socket上的事件来选择对应的事件处理器来处理这个事件。

如果被监听的socket准备好执行accept、read、write、close等操作的时候,跟操作对应的文件事件就会产生,这个时候文件事件分派器就会调用之前关联好的事件处理器来处理这个事件。

文件事件处理器是单线程模式运行的,但是通过IO多路复用机制监听多个socket,可以实现高性能的网络通信模型,又可以跟内部其他单线程的模块进行对接,保证了redis内部的线程模型的简单性。

Redis的结构包含4个部分:多个socket,IO多路复用程序,文件事件分派器,事件处理器(命令请求处理器、命令回复处理器、连接应答处理器)。

多个socket可能并发的产生不同的操作,每个操作对应不同的文件事件,但是IO多路复用程序会监听多个socket,但是会将socket放入一个队列中排队,每次从队列中取出一个socket给事件分派器,事件分派器把socket给对应的事件处理器。

然后一个socket的事件处理完之后,IO多路复用程序才会将队列中的下一个socket给事件分派器。文件事件分派器会根据每个socket当前产生的事件,来选择对应的事件处理器来处理。

2)文件事件

当socket会产生文件事件被IO多路复用程序所监听到,然后把该socket压到一个队列中。文件事件分派器每次会从队列中取一个socket来获取参数并分配处理器执行。

IO多路复用程序可以同时监听AE_REABLE和AE_WRITABLE两种事件,要是一个socket同时产生了AE_READABLE和AE_WRITABLE两种事件,那么文件事件分派器会优先处理AE_REABLE事件,然后才是AE_WRITABLE事件。

3)文件事件处理

连接应答处理器
命令请求处理器
命令回复处理器

4)客户端与redis通信的一次流程

1.Redis初始化时建立连接

客户端socket01会发送建立连接的请求到Redis中的server socket 上(server socket 专门用来创建连接),
server socket 会产生一个AE_READABLE事件被IO多路复用程序所监听到。因为此socket是第一次连接,所以会关联连接应答处理器执行。连接应答处理器的工作:创建服务器端对应的socket02,并将这个服务器端新创建的socket02的AE_READABLE事件绑定到命令请求处理器。也就是说,从第二次开始,以后AE_READABLE这个事件就会关联执行命令请求处理器了。

2.当客户端向redis发起请求的时候(不管是读请求还是写请求,都一样)

不管是读请求还是写请求,首先就会在socket产生一个AE_READABLE事件,然后由对应的命令请求处理器来处理(因为初始化AE_READABLE后绑定的是命令请求处理器)。这个命令请求处理器就会从socket中读取请求相关数据,然后进行执行和处理。

当redis准备好了给客户端的响应数据之后,就会将socket的AE_WRITABLE事件跟命令回复处理器关联起来。
当客户端这边准备好读取响应数据时,就会在socket产生一个AE_WRITABLE事件,会由对应的命令回复处理器来处理,就是将准备好的响应数据写入socket,供客户端来读取。

命令回复处理器写完之后,就会删除这个socket的AE_WRITABLE事件和命令回复处理器的关联关系。

5)为啥redis单线程模型也能效率这么高?

1)纯内存操作
2)核心是基于非阻塞的IO多路复用机制
3)单线程反而避免了多线程的频繁上下文切换问题

四、Redis都有哪些数据类型?分别在哪些场景下使用比较合适?

主要举例各数据类型的具体应用:

1)string

这是最基本的类型了,没啥可说的,就是普通的set和get,做简单的kv缓存

2)hash

这个是类似map的一种结构,这个一般就是可以将结构化的数据,比如一个对象(前提是这个对象没嵌套其他的对象)给缓存在redis里,然后每次读写缓存的时候,可以就操作hash里的某个字段。

key=150

value={
  “id”: 150,
  “name”: “zhangsan”,
  “age”: 20
}

hash类的数据结构,主要是用来存放一些对象,把一些简单的对象给缓存起来,后续操作的时候,你可以直接仅仅修改这个对象中的某个字段的值

value={
  “id”: 150,
  “name”: “zhangsan”,
  “age”: 21
}

3)list

有序列表,这个是可以玩儿出很多花样的

微博,某个大v的粉丝,就可以以list的格式放在redis里去缓存

key=某大v

value=[zhangsan, lisi, wangwu]

比如可以通过list存储一些列表型的数据结构类似粉丝列表了、文章的评论列表了之类的东西

比如可以通过lrange命令,就是从某个元素开始读取多少个元素,可以基于list实现分页查询,这个很棒的一个功能,基于redis实现简单的高性能分页,可以做类似微博那种下拉不断分页的东西,性能高,就一页一页走

4)set

无序集合,自动去重

直接基于set将系统里需要去重的数据扔进去,自动就给去重了,如果你需要对一些数据进行快速的全局去重,你当然也可以基于jvm内存里的HashSet进行去重,但是如果你的某个系统部署在多台机器上呢?

得基于redis进行全局的set去重

可以基于set玩儿交集、并集、差集的操作,比如交集吧,可以把两个人的粉丝列表整一个交集,看看俩人的共同好友是谁?对吧

把两个大v的粉丝都放在两个set中,对两个set做交集

5)sorted set

排序的set,去重但是可以排序,写进去的时候给一个分数,自动根据分数排序,这个可以玩儿很多的花样,最大的特点是有个分数可以自定义排序规则

比如说你要是想根据时间对数据排序,那么可以写入进去的时候用某个时间作为分数,人家自动给你按照时间排序了

排行榜
将每个用户以及其对应的什么分数写入进去,zadd board score username,接着zrevrange board 0 99,就可以获取排名前100的用户;zrank board username,可以看到用户在排行榜里的排名

zadd board 85 zhangsan
zadd board 72 wangwu
zadd board 96 lisi
zadd board 62 zhaoliu

96 lisi
85 zhangsan
72 wangwu
62 zhaoliu

zrevrange board 0 3 获取排名前3的用户

96 lisi
85 zhangsan
72 wangwu

五、redis的过期策略都有哪些?内存淘汰机制都有哪些?

1)设置过期时间

我们set key的时候,都可以给一个expire time,就是过期时间,指定这个key比如说只能存活1个小时?10分钟?这个很有用,我们自己可以指定缓存到期就失效。

如果假设你设置一个一批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内存块耗尽了,咋整?

答案是:走内存淘汰机制。

2)内存淘汰

如果redis的内存占用过多的时候,此时会进行内存淘汰,有如下一些策略:

redis 10个key,现在已经满了,redis需要删除掉5个key

1个key,最近1分钟被查询了100次
1个key,最近10分钟被查询了50次
1个key,最近1个小时被查询了1次
。。。

1)noeviction:当内存不足以容纳新写入数据时,新写入操作会报错,这个一般没人用吧,实在是太恶心了
2)allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key(这个是最常用的)
3)allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key,这个一般没人用吧,为啥要随机,肯定是把最近最少使用的key给干掉啊
4volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key(这个一般不太合适)
5volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key
6volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除

六、Redis持久化

redis如果仅仅只是将数据缓存在内存里面,如果redis宕机了,再重启,内存里的数据就全部都弄丢了,所以必须得用redis的持久化机制,将数据写入内存的同时,异步的慢慢的将数据写入磁盘文件里,进行持久化。这样一来,如果redis宕机了,重启启动,自动从磁盘上加载之前持久化的一些数据,就可以了,也许会丢失少许数据,但是至少不会将所有数据都弄丢。

redis持久化:RDB,AOF


1、RDB和AOF两种持久化机制的介绍

RDB持久化机制,对redis中的数据执行周期性的持久化

AOF机制对每条写入命令作为日志,以append-only的模式写入一个日志文件中在redis重启的时候,可以通过回放AOF日志中的写入指令来重新构建整个数据集

一般场景:

1)如果我们想要redis仅仅作为纯内存的缓存来用,那么可以禁止RDB和AOF所有的持久化机制

2)通过RDB或AOF,都可以将redis内存中的数据给持久化到磁盘上面来,然后可以将这些数据备份到别的地方去,比如说阿里云,云服务

3)如果redis挂了,服务器上的内存和磁盘上的数据都丢了,可以从云服务上拷贝回来之前的数据,放到指定的目录中,然后重新启动redis,redis就会自动根据持久化数据文件中的数据,去恢复内存中的数据,继续对外提供服务

4)如果同时使用RDB和AOF两种持久化机制,那么在redis重启的时候,会使用AOF来重新构建数据,因为AOF中的数据更加完整


2、RDB持久化机制的优点

快速恢复数据,对性能影响小,备份整个数据镜像BUG少

(1)RDB会生成多个数据文件,每个数据文件都代表了某一个时刻中redis的数据,这种多个数据文件的方式,非常适合做冷备,可以将这种完整的数据文件发送到一些远程的安全存储上去,比如说Amazon的S3云服务上去,在国内可以是阿里云的ODPS分布式存储上,以预定好的备份策略来定期备份redis中的数据

(2)RDB对redis对外提供的读写服务,影响非常小,可以让redis保持高性能,因为redis主进程只需要fork一个子进程,让子进程执行磁盘IO操作来进行RDB持久化即可

(3)相对于AOF持久化机制来说,直接基于RDB数据文件来重启和恢复redis进程,更加快速


3、RDB持久化机制的缺点

数据丢失多,备份时如果数据量大会导致服务暂停

(1)如果想要在redis故障时,尽可能少的丢失数据,那么RDB没有AOF好。一般来说,RDB数据快照文件,都是每隔5分钟,或者更长时间生成一次,这个时候就得接受一旦redis进程宕机,那么会丢失最近5分钟的数据

(2)RDB每次在fork子进程来执行RDB快照数据文件生成的时候,如果数据文件特别大,可能会导致对客户端提供的服务暂停数毫秒,或者甚至数秒


4、AOF持久化机制的优点

数据丢失少(1S),可以通过日志来进行误删除恢复

(1)AOF可以更好的保护数据不丢失,一般AOF会每隔1秒,通过一个后台线程执行一次fsync操作,最多丢失1秒钟的数据

(2)AOF日志文件以append-only模式写入,所以没有任何磁盘寻址的开销,写入性能非常高,而且文件不容易破损,即使文件尾部破损,也很容易修复

(3)AOF日志文件即使过大的时候,出现后台重写操作,也不会影响客户端的读写。因为在rewrite log的时候,会对其中的指导进行压缩,创建出一份需要恢复数据的最小日志出来。再创建新日志文件的时候,老的日志文件还是照常写入。当新的merge后的日志文件ready的时候,再交换新老日志文件即可。

(4)AOF日志文件的命令通过非常可读的方式进行记录,这个特性非常适合做灾难性的误删除的紧急恢复。比如某人不小心用flushall命令清空了所有数据,只要这个时候后台rewrite还没有发生,那么就可以立即拷贝AOF文件,将最后一条flushall命令给删了,然后再将该AOF文件放回去,就可以通过恢复机制,自动恢复所有数据


5、AOF持久化机制的缺点

备份文件大,比RDB写性能慢一点,日志回放容易出现BUG

(1)对于同一份数据来说,AOF日志文件通常比RDB数据快照文件更大

(2)AOF开启后,支持的写QPS会比RDB支持的写QPS低,因为AOF一般会配置成每秒fsync一次日志文件,当然,每秒一次fsync,性能也还是很高的

(3)以前AOF发生过bug,就是通过AOF记录的日志,进行数据恢复的时候,没有恢复一模一样的数据出来。所以说,类似AOF这种较为复杂的基于命令日志/merge/回放的方式,比基于RDB每次持久化一份完整的数据快照文件的方式,更加脆弱一些,容易有bug。不过AOF就是为了避免rewrite过程导致的bug,因此每次rewrite并不是基于旧的指令日志进行merge的,而是基于当时内存中的数据进行指令的重新构建,这样健壮性会好很多。


6、RDB和AOF到底该如何选择

同时使用

(1)不要仅仅使用RDB,因为那样会导致你丢失很多数据

(2)也不要仅仅使用AOF,因为那样有两个问题,第一,你通过AOF做冷备,没有RDB做冷备,来的恢复速度更快; 第二,RDB每次简单粗暴生成数据快照,更加健壮,可以避免AOF这种复杂的备份和恢复机制的bug

(3)综合使用AOF和RDB两种持久化机制,用AOF来保证数据不丢失,作为数据恢复的第一选择; 用RDB来做不同程度的冷备,在AOF文件都丢失或损坏不可用的时候,还可以使用RDB来进行快速的数据恢复

七、了解什么是redis的雪崩和穿透?redis崩溃之后会怎么样?系统该如何应对这种情况?如何处理redis的穿透?

缓存雪崩

什么是缓存雪崩:
在这里插入图片描述
如何应对缓存雪崩:
在这里插入图片描述
事前:redis高可用,主从+哨兵,redis cluster,避免全盘崩溃
事中:本地ehcache缓存 + 限流&降级,避免MySQL被打死
事后:redis持久化,快速恢复缓存数据

缓存穿透

在这里插入图片描述
由于缓存未命中,导致数据库压力大。
解决方案:
1)判断请求参数的正确性
2)只要在数据库里查不到的,在缓存里存该key的值为UNKNOWN,下次缓存就能拦截到这个 KEY的请求。

八、数据库和缓存的数据一致性问题

1、缓存不一致问题以及解决方案

问题:先修改数据库,再删除缓存,如果删除缓存失败了,那么会导致数据库中是新数据,缓存中是旧数据,数据出现不一致

解决思路:先删除缓存,再修改数据库

但是如果删除缓存成功了,如果修改数据库失败了,那么数据库中是旧数据,缓存中是空的,那么数据还是会不一致

因为读的时候缓存没有,则读数据库中旧数据,然后更新到缓存中

2、比较复杂的数据不一致问题分析

数据发生了变更,先删除了缓存,然后要去修改数据库,此时还没修改,此时一个请求过来,去读缓存,发现缓存空了,去查询数据库,查到了修改前的旧数据,放到了缓存中。然后数据变更的程序才完成了数据库的修改。导致,数据库和缓存中的数据不一样了。。。。

3、数据库与缓存更新 与 读取操作进行异步串行化

同步数据方案:
更新数据的时候,根据数据的唯一标识(类似ID),将删除缓存操作+更新数据库操作 路由之后,发送到一个jvm内部的队列中

读取数据的时候,如果发现数据不在缓存中,那么将重新读取数据+更新缓存的操作,根据数据唯一标识(类似ID)路由之后,也发送同一个jvm内部的队列中

此时一个队列对应一个工作线程,每个工作线程串行拿到对应的操作,然后一条一条的执行

场景举例:
一个数据变更的操作,先执行:删除缓存,然后再去更新数据库,但是还没完成更新,此时如果一个读请求过来,读到了空的缓存,那么可以先将缓存更新的操作请求发送到同一队列中,此时会在队列中积压,然后同步等待缓存更新完成。避免出现上述分析的问题。

优化–队列中更新缓存的请求去重
这里有一个优化点,一个队列中,其实多个更新缓存请求串在一起是没意义的,因此可以做过滤,如果发现队列中已经有一个更新缓存的请求了,那么就不用再放个更新请求操作进去了,直接等待前面的更新操作请求完成即可,待那个队列对应的工作线程完成了上一个操作的数据库的修改之后,才会去执行下一个操作,也就是缓存更新的操作,此时会从数据库中读取最新的值,然后写入缓存中,如果请求还在等待时间范围内,不断轮询发现可以取到值了,那么就直接返回; 如果请求等待的时间超过一定时长,那么这一次直接从数据库中读取当前的旧值。

九、redis的并发竞争问题是什么?如何解决这个问题?

在这里插入图片描述
解决方案:
从数据库中查询时,查询出该值被更新的时间戳,在更新缓存时,比较缓存中的时间戳和要更新的时间戳,如果缓存中的时间戳更大,就不更新。
多个系统/进程要更新同一个KEY时,需要获取分布式锁,可以通过zookeeper的目录服务实现。拿到锁时才可以操作缓存。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值