原文为黑马程序员的飞书云文档,链接在这:原文链接
这篇文章的目标,完成下面的内容:
-
知道单体架构的特点
-
知道微服务架构的特点
-
学会拆分微服务
-
会使用Nacos实现服务治理
-
会使用OpenFeign实现远程调用
本章重点思路梳理:
使用了微服务架构后,因为每一个微服务都会有自己独立的数据库服务,所以数据产生了隔离,还有服务之间也会产生隔离
->这个时候就需要用到远程调用的工具,从一个服务去调用另一个服务
->而在实际生产中,可能一个服务有多个实例(难以管理),这个时候就要用到注册中心去管理我们的服务,各个服务会将自己的服务信息注册到注册中心中
->然后调用的时候服务会自己去注册中心中获取要调用的服务的实例列表,再用负载均衡算法从中选择一个服务实例进行调用
- 使用openfeign编写远程调用时,服务实例其实就是ip:port,然后再拼接使用SpringMVC注解编写的请求路径->这个就是完整的url地址
目录
1.1.单体架构
4.1.6 OpenFeign不依赖注册中心实现服务的远程调用
4.1.7 使用OpenFeing远程调用时为方法添加请求头
1.认识微服务
这一章我们从单体架构的优缺点来分析,看看开发大型项目采用单体架构存在哪些问题,而微服务架构又是如何解决这些问题的。
1.1.单体架构
单体架构(monolithic structure):项目中所有的功能模块都在一个工程中开发,项目部署时对所有模块一起编译、打包;项目的架构设计、开发模式都非常简单。
当项目规模较小时,这种模式上手快,部署、运维也都很方便,因此早期很多小型项目都采用这种模式。
但随着项目的业务规模越来越大,团队开发人员也不断增加,单体架构就呈现出越来越多的问题:
-
团队协作成本高:试想一下,你们团队数十个人同时协作开发同一个项目,由于所有模块都在一个项目中,不同模块的代码之间物理边界越来越模糊。最终要把功能合并到一个分支,你绝对会陷入到解决冲突的泥潭之中。
-
系统发布效率低:任何模块变更都需要发布整个系统,而系统发布过程中需要多个模块之间制约较多,需要对比各种文件,任何一处出现问题都会导致发布失败,往往一次发布需要数十分钟甚至数小时。
-
系统可用性差:单体架构的各个功能模块是作为一个服务部署的,模块相互之间会影响,一些热点功能会耗尽系统资源,导致其它服务低可用。
1.2.微服务
微服务架构,首先是服务化,也就是将单体架构中的功能模块 构建成 多个独立的项目(服务),并独立部署。同时要满足下面的一些特点:
-
单一职责:一个微服务负责一部分业务功能,并且其核心数据不依赖于其它模块。
-
团队自治:每个微服务都有自己独立的开发、测试、发布、运维人员,团队人员规模不超过10人(2张披萨能喂饱)
-
服务自治:每个微服务都独立打包部署,访问自己独立的数据库。并且要做好服务隔离,避免对其它服务产生影响
例如,黑马商城项目,我们就可以把商品、用户、购物车、交易等模块拆分,交给不同的团队去开发,并独立部署:
那么,单体架构存在的问题有没有解决呢?
-
团队协作成本高?
-
由于服务拆分,每个服务代码量大大减少,参与开发的后台人员在1~3名,协作成本大大降低
-
-
系统发布效率低?
-
每个服务都是独立部署,当有某个服务有代码变更时,只需要打包部署该服务即可
-
-
系统可用性差?
-
每个服务独立部署,并且做好服务隔离,使用自己的服务器资源,不会影响到其它服务。
-
综上所述,微服务架构解决了单体架构存在的问题,特别适合大型互联网项目的开发,因此被各大互联网公司普遍采用。大家以前可能听说过分布式架构,分布式就是服务拆分的过程,其实微服务架构正是分布式架构的一种最佳实践的方案。
当然,微服务架构虽然能解决单体架构的各种问题,但在拆分的过程中,还会面临很多其它问题。比如:
-
如果出现跨服务的业务该如何处理?
-
页面请求到底该访问哪个服务?
-
如何实现各个服务之间的服务隔离?
这些问题,我们在后续的学习中会给大家逐一解答。
1.3.SpringCloud
微服务拆分以后碰到的各种问题都有对应的解决方案和微服务组件,而SpringCloud框架可以说是目前Java领域最全面的微服务组件的集合了。
SpringCloud集成了各种微服务功能组件,并基于SpringBoot实现了这些组件的自动装配,从而提供了良好的开箱即用体验:
目前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) |
2.2.x, 2.3.x (Starting with SR5) | |
2.1.x | |
2.0.x | |
1.5.x | |
1.5.x |
因此,我们推荐使用次新版本:Spring Cloud 2021.0.x以及Spring Boot 2.7.x版本。
另外,Alibaba的微服务产品SpringCloudAlibaba目前也成为了SpringCloud组件中的一员,我们课堂中也会使用其中的部分组件。
在我们的父工程hmall中已经配置了SpringCloud以及SpringCloudAlibaba的依赖:
对应的版本:
这样,我们在后续需要使用SpringCloud或者SpringCloudAlibaba组件时,就无需单独指定版本了。
2.微服务拆分
接下来,将黑马商城这个单体项目拆分为微服务项目,并解决其中出现的各种问题。
2.1.熟悉黑马商城
首先,我们需要熟悉黑马商城项目的基本结构:
大家可以直接启动该项目,测试效果。不过,需要修改数据库连接参数,在application-local.yaml中:
hm:
db:
host: 192.168.150.101 # 修改为你自己的虚拟机IP地址
pw: 123 # 修改为docker中的MySQL密码
同时配置启动项激活的是local环境:
2.1.1.登录
首先来看一下登录业务流程:
登录入口在com.hmall.controller.UserController
中的login
方法:
2.2.2.搜索商品
在首页搜索框输入关键字,点击搜索即可进入搜索列表页面:
该页面会调用接口:/search/list
,对应的服务端入口在com.hmall.controller.SearchController
中的search
方法:
这里目前是利用数据库实现了简单的分页查询。
2.2.3.购物车
在搜索到的商品列表中,点击按钮加入购物车
,即可将商品加入购物车:
加入成功后即可进入购物车列表页,查看自己购物车商品列表:
同时这里还可以对购物车实现修改、删除等操作。
相关功能全部在com.hmall.controller.CartController
中:
其中,查询购物车列表时,由于要判断商品最新的价格和状态,所以还需要查询商品信息,业务流程如下(注意理解这个,后面变成微服务架构后,会出现服务的远程调用):
2.2.4.下单
在购物车页面点击结算
按钮,会进入订单结算页面:
点击提交订单,会提交请求到服务端,服务端做3件事情:
-
创建一个新的订单
-
扣减商品库存
-
清理购物车中商品
业务入口在com.hmall.controller.OrderController
中的createOrder
方法:
2.2.5.支付
下单完成后会跳转到支付页面,目前只支持余额支付:
在选择余额支付这种方式后,会发起请求到服务端,服务端会立刻创建一个支付流水单,并返回支付流水单号到前端。
当用户输入用户密码,然后点击确认支付时,页面会发送请求到服务端,而服务端会做几件事情:
-
校验用户密码
-
扣减余额
-
修改支付流水状态
-
修改交易订单状态
请求入口在com.hmall.controller.PayController
中:
2.2.服务拆分原则111
服务拆分一定要考虑的几个问题:
-
什么时候拆?
-
如何拆?
2.2.1.什么时候拆
创业型项目:先采用单体架构,快速开发,快速试错。随着规模扩大,逐渐拆分。
确定的大型项目:资金充足,目标明确,可以直接选择微服务架构,避免后续拆分的麻烦。
2.2.2.怎么拆(怎么构建微服务项目)
从拆分目标(构建目标)来说,要做到:
- 高内聚:每个微服务的职责要尽量单一,包含的业务相互关联度高、完整度高。
- 低耦合:每个微服务的功能要相对独立,尽量减少对其它微服务的依赖。
从拆分方式(构建方式)来说,一般包含两种方式:
- 纵向拆分(构建):按照项目的业务模块(功能模块)来拆分(构建):例如黑马商城中,有用户管理功能、订单管理功能、购物车功能、商品管理功能、支付功能等。那么按照功能模块将他们拆分为一个个服务。这种拆分(构建)模式可以尽可能提高服务的内聚性。
- 横向拆分:是看各个功能模块之间有没有公共的业务部分,如果有将其抽取出来作为通用服务,提供复用性:例如用户登录是需要发送消息通知,记录风控数据,下单时也要发送短信,记录风控数据。因此消息发送、风控数据记录就是通用的业务功能,因此可以将他们分别抽取为公共服务:消息中心服务、风控管理服务,同时通用业务一般接口稳定性较强,也不会使服务之间过分耦合。
当然,由于黑马商城并不是一个完整的项目,其中的短信发送、风控管理并没有实现,这里就不再考虑了。而其它的业务按照纵向拆分,可以分为以下几个微服务:
-
用户服务
-
商品服务
-
订单服务
-
购物车服务
-
支付服务
2.3.拆分购物车、商品服务111
接下来,先把商品管理功能、购物车功能抽取为两个独立的服务。
一般 微服务项目 有两种工程结构:
-
完全解耦:每一个微服务都创建为一个独立的工程,甚至可以使用不同的开发语言来开发,项目完全解耦。
-
优点:服务之间耦合度低
-
缺点:每个项目都有自己的独立仓库,管理起来比较麻烦
-
-
Maven聚合:整个项目为一个Project,然后每个微服务是其中的一个Module
-
优点:项目代码集中,管理和运维方便(授课也方便)
-
缺点:服务之间耦合,编译时间较长
-
这里为了学习方便,采用Maven聚合工程。
在hmall父工程之中,已经提前定义了SpringBoot、SpringCloud的依赖版本,所以为了方便期间,我们直接在这个项目中创建微服务module.
2.3.1.商品服务
在hmall中创建module:
选择maven模块,并设定JDK版本为11:
商品模块,我们起名为item-service
:
引入依赖:
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>hmall</artifactId>
<groupId>com.heima</groupId>
<version>1.0.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<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>
编写启动类:
代码如下:
package com.hmall.item;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@MapperScan("com.hmall.item.mapper")
@SpringBootApplication
public class ItemApplication {
public static void main(String[] args) {
SpringApplication.run(ItemApplication.class, args);
}
}
接下来是配置文件,可以从hm-service
中拷贝过来item-service服务中:
其中,application.yaml
内容如下:
注意:
- server.port:每个微服务用一个不同的端口号
- spring.application.name:每个微服务都需要一个名称,非常重要
- 每个微服务都要做到独立,所以每个微服务都需要独享一台数据库
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.yaml
和application-local.yaml
直接从hm-service拷贝即可。
然后拷贝hm-service
中与商品管理有关的代码到item-service
,如图:
这里有一个地方的代码需要改动,就是ItemServiceImpl
中的deductStock
方法:
改动前:
改动后:
这也是因为ItemMapper的所在包发生了变化,因此这里代码必须修改包路径。
最后,还要导入数据库表。默认的数据库连接的是虚拟机,在你docker数据库执行课前资料提供的SQL文件:
最终,会在数据库创建一个名为hm-item的database,将来的每一个微服务都会有自己的一个database:
- 注意:在企业开发的生产环境中,每一个微服务都应该有自己的 独立数据库服务 ,而不仅仅是database,在学习中使用用database来代替。
接下来,就可以启动测试了,在启动前我们要配置一下启动项,让默认激活的配置文件为local
而不是dev
:
在打开的编辑框填写active profiles
:
接着,启动item-service
,访问商品微服务的swagger接口文档:http://localhost:8081/doc.html
然后测试其中的根据id批量查询商品这个接口:
测试参数:100002672302,100002624500,100002533430,结果如下:
说明商品微服务抽取成功了。
2.3.2.购物车服务
与商品服务类似,在hmall下创建一个新的module
,起名为cart-service
:
然后是依赖:
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>hmall</artifactId>
<groupId>com.heima</groupId>
<version>1.0.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>cart-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>
然后是启动类:
package com.hmall.cart;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@MapperScan("com.hmall.cart.mapper")
@SpringBootApplication
public class CartApplication {
public static void main(String[] args) {
SpringApplication.run(CartApplication.class, args);
}
}
然后是配置文件,同样可以拷贝自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) {
int count = lambdaQuery().eq(Cart::getUserId, userId).count();
if (count >= 10) {
throw new BizIllegalException(StrUtil.format("用户购物车课程不能超过{}", 10));
}
}
private boolean checkItemExists(Long itemId, Long userId) {
int count = lambdaQuery()
.eq(Cart::getUserId, userId)
.eq(Cart::getItemId, itemId)
.count();
return count > 0;
}
}
最后,还是要导入数据库表,在本地数据库直接执行课前资料对应的SQL文件:
在数据库中会出现名为hm-cart
的database
,以及其中的cart
表,代表购物车:
接下来,就可以测试了。不过在启动前,同样要配置启动项的active profile
为local
:
然后启动CartApplication
,访问swagger文档页面:http://localhost:8082/doc.html
我们测试其中的查询我的购物车列表
接口:
无需填写参数,直接访问:
我们注意到,其中与商品有关的几个字段值都为空!这就是因为刚才我们注释掉了查询购物车时,查询商品信息的相关代码。
那么,我们该如何在cart-service
服务中实现对item-service
服务的查询呢?
2.4.服务调用
本节总结:在使用了微服务架构后,每一个微服务都会有自己独立的数据库服务,数据产生了隔离,服务之间也产生了隔离,无法像单体项目进行本地调用,所以需要在Java代码中进行远程调用(发送http请求),去访问另一个服务的接口
在拆分的时候,我们发现一个问题:就是购物车业务中需要查询商品信息,但商品信息查询的逻辑全部迁移到了item-service
服务,导致我们无法查询。
最终结果就是查询到的购物车数据不完整,因此要想解决这个问题,我们就必须改造其中的代码,把原本本地方法调用,改造成跨微服务的远程调用(RPC,即Remote Produce Call)。
微服务进行了拆分,数据产生了隔离,服务之间也产生了隔离,无法像单体项目那样本地调用,所以服务之间需要网络(远程)调用查别人的数据
服务拆分之后,一个模块需要使用到另一个模块的数据,可以向前端那样,基于http请求,去获取另一个模块的数据
因此,现在查询购物车列表的流程变成了这样:
代码中需要变化的就是这一步:
那么问题来了:我们该如何跨服务调用,准确的说,如何在cart-service
中获取item-service
服务中的提供的商品数据呢?
大家思考一下,我们以前有没有实现过类似的远程查询的功能呢?
答案是肯定的,我们前端向服务端查询数据,其实就是从浏览器远程查询服务端数据。比如我们刚才通过Swagger测试商品查询接口,就是向http://localhost:8081/items
这个接口发起的请求:
而这种查询就是通过http请求的方式来完成的,不仅仅可以实现远程查询,还可以实现新增、删除等各种远程请求。
假如我们在cart-service中能模拟浏览器,发送http请求到item-service,就实现了跨微服务的远程调用了
那么,我们该如何用Java代码发送Http的请求呢?
2.4.1.RestTemplate
Spring给我们提供了一个RestTemplate的API,可以方便的实现Http请求的发送(HttpClient也可以实现)。
先了解下面这个图,其实使用RestTemplate就这两步:
其中提供了大量的方法,方便我们发送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();
}
}
2.4.2.远程调用
接下来,修改cart-service
中的com.hmall.cart.service.impl.
CartServiceImpl
的handleCartItems
方法,发送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集合来封装请求参数,CollUtil.join方法用于拼接集合中的各个元素
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再次测试查询我的购物车列表接口:
可以发现,所有商品相关数据都已经查询到了。
在这个过程中,item-service
提供了查询接口,cart-service
利用Http请求调用该接口。因此item-service
可以称为服务的提供者,而cart-service
则称为服务的消费者或服务调用者。
2.5.总结
什么时候需要拆分微服务?
-
如果是创业型公司,最好先用单体架构快速迭代开发,验证市场运作模型,快速试错。当业务跑通以后,随着业务规模扩大、人员规模增加,再考虑拆分微服务。
-
如果是大型企业,有充足的资源,可以在项目开始之初就搭建微服务架构。
如何拆分?
-
首先要做到高内聚、低耦合
-
从拆分方式来说,有横向拆分和纵向拆分两种。纵向就是按照业务功能模块,横向则是拆分通用性业务,提高复用性
服务拆分之后,不可避免的会出现跨微服务的业务,此时微服务之间就需要进行远程调用。微服务之间的远程调用被称为RPC,即远程过程调用。RPC的实现方式有很多,比如:
-
基于Http协议
-
基于Dubbo协议
我们课堂中使用的是Http方式,这种方式不关心服务提供者的具体技术实现,只要对外暴露Http接口即可,更符合微服务的需要。
Java发送http请求可以使用Spring提供的RestTemplate,使用的基本步骤如下:
-
注册RestTemplate到Spring容器
-
调用RestTemplate的API发送请求,常见方法有:
-
getForObject:发送Get请求并返回指定类型对象
-
PostForObject:发送Post请求并返回指定类型对象
-
put:发送PUT请求
-
delete:发送Delete请求
-
exchange:发送任意类型请求,返回ResponseEntity
-
3.服务注册和发现
在上一章实现了微服务拆分,并且通过Http请求(写死的方式)实现了跨微服务的远程调用但,这种手动发送Http请求的方式还存在一些问题。
试想一下,假如商品微服务被调用较多,为了应对更高的并发,我们进行了多实例部署,如图:
此时,每个item-service
实例的IP或端口都不同,那么问题来了:
-
item-service这么多实例,cart-service如何知道每一个实例的地址?
-
http请求要写url地址,
cart-service
服务到底该调用哪个实例呢? -
如果在运行过程中,某一个
item-service
实例宕机,cart-service
依然在调用该怎么办? -
如果并发太高,
item-service
临时多部署了N台实例,cart-service
如何知道新实例的地址?
为了解决上述问题,就必须引入注册中心的概念了,接下来我们就一起来分析下注册中心的原理。
3.1.注册中心原理
在微服务远程调用的过程中,包括两个角色:
-
服务的提供者:提供接口供其它微服务访问,比如
item-service
-
服务的消费者:调用其它微服务提供的接口,比如
cart-service
注册中心原理几个重要的点:
1、服务治理中的三个角色分别是服务提供者、服务调用者、注册中心(服务提供者、服务调用者都是服务,基本都是一个服务又是提供者又是调用者)
2、服务调用者 如何知道 服务提供者的地址?在注册中心中查看
3、消费者如何获知服务状态变更:心跳请求
4、当服务提供者有多个实例时,消费者该选择哪一个:使用负载均衡的算法(随机,轮询),从多个实例从选择一个
在大型微服务项目中,服务提供者的数量会非常多,为了管理这些服务就引入了注册中心。注册中心、服务提供者、服务消费者三者间关系如下:
流程如下:
-
服务启动时,会注册自己的服务信息(服务名、IP、端口)到注册中心
-
服务调用者可以从注册中心,获取服务对应的实例列表(1个服务可能多实例部署)
-
然后调用者对实例列表负载均衡,挑选一个实例
-
调用者向该实例发起远程调用
当服务提供者的实例宕机或者启动新实例时,调用者如何得知呢?
-
服务的提供者会定期向注册中心发送请求(心跳请求),报告自己的健康状态
-
当注册中心长时间收不到服务提供者的心跳请求时,会认为该实例宕机,将其从服务的实例列表中剔除
-
当服务有新实例启动时,会发送注册服务请求,其信息会被记录在注册中心的服务实例列表
-
当注册中心服务列表变更时,会主动通知微服务,更新本地服务列表
3.2.Nacos注册中心
目前开源的注册中心框架(实现)有很多,国内比较常见的有:
-
Eureka:Netflix公司出品,目前被集成在SpringCloud当中,一般用于Java应用
-
Nacos:Alibaba公司出品,目前被集成在SpringCloudAlibaba中,一般用于Java应用
-
Consul:HashiCorp公司出品,目前集成在SpringCloud中,不限制微服务语言
以上几种注册中心都遵循SpringCloud中的API规范,因此在业务开发使用上没有太大差异。由于Nacos是国内产品,中文文档比较丰富,而且同时具备配置管理功能(后面会学习),因此在国内使用较多,所以使用Nacos为例来学习。
官方网站如下:
Redirecting to: https://nacos.io/https://nacos.io/zh-cn/https://nacos.io/zh-cn/https://nacos.io/zh-cn/https://nacos.io/zh-cn/下面来看如何基于Docker部署Nacos注册中心,首先我们要准备一些MySQL的数据库表,用来存储Nacos的数据。由于是Docker部署,所以大家需要将资料中的SQL文件导入到你Docker中的MySQL容器中:
最终表结构如下:
然后,找到课前资料下的nacos文件夹:
其中的nacos/custom.env
文件中,有一个MYSQL_SERVICE_HOST也就是mysql地址,需要修改为自己的虚拟机IP地址:
然后,将课前资料中的nacos
目录上传至虚拟机的/root
目录。
进入root目录,然后执行下面的docker命令:
docker run -d \
--name nacos \
--env-file ./nacos/custom.env \
-p 8848:8848 \
-p 9848:9848 \
-p 9849:9849 \
--restart=always \
nacos/nacos-server:v2.1.0-slim
启动完成后,访问下面地址:http://192.168.150.101:8848/nacos/,注意将192.168.150.101
替换为你自己的虚拟机IP地址。
首次访问会跳转到登录页,账号密码都是nacos
3.3.服务注册
本节总结:服务注册并不需要我们做什么,只是需要引入Nacos的依赖和配置下注册中心的地址在application.yml中,服务启动的时候就去注册到注册中心。
将item-service服务,
注册到Nacos,步骤如下:
-
引入依赖
-
配置Nacos地址
-
重启
3.3.1.添加依赖
在item-service
的pom.xml
中添加依赖:
- 下面是nacos的依赖,包含服务的注册和发现功能
<!--nacos 服务注册发现-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
3.3.2.配置Nacos
在item-service服务
的application.yml
中添加nacos的地址:
spring:
application:
name: item-service # 服务名称
cloud:
nacos:
server-addr: 192.168.150.101:8848 # nacos地址
3.3.3.启动服务实例
为了测试一个服务多个实例的情况,我们再配置一个item-service
的部署实例:
然后配置启动项,注意重命名并且配置新的端口,避免冲突:
重启item-service
的两个实例:
访问nacos控制台,可以发现服务注册成功:
点击详情,可以查看到item-service
服务的两个实例信息:
3.4.服务发现
本节总结:服务发现其实就是引入注册中心和负载均衡的依赖后,再配置注册中心的地址,然后调用者就可以 根据服务名 去 注册中心 获取服务的实例列表,再利用负载均衡算法从中挑选一个服务即可。
服务的调用者去nacos订阅服务,这个过程就是服务发现,步骤如下:
-
引入依赖
-
配置Nacos地址
-
发现并调用服务
调用者可以从注册中心订阅想要的服务,获取服务对应的实例列表(1个服务可能多实例部署)
3.4.1.引入依赖
服务发现除了要引入nacos依赖,还需要引入负载均衡的依赖(因为另一个服务可能有多个实例,所以要用负载均衡去选择一个实例就可以了),因此要引入SpringCloud提供的LoadBalancer依赖。
我们在cart-service
中的pom.xml
中添加下面的依赖:
<!--nacos 服务注册发现-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
这里Nacos的依赖与服务注册时一致,这个依赖中同时包含了服务注册和发现的功能。因为任何一个微服务都可以调用别人,也可以被别人调用,即可以是调用者,也可以是提供者。
因此,等一会儿cart-service
启动,同样会注册到Nacos
3.4.2.配置Nacos地址
在cart-service
的application.yml
中添加nacos地址配置:
spring:
cloud:
nacos:
server-addr: 192.168.150.101:8848
3.4.3.发现并调用服务
接下来,服务调用者cart-service
就可以去订阅item-service
服务了。不过item-service有多个实例,而真正发起调用时只需要知道一个实例的地址。
因此,服务调用者必须利用负载均衡的算法,从多个实例中挑选一个去访问。常见的负载均衡算法有:
-
随机
-
轮询
-
IP的hash
-
最近最少访问
-
...
这里我们可以选择最简单的随机负载均衡。
另外,服务发现需要用到一个工具,DiscoveryClient,SpringCloud已经帮我们自动装配,我们可以直接注入使用:
接下来,我们就可以对原来的远程调用做修改了,之前调用时我们需要写死服务提供者的IP和端口:
但现在不需要了,我们通过DiscoveryClient发现服务的实例列表,然后通过负载均衡算法,选择一个实例去调用:
- 2.1 根据服务名获取服务的实例列表
- 2.2 利用负载均衡从服务列表中获取一个实例
经过swagger测试,发现没有任何问题。
4.OpenFeign
在上一章,我们利用Nacos实现了服务治理,利用RestTemplate实现了服务的远程调用。但是远程调用的代码太复杂了,所以我们要使用OpenFeign去简化远程调用:
OpenFeign是一个声明式的http客户端,用于在Java代码中发送http请求,它基于SpringMVC的常见注解,优雅的发送http请求。
远程调用的关键点就在于五个:
- 请求的服务
- 请求方式
-
请求路径
-
需要发送的请求参数
-
返回值类型
所以,OpenFeign就利用SpringMVC的相关注解来声明上述4个参数,然后基于动态代理帮我们生成远程调用的代码,而无需我们手动再编写,非常方便。
4.1.快速入门
我们还是以cart-service中的查询我的购物车为例。因此下面的操作都是在cart-service中进行。
4.1.1.引入依赖
在cart-service
服务的pom.xml中引入OpenFeign
的依赖和loadBalancer
依赖:
<!--openFeign-->
<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>
4.1.2.开启OpenFeign功能
接下来,在cart-service
的CartApplication
启动类上添加@EnableFeignClients注解,启动OpenFeign的功能:
4.1.3.编写OpenFeign客户端
在cart-service
中,定义一个Feign客户端接口:
其中代码如下:
package com.hmall.cart.client;
import com.hmall.cart.domain.dto.ItemDTO;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import java.util.List;
@FeignClient("item-service")
public interface ItemClient {
@GetMapping("/items")
List<ItemDTO> queryItemByIds(@RequestParam("ids") Collection<Long> ids);
}
只用定义接口,无需实现,OpenFeign会利用动态代理帮我们实现这个接口,接口中的几个关键信息:
-
@FeignClient("item-service")
:告诉OpenFeign这是Feign客户端,同时用value属性指定要获取哪个服务的实例列表 -
@GetMapping
:声明请求方式 -
@GetMapping("/items")
:声明请求路径 -
@RequestParam("ids") Collection<Long> ids
:声明需要发送的请求参数 -
List<ItemDTO>
:返回值类型(自动将响应结果的JSON格式 转为 此Java对象)
其实就是声明向哪个服务发送什么类型的请求,请求路径是什么,并通过形参封装请求参数,声明返回值类型封装响应
有了上述信息,OpenFeign就可以利用动态代理实现这个方法,并且向http://item-service/items
发送一个GET
请求,携带ids为请求参数,并自动将返回值处理为List<ItemDTO>
。
4.1.4.使用FeignClient
最后在cart-service服务
的com.hmall.cart.service.impl.CartServiceImpl
中改造代码,直接调用ItemClient中
的方法:
总结:feign替我们完成了服务拉取、负载均衡、发送http请求的所有工作,现在优雅多了。而且,这里我们不再需要RestTemplate了,还省去了RestTemplate的注册。
4.1.5 OpenFeign动态生成的代理类
OpenFeign会利用动态代理编写的代理类做下图的事情:
4.1.6 OpenFeign不依赖注册中心实现服务的远程调用
给@FeignClient注解添加url属性:
@FeignClient(name = "formflowprovider", url = "http://ip:port/formflowprovider")
通常情况下,@FeignClient 注解中会使用 name 属性来指定服务的名称,然后通过服务发现机制(如 Nacos、Consul)来发现远程服务的位置。
url 属性:直接指定调用服务的基础URL。
- 指定了 url 属性后,将不再依赖于服务发现机制
在上述代码中,url 属性指定了 formflowprovider 服务的基础 URL 地址为 http://ip:port/formflowprovider。这意味着在使用 @FeignClient 调用远程服务时,会使用这个指定的 URL 地址作为远程服务的基础地址。
4.1.7 使用OpenFeing远程调用时为方法添加请求头
4.2.连接池
Feign底层发起http请求,依赖于其它的框架。其底层支持的http客户端实现包括:
-
HttpURLConnection:默认实现,不支持连接池
-
Apache HttpClient :支持连接池
-
OKHttp:支持连接池
因此我们通常会使用带有连接池的客户端来代替默认的HttpURLConnection。比如,我们使用OK Http,因为Feign默认的http客户端不支持连接池,所以使用这个带连接池的来加快性能
4.2.1.引入依赖
在cart-service
的pom.xml
中引入依赖:
<!--OK http 的依赖 -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-okhttp</artifactId>
</dependency>
4.2.2.开启连接池
在cart-service服务
的application.yml
配置文件中开启Feign的连接池功能:
feign:
okhttp:
enabled: true # 开启OKHttp功能
重启服务,连接池就生效了。
4.2.3.验证
我们可以打断点验证连接池是否生效,在org.springframework.cloud.openfeign.loadbalancer.FeignBlockingLoadBalancerClient
中的execute
方法中打断点:
Debug方式启动cart-service,请求一次查询我的购物车方法,进入断点:
可以发现这里底层的实现已经改为OkHttpClient
4.3.OpenFeign的最佳实践(重要)
交易微服务(trade-service
),它也需要远程调用item-service
中的根据id批量查询商品功能。这个需求与cart-service
中是一样的。
因此,我们就需要在trade-service
中再次定义ItemClient
接口,这不是重复编码吗? 有什么办法能加避免重复编码呢?
4.3.1.思路分析
避免重复编码的办法就是抽取。不过这里有两种抽取思路:
-
思路1:将Feign调用抽取到微服务之外的一个公共module中。
-
抽取Feign客户端,抽取到微服务之外的一个公共的module中,不同模块都可以调用这个hm-api中的接口。
-
-
思路2:每个微服务自己抽取一个module
如图:
方案1抽取更加简单,工程结构也比较清晰,但缺点是整个项目耦合度偏高。
方案2抽取相对麻烦,工程结构相对更复杂,但服务之间耦合度降低。
由于item-service已经创建好,无法继续拆分,因此这里我们采用方案1.
4.3.2.抽取Feign客户端
在hmall
下定义一个新的module,命名为hm-api
其依赖如下:
- 这里为什么还需要引入负载均衡的依赖,因为每个服务的可能有多个实例,而只要我们要远程调用某个服务,就必须从多个服务实例中选择一个,所以也要引入这个依赖
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>hmall</artifactId>
<groupId>com.heima</groupId>
<version>1.0.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>hm-api</artifactId>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
<dependencies>
<!--open feign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- load balancer-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<!-- swagger 注解依赖 -->
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-annotations</artifactId>
<version>1.6.6</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>
然后把ItemDTO和ItemClient都拷贝过来(需要的DTO和FeignClient都可以抽取到这里面),最终结构如下:
现在,任何微服务要调用item-service
中的接口,只需要引入hm-api
模块依赖即可,无需自己编写Feign客户端了。
4.3.3.扫描包
接下来,我们在cart-service(任意一个微服务)
的pom.xml
中引入hm-api
模块:
<!--feign模块-->
<dependency>
<groupId>com.heima</groupId>
<artifactId>hm-api</artifactId>
<version>1.0.0</version>
</dependency>
删除cart-service
中原来的ItemDTO和ItemClient,重启项目,发现报错了:
这里因为ItemClient
现在定义到了com.hmall.api.client
包下,而cart-service的启动类定义在com.hmall.cart
包下,扫描不到ItemClient
,所以报错了。
解决办法很简单,在cart-service的启动类上添加声明即可,两种方式:
-
方式1:声明扫描包:
-
方式2:声明要用的FeignClient
4.4.日志配置
OpenFeign只会在FeignClient所在包的日志级别为DEBUG时,才会输出日志。而且其日志级别有4级:
-
NONE:不记录任何日志信息,这是默认值。
-
BASIC:仅记录请求的方法,URL以及响应状态码和执行时间
-
HEADERS:在BASIC的基础上,额外记录了请求和响应的头信息
-
FULL:记录所有请求和响应的明细,包括头信息、请求体、元数据。
Feign默认的日志级别就是NONE,所以默认我们看不到请求日志。
一般也不开启feign的日志配置,只有在需要调试feign的时候才开启日志,因为日志输出的内容有很多,输出时会影响性能。
4.4.1.定义日志级别
在hm-api模块下新建一个配置类,定义Feign的日志级别:
代码如下:
package com.hmall.api.config;
import feign.Logger;
import org.springframework.context.annotation.Bean;
public class DefaultFeignConfig {
@Bean
public Logger.Level feignLogLevel(){
return Logger.Level.FULL;
}
}
4.4.2.配置
接下来,要让日志级别生效,还需要配置这个类。有两种方式:
-
局部生效:在某个
FeignClient
中配置,只对当前FeignClient
生效
@FeignClient(value = "item-service", configuration = DefaultFeignConfig.class)
-
全局生效:在
@EnableFeignClients
中配置,针对所有FeignClient
生效。
@EnableFeignClients(defaultConfiguration = DefaultFeignConfig.class)
日志格式:
17:35:32:148 DEBUG 18620 --- [nio-8082-exec-1] com.hmall.api.client.ItemClient : [ItemClient#queryItemByIds] ---> GET http://item-service/items?ids=100000006163 HTTP/1.1
17:35:32:148 DEBUG 18620 --- [nio-8082-exec-1] com.hmall.api.client.ItemClient : [ItemClient#queryItemByIds] ---> END HTTP (0-byte body)
17:35:32:278 DEBUG 18620 --- [nio-8082-exec-1] com.hmall.api.client.ItemClient : [ItemClient#queryItemByIds] <--- HTTP/1.1 200 (127ms)
17:35:32:279 DEBUG 18620 --- [nio-8082-exec-1] com.hmall.api.client.ItemClient : [ItemClient#queryItemByIds] connection: keep-alive
17:35:32:279 DEBUG 18620 --- [nio-8082-exec-1] com.hmall.api.client.ItemClient : [ItemClient#queryItemByIds] content-type: application/json
17:35:32:279 DEBUG 18620 --- [nio-8082-exec-1] com.hmall.api.client.ItemClient : [ItemClient#queryItemByIds] date: Fri, 26 May 2023 09:35:32 GMT
17:35:32:279 DEBUG 18620 --- [nio-8082-exec-1] com.hmall.api.client.ItemClient : [ItemClient#queryItemByIds] keep-alive: timeout=60
17:35:32:279 DEBUG 18620 --- [nio-8082-exec-1] com.hmall.api.client.ItemClient : [ItemClient#queryItemByIds] transfer-encoding: chunked
17:35:32:279 DEBUG 18620 --- [nio-8082-exec-1] com.hmall.api.client.ItemClient : [ItemClient#queryItemByIds]
17:35:32:280 DEBUG 18620 --- [nio-8082-exec-1] com.hmall.api.client.ItemClient : [ItemClient#queryItemByIds] [{"id":100000006163,"name":"巴布豆(BOBDOG)柔薄悦动婴儿拉拉裤XXL码80片(15kg以上)","price":67100,"stock":10000,"image":"https://m.360buyimg.com/mobilecms/s720x720_jfs/t23998/350/2363990466/222391/a6e9581d/5b7cba5bN0c18fb4f.jpg!q70.jpg.webp","category":"拉拉裤","brand":"巴布豆","spec":"{}","sold":11,"commentCount":33343434,"isAD":false,"status":2}]
17:35:32:281 DEBUG 18620 --- [nio-8082-exec-1] com.hmall.api.client.ItemClient : [ItemClient#queryItemByIds] <--- END HTTP (369-byte body)