前言(微服务架构下需要解决的问题)
后台服务之间如何通信
在之前《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. 测试
访问账户服务,远程调用成功。