当前位置: 首页 > news >正文

微服务快速入门

一、微服务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: gatewaycloud:nacos:server-addr: 192.168.150.101:8848gateway:routes:- id: item # 路由规则id,自定义,唯一uri: lb://item-service # 路由的目标服务,lb代表负载均衡,会从注册中心拉取服务列表predicates: # 路由断言,判断当前请求是否符合当前规则,符合则路由到目标服务- Path=/items/**,/search/** # 这里是以请求路径作为判断规则- id: carturi: lb://cart-servicepredicates:- Path=/carts/**- id: useruri: lb://user-servicepredicates:- Path=/users/**,/addresses/**- id: tradeuri: lb://trade-servicepredicates:- Path=/orders/**- id: payuri: lb://pay-servicepredicates:- 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();@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {// 1.获取RequestServerHttpRequest request = exchange.getRequest();// 2.判断是否不需要拦截if(isExclude(request.getPath().toString())){// 无需拦截,直接放行return chain.filter(exchange);}// 3.获取请求头中的tokenString token = null;List<String> headers = request.getHeaders().get("authorization");if (!CollUtils.isEmpty(headers)) {token = headers.get(0);}// 4.校验并解析tokenLong 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;}@Overridepublic 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 {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 1.获取请求头中的用户信息String userInfo = request.getHeader("user-info");// 2.判断是否为空if (StrUtil.isNotBlank(userInfo)) {// 不为空,保存到ThreadLocalUserContext.setUser(Long.valueOf(userInfo));}// 3.放行return true;}@Overridepublic 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 {@Overridepublic 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() {@Overridepublic 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;
}

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

http://www.dtcms.com/a/305097.html

相关文章:

  • BehaviorTree.Ros2 编译教程
  • JavaWeb 入门:JavaScript 基础与实战详解(Java 开发者视角)
  • 飞算科技:以原创之力,开启Java开发新纪元与行业数智变革
  • 技术QA | GNSS模拟器如何赋能自动驾驶?聚焦HIL、多实例与精准轨迹仿真的技术优势
  • Ignite(Apache Ignite)中计算与数据共置的核心概念和编程实践
  • 小程序视频播放,与父视图一致等样式设置
  • Electron将视频文件单独打包成asar并调用
  • 如何在Linux系统下进行C语言程序的编写和debug测试
  • 解锁全球数据:Bright Data MCP 智能解决代理访问难题
  • 三极管、MOS 管、CMOS 管的特点、属性及综合对比
  • DAY27 函数专题2:装饰器
  • 【算法训练营Day18】二叉树part8
  • BOSMA博冠推出8K广播级讯道摄像机DC0300 EFP
  • 项目开发需求管理
  • 项目目标如何设定?遵循的主要原则分析
  • unity 使用PropertyDrawer 在Inspector 面板上自定义字段的显示方式
  • Android User版本默认用test-keys,如何改用release-keys
  • IDDR原语基本使用
  • 【三桥君】AI技术发展下,单智能体局限性凸显,如何通过MCP和A2A协议实现智能体团队协作转变?
  • Day 25:异常处理
  • GitLab的安装及使用
  • 嵌入式第十四课!!!指针在字符数组的应用与数组指针
  • 【Lua】题目小练3
  • 13、select_points_object_model_3d解析
  • Excel制作滑珠图、哑铃图
  • HCIP--MGRE综合实验
  • 从0到1学PHP(五):PHP 数组:高效存储与处理数据
  • C#_ArrayList动态数组
  • 【C#获取高精度时间】
  • 同一个局域网段,如何实现所有用户都可以访问本地mysql数据库?