LyMySQLCore
LyMySQLCore 是离渊系列插件使用 MySQL 存储时的数据库前置,负责统一处理数据库连接、玩家数据锁、玩家数据加载、玩家数据保存和跨服数据安全。
凡是离渊系列插件中启用了 MySQL 存储的功能,都必须先安装 LyMySQLCore,并确认插件已经成功连接数据库。只有在 LyMySQLCore 正常加载且数据库连接成功后,相关数据读取、保存和跨服锁功能才会生效。
插件信息
| 项目 | 内容 |
|---|---|
| 插件名 | LyMySQLCore |
| 当前版本 | 1.0.5 |
| 作者 | Liyuan |
| 主类 | Ly.sqlcore.safer.MySQLSafer |
| Java 版本 | Java 8 |
| 编译平台 | Paper API 1.16.5-R0.1-SNAPSHOT |
| 适用服务端 | Bukkit、Spigot、Paper 系列服务端;项目证据仅明确使用 Paper 1.16.5 API 编译 |
| 主要用途 | 离渊系列插件的 MySQL 数据前置 |
警告
启用任何依赖 MySQL 的功能前,必须安装 LyMySQLCore,正确填写数据库配置,并确认控制台出现数据库连接成功提示。
如果 LyMySQLCore 未加载、数据库连接失败或插件启动失败,相关插件的数据读取、数据保存、跨服同步、冷却记录和次数记录等功能都不能正常工作。
玩法与作用
LyMySQLCore 不提供独立玩法内容,也不提供玩家操作指令。它作为数据库前置,为其它插件提供安全的数据处理流程:
- 玩家进入服务器后,先登记玩家状态并尝试获取数据库锁。
- 获取锁之前,限制玩家进行可能影响数据的操作。
- 获取锁后,异步执行已注册的数据加载器。
- 数据加载完成后,触发安全加载事件并解除限制。
- 玩家离开服务器时,执行已注册的数据保存器,然后释放玩家锁。
- 服务器关闭或执行重启时,等待当前服务器持有的玩家锁处理完成。
依赖与支持
必需条件
| 项目 | 要求 |
|---|---|
| 前置插件 | LyMySQLCore 本身就是离渊系列插件的 MySQL 前置 |
| 数据库 | 可通过 MySQL JDBC 地址连接的 MySQL 数据库 |
| 服务端 API | 项目使用 Paper API 1.16.5-R0.1-SNAPSHOT 编译 |
| Java | Java 8 |
项目的 plugin.yml 未声明其它 Bukkit 插件依赖。项目构建文件中将 HikariCP 和 Guava 打包到插件中使用。
功能
| 功能 | 说明 |
|---|---|
| MySQL 连接 | 从 plugins/LyMySQLCore/config.yml 读取数据库地址、端口、库名、账号、密码和连接参数 |
| HikariCP 连接池 | 使用 HikariCP 管理数据库连接,最小空闲连接数为 1,最大连接数根据处理器数量计算,最高为 20 |
| 玩家数据锁 | 使用 lymysqlcore_lock 记录玩家当前持有数据锁的服务器 |
| 跨服安全 | 玩家在其它服务器存在有效锁时,不会立即读取数据,避免多个服务器同时操作同一玩家数据 |
| 锁续期 | 定时更新在线玩家锁的时间,维持当前服务器对玩家数据的占用状态 |
| 玩家加载流程 | 玩家进服后先获得锁,再异步执行数据加载器 |
| 加载期间保护 | 玩家数据加载期间限制移动、交互、丢弃物品、攻击、打开背包、点击背包和聊天 |
| 加载状态提示 | 数据加载期间为玩家添加失明效果,加载完成后移除该效果 |
| 加载失败处理 | 数据加载异常或长时间无法获得锁时,清理状态并将在线玩家踢出服务器 |
| 玩家离服保存 | 玩家退出或被踢出时执行数据保存,并释放玩家锁 |
| 周期保存 | 定时为在线且已完成加载的玩家执行周期保存 |
| 服务器关闭保护 | 服务器关闭时等待当前服务器持有的玩家锁处理完成,最多循环等待十次 |
| 玩家日志 | 使用 lymysqlcore_playerlog 记录玩家名称和 UUID |
| 数据供应器 | 允许其它插件注册数据加载、保存和周期保存处理器 |
| 优先级处理 | 按 MySQLSupplierPriority 顺序执行已注册的数据供应器 |
| Bukkit 事件 | 提供玩家加载、保存和周期保存事件供其它插件监听 |
指令
项目的 plugin.yml 未声明任何命令。
| 指令 | 说明 |
|---|---|
| 无 | LyMySQLCore 不提供玩家或管理员指令 |
权限
项目的 plugin.yml 未声明任何权限节点。
| 权限 | 说明 |
|---|---|
| 无 | LyMySQLCore 不提供权限节点 |
配置文件
配置文件位置:
text
plugins/LyMySQLCore/config.yml当前项目配置文件如下:
yaml
mysql:
databasename: mc2
username: mc2
password: mc1234
port: 3306
ip: 127.0.0.1
link: 'useSSL=false&serverTimezone=UTC&characterEncoding=UTF-8'
join-load-delay: 20mysql
mysql 用于配置数据库连接信息。数据库配置正确并且连接成功后,后续的玩家数据锁、数据加载和数据保存流程才会正常运行。
| 配置键 | 类型 | 默认值 | 说明 |
|---|---|---|---|
mysql.databasename | 字符串 | mc2 | MySQL 数据库名称 |
mysql.username | 字符串 | mc2 | MySQL 登录用户名 |
mysql.password | 字符串 | mc1234 | MySQL 登录密码 |
mysql.port | 整数 | 3306 | MySQL 服务端口 |
mysql.ip | 字符串 | 127.0.0.1 | MySQL 服务器地址 |
mysql.link | 字符串 | useSSL=false&serverTimezone=UTC&characterEncoding=UTF-8 | 拼接到 JDBC 地址后的连接参数 |
插件最终使用的 JDBC 地址格式为:
text
jdbc:mysql://<ip>:<port>/<databasename>?<link>其中 <ip>、<port>、<databasename> 和 <link> 对应配置文件中的实际值。
join-load-delay
| 配置键 | 类型 | 默认值 | 单位 | 说明 |
|---|---|---|---|---|
join-load-delay | 长整数 | 20 | tick | 数据加载完成后,触发 MySQLSafePlayerLoadEvent 并解除玩家加载状态前的延迟 |
20 tick 通常对应约 1 秒,但实际时间取决于服务器 tick 执行情况。
启动流程
插件启用时会执行以下操作:
- 读取或生成
config.yml。 - 根据
mysql配置建立 HikariCP 数据库连接池。 - 创建
lymysqlcore_lock数据表。 - 创建
lymysqlcore_playerlog数据表。 - 注册玩家锁监听器。
- 启动锁续期、周期保存和玩家数据加载任务。
- 清理当前服务器名称对应的旧锁记录。
- 输出初始化成功信息。
如果数据库连接或数据表初始化失败,插件会输出初始化失败信息并禁用自身。
数据库连接成功时,控制台会输出包含以下内容的日志:
text
[LyMySQLCore] 数据库连接成功
[LyMySQLCore] 初始化成功,插件启动成功!实际日志还会包含 JDBC 地址、连接池连接数量和耗时。
玩家数据流程
进入服务器
玩家加入服务器后,插件会:
- 记录玩家名称和 UUID;
- 记录玩家进入时的位置;
- 将玩家标记为等待获得数据锁;
- 为玩家添加失明效果;
- 检查玩家是否在其它服务器持有有效锁;
- 获取锁后,异步执行所有已注册供应器的
loadData; - 加载完成后,等待
join-load-delay配置的 tick 数; - 触发
MySQLSafePlayerLoadEvent; - 标记玩家数据为已加载并移除失明效果。
如果玩家在其它服务器存在未过期锁,插件会持续尝试获取锁。尝试次数达到 100 次后,玩家会被踢出并显示“数据加载失败,请稍后再试”。
数据加载期间
玩家处于 CONNECTING、LOADING 或 FAILED 阶段时,插件会视为仍在加载数据,并限制以下操作:
| 操作 | 处理 |
|---|---|
| 移动 | 取消移动并尝试传送回进入时的位置 |
| 交互 | 取消玩家交互 |
| 丢弃物品 | 取消丢弃物品 |
| 攻击实体 | 取消玩家作为攻击者造成的伤害 |
| 点击背包 | 取消背包点击 |
| 打开背包 | 取消打开背包 |
| 聊天 | 取消聊天消息 |
离开服务器
玩家退出或被踢出时,插件会:
- 检查玩家是否持有当前服务器的数据锁。
- 防止重复执行离服流程。
- 执行所有已注册供应器的
saveData。 - 触发
MySQLSafePlayerSaveEvent。 - 释放当前服务器持有的玩家锁。
- 清理玩家运行状态。
周期保存
插件会定时处理在线且已完成数据加载的玩家,并调用供应器的 cycleSaveData。
供应器的周期保存任务会尽量分散执行:当某个供应器距离上次周期保存超过 160 秒,或随机条件满足时,才会进入本轮周期保存。周期保存完成后,会为对应玩家触发 MySQLSafePlayerCycleSaveEvent。
服务器关闭
服务器关闭时,插件会检查当前服务器是否仍有有效玩家锁:
- 如果仍有锁,输出等待日志并每次等待 1 秒;
- 最多尝试等待 10 次;
- 检测到没有当前服务器锁后,输出数据保存完成日志并继续关闭服务器。
数据表
插件启动时会自动创建以下数据表。业务插件自己的数据表不由 LyMySQLCore 创建。
lymysqlcore_lock
用于记录玩家数据锁和服务器锁状态。
| 字段 | 类型 | 说明 |
|---|---|---|
id | int(16) UNSIGNED | 自增主键 |
player_name | varchar(32) | 玩家名称,唯一 |
server | varchar(32) | 持有锁的服务器名称;释放后使用内部解锁标记 |
lock_time | timestamp | 锁的创建或更新时间 |
表使用 InnoDB 引擎和 utf8mb4 字符集,并创建玩家名称、玩家名称与服务器、服务器相关索引。
lymysqlcore_playerlog
用于记录玩家名称和 UUID。
| 字段 | 类型 | 说明 |
|---|---|---|
id | int(16) UNSIGNED | 自增主键 |
player_name | varchar(128) | 玩家名称,唯一 |
uuid | varchar(36) | 玩家 UUID |
表使用 utf8mb4 字符集,并创建玩家名称与 UUID 的联合唯一索引。
数据供应器 API
其它插件可以通过 MySQLSupplierManager 注册数据供应器。供应器负责实现具体业务数据的加载、保存和周期保存,LyMySQLCore 负责按照优先级调用这些方法。
MySQLSupplier
实现接口:
java
public interface MySQLSupplier {
String getName();
void loadData(OfflinePlayer player);
void saveData(OfflinePlayer player);
void cycleSaveData(OfflinePlayer player);
}| 方法 | 说明 |
|---|---|
getName() | 返回供应器名称。名称用于识别供应器,不能与已注册供应器重复 |
loadData(OfflinePlayer player) | 加载玩家业务数据 |
saveData(OfflinePlayer player) | 保存玩家业务数据 |
cycleSaveData(OfflinePlayer player) | 执行周期保存 |
注册供应器
不指定优先级时,供应器使用 NORMAL:
java
MySQLSupplierManager.registerSupplier(supplier);指定优先级时:
java
MySQLSupplierManager.registerSupplier(supplier, MySQLSupplierPriority.HIGH);如果已有供应器使用相同名称,注册时会抛出异常。
删除供应器
按名称删除:
java
MySQLSupplierManager.deleteSupplier("供应器名称");按供应器对象删除:
java
MySQLSupplierManager.deleteSupplier(supplier);查询供应器
java
Set<MySQLSupplierWrapper> suppliers = MySQLSupplierManager.getSuppliers();getSuppliers() 返回当前已注册供应器集合。MySQLSupplierWrapper 为内部包装类型,业务插件通常只需要使用注册和删除方法。
供应器优先级
供应器按照以下顺序执行,数值越小越先执行:
| 优先级 | 数值 |
|---|---|
LOWEST | 0 |
LOW | 1 |
NORMAL | 2 |
HIGH | 3 |
HIGHEST | 4 |
MONITOR | 5 |
加载、保存和周期保存都会按照这个顺序遍历供应器。
Bukkit 事件
MySQLSafePlayerLoadEvent
玩家数据供应器加载完成,并且等待 join-load-delay 延迟后触发。监听此事件后,可以开始执行依赖玩家数据的相关操作。
java
@EventHandler
public void onPlayerDataLoad(MySQLSafePlayerLoadEvent event) {
Player player = event.getPlayer();
}MySQLSafePlayerSaveEvent
玩家离开服务器时,供应器完成数据保存后触发。
java
@EventHandler
public void onPlayerDataSave(MySQLSafePlayerSaveEvent event) {
Player player = event.getPlayer();
}MySQLSafePlayerCycleSaveEvent
玩家完成一次周期保存后触发。
java
@EventHandler
public void onPlayerCycleSave(MySQLSafePlayerCycleSaveEvent event) {
Player player = event.getPlayer();
}玩家状态机制
插件使用 PlayerPhase 标记玩家当前的数据处理阶段:
| 状态 | 说明 |
|---|---|
CONNECTING | 玩家刚进入服务器,等待获得数据锁 |
LOADING | 已获得数据锁,正在异步加载数据 |
LOADED | 玩家数据已经加载完成,可以正常参与游戏 |
LEAVING | 玩家离服保存流程已经开始,用于避免重复保存 |
FAILED | 玩家数据加载失败,等待清理状态并踢出 |
运行任务
插件启动后会注册以下定时任务:
| 任务 | 首次延迟 | 执行间隔 | 作用 |
|---|---|---|---|
| 玩家锁续期 | 30 * 20 tick | 30 * 20 tick | 更新已加载玩家的锁时间 |
| 周期保存 | 20 * 20 tick | 20 * 20 tick | 为在线玩家执行周期保存 |
| 玩家数据加载检查 | 2 tick | 2 tick | 检查进服玩家并推进数据加载流程 |
这些任务由插件内部自动注册,不提供配置项调整执行间隔。
常见问题
数据库连接失败怎么办?
检查 plugins/LyMySQLCore/config.yml 中的以下配置:
mysql.databasenamemysql.usernamemysql.passwordmysql.portmysql.ipmysql.link
确认数据库服务正在运行,账号具有对应数据库的访问权限,并确认服务器能够访问数据库地址。连接失败时,插件会输出数据库连接失败日志并在初始化失败后禁用自身。
为什么依赖 MySQL 的功能没有效果?
确认以下条件全部满足:
LyMySQLCore已放入插件目录并成功加载。plugins/LyMySQLCore/config.yml已填写正确。- 控制台出现数据库连接成功日志。
- 控制台出现初始化成功日志。
- 相关业务插件确实使用了 MySQL 存储。
只安装业务插件而没有安装 LyMySQLCore,或数据库没有成功连接时,相关 MySQL 功能不会生效。
玩家为什么进服后不能移动或操作?
这是插件的数据加载保护机制。玩家处于 CONNECTING、LOADING 或 FAILED 状态时,插件会限制移动、交互、背包操作、聊天和攻击,避免玩家在业务数据加载完成前操作插件功能。
数据成功加载、触发 MySQLSafePlayerLoadEvent 并完成延迟放行后,限制会解除。
玩家为什么被提示“数据加载失败,请稍后再试”?
常见原因包括:
- 数据库连接异常;
- 其它服务器仍持有该玩家的有效数据锁;
- 数据供应器加载数据时抛出异常;
- 玩家长时间无法获得数据锁,尝试次数达到 100 次。
应先检查数据库连接日志、跨服服务器状态和业务插件的数据加载逻辑。
插件会自动创建数据表吗?
会。插件启动时会创建:
lymysqlcore_locklymysqlcore_playerlog
业务插件自己的数据表不由 LyMySQLCore 创建,具体由对应业务插件负责。
为什么服务器关闭时会等待?
插件会等待当前服务器持有的玩家锁处理完成,避免服务器关闭时仍有玩家数据没有完成保存。等待期间控制台会输出等待日志。
业务插件应该什么时候读取玩家数据?
业务插件应监听 MySQLSafePlayerLoadEvent,在该事件触发后再执行依赖玩家数据的操作。玩家数据成功读取前,不应允许玩家进行依赖该数据的业务操作。
业务插件应该如何保存玩家数据?
建议通过 MySQLSupplier 注册数据供应器,并实现 loadData、saveData 和 cycleSaveData。这样可以接入 LyMySQLCore 的统一加载、离服保存和周期保存流程。