SpringCloud Alibaba學習筆記
目錄
目錄- 目錄
- 導學
- 環境搭建
- Spring Boot必知必會
- 微服務拆分與編寫
- Spring Cloud介紹
- 服務發現
- 實現負載均衡-Ribbon
- 宣告式HTTP客戶端Feign
- 服務容錯-Sentinel
導學
為什麼學
- 元件效能更強
- 良好的視覺化介面
- 搭建簡單,學習曲線低
- 文件豐富並且是中文
學習目標
- Spring Cloud Alibaba核心元件的用法及實現原理
- Spring Cloud Alibaba結合微信小程式從"0"學習真正開發中的使用
- 實際工作中如何避免踩坑,正確的思考問題方式
- Spring Cloud Alibaba的進階:程式碼的優化和改善,微服務監控
進階目標
- 如何提升團隊的程式碼質量
- 編碼技巧
- 心得總結
- 如何改善程式碼結構設計
- 藉助監控工具
- 定位問題
- 解決問題
思路
分析並拆解微服務->編寫程式碼->分析現有架構問題->引入微服務元件->優化重構->總結完善
Spring Cloud Alibaba的重要元件
- 服務發現Nacos
- 服務發現原理剖析
- Nacos Server/Client
- 高可用Nacos搭建
- 實現負載均衡Ribbon
- 負載均衡的常見模式
- RestTemplate整合Ribbon
- Ribbon配置自定義
- 如何擴充套件Ribbon
- 宣告式HTTP客戶端-Feign
- 如何使用Feign
- Feign配置自定義
- 如何擴充套件Feign
- 服務容錯Sentinel
- 服務容錯原理
- Sentinel
- Sentinel DashBoard
- Sentinel核心原理分析
- 訊息驅動RocketMq
- Spring Cloud Stream
- 實現非同步訊息推送與消費
- API閘道器Gateway
- 整合Gateway
- 三大核心
- 聚合微服務請求
- 使用者認證與授權
- 認證授權的常見方案
- 改造Gateway
- 擴充套件Feign
- 配置管理Nacos
- 配置如何管理
- 配置動態重新整理
- 配置管理的最佳實踐
- 呼叫鏈監控Sleuth
- 呼叫鏈監控原理剖析
- Sleuth使用
- Zipkin使用
環境搭建
- JDK8
- MySQL
- Maven的安裝與配置
- IDEA
Spring Boot必知必會
Spring Boot特性
- 無需部署WAR檔案
- 提供stater簡化配置
- 儘可能自動配置Spring以及第三方庫
- 提供"生產就緒"功能,例如指標、健康檢查、外部配置等
- 無程式碼生成&無XML
編寫第一個Spring Boot應用
Spring Boot應用組成分析
- 依賴:pom.xml
- 啟動類:註解
- 配置:application.properties
- static目錄:靜態檔案
- templates目錄:模板檔案
Spring Boot開發三板斧
- 加依賴
- 寫註解
- 寫配置
Spring Boot Actuator
監控工具
/actuator
入口
/health
健康檢查
顯示詳情配置
management.endpoint.health.show-details=always
# 顯示所有監控端點
management.endpoints.web.exposure.include=*
# 描述資訊(自定義鍵值對)
info.app-name=spring-boot-demo
info.author=kim
[email protected]
Spring Boot配置管理
支援的配置格式
management:
endpoint:
health:
show-details: always
endpoints:
web:
exposure:
include: '*'
# 描述資訊
info:
app-name: spring-boot-demo
author: kim
email: [email protected]
注意:值是*,yml寫法需要加引號
- yml是使用趨勢
- yml在有的配置中可以表達順序,properties不行
17種配置方式
實際專案種經常用到的配置管理方式:
- 配置檔案
- 環境變數
- 外部配置檔案
- 命令列引數
環境變數方式配置管理
application.yml
management:
endpoint:
health:
show-details: ${SOME_ENV}
endpoints:
web:
exposure:
include: '*'
# 描述資訊
info:
app-name: spring-boot-demo
author: kim
email: [email protected]
設定環境變數SOME_ENV
環境變數方式配置管理(java -jar方式)
mvn clean install -DskipTests
java -jar spring-boot-demo-0.0.1-SNAPSHOT.jar --SOME_ENV=always
外部配置檔案方式配置管理
將打的jar包和配置檔案放在同一目錄,會優先讀取該配置檔案內配置
命令列引數方式配置管理
java -jar spring-boot-demo-0.0.1-SNAPSHOT.jar --server.port=8081
最佳實踐
KISS,規避掉優先順序,沒人會記住17中配置姿勢的優先順序。
Profile
# 所有環境下公用的配置屬性
management:
endpoint:
health:
show-details: ${SOME_ENV}
endpoints:
web:
exposure:
include: '*'
# 描述資訊
info:
app-name: spring-boot-demo
author: kim
email: [email protected]
# 連字元
---
# profile=x的專用屬性,也就是說某個環境下的專用屬性
# 開發環境
spring:
profiles: dev
---
# profile=y的專用屬性,也就是說某個環境下的專用屬性
# 生產環境
spring:
profiles: prod
server:
tomcat:
max-threads: 300
max-connections: 1000
IDEA啟動配置
訪問http://localhost:8080/actuator/configprops通過actuator埠檢視
預設使用default,可以通過新增配置設定預設profile
spring:
profiles:
active: dev
最佳實踐
KISS,不要使用優先順序,規劃好公用和專用配置
微服務拆分與編寫
- 單體架構vs微服務架構
- 單體架構是什麼
- 微服務是什麼
- 微服務特性
- 微服務全景架構圖
- 微服務優缺點
- 微服務適用場景
- 業務分析與建模
- 專案功能演示與分析
- 微服務拆分
- 專案架構圖
- 資料庫設計
- API文件
- 編寫微服務
- 建立小程式
- 建立專案
- 編寫使用者微服務
- 編寫內容微服務
單體架構
優點:
- 架構簡單
- 開發、測試、部署方便
缺點:
- 複雜性高
- 部署慢,頻率低
- 擴充套件能力受限(比如使用者模組是CPU密集的,只能通過買更好的CPU的機器,比如內容模組是IO密集的,只能通過購買更多記憶體)
- 阻礙技術創新(SpringMVC->Spring Web Flux,改動大)
不適合龐大複雜的系統
微服務
拆分後的小型服務
微服務的特性
- 每個微服務可獨立執行在自己的程序裡;(每個服務一個Tomcat)
- 一系列獨立執行的微服務共同構建起整個系統
- 每個服務為獨立的業務開發,一個微服務只關注某個特定的功能,例如訂單管理、使用者管理
- 可以使用不同的語言與資料儲存技術(契合專案情況和團隊實力)
- 微服務之間通過輕量的通訊機制進行通訊,例如通過Rest API進行呼叫;(通訊協議輕量、跨平臺)
- 全自動的部署機制
微服務全景架構圖
優點
- 單個服務更易於開發、維護
- 單個微服務啟動較快
- 區域性修改容易部署
- 技術棧不受限
缺點
- 運維要求高
- 分散式固有的複雜性
- 重複勞動(不同語言呼叫相同功能時)
適用場景
- 大型、複雜的專案
- 有快速迭代的需求
- 訪問壓力大(微服務去中心化,把業務和資料都拆分了,可以應對訪問壓力)
不適用微服務的場景
- 業務穩定
- 迭代週期長
專案演示
微服務拆分
- 業界流行的拆分方法論
- 個人心得
- 合理粒度
- 小程式的拆分
方法論
-
領域驅動設計(Domain Driven Design)(概念太多,學習曲線高)
-
面向物件(by name./by verb)(通過名詞(狀態),動詞(行為)拆分)
個人心得
職責劃分
規劃好微服務的邊界。比如訂單微服務只負責訂單功能。
通用性劃分
把一些通用功能做成微服務。比如訊息中心和使用者中心。
合理的粒度
- 良好的滿足業務(這是前提)
- 幸福感(你的團隊沒有人認為微服務太大,難以維護,同時部署也非常高效,不會每次釋出都發布N多微服務)
- 增量迭代
- 持續進化
小程式的拆分
以面向物件方式拆分
使用者中心按照通用性劃分,內容中心按照職責劃分。
專案初期不建議拆分太細,後期如果發現某個微服務過分龐大再細分。
專案架構圖
資料庫設計
資料建模
建表
user-center-create-table.sql
USE `user_center`;
-- -----------------------------------------------------
-- Table `user`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `user` (
`id` INT NOT NULL AUTO_INCREMENT COMMENT 'Id',
`wx_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '微信id',
`wx_nickname` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '微信暱稱',
`roles` VARCHAR(100) NOT NULL DEFAULT '' COMMENT '角色',
`avatar_url` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '頭像地址',
`create_time` DATETIME NOT NULL COMMENT '建立時間',
`update_time` DATETIME NOT NULL COMMENT '修改時間',
`bonus` INT NOT NULL DEFAULT 300 COMMENT '積分',
PRIMARY KEY (`id`))
COMMENT = '分享';
-- -----------------------------------------------------
-- Table `bonus_event_log`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `bonus_event_log` (
`id` INT NOT NULL AUTO_INCREMENT COMMENT 'Id',
`user_id` INT NULL COMMENT 'user.id',
`value` INT NULL COMMENT '積分操作值',
`event` VARCHAR(20) NULL COMMENT '發生的事件',
`create_time` DATETIME NULL COMMENT '建立時間',
`description` VARCHAR(100) NULL COMMENT '描述',
PRIMARY KEY (`id`),
INDEX `fk_bonus_event_log_user1_idx` (`user_id` ASC) )
ENGINE = InnoDB
COMMENT = '積分變更記錄表';
content-center-create-table.sql
USE `content_center`;
-- -----------------------------------------------------
-- Table `share`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `share` (
`id` INT NOT NULL AUTO_INCREMENT COMMENT 'id',
`user_id` INT NOT NULL DEFAULT 0 COMMENT '釋出人id',
`title` VARCHAR(80) NOT NULL DEFAULT '' COMMENT '標題',
`create_time` DATETIME NOT NULL COMMENT '建立時間',
`update_time` DATETIME NOT NULL COMMENT '修改時間',
`is_original` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否原創 0:否 1:是',
`author` VARCHAR(45) NOT NULL DEFAULT '' COMMENT '作者',
`cover` VARCHAR(256) NOT NULL DEFAULT '' COMMENT '封面',
`summary` VARCHAR(256) NOT NULL DEFAULT '' COMMENT '概要資訊',
`price` INT NOT NULL DEFAULT 0 COMMENT '價格(需要的積分)',
`download_url` VARCHAR(256) NOT NULL DEFAULT '' COMMENT '下載地址',
`buy_count` INT NOT NULL DEFAULT 0 COMMENT '下載數 ',
`show_flag` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否顯示 0:否 1:是',
`audit_status` VARCHAR(10) NOT NULL DEFAULT 0 COMMENT '稽核狀態 NOT_YET: 待稽核 PASSED:稽核通過 REJECTED:稽核不通過',
`reason` VARCHAR(200) NOT NULL DEFAULT '' COMMENT '稽核不通過原因',
PRIMARY KEY (`id`))
ENGINE = InnoDB
COMMENT = '分享表';
-- -----------------------------------------------------
-- Table `mid_user_share`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `mid_user_share` (
`id` INT NOT NULL AUTO_INCREMENT,
`share_id` INT NOT NULL COMMENT 'share.id',
`user_id` INT NOT NULL COMMENT 'user.id',
PRIMARY KEY (`id`),
INDEX `fk_mid_user_share_share1_idx` (`share_id` ASC) ,
INDEX `fk_mid_user_share_user1_idx` (`user_id` ASC) )
ENGINE = InnoDB
COMMENT = '使用者-分享中間表【描述使用者購買的分享】';
-- -----------------------------------------------------
-- Table `notice`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `notice` (
`id` INT NOT NULL AUTO_INCREMENT COMMENT 'id',
`content` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '內容',
`show_flag` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否顯示 0:否 1:是',
`create_time` DATETIME NOT NULL COMMENT '建立時間',
PRIMARY KEY (`id`));
API 文件
課程文件主要分四類:
- API文件:https://t.itmuch.com/doc.html
- 課程配套程式碼:https://git.imooc.com/coding-358/
- 課程相關資源(例如檢表語句、資料模型、課上用到的軟體等):https://git.imooc.com/coding-358/resource
- 課上用到的一些課外讀物(慕課網手記):http://www.imooc.com/t/1863086
如何建立小程式
按照提示填寫資訊
前端程式碼如何使用
建立專案
技術選型
- Spring Boot
- Spring MVC
- Mybatis+通用Mapper
- Spring Cloud Alibaba(分散式)
工程結構規劃
建立專案,整合框架
pom.xml
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.13.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.itmuch</groupId>
<artifactId>user-center</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>user-center</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 引入通用mapper-->
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper-spring-boot-starter</artifactId>
<version>2.1.5</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-maven-plugin</artifactId>
<version>1.3.6</version>
<configuration>
<configurationFile>
${basedir}/src/main/resources/generator/generatorConfig.xml
</configurationFile>
<overwrite>true</overwrite>
<verbose>true</verbose>
</configuration>
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.19</version>
</dependency>
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper</artifactId>
<version>4.1.5</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
</project>
通用Mapper包掃描配置
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import tk.mybatis.spring.annotation.MapperScan;//注意是tk的MapperScan註解
@SpringBootApplication
@MapperScan("com.itmuch")
public class UserCenterApplication {
public static void main(String[] args) {
SpringApplication.run(UserCenterApplication.class, args);
}
}
在resources目錄下新建generator目錄,新增mybatis.generator配置
generator/generatorConfig.xml
<!DOCTYPE generatorConfiguration
PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
"http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
<generatorConfiguration>
<properties resource="generator/config.properties"/>
<context id="Mysql" targetRuntime="MyBatis3Simple" defaultModelType="flat">
<property name="beginningDelimiter" value="`"/>
<property name="endingDelimiter" value="`"/>
<plugin type="tk.mybatis.mapper.generator.MapperPlugin">
<property name="mappers" value="tk.mybatis.mapper.common.Mapper"/>
<property name="caseSensitive" value="true"/>
</plugin>
<jdbcConnection driverClass="${jdbc.driverClass}"
connectionURL="${jdbc.url}"
userId="${jdbc.user}"
password="${jdbc.password}">
</jdbcConnection>
<javaModelGenerator targetPackage="com.itmuch.usercenter.domain.entity.${moduleName}"
targetProject="src/main/java"/>
<sqlMapGenerator targetPackage="com.itmuch.usercenter.dao.${moduleName}"
targetProject="src/main/resources"/>
<javaClientGenerator targetPackage="com.itmuch.usercenter.dao.${moduleName}"
targetProject="src/main/java"
type="XMLMAPPER"/>
<table tableName="${tableName}">
<generatedKey column="id" sqlStatement="JDBC"/>
</table>
</context>
</generatorConfiguration>
generator/config.properties
jdbc.driverClass=com.mysql.cj.jdbc.Driver
# nullCatalogMeansCurrent=true 如果不加這個配置,出現表名user在其他庫,比如系統庫的,會生產系統庫的user
jdbc.url=jdbc:mysql://localhost:3306/user_center?nullCatalogMeansCurrent=true
jdbc.user=root
jdbc.password=kim@2020
# 包名
moduleName=user
# 表名
tableName=user
application.yml
spring:
datasource:
url: jdbc:mysql://localhost:3306/user_center
hikari:
username: root
password: kim@2020
# >=6.x com.mysql.cj.jdbc.Driver
# <=5.x com.mysql.jdbc.Driver
driver-class-name: com.mysql.cj.jdbc.Driver
執行逆向生產程式碼
整合Lombok簡化程式碼
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.10</version>
<scope>provided</scope>
</dependency>
常用註解
@Data
@NoArgsConstructor//生成無參構造
@AllArgsConstructor//為所有引數生成構造
@RequiredArgsConstructor//為final屬性生成構造方法
@Builder //建造者模式
@Slf4j
更多查詢官網
在通用mapper
wiki
搜lombok
,看有沒有生成支援lombok
的配置
mybatis.generator新增lombok支援
<plugin type="tk.mybatis.mapper.generator.MapperPlugin">
<property name="mappers" value="tk.mybatis.mapper.common.Mapper"/>
<property name="caseSensitive" value="true"/>
<property name="lombok" value="Getter,Setter,ToString"/><!-- 新增的行 -->
</plugin>
文件也說了,目前只支援@Getter@Setter@ToString@Accessors(chain = true)
4種註解,一般我們自己的domain上還是習慣加如下註解:
@Data
@NoArgsConstructor//生成無參構造
@AllArgsConstructor//為所有引數生成構造
@Builder //建造者模式
可以手動加,更簡單。
解決IDEA的紅色警告
出現警告的原因:
IDEA是非常智慧的,它可以理解Spring的上下文。然而 UserMapper
這個介面是Mybatis的,IDEA理解不了。
而 @Autowired
註解,預設情況下要求依賴物件(也就是 userMapper
)必須存在。而IDEA認為這個物件的例項/代理是個null,所以就友好地給個提示。
解決方法:參見這篇手記
作業1: 課後研究一下@Resource和@Autowired註解
作業2: 研究@Repository、@Component、@Service、@Controller之間的區別和聯絡
編寫使用者微服務和內容微服務
注意:核心業務,一定要設計好業務流程,分析的過程中,使用業務流程圖、活動圖、用例圖、序列圖。重視業務和建模,沒有建模的微服務是沒有靈魂的。
實際開發流程
Schema First
1、分析業務(流程圖、用例圖...架構圖等) 建模業務,確定架構
2、敲定業務流程(評審)
3、設計API/資料模型(表結構設計|類圖|ER圖)
4、編寫API文件
5、編寫程式碼
API First
1、分析業務(流程圖、用例圖...架構圖等) 建模業務,確定架構
2、敲定業務流程(評審)
3、設計API/資料模型(表結構設計|類圖|ER圖)
4、編寫程式碼
5、編寫API文件
但是實際也不是完全按照這樣等流程走。
編碼。。。
RestTemplate的使用
現有架構存在的問題
- 硬編碼IP,IP變化怎麼辦
- 如何實現負載均衡?
- 使用者中心掛了怎麼辦?
Spring Cloud介紹
什麼是Spring Cloud Alibaba
- Spring Cloud的子專案
- 致力於提供微服務開發的一站式解決方案
- 包含微服務開發的必備元件
- 基於Spring Cloud,符合Spring Cloud標準
- 阿里的微服務解決方案
版本與相容性
- Spring Cloud 版本命名
- Spring Cloud 生命週期
- Spring Boot 、Spring Cloud、Spring Cloud Alibaba的相容性關係
- 生產環境怎麼選擇版本?
Spring Cloud 版本命名
語義化
2.1.13.RELEASE
2:主版本,第幾代
1:次版本,一些功能的增加,但是架構沒有太大變化,是相容的
13:增量版本,bug修復
RELEASE:里程碑。SNAPSHOT:開發版 ,M:里程碑 ,RELEASE:正式版
Greenwich SR1 :Greenwich版本的第一個bug修復版
SR:Service Release bug修復
Release Train. 釋出列車
倫敦地鐵站站名。避免混淆,噱頭。
Greenwich RELEASE: Greenwich版本的第一個正式版
Spring Cloud 生命週期
- 版本釋出規劃
- 版本釋出記錄
- 版本終止宣告
版本相容性
https://spring.io/projects/spring-cloud-alibaba#overview
https://github.com/alibaba/spring-cloud-alibaba/blob/master/README-zh.md
生產環境怎麼選擇版本?
- 堅決不用非穩定版本/end-of-life版本
- 儘量用最新一代
- xxx.RELEASE版本緩一緩
- SR2之後一般可大規模使用
整合Spring Cloud Alibaba
整合好後,引入元件不需要指定版本
服務發現
服務提供者與服務消費者
名次 | 定義 |
---|---|
服務提供者 | 服務的被呼叫方(即:為其他微服務提供介面的微服務) |
服務消費者 | 服務的呼叫方(即:呼叫其他微服務介面的微服務) |
如何讓服務消費者感知到服務提供者
服務消費者內部使用定時任務去服務發現元件獲取提供者資訊,並快取到本地,服務消費者每次呼叫服務提供者從本地快取那提供者資訊。
新增心跳機制,通過心跳機制改變服務狀態
什麼是Nacos
搭建Nacos Server
選擇Nacos Server
版本
檢視引入到spring-cloud-alibaba-dependencie
依賴
- 下載地址
- 搭建Nacos Server
啟動伺服器
startup.sh -m standalone
訪問控制檯
預設使用者名稱密碼都是nacos
將應用註冊到Nacos
- 使用者中心註冊到Nacos
- 內容中心註冊到Nacos
- 測試:內容中心總能找到使用者中心
引入依賴
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
配置
spring:
cloud:
nacos:
discovery:
server-addr: localhost:8848
application:
# 服務名稱儘量用-,不要用_,不要用特殊字元
name: content-center
引入服務發現
@Slf4j
@Service
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class ShareService {
private final ShareMapper shareMapper;
private final RestTemplate restTemplate;
private final DiscoveryClient discoveryClient;
public ShareDto findById(Integer id){
//獲取分享詳情
Share share = this.shareMapper.selectByPrimaryKey(id);
//釋出人id
Integer userId = share.getUserId();
//使用者中心所有例項的資訊
List<ServiceInstance> instances = discoveryClient.getInstances("user-center");
String targetURL = instances.stream()
.map(instance -> instance.getUri().toString() + "/users/{id}")
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("當前沒有例項!"));
log.info("請求的目標地址:{}", targetURL);
UserDto userDto = restTemplate.getForObject(
targetURL,
UserDto.class,
userId);
ShareDto shareDto = new ShareDto();
//訊息的裝配
BeanUtils.copyProperties(share, shareDto);
shareDto.setWxNickName(userDto.getWxNickname());
return shareDto;
}
}
Nacos服務發現的領域模型
Namespace:只要用來實現環境隔離,預設public
Group:預設DEFAULT_GROUP,管理服務分組
Service:微服務
Cluster:微服務叢集,對指定微服務的虛擬劃分
Instance:微服務例項
如何使用
Namespace,在控制檯頁面建立。配置的時候使用生成的uuid。
spring:
cloud:
nacos:
discovery:
# 指定nacos server的地址
server-addr: localhost:8848
cluster-name: BJ
namespace: 56116141-d837-4d15-8842-94e153bb6cfb
Nacos元資料
- 官方描述:https://nacos.io/zh-ch/docs/concepts.html
- 級別:【服務級別、叢集級別、例項級別】
元資料的作用:
- 提供描述資訊
- 讓微服務呼叫更靈活
- 例如:微服務版本控制
如何為微服務設定元資料
- 控制檯介面
- 配置檔案指定
spring:
cloud:
nacos:
discovery:
# 指定nacos server的地址
server-addr: localhost:8848
cluster-name: BJ
namespace: 56116141-d837-4d15-8842-94e153bb6cfb
metadata:
instance: c
haha: hehe
version: 1
實現負載均衡-Ribbon
負載均衡的兩種方式
- 服務端負載均衡
- 客戶端負載均衡(客戶端呼叫的時候使用選擇負載均衡演算法)
手寫一個客戶端負載均衡器
改寫一下ShareService
的findById
方法。從Nacos
獲取到URL
列表,然後隨機從列表中取一個作為本次請求的服務提供者例項。
List<String> targetURLs = instances.stream()
.map(instance -> instance.getUri().toString() + "/users/{id}").collect(Collectors.toList());
int i = ThreadLocalRandom.current().nextInt(targetURLs.size());
String targetURL= targetURLs.get(i);
隨後啟動content-center
啟動多個user-center
配置允許並行執行
修改埠,執行啟動類
server:
port: 8082
使用Ribbon實現負載均衡
- Ribbon是什麼
- 引入Ribbon後到架構演進
- 整合Ribbon實現負載均衡
Ribbon是什麼
負載均衡器
架構演進
整合Ribbon實現負載均衡
引入Nacos
我們引入spring-cloud-starter-alibaba-nacos-discovery
時,已經引入了Ribbon
。
直接使用就行了。
寫註解
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
配置RestTemplate
的地方新增@LoadBalanced
註解即可。
使用
UserDto userDto = restTemplate.getForObject(
"http://user-center/users/{userId}",
UserDto.class,
userId);
Ribbon組成
先有個印象。二次開發再回頭看
Ribbon內建的負載均衡規則
預設是ZoneAvoidanceRule。
每一個負載均衡演算法原始碼都值得看一下。
細粒度配置自定義
- Java程式碼配置
- 用配置屬性配置
- 最佳實踐總結
場景:當內容中心呼叫使用者中心微服務的時候使用隨機負載,當內容中心呼叫其他微服務的時候使用預設負載均衡策略。
Java程式碼配置
新建配置類,註冊一個RandomRule。
package ribbonconfiguration;
import com.netflix.loadbalancer.IRule;
import com.netflix.loadbalancer.RandomRule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 配置類所在的包必須是和啟動類不一樣的包
*/
@Configuration
public class RibbonConfiguration {
@Bean
public IRule ribbonRule(){
return new RandomRule();
}
}
新建一個user-center
的ribbon
負載配置類,配置規則使用上面的隨機規則。
package com.itmuch.contentcenter.configuration;
import org.springframework.cloud.netflix.ribbon.RibbonClient;
import org.springframework.context.annotation.Configuration;
import ribbonconfiguration.RibbonConfiguration;
@Configuration
@RibbonClient(name = "user-center",configuration = RibbonConfiguration.class)
public class UserCenterRibbonConfiguration {
}
@RibbonClient
註解配置Ribbon
自定義配置
name="user-center"
表示為user-center
配置的。
configuration = RibbonConfiguration.class
用來指定負載均衡演算法,或者負載均衡規則
父子上下文
這裡的上下文是指Spring Context
。
啟動類擁有一個上下文,是父上下文,Ribbon會啟動一個子上下文,父子上下文不能重疊。
啟動類的上下文,會掃描啟動類所在包及子包下的Bean。
Ribbon
的配置類不能被啟動類的上下文掃描到。因為Spring context
是一個樹狀上下文。父子上下文掃描到包如果重疊會有各種問題。比如,導致事務不生效。
如果上面配置的RibbonConfiguration在啟動類掃描範圍內,會導致自定義配置失效,RibbonConfiguration配置的隨機負載均衡全域性生效。
配置屬性方式
user-center:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
這種方式沒有上下文重疊的坑
兩種配置方式的對比
細粒度配置最佳實踐
- 儘量使用屬性配置,屬性方式實現不了的情況下再考慮用程式碼配置
- 在同一個微服務內儘量保持單一性,比如統一使用屬性配置,不要兩種方式混用,增加定位程式碼的複雜性
全域性配置
- 方式一:讓
ComponentScan
上下文重疊(強烈不建議使用) - 方式二:唯一正確的途徑:
@RibbonClients(defaultConfiguration = xxx.class)
package com.itmuch.contentcenter.configuration;
import org.springframework.cloud.netflix.ribbon.RibbonClients;
import org.springframework.context.annotation.Configuration;
import ribbonconfiguration.RibbonConfiguration;
@Configuration
@RibbonClients(defaultConfiguration = RibbonConfiguration.class)
public class UserCenterRibbonConfiguration {
}
支援的配置項
Java Config方式:見Ribbon組成
一節的介面
配置檔案方式:
飢餓載入
預設是懶載入,在呼叫restTemplate
時才會建立一個叫user-center
的Ribbon Client
user-center
是要呼叫的客戶端名字
懶載入的問題:在第一次呼叫user-center
的介面時,訪問會慢。
可以使用飢餓載入避免這個問題。
ribbon:
eager-load:
enabled: true
clients: user-center
擴充套件Ribbon
支援Nacos權重
首先了解一下,Nacos的權重在0-1
之間,1
最大
Ribbon內建的負載均衡規則都不支援Nacos的權重,需要自己定義一個負載均衡規則。
@Slf4j
public class NacosWeightedRule extends AbstractLoadBalancerRule {
@Autowired
private NacosDiscoveryProperties nacosDiscoveryProperties;
@Override
public void initWithNiwsConfig(IClientConfig iClientConfig) {
//讀取配置檔案,並初始化當前配置NacosWeightedRule,一般不需要實現
}
@Override
public Server choose(Object key) {
BaseLoadBalancer loadBalancer = (BaseLoadBalancer) this.getLoadBalancer();
log.info("loadBalancer = {}", loadBalancer);
//想要請求的微服務的名稱
String name = loadBalancer.getName();
//實現負載均衡演算法
//這裡不自己實現,直接使用nacos提供的
//拿到服務發現的相關API
NamingService namingService = nacosDiscoveryProperties.namingServiceInstance();
try {
Instance instance = namingService.selectOneHealthyInstance(name);
log.info("選擇的例項是:port = {}, instance = {}", instance.getPort(), instance);
return new NacosServer(instance);
} catch (NacosException e) {
return null;
}
}
}
配置為全域性規則
/**
* 配置類所在的包必須是和啟動類不一樣的包
*/
@Configuration
public class RibbonConfiguration {
@Bean
public IRule ribbonRule(){
return new NacosWeightedRule();
}
}
更多擴充套件方式可以擴充套件Ribbon支援Nacos權重的三種方式
同一叢集優先呼叫
為了實現容災,把內容中心和使用者中心部署在北京機房和南京機房裡,希望呼叫的時候同機房優先。
使用Nacos服務發現領域模型裡的Cluster
編寫同叢集優先呼叫規則
@Slf4j
public class NacosSameClusterWeightedRule extends AbstractLoadBalancerRule {
@Autowired
private NacosDiscoveryProperties nacosDiscoveryProperties;
@Override
public void initWithNiwsConfig(IClientConfig iClientConfig) {
}
@Override
public Server choose(Object key) {
//拿到配置檔案中的叢集名稱
String clusterName = nacosDiscoveryProperties.getClusterName();
BaseLoadBalancer loadBalancer = (BaseLoadBalancer) this.getLoadBalancer();
log.info("loadBalancer = {}", loadBalancer);
//想要請求的微服務的名稱
String name = loadBalancer.getName();
//拿到服務發現的相關API
NamingService namingService = nacosDiscoveryProperties.namingServiceInstance();
try {
//1 找到指定服務的所有例項
List<Instance> instances = namingService.selectInstances(name, true);
//2 過濾出相同叢集下的所有例項
Stream<Instance> instanceStream = instances.stream()
.filter(instance -> Objects.equals(instance.getClusterName(), clusterName));
List<Instance> sameClusterInstances = instanceStream.collect(Collectors.toList());
List<Instance> instancesToBeChosen;
if(CollectionUtils.isEmpty(sameClusterInstances)){
instancesToBeChosen = instances;
log.warn("發生跨叢集呼叫,name = {}, clusterName = {}, instances = {}",
name,
clusterName,
instances);
}else {
instancesToBeChosen = sameClusterInstances;
}
//3 基於權重的負載均衡演算法,返回1個例項
Instance instance = ExtendBalancer.getHostByRandomWeight2(instancesToBeChosen);
log.info("選擇的例項是 port = {}, instances = {} ",instance.getPort(), instance);
return new NacosServer(instance);
} catch (NacosException e) {
log.error("發生異常",e);
}
return null;
}
}
class ExtendBalancer extends Balancer{
//Nacos沒有暴露從例項列表中選一個,只有selectOneHealthyInstance
public static Instance getHostByRandomWeight2(List<Instance> hosts) {
return getHostByRandomWeight(hosts);
}
}
配置全域性NacosSameClusterWeightedRule
@Configuration
public class RibbonConfiguration {
@Bean
public IRule ribbonRule(){
return new NacosSameClusterWeightedRule();
}
}
配置所在叢集
spring:
datasource:
url: jdbc:mysql://localhost:3306/content_center
hikari:
username: root
password: kim@2020
# >=6.x com.mysql.cj.jdbc.Driver
# <=5.x com.mysql.jdbc.Driver
driver-class-name: com.mysql.cj.jdbc.Driver
cloud:
nacos:
discovery:
server-addr: localhost:8848
cluster-name: BJ
application:
# 服務名稱儘量用-,不要用_,不要用特殊字元
name: content-center
logging:
level:
com.itmuch.usercenter.dao.content: debug
server:
servlet:
context-path:
port: 8010
#user-center:
# ribbon:
# NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
ribbon:
eager-load:
enabled: true
clients: user-center
啟動內容中心服務
接下來,配置兩個使用者中心服務,分別配置不同的叢集和埠
spring:
datasource:
url: jdbc:mysql://localhost:3306/user_center
hikari:
username: root
password: kim@2020
# >=6.x com.mysql.cj.jdbc.Driver
# <=5.x com.mysql.jdbc.Driver
driver-class-name: com.mysql.cj.jdbc.Driver
cloud:
nacos:
discovery:
server-addr: localhost:8848
# 多叢集配置
cluster-name: BJ
application:
# 服務名稱儘量用-,不要用_,不要用特殊字元
name: user-center
logging:
level:
com.itmuch.usercenter.dao.user: debug
server:
# 本地啟動多個例項,啟動前記得改埠
port: 8081
檢視Nacos控制檯
觀察到user-center
的叢集數目是2。點選詳情
頁面訪問請求http://localhost:8010/shares/1
可以看到總是請求到相同機房的例項(8081也屬於BJ叢集)。
模擬BJ叢集下線。選擇Nacos控制檯裡BJ叢集的8081例項,將其下線。
再次瀏覽器訪問http://localhost:8010/shares/1
可以觀察到已經請求到了異地機房的NJ機房的例項8082
番外:為開源專案貢獻程式碼
目前同叢集優先呼叫規則已經在新版本中被採納了,可以直接配置。我用的是2.1.0.RELEASE版本
同叢集優先呼叫規則的類是com.alibaba.cloud.nacos.ribbon.NacosRule
,直接配置這個類使用,不需要再擴充套件了。
基於元資料的版本控制
配置元資料,只要在spring.cloud.nacos.discovery.metadata
下配置key-value
對就可以
spring:
cloud:
nacos:
discovery:
server-addr: localhost:8848
cluster-name: BJ
metadata:
version: v1.0
核心邏輯是,服務提供者和服務消費者配置相同的或不同的version
元資料,在服務消費者請求服務提供者的時候,從待選例項中過濾一下,找到相同版本號的例項列表,再用一種負載演算法從從版本號列表中選一個例項。
String version = nacosDiscoveryProperties.getMetadata().get("version");
NamingService namingService = nacosDiscoveryProperties.namingServiceInstance();
try {
//1 找到指定服務的所有例項
List<Instance> instances = namingService.selectInstances(name, true);
//過濾出同叢集的例項列表
//過濾出版本號相同的例項列表
List<Instance> sameVersionInstances = instancesToBeChosen.stream()
.filter(instance -> Objects.equals(instance.getMetadata().get("version"), version))
.collect(Collectors.toList());
//從列表中選出一個例項
Instance instance = ExtendBalancer.getHostByRandomWeight2(instancesToBeChosen);
具體實現參見手記
深入理解Namespace
配置namespace
spring:
datasource:
url: jdbc:mysql://localhost:3306/content_center
hikari:
username: root
password: kim@2020
# >=6.x com.mysql.cj.jdbc.Driver
# <=5.x com.mysql.jdbc.Driver
driver-class-name: com.mysql.cj.jdbc.Driver
cloud:
nacos:
discovery:
server-addr: localhost:8848
cluster-name: BJ
metadata:
version: v1.0
# 指定namespace
namespace: bc4f4e1a-bf4e-4bcc-86f1-7f6252f81e45
application:
# 服務名稱儘量用-,不要用_,不要用特殊字元
name: content-center
跨namespace不能呼叫
在使用者中心和內容中心分別配上同樣的名稱空間ID。才可以正常訪問。
現有架構存在的問題
- 程式碼不可讀
- 複雜的url難以維護
- 難以響應需求變化,變化沒有幸福感
- 程式設計體驗不統一
宣告式HTTP客戶端Feign
- Feign是Netflix開源的宣告式HTTP客戶端
- Github地址
使用Feign實現遠端HTTP呼叫
引入依賴
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
寫註解
啟動類上加上@EnableFeignClients
註解
寫配置
暫時沒有
實現一個Feign介面
@FeignClient(name = "user-center")
public interface UserCenterFeignClient {
/**
* http://user-center/users/{id}
* @param id
* @return
*/
@GetMapping("/users/{id}")
UserDto findById(@PathVariable Integer id);
}
@Slf4j
@Service
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class ShareService {
private final ShareMapper shareMapper;
private final UserCenterFeignClient userCenterFeignClient;
public ShareDto findById(Integer id){
//獲取分享詳情
Share share = this.shareMapper.selectByPrimaryKey(id);
//釋出人id
Integer userId = share.getUserId();
UserDto userDto = this.userCenterFeignClient.findById(userId);
ShareDto shareDto = new ShareDto();
//訊息的裝配
BeanUtils.copyProperties(share, shareDto);
shareDto.setWxNickName(userDto.getWxNickname());
return shareDto;
}
}
所謂的宣告式HTTP客戶端,就是隻需要宣告一個Feign Client介面,Feign就會根據宣告的介面,自動幫我們構造請求的目標地址,並幫助你請求。
Feign的組成
細粒度配置自定義
預設Feign不列印任何日誌,可以自定義Feign日誌級別,讓其列印日誌
Feign日誌級別
Java配置方式
UserCenterFeignConfiguration
/**
* feign的配置類,最佳實踐不要加@Configuration註解,否則必須挪到@ComponentScan能掃描的包以外。
* 是因為重複掃描,父子上下文的問題
*/
public class UserCenterFeignConfiguration {
@Bean
public Logger.Level level(){
//列印所有請求的細節
return Logger.Level.FULL;
}
}
UserCenterFeignClient
@FeignClient(name = "user-center", configuration = UserCenterFeignConfiguration.class)
public interface UserCenterFeignClient {
/**
* http://user-center/users/{id}
* @param id
* @return
*/
@GetMapping("/users/{id}")
UserDto findById(@PathVariable Integer id);
}
logging:
level:
com.itmuch.usercenter.dao.content: debug
com.itmuch.contentcenter.feignclient.UserCenterFeignClient: debug
屬性方式配置
feign:
client:
config:
# 想要呼叫的微服務的名稱
user-center:
loggerLevel: full
全域性配置
程式碼方式
將細粒度的配置方式都註釋掉
在啟動類配置上全域性配置
@EnableFeignClients(defaultConfiguration = UserCenterFeignConfiguration.class)
配置屬性方式
feign:
client:
config:
default:
loggerLevel: full
支援的配置項
程式碼方式
屬性配置方式
配置最佳實踐
Ribbon配置 vs Feign配置
Ribbon是一個負載均衡器,幫我們選擇一個例項
Feign是一個宣告式HTTP客戶端,幫助我們更優雅的請求
Feign程式碼方式vs屬性方式
優先順序:全域性程式碼<全域性屬性<細粒度程式碼<細粒度屬性
最佳實踐
- 儘量使用屬性配置,屬性方式實現不了的情況再考慮用程式碼配置
- 在同一個微服務內儘量保持單一性,比如統一使用屬性配置,不要兩種方式混用,增加定位程式碼的複雜性
Feign的繼承
這個特性帶來了緊耦合,因為在微服務間共享介面,官方不建議使用。
現狀:很多公司用,程式碼複用。
新專案如何選擇:權衡利弊,會得到什麼好處,失去什麼,是不是划算,划算就上。
多引數請求構造
Get請求引數使用@SpringQueryMap註解
@FeignClient(name = "user-center")
public interface TestUserCenterFeignClient {
@GetMapping("/q")
UserDto query(@SpringQueryMap UserDto userDto);
}
Post請求多引數,也可以使用@RequestBody。
Feign脫離Ribbon使用
@FeignClient(name = "baidu", url="http://www.baidu.com")
public interface TestBaiduFeignClient {
@GetMapping("")
String index();
}
@GetMapping("baidu")
public String baiduIndex(){
return this.testBaiduFeignClient.index();
}
RestTemplate vs Feign
如何選擇?
- 原則:儘量用Feign,杜絕使用RestTemplate
儘量減少開發人員的選擇,共存會帶來風格的不統一,額外的學習成本和額外的程式碼理解成本
- 事無絕對,合理選擇
Feign解決不了,才用RestTemplate
Feign的效能優化
- 連線池【提升15%左右】,預設使用URLConnection,可以修改
可以選用httpclient或者okhttp
新增依賴
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-okhttp</artifactId>
</dependency>
feign:
client:
config:
default:
loggerLevel: full
httpclient:
# 讓feign使用apache httpclient做請求,而不是預設的urlclient
enabled: true
# feign的最大連線數
max-connections: 200
# feign單個路徑的最大連線數
max-connections-per-route: 50
okhttp:
enabled: true
# feign的最大連線數
max-connections: 200
# feign單個路徑的最大連線數
max-connections-per-route: 50
- 日誌級別
生產環境建議設定為basic
Feign常見問題總結
現有架構總結
服務容錯-Sentinel
雪崩效應:基礎服務故障,導致導致上層服務故障,並且故障不斷放大。又稱為cascading failure,級聯失效,級聯故障。
雪崩效應是因為服務沒有做好容錯。
常見的容錯方案(容錯思想)
- 超時
- 限流
- 倉壁模式(執行緒池隔離)
- 斷路器模式
5秒內錯誤率、錯誤次數達到就跳閘。
斷路器三態:
使用Sentinel實現容錯
是什麼:輕量級的流量控制、熔斷降級Java庫。
整合Sentinel
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
可以使用/actuator/sentinel斷點檢視sentinel相關資訊。
整合Actuator
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
需要加入配置才能暴露sentinel端點。
management:
endpoints:
web:
exposure:
include: '*'
Sentinel控制檯
搭建Sentinel控制檯
https://github.com/alibaba/Sentinel/releases
生產環境,控制檯版本最好和整體版本一致。
啟動sentinel控制檯
java -jar /Users/kim/Downloads/sentinel-dashboard-1.7.2.jar
預設在localhost:8080埠,使用者名稱密碼都是sentinel。
為內容中心整合sentinel控制檯
# 指定sentinel 控制檯地址
spring.cloud.sentinel.transport.dashboard: localhost:8080
確保nacos、sentinel控制檯、內容中心和使用者中心都啟動了。然後訪問http://localhost:8010/shares/1多次,就可以在實時監控裡看到效果。
流控規則
點選簇點鏈路,點選/shares/1的流控按鈕,就可以為這個訪問路徑設定流控規則。
資源名
預設是請求路徑。
針對來源
針對呼叫者限流。針對來源是呼叫者微服務名稱。
閾值型別
QPS、執行緒數。比如選擇QPS,表示:當呼叫當前資源的QPS達到閾值時,就去限流。
是否叢集
流控模式
直接
關聯
<1>
當關聯的資源達到閾值,就限流自己
比如我們設定關聯資源為/actuator/sentinel
,當關聯資源的qps達到1時,就限流/shares/1
寫一個測試類,呼叫/actuator/sentinel
public class SentinelTest {
public static void main(String[] args) throws InterruptedException {
RestTemplate restTemplate = new RestTemplate();
for (int i = 0; i < 10000; i++) {
String forObject = restTemplate.getForObject("http://localhost:8010/actuator/sentinel", String.class);
Thread.sleep(500);
}
}
}
執行這個測試類,再去呼叫/shares/1
,發信啊已經被限流了。
實際應用,如果希望修改優先,可以配置關聯API為修改的API,資源名設定為查詢的API。當修改的測試過多,就限流查詢,保證效能。
鏈路
只記錄指定鏈路上的流量