高并发限制因素:
网络,cpu,内存,硬盘io
优化:1.优化业务代码,使用合理的算法,减少计算量或者使用的空间,减少数据库的查询2.使用缓存,如果单体应用,可以直接使用map(本地缓存)。如果是分布式,用缓存中间件,保持数据一致性3.前后端分离,tomcat不需要处理静态资源请求4.优化中间件(影响没那么大)5.jvm调优
Jmeter 压力测试,可以看:https://www.bilibili.com/video/BV1np4y1C7Yf?p=142
jvm调优:【使用jvisualvm】
1.主要是在堆上面调优,堆分为 Eden,Survivor 0 ,Survivor 1,Old,Metaspace
先在Eden分配内存,要是Eden不够,小规模在Eden GC,再不够,大规模GC
大规模GC时间久,所有提高性能需要加大 Eden,减少大规模GC次数
不适合放在缓存中的数据:修改较多,实时性要求高【缓存一致性问题】
加锁:
锁的基本结构,以简单的分布式锁为例:
【过期时间,操作原子性,删自己的锁,自旋】
// 加锁:解决缓存击穿
public Map<String, List<Catelog2Vo>> getCatelogJsonFromDb() {
// 解决缓存击穿,加锁
// TODO 本机锁:synchronized JUC(lock) 在分布式情况下,想锁住所有,用分布式锁
// 1.加单机同步锁
// synchronized是单机同步锁
// springboot中,对象一般都是单例的,synchronized (this) 可以锁住【适合单机,分布式也行,压力也不会太大】
//synchronized (this) {
// return getDataFromDb();
//}
// 2.分布式锁-1.去redis占坑【在redis设置一个值,设置成功相当于获得锁】
// 优化:
// 设置值时加过期时间【需要原子操作,防止出现以为加不了过期时间】。
// 使用uuid,防止删掉别人的锁[因为可能业务超出锁的过期时间,别人也获取到了锁]【也需要原子操作】
String uuid = UUID.randomUUID().toString();
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid,10,TimeUnit.SECONDS);
if(lock){
// 加锁成功,执行业务
Map<String, List<Catelog2Vo>> dataFromDb;
// 要是执行业务出异常,解锁
try{
dataFromDb= getDataFromDb();
}finally {
// 获取数据后,解锁(删除掉redis中的lock kv)【查询数据以及删除需要原子性!!!使用lua脚本】
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);
}
//String lock1 = redisTemplate.opsForValue().get("lock");
//if(uuid.equals(lock1)){
// redisTemplate.delete("lock");
//}
return dataFromDb;
}else{
// 加锁失败,重试
// 休眠200毫秒
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
return getCatelogJsonFromDb(); // 自旋
// 个人感觉其实可以用while循环,循环里面一直查缓存,直到从缓存中查出
// 要是超过一定时间找不出,查数据库,加缓存
// 因为加锁之后只能有一个线程查询,一直拿锁很影响效率,最好还是一个人获得锁之后,其他人不拿锁,等结果
}
}
分布式锁
【redis基础上,使用Redisson】【GitHub有中文文档,可参考】【Redisson还有分布式对象等功能】
能够实现分布式的原因:值不是保存在本地,而是保存在redis中,比如信号量,比如锁的值
【锁的名字不同,锁就不同。最好使锁的粒度最小。比如某种商品就加某种商品的锁,不加所有商品的锁】
配置:
<!-- 以后使用Redisson作为所有分布式锁,分布式对象 -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.12.0</version>
</dependency>
配置类:
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.io.IOException;
@Configuration
public class MyRedissonConfig {
/**
* 所有对Redisson的使用都是通过RedissonClient
*/
@Bean(destroyMethod="shutdown")
public RedissonClient redissonClient() throws IOException {
//1、创建配置【单节点配置】
Config config = new Config();
config.useSingleServer().setAddress("redis://192.168.190.150:6379");
//2、根据Config创建出RedissonClient实例
//Redis url should start with redis:// or rediss://
RedissonClient redissonClient = Redisson.create(config);
return redissonClient;
}
}
Redisson锁:
【底层其实也是在redis里面设置一个 keyvalue,占坑】
redisson的锁,比上面自己写的锁好:
1.阻塞式等待,不需要像上面那样写,循环调用自己
2.解决锁的自动续期。不用担心业务时间长,锁自动过期失效
\3. 如果我们传递了锁的超时时间,就发送给redis执行脚本,进行占锁,默认超时就是我们指定的时间
如果我们未指定锁的超时时间,就使用38*1800【LockWatchdogTimeout看门狗的默认时间】;
只要占锁成功,就会启动一个定时任务【重新给锁设置过期时间,新的过期时间就是看门狗的默认时间】:internalLockLeaseTime【看门狗时间】/3,10s
普通锁:【可重入锁】(不设置超时时间,设置超时时间,尝试获取锁,公平锁)
@GetMapping("/hello")
@ResponseBody
public String hello(){
// 只要名字一样,就是同一把锁
RLock lock = redisson.getLock("my-lock");
// 1.加锁。不设置过期时间,自动续期。业务完成自动过期
lock.lock();
// 2.加锁。设置10秒过期时间。不会自动续期,要确保锁的过期时间比业务运行时间长
//lock.lock(10, TimeUnit.SECONDS);
// 3.尝试加锁,最多等待100秒,上锁以后10秒自动解锁
//boolean res = lock.tryLock(100,10,TimeUnit.SECONDS);
// 4.公平锁,按照一开始先来后到的顺序获取锁【获取锁之后的加锁操作也是 fairLock.lock();】
//RLock fairLock redisson.getFairLock("anyLock");
try{
System.out.println("加锁成功,执行业务。。。"+Thread.currentThread().getName());
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
System.out.println("释放锁。。。"+Thread.currentThread().getName());
lock.unlock();
}
return "hello";
}
读写锁
使用读写锁:
//保证一定能读到最新数据,修改期间,写锁是一个排他锁(互斥锁、独享锁),读锁是一个共享锁
//写锁没释放读就必须等待
//读+读:相当于无锁,并发读,只会在redis中记录好,所有当前的读锁。他们都会同时加锁成功
//写+读:等待写锁释放
//写+写:阻塞方式
//读+写:有读锁。写也需要等待。
//只要有写的存在,都必须等待
@GetMapping("/write")
@ResponseBody
public String writeValue(){
// 获得读写锁
RReadWriteLock lock = redisson.getReadWriteLock("rw-lock");
// 获得写锁
RLock rLock = lock.writeLock();
// 获得写锁+加锁,可以这样写:lock.writeLock().lock();
String s = "";
try {
// 改数据加写锁,读数据加读锁
rLock.lock();
s = UUID.randomUUID().toString();
Thread.sleep(3000);
redisTemplate.opsForValue().set("writeValue",s);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
// 释放锁
rLock.unlock();
}
return s;
}
@GetMapping("/read")
@ResponseBody
public String readValue(){
String s = "";
// 获得读写锁
RReadWriteLock lock = redisson.getReadWriteLock("rw-lock");
// 获得读锁
RLock rLock = lock.readLock();
// 加锁
rLock.lock();
// 获得写锁+加锁,可以这样写:lock.readLock().lock();
try{
redisTemplate.opsForValue().get("writeValue");
}catch (Exception e){
e.printStackTrace();
}finally {
// 释放锁
rLock.unlock();
}
return s;
}
信号量【可以用来进行限流操作:先在redis设置一个键值,键为信号量名字。请求尝试获取信号量,获取不到返回false,不进行等待】
/**
* 车库停车
* 3车位
* 信号量也可以做分布式限流
*/
@GetMapping(value = "/park")
@ResponseBody
public String park() throws InterruptedException {
【在redis里面,设置一个键为park,值为3】
RSemaphore park = redisson.getSemaphore("park");
//获取一个信号、获取一个值,占一个车位
//park.acquire();
// 尝试获取一个信号量,获取不到返回false,不会阻塞等待
boolean flag = park.tryAcquire();
if (flag) {
//执行业务
} else {
return "当前停车位不足,请稍后再尝试";
}
return "ok=>" + flag;
}
@GetMapping(value = "/go")
@ResponseBody
public String go() {
RSemaphore park = redisson.getSemaphore("park");
park.release(); //释放一个车位
return "ok";
}
闭锁:等所有的条件都完成才执行
/**
* 放假、锁门
* 1班没人了
* 5个班,全部走完,我们才可以锁大门
* 分布式闭锁
*/
@GetMapping(value = "/lockDoor")
@ResponseBody
public String lockDoor() throws InterruptedException {
RCountDownLatch door = redisson.getCountDownLatch("door");
door.trySetCount(5);
door.await(); //等待闭锁完成
return "放假了...";
}
@GetMapping(value = "/gogogo/{id}")
@ResponseBody
public String gogogo(@PathVariable("id") Long id) {
RCountDownLatch door = redisson.getCountDownLatch("door");
door.countDown(); //计数-1
return id + "班的人都走了...";
}