缓存异常
- 缓存穿透:数据不存在
- 布隆过滤器
- 缓存击穿:热点数据过期
- 互斥锁
- 逻辑过期(不设置真正的过期时间)
- 缓存雪崩:大量数据同时过期
- 设置随机过期时间
- redis集群
缓存穿透
-
什么是缓存穿透?
- 就是查询一个不存在的数据时,由于在缓存中查不到,就每次都要到数据库中查询,而数据库中也查不到,这时就不能构建缓存来服务后续的请求。
-
怎么解决缓存穿透?
一般有两种方案:一是缓存无效的key,二是布隆过滤器- 第一种就是针对查询的数据,在缓存中设置一个空值或者默认值,这样后续请求就可以从缓存中读取到空值或者默认值,返回给应用,而不会继续查询数据库。
- 先使用布隆过滤器判断该数据是否存在
-
介绍一下布隆过滤器
- 布隆过滤器的作用就是检查一个元素是否存在在集合中
- 底层是一个一个很大的bit数组和一些哈希函数。
- 在添加一个元素时,计算它的不同哈希函数的哈希值,把这些哈希值的位置置为1。
- 在查询元素是否存在时,同样地计算其哈希值,并检查这些哈希值的位置,如果存在一个值不为1,就认为这个元素不存在于该集合;
- 布隆过滤器也会产生误判,就是有可能一个不存在的元素被判定为存在,因为可能是该元素的哈希值和其他多个元素的哈希值都发生了碰撞。我们可以设置误判率,一般不超过5%,数组的长度越大,误判率就越低
缓存击穿
-
什么是缓存击穿?
- 缓存击穿一般是当某一个热点key过期时,恰好有大量的请求访问这个key,当这些请求发现缓存中没有这个key时,就会到数据库中加载并重建缓存,而大量的并发请求访问数据库,可能会将数据库压垮。
- 注意【缓存穿透】是缓存和数据库中都不存在key,而【缓存击穿】是缓存中国不存在(过期了),而数据库中存在。
-
缓存击穿的解决方案?
- 互斥锁:强一致性,适合金钱交易等安全性高的场景,但性能比较差
- 当一个线程在缓存中查不到时,先获取互斥锁,再访问数据库并将过期数据加载到缓存,最后释放锁。目的就是保证只能有一个线程来访问数据库重建缓存。
- 逻辑过期:高可用性,但不能保证数据一致
- 对热点数据不设置过期时间,而是使用一个过期时间字段在逻辑上模拟过期的效果。当线程1查询时,从缓存中读取数据后需要先根据字段判断是否过期,无论是否过期都直接返回数据。但如果过期,就要先获取一个互斥锁,再新建一个线程2从数据库中重新读取数据并重置过期字段,最后释放锁。这时互斥锁的作用是保证只能有一个线程从数据库中重建缓存。
- 这样请求无需等待,但从缓存中读取到的数据有可能是过期的。
- 互斥锁:强一致性,适合金钱交易等安全性高的场景,但性能比较差
缓存雪崩
- 什么是缓存雪崩?
- 缓存雪崩一般是由于大量的缓存数据同时过期或者redis宕机,导致大量请求访问数据库,导致数据库压力骤增。
- 注意缓存雪崩是大量数据同时过期,而缓存击穿是某一个热点数据过期。
- 缓存雪崩的解决方案?
- 为了防止大量缓存数据同时过期,可以在设置过期时间时加上一个随机值
- 为了防止redis宕机引发雪崩,可以采用redis集群
Redis集群
- 怎么保证Redis的高可用性?
- 使用集群,当一个节点挂掉,还有别的节点来接替它的工作。Redis主要有三种集群方案:主从复制,哨兵模式,Cluster分片集群。
- 主从复制就是,使用一个主节点和若干从节点实现读写分离,即主节点负责写操作请求,从节点负责读操作,这样可以增加redis的并发能力,主从节点之间需要数据同步。这种方案比较简单,但并不经常使用,因为当主节点挂掉后需要手动进行主从切换。
- 哨兵模式相当于主从模式的升级版,可以自动进行主从切换。就是增加哨兵节点来监控Redis系统的状况,如果它发现主节点挂了,那么就会选取一个从节点作为主节点,并将新主节点的信息发送给其他节点。适用于读多写少的场景。
- 分片模式主要是为了解决海量数据存储和高并发写的问题,集群中有多个主节点,每个主节点保存不同的数据,可以给每个主节点设置多个从节点,同时每个主节点之间通Ping监控彼此的状态。只要客户端请求能够访问集群的任意节点,请求最终都会被转发到正确的节点。
主从模式
高可用,高并发
- 主从节点是怎么进行数据同步的?
主要分为三个阶段- 建立主从连接
- 从节点向主节点发送同步请求,携带版本信息(replication id和offeset),主节点通过replication id判断这是否为第一次同步,如果是的话,就将自己的版本信息发送给从节点,使其保持一致,并进行全量复制。
- 如果不是第一次同步
- 全量复制:RDB文件
- 主节点执行bgsave生成RDB文件发送给从节点,从节点先清空自己的旧数据,再加载RDB文件。当然生成RDB是比较耗时的,所以期间主节点将写命令写入repl backlog buffer缓冲区,待从节点完成全量复制后,从缓冲区读取命令。
- 增量复制/命令传播:基于TCP长连接
- 在完成全量复制后,主从节点会建立一个TCP长连接,主节点将之后执行的写命令发送给从节点执行。
- 建立主从连接
- 从节点断连了一段时间怎么办?
- 当从节点再次连接后,会进行增量复制,就是把断连这段时间主节点执行的写命令同步给从节点。
- 实际上,在主节点进行命令传播时,不仅把写命令发送给从节点,也写入了该从节点对应的replication buffer缓冲区,主从节点都有各自的偏移量,主节点偏移量表示写到的位置,从节点偏移量表示读到的位置。当从节点再次连接到主节点时,就会发送这个偏移量,主节点根据偏移量的差距同步这些增量命令给从节点。
- 主从复制是同步还是异步的?
- 主从复制是异步的,
哨兵模式
- 哨兵监控的原理?
- 哨兵基于心跳机制监测,简单来说就是哨兵节点每隔1s就向其他节点发送ping,如果不能ping通,就认为这个节点挂了。但也可能因为网络问题出现误判。
- 哨兵会出现误判吗?哨兵集群
- 为了减少误判,通常需要设置哨兵集群(至少3个哨兵节点),当一个哨兵节点认为一个节点挂掉后将其设置为【主观下线】,并在哨兵集群中发起投票,在超过一定量的哨兵节点都认为这个节点挂掉后才将其设置为【客观下线】。
- 并在作出【主观下线】判断的哨兵中选出一个领导者进行主从切换。
双写一致性
-
项目中mysql中的数据是如何与redis中的进行同步的?
- 因为查询商品这个业务对一致性要求并不严格,且并发量不很高,就采用了先更新数据库,后删缓存的策略。
- 具体实现就是在修改商品数据的方法上使用@CacheEvict注解,每次对商品详情进行修改时,先执行方法修改数据库,再删除缓存;
- 在查询商品商品的方法上使用@Cacheable,这样再次查询商品详情时,将方法的返回值(即更新后的数据,ResultVO对象)重新写入缓存。
- 这样的缺点就是有可能会读到旧的数据,就是如果一个线程1缓存未命中,去查询了数据库后,准备将数据库中的数据加载到缓存中时(还未存入缓存),另一个线程2更新了数据库并删除缓存,之后线程1加载的缓存还是它之前查到的旧数据。
- 为什么不用@CacheaPut注解在修改数据库时直接更新缓存?因为项目中修改数据库商品的方法的返回值是ModelAndView而不是商品数据,而@CachePut是更新缓存为方法的返回值
-
在高并发场景下,如何保证双写一致性?
- 延时双删:不能保证强一致性
- 读写锁:redission提供,一致性较高
-
介绍延时双删策略
- 就是在修改数据库时,先删除缓存,再修改数据库,延时一段时间,再次删除缓存
- 为什么要删除两次?因为如果没有第二次删除,那么在线程1删除了缓存,更新数据库之前,如果有另一个线程2查询数据库并写入缓存,那么缓存中仍然是旧数据。所以第二次删除就是为了删去数据库更新完成以前的缓存。
- 为什么第二次要延时?因为更新数据库需要一定的时间,如果第二次是立即删除,那么可能删除的还是旧数据的缓存。所以这也暴露了延时双删的另一个缺点,就是延时的时间不好确定。
- 为什么不是更新缓存,而是删除缓存?更新缓存操作比删除缓存更耗时,而且不是所有缓存都是被频繁访问的,所以适合先删除,等下一次查询时再写入缓存。.
Redis持久化
- RDB:Redis Database Backup 数据快照
- bgsave
- fork
- copy-on-write
- AOF:Append Only File
- 重写机制
RDB
- 介绍RDB机制?
- RDB 即Redis数据备份文件,也叫数据快照,是Redis4.0之前的默认持久化机制。就是把某一时刻的内存中的所有数据以二进制文件的形式记录到磁盘。
- 怎么触发RDB?
- save命令:由Redis主进程执行RDB,会阻塞其他命令
- bgsave命令:先fork出一个子进程,专门用于将快照写入磁盘,主进程继续处理客户端请求;
- 配置文件:在redis.conf文件中,配置自动执行bgsave的触发条件。
- bgsave命令是如何执行的?/RDB的原理?
- 通过fork()创建出一个子进程,子进程具有和主进程相同的页表,所以子进程和主进程共享一片物理内存,子进程就负责将这些数据写入RDB文件。
- 页表:记录虚拟地址和物理地址的映射关系,Linux中进程无法直接操作物理地址。
- 在子进程写入RDB文件的过程中,如果主进程想要修改数据,怎么执行?(fork的写时复制技术)
- fork是采用了Copy-on-Write的策略,简单来说就是写数据时是在原来数据页的副本上修改(独立内存空间)。
- 父进程中所有的内存页都是只读的,fork创建出的子进程开始和主进程共享一片内存空间,但当主进程想要修改某一片数据时,就会把想要修改的这片数据复制一份作为副本(独立物理空间),主进程在这个副本上进行写操作,而子进程继续将原来的数据写入RDB。
- fork期间主进程写入的数据还是存在了内存当中,只有等到下次进行RDB持久化时才会把 ” 写入的数据 ” 落盘到RDB文件中。
- Redis内存中的全量数据由一个个的"数据段页面"组成,每个数据段页面的大小为4K,客户端要修改的数据在哪个页面中,就会复制一份这个页面到内存中。
AOF
- 介绍AOF
- AOF是追加写命令的日志,即每执行一个写命令,就把该命令记录到日志中,在恢复数据时就重新执行这些写命令。
- AOF默认是关闭的,在redis.conf配置文件中开启,也可以设置AOF的刷盘时机,就是什么时候把AOF缓冲区中的命令写入AOF文件。
- 怎么避免AOF文件不断增大?(重写策略)
- 执行bgrewriteof命令,执行重写功能进行压缩,会fork出一个子进程记录每个key的最后一条写命令到一个临时文件,再替换原来的AOF文件。
- 也可以在redis.conf中配置AOF文件大小超过一定阈值后自动触发重写。
- AOF和RDB的区别有哪些?
- 从文件大小来说,RDB是压缩后的二进制文件,而AOF是记录命令的日志,所以RDB的体积更小。
- 从数据恢复速度来说,RDB只需直接从磁盘中读取原来的数据即可,而AOF需要重新执行这些命令,所以RDB更快。
- 但从数据完整性上考虑,RDB在两次备份之间的数据有可能会丢失,而AOF根据刷盘策略可以达到实时或秒级持久化,所以AOF的数据完整性更好。
混合持久化
- Redis4.0以后的默认持久化机制是什么?
- RDB-AOF混合持久化,就是在AOF持久化的基础上定期进行RDB持久化。
- 具体来说,就是在子进程进行AOF重写时,开始重写这一刻的数据快照写到AOF文件的开头,后面再将重写缓冲区的增量数据以命令的形式写入AOF临时文件,最后用临时文件替换原来的文件。
- 这样在Redis重启的时候,可以先加载RDB的内容,然后再重放增量AOF日志就可以完全替代之前的AOF全量文件重放,因此重启效率大幅得到提升
Redis过期数据删除策略
惰性删除+定期删除
- Redis的过期删除策略是惰性删除和定期删除结合使用
- 惰性删除就是当使用key时先去检查是否过期,如果过期了就删除;
- 定期删除就是定期检查key,如果删除掉过期的key。redis中有两种定期删除策略,slow和fast在配置文件中修改。
Redis数据淘汰策略
- Redis的内存满了怎么办?
- 如果是默认配置noeviction,那么内存满后就不允许写入新数据,会直接报错。
- 可以设置redis的数据淘汰策略,即淘汰掉一些数据。常用的有allkeys-LRU(最近最少使用)和allkeys-LFU(最少频率使用)。
- LRU算法是什么?
- Least Recently Used 最近最少使用,就是淘汰掉最久没有被使用的数据,仅与时间相关。就是保留最近使用的数据。
- LFU算法是什么?
- Least Frequently Used 最少频率使用,统计每个key的访问频率,淘汰频率低的数据。即保留访问频率高数据。
- 场景题:数据库有1000万数据 ,Redis只能缓存20w数据, 如何保证Redis中的数据都是热点数据 ?
- 可以使用LRU策略,就是淘汰掉不常使用的数据,那么保留的就是经常访问的热点数据
Redis分布式锁
- 为什么需要分布式锁?
- 在多线程环境下,如果多个线程同时访问共享资源,可能造成脏数据等问题。在单机环境下,我们可以通过给资源加本地锁实现资源互斥访问。但在分布式系统下,不同的服务运行在不同的进程中,进程内的本地锁不能保证不同的进程对资源的互斥访问。
- Redis怎么实现分布式锁?
- setnx命令:最简易的实现
//加锁:设置过期时间为10 SET lock_key random_value NX EX 10 //解锁:释放锁时要先判断 DEL lock_key
- 为什么要设置过期时间?不设置过期时间可能造成死锁,比如当一个服务获取锁后宕机,那么锁就永远不会被释放;加锁和设置过期时间需要是同一个语句,这样才能保证原子性。
- random_value 是一个随机生成的字符串,要保证在一段时间内都是唯一的。因为释放锁时,为了避免误删其他的锁,要先判断锁的value和提供的values是否相同。这通常用Lua脚本完成,这可以保证原子性。
// 释放锁时,先比较锁对应的 value 值是否相等,避免锁的误释放 if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end
- 怎么设置合理的锁过期时间?加锁后服务的业务执行时间 超过 锁的过期时间时,分布式锁就提前失效了。所以需要给锁续期,可以利用Redission框架实现
Redission
- Redission实现分布式锁的原理?
- Redisson 中的分布式锁自带自动续期机制,主要是利用看门狗的机制。所谓看门狗就是另开了一个线程每隔一段时间自动给锁续期,当服务完成释放锁后就会关闭看门狗。默认是30s,看门狗会每30/3=10s检测一次业务是否执行完成。
- 加锁,设置过期时间都是通过Lua脚本实现的,这可以保证原子性。
- 注意redission获取锁时如果设置了锁的过期时间那么就不会开启看门狗机制
- Reddison分布式锁可重入吗?
- 可重入锁就是在一个线程获取锁之后,再次获取这个锁。
- Redission分布式锁是可重入的,它会在获取锁时通过唯一的线程id来判断是否是当前自己占用的锁,具体通过HashMap其key是线程id, value是重入次数,当重入次数为0时,才删除该锁。
Redis为什么快
- 直接操作内存;
- 单线程,避免多线程切换,且避免了线程安全问题;
- IO多路复用,非阻塞IO。
- Redis是单线程的吗?
- Redis的单线程指的只有单个主线程处理请求。
- 实际上,Redis还为关闭文件,AOF刷盘,释放内存任务创建了单独的后台进来处理。因为这些任务比较耗时,放在主线程上会造成阻塞。
- 什么是 IO多路复用?
- 因为Redis是单线程的,所以它的性能瓶颈并不是CPU执行时间而是网络延迟。
- 在传统的IO阻塞模型中,每个IO操作都会阻塞当前线程,直到IO操作完成,这非常耗时。
- 为了解决这个问题,Redis采用了IO多路复用技术,就是用单线程处理多个IO流,单线程同时监听多个Socket,一旦有请求到达,就交给redis主线程处理,避免了无效的等待。