黑马点评-07缓存击穿问题(热点key失效)及解决方案,互斥锁和设置逻辑过期时间

缓存击穿问题(热点key失效)

缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且重建缓存业务较复杂的key突然失效了,此时无数的请求访问会在瞬间打到数据库,带来巨大的冲击

  • 一件秒杀中的商品的key突然失效了,由于大家都在疯狂抢购那么这个瞬间就会有无数的请求访问去直接抵达数据库,从而造成缓存击穿

在这里插入图片描述

互斥锁

如果缓存中没有缓存对应的店铺信息时,所有的线程过来后需要先获取锁才能查询数据库中的店铺信息,保证只有一个线程访问数据库,避免数据库访问压力过大

  • 优点: 实现简单且没有额外内存销毁(加一把锁), 当拿到线程锁的线程把缓存数据重建好后,其他线程再访问时从缓存中查询的数据和数据库中的数据就是一致的
  • 缺点: 当拿到线程锁的线程在操作数据库的时候,其他线程只能等待,将查询的性能从并行变成了串行(tryLock方法+double check可以解决),但是还有死锁的风险

在这里插入图片描述

setnx实现互斥锁

根据店铺Id查询商铺信息,增加了获取互斥锁的环节,即缓存未命中时只有获取锁成功的线程才能查询数据库,保证只有一个线程去数据库执行查询语句,防止缓存击穿

在这里插入图片描述

利用redis提供的setnx key(锁Id) value命令判断是否有线程成功插入key(锁), del key表示释放锁

返回值描述
0表示线程插入key失败,即线程获取锁失败
1表示线程插入key成功即线程获取锁成功

StringRedisTemplate中对应setnx指令的方法是setIfAbsent(),返回true表示插入成功,fasle表示插入失败

// 每一个店铺都有自己的锁,根据锁的Id(锁前缀+店铺ID)尝试获取锁(本质是插入key)
private boolean tryLock(String key) {Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);// 我们这里使用了BooleanUtil工具类将Boolean类型的变量转化为boolean,避免在拆箱过程中返回nullreturn BooleanUtil.isTrue(flag);
}// 释放锁(本质是删除key)
private void unlock(String key) {stringRedisTemplate.delete(key);
}

单独实现负责解决缓存击穿问题的方法queryWithMutex,在该方法中如果查到店铺信息返回shop查不到则返回null,最后在queryById中做统一判断返回结果类

  • 获取锁成功,应该再次检测redis缓存是否存在,因为此时可能其他线程重建完缓存刚释放完锁后,做双重检查,如果存在则无需重建缓存

在这里插入图片描述

@Override
public Result queryById(Long id) {    // 使用互斥锁解决缓存击穿Shop shop = queryWithMutex(id);// 如果shop等于null,表示数据库中对应店铺不存在或者缓存的店铺信息是空字符串if (shop == null) {return Result.fail("店铺不存在!!");}// shop不等于null,把查询到的商户信息返回给前端return Result.ok(shop);
}
@Override
public Shop queryWithMutex(Long id) {//1.先从Redis中查询对应的店铺缓存信息,这里的常量值是固定的店铺前缀+查询店铺的IdString shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);//2.如果在Redis中查询到了店铺信息,并且店铺的信息不是空字符串则转为Shop类型直接返回,""和null以及"/t/n(换行)"都会判定为空即返回falseif (StrUtil.isNotBlank(shopJson)) {Shop shop = JSONUtil.toBean(shopJson, Shop.class);return shop;}//3.如果命中的是空字符串即我们缓存的空数据,返回nullif (shopJson != null) {return null;}// 4.没有命中则尝试根据锁的Id(锁前缀+店铺Id)获取互斥锁(本质是插入key),实现缓存重构// 调用Thread的sleep方法会抛出异常,可以使用try/catch/finally把获取锁和释放锁的过程包裹起来Shop shop = null;try {// 4.1 获取互斥锁boolean isLock = tryLock(LOCK_SHOP_KEY + id);// 4.2 判断是否获取锁成功(插入key是否成功)if(!isLock){//4.3 获取锁失败(插入key失败),则休眠一段时间重新查询商铺缓存(递归)Thread.sleep(50);return queryWithMutex(id);}//4.4 获取锁成功(插入key成功),则根据店铺的Id查询数据库shop = getById(id);// 由于本地查询数据库较快,这里可以模拟重建延时触发并发冲突Thread.sleep(200);// 5.在数据库中查不到对应的店铺则将空字符串写入Redis同时设置有效期if(shop == null){stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id,"",CACHE_NULL_TTL,TimeUnit.MINUTES);return null;}//6.在数据库中查到了店铺信息即shop不为null,将shop对象转化为json字符串写入redis并设置TTLstringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id,JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL,TimeUnit.MINUTES);}catch (Exception e){throw new RuntimeException(e);}finally {//7.不管前面是否会有异常,最终都必须释放锁unlock(lockKey);}// 最终把查询到的商户信息返回给前端return shop;
}

测试互斥锁解决缓存击穿

使用Jmeter模拟缓存击穿情景,在某时刻一个热点店铺的缓存的TTL到期了,此时用户不能从Redis中获取热点店铺的缓存数据,然后就都得去数据库里查询店铺信息

  • 首先将Redis中的热点店铺的缓存数据删除模拟TTL到期,然后使用Jmete开100个线程来访问这个没有缓存的店铺信息

  • 如果后台日志只输出了一条SQL语句则说明我们的互斥锁是生效的,没有造成大量用户都去数据库执行SQL语句查询店铺的信息

PLAINTEXT
: ==>  Preparing: SELECT id,name,type_id,images,area,address,x,y,avg_price,sold,comments,score,open_hours,create_time,update_time FROM tb_shop WHERE id=?
: ==> Parameters: 2(Long)
: <==      Total: 1

在这里插入图片描述

逻辑过期(缓存预热)

缓存击穿问题主要原因是由于我们对key设置了过期时间,假设我们不设置过期时间其实就不会有缓存击穿的问题,但是不设置过期时间,缓存数据又会一直占用内存

  • 优点: 通过异步线程构建缓存,避免其他线程出现等待,提高了性能
  • 缺点: 构建异步线程业务复杂,需要维护一个expire字段需要额外内存消耗, 在异步线程构建完缓存之前,其他线程返回的都是过期的数据(脏数据)导致数据不一致

在这里插入图片描述

逻辑过期应用

实现根据店铺Id查询商铺的业务,基于逻辑过期方式(需要提前添加热点key)来解决缓存击穿问题

在这里插入图片描述

第一步: 因为现在redis中存储的数据的value需要带上过期时间属性,可以新建一个实体类包含原有的数据和过期时间字段(不侵入原来代码)

@Data
public class RedisData {// 过期时间private LocalDateTime expireTime// 原有数据(用万能的Object) private Object data;
}

第二步: 在ShopServiceImpl中新增一个方法,利用单元测试进行缓存预热即添加热点key,将热点店铺信息和过期时间字段封装到RedisData对象中并写入Redis缓存中

public void saveShop2Redis(Long id, Long expirSeconds) {// 1.根据店铺Id去数据库中查询店铺数据Shop shop = getById(id);// 由于本地查询数据库较快,模拟重建延时Thread.sleep(200);// 2.封装逻辑过期时间(当前时间转换为秒)RedisData redisData = new RedisData();// 设置热点店铺信息redisData.setData(shop);redisData.setExpireTime(LocalDateTime.now().plusSeconds(expirSeconds));// 3.将包含热点的店铺信息和逻辑过期时间字段的RedisData对象转化为JSON字符串缓存到RedisstringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));
}

第三步: 在测试类中运行测试方法,然后去Redis图形化页面查看存入的value(含有data字段即shop对象和expireTime逻辑过期时间字段)

@SpringBootTest
class HmDianPingApplicationTests {@Autowiredprivate ShopServiceImpl shopService;@Testpublic void test(){shopService.saveShop2Redis(1L,1000L);}
}
{"data": {"area": "大关","openHours": "10:00-22:00","sold": 4215,"images": "https://qcloud.dpfile.com/pc/jiclIsCKmOI2arxKN1Uf0Hx3PucIJH8q0QSz-Z8llzcN56-IdNpm8K8sG4.jpg","address": "金华路锦昌文华苑29号","comments": 3035,"avgPrice": 80,"updateTime": 1666502007000,"score": 37,"createTime": 1640167839000,"name": "476茶餐厅","x": 120.149192,"y": 30.316078,"typeId": 1,"id": 1},"expireTime": 1666519036559
}

第四步: 编写queryWithLogicalExpire方法,在该方法中如果查到店铺信息返回shop查不到则返回null,最后在queryById方法中做统一判断并返回结果类

在这里插入图片描述

//声明一个线程池,因为使用逻辑过期解决缓存击穿的方式需要新建一个线程来完成重构缓存
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);@Override
public Result queryById(Long id) {       // 测试使用逻辑过期的方式解决缓存击穿Shop shop = queryWithLogicalExpire(id);// 如果shop等于null,表示数据库中对应店铺不存在或者缓存的店铺信息是空字符串if (shop == null) {return Result.fail("店铺不存在!!");}// shop不等于null,把查询到的商户信息返回给前端return Result.ok(shop);
}public Shop queryWithLogicalExpire(Long id) {//1.先从Redis中查询对应的热点店铺缓存信息(包含过期时间),这里的常量值是固定的店铺前缀+查询店铺的IdString json = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);//2.如果未命中即json等于null或命中了但json等于空字符串直接返回null(说明我们没有导入对应的key)//""和null以及"/t/n(换行)"都会判定为空即返回falseif (StrUtil.isBlank(json)) {return null;}//3.如果在Redis中查询到了热点店铺信息并且不是空字符串,则将JSON字符串转化为RedisData对象RedisData redisData = JSONUtil.toBean(json, RedisData.class);//4.redisData.getData()的本质类型是JSONObject类型(还是JSON字符串)并不是Object类型对象,所以不能直接强转为Shop类型,需要使用工具类Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);//5.获取RedisData对象中封装的过期时间,判断是否过期LocalDateTime expireTime = redisData.getExpireTime();if(expireTime.isAfter(LocalDateTime.now())) {// 5.1.未过期,直接返回店铺信息 return shop;}// 6.已过期,需要缓存重建,查询数据库对应的店铺信息然后写入Redis同时设置逻辑过期时间// 6.1.获取互斥锁boolean isLock = tryLock(LOCK_SHOP_KEY + id);// 6.2.判断是否获取锁成功if (isLock){// 再次检测Redis缓存是否过期(双重检查),如果存在则无需重建缓存// 如果Redis中缓存的店铺信息还是过期,开启独立线程,实现缓存重建(测试的时候可以休眠200ms),实际中缓存的逻辑过期时间设置为30分钟CACHE_REBUILD_EXECUTOR.submit( ()->{// 开启独立线程try{this.saveShop2Redis(id,20L);}catch (Exception e){throw new RuntimeException(e);}finally {unlock(LOCK_SHOP_KEY + id);}});}// 6.4.返回过期的商铺信息return shop;
}public void saveShop2Redis(Long id, Long expirSeconds) {// 1.根据店铺Id去数据库中查询店铺数据Shop shop = getById(id);// 由于本地查询数据库较快,模拟重建延时Thread.sleep(200);// 2.封装逻辑过期时间(当前时间转换为秒)RedisData redisData = new RedisData();// 设置热点店铺信息redisData.setData(shop);redisData.setExpireTime(LocalDateTime.now().plusSeconds(expirSeconds));// 3.将包含热点的店铺信息和逻辑过期时间字段的RedisData对象转化为JSON字符串缓存到RedisstringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));
}

测试逻辑过期解决缓存击穿

使用Jmeter进行测试所有的线程查不到数据时是否都会执行缓存重建还是返回旧数据,重建数据时如果数据不一致会不会更新Redis中的缓存数据

  • 在测试类HmDianPingApplicationTests中使用saveShop2Redis方法,向Redis中添加一个热点店铺信息的缓存同时设置逻辑过期时间为2秒
  • 在MySQL数据库中手动修改这个热点店铺的信息,2秒后Redis中缓存的热点店铺数据逻辑过期且和MySQL数据库中对应的店铺信息不一致
  • 当用户访问到过期的缓存数据的时候就需要来新开一个线程重构缓存数据,在重构之前只能获得脏数据(修改前的数据),重构完后才能获得新数据(修改后的数据)

开100个去访问逻辑过期数据

在这里插入图片描述

前面的用户只能看到脏数据,后面的用户看到的才是新数据

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/130163.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

小谈设计模式(19)—备忘录模式

小谈设计模式&#xff08;19&#xff09;—备忘录模式 专栏介绍专栏地址专栏介绍 备忘录模式主要角色发起人&#xff08;Originator&#xff09;备忘录&#xff08;Memento&#xff09;管理者&#xff08;Caretaker&#xff09; 应用场景结构实现步骤Java程序实现首先&#xff…

TLR4-IN-C34-C2-COO,一种结合了TLR4抑制剂TLR4-IN-C34的连接器

TLR4-IN-C34-C2-COO是一种结合了TLR4抑制剂TLR4-IN-C34的连接器&#xff0c;在免疫调节中发挥重要作用&#xff0c;它通过抑制TLR4信号通路的传导&#xff0c;从而达到降低炎症反应的目的。TLR4是Toll样受体家族中的一员&#xff0c;它主要识别来自细菌和病毒的保守模式&#x…

vue2踩坑之项目:Swiper轮播图使用

首先安装swiper插件 npm i swiper5 安装出现错误&#xff1a;npm ERR npm ERR! code ERESOLVE npm ERR! ERESOLVE could not resolve npm ERR! npm ERR! While resolving: vue/eslint-config-standard6.1.0 npm ERR! Found: eslint-plugin-vue8.7.1 npm ERR! node_modules/esl…

基于Java+SpringBoot+Vue线上医院挂号系统的设计与实现 前后端分离【Java毕业设计·文档报告·代码讲解·安装调试】

&#x1f34a;作者&#xff1a;计算机编程-吉哥 &#x1f34a;简介&#xff1a;专业从事JavaWeb程序开发&#xff0c;微信小程序开发&#xff0c;定制化项目、 源码、代码讲解、文档撰写、ppt制作。做自己喜欢的事&#xff0c;生活就是快乐的。 &#x1f34a;心愿&#xff1a;点…

C# Windows 窗体控件中的边距和填充

可以将 Margin 属性、Left、Top、Right、Bottom 的每个方面设置为不同的值&#xff0c;也可以使用 All 属性将它们全部设置为相同的值。 在代码中设置Margin&#xff0c;元素的左边设置为5个单位、上边设置为10个单位、右边设置为15个单位和下边设置为20个单位。 TextBox myT…

HTTP长连接实现原理

1. HTTP长连接和短连接的定义 HTTP长连接 浏览器向服务器进行一次HTTP会话访问后&#xff0c;并不会直接关闭这个连接&#xff0c;而是会默认保持一段时间&#xff0c;那么下一次浏览器继续访问的时候就会再次利用到这个连接。在HTTP/1.1版本中&#xff0c;默认的连接都是长连…

vue-7-vuex

一、Vuex 概述 目标&#xff1a;明确Vuex是什么&#xff0c;应用场景以及优势 1.是什么 Vuex 是一个 Vue 的 状态管理工具&#xff0c;状态就是数据。 大白话&#xff1a;Vuex 是一个插件&#xff0c;可以帮我们管理 Vue 通用的数据 (多组件共享的数据)。例如&#xff1a;购…

Jmeter 链接MySQL测试

1.环境部署 1.1官网下载MySQL Connector https://dev.mysql.com/downloads/connector/j/ 1.2 解压后&#xff0c;将jar放到jmeter/lib目录下 1.3 在测试计划中添加引用 2.脚本设置 2.1设置JDBC Connection Configuration 先添加一个setUp线程中&#xff0c;在setUp中添加“…

MongoDB 笔记

1 insert 、create、save区别 insert: 主键不存在则正常插入&#xff1b;主键已存在&#xff0c;抛出DuplicateKeyException 异常 save: 主键不存在则正常插入&#xff1b;主键已存在则更新 insertMany&#xff1a;批量插入&#xff0c;等同于批量执行 insert create&#x…

黑马点评-05缓存穿透问题及其解决方案,缓存空字符串或使用布隆过滤器

缓存穿透问题(缓存空) 缓存穿透的解决方案 缓存穿透(数据穿透缓存直击数据库): 缓存穿透是指客户端请求访问缓存中和数据库中都不存在的数据,此时缓存永远不会生效并且用户的请求都会打到数据库 数据库能够承载的并发不如Redis这么高&#xff0c;如果大量的请求同时访问这种…

vue使用localstorage超出限制解决方法

最近在项目中&#xff0c;遇到一个报错&#xff0c;QuotaExceededError: The quota has been exceeded。如图&#xff1a; 搜索了一下&#xff0c;结合项目代码&#xff0c;得到的结论是localStorage超出5M限制了&#xff0c;项目中使用了vuex-persistedstate插件&#xff0c;…

Redis-双写一致性

双写一致性 双写一致性解决方案延迟双删&#xff08;有脏数据的风险&#xff09;分布式锁&#xff08;强一致性&#xff0c;性能比较低&#xff09;异步通知&#xff08;保证数据的最终一致性&#xff0c;高并发情况下会出现短暂的不一致情况&#xff09; 双写一致性 当修改了数…