03、Spring Cloud 2020.x 集成Open Feign

前言(微服务架构下需要解决的问题)

后台服务之间如何通信

在之前《Spring Cloud Gateway系列》中,我们介绍了微服务架构下,前后端分离,客户端通过API 网关来和后台应用进行通信,网关提供统一服务入口。
 
而所有的微服务都是独立的 Java 进程跑在独立的虚拟机上,所以服务间的通信就是 IPC(Inter Process Communication),已经有很多成熟的方案。现在基本最通用的有两种方式:同步调用、异步消息调用

同步调用

  • REST(JAX-RS,Spring Boot)
  • RPC(Thrift, Dubbo)

同步调用比较简单,一致性强,但是容易出调用问题,性能体验上也会差些,特别是调用层次多的时候。一般 REST 基于 HTTP,更容易实现,更容易被接受,服务端实现技术也更灵活些,各个语言都能支持,同时能跨客户端,对客户端没有特殊的要求,只要封装了 HTTP 的 SDK 就能调用,所以相对使用的广一些。RPC 也有自己的优点,传输协议更高效,安全更可控,特别在一个公司内部,如果有统一个的开发规范和统一的服务框架时,他的开发效率优势更明显些。就看各自的技术积累实际条件,自己的选择了。

异步消息调用

  • Kafka
  • Notify
  • MessageQueue
     
    异步消息的方式在分布式系统中有特别广泛的应用,他既能减低调用服务之间的耦合,又能成为调用之间的缓冲,确保消息积压不会冲垮被调用方,同时能保证调用方的服务体验,继续干自己该干的活,不至于被后台性能拖慢。不过需要付出的代价是一致性的减弱,需要接受数据 最终一致性;还有就是后台服务一般要实现 幂等性,因为消息送出于性能的考虑一般会有重复(保证消息的被收到且仅收到一次对性能是很大的考验);最后就是必须引入一个独立的 Broker。

如此多的服务,如何实现?

在微服务架构中,一般每一个服务都是有多个拷贝,来做负载均衡。一个服务随时可能下线,也可能应对临时访问压力增加新的服务节点。服务之间如何相互感知?服务如何管理?

这就是服务发现的问题了。一般有两类做法,也各有优缺点。基本都是通过 Zookeeper 等类似技术做服务注册信息的分布式管理。当服务上线时,服务提供者将自己的服务信息注册到 ZK(或类似框架),并通过心跳维持长链接,实时更新链接信息。服务调用者通过 ZK 寻址,根据可定制算法,找到一个服务,还可以将服务信息缓存在本地以提高性能。当服务下线时,ZK 会发通知给服务客户端。

基于客户端的服务注册与发现

优点是架构简单,扩展灵活,只对服务注册器依赖。缺点是客户端要维护所有调用服务的地址,有技术难度,一般大公司都有成熟的内部框架支持,比如 Dubbo。
 

基于服务端的服务注册与发现

优点是简单,所有服务对于前台调用方透明,一般在小公司在云服务上部署的应用采用的比较多。
 

服务挂了,如何解决?

前面提到,Monolithic 方式开发一个很大的风险是,把所有鸡蛋放在一个篮子里,一荣俱荣,一损俱损。而分布式最大的特性就是网络是不可靠的。通过微服务拆分能降低这个风险,不过如果没有特别的保障,结局肯定是噩梦。所以当我们的系统是由一系列的服务调用链组成的时候,我们必须确保任一环节出问题都不至于影响整体链路。相应的手段有很多:

  • 重试机制
  • 限流
  • 熔断机制
  • 负载均衡
  • 降级(本地缓存)

Cloud 2020.x 集成Open Feign

在之前的案例中,我们介绍了Feign和其原生注解的使用,接下来使用 Spring Cloud Open Feign来实现微服务架构下,如何进行服务之间的通信。

Open Feign是Spring Cloud 在Feign的基础上支持了Spring MVC的注解支持,如 @RequesMapping@Pathvariable等等。Open Feign@FeignClient可以解析Spring MVC的@RequestMapping等注解下的接口,并通过动态代理的方式产生实现类,实现类中做负载均衡并调用服务。

1. 创建项目

首先创建项目结构,创建一个账号服务及订单服务,账号服务通过Feign 调用订单服务远程接口,实现下订单功能。
 

2. 引入依赖

在feign-demo 中,直接引入版本控制及相关jar 包依赖,注意 Spring Cloud 2020 以上版本,移除了 Ribbon ,使用loadbalancer 作为负载均衡插件,这里需要注意下,各组件版本如下:

  • spring.boot:2.5.2
  • spring.cloud:2020.0.3
  • alibaba cloud : 2.2.6.RELEASE
<?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>org.pearl</groupId>
    <artifactId>feign-demo</artifactId>
    <packaging>pom</packaging>
    <version>1.0-SNAPSHOT</version>
    <modules>
        <module>account-service</module>
        <module>order-service</module>
    </modules>

    <properties>
        <java.version>1.8</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <!--Spring-->
        <spring.boot.version>2.5.2</spring.boot.version>
        <spring.cloud.version>2020.0.3</spring.cloud.version>
        <spring.cloud.alibaba.version>2.2.6.RELEASE</spring.cloud.alibaba.version>
    </properties>

    <!--Spring版本-->
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>${spring.cloud.alibaba.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring.boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <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>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
            <!--不使用Ribbon 进行客户端负载均衡-->
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.cloud</groupId>
                    <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <!--SpringCloud Feign在Hoxton.M2 RELEASED版本之后不再使用Ribbon而是使用spring-cloud-loadbalancer-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-loadbalancer</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

    <repositories>
        <repository>
            <id>aliyun-repos</id>
            <url>https://maven.aliyun.com/nexus/content/groups/public/</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </repository>
    </repositories>
    <pluginRepositories>
        <pluginRepository>
            <id>aliyun-plugin</id>
            <url>https://maven.aliyun.com/nexus/content/groups/public/</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </pluginRepository>
    </pluginRepositories>

</project>

在account-service 、order-service中,添加YML配置文件,主要是Nacos地址、端口号、服务名配置

server:
  port: 9000
spring:
  cloud:
    nacos:
      server-addr: localhost:8848
  application:
    name: account-service

3. 编写测试代码

添加启动类,主要是使用@EnableDiscoveryClient@EnableFeignClients开启服务发现及@FeignClient注解扫描。

@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class AccountApp {

    public static void main(String[] args) {

        SpringApplication.run(AccountApp.class,args);
    }
}

在order-service 服务中,添加订单实体类和添加订单接口。

订单实体类

@Data
public class Order {

    private Long accountId;
    private String commodityCode;
    private Long count;
    private Long money;
}

添加订单接口

@RestController
@RequestMapping("/order")
public class OrderTblController {

    @GetMapping("insert")
    public Order insertOrder(Long accountId, String commodityCode, Long count, Long money) {

        // 模拟下订单并返回
        Order order = new Order();
        order.setAccountId(accountId);
        order.setCommodityCode(commodityCode);
        order.setCount(count);
        order.setMoney(1L);
        return order;
    }
}

在account-service 中,添加一个Feign 接口,@FeignClient 指定要调用的服务名,也就是在订单服务在Nacos 中注册的服务名,@GetMapping指定远程调用的接口路径。

@FeignClient(name = "order-service")
public interface OrderFeign {

    @GetMapping("order/insert")
    Order insertOrder(@RequestParam Long accountId, @RequestParam  String commodityCode, @RequestParam  Long count, @RequestParam  Long money);
}

再添加一个测试接口,注入Feign客户端接口,使用Feign 客户端远程调用订单服务。

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

    @Autowired
    OrderFeign orderFeign;

    @GetMapping("/insertOrder")
    public Object insertOrder() {

        // 模拟给当前账户下单
        return orderFeign.insertOrder(111L, "iphone11", 1L, 100L);
    }
}

4. 测试

访问账户服务,远程调用成功。
&nbsp;

参考文档

千锋教育