微服务快速入门

发布于:2025-07-30 ⋅ 阅读:(13) ⋅ 点赞:(0)

一、微服务01

1、单体架构

单体架构: 将业务的所有功能集中在一个项目中开发,打成一个包部署。

优点:

  • 架构简单
  • 部署成本低

缺点:

  • 团队协作成本高
  • 系统发布效率低
  • 系统可用性差

总结:
单体架构适合开发功能相对简单,规模较小的项目。

2、微服务

微服务架构,是服务化思想指导下的一套最佳实践架构方案。服务化,就是把单体架构中的功能模块拆分为多个独立项目。

  • 粒度小
  • 团队自治
  • 服务自治

3、SpringCould

SpringCloud是目前国内使用最广泛的微服务框架。官网地址:https://spring.io/projects/spring-cloud SpringCloud集成了各种微服务功能组件,并基于SpringBoot实现了这些组件的自动装配,从而提供了良好的开箱即用体验。

(1)、服务拆分原则

什么时候拆分?
  • 创业型项目:先采用单体架构,快速开发,快速试错。随着规模扩大,逐渐拆分。
  • 确定的大型项目:资金充足,目标明确,可以直接选择微服务架构,避免后续拆分的麻烦。
怎么拆分?

从拆分目标来说,要做到:

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

从拆分方式来说,一般包含两种方式:

  • 纵向拆分: 按照业务模块来拆分
  • 横向拆分: 抽取公共服务,提高复用性

(2)、拆分服务

工程结构有两种:

  • 独立Project
  • Maven聚合

微服务拆分时,可以把每个小块服务创建成模块放在总项目下;拆分时,只把与自己功能有关的代码拆出来,配置类里的部分内容要进行修改(如类名等)。

(3)、远程调用

不同的小功能之间难免会有所依赖,如:购物车功能要查询商品的价格,这时候就不能直接注入调用功能了,此时就要用到远程调用

Spring给我们提供了一个RestTemplate工具,可以方便的实现Http请求的发送。使用步骤如下:

① 创建RestTemplate对象

 ② 注入RestTemplate到Spring容器

③ 发起远程调用

 注册中心原理

  •  服务提供者:暴露服务接口,供其它服务调用
  • 服务消费者:调用其它服务提供的接口
  • 注册中心:记录并监控微服务各实例状态,推送服务变更信息

 

 Nacos注册中心

在虚拟机中创建一个 nacos/custom.env 文件内容如下:

 PREFER_HOST_MODE=hostname

MODE=standalone

SPRING_DATASOURCE_PLATFORM=mysql

MYSQL_SERVICE_HOST=192.168.88.130(修改成自己的IP)

MYSQL_SERVICE_DB_NAME=nacos

MYSQL_SERVICE_PORT=3306

MYSQL_SERVICE_USER=root

MYSQL_SERVICE_PASSWORD=123(修改成自己的密码)

MYSQL_SERVICE_DB_PARAM=characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai

 数据库里也要创建 nacos 需要的表

之后运行以下命令,并把nacos加入MySQL的网络

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.88.130:8848/nacos(把IP换成自己的)账号密码默认都是 nacos

 服务注册

 服务发现和负载均衡

OpenFeign

OpenFeign是一个声明式的http客户端,是SprinqCloud在Eureka公司开源的Feign基础上改造而来。其作用就是基于SpringMVC的常见注解,帮我们优雅的实现http请求的发送。

OpenFeign已经被SpringCloud自动装配,实现起来非常简单:

① 引入依赖,包括0penFeiqn和负载均衡组件SpringCloudLoadBalancer

② 通过@EnableFeiqnClients注解,启用OpenFeiqn功能

③ 编写Feiqnclient

④ 使用FeignClient,实现远程调用

连接池

Feign底层发起http请求,依赖于其它的框架。其底层支持的http客户端实现包括:

  • HttpURLConnection:默认实现,不支持连接池

  • Apache HttpClient :支持连接池

  • OKHttp:支持连接池

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

① 引入依赖

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

 ② 在yaml中开启连接池功能

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

最佳实践

之前写代码时,一个功能用到另一个功能的类时,都是直接再写一遍,但是这样就重复编码了,为了避免重复编码,我们可以把它抽取出来。

  • 思路1:抽取到微服务之外的公共module

  • 思路2:每个微服务自己抽取一个module,放在自己下面

前面拆分微服务时有两种方法,一种是在一个文件夹中放每个功能的项目,适合思路2,另一种是在一个总项目中,把每个功能写成模块,适合思路1

方案1抽取更加简单,工程结构也比较清晰,但缺点是整个项目耦合度偏高。

方案2抽取相对麻烦,工程结构相对更复杂,但服务之间耦合度降低。

使用第一种思路时可能会因为包扫描出错,因为引用模块没有扫描到提取出来的类(原因:包名不同)

  • 方式1:声明扫描包:

  • 方式2:声明要用的FeignClient

日志

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

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

要自定义日志级别需要声明一个类型为Logger.Level的Bean,在其中定义日志级别:

但此时这个Bean并未生效,要想配置某个FeiqnClient的日志,可以在@FeignClient注解中声明:

如果想要全局配置,让所有FeianClient都按照这个日志配置,则需要在@EnableFeianClients注解中声明:

二、微服务02

1、网关

网关的实现:Spring Cloud Gateway
  • Spring官方出品
  • 基于WebFlux响应式编程
  • 无需调优即可获得优异性能

接下来,我们先看下如何利用网关实现请求路由。由于网关本身也是一个独立的微服务,因此也需要创建一个模块开发功能。大概步骤如下:

  • 创建网关微服务

  • 引入SpringCloudGateway、NacosDiscovery依赖

  • 编写启动类

  • 配置网关路由

server:
  port: 8080
spring:
  application:
    name: gateway
  cloud:
    nacos:
      server-addr: 192.168.150.101: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/**
路由属性

网关路由对应的Java类型是RouteDefinition,其中常见的属性有:

  • id: 路由唯一标示
  • uri: 路由目标地址
  • predicates: 路由断言,判断请求是否符合当前路由。
  • filters: 路由过滤器,对请求或响应做特殊处理。

 路由断言

 Spring提供了12种基本的RoutePredicateFactory实现:

 路由 过滤器

 网关中提供了33种路由过滤器,每种过滤器都有独特的作用。

 网关登录校验--思路分析
 自定义过滤器

 登录校验过滤器
package com.hmall.gateway.filter;

import com.hmall.common.exception.UnauthorizedException;
import com.hmall.common.utils.CollUtils;
import com.hmall.gateway.config.AuthProperties;
import com.hmall.gateway.util.JwtTool;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.util.List;

@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;
    }
}
网关传递用户到微服务

现在,网关已经可以完成登录校验并获取登录用户身份信息。但是当网关将请求转发到微服务时,微服务又该如何获取用户身份呢?

由于网关发送请求到微服务依然采用的是Http请求,因此我们可以将用户信息以请求头的方式传递到下游微服务。然后微服务可以从请求头中获取登录用户信息。考虑到微服务内部可能很多地方都需要用到登录用户信息,因此我们可以利用SpringMVC的拦截器来实现登录用户信息获取,并存入ThreadLocal,方便后续使用。

因此,接下来我们要做的事情有:

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

  • 编写微服务拦截器,拦截请求获取用户信息,保存到ThreadLocal后放行

首先,我们修改登录校验拦截器的处理逻辑,保存用户信息到请求头中:

接下来,我们只需要编写拦截器,获取用户信息并保存到UserContext,然后放行即可。

由于每个微服务都有获取登录用户的需求,因此拦截器我们直接写在hm-common中,并写好自动装配。这样微服务只需要引入hm-common就可以直接具备拦截器功能,无需重复编写。

我们在hm-common模块下定义一个拦截器:

package com.hmall.common.interceptor;

import cn.hutool.core.util.StrUtil;
import com.hmall.common.utils.UserContext;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

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;
    }

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

接着在hm-common模块下编写SpringMVC的配置类,配置登录拦截器:

package com.hmall.common.config;

import com.hmall.common.interceptors.UserInfoInterceptor;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
@ConditionalOnClass(DispatcherServlet.class)
public class MvcConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new UserInfoInterceptor());
    }
}

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

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

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.hmall.common.config.MyBatisConfig,\
  com.hmall.common.config.MvcConfig
OpenFeign传递用户

前端发起的请求都会经过网关再到微服务,由于我们之前编写的过滤器和拦截器功能,微服务可以轻松获取登录用户信息。

但有些业务是比较复杂的,请求到达微服务后还需要调用其它多个微服务。比如下单业务,流程如下:

下单的过程中,需要调用商品服务扣减库存,调用购物车服务清理用户购物车。而清理购物车时必须知道当前登录的用户身份。但是,订单服务调用购物车时并没有传递用户信息,购物车服务无法知道当前用户是谁!

由于微服务获取用户信息是通过拦截器在请求头中读取,因此要想实现微服务之间的用户信息传递,就必须在微服务发起调用时把用户信息存入请求头

微服务之间调用是基于OpenFeign来实现的,并不是我们自己发送的请求。我们如何才能让每一个由OpenFeign发起的请求自动携带登录用户信息呢?

这里要借助Feign中提供的一个拦截器接口:feign.RequestInterceptor

public interface RequestInterceptor {

  /**
   * Called for every request. 
   * Add data using methods on the supplied {@link RequestTemplate}.
   */
  void apply(RequestTemplate template);
}

我们只需要实现这个接口,然后实现apply方法,利用RequestTemplate类来添加请求头,将用户信息保存到请求头中。这样以来,每次OpenFeign发起请求的时候都会调用该方法,传递用户信息。

@Bean
public RequestInterceptor userInfoRequestInterceptor(){
    return new RequestInterceptor() {
        @Override
        public void apply(RequestTemplate template) {
            // 获取登录用户
            Long userId = UserContext.getUser();
            if(userId == null) {
                // 如果为空则直接跳过
                return;
            }
            // 如果不为空则放入请求头中,传递给下游微服务
            template.header("user-info", userId.toString());
        }
    };
}

 配置管理

 我们在nacos控制台分别添加需要的配置(重复的),类似数据库配置时的数据源,各个服务的数据源不同,可以用${}来写,从配置类中读取,配置类中只用写这些不同的配置。

拉取共享配置

基于NacosConfig拉取共享配置代替微服务的本地配置。

① 引入依赖

② 新建bootstrap.yaml

 配置热更新

配置热更新: 当修改配置文件中的配置时,微服务无需重启即可使配置生效。

前提条件:

① nacos中要有一个与微服务名有关的配置文件。

② 微服务中要以特定方式读取需要热更新的配置属性。 

package com.hmall.cart.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Data
@Component
@ConfigurationProperties(prefix = "hm.cart")
public class CartProperties {
    private Integer maxAmount;
}

 接着,在业务中使用该属性加载类:


网站公告

今日签到

点亮在社区的每一天
去签到