什么是微服务?
一般来说我们的项目一打开来就需要运行一个springboot就行了,也就是说所有的服务(比如说用户模块,订单模块等)都是放在一个springboot项目里面的,然而微服务就是将这些集成在一个项目里面的模块给差分出来,每一个模块主要负责自己的那一块任务,每个模块都能独立运行,模块之间互相调用,能做到互相不影响(一个服务宕机了,不会影响其他的服务)
微服务技术
spring cloud
Doubbo (RPC调用)
RPC
本质上是通过HTTP,或者其他1的网络协议通讯来实现
RPC 调用 与 Http调用的区别 HTTP和RPC的区别-腾讯云开发者社区-腾讯云 (tencent.com)
分布式与微服务的区别
分布式应该是将 你的 项目拆分开来放到多个机器中去运行(也可以是把子模块拆开来放到其他的机器上)
微服务就是将整个大的项目拆分开一个个小的项目用网络协议来进行互相调用
分布式一定是微服务,微服务不一定是分布式
Spring Cloud Alibaba
阿里巴巴在使用多年springcloud之后,在他的原本的功能的基础上增加了一些阿里的各个功能的一些模块
大致理解
一:服务调用
我们将所有的一个个服务都拆分开来的那应该如何互相调用呢?
一般这些都是用服务于注册发现模型如下
需要一个注册中心,将生产者和消费者都注册进去,消费者要调用其他服务的接口就可以去注册中心查看(对应的服务名称),找到提供接口的那一方(ip和端口),然后就可以调用服务
二:配置管理
就是可以去修改软件中的一些配置,然后影响软件的运行流程,例如springboot的yml的文件,可以让软件变得更加的灵活,但是我们这些配置一般都是直接都是一起打包起来然后上传到服务器,如果要修改还要重新打包再上传(不宜维护)
有一种解决方式就是配置中心软件架构
有如下几个优点:
我们只需要在配置中心修改文件信息,就可以影响到软件,我们就可以在外部,动态的更新配置,这是其中的一个优点。
还可以对配置进行一些保护,对配置文件加密。
还可以隔离环境,例如开发环境就有开发环境的配置,测试就有测试的等等
如果有大量软件需要配置维护的时候,很方便
微服务项目图
微服务改造
从业务需求出发:思考单机和分布式的区别
用户登录功能:需要改造为分布式登录(session单机可能是存在自己的单个tomcat中的,其他的用不了你的session)
其他内容:
- 是否使用到单机锁,修改为分布式锁redis
- 是否使用到事务,是否操作了多个库
- 是否用到本地缓存,存入redis
拆分出来一些公共的模块:
- common公共模块:全局处理,请求响应封装类,公用的工具类
- model模型模块:服务公用的实体类
- 公共接口模块:只存放接口,不存放实现(多个服务之间共享)
业务功能:
按照最开始的设计好的模块来划分
用户服务模块:
- 用户登录
- 用户注册
- 用户管理
题目服务
- 创建题目
- 删除题目
- 修改题目
- 搜索题目
- 在线做题
- 题目提交
判题服务(较重的操作从题目服务抽出来)
- 执行判题逻辑
- 错误处理
- 沙箱
- 开放接口
我们服务都分开来了,现在前端发送的请求,肯定跟之前有很大的不一样,不如说各个端口,所以这些操作一般都由后端来处理,所以我们就需要一个网关来做路由分发
接下来需要使用网关来控制前端发送的信息到底转发到那个服务路由分发
用户服务:
- /api/usr
- /api/usr/inner(内部调用,需要做限制)
题目服务:
- /api/question(也包括题目提交信息)
- /api/question/inner(内部调用,需要做限制)
判题服务
- /api/judge
- /api/judge/inner(内部调用,需要做限制)
基础的项目构建
直接是新建一个最普通的项目然后勾选为使用maven导入依赖
一.最普通的模块创建例如 common 与 model 不需要添加其他的依赖(最初创建,之后按需慢慢加入依赖)
1.common模块 加入的依赖中 org.apache.commons(可以将这个直接提到父模块中,因为其他的模块可能也需要)中的
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.4</version>
</dependency>
需要单独在引入,因为他这个大包,默认不会给你加这个
2.model模块 中一些数据库的是实体类 是需要mybatisplus的注解 考虑到其他的模块也需要这个所以我们直接放入到父pom中
当你发现引入的东西需要别的模块的类的时候可以直接引入有那个类的模块 放入 当前的 模块中
二 .创建一个其他服务都要调用的接口端 clinet,由于需要给其他项目调用的接口所以要引入openfen(保险起见可以直接使用springboot初始化)
client(专门放接口的被的服务调用所以直接取名client)
如果是将普通的项目转换为微服务,可以直接暂时先将所有的接口直接放入。
如果遇到引入不了父模块定义好的一些模块 可以看一下父模块中的 模块的版本,然后直接在子模块里面写上父依赖里面一致的版本号(正常来说他们是不写版本号的)
创建服务
都是springboot初始化
对于需要相互调用的接口统一操作
- 这些服务之间需要相互调用,我们先将各个服务的需要远程调用的接口取出来,之后就直接client中编写一个相关的接口(UserFeginClinet),放入这些需要服务器内部调用的接口,有些比较简单的接口,就可以直接接口defual默认在接口处实现即可,没必要再远程操作传输,影响性能(或者一些比较复杂啊类例如request)
OpenFegin 如何调用? (其实就是简化了我们的使用http请求的操作)
一 .接口端操作
在接口的上方写上@FeignClient(name = "",path = "")
主要注意这俩点 (找到是哪个服务-->name 服务的哪个路径-->path)
- name 是让这个接口知道是哪个模块实现的这些方法,一般都是如下,他们注册到服务中心的名字都是根据每个项目的spring application name 来确定的,我们去注册中心就可以根据这个名字来寻找到相关的模块。
spring:
application:
name: lc-oj-jufge-service
- path 是我们需要知道这个项目的前缀的是什么,就好比确定的请求前缀,一般都是如下的这个context-path来确定
server:
address: 0.0.0.0
port: 8105
servlet:
context-path: /api/judge
注意:接口尽量少继承一些例如mybatis plus 提供的类,每一个写的OpenFeign的接口都要写上 传参数的相对于的 注解,例如
@GetMapping("/get/id")
Question getQuestionById(@RequestParam("questionId") long questionId);
@GetMapping("/question_submit/get/id")
QuestionSubmit getQuestionSubmitById(@RequestParam("questionSubmitId") long questionSubmitId);
@PostMapping("/question_submit/update")
boolean updateById(@RequestBody QuestionSubmit questionSubmit);
不然可能在传输的时候丢失
二.实现方
现在知识定义好了接口方法,我们需要在各个模块中做相对的实现
首先这个路径要完全相同,最好方法名也要相同,路径就是由上面的@FeignClient(name = "",path = "")name和path来决定
如下
接口
/**
* 用户服务
*/
@FeignClient(name = "lc-oj-user-service",path = "/api/user/inner")
public interface UserFeignClient {
@GetMapping("/get/id")
User getById(@RequestParam("userId") long userId);
@GetMapping("/get/ids")
List<User> listByIds(@RequestParam("userIdSet") Collection<Long> userIdSet);
}
实现(这是在spring:application:name:lc-oj-user-service中的操作)
@RestController
@RequestMapping("/inner")
public class UserInnerController {
@Resource
private UserService userService;
@GetMapping("/get/id")
public User getById(@RequestParam("userId") long userId){
return userService.getById(userId);
}
@GetMapping("/get/ids")
public List<User> listByIds(@RequestParam("userIdSet") Collection<Long> userIdSet){
return userService.listByIds(userIdSet);
}
}
三.公共的操作
接下来需要让所有的类都要相互认识我们就需要在springboot中配置号nacos,才能让上面的所有的操作都注册进nacos
spring:
cloud:
nacos:
serverAddr: 127.0.0.1:8848
我们所有的服务都要放入,因为我们现在的所有的服务都要互相调用(网关也需要),除了一些公共模块
client要不要也写入?
答:个人觉得 不需要他只是定义了接口 还有能够让 openfeign找到的接口就行 ❌
实际上:他只是接口还是要到注册中心去找谁的实现 ✔
接下来我们需要在springboot启动类上面设置nacos与openfeign的启动注解
@EnableDiscoveryClient (服务发现,让调用方能够找到他)
单单这一步还是不够的,那我们的接口端client应该怎么才能让他去找到呢?
答: @EnableFeignClients(basePackages = "com.lc.oj.service") ,再加上这个注解才能够实现让这个springboot找到openfeign定义的接口
注意:这个写在springboot类上的 就不要在client里面了,因为他根本就需要去开启nacos
最后我们发现还是不能启动,因为当springcloud alibaba 启动的时候它会去默认使用负载均衡的包去设置负载均衡,然后进行多个服务之间的调用所以我们还需要在公共模块中写入
以下报错
No Feign Client for loadBalancing defined. Did you forget to include spring-cloud-starter-loadbalancer?
引入依赖 (根据spring cloud 的版本确认)
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-loadbalancer</artifactId>
<version>3.1.5</version>
</dependency>
用户服务 (没有调用其他的接口)
依赖: spring web
迁移:直接复制粘贴 service , mapper , controller,处理user相关的功能留下
由于这个是springboot启动的一个服务 千万别忘了粘贴 springboot上面的启动的一些配置,特别是加上@ComponentScan("com.lc") 因为你需要的一些其他的类在其他的包中,所以你需要设置扫描所有的com.lc的包下面的东西放入你的spring容器中
题目服务
依赖:spring web
迁移操作如上
如果有一些service 需要调用的服务的接口那需要 openfen (服务之间相互调用)
需要调用的接口
//写一个UserFeignClient
User userService.getLoginUser(request);
boolean userService.isAdmin(request);
User userService.getById(userId)
UserVo userService.getUserVO(user)
List<User> userService.listByIds(userIdSet)
judgeService.doJudge(questionSubmitId);
判题服务
依赖:spring web
迁移操作如上
需要调用的接口
Question questionService.getById(questionId);
QuestionSubmit questionSubmitService.getById(questionSubmitId);
boolean
questionSubmitService.updateById(updateQuestionSubmit);
网关
官方文档: Spring Cloud Gateway(引用层网关,可能有一些业务逻辑)
Nginx (接入层网关,通常没有业务,直接就是转发)
创建的时候可以添加:
1.spring getway
2.spring web
什么是微服务网关?
- 就像是一个电影院的进票口一样,挡在所有人的面前(请求),需要出示相关的电影票才可以进入(像是权限与路由的校验,转发等)
为什么用微服务网关?
- 当后端有多个服务(多个端口),前端发送消息的时候无法做到统一的端口,我们可以用网关来做一个分发,前端只需要用规定号的路由即可
- 当后端需要对所有的请求过来的操作做一些统一的权限校验,流量染色,接口文档,解决跨域,限流,接口安全等操作可以使用网关(后端有多个端口)
Gateway:缺点就是需要做定制化比较麻烦,对其需要有很深刻的了解
接口路由
当前端发送信息过来后我们应该如何让他转发到相应的路径呢?
可以直接在gateway的ymal里面配置
1.首先前端发送的一个请求之后,我们要根据这个前端做判断比如说/api/user我们就要转发到 user的服务,那我们应该如何找到这个服务呢?
2.根据注册中心去找,使用注册中心的名字(因为可能这个服务的端口号等都不是固定的,所以最好采用负载均衡的方式+名字,来做分发)
如下(可写多个)
spring:
cloud:
nacos:
serverAddr: 127.0.0.1:8848
gateway:
routes:
# id 最好是服务名,方便认
- id: lc-oj-user-service
# 使用负载均衡 + 名字
uri: lc://lc-oj-user-service
# 规则
predicates:
- Path=/api/user/**
启动的时候报错
1.spring mvc 与 spring gateway 是不兼容的,因为springmvc和spring gateway是响应式的web应用框架与springmvc这种传统的不一样
解决:
spring.main.web-application-type=reactive
2.启动的时候报错,找不到数据库数据源
Failed to configure a DataSource: 'url' attribute is not specified and no embedded datasource could be configured.
Reason: Failed to determine a suitable driver class
解决:因为是gateway项目所以我们不需要数据库的加载可以直接在spring启动中去除自动载入数据库的操作就行(操作数据库也是用远程调用)
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
接口文档
网关的依赖依赖
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-gateway-spring-boot-starter</artifactId>
<version>4.3.0</version>
</dependency>
gateway
knife4j:
gateway:
# ① 第一个配置,开启gateway聚合组件
enabled: true
# ② 第二行配置,设置聚合模式采用discover服务发现的模式
strategy: discover
discover:
# ③ 第三行配置,开启discover模式
enabled: true
# ④ 第四行配置,聚合子服务全部为Swagger2规范的文档
version: swagger2
各个的依赖
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi2-spring-boot-starter</artifactId>
<version>4.3.0</version>
</dependency>
各个服务的ymal
knife4j:
enable: true
跨域解决
@Configuration
public class CorsConfig {
@Bean
public CorsWebFilter cosFilter(){
CorsConfiguration config = new CorsConfiguration();
config.addAllowedHeader("*");
config.setAllowCredentials(true);
// todo 设置为实际的线上真实域名,本地域名
config.setAllowedOriginPatterns(Arrays.asList("*"));
config.addAllowedHeader("*");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser());
source.registerCorsConfiguration("/**",config);
return new CorsWebFilter(source);
}
}
访问权限校验
不让前端的用户直接访问内部调用的接口
@Component
public class GlobalAuthFilter implements GlobalFilter, Ordered {
private AntPathMatcher antPathMatcher = new AntPathMatcher();
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest serverHttpRequest = exchange.getRequest();
String path = serverHttpRequest.getURI().getPath();
if(antPathMatcher.match("/**/inner/**",path)){
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.FORBIDDEN);
DataBufferFactory dataBufferFactory = response.bufferFactory();
DataBuffer dataBuffer = dataBufferFactory.wrap("无权限".getBytes(StandardCharsets.UTF_8));
return response.writeWith(Mono.just(dataBuffer));
}
return chain.filter(exchange);
}
/**
* 后期可能有多个过滤器,权限过滤器优先级设置为最后,可以节省一定的性能开销
* @return
*/
@Override
public int getOrder() {
return 0;
}
}
依赖
1.新建一个基本的项目,将pom.xml设置为最高级的父pom(设置为pom包)
<packaging>pom</packaging>
相关细节
pom.xml方面
我们需要检查字符关系依赖,我们其他项目应该都在最开始的初始化项目的内部,可以从右侧maven框查看
需要检查父模块里面的pom.xml
<modules>
<module>lc-backend-common</module>
<module>lc-backedn-model</module>
</modules>
需要在里面添加你的所有的子模块,我们直接创建maven项目会自动放入,但是用springboot初始化就要自己手动设置
设置完了还要在子模块里面设置parent为父pom,让其都去依赖夫pom里设置的依赖
那我们为什么要这么麻烦弄来弄过去这么操作呢?
为了方便管理,只要夫模块中定义好的依赖版本,如果子模块映入对于的模块,不用指定版本号,就可以直接是和父模块的版本号是一样的了,防止依赖冲突(同一版本,继承)
提示:在公共类中定义了很多的依赖,如果子模块没有使用这些依赖,在打包的时候也不会打入父模块的依赖
特别鸣谢 程序员鱼皮!!! 这些是跟皮总学完后的一些总结。