Java后端面试:Redis面试篇(原理+场景题)

👨‍🎓作者简介:一位大四、研0学生,正在努力准备大四暑假的实习
🌌上期文章:首期文章
📚订阅专栏:Java后端面试
希望文章对你们有所帮助

缓存穿透

缓存穿透:查询一个不存在的数据,MySQL查询不到数据,也就不会直接写入Redis缓存,就会导致每次请求都查数据库,恶意攻击下容易把数据库搞崩。

解决方案

方案一——缓存空数据

缓存空数据,查询返回的数据为空,就把这个空结果缓存到Redis。
例如:{key:-1,value:null}
优点:简单
缺点:消耗内存(假如空数据的数量很多,Redis就要存储很多空数据)。

方案二——布隆过滤器

在请求和Redis之间增加布隆过滤器,用户先请求布隆过滤器,不存在就直接拒绝。这底层是通过算法实现的。
bitmap(位图):以位为单位的数组,只存储0、1。
布隆过滤器作用:检索一个元素是否在一个集合中。
在这里插入图片描述
但是可能会出现误判:
在这里插入图片描述
误判率:数组越小误判率越大,数组越大误判率越小,但是也会带来更大内存消耗。
实际应用的时候可以通过Redisson设置误判率,底层会自动创建相应的数组,做相应的操作。

优点:内存占用少,没有多余的key
缺点:复杂,存在误判

总结

缓存穿透:查询一个不存在的数据,MySQL查询不到数据,也就不会直接写入Redis缓存,就会导致每次请求都查数据库。
解决方案
1、缓存空数据
2、布隆过滤器

缓存击穿

缓存击穿:给一个key设置了过期时间,当key过期了,key重建还没完成,恰好有大量的并发请求过来,可能会瞬间把数据库压垮。

解决方案

方案一——互斥锁

在这里插入图片描述
**特点:**强一致、性能差

方案二——逻辑过期

逻辑过期的方案,并不会给key设置过期时间,而是特意在value中设置了过期字段expire。
如果查询发现expire到期了,就会获取互斥锁,但这个锁只是为了开辟新线程做数据重建的,而数据重建完成之前,查询的数据都直接返回,当然了,这时候是旧数据。
在这里插入图片描述
特点:高可用、性能优

总结

缓存击穿:给一个key设置了过期时间,当key过期了,key重建还没完成,恰好有大量的并发请求过来,可能会瞬间把数据库压垮。
解决方案
1、互斥锁:强一致但性能差。
2、逻辑过期:高可用、性能优,但是不能保证数据的绝对一致性。

缓存雪崩

缓存雪崩:同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。

解决方案
1、给不同的key的TTL设置随机值,这样就不会出现大量的缓存key同时失效。
2、利用Redis集群提高服务的可用性(哨兵模式、集群模式)。
3、给缓存业务添加降级限流的策略(nginx、SpringCloud网关):降级可以作为系统的保底策略,适用于穿透、击穿、雪崩。
4、给业务添加多级缓存。

双写一致性

Redis作为缓存,MySQL中的数据要和Redis的数据同步,即双写一致性问题。
这其实是分业务来看的,可能业务的要求是一致性要求要很高,可能业务的要求允许延迟一致,根据具体的业务来分析。

双写一致性:当修改了MySQL的数据,也要同时更新缓存的数据,保持数据的一致性。

  • 读操作:缓存命中,直接返回;缓存未命中就查询MySQL,再写入缓存,设定超时时间。
  • 写操作:延时双删

延时双删

也就是:(1)删除缓存;(2)修改数据库;(3)延时后删除缓存。
需要思考一些问题:
1、先删除缓存还是先修改数据库?
答案是两种方式都会遇到问题,都有可能会读到脏数据,具体分析可以看我之前Redis专栏中的文章。
2、为什么要删除两次缓存?
先删除缓存,再修改数据库,还是有可能会读到脏数据,双删可以解决。
3、为什么要延时双删?
因为数据库一般是主从分离的,修改完数据库,要让主节点的数据同步到从节点。

但是因为延时双删的延时时间难以控制,所以还是有读脏数据的风险。

分布式锁(读写锁)

如果一定要避免数据一致性,可以加分布式锁。
但是分布式锁也会影响性能,因此适合读多写少的情况。

当我们读和写的时候都加锁,就会大大提升性能,同时保证强一致性。
共享锁:读读共享,写互斥
排他锁:读写互斥

总结

1、实时性要求不高:用异步方案同步数据(可以想到MQ)
2、实时性要求很高:需要保证数据的强一致,采用Redisson提供的读写锁来保证数据的同步
3、异步方案:允许延时一致的业务,采用异步通知。(MQ中间件:更新数据后,通知缓存删除)
4、强一致性:使用Redisson提供的读写锁

持久化

RDB

RDB(Redis Database Backup file,Redis数据备份文件),也叫Redis数据快照,把内存中的所有数据都记录到磁盘中,当Redis实例故障重启后,从磁盘读取快照文件,恢复数据。
人工备份:

save # Redis主进程执行备份,会阻塞其他命令,不推荐
bgsave # 开启子进程执行备份,推荐

Redis内部有出发RDB的机制,可以配置redis.conf文件:

# 900s内有至少1个key被修改,执行bgsave
save 900 1

RDB的执行原理

bgsave开始时会fork主进程得到子进程,子进程共享主进程的内存数据,完成fork后读取内存数据并写入RDB文件。
需要注意,当fork出子进程后,不会把物理内存拷贝过去,仅仅是拷贝页表过去,因为页表本身记录了虚拟地址与物理地址的映射关系
子进程写新的RDB文件,会替换磁盘中旧的RDB文件,但是如果此时主进程还在进行写操作,可能保存进去的又是脏数据,导致数据不一致。

因此,fork采用的是copy-to-write技术:

1、当主进程执行读操作时,访问共享内存
2、当主进程执行写操作时,会拷贝一份数据,然后再执行写操作

AOF

AOP(追加文件,Append Only File),Redis处理的每一个写命令都会记录在AOF文件,可以看作是命令日志文件。
AOF默认关闭,需要修改redis.conf配置文件来开启AOF:

appendonly yes
appendfilename "appendonly.aof" # AOF文件名

命令记录的频率也可以通过redis.conf配置:

# 每执行一次写,都会记录
appendfsync always
# 写命令执行完先放AOF缓冲区,再每隔1s将缓冲区数据写到AOF文件,是默认方案
appendfsync everysec
# 写命令执行完先放AOF缓冲区,由操作系统决定何时将缓冲区内容写回磁盘
appendfsync no

通过执行bgrewriteaof命令,可以让AOF文件执行重写功能(可能一个key被修改多次,但是只有最后一次有用),用最少命令达到相同的效果。
例如,set num 123、set name jack、set num 666,执行完bgrewriteaof后变为mset name jack num 666
Redis也会在触发阈值时自动去重写AOF文件,在redis.conf中配置:

# 比上次文件增长多少百分比,则触发重写
auto-aof-rewrite-percentage 100
# 体积到了多大触发重写
auto-aof-rewrite-min-size 64mb

数据过期策略

一种常见的问法:Redis的key过期之后,会立即删除吗?这种问题其实就是在问数据过期的策略。
数据过期策略:Redis对数据设置数据的有效时间,数据过期之后,需要从内存中删除,可以按照不同的规则删除,这种删除规则被称为数据的删除策略(数据过期策略)。
在Redis中,有两种数据过期策略:惰性删除、定期删除

惰性删除

惰性删除:设置该key过期时间后,我们不去管,当需要该key时,我们再检查其是否过期,若过期,则删除,反之返回该key。

set name wxj 10
get name # 发现name过期,直接删除key

优点:对CPU友好,只会在使用该key时才会进行过期检查,对于很多用不到的key不用浪费时间进行过期检查。
缺点:对内存不友好,若一个key已经过期但一直没使用,内存也不释放,造成内存浪费。

定期删除

定期删除:每隔一段时间,对一定的key进行检查,删除里面过期的key(从一定数量的数据库中取出一定数量的随机key进行检查,并删除其中的过期key)。
定期删除的两种模式:

  • SLOW模式:定时任务,执行频率默认10hz,每次不超过25ms,通过修改redis.conf的hz选项来调整次数
  • FAST模式:执行频率不固定,但两次间隔不低于2ms,每次耗时不超过1ms

优点:可以通过限制删除操作执行的时长和频率来减少删除操作对CPU的影响。另外定期删除也能释放过期键占用的内存。
缺点:难以确定删除执行的时长和频率。

Redis的过期删除策略:惰性删除+定期删除两种策略进行配合使用。

数据淘汰策略

假如缓存过多,内存被占满,就需要使用数据淘汰策略。
数据淘汰策略:当Redis内存不够用时,此时向Redis中添加新的key,那么Redis就会按照某一种规则将内存中的数据删除掉,这种数据的删除规则被称之为内存的淘汰策略。
Redis只是8种策略来选择要删除的key:

  • noeviction:不淘汰任何key,但内存满了不允许写入新数据(默认)
  • volatile-ttl:对设置了TTL的key,比较key的剩余TTL值,TTL越小越先被淘汰
  • allkeys-random:对全体key随机淘汰
  • volatile-random:对设置了TTL的key随机淘汰
  • allkeys-lru:对全体key,基于LRU(最近最少使用)算法进行淘汰
  • volatile-lru:对设置了TTL的key,基于LRU算法进行淘汰
  • allkeys-lfu:对全体key,基于LFU(最少频率使用)算法进行淘汰
  • volatile-lfu:对设置了TTL的key,基于LFU算法进行淘汰

使用建议

1、优先使用allkeys-lru,它把最近最常访问的数据留在了缓存中,若业务有明显的冷热数据区分则非常建议使用(而LFU有缺陷,虽然也统计频率,但是无法展示出最近常用)。
2、业务数据访问频率差别不大,用volatile-random。
3、若业务有置顶的需求,可用volatile-lru,同时置顶数据不设置过期时间,这样就不会被删除了,就会淘汰其他设置过期时间的数据。
4、业务中有短时高频访问的数据,可以用allkeys-lfu或volatile-lfu。

常见面试题

1、数据库中有1000万数据,Redis只能缓存20W,如何保证Redis中的数据都是热点数据?

使用allkeys-lru策略,流下来的都是经常访问的热点数据

2、Redis的内存用完了会发生什么?

主要看数据的淘汰策略,若是默认的,则会直接报错

Redis分布式锁

使用场景

大家应该做过抢券的业务,如果不加锁,可能会出现超卖。对于一个单体项目,我们可以直接加一个锁synchronized来对这部分逻辑进行加锁,单体项目的情况下自然就不会出现超卖。
但若是在集群部署的项目下,synchronized只是针对当前的JVM的,因此不同的服务器端口的synchronized锁彼此并不会影响,依旧会发生超卖问题。
因此我们要对集群增加一个分布式锁,让不同JVM中的线程都可以互斥的进行抢券操作。

实现原理

setnx

setnx表示SET if not exists。
获取锁

# NX表示互斥,EX表示超时时间
SET lock value NX EX 10

释放锁

DEL key

为什么要设置超时时间?

如果不设置超时时间,此时若业务超时了,或者服务宕机了,其他的业务需要获取分布式锁就会失败,因此需要设置超时时间。

但Redis实现分布式锁是如何合理控制锁的有效时长的呢?

Redisson的看门狗机制可以实现

Redisson看门狗机制(面试高频)

其实Redisson本身也是对setnx做了很多的优化的,需要掌握加锁和释放锁的流程。
加锁流程:

对业务加锁,若成功,则可以操作Redis。但增加了看门狗(Watch Dog),看门狗会不断的监听持有锁的线程,且每隔(releaseTime / 3)的时间做一次续期,其中releaseTime的默认值为30,也就是说每隔10s就会做一个续期(续期就是回归之前的过期时间)

释放锁:

手动释放锁,需要通知一下对应线程的看门狗,让它不用再去做监听了,因为锁已经被删除了

除了这些操作,还有其他的细节,如果此时有其他线程尝试获取锁失败,则会进入while训练,不断尝试获取锁,但需要设定阈值(最大等待时间),这是Redisson看门狗机制中的重试机制

注意:加锁、设置过期时间等操作都是基于Lua脚本完成的,以此来保持原子性

可重入

方法一获取分布式锁成功,方法一的方法体中调用了方法二,方法二也尝试获取同一个分布式锁,这在Redisson中是可以获取成功的,这也就是Redisson分布式锁的可重入特性。

这是因为方法一调用方法二,它们其实是用的同一个线程的,而分布式锁正是以线程为单位的,因此是可以获取成功的。
同时Redisson会利用hash结构来记录线程id重入次数,当同一个线程的某个方法释放了分布式锁,该分布式锁并不会真正的释放,而是将重入次数-1,当重入次数为0时,该锁会被真正释放。

主从一致性

可以根据一个场景的问题来分析:

主节点是写数据的,从节点是读数据的。若此时进行写操作,主节点就会获取分布式锁并写数据,写完数据后要同步到从节点。但如果还没有同步完成主节点就宕机了,根据Redis的哨兵机制,会选出一个从节点来作为新的主节点。因为主从同步没完成,锁失效了,这样就会发生线程并发问题。

因此要保证主从一致性,就需要使用红锁RedLock:

不能只在一个Redis实例上创建锁,应该是在多个Redis实例上创建锁(n/2+1),避免在一个Redis实例上加锁。

总结

Redisson实现分布式锁是如何合理控制锁的有效时长的?

在Redisson的分布式锁中,提供了一个WatchDog(看门狗),一个线程获取锁成功以后,WatchDog会给持有锁的线程续期(默认10s续期一次)

Redisson是可重入的吗?

是的,多个锁重入需要判断是否为当前线程,在Redis中进行存储的时候使用的hash结构,来存储线程信息重入次数

Redisson锁能否解决数据的主从一致问题

不可以,可以用红锁解决,但性能太低,如果要求强一致性,推荐使用zookeeper实现的分布式锁

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

布布要成为最负责的男人

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值