2.2.2 持久化全链路审计
数据库关闭与重载、背包条件保存及恢复、未确认交易核对、旧会话保护与剩余边界。
在 2.2.1 修复 MySQL 连接事务状态后,继续检查数据库关闭、配置重载、背包编辑、技能书、购买以及宠物会话保存。本次修复能让旧快照覆盖、未执行任务无响应和结果不确定时错误退款等路径得到明确处理。
数据库生命周期
- 数据库参数变更需要正常重启。 修改当前生效的数据库类型、SQLite 文件或 MySQL 连接参数后,
/lipet reload拒绝整次重载,运行中的配置和数据库目标继续保持一致。文件编辑不会被撤销,正常重启时再读取。数据库设置相同仍可重载其他支持热更新的配置。 - 已受理任务必须有结果。 关闭仓库先进行有界等待;等待结束后只拒绝尚未开始的任务,让调用方收到明确异常。已开始的 SQL 不会被当作“未写入”提前取消,继续返回实际结果,最后一项完成后才关闭连接池和 JDBC 依赖。
- 数据库线程内关闭不会等待自身。 Future 回调触发关闭时改由独立等待线程处理;日志分别说明已拒绝的排队任务与仍在执行的任务。
- 坏档案保留并给出定位信息。 无法解析的宠物记录继续让本次读取失败,异常附带
pet_id。不会跳过坏记录伪装成空列表,也不会自动删除或重建它。
停止在线换库是为了避免同一次业务操作的读取落在旧库、后续写入落在新库。迁库仍须先正常停服、完整备份并迁移数据;插件不会把旧库数据自动复制到新库。
背包保存与恢复
同一服务器对同一只宠物只允许一个编辑会话,保存未确认前阻止再次打开。保存采用原子条件更新:只有数据库内容仍等于打开时的原始快照,且宠物记录仍存在,才写入本次结果。MySQL 按内容逐字节比较,大小写或尾空格不同也视为冲突;两个基于相同旧快照的不同写入不能同时覆盖成功。
编辑及保存快照写入 plugins/LiPet/inventory-recovery/<宠物UUID>.yml。正常启动时尝试恢复未完成项:数据库仍是预期原始内容时才条件保存;数据库已经是目标内容时确认完成;主人变化、内容冲突或存储失败则保留文件并提示核对。恢复日志损坏时停止受理背包编辑,保留原始证据。
这些文件包含物品快照,不是可直接领取的补偿包。遇到冲突应备份文件,并核对主人、宠物 ID、当前数据库内容及玩家背包后处理,不能直接覆盖数据库或重复发放其中物品。跨服条件保存能拒绝覆盖,但不等同于数据库和 Minecraft 玩家背包之间的分布式事务。
技能书与购买确认
被动技能书使用前先记录事务凭据。学习已经成功、只是后续技能缓存刷新失败时,不再按学习失败退书。只有已确认应退款的条目才自动尝试发放;物品发放前记录发放阶段,重启后不会把结果不明的学习或发放再次执行。
plugins/LiPet/pending-skillbook-refunds.yml 中各状态含义:
| 状态 | 含义与处理 |
|---|---|
RESERVED |
已记录预扣意图,最终学习结果未确认;保留,结合宠物被动技能核对 |
REFUNDABLE |
已明确可以退款;在线或下次加入时按玩家线程尝试发放 |
DELIVERING |
已开始发放,但完成确认未落盘;保留,核对玩家实际物品 |
REVIEW_REQUIRED |
旧记录迁移或确认失败,需要人工核对 |
购买流程只对明确失败的创建尝试退款。扣款、创建或退款响应未确认时,在 plugins/LiPet/transaction-recovery/purchases/ 保留订单阶段、玩家、币种、金额及可用的宠物 ID;不因一次异常盲目重新扣款或退款。创建响应丢失后,按同一宠物 UUID 核验初始档案,避免重复创建。
以上台账采用原子文件替换并保留异常凭据,不能替代数据库、经济插件和玩家物品之间的跨系统事务。管理员应先查数据库和货币流水,再决定补偿;不要通过删除台账来绕过核对。
捕捉创建未确认
创建返回异常时,先查询本次生成的同一个宠物 UUID,并逐项核对初始业务字段。确认档案已落盘时仍返回创建成功;查明不存在时才按明确失败处理;查询失败或记录内容不同则记为未确认,保留原始创建档案,不重新生成宠物 ID 再试一次。
捕捉遇到未确认结果,在 plugins/LiPet/transaction-recovery/captures/ 保存实际捕捉球组件、目标与待确认宠物档案,并锁定该目标,重启后继续识别锁定记录。管理员需结合宠物 UUID、目标和球数量核对,不能把“未收到成功响应”直接当作没保存并退款。该记录也不证明实际已成功创建。
宠物会话与保存
单服死亡和停服保存从权威档案重新计算,保留已删除、已死亡等较新状态,避免旧的活动快照覆盖。群组服先确认租约,再要求数据库 revision 与原会话一致;发现较新版本就拒绝旧会话写回,不把旧会话重新套到新版本上。
续租按实体 UUID 隔离会话,并按请求序号处理乱序响应。续租明确失败或异常时,撤下本地会话,不用旧快照写数据库,也不释放可能已经属于另一子服的租约。跨服删除通知可以清理远端相同 revision 的本地会话,更高 revision 的会话继续保留。
死亡重试凭据仅限本进程生命周期:数据库断联后同时发生硬退出,未提交的死亡惩罚仍可能丢失,原宠物档案仍在。本次没有增加死亡凭据跨重启恢复日志,也没有实现 SQL 层的全局会话写入栅栏。
升级与适用边界
本次不更改数据库表结构,不迁移管理员的数据库数据,112 项主动技能和现有配置继续保留。成品核对确认业务配置值与 schema 保持原样,消息配置仅补缺失提示。备份数据库和完整插件数据目录后正常停服,更换 2.2.2 Jar,再启动确认 Enabling LiPet v2.2.2;不能用热重载替代新版 Java 代码加载。
审计仍发现“喂食成功后再扣物品”的免费喂食边界,本轮未纳入修复;没有实现喂食物品预扣的跨系统事务。也不承诺跨服物品转移 exactly-once、任意崩溃下绝不丢失、自动恢复已经回滚的新宠物或已被覆盖的历史物品。存在未确认结果时,选择保留凭据和阻止危险重试,而不是推断为成功或失败。
本机 F:/26.2 使用 SQLite,不是用户报告问题的生产 MySQL 现场。隔离测试不会修改生产数据库;待更新目录中的 Jar 也不代表运行服已加载新版。
验证结果
本轮 Maven 构建成功,769 项单元测试全部通过,失败、错误和跳过均为 0。覆盖数据库任务关闭、背包条件保存、技能书与购买确认、旧会话和死亡保存等回归场景。
真实 MySQL Community Server 8.0.45、Connector/J 9.2.0 隔离验证共 83 项通过:
| 检查范围 | 数量与结果 |
|---|---|
| 本轮背包 CAS、创建确认、关闭队列与短等待窗口 | 46 项通过 |
| 2.2.1 事务状态复位功能在本次 2.2.2 Jar 上重跑 | 33 项通过 |
| 真正停止并重启 MySQL,再用新 Java 进程读取 | 4 项通过 |
其中 16 个并发背包保存仅一个成功;不存在与空字符串、大小写、尾空格和父宠删除得到分别校验。真正被 MySQL 行锁阻塞的写入,在关闭过程中未被中断;排队任务明确失败,运行任务解除锁后返回真实提交结果,最后释放连接池。创建确认测试在真实提交后注入响应异常,再核对原 UUID 只插入一次;这不是网络包故障注入。
同一最终 Jar 在 Paper 26.1.2、Paper 26.2、Folia 26.2 各通过 27 项检查,合计 81 项。覆盖启动、正常停服、旧台账初始化、配置补缺与自定义保留、Bukkit 物品编码及容量缩小时拒绝截断、SQLite 背包恢复和旧快照拒绝。
首轮服务端检查发现旧版合法空技能书台账以中文注释加 {} 保存时未正确识别;已修复严格空台账兼容并新增迁移用例,最终使用保留的原始文件复测通过。含未确认历史条目的台账仍进入人工核对,不因此放宽自动退款规则。
上述是本次最终成品的单元测试、真实数据库与服务端检查,不等同于真人客户端验收,也不是用户生产 MySQL 的现场检查。测试数据库与三台隔离服务端均已正常停止。
最终已验成品 Jar 的 SHA-256:
3004DE0F2E1ECB725E1F65E34B26FF5BD81E659B9E086C461E6B57E585276B8C