问: 如果发生了缓存穿透、击穿、雪崩该如何解决?
redis使用场景-缓存-缓存穿透
缓存穿透:是指在缓存和数据库中,攻击者试图通过提交大量不存在的键值对查询请求,当redis查询不到时候去数据库中查询也查询不到数据也不会直接写入缓存中,导致这些请求直接穿透缓存层,直接访问数据库,从而对数据库造成巨大压力甚至崩溃
解决方案一:缓存空数据
实现思路原理回答: 对于不存在的键值对,仍把这个空结果进行缓存(如null)缓存起来。这样,后续的查询可以直接从缓存中获取到结果,而不需要每次都穿透到数据库。
实现步骤:
-
在缓存中设置不存在的键值对及其对应的空结果(例如null)。
-
设置一个合理的过期时间,避免无限期地缓存空结果。
优点:简单
缺点:消耗内存,可能会发生不一致的问题
解决方案二:布隆过滤器
布隆过滤器实现思路原理回答: 布隆过滤器主要是用于检索一个元素是否在一个集合中。我们当时使用的是redisson实现的布降过滤器·
它的底层主要是先去初始化一个比较大数组,里面存放的二进制0或1。在一开始都是0,当一个key来了之后经过3次hash计算( 运算次数不一定几次,根据你设置的命中率决定的,你想要命中率高,就会多次进行hash运算,因为hash运算是有hash冲突的。),模于数组长度找到数据的下标然后把数组中原来的0改为1,这样的话,三个数组的位置就能标明一个key的存在。查找的过程也是一样的。
当然是有缺点的,布隆过滤器有可能会产生一定的误判,我们一般可以设置这个误判率,大概不会超过5%,其实这个误判是必然存在的,要不就得增加数组的长度,其实已经算是很划分了,5%以内的误判率一般的项目也能接受,不至于高并发下压倒数据库。
优点:
1:它是由二进制组成,占的空间特别小。
2:根据下标去查询,所以速度特别快。
缺点:
1:它是有误判的,明明这个数据不存在的?但是却认为存在。hash冲突原理
2:数据删除不了,
如果用本地内存实现,每半个月重启一次服务器,数据丢失,redis实现,重复数据会越来越多。时间越久,结果越不准确。因为数据只增不减
redis使用场景-缓存-缓存击穿
缓存击穿: 给某一个key设置了过期时间,当key过期的时候,恰好这时间点对这个key有大量的并发请求过来,这些并发的请求可能会瞬间把DB压垮
解决方案一:互斥锁(强一致性,性能差)
实现思路原理回答:使用互斥锁,当缓存失效时,不立即去load db,先使用如 Redis 的setnx 去设置一个互斥锁,当操作成功返回时再进行load db的操作并回设缓存,否则重试get缓存的方法
解决方案二:逻辑过期(高可用,性能优,不能保证数据绝对一致)
实现思路原理回答:
- 在设置key的时候,设置一个过期时间字段一块存入缓存中,不给当前key设置过期时间
- 当查询的时候,从redis取出数据后判断时间是否过期
- 如果过期则开通另外一个线程进行数据同步,当前线程正常返回数据,这个数据不是最新
当然两种方案各有利弊:
如果选择数据的强一致性,建议使用分布式锁的方案,性能上可能没那么高,锁需要等,也有可能产生死锁的问题(给锁设置合理的过期时间,防止死锁。)
如果选择key的逻辑删除,则优先考虑的高可用性,性能比较高,但是数据同步这块做不到强一致。
redis使用场景-缓存-缓存雪崩
缓存雪崩: 缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,数据库瞬时压力过重带来巨大压力导致雪崩。 (与缓存击穿的区别是: 雪崩是很多key,击穿是某一个key缓存)
解决方案:
- 给不同的key的TTL添加随机值
- 利用Redis集群提高服务的可用性 (哨兵模式、集群模式)
- 给缓存业务添加降级限流策略 降级可作为系统的保底策略,适用于穿透、击穿、雪崩
- 给业务添加多级缓存
第一种给不同的key的TTL添加随机值 实现思路原理回答:解决方案主要是可以将缓存失效时间分散开,比如可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
《缓存三兄弟》 方便记忆
穿透无中生有key,布隆过滤null隔离。
缓存击穿过期key,锁与非期解难题。
雪崩大量过期key,过期时间要随机。
面试必考三兄弟,可用限流来保底。
问:redis作为缓存,mysql的数据如何与redis进行同步呢?(考察双写一致性问题)
双写一致性
双写一致性:当修改了数据库的数据也要同时更新缓存的数据,缓存和数据库的数据要保持一致
针对这个问题的回答一定要设置前提,先介绍自己的业务背景来回答,两种不同的业务场景分别为 强一致性和允许延迟一致
在介绍双写一致性的时候我们需要知道什么场景下会出现脏数据达不到双写的强一致性
如下:
先删除缓存还是先修改数据库?
关于双写一致性问题,无论是先删除缓存再修改数据库,还是先修改数据库再删除缓存都会有脏数据的问题,达不到双写一致性(因为程序运行时同时会有多个请求,不同的线程在执行操作,很有可能一个线程在修改数据的同时,另一个线程已经读取到了脏数据)
为什么不用延时双删?
延时双删如果是写操作,我们先把缓存中的数据删除,然后更新数据库,最后再延时删除缓存中的数据,其中这个延时多久不太好确定(如数据库设置主从时,可能需要留足数据库主从数据同步的时间),在延时过程中可能会出现脏数据
强一致性实现
第一种使用分布式锁
这种分布式锁的方式虽然解决了数据强一致性问题,但是性能太低,因为对读和写都加锁同时只有一个线程在进行
第二种使用读写锁 (正常使用redis存储数据的场景都是读多写少的场景,读多写少的场景适合使用读写锁来提高性能)
这种读写锁redisson框架已经给我们提供,实现代码可以看下边Redisson模块的代码示例
允许延时一致实现
异步通知保证数据的最终一致性:
基于Canal的异步通知:
双写一致性问题的总结:
Redisson读写锁
1. 什么是Redisson读写锁
Redisson读写锁(Read-Write Lock)是Redisson框架提供的一种基于Redis实现的分布式读写锁。它允许多个读操作同时进行而不互相阻塞,但写操作会独占访问权,即写操作会阻塞其他写操作和读操作。这种锁机制对于提高读密集型应用的性能非常有效。
2. Redisson读写锁的工作原理
Redisson读写锁通过Redis的数据结构和命令来实现。在Redisson中,读写锁通常通过Redis的发布/订阅、Lua脚本、以及Redis的数据类型(如哈希表、列表等)来协同工作。写锁会设置一个特定的标识(如一个特定的键和值),并在持有期间阻止其他写锁和读锁的获取。读锁则通过一种计数机制来允许多个读操作同时进行,而不会互相干扰。
3. 使用Redisson读写锁的基本代码示例
以下是使用Redisson读写锁的一个基本示例。首先,确保你已经添加了Redisson的依赖到你的项目中。
4. Redisson读写锁的使用场景
- 读多写少的应用:在这种场景下,Redisson读写锁可以显著提高性能,因为多个读操作可以同时进行。
- 缓存更新:在缓存系统中,当需要更新缓存时,可以使用写锁来确保缓存的一致性和完整性。
- 分布式数据访问:在分布式系统中,多个服务或实例可能需要同时访问同一份数据。使用Redisson读写锁可以协调这些访问,避免数据冲突。
5. 使用Redisson读写锁时需要注意的事项
- 死锁问题:在使用读写锁时,要特别注意避免死锁。确保在获取锁后能够正常释放锁,并在必要时实现超时机制。
- 性能考虑:虽然读写锁可以提高读操作的性能,但在高并发的写操作场景下,写锁的独占性可能会导致性能瓶颈。
- 异常处理:在获取锁和释放锁的过程中,要妥善处理可能出现的异常,确保锁能够正确释放。
- 客户端重启:如果Redisson客户端重启,之前持有的锁可能会丢失。因此,在设计系统时要考虑到这种情况,并采取相应的措施(如使用看门狗等)。
在编程中,读写锁(ReadWriteLock)和看门狗(Watchdog)是两个不同的概念,但它们可以在某些场景下结合使用以提高系统的稳定性和可靠性。以下是如何在读写锁的场景中使用看门狗机制的一些解释和示例。
看门狗机制
看门狗机制通常用于监控系统的运行状态,以防止系统因程序错误或外部干扰而陷入死循环或停滞状态。在硬件系统中,看门狗通常是一个定时器电路,如果主程序在一定时间内没有对其进行复位(即“喂狗”),看门狗就会触发复位信号,使系统重新启动。在软件系统中,看门狗机制可以通过定时任务或线程来实现类似的功能。
使用Redisson实现分布式读写锁并结合看门狗机制的示例:
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.api.RReadWriteLock;
import org.redisson.config.Config;
import java.util.concurrent.TimeUnit;
public class RedissonWatchdogExample {
public static void main(String[] args) {
// 配置Redisson客户端
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
Redisson redisson = Redisson.create(config);
// 获取读写锁
RReadWriteLock readWriteLock = redisson.getReadWriteLock("readWriteLock");
// 使用读锁
new Thread(() -> {
RLock readLock = readWriteLock.readLock();
try {
readLock.lock();
// 执行读操作
System.out.println("Read lock acquired");
// 模拟读操作耗时
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
readLock.unlock();
System.out.println("Read lock released");
}
}).start();
// 使用写锁并结合看门狗机制(不设置超时时间则启用看门狗)
new Thread(() -> {
RLock writeLock = readWriteLock.writeLock();
try {
writeLock.lock();
// 执行写操作
System.out.println("Write lock acquired");
// 模拟写操作耗时(超过默认看门狗续期时间)
Thread.sleep(40000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
writeLock.unlock();
System.out.println("Write lock released");
}
}).start();
}
}
在这个示例中,我们使用了Redisson库来实现分布式读写锁。对于写锁,我们没有设置超时时间,因此Redisson会启用看门狗机制来自动续期锁。如果持有写锁的线程因某种原因长时间无法完成操作,看门狗机制会定期续期锁,以防止锁被意外释放。然而,需要注意的是,如果系统或网络出现故障,看门狗机制可能无法正常工作,因此在实际应用中还需要结合其他容错和恢复策略来提高系统的可靠性。