0.學習目標
瞭解系統架構的演變
瞭解RPC與Http的區別
掌握HttpClient的簡單使用
知道什麼是SpringCloud
獨立搭建Eureka註冊中心
獨立配置Robbin負載均衡
-Xms128m -Xmx128mhtml
1.系統架構演變
隨着互聯網的發展,網站應用的規模不斷擴大。需求的激增,帶來的是技術上的壓力。系統架構也所以也不斷的演進、升級、迭代。從單一應用,到垂直拆分,到分佈式服務,到SOA,以及如今火熱的微服務架構,還有在Google帶領下來勢洶涌的Service Mesh。咱們究竟是該乘坐微服務的船隻駛向遠方,仍是偏安一隅得過且過?前端
其實生活不止眼前的苟且,還有詩和遠方。因此咱們今天就回顧歷史,看一看系統架構演變的歷程;把握如今,學習如今最火的技術架構;展望將來,爭取成爲一名優秀的Java工程師。java
1.1. 集中式架構
當網站流量很小時,只需一個應用,將全部功能都部署在一塊兒,以減小部署節點和成本。此時,用於簡化增刪改查工做量的數據訪問框架(ORM)是影響項目開發的關鍵。mysql
存在的問題:程序員
代碼耦合,開發維護困難
沒法針對不一樣模塊進行鍼對性優化
沒法水平擴展
單點容錯率低,併發能力差
1.2.垂直拆分
當訪問量逐漸增大,單一應用沒法知足需求,此時爲了應對更高的併發和業務需求,咱們根據業務功能對系統進行拆分:web
優勢:算法
系統拆分實現了流量分擔,解決了併發問題
能夠針對不一樣模塊進行優化
方便水平擴展,負載均衡,容錯率提升
缺點:spring
系統間相互獨立,會有不少重複開發工做,影響開發效率
1.3.分佈式服務
當垂直應用愈來愈多,應用之間交互不可避免,將核心業務抽取出來,做爲獨立的服務,逐漸造成穩定的服務中心,使前端應用能更快速的響應多變的市場需求。此時,用於提升業務複用及整合的分佈式調用是關鍵。sql
優勢:數據庫
將基礎服務進行了抽取,系統間相互調用,提升了代碼複用和開發效率
缺點:
系統間耦合度變高,調用關係錯綜複雜,難以維護
1.4.服務治理(SOA)
當服務愈來愈多,容量的評估,小服務資源的浪費等問題逐漸顯現,此時需增長一個調度中心基於訪問壓力實時管理集羣容量,提升集羣利用率。此時,用於提升機器利用率的資源調度和治理中心(SOA)是關鍵
之前出現了什麼問題?
服務愈來愈多,須要管理每一個服務的地址
調用關係錯綜複雜,難以理清依賴關係
服務過多,服務狀態難以管理,沒法根據服務狀況動態管理
服務治理要作什麼?
服務註冊中心,實現服務自動註冊和發現,無需人爲記錄服務地址
服務自動訂閱,服務列表自動推送,服務調用透明化,無需關心依賴關係
動態監控服務狀態監控報告,人爲控制服務狀態
缺點:
服務間會有依賴關係,一旦某個環節出錯會影響較大
服務關係複雜,運維、測試部署困難,不符合DevOps思想
1.5.微服務
前面說的SOA,英文翻譯過來是面向服務。微服務,彷佛也是服務,都是對系統進行拆分。所以二者很是容易混淆,但其實缺有一些差異:
微服務的特色:
單一職責:微服務中每個服務都對應惟一的業務能力,作到單一職責
微:微服務的服務拆分粒度很小,例如一個用戶管理就能夠做爲一個服務。每一個服務雖小,但「五臟俱全」。
面向服務:面向服務是說每一個服務都要對外暴露服務接口API。並不關心服務的技術實現,作到與平臺和語言無關,也不限定用什麼技術實現,只要提供Rest的接口便可。
自治:自治是說服務間互相獨立,互不干擾
團隊獨立:每一個服務都是一個獨立的開發團隊,人數不能過多。
技術獨立:由於是面向服務,提供Rest接口,使用什麼技術沒有別人干涉
先後端分離:採用先後端分離開發,提供統一Rest接口,後端不用再爲PC、移動段開發不一樣接口
數據庫分離:每一個服務都使用本身的數據源
部署獨立,服務間雖然有調用,但要作到服務重啓不影響其它服務。有利於持續集成和持續交付。每一個服務都是獨立的組件,可複用,可替換,下降耦合,易維護
微服務結構圖:
2.遠程調用方式
不管是微服務仍是SOA,都面臨着服務間的遠程調用。那麼服務間的遠程調用方式有哪些呢?
常見的遠程調用方式有如下幾種:
RPC:Remote Produce Call遠程過程調用,相似的還有RMI。自定義數據格式,基於原生TCP通訊,速度快,效率高。早期的webservice,如今熱門的dubbo,都是RPC的典型
Http:http實際上是一種網絡傳輸協議,基於TCP,規定了數據傳輸的格式。如今客戶端瀏覽器與服務端通訊基本都是採用Http協議。也能夠用來進行遠程服務調用。缺點是消息封裝臃腫。
如今熱門的Rest風格,就能夠經過http協議來實現。
2.1.認識RPC
RPC,即 Remote Procedure Call(遠程過程調用),是一個計算機通訊協議。 該協議容許運行於一臺計算機的程序調用另外一臺計算機的子程序,而程序員無需額外地爲這個交互做用編程。說得通俗一點就是:A計算機提供一個服務,B計算機能夠像調用本地服務那樣調用A計算機的服務。
經過上面的概念,咱們能夠知道,實現RPC主要是作到兩點:
實現遠程調用其餘計算機的服務
要實現遠程調用,確定是經過網絡傳輸數據。A程序提供服務,B程序經過網絡將請求參數傳遞給A,A本地執行後獲得結果,再將結果返回給B程序。這裏須要關注的有兩點:
1)採用何種網絡通信協議?
如今比較流行的RPC框架,都會採用TCP做爲底層傳輸協議
2)數據傳輸的格式怎樣?
兩個程序進行通信,必須約定好數據傳輸格式。就比如兩我的聊天,要用同一種語言,不然沒法溝通。因此,咱們必須定義好請求和響應的格式。另外,數據在網路中傳輸須要進行序列化,因此還須要約定統一的序列化的方式。
像調用本地服務同樣調用遠程服務
若是僅僅是遠程調用,還不算是RPC,由於RPC強調的是過程調用,調用的過程對用戶而言是應該是透明的,用戶不該該關心調用的細節,能夠像調用本地服務同樣調用遠程服務。因此RPC必定要對調用的過程進行封裝
RPC調用流程圖:
想要了解詳細的RPC實現,給你們推薦一篇文章:本身動手實現RPC
2.2.認識Http
Http協議:超文本傳輸協議,是一種應用層協議。規定了網絡傳輸的請求格式、響應格式、資源定位和操做的方式等。可是底層採用什麼網絡傳輸協議,並無規定,不過如今都是採用TCP協議做爲底層傳輸協議。說到這裏,你們可能以爲,Http與RPC的遠程調用很是像,都是按照某種規定好的數據格式進行網絡通訊,有請求,有響應。沒錯,在這點來看,二者很是類似,可是仍是有一些細微差異。
RPC並無規定數據傳輸格式,這個格式能夠任意指定,不一樣的RPC協議,數據格式不必定相同。
Http中還定義了資源定位的路徑,RPC中並不須要
最重要的一點:RPC須要知足像調用本地服務同樣調用遠程服務,也就是對調用過程在API層面進行封裝。Http協議沒有這樣的要求,所以請求、響應等細節須要咱們本身去實現。
優勢:RPC方式更加透明,對用戶更方便。Http方式更靈活,沒有規定API和語言,跨語言、跨平臺
缺點:RPC方式須要在API層面進行封裝,限制了開發的語言環境。
例如咱們經過瀏覽器訪問網站,就是經過Http協議。只不過瀏覽器把請求封裝,發起請求以及接收響應,解析響應的事情都幫咱們作了。若是是不經過瀏覽器,那麼這些事情都須要本身去完成。
2.3.如何選擇?
既然兩種方式均可以實現遠程調用,咱們該如何選擇呢?
速度來看,RPC要比http更快,雖然底層都是TCP,可是http協議的信息每每比較臃腫,不過能夠採用gzip壓縮。
難度來看,RPC實現較爲複雜,http相對比較簡單
靈活性來看,http更勝一籌,由於它不關心實現細節,跨平臺、跨語言。
所以,二者都有不一樣的使用場景:
若是對效率要求更高,而且開發過程使用統一的技術棧,那麼用RPC仍是不錯的。
若是須要更加靈活,跨語言、跨平臺,顯然http更合適
那麼咱們該怎麼選擇呢?
微服務,更增強調的是獨立、自治、靈活。而RPC方式的限制較多,所以微服務框架中,通常都會採用基於Http的Rest風格服務。
3.Http客戶端工具
既然微服務選擇了Http,那麼咱們就須要考慮本身來實現對請求和響應的處理。不過開源世界已經有不少的http客戶端工具,可以幫助咱們作這些事情,例如:
HttpClient
OKHttp
URLConnection
接下來,咱們就一塊兒瞭解一款比較流行的客戶端工具:HttpClient
3.1.HttpClient
3.1.1.介紹
HttpClient是Apache公司的產品,是Http Components下的一個組件。
特色:
基於標準、純淨的Java語言。實現了Http1.0和Http1.1
以可擴展的面向對象的結構實現了Http所有的方法(GET, POST, PUT, DELETE, HEAD, OPTIONS, and TRACE)
支持HTTPS協議。
經過Http代理創建透明的鏈接。
自動處理Set-Cookie中的Cookie。
3.1.2.使用
咱們導入課前資料提供的demo工程:《http-demo》
發起get請求:
@Test public void testGet() throws IOException { HttpGet request = new HttpGet("http://www.baidu.com"); String response = this.httpClient.execute(request, new BasicResponseHandler()); System.out.println(response); }
發起Post請求:
@Test public void testPost() throws IOException { HttpPost request = new HttpPost(「http://www.oschina.net/」); request.setHeader(「User-Agent」, 「Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36」); String response = this.httpClient.execute(request, new BasicResponseHandler()); System.out.println(response); }
嘗試訪問昨天編寫的接口:http://localhost/hello
這個接口返回一個User對象
@Test public void testGetPojo() throws IOException { HttpGet request = new HttpGet(「http://localhost/hello」); String response = this.httpClient.execute(request, new BasicResponseHandler()); System.out.println(response); }
咱們實際獲得的是一個json字符串:
「id」: 8,
「userName」: 「liuyan」,
「password」: 「123456」,
「name」: 「柳巖」,
「age」: 21,
「sex」: 2,
「birthday」: 「1995-08-07T16:00:00.000+0000」,
「created」: 「2014-09-20T03:41:15.000+0000」,
「updated」: 「2014-09-20T03:41:15.000+0000」,
「note」: 「柳巖同窗在傳智播客學表演」
}
若是想要獲得對象,咱們還須要手動進行Json反序列化,這一點比較麻煩。
3.1.3.Json轉換工具
HttpClient請求數據後是json字符串,須要咱們本身把Json字符串反序列化爲對象,咱們會使用JacksonJson工具來實現。
JacksonJson是SpringMVC內置的json處理工具,其中有一個ObjectMapper類,能夠方便的實現對json的處理:
對象轉json
// json處理工具 private ObjectMapper mapper = new ObjectMapper(); @Test public void testJson() throws JsonProcessingException { User user = new User(); user.setId(8L); user.setAge(21); user.setName(「柳巖」); user.setUserName(「liuyan」); // 序列化 String json = mapper.writeValueAsString(user); System.out.println("json = " + json); }
結果:
json轉普通對象
// json處理工具 private ObjectMapper mapper = new ObjectMapper(); @Test public void testJson() throws IOException { User user = new User(); user.setId(8L); user.setAge(21); user.setName(「柳巖」); user.setUserName(「liuyan」); // 序列化 String json = mapper.writeValueAsString(user); // 反序列化,接收兩個參數:json數據,反序列化的目標類字節碼 User result = mapper.readValue(json, User.class); System.out.println("result = " + result); }
結果:
json轉集合
json轉集合比較麻煩,由於你沒法同時把集合的class和元素的class同時傳遞到一個參數。
所以Jackson作了一個類型工廠,用來解決這個問題:
// json處理工具 private ObjectMapper mapper = new ObjectMapper(); @Test public void testJson() throws IOException { User user = new User(); user.setId(8L); user.setAge(21); user.setName(「柳巖」); user.setUserName(「liuyan」); // 序列化,獲得對象集合的json字符串 String json = mapper.writeValueAsString(Arrays.asList(user, user)); // 反序列化,接收兩個參數:json數據,反序列化的目標類字節碼 List<User> users = mapper.readValue(json, mapper.getTypeFactory().constructCollectionType(List.class, User.class)); for (User u : users) { System.out.println("u = " + u); } }
結果:
json轉任意複雜類型
當對象泛型關係複雜時,類型工廠也很差使了。這個時候Jackson提供了TypeReference來接收類型泛型,而後底層經過反射來獲取泛型上的具體類型。實現數據轉換。
// json處理工具 private ObjectMapper mapper = new ObjectMapper(); @Test public void testJson() throws IOException { User user = new User(); user.setId(8L); user.setAge(21); user.setName(「柳巖」); user.setUserName(「liuyan」); // 序列化,獲得對象集合的json字符串 String json = mapper.writeValueAsString(Arrays.asList(user, user)); // 反序列化,接收兩個參數:json數據,反序列化的目標類字節碼 List<User> users = mapper.readValue(json, new TypeReference<List<User>>(){}); for (User u : users) { System.out.println("u = " + u); } }
結果:
3.3.Spring的RestTemplate
Spring提供了一個RestTemplate模板工具類,對基於Http的客戶端進行了封裝,而且實現了對象與json的序列化和反序列化,很是方便。RestTemplate並無限定Http的客戶端類型,而是進行了抽象,目前經常使用的3種都有支持:
HttpClient
OkHttp
JDK原生的URLConnection(默認的)
首先在項目中註冊一個RestTemplate對象,能夠在啓動類位置註冊:
@SpringBootApplication public class HttpDemoApplication { public static void main(String[] args) { SpringApplication.run(HttpDemoApplication.class, args); } @Bean public RestTemplate restTemplate() { // 默認的RestTemplate,底層是走JDK的URLConnection方式。 return new RestTemplate(); } }
在測試類中直接@Autowired注入:
@RunWith(SpringRunner.class) @SpringBootTest(classes = HttpDemoApplication.class) public class HttpDemoApplicationTests { @Autowired private RestTemplate restTemplate; @Test public void httpGet() { User user = this.restTemplate.getForObject("http://localhost/hello", User.class); System.out.println(user); } }
經過RestTemplate的getForObject()方法,傳遞url地址及實體類的字節碼,RestTemplate會自動發起請求,接收響應,而且幫咱們對響應結果進行反序列化。
學習完了Http客戶端工具,接下來就能夠正式學習微服務了。
4.初始SpringCloud
微服務是一種架構方式,最終確定須要技術架構去實施。
微服務的實現方式不少,可是最火的莫過於Spring Cloud了。爲何?
後臺硬:做爲Spring家族的一員,有整個Spring全家桶靠山,背景十分強大。
技術強:Spring做爲Java領域的前輩,能夠說是功力深厚。有強力的技術團隊支撐,通常人還真比不了
羣衆基礎好:能夠說大多數程序員的成長都伴隨着Spring框架,試問:如今有幾家公司開發不用Spring?SpringCloud與Spring的各個框架無縫整合,對你們來講一切都是熟悉的配方,熟悉的味道。
使用方便:相信你們都體會到了SpringBoot給咱們開發帶來的便利,而SpringCloud徹底支持SpringBoot的開發,用不多的配置就能完成微服務框架的搭建
4.1.簡介
SpringCloud是Spring旗下的項目之一,官網地址:http://projects.spring.io/spring-cloud/
Spring最擅長的就是集成,把世界上最好的框架拿過來,集成到本身的項目中。
SpringCloud也是同樣,它將如今很是流行的一些技術整合到一塊兒,實現了諸如:配置管理,服務發現,智能路由,負載均衡,熔斷器,控制總線,集羣狀態等等功能。其主要涉及的組件包括:
netflix
Eureka:註冊中心
Zuul:服務網關
Ribbon:負載均衡
Feign:服務調用
Hystix:熔斷器
以上只是其中一部分,架構圖:
4.2.版本
SpringCloud的版本命名比較特殊,由於它不是一個組件,而是許多組件的集合,它的命名是以A到Z的爲首字母的一些單詞組成:
咱們在項目中,會是以Finchley的版本。
其中包含的組件,也都有各自的版本,以下表:
Component Edgware.SR3 Finchley.RC1 Finchley.BUILD-SNAPSHOT
spring-cloud-aws 1.2.2.RELEASE 2.0.0.RC1 2.0.0.BUILD-SNAPSHOT
spring-cloud-bus 1.3.2.RELEASE 2.0.0.RC1 2.0.0.BUILD-SNAPSHOT
spring-cloud-cli 1.4.1.RELEASE 2.0.0.RC1 2.0.0.BUILD-SNAPSHOT
spring-cloud-commons 1.3.3.RELEASE 2.0.0.RC1 2.0.0.BUILD-SNAPSHOT
spring-cloud-contract 1.2.4.RELEASE 2.0.0.RC1 2.0.0.BUILD-SNAPSHOT
spring-cloud-config 1.4.3.RELEASE 2.0.0.RC1 2.0.0.BUILD-SNAPSHOT
spring-cloud-netflix 1.4.4.RELEASE 2.0.0.RC1 2.0.0.BUILD-SNAPSHOT
spring-cloud-security 1.2.2.RELEASE 2.0.0.RC1 2.0.0.BUILD-SNAPSHOT
spring-cloud-cloudfoundry 1.1.1.RELEASE 2.0.0.RC1 2.0.0.BUILD-SNAPSHOT
spring-cloud-consul 1.3.3.RELEASE 2.0.0.RC1 2.0.0.BUILD-SNAPSHOT
spring-cloud-sleuth 1.3.3.RELEASE 2.0.0.RC1 2.0.0.BUILD-SNAPSHOT
spring-cloud-stream Ditmars.SR3 Elmhurst.RELEASE Elmhurst.BUILD-SNAPSHOT
spring-cloud-zookeeper 1.2.1.RELEASE 2.0.0.RC1 2.0.0.BUILD-SNAPSHOT
spring-boot 1.5.10.RELEASE 2.0.1.RELEASE 2.0.0.BUILD-SNAPSHOT
spring-cloud-task 1.2.2.RELEASE 2.0.0.RC1 2.0.0.RELEASE
spring-cloud-vault 1.1.0.RELEASE 2.0.0.RC1 2.0.0.BUILD-SNAPSHOT
spring-cloud-gateway 1.0.1.RELEASE 2.0.0.RC1 2.0.0.BUILD-SNAPSHOT
spring-cloud-openfeign 2.0.0.RC1 2.0.0.BUILD-SNAPSHOT
接下來,咱們就一一學習SpringCloud中的重要組件。
5.微服務場景模擬
首先,咱們須要模擬一個服務調用的場景。方便後面學習微服務架構
5.1.服務提供者
咱們新建一個項目,對外提供查詢用戶的服務。
5.1.1.Spring腳手架建立工程
藉助於Spring提供的快速搭建工具:
填寫項目信息:
添加web依賴:
添加mybatis依賴:
填寫項目位置:
生成的項目結構:
依賴也已經所有自動引入:
<?xml version="1.0" encoding="UTF-8"?> 4.0.0 <groupId>com.leyou.demo</groupId> <artifactId>user-service-demo</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>user-service-demo</name> <description>Demo project for Spring Boot</description> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.1.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.3.2</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>
5.1.2.編寫代碼
添加一個對外查詢的接口:
@RestController @RequestMapping(「user」) public class UserController { @Autowired private UserService userService; @GetMapping("/{id}") public User queryById(@PathVariable("id") Long id) { return this.userService.queryById(id); } } Service: @Service public class UserService { @Autowired private UserMapper userMapper; public User queryById(Long id) { return this.userMapper.selectByPrimaryKey(id); } } mapper: @Mapper public interface UserMapper extends tk.mybatis.mapper.common.Mapper{ } 實體類: @Table(name = 「tb_user」) public class User implements Serializable { private static final long serialVersionUID = 1L; @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; // 用戶名 private String userName; // 密碼 private String password; // 姓名 private String name; // 年齡 private Integer age; // 性別,1男性,2女性 private Integer sex; // 出生日期 private Date birthday; // 建立時間 private Date created; // 更新時間 private Date updated; // 備註 private String note; // 。。。省略getters和setters }
屬性文件,這裏咱們採用了yaml語法,而不是properties:
server: port: 8081 spring: datasource: url: jdbc:mysql://localhost:3306/mydb01 username: root password: 123 hikari: maximum-pool-size: 20 minimum-idle: 10 mybatis: type-aliases-package: com.leyou.userservice.pojo
項目結構:
5.1.3.啓動並測試:
啓動項目,訪問接口:http://localhost:8081/user/7
5.2.服務調用者
5.2.1.建立工程
與上面相似,這裏再也不贅述,須要注意的是,咱們調用user-service的功能,所以不須要mybatis相關依賴了。
pom:
4.0.0
<groupId>com.leyou.demo</groupId> <artifactId>user-consumer-demo</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>user-consumer-demo</name> <description>Demo project for Spring Boot</description> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.1.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- 添加OkHttp支持 --> <dependency> <groupId>com.squareup.okhttp3</groupId> <artifactId>okhttp</artifactId> <version>3.9.0</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>
@SpringBootApplication public class UserConsumerDemoApplication { @Bean public RestTemplate restTemplate() { // 此次咱們使用了OkHttp客戶端,只須要注入工廠便可 return new RestTemplate(new OkHttp3ClientHttpRequestFactory()); } public static void main(String[] args) { SpringApplication.run(UserConsumerDemoApplication.class, args); } }
而後編寫UserDao,注意,這裏不是調用mapper查數據庫,而是經過RestTemplate遠程查詢user-service-demo中的接口:
@Component public class UserDao { @Autowired private RestTemplate restTemplate; public User queryUserById(Long id){ String url = "http://localhost:8081/user/" + id; return this.restTemplate.getForObject(url, User.class); } }
而後編寫user-service,循環查詢UserDAO信息:
@Service public class UserService { @Autowired private UserDao userDao; public List<User> querUserByIds(List<Long> ids){ List<User> users = new ArrayList<>(); for (Long id : ids) { User user = this.userDao.queryUserById(id); users.add(user); } return users; } }
編寫controller:
@RestController @RequestMapping(「consume」) public class ConsumerController { @Autowired private UserService userService; @GetMapping public List<User> consume(@RequestParam("ids") List<Long> ids) { return this.userService.queryUserByIds(ids); } }
5.2.3.啓動測試:
由於咱們沒有配置端口,那麼默認就是8080,咱們訪問:http://localhost:8080/consume?ids=6,7,8
一個簡單的遠程服務調用案例就實現了。
5.3.有沒有問題?
簡單回顧一下,剛纔咱們寫了什麼:
use-service-demo:一個提供根據id查詢用戶的微服務
consumer-demo:一個服務調用者,經過RestTemplate遠程調用user-service-demo
流程以下:
存在什麼問題?
在consumer中,咱們把url地址硬編碼到了代碼中,不方便後期維護
consumer須要記憶user-service的地址,若是出現變動,可能得不到通知,地址將失效
consumer不清楚user-service的狀態,服務宕機也不知道
user-service只有1臺服務,不具有高可用性
即使user-service造成集羣,consumer還需本身實現負載均衡
其實上面說的問題,歸納一下就是分佈式服務必然要面臨的問題:
服務管理
如何自動註冊和發現
如何實現狀態監管
如何實現動態路由
服務如何實現負載均衡
服務如何解決容災問題
服務如何實現統一配置
以上的問題,咱們都將在SpringCloud中獲得答案。
6.Eureka註冊中心
6.1.認識Eureka
首先咱們來解決第一問題,服務的管理。
問題分析
在剛纔的案例中,user-service對外提供服務,須要對外暴露本身的地址。而consumer(調用者)須要記錄服務提供者的地址。未來地址出現變動,還須要及時更新。這在服務較少的時候並不以爲有什麼,可是在如今日益複雜的互聯網環境,一個項目確定會拆分出十幾,甚至數十個微服務。此時若是還人爲管理地址,不只開發困難,未來測試、發佈上線都會很是麻煩,這與DevOps的思想是背道而馳的。
網約車
這就比如是 網約車出現之前,人們出門叫車只能叫出租車。一些私家車想作出租卻沒有資格,被稱爲黑車。而不少人想要約車,可是無奈出租車太少,不方便。私家車不少卻不敢攔,並且滿大街的車,誰知道哪一個纔是願意載人的。一個想要,一個願意給,就是缺乏引子,缺少管理啊。
此時滴滴這樣的網約車平臺出現了,全部想載客的私家車所有到滴滴注冊,記錄你的車型(服務類型),身份信息(聯繫方式)。這樣提供服務的私家車,在滴滴那裏都能找到,一目瞭然。
此時要叫車的人,只須要打開APP,輸入你的目的地,選擇車型(服務類型),滴滴自動安排一個符合需求的車到你面前,爲你服務,完美!
Eureka作什麼?
Eureka就比如是滴滴,負責管理、記錄服務提供者的信息。服務調用者無需本身尋找服務,而是把本身的需求告訴Eureka,而後Eureka會把符合你需求的服務告訴你。
同時,服務提供方與Eureka之間經過「心跳」機制進行監控,當某個服務提供方出現問題,Eureka天然會把它從服務列表中剔除。
這就實現了服務的自動註冊、發現、狀態監控。
6.2.原理圖
基本架構:
Eureka:就是服務註冊中心(能夠是一個集羣),對外暴露本身的地址
提供者:啓動後向Eureka註冊本身信息(地址,提供什麼服務)
消費者:向Eureka訂閱服務,Eureka會將對應服務的全部提供者地址列表發送給消費者,而且按期更新
心跳(續約):提供者按期經過http方式向Eureka刷新本身的狀態
6.3.入門案例
6.3.1.編寫EurekaServer
接下來咱們建立一個項目,啓動一個EurekaServer:
依然使用spring提供的快速搭建工具:
選擇依賴:
完整的Pom文件:
4.0.0
<groupId>com.leyou.demo</groupId> <artifactId>eureka-demo</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>eureka-demo</name> <description>Demo project for Spring Boot</description> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.1.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> <!-- SpringCloud版本,是最新的F系列 --> <spring-cloud.version>Finchley.RC1</spring-cloud.version> </properties> <dependencies> <!-- Eureka服務端 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId> </dependency> </dependencies> <dependencyManagement> <dependencies> <!-- SpringCloud依賴,必定要放到dependencyManagement中,起到管理版本的做用便可 --> <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> <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>
@SpringBootApplication @EnableEurekaServer // 聲明這個應用是一個EurekaServer public class EurekaDemoApplication { public static void main(String[] args) { SpringApplication.run(EurekaDemoApplication.class, args); } }
編寫配置:
server: port: 10086 # 端口 spring: application: name: eureka-server # 應用名稱,會在Eureka中顯示 eureka: client: register-with-eureka: false # 是否註冊本身的信息到EurekaServer,默認是true fetch-registry: false # 是否拉取其它服務的信息,默認是true service-url: # EurekaServer的地址,如今是本身的地址,若是是集羣,須要加上其它Server的地址。 defaultZone: http://127.0.0.1😒{server.port}/eureka
啓動服務,並訪問:http://127.0.0.1:10086/eureka
6.3.2.將user-service註冊到Eureka
註冊服務,就是在服務上添加Eureka的客戶端依賴,客戶端代碼會自動把服務註冊到EurekaServer中。
咱們在user-service-demo中添加Eureka客戶端依賴:
先添加SpringCloud依賴:
經過添加@EnableDiscoveryClient來開啓Eureka客戶端功能
@SpringBootApplication @EnableDiscoveryClient // 開啓EurekaClient功能 public class UserServiceDemoApplication { public static void main(String[] args) { SpringApplication.run(UserServiceDemoApplication.class, args); } }
編寫配置
server: port: 8081 spring: datasource: url: jdbc:mysql://localhost:3306/mydb01 username: root password: 123 hikari: maximum-pool-size: 20 minimum-idle: 10 application: name: user-service # 應用名稱 mybatis: type-aliases-package: com.leyou.userservice.pojo eureka: client: service-url: # EurekaServer地址 defaultZone: http://127.0.0.1:10086/eureka instance: prefer-ip-address: true # 當調用getHostname獲取實例的hostname時,返回ip而不是host名稱 ip-address: 127.0.0.1 # 指定本身的ip信息,不指定的話會本身尋找
注意:
這裏咱們添加了spring.application.name屬性來指定應用名稱,未來會做爲應用的id使用。
不用指定register-with-eureka和fetch-registry,由於默認是true
重啓項目,訪問Eureka監控頁面查看
咱們發現user-service服務已經註冊成功了
6.3.3.消費者從Eureka獲取服務
接下來咱們修改consumer-demo,嘗試從EurekaServer獲取服務。
方法與消費者相似,只須要在項目中添加EurekaClient依賴,就能夠經過服務名稱來獲取信息了!
1)添加依賴:
先添加SpringCloud依賴:
@SpringBootApplication @EnableDiscoveryClient // 開啓Eureka客戶端 public class UserConsumerDemoApplication { @Bean public RestTemplate restTemplate() { return new RestTemplate(new OkHttp3ClientHttpRequestFactory()); } public static void main(String[] args) { SpringApplication.run(UserConsumerDemoApplication.class, args); } }
3)修改配置:
server: port: 8080 spring: application: name: consumer # 應用名稱 eureka: client: service-url: # EurekaServer地址 defaultZone: http://127.0.0.1:10086/eureka instance: prefer-ip-address: true # 當其它服務獲取地址時提供ip而不是hostname ip-address: 127.0.0.1 # 指定本身的ip信息,不指定的話會本身尋找
4)修改代碼,用DiscoveryClient類的方法,根據服務名稱,獲取服務實例:
@Service public class UserService { @Autowired private RestTemplate restTemplate; @Autowired private DiscoveryClient discoveryClient;// Eureka客戶端,能夠獲取到服務實例信息 public List<User> queryUserByIds(List<Long> ids) { List<User> users = new ArrayList<>(); // String baseUrl = "http://localhost:8081/user/"; // 根據服務名稱,獲取服務實例 List<ServiceInstance> instances = discoveryClient.getInstances("user-service"); // 由於只有一個UserService,所以咱們直接get(0)獲取 ServiceInstance instance = instances.get(0); // 獲取ip和端口信息 String baseUrl = "http://"+instance.getHost() + ":" + instance.getPort()+"/user/"; ids.forEach(id -> { // 咱們測試屢次查詢, users.add(this.restTemplate.getForObject(baseUrl + id, User.class)); // 每次間隔500毫秒 try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } }); return users; } }
5)Debug跟蹤運行:
生成的URL:
訪問結果:
6.4.Eureka詳解
接下來咱們詳細講解Eureka的原理及配置。
6.4.1.基礎架構
Eureka架構中的三個核心角色:
服務註冊中心
Eureka的服務端應用,提供服務註冊和發現功能,就是剛剛咱們創建的eureka-demo
服務提供者
提供服務的應用,能夠是SpringBoot應用,也能夠是其它任意技術實現,只要對外提供的是Rest風格服務便可。本例中就是咱們實現的user-service-demo
服務消費者
消費應用從註冊中心獲取服務列表,從而得知每一個服務方的信息,知道去哪裏調用服務方。本例中就是咱們實現的consumer-demo
6.4.2.高可用的Eureka Server
Eureka Server即服務的註冊中心,在剛纔的案例中,咱們只有一個EurekaServer,事實上EurekaServer也能夠是一個集羣,造成高可用的Eureka中心。
服務同步
多個Eureka Server之間也會互相註冊爲服務,當服務提供者註冊到Eureka Server集羣中的某個節點時,該節點會把服務的信息同步給集羣中的每一個節點,從而實現數據同步。所以,不管客戶端訪問到Eureka Server集羣中的任意一個節點,均可以獲取到完整的服務列表信息。
動手搭建高可用的EurekaServer
咱們假設要搭建兩條EurekaServer的集羣,端口分別爲:10086和10087
1)咱們修改原來的EurekaServer配置:
server: port: 10086 # 端口 spring: application: name: eureka-server # 應用名稱,會在Eureka中顯示 eureka: client: service-url: # 配置其餘Eureka服務的地址,而不是本身,好比10087 defaultZone: http://127.0.0.1:10087/eureka
所謂的高可用註冊中心,其實就是把EurekaServer本身也做爲一個服務進行註冊,這樣多個EurekaServer之間就能互相發現對方,從而造成集羣。所以咱們作了如下修改:
刪除了register-with-eureka=false和fetch-registry=false兩個配置。由於默認值是true,這樣就會吧本身註冊到註冊中心了。
把service-url的值改爲了另一臺EurekaServer的地址,而不是本身
2)另一臺配置剛好相反:
server: port: 10087 # 端口 spring: application: name: eureka-server # 應用名稱,會在Eureka中顯示 eureka: client: service-url: # 配置其餘Eureka服務的地址,而不是本身,好比10087 defaultZone: http://127.0.0.1:10086/eureka
注意:idea中一個應用不能啓動兩次,咱們須要從新配置一個啓動器:
而後啓動便可。
3)啓動測試:
4)客戶端註冊服務到集羣
由於EurekaServer不止一個,所以註冊服務的時候,service-url參數須要變化:
eureka: client: service-url: # EurekaServer地址,多個地址以’,'隔開 defaultZone: http://127.0.0.1:10086/eureka,http://127.0.0.1:10087/eureka
6.4.3.服務提供者
服務提供者要向EurekaServer註冊服務,而且完成服務續約等工做。
服務註冊
服務提供者在啓動時,會檢測配置屬性中的:eureka.client.register-with-erueka=true參數是否正確,事實上默認就是true。若是值確實爲true,則會向EurekaServer發起一個Rest請求,並攜帶本身的元數據信息,Eureka Server會把這些信息保存到一個雙層Map結構中。第一層Map的Key就是服務名稱,第二層Map的key是服務的實例id。
服務續約
在註冊服務完成之後,服務提供者會維持一個心跳(定時向EurekaServer發起Rest請求),告訴EurekaServer:「我還活着」。這個咱們稱爲服務的續約(renew);
有兩個重要參數能夠修改服務續約的行爲:
eureka:
instance:
lease-expiration-duration-in-seconds: 90
lease-renewal-interval-in-seconds: 30
lease-renewal-interval-in-seconds:服務續約(renew)的間隔,默認爲30秒
lease-expiration-duration-in-seconds:服務失效時間,默認值90秒
也就是說,默認狀況下每一個30秒服務會向註冊中心發送一次心跳,證實本身還活着。若是超過90秒沒有發送心跳,EurekaServer就會認爲該服務宕機,會從服務列表中移除,這兩個值在生產環境不要修改,默認便可。
可是在開發時,這個值有點太長了,常常咱們關掉一個服務,會發現Eureka依然認爲服務在活着。因此咱們在開發階段能夠適當調小。
eureka: instance: lease-expiration-duration-in-seconds: 10 # 10秒即過時 lease-renewal-interval-in-seconds: 5 # 5秒一次心跳
實例id
先來看一下服務狀態信息:
在Eureka監控頁面,查看服務註冊信息:
在status一列中,顯示如下信息:
UP(1):表明如今是啓動了1個示例,沒有集羣
DESKTOP-2MVEC12:user-service:8081:是示例的名稱(instance-id),
默認格式是:${hostname} + ${spring.application.name} + ${server.port}
instance-id是區分同一服務的不一樣實例的惟一標準,所以不能重複。
咱們能夠經過instance-id屬性來修改它的構成:
eureka:
instance:
instance-id: {spring.application.name}:spring.application.name:{server.port}
重啓服務再試試看:
6.4.4.服務消費者
獲取服務列表
當服務消費者啓動是,會檢測eureka.client.fetch-registry=true參數的值,若是爲true,則會從Eureka Server服務的列表只讀備份,而後緩存在本地。而且每隔30秒會從新獲取並更新數據。咱們能夠經過下面的參數來修改:
eureka:
client:
registry-fetch-interval-seconds: 5
生產環境中,咱們不須要修改這個值。
可是爲了開發環境下,可以快速獲得服務的最新狀態,咱們能夠將其設置小一點。
6.4.5.失效剔除和自我保護
失效剔除
有些時候,咱們的服務提供方並不必定會正常下線,可能由於內存溢出、網絡故障等緣由致使服務沒法正常工做。Eureka Server須要將這樣的服務剔除出服務列表。所以它會開啓一個定時任務,每隔60秒對全部失效的服務(超過90秒未響應)進行剔除。
能夠經過eureka.server.eviction-interval-timer-in-ms參數對其進行修改,單位是毫秒,生成環境不要修改。
這個會對咱們開發帶來極大的不變,你對服務重啓,隔了60秒Eureka才反應過來。開發階段能夠適當調整,好比10S
自我保護
咱們關停一個服務,就會在Eureka面板看到一條警告:
這是觸發了Eureka的自我保護機制。當一個服務未按時進行心跳續約時,Eureka會統計最近15分鐘心跳失敗的服務實例的比例是否超過了85%。在生產環境下,由於網絡延遲等緣由,心跳失敗實例的比例頗有可能超標,可是此時就把服務剔除列表並不穩當,由於服務可能沒有宕機。Eureka就會把當前實例的註冊信息保護起來,不予剔除。生產環境下這頗有效,保證了大多數服務依然可用。
可是這給咱們的開發帶來了麻煩, 所以開發階段咱們都會關閉自我保護模式:
eureka:
server:
enable-self-preservation: false # 關閉自我保護模式(缺省爲打開)
eviction-interval-timer-in-ms: 1000 # 掃描失效服務的間隔時間(缺省爲60*1000ms)
7.負載均衡Robbin
在剛纔的案例中,咱們啓動了一個user-service,而後經過DiscoveryClient來獲取服務實例信息,而後獲取ip和端口來訪問。
可是實際環境中,咱們每每會開啓不少個user-service的集羣。此時咱們獲取的服務列表中就會有多個,到底該訪問哪個呢?
通常這種狀況下咱們就須要編寫負載均衡算法,在多個實例列表中進行選擇。
不過Eureka中已經幫咱們集成了負載均衡組件:Ribbon,簡單修改代碼便可使用。
什麼是Ribbon:
接下來,咱們就來使用Ribbon實現負載均衡。
7.1.啓動兩個服務實例
首先咱們啓動兩個user-service實例,一個8081,一個8082。
Eureka監控面板:
7.2.開啓負載均衡
由於Eureka中已經集成了Ribbon,因此咱們無需引入新的依賴。直接修改代碼:
在RestTemplate的配置方法上添加@LoadBalanced註解:
@Bean @LoadBalanced public RestTemplate restTemplate() { return new RestTemplate(new OkHttp3ClientHttpRequestFactory()); }
修改調用方式,再也不手動獲取ip和端口,而是直接經過服務名稱調用:
@Service public class UserService { @Autowired private RestTemplate restTemplate; @Autowired private DiscoveryClient discoveryClient; public List<User> queryUserByIds(List<Long> ids) { List<User> users = new ArrayList<>(); // 地址直接寫服務名稱便可 String baseUrl = "http://user-service/user/"; ids.forEach(id -> { // 咱們測試屢次查詢, users.add(this.restTemplate.getForObject(baseUrl + id, User.class)); // 每次間隔500毫秒 try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } }); return users; } }
訪問頁面,查看結果:
完美!
7.3.源碼跟蹤
爲何咱們只輸入了service名稱就能夠訪問了呢?以前還要獲取ip和端口。
顯然有人幫咱們根據service名稱,獲取到了服務實例的ip和端口。它就是LoadBalancerInterceptor
咱們進行源碼跟蹤:
繼續跟入execute方法:發現獲取了8082端口的服務
再跟下一次,發現獲取的是8081:
7.4.負載均衡策略
Ribbon默認的負載均衡策略是簡單的輪詢,咱們能夠測試一下:
編寫測試類,在剛纔的源碼中咱們看到攔截中是使用RibbonLoadBalanceClient來進行負載均衡的,其中有一個choose方法,是這樣介紹的:
如今這個就是負載均衡獲取實例的方法。
咱們對注入這個類的對象,而後對其測試:
@RunWith(SpringRunner.class) @SpringBootTest(classes = UserConsumerDemoApplication.class) public class LoadBalanceTest { @Autowired RibbonLoadBalancerClient client; @Test public void test(){ for (int i = 0; i < 100; i++) { ServiceInstance instance = this.client.choose("user-service"); System.out.println(instance.getHost() + ":" + instance.getPort()); } } }
結果:
符合了咱們的預期推測,確實是輪詢方式。
咱們是否能夠修改負載均衡的策略呢?
繼續跟蹤源碼,發現這麼一段代碼:
咱們看看這個rule是誰:
這裏的rule默認值是一個RoundRobinRule,看類的介紹:
這不就是輪詢的意思嘛。
咱們注意到,這個類實際上是實現了接口IRule的,查看一下:
定義負載均衡的規則接口。
它有如下實現:
SpringBoot也幫咱們提供了修改負載均衡規則的配置入口:
user-service:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
格式是:{服務名稱}.ribbon.NFLoadBalancerRuleClassName,值就是IRule的實現類。
再次測試,發現結果變成了隨機:
7.5.重試機制
Eureka的服務治理強調了CAP原則中的AP,便可用性和可靠性。它與Zookeeper這一類強調CP(一致性,可靠性)的服務治理框架最大的區別在於:Eureka爲了實現更高的服務可用性,犧牲了必定的一致性,極端狀況下它寧願接收故障實例也不肯丟掉健康實例,正如咱們上面所說的自我保護機制。
可是,此時若是咱們調用了這些不正常的服務,調用就會失敗,從而致使其它服務不能正常工做!這顯然不是咱們願意看到的。
咱們如今關閉一個user-service實例:
由於服務剔除的延遲,consumer並不會當即獲得最新的服務列表,此時再次訪問你會獲得錯誤提示:
可是此時,8081服務實際上是正常的。
所以Spring Cloud 整合了Spring Retry 來加強RestTemplate的重試能力,當一次服務調用失敗後,不會當即拋出一次,而是再次重試另外一個服務。
只須要簡單配置便可實現Ribbon的重試:
spring: cloud: loadbalancer: retry: enabled: true # 開啓Spring Cloud的重試功能 user-service: ribbon: ConnectTimeout: 250 # Ribbon的鏈接超時時間 ReadTimeout: 1000 # Ribbon的數據讀取超時時間 OkToRetryOnAllOperations: true # 是否對全部操做都進行重試 MaxAutoRetriesNextServer: 1 # 切換實例的重試次數 MaxAutoRetries: 1 # 對當前實例的重試次數
根據如上配置,當訪問到某個服務超時後,它會再次嘗試訪問下一個服務實例,若是不行就再換一個實例,若是不行,則返回失敗。切換次數取決於MaxAutoRetriesNextServer參數的值
引入spring-retry依賴