微服务实战系列之SpringCloud Alibaba学习(一)

通过实战案例来学习如何使用SpringCloud Alibaba开发实际项目。

微服务实战系列之SpringCloud Alibaba:

1. 案例驱动

在案例上,选择了比较熟悉的电商项目中的用户、商品和订单模块为例,因为这些模块的核心逻辑比较简单,并且真实的电商项目一般都采用分布式或微服务的架构模式,并且都会承载高并发大流量的场景。同时,涉及到分布式或微服务的场景时,一般又会产生分布式事务的问题。

所以以电商系统中的用户、商品和订单模块为例,学习SpringCloud Alibaba。在开发电商系统的过程中,每个阶段会产生不同的需求,而我们在每个阶段针对不同的需求,会使用SpringCloud Alibaba中的对应组件来实现。

例如:

  • 涉及到服务治理服务配置的需求时,我们会使用Nacos实现。
  • 涉及到负载均衡的需求时,我们会使用Ribbon实现。
  • 涉及到远程服务调用的需求时,我们会使用Fegin实现。
  • 涉及到服务容错的需求时,我们会使用Sentinel实现。
  • 涉及到服务网关的需求时,由于目前SpringCloud Alibaba中并未提供自己的网关,所以,我们使用SpringCloud中的Gateway实现。
  • 涉及到链路追踪的需求时,我们会使用Sleuth+ZipKin实现。
  • 涉及到消息服务的需求时,我们会使用RocketMQ实现。
  • 涉及到分布式事务的需求时,我们会使用Seata实现。
  • 涉及到数据存储的需求时,我们会MySQL+ElasticSearch实现。

2. 微服务介绍

2.1 微服务化后的问题:

一个系统,尤其是大型系统使用微服务架构模式进行搭建和开发时,虽然总体上能够提高研发效率、能够支持更高的并发,也能够提高系统整体的性能和可靠性,以及可维护性。但是在实现细节上还是存在着不少的问题。

  • (1)将系统拆分成各个微服务后,如果管理和感知那么多的服务呢?
  • (2)将系统拆分成各个微服务后,各个微服务之间如何通信?
  • (3)将系统拆分成各个微服务后,一旦系统出现问题,如何快速定位问题呢?
  • (4)将系统拆分成各个微服务后,如何最大程度的保证系统的可用性?
  • (5)将系统拆分成各个微服务后,客户端如何访问那么多的微服务?
2.2 微服务常见概念

主要涉及的技术:

在这里插入图片描述
注:图片来自于冰河技术:SA实战 ·《SpringCloud Alibaba实战》

服务调用:将一个系统拆分成各个微服务后,各个微服务之间协同工作才能对外提供完整的服务,这就涉及到各个微服务之间的调用问题。目前各个微服务之间一般会采用Restful接口或者RPC协议的方式进行调用。

  • (1)Restful接口

    Restful接口一般是基于HTTP协议实现的,这种协议使用上比较广泛,几乎所有的编程语言都支持HTTP协议。

  • (2)RPC协议

    RPC是一种远程过程调用,能够做到像调用本地服务一样调用远程服务。RPC框架在底层屏蔽了数据的传输方式,序列化方式和交互的细节信息,让使用RPC框架开发微服务的人员觉得更加简单,实现起来更加容易。
    服务治理

服务治理:说白了就是如何自动化的管理各个微服务,核心的功能就是服务的注册、发现和剔除

  • (1)服务注册

    各个微服务实例在启动时,能够将自身提供的服务注册到某个注册中心。

  • (2)服务发现

    当某个微服务将自身提供的服务注册到注册中心时,其他微服务实例能够通过注册中心感知到这个微服务提供的服务,并且能够获取到这个微服务的实例信息,通过这个微服务的实例信息就能够调用这个微服务的方法,来进行相应的读写操作。

  • (3)服务剔除

    如果某个微服务实例出现故障,或者连接一直超时,则注册中心会认为当前微服务实例不可用,就会将这个微服务实例剔除出注册中心,使其不再被其他微服务感知到和调用到。

注册中心:提供微服务注册、发现和剔除功能的服务组件。

服务网关:所有微服务的入口,客户端在访问各个微服务时,首先需要经过服务网关。接入服务网关后,会将所有API的调用统一接入到API的网关层,由网关层统一接收参数进行路由转发,将返回的结果数据返回给客户端。

通常情况下,一个服务网关最基本的功能包括:统一接入、限流、熔断、降级、安全防护、协议适配、容错等等。主要专注的是对系统安全、流量和路由等的管理。这样,业务开发人员就可以专注于开发业务逻辑啦。

服务限流:在高并发大流量场景下,经常会出现某个服务或者接口因为调用的流量过大而导致不可用的情况,由于某个服务或者接口的不可用,可能还会导致整个系统崩溃。此时,就会对系统采取限流的手段来进行防护,当请求达到一定的频率或者速率时,对这些请求采取排队、等待、降级等策略,甚至是拒绝服务。

服务熔断:如果某个服务出现故障不可用,或者调用超时,为了不让其他服务受到牵累而导致整个系统不可用,则断开与这个服务的连接,暂停对这个服务的调用。

服务降级:主要是从整个系统的负载情况进行考虑,如果某些服务的负载情况比较高,则为了预防某些功能出现负载过高而导致响应慢的问题,会在提供这些功能的方法内部暂时舍弃对一些非核心功能接口的调用,直接返回一个提前准备好的错误处理信息。服务降级是有损服务,但是能够保证整个系统的稳定性和可用性。

服务容错:指的是微服务能够容纳一定错误情况的发生。从某种意义上说,服务限流、服务熔断和服务降级都是服务容错的措施

链路追踪:当系统被拆分成各个微服务后,一次请求往往会涉及到多个服务之间的调用关系。如果系统出现问题,则会增加定位问题的难度。为了解决这个问题,就需要对一次请求涉及到的多个服务链路的日志进行追踪和记录,一方面可以记录调用的链路,另一方面还可以监控系统中各个调用环节的性能,这就是链路追踪。

2.3 SpringCloud Alibaba

SpringCloud Alibaba是阿里开源的一套微服务解决方案,包含各种微服务组件,能够极大的方便开发人员构建微服务应用。

主要功能:

  • 服务限流降级:默认支持 WebServlet、WebFlux、OpenFeign、RestTemplate、Spring Cloud Gateway、Zuul、Dubbo 和 RocketMQ 限流降级功能的接入,可以在运行时通过控制台实时修改限流降级规则,还支持查看限流降级 Metrics 监控。
  • 服务注册与发现:适配 Spring Cloud 服务注册与发现标准,默认集成了 Ribbon 的支持。
  • 分布式配置管理:支持分布式系统中的外部化配置,配置更改时自动刷新。
  • 消息驱动能力:基于 Spring Cloud Stream 为微服务应用构建消息驱动能力。
  • 分布式事务:使用 @GlobalTransactional 注解, 高效并且对业务零侵入地解决分布式事务问题。
  • 阿里云对象存储:阿里云提供的海量、安全、低成本、高可靠的云存储服务。支持在任何应用、任何时间、任何地点存储和访问任意类型的数据。
  • 分布式任务调度:提供秒级、精准、高可靠、高可用的定时(基于 Cron 表达式)任务调度服务。同时提供分布式的任务执行模型,如网格任务。网格任务支持海量子任务均匀分配到所有 Worker(schedulerx-client)上执行。
  • 阿里云短信服务:覆盖全球的短信服务,友好、高效、智能的互联化通讯能力,帮助企业迅速搭建客户触达通道。

除了上述所具有的功能外,针对企业级用户的场景,Spring Cloud Alibaba 配套的企业版微服务治理方案: 微服务引擎 MSE 还提供了企业级微服务治理中心,包括全链路灰度、服务预热、无损上下线和离群实例摘除等更多更强大的治理能力,同时还提供了企业级 Nacos 注册配置中心,企业级云原生网关等多种产品及解决方案。

组件:

  • Sentinel:把流量作为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。
  • Nacos:一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。
  • RocketMQ:一款开源的分布式消息系统,基于高可用分布式集群技术,提供低延时的、高可靠的消息发布与订阅服务。
  • Dubbo:Apache Dubbo™ 是一款高性能 Java RPC 框架。
  • Seata:阿里巴巴开源产品,一个易于使用的高性能微服务分布式事务解决方案。
  • Alibaba Cloud OSS: 阿里云对象存储服务(Object Storage Service,简称 OSS),是阿里云提供的海量、安全、低成本、高可靠的云存储服务。您可以在任何应用、任何时间、任何地点存储和访问任意类型的数据。
  • Alibaba Cloud SchedulerX: 阿里中间件团队开发的一款分布式任务调度产品,提供秒级、精准、高可靠、高可用的定时(基于Cron 表达式)任务调度服务。
  • Alibaba Cloud SMS: 覆盖全球的短信服务,友好、高效、智能的互联化通讯能力,帮助企业迅速搭建客户触达通道。

3. 项目说明

在案例上,选择了比较熟悉的电商项目中的用户、商品和订单模块为例。一方面是这些模块的业务逻辑比较简单,另一方面,案例最终会以微服务的形式呈现。

3.1 项目流程设计

整个项目主要分为用户微服务、商品微服务和订单微服务,整个过程模拟的是用户下单扣减库存的操作。这里,为了简化整个流程,将商品的库存信息保存到了商品数据表,同时,使用商品微服务来扣减库存。

在实现时,也可以将商品库存信息单独开发一个微服务模块,主体逻辑和将库存信息放在商品微服务进行管理是一样的。各服务之间的调用流程如下。
在这里插入图片描述注:图片引用同上。

用户微服务、商品微服务和订单微服务的整体流程为:

用户通过客户端调用订单微服务的提交订单的接口后,订单微服务会分别调用用户微服务和商品微服务的接口来查询用户信息和商品信息,并校验商品库存是否充足,如果商品库存充足的话,就会保存订单。并且会调用商品微服务的扣减库存的接口来扣减库存。

3.2 技术选型

整个项目采用SpringCloud Alibaba技术栈实现,主要的技术选型如下所示。

  • 持久层框架:MyBatis、MyBatis-Plus
  • 微服务框架:SpringCloud Alibaba
  • 消息中间件:RocketMQ
  • 服务治理与服务配置:Nacos
  • 负载均衡组件:Ribbon
  • 远程服务调用:Fegin
  • 服务限流与容错:Sentinel
  • 服务网关:SpringCloud-Gateway
  • 服务链路追踪:Sleuth+ZipKin
  • 分布式事务:Seata
  • 数据存储:MySQL+ElasticSearch
3.3 模块划分

为了方便开发和维护,同时为了模块的复用性,整体项目在搭建时,会将用户微服务、商品微服务和订单微服务放在同一个Maven父工程下,作为父工程的子模块,同时,将用户微服务、商品微服务和订单微服务都会使用的JavaBean单独作为一个Maven模块,以及各服务都会使用的工具类单独作为一个Maven模块。

在这里插入图片描述
注:图片引用同上。

其中各模块的说明如下所示:

  • shop-springcloud-alibaba:Maven父工程。
  • shop-bean:各服务都会使用的JavaBean模块,包含实体类、Dto、Vo等JavaBean。
  • shop-utils:各服务都会使用的工具类模块。
  • shop-order:订单微服务。
  • shop-product:商品微服务。
  • shop-user:用户微服务。

4. 通用模块开发

4.1 项目总体结构

项目总体上包含一个Maven父工程,实体类模块、工具类模块、用户微服务、商品微服务和订单微服务都以Maven子模块的形式存在,项目总体结构如下所示。

其中各模块的说明如下所示:

  • shop-springcloud-alibaba:Maven父工程。
  • shop-bean:各服务都会使用的JavaBean模块,包含实体类、Dto、Vo等JavaBean。
  • shop-utils:各服务都会使用的工具类模块。
  • shop-order:订单微服务,监听的端口为8080。
  • shop-product:商品微服务,监听的端口为8070。
  • shop-user:用户微服务,监听的端口为8060。
4.2 创建Maven父工程

在IDEA中创建Maven工程,名称为shop-springcloud-alibaba,创建后在项目的pom.xml文件中添加StringBoot与SpringCloud alibaba相关的配置,如下所示。

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.2.6.RELEASE</version>
</parent>

<properties>
    <java.version>1.8</java.version>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <spring-cloud.version>Greenwich.RELEASE</spring-cloud.version>
    <spring-cloud-alibaba.version>2.1.0.RELEASE</spring-cloud-alibaba.version>
    <logback.version>1.1.7</logback.version>
    <slf4j.version>1.7.21</slf4j.version>
    <common.logging>1.2</common.logging>
    <fastjson.version>1.2.51</fastjson.version>
    <mybatis.version>3.4.6</mybatis.version>
    <mybatis.plus.version>3.4.1</mybatis.plus.version>
    <mysql.jdbc.version>8.0.19</mysql.jdbc.version>
    <druid.version>1.1.10</druid.version>
</properties>

<dependencyManagement>
    <dependencies>
        <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>
4.3 创建工具类模块

在父工程下创建工具类模块shop-utils,作为整个项目的通用工具类模块。工具类模块的总体结构如下所示。
在这里插入图片描述
注:图片引用同上。

4.3.1 添加项目依赖

在shop-utils模块的pom.xml文件中添加项目依赖的一些类库,如下所示。

<dependencies>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.4.1</version>
    </dependency>

    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>${mysql.jdbc.version}</version>
    </dependency>

    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>${druid.version}</version>
    </dependency>

    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid-spring-boot-starter</artifactId>
        <version>${druid.version}</version>
    </dependency>

    <dependency>
        <groupId>commons-logging</groupId>
        <artifactId>commons-logging</artifactId>
        <version>${common.logging}</version>
    </dependency>

    <!-- log -->
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-api</artifactId>
        <version>${slf4j.version}</version>
    </dependency>

    <dependency>
        <groupId>ch.qos.logback</groupId>
        <artifactId>logback-classic</artifactId>
        <version>${logback.version}</version>
    </dependency>

    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
        <version>${fastjson.version}</version>
    </dependency>

    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.8</version>
    </dependency>

</dependencies>
4.3.2 核心类开发

1. 创建HTTP状态码封装类

在项目的io.binghe.shop.utils.constants包下创建HttpCode类,作为HTTP状态码的常量类。这里,暂时定义了两个状态码,200表示处理成功,500表示服务器异常,源码如下所示:

/**
 * @description http状态码
 */
public class HttpCode {

    /**
     * 成功的状态码
     */
    public static final int SUCCESS = 200;
    /**
     * 错误状态码
     */
    public static final int FAILURE = 500;
}

2. 创建全局异常捕获类

在项目的io.binghe.shop.utils.exception包下新建全局异常捕获类RestCtrlExceptionHandler,统一捕获整个项目抛出的Exception异常,源码如下所示。

/**
 * @description 全局异常处理器
 */
@RestControllerAdvice
public class RestCtrlExceptionHandler {

private final Logger logger = LoggerFactory.getLogger(RestCtrlExceptionHandler.class);
 
 /**
  * 全局异常处理,统一返回状态码
  */
@ExceptionHandler(Exception.class)
public Result<String> handleException(Exception e) {
	 logger.error("服务器抛出了异常:{}", e);
	 return new Result<String>(HttpCode.FAILURE, "执行失败", e.getMessage());
	}
}

3. 创建通用MD5与密码加密类

在io.binghe.shop.utils.md5包下新建MD5Hash类,提供通用的MD5加密算法,

在io.binghe.shop.utils.psswd包下新建PasswordUtils类,提供密码的加密功能。

这两个类的实现比较简单,这里就不再赘述了。

4. 创建通用数据响应类

在项目的io.binghe.shop.utils.resp包下新建Result类,用于封装统一的数据返回格式,源码如下所示。

/**
 * @description 返回的结果数据
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result<T> implements Serializable {

    private static final long serialVersionUID = 1497405107265595284L;
    /**
     * 状态码
     */
    private Integer code;

    /**
     * 状态描述
     */
    private String codeMsg;

    /**
     *  返回的数据
     */
    private T data;

}

这里,需要注意的是:在Result类中使用了泛型,返回的具体业务数据类型会根据泛型的具体类型确定。Result类中的每个字段的含义如下所示。

  • code:返回的状态码。
  • codeMsg:返回的状态描述信息。
  • data:具体的业务数据,数据类型根据泛型确定。

5. 创建分布式id核心类

(1)在项目的io.binghe.shop.utils.id包下创建实现整个分布式id最核心的类SnowFlake,SnowFlake类主要是使用Java实现了雪花算法,具体的逻辑见如下源码。

/**
 * @description 雪花算法生成分布式序列号
 */
public class SnowFlake {
    /**
     * 起始的时间戳:2022-04-12 11:56:45,使用时此值不可修改
     */
    private final static long START_STAMP = 1649735805910L;

    /**
     * 每一部分占用的位数
     */
    private final static long SEQUENCE_BIT   = 12; //序列号占用的位数
    private final static long MACHINE_BIT    = 5;   //机器标识占用的位数
    private final static long DATACENTER_BIT = 5;//数据中心占用的位数

    /**
     * 每一部分的最大值
     */
    private final static long MAX_DATACENTER_NUM = -1L ^ (-1L << DATACENTER_BIT);
    private final static long MAX_MACHINE_NUM    = -1L ^ (-1L << MACHINE_BIT);
    private final static long MAX_SEQUENCE       = -1L ^ (-1L << SEQUENCE_BIT);
    
    /**
     * 每一部分向左的位移
     */
    private final static long MACHINE_LEFT    = SEQUENCE_BIT;
    private final static long DATACENTER_LEFT = SEQUENCE_BIT + MACHINE_BIT;
    private final static long TIMESTMP_LEFT   = DATACENTER_LEFT + DATACENTER_BIT;

    private long datacenterId;  //数据中心
    private long machineId;     //机器标识
    private long sequence = 0L; //序列号
    private long lastStmp = -1L;//上一次时间戳

    public SnowFlake(long datacenterId, long machineId) {
        if (datacenterId > MAX_DATACENTER_NUM || datacenterId < 0) {
            throw new IllegalArgumentException("datacenterId can't be greater than MAX_DATACENTER_NUM or less than 0");
        }
        if (machineId > MAX_MACHINE_NUM || machineId < 0) {
            throw new IllegalArgumentException("machineId can't be greater than MAX_MACHINE_NUM or less than 0");
        }
        this.datacenterId = datacenterId;
        this.machineId = machineId;
    }

    /**
     * 产生下一个ID
     */
    public synchronized long nextId() {
        long currStmp = getNewstmp();
        if (currStmp < lastStmp) {
            throw new RuntimeException("Clock moved backwards.  Refusing to generate id");
        }

        if (currStmp == lastStmp) {
            //相同毫秒内,序列号自增
            sequence = (sequence + 1) & MAX_SEQUENCE;
            //同一毫秒的序列数已经达到最大
            if (sequence == 0L) {
                currStmp = getNextMill();
            }
        } else {
            //不同毫秒内,序列号置为0
            sequence = 0L;
        }

        lastStmp = currStmp;

        return (currStmp - START_STAMP) << TIMESTMP_LEFT //时间戳部分
                | datacenterId << DATACENTER_LEFT       //数据中心部分
                | machineId << MACHINE_LEFT             //机器标识部分
                | sequence;                             //序列号部分
    }

    private long getNextMill() {
        long mill = getNewstmp();
        while (mill <= lastStmp) {
            mill = getNewstmp();
        }
        return mill;
    }

    private long getNewstmp() {
        return System.currentTimeMillis();
    }
    
    public static Long getMaxDataCeneterNum() {
        return MAX_DATACENTER_NUM;
    }
    
    public static Long getMaxMachineNum() {
        return MAX_MACHINE_NUM;
    }
}

根据雪花算法的实现可以发现,SnowFlake类提供了一个有参构造函数,如下所示。

public SnowFlake(long datacenterId, long machineId) {
    if (datacenterId > MAX_DATACENTER_NUM || datacenterId < 0) {
        throw new IllegalArgumentException("datacenterId can't be greater than MAX_DATACENTER_NUM or less than 0");
    }
    if (machineId > MAX_MACHINE_NUM || machineId < 0) {
        throw new IllegalArgumentException("machineId can't be greater than MAX_MACHINE_NUM or less than 0");
    }
    this.datacenterId = datacenterId;
    this.machineId = machineId;
}

其中,第一个参数datacenterId表示数据中心id,也可以认为是机房的id,machineId表示机器id,也可以认为是服务所在的服务器id。在SnowFlake的构造方法中,对这两个参数进行了限制,如下所示。

  • datacenterId:大于或者等于0,并且要小于MAX_DATACENTER_NUM,也就是小于31。
  • machineId:大于或者等于0,并且要小于MAX_DATACENTER_NUM,也就是小于31。

所以,类实现的雪花算法支持32个不同的数据中心或机房,并且在每个数据中心或机房中支持32个机器上部署分布式id服务。这对一般的场景来说,已经足够了。

注意:雪花算法的实现强依赖时间戳,所以在SnowFlake源码中存在如下常量,并标注了使用时此值不可更改的注释。

/**
 * 起始的时间戳:2022-04-12 11:56:45,使用时此值不可修改
 */
private final static long START_STAMP = 1649735805910L;

(2)为了防止每次使用SnowFlake类时都会新建一个对象,这里,在io.binghe.shop.utils.id包下新建SnowFlakeFactory类,作为SnowFlake的简单工厂类,在SnowFlakeFactory类中,主要是定义了一个ConcurrentMap类型的成员变量snowFlakeCache用来缓存SnowFlake类的对象,这样就不用在使用SnowFlake类时,每次都要新建一个类对象了。

不就是新建一个对象嘛,为啥还要缓存起来呢?

其实,在普通场景下,新建不新建对象,缓存不缓存对象几乎没啥影响,但是在高并发、大流量的场景下,尤其是经历过高并发、大流量的秒杀系统,如果每次都创建对象的话,系统的性能与资源损耗还是比较大的。

SnowFlakeFactory类的源码如下所示。

/**
 * @description 雪花算法工厂
 */  
public class SnowFlakeFactory { 
 
 /**
 * 默认数据中心id
  */
private static final long DEFAULT_DATACENTER_ID = 1;
 /**
 * 默认的机器id
  */
private static final long DEFAULT_MACHINE_ID = 1;
 
 /**
 * 默认的雪花算法句柄
  */
private static final String DEFAULT_SNOW_FLAKE = "snow_flake";
 
 /**
 * 缓存SnowFlake对象
  */
private static ConcurrentMap<String, SnowFlake> snowFlakeCache = new ConcurrentHashMap<>(2);
 
public static SnowFlake getSnowFlake(long datacenterId, long machineId) {
    return new SnowFlake(datacenterId, machineId);
}
 
public static SnowFlake getSnowFlake() {
    return new SnowFlake(DEFAULT_DATACENTER_ID, DEFAULT_MACHINE_ID);
}
 
public static SnowFlake getSnowFlakeFromCache() {
	SnowFlake snowFlake = snowFlakeCache.get(DEFAULT_SNOW_FLAKE);
	if(snowFlake == null) {
	    snowFlake = new SnowFlake(DEFAULT_DATACENTER_ID, DEFAULT_MACHINE_ID);
	    snowFlakeCache.put(DEFAULT_SNOW_FLAKE, snowFlake);
	}
	return snowFlake;
 }
 
 /**
 * 根据数据中心id和机器id从缓存中获取全局id
 * @param dataCenterId: 取值为1~31
 * @param machineId: 取值为1~31
  */
 public static SnowFlake getSnowFlakeByDataCenterIdAndMachineIdFromCache(Long dataCenterId, Long machineId) {
	  if (dataCenterId > SnowFlake.getMaxDataCeneterNum() || dataCenterId < 0) {
          throw new IllegalArgumentException("datacenterId can't be greater than MAX_DATACENTER_NUM or less than 0");
      }
      if (machineId > SnowFlake.getMaxMachineNum() || machineId < 0) {
          throw new IllegalArgumentException("machineId can't be greater than MAX_MACHINE_NUM or less than 0");
      }
	  String key = DEFAULT_SNOW_FLAKE.concat("_").concat(String.valueOf(dataCenterId)).concat("_").concat(String.valueOf(machineId));
	  SnowFlake snowFlake = snowFlakeCache.get(key);
	  if(snowFlake == null) {
	      snowFlake = new SnowFlake(dataCenterId, machineId);
	      snowFlakeCache.put(key, snowFlake);
	  }
	  return snowFlake;
	  }
}

在SnowFlakeFactory类中,主要对外提供了两个获取SnowFlake的方法,一个是getSnowFlakeFromCache()方法,另一个是getSnowFlakeByDataCenterIdAndMachineIdFromCache()方法。

  • getSnowFlakeFromCache()方法

在snowFlakeCache缓存中获取默认的SnowFlake对象实例,如果对象不存在,则调用SnowFlake类的构造方法,并且传入默认的数据中心id和机器id,将实例化后的SnowFlake对象加入缓存,并且返回SnowFlake对象。源码如下所示。

public static SnowFlake getSnowFlakeFromCache() {
    SnowFlake snowFlake = snowFlakeCache.get(DEFAULT_SNOW_FLAKE);
    if(snowFlake == null) {
        snowFlake = new SnowFlake(DEFAULT_DATACENTER_ID, DEFAULT_MACHINE_ID);
        snowFlakeCache.put(DEFAULT_SNOW_FLAKE, snowFlake);
    }
    return snowFlake;
}
  • getSnowFlakeByDataCenterIdAndMachineIdFromCache()方法

getSnowFlakeByDataCenterIdAndMachineIdFromCache()方法提供了两个参数,一个是Long类型的dataCenterId,表示数据中心或者机房的id,一个是Long类型的machineId,表示机器id或者服务所在的服务器id。

在getSnowFlakeByDataCenterIdAndMachineIdFromCache()方法中,会对传入的两个参数进行限制。然后生成缓存SnowFlake对象实例的缓存Key,根据生成的Key到snowFlakeCache缓存中获取SnowFlake对象实例,如果对象实例不存在,则根据传入的dataCenterId和machineId生成SnowFlake对象实例,并放入snowFlakeCache缓存中,最后返回SnowFlake对象实例。源码如下所示。

/**
 * 根据数据中心id和机器id从缓存中获取全局id
 * @param dataCenterId: 取值为1~31
 * @param machineId: 取值为1~31
 */
public static SnowFlake getSnowFlakeByDataCenterIdAndMachineIdFromCache(Long dataCenterId, Long machineId) {
    if (dataCenterId > SnowFlake.getMaxDataCeneterNum() || dataCenterId < 0) {
        throw new IllegalArgumentException("datacenterId can't be greater than MAX_DATACENTER_NUM or less than 0");
    }
    if (machineId > SnowFlake.getMaxMachineNum() || machineId < 0) {
        throw new IllegalArgumentException("machineId can't be greater than MAX_MACHINE_NUM or less than 0");
    }
    String key = DEFAULT_SNOW_FLAKE.concat("_").concat(String.valueOf(dataCenterId)).concat("_").concat(String.valueOf(machineId));
    SnowFlake snowFlake = snowFlakeCache.get(key);
    if(snowFlake == null) {
        snowFlake = new SnowFlake(dataCenterId, machineId);
        snowFlakeCache.put(key, snowFlake);
    }
    return snowFlake;
}

(3)为了便于管理每个服务的dataCenterId和machineId,这里将每个服务的dataCenterId和machineId作为配置参数,后续也可以存储到Zookeeper或者Etcd等分布式配置中心。

所以,在io.binghe.shop.utils.id包下新建SnowFlakeLoader类,io.binghe.shop.utils.id.SnowFlakeLoader类的作用主要是加载classpath类路径下的snowflake/snowflake.properties文件,读取dataCenterId和machineId,SnowFlakeLoader类的源码如下所示。

/**
 * @description 定义加载params.properties文件的工具类
 */
public class SnowFlakeLoader {

 public static final String DATA_CENTER_ID = "data.center.id";
 public static final String MACHINE_ID = "machine.id";

 private volatile static Properties instance;
    static {
        InputStream in = SnowFlakeLoader.class.getClassLoader().getResourceAsStream("snowflake/snowflake.properties");
        instance = new Properties();
        try {
            instance.load(in);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private static String getStringValue(String key){
        if(instance == null) return "";
        return instance.getProperty(key, "");
    }

    private static Long getLongValue(String key){
       String v = getStringValue(key);
       return (v == null || v.trim().isEmpty()) ? 0 : Long.parseLong(v);
    }

    public static Long getDataCenterId() {
     return getLongValue(DATA_CENTER_ID);
    }

    public static Long getMachineId() {
     return getLongValue(MACHINE_ID);
    }
}

(4)为了配合SnowFlakeLoader类读取配置文件中的内容,在项目的resources目录下新建snowflake目录,并在snowflake目录下新建snowflake.properties文件,snowflake.properties文件的内容如下所示。

data.center.id=1
machine.id=1

至此,我们项目的通用工具类模块就实现完毕了,后续在开发具体业务时,如果需要扩展,我们在一起扩展通用工具类模块。

4.4 创建实体类模块

在父工程下创建实体类模块shop-bean,作为整个项目的通用实体类模块,实体类模块的总体结构如下所示。
在这里插入图片描述

4.4.1 添加项目依赖

shop-bean模块的依赖相对来说就比较简单了,只需要依赖shop-utils模块即可。在shop-bean模块的pom.xml文件中添加如下配置。

<dependencies>
    <dependency>
        <groupId>io.binghe.shop</groupId>
        <artifactId>shop-utils</artifactId>
        <version>1.0.0-SNAPSHOT</version>
    </dependency>
</dependencies>
4.4.2 核心类开发

对于shop-bean模块来说,主要的功能就是提供JavaBean,目前主要提供四个实体类,分别如下所示。

(1)io.binghe.shop.bean#User类,表示用户类

源码如下所示。

/**
 * @description 用户实体类
 */
@Data
@TableName("t_user")
public class User implements Serializable {

    private static final long serialVersionUID = -7032479567987350240L;

    /**
     * 数据id
     */
    @TableId(value = "id", type = IdType.INPUT)
    @TableField(value = "id", fill = FieldFill.INSERT)
    private Long id;

    /**
     * 用户名
     */
    @TableField("t_username")
    private String username;

    /**
     * 密码
     */
    @TableField("t_password")
    private String password;

    /**
     * 手机号
     */
    @TableField("t_phone")
    private String phone;

    /**
     * 地址
     */
    @TableField("t_address")
    private String address;

    public User(){
        this.id = SnowFlakeFactory.getSnowFlakeFromCache().nextId();
        //默认密码
        this.password = PasswordUtils.getPassowrd("123456");
    }
}

(2)io.binghe.shop.bean#Product类,表示商品类

源码如下所示。

/**
 * @description 商品
 */
@Data
@TableName("t_product")
public class Product implements Serializable {
    private static final long serialVersionUID = -2907409980909070073L;
    /**
     * 数据id
     */
    @TableId(value = "id", type = IdType.INPUT)
    @TableField(value = "id", fill = FieldFill.INSERT)
    private Long id;

    /**
     * 商品名称
     */
    @TableField("t_pro_name")
    private String proName;

    /**
     * 商品价格
     */
    @TableField("t_pro_price")
    private BigDecimal proPrice;

    /**
     * 商品库存
     */
    @TableField("t_pro_stock")
    private Integer proStock;

    public Product(){
        this.id = SnowFlakeFactory.getSnowFlakeFromCache().nextId();
    }
}

(3)io.binghe.shop.bean#Order类,表示订单类

源码如下所示。

/**
 * @description 订单
 */
@Data
@TableName("t_order")
public class Order implements Serializable {
    private static final long serialVersionUID = -2907409980909070073L;
    /**
     * 数据id
     */
    @TableId(value = "id", type = IdType.INPUT)
    @TableField(value = "id", fill = FieldFill.INSERT)
    private Long id;

    /**
     * 用户id
     */
    @TableField("t_user_id")
    private Long userId;

    /**
     * 用户名
     */
    @TableField("t_user_name")
    private String username;

    /**
     * 手机号
     */
    @TableField("t_phone")
    private String phone;

    /**
     * 地址
     */
    @TableField("t_address")
    private String address;


    /**
     * 商品价格(总价)
     */
    @TableField("t_total_price")
    private BigDecimal totalPrice;

    public Order(){
        this.id = SnowFlakeFactory.getSnowFlakeFromCache().nextId();
    }
}

(4)io.binghe.shop.bean#OrderItem类,表示订单条目类

/**
 * @description 订单明细
 */
@Data
@TableName("t_order_item")
public class OrderItem implements Serializable {
    private static final long serialVersionUID = -1329173923755780293L;

    /**
     * 数据id
     */
    @TableId(value = "id", type = IdType.INPUT)
    @TableField(value = "id", fill = FieldFill.INSERT)
    private Long id;

    @TableField("t_order_id")
    private Long orderId;

    /**
     * 商品id
     */
    @TableField("t_pro_id")
    private Long proId;

    /**
     * 商品名称
     */
    @TableField("t_pro_name")
    private String proName;

    /**
     * 商品价格(单价)
     */
    @TableField("t_pro_price")
    private BigDecimal proPrice;

    /**
     * 购买数量
     */
    @TableField("t_number")
    private Integer number;

    public OrderItem(){
        this.id = SnowFlakeFactory.getSnowFlakeFromCache().nextId();
    }
}

这里是为简化商品的下单逻辑而创建的实体类,实际场景下的实体类会远比这些复杂。

4.5 创建数据表

这里,主要创建四个数据表,分别为用户表、商品表、订单表和订单条目表,分别对应着四个实体类,如下所示。

  • t_user用户表,与User实体类对应

在这里插入图片描述

  • t_product商品表,与Product实体类对应

在这里插入图片描述

  • t_order订单表,与Order实体类对应

在这里插入图片描述

  • t_order_item订单条目表,与OrderItem实体类对应

在这里插入图片描述
至此,项目中的通用工具类模块、通用实体类模块就开发完成了,同时,数据表也创建完毕了。

5. 搭建三大微服务并完成交互开发与测试

在这里插入图片描述

5.1 项目概述

今天我们先使用SpringBoot快速搭建三大微服务,并基于SpringBoot完成三大微服务之间的交互。然后会陆续加入SpringCloud Alibaba的各种组件进行优化。

5.1.1 整体交互流程

用户微服务、商品微服务和订单微服务的交互流程(前面已经描述过)如下:
在这里插入图片描述

5.1.2 服务规划

对于用户微服务、商品微服务和订单微服务来说,每个服务占用的端口和访问的基础路径是不同的,这里就将每个服务占用的端口和访问的基础路径整理成下表所示。

在这里插入图片描述

5.2 用户微服务

用户微服务主要用来提供对于用户基础信息的增删改查操作,用户执行下单操作时,会从用户微服务中查询用户的基本信息。用户微服务的总体结构如下图所示。
在这里插入图片描述

5.2.1 搭建项目

创建名称为shop-user的Maven项目,由于我们在前面,已经完成了对项目整体结构的搭建,所以,在shop-user的pom.xml文件里添加如下依赖即可。

<dependencies>
    <dependency>
        <groupId>io.binghe.shop</groupId>
        <artifactId>shop-bean</artifactId>
        <version>1.0.0-SNAPSHOT</version>
    </dependency>
</dependencies>

接下来,在项目的resources目录下创建application.yml文件,并在application.yml文件中添加如下配置。

server:
  port: 8060
  servlet:
    context-path: /user
spring:
  application:
    name: server-user
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/shop?useSSL=false&useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&serverTimezone=Asia/Shanghai
    username: root
    password: root
    platform: mysql
    type: com.alibaba.druid.pool.DruidDataSource
    # 下面为连接池的补充设置,应用到上面所有数据源中
    # 初始化大小,最小,最大
    initialSize: 10
    minIdle: 5
    maxActive: 20
    # 配置获取连接等待超时的时间
    maxWait: 60000
    # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
    timeBetweenEvictionRunsMillis: 3600000
    # 配置一个连接在池中最小生存的时间,单位是毫秒
    minEvictableIdleTimeMillis: 3600000
    validationQuery: select 1 from dual
    testWhileIdle: true
    testOnBorrow: false
    testOnReturn: false
    # 打开PSCache,并且指定每个连接上PSCache的大小
    poolPreparedStatements: true
    maxPoolPreparedStatementPerConnectionSize: 20
    maxOpenPreparedStatements: 20
    # 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
    filters: stat

  http:
    encoding:
      enabled: true
      charset: UTF-8
      force: true

  mybatis-plus:
    global-config:
      db-config:
        id-type: auto
        field-strategy: not-empty
        table-underline: true
        db-type: oracle
        logic-delete-value: 1
        logic-not-delete-value: 0
    mapper-locations: classpath:/mapper/*.xml
    configuration:
      jdbc-type-for-null: 'null'

从配置文件中,我们也可以看出,用户微服务启动后监听的端口为8060,基础的访问路径为/user,应用的名称为server-user,使用的数据库为MySQL,使用的数据库连接池为阿里开源的Druid,项目的交互编码采用UTF-8,持久化框架采用的是MyBatis-Plus。

这里注意一点的是,我们在application.yml文件中的mybatis-plus节点下定义了Mapper的xml文件的存放位置,如下所示:

spring:
  mybatis-plus:
    mapper-locations: classpath:/mapper/*.xml

Mapper的xml文件位置在当前项目的classpath目录下的mapper目录下。也就是说,我们在使用MyBatis-Plus框架时,如果涉及到自己写SQL语句时,会将SQL语句写到XML文件中,而不是通过注解的方式写到接口的方法上。

5.2.2 开发持久层

用户持久层主要是提供对于用户数据表的基本增删改查操作。持久化框架采用的是MyBatis-Plus,在项目的io.binghe.shop.user.mapper包下创建UserMapper接口,主要用作使用MyBatis-Plus框架操作用户数据表的基础Mapper接口,源码如下所示。

/**
 * @description 用户Mapper
 */
public interface UserMapper extends BaseMapper<User> {
    
}

可以看到,在UserMapper接口中没有定义任何方法,这是由于MyBatis-Plus框架中已经封装好了针对实体类的基础增删改查操作,而我们目前快速搭建三大微服务的过程中,使用MyBatis-Plus框架封装的基础增删改查操作就能够满足需求了。所以,在UserMapper接口中,未定义任何方法。

接下来,在resources目录下创建mapper目录里创建UserMapper.xml文件,文件的内容如下所示。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="io.binghe.shop.user.mapper.UserMapper">
 
</mapper>

UserMapper.xml文件与UserMapper接口是一一对应的,UserMapper中定义的接口方法,都会在UserMapper.xml文件中写对应的SQL语句。由于UserMapper接口中位定义任何方法,所以在UserMapper.xml文件中,也就未声明任何SQL语句了。

5.2.3 开发业务逻辑层

用户微服务的业务逻辑层主要提供针对用户信息的业务逻辑开发,在io.binghe.shop.user.service包下创建UserService接口,源码如下所示。

/**
 * @description 用户业务接口
 */
public interface UserService {

    /**
     * 根据id获取用户信息
     */
    User getUserById(Long userId);
}

可以看到,在UserService接口中,定义了一个通过用户id获取用户信息的方法getUserById(),提供这个方法传入用户的id编号就可以获取到用户的信息。

接下来,在io.binghe.shop.user.service.impl包下创建UserServiceImpl类,实现UserService接口,并实现UserService接口定义的方法getUserById(),UserServiceImpl类的源码如下所示。

/**
 * @description 用户业务实现类
 */
@Service
public class UserServiceImpl implements UserService {
    @Autowired
    private UserMapper userMapper;

    @Override
    public User getUserById(Long userId) {
        return userMapper.selectById(userId);
    }
}

由于项目中使用了SpringBoot,SpringBoot又是基于Spring开发的,所以我们在项目开发过程中,都是使用Spring来管理Java对象的生命周期,也就是bean的生命周期。所以在UserServiceImpl类上标注了@Service注解,使用@Autowired注解注入了UserMapper对象,并在getUserById()中调用UserMapper对象的selectById()方法获取用户的基本信息。

5.2.4 开发接口层

用户微服务的接口层主要对外提供用户微服务的接口,在io.binghe.shop.user.controller包下创建UserController类,源码如下所示。

/**
 * @description 用户接口
 */
@Slf4j
@RestController
public class UserController {

    @Autowired
    private UserService userService;

    @GetMapping(value = "/get/{uid}")
    public User getUser(@PathVariable("uid") Long uid){
        User user = userService.getUserById(uid);
        log.info("获取到的用户信息为:{}", JSONObject.toJSONString(user));
        return user;
    }
}

在UserController上标注了@RestController注解,表明UserController能够提供Restful风格的接口,同时在UserController类中使用@Autowired注解注入了UserService对象,在getUser()方法中调用UserService对象的getUserById()方法获取用户的信息,并返回用户的信息,同时在getUserById()方法上标注了@GetMapping注解表示这个接口采用HTTP GET方式访问,并定义了这个接口的访问路径。

用户调用订单微服务的提交订单接口下单时,订单微服务会调用用户微服务的接口获取用户的基本信息。

5.2.5 开发服务启动类

在用户微服务的io.binghe.shop包下创建UserStarter类,作为用户微服务的启动类,源码如下所示。

/**
 * @description 启动用户服的类
 */
@SpringBootApplication
@EnableTransactionManagement(proxyTargetClass = true)
@MapperScan(value = { "io.binghe.shop.user.mapper" })
public class UserStarter {

    public static void main(String[] args){
        SpringApplication.run(UserStarter.class, args);
    }
}

至此,用户微服务开发完成。

5.3 商品微服务

商品微服务主要用来提供对于商品基础信息的增删改查操作,用户执行下单操作时,会从商品微服务中查询商品的基本信息,并完成商品的库存扣减操作。商品微服务的总体结构如下图所示。
在这里插入图片描述

5.3.1 搭建项目

商品微服务的搭建过程和用户微服务的搭建过程类似,只是在application.yml文件中的部分配置不同,在商品微服务的application.yml文件中,需要将端口修改为8070,基础访问路径修改为/product,应用名称修改为server-product。

5.3.2 开发持久层

商品持久层主要提供对于商品数据表的增删改查操作,在io.binghe.shop.product.mapper包下创建ProductMapper接口,用于MyBatis-Plus操作商品数据表的基础Mapper接口,源码如下所示。

/**
 * @description 商品服务Mapper接口
 */
public interface ProductMapper extends BaseMapper<Product> {

    /**
     * 扣减商品库存
     */
    int updateProductStockById(@Param("count") Integer count, @Param("id") Long id);
}

可以看到,在ProductMapper接口中除了可以使用MyBatis-Plus框架提供的基础增删改查外,还提供了一个单独的扣减商品库存的方法updateProductStockById(),传入需要扣减的商品数量和商品的id就可以实现商品库存的扣减操作。

接下来,在resources的mapper目录下创建ProductMapper.xml文件,在ProductMapper.xml文件中实现updateProductStockById对应的SQL语句,如下所示。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="io.binghe.shop.product.mapper.ProductMapper">
    <update id="updateProductStockById">
        update t_product set t_pro_stock = t_pro_stock - #{count} where id = #{id}
    </update>
</mapper>
5.3.3 开发业务逻辑层

商品微服务的业务逻辑层主要提供针对商品信息的业务逻辑操作,在io.binghe.shop.product.service包下创建ProductService接口,源码如下所示。

/**
 * @description 商品Service接口
 */
public interface ProductService {

    /**
     * 根据商品id获取商品信息
     */
    Product getProductById(Long pid);


    /**
     * 扣减商品库存
     */
    int updateProductStockById(Integer count, Long id);
}

可以看到,在ProductService接口中定义了获取商品信息的方法getProductById()和扣减商品库存的方法updateProductStockById()。

接下来,在io.binghe.shop.product.service.impl包下创建ProductServiceImpl类,实现ProductService接口,并实现ProductService接口中定义的getProductById()方法和updateProductStockById()方法,如下所示。

/**
 * @description 商品业务实现类
 */
@Service
public class ProductServiceImpl implements ProductService {
    @Autowired
    private ProductMapper productMapper;
    @Override
    public Product getProductById(Long pid) {
        return productMapper.selectById(pid);
    }

    @Override
    public int updateProductStockById(Integer count, Long id) {
        return productMapper.updateProductStockById(count, id);
    }
}

在ProductServiceImpl类中,getProductById()方法使用的是MyBatis-Plus框架中提供的selectById()方法获取商品的信息,updateProductStockById()方法中使用的是ProductMapper接口中定义的updateProductStockById()方法扣减商品的库存。

5.3.4 开发接口层

商品微服务的接口层主要是商品对外提供的接口,在io.binghe.shop.product.controller包下创建ProductController类,并在类上标注@RestController注解表示ProductController类提供的是Restful风格的接口。ProductController类的源码如下所示。

/**
 * @description 商品api
 */
@RestController
@Slf4j
public class ProductController {

    @Autowired
    private ProductService productService;

    @GetMapping(value = "/get/{pid}")
    public Product getProduct(@PathVariable("pid") Long pid){
        Product product = productService.getProductById(pid);
        log.info("获取到的商品信息为:{}", JSONObject.toJSONString(product));
        return product;
    }

    @GetMapping(value = "/update_count/{pid}/{count}")
    public Result<Integer> updateCount(@PathVariable("pid") Long pid, @PathVariable("count") Integer count){
        log.info("更新商品库存传递的参数为: 商品id:{}, 购买数量:{} ", pid, count);
        int updateCount = productService.updateProductStockById(count, pid);
        Result<Integer> result = new Result<>(HttpCode.SUCCESS, "执行成功", updateCount);
        return result;
    }
}

可以看到,在ProductController类中,提供了一个获取商品信息的接口和扣减商品库存的接口,用户调用订单微服务的提交订单接口下单时,订单微服务会调用商品微服务的接口获取商品的基本信息并扣减商品的库存。

5.3.5 开发服务启动类

在商品微服务的io.binghe.shop包下创建ProductStarter类,作为商品微服务的启动类,源码如下所示。

/**
 * @description 商品服务启动类
 */
@SpringBootApplication
@MapperScan(value = { "io.binghe.shop.product.mapper" })
@EnableTransactionManagement(proxyTargetClass = true)
public class ProductStarter {

    public static void main(String[] args){
        SpringApplication.run(ProductStarter.class, args);
    }
}

至此,商品微服务开发完成。

5.4 订单微服务

订单微服务主要用来提供用户下单操作的业务逻辑,用户执行下单操作时,订单微服务会调用用户微服务的接口获取用户的基本信息,会调用商品微服务的接口获取商品的基本信息。在订单微服务中校验用户的合法性和校验商品库存是否充足,如果用户合法并且商品库存充足,就会向订单数据表中记录订单信息并调用商品微服务的接口来扣减商品的库存。

订单微服务的总体结构如下图所示。
在这里插入图片描述

5.4.1 搭建项目

订单微服务的项目搭建过程与用户微服务和商品微服务的项目搭建过程类似,只是在application.yml文件中的部分配置不同,在订单微服务的application.yml文件中,需要将端口修改为8080,基础访问路径修改为/order,应用名称修改为server-order。

5.4.2 开发持久层

订单微服务的持久层主要提供对订单数据表的增删改查操作,订单服务会涉及到对t_order订单数据表和t_order_item订单条目数据表的操作,所以,在io.binghe.shop.order.mapper包下会创建OrderMapperOrderItemMapper两个接口,如下所示。

  • OrderMapper接口
/**
 * @description 订单Mapper
 */
public interface OrderMapper extends BaseMapper<Order> {
}
  • OrderItemMapper接口
/**
 * @description 订单条目Mapper
 */
public interface OrderItemMapper extends BaseMapper<OrderItem> {
}

由于在订单微服务中,对于订单数据表和订单条目数据表的操作,使用MyBatis-Plus框架提供的基本增删改查功能就能满足需求,所以在OrderMapper和OrderItemMapper接口中并没有定义任何方法。

5.4.3 开发业务逻辑层

订单微服务的业务逻辑层主要完成提交订单的业务逻辑,用户执行下单操作时,订单微服务会调用用户微服务的接口获取用户的基本信息,会调用商品微服务的接口获取商品的基本信息。在订单微服务中校验用户的合法性和校验商品库存是否充足,如果用户合法并且商品库存充足,就会向订单数据表中记录订单信息并调用商品微服务的接口来扣减商品的库存。

在io.binghe.shop.order.service包下创建OrderService接口,在接口中定义一个保存订单的接口saveOrder(),源码如下所示。

/**
 * @description 订单业务接口
 */
public interface OrderService {
    /**
     * 保存订单
     */
    void saveOrder(OrderParams orderParams);
}

接下来,在io.binghe.shop.order.service.impl包下创建OrderServiceImpl类,实现OrderService接口,源码如下所示。

/**
 * @description 订单服务实现类
 */
@Service
@Slf4j
public class OrderServiceImpl implements OrderService {

    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    private OrderItemMapper orderItemMapper;
    @Autowired
    private RestTemplate restTemplate;


    @Override
    @Transactional(rollbackFor = Exception.class)
    public void saveOrder(OrderParams orderParams) {
        if (orderParams.isEmpty()){
            throw new RuntimeException("参数异常: " + JSONObject.toJSONString(orderParams));
        }

        User user = restTemplate.getForObject("http://localhost:8060/user/get/" + orderParams.getUserId(), User.class);
        if (user == null){
            throw new RuntimeException("未获取到用户信息: " + JSONObject.toJSONString(orderParams));
        }
        Product product = restTemplate.getForObject("http://localhost:8070/product/get/" + orderParams.getProductId(), Product.class);
        if (product == null){
            throw new RuntimeException("未获取到商品信息: " + JSONObject.toJSONString(orderParams));
        }
        if (product.getProStock() < orderParams.getCount()){
            throw new RuntimeException("商品库存不足: " + JSONObject.toJSONString(orderParams));
        }
        Order order = new Order();
        order.setAddress(user.getAddress());
        order.setPhone(user.getPhone());
        order.setUserId(user.getId());
        order.setUsername(user.getUsername());
        order.setTotalPrice(product.getProPrice().multiply(BigDecimal.valueOf(orderParams.getCount())));
        orderMapper.insert(order);

        OrderItem orderItem = new OrderItem();
        orderItem.setNumber(orderParams.getCount());
        orderItem.setOrderId(order.getId());
        orderItem.setProId(product.getId());
        orderItem.setProName(product.getProName());
        orderItem.setProPrice(product.getProPrice());
        orderItemMapper.insert(orderItem);

        Result<Integer> result = restTemplate.getForObject("http://localhost:8070/product/update_count/" + orderParams.getProductId() + "/" + orderParams.getCount(), Result.class);
        if (result.getCode() != HttpCode.SUCCESS){
            throw new RuntimeException("库存扣减失败");
        }
        log.info("库存扣减成功");
    }
}

可以看到,在OrderServiceImpl类的实现中,使用了OrderMapper、OrderItemMapper和RestTemplate,RestTemplate主要用来实现远程调用。

在saveOrder()方法的实现中,实现的主要逻辑如下。

(1)判断orderParams封装的参数是否为空,如果参数为空,则抛出参数异常。

(2)通过RestTemplate调用用户微服务获取用户的基本信息,如果获取的用户信息为空,则抛出未获取到用户信息的异常。

(3)通过RestTemplate调用商品微服务获取商品的基本信息,如果获取的商品信息为空,则抛出未获取到商品信息的异常。

(4)判断商品的库存是否小于待扣减的商品数量,如果商品的库存小于待扣减的商品数量,则抛出商品库存不足的异常。

(5)如果orderParams封装的参数不为空,并且获取的用户信息和商品信息不为空,同时商品的库存充足,则创建订单对象保存订单信息,创建订单条目对象,保存订单条目信息。

(6)调用商品微服务的接口扣减商品库存。

5.4.4 开发接口层

订单微服务的接口层主要是订单微服务对外提供相应的接口,在io.binghe.shop.order.controller包下创建OrderController类,并在OrderController类上添加@RestController注解,表示OrderController类提供的接口是Restful风格的接口,OrderController类的源码如下所示。

@Slf4j
@RestController
public class OrderController {
    @Autowired
    private OrderService orderService;

    @GetMapping(value = "/submit_order")
    public String submitOrder(OrderParams orderParams){
        log.info("提交订单时传递的参数:{}", JSONObject.toJSONString(orderParams));
        orderService.saveOrder(orderParams);
        return "success";
    }

可以看到,OrderController类提供的接口就比较简单了,通过传入相应的参数,调用OrderService的saveOrder方法完成下单操作。

5.4.5 开发服务启动类

在订单微服务的io.binghe.shop包下创建OrderStarter类,作为订单微服务的启动类,源码如下所示。

/**
 * @description 订单服务启动类
 */
@SpringBootApplication
@EnableTransactionManagement(proxyTargetClass = true)
@MapperScan(value = { "io.binghe.shop.order.mapper" })
public class OrderStarter {
    public static void main(String[] args){
        SpringApplication.run(OrderStarter.class, args);
    }
}
5.4.6 开发辅助类

在订单微服务中,使用了RestTemplate来完成远程服务的调用,关于RestTemplate的配置,在io.binghe.shop.order.config包下新建LoadBalanceConfig类,并在LoadBalanceConfig类上标注@Configuration注解,表示LoadBalanceConfig类是一个配置类,在LoadBalanceConfig类中使用@Bean注解将RestTemplate对象交由Spring管理,LoadBalanceConfig类的源码如下所示。

/**
 * @description 配置类
 */
@Configuration
public class LoadBalanceConfig {

    @Bean
    public RestTemplate restTemplate(){
        return new RestTemplate();
    }
}

至此,订单微服务开发完成,接下来,我们进行测试。

5.5 测试项目

开发完成后,我们对快速搭建并开发完成的三大微服务进行简单的测试,在测试之前我们需要先在数据表中添加一些测试数据。

5.5.1 添加测试数据

(1)在用户表中添加一条id为1001的记录,如下所示。

INSERT INTO `shop`.`t_user`(`id`, `t_username`, `t_password`, `t_phone`, `t_address`) VALUES (1001, 'binghe', 'c26be8aaf53b15054896983b43eb6a65', '13212345678', '北京');

(2)在商品数据表中添加几条商品记录,如下所示。

INSERT INTO `shop`.`t_product`(`id`, `t_pro_name`, `t_pro_price`, `t_pro_stock`) VALUES (1001, '华为', 2399.00, 100);
INSERT INTO `shop`.`t_product`(`id`, `t_pro_name`, `t_pro_price`, `t_pro_stock`) VALUES (1002, '小米', 1999.00, 100);
INSERT INTO `shop`.`t_product`(`id`, `t_pro_name`, `t_pro_price`, `t_pro_stock`) VALUES (1003, 'iphone', 4999.00, 100);
5.5.2 测试库存不足的情况

(1)分别启动用户微服务、商品微服务和订单微服务。

(2)查询id为1001的商品信息,如下所示。

mysql> select * from t_product where id = 1001;
+------+------------+-------------+-------------+
| id   | t_pro_name | t_pro_price | t_pro_stock |
+------+------------+-------------+-------------+
| 1001 | 华为       |     2399.00 |         100 |
+------+------------+-------------+-------------+
1 row in set (0.00 sec)

可以看到,id为1001的商品的库存为100。

(3)查询订单表和订单条目表中的数据,如下所示。

  • 查询订单表
mysql> select * from t_order;
Empty set (0.00 sec)

可以看到,订单数据表的数据为空。

  • 查询订单条目表
mysql> select * from t_order_item;
Empty set (0.00 sec)

可以看到,订单条目数据表的数据为空。

(4)在浏览器中调用订单微服务的下单接口,传入的商品数量为1001,如下所示。

在这里插入图片描述
(5)再次查询id为1001的商品信息,如下所示。

mysql> select * from t_product where id = 1001;
+------+------------+-------------+-------------+
| id   | t_pro_name | t_pro_price | t_pro_stock |
+------+------------+-------------+-------------+
| 1001 | 华为       |     2399.00 |         100 |
+------+------------+-------------+-------------+
1 row in set (0.00 sec)

可以看到,商品id为1001的商品库存仍为100,并没有减少。

(6)再次查询订单表和订单条目表中的数据,如下所示。

  • 查询订单表
mysql> select * from t_order;
Empty set (0.00 sec)

可以看到,订单数据表的数据为空。

  • 查询订单条目表
mysql> select * from t_order_item;
Empty set (0.00 sec)

可以看到,订单条目数据表的数据为空。

综上,当提交订单时传入的商品数量大于商品的库存数量时,系统会抛出异常,并不会执行提交订单和扣减库存的操作。

5.5.3 测试正常下单的情况

(1)在测试库存不足的情况的基础上,我们将调用提交订单的接口时传入的商品数量修改为10,如下所示。

在这里插入图片描述
可以看到,当商品库存充足时,调用订单微服务的下单接口,返回的数据为success表示下单成功。

(2)再次查询id为1001的商品信息,如下所示。

mysql> select * from t_product where id = 1001;
+------+------------+-------------+-------------+
| id   | t_pro_name | t_pro_price | t_pro_stock |
+------+------------+-------------+-------------+
| 1001 | 华为       |     2399.00 |          90 |
+------+------------+-------------+-------------+
1 row in set (0.00 sec)

可以看到,id为1001的商品库存由原来的100变更为90,减少了10个库存。

(3)再次查询订单表和订单条目表中的数据,如下所示。

  • 查询订单表
mysql> select * from t_order;
+------------------+-----------+-------------+-------------+-----------+---------------+
| id               | t_user_id | t_user_name | t_phone     | t_address | t_total_price |
+------------------+-----------+-------------+-------------+-----------+---------------+
| 3270016896208896 |      1001 | binghe      | 13212345678 | 北京      |      23990.00 |
+------------------+-----------+-------------+-------------+-----------+---------------+
1 row in set (0.00 sec)

可以看到,订单数据表中成功记录了订单的信息

  • 查询订单条目表
mysql> select * from t_order_item;
+------------------+------------------+----------+------------+-------------+----------+
| id               | t_order_id       | t_pro_id | t_pro_name | t_pro_price | t_number |
+------------------+------------------+----------+------------+-------------+----------+
| 3270017277890560 | 3270016896208896 |     1001 | 华为       |     2399.00 |       10 |
+------------------+------------------+----------+------------+-------------+----------+
1 row in set (0.00 sec)

可以看到,订单条目数据表中成功记录了订单条目的信息。

至此,项目的测试完毕。


注:直接搭建项目微服务,会遇到很多问题。需要用到各种微服务架构来解决问题。

  • 涉及到服务治理服务配置的需求时,我们会使用Nacos实现。
  • 涉及到负载均衡的需求时,我们会使用Ribbon实现。
  • 涉及到远程服务调用的需求时,我们会使用Fegin实现。
  • 涉及到服务容错的需求时,我们会使用Sentinel实现。
  • 涉及到服务网关的需求时,由于目前SpringCloud Alibaba中并未提供自己的网关,所以,我们使用SpringCloud中的Gateway实现。
  • 涉及到链路追踪的需求时,我们会使用Sleuth+ZipKin实现。
  • 涉及到消息服务的需求时,我们会使用RocketMQ实现。
  • 涉及到分布式事务的需求时,我们会使用Seata实现。
  • 涉及到数据存储的需求时,我们会MySQL+ElasticSearch实现。

参考:

本文是参考冰河技术得SpringCloud Alibaba实战专栏内容整理的,只是方便学习使用(侵权删),具体想要了解或想要获取源码的,请参考下面冰河技术的链接,关注公众号获取源码。

SpringCloud Alibaba

冰河技术:《SpringCloud Alibaba实战》

  • 2
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值