天天看点

微服务-性能压测\缓存redis和分布式锁redisson和SpringCache

1.0 压力测试

内存泄漏(循环),并发与同步
  • 响应时间
  • hps: 每秒点击次数
  • tps: 系统每秒处理交易次数(事务 完整的场景链)
  • qps: 系统每秒处理查询次数,
  • 最大响应时间
  • 最小响应时间
  • 90%响应时间, 排序后90% 内响应时间
  • 吞吐量,响应时间,错误率

1.1 JMeter 安装

apache

微服务-性能压测\缓存redis和分布式锁redisson和SpringCache
微服务-性能压测\缓存redis和分布式锁redisson和SpringCache

2.性能监控 堆内存与垃圾回收

cpu密集型和IO密集型

2.1 jvm内存模型

1. 堆(Heap)

堆内存是所有线程共有的,可以分为两个部分:年轻代和老年代。下图中的Perm代表的是永久代,但是注意永久代并不属于堆内存中的一部分,同时jdk1.8之后永久代也将被移除。

微服务-性能压测\缓存redis和分布式锁redisson和SpringCache

堆是java虚拟机所管理的内存中最大的一块内存区域,也是被各个线程共享的内存区域,该内存区域存放了对象实例及数组(但不是所有的对象实例都在堆中)。其大小通过-Xms(最小值)和-Xmx(最大值)参数设置(最大最小值都要小于1G),前者为启动时申请的最小内存,默认为操作系统物理内存的1/64,后者为JVM可申请的最大内存,默认为物理内存的1/4,默认当空余堆内存小于40%时,JVM会增大堆内存到-Xmx指定的大小,可通过-XX:MinHeapFreeRation=来指定这个比列;当空余堆内存大于70%时,JVM会减小堆内存的大小到-Xms指定的大小,可通过XX:MaxHeapFreeRation=来指定这个比列,当然为了避免在运行时频繁调整Heap的大小,通常-Xms与-Xmx的值设成一样。堆内存 = 新生代+老生代+持久代。在我们垃圾回收的时候,我们往往将堆内存分成新生代和老生代(大小比例1:2),新生代中由Eden和Survivor0,Survivor1组成,三者的比例是8:1:1,新生代的回收机制采用复制算法***,在Minor GC的时候,我们都留一个存活区用来存放存活的对象,真正进行的区域是Eden+其中一个存活区,当我们的对象时长超过一定年龄时(默认15,可以通过参数设置),将会把对象放入老生代,当然大的对象会直接进入老生代。老生代采用的回收算法是标记整理算法。*

2. 方法区(Method Area)

方法区也称”永久代“,它用于存储虚拟机加载的类信息、常量、静态变量、是各个线程共享的内存区域。默认最小值为16MB,最大值为64MB(64位JVM由于指针膨胀,默认是85M),可以通过-XX:PermSize 和 -XX:MaxPermSize 参数限制方法区的大小。它是一片连续的堆空间,永久代的垃圾收集是和老年代(old generation)捆绑在一起的,因此无论谁满了,都会触发永久代和老年代的垃圾收集。不过,一个明显的问题是,当JVM加载的类信息容量超过了参数-XX:MaxPermSize设定的值时,应用将会报OOM的错误。参数是通过-XX:PermSize和-XX:MaxPermSize来设定的。

3.虚拟机栈(JVM Stack)

描述的是java方法执行的内存模型:每个方法被执行的时候都会创建一个”栈帧”,用于存储局部变量表(包括参数)、操作栈、方法出口等信息。每个方法被调用到执行完的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。声明周期与线程相同,是线程私有的。栈帧由三部分组成:局部变量区、操作数栈、帧数据区。局部变量区被组织为以一个字长为单位、从0开始计数的数组,和局部变量区一样,操作数栈也被组织成一个以字长为单位的数组。但和前者不同的是,它不是通过索引来访问的,而是通过入栈和出栈来访问的,可以看作为临时数据的存储区域。除了局部变量区和操作数栈外,java栈帧还需要一些数据来支持常量池解析、正常方法返回以及异常派发机制。这些数据都保存在java栈帧的帧数据区中。

局部变量表: 存放了编译器可知的各种基本数据类型、对象引用(引用指针,并非对象本身),其中64位长度的long和double类型的数据会占用2个局部变量的空间,其余数据类型只占1个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量是完全确定的,在运行期间栈帧不会改变局部变量表的大小空间。

4.本地方法栈(Native Stack)

与虚拟机栈基本类似,区别在于虚拟机栈为虚拟机执行的java方法服务,而本地方法栈则是为Native方法服务。(栈的空间大小远远小于堆)

5.程序计数器(PC Register)

是最小的一块内存区域,它的作用是当前线程所执行的字节码的行号指示器,在虚拟机的模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、异常处理、线程恢复等基础功能都需要依赖计数器完成。

6.直接内存

直接内存并不是虚拟机内存的一部分,也不是Java虚拟机规范中定义的内存区域。jdk1.4中新加入的NIO,引入了通道与缓冲区的IO方式,它可以调用Native方法直接分配堆外内存,这个堆外内存就是本机内存,不会影响到堆内存的大小.

2.2 Jconsole,Jvisualvm

微服务-性能压测\缓存redis和分布式锁redisson和SpringCache

休眠: sleep, 等待:wait ,驻留:线程池中空闲的 ,监视: 阻塞的线程,等待锁

下载插件:Visual gc

2.3 汇总各个中间件和服务的内存\cpu使用情况

  1. nginx使用情况

    给阿里云服务器中的docker nginx发送请求,

docker stats
微服务-性能压测\缓存redis和分布式锁redisson和SpringCache
  1. 网关服务,发送localhost:88 端口发送请求,打开jvisualvm 查看cpu,内存情况
  2. 简单服务 /hello ,直接返回一个"hello"
  3. 经过网关,发送一个请求,"/hello" 即:网关+简单服务
  4. 全链路 ,gulimall.com/hello
  5. 首页一级菜单渲染,index.html显示,localhost:10000/ 经过了数据库查询和thymeleaf渲染
  6. 三级分类数据获取, “localhost:10000/index/catelog.json”
  7. 首页全量数据获取 静态css,logo等
    微服务-性能压测\缓存redis和分布式锁redisson和SpringCache

2.4 优化

  1. thymeleaf缓存** ,spring.thymeleaf.cache =true
  2. logging只打印错误日志, logging.level.com.atguigu.gulimall: error
  3. 数据库优化: 给查询的字段加上 索引,
  4. 静态资源,动静分离放到nginx上, 规则:/static/** 所有请求都有nginx直接返回

    在服务器上 /mydata/nginx/html/ 下创建一个 static目录,然后将静态资源放进去.将网页中静态资源请求,改到nginx中的/static 下 , 即gulimall.conf vi插入

    微服务-性能压测\缓存redis和分布式锁redisson和SpringCache
  5. 给堆内存扩大: -Xmx1024m -Xmx1024m -Xmn512m
  6. 业务逻辑优化: 减少数据库查询次数,抽取总数据方法,然后各个方法从这个数据中 分类获取
  7. 缓存和分布式锁:

3.缓存

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

3.1

本地缓存和分布式缓存

微服务-性能压测\缓存redis和分布式锁redisson和SpringCache
  1. 引入springboot的redis依赖,host信息
<dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-redis</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>io.lettuce</groupId>
                    <artifactId>lettuce-core</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
 </dependency>
spring
	redis:
	    host: 127.0.0.1
	    port: 6379
           
  1. 使用Springboot自动配置好的StringRedisTemplate 来操作redis
@Autowired
   private StringRedisTemplate redisTemplate;
   
//TODO 产生堆外内存溢出, Outofdirectmemoryerror :
   //1.springboot 2.0后默认使用了lettuce 操作客户端,使用netty进行网络通信
   //解决方案 -Dio.netty.maxDirectMemory  1.升级lettuce客户端, 2.切换使用jedis
   // lettuce ,jedis 操作redis的底层客户端, spring 又封装了这二者为 redistemplate
   @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;
   }
           

3.2

高并发下缓存失效问题

微服务-性能压测\缓存redis和分布式锁redisson和SpringCache
微服务-性能压测\缓存redis和分布式锁redisson和SpringCache
微服务-性能压测\缓存redis和分布式锁redisson和SpringCache

本地锁

public Map<String, List<Catelog2Vo>> getCatalogJsonFromDb() {
        //加锁,同一把锁,就能锁住这个锁的所有线程
        //TODO 本地锁:synchronized JUC(lock),在分布式情况下,想要锁住所有,使用分布式锁
        //1. synchronized (this)  :springboot 所有的组件在容器都是单例
       synchronized (this) {
            String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");
            //加锁一个,拿到锁, 应该再去缓存中看
            if (!StringUtils.isEmpty(catalogJSON)) {
                Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catelog2Vo>>>() {
                });
                return result;
            }
            List<CategoryEntity> selectList = baseMapper.selectList(null);
            //1.查出所有1级分类
            List<CategoryEntity> level1Categorys = getParent_cid(selectList, 0L);
            //2 封装数据
            Map<String, List<Catelog2Vo>> parent_cid = level1Categorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
                //1.每一个1级下的 所有2级分类
                List<CategoryEntity> categoryEntities = getParent_cid(selectList, v.getCatId());
                List<Catelog2Vo> catelog2Vos = null;
                if (categoryEntities != null) {
                    catelog2Vos = categoryEntities.stream().map(l2 -> {
                        Catelog2Vo catelog2Vo = new Catelog2Vo(v.getCatId().toString(), null, l2.getCatId().toString(), l2.getName());
                        // 2级分类下的 三级分类
                        List<CategoryEntity> level3Catelog = getParent_cid(selectList, l2.getCatId());
                        if (level3Catelog != null) {
                            List<Catelog2Vo.Catelog3Vo> catelog3Vos = level3Catelog.stream().map(l3 -> {
                                Catelog2Vo.Catelog3Vo catelog3Vo = new Catelog2Vo.Catelog3Vo(l2.getCatId().toString(), l3.getCatId().toString(), l3.getName());
                                return catelog3Vo;
                            }).collect(Collectors.toList());
                            catelog2Vo.setCatalog3List(catelog3Vos);
                        }
                        return catelog2Vo;
                    }).collect(Collectors.toList());
                }

                return catelog2Vos;
            }));
            
             //3.将查出数据转为json字符串,,然后放入缓存中
            String s = JSON.toJSONString(parent_cid);
            redisTemplate.opsForValue().set("catalogJSON", s, 1, TimeUnit.DAYS);
          
            return parent_cid;
        }
    }
           

3.3

分布式锁

复制多个 微服务

–server.port=10001

进入全部回话的redis中

docker exec -it redis redis-cli

set lock uuid EX 30 NX //占坑,30s后自动过期删除 (原子性)

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) {
            //加锁成功 ,执行业务
            //设置过期时间 必须和加锁是原子性 同步
            Map<String, List<Catelog2Vo>> dataFormDb;
            try{
                dataFormDb = getDataFormDb();
            }finally {
                String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
                //删除锁
                Longlock1 = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class)
                        , Arrays.asList("lock"), uuid);
            }
            //对比和删除 也要原子性 lua脚本操作
//            String s = redisTemplate.opsForValue().get("lock");
//            if (uuid.equals(s)){
//                redisTemplate.delete("lock"); //删除锁
//            }
            return dataFormDb;

        } else {
            // 等待重试
            Thread.sleep(300);
            return getCatalogJsonFromDbWithRedisLock(); //自旋的方式
        }
    }
           

4. Redisson 分布式锁

<dependency>
         <groupId>org.redisson</groupId>
         <artifactId>redisson</artifactId>
         <version>3.12.0</version>
</dependency>


public class MyRedissonConfig {
 @Bean(destroyMethod="shutdown")
 RedissonClient redisson() throws IOException {
     Config config = new Config();
     config.useSingleServer()
             .setAddress("redis://127.0.0.1:6379");
     return Redisson.create(config);
 }
}
           

4.1

可重入锁(Reentrant Lock)

@Autowired
 RedissonClient redisson;
 @ResponseBody
 @GetMapping("/hello")
 public String hello(){
     //1. 获取一把锁,只要锁的名字一样,就是同一把锁
     RLock lock = redisson.getLock("my-lock");
     //加锁
     lock.lock(); //阻塞式等待
     try {
         System.out.println("加锁成功,执行业务..."+Thread.currentThread().getId());
         Thread.sleep(30000);
     }catch (Exception e){

     }finally {
         //解锁 ,->假设 解锁还未执行,程序死机了, redisson 会不会死锁?
         // 不会,因为 : 1)锁 的自动续期,如果业务超长,运行期间自动给锁上新的30s,不用担心 锁过期自动删掉
         // 2) 加锁的业务只要运行完成,就不会给当前锁续期,即使不手动解锁,30s后也会自动解锁
         // 3)
         lock.unlock(); //解锁
     }
     return "hello";
 }
           

4.2

读写锁(共享锁:读)(排它锁:写)

//保证一定能读到最新数据, 修改期间,写锁是一个排它锁, 读锁是一个共享锁
  //写锁能释放 读锁就必须等待
  // 读+读 :相当于无锁,并发执行.redis 只会记录.
  //写+读  :读要 等待写锁释放
  //写+ 写: 阻塞式方式
  //读+写: 读锁完后,写锁才能执行
  @GetMapping("/write")
  @ResponseBody
  public String writeValue() {
      RReadWriteLock lock = redisson.getReadWriteLock("rw-lock");
      String s = "";
      RLock rLock = lock.writeLock();
      try {
          //1. 改数据加写锁, 读数据加读锁
          rLock.lock();
          System.out.println("写锁加锁成功..." + Thread.currentThread().getId());
          s = UUID.randomUUID().toString();
          Thread.sleep(30000);
          redisTemplate.opsForValue().set("writeValue", s);
      } catch (Exception e) {
          e.printStackTrace();
      } finally {
          rLock.unlock();
          System.out.println("写锁释放: " + Thread.currentThread().getId());
      }
      return s;
  }

  @ResponseBody
  @GetMapping("/read")
  public String readValue(){
      RReadWriteLock lock = redisson.getReadWriteLock("rw-lock");
      String s = "";
      //加读锁
      RLock rLock = lock.readLock();
      rLock.lock();
      try {
          s = redisTemplate.opsForValue().get("writeValue");
      } catch (Exception e) {
          e.printStackTrace();
      } finally {
          rLock.unlock();
      }

      return s;
  }
           

4.3 CountDownLatch 闭锁

/**
  * 放假: 锁门
  * 1班没人了 2班没人了,5个班全部走完,才可以锁大门
  */
 @GetMapping("/lock")
 @ResponseBody
 public String lockDoor() throws InterruptedException {
     RCountDownLatch door = redisson.getCountDownLatch("door");
     door.trySetCount(5);
     door.await(); //等待闭锁都完成
     return "放假了...";
 }
 @GetMapping("/gogo/{id}")
 @ResponseBody
 public String gogo(@PathParam("id") Long id){
     RCountDownLatch door = redisson.getCountDownLatch("door");
     door.countDown(); //计数 -1
     return id+"班的人都走了...";
 }
           

4.4

Semaphore 信号量(阻塞式)

-->分布式限流

/**
    *车库停车
    * 3车位
    */
   @GetMapping("/park")
   @ResponseBody
   public String park() throws InterruptedException {
       RSemaphore park = redisson.getSemaphore("park");
//        park.acquire(); //获取一个信号 (值或占一个车位) 没车位后阻塞式等待
       boolean b = park.tryAcquire(); // 没车位后直接返回false
       if (b){
           //执行业务
       }else {
           return "error";
       }
       return "ok=>"+b;

   }

   @GetMapping("/go")
   @ResponseBody
   public String go(){
       RSemaphore park = redisson.getSemaphore("park");
       park.release(); //释放一个车位
//        Semaphore semaphore = new Semaphore(5);
//        semaphore.release();
       return "ok";
   }
           

4.5

缓存一致性解决

缓存中的数据和数据库保持一致?  缓存一致性
  1) 双写模式 : 写数据库, 写缓存
  2) 失效模式:  redis.del("catalogJSON") ,等待下次主动查询进行增加缓存
  public Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithRedissonLock() {
      //1. 占分布式锁,锁的粒度,越细越好
      // 具体的缓存某个数据,11号商品: product-11-lock ...
      RLock lock = redisson.getLock("CatalogJson-lock");
      lock.lock();
      Map<String, List<Catelog2Vo>> dataFromDb;
      try {
          dataFromDb = getDataFormDb();
      } finally {
          lock.unlock();
      }
      return dataFromDb;
  }
           
微服务-性能压测\缓存redis和分布式锁redisson和SpringCache
微服务-性能压测\缓存redis和分布式锁redisson和SpringCache
微服务-性能压测\缓存redis和分布式锁redisson和SpringCache

5.0 SpringCache

微服务-性能压测\缓存redis和分布式锁redisson和SpringCache

5.1 整合SpringCache

微服务-性能压测\缓存redis和分布式锁redisson和SpringCache