跳到主要内容Spring Boot 微服务架构:独立匹配系统设计及后端对接 | 极客日志Javajava算法
Spring Boot 微服务架构:独立匹配系统设计及后端对接
综述由AI生成介绍基于 Spring Boot 构建独立微服务匹配系统的方案。通过将匹配逻辑从主后端剥离为 Matching System 服务,利用 HTTP 接口与 Backend 交互。实现了玩家注册、移除及基于 Rating 的匹配算法,并通过 RestTemplate 进行服务间通信。同时配置了 Spring Security 保障接口安全,最终完成匹配结果回传至 WebSocket 服务的完整流程。
利刃28 浏览 背景
在开发实时对战游戏时,匹配系统是连接玩家与对局的核心模块。传统的单体架构将匹配逻辑与游戏主服务耦合在一起,不仅难以扩展,还会因高并发匹配请求而影响核心业务的稳定性。为了解耦、提升系统弹性,采用微服务架构,将匹配系统独立为一个服务,通过 HTTP 接口与主后端(Backend)及 WebSocket 服务协作。
微服务实现匹配系统
根据前文设计逻辑,使用微服务代替调试用的匹配系统,使功能更加完善。微服务是一个独立的程序,可视为新的 SpringBoot 项目。我们将这个新的 SpringBoot 命名为 Matching System,与之对应的是 Matching Server(匹配的服务器后端)。
当游戏对战的服务器后端(backend Server)获取了两个匹配的玩家信息后,会向 Matching Server 发送一个 HTTP 请求。Matching Server 接收到请求后,开启独立线程进行玩家匹配。匹配逻辑简单,每隔 1s 扫描当前已有的所有玩家,判断 rating 是否相近,若能匹配则将结果返回给 backend Server。
项目结构搭建
修改项目结构,将两个后端改为子项目,新建父级项目 backendcloud。
注意:backendcloud 创建时要引入 Spring Web 依赖。
配置 pom.xml:
<packaging>pom</packaging>
加上 Spring Cloud 依赖:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
在 backendcloud 目录下创建两个模块:MatchingSystem, backend。
接着创建匹配系统子项目 matchingsystem,组 ID 设置为 com.gameforces.matchingsystem。
将父级目录的 pom.xml 中的 Spring Web 依赖剪切到 matchingsystem 中的 pom.xml:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
Matching System 配置
配置 pom.xml,添加 Security 依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>2.7.1</version>
</dependency>
配置端口 resources/application.properties:
编写匹配的服务接口 MatchingService:
package com.kob.matchingsystem.service;
public interface MatchingService {
String addPlayer(Integer userId, Integer rating);
String removePlayer(Integer userId);
}
package com.kob.matchingsystem.service.impl;
import com.kob.matchingsystem.service.MatchingService;
import org.springframework.stereotype.Service;
@Service
public class MatchingServiceImpl implements MatchingService {
@Override
public String addPlayer(Integer userId, Integer rating) {
System.out.println("addPlayer: " + userId + " " + rating);
return "add Player success";
}
@Override
public String removePlayer(Integer userId) {
System.out.println("removePlayer: " + userId);
return "remove Player success";
}
}
package com.kob.matchingsystem.controller;
import com.kob.matchingsystem.service.MatchingService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class MatchingController {
@Autowired
private MatchingService matchingService;
@PostMapping("/player/add/")
public String addPlayer(@RequestParam MultiValueMap<String, String> data) {
Integer userId = Integer.parseInt(data.getFirst("user_id"));
Integer rating = Integer.parseInt(data.getFirst("rating"));
return matchingService.addPlayer(userId, rating);
}
@PostMapping("/player/remove/")
public String removePlayer(@RequestParam MultiValueMap<String, String> data) {
Integer userId = Integer.parseInt(data.getFirst("user_id"));
return matchingService.removePlayer(userId);
}
}
注意:这里用的是 MultiValueMap,即一个键值 key 可以对应多个 value 值。若 URL 返回多个参数,map 只能接受一个 value,而 MultiValueMap 可以处理列表,避免潜在错误。
设置网关
为了防止用户破坏系统,设置访问权限,配置 SecurityConfig:
package com.kob.matchingsystem.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/player/add/", "/player/remove/").hasIpAddress("127.0.0.1")
.antMatchers(HttpMethod.OPTIONS).permitAll()
.anyRequest().authenticated();
}
}
启动入口 MatchingSystemApplication.java:
package com.kob.matchingsystem;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class MatchingSystemApplication {
public static void main(String[] args) {
SpringApplication.run(MatchingSystemApplication.class, args);
}
}
backend 对接
将之前写的 springboot 项目 backend 引入进现在的 backendcloud。
将匹配链接对接到 Matching System
工具:RestTemplate,用于两个 springboot 之间通信。
建立 config 类 RestTemplateConfig.java:
@Configuration
public class RestTemplateConfig {
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
}
backend\consumer\utils\WebSocketServer.java 中编写新的匹配逻辑 startGame:
private void startGame(Integer aId, Integer bId) {
User a = userMapper.selectById(aId);
User b = userMapper.selectById(bId);
Game game = new Game(13, 14, 20, a.getId(), b.getId());
game.createMap();
users.get(a.getId()).game = game;
users.get(b.getId()).game = game;
game.start();
JSONObject respGame = new JSONObject();
respGame.put("a_id", game.getPlayerA().getId());
respGame.put("a_sx", game.getPlayerA().getSx());
respGame.put("a_sy", game.getPlayerA().getSy());
respGame.put("b_id", game.getPlayerB().getId());
respGame.put("b_sx", game.getPlayerB().getSx());
respGame.put("b_sy", game.getPlayerB().getSy());
respGame.put("map", game.getG());
JSONObject respA = new JSONObject();
respA.put("event", "start-matching");
respA.put("opponent_username", b.getUsername());
respA.put("opponent_photo", b.getPhoto());
respA.put("game", respGame);
users.get(a.getId()).sendMessage(respA.toJSONString());
JSONObject respB = new JSONObject();
respB.put("event", "start-matching");
respB.put("opponent_username", a.getUsername());
respB.put("opponent_photo", a.getPhoto());
respB.put("game", respGame);
users.get(b.getId()).sendMessage(respB.toJSONString());
}
开始匹配服务
private void startMatching() {
System.out.println("start matching!");
MultiValueMap<String, String> data = new LinkedMultiValueMap<>();
data.add("user_id", this.user.getId().toString());
data.add("rating", this.user.getRating().toString());
restTemplate.postForObject(addPlayerUrl, data, String.class);
}
删除匹配服务
private void stopMatching() {
System.out.println("stop matching");
MultiValueMap<String, String> data = new LinkedMultiValueMap<>();
data.add("user_id", this.user.getId().toString());
restTemplate.postForObject(removePlayerUrl, data, String.class);
}
实现收到请求后的匹配具体逻辑
思路:把所有当前匹配的用户放在一个数组 (matchinPool) 里,每隔 1s 扫描一遍数组,把 rating 较接近的两名用户匹配在一起。等待时间越长,允许的 rating 差越大。
在 Impl 文件夹里新建 utils 工具包,编写 MatchingPool.java 和 Player.java 类。
MatchingPool.java 继承自 Thread:
package com.kob.matchingsystem.service.impl.utils;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.ReentrantLock;
public class MatchingPool extends Thread {
private static List<Player> players = new ArrayList<>();
private ReentrantLock lock = new ReentrantLock();
public void addPlayer(Integer userId, Integer rating) {
lock.lock();
try {
players.add(new Player(userId, rating, 0));
} finally {
lock.unlock();
}
}
public void removePlayer(Integer userId) {
lock.lock();
try {
List<Player> newPlayers = new ArrayList<>();
for (Player player : players) {
if (!player.getUserId().equals(userId)) {
newPlayers.add(player);
}
}
players = newPlayers;
} finally {
lock.unlock();
}
}
@Override
public void run() {
}
}
MatchingServiceImpl.java:
package com.kob.matchingsystem.service.impl;
import com.kob.matchingsystem.service.MatchingService;
import com.kob.matchingsystem.service.impl.utils.MatchingPool;
import org.springframework.stereotype.Service;
@Service
public class MatchingServiceImpl implements MatchingService {
public final static MatchingPool matchingPool = new MatchingPool();
@Override
public String addPlayer(Integer userId, Integer rating) {
System.out.println("addPlayer: " + userId + " " + rating);
matchingPool.addPlayer(userId, rating);
return "add Player success";
}
@Override
public String removePlayer(Integer userId) {
System.out.println("removePlayer: " + userId);
matchingPool.removePlayer(userId);
return "remove Player success";
}
}
匹配逻辑:无限循环,周期性执行,sleep(1000)。若没有匹配的人选,则等待时间++;若有匹配的人选则进行匹配。匹配的 rating 差会随着等待时间而增加(rating 差每等待 1s 则 *10)。
private void matchPlayers() {
boolean[] used = new boolean[players.size()];
for (int i = 0; i < players.size(); i++) {
if (used[i]) continue;
for (int j = i + 1; j < players.size(); j++) {
if (used[j]) continue;
Player a = players.get(i), b = players.get(j);
if (checkMatched(a, b)) {
used[i] = used[j] = true;
sendResult(a, b);
break;
}
}
}
List<Player> newPlayers = new ArrayList<>();
for (int i = 0; i < players.size(); i++) {
if (!used[i]) {
newPlayers.add(players.get(i));
}
}
players = newPlayers;
}
checkMatch 判断两名玩家是否匹配,考虑其等待时间:ratingDelta <= min(waitingTimea, waitingTimeb) * 10。
private boolean checkMatched(Player a, Player b) {
int ratingDelta = Math.abs(a.getRating() - b.getRating());
int waitingTime = Math.min(a.getWaitingTime(), b.getWaitingTime());
return ratingDelta <= waitingTime * 10;
}
接收匹配成功的信息
在 backend 端写一个接受 MatchingSystem 端匹配成功信息的 Service 和 Controller。
GameStartController.java:
@RestController
public class StartGameController {
@Autowired
private StartGameService startGameService;
@PostMapping("/pk/start/game/")
public String startGame(@RequestParam MultiValueMap<String, String> data) {
Integer aId = Integer.parseInt(Objects.requireNonNull(data.getFirst("a_id")));
Integer bId = Integer.parseInt(Objects.requireNonNull(data.getFirst("b_id")));
return startGameService.startGame(aId, bId);
}
}
GameStartServiceImpl.java:
@Service
public class StartGameServiceImpl implements StartGameService {
@Override
public String startGame(Integer aId, Integer bId) {
System.out.println("start game: " + aId + " " + bId);
WebSocketServer.startGame(aId, bId);
return "start game successfully";
}
}
注意:要把路由 /pk/start/game/ 放行,只能本地访问。
SecurityConfig.java:
Matching System 调用 ws 端的接口
为了能让 Spring 里面的 Bean 注入进来,需要在 MatchingPool.java 里加上 @Component。
@Component
public class MatchingPool extends Thread {
private static RestTemplate restTemplate;
@Autowired
public void setRestTemplate(RestTemplate restTemplate) {
MatchingPool.restTemplate = restTemplate;
}
private void sendResult(Player a, Player b) {
MultiValueMap<String, String> data = new LinkedMultiValueMap<>();
data.add("a_id", a.getUserId().toString());
data.add("b_id", b.getUserId().toString());
restTemplate.postForObject(startGameURL, data, String.class);
}
}
总结
经过以上设计与实现,成功构建了一个基于微服务的匹配系统,并顺利将其对接至主后端和 WebSocket 服务。
相关免费在线工具
- Keycode 信息
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
- Escape 与 Native 编解码
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
- JavaScript / HTML 格式化
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
- JavaScript 压缩与混淆
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
- 加密/解密文本
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
- Gemini 图片去水印
基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online