1、redis缓存穿透,缓存击穿以及缓存雪崩问题和对应的解决方案
缓存穿透:当客户端访问一个不存在的数据,这个数据在缓存和数据库中都不能命中,如果有大量的这种穿过缓存直接访问数据库的请求,就会给数据库带来很大压力。
解决思路:
- 缓存空对象:当发现数据库中也没有该数据时,我们把这个数据存在redis中,但值设置为空,这样下次访问时就会直接从redis中获取空对象。实现简单,维护方便,但内存消耗更大
- 布隆过滤器:布隆过滤器实际上是一串很长的二进制数组,它通过哈希思想去判断key是否存在,如果不存在则拒绝请求,存在则放行右redis和数据库处理。内存占用少,但实现复杂,有误判可能(存在也可能被判为不存在)
实例解决方案(缓存空对象)
1、判断缓存中是否有数据,如果有直接返回,没有则去数据库查数据
2、判断从数据库查出的数据是否为空,如果为空,则将空对象存在redis中
缓存击穿:当一个被高并发访问并且重建业务比较复杂的热点key失效时,会有大量的请求直接访问到数据库,给数据库带来巨大冲击。
解决思路:
- 分布式互斥锁:当一个线程获得锁然后执行重建缓存的逻辑时,其他线程只能等待缓存重建完成后再去获取缓存(使用redis的setnx方法来表示获取锁 ,set if not exist)。有死锁和线程池阻塞的风险,降低吞吐量,但能有效保持数据一致
- 设置永不过期:不直接设置key的过期时间,而是将过期时间放在value中,当有线程去查询key并发现已经到过期时间后,就异步地去更新缓存。解决了热点key的一系列危机,但在缓存更新时不能保证数据一致性
实例解决方案(分布式互斥锁)
篇幅限制下面就只能给大家展示小册部分内容了。这份面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题
需要全套面试笔记【点击此处】即可免费获取
缓存雪崩:当缓存中大量的key同时失效或redis服务崩溃时,会有大量请求直接访问到数据库,带来巨大压力
解决思路:
- 给不同的key的TTL增加随机值
- 设置key永不过期,加分布式锁
- 利用redis集群提高服务的可用性(比如使用redis哨兵)
- 给缓存业务添加降级限流策略
- 添加多级缓存
实例解决方案(设置key永不过期)
与分布式互斥锁相比,其实就是在缓存命中之后加一层缓存是否过期的校验,过期后就执行获取锁更新缓存的操作,没过期就直接返回
值得注意的点:
- 该方案中可以封装一个实体类,里面存有逻辑缓存过期时间和真实value,redis中的value就引用该对象实例
- 判断缓存是否过期通过拿逻辑缓存过期时间与LocalDateTime做对比
- 更新缓存时,获得锁后,应该开启独立线程去更新缓存,然后直接返回旧的缓存数据,目的是减少响应时间,因为设置key永不过期本身就不能保证数据完全同步
- 开启独立线程使用的是Executor,一套线程池管理框架,可以处理多线程操作,具体实现如下
private static final ExecutorService CACHE_REBUILD_EXECUTOR =
Executors.newFixedThreadPool(10);
if (isLock){
CACHE_REBUILD_EXECUTOR.submit( ()->{
try{
//重建缓存
this.saveShop2Redis(id,20L);
}catch (Exception e){
throw new RuntimeException(e);
}finally {
unlock(lockKey);
}
});
}
2、redis的五种数据类型
篇幅限制下面就只能给大家展示小册部分内容了。这份面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题
需要全套面试笔记【点击此处】即可免费获取
3.1、redis键值设计
①优雅的key结构 业务名称:数据名称:id 例:login:user:1
优点:可读性强,避免key冲突,方便管理,节省内存空间
3.2、bigkey和bigkey的危害
bigkey以key的成员数量和key的大小来综合评估,比如key本身数据量过大,key的成员数量过多
bigkey的危害有:
- 网络阻塞:读取bigkey时,少量的QPS就可能宽带使用率拉满,造成网络阻塞,导致物理机和redis执行速度变慢
- 数据倾斜:bigkey的内存使用率大,导致数据分片的内存分配不均衡
- redis阻塞:对bigkey的数据进行一些运算操作时,耗时会比较久,就有可能导致redis进程阻塞
- CPU压力:对bigkey进行序列化和反序列化将导致CPU的使用率飙升,给予CPU巨大压力
3.3 总结
对于key:
- 固定格式:业务名称:数据名称:id
- 足够简短:尽量不超过44字节,因为key是string类型,底层编码包含int、embstr和raw三种。embstr在小于44字节时使用,采用连续内存空间,内存占用更小
- 不要包含特殊字符
对于value:
- 合理地 拆分数据,拒绝bigkey
- 选择合适的数据结构
- hash的entry数量尽量不要大于500,因为hash的entry数量大于500时,会使用哈希表结构而不是ziplist,增大内存占用
- 设置合理地过期时间
3.4 redis的更新策略
更新缓存还是删除缓存?
一般选择删除缓存,有以下好处:
没有无效更新操作,线程安全问题小
先操作数据库还是先操作缓存
一般选择先操作数据库,理由如下:
在两个线程并发来访问时,假设线程1先来,他先把缓存删了,此时线程2过来,他查询缓存数据并不存在,此时他写入缓存,当他写入缓存后,线程1再执行更新动作时,实际上写入的就是旧的数据,新的数据被旧数据覆盖了。
而先操作数据库就不会有这种问题,安全系数更高
4、redis为什么快?
1、基于内存实现
Redis是基于内存存储的,读取数据时不需要进行磁盘IO,要比数据存储在磁盘的数据库快
2、高效的数据结构
redis中采用了许多高效的数据结构,比如
哈希表:可以保证查找操作空间复杂度是O(1)
跳表:保证查找/添加/插入/删除操作都能够在O(LogN)的复杂度内完成。
3、单线程操作,避免了不必要的线程上下文切换和竞争锁消耗,而且也不用去考虑线程安全问题,提高性能
4、高性能的多路复用IO模型
redis网络模型使用IO多路复用结合事件处理器
IO多路复用让redis在单线程的情况下也能处理多个连接请求,并且由于IO多路复用是非阻塞的,让redis能够高效网络通信
事件处理器通过事件监听和通知机制,避免了redis一直轮询关注事件进度,避免CPU浪费
并且在redis6.0中,为了提升性能,在命令回复和命令转换中使用了多线程
5、虚拟内存机制
Redis直接自己构建了VM机制 ,虚拟内存机制就是暂时把不经常访问的数据(冷数据)从内存交换到磁盘中,从而腾出宝贵的内存空间用于其它需要访问的数据(热数据)。
RedisIO多路复用模型:
IO多路复用模型通过调用select/poll/epoll等函数,开启一个监听线程(selector)去监听多个socket客户端,此时监听线程会阻塞,其他非监听线程仍可以继续工作,一旦某个socket客户端有IO操作就绪,selector会解除阻塞并通知redis,Redis就会建立连接,接收数据,然后selector继续阻塞等待客户端就绪。
一条命令在Redis是如何完成执行的?
Redis Server一旦和某一客户端建立连接后,就会在事件驱动框架中注册可读事件,对应客户端的命令请求。整个命令处理的过程可分为如下阶段:
-
命令解析,对应processInputBufferAndReplicate
-
命令执行,对应processCommand
-
结果返回,对应addReply
5、Redis为什么是单线程的?
首先要知道,Redis的性能瓶颈在内存和网络IO,而不是CPU
使用多线程对Redis的性能提升并不大,而多线程本身也有线程安全和上下文切换问题,所以Redis开发者选择使用单线程。
但是在Redis6.0之后,在发送响应数据中引用了多线程操作,这主要是为了提升网络IO性能
6、Redis双写一致性如何保证
Redis跟数据库双写方案需要根据业务对数据一致性的需求来决定
1、需要强一致性
采用Redission的读写锁,读数据采用读锁,写业务采用写锁
比如在抢券业务中,券的剩余量是需要强一致性的
2、允许延迟一致
采用异步方案同步,即写操作先写入数据库,然后异步地去修改缓存
异步方案:MQ(基于消息队列),Canal(基于mysql binlog),
7、Redis持久化方式
Redis进行持久化时,在主进程中会fork一个子进程,子进程会共享主进程的内存,并且通过内存中的页表对物理内存进行读取,然后将数据写入磁盘文件中
Redis持久化有两种方式
1、RDB(Redis DataBase Backup file)
RDB是Redis默认持久化方式,它按照配置好的时间周期策略将内存数据以快照的方式存入磁盘,生成rbg后缀文件,可以通过修改配置文件中的save参数来配置策略
2、AOF(Append only file)
AOF会将每一个写命令通过wirte函数追加到aof文件最后,类似于Mysql的binlog,当redis重启后,会执行文件中保存的写命令恢复数据
当两种方式都开启时,默认执行AOF
8、Redis过期删除策略
Redis采用惰性删除+定期删除策略
惰性删除:当访问key时判断是否过期,如果过期,就将key删除
定期删除:定期检查一定量key是否过期,如果过期就删除
惰性删除的问题是,如果key过期了,但却一直没有被访问,就无法删除,造成内存资源浪费
定期删除的问题是,会有很多key过期了但无法删除,不仅占用内存,访问时还会访问到过期数据
为什么不用到期自动删除?
因为这样就需要一个定时器去监视每个key是否过期,十分浪费CPU资源
9、Redis数据淘汰策略
Redis数据淘汰一共有8种策略
比较关键的是后面四种:
如果业务中数据访问频率差距大,有冷热数据之分,建议使用lru
如果要保留置顶数据,建议使用volatile,然后将置顶数据不设置过期时间
如果业务中有很多短时间内高频访问的数据,建议使用lfu,保留这些高频数据
场景题、数据库有1000w数据,但redis只能保存20w,如何让Redis存的数据更有价值?
首先建议采用lru算法,它能有效保存经常被访问的热点数据,然后如果有需要固定一些数据,则可以使用volatile-lru,然后不给固定数据设置TTL
10、在项目中Redis的使用场景
秒杀抢购逻辑:
在项目中,我使用了Redis保存了优惠券库存信息(string)和优惠券订单信息(set)
-
用户抢购时,首先要从Redis中获取优惠券信息,是否库存大于0,是才能继续,否则直接返回
-
接下来从Redis中获取优惠券订单信息,判断set集合中是否有用户id,如果是说明用户已经有订单了,就直接返回。否则将库存-1,然后在订单集合中增加该用户id。
以上均在lua脚本中完成
优惠券基本信息的缓存在添加优惠券时写入,TTL设置为优惠券过期时间减去当前时间。
判断有下单资格后,通过kafka消息队列异步生成订单,然后直接返回订单Id。
在项目中,我还使用了Redission分布式锁在生成订单操作前上锁,锁的key为order:user:{userId}
这里加锁主要是一个兜底,防止一人多单,实际上lua脚本中都判断过了
篇幅限制下面就只能给大家展示小册部分内容了。这份面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题
需要全套面试笔记【点击此处】即可免费获取
11、Redission分布式锁原理
Redission是Redis官方推荐的客户端,继承了juc中的lock接口,实现了可中断,可重入,超时释放等操作
底层结构:Redis中的hash结构,key存储锁名,field存储客户端id(uuid+ThreadId),value用于存储线程重入次数
加锁实现:
Redission加锁采用的是lua脚本来保证原子性,它首先会判断该key是否存在,如果不存在就创建一个,并设置线程重入次数为1,然后返回null
如果key存在就判断客户端id是否存在,如果存在就将重入次数+1,然后返回null
如果以上都不满足,就返回TTL
每次在加锁时都会判断,如果返回的TTL为null,说明加锁成功,否则失败,然后循环尝试获取锁
lease续约实现:
Redission中为了业务的安全性和可靠性引入watchdog机制,这个机制就是说在任务获取锁时,会同步开启一个守护线程,它会每隔leaseTime/3时间续约锁的超时时间,默认续约30s,可以配置,守护线程会一直续约直到锁释放,这样就保证了执行业务期间不会因超时释放锁
解锁实现:
首先判断锁是否是存在的,如果不存在直接返回nil; 如果该线程持有锁,则对当前的重入值-1,如果计算完后大于0,重新设置超时持有时间返回0; 如果算完不大于0,删除这个Hash,并且进行广播,通知watch dog停止进行刷新,并且 返回1.
RedissionRedLock红锁实现:
Redission锁没有解决主从节点切换导致的多个线程持有锁的问题,所以提供了,RedissionRedLock来实现,它要求对多个Redis实例都进行加锁,只有当超过一半的锁加锁成功时,才认为成功加锁。
12、Redis主从同步原理
单节点的Redis并发能力是有上限的,往往需要搭建Redis主从集群来提升性能,一般是一主多从,读写分离,主负责写,从负责读
主从同步的流程:
1、从节点发送同步请求
2、主节点判断是否是第一次请求,如果是,则进行全量同步,否则进行增量同步
全量同步:
1、首先与从节点同步版本信息(replication id和offset)
2、然后主节点执行一次bgsave,生成rgb文件,然后将rgb文件传给从节点去执行
3、在bgsave期间,主节点会将这期间的命令写入到一个日志文件中(repl_back.log)
4、在发送rgb文件之后,主节点会将日志文件发送给从节点同步
增量同步:
1、主节点获取从节点的offset信息
2、从命令日志文件中读取offset之后的数据,发送给从节点同步
13、Redis高可用,哨兵模式,脑裂问题
哨兵模式:哨兵模式可以实现主从集群的故障恢复
它主要实现了三个功能:
1、监控:每隔一秒钟向集群实例发送ping命令,检查集群实例是否在工作,如果有超过一定数量(可配置)的sentinel都认为某实例已经下线,则该实例被认为客观下线。
2、自动故障修复:当master节点被认为客观下线后,哨兵会将一个从节点提升为主节点,故障实例恢复后也以新的主节点为主
3、通知:Sentinel用来充当Redis的服务发现源,当Redis集群发生故障转移后,Sentinel会及时的将新的Redis节点信息推送到客户端。
脑裂问题:
发生脑裂问题一般是这种情况:
1、主节点因某些原因出现暂时性失联,但仍在正常工作,Sentinel此时联系不到主节点,于是将一个从节点升级为主节点。
2、然而Redis客户端会在Sentinel通知还未到时,继续向旧主节点写入数据
3、当旧主节点恢复与哨兵的联系时,会清除自己的数据与新主节点同步,造成了一段时间的数据丢失。
脑裂解决办法:
1、设置Redis中的min-slave-to-write参数:这个参数确保了主节点要至少有n个从节点才能执行写命令
2、设置Redis中的min-slave-max-lag参数:这个参数确保在主从复制时,如果ACK没在规定时间内,就拒绝写命令
这样一来,即使主节点失联了,也会因为联系不上从节点而拒绝写命令,防止数据丢失。
14、Redis分片集群的作用和原理
说到Redis分片集群,我们可以从特点说起:
Redis分片集群有以下特点:
-
有多个master节点,每个master节点中存储不同的数据——支持高并发写
-
每个master节点都有多个slave节点——支持高并发读
-
每个master节点会互相ping监测彼此健康状态——主节点监听
-
每个请求最终都会被转发到正确的节点中——请求路由转发
篇幅限制下面就只能给大家展示小册部分内容了。这份面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题
需要全套面试笔记【点击此处】即可免费获取
Redis分片集群存储和读取原理
-
Redis分片集群引入了哈希槽的概念,哈希槽可以看做若干哈希值的集合,Redis中共有16384个哈希槽。
-
Redis将哈希槽分配在各个master实例上
-
当读写请求到达集群时,会使用CRC16算法计算key的hash值,然后对16384取模来决定访问哪个节点