1. 程式人生 > >樂優商場開發第二天筆記

樂優商場開發第二天筆記

0.學習目標

  • 瞭解系統架構的演變

  • 瞭解RPC與Http的區別

  • 掌握HttpClient的簡單使用

  • 知道什麼是SpringCloud

  • 獨立搭建Eureka註冊中心

  • 獨立配置Robbin負載均衡

 

1.系統架構演變

隨著網際網路的發展,網站應用的規模不斷擴大。需求的激增,帶來的是技術上的壓力。系統架構也因此也不斷的演進、升級、迭代。從單一應用,到垂直拆分,到分散式服務,到SOA,以及現在火熱的微服務架構,還有在Google帶領下來勢洶湧的Service Mesh。我們到底是該乘坐微服務的船隻駛向遠方,還是偏安一隅得過且過?

其實生活不止眼前的苟且,還有詩和遠方。所以我們今天就回顧歷史,看一看系統架構演變的歷程;把握現在,學習現在最火的技術架構;展望未來,爭取成為一名優秀的Java工程師。

1.1. 集中式架構

當網站流量很小時,只需一個應用,將所有功能都部署在一起,以減少部署節點和成本。此時,用於簡化增刪改查工作量的資料訪問框架(ORM)是影響專案開發的關鍵。

 

存在的問題:

  • 程式碼耦合,開發維護困難

  • 無法針對不同模組進行鍼對性優化

  • 無法水平擴充套件

  • 單點容錯率低,併發能力差

 

1.2.垂直拆分

當訪問量逐漸增大,單一應用無法滿足需求,此時為了應對更高的併發和業務需求,我們根據業務功能對系統進行拆分:

 

優點:

  • 系統拆分實現了流量分擔,解決了併發問題

  • 可以針對不同模組進行優化

  • 方便水平擴充套件,負載均衡,容錯率提高

缺點:

  • 系統間相互獨立,會有很多重複開發工作,影響開發效率

1.3.分散式服務

當垂直應用越來越多,應用之間互動不可避免,將核心業務抽取出來,作為獨立的服務,逐漸形成穩定的服務中心,使前端應用能更快速的響應多變的市場需求。此時,用於提高業務複用及整合的分散式呼叫是關鍵。

 

優點:

  • 將基礎服務進行了抽取,系統間相互呼叫,提高了程式碼複用和開發效率

缺點:

  • 系統間耦合度變高,呼叫關係錯綜複雜,難以維護

 

1.4.服務治理(SOA)

SOA :面向服務的架構

當服務越來越多,容量的評估,小服務資源的浪費等問題逐漸顯現,此時需增加一個排程中心基於訪問壓力實時管理叢集容量,提高叢集利用率。此時,用於提高機器利用率的資源排程和治理中心(SOA)是關鍵

 

以前出現了什麼問題?

  • 服務越來越多,需要管理每個服務的地址

  • 呼叫關係錯綜複雜,難以理清依賴關係

  • 服務過多,服務狀態難以管理,無法根據服務情況動態管理

服務治理要做什麼?

  • 服務註冊中心,實現服務自動註冊和發現,無需人為記錄服務地址

  • 服務自動訂閱,服務列表自動推送,服務呼叫透明化,無需關心依賴關係

  • 動態監控服務狀態監控報告,人為控制服務狀態

缺點:

  • 服務間會有依賴關係,一旦某個環節出錯會影響較大

  • 服務關係複雜,運維、測試部署困難,不符合DevOps思想

1.5.微服務

前面說的SOA,英文翻譯過來是面向服務。微服務,似乎也是服務,都是對系統進行拆分。因此兩者非常容易混淆,但其實缺有一些差別:

 

微服務的特點:

  • 單一職責:微服務中每一個服務都對應唯一的業務能力,做到單一職責

  • 微:微服務的服務拆分粒度很小,例如一個使用者管理就可以作為一個服務。每個服務雖小,但“五臟俱全”。

  • 面向服務:面向服務是說每個服務都要對外暴露Rest風格服務介面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下的一個元件。

官網地址:http://hc.apache.org/index.html

 

特點:

  • 基於標準、純淨的Java語言。實現了Http1.0和Http1.1

  • 以可擴充套件的面向物件的結構實現了Http全部的方法(GET, POST, PUT, DELETE, HEAD, OPTIONS, and TRACE)

  • 支援HTTPS協議。

  • 通過Http代理建立透明的連線。

  • 自動處理Set-Cookie中的Cookie。

Rest風格:

  • 查詢:GET,/user/12

  • 新增:POST, /user

  • 修改:PUT, /user

  • 刪除:DELTE, /user/12

 

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.建立父工程

 

編寫專案資訊:

 

編寫儲存位置:

 

然後將Pom修改成這樣:

<?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>cn.itcast.demo</groupId>
    <artifactId>cloud-demo</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>pom</packaging>
​
    <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>
        <mybatis.starter.version>1.3.2</mybatis.starter.version>
        <mapper.starter.version>2.0.2</mapper.starter.version>
        <druid.starter.version>1.1.9</druid.starter.version>
        <mysql.version>5.1.32</mysql.version>
        <pageHelper.starter.version>1.2.3</pageHelper.starter.version>
        <leyou.latest.version>1.0.0-SNAPSHOT</leyou.latest.version>
    </properties>
​
    <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>
            <!-- mybatis啟動器 -->
            <dependency>
                <groupId>org.mybatis.spring.boot</groupId>
                <artifactId>mybatis-spring-boot-starter</artifactId>
                <version>${mybatis.starter.version}</version>
            </dependency>
            <!-- 通用Mapper啟動器 -->
            <dependency>
                <groupId>tk.mybatis</groupId>
                <artifactId>mapper-spring-boot-starter</artifactId>
                <version>${mapper.starter.version}</version>
            </dependency>
            <!-- 分頁助手啟動器 -->
            <dependency>
                <groupId>com.github.pagehelper</groupId>
                <artifactId>pagehelper-spring-boot-starter</artifactId>
                <version>${pageHelper.starter.version}</version>
            </dependency>
            <!-- mysql驅動 -->
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>${mysql.version}</version>
            </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>
</project>

這裡已經對大部分要用到的依賴的版本進行了 管理,方便後續使用

 

 

5.2.服務提供者

我們新建一個專案,對外提供查詢使用者的服務。

5.2.1.建立module

 

 

5.2.2.依賴

<?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>cn.itcast.demo</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
​
    <groupId>cn.itcast.demo</groupId>
    <artifactId>user-service</artifactId>
    <version>1.0-SNAPSHOT</version>
​
    <dependencies>
        <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>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>tk.mybatis</groupId>
            <artifactId>mapper-spring-boot-starter</artifactId>
        </dependency>
    </dependencies>
</project>

 

專案結構:

 

 

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:

public interface UserMapper extends tk.mybatis.mapper.common.Mapper<User>{
}

實體類:

@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
mybatis:
  type-aliases-package: cn.itcast.user.pojo

啟動類:

@SpringBootApplication
public class UserApplication {
    public static void main(String[] args) {
        SpringApplication.run(UserApplication.class, args);
    }
}

專案結構:

 

5.1.3.啟動並測試:

啟動專案,訪問介面:http://localhost:8081/user/7

 

 

5.2.服務呼叫者

5.2.1.建立工程

與上面類似,這裡不再贅述,需要注意的是,我們呼叫user-service的功能,因此不需要mybatis相關依賴了。

 

 

pom:

<?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>cn.itcast.demo</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
​
    <groupId>cn.itcast.demo</groupId>
    <artifactId>consumer-demo</artifactId>
    <version>1.0-SNAPSHOT</version>
​
    <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>
</project>

 

結構:

 

 

5.2.2.編寫程式碼

首先在啟動類中註冊RestTemplate

@SpringBootApplication
public class ConsumerDemoApplication {
​
    @Bean
    public RestTemplate restTemplate() {
        // 這次我們使用了OkHttp客戶端,只需要注入工廠即可
        return new RestTemplate(new OkHttp3ClientHttpRequestFactory());
    }
​
    public static void main(String[] args) {
        SpringApplication.run(ConsumerDemoApplication.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:

 

 

 

編寫啟動類:

@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客戶端依賴:

<!-- Eureka客戶端 -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

 

在啟動類上開啟Eureka客戶端功能

通過新增@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
  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)新增依賴:

<!-- Eureka客戶端 -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

 

2)在啟動類開啟Eureka客戶端

@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

  • 服務消費者

    消費應用從註冊中心獲取服務列表,從而得知每個服務方的資訊,知道去哪裡呼叫服務方。本例中就是我們實現的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}:${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;
    }
}

訪問頁面,檢視結果:

&nb