Java微服务应用开发(简版)实战之SpringCloud

本文涉及的产品
服务治理 MSE Sentinel/OpenSergo,Agent数量 不受限
云原生网关 MSE Higress,422元/月
注册配置 MSE Nacos/ZooKeeper,118元/月
简介: 简单粗暴的SpringCloud实操

微服务核心模块

cloud.png

这是微服务的基本架构图,不同终端可以通过网关调用我们的核心服务,每个服务可以独立水平扩展,它们各自管辖自己的数据库。下面是SpringCloud相关常见技术栈(模块),我们将通过一个简化后的真实案例来串联起它们:

Eureka/Nacos:服务注册中心,后者由阿里巴巴开源

Ribbon:负载均衡组件

Hystrix:熔断器组件

Feign:请求客户端组件

SpringCloud GateWay:网关组件,提供路由、过滤等功能

1. 准备工作

下面我们通过一个案例来整体介绍这些组件。案例背景:B2C商城里,用户在购物时会生成订单,除了支付业务本身的订单状态处理之外,系统还会围绕这些订单分别给商家、用户端做些处理。最典型的比如,商家端要做订单统计、用户端要做订单查询、积分计算等等。为了将不同端的订单处理分层解耦,通常会划分多个服务,最简单的方案是分为商家服务和用户服务,商家服务管理商家订单、用户服务管理用户订单。

当用户下单后,前端通过调用平台聚合层来分别调用商家、用户服务。

所以,我们可以新建三个服务项目,PlatformDemo、MerchantDemo、UserDemo。按照微服务的理论,每个服务管控自己的数据库,所以可以新建两个单独的库,分别是merchantdb、userdb,然后分别新建各自的订单表merchantorder、userorder。(其实就是垂直分库)

编译相关命令

clean compile package -Dmaven.test.skip=true

PlatformDemo怎么调用MerchantDemo和UserDemo呢?两种方式:

  1. platform直接通过http调用merchant和user,优点是:简单,缺点是:假如merchant和user是多实例的,那么platform需要手动维护每个实例的地址;
  2. 将merchant、user注册到一个服务注册中心,然后platform仅通过单一的【服务名称】来路由到不同的merchnt、user服务实例。优点是:服务实例水平扩展很方便,不需要在platform维护实例地址。缺点是:要安装单独的服务注册中心。

在实际场景中,肯定会选2,原因就在于,微服务的意义就是让服务实例更方便的水平扩展,假如每次还得在调用层手动维护实例地址,会非常麻烦。另外,注册中心只需要安装一次,也不存在其他太复杂的操作。

2. Nacos基本介绍

微服务比较常见的注册中心有Eureka、ZK、Consul、Nacos等。Nacos由阿里巴巴开源,它提供了服务注册、配置管理等功能。其简单易用的风格,越来越受到大家的关注,我们的生产级项目都已采用,目前运行良好。
Nacos注册中心.png

实际上Nacos思路非常简单,它提供中心服务器(可集群扩展,消除单点)及控制台,服务提供者(比如Merchant服务)首先主动注册到中心服务,中心服务轮询其存活状态。服务消费者(比如Platform)根据固定的服务名从中心服务器调用目标服务。这种架构的优点是:服务提供者的水平扩展可以对服务消费者完全透明,后者不需要手动维护前者服务列表。

下面我们以Nacos为例,来对注册中心做个演示。

Nacos服务安装

安装过程可以看这里:https://nacos.io/zh-cn/docs/quick-start.html

我这里是按照源码方式安装,相关nacos命令在 distribution/target/nacos-server-$version/nacos/bin目录下。

启动命令:

sh startup.sh -m standalone

关停命令:

sh shutdown.sh

控制台页面: http://localhost:8848/nacos/ 默认密码:nacos/nacos

使用Nacos进行服务注册

首先引入nacos依赖:

<dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
      <version>>0.2.1.RELEASE</version>
</dependency>

这里我们采用生产验证过的0.2.1版本

代码及配置方面的变动:

在主类上加上@EnableDiscoveryClient注解
在配置文件中新增如下内容:

server.port=0
spring.application.name=merchant-service
spring.cloud.nacos.discovery.server-addr=localhost:8848

这里将port设置为0,意味着每次启动都会使用随机端口号,这主要是因为同一类的微服务实例通常会有多个,使用同样的固定端口会造成端口占用的问题。

Nacos控制台初探

启动主类后,即可在控制台的【服务列表】中看到merchant-service 服务:

nacos01.jpg

这里我们启动了3个实例,点击详情后,我们可以看到实例的权重及运行情况:

nacos2.jpg

在这里,我们可以直接编辑实例的权重,也可以直接上下线实例,后面我们会对此进行演示。

借助Nacos进行微服务调用

如之前所说,我们需要在Platform中调用Merchant服务,完成订单入库的操作。由于Merchant已经注册在了Nacos,所以Platform必须借助Nacos来完成服务的调用。

Platform项目的配置和前面类似,这里不再赘述,我们直接看怎么轮询调用Merchant服务。为了更清楚的演示轮询过程,我们直接采用LoadBalancerClient+RestTemplate的方案手动调用服务。LoadBalancerClient用于通过服务名选取服务信息(ip地址、端口号),RestTemplate用于做Http请求。

下面首先配置RestTemplate:

@Configuration
public class RestTemplateConfig {
    @Bean
    public RestTemplate restTemplate(ClientHttpRequestFactory factory){
        return new RestTemplate(factory);
    }

    @Bean
    public ClientHttpRequestFactory simpleClientHttpRequestFactory(){
        SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
        factory.setReadTimeout(3000);//单位为ms
        factory.setConnectTimeout(3000);//单位为ms
        return factory;
    }
}

然后新建测试类,核心测试代码如下:

ServiceInstance serviceInstance = loadBalancerClient.choose("merchant-service");
String url = String.format("http://%s:%s/merchant/saveOrder",serviceInstance.getHost(),serviceInstance.getPort());
System.out.println("request url:"+url);
Object value=restTemplate.postForObject(url,null,String.class);

代码解释:首先通过ServiceInstance根据权重获取服务信息,该信息包括ip+端口,然后拼接服务地址信息,最后通过RestTemplate进行Http请求。

注意:在test之前,先启动多个merchant服务实例。大家不妨测试一下,假如请求多次,是能看到均衡负载的效果的。

上面这种方式比较手工一点,实际上,我们可以直接让RestTemplate集成Ribbon,实现LoadBalance的效果,做法很简单:

  1. 在构建RestTemplate时加上@LoadBalanced注解:
@LoadBalanced
@Bean
public RestTemplate restTemplate(ClientHttpRequestFactory factory){
    return new RestTemplate(factory);
}
  1. 请求服务时,直接使用服务名而非IP+端口:
restTemplate.postForObject("http://merchant-service/merchant/saveOrder",null,String.class);

3. 微服务调用之Feign

Feign是SpringCloud中非常常用的一个HTTP客户端组件,它提供了接口式的微服务调用API。

首先确保项目中已经导入了Feign依赖:

<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-openfeign</artifactId>
  <version>2.0.0.RELEASE</version>
</dependency>

然后创建目标服务的接口,比如我们这里需要调用Merchant服务,那么可以新建MerchantService接口专门来处理与之相关的服务调用:

@FeignClient(value="merchant-service")
public interface MerchantService {

    @PostMapping("/merchant/saveOrder")
    public String saveMerchantOrder();
}

这个接口非常容易理解:使用@FeignClient将接口定义为服务接口,使用SpringMVC的@PostMapping、@GetMapping注解将接口方法定义为服务映射方法。就这样,调用微服务的方式就和普通方法调用的方式没太大区别(至少感觉上是这样)。

有时候,我们需要在发起Feign请求时,可以做一些统一的处理,比如:header设置、请求监控等。此时我们可以配置Feign拦截器来实现。

Feign拦截器的实现方式非常简单,主要分为两步:

  1. 实现feign.RequestInterceptor接口,重写其apply方法,如下:
public class FeignRequestInterceptor implements RequestInterceptor {

    @Override
    public void apply(RequestTemplate requestTemplate) {
        requestTemplate.header("token","123");
    }
}
  1. 将其配置在@FeignClient(configuration)中:
@FeignClient(value="merchant-service",configuration = FeignRequestInterceptor.class)
public interface MerchantService {


    @PostMapping("/merchant/saveOrder")
    public String saveMerchantOrder();
}

此时我们可以先调整下Merchant服务的接口,使用@RequestHeader("token")来接收token参数。

Feign超时及重试机制

微服务之间调用最大的一个问题就是超时问题(没有之一)。比如说,当Platform调用Merchant时,由于网络不通或者Merchant服务响应缓慢,那么Platform是不能一直等待下去的,这样资源会一致被占用,前端也得不到快速响应。此时一般会设置超时时间。

大家可以测试一下,当连接不上服务端时,会报connect timeout,当服务端响应时间过长,会报read timeout。默认情况下,Feign是不会重试的,即重试逻辑为Retryer.NEVER_RETRY。我们可以根据实际情况作如下配置:

@Configuration
public class FeignConfigure {
    @Bean
    Request.Options feignOptions() {
        return new Request.Options(
                /**connectTimeoutMillis**/
                1 * 1000,
                /** readTimeoutMillis **/
                1 * 5000);
    }

    @Bean
    public Retryer feignRetryer() {
       return new Retryer.Default();
    }
}

该配置类里面,我们设置了连接超时未1秒、读取超时未5秒,然后默认重试机制会重试5次。测试方式比较简单,比如我们可以把Merchant服务从Nacos上摘除下来,或者在接口中手动设置sleep,这里不再给出。

调用方在收到超时异常时,很可能服务方会继续执行(比如执行过长导致de 超时),所以重试的前提是:【一定要保证服务方的幂等性】,即重复多次不会影响业务逻辑。

4. 微服务间的数据传输

在实际开发中,有一个很现实的问题是数据传输格式的约定问题。在微服务架构中,实现一个完整的功能需要涉及到多个服务的调用,每个服务都有与自己领域相关的数据封装,微服务之间的调用需要遵循对方的数据格式要求。以前面的订单为例,Platform在调用Merchant时,应该传入商户订单对象,然后被返回Merchant服务的响应对象。听起来很简单对吧?但是Platform和Merchant是不同项目,后者约定好的对象类在前者是不存在的,前者工程师需要手动新建匹配的类才行。在服务接口非常繁多的情况下,这种手工处理会占用工程师很多时间。为了让他们过的爽一点,我们应该让这些类/对象共享才对。

所以,笔者建议针对每个服务都新建一个DTO项目,专门用于定义数据传输对象。比如我们可以新建MerchantDTO,专门定义该服务对应的输入、输出对象,每次更新升级时,可以将其打入公司的私有仓库中。为了自动化这一过程,可以使用CI/CD工具(比如jenkins)自动拉取git代码并install/deploy到私有仓库。在需要调用Merchant服务时,在pom中加入依赖就可以了。在项目规模较小时,可以暂时只做一个DTO项目,涵盖所有服务,以后再拆也是OK的。

在DTO中,我们会约定两种类型的数据:请求参数值、响应返回值。请求参数与业务域相关。比如保存商户订单,那么请求参数就是商户订单数据,比如:

@ApiModel("商户订单实体")
@Setter
@Getter
public class MerchantOrderRequest {

    @ApiModelProperty(name = "ordername",value = "订单名称")
    private String ordername;

    @ApiModelProperty(name="price",value = "价格")
    private double price;

}

通常来说,响应返回值都会有些公共的字段,比如code、message等,一般来说会设计响应对象的基类,这样便于后面做统一的code处理:

@ApiModel(value = "默认响应实体")
@Setter
@Getter
public class DefaultResponseData {


    @ApiModelProperty(name = "code",value = "返回码,默认1000是成功、5000是失败")
    private String code;

    @ApiModelProperty(name = "message",value = "返回信息")
    private String message;
    /**
     * 额外数据
     */
    @ApiModelProperty(name = "extra",value = "额外数据")
    private String extra;
}

这里用到了swagger注解,这样在接口文档中就会有明确说明,方便调试。@Setter、@Getter主要用于生产Setter/Getter代码,有助于解放大家的双手,具体安装及依赖过程可以看这篇文章:
如何使用Lombok简化你的代码?

我们改造一下之前的saveMerchantOrder方法,让其传入MerchantOrderRequest、返回DefaultResponseData。

public DefaultResponseData saveMerchantOrder(
  @RequestBody MerchantOrderRequest merchantOrderRequest, 
  @RequestHeader("token") String token){
...
    
}

重新启动Merchant服务,打开swagger,可以看到请求和响应参数的描述:
m1.jpg

User服务可以完全按照同样的处理策略,这里不再赘述。

5. 使用Hystrix进行熔断保护(降级)

在分布式/微服务环境中往往会出现各种各样的问题,比如网络异常,超时等,而这些问题可能会导致系统的级联失败,即使不断重试,也可能无法解决的,还会耗费更多的资源。比如说我们Platform在调用Merchant时,后者的数据库突然挂了,然后系统卡顿或者不停报错,用户此时可能会不断刷新,做更多的请求。这最终会让应用程序由于资源耗尽而导致雪崩。遇到这种情况,更好的做法是在调用阶段进行熔断保护并做降级处理。

熔断保护类似于电路中的保险丝,当电流异常升高时会自动切断电流,以保护电路安全。在开发中,熔断器通常有三个状态,即Closed、Open、Half-Open,如下图:

hystrix.png

默认情况下,熔断器是关闭(Closed)的,一旦在某个时间窗口T(默认10秒)内发生异常或者超时的次数在N以上,那么熔断器就会开启(Open),然后在时间窗口S之后,熔断器会进入半开状态(Half-Open),此时假如新请求成功执行,那么会进入关闭状态(Closed),否则继续开启(Open)。为了让熔断后能快速降级,我们通常需要指定相应的fallback处理逻辑。

在SpringCloud中,我们主要使用Hystrix组件来完成熔断降级,下面看看怎么实现。

首先,我们得引入依赖:

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

在启动类上加@EnableHystrix注解,开启Hystrix。

在API层,我们只需要加@HystrixCommand注解即可。如前面所说,当接口熔断后,我们需要指定降级逻辑,即指定fallback方法:

   @GetMapping("/simpleHystrix")
    @HystrixCommand(fallbackMethod = "fallbackHandler"
    })
    public String simpleHystrix(@RequestParam("count") Integer count){
        System.out.println("执行.................");
        int i=10/count;
        return "success";
    }

    public String fallbackHandler(Integer count){
        System.out.println("count="+count);
        return "fail";
    }

这里我们定义了一个简单的接口,当count=0时,很明显会发生异常,在某段时间内出现异常的次数达到阈值,新请求就会进入fallbackHandler进行处理,不会继续调用simpleHystrix的逻辑。我们可以通过commandProperties/@HystrixProperty指定一些基本的参数,比如:

commandProperties = {
            @HystrixProperty(name=HystrixPropertiesManager.CIRCUIT_BREAKER_REQUEST_VOLUME_THRESHOLD,value = "3"),
            @HystrixProperty(name =HystrixPropertiesManager.CIRCUIT_BREAKER_SLEEP_WINDOW_IN_MILLISECONDS,value = "20000")

这里我们指定了10秒内出现3次异常,就会进入Open状态,然后再20秒之后,会进入Half-Open状态。

Feign整合Hystrix

在实际场景中,熔断器解决的大部分是微服务调用的问题,所以这里我们看看怎样让Feign整合Hystrix。

前面提到过的@FeignClient,其实直接支持配置Hystrix。它支持的方式有两种:fallback、fallbackFactory。前者比较简单,仅需要配置当前接口实现类作为降级函数,后者功能丰富一点,可以获取触发降级的原因。我们这里先用前者快速实现一下。

首先定义fallback类,该类实现服务接口及其所有方法,以MerchantService为例:

public class MerchantServiceFallBack implements MerchantService {
    
    @Override
    public DefaultResponseData saveMerchantOrder(MerchantOrderRequest merchantOrderRequest) {
        DefaultResponseData responseData=new DefaultResponseData();
        responseData.setCode("1001");
        responseData.setMessage("fallback");
        return responseData;
    }
}

然后在@FeignClient中加上:

fallback = MerchantServiceFallBack.class

最后,别忘记在配置文件中开启feign-hystrix:

feign.hystrix.enabled=true

当调用MerchantService接口服务时,一旦出现异常情况,会转入MerchantServiceFallBack的逻辑。

6. API网关之Spring Cloud Gateway

API网关主要解决的问题有:API鉴权、流量控制、请求过滤、聚合服务等。它并非微服务的必需品,具体怎么用得看实际场景。目前比较流行的网关有Zuul、Spring Cloud GateWay等。前者比较老牌了,网上资料也较多,而后者是新贵,算是SpringCloud的亲儿子,个人感觉也更好用,我们以它为例来讲解API网关的常见用法。

Spring Cloud Gateway基于Spring5、Reactor以及SpringBoot2构建,提供路由(断言、过滤器)、熔断集成、请求限流、URL重写等功能。

SpringBoot/SpringCloud系列的组件太多,经常会出现版本不对应,以下是经过测试无误的搭配:

  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.0.4.RELEASE</version>
  </parent>
  
  <dependencies>
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-dependencies</artifactId>
      <version>Finchley.RELEASE</version>
      <type>pom</type>
    </dependency>

    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-gateway</artifactId>
      <version>2.0.4.RELEASE</version>
    </dependency>

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

最重要的一步是定义路由:

@Bean
public RouteLocator myRoutes(RouteLocatorBuilder builder) {
    return builder.routes()
            .route(p -> p
                    .path("/api/order/gateWay")
                    .uri("http://localhost:8889"))
                .build();
    }

代码解释:当访问本服务的/api/order/gateWay时,会将请求转发到http://localhost:8889/api/order/gateWay。然后我们也可以在转发请求前进行过滤处理,比如新增header参数、请求参数等,大家可以自行测试:

filters(f -> f.addRequestHeader("token", "123"))

在实际项目中,网关所调用的目标服务都注册在注册中心里面,所以一般来说,会让网关访问注册中心地址。假如用的是Nacos,可以将uri中的http换成lb:

lb://platform-service
相关实践学习
SLB负载均衡实践
本场景通过使用阿里云负载均衡 SLB 以及对负载均衡 SLB 后端服务器 ECS 的权重进行修改,快速解决服务器响应速度慢的问题
负载均衡入门与产品使用指南
负载均衡(Server Load Balancer)是对多台云服务器进行流量分发的负载均衡服务,可以通过流量分发扩展应用系统对外的服务能力,通过消除单点故障提升应用系统的可用性。 本课程主要介绍负载均衡的相关技术以及阿里云负载均衡产品的使用方法。
目录
相关文章
|
11天前
|
JSON Java Apache
非常实用的Http应用框架,杜绝Java Http 接口对接繁琐编程
UniHttp 是一个声明式的 HTTP 接口对接框架,帮助开发者快速对接第三方 HTTP 接口。通过 @HttpApi 注解定义接口,使用 @GetHttpInterface 和 @PostHttpInterface 等注解配置请求方法和参数。支持自定义代理逻辑、全局请求参数、错误处理和连接池配置,提高代码的内聚性和可读性。
|
20天前
|
人工智能 前端开发 Java
基于开源框架Spring AI Alibaba快速构建Java应用
本文旨在帮助开发者快速掌握并应用 Spring AI Alibaba,提升基于 Java 的大模型应用开发效率和安全性。
基于开源框架Spring AI Alibaba快速构建Java应用
|
12天前
|
JSON Java 测试技术
SpringCloud2023实战之接口服务测试工具SpringBootTest
SpringBootTest同时集成了JUnit Jupiter、AssertJ、Hamcrest测试辅助库,使得更容易编写但愿测试代码。
44 3
|
14天前
|
SQL Java 数据库连接
从理论到实践:Hibernate与JPA在Java项目中的实际应用
本文介绍了Java持久层框架Hibernate和JPA的基本概念及其在具体项目中的应用。通过一个在线书店系统的实例,展示了如何使用@Entity注解定义实体类、通过Spring Data JPA定义仓库接口、在服务层调用方法进行数据库操作,以及使用JPQL编写自定义查询和管理事务。这些技术不仅简化了数据库操作,还显著提升了开发效率。
28 3
|
20天前
|
运维 NoSQL Java
后端架构演进:微服务架构的优缺点与实战案例分析
【10月更文挑战第28天】本文探讨了微服务架构与单体架构的优缺点,并通过实战案例分析了微服务架构在实际应用中的表现。微服务架构具有高内聚、低耦合、独立部署等优势,但也面临分布式系统的复杂性和较高的运维成本。通过某电商平台的实际案例,展示了微服务架构在提升系统性能和团队协作效率方面的显著效果,同时也指出了其带来的挑战。
57 4
|
24天前
|
SQL 监控 Java
技术前沿:Java连接池技术的最新发展与应用
本文探讨了Java连接池技术的最新发展与应用,包括高性能与低延迟、智能化管理和监控、扩展性与兼容性等方面。同时,结合最佳实践,介绍了如何选择合适的连接池库、合理配置参数、使用监控工具及优化数据库操作,为开发者提供了一份详尽的技术指南。
31 7
|
21天前
|
SQL Java 数据库连接
在Java应用中,数据库访问常成为性能瓶颈。连接池技术通过预建立并复用数据库连接,有效减少连接开销,提升访问效率
在Java应用中,数据库访问常成为性能瓶颈。连接池技术通过预建立并复用数据库连接,有效减少连接开销,提升访问效率。本文介绍了连接池的工作原理、优势及实现方法,并提供了HikariCP的示例代码。
36 3
|
21天前
|
存储 Java 关系型数据库
在Java开发中,数据库连接是应用与数据交互的关键环节。本文通过案例分析,深入探讨Java连接池的原理与最佳实践
在Java开发中,数据库连接是应用与数据交互的关键环节。本文通过案例分析,深入探讨Java连接池的原理与最佳实践,包括连接创建、分配、复用和释放等操作,并通过电商应用实例展示了如何选择合适的连接池库(如HikariCP)和配置参数,实现高效、稳定的数据库连接管理。
40 2
|
23天前
|
缓存 Java 数据库连接
Hibernate:Java持久层框架的高效应用
通过上述步骤,可以在Java项目中高效应用Hibernate框架,实现对关系数据库的透明持久化管理。Hibernate提供的强大功能和灵活配置,使得开发者能够专注于业务逻辑的实现,而不必过多关注底层数据库操作。
13 1
|
26天前
|
移动开发 前端开发 JavaScript
java家政系统成品源码的关键特点和技术应用
家政系统成品源码是已开发完成的家政服务管理软件,支持用户注册、登录、管理个人资料,家政人员信息管理,服务项目分类,订单与预约管理,支付集成,评价与反馈,地图定位等功能。适用于各种规模的家政服务公司,采用uniapp、SpringBoot、MySQL等技术栈,确保高效管理和优质用户体验。