防止缓存击穿、缓存穿透和缓存雪崩

2024-09-08 14:28

本文主要是介绍防止缓存击穿、缓存穿透和缓存雪崩,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

使用Redis缓存防止缓存击穿、缓存穿透和缓存雪崩

在高并发系统中,缓存击穿、缓存穿透和缓存雪崩是三种常见的缓存问题。本文将介绍如何使用Redis、分布式锁和布隆过滤器有效解决这些问题,并且会通过Java代码详细说明实现的思路和原因。


1. 背景

缓存穿透:指的是大量请求缓存中不存在且数据库中也不存在的数据,导致大量请求直接打到数据库上,形成数据库压力。

缓存击穿:指的是某个热点数据在高并发时失效,大量请求同时穿透缓存,导致数据库负载瞬间激增。

缓存雪崩:指的是缓存集中过期或宕机,导致短时间内大量请求打到数据库,压垮后端服务。

为了应对这三种情况,本文介绍了三种方法:

  1. 缓存穿透:使用空值缓存和布隆过滤器。
  2. 缓存击穿:使用分布式锁保证只有一个线程访问数据库并重建缓存。
  3. 缓存雪崩:合理设置缓存过期时间,防止大规模缓存同时失效。

2. 缓存穿透处理

我们首先来看queryWithPassThrough方法,它处理的是缓存穿透问题。

public <R, P> R queryWithPassThrough(String key, Class<R> clazz, Function<P, R> bdCallback, P params, Duration duration) {// 1. 从缓存中查询数据String dataJsonStr = stringRedisTemplate.opsForValue().get(key);// 2. 如果缓存中存在数据,直接返回if (StrUtil.isNotBlank(dataJsonStr)) {return JSONUtil.toBean(dataJsonStr, clazz);}// 3. 如果缓存存在的是空字符串,表示数据库中也没有该数据,直接返回null,防止缓存穿透if ("".equals(dataJsonStr)) {return null;}// 4. 缓存没有命中,调用数据库查询R data = bdCallback.apply(params);// 5. 如果数据库中没有数据,缓存空字符串以防止缓存穿透,并设置较短的过期时间if (data == null) {stringRedisTemplate.opsForValue().set(key, "", Duration.ofSeconds(20));return null;}// 6. 数据库查询有结果,将结果缓存并设置过期时间stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(data), duration);return data;
}
解析:
  1. 缓存查询:首先从Redis缓存中查询数据,如果有数据,直接返回。
  2. 缓存空值机制:如果缓存中存储的是空字符串(即以前数据库查询也没有该数据),防止缓存穿透(同一个不存在的请求反复击穿缓存,查询数据库)。
  3. 查询数据库:当缓存没有命中且没有空值时,查询数据库并缓存结果。如果数据库中没有数据,缓存空值并设置短期过期时间(例如20秒)。
为什么要缓存空值?

防止缓存穿透。如果没有缓存空值,对于不存在的数据的查询会反复击穿缓存,导致数据库压力过大。


3. 缓存击穿处理

缓存击穿问题通常发生在热点数据过期时,同时有大量请求到达数据库。为了解决这个问题,我们使用分布式锁来保证在缓存失效时,只有一个线程能够访问数据库并更新缓存。

private <R, P> R queryWithMutex(String key, Class<R> clazz, Duration expireSeconds, Function<P, R> bdCallback, P params) {// 1. 从缓存查询数据String dataJson = stringRedisTemplate.opsForValue().get(key);// 2. 如果缓存中有数据,直接返回if (StrUtil.isNotBlank(dataJson)) {return JSONUtil.toBean(dataJson, clazz);}// 3. 如果缓存中是空值,返回null,防止穿透if ("".equals(dataJson)) {return null;}// 4. 缓存没有命中,尝试加锁String lock = "lock:" + key;try {while (!tryLock(lock)) {// 如果获取不到锁,等待并重复检查缓存Thread.sleep(50L);dataJson = stringRedisTemplate.opsForValue().get(key);if (StrUtil.isNotBlank(dataJson)) {return JSONUtil.toBean(dataJson, clazz);}if ("".equals(dataJson)) {return null;}}// 5. 加锁后再次检查缓存,防止其他线程已经重建缓存dataJson = stringRedisTemplate.opsForValue().get(key);if (StrUtil.isNotBlank(dataJson)) {return JSONUtil.toBean(dataJson, clazz);}if ("".equals(dataJson)) {return null;}// 6. 如果缓存没有数据,查询数据库R apply = bdCallback.apply(params);if (apply == null) {// 如果数据库没有数据,缓存空值stringRedisTemplate.opsForValue().set(key, "", Duration.ofMinutes(2));return null;}// 7. 将数据库数据存入缓存stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(apply), expireSeconds);return apply;} catch (Exception e) {throw new RuntimeException(e);} finally {// 8. 释放锁releaseLock(lock);}
}
解析:
  1. 缓存查询:如果缓存中有数据,直接返回,避免击穿。
  2. 加锁机制:如果缓存没有数据,尝试获取锁。如果获取不到锁,说明其他线程正在重建缓存,此时线程等待并轮询缓存,直到锁释放或缓存更新。
  3. 缓存重建:拿到锁的线程查询数据库,并将结果存入缓存。
  4. 锁的释放:确保在缓存重建完成后释放锁,避免死锁。
为什么需要加锁?

防止缓存击穿时,多个线程同时查询数据库,导致数据库压力激增。通过分布式锁,保证只有一个线程可以查询数据库并更新缓存。


4. 逻辑过期处理

为了解决缓存雪崩问题,可以通过逻辑过期方式处理热点数据的过期。即使数据过期了,系统也能继续返回旧数据,同时后台线程异步更新缓存。

private <R, P> R queryWithLogicalExpire(String key, Class<R> clazz, Duration expireSeconds, Function<P, R> bdCallback, P params) {// 1. 从缓存查询数据String dataJson = stringRedisTemplate.opsForValue().get(key);if (StrUtil.isBlank(dataJson)) {return null;}// 2. 反序列化数据RedisData data = JSONUtil.toBean(dataJson, RedisData.class);R r = JSONUtil.toBean((JSONObject) data.getData(), clazz);// 3. 检查数据是否过期,如果没有过期,直接返回数据if (data.getExpireTime().isAfter(LocalDateTime.now())) {return r;}// 4. 如果数据已过期,尝试加锁,进行缓存重建String lock = "lock:" + key;if (tryLock(lock)) {try {dataJson = stringRedisTemplate.opsForValue().get(key);data = JSONUtil.toBean(dataJson, RedisData.class);if (data.getExpireTime().isAfter(LocalDateTime.now())) {return JSONUtil.toBean((JSONObject) data.getData(), clazz);}// 5. 异步重建缓存CACHE_REBUILD_EXECUTOR.submit(() -> {try {R newData = bdCallback.apply(params);RedisData redisData = new RedisData(newData, LocalDateTime.now().plusSeconds(expireSeconds.getSeconds()));stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));} catch (Exception e) {throw new RuntimeException(e);} finally {releaseLock(lock);}});} finally {releaseLock(lock);}}// 6. 返回过期数据,保证系统可用性return r;
}
解析:
  1. 缓存逻辑过期:数据存入缓存时,同时存储逻辑过期时间。即使数据过期了,系统可以继续返回旧数据,防止雪崩。
  2. 后台异步更新:通过线程池,异步执行缓存重建,防止阻塞用户请求。
  3. 双重检查:在获取锁后,再次检查缓存

,避免重复重建缓存。

为什么需要异步更新?

即使数据过期,也可以返回旧数据,保证服务可用性。同时在后台异步更新缓存,减少对前台服务的影响。


5. 分布式锁

为确保只有一个线程能重建缓存,我们使用了Redis的setIfAbsent方法实现分布式锁,并且为锁设置了过期时间,防止死锁。

private boolean tryLock(String key) {// 尝试获取锁,并设置锁的过期时间,防止死锁Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", Duration.ofSeconds(10L));return Boolean.TRUE.equals(flag);
}private void releaseLock(String key) {// 释放锁stringRedisTemplate.delete(key);
}
解析:
  • 获取锁:通过setIfAbsent,确保只有一个线程能获取锁,并设置锁过期时间,防止死锁。
  • 释放锁:任务完成后,释放锁,允许其他线程继续操作。
为什么要设置锁的过期时间?

防止由于异常情况(例如服务器宕机)导致锁无法释放,产生死锁问题。


6. 总结

通过使用Redis缓存、分布式锁和逻辑过期策略,我们可以有效解决缓存穿透缓存击穿缓存雪崩问题。通过这套方案,我们能够在保证系统高可用的同时,减少数据库压力,并提高服务的性能和稳定性。

关键点包括:

  • 缓存空值机制:防止缓存穿透。
  • 分布式锁:防止缓存击穿时多个线程同时访问数据库。

这套方案对于处理高并发场景下的缓存问题是非常有效的。希望这篇文章能帮助大家更好地理解和应对缓存问题。

这篇关于防止缓存击穿、缓存穿透和缓存雪崩的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1148362

相关文章

缓存雪崩问题

缓存雪崩是缓存中大量key失效后当高并发到来时导致大量请求到数据库,瞬间耗尽数据库资源,导致数据库无法使用。 解决方案: 1、使用锁进行控制 2、对同一类型信息的key设置不同的过期时间 3、缓存预热 1. 什么是缓存雪崩 缓存雪崩是指在短时间内,大量缓存数据同时失效,导致所有请求直接涌向数据库,瞬间增加数据库的负载压力,可能导致数据库性能下降甚至崩溃。这种情况往往发生在缓存中大量 k

Redis中使用布隆过滤器解决缓存穿透问题

一、缓存穿透(失效)问题 缓存穿透是指查询一个一定不存在的数据,由于缓存中没有命中,会去数据库中查询,而数据库中也没有该数据,并且每次查询都不会命中缓存,从而每次请求都直接打到了数据库上,这会给数据库带来巨大压力。 二、布隆过滤器原理 布隆过滤器(Bloom Filter)是一种空间效率很高的随机数据结构,它利用多个不同的哈希函数将一个元素映射到一个位数组中的多个位置,并将这些位置的值置

C# 防止按钮botton重复“点击”的方法

在使用C#的按钮控件的时候,经常我们想如果出现了多次点击的时候只让其在执行的时候只响应一次。这个时候很多人可能会想到使用Enable=false, 但是实际情况是还是会被多次触发,因为C#采用的是消息队列机制,这个时候我们只需要在Enable = true 之前加一句 Application.DoEvents();就能达到防止重复点击的问题。 private void btnGenerateSh

PHP防止SQL注入详解及防范

SQL 注入是PHP应用中最常见的漏洞之一。事实上令人惊奇的是,开发者要同时犯两个错误才会引发一个SQL注入漏洞。 一个是没有对输入的数据进行过滤(过滤输入),还有一个是没有对发送到数据库的数据进行转义(转义输出)。这两个重要的步骤缺一不可,需要同时加以特别关注以减少程序错误。 对于攻击者来说,进行SQL注入攻击需要思考和试验,对数据库方案进行有根有据的推理非常有必要(当然假设攻击者看不到你的

PHP防止SQL注入的方法(2)

如果用户输入的是直接插入到一个SQL语句中的查询,应用程序会很容易受到SQL注入,例如下面的例子: $unsafe_variable = $_POST['user_input'];mysql_query("INSERT INTO table (column) VALUES ('" . $unsafe_variable . "')"); 这是因为用户可以输入类似VALUE”); DROP TA

PHP防止SQL注入的方法(1)

(1)mysql_real_escape_string – 转义 SQL 语句中使用的字符串中的特殊字符,并考虑到连接的当前字符集 使用方法如下: $sql = "select count(*) as ctr from users where username ='".mysql_real_escape_string($username)."' and password='". mysql_r

PHP APC缓存函数使用教程

APC,全称是Alternative PHP Cache,官方翻译叫”可选PHP缓存”。它为我们提供了缓存和优化PHP的中间代码的框架。 APC的缓存分两部分:系统缓存和用户数据缓存。(Linux APC扩展安装) 系统缓存 它是指APC把PHP文件源码的编译结果缓存起来,然后在每次调用时先对比时间标记。如果未过期,则使用缓存的中间代码运行。默认缓存 3600s(一小时)。但是这样仍会浪费大量C

缓存策略使用总结

缓存是提高系统性能的最简单方法之一。相对而言,数据库(or NoSQL数据库)的速度比较慢,而速度却又是致胜的关键。 如果使用得当,缓存可以减少相应时间、减少数据库负载以及节省成本。本文罗列了几种缓存策略,选择正确的一种会有很大的不同。缓存策略取决于数据和数据访问模式。换句话说,数据是如何写和读的。例如: 系统是写多读少的吗?(例如基于时间的日志)数据是否是只写入一次并被读取多次?(例如用户配

起点中文网防止网页调试的代码展示

起点中文网对爬虫非常敏感。如图,想在页面启用调试后会显示“已在调试程序中暂停”。 选择停用断点并继续运行后会造成cpu占用率升高电脑卡顿。 经简单分析网站使用了js代码用于防止调试并在强制继续运行后造成电脑卡顿,代码如下: function A(A, B) {if (null != B && "undefined" != typeof Symbol && B[Symbol.hasInstan

java-redis-雪崩

Redis 雪崩问题 Redis雪崩 是指在 Redis 缓存系统中,当大量缓存同时失效时,所有请求直接打到数据库,导致数据库瞬间压力激增,甚至崩溃的现象。雪崩问题通常出现在高并发的系统中,因为缓存的失效导致后端数据库承受不了巨大的请求量。 具体表现: 大量缓存同时失效后,所有流量直接访问数据库。数据库承载过大的并发量,导致性能急剧下降,甚至崩溃。之后,当 Redis 缓存恢复正常时,由于数