六、服務(wù)網(wǎng)關(guān) SpringCloud Gateway
在微服務(wù)架構(gòu)中,一個(gè)系統(tǒng)會(huì)被拆分為很多個(gè)微服務(wù)。那么作為客戶端要如何去調(diào)用這么多的微服務(wù)呢?
如果沒(méi)有網(wǎng)關(guān)的存在,我們只能在客戶端記錄每個(gè)微服務(wù)的地址,然后分別去調(diào)用。

這樣的話,會(huì)有很多問(wèn)題:
- 客戶端多次請(qǐng)求不同的微服務(wù),增加客戶端代碼或配置編寫的復(fù)雜性
- 認(rèn)證復(fù)雜,每個(gè)服務(wù)都需要獨(dú)立認(rèn)證。
- 各個(gè)微服務(wù)都存在跨域請(qǐng)求,在一定場(chǎng)景下處理相對(duì)復(fù)雜。
這些問(wèn)題,我們就可以采用網(wǎng)關(guān)來(lái)解決。
所謂的API網(wǎng)關(guān),就是指系統(tǒng)的統(tǒng)一入口,它封裝了應(yīng)用程序的內(nèi)部結(jié)構(gòu),為客戶端提供統(tǒng)一服務(wù),一些與業(yè)務(wù)本身功能無(wú)關(guān)的公共邏輯可以在這里實(shí)現(xiàn),諸如認(rèn)證、鑒權(quán)、監(jiān)控、路由轉(zhuǎn)發(fā)等等。
添加上API網(wǎng)關(guān)之后,系統(tǒng)的架構(gòu)圖變成了如下所示:

6.1 目前可以使用的網(wǎng)關(guān)
Ngnix+lua
使用nginx的反向代理和負(fù)載均衡可實(shí)現(xiàn)對(duì)api服務(wù)器的負(fù)載均衡及高可用
lua是一種腳本語(yǔ)言,可以來(lái)編寫一些簡(jiǎn)單的邏輯, nginx支持lua腳本
Kong
基于Nginx+Lua開發(fā),性能高,穩(wěn)定,有多個(gè)可用的插件(限流、鑒權(quán)等等)可以開箱即用。 問(wèn)題:只支持Http協(xié)議;二次開發(fā),自由擴(kuò)展困難;提供管理API,缺乏更易用的管控、配置方式。
Zuul
Netflix開源的網(wǎng)關(guān),功能豐富,使用JAVA開發(fā),易于二次開發(fā)。問(wèn)題:缺乏管控,無(wú)法動(dòng)態(tài)配置;依賴組件較多;處理Http請(qǐng)求依賴的是Web容器,性能不如Nginx。
Spring Cloud Gateway
Spring Cloud Gateway是Spring公司基于Spring 5.0,Spring Boot 2.0 和 Project Reactor 等技術(shù)開發(fā)的網(wǎng)關(guān),它旨在為微服務(wù)架構(gòu)提供一種簡(jiǎn)單有效的統(tǒng)一的 API 路由管理方式。它的目標(biāo)是替代 Netflix Zuul,其不僅提供統(tǒng)一的路由方式,并且基于 Filter 鏈的方式提供了網(wǎng)關(guān)基本的功能,例如:安全,監(jiān)控和限流。
優(yōu)點(diǎn):
- 性能強(qiáng)勁:是Zuul1.0的1.6倍
- 功能強(qiáng)大:內(nèi)置了很多實(shí)用的功能,例如轉(zhuǎn)發(fā)、監(jiān)控、限流等
- 設(shè)計(jì)優(yōu)雅,容易擴(kuò)展
缺點(diǎn):
- 其實(shí)現(xiàn)依賴Netty與WebFlux,不是傳統(tǒng)的Servlet編程模型,學(xué)習(xí)成本高
- 不能將其部署在Tomcat、Jetty等Servlet容器里,只能打成jar包執(zhí)行
- 需要Spring Boot 2.0及以上的版本,才支持
6.3 快速開始
基礎(chǔ)版
1、創(chuàng)建api-gateway模塊
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
2、添加配置文件
server:
port: 7000
spring:
application:
name: api-gateway
cloud:
gateway:
routes:
- id: product_route
uri: http://127.0.0.1:8081
order: 1
predicates: # 斷言,當(dāng)路由滿足全部斷言時(shí)進(jìn)行轉(zhuǎn)發(fā)
- Path=/product-serv/**
filters: # 過(guò)濾器
- StripPrefix=1 # 刪除一級(jí)路徑
3、啟動(dòng)網(wǎng)關(guān)和商品微服務(wù),請(qǐng)求 http://127.0.0.1:7000/product-serv/product/1

接入注冊(cè)中心nacos
1、引入nacos依賴
<!--nacos客戶端-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
2、修改配置文件
server:
port: 7000
spring:
application:
name: api-gateway
cloud:
nacos:
discovery:
server-addr: localhost:8848 # 將gateway注冊(cè)到nacos
gateway:
discovery:
locator:
enabled: true # 讓gateway從nacos中獲取服務(wù)信息
routes:
- id: product_route
uri: lb://service-product
# uri: http://127.0.0.1:8081
order: 1
predicates: # 斷言,當(dāng)路由滿足全部斷言時(shí)進(jìn)行轉(zhuǎn)發(fā)
- Path=/product-serv/**
filters: # 過(guò)濾器
- StripPrefix=1 # 刪除一級(jí)路徑
3、請(qǐng)求 http://127.0.0.1:7000/product-serv/product/1
6.4 Gateway的基本概念&請(qǐng)求處理流程
基本概念
路由(Route) 是 gateway 中最基本的組件之一,表示一個(gè)具體的路由信息載體。主要定義了下面的幾個(gè)信息:
- id,路由標(biāo)識(shí)符,區(qū)別于其他 Route。
- uri,路由指向的目的地 uri,即客戶端請(qǐng)求最終被轉(zhuǎn)發(fā)到的微服務(wù)。
- order,用于多個(gè) Route 之間的排序,數(shù)值越小排序越靠前,匹配優(yōu)先級(jí)越高。
- predicate,斷言的作用是進(jìn)行條件判斷,只有斷言都返回真,才會(huì)真正的執(zhí)行路由。 、- filter,過(guò)濾器用于修改請(qǐng)求和響應(yīng)信息。
請(qǐng)求處理流程

- Gateway Client向Gateway Server發(fā)送請(qǐng)求
- 請(qǐng)求首先會(huì)被HttpWebHandlerAdapter進(jìn)行提取組裝成網(wǎng)關(guān)上下文
- 然后網(wǎng)關(guān)的上下文會(huì)傳遞到DispatcherHandler,它負(fù)責(zé)將請(qǐng)求分發(fā)給RoutePredicateHandlerMapping
- RoutePredicateHandlerMapping負(fù)責(zé)路由查找,并根據(jù)路由斷言判斷路由是否可用
- 如果過(guò)斷言成功,由FilteringWebHandler創(chuàng)建過(guò)濾器鏈并調(diào)用
- 請(qǐng)求會(huì)一次經(jīng)過(guò)PreFilter--微服務(wù)--PostFilter的方法,最終返回響應(yīng)
6.5 斷言
6.5.1 內(nèi)置斷言工廠
基于Datetime類型的斷言工廠
此類型的斷言根據(jù)時(shí)間做判斷,主要有三個(gè):
AfterRoutePredicateFactory: 接收一個(gè)日期參數(shù),判斷請(qǐng)求日期是否晚于指定日期
BeforeRoutePredicateFactory: 接收一個(gè)日期參數(shù),判斷請(qǐng)求日期是否早于指定日期
BetweenRoutePredicateFactory: 接收兩個(gè)日期參數(shù),判斷請(qǐng)求日期是否在指定時(shí)間段內(nèi)
-After=2019-12-31T23:59:59.789+08:00[Asia/Shanghai]
基于遠(yuǎn)程地址的斷言工廠
RemoteAddrRoutePredicateFactory:接收一個(gè)IP地址段,判斷請(qǐng)求主機(jī)地址是否在地址段中
-RemoteAddr=192.168.1.1/24
基于Cookie的斷言工廠
CookieRoutePredicateFactory:接收兩個(gè)參數(shù),cookie 名字和一個(gè)正則表達(dá)式。 判斷請(qǐng)求cookie是否具有給定名稱且值與正則表達(dá)式匹配。
-Cookie=chocolate, ch.
基于Header的斷言工廠
HeaderRoutePredicateFactory:接收兩個(gè)參數(shù),標(biāo)題名稱和正則表達(dá)式。判斷請(qǐng)求Header是否具有給定名稱且值與正則表達(dá)式匹配。
-Header=X-Request-Id, \d+
基于Host的斷言工廠
HostRoutePredicateFactory:接收一個(gè)參數(shù),主機(jī)名模式。判斷請(qǐng)求的Host是否滿足匹配規(guī)則。
-Host=**.testhost.org
基于Method請(qǐng)求方法的斷言工廠
MethodRoutePredicateFactory:接收一個(gè)參數(shù),判斷請(qǐng)求類型是否跟指定的類型匹配。
-Method=GET
基于Path請(qǐng)求路徑的斷言工廠
PathRoutePredicateFactory:接收一個(gè)參數(shù),判斷請(qǐng)求的URI部分是否滿足路徑規(guī)則。
-Path=/foo/{segment}
基于Query請(qǐng)求參數(shù)的斷言工廠
QueryRoutePredicateFactory :接收兩個(gè)參數(shù),請(qǐng)求param和正則表達(dá)式,判斷請(qǐng)求參數(shù)是否具有給定名稱且值與正則表達(dá)式匹配。
-Query=baz, ba.
基于路由權(quán)重的斷言工廠
WeightRoutePredicateFactory:接收一個(gè)[組名,權(quán)重], 然后對(duì)于同一個(gè)組內(nèi)的路由按照權(quán)重轉(zhuǎn)發(fā)
routes:
-id: weight_route1
uri: host1
predicates:
-Path=/product/**
-Weight=group3, 1
-id: weight_route2
uri: host2
predicates:
-Path=/product/**
-Weight= group3, 9
6.5.2 自定義斷言工廠
我們來(lái)設(shè)定一個(gè)場(chǎng)景: 假設(shè)我們的應(yīng)用僅僅讓age在(min,max)之間的人來(lái)訪問(wèn)。
1、在配置文件中,添加一個(gè)Age的斷言配置
server:
port: 7000
spring:
application:
name: api-gateway
cloud:
nacos:
discovery:
server-addr: localhost:8848 # 將gateway注冊(cè)到nacos
gateway:
discovery:
locator:
enabled: true # 讓gateway從nacos中獲取服務(wù)信息
routes:
- id: product_route
uri: lb://service-product
# uri: http://127.0.0.1:8081
order: 1
predicates: # 斷言,當(dāng)路由滿足全部斷言時(shí)進(jìn)行轉(zhuǎn)發(fā)
- Path=/product-serv/**
- Age=18,60 # ********看這里
filters: # 過(guò)濾器
- StripPrefix=1 # 刪除一級(jí)路徑
2、新建一個(gè)斷言工廠,實(shí)現(xiàn)斷言方法
//這是一個(gè)自定義的路由斷言工廠類,要求有兩個(gè)
//1 名字必須是 配置+RoutePredicateFactory
//2 必須繼承AbstractRoutePredicateFactory<配置類>
@Component
public class AgeRoutePredicateFactory extends AbstractRoutePredicateFactory<AgeRoutePredicateFactory.Config> {
//構(gòu)造函數(shù)
public AgeRoutePredicateFactory() {
super(Config.class);
}
//讀取配置文件的中參數(shù)值 給他賦值到配置類中的屬性上
public List<String> shortcutFieldOrder() {
//這個(gè)位置的順序必須跟配置文件中的值的順序?qū)?yīng)
return Arrays.asList("minAge", "maxAge");
}
//斷言邏輯
public Predicate<ServerWebExchange> apply(Config config) {
return new Predicate<ServerWebExchange>() {
@Override
public boolean test(ServerWebExchange serverWebExchange) {
//1 接收前臺(tái)傳入的age參數(shù)
String ageStr = serverWebExchange.getRequest().getQueryParams().getFirst("age");
//2 先判斷是否為空
if (StringUtils.isNotEmpty(ageStr)) {
//3 如果不為空,再進(jìn)行路由邏輯判斷
int age = Integer.parseInt(ageStr);
if (age < config.getMaxAge() && age > config.getMinAge()) {
return true;
} else {
return false;
}
}
return false;
}
};
}
//配置類,用于接收配置文件中的對(duì)應(yīng)參數(shù)
@Data
@NoArgsConstructor
public static class Config {
private int minAge;//18
private int maxAge;//60
}
}
3、重啟網(wǎng)關(guān)服務(wù),訪問(wèn):
http://localhost:7000/product-serv/product/1?age=30
http://localhost:7000/product-serv/product/1?age=10
6.6 過(guò)濾器
作用:在請(qǐng)求的傳遞過(guò)程中,對(duì)請(qǐng)求和響應(yīng)做一些手腳。
在Gateway中,F(xiàn)ilter的生命周期只有兩個(gè):
- PRE:這種過(guò)濾器在請(qǐng)求被路由之前調(diào)用。我們可利用這種過(guò)濾器實(shí)現(xiàn)身份驗(yàn)證、在集群中選擇請(qǐng)求的微服務(wù)、記錄調(diào)試信息等。
- POST:這種過(guò)濾器在路由到微服務(wù)以后執(zhí)行。這種過(guò)濾器可用來(lái)為響應(yīng)添加標(biāo)準(zhǔn)的HTTP Header、收集統(tǒng)計(jì)信息和指標(biāo)、將響應(yīng)從微服務(wù)發(fā)送給客戶端等。
Gateway 的Filter從作用范圍可分為兩種:
- GatewayFilter:應(yīng)用到單個(gè)路由或者一個(gè)分組的路由上。
- GlobalFilter:應(yīng)用到所有的路由上。
6.6.1 局部過(guò)濾器
局部過(guò)濾器是針對(duì)單個(gè)路由的過(guò)濾器。
內(nèi)置局部過(guò)濾器
| 過(guò)濾器工廠 | 作用 | 參數(shù) |
|---|---|---|
| AddRequestHeader | 為原始請(qǐng)求添加Header | Header的名稱及值 |
| AddRequestParameter | 為原始請(qǐng)求添加請(qǐng)求參數(shù) | 參數(shù)名稱及值 |
| AddResponseHeader | 為原始響應(yīng)添加Header | Header的名稱及值 |
| DedupeResponseHeader | 剔除響應(yīng)頭中重復(fù)的值 | 需要去重的Header名稱及去重策略 |
| Hystrix | 為路由引入Hystrix的斷路器保護(hù) |
HystrixCommand的名稱 |
| FallbackHeaders | 為fallbackUri的請(qǐng)求頭中添加具體的異常信息 | Header的名稱 |
| PrefixPath | 為原始請(qǐng)求路徑添加前綴 | 前綴路徑 |
| PreserveHostHeader | 為請(qǐng)求添加一個(gè)preserveHostHeader=true的屬性,路由過(guò)濾器會(huì)檢查該屬性以決定是否要發(fā)送原始的Host | 無(wú) |
| RequestRateLimiter | 用于對(duì)請(qǐng)求限流,限流算法為令牌桶 | keyResolver、rateLimiter、statusCode、denyEmptyKey、emptyKeyStatus |
| RedirectTo | 將原始請(qǐng)求重定向到指定的URL | http狀態(tài)碼及重定向的url |
| RemoveHopByHopHeadersFilter | 為原始請(qǐng)求刪除IETF組織規(guī)定的一系列Header | 默認(rèn)就會(huì)啟用,可以通過(guò)配置指定僅刪除哪些Header |
| RemoveRequestHeader | 為原始請(qǐng)求刪除某個(gè)Header | Header名稱 |
| RemoveResponseHeader | 為原始響應(yīng)刪除某個(gè)Header | Header名稱 |
| RewritePath | 重寫原始的請(qǐng)求路徑 | 原始路徑正則表達(dá)式以及重寫后路徑的正則表達(dá)式 |
| RewriteResponseHeader | 重寫原始響應(yīng)中的某個(gè)Header | Header名稱,值的正則表達(dá)式,重寫后的值 |
| SaveSession | 在轉(zhuǎn)發(fā)請(qǐng)求之前,強(qiáng)制執(zhí)行WebSession::save操作 |
無(wú) |
| secureHeaders | 為原始響應(yīng)添加一系列起安全作用的響應(yīng)頭 | 無(wú),支持修改這些安全響應(yīng)頭的值 |
| SetPath | 修改原始的請(qǐng)求路徑 | 修改后的路徑 |
| SetResponseHeader | 修改原始響應(yīng)中某個(gè)Header的值 | Header名稱,修改后的值 |
| SetStatus | 修改原始響應(yīng)的狀態(tài)碼 | HTTP 狀態(tài)碼,可以是數(shù)字,也可以是字符串 |
| StripPrefix | 用于截?cái)嘣颊?qǐng)求的路徑 | 使用數(shù)字表示要截?cái)嗟穆窂降臄?shù)量 |
| Retry | 針對(duì)不同的響應(yīng)進(jìn)行重試 | retries、statuses、methods、series |
| RequestSize | 設(shè)置允許接收最大請(qǐng)求包的大小。如果請(qǐng)求包大小超過(guò)設(shè)置的值,則返回 413 Payload Too Large
|
請(qǐng)求包大小,單位為字節(jié),默認(rèn)值為5M |
| ModifyRequestBody | 在轉(zhuǎn)發(fā)請(qǐng)求之前修改原始請(qǐng)求體內(nèi)容 | 修改后的請(qǐng)求體內(nèi)容 |
| ModifyResponseBody | 修改原始響應(yīng)體的內(nèi)容 | 修改后的響應(yīng)體內(nèi)容 |
| Default | 為所有路由添加過(guò)濾器 | 過(guò)濾器工廠名稱及值 |
Tips:每個(gè)過(guò)濾器工廠都對(duì)應(yīng)一個(gè)實(shí)現(xiàn)類,并且這些類的名稱必須以GatewayFilterFactory結(jié)尾,這是Spring Cloud Gateway的一個(gè)約定,例如AddRequestHeader對(duì)應(yīng)的實(shí)現(xiàn)類為AddRequestHeaderGatewayFilterFactory。
自定義局部過(guò)濾器
1、在配置文件中,添加一個(gè)Log的過(guò)濾器配置
server:
port: 7000
spring:
application:
name: api-gateway
cloud:
nacos:
discovery:
server-addr: localhost:8848 # 將gateway注冊(cè)到nacos
gateway:
discovery:
locator:
enabled: true # 讓gateway從nacos中獲取服務(wù)信息
routes:
- id: product_route
uri: lb://service-product
# uri: http://127.0.0.1:8081
order: 1
predicates: # 斷言,當(dāng)路由滿足全部斷言時(shí)進(jìn)行轉(zhuǎn)發(fā)
- Path=/product-serv/**
# - Age=18,60
filters: # 過(guò)濾器
- StripPrefix=1 # 刪除一級(jí)路徑
- Log=true,false # 控制日志是否開啟
2、自定義一個(gè)過(guò)濾器工廠
//自定義局部過(guò)濾器
@Component
public class LogGatewayFilterFactory
extends AbstractGatewayFilterFactory<LogGatewayFilterFactory.Config> {
//構(gòu)造函數(shù)
public LogGatewayFilterFactory() {
super(Config.class);
}
//讀取配置文件中的參數(shù) 賦值到 配置類中
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList("consoleLog", "cacheLog");
}
//過(guò)濾器邏輯
@Override
public GatewayFilter apply(Config config) {
return new GatewayFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
if (config.isCacheLog()) {
System.out.println("cacheLog已經(jīng)開啟了....");
}
if (config.isConsoleLog()) {
System.out.println("consoleLog已經(jīng)開啟了....");
}
return chain.filter(exchange);
}
};
}
//配置類 接收配置參數(shù)
@Data
@NoArgsConstructor
public static class Config {
private boolean consoleLog;
private boolean cacheLog;
}
}
3、訪問(wèn) http://localhost:7000/product-serv/product/1 查看控制臺(tái)
6.6.2 全局過(guò)濾器
全局過(guò)濾器作用于所有路由,無(wú)需配置。通過(guò)全局過(guò)濾器可以實(shí)現(xiàn)對(duì)權(quán)限的統(tǒng)一校驗(yàn),安全性驗(yàn)證等功能。
內(nèi)置全局過(guò)濾器

其中LBCFilter在我們寫uri: lb://service-product時(shí),已經(jīng)使用到了。
自定義全局過(guò)濾器
內(nèi)置的過(guò)濾器已經(jīng)可以完成大部分的功能,但是對(duì)于企業(yè)開發(fā)的一些業(yè)務(wù)功能處理,還是需要我們自己編寫過(guò)濾器來(lái)實(shí)現(xiàn)的,那么我們一起通過(guò)代碼的形式自定義一個(gè)過(guò)濾器,去完成統(tǒng)一的權(quán)限校驗(yàn)。
eg。實(shí)現(xiàn)服務(wù)鑒權(quán)
當(dāng)客戶端第一次請(qǐng)求服務(wù)時(shí),服務(wù)端對(duì)用戶進(jìn)行信息認(rèn)證(登錄)
認(rèn)證通過(guò),將用戶信息進(jìn)行加密形成token,返回給客戶端,作為登錄憑證
以后每次請(qǐng)求,客戶端都攜帶認(rèn)證的token
服務(wù)端對(duì)token進(jìn)行解密,判斷是否有效。

1、實(shí)現(xiàn)GlobalFilter, Ordered 接口,書寫自己的鑒權(quán)邏輯
//自定義全局過(guò)濾器需要實(shí)現(xiàn)GlobalFilter和Ordered接口
@Component
public class AuthGlobalFilter implements GlobalFilter, Ordered {
//完成判斷邏輯
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String token = exchange.getRequest().getQueryParams().getFirst("token");
if (!StringUtils.equals(token, "admin")) {
System.out.println("鑒權(quán)失敗");
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
//調(diào)用chain.filter繼續(xù)向下游執(zhí)行
return chain.filter(exchange);
}
//順序,數(shù)值越小,優(yōu)先級(jí)越高
@Override
public int getOrder() {
return 0;
}
}
2、訪問(wèn)
http://localhost:7000/product-serv/product/1
http://localhost:7000/product-serv/product/1?token=admin
6.7 網(wǎng)關(guān)限流(感覺(jué)用途不大)
網(wǎng)關(guān)是所有請(qǐng)求的公共入口,所以可以在網(wǎng)關(guān)進(jìn)行限流,而且限流的方式也很多,我們本次采用前面學(xué)過(guò)的Sentinel組件來(lái)實(shí)現(xiàn)網(wǎng)關(guān)的限流。Sentinel支持對(duì)SpringCloud Gateway、Zuul等主流網(wǎng)關(guān)進(jìn)行限流。

從1.6.0版本開始,Sentinel提供了SpringCloud Gateway的適配模塊,可以提供兩種資源維度的限流:
- route維度:即在Spring配置文件中配置的路由條目,資源名為對(duì)應(yīng)的routeId
- 自定義API維度:用戶可以利用Sentinel提供的API來(lái)自定義一些API分組
路由維度
1、引入sentinel依賴
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-spring-cloud-gateway-adapter</artifactId>
</dependency>
2、編寫配置類
基于Sentinel的Gateway限流是通過(guò)其提供的Filter來(lái)完成的,使用時(shí)只需注入對(duì)應(yīng)的 SentinelGatewayFilter實(shí)例以及 SentinelGatewayBlockExceptionHandler 實(shí)例即可。
代碼見(jiàn) cn.x5456.gateway.config.GatewayConfiguration
3、在一秒鐘內(nèi)多次訪問(wèn)http://localhost:7000/product-serv/product/1就可以看到限流啟作用了。
API維度
代碼見(jiàn) cn.x5456.gateway.config.GatewayConfiguration
七、鏈路追蹤 Sleuth
在大型系統(tǒng)的微服務(wù)化構(gòu)建中,一個(gè)系統(tǒng)被拆分成了許多模塊。這些模塊負(fù)責(zé)不同的功能,組合成系統(tǒng),最終可以提供豐富的功能。在這種架構(gòu)中,一次請(qǐng)求往往需要涉及到多個(gè)服務(wù)?;ヂ?lián)網(wǎng)應(yīng)用構(gòu)建在不同的軟件模塊集上,這些軟件模塊,有可能是由不同的團(tuán)隊(duì)開發(fā)、可能使用不同的編程語(yǔ)言來(lái)實(shí)現(xiàn)、有可能布在了幾千臺(tái)服務(wù)器,橫跨多個(gè)不同的數(shù)據(jù)中心,也就意味著這種架構(gòu)形式也會(huì)存在一些問(wèn)題:
- 如何快速發(fā)現(xiàn)問(wèn)題?
- 如何判斷故障影響范圍?
- 如何梳理服務(wù)依賴以及依賴的合理性?
- 如何分析鏈路性能問(wèn)題以及實(shí)時(shí)容量規(guī)劃?
分布式鏈路追蹤(Distributed Tracing),就是將一次分布式請(qǐng)求還原成調(diào)用鏈路,進(jìn)行日志記 錄,性能監(jiān)控并將一次分布式請(qǐng)求的調(diào)用情況集中展示。比如各個(gè)服務(wù)節(jié)點(diǎn)上的耗時(shí)、請(qǐng)求具體到達(dá)哪 臺(tái)機(jī)器上、每個(gè)服務(wù)節(jié)點(diǎn)的請(qǐng)求狀態(tài)等等。
7.1 常用鏈路追蹤技術(shù)
cat 由大眾點(diǎn)評(píng)開源,基于Java開發(fā)的實(shí)時(shí)應(yīng)用監(jiān)控平臺(tái),包括實(shí)時(shí)應(yīng)用監(jiān)控,業(yè)務(wù)監(jiān)控。集成方案是通過(guò)代碼埋點(diǎn)的方式來(lái)實(shí)現(xiàn)監(jiān)控,比如: 攔截器,過(guò)濾器等。對(duì)代碼的侵入性很大,集成成本較高。風(fēng)險(xiǎn)較大。
zipkin 由Twitter公司開源,開放源代碼分布式的跟蹤系統(tǒng),用于收集服務(wù)的定時(shí)數(shù)據(jù),以解決微服務(wù)架構(gòu)中的延遲問(wèn)題,包括:數(shù)據(jù)的收集、存儲(chǔ)、查找和展現(xiàn)。該產(chǎn)品結(jié)合spring-cloud-sleuth 使用較為簡(jiǎn)單,集成很方便,但是功能較簡(jiǎn)單。
pinpoint Pinpoint是韓國(guó)人開源的基于字節(jié)碼注入的調(diào)用鏈分析,以及應(yīng)用監(jiān)控分析工具。特點(diǎn)是支持多種插件,UI功能強(qiáng)大,接入端無(wú)代碼侵入。
SkyWalking是本土開源的基于字節(jié)碼注入的調(diào)用鏈分析,以及應(yīng)用監(jiān)控分析工具。特點(diǎn)是支持多種插件,UI功能較強(qiáng),接入端無(wú)代碼侵入。目前已加入Apache孵化器。
SpringCloud Sleuth 提供的分布式系統(tǒng)中鏈路追蹤解決方案。
注:我們可以使用SpringCloud Sleuth收集鏈路中的日志信息,交給zipkin來(lái)展示可視化界面
7.2 SpringCloud Sleuth 基本概念 & 快速開始
基本概念
SpringCloud Sleuth主要功能就是在分布式系統(tǒng)中提供追蹤解決方案。它大量借用了Google Dapper的設(shè)計(jì),先來(lái)了解一下Sleuth中的術(shù)語(yǔ)和相關(guān)概念。
Trace
由一組Trace Id相同的Span串聯(lián)形成一個(gè)樹狀結(jié)構(gòu)。為了實(shí)現(xiàn)請(qǐng)求跟蹤,當(dāng)請(qǐng)求到達(dá)分布式系統(tǒng)的入口端點(diǎn)時(shí),只需要服務(wù)跟蹤框架為該請(qǐng)求創(chuàng)建一個(gè)唯一的標(biāo)識(shí)(即TraceId),同時(shí)在分布式系統(tǒng)內(nèi)部流轉(zhuǎn)的時(shí)候,框架始終保持傳遞該唯一值,直到整個(gè)請(qǐng)求的返回。那么我們就可以使用該唯一標(biāo)識(shí)將所有的請(qǐng)求串聯(lián)起來(lái),形成一條完整的請(qǐng)求鏈路。
Span
代表了一組基本的工作單元。為了統(tǒng)計(jì)各處理單元的延遲,當(dāng)請(qǐng)求到達(dá)各個(gè)服務(wù)組件的時(shí) 候,也通過(guò)一個(gè)唯一標(biāo)識(shí)(SpanId)來(lái)標(biāo)記它的開始、具體過(guò)程和結(jié)束。通過(guò)SpanId的開始和結(jié)束時(shí)間戳,就能統(tǒng)計(jì)該span的調(diào)用時(shí)間,除此之外,我們還可以獲取如事件的名稱。請(qǐng)求信息等元數(shù)據(jù)。
Annotation
用它記錄一段時(shí)間內(nèi)的事件,內(nèi)部使用的重要注釋:
- cs(Client Send)客戶端發(fā)出請(qǐng)求,開始一個(gè)請(qǐng)求的生命
- sr(Server Received)服務(wù)端接受到請(qǐng)求開始進(jìn)行處理, sr-cs = 網(wǎng)絡(luò)延遲(服務(wù)調(diào)用的時(shí)間)
- ss(Server Send)服務(wù)端處理完畢準(zhǔn)備發(fā)送到客戶端,ss - sr = 服務(wù)器上的請(qǐng)求處理時(shí)間
- cr(Client Reveived)客戶端接受到服務(wù)端的響應(yīng),請(qǐng)求結(jié)束。 cr - sr = 請(qǐng)求的總時(shí)間
QuickStart
1、在api-gateway、shop-order、shop-product微服務(wù)中引入sleuth依賴
<!--鏈路追蹤-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
2、啟動(dòng)3個(gè)微服務(wù),訪問(wèn) http://127.0.0.1:7000/order-serv/order/prod/2 查看日志
微服務(wù)名稱, traceId, spanid,是否將鏈路的追蹤結(jié)果輸出到第三方平臺(tái)
[api-gateway,3977125f73391553,3977125f73391553,false]
[service-order,3977125f73391553,57547b5bf71f8242,false] [service-product,3977125f73391553,449f5b3f3ef8d5c5,false]
通過(guò)TraceId,通過(guò)查看日志,我們可以將調(diào)用鏈路串起來(lái)。
但查看日志文件并不是一個(gè)很好的方法,當(dāng)微服務(wù)越來(lái)越多日志文件也會(huì)越來(lái)越多,通過(guò)Zipkin可以將日志聚合,并進(jìn)行可視化展示和全文檢索。
7.3 集成 ZipKin
Zipkin 是 Twitter 的一個(gè)開源項(xiàng)目,它基于Google Dapper實(shí)現(xiàn),它致力于收集服務(wù)的定時(shí)數(shù)據(jù),以解決微服務(wù)架構(gòu)中的延遲問(wèn)題,包括數(shù)據(jù)的收集、存儲(chǔ)、查找和展現(xiàn)。
我們可以使用它來(lái)收集各個(gè)服務(wù)器上請(qǐng)求鏈路的跟蹤數(shù)據(jù),并通過(guò)它提供的REST API接口來(lái)輔助我們查詢跟蹤數(shù)據(jù)以實(shí)現(xiàn)對(duì)分布式系統(tǒng)的監(jiān)控程序,從而及時(shí)地發(fā)現(xiàn)系統(tǒng)中出現(xiàn)的延遲升高問(wèn)題并找出系統(tǒng)性能瓶頸的根源。
除了面向開發(fā)的 API 接口之外,它也提供了方便的UI組件來(lái)幫助我們直觀的搜索跟蹤信息和分析請(qǐng) 求鏈路明細(xì),比如:可以查詢某段時(shí)間內(nèi)各用戶請(qǐng)求的處理時(shí)間等。
Zipkin 提供了可插拔數(shù)據(jù)存儲(chǔ)方式:In-Memory、MySql、Cassandra 以及 Elasticsearch。

上圖展示了 Zipkin 的基礎(chǔ)架構(gòu),它主要由 4 個(gè)核心組件構(gòu)成:
- Collector:收集器組件,它主要用于處理從外部系統(tǒng)發(fā)送過(guò)來(lái)的跟蹤信息,將這些信息轉(zhuǎn)換為Zipkin內(nèi)部處理的 Span 格式,以支持后續(xù)的存儲(chǔ)、分析、展示等功能。
- Storage:存儲(chǔ)組件,它主要對(duì)處理收集器接收到的跟蹤信息,默認(rèn)會(huì)將這些信息存儲(chǔ)在內(nèi)存中,我們也可以修改此存儲(chǔ)策略,通過(guò)使用其他存儲(chǔ)組件將跟蹤信息存儲(chǔ)到數(shù)據(jù)庫(kù)中。
- RESTful API:API 組件,它主要用來(lái)提供外部訪問(wèn)接口。比如給客戶端展示跟蹤信息,或是外接系統(tǒng)訪問(wèn)以實(shí)現(xiàn)監(jiān)控等。
- Web UI:UI 組件, 基于API組件實(shí)現(xiàn)的上層應(yīng)用。通過(guò)UI組件用戶可以方便而有直觀地查詢和分 析跟蹤信息。
Zipkin分為兩端,一個(gè)是 Zipkin服務(wù)端,一個(gè)是 Zipkin客戶端,客戶端也就是微服務(wù)的應(yīng)用??蛻舳藭?huì)配置服務(wù)端的 URL 地址,一旦發(fā)生服務(wù)間的調(diào)用的時(shí)候,會(huì)被配置在微服務(wù)里面的 Sleuth 的監(jiān)聽(tīng)器監(jiān)聽(tīng),并生成相應(yīng)的 Trace 和 Span 信息發(fā)送給服務(wù)端。
7.3.1 安裝 ZipKin 服務(wù)端
jar包在git中有
java -jar zipkin-server-2.12.9-exec.jar
訪問(wèn) http://127.0.0.1:9411
7.3.2 客戶端集成Zipkin
1、在3個(gè)微服務(wù)中添加依賴
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
2、在3個(gè)微服務(wù)中添加配置
spring:
zipkin:
base-url: http://127.0.0.1:9411/ #zipkin server的請(qǐng)求地址
discoveryClientEnabled: false #讓nacos把它當(dāng)成一個(gè)URL,而不要當(dāng)做服務(wù)名
sleuth:
sampler:
probability: 1.0 #采樣的百分比
3、重啟3個(gè)微服務(wù),訪問(wèn) http://127.0.0.1:7000/order-serv/order/prod/2
4、進(jìn)入zipkin,查看

7.4 持久化
Zipkin Server默認(rèn)會(huì)將追蹤數(shù)據(jù)信息保存到內(nèi)存,但這種方式不適合生產(chǎn)環(huán)境。Zipkin支持將追蹤數(shù)據(jù)持久化到mysql數(shù)據(jù)庫(kù)或elasticsearch中。
7.4.1 持久化到MySQL中
1、創(chuàng)建MySQL表
CREATE TABLE IF NOT EXISTS zipkin_spans (
`trace_id_high` BIGINT NOT NULL DEFAULT 0 COMMENT 'If non zero, this means the trace uses 128 bit traceIds instead of 64 bit',
`trace_id` BIGINT NOT NULL,
`id` BIGINT NOT NULL,
`name` VARCHAR(255) NOT NULL,
`parent_id` BIGINT,
`debug` BIT(1),
`start_ts` BIGINT COMMENT 'Span.timestamp(): epoch micros used for endTs query and to implement TTL',
`duration` BIGINT COMMENT 'Span.duration(): micros used for minDuration and maxDuration query',
PRIMARY KEY (`trace_id_high`, `trace_id`, `id`)
) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8 COLLATE utf8_general_ci;
ALTER TABLE zipkin_spans ADD INDEX(`trace_id_high`, `trace_id`) COMMENT 'for getTracesByIds';
ALTER TABLE zipkin_spans ADD INDEX(`name`) COMMENT 'for getTraces and getSpanNames';
ALTER TABLE zipkin_spans ADD INDEX(`start_ts`) COMMENT 'for getTraces ordering and range';
CREATE TABLE IF NOT EXISTS zipkin_annotations (
`trace_id_high` BIGINT NOT NULL DEFAULT 0 COMMENT 'If non zero, this means the trace uses 128 bit traceIds instead of 64 bit',
`trace_id` BIGINT NOT NULL COMMENT 'coincides with zipkin_spans.trace_id',
`span_id` BIGINT NOT NULL COMMENT 'coincides with zipkin_spans.id',
`a_key` VARCHAR(255) NOT NULL COMMENT 'BinaryAnnotation.key or Annotation.value if type == -1',
`a_value` BLOB COMMENT 'BinaryAnnotation.value(), which must be smaller than 64KB',
`a_type` INT NOT NULL COMMENT 'BinaryAnnotation.type() or -1 if Annotation',
`a_timestamp` BIGINT COMMENT 'Used to implement TTL; Annotation.timestamp or zipkin_spans.timestamp',
`endpoint_ipv4` INT COMMENT 'Null when Binary/Annotation.endpoint is null',
`endpoint_ipv6` BINARY(16) COMMENT 'Null when Binary/Annotation.endpoint is null, or no IPv6 address',
`endpoint_port` SMALLINT COMMENT 'Null when Binary/Annotation.endpoint is null',
`endpoint_service_name` VARCHAR(255) COMMENT 'Null when Binary/Annotation.endpoint is null'
) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8 COLLATE utf8_general_ci;
ALTER TABLE zipkin_annotations ADD UNIQUE KEY(`trace_id_high`, `trace_id`, `span_id`, `a_key`, `a_timestamp`) COMMENT 'Ignore insert on duplicate';
ALTER TABLE zipkin_annotations ADD INDEX(`trace_id_high`, `trace_id`, `span_id`) COMMENT 'for joining with zipkin_spans';
ALTER TABLE zipkin_annotations ADD INDEX(`trace_id_high`, `trace_id`) COMMENT 'for getTraces/ByIds';
ALTER TABLE zipkin_annotations ADD INDEX(`endpoint_service_name`) COMMENT 'for getTraces and getServiceNames';
ALTER TABLE zipkin_annotations ADD INDEX(`a_type`) COMMENT 'for getTraces and autocomplete values';
ALTER TABLE zipkin_annotations ADD INDEX(`a_key`) COMMENT 'for getTraces and autocomplete values';
ALTER TABLE zipkin_annotations ADD INDEX(`trace_id`, `span_id`, `a_key`) COMMENT 'for dependencies job';
CREATE TABLE IF NOT EXISTS zipkin_dependencies (
`day` DATE NOT NULL,
`parent` VARCHAR(255) NOT NULL,
`child` VARCHAR(255) NOT NULL,
`call_count` BIGINT,
`error_count` BIGINT,
PRIMARY KEY (`day`, `parent`, `child`)
) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8 COLLATE utf8_general_ci;
2、刪除之前的zipkin容器,重新執(zhí)行下面代碼
java -jar zipkin-server-2.12.9-exec.jar --STORAGE_TYPE=mysql --MYSQL_HOST=127.0.0.1 --MYSQL_TCP_PORT=3306 --MYSQL_DB=zipkin --MYSQL_USER=root --MYSQL_PASS=5456
7.4.2 持久化到elasticsearch中
java -jar zipkin-server-2.12.9-exec.jar --STORAGE_TYPE=elasticsearch --ES-HOST=localhost:9200
八、服務(wù)配置中心 Nacos Config
微服務(wù)架構(gòu)下關(guān)于配置文件存在的一些問(wèn)題:
- 配置文件相對(duì)分散。在一個(gè)微服務(wù)架構(gòu)下,配置文件會(huì)隨著微服務(wù)的增多變的越來(lái)越多,而且分散在各個(gè)微服務(wù)中,不好統(tǒng)一配置和管理。
- 配置文件無(wú)法區(qū)分環(huán)。微服務(wù)項(xiàng)目可能會(huì)有多個(gè)環(huán)境,例如:測(cè)試環(huán)境、預(yù)發(fā)布環(huán)境、生產(chǎn)環(huán)境。每一個(gè)環(huán)境所使用的配置理論上都是不同的,一旦需要修改,就需要我們?nèi)ジ鱾€(gè)微服務(wù)下手動(dòng)維護(hù),這比較困難。
- 配置文件無(wú)法實(shí)時(shí)更新。我們修改了配置文件之后,必須重新啟動(dòng)微服務(wù)才能使配置生效,這對(duì)一個(gè)正在運(yùn)行的項(xiàng)目來(lái)說(shuō)是非常不友好的。
基于上面這些問(wèn)題,我們就需要配置中心的加入來(lái)解決這些問(wèn)題。
配置中心的思路是:
- 首先把項(xiàng)目中各種配置全部都放到一個(gè)集中的地方進(jìn)行統(tǒng)一管理,并提供一套標(biāo)準(zhǔn)的接口。
- 當(dāng)各個(gè)服務(wù)需要獲取配置的時(shí)候,就來(lái)配置中心的接口拉取自己的配置。
- 當(dāng)配置中心中的各種參數(shù)有更新的時(shí)候,也能通知到各個(gè)服務(wù)實(shí)時(shí)的過(guò)來(lái)同步最新的信息,使之動(dòng)態(tài)更新。
當(dāng)加入了服務(wù)配置中心之后,我們的系統(tǒng)架構(gòu)圖會(huì)變成下面這樣:

8.1 常見(jiàn)的服務(wù)配置中心
Apollo是由攜程開源的分布式配置中心。特點(diǎn)有很多,比如:配置更新之后可以實(shí)時(shí)生效,支持灰度發(fā)布功能,并且能對(duì)所有的配置進(jìn)行版本管理、操作審計(jì)等功能,提供開放平臺(tái)API。并且資料也寫的很詳細(xì)。
Disconf是由百度開源的分布式配置中心。它是基于Zookeeper來(lái)實(shí)現(xiàn)配置變更后實(shí)時(shí)通知和生效的。
SpringCloud Config是Spring Cloud中帶的配置中心組件。它和Spring是無(wú)縫集成,使用起來(lái)非常方便,并且它的配置存儲(chǔ)支持Git。不過(guò)它沒(méi)有可視化的操作界面,配置的生效也不是實(shí)時(shí)的,需要重啟或去刷新。
Nacos是SpingCloud alibaba技術(shù)棧中的一個(gè)組件,前面我們已經(jīng)使用它做過(guò)服務(wù)注冊(cè)中心。其實(shí)它也集成了服務(wù)配置的功能,我們可以直接使用它作為服務(wù)配置中心。
8.2 Nacos Config 快速開始
1、在商品微服務(wù)中引入nacos config的依賴
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
2、新建bootstrap.yml
spring:
application:
name: service-product
cloud:
nacos:
config:
server-addr: 127.0.0.1:8848 #nacos中心地址
file-extension: yaml # 配置文件格式
profiles:
active: dev # 環(huán)境標(biāo)識(shí)
注:配置文件優(yōu)先級(jí)(由高到低):bootstrap.properties -> bootstrap.yml -> application.properties -> application.yml
3、將application.yml中的配置刪除,在nacos中添加配置


server:
port: 8081
spring:
zipkin:
base-url: http://127.0.0.1:9411 #zipkin server的請(qǐng)求地址
discoveryClientEnabled: false #讓nacos把它當(dāng)成一個(gè)URL,而不要當(dāng)做服務(wù)名
sleuth:
sampler:
probability: 1.0 #采樣的百分比
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/shop?characterEncoding=UTF-8
username: root
password: 5456
jpa:
properties:
hibernate:
hbm2ddl:
auto: update
dialect: org.hibernate.dialect.MySQL5InnoDBDialect
cloud:
nacos:
discovery:
server-addr: localhost:8848
4、重啟商品微服務(wù)
8.3 配置動(dòng)態(tài)刷新
在nacos中的配置中,新增下面配置:
config:
appName: product
方式一
@RestController
public class NacosConfigController {
@Autowired
private ConfigurableApplicationContext applicationContext;
@RequestMapping("/test-config1")
public String testConfig1() {
return applicationContext.getEnvironment().getProperty("config.appName");
}
}
方式二
@RestController
@RefreshScope//動(dòng)態(tài)刷新的注解
public class NacosConfigController {
@Autowired
private ConfigurableApplicationContext applicationContext;
@Value("${config.appName}")
private String appName;
@RequestMapping("/test-config1")
public String testConfig1() {
return applicationContext.getEnvironment().getProperty("config.appName");
}
@RequestMapping("/test-config2")
public String testConfig2() {
return appName;
}
}
注:類似數(shù)據(jù)庫(kù)連接的配置修改后是不會(huì)動(dòng)態(tài)更新的
8.4 配置共享
同一個(gè)微服務(wù)的不同環(huán)境之間共享配置
當(dāng)配置越來(lái)越多的時(shí)候,我們就發(fā)現(xiàn)有很多配置是重復(fù)的,這時(shí)候就考慮可不可以將公共配置文件提取出來(lái),然后實(shí)現(xiàn)共享呢?當(dāng)然是可以的。接下來(lái)我們就來(lái)探討如何實(shí)現(xiàn)這一功能。
如果想在同一個(gè)微服務(wù)的不同環(huán)境之間實(shí)現(xiàn)配置共享,其實(shí)很簡(jiǎn)單。只需要提取一個(gè)以 spring.application.name 命名的配置文件,然后將其所有環(huán)境的公共配置放在里面即可。
1、新建一個(gè)名為service-product.yaml配置存放商品微服務(wù)的公共配置

2、刪除dev配置文件的這部分
3、重啟系統(tǒng)
不同微服務(wù)中間共享配置
不同為服務(wù)之間實(shí)現(xiàn)配置共享的原理類似于文件引入,就是定義一個(gè)公共配置,然后在當(dāng)前配置中引入。
1、在nacos中定義一個(gè)DataID為all-service.yaml(這個(gè)名字可以隨便寫)的配置,用于所有微服務(wù)共享

spring:
zipkin:
base-url: http://127.0.0.1:9411 #zipkin server的請(qǐng)求地址
discoveryClientEnabled: false #讓nacos把它當(dāng)成一個(gè)URL,而不要當(dāng)做服務(wù)名
sleuth:
sampler:
probability: 1.0 #采樣的百分比
2、刪除商品微服務(wù)-dev的這部分配置
3、修改bootstrap.yml
spring:
application:
name: service-product
cloud:
nacos:
config:
server-addr: 127.0.0.1:8848 #nacos中心地址
file-extension: yaml # 配置文件格式
shared-dataids: all-service.yaml # 配置要引入的配置
refreshable-dataids: all-service.yaml # 配置要實(shí)現(xiàn)動(dòng)態(tài)配置刷新的配置
profiles:
active: dev # 環(huán)境標(biāo)識(shí)
4、重啟微服務(wù)
8.5 nacos-config 的幾個(gè)概念
命名空間(Namespace) 命名空間可用于進(jìn)行不同環(huán)境的配置隔離。一般一個(gè)環(huán)境劃分到一個(gè)命名空間
配置分組(Group) 配置分組用于將不同的服務(wù)可以歸類到同一分組。一般將一個(gè)項(xiàng)目的配置分到一組
配置集(Data ID) 在系統(tǒng)中,一個(gè)配置文件通常就是一個(gè)配置集。一般一個(gè)微服務(wù)的配置就是一個(gè)配置集

九、分布式事務(wù) Seata
概念見(jiàn) http://www.itdecent.cn/p/19492cfc71fb
9.1 Seata簡(jiǎn)介
2019 年 1 月,阿里巴巴中間件團(tuán)隊(duì)發(fā)起了開源項(xiàng)目 Fescar(Fast & EaSy Commit And Rollback),其愿景是讓分布式事務(wù)的使用像本地事務(wù)的使用一樣,簡(jiǎn)單和高效,并逐步解決開發(fā)者們遇到的分布式事務(wù)方面的所有難題。后來(lái)更名為 Seata,意為:Simple Extensible Autonomous Transaction Architecture,是一套分布式事務(wù)解決方案。
Seata的設(shè)計(jì)目標(biāo)是對(duì)業(yè)務(wù)無(wú)侵入,因此從業(yè)務(wù)無(wú)侵入的2PC方案著手,在傳統(tǒng)2PC的基礎(chǔ)上演進(jìn)。它把一個(gè)分布式事務(wù)理解成一個(gè)包含了若干分支事務(wù)的全局事務(wù)。全局事務(wù)的職責(zé)是協(xié)調(diào)其下管轄的分支事務(wù)達(dá)成一致,要么一起成功提交,要么一起失敗回滾。此外,通常分支事務(wù)本身就是一個(gè)關(guān)系數(shù)據(jù)庫(kù)的本地事務(wù)。

Seata主要由三個(gè)重要組件組成:
- TC:Transaction Coordinator 事務(wù)協(xié)調(diào)器,管理全局的分支事務(wù)的狀態(tài),用于全局性事務(wù)的提交和回滾。
- TM:Transaction Manager 事務(wù)管理器,用于開啟、提交或者回滾全局事務(wù)。
- RM:Resource Manager 資源管理器,用于分支事務(wù)上的資源管理,向TC注冊(cè)分支事務(wù),上報(bào)分支事務(wù)的狀態(tài),接受TC的命令來(lái)提交或者回滾分支事務(wù)。

A服務(wù)的TM向TC申請(qǐng)開啟一個(gè)全局事務(wù),TC就會(huì)創(chuàng)建一個(gè)全局事務(wù)并返回一個(gè)唯一的XID
A服務(wù)的RM向TC注冊(cè)分支事務(wù),并及其納入XID對(duì)應(yīng)全局事務(wù)的管轄
A服務(wù)執(zhí)行分支事務(wù),向數(shù)據(jù)庫(kù)做操作
A服務(wù)開始遠(yuǎn)程調(diào)用B服務(wù),此時(shí)XID會(huì)在微服務(wù)的調(diào)用鏈上傳播
B服務(wù)的RM向TC注冊(cè)分支事務(wù),并將其納入XID對(duì)應(yīng)的全局事務(wù)的管轄
B服務(wù)執(zhí)行分支事務(wù),向數(shù)據(jù)庫(kù)做操作
全局事務(wù)調(diào)用鏈處理完畢,TM根據(jù)有無(wú)異常向TC發(fā)起全局事務(wù)的提交或者回滾
TC協(xié)調(diào)其管轄之下的所有分支事務(wù),決定是否回滾
Seata實(shí)現(xiàn)2PC與傳統(tǒng)2PC的差別
架構(gòu)層次方面,傳統(tǒng)2PC方案的 RM 實(shí)際上是在數(shù)據(jù)庫(kù)層,RM本質(zhì)上就是數(shù)據(jù)庫(kù)自身,通過(guò)XA協(xié)議實(shí)現(xiàn),而 Seata 的RM是以jar包的形式作為中間件層部署在應(yīng)用程序這一側(cè)的。
兩階段提交方面,傳統(tǒng)2PC無(wú)論第二階段的決議是commit還是rollback,事務(wù)性資源的鎖都要保持到Phase2完成才釋放。而Seata的做法是在Phase1 就將本地事務(wù)提交,這樣就可以省去Phase2 持鎖的時(shí)間,整體提高效率。
?這樣會(huì)不會(huì)出現(xiàn)線程安全問(wèn)題啊
9.2 快速開始
模擬電商中的下單和扣庫(kù)存的過(guò)程,我們通過(guò)訂單微服務(wù)執(zhí)行下單操作,然后由訂單微服務(wù)調(diào)用商品微服務(wù)扣除庫(kù)存。
1、新建OrderController3
@RestController
@Slf4j
public class OrderController3 {
@Autowired
private OrderDao orderDao;
@Autowired
private ProductClient productClient;
//下單--fegin
@RequestMapping("/order3/prod/{pid}")
public Order order(@PathVariable("pid") Integer pid) {
log.info("接收到{}號(hào)商品的下單請(qǐng)求,接下來(lái)調(diào)用商品微服務(wù)查詢此商品信息", pid);
//調(diào)用商品微服務(wù),查詢商品信息
Product product = productClient.findByPid(pid);
if (product.getPid() == -100) {
Order order = new Order();
order.setOid(-100L);
order.setPname("下單失敗");
return order;
}
log.info("查詢到{}號(hào)商品的信息,內(nèi)容是:{}", pid, JSON.toJSONString(product));
//下單(創(chuàng)建訂單)
Order order = new Order();
order.setUid(1);
order.setUsername("測(cè)試用戶");
order.setPid(pid);
order.setPname(product.getPname());
order.setPprice(product.getPprice());
order.setNumber(1);
orderDao.save(order);
log.info("創(chuàng)建訂單成功,訂單信息為{}", JSON.toJSONString(order));
//扣庫(kù)存
productClient.reduceInventory(pid, order.getNumber());
return order;
}
}
2、在ProductClient中添加扣減庫(kù)存
//value用于指定調(diào)用nacos下哪個(gè)微服務(wù)
@FeignClient(value = "service-product"/*, fallbackFactory = ProductServiceFallbackFactory.class*/)
public interface ProductClient {
//@FeignClient的value + @RequestMapping的value值 其實(shí)就是完成的請(qǐng)求地址 "http://service-product/product/" + pid
//指定請(qǐng)求的URI部分
@RequestMapping("/product/{pid}")
Product findByPid(@PathVariable("pid") Integer pid);
// 扣減庫(kù)存
@RequestMapping("/product/reduceInventory")
void reduceInventory(@RequestParam("pid") Integer pid, @RequestParam("num") Integer num);
}
3、在商品微服務(wù)中添加扣減庫(kù)存方法
@Transactional
@RequestMapping("/product/reduceInventory")
public void reduceInventory(@RequestParam("pid") Integer pid, @RequestParam("num") Integer num) {
Product product = productDao.findById(pid).get();
product.setStock(product.getStock() - num);
productDao.save(product);
}
4、啟動(dòng)項(xiàng)目測(cè)試一下 http://127.0.0.1:8091/order3/prod/1
5、下載Seata服務(wù)端,修改conf/registry.conf
這是注冊(cè)中心和配置中心的配置
registry {
type = "nacos"
nacos {
serverAddr = "localhost"
namespace = "public"
cluster = "default"
}
}
config {
type = "nacos"
nacos {
serverAddr = "localhost"
namespace = "public"
cluster = "default"
}
}
6、刪除nacos中的所有配置,將商品微服務(wù)的配置改回之前的
7、打開nacos-config.txt,添加
service.vgroup_mapping.service-product=default
service.vgroup_mapping.service-order=default
這里的語(yǔ)法為:service.vgroup_mapping.{your-service-gruop} 為自己定義的服務(wù)組名稱, 這里需要我們?cè)诔绦虻呐渲梦募信渲谩?/p>
7、初始化seata在nacos的配置
./nacos-config.sh 127.0.0.1
執(zhí)行成功后可以打開Nacos的控制臺(tái),在配置列表中,可以看到初始化了很多Group為SEATA_GROUP的配置。

8、啟動(dòng)seata服務(wù)
cd ../bin
./seata-server.sh -p 9000 -m file
啟動(dòng)后在 Nacos 的服務(wù)列表下面可以看到一個(gè)名為 serverAddr 的服務(wù)。

9、在我們的數(shù)據(jù)庫(kù)(每個(gè)業(yè)務(wù)庫(kù)都要有)中加入一張undo_log表,這是Seata記錄事務(wù)日志要用到的表
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;
10、在商品和order微服務(wù)中添加以下依賴
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
11、在商品和order微服務(wù)中添加DataSourceProxyConfig
Seata 是通過(guò)代理數(shù)據(jù)源實(shí)現(xiàn)事務(wù)分支的,所以需要配置 io.seata.rm.datasource.DataSourceProxy的Bean,且是@Primary默認(rèn)的數(shù)據(jù)源,否則事務(wù)不會(huì)回滾,無(wú)法實(shí)現(xiàn)分布式事務(wù)
@Configuration
public class DataSourceProxyConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DruidDataSource druidDataSource() {
return new DruidDataSource();
}
@Primary
@Bean
public DataSourceProxy dataSource(DruidDataSource druidDataSource) {
return new DataSourceProxy(druidDataSource);
}
}
12、在resources下添加Seata的配置文件 registry.conf
和上面的那個(gè)一樣
registry {
type = "nacos"
nacos {
serverAddr = "localhost"
namespace = "public"
cluster = "default"
}
}
config {
type = "nacos"
nacos {
serverAddr = "localhost"
namespace = "public"
cluster = "default"
}
}
13、修改bootstarp.yml,添加如下配置
spring:
cloud:
nacos:
config:
server-addr: localhost:8848 # nacos的服務(wù)端地址
namespace: public
group: SEATA_GROUP
alibaba:
seata:
tx-service-group: service-order # 與第7步是對(duì)應(yīng)的
14、開啟全局事務(wù)
@GlobalTransactional
@RequestMapping("/order3/prod/{pid}")
public Order order(@PathVariable("pid") Integer pid) {
15、添加異常
@Transactional
@RequestMapping("/product/reduceInventory")
public void reduceInventory(@RequestParam("pid") Integer pid, @RequestParam("num") Integer num) {
Product product = productDao.findById(pid).get();
product.setStock(product.getStock() - num);
productDao.save(product);
int i = 1 / 0;
}
16、測(cè)試
注意:alibaba的版本一定要切換到2.1.0.RELEASE,見(jiàn)5.5.1。
9.3 seata運(yùn)行流程分析

1、每個(gè)RM使用DataSourceProxy連接數(shù)據(jù)庫(kù),其目的是使用ConnectionProxy,使用數(shù)據(jù)源和數(shù)據(jù)連接代理的目的就是在第一階段將undo_log和業(yè)務(wù)數(shù)據(jù)放在一個(gè)本地事務(wù)提交,這樣就保存了只要有業(yè)務(wù)操作就一定有undo_log。
2、在第一階段undo_log中存放了數(shù)據(jù)修改前和修改后的值,為事務(wù)回滾作好準(zhǔn)備,所以第一階段完成就已經(jīng)將分支事務(wù)提交,也就釋放了鎖資源。
3、TM開啟全局事務(wù)開始,將XID全局事務(wù)id放在事務(wù)上下文中,通過(guò)feign調(diào)用也將XID傳入下游分支事務(wù),每個(gè)分支事務(wù)將自己的Branch ID分支事務(wù)ID與XID關(guān)聯(lián)。
4、第二階段全局事務(wù)提交,TC會(huì)通知各各分支參與者提交分支事務(wù),在第一階段就已經(jīng)提交了分支事務(wù),這里各各參與者只需要?jiǎng)h除undo_log即可,并且可以異步執(zhí)行,第二階段很快可以完成。
5、第二階段全局事務(wù)回滾,TC會(huì)通知各各分支參與者回滾分支事務(wù),通過(guò) XID 和 Branch ID 找到相應(yīng)的回滾日志,通過(guò)回滾日志生成反向的 SQL 并執(zhí)行,以完成分支事務(wù)回滾到之前的狀態(tài),如果回滾失 敗則會(huì)重試回滾操作。