一、微服务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;
}
接着,在业务中使用该属性加载类: