1. 项目流程搭建
整个项目主要分为 用户微服务、商品微服务和订单微服务,整个过程模拟的是用户下单扣减库存的操作。这里,为了简化整个流程,将商品的库存信息保存到了商品数据表,同时,使用商品微服务来扣减库存。小伙伴们在实现时,也可以将商品库存信息单独开发一个微服务模块,主体逻辑和将库存信息放在商品微服务进行管理是一样的。各服务之间的调用流程如下:
用户微服务、商品微服务和订单微服务的整体流程为:用户通过客户端调用订单微服务的提交订单的接口后,订单微服务会分别调用用户微服务和商品微服务的接口来查询用户信息和商品信息,并校验商品库存是否充足,如果商品库存充足的话,就会保存订单。并且会调用商品微服务的扣减库存的接口来扣减库存
2. 技术选型
整个项目采用 SpringCloud Alibaba 技术栈实现,主要的技术选型如下所示:
- 持久层框架:MyBatis、MyBatis-Plus
- 微服务框架:SpringCloud Alibaba
- 消息中间件:RocketMQ
- 服务治理与服务配置:Nacos
- 负载均衡组件:Ribbon
- 远程服务调用:Fegin
- 服务限流与容错:Sentinel
- 服务网关:SpringCloud-Gateway
- 服务链路追踪:Sleuth + ZipKin
- 分布式事务:Seata
- 数据存储:MySQL+ElasticSearch
3. 模块划分
为了方便开发和维护,同时为了模块的复用性,整体项目在搭建时,会将用户微服务、商品微服务和订单微服务放在同一个 Maven 父工程下,作为父工程的子模块,同时,将用户微服务、商品微服务和订单微服务都会使用的 JavaBean 单独作为一个 Maven 模块,以及各服务都会使用的工具类单独作为一个 Maven 模块
其中各模块的说明如下所示:
- shop-springcloud-alibaba:Maven 父工程。
- shop-bean:各服务都会使用的 JavaBean 模块,包含实体类、Dto、Vo 等 JavaBean。
- shop-utils:各服务都会使用的工具类模块。
- shop-order:订单微服务。
- shop-product:商品微服务。
- shop-user:用户微服务
4. 代码地址
代码码云地址
其中,数据库文件位于 db
文件夹下。
5. 模块开发
代码已托管到码云,这里只贴部分代码。
5.1 创建 maven 父工程
在IDEA中创建 Maven 工程,名称为 shop-springcloud-alibaba,创建后在项目的 pom.xml 文件中添加
StringBoot 与 SpringCloud alibaba 相关的配置,如下所示:
<?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>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.6.RELEASE</version>
<relativePath/>
</parent>
<packaging>pom</packaging>
<groupId>com.zzc</groupId>
<artifactId>shop-springcloud-alibaba</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>shop-springcloud-alibaba</name>
<description>shop-springcloud-alibaba</description>
<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>
</project>
5.2 创建工具类模块
在父工程下创建工具类模块 shop-utils,作为整个项目的通用工具类模块。工具类模块的总体结构如下所示:
代码结构说明:
HttpCode
:HTTP 状态码封装类RestCtrlExceptionHandler
:全局异常捕获类md5
包:通用 MD5 与密码加密类Result
:数据响应类id
包:使用雪花算法生成 Id
5.3 创建其它微服务模块
5.3.1 创建实体类模块
在父工程下创建实体类模块 shop-bean,作为整个项目的通用实体类模块
shop-bean 模块的依赖相对来说就比较简单了,只需要依赖 shop-utils 模块即可。在 shop-bean 模块的 pom.xml 文件中添加如下配置:
<dependency>
<groupId>com.zzc</groupId>
<artifactId>shop-utils</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
5.3.2 创建三大微服务并完成交互
对于用户微服务、商品微服务和订单微服务来说,每个服务占用的端口和访问的基础路径是不同的,这里就将每个服务占用的端口和访问的基础路径整理成下表所示:
服务名称 | 项目名称 | 占用端口 | 访问的基础路径 | 备注 |
---|---|---|---|---|
用户微服务 | shop-user | 8060 | /user | 用户的增删改查 |
商品微服务 | shop-product | 8070 | /product | 商品的增删改查 |
订单微服务 | shop-order | 8080 | /order | 订单的增删改查 |
创建微服务
创建名称为 shop-user
的 Maven 项目,由于我们在前面的文章中,已经完成了对项目整体结构的搭建,所以,在 shop-user 的 pom.xml 文件里添加如下依赖即可:
<dependency>
<groupId>com.zzc</groupId>
<artifactId>shop-bean</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
shop-product
、shop-order
微服务与 shop-user
类似,只是配置文件 application.yml
配置端口、访问路径不同。
6. 订单微服务下单接口
这里重点看看 shop-order
的下单接口:
@Override
@Transactional(rollbackFor = Exception.class)
public void saveOrder(OrderParamVo orderParamVo) {
if (orderParamVo.isEmpty()){
throw new RuntimeException("参数异常: " + JSONObject.toJSONString(orderParamVo));
}
User user = restTemplate.getForObject("http://localhost:8060/user/get/" + orderParamVo.getUserId(), User.class);
if (user == null){
throw new RuntimeException("未获取到用户信息: " + JSONObject.toJSONString(orderParamVo));
}
Product product = restTemplate.getForObject("http://localhost:8070/product/get/" + orderParamVo.getProductId(), Product.class);
if (product == null){
throw new RuntimeException("未获取到商品信息: " + JSONObject.toJSONString(orderParamVo));
}
if (product.getProStock() < orderParamVo.getCount()){
throw new RuntimeException("商品库存不足: " + JSONObject.toJSONString(orderParamVo));
}
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(orderParamVo.getCount())));
baseMapper.insert(order);
OrderItem orderItem = new OrderItem();
orderItem.setNumber(orderParamVo.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/" + orderParamVo.getProductId() + "/" + orderParamVo.getCount(), Result.class);
if (result.getCode() != HttpCode.SUCCESS){
throw new RuntimeException("库存扣减失败");
}
log.info("库存扣减成功");
}
在 saveOrder()
方法的实现中,实现的主要逻辑如下:
- 判断 orderParams 封装的参数是否为空,如果参数为空,则抛出参数异常。
- 通过 RestTemplate 调用用户微服务获取用户的基本信息,如果获取的用户信息为空,则抛出未获
取到用户信息的异常。 - 通过 RestTemplate 调用商品微服务获取商品的基本信息,如果获取的商品信息为空,则抛出未获
取到商品信息的异常。 - 判断商品的库存是否小于待扣减的商品数量,如果商品的库存小于待扣减的商品数量,则抛出商品
库存不足的异常。 - 如果 orderParams 封装的参数不为空,并且获取的用户信息和商品信息不为空,同时商品的库存
充足,则创建订单对象保存订单信息,创建订单条目对象,保存订单条目信息。 - 调用商品微服务的接口扣减商品库存
在上述实现的过程中,存在一个很明显的问题:那就是将用户微服务所在的IP和端口,以及商品微服务所在的IP和端口硬编码到订单微服务的代码中了。这样的做法存在着非常多的问题
硬编码的问题
如果将用户微服务和商品微服务所在的IP地址和端口号硬编码到订单微服务中,会存在非常多的问题,其中,最明显的问题有三个,如下所示。
- 如果用户微服务和商品微服务的IP地址或者端口号发生了变化,则订单微服务将变得不可用,需要
对同步修改订单微服务中调用用户微服务和商品微服务的IP地址和端口号。 - 如果系统中提供了多个用户微服务和商品微服务,则无法实现微服务的负载均衡功能。
- 如果系统需要支持更高的并发,需要部署更多的用户微服务和商品微服务以及订单微服务,如果将用户微服务和商品微服务的IP地址和端口硬编码到订单微服务,则后续的维护会变得异常复杂。
所以,在微服务开发的过程中,需要引入服务治理功能,实现微服务之间的动态注册与发现。