文章目录
十五、压力测试
这里是使用jmeter作为压力测试工具。
1.一些基本概念
TPS 每秒处理的交易数
QPS 每秒处理的请求数
还有一个90%响应时间,也就是说,我们要尽量满足90%的请求都顺利完成,那么这个接口就很不错了。
从外部来看就是吞吐量越大越好,响应时间越短越好,错误率越低越好
SQL耗时越小越好,一般情况下微秒级别
命中率越高越好,一般情况下不能低于95%
锁等待次数越低越好,等待时间越短越好
中间件越多,性能损失越大,大多都损失在网络交互了
2.JVM内存机制
视频里涉及到了,原版笔记里有详细的jvm笔记,这里就不去深究。
https://blog.csdn.net/hancoder/article/details/107612746
大致是JVM分堆与栈,堆是线程共享的,栈是线程隔离的。
然后垃圾回收,主要是针对堆,因为堆是线程共享。
分youngGC和fullGC,当eden区满了又有新对象生成时,就youngGC,当youngGC之后eden区还是满的,就尝试放到老年区。当老年区满了,就fullGC,fullGC耗时是youngGC的十倍,所以JVM调优要尽量避免fullGC、fullGC再没空间就oom异常。
3.压测记录
主要包括
压测内容、压测线程数、吞吐量、90响应时间、99响应时间
4.Nginx动静分离
由于动态资源和静态资源都位于服务器中,导致服务器的压力过大。
而静态资源本身是不需要服务器处理的(大多是时候是不变的)
所以关于静态资源的请求我们并不需要放到服务器去做、也就是nginx收到静态资源的请求,其实并不需要转发到服务器网关。
具体步骤:
-
静态文件上传到 mydata/nginx/html/static/index/css
-
修改index.html的静态资源路径,加上static前缀src="/static/index/img/img_09.png"
-
修改/mydata/nginx/conf/conf.d/gulimall.conf
-
如果遇到有/static为前缀的请求,转发至html文件夹
gulimall.conf
location /static {
root /usr/share/nginx/html; #遇到/static 路径的请求,直接去本地linux目录寻找
}
location / {
proxy_pass http://gulimall; # 遇到除/static 路径意外的请求,路由至http://gulimall 并重新加上请求头
proxy_set_header Host $host; # (因为nginx转发会丢失消息头)
}
5.优化三级分类查询
原本是每次获取子类pid,再去查子类。现在是一次性把所有的三级分类都查出来,封装成一个集合,再给他们分别包装。
//优化业务逻辑,仅查询一次数据库
List<CategoryEntity> categoryEntities = this.list();
//查出所有一级分类
List<CategoryEntity> level1Categories = getCategoryByParentCid(categoryEntities, 0L);
Map<String, List<Catalog2Vo>> listMap = level1Categories.stream().collect(Collectors.toMap(k->k.getCatId().toString(), v -> {
//遍历查找出二级分类
List<CategoryEntity> level2Categories = getCategoryByParentCid(categoryEntities, v.getCatId());
List<Catalog2Vo> catalog2Vos=null;
if (level2Categories!=null){
//封装二级分类到vo并且查出其中的三级分类
catalog2Vos = level2Categories.stream().map(cat -> {
//遍历查出三级分类并封装
List<CategoryEntity> level3Catagories = getCategoryByParentCid(categoryEntities, cat.getCatId());
List<Catalog2Vo.Catalog3Vo> catalog3Vos = null;
if (level3Catagories != null) {
catalog3Vos = level3Catagories.stream()
.map(level3 -> new Catalog2Vo.Catalog3Vo(level3.getParentCid().toString(), level3.getCatId().toString(), level3.getName()))
.collect(Collectors.toList());
}
Catalog2Vo catalog2Vo = new Catalog2Vo(v.getCatId().toString(), cat.getCatId().toString(), cat.getName(), catalog3Vos);
return catalog2Vo;
}).collect(Collectors.toList());
}
return catalog2Vos;
}));
return listMap;
十六、redisson分布式锁与缓存
1.概念
- 本地缓存:和微服务同一个进程。缺点:分布式时
- 分布式缓存:缓存中间件
2.redis
使用jedis
使用redis似乎有bug,jedis是操作redis的底层客户端
具体操作略
3.缓存失效
缓存穿透
缓存和数据库中都没有的数据,用户反复请求,如id=“-1”
解决:缓存空对象
缓存雪崩
设置的key同时失效,对这些key的查询全部涌向db 不同数据
解决:分散key的失效时间
缓存击穿
数据库中有但缓存中没有,一般是缓存过期,大量用户同时查同一条数据涌向db
解决:设置缓存永不过期或加互斥锁
互斥锁:
当读取缓存为空时,先返回成功,而不是直接去查db
4.缓存击穿
查数据库时,拿到竞争锁后,再次确认缓存中没有再去查db
如何复制微服务:
右键点击服务,copy configuration
在program arguments: --server.port=10003
5.分布式缓存
概念
本地缓存问题:每个微服务都要有缓存服务、数据更新时只更新自己的缓存,造成缓存数据不一致
解决方案:分布式缓存,微服务共用 缓存中间件
原则
- 我们能放入缓存的数据本就不应该是实时性、一致性要求超高的。所以缓存数据的时候加上过期时间,保证每天拿到当前最新数据即可。
- 我们不应该过度设计,增加系统的复杂性
- 遇到实时性、一致性要求高的数据,就应该查数据库,即使慢点。
基本流程
没获取到锁阻塞或者sleep一会
设置好了锁,万一服务出现宕机,没有执行删除锁逻辑,这就造成了死锁
解决:设置过期时间
务还没执行完锁就过期了,别人拿到锁,自己执行完去删了别人的锁
解决:锁续期(redisson有看门狗) 删锁的时候明确是自己的锁。如uuid
判断uuid对了,但是将要删除的时候锁过期了,别人设置了新值,那删除了别人的锁
解决:删除锁必须保证原子性(保证判断和删锁是原子的)。使用redis+Lua脚本完成,脚本是原子的
但是lua脚本写法每次用分布式锁时比较麻烦,我们可以采用redisson现有框架
6.Redisson
Redisson提供了使用Redis的最简单和最便捷的方法。Redisson的宗旨是促进使用者对Redis的关注分离(Separation of Concern),从而让使用者能够将精力更集中地放在处理业务逻辑上。
环境搭建
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.13.4</version>
</dependency>
<-!这个用作练习,后面可以使用redisson-spring-boot-starter->
@Configuration
public class MyRedisConfig {
@Value("${ipAddr}")
private String ipAddr;
// redission通过redissonClient对象使用 // 如果是多个redis集群,可以配置
@Bean(destroyMethod = "shutdown")
public RedissonClient redisson() {
Config config = new Config();
// 创建单例模式的配置
config.useSingleServer().setAddress("redis://" + ipAddr + ":6379");
return Redisson.create(config);
}
}
可重入锁
可重入锁就是单个线程可以多次获得同一个锁。(其他线程还是无法获取)
要注意的是,获取锁的次数一定要和释放锁的次数一致,否则会导致其他线程一直无法获得锁。
// 参数为锁名字
RLock lock = redissonClient.getLock("CatalogJson-Lock");//该锁实现了JUC.locks.lock接口
lock.lock();//阻塞等待
// 解锁放到finally // 如果这里宕机:有看门狗,不用担心
lock.unlock();
锁的续期
大家都知道,如果负责储存这个分布式锁的Redisson节点宕机以后,而且这个锁正好处于锁住的状态时,这个锁会出现锁死的状态。为了避免这种情况的发生,Redisson内部提供了一个监控锁的看门狗,它的作用是在Redisson实例被关闭前,不断的延长锁的有效期。默认情况下,看门狗的检查锁的超时时间是30秒钟(每到20s就会自动续借成30s,是1/3的关系),也可以通过修Config.lockWatchdogTimeout来另行指定。
读写锁
读写锁维护了一对锁,一个读锁和一个写锁,读锁能够同时的被多个读线程所获取,而写锁是一个排它锁,在同一时刻只能有一个线程获取。读写锁需要确保任意写线程对写锁的获取将会阻塞读线程对读锁的获取,二者之间是有关联的。
互斥原则:
- 读-读能共存,
- 读-写不能共存,
- 写-写不能共存。
一般情况下,读写锁的性能都会比排它锁要好,因为大部分的场景是读大于写。如果一个共享的数据结构,它大部分时间都是在不停的被查询和搜索,而很少的时间会做更新,那么这种场景就是读写锁的使用场景。也就是说,在读大于写的情况下,读写锁能够提供比排它锁更好的吞吐量,但是反之,在一个写偏重的场景下,读写锁就不再擅长了,实际场景中,要结合实际情况进行具体的分析,考察是否应该采用读写锁。
RReadWriteLock rwlock = redisson.getReadWriteLock("anyRWLock");
// 最常见的使用方法
rwlock.readLock().lock();
// 或
rwlock.writeLock().lock();
// 10秒钟以后自动解锁
// 无需调用unlock方法手动解锁
rwlock.readLock().lock(10, TimeUnit.SECONDS);
// 或
rwlock.writeLock().lock(10, TimeUnit.SECONDS);
// 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
boolean res = rwlock.readLock().tryLock(100, 10, TimeUnit.SECONDS);
// 或
boolean res = rwlock.writeLock().tryLock(100, 10, TimeUnit.SECONDS);
...
lock.unlock();
信号量(Semaphore)
信号量为存储在redis中的一个数字,当这个数字大于0时,即可以调用acquire()方法增加数量,也可以调用release()方法减少数量,但是当调用release()之后小于0的话方法就会阻塞,直到数字大于0
基于Redis的Redisson的分布式信号量(Semaphore)Java对象RSemaphore采用了与java.util.concurrent.Semaphore相似的接口和用法。同时还提供了异步(Async)、反射式(Reactive)和RxJava2标准的接口。
闭锁
闭锁是一种同步工具,可以延迟线程直到其达到其终止状态。
例如:DOTA2中匹配等待点确定界面的设计,需要等待所有十个玩家都点就绪才能继续进行。其实也有些类似于之前CUDA编程中用到的 __syncthreads()方法去同步同一个块内的线程。
基本方法:
void await():使当前线程在锁存器倒计数至零之前一直等待,除非线程被中断。
boolean await(long timeout, TimeUnit unit):使当前线程在锁存器倒计数至零之前一直等待,除非线程被中断或超出了指定的等待时间。
void countDown():递减锁存器的计数,如果计数到达零,则释放所有等待的线程。
long getCount():返回当前计数。
以下代码只有offLatch()被调用5次后 setLatch()才能继续执行
RCountDownLatch latch = redisson.getCountDownLatch("anyCountDownLatch");
latch.trySetCount(1);
latch.await();
// 在其他线程或其他JVM里
RCountDownLatch latch = redisson.getCountDownLatch("anyCountDownLatch");
latch.countDown();
7.缓存和数据库一致性
- 我们能放入缓存的数据本就不应该是实时性、一致性要求超高的。所以缓存数据的时候加上过期时间,保证每天拿到当前最新数据即可。
- 我们不应该过度设计,增加系统的复杂性
- 遇到实时性、一致性要求高的数据,就应该查数据库,即使慢点。