一、系统架构的演变

随着互联网的发展,网站应用的规模不断扩大。需求的激增,带来的是技术上的压力。系统架构也因此不断的演进、升级、迭代。从单一应用,到垂直拆分,到分布式服务,到SOA,以及现在火热的微服务架构,还有在Google带领下来势汹涌的Service Mesh。我们到底是该乘坐微服务的船只驶向远方,还是偏安一隅得过且过?

其实生活不止眼前的苟且,还有诗和远方。所以我们今天就回顾历史,看一看系统架构演变的历程;把握现在,学习现在最火的技术架构;展望未来,争取成为一名优秀的Java工程师。

1.1 集中式架构

当网站流量很小时,只需一个应用,将所有功能都部署在一起,以减少部署节点和成本。此时,用于简化增删改查工作量的数据访问框架(ORM)是影响项目开发的关键。

系统架构演进的试题 系统架构演变过程_服务调用


存在的问题:

1.代码耦合,开发维护困难
2.无法针对不同模块进行针对性优化
3.无法水平扩展
4.单点容错率低,并发能力差
1.2 垂直拆分

当访问量逐渐增大,单一应用无法满足需求,此时为了应对更高的并发和业务需求,我们根据业务功能对系统进行拆分。

系统架构演进的试题 系统架构演变过程_服务调用_02


优点:

1.系统拆分实现了流量分担,解决了并发问题
2.可以针对不同模块进行优化
3.方便水平扩展,负载均衡,容错率提高

缺点:

系统间相互独立,会有很多重复开发工作,影响开发效率
1.3 分布式服务

当垂直应用越来越多,应用之间交互不可避免,将核心业务抽取出来,作为独立的服务,逐渐形成稳定的服务中心,使前端应用能更快速的响应多变的市场需求。此时,用于提高业务复用及整合的分布式调用是关键。

系统架构演进的试题 系统架构演变过程_spring_03


优点:

将基础服务进行了抽取,系统间相互调用,提高了代码复用和开发效率

缺点:

系统间耦合度变高,调用关系错综复杂,难以维护
1.4 流动计算架构(SOA)

SOA :面向服务的架构

当服务越来越多,容量的评估,小服务资源的浪费等问题逐渐显现,此时需增加一个调度中心基于访问压力实时管理集群容量,提高集群利用率。此时,用于提高机器利用率的资源调度和治理中心(SOA)是关键

系统架构演进的试题 系统架构演变过程_服务调用_04


以前出现了什么问题?

服务越来越多,需要管理每个服务的地址
调用关系错综复杂,难以理清依赖关系
服务过多,服务状态难以管理,无法根据服务情况动态管理

服务治理要做什么?

服务注册中心,实现服务自动注册和发现,无需人为记录服务地址
服务自动订阅,服务列表自动推送,服务调用透明化,无需关心依赖关系
动态监控服务状态监控报告,人为控制服务状态

缺点:

服务间会有依赖关系,一旦某个环节出错会影响较大
服务关系复杂,运维、测试部署困难,不符合DevOps思想
1.5 微服务

就目前而言,业界对微服务并没有一个统一的标准的定义。但通常而言,微服务架构是一种架构模式或者架构风格,它提倡将单一应用程序划分成一组小的服务。每个服务运行在其独立的进程中。服务之间互相协调互相配合,为用户提供最终价值。服务之间采取轻量级的通信机制互相沟通(通常是基于HTTP的Restful API).每个服务都围绕这具体业务进行构建,并且能够被独立的部署到生产环境、类生产环境等。另外,应尽量的避免统一的、集中式的服务管理机制,对具体的一个服务而言,应根据业务上下文,选择合适的语言、工具对其进行构建,可以有一个非常轻量级的集中式管理来协调这些服务,可以使用不同的语言来编写这些服务,也可以使用不同的数据存储。

系统架构演进的试题 系统架构演变过程_spring_05

二、服务调用方式

RPC和HTTP
无论是微服务还是SOA,都面临着服务间的远程调用。那么服务间的远程调用方式有哪些呢?

常见的远程调用方式有以下2种:
RPC:Remote Produce Call远程过程调用,类似的还有RMI。自定义数据格式,基于原生TCP通信,速度快,效率高。早期的webservice,现在热门的dubbo,都是RPC的典型代表

Http:http其实是一种网络传输协议,基于TCP,规定了数据传输的格式。现在客户端浏览器与服务端通信基本都是采用Http协议,也可以用来进行远程服务调用。缺点是消息封装臃肿,优势是对服务的提供和调用方没有任何技术限定,自由灵活,更符合微服务理念。
现在热门的Rest风格,就可以通过http协议来实现。
如果你们公司全部采用Java技术栈,那么使用Dubbo作为微服务架构是一个不错的选择。
相反,如果公司的技术栈多样化,而且你更青睐Spring家族,那么SpringCloud搭建微服务是不二之选。在我们的项目中,我们会选择SpringCloud套件,因此我们会使用Http方式来实现服务间调用。

三、初识springcloud

微服务是一种架构方式,最终肯定需要技术架构去实施。
微服务的实现方式很多,但是最火的莫过于Spring Cloud了。为什么?

1.后台硬:作为Spring家族的一员,有整个Spring全家桶靠山,背景十分强大。
2.技术强:Spring作为Java领域的前辈,可以说是功力深厚。有强力的技术团队支撑,一般人还真比不了
3.群众基础好:可以说大多数程序员的成长都伴随着Spring框架,试问:现在有几家公司开发不用Spring?SpringCloud与Spring的各个框架无缝整合,对大家来说一切都是熟悉的配方,熟悉的味道。
4.使用方便:相信大家都体会到了SpringBoot给我们开发带来的便利,而SpringCloud完全支持SpringBoot的开发,用很少的配置就能完成微服务框架的搭建
3.1 springcloud简介

SpringCloud是Spring旗下的项目之一,官网地址:http://projects.spring.io/spring-cloud/

Spring最擅长的就是集成,把世界上最好的框架拿过来,集成到自己的项目中。
SpringCloud也是一样,它将现在非常流行的一些技术整合到一起,实现了诸如:配置管理,服务发现,智能路由,负载均衡,熔断器,控制总线,集群状态等等功能。其主要涉及的组件包括:

1.Eureka:服务治理组件,包含服务注册中心,服务注册与发现机制的实现。(服务治理,服务注册/发现) 
2.Zuul:网关组件,提供智能路由,访问过滤功能 
3.Ribbon:客户端负载均衡的服务调用组件(客户端负载) 
4.Feign:服务调用,给予Ribbon和Hystrix的声明式服务调用组件 (声明式服务调用) 
5.Hystrix:容错管理组件,实现断路器模式,帮助服务依赖中出现的延迟和为故障提供强大的容错能力。(熔断、断路器,容错)

系统架构演进的试题 系统架构演变过程_spring_06

3.2 微服务场景模拟

首先,我们需要模拟一个服务调用的场景
搭建两个工程:oracle-service-provider(服务提供方)和oracle-service-consumer(服务调用方)。

服务提供方:使用mybatis操作数据库,实现对数据的增删改查;并对外提供rest接口服务。
服务消费方:使用restTemplate远程调用服务提供方的rest接口服务,获取数据。

(1)创建服务提供者springclouddemo1_provider

系统架构演进的试题 系统架构演变过程_系统架构演进的试题_07

  • 在项目中引入依赖
<parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-parent</artifactId>
  <version>2.2.5.RELEASE</version>
  <relativePath/> <!-- lookup parent from repository -->
</parent>

<dependencies>
   <!-- Eureka客户端 -->
   <dependency>
     <groupId>org.springframework.cloud</groupId>
     <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
     <version>2.2.1.RELEASE</version>
   </dependency>
   
   <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>2.1.1</version>
   </dependency>
   <dependency>
     <groupId>mysql</groupId>
     <artifactId>mysql-connector-java</artifactId>
     <scope>runtime</scope>
   </dependency>

   <!-- 需要手动引入通用mapper的启动器,spring没有收录该依赖 -->
   <dependency>
     <groupId>tk.mybatis</groupId>
     <artifactId>mapper-spring-boot-starter</artifactId>
     <version>2.0.4</version>
   </dependency>
   <dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-test</artifactId>
     <scope>test</scope>
     <exclusions>
       <exclusion>
         <groupId>org.junit.vintage</groupId>
         <artifactId>junit-vintage-engine</artifactId>
       </exclusion>
     </exclusions>
   </dependency>
   <dependency>
     <groupId>org.projectlombok</groupId>
     <artifactId>lombok</artifactId>
   </dependency>
</dependencies>

<build>
  <plugins>
    <plugin>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-maven-plugin</artifactId>
    </plugin>
  </plugins>
</build>
  • Application.yml配置
server:
  port: 8086
spring:
  datasource:
    username: root
    password: 123
    url: jdbc:mysql://localhost:3306/ssm?serverTimezone=GMT
    driver-class-name: com.mysql.jdbc.Driver
  • 数据表
  • 实体类
package com.bianyiit.pojo;

import lombok.Data;

import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import java.io.Serializable;

@Table(name = "account") //实体类绑定的表
@Data  //lombok插件
public class Account implements Serializable {
    private static final long serialVersionUID = 3024382278891947015L;
    @Id //标识当前id为主键
    @GeneratedValue(strategy= GenerationType.IDENTITY)   //主键自增长
    private Integer id;
    private String name;
    private Double money;
}
  • UserMapper
package com.bianyiit.mapper;

import com.bianyiit.pojo.Account;
import org.springframework.stereotype.Repository;
import tk.mybatis.mapper.common.Mapper;

//mapper插件对于单表的增删改查操作都不用我们自己写 tk.mybatis.mapper.common.mapper
@Repository
public interface AccountMapper extends Mapper<Account> {
    //集成Mapper(里面的泛型为实体类)
}
  • UserService
package com.bianyiit.service;

import com.bianyiit.mapper.AccountMapper;
import com.bianyiit.pojo.Account;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class AccountService {
    @Autowired
    AccountMapper accountMapper;

    //根据id查询Account
    public Account findAccountById(Integer id){
        return accountMapper.selectByPrimaryKey(id);  //通用mapper中提供了很多方法可以让我们去操作单表
    }
}
  • UserController
package com.bianyiit.controller;

import com.bianyiit.pojo.Account;
import com.bianyiit.service.AccountService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/account")
public class AccountController {

    @Autowired
    AccountService accountService;

    @GetMapping("/findAccountById/{id}")   //localhost:8080/account/findAccountById/2
    public Account getAccountById(@PathVariable("id") Integer id){
        return accountService.findAccountById(id);
    }
}
  • 编写springboot启动类
package com.bianyiit;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import tk.mybatis.spring.annotation.MapperScan;

@SpringBootApplication
@MapperScan("com.bianyiit.mapper")  //设置包扫描mapper,通用mapper提供的注解(tk.mybatis.spring.annotation.MapperSan)
public class ServiceProviderApplication {
    public static void main(String[] args) {
        SpringApplication.run(ServiceProviderApplication.class);
    }
}
  • 启动并测试
    启动项目,访问接口:http://localhost:8081/account/findAccountById/2

(1)创建服务消费者springclouddemo1_consumer

系统架构演进的试题 系统架构演变过程_微服务_08

  • 导入依赖
<parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-parent</artifactId>
  <version>2.2.5.RELEASE</version>
  <relativePath/> <!-- lookup parent from repository -->
</parent>

<dependencies>
	<!-- Eureka客户端 -->
	<dependency>
	  <groupId>org.springframework.cloud</groupId>
	  <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
	  <version>2.2.1.RELEASE</version>
	</dependency>
	
	<dependency>
	  <groupId>org.springframework.boot</groupId>
	  <artifactId>spring-boot-starter-web</artifactId>
	</dependency>
	
	<dependency>
	  <groupId>org.springframework.boot</groupId>
	  <artifactId>spring-boot-starter-test</artifactId>
	  <scope>test</scope>
	  <exclusions>
	    <exclusion>
	      <groupId>org.junit.vintage</groupId>
	      <artifactId>junit-vintage-engine</artifactId>
	    </exclusion>
	  </exclusions>
	</dependency>
	<dependency>
	  <groupId>org.projectlombok</groupId>
	  <artifactId>lombok</artifactId>
	</dependency>
	<dependency>
	  <groupId>junit</groupId>
	  <artifactId>junit</artifactId>
	  <scope>test</scope>
	</dependency>
</dependencies>

<build>
	<plugins>
	  <plugin>
	    <groupId>org.springframework.boot</groupId>
	    <artifactId>spring-boot-maven-plugin</artifactId>
	  </plugin>
	</plugins>
</build>
  • 配置application.yml
server:
  port: 8084
  • 提供User实体
package com.bianyiit.pojo;

import lombok.Data;

import java.io.Serializable;

//这里不用使用@Table注解修饰,因为不需要去连接数据库查表,只需要进行数据的封装
@Data
public class Account implements Serializable {
    private Integer id;
    private String name;
    private Double money;
}
  • 提供配置类
package com.bianyiit.config;

import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

@Configuration
public class MyConfig {

    //注册RestTemplate,用于服务之间的远程调用
    @Bean
    @LoadBalanced
    public RestTemplate restTemplate(){
        return new RestTemplate();
    }
}
  • 提供AccountController
package com.bianyiit.controller;

import com.bianyiit.pojo.Account;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

@RestController
@RequestMapping("/consumer")
public class AccountController {

    @Autowired
    RestTemplate restTemplate;  //使用它做一个远程调用

    @GetMapping("/findAccountById/{id}")
    public Account findAccountById(@PathVariable("id") Integer id){
        //http://localhost:8081/account/findAccountById?id=2  @RequstParam
        //http://localhost:8081/account/findAccountById/2  @PathVariable
        return restTemplate.getForObject("http://localhost:8081/account/findAccountById/"+id,Account.class); //进行远程调用
    }

}
  • 启动并访问,访问路径:http://localhost:8082/consumer/findAccountById/2
3.3 以上案例存在的问题

系统架构演进的试题 系统架构演变过程_系统架构演进的试题_09

四、Eureka注册中心

4.1 认识Eureka 首先我们来解决第一问题,服务的管理。

  • 问题分析
    在刚才的案例中,oracle-service-provider对外提供服务,需要对外暴露自己的地址。而consumer(调用者)需要记录服务提供者的地址。将来地址出现变更,还需要及时更新。这在服务较少的时候并不觉得有什么,但是在现在日益复杂的互联网环境,一个项目肯定会拆分出十几,甚至数十个微服务。此时如果还人为管理地址,不仅开发困难,将来测试、发布上线都会非常麻烦,这与DevOps的思想是背道而驰的。
  • 网约车
    这就好比是网约车出现以前,人们出门叫车只能叫出租车。一些私家车想做出租却没有资格,被称为黑车。而很多人想要约车,但是无奈出租车太少,不方便。私家车很多却不敢拦,而且满大街的车,谁知道哪个才是愿意载人的。一个想要,一个愿意给,就是缺少引子,缺乏管理啊。
    此时滴滴这样的网约车平台出现了,所有想载客的私家车全部到滴滴注册,记录你的车型(服务类型),身份信息(联系方式)。这样提供服务的私家车,在滴滴那里都能找到,一目了然。此时要叫车的人,只需要打开APP,输入你的目的地,选择车型(服务类型),滴滴自动安排一个符合需求的车到你面前,为你服务,完美!
  • Eureka做什么?
    Eureka就好比是滴滴,负责管理、记录服务提供者的信息。服务调用者无需自己寻找服务,而是把自己的需求告诉Eureka,然后Eureka会把符合你需求的服务告诉你。同时,服务提供方与Eureka之间通过“心跳”机制进行监控,当某个服务提供方出现问题,Eureka自然会把它从服务列表中剔除。这就实现了服务的自动注册、发现、状态监控。
4.2 Eureka原理图

系统架构演进的试题 系统架构演变过程_微服务_10

  • Eureka:就是服务注册中心(可以是一个集群),对外暴露自己的地址
  • 提供者:启动后向Eureka注册自己信息(地址,提供什么服务)
  • 消费者:向Eureka订阅服务,Eureka会将对应服务的所有提供者地址列表发送给消费者,并且定期更新
  • 心跳(续约):提供者定期通过http方式向Eureka刷新自己的状态
4.3 搭建Eureka Server

系统架构演进的试题 系统架构演变过程_微服务_11

  • 引入依赖
<parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-parent</artifactId>
  <version>2.2.5.RELEASE</version>
  <relativePath/> <!-- lookup parent from repository -->
</parent>

<properties>
  <spring-cloud.version>Hoxton.SR1</spring-cloud.version>
</properties>

<dependencies>
  <!--Eureka启动器-->
  <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
    <version>2.2.1.RELEASE</version>
  </dependency>

  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
    <exclusions>
      <exclusion>
        <groupId>org.junit.vintage</groupId>
        <artifactId>junit-vintage-engine</artifactId>
      </exclusion>
    </exclusions>
  </dependency>
</dependencies>
<!--统一管理springcloud的版本号-->
<dependencyManagement>
  <dependencies>
    <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>
  </repository>
</repositories>
  • 编写application.yml配置
server:
  port: 10087
spring:
  application:
    name: springclouddemo1_enableEureka  #注册的服务名称,会在eureka中显示
eureka:
  client:
    service-url: #配置eureka的默认访问地址
      defaultZone: http://127.0.0.1:10086/eureka
  • 修改引导类,在类上添加@EnableEurekaServer注解
package com.bianyiit;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

@SpringBootApplication
@EnableEurekaServer  声明当前应用是一个Eureka服务中心
public class ServiceEurekaApplication {
    public static void main(String[] args) {
        SpringApplication.run(ServiceEurekaApplication.class, args);
    }
}
  • 启动服务并访问,启动服务,并访问:http://127.0.0.1:10086/
4.4 将服务注册到eureka
  • 修改springclouddemo1_provider的applicaton.yml配置文件
server:
  port: 8086
spring:
  datasource:
    username: root
    password: 123
    url: jdbc:mysql://localhost:3306/ssm?serverTimezone=GMT
    driver-class-name: com.mysql.jdbc.Driver
  application:
    name: springclouddemo1-provider  #注册到eureka的服务名称
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:10086/eureka  #连接eureka的地址
  • 在springclouddemo1启动类ServiceProviderApplication上开启eureka客户端
  • 重启项目,访问Eureka监控页面查看
4.5 从Eureka拉取服务
  • 修改springclouddemo1_consumer的applicaton.yml配置文件![]
server:
  port: 8084
spring:
  application:
    name: springclouddemo1_consumer
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:10086/eureka
  • 在springclouddemo1_consumer启动类ServiceConsumerApplication上开启eureka客户端
  • 重启项目,访问Eureka监控页面查看
4.6 改造服务消费方拉取服务列表硬编码问题
package com.bianyiit.controller;

import com.bianyiit.pojo.Account;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

@RestController
@RequestMapping("/consumer")
public class AccountController {

    @Autowired
    RestTemplate restTemplate;  //使用它做一个远程调用

    @Autowired
    DiscoveryClient discoveryClient;  //服务消费方在eureka注册中心拉取的服务列表都封装到DiscoveryClient里面

    @GetMapping("/findAccountById/{id}")
    public Account findAccountById(@PathVariable("id") Integer id){
        // 根据服务名称,获取服务实例。有可能是集群,所以是service实例集合,需要根据application.yml中注册的服务名称去填写
        List<ServiceInstance> instances  = discoveryClient.getInstances("springclouddemo1_provider");
        // 因为只有一个Service-provider,所以获取第一个实例
        ServiceInstance instance = instances.get(0);
        String url="http://"+instance.getHost()+":"+instance.getPort()+"/account/findAccountById/"+id; 
        return restTemplate.getForObject(url,Account.class);
    }
}
五、Eureka详解
5.1 Eureka基础架构

Eureka架构中的三个核心角色:

  • 服务注册中心
    Eureka的服务端应用,提供服务注册和发现功能,就是刚刚我们建立的oracle-eureka。
  • 服务提供者
    提供服务的应用,可以是SpringBoot应用,也可以是其它任意技术实现,只要对外提供的是Rest风格服务即可。本例中就是我们实现的oracle-service-provider。
  • 服务消费者
    消费应用从注册中心获取服务列表,从而得知每个服务方的信息,知道去哪里调用服务方。本例中就是我们实现oracle-service-consumer。
5.2 搭建高可用的Eureka(了解)
  • Eureka Server即服务的注册中心,在刚才的案例中,我们只有一个EurekaServer,但是如果这个EurekaServer挂掉了会影响整个应用。事实上EurekaServer也可以是一个集群,形成高可用的Eureka中心,一个Eureka服务中心挂掉了还有其他的服务注册中心。
  • 服务同步:
    多个Eureka Server之间也会互相注册为服务,当服务提供者注册到Eureka Server集群中的某个节点时,该节点会把服务的信息同步给集群中的每个节点,从而实现数据同步。因此,无论客户端访问到Eureka Server集群中的任意一个节点,都可以获取到完整的服务列表信息。

动手搭建高可用的EurekaServer: 需求: 我们假设要运行两个EurekaServer的集群,端口分别为:10086和10087。只需要把bianyi-eureka启动两次即可。

复制一个新的Eureka注册中心

系统架构演进的试题 系统架构演变过程_微服务_12


将10086端口的Eureka注册到10087端口号的Eureka,然后启动该Eureka注册中心

server:
  port: 10086
spring:
  application:
    name: springclouddemo1_enableEureka  #注册的服务名称,会在eureka中显示
eureka:
  client:
    service-url: #配置eureka的默认访问地址
      defaultZone: http://127.0.0.1:10087/eureka

注意:此时启动会报错,很正常。因为10087服务没有启动

由于两个Eureka共用一个application.yml,所以启动完上一个之后,直接更改applicaiton.yml中的端口即可,将10087端口的Eureka注册到端口号是10086端口号的Eureka,然后启动该Eureka注册中心

server:
  port: 10087
spring:
  application:
    name: springclouddemo1_enableEureka  #注册的服务名称,会在eureka中显示
eureka:
  client:
    service-url: #配置eureka的默认访问地址
      defaultZone: http://127.0.0.1:10086/eureka

注意:为什么要10086端口的Eureka注册到10087端口的Eureka呢?? 因为多个Eureka Server之间也会互相注册为服务,当服务提供者注册到Eureka Server集群中的某个节点时,该节点会把服务的信息同步给集群中的每个节点,从而实现数据同步。因此,无论客户端访问到Eureka Server集群中的任意一个节点,都可以获取到完整的服务列表信息。

假如有三个Eureka注册中心,只需要三个Eureka首尾相连形成一个闭环即可

分别启动:

系统架构演进的试题 系统架构演变过程_spring_13


系统架构演进的试题 系统架构演变过程_服务调用_14

六、负载均衡Ribbon

在刚才的案例中,我们启动了一个oracle-service-provider,然后通过DiscoveryClient来获取服务实例信息,然后获取ip和端口来访问。但是实际环境中,我们往往会开启很多个oracle-service-provider的集群。此时我们获取的服务列表中就会有多个,到底该访问哪一个呢?一般这种情况下我们就需要编写负载均衡算法,在多个实例列表中进行选择。不过Eureka中已经帮我们集成了负载均衡组件:Ribbon,简单修改代码即可使用。

什么是Ribbon:

系统架构演进的试题 系统架构演变过程_spring_15


接下来,我们就来使用Ribbon实现负载均衡

6.1 负载均衡演示

注意:实现负载均衡,服务的名称不能带下划线,否则会消费者报java.lang.IllegalStateException: Request URI does not contain a valid hostname:的异常

  • 需求:启动两个服务实例, 端口分别为8081和8083
    启动两个服务实例,启动方式和启动多个Eureka步骤一样
  • 开启负载均衡
    因为Eureka中已经集成了Ribbon,所以我们无需引入新的依赖,直接修改代码。

在服务消费方的引导类MyConfig上的RestTemplate的配置方法上添加@LoadBalanced注解

package com.bianyiit.config;

import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

@Configuration
public class MyConfig {

    //注册RestTemplate,用于服务之间的远程调用
    @Bean
    @LoadBalanced
    public RestTemplate restTemplate(){
        return new RestTemplate();
    }
}

修改调用方式,不再手动获取ip和端口,而是直接通过服务名称调用

系统架构演进的试题 系统架构演变过程_spring_16


访问页面,查看结果:http://localhost:8082/consumer/findAccountById/2

系统架构演进的试题 系统架构演变过程_spring_17

6.2 负载均衡的策略

Ribbon默认的负载均衡策略是简单的轮询,在springcloud中使用RibbonLoadBalanceClient来进行负载均衡的。其中有一个choose方法,找到choose方法的接口方法,是这样介绍的:

系统架构演进的试题 系统架构演变过程_微服务_18


现在这个就是负载均衡获取实例的方法。

我们注入这个类的对象,然后对其测试:

package com.bianyiit;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)
@SpringBootTest(classes = ServiceConsumerApplication.class)
public class LoadBalanceTest {
    @Autowired
    LoadBalancerClient client;
    @Test
    public void testLoadBalance(){
        for(int i = 1;i<=100;i++){
            ServiceInstance instance = client.choose("springclouddemo1-provider");
            System.out.println(instance.getHost() + ":" +instance.getPort());
        }
    }
}

测试结果:

系统架构演进的试题 系统架构演变过程_微服务_19

符合了我们的预期推测,确实是轮询方式。

我们也可以改变默认的负载均衡策略

SpringBoot也帮我们提供了修改负载均衡规则的配置入口,在springclouddemo1_consumer的application.yml中添加如下配置:

系统架构演进的试题 系统架构演变过程_系统架构演进的试题_20


系统架构演进的试题 系统架构演变过程_微服务_21

server:
  port: 8084
spring:
  application:
    name: springclouddemo1_consumer
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:10086/eureka
springclouddemo1-provider:  #注意这里填写的是你的服务提供者注册的名字
  ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule #改变负载均衡策略为随机

再次测试,发现结果变成了随机:

系统架构演进的试题 系统架构演变过程_微服务_22