入门微服务架构设计:由下单场景实现服务注册、发现以及调用

举报
菜菜的后端私房菜 发表于 2024/11/08 17:27:14 2024/11/08
【摘要】 入门微服务架构设计:由下单场景实现服务注册、发现以及调用本文旨在介绍如何使用 Java 和 Spring Boot 框架设计一个微服务架构,实现服务的注册、发现和调用,并通过Spring Cloud (Alibaba)进行服务治理我们将通过老生常谈的下单业务场景示例,详细阐述微服务架构的设计思路和技术实现过程通过本文可以了解微服务架构解决单体架构痛点带来的优势,以及其存在的劣势与常用解决方...

入门微服务架构设计:由下单场景实现服务注册、发现以及调用

本文旨在介绍如何使用 Java 和 Spring Boot 框架设计一个微服务架构,实现服务的注册、发现和调用,并通过Spring Cloud (Alibaba)进行服务治理

我们将通过老生常谈的下单业务场景示例,详细阐述微服务架构的设计思路和技术实现过程

通过本文可以了解微服务架构解决单体架构痛点带来的优势,以及其存在的劣势与常用解决方案

背景

传统的单体架构虽然在早期能够满足大部分业务需求,但随着系统业务需求的变更、日益增长的用户量,其局限性逐渐显现

单体架构将所有功能模块集成在一个单一的应用程序中,这导致代码臃肿、部署周期长、可维护性差以及难以扩展等问题

特别是数据量、并发逐步提升的挑战时,单体架构的瓶颈愈发明显,为了解决单体架构带来的种种问题,微服务架构应运而生

微服务架构将大型应用拆分为一组小型、独立的服务,每个服务都围绕着特定的业务功能进行构建,并且可以独立地部署、扩展和维护,能够有效解决单体架构的难点与痛点,但同时也带来以下的问题:

  1. 这么多服务的配置能不能集中起来进行配置,分别维护?(配置中心:集中化管理、环境隔离、动态更新
  2. 这么多服务如何让它们彼此能够发现?(注册中心:负责服务注册与发现,心跳监听服务的状态
  3. 服务要进行网络通信调用其他服务,多节点的情况下又如何负载?(远程调用+负载均衡:使用自定义协议或HTTP协议网络通信调用服务;服务中多节点的情况下,将请求平衡分发到不同节点
  4. 服务调用链路太长,某个服务故障超时会导致整个链路阻塞从而影响其他服务,该如何解决?(熔断:故障服务节点立马进行响应,避免影响其他服务节点
  5. 这么多服务如何进行管理?(网关:鉴权、过滤、限流、负载、熔断降级…
  6. 数据一致性问题…

技术选型

虽然微服务架构下存在许多问题,但是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中进行配置

运行项目

在本地搭建Nacos并运行

运行后,访问http://127.0.0.1:8848/nacos,账号密码通常都为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

nacos查看服务列表

测试请求127.0.0.1:8000/order/pay/100访问网关端口8000,请求以order开头,会被负载到订单服务,最终访问支付接口,接口中会调用库存扣减接口,初始化库存为100,扣减1后剩余99返回

测试请求

至此我们通过简易的扣减库存流程,实现微服务架构中服务注册与发现以及调用

总结

本文使用 Java 和 Spring Boot 框架设计一个微服务架构,以扣减库存的流程来实现服务的注册、发现和调用,并通过Spring Cloud (Alibaba)进行服务治理

虽然我们已经简化很多流程,但不难看出微服务架构下的开发要比单体架构下麻烦的多

微服务架构虽然能够解决单体架构所带来的问题,但同时也会引入一系列的问题

因此选择不同的架构时需要结合项目、场景等多方面因素进行选择

最后(点赞、收藏、关注求求啦~)

本篇文章被收入专栏 深入浅出微服务,感兴趣的同学可以持续关注喔

本篇文章笔记以及案例被收入 Gitee-CaiCaiJavaGithub-CaiCaiJava,除此之外还有更多Java进阶相关知识,感兴趣的同学可以starred持续关注喔~

有什么问题可以在评论区交流,如果觉得菜菜写的不错,可以点赞、关注、收藏支持一下~

关注菜菜,分享更多技术干货,公众号:菜菜的后端私房菜

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。