第141集 压力测试概念
压力测试考察当前软硬件环境下系统所能承受的最大负荷并帮助找出系统瓶颈所在。压测都是为了系统在线上的处理能力和稳定性维持在一个标准范围内,做到心中有数。
使用压力测试,我们有希望找到很多种用其他测试方法更难发现的错误。有两种错误类型是:内存泄漏,并发与同步。
有效的压力测试系统将应用以下这些关键条件:重复,并发,量级,随机变化。
相关专业概念:
响应时间(ResponseTime:RT)响应时间指用户从客户端发起一个请求开始,到客户端接收到从服务器端返回的响应结束,整个过程所耗费的时间。
HPSS(Hits Per Second):每秒点击次数,单位是次/秒。
TPS(Transaction per Second):系统每秒处理交易数,单位是笔/秒。
OPS(Query per Second) 系统每秒处理查询次数,单位是次/秒。对于互联网业务中,如果某些业务有且仅有一个请求连接,那么TPS=QPS=HPS,一般情况下用TPS来衡量整个业务流程,用QPS来衡量接口查询次数,用HPS来表示对服务器单击请求。
无论TPS、QPS、HPS,此指标是衡量系统处理能力非常重要的指标,越大越好,根据经验,一般情况下:
金融行业:1000TPS~50000TPS,不包括互联网化的活动保险行业:100TPS~100000TPS,
不包括互联网化的活动制造行业:10TPS~5000TPS
互联网电子商务:10000TPS~1000000TPS
互联网中型网站:1000TPS~50000TPS
互联网小型网站:500TPS~10000TPS
从外部看,性能测试主要关注如下三个指标
吞吐量:每秒钟系统能够处理的请求数、任务数。
响应时间:服务处理一个请求或一个任务的耗时。
错误率:一批请求中结果出错的请求所占比例。
第142集JMeter下载
可以参考这个博主:
jmeter下载安装配置(超细)_jmeter5.4.3对应什么版本的jdk-CSDN博客
运行jmeter
运行jmeter里面的bin文件下的jmeter.bat文件,可成功打开jmeter即可;
第143集 JMeter Address Already in use 错误解决
这个问题我没遇到,所以直接过
第144集 堆内存和垃圾回收
测压分析
有错误率同开发确认,确定是否允许错误的发生或者错误率允许在多大的范围内;
Throughput 吞吐量每秒请求的数大于并发数,则可以慢慢的往上面增加;若在压测的机
器性能很好的情况下,出现吞吐量小于并发数,说明并发数不能再增加了,可以慢慢的
往下减,找到最佳的并发数;
压测结束,登陆相应的 web 服务器查看 CPU 等性能指标,进行数据的分析;
最大的 tps,不断的增加并发数,加到 tps 达到一定值开始出现下降,那么那个值就是
最大的 tps。
最大的并发数:最大的并发数和最大的 tps 是不同的概率,一般不断增加并发数,达到
一个值后,服务器出现请求超时,则可认为该值为最大的并发数。
压测过程出现性能瓶颈,若压力机任务管理器查看到的 cpu、网络和 cpu 都正常,未达
到 90%以上,则可以说明服务器有问题,压力机没有问题。
影响性能考虑点包括:
数据库、应用程序、中间件(tomact、Nginx)、网络和操作系统等方面
首先考虑自己的应用属于 CPU 密集型还是 IO 密集型
JVM相关内容学习
程序计数器 Program Counter Register:
记录的是正在执行的虚拟机字节码指令的地址,
此内存区域是唯一一个在JAVA虚拟机规范中没有规定任何OutOfMemoryError的区
域
虚拟机:VM Stack
描述的是 JAVA 方法执行的内存模型,每个方法在执行的时候都会创建一个栈帧,
用于存储局部变量表,操作数栈,动态链接,方法接口等信息
局部变量表存储了编译期可知的各种基本数据类型、对象引用
线程请求的栈深度不够会报 StackOverflowError 异常
栈动态扩展的容量不够会报 OutOfMemoryError 异常
虚拟机栈是线程隔离的,即每个线程都有自己独立的虚拟机栈
本地方法:Native Stack
本地方法栈类似于虚拟机栈,只不过本地方法栈使用的是本地方法
堆:Heap
几乎所有的对象实例都在堆上分配内存
内存调优主要就是调堆
堆
由于full gc很慢,所以我们在优化的时候避免full gc.younggc会把那些没用对象踢出来,然后剩下的转入到幸存者区。如果younggc的对象超过15次还存在,就将其放到老年代。如果对象过大,则可以直接放老年代。
第145集 性能监控 jconsole
命令行运行 jconsole,jdk自带的内容。
jvisualvm(主要用)
下载可以参考这个博主
jvisualvm保姆级教程-CSDN博客
安装gc插件,成功
第146集 中间件对性能的影响
测试中间件
这里使用的就是jmeter挨个对接口和中间件发送循环请求,检测吞吐率等信息。参考老师的结果,知道了中间件越多,性能损失越大。损失在传输上。
第147集 吞吐量测试
这一集也是测接口吞吐率,和上次操作手法一致。
之后就是有一些优化手段了,包括对数据库的优化,前端开缓存(这里主要项目的原因正式工作,前端不会用themleaf返回页面的,太影响性能了),关闭日志等手段。
第148集 nginx动静分离
首先在html包下放static文件夹
修改 index.html 的src上都添加上static,我是直接用老师的文件进行替换的。
之后添加一段到nginx.conf文件里 这的含义就是,将/static 转化为 html,因为我们的路径为
http://gulimall.com/static/index/img/5a13bf0bNe1606e58.jpg ,把/static 转化为 html就为
http://gulimall.com/html/index/img/5a13bf0bNe1606e58.jpg这个然后这个jpg文件就是放在html包下的index的img的下面,这样就匹配好了。
#charset
location /static{
root html;
}
第149集 模拟内存满导致的宕机问题
就是发大量请求三级分类数据接口。导致老年代持续饱满
第150集 优化上面的问题
原本的代码如下,就是在循环中将每一个一级分类id都去找对应分类的二级分类。
我们的优化就是既然三级分类就这么多,直接查出所有三级分类,然后本地缓存起来,然后用到再查即可。这个给我们一个启示就是,不要在循环之中,查数据库,除非迫不得已。
//1、查出所有分类,就是本地缓存的意思。
List<CategoryEntity> selectList = this.baseMapper.selectList(null);
//1、1)查出所有一级分类
List<CategoryEntity> level1Categorys = getParent_cid(selectList, 0L);
//封装数据
Map<String, List<Catelog2Vo>> parentCid = level1Categorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
//1、每一个的一级分类,查到这个一级分类的二级分类
List<CategoryEntity> categoryEntities = getParent_cid(selectList, v.getCatId());
private List<CategoryEntity> getParent_cid(List<CategoryEntity> selectList,Long parentCid) {
List<CategoryEntity> categoryEntities = selectList.stream().filter(item -> item.getParentCid().equals(parentCid)).collect(Collectors.toList());
return categoryEntities;
// return this.baseMapper.selectList(
// new QueryWrapper<CategoryEntity>().eq("parent_cid", parentCid));
}
第151集 本地缓存和分布式锁
第152集,第153集 整合redis测试
老样子 pom ,yml,测试成功
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
redis:
host: 127.0.0.1
port: 6379
@Test
public void teststringRedis(){
ValueOperations<String, String> stringStringValueOperations = stringRedisTemplate.opsForValue();
stringStringValueOperations.set("hello","world");
}
第153集 改造三级分类业务
逻辑就是如果有缓存就查缓存,如果没有就查数据库
@Override
public Map<String,List<Catelog2Vo>> getCatalogJson(){
//给缓存中放json字符串,拿出的json字符串,还用逆转为能用的对象类型;【序列化与反序列化】
// 1、加入缓存逻辑,缓存中存的数据是json字符串。
// JSON跨语言,跨平台兼容。
String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");
if(StringUtils.isEmpty(catalogJSON)){
//2、缓存中没有,查询数据库
Map<String,List<Catelog2Vo>> catalogJsonFromDb =getCatalogJsonFromDb();
//3、查到的数据再放入缓存,将对象转为json放在缓存中
String s = JSON.toJSONString(catalogJsonFromDb);
redisTemplate.opsForValue().set("catalogJSON",s);
//转为我们指定的对象。
return catalogJsonFromDb;
}
Map<String, List<Catelog2Vo>>result=JSON.parseObject(catalogJSON,new TypeReference<Map<String, List<Catelog2Vo>>>(){});
return result;
}
这个是将redis拿到的数据进行序列化的手段
Map<String, List<Catelog2Vo>>result=JSON.parseObject(catalogJSON,new TypeReference<Map<String, List<Catelog2Vo>>>(){});
第154集 对改造后的内容进行压力测试
这集没啥用,主要就测试下,使用redis对性能的一个影响。
第155集 redis面试题
缓存击穿
对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。如果这个key在大量请求同时进来前正好失效,那么所有对这个key的数据查询都落到db,我们称为缓存击穿。
解决:加锁大量并发只让一个去查,其他人等待,查到以后释放锁,其他人获取到锁,先查缓存,就会有数据,不用去db。
缓存穿透
缓存穿透:指查询一个一定不存在的数据,由于缓存是不命中,将去查询数据库,但是数据库也无此记录,我们没有将这次查询的null写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义
风险:利用不存在的数据进行攻击,数据库瞬时压力增大,最终导致崩溃
解决:null结果缓存,并加入短暂过期时间
缓存雪崩
缓存雪崩:缓存雪崩是指在我们设置缓存时key采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重雪崩
解决:原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低就很难引发集体失效的事件
在代码中 1、空结果缓存:解决缓存穿透 2、设置过期时间(加随机值):解决缓存雪崩 3、加锁:解决缓存击穿。前两个都好解决,重要的是第三个。
第156集,第157集,第158集 加锁解决缓存击穿问题
sychronized,JUC(Lock),在分布式情况下,想要锁住所有,必须使用分布式锁
我们首先查询数据库的方法用的sychronized本地锁,这样对于单个服务是满足的,对于多个同一服务的情况是不法保证的。
使用redis原生方法必须要保证加锁的原子性
1、占分布式锁。去redis占坑
2、设置过期时间必须和加锁是同步的,保证原子性(避免死锁)
3、判断是否有锁,有锁,就执行业务,执行业务完成,通过获取值对比,对比成功删除=原子性 lua脚本解锁。
4、如果没锁,则进行自旋操作,重复获取锁。
问题1:1、setnx占好了位,业务代码异常或者程序在页面过程中岩机。没有执行删除锁逻辑,这就造成了死锁。解决:设置锁的自动过期,即使没有删除,会自动删除
问题2:1、setnx占好了位,业务代码异常或者程序在页面过程中岩机。没有执行删除锁逻辑,这就造成了死锁。解决:设置锁的自动过期,即使没有删除,会自动删除
问题3:1、删除锁直接删除???如果由于业务时间很长,锁自己过期了,我们直接删除,有可能把别人正在持有的锁删除了。解决:占锁的时候,值指定为uuid,每个人匹配是自己的锁才删除。
问题:1、如果正好判断是当前值,正要删除锁的时候,锁已经过期,别人已经设置到了新的值。那么我们删除的是别人的锁解决:删除锁必须保证原子性。使用redis+Lua脚本完成
总结要点,redis锁必须设置过期时间,设置过期时间必须和加锁是同步的原子性的,锁名要uuid的。 查数据库,放缓存要是原子性的。
public Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithRedisLock() {
//1、占分布式锁。去redis占坑 设置过期时间必须和加锁是同步的,保证原子性(避免死锁)
String uuid = UUID.randomUUID().toString();
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid,300,TimeUnit.SECONDS);
if (lock) {
System.out.println("获取分布式锁成功...");
Map<String, List<Catelog2Vo>> dataFromDb = null;
try {
//加锁成功...执行业务
dataFromDb = getCatalogJsonFromDb();
} finally {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
//删除锁
redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList("lock"), uuid);
}
//先去redis查询下保证当前的锁是自己的
//获取值对比,对比成功删除=原子性 lua脚本解锁
// String lockValue = stringRedisTemplate.opsForValue().get("lock");
// if (uuid.equals(lockValue)) {
// //删除我自己的锁
// stringRedisTemplate.delete("lock");
// }
return dataFromDb;
} else {
System.out.println("获取分布式锁失败...等待重试...");
//加锁失败...重试机制
//休眠一百毫秒
try { TimeUnit.MILLISECONDS.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); }
return getCatalogJsonFromDbWithRedisLock(); //自旋的方式
}
}
第159集 Redisson简介整合
1、引入依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.13.6</version>
</dependency>
2、config文件
@Configuration
public class MyRedisson {
@Bean(destroyMethod="shutdown")
public RedissonClient redisson() throws IOException {
//1、创建配置
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
//2、根据config创建出RedissonCLient示例
RedissonClient redissonClient = Redisson.create(config);
return redissonClient;
}
}
3、测试成功
@Test
public void redisson(){
System.out.println(redissonClient);
}
第160集,第161集 Redisson-lock的使用
主要就这个代码
@ResponseBody
@GetMapping(value = "/hello")
public String hello() {
//1、获取一把锁,只要锁的名字一样,就是同一把锁
RLock myLock = redisson.getLock("my-lock");
//2、加锁
myLock.lock(); //阻塞式等待,相当于循环等待。默认加的锁都是30s
//1)、锁的自动续期,如果业务超长,运行期间自动锁上新的30s。不用担心业务时间长,锁自动过期被删掉
//2)、加锁的业务只要运行完成,就不会给当前锁续期,即使不手动解锁,锁默认会在30s内自动过期,不会产生死锁问题
// myLock.lock(10,TimeUnit.SECONDS); //10秒钟自动解锁,自动解锁时间一定要大于业务执行时间
//问题:在锁时间到了以后,不会自动续期
//1、如果我们传递了锁的超时时间,就发送给redis执行脚本,进行占锁,默认超时就是 我们制定的时间
//2、如果我们指定锁的超时时间,就使用 lockWatchdogTimeout = 30 * 1000 【看门狗默认时间】
//只要占锁成功,就会启动一个定时任务【重新给锁设置过期时间,新的过期时间就是看门狗的默认时间】,每隔10秒都会自动的再次续期,续成30秒
// internalLockLeaseTime 【看门狗时间】 / 3, 10s
try {
System.out.println("加锁成功,执行业务..." + Thread.currentThread().getId());
try { TimeUnit.SECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }
} catch (Exception ex) {
ex.printStackTrace();
} finally {
//3、解锁 假设解锁代码没有运行,Redisson会不会出现死锁
System.out.println("释放锁..." + Thread.currentThread().getId());
myLock.unlock();
}
return "hello";
}
要点:1、锁的自动续期(看门狗机制),如果业务超长,运行期间自动锁上新的30s。不用担心业务时间长,锁自动过期被删掉 2、加锁的业务只要运行完成,就不会给当前锁续期,即使不手动解锁,锁默认会在30s内自动过期,不会产生死锁问题,这里用的看门狗,可能是心跳机制检测业务是否完成。