穀粒商城高階篇筆記1

烏拉1nl發表於2020-12-18


0、ElasticSearch


1、Nginx配置域名問題


01、Nginx(反向代理) 配置


> 正向代理和反向代理

Nginx6


配置反向代理

我們打算訪問 wulawula.com 網址轉到我們本地的localhost:10000

1、使用 SwitchHosts 工具 配置域名 使用管理員方式開啟

Nginx1

2、使用 自定義的域名訪問 預設訪問的是Nginx自定義的html頁面

Nginx2

3、開啟 nginx.conf 路徑:/mydata/nginx/conf/nginx.conf

nginx.conf的介紹圖:

在這裡插入圖片描述

發現把server塊放到了 conf.d資料夾下

http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';
    access_log  /var/log/nginx/access.log  main;
    sendfile        on;
    #tcp_nopush     on;
    keepalive_timeout  65;
    #gzip  on;
    include /etc/nginx/conf.d/*.conf;
}

4、開啟 default.conf 路徑:/mydata/nginx/conf/conf.d/default.conf

修改以下箭頭部分

server {
    listen       80;
    server_name  wulawula.com;  <------

    #charset koi8-r;
    #access_log  /var/log/nginx/log/host.access.log  main;

    location / {
    	proxy_pass http://192.168.56.1:10000;	<------ 注意分號結尾
    }

    #error_page  404              /404.html;

    # redirect server error pages to the static page /50x.html
    #

重啟nginx docker restart nginx

5、訪問 wulawula.com 發現訪問的是我們原來的localhost:10000的頁面

在這裡插入圖片描述


02、Nginx(負載均衡)+ 閘道器 配置


1、開啟 nginx.conf 路徑:/mydata/nginx/conf/nginx.conf

http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    keepalive_timeout  65;

    #gzip  on;
    
    upstream gulimall{			   <------ 上游伺服器組 名稱為 gulimall
        server 192.168.56.1:88;    <------ 配置伺服器 (此處配置閘道器)
    }							   <------

    include /etc/nginx/conf.d/*.conf;
}   

2、開啟 default.conf 路徑:/mydata/nginx/conf/conf.d/default.conf

server {
    listen       80;
    server_name  wulawula.com;	<------

    #charset koi8-r;
    #access_log  /var/log/nginx/log/host.access.log  main;

    location / {
        proxy_pass http://gulimall;  <------找到上游伺服器組,此處上有伺服器組的名稱為 gulimall
    }

    #error_page  404              /404.html;

    # redirect server error pages to the static page /50x.html

3、增加gateway配置檔案的閘道器規則

注意:此規則一定要放到其他所有規則的最後面

# nginx + gateway 配置域名訪問 wulawula.com
- id: gulimall_host_route
	# lb 代表的就是負載均衡
	uri: lb://gulimall-product
	predicates:
		- Host=**.wulawula.com

4、然後發現報錯404 原因:nginx代理給閘道器的時候,會丟失請求的host資訊 需要新增 proxy_set_header Host $host;

server {
    listen       80;
    server_name  wulawula.com;

    #charset koi8-r;
    #access_log  /var/log/nginx/log/host.access.log  main;

    location / {
        proxy_set_header Host $host;	<------
        proxy_pass http://gulimall;
    }

重啟nginx docker restart nginx

5、訪問 wulawula.com 發現訪問的是我們原來的localhost:10000的頁面

在這裡插入圖片描述


原理圖

在這裡插入圖片描述


03、Nginx動靜分離


1、在html資料夾下 新建static資料夾 路徑: /mydata/nginx/html/static

2、把靜態資源放到了 static下 【static例包含index index下包含js、css、img】 把專案resources/static的靜態資源刪除

3、修改 default.conf 路徑: /mydata/nginx/conf/conf.d/default.conf

動靜分離1

4、重啟 nginx docker restart nginx

5、訪問 wulawula.com 靜態資源正常顯示

動靜分離2


2、JMeter 壓力測試


影響效能考慮點包括:
  • 資料庫、應用程式、中介軟體( tomact、Nginx)、網路和作業系統等方面首先考慮自己的應用屬於CPu密集型還是I0密集型

01、基本測試


1、建立一個 **執行緒組**

JMeter1

2、設定執行緒組引數 所謂執行緒數就是併發使用者數

JMeter2

3、線上程組下建立 HTTP請求

JMeter3

4、設定 HTTP引數(此處以百度為例)

在這裡插入圖片描述

5、線上程組下建立 監聽器 此處以:檢視結果樹、彙總報告、聚合報告、彙總圖 為例

JMeter4

6、點選執行 執行完後 自動停止

JMeter11

點選執行完後可以看到資料

  • 檢視結果樹
  • 可以發現每次 HTTP請求都完成

JMeter6

  • 彙總報告 單位毫秒
  • 平均1.241s 最小0.015s 最大5.265s 異常0%

JMeter7

  • 聚合報告 單位毫秒
  • 90%的在 1.465s完成 95%的在1.649s完成 99%的在2.514s完成

在這裡插入圖片描述

  • 彙總圖 單位毫秒
  • 可以看到圖表資訊

JMeter9

在這裡插入圖片描述

注意:自己寫的專案。壓測資料的時候 測試的資料和此處的記憶體大小也有關係

在這裡插入圖片描述


02、嘗試用大於5000的TCP埠連線時發生錯誤


JMeter Address Already in use錯誤解決 `地址被佔用`

原因

windows本身提供的埠訪問機制的問題。

Windows提供給TCP/IP連結的埠為1024-5000,並且要四分鐘來迴圈回收他們。就導致我們在短時間內跑大量的請求時將埠占滿了。

在這裡插入圖片描述

在這裡插入圖片描述

解決


1.cmd中,用regedit命令開啟登錄檔

2.在 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters 下,

​ 1.右擊parameters,新增一個新的 DWORD,名字為 MaxUserPort

JMeter15

​ 2 .然後雙擊MaxUserPort,輸入數值資料為65534,基數選擇十進位制(如果是分散式執行的話,控制機器和負載機器都需要這樣操作哦)

JMeter16

也可以設定這個:

  • 右擊parameters,新增一個新的 DWORD,名字為 TCPTimedWaitDelay
  • 雙擊TCPTimedWaitDelay 輸入數值資料為 30 (原來的回收時間是4分鐘,此處的含義為回收時間改為30秒

在這裡插入圖片描述

3.修改配置完畢之後記得重啟機器才會生效


3、效能監控


01、jconsole


1、cmd開啟命令視窗 輸入 jconsole 回車

在這裡插入圖片描述

2、選擇我們想要檢視的 微服務 ,此處以紅色箭頭處為例

在這裡插入圖片描述


02、jvisualvm


1、cmd開啟命令視窗 輸入 jvisualvm回車

在這裡插入圖片描述

2、選擇我們想要檢視的 微服務 ,此處以紅色箭頭處為例

在這裡插入圖片描述

  • 執行:正在執行的
  • 休眠:呼叫sleep方法的
  • 等待:呼叫wait方法的
  • 駐留:執行緒池裡面的空閒執行緒
  • 監視:阻塞的執行緒,正在等待鎖的

擴充套件外掛

1、點選 工具 -> 外掛 -> 可用外掛 -> 檢查最新外掛

在這裡插入圖片描述

2、安裝完成後 退出 重新開啟 jvisualvm

在這裡插入圖片描述


擴充套件外掛 報錯

如果點選 檢查最新版本 時有以下錯誤

在這裡插入圖片描述

解決:

  • 1、cmd檢視自己的 jdk 的版本 java -version 舉例:此次電腦上的jdk版本為 java version “1.8.0_231”
  • 2、開啟 https://visualvm.github.io/pluginscenters.html 找到自己的 jdk對應的版本 點選連結進去

jvisualvm4

  • 3、複製 此處URL

jvisualvm5

  • 4、複製到此處 注意URL的字尾一定是 updates.xml.gz

在這裡插入圖片描述


03、jvisualvm + JMeter 結合測試

測試截圖資料略…


因為我們的這個測試專案的構架是這樣的:

請求 -> Nginx -> GateWay -> 服務叢集的商品服務 --- 服務處理完成 -> GateWay -> Nginx -> 請求傳送者

所以我們需要先測試中介軟體 Nginx GateWay

測試Nginx

Nginx的埠為80 本地Linux地址為 192.168.56.10 壓力測試為1秒200個執行緒迴圈次數為無限

測試GateWay

Nginx的埠為88 地址為 localhost 壓力測試為1秒200個執行緒迴圈次數為無限

開始壓力測試 發現GateWay的CPU佔用率高

測試簡單服務

//測試簡單服務 用於調優
@ResponseBody
@GetMapping("/hello")
public String hello(){
  return "hello";
}

簡單服務的埠為10000 地址為 localhost http://localhost:10000/hello 壓力測試為1秒200個執行緒迴圈次數為無限

測試GateWay+簡單服務

  • 加入閘道器配置yml
- id: product_route
	uri: lb://gulimall-product
    predicates:
		- Path=/api/product/**,/hello  
    filters:
    	- RewritePath=/api/(?<segment>.*),/$\{segment}  #路徑重寫

GateWay+簡單服務的埠為88 地址為localhost http://localhost:88/hello 壓力測試為1秒200個執行緒迴圈次數為無限

全鏈路

GateWay + Nginx + 簡單服務

全鏈路的埠為80 地址為wulawula.com http://wulawula.com/hello 壓力測試為1秒200個執行緒迴圈次數為無限

頁面一級選單渲染

頁面一級選單渲染的埠為10000 地址為localhost 壓力測試為1秒200個執行緒迴圈次數為無限

需要渲染前臺。慢

三級分類資料獲取

三級分類資料獲取的埠為10000 http://localhost:10000/index/catalog.json 地址為localhost 壓力測試為1秒200個執行緒迴圈次數為無限

需要反覆查資料庫。慢

首頁全量資料獲取

首頁全量資料獲取的埠為10000 http://localhost:10000 地址為localhost 壓力測試為1秒200個執行緒迴圈次數為無限

高階 如下設定: 並行下載可以限制一下 預設為6

首頁全量資料獲取 (開快取、優化資料庫、關日誌) 優化

  • 開啟快取 調整日誌級別
#開啟快取
spring  
  thymeleaf:
	cache: true
      
#調整日誌級別
logging:
  level:
    com.wulawula.gulimall: error
  • 新增索引

4、優化


Nginx動靜分離

  • 1.03、有詳細

首頁全量資料獲取 (開快取、優化資料庫、關日誌) 優化

  • 開啟快取 調整日誌級別
#開啟快取
spring  
  thymeleaf:
	cache: true
      
#調整日誌級別
logging:
  level:
    com.wulawula.gulimall: error
  • 新增索引

優化測試22

設定JVM

-Xmx1024m -Xms1024m -Xmn512m

  • -Xms:JVM 初始分配的記憶體 -> 數值大點,程式會啟動的快一點,但是也可能會導致機器暫時間變慢。
  • -Xmx:JVM最大可用記憶體 -> 如果程式執行需要佔用更多的記憶體,超出了這個設定值,會丟擲OutOfMemory異常
  • -Xss: 每個執行緒的堆疊大小 -> 根據應用的執行緒所需記憶體大小進行調整。
  • -Xmn:年輕代大小 -> 整個JVM記憶體大小=年輕代大小 + 年老代大小 + 持久代大小

3級分類優化業務

  • 優化: 抽取方法 —> 將資料庫的多次查詢變為1次
//處理2級和3級
    @Override
    public Map<String,List<Catelog2Vo>> getCatalogJson() {

        /*
        *   【優化】
        *   1、將資料庫的多次查詢變為1次
        */
        List<CategoryEntity> selectList = baseMapper.selectList(null);

        //1、查出所有一級分類
        List<CategoryEntity> level1Categorys = getParent_cid(selectList,0L);

        //2、封裝1級的資料                                                                         // k:一級分類的id
        Map<String, List<Catelog2Vo>> parent_cid = level1Categorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {

//-------------------------------二級---------------------------------------
            //1、通過每一個的一級分類,查到這個一級分類的二級分類                                                              //v:當前遍歷的一級分類
            List<CategoryEntity> level2Catelog = getParent_cid(selectList,v.getCatId()); //二級分類的parent_cid 【eq】 一級分類的catid

            //2、封裝2級的資料
            List<Catelog2Vo> catelog2Vos = null;

            /*2級分類的集合*/
            if (level2Catelog != null) {
                catelog2Vos = level2Catelog.stream().map(l2 -> {
                    /***
                     * v.getCatId()  一級分類的id
                     * catalo3List在下面
                     * l2.getCatId() 二級分類的id
                     * l2.getName()  二級分類的名字
                     */
                    Catelog2Vo catelog2Vo = new Catelog2Vo(v.getCatId().toString(), null, l2.getCatId().toString(), l2.getName());//封裝

//-------------------------------三級---------------------------------------
                    //1、找當前二級分類的三級分類封裝成VO
                    List<CategoryEntity> level3Catelog = getParent_cid(selectList,l2.getCatId()); //三級分類的parent_cid 【eq】 二級分類的catid
                    if (level3Catelog != null){
                        /*3及分類的集合*/
                        List<Catelog2Vo.Catelog3Vo> collect = level3Catelog.stream().map(l3 -> {
                            //2、封裝成指定格式
                            /***
                             * item.getCatId() 2級節點的id
                             * l3.getCatId() 當前3級節點的id
                             * l3.getName()  當前3級節點的名字
                             */
                            Catelog2Vo.Catelog3Vo catelog3Vo = new Catelog2Vo.Catelog3Vo(l2.getCatId().toString(),l3.getCatId().toString(),l3.getName()); //封裝

                            return catelog3Vo;
                        }).collect(Collectors.toList());
                        catelog2Vo.setCatalog3List(collect);
                    }

                    return catelog2Vo;
                }).collect(Collectors.toList());
            }
            return catelog2Vos;
        }));
        return parent_cid;
    }

    //抽取方法
    /*
    *   List<CategoryEntity> selectList 從這個裡面查詢
    *   Long parent_cid 條件
    */
    private List<CategoryEntity> getParent_cid(List<CategoryEntity> selectList,Long parent_cid) {

        List<CategoryEntity> collect = selectList.stream().filter(item -> item.getParentCid().equals(parent_cid)).collect(Collectors.toList());
        return collect;

//        return baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", v.getCatId()));
    }

優化1

快取Redis

壓測內容壓測執行緒數吞吐量/s90%響應時間99%響應時間
Nginx2001363.761949
GateWay2005070.1251340
簡單服務20011596.63770
頁面一級選單渲染200290.78041443
三級分類資料獲取20010.7(db)1857418610
三級分類資料獲取(優化業務)200131.622203781
三級分類(Redis)200408.56341374
首頁全量資料獲取20011.6(靜態)2347724138
首頁全量(開快取、優化資料庫、關日誌)20058.81543314579
Nginx+GateWay200
GateWay+簡單服務2001780.21401975
全鏈路200567.115864956

中介軟體越多,效能損失越大,大多都損失在網路互動了

業務:DB(MySql優化)、模板的渲染速度(上線需要開啟快取)、靜態資源 影響


5、快取


為了系統效能的提升,我們一般都會將部分資料放入快取中,加速訪間。而db承擔資料落盤工作。

哪些資料適合放入快取:

  • 即時性、資料一致性要求不高的
  • 訪問量大且更新頻字不高的資料(讀多,寫少)

舉例:電商類應用,商品分類,商品列表等適合快取並加一個失效時間(根據資料更新頻率來定),後臺如果釋出一個商品,買家需要5分鐘才能看到新的商品一般還是可以接受的。

快取1


01、本地快取

不推薦

在這裡插入圖片描述

/* 【優化2  將資料庫的多次查詢變為1次 且加入本地快取 】 --------------------------------------------------------------------------------------------*/
    //處理2級和3級
    @Override
    public Map<String,List<Catelog2Vo>> getCatalogJson() {

        /* 自定義快取  本地快取 */
        //1、如果快取中有 就用快取的
        Map<String, List<Catelog2Vo>> catalogJson = (Map<String, List<Catelog2Vo>>) cache.get("catalogJson");
        //2、如果沒有就查資料庫   且最後給快取放一份
        if (catalogJson == null) {

            /*
             *   【優化】
             *   1、將資料庫的多次查詢變為1次
             */
            List<CategoryEntity> selectList = baseMapper.selectList(null);

            //1、查出所有一級分類
            List<CategoryEntity> level1Categorys = getParent_cid(selectList, 0L);

            //2、封裝1級的資料                                                                         // k:一級分類的id
            Map<String, List<Catelog2Vo>> parent_cid = level1Categorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {

//-------------------------------二級---------------------------------------
                //1、通過每一個的一級分類,查到這個一級分類的二級分類                                                              //v:當前遍歷的一級分類
                List<CategoryEntity> level2Catelog = getParent_cid(selectList, v.getCatId()); //二級分類的parent_cid 【eq】 一級分類的catid

                //2、封裝2級的資料
                List<Catelog2Vo> catelog2Vos = null;

                /*2級分類的集合*/
                if (level2Catelog != null) {
                    catelog2Vos = level2Catelog.stream().map(l2 -> {
                        /***
                         * v.getCatId()  一級分類的id
                         * catalo3List在下面
                         * l2.getCatId() 二級分類的id
                         * l2.getName()  二級分類的名字
                         */
                        Catelog2Vo catelog2Vo = new Catelog2Vo(v.getCatId().toString(), null, l2.getCatId().toString(), l2.getName());//封裝

//-------------------------------三級---------------------------------------
                        //1、找當前二級分類的三級分類封裝成VO
                        List<CategoryEntity> level3Catelog = getParent_cid(selectList, l2.getCatId()); //三級分類的parent_cid 【eq】 二級分類的catid
                        if (level3Catelog != null) {
                            /*3及分類的集合*/
                            List<Catelog2Vo.Catelog3Vo> collect = level3Catelog.stream().map(l3 -> {
                                //2、封裝成指定格式
                                /***
                                 * item.getCatId() 2級節點的id
                                 * l3.getCatId() 當前3級節點的id
                                 * l3.getName()  當前3級節點的名字
                                 */
                                Catelog2Vo.Catelog3Vo catelog3Vo = new Catelog2Vo.Catelog3Vo(l2.getCatId().toString(), l3.getCatId().toString(), l3.getName()); //封裝

                                return catelog3Vo;
                            }).collect(Collectors.toList());
                            catelog2Vo.setCatalog3List(collect);
                        }
                        return catelog2Vo;
                    }).collect(Collectors.toList());
                }
                return catelog2Vos;
            }));
            //3、給快取放一份
            cache.put("catalogJson",parent_cid);
            return parent_cid;
        }
    }

    //抽取方法
    /*
     *   List<CategoryEntity> selectList 從這個裡面查詢
     *   Long parent_cid 條件
     */
    private List<CategoryEntity> getParent_cid(List<CategoryEntity> selectList,Long parent_cid) {

        List<CategoryEntity> collect = selectList.stream().filter(item -> item.getParentCid().equals(parent_cid)).collect(Collectors.toList());
        return collect;

//        return baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", v.getCatId()));
    }

02、分散式快取 Redis

推薦

快取3

  • pom
<!--引入Redis-->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

優化3:Redis 【注意:value轉為了JSON字串存取】

【注意:因為給Redis存入的是JSON字串,取出JSON字串的時候需要逆轉為能用的物件型別 --> 序列化與反序列化】

Alibaba.fastJson

  • 物件 -> JSON:
String str = JSON.toJSONString(infoDo);
  • JSON -> 物件:
InfoDo infoDo = JSON.parseObject(strInfoDo, InfoDo.class);
  • 物件集合 -> JSON:
String users = JSON.toJSONString(users);
  • JSON -> 物件集合:
List<User> userList = JSON.parseArray(userStr, User.class);
  • 這樣寫會出現問題 注意看 18、03、 堆外記憶體溢位問題
 /*
 *   優化3:Redis  【注意:value轉為了JSON字串存取】
 *	【注意:因為給Redis存入的是JSON字串,取出JSON字串的時候需要逆轉為能用的物件型別 --> 序列化與反序列化】
 */
    @Override
    public Map<String,List<Catelog2Vo>> getCatalogJson() {

        //1、加入快取邏輯
        //從Redis中獲取快取
        String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");
        //判斷是否為空
        if (StringUtils.isEmpty(catalogJSON)){
            //2、如果快取中沒有資料
            //查詢資料庫
            Map<String, List<Catelog2Vo>> catalogJsonDb = getCatalogJsonDB();
            //3、查到的資料再放入快取,
            //因為用的是StringRedisTemplate 接受的value值是一個String 所以 catalogJsonDb 不能直接放入,需要轉換一下
            //將查到的物件轉為 JSON 放入快取中
            /* 快取中存的是 JSON 字串,優點:JSON 有跨語言跨平臺的特點 */
            String s = JSON.toJSONString(catalogJsonDb);
            redisTemplate.opsForValue().set("catalogJSON",s);
        }
        //4、轉為我們指定的物件(查完資料庫 或者 Redis有快取)
                                                             // 要轉換的JSON    要轉換的型別 把自己想要的型別給 TypeReference 這個方法是受保護的,我們需要自己寫一個靜態內部類來處理
        Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJSON,new TypeReference<Map<String, List<Catelog2Vo>>>(){});
        //5、返回轉換後的結果
        return result;
    }   


//處理2級和3級
    public Map<String,List<Catelog2Vo>> getCatalogJsonDB() {
        /*
         *   【優化】
         *   1、將資料庫的多次查詢變為1次
         */
        List<CategoryEntity> selectList = baseMapper.selectList(null);

        //1、查出所有一級分類
        List<CategoryEntity> level1Categorys = getParent_cid(selectList, 0L);

        //2、封裝1級的資料                                                                         // k:一級分類的id
        Map<String, List<Catelog2Vo>> parent_cid = level1Categorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {

//-------------------------------二級---------------------------------------
            //1、通過每一個的一級分類,查到這個一級分類的二級分類                                                              //v:當前遍歷的一級分類
            List<CategoryEntity> level2Catelog = getParent_cid(selectList, v.getCatId()); //二級分類的parent_cid 【eq】 一級分類的catid

            //2、封裝2級的資料
            List<Catelog2Vo> catelog2Vos = null;

            /*2級分類的集合*/
            if (level2Catelog != null) {
                catelog2Vos = level2Catelog.stream().map(l2 -> {
                    /***
                     * v.getCatId()  一級分類的id
                     * catalo3List在下面
                     * l2.getCatId() 二級分類的id
                     * l2.getName()  二級分類的名字
                     */
                    Catelog2Vo catelog2Vo = new Catelog2Vo(v.getCatId().toString(), null, l2.getCatId().toString(), l2.getName());//封裝

//-------------------------------三級---------------------------------------
                    //1、找當前二級分類的三級分類封裝成VO
                    List<CategoryEntity> level3Catelog = getParent_cid(selectList, l2.getCatId()); //三級分類的parent_cid 【eq】 二級分類的catid
                    if (level3Catelog != null) {
                        /*3及分類的集合*/
                        List<Catelog2Vo.Catelog3Vo> collect = level3Catelog.stream().map(l3 -> {
                            //2、封裝成指定格式
                            /***
                             * item.getCatId() 2級節點的id
                             * l3.getCatId() 當前3級節點的id
                             * l3.getName()  當前3級節點的名字
                             */
                            Catelog2Vo.Catelog3Vo catelog3Vo = new Catelog2Vo.Catelog3Vo(l2.getCatId().toString(), l3.getCatId().toString(), l3.getName()); //封裝

                            return catelog3Vo;
                        }).collect(Collectors.toList());
                        catelog2Vo.setCatalog3List(collect);
                    }

                    return catelog2Vo;
                }).collect(Collectors.toList());
            }
            return catelog2Vos;
        }));

        return parent_cid;

    }


    //抽取方法
    /*
     *   List<CategoryEntity> selectList 從這個裡面查詢
     *   Long parent_cid 條件
     */
    private List<CategoryEntity> getParent_cid(List<CategoryEntity> selectList,Long parent_cid) {

        List<CategoryEntity> collect = selectList.stream().filter(item -> item.getParentCid().equals(parent_cid)).collect(Collectors.toList());
        return collect;

//        return baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", v.getCatId()));
    }

03、堆外記憶體溢位問題 OutOfDirectMemoryError


原因:

  1. springboot 2.0 以後預設使用 lettuce 作為操作 redis 的客戶端,它使用 netty 進行通訊
  2. lettuce 的 bug 導致 netty 堆外記憶體溢位
  3. -Xmx300m:如果沒有指定堆外記憶體,netty 預設使用 堆記憶體(Xmx) 作為 堆外記憶體

解決方案:

注意:不能使用 -Dio.netty.maxDirectMemory 只調大堆外記憶體

  1. 升級lettuce客戶端
  2. 切換使用jedis [推薦]
  • pom
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <exclusions>
        <exclusion>
            <groupId>io.lettuce</groupId>
            <artifactId>lettuce-core</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<!--  移除預設的 lettuce-core 核心,加入 jedis 核心  -->
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
</dependency>
  • lettuce、jedis 都是操作redis的底層客戶端 Spring 將其兩個進行了再封裝 --> RedisTemplate

快取6


04、快取穿透、雪崩、擊穿


  • 快取穿透 --> 空結果快取
  • 快取雪崩 --> 設定過期時間(加隨機值)
  • 快取擊穿 --> 加鎖

快取穿透

指高併發情況下一直查詢一個不存在的資料。查詢到null,但是並沒有將null放入快取,就進行多次查資料庫,導致資料庫瞬時壓力增大,最終導致崩潰

快取7

快取雪崩

指快取設定的過期時間一致,在某一時刻快取大面積失效,高併發情況下的查詢,進而轉到了資料庫進行查詢,導致資料庫瞬時壓力過大,最終導致崩潰

在這裡插入圖片描述

快取擊穿

指失效的快取為查詢頻率很高的某一個單點key --> 熱點資料,在某一時刻失效,高併發情況下的查詢,進而轉到了資料庫進行查詢,導致資料庫瞬時壓力過大,最終導致崩潰

快取9


6、本地鎖和分散式鎖 解決擊穿


01、本地鎖 synchronized 不推薦


使用this的方式

只要是同一把鎖,就能鎖住需要這個鎖的所有執行緒

  • 1、程式碼塊 使用 synchronized

    synchronized(this) :SpringBoot所有的元件在容器中都是單例的 this就是當前例項物件

  • 2、方法 使用 synchronized

    直接給方法加synchronized也是可以的

    public synchronized Map<String,List<Catelog2Vo>> getCatalogJsonDB() { }
    
@Override
    public Map<String,List<Catelog2Vo>> getCatalogJson() {

        

        //1、加入快取邏輯
        //從Redis中獲取快取
        String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");
        //判斷是否為空
        if (StringUtils.isEmpty(catalogJSON)){
            //2、如果快取中沒有資料
          	System.out.println("快取不命中...查詢資料庫...");
            //查詢資料庫
            Map<String, List<Catelog2Vo>> catalogJsonDb = getCatalogJsonDB();
            //3、查到的資料再放入快取,
            //因為用的是StringRedisTemplate 接受的value值是一個String 所以 catalogJsonDb 不能直接放入,需要轉換一下
            //將查到的物件轉為 JSON 放入快取中
            /* 快取中存的是 JSON 字串,優點:JSON 有跨語言跨平臺的特點 */
            String s = JSON.toJSONString(catalogJsonDb);
            redisTemplate.opsForValue().set("catalogJSON",s);
        }
        //4、轉為我們指定的物件(查完資料庫 或者 Redis有快取)
      	System.out.println("快取命中...");
        // 要轉換的JSON    要轉換的型別 把自己想要的型別給 TypeReference 這個方法是受保護的,我們需要自己寫一個靜態內部類來處理
        Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJSON,new TypeReference<Map<String, List<Catelog2Vo>>>(){});
        //5、返回轉換後的結果
        return result;
    }

/* 【優化2  將資料庫的多次查詢變為1次 且加入本地快取 】 --------------------------------------------------------------------------------------------*/
    //處理2級和3級
    public Map<String,List<Catelog2Vo>> getCatalogJsonDB() {


        /***
         *  快取擊穿 解決一:synchronized
         *  只要是同一把鎖,就能鎖住需要這個鎖的所有執行緒
         *
         *  1、場景一:使用this的方式
         *      程式碼塊 使用 synchronized
         *      synchronized(this) :SpringBoot所有的元件在容器中都是單例的  this就是當前例項物件
         *      方法 使用 synchronized
         *      public synchronized Map<String,List<Catelog2Vo>> getCatalogJsonDB() { 直接給方法加synchronized也是可以的
         */
        //TODO 本地鎖:synchronized、JUC的Lock鎖,在分散式的情況下,想要鎖住所有,必須使用 “分散式鎖”

        synchronized (this){

            /***
             * 得到鎖之後,我們應該再去快取中確定一次,如果沒有才需要繼續查詢
             */
          	
            String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");
            //如果快取不為空  直接 轉換物件return就可以了
            if( ! StringUtils.isEmpty(catalogJSON)){
                //JSON --> 物件
                Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJSON,new TypeReference<Map<String, List<Catelog2Vo>>>(){});
                //返回轉換後的結果
                return result;
            }
          	System.out.println("查詢了資料庫 進入鎖------");

            /*
             *   【優化】
             *   1、將資料庫的多次查詢變為1次
             */
            List<CategoryEntity> selectList = baseMapper.selectList(null);

            //1、查出所有一級分類
            List<CategoryEntity> level1Categorys = getParent_cid(selectList, 0L);

            //2、封裝1級的資料                                                                         // k:一級分類的id
            Map<String, List<Catelog2Vo>> parent_cid = level1Categorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {

//-------------------------------二級---------------------------------------
                //1、通過每一個的一級分類,查到這個一級分類的二級分類                                                              //v:當前遍歷的一級分類
                List<CategoryEntity> level2Catelog = getParent_cid(selectList, v.getCatId()); //二級分類的parent_cid 【eq】 一級分類的catid

                //2、封裝2級的資料
                List<Catelog2Vo> catelog2Vos = null;

                /*2級分類的集合*/
                if (level2Catelog != null) {
                    catelog2Vos = level2Catelog.stream().map(l2 -> {
                        /***
                         * v.getCatId()  一級分類的id
                         * catalo3List在下面
                         * l2.getCatId() 二級分類的id
                         * l2.getName()  二級分類的名字
                         */
                        Catelog2Vo catelog2Vo = new Catelog2Vo(v.getCatId().toString(), null, l2.getCatId().toString(), l2.getName());//封裝

//-------------------------------三級---------------------------------------
                        //1、找當前二級分類的三級分類封裝成VO
                        List<CategoryEntity> level3Catelog = getParent_cid(selectList, l2.getCatId()); //三級分類的parent_cid 【eq】 二級分類的catid
                        if (level3Catelog != null) {
                            /*3及分類的集合*/
                            List<Catelog2Vo.Catelog3Vo> collect = level3Catelog.stream().map(l3 -> {
                                //2、封裝成指定格式
                                /***
                                 * item.getCatId() 2級節點的id
                                 * l3.getCatId() 當前3級節點的id
                                 * l3.getName()  當前3級節點的名字
                                 */
                                Catelog2Vo.Catelog3Vo catelog3Vo = new Catelog2Vo.Catelog3Vo(l2.getCatId().toString(), l3.getCatId().toString(), l3.getName()); //封裝

                                return catelog3Vo;
                            }).collect(Collectors.toList());
                            catelog2Vo.setCatalog3List(collect);
                        }

                        return catelog2Vo;
                    }).collect(Collectors.toList());
                }
                return catelog2Vos;
            }));

            return parent_cid;
        }
    }

    //抽取方法
    /*
     *   List<CategoryEntity> selectList 從這個裡面查詢
     *   Long parent_cid 條件
     */
    private List<CategoryEntity> getParent_cid(List<CategoryEntity> selectList,Long parent_cid) {

        List<CategoryEntity> collect = selectList.stream().filter(item -> item.getParentCid().equals(parent_cid)).collect(Collectors.toList());
        return collect;
    }

注意:

本地鎖:synchronized、JUC的Lock鎖

synchronized(this) 為本地鎖,只能鎖住當前程式,在分散式的情況下,想要鎖住所有,必須使用 分散式鎖

this鎖的是當前例項物件,加入叢集環境下,需要每一臺機器都去加synchronized(this),每一個this都是不同的鎖,進而情況就是,1號機器有很多請求但是隻放進了1個,2號機器也只放進了1個,導致有幾臺機器就放進多少個執行緒進來,去資料庫查詢相同的資料

在這裡插入圖片描述

  • 且存在鎖的時序問題
  • 查詢了資料庫 進入鎖------ 控制檯輸出了兩次以上 表示不止一個執行緒去查了資料庫

02、本地鎖 的時序問題


在這裡插入圖片描述

會造成高併發情況下,至少兩個執行緒以上進入 synchronized (this) 來查詢資料庫,

原因:假如1號執行緒查詢完資料庫,釋放鎖,然後1號執行緒往Redis中放資料,這是一次網路互動,是有時間的,在這很短的時間內,緊接著2號執行緒進來了,發現快取中沒有,繼續查詢資料庫,然後繼續這種迴圈,可能在這一段時間內,還會有更多的執行緒進來,所以我們需要把查詢到的資料放入快取中的這一部分放在鎖裡面

@Override
    public Map<String,List<Catelog2Vo>> getCatalogJson() {

        //1、加入快取邏輯
        //從Redis中獲取快取
        String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");
        //判斷是否為空
        if (StringUtils.isEmpty(catalogJSON)){
            //2、如果快取中沒有資料
            System.out.println("快取不命中...查詢資料庫...");
            //查詢資料庫
            Map<String, List<Catelog2Vo>> catalogJsonDb = getCatalogJsonDB();
			
 //===============================鎖的時序問題=====================
          
        	 /***
             *  【注意】
             *
             * 註釋這樣寫會造成高併發情況下,至少兩個執行緒以上進入 synchronized (this) 來查詢資料庫,
             * 原因:假如1號執行緒查詢完資料庫,釋放鎖,然後1號執行緒往Redis中放資料,這是一次網路互動,是有時間的,在這很短的時間內,緊接著2號執行緒進來了,發現快取中沒有,繼續查詢資料庫,然後繼續這種迴圈,可能在這一段時間內,還會有更多的執行緒進來,所以我們需要把查詢到的資料放入快取中的這一部分放在鎖裡面
             */
          
//            //3、查到的資料再放入快取,
//            //因為用的是StringRedisTemplate 接受的value值是一個String 所以 catalogJsonDb 不能直接放入,需要轉換一下
//            //將查到的物件轉為 JSON 放入快取中
//            /* 快取中存的是 JSON 字串,優點:JSON 有跨語言跨平臺的特點 */
//            String s = JSON.toJSONString(catalogJsonDb);
//            /***
//             *  設定過期時間 解決快取雪崩
//             */
//            redisTemplate.opsForValue().set("catalogJSON",s,1, TimeUnit.DAYS);
          
            return catalogJsonDb;

			          
        }
        //4、轉為我們指定的物件(查完資料庫 或者 Redis有快取)
      	System.out.println("快取命中...");
        Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJSON,new TypeReference<Map<String, List<Catelog2Vo>>>(){});
        //5、返回轉換後的結果
        return result;
    }

/* 【優化2  將資料庫的多次查詢變為1次 且加入本地快取 】 --------------------------------------------------------------------------------------------*/
    //處理2級和3級
    public Map<String,List<Catelog2Vo>> getCatalogJsonDB() {

		==
        /*** 
         *  快取擊穿 解決一:synchronized
         *  只要是同一把鎖,就能鎖住需要這個鎖的所有執行緒
         *
         *  1、場景一:使用this的方式
         *      程式碼塊 使用 synchronized
         *      synchronized(this) :SpringBoot所有的元件在容器中都是單例的  this就是當前例項物件
         *      方法 使用 synchronized
         *      public synchronized Map<String,List<Catelog2Vo>> getCatalogJsonDB() { 直接給方法加synchronized也是可以的
         */
        //TODO 本地鎖:synchronized、JUC的Lock鎖,在分散式的情況下,想要鎖住所有,必須使用 “分散式鎖”

        synchronized (this){

            /***
             * 得到鎖之後,我們應該再去快取中確定一次,如果沒有才需要繼續查詢
             */
          	
            String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");
            //如果快取不為空  直接 轉換物件return就可以了
            if( ! StringUtils.isEmpty(catalogJSON)){
                //JSON --> 物件
                Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJSON,new TypeReference<Map<String, List<Catelog2Vo>>>(){});
                //返回轉換後的結果
                return result;
            }
          
          	System.out.println("查詢了資料庫 進入鎖------");

            /*
             *   【優化】
             *   1、將資料庫的多次查詢變為1次
             */
            List<CategoryEntity> selectList = baseMapper.selectList(null);

            //1、查出所有一級分類
            List<CategoryEntity> level1Categorys = getParent_cid(selectList, 0L);

            //2、封裝1級的資料                                                                         // k:一級分類的id
            Map<String, List<Catelog2Vo>> parent_cid = level1Categorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {

//-------------------------------二級---------------------------------------
                //1、通過每一個的一級分類,查到這個一級分類的二級分類                                                              //v:當前遍歷的一級分類
                List<CategoryEntity> level2Catelog = getParent_cid(selectList, v.getCatId()); //二級分類的parent_cid 【eq】 一級分類的catid

                //2、封裝2級的資料
                List<Catelog2Vo> catelog2Vos = null;

                /*2級分類的集合*/
                if (level2Catelog != null) {
                    catelog2Vos = level2Catelog.stream().map(l2 -> {
                        /***
                         * v.getCatId()  一級分類的id
                         * catalo3List在下面
                         * l2.getCatId() 二級分類的id
                         * l2.getName()  二級分類的名字
                         */
                        Catelog2Vo catelog2Vo = new Catelog2Vo(v.getCatId().toString(), null, l2.getCatId().toString(), l2.getName());//封裝

//-------------------------------三級---------------------------------------
                        //1、找當前二級分類的三級分類封裝成VO
                        List<CategoryEntity> level3Catelog = getParent_cid(selectList, l2.getCatId()); //三級分類的parent_cid 【eq】 二級分類的catid
                        if (level3Catelog != null) {
                            /*3及分類的集合*/
                            List<Catelog2Vo.Catelog3Vo> collect = level3Catelog.stream().map(l3 -> {
                                //2、封裝成指定格式
                                /***
                                 * item.getCatId() 2級節點的id
                                 * l3.getCatId() 當前3級節點的id
                                 * l3.getName()  當前3級節點的名字
                                 */
                                Catelog2Vo.Catelog3Vo catelog3Vo = new Catelog2Vo.Catelog3Vo(l2.getCatId().toString(), l3.getCatId().toString(), l3.getName()); //封裝

                                return catelog3Vo;
                            }).collect(Collectors.toList());
                            catelog2Vo.setCatalog3List(collect);
                        }

                        return catelog2Vo;
                    }).collect(Collectors.toList());
                }
                return catelog2Vos;
            }));
          	
 //===============================鎖的時序問題=====================
          
          	//【注意這一段是從上面優化到下面的】  //在此處將資料庫查詢到的資料放入快取,解決鎖的時序問題
            //3、查到的資料再放入快取,
            //因為用的是StringRedisTemplate 接受的value值是一個String 所以 catalogJsonDb 不能直接放入,需要轉換一下
            //將查到的物件轉為 JSON 放入快取中
            /* 快取中存的是 JSON 字串,優點:JSON 有跨語言跨平臺的特點 */
            String s = JSON.toJSONString(parent_cid);
            /***
             *  設定過期時間 解決快取雪崩
             */
            redisTemplate.opsForValue().set("catalogJSON",s,1, TimeUnit.DAYS);

          
            return parent_cid;
        }
    }

    //抽取方法
    /*
     *   List<CategoryEntity> selectList 從這個裡面查詢
     *   Long parent_cid 條件
     */
    private List<CategoryEntity> getParent_cid(List<CategoryEntity> selectList,Long parent_cid) {

        List<CategoryEntity> collect = selectList.stream().filter(item -> item.getParentCid().equals(parent_cid)).collect(Collectors.toList());
        return collect;
    }
  • 只輸出了一次:查詢了資料庫 進入鎖------ 表示一直是隻有一個執行緒進入了鎖,鎖的時序問題得以解決

03、本地鎖 壓力測試


1、啟動 4個微服務

在這裡插入圖片描述

2、設定引數 清空Redis的資料

鎖5

3、測試 每個微服務只列印了一次 查詢了資料庫 進入鎖------ 一個微服務一把鎖

在這裡插入圖片描述


04、分散式鎖


在這裡插入圖片描述

分散式鎖演化1

docker exec -it redis redis-cli   //連線客戶端
docker lock wula NX  //使用NX引數 佔位

分散式鎖1

在這裡插入圖片描述

分散式鎖演化2

在這裡插入圖片描述

EX:設定過期時間 NX:佔位 設定過期時間和加鎖必須是同步的、原子的

127.0.0.1:6379> set lock wula EX 300  //NX 設定過期時間為300s,且用NX佔位 
OK
127.0.0.1:6379> ttl lock  //查詢過期時間
(integer) 295
127.0.0.1:6379> ttl lock
(integer) 294
127.0.0.1:6379> ttl lock
(integer) 291
127.0.0.1:6379> ttl lock
(integer) 288
127.0.0.1:6379> ttl lock
(integer) 281

分散式鎖演化3

分散式鎖5

分散式鎖演化4

在這裡插入圖片描述

分散式鎖演化5

使用Lua指令碼

String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; //Lua指令碼

在這裡插入圖片描述

@Override
    public Map<String, List<Catelog2Vo>> getCatalogJson() {

        //1、加入快取邏輯
        //從Redis中獲取快取
        String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");
        //判斷是否為空
        if (StringUtils.isEmpty(catalogJSON)) {
            //2、如果快取中沒有資料
            System.out.println("快取不命中...查詢資料庫...");
            //查詢資料庫
            Map<String, List<Catelog2Vo>> catalogJsonDb = getCatalogJsonDBWithRedisLock();

            return catalogJsonDb;
        }
        //4、轉為我們指定的物件(查完資料庫 或者 Redis有快取)
        System.out.println("快取命中...");
        // 要轉換的JSON    要轉換的型別 把自己想要的型別給 TypeReference 這個方法是受保護的,我們需要自己寫一個靜態內部類來處理
        Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catelog2Vo>>>() {
        });
        //5、返回轉換後的結果
        return result;
    }

    /***
     *  分散式鎖
     */
    public Map<String, List<Catelog2Vo>> getCatalogJsonDBWithRedisLock() {

        //4、設定value為隨機值 根據value來刪鎖 每個執行緒都有自己唯一的uuid
        String uuid = UUID.randomUUID().toString();
//        //1、佔分布式鎖。去Redis佔坑
//        Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", String.valueOf(wula)); //setIfAbsent 相當於 NX引數
//        //3、設定過期時間和加鎖必須是同步的、原子的
//        Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "wula", 300, TimeUnit.SECONDS); //setIfAbsent 相當於 EX引數 和 NX引數
        Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 300, TimeUnit.SECONDS); //setIfAbsent 相當於 EX引數 和 NX引數

        if (lock){
            System.out.println("獲取分散式鎖成功");
            //加鎖成功...執行業務
            2、設定過期時間 --> 防止死鎖(不是原子的)
//            redisTemplate.expire("lock",30,TimeUnit.SECONDS);
            Map<String, List<Catelog2Vo>> dataFromDb;
            try{
                dataFromDb = getDataFromDb();
            }finally {
                //獲取值對比,對比成功才刪除  --> 原子操作 Lua指令碼
                String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; //Lua指令碼
                //刪除鎖 --> 原子刪鎖
                Long lua = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList("lock"), uuid);

            }
//            redisTemplate.delete("lock"); // 業務成功之後,刪除鎖

//            //獲取值對比,對比成功才刪除  這兩步也應該是一個原子操作
//            String lockValue = redisTemplate.opsForValue().get("lock");
//            if (uuid.equals(lockValue)){
//                //如果 Redis例存的值和設定的值一樣,才進行刪除
//                redisTemplate.delete("lock"); // 業務成功之後,刪除鎖
//            }

            return dataFromDb;
        }else {
            //加鎖失敗...重試
            //休眠100ms重試
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("獲取分散式鎖失敗............");
            return getCatalogJsonDBWithRedisLock(); /* 自旋 */
        }
    }

    //抽取方法二
    private Map<String, List<Catelog2Vo>> getDataFromDb() {
        //再次判斷快取是否有資料
        String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");
        //如果快取不為空  直接 轉換物件return就可以了
        if (!StringUtils.isEmpty(catalogJSON)) {
            //JSON --> 物件
            Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catelog2Vo>>>() {
            });
            //返回轉換後的結果
            return result;
        }
        System.out.println("查詢了資料庫 進入鎖------");

        /*
         *   【優化】
         *   1、將資料庫的多次查詢變為1次
         */
        List<CategoryEntity> selectList = baseMapper.selectList(null);

        //1、查出所有一級分類
        List<CategoryEntity> level1Categorys = getParent_cid(selectList, 0L);

        //2、封裝1級的資料                                                                         // k:一級分類的id
        Map<String, List<Catelog2Vo>> parent_cid = level1Categorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {

//-------------------------------二級---------------------------------------
            //1、通過每一個的一級分類,查到這個一級分類的二級分類                                                              //v:當前遍歷的一級分類
            List<CategoryEntity> level2Catelog = getParent_cid(selectList, v.getCatId()); //二級分類的parent_cid 【eq】 一級分類的catid

            //2、封裝2級的資料
            List<Catelog2Vo> catelog2Vos = null;

            /*2級分類的集合*/
            if (level2Catelog != null) {
                catelog2Vos = level2Catelog.stream().map(l2 -> {
                    /***
                     * v.getCatId()  一級分類的id
                     * catalo3List在下面
                     * l2.getCatId() 二級分類的id
                     * l2.getName()  二級分類的名字
                     */
                    Catelog2Vo catelog2Vo = new Catelog2Vo(v.getCatId().toString(), null, l2.getCatId().toString(), l2.getName());//封裝

//-------------------------------三級---------------------------------------
                    //1、找當前二級分類的三級分類封裝成VO
                    List<CategoryEntity> level3Catelog = getParent_cid(selectList, l2.getCatId()); //三級分類的parent_cid 【eq】 二級分類的catid
                    if (level3Catelog != null) {
                        /*3及分類的集合*/
                        List<Catelog2Vo.Catelog3Vo> collect = level3Catelog.stream().map(l3 -> {
                            //2、封裝成指定格式
                            /***
                             * item.getCatId() 2級節點的id
                             * l3.getCatId() 當前3級節點的id
                             * l3.getName()  當前3級節點的名字
                             */
                            Catelog2Vo.Catelog3Vo catelog3Vo = new Catelog2Vo.Catelog3Vo(l2.getCatId().toString(), l3.getCatId().toString(), l3.getName()); //封裝

                            return catelog3Vo;
                        }).collect(Collectors.toList());
                        catelog2Vo.setCatalog3List(collect);
                    }

                    return catelog2Vo;
                }).collect(Collectors.toList());
            }
            return catelog2Vos;
        }));

        //3、查到的資料再放入快取,
        //因為用的是StringRedisTemplate 接受的value值是一個String 所以 catalogJsonDb 不能直接放入,需要轉換一下
        //將查到的物件轉為 JSON 放入快取中
        /* 快取中存的是 JSON 字串,優點:JSON 有跨語言跨平臺的特點 */
        String s = JSON.toJSONString(parent_cid);
        /***
         *  設定過期時間 解決快取雪崩
         */
        redisTemplate.opsForValue().set("catalogJSON", s, 1, TimeUnit.DAYS);

        return parent_cid;
    }

    //抽取方法
    /*
     *   List<CategoryEntity> selectList 從這個裡面查詢
     *   Long parent_cid 條件
     */
    private List<CategoryEntity> getParent_cid(List<CategoryEntity> selectList, Long parent_cid) {

        List<CategoryEntity> collect = selectList.stream().filter(item -> item.getParentCid().equals(parent_cid)).collect(Collectors.toList());
        return collect;

    }
    public Map<String, List<Catelog2Vo>> getCatalogJsonDBWithRedisLock() {
        //設定value為隨機值 根據value來刪鎖 每個執行緒都有自己唯一的uuid
        String uuid = UUID.randomUUID().toString();
        //佔分布式鎖。去Redis佔坑	設定過期時間和加鎖必須是同步的、原子的
        Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 300, TimeUnit.SECONDS); //setIfAbsent 相當於 EX引數 和 NX引數

        if (lock){
            System.out.println("獲取分散式鎖成功");
            //加鎖成功...執行業務
            Map<String, List<Catelog2Vo>> dataFromDb;
            try{
                dataFromDb = getDataFromDb();
            }finally {
                //獲取值對比,對比成功才刪除  --> 原子操作 Lua指令碼
                String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; //Lua指令碼
                //刪除鎖 --> 原子刪鎖
                Long lua = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList("lock"), uuid);
            }
            return dataFromDb;
        }else {
            //加鎖失敗...重試
            //休眠100ms重試
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("獲取分散式鎖失敗............");
            return getCatalogJsonDBWithRedisLock(); /* 重試:自旋 */
        }
	 }

7、Redisson 分散式鎖

Redisson分散式鎖


8、SpringCache


簡介

cache1

cache3


01、整合SpringCache

  • pom
<!--  整合SpringCache  -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>
  • 配置redis使用
<!--引入Redis-->
<dependency>
	<groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <exclusions>
    	<exclusion>
        	<groupId>io.lettuce</groupId>
            <artifactId>lettuce-core</artifactId>
        </exclusion>
     </exclusions>
</dependency>
<!--  移除預設的 lettuce-core 核心,加入 jedis 核心  -->
<dependency>
	<groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
</dependency>
  • 配置

自動配置了哪些

CacheAutoConfiguration,會匯入RedisCacheConfiguration 自動配置了快取管理器RedisCacheManager

配置使用redis作為快取 (在Redis配置好的前提下)

spring.cache.type=redis

常用註解:

  •  `@Cacheable` :觸發將資料儲存到快取的操作
    
  •  `@CacheEvict` :觸發將資料從快取中刪除的操作      --> 失效模式
    
  •  `@CachePut` :在不影響方法執行的情況下更新快取   --> 雙寫模式
    
  •  `@Caching` :組合以上多個操作
    
  •  `@CacheConfig` :在類級別共享快取的相同設定。
    

測試使用快取:

  • 開啟快取功能,在主啟動類上,標註 @EnableCaching
  • 只需要使用註解,就可以完成快取操作
  • 業務方法的頭部標上@Cacheable,表示當前方法的結果需要快取,如果快取中有,該方法不會呼叫。如果快取中沒有,就會呼叫該方法,最終將方法的結果放入快取
  • 指定快取分割槽。每一次需要快取的資料,我們都需要指定要放到哪個名字的快取【快取的分割槽】通常按照業務型別進行劃
  • 示例:將前臺:查詢一級分類 放入快取中 分割槽為 category
@Cacheable({"category"}) //表示當前快取最終放到 category 分割槽裡了
@Override
public List<CategoryEntity> getLevel1Categorys() {
	System.out.println("getLevel1Categorys......");
    long l = System.currentTimeMillis();
    List<CategoryEntity> categoryEntities = baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", 0));
    return categoryEntities;
}

cache5


細節

上面我們將一級分類資料的資訊快取到Redis中了,快取到Redis中資料具有如下的特點:

  • 如果快取中有,方法不會被呼叫;

  • key預設自動生成;形式為快取的名字::SimpleKey [](自動生成的key值)

  • 快取的value值,預設使用jdk序列化機制,將序列化後的資料快取到redis;

  • 預設TTL時間為-1,表示永不過期

然而這些並不能夠滿足我們的需要,我們希望:

  1. 能夠指定生成快取所使用的key;
  2. 指定快取的資料的存活時間;
  3. 將快取的資料儲存為json形式;

改進

針對第一點:可以使用@Cacheable註解的時候,設定key屬性,接收一個SpEL 注意:加上單引號,讓其成為字串

@Cacheable(value = {"category"},key = "'level1Categorys'") 

針對第二點:在配置檔案中指定TTL

#設定快取(TTL)存活時間  單位為毫秒
spring.cache.redis.time-to-live= 3600000

清空Redis,進行測試: http://localhost:10000

在這裡插入圖片描述

SpEL的詳細語法,在文件中給予了詳細的說明:https://docs.spring.io/spring-framework/docs/5.3.0-SNAPSHOT/spring-framework-reference/integration.html#cache-spel-context

在這裡插入圖片描述

示例: 將方法的名字設定為key值

@Cacheable(value = {"category"},key = "#root.method.name") 

在這裡插入圖片描述


02、整合–自定義快取配置


針對第三點:將快取的資料儲存為json形式

這涉及到修改快取管理器的設定,CacheAutoConfiguration匯入了RedisCacheConfiguration,而RedisCacheConfiguration中自動配置了快取管理器RedisCacheManager,而RedisCacheManager要初始化所有的快取,每個快取決定使用什麼樣的配置,如果RedisCacheConfiguration有,就用已有的,沒有就用預設配置。

想要修改快取的配置,只需要給容器中放一個redisCacheConfiguration即可,這樣就會應用到當前RedisCacheManager管理的所有快取分割槽中。

  • Config
/***
 *  Cache 自定義快取配置
 */

@EnableConfigurationProperties(CacheProperties.class) //開啟配置檔案的繫結功能
@Configuration
@EnableCaching
public class MyCacheConfig {

//    注入 CacheProperties 方式1
//    @Autowired
//    CacheProperties cacheProperties;

    /***
     *  原來配置檔案重的東西都沒用上:
     *      1、原來配置檔案中的繫結的配置類是這樣子的:
     *          @ConfigurationProperties(prefix="spring.cache")
     *          public class CacheProperties
     *      2、讓他生效
     *          @EnableConfigurationProperties(CacheProperties.class)
     *          @return
     */
    @Bean
    RedisCacheConfiguration rcc(CacheProperties cacheProperties){ //注入 CacheProperties 方式2

        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
//        config = config.entryTtl();

        //key的序列化用什麼
        config = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()));
        //value的序列化用什麼
        config = config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));

        //將配置檔案中的所有配置都生效
        CacheProperties.Redis redisProperties = cacheProperties.getRedis();

        if (redisProperties.getTimeToLive() != null) {
            config = config.entryTtl(redisProperties.getTimeToLive());
        }
        if (redisProperties.getKeyPrefix() != null) {
            config = config.prefixKeysWith(redisProperties.getKeyPrefix());
        }
        if (!redisProperties.isCacheNullValues()) {
            config = config.disableCachingNullValues();
        }
        if (!redisProperties.isUseKeyPrefix()) {
            config = config.disableKeyPrefix();
        }
        return config;
    }
}

在這裡插入圖片描述

  • 其他properties配置
#配置使用Redis作為快取
spring.cache.type=redis
#設定快取(TTL)存活時間  單位為毫秒
spring.cache.redis.time-to-live= 3600000
#設定Key的字首,如果指定了字首,則使用我們定義的字首,否則使用快取的名字作為字首 --> 不建議使用
spring.cache.redis.key-prefix=CACHE_
#是否使用字首功能 --> 不建議使用
spring.cache.redis.use-key-prefix=false 
#是否快取空值 --> 防止快取穿透問題
spring.cache.redis.cache-null-values=true

在這裡插入圖片描述


@CacheEvict:將資料從快取中刪除 --> 用於失效模式

修改選單之後,刪除快取中的僅有一條原有快取快取,重新查詢才會重新生成最新的資料

@CacheEvict(value = "category",key = "'getLevel1Categorys'") //value:指定刪除的片區  key:指定刪除的快取名
@Transactional //事務
@Override
public void updateCascade(CategoryEntity category) {
	this.updateById(category); //更新自己
    //更新關聯表                                 //三級分類的id        更新的名字
	categoryBrandRelationService.updateCategory(category.getCatId(), category.getName());
}

cache10


1級2級3級分類資料 聯合新增到快取

另外在修改了一級快取時,對應的二級快取也需要更新,需要修改原來二級分類的執行邏輯。

getCatelogJson恢復成為原來的邏輯,但是設定@Cacheable,非侵入的方式將查詢結果快取到redis中:

  • 修改1級分類
    @Override
    //使用SpEL來指定value,value為該方法的名字
    @Cacheable(value = {"category"},key = "#root.method.name")
    public List<CategoryEntity> getLevel1Categorys() {
        System.out.println("getLevel1Categorys......");
        long l = System.currentTimeMillis();
        List<CategoryEntity> categoryEntities = baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", 0));
        return categoryEntities;
    }
  • 根據1級分類修改對應的2級、3級分類
@Cacheable(value = "category",key = "#root.methodName")
    @Override
    public Map<String, List<Catelog2Vo>> getCatalogJson() {
        /*
         *   【優化】
         *   1、將資料庫的多次查詢變為1次
         */
        List<CategoryEntity> selectList = baseMapper.selectList(null);
        //1、查出所有一級分類
        List<CategoryEntity> level1Categorys = getParent_cid(selectList, 0L);
        //2、封裝1級的資料                                                                         // k:一級分類的id
        Map<String, List<Catelog2Vo>> parent_cid = level1Categorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
//-------------------------------二級---------------------------------------
            //1、通過每一個的一級分類,查到這個一級分類的二級分類                                                              //v:當前遍歷的一級分類
            List<CategoryEntity> level2Catelog = getParent_cid(selectList, v.getCatId()); //二級分類的parent_cid 【eq】 一級分類的catid

            //2、封裝2級的資料
            List<Catelog2Vo> catelog2Vos = null;

            /*2級分類的集合*/
            if (level2Catelog != null) {
                catelog2Vos = level2Catelog.stream().map(l2 -> {
                    /***
                     * v.getCatId()  一級分類的id
                     * catalo3List在下面
                     * l2.getCatId() 二級分類的id
                     * l2.getName()  二級分類的名字
                     */
                    Catelog2Vo catelog2Vo = new Catelog2Vo(v.getCatId().toString(), null, l2.getCatId().toString(), l2.getName());//封裝
//-------------------------------三級---------------------------------------
                    //1、找當前二級分類的三級分類封裝成VO
                    List<CategoryEntity> level3Catelog = getParent_cid(selectList, l2.getCatId()); //三級分類的parent_cid 【eq】 二級分類的catid
                    if (level3Catelog != null) {
                        /*3及分類的集合*/
                        List<Catelog2Vo.Catelog3Vo> collect = level3Catelog.stream().map(l3 -> {
                            //2、封裝成指定格式
                            /***
                             * item.getCatId() 2級節點的id
                             * l3.getCatId() 當前3級節點的id
                             * l3.getName()  當前3級節點的名字
                             */
                            Catelog2Vo.Catelog3Vo catelog3Vo = new Catelog2Vo.Catelog3Vo(l2.getCatId().toString(), l3.getCatId().toString(), l3.getName()); //封裝

                            return catelog3Vo;
                        }).collect(Collectors.toList());
                        catelog2Vo.setCatalog3List(collect);
                    }
                    return catelog2Vo;
                }).collect(Collectors.toList());
            }
            return catelog2Vos;
        }));
        return parent_cid;
    }

訪問:wulawula.com 且重複訪問時,沒有重新查資料庫 發現Redis裡有兩個快取

在這裡插入圖片描述


@Caching:組合多個Cache操作

1級2級3級分類資料 聯合新增到快取 的基礎上操作

修改選單之後,刪除快取中的兩條以上原有快取資料,重新查詢才會重新生成最新的資料

@Caching(evict = { //組合多個cache操作
	@CacheEvict(value = "category",key = "'getLevel1Categorys'"),
    @CacheEvict(value = "category",key = "'getCatalogJson'")
})
@Transactional //事務
@Override
public void updateCascade(CategoryEntity category) {
	this.updateById(category); //更新自己
    //更新關聯表                                 //三級分類的id        更新的名字
    categoryBrandRelationService.updateCategory(category.getCatId(), category.getName());
}

在這裡插入圖片描述
cache11


只使用@CacheEvict也可以刪除多個快取資料

@CacheEvict(value = "category",allEntries = true)

它表示要刪除category分割槽下的所有資料。

注意:

可以看到儲存同一型別的資料,都可以指定未同一個分割槽,可以批量刪除這個分割槽下的資料。以後不建議使用分割槽字首,而是使用預設的分割槽字首

#設定Key的字首,如果指定了字首,則使用我們定義的字首,否則使用快取的名字作為字首 --> 不建議使用
#spring.cache.redis.key-prefix=CACHE_
#是否使用字首功能 --> 不建議使用
#spring.cache.redis.use-key-prefix=false

在這裡插入圖片描述


03、 總結


1)讀模式

  • 快取穿透:查詢一個null值。解決,快取空資料;cache-null-value=true;

  • 快取擊穿:大量併發進來同時查詢一個正好過期的資料。解決方法,是進行加鎖,預設是沒有加鎖的,查詢時設定Cacheable的sync=true即可解決快取擊穿。

  • 快取雪崩:大量的key同時過期。解決方法:加上隨機時間;加上過期時間。spring.cache.redis.time-to-live=3600000

2)寫模式(為了保證 --> 快取與資料一致)

  1. 讀寫加鎖;
  2. 引入canal,感知到mysql的更新去更新資料庫;
  3. 讀多寫少,直接去資料庫查詢就行;

總結:

  • 常規資料(讀多寫少,即時性,一致性要求不高的資料):完全可以使用spring-cache;寫模式,只要快取的資料有過期時間就足夠了;

  • 特殊資料:特殊設計;


9、前臺檢索


檢索條件分析

  • 全文檢索:skuTitle->keyword
  • 排序:saleCount(銷量)、hotScore(熱度分)、skuPrice(價格)
  • 過濾:hasStock(是否有貨 0/1)、skuPrice(價格區間)、brandId、catalog3Id、attrs
  • 聚合:attrs

完整查詢引數 keyword=小米&sort=saleCount_desc/asc&hasStock=0/1&skuPrice=400_1900&brandId=1&catalog3Id=1&at trs=1_3G:4G:5G&attrs=2_驍龍845&attrs=4_高清屏

PUT product
{
  "mappings": {
    "properties": {
      "attrs": {
        "type": "nested",
        "properties": {
          "attrId": {
            "type": "long"
          },
          "attrName": {
            "type": "keyword"
          },
          "attrValue": {
            "type": "keyword"
          }
        }
      },
      "autoGeneratedTimestamp": {
        "type": "long"
      },
      "brandId": {
        "type": "long"
      },
      "brandImg": {
        "type": "keyword"
      },
      "brandName": {
        "type": "keyword"
      },
      "catalogId": {
        "type": "long"
      },
      "catalogName": {
        "type": "keyword"
      },
      "description": {
        "type": "text",
        "fields": {
          "keyword": {
            "type": "keyword",
            "ignore_above": 256
          }
        }
      },
      "hasStock": {
        "type": "boolean"
      },
      "hotScore": {
        "type": "long"
      },
      "parentTask": {
        "properties": {
          "id": {
            "type": "long"
          },
          "nodeId": {
            "type": "text",
            "fields": {
              "keyword": {
                "type": "keyword",
                "ignore_above": 256
              }
            }
          },
          "set": {
            "type": "boolean"
          }
        }
      },
      "refreshPolicy": {
        "type": "text",
        "fields": {
          "keyword": {
            "type": "keyword",
            "ignore_above": 256
          }
        }
      },
      "retry": {
        "type": "boolean"
      },
      "saleCount": {
        "type": "long"
      },
      "shouldStoreResult": {
        "type": "boolean"
      },
      "skuId": {
        "type": "long"
      },
      "skuImg": {
        "type": "keyword"
      },
      "skuPrice": {
        "type": "keyword"
      },
      "skuTitle": {
        "type": "text",
        "analyzer": "ik_smart"
      },
      "spuId": {
        "type": "keyword"
      }
    }
  }
}

完整查詢引數 es.wulawula.com/list.html?catalog3Id=225&keyword=華為&brandId=10&brand=20&attrs=12_海思(HS)&attrs=12_驍龍&sort=saleCount_desc/asc&hasStock=0/1&skuPrice=400_1900&catalog3Id=1&at trs=1_3G:4G:5G

GET product/_search
{
  "query": {=== 檢索 ===
    "bool": {
      "must": [  <-- 必須
        {
          "match": { <--全文匹配
            "skuTitle": "華為" //商品標題
          }
        }
      ],
      "filter": [ <--過濾寫在filter
        {
          "term": { <-- 精確查詢
            "catalogId": "225" //根據3級分類Id
          }
        },
        {
          "terms": { <-- 多個值匹配
            "brandId": [ //根據品牌Id
              "10",
              "20"
            ]
          }
        },
        {
          "nested": { < ** 嵌入式查詢
            "path": "attrs",
            "query": {
              "bool": {
                "must": [
                  {
                    "term": {<-- 精確查詢
                      "attrs.attrId": { //根據屬性Id
                        "value": "12"
                      }
                    }
                  },
                  {
                    "terms": { <-- 多個精確查詢
                      "attrs.attrValue": [ //根據屬性的值
                        "海思(HS)",
                        "驍龍"
                      ]
                    }
                  }
                ]
              }
            }
          }
        },
        {
          "term": {
            "hasStock": { //根據是否有庫存
              "value": true
            }
          }
        },
        {
          "range": { <--區間檢索
            "skuPrice": { //根據價格區間
              "gte": 0, // >=0
              "lte": 6000 // <=6000
            }
          }
        }
      ]
    }
  },
  "sort": [=== 排序 ===
    {
      "skuPrice": { //根據價格降序
        "order": "desc"
      }
    }
  ],
  "from": 0, <--分頁  從幾開始
  "size": 4, <--分頁  查詢幾個
  "highlight": {===  高亮 ===
    "fields": {"skuTitle": {}}, //指定哪個屬性高亮 
    "pre_tags": "<b style='color:red'>", //前置標籤
    "post_tags": "</b>" //後置標籤
  },
  "aggs": {=== 聚合 ===
    "brand_agg": { <---聚合1
      "terms": {
        "field": "brandId", //根據品牌Id聚合
        "size": 50
      },
      "aggs": { <-- 子聚合
        "brand_name_agg": {
          "terms": {
            "field": "brandName", //根據品牌Id得到 品牌名稱聚合
            "size": 1
          }
        },
        "brand_img_agg": {
          "terms": {
            "field": "brandImg", //根據品牌Id得到 品牌圖片聚合
            "size": 1
          }
        }
      }
    },
    "catalog_agg": { <---聚合2
      "terms": {
        "field": "catalogId", //根據分類Id聚合
        "size": 20
      },
      "aggs": { <-- 子聚合
        "catalog_name_agg": {
          "terms": {
            "field": "catalogName", //根據分類Id得到 分類名稱聚合
            "size": 1
          }
        }
      }
    },
    "attr_agg": { <---聚合3
      "nested": { <** 嵌入式聚合
        "path": "attrs" < ** 需要先宣告是嵌入式聚合
      },
      "aggs": { <-- 子聚合
        "attr_id_agg": {
          "terms": {
            "field": "attrs.attrId", //根據屬性得到 屬性Id聚合
            "size": 10
          },
          "aggs": { <-- 子聚合
            "attr_name_agg": {
              "terms": {
                "field": "attrs.attrName", //根據屬性Id得到 屬性名稱聚合
                "size": 1
              }
            },
            "attr_value_agg": {
              "terms": {
                "field": "attrs.attrValue", //根據屬性Id得到 屬性值聚合
                "size": 50
              }
            }
          }
        }
      }
    }
  }
}

10、頁面渲染


01、基本效果渲染


<!--排序內容 商品每四個是一組-->
<div class="rig_tab">
    <!-- 遍歷每個商品-->
    <div th:each="product:${result.getProducts()}">
        <div class="ico">
            <i class="iconfont icon-weiguanzhu"></i>
            <a href="/static/es/#">關注</a>
        </div>
        <p class="da">
            <!--圖片 -->
            <a href="/static/es/#">
                <img th:src="${product.skuImg}" class="dim">
            </a>
        </p>
        <ul class="tab_im">
            <li><a href="/static/es/#" title="黑色">
                <img th:src="${product.skuImg}"></a>
            </li>
        </ul>
        <p class="tab_R">
            <!-- 價格 -->
            <span th:text="''+${product.skuPrice}">¥5199.00</span>
        </p>
        <p class="tab_JE">
            <!-- 標題 -->
            <!-- 使用utext標籤,使檢索時高亮不會被轉義-->
            <a href="/static/es/#" th:utext="${product.skuTitle}">
                Apple iPhone 7 Plus (A1661) 32G 黑色 移動聯通電信4G手機
            </a>
        </p>
        <p class="tab_PI">已有<span>11萬+</span>熱門評價
            <a href="/static/es/#">二手有售</a>
        </p>
        <p class="tab_CP"><a href="/static/es/#" title="穀粒商城Apple產品專營店">穀粒商城Apple產品...</a>
            <a href='#' title="聯絡供應商進行諮詢">
                <img src="/static/es/img/xcxc.png">
            </a>
        </p>
        <div class="tab_FO">
            <div class="FO_one">
                <p>自營
                    <span>穀粒商城自營,品質保證</span>
                </p>
                <p>滿贈
                    <span>該商品參加滿贈活動</span>
                </p>
            </div>
        </div>
    </div>
</div>

02、篩選條件渲染


將結果的品牌、分類、商品屬性進行遍歷顯示,並且點選某個屬性值時可以通過拼接url進行跳轉

  • html
<div class="JD_selector">
            <!--手機商品篩選-->
            <div class="title">
                <h3><b>手機</b><em>商品篩選</em></h3>
                <div class="st-ext">&nbsp;<span>10135</span>個商品</div>
            </div>
            <div class="JD_nav_logo">
                <!--品牌-->
                <div class="JD_nav_wrap">
                    <div class="sl_key">
                        <span><b>品牌:</b></span>
                    </div>
                    <div class="sl_value">
                        <div class="sl_value_logo">
                            <ul>
                                <li th:each="brand:${result.brands}">
                                    <!--拼接URL-->
                                    <a href="/static/es/#" th:href="${'javascript:searchProducts(&quot;brandId&quot;,'+brand.brandId+')'}">
                                        <img th:src="${brand.brandImg}" alt="">
                                        <div th:text="${brand.brandName}">
                                            華為(HUAWEI)
                                        </div>
                                    </a>
                                </li>
                            </ul>
                        </div>
                    </div>
                    <div class="sl_ext">
                        <a href="/static/es/#">
                            更多
                            <i style='background: url("image/search.ele.png")no-repeat 3px 7px'></i>
                            <b style='background: url("image/search.ele.png")no-repeat 3px -44px'></b>
                        </a>
                        <a href="/static/es/#">
                            多選
                            <i>+</i>
                            <span>+</span>
                        </a>
                    </div>
                </div>

                <!--分類-->
                <div class="JD_pre">
                    <div class="sl_key">
                        <span><b>分類:</b></span>
                    </div>
                    <div class="sl_value">
                        <ul>
                            <li th:each="catalog:${result.catalogs}">
                              <!--拼接URL-->  
                              <a href="/static/es/#"
                                   		th:href="${'javascript:searchProducts(&quot;catalog3Id&quot;,'+catalog.catalogId+')'}"
                                   th:text="${catalog.catalogName}">分類名稱</a>
                            </li>
                        </ul>
                    </div>
                    <div class="sl_ext">
                        <a href="/static/es/#">
                            更多
                            <i style='background: url("image/search.ele.png")no-repeat 3px 7px'></i>
                            <b style='background: url("image/search.ele.png")no-repeat 3px -44px'></b>
                        </a>
                        <a href="/static/es/#">
                            多選
                            <i>+</i>
                            <span>+</span>
                        </a>
                    </div>
                </div>


                <!--其他的所有需要展示的屬性-->
                <div class="JD_pre" th:each="attr:${result.attrs}">
                    <div class="sl_key">
                        <span th:text="${attr.attrName}">其他屬性:</span>
                    </div>
                    <div class="sl_value">
                        <ul>
                            <!--此處的value也是一個list,也需要遍歷-->
                            <li th:each="val:${attr.attrValue}">
                            <!--拼接URL-->
                            <a href="/static/es/#"
                                                                   th:href="${'javascript:searchProducts(&quot;attrs&quot;,&quot;'+attr.attrId+'_'+val+'&quot;)'}"
                                                                   th:text="${val}">其他屬性</a>
                            </li>
                        </ul>
                    </div>
                </div>
            </div>
            <div class="JD_show">
                <a href="/static/es/#">
                    <span>
                        更多選項( CPU核數、網路、機身顏色 等)
                    </span>
                </a>
            </div>
        </div>
  • js
//按照屬性篩選
function searchProducts(name, value) {
	//原來的頁面
	location.href = replaceParamVal(location.href,name,value,true);
}

搜尋頁面1


03、搜尋欄


  • html
<!--搜尋導航-->
<div class="header_sous">
    <div class="logo">
        <a href="http://wulawula.com"><img src="/static/es/./image/logo1.jpg" alt=""></a>
    </div>
    <div class="header_form">
        <input id="keyword_input" type="text" placeholder="手機"/>
        <a href="javascript:searchByKeyword();">搜尋</a>
    </div>
</div>
  • js
//搜尋欄
function searchByKeyword() {
	searchProducts("keyword",$("#keyword_input").val())
}

搜尋頁面2


04、分頁


  • html
<!--分頁-->
<div class="filter_page">
    <div class="page_wrap">
        <span class="page_span1">
            <!--上一頁-->
            <!--th:attr="pn=${result.pageNum - 1}" 自定義屬性:當前頁+1 -->
            <a class="page_a"
               href="#"
               th:attr="pn=${result.pageNum - 1}"
               th:if="${result.pageNum >1}"><!-- 當前頁>1 -->
                《 上一頁
            </a>
            <!--當前頁-->
            <a class="page_a"
               href="#"
               th:attr="pn=${nav},style=${nav == result.pageNum?'border: 0;color:#ee2222;background: #fff':''}"
               th:each="nav:${result.pageNavs}">[[${nav}]]</a>
            <!--下一頁-->
            <a class="page_a"
               href="#"
               th:attr="pn=${result.pageNum + 1}"
               th:if="${result.pageNum < result.totalPages}"><!-- 當前頁>總頁碼 -->
                下一頁 》
            </a>
        </span>
        <span class="page_span2">
            <em><b>[[${result.totalPages}]]</b>&nbsp;&nbsp;到第</em>
            <input type="number" value="1">
            <em></em>
            <a class="page_submit">確定</a>
        </span>
    </div>
</div>
  • js
//分頁 1
$(".page_a").click(function () {
    var pn=$(this).attr("pn");
    location.href=replaceParamVal(location.href,"pageNum",pn,false);
    console.log(replaceParamVal(location.href,"pageNum",pn,false))
})
//分頁2
/**
 * @param url 目前的url
 * @param paramName 需要替換的引數屬性名
 * @param replaceVal 需要替換的引數的新屬性值
 * @param forceAdd 該引數是否可以重複查詢(attrs=1_3G:4G:5G&attrs=2_驍龍845&attrs=4_高清屏)
 * @returns {string} 替換或新增後的url
 */
function replaceParamVal(url, paramName, replaceVal, forceAdd) {
    var oUrl = url.toString();
    var nUrl;
    if (oUrl.indexOf(paramName) != -1) {
        if (forceAdd && oUrl.indexOf(paramName + "=" + replaceVal) == -1) {
            if (oUrl.indexOf("?") != -1) {
                nUrl = oUrl + "&" + paramName + "=" + replaceVal;
            } else {
                nUrl = oUrl + "?" + paramName + "=" + replaceVal;
            }
        } else {
            var re = eval('/(' + paramName + '=)([^&]*)/gi');
            nUrl = oUrl.replace(re, paramName + '=' + replaceVal);
        }
    } else {
        if (oUrl.indexOf("?") != -1) {
            nUrl = oUrl + "&" + paramName + "=" + replaceVal;
        } else {
            nUrl = oUrl + "?" + paramName + "=" + replaceVal;
        }
    }
    return nUrl;
};

05、排序


  • html
<!--綜合排序-->
<div class="filter_top">
    <div class="filter_top_left" th:with="p = ${param.sort}"> <!--不能直接用 param.sort去判斷,所以用p來做中間替換,將param.sort 賦值給p,p當作text型別來用-->
        <!--判斷當param.sort不為空,並且是以自己的param.sort開始的,並且是以desc結尾的 然後3元運算 更改值-->
        <!--判斷param.sort是否為空,或者是否是以自己的param.sort開始的 然後3元運算拼接高亮樣式和預設樣式-->
        <a  th:class="${(! #strings.isEmpty(p) && #strings.startsWith(p,'hotScore') && #strings.endsWith(p,'desc')) ? 'sort_a desc':'sort_a'}"
            th:attr="style = ${#strings.isEmpty(p) || #strings.startsWith(p,'hotScore')} ? 'color: #FFF;border-color:#e4393c;background: #e4393c' : 'color: #333;border-color:#ccc;background: #FFF'"
            sort="hotScore" href="/static/es/#">綜合排序 [[${(! #strings.isEmpty(p) && #strings.startsWith(p,'hotScore') && #strings.endsWith(p,'desc'))?'↓':'↑'}]]</a><!--判斷3元運算,是否需要加上↓ 和 ↑-->
        <a  th:class="${(! #strings.isEmpty(p) && #strings.startsWith(p,'saleCount') && #strings.endsWith(p,'desc')) ? 'sort_a desc':'sort_a'}"
            th:attr="style = ${#strings.isEmpty(p) || #strings.startsWith(p,'saleCount')} ? 'color: #FFF;border-color:#e4393c;background: #e4393c' : 'color: #333;border-color:#ccc;background: #FFF'"
            sort="saleCount" href="/static/es/#">銷量 [[${(! #strings.isEmpty(p) && #strings.startsWith(p,'saleCount') && #strings.endsWith(p,'desc'))?'↓':'↑'}]]</a>
        <a  th:class="${(! #strings.isEmpty(p) && #strings.startsWith(p,'skuPrice') && #strings.endsWith(p,'desc')) ? 'sort_a desc':'sort_a'}"
            th:attr="style = ${#strings.isEmpty(p) || #strings.startsWith(p,'skuPrice')} ? 'color: #FFF;border-color:#e4393c;background: #e4393c' : 'color: #333;border-color:#ccc;background: #FFF'"
            sort="skuPrice" href="/static/es/#">價格 [[${(! #strings.isEmpty(p) && #strings.startsWith(p,'skuPrice') && #strings.endsWith(p,'desc'))?'↓':'↑'}]]</a>
        <a href="/static/es/#">評論分</a>
        <a href="/static/es/#">上架時間</a>
    </div>
    <div class="filter_top_right">
                            <span class="fp-text">
                               <b>1</b><em>/</em><i>169</i>
                           </span>
        <a href="/static/es/#" class="prev"></a>
        <a href="/static/es/#" class="next"></a>
    </div>
</div>

  • js
$(".sort_a").click(function () {
    //呼叫設定好的樣式
    // changStyle(this); //傳入 this:當前元素  方法用ele接收了這個this
    //跳轉到指定位置
    $(this).toggleClass("desc"); //被點選自動加上 desc的樣式 再次點選就會取消
    var sort = $(this).attr("sort"); //得到當前點選元素的sort裡的值
    console.log("sort = "+sort)
    sort = $(this).hasClass("desc") ? sort+"_desc":sort+"_asc"; //判斷是否包含desc 是否進行拼串
    location.href = replaceParamVal(location.href,"sort",sort)
    //禁用預設行為,禁止繫結標籤的href跳轉等
    return false;
});

  • 樣式參考
function changStyle(ele) {
    /***
     *  1、改變當前元素和兄弟元素的樣式(當前被點選的元素變為選中狀態)
     */
    //1.清空之前元素的樣式
    //預設樣式 color: #333;border-color:#ccc;background: #FFF
    $(".sort_a").css({"color":"#333","border-color":"#ccc","background":"#FFF"})
    //2.改變當前被點選的元素變成被選中狀態
    //高亮樣式 color: #FFF;border-color:#e4393c;background: #e4393c
    $(ele).css({"color":"#FFF","border-color":"#e4393c","background":"#e4393c"}) //( ${this}就是當前元素 )
    //3.去掉兄弟元素的 ↑ ↓ 符號
    $(".sort_a").each(function () { //使用each遍歷兄弟元素
        var text = $(this).text().replace("↓","").replace("↑",""); //去掉全部的 ↑ ↓
        $(ele).text(text); //設定進原始文字內容(不帶拼接的)
    });
    /***
     *  2、改變升序降序
     */
    //被點選自動加上 desc的樣式 再次點選就會取消
    $(ele).toggleClass("desc"); //加上就是降序,不加就是升序
    if ($(ele).hasClass("desc")){ //檢查被選元素是否包含指定的class內容
        //包含 -> 降序
        var text = $(ele).text().replace("↓","").replace("↑",""); //得到當前的文字值 例如:綜合排序。然後將 ↓ ↑ 兩個符號的文字清空
        console.log("text1" + text)
        text = text+"↓"; //在文字值的基礎上拼接 ↓
        console.log("text2" + text)
        $(ele).text(text) //將拼接好的文字值 設定到當前元素的文字上用於顯示
    }else {
        //不包含 -> 升序
        var text = $(ele).text().replace("↓","").replace("↑","");
        text = text+"↑";
        $(ele).text(text)
    }
}

06、價格區間


  • html
<!--不能直接用 param.sort去判斷,所以用p來做中間替換,將param.sort 賦值給p,p當作text型別來用。priceRange也是替換param.skuPrics的值-->
<div class="filter_top_left" th:with="p = ${param.sort},priceRange=${param.skuPrice}"> 
    <!--上架時間-->
    <a href="/static/es/#">上架時間</a>
    <!--價格區間-->
    <!-- th:value="${#strings.isEmpty(priceRange)}" 用於input框的回顯。且把param.skuPrics的值賦值給pricsRange 並且判斷是否為空 -->
    <!-- #strings.substringBefore(priceRange,'_') 擷取出指定字串的 _ 前面的內容  substringAfter為擷取指定字串的 _ 後面的內容-->
    <input id="skuPriceFrom" type="number" style="width: 100px; margin-left: 20px;"
           th:value="${#strings.isEmpty(priceRange) ? '' :  #strings.substringBefore(priceRange,'_')}"> -
    <input id="skuPriceTo" type="number" style="width: 100px;"
           th:value="${#strings.isEmpty(priceRange) ? '' :  #strings.substringAfter(priceRange,'_')}">
    <button id="skuPriceSearchBth">確定</button>
</div>
  • js
$("#skuPriceSearchBth").click(function () {
	//1、拼接上價格區間的查詢條件
    var from = $("#skuPriceFrom").val(); //價格開始
    var to = $("#skuPriceTo").val(); //價格結束
    var query = from + "_" + to; //拼接字串
    location.href = replaceParamVal(location.href,"skuPrice",query); //拼接字串後呼叫方法替換url
});

07、僅顯示有貨


  • html
<li>
    <a href="#" th:with="check = ${param.hasStock}">
        <input id="showHasStock" type="checkbox"
               th:checked="${#strings.equals(param.hasStock,'1')}"><!--使用equals()判斷param.hasStock裡是否是1-->
        僅顯示有貨
    </a>
</li>
  • js
//僅顯示有貨 -> 選擇框
$("#showHasStock").change(function () {
    //prop可以獲取呼叫此方法的checked型別的值為true或false(checked返回true或false)
    if ($(this).prop('checked')) {
        //true -> 有庫存
        location.href = replaceParamVal(location.href, "hasStock", 1);
    } else {
        //false -> 沒選中 (有庫存+無庫存)
        var re = eval('/(hasStock=)([^&]*)/gi'); //正規表示式匹配hasStock的值
        location.href = (location.href + "").replace(re, ''); //將hasStock替換成空串
    }
    return false;
});

08、麵包屑導航 + 條件篩選聯動


  • EsResult
/***
*  麵包屑導航
*/
private List<NavVo> navs = new ArrayList<>();

@Data
public static class NavVo{
	private String navName; //導航的名字
    private String navValue; //導航的值
    private String link; //取消一個導航後要跳轉的位置
}

/***
*  判斷哪些屬性被篩選了。頁面就不需要展示了,點選x才顯示
*/
private List<Long> attrIds = new ArrayList<>();
  • Impl
/***   8、構建麵包屑導航功能   */
//屬性
if (param.getAttrs() != null && param.getAttrs().size()>0){
    List<EsResult.NavVo> collect = param.getAttrs().stream().map(attr -> {
        //1、分析出每個attr傳過來的查詢引數值
        EsResult.NavVo navVo = new EsResult.NavVo();
        //示例:attrs=2_5寸:6寸
        String[] s = attr.split("_"); //分割
        navVo.setNavValue(s[1]); //值
        // id需要查詢出對應的名字
        //遠端呼叫
        R r = productFeignService.attrInfo(Long.parseLong(s[0]));
        result.getAttrIds().add(Long.parseLong(s[0]));
        if (r.getCode() == 0){ //正常返回
            AttrResponseVo data = r.getData("attr", new TypeReference<AttrResponseVo>() {
            });
            navVo.setNavName(data.getAttrName());
        }else {
            navVo.setNavName(s[0]);
        }

        //2、取消了麵包屑以後,跳轉到哪個地方。將請求地址的URL裡面的當前條件置空
        //拿到所有的查詢條件,去掉當前
        String replace = replaceQueryString(param, attr,"attrs");
        navVo.setLink("http://es.wulawula.com/list.html?" + replace);
        return navVo;
    }).collect(Collectors.toList());
    result.setNavs(collect);
}

//品牌、分類
if (param.getBrandId() != null && param.getBrandId().size()>0){
    List<EsResult.NavVo> navs = result.getNavs();
    EsResult.NavVo navVo = new EsResult.NavVo();
    navVo.setNavName("品牌");
    //遠端呼叫
    R r = productFeignService.brandsInfo(param.getBrandId());
    if (r.getCode() == 0) {
        List<BrandVo> brand = r.getData("brand", new TypeReference<List<BrandVo>>() {
        });
        StringBuffer buffer = new StringBuffer();
        String replace = "";
        for (BrandVo brandVo:brand){
            buffer.append(brandVo.getName());
            replace = replaceQueryString(param, brandVo.getBrandId()+"","brandId");
        }
        navVo.setNavValue(buffer.toString());
        navVo.setLink("http://es.wulawula.com/list.html?" + replace);
    }
    navs.add(navVo);
}

  • R程式碼
public <T> T getData(String key,TypeReference<T> typeReference){
	Object data = get(key);
	String s = JSON.toJSONString(data); //物件 --> JSON字串
	//轉換物件,可以將字串型別的物件轉換成我們指定型別的物件
	T t = JSON.parseObject(s, typeReference); //JSON字串 --> 相應的物件
	return t;
}
@Data
public class BrandVo {
    private Long brandId; //品牌id
    private String name; //品牌名字
}
  • feign
@FeignClient("gulimall-product")
public interface ProductFeignService {

    //查詢屬性的資訊
    @GetMapping("/product/attr/info/{attrId}")
    public R attrInfo(@PathVariable("attrId") Long attrId);

    //返回所有的品牌資料
    @GetMapping("/product/brand/infos")
    public R brandsInfo(@RequestParam("brandIds") List<Long> brandIds);
    
}
  • html
<!--遍歷麵包屑功能-->
<div class="JD_ipone_one c">
    <!--獲取點選x後跳轉的地址-->
    <a th:href="${nav.link}"
       th:each="nav:${result.navs}">
        <!--獲取跳轉的屬性名和值-->
        <span th:text="${nav.navName}"></span>
        <span th:text="${nav.navValue}"></span> x </a>
</div>
  • html
  • 使用 #lists.contains() 方法獲取list中的返回的值
<!--其他的所有需要展示的屬性-->
<div class="JD_pre" th:each="attr:${result.attrs}" th:if="${!#lists.contains(result.attrIds,attr.attrId)}">
    <div class="sl_key">
        <span th:text="${attr.attrName}">屬性名字</span>
    </div>
    <div class="sl_value">
        <ul>
            <!--此處的value也是一個list,也需要遍歷-->
            <li th:each="val:${attr.attrValue}"><a
                                                   th:href="${'javascript:searchProducts(&quot;attrs&quot;,&quot;'+attr.attrId+'_'+val+'&quot;)'}"
                                                   th:text="${val}">顯示所有屬性值</a>
            </li>
        </ul>
    </div>
</div>


11、非同步

非同步 & CompletableFuture非同步編排


12、CompletableFuture非同步編排

非同步 & CompletableFuture非同步編排


13、商品詳情


01、初步


1、修改hosts的http規則

# gulimall
192.168.56.10 wulawula.com
192.168.56.10 es.wulawula.com
192.168.56.10 item.wulawula.com

2、修改 Nginx 配置檔案

/mydata/nginx/conf/conf.d/mydata/nginx/conf/conf.d

商品詳情1

3、配置 GateWay

### 搜尋頁面
        - id: gulimall_es_route
          # lb 代表的就是負載均衡
          uri: lb://gulimall-es
          predicates:
            - Host=es.wulawula.com

4、修改商品列表的跳轉路徑,點選商品圖片即可跳轉到商品詳情頁

<!--圖片 -->
<a th:href="|http://item.wulawula.com/${product.skuId}.html|">
	<img th:src="${product.skuImg}" class="dim">
</a>

使用 |${}| 的方式動態拼接url路徑:|http://item.wulawula.com/${product.skuId}.html|

5、封裝資料Vo

  • 總封裝vo
/***
 *  封裝前臺商品詳情頁資訊
 */
@Data
public class SkuItemVo {

    //1、sku基本資訊獲取   pms_sku_info
    SkuInfoEntity info;

    //2、sku的圖片資訊    pms_sku_images
    List<SkuImagesEntity> images;

    //3、獲取spu的銷售屬性組合 -> 組合有多種 -> 將單個的銷售屬性組合起來
    List<SkuItemSaleAttrVo> saleAttr;

    //4、獲取spu的介紹    pms_spu_info_desc
    SpuInfoDescEntity desc;

    //5、獲取spu的規格引數資訊
    SpuItemAttrGroup groupAttrs;
}
  • 下屬vo 2.1
//2.1、獲取spu的銷售屬性(單個)
@Data
public class SkuItemSaleAttrVo{
    private Long skuId; //id
    private String attrName; //名稱
    private List<String> attrValues; //值
}
  • 下屬vo 5.1
//5.1、獲取spu的基本屬性分組(分組下有多個屬性)
@ToString
@Data
public class SpuItemAttrGroupVo{
    private String groupName; //分組的名字
    private List<SpuBaseAttrVo> attrs; //分組下對應的屬性
}
  • 下屬vo 5.1.1
//5.1.1、spu的基本屬性(單個)
@ToString
@Data
public class SpuBaseAttrVo{
    private String attrName; //屬性的名字
    private String attrValue; //屬性的值
}
  • controller
@Controller
public class ItemController {
    @Autowired
    SkuInfoService skuInfoService;
    /***
     * 展示當前sku的詳情
     */
    @GetMapping("/{skuId}.html")
    public String skuItem(@PathVariable Long skuId, Model model){
        System.out.println("準備查詢" + skuId + "詳情");
        //前臺:查詢出sku的詳情內容,用於展示
        SkuItemVo vo = skuInfoService.item(skuId);
        model.addAttribute("item",vo);
        return "item";
    }
}
  • SkuInfoServiceImpl
    //前臺:查詢出sku的詳情內容,用於展示
    @Override
    public SkuItemVo item(Long skuId) {

        SkuItemVo skuItemVo = new SkuItemVo();

        //1、sku基本資訊獲取    pms_sku_info
        SkuInfoEntity info = getById(skuId);
        skuItemVo.setInfo(info);

        Long catalogId = info.getCatalogId();
        Long spuId = info.getSpuId();

        //2、sku的圖片資訊      pms_sku_images
        //前臺:通過skuid查詢sku的圖片資訊
        List<SkuImagesEntity> images = imagesService.getImagesBySkuId(skuId);
        skuItemVo.setImages(images);

        //3、獲取spu的銷售屬性組合 -> 組合有多種
        List<SkuItemSaleAttrVo> saleAttrVos = skuSaleAttrValueService.getSaleAttrsBySpuId(spuId);
        skuItemVo.setSaleAttr(saleAttrVos);

        //4、獲取spu的介紹 pms_spu_info_desc
        SpuInfoDescEntity desc = spuInfoDescService.getById(spuId);
        skuItemVo.setDesc(desc);

        //5、獲取spu的規格引數資訊 -> 組合
        //獲取spu的銷售屬性(單個)
        //通過spuid查詢出屬性分組
        List<SpuItemAttrGroupVo>attrGroupVos = attrGroupService.getAttrGroupWithAttrsBySpuId(spuId,catalogId);
        skuItemVo.setGroupAttrs(attrGroupVos);

        return skuItemVo;
    }

針對 3、獲取spu的銷售屬性組合 -> 組合有多種 寫sql

  • SkuSaleAttrValueServiceImpl
    @Override
    public List<SkuItemSaleAttrVo> getSaleAttrsBySpuId(Long spuId) {
        SkuSaleAttrValueDao dao = this.baseMapper;
        //sql
        List<SkuItemSaleAttrVo> saleAttrVos = dao.getSaleAttrsBySpuId(spuId);
        return saleAttrVos;
    }
  • SkuSaleAttrValueDao
    //3、獲取spu的銷售屬性組合 -> 組合有多種
    List<SkuItemSaleAttrVo> getSaleAttrsBySpuId(@Param("spuId") Long spuId);
  • SkuSaleAttrValueDao.xml
<select id="getSaleAttrsBySpuId" resultType="com.wulawula.gulimall.product.vo.web.SkuItemSaleAttrVo">
SELECT
	ssav.attr_id attr_id,
	ssav.attr_name attr_name,
	GROUP_CONCAT(DISTINCT ssav.attr_value) attrValues
FROM 
	pms_sku_info info
LEFT JOIN 
	pms_sku_sale_attr_value ssav ON ssav.sku_id = info.sku_id
WHERE 
	info.spu_id=#{spuId}
GROUP BY 
	ssav.attr_id,ssav.attr_name
	
</select>

針對 5、獲取spu的規格引數資訊 -> 組合 寫sql

    @Override
    public List<SpuItemAttrGroupVo> getAttrGroupWithAttrsBySpuId(Long spuId, Long catalogId) {

        //1、查出當前spu對應的所有屬性的分組資訊以及當前分組下的所有屬性對應的值
        AttrGroupDao baseMapper = this.getBaseMapper();
        //sql
        List<SpuItemAttrGroupVo> vos = baseMapper.getAttrGroupWithAttrsBySpuId(spuId,catalogId);
        return vos;
    }

  • AttrGroupDao
    List<SpuItemAttrGroupVo> getAttrGroupWithAttrsBySpuId(@Param("spuId") Long spuId, @Param("catalogId") Long catalogId);
  • AttrGroupDao.xml
    <!-- resultType:返回集合裡面元素的型別,只要有巢狀屬性就要封裝自定義結果集 -->
    <resultMap id="spuItemAttrGroupVo" type="com.wulawula.gulimall.product.vo.web.SpuItemAttrGroupVo">
        <result property="groupName" column="attr_group_name"/>
        <collection property="attrs" ofType="com.wulawula.gulimall.product.vo.web.SpuBaseAttrVo">
            <result property="attrName" column="attr_name"/>
            <result property="attrValue" column="attr_value"/>
        </collection>
    </resultMap>

    <select id="getAttrGroupWithAttrsBySpuId"resultMap="spuItemAttrGroupVo">

        SELECT pav.spu_id, ag.attr_group_name, ag.attr_group_id, aar.attr_id, attr.attr_name, pav.attr_value
        FROM pms_attr_group ag
        LEFT JOIN pms_attr_attrgroup_relation aar ON aar.attr_group_id = ag.attr_group_id
		LEFT JOIN pms_attr attr ON attr.attr_id = aar.attr_id
        LEFT JOIN pms_product_attr_value pav ON pav.attr_id = attr.attr_id
        WHERE  ag.catelog_id = #{catalogId} AND pav.spu_id = #{spuId}
    </select>

商品詳情2


02、sku組合切換


  • 修改 SkuItemSaleAttrVo
//2.1、獲取spu的銷售屬性(單個)
@ToString
@Data
public class SkuItemSaleAttrVo{
    private Long attrId; //id
    private String attrName; //名稱
    private List<AttrValueWithSkuIdVo> attrValues; //值(修改為有多種)
}
  • 新增 AttrValueWithSkuIdVo
//2.1.1、組合多種值
@Data
public class AttrValueWithSkuIdVo {
    private String attrValue;
    private String skuIds;
}
  • 修改 SkuSaleAttrValueDao.xml
    <resultMap id="skuItemSaleAttrVo" type="com.wulawula.gulimall.product.vo.web.SkuItemSaleAttrVo">
        <result property="attrId" column="attr_id"/>
        <result property="attrName" column="attr_name"/>
        <collection property="attrValues" ofType="com.wulawula.gulimall.product.vo.web.AttrValueWithSkuIdVo">
            <result property="attrValue" column="attr_value"/>
            <result property="skuIds" column="sku_ids"/>
        </collection>
    </resultMap>

    <select id="getSaleAttrsBySpuId" resultMap="skuItemSaleAttrVo">
        SELECT
            ssav.attr_id attr_id,
            ssav.attr_name attr_name,
            ssav.attr_value attr_value,
            GROUP_CONCAT(DISTINCT info.sku_id) sku_ids
        FROM
            pms_sku_info info
        LEFT JOIN
            pms_sku_sale_attr_value ssav ON ssav.sku_id = info.sku_id
        WHERE
            info.spu_id = #{spuId}
        GROUP BY
            ssav.attr_id,ssav.attr_name, ssav.attr_value
    </select>

商品詳情3

  • html
<div class="box-attr-3">
					<div class="box-attr clear" th:each="attr:${item.saleAttr}">
						<dl>
							<!--遍歷屬性-->
							<dt>選擇[[${attr.attrName}]]</dt>
							<!--遍歷下面的陣列用逗號分隔開  val就是每一個值-->
							<dd th:each="vals:${attr.attrValues}">
								<!--用逗號把vals.skuIds分隔開。再判斷這倆門面是否包含item.info.skuId-->
								<!--然後自定義方法判斷是否包含?包含的話加上checked   示例:商品為:黑色8GB+128GB的手機 檢視它的屬性。只有黑色和8GB+128GB兩個屬性會有checked-->
								<a
									th:attr="skus=${vals.skuIds} , class=${#strings.contains(#strings.listSplit(vals.skuIds,','),item.info.skuId.toString()) ? 'sku_attr_value checked' : 'sku_attr_value'}" >
									[[${vals.attrValue}]]
<!--											<img src="/static/item/img/59ddfcb1Nc3edb8f1.jpg" /> -->
								</a>
							</dd>
						</dl>
					</div>
				</div>
  • js1
$(".sku_attr_value").click(function () {
		var skus = new Array();
		//1、點選的元素加上clicked自定義屬性值,代表這個是我們剛剛點選的
		$(this).addClass("clicked");
		//2、獲得當前元素的sku集合 字串 並用逗號分隔開轉為陣列
		var curr = $(this).attr("skus").split(",");
		skus.push(curr); //當前被點選的所有sku組合的陣列放到集合裡面
		//3、去掉原來的checked屬性,在當前標籤用parent()往上找兩級父標籤,再用find()去找後代標籤,用removeClass()移除指定的屬性
		$(this).parent().parent().find(".sku_attr_value").removeClass("checked")
		//其他屬性也遍歷放進集合裡面
		$("a[class='sku_attr_value checked']").each(function () {
			skus.push($(this).attr("skus").split(","));
		});
		console.log(skus);
		//2、取出它們的交集,得到skuId
		// console.log($(skus[0]).filter(skus[1])[0]);
		var filterEle = skus[0];
		for (var i=1;i<skus.length;i++){
			filterEle = $(filterEle).filter(skus[1])
		}
		//跳轉到指定的顏色介面
		location.href = "http://item.wulawula.com/"+ filterEle[0] +".html";
		console.log(filterEle[0]);
	});
  • js2
//判斷是否選中標籤,去改變它的css樣式
	$(function () {
		//因為方法加再了dd標籤 用parent()方法去找它的父標籤
		$("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.checked").parent().css({"border":"solid 1px red"})
		// $(".sku_attr_value").parent().css({"border":"solid 1px #CCC"})
	})

03、非同步編排優化


1、自定義執行緒池

  • config
//如果指定的 ThreadPoolConfigProperties類 沒有用Component加到容器中,那麼我們要在需要此配置的類裡開啟屬性配置,
//來使用此class的配置內容。示例:都不加的話,此類的方法上不能傳入 ThreadPoolConfigProperties類
//@EnableConfigurationProperties(ThreadPoolConfigProperties.class) //開啟屬性配置
@Configuration
public class MyThreadConfig {

    @Bean
    public ThreadPoolExecutor executor(ThreadPoolConfigProperties pool) {
        return new ThreadPoolExecutor(pool.getCoreSize(), /*核心執行緒大小*/
                pool.getMaxSize(), /*最大執行緒大小*/
                pool.getKeepAliveTime(), /*空閒執行緒多久關閉*/
                TimeUnit.SECONDS, /*時間單位 -> 秒*/
                new LinkedBlockingQueue<>(100000), /*阻塞佇列長度*/
                Executors.defaultThreadFactory(), /*執行緒工廠 -> 此處使用預設的*/
                new ThreadPoolExecutor.AbortPolicy() /*拒絕策略 -> 拋棄*/

        );
    }
}

2、設定 執行緒池屬性配置類 讓其可以在yml裡作為可配置的

  • ConfigProperties
@ConfigurationProperties(prefix = "wulawula.thread" ) //生成配置後設資料,可在yml裡面配置
@Component
@Data
public class ThreadPoolConfigProperties {
    private Integer coreSize;
    private Integer maxSize;
    private Integer keepAliveTime;
}

3、在yml裡設定 配置類 的引數

  • yml
wulawula:
  thread:
    core-size: 20
    max-size: 200
    keep-alive-time: 10

4、修改程式 -> 非同步編排

在這次查詢中任務2、3、4、5、6都需要spuId,因此需要等待任務1執行完畢,得到任務1的執行結果。因此任務1採用supplyAsync,需要其有返回值。任務2、3、4、5、6呼叫thenAcceptAsync()可以接受上一步的結果且沒有返回值。任務1和任務7誰也不依賴誰,平級的。都需要傳入一個skuId,因此可以建立兩個非同步物件。 記得注入執行緒池

最後,呼叫 allOf().get() 方法使得所有方法都已經執行完成且是按照順序執行

//注入執行緒池
    @Autowired
    ThreadPoolExecutor executor;


//前臺:查詢出sku的詳情內容,用於展示
    @Override
    public SkuItemVo item(Long skuId) throws ExecutionException, InterruptedException {

        SkuItemVo skuItemVo = new SkuItemVo();

        /***
         * 非同步編排
         */
        /* 1.建立非同步任務 -> 有返回值 */
        CompletableFuture<SkuInfoEntity> infoFuture = CompletableFuture.supplyAsync(() -> {
            //1、sku基本資訊獲取    pms_sku_info
            SkuInfoEntity info = getById(skuId);
            skuItemVo.setInfo(info);
            return info;
        }, executor);

        /* 2.接收上一步結果,並消費處理該結果,無返回值 */
        CompletableFuture<Void> saleAttrFuture = infoFuture.thenAcceptAsync(result -> {
            //3、獲取spu的銷售屬性組合 -> 組合有多種
            List<SkuItemSaleAttrVo> saleAttrVos = skuSaleAttrValueService.getSaleAttrsBySpuId(result.getSpuId());
            skuItemVo.setSaleAttr(saleAttrVos);
        }, executor);

        /* 3.接收上一步結果,並消費處理該結果,無返回值 */
        CompletableFuture<Void> descFuture = infoFuture.thenAcceptAsync(result -> {
            //4、獲取spu的介紹 pms_spu_info_desc
            SpuInfoDescEntity desc = spuInfoDescService.getById(result.getSpuId());
            skuItemVo.setDesc(desc);
        }, executor);

        /* 4.接收上一步結果,並消費處理該結果,無返回值 */
        CompletableFuture<Void> baseAttrFuture = infoFuture.thenAcceptAsync(result -> {
            //5、獲取spu的規格引數資訊 -> 組合
            //獲取spu的銷售屬性(單個)
            //通過spuid查詢出屬性分組
            List<SpuItemAttrGroupVo> attrGroupVos = attrGroupService.getAttrGroupWithAttrsBySpuId(result.getSpuId(), result.getCatalogId());
            skuItemVo.setGroupAttrs(attrGroupVos);
        }, executor);

        /* 5.接收上一步結果,並消費處理該結果,無返回值 */
        CompletableFuture<Void> spuInfoFuture = infoFuture.thenAcceptAsync(result -> {
            //商品描述
            SpuInfoEntity spuInfo = spuInfoService.getById(result.getSpuId());
            skuItemVo.setSpuInfo(spuInfo);
        }, executor);

        /* 6.接收上一步結果,並消費處理該結果,無返回值 */
        CompletableFuture<Void> brandFuture = infoFuture.thenAcceptAsync(result -> {
            //品牌名
            Long brandId = result.getBrandId();
            BrandEntity byId = brandService.getById(brandId);
            skuItemVo.setBrand(byId);
        }, executor);

        /* 7.新開一個非同步任務,無返回值 */
        CompletableFuture<Void> imagesFuture = CompletableFuture.runAsync(() -> {
            //2、sku的圖片資訊      pms_sku_images
            //前臺:通過skuid查詢sku的圖片資訊
            List<SkuImagesEntity> images = imagesService.getImagesBySkuId(skuId);
            skuItemVo.setImages(images);
        }, executor);

        /* 8.等待所有vo都完成 */ CompletableFuture.allOf(saleAttrFuture,descFuture,baseAttrFuture,spuInfoFuture,brandFuture,imagesFuture).get();

        return skuItemVo;
    }

14、登入 註冊


1、修改hosts的http規則

# gulimall
192.168.56.10 wulawula.com
192.168.56.10 es.wulawula.com
192.168.56.10 item.wulawula.com
192.168.56.10 auth.wulawula.com

2、修改 Nginx 配置檔案

/mydata/nginx/conf/conf.d/mydata/nginx/conf/conf.d

商品詳情1

3、配置 GateWay

### 登陸頁面註冊頁面
        - id: gulimall_auth_route
          # lb 代表的就是負載均衡
          uri: lb://gulimall-auth-server
          predicates:
            - Host=auth.wulawula.com

4、


01、倒數計時效果


1、給a標籤繫結一個sendCode

<a id="sendCode">傳送驗證碼</a>

2、自定義的倒數計時方法

//傳送驗證碼。倒數計時
$(function f() {
	$("#sendCode").click(function () {
		//1、倒數計時效果
		//判斷class裡是否有disavled。如果有的話代表正在倒數計時。不能重複點選自定義的倒數計時方法
		if ($(this).hasClass("disavled")){
			//正在倒數計時
		}else {
			//2、給指定手機號傳送驗證碼
			timeoutChangeStyle();
		}
	})
})

//自定義的倒數計時方法
var num = 60; //設定初始時間
function timeoutChangeStyle() {
	$("#sendCode").attr("class","disavled"); //給a標籤的class設定一個class值,防止點選完後還可以重複點選
	if (num == 0){
		//傳送驗證碼倒數計時完成,重新傳送
		$("#sendCode").text("傳送驗證碼"); //重新修改文字內容
		num = 60;
		$("#sendCode").attr("class",""); //此時移除class的值,讓其可以再次點選
	}else {
		//驗證碼倒數計時
		var str = num+"s 後再次傳送";
		$("#sendCode").text(str); //文字內容
		setTimeout("timeoutChangeStyle()",1000); //計時器方法。不斷的呼叫此大方法,時間間隔為1s
		num --;
	}
}

在這裡插入圖片描述


02、檢視對映跳轉


  • controller (以前的方法。直接在controller裡跳轉頁面)
@Controller
public class LoginController {
    //跳轉登入頁
    @GetMapping("/login.html")
    public String loginPage(){
        return "login";
    }
    //跳轉註冊頁
    @GetMapping("/reg.html")
    public String regPage(){
        return "reg";
    }
}

傳送一個請求直接跳轉到一個頁面,此請求不傳遞資料。只是單純的跳轉方法就可以使用檢視對映

SpringMVC viewcontroller:將請求和頁面對映過來

  • config (檢視對映 實現WebMvcConfigurer介面 引數可以參考之前的controller方法)
@Configuration
public class GulimallConfig implements WebMvcConfigurer {
    /***
     *  檢視對映
     */
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
    /*  addViewController:新增檢視配置器  引數1:url路徑       引數2:檢視名 */
        registry.addViewController("/login.html").setViewName("login");
        registry.addViewController("/reg.html").setViewName("reg");
    }
}

03、整合阿里簡訊服務


第三方呼叫模組

  • 定義方法類 簡訊服務的具體方法
@ConfigurationProperties(prefix = "spring.cloud.alicloud.sms")
@Data //為這些方法生成getter,setter
@Component
public class SmsComponent {

    //aliyuncs的引數
    private String accessKeyID;
    private String accessKeySecret;
    private String signName;//您的申請簽名
    private String templateCode; //你的模板

    //param裡面包含驗證碼,phone裡面是手機號
    public void sendSmsCode(String phone, String param) {
        //判斷手機號是否為空
        if (StringUtils.isEmpty(phone)) {
            System.out.println("手機號為空");
        }else {
            DefaultProfile profile = DefaultProfile.getProfile("default", accessKeyID, accessKeySecret);
            IAcsClient client = new DefaultAcsClient(profile);

            //設定相關固定的引數
            CommonRequest request = new CommonRequest();
            request.setMethod(MethodType.POST); //提交方式
            request.setDomain("dysmsapi.aliyuncs.com");
            request.setVersion("2017-05-25");
            request.setAction("SendSms");

            //設定傳送相關的引數
            request.putQueryParameter("PhoneNumbers", phone);   //手機號
            request.putQueryParameter("SignName", signName);    //申請的阿里雲的 簽名名稱
            request.putQueryParameter("TemplateCode", templateCode);   //申請的阿里雲的 模板code

            HashMap<String, Object> params = new HashMap<>();
            params.put("code",param);
            request.putQueryParameter("TemplateParam", JSONObject.toJSONString(params)); //驗證碼資料,轉換json資料傳遞

            try {
                //最終傳送
                CommonResponse response = client.getCommonResponse(request);
                System.out.println("傳送成功");
                //判斷成功還是失敗
            }catch (ServerException e) {
                e.printStackTrace();
                System.out.println("傳送失敗1");
            } catch (ClientException e) {
                e.printStackTrace();
                System.out.println("傳送失敗2");
            }
        }
    }
}
  • application.yml 配置自定義的配置檔案引數
spring:
  cloud:
    alicloud:
      ### 自定義簡訊配置檔案
      sms:
        access-key-i-d: 自己的AccessKeyId
        access-key-secret: 自己的AccessKeySecret
        sign-name: 申請的阿里雲的 簽名名稱
        template-code: 申請的阿里雲的 模板code

主程式模組

  • 錯誤狀態碼列舉類
public enum BizCodeEnume {

    UNKNOWN_EXCEPTION(10000,"系統未知異常"),
    VAILD_EXCEPTION(10001,"引數格式校驗失敗"),
    PRODUCT_UP_EXCEPTION(11000,"商品上架異常"),
    SMS_CODE_EXCEPTION(10002,"驗證碼獲取頻率太高,稍後再試");

    private int code;
    private String msg;
    BizCodeEnume(int code,String msg){
        this.code = code;
        this.msg = msg;
    }
    public int getCode(){
        return code;
    }
    public String getMsg(){
        return msg;
    }
}
  • openfeign 遠端呼叫第三方模組傳送驗證碼
@FeignClient("gulimall-third-party")
public interface ThirdPartFeignService {
    //呼叫第三方模組傳送驗證碼
    @GetMapping("/sms/sendcode")
    public R sendCode(@RequestParam("phone") String phone, @RequestParam("code") String code);
}
  • controller
  • 驗證碼的再次校驗:存進redis
  • 防止同一個手機號在60s內再次傳送驗證碼:存的時候加上當前的時間戳。並在每一次呼叫傳送驗證碼的遠端介面時先取出來redis裡存的驗證碼。並以 _ 切割字串。判斷redis裡存取的時間和當前時間的時間差是否在60000毫秒以內,在的話丟擲列舉類裡定義的異常
@Controller
public class LoginController {

    @Autowired
    ThirdPartFeignService thirdPartFeignService; //注入遠端呼叫
    @Autowired
    StringRedisTemplate stringRedisTemplate; //注入redis

    //呼叫簡訊傳送功能
    @ResponseBody
    @GetMapping("/sms/sendcode")
    public R sendCode(@RequestParam("phone") String phone){
        
        //每次執行先讀取是否由此驗證碼
        String redisCode = stringRedisTemplate.opsForValue().get(AuthServerConstant.SMS_CODE_CACHE + phone);

        //TODO 1、介面防刷
      
      
      	//第一次傳送驗證碼肯定是空的。所以此處不為空時,才判斷是否在60s之內
        if (!StringUtils.isEmpty(redisCode)){ 
            long s = Long.parseLong(redisCode.split("_")[1]); //擷取存進redis的時間
            if (System.currentTimeMillis() - s < 60000){
                //防止同一個手機號在60s內再次傳送驗證碼
                System.out.println("60s內不能再次傳送");
                return R.error(BizCodeEnume.SMS_CODE_EXCEPTION.getCode(),BizCodeEnume.SMS_CODE_EXCEPTION.getMsg());
            }
        }
        String code = UUID.randomUUID().toString().substring(0, 5) + "_" + System.currentTimeMillis(); //給redis寸資料加時間戳。
        
        //2、驗證碼的再次校驗 -> redis  key對應phone value對應code   sms:code:18346779985 -> 123456
        stringRedisTemplate.opsForValue().set(AuthServerConstant.SMS_CODE_CACHE+phone,code,10, TimeUnit.MINUTES); //指定過期時間為10分鐘
        
        thirdPartFeignService.sendCode(phone,code);
        return R.ok();
    }
}
  • js 給指定手機號傳送完驗證碼後,呼叫回撥方法。判斷 主程式的code的值
//傳送驗證碼。倒數計時
$(function f() {
	$("#sendCode").click(function () {
		//1、倒數計時效果
		//判斷class裡是否有disavled。如果有的話代表正在倒數計時。不能重複點選自定義的倒數計時方法
		if ($(this).hasClass("disavled")){
			//正在倒數計時
		}else {
			//2、給指定手機號傳送驗證碼
			phoneNumber = $("#phoneNum").val(),//得到手機號
			$.get("/sms/sendcode?phone="+ phoneNumber,function (data) {
				if (data.code != 0){
					alert(data.msg);
				}
			}); //給LoginController傳送請求。拼接phone引數
			timeoutChangeStyle();
		}
	})
})

在這裡插入圖片描述


04、註冊【異常機制、加密】


被呼叫服務 -> 會員服務

  • 錯誤狀態碼列舉類 定義列舉 用於判斷手機號或者驗證碼是否重複
    //  錯誤狀態碼列舉類

public enum BizCodeEnume {

    UNKNOWN_EXCEPTION(10000,"系統未知異常"),
    VAILD_EXCEPTION(10001,"引數格式校驗失敗"),
    PRODUCT_UP_EXCEPTION(11000,"商品上架異常"),
    SMS_CODE_EXCEPTION(10002,"驗證碼獲取頻率太高,稍後再試"),
    USER_EXIST_EXCEPTION(15001,"使用者名稱已存在"),
    PHONE_EXIST_EXCEPTION(15002,"手機號已存在");

    private int code;
    private String msg;
    BizCodeEnume(int code,String msg){
        this.code = code;
        this.msg = msg;
    }

    public int getCode(){
        return code;
    }

    public String getMsg(){
        return msg;
    }
}
  • vo 【接收註冊頁面傳過來的註冊資訊的vo】
//接收註冊頁面傳過來的註冊資訊的vo
@Data
public class MemberRegistVo {
    private String username;
    private String password;
    private String phone;
}
  • controller 需要捕獲異常 -> 判斷手機號已存在 使用者名稱已存在。然後返回給呼叫服務
  • 通過異常機制判斷當前註冊會員名和電話號碼是否已經註冊,如果已經註冊,則丟擲對應的自定義異常,並在返回時封裝對應的錯誤資訊
  • 如果沒有註冊,則封裝傳遞過來的會員資訊,並設定預設的會員等級、建立時間
/***
     *      會員的註冊功能 -> 遠端呼叫
     */
    @PostMapping("/regist")
    public R regist(@RequestBody MemberRegistVo vo){

        try{
            memberService.regist(vo);
            //捕獲異常 -> 手機號已存在 使用者名稱已存在
        }catch (PhoneExistException e){
            R.error(BizCodeEnume.PHONE_EXIST_EXCEPTION.getCode(),BizCodeEnume.PHONE_EXIST_EXCEPTION.getMsg());
        }catch (UserNameExistException e){
            R.error(BizCodeEnume.USER_EXIST_EXCEPTION.getCode(),BizCodeEnume.USER_EXIST_EXCEPTION.getMsg());
        }

        return R.ok();
    }
  • server 介面
public interface MemberService extends IService<MemberEntity> {
    //會員的註冊功能
    void regist(MemberRegistVo vo);

    //檢查使用者名稱和手機號是否唯一
    void checkPhoneUnique(String phone) throws PhoneExistException;
    void checkUseNameUnique(String username) throws UserNameExistException;
}
  • serviceImpl 異常機制
@Service("memberService")
public class MemberServiceImpl extends ServiceImpl<MemberDao, MemberEntity> implements MemberService {

    @Autowired
    MemberLevelDao memberLevelDao;

    //會員的註冊功能
    @Override
    public void regist(MemberRegistVo vo) {
        MemberDao memberDao = this.baseMapper;
        MemberEntity entity = new MemberEntity();

        //1、註冊時設定預設等級:普通會員。
        MemberLevelEntity levelEntity = memberLevelDao.getDefaultLevel();
        entity.setLevelId(levelEntity.getId());

        //2、檢查使用者名稱和手機號是否唯一,為了讓controller感知到異常,需要使用異常機制
        checkPhoneUnique(vo.getPhone());
        checkUseNameUnique(vo.getUsername());

        entity.setMobile(vo.getPhone());
        entity.setUsername(vo.getUsername());

        //密碼要進行加密儲存 -> 不可逆 -> MD5 -> MD5鹽值 -> BCryptPasswordEncoder
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        String encode = passwordEncoder.encode(vo.getPassword());
        entity.setPassword(encode);

        //其他的預設資訊

        //儲存
        memberDao.insert(entity);
    }
	
  	//-------------------------------------異常機制----------------------------------------
    //檢查使用者名稱和手機號是否唯一   
    @Override
    public void checkPhoneUnique(String phone) throws PhoneExistException{
        MemberDao memberDao = this.baseMapper;
        Integer count1 = memberDao.selectCount(new QueryWrapper<MemberEntity>().eq("mobile", phone));
        if (count1 > 0){
            throw new PhoneExistException();
        }
    }
    @Override
    public void checkUseNameUnique(String username) throws UserNameExistException{
        MemberDao memberDao = this.baseMapper;
        Integer count2 = memberDao.selectCount(new QueryWrapper<MemberEntity>().eq("username", username));
        if (count2 > 0){
            throw new UserNameExistException();
        }
    }
}
  • 設定使用者的預設等級
  • MemberLevelDao
    //1、註冊時設定預設等級:普通會員 -> default_status = 1
    MemberLevelEntity getDefaultLevel();
  • MemberLevelDao.xml
    <select id="getDefaultLevel" resultType="com.wulawula.gulimall.member.entity.MemberLevelEntity">
        SELECT * FROM ums_member_level WHERE default_status = 1
    </select>

加密

1、 MD5加密

  • MD5不可直接進行加密儲存:
  • 抗修改性 -> MD5暴力破解網站每天大量搜尋MD5的值。 -> 製作成彩虹表 每個數段的MD5值都是一樣的,就會儲存起來,等待查詢
@Test
void contextLoads() {
  	
  	//e10adc3949ba59abbe56e057f20f883e
	String s = DigestUtils.md5Hex("123456");
	
	//7e8feb2276322ecddd4423b649dfd4d9
	String s = DigestUtils.md5Hex("123456 ");
  
  	System.out.println(s)
}

2、 MD5鹽值加密

  • 鹽值加密 加鹽(隨機鹽): $1$ + 8位字元
  • 且兩次的執行結果是一樣的
  • 驗證密碼進行登陸:再次將使用者輸入的 123456 進行鹽值加密 【鹽值要去資料庫查,資料庫需要儲存鹽值欄位】
@Test
void contextLoads() {
  	
  	//$1$YdkJTmB1$jsWeFyCOFNJ1jXRp3rHJe1
	String s = Md5Crypt.md5Crypt("123456".getBytes());
  
    //第一次:$1$88888888$.04CpISZfzlbsDnC6Fjr11
    //第二次:$1$88888888$.04CpISZfzlbsDnC6Fjr11
	String s = Md5Crypt.md5Crypt("123456".getBytes(),"$1$88888888");

  	System.out.println(s)
}

3、 BCryptPasswordEncoder推薦

  • Spring的密碼加密器(基於鹽值加密)

  • 也是拼接了隨機字串 即使要加密的資料一樣,得到的加密字串卻不一樣

  • 加密使用 encode 解密使用 matches

  • 一個字串加密兩次得到兩個不同的結果,將兩個結果解密,發現兩個都是123456的加密字元,返回true

@Test
void contextLoads() {
  	
	BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();

    //第一次:$2a$10$6VBbkOIjUzoYsA/Wo/glsui6uZ1kJ8lMaygh6YjdhY2tMC3c4kPf6
    //第二次:$2a$10$ezArPw7xXBsZ.DMFLFEh1u4VnOccNSDCwHnnih3mGVRc5ewzpVV9q
    String s = passwordEncoder.encode("123456");

    //true
    boolean matches = passwordEncoder.matches("123456", "$2a$10$6VBbkOIjUzoYsA/Wo/glsui6uZ1kJ8lMaygh6YjdhY2tMC3c4kPf6");
    //true
    boolean matches = passwordEncoder.matches("123456", "$2a$10$ezArPw7xXBsZ.DMFLFEh1u4VnOccNSDCwHnnih3mGVRc5ewzpVV9q");

	System.out.println(s + " -> " + matches);
}

呼叫服務 -> 登入註冊服務

  • feign 【會員的註冊功能 -> 遠端呼叫】
@FeignClient("gulimall-member")
public interface MemberFeignService {

    //會員的註冊功能 -> 遠端呼叫
    @PostMapping("/member/member/regist")
    R regist(@RequestBody UserRegistVo vo);
}
  • VO 【此處使用JSR303校驗vo欄位】
@Data
public class UserRegistVo {

    @NotEmpty(message = "使用者名稱不能為空") //不能為空
    @Length(min = 6,max = 18,message = "使用者名稱必須是6-18位字元") //使用者名稱長度限制
    private String username;

    @NotEmpty(message = "密碼不能為空") //不能為空
    @Length(min = 6,max = 18,message = "使用者名稱必須是6-18位字元") //密碼長度限制
    private String password;

    @NotEmpty(message = "手機號不能為空") //不能為空
    @Pattern(regexp = "^[1]([3-9])[0-9]{9}$",message = "手機號格式不正確") //正規表示式 第一個數字是1,第2個數字是3-9內的,後面的9個數字是0-9之內的
    private String phone;

    @NotEmpty(message = "驗證碼不能為空") //不能為空
    private String code;
}
  • controller 【此處的註冊功能是根據是根據另一個服務呼叫的】
  • 若JSR303校驗未通過,則通過BindingResult封裝錯誤資訊,並重定向至註冊頁面
  • 若通過JSR303校驗,則需要從redis中取值判斷驗證碼是否正確,正確的話通過會員服務註冊
  • 會員服務呼叫成功則重定向至登入頁,否則封裝遠端服務返回的錯誤資訊返回至註冊頁面
  • RedirectAttributes可以通過session儲存資訊並在重定向的時候攜帶過去

	@Autowired
    StringRedisTemplate stringRedisTemplate; //Redis
    @Autowired
    MemberFeignService memberFeignService; //遠端呼叫

	//註冊

    @PostMapping("/regist")
    //@Valid開啟校驗功能。錯誤資訊都會在BindingResult中
    //RedirectAttributes 重定向攜帶資料,不要用Model了
    // TODO 重定向攜帶資料,利用session原理,將資料放在session中。只要跳轉到下一個頁面且去除這個資料以後,session裡面的資料就會被刪除
    // TODO 分散式下session會出現問題

    public String regist(@Valid UserRegistVo vo, BindingResult result, /*Model model*/ RedirectAttributes redirectAttributes){
        /***
         *  1、使用JSR303校驗填寫的內容是否出錯。出錯則重定向到註冊頁面。且將錯誤資訊返回給前臺。
         */
        //1、如果BindingResult裡有校驗錯誤
        if (result.hasErrors()){

            //將收集的錯誤資訊手機成一個map集合。傳給前臺頁面。用於展示
            //方法 1
//            HashMap<String, String> errors = new HashMap<>();
//            result.getFieldErrors().stream().map(fieldError -> {
//                String field = fieldError.getField(); //哪個欄位出現了錯誤
//                String defaultMessage = fieldError.getDefaultMessage(); //錯誤的訊息
//                errors.put(field,defaultMessage);
//                return errors;
//            });

            //方法 2
            Map<String, String> errors = result.getFieldErrors().stream().collect(Collectors.toMap(FieldError::getField, FieldError::getDefaultMessage));

            //            model.addAttribute("errors",errors);
            redirectAttributes.addFlashAttribute("errors",errors); //重定向的寫法

            //2、校驗出錯。重定向到註冊頁
            /**
             * 使用 return "forward:/reg.html"; 會出現
             * 問題:Request method 'POST' not supported的問題
             * 原因:使用者註冊-> /regist[post] ------>轉發/reg.html (路徑對映預設都是get方式訪問的)
             */
//            return "foeward:/reg.html"; //轉發
//            return "reg";    //轉發會出現重複提交的問題,不要以轉發的方式
            //使用重定向  解決重複提交的問題。但面臨著資料不能攜帶的問題,就用RedirectAttributes
            return "redirect:http://auth.wulawula.com/reg.html";
        }

        /***
         *  2、校驗驗證碼。呼叫遠端服務,真正的進行註冊
         */
        //1、校驗驗證碼
        String code = vo.getCode();
        String rediscode = stringRedisTemplate.opsForValue().get(AuthServerConstant.SMS_CODE_CACHE + vo.getPhone());
        //判斷redis裡是否有驗證碼
        if (! StringUtils.isEmpty(rediscode)){
            String s = rediscode.split("_")[0];
            //判斷驗證碼是否正確
            if (code.equals(s)){
                //校驗成功。刪除驗證碼(下次再用舊的驗證碼就不能通過了) --> 令牌機制
                stringRedisTemplate.delete(AuthServerConstant.SMS_CODE_CACHE + vo.getPhone());
                //2、驗證碼通過  真正的註冊,呼叫遠端讀取進行註冊
                R r = memberFeignService.regist(vo);
                //判斷狀態碼
                if (r.getCode() == 0){
                    //成功
                    return "redirect:http:/login.html";
                }else {
                    //失敗 異常
                    HashMap<String, String> errors = new HashMap<>();
                    errors.put("msg",r.getData(new TypeReference<String>(){}));
                    redirectAttributes.addFlashAttribute("errors",errors);

                    return "redirect:http://auth.wulawula.com/reg.html";
                }

            }else {
                //驗證碼出錯
                Map<String, String> errors = new HashMap<>();
                errors.put("code","驗證碼錯誤");
                redirectAttributes.addFlashAttribute("errors",errors); //重定向的寫法
                return "redirect:http://auth.wulawula.com/reg.html";
            }
        }else {
            //redis裡沒有驗證碼
            Map<String, String> errors = new HashMap<>();
            errors.put("code","驗證碼錯誤");
            redirectAttributes.addFlashAttribute("errors",errors); //重定向的寫法
            return "redirect:http://auth.wulawula.com/reg.html";
        }

        //3、註冊成功,回到登入頁
//        return "redirect:http://auth.wulawula.com/login.html";
//        return "redirect:http:/login.html"; //重定向
    }

轉發和重定向的區別

forward 【轉發】redirect 【重定向】
位址列伺服器的直接跳轉,客戶端瀏覽器並不知道,位址列內容不變(伺服器內部的動作)為客戶端瀏覽器根據url地址重新向伺服器請求,位址列變(有可能是請求的URI地址發生變化)
資料共享共享瀏覽器傳來的request全新的request
運用的地方使用者登入後根據角色跳轉頁面在使用者登出後跳轉主頁或其他頁面
效率較高(比重定向少了一次伺服器請求)較低

05、簡單登入


被呼叫的服務 -> 會員服務

  • 錯誤狀態碼列舉類
public enum BizCodeEnume {

    UNKNOWN_EXCEPTION(10000,"系統未知異常"),
    VAILD_EXCEPTION(10001,"引數格式校驗失敗"),
    PRODUCT_UP_EXCEPTION(11000,"商品上架異常"),
    SMS_CODE_EXCEPTION(10002,"驗證碼獲取頻率太高,稍後再試"),
    USER_EXIST_EXCEPTION(15001,"使用者名稱已存在"),
    PHONE_EXIST_EXCEPTION(15002,"手機號已存在"),
    LOGINACCT_PASSWORD_INVAILD_EXCEPTION(15003,"賬號或密碼錯誤");

    private int code;
    private String msg;
    BizCodeEnume(int code,String msg){
        this.code = code;
        this.msg = msg;
    }
    public int getCode(){
        return code;
    }
    public String getMsg(){
        return msg;
    }
}
  • vo 【封裝登陸傳過來的資料】
@Data
public class MemberLoginVo {
    private String loginacct;
    private String password;
}
  • controller 【會員的登入功能 -> 遠端呼叫】
@PostMapping("/login")
    public R login(@RequestBody MemberLoginVo vo){

        MemberEntity entity = memberService.login(vo);
        if (entity != null){
            return R.ok();
        }else {
            return R.error(BizCodeEnume.LOGINACCT_PASSWORD_INVAILD_EXCEPTION.getCode(),BizCodeEnume.LOGINACCT_PASSWORD_INVAILD_EXCEPTION.getMsg());
        }
    }
  • MemberServiceImpl 【會員的登入功能】
  • 因為是使用的 BCryptPasswordEncoder 加密。必須去資料庫查詢,然後解密比對
@Service("memberService")
public class MemberServiceImpl extends ServiceImpl<MemberDao, MemberEntity> implements MemberService {

    @Override
    public MemberEntity login(MemberLoginVo vo) {

        String loginacct = vo.getLoginacct();
        String password = vo.getPassword();

        //1、去資料庫查詢 SELECT * FROM ums_member WHERE username = ? OR mobile = ?
        MemberDao memberDao = this.baseMapper;
        //使用者名稱和手機號都可以當作登入名使用
        MemberEntity entity = memberDao.selectOne(new QueryWrapper<MemberEntity>().eq("username", loginacct).or().eq("mobile",loginacct));

        if (entity == null){
            //登陸失敗 手機號或使用者名稱匹配不上
            return null;
        }else {
            //判斷密碼是否匹配  需要解密
            String passwordDb = entity.getPassword();
            //解密
            BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
            //2、進行密碼匹配  						引數1:明文密碼  引數2:資料庫查到的加密密碼
            boolean matches = passwordEncoder.matches(password, passwordDb); 
            if (matches){
                return entity;
            }else {
                return null;
            }
        }
    }
}

呼叫的服務 -> 登入註冊服務

通過會員服務遠端呼叫登入介面

  • 如果呼叫成功,重定向至首頁
  • 如果呼叫失敗,則封裝錯誤資訊並攜帶錯誤資訊重定向至登入頁
  • vo 【封裝前臺傳來的資料】
//登入資料封裝
@Data
public class UserLoginVo {
    private String loginacct;
    private String password;
}
  • feign
@FeignClient("gulimall-member")
public interface MemberFeignService {
    //會員的登入功能 -> 遠端呼叫
    @PostMapping("/member/member/login")
    R login(@RequestBody UserLoginVo vo);
}
  • controller 【登入】
@Controller
public class LoginController {

    @Autowired
    MemberFeignService memberFeignService; //呼叫遠端服務

    @PostMapping("/login")
    public String login(UserLoginVo vo,RedirectAttributes result){

        //遠端登陸
        R r = memberFeignService.login(vo);
        if (r.getCode() == 0){
            //成功 -> 首頁
            return "redirect:http://wulawula.com"; //重定向
        }else {
            //失敗 -> 登入頁
            HashMap<String, String> errors = new HashMap<>();
            errors.put("msg",r.getData("msg",new TypeReference<String>(){}));
            result.addFlashAttribute("errors",errors);
            System.out.println(errors);
            return "redirect:http://auth.wulawula.com/login.html"; //重定向
        }
    }
} 
  • html
<div class="si_bom1 tab" style="display: none;">
	<div class="error">
		<div></div>
		請輸入賬戶名和密碼
	</div>

	<form action="/login" method="post">
<!--						<div style="color: red" th:text="${errors != null ? (#maps.containsKey(errors,'msg')?errors.msg:''):''}"></div>-->
		<ul>
			<li class="top_1">
				<img src="/static/login/JD_img/user_03.png" class="err_img1"/>
				<input type="text" name="loginacct" placeholder=" 郵箱/使用者名稱/已驗證手機" class="user"/>
			</li>
			<li>
				<img src="/static/login/JD_img/user_06.png" class="err_img2"/>
				<input type="password" name="password" placeholder=" 密碼" class="password"/>
			</li>
			<li class="bri">
				<a href="">忘記密碼</a>
				<div style="color: red" th:text="${errors != null ? (#maps.containsKey(errors,'msg')?errors.msg:''):''}"></div>
			</li>
			<li class="ent">
				<button class="btn2" type="submit"><a>&nbsp; &nbsp;</a></button>
			</li>
		</ul>
	</form>
</div>

06、社交登入 OAuth2.0


註冊登入3


微博作為社交賬號進行社交登入為例

1、進入微博開放平臺且登陸自己的微博賬號

https://open.weibo.com/

2、點選 微連線 -> 網站接入 -> 立即接入

在這裡插入圖片描述

3、設定回撥頁面路徑

在這裡插入圖片描述

4、點選 文件 -> OAuth2.0授權認證 【裡面有需要的資源】

在這裡插入圖片描述

5、引導需要授權的使用者到如下地址:URL GET請求

https://api.weibo.com/oauth2/authorize?client_id=YOUR_CLIENT_ID&response_type=code&redirect_uri=YOUR_REGISTERED_REDIRECT_URI

  • YOUR_CLIENT_ID:你申請的應用的AppKey
  • YOUR_REGISTERED_REDIRECT_URI:登陸之後重定向的跳轉URI(授權回撥頁)

HTML

<li>
	<a href="https://api.weibo.com/oauth2/authorize?client_id=2416521972&response_type=code&redirect_uri=http://wulawula.com/success">
	<img style="width: 50px;height: 18px" src="/static/login/JD_img/weibo.png" />
	</a>
</li>

點選登陸之後。跳轉成功

在這裡插入圖片描述

6、如果使用者同意授權,頁面跳轉至 授權回撥頁/?code=CODE

code是我們用來換取令牌的引數

http://wulawula.com/success?code=f53d29f843b681a28d1fe6321f00476e

7、 換取Access Token(訪問令牌)URL Code只能換取一次Access Token

https://api.weibo.com/oauth2/access_token?client_id=YOUR_CLIENT_ID&client_secret=YOUR_CLIENT_SECRET&grant_type=authorization_code&redirect_uri=YOUR_REGISTERED_REDIRECT_URI&code=CODE

  • YOUR_CLIENT_ID: 你申請的應用的AppKey
  • YOUR_CLIENT_SECRET: 建立網站應用時的App Secret
  • YOUR_REGISTERED_REDIRECT_URI: 登陸之後重定向的跳轉URI(授權回撥頁)
  • CODE:換取令牌的認證碼
https://api.weibo.com/oauth2/access_token?client_id=2416521972&client_secret=bb9dd96f8c51c01799ab5915672b6cdb&grant_type=authorization_code&redirect_uri=http://wulawula.com/success&code=f53d29f843b681a28d1fe6321f00476e

使用PostMan測試得到以下JSON POST請求 可以發現得到了 Access Token

{
    "access_token": "2.00iOICcGqkTXdC0ca7b7d7e5moPOSC",
    "remind_in": "157679999",
    "expires_in": 157679999,
    "uid": "6058806080",
    "isRealName": "true"
}

8、使用獲得的Access Token呼叫API

點選介面管理 -> 根據使用者ID獲取使用者資訊 -> 測試介面示例:

https://api.weibo.com/2/users/show.json?access_token=2.00iOICcGqkTXdC0ca7b7d7e5moPOSC&uid=6058806080

使用PostMan測試得到以下JSON POST請求

{
    "id": 6058806080,
    "idstr":  "個人資訊......",
    "class":  個人資訊......,
    "screen_name":  "個人資訊......",
    "name": "個人資訊......",
    "province":  "個人資訊......",
    "city":  "個人資訊......",
    "location":  "個人資訊......",
    "description":  "個人資訊......",
    ......
}

在這裡插入圖片描述

1、如果使用者同意授權,頁面跳轉至 授權回撥頁/?code=CODE code是我們用來換取令牌的引數

http://wulawula.com/success?code=f53d29f843b681a28d1fe6321f00476e

此處也應該遮蔽掉

2、應用的 client_idclient_secret 應該都是保密的,不應該由頁面處理,應該由伺服器後臺處理

修改授權回撥頁 http://auth.wulawula.com/oauth2.0/weibo/success

登陸之後重定向的跳轉URI

在這裡插入圖片描述

流程圖

在這裡插入圖片描述


被呼叫的服務

  • SocialUser 【社交登入封裝遠端呼叫的資料。註冊進會員表】
@Data
public class SocialUser {
    private String access_token;
    private String remind_in;
    private long expires_in;
    private String uid; //社交id
    private String isRealName;
}
  • MemberOAuth2Entity 【封裝資料的實體類】
@Data
@TableName("ums_member_oauth2")
public class MemberOAuth2Entity implements Serializable {
	private static final long serialVersionUID = 1L;
	   // id
	private Long id;
	   // 會員等級id
	private Long levelId;
	   // 使用者名稱
	private String username;
	   // 密碼
	private String password;
	   // 暱稱
	private String nickname;
	   // 手機號碼
	private String mobile;
	   // 郵箱
	private String email;
	   // 頭像
	private String header;
	   // 性別
	private Integer gender;
	   // 生日
	private Date birth;
	   // 所在城市
	private String city;
	   // 職業
	private String job;
	   // 個性簽名
	private String sign;
	   // 使用者來源
	private Integer sourceType;
	   // 積分
	private Integer integration;
	   // 成長值
	private Integer growth;
	   // 啟用狀態
	private Integer status;
	   // 註冊時間
	private Date createTime;
	   //	社交帳號id
	private String socialUid;
	   //	社交帳號訪問令牌
	private String accessToken;
	   //	社交帳號訪問令牌的過期時間
	private Long expiresIn;
}
  • MemberOAuth2Controller
  • 登入包含兩種流程,實際上包括了註冊和登入
  • 如果之前未使用該社交賬號登入,則使用token呼叫開放api獲取社交賬號相關資訊,註冊並將結果返回
  • 如果之前已經使用該社交賬號登入,則更新token並將結果返回
@RestController
@RequestMapping("/member/oauth")
public class MemberOAuth2Controller {

    @Autowired
    MemberOAuth2Service memberOAuth2Service;

    /***
     *      社交使用者(登入+註冊 合併) -> 遠端呼叫
     */
    @PostMapping("/oauth2/login")
    public R oauthlogin(@RequestBody SocialUser socialUser) throws Exception {
        MemberOAuth2Entity entity = memberOAuth2Service.oauthlogin(socialUser);
        if (entity != null){
            //TODO 1、登陸成功處理
            return R.ok().setData(entity);
        }else {
            return R.error(BizCodeEnume.LOGINACCT_PASSWORD_INVAILD_EXCEPTION.getCode(),BizCodeEnume.LOGINACCT_PASSWORD_INVAILD_EXCEPTION.getMsg());
        }
    }
}
  • MemberOAuth2ServiceImpl
@Service("memberOAuth2Service")
public class MemberOAuth2ServiceImpl extends ServiceImpl<MemberOAuth2Dao, MemberOAuth2Entity>  implements MemberOAuth2Service {

    @Autowired
    MemberOAuth2Dao memberOAuth2Dao;


    //社交使用者(登入+註冊 合併) -> 遠端呼叫
    @Override
    public MemberOAuth2Entity oauthlogin(SocialUser socialUser) throws Exception {
        String uid = socialUser.getUid();
        //1、判斷當前社交使用者是否已經登陸過系統
        MemberOAuth2Dao memberDao = this.baseMapper;

        MemberOAuth2Entity memberOAuth2Entity = memberDao.selectOne(new QueryWrapper<MemberOAuth2Entity>().eq("social_uid", uid));
        if (memberOAuth2Entity != null){
//------------------登入------------------
            //2、這個使用者已經註冊過了
            MemberOAuth2Entity update = new MemberOAuth2Entity();

            update.setId(memberOAuth2Entity.getId());
            update.setAccessToken(socialUser.getAccess_token());
            update.setExpiresIn(socialUser.getExpires_in());

            memberDao.updateById(update);

            memberOAuth2Entity.setAccessToken(socialUser.getAccess_token());
            memberOAuth2Entity.setExpiresIn(socialUser.getExpires_in());
            return memberOAuth2Entity;
        }else {
//------------------註冊------------------
            //3、沒有查到當前社交使用者對應的記錄,註冊
            MemberOAuth2Entity regist = new MemberOAuth2Entity();
            //4、查詢當前社交使用者的社交登入賬號(暱稱、性別等)
            try {
                HashMap<String, String> query = new HashMap<>();
                query.put("access_token",socialUser.getAccess_token());
                query.put("uid",socialUser.getUid());
                HttpResponse response = HttpUtils.doGet("https://api.weibo.com", "/2/users/show.json", "get", new HashMap<String,String>(), query);
                if (response.getStatusLine().getStatusCode() == 200){ //返回狀態碼 200
                    //查詢成功
                    String json = EntityUtils.toString(response.getEntity()); //得到請求體物件轉為JSON
                    JSONObject jsonObject = JSON.parseObject(json); //直接從json裡取值
                    String name = jsonObject.getString("name"); //暱稱
                    String gender = jsonObject.getString("gender"); //性別

                    //---將查詢到的資訊寫入實體類---
                    regist.setNickname(name);
                    regist.setGender("m".equals(gender)? 1 : 0);
                }
            }catch (Exception e){
                e.printStackTrace();
            }

            regist.setSocialUid(socialUser.getUid());
            regist.setAccessToken(socialUser.getAccess_token());
            regist.setExpiresIn(socialUser.getExpires_in());
            System.out.println("regist =" + regist);
            memberDao.insert(regist);
            return regist;
        }
    }
}

呼叫的服務

SocialUserMemberOAuth2ResponseVo 分別對應被呼叫程式的 SocialUser 和 **MemberOAuth2Entity **

  • feign
@FeignClient("gulimall-member")
public interface MemberFeignService {

    //社交使用者(登入+註冊 合併) -> 遠端呼叫
    @PostMapping("/member/oauth/oauth2/login")
    R oauthlogin(@RequestBody SocialUser socialUser);
}
  • OAuth2Controller 【處理社交登入請求】
  • 通過HttpUtils傳送請求獲取token,並將token等資訊交給member服務進行社交登入
  • 若獲取token失敗或遠端呼叫服務失敗,則封裝錯誤資訊重新轉回登入頁
@Slf4j
@Controller
public class OAuth2Controller {

    @Autowired
    MemberFeignService memberFeignService;

    /***
     * 根據使用者登入得到的code換取Access Token
     */
    @RequestMapping("/oauth2.0/weibo/success")
    public String weibo(@RequestParam("code") String code) throws Exception {

        //1、根據code換取Access Token  ->  說明登陸成功
        HashMap<String, String> map = new HashMap<>();
        map.put("client_id","24******72");
        map.put("client_secret","bb9dd96f8c************15672b6cdb");
        map.put("grant_type","authorization_code");
        map.put("redirect_uri","http://auth.wulawula.com/oauth2.0/weibo/success");
        map.put("code",code);
        /***
         *  引數1:換取Access Token的主機地址
         *  引數2:path給哪裡發請求
         *  引數3:請求方式
         *  引數4:請求頭
         *  引數5:查詢引數
         *  引數6:請求體
         */
        Map<String, String> headers = new HashMap<>();
        HttpResponse response = HttpUtils.doPost("https://api.weibo.com", "/oauth2/access_token", "post", headers, null, map);

        //2、處理
        //獲取響應狀態碼
        if (response.getStatusLine().getStatusCode() == 200){
            //成功 -> 獲取到了 Access Token
                // response.getEntity() 獲取到響應體內容
                // EntityUtils.toString() 可以將響應體內容轉換為JSON
            String json = EntityUtils.toString(response.getEntity());
            // JSON -> 實體類物件
            SocialUser socialUser = JSON.parseObject(json, SocialUser.class);

            //知道是哪個社交使用者了
            // 3.如果使用者是第一次進來 自動註冊進來(為當前社交使用者生成一個會員資訊 以後這個賬戶就會關聯這個賬號)
            //登陸或者註冊這個社交使用者
            R login = memberFeignService.oauthlogin(socialUser);
            if (login.getCode() == 0){
                MemberOAuth2ResponseVo data = login.getData("data", new TypeReference<MemberOAuth2ResponseVo>() {
                });
                System.out.println("登陸成功...... 使用者資訊: " + data);
                log.info("登陸成功,使用者資訊:{}");
                //登陸成功,跳回首頁
                return "redirect:http://wulawula.com";
            }else {
                //失敗 -> 重定向到登陸頁
                return "redirect:http://auth.wulawula.com/login.html";
            }
        }else {
            //失敗 -> 重定向到登陸頁
            return "redirect:http://auth.wulawula.com/login.html";
        }
    }
}

07、Session


註冊登入11

jsessionid相當於銀行卡卡號,存在伺服器的session相當於儲存的現金,每次都能通過jsessionid取出現金


分散式下session的共享問題

在這裡插入圖片描述

  • 問題1、同一服務的叢集,session不同步問題

如果第一次訪問1號伺服器,且儲存了cookie在1號伺服器的記憶體空間中,因為是分散式叢集環境,第2次可能訪問2號伺服器,雖然請求也攜帶了cookie,但是之前cookie存的是在1號伺服器的記憶體中,2號伺服器並沒有,因此也會出現問題

  • 問題2、:不同服務、不同域名的session跨域問題

正常情況下session不可跨域,它有自己的作用範圍,session只能在當前域名(Domain)下生效,域名一換,session就找不到了

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-jbZNR4Ji-1608222854287)(D:\Java專案\00_A_Java筆記\小技術點筆記配套圖片\註冊登入12.png)]


問題1解決方式:同一服務的叢集,session不同步問題

1、session複製 不推薦

註冊登入14

2、客戶端儲存 不推薦

在這裡插入圖片描述

3、hash一致性

在這裡插入圖片描述

4、統一儲存 整合 Redis + SpringSession 推薦

在這裡插入圖片描述


問題2解決方式:不同服務、不同域名的session跨域問題

示例:父域名:wulawula.com 子域名:auth.wulawula.com order.wulawula.com

示例:第一次瀏覽器在會員服務裡登陸成功,會員服務會將資料存到session,且session不在自己服務的記憶體裡儲存,讓其在redis裡儲存,然後給瀏覽器返回cookie,此處讓這個cookie的JSESSIONID對應的的作用域不能只是自己的服務,此處應該放大作用域,由子域名放大到父域名,使得以後的訪問都由父域名進行訪問,且資料都統一儲存在redis中,就算要使用JSESSIONID取出對應的資料,都要去Redis裡去查詢

在這裡插入圖片描述


08、整合SpringSession


因為是分散式環境。登入模組為 gulimall-auth-server。 首頁模組為 gulimall-product。公共模組為 gulimall-common

gulimall-common

  • MemberOAuth2ResponseVo 【序列化】
@ToString
@Data
//由於SpringSession預設使用jdk進行序列化,通過匯入RedisSerializer修改為json序列化
public class MemberOAuth2ResponseVo implements Serializable {
	   // id
	private Long id;
	   // 會員等級id
	private Long levelId;
	   // 使用者名稱
	private String username;
	   // 密碼
	private String password;
	   // 暱稱
	private String nickname;
	   // 手機號碼
	private String mobile;
	   // 郵箱
	private String email;
	   // 頭像
	private String header;
	   // 性別
	private Integer gender;
	   // 生日
	private Date birth;
	   // 所在城市
	private String city;
	   // 職業
	private String job;
	   // 個性簽名
	private String sign;
	   // 使用者來源
	private Integer sourceType;
	   // 積分
	private Integer integration;
	   // 成長值
	private Integer growth;
	   // 啟用狀態
	private Integer status;
	   // 註冊時間
	private Date createTime;
	   //	社交帳號id
	private String socialUid;
	   //	社交帳號訪問令牌
	private String accessToken;
	   //	社交帳號訪問令牌的過期時間
	private Long expiresIn;
}

gulimall-auth-server

  • pom
<dependency>
	<groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>
  • properties
###配置SpringSession
#SpringSession的儲存型別
spring.session.store-type=redis
#session的過期時間 (預設30分鐘)
server.servlet.session.timeout=30m

啟動類新增註解 整合Redis作為session的儲存 @EnableRedisHttpSession

  • 自定義配置 config 【兩個模組都需要加】
/***
 * 需要解決子域共享問題
 */
@Configuration
public class GulimallSessionConfig {

    //需要解決子域共享問題 -> 預設是子域,需要改為父域
    @Bean
    public CookieSerializer cookieSerializer(){
        DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
        cookieSerializer.setDomainName("wulawula.com"); //指定作用域為父域 而不是子域
        cookieSerializer.setCookieName("WULA_SESSION"); //修改名字
        return cookieSerializer;
    }

    //使用JSON的序列化方式來序列化物件資料到redis中 -> redis中看到的就不是二進位制字元了
    @Bean
    public RedisSerializer<Object> springSessionDefaultRedisSerializer(){
        return new GenericJackson2JsonRedisSerializer();
    }
}
  • controller
@Slf4j
@Controller
public class OAuth2Controller {

    @Autowired
    MemberFeignService memberFeignService;

    /***
     * 根據使用者登入得到的code換取Access Token
     * @param code
     * @return
     * @throws Exception
     */
    @RequestMapping("/oauth2.0/weibo/success")
    public String weibo(@RequestParam("code") String code, HttpSession session) throws Exception {

        //1、根據code換取Access Token  ->  說明登陸成功
        HashMap<String, String> map = new HashMap<>();
        map.put("client_id","2416521972");
        map.put("client_secret","bb9dd96f8c51c01799ab5915672b6cdb");
        map.put("grant_type","authorization_code");
        map.put("redirect_uri","http://auth.wulawula.com/oauth2.0/weibo/success");
        map.put("code",code);
        /***
         *  引數1:換取Access Token的主機地址
         *  引數2:path給哪裡發請求
         *  引數3:請求方式
         *  引數4:請求頭
         *  引數5:查詢引數
         *  引數6:請求體
         */
        Map<String, String> headers = new HashMap<>();
        HttpResponse response = HttpUtils.doPost("https://api.weibo.com", "/oauth2/access_token", "post", headers, null, map);

        //2、處理
        //獲取響應狀態碼
        if (response.getStatusLine().getStatusCode() == 200){
            System.out.println("獲取token成功");
            //成功 -> 獲取到了 Access Token
                // response.getEntity() 獲取到響應體內容
                // EntityUtils.toString() 可以將響應體內容轉換為JSON
            String json = EntityUtils.toString(response.getEntity());
            // JSON -> 實體類物件
            SocialUser socialUser = JSON.parseObject(json, SocialUser.class);

            //知道是哪個社交使用者了
            // 3.如果使用者是第一次進來 自動註冊進來(為當前社交使用者生成一個會員資訊 以後這個賬戶就會關聯這個賬號)
            //登陸或者註冊這個社交使用者
            R login = memberFeignService.oauthlogin(socialUser);
            if (login.getCode() == 0){
                System.out.println("登陸成功");
                MemberOAuth2ResponseVo data = login.getData("data", new TypeReference<MemberOAuth2ResponseVo>() {
                });
                System.out.println("登陸成功...... 使用者資訊: " + data);
                log.info("登陸成功,使用者資訊:{}");
/*session---------------------------------------------*/
                //TODO 1、預設發的令牌.session=唯一的字串。作用域為當前域   (需要解決子域共享問題)
                //TODO 2、使用JSON的序列化方式來序列化物件資料到redis中
                session.setAttribute("loginUser",data); //直接加入
/*--------------------------------------------------*/
                //登陸成功,跳回首頁
                return "redirect:http://wulawula.com";
            }else {
                System.out.println("登陸失敗");
                //失敗 -> 重定向到登陸頁
                return "redirect:http://auth.wulawula.com/login.html";
            }
        }else {
            //失敗 -> 重定向到登陸頁
            System.out.println("獲取token失敗");
            return "redirect:http://auth.wulawula.com/login.html";
        }
    }
}

gulimall-product

  • pom
<dependency>
	<groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>
  • properties
###配置SpringSession
#SpringSession的儲存型別
spring.session.store-type=redis

啟動類新增註解 整合Redis作為session的儲存 @EnableRedisHttpSession

  • 自定義配置 config 【兩個模組都需要加】
/***
 * 需要解決子域共享問題
 */
@Configuration
public class GulimallSessionConfig {

    //需要解決子域共享問題 -> 預設是子域,需要改為父域
    @Bean
    public CookieSerializer cookieSerializer(){
        DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
        cookieSerializer.setDomainName("wulawula.com"); //指定作用域為父域 而不是子域
        cookieSerializer.setCookieName("WULA_SESSION"); //修改名字
        return cookieSerializer;
    }

    //使用JSON的序列化方式來序列化物件資料到redis中 -> redis中看到的就不是二進位制字元了
    @Bean
    public RedisSerializer<Object> springSessionDefaultRedisSerializer(){
        return new GenericJackson2JsonRedisSerializer();
    }
}
  • js 此處使用三元判斷判斷顯示暱稱問題
<li>
	<a href="http://auth.wulawula.com/login.html">你好,請登入 [[${session.loginUser==null ? '' : session.loginUser.nickname}]]</a>
</li>
<li>
	 <a href="http://auth.wulawula.com/reg.html" class="li_2">免費註冊</a>
</li>

註冊登入20

自動延期:redis中的資料也是有過期時間的


SpringSession核心原理 - 裝飾者模式

  • 原生的獲取session時是通過HttpServletRequest獲取的
  • SpringSession是包裝過的

@EnableRedisHttpSession匯入RedisHttpSessionConfiguration.class配置

  • 1、給容器中新增了一個元件

​ SessionRepository=》》》 【RedisIndexedSessionRepository】=>redis操作session.session的增刪改查的封裝類

  • 2、SessionRepositoryFilter=》Filter: session儲存過濾器,每個請求過來都必須經過filter
    • 1、建立的時候,就自動從容器中獲取到了SessionRepository:
    • 2、原生的request,response都被包裝。SessionRepositoryRequestWrapper,SessionRepositoryResponseWrapper
    • 3、以前獲取session。 request.getSession()
    • 4、以後獲取session。wrapperedRequest.getSession();===>SressionRepository中獲取到
@Override
protected void doFilterInternal(HttpServletRequest request,
      HttpServletResponse response, FilterChain filterChain)
      throws ServletException, IOException {
   request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository);
 
   //包裝原始的請求物件
   SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryRequestWrapper(
         request, response, this.servletContext);
   //包裝原始的響應物件
   SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryResponseWrapper(
         wrappedRequest, response);
 
   try {
      //此執行鏈使用的是包裝後的物件
      filterChain.doFilter(wrappedRequest, wrappedResponse); //使得我們獲取的session都是包裝過的
   }
   finally {
      wrappedRequest.commitSession();
   }
}

09、細節修改


  • gulimall-common 的 AuthServerConstant
  • 存redis時定義的key
public class AuthServerConstant {
    public static final String SMS_CODE_CACHE= "sms:code:"; //redis存取驗證碼的字首
    public static final String LOGIN_USER= "loginUser"; //redis存取登陸的key的字首
}
  • gulimall-auth-server 的 LoginController
  • 嘗試獲取session。有session就是登陸了 -> 首頁 。 沒有session就是沒登陸 -> 登入頁
    @GetMapping("/login.html")
    public String loginPage(HttpSession session){
        Object attribute = session.getAttribute(AuthServerConstant.LOGIN_USER);
        if (attribute == null){
            //沒登陸
            return "login";
        }else {
            //登陸了
            return "redirect:http://wulawula.com"; //重定向
        }
    }
  • GulimallConfig 【檢視對映】
  • 需要判斷是否有session。檢視對映不能直接跳轉。
@Configuration
public class GulimallConfig implements WebMvcConfigurer {

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
                              /*  引數1:url路徑       引數2:檢視名 */
//        registry.addViewController("/login.html").setViewName("login"); 
        registry.addViewController("/reg.html").setViewName("reg");
    }
}

es服務也需要加入 08整合SpringSession中的 pom、properties、自定義配置 config、以及redis的pom

只要session沒過期,讓其在首頁、搜尋頁、商品頁都顯示登陸的會員的暱稱

  • 示例 html
<li>
	<a href="http://auth.wulawula.com/login.html" class="li_2" th:if="${session.loginUser == null}">你好,請登入</a>
    <a class="li_2" th:else style="width: 100px">歡迎:[[${session.loginUser.nickname}]]</a>
</li>
<li>
	<a href="http://auth.wulawula.com/reg.html" th:if="${session.loginUser == null}">免費註冊</a>
</li>

010、單點登入


希望一個賬號登陸旗下多個不同應用。一處登入處處登入。一處退出處處退出

單點登入2


1、配置XXL-SSO

XXL-SSO 是三個分散式單點登入框架。只需要登入一次就可以訪問所有相互信任的應用系統。

擁有"輕量級、分散式、跨域、Cookie+Token均支援、Web+APP均支援""等特性。現已開放原始碼,開箱即用。

編排:

  • ssoserver.com 登入認證伺服器
  • client1.com 客戶端1
  • client2.com 客戶端2

1、修改SwitchHosts

# XLL-OOS
127.0.0.1 ssoserver.com
127.0.0.1 client1.com 
127.0.0.1 client2.com 

修改 xxl-sso配置檔案 application.properties 【且知道不同的埠號以及訪問路徑】

8080/xxl-sso-server

8081/xxl-sso-web-sample-springboot

2、打包gitee專案

刪除 .git檔案 --> 在xxl-sso頁 cmd --> mvn clean package -Dmaven.skip.test=true

D:\Java專案\單點登入demo\xxl-sso>mvn clean package -Dmaven.skip.test=true

3、啟動 在target頁 cmd --> java -jar xxl-sso-server-1.1.1-SNAPSHOT.jar

D:\Java專案\單點登入demo\xxl-sso\xxl-sso-server\target>java -jar xxl-sso-server-1.1.1-SNAPSHOT.jar

訪問 http://ssoserver.com:8080/xxl-sso-server/login

4、啟動客戶端 方法同3 【一個客戶端啟動兩次,修改埠號即可】

第一個加上server.port=8081

D:\Java專案\單點登入demo\xxl-sso\xxl-sso-samples\xxl-sso-web-sample-springboot\target>java -jar xxl-sso-web-sample-springboot-1.1.1-SNAPSHOT.jar --server.port=8081

訪問 http://client1.com:8081/xxl-sso-web-sample-springboot/

第二個加上server.port=8082

D:\Java專案\單點登入demo\xxl-sso\xxl-sso-samples\xxl-sso-web-sample-springboot\target>java -jar xxl-sso-web-sample-springboot-1.1.1-SNAPSHOT.jar --server.port=8082

訪問 http://client1.com:8082/xxl-sso-web-sample-springboot/


2、測試

略…

未完 接下一章.

相關文章