個人學習進度:視頻跟敲+筆記(12天)
學習視頻:尚硅谷微服務速通(7小時左右課程)
資源:
1.pdf:微服務pdf(課程):https://pan.baidu.com/s/1g_TAuBjQ9Dw6WU7b7lwvCA?pwd=yyds?
2.語雀筆記(尚硅谷速通):尚硅谷 · 語雀
3.nacos面板地址:
- http://192.168.43.71:8848/nacos/index.html
alt+enter:快捷鍵的使用(智能提示!!!)
測試接口:http://localhost:8000/create?userId=1&productId=100
一.微服務概念(名詞了解)
SpringCloud微服務架構中,常見的概念和名詞通過具體的技術棧得以實現和解決實際問題。
微服務架構風格,就像是把一個單獨的應用程序開發為一套小服務,每個小服務運行在自己的進程中,并使用輕量級機制通信,通常是 HTTP API。
簡而言之:拒絕大型單體應用,基于業務邊界進行服務微化拆分,各個服務獨立部署運行。
1.微服務架構
微服務架構是將應用程序分解為一組小型、獨立服務的開發方法。在SpringCloud中,每個服務可以獨立開發、部署和擴展,它們通過輕量級通信協議(如HTTP RESTful API)進行交互。
場景理解:微服務架構是一種將應用程序分解為一組小型、獨立服務的開發方法。每個服務專注于特定的業務功能,并且可以獨立開發、部署和擴展。就像一家大型餐廳,有負責烹飪的廚師團隊(后端服務)、負責接待客人的服務員團隊(前端服務)、負責食材采購的采購團隊(數據訪問服務)等,每個團隊獨立運作但又相互協作,共同為顧客(用戶)提供完整的用餐體驗(應用程序功能)。
實現與解決:通過SpringBoot可以快速創建獨立運行的服務,SpringCloud則提供了服務之間的通信、發現等基礎設施支持,使得服務能夠相互協作完成復雜的業務功能。
Spring Cloud 系列:
- 官網:Spring Cloud
- 遠程調用:OpenFeign
- 網關:Gateway
Spring Cloud Alibaba 系列:
- 官網:https://sca.aliyun.com/
- 注冊中心/配置中心:Nacos
- 服務保護:Sentinel
- 分布式事務:Seata
2.服務注冊與發現
是微服務架構中用于服務實例注冊、存儲及查詢定位,以便服務間通信協作的關鍵機制。
3.配置中心
集中管理應用配置信息,支持動態調整配置,實現配置與代碼分離,便于統一管理。
4.API網關
作為微服務架構的統一入口,負責路由、鑒權、限流等任務,簡化服務調用。
5.服務熔斷器
當服務調用失敗率高時,熔斷依賴服務調用鏈路,避免故障擴散,保障系統穩定。
6.服務降級
在系統故障或資源不足時,優先保障核心業務,降低非核心服務的質量或可用性。
7.分布式事務
確保分布式系統中多個服務的一系列操作要么全部成功,要么全部回滾,維持數據一致性。
8.服務監控與追蹤
實時監控微服務運行狀態和性能指標,追蹤服務調用鏈路,便于及時發現和解決問題。
9. 實際示例(步驟)
假設我們要構建一個電商系統,包含用戶服務、訂單服務和庫存服務:
1. 服務注冊與發現:每個服務在啟動時向Nacos注冊,其他服務通過Nacos發現并調用。
2. 配置管理:所有服務的配置信息(如數據庫連接)存儲在Nacos中,服務啟動時從Nacos獲取配置。
3. API網關:使用SpringCloud Gateway作為系統入口,處理用戶請求的路由、認證和限流。
4. 服務熔斷與降級:在訂單服務調用支付服務時,若支付服務出現故障,使用Resilience4j熔斷調用,并返回降級提示給用戶。
5. 分布式事務:在下單流程中,涉及訂單服務、庫存服務和支付服務的數據庫操作,使用Seata保證分布式事務的一致性。
6. 服務監控與追蹤:通過SpringCloud Sleuth和Zipkin記錄服務調用鏈路和性能指標,便于后續分析和優化。
10.微服務配圖
二.Nacos配置(注冊中心)
1.項目搭建
第一步:下載nacos
- 下載軟件包:📎nacos-server-2.4.3.zip
- 啟動控制面板:
startup.cmd -m standalone
- 打開地址:http://localhost:8848/nacos/index.html
第二步:創建SpringBoot項目
SpringBoot版本:3.3.4
項目名稱:cloud,根pom如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>3.3.4</version><relativePath/> <!-- lookup parent from repository --></parent><modules><module>services</module></modules><modelVersion>4.0.0</modelVersion><groupId>com.weiyan</groupId><artifactId>spring-cloud-demo</artifactId><version>1.0-SNAPSHOT</version><packaging>pom</packaging><properties><maven.compiler.source>17</maven.compiler.source><maven.compiler.target>17</maven.compiler.target><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><spring-cloud.version>2023.0.3</spring-cloud.version><spring-cloud-alibaba.version>2023.0.3.2</spring-cloud-alibaba.version></properties><dependencyManagement><dependencies><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-dependencies</artifactId><version>${spring-cloud.version}</version><type>pom</type><scope>import</scope></dependency><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-alibaba-dependencies</artifactId><version>${spring-cloud-alibaba.version}</version><type>pom</type><scope>import</scope></dependency></dependencies></dependencyManagement></project>
創建services:
下面有兩個子模塊:service-order(訂單服務)和service-product(商品服務)
第三步:引入公共依賴
services的pom中引入公共依賴,刷新:
<!-- 導入公共依賴--><dependencies>
<!-- 服務發現--><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId></dependency>
<!-- 遠程調用--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId></dependency></dependencies>
services中的兩個子模塊分別引入web和test依賴:
<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId></dependency></dependencies>
maven如下所示,按組查看:
第四步:編寫配置文件和主類
配置文件名:application.properties
# 服務名稱
spring.application.name=service-order
# 服務端口
server.port=8000
# nacos注冊中心地址
spring.cloud.nacos.server-addr=127.0.0.1:8848
主類:@SpringBootApplication注解
@SpringBootApplication
public class OrderMainApplication {public static void main(String[] args) {SpringApplication.run(OrderMainApplication.class, args);}
}
第五步:查看服務啟動
啟動其中一個服務后,查看nacos的控制面板:
- http://192.168.43.71:8848/nacos/index.html
先打開nacos的目錄文件,bin目錄下,cmd命令彈窗,啟動:startup.cmd -m standalone
如下所示:
OK,項目搭建完成
2.demo1(集群服務)
第一步:打開services
右擊鼠標,賦值一個新的服務,需要修改配置
第二步:修改配置和端口
項目名稱修改,可以在后面加上數字,如:-2
需要修改選項(Modify Option):Program argument
輸入新的端口號:(自定義)
--server.port=8001
第三步:全選執行
ctrl+鼠標全選服務,run,運行:
運行成功,打開nacos面板(期間彈窗不關閉):http://localhost:8848/nacos/index.html
第四步:添加主類服務注解(必須的)
@EnableDiscoveryClient // 開啟服務注冊發現功能,需要重新啟動服務
@SpringBootApplication
public class ProductMainApplication {public static void main(String[] args) {SpringApplication.run(ProductMainApplication.class, args);}
}
開啟服務注冊發現功能后,可以運行下面測試類:ServiceInstance為服務實例
@SpringBootTest
public class DiscoveryTest {//標準注冊中心api(通用)@AutowiredDiscoveryClient discoveryClient;//nacos注冊中心api@AutowiredNacosServiceDiscovery nacosServiceDiscovery;@Testvoid nacosServiceDiscoveryTest() throws NacosException {for (String service : nacosServiceDiscovery.getServices()) {//微服務名稱System.out.println("service = " + service);//獲取ip+port(每個微服務名稱下有幾個端口)List<ServiceInstance> instances = nacosServiceDiscovery.getInstances(service);for (ServiceInstance instance : instances) {//主機地址+端口號System.out.println("ip:"+instance.getHost()+";"+"port = " + instance.getPort());}}}@Testvoid discoveryClientTest(){for (String service : discoveryClient.getServices()) {//微服務名稱System.out.println("service = " + service);//獲取ip+port(每個微服務名稱下有幾個端口)List<ServiceInstance> instances = discoveryClient.getInstances(service);for (ServiceInstance instance : instances) {//主機地址+端口號System.out.println("ip:"+instance.getHost()+";"+"port = " + instance.getPort());}}}
}
運行結果如下:
3.demo2(遠程調用)
功能實現:(簡單示例)
1.一個訂單下有多個商品,這里以一個商品為例
2.訂單遠程調用商品模塊數據,計算的出總金額
3.遠程查詢訂單商品列表
數據公共模塊(model)
所有微服務的數據模型要公共使用,創建一個公共模塊model,數據在bean目錄下:
//可以使用@Data注解
<dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><scope>annotationProcessor</scope>
</dependency>
@Data
public class Order {private Long id;private BigDecimal totalAmount;private Long userId;private String nickName;private String address;private List<Object> productList;
}
@Data
public class Product {private Long id;private BigDecimal price;private String productName;private int num;
}
在services的模塊下的pom文件中;導入model模塊的使用:
<dependency><groupId>com.weiyan</groupId><artifactId>model</artifactId><version>1.0-SNAPSHOT</version>
</dependency>
業務代碼實現:
配置文件config:
RestTemplate
是 Spring 框架中提供的一個用于簡化 HTTP 請求和處理響應的工具類。[遠程調用]
@Configuration
public class ProductServiceConfig {@Beanpublic RestTemplate restTemplate() {return new RestTemplate();}
}
@Configuration
public class OrderServiceConfig {@Beanpublic RestTemplate restTemplate() {return new RestTemplate();}
}
service接口層
public interface ProductService{Product getProductById(Long productId);
}
public interface OrderService {// 根據用戶id和商品id,創建購物清單Order cresteOrder(Long productId,Long userId);
}
sevice的實現層(impl)
@Service
public class ProductServiceImpl implements ProductService {@Overridepublic Product getProductById(Long productId){Product product = new Product();product.setId(productId);//設置默認值product.setProductName("蘋果-"+productId);product.setPrice(new BigDecimal("99"));product.setNum(2);return product;}
}
@Slf4j
@Service
public class OrderServiceImpl implements OrderService {@AutowiredDiscoveryClient discoveryClient;@AutowiredRestTemplate restTemplate;@Overridepublic Order cresteOrder(Long productId, Long userId) {//從遠程獲取一個商品數據(示例:模擬只有一個商品)Product product = getProductFromRemote(productId);Order order = new Order();order.setId(1L);//總金額:商品的金額*商品購買的數量order.setTotalAmount( product.getPrice().multiply(new BigDecimal(product.getNum())));order.setAddress("北京");order.setNickName("張三");order.setUserId(userId);//遠程查詢商品列表order.setProductList(Arrays.asList(product));return order;}//獲取遠程調用的商品數據private Product getProductFromRemote(Long productId){//1.獲取所有商品服務所在的所有機器IP+port(獲取實例)List<ServiceInstance> instances = discoveryClient.getInstances("service-product");//2.獲取實例,遠程調用一個url地址ServiceInstance instance = instances.get(0);String url = "http://"+instance.getHost() + ":" + instance.getPort()+"/product/"+productId;log.info("遠程請求:{}",url);//3.給遠程發送請求RestTemplate(),線程安全的。url返回的json數據自動轉換為class對象Product product = restTemplate.getForObject(url, Product.class);return product;}
}
Controller層
@RestController
public class ProductController {@AutowiredProductService productService;//根據商品id,查詢商品@GetMapping("/product/{id}")public Product getProduct(@PathVariable("id") Long productId){Product product =productService.getProductById(productId);return product;}
}
@RestController
public class OrderController {@AutowiredOrderService orderService;//創建訂單,商品id+用戶id@GetMapping("/create")public Order createOrder(@RequestParam("productId") Long productId,@RequestParam("userId") Long userId){Order order =orderService.cresteOrder(productId,userId);return order;}
}
測試結果:
這里默認設置商品數量為2,價格99,id和商品名稱動態變化
商品服務:http://localhost:9002/product/4
訂單服務:http://localhost:8000/create?userId=2&productId=100
{"id": 4,"price": 99,"productName": "蘋果-4","num": 2
}
{"id": 1,"totalAmount": 198,"userId": 2,"nickName": "張三","address": "北京","productList": [{"id": 100,"price": 99,"productName": "蘋果-100","num": 2}]
}
日志信息:
@Slf4j注釋,使用{}占位符(使用了):log.info("遠程請求:{}", url)
示例:log.info("用戶 {} 訪問了 {} 頁面", userId, pageUrl);
結果如下:
停掉其中一個服務后,依舊可以運行:(遠程請求端口變了)
缺陷:每次固定發給第一個服務端口,除非宕機了
改進:實現負載均衡,每次請求分別發給不同的服務端口(均衡)
4.demo3(負載均衡)
方法一:choose方法
依賴導入:
<!-- 負載均衡 -->
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<!--測試 -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope>
</dependency>
測試類:
@SpringBootTest
public class LoadBalancerTest {@AutowiredLoadBalancerClient loadBalancerClient;@Testvoid test() {ServiceInstance instance = loadBalancerClient.choose("service-product");System.out.println("choose="+instance.getHost() + ":" + instance.getPort());instance = loadBalancerClient.choose("service-product");System.out.println("choose="+instance.getHost() + ":" + instance.getPort());instance = loadBalancerClient.choose("service-product");System.out.println("choose="+instance.getHost() + ":" + instance.getPort());instance = loadBalancerClient.choose("service-product");System.out.println("choose="+instance.getHost() + ":" + instance.getPort());}
}
改進方法:
//上面引入@AutowiredLoadBalancerClient loadBalancerClient;
//負載均衡獲取遠程調用的商品數據private Product getProductFromRemoteWithLoadBalance(Long productId){ServiceInstance instance = loadBalancerClient.choose("service-product");String url = "http://"+instance.getHost() + ":" + instance.getPort()+"/product/"+productId;log.info("遠程請求:{}",url);//3.給遠程發送請求RestTemplate(),線程安全的。url返回的json數據自動轉換為class對象Product product = restTemplate.getForObject(url, Product.class);return product;}
多次請求,刷新界面后,日志的遠程請求地址變了:
方法二:注解實現
RestTemplate
是 Spring 框架中提供的一個用于簡化 HTTP 請求和處理響應的工具類。
添加注解實現負載均衡: @LoadBalanced
@Configuration
public class OrderServiceConfig {@LoadBalanced //注解式實現負載均衡@Beanpublic RestTemplate restTemplate() {return new RestTemplate();}
}
private Product getProductFromRemoteWithLoadBalanceAnnotation(Long productId){//這里ip地址直接寫成對方微服務的名字,會動態替換String url = "http://service-product/product/"+productId;log.info("遠程請求:{}",url);//restTemplate在請求發送前:動態將service-product負載均衡的替換成一個IP地址Product product = restTemplate.getForObject(url, Product.class);return product;}
因為restTemplate動態替換了,也不知道想誰發送了請求,在商品控制層輸出,hello打招呼:
這里8000日志輸出,ip地址被隱藏了,動態替換。不知道向誰發出的遠程請求。
@RestController
public class ProductController {@AutowiredProductService productService;//根據商品id,查詢商品@GetMapping("/product/{id}")public Product getProduct(@PathVariable("id") Long productId){System.out.println("hello");Product product =productService.getProductById(productId);return product;}
}
一共遠程請求了5次,9000和9001分別兩次,9002請求一次。(如下所示)
5.配置中心
一.基本使用
依賴引入:
<dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
配置文件:
# 指定配置中心地址
spring.cloud.nacos.server-addr=localhost:8848
spring.config.import=nacos:service-order.properties
Naco控制面板,添加配置:
重啟項目:controller層添加:
@Value("${order.timeout}")
String orderTimeout;
@Value("${order.auto-confirm}")
String orderAutoConfirm;//接口
@GetMapping("/config")
public String config(){return "order.timeout="+orderTimeout+" order.auto-confirm="+orderAutoConfirm;
}
運行項目,調用接口:
如果其他服務模塊沒有使用配置中心,添加配置:
# 禁用導入檢查(配置中心)
spring.cloud.nacos.config.import-check.enabled=false
二.動態刷新
第一種方法:
controller層添加注解:@RefreshScope
修改配置:
接口數據頁自動刷新:
第二種方法(推薦):
ConfigurationProperties
創建包:Properties,存放采用配置,統一管理
@Component
//配置批量綁定在nacos下,可以無需@RefreshScope就能實現自動刷新(前綴)
@ConfigurationProperties(prefix = "order")
@Data
public class OrderProperties {String timeout;String autoConfirm;
// String dbUrl;
}
修改控制層:
//@RefreshScope //自動刷新
@RestController
public class OrderController {@AutowiredOrderService orderService;// @Value("${order.timeout}")
// String orderTimeout;
// @Value("${order.auto-confirm}")
// String orderAutoConfirm;@AutowiredOrderProperties orderProperties;@GetMapping("/config")public String config(){return "order.timeout="+orderProperties.getTimeout()+"; " +"order.auto-confirm="+orderProperties.getAutoConfirm() +";";}//創建訂單,商品id+用戶id@GetMapping("/create")public Order createOrder(@RequestParam("productId") Long productId,@RequestParam("userId") Long userId){Order order =orderService.cresteOrder(productId,userId);return order;}
}
三.配置監聽
啟動類添加:
@BeanApplicationRunner applicationRunner(NacosConfigManager manager){return args -> {ConfigService configService = manager.getConfigService();configService.addListener("service-order.properties", "DEFAULT_GROUP", new Listener() {@Overridepublic Executor getExecutor() {return Executors.newFixedThreadPool(4);}@Overridepublic void receiveConfigInfo(String configInfo) {System.out.println("變化的配置:configInfo = " + configInfo);}});};}
四.數據隔離
用名稱空間來區分多套環境:
首先,創建不同命名空間:(四個)
在不同命名空間下,設置配置,其中分組來區分微服務(每個組就是一個微服務):
order.timeout=1min
order.auto-confirm=1h
依次完成配置和操作,開始克隆:
server:port: 8000
spring:
# 激活環境profiles:active: devapplication:name: service-ordercloud:nacos:server-addr: 127.0.0.1:8848config:
# 導入檢查禁用import-check:enabled: false# 動態取值,看當前激活環境,默認為devnamespace: ${spring.profiles.active:dev}#同一個文件中定義多個獨立的配置塊,使用---
---
# 導入不同空間下的order組的配置文件
spring:config:import:- nacos:common.properties?group=order- nacos:database.properties?group=orderactivate:on-profile: test---
spring:config:import:- nacos:common.properties?group=order- nacos:database.properties?group=orderactivate:on-profile: prod---
spring:config:import:- nacos:common.properties?group=order- nacos:database.properties?group=orderactivate:on-profile: dev
6.Nacos經典面試題
一.注冊中心宕機
二.配置項優先級
以配置中心的數據為準,在項目中優先級低(規則如下:)
小結:
三.OpenFeign(遠程調用)
1.基本介紹
OpenFeign 是一個聲明式遠程調用客戶端;通過注解方式更加便捷。
上面使用的編程式遠程調用請求:RestTemplate(自己手動);這里聲明式:OpenFeign(注解)
2.基本使用
其中mvc注解兩套使用邏輯:
1.標注在controller上,是接受這樣的請求
2.標注在feignClient上,是發生這樣的請求
實現功能:
1.引入依賴:
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
2.主類中添加注解:@EnableFeignClients //開啟Feign遠程調用功能
3.創建feign包,創建接口:自動連接注冊中心,負載均衡的挑選一個
@FeignClient(value = "service-product") //feign客戶端+遠程微服務名
public interface ProductFeignClient {// 其中mvc注解兩套使用邏輯:
// 1.標注在controller上,是接受這樣的請求
// 2.標注在feignClient上,是發生這樣的請求@GetMapping("/product/{id}")Product getProductById(@PathVariable("id") Long id);
}
4.修改OrderServiceimpl中代碼:
//引入OpenFeignClient@AutowiredProductFeignClient productFeignClient;@Overridepublic Order cresteOrder(Long productId, Long userId) {//從遠程獲取一個商品數據(示例:模擬只有一個商品)// Product product = getProductFromRemoteWithLoadBalanceAnnotation(productId);// 改用openfeign注解遠程調用Product product = productFeignClient.getProductById(productId);Order order = new Order();order.setId(1L);//總金額:商品的金額*商品購買的數量order.setTotalAmount( product.getPrice().multiply(new BigDecimal(product.getNum())));order.setAddress("北京");order.setNickName("張三");order.setUserId(userId);//遠程查詢商品列表order.setProductList(Arrays.asList(product));return order;}
5.測試結果:同樣實現遠程調用,而且實現了負載均衡使用
兩次
一次
3.遠程調用技巧
1.遠程使用第三方api:指定url地址[墨跡天氣為例]
2.調用業務api(自己 編寫):直接復制對方的controller簽名
4.進階配置
一.開啟日志
yml文件配置:
logging:level:# 監控日志包名(整個包),級別debug,記錄詳細記錄com.atguigu.order.feign: debug
config容器中配置:feign下的logger
@Configuration
public class OrderServiceConfig {// 創建RestTemplate實例, 放到容器中并返回(線程安全)@LoadBalanced //注解式實現負載均衡@Beanpublic RestTemplate restTemplate() {return new RestTemplate();}//日志注解@BeanLogger.Level feignLoggerLevel() {return Logger.Level.FULL;}
}
二.超時控制
connectTimeout:連接超時
readTimeout:讀取超時{業務處理時間}
默認配置如下所示:讀取60s,連接10s
訂單impl設置睡眠時間:目的,延遲創建訂單的業務處理時間100s
//設置連接時間:100秒try{TimeUnit.SECONDS.sleep(100);}catch (InterruptedException e){throw new RuntimeException(e);}
調用接口,創建訂單:http://localhost:8000/create?userId=1&productId=100
在超時前已一直加載轉圈:
時間到了之后,返回500錯誤:讀取超時
控制臺輸出:超時信息
三.超時配置
訂單order的配置文件,添加一個application-feign.yml;并在yml中調用激活:
# 激活環境profiles:active: test
# 激活環境標識include: feign
spring:cloud:openfeign:client:config:# 客戶端名稱(其中default默認配置)default:logger-level: fullconnect-timeout: 1000read-timeout: 2000service-product:logger-level: fullconnect-timeout: 3000read-timeout: 5000
添加這個配置后,5s后就有返回結果:500
四.重試機制
@Bean
Retryer retryer(){return new Retryer.Default();
}
可以查看重試默認配置:5次,請求失敗了也會重新請求,最多5次,每次間隔100ms
時間間隔=超時+延遲等待(100ms*1.5)
public Default() {this(100L, TimeUnit.SECONDS.toMillis(1L), 5);}
使用結果測試:http://localhost:8000/create?userId=1&productId=100
五.攔截器
應用:上游放共享數據,下游可以收到(全局有效)
@Component
public class XTokenRequestInterceptor implements RequestInterceptor {/*** 請求攔截器* @param requestTemplate 請求模版**/@Overridepublic void apply(RequestTemplate requestTemplate) {requestTemplate.header("X-Token", UUID.randomUUID().toString());}
}
控制層:測試輸出
@RestController
public class ProductController {@AutowiredProductService productService;//根據商品id,查詢商品@GetMapping("/product/{id}")public Product getProduct(@PathVariable("id") Long productId, HttpServletRequest request){String header = request.getHeader("X-Token");System.out.println("hello...token="+header);Product product =productService.getProductById(productId);return product;}
}
結果(先注釋之前的超時控制):http://localhost:8000/create?userId=1&productId=100
六.Fallback兜底返回
引入依賴:sentinel
<dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-sentinel</artifactId></dependency>
@FeignClient(value = "service-product",fallback = ProductFeignClientFallback.class)
public interface ProductFeignClient {@GetMapping("/product/{id}")Product getProductById(@PathVariable("id") Long id);
}
@Component
public class ProductFeignClientFallback implements ProductFeignClient {@Overridepublic Product getProductById(Long id) {System.out.println("兜底回調....");Product product = new Product();product.setId(id);product.setPrice(new BigDecimal("0"));product.setProductName("未知商品");product.setNum(0);return product;}
}
注意:
1.注釋掉上面的重傳機制和超時設置
2.停掉商品的服務
3.開啟熔斷:
feign:sentinel:enabled: true
返回結果:
當商品服務開啟時:
5.經典面試題
客服端負載均衡與服務端負載均衡的區別:
四.Sentinel(服務保護)
1.基本使用
常用功能:服務保護,限流,熔斷降級
官網:home | Sentinel
wiki:https://github.com/alibaba/Sentinel/wiki
下載控制臺:📎sentinel-dashboard-1.8.8.jar
啟動命令:java -jar sentinel-dashboard-1.8.8.jar
訪問面板:http://localhost:8080/#/login
1.1依賴和配置:
<dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
其中order的feign的yml配置
spring:cloud:sentinel:transport:dashboard: localhost:8080eager: true # 默認懶加載,這里改為一開始就啟動
product的配置:
spring.cloud.sentinel.transport.dashboard=localhost:8080
spring.cloud.sentinel.eager=true
1.2面板和流控測試
測試接口:http://localhost:8000/create?userId=1&productId=100
添加注解:OrderServiceImpl下的創建方法添加:@SentinelResource(value = "createOrder")
測試:每秒創建一個,多了報錯。
快速刷新請求,出現默認錯誤:
2.異常處理
2.1自定義BlockException異常
自定義返回的json對象:R
@Data
public class R {private Integer code;private String msg;private Object data;public static R ok() {R r = new R();r.setCode(200);return r;}public static R ok(String msg,Object data) {R r = new R();r.setCode(200);r.setMsg(msg);r.setData(data);return r;}public static R error() {R r = new R();r.setCode(500);return r;}public static R error(Integer code,String msg) {R r = new R();r.setCode(code);r.setMsg(msg);return r;}
}
編寫自定義異常:
@Component
public class MyBlockExceptionHandler implements BlockExceptionHandler {private ObjectMapper objectMapper = new ObjectMapper();@Overridepublic void handle(HttpServletRequest request, HttpServletResponse response,String resourceName, BlockException e) throws Exception {// response.setStatus(429); //too many requests,這里//這里把對象轉換為json格式的范湖類型response.setContentType("application/json;charset=utf-8");PrintWriter writer = response.getWriter();R error = R.error(500, resourceName + " 被Sentinel限制了,原因:" + e.getClass());String json = objectMapper.writeValueAsString(error);writer.write(json);writer.flush();writer.close();}
}
重啟項目,重新配置sentinel的限流,多次刷新后:設置create限流
若如果添加的createOrder限流,多次刷新,出現:
原因:只給資源起了名,沒有標注blockhander和fallback,異常沒人管,直接回調Springboot的異常
(設置全局異常可以處理)
2.2blockHandler(指定兜底回調)
對createOrder限流,多次刷新
@SentinelResource(value = "createOrder",blockHandler = "createOrderFallback")@Overridepublic Order createOrder(Long productId, Long userId) {
// Product product = getProductFromRemoteWithLoadBalanceAnnotation(productId);//使用Feign完成遠程調用Product product = productFeignClient.getProductById(productId);Order order = new Order();order.setId(1L);// 總金額order.setTotalAmount(product.getPrice().multiply(new BigDecimal(product.getNum())));order.setUserId(userId);order.setNickName("zhangsan");order.setAddress("尚硅谷");//遠程查詢商品列表order.setProductList(Arrays.asList(product));return order;}//兜底回調public Order createOrderFallback(Long productId, Long userId, BlockException e){Order order = new Order();order.setId(0L);order.setTotalAmount(new BigDecimal("0"));order.setUserId(userId);order.setNickName("未知用戶");order.setAddress("異常信息:"+e.getClass());return order;}
顯示流控異常:
2.3OpenFeign - 兜底回調
對GET:http://service-product/product/{id}添加流量控制
默認會調用之前寫好的兜底回調:
3.流控規則
3.1 開啟集群
示例:均攤閾值設置為5,每個集群有三個微服務。
單擊均攤:表示每個微服務可以每秒5次請求
總體閾值:表示三個微服務1秒一共5次請求
3.2流控模式
(1)默認:直接+快速失敗(如上)
(2)鏈路:控流B,限制C,示例:秒殺場景
spring:cloud:sentinel:transport:dashboard: localhost:8080eager: trueweb-context-unify: false #先關閉統一的上下文
秒殺接口:
@GetMapping("/kill")public Order killOrder(@RequestParam("productId") Long productId,@RequestParam("userId") Long userId){Order order =orderService.cresteOrder(productId,userId);order.setId(Long.MAX_VALUE);return order;}
這種方式,斷開鏈路,僅對某一個鏈路生效
(3)關聯模式:寫優先策略
接口:
@GetMapping("/writeDb")public String writeDb(){return "writeDb";}@GetMapping("/readDb")public String readDb(){return "readDb";}
正常讀接口沒有問題,但當寫接口1s內請求過多時,讀接口被限制住了
開兩個窗口測試:(系統內出現資源競爭時,使用關聯策略)
3.3流控效果[直接模式]
3.3.1快速失敗
orderController添加日志注解@SLf4j
@GetMapping("/writeDb")public String writeDb(){return "writeDb";}@GetMapping("/readDb")public String readDb(){log.info("readDb"); //日志打印return "readDb";}
// 異常處理首先設置狀態碼response.setStatus(429); //too many requests
重啟項目,添加鏈路規則,限流1
使用apipost進行壓力測試:并發數10次,進行5秒
設置后置斷言,成功為200,失敗設置了狀態碼為429
開始測試:
3.3.2 預熱/冷啟動
慢慢啟動,直到到達峰值
這個設置表示,3秒內,逐漸達到峰值10,一開始默認為三分之一處,3個請求。
3.3.3均速排隊
其其中QPS數量不能大于1000,默認單位為ms
隊排不上,會被拋棄;底層使用漏桶算法。
4.熔斷規則
熔斷時間到了之后,半開狀態,有探測數據進行探測
1.測試:修改商品服務接口,故意休眠2秒
//根據商品id,查詢商品@GetMapping("/product/{id}")public Product getProduct(@PathVariable("id") Long productId, HttpServletRequest request){String header = request.getHeader("X-Token");System.out.println("hello...token="+header);Product product =productService.getProductById(productId);//休眠2秒try {TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {throw new RuntimeException(e);}return product;}
2.太慢了,會使用兜底回調,30s內熔斷了。5s內先進行熔斷檢測,統計5s內的前五個請求的異常比例
3.可知,30s后會處于半開狀態,當商品服務正常使用,不慢時,又可以正常使用了。(超時注釋掉)
5.熱點參數
可以限流對象,不再是資源級別,而是精細到參數級別,對于請求方法的某一個參數進行限制。
應用:
1.商城中的秒殺下單,用戶id需要限制。[防止機器人,瘋狂下單]
2.用戶id為vip,不限流QPS
3商品666是下架商品,需要限流
需要,先自定義資源,不可以和請求方法重名:@SentinelResource注解
資源名稱為seckill-order,對它進行熱點規則限流
@GetMapping("/seckill")@SentinelResource(value = "seckill-order",fallback = "seckillFallback")public Order seckill(@RequestParam(value = "userId",required = false) Long userId,@RequestParam(value = "productId",defaultValue = "1000") Long productId){Order order = orderService.createOrder(productId, userId);order.setId(Long.MAX_VALUE);return order;}public Order seckillFallback(Long userId,Long productId, BlockException exception){System.out.println("seckillFallback....");Order order = new Order();order.setId(productId);order.setUserId(userId);order.setAddress("異常信息:"+exception.getClass());return order;}
測試:http://localhost:8000/seckill?userId=1&productId=100
1.設置熱點規則,每個參數0[用戶ID],無論id是幾,每秒用戶id只能創建一個訂單
攜帶用戶id限流空,不帶用戶id不限流
2.用戶6為VIP,不限制QPS請求
3.商品666號為下架商品,不允許訪問(限制第二個參數)
4.將上面的 BlockException改為Throwable
可以處理所有異常,使用兜底回調,返回兜底回調,而不是500報錯界面
項目重啟后,sentinel設置的規則都失效了。sentinel沒有對這些規則實現持久化,需要結合nacos,配置好后,然后連接數據庫中,實現規則的持久化。
五.GateWay(網關)
官網:Spring Cloud Gateway
1.基本配置(路由)
創建對應模塊,引入依賴:
<dependencies><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-loadbalancer</artifactId></dependency><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-gateway</artifactId></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><scope>annotationProcessor</scope></dependency></dependencies>
配置文件application.yml:
spring:profiles:include: routeapplication:name: gatewaycloud:nacos:server-addr: 127.0.0.1:8848# localhost/api/order
server:port: 80
路由規則:
spring:cloud:gateway:routes:- id: order-routeuri: lb://service-orderpredicates:- Path=/api/order/**- id: order-routeuri: lb://service-productpredicates:- Path=/api/product/**
啟動服務:
http://localhost/api/order/readDb
2.斷言
名 | 參數(個數/類型) | 作用 |
After | 1/datetime | 在指定時間之后 |
Before | 1/datetime | 在指定時間之前 |
Between | 2/datetime | 在指定時間區間內 |
Cookie | 2/string,regexp | 包含cookie名且必須匹配指定值 |
Header | 2/string,regexp | 包含請求頭且必須匹配指定值 |
Host | N/string | 請求host必須是指定枚舉值 |
Method | N/string | 請求方式必須是指定枚舉值 |
Path | 2/List<String>,bool | 請求路徑滿足規則,是否匹配最后的/ |
Query | 2/string,regexp | 包含指定請求參數 |
RemoteAddr | 1/List<String> | 請求來源于指定網絡域(CIDR寫法) |
Weight | 2/string,int | 按指定權重負載均衡 |
XForwardedRemoteAddr | 1/List<string> | 從X-Forwarded-For請求頭中解析請求來源,并判斷是否來源于指定網絡域 |
測試:
spring:cloud:gateway:routes:- id: bing-routeuri: https://www.baidu.com/predicates:- name: Pathargs:patterns: /s- name: Queryargs:param: wdregexp: haha- name: Vipargs:param: uservalue: leifengyangorder: 10metadata:hello: world- id: order-routeuri: lb://service-orderpredicates:- name: Pathargs:patterns: /api/order/**matchTrailingSlash: trueorder: 1- id: product-routeuri: lb://service-productpredicates:- name: Pathargs:patterns: /api/product/**order: 1
編寫一個自定義的路由斷言工廠:
@Component
public class VipRoutePredicateFactory extends AbstractRoutePredicateFactory<VipRoutePredicateFactory.Config> {public VipRoutePredicateFactory() {super(Config.class);}@Overridepublic Predicate<ServerWebExchange> apply(Config config) {return new GatewayPredicate() {@Overridepublic boolean test(ServerWebExchange serverWebExchange) {// localhost/s?wd=haha&user=leifengyangServerHttpRequest request = serverWebExchange.getRequest();String first = request.getQueryParams().getFirst(config.param);return StringUtils.hasText(first) && first.equals(config.value);}};}@Overridepublic List<String> shortcutFieldOrder() {return Arrays.asList("param", "value");}/*** 可以配置的參數*/@Validatedpublic static class Config {@NotEmptyprivate String param;@NotEmptyprivate String value;public @NotEmpty String getParam() {return param;}public void setParam(@NotEmpty String param) {this.param = param;}public @NotEmpty String getValue() {return value;}public void setValue(@NotEmpty String value) {this.value = value;}}
}
必須要指定參數和用戶才可以!
3.過濾器
名 | 參數(個數/類型) | 作用 |
AddRequestHeader | 2/string | 添加請求頭 |
AddRequestHeadersIfNotPresent | 1/List<string> | 如果沒有則添加請求頭,key:value方式 |
AddRequestParameter | 2/string、string | 添加請求參數 |
AddResponseHeader | 2/string、string | 添加響應頭 |
CircuitBreaker | 1/string | 僅支持forward:/inCaseOfFailureUseThis方式進行熔斷 |
CacheRequestBody | 1/string | 緩存請求體 |
DedupeResponseHeader | 1/string | 移除重復響應頭,多個用空格分割 |
FallbackHeaders | 1/string | 設置Fallback頭 |
JsonToGrpc | 請求體Json轉為gRPC | |
LocalResponseCache | 2/string | 響應數據本地緩存 |
MapRequestHeader | 2/string | 把某個請求頭名字變為另一個名字 |
ModifyRequestBody | 僅 Java 代碼方式 | 修改請求體 |
ModifyResponseBody | 僅 Java 代碼方式 | 修改響應體 |
PrefixPath | 1/string | 自動添加請求前綴路徑 |
PreserveHostHeader | 0 | 保護Host頭 |
RedirectTo | 3/string | 重定向到指定位置 |
RemoveJsonAttributesResponseBody | 1/string | 移除響應體中的某些Json字段,多個用,分割 |
RemoveRequestHeader | 1/string | 移除請求頭 |
RemoveRequestParameter | 1/string | 移除請求參數 |
RemoveResponseHeader | 1/string | 移除響應頭 |
RequestHeaderSize | 2/string | 設置請求大小,超出則響應431狀態碼 |
RequestRateLimiter | 1/string | 請求限流 |
RewriteLocationResponseHeader | 4/string | 重寫Location響應頭 |
RewritePath | 2/string | 路徑重寫 |
RewriteRequestParameter | 2/string | 請求參數重寫 |
RewriteResponseHeader | 3/string | 響應頭重寫 |
SaveSession | 0 | session保存,配合spring-session框架 |
SecureHeaders | 0 | 安全頭設置 |
SetPath | 1/string | 路徑修改 |
SetRequestHeader | 2/string | 請求頭修改 |
SetResponseHeader | 2/string | 響應頭修改 |
SetStatus | 1/int | 設置響應狀態碼 |
StripPrefix | 1/int | 路徑層級拆除 |
Retry | 7/string | 請求重試設置 |
RequestSize | 1/string | 請求大小限定 |
SetRequestHostHeader | 1/string | 設置Host請求頭 |
TokenRelay | 1/string | OAuth2的token轉發 |
1.網關會將原路徑原封不動的轉過去,在網關向下轉的時候,進行路徑重寫。
2.使用過濾器,路徑重寫:(使用通義靈碼解析,會有就行,正則表達式)
3.注釋掉之前的路徑,重啟項目
spring:cloud:gateway:routes:- id: bing-routeuri: https://www.baidu.com/predicates:- name: Pathargs:patterns: /s- name: Queryargs:param: wdregexp: haha- name: Vipargs:param: uservalue: leifengyangorder: 10metadata:hello: world- id: order-routeuri: lb://service-orderpredicates:- name: Pathargs:patterns: /api/order/**matchTrailingSlash: truefilters: #將/api/order/ab/c 轉換為/ab/c- RewritePath=/api/order/?(?<segment>.*), /$\{segment}- OnceToken=X-Response-Token, jwtorder: 1- id: product-routeuri: lb://service-productpredicates:- name: Pathargs:patterns: /api/product/**filters:- RewritePath=/api/product/?(?<segment>.*), /$\{segment}order: 1
4.配置默認過濾器
default-filters:- AddResponseHeader=X-Response-Abc, 123
5.全局過濾器
@Component
@Slf4j
public class RtGlobalFilter implements GlobalFilter, Ordered {@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {ServerHttpRequest request = exchange.getRequest();ServerHttpResponse response = exchange.getResponse();String uri = request.getURI().toString();long start = System.currentTimeMillis();log.info("請求【{}】開始:時間:{}",uri,start);//========================以上是前置邏輯=========================Mono<Void> filter = chain.filter(exchange).doFinally((result)->{//=======================以下是后置邏輯=========================long end = System.currentTimeMillis();log.info("請求【{}】結束:時間:{},耗時:{}ms",uri,end,end-start);}); //放行 10sreturn filter;}@Overridepublic int getOrder() {return 0;}
}
4.微服務跨域(允許跨域)
spring:cloud:gateway:globalcors:cors-configurations:'[/**]':allowed-origin-patterns: '*'allowed-headers: '*'allowed-methods: '*'
六.Seata(分布式事務)---了解使用
1.基礎環境
建表:
CREATE DATABASE IF NOT EXISTS `storage_db`;
USE `storage_db`;
DROP TABLE IF EXISTS `storage_tbl`;
CREATE TABLE `storage_tbl` (`id` int(11) NOT NULL AUTO_INCREMENT,`commodity_code` varchar(255) DEFAULT NULL,`count` int(11) DEFAULT 0,PRIMARY KEY (`id`),UNIQUE KEY (`commodity_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO storage_tbl (commodity_code, count) VALUES ('P0001', 100);
INSERT INTO storage_tbl (commodity_code, count) VALUES ('B1234', 10);-- 注意此處0.3.0+ 增加唯一索引 ux_undo_log
DROP TABLE IF EXISTS `undo_log`;
CREATE TABLE `undo_log` (`id` bigint(20) NOT NULL AUTO_INCREMENT,`branch_id` bigint(20) NOT NULL,`xid` varchar(100) NOT NULL,`context` varchar(128) NOT NULL,`rollback_info` longblob NOT NULL,`log_status` int(11) NOT NULL,`log_created` datetime NOT NULL,`log_modified` datetime NOT NULL,`ext` varchar(100) DEFAULT NULL,PRIMARY KEY (`id`),UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;CREATE DATABASE IF NOT EXISTS `order_db`;
USE `order_db`;
DROP TABLE IF EXISTS `order_tbl`;
CREATE TABLE `order_tbl` (`id` int(11) NOT NULL AUTO_INCREMENT,`user_id` varchar(255) DEFAULT NULL,`commodity_code` varchar(255) DEFAULT NULL,`count` int(11) DEFAULT 0,`money` int(11) DEFAULT 0,PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- 注意此處0.3.0+ 增加唯一索引 ux_undo_log
DROP TABLE IF EXISTS `undo_log`;
CREATE TABLE `undo_log` (`id` bigint(20) NOT NULL AUTO_INCREMENT,`branch_id` bigint(20) NOT NULL,`xid` varchar(100) NOT NULL,`context` varchar(128) NOT NULL,`rollback_info` longblob NOT NULL,`log_status` int(11) NOT NULL,`log_created` datetime NOT NULL,`log_modified` datetime NOT NULL,`ext` varchar(100) DEFAULT NULL,PRIMARY KEY (`id`),UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;CREATE DATABASE IF NOT EXISTS `account_db`;
USE `account_db`;
DROP TABLE IF EXISTS `account_tbl`;
CREATE TABLE `account_tbl` (`id` int(11) NOT NULL AUTO_INCREMENT,`user_id` varchar(255) DEFAULT NULL,`money` int(11) DEFAULT 0,PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO account_tbl (user_id, money) VALUES ('1', 10000);
-- 注意此處0.3.0+ 增加唯一索引 ux_undo_log
DROP TABLE IF EXISTS `undo_log`;
CREATE TABLE `undo_log` (`id` bigint(20) NOT NULL AUTO_INCREMENT,`branch_id` bigint(20) NOT NULL,`xid` varchar(100) NOT NULL,`context` varchar(128) NOT NULL,`rollback_info` longblob NOT NULL,`log_status` int(11) NOT NULL,`log_created` datetime NOT NULL,`log_modified` datetime NOT NULL,`ext` varchar(100) DEFAULT NULL,PRIMARY KEY (`id`),UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
接口測試:
2.遠程鏈路
- 下載seata
📎apache-seata-2.1.0-incubating-bin.tar.gz
- 解壓并啟動:
seata-server.bat
http://127.0.0.1:7091
導入依賴:
<dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
編寫file.conf文件:
service {#transaction service group mappingvgroupMapping.default_tx_group = "default"#only support when registry.type=file, please don't set multiple addressesdefault.grouplist = "127.0.0.1:8091"#degrade, current not supportenableDegrade = false#disable seatadisableGlobalTransaction = false
}
添加全局事務注解:@GlobalTransactional