微服务-黑马商城笔记

搭配课程说明 – 飞书云文档食用。

0 基本概念

0.1 对比单体架构

与单体架构对应的是微服务架构。以下是两者的对比:

单体架构

  • 优点
    • 开发简单
    • 部署方便
    • 测试容易
  • 缺点
    • 可维护性差
    • 扩展性受限
    • 技术栈受限

微服务架构

  • 优点
    • 高可扩展性:每个微服务都可以独立进行扩展,可以根据不同服务的负载情况,灵活地分配资源,提高资源利用率。
    • 可维护性好:微服务将一个大型应用拆分成多个小型的、独立的服务,每个服务的代码量相对较少,结构相对简单,维护难度降低。开发人员可以专注于自己负责的服务,提高开发效率。
    • 技术多样性:不同的微服务可以根据自身的需求选择最合适的技术栈,提高了技术选型的灵活性。例如,对于数据处理密集型的服务,可以选择使用 Python;对于高并发的服务,可以选择使用 Go。
    • 容错性强:当某个微服务出现故障时,不会影响其他微服务的正常运行,提高了整个系统的容错能力。通过熔断、限流等机制,可以进一步增强系统的稳定性。
  • 缺点
    • 运维复杂度高:微服务架构中包含多个独立的服务,需要对每个服务进行独立的部署、监控和维护,增加了运维的复杂度和成本。
    • 服务间通信复杂:微服务之间需要通过网络进行通信,增加了通信的延迟和复杂性。同时,还需要处理服务间的调用关系、数据一致性等问题。
    • 分布式系统问题:微服务架构是一种分布式系统,会面临分布式系统的各种问题,如网络分区、数据一致性、事务处理等,增加了系统的开发和维护难度。

0.2 springcloud

集成了各种微服务功能组件、并基于Springboot实现组件的自动装配的微服务框架。制定了一系列组件标准。

依赖管理:

Spring Cloud BOM 是官方提供的依赖版本管理清单,预先定义了一套兼容的组件版本组合。在 Maven 的 dependencyManagement 中引入 Spring Cloud BOM。引入后,开发者添加具体组件依赖时无需指定版本,由 BOM 自动匹配兼容版本,简化配置并避免版本冲突,是推荐的管理方式。

一、微服务:

黑马商城架构:

  • 用户模块
  • 商品模块
  • 购物车模块
  • 订单模块
  • 支付模块

1.服务拆分原则:

  • 从拆分目标角度:
    • 高内聚:每个微服务的职责要尽量单一,包含的业务相互关联度高、完整度高。
    • 耦合:每个微服务的功能要相对独立,尽量减少对其它微服务的依赖,或者依赖接口的稳定性要强。
  • 从拆分方式:
    • 纵向拆分:按照业务模块
    • 横向拆分:抽取公共服务,提高复用性

2.拆分服务:

两种工程结构:

  • 独立project:每个业务模块就是一个单独的projext
  • maven聚合:整个项目还是一个project,而每个业务模块是一个Module。每一个module就是一个微服务。

本项目采用第二种方式。

2.1 拆分商品服务和购物车服务:

创建一个module,按需拷贝pom依赖配置文件、三个application配置文件、domain、mapper、service、controller,并且修改需要对应位item-server的部分。(纯体力活…)

2.2 远程调用:

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

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

Spring的RestTemplate工具可以发送http请求。
例:

// 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();

但是这种方式对于微服务存在以下问题:

  1. 服务地址发现问题:cart-service 如何动态获取 item-service 所有实例的网络地址(IP / 端口)
  2. 服务选择问题:在多个 item-service 实例中,cart-service 应如何选择具体调用哪个实例
  3. 服务健康检测与故障处理问题:如何感知 item-service 实例的宕机状态并避免调用故障实例
  4. 服务动态扩缩容感知问题:如何自动发现新部署的 item-service 实例或移除已下线的实例

因此,实际不使用RestTemplate,而是使用OpenFeign 这种 HTTP 客户端配合注册中心使用。

3 服务治理:

服务治理是在分布式或微服务架构中,对服务的全生命周期(从创建、运行到维护)进行管理和调控的机制。核心是通过一系列技术(如服务注册发现、负载均衡、监控追踪等)解决服务间交互的复杂性,确保服务稳定、高效、安全地运行,比如避免某一服务过载,或在故障时快速隔离影响。

3.1 注册中心:

在微服务远程调用的过程中,包括两个角色:

  • 服务提供者:提供接口供其它微服务访问,比如item-service
  • 服务消费者:调用其它微服务提供的接口,比如cart-service

在大型微服务项目中,服务提供者的数量会非常多,为了管理这些服务就引入了注册中心的概念。注册中心、服务提供者、服务消费者三者间关系如下:

3.2 Nacos:

Nacos:Alibaba公司出品,目前被集成在SpringCloudAlibaba中,一般用于Java应用

通过docker使用该组件,注意要和mysql在同一网络内。

3.3 服务注册:

item-servicepom.xml中添加依赖:

<!--nacos 服务注册发现-->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

item-serviceapplication.yml中添加nacos地址配置:

spring:
  application:
    name: item-service # 服务名称
  cloud:
    nacos:
      server-addr: 192.168.232.128:8848 # nacos地址

原理:微服务启动时,通过客户端向注册中心(如 Nacos)主动发送包含服务名、IP、端口等信息的注册请求,注册中心将这些信息存入注册表形成 “服务目录”;随后服务定期向注册中心发送心跳请求以维持存活状态,若注册中心未在规定时间内收到心跳,会将该服务实例标记为不可用或移除,确保注册表实时反映服务健康状态,从而为其他服务的发现与调用提供可靠的基础信息。

3.4.服务发现

服务的消费者要去nacos订阅服务,这个过程就是服务发现,步骤如下:

  • 引入依赖
  • 配置Nacos地址
  • 发现并调用服务

示例:

// 2.查询商品
//2.1根据服务名称获取服务的实例列表
List<ServiceInstance> instances = discoveryClient.getInstances("item-service");
if (CollUtils.isEmpty(instances)){
return;
}
//2.2手写负载均衡,挑出一个实例
int index = RandomUtil.randomInt(instances.size());
//log.info("使用第{}个实例", index);
ServiceInstance itemService = instances.get(index);

// 2.3.利用RestTemplate发起http请求,得到http的响应
ResponseEntity<List<ItemDTO>> response = restTemplate.exchange(
itemService.getUri()+"/items?ids={ids}",
HttpMethod.GET,
null,
new ParameterizedTypeReference<List<ItemDTO>>() {
},
Map.of("ids", CollUtil.join(itemIds, ","))
);
// 2.4.解析响应
if(!response.getStatusCode().is2xxSuccessful()){
// 查询失败,直接结束
return;
}
List<ItemDTO> items = response.getBody();

4 OpenFeign

OpenFeign 是对 HTTP 客户端的封装,通过声明式编程的方式,屏蔽了底层服务调用的细节(如 URL 拼接、参数处理、负载均衡等),让开发者像调用本地方法一样调用远程服务,大幅简化了微服务间的通信代码,提高了开发效率。

4.1 快速入门:

4.1.1依赖:

  <!--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>

在启动类上加注解启动:

@EnableFeignClients

4.1.2 编写OpenFeign客户端

注意是接口:

@FeignClient("item-service")
public interface ItemClient {

    @GetMapping("/items")
    List<ItemDTO> queryItemsByIds(@RequestParam("ids") Collection<Long> ids);
}
  • @FeignClient("item-service") :声明服务名称
  • @GetMapping :声明请求方式
  • @GetMapping("/items") :声明请求路径
  • @RequestParam("ids") Collection<Long> ids :声明请求参数
  • List<ItemDTO> :返回值类型

上述的繁杂的获取过程就可写为:

List<ItemDTO> items = itemClient.queryItemsByIds(itemIds);

4.2 连接池:

因此我们通常会使用带有连接池的客户端来代替默认的HttpURLConnection。比如,我们使用OK Http.

通过使用连接池来减少创建、销毁连接的开销、提高性能。

4.2.1 引入OKHttp依赖:

<!--OK http 的依赖 -->
<dependency>
  <groupId>io.github.openfeign</groupId>
  <artifactId>feign-okhttp</artifactId>
</dependency>

4.2.2 配置开启连接池功能:

feign:
  okhttp:
    enabled: true # 开启OKHttp功能

4.3 最佳实践:

将通用功能提取出来,有两种思路:

  • 思路1:抽取到微服务之外的公共module
  • 思路2:每个微服务自己抽取一个module

方案1抽取更加简单,工程结构也比较清晰,但缺点是整个项目耦合度偏高。
方案2抽取相对麻烦,工程结构相对更复杂,但服务之间耦合度降低。

由于item-service已经创建好,无法继续拆分,因此这里我们采用方案1.

建立module:hm-api。

引入共用部分代码。

这里因为ItemClient现在定义到了com.hmall.api.client包下,而cart-service的启动类定义在com.hmall.cart包下,扫描不到ItemClient,所以报错了。

解决办法很简单,在cart-service的启动类上添加声明即可,两种方式:

  • 方式1:声明扫描包:
@EnableFeignClients(basePackages = "com.hmall.api.client")
@MapperScan("com.hmall.cart.mapper")
@SpringBootApplication
public class CartApplication {
    public static void main(String[] args) {
        SpringApplication.run(CartApplication.class, args);
    }
}

方式2:声明要用的FeignClient

4.4.日志配置

OpenFeign只会在FeignClient所在包的日志级别为DEBUG时,才会输出日志。而且其日志级别有4级:

  • NONE:不记录任何日志信息,这是默认值。
  • BASIC:仅记录请求的方法,URL以及响应状态码和执行时间
  • HEADERS:在BASIC的基础上,额外记录了请求和响应的头信息
  • FULL:记录所有请求和响应的明细,包括头信息、请求体、元数据。

Feign默认的日志级别就是NONE,所以默认我们看不到请求日志。

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)

在配置文件里配置:

feign:
  client:
    config:
      default:  # 全局配置
        logger-level: full  # 等效于Logger.Level.FULL

5 网关路由:


微服务网关是微服务架构的统一入口,负责请求路由、负载均衡、协议转换,集中处理认证授权、限流熔断,还能实现日志监控、缓存等功能,简化客户端调用,保障系统高效安全运行。

现多用SpringCloudGateway。

网关也是一个单独的微服务

5.1 基础

同样的,创建为module,需要启动类。配置:

server:
port: 8080
spring:
application:
name: gateway
cloud:
nacos:
server-addr: 192.168.232.128:8848
gateway:
routes:
- id: item # 路由规则id,自定义,唯一
uri: lb://item-service # 路由的目标服务,lb代表负载均衡,会从注册中心拉取服务列表
predicates: # 路由断言,判断当前请求是否符合当前规则,符合则路由到目标服务
- Path=/items/**,/search/** # 这里是以请求路径作为判断规则
- id: cart
uri: lb://cart-service
predicates:
- Path=/carts/**
- id: user
uri: lb://user-service
predicates:
- Path=/users/**,/addresses/**
- id: trade
uri: lb://trade-service
predicates:
- Path=/orders/**
- id: pay
uri: lb://pay-service
predicates:
- Path=/pay-orders/**

5.2 路由属性:

  • ID:路由唯一标识,方便配置和区分不同路由,像user-service-route 。
  • URI:指定请求转发目的地,能是普通 HTTP 地址,也能是基于负载均衡的微服务名称,如lb://order-service 。
  • Predicates(断言):一组条件判断,只有请求满足这些条件,对应路由才会匹配应用,比如根据请求路径、参数、头信息等判断 。
  • Filters(过滤器):可在请求转发前或响应返回后,对请求或响应进行处理,像修改请求头、限流、日志记录等。

参数边用变查就行。

6 网关登录校验:

6.1:实现思路:

网关是所有微服务的入口,一切请求都需要先经过网关。把登录校验的工作放到网关

  • 只需要在网关和用户服务保存秘钥
  • 只需要在网关开发登录校验功能

登录校验必须在请求转发到微服务之前做。

Gateway内部工作的基本原理:

  1. 客户端请求进入网关后由HandlerMapping对请求做判断,找到与当前请求匹配的路由规则(Route),然后将请求交给WebHandler去处理。
  2. WebHandler则会加载当前路由下需要执行的过滤器链(Filter chain),然后按照顺序逐一执行过滤器(后面称为Filter)。
  3. 图中Filter被虚线分为左右两部分,是因为Filter内部的逻辑分为prepost两部分,分别会在请求路由到微服务之前之后被执行。
  4. 只有所有Filterpre逻辑都依次顺序执行通过后,请求才会被路由到微服务。
  5. 微服务返回结果后,再倒序执行Filterpost逻辑。
  6. 最终把响应结果返回。

此处有三个重要问题:

1.如何在网关转发之前做登录校验?

定义一个过滤器,在其中实现登录校验逻辑,并且将过滤器执行顺序定义到NettyRoutingFilter之前

2.网关如何将用户信息传递给微服务?

校验 Token 后,解析出用户信息(如用户 ID、角色),通过 请求头 / 请求参数 传递:

  • 解析 Token 得到用户信息,存到新的请求头(如 X-User-IdX-User-Role)。
  • 微服务通过 @RequestHeader 读取这些头信息,拿到用户数据。

3.如何在微服务之间传递用户信息?

微服务调用链中,通过 Feign 拦截器 + 请求头透传 实现:

  • 用 Feign 的 RequestInterceptor,在微服务调用其他服务时,从当前请求头中取出用户信息(如 X-User-Id),再添加到 Feign 请求头里。
  • 下游微服务同样通过 @RequestHeader 读取这些头信息。

6.2:自定义过滤器:

网关过滤器链中的过滤器有两种:

  • GatewayFilter:路由过滤器,作用范围比较灵活,可以是任意指定的路由Route.
  • GlobalFilter:全局过滤器,作用范围是所有路由,不可配置。

一般使用较简单的GlobalFilter

@Component
public class PrintAnyGlobalFilter implements GlobalFilter, Ordered {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 编写过滤器逻辑
        System.out.println("未登录,无法访问");
        // 放行
        // return chain.filter(exchange);

        // 拦截
        ServerHttpResponse response = exchange.getResponse();
        response.setRawStatusCode(401);
        return response.setComplete();
    }

    @Override
    public int getOrder() {
        // 过滤器执行顺序,值越小,优先级越高
        return 0;
    }
}

6.3 实现登录校验:

配合配置类以及配置文件使用。

@Component
@RequiredArgsConstructor
@EnableConfigurationProperties(AuthProperties.class)
public class AuthGlobalFilter implements GlobalFilter, Ordered {

private final JwtTool jwtTool;

private final AuthProperties authProperties;

private final AntPathMatcher antPathMatcher = new AntPathMatcher();

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 1.获取Request
ServerHttpRequest request = exchange.getRequest();
// 2.判断是否不需要拦截
if(isExclude(request.getPath().toString())){
// 无需拦截,直接放行
return chain.filter(exchange);
}
// 3.获取请求头中的token
String token = null;
List<String> headers = request.getHeaders().get("authorization");
if (!CollUtils.isEmpty(headers)) {
token = headers.get(0);
}
// 4.校验并解析token
Long userId = null;
try {
userId = jwtTool.parseToken(token);
} catch (UnauthorizedException e) {
// 如果无效,拦截
ServerHttpResponse response = exchange.getResponse();
response.setRawStatusCode(401);
return response.setComplete();
}

// TODO 5.如果有效,传递用户信息
System.out.println("userId = " + userId);
// 6.放行
return chain.filter(exchange);
}

private boolean isExclude(String antPath) {
for (String pathPattern : authProperties.getExcludePaths()) {
if(antPathMatcher.match(pathPattern, antPath)){
return true;
}
}
return false;
}

@Override
public int getOrder() {
return 0;
}
}

6.4 网关传递用户:

思路:

添加内容:

  • 改造网关过滤器,在获取用户信息后保存到请求头,转发到下游微服务
  • 编写微服务拦截器,拦截请求获取用户信息,保存到ThreadLocal后放行

6.4.1 保存用户到请求头

String userInfo = userId.toString();
//5.如果有效,传递用户信息
ServerWebExchange swe = exchange.mutate()
.request(builder -> builder.header("user-info", userInfo))
.build();
// 6.放行
return chain.filter(swe);

6.4.2.拦截器获取用户

思路是:

1.在common模块里编写拦截器和ThreadLocal工具(UserContext)。

public class UserInfoInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1.获取请求头中的用户信息
        String userInfo = request.getHeader("user-info");
        // 2.判断是否为空
        if (StrUtil.isNotBlank(userInfo)) {
            // 不为空,保存到ThreadLocal
                UserContext.setUser(Long.valueOf(userInfo));
        }
        // 3.放行
        return true;
    }

    @Overrid
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 移除用户
        UserContext.removeUser();
    }
}

2.在hm-common模块下编写SpringMVC的配置类,配置登录拦截器。

@Configuration
@ConditionalOnClass(DispatcherServlet.class)//排除网关module(网关底层原理不是springmvc)publicclassMvcConfig implementsWebMvcConfigurer {
@Override
publicvoidaddInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(newUserInfoInterceptor());
}
}

不过,需要注意的是,这个配置类默认是不会生效的,因为它所在的包是com.hmall.common.config,与其它微服务的扫描包不一致,无法被扫描到,因此无法生效。

3.基于SpringBoot的自动装配原理,我们要将其添加到resources目录下的META-INF/spring.factories文件中,让跨包的配置类生效:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.hmall.common.config.MyBatisConfig,\
  com.hmall.common.config.MvcConfig

6.5 openFeign传递用户:

微服务之间的调用是通过openfeign的,所以要通过openFeign传递信息。

这里要借助Feign中提供的一个拦截器接口:feign.RequestInterceptor,所有由OpenFeign发起的请求都会先调用拦截器处理请求。

视频中将该拦截器的实现放在了api模块配置类,使用了匿名内部类,个人感觉不够规范,遂采用以下方法:

1.在api中编写拦截器类:

@Slf4j
@Component
public class UserInfoRequestInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate requestTemplate) {
Long userId = UserContext.getUser();
//TEST:
//log.info("userId:{}", userId);
if (userId != null){
//log.info("SUCCESS!!! userId:{}", userId);
requestTemplate.header("user-info", String.valueOf(userId));
}
}
}

2.此时的拦截器虽然有@Component,但并不会被扫描(因为api模块没有启动类),所以通过META-INF/spring.factories让该拦截器生效:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
    com.hmall.api.interceptor.UserInfoRequestInterceptor

(理解 EnableAutoConfiguration 在 Spring Boot 中的作用—— 它并非只能配置 “配置类”,而是可以自动注册任何需要被 Spring 容器管理的 Bean,只要这个类符合 Bean 的定义规则)

这样就跨包让拦截器生效在业务微服务中了。

7 配置管理:

此节先跳过。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇