基于黑马教程——微服务架构解析(一)
本篇文章基于黑马程序员的微服务课程内容,结合个人学习过程中的理解与思考进行整理。本节将围绕以下几个问题展开:什么是微服务?如何将一个单体项目拆分为微服务架构?以及微服务之间是如何进行协同与关联的?
1.认识微服务
首先我们对单体架构的优缺点进行分析,看看开发大型项目采用单体架构存在哪些问题,而微服务架构又是如何解决这些问题的。
1.1 单体项目
我们将从单体项目的架构入手,探讨其在实际开发过程中的局限性,并进一步说明为何需要引入微服务架构来应对这些问题。
但随着项目的业务规模越来越大,团队开发人员也不断增加,单体架构就呈现出越来越多的问题:
- 团队协作成本高:试想一下,你们团队数十个人同时协作开发同一个项目,由于所有模块都在一个项目中,不同模块的代码之间物理边界越来越模糊。最终要把功能合并到一个分支,你绝对会陷入到解决冲突的泥潭之中。
- 系统发布效率低:任何模块变更都需要发布整个系统,而系统发布过程中需要多个模块之间制约较多,需要对比各种文件,任何一处出现问题都会导致发布失败,往往一次发布需要数十分钟甚至数小时。
- 系统可用性差:单体架构各个功能模块是作为一个服务部署,相互之间会互相影响,一些热点功能会耗尽系统资源,导致其它服务低可用。
1.2 微服务
微服务架构,首先是服务化,就是将单体架构中的功能模块从单体应用中拆分出来,独立部署为多个服务。同时要满足下面的一些特点:
- 单一职责:一个微服务负责一部分业务功能,并且其核心数据不依赖于其它模块。
- 团队自治:每个微服务都有自己独立的开发、测试、发布、运维人员,团队人员规模不超过10人(2张披萨能喂饱)
- 服务自治:每个微服务都独立打包部署,访问自己独立的数据库。并且要做好服务隔离,避免对其它服务产生影响
例如,黑马商城项目,我们就可以把商品、用户、购物车、交易等模块拆分,交给不同的团队去开发,并独立部署:
综上所述,微服务架构解决了单体架构存在的问题,特别适合大型互联网项目的开发,因此被各大互联网公司普遍采用。大家以前可能听说过分布式架构,分布式就是服务拆分的过程,其实微服务架构正式分布式架构的一种最佳实践的方案
当然,微服务架构虽然能解决单体架构的各种问题,但在拆分的过程中,还会面临很多其它问题。比如:
- 如果出现跨服务的业务该如何处理?
- 页面请求到底该访问哪个服务?
- 如何实现各个服务之间的服务隔离?
1.3 SpringCloud
将服务进行拆分以后将会碰到很多问题,当然这些问题都有相应的解决方法和组件。SPringCloud就是集成目前Java领域最全面微服务组件的框架。
2.服务的拆分
这里分服务拆分是以黑马商城为例:
2.1 服务拆分的原则
在拆分为微服务架构时,通常遵循以下原则:
- 按业务功能划分服务(面向领域):每个服务聚焦于一个明确的业务边界;
- 高内聚、低耦合:服务内部功能紧密相关,服务之间通过接口通信;
- 独立部署、独立数据库:每个服务可独立升级,数据相互隔离;
- 服务自治:服务内部业务逻辑不依赖其他服务的内部实
2.2 拆分一个商品服务和购物车服务
2.2.1 商品服务
主要流程为:
- 创建子模块(最好是Maven空模块),命名为item-service
- 引入该服务所需要的依赖
- 编写一个基本SpringBoot文件架构(SSM架构、一些工具类和配置类等)和编写启动类
- 构建配置文件,数据库、swagger配置等
- 然后一些细节的全限定类名之类细节需要根据新项目文件进行修改
2.2.2 购物车
也是同理,如果需要看详细教程看黑马的笔记,这篇文章主要是回顾和复习
3.服务的调用
在之前拆分的时候,我们发现购物车的服务需要去调用商品信息的服务。那我怎么去实现服务之间的调用。这里可以使用RestTemplate。
3.1 RestTemplate
RestTemplate
是 Spring 框架提供的一个用于 发起 HTTP 请求、访问 RESTful 服务 的客户端工具。它封装了底层的 HttpClient
,让我们可以以更简洁的方式实现服务间的通信,尤其适用于微服务架构中的 服务调用。
在传统的前后端分离项目中,前端通常通过 HTTP 请求与后端进行数据交互;同样,后端之间也可以通过 HTTP 接口实现服务间的通信。
3.2 案例
我们将以 cart-service
中的 com.hmall.cart.service.impl.CartServiceImpl
的 handleCartItems
方法为例,向 item-service
发送 HTTP 请求获取商品详情。
可以看到,使用 RestTemplate 发起服务请求的方式与前端 AJAX 请求类似,都包含以下四个核心要素:
- 请求方式(GET / POST / PUT / DELETE)
- 请求路径(URL 或服务名 + 路径)
- 请求参数(URL 参数 / 请求体)
- 返回值类型(需要反序列化成的 Java 类型)
@Service
public class CartServiceImpl implements CartService {@Autowiredprivate RestTemplate restTemplate;@Overridepublic void handleCartItems(Long itemId) {// ① 请求方式:GET// ② 请求路径:调用 item-service 获取商品信息String url = "http://item-service/items/" + itemId;// ③ 请求参数:itemId 作为路径参数传递// ④ 返回值类型:Item 类对象(假设定义了 Item 实体类)Item item = restTemplate.getForObject(url, Item.class);// 使用返回的数据处理购物车逻辑if (item != null) {// ... 处理 item 信息System.out.println("获取商品信息成功:" + item.getName());}}
}
4.服务注册和发现
在前面的开发过程中,我们发现在服务之间进行调用时,每次都需要手动指定服务地址和端口号。比如:
String url = "http://localhost:8081/items/1";
这种方式存在明显的缺点:
- 不利于维护:每次服务地址变更,都需要修改代码;
- 无法实现负载均衡:无法同时调用多个实例;
- 不具备服务自动发现能力:服务上线或下线,系统无法感知。
为了解决这些问题,引入了 服务注册与发现机制。这就是我们为什么要使用 Nacos。
5. OpenFeign —— 更优雅的服务远程调用方式
在上一章节中,我们通过 Nacos 实现了服务的注册与发现,使用 RestTemplate 实现了服务间的远程调用。但我们也发现,RestTemplate 的调用方式存在以下问题:
- 代码冗长,需要手动拼接 URL、设置参数、解析结果;
- 调用方式与本地方法差异较大,影响编码体验;
- 缺乏可复用性,重复代码多;
- 不支持声明式、统一式的调用风格。
为了解决这些问题,我们引入了 OpenFeign —— 一种基于接口的声明式 HTTP 客户端。
5.1 核心原理
远程调用的本质无非是四个核心要素:
- 请求方式(GET / POST)
- 请求路径(URL)
- 请求参数(如路径参数、查询参数等)
- 返回值类型(响应数据结构)
OpenFeign 正是基于这些要素,通过 SpringMVC 注解(如 @GetMapping
、@RequestParam
等)来声明调用规则,再结合 动态代理 自动生成 HTTP 请求代码,帮助我们完成远程调用。
5.2 快速入门示例:在 cart-service 中使用 Feign 调用 item-service
5.2.1 引入依赖
在 cart-service
的 pom.xml
中添加如下依赖:
<!-- 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>
5.2.2 启用 OpenFeign 功能
在 cart-service
的启动类 CartApplication
上添加注解:
@EnableFeignClients
@SpringBootApplication
public class CartApplication {public static void main(String[] args) {SpringApplication.run(CartApplication.class, args);}
}
5.2.3 定义 Feign 客户端接口
在 cart-service
中创建接口 ItemClient
,用于远程调用 item-service
:
@FeignClient("item-service") // 指定服务名
public interface ItemClient {@GetMapping("/items")
List<ItemDTO> queryItemByIds(@RequestParam("ids") Collection<Long> ids);
}
@FeignClient("item-service")
:指定目标服务的服务名;@GetMapping("/items")
:请求方式和路径;@RequestParam("ids")
:请求参数;List<ItemDTO>
:返回值类型。
无需写实现类,Feign 会基于接口定义自动生成远程调用逻辑。
5.2.4 在业务中使用 Feign 调用
在 CartServiceImpl
中注入并使用 ItemClient
:
@Service
public class CartServiceImpl implements CartService {@Autowiredprivate ItemClient itemClient;@Overridepublic void handleCartItems(List<Long> itemIds) {List<ItemDTO> items = itemClient.queryItemByIds(itemIds);// 后续处理...}
}
是不是比 RestTemplate 优雅很多?这就是 Feign 的魅力!
5.3 使用连接池优化 Feign 性能
Feign 默认使用 HttpURLConnection
发送请求,它不支持连接池。为了提升性能,我们推荐使用支持连接池的客户端,如 OkHttp
或 Apache HttpClient
。
5.3.1 引入 OkHttp 依赖
<dependency><groupId>io.github.openfeign</groupId><artifactId>feign-okhttp</artifactId>
</dependency>
5.3.2 启用 OkHttp
在 application.yml
中添加配置:
feign:okhttp:enabled: true # 开启 OkHttp 支持
5.3.3 抽取公共 Feign 客户端模块(避免重复编码)
假设 trade-service
也需要调用 item-service
的接口。如果每个服务都自己定义 ItemClient
,将导致大量重复代码。
💡 解决方案:抽取通用模块 hm-api
① 创建一个新模块:hm-api
结构如下:
hm-api/
├── dto/ItemDTO.java
└── client/ItemClient.java
pom.xml
:
<artifactId>hm-api</artifactId>
<dependencies><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-loadbalancer</artifactId></dependency>
</dependencies>
② 其他服务引入依赖
在 cart-service
或 trade-service
的 pom.xml
中引入:
xml复制编辑<dependency><groupId>com.heima</groupId><artifactId>hm-api</artifactId><version>1.0.0</version>
</dependency>
5.3.4 扫描配置(解决找不到 FeignClient 问题)
由于 ItemClient
移到了 hm-api
,如果不在当前包路径下,需要通过以下方式之一显式声明扫描路径:
方式一:配置扫描包
@EnableFeignClients(basePackages = "com.hmall.api.client")
方式二:指定具体 FeignClient 类
@EnableFeignClients(clients = ItemClient.class)
5.4 Feign 日志配置
Feign 提供了四种日志级别:
NONE
(默认):不输出任何日志BASIC
:记录请求方法、URL、响应状态、耗时HEADERS
:记录请求和响应头信息FULL
:记录所有内容,包括请求体和响应体
5.4.1 配置日志等级
在 hm-api
中创建配置类:
@Configuration
public class DefaultFeignConfig {@Beanpublic Logger.Level feignLogLevel() {return Logger.Level.FULL;}
}
5.4.2 应用配置
方式一:局部生效
@FeignClient(name = "item-service", configuration = DefaultFeignConfig.class)
方式二:全局生效
@EnableFeignClients(defaultConfiguration = DefaultFeignConfig.class)