【Redis笔记】缓存穿透与缓存击穿以及应对方法

  • 一、缓存穿透
    • 1. 缓存穿透概念
    • 2. 缓存穿透解决方法
      • 示例代码
  • 二、缓存击穿
    • 1. 缓存击穿概念
    • 2. 缓存击穿解决方法
      • 方法一:互斥锁
        • 示例代码
      • 方法二:设置逻辑过期时间
        • 示例代码
  • 三、工具类示例

一、缓存穿透

1. 缓存穿透概念

缓存穿透指查询不存在的数据时,不会命中缓存(缓存层),便会向数据库(持久层)发送查询请求,但同样查不到数据。

会给数据库性能带来压力

2. 缓存穿透解决方法

持久层查询到不存在的空数据时,也将其添加至缓存,并设置过期时间

对空数据进行缓存,可以避免重复查找空数据给数据库带来过多压力

访问无效数据的请求,一般只会在短期内重复访问(误操作或恶意访问),可以设置较短的过期时间,如 5 分钟,便于节省空间

示例代码

场景:根据用户 id 查看某用户的信息

业务层 UserServiceImpl 示例如下:

@Slf4j
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {@Resourceprivate StringRedisTemplate stringRedisTemplate;@Overridepublic User getByIdTreatPenetration(Long id) {// 在缓存中查找用户String key = "cache:user:" + id;String userStr = stringRedisTemplate.opsForValue().get(key);// 判断缓存是否命中,命中则直接返回if (userStr != null) {if (userStr.equals(""))return null;elsereturn JSONUtil.toBean(userStr, User.class);}// 缓存未命中,从数据库查找User user = this.getById(id);// 将数据存入缓存stringRedisTemplate.opsForValue().set(key, (user == null) ? "" : JSONUtil.toJsonStr(user)  // 空数据缓存空字符串, (user == null) ? 5 : 60 // 空数据缓存 5 分钟,非空缓存 60 分钟, TimeUnit.MINUTES);return user;}
}

二、缓存击穿

1. 缓存击穿概念

缓存击穿是指,当某个热点数据的缓存过期后,服务器同时收到大量查询此数据的请求,此时新的缓存尚未建立,就会向数据库发送大量查询请求

2. 缓存击穿解决方法

方法一:互斥锁

缓存未命中时获取锁,建立新的缓存,其它线程等待锁释放后再查找数据

如此可以达到 同一时刻对同一数据只有一个请求在访问数据库

【互斥锁特点】

  • 缓存正在被建立时,其它请求会等待建立完成,速度可能较慢
  • 用户查到的数据严格与数据库一致

示例代码

同样根据用户 id 查询某用户的信息

锁的操作使用了 Redisson,在处理缓存穿透的基础上,为未命中缓存的部分补充锁的部分即可

@Slf4j
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {@Resourceprivate StringRedisTemplate stringRedisTemplate;@Overridepublic User getByIdTreatBreakdownWithLock(Long id) {// 在缓存中查找用户String key = "user:" + id;String userStr = stringRedisTemplate.opsForValue().get(key);// 判断缓存是否命中,命中则直接返回if (userStr != null) {if (userStr.equals(""))return null;elsereturn JSONUtil.toBean(userStr, User.class);}// 缓存未命中,获取锁,查询数据库并建立缓存// 获取锁 Redisson 对象String lockName = "lock:user" + id;Config config = new Config();config.useSingleServer().setAddress("redis://192.168.100.103:6379").setPassword("123456").setDatabase(0);RLock lock = Redisson.create(config).getLock(lockName);try {// 尝试获取锁boolean flag = lock.tryLock();if (!flag) {// 获取锁失败,锁已被占用,线程睡眠 50 毫秒,然后递归调用此方法查询数据Thread.sleep(50);this.getByIdTreatBreakdownWithLock(id);}// 从数据库查询User user = this.getById(id);// 将数据存入缓存stringRedisTemplate.opsForValue().set(key, (user == null) ? "" : JSONUtil.toJsonStr(user) // 空数据缓存空字符串, (user == null) ? 5 : 60 // 空数据缓存 5 分钟,非空缓存 60 分钟, TimeUnit.MINUTES);return user;} catch (Exception e) {throw new RuntimeException(e.getMessage());} finally {// 释放锁lock.unlock();}}}

方法二:设置逻辑过期时间

前提:数据被建立时就加入缓存,并设置逻辑过期时间(实际缓存过期时间为 -1)

缓存未命中时,即说明数据库中不存在此数据,返回空
缓存命中时,查看逻辑过期时间是否过期,若已过期,返回已过期的数据,并在新线程中重新建立缓存并设置逻辑过期时间

【逻辑过期特点】

  • 数据被建立时就必须加入缓存
  • 异步建立缓存,不影响其它请求,速度较快
  • 由于逻辑过期依然返回已过期的数据,用户查到的数据可能与数据库一致

示例代码

根据用户 id 查询某用户的信息

由于要设置逻辑过期时间,可以将实际数据与过期时间包装成类,示例如下:

@Data
public RedisData getRedisData(String key) {String jsonStr = get(key);if (jsonStr == null) {return null;}return JSONUtil.toBean(jsonStr, RedisData.class);
}

业务层示例如下:

@Slf4j
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {// 线程池private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);@Resourceprivate StringRedisTemplate stringRedisTemplate;@Resourceprivate RedissonClient redissonClient;@Overridepublic User getByIdTreatBreakdownWithLogicExpire(Long id) {String key = "user:" + id;// 查询缓存String jsonStr = stringRedisTemplate.opsForValue().get(key);// 缓存未命中,即代表数据不存在,返回空if (redisData == null) {return null;}// 将缓存字符串装换为对象RedisData redisData = JSONUtil.toBean(jsonStr, RedisData.class);JSONObject jsonObject = (JSONObject) redisData.getData();User user = jsonObject.toBean(User.class);LocalDateTime expirationTime = redisData.getExpirationTime();// 未到期,直接返回if (LocalDateTime.now().isBefore(expirationTime)) {return user;}// 已到期,重建缓存String lockName = "lock:user:" + id;RLock lock = redissonClient.getLock(lockName);boolean flag = lock.tryLock();if (flag) {// 获取锁后进行二次判断,现有缓存是否过期jsonStr = stringRedisTemplate.opsForValue().get(key);redisData = JSONUtil.toBean(jsonStr, StringRedisCacheUtil.RedisData.class);expirationTime = redisData.getExpirationTime();// 第二次判断未到期,说明缓存已被别的线程重建,返回结果if (LocalDateTime.now().isBefore(expirationTime)) {return user;}// 新线程中重建缓存CACHE_REBUILD_EXECUTOR.submit(() -> {User user1 = this.getById(id);StringRedisCacheUtil.RedisData redisData1 = new StringRedisCacheUtil.RedisData();redisData1.setData(user1);redisData1.setExpirationTime(LocalDateTime.now().plusMinutes(60));stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData1));lock.unlock();});}return user;}
}

三、工具类示例

@Component
public class StringRedisCacheUtil {private static StringRedisTemplate stringRedisTemplate;private static RedissonClient redissonClient;private static ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);@Datapublic static class RedisData {private Object data;private LocalDateTime expirationTime;}private static Long NULL_CACHE_TTL = 5L;private static TimeUnit NULL_CACHE_TIMEUNIT = TimeUnit.MINUTES;// Spring 容器启动时,为静态变量注入对象// ----------------------------------------------------@Autowiredprivate StringRedisTemplate redisTemplate;@Autowiredprivate RedissonClient client;@PostConstructprivate void init() {stringRedisTemplate = this.redisTemplate;redissonClient = client;}private StringRedisCacheUtil() { }// ----------------------------------------------------// 根据键获取值public static String get(String key) {return stringRedisTemplate.opsForValue().get(key);}// 根据键获取带逻辑时间的值public static RedisData getRedisData(String key) {String jsonStr = get(key);if (jsonStr == null) {return null;}return JSONUtil.toBean(jsonStr, RedisData.class);}// 判断是否超过逻辑过期时间public static boolean isExpired(RedisData redisData) {return redisData == null || LocalDateTime.now().isAfter(redisData.getExpirationTime());}// 判断是否还没有逻辑过期时间public static boolean isNotExpired(RedisData redisData) {return !isExpired(redisData);}// 设置缓存public static void set(String key, String value, Long timeout, TimeUnit unit) {stringRedisTemplate.opsForValue().set(key, value, timeout, unit);}// 为空数据设置缓存public static void setNull(String key) {set(key, "", NULL_CACHE_TTL, NULL_CACHE_TIMEUNIT);}// 设置缓存并带有逻辑过期时间public static void setWithLogicalExpire(String key, Object value, Long timeout, TimeUnit unit) {RedisData redisData = new RedisData();redisData.setData(value);LocalDateTime now = LocalDateTime.now();redisData.setExpirationTime(now.plusMinutes(unit.toSeconds(timeout)));stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));}/*** 根据 id 查询数据,并解决缓存穿透问题(不解决击穿问题)** @param keyPrefix      键前缀* @param id             主键 id* @param resultType     返回类型的 Class* @param dbCallbackFunc 缓存未命中时,查询数据库所用的回调函数* @param timeOut        过期时间* @param unit           时间单位* @param <R>            返回类型* @param <ID>           主键 id 类型* @return 根据 id 查询到的数据或 null*/public static <R, ID> R getByIdTreatPenetration(String keyPrefix, ID id, Class<R> resultType,Function<ID, R> dbCallbackFunc, Long timeOut, TimeUnit unit) {String key = keyPrefix + id.toString();String JSONStr = get(key);if (JSONStr != null) {if (JSONStr.equals("")) return null;return JSONUtil.toBean(JSONStr, resultType);}R result = dbCallbackFunc.apply(id);if (result == null) setNull(key);else set(key, JSONUtil.toJsonStr(result), timeOut, unit);return result;}/*** 根据 id 查询数据,利用互斥锁解决缓存击穿问题** @param keyPrefix      键前缀* @param lockNamePrefix 锁名称前缀* @param id             主键 id* @param resultType     返回类型的 Class* @param dbCallbackFunc 缓存未命中时,查询数据库所用的回调函数* @param timeOut        过期寿命* @param unit           时间单位* @param <R>            返回类型* @param <ID>           主键 id 类型* @return 根据 id 查询到的数据或 null*/public static <R, ID> R getByIdTreatBreakdownWithLock(String keyPrefix, String lockNamePrefix, ID id, Class<R> resultType,Function<ID, R> dbCallbackFunc, Long timeOut, TimeUnit unit) {String key = keyPrefix + id.toString();String JSONStr = get(key);if (JSONStr != null) {if (JSONStr.equals("")) return null;return JSONUtil.toBean(JSONStr, resultType);}String lockName = lockNamePrefix + id;RLock lock = redissonClient.getLock(lockName);try {boolean flag = lock.tryLock();if (!flag) {Thread.sleep(50);getByIdTreatBreakdownWithLock(keyPrefix, lockNamePrefix, id, resultType, dbCallbackFunc, timeOut, unit);}R result = dbCallbackFunc.apply(id);if (result == null) setNull(key);else set(key, JSONUtil.toJsonStr(result), timeOut, unit);return result;} catch (Exception e) {throw new RuntimeException(e.getMessage());} finally {// 释放锁lock.unlock();}}/*** 根据 id 查询数据,利用逻辑过期解决缓存击穿问题(缓存未命中则代表不存在数据,不考虑穿透问题)** @param keyPrefix      键前缀* @param lockNamePrefix 锁名称前缀* @param id             主键 id* @param resultType     返回类型的 Class* @param dbCallbackFunc 缓存未命中时,查询数据库所用的回调函数* @param timeOut        逻辑过期寿命* @param unit           时间单位* @param <R>            返回类型* @param <ID>           主键 id 类型* @return 根据 id 查询到的数据*/public static <R, ID> R getByIdTreatBreakdownWithLogicExpire(String keyPrefix, String lockNamePrefix, ID id, Class<R> resultType,Function<ID, R> dbCallbackFunc, Long timeOut, TimeUnit unit) {String key = keyPrefix + id;RedisData redisData = getRedisData(key);if (redisData == null) {return null;}JSONObject jsonObject = (JSONObject) redisData.getData();R result = JSONUtil.toBean(jsonObject, resultType);if (isNotExpired(redisData)) {return result;}String lockName = lockNamePrefix + id.toString();RLock lock = redissonClient.getLock(lockName);boolean flag = lock.tryLock();if (flag && isExpired(getRedisData(key))) {CACHE_REBUILD_EXECUTOR.submit(() -> {R data = dbCallbackFunc.apply(id);setWithLogicalExpire(key, data, timeOut, unit);lock.unlock();});}return result;}}

【Redis笔记】缓存穿透与缓存击穿以及应对方法相关推荐

  1. Redis 缓存击穿(失效)、缓存穿透、缓存雪崩怎么解决?

    欢迎关注方志朋的博客,回复"666"获面试宝典 原始数据存储在 DB 中(如 MySQL.Hbase 等),但 DB 的读写性能低.延迟高. 比如 MySQL 在 4 核 8G 上 ...

  2. Redis专题-缓存穿透、缓存雪崩、缓存击穿

    一.缓存穿透 缓存穿透概念 缓存穿透是指查询一个一定不存在的数据,在数据库没有,自然在缓存中也不会有.导致用户查询的时候,在缓存中找不到对应key的value,每次都要去数据库再查询一遍,如果从存储层 ...

  3. Redis的缓存雪崩、缓存击穿、缓存穿透与缓存预热、缓存降级

    一.缓存雪崩: 1.什么是缓存雪崩: 如果缓在某一个时刻出现大规模的key失效,那么就会导致大量的请求打在了数据库上面,导致数据库压力巨大,如果在高并发的情况下,可能瞬间就会导致数据库宕机.这时候如果 ...

  4. Redis缓存穿透、缓存击穿和缓存雪崩

    目录 一.缓存穿透 概念 解决方案 布隆过滤器 缓存空对象 二.缓存击穿 概念 解决方案 使用互斥锁(mutex key) 设置热点数据永不过期 三.缓存雪崩 概念 解决方案 redis高可用 限流降 ...

  5. redis缓存穿透、缓存雪崩、缓存击穿、并发竞争

    关注微信公众号"虾米聊吧",每天更新一篇技术文章,文章内容涵盖架构师成长必经之路应掌握的技术,一起学习,一起交流. 缓存穿透.缓存雪崩.缓存击穿.并发竞争是缓存最常见的几个问题,接 ...

  6. Redis学习之缓存穿透、缓存击穿和缓存雪崩详解

    目录 缓存穿透 解决方案 缓存空对象 布隆过滤器 缓存击穿 解决方案 对访问数据库的操作加锁 提前缓存热点数据,设置热点数据永不过期 缓存雪崩 解决方案 Redis高可用 限流降级 数据预热 设置合理 ...

  7. redis专题:redis缓存穿透、缓存击穿、缓存雪崩等问题如何解决?

    文章目录 1. 缓存穿透 1.1 缓存空对象 1.2 布隆过滤器 2. 缓存击穿(失效) 3. 缓存雪崩 在高并发项目中,redis作为热门中间件,在为项目带来便利性的同时,也存在一些隐患,比如缓存穿 ...

  8. redis缓存穿透,缓存击穿与缓存雪崩详解

    前言 在web应用开发中,redis越来越多的应用于各种需要缓存的场景中,比较经典的使用场景就是,使用redis配合mysql做二级缓存,以应对在流量高峰的时候,减少高并发请求对数据库的压力 但是在这 ...

  9. 【重难点】【Redis 03】缓存雪崩、缓存穿透、缓存击穿、Redis 的内存过期策略、并发读写和双写

    [重难点][Redis 03]缓存雪崩.缓存穿透.缓存击穿.Redis 的内存过期策略.并发读写和双写 文章目录 [重难点][Redis 03]缓存雪崩.缓存穿透.缓存击穿.Redis 的内存过期策略 ...

最新文章

  1. Ubuntu 误改/etc/sudoers 文件权限
  2. 调用startActivityForResult后,onActivityResult无响应的题目
  3. C#各种配置文件使用,操作方法总结
  4. arcgis按属性设置符号大小
  5. JAVA追加写文件方法
  6. 让secureCRT正确显示中文
  7. IO中同步、异步与阻塞、非阻塞的区别
  8. 控制台js常用解决方案,字符串替换和抓取列表页链接
  9. linux tar文件夹打包不包含目录,tar打包如何不打包某一个文件夹(排除某些文件夹)...
  10. python实现集合并交差运算_集合的并交差运算
  11. java8中的date和joda time中的日期相互转换
  12. 《致加西亚的一封信》读后感
  13. html5 图片羽化,html5+webgl仿ps羽化笔刷液态动画特效
  14. docx,pptx等正确的mime类型是什么?
  15. 阿里无人超市 “微笑打折”成世界互联网大会热点
  16. python中map函数返回值类型_Python学习第42课-map()函数
  17. 为什么你会觉得苹果已无创新?耶稣已死,商人掌舵!!
  18. qt将html加载到资源文件,web页面嵌入到Qt
  19. 《道德经》与“低熵”思想炫酷实现(.html)
  20. 计算机网络经典图书推荐

热门文章

  1. 比尔·盖茨买百万亩农地成美“头号地主”,图扑数字孪生农场
  2. Linux 串口终端kermit安装和使用
  3. AI未来是什么样子,这些科幻电影里已经有了答案
  4. Linux ubuntu 装openCV,Linux(ubuntu 16.04) 安装 opencv C++
  5. python官网学习爬虫资料_Python爬虫学习?
  6. 08-02-loggin-模块
  7. 深入理解RunLoop
  8. MacBook Pro (M1 Pro芯片)使用安卓USB共享上网
  9. KubeCon 上海 SOFAStack Cloud Native Workshop 报名中
  10. Android 实现自动点击屏幕的方法