谷粒商城學習筆記以及錯誤總結:
1:Vue前端項目配置調用api接口:
// api接口請求地址
window.SITE_CONFIG['baseUrl'] = 'http://localhost:8080/renren-fast';
最開始這樣寫,但發現是通過網關來處理的
后台renren-fast設置允許跨域請求:
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowCredentials(true)
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.maxAge(3600);
}
2:
SpringCloud Alibaba簡介:
- 注冊中心:
- 配置中心:
- 網關:
No Feign Client for loadBalancing defined. Did you forget to include spring-cloud-starter-loadbalancer?:
添加spring-cloud-starter-loadbalancer,排除 Nacos 的 Ribbon:
https://blog.csdn.net/qq_43416157/article/details/114318283
在項目中多配置集啟動失敗(暫未找到原因,下載下來的項目都能成功啟動,真是沒閃)
##Nacos加載多配置集
spring.cloud.nacos.config.extension-configs[0].data-id=datasource.yml
spring.cloud.nacos.config.extension-configs[0].group=dev
spring.cloud.nacos.config.extension-configs[0].refresh=true
spring.cloud.nacos.config.extension-configs[1].data-id=mybatis.yml
spring.cloud.nacos.config.extension-configs[1].group=dev
spring.cloud.nacos.config.extension-configs[1].refresh=true
spring.cloud.nacos.config.extension-configs[2].data-id=other.yml
spring.cloud.nacos.config.extension-configs[2].group=dev
spring.cloud.nacos.config.extension-configs[2].refresh=true
網關gateway:
動態上下線:發送請求需要知道商品服務的地址,如果商品服務器有123服務器,1號掉線后,還得改,所以需要網關動態地管理,他能從注冊中心中實時地感知某個服務上線還是下線。【先通過網關,網關路由到服務提供者】
攔截:請求也要加上詢問權限,看用戶有沒有權限訪問這個請求,也需要網關。
所以我們使用spring cloud的gateway組件做網關功能。
網關是請求流量的入口,常用功能包括路由轉發,權限校驗,限流控制等。springcloud gateway取代了zuul網關。
https://spring.io/projects/spring-cloud-gateway
三大核心概念:
Route: The basic building block of the gateway. It is defined by an ID, a destination URI, a collection of predicates斷言, and a collection of filters. A route is matched if the aggregate predicate is true.發一個請求給網關,網關要將請求路由到指定的服務。路由有id,目的地uri,斷言的集合,匹配了斷言就能到達指定位置,
Predicate斷言: This is a Java 8 Function Predicate. The input type is a Spring Framework ServerWebExchange. This lets you match on anything from the HTTP request, such as headers or parameters.就是java里的斷言函數,匹配請求里的任何信息,包括請求頭等。根據請求頭路由哪個服務
Filter: These are instances of Spring Framework GatewayFilter that have been constructed with a specific factory. Here, you can modify requests and responses before or after sending the downstream request.過濾器請求和響應都可以被修改。
客戶端發請求給服務端。中間有網關。先交給映射器,如果能處理就交給handler處理,然后交給一系列filer,然后給指定的服務,再返回回來給客戶端。

- id: test_route
uri: https://www.baidu.com/?tn=98010089_dg&ch=12
predicates:
- Query=url,baidu
訪問http://localhost:88/?url=baidu即可跳轉到百度
// api接口請求地址
window.SITE_CONFIG['baseUrl'] = 'http://localhost:88/api';
88是網關端口號,由application.yml配置來處理前端發送過來的請求
網關-路徑重寫:
- id: admin_route
uri: lb://renren-fast
predicates:
- Path=/api/**
filters:
- RewritePath=/api/(?<segment>/?.*), /renren-fast/$\{segment}
如出現Access to XMLHttpRequest at 'http://localhost:88/api/sys/login' from origin 'http://localhost:8001' has been blocked by CORS policy: The 'Access-Control-Allow-Origin' header contains multiple values 'http://localhost:8001, http://localhost:8001', but only one is allowed.
這表示同源請求有多個,原因是配置了多個跨越,把renren-fast的跨越請求注釋即可!
跨域的解決方案
方法1:設置nginx包含admin和gateway。都先請求nginx,這樣端口就統一了
方法2:讓服務器告訴預檢請求能跨域
解決方案1:

解決方案二為在服務端2配置允許跨域
在響應頭中添加:參考:https://blog.csdn.net/qq_38128179/article/details/84956552
Access-Control-Allow-Origin : 支持哪些來源的請求跨域
Access-Control-Allow-Method : 支持那些方法跨域
Access-Control-Allow-Credentials :跨域請求默認不包含cookie,設置為true可以包含cookie
Access-Control-Expose-Headers : 跨域請求暴露的字段
CORS請求時,XMLHttpRequest對象的getResponseHeader()方法只能拿到6個基本字段:
Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma
如果想拿到其他字段,就必須在Access-Control-Expose-Headers里面指定。
Access-Control-Max-Age :表明該響應的有效時間為多少秒。在有效時間內,瀏覽器無須為同一請求再次發起預檢請求。請注意,瀏覽器自身維護了一個最大有效時間,如果該首部字段的值超過了最大有效時間,將失效
解決方法:在網關中定義“GulimallCorsConfiguration”類,該類用來做過濾,允許所有的請求跨域。
package com.atguigu.gulimall.gateway.config;
@Configuration // gateway
public class GulimallCorsConfiguration {
@Bean // 添加過濾器
public CorsWebFilter corsWebFilter(){
// 基於url跨域,選擇reactive包下的
UrlBasedCorsConfigurationSource source=new UrlBasedCorsConfigurationSource();
// 跨域配置信息
CorsConfiguration corsConfiguration = new CorsConfiguration();
// 允許跨域的頭
corsConfiguration.addAllowedHeader("*");
// 允許跨域的請求方式
corsConfiguration.addAllowedMethod("*");
// 允許跨域的請求來源
corsConfiguration.addAllowedOrigin("*");
// 是否允許攜帶cookie跨域
corsConfiguration.setAllowCredentials(true);
// 任意url都要進行跨域配置
source.registerCorsConfiguration("/**",corsConfiguration);
return new CorsWebFilter(source);
}
}
https://easydoc.net/s/78237135/ZUqEdvA4/7C3tMIuF
高級篇:
一、Elastic Search:
ES:
ElasticSearch
mysql用作持久化存儲,ES用作檢索
基本概念:index庫>type表>document文檔
index索引
動詞:相當於mysql的insert
名詞:相當於mysql的db
Type類型
在index中,可以定義一個或多個類型
類似於mysql的table,每一種類型的數據放在一起
Document文檔
保存在某個index下,某種type的一個數據document,文檔是json格式的,document就像是mysql中的某個table里面的內容。每一行對應的列叫屬性
1:安裝ES
直接點擊:https://www.elastic.co/cn/downloads/past-releases#elasticsearch 官網選擇版本,這里的版本最好對應Java pom.xml
<properties>
<java.version>1.8</java.version>
<elasticsearch.version>7.6.2</elasticsearch.version>
</properties>
的版本
下載之后解壓文件,切換到解壓文件路徑下,執行
切換到bin目錄下 輸入 elasticsearch.bat #運行es
即可看到如下內容:
{
"name" : "HOMEWCC",
"cluster_name" : "elasticsearch",
"cluster_uuid" : "PkQa3LzwQIa6micTjlM0_g",
"version" : {
"number" : "7.9.2",
"build_flavor" : "default",
"build_type" : "zip",
"build_hash" : "d34da0ea4a966c4e49417f2da2f244e3e97b4e6e",
"build_date" : "2020-09-23T00:45:33.626720Z",
"build_snapshot" : false,
"lucene_version" : "8.6.2",
"minimum_wire_compatibility_version" : "6.8.0",
"minimum_index_compatibility_version" : "6.0.0-beta1"
},
"tagline" : "You Know, for Search"
}
2:Kibana 的安裝(Windows版本)
什么是Kibana?
Kibana 是一個設計出來用於和 Elasticsearch 一起使用的開源的分析與可視化平台,可以用 kibana 搜索、查看、交互存放在Elasticsearch 索引里的數據,使用各種不同的圖表、表格、地圖等展示高級數據分析與可視化,基於瀏覽器的接口使你能快速創建和分享實時展現Elasticsearch查詢變化的動態儀表盤,讓大量數據變得簡單,容易理解。
kibana的版本和elasticsearch的版本和必須一致。
打開下圖的路徑文件kibana.yml(可以通過記事本方式)

設置elasticsearch.url為啟動的elasticsearch(http://localhost:9200/)(其實按照默認可以不用修改配置文件)

進入kibana的bin目錄,雙擊kibana.bat(第一種方式)

通過cmd的方式進入kibana的bin目錄,運行kibana.bat(第二種方式);

訪問:http://localhost:5601,出現以下界面即完成安裝。
3:ElasticSearch:IK分詞及windows安裝IK分詞
ik分詞器下載地址:https://github.com/medcl/elasticsearch-analysis-ik/releases
pinyin分詞器下載地址:https://github.com/medcl/elasticsearch-analysis-pinyin/releases
ik分詞器要與ES版本一致
elasticsearch目錄的plugins目錄新建 analysis-ik ,analysis-pinyin文件夾,然后將我們第一步下載的IK分詞包解壓到analysis-ik文件夾,重新啟動elasticsearch
驗證分詞:
標准分詞器
POST _analyze { "analyzer": "standard", "text": "The 2 Brown-Foxes bone." }
使用分詞器查詢結果:
POST _analyze
{
"analyzer": "ik_smart",
"text": "我是中國人"
}
GET _analyze
{
"analyzer": "ik_max_word",
"text":"我是中國人"
}
自定義詞庫:
安裝Nginx:
直接參考:https://www.cnblogs.com/jiangwangxiang/p/8481661.html
需要在html文件夾里面建立es文件夾,里面放分詞組fenci.txt文本
修改elasticsearch-7.6.2\plugins\analysis-ik\config\IKAnalyzer.cfg.xml:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
<comment>IK Analyzer 擴展配置</comment>
<!--用戶可以在這里配置自己的擴展字典 -->
<entry key="ext_dict"></entry>
<!--用戶可以在這里配置自己的擴展停止詞字典-->
<entry key="ext_stopwords"></entry>
<!--用戶可以在這里配置遠程擴展字典 -->
<entry key="remote_ext_dict">http://127.0.0.1/es/fenci.txt</entry>
<!--用戶可以在這里配置遠程擴展停止詞字典-->
<!-- <entry key="remote_ext_stopwords">words_location</entry> -->
</properties>
修改完成后,需要重啟elasticsearch容器,否則修改不生效。
在Kibana輸入
GET _analyze { "analyzer": "ik_smart", "text":"櫻桃薩其馬,帶你甜蜜入夏" }進行測試
商品上架功能:
#商品上架的最終選用的數據模型:
DELETE gulimall_product
PUT gulimall_product
{
"mappings": {
"properties": {
"skuId": {
"type": "long"
},
"spuId": {
"type": "keyword"
},
"skuTitle": {
"type": "text",
"analyzer": "ik_smart"
},
"skuPrice": {
"type": "keyword"
},
"skuImg": {
"type": "keyword"
},
"saleCount": {
"type": "long"
},
"hasStock": {
"type": "boolean"
},
"hotScore": {
"type": "long"
},
"brandId": {
"type": "long"
},
"catelogId": {
"type": "long"
},
"brandName": {
"type": "keyword"
},
"brandImg": {
"type": "keyword"
},
"catelogName": {
"type": "keyword"
},
"attrs": {
"type": "nested",
"properties": {
"attrId": {
"type": "long"
},
"attrName": {
"type": "keyword"
},
"attrValue": {
"type": "keyword"
}
}
}
}
}
}
GET gulimall_product/_search
二、公用工具:
三、商城系統首頁:
四、Nginx:
1:Nginx+網關+openFeign的邏輯:
正向代理:如Science and the Internet,隱藏客戶端信息
反向代理:屏蔽內網服務器信息,負載均衡訪問
gulimall.com在hosts文件配置,由於在windows上面修改的hosts文件,windows里面安裝了Nginx
訪問gulimall.com的時候不帶端口,默認就來到了Nginx默認的80端口,
在Nginx里面配置了專門監聽【指定80端口來的可以訪問gulimall.com】
listen 80; server_name gulimall.com *.gulimall.com;
Nginx監聽到后在代理給網關,在代理網關過程中,
Nginx會丟掉Host,所以
修改nginx/conf/conf.d/gulimall.conf,接收到gulimall.com的訪問后,如果是/,轉交給指定的upstream,由於nginx的轉發會丟失host頭,造成網關不知道原host,所以我們添加頭信息
location / {
proxy_pass http://gulimall;
proxy_set_header Host $host;
}
,網關在做處理Api進行訪問
2、Nginx配置文件
3、Nginx+網關配置
配置gateway為服務器,將域名為**.gulimall.com轉發至商品服務。配置的時候注意 網關優先匹配的原則,所以要把這個配置放到后面
- id: gulimall_host_route
uri: lb://gulimall-product
predicates:
- Host=**.gulimall.com
由於1,2,3部在windows下面沒有。所以在windows下面沒能配置
五、壓力測試:
JVM參數、工具、調優筆記:https://blog.csdn.net/hancoder/article/details/108312012
Jmeter
下載:https://jmeter.apache.org/download_jmeter.cgi
Jconsole、JvisualVM:
運行狀態:
運行:正在運行
休眠:sleep
等待:wait
駐留:線程池里面的空閑線程
監視:阻塞的線程,正在等待鎖
要監控GC,安裝插件:工具-插件。可用插件-檢查最新版本 報錯的時候百度“插件中心”,改個JVM對應的插件中心url.xml.z
安裝visual GC
優化
SQL耗時越小越好,一般情況下微秒級別
命中率越高越好,一般情況下不能低於95%
鎖等待次數越低越好,等待時間越短越好
中間件越多,性能損失雨大,大多都損失在網絡交互了
Nginx動靜分離
由於動態資源和靜態資源目前都處於服務端,所以為了減輕服務器壓力,我們將js、css、img等靜態資源放置在Nginx端,以減輕服務器壓力
靜態文件上傳到 mydata/nginx/html/static/index/css,這種格式
修改index.html的靜態資源路徑,加上static前綴src="/static/index/img/img_09.png"
修改/mydata/nginx/conf/conf.d/gulimall.conf
如果遇到有/static為前綴的請求,轉發至html文件夾
location /static {
root /usr/share/nginx/html;
}
location / {
proxy_pass http://gulimall;
proxy_set_header Host $host;
}
六、redisson分布式鎖與緩存:
【谷粒商城】高級篇-分布式鎖與緩存:
緩存
- 本地緩存:和微服務同一個進程。缺點:分布式時造成緩存數據不一致
- 分布式緩存:緩存中間件
lettuce堆外內存溢出bug
當進行壓力測試時后期后出現堆外內存溢出OutOfDirectMemoryError
產生原因:
1)、springboot2.0以后默認使用lettuce作為操作redis的客戶端,它使用netty進行網絡通信
2)、lettuce的bug導致netty堆外內存溢出。netty如果沒有指定堆外內存,默認使用Xms的值,可以使用-Dio.netty.maxDirectMemory進行設置
解決方案:由於是lettuce的bug造成,不要直接使用-Dio.netty.maxDirectMemory去調大虛擬機堆外內存,治標不治本。
1)、升級lettuce客戶端。但是沒有解決的
2)、切換使用jedis
<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>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
緩存失效
緩存穿透
緩存穿透是指緩存和數據庫中都沒有的數據,而用戶不斷發起請求,如發起為id為“-1”的數據或id為特別大不存在的數據。這時的用戶很可能是攻擊者,攻擊會導致數據庫壓力過大。
解決:緩存空對象、布隆過濾器、mvc攔截器
緩存雪崩
緩存雪崩是指在我們設置緩存時key采用了相同的過期時間,導致緩存在某一時刻同時失效,請求全部轉發到DB,DB瞬時壓力過重雪崩。
解決方案:
規避雪崩:緩存數據的過期時間設置隨機,防止同一時間大量數據過期現象發生。
如果緩存數據庫是分布式部署,將熱點數據均勻分布在不同緩存數據庫中。
設置熱點數據永遠不過期。
出現雪崩:降級 熔斷
事前:盡量保證整個 redis 集群的高可用性,發現機器宕機盡快補上。選擇合適的內存淘汰策略。
事中:本地ehcache緩存 + hystrix限流&降級,避免MySQL崩掉
事后:利用 redis 持久化機制保存的數據盡快恢復緩存
緩存擊穿
緩存雪崩和緩存擊穿不同的是:
緩存擊穿 指 並發查同一條數據。緩存擊穿是指緩存中沒有但數據庫中有的數據(一般是緩存時間到期),這時由於並發用戶特別多,同時讀緩存沒讀到數據,又同時去數據庫去取數據,引起數據庫壓力瞬間增大,造成過大壓力
緩存雪崩是不同數據都過期了,很多數據都查不到從而查數據庫。
解決方案:
設置熱點數據永遠不過期。
加互斥鎖:業界比較常用的做法,是使用mutex。簡單地來說,就是在緩存失效的時候(判斷拿出來的值為空),不是立即去load db去數據庫加載,而是先使用緩存工具的某些帶成功操作返回值的操作(比如Redis的SETNX或者Memcache的ADD)去set一個mutex key,當操作返回成功時,再進行load db的操作並回設緩存;否則,就重試整個get緩存的方法。
緩存擊穿:加鎖
不好的方法是synchronized(this),肯定不能這么寫 ,不具體寫了
鎖時序問題:之前的邏輯是查緩存沒有,然后取競爭鎖查數據庫,這樣就造成多次查數據庫。
解決方法:競爭到鎖后,再次確認緩存中沒有,再去查數據庫。

分布式緩存:
本地緩存問題:每個微服務都要有緩存服務、數據更新時只更新自己的緩存,造成緩存數據不一致
解決方案:分布式緩存,微服務共用 緩存中間件
分布式鎖
分布式項目時,但本地鎖只能鎖住當前服務,需要分布式鎖
三種情況分析:
設置好了鎖,玩意服務出現宕機,沒有執行刪除鎖邏輯,這就造成了死鎖
解決:設置過期時間
業務還沒執行完鎖就過期了,別人拿到鎖,自己執行完去刪了別人的鎖
解決:鎖續期(redisson有看門狗),。刪鎖的時候明確是自己的鎖。如uuid
判斷uuid對了,但是將要刪除的時候鎖過期了,別人設置了新值,那刪除了別人的鎖
解決:刪除鎖必須保證原子性(保證判斷和刪鎖是原子的)。使用redis+Lua腳本完成,腳本是原子的
Redisson:
Redisson是一個在Redis的基礎上實現的Java駐內存數據網格(In-Memory Data Grid)。它不僅提供了一系列的分布式的Java常用對象,還提供了許多分布式服務。其中包括(BitSet, Set, Multimap, SortedSet, Map, List, Queue, BlockingQueue, Deque, BlockingDeque, Semaphore, Lock, AtomicLong, CountDownLatch, Publish / Subscribe, Bloom filter, Remote service, Spring cache, Executor service, Live Object service, Scheduler service) Redisson提供了使用Redis的最簡單和最便捷的方法。Redisson的宗旨是促進使用者對Redis的關注分離(Separation of Concern),從而讓使用者能夠將精力更集中地放在處理業務邏輯上。
可重入鎖(Reentrant Lock):
A調用B。AB都需要同一鎖,此時可重入鎖就可以重入,A就可以調用B。不可重入鎖時,A調用B將死鎖。
如果負責儲存這個分布式鎖的Redisson節點宕機以后,而且這個鎖正好處於鎖住的狀態時,這個鎖會出現鎖死的狀態。為了避免這種情況的發生,Redisson內部提供了一個監控鎖的看門狗,它的作用是在Redisson實例被關閉前,不斷的延長鎖的有效期。默認情況下,看門狗的檢查鎖的超時時間是30秒鍾(每到20s就會自動續借成30s,是1/3的關系)
讀寫鎖(ReadWriteLock):
基於Redis的Redisson分布式可重入讀寫鎖RReadWriteLock Java對象實現了java.util.concurrent.locks.ReadWriteLock接口。其中讀鎖和寫鎖都繼承了RLock接口。
分布式可重入讀寫鎖允許同時有多個讀鎖和一個寫鎖處於加鎖狀態。
信號量(Semaphore):
信號量為存儲在redis中的一個數字,當這個數字大於0時,即可以調用acquire()方法增加數量,也可以調用release()方法減少數量,但是當調用release()之后小於0的話方法就會阻塞,直到數字大於0
閉鎖(CountDownLatch):
基於Redisson的Redisson分布式閉鎖(CountDownLatch)Java對象RCountDownLatch采用了與java.util.concurrent.CountDownLatch相似的接口和用法。
以下代碼只有offLatch()被調用5次后 setLatch()才能繼續執行
RCountDownLatch latch = redisson.getCountDownLatch("anyCountDownLatch");
latch.trySetCount(1);
latch.await();
// 在其他線程或其他JVM里
RCountDownLatch latch = redisson.getCountDownLatch("anyCountDownLatch");
latch.countDown();
緩存和數據庫一致性:
雙寫模式:寫數據庫后,寫緩存
問題:並發時,線程2寫進入,寫完DB后都寫緩存。有暫時的臟數據
失效模式:寫完數據庫后,刪緩存
問題:還沒存入數據庫,線程2又讀到舊的DB了(線程2讀取到還沒刪除的舊Redis數據)
解決:緩存設置過期時間,定期更新
解決:寫數據寫時,加分布式的讀寫鎖。
解決方案:
如果是用戶緯度數據(訂單數據、用戶數據),這種並發幾率非常小,不用考慮這個問題,緩存數據加上過期時間,每隔一段時間觸發讀的主動更新即可
如果是菜單,商品介紹等基礎數據,也可以去使用canal訂閱binlog的方式
緩存數據+過期時間也足夠解決大部分業務對於緩存的要求。
通過加鎖保證並發讀寫,寫寫的時候按順序排好隊。讀讀無所謂。所以適合使用讀寫鎖。(業務不關心臟數據,允許臨時臟數據可忽略);
總結:
我們能放入緩存的數據本就不應該是實時性、一致性要求超高的。所以緩存數據的時候加上過期時間,保證每天拿到當前最新數據即可。
我們不應該過度設計,增加系統的復雜性
遇到實時性、一致性要求高的數據,就應該查數據庫,即使慢點。

springCache:
Cache接口的實現包括RedisCache、EhCacheCache、ConcurrentMapCache等
使用Spring緩存抽象時我們需要關注以下兩點:
1、確定方法需要緩存以及他們的緩存策略
2、從緩存中讀取之前緩存存儲的數據
配置:
#SpringCache配置
spring.cache.type=redis
#設置超時時間,默認是毫秒
spring.cache.redis.time-to-live=3600000
#設置Key的前綴,如果指定了前綴,則使用我們定義的前綴,否則使用緩存的名字作為前綴
#spring.cache.redis.key-prefix=CACHE_
#配置是否使用前綴
spring.cache.redis.use-key-prefix=true
#是否緩存空值,防止緩存穿透
spring.cache.redis.cache-null-values=true
緩存使用@Cacheable@CacheEvict:
第一個方法存放緩存,第二個方法清空緩存
* (1)更新分類數據
* (2)@CacheEvict采用失效模式更新緩存
* (3)可以有兩種方式來實現同時更新緩存到redis的一級或三級分類數據
* (4)Caching組合刪除多個緩存
* (5)allEntries = true刪除value = {"category"}的所有緩存分區
#root.methodName以value的值作為key
@Cacheable(value = {"category"}, key = "#root.methodName")
SpringCache原理與不足:
1)、讀模式
1:緩存穿透:查詢一個null數據。解決方案:緩存空數據,可通過spring.cache.redis.cache-null-values=true
2;緩存擊穿:大量並發進來同時查詢一個正好過期的數據。解決方案:加鎖 ? 默認是無加鎖的;
使用sync = true來解決擊穿問題
3:緩存雪崩:大量的key同時過期。解決:加隨機時間。
2)、寫模式:(緩存與數據庫一致)
1:讀寫加鎖。
2:引入Canal,感知到MySQL的更新去更新Redis
讀多寫多,直接去數據庫查詢就行
3)、總結:
常規數據(讀多寫少,即時性,一致性要求不高的數據,完全可以使用Spring-Cache):
寫模式(只要緩存的數據有過期時間就足夠了)
特殊數據:特殊設計
七、檢索:
商品檢索服務:
需求:
上架的商品才可以在網站展示。
上架的商品需要可以被檢索。
分析sku在es中如何存儲
商品mapping
分析:商品上架在es中是存sku還是spu?
1)、檢索的時候輸入名字,是需要按照sku的title進行全文檢索的
2)、檢素使用商品規格,規格是spu的公共屬性,每個spu是一樣的
3)、按照分類id進去的都是直接列出spu的,還可以切換。
4〕、我們如果將sku的全量信息保存到es中(包括spu屬性〕就太多字段了
nested嵌入式對象
屬性是"type": “nested”,因為是內部的屬性進行檢索
數組類型的對象會被扁平化處理(對象的每個屬性會分別存儲到一起)
渲染檢索頁面:
ES語句DSL
此處先寫出如何檢索指定的商品,如檢索"華為"關鍵字
- 嵌入式的屬性
- highlight:設置該值后,返回的時候就包裝過了
- 查出結果后,附屬欄也要對應變化
- 嵌入式的聚合時候也要注意
八、異步編排:
異步編排參考網上鏈接即可:https://blog.csdn.net/weixin_45762031/article/details/103519459
九、商品詳情:
sql構建
我們觀察商品頁面與VO,可以大致分為5個部分需要封裝。1 2 4比較簡單,單表就查出來了。我們分析3、5
我們在url中首先有sku_id,在從sku_info表查標題的時候,順便查到了spu_id、catelog_id,這樣我們就可以操作剩下表了。
分組規格參數
在5查詢規格參數中
pms_product_attr_value 根據spu_id獲得spu相關屬性
pms_attr_attrgroup_relation根據catelog_id獲得屬性的分組
優化:異步編排
因為商品詳情是查多個sql,所以可以利用線程池進行異步操作,但是因為有的步驟需要用到第一步的spu_d結果等想你想,所以需要使用異步編排。
調用thenAcceptAsync()可以接受上一步的結果且沒有返回值。
最后調用get()方法使主線程阻塞到其他線程完成任務。
/**
* 商品詳情信息返回數據
*
* @param skuId
* @return
*/
@Override
public SkuItemVo item(Long skuId) throws ExecutionException, InterruptedException {
SkuItemVo skuItemVo = new SkuItemVo();
CompletableFuture<SkuInfoEntity> infoFutrue = CompletableFuture.supplyAsync(() -> {
//1 sku基本信息
SkuInfoEntity info = getById(skuId);
skuItemVo.setInfo(info);
return info;
}, executor);
CompletableFuture<Void> spuInfoFuture = infoFutrue.thenAcceptAsync(res -> {
//查詢商品基本信息
SpuInfoEntity spuInfo = spuInfoService.getById(res.getSpuId());
skuItemVo.setSpuInfo(spuInfo);
}, executor);
CompletableFuture<Void> ImgageFuture = CompletableFuture.runAsync(() -> {
//2 sku圖片信息
List<SkuImagesEntity> images = imagesService.getImagesBySkuId(skuId);
skuItemVo.setImages(images);
}, executor);
//3. 獲取SPU銷售屬性組合 pms_product_attr_value
CompletableFuture<Void> saleAttrFuture = infoFutrue.thenAcceptAsync(res -> {
//3 獲取spu銷售屬性組合 list
List<SkuItemSaleAttrVo> saleAttrVos = skuSaleAttrValueService.getSaleAttrsBySpuId(res.getSpuId());
skuItemVo.setSaleAttr(saleAttrVos);
}, executor);
CompletableFuture<Void> descFuture = infoFutrue.thenAcceptAsync(res -> {
//4. 獲取SPU的介紹 pms_spu_info_desc
SpuInfoDescEntity spuInfoDescEntity = spuInfoDescService.getById(res.getSpuId());
skuItemVo.setDesp(spuInfoDescEntity);
}, executor);
CompletableFuture<Void> baseAttrFuture = infoFutrue.thenAcceptAsync(res -> {
//5. 獲取SPU的規格參數信息
List<SpuItemAttrGroupVo> spuItemAttrGroupVos = attrGroupService.getAttrGroupWithAttrsBySpuId(res.getSpuId(), res.getCatalogId());
skuItemVo.setGroupAttrs(spuItemAttrGroupVos);
}, executor);
// 等待所有任務都完成再返回
CompletableFuture.allOf(spuInfoFuture, ImgageFuture, saleAttrFuture, descFuture, baseAttrFuture).get();
return skuItemVo;
}
十、認證服務:
為了防止惡意攻擊短信接口,用redis緩存電話號:
在redis中以phone-code為前綴將電話號碼和驗證碼進行存儲並將當前時間與code一起存儲
如果調用時以當前phone取出的redis值不為空且當前時間在存儲時間的60s以內,說明60s內該號碼已經調用過,返回錯誤信息
60s以后再次調用,需要刪除之前存儲的phone-code
code存在一個過期時間,我們設置為10min,10min內驗證該驗證碼有效
session要能在不同服務和同服務的集群的共享:
十一、購物車服務:
十二、訂單服務:
遠程調用丟失用戶信息:
原因:feign遠程調用的請求頭中沒有含有JSESSIONID的cookie,所以也就不能得到服務端的session數據,也就沒有用戶數據
解決方案:向容器中導入定制的RequestInterceptor為請求加上cookie。
線程異步丟失上下文問題:
原因:由於RequestContextHolder使用ThreadLocal共享數據,所以在開啟異步時獲取不到老請求的信息,自然也就無法共享cookie了。
解決方案:開啟異步的時候將老請求的RequestContextHolder的數據設置進去
// 從主線程獲取用戶數據 放到局部變量中
RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
CompletableFuture<Void> getAddressFuture = CompletableFuture.runAsync(() -> {
// 把舊RequestAttributes放到新線程的RequestContextHolder中
RequestContextHolder.setRequestAttributes(attributes);
}
接口冪等性討論:
冪等性:訂單提交一次和提交多次結果是一致的。
哪些情況要防止:
- 用戶多次點擊按鈕
- 用戶頁面回退再次提交
- 服務相互調用,由於網絡間題,導致請求失敗。feign觸發重試機制
- 其他業務情況
冪等解決方案:
(1)、token機制:
服務端提供了發送token的接囗。我們在分析業務的時候,哪些業務是存在冪等問題的,就必須在執行業務前,先去獲取token,服務器會把token保存到redis中。
服務器判斷token是否存在redis中,存在表示第一次請求,然后先刪除token,繼續執行業務。
但要保證只能有一個去redis看,否則就可能都看到redis中有,刪除兩次
【對比+刪除】得是原子性的,所以就想到了用redis-luna腳本分布式鎖
if redis.call('get',KEYS[1])==ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end
如果判斷token不存在redis中,就表示是重復操作,直接返回重復標記給client,這樣就保證了業務代碼,不重復執行。
(2)、各種鎖:
a、數據庫悲觀鎖
b、數據庫樂觀鎖
c、業務層分布式鎖
(3)、各種唯一約束:
a、數據庫唯一約束
b、redis set防重
(4)、防重表:
把唯一幸引插入去重表,再進行業務操作,且他們在同一個事務中。
(5)、全局請求唯一id
調用接口時,生成一個唯一id,redis將數據保存到集合(去重),存在即處理過。
可以使用nginx設置每一個請求的唯一id
proxy_set_header X-Request-id $request_id;
分布式事務:
public interface AService {
public void a();
public void b();
}
@Service()
public class AServiceImpl1 implements AService{
@Transactional(propagation = Propagation.REQUIRED)
public void a() {
this.b();
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void b() {
}
}
此處的this指向目標對象,因此調用this.b()將不會執行b事務切面,即不會執行事務增強,
因此b方法的事務定義“@Transactional(propagation = Propagation.REQUIRES_NEW)”將不會實施,
即結果是b和a方法的事務定義是一樣的(我們可以看到事務切面只對a方法進行了事務增強,沒有對b方法進行增強)
Q1:b中的事務會不會生效?
A1:不會,a的事務會生效,b中不會有事務,因為a中調用b屬於內部調用,沒有通過代理,所以不會有事務產生。
Q2:如果想要b中有事務存在,要如何做?
A2:<aop:aspectj-autoproxy expose-proxy=“true”> ,設置expose-proxy屬性為true,將代理暴露出來,使用AopContext.currentProxy()獲取當前代理,將this.b()改為((UserService)AopContext.currentProxy()).b()
CAP理論
CAP原則又稱CAP定理,指的是在一個分布式系統中
一致性(consistency)
在分布式系統中的所有數據備份,在同一時刻是否同樣的值。(等同於所有節點訪問同一份最新的數據副本)
可用性(Availability)
在集群中一部分節點故障后,集群整體是否還能響應客戶端的讀寫請求。(對數據更新具備高可用性)
分區容惜性(Partitiontolerance)
大多數分布式系統都分布在多個子網絡。每個子網絡就叫做一個區(partition)。
分區容錯的意思是,區間通信可能失敗。比如,一台服務器放在中國,另一台服務器放在美國,這就是兩個區,它們之間可能無法通信。
CAP原則指的是,這三個要素最多只能同時實現兩點,不可能三者兼顧
CP要求一致性(有一個沒同步好就不可用)
AP要求高可用
raft是一個實現分布式一致性的協議
BASE理論是對CAP理論的延伸,思想是即使無法做到強一致性(CAP的一致性就是強一致性),但可以采用弱一致性,即最終一致性
BASE是指:
基本可用(BasicallyAvailable)
基本可用是指分布式系統在出現故障的時候,允許損失部分可用性(例如響應時間、功能上的可用性),允許損失部分可用性。需要注意的是,基本可用絕不等價於系統不可用。
響應時間上的損失:正常情況下搜索引擎需要在0.5秒之內返回給用戶相應的查詢結果,但由於出現故障(比如系統部分機房發生斷電或斷網故障),查詢結果的響應時間增加到了1~2秒。
功能上的損失:購物網站在購物高峰(如雙十一)時,為了保護系統的穩定性,部分消費者可能會被引導到一個降級頁面。
軟狀態(soft state)
軟狀態是指允許系統存在中間狀態,而該中間狀態不會影響系統整體可用性。分布式存儲中一般一份數據會有多個副本,允許不同副本同步的延時就是軟狀態的體現。mysql replication的異步復制也是一種體現。
最終一致性(Eventual Consistency)
最終一致性是指系統中的所有數據副本經過一定時間后,最終能夠達到一致的狀態。弱一致性和強一致性相反,最終一致性是弱一致性的一種特殊情況。(這也是分布式事務的想法)
從客戶端角度,多進程並發訪同時,更新過的數據在不同程如何獲的不同策珞,決定了不同的一致性。
對於關系型要求更新過據能后續的訪同都能看到,這是強一致性。
如果能容忍后經部分過者全部訪問不到,則是弱一致性
如果經過一段時間后要求能訪問到更新后的數據,則是最終一致性
分布式事務幾種方案:
1) 2PC模式(XA事務)
數據庫支持的2pc【2二階段提交】,又叫做XA Transactions
2) 柔性事務-TCC事務補償型方案
- 剛性事務:遵循ACID原則,強一致性。
- 柔性事務:遵循BASE理論,最終一致性;
與剛性事務不同,柔性事務允許一定時間內,不同節點的數據不一致,但要求最終一致。
- 一階段
prepare行為:調用自定義的prepare邏輯。 - 二階段
commit行為:調用自定義的commit邏憬。 - 二階段
rollback行為:調用自定義的rollback邏輯。
3)柔性事務-最大努力通知型方案
按規律進行通知,不保證數據一定能通知成功,但會提供可查詢操作接囗進行核對。這種方案主要用在與第三方系統通訊時,比如:調用微信或支付寶支付后的支付結果通知。這種方案也是結合MQ進行實現,例如:通過MQ發送就請求,設置最大通知次數。達到通知次數后即不再通知。
案例:銀行涌知、商戶通知等(各大交易業務平台間的商戶涌知:多次通知、查詢校對、對賬文件),支付寶的支付成功異步回調
大業務調用訂單,庫存,積分。最后積分失敗,則一遍遍通知他們回滾
讓子業務監聽消息隊列
如果收不到就重新發
4)柔性事務=可靠消息+最終一致性方案(異步確保型)
實現:業務處理服務在業務事務提交之前,向實時消息服務請求發送捎息,實時捎息服務只記錄消息數據,而不是真正的發送。
業務處理服務在業務事務提交之后,向實時消息服務確認發送。
只有在得到確認發送指令后,實時消息服務才會真正發送。
seata解決分布式事務問題
Seata 是一款開源的分布式事務解決方案,致力於提供高性能和簡單易用的分布式事務服務。Seata 將為用戶提供了 AT、TCC、SAGA 和 XA 事務模式,為用戶打造一站式的分布式解決方案。
快速開始:http://seata.io/zh-cn/docs/user/quickstart.html
TC (Transaction Coordinator) - 事務協調者
維護全局和分支事務的狀態,驅動全局事務提交或回滾。
TM (Transaction Manager) - 事務管理器
定義全局事務的范圍:開始全局事務、提交或回滾全局事務。
RM (Resource Manager) - 資源管理器
管理分支事務處理的資源,與TC交談以注冊分支事務和報告分支事務的狀態,並驅動分支事務提交或回滾。
消息隊列實現最終一致性:
(1) 延遲隊列
- 解決:rabbitmq的消息TTL和死信Exchange結合
定義:延遲隊列存儲的對象肯定是對應的延時消息,所謂"延時消息"是指當消息被發送以后,並不想讓消費者立即拿到消息,而是等待指定時間后,消費者才拿到這個消息進行消費。
實現:rabbitmq可以通過設置隊列的TTL+死信路由實現延遲隊列
TTL:RabbitMQ可以針對Queue設置x-expires 或者 針對Message設置 x-message-ttl,來控制消息的生存時間,如果超時(兩者同時設置以最先到期的時間為准),則消息變為dead letter(死信)
死信路由DLX:RabbitMQ的Queue可以配置x-dead-letter-exchange 和x-dead-letter-routing-key(可選)兩個參數,如果隊列內出現了dead letter,則按照這兩個參數重新路由轉發到指定的隊列。
x-dead-letter-exchange:出現dead letter之后將dead letter重新發送到指定exchange
x-dead-letter-routing-key:出現dead letter之后將dead letter重新按照指定的routing-key發送
(2) 延遲隊列使用場景

十三、Sentinel整合:
Sentinel服務流控、熔斷和降級
自定義被限流響應
設置被限流后看到的頁面
@Component
public class GulimallSentinelConfig
implements UrlBlockHandler{
@Override
public void blocked(HttpServletRequest request,
HttpServletResponse response,
BlockException ex) throws IOException {
R r = R.error(BizCodeEnum.SECKILL_EXCEPTION.getCode(),BizCodeEnum.SECKILL_EXCEPTION.getMsg());
response.setContentType("application/json;charset=utf-8");
response.getWriter().write(JSON.toJSONString(r)+"servlet用法");
}
}
網關流控
如果能在網關層就進行流控,可以避免請求流入業務,減小服務壓力
feign的流控和降級
默認情況下,sentinel是不會對feign遠程調用進行監控的,需要開啟配置
feign: sentinel: enabled: true
在@FeignClient設置fallback屬性
@FeignClient(value = "gulimall-seckill",
fallback = SeckillFallbackService.class) // 被限流后的處理類
public interface SeckillFeignService {
@ResponseBody
@GetMapping(value = "/getSeckillSkuInfo/{skuId}")
R getSeckillSkuInfo(@PathVariable("skuId") Long skuId);
}
在降級類中實現對應的feign接口,並重寫降級方法
@Component
public class SeckillFallbackService implements SeckillFeignService {
@Override
public R getSeckillSkuInfo(Long skuId) {
return R.error(BizCodeEnum.READ_TIME_OUT_EXCEPTION.getCode(), BizCodeEnum.READ_TIME_OUT_EXCEPTION.getMsg());
}
}
Zipkin鏈路追蹤
由於微服務項目模塊眾多,相互之間的調用關系十分復雜,因此為了分析工作過程中的調用關系,需要使用zipkin來進行鏈路追蹤
導入依賴
<!--鏈路追蹤-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
1
2
3
4
5
配置
spring:
zipkin:
base-url: http://localhost:9411
sender:
type: web
# 取消nacos對zipkin的服務發現
discovery-client-enabled: false
#采樣取值介於 0到1之間,1則表示全部收集
sleuth:
sampler:
probability: 1
