上文我们创建了注册中心,以及服务的提供者microservice-provider-user,并成功地将服务提供者注册到了注册中心上。

要想消费microservice-provider-user的服务是很简单的,我们只需要使用RestTemplate即可,或者例如HttpClient之类的http工具也是可以的。但是在集群环境下,我们必然是每个服务部署多个实例,那么服务消费者消费服务提供者时的负载均衡又要如何做呢?

准备工作

  1. 启动注册中心:microservice-discovery-eureka
  2. 启动服务提供方:microservice-provider-user
  3. 修改microservice-provider-user的端口为8002,另外启动一个实例

此时,访问http://discovery:8761

Eureka上面注册2个服务提供者

可以在Eureka中看到microservice-provider-user有两个实例在运行。

下面我们创建一个新的微服务(microservice-consumer-movie-*),负载均衡地消费microservice-provider-user的服务。

负载均衡:Ribbon

Ribbon介绍

Ribbon是Netflix发布的开源项目,主要功能是提供客户端的软件负载均衡算法,将Netflix的中间层服务连接在一起。Ribbon客户端组件提供一系列完善的配置项如连接超时,重试等。简单的说,就是在配置文件中列出Load Balancer后面所有的机器,Ribbon会自动的帮助你基于某种规则(如简单轮询,随即连接等)去连接这些机器。我们也很容易使用Ribbon实现自定义的负载均衡算法。简单地说,Ribbon是一个客户端负载均衡器。

Ribbon代码示例

创建一个Maven项目,并在pom.xml中加入如下内容:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <artifactId>microservice-consumer-movie-ribbon</artifactId>
    <packaging>jar</packaging>

    <parent>
        <groupId>com.itmuch.cloud</groupId>
        <artifactId>spring-cloud-microservice-study</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>

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

        <!-- 整合ribbon -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-ribbon</artifactId>
        </dependency>

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

启动类:MovieRibbonApplication.java。使用@LoadBalanced注解,为RestTemplate开启负载均衡的能力。

@SpringBootApplication
@EnableDiscoveryClient
public class MovieRibbonApplication {
    /**
     * 实例化RestTemplate,通过@LoadBalanced注解开启均衡负载能力.
     * @return restTemplate
     */
    @Bean
    @LoadBalanced
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }

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

实体类:User.java

public class User {
    private Long id;
    private String username;
    private Integer age;
    ...
    // getters and setters
}

Ribbon的测试类:TestRibbonController.java

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

    @GetMapping("/ribbon/{id}")
    public User findById(@PathVariable Long id) {
        // http://服务提供者的serviceId/url
        return this.restTemplate.getForObject("http://microservice-provider-user/1", User.class);
    }
}

application.yml

server:
  port: 8011
spring:
  application:
    name: microservice-consumer-movie-ribbon
eureka:
  client:
    serviceUrl:
      defaultZone: http://discovery:8761/eureka/
  instance:
    preferIpAddress: true

启动后,访问多次http://localhost:8011/ribbon/1,返回结果:

{
    "id": 1,
    "username": "Tom",
    "age": 12
}

然后打开两个microservice-provider-user实例的控制台,发现两个实例都输出了类似如下的日志内容:

Hibernate: select user0_.id as id1_0_0_, user0_.age as age2_0_0_, user0_.username as username3_0_0_ from user user0_ where user0_.id=?
2016-08-30 16:25:36.742 TRACE 24404 --- [nio-8002-exec-3] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [BIGINT] - [1]
2016-08-30 16:25:36.743 TRACE 24404 --- [nio-8002-exec-3] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([age2_0_0_] : [INTEGER]) - [12]
2016-08-30 16:25:36.743 TRACE 24404 --- [nio-8002-exec-3] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([username3_0_0_] : [VARCHAR]) - [Tom]

至此,我们已经通过Ribbon在客户端侧实现了均衡负载。

Feign

Feign介绍

Feign是一个声明式的web service客户端,它使得编写web service客户端更为容易。创建接口,为接口添加注解,即可使用Feign。Feign可以使用Feign注解或者JAX-RS注解,还支持热插拔的编码器和解码器。Spring Cloud为Feign添加了Spring MVC的注解支持,并整合了Ribbon和Eureka来为使用Feign时提供负载均衡。

翻译自:http://projects.spring.io/spring-cloud/docs/1.0.3/spring-cloud.html#spring-cloud-feign

Feign示例

创建一个Maven项目,并在pom.xml添加如下内容:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <artifactId>microservice-consumer-movie-feign</artifactId>
    <packaging>jar</packaging>

    <parent>
        <groupId>com.itmuch.cloud</groupId>
        <artifactId>spring-cloud-microservice-study</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>

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

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

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

启动类:MovieFeignApplication.java

/**
 * 使用@EnableFeignClients开启Feign
 * @author eacdy
 */
@SpringBootApplication
@EnableFeignClients
@EnableDiscoveryClient
public class MovieFeignApplication {
    public static void main(String[] args) {
        SpringApplication.run(MovieFeignApplication.class, args);
    }
}

实体类:User.java

public class User {
    private Long id;
    private String username;
    private Integer age;
    ...
    // getters and setters
}

Feign测试类:UserClient.java。

/**
 * 使用@FeignClient("microservice-provider-user")注解绑定microservice-provider-user服务,还可以使用url参数指定一个URL。
 * @author eacdy
 */
@FeignClient(name = "microservice-provider-user")
public interface UserClient {
    @RequestMapping("/{id}")
    public User findById(@RequestParam("id") Long id);
}

Feign的测试类:TestFeignController.java

@RestController
public class TestFeignController {
    @Autowired
    private UserClient userClient;

    @GetMapping("feign/{id}")
    public User test(@PathVariable Long id) {
        User user = this.userClient.findById(id);
        return user;
    }
}

application.yml

server:
  port: 8012
spring:
  application:
    name: microservice-consumer-movie-feign
eureka:
  client:
    serviceUrl:
      defaultZone: http://discovery:8761/eureka/
  instance:
    preferIpAddress: true
ribbon:
  eureka:
    enabled: true         # 默认为true。如果设置为false,Ribbon将不会从Eureka中获得服务列表,而是使用静态配置的服务列表。静态服务列表可使用:<client>.ribbon.listOfServers来指定。参考:http://projects.spring.io/spring-cloud/docs/1.0.3/spring-cloud.html#spring-cloud-ribbon-without-eureka

### 参考:https://spring.io/guides/gs/client-side-load-balancing/

同样的,启动该应用,多次访问[http://192.168.0.59:8012/feign/1,我们会发现和Ribbon示例一样实现了负载均衡。

代码地址(任选其一):

Ribbon代码地址:

http://git.oschina.net/itmuch/spring-cloud-study/tree/master/microservice-consumer-movie-ribbon
https://github.com/eacdy/spring-cloud-study/tree/master/microservice-consumer-movie-ribbon

Feign代码地址:

http://git.oschina.net/itmuch/spring-cloud-study/tree/master/microservice-consumer-movie-feign
https://github.com/eacdy/spring-cloud-study/tree/master/microservice-consumer-movie-feign

服务提供者和服务消费者

“服务提供者”和“服务消费者”的名词是借用的,笔者并没有在Spring Cloud中看到这样的概念。下面这张表格,简单描述了服务提供者/消费者是什么:

名词 概念
服务提供者 服务的被调用方(即:为其他服务提供服务的服务)
服务消费者 服务的调用方(即:依赖其他服务的服务)

服务提供者(microservice-provider-user)

这是一个稍微有点复杂的程序。我们使用spring-data-jpa操作h2数据库,同时将该服务注册到注册中心Eureka中。

创建一个Maven工程,并在pom.xml中添加如下内容:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <artifactId>microservice-provider-user</artifactId>
    <packaging>jar</packaging>

    <parent>
        <groupId>com.itmuch.cloud</groupId>
        <artifactId>spring-cloud-microservice-study</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>

    <!-- 添加Eureka的依赖 -->
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-eureka</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>
    </dependencies>
</project>

配置文件:application.yml

server:
  port: 8001
spring:
  application:
    name: microservice-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数据库的insert脚本
logging:
  level:
    root: INFO
    org.hibernate: INFO
    org.hibernate.type.descriptor.sql.BasicBinder: TRACE
    org.hibernate.type.descriptor.sql.BasicExtractor: TRACE
    com.itmuch.youran.persistence: ERROR
eureka:
  client:
    serviceUrl:
      defaultZone: http://discovery:8761/eureka/    # 指定注册中心的ip
  instance:
    preferIpAddress: true

建表语句:schema.sql

drop table user if exists;
create table user (id bigint generated by default as identity, username varchar(255), age int, primary key (id));

插库语句:data.sql

insert into user (id, username, age) values (1,'Tom',12);
insert into user (id, username, age) values (2,'Jerry', 23);
insert into user (id, username, age) values (3,'Reno', 44);
insert into user (id, username, age) values (4,'Josh', 55);

DAO:

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

Controller:

/**
 * 作用:
 * ① 测试服务实例的相关内容
 * ② 为后来的服务做提供
 * @author eacdy
 */
@RestController
public class UserController {
    @Autowired
    private DiscoveryClient discoveryClient;
    @Autowired
    private UserRepository userRepository;

    /**
     * 注:@GetMapping("/{id}")是spring 4.3的新注解等价于:
     * @RequestMapping(value = "/id", method = RequestMethod.GET)
     * 类似的注解还有@PostMapping等等
     * @param id
     * @return user信息
     */
    @GetMapping("/{id}")
    public User findById(@PathVariable Long id) {
        User findOne = userRepository.findOne(id);
        return findOne ;
    }

    /**
     * 本地服务实例的信息
     * @return
     */
    @GetMapping("/instance-info")
    public ServiceInstance showInfo(){
        ServiceInstance localServiceInstance = this.discoveryClient.getLocalServiceInstance();
        return localServiceInstance;
    }
}

实体类:

@Entity
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    @Column
    private String username;
    @Column
    private Integer age;
    ...
    //getters and setters...
}

编写Spring Boot启动程序,通过@EnableDiscoveryClient注解,即可将microservice-provider-user服务注册到Eureka上面去

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

至此,代码编写完成。

我们依次启动Eureka服务和microservice-provider-user服务。

访问:http://localhost:8761,如下图。我们会发现microservice-provider-user服务已经被注册到了Eureka上面了。

microservice-provider-user服务注册到Eureka上

访问:http://localhost:8001/instance-info,返回结果:

{
    "host": "10.0.75.1",
    "port": 8001,
    "metadata": {},
    "uri": "http://10.0.75.1:8001",
    "secure": false,
    "serviceId": "microservice-provider-user"
}

访问:[http://discovery:8001/1,返回结果:

{
    "id": 1,
    "username": "Tom",
    "age": 12
}

代码地址(任选其一):

http://git.oschina.net/itmuch/spring-cloud-study/tree/master/microservice-provider-user
https://github.com/eacdy/spring-cloud-study/tree/master/microservice-provider-user

关于服务发现

在微服务架构中,服务发现(Service Discovery)是关键原则之一。手动配置每个客户端或某种形式的约定是很难做的,并且很脆弱。Spring Cloud提供了多种服务发现的实现方式,例如:Eureka、Consul、Zookeeper。本文暂时只讲述基于Eureka的服务发现。后续会补上基于Consul和Zookeeper的服务发现。

Eureka Server示例

创建一个Maven工程(microservice-discovery-eureka),并在pom.xml中加入如下内容:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <artifactId>microservice-discovery-eureka</artifactId>
    <packaging>jar</packaging>

    <parent>
        <groupId>com.itmuch.cloud</groupId>
        <artifactId>spring-cloud-microservice-study</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>

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

编写Spring Boot启动程序:通过@EnableEurekaServer申明一个注册中心

/**
 * 使用Eureka做服务发现.
 * @author eacdy
 */
@SpringBootApplication
@EnableEurekaServer
public class EurekaApplication {
    public static void main(String[] args) {
        SpringApplication.run(EurekaApplication.class, args);
    }
}

在默认情况下,Eureka会将自己也作为客户端尝试注册,所以在单机模式下,我们需要禁止该行为,只需要在application.yml中如下配置:

server:
  port: 8761                    # 指定该Eureka实例的端口

eureka:
  instance:
    hostname: discovery         # 指定该Eureka实例的主机名
  client:
    registerWithEureka: false
    fetchRegistry: false
    serviceUrl:
      defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/

# 参考文档:http://projects.spring.io/spring-cloud/docs/1.0.3/spring-cloud.html#_standalone_mode
# 参考文档:http://my.oschina.net/buwei/blog/618756

启动工程后,访问:http://discovery:8761/,如下图。我们会发现此时还没有服务注册到Eureka上面。

Eureka首页

代码地址(任选其一):

http://git.oschina.net/itmuch/spring-cloud-study/tree/master/microservice-discovery-eureka
https://github.com/eacdy/spring-cloud-study/tree/master/microservice-discovery-eureka

什么是微服务架构

近年来,在软件开发领域关于微服务的讨论呈现出火爆的局面,有人倾向于在系统设计与开发中采用微服务方式实现软件系统的松耦合、跨部门开发;同时,反对之声也很强烈,持反对观点的人表示微服务增加了系统维护、部署的难度,导致一些功能模块或代码无法复用,同时微服务允许使用不同的语言和框架来开发各个系统模块,这又会增加系统集成与测试的难度,而且随着系统规模的日渐增长,微服务在一定程度上也会导致系统变得越来越复杂。尽管一些公司已经在生产系统中采用了微服务架构,并且取得了良好的效果;但更多公司还是处在观望的态度。

什么是微服务架构呢?简单说就是将一个完整的应用(单体应用)按照一定的拆分规则(后文讲述)拆分成多个不同的服务,每个服务都能独立地进行开发、部署、扩展。服务于服务之间通过注入RESTful api或其他方式调用。大家可以搜索到很多相关介绍和文章。本文暂不细表。

在此推荐两个比较好的博客:

http://microservices.io/
http://martinfowler.com/articles/microservices.html

Spring Cloud 简介

Spring Cloud是在Spring Boot的基础上构建的,为开发人员提供快速建立分布式系统中的一些常见的模式

例如:配置管理(configuration management),服务发现(service discovery),断路器(circuit breakers),智能路由( intelligent routing),微代理(micro-proxy),控制总线(control bus),一次性令牌( one-time tokens),全局锁(global locks),领导选举(leadership election),分布式会话(distributed sessions),集群状态(cluster state)。

Spring Cloud 包含了多个子项目:

例如:Spring Cloud Config、Spring Cloud Netflix等

Spring Cloud 项目主页:http://projects.spring.io/spring-cloud/

Talk is cheep, show me the code.下面我们将以代码与讲解结合的方式,为大家讲解Spring Cloud中的各种组件。

准备

环境准备:

工具 版本或描述
JDK 1.8
IDE STS 或者 IntelliJ IDEA
Maven 3.x

主机名配置:

主机名配置(C:\Windows\System32\drivers\etc\hosts文件)
127.0.0.1 discovery configserver gateway

在进入主题之前,我们首先创建一个父项目(spring-cloud-microservice-study),这样可以对项目中的Maven依赖进行统一的管理。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.itmuch.cloud</groupId>
    <artifactId>spring-cloud-microservice-study</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>pom</packaging>

    <modules>
        <module>microservice-discovery-eureka</module>
        <module>microservice-provider-user</module>
    </modules>

    <!-- 使用最新的spring-boot版本 -->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.4.0.RELEASE</version>
    </parent>

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

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Brixton.SR4</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>
</project>