初识微服务

发布于:2025-08-03 ⋅ 阅读:(16) ⋅ 点赞:(0)

导入项目

首先,我们需要先导入这个单体项目, 然后再来进行接下来的学习

注意:如果是学习过上面Docker课程的同学,虚拟机中已经有了黑马商城项目及MySQL数据库了,不过为了跟其他同学保持一致,可以先将整个项目移除。使用下面的命令:

cd /root
docker compose down

安装MySQL

先导入MySQL文件, 将其复制到虚拟机的/root目录。如果/root下已经存在mysql目录则删除旧的,如果不存在则直接复制本地的:

然后创建一个通用网络:

docker network create hm-net

使用下面的命令来安装MySQL:

docker run -d \
  --name mysql \
  -p 3306:3306 \
  -e TZ=Asia/Shanghai \
  -e MYSQL_ROOT_PASSWORD=123 \
  -v /root/mysql/data:/var/lib/mysql \
  -v /root/mysql/conf:/etc/mysql/conf.d \
  -v /root/mysql/init:/docker-entrypoint-initdb.d \
  --network hm-net\
  mysql

此时,通过命令查看mysql容器:

docker ps

如图:

发现mysql容器正常运行。

如果在过程中遇见了安装失败端口被占用的问题可以参考:

解决docker安装MySQL报错端口被占用问题-CSDN博客

后端

然后是Java代码,利用Idea打开hmall文件

按下ALT + 8键打开services窗口,新增一个启动项:

在弹出窗口中鼠标向下滚动,找到Spring Boot:

点击后应该会在services中出现hmall的启动项:

点击对应按钮,即可实现运行或DEBUG运行。

不过别着急!!

我们还需要对这个启动项做简单配置,在HMallApplication上点击鼠标右键,会弹出窗口,然后选择Edit Configuration

在弹出窗口中配置SpringBoot的启动环境为local:

点击OK配置完成。接下来就可以运行了!

启动完成后,试试看访问下 http://localhost:8080/hi 吧!

如果运行跑不起来, 出现了Property ‘sqlSessionFactory‘ or ‘sqlSessionTemplate‘ are required报错问题, 可能是版本问题, 可以参考:

解决Property ‘sqlSessionFactory‘ or ‘sqlSessionTemplate‘ are required报错问题-CSDN博客

前端

导入hmall-nginx文件, 其中就是一个nginx程序以及我们的前端代码,直接在windows下将其复制到一个非中文、不包含特殊字符的目录下。然后进入hmall-nginx后,利用cmd启动即可:

# 启动nginx
start nginx.exe
# 停止
nginx.exe -s stop
# 重新加载配置
nginx.exe -s reload
# 重启
nginx.exe -s restart

特别注意:

nginx.exe 不要双击启动,而是打开cmd窗口,通过命令行启动。停止的时候也一样要是用命令停止。如果启动失败不要重复启动,而是查看logs目录中的error.log日志,查看是否是端口冲突。如果是端口冲突则自行修改端口解决。

启动成功后,访问http://localhost:18080,应该能看到页面了

认识微服务

单体架构

我们先来认识一下单体架构

单体架构(monolithic structure):顾名思义,整个项目中所有功能模块都在一个工程中开发;项目部署时需要对所有模块一起编译、打包;项目的架构设计、开发模式都非常简单。

当项目规模较小时,这种模式上手快,部署、运维也都很方便,因此早期很多小型项目都采用这种模式。

但随着项目的业务规模越来越大,团队开发人员也不断增加,单体架构就呈现出越来越多的问题:

  • 团队协作成本高:试想一下,你们团队数十个人同时协作开发同一个项目,由于所有模块都在一个项目中,不同模块的代码之间物理边界越来越模糊。最终要把功能合并到一个分支,你绝对会陷入到解决冲突的泥潭之中。
  • 系统发布效率低:任何模块变更都需要发布整个系统,而系统发布过程中需要多个模块之间制约较多,需要对比各种文件,任何一处出现问题都会导致发布失败,往往一次发布需要数十分钟甚至数小时。
  • 系统可用性差:单体架构各个功能模块是作为一个服务部署,相互之间会互相影响,一些热点功能会耗尽系统资源,导致其它服务低可用。

此时如果我们对系统做水平扩展,增加更多机器,资源还是会被这样的热点接口占用,从而影响到其它接口,并不能从根本上解决问题。这也就是单体架构的扩展性差的一个原因。

而要想解决这些问题,就需要使用微服务架构了

微服务

微服务架构,首先是服务化,就是将单体架构中的功能模块从单体应用中拆分出来,独立部署为多个服务。同时要满足下面的一些特点:

  • 单一职责:一个微服务负责一部分业务功能,并且其核心数据不依赖于其它模块。
  • 团队自治:每个微服务都有自己独立的开发、测试、发布、运维人员,团队人员规模不超过10人(2张披萨能喂饱)
  • 服务自治:每个微服务都独立打包部署,访问自己独立的数据库。并且要做好服务隔离,避免对其它服务产生影响

例如,黑马商城项目,我们就可以把商品、用户、购物车、交易等模块拆分,交给不同的团队去开发,并独立部署:

那么,单体架构存在的问题有没有解决呢?

  • 团队协作成本高?
    • 由于服务拆分,每个服务代码量大大减少,参与开发的后台人员在1~3名,协作成本大大降低
  • 系统发布效率低?
    • 每个服务都是独立部署,当有某个服务有代码变更时,只需要打包部署该服务即可
  • 系统可用性差?
    • 每个服务独立部署,并且做好服务隔离,使用自己的服务器资源,不会影响到其它服务。

综上所述,微服务架构解决了单体架构存在的问题,特别适合大型互联网项目的开发,因此被各大互联网公司普遍采用。大家以前可能听说过分布式架构,分布式就是服务拆分的过程,其实微服务架构正式分布式架构的一种最佳实践的方案。

当然,微服务架构虽然能解决单体架构的各种问题,但在拆分的过程中,还会面临很多其它问题。比如:

  • 如果出现跨服务的业务该如何处理?
  • 页面请求到底该访问哪个服务?
  • 如何实现各个服务之间的服务隔离?

SpringCloud

微服务拆分以后碰到的各种问题都有对应的解决方案和微服务组件,而SpringCloud框架可以说是目前Java领域最全面的微服务组件的集合了。

而且SpringCloud依托于SpringBoot的自动装配能力,大大降低了其项目搭建、组件使用的成本。对于没有自研微服务组件能力的中小型企业,使用SpringCloud全家桶来实现微服务开发可以说是最合适的选择了!

目前SpringCloud最新版本为2022.0.x版本,对应的SpringBoot版本为3.x版本,但它们全部依赖于JDK17,目前在企业中使用相对较少。

SpringCloud版本
SpringBoot版本

2022.0.x aka Kilburn

3.0.x

2021.0.x aka Jubilee

2.6.x, 2.7.x (Starting with 2021.0.3)

2020.0.x aka Ilford

2.4.x, 2.5.x (Starting with 2020.0.3)

Hoxton

2.2.x, 2.3.x (Starting with SR5)

Greenwich

2.1.x

Finchley

2.0.x

Edgware

1.5.x

Dalston

1.5.x

因此,我们推荐使用次新版本:Spring Cloud 2021.0.x以及Spring Boot 2.7.x版本。

另外,Alibaba的微服务产品SpringCloudAlibaba目前也成为了SpringCloud组件中的一员

熟悉黑马商城

首先,我们需要熟悉黑马商城项目的基本结构:

大家可以直接启动该项目,测试效果。不过,需要修改数据库连接参数,在application-local.yaml中:

hm:
  db:
    host: 192.168.150.101 # 修改为你自己的虚拟机IP地址
    pw: 123 # 修改为docker中的MySQL密码

同时配置启动项激活的是local环境

登录

首先来看一下登录业务流程:

登录入口在com.hmall.controller.UserController中的login方法:

搜索商品

在首页搜索框输入关键字,点击搜索即可进入搜索列表页面:

该页面会调用接口:/search/list,对应的服务端入口在com.hmall.controller.SearchController中的search方法:

这里目前是利用数据库实现了简单的分页查询。

购物车

在搜索到的商品列表中,点击按钮加入购物车,即可将商品加入购物车:

加入成功后即可进入购物车列表页,查看自己购物车商品列表:

同时这里还可以对购物车实现修改、删除等操作。

相关功能全部在com.hmall.controller.CartController中:

其中,查询购物车列表时,由于要判断商品最新的价格和状态,所以还需要查询商品信息,业务流程如下:

下单

在购物车页面点击结算按钮,会进入订单结算页面:

点击提交订单,会提交请求到服务端,服务端做3件事情:

  • 创建一个新的订单
  • 扣减商品库存
  • 清理购物车中商品

业务入口在com.hmall.controller.OrderController中的createOrder方法:

支付

下单完成后会跳转到支付页面,目前只支持余额支付

在选择余额支付这种方式后,会发起请求到服务端,服务端会立刻创建一个支付流水单,并返回支付流水单号到前端。

当用户输入用户密码,然后点击确认支付时,页面会发送请求到服务端,而服务端会做几件事情:

  • 校验用户密码
  • 扣减余额
  • 修改支付流水状态
  • 修改交易订单状态

请求入口在com.hmall.controller.PayController中:

服务拆分原则

服务拆分一定要考虑几个问题:

  • 什么时候拆?
  • 如何拆?

什么时候拆

一般情况下,对于一个初创的项目,首先要做的是验证项目的可行性。因此这一阶段的首要任务是敏捷开发,快速产出生产可用的产品,投入市场做验证。为了达成这一目的,该阶段项目架构往往会比较简单,很多情况下会直接采用单体架构,这样开发成本比较低,可以快速产出结果,一旦发现项目不符合市场,损失较小。

如果这一阶段采用复杂的微服务架构,投入大量的人力和时间成本用于架构设计,最终发现产品不符合市场需求,等于全部做了无用功。

所以,对于大多数小型项目来说,一般是先采用单体架构,随着用户规模扩大、业务复杂后再逐渐拆分为微服务架构。这样初期成本会比较低,可以快速试错。但是,这么做的问题就在于后期做服务拆分时,可能会遇到很多代码耦合带来的问题,拆分比较困难(前易后难)。

而对于一些大型项目,在立项之初目的就很明确,为了长远考虑,在架构设计时就直接选择微服务架构。虽然前期投入较多,但后期就少了拆分服务的烦恼(前难后易)。

怎么拆

之前我们说过,微服务拆分时粒度要小,这其实是拆分的目标。具体可以从两个角度来分析:

  • 高内聚:每个微服务的职责要尽量单一,包含的业务相互关联度高、完整度高。
  • 耦合:每个微服务的功能要相对独立,尽量减少对其它微服务的依赖,或者依赖接口的稳定性要强。

高内聚首先是单一职责,但不能说一个微服务就一个接口,而是要保证微服务内部业务的完整性为前提。目标是当我们要修改某个业务时,最好就只修改当前微服务,这样变更的成本更低。

一旦微服务做到了高内聚,那么服务之间的耦合度自然就降低了。

当然,微服务之间不可避免的会有或多或少的业务交互,比如下单时需要查询商品数据。这个时候我们不能在订单服务直接查询商品数据库,否则就导致了数据耦合。而应该由商品服务对应暴露接口,并且一定要保证微服务对外接口的稳定性(即:尽量保证接口外观不变)。虽然出现了服务间调用,但此时无论你如何在商品服务做内部修改,都不会影响到订单微服务,服务间的耦合度就降低了。

明确了拆分目标,接下来就是拆分方式了。我们在做服务拆分时一般有两种方式:

  • 纵向拆分
  • 横向拆分

所谓纵向拆分,就是按照项目的功能模块来拆分。例如黑马商城中,就有用户管理功能、订单管理功能、购物车功能、商品管理功能、支付功能等。那么按照功能模块将他们拆分为一个个服务,就属于纵向拆分。这种拆分模式可以尽可能提高服务的内聚性。

横向拆分,是看各个功能模块之间有没有公共的业务部分,如果有将其抽取出来作为通用服务。例如用户登录是需要发送消息通知,记录风控数据,下单时也要发送短信,记录风控数据。因此消息发送、风控数据记录就是通用的业务功能,因此可以将他们分别抽取为公共服务:消息中心服务、风控管理服务。这样可以提高业务的复用性,避免重复开发。同时通用业务一般接口稳定性较强,也不会使服务之间过分耦合。

当然,由于黑马商城并不是一个完整的项目,其中的短信发送、风控管理并没有实现,这里就不再考虑了。而其它的业务按照纵向拆分,可以分为以下几个微服务:

  • 用户服务
  • 商品服务
  • 订单服务
  • 购物车服务
  • 支付服务

拆分购物车、商品服务

接下来,我们先把商品管理功能、购物车功能抽取为两个独立服务。

一般微服务项目有两种不同的工程结构:

  • 完全解耦:每一个微服务都创建为一个独立的工程,甚至可以使用不同的开发语言来开发,项目完全解耦。
    • 优点:服务之间耦合度低
    • 缺点:每个项目都有自己的独立仓库,管理起来比较麻烦
  • Maven聚合:整个项目为一个Project,然后每个微服务是其中的一个Module
    • 优点:项目代码集中,管理和运维方便(授课也方便)
    • 缺点:服务之间耦合,编译时间较长

在hmall父工程之中,我已经提前定义了SpringBoot、SpringCloud的依赖版本,所以为了方便期间,我们直接在这个项目中创建微服务module

商品服务

在hmall中创建module:

选择maven模块,并设定JDK版本为11, 商品模块,我们起名为item-service

引入依赖:

<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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.heima</groupId>
        <artifactId>hmall</artifactId>
        <version>1.0.0</version>
    </parent>

    <artifactId>item-service</artifactId>

    <properties>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
    </properties>
    <dependencies>
        <!--common-->
        <dependency>
            <groupId>com.heima</groupId>
            <artifactId>hm-common</artifactId>
            <version>1.0.0</version>
        </dependency>
        <!--web-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--数据库-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!--mybatis-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
        </dependency>
        <!--单元测试-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
    </dependencies>
    <build>
        <finalName>${project.artifactId}</finalName>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

编写启动类:

接下来是配置文件,可以从hm-service中拷贝:

其中,application.yaml内容如下:

server:
  port: 8081
spring:
  application:
    name: item-service
  profiles:
    active: dev
  datasource:
    url: jdbc:mysql://${hm.db.host}:3306/hm-item?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: ${hm.db.pw}
mybatis-plus:
  configuration:
    default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler
  global-config:
    db-config:
      update-strategy: not_null
      id-type: auto
logging:
  level:
    com.hmall: debug
  pattern:
    dateformat: HH:mm:ss:SSS
  file:
    path: "logs/${spring.application.name}"
knife4j:
  enable: true
  openapi:
    title: 商品服务接口文档
    description: "信息"
    email: zhanghuyi@itcast.cn
    concat: 虎哥
    url: https://www.itcast.cn
    version: v1.0.0
    group:
      default:
        group-name: default
        api-rule: package
        api-rule-resources:
          - com.hmall.item.controller

剩下的application-dev.yamlapplication-local.yaml直接从hm-service拷贝即可。

然后拷贝hm-service中与商品管理有关的代码到item-service,如图:

这里有一个地方的代码需要改动,就是ItemServiceImpl中的deductStock方法:

这也是因为ItemMapper的所在包发生了变化,因此这里代码记得必须修改包路径

最后,还要导入数据库表。默认的数据库连接的是虚拟机,在docker数据库执行SQL文件

注意:为了保证服务之间的数据隔离,需要新建一个hm-item数据库(一般来说是一个新的MySQL实例,但是此处为了方便,就在同一个架构下创建多个数据库);会在数据库创建一个名为hm-item的database,将来的每一个微服务都会有自己的一个database, 在企业开发的生产环境中,每一个微服务都应该有自己的独立数据库服务,而不仅仅是database,这里我们用database来代替。

1.先创建数据源

主机是自己的虚拟机, 驱动程序要选择MySQL, 不要选择另外一个了Amazon Aurora MySQL, 不然就会测试连接失败

2.导入SQL脚本

3.选择对应的架构

接下来,就可以启动测试了,在启动前我们要配置一下启动项,让默认激活的配置为local而不是dev

填写active profiles:

接着,启动item-service,访问商品微服务的swagger接口文档:http://localhost:8081/doc.html

然后测试其中的根据id批量查询商品这个接口:

商品微服务抽取成功了

购物车服务

与商品服务类似,在hmall下创建一个新的module,起名为cart-service:

然后是依赖:

<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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.heima</groupId>
        <artifactId>hmall</artifactId>
        <version>1.0.0</version>
    </parent>

    <artifactId>cart-service</artifactId>
    <packaging>jar</packaging>

    <name>cart-service</name>
    <url>http://maven.apache.org</url>

    <properties>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
    </properties>

    <dependencies>
        <!--common-->
        <dependency>
            <groupId>com.heima</groupId>
            <artifactId>hm-common</artifactId>
            <version>1.0.0</version>
        </dependency>
        <!--web-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--数据库-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!--mybatis-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
        </dependency>
        <!--单元测试-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
    </dependencies>
    <build>
        <finalName>${project.artifactId}</finalName>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

然后是启动类:

然后是配置文件,同样可以拷贝自item-service,不过其中的application.yaml需要修改:

server:
  port: 8082
spring:
  application:
    name: cart-service
  profiles:
    active: dev
  datasource:
    url: jdbc:mysql://${hm.db.host}:3306/hm-cart?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: ${hm.db.pw}
mybatis-plus:
  configuration:
    default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler
  global-config:
    db-config:
      update-strategy: not_null
      id-type: auto
logging:
  level:
    com.hmall: debug
  pattern:
    dateformat: HH:mm:ss:SSS
  file:
    path: "logs/${spring.application.name}"
knife4j:
  enable: true
  openapi:
    title: 商品服务接口文档
    description: "信息"
    email: zhanghuyi@itcast.cn
    concat: 虎哥
    url: https://www.itcast.cn
    version: v1.0.0
    group:
      default:
        group-name: default
        api-rule: package
        api-rule-resources:
          - com.hmall.cart.controller

最后,把hm-service中的与购物车有关功能拷贝过来,最终的项目结构如下:

特别注意的是com.hmall.cart.service.impl.CartServiceImpl,其中有两个地方需要处理:

  • 需要获取登录用户信息,但登录校验功能目前没有复制过来,先写死固定用户id
  • 查询购物车时需要查询商品信息,而商品信息不在当前服务,需要先将这部分代码注释

我们对这部分代码做如下修改:

package com.hmall.cart.service.impl;

import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmall.cart.domain.dto.CartFormDTO;
import com.hmall.cart.domain.po.Cart;
import com.hmall.cart.domain.vo.CartVO;
import com.hmall.cart.mapper.CartMapper;
import com.hmall.cart.service.ICartService;
import com.hmall.common.exception.BizIllegalException;
import com.hmall.common.utils.BeanUtils;
import com.hmall.common.utils.CollUtils;
import com.hmall.common.utils.UserContext;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.util.Collection;
import java.util.List;

/**
 * <p>
 * 订单详情表 服务实现类
 * </p>
 *
 * @author 虎哥
 * @since 2023-05-05
 */
@Service
@RequiredArgsConstructor
public class CartServiceImpl extends ServiceImpl<CartMapper, Cart> implements ICartService {

    // private final IItemService itemService;

    @Override
    public void addItem2Cart(CartFormDTO cartFormDTO) {
        // 1.获取登录用户
        Long userId = UserContext.getUser();

        // 2.判断是否已经存在
        if (checkItemExists(cartFormDTO.getItemId(), userId)) {
            // 2.1.存在,则更新数量
            baseMapper.updateNum(cartFormDTO.getItemId(), userId);
            return;
        }
        // 2.2.不存在,判断是否超过购物车数量
        checkCartsFull(userId);

        // 3.新增购物车条目
        // 3.1.转换PO
        Cart cart = BeanUtils.copyBean(cartFormDTO, Cart.class);
        // 3.2.保存当前用户
        cart.setUserId(userId);
        // 3.3.保存到数据库
        save(cart);
    }

    @Override
    public List<CartVO> queryMyCarts() {
        // 1.查询我的购物车列表
        List<Cart> carts = lambdaQuery().eq(Cart::getUserId, 1L /*TODO UserContext.getUser()*/).list();
        if (CollUtils.isEmpty(carts)) {
            return CollUtils.emptyList();
        }
        // 2.转换VO
        List<CartVO> vos = BeanUtils.copyList(carts, CartVO.class);
        // 3.处理VO中的商品信息
        handleCartItems(vos);
        // 4.返回
        return vos;
    }

    private void handleCartItems(List<CartVO> vos) {
        // 1.获取商品id TODO 处理商品信息
        /*Set<Long> itemIds = vos.stream().map(CartVO::getItemId).collect(Collectors.toSet());
        // 2.查询商品
        List<ItemDTO> items = itemService.queryItemByIds(itemIds);
        if (CollUtils.isEmpty(items)) {
            throw new BadRequestException("购物车中商品不存在!");
        }
        // 3.转为 id 到 item的map
        Map<Long, ItemDTO> itemMap = items.stream().collect(Collectors.toMap(ItemDTO::getId, Function.identity()));
        // 4.写入vo
        for (CartVO v : vos) {
            ItemDTO item = itemMap.get(v.getItemId());
            if (item == null) {
                continue;
            }
            v.setNewPrice(item.getPrice());
            v.setStatus(item.getStatus());
            v.setStock(item.getStock());
        }*/
    }

    @Override
    public void removeByItemIds(Collection<Long> itemIds) {
        // 1.构建删除条件,userId和itemId
        QueryWrapper<Cart> queryWrapper = new QueryWrapper<Cart>();
        queryWrapper.lambda()
                .eq(Cart::getUserId, UserContext.getUser())
                .in(Cart::getItemId, itemIds);
        // 2.删除
        remove(queryWrapper);
    }

    private void checkCartsFull(Long userId) {
        Long count = lambdaQuery().eq(Cart::getUserId, userId).count();
        if (count >= 10) {
            throw new BizIllegalException(StrUtil.format("用户购物车课程不能超过{}", 10));
        }
    }

    private boolean checkItemExists(Long itemId, Long userId) {
        Long count = lambdaQuery()
                .eq(Cart::getUserId, userId)
                .eq(Cart::getItemId, itemId)
                .count();
        return count > 0;
    }
}

最后,还是要导入数据库表,在本地数据库直接执行对应的SQL文件

在数据库中会出现名为hm-cartdatabase,以及其中的cart表,代表购物车:

接下来,就可以测试了。不过在启动前,同样要配置启动项的active profilelocal

然后启动CartApplication,访问swagger文档页面:http://localhost:8082/doc.html

我们测试其中的查询我的购物车列表接口:

我们注意到,其中与商品有关的几个字段值都为空!这就是因为刚才我们注释掉了查询购物车时,查询商品信息的相关代码。

那么,我们该如何在cart-service服务中实现对item-service服务的查询呢?

服务调用

在拆分的时候,我们发现一个问题:就是购物车业务中需要查询商品信息,但商品信息查询的逻辑全部迁移到了item-service服务,导致我们无法查询。

最终结果就是查询到的购物车数据不完整,因此要想解决这个问题,我们就必须改造其中的代码,把原本本地方法调用,改造成跨微服务的远程调用(RPC,即Remote Produce Call)。

因此,现在查询购物车列表的流程变成了这样:

代码中需要变化的就是这一步:

那么问题来了:我们该如何跨服务调用,准确的说,如何在cart-service中获取item-service服务中的提供的商品数据呢?

大家思考一下,我们以前有没有实现过类似的远程查询的功能呢?

答案是肯定的,我们前端向服务端查询数据,其实就是从浏览器远程查询服务端数据。比如我们刚才通过Swagger测试商品查询接口,就是向http://localhost:8081/items这个接口发起的请求:

而这种查询就是通过http请求的方式来完成的,不仅仅可以实现远程查询,还可以实现新增、删除等各种远程请求。

假如我们在cart-service中能模拟浏览器,发送http请求到item-service,是不是就实现了跨微服务的远程调用了呢?

那么:我们该如何用Java代码发送Http的请求呢?

RestTemplate

Spring给我们提供了一个RestTemplate的API,可以方便的实现Http请求的发送。

其中提供了大量的方法,方便我们发送Http请求,例如:

可以看到常见的Get、Post、Put、Delete请求都支持,如果请求参数比较复杂,还可以使用exchange方法来构造请求。

我们在cart-service服务中定义一个配置类:

先将RestTemplate注册为一个Bean:

package com.hmall.cart.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

@Configuration
public class RemoteCallConfig {

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

远程调用

接下来,我们修改cart-service中的com.hmall.cart.service.impl.CartServiceImplhandleCartItems方法,发送http请求到item-service

可以看到,利用RestTemplate发送http请求与前端ajax发送请求非常相似,都包含四部分信息:

  • ① 请求路径
  • ② 请求方式
  • ③ 返回值类型
  • ④ 请求参数

handleCartItems方法的完整代码如下:

记得注入:

private void handleCartItems(List<CartVO> vos) {
        // TODO 1.获取商品id
        Set<Long> itemIds = vos.stream().map(CartVO::getItemId).collect(Collectors.toSet());
        // 2.查询商品
        // List<ItemDTO> items = itemService.queryItemByIds(itemIds);
        // 2.1.利用RestTemplate发起http请求,得到http的响应
        ResponseEntity<List<ItemDTO>> response = restTemplate.exchange(
                "http://localhost:8081/items?ids={ids}",
                HttpMethod.GET,
                null,
                new ParameterizedTypeReference<List<ItemDTO>>() {
                },
                Map.of("ids", CollUtil.join(itemIds, ","))
        );
        // 2.2.解析响应
        if(!response.getStatusCode().is2xxSuccessful()){
            // 查询失败,直接结束
            return;
        }
        List<ItemDTO> items = response.getBody();
        if (CollUtils.isEmpty(items)) {
            return;
        }
        // 3.转为 id 到 item的map
        Map<Long, ItemDTO> itemMap = items.stream().collect(Collectors.toMap(ItemDTO::getId, Function.identity()));
        // 4.写入vo
        for (CartVO v : vos) {
            ItemDTO item = itemMap.get(v.getItemId());
            if (item == null) {
                continue;
            }
            v.setNewPrice(item.getPrice());
            v.setStatus(item.getStatus());
            v.setStock(item.getStock());
        }
    }

好了,现在重启cart-service,再次测试查询我的购物车列表接口:

可以发现,所有商品相关数据都已经查询到了(注意: 无需填写参数, 直接发送访问)

在这个过程中,item-service提供了查询接口,cart-service利用Http请求调用该接口。因此item-service可以称为服务的提供者,而cart-service则称为服务的消费者或服务调用者。

注意: 启动的时候item-service也要启动, 因为要调用