💬 概述
随着应用技术的发展和架构的演进,微服务从最初的概念到如今早已成为越来越多企业的选择用于开发业务应用。微服务架构并不是一蹴而就的,而是通过一步步演进来的,从单体应用到分布式应用,再到微服务化。在微服务架构下服务部署更简单高效、负载扩展更快速、架构升级更灵活、故障容错更有保障,可以说微服务架构极大提升了开发效率和系统整体的稳定性。
然而微服务在实施过程中,也很容易遇到许多难点。微服务伴随着系统复杂性,开发、运维部署的复杂度增加,进而影响开发迭代的速度,甚至影响系统的整体稳定性。因此对于微服务系统有必要对微服务架构作系统设计。
本文将引领读者简单了解 Spring Cloud 微服务体系并完成技术选型,之后基于 Spring Cloud Alibaba 实现订单服务与库存服务的通信完成一个最简单的微服务治理过程,其中包括服务注册与发现、负载均衡,以及服务远程过程调用。
🧭 微服务体系&技术选型
Spring Cloud 微服务体系
Spring Cloud 是分布式微服务架构的一站式解决方案,它提供了一套简单易用的编程模型,使我们能在 Spring Boot 的基础上轻松地实现微服务系统的构建。 Spring Cloud 提供以微服务为核心的分布式系统构建标准。
Spring Cloud 本身并非一个开箱即用的框架,而是一套微服务规范,共有两代实现:
- Spring Cloud Netflix:Spring Cloud 的第一代实现,主要由 Eureka、Ribbon、Feign、Hystrix 等组件组成。
- Spring Cloud Alibaba:Spring Cloud 的第二代实现,主要由 Nacos、Sentinel、Seata 等组件组成。
由于 Spring Cloud Netflix 中的大多数组件如如 Eureka、Hystrix、Zuul 等已停止维护,所以我们建议使用 Spring Cloud Alibaba 作为微服务开发框架。
Spring Cloud Alibaba 技术选型
微服务开发框架的技术版本选型是十分必要的一环,为了尽可能地契合基底的 Spring Boot 框架以及降低不同组件版本的兼容性问题,我们可以参考官方文档的推荐完成版本适配:版本发布说明。
这里我使用最新的 2023.x 分支:对应 Spring Cloud 2023、Spring Boot 3.2.x,支持 JDK 17 或更高版本。下面分别列出完整的版本对应表以及开发中所需技术的版本选型。
框架版本
Spring Cloud Alibaba | Spring Cloud | Spring Boot |
---|---|---|
2023.0.1.0 | 2023.0.1 | 3.2.4 |
组件版本
Spring Cloud Alibaba | Nacos | Seata | Sentinel | RocketMQ |
---|---|---|---|---|
2023.0.1.0 | 2.3.2 | 2.0.0 | 1.8.6 | 5.1.4 |
其他
- JDK版本:JDK 21
- 项目构建管理:Maven 3.9.9
- 数据库:MySQL 8.0
- 集成开发环境:IntelliJ IDEA 2024.2.3
🏗 搭建微服务架构
既然是“从0开始搭建微服务架构”,那么本文就从项目创建起始一步一步完成架构的搭建。同时为了更好地了解微服务中各组件的作用,搭建的过程中会编写简单的微服务完成示例说明,对于“服务发现与通信”,会分别编写“订单微服务”和“库存微服务”来辅助讲解服务注册发现组件——Nacos、服务通信组件——OpenFeign。\
项目工程:sca
创建项目
无论是 Spring Cloud 还是 Spring Cloud Alibaba 它们都是基于 Spring Boot 的分布式系统开发框架,所以我们的架构工程直接通过创建Spring Boot
项目开始,项目名称是sca
(即 Spring Cloud Alibaba 的首字母缩写)。
接着在 Spring Boot 版本以及依赖选择页面这里可以不用特别指定,可以等下在构建依赖中手动修改。
之后创建出来的工程目录结构如下(左图),我们可以删除不必要的,保留pom.xml
即可(右图)。
构建依赖
接下来开始我们微服务框架的构建依赖调整。
打开pom.xml
,修改<packaging>
为pom
,因为这里的顶层 POM 将作为聚合模块,主要用于依赖管理。
<groupId>com.vie</groupId>
<artifactId>sca</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>pom</packaging>
<name>sca</name>
<description>sca</description>
根据前面的 Spring Cloud Alibaba 版本选型,配置以下依赖版本的属性变量,以便于版本管理。
<properties>
<java.version>21</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-boot.version>3.2.4</spring-boot.version>
<spring-cloud.version>2023.0.1</spring-cloud.version>
<spring-cloud-alibaba.version>2023.0.1.0</spring-cloud-alibaba.version>
</properties>
引入依赖,这里在聚合 POM 引入的依赖会在子模块项目中自动引入。
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
</dependencies>
依赖管理,对 Spring Boot、Spring Cloud 以及 Spring Cloud Alibaba 的依赖版本统一管理。
<dependencyManagement>
<dependencies>
<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>
<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>
</dependencies>
</dependencyManagement>
服务子模块:sca-storage
在微服务架构中,子模块可以是实现特定业务功能的组件模块,也可以是组成整个微服务架构的单个、独立的服务的模块。将服务拆分作为子模块,是微服务架构中常用的设计方式,这样就可以作为一个小型的、自治的服务,方便独立部署和扩展。
创建子模块
创建库存服务子模块sca-storage
,可以在 IDEA 的项目根目录右键,在 “New” 选项中选择 “Module...”。
构建依赖
在sca-storage/pom.xml
引入spring-boot-starter-web
依赖,这是构建 Web 应用必要的依赖。
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
API:减库存
在本篇文章中,我们会编写两个服务,订单服务和库存服务来作微服务通信的演示,其中对于库存服务最主要的是 “减库存” 这一功能。
编写sca/storage/StorageServiceApplication.java
,用于启动 Spring Boot 应用服务。
@SpringBootApplication
public class StorageServiceApplication {
public static void main(String[] args) {
SpringApplication.run(StorageServiceApplication.class, args);
}
}
在sca/storage/controller/StorageController.java
编写 “减库存” 接口,这里简单地模拟调用接口即减库存成功。
@RestController
@RequestMapping("/storage")
public class StorageController {
@RequestMapping("/reduce-stock")
public String reduceStock() {
return "减库存";
}
}
服务子模块:sca-order
创建子模块
创建订单服务子模块sca-order
。
构建依赖
在sca-order/pom.xml
同样引入spring-boot-starter-web
依赖,这是构建 Web 应用必要的依赖。
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
API:创建订单
编写sca/order/OrderServiceApplication.java
,用于启动 Spring Boot 应用服务。
同时这里先暂时定义了一个RestTemplate
,它是 Spring 框架提供的一个客户端 HTTP 访问工具类。通过RestTemplate
可以简化 HTTP 服务的通信,实现服务间的远程调用。
@SpringBootApplication
public class OrderServiceApplication {
public static void main(String[] args) {
SpringApplication.run(OrderServiceApplication.class, args);
}
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
接着在sca/order/controller/OrderController.java
编写 “下单” 接口。
在这个下单流程中,需要先完成 “减库存” 后才能完成 “创建订单”,因此这里先通过RestTemplate
向库存服务发起请求完成 “减库存”,然后才返回下单成功的信息。其中localhost:8011
是库存服务的地址。
@RestController
@RequestMapping("/order")
public class OrderController {
@Autowired
private RestTemplate restTemplate;
@RequestMapping("/create")
public String createOrder(){
String message = restTemplate.getForObject("http://localhost:8011/storage/reduce-stock", String.class);
return "下单成功," + message;
}
}
🔍 注册中心
在前面订单服务向库存服务发起调用时,我们是使用RestTemplate
,这样已经能够完成服务之间的调用了。不过很明显这里还存在着一个大问题,就是服务地址是硬编码的。当服务地址发生变化时,还需要修改代码并重新部署,而且也不利于服务的负载均衡和动态伸缩。因此这里就需要考虑一个可行的解决方案,能够帮助我们自动找到要调用的服务,在 Spring Cloud Alibaba 中就可以引入 Nacos。
Nacos 简介
Nacos 担任的是一个注册和配置中心的角色,是一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。
我们通过在微服务架构中引入注册中心组件——Nacos,来作为服务之间协调者。其最简化的过程是:
- 服务注册:所有的微服务应用在启动过程中会将自身包含服务名称、主机 IP 地址和端口号等信息发送到注册中心
- 服务发现:上游的微服务在处理请求过程中,根据服务名称到注册中心中查找对应服务的所有实例 IP 地址和端口号
- 服务调用:通过 RPC (HTTP、gRPC) 来进行服务的远程过程调用。
启动 Nacos Server
在接入 Nacos 前,需要先启动 Nacos 服务端,具体启动方式可参考 Nacos 官网。
这里我以 Docker 容器化的方式启动单例 Nacos Server,使用 Docker Compose 运行服务。这里我使用自定义数据库,所以需要先自己初始化数据库脚本。
version: "2"
services:
nacos:
image: nacos/nacos-server:v2.3.2-slim
container_name: nacos-server
env_file:
- ./env/nacos-standlone-mysql.env
volumes:
- ./standalone-logs/:/home/nacos/logs
ports:
- "8848:8848"
- "9848:9848"
其中使用的启动环境变量如下,主要修改 MySQL 数据库相关的配置。
PREFER_HOST_MODE=hostname
MODE=standalone
NACOS_AUTH_ENABLE=true
NACOS_AUTH_IDENTITY_KEY=2222
NACOS_AUTH_IDENTITY_VALUE=2xxx
NACOS_AUTH_TOKEN=SecretKey012345678901234567890123456789012345678901234567890123456789
SPRING_DATASOURCE_PLATFORM=mysql
MYSQL_SERVICE_HOST=192.168.0.15
MYSQL_SERVICE_DB_NAME=nacos_config
MYSQL_SERVICE_PORT=3306
MYSQL_SERVICE_USER=nacos
MYSQL_SERVICE_PASSWORD=nacos
MYSQL_SERVICE_DB_PARAM=characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
Nacos Server 启动成功之后,浏览器地址栏输入http://localhost:8848/nacos
即可查看 Nacos 控制台(默认账号名和密码为 nacos/nacos)。
集成 Nacos Discovery
确保 Nacos Server 已启动后,我们就可以在框架中接入 Nacos Discovery 组件。
在所有服务子模块的 POM 中添加以下依赖。
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
然后在各个服务应用的application.yaml
配置中分别指定spring.application.name
作为唯一的服务名(比如订单服务是order-service
),并配置spring.cloud.nacos.discovery
。
spring:
application:
name: order-service
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
username: nacos
password: nacos
之后我们分别启动一个order-service
和两个storage-service
,然后在 Nacos 控制台中查看服务注册情况,如下就代表服务成功注册了。
🎡 服务负载均衡
在引入 Nacos 之后,我们发起服务调用就不再需要硬编码服务地址了,直接使用服务名称替换即可。不过在正式发起调用前,还需要考虑一件事,就是storage-service
作为服务提供者存在多个实例(前面我启动了两个库存服务),如何完成服务的负载调用。对于RestTemplate
它本身是无法解析storage-service
这样的主机名解析的,即使是只存在一个服务提供者实例,在程序执行过程中也会报错。
@RestController
@RequestMapping("/order")
public class OrderController {
@Autowired
private RestTemplate restTemplate;
@RequestMapping("/create")
public String createOrder(){
//String message = restTemplate.getForObject("http://localhost:8011/storage/reduce-stock", String.class);
String message = restTemplate.getForObject("http://storage-service/storage/reduce-stock", String.class);
return "下单成功," + message;
}
}
集成 Spring Cloud Loadbalancer
为了解决服务负载调用的问题,在微服务系统中就需要引入负载均衡器,这里我们可以使用 Spring Cloud Loadbalancer。Spring Cloud LoadBalancer 是 Spring Cloud 提供的负载均衡框架,它提供了多种负载均衡算法并且支持动态服务发现。
因为 Spring Cloud Loadbalancer 是一个客户端侧的负载均衡器,因此只需要在必要的服务消费方(如订单服务)引入spring-cloud-starter-loadbalancer
即可。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
接着在订单应用的application.yaml
配置中开启spring.cloud.loadbalancer
即可。
spring:
application:
name: order-service
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
username: nacos
password: nacos
loadbalancer:
nacos:
enabled: true
服务消费
回到RestTemplate
Bean,我们需要添加@LoadBalanced
注解才可使用客户端侧的负载均衡。
@SpringBootApplication
public class OrderServiceApplication {
public static void main(String[] args) {
SpringApplication.run(OrderServiceApplication.class, args);
}
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
为了便于观察服务的负载调用情况,我们可以在库存服务的减库存接口中返回其当前运行的服务端口。
@RestController
@RequestMapping("/storage")
public class StorageController {
@Autowired
Environment environment;
@RequestMapping("/reduce-stock")
public String reduceStock() {
return environment.getProperty("local.server.port") + "减库存";
}
}
尝试多次执行curl -X GET localhost:8010/order/create
发起 “创建订单”,可以观察到服务负载起到作用了。\
📡 服务通信
前面通过RestTemplate + Loadbalancer
的方式就可以完成微服务系统的服务负载调用了,只不过调用的编码方式相比以往使用xxxService
接口的调用方式看起来不够简洁优雅、也不太符合面向接口编程的理念,最重要的是调用的服务地址依然是硬编码的。因此我们还应当探讨如何简化服务的调用方式,Spring Cloud OpenFeign 就是这样一个解决方案。
OpenFeign 简介
Spring Cloud OpenFeign 是一种基于 Spring Cloud 的声明式 REST 客户端,它简化了与 HTTP 服务交互的过程。它将 REST 客户端的定义转化为 Java 接口,并且可以通过注解的方式来声明请求参数、请求方式、请求头等信息,从而使得客户端的使用更加方便和简洁。
简单来说,就是通过 OpenFeign 可以像调用本地服务接口(xxxService
)一样调用远程的服务,而开发者无需关心请求构建、响应处理等细节的编写。
集成 Spring Cloud OpenFeign
Spring Cloud OpenFeign 本身也提供了负载均衡和服务发现等功能,其底层也依赖于 Spring Cloud Loadbalancer,因此引入 OpenFeign 后可以不再需要单独引入spring-cloud-starter-loadbalancer
。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
启动类添加注解@EnableFeignClients
。应用启动时会自动扫描@FeignClient
的客户端接口类,必要时请指定客户端的位置,例如:@EnableFeignClients(basePackages = "com.example.clients")
。
@EnableFeignClients
@SpringBootApplication
public class OrderServiceApplication {
public static void main(String[] args) {
SpringApplication.run(OrderServiceApplication.class, args);
}
// @Bean
// @LoadBalanced
// public RestTemplate restTemplate() {
// return new RestTemplate();
// }
}
声明式客户端
接着来讲解如何使用 OpenFeign 定义声明式客户端。
在我们的示例中,订单服务向库存服务发起调用,订单服务就称作客户端,相应的库存服务就是服务端。因此,需要在客户端侧的订单服务中定义声明式客户端。
编写sca/order/feign/StorageServiceFeignClient.java
,客户端接口类添加注解@FeignClient
。其中的storage-service
为定义的客户端名称,它会被用于创建 Spring Cloud LoadBalancer client,之后负载均衡器客户端会指向storage-service
服务的物理地址;path
用于集中管理路径前缀。
@FeignClient(name = "storage-service", path = "storage")
public interface StorageServiceFeignClient {
@RequestMapping("/reduce-stock")
Result<?> reduceStock();
}
分别编写sca/order/service/OrderService.java
、sca/order/service/impl/OrderServiceImpl.java
,这里就像往常的 Spring Boot 项目服务层接口定义一样。
public interface OrderService {
Result<?> createOrder();
}
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
private StorageServiceFeignClient storageService;
@Override
public Result<?> createOrder() {
return storageService.reduceStock();
}
}
服务调用
这里为了让整个服务调用的 API 更符合 Web 应用的设计风格,简单编写了一个Result
响应类。
public class Result<T> {
private Integer code;
private String message;
private T data;
//.....
}
完成 Feign 客户端编写后,就可以直接通过xxxService
方式发起服务调用了。
@RestController
@RequestMapping("/order")
public class OrderController {
// @Autowired
// private RestTemplate restTemplate;
@Autowired
private OrderService orderService;
@RequestMapping("/create")
public Result<?> createOrder(){
// String message = restTemplate.getForObject("http://localhost:8011/storage/reduce-stock", String.class);
// Result<?> result = restTemplate.getForObject("http://storage-service/storage/reduce-stock", Result.class);
Result<?> result = orderService.createOrder();
return Result.success("下单成功", result.getData());
}
}
多次执行curl -X GET localhost:8010/order/create
发起 “创建订单”,可以观察到服务调用成功了,并且也自动实现了服务的负载均衡。\
🎉 小结
通过前面一步步地搭建微服务框架,逐步完成了微服务架构中的服务注册与发现、服务负载均衡以及服务通信的课题。至此,一个简单的微服务治理架构已经初步形成。之后,我们还可以进一步完善这个微服务治理体系,通过引入更多关键组件和技术来提升系统的稳定性、可维护性和安全性,如使用 Nacos 作为配置中心、引入 Seata 解决分布式事务、应用 Sentinel 承担限流降级等等。
那么本篇文章就到这里,祝各位小伙伴有所收获🌟!