谷粒商城学习笔记,第七天:性能压测+缓存+分布式锁
一、性能压测
我们希望通过压测发现其他测试更难发现的错误:内存泄漏、并发与同步。
1、性能指标
吞吐量、响应时间QPS TPS、错误率
RT:Response Time 响应时间
HPS:hits per second 每秒点击次数
TPS:Transaction per second 系统每秒处理交易数
QPS:query per second 每秒处理查询次数
2、JMeter
下载地址
2.1、运行
bin目录下的ApacheJMeter.jar 或者jmeter.bat
2.2、步骤
创建线程组
测试计划 > 添加 > 线程 > 线程组
创建请求:
测试线程组 > 添加 > 取样器 > HTTP请求、JDBC请求等
查看请求结果:
测试线程组 > 添加 > 监听器 > 查看结果树、汇总报告、聚合报告
2.3、异常
测试本地服务时有时会出现如下异常
java.net.BindException:Address already in use : connect
windows本身提供的端口访问机制的问题。
Windows提供TCP/IP链接的端口为1024-5000,并且要四分钟来循环回收他们。就导致我们在短时间内跑大量的请求时将端口沾满
解决:
1、cmd中,用regedit命令打开注册表
2、在 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters
3、右键Parameters
4、添加新的DWORD,名字为MaxUserPort和TcpTimedWaitDelay
5、分别输入数值数据为65534和30,基数选择十进制;以增大可分配的tcp连接端口数、减小处于TIME_WAIT状态的连接的生存时间
6、修改配置完毕之后记得重启机器才会生效
2.4、影响因素
##影响性能考虑点:
数据库、应用程序、中间件(TOMCAT NGINX MQ)等、网络带宽和操作系统等
##考虑自己的应用数据CPU密集型 还是IO密集型
3、JConsole 和 JvisualVM
//TODO 之后在JVM虚拟机 总结
jconsole
jvisualvm
二、缓存
那些数据适合放入缓存:
1、即时性、数据一致性要求不高的
2、访问量大且更新频率不高的数据(读多、写少)
1、整合redis
1.1、导入依赖:
org.springframework.boot
spring-boot-starter-data-redis
1.2、配置:
spring:
# redis 配置
redis:
# 地址
host: localhost
# 端口,默认为6379
port: 6379
# 密码
password: admin123
##foobared
# 连接超时时间
timeout: 10s
lettuce:
pool:
# 连接池中的最小空闲连接
min-idle: 0
# 连接池中的最大空闲连接
max-idle: 8
# 连接池的最大数据库连接数
max-active: 8
# #连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: -1ms
1.3、使用:
private RedisTemplate redisTemplate;
或者
private StringRedisTemplate redisTemplate;
@Test
public void testRedisConnect(){
ValueOperations ops = redisTemplate.opsForValue();
ops.set("hello","world_"+ UUID.randomUUID().toString());
String hello = ops.get("hello");
System.out.println(hello);
}
1.4、异常:OutofDirectNemoryError
堆外内存溢出:
springboot2.o以后默认使用lettuce作为操作redis的客户端。它使用netty进行网络通信。
lettuce的bug导致netty堆外内存溢出
netty如果没有指定堆外内存,默认使用应用配置的-Xmx300ml
也可以通过-Dio.netty.maxDirectMemory进行设置
解决方案:
不能使用-Dio.netty.maxDirectNemory只去调大堆外内存。
1)、升级Lettuce客户端。2)、切换使用jedis
【推荐使用jedis】
1.5、重新引入redis
org.springframework.boot
spring-boot-starter-data-redis
io.lettuce
lettuce-core
redis.clients
jedis
1.6、说明
//lettuce和jedis都是操作redis的底层客户端
//springboot对lettuce和jedis进行了再次封装,成了RedisTemplate
//springboot导入了LettuceConnectionConfiguration和JedisConnectionConfiguration
@EnableConfigurationProperties({RedisProperties.class})
@Import({LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class})
public class RedisAutoConfiguration {
}
2、缓存穿透
查询一个不存在的数据
缓存穿透是指缓存和数据库中都没有的数据,而用户恶意不断发起请求,如发起为id为“-1”的数据或id为特别大不存在的数据。
这时的用户很可能是攻击者,攻击会导致数据库压力过大。
解决方案:
1>、接口层增加校验,如用户鉴权校验,id做基础校验,id小于等于0的直接拦截
2>、NULL结果缓存,并加入短暂的过期时间【推荐】
从缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,
缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用)。
这样可以防止攻击用户反复用同一个id暴力攻击
3、缓存雪崩
缓存大面积失效
缓存雪崩是指我们设置缓存是KEY采用了相同的过期时间,导致缓存在某一时刻同时失效。
请求全部转发到了DB,DB瞬时压力过重雪崩。
解决方案:
1>、缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。【推荐】
在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率
就会降低,就很难应发集体失效的事件
2>、如果缓存数据库是分布式部署,将热点数据均匀分布在不同的缓存数据库中。
3>、设置热点数据永远不过期。
4、缓存击穿
查询一个存在的数据,但是缓存中没有
缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,
同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力
解决方案:
1>、设置热点数据永远不过期。
2>、加互斥锁【推荐】
大量并发只让一个去查,其他人等待,查到以后释放锁,其他人获取到锁,先插缓存,就会有数据,不用去DB了
实例:
public static String getData(String key){
String result;
try{
//从缓存读取数据
result = getDataFromRedis(key);
//缓存中不存在数据
if(result == null){
//去获取锁,获取成功,去数据库取数据
if(reenLock.tryLock()){
//去获取锁,获取成功,去DB取数据
result = getDataFromDB(key);
//更新缓存数据
if(result != null){
setDataToRedis(key,result);
}
//释放锁
reenLock.unlock();
}else{
//获取锁失败
//赞同100ms再去重新获取数据
Thread.sleep(100);
result = getData(key);
}
}
}catch(Exception e){
//发生异常后释放锁
lock.unlock;
}
return result;
}
三、分布式锁
1、分布式锁原理与使用
1.1、思考问题
1>、setnx占好了位,业务代码异常或者程序在业务过程中党纪。没有执行删除锁逻辑,这就造成了死锁
解决:设置锁的自动过期,即使没有手动删除,也会自动删除
2>、setnx设置好,正要去设置过期时间,宕机又死锁了
解决:设置过期时间和占位必须是原子的,redis支持使用setnx ex命令
3>、如果业务时间过长,锁自动过期了,我们直接删除,有可能把别人正在持有的锁删除了,导致大量线程的进入
解决:占锁的时候,值定位UUID,每一个线程只能匹配到自己的锁才能删除
4>、在删除锁的过程中,刚根据UUID匹配出自己持有的锁,锁自动过期,别人已经设置到了新的锁值,那么我们会删除到别人的锁
解决:删除锁必须保证原子性,使用redis lua脚本完成
5>、保证加锁【占位+过期时间】和删除锁【判断+删除】的原子性。更难的事情是,锁的自动续期
1.2、代码实现
@Test
public void testRedisLock(){
//创建锁,去Redis占位
String locKValue = UUID.randomUUID().toString();
//原子操作
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", locKValue, 300, TimeUnit.SECONDS);
if (lock){
//创建锁成功:
//处理业务逻辑
try{
System.out.println("----->处理业务逻辑");
}finally {
//删除锁:原子操作,查询和删除一起:LUA脚本
String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
//成功1失败0
Long lock1 = redisTemplate.execute(new DefaultRedisScript(script, Long.class), Arrays.asList("lock"), locKValue);
}
}else{
//创建锁失败:重新调用自己,尝试是否可以创建锁
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
testRedisLock();
}
}
2、分布式锁Redisson
官方地址
2.1、原生redisson使用
导入:
org.redisson
redisson
3.12.0
配置:
@Component
public class RedissonConfig {
@Bean(destroyMethod="shutdown")
public RedissonClient redissonClient(){
//创建配置
Config config = new Config();
SingleServerConfig singleServerConfig = config.useSingleServer();
singleServerConfig.setAddress("redis://60.205.254.48:6380");
singleServerConfig.setPassword("admin123");
//创建实例
RedissonClient redissonClient = Redisson.create(config);
return redissonClient;
}
}
使用:
@Autowired
private RedissonClient redissonClient;
@Test
public void testRedisson(){
System.out.println(redissonClient);
}
2.2、redisson做分布式锁
@Autowired
private RedissonClient redissonClient;
@Test
public void testRedissonLock(){
RLock lock = redissonClient.getLock("my-lock");
//加锁:阻塞式等待 且 Redisson会对锁进行自动续期
lock.lock();
try {
System.out.println("------处理业务逻辑------");
}finally {
//解锁
lock.unlock();
}
}
注意:
lock.lock();是阻塞式等待,默认加锁时间是30s
Redisson会给锁自动续期,不用担心业务时间过长,锁自动过期的情况
加锁的业务只要运行完成,Redisson就不会给锁续期了,即使不手动解锁,锁默认在30s后自动过期
但如果使用lock.lock(10,TimeUnit.SECONDS)给锁设置了过期时间,锁就不会自动续期了
看门狗原理
2.3、读写锁
说明:写锁是一个排它锁(互斥锁),读锁是一个共享锁
@RestController
@RequestMapping("/redission")
public class RedissonController {
@Autowired
private RedissonClient redissonClient;
@Autowired
private StringRedisTemplate redisTemplate;
//写锁
@GetMapping("/writeLock")
public String writeLock(){
RReadWriteLock rReadWriteLock = redissonClient.getReadWriteLock("rw-lock");
//获取写锁
RLock rLock = rReadWriteLock.writeLock();
String s = "";
try {
rLock.lock();
s = UUID.randomUUID().toString();
Thread.sleep(30000);
redisTemplate.opsForValue().set("aa",s);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
rLock.unlock();
}
return s;
}
//读锁
@GetMapping("/readLock")
public String readLock(){
RReadWriteLock readWriteLock = redissonClient.getReadWriteLock("rw-lock");
RLock rLock = readWriteLock.readLock();
String s = "";
rLock.lock();
try {
s = redisTemplate.opsForValue().get("aa");
} finally {
rLock.unlock();
}
return s;
}
}
测试:
##访问:http://localhost:10000/redission/readLock
##和: http://localhost:10000/redission/writeLock
2.4、闭锁
@RestController
@RequestMapping("/redission")
public class RedissonController {
@Autowired
private RedissonClient redissonClient;
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 闭锁
* 学校里有5个班,所有班里的人都走了,才能锁校门
*/
@GetMapping("/closeDoor")
public String closeDoor() throws InterruptedException {
RCountDownLatch countDownLatch = redissonClient.getCountDownLatch("close-door");
countDownLatch.trySetCount(5);//设置总共有5个班,5个班都走了 才能继续往下执行
countDownLatch.await();//等待闭锁都完成
return "...所有人都走了,锁校门";
}
@GetMapping("/gogogo/{id}")
public String gogogo(@PathVariable("id") Integer id){
RCountDownLatch countDownLatch = redissonClient.getCountDownLatch("close-door");
countDownLatch.countDown();//计数减一
return id+"班的人走了";
}
}
测试:
##访问
http://localhost:10000/redission/gogogo/1 2 3 4 5
http://localhost:10000/redission/closeDoor
2.5、信号量
@RestController
@RequestMapping("/redission")
public class RedissonController {
@Autowired
private RedissonClient redissonClient;
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 信号量
* 总共有3个车位
* 三个车位满了以后,开走一辆车才能再停一辆车
*/
@GetMapping("/park")
public String park() throws InterruptedException {
RSemaphore park = redissonClient.getSemaphore("park");
park.acquire();//获取一个信号量,占取一个车位
return "...占取车位SUCCESS";
}
@GetMapping("/go")
public String go(){
RSemaphore park = redissonClient.getSemaphore("park");
park.release();//释放一个信号量,一辆车开走了
return "一辆车开走了";
}
}
测试:
##访问
http://localhost:10000/redission/park
http://localhost:10000/redission/go
四、缓存数据一致性的问题
##一般解决缓存数据一致性的问题,有两种方案:
#####双写模式
如果修改了数据,数据库和缓存在同一个方法下同时修改
问题:产生脏数据,如下图,请求1比请求2执行的慢,最后会出现数据库是2缓存是1的状况
#####失效模式【推荐】
如果修改了数据,数据库中的数据修改且同时直接删除缓存,等读取数据的时候就会重新添加进缓存了
问题:产生脏数据,如下图,请求3在请求2还没修改完数据库后就读取了数据库,读到的数据是1,
等请求2写完数据库又删除了缓存后,请求3将缓存更新为了数据库1的内容,最后导致数据库是2的内容,缓存是1的内容
【解决方案】
1、修改的时候使用读写锁redissonClient.getReadWriteLock,但是会影响效率
2、使用阿里的中间件Canel