谷粒商城學習筆記以及錯誤總結


谷粒商城學習筆記以及錯誤總結:

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 
 
        

在瀏覽器輸入以下地址:http://127.0.0.1:9200/

即可看到如下內容:

{
  "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遠程調用的請求頭中沒有含有JSESSIONIDcookie,所以也就不能得到服務端的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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2026 CODEPRJ.COM