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

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

相关文章

Linux修改pip和conda缓存路径的几种方法

《Linux修改pip和conda缓存路径的几种方法》在Python生态中,pip和conda是两种常见的软件包管理工具,它们在安装、更新和卸载软件包时都会使用缓存来提高效率,适当地修改它们的缓存路径... 目录一、pip 和 conda 的缓存机制1. pip 的缓存机制默认缓存路径2. conda 的缓

Redis解决缓存击穿问题的两种方法

《Redis解决缓存击穿问题的两种方法》缓存击穿问题也叫热点Key问题,就是⼀个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击,本文给大家介绍了Re... 目录引言解决办法互斥锁(强一致,性能差)逻辑过期(高可用,性能优)设计逻辑过期时间引言缓存击穿:给

如何通过Golang的container/list实现LRU缓存算法

《如何通过Golang的container/list实现LRU缓存算法》文章介绍了Go语言中container/list包实现的双向链表,并探讨了如何使用链表实现LRU缓存,LRU缓存通过维护一个双向... 目录力扣:146. LRU 缓存主要结构 List 和 Element常用方法1. 初始化链表2.

如何关闭 Mac 触发角功能或设置修饰键? mac电脑防止误触设置技巧

《如何关闭Mac触发角功能或设置修饰键?mac电脑防止误触设置技巧》从Windows换到iOS大半年来,触发角是我觉得值得吹爆的MacBook效率神器,成为一大说服理由,下面我们就来看看mac电... MAC 的「触发角」功能虽然提高了效率,但过于灵敏也让不少用户感到头疼。特别是在关键时刻,一不小心就可能触

一文详解Nginx的强缓存和协商缓存

《一文详解Nginx的强缓存和协商缓存》这篇文章主要为大家详细介绍了Nginx中强缓存和协商缓存的相关知识,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录一、强缓存(Strong Cache)1. 定义2. 响应头3. Nginx 配置示例4. 行为5. 适用场景二、协商缓存(协

Golang基于内存的键值存储缓存库go-cache

《Golang基于内存的键值存储缓存库go-cache》go-cache是一个内存中的key:valuestore/cache库,适用于单机应用程序,本文主要介绍了Golang基于内存的键值存储缓存库... 目录文档安装方法示例1示例2使用注意点优点缺点go-cache 和 Redis 缓存对比1)功能特性

MySQL8.0设置redo缓存大小的实现

《MySQL8.0设置redo缓存大小的实现》本文主要在MySQL8.0.30及之后版本中使用innodb_redo_log_capacity参数在线更改redo缓存文件大小,下面就来介绍一下,具有一... mysql 8.0.30及之后版本可以使用innodb_redo_log_capacity参数来更改

MySQL 缓存机制与架构解析(最新推荐)

《MySQL缓存机制与架构解析(最新推荐)》本文详细介绍了MySQL的缓存机制和整体架构,包括一级缓存(InnoDBBufferPool)和二级缓存(QueryCache),文章还探讨了SQL... 目录一、mysql缓存机制概述二、MySQL整体架构三、SQL查询执行全流程四、MySQL 8.0为何移除查

redis防止短信恶意调用的实现

《redis防止短信恶意调用的实现》本文主要介绍了在场景登录或注册接口中使用短信验证码时遇到的恶意调用问题,并通过使用Redis分布式锁来解决,具有一定的参考价值,感兴趣的可以了解一下... 目录1.场景2.排查3.解决方案3.1 Redis锁实现3.2 方法调用1.场景登录或注册接口中,使用短信验证码场

Redis缓存问题与缓存更新机制详解

《Redis缓存问题与缓存更新机制详解》本文主要介绍了缓存问题及其解决方案,包括缓存穿透、缓存击穿、缓存雪崩等问题的成因以及相应的预防和解决方法,同时,还详细探讨了缓存更新机制,包括不同情况下的缓存更... 目录一、缓存问题1.1 缓存穿透1.1.1 问题来源1.1.2 解决方案1.2 缓存击穿1.2.1