OJ判题项目-微服务改造(七)
往期开发(已经完成)
- OJ 判题项目-介绍(一)
- OJ 判题项目-初始化(二)
- OJ 判题项目-前后端联调(三)
- OJ 判题项目-简易判题机(四)
- OJ 判题项目-原生代码沙箱(五)
- OJ 判题项目-Docker 代码沙箱(六)
今日后端开发
微服务定义
什么是微服务?服务是什么意思?
微服务:专注于提供某类特定功能的代码,而不是把所有的功能代码都放在一个项目中,把大的项目按照一定的功能逻辑进行拆分,拆分为多个子模块,每个子模块可以独立运行,独立负责一类功能,子模块之间可以进行相互调用、互不影响
服务:提供某类功能的代码微服务的技术选型
Spring Cloud Alibaba
文档:https://sca.aliyun.com/zh-cn/微服务介绍
- Spring Cloud GateWay:网关
- Nacos:注册中心
- Sentinel:熔断限流
- Seata:分布式事务
- RocketMQ:消息队列,削峰填谷
- Docker:使用 Docker 进行容器化部署
- Kubernetes:使用 K8s 进行容器化部署

微服务的执行流程

微服务改造
先将项目登录改造为分布式登录使用 redisson 进行改造
引入依赖1
2
3
4
5
6
7
8
9<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>修改配置文件:
1
2
3
4
5
6
7
8# session 配置
session:
store-type: redis
redis:
database: 2
host: localhost
port: 6379
timeout: 50001
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class RedissonConfig {
private Integer database;
private String host;
private Integer port;
private String password;
public RedissonClient redissonClient() {
Config config = new Config();
config.useSingleServer()
.setDatabase(database)
.setAddress("redis://" + host + ":" + port);
RedissonClient redisson = Redisson.create(config);
return redisson;
}
}项目结构划分
先进行子工程划分结构,结构如下
然后给父级工程引入注册中心,微服务,网关等依赖1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-collections4 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.4</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.baomidou/mybatis-plus-boot-starter -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.2</version>
</dependency>
<!-- https://hutool.cn/docs/index.html#/-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.8</version>
</dependency>
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>2021.0.5</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-loadbalancer</artifactId>
<version>3.1.5</version>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>抽离出各个服务之间需要调用的 service 使用 open feign 内部进行调用暴露 service 服务(名称和路径一定要写对)
例如:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68/**
* 用户服务
*/
public interface UserFeignClient {
/**
* 通过ids 来获取用户列表
*
* @param ids
* @return
*/
List<User> listByIds( Collection<Long> ids);
/**
* 根据id来获取用户
*
* @param id
* @return
*/
User getById( Long id);
/**
* 获取当前登录用户 写成默认方法 不再远程调用服务 提升性能
*
* @param request
* @return
*/
default User getLoginUser(HttpServletRequest request) {
// 先判断是否已登录
Object userObj = request.getSession().getAttribute(USER_LOGIN_STATE);
User currentUser = (User) userObj;
if (currentUser == null || currentUser.getId() == null) {
throw new BusinessException(ErrorCode.NOT_LOGIN_ERROR);
}
return currentUser;
}
/**
* 是否为管理员 写成默认方法 不再远程调用服务 提升性能
*
* @param user
* @return
*/
default boolean isAdmin(User user) {
return user != null && UserRoleEnum.ADMIN.getValue().equals(user.getUserRole());
}
/**
* 获取脱敏的用户信息 写成默认方法 不再远程调用服务 提升性能
*
* @param user
* @return
*/
default UserVO getUserVO(User user) {
if (user == null) {
return null;
}
UserVO userVO = new UserVO();
BeanUtils.copyProperties(user, userVO);
return userVO;
}
}按照这样抽离出所有内部服务需要暴露的 service

然后给需要暴露 service 服务的模块编写内部调用 controller,例如用户服务
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35/**
* 该服务只提供服务内部之间进行调用
*/
public class UserInnerController implements UserFeignClient {
private UserService userService;
/**
* 通过ids 来获取用户列表
*
* @param ids
* @return
*/
public List<User> listByIds( Collection<Long> ids) {
return userService.listByIds(ids);
}
/**
* 根据id来获取用户
*
* @param id
* @return
*/
public User getById( Long id) {
return userService.getById(id);
}
}依次给需要的服务编写 innerController 即可
为什么需要这样去调用?
由于每个模块都有一个独立的启动入口例如:1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ChenojBackendUserServiceApplication {
public static void main(String[] args) {
SpringApplication.run(ChenojBackendUserServiceApplication.class, args);
}
}自己当前模块使用了其他模块的 service 服务,但是没有其实现类,就会导致 bean 注入失败,所以通过微服务提供的 open feign 来暴露各个服务需要给其他内部进行调用的 service,然后来实现跨模块的内部服务调用
其他模块需要导入***FeignClient 即可调用需要的服务
改造完调用方式后,这个时候就需要微服务的网关来进行服务路由,路由到调用的服务地址,达到服务聚合,统一分发,这样前端只需要访问网关地址即可,
配置文件如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
gateway:
routes:
- id: chenoj-backend-judge-service
uri: lb://chenoj-backend-judge-service
predicates:
- Path=/api/judge/**
- id: chenoj-backend-question-service
uri: lb://chenoj-backend-question-service
predicates:
- Path=/api/question/**
- id: chenoj-backend-user-service
uri: lb://chenoj-backend-user-service
predicates:
- Path=/api/user/**
application:
name: chenoj-backend-gateway
main:
web-application-type: reactive注意网关启动时候,需要禁用掉应用的 web,网关只负责去转发一些服务的调用,不做 web 应用处理: web-application-type: reactive
统一的文档聚合
给有启动类的模块全部引入文档依赖(knife4j)1
2
3
4
5<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-gateway-spring-boot-starter</artifactId>
<version>4.3.0</version>
</dependency>然后进行配置开启文档
网关的配置(特殊)1
2
3
4
5
6
7
8
9
10
11knife4j:
gateway:
# ① 第一个配置,开启gateway聚合组件
enabled: true
# ② 第二行配置,设置聚合模式采用discover服务发现的模式
strategy: discover
discover:
# ③ 第三行配置,开启discover模式
enabled: true
# ④ 第四行配置,聚合子服务全部为Swagger2规范的文档
version: swagger2服务的配置
1
2knife4j:
enable: true- 微服务改造后模块化导致分布式登录失效需要指定 cookie 的配置
1
2
3
4
5
6
7
8
9
10server:
address: 0.0.0.0
port: 8093
servlet:
context-path: /api/judge
# cookie 30 天过期
session:
cookie:
max-age: 2592000
path: /api 全局跨域
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33package com.chen.gateway.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
import org.springframework.web.util.pattern.PathPatternParser;
import java.util.Arrays;
/**
* 全局跨域配置
*/
public class CorsConfig {
public CorsWebFilter corsWebFilter() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.addAllowedMethod("*");
corsConfiguration.setAllowCredentials(true);
// todo 实际为上线真实域名
corsConfiguration.setAllowedOriginPatterns(Arrays.asList("*"));
corsConfiguration.addAllowedHeader("*");
UrlBasedCorsConfigurationSource urlBasedCorsConfigurationSource = new UrlBasedCorsConfigurationSource(new PathPatternParser());
urlBasedCorsConfigurationSource.registerCorsConfiguration("/**", corsConfiguration);
return new CorsWebFilter(urlBasedCorsConfigurationSource);
}
}权限校验
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48package com.chen.gateway.filter;
import com.alibaba.nacos.common.packagescan.resource.AntPathMatcher;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.nio.charset.StandardCharsets;
public class GlobalAuthFilter implements GlobalFilter, Ordered {
private final AntPathMatcher antPathMatcher = new AntPathMatcher();
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 wrap = dataBufferFactory.wrap("无权限".getBytes(StandardCharsets.UTF_8));
return response.writeWith(Mono.just(wrap));
}
// todo 做统一校验 使用JWT Token做验证校验
return chain.filter(exchange);
}
/**
* 过滤优先级
*
* @return
*/
public int getOrder() {
return 0;
}
}
消息队列引入(上一个项目已经学习过)
引入依赖
1
2
3
4
5
6
7
8
9
10
11
12<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-amqp -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
<version>2.7.2</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.rabbitmq/amqp-client -->
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.17.0</version>
</dependency>引入配置
1
2
3
4
5rabbitmq:
host: localhost
port: 5672
password: guest
username: guest初始化 mq
在启动项目时候调用1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38package com.chen.judgeservice.rabbitmq;
import com.chen.common.constant.OJMqConstant;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import lombok.extern.slf4j.Slf4j;
public class RabbitMQInit {
/**
* RabbitMQ初始化
*/
public static void Init() {
try {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
factory.setPort(5672);
factory.setUsername("guest");
factory.setPassword("guest");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
// 声明交换机 类型为直接交换机
channel.exchangeDeclare(OJMqConstant.OJ_EXCHANGE_NAME, "direct");
// 声明队列
channel.queueDeclare(OJMqConstant.OJ_QUEUE_NAME, true, false, false, null);
// 绑定队列到交换机
channel.queueBind(OJMqConstant.OJ_QUEUE_NAME, OJMqConstant.OJ_EXCHANGE_NAME, OJMqConstant.OJ_ROUTING_KEY);
// 限制任务数
channel.basicQos(1);
log.info("RabbitMQ初始化完成!");
} catch (Exception e) {
log.error("RabbitMQ初始化失败!错误信息为:{0}", e);
}
}
}生产者代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24package com.chen.questionservice.rabbitmq;
import com.chen.common.constant.OJMqConstant;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
public class OJMessageProducer {
private RabbitTemplate rabbitTemplate;
/**
* 发送消息
*
* @param message
*/
public void sendMessage(String message) {
rabbitTemplate.convertAndSend(OJMqConstant.OJ_EXCHANGE_NAME, OJMqConstant.OJ_ROUTING_KEY, message);
}
}消费者代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60package com.chen.judgeservice.rabbitmq;
import com.chen.model.entity.Question;
import com.chen.model.entity.QuestionSubmit;
import cn.hutool.json.JSONUtil;
import com.chen.common.common.ErrorCode;
import com.chen.common.constant.OJMqConstant;
import com.chen.common.exception.BusinessException;
import com.chen.judgeservice.codesandbox.service.CodeSandBoxService;
import com.chen.model.dto.codesandbox.CodesandBoxRequest;
import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.support.AmqpHeaders;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.handler.annotation.Header;
import org.springframework.stereotype.Component;
public class OJMessageConsumer {
private CodeSandBoxService codeSandBoxService;
/**
* 监听消息消费
*
* @param message
* @param channel
* @param deliverTag
*/
public void receiveMessage(String message, Channel channel, long deliverTag) {
try {
log.info("receiveMessage: {}", message);
if (StringUtils.isBlank(message)) {
// 参数 消息的tag 是否要全部拒绝 false为只拒绝当前 是否要重入队列 false为不入
channel.basicNack(deliverTag, false, false);
throw new BusinessException(ErrorCode.SYSTEM_ERROR);
}
CodesandBoxRequest codesandBoxRequest = JSONUtil.toBean(message, CodesandBoxRequest.class);
Question question = codesandBoxRequest.getQuestion();
QuestionSubmit questionSubmit = codesandBoxRequest.getQuestionSubmit();
try {
codeSandBoxService.doCodeSandBoxJudge(questionSubmit, question);
} catch (Exception e) {
channel.basicNack(deliverTag, false, false);
}
channel.basicAck(deliverTag, false);
} catch (Exception e) {
log.error("消息消费失败!");
throw new BusinessException(ErrorCode.SYSTEM_ERROR);
}
}
}项目改造
1
2
3
4
5// todo 执行判题 异步执行 改造为RabbitMQ消息队列
CompletableFuture.runAsync(() -> {
// 向消息队列发送消息
ojMessageProducer.sendMessage(JSONUtil.toJsonStr(codesandBoxRequest));
});
