穀粒商城高階篇筆記1
這裡寫自定義目錄標題
0、ElasticSearch
1、Nginx配置域名問題
01、Nginx(反向代理) 配置
> 正向代理和反向代理
配置反向代理
我們打算訪問 wulawula.com 網址轉到我們本地的localhost:10000
1、使用 SwitchHosts 工具 配置域名 使用管理員方式開啟
2、使用 自定義的域名訪問 預設訪問的是Nginx自定義的html頁面
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
4、重啟 nginx docker restart nginx
5、訪問 wulawula.com 靜態資源正常顯示
2、JMeter 壓力測試
影響效能考慮點包括:
- 資料庫、應用程式、中介軟體( tomact、Nginx)、網路和作業系統等方面首先考慮自己的應用屬於CPu密集型還是I0密集型
01、基本測試
1、建立一個 **執行緒組**
2、設定執行緒組引數 所謂執行緒數就是併發使用者數
3、線上程組下建立 HTTP請求
4、設定 HTTP引數(此處以百度為例)
5、線上程組下建立 監聽器 此處以:檢視結果樹、彙總報告、聚合報告、彙總圖 為例
6、點選執行 執行完後 自動停止
點選執行完後可以看到資料
- 檢視結果樹
- 可以發現每次 HTTP請求都完成
- 彙總報告 單位毫秒
- 平均1.241s 最小0.015s 最大5.265s 異常0%
- 聚合報告 單位毫秒
- 90%的在 1.465s完成 95%的在1.649s完成 99%的在2.514s完成
- 彙總圖 單位毫秒
- 可以看到圖表資訊
注意
:自己寫的專案。壓測資料的時候 測試的資料和此處的記憶體大小也有關係
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
2 .然後雙擊MaxUserPort
,輸入數值資料為65534
,基數選擇十進位制
(如果是分散式執行的話,控制機器和負載機器都需要這樣操作哦)
也可以設定這個:
- 右擊
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對應的版本 點選連結進去
- 3、複製 此處URL
- 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
- 新增索引
設定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()));
}
快取Redis
壓測內容 | 壓測執行緒數 | 吞吐量/s | 90%響應時間 | 99%響應時間 |
---|---|---|---|---|
Nginx | 200 | 1363.7 | 6 | 1949 |
GateWay | 200 | 5070.1 | 25 | 1340 |
簡單服務 | 200 | 11596.6 | 37 | 70 |
頁面一級選單渲染 | 200 | 290.7 | 804 | 1443 |
三級分類資料獲取 | 200 | 10.7(db) | 18574 | 18610 |
三級分類資料獲取(優化業務) | 200 | 131.6 | 2220 | 3781 |
三級分類(Redis) | 200 | 408.5 | 634 | 1374 |
首頁全量資料獲取 | 200 | 11.6(靜態) | 23477 | 24138 |
首頁全量(開快取、優化資料庫、關日誌) | 200 | 58.8 | 15433 | 14579 |
Nginx+GateWay | 200 | |||
GateWay+簡單服務 | 200 | 1780.2 | 140 | 1975 |
全鏈路 | 200 | 567.1 | 1586 | 4956 |
中介軟體越多,效能損失越大,大多都損失在網路互動了
業務:DB(MySql優化)、模板的渲染速度(上線需要開啟快取)、靜態資源 影響
5、快取
為了系統效能的提升,我們一般都會將部分資料放入快取中,加速訪間。而db承擔資料落盤工作。
哪些資料適合放入快取:
- 即時性、資料一致性要求不高的
- 訪問量大且更新頻字不高的資料(讀多,寫少)
舉例:電商類應用,商品分類,商品列表等適合快取並加一個失效時間(根據資料更新頻率來定),後臺如果釋出一個商品,買家需要5分鐘才能看到新的商品一般還是可以接受的。
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
推薦
- 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
原因:
- springboot 2.0 以後預設使用
lettuce
作為操作 redis 的客戶端,它使用netty
進行通訊 - lettuce 的 bug 導致 netty 堆外記憶體溢位
-Xmx300m
:如果沒有指定堆外記憶體,netty 預設使用堆記憶體(Xmx)
作為 堆外記憶體
解決方案:
注意:不能使用 -Dio.netty.maxDirectMemory
只調大堆外記憶體
- 升級
lettuce
客戶端 - 切換使用
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
04、快取穿透、雪崩、擊穿
- 快取穿透 --> 空結果快取
- 快取雪崩 --> 設定過期時間(加隨機值)
- 快取擊穿 --> 加鎖
快取穿透
指高併發情況下一直查詢一個不存在的資料
。查詢到null,但是並沒有將null放入快取,就進行多次查資料庫,導致資料庫瞬時壓力增大,最終導致崩潰
快取雪崩
指快取設定的過期時間一致,在某一時刻快取大面積失效
,高併發情況下的查詢,進而轉到了資料庫進行查詢,導致資料庫瞬時壓力過大,最終導致崩潰
快取擊穿
指失效的快取為查詢頻率很高的某一個單點key --> 熱點資料
,在某一時刻失效,高併發情況下的查詢,進而轉到了資料庫進行查詢,導致資料庫瞬時壓力過大,最終導致崩潰
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的資料
3、測試 每個微服務只列印了一次 查詢了資料庫 進入鎖------ 一個微服務一把鎖
04、分散式鎖
分散式鎖演化1
docker exec -it redis redis-cli //連線客戶端
docker lock wula NX //使用NX引數 佔位
分散式鎖演化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
分散式鎖演化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 分散式鎖
8、SpringCache
簡介
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;
}
細節
上面我們將一級分類資料的資訊快取到Redis中了,快取到Redis中資料具有如下的特點:
-
如果快取中有,方法不會被呼叫;
-
key預設自動生成;形式為
快取的名字::SimpleKey [](自動生成的key值)
; -
快取的value值,預設使用jdk序列化機制,將序列化後的資料快取到redis;
-
預設
TTL
時間為-1,表示永不過期
然而這些並不能夠滿足我們的需要,我們希望:
- 能夠指定生成快取所使用的key;
- 指定快取的資料的存活時間;
- 將快取的資料儲存為
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());
}
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());
}
只使用
@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)寫模式(為了保證 --> 快取與資料一致)
- 讀寫加鎖;
- 引入canal,感知到mysql的更新去更新資料庫;
- 讀多寫少,直接去資料庫查詢就行;
總結:
-
常規資料(讀多寫少,即時性,一致性要求不高的資料):完全可以使用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">共 <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("brandId",'+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("catalog3Id",'+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("attrs","'+attr.attrId+'_'+val+'")'}"
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);
}
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())
}
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>頁 到第</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("attrs","'+attr.attrId+'_'+val+'")'}"
th:text="${val}">顯示所有屬性值</a>
</li>
</ul>
</div>
</div>
11、非同步
12、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
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>
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>
- 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
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>登 錄</a></button>
</li>
</ul>
</form>
</div>
06、社交登入 OAuth2.0
以
微博作為社交賬號
進行社交登入為例
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_id
和 client_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;
}
}
}
呼叫的服務
SocialUser 和 MemberOAuth2ResponseVo 分別對應被呼叫程式的 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
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複製 不推薦
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>
自動延期: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、單點登入
希望一個賬號登陸旗下多個不同應用。一處登入處處登入。一處退出處處退出
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、測試
略…
未完 接下一章.
相關文章
- 06穀粒商城-高階篇六
- 05穀粒商城-高階篇五
- 04穀粒商城-高階篇四
- 03穀粒商城-高階篇三
- 02穀粒商城-高階篇二
- 穀粒商城筆記筆記
- 穀粒商城-基礎篇
- 穀粒商城
- MySQL高階篇筆記MySql筆記
- redis學習筆記(詳細)——高階篇Redis筆記
- 穀粒學院-2-mybatisplusMyBatis
- 高階軟體工程筆記軟體工程筆記
- 高階電影顆粒感影像lightroom預設OOM
- TypeScript筆記(二)高階型別TypeScript筆記型別
- MySQL高階學習筆記(二)MySql筆記
- 《Kafka筆記》3、Kafka高階APIKafka筆記API
- JavaScript高階程式設計筆記JavaScript程式設計筆記
- 穀粒joycon霍爾電磁搖桿釋出
- React高階實戰 - 我的筆記React筆記
- swift高階運算子-讀書筆記Swift筆記
- 《python運維和開發實戰-高階篇》視訊課程筆記Python運維筆記
- JavaScript 高階程式設計 第三章 讀書筆記(1)JavaScript程式設計筆記
- 高併發設計筆記(續篇)筆記
- 【JAVA】筆記(12)---集合(1)-概述篇Java筆記
- 穀粒學院(四)前端開發之ES6 | Vue前端Vue
- Javascript高階程式設計 學習筆記JavaScript程式設計筆記
- oracle學習筆記(十一) 高階查詢Oracle筆記
- 《JavaScript 高階程式設計》精讀筆記JavaScript程式設計筆記
- js高階 物件導向 學習筆記JS物件筆記
- 《JavaScript高階程式設計》筆記:DOM(十)JavaScript程式設計筆記
- 《python運維和開發實戰-高階篇》視訊課程筆記二Python運維筆記
- nginx高階篇rewriteNginx
- python高階程式設計讀書筆記(一)Python程式設計筆記
- C#高階程式設計 讀書筆記C#程式設計筆記
- MySQL 高階特性篇教程MySql
- Oracle高階培訓 第5課 學習筆記Oracle筆記
- Oracle高階培訓 第6課 學習筆記Oracle筆記
- Oracle高階培訓 第7課 學習筆記Oracle筆記