簡單介紹
隨着互聯網的發展,網站 應用的規模不斷擴大,需求的激增嗎,帶來了技術上的革命,系統架構也在不斷的演進。從以前的單一應用,到垂直拆分,再到分布式服務,再到SOA(面向服務的架構),再到微服務架構,今天的SpringCloud和另一個阿里的Dubbo都是微服務架構目前比較火的兩個框架;Spring善於集成,這個大家都是知道的,把世界上最好的框架拿過來,集成到自己的項目中,SpringCloud也還是一樣,把當前非常流行的技術整合到一起,就成就了今天的SpringCloud,
比如他集成了以前是一家刻光盤后來轉技術的公司的諸多技術:
-
Eureka:注冊中心
-
Zuul :網關
-
Ribbon:負載均衡
-
Feign :遠程服務調用
-
Hystrix:熔斷器
-
......以上部分也是我們玩SpringCloud的核心技術。還有諸多諸多......
微服務的特點
-
單一職責,微服務中每一個服務都是一個唯一的業務,也就是說一個服務只干一件事情;
-
微服務服務拆分粒度很小,但是五臟俱全
-
微服務一般向外暴露Rest風格服務接口api,不關心服務的技術實現,可以是Java,可以是其他語言
-
每個服務之間互相獨立,互不干擾:
-
面向服務,提供Rest接口,使用什么技術無人干涉
-
前后端分離,提供統一Rest接口,不必在為PC,移動端單獨開發接口
-
數據庫分離,每個服務都是用自己的數據源
-
部署獨立,每個服務都是獨立的組件,可復用,可替換,降低耦合,容易維護;
無論是Dubbo還是SpringCloud都會涉及到服務間的遠程調用,目前常見的服務遠程調用方式就一下兩種
-
RPC:Dubbo是其使用者,自定義數據格式,基於原生TCP通信,速度快,效率高
-
Http:Http其實是一種網絡傳輸協議,也是基於TCP,但他規定了數據的傳輸格式,現在瀏覽器和服務端基本使用的都是Http協議,他也可以用來作為遠程服務調用,缺點就是里面封裝的數據過多,不信你打開瀏覽器,看F12里面的數據,是不是有很多字段比如請求頭那一堆堆...
蜻蜓點水RPC
RPC,即 Remote Procedure Call(遠程過程調用),是一個計算機通信協議。 該協議允許運行於一台計算機的程序調用另一台計算機的子程序,說得通俗一點就是:A計算機提供一個服務,B計算機可以像調用本地服務那樣調用A計算機的服務,RPC調用流程圖如下:
蜻蜓點水Http
Http協議:超文本傳輸協議,是一種應用層協議。規定了網絡傳輸的請求格式、響應格式、資源定位和操作的方式等。但是底層采用什么網絡傳輸協議,並沒有規定,不過現在都是采用TCP協議作為底層傳輸協議。例如我們通過瀏覽器訪問網站,就是通過Http協議。只不過瀏覽器把請求封裝,發起請求以及接收響應,解析響應的事情都幫我們做了。如果是不通過瀏覽器,那么這些事情都需要自己去完成。
RPC和Http的異同
Http與RPC的遠程調用非常像,都是按照某種規定好的數據格式進行網絡通信,有請求,有響應。在這方面,兩者非常相似,但是還是有一些細微差別。
-
RPC並沒有規定數據傳輸格式,這個格式可以任意指定,不同的RPC協議,數據格式不一定相同。
-
Http中還定義了資源定位的路徑,RPC中並不需要
-
最重要的一點:RPC需要滿足像調用本地服務一樣調用遠程服務,也就是對調用過程在API層面進行封裝。Http協議沒有這樣的要求,因此請求、響應等細節需要我們自己去實現。
-
優點:RPC方式更加透明,對用戶更方便。Http方式更靈活,沒有規定API和語言,跨語言、跨平台
-
缺點:RPC方式需要在API層面進行封裝,限制了開發的語言環境
-
如何選擇?
既然兩種方式都可以實現遠程調用,我們該如何選擇呢?
-
速度來看,RPC要比http更快,雖然底層都是TCP,但是http協議的信息往往比較臃腫,不過可以采用gzip壓縮。
-
難度來看,RPC實現較為復雜,http相對比較簡單
-
靈活性來看,http更勝一籌,因為它不關心實現細節,跨平台、跨語言。
因此,兩者都有不同的使用場景:
-
如果對效率要求更高,並且開發過程使用統一的技術棧,那么用RPC還是不錯的。
-
如果需要更加靈活,跨語言、跨平台,顯然http更合適
微服務,更加強調的是獨立、自治、靈活。而RPC方式的限制較多,因此微服務框架中,一般都會采用基於Http的Rest風格服務。
上面我們已經確定微服務要使用Http,目前比較常用的Http客戶端工具有如下幾款:
-
HttpClient
-
OkHttp
-
URLConnection()
以上三種就不詳細說明了,因為Spring提供了一個ResTemplate模版 工具類對基於Http的客戶端進行了封裝,並且還支持序列化和反序列化,非常的nice,RestTemplate並沒有規定Http的客戶端類型,目前三種常用的三種都支持!
[基礎]服務的遠程調用:提供者和消費者
創建一個父工程,我們不做過多的依賴,就定義一下常用的配置,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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.ccl.demo</groupId>
<artifactId>cloud-demo</artifactId>
<packaging>pom</packaging>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.1.RELEASE</version>
<relativePath/>
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<spring-cloud.version>Finchley.RC1</spring-cloud.version>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
</project>
服務的提供者
在父工程中創建一個Moudle,選擇maven,因為我們已經有父親工程了,整體架構如下:
pom.xml內容:——>
<dependencies>
<!--Spring Boot-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>2.1.6.RELEASE</version>
</dependency>
<!--Web-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.1.6.RELEASE</version>
</dependency>
<!--阿里的Druid連接池-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.10</version>
</dependency>
<!--mysql連接驅動-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.16</version>
</dependency>
<!--簡化set get 有參無參等的工具依賴-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.8</version>
</dependency>
<!--持久層框架 : JPA -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<version>2.1.6.RELEASE</version>
<scope>compile</scope>
</dependency>
</dependencies>
實體類:Employee ——>
/*提供有參無參,get set*/ @Data /*用於指定數據庫表的名稱,不指定默認類名*/ @Entity(name = "test_employee") /*JPA底層使用Hibernate,采用延遲加載,返回代理對象在RestController想轉Json時會報錯,代理對象沒有數據填充*/ @JsonIgnoreProperties({"hibernateLazyInitializer", "handler", "fieldHandler"}) public class Employee { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private int id; private String name; private String dbase; }
持久層Repository——>
/** * JpaRepository<Employee,Integer> * 第一個泛型為確定對象關系映射的類 * 第二個泛型確定該類的主鍵類型 */ @Component public interface EmployeeRepository extends JpaRepository<Employee, Integer> { }
業務層Service——>
@Service public class EmployeeServiceImpl implements EmployeeService { @Autowired private EmployeeRepository repository; @Override public boolean saveEmployee(Employee employee) { Employee employee1 = repository.save(employee); if (employee != null) { return true; } return false; } @Override public boolean removeEmployee(int id) { //Jpa的deleteById方法,如果id不存在就會拋出異常,在進行操作是,應先確定其存在
if (repository.existsById(id)) { repository.deleteById(id); return true; } return false; } @Override public boolean modiflyEmployee(Employee employee) { Employee employee1 = repository.save(employee); if (employee != null) { return true; } return false; } @Override public Employee getEmployeeById(int id) { //Jpa的getOne方法,如果id不存在就會拋出異常,在進行操作是,應先確定其存在
if (repository.existsById(id)) { return repository.getOne(id); } Employee employee = new Employee(); employee.setName("no this employee"); return null; } @Override public List<Employee> listAllEmployee() { return repository.findAll(); } }
web層Controller——>
@RestController @RequestMapping("/provider/employee") public class EmployeeController { @Autowired private EmployeeService service; @PostMapping("/save") public boolean saveHandler(@RequestBody Employee employee) { return service.saveEmployee(employee); } @DeleteMapping("/del/{id}") public boolean removeEmployeeById(@PathVariable int id) { return service.removeEmployee(id); } @PostMapping("/update") public boolean updateHandler(@RequestBody Employee employee) { return service.modiflyEmployee(employee); } @GetMapping("/get/{id}") public Employee getOneById(@PathVariable int id) { return service.getEmployeeById(id); } @GetMapping("/list") public List<Employee> listAllEmployee() { return service.listAllEmployee(); } }
Spring Boot啟動類——>
@SpringBootApplication public class ProviderRun { public static void main(String[] args) { SpringApplication.run(ProviderRun.class, args); } }
配置文件 application.yml——>
server:
port: 8081
spring: jpa: database: mysql #數據庫類型為mysql
generate-ddl: true #在spring容器啟動時,根據Bean自動創建數據表
show-sql: true #指定在控制台是否顯示sql語句
hibernate:
ddl-auto: none #指定應用重啟時,不重新建表
datasource:
type: com.alibaba.druid.pool.DruidDataSource
url: jdbc:mysql://localhost:3306/mytest?serverTimezone=UTC
username: root
password: root
logging:
#設置日志輸出格式
pattern:
console: level-%level %msg%n
level:
root: info #Spring Boot啟動時的日志級別
org.hibernage: info #hibernate的運行日志級別
org.hibernate.type.descriptor.sql.BasicBinder: trace
org.hibernate.hql.internal.ast.exec.BasicExecutor: trace
com.ccl: debug
這個時候,我們就可以啟動這個這個服務了,然后通過測試工具,測試一下服務是否正常可訪問,我已經測過了,所有這里不做過多驗證。
服務的消費者
實體類bean:因為不接觸到數據庫,所以不做過多的JPA的注解——>
/*提供有參無參,get set*/ @Data public class Employee { private int id; private String name; private String dbase; }
簡化后的服務調用Controller——>
@RestController @RequestMapping("/consumer/employee") public class ConsumerController { @Autowired private RestTemplate restTemplate; @PostMapping("/save") public boolean saveHandler(@RequestBody Employee employee) { String url = "http://localhost:8081//provider/employee/save"; //第一個參數:服務提供着請求路徑 //第二個參數:我們要操作的對象 //第三個參數:服務提供者的返回值類型
return restTemplate.postForObject(url, employee, Boolean.class); } @DeleteMapping("/del/{id}") public void removeEmployeeById(@PathVariable int id) { String url = "http://localhost:8081/provider/employee/del" + id; //delete方法沒有返回值,不做返回處理
restTemplate.delete(url); } @PostMapping("/update") public void updateHandler(@RequestBody Employee employee) { String url = "http://localhost:8081/provider/employee/update"; restTemplate.put(url, employee); } @GetMapping("/get/{id}") public Employee getOneById(@PathVariable int id) { String url = "http://localhost:8081/provider/employee/get/" + id; return restTemplate.getForObject(url, Employee.class); } @GetMapping("/list/ids") public List<Employee> listAllEmployee() { String url = "http://localhost:8081/provider/employee/list"; return restTemplate.getForObject(url, List.class); } }
Spring Boot啟動類——>
@SpringBootApplication public class ConsumerRun { public static void main(String[] args) { SpringApplication.run(ConsumerRun.class, args); } @Bean public RestTemplate restTemplate(){ //RestTemplate支持三種三種http客戶端類型 //HttpClient 、 OkHttp 、JDK原生的URLConnection(這個是默認的 ) //默認就是不給參數,現在我們使用的是OkHttp
return new RestTemplate(new OkHttp3ClientHttpRequestFactory()); } }
配置文件application.yml——>
server:
port: 8082
logging:
#設置日志輸出格式
pattern:
console: level-%level %msg%n
測試工具:Postman
扭開Postman測試工具,發起請求驗證服務消費者是否通過調用服務提供者的服務,間接的操作數據庫數據,完成服務的遠程調用
在獲取一條數據試試
沒Get到也沒關系,因為上面這個東西,太原生了,上面的Demo存在明顯的短板:
-
比如消費服務時的訪問路徑、硬編碼在邏輯代碼中。后期發生變更不易維護
-
其次萬一服務的提供者宕機,服務的消費者也不知道
-
然后就是服務的提供者的集群,混在均衡得自己實現
Eureka:中文意思"我發現了","我找到了",你們知道Zookeeper嗎,就是Dubbo建議使用的注冊中心那個Zookeeper,二者就是差不多的,但是Dubbo和Eureka側重點不同,Eureka是犧牲了一致性,保證了可用性,而Zookeeper犧牲了可用性,保留了一致性,所謂的CAP的"三二原則",
Eureka的作用這里也簡單帶過:
-
【服務的注冊、發現】:負責管理,紀錄服務提供者的信息,服務調用者無需自己尋找服務的,而是把自己的需求告訴Eureka,Eureka就會吧符合你需求的服務告訴你,好比租房中介。
-
【服務的監控】:服務的提供者和Eureka之間還通過"心跳",保持着聯系,當某個服務提供方出現了問題,Eureka自會在指定的時間范圍內將其剔除,就好比租房子,房東已經把房子租出去了,中介就會把這個房子排除在自己掌握的房源名單里
這張圖理解為一個房東,一個中介,一個打工仔:
-
房東有房,把房子托給中介公司幫忙出租,房東和中介保持着聯系(心跳),如果這個房子房東自己z住不想出租了或者房子漏水暫時不租了,中介第一時間就會知道,然后停止該房子的出租,而打工仔一個人孤苦伶仃的來到一個陌生的城市拼搏,他找到了中介,中介給了他一種表單,里面羅列了這個中介的所有的房源,看他需求什么,自己有的話就可以幫他聯系上房東,通過這種方式,打工仔身在異鄉但仍然感受到了家的溫暖。
Eureka:中介
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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>cloud-demo</artifactId>
<groupId>com.ccl.demo</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>00-eureke-server</artifactId> <dependencyManagement>
<dependencies>
<!-- springCloud -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
<version>2.0.1.RELEASE</version>
</dependency>
</dependencies>
</project>
application.yml——>
server:
port: 8080
spring:
application:
name: Eureka-Server #會在Eureka服務列表顯示的應用名
eureka:
instance:
hostname: localhost
client:
register-with-eureka: false #是否注冊值的信息到Eureka,默認True
fetch-registry: false #是否拉取Eureka上的服務列表,當前是Server,不需要
service-url: # EurekaServer的地址,如果是集群,需要加上其它Server的地址。
defaultZone: Http://${eureka.instance.hostname}:${server.port}/eureka
啟動類——>
@SpringBootApplication @EnableEurekaServer public class EurekServerRun { public static void main(String[] args) { SpringApplication.run(EurekServerRun.class, args); } }
服務的提供者:房東向中介注冊房子
我們上一個服務提供者,稍加改造:
pom.xml—添加Eureka客戶端依賴—>
<!-- Eureka客戶端 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
aapplication.yml——>
增加了application.name和Eureka相關的配置
server:
port: 8081
spring:
application:
name: 02-provider
eureka:
client:
service-url: #Eureka的地址
defaultZone: Http://localhost:8080/eureka
instance:
prefer-ip-address: true #當調用getHostname獲取實例的hostname時,返回ip而不是host名稱
ip-address: 127.0.0.1 #指定自己的ip,不指定的話會自己尋找
這里注意一下:
-
不用指定register-with-eureka和fetch-registry,因為默認是true
-
fetch-registry: true #eureka注冊中心配置 表明該項目是服務端,不用拉取服務 register-with-eureka: true #不用在eureka中注冊自己
啟動類:
@SpringBootApplication @EnableDiscoveryClient //開啟Eureka客戶端
public class ProviderRun { public static void main(String[] args) { SpringApplication.run(ProviderRun.class, args); } }
至於中間業務層和持久層,和上一個Demo一樣,不做說明
這個時候訪問localhost:8080,應該就會發現相關的服務應該被注冊上了,但我此時不做演示,一輪測試
服務的消費者:打工仔向中介打聽房子
改造之前的服務消費者,這次我們要想Eureka索取在線服務列表,調用我們想調用的服務
添加依賴:
<!-- Eureka客戶端 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
配置application.yml:
server:
port: 8082
spring:
application:
name: 02-consumer
eureka:
client:
service-url: #Eureka的地址
defaultZone: http://localhost:8080/eureka
instance:
prefer-ip-address: true #當其它服務獲取地址時提供ip而不是hostname
ip-address: 127.0.0.1 #指定自己的ip信息,不指定的話會自己尋找
啟動類:
@SpringBootApplication @EnableDiscoveryClient //開啟Eureka客戶端
public class ConsumerRun { public static void main(String[] args) { SpringApplication.run(ConsumerRun.class, args); } @Bean public RestTemplate restTemplate(){ //RestTemplate支持三種三種http客戶端類型 //HttpClient 、 OkHttp 、JDK原生的URLConnection(這個是默認的 ) //默認就是不給參數,現在我們使用的是OkHttp
return new RestTemplate(new OkHttp3ClientHttpRequestFactory()); } }
Controller:
@RestController @RequestMapping("/consumer/employee") public class ConsumerController { @Autowired private RestTemplate template; @Autowired private DiscoveryClient client; @GetMapping("/getAll") public List<Employee> allListEmployee(){ //根據服務的名稱獲取服務的實列,一個服務可能有多個提供者,所以是List //你可以把這個步驟想象成在向Spring容器根據接口索要實列對象
List<ServiceInstance> instances = client.getInstances("02-provider"); for (ServiceInstance si : instances){ String host = si.getHost(); int port = si.getPort(); System.out.println(host + ":" + port); //獲取的ip和端口,動態的組裝成url
String url = "http://" + host + ":" + port + "/provider/employee/list"; //發起請求,獲得數據並返回
List employees = this.template.getForObject(url, List.class); return employees; } return null; }
Postman登場發表獲獎感言
我們通過Eureka獲取到了指定應用名的服務的IP和Port,動態的組成了我們的請求Rest連接,成功的調用了服務的提供者的一個接口
然后我們去看看我們的Eureka:
暫時就這么點東西吧,當然Eureka還支持集群和負載均衡:
還記得之前的故事嗎?中介給了打工仔一個表單,在實際中也是這樣的,Eureka將所有的服務列表全部給調用方,調用方自己匹配,負載均衡是調用方自己的事,而不是Eureka的事,他只是一個房子的搬運工,負載均衡也很簡單,下面來看看: 在返回RestTemplate的方法上給一個注解 : @LoadBalanced,默認使用的輪詢的方式,也可以指定為特定的負載均衡算法
Eureka集群:多個中介
說到集群,避免單點故障,提高吞吐量,我們就弄三個Eureka服務吧,因為都是本地為了區分,這里又得動hosts文件了:
先說一下思路:三個Eureka
-
第一步分別修改 eureka.instance.hostname: eureka8080 / eureka8081 / eureka8082
-
第二步修改本地的映射文件,eureka8080 / eureka8081 / eureka8082 都指向192.0.0.1
-
第三步刪除register-with-eureka=false和fetch-registry=false兩個配置。這樣就會把自己注冊到注冊中心了。
-
第四步將參與集群的機器全部羅列在service-url : defaultZon中,整體如下:
-
然后就是修改我們本地的映射文件,為了區分,不為其他的用途,如下
然后這幾個機器算是形成集群了,我們先啟動訪問測試一下:
接下來就修改一下我們的服務注冊者和服務消費者的注冊和拉取請求連接即可:
修改服務提供者的配置文件:
修改服務消費者的配置文件:
不慌,我的端口和前面的Eureka的端口碰撞了,這里重新修改了端口,在這里還得注意一下,不知道是不是使用負載均衡的原因,還是因為使用了集群的原因,我們的RestTemplate在發起請求的時候不能再拼接真實的地址了,會服務器異常,拋出異常:No instances available for 127.0.0.1
只能用在Eureka中注冊的服務名進行調用,如下:
Postman測試工具啟動:完美獲得數據;
結束語 = 補充干貨
但我以前的學習中的有些內容並沒有講解,我發現這個課程並沒有講,所以我做點補充吧!
上面的很多屬性經過這個Demo + 后面的注釋,我相信大家都已經比較熟悉了,但是下面的部分屬性,算作補充內容吧
-
從服務的注冊說起
服務的提供者在服務啟動時,就會檢查屬性中的是否將自己也想Eureka注冊這個配置,默認為True,如果我們沒有手動設置,那么就會向Eureka服務發起一個Rest請求,並帶上自己的數據,Eureka收到這些數據時,就會將其保存到一個雙層Map結構中,第一層Map的key就是服務名稱,第二層Map的key就是服務實列id
-
然后再說服務的續約,牽扯到上圖的屬性
在我們的服務注冊完成之后,通過心跳維持聯系,證明自己還或者(這是服務的提供者定時向Eureka發),這個我們成為服務的續約,續約方式:"發起心跳"
然后就有兩個屬性,可以被我們安排上:在開發上可以如我那樣配置參數
lease-renewal-interval-in-seconds: 30 :每隔30秒一個心跳
lease-expiration-duration-in-seconds: 90 :90秒沒有動靜,認為服務提供者宕機了
-
再然后就是 "實列id"
看看這張圖:
UP(1) : 表示只有一個實列
DESK...:port :實列的名稱(instance-id)
默認格式就是 "hostname" + "spring.application.name" + "server.port"
可以改成為我筆記中的那樣,簡單明了且大方!
這里得說明一下這個屬性是區分統一服務不同實現的唯一標准,是不能重復的
-
服務的提供者說完,我們來說服務的消費者
當我們的服務消費者啟動后,會檢測eureka.client.fetch-registry=true,如果為true,就會去備份Eureka的服務列表到本地,並且可以通過registry-fetch-interval-seconds: 5 來設置多久更新一下備份數據,默認30 ,生產環境下不做修改,開發環境可以調小
-
服務的提供者也說完了,最后我們就來說說Eureka吧
-
失效剔除
有時候,我沒得服務提供方並不一定是宕機了,可能是一起其他的原因(網絡延遲啥的)造成的服務一時沒有正常工作,也就是沒心跳且超貴最長時限,Eureka會將這些服務剔除出自己的服務列表
屬性:eureka.server.eviction-interval-timer-in-ms,默認60S 單位毫秒,
-
自我保護
這個橫幅就是觸發了Eureka的自我保護機制了,當一個服務為按時心跳續約時,Eureka會統計最近15分鍾所有的服務的爽約比列,超過85%(生產環境下因為網絡原因及其有可能造成這么局面),Eureka此時並不會將服務給剔除,而是將其保護起來,生產環境下作用還是很明顯,起碼不會因為網絡原因導致大部分服務爽約,Eureka將全部服務剔除的局面出現
但在開發中,我們一般將其關閉 :enable-self-preservation: false
-
SpringCloud的另一個組件:OpenFeign
大家可以回憶一下之前我們寫的Demo,,沒有回憶的話,我疏導回憶一下,最開始我們RestTemplate,當時直連消費者和提供者,將請求路徑寫死在代碼中,而且負載均衡只有自己手寫,RestTemplate只能給我們提供遠程調用的功能,后來我們加入了Eureka,作為一個中間人,利用Eureka的服務的注冊發現和監控和負載均衡,通過Eureka客戶端獲取指定服務名的ip或者應用名+端口,動態拼接成url,外加上RestTemplate一起完成遠程的調用,但你有沒有發現RestTemplate這個包裝類有點小問題,而且這樣搞起來很麻煩
-
服務提供者有返回數據,但經過RestTemplate相關api的CRUD的API沒有返回值為void
-
再者,分模塊開發,我們根本不知道服務的提供者的返回值是什么,不可能一個一個的問
下面我們就要學習另一個組件取代RestTemplate,他就是OpenFeign
優雅的簡單介紹
Feign的中文意思是偽裝、裝作的意思;OpenFeign是Feign的更新版本,是一種聲明式REST客戶端,使用起來聽說更為便捷和優雅!Spring Boot1.X的時候就叫feign,我用的就是Feign;SpringCloud對Feign進行了增強,這個Feign也是那個租碟片公司研發的組件;
快速上手
首先是依賴問題,這里有點出入:
首先是第一個依賴肯定是要加的,后面兩個依賴是后來百度異常信息加上的,方可運行
<!--openFeign的依賴 以下三個,不然拋出ClassNotFoundException-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<version>2.0.1.RELEASE</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>18.0</version>
</dependency>
<dependency>
<groupId>com.netflix.archaius</groupId>
<artifactId>archaius-core</artifactId>
<version>0.7.6</version>
</dependency>
服務的提供者:無需改動
服務的消費者:
定義service接口,,這是Feign的核心所在,下面詳細說明
@FeignClient("03-provider") //屬性為服務提供者的應用名
@RequestMapping("/provider/employee") public interface EmployeeService { @PostMapping("/save") boolean saveEmployee(Employee employee); @DeleteMapping("/del/{id}"boolean removeEmployee(@PathVariable("id") int id); @PostMapping("/update") boolean modiflyEmployee(Employee employee); @GetMapping("/get/{id}") Employee getEmployeeById(@PathVariable("id")int id); @GetMapping("/list") List<Employee> listAllEmployee(); }
-
首先這是一個接口,在服務的消費方,你可以把他當作Service層(偷懶)也可以當成Dao層
-
首先整體上來講,Feign會通過動態代理幫我們生成實現類;
-
其次開局第一個注解@FeignClient,生命這是一個Feign客戶端,同時通過value指定了服務提供者應用名
-
最后接口中定義的方法,方法是來自於服務提供者的service接口中的方法,但是方法上的注解確實來自服務提供者Controller上的注解,完全采用SpringMVC的注釋,Feign會根據注解幫我們生成URL,並訪問相應的服務接口
然后你可以在Controller層或者service層直接@Autowired這個接口,直接調用接口中的方法即可實現遠程調用
然后還得在啟動類上添加注解如下:
@SpringBootApplication @EnableDiscoveryClient //開啟Eureka客戶端
@EnableFeignClients(basePackages = "com.ccl.test.service") //開啟Feign,並指定Service所在的包
public class ConsumerRun { public static void main(String[] args) { SpringApplication.run(ConsumerRun.class, args); } }
此外,Feign中還集成了Ribbon負載均衡,所以這里我們直接拋棄了RestTemplate,接下來把項目跑起來,通過我們的服務消費者遠程調用服務提供者的api完成這次遠程調用;說到這里,我們就必須得明白Ribbo了,下面我們就來學習一下
SpringCloud的另一組件:Ribbon
優雅而不失風度的簡單介紹
-
Ribbon還是那個當初租碟片營生起家的NetFlix公司研發發布的,並被SpringCloud集成到了項目中,當我們為Ribbon配置服務提供者的地址列表后,Ribbon就可以根據多種之一的負載均衡算法自動的去分配服務消費者的請求;
-
由於我們已經學習過了Feign,所以RestTemplate的負載均衡我記得之前的筆記中是有的,在返回RestTemplate到Spring容器的方法上加上一個@LoadBalanced注解即可實現,只是現在被我證實了,當我們使用Ribbon負載均衡后,我們不能再通過拼接ip+port的方法發起調用,只能通過應用名的方式發起遠程調用,因為這是Ribbon根據應用名相同采取負載均衡的前提;
-
下面我們來說說我們后面常用的OpenFeign的Ribbon負載均衡,Feign本身也是集成了Ribbon的依賴和自動配置的,在這里我們就要單獨的配置Ribbon了,而不是簡單的加一個注解
03-provider: ribbon: ConnectTimeout: 250 # 連接超時時間(ms) ReadTimeout: 1000 # 通信超時時間(ms) OkToRetryOnAllOperations: true # 是否對所有操作重試 MaxAutoRetriesNextServer: 1 # 同一服務不同實例的重試次數 MaxAutoRetries: 1 # 同一實例的重試次數
#這個配置是為某個03-provider這個服務配置的局部壞均衡,若要全局,不需要指定服務名,直接ribbon.XXX實現全局配置
Ribbon的負載均衡算法之IRule接口
Ribbon提供了點多鍾輪詢算法,常見負載均衡算法比如默認的輪詢,其他的隨機、響應時間加權算法等;
想要修改Ribbon的負載均衡算法,就必須得知道下面這個接口
IRule接口
-
Ribbon的負載均衡算法需要實現IRule接口,該接口中和核心方法就是choose()方法,對服務提供者的選擇方式就是在該方法中體現的,該方法就是在所有可用的方法集合中選擇一個可用的服務
7個均衡算法
-
RoundRobbinRule:輪詢策略
-
BestAvailableRule:選擇並發量最小的服務策略
-
AvailabilityFilteringRule:過濾掉不可用的provider,在剩余的provider中采用輪詢策略
-
ZoneAvoidanceRule:復合判斷provider所在區域的性能及可用性選擇服務器
-
RandomRule:隨機策略
-
RetryRule:先按照輪詢的策略選擇服務。若獲取失敗則在指定的時間內重試,默認500毫秒
-
WeightedResponseTimeRule:權重響應時間策略,根據每個provider的響應時間計算權重,響應時間越快,被選中的幾率就越高,剛啟動時采用輪詢策略,后面就轉換為根據選擇選擇
更換負載均衡算法也很簡單,在我們的啟動類下將其被我spring容器所管理即可
當然也是使用配置方式配置指定的負載均衡策略:
03-consumer:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
格式就是 :{服務名稱}.ribbon.NFLoadBalancerRuleClassName`,值就是IRule的實現類。
當然這是在他規定的集中負載均衡算法中選取,我們也可以自定義算法,但我覺得沒有必要,你說是不是?官方都給了7中算法,我們還去自定義算法,是不是不太合適?如果非要使用自定義算法的話,實現IRule接口,重寫方法,將其給Spring容器管理。
SpringCloud的另一組件:OpenFeign + Hystrix
在學習這個組件之前,有兩個專業性名詞需要我們在一起學習一下
-
服務熔斷
-
服務雪崩:是一種因服務提供者的不可用導致服務調用者的不可用,並將不可用逐漸放大的過程。
-
雪崩效應:服務提供者因為不可用或者延遲高在成的服務調用者的請求線程,阻塞的請求會占用系統的固有線程數、IO等資源,當這樣的被阻塞的線程越來越多的時候,系統瓶頸造成業務系統炎黃崩潰,這種現象成為雪崩效應
-
熔斷機制:熔斷機制是服務雪崩的一種有效解決方案,當服務消費者請求的服務提供者因為宕機或者網絡延遲高等原因造車過暫時不能提供服務時,采用熔斷機制,當我們的請求在設定的最常等待響應閥值最大時仍然沒有得到服務提供者的響應的時候,系統將通過斷路器直接將吃請求鏈路斷開,這種解決方案稱為熔斷機制
-
-
服務降級
-
理解了上面所說的服務熔斷相關的知識,想想在服務熔斷發生時,該請求線程仍然占用着系統的資源,為了解決這個問題,在編寫消費者[重點:消費者]端代碼時就設置了預案,當服務熔斷發生時,直接響應有服務消費者自己給出的一種默認的,臨時的處理方案,再次注意"這是由服務的消費者提供的",服務的質量就相對降級了,這就是服務降級,當然服務降級可以發生在系統自動因為服務提供者斷供造成的服務熔斷,也可運用在為了保證核心業務正常運行,將一些不重要的服務暫時停用,不重要的服務的響應都由消費者給出,將更多的系統資源用作去支撐核心服務的正常運行,比如雙11期間,收貨地址服務全部采用默認,不能再修改,就是收貨地址服務停了,把更多的系統資源用作去支撐購物車、下單、支付等服務去了。
-
我們再簡單歸納一下:簡單來說就是服務提供者斷供了,其一為了保證系統可以正常運行,其二為了增加用戶的體驗,由服務的消費者調用自己的方法作為返回,暫時給用戶響應結果的一種解決方案;
-
Hystrix的中文意思是:豪豬,在我們的應用中Hystrix的作用就是充當斷路器
關於單獨的Hystrix就不做過多的闡述,因為他雖然可以單獨使用,但在我們SpringCloud中,Feign默認也有對Hystrix的集成,只不過默認情況下是關閉的,需要我們手動開啟
關於Fallback的配置這就得我們自己手動配置了,如下
首先服務消費者引入相關依賴:
<!--Hystrix-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
<version>2.0.1.RELEASE</version>
</dependency>
application.yml的配置如下:Feign
feign: #feign對hystrix的支持開啟,注意這些屬性idea不會給出提示
hystrix:
enabled: true
hystrix: #服務響應慢,無響應的熔斷保護機制
command:
default:
execution:
isolation:
thread:
timeoutInMillisecond: 5000 # 熔斷超時時長:默認50000ms
服務消費者編寫回滾函數
/** * 定義一個類,實現FallbackFactory,並將Feign客戶端傳入 * 通過下面create方法內部使用內部類方式做一個Feign客戶端的實現 * 當我們的某個方法服務不可用時,就調用我們內部類中對應的方法 */ @Component public class EmployeeServiceFallback implements FallbackFactory<EmployeeService> { @Override public EmployeeService create(Throwable throwable) {
return new EmployeeService(){ //待會兒我們就以這個方法為列進行測試
@Override public Employee getEmployeeById(int id) { Employee employee = new Employee(); employee.setName("No this Employee"); employee.setDbase("No this Employee"); return employee; } @Override public boolean saveEmployee(Employee employee) { System.out.println("服務降級,saveEmployee服務不可用,請稍后再試"); return false; } @Override public boolean removeEmployee(int id) { System.out.println("服務降級,removeEmployee服務不可用,請稍后再試"); return false; } @Override public boolean modiflyEmployee(Employee employee) { System.out.println("服務降級,modiflyEmployee服務不可用,請稍后再試"); return false; } @Override public List<Employee> listAllEmployee() { System.out.println("服務降級,listAllEmployee服務不可用,請稍后再試"); return null; } }; } }
FeignClient如下:
@Service //fei客戶端 第一個參數為服務提供者的應用名 第二個為回滾的指定實現所在
@FeignClient(value="03-provider",fallbackFactory = EmployeeServiceFallback.class) @RequestMapping("/provider/employee") public interface EmployeeService { @PostMapping("/save") boolean saveEmployee(Employee employee); @DeleteMapping("/del/{id}") boolean removeEmployee(@PathVariable("id") int id); @PostMapping("/update") boolean modiflyEmployee(Employee employee); @GetMapping("/get/{id}") Employee getEmployeeById(@PathVariable("id")int id); @GetMapping("/list") List<Employee> listAllEmployee(); }
然后就是啟動類上開啟服務降級
@SpringBootApplication @EnableDiscoveryClient //開啟Eureka客戶端
@EnableFeignClients(basePackages = "com.ccl.test.service") //開啟Feign,並指定Service所在的包
@EnableCircuitBreaker //開啟Hystrix的服務降級
public class ConsumerRun { public static void main(String[] args) { SpringApplication.run(ConsumerRun.class, args); } }
好了服務的消費者方就已經准備好了,就差服務方因為網絡延遲等原因造成沒有響應了,我們在服務的提供方制造一個異常,我們將異常寫進我們待會服務調用會使用到的方法中,造成服務不可用
@Override public Employee getEmployeeById(int id) { if (repository.existsById(id)) { //手動創造一個異常,待會兒調用就會拋出異常造成服務不可用
int flag = 1 / 0; return repository.getOne(id); } Employee employee = new Employee(); employee.setName("no this employee"); return null; }
雙雙啟動項目和Eureka,進行測試
SpringCloud的另一組件:網關Zuul
經過前面的學習,目前我們對於SpringCloud基本技術棧已經快一半了,我們使用Eureka實現服務的注冊與發現,服務之間通過Feignj進行調用,並通過Ribbon實現負載均衡,在服務的過程中,可能服務的提供者會斷供,我們通過Hystrix的熔斷機制實現了服務的降級和故障的蔓延,下面我們將學習網關Zuul和外部化配置管理的springCloud config,學完這兩個SpringCloud基本上的技術棧就算入門了,可以正常做開發了。
-
Zuu:服務網關,一個微服務中不可獲取的組件,通過Zuul網關統一向外提供Rest API,服務網關除了具備服務路由、負載均衡的功能之外,他還得具備權限控制等功能,在SpringCloud中,Zuul就是處於對外訪問最前端的地方:
-
Zuul加入后的架構
-
-
不管是來自於客戶端(PC或移動端)的請求,還是服務內部調用。一切對服務的請求都會經過Zuul這個網關,然后再由網關來實現 鑒權、動態路由等等操作。Zuul就是我們服務的統一入口。
快速搭建Zuul
我們就不一步一步來了,直接在懟最終版的時候再補充,首先依賴:
這里說一下,Zuul是必須的,然后Eureka的依賴是為了Zuul去Eureka拉取服務所以這里就需要這連個依賴
<dependencies>
<!--Zuul-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
<version>2.0.1.RELEASE</version>
</dependency>
<!--Eureka-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
<version>2.0.1.RELEASE</version>
</dependency>
</dependencies>
啟動類:
@SpringBootApplication @EnableZuulProxy //開啟Zuul的網關功能
@EnableDiscoveryClient //開啟Eureka的客戶端發現功能
public class zuulRun { public static void main(String[]args) { SpringApplication.run(zuulRun.class,args); } }
然后就是配置文件application.yml
server: port: 9000 spring: application: name: zuul-gateway eureka: client: registry-fetch-interval-seconds: 5 # 獲取服務列表的周期:5s service-url: #Eureka的地址 defaultZone: Http://localhost:8000/eureka
instance: prefer-ip-address: true #當其它服務獲取地址時提供ip而不是hostname ip-address: 127.0.0.1 #指定自己的ip信息,不指定的話會自己尋找 zuul: routes: 03-provider: #這里的可以隨便寫 serviceId: 03-provider #指定服務名稱 path: /haha/** #這里是映射路徑
前面的配置我相信大家心里都知道的七七八八了吧,我們就說說zuul下的配置
按照進化版本,我這里也一一羅列出來
-
原始版本,了解即可,無需Eureka
zuul: routes: 03-provider: # 這里是路由id,隨意 url: http://127.0.0.1:8090 # 映射路徑對應的實際url地址 path: /03-provider/** # 這里是映射路徑
講解:我們將復合path規則的一切請求都代理到url參數指定的地址
-
初級進化,了解即可,加持Eureka
zuul: routes: 03-provider: #這里的可以隨便寫 serviceId: 03-provider #指定服務名稱 path: /haha/** #這里是映射路徑
講解:因為加持了Eureka,我們可以去Eureka去獲取服務的地址信息,通過服務名來訪問
到了這一步以為是通過服務名來獲取服務的,所以集成了Ribon的負載均衡功能
因為我們的Zuul的端口為9000,:http://localhost:9000/haha/provider/employee/get/1
網關地址 + 映射路徑 + Controller的@RequestMapping的映射規則,完成訪問
-
究級進化,掌握
zuul: routes: 03-PROVIDER : /03-provider/**
解釋:這個就需要掌握了,后面我們常用的的
默認情況下,我們的路由名和服務名往往是一致的,因此Zuul提供了一套新的規則,就如上面那樣
服務名 : 映射路徑 ;訪問路徑為:http://localhost:9000/03-provider/provider/employee/get/1
因為這個這個規則有個特點,就是映射路徑就是服務名本身,所有即使我們不做配置,也是能訪問的,但有時候還是要配,要配一些其他的屬性
在究級進化的基礎上,我們還有一些其他的屬性可以配置
再添加兩個常用的屬性:
sensitiveHeaders:敏感頭設置,默認Zuul是將cookie攔截再黑名單中的,這樣設置為空,表示不過濾
ignoreHeaders:可以設置過濾的頭信息,這里我們設置為空,表示不過濾任何頭
Zuul的過濾器
Zuul最為網關使他的一個功能之一,我們想實現請求的鑒權,就是通過Zuul提供的過濾器來實現的,下面我們來認識認識一下
-
ZuulFilter:過濾器的頂級父類
@Component public class loginFilter extends ZuulFilter { @Override public String filterType() { //返回過濾器的類型[pre、routing、post、error] //請求在被路由之前、之時調用,在routing之后error之前,處理請求發生錯誤時 return null; } @Override public int filterOrder() { //返回int值表示該過濾器的的優先級,越小越高 return 0; } @Override public boolean shouldFilter() { //是否啟動該過濾器 return false; } @Override public Object run() throws ZuulException { //過濾器的具體業務邏輯 return null; } }
-
下面我們再看一官網的提供的請求生命周期圖,表現了一個請求在各個過濾器的執行順序
-
正常流程:
-
請求到達首先會經過pre類型過濾器,而后到達routing類型,進行路由,請求就到達真正的服務提供者,執行請求,返回結果后,會到達post過濾器。而后返回響應。
-
-
異常流程:
-
整個過程中,pre或者routing過濾器出現異常,都會直接進入error過濾器,再error處理完畢后,會將請求交給POST過濾器,最后返回給用戶。
-
如果是error過濾器自己出現異常,最終也會進入POST過濾器,而后返回。
-
如果是POST過濾器出現異常,會跳轉到error過濾器,但是與pre和routing不同的時,請求不會再到達POST過濾器了。
-
-
使用場景
-
請求鑒權:一般放在路由之前,如果發現沒有權限,可以攔截+轉發,比如淘寶沒登錄查看購物車,攔截轉發到登錄頁面
-
異常處理:一般會放在erroe類型和post類型過濾器中結合來處理
-
服務時長統計:post的現在時 - pre的現在時
-
上面我們自定義了過濾器,下面我們就賴模擬一個登錄的校驗,如果請求中有access-token參數,我們就放行,如果沒有我們就攔截不做其他表示
@Component public class loginFilter extends ZuulFilter { @Override public String filterType() { //返回過濾器的類型[pre、routing、post、error] //請求在被路由之前、之時調用,在routing之后error之前,處理請求發生錯誤時 return "pre"; } @Override public int filterOrder() { //返回int值表示該過濾器的的優先級,越小越高 return 1; } @Override public boolean shouldFilter() { //是否啟動該過濾器 return true; } @Override public Object run() throws ZuulException { //過濾器的具體業務邏輯 RequestContext context = RequestContext.getCurrentContext(); String token = context.getRequest().getParameter("login-token"); if (token == null || "".equals(token.trim())){ context.setSendZuulResponse(false); //返回401狀態碼,也可以重定向到某個頁面 context.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value()); System.out.println("攔截到一個請求,請求處理"); } //校驗通過,可以把用戶信息啥的方法放到LocalThread等操作 return null; } }
准備就緒,開始訪問,沒有token時 和有Token訪問時:http://localhost:9000/03-provider/provider/employee/get/1
我們把login-token帶上,進行測試:http://localhost:9000/03-provider/provider/employee/get/1?login-token=123
負載均衡和熔斷的支持
Zuul中默認就已經集成了Ribbon負載均衡和Hystix熔斷機制。不配置的話都走的默認值,不妥: