天天看點

微服務-性能壓測\緩存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