谷粒商城学习笔记,第七天:性能压测+缓存+分布式锁

谷粒商城学习笔记,第七天:性能压测+缓存+分布式锁

一、性能压测

我们希望通过压测发现其他测试更难发现的错误:内存泄漏、并发与同步

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、步骤

创建线程组

测试计划 > 添加 > 线程 > 线程组

谷粒商城学习笔记,第七天:性能压测+缓存+分布式锁_第1张图片

创建请求:

测试线程组 > 添加 > 取样器 > HTTP请求、JDBC请求等

谷粒商城学习笔记,第七天:性能压测+缓存+分布式锁_第2张图片

查看请求结果:

测试线程组 > 添加  >  监听器  >  查看结果树、汇总报告、聚合报告

谷粒商城学习笔记,第七天:性能压测+缓存+分布式锁_第3张图片

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

谷粒商城学习笔记,第七天:性能压测+缓存+分布式锁_第4张图片

jvisualvm

谷粒商城学习笔记,第七天:性能压测+缓存+分布式锁_第5张图片

二、缓存

那些数据适合放入缓存:

1、即时性、数据一致性要求不高的
2、访问量大且更新频率不高的数据(读多、写少)

谷粒商城学习笔记,第七天:性能压测+缓存+分布式锁_第6张图片

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();
    }
}

谷粒商城学习笔记,第七天:性能压测+缓存+分布式锁_第7张图片

注意:

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

谷粒商城学习笔记,第七天:性能压测+缓存+分布式锁_第8张图片

谷粒商城学习笔记,第七天:性能压测+缓存+分布式锁_第9张图片

你可能感兴趣的:(数据库,分布式,redis,java,多线程)