使用 Vert.X 构建足球运动员微服务
1. 引言
在开发过程中,我们常常需要构建能够高效处理数据的微服务。本文将详细介绍如何使用 Vert.X 构建一个用于管理足球运动员注册表的微服务,包括创建源代码、数据访问层的构建、RESTful 服务的实现、测试代码的编写,以及如何结合 RxJava 进一步优化。
2. 创建源代码
2.1 项目骨架生成
使用 Vert.x 项目生成器工具(http://start.vertx.io/ )获取项目骨架。该微服务需要展示允许执行 CRUD 操作的 API。为实现此微服务,我们将使用以下组件:
-
Core
:包含核心功能,如对 HTTP 的支持。
-
Config
:为 Vert.X 应用程序提供可扩展的配置方式。
-
Web
:包含构建 Web 应用程序和 HTTP 微服务所需的实用工具。
-
JDBC 客户端
:使开发人员能够以异步 API 的方式与任何符合 JDBC 标准的数据库进行通信。
设置项目的 Maven Group 名称为
com.packtpub.vertx
,Artifact 为
footballplayermicroservice
,点击“Generate Project”创建并下载包含项目骨架的 ZIP 文件。将文件解压到指定目录,然后使用喜欢的 IDE(如 Eclipse、NetBeans、IntelliJ 等)打开 Maven 项目。
2.2 配置 Maven pom.xml 文件
项目的核心是 Maven 的
pom.xml
文件,它包含了实现微服务所需的所有依赖项。项目使用
vertx-stack-depchain
来正确管理依赖项:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-stack-depchain</artifactId>
<version>${vertx.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
同时,需要添加 PostgreSQL JDBC 驱动依赖以连接存储足球运动员信息的数据库:
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>9.4.1212</version>
</dependency>
2.3 首次构建与测试
使用以下命令进行首次构建:
$ mvn clean package
构建完成后,使用以下命令测试应用程序是否正常运行:
$ java -jar $PROJECT_HOME/footballplayermicroservice/target/footballplayermicroservice-1.0.0-SNAPSHOT-fat.jar
其中,
$PROJECT_HOME
是解压 Vert.x 项目生成器工具生成项目的路径。运行后,控制台将显示如下输出:
HTTP server started on http://localhost:8080
Oct 11, 2018 10:41:06 AM
io.vertx.core.impl.launcher.commands.VertxIsolatedDeployer
INFO: Succeeded in deploying verticle
将
http://localhost:8080
输入浏览器,将看到消息:
Hello from Vert.x!
。使用
Ctrl + C
停止应用程序,开始更新项目。
3. 数据访问层
3.1 创建数据传输对象
由于 Vert.X 没有使用 JPA 规范的模块,我们不创建实体,而是创建一个数据传输对象
FootballPlayer
来在数据库和程序之间传输足球运动员的属性:
package com.packtpub.vertx.footballplayermicroservice.model;
import java.io.Serializable;
import java.math.BigInteger;
public class FootballPlayer implements Serializable {
private static final long serialVersionUID = -92346781936044228L;
private Integer id;
private String name;
private String surname;
private int age;
private String team;
private String position;
private BigInteger price;
public FootballPlayer() {
}
public FootballPlayer(Integer id, String name, String surname, int age,
String team, String position, BigInteger price) {
this.id = id;
this.name = name;
this.surname = surname;
this.age = age;
this.team = team;
this.position = position;
this.price = price;
}
// Getters and Setters
...
}
3.2 创建配置文件
在
src/main/conf
目录下创建
my-application-conf.json
文件,包含建立与数据库的 JDBC 连接所需的参数:
{
"url": "jdbc:postgresql://localhost:5532/football_players_registry",
"driver_class": "org.postgresql.Driver",
"user": "postgres",
"password": "postgresPwd"
}
在生产环境中,必须对 JDBC 凭证进行掩码处理。
3.3 创建数据库交互类
在
src/main/resources
文件夹中创建
schema.sql
和
data.sql
文件,用于创建所需的表结构并向数据库预加载数据。代码可在 GitHub 仓库中找到。
最后,构建一个负责与数据库交互的类
FootballPlayerDAO
,以执行以下操作:
- 创建数据库表
- 预加载一组数据
- CRUD 操作以及
findAll
方法
以下是该类的部分代码:
public class FootballPlayerDAO {
public Future<FootballPlayer> insert(SQLConnection connection,
FootballPlayer footballPlayer, boolean closeConnection) {
Future<FootballPlayer> future = Future.future();
String sql = "INSERT INTO football_player (name, surname, age, team, " +
"position, price) VALUES (?, ?, ?, ?, ?, ?)";
connection.updateWithParams(sql, new
JsonArray().add(footballPlayer.getName())
.add(footballPlayer.getSurname())
.add(footballPlayer.getAge()).add(footballPlayer.getTeam())
.add(footballPlayer.getPosition())
.add(footballPlayer.getPrice().intValue()),
ar -> {
if (closeConnection) {
connection.close();
}
future.handle(ar.map(res -> new
FootballPlayer(res.getKeys().getInteger(0),
footballPlayer.getName(), footballPlayer.getSurname(),
footballPlayer.getAge(), footballPlayer.getTeam(),
footballPlayer.getPosition(), footballPlayer.getPrice())));
});
return future;
}
public Future<SQLConnection> connect(JDBCClient jdbc) {
Future<SQLConnection> future = Future.future();
jdbc.getConnection(ar -> future.handle(ar.map(c -> c.setOptions(
new SQLOptions().setAutoGeneratedKeys(true))))
);
return future;
}
...
}
Vert.X 使用与传统 Java JDBC API 不同的方法进行数据库交互,所有操作都是异步的,并由
Future
类处理。
2.4 整体流程
graph TD;
A[使用 Vert.x 项目生成器获取项目骨架] --> B[设置 Maven 依赖];
B --> C[构建项目];
C --> D[测试应用程序];
D --> E[停止应用程序并更新项目];
E --> F[创建数据传输对象];
F --> G[创建配置文件];
G --> H[创建数据库交互类];
4. RESTful 服务
4.1 辅助类 ActionHelper
创建一个辅助类
ActionHelper
来帮助构建与特定 HTTP 动词相关的响应:
public class ActionHelper {
private static <T> Handler<AsyncResult<T>>
writeJsonResponse(RoutingContext context, int status) {
return ar -> {
if (ar.failed()) {
if (ar.cause() instanceof NoSuchElementException) {
context.response().setStatusCode(404).end(ar.cause().getMessage());
} else {
context.fail(ar.cause());
}
} else {
context.response().setStatusCode(status).putHeader("content-type",
"application/json;charset=utf-8")
.end(Json.encodePrettily(ar.result()));
}
};
}
public static <T> Handler<AsyncResult<T>> ok(RoutingContext rc) {
return writeJsonResponse(rc, 200);
}
public static <T> Handler<AsyncResult<T>> created(RoutingContext rc) {
return writeJsonResponse(rc, 201);
}
public static Handler<AsyncResult<Void>> noContent(RoutingContext rc) {
return ar -> {
if (ar.failed()) {
if (ar.cause() instanceof NoSuchElementException) {
rc.response().setStatusCode(404).end(ar.cause().getMessage());
} else {
rc.fail(ar.cause());
}
} else {
rc.response().setStatusCode(204).end();
}
};
}
private ActionHelper() {
}
}
4.2 主类 FootballPlayerVerticle
创建
FootballPlayerVerticle
类来实现路由并暴露 API。在该类中,我们将执行以下操作:
1. 创建一个监听 8080 端口的 HTTP 服务器。
2. 使其能够处理我们 API 路径的请求。
3. 定义创建数据库表和预加载数据的方式。
public class FootballPlayerVerticle extends AbstractVerticle {
private JDBCClient jdbc;
@Override
public void start(Future<Void> fut) {
Router router = Router.router(vertx);
router.route("/").handler(routingContext -> {
HttpServerResponse response = routingContext.response();
response.putHeader("content-type", "text/html")
.end("<h1>Football Player Vert.x 3 microservice application</h1>");
});
router.get("/footballplayer").handler(this::getAll);
router.get("/footballplayer/show/:id").handler(this::getOne);
router.route("/footballplayer*").handler(BodyHandler.create());
router.post("/footballplayer/save").handler(this::addOne);
router.delete("/footballplayer/delete/:id").handler(this::deleteOne);
router.put("/footballplayer/update/:id").handler(this::updateOne);
ConfigStoreOptions fileStore = new
ConfigStoreOptions().setType("file")
.setFormat("json").setConfig(new JsonObject().put("path",
"src/main/conf/my-application-conf.json"));
ConfigRetrieverOptions options = new
ConfigRetrieverOptions().addStore(fileStore);
ConfigRetriever retriever = ConfigRetriever.create(vertx, options);
ConfigRetriever.getConfigAsFuture(retriever).compose(config -> {
jdbc = JDBCClient.createShared(vertx, config, "Players-List");
FootballPlayerDAO dao = new FootballPlayerDAO();
return dao.connect(jdbc).compose(connection -> {
Future<Void> future = Future.future();
createTableIfNeeded(connection).compose(this::createSomeDataIfNone)
.setHandler(x -> {
connection.close();
future.handle(x.mapEmpty());
});
return future;
})
.compose(v -> createHttpServer(config, router));
})
.setHandler(fut);
}
private Future<Void> createHttpServer(JsonObject config, Router router) {
Future<Void> future = Future.future();
vertx.createHttpServer().requestHandler(router::accept)
.listen(config.getInteger("HTTP_PORT", 8080),
res -> future.handle(res.mapEmpty()));
return future;
}
private Future<SQLConnection> createTableIfNeeded(SQLConnection connection) {
FootballPlayerDAO dao = new FootballPlayerDAO();
return dao.createTableIfNeeded(vertx.fileSystem(), connection);
}
private Future<SQLConnection> createSomeDataIfNone(SQLConnection connection) {
FootballPlayerDAO dao = new FootballPlayerDAO();
return dao.createSomeDataIfNone(vertx.fileSystem(), connection);
}
...
}
4.3 路由处理示例
以
/footballplayer/show/:id
路径为例,其映射到
getOne
方法:
private void getOne(RoutingContext rc) {
String id = rc.pathParam("id");
FootballPlayerDAO dao = new FootballPlayerDAO();
dao.connect(jdbc).compose(connection -> dao.queryOne(connection, id)).setHandler(ok(rc));
}
4.4 测试 API
启动应用程序:
$ java -jar $PROJECT_HOME/target/footballplayermicroservice-1.0.0-SNAPSHOT-fat.jar
调用 API:
$ curl http://localhost:8080/footballplayer/show/1 | json_pp
将返回足球运动员信息:
{
"team": "Paris Saint Germain",
"id": 1,
"name": "Gianluigi",
"age": 40,
"price": 2,
"surname": "Buffon",
"position": "goalkeeper"
}
4.5 RESTful 服务流程
graph TD;
A[创建 ActionHelper 辅助类] --> B[创建 FootballPlayerVerticle 主类];
B --> C[配置路由];
C --> D[处理请求];
D --> E[与数据库交互];
E --> F[返回响应];
5. 创建测试代码
5.1 添加依赖
在 Maven
pom.xml
文件中添加 JUnit 相关依赖:
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-unit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-junit5</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-launcher</artifactId>
<version>${junit-platform-launcher.version}</version>
<scope>test</scope>
</dependency>
5.2 创建测试类
创建
TestFootballPlayerVerticle
类来验证 API 的结果:
@ExtendWith(VertxExtension.class)
public class TestFootballPlayerVerticle {
@BeforeEach
void deploy_verticle(Vertx vertx, VertxTestContext testContext) {
vertx.deployVerticle(new FootballPlayerVerticle(), testContext.
succeeding(id -> testContext.completeNow()));
}
@Test
@DisplayName("Should start a Web Server on port 8080 and the GET all API"
+ "returns an array of 24 elements")
@Timeout(value = 10, timeUnit = TimeUnit.SECONDS)
void findAll(Vertx vertx, VertxTestContext testContext) throws Throwable {
System.out.println("FIND ALL *****************");
vertx.createHttpClient().getNow(8080, "localhost",
"/footballplayer",
response -> testContext.verify(() -> {
assertTrue(response.statusCode() == 200);
response.bodyHandler(body -> {
JsonArray array = new JsonArray(body);
assertTrue(23 == array.size());
testContext.completeNow();
});
}));
}
@Test
@DisplayName(
"Should start a Web Server on port 8080 and, using the POST API,"
+ "insert a new football player")
@Timeout(value = 10, timeUnit = TimeUnit.SECONDS)
public void create(Vertx vertx, VertxTestContext context) {
System.out.println("CREATE *****************");
final String json = Json.encodePrettily(new FootballPlayer(null,
"Mauro", "Vocale", 38,
"Juventus", "central midfielder", new BigInteger("100")));
final String length = Integer.toString(json.length());
vertx.createHttpClient().post(8080, "localhost",
"/footballplayer/save")
.putHeader("content-type", "application/json")
.putHeader("content-length", length)
.handler(response -> {
assertTrue(response.statusCode() == 201);
assertTrue(response.headers().get("content-type").contains("application/json"));
response.bodyHandler(body -> {
final FootballPlayer footballPlayer =
Json.decodeValue(
body.toString(), FootballPlayer.class);
assertTrue(footballPlayer.getName().equalsIgnoreCase("Mauro"));
assertTrue(footballPlayer.getAge() == 38);
assertTrue(footballPlayer.getId() != null);
context.completeNow();
});
}).write(json).end();
}
...
}
5.3 运行测试
使用以下命令运行测试:
$ mvn test
测试通过后将显示:
[INFO] Tests run: 5, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.677 s - in com.packtpub.vertx.footballplayermicroservice.TestFootballPlayerVerticle
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 5, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
5.4 测试代码流程
graph TD;
A[添加 JUnit 依赖] --> B[创建测试类];
B --> C[部署 Verticle];
C --> D[执行测试方法];
D --> E[验证结果];
6. 结合 RxJava
6.1 添加依赖
在 Maven
pom.xml
文件中添加 RxJava 依赖:
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-rx-java2</artifactId>
</dependency>
6.2 创建反应式类
创建三个新类以反应式方式实现之前描述的相同方法:
-
FootballPlayerReactiveVerticle
-
ActionHelperReactive
-
FootballPlayerReactiveDAO
6.3 修改方法
以
createHttpServer
方法为例,修改为返回 Rx
Completable
类:
private Completable createHttpServerReactive(JsonObject config, Router router) {
return vertx.createHttpServer().requestHandler(router::accept)
.rxListen(config.getInteger("HTTP_PORT", 8080)).toCompletable();
}
修改数据库连接方法返回 Rx
Single
:
public Single<SQLConnection> connectReactive(JDBCClient jdbc) {
return jdbc.rxGetConnection().map(c -> c.setOptions(new
SQLOptions().setAutoGeneratedKeys(true)));
}
6.4 反应式交互示例
读取包含创建数据库表和填充数据指令的文件:
public Single<SQLConnection> createTableIfNeeded(FileSystem fileSystem,
SQLConnection connection) {
return fileSystem.rxReadFile("schema.sql").map(Buffer::toString)
.flatMapCompletable(connection::rxExecute).toSingleDefault(connection);
}
6.5 最终管道
构建 HTTP 服务器的最终管道:
retriever.rxGetConfig().doOnSuccess(config -> jdbc =
JDBCClient.createShared(vertx,
config, "My-Reading-List"))
.flatMap(config -> dao.connect(jdbc)
.flatMap(connection -> this.createTableIfNeeded(connection)
.flatMap(this::createSomeDataIfNone)
.doAfterTerminate(connection::close))
.map(x -> config))
.flatMapCompletable(config -> createHttpServer(config, router))
.subscribe(CompletableHelper.toObserver(fut));
6.6 RxJava 结合流程
graph TD;
A[添加 RxJava 依赖] --> B[创建反应式类];
B --> C[修改方法为反应式];
C --> D[进行反应式交互];
D --> E[构建最终管道];
E --> F[订阅执行];
7. 总结
通过以上步骤,我们成功使用 Vert.X 构建了一个足球运动员微服务,包括源代码的创建、数据访问层的实现、RESTful 服务的搭建、测试代码的编写,以及结合 RxJava 实现反应式编程。每个部分都有其特定的功能和实现方式,通过合理的配置和编码,我们可以构建出高效、稳定的微服务。在实际开发中,可以根据需求进一步扩展和优化该微服务。
8. 关键技术点分析
8.1 Vert.X 异步编程模型
Vert.X 采用异步、非阻塞的编程模型,这在处理高并发和 I/O 密集型任务时具有显著优势。在数据库交互和 HTTP 服务器处理请求的过程中,大量使用了
Future
类来处理异步操作。例如,在
FootballPlayerDAO
类的
insert
方法中:
public Future<FootballPlayer> insert(SQLConnection connection,
FootballPlayer footballPlayer, boolean closeConnection) {
Future<FootballPlayer> future = Future.future();
String sql = "INSERT INTO football_player (name, surname, age, team, " +
"position, price) VALUES (?, ?, ?, ?, ?, ?)";
connection.updateWithParams(sql, new
JsonArray().add(footballPlayer.getName())
.add(footballPlayer.getSurname())
.add(footballPlayer.getAge()).add(footballPlayer.getTeam())
.add(footballPlayer.getPosition())
.add(footballPlayer.getPrice().intValue()),
ar -> {
if (closeConnection) {
connection.close();
}
future.handle(ar.map(res -> new
FootballPlayer(res.getKeys().getInteger(0),
footballPlayer.getName(), footballPlayer.getSurname(),
footballPlayer.getAge(), footballPlayer.getTeam(),
footballPlayer.getPosition(), footballPlayer.getPrice())));
});
return future;
}
在这个方法中,
updateWithParams
是一个异步操作,通过回调函数处理操作结果,并将结果封装在
Future
中返回。这种方式避免了线程阻塞,提高了系统的并发处理能力。
8.2 RxJava 与 Vert.X 的结合
RxJava 为 Vert.X 带来了更多强大的功能,它提供了丰富的操作符,能够更灵活地处理异步数据流。结合 RxJava 后,原本使用
Future
处理的异步操作可以使用 RxJava 的
Single
、
Completable
等类来处理。例如,在
FootballPlayerReactiveDAO
类中,将数据库连接方法修改为返回
Single
:
public Single<SQLConnection> connectReactive(JDBCClient jdbc) {
return jdbc.rxGetConnection().map(c -> c.setOptions(new
SQLOptions().setAutoGeneratedKeys(true)));
}
通过这种方式,可以利用 RxJava 的操作符对数据流进行转换、过滤、合并等操作,增强代码的可读性和可维护性。
8.3 RESTful 服务设计
RESTful 服务的设计遵循了 REST 架构风格,通过 HTTP 动词(GET、POST、PUT、DELETE)来实现资源的 CRUD 操作。在
FootballPlayerVerticle
类中,通过
Router
来定义路由,将不同的 HTTP 请求映射到相应的处理方法:
router.get("/footballplayer").handler(this::getAll);
router.get("/footballplayer/show/:id").handler(this::getOne);
router.post("/footballplayer/save").handler(this::addOne);
router.delete("/footballplayer/delete/:id").handler(this::deleteOne);
router.put("/footballplayer/update/:id").handler(this::updateOne);
这种设计使得服务的接口清晰,易于理解和使用。
9. 性能优化建议
9.1 数据库连接池
在高并发场景下,频繁地创建和销毁数据库连接会带来较大的性能开销。可以使用数据库连接池来管理数据库连接,减少连接创建和销毁的次数。例如,在使用 Vert.X 的
JDBCClient
时,可以通过配置连接池参数来优化性能:
ConfigRetriever.getConfigAsFuture(retriever).compose(config -> {
jdbc = JDBCClient.createShared(vertx, config, "Players-List");
// 可以进一步配置连接池参数
// 例如:设置最大连接数、最小空闲连接数等
return ...;
})
9.2 缓存机制
对于一些频繁访问且数据更新不频繁的资源,可以使用缓存机制来减少数据库查询次数。例如,可以使用 Redis 等缓存服务器,将查询结果缓存起来,下次请求时先从缓存中获取数据,如果缓存中不存在再查询数据库。
9.3 异步处理优化
在处理复杂业务逻辑时,尽量将耗时的操作异步化,避免阻塞主线程。例如,在处理数据库查询时,使用异步查询方法,让主线程可以继续处理其他请求。
10. 常见问题及解决方案
10.1 数据库连接失败
- 问题描述 :应用程序无法连接到数据库。
- 可能原因 :数据库服务未启动、JDBC 配置参数错误、数据库权限不足等。
-
解决方案
:检查数据库服务是否正常运行,确认
my-application-conf.json文件中的 JDBC 配置参数是否正确,检查数据库用户的权限。
10.2 测试用例失败
- 问题描述 :运行测试用例时部分或全部失败。
- 可能原因 :服务未正常启动、数据库数据不一致、测试代码逻辑错误等。
- 解决方案 :检查服务是否正常启动,确认数据库中的数据是否符合测试用例的预期,仔细检查测试代码的逻辑。
10.3 性能问题
- 问题描述 :应用程序响应缓慢,处理请求的时间过长。
- 可能原因 :数据库查询性能不佳、代码中存在阻塞操作、服务器资源不足等。
- 解决方案 :优化数据库查询语句,检查代码中是否存在阻塞操作并将其异步化,检查服务器的 CPU、内存、磁盘 I/O 等资源使用情况,必要时进行扩容。
11. 总结与展望
通过本文的介绍,我们详细了解了如何使用 Vert.X 构建一个足球运动员微服务,并结合 RxJava 实现反应式编程。从项目的初始化、数据访问层的实现、RESTful 服务的搭建到测试代码的编写,每个步骤都有清晰的操作说明和代码示例。
在未来的开发中,可以进一步拓展该微服务的功能,例如添加用户认证和授权机制,提高服务的安全性;集成监控和日志系统,方便对服务的运行状态进行监控和故障排查;使用容器化技术(如 Docker、Kubernetes)进行部署,提高服务的可扩展性和可靠性。
同时,不断学习和掌握新的技术和工具,将其应用到实际项目中,以提升微服务的性能和质量。希望本文能够为开发者在构建微服务时提供一些参考和帮助。
相关表格
组件功能表格
| 组件名称 | 功能描述 |
|---|---|
| Core | 包含核心功能,如对 HTTP 的支持 |
| Config | 为 Vert.X 应用程序提供可扩展的配置方式 |
| Web | 包含构建 Web 应用程序和 HTTP 微服务所需的实用工具 |
| JDBC 客户端 | 使开发人员能够以异步 API 的方式与任何符合 JDBC 标准的数据库进行通信 |
测试用例表格
| 测试用例名称 | 测试目的 | 预期结果 |
|---|---|---|
| findAll | 验证 GET 所有足球运动员 API 是否返回 24 个元素的数组 | 状态码为 200,返回数组大小为 24 |
| create | 验证 POST API 是否能插入新的足球运动员 | 状态码为 201,返回的足球运动员信息正确 |
性能优化策略表格
| 优化策略 | 具体操作 |
|---|---|
| 数据库连接池 | 配置连接池参数,如最大连接数、最小空闲连接数等 |
| 缓存机制 | 使用 Redis 等缓存服务器,缓存频繁访问的数据 |
| 异步处理优化 | 将耗时操作异步化,避免阻塞主线程 |
常见问题及解决方案表格
| 问题描述 | 可能原因 | 解决方案 |
|---|---|---|
| 数据库连接失败 | 数据库服务未启动、JDBC 配置参数错误、数据库权限不足等 | 检查数据库服务状态,确认 JDBC 配置参数,检查数据库用户权限 |
| 测试用例失败 | 服务未正常启动、数据库数据不一致、测试代码逻辑错误等 | 检查服务启动情况,确认数据库数据,检查测试代码逻辑 |
| 性能问题 | 数据库查询性能不佳、代码中存在阻塞操作、服务器资源不足等 | 优化数据库查询语句,异步化阻塞操作,检查服务器资源使用情况 |
超级会员免费看
100

被折叠的 条评论
为什么被折叠?



