Skip to content

Commit 716bbc8

Browse files
feat(api): 优化玩家数据API并发处理和线程安全
- 为PlayerDataApiController添加插件依赖和并发查询限制 - 使用CompletableFuture和Bukkit调度器确保主线程调用Bukkit API - 添加查询超时机制(3秒)和并发信号量控制(最多5个并发) - 移除对离线玩家数据的查询支持,仅处理在线玩家 - 增加友好的错误提示和日志记录 refactor(database): 提升数据库并发性能和连接稳定性 - 增加数据库线程池大小从3到8以处理更多并发操作 - 为SQLite连接启用WAL模式、缓存优化等性能配置 - 设置busy_timeout为30秒,避免锁等待超时 - 为每个数据库连接单独设置PRAGMA busy_timeout - 添加WAL自动检查点配置提升写入性能 perf(sync): 调整同步任务处理器启动延迟和超时设置 - 任务处理器启动延迟从0秒调整为10秒,避免启动时数据库过载 - 获取待处理任务的查询超时时间从5秒增加到15秒 - 为SQL查询显式设置10秒查询超时 - 增强超时异常处理,避免任务处理中断 fix(whitelist): 修复PlayerDataApiController插件依赖注入 - 在WhitelistSystem中为PlayerDataApiController正确传递Plugin实例
1 parent a585b5c commit 716bbc8

4 files changed

Lines changed: 85 additions & 34 deletions

File tree

src/main/java/com/xaoxiao/convenientaccess/api/PlayerDataApiController.java

Lines changed: 50 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
import java.util.HashMap;
66
import java.util.List;
77
import java.util.Map;
8+
import java.util.concurrent.CompletableFuture;
9+
import java.util.concurrent.TimeUnit;
810

911
import org.bukkit.Bukkit;
1012
import org.bukkit.Location;
@@ -26,18 +28,26 @@
2628
/**
2729
* 玩家数据API控制器
2830
* 处理玩家详细数据查询的API请求
31+
* 注意: 所有Bukkit API调用都必须在主线程中执行
2932
*/
3033
public class PlayerDataApiController {
3134
private static final Logger logger = LoggerFactory.getLogger(PlayerDataApiController.class);
3235

3336
private final Gson gson;
37+
private final org.bukkit.plugin.Plugin plugin;
3438

35-
public PlayerDataApiController() {
39+
// 并发查询限制
40+
private final java.util.concurrent.Semaphore querySemaphore = new java.util.concurrent.Semaphore(5);
41+
42+
public PlayerDataApiController(org.bukkit.plugin.Plugin plugin) {
3643
this.gson = new Gson();
44+
this.plugin = plugin;
3745
}
3846

3947
/**
4048
* 处理GET /api/v1/player?name=玩家名 - 获取玩家详细数据
49+
* 注意: 必须在主线程中调用Bukkit API以避免线程安全问题
50+
* 优化: 添加并发限制和更短的超时时间
4151
*/
4252
public void handleGetPlayerData(HttpServletRequest request, HttpServletResponse response) throws IOException {
4353
// 从查询参数中获取玩家名
@@ -49,29 +59,49 @@ public void handleGetPlayerData(HttpServletRequest request, HttpServletResponse
4959
return;
5060
}
5161

52-
// 首先尝试通过玩家名获取在线玩家
53-
Player onlinePlayer = Bukkit.getPlayerExact(playerName);
62+
// 尝试获取查询许可(最多5个并发查询)
63+
if (!querySemaphore.tryAcquire(100, TimeUnit.MILLISECONDS)) {
64+
logger.warn("查询请求过多,拒绝查询玩家: {}", playerName);
65+
sendJsonResponse(response, 429, ApiResponse.error("查询请求过多,请稍后再试"));
66+
return;
67+
}
5468

55-
if (onlinePlayer != null) {
56-
// 玩家在线,获取完整数据
57-
PlayerData playerData = collectOnlinePlayerData(onlinePlayer);
58-
sendJsonResponse(response, 200, ApiResponse.success(playerData, "成功获取玩家数据(在线)"));
59-
logger.info("成功获取在线玩家数据: {}", playerName);
60-
} else {
61-
// 玩家离线,尝试获取离线玩家数据
62-
@SuppressWarnings("deprecation")
63-
OfflinePlayer offlinePlayer = Bukkit.getOfflinePlayer(playerName);
69+
try {
70+
// 使用CompletableFuture在主线程中执行Bukkit API调用
71+
CompletableFuture<PlayerData> future = new CompletableFuture<>();
72+
73+
Bukkit.getScheduler().runTask(plugin, () -> {
74+
try {
75+
// 只查询在线玩家
76+
Player onlinePlayer = Bukkit.getPlayerExact(playerName);
77+
78+
if (onlinePlayer != null && onlinePlayer.isOnline()) {
79+
// 玩家在线,获取完整数据
80+
PlayerData playerData = collectOnlinePlayerData(onlinePlayer);
81+
future.complete(playerData);
82+
} else {
83+
future.completeExceptionally(new IllegalArgumentException("玩家不在线或不存在"));
84+
}
85+
} catch (Exception e) {
86+
future.completeExceptionally(e);
87+
}
88+
});
89+
90+
// 缩短超时时间到3秒 - 如果服务器TPS不健康,快速失败
91+
PlayerData playerData = future.get(3, TimeUnit.SECONDS);
92+
sendJsonResponse(response, 200, ApiResponse.success(playerData, "成功获取玩家数据"));
93+
logger.debug("成功获取在线玩家数据: {}", playerName);
6494

65-
if (offlinePlayer.hasPlayedBefore()) {
66-
PlayerData playerData = collectOfflinePlayerData(offlinePlayer);
67-
sendJsonResponse(response, 200, ApiResponse.success(playerData, "成功获取玩家数据(离线)"));
68-
logger.info("成功获取离线玩家数据: {}", playerName);
69-
} else {
70-
sendJsonResponse(response, 404, ApiResponse.notFound("玩家不存在或从未登录过服务器"));
71-
logger.warn("玩家不存在: {}", playerName);
72-
}
95+
} finally {
96+
querySemaphore.release();
7397
}
7498

99+
} catch (IllegalArgumentException e) {
100+
sendJsonResponse(response, 404, ApiResponse.notFound(e.getMessage()));
101+
logger.debug("玩家不在线: {}", playerName);
102+
} catch (java.util.concurrent.TimeoutException e) {
103+
logger.warn("获取玩家数据超时(3秒): {} - 服务器TPS可能不健康", playerName);
104+
sendJsonResponse(response, 504, ApiResponse.error("服务器繁忙,请稍后重试(TPS过低)"));
75105
} catch (Exception e) {
76106
logger.error("获取玩家数据时发生错误: {}", playerName, e);
77107
sendJsonResponse(response, 500, ApiResponse.error("获取玩家数据失败: " + e.getMessage()));

src/main/java/com/xaoxiao/convenientaccess/database/DatabaseManager.java

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,24 @@
11
package com.xaoxiao.convenientaccess.database;
22

3-
import com.xaoxiao.convenientaccess.ConvenientAccessPlugin;
4-
import org.slf4j.Logger;
5-
import org.slf4j.LoggerFactory;
6-
73
import java.io.File;
84
import java.io.IOException;
95
import java.io.InputStream;
10-
import java.nio.file.Files;
11-
import java.nio.file.Path;
12-
import java.sql.*;
6+
import java.sql.Connection;
7+
import java.sql.DriverManager;
8+
import java.sql.PreparedStatement;
9+
import java.sql.ResultSet;
10+
import java.sql.SQLException;
11+
import java.sql.Statement;
1312
import java.util.concurrent.CompletableFuture;
1413
import java.util.concurrent.ExecutorService;
1514
import java.util.concurrent.Executors;
1615
import java.util.concurrent.atomic.AtomicBoolean;
1716

17+
import org.slf4j.Logger;
18+
import org.slf4j.LoggerFactory;
19+
20+
import com.xaoxiao.convenientaccess.ConvenientAccessPlugin;
21+
1822
/**
1923
* SQLite数据库管理器
2024
* 负责数据库连接池管理、初始化、迁移和事务管理
@@ -33,7 +37,8 @@ public class DatabaseManager {
3337
public DatabaseManager(ConvenientAccessPlugin plugin) {
3438
this.plugin = plugin;
3539
this.databasePath = plugin.getDataFolder().getAbsolutePath() + File.separator + "whitelist.db";
36-
this.executorService = Executors.newFixedThreadPool(3, r -> {
40+
// 增加线程池大小以处理更多并发数据库操作
41+
this.executorService = Executors.newFixedThreadPool(8, r -> {
3742
Thread thread = new Thread(r, "DatabaseManager-Thread");
3843
thread.setDaemon(true);
3944
return thread;
@@ -54,13 +59,15 @@ public CompletableFuture<Boolean> initialize() {
5459

5560
// 创建数据库连接
5661
try (Connection connection = getConnection()) {
57-
// 启用外键约束
62+
// 启用外键约束和优化并发性能
5863
try (Statement stmt = connection.createStatement()) {
5964
stmt.execute("PRAGMA foreign_keys = ON");
6065
stmt.execute("PRAGMA journal_mode = WAL");
6166
stmt.execute("PRAGMA synchronous = NORMAL");
6267
stmt.execute("PRAGMA cache_size = 10000");
6368
stmt.execute("PRAGMA temp_store = MEMORY");
69+
stmt.execute("PRAGMA busy_timeout = 30000"); // 设置30秒的锁等待超时
70+
stmt.execute("PRAGMA wal_autocheckpoint = 1000"); // WAL自动检查点
6471
}
6572

6673
// 检查数据库版本
@@ -90,7 +97,14 @@ public CompletableFuture<Boolean> initialize() {
9097
* 获取数据库连接
9198
*/
9299
public Connection getConnection() throws SQLException {
93-
return DriverManager.getConnection("jdbc:sqlite:" + databasePath);
100+
Connection conn = DriverManager.getConnection("jdbc:sqlite:" + databasePath);
101+
// 为每个连接设置 busy_timeout,处理并发锁等待
102+
try (Statement stmt = conn.createStatement()) {
103+
stmt.execute("PRAGMA busy_timeout = 30000"); // 30秒超时
104+
} catch (SQLException e) {
105+
logger.warn("设置 busy_timeout 失败: {}", e.getMessage());
106+
}
107+
return conn;
94108
}
95109

96110
/**

src/main/java/com/xaoxiao/convenientaccess/sync/SyncTaskManager.java

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -384,11 +384,14 @@ public CompletableFuture<Long> scheduleUuidUpdate(String name, String uuid) {
384384
* 启动任务处理器
385385
*/
386386
private void startTaskProcessor() {
387+
// 延迟10秒后开始处理任务,避免启动时数据库繁忙
387388
// 每5秒检查一次待处理任务
388-
scheduler.scheduleWithFixedDelay(this::processPendingTasks, 0, 5, TimeUnit.SECONDS);
389+
scheduler.scheduleWithFixedDelay(this::processPendingTasks, 10, 5, TimeUnit.SECONDS);
389390

390-
// 每30秒清理已完成的任务
391+
// 延迟30秒后开始清理,每30秒清理一次已完成的任务
391392
scheduler.scheduleWithFixedDelay(this::cleanupCompletedTasks, 30, 30, TimeUnit.SECONDS);
393+
394+
logger.info("任务处理器已启动,将在10秒后开始处理待处理任务");
392395
}
393396

394397
/**
@@ -425,6 +428,7 @@ private List<SyncTask> getPendingTasks(int limit) {
425428
List<SyncTask> tasks = new ArrayList<>();
426429
try (PreparedStatement stmt = connection.prepareStatement(sql)) {
427430
stmt.setInt(1, limit);
431+
stmt.setQueryTimeout(10); // 设置SQL查询超时为10秒
428432

429433
try (ResultSet rs = stmt.executeQuery()) {
430434
while (rs.next()) {
@@ -433,7 +437,10 @@ private List<SyncTask> getPendingTasks(int limit) {
433437
}
434438
}
435439
return tasks;
436-
}).get(5, TimeUnit.SECONDS);
440+
}).get(15, TimeUnit.SECONDS); // 增加超时时间到15秒
441+
} catch (java.util.concurrent.TimeoutException e) {
442+
logger.warn("获取待处理任务超时,可能数据库繁忙,将在下次调度时重试");
443+
return new ArrayList<>();
437444
} catch (Exception e) {
438445
logger.error("获取待处理任务失败", e);
439446
return new ArrayList<>();

src/main/java/com/xaoxiao/convenientaccess/whitelist/WhitelistSystem.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ public CompletableFuture<Boolean> initialize() {
9494
// 初始化API组件
9595
whitelistApiController = new WhitelistApiController(whitelistManager, syncTaskManager);
9696
userApiController = new UserApiController(registrationTokenManager, whitelistManager);
97-
PlayerDataApiController playerDataApiController = new PlayerDataApiController();
97+
PlayerDataApiController playerDataApiController = new PlayerDataApiController(plugin);
9898

9999
// 设置管理员密码到UserApiController
100100
userApiController.setAdminPassword(adminPassword);

0 commit comments

Comments
 (0)