3

Spring Cloud之Finchley版学习(二)-构建分布式应用

 3 years ago
source link: https://www.wencst.com/archives/1140
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

上一节说过,Spring Cloud是一个快速构建分布式应用的工具集。本节,我们就来编写一个简单的分布式应用,并探讨这个分布式应用有哪些问题。

服务消费者 & 提供者

本书使用服务提供者与服务消费者来描述微服务之间的调用关系。下表解释了服务提供者与服务消费者。

表-服务提供者与服务消费者

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

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

图3-1 服务提供者与服务消费者

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

TIPS

服务消费者和服务提供者描述的只是微服务之间的调用关系,一般成对出现。例如本文,用户微服务是是电影微服务的服务提供者,电影微服务是用户微服务的服务消费者。很多初学者和笔者交流时,会描述提供者如何如何……仿佛消费者和提供者是微服务的固有属性,这是不对的——例如A调用B,B调用C,那么B相对A就是提供者,B相对C就消费者。

Spring Boot/Spring Cloud应用开发套路

Spring Boot/Spring Cloud时代后,应用开发基本遵循三板斧

至于你的业务代码,该怎么写还怎么写。

TIPS

对于懒人,可使用Spring Initilizr(IDEA、Spring Tool Suite等IDE上均有集成,也可在http://start.spring.io 使用网页版)创建应用,它会给你生成项目的依赖以及项目的骨架。后续,笔者会以番外的形式更新相关教程。

编写服务提供者【用户微服务】

  • 创建一个Maven项目,依赖如下:
    <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>microservice-simple-provider-user</artifactId>
      <version>0.0.1-SNAPSHOT</version>
      <packaging>jar</packaging>
      <!-- 引入spring boot的依赖 -->
      <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.7.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>
        <!-- 引入H2数据库,一种内嵌的数据库,语法类似MySQL -->
        <dependency>
          <groupId>com.h2database</groupId>
          <artifactId>h2</artifactId>
        </dependency>
        <!-- 引入Lombok -->
        <dependency>
          <groupId>org.projectlombok</groupId>
          <artifactId>lombok</artifactId>
          <optional>true</optional>
        </dependency>
      </dependencies>
      <!-- 引入spring cloud的依赖,不能少,主要用来管理Spring Cloud生态各组件的版本 -->
      <dependencyManagement>
        <dependencies>
          <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>Finchley.SR2</version>
            <type>pom</type>
            <scope>import</scope>
          </dependency>
        </dependencies>
      </dependencyManagement>
      <!-- 添加spring-boot的maven插件,不能少,打jar包时得用 -->
      <build>
        <plugins>
          <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
          </plugin>
        </plugins>
      </build>
    </project>

    其中,spring-boot-starter-web 提供了Spring MVC的支持;spring-boot-starter-data-jpa提供了Spring Data JPA的支持;h2 是一种内嵌的数据库,语法和MySQL类似(笔者实在没有动力为了简单的演示再写一大堆内容去演示怎么安装MySQL数据库);lombok 则是一款开发利器,可以帮助你简化掉N多冗余代码。

    WARNING

    TIPS

  • 创建实体类:
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
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;
}
  • 创建DAO:
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
}
  • 创建Controller:
    @RequestMapping("/users")
    @RestController
    public class UserController {
      @Autowired
      private UserRepository userRepository;
      @GetMapping("/{id}")
      public Optional<User> findById(@PathVariable Long id) {
        return this.userRepository.findById(id);
      }
    }

    其中, @GetMapping,是Spring 4.3提供的新注解。它是一个组合注解,等价于@RequestMapping(method = RequestMethod.GET),用于简化开发。同理还有@PostMapping@PutMapping@DeleteMapping@PatchMapping等。

  • 编写启动类:
    @SpringBootApplication
    public class ProviderUserApplication {
      public static void main(String[] args) {
        SpringApplication.run(ProviderUserApplication.class, args);
      }
      /**
       * 初始化用户信息
       * 注:Spring Boot2不能像1.x一样,用spring.datasource.schema/data指定初始化SQL脚本,否则与actuator不能共存
       * 原因详见:
       * https://github.com/spring-projects/spring-boot/issues/13042
       * https://github.com/spring-projects/spring-boot/issues/13539
       *
       * @param repository repo
       * @return runner
       */
      @Bean
      ApplicationRunner init(UserRepository repository) {
        return args -> {
          User user1 = new User(1L, "account1", "张三", 20, new BigDecimal(100.00));
          User user2 = new User(2L, "account2", "李四", 28, new BigDecimal(180.00));
          User user3 = new User(3L, "account3", "王五", 32, new BigDecimal(280.00));
          Stream.of(user1, user2, user3)
            .forEach(repository::save);
        };
      }
    }

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

    在启动时,我们使用了ApplicationRunner init(UserRepository repository) 初始化了三条数据,分别是张三、李四、王五。@Bean 则是一个方法注解,作用是实例化一个Bean并使用该方法的名称命名。类似于XML配置方式的<bean id="init" class="...ApplicationRunner"/> 。

  • 编写配置文件application.yml :
    server:
      # 指定Tomcat端口
      port: 8000
    spring:
      jpa:
        # 让hibernate打印执行的SQL
        show-sql: true
    logging:
      level:
        root: INFO
        # 配置日志级别,让hibernate打印出执行的SQL参数
        org.hibernate: INFO
        org.hibernate.type.descriptor.sql.BasicBinder: TRACE
        org.hibernate.type.descriptor.sql.BasicExtractor: TRACE
    

    传统Web应用开发中,常使用properties格式文件作为配置文件。Spring Boot以及Spring Cloud支持使用properties或者yml格式的文件作为配置文件。

    yml文件格式是YAML(Yet Another Markup Language)编写的文件格式,YAML和properties格式的文件可互相转换,例如本节中的application.yml,就等价于如下的properties文件:

    server.port=8000
    spring.jpa.show-sql=true
    logging.level.root=INFO
    logging.level.org.hibernate=INFO
    logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE
    logging.level.org.hibernate.type.descriptor.sql.BasicExtractor=TRACE
    

    从中不难看出,YAML比properties结构清晰;可读性、可维护性也更强,并且语法非常简洁。因此,本书使用YAML格式作为配置文件。但,yml有严格的缩进,并且key与value之间使用: 分隔,冒号后的空格不能少,请大家注意

访问http://localhost:8000/users/1 ,可获得结果:

{"id":1,"username":"account1","name":"张三","age":20,"balance":100.00}

编写服务消费者【电影微服务】

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

  • 创建一个Maven项目,ArtifactId是microservice-simple-consumer-movie 。
  • 加依赖:
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
  • 创建实体类:
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class User {
      private Long id;
      private String username;
      private String name;
      private Integer age;
      private BigDecimal balance;
    }
  • 创建启动类:
    @SpringBootApplication
    public class ConsumerMovieApplication {
      @Bean
      public RestTemplate restTemplate() {
        return new RestTemplate();
      }
      public static void main(String[] args) {
        SpringApplication.run(ConsumerMovieApplication.class, args);
      }
    }
  • 创建Controller:
    @RequestMapping("/movies")
    @RestController
    public class MovieController {
      @Autowired
      private RestTemplate restTemplate;
      @GetMapping("/users/{id}")
      public User findById(@PathVariable Long id) {
        // 这里用到了RestTemplate的占位符能力
        User user = this.restTemplate.getForObject("http://localhost:8000/users/{id}", User.class, id);
        // ...电影微服务的业务...
        return user;
      }
    }

    由代码可知,Controller使用RestTemplate调用用户微服务的RESTful API。

  • 编写配置文件application.yml :
    server:
      port: 8010
    

拓展阅读

本文使用RestTemplate实现了基于HTTP的远程调用,事实上,Spring 5开始,WebFlux提供了Reactive的Web Client:WebClinet ,使用方式和RestTemplate基本类似,但性能更强,吞吐更好。有兴趣的可前往https://docs.spring.io/spring/docs/5.1.3.RELEASE/spring-framework-reference/web-reactive.html#webflux-client-builder 了解。在这里,笔者对WebClient做了一些简单的封装,也可关注:https://github.com/itmuch/thor-test/blob/master/src/main/java/com/itmuch/thor/httpclient/WebClientUtil.java

访问:http://localhost:8010/movies/users/1 ,结果如下:

{"id":1,"username":"account1","name":"张三","age":20,"balance":100.00}

存在的问题

至此,我们已经实现了这个最简单的分布式应用,应用之间通过HTTP通信。代码非常简单,但这些简单的代码里,存在着若干问题:

  • 应用没有监控,没有画板,一切指标都没有。在这个Growth Hack逐渐成为主流的时代,不弄个Dashboard把系统压力、QPS、CPU、内存、日活啥的可视化,你好意思出来混吗……
  • 地址硬编码问题——电影微服务中将用户微服务的地址写死,如果用户微服务地址发生变化,难道要重新上线电影微服务吗?

    你可能会质疑:用户微服务地址为什么会变,让它保持不变就行了啊,这不是问题。这里举两个例子:

    例1:如果你用Docker,那么地址几乎每次启动都会变……

    例2:你之前用的是TXYun,后来你想把用户微服务迁移到Aliyun。这个时候IP就会发生变化。我相信你不会乐意找到哪些服务调用了用户微服务的接口,然后所有调用用户微服务的服务统一修改地址……

  • 负载均衡如何考虑?难道得在电影微服务和用户微服务之间加个NGINX做负载均衡吗?听起来是可行的,但如果有10000+服务(这并不夸张,我司的微服务数目是这个数字乘以N,N >= m,哈哈哈)那这个NGINX的配置得有多复杂……
  • 服务之间没有容错机制,相信对技术有激情的你已经不止一次听过容错、降级、fallback、回退之类的词汇。
  • 如果应用发生故障,你怎么迅速找到问题所在?
  • 用户认证和授权呢?被狗吃了吗?

如上词汇,你可能看得懂,你也可能看不懂。没有关系,请继续阅读,笔者将会用通俗的语言去描述,在你看完本系列后,你会知道,原来那些所谓的高大上的理论、术语、技术,原来也就是这么回事儿。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK