Redis高频八股文与解决策略

本文详细解释了Redis的特性,介绍了缓存穿透、缓存雪崩和缓存击穿的概念,并探讨了如何通过策略如参数校验、布隆过滤器等解决这些问题。此外,文章还讨论了保证缓存与数据库数据一致性的方式,包括各种更新缓存和数据库的方案及其优缺点。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

什么是Redis?

Redis是一个高性能的非关系型的键值对数据库,使用C编写实现的。与传统的数据库不同的是Redis是存在内存中的,所以读写速度非常快,每秒可以处理超过10万次的读写操作,这也是Redis常常被用作缓存的原因。

什么是缓存穿透?

缓存穿透是指用户的请求没有经过缓存而直接请求到数据库上了,比如用户请求的key在Redis中不存在,或者用户恶意伪造大量不存在的key进行请求,都可以绕过缓存,导致数据库压力太大挂掉。

如何解决缓存穿透?

  • 参数校验、屏蔽非法参数
    检验用户请求的合法性,屏蔽掉不合理的参数请求(无法彻底避免)

  • 缓存空值
    数据库查询为空,可以给缓存一个空值或默认值,防止第二次再去数据库(导致Redis缓存大量无效数据,甚至内存爆掉)

  • 布隆过滤器
    使用布隆过滤器快速判断数据是否存在,将所有可能存在的数据哈希存到一个足够大的容器中,不存在的数据被这个bitmap拦截掉(布隆过滤器存在一定误差,且不支持删除数据,可通过定期重构解决数据变更问题)

什么是缓存雪崩?

缓存雪崩是指在某一个时刻出现大规模的缓存失效的情况,大量的请求直接打在数据库上面,可能会导致数据库宕机,如果这时重启数据库并不能解决根本问题,会再次造成缓存雪崩。

为什么会造成缓存雪崩?

一般来说,造成缓存雪崩主要有两种可能

  • Redis宕机了
  • 很多key采取了相同的过期时间

如何解决缓存雪崩?

  • 为避免Redis宕机造成缓存雪崩,可以搭建Redis集群
  • 尽量不要设置相同的过期时间,例如可以在原有的过期时间加上随机数
  • 服务降级,当流量到达一定的阈值时,就直接返回“系统繁忙”之类的提示,防止过多的请求打在数据库上,这样虽然用户体验较差,但至少可以使用,避免直接把数据库搞挂

什么是缓存击穿?

缓存雪崩是大规模的key失效,而缓存击穿是一个热点的Key,有大并发集中对其进行访问,突然间这个Key失效了,导致大并发全部打在数据库上,导致数据库压力剧增,这种现象就叫做缓存击穿。

比较经典的例子是商品秒杀时,大量的用户在抢某个商品时,商品的key突然过期失效了,所有请求都到数据库上了。

如何解决缓存击穿?

  • 虑热点key不设置过期时间,避免key过期失效
  • 在数据的value上增加一个逻辑到期时间,数据查询发现过期后进行加锁,拿到锁的请求去查询数据库进行Redis更新,未抢到锁的线程可以返回过期前的默认数据,用弱一致性来避免缓存击穿

如何保证缓存与数据库双写时的数据一致性?

这是面试的高频题,需要好好掌握,这个问题是没有最优解的,只能数据一致性和性能之间找到一个最适合业务的平衡点

首先先来了解下一致性,在分布式系统中,一致性是指多副本问题中的数据一致性。一致性可以分为强一致性、弱一致性和最终一致性

  • 强一致性:当更新操作完成之后,任何多个后续进程或者线程的访问都会返回最新的更新过的值。强一致性对用户比较友好,但对系统性能影响比较大。
  • 弱一致性:系统并不保证后续进程或者线程的访问都会返回最新的更新过的值。
  • 最终一致性:也是弱一致性的一种特殊形式,系统保证在没有后续更新的前提下,系统最终返回上一次更新操作的值。
    大多数系统都是采用的最终一致性,最终一致性是指系统中所有的副本经过一段时间的异步同步之后,最终能够达到一个一致性的状态,也就是说在数据的一致性上存在一个短暂的延迟。

如果想保证缓存和数据库的数据一致性,最简单的想法就是同时更新数据库和缓存,但是这实现起来并不现实,常见的方案主要有以下几种:

  • 先更新数据库,后更新缓存
  • 先更新缓存,后更新数据库
  • 先更新数据库,后删除缓存
  • 先删除缓存,后更新数据库

乍一看,感觉第一种方案就可以实现缓存和数据库一致性,其实不然,更新缓存是个坑,一般不会有更新缓存的操作。因为很多时候缓存中存的值不是直接从数据库直接取出来放到缓存中的,而是经过一系列计算得到的缓存值,如果数据库写操作频繁,缓存也会频繁更改,所以更新缓存代价是比较大的,并且更改后的缓存也不一定会被访问就又要重新更改了,这样做无意义的性能消耗太大了。

下面介绍删除缓存的方案

先更新数据库,后删除缓存

这种方案也存在一个问题,如果更新数据库成功了,删除缓存时没有成功,那么后面每次读取缓存时都是错误的数据。
解决这个问题的办法是删除重试机制,常见的方案有利用消息队列和数据库的日志
但这个方案也有一些缺点,比如系统复杂度高,对业务代码入侵严重,这时可以采用订阅数据库日志的方法删除缓存。

先删除缓存,后更新数据库

这种方案也存在一些问题,比如在并发环境下,有两个请求A和B,A是更新操作,B是查询操作,假设A请求先执行,会先删除缓存中的数据,然后去更新数据库,B请求查询缓存发现为空,会去查询数据库,并把这个值放到缓存中,在B查询数据库时A还没有完全更新成功,所以B查询并放到缓存中的是旧的值,并且以后每次查询缓存中的值都是错误的旧值。

这种情况的解决方法通常是采用延迟双删,就是为保证A操作已经完成,最后再删除一次缓存

逻辑很简单,删除缓存后,休眠一会儿再删除一次缓存,虽然逻辑看起来简单,但实现起来并不容易,问题就出在延迟时间设置多少合适,延迟时间一般大于B操作读取数据库+写入缓存的时间,这个只能是估算,一般可以考虑读业务逻辑数据的耗时 + 几百毫秒。

在实际应用中,还是先更新数据库后删除缓存这种方案用的多些,因为延迟双删这个操作的颗粒度过于庞大,不太适合实际开发。

需要注意的是,无论哪种方案,如果数据库采取读写分离+主从复制延迟的话,即使采用先更新数据库后删除缓存也会出现类似先删除缓存后更新数据库中出现的问题,举个例子

  1. A操作更新主库后,删除了缓存
  2. B操作查询缓存没有查到数据,查询从库拿到旧值
  3. 主库将新值同步到从库
  4. B操作将拿到的旧值写入缓存

这就造成了缓存中的是旧值,数据库中的是新值,解决方法还是上面说的延迟双删,延迟时间要大于主从复制的时间。

### Java面试高频问题及答案 #### 1. JRE、JDK、JVM 和 JIT 的区别 JRE(Java Runtime Environment)是运行 Java 程序所必需的环境,包含了 JVM 和其他支持文件[^1]。JDK(Java Development Kit)不仅包含 JRE,还提供了开发工具,例如编译器等。JVM(Java Virtual Machine)负责运行 Java 应用程序,通过解释或编译字节码来执行程序。JIT(Just In Time Compilation)是一种优化机制,当某些代码段被频繁调用时,JIT 编译器会将这些字节码转换为本地机器码以提高性能[^1]。 #### 2. 常见的字符串操作方法 `trim()` 方法用于去除字符串两端的空白字符[^2]。`split(String regex)` 方法根据指定的正则表达式分割字符串,并返回一个字符串数组[^2]。`getBytes()` 方法返回字符串对应的字节数组[^2]。`length()` 方法返回字符串的长度,即字符的数量[^2]。`toLowerCase()` 和 `toUpperCase()` 分别用于将字符串转换为小写和大写形式[^2]。`substring(int beginIndex, int endIndex)` 方法用于截取字符串的一部分[^2]。`equals(Object anObject)` 方法用于比较两个字符串是否相等[^2]。 #### 3. 栈溢出堆溢出的区别 如果线程的栈内存没有足够的空间存储方法调用或局部变量,JVM 将抛出 `java.lang.StackOverflowError` 异常[^3]。而当堆内存不足以分配新对象时,JVM 则会抛出 `java.lang.OutOfMemoryError` 异常[^3]。 #### 4. MySQL 日志类型 MySQL 中有三种主要的日志:Redo Log(重做日志)、Undo Log(回滚日志)和 Binlog(二进制日志)。Redo Log 用于在数据库崩溃后恢复未完成的事务;Undo Log 用于事务回滚以及多版本并发控制(MVCC);Binlog 记录了数据库的所有更改操作,可用于主从复制和数据恢复[^4]。 #### 5. 数据库索引类型 B+树是 MySQL 中最常用的索引结构。聚簇索引(Clustered Index)将数据行索引存储在一起,而非聚簇索引(Non-Clustered Index)则存储的是指向实际数据行的指针。最左匹配原则是指在使用复合索引时,查询条件必须从索引的最左边列开始匹配,否则索引可能无法被有效利用。 #### 6. 数据库事务的隔离级别 SQL 标准定义了四种事务隔离级别:Read Uncommitted(读未提交)、Read Committed(读已提交)、Repeatable Read(可重复读)和 Serializable(串行化)。每种隔离级别对应不同的并发控制策略,如脏读、不可重复读和幻读等问题[^4]。 #### 7. Redis 数据类型 Redis 支持五种主要的数据类型:String(字符串)、List(列表)、Set(集合)、Hash(哈希表)和 ZSet(有序集合)。每种数据类型都有其特定的应用场景,例如缓存、消息队列和计数器等[^4]。 #### 8. Redis 内存管理淘汰策略 Redis 使用内存存储数据,默认情况下不会持久化到磁盘。为了防止内存耗尽,Redis 提供了多种淘汰策略,如 LRU(Least Recently Used,最近最少使用)、LFU(Least Frequently Used,最不经常使用)和 TTL(Time To Live,生存时间)等[^4]。 #### 9. 线程模型锁机制 Java 中的线程模型基于操作系统级别的线程实现。线程之间的同步可以通过锁机制来实现,常见的锁包括互斥锁、读写锁和乐观锁等。此外,Java 还提供了高级的并发工具类,如 `Lock` 接口和 `Condition` 类,用于更精细地控制线程间的协作。 ```java // 示例代码:创建并启动一个线程 Thread thread = new Thread(() -> { System.out.println("Hello from a thread!"); }); thread.start(); ``` #### 10. JVM 内存模型 JVM 的内存分为几个区域:堆(Heap)、栈(Stack)、方法区(Method Area)、本地方法栈(Native Method Stack)和程序计数器(Program Counter Register)。其中,堆是所有线程共享的区域,用于存储对象实例;栈则是每个线程私有的,用于存储局部变量和方法调用信息[^3]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值