day03-商家查询缓存02
功能02-商铺查询缓存02
知识补充
(1)缓存穿透
缓存穿透(cache penetration)是指用户访问的数据既不在缓存当中,也不在数据库中。出于容错的考虑,如果从底层数据库查询不到数据,则不写入缓存。这就导致每次请求都会到底层数据库进行查询,缓存也失去了意义。当高并发或有人利用不存在的Key频繁攻击时,数据库的压力骤增,甚至崩溃,这就是缓存穿透问题。
简单地说,缓存穿透是指用户请求的数据在缓存和数据库中都不存在,则每次请求都会打到数据库中,给数据库带来巨大压力。
常见的两种解决方案
(1)缓存空对象:是指在持久层没有命中的情况下,对key进行set (key,null)。
缓存空对象会有两个问题:
value为null 不代表不占用内存空间,空值做了缓存,意味着缓存层中存了更多的键,需要更多的内存空间,比较有效的方法是针对这类数据设置一个较短的过期时间,让其自动剔除。
缓存层和存储层的数据会有一段时间窗口的不一致,可能会对业务有一定影响。例如过期时间设置为5分钟,如果此时存储层添加了这个数据,那此段时间就会出现缓存层和存储层数据的不一致,此时可以利用消息系统或者其他方式清除掉缓存层中的空对象。
(2)布隆过滤器:
在访问缓存层和存储层之前,将存在的key用布隆过滤器提前保存起来,做第一层拦截,当收到一个对key请求时,先用布隆过滤器验证是key否存在,如果存在再进入缓存层、存储层。
可以使用bitmap做布隆过滤器。这种方法适用于数据命中不高、数据相对固定、实时性低的应用场景,代码维护较为复杂,但是缓存空间占用少。
布隆过滤器实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。
布隆过滤器拦截的算法描述:
初始状态时,BloomFilter是一个长度为m的位数组,每一位都置为0。添加元素x时,x使用k个hash函数得到k个hash值,对m取余,对应的bit位设置为1。
判断y是否属于这个集合,对y使用k个哈希函数得到k个哈希值,对m取余,所有对应的位置都是1,则认为y属于该集合(哈希冲突,可能存在误判),否则就认为y不属于该集合。可以通过增加哈希函数和增加二进制位数组的长度来降低错报率
两种方案的比较:
缓存穿透的方案 | 使用场景 | 维护成本 |
---|---|---|
缓存空对象 | 1.数据命中率不高 2.数据频繁变化实时性高 | 1.代码维护简单 2.需要过多的缓存空间 3.数据不一致 |
布隆过滤器 | 1.数据命中不高 2.数据相对固定实时性低 | 1.代码维护复杂 2.缓存空间占用少 |
缓存穿透的解决方案还有:
(2)缓存雪崩
缓存雪崩
在使用缓存时,通常会对缓存设置过期时间,一方面目的是保持缓存与数据库数据的一致性,另一方面是减少冷缓存占用过多的内存空间。但当缓存中大量热点缓存采用了相同的实效时间,就会导致缓存在某一个时刻同时实效,请求全部转发到数据库,从而导致数据库压力骤增,甚至宕机。从而形成一系列的连锁反应,造成系统崩溃等情况,这就是缓存雪崩(Cache Avalanche)。
简单地说,缓存雪崩是指在同一时间段大量的热点key同时失效,或者Redis服务宕机,导致大量请求到达数据库,给数据库带来巨大压力。
解决方案
- 给不同的key的TTL添加随机值(比如随机1-5分钟),让key均匀地失效
- 利用redis集群提高服务的可用性(提高高可用性)
- 给缓存业务添加熔断、降级、限流策略
- 给业务添加多级缓存
(3)缓存击穿
缓存击穿
如果有一个热点key,在不停的扛着大并发,在这个key失效的瞬间,持续的大并发请求就会击破缓存,直接请求到数据库,好像蛮力击穿一样。这种情况就是缓存击穿(Cache Breakdown)。
缓存击穿问题也叫做热点key问题,简单来说,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问在瞬间给数据库带来巨大的冲击。
从定义上可以看出,缓存击穿和缓存雪崩很类似,只不过是缓存击穿是一个热点key失效,而缓存雪崩是大量热点key失效。因此,可以将缓存击穿看作是缓存雪崩的一个子集。
解决方案
方案一:使用互斥锁(Mutex Key),只让一个线程构建缓存,其他线程等待构建缓存执行完毕,重新从缓存中获取数据。单机通过synchronized或lock来处理,分布式环境采用分布式锁。
方案二:逻辑过期。热点数据不设置过期时间,只在value中设置逻辑上的过期时间。后台异步更新缓存,适用于不严格要求缓存一致性的场景。
两种方案的对比:
3.功能02-商铺查询缓存
3.4查询商铺id的缓存穿透问题
3.4.3需求分析
解决查询商铺查询可能存在的缓存穿透问题:当访问不存在的店铺时,请求会直接打到数据库上,并且redis缓存永远不会生效。
这里使用缓存空对象的方式来解决。
3.4.4代码实现
(1)修改ShopServiceImpl.java的queryById方法
@Override
public Result queryById(Long id) {
String key = CACHE_SHOP_KEY + id;
//1.从redis中查询商铺缓存
String shopJson = stringRedisTemplate.opsForValue().get(key);
//2.判断缓存是否命中
if (StrUtil.isNotBlank(shopJson)) {
//2.1若命中,直接返回商铺信息
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return Result.ok(shop);
}
//判断命中的是否是redis的空值
if (shopJson != null) {
return Result.fail("店铺不存在!");
}
//2.2未命中,根据id查询数据库,判断商铺是否存在数据库中
Shop shop = getById(id);
if (shop == null) {
//2.2.1不存在,防止缓存穿透,将空值存入redis,TTL设置为2min
stringRedisTemplate.opsForValue().set(key, "",
CACHE_NULL_TTL, TimeUnit.MINUTES);
//返回错误信息
return Result.fail("店铺不存在!");
}
//2.2.2存在,则将商铺数据写入redis中
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop),
CACHE_SHOP_TTL, TimeUnit.MINUTES);
return Result.ok(shop);
}
(2)测试,访问一个缓存和数据库都不存在的数据:
可以看到redis已经缓存了一个空值
之后再访问该数据,只要redis的空值对没有过期,就不会访问到数据库,从而起到保护数据库的作用。
3.5查询商铺id的缓存击穿问题
当查询店铺id时,可能会出现该店铺id对应的缓存失效,从而大量请求发送到数据库的情况,这里使用两种方案分别解决该问题。
3.5.1基于互斥锁方案解决
3.5.1.1需求分析
修改根据id查询商铺的业务,基于互斥锁方式来解决缓存击穿问题。
如下,当出现缓存击穿问题,首先需要判断当前的线程是否能够获取锁:
- 若可以,则进行缓存重建(将数据库数据重新写入缓存中),然后释放锁。
- 如果不能,则线程等待一段时间,然后再判断缓存是否能命中。
- 如果未命中,则重复获取锁的流程,直到缓存命中,或者获得锁,重建缓存。
根据redis的setnx命令,当setnx设置某个key之后,如果该key存在,则其他线程无法设置该key。
我们可以根据这个特性,作为一个lock的逻辑标志,当一个线程setnx某个key后,代表获取了“锁”。当删除这个key时,代表释放“锁”,这样其他线程就可以重新获取“锁”。此外,可以对该key设置一个有效期,防止删除key失败,产生“死锁”。
3.5.1.2代码实现
(1)修改 ShopServiceImpl.java
package com.hmdp.service.impl;
import ...
/**
* 服务实现类
*
* @author 李
* @version 1.0
*/
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop>
implements IShopService {
@Resource
StringRedisTemplate stringRedisTemplate;
@Override
public Result queryById(Long id) {
Shop shop = queryWithMutex(id);
if (shop == null) {
return Result.fail("店铺不存在!");
}
return Result.ok(shop);
}
//缓存穿透(存储空对象)+缓存击穿解决(互斥锁解决)
public Shop queryWithMutex(Long id) {
String key = CACHE_SHOP_KEY + id;
//从redis中查询商铺缓存
String shopJson = stringRedisTemplate.opsForValue().get(key);
//判断缓存是否命中
if (StrUtil.isNotBlank(shopJson)) {
//命中,直接返回商铺信息
return JSONUtil.toBean(shopJson, Shop.class);
}
//判断命中的是否是redis的空值(缓存击穿解决)
if (shopJson != null) {
return null;
}
//未命中,尝试获取互斥锁
String lockKey = "lock:shop:" + id;
boolean isLock = false;
Shop shop = null;
try {
//获取互斥锁
isLock = tryLock(lockKey);
//判断是否获取成功
if (!isLock) {//失败
//等待并重试
Thread.sleep(50);
//直到缓存命中,或者获取到锁
return queryWithMutex(id);
}
//获取锁成功,开始重建缓存
//根据id查询数据库,判断商铺是否存在数据库中
shop = getById(id);
//模拟重建缓存的延迟-----------
Thread.sleep(200);
if (shop == null) {
//不存在,防止缓存穿透,将空值存入redis,TTL设置为2min
stringRedisTemplate.opsForValue().set(key, "",
CACHE_NULL_TTL, TimeUnit.MINUTES);
//返回错误信息
return null;
}
//存在,则将商铺数据写入redis中
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop),
CACHE_SHOP_TTL, TimeUnit.MINUTES);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
//释放互斥锁
unLock(lockKey);
}
//返回从缓存或数据库中查到的数据
return shop;
}
//缓存穿透方案
// public Shop queryWithPassThrough(Long id) {
// String key = CACHE_SHOP_KEY + id;
// //1.从redis中查询商铺缓存
// String shopJson = stringRedisTemplate.opsForValue().get(key);
// //2.判断缓存是否命中
// if (StrUtil.isNotBlank(shopJson)) {
// //2.1若命中,直接返回商铺信息
// return JSONUtil.toBean(shopJson, Shop.class);
// }
// //判断命中的是否是redis的空值
// if (shopJson != null) {
// return null;
// }
// //2.2未命中,根据id查询数据库,判断商铺是否存在数据库中
// Shop shop = getById(id);
// if (shop == null) {
// //2.2.1不存在,防止缓存穿透,将空值存入redis,TTL设置为2min
// stringRedisTemplate.opsForValue().set(key, "",
// CACHE_NULL_TTL, TimeUnit.MINUTES);
// //返回错误信息
// return null;
// }
// //2.2.2存在,则将商铺数据写入redis中
// stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop),
// CACHE_SHOP_TTL, TimeUnit.MINUTES);
// return shop;
// }
private boolean tryLock(String key) {
Boolean flag = stringRedisTemplate.opsForValue()
.setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
return BooleanUtil.isTrue(flag);
}
private void unLock(String key) {
stringRedisTemplate.delete(key);
}
@Override
@Transactional
public Result update(Shop shop) {
Long id = shop.getId();
if (id == null) {
return Result.fail("店铺id不能为空");
}
//1.更新数据库
updateById(shop);
//2.删除redis缓存
stringRedisTemplate.delete(CACHE_SHOP_KEY + id);
return Result.ok();
}
}
(2)使用jemeter模拟高并发的情况:
5秒发起1000个请求线程:
模拟http请求:
全部请求成功,获取到数据:
在服务器的控制台中可以看到:对于数据库的请求只触发了一次,证明在高并发的场景下,只有一个线程对数据库发起请求,并对redis对应的缓存重新设置。
3.5.2基于逻辑过期方案解决
day03-商家查询缓存02的更多相关文章
- ECMall关于数据查询缓存的问题
刚接触Ecmall的二次开发不久,接到一个任务.很常见的任务,主要是对数据库进行一些操作,其中查询的方法我写成这样: 01 function get_order_data($goods_id) 02 ...
- mysql中利用show profile很直观的看到查询缓存的作用。
1.首先,开启mysql的查询缓存. 查看查询缓存情况: MariaDB [test]> show variables like '%query_cache%';+--------------- ...
- ADO.NET EF 4.2 中的查询缓存(避免查询缓存)
在WinForm系统中遇到了个问题,Form1是查询窗口,根据条件查询出所有数据,双击列表后创建弹出Form2窗口编辑单个记录,但编辑后保存后,在Form2中查询到的还是旧的数据,实际数据库中已经更新 ...
- 生产要不要开启MySQL查询缓存
一.前言 在当今的各种系统中,缓存是对系统性能优化的重要手段.MySQL Query Cache(MySQL查询缓存)在MySQL Server中是默认打开的,但是网上各种资料以及有经验的DBA都建议 ...
- hibernate笔记--缓存机制之 二级缓存(sessionFactory)和查询缓存
二级缓存(sessionFactory): Hibernate的二级缓存由SessionFactory对象管理,是应用级别的缓存.它可以缓存整个应用的持久化对象,所以又称为“SessionFactor ...
- mybatis入门基础(八)-----查询缓存
一.什么是查询缓存 mybatis提供查询缓存,用于减轻数据压力,提高数据库性能. mybaits提供一级缓存,和二级缓存. 1.1. 一级缓存是sqlSession级别的缓存.在操作数据库时需要构造 ...
- mybatis中的查询缓存
一: 查询缓存 Mybatis提供查询缓存,用于减轻数据压力,提高数据库压力. Mybatis提供一级缓存和二级缓存. 在操作数据库时需要构造SqlSession对象,在对象中有一个数据结构(Hash ...
- 11g新特性-查询缓存(1)
众所周知,访问内存比访问硬盘快得多,除非硬盘体系发生革命性的改变.可以说缓存在Oracle里面无处不在,结果集缓存(Result Cache)是Oracle Database 11g新引入的功能,引入 ...
- 【Mybatis框架】查询缓存(一级缓存)
做Java的各位程序员们,估计SSH和SSM是我们的基础必备框架.也就是说我们都已经至少接触过了这两套常见的集成框架.当我们用SSH的时候,相信很多人都接触过hibernate的两级缓存,同样,相对应 ...
- MySQL查询缓存
MySQL查询缓存 用于保存MySQL查询语句返回的完整结果,被命中时,MySQL会立即返回结果,省去解析.优化和执行等阶段. 如何检查缓存? MySQL保存结果于缓存中: 把SELECT语句本身做h ...
随机推荐
- IEEE会议论文投稿系统问题之一:WinEdt编译生成DVI文件的方法
问题描述:如何将tex文件正确编译为dvi文件,以正常使用IEEE投稿系统? 方法: 1.打开WinEdt中的命令行: 2.使用latex TexFileName编译tex生成aux文件和dvi文件: ...
- STM32F103使用FSMC对接正点原子3.5寸TFTLCD屏幕
fsmc的使用算是32里面有点绕的一个知识点,但是想明白了其实也没啥了. 首先我先放32个0在这儿: 0000 0000 0000 0000 0000 0000 0000 0000 [3 ...
- 七、CSS网格
构造一个5*5的网格,如下图所示,同一颜色表示同个区域,黑线表示间隔5px 1.普通方式建立网格 <!DOCTYPE html> <html> <body> < ...
- Angular架构学习
定义 Angular 是一个用 HTML 和 JavaScript 或者一个可以编译成 JavaScript 的语言(例如 Dart 或者 TypeScript ),来构建客户端应用的框架. 写 An ...
- k8s ingress
ingress ingress为k8s集群中的服务提供了入口,可以提供复制均衡,ssl终止和基于名称的虚拟主机,再生产环境中,常用的ingress有Treafik,Nginx,HAProxy,Is ...
- 【其他】etcd
配置 node1 name: etcd-1 data-dir: /data/etcd/node1 listen-client-urls: http://127.0.0.1:6701 advertise ...
- 使用react-vite-antd,修改antd主题,报错 [vite] Internal server error: Inline JavaScript is not enabled. Is it set in your options? It is hacky way to make this function will be compiled preferentially by less
一般报错 在官方文档中,没有关于vite中如何使用自定义主题的相关配置,经过查阅 1.安装less yarn add less (已经安装了就不必再安装) 2.首先将App.css改成App.les ...
- Redis内存淘汰策略
目录 一.内存淘汰策略重要性 二.Key值过期策略 三.内存淘汰策略 三.Redis内存淘汰策略配置 一.内存淘汰策略重要性 我们都知道redis的性能很高,最主要的原因之一就是redis的数据都在内 ...
- 什么是Markdown
什么是markdown? Markdown是一种轻量级标记语言,它允许人们使用已读一些的纯文本格式编写文档,然后转换成有效的XHTML(或者HTML)文档.这种语言吸收了很多在电子邮件中已有的纯文本标 ...
- 说来惭愧,关于Session的某块知识的学习,感觉涨了知识
Session的查漏补缺 今天在写界面的时候,想要利用servlet和jsp页面实现界面的跳转,之前实现这些内容的时候,我是没有用到session来实现这个功能的. 直到今天,想要将第一个界面的数据隔 ...