Spring Cloud版——电影售票系统<—>Eureka微服务注册与发现

GitHub地址:https://github.com/leebingbin/SpringCloud.MovieTicketing

 

一、服务提供者与服务消费者

    使用微服务构建的是分布式系统,微服务之间通过网络进行通信。我们使用服务提供者与服务消费者来描述微服务之间的调用关系。

名称 定义
服务提供者 服务的被调用方(为其他服务提供服务的服务)
服务消费者 服务的调用方(依赖其他服务的服务)

    例如,在电影售票系统中,用户向电影微服务发起了一个购票的请求。在进行购票的业务操作前,电影微服务需要调用用户微服务的接口,查询当前用户的余额是多少,是不是符合购票标准等。在这种场景下,用户微服务就是一个服务提供者,电影微服务则是一个服务消费者。

    就以该场景,先来编写一个用户微服务,然后编写一个电影微服务。

二、编写服务提供者

    编写一个服务提供者(用户微服务),该服务可通过主键查询用户信息。开发测试阶段,使用Spring Data JPA作为持久化层框架,使用H2作为数据库。

    创建一个Maven项目,它的ArtifactId是movieticketing-provider-user,对应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/maven-v4_0_0.xsd">

  <modelVersion>4.0.0</modelVersion>

  <groupId>com.bingbinlee.springcloud</groupId>
  <artifactId>movieticketing-provider-user</artifactId>
  <packaging>jar</packaging>
  <version>1.0-SNAPSHOT</version>

  <!-- 引入spring boot的依赖 -->
  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>1.4.3.RELEASE</version>
  </parent>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <java.version>1.8</java.version>
  </properties>

  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
      <groupId>com.h2database</groupId>
      <artifactId>h2</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>

    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-eureka</artifactId>
    </dependency>
  </dependencies>

  <!-- 引入spring cloud的依赖 -->
  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-dependencies</artifactId>
        <version>Camden.SR4</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
    </dependencies>
  </dependencyManagement>

  <!-- 添加spring-boot的maven插件 -->
  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>
    </plugins>
  </build>

</project>

    其中,spring-boot-starter-web提供了SpringMVC的支持;spring-boot-starter-data-jpa提供了Spring Data JPA的支持。

     在项目中建立文件夹DB存放schema.sql,内容如下:

drop table user if exists;
create table user (
  id bigint generated by default as identity,
  username varchar(40),
  name varchar(20),
  age int(3),
  balance decimal(10,2),
  primary key(id)
);
insert into user (id, username, name, age, balance) values(1, 'user1', "张三", 20, 100.00);
insert into user (id, username, name, age, balance) values(2, 'user2', "李四", 30, 200.00);
insert into user (id, username, name, age, balance) values(3, 'user3', "王麻子", 40, 300.00);

    创建用户实体类:

package com.bingbinlee.springcloud.micro.entity;

import javax.persistence.*;
import java.math.BigDecimal;

@Entity
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    @Column
    private String username;
    @Column
    private String name;
    @Column
    private Integer age;
    @Column
    private BigDecimal balance;

    public Long getId() {
        return this.id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getUsername() {
        return this.username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getName() {
        return this.name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return this.age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public BigDecimal getBalance() {
        return this.balance;
    }

    public void setBalance(BigDecimal balance) {
        this.balance = balance;
    }

}

    创建DAO:

package com.bingbinlee.springcloud.micro.repository;

import com.bingbinlee.springcloud.micro.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
}

    创建Controller:

package com.bingbinlee.springcloud.micro.controller;

import com.bingbinlee.springcloud.micro.entity.User;
import com.bingbinlee.springcloud.micro.repository.UserRepository;
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.RestController;

@RestController
public class UserController {
    @Autowired
    private UserRepository userRepository;

    @GetMapping("/{id}")
    public User findById(@PathVariable Long id){
        User findOne = this.userRepository.findOne(id);
        return findOne;
    }

}

        Tips: Controller中用到的@GetMapping,是Spring4.3提供的新注解。它是一个组合注解,等价于@RequestMapping(meth = RequestMethod.GET),用于简化开发。同理,还有 @PostMapping, @PutMapping, @DeleteMapping, @PatchMapping 等等。

    编写启动类,在类上使用@SpringBootApplication声明这是一个Spring Boot项目。

package com.bingbinlee.springcloud.micro;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

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

    Tips1: @SpringBootApplication 是一个组合注解,它整合了@Configuration、@EnableAutoConfiguration和@ComponentScan注解,并开启了Spring Boot程序的组件扫描和自动配置功能。在开发Spring Boot 程序的过程中,常常会组合使用 @Configuration、@EnableAutoConfiguration 和 @ComponentScan 等注解,所以Spring Boot提供了@SpringBootApplication, 来简化开发。

        Tips2: SpringCLoud中的“Discovery Service”有多种实现,比如:eureka, consul, zookeeper。
@EnableDiscoveryClient注解是基于spring-cloud-commons依赖,并且在classpath中实现; @EnableEurekaClient注解是基于spring-cloud-netflix依赖,只能为eureka作用;如果你的classpath中添加了eureka,则它们的作用是一样的。

    编写配置文件,命名为application.yml

server:
  port: 8000
spring:
  application:
    name: movieticketing-provider-user
  jpa:
    generate-ddl: false
    show-sql: true
    hibernate:
      ddl-auto: none
  datasource:                           # 指定数据源
    platform: h2                        # 指定数据源类型
    schema: classpath:schema.sql        # 指定h2数据库的建表脚本
    data: classpath:data.sql            # 指定h2数据库的数据脚本
logging:                                # 配置日志级别,让hibernate打印出执行的SQL
  level:
    root: INFO
    org.hibernate: INFO
    org.hibernate.type.descriptor.sql.BasicBinder: TRACE
    org.hibernate.type.descriptor.sql.BasicExtractor: TRACE
eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/
  instance:
    prefer-ip-address: true

    Tips: 在传统的WebApp开发中,常使用properties格式文件作为配置文件。Spring Boot以及Spring Cloud支持使用properties或者yml格式的文件作为配置文件。yml 文件格式是YAML (Yet Another Markup Language)编写的文件格式,YAML 和 properties 格式的文件可互相转换。YAML 比 properties结构清晰;可读性、可维护性也更强,并且语法非常简洁;但是yml有严格的缩进。

    测试访问:http://localhost:8000/2,获得结果如下

    说明已通过ID查询用户信息。

三、 编写服务消费者

    上节编写了一个服务提供者(用户微服务),本节来编写一个服务消费者(电影微服务)。该服务很简单,使用RestTemplate调用用户微服务的API,从而查询指定id的用户信息。

    首先,创建一个Maven项目,ArifactId是movieticketing-consumer-movie。添加依赖如下:

<?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/maven-v4_0_0.xsd">

  <modelVersion>4.0.0</modelVersion>

  <groupId>com.bingbinlee.springcloud</groupId>
  <artifactId>movieticketing-consumer-movie</artifactId>
  <packaging>jar</packaging>
  <version>1.0-SNAPSHOT</version>

  <!-- 引入spring boot的依赖 -->
  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>1.4.3.RELEASE</version>
  </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>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>

    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-eureka</artifactId>
    </dependency>
  </dependencies>

  <!-- 引入spring cloud的依赖 -->
  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-dependencies</artifactId>
        <version>Camden.SR4</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
    </dependencies>
  </dependencyManagement>

  <!-- 添加spring-boot的maven插件 -->
  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>
    </plugins>
  </build>

</project>

    创建用户实体类,该类是一个POJO。

package com.bingbinlee.springcloud.micro.entity;

import java.math.BigDecimal;

public class User {
    private Long id;
    private String username;
    private String name;
    private Integer age;
    private BigDecimal balance;

    public Long getId() {
        return this.id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getUsername() {
        return this.username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getName() {
        return this.name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return this.age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public BigDecimal getBalance() {
        return this.balance;
    }

    public void setBalance(BigDecimal balance) {
        this.balance = balance;
    }
}

    创建启动类。

package com.bingbinlee.springcloud.micro;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

@SpringBootApplication
public class ConsumerMovieApplication {
    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }

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

    Tips: @Bean是一个方法注解,作用是实例化一个Bean 并使用该方法的名称命名。例如,添加@Bean注解的restTemplate()方法,等价于RestTemplate restTemplate = new RestTemplate();

    创建Controller,在其中使用RestTemplate请求用户微服务的API。

package com.bingbinlee.springcloud.micro.controller;

import com.bingbinlee.springcloud.micro.entity.User;
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.RestController;
import org.springframework.web.client.RestTemplate;


@RestController
public class MovieController {
    @Autowired
    private RestTemplate restTemplate;

    @GetMapping("/user/{id}")
    public User findById(@PathVariable Long id) {
        return this.restTemplate.getForObject("http://localhost:8000/" + id, User.class);
    }
}

    编写配置文件application.yml

server:
  port: 8010
spring:
  application:
    name: movieticketing-consumer-movie
#eureka:
# client:
#    serviceUrl:
#      defaultZone: http://localhost:8761/eureka/
#  instance:
#    prefer-ip-address: true

    至此,一个简单的电影微服务就完成了!

    测试,访问:http://127.0.0.1:8010/user/1 ,结果如下:

    

    测试结果,说明电影微服务可以正常使用RestTemplate调用用户微服务的API。

 四、为项目整合和使用Spring Boot Actuator

    Spring Boot Actuator提供很多监控端点。可使用 http://{ip}:{port}/{endpoint} 的形式访问这些端点,从而了解应用程序的运行状况。

端点 描述 HTTP方法
autoconfig 显示自动配置的信息 GET
beans 显示应用程序上下文所有的Spring bean GET
configprops 显示所有@ConfigurationProperties的配置属性列表 GET
dump 显示线程活动的快照 GET
env 显示应用的环境变量 GET
health 显示应用程序的健康指标,这些值由HealthIndicator的实现类提供 GET
info 显示应用的信息,可使用info.*属性自定义info端点公开的数据 GET
mapping 显示所有的URL路径 GET
metrics 显示应用的度量标准信息 GET
shutdown 关闭应用(默认情况下不启用,如需启动,需设置endpoints.shutdown.enabled=true) POST
trace 显示跟踪信息(默认情况下为最近100个HTTP请求) GET

    如需为项目整合Actuator,为项目添加以下依赖:

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>

    测试访问: http://localhost:8000/health , 如下

    Tips: UP表示运行正常。除UP外,DOWN、OUT_OF_SERVICE、UNKNOWN等状态。

五、硬编码的问题

    以MovieController.java中:

    @GetMapping("/user/{id}")
    public User findById(@PathVariable Long id) {
        return this.restTemplate.getForObject("http://localhost:8000/" + id, User.class);
    }

    由此可知,我们把提供者的网络地址(IP和端口等)硬编码在代码中的,当然,也可将其提取到配置文件中去。例如:

user:
  userServiceUrl:http://localhost:8000

代码改为:

@Value("user.userServiceUrl")
private String userServiceUrl;

@GetMapping("/user/{id}")
public User findById(@PathVariable Long id) {
	return this.restTemplate.getForObject(this.userServiceUrl + id, User.class);
}

    在传统的应用程序中,一般都是这么做。但这种方式有很多问题:

    1、适用场景有局限:如果服务提供者的网络地址(IP和端口)发生了变化,将会影响服务消费者。

    2、无法动态伸缩:在生产环境中,每个微服务一般都会部署多个实例,从而实现容灾和负载均衡。在微服务架构中,还需要系统具备自动伸缩的能力。硬编码无法适应这种需求。

六、微服务注册与发现

    硬编码提供者地址的方式有不少问题。要想解决这些问题,服务消费者需要一个强大的服务发现机制,服务消费者使用这种机制获取服务提供者的网络消息。不仅如此,即使服务提供者的消息发生变化,服务消费者也无须修改配置文件。

    服务发现组件提供这种能力。在微服务架构中,服务发现组件是一个非常关键的组件。

    服务提供者、服务消费者、服务发现组件这三者之间的关系大致如下:

    * 各个微服务在启动时,将自己的网络地址等信息注册到服务发现组件中,服务发现组件会存储这些信息。

    * 服务消费者可从服务发现组件查询服务提供者的网络地址,并使用该地址调用服务提供者的接口。

    * 各个微服务与服务发现组件使用一定的机制(例如,心跳)通信。服务发现组件如长时间无法与某微服务实例通信,就会注销该实例。

    * 微服务网络地址发生变更(例如实例增减或者IP端口发生变化等)时,会重新注册到服务发现组件。使用这种方式,服务消费者就无须人工修改提供者的网络地址了。

    综上所述,服务发现组件应具备以下功能:

    * 服务注册表:是服务发现组件的核心,它用来记录各个微服务的信息(例如微服务的名称、IP、端口等)。服务注册表提供查询API和管理API, 查询API 用于查询可用的微服务实例,管理API用于服务的注册和注销。

    * 服务注册与服务发现:服务注册是指微服务在启动时,将自己的信息注册到服务发现组件上的过程。服务发现是指查询可用微服务列表及其网络地址的机制。

    * 服务检查:服务发现组件使用一定机制定时检测已注册的服务,如发现某实例长时间无法访问,就会从服务注册表中移除该实例。

    Spring Cloud提供了多种服务发现组件的支持,例如Eureka、Consul和Zookeeper等。

    Eureka是Netflix开发的服务发现框架,SpringCloud将它集成在自己的子项目spring-cloud-netflix中,实现SpringCloud的服务发现功能。 
    为什么要使用Eureka,因为在一个完整的系统架构中,任何单点的服务都不能保证不会中断,因此我们需要服务发现机制,在某个节点中断后,其它的节点能够继续提供服务,从而保证整个系统是高可用的。 
    服务发现有两种模式:一种是客户端发现模式,一种是服务端发现模式。Erueka采用的是客户端发现模式。

七、 Rureka简介

    Eureka是Netflix开源的服务发现组件,本身是一个基于REST的服务。它包含Server和Client两部分。Spring Cloud将它集成在子项目Spring Cloud Netflix中,从而实现微服务的注册与发现。

    分析Eureka的原理之前,先了解一下Region和Availability Zone:

    Region 和 Availability Zone均是AWS的概念。其中,Region 表示AWS的地理位置,每个Region都有多个Availability Zone, 各个Region 之间完全隔离。AWS通过这种方式实现了最大的容错和稳定性。

    Spring Cloud 默认使用的Region 是us-east-1, 在非AWS环境下,可以将Availability Zone理解为机房,将Region 理解为跨机房的Eureka集群。

    

    该Eureka官方的架构图比较详细地描述了Eurka集群的工作原理:

    * Application Service 相当于“电影售票系统”中的服务提供者。

    * Application Client 相当于“电影售票系统”中的服务消费者。

    * Make Remote Call, 可以理解成调用RESTful API的行为。

    * us-east-1c、us-east-1d等都是zone,它们属于us-east-1这个region。

    Eureka包含两个组件:Eureka Server 和 Eureka Client, 它们的作用如下:

    * Eureka Server 提供服务发现的能力,各个微服务启动时,会向Eureka Server注册自己的信息(例如IP,端口、微服务名称等),Eureka Server 会存储这些信息。

    * Eureka Client 是一个Java客户端,用于简化与Eureka Server的交互。

    * 微服务启动后,会周期性(默认30秒)地向Eureka Server 发送心跳以续约自己的“租期”。

    * 如果Eureka Server 在一定时间内没有接收到某个微服务实例的心跳,Eureka Server 将会注销该实例(默认90秒)。

    * 默认情况下,Eureka Server同时也是Eureka Client 。多个Eureka Server实例,互相之间通过复制的方式,来实现服务注册表中数据的同步。

    * Eureka Client 会缓存服务注册表中的信息。这种方式有一定的优势——首先,微服务无需每次请求都查询到Eureka Server , 从而降低了Eureka Server 的压力;其次,即使Eureka Server 所有节点都宕掉,服务消费者依然可以使用缓存中的信息找到服务提供者并完成调用。

    综上,Eureka 通过心跳检查、客户端缓存等机制,提高了系统的灵活性、可伸缩性和可用性。

    Tips:    eureka.client.healthcheck.enabled=true只能配置在application.yml中,如果配置在bootstrap.yml中,可能会导致一些不良的副作用,如,应用注册到Eureka Server上的状态是UNKNOWN。

    当eureka.client.healthcheck.enabled=true时,/pause 端点(该端点由Spring Boot Actuator提供,用于暂停应用)无法正常工作,经测试,发现当eureka.client.healthcheck.enabled=true时,请求 /pause 端点无法将应用在Eureka Server 上的状态标记为DOWN。(Spring Boot: Spring Boot 1.4.3RELEASE ; Spring Cloud : Spring Cloud Camden SR4)该Bug尚未修复。

本文为博主原创文章,转载请注明出处!

https://my.oschina.net/u/3375733/blog/

猜你喜欢

转载自my.oschina.net/u/3375733/blog/1512406