1. 程式人生 > 實用技巧 >SpringCloud Alibaba學習筆記

SpringCloud Alibaba學習筆記

目錄

目錄

導學

為什麼學

  • 元件效能更強
  • 良好的視覺化介面
  • 搭建簡單,學習曲線低
  • 文件豐富並且是中文

學習目標

  • 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 文件

課程文件主要分四類:

  1. API文件:https://t.itmuch.com/doc.html
  2. 課程配套程式碼:https://git.imooc.com/coding-358/
  3. 課程相關資源(例如檢表語句、資料模型、課上用到的軟體等):https://git.imooc.com/coding-358/resource
  4. 課上用到的一些課外讀物(慕課網手記):http://www.imooc.com/t/1863086

如何建立小程式

註冊賬號:https://mp.weixin.qq.com

按照提示填寫資訊

前端程式碼如何使用

建立專案

技術選型
  • 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 wikilombok,看有沒有生成支援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

搭建Nacos Server

選擇Nacos Server版本

檢視引入到spring-cloud-alibaba-dependencie依賴

啟動伺服器

startup.sh -m standalone

訪問控制檯

http://localhost:8848/nacos

預設使用者名稱密碼都是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元資料

元資料的作用:

  • 提供描述資訊
  • 讓微服務呼叫更靈活
    • 例如:微服務版本控制

如何為微服務設定元資料

  • 控制檯介面
  • 配置檔案指定
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

負載均衡的兩種方式

  • 服務端負載均衡
  • 客戶端負載均衡(客戶端呼叫的時候使用選擇負載均衡演算法)

手寫一個客戶端負載均衡器

改寫一下ShareServicefindById方法。從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-centerribbon負載配置類,配置規則使用上面的隨機規則。

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-centerRibbon 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。才可以正常訪問。

現有架構存在的問題

  1. 程式碼不可讀
  2. 複雜的url難以維護
  3. 難以響應需求變化,變化沒有幸福感
  4. 程式設計體驗不統一

宣告式HTTP客戶端Feign

使用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的繼承

這個特性帶來了緊耦合,因為在微服務間共享介面,官方不建議使用。

現狀:很多公司用,程式碼複用。

新專案如何選擇:權衡利弊,會得到什麼好處,失去什麼,是不是划算,划算就上。

多引數請求構造

如何使用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。當修改的測試過多,就限流查詢,保證效能。

鏈路

只記錄指定鏈路上的流量

流控效果