目錄
- 前言
- 開發環境
- 原始碼地址
- 創建工程
- 服務注冊中心(Eureka)
- Eureka Server
- Eureka Client
- 注冊中心高可用
- 小結
- 負載均衡(Ribbon)
- RestTemplate呼叫
- 負載均衡呼叫
- 應用名稱呼叫
- 小結
- 宣告式服務呼叫(Feign)
- 服務端實作
- 客戶端實作
- 小結
- 統一配置中心(Config)
- Config Server
- 向服務中心注冊
- 服務提供端改造
- 配置動態重繪
- 配置 Webhook
- 小結
- 異步訊息(Stream)
- 應用場景
- 當前專案場景
- stream-rabbit集成
- 改造Order和Product專案
- 小結
- 微服務網關(Zuul)
- 作業原理
- 添加網關
- 自定義路由
- Cookie與頭資訊
- 動態路由
- 自定義Filter
- 限流
- 小結
- 服務容錯(Hystrix)
- 設計原則
- 如何實作
- 觸發降級
- 超時設定
- 熔斷機制
- 可視化組件
- 小結
- 服務追蹤(Sleuth)
- OpenTracing
- 鏈路追蹤
- Zipkin
- 小結
- 容器化部署
- 安裝Docker
- 安裝Docker-Compose
- Eureka部署
- MySQL部署
- RabbitMQ
- OpenZipkin
- 編排鏡像
前言
開發環境
-
部署環境:阿里云ECS服務器
- 作業系統:CentOS 7.7 64位
- IDEA 版本:2019.3.1
- docker 版本:1.13.1
- docker-compose版本:1.25.0
- spring cloud 版本:Hoxton.SR1
- spring boot 版本:2.2.2.RELEASE
- mysql 版本:5.7
- redis 版本:5.0.7
- rabbitmq 版本:3.8.2-management
- zipkin 版本:2.19.2
-
埠映射資訊:
-
eureka1:8761 | eureka2:8762
-
config-server:8888
-
shopping-product:11100
-
shopping-order:11110
-
api-gateway:8080
-
open-api:8081
-
原始碼地址
https://github.com/lizzie2008/spring-cloud-app.git
創建工程
- 新建一個工程:選擇Spring Cloud Bootstrap,對應的Spring Boot 版本2.2.2,

- 專案生成后,看到對應的Spring版本的依賴沒有問題,

- 因為是父工程,我們將打包格式改成pom,并把src等無用的檔案洗掉,
<groupId>tech.lancelot</groupId>
<artifactId>spring-cloud-app</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-cloud-app</name>
<description>Demo project for Spring Cloud</description>
<packaging>pom</packaging>
服務注冊中心(Eureka)
Eureka Server
- 新建Module->選擇Eureka Server

-
因為Module作為子專案,我們改寫下對應的POM檔案,
<parent> <groupId>tech.lancelot</groupId> <artifactId>spring-cloud-app</artifactId> <version>0.0.1-SNAPSHOT</version> </parent> <groupId>tech.lancelot</groupId> <artifactId>eureka-server</artifactId> <version>0.0.1-SNAPSHOT</version> <name>eureka-server</name> <description>Registry Center</description> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId> </dependency> </dependencies> -
重新Build一下專案,能正常編譯,但是此時Eureka Server是不能正常啟動作業的,需要在application類增加
@EnableEurekaServer,
此時,我們再運行Eureka Server,發現可以正常啟動服務注冊服務器,服務埠8080,注冊地址:http://localhost:8761/eureka/,

- 打開瀏覽器,訪問8080埠,查看可視化管理界面,

- 當然,我們沒有做任何配置,并且控制臺一直報錯,這是因為默認情況下,本身也是需要獲取注冊資訊和注冊到注冊中心,而此時找不到對應服務器,我們可以修改組態檔,做相應的配置,調整服務埠為8761,重新啟動后,發現不再報錯,
eureka:
client:
fetch-registry: false #設定不從注冊中心獲取注冊資訊
register-with-eureka: false #設定自身不作為客戶端注冊到注冊中心
spring:
application:
name: eureka-server #應用名稱
server:
port: 8761 #應用服務埠
Eureka Client
- 我們再建一個Module工程,作為服務客戶端,向Eureka Server服務中心注冊,

-
同樣,我們修改POM檔案,依賴于父專案,注意這里需要引入
eureka-client和spring-boot-starter-web依賴,<parent> <groupId>tech.lancelot</groupId> <artifactId>spring-cloud-app</artifactId> <version>0.0.1-SNAPSHOT</version> </parent> <groupId>tech.lancelot</groupId> <artifactId>shopping-provider</artifactId> <version>0.0.1-SNAPSHOT</version> <name>shopping-provider</name> <description>shopping service provider</description> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> </dependencies> -
需要在application類增加
@EnableDiscoveryClient,同時修改組態檔,eureka: client: serviceUrl: defaultZone: http://localhost:8761/eureka/ #指定服務注冊地址 spring: application: name: shopping-provider #應用名稱 -
重啟Eureka Client,啟動后再次訪問Eureka Server管理界面,可以發現order-provider服務已注冊,

注冊中心高可用
-
之前我們的Eureka Server是單點服務,實際生產中,經常是多臺注冊中心,因此我們嘗試下配置2臺注冊中心,
啟動服務器實體1:
eureka: client: # fetch-registry: false #設定不從注冊中心獲取注冊資訊 # register-with-eureka: false #設定自身不作為客戶端注冊到注冊中心 defaultZone: http://localhost:8762/eureka/ #指定服務注冊地址 spring: application: name: eureka-server1 #應用名稱 server: port: 8761 #應用服務埠啟動服務器實體2:
eureka: client: # fetch-registry: false #設定不從注冊中心獲取注冊資訊 # register-with-eureka: false #設定自身不作為客戶端注冊到注冊中心 defaultZone: http://localhost:8761/eureka/ #指定服務注冊地址 spring: application: name: eureka-server2 #應用名稱 server: port: 8762 #應用服務埠 -
重啟2臺注冊中心,啟動后分別訪問2臺的管理界面,可以看到2臺注冊中心已經相互注冊,


小結
專案增加2個服務模塊,并向Eureka Server注冊:shopping-product(商品服務)、shopping-order(訂單服務),實作相應業務邏輯,這部分詳細實作不再闡述,
整體專案結構如下:
spring-cloud-app
? --eureka-server(服務注冊中心)
? --shopping-common(購物公共模塊)
? --shopping-product(商品服務模塊)
? --shopping-order(訂單服務模塊)
系統架構如圖,比較簡單,一個集群服務中心,目前有2個服務提供并注冊:

負載均衡(Ribbon)
Spring Cloud Ribbon 是一個客戶端的負載均衡器,它提供對大量的HTTP和TCP客戶端的訪問控制,
客戶端負載均衡即是當瀏覽器向后臺發出請求的時候,客戶端會向 Eureka Server 讀取注冊到服務器的可用服務資訊串列,然后根據設定的負載均衡策略(沒有設定即用默認的),抉擇出向哪臺服務器發送請求,
假設有以下業務場景,shopping-order模塊需要呼叫shopping-product提供的API介面,我們看如何實作,
RestTemplate呼叫
第一種方法使用構造RestTemplate,呼叫遠程API,這種方法url是寫死,如果啟動多臺shopping-product服務的話,那又該如何?
@Test
void getProductByRestTemplate() {
//1.第一種方法
RestTemplate restTemplate = new RestTemplate();
String response = restTemplate.getForObject("http://localhost:11100/api/products", String.class);
Assert.hasLength(response,"未獲取內容");
}
負載均衡呼叫
第二種方法:我們啟動2個shopping-product服務實體,分別是11100埠和9001埠,運行測驗發現,會根據loadBalancerClient負載均衡機制幫我們選擇一個服務地址,進行訪問呼叫,
@Autowired
private LoadBalancerClient loadBalancerClient;
@Test
void getProductByLoadBalance(){
//2.第二種方法,先獲取負載均衡的地址再呼叫API
ServiceInstance instance = loadBalancerClient.choose("shopping-product");
String url=String.format("http://%s:%s/api/products",instance.getHost(),instance.getPort());
RestTemplate restTemplate = new RestTemplate();
String response = restTemplate.getForObject(url, String.class);
log.info("port:"+instance.getPort()+response);
}
應用名稱呼叫
但這樣依舊很是麻煩,接下來看第三種方法,第三種方法屏蔽了API的具體url資訊,只用ServerId,并根據負載均衡規則,自動路由到對應的地址,
因為eureka包中已經添加了對Ribbon的依賴,我們可以增加斷點,除錯程式,發現進入RibbonLoadBalancerClient-->choose方法,回傳負載均衡策略選擇的ServiceInstance,
@Component
public class RestTemplateConfiguration {
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
@SpringBootTest
@Slf4j
class OrderServiceTest {
@Autowired
private RestTemplate restTemplate;
@Test
void getProductByServerId() {
String response = restTemplate.getForObject("http://shopping-product/api/products", String.class);
log.info(response);
}
}
當然,我們也可以指定應用服務的負載均衡策略:
shopping-order:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
小結
目前系統架構如圖,實作shopping-product和shopping-order集群化部署,呼叫方式通過客戶端負載均衡,來路由消費端的請求,

宣告式服務呼叫(Feign)
Feign是一個宣告式的Web Service客戶端,它的目的就是讓Web Service呼叫更加簡單,Feign提供了HTTP請求的模板,通過撰寫簡單的介面和插入注解,就可以定義好HTTP請求的引數、格式、地址等資訊,
而Feign則會完全代理HTTP請求,我們只需要像呼叫方法一樣呼叫它就可以完成服務請求及相關處理,Feign整合了Ribbon和Hystrix(關于Hystrix我們后面再講),可以讓我們不再需要顯式地使用這兩個組件,
總起來說,Feign具有如下特性:
- 可插拔的注解支持,包括Feign注解和JAX-RS注解;
- 支持可插拔的HTTP編碼器和解碼器;
- 支持Hystrix和它的Fallback;
- 支持Ribbon的負載均衡;
- 支持HTTP請求和回應的壓縮,
服務端實作
shopping-product服務提供端暴露API,
@GetMapping("/productInfos")
public List<ProductInfoOutput> findProductInfosByIds(@RequestParam(required = false) String productIds) throws Exception {
//如果傳入商品id引數
if (StringUtils.isNotEmpty(productIds)) {
List<String> ids = Arrays.asList(productIds.split(","));
List<ProductInfo> productInfos = productService.findProductInfosByIds(ids);
List<ProductInfoOutput> productInfoOutputs = ListUtils.copyProperties(productInfos, ProductInfoOutput.class);
return productInfoOutputs;
}else{
List<ProductInfo> productInfos = productService.findProductInfos();
List<ProductInfoOutput> productInfoOutputs = ListUtils.copyProperties(productInfos, ProductInfoOutput.class);
return productInfoOutputs;
}
}
客戶端實作
- 引入Feign
shopping-order模塊需要呼叫shopping-product介面,首先我們在服務呼叫端增加Maven依賴
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-feign</artifactId>
</dependency>
啟動類標注開啟Feign服務
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class ShoppingOrderApplication {
public static void main(String[] args) {
SpringApplication.run(ShoppingOrderApplication.class,args);
}
}
- 創建宣告式服務
/**
* 宣告式服務
*/
@FeignClient("shopping-product/api/v1")
public interface ProductClient {
@GetMapping("/productInfos")
List<ProductInfoOutput> findProductInfosByIds(@RequestParam(required = false) String productIds);
}
@FeignClient(“服務名稱”)映射服務呼叫,本質還是http請求,只不過Feign幫我們屏蔽了底層的請求路由,對開發者完全透明,使得呼叫遠程服務感覺跟呼叫本地服務一致的編碼體驗,
本地呼叫測驗,可以正常回傳介面資料,
@GetMapping("/orders/findProductInfosByIds")
public List<ProductInfoOutput> findProductInfosByIds(){
List<ProductInfoOutput> productInfoOutputs = productClient
.findProductInfosByIds("157875196366160022, 157875227953464068");
return productInfoOutputs;
}
小結
在實作負載均衡基礎上,封裝宣告式服務呼叫,實作shopping-order對shopping-product的透明呼叫,系統架構如圖如下,

統一配置中心(Config)
上個環境中,我們有2個服務提供者,首先看下各自的配置,可以發現很大一部分都是重復的,
如果微服務架構中沒有使用統一配置中心時,所存在的問題:
- 組態檔分散在各個專案里,不方便維護
- 配置內容安全與權限,實際開發中,開發人員是不知道線上環境的配置的
- 更新配置后,專案需要重啟
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/ #指定服務注冊地址
spring:
application:
name: shopping-order #應用名稱
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: 123456
url: jdbc:mysql://localhost:3306/spring_cloud_app?characterEncoding=utf-8&useSSL=false&serverTimezone=UTC
jpa:
show-sql: true
database-platform: org.hibernate.dialect.MySQLDialect
server:
port: 11110
對于一些簡單的專案來說,我們一般都是直接把相關配置放在單獨的組態檔中,以 properties 或者 yml 的格式出現,更省事兒的方式是直接放到 application.properties 或 application.yml 中,在集群部署情況下,我們嘗試來實作配置的集中管理,并支持配置的動態重繪,
Config Server
- 我們新建一個Module工程,統一配置中心,保存所以的配置資訊,
同樣,我們作為子專案,修改相關依賴,加入對spring-cloud-config-server依賴
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>tech.lancelot</groupId>
<artifactId>spring-cloud-app</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>config-server</artifactId>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- spring cloud config 服務端包 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
</dependency>
</dependencies>
- application.properties進行如下配置
spring:
application:
name: config-server # 應用名稱
cloud:
config:
server:
git:
uri: https://github.com/lizzie2008/Central-Configuration.git #組態檔所在倉庫
username: 'Github username'
password: 'Github password'
default-label: master #組態檔分支
search-paths: spring-cloud-app #組態檔所在根目錄
server:
port: 8888
- 在 Application 啟動類上增加相關注解
@EnableConfigServer
@EnableConfigServer
@SpringBootApplication
public class ConfigServerApplication {
public static void main(String[] args) {
SpringApplication.run(ConfigServerApplication.class, args);
}
}
- 啟動服務,接下來測驗一下,
Spring Cloud Config 有它的一套訪問規則,我們通過這套規則在瀏覽器上直接訪問就可以,
/{application}/{profile}[/{label}]
/{application}-{profile}.yml
/{label}/{application}-{profile}.yml
/{application}-{profile}.properties
/{label}/{application}-{profile}.properties
{application} 就是應用名稱,對應到組態檔上來,就是組態檔的名稱部分,例如我上面創建的組態檔,
{profile} 就是組態檔的版本,我們的專案有開發版本、測驗環境版本、生產環境版本,對應到組態檔上來就是以 application-{profile}.yml 加以區分,例如application-dev.yml、application-sit.yml、application-prod.yml,
{label} 表示 git 分支,默認是 master 分支,如果專案是以分支做區分也是可以的,那就可以通過不同的 label 來控制訪問不同的組態檔了,
我們在git專案中,新建spring-cloud-app/config-eureka-server.yml組態檔,然后訪問配置中心服務器,看看能正常獲取組態檔,

向服務中心注冊
config-server本身作為一個服務,也可以作為服務提供方,向服務中心注冊,其他的服務想要獲取組態檔,只需要通過服務名稱就會訪問,
- 引入Eureka Client依賴
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
- 啟動類上增加
@EnableDiscoveryClient注解
@EnableConfigServer
@EnableDiscoveryClient
@SpringBootApplication
public class ConfigServerApplication {
public static void main(String[] args) {
SpringApplication.run(ConfigServerApplication.class, args);
}
}
- 組態檔中增加eureka注冊,
eureka:
client:
serviceUrl:
defaultZone: http://eureka1:8761/eureka/,http://eureka2:8762/eureka/ #指定服務注冊地址
- 啟動eureka-server,看看config-server是否注冊成功,

服務提供端改造
- shopping-product專案中,把原先的application.yml檔案重命名為bootstrap.yml,并配置Eureka Server地址、應用名稱、Config的實體名稱,服務啟動后,會鏈接Eureka Server服務器,根據Config的實體名稱找到對應的Config服務器,并根據實體名稱(可以增加profile屬性)來匹配組態檔,
eureka:
client:
serviceUrl:
defaultZone: http://eureka1:8761/eureka/,http://eureka2:8762/eureka/ #指定服務注冊地址
spring:
application:
name: shopping-product #應用名稱
cloud:
config:
discovery:
enabled: true
service-id: config-server
- 之前服務端其余的配置,填寫在github配置專案shopping-product.yml檔案中
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: 123456
url: jdbc:mysql://localhost:3306/spring_cloud_app?characterEncoding=utf-8&useSSL=false&serverTimezone=UTC
jpa:
show-sql: true
database-platform: org.hibernate.dialect.MySQLDialect
server:
port: 11100
- 同樣,shopping-order專案也如此改造,最后我們啟動所有的服務,看是否都能正常啟動,

配置動態重繪
- 首先,在
shopping-product.yml增加一個配置屬性來進行測驗
env: dev
- 新建一個測驗controller來系結這個配置屬性,并提供api來回傳屬性的值
@RestController
@RefreshScope
@RequestMapping("api/env")
public class EnvController {
@Value("${env}")
private String env;
@RequestMapping
public String printEnv() {
return env;
}
}
-
訪問http://localhost:11100/api/env,回傳當前的值dev,
Spring Cloud Config 在專案啟動時加載配置內容這一機制,但是如果我們修改組態檔內容后,不會自動重繪,例如我們上面的專案,當服務已經啟動的時候,去修改 github 上的組態檔內容,這時候,再次重繪頁面,對不起,還是舊的配置內容,新內容不會主動重繪過來,那應該怎么去觸發配置資訊的動態重繪呢?
-
它提供了一個重繪機制,但是需要我們主動觸發,那就是 @RefreshScope 注解并結合 actuator ,注意要引入 spring-boot-starter-actuator 包,
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
- EnvController上增加
@RefreshScope注解 - 發送 POST 請求到 http://localhost:11100/actuator/refresh 這個介面,默認沒有開放endpoint的權限,所以這塊我們首先配置開放權限
management:
endpoints:
web:
exposure:
include: "*"
- 這時呼叫介面結束后,我們看到介面回傳訊息,表明env這個屬性值已經重繪
[
"config.client.version",
"env"
]
- 再次訪問http://localhost:11100/api/env,回傳當前的值就是修改后的值test,證明配置屬性的值已經動態重繪,我們的程式也不用再次啟動,
配置 Webhook
每次改了配置后,就用 postman 訪問一下 refresh 介面,還是不夠方便, github 提供了一種 webhook 的方式,當有代碼變更的時候,會呼叫我們設定的地址,來實作我們想達到的目的,
- 進入 github 倉庫配置頁面,選擇 Webhooks ,并點擊 add webhook;

-
填上回呼的地址
也就是上面提到的 actuator/refresh 這個地址,但是必須保證這個地址是可以被 github 訪問到的,這樣每當github上修改了組態檔,就自動通知對應的hook地址自動重繪,
小結
整體專案結構如下:
spring-cloud-app
? --config-server(統一配置中心)
? --eureka-server(服務注冊中心)
? --shopping-common(購物公共模塊)
? --shopping-product(商品服務模塊)
? --shopping-order(訂單服務模塊)
更新系統架構,新建config-server節點,也向eureka-server注冊,相關服務注冊節點根據配置實體名稱,路由到config-server節點,動態的加載配置,

異步訊息(Stream)
應用場景
1、異步處理
比如用戶在電商網站下單,下單完成后會給用戶推送短信或郵件,發短信和郵件的程序就可以異步完成,因為下單付款是核心業務,發郵件和短信并不屬于核心功能,并且可能耗時較長,所以針對這種業務場景可以選擇先放到訊息佇列中,有其他服務來異步處理,
2、應用解耦:
假設公司有幾個不同的系統,各系統在某些業務有聯動關系,比如 A 系統完成了某些操作,需要觸發 B 系統及 C 系統,如果 A 系統完成操作,主動呼叫 B 系統的介面或 C 系統的介面,可以完成功能,但是各個系統之間就產生了耦合,用訊息中間件就可以完成解耦,當 A 系統完成操作將資料放進訊息佇列,B 和 C 系統去訂閱訊息就可以了,這樣各系統只要約定好訊息的格式就好了,
3、流量削峰
比如秒殺活動,一下子進來好多請求,有的服務可能承受不住瞬時高并發而崩潰,所以針對這種瞬時高并發的場景,在中間加一層訊息佇列,把請求先入佇列,然后再把佇列中的請求平滑的推送給服務,或者讓服務去佇列拉取,
4、日志處理
kafka 最開始就是專門為了處理日志產生的,
當碰到上面的幾種情況的時候,就要考慮用訊息佇列了,如果你碰巧使用的是 RabbitMQ 或者 kafka ,而且同樣也是在使用 Spring Cloud ,那可以考慮下用 Spring Cloud Stream,Spring Cloud Stream 是訊息中間件組件,它集成了 kafka 和 rabbitmq ,本文以rabbitmq 為例,
當前專案場景
分析目前shopping-order專案中,創建訂單的代碼如下:
/**
* 創建訂單
*
*/
@Transactional
public String Create(OrderInput orderInput) throws Exception {
//扣庫存
ResultVo result1=productClient.decreaseStock(orderInput.getOrderItemInputs());
if (result1.getCode() != 0)
throw new Exception("呼叫訂單扣減庫存介面出錯:" + result1.getMsg());
//構建訂單主表
OrderMaster orderMaster = new OrderMaster();
BeanUtils.copyProperties(orderInput, orderMaster);
//指定默認值
orderMaster.setOrderId(KeyUtil.genUniqueKey("OM"));
orderMaster.setOrderStatus(OrderStatus.NEW);
orderMaster.setPayStatus(PayStatus.WAIT);
//構建訂單明細
List<String> productIds = orderInput.getOrderItemInputs().stream().map(OrderItemInput::getProductId).collect(Collectors.toList());
ResultVo<List<ProductInfoOutput>> result2 = productClient.findProductInfosByIds(String.join(",", productIds));
if (result2.getCode() != 0)
throw new Exception("呼叫訂單查詢介面出錯:" + result2.getMsg());
List<ProductInfoOutput> productInfoOutputs = result2.getData();
//訂單金額總計
BigDecimal total = new BigDecimal(BigInteger.ZERO);
for (OrderItemInput orderItemInput : orderInput.getOrderItemInputs()) {
OrderDetail orderDetail = new OrderDetail();
BeanUtils.copyProperties(orderItemInput, orderDetail);
Optional<ProductInfoOutput> productInfoOutputOptional = productInfoOutputs.stream()
.filter(s -> s.getProductId().equals(orderItemInput.getProductId())).findFirst();
if (!productInfoOutputOptional.isPresent())
throw new Exception(String.format("商品【%s】不存在", orderItemInput.getProductId()));
ProductInfoOutput productInfoOutput = productInfoOutputOptional.get();
orderDetail.setDetailId(KeyUtil.genUniqueKey("OD"));
orderDetail.setOrderId(orderMaster.getOrderId());
orderDetail.setProductName(productInfoOutput.getProductName());
orderDetail.setProductPrice(productInfoOutput.getProductPrice().multiply(new BigDecimal(orderDetail.getProductQuantity())));
orderDetail.setProductIcon(productInfoOutput.getProductIcon());
total = total.add(orderDetail.getProductPrice());
orderDetailRepository.save(orderDetail);
}
orderMaster.setOrderAmount(total);
orderMasterRepository.save(orderMaster);
return orderMaster.getOrderId();
}
創建訂單的同時,先呼叫商品介面扣減庫存,如果占用庫存成功,再生成訂單,這樣的話,生成訂單的操作和占用商品庫存的操作其實是耦合在一起的,在實際電商高并發、高流量的情況下,我們很少這么做,所以,我們要將業務解耦,實作訂單和扣減庫存的異步處理,
大體思路如下:生成訂單》通知商品呼叫庫存》商品占用庫存》通知訂單占用成功》更新訂單占用庫存狀態
stream-rabbit集成
shopping-order、shopping-product專案中
- 首先引入stream-rabbit依賴:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
- application.yml中作相應的配置:
spring:
rabbitmq:
host: aliyun.host
port: 5672
username: guest
password: guest
- 訊息介面定義
public interface StreamClient {
String INPUT = "myMessage";
@Input(StreamClient.INPUT)
SubscribableChannel input();
@Output(StreamClient.INPUT)
MessageChannel output();
}
- 接收端處理邏輯
@Component
@EnableBinding(StreamClient.class)
@Slf4j
public class StreamReceiver {
@StreamListener(value = https://www.cnblogs.com/lizzie-xhu/p/StreamClient.INPUT)
public void process(OrderInput orderInput) {
log.info("StreamReceiver: {}", orderInput);
}
}
- 發送端處理邏輯
@RestController
@RequestMapping("api/v1/stream")
@Slf4j
public class StreamController {
private final StreamClient streamClient;
@Autowired
public StreamController(StreamClient streamClient) {
this.streamClient = streamClient;
}
@GetMapping("/sendMessage")
public void sendMessage() {
OrderInput orderInput=new OrderInput();
orderInput.setBuyerName("小王");
orderInput.setBuyerPhone("15011111111");
orderInput.setBuyerAddress("姥姥家");
orderInput.setBuyerOpenid("11111");
streamClient.output().send(MessageBuilder.withPayload(orderInput).build());
}
}
啟動應用程式,測驗發送介面,發現spring-cloud-stream幫我們自動創建了一個佇列,訊息發送到這個佇列,然后被接收端消費,

此時,如果我們啟動多個shopping-product服務實體,會有個問題,如果發送端發送一條訊息,會被2個實體同時消費,在正常的業務中,這種情況是應該避免的,所以我們需要對訊息進行分組,在application.yml中增加如下配置,保證只有一個服務實體來消費,
spring:
rabbitmq:
host: aliyun.host
port: 5672
username: guest
password: guest
cloud:
stream:
bindings:
myMessage:
group: shopping-order
content-type: application/json
改造Order和Product專案
shopping-order作為庫存占用命令的訊息發送者,首先向shopping-product發送訊息stock_apply(占用庫存申請),shopping-product接收此訊息進行庫存處理,然后將庫存占用處理的結果作為訊息stock_result(占用庫存結果)發送,shopping-order端再收到結果訊息對訂單狀態進行更新,
- shopping-order配置:
spring:
cloud:
stream:
bindings:
stock_apply_output: #占用庫存申請
destination: stock.apply
stock_result_input: #占用庫存結果
destination: stock.result
group: shopping-order
- shopping-product配置:
spring:
cloud:
stream:
bindings:
stock_apply_input: #占用庫存申請
destination: stock.apply
group: shopping-product
stock_result_output: #占用庫存結果
destination: stock.result
- shopping-order定義channel
public interface OrderStream {
String STOCK_APPLY_OUTPUT = "stock_apply_output";
@Output(OrderStream.STOCK_APPLY_OUTPUT)
MessageChannel stockApplyOutput();
String STOCK_RESULT_INPUT = "stock_result_input";
@Input(OrderStream.STOCK_RESULT_INPUT)
SubscribableChannel stockResultInput();
}
- shopping-product定義channel
public interface ProductStream {
String STOCK_APPLY_INPUT = "stock_apply_input";
@Input(ProductStream.STOCK_APPLY_INPUT)
SubscribableChannel stockApplyInput();
String STOCK_RESULT_OUTPUT = "stock_result_output";
@Output(ProductStream.STOCK_RESULT_OUTPUT)
MessageChannel stockResultOutput();
}
- shopping-order發送庫存申請訊息
/**
* 創建訂單
*/
@Transactional
public String Create(OrderInput orderInput) throws Exception {
//構建訂單主表
OrderMaster orderMaster = new OrderMaster();
BeanUtils.copyProperties(orderInput, orderMaster);
//指定默認值
orderMaster.setOrderId(KeyUtil.genUniqueKey("OM"));
orderMaster.setOrderStatus(OrderStatus.NEW);
orderMaster.setPayStatus(PayStatus.WAIT);
//構建訂單明細
List<String> productIds = orderInput.getOrderItemInputs().stream().map(OrderItemInput::getProductId).collect(Collectors.toList());
ResultVo<List<ProductInfoOutput>> result2 = productClient.findProductInfosByIds(String.join(",", productIds));
if (result2.getCode() != 0)
throw new Exception("呼叫訂單查詢介面出錯:" + result2.getMsg());
List<ProductInfoOutput> productInfoOutputs = result2.getData();
//訂單金額總計
BigDecimal total = new BigDecimal(BigInteger.ZERO);
for (OrderItemInput orderItemInput : orderInput.getOrderItemInputs()) {
OrderDetail orderDetail = new OrderDetail();
BeanUtils.copyProperties(orderItemInput, orderDetail);
Optional<ProductInfoOutput> productInfoOutputOptional = productInfoOutputs.stream()
.filter(s -> s.getProductId().equals(orderItemInput.getProductId())).findFirst();
if (!productInfoOutputOptional.isPresent())
throw new Exception(String.format("商品【%s】不存在", orderItemInput.getProductId()));
ProductInfoOutput productInfoOutput = productInfoOutputOptional.get();
orderDetail.setDetailId(KeyUtil.genUniqueKey("OD"));
orderDetail.setOrderId(orderMaster.getOrderId());
orderDetail.setProductName(productInfoOutput.getProductName());
orderDetail.setProductPrice(productInfoOutput.getProductPrice().multiply(new BigDecimal(orderDetail.getProductQuantity())));
orderDetail.setProductIcon(productInfoOutput.getProductIcon());
total = total.add(orderDetail.getProductPrice());
orderDetailRepository.save(orderDetail);
}
orderMaster.setOrderAmount(total);
orderMasterRepository.save(orderMaster);
//扣庫存
StockApplyInput stockApplyInput = new StockApplyInput();
stockApplyInput.setOrderId(orderMaster.getOrderId());
stockApplyInput.setOrderItemInputs(orderInput.getOrderItemInputs());
orderStream.stockApplyOutput().send(MessageBuilder.withPayload(stockApplyInput).build());
return orderMaster.getOrderId();
}
- shopping-product處理庫存申請訊息,并發送庫存處理結果
@Service
@Slf4j
@EnableBinding(ProductStream.class)
public class ProductService {
private final ProductInfoRepository productInfoRepository;
private final ProductCategoryRepository productCategoryRepository;
@Autowired
public ProductService(ProductInfoRepository productInfoRepository,
ProductCategoryRepository productCategoryRepository) {
this.productInfoRepository = productInfoRepository;
this.productCategoryRepository = productCategoryRepository;
}
/**
* 扣減庫存
*
*/
@Transactional
@StreamListener(ProductStream.STOCK_APPLY_INPUT)
@SendTo(ProductStream.STOCK_RESULT_OUTPUT)
public StockResultOutput processStockApply(StockApplyInput stockApplyInput) throws Exception {
log.info("占用庫存訊息被消費...");
StockResultOutput stockResultOutput = new StockResultOutput();
stockResultOutput.setOrderId(stockApplyInput.getOrderId());
try {
for (OrderItemInput orderItemInput : stockApplyInput.getOrderItemInputs()) {
Optional<ProductInfo> productInfoOptional = productInfoRepository.findById(orderItemInput.getProductId());
if (!productInfoOptional.isPresent())
throw new Exception("商品不存在.");
ProductInfo productInfo = productInfoOptional.get();
int result = productInfo.getProductStock() - orderItemInput.getProductQuantity();
if (result < 0)
throw new Exception("商品庫存不滿足.");
productInfo.setProductStock(result);
productInfoRepository.save(productInfo);
}
stockResultOutput.setIsSuccess(true);
stockResultOutput.setMessage("OK");
return stockResultOutput;
} catch (Exception e) {
stockResultOutput.setIsSuccess(false);
stockResultOutput.setMessage(e.getMessage());
return stockResultOutput;
}
}
}
- shopping-order處理庫存處理結果
@StreamListener(OrderStream.STOCK_RESULT_INPUT)
public void processStockResult(StockResultOutput stockResultOutput) {
log.info("庫存訊息回傳" + stockResultOutput);
Optional<OrderMaster> optionalOrderMaster = orderMasterRepository.findById(stockResultOutput.getOrderId());
if (optionalOrderMaster.isPresent()) {
OrderMaster orderMaster = optionalOrderMaster.get();
if (stockResultOutput.getIsSuccess()) {
orderMaster.setOrderStatus(OrderStatus.OCCUPY_SUCCESS);
} else {
orderMaster.setOrderStatus(OrderStatus.OCCUPY_FAILURE);
}
orderMasterRepository.save(orderMaster);
}
}
執行除錯結果,跟蹤執行結果:生成訂單同時發送庫存申請命令,商品模塊處理庫存申請成功后,回傳庫存占用結果告知訂單模塊,從而實作訂單生成和商品庫存占用的邏輯的解耦,
小結
在原有的架構基礎上,我們對商品和訂單服務進行了應用解耦,庫存占用邏輯異步化,通過訊息佇列傳遞訊息,并結合spring cloud stream對訊息input和output系結,使得在程式中很方便的進行訊息發送和接收處理,

微服務網關(Zuul)
Zuul是Netflix開源的微服務網關,可以和Eureka、Ribbon、Hystrix等組件配合使用,Spring Cloud對Zuul進行了整合與增強,Zuul默認使用的HTTP客戶端是Apache HTTPClient,也可以使用RestClient或okhttp3.OkHttpClient, Zuul的主要功能是路由轉發和過濾器,zuul默認和Ribbon結合實作了負載均衡的功能
作業原理
zuul的核心是一系列的filters, 其作用類比Servlet框架的Filter,或者AOP,zuul把請求路由到用戶處理邏輯的程序中,這些filter參與一些過濾處理,比如Authentication,Load Shedding等

Zuul使用一系列不同型別的過濾器,使我們能夠快速靈活地將功能應用于我們的邊緣服務,這些過濾器可幫助我們執行以下功能:
- 身份驗證和安全性 - 確定每個資源的身份驗證要求并拒絕不滿足這些要求的請求
- 洞察和監控 - 在邊緣跟蹤有意義的資料和統計資料,以便為我們提供準確的生產視圖
- 動態路由 - 根據需要動態地將請求路由到不同的后端群集
- 壓力測驗 - 逐漸增加群集的流量以衡量性能,
- Load Shedding - 為每種型別的請求分配容量并洗掉超過限制的請求
- 靜態回應處理 - 直接在邊緣構建一些回應,而不是將它們轉發到內部集群
添加網關
- 新建api-gateway子模塊,作為服務網關、服務發現客戶端、獲取配置客戶端,因此需要引入以下依賴,
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
</dependencies>
- 在啟動類上增加
EnableDiscoveryClient和@EnableZuulProxy注解,
@EnableDiscoveryClient
@EnableZuulProxy
@SpringBootApplication
public class ApiGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(ApiGatewayApplication.class, args);
}
}
- 啟動服務,看看是否能正常獲取配置,并注冊到Eureka Server,

- Zuul網關目前暴露的埠是8080,之前我們訪問商品服務的api,是通過呼叫 http://localhost:11100/api/v1/product/productInfos來訪問的,現在我們就可以通過Zuul,根據商品的服務名稱shopping-produc來訪問 http://localhost:8080/shopping-product//api/v1/product/productInfos,非常輕松的實作了路由的功能,

自定義路由
默認的路由規則是按照服務的名稱來路由服務,當然我們也可以自定義,在zuul中,路由匹配的路徑運算式采用ant風格定義
| 通配符 | 說明 |
|---|---|
| ? | 匹配任意單個字符 |
| * | 匹配任意數量的字符 |
| ** | 匹配任意數量的字符,支持多級目錄 |
zuul:
routes:
# 簡潔寫法
shopping-product: /product/**
- 將命名為product的所有路徑都映射到shopping-product服務中去,然后通過product名稱來訪問,依舊能訪問成功,

- 如果我們需要查看目前所有的路徑映射呢,首先得引入actuator
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
- 其次,需要放開actuator維護埠的權限
management:
endpoints:
web:
exposure:
include: "*"
- 訪問 http://localhost:8080/actuator/routes ,可以看到目前網關的所有路由映射

- 如果需要定義哪些方法不能通過網關呼叫,還可以設定排除哪些路由的規則
zuul:
routes:
# 簡潔寫法
shopping-product: /product/**
# 排除某些路由
ignored-patterns:
- /**/productInfos
這樣我們再訪問這個介面時,就提示 Not Found 錯誤了

Cookie與頭資訊
默認情況下,spring cloud zuul在請求路由時,會過濾掉http請求頭資訊中一些敏感資訊,防止它們被傳遞到下游的外部服務器,默認的敏感頭資訊通過zuul.sensitiveHeaders引數定義,默認包括cookie,set-Cookie,authorization三個屬性,所以,我們在開發web專案時常用的cookie在spring cloud zuul網關中默認時不傳遞的,這就會引發一個常見的問題,如果我們要將使用了spring security,shiro等安全框架構建的web應用通過spring cloud zuul構建的網關來進行路由時,由于cookie資訊無法傳遞,我們的web應用將無法實作登錄和鑒權,有時候,針對某些路由,我們需要傳遞這個cookie,
zuul:
routes:
# 完全寫法
product-route:
path: /product/**
serviceId: shopping-product
# 將指定路由的敏感頭設定為空
sensitiveHeaders:
動態路由
之前路由的配置都是寫在組態檔中,如果路由規則變化以后,需要重啟網關服務,但是實際生產環境,一般都需要動態的加載路由的配置,不能輕易重啟網關服務,
- 將配置資訊集中到統一配置中心服務進行管理,具體實施參考前面章節-統一配置中心,
eureka:
client:
serviceUrl:
defaultZone: http://eureka1:8761/eureka/,http://eureka2:8762/eureka/ #指定服務注冊地址
spring:
application:
name: api-gateway #應用名稱
cloud:
config:
discovery:
enabled: true
service-id: config-server
- 將zuul配置屬性定義成支持動態重繪,增加
@RefreshScope注解
@Component
public class ZuulConfiguration {
@ConfigurationProperties("zuul")
@RefreshScope
public ZuulProperties zuulProperties(){
return new ZuulProperties();
}
}
自定義Filter
設想以下場景:我們需要判斷用戶請求的引數是否包含認證資訊,如果包含token資訊,則可以訪問,否則禁止訪問,可以用Zuul Filter很方便的實作在網關端,統一進行認證,
- 新建自定義的Filter,并繼承ZuulFilter,默認需要實作4個介面
- filterType():回傳 filter 的型別,設定為
PRE_TYPE - filterOrder():回傳 filter 的順序,設定為
PRE_DECORATION_FILTER_ORDER-1 - shouldFilter():是否啟用 filter,設定為
true - run():執行具體的過濾器邏輯
- filterType():回傳 filter 的型別,設定為
/**
* 驗證token 過濾器
*/
@Component
public class TokenFilter extends ZuulFilter {
@Override
public String filterType() {
return PRE_TYPE;
}
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() throws ZuulException {
RequestContext currentContext = RequestContext.getCurrentContext();
HttpServletRequest request = currentContext.getRequest();
//測驗在url引數中獲取token
String token = request.getParameter("token");
if(StringUtils.isEmpty(token)){
currentContext.setSendZuulResponse(false);
currentContext.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());
}
return null;
}
}
- 驗證結果,如果url中添加了 token 引數,TokenFilter 驗證通過,正確回傳結果;如果沒有 token 引數,則回傳 401(UNAUTHORIZED)錯誤

- 還可以在呼叫介面回傳中,設定回應頭
@Component
public class AddResHeaderFilter extends ZuulFilter{
@Override
public String filterType() {
return POST_TYPE;
}
@Override
public int filterOrder() {
return SEND_RESPONSE_FILTER_ORDER - 1;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() {
RequestContext requestContext = RequestContext.getCurrentContext();
HttpServletResponse response = requestContext.getResponse();
response.setHeader("X-Foo", UUID.randomUUID().toString());
return null;
}
}
限流
這里介紹一種限流的設計方案:
對于很多應用場景來說,除了要求能夠限制資料的平均傳輸速率外,還要求允許某種程度的突發傳輸,這時候漏桶演算法可能就不合適了,令牌桶演算法更為適合,如圖所示,令牌桶演算法的原理是系統會以一個恒定的速度往桶里放入令牌,而如果請求需要被處理,則需要先從桶里獲取一個令牌,當桶里沒有令牌可取時,則拒絕服務,

Google公司已經實作了上述的令牌桶的演算法,直接使用 RateLimiter 就可以通過Zuul實作限流的功能:
@Component
public class RateLimitFilter extends ZuulFilter {
private static final RateLimiter RATE_LIMITER = RateLimiter.create(100);
@Override
public String filterType() {
return PRE_TYPE;
}
@Override
public int filterOrder() {
return SERVLET_DETECTION_FILTER_ORDER - 1;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() {
if (!RATE_LIMITER.tryAcquire()) {
throw new RuntimeException("未能獲取到令牌.");
}
return null;
}
}
小結
整體專案結構如下:
spring-cloud-app
? --api-gateway(服務網關)
? --config-server(統一配置中心)
? --eureka-server(服務注冊中心)
? --shopping-common(購物公共模塊)
? --shopping-product(商品服務模塊)
? --shopping-order(訂單服務模塊)

目前所有的客戶端請求,首先被發送到統一網關服務處理,然后由網關進行限流、熔斷、權限驗證、記錄日志等等,然后根據自定義的路由規則,再分發到不同的應用服務中去,應用服務器回傳處理結果后,由網關統一回傳給客戶端,
服務容錯(Hystrix)
在分布式環境中,許多服務依賴項中的一些必然會失敗,Hystrix是一個庫,通過添加延遲容忍和容錯邏輯,幫助你控制這些分布式服務之間的互動,Hystrix通過隔離服務之間的訪問點、停止級聯失敗和提供回退選項來實作這一點,所有這些都可以提高系統的整體彈性,
設計原則
- 防止任何單個依賴項耗盡所有容器(如Tomcat)用戶執行緒,
- 甩掉包袱,快速失敗而不是排隊,
- 在任何可行的地方提供回退,以保護用戶不受失敗的影響,
- 使用隔離技術(如隔離板、泳道和斷路器模式)來限制任何一個依賴項的影響,
- 通過近實時的度量、監視和警報來優化發現時間,
- 通過配置的低延遲傳播來優化恢復時間,
- 支持對Hystrix的大多數方面的動態屬性更改,允許使用低延遲反饋回圈進行實時操作修改,
- 避免在整個依賴客戶端執行中出現故障,而不僅僅是在網路流量中,
如何實作
- 用一個HystrixCommand 或者 HystrixObservableCommand (這是命令模式的一個例子)包裝所有的對外部系統(或者依賴)的呼叫,典型地它們在一個單獨的執行緒中執行
- 呼叫超時時間比你自己定義的閾值要長,有一個默認值,對于大多數的依賴項你是可以自定義超時時間的,
- 為每個依賴項維護一個小的執行緒池(或信號量);如果執行緒池滿了,那么該依賴性將會立即拒絕請求,而不是排隊,
- 呼叫的結果有這么幾種:成功、失敗(客戶端拋出例外)、超時、拒絕,
- 在一段時間內,如果服務的錯誤百分比超過了一個閾值,就會觸發一個斷路器來停止對特定服務的所有請求,無論是手動的還是自動的,
- 當請求失敗、被拒絕、超時或短路時,執行回退邏輯,
- 近實時監控指標和配置變化,
觸發降級
在實際作業中,尤其是分布式、微服務越來越普遍的今天,一個服務經常需要呼叫其他的服務,即RPC呼叫,而呼叫最多的方式還是通過http請求進行呼叫,這里面就有一個問題了,如果呼叫程序中,因為網路等原因,造成某個服務呼叫超時,如果沒有熔斷機制,此處的呼叫鏈路將會一直阻塞在這里,在高并發的環境下,如果許多個請求都卡在這里的話,服務器不得不為此分配更多的執行緒來處理源源不斷涌入的請求,
更恐怖的是,如果這是一個多級呼叫,即此處的服務的呼叫結果還被其他服務呼叫了,這就形成了所謂的雪崩效應,后果將不堪設想,因此,需要某種機制,在一定的例外介面呼叫出現的時候,能夠自動發現這種例外,并快速進行服務降級,
- 首先,shopping-order專案模擬一個遠程呼叫shopping-product服務http請求
/**
* Hystrix 測驗
*/
@RestController
@RequestMapping("api/hystrix")
public class HystrixController {
@GetMapping("/getProductEnv")
public String getProductEnv() {
RestTemplate restTemplate = new RestTemplate();
return restTemplate.postForObject("http://localhost:11100/api/env", null, String.class);
}
}
- 如果此時將shopping-product服務關閉,則shopping-order呼叫遠程服務不可用,進入等待,超時時回傳 Error Page的錯誤頁面,其實我們希望服務不可用的時候直接處理,回傳通知服務的不可用狀態,可以引入Hystrix,
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
- 在啟動類上增加
@EnableCircuitBreaker注解,或者將@SpringBootApplication、@EnableDiscoveryClient、@EnableCircuitBreaker三個合并成一個@SpringCloudApplication注解,
@EnableFeignClients(basePackages = "tech.lancelot.shoppingorder.client")
//@SpringBootApplication
//@EnableDiscoveryClient
//@EnableCircuitBreaker
@SpringCloudApplication
public class ShoppingOrderApplication {
public static void main(String[] args) {
SpringApplication.run(ShoppingOrderApplication.class, args);
}
}
- 修改 HystrixController,增加
@HystrixCommand注解,并指定呼叫方法失敗時的錯誤處理回呼,也可以為整個類增加@DefaultProperties注解,定義一個默認的回傳方法
/**
* Hystrix 測驗
*/
@RestController
@RequestMapping("api/hystrix")
public class HystrixController {
@HystrixCommand(fallbackMethod = "defaultFallback")
@GetMapping("/getProductEnv")
public String getProductEnv() {
RestTemplate restTemplate = new RestTemplate();
return restTemplate.postForObject("http://localhost:11100/api/env", null, String.class);
}
// 默認服務不可達的回傳資訊
private String defaultFallback() {
return "太擁擠了, 請稍后再試~~";
}
}
- 重啟啟動后,再次訪問介面,就會發現介面直接回傳錯誤資訊,不會阻塞在這里,

超時設定
如果我們沒有配置默認的超時時間,Hystrix 將取 default_executionTimeoutInMilliseconds(1秒)作為默認超時時間,也可以自定義超時時間,
- 代碼中修改默認超時配置(改為3秒):
@HystrixCommand(commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "https://www.cnblogs.com/lizzie-xhu/p/3000")})
這樣的話,shopping-order呼叫遠程服務,超過3s之后,立刻回傳錯誤處理,不會再阻塞,
- 可以在組態檔中定義HystrixCommand屬性
hystrix:
command:
default: # 方法默認屬性
execution:
isolation:
thread:
timeoutInMilliseconds: 1000
getProductEnv: # 該名稱方法屬性
execution:
isolation:
thread:
timeoutInMilliseconds: 3000
熔斷機制
如果某個目標服務呼叫慢或者有大量超時,此時,熔斷該服務的呼叫,對于后續呼叫請求,不在繼續呼叫目標服務,直接回傳,快速釋放資源,如果目標服務情況好轉則恢復呼叫,
熔斷器有三個狀態 CLOSED、OPEN、HALF_OPEN 熔斷器默認關閉狀態,當觸發熔斷(至少有 circuitBreaker.requestVolumeThreshold 個請求,錯誤率達到 circuitBreaker.errorThresholdPercentage)后狀態變更為 OPEN,在等待到指定的時間(circuitBreaker.sleepWindowInMilliseconds),Hystrix會放請求檢測服務是否開啟,這期間熔斷器會變為HALF_OPEN 半開啟狀態,熔斷探測服務可用則繼續變更為 CLOSED關閉熔斷器,
- 在方法上增加熔斷屬性的相關設定
@HystrixCommand(commandProperties = {
@HystrixProperty(name = "circuitBreaker.enabled", value = "https://www.cnblogs.com/lizzie-xhu/p/true"), //設定熔斷
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "https://www.cnblogs.com/lizzie-xhu/p/10"),//請求數達到后才計算
@HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "https://www.cnblogs.com/lizzie-xhu/p/10000"), //休眠時間窗
@HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "https://www.cnblogs.com/lizzie-xhu/p/60"), //錯誤率
})
可視化組件
Spring Coud 還給 Hytrix 提供了一個可視化的組件:
- 首先引入相關依賴
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
</dependency>
- 啟動類上增加
@EnableHystrixDashboard注解
@EnableFeignClients(basePackages = "tech.lancelot.shoppingorder.client")
//@SpringBootApplication
//@EnableDiscoveryClient
//@EnableCircuitBreaker
@SpringCloudApplication
@EnableHystrixDashboard
public class ShoppingOrderApplication {
public static void main(String[] args) {
SpringApplication.run(ShoppingOrderApplication.class, args);
}
}
- 重啟 shopping-order 服務,訪問 http://localhost:11110/hystrix ,進入可視化管理界面

- 填上我們監聽的地址:http://shopping-order:11110/actuator/hystrix.stream ,點擊Monitor Stream,進入監控的界面,我們再多重繪呼叫介面api,看看熔斷執行的效果,

小結
通過以上容錯方法的實作,就可以構建更加穩定、可靠的分布式系統:

服務追蹤(Sleuth)
微服務架構是一個分布式架構,它按業務劃分服務單元,一個分布式系統往往有很多個服務單元,由于服務單元數量眾多,業務的復雜性,如果出現了錯誤和例外,很難去定位,主要體現在,一個請求可能需要呼叫很多個服務,而內部服務的呼叫復雜性,決定了問題難以定位,所以微服務架構中,必須實作分布式鏈路追蹤,去跟進一個請求到底有哪些服務參與,參與的順序又是怎樣的,從而達到每個請求的步驟清晰可見,出了問題,很快定位,
OpenTracing
OpenTracing 是一個輕量級的標準化層,它位于應用程式/類別庫和追蹤或日志分析程式之間,
+-------------+ +---------+ +----------+ +------------+
| Application | | Library | | OSS | | RPC/IPC |
| Code | | Code | | Services | | Frameworks |
+-------------+ +---------+ +----------+ +------------+
| | | |
| | | |
v v v v
+------------------------------------------------------+
| OpenTracing |
+------------------------------------------------------+
| | | |
| | | |
v v v v
+-----------+ +-------------+ +-------------+ +-----------+
| Tracing | | Logging | | Metrics | | Tracing |
| System A | | Framework B | | Framework C | | System D |
+-----------+ +-------------+ +-------------+ +-----------+
OpenTracing 的優勢
- OpenTracing 已進入 CNCF,正在為全球的分布式追蹤,提供統一的概念和資料標準,
- OpenTracing 通過提供平臺無關、廠商無關的 API,使得開發人員能夠方便的添加(或更換)追蹤系統的實作,
OpenTracing 資料模型
OpenTracing 中的 Trace(呼叫鏈)通過歸屬于此呼叫鏈的 Span 來隱性的定義,
特別說明,一條 Trace(呼叫鏈)可以被認為是一個由多個 Span 組成的有向無環圖(DAG圖),Span 與 Span 的關系被命名為 References,
例如:下面的示例 Trace 就是由8個 Span 組成:
單個 Trace 中,span 間的因果關系
[Span A] ←←←(the root span)
|
+------+------+
| |
[Span B] [Span C] ←←←(Span C 是 Span A 的孩子節點, ChildOf)
| |
[Span D] +---+-------+
| |
[Span E] [Span F] >>> [Span G] >>> [Span H]
↑
↑
↑
(Span G 在 Span F 后被呼叫, FollowsFrom)
有些時候,使用下面這種,基于時間軸的時序圖可以更好的展現 Trace(呼叫鏈):
單個 Trace 中,span 間的時間關系
––|–––––––|–––––––|–––––––|–––––––|–––––––|–––––––|–––––––|–> time
[Span A···················································]
[Span B··············································]
[Span D··········································]
[Span C········································]
[Span E·······] [Span F··] [Span G··] [Span H··]
每個 Span 包含以下的狀態:(譯者注:由于這些狀態會反映在 OpenTracing API 中,所以會保留部分英文說明)
- An operation name,操作名稱
- A start timestamp,起始時間
- A finish timestamp,結束時間
- Span Tag,一組鍵值對構成的 Span 標簽集合,鍵值對中,鍵必須為 string,值可以是字串,布爾,或者數字型別,
- Span Log,一組 span 的日志集合,
每次 log 操作包含一個鍵值對,以及一個時間戳,
鍵值對中,鍵必須為 string,值可以是任意型別,
但是需要注意,不是所有的支持 OpenTracing 的 Tracer,都需要支持所有的值型別,
- SpanContext,Span 背景關系物件 (下面會詳細說明)
- References(Span間關系),相關的零個或者多個 Span(Span 間通過 SpanContext 建立這種關系)
每一個 SpanContext 包含以下狀態:
- 任何一個 OpenTracing 的實作,都需要將當前呼叫鏈的狀態(例如:trace 和 span 的 id),依賴一個獨特的 Span 去跨行程邊界傳輸
- Baggage Items,Trace 的隨行資料,是一個鍵值對集合,它存在于 trace 中,也需要跨行程邊界傳輸
更多關于 OpenTracing 資料模型的知識,請參考 OpenTracing語意標準,
OpenTracing 實作
這篇檔案列出了所有 OpenTracing 實作,在這些實作中,比較流行的為 Jaeger 和 Zipkin,
事件型別
-
cs ( Client Send ) :客戶端發起請求的時間
-
cr ( Client Received ) :客戶端收到處理完請求的時間,
-
ss ( Server Send ) :服務端處理完邏輯的時間,
-
sr ( Server Received ) :服務端收到呼叫端請求的時間,
客戶端呼叫時間=cr-cs
服務端處理時間=sr-ss
鏈路追蹤
- 打開 shopping-order 專案,增加引入相應的依賴
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
- 重啟服務,用postman呼叫創建訂單的介面,觀察控制臺的日志資訊,發現多了sleuth記錄的鏈路日志資訊
- 第一個引數shopping-order:應用名稱,對應我們application.yml中定義的application-name,
- 第二個引數ee76d19cc6396875:Trace ID, 標識一條請求鏈路,一條請求鏈路包含一個Trace ID,多個Span ID,一條鏈路上的Trace ID是相同的,注意上面的日志資訊第二個引數即Trace ID是一樣的,
- 第三個引數7d13c8acb73bb2a1:Span ID,一個基本的作業單元,如一個http請求,
- 第四個引數true:表示是否要將該資訊輸出到Zipkin等服務中來收集和展示,

- 同樣,shopping-product專案,也增加引入相應的依賴,看看sleuth記錄的日志資訊有啥不同,可以看到他們的 Trace ID 是相同的,而 Span ID 是不同的

Zipkin
目前,鏈路追蹤組件有Google的Dapper,Twitter 的 Zipkin,以及阿里的Eagleeye (鷹眼)等,它們都是非常優秀的鏈路追蹤開源組件,本文主要講述如何在Spring Cloud Sleuth中集成Zipkin,
Zipkin Server主要包括四個模塊:
-
Collector 接識訓收集各應用傳輸的資料
-
Storage 存盤接受或收集過來的資料,當前支持Memory,MySQL,Cassandra,ElasticSearch等,默認存盤在記憶體中,
-
API(Query) 負責查詢Storage中存盤的資料,提供簡單的JSON API獲取資料,主要提供給web UI使用
-
Web 提供簡單的web界面

-
首先,安裝 zipkin,為了方便直接用 docker 進行安裝,具體詳見容器化部署章節,這里不再詳述,
-
引入sleuth-zipkin相關依賴,因為 starter-zipkin 已經包含 starter-sleuth 的依賴,所以可以把原先的 sleuth依賴去掉,
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
- 配置 Zipkin Server服務的地址
spring:
zipkin:
base-url: http://zipkin:9411/
- Sleuth 有個抽樣比例的屬性,默認是0.1,就是默認會將10%的鏈路資訊上傳,為了方便測驗觀察,我們把這個屬性值調成100%
spring:
sleuth:
sampler:
rate: 100
zipkin:
base-url: http://zipkin:9411/
- 重啟服務,呼叫2次api介面,再次訪問 Zipkin Server 管理界面,可以看到對應的鏈路資訊,

- 可以點擊查看詳情,很方便的看到一次鏈路呼叫,每個節點的訪問時間,利于我們排查性能問題

小結
在服務呼叫的程序中,通過Sleuth將鏈路資訊(經過抽樣后的資訊)統一上報給Zipkin,通過Zipkin就可以集中查看和管理微服務架構中的呼叫鏈路資訊,便于開發人員與運維人員跟蹤和除錯問題,

容器化部署
安裝Docker
[root@localhost ~]# yum install docker
[root@localhost ~]# systemctrl enable docker #設定docker開機啟動
[root@localhost ~]# systemctrl start docker #啟動docker
- 配置vi /etc/docker/deamon.json,添加國內加速鏡像
{
"registry-mirrors": ["http://hub-mirror.c.163.com"],
"registry-mirrors": ["https://njrds9qc.mirror.aliyuncs.com"]
}
- 重啟生效
[root@localhost ~]# systemctl daemon-reload
[root@localhost ~]# systemctl restart docker
- 驗證是否成功安裝
[root@localhost ~]# docker -v
Docker version 1.13.1, build 7f2769b/1.13.1
安裝Docker-Compose
- 檢查是否安裝python-pip
[root@localhost ~]# pip -V
- 已安裝pip則跳過該步驟,否則安裝pip
[root@localhost ~]# yum -y install epel-release
[root@localhost ~]# yum -y install python-pip
[root@localhost ~]# pip install --upgrade pip
- 安裝docker-compose
[root@localhost ~]# pip install docker-compose
- 安裝程序中如果出現Command errored python setup.py egg_info 可嘗試升級setuptools
[root@localhost ~]# pip install more-itertools==5.0.0
- 驗證是否成功安裝
[root@localhost ~]# docker-compose -v
docker-compose version 1.25.0, build b42d419
Eureka部署
- 首先我們創建2個節點的組態檔
application.yml:
spring:
application:
name: eureka-server #應用名稱
profiles:
active: peer1
application-peer1.yml:
eureka:
client:
service-url:
defaultZone: http://peer2:8762/eureka/ #指定服務注冊地址
server:
port: 8761 #應用服務埠
application-peer2.yml:
eureka:
client:
service-url:
defaultZone: http://peer1:8761/eureka/ #指定服務注冊地址
server:
port: 8762 #應用服務埠
- 在eureka-server專案下新建Dockerfile檔案
FROM hub.c.163.com/library/java:8-alpine
ADD target/*.jar app.jar
EXPOSE 8761
EXPOSE 8762
ENTRYPOINT ["java","-jar","/app.jar"]
- 構建鏡像:
mvn clean package -Dmaven.test.skip=true -U
docker build -t spring-cloud-app/eureka-server:v1 .
MySQL部署
- 拉取MySQL的鏡像檔案
[root@localhost ~]# docker pull mysql:5.7
- 在docker-compose.yml檔案中相關配置
mysql:
image: docker.io/mysql:5.7
hostname: mysql
networks:
- eureka-net
ports:
- "3306:3306"
environment:
MYSQL_ROOT_PASSWORD: "123456"
volumes:
- "./mysql/conf:/etc/mysql"
- "./mysql/logs:/var/log/mysql"
- "./mysql/data:/var/lib/mysql"
RabbitMQ
-management 表示有管理界面的,可以瀏覽器訪問,5672是訪問埠,15672是管理埠,
[root@localhost ~]# docker run -d --hostname rabbitmq -p 5672:5672 -p 15672:15672 rabbitmq:3.8.2-management
訪問埠管理界面,輸入默認用戶名/密碼 :guest/guest

OpenZipkin
[root@localhost ~]# docker run -d -p 9411:9411 openzipkin/zipkin

編排鏡像
docker-compose.yml:
version: "2"
services:
eureka1:
image: spring-cloud-app/eureka-server:v1
hostname: eureka1
networks:
- eureka-net
ports:
- "8761:8761"
environment:
- spring.profiles.active=peer1
eureka2:
image: spring-cloud-app/eureka-server:v1
hostname: eureka2
networks:
- eureka-net
ports:
- "8762:8762"
environment:
- spring.profiles.active=peer2
config-server:
image: spring-cloud-app/config-server:v1
hostname: config-server
networks:
- eureka-net
ports:
- "8888:8888"
mysql:
image: docker.io/mysql:5.7
hostname: mysql
networks:
- eureka-net
ports:
- "3306:3306"
environment:
MYSQL_ROOT_PASSWORD: "123456"
volumes:
- "./mysql/conf:/etc/mysql"
- "./mysql/logs:/var/log/mysql"
- "./mysql/data:/var/lib/mysql"
rabbitmq:
image: docker.io/rabbitmq:3.8.2-management
hostname: rabbitmq
networks:
- eureka-net
ports:
- "5672:5672"
- "15672:15672"
zipkin:
image: docker.io/openzipkin/zipkin:2.19.2
hostname: zipkin
networks:
- eureka-net
ports:
- "9411:9411"
networks:
eureka-net:
driver: bridge
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/19548.html
標籤:架構設計
