Java面试---redis篇
1.为什么要用缓存?
常见的sql数据库(如mysql,oracle等)的数据是存在磁盘中的,虽然数据库本身会有缓存技术来减少数据库IO的压力,但是由于数据库的缓存一般是针对于查询内容,并且粒度较小,一般只有表中数据没变化时,数据库中的缓存才会发产生作用。这并不能减轻数据库增删改的IO压力,因此缓存技术应运而生,该技术实现了对热点数据的高速缓存,缓解数据库压力。
数据库本身缓存技术?(mysql/oracle)
主流应用架构:
2.什么样的数据适合缓存?
3.什么是缓存击穿?
使用不存在的key频繁大量的进行高并发查询,导致缓存无法命中,每次请求都要击穿到后端数据库查询,最终可能导致数据库服务崩溃。
可以将空值缓存,并且设置较短的过期时间以应对恶意攻击,设置较短的时间能避免缓存空间的浪费。
避免恶意攻击方猜到我们使用这种策略,进而选择使用不同的key来攻击,可以进行数据拦截校验,如果key不符合我们的规则,直接返回,能拦截一部分的恶意请求。
4.什么是缓存并发?
当缓存失效时,高并发场景下,可能有大量请求同时访问同一个key,并且要写入数据到缓存,会增加应用和数据库的压力,违背了缓存的设计初衷,可能导致应用崩溃或者数据库崩溃。
1.加锁(分布式锁/本地锁):
使用分布式锁(本地锁),保证同一个key只有一个线程去访问数据库,其他线程等待数据写入缓存后,从缓存读取数据即可。
图示双重检查锁
2.不设置缓存过期时间,由后端定时任务控制刷新缓存数据,这样就不需要担心缓存过期,但要考虑数据量大的话,分批刷新缓存数据
5.什么是缓存雪崩?
缓存雪崩,是指在同一时刻,缓存集体失效,会导致数据库在这个时刻承受巨大压力。
6.什么时候使用redis?什么时候使用memcache?
主要有以下方面:(详见链接)
内存结构
持久化
高可用
内存分配
7.为什么redis这么快?
redis效率很高,官方给出的数据是100000+QPS(每秒查询率)
1.redis完全基于内存
2.redis是使用单进程单线程的(k,v)数据库,数据存储在内存中,不受磁盘I/O制约
3.数据结构简单,操作也简单,redis是no-sql数据库,不使用表,才用key-value键值对存储,存取效率高
4.redis使用多路I/O复用模型,为非阻塞I/O,redis使用的I/O多路复用函数是:
epoll/kqueue/evport/select
选用策略:
因地制宜,优先选择时间复杂度为 O(1) 的 I/O 多路复用函数作为底层实现。
由于 Select 要遍历每一个 IO,所以其时间复杂度为 O(n),通常被作为保底方案。 基于 React 设计模式监听 I/O 事件。
redis数据类型?
1.String类型:最大512M,二进制安全(可包含任何二进制数据,包括jpg等)
重复写入,会覆盖之前数据
2.Hash类型:String元素组成的字典,用于储存对象
3.List类型:列表,按照String插入顺序,后入先出,具有栈的特性(最新消息排行)
4.Set类型,String元素组成,无序集合,通过哈希表实现(增删改查时间复杂度O(1)),不允许重复
5.Sorted Set:通过分数来为集合排序(从小到大),不允许重复
更高级的redis数据类型:
用于计数的HyperLogLog,用于支持存储地理位置信息的Geo
8.从海量key中查询出某一个固定前缀的key?
1.使用keys[pattern]:查找符合条件的所有key,一次性返回,可能造成redis卡顿,同时也会大量消耗内存
2.使用scan cursor [pattern] [count]
cursor:游标
pattern:查询条件
count:返回的条数
9.分布式锁?
控制分布式系统之间共同访问共享资源的一种锁的实现。如果一个系统,或者不同系统之间共享某个资源时,要互斥,确保数据的一致性
分布式锁要解决的问题:
1.互斥性:任意时刻,只能有一个客户端获取锁
2.安全性:锁只能被持有的客户端删除
3.死锁:当客户端获取锁以后宕机,由于没有释放锁,会导致其他客户端也不能持有锁,需要解决来避免死锁
4.容错:当某个redis节点宕机的时候,客户端依然可以持有和释放锁。
10.如何通过redis实现分布式锁?
1.通过setnx来实现:key不存在,就创建并且赋值,返回1,否则返回0
setnx操作是原子性的
setnx是长久存在的,所以当持有锁的客户端,由于某些问题没有释放锁的时候,会导致其他客户端一直不能正常持有锁。
可以通过expire 指令来设置锁的过期时间
程序:
问题在于,这两步操作并非原子性操作,所以当在设置过期时间之前,程序就出现故障时,锁还是无法释放
2.从redis的2.6.12版本以后,可以通过set指令来完成
ex seconds 键过期时间为seconds秒
px milliseconds 键过期时间为milliseconds毫秒
nx 只有键不存在时才进行操作
xx 只有键存在时才进行操作
设置成功返回ok,失败返回nil。
代码示例:
RedisService redisService = SpringUtils.getBean(RedisService.class);
String result = redisService.set(lockKey,requestId,SET_IF_NOT_EXIST,SET_WITH_EXPIRE_TIME,expireTime);
if("OK".equals(result)){
doOcuppiredWork();
}
3.此外还可以通过lua脚本来实现设置锁的原子性操作
11.如何实现异步队列?
1.使用redis中的list作为队列
rpush生产消息,lpop消费消息
缺点:
lpop不会等待生产队列中有消息才会去消费,而是直接消费
解决:
通过在应用层引入sleep机制去调用lpop重试
2.使用 BLPOP key [key…] timeout
缺点:生产后只能给单一消费者消费
3.pub/sub:主题订阅模式
订阅者可以订阅任意数量的频道
缺点:消息发布是无状态的,无法确保消息的送达。比如一个消费者在消息发布时下线,那么重新上线以后无法接收到消息。如果要求较高,最好使用专业的消息队列,如kafka,active mq等~
12.redis持久化
1.什么是持久化?
数据持久储存,redis的数据一般存于内存中,如果内存断电,则数据也会丢失,redis有持久化机制来解决这一问题。
2.redis如何持久化?
RDB和AOF。
RDB(快照):会在某个特定的间隔,保存那个时间点的全量数据快照。
redis RDB配置:
a.RDB的创建和载入:
save:save指令会阻塞redis进程,知道整个rdb文件创建完成,因此很少使用
bgsave:fork一个子进程来进行rdb文件的写入。子进程完成创建会想父进程发送信号,父进程在接收客户端请求的过程中,在一定的时间间隔会通过轮询来接收子进程的信号
可以通过lastsave指令来查看bgsave的执行是否成功,lastsave会返回最后一次bgsave执行成功的时间
b.自动化触发rdb持久化
方式:
根据redis.conf配置里面的save m n定时触发(实际上使用的是bgsave)
主从复制时,主节点自动触发
执行debug reload
执行shutdown 且没有开启AOF
c.bgsave的执行原理
启动:
检查是否有子进程正在执行AOP或者RDB持久化任务,如果是,直接返回false
调用redis源码中的rdbbackground方法,方法中执行fork(),产生子进程,执行rdb操作
fork()中的copy-on-write
fork() 在 Linux 中创建子进程采用 Copy-On-Write(写时拷贝技术),即如果有多个调用者同时要求相同资源(如内存或磁盘上的数据存储)。
他们会共同获取相同的指针指向相同的资源,直到某个调用者试图修改资源的内容时,系统才会真正复制一份专用副本给调用者,而其他调用者所见到的最初的资源仍然保持不变。
缺点:
全量同步,数据量大的时候,可能会由于I/O严重影响性能
可能会丢失从当前到最近一次快照期间的数据
AOF持久化:保存写状态
AOF持久化是通过保存redis写状态来记录数据库的,保存的是redis出查询外的变更数据库的指令
以增量的形式,保存在aof文件中
开启AOF持久化?
1.将redis.conf中的appendonly修改为yes
2.修改appendfsync属性,有三个值:
always:即时写入
everysec:每隔一秒写入一次
no:由操作系统决定,一般来说,考虑效率问题,操作系统会等待缓存区满后才写入
13.日志重写:解决AOF文件不断增大的问题:
例如,计数器递增100次,使用rdb只需要存储最终结果100,而aof的方式会存储100条操作记录,事实上恢复数据,只需要一条指令即可,实际上,这100条指令可以精简为1条
redis支持这样的功能,在不中断服务的情况下,fork()子进程,重写AOF文件,同样用到了c-o-w(写时拷贝)
重写过程:
调用fork(),创建一个子进程
子进程把新的AOF文件写到一个临时文件中,不依赖原有的AOF文件
主进程持续将新的变动同时写到内存和原AOF文件
主进程获取子进程重写AOF完成信号,往新的AOF文件增量变动
用新的AOF文件替换旧的
两种方式优缺点比较:
RDB优点:数据量相对较小,恢复快
RDB缺点:无法报错最近一次快照之后的数据
AOF优点:可读性高,适合保存增量数据,不易丢失
AOF缺点:文件较大,恢复慢
RDB-AOF混合的持久化方式:
redis 4.0以后,推出了这种持久化方式,RDB做全量备份,AOF做增量备份,并且作为默认方式使用。
在RDB-AOF方式下,持久化策略是,先将缓存中数据以RDB的方式写入文件,再将增量数据以AOF的方式追加在RDB文件后面,下一次RDB持久化的时候将AOF数据重新写入。
14.redis高可用?
主从复制
redis集群(哨兵模式)
15.Redis常见数据丢失情况分析及解决
情况分析
(1)异步复制导致的数据丢失
因为master->slave的数据同步是异步的,所以可能存在部分数据还没有同步到slave,master就宕机了,此时这部分数据就丢失了。
(2)脑裂导致的数据丢失
当master所在的机器突然脱离的正常的网络,与其他slave、sentinel失去了连接,但是master还在运行着。此时sentinel就会认为master宕机了,会开始选举把slave提升为新的master,这个时候集群中就会出现两个master,也就是所谓的脑裂。
此时虽然产生了新的master节点,但是客户端可能还没来得及切换到新的master,会继续向旧的master写入数据。
当网络恢复正常时,旧的master会变成新的master的从节点,自己的数据会清空,重新从新的master上复制数据。
解决方案
Redis提供了这两个配置用来降低数据丢失的可能性
min-slaves-to-write 1
min-slaves-max-lag 10
上面两行配置的意思是,要求至少有1个slave,数据复制和同步的延迟不能超过10秒,如果不符合这个条件,那么master将不会接收任何请求。 (1)减少异步复制的数据丢失
有了min-slaves-max-lag这个配置,就可以确保,一旦slave复制数据和ack延时太长,就认为master宕机后损失的数据太多了,那么就拒绝写请求,这样可以把master宕机时由于部分数据未同步到slave导致的数据丢失降低到可控范围内。
(2)减少脑裂的数据丢失
如果一个master出现了脑裂,跟其他slave丢了连接,那么上面两个配置可以确保,如果不能继续给指定数量的slave发送数据,而且slave超过10秒没有给自己ack消息,那么就直接拒绝客户端的写请求 这样脑裂后的旧master就不会接受client的新数据,也就避免了数据丢失。 Redis并不能保证数据的强一致性,看官方文档的说明