隨着互聯網的發展,網站 應用的規模不斷擴大,需求的激增嗎,帶來了技術上的革命,系統架構也在不斷的演進。從之前的單一應用,到垂直拆分,再到分佈式服務,再到SOA(面向服務的架構),再到微服務架構,今天的SpringCloud和另外一個阿里的Dubbo都是微服務架構目前比較火的兩個框架;Spring善於集成,這個你們都是知道的,把世界上最好的框架拿過來,集成到本身的項目中,SpringCloud也仍是同樣,把當前很是流行的技術整合到一塊兒,就成就了今天的SpringCloud,前端
好比他集成了之前是一家刻光盤後來轉技術的公司的諸多技術:java
Eureka:註冊中心mysql
Zuul :網關git
Ribbon:負載均衡web
Feign :遠程服務調用算法
Hystrix:熔斷器spring
......以上部分也是咱們玩SpringCloud的核心技術。還有諸多諸多......sql
單一職責,微服務中每個服務都是一個惟一的業務,也就是說一個服務只幹一件事情;數據庫
微服務服務拆分粒度很小,可是五臟俱全apache
微服務通常向外暴露Rest風格服務接口api,不關心服務的技術實現,能夠是Java,能夠是其餘語言
每一個服務之間互相獨立,互不干擾:
面向服務,提供Rest接口,使用什麼技術無人干涉
先後端分離,提供統一Rest接口,沒必要在爲PC,移動端單獨開發接口
數據庫分離,每一個服務都是用本身的數據源
部署獨立,每一個服務都是獨立的組件,可複用,可替換,下降耦合,容易維護;
不管是Dubbo仍是SpringCloud都會涉及到服務間的遠程調用,目前常見的服務遠程調用方式就一下兩種
RPC:Dubbo是其使用者,自定義數據格式,基於原生TCP通訊,速度快,效率高
Http:Http實際上是一種網絡傳輸協議,也是基於TCP,但他規定了數據的傳輸格式,如今瀏覽器和服務端基本使用的都是Http協議,他也能夠用來做爲遠程服務調用,缺點就是裏面封裝的數據過多,不信你打開瀏覽器,看F12裏面的數據,是否是有不少字段好比請求頭那一堆堆...
RPC,即 Remote Procedure Call(遠程過程調用),是一個計算機通訊協議。 該協議容許運行於一臺計算機的程序調用另外一臺計算機的子程序,說得通俗一點就是:A計算機提供一個服務,B計算機能夠像調用本地服務那樣調用A計算機的服務,RPC調用流程圖以下:
Http協議:超文本傳輸協議,是一種應用層協議。規定了網絡傳輸的請求格式、響應格式、資源定位和操做的方式等。可是底層採用什麼網絡傳輸協議,並無規定,不過如今都是採用TCP協議做爲底層傳輸協議。例如咱們經過瀏覽器訪問網站,就是經過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測試工具,發起請求驗證服務消費者是否經過調用服務提供者的服務,間接的操做數據庫數據,完成服務的遠程調用
在獲取一條數據試試
沒Get到也不要緊,由於上面這個東西,太原生了,上面的Demo存在明顯的短板:
好比消費服務時的訪問路徑、硬編碼在邏輯代碼中。後期發生變動不易維護
其次萬一服務的提供者宕機,服務的消費者也不知道
而後就是服務的提供者的集羣,混在均衡得本身實現
Eureka:中文意思"我發現了","我找到了",大家知道Zookeeper嗎,就是Dubbo建議使用的註冊中心那個Zookeeper,兩者就是差很少的,可是Dubbo和Eureka側重點不一樣,Eureka是犧牲了一致性,保證了可用性,而Zookeeper犧牲了可用性,保留了一致性,所謂的CAP的"三二原則",
Eureka的做用這裏也簡單帶過:
【服務的註冊、發現】:負責管理,紀錄服務提供者的信息,服務調用者無需本身尋找服務的,而是把本身的需求告訴Eureka,Eureka就會吧符合你需求的服務告訴你,比如租房中介。
【服務的監控】:服務的提供者和Eureka之間還經過"心跳",保持着聯繫,當某個服務提供方出現了問題,Eureka自會在指定的時間範圍內將其剔除,就比如租房子,房東已經把房子租出去了,中介就會把這個房子排除在本身掌握的房源名單裏
這張圖理解爲一個房東,一箇中介,一個打工仔:
房東有房,把房子託給中介公司幫忙出租,房東和中介保持着聯繫(心跳),若是這個房子房東本身z住不想出租了或者房子漏水暫時不租了,中介第一時間就會知道,而後中止該房子的出租,而打工仔一我的孤苦伶仃的來到一個陌生的城市拼搏,他找到了中介,中介給了他一種表單,裏面羅列了這個中介的全部的房源,看他需求什麼,本身有的話就能夠幫他聯繫上房東,經過這種方式,打工仔身在異鄉但仍然感覺到了家的溫暖。
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; }
咱們經過Eureka獲取到了指定應用名的服務的IP和Port,動態的組成了咱們的請求Rest鏈接,成功的調用了服務的提供者的一個接口
而後咱們去看看咱們的Eureka:
暫時就這麼點東西吧,固然Eureka還支持集羣和負載均衡:
還記得以前的故事嗎?中介給了打工仔一個表單,在實際中也是這樣的,Eureka將全部的服務列表所有給調用方,調用方本身匹配,負載均衡是調用方本身的事,而不是Eureka的事,他只是一個房子的搬運工,負載均衡也很簡單,下面來看看: 在返回RestTemplate的方法上給一個註解 : @LoadBalanced,默認使用的輪詢的方式,也能夠指定爲特定的負載均衡算法
說到集羣,避免單點故障,提升吞吐量,咱們就弄三個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
你們能夠回憶一下以前咱們寫的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了,下面咱們就來學習一下
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提供了點多鐘輪詢算法,常見負載均衡算法好比默認的輪詢,其餘的隨機、響應時間加權算法等;
想要修改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容器管理。
在學習這個組件以前,有兩個專業性名詞須要咱們在一塊兒學習一下
服務熔斷
服務雪崩:是一種因服務提供者的不可用致使服務調用者的不可用,並將不可用逐漸放大的過程。
雪崩效應:服務提供者由於不可用或者延遲高在成的服務調用者的請求線程,阻塞的請求會佔用系統的固有線程數、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基本技術棧已經快一半了,咱們使用Eureka實現服務的註冊與發現,服務之間經過Feignj進行調用,並經過Ribbon實現負載均衡,在服務的過程當中,可能服務的提供者會斷供,咱們經過Hystrix的熔斷機制實現了服務的降級和故障的蔓延,下面咱們將學習網關Zuul和外部化配置管理的springCloud config,學完這兩個SpringCloud基本上的技術棧就算入門了,能夠正常作開發了。
Zuu:服務網關,一個微服務中不可獲取的組件,經過Zuul網關統一貫外提供Rest API,服務網關除了具有服務路由、負載均衡的功能以外,他還得具有權限控制等功能,在SpringCloud中,Zuul就是處於對外訪問最前端的地方:
無論是來自於客戶端(PC或移動端)的請求,仍是服務內部調用。一切對服務的請求都會通過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提供的過濾器來實現的,下面咱們來認識認識一下
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熔斷機制。不配置的話都走的默認值,不妥: