Redis面试篇笔记:
缓存:
1. 缓存三兄弟
1.1 缓存穿透:
缓存穿透: 指查询一个不存在的数据,mysql查询不到数据也不会直接写入缓存,就会导致每次请求都查数据库
方案一:
缓存空数据:
- 实现:查询结果为
null
的数据也存储到缓存中,并设置一个较短的过期时间。 - 优点:简单易实现。
- 缺点: 消耗内存,可能发生短暂的数据不一致问题,比如存储缓存前没有这个数据,将其为
null
存储到缓存中,数据被添加,但缓存未更新。然后导致下次请求,命中缓存,访问的还是以前的null 数据,导致数据不一致效果:
方案二: 布隆过滤器
什么是布隆过滤器:是一种空间效率极高的概率型数据结构,用于判断一个元素是否存在于一个集合中。它的核心特点是:
-
空间效率高:使用少量的内存即可存储大量数据。
-
概率型判断:判断结果可能存在一定的误判率(False Positive),但不会漏判(False Negative)。
用 Redisson 实现布隆过滤器使
1. 添加 Redisson 依赖
首先,在你的项目中添加 Redisson 的依赖。如果你使用 Maven,可以在 pom.xml
中添加以下依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.20.0</version> <!-- 请使用最新版本 -->
</dependency>
2. 配置 Redisson 客户端
在使用 Redisson 之前,需要配置并初始化 Redisson 客户端。以下是一个简单的配置示例:
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
public class RedissonConfig {
public static RedissonClient createClient() {
// 创建配置
Config config = new Config();
config.useSingleServer()
.setAddress("redis://127.0.0.1:6379"); // Redis 服务器地址
// 创建 Redisson 客户端
return Redisson.create(config);
}
}
3. 使用 Redisson 的布隆过滤器
Redisson 提供了 RBloomFilter
接口来实现布隆过滤器。以下是完整的代码示例:
import org.redisson.api.RBloomFilter;
import org.redisson.api.RedissonClient;
public class BloomFilterExample {
public static void main(String[] args) {
// 创建 Redisson 客户端
RedissonClient redissonClient = RedissonConfig.createClient();
// 获取或创建布隆过滤器
RBloomFilter<String> bloomFilter = redissonClient.getBloomFilter("myBloomFilter");
// 初始化布隆过滤器
// 参数1:预期插入的元素数量
// 参数2:误判率(0 < 误判率 < 1)
bloomFilter.tryInit(10000L, 0.01);
// 添加元素
bloomFilter.add("apple");
bloomFilter.add("banana");
bloomFilter.add("cherry");
// 检查元素是否存在
System.out.println("Contains 'apple': " + bloomFilter.contains("apple")); // true
System.out.println("Contains 'banana': " + bloomFilter.contains("banana")); // true
System.out.println("Contains 'cherry': " + bloomFilter.contains("cherry")); // true
System.out.println("Contains 'orange': " + bloomFilter.contains("orange")); // false(可能误判为 true)
// 关闭 Redisson 客户端
redissonClient.shutdown();
}
}
代码说明
-
初始化布隆过滤器:
-
使用
tryInit
方法初始化布隆过滤器。 -
需要指定预期插入的元素数量和误判率。
-
例如:
bloomFilter.tryInit(10000L, 0.01)
表示预期插入 10,000 个元素,误判率为 1%。
-
-
添加元素:
-
使用
add
方法将元素添加到布隆过滤器中。
-
-
检查元素是否存在:
-
使用
contains
方法检查元素是否可能存在于布隆过滤器中。 -
如果返回
true
,表示元素可能存在(可能有误判)。 -
如果返回
false
,表示元素一定不存在。
-
-
关闭客户端:
-
使用
redissonClient.shutdown()
关闭 Redisson 客户端,释放资源。
-
4. 布隆过滤器的特点
-
优点:
-
空间效率高,适合存储大量数据。
-
查询速度快,时间复杂度为 O(k)O(k),其中 kk 是哈希函数的数量。
-
-
缺点:
-
存在误判率,可能将不存在的元素误判为存在。
-
不支持删除操作(Redisson 的布隆过滤器不支持删除)。
-
5. 误判率与参数选择
-
误判率:
-
误判率越低,布隆过滤器占用的空间越大。
-
误判率可以通过
tryInit
方法的第二个参数设置。
-
-
预期插入数量:
-
预期插入数量越大,布隆过滤器占用的空间越大。
-
如果实际插入数量超过预期值,误判率会显著增加。
-
6. 使用场景
-
缓存穿透保护:
-
在缓存系统中,使用布隆过滤器过滤掉不存在的请求,避免直接访问数据库。
-
-
去重:
-
在大规模数据去重场景中,使用布隆过滤器快速判断元素是否已存在。
-
-
爬虫 URL 去重:
-
在爬虫系统中,使用布隆过滤器判断 URL 是否已爬取。
-
7. 注意事项
-
误判率:
-
布隆过滤器存在误判率,不适合需要 100% 准确性的场景。
-
-
不支持删除:
-
标准的布隆过滤器不支持删除操作。如果需要删除功能,可以考虑使用变种(如 Counting Bloom Filter)。
-
-
Redis 内存占用:
-
布隆过滤器会占用 Redis 的内存,需要根据实际需求合理设置参数。
-
1.2 缓存击穿:
方案一:
互斥锁:
- 实现:当未命中缓存的时候,获取互斥锁,然后去重够缓存,其他线程发现锁已经拿到了,就休眠一会,等待锁释放后直接读取缓存。
- 优点: 强一致,
- 缺点:性能差。
方案二:
逻辑过期:
- 实现:缓存中存储逻辑过期时间,当发现缓存过期时,获取互斥锁并异步更新缓存,当前线程返回旧数据。
- 优点:性能较好。
- 缺点:数据一致性较弱。
1.3 缓存雪崩
缓存雪崩:是指同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到底数据库,带来巨大压力。
解决方案:
- 给不同的key的TTL添加随机值:
- 利用Redis的集群提高服务的可用性 ->(集群模式,哨兵模式)
- 给缓存业务添加降级限流策略 (nginx 或 gateway)
- 给业务添加级缓存 (Guava 或 Caffeine)
口诀:
《缓存三兄弟》
穿透无中生有key,布隆过滤null隔离。
缓存击穿过期key,锁与非期解难题。
雪崩大量过期key,过期时间要随机。
面试必考三兄弟,可用限流来保底。
2. 双写一致性:
1. 先删除缓存,在修改数据
2. 先修改数据,在删除缓存
-
先删除缓存,再更新数据库:
-
问题:删除缓存后,更新数据库前,可能有线程读取旧数据并写入缓存。
-
解决方案:延时删除缓存,减少脏数据风险。
-
-
先更新数据库,再删除缓存:
-
问题:更新数据库前,刚好缓存前失效,并且有一个线程来读取数据,此时拿到原来数据,然后在执行当前线程的更新操作,在删除缓存后,在执行另一个线程的写入缓存,导致数据与缓存不一致(这个发生的概率比较低,一般而言业务上都会使用这个方案。)
-
解决方案:延时删除缓存。
-
2. 为什么要删除2次?
第二次删除:第一次删除缓存后,还没更新数据库前,可能有线程重新访问数据库写入旧数据到缓存,因此需要删除可能的旧数据缓存
3. 为什么要延时删除?
极大的控制了脏数据的风险,也只控制一部分,做不到绝对强一致,时间有范围
双写一致性的强一致性:
强一致:缓存一般都是不轻易更改的,读多写少,可以加共享锁(其他线程都是可以读的操作),和排他锁来(只有当前线程可以读写,阻止其他线程对其读写操作)
延迟一致:
通过中间件,MQ来异步删除缓存
也可有通过Cannal中间件。来监听mysql中的binlog日志来更新缓存
也可以通过canal之类的监听MySQL的binlog日志去同步数据到Redis
3. 持久化
redis 作为缓存,数据的持久化怎么做的?
redis中提供了2中方式:
RDB (Redis Database) 快照:
- ·RDB是通过生成某一时刻的数据快照来实现持久化的,可以在特定时间间隔内保存数据的快照。
- ·适合灾难恢复和备份,能生成紧凑的二进制文件,但可能会在崩溃时丢失最后一次快照之后的数据。
AOF (Append Only File) 日志:
- ·AOF通过将每个写操作追加到日志文件中实现持久化,支持将所有写操作记录下来以便恢复。
- ·数据恢复更为精确,但文件体积较大,重写时可能会消耗更多资源。
- Redis4.0新增了RDB和AOF的混合持久化机制。
1. RDB
RDB执行原理:
注:写时复制:
- 在子进程创建时,它不会直接将主进程地址空间全部复制,而是共享同一个内存。
- 之后如果任意一个进程需要对内存进行修改操作,内存会重新复制一份提供给修改的进程单独使用。
那这两份数据原数据跟备份数据最后是怎么处理的呢?不能同时使用两份吧,也不能每次拷贝出来就不管了吧
这里解释一下,子进程执行完后会被回收,数据副本B会被后来新的子进程进行RDB操作,循环往复
2. AOF
AOF持久化方式,redis会将每一个收到的写命令都通过write函数追加到文件中(默认是app:endonly.aof)
AOF提供了三种写回策略,决定何时将数据同步到磁盘中:
·always:每次写操作后立即调用fsync,将数据同步到磁盘。这种策略保证了最高的数据安全性,但也
会显著降低性能,因为每个写操作都要等待磁盘写入完成。
·everysec:每秒调用一次fsync,将数据同步到磁盘。这种策略在性能和数据安全性之间做了折中,默
认情况下,Redis使用这种策略。最多会丢失1秒的数据。
·no:由操作系统决定何时将数据写入磁盘。通常,操作系统会在一定时间后或缓冲区满时同步数据到
磁盘。这种策略具有最高的性能,但数据安全性较低,因为在Redis崩溃时可能会丢失较多的数据。
问:设置always能一定保证数据不丢失吗?
答案是不能!因为Redis是先执行命令再写入aof,所以如果执行命令写入aof这段时间Redis宕机了,
重启后也无法利用aof恢复!
所以Redis的持久化机制,并不能保证数据不丢失!
缺点:
- AOF 文件RDB文件更占用更多的磁盘空间,对同一个key操作多次,只有最后一次是有效的,重复性记录之前记录,可以执行重写文件,优化!
- AOF机制对于数据恢复的时间比RDB机制更加耗时,因为要重新执行AOF文件中的所有操作命令。
优点:
·AOF机制比RDB机制更加可靠,因为AOF文件记录了Redis执行的所有写命令,可以在每次写操作命令执行完毕后都落盘存储。
二者结合:
如果同时有RDB和AOF,那么进行数据恢复的时候,只会按照AOF的方式进行数据恢复
redis4.0混合机制,用rdb来转换数据流,再用aof来记录
7. 示例流程
数据保存
-
Redis 运行期间,所有写操作记录到 AOF 文件。
-
当 AOF 文件达到重写条件时,触发 AOF 重写。
-
重写过程中,生成一个混合文件:
-
前半部分:当前内存数据的 RDB 快照。
-
后半部分:重写期间的增量 AOF 数据。
-
-
重写完成后,替换旧的 AOF 文件。
数据恢复
-
Redis 启动时,加载混合 AOF 文件。
-
先加载 RDB 部分,快速恢复数据。
-
再加载 AOF 部分,重放增量写操作。
-
数据恢复完成,Redis 进入就绪状态。
4. 数据过期
假如redis 的key 过期后会立即删除吗?
Redis对数据设置数据的有效时间,数据过期以后,就需要将数据从内存中删除掉。可以按照不同的规则进行
删除,这种删除规则就被称之为数据的删除策略(数据过期策略)。
惰性删除
比如:然后好多过期食品都没管。家里堆满了垃圾
定期删除
总:
Redis的过期删除策略:惰性删除+定期删除两种策略进行配合使用
5. 数据淘汰策略
当redos中内存不够的时候,再向redis中添加数据的时候,那么此时会按照一点规则来删除数据,与数据过期不同,数据过期是设置了有一个过期的数时间!!1
数据淘汰策略-使用建议:
1. 优先使用allkeys-Iru策略。充分利用LRU算法的优势,把最近最常访问的数据留在缓存中。如果业务有明显的冷热数据区分,建议使用。
2. 如果业务中数据访问频率差别不大,没有明显冷热数据区分,建议使用allkeys-random,随机选择淘汰。
3. 如果业务中有置顶的需求,可以使用volatile-Iru策略,同时置顶数据不设置过期时间,这些数据就一直不被删除,会淘汰其他设置过期时间的数据。
4. 如果业务中有短时高频访问的数据,可以使用allkeys-lfu或volatile-lfu策略。
若:数据库有1000万数据,,Redis只能缓存20w数据,如何保证Redis中的数据都是热点数据?
使用allkeys-lru(挑选最近最少使用的数据淘汰)淘汰策略,留下来的都是经常访问的热点数据
若:Redis的内存用完了会发生什么?
主要看数据淘汰策略是什么?如果是默认的配置(noeviction),会直接报错
分布式锁:
setnx:
redisson:
数据库:乐观锁;Redis:Set NX 和 Redisson;Zookeeper
集群下抢单优惠卷,本地锁不能解决超卖,分布式锁来解决:
要是是普通单体服务完全可以使用本地锁,但是集群情况是微服务,所以只能用分布式锁来代替本地锁咯
rdis分布式锁:
常见方法是通过setexnx命令+lua脚本组合使用。确保多个客户端不会获得同一个资源锁的同时,也保证了安全解锁和意外情况下锁的自动释放。
1) 加加锁:SET lock_key uniqueValue EX expire_time NX
2)解锁:使用lua脚本,先通过get获取key的value判断锁是否是自己加的,如果是则del。
为什么解锁需要使用lua脚本呢?
因为我们知道,锁是有过期时间的,如果你没有过期时间,如果某个客服端加了锁宕机后,没有设置过期时间,会使得其他客服端都无法抢到锁。
-
场景:
-
客户端 A 获取锁,锁的值为
uuid_A
。 -
客户端 A 在执行完业务逻辑后,准备释放锁。
-
在客户端 A 检查锁的值(
uuid_A
)之后,锁过期了。 -
客户端 B 获取了锁,锁的值变为
uuid_B
。 -
客户端 A 继续执行删除锁的操作,误删了客户端 B 的锁。
-
-
结果:客户端 B 的锁被错误释放,导致锁的安全性被破坏。
' 释放锁的两步操作(判断锁的值和删除锁)需要保证原子性,否则会出现竞态条件。
注:lua脚本本身不具备原子性的,因为redis 是单线程的,Lua 脚本在执行时不会被其他命令打断。
redisson:
Redisson 在 Redis 分布式锁的基础上做了很多优化和封装,主要包括以下几点:
封装 SETNX
和 Lua 脚本
Redisson 使用
SETNX
和 Lua 脚本来实现加锁和解锁的逻辑。加锁时,Redisson 会生成一个唯一的锁标识(UUID + 线程 ID),并将其作为锁的值。
解锁时,Redisson 会通过 Lua 脚本检查锁的值是否匹配,确保只有加锁的客户端才能解锁。
看门狗机制(Watchdog)
问题:如果锁的过期时间设置得太短,可能会导致任务未完成锁就自动释放;如果设置得太长,可能会导致锁无法及时释放。
解决方案:Redisson 引入了看门狗机制,自动为锁续期。
加锁时,Redisson 会启动一个后台线程(看门狗),定期检查锁是否仍然持有。
如果锁仍然持有,Redisson 会自动延长锁的过期时间(默认 30 秒)。
如果客户端崩溃或失去连接,看门狗线程会停止,锁最终会自动过期。
可重入锁
Redisson 的分布式锁支持可重入,即同一个线程可以多次获取同一把锁。
内部通过计数器记录锁的重入次数。
同个线程内可以重复获得同个锁,称为锁重入 :每一个线程都有唯一的线程Id标识:根据线程ID来识别的
RedLock(红锁):
RedLock 的背景
在分布式系统中,使用单个 Redis 实例实现分布式锁存在以下问题:
-
单点故障:如果 Redis 实例宕机,锁将无法正常工作。
-
网络分区:如果客户端与 Redis 实例之间的网络出现问题,可能导致锁的状态不一致。
为了解决这些问题,Redis 的作者 Salvatore Sanfilippo(Antirez)提出了 RedLock 算法。
RedLock 的核心思想是使用多个独立的 Redis 实例(通常是 5 个)来共同管理锁。客户端需要在这些实例中的大多数(N/2 + 1)上加锁成功,才能认为锁获取成功。
它主要解决的问题就是当部分节点发生故障也不会影响锁的使用和数据问题的产生。
redis分布式锁是无法解决主从不一致的问题的,只能说出现了再想办法
总结:
redis 提供集群方案有多种:
主从集群 解决 高并发 !!!
主从复制 :
主从数据同步原理:
分 全量同步:
从节点 向主节点复制,吧自己的repid 发送到主节点对比,不一致,就是第一次,然后更新从节点id为主节点,下次就一致,然后生成二进制rdb文件,期间有新数据,会记录到repl_Biglog日志中去,并且通过偏移量来记录记录然后更新数据,下次就通过偏移量了
增量同步:
总:
哨兵模式 :
保证redis的高可用:作用包括:监控,自动故障恢复,通知
监控原理:
redis 集群(哨兵模式)脑裂问题:
原理:配置俩个参数后,如果发生脑裂,原主节点就会满足不了这俩个参数,原主节点拒绝旧客户端写请求,等待被降级,减少数据的丢失 !!
总:
问:怎么保证Redis的高并发高可用?
答: 哨兵模式:实现主从集群的自动故障恢复(监控、自动故障恢复、通知)
问:你们使用redis是单点还是集群,哪种集群?
答: 主从(1主1从)+哨兵就可以了。单节点不超过10G内存,如果Redis内存不足则可以给不同服务分配独立的Redis主从节点
问:redis集群脑裂,该怎么解决呢?
答: 集群脑裂是由于主节点和从节点和sentinel处于不同的网络分区,使得sentinel没有能够心跳感知到主节点,所以通过选举的方式提升了一个从节点为主,这样就存在了两个master,就像大脑分裂了一样,这样会导致客户端还在老的主节点那里写入数据,新节点无法同步数据,当网络恢复后,sentinel会将老的主节点降为从节点,这时再从新master同步数据,就会导致数据丢失
解决:我们可以修改redis的配置,可以设置最少的从节点数量以及缩短主从数据同步的延迟时间,达不到要求就拒绝请求,就可以避免大量的数据丢失
图片:
分片集群 :
怎么随机访问节点后,被正确转发到正确的节点的?
就是每一个主节点只会储存一部分数据,有一个插槽的东西,如果访问的数据不在这个节点,就会自动路由到正确的节点上去获取数据
hash冲突应该没关系,大不了都存在一个redis节点,算hash单纯是为了找redis节点罢了
1.redis主从数据同步的流程是什么?
2.怎么保证redis的高并发高可用?
3.你们使用redis是单点还是集群,哪种集群?
4.Redis分片集群中数据是怎么存储和读取的?
5.Redis集群脑裂,该怎么解决呢?
-
冲突的键会被分配到同一个哈希槽,而同一个哈希槽只会由一个节点负责。
-
因此,冲突的键会被存储在同一节点上,不会影响集群的正常运行
Redis是单线程的,但是为什么还那么快
常识:
用户空间 和 内核空间:
阻塞IO :
非阻塞IO :
IO多路复用:
是利用单个线程来同时监听多个Socket,并在某个Socket可读、可写时得到通知,从而避免无效的等待,充分利用CPU资源。
IO多路复用:同时监听多个朋友的写作业情况,哪个写完了就和哪个玩,玩完继续监听
监听方式也多种:
epoll,在得知谁就绪的同时,自然也吧用户写到用户空间,就可以直接操作数据,返回数据了
redis中的网络模型:
能解释一下IO多路复用吗 ???
1.I/0多路复用
是指利用单个线程来同时监听多个Socket ,并在某个Socket可读、可写时得到通知,从而避免无效的等待,充分利
用CPU资源。目前的I/0多路复用都是采用的epol模式实现,它会在通知用户进程Socket就绪的同时,把已就绪的
Socket写入用户空间,不需要挨个遍历Socket来判断是否就绪,提升了性能。
2.Redis网络模型
就是使用I/0多路复用结合事件的处理器来应对多个Socket请求
连接应答处理器
命令回复处理器,
在Redis6.0之后,为了提升更好的性能,使用了多线程来处理回复事件
命令请求处理器,在Redis6.0之后,将命令的转换使用了多线程,增加命令转换速度,在命令执行的时候,依然是单线程
面试: