入门微服务架构设计:由下单场景实现服务注册、发现以及调用
入门微服务架构设计:由下单场景实现服务注册、发现以及调用
本文旨在介绍如何使用 Java 和 Spring Boot 框架设计一个微服务架构,实现服务的注册、发现和调用,并通过Spring Cloud (Alibaba)进行服务治理
我们将通过老生常谈的下单业务场景示例,详细阐述微服务架构的设计思路和技术实现过程
通过本文可以了解微服务架构解决单体架构痛点带来的优势,以及其存在的劣势与常用解决方案
背景
传统的单体架构虽然在早期能够满足大部分业务需求,但随着系统业务需求的变更、日益增长的用户量,其局限性逐渐显现
单体架构将所有功能模块集成在一个单一的应用程序中,这导致代码臃肿、部署周期长、可维护性差以及难以扩展等问题
特别是数据量、并发逐步提升的挑战时,单体架构的瓶颈愈发明显,为了解决单体架构带来的种种问题,微服务架构应运而生
微服务架构将大型应用拆分为一组小型、独立的服务,每个服务都围绕着特定的业务功能进行构建,并且可以独立地部署、扩展和维护,能够有效解决单体架构的难点与痛点,但同时也带来以下的问题:
- 这么多服务的配置能不能集中起来进行配置,分别维护?(配置中心:集中化管理、环境隔离、动态更新)
- 这么多服务如何让它们彼此能够发现?(注册中心:负责服务注册与发现,心跳监听服务的状态)
- 服务要进行网络通信调用其他服务,多节点的情况下又如何负载?(远程调用+负载均衡:使用自定义协议或HTTP协议网络通信调用服务;服务中多节点的情况下,将请求平衡分发到不同节点)
- 服务调用链路太长,某个服务故障超时会导致整个链路阻塞从而影响其他服务,该如何解决?(熔断:故障服务节点立马进行响应,避免影响其他服务节点)
- 这么多服务如何进行管理?(网关:鉴权、过滤、限流、负载、熔断降级…)
- 数据一致性问题…
技术选型
虽然微服务架构下存在许多问题,但是Spring Cloud(Alibaba)框架通过一系列的组件来解决这些问题
本篇文章主要采用以下组件来实现服务的注册、发现以及调用:
Nacos 服务的注册中心与配置中心
OpenFeign 服务间的远程调用(HTTP),并使用Ribbon进行负载均衡
Gateway 网关作为请求入口,统一鉴权、负载、限流
Sentinel 流控、Seata 分布式事务等其他组件后续在本专栏再进行说明
在下单的业务场景中,通常包含用户、商品、订单、库存、支付、通知等服务
为了简化服务调用的流程,只演示其中的订单、库存服务,下单业务流程简化为:更新订单状态并扣减库存
整体架构图如下所示:
目前,我们只需重点关注中间的微服务,网关,注册/配置中心
搭建项目
设计完架构后,我们需要使用Spring Boot、Spring Cloud框架搭建环境
项目整体目录结构如下:
Cloud
├─cloud-api #存储远程调用的API接口
├─cloud-gateway #网关
├─cloud-order #订单服务
└─cloud-stock #库存服务
父工程
我们使用Maven搭建父子工程项目,其中父工程负责依赖版本的管理pom.xml如下:
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.caicaijava</groupId>
<artifactId>cloud</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>Cloud</name>
<description>Cloud Parent</description>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<spring-boot.version>2.7.6</spring-boot.version>
<spring-cloud.version>2021.0.5</spring-cloud.version>
<spring-cloud-alibaba.version>2021.0.4.0</spring-cloud-alibaba.version>
<log4j.version>1.2.17</log4j.version>
<spring-cloud-bootstrap>3.0.3</spring-cloud-bootstrap>
<logback-core>1.2.3</logback-core>
</properties>
<packaging>pom</packaging>
<modules>
<module>cloud-gateway</module>
<module>cloud-order</module>
<module>cloud-stock</module>
</modules>
<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>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
<version>${spring-cloud-bootstrap}</version>
</dependency>
<!--日志-->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>${log4j.version}</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
<version>${logback-core}</version>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<encoding>${project.build.sourceEncoding}</encoding>
</configuration>
</plugin>
</plugins>
</build>
</project>
库存服务——被调用方
库存服务作为被调用方,需要注册到nacos,因此会引入nacos注册中心的依赖(顺便加了配置中心的依赖)
除此之外还需要开启web容器,对外提供库存扣减的接口,因此需要引入web依赖 spring-boot-starter-web
在父工程中创建一个Spring Boot项目,maven配置文件如下:
pom.xml
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.caicaijava</groupId>
<artifactId>cloud</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>cloud-stock</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>cloud-stock</name>
<properties>
<java.version>8</java.version>
</properties>
<dependencies>
<!-- nacos 注册中心 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- nacos 配置中心 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--日志-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
</dependency>
</dependencies>
</project>
配置文件 bootstrap.yml
spring:
cloud:
nacos:
#注册中心
discovery:
server-addr: 127.0.0.1:8848
#配置中心
config:
server-addr: 127.0.0.1:8848
file-extension: yml
application:
#服务名
name: cloud-stock
profiles:
#环境
active: dev
server:
#端口
port: 8002
提供调用的库存扣减API接口
为了简化流程,这里库存未使用数据库,而是在内存中进行模拟:初次访问时根据商品ID的值初始化库存数量
@RestController
@RequestMapping("/stockApi")
public class StockApiController {
@GetMapping("/ded/{id}")
public Long ded(@PathVariable("id") Long id) {
return dedStockBySkuId(id, 1);
}
private final Map<Long /*id*/, AtomicLong /*stock*/> skuStock = new ConcurrentHashMap<>(16);
/**
* @param id 商品ID
* @param dedNum 扣减数量
* @return
*/
private Long dedStockBySkuId(Long id, long dedNum) {
//如果不存在就创建 库存数量为id值
AtomicLong stock = skuStock.computeIfAbsent(id, AtomicLong::new);
long stockNum;
//防并发
do {
stockNum = stock.get();
if (stockNum <= 0) return -1L;
} while (!stock.compareAndSet(stockNum, stockNum - dedNum));
return stockNum - 1;
}
}
由于读取库存数量与扣减库存数量并不是一个原子性的操作,因此需要使用同步手段防止并发,这里采用乐观锁 + 失败重试(自旋锁)
API接口——公共依赖
对于服务间的远程调用,通常会把提供服务的API接口放在公共服务下进行依赖
maven配置文件的依赖只需要openfeign与负载均衡的依赖
pom.xml
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.caicaijava</groupId>
<artifactId>cloud</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>cloud-api</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>cloud-api</name>
<properties>
<java.version>8</java.version>
</properties>
<dependencies>
<!--服务调用-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
</dependencies>
</project>
定义调用接口
对于远程调用需要进行网络通信,并且过程存在负载均衡、协议解析、请求数据包的转换,流程过于繁杂
feign为我们简化远程调用的复杂流程,让远程调用变得像本地接口调用一样简单
只需要定义调用的接口,并使用@FeignClient注解标识要调用的服务名
@FeignClient(name = "cloud-stock")
public interface StockApiService {
@GetMapping("/stockApi/ded/{id}")
Long ded(@PathVariable("id") Long id);
}
订单服务——调用方
订单服务的maven配置文件与库存服务类似,但需要依赖API接口
pom.xml
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.caicaijava</groupId>
<artifactId>cloud</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>cloud-order</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>cloud-order</name>
<properties>
<java.version>8</java.version>
</properties>
<dependencies>
<!-- api 接口 -->
<dependency>
<groupId>com.caicaijava</groupId>
<artifactId>cloud-api</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<!-- nacos 注册中心 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- nacos 配置中心 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--日志-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
</dependency>
</dependencies>
</project>
配置文件 bootstrap.yml
spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
config:
server-addr: 127.0.0.1:8848
file-extension: yml
application:
name: cloud-order
profiles:
active: dev
server:
port: 8001
注解调整
同时订单服务注解需要调整,默认情况下@SpringBootApplication
会扫描当前类所在目录以及目录下的Bean
但是公共API接口并不在该目录中,因此要使用@ComponentScan
显示标出要扫描的api目录com.caicaijava.api
以及当前项目目录com.caicaijava.cloudorder
同时使用@EnableFeignClients
开启去api包下扫描Feign客户端
@SpringBootApplication
@ComponentScan(basePackages = {"com.caicaijava.api","com.caicaijava.cloudorder"})
@EnableFeignClients(basePackages = "com.caicaijava.api")
public class CloudOrderApplication {
public static void main(String[] args) {
SpringApplication.run(CloudOrderApplication.class, args);
}
}
提供支付订单接口
为了方便描述、简化流程,编写支付订单pay接口中只有修改订单状态以及库存扣减,并且也没有根据订单查询出要扣减的对应商品ID,而是直接根据订单ID进行扣减库存(见谅~)
同时也未考虑分布式事务的情况,这些问题专栏后文再一一进行描述、解决
@RestController
@RequestMapping("/order")
public class OrderController {
@Autowired
private StockApiService stockApiService;
private final Logger logger = LoggerFactory.getLogger(OrderController.class);
@GetMapping("/pay/{id}")
public String pay(@PathVariable("id") Long id) {
//订单状态更改..
updateOrderStatus(id);
//扣减库存..
Long stockNum = stockApiService.ded(id);
if (stockNum < 0L) {
return "支付失败,库存不足";
}
return "支付成功,剩余库存:" + stockNum;
}
private void updateOrderStatus(Long id) {
logger.info("{}订单状态更改..", id);
}
}
网关——公共入口
网关也需要注册到nacos中,maven配置如下:
pom.xml
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.caicaijava</groupId>
<artifactId>cloud</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>cloud-gateway</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>cloud-gateway</name>
<description>cloud-gateway</description>
<properties>
<java.version>8</java.version>
</properties>
<dependencies>
<!-- nacos 注册中心 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- nacos 配置中心 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
<!-- gateway -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<!--日志-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
</dependency>
</dependencies>
</project>
配置 bootstrap.yml
spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
config:
server-addr: 127.0.0.1:8848
file-extension: yml
application:
name: cloud-gateway
profiles:
active: dev
server:
port: 8000
网关还需要配置请求的负载均衡,为了演示nacos配置中心,将这部分配置待会在nacos中进行配置
运行项目
运行后,访问http://127.0.0.1:8848/nacos
,账号密码通常都为nacos
在配置列表中新建配置cloud-gateway-dev.yml
配置文件名是不可以乱填的,通常是 服务名-环境.后缀名
服务名为cloud-gateway,环境为dev,后缀名配置file-extension
为yml,因此是cloud-gateway-dev.yml
,配置如下:
spring:
cloud:
gateway:
routes:
- id: cloud-order
uri: lb://cloud-order
predicates:
- Path=/order/**
server:
port: 8000
将网关端口改为8000,后续/order相关的请求都会被负载给cloud-order服务
将网关、库存、订单服务都启动后,在nacos服务列表查看说明都注册到nacos
测试请求127.0.0.1:8000/order/pay/100
访问网关端口8000,请求以order
开头,会被负载到订单服务,最终访问支付接口,接口中会调用库存扣减接口,初始化库存为100,扣减1后剩余99返回
至此我们通过简易的扣减库存流程,实现微服务架构中服务注册与发现以及调用
总结
本文使用 Java 和 Spring Boot 框架设计一个微服务架构,以扣减库存的流程来实现服务的注册、发现和调用,并通过Spring Cloud (Alibaba)进行服务治理
虽然我们已经简化很多流程,但不难看出微服务架构下的开发要比单体架构下麻烦的多
微服务架构虽然能够解决单体架构所带来的问题,但同时也会引入一系列的问题
因此选择不同的架构时需要结合项目、场景等多方面因素进行选择
最后(点赞、收藏、关注求求啦~)
本篇文章被收入专栏 深入浅出微服务,感兴趣的同学可以持续关注喔
本篇文章笔记以及案例被收入 Gitee-CaiCaiJava、 Github-CaiCaiJava,除此之外还有更多Java进阶相关知识,感兴趣的同学可以starred持续关注喔~
有什么问题可以在评论区交流,如果觉得菜菜写的不错,可以点赞、关注、收藏支持一下~
关注菜菜,分享更多技术干货,公众号:菜菜的后端私房菜
- 点赞
- 收藏
- 关注作者
评论(0)