1. 程式人生 > 實用技巧 >分散式高階篇(五) - 商城業務 - 非同步和商品詳情-cnblog

分散式高階篇(五) - 商城業務 - 非同步和商品詳情-cnblog

分散式高階篇(五) - 商城業務 - 商品詳情

非同步&執行緒池

執行緒回顧

初始化執行緒的4種方式

  • 1、繼承Thread

  • 2、實現Runnable介面

  • 3、實現Callable介面 + FutureTask (可以拿到返回結果,可以處理異常)

  • 4、執行緒池

    方式1和方式2:主程序無法獲取執行緒的運算結果,不適合當前場景

    方式3:主程序可以獲取執行緒的運算結果,但是不利於控制伺服器中的執行緒資源。可能導致伺服器資源耗盡

    方式4:通過如下兩種方式初始化執行緒池

    // 【將所有的多執行緒非同步任務交給執行緒池執行】
    // 當前系統中池只有一兩個,每個非同步任務,提交給執行緒池,讓它自己排程
    

// 可以控制資源,效能穩定

//第一種:Executors
//第二種:ThreadPoolExecutor(原生)


通過執行緒池效能穩定,也可以獲取執行結果,並捕獲異常。但是**在業務複雜的情況下,一個非同步呼叫可能會依賴另一個非同步呼叫的執行結果**

#### 執行緒池的7大引數

* ThreadPoolExecutor

![image-20210119113734089](https://img2020.cnblogs.com/blog/1875400/202101/1875400-20210121151307073-240937379.png)

![image-20210119152305430](https://img2020.cnblogs.com/blog/1875400/202101/1875400-20210121151306847-114976845.png)



* 核心執行緒數:corePoolSize(只要執行緒池不銷燬,就會一直存在,除非設定了 `allowCoreThreadTimeOut`)

  執行緒池建立好以後,就準備就緒的執行緒數量,就等待非同步任務來執行

* 最大執行緒數量:maximumPoolSize(控制資源)

* 存活時間:keepAliveTime

  如果當前執行緒數量大於核心執行緒數,只要**執行緒空閒時間大於指定的keepAliveTime**,釋放空閒執行緒(maximumPoolSize - corePoolSize)

* TimeUnit unit

  keepAliveTime的時間單位

* 阻塞佇列:BlockingQueue<Runnable> workQueue

  如果任務有很多,就會將目前多的任務放在佇列裡。只要執行緒空閒,就會去佇列裡取出新的任務繼續執行

* 執行緒的建立工廠:threadFactory

* RejectedExecutionHandler  handler:如果佇列滿了,按照我們指定的拒絕策略拒絕執行任務

##### 執行流程(*)

1. 執行緒池建立,準備好core(核心執行緒數)數量的核心執行緒數,準備接受任務
2. 新的任務進來,用core準備好的空閒執行緒執行
 * 核心執行緒數滿了,將再進來的任務放入阻塞佇列中,空閒的core會自動去阻塞佇列獲取任務執行
 * 阻塞佇列滿了,就直接開新的執行緒執行,最大隻能開到max指定的數量
 * max都執行好了,Max-core個數的空閒執行緒會在 `keepAliveTime`指定的時間後自動銷燬,最終保持到core大小
 * 如果執行緒數開到了max數量,還有新任務進來,就會使用`RejectedExecutionHandler` 指定的拒絕策略進行處理
3. 索引的執行緒建立都是由指定的factory建立的

##### 執行緒池設計題

* 一個執行緒池 core:7、max:20、queue:50,、這時候100併發進來怎麼分配?

答:7個會立即執行,50個會進入阻塞佇列,再開13個執行緒進行執行,總計70個,剩下的30個就會使用拒絕策略進行處理(一般使用拋棄策略AbortPolicy,如果不想拋棄還想繼續執行,可以使用CallerRunsPolicy,以同步的方式執行)

#### 常見的4種執行緒池

* newCachedThreadPool(core是0,所有都可回收)

建立一個可快取執行緒池,如果執行緒池長度超過處理需要,可靈活回收空閒執行緒,若無可回收,則新建執行緒

* newFixedThreadPool(core==max)

建立一個定長執行緒池,可控制執行緒最大併發數,超出的執行緒會在佇列中等待

* newScheduledThreadPool

建立一個定長執行緒池,支援定時以及週期性任務執行

* newSingleThreadExecutor 

建立一個單執行緒化的執行緒池,它只會用唯一的工作執行緒來執行任務,保證所有任務順序執行

#### 為什麼使用執行緒

* 降低資源的消耗

* 通過重複利用已經建立好的執行緒降低執行緒的建立和銷燬帶來的損耗

* 提高響應速度

* 因為執行緒池中的執行緒數沒有超過執行緒池的最大上限時,有的執行緒處於等待分配任務的狀態,當任務來時無需建立新的執行緒就能執行

* 提高執行緒的可管理性

* 執行緒池會根據當前系統特點對池內的執行緒進行優化處理,減少建立和銷燬執行緒帶來的系統開銷,無限的建立和銷燬執行緒不僅消耗系統資源,還降低系統的穩定性,使用執行緒池進行統一分配

* **例子**:以單核CPU為例,不使用執行緒池的情況下:假設當前有1000個執行緒同時執行,看起來是同時執行,但CPU需要拿到它的**時間片**才會執行。cpu需要再1000個執行緒上來回切換、執行緒恢復,**整個過程非常耗費資源**

如果是以執行緒池的方式控制,最多隻有max個執行緒同時執行,cpu的時間片切換和執行緒恢復也只需要在max個之間進行,降低了資源消耗,提高了效應速度

### CompletableFuture非同步編排

* 業務場景:

查詢商品詳情頁的邏輯比較複雜,有些資料還需要遠端呼叫,必然需要花費更多的時間

```java
//1、獲取sku的基本資訊 0.5s
//2、獲取sku的圖片資訊 0.5s
//3、獲取sku的促銷資訊 1s
//4、獲取spu的所有銷售屬性 1s
//5、獲取規格引數、組以及組下的規格引數 1.5s
//6、spu詳情 1s

假如商品詳情頁的每個查詢,需要如下標註的時間才能完成

那麼使用者需要5.5s後才能看到商品詳情頁的內容,很顯然是不能接受的

如果有多個執行緒同時完成這6步操作,也許只需要1.5s即可完成響應

建立非同步物件

  • CompletableFuture 提供了四個靜態方法來建立一個非同步操作

    • 1、runXXX都是沒有返回結果的,supplyXXX都是可以獲取返回結果的

    • 2、可以傳入自定義的執行緒池,否則就用預設的執行緒池

計算完成時回撥方法

  • whenComplete 和 exceptionally

    • whenComplete 可以處理正常和異常的計算結果,whenComplete 處理異常情況

    • whenComplete 和 whenCompleteAsync 的區別:

      whenComplete :是執行當前任務的執行緒繼續執行 whenComplete 的任務

      whenCompleteAsync : 是執行把 whenCompleteAsync 這個任務繼續提交給執行緒池來執行

    • exceptionally:感知異常、處理異常,並可以設定返回的預設值

    方法不以Async結尾,意味著Action使用相同的執行緒執行,而Async可能會使用其他執行緒執行(如果是使用相同的執行緒池,也可能會被同一個執行緒選中執行)

  • 舉例說明

handle方法

  • hanlde

    whenComplete 是方法執行完成後的處理

    handle 是無論方法成功還是失敗,執行的處理(可以感知異常、處理異常,改變返回值)

  • 舉例說明

執行緒序列化方法

  • 序列化

    thenRun / thenRunAsync :只要上一個任務執行完成,就開始執行thenRun,只是執行完成後,執行thenRun的後續邏輯

    thenAccept / thenAcceptAsync:消費處理結果;接收任務的處理結果,並消費結果,無返回結果

    thenApply / thenApplyAsync:當一個執行緒依賴另一個執行緒時,獲取上一個任務返回的結果,並返回當前任務的返回值

    帶有Async 預設是非同步執行的,不是使用同一個執行緒完成

  • 舉例說明

兩任務組合 - 都要完成

  • 兩個任務必須都完成,觸發該任務

    • thenCombine:組合兩個future,獲取兩個future的返回結果,並返回當前任務的返回值
    • thenAcceptBoth:組合兩個future,獲取兩個future任務的返回結果,然後處理任務,沒有返回值
    • runAfterBoth:組合兩個future,不需要獲取兩個future的結果,只需要在兩個future處理完成後,處理該任務

  • 舉例說明

兩任務組合 - 一個完成

  • 當兩個任務中,任意一個future任務完成的時候,執行任務

    • applyToEither:兩個任務有一個執行完成,獲取它的返回值,處理任務並有新的返回值
    • acceptEither:兩個任務有一個執行完成,獲取它的返回值,處理任務,沒有新的返回值
    • runAfterEither:兩個任務有一個執行完成,獲取它的返回值,不需要獲取future的返回值,處理任務,也沒用返回值

  • 舉例說明

多工組合

  • allOf:等待所有任務完成

  • anyOf:只要有一個任務完成

  • 舉例說明

商品詳情

搭建商品詳情頁面環境

  • 新增詳情頁的域名 修改hosts檔案

    C:\Windows\System32\drivers\etc\hosts

  • 新增nginx配置

    item.mall.com 包含在現有的規則中,可以不用修改(*.mall.com

  • 新增閘道器路由規則(配置完成,重啟閘道器服務)

    瀏覽器輸入 item.mall.com -->nginx -->轉發給 gateway --> 商品服務

  • 瀏覽器輸入:item.mall.com 測試

  • 靜態資源 存放到nginx

    拷貝到 nginx/html/static/item 目錄下

    修改 詳情頁.html 的所有靜態資源請求路徑

  • 修改檢索服務,商品點選的跳轉為:

  • 最終效果

查詢商品詳情

商品詳情資料模型抽取

  • 頁面需要的所有屬性

    //1、sku基本資訊獲取 pms_sku_info
    //2、sku的圖片資訊 pms_sku_images
    //3、獲取spu的銷售屬性
    //4、獲取spu的介紹
    //5、獲取spu的規則引數資訊
    

查詢商品詳情

  • 查出當前spu對應的所有屬性的分組資訊,以及屬性對應的值

    • sql語句

      # 使用別名,方便結果封裝成我們所需要的物件
      SELECT 
      ppav.spu_id,
      pag.attr_group_name  groupName,
      pag.attr_group_id,
      paar.attr_id attrId,
      pa.attr_name atrrName, 
      ppav.attr_value attrValue
      FROM `pms_attr_group`  pag  
      LEFT JOIN pms_attr_attrgroup_relation paar on pag.attr_group_id=paar.attr_group_id
      LEFT JOIN pms_attr pa ON pa.attr_id=paar.attr_id
      LEFT JOIN pms_product_attr_value ppav ON ppav.attr_id=paar.attr_id
      WHERE pag.catelog_id=225 AND ppav.spu_id=10
      
    • xml

       <!--只要有巢狀結果,就需要封裝自定義結果集-->
          <resultMap id="spuItemGroupAttrVo" type="com.touch.air.mall.product.vo.SpuItemGroupAttrVo">
              <result property="groupName" column="attr_group_name"></result>
              <collection property="attrs" ofType="com.touch.air.mall.product.vo.SpuBaseAttrVo">
                  <result property="attrValue" column="attr_value"></result>
                  <result property="attrName" column="attr_name"></result>
              </collection>
          </resultMap>
          <select id="getAttrGroupWithAttrsBySpuId"
                  resultMap="spuItemGroupAttrVo">
              SELECT
              ppav.spu_id,
              pag.attr_group_name,
              pag.attr_group_id,
              paar.attr_id ,
              pa.attr_name,
              ppav.attr_value
              FROM `pms_attr_group`  pag
              LEFT JOIN pms_attr_attrgroup_relation paar on pag.attr_group_id=paar.attr_group_id
              LEFT JOIN pms_attr pa ON pa.attr_id=paar.attr_id
              LEFT JOIN pms_product_attr_value ppav ON ppav.attr_id=paar.attr_id
              WHERE pag.catelog_id=#{catalogId} AND ppav.spu_id=#{spuId}
          </select>
      
  • 獲取spu的銷售屬性

    • sql語句

      #傳入spuId
      # 1、分析當前spu的有多少個sku,所有sku涉及到的屬性組合
      SELECT  
      pssav.attr_id attrId,
      pssav.attr_name attrName,
      GROUP_CONCAT(DISTINCT pssav.attr_value) attrValue
      FROM pms_sku_info psi
      LEFT JOIN pms_sku_sale_attr_value pssav ON pssav.sku_id=psi.sku_id
      WHERE psi.spu_id=2
      GROUP BY pssav.attr_id,pssav.attr_name
      
    • xml

       <select id="getSaleAttrsBySpuId" resultType="com.touch.air.mall.product.vo.SkuItemSaleAttrVo">
              SELECT
              pssav.attr_id attrId,
              pssav.attr_name attrName,
              GROUP_CONCAT(DISTINCT pssav.attr_value) attrValue
              FROM pms_sku_info psi
              LEFT JOIN pms_sku_sale_attr_value pssav ON pssav.sku_id=psi.sku_id
              WHERE psi.spu_id=#{spuId}
              GROUP BY pssav.attr_id,pssav.attr_name
          </select>
      
  • 單元測試

詳情頁渲染

  • 頁面標籤位置

    • 標題 :class="box-name"
    • 副標題:class="box-hide"
    • 大圖:class="probox"
    • 大圖放大:class="showbox"
    • 小圖:class="box-lh-one"
    • 價格:class="box-summary clear"
    • 有貨無貨
    • 選擇屬性(顏色、版本、記憶體):class="box-attr-3"
    • 商品介紹:class="jieshoa actives"
    • 規格包裝:class="baozhuang actives"

銷售屬性渲染(切換)

  • 渲染需求:根據選擇的機身顏色,記憶體,切換不同商品的詳情

    分析:修改上一步:獲取spu的銷售屬性 需要獲取每種組合的skuId

  • 改造 獲取spu的銷售屬性

    • sql語句

      #傳入spuId
      # 1、分析當前spu的有多少個sku,所有sku涉及到的屬性組合
      SELECT  
      pssav.attr_id attrId,
      pssav.attr_name attrName,
      pssav.attr_value attrValue,
      GROUP_CONCAT(DISTINCT psi.sku_id) skuIds
      FROM pms_sku_info psi
      LEFT JOIN pms_sku_sale_attr_value pssav ON pssav.sku_id=psi.sku_id
      WHERE psi.spu_id=2
      GROUP BY attrId,attrName,attrValue
      

    • mapper.SkuSaleAttrValueDao.xml

      <resultMap id="skuItemSaleAttrVo" type="com.touch.air.mall.product.vo.SkuItemSaleAttrVo">
              <result column="attrId" property="attrId"></result>
              <result column="attrName" property="attrName"></result>
              <collection property="attrValue" ofType="com.touch.air.mall.product.vo.AttrValueWithSkuIdVo">
                  <result column="attrValue" property="attrValue"></result>
                  <result column="skuIds" property="skuIds"></result>
              </collection>
          </resultMap>
          <select id="getSaleAttrsBySpuId" resultMap="skuItemSaleAttrVo">
              SELECT
              pssav.attr_id attrId,
              pssav.attr_name attrName,
              pssav.attr_value attrValue,
              GROUP_CONCAT(DISTINCT psi.sku_id) skuIds
              FROM pms_sku_info psi
              LEFT JOIN pms_sku_sale_attr_value pssav ON pssav.sku_id=psi.sku_id
              WHERE psi.spu_id=#{spuId}
              GROUP BY attrId,attrName,attrValue
          </select>
      
    • 單元測試

  • 重新渲染頁面

    • 選擇屬性(顏色、版本、記憶體):class="box-attr-3"

    • 回顯當前商品屬性

SKU屬性切換商品

  • 切換js實現

    //給checked 加上顏色
    $(function () {
    	$("a[class='sku_attr_value']").parent().css({"border": "solid 1px #CCC"});
    	$("a[class='sku_attr_value checked']").parent().css({"border": "solid 1px red"});
    })
    
    $(".sku_attr_value").click(function () {
    	//1、獲取到所有checked 當前的skus組合
    	// 點選的元素先新增上自定義的屬性,為了識別我們是剛才被點選的
    	//注意:當前選擇所在的屬性行,以clicked屬性為準,其他屬性行 以checked為準
    	let skus = new Array();
    	$(this).addClass("clicked");
    	let cur = $(this).attr("skus").split(',');
    	skus.push(cur);
    	//去掉clicked 同一行的所有 checked
    	$(this).parent().parent().find(".sku_attr_value").removeClass('checked')
    	//找到其他銷售屬性行的skus
    
    	$("a[class='sku_attr_value checked']").each(function () {
    		skus.push($(this).attr("skus").split(','));
    	})
    	//console.log(skus);
    	//2、取出交集,得到skuId
    	// let skuId = $(skus[0]).filter(skus[1])[0];
    	let filterEle = skus[0];
    	for (var i = 1; i < skus.length; i++) {
    		filterEle = $(filterEle).filter(skus[i]);
    	}
    	// console.log(filterEle[0]);
    	//3、跳轉
    	location.href = "http://item.mall.com/"+filterEle[0]+".html";
    

非同步編排優化

配置執行緒池

  • 引數可配置

商品詳情非同步編排

  • 非同步編排目的,提高系統資源利用率,加速查詢,提高吞吐量等

  • 商品詳情非同步思路

    //1、sku基本資訊獲取 pms_sku_info
    //2、sku的圖片資訊 pms_sku_images
    //3、獲取spu的銷售屬性
    //4、獲取spu的介紹
    //5、獲取spu的規則引數資訊
    
    //查詢1和查詢2 互不相關 :兩個非同步任務
    //查詢3、4、5都依賴於查詢1的結果 :查詢1.thenAccept()