Skip to content

数据持久化 ​

前面所有系统(经济、等级、公会)的数据,都必须落盘——否则重启全没。这一篇讲从文件到数据库的完整方案。

三层方案,按规模选 ​

方案适合特点
YAML 文件小服(百人内)简单、零依赖、读写慢
MySQL中大型服可靠、支持复杂查询、并发好
MySQL + Redis 缓存大型服快速读写 + 持久化,最佳实践

YAML 文件(小服) ​

用配置 API 的 YamlConfiguration。每个玩家一个文件,或全部塞一个文件。

单文件存全部 ​

java
public void saveAll() {
    FileConfiguration cfg = new YamlConfiguration();
    for (Map.Entry<UUID, PlayerData> e : players.entrySet()) {
        String path = e.getKey().toString();
        cfg.set(path + ".level", e.getValue().getLevel());
        cfg.set(path + ".coins", e.getValue().getCoins());
    }
    cfg.save(new File(plugin.getDataFolder(), "players.yml"));
}

单文件会膨胀

玩家多了 players.yml 会变成几十 MB,每次全量读写都慢。超过几百人就该上数据库。

按玩家分文件 ​

java
public void save(UUID id, PlayerData data) {
    File file = new File(plugin.getDataFolder(), "players/" + id + ".yml");
    FileConfiguration cfg = YamlConfiguration.loadConfiguration(file);
    cfg.set("level", data.getLevel());
    cfg.set("coins", data.getCoins());
    cfg.save(file);
}

读写只动单个文件,比单文件好。但仍不如数据库。

MySQL(中型服) ​

建表 ​

sql
CREATE TABLE player_data (
    uuid VARCHAR(36) PRIMARY KEY,
    name VARCHAR(16),
    level INT DEFAULT 1,
    exp DOUBLE DEFAULT 0,
    coins DOUBLE DEFAULT 0,
    job VARCHAR(20),
    guild_id VARCHAR(36),
    last_login BIGINT,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

连接池 ​

别每次查询都新建连接

建连接很慢(握手开销)。用连接池(HikariCP)复用连接:

java
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://host:3306/dbname");
config.setUsername("user");
config.setPassword("pass");
config.setMaximumPoolSize(10);
HikariDataSource ds = new HikariDataSource(config);

查询时从池借连接、用完归还。

异步查询 ​

数据库操作必须走异步(见调度器红线):

java
public void loadAsync(UUID id, Consumer<PlayerData> callback) {
    Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
        PlayerData data = loadFromDb(id); // 异步查库
        Bukkit.getScheduler().runTask(plugin, () -> {
            callback.accept(data); // 回主线程回调
        });
    });
}

玩家进服时序

玩家进服触发 PlayerJoinEvent,此时数据还没从库加载完。要等异步加载完成再给玩家发奖/同步状态。期间标记"加载中",避免数据 race。

Redis 缓存(大型服) ​

MySQL 每次查都要走网络 + 磁盘,高频读(查余额)仍慢。Redis 是内存数据库,读写微秒级。

缓存模式 ​

读:先查 Redis → 命中直接返回;未命中查 MySQL → 写回 Redis → 返回
写:先写 Redis → 异步写 MySQL(或写穿:同时写两边)
java
public double getCoins(UUID id) {
    // 1. 先查 Redis
    String cached = jedis.get("coins:" + id);
    if (cached != null) return Double.parseDouble(cached);
    // 2. 未命中查 MySQL
    double coins = queryCoinsFromDb(id);
    // 3. 回填 Redis(带过期)
    jedis.setex("coins:" + id, 300, String.valueOf(coins));
    return coins;
}

排行榜用 ZSET ​

Redis 的 Sorted Set 天生为排行榜设计(见排行榜)。金币、等级榜都用 ZSET 存。

Redis 适合什么

  • 缓存:玩家常用数据(余额、等级)
  • 排行榜:ZSET
  • 计数器:在线人数、今日击杀(INCR)
  • 跨服共享:多子服连同一 Redis(见跨服)

玩家数据生命周期 ​

完整流程:

进服 → 异步加载(库→内存) → 标记加载完成 → 正常游玩(改内存)
         ↑                                        ↓
         │                                   定时保存(内存→库)
         │                                        ↓
      重连复用内存                            退服立即保存(内存→库)

关键时序 ​

java
private final Map<UUID, PlayerData> cache = new HashMap<>();
private final Set<UUID> loading = new HashSet<>();

@EventHandler
public void onJoin(PlayerJoinEvent e) {
    UUID id = e.getPlayer().getUniqueId();
    loading.add(id);
    loadAsync(id, data -> {
        cache.put(id, data);
        loading.remove(id);
        applyToPlayer(e.getPlayer(), data); // 同步到玩家(等级、经验条)
    });
}

@EventHandler
public void onQuit(PlayerQuitEvent e) {
    UUID id = e.getPlayer().getUniqueId();
    PlayerData data = cache.remove(id);
    if (data != null) saveAsync(id, data); // 退服存库
}

加载中禁止改数据

loading 集合里的玩家,其他事件(打怪给经验)要先跳过或排队,等加载完再处理。否则会覆盖刚加载的旧数据。

定时保存 ​

java
new BukkitRunnable() {
    @Override
    public void run() {
        for (Map.Entry<UUID, PlayerData> e : cache.entrySet()) {
            saveAsync(e.getKey(), e.getValue());
        }
    }
}.runTaskTimer(plugin, 0L, 6000L); // 每 5 分钟(6000 tick)

常见坑 ​

  • 崩服丢数据:只靠定时保存,崩服会丢定时窗口内的改动。关键操作(大额交易)即时存
  • 死锁:异步线程和主线程同时改同一数据 → 加锁或单线程化
  • 连接泄漏:借了连接没归还 → 用 try-with-resources
  • UUID 为主键:玩家能改名,别用 name 当主键,永远用 UUID
  • 跨服数据竞争:多服同时改同一玩家数据 → 用 Redis 分布式锁或单服写

下一篇性能优化与 TPS。