压力测试考察当前软硬件环境下系统所能承受的最大负荷并帮助找出系统瓶颈所在。 压测都是为了系统在线上的处理能力和稳定性维持在一个标准范围内, 做到心中有数。
使用压力测试, 我们有希望找到很多种用其他测试方法更难发现的错误。 有两种错误类型是:内存泄漏, 并发与同步。
有效的压力测试系统将应用以下这些关键条件:重复, 并发, 量级, 随机变化。
响应时间(Response Time:RT)
响应时间指用户从客户端发起一个请求开始,到客户端接收到从服务器端返回的响应结束,整个过程所耗费的时间。
HPS(Hits Per Second):每秒点击次数,单位是次/秒。【不是特别重要】
TPS(Transaction per Second):系统每秒处理交易数,单位是笔/秒。
Qps(Query per Second):系统每秒处理查询次数,单位是次/秒。
对于互联网业务中,如果某些业务有且仅有一个请求连接,那么TPS=QPS=HPS,一般情况下用 TPS来衡量整个业务流程,用QPS来衡量接口查询次数,用HPS来表示对服务器单击请求。
无论TPS、QPS、HPS,此指标是衡量系统处理能力非常重要的指标,越大越好,根据经
验,一般情况下:
金融行业:1000TPS~5000OTPS,不包括互联网化的活动
保险行业:100TPS~10000OTPS,不包括互联网化的活动
制造行业:10TPS~5000TPS
互联网电子商务:1000OTPS~1000000TPS
互联网中型网站:1000TPS~50000TPS
互联网小型网站:50OTPS~10000TPS
**最大响应时间(MaxResponse Time)**指用户发出请求或者指令到系统做出反应(响应)的最大时间。
**最少响应时间(Mininum ResponseTime)**指用户发出请求或者指令到系统做出反应(响应)的最少时间。
**90%响应时间(90%Response Time)**是指所有用户的响应时间进行排序,第90%的响应时间。
从外部看,性能测试主要关注如下三个指标
吞吐量:每秒钟系统能够处理的请求数、任务数。
响应时间:服务处理一个请求或一个任务的耗时。
错误率。一批请求中结果出错的请求所占比例。
JMeter下载地址
运行jmeter.bat
线程组参数详解:
3.聚合报告
结果分析 :
有错误率同开发确认, 确定是否允许错误的发生或者错误率允许在多大的范围内;
Throughput 吞吐量每秒请求的数大于并发数, 则可以慢慢的往上面增加; 若在压测的机器性能很好的情况下, 出现吞吐量小于并发数, 说明并发数不能再增加了, 可以慢慢的往下减, 找到最佳的并发数;
压测结束, 登陆相应的 web 服务器查看 CPU 等性能指标, 进行数据的分析;
最大的 tps, 不断的增加并发数, 加到 tps 达到一定值开始出现下降, 那么那个值就是
最大的 tps。
最大的并发数: 最大的并发数和最大的 tps 是不同的概率, 一般不断增加并发数, 达到一个值后, 服务器出现请求超时, 则可认为该值为最大的并发数。
压测过程出现性能瓶颈, 若压力机任务管理器查看到的 cpu、 网络和 cpu 都正常, 未达 到 90%以上, 则可以说明服务器有问题, 压力机没有问题。
影响性能考虑点包括:数据库、 应用程序、 中间件(tomact、 Nginx) 、 网络和操作系统等方面
首先考虑自己的应用属于 CPU 密集型还是 IO 密集型
出现原因
windows 本身提供的端口访问机制的问题。
Windows 提供给 TCP/IP 链接的端口为 1024-5000, 并且要四分钟来循环回收他们。 就导致我们在短时间内跑大量的请求时将端口占满了。
解决思路
扩大提供给 TCP/IP 链接的端口
缩短循环回收时间
计算机\HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters
详细模型
所有的对象实例以及数组都要在堆上分配。 堆是垃圾收集器管理的主要区域, 也被称为“GC堆” ; 也是我们优化最多考虑的地方。
堆可以细分为:
新生代
Eden 空间
From Survivor 空间
To Survivor 空间
老年代
永久代/元空间
Java8 以前永久代, 受 jvm 管理, java8 以后元空间, 直接使用物理内存。 因此,默认情况下, 元空间的大小仅受本地内存限制。
详细流程
创建对象放到堆内存的流程:
1、放到eden区,如果放不下执行MinorGC
2、再判断是否放得下不,放判断老年代是否放得下,放不下执行 FullGC,【FullGC会先触发MinorGC】
3、如果老年代放不下OOM
旧对象:
1、放到幸存者区survivor,如果放得下放在to区【然后from 和 to转变身份】【超过阈值15放到老年代】
2、如果survivor放不下判断老年代是否放得下,放不下执行FullGC
3、如果老年代放不下OOM异常
Jdk 的两个小工具 jconsole、 jvisualvm(升级版的 jconsole) ;通过命令行启动, 可监控本地和远程应用。 远程应用需要配置
进入jconsole页面选择需要查看的进程
监控内存泄露, 跟踪垃圾回收, 执行时内存、 cpu 分析, 线程分析…
原因:
- 可能是因为更新链接配置的版本不对
- 自己有代理的话一定要关闭
我的版本是281xxx
找到对应的版本复制链接
#动态查看doker各个容器的状态
docker stats
初始状态
50个线程压测后状态
得出结论nginx是CPU 密集型
压测内容 | 压测线程数 | 吞吐量/s | 90%响应时间 | 99%响应时间 |
---|---|---|---|---|
Nginx | 50 | 2335 | 11 | 944 |
压测内容 | 压测线程数 | 吞吐量/s | 90%响应时间 | 99%响应时间 |
---|---|---|---|---|
Nginx | 50 | 2335 | 11 | 944 |
Gateway | 50 | 10367 | 8 | 31 |
对cpu与内存进行监控
得出结论网关也是cup密集型
对gc进行监控
发现网关的不断在进行轻gc,偶尔执行重gc
虽然轻gc的次数远远大于重gc的次数,但是所用时间并没有多多少
得出结论:
可以适当调大内存区的大小,避免gc次数太多而造成性能的下降
gulimall-product/src/main/java/site/zhourui/gulimall/product/web/IndexController.java
//压测简单服务(无任何业务逻辑):
@ResponseBody
@GetMapping("/hello")
public String hello() {
return "hello";
}
压测内容 | 压测线程数 | 吞吐量/s | 90%响应时间 | 99%响应时间 |
---|---|---|---|---|
Nginx | 50 | 2335 | 11 | 944 |
Gateway | 50 | 10367 | 8 | 31 |
简单服务 | 50 | 11341 | 8 | 17 |
压测内容 | 压测线程数 | 吞吐量/s | 90%响应时间 | 99%响应时间 |
---|---|---|---|---|
Nginx | 50 | 2335 | 11 | 944 |
Gateway | 50 | 10367 | 8 | 31 |
简单服务 | 50 | 11341 | 8 | 17 |
首页一级菜单渲染 | 50 | 270(db,thymeleaf) | 267 | 365 |
thymeleaf开启缓存后吞吐量有一定的提升
压测内容 | 压测线程数 | 吞吐量/s | 90%响应时间 | 99%响应时间 |
---|---|---|---|---|
Nginx | 50 | 2335 | 11 | 944 |
Gateway | 50 | 10367 | 8 | 31 |
简单服务 | 50 | 11341 | 8 | 17 |
首页一级菜单渲染 | 50 | 270(db,thymeleaf) | 267 | 365 |
首页渲染(开缓存) | 50 | 290 | 251 | 365 |
开启缓存
thymeleaf:
cache: true
对pms_category表的parent_cid
加上索引
logging:
level:
site.zhourui.gulimall: error
数据库的优化(加索引)对性能的提升还是挺大的
压测内容 | 压测线程数 | 吞吐量/s | 90%响应时间 | 99%响应时间 |
---|---|---|---|---|
Nginx | 50 | 2335 | 11 | 944 |
Gateway | 50 | 10367 | 8 | 31 |
简单服务 | 50 | 11341 | 8 | 17 |
首页一级菜单渲染 | 50 | 270(db,thymeleaf) | 267 | 365 |
首页渲染(开缓存) | 50 | 290 | 251 | 365 |
首页渲染(开缓存、 优化数据库、 关日 志) | 50 | 700 | 105 | 183 |
主要是数据库导致的吞吐量降低,太慢了
localhost:10001/index/catalog.json
压测内容 | 压测线程数 | 吞吐量/s | 90%响应时间 | 99%响应时间 |
---|---|---|---|---|
Nginx | 50 | 2335 | 11 | 944 |
Gateway | 50 | 10367 | 8 | 31 |
简单服务 | 50 | 11341 | 8 | 17 |
首页一级菜单渲染 | 50 | 270(db,thymeleaf) | 267 | 365 |
首页渲染(开缓存) | 50 | 290 | 251 | 365 |
首页渲染(开缓存、 优化数据库、 关日 志) | 50 | 700 | 105 | 183 |
三级分类数据获取 | 50 | 2(db) | … | … |
对pms_category表的parent_cid
加上索引
吞吐量有一定的提升
压测内容 | 压测线程数 | 吞吐量/s | 90%响应时间 | 99%响应时间 |
---|---|---|---|---|
Nginx | 50 | 2335 | 11 | 944 |
Gateway | 50 | 10367 | 8 | 31 |
简单服务 | 50 | 11341 | 8 | 17 |
首页一级菜单渲染 | 50 | 270(db,thymeleaf) | 267 | 365 |
首页渲染(开缓存) | 50 | 290 | 251 | 365 |
首页渲染(开缓存、 优化数据库、 关日 志) | 50 | 700 | 105 | 183 |
三级分类数据获取 | 50 | 2(db) | … | … |
三级分类数据获取(加索引) | 50 | 8 |
1)、优化业务逻辑:
1、一次性查询出来
2、将下面查库抽取为方法,不是真的查库baseMapper.selectList(new QueryWrapper().eq(“parent_cid”, level1.getCatId()));抽取为一个方法
将第一次查询的数据存起来,封装一个方法查询这个数据,就不会重复查询数据库
选中右键:refacto=》extract=》Method
优化业务后吞吐量有了质的飞越,说明业务对性能的影响也挺大的
压测内容 | 压测线程数 | 吞吐量/s | 90%响应时间 | 99%响应时间 |
---|---|---|---|---|
Nginx | 50 | 2335 | 11 | 944 |
Gateway | 50 | 10367 | 8 | 31 |
简单服务 | 50 | 11341 | 8 | 17 |
首页一级菜单渲染 | 50 | 270(db,thymeleaf) | 267 | 365 |
首页渲染(开缓存) | 50 | 290 | 251 | 365 |
首页渲染(开缓存、 优化数据库、 关日 志) | 50 | 700 | 105 | 183 |
三级分类数据获取 | 50 | 2(db) | … | … |
三级分类数据获取(加索引) | 50 | 8 | ||
三级分类( 优化业 务) | 50 | 111 | 571 | 896 |
吞吐量也有比较明显的提升
压测内容 | 压测线程数 | 吞吐量/s | 90%响应时间 | 99%响应时间 |
---|---|---|---|---|
Nginx | 50 | 2335 | 11 | 944 |
Gateway | 50 | 10367 | 8 | 31 |
简单服务 | 50 | 11341 | 8 | 17 |
首页一级菜单渲染 | 50 | 270(db,thymeleaf) | 267 | 365 |
首页渲染(开缓存) | 50 | 290 | 251 | 365 |
首页渲染(开缓存、 优化数据库、 关日 志) | 50 | 700 | 105 | 183 |
三级分类数据获取 | 50 | 2(db) | … | … |
三级分类数据获取(加索引) | 50 | 8 | ||
三级分类( 优化业 务) | 50 | 111 | 571 | 896 |
三 级 分 类 ( 使 用 redis 作为缓存) | 50 | 411 | 153 | 217 |
之前的压测都没有导入静态资源
压测内容 | 压测线程数 | 吞吐量/s | 90%响应时间 | 99%响应时间 |
---|---|---|---|---|
Nginx | 50 | 2335 | 11 | 944 |
Gateway | 50 | 10367 | 8 | 31 |
简单服务 | 50 | 11341 | 8 | 17 |
首页一级菜单渲染 | 50 | 270(db,thymeleaf) | 267 | 365 |
首页渲染(开缓存) | 50 | 290 | 251 | 365 |
首页渲染(开缓存、 优化数据库、 关日 志) | 50 | 700 | 105 | 183 |
三级分类数据获取 | 50 | 2(db) | … | … |
三级分类数据获取(加索引) | 50 | 8 | ||
三级分类( 优化业 务) | 50 | 111 | 571 | 896 |
三 级 分 类 ( 使 用 redis 作为缓存) | 50 | 411 | 153 | 217 |
首页全量数据获取 | 50 | 7(静态资源) |
压测内容 | 压测线程数 | 吞吐量/s | 90%响应时间 | 99%响应时间 |
---|---|---|---|---|
Nginx | 50 | 2335 | 11 | 944 |
Gateway | 50 | 10367 | 8 | 31 |
简单服务 | 50 | 11341 | 8 | 17 |
首页一级菜单渲染 | 50 | 270(db,thymeleaf) | 267 | 365 |
首页渲染(开缓存) | 50 | 290 | 251 | 365 |
首页渲染(开缓存、 优化数据库、 关日 志) | 50 | 700 | 105 | 183 |
三级分类数据获取 | 50 | 2(db) | … | … |
三级分类数据获取(加索引) | 50 | 8 | ||
三级分类( 优化业 务) | 50 | 111 | 571 | 896 |
三 级 分 类 ( 使 用 redis 作为缓存) | 50 | 411 | 153 | 217 |
首页全量数据获取 | 50 | 7(静态资源) | ||
Nginx+Gateway | 50 | |||
Gateway+简单服务 | 50 | 3124 | 30 | 125 |
压测内容 | 压测线程数 | 吞吐量/s | 90%响应时间 | 99%响应时间 |
---|---|---|---|---|
Nginx | 50 | 2335 | 11 | 944 |
Gateway | 50 | 10367 | 8 | 31 |
简单服务 | 50 | 11341 | 8 | 17 |
首页一级菜单渲染 | 50 | 270(db,thymeleaf) | 267 | 365 |
首页渲染(开缓存) | 50 | 290 | 251 | 365 |
首页渲染(开缓存、 优化数据库、 关日 志) | 50 | 700 | 105 | 183 |
三级分类数据获取 | 50 | 2(db) | … | … |
三级分类数据获取(加索引) | 50 | 8 | ||
三级分类( 优化业 务) | 50 | 111 | 571 | 896 |
三 级 分 类 ( 使 用 redis 作为缓存) | 50 | 411 | 153 | 217 |
首页全量数据获取 | 50 | 7(静态资源) | ||
Nginx+Gateway | 50 | |||
Gateway+简单服务 | 50 | 3124 | 30 | 125 |
全链路简单服务 | 50 | 800 | 88 | 310 |
为什么要进行动静分离?
如果是前后端分离的项目,那么就不会出现这个情况
未分离的项目静态资源放在后端,无论是动态请求还是静态请求都会来到后台,这极大的损耗了后台性能(大部分性能都用来处理静态请求)
动静分离后,后台只会处理动态请求,而静态资源直接由nginx返回
mkdir /mydata/nginx/html/static
将gulimall-product的静态资源复制到该目录,并将本地静态资源删除
gulimall-product/src/main/resources/templates/index.html
将原来的index/xxx路径修改为/static/index/xxx
vim /mydata/nginx/conf/conf.d/gulimall.conf
配置如下内容
#监听gulimall.com:80/static,返回root
location /static {
root /usr/share/nginx/html;
}
刷新后静态资源加载成功
线上应用内存崩溃宕机
原因:
服务分配的内存太小,导致新生代,老年代空间都满了,gc 后也没有空间
解决方案:
调大堆内存
-Xmx1024m -Xms1024m -Xmn512m
- -Xms :初始堆大小
- -Xmx :最大堆大小
- -Xmn :堆中新生代初始及最大大小
为了系统性能的提升, 我们一般都会将部分数据放入缓存中, 加速访问。 而 db 承担数据落盘工作。
**注意:**在开发中, 凡是放入缓存中的数据我们都应该指定过期时间, 使其可以在系统即使没有主动更新数据也能自动触发数据加载进缓存的流程。 避免业务崩溃导致的数据永久不一致问题
本地缓存可以用map实现,将需要缓存的数据存入map,查询时先判断是否为空,不为空就直接从map中取值,不用查询数据库,不为空就需要查询数据,并将数据存入map中,下次查询就不用查询数据库
本地缓存在分布式下的问题
- 集群下的本地缓存不共享,存在于jvm中【并且负载均衡到新的机器后会重新查询】
- 数据一致性:如果一台机器修改了数据库+缓存,但是集群下其他机器的缓存未修改所以分布式情况下不使用本地缓存
使用redis作为缓存中间件
redis内存不足时可以进行集群+分片操作
如:redis:
集群+分片【110000,1000120000】
gulimall-product/pom.xml
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
配置redis属性,host,port
spring:
redis:
host: 192.168.157.128
port: 6379
测试
gulimall-product/src/test/java/site/zhourui/gulimall/product/GulimallProductApplicationTests.java
@Autowired
StringRedisTemplate redisTemplate;
@Test
public void testRedis() {
ValueOperations<String, String> ops = redisTemplate.opsForValue();
// 保存
ops.set("hello", "world_" + UUID.randomUUID().toString());
// 查询
String hello = ops.get("hello");
System.out.println(hello);
}
Autowired
StringRedisTemplate redisTemplate;
@Override
public Map<String, List<Catalog2Vo>> getCatalogJson() {
//1.加入缓存逻辑
String catelogJSON = redisTemplate.opsForValue().get("catelogJSON");
if (StringUtils.isEmpty(catelogJSON)){
//2.缓存中没有,查询数据库
Map<String, List<Catalog2Vo>> catalogJsonFromDb = getCatalogJsonFromDb();
//3.查到的数据再放入缓存,将对象转为json放在缓存中
String s = JSON.toJSONString(catalogJsonFromDb);
redisTemplate.opsForValue().set("catelogJSON",s);
}
Map<String, List<Catalog2Vo>> result = JSON.parseObject(catelogJSON, new TypeReference<Map<String, List<Catalog2Vo>>>() {
});
return result;
}
//从数据库查询并封装分类的数据
public Map<String, List<Catalog2Vo>> getCatalogJsonFromDb() {
// 一次性获取所有 数据
List<CategoryEntity> selectList = baseMapper.selectList(null);
System.out.println("调用了 getCatalogJson 查询了数据库........【三级分类】");
// 1)、所有1级分类
List<CategoryEntity> level1Categorys = getParent_cid(selectList, 0L);
// 2)、封装数据
Map<String, List<Catalog2Vo>> collect = level1Categorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), level1 -> {
// 查到当前1级分类的2级分类
List<CategoryEntity> category2level = getParent_cid(selectList, level1.getCatId());
List<Catalog2Vo> catalog2Vos = null;
if (category2level != null) {
catalog2Vos = category2level.stream().map(level12 -> {
// 查询当前2级分类的3级分类
List<CategoryEntity> category3level = getParent_cid(selectList, level12.getCatId());
List<Catalog2Vo.Catalog3Vo> catalog3Vos = null;
if (category3level != null) {
catalog3Vos = category3level.stream().map(level13 -> {
return new Catalog2Vo.Catalog3Vo(level12.getCatId().toString(), level13.getCatId().toString(), level13.getName());
}).collect(Collectors.toList());
}
return new Catalog2Vo(level1.getCatId().toString(), catalog3Vos, level12.getCatId().toString(), level12.getName());
}).collect(Collectors.toList());
}
return catalog2Vos;
}));
return collect;
}
原因
- springboot2.0以后默认使用lettuce作为操作redis的客户端。他使用netty进行网络通信
- lettuce的bug导致netty堆外内存溢出 -Xmx1024m;netty如果没有指定堆外内存,默认使用-Xmx1024m,跟jvm设置的一样【迟早会出异常】
解决方案
- 可以通过-Dio.netty.maxDirectMemory进行设置【仍然会异常】
- 解决方案:不能使用-Dio.netty.maxDirectMemory
- 升级lettuce客户端;【2.3.2已解决】【lettuce使用netty吞吐量很大】
- 切换使用jedis客户端【这里学习一下如何使用jedis,但是最后不选用】
剔除lettuce-core,使用jedis
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
<exclusions>
<exclusion>
<groupId>io.lettucegroupId>
<artifactId>lettuce-coreartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>redis.clientsgroupId>
<artifactId>jedisartifactId>
dependency>
0、Lettuce和Jedis是redis的客户端,RedisTemplate是对Lettuce和Jedis的再一层封装
1、RedisAutoConfiguration自动配置类,会导入Lettuce和Jedis的配置类
2、JedisConfiguration.java类会给容器放一个@Bean::JedisConnectionFactory
缓存穿透:指查询一个一定不存在的数据,由于缓存是不命中,将去查询数据库,但是数据库也无此记录,我们没有将这次查询的null写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义
风险:利用不存在的数据进行攻击,数据库瞬时压力增大,最终导致崩溃 缓存
解决:null结果缓存,并加入短暂过期时间
缓存雪崩:缓存雪崩是指在我们设置缓存时key采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重雪崩。
解决:原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体
失效的事件。
如果已经出现的情况:解决方法一:熔断、降级
一条数据过期了,高并发情况下导致所有请求到达DB
解决:加分布式锁,获取到锁,先查缓存,其他人就有数据,不用去DB
缓存击穿:
解决:
加锁
大量并发只让一个去查,其他人等待,查到以后释放锁,其他人获取到锁,先查缓存,就会有数据,不用去db
//从数据库查询并封装分类的数据
public Map<String, List<Catalog2Vo>> getCatalogJsonFromDb() {
//只要是同一把锁,就能锁住需要这个锁的所有线程
//synchronized (this):SpringBoot所有组件在容器中都是单例的
//TODO 本地锁:synchronized,JUC(Lock)
synchronized (this) {
//得到锁后,我们应该再去缓存确定一次,如果没有才需要继续查询
String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");
if (!StringUtils.isEmpty(catalogJSON)) {
Map<String, List<Catalog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catalog2Vo>>>() {
});
return result;
}
System.out.println("查询了数据库.....");
// 一次性获取所有 数据
List<CategoryEntity> selectList = baseMapper.selectList(null);
System.out.println("调用了 getCatalogJson 查询了数据库........【三级分类】");
// 1)、所有1级分类
List<CategoryEntity> level1Categorys = getParent_cid(selectList, 0L);
// 2)、封装数据
Map<String, List<Catalog2Vo>> collect = level1Categorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), level1 -> {
// 查到当前1级分类的2级分类
List<CategoryEntity> category2level = getParent_cid(selectList, level1.getCatId());
List<Catalog2Vo> catalog2Vos = null;
if (category2level != null) {
catalog2Vos = category2level.stream().map(level12 -> {
// 查询当前2级分类的3级分类
List<CategoryEntity> category3level = getParent_cid(selectList, level12.getCatId());
List<Catalog2Vo.Catalog3Vo> catalog3Vos = null;
if (category3level != null) {
catalog3Vos = category3level.stream().map(level13 -> {
return new Catalog2Vo.Catalog3Vo(level12.getCatId().toString(), level13.getCatId().toString(), level13.getName());
}).collect(Collectors.toList());
}
return new Catalog2Vo(level1.getCatId().toString(), catalog3Vos, level12.getCatId().toString(), level12.getName());
}).collect(Collectors.toList());
}
return catalog2Vos;
}));
return collect;
}
}
3.2.3.1的加锁没有完全锁住线程
原因:下图是3.2.3.1业务逻辑,在压测中当线程一加锁后执行查询数据库后锁就被释放了,在将结果放入redis缓存这个过程中下一个进程又拿到了锁,因此会打印两次查询了数据库.....
解决办法:
把存入缓存的操作放在锁中
优化代码:
// TODO 产生堆外内存溢出:OutOfDirectMemoryError
// 1)springboot2.0以后默认使用lettuce作为操作redis的客户端。他使用netty进行网络通信
// 2)lettuce的bug导致netty堆外内存溢出 -Xmx1024m;netty如果没有指定堆外内存,默认使用-Xmx1024m,跟jvm设置的一样【迟早会出异常】
// 可以通过-Dio.netty.maxDirectMemory进行设置【仍然会异常】
// 解决方案:不能使用-Dio.netty.maxDirectMemory
// 1)升级lettuce客户端;【2.3.2已解决】【lettuce使用netty吞吐量很大】
// 2)切换使用jedis客户端【这里学习一下如何使用jedis,但是最后不选用】
@Override
public Map<String, List<Catalog2Vo>> getCatalogJson() {
//1.加入缓存逻辑
String catelogJSON = redisTemplate.opsForValue().get("catelogJSON");
if (StringUtils.isEmpty(catelogJSON)){
//2.缓存中没有,查询数据库
System.out.println("缓存未命中.....查询数据库");
Map<String, List<Catalog2Vo>> catalogJsonFromDb = getCatalogJsonFromDb();
//3.查到的数据再放入缓存,将对象转为json放在缓存中
}
System.out.println("缓存命中.....直接返回");
Map<String, List<Catalog2Vo>> result = JSON.parseObject(catelogJSON, new TypeReference<Map<String, List<Catalog2Vo>>>() {
});
return result;
}
//从数据库查询并封装分类的数据
public Map<String, List<Catalog2Vo>> getCatalogJsonFromDb() {
//只要是同一把锁,就能锁住需要这个锁的所有线程
//synchronized (this):SpringBoot所有组件在容器中都是单例的
//TODO 本地锁:synchronized,JUC(Lock)
synchronized (this) {
//得到锁后,我们应该再去缓存确定一次,如果没有才需要继续查询
String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");
if (!StringUtils.isEmpty(catalogJSON)) {
Map<String, List<Catalog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catalog2Vo>>>() {
});
return result;
}
System.out.println("查询了数据库.....");
// 一次性获取所有 数据
List<CategoryEntity> selectList = baseMapper.selectList(null);
System.out.println("调用了 getCatalogJson 查询了数据库........【三级分类】");
// 1)、所有1级分类
List<CategoryEntity> level1Categorys = getParent_cid(selectList, 0L);
// 2)、封装数据
Map<String, List<Catalog2Vo>> collect = level1Categorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), level1 -> {
// 查到当前1级分类的2级分类
List<CategoryEntity> category2level = getParent_cid(selectList, level1.getCatId());
List<Catalog2Vo> catalog2Vos = null;
if (category2level != null) {
catalog2Vos = category2level.stream().map(level12 -> {
// 查询当前2级分类的3级分类
List<CategoryEntity> category3level = getParent_cid(selectList, level12.getCatId());
List<Catalog2Vo.Catalog3Vo> catalog3Vos = null;
if (category3level != null) {
catalog3Vos = category3level.stream().map(level13 -> {
return new Catalog2Vo.Catalog3Vo(level12.getCatId().toString(), level13.getCatId().toString(), level13.getName());
}).collect(Collectors.toList());
}
return new Catalog2Vo(level1.getCatId().toString(), catalog3Vos, level12.getCatId().toString(), level12.getName());
}).collect(Collectors.toList());
}
return catalog2Vos;
}));
// redisTemplate.opsForValue().set("catelogJSON",s);
String s = JSON.toJSONString(collect);
redisTemplate.opsForValue().set("catelogJSON",s,1, TimeUnit.DAYS);
return collect;
}
}
再次测试:
只有一次了
本地锁只能锁住当前进程
测试:新增几个容器
压测
10001商品服务
10002商品服务
10003商品服务
每个服务都对数据库进行了一次查询操作,得出结论本地锁只能锁本地服务
分布式锁最重要的是要保证占锁与删锁的原子性
set nx
测试使用redis
set nx
命令实现占锁
redis中文官方占锁
set nx
测试用xshell新增几个虚拟机会话
打开撰写栏
进入redis-cli docker容器
docker exec -it redis redis-cli
测试set nx命令
set lock test NX
测试结果只有会话三返回ok,
nx实现了原子加锁
测试代码 抽取getDataFromDb
问题:setnx占好了位,业务代码异常或者程序在页面过程中宕机。没有执行删除锁逻辑,这就造成了死锁
解决:设置锁的自动过期,即使没有删除,会自动删除 阶段二解决
//分布式锁
public Map<String, List<Catalog2Vo>> getCatalogJsonFromDbWithRedisLock() {
//1.占分布式锁,去redis占坑
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "111");
if (lock){
//加锁成功...执行业务
Map<String, List<Catalog2Vo>> dataFromDb = getDataFromDb();
//删除锁
redisTemplate.delete("lock");
return dataFromDb;
}else {
//加锁失败...重试
//休眠100ms重试
return getCatalogJsonFromDbWithRedisLock();//自旋的方式
}
}
private Map<String, List<Catalog2Vo>> getDataFromDb() {
// 一次性获取所有 数据
List<CategoryEntity> selectList = baseMapper.selectList(null);
// 1)、所有1级分类
List<CategoryEntity> level1Categorys = getParent_cid(selectList, 0L);
// 2)、封装数据
Map<String, List<Catalog2Vo>> collect = level1Categorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), level1 -> {
// 查到当前1级分类的2级分类
List<CategoryEntity> category2level = getParent_cid(selectList, level1.getCatId());
List<Catalog2Vo> catalog2Vos = null;
if (category2level != null) {
catalog2Vos = category2level.stream().map(level12 -> {
// 查询当前2级分类的3级分类
List<CategoryEntity> category3level = getParent_cid(selectList, level12.getCatId());
List<Catalog2Vo.Catalog3Vo> catalog3Vos = null;
if (category3level != null) {
catalog3Vos = category3level.stream().map(level13 -> {
return new Catalog2Vo.Catalog3Vo(level12.getCatId().toString(), level13.getCatId().toString(), level13.getName());
}).collect(Collectors.toList());
}
return new Catalog2Vo(level1.getCatId().toString(), catalog3Vos, level12.getCatId().toString(), level12.getName());
}).collect(Collectors.toList());
}
return catalog2Vos;
}));
// redisTemplate.opsForValue().set("catelogJSON",s);
String s = JSON.toJSONString(collect);
redisTemplate.opsForValue().set("catelogJSON",s,1, TimeUnit.DAYS);
return collect;
}
设置过期时间遇到的问题:
redisTemplate.expire("lock",30,TimeUnit.MINUTES);
setnx设置好,正要去设置过期时间,宕机。又死锁了。
解决:
设置过期时间和占位必须是原子的。redis支持使用setnx ex命令 阶段三解决
//分布式锁
public Map<String, List<Catalog2Vo>> getCatalogJsonFromDbWithRedisLock() {
//1.占分布式锁,去redis占坑
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "111");
if (lock){
//设置过期时间 发生断电
redisTemplate.expire("lock",30,TimeUnit.MINUTES);
//加锁成功...执行业务
Map<String, List<Catalog2Vo>> dataFromDb = getDataFromDb();
//删除锁
redisTemplate.delete("lock");
return dataFromDb;
}else {
//加锁失败...重试
//休眠100ms重试
return getCatalogJsonFromDbWithRedisLock();//自旋的方式
}
}
redis加锁并设置过期时间(原子性)
redis操作
set lock test EX 300 NX
redisTemplate操作
redisTemplate.opsForValue().setIfAbsent("lock", "111",300,TimeUnit.SECONDS);
问题:
删除锁直接删除???
如果由于业务时间很长,锁自己过期了,我们直接删除,有可能把别人正在持有的锁删除了。解决:
占锁的时候,值指定为uuid,每个人匹配是自己的锁才删除。 阶段四解决
//分布式锁
public Map<String, List<Catalog2Vo>> getCatalogJsonFromDbWithRedisLock() {
//1.占分布式锁,去redis占坑
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "111",300,TimeUnit.SECONDS);
if (lock){
//设置过期时间 发生断电
// redisTemplate.expire("lock",30,TimeUnit.MINUTES);
//加锁成功...执行业务
Map<String, List<Catalog2Vo>> dataFromDb = getDataFromDb();
//删除锁
redisTemplate.delete("lock");
return dataFromDb;
}else {
//加锁失败...重试
//休眠100ms重试
return getCatalogJsonFromDbWithRedisLock();//自旋的方式
}
}
问题:
如果正好判断是当前值,正要删除锁的时候,锁已经过期,别人已经设置到了新的值。那么我们删除的是别人的锁解决:
删除锁必须保证原子性。使用redis+Lua脚本完成 阶段五完成lua脚本
if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end
//分布式锁
public Map<String, List<Catalog2Vo>> getCatalogJsonFromDbWithRedisLock() {
String uuid = UUID.randomUUID().toString();
//1.占分布式锁,去redis占坑
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "111",300,TimeUnit.SECONDS);
if (lock){
//设置过期时间 发生断电
// redisTemplate.expire("lock",30,TimeUnit.MINUTES);
//加锁成功...执行业务
Map<String, List<Catalog2Vo>> dataFromDb = getDataFromDb();
//删除锁
// redisTemplate.delete("lock");
String lockValue = redisTemplate.opsForValue().get("lock");
if (uuid.equals(lockValue)){
redisTemplate.delete("lock");
}
return dataFromDb;
}else {
//加锁失败...重试
//休眠100ms重试
return getCatalogJsonFromDbWithRedisLock();//自旋的方式
}
}
lua脚本解锁保证了解锁的原子性
保证加锁【占位+过期时间】和删除锁【判断+删除】的原子性。更难的事情,锁的自动续期
完整版代码
问题:业务执行时间比较长,可以延长过期时间
//分布式锁
public Map<String, List<Catalog2Vo>> getCatalogJsonFromDbWithRedisLock() {
// 1、占本分布式锁。去redis占坑,同时设置过期时间
String uuid = UUID.randomUUID().toString();
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 300, TimeUnit.SECONDS);
if (lock) {
// 加锁成功....执行业务【内部会判断一次redis是否有值】
System.out.println("获取分布式锁成功....");
Map<String, List<Catalog2Vo>> dataFromDB = null;
try {
dataFromDB = getDataFromDb();
} finally {
// 2、查询UUID是否是自己,是自己的lock就删除
// 查询+删除 必须是原子操作:lua脚本解锁
String luaScript = "if redis.call('get',KEYS[1]) == ARGV[1]\n" +
"then\n" +
" return redis.call('del',KEYS[1])\n" +
"else\n" +
" return 0\n" +
"end";
// 删除锁
Long lock1 = redisTemplate.execute(new DefaultRedisScript<Long>(luaScript, Long.class),
Arrays.asList("lock"), uuid);
}
return dataFromDB;
} else {
System.out.println("获取分布式锁失败....等待重试...");
// 加锁失败....重试
// 休眠100ms重试
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
return getCatalogJsonFromDbWithRedisLock();// 自旋的方式
}
}
Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务。其中包括(BitSet
, Set
, Multimap
, SortedSet
, Map
, List
, Queue
, BlockingQueue
, Deque
, BlockingDeque
, Semaphore
, Lock
, AtomicLong
, CountDownLatch
, Publish / Subscribe
, Bloom filter
, Remote service
, Spring cache
, Executor service
, Live Object service
, Scheduler service
) Redisson提供了使用Redis的最简单和最便捷的方法。Redisson的宗旨是促进使用者对Redis的关注分离(Separation of Concern),从而让使用者能够将精力更集中地放在处理业务逻辑上。
本文我们仅关注分布式锁的实现,更多请参考Redisson官方文档
<dependency>
<groupId>org.redissongroupId>
<artifactId>redissonartifactId>
<version>3.13.4version>
dependency>
@Configuration
public class RedissonConfig {
@Bean
public RedissonClient redissonClient(){
Config config = new Config();
config.useSingleServer().setAddress("redis://192.168.56.102:6379");
RedissonClient redisson = Redisson.create(config);
return redisson;
}
}
@Autowired
RedissonClient redissonClient;
@Test
public void name() {
System.out.println(redissonClient);
}
成功拿到redissonClient对象
可重入锁: 当a业务包含b业务时,并且a业务与b业务都需要抢占统一资源,当a业务执行到b业务时,b业务发现该资源已上锁,如果是可重入锁b业务就可拿到锁,执行业务;反之如果此时b业务拿不到资源,就是不可重入锁,这样程序就会死锁.
如果负责储存这个分布式锁的Redisson节点宕机以后,而且这个锁正好处于锁住的状态时,这个锁会出现锁死的状态。为了避免这种情况的发生,所以就设置了过期时间,但是如果业务执行时间过长,业务还未执行完锁就已经过期,那么就会出现解锁时解了其他线程的锁的情况。
所以Redisson内部提供了一个监控锁的看门狗,它的作用是在Redisson实例被关闭前,不断的延长锁的有效期。默认情况下,看门狗的检查锁的超时时间是30秒钟,也可以通过修改Config.lockWatchdogTimeout来另行指定。
在本次测试中CatalogJson-Lock
的初始过期时间TTL为30s,但是每到20s(经过三分之一看门狗时间后)就会自动续借成30s
@ResponseBody
@GetMapping("/hello")
public String hello() {
//1.获取一把锁,只要锁的名字一样,就是同一把锁
RLock lock = redissonClient.getLock("my-lock");
//加锁
lock.lock();//阻塞式等待,默认加的锁等待时间为30s
//1.锁的自动续期,如果在业务执行期间业务没有执行完成,redisson会为该锁自动续期
//2.加锁的业务只要运行完成,就不会自动续期,即使不手动解锁,锁在默认的30s后会自动删除
try {
System.out.println("加锁成功,执行业务......"+Thread.currentThread().getId());
Thread.sleep(30000);
}catch (Exception e){
}finally {
//解锁,假设代码没有运行,redisson不会出现死锁
System.out.println("锁释放..."+Thread.currentThread().getId());
lock.unlock();
}
return "hello";
}
加锁成功
redisson分布式锁的自动续期
//在自定义锁的存在时间时不会自动解锁
lock.lock(10, TimeUnit.SECONDS);
lock()方法的两大特点:
1、会有一个看门狗机制,在我们业务运行期间,将我们的锁自动续期
2、为了防止死锁,加的锁默认是30秒的过期时间,即使由于我们的业务宕机,没有手动调用解锁代码,30s后redis也会对他自动解锁
@GetMapping("/read")
@ResponseBody
public String read() {
RReadWriteLock lock = redissonClient.getReadWriteLock("ReadWrite-Lock");
RLock rLock = lock.readLock();
String s = "";
try {
rLock.lock();
System.out.println("读锁加锁"+Thread.currentThread().getId());
Thread.sleep(5000);
s= redisTemplate.opsForValue().get("lock-value");
}finally {
rLock.unlock();
return "读取完成:"+s;
}
}
@GetMapping("/write")
@ResponseBody
public String write() {
RReadWriteLock lock = redissonClient.getReadWriteLock("ReadWrite-Lock");
RLock wLock = lock.writeLock();
String s = UUID.randomUUID().toString();
try {
wLock.lock();
System.out.println("写锁加锁"+Thread.currentThread().getId());
Thread.sleep(10000);
redisTemplate.opsForValue().set("lock-value",s);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
wLock.unlock();
return "写入完成:"+s;
}
}
写锁会阻塞读锁,但是读锁不会阻塞读锁,但读锁会阻塞写锁
总之含有写的过程都会被阻塞,只有读读不会被阻塞
上锁时在redis的状态
信号量为存储在redis中的一个数字,当这个数字大于0时,即可以调用acquire()
方法增加数量,也可以调用release()
方法减少数量,但是当调用release()
之后小于0的话方法就会阻塞,直到数字大于0
@GetMapping("/park")
@ResponseBody
public String park() {
RSemaphore park = redissonClient.getSemaphore("park");
try {
park.acquire(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "停进2";
}
@GetMapping("/go")
@ResponseBody
public String go() {
RSemaphore park = redissonClient.getSemaphore("park");
park.release(2);
return "开走2";
}
可以理解为门栓,使用若干个门栓将当前方法阻塞,只有当全部门栓都被放开时,当前方法才能继续执行。
以下代码只有offLatch()
被调用5次后 setLatch()
才能继续执行
@GetMapping("/setLatch")
@ResponseBody
public String setLatch() {
RCountDownLatch latch = redissonClient.getCountDownLatch("CountDownLatch");
try {
latch.trySetCount(5);
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
return "门栓被放开";
}
@GetMapping("/offLatch")
@ResponseBody
public String offLatch() {
RCountDownLatch latch = redissonClient.getCountDownLatch("CountDownLatch");
latch.countDown();
return "门栓被放开1";
}
个人理解
他们都是标志位为0时解锁
但是信号量的标志位可以加,但是闭锁不能,闭锁是能减,直到标志位为0解锁
双写模式:在数据库进行写操作的同时对缓存也进行写操作,确保缓存数据与数据库数据的一致性
问题: 两个线程同时进行写操作时由于缓存是存储在redis,写缓存时需要发送网络请求,导致虽然线程一先发送写缓存的网络请求但是比线程二发送的写缓存的网络请求后到达redis,造成数据被覆盖
是否满足最终一致性:满足,原因 缓存过期以后,又能得到最新的正确数据读到的最新数据有延迟:最终一致性
失效模式:在数据库进行更新操作时,删除原来的缓存,再次查询数据库就可以更新最新数据
存在问题
当两个请求同时修改数据库,一个请求已经更新成功并删除缓存时又有读数据的请求进来,这时候发现缓存中无数据就去数据库中查询并放入缓存,在放入缓存前第二个更新数据库的请求成功,这时候留在缓存中的数据依然是第一次数据更新的数据
解决方法
1、缓存的所有数据都有过期时间,数据过期下一次查询触发主动更新
2、读写数据的时候(并且写的不频繁),加上分布式的读写锁。
SpringCache官方文档
Cache | 缓存接口,定义缓存操作.实现有:RedisCache,RhCacheCache,ConcurrentMapCache等 |
CacheManager | 缓存管理器,管理各种缓存组件 |
@Cacheable | 主要针对方法配置,能够根据方法的请求参数对其结果进行缓存 |
@CacheEvict | 清空缓存 |
@CachePut | 保证方法被调用,又希望结果被缓存 |
@Caching | 组合上面三个注解多个操作 |
@EnableCaching | 开启基于注解的缓存 |
@CacheConfig | 在类级别分享缓存的相同配置 |
keyGenerator | 缓存数据是key生成策略 |
serialize | 缓存数据是value序列化策略 |
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-cacheartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
spring:
cache:
type: redis
开启缓存功能 @EnableCaching
使用缓存注解
注解 | 作用 |
---|---|
@Cacheable | 主要针对方法配置,能够根据方法的请求参数对其结果进行缓存 |
@CacheEvict | 清空缓存 |
@CachePut | 保证方法被调用,又希望结果被缓存 |
@Caching | 组合上面三个注解多个操作 |
@EnableCaching | 开启基于注解的缓存 |
@CacheConfig | 在类级别分享缓存的相同配置 |
@Cacheable:标注方法上:当前方法的结果存入缓存,如果缓存中有,方法不调用
@CacheEvict:触发将数据从缓存删除的操作
@CachePut:不影响方法执行更新缓存
@Caching:组合以上多个操作
@CacheConfig:在类级别共享缓存的相同配置
getLevel1Categorys方法加上@Cacheable(“category”)注解
/**
* 查询一级分类。
* 父ID是0, 或者 层级是1
*/
@Cacheable("category")
@Override
public List<CategoryEntity> getLevel1Categorys() {
System.out.println("调用了 getLevel1Categorys 查询了数据库........【一级分类】");
return baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", 0));
}
测试结果
指定一个名字,放入哪个分区@Cacheable({“category”})
1)当前方法的结果需要缓存,如果缓存中有,方法不被调用
2)默认缓存数据的key: category::SimpleKey []
3)默认使用jdk序列化机制,将序列化后的数据存到redis
4)默认过期时间-1,永不过期
/**
* 查询一级分类。
* 父ID是0, 或者 层级是1
*/
@Cacheable(value = "category",key = "#root.method.name")
@Override
public List<CategoryEntity> getLevel1Categorys() {
System.out.println("调用了 getLevel1Categorys 查询了数据库........【一级分类】");
return baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", 0));
}
spring:
cache:
type: redis
redis:
time-to-live: 3600000
重启测试
ttl设置为我们自定义值
缓存的key值指定为方法名
1)指定key,可接收SpEL表达式 @Cacheable(value = {“category”}, key = “‘level1Categorys’”)
SpEL表达式可以参照官网 Avaliable Caching SpEL Evaluation Context
使用方法名用key:key = “#root.method.name”
2)指定时间 cache.redis.time-to-live=3600s
3)将数据保存为json格式:
4)前缀CACHE_,如果未指定,则使用缓存名字作为前缀:category
package site.zhourui.gulimall.product.config;
import org.springframework.boot.autoconfigure.cache.CacheProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* @author zr
* @date 2021/11/14 16:57
*/
@EnableConfigurationProperties(CacheProperties.class)
@EnableCaching
@Configuration
public class MyCacheConfig {
// @Autowired
// CacheProperties cacheProperties;
/**
* 需要将配置文件中的配置设置上
* 1、使配置类生效
* 1)开启配置类与属性绑定功能EnableConfigurationProperties
*
* @ConfigurationProperties(prefix = "spring.cache") public class CacheProperties
* 2)注入就可以使用了
* @Autowired CacheProperties cacheProperties;
* 3)直接在方法参数上加入属性参数redisCacheConfiguration(CacheProperties redisProperties)
* 自动从IOC容器中找
*
* 2、给config设置上
*/
@Bean
RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
config = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()));
//指定缓存序列化方式为json
config = config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
// 配置文件生效:RedisCacheConfiguration
CacheProperties.Redis redisProperties = cacheProperties.getRedis();
//设置配置文件中的各项配置,如过期时间,如果此处以下的代码没有配置,配置文件中的配置不会生效
if (redisProperties.getTimeToLive() != null) {
config = config.entryTtl(redisProperties.getTimeToLive());
}
if (redisProperties.getKeyPrefix() != null) {
config = config.prefixKeysWith(redisProperties.getKeyPrefix());
}
if (!redisProperties.isCacheNullValues()) {
config = config.disableCachingNullValues();
}
if (!redisProperties.isUseKeyPrefix()) {
config = config.disableKeyPrefix();
}
return config;
}
}
spring:
cache:
type: redis
redis:
time-to-live: 3600000
#设置key的前缀,一般情况下不要自定统一前缀,方便分区处理
# key-prefix: _CACHE
#key是否使用前缀
use-key-prefix: true
#是否允许空值 # 防止缓存穿透,可缓存null值
cache-null-values: true
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7kpaohud-1636947687908)(http://zr.zhourui.site/img/Snipaste_2020-09-10_19-40-20 (2)].png)
/**
* 级联更新所有关联的数据
* @param category
*/
@Transactional
@CacheEvict(value = {"category"},key ="'getLevel1Categorys'")//删除指定key值的缓存
@Override
public void updateCascade(CategoryEntity category) {
this.updateById(category);
categoryBrandRelationService.updateCategory(category.getCatId(),category.getName());
}
升级用法:
删除带category前缀的所有缓存
allEntries = true
/**
* 级联更新所有关联的数据
* @param category
*/
@Transactional
@CacheEvict(value = {"category"},allEntries = true) //调用该方法(updateCascade)会删除缓存category下的所有cache
@Override
public void updateCascade(CategoryEntity category) {
this.updateById(category);
categoryBrandRelationService.updateCategory(category.getCatId(),category.getName());
}
测试
随意修改一个类目名,修改成功后发现缓存也被删除了
3.2.6.4.7 @Caching 的使用
作用:在数据修改时需要对多个缓存进行操作时使用
@Transactional
// @CacheEvict(value = {"category"},key ="'getLevel1Categorys'")//删除指定key值的缓存
@Caching(evict = {
@CacheEvict(value = {"category"},key ="'getLevel1Categorys'"),
@CacheEvict(value = {"category"},key ="'getCatalogJson'")
})
@Override
public void updateCascade(CategoryEntity category) {
this.updateById(category);
categoryBrandRelationService.updateCategory(category.getCatId(),category.getName());
}
@Cacheable:标注方法上:当前方法的结果存入缓存,如果缓存中有,方法不调用
@CacheEvict:触发将数据从缓存删除的操作【删除缓存】【可实现失效模式】
@CachePut:不影响方法执行更新缓存【更新缓存】【可实现双写模式】
@Caching:组合以上多个操作【实现双写+失效模式】
@CacheConfig:在类级别共享缓存的相同配置
1、读模式:
缓存穿透:查询一个DB不存在的数据。解决:缓存空数据;ache-null-values=true【布隆过滤器】
缓存击穿:大量并发进来同时查询一个正好过期的数据。解决:加锁; 默认未加锁【sync = true】本地锁
@Cacheable(value = "category",key = "#root.method.name",sync = true)
缓存雪崩:大量的key同时过期。解决:加上过期时间。: spring.cache.redis.time-to-live= 360000s
2、写模式:(缓存与数据库一致)(没有解决)
1)、读写加锁。
2)、引入canal,感知mysql的更新去更新缓存
3)、读多写多,直接去查询数据库就行
总结:
常规数据(读多写少,即时性,一致性要求不高的数据)﹔完全可以使用Spring-Cache,写模式(只要缓存的数据有过期时间就可以)
特殊数据:特殊设计