强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

PaperMC 插件开发完全指南 / 第 12 章:数据库集成

第 12 章:数据库集成

掌握 SQLite、MySQL 和 Redis 的集成方法,为插件添加持久化存储能力。


12.1 数据存储方案对比

方案 适用场景 优点 缺点
YAML/JSON 文件 简单配置、少量数据 无需额外依赖,易读 不适合频繁读写,并发差
SQLite 中小型服务器、单机 零配置,嵌入式 不适合高并发写入
MySQL 大型服务器、多服共享 高并发,成熟稳定 需要独立服务,配置复杂
Redis 缓存、会话、排行榜 极高性能,支持发布订阅 数据需要持久化策略
MongoDB 文档型数据、日志 灵活 schema Java 驱动较重

12.2 SQLite 集成

SQLite 是嵌入式数据库,无需额外服务,非常适合中小型服务器。

添加 JDBC 驱动

<dependency>
    <groupId>org.xerial</groupId>
    <artifactId>sqlite-jdbc</artifactId>
    <version>3.46.0.0</version>
</dependency>

数据库管理器

package com.example.myplugin.database;

import org.bukkit.plugin.java.JavaPlugin;

import java.io.File;
import java.sql.*;

public class SQLiteManager {

    private final JavaPlugin plugin;
    private Connection connection;

    public SQLiteManager(JavaPlugin plugin) {
        this.plugin = plugin;
    }

    /**
     * 初始化数据库连接
     */
    public void initialize() throws SQLException {
        File dbFile = new File(plugin.getDataFolder(), "data.db");
        plugin.getDataFolder().mkdirs();

        String url = "jdbc:sqlite:" + dbFile.getAbsolutePath();
        connection = DriverManager.getConnection(url);

        // 启用 WAL 模式(提升并发性能)
        try (Statement stmt = connection.createStatement()) {
            stmt.execute("PRAGMA journal_mode=WAL");
            stmt.execute("PRAGMA synchronous=NORMAL");
        }

        // 创建表
        createTables();

        plugin.getLogger().info("SQLite 数据库已初始化!");
    }

    /**
     * 创建数据表
     */
    private void createTables() throws SQLException {
        String sql = """
            CREATE TABLE IF NOT EXISTS players (
                uuid TEXT PRIMARY KEY,
                name TEXT NOT NULL,
                balance REAL DEFAULT 0.0,
                level INTEGER DEFAULT 1,
                play_time INTEGER DEFAULT 0,
                first_join TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                last_join TIMESTAMP DEFAULT CURRENT_TIMESTAMP
            )
            """;

        try (Statement stmt = connection.createStatement()) {
            stmt.execute(sql);
        }
    }

    /**
     * 获取连接
     */
    public Connection getConnection() throws SQLException {
        if (connection == null || connection.isClosed()) {
            initialize();
        }
        return connection;
    }

    /**
     * 关闭连接
     */
    public void close() {
        try {
            if (connection != null && !connection.isClosed()) {
                connection.close();
            }
        } catch (SQLException e) {
            plugin.getLogger().severe("关闭数据库失败: " + e.getMessage());
        }
    }
}

基本 CRUD 操作

public class PlayerDataDAO {

    private final SQLiteManager dbManager;

    public PlayerDataDAO(SQLiteManager dbManager) {
        this.dbManager = dbManager;
    }

    /**
     * 插入或更新玩家数据
     */
    public void savePlayer(PlayerData data) throws SQLException {
        String sql = """
            INSERT INTO players (uuid, name, balance, level, play_time, last_join)
            VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
            ON CONFLICT(uuid) DO UPDATE SET
                name = excluded.name,
                balance = excluded.balance,
                level = excluded.level,
                play_time = excluded.play_time,
                last_join = CURRENT_TIMESTAMP
            """;

        try (Connection conn = dbManager.getConnection();
             PreparedStatement pstmt = conn.prepareStatement(sql)) {
            pstmt.setString(1, data.getUuid().toString());
            pstmt.setString(2, data.getName());
            pstmt.setDouble(3, data.getBalance());
            pstmt.setInt(4, data.getLevel());
            pstmt.setLong(5, data.getPlayTime());
            pstmt.executeUpdate();
        }
    }

    /**
     * 查询玩家数据
     */
    public PlayerData loadPlayer(UUID uuid) throws SQLException {
        String sql = "SELECT * FROM players WHERE uuid = ?";

        try (Connection conn = dbManager.getConnection();
             PreparedStatement pstmt = conn.prepareStatement(sql)) {
            pstmt.setString(1, uuid.toString());

            ResultSet rs = pstmt.executeQuery();
            if (rs.next()) {
                return new PlayerData(
                    uuid,
                    rs.getString("name"),
                    rs.getDouble("balance"),
                    rs.getInt("level"),
                    rs.getLong("play_time")
                );
            }
        }

        return null; // 玩家不存在
    }

    /**
     * 批量查询
     */
    public List<PlayerData> getTopPlayers(int limit) throws SQLException {
        String sql = "SELECT * FROM players ORDER BY balance DESC LIMIT ?";
        List<PlayerData> result = new ArrayList<>();

        try (Connection conn = dbManager.getConnection();
             PreparedStatement pstmt = conn.prepareStatement(sql)) {
            pstmt.setInt(1, limit);

            ResultSet rs = pstmt.executeQuery();
            while (rs.next()) {
                result.add(new PlayerData(
                    UUID.fromString(rs.getString("uuid")),
                    rs.getString("name"),
                    rs.getDouble("balance"),
                    rs.getInt("level"),
                    rs.getLong("play_time")
                ));
            }
        }

        return result;
    }

    /**
     * 删除玩家数据
     */
    public void deletePlayer(UUID uuid) throws SQLException {
        String sql = "DELETE FROM players WHERE uuid = ?";

        try (Connection conn = dbManager.getConnection();
             PreparedStatement pstmt = conn.prepareStatement(sql)) {
            pstmt.setString(1, uuid.toString());
            pstmt.executeUpdate();
        }
    }
}

12.3 MySQL 集成

MySQL 适合多服务器共享数据或高并发场景。

连接池(HikariCP 推荐)

<dependency>
    <groupId>com.zaxxer</groupId>
    <artifactId>HikariCP</artifactId>
    <version>5.1.0</version>
</dependency>

MySQL 连接池管理

package com.example.myplugin.database;

import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import org.bukkit.configuration.file.FileConfiguration;

import java.sql.Connection;
import java.sql.SQLException;

public class MySQLManager {

    private HikariDataSource dataSource;

    /**
     * 初始化连接池
     */
    public void initialize(FileConfiguration config) {
        HikariConfig hikariConfig = new HikariConfig();

        String host = config.getString("database.host", "localhost");
        int port = config.getInt("database.port", 3306);
        String database = config.getString("database.name", "minecraft");
        String username = config.getString("database.username", "root");
        String password = config.getString("database.password", "");

        hikariConfig.setJdbcUrl(
            "jdbc:mysql://" + host + ":" + port + "/" + database
                + "?useSSL=false&autoReconnect=true&serverTimezone=Asia/Shanghai"
        );
        hikariConfig.setUsername(username);
        hikariConfig.setPassword(password);

        // 连接池配置
        hikariConfig.setMaximumPoolSize(10);          // 最大连接数
        hikariConfig.setMinimumIdle(5);               // 最小空闲连接
        hikariConfig.setConnectionTimeout(30000);     // 连接超时 30 秒
        hikariConfig.setIdleTimeout(600000);          // 空闲超时 10 分钟
        hikariConfig.setMaxLifetime(1800000);         // 最大生命周期 30 分钟

        // 性能优化
        hikariConfig.addDataSourceProperty("cachePrepStmts", "true");
        hikariConfig.addDataSourceProperty("prepStmtCacheSize", "250");
        hikariConfig.addDataSourceProperty("prepStmtCacheSqlLimit", "2048");

        dataSource = new HikariDataSource(hikariConfig);
    }

    /**
     * 获取连接
     */
    public Connection getConnection() throws SQLException {
        return dataSource.getConnection();
    }

    /**
     * 关闭连接池
     */
    public void close() {
        if (dataSource != null && !dataSource.isClosed()) {
            dataSource.close();
        }
    }
}

配置文件(config.yml)

database:
  type: mysql  # sqlite 或 mysql
  host: localhost
  port: 3306
  name: minecraft
  username: root
  password: ""

12.4 异步数据库操作

关键原则: 不要在主线程执行数据库操作!

异步查询

public class AsyncDatabase {

    private final MySQLManager dbManager;
    private final ExecutorService executor;

    public AsyncDatabase(MySQLManager dbManager) {
        this.dbManager = dbManager;
        // 创建固定大小的线程池
        this.executor = Executors.newFixedThreadPool(4);
    }

    /**
     * 异步保存玩家数据
     */
    public void savePlayerAsync(PlayerData data, Consumer<Boolean> callback) {
        executor.submit(() -> {
            try {
                new PlayerDataDAO(dbManager).savePlayer(data);
                if (callback != null) callback.accept(true);
            } catch (SQLException e) {
                e.printStackTrace();
                if (callback != null) callback.accept(false);
            }
        });
    }

    /**
     * 异步加载玩家数据
     */
    public void loadPlayerAsync(UUID uuid, Consumer<PlayerData> callback) {
        executor.submit(() -> {
            try {
                PlayerData data = new PlayerDataDAO(dbManager).loadPlayer(uuid);
                // 回到主线程回调
                Bukkit.getScheduler().runTask(plugin, () -> callback.accept(data));
            } catch (SQLException e) {
                e.printStackTrace();
                Bukkit.getScheduler().runTask(plugin, () -> callback.accept(null));
            }
        });
    }

    /**
     * 关闭线程池
     */
    public void shutdown() {
        executor.shutdown();
        try {
            if (!executor.awaitTermination(10, TimeUnit.SECONDS)) {
                executor.shutdownNow();
            }
        } catch (InterruptedException e) {
            executor.shutdownNow();
        }
    }
}

使用 CompletableFuture

/**
 * 使用 CompletableFuture 的异步查询
 */
public CompletableFuture<PlayerData> loadPlayer(UUID uuid) {
    return CompletableFuture.supplyAsync(() -> {
        try {
            return new PlayerDataDAO(dbManager).loadPlayer(uuid);
        } catch (SQLException e) {
            throw new CompletionException(e);
        }
    }, executor);
}

// 使用方式
asyncDB.loadPlayer(player.getUniqueId())
    .thenAcceptAsync(data -> {
        // 主线程处理
        if (data != null) {
            player.sendMessage("§a你的余额: " + data.getBalance());
        }
    }, runnable -> Bukkit.getScheduler().runTask(plugin, runnable))
    .exceptionally(ex -> {
        player.sendMessage("§c加载数据失败!");
        return null;
    });

12.5 Redis 集成

Redis 适合缓存热数据、排行榜、会话管理和发布/订阅。

依赖

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>5.1.0</version>
</dependency>

Redis 管理器

package com.example.myplugin.database;

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

public class RedisManager {

    private JedisPool jedisPool;

    public void initialize(String host, int port, String password) {
        JedisPoolConfig poolConfig = new JedisPoolConfig();
        poolConfig.setMaxTotal(16);
        poolConfig.setMaxIdle(8);
        poolConfig.setMinIdle(2);
        poolConfig.setTestOnBorrow(true);

        if (password == null || password.isEmpty()) {
            jedisPool = new JedisPool(poolConfig, host, port);
        } else {
            jedisPool = new JedisPool(poolConfig, host, port, 2000, password);
        }
    }

    /**
     * 执行 Redis 操作
     */
    public <T> T execute(Function<Jedis, T> action) {
        try (Jedis jedis = jedisPool.getResource()) {
            return action.apply(jedis);
        }
    }

    public void close() {
        if (jedisPool != null) {
            jedisPool.close();
        }
    }
}

Redis 使用示例

public class RedisCache {

    private final RedisManager redis;
    private static final String PREFIX = "myplugin:";

    public RedisCache(RedisManager redis) {
        this.redis = redis;
    }

    /**
     * 缓存玩家余额
     */
    public void setBalance(UUID uuid, double balance) {
        redis.execute(jedis -> {
            jedis.setex(
                PREFIX + "balance:" + uuid,
                300, // 5 分钟过期
                String.valueOf(balance)
            );
            return null;
        });
    }

    /**
     * 获取缓存的余额
     */
    public OptionalDouble getBalance(UUID uuid) {
        String value = redis.execute(jedis ->
            jedis.get(PREFIX + "balance:" + uuid));

        if (value == null) return OptionalDouble.empty();
        return OptionalDouble.of(Double.parseDouble(value));
    }

    /**
     * 更新排行榜
     */
    public void updateLeaderboard(String board, String member, double score) {
        redis.execute(jedis -> {
            jedis.zadd(PREFIX + "leaderboard:" + board, score, member);
            return null;
        });
    }

    /**
     * 获取排行榜前 N 名
     */
    public List<String> getTopPlayers(String board, int count) {
        return redis.execute(jedis ->
            new ArrayList<>(jedis.zrevrange(
                PREFIX + "leaderboard:" + board, 0, count - 1))
        );
    }

    /**
     * 发布消息(跨服务器通信)
     */
    public void publish(String channel, String message) {
        redis.execute(jedis -> {
            jedis.publish(PREFIX + channel, message);
            return null;
        });
    }
}

12.6 数据迁移

/**
 * SQLite 迁移到 MySQL
 */
public class DataMigration {

    public void migrateSQLiteToMySQL(SQLiteManager sqlite, MySQLManager mysql) {
        // 从 SQLite 读取所有数据
        List<PlayerData> allPlayers;
        try {
            allPlayers = new PlayerDataDAO(sqlite).getAllPlayers();
        } catch (SQLException e) {
            throw new RuntimeException("读取 SQLite 数据失败", e);
        }

        // 批量写入 MySQL
        try (Connection conn = mysql.getConnection()) {
            conn.setAutoCommit(false);

            String sql = "INSERT INTO players (uuid, name, balance, level, play_time) "
                + "VALUES (?, ?, ?, ?, ?) "
                + "ON DUPLICATE KEY UPDATE "
                + "name=VALUES(name), balance=VALUES(balance), "
                + "level=VALUES(level), play_time=VALUES(play_time)";

            try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
                int batchCount = 0;
                for (PlayerData data : allPlayers) {
                    pstmt.setString(1, data.getUuid().toString());
                    pstmt.setString(2, data.getName());
                    pstmt.setDouble(3, data.getBalance());
                    pstmt.setInt(4, data.getLevel());
                    pstmt.setLong(5, data.getPlayTime());
                    pstmt.addBatch();

                    batchCount++;
                    if (batchCount % 1000 == 0) {
                        pstmt.executeBatch();
                    }
                }
                pstmt.executeBatch();
                conn.commit();
            }
        } catch (SQLException e) {
            throw new RuntimeException("写入 MySQL 数据失败", e);
        }
    }
}

12.7 连接池管理

连接池最佳实践

public class ConnectionPoolManager {

    /**
     * 使用 try-with-resources 确保连接释放
     */
    public void safeQuery() {
        try (Connection conn = pool.getConnection();
             PreparedStatement pstmt = conn.prepareStatement("SELECT * FROM players")) {

            ResultSet rs = pstmt.executeQuery();
            while (rs.next()) {
                // 处理数据
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
        // 连接自动归还到池中
    }

    /**
     * 事务处理
     */
    public void transaction() throws SQLException {
        try (Connection conn = pool.getConnection()) {
            conn.setAutoCommit(false);
            try {
                // 执行多个操作
                updatePlayer(conn, uuid, balance);
                logTransaction(conn, uuid, amount);

                conn.commit();
            } catch (SQLException e) {
                conn.rollback();
                throw e;
            } finally {
                conn.setAutoCommit(true);
            }
        }
    }
}

12.8 业务场景:玩家数据管理器

public class PlayerDataManager {

    private final AsyncDatabase asyncDB;
    private final RedisCache redisCache;

    /**
     * 玩家加入时加载数据
     */
    @EventHandler
    public void onJoin(PlayerJoinEvent event) {
        Player player = event.getPlayer();
        UUID uuid = player.getUniqueId();

        // 先从 Redis 缓存读取
        OptionalDouble cached = redisCache.getBalance(uuid);
        if (cached.isPresent()) {
            // 缓存命中,直接使用
            applyBalance(player, cached.getAsDouble());
        }

        // 异步从数据库加载完整数据
        asyncDB.loadPlayerAsync(uuid, data -> {
            if (data == null) {
                // 新玩家
                data = new PlayerData(uuid, player.getName(), 0, 1, 0);
            }
            applyPlayerData(player, data);

            // 更新 Redis 缓存
            redisCache.setBalance(uuid, data.getBalance());
        });
    }

    /**
     * 玩家退出时保存数据
     */
    @EventHandler
    public void onQuit(PlayerQuitEvent event) {
        Player player = event.getPlayer();
        PlayerData data = getPlayerData(player);

        if (data != null) {
            asyncDB.savePlayerAsync(data, success -> {
                if (!success) {
                    plugin.getLogger().severe(
                        "保存玩家数据失败: " + player.getName());
                }
            });
        }
    }
}

12.9 常见问题排查

问题 原因 解决方案
主线程卡顿 同步数据库操作 全部使用异步
连接超时 连接池配置不当 调整超时参数
内存溢出 未关闭连接 使用 try-with-resources
数据竞争 并发写入 使用事务 + 乐观锁
SQLite 锁定 多线程写入 使用 WAL 模式或改为 MySQL

12.10 扩展阅读


12.11 本章小结

要点 内容
SQLite 零配置嵌入式数据库,适合中小型服务器
MySQL 高并发关系型数据库,使用 HikariCP 连接池
Redis 高性能缓存,适合排行榜、会话管理
异步操作 所有数据库操作必须在异步线程执行
连接管理 try-with-resources 确保连接释放
数据迁移 批量操作 + 事务保证一致性

下一章: 第 13 章:任务调度 — 学习同步/异步任务、定时器和延迟任务。