7-1 不能忘记的事情
记忆和遗忘
当插件的功能逐渐变得复杂时,你会发现有更多的数据要存储,比如下面这个经典的插件:
旅行背包(Backpack)
为玩家提供额外的背包空间。
- 背包不与任何箱子或潜影盒关联,属于独立空间。
- 通过命令
/backpack或/bp能够打开背包。- 可以在配置文件中设置背包大小。
- 背包中物品即使死亡也不会掉落。
回想一下我们的菜单插件,玩家的物品栏和我们创建的物品栏会同时显示,玩家可以从中取出物品,或者向其中放入物品。在之前,我们只是简单取消这些事件,不过现在我们希望利用这个功能,让玩家能在物品栏之外拥有额外的背包空间。
遗憾的是,简单创建一个 InventoryHolder 是不足以实现背包功能的,因为 Bukkit 并不会自动记住物品栏的内容 —— 插件需要负责存储所创建物品栏的内容,否则,在物品栏关闭(并且被 Kotlin 清理)后,物品栏中的内容就会丢失。对于菜单插件而言这不是什么问题,因为我们每次都创建一个新的物品栏,但对于背包插件,如果关闭后里面的东西就丢失了,那还不如叫做『垃圾桶』插件呢(笑)。
外置存储
你可能会想,我们能够像之前一样,把物品栏数据存放在玩家的 PDC 中,但是在 PDC 中存储像物品栏这样的数据不是太好的选择,原因在于 PDC 和其所有者的数据存储在一起,而物品栏的数据可能非常复杂(特别是如果物品栏中有书或者潜影盒的情况下),将它们全部存放在 PDC 中会影响玩家数据的读写性能,并且可能会被坏人利用来攻击服务器。
你也许觉得这是危言耸听,但这种事情曾经在 1.14 附近的几个版本发生过,并且能被用来 复制物品。这种溢出是一种非常危险的漏洞,没有理由认为相同的事情不会在玩家数据中发生。
想象一下,把所有东西都塞到物品栏里,找起来肯定会非常麻烦。在游戏中,你会使用箱子来存放多出来的物品,而在插件开发中亦是如此。相比 PDC,一种更好的方法是由插件来自行存储这些数据,把它存放在 Bukkit 以外的系统中 —— 这就是数据库!
数据库(Database) 是一个宽泛的概念,泛指任何能存放数据的系统。通常而言,插件开发中所说的数据库指的是持久化数据库,它们通常有以下性质:
- 能存储远大于内存大小的数据。
- 可以同时由多个服务器上的插件访问。
- 提供方便的方法来查找数据。
- 即使遇到极端的情况也能保证数据的完整。
如果你在阅读本书前去搜索过『插件开发需要学习什么』,一定有人告诉过你要学习 SQL —— 这是一种用于操作关系型数据库(Relational Database) 的语言。不过遗憾的是,这种说法不怎么准确。关系型数据库非常强大,但使用起来也同样困难,相对的,即使完全不使用 SQL,也能创造出能可靠存储大量数据的插件。读者应当明白,任何存储技术都有其优劣,应当根据插件的功能选用合适的技术。
在插件开发中使用的数据库分为两类:
-
集成式数据库(Embedded):由插件负责运行和维护,管理数据库的代码存放在插件中。
这种数据库不需要管理员进行任何操作,插件能自行负责数据读写。SQLite 是这类数据库的典型代表。由于数据库和插件作为游戏服务器的一部分运行,因此可用的资源受限于游戏服务器,在存取大量数据时会影响游戏性能。
这种数据库有些像你基地中的一堆大箱子,要找到什么物品就得自己去翻找,而且会消耗你用来挖矿或者冒险的时间。
-
独立式数据库(Standalone):由管理员负责运行和维护,插件仅通过连接数据库进行数据存取。
这种数据库需要由管理员另行安装和启动,并在插件中配置连接信息。MySQL 是这类数据库的典型代表。这些数据库常常使用单独的进程,甚至是单独的机器,因此可以高度可靠地存储极其大量的数据,并能在多个插件之间共享,而不必受制于游戏服务器的资源。
这些数据库有些像大型的物品分类器,它们能自动分类处理数据,并且你可以很快找到需要的物品,但是建造起来非常麻烦。
最简单的数据库你已经使用过了!那就是配置文件,配置文件能存储数字、文本、表等各种内容,并且能由插件进行读写,它当然也是一种数据库 —— 虽然看上去不怎么可靠。我们可以在配置文件中存储物品栏信息,但如果玩家的数量增加,这可不是什么好主意。
选取数据库
我们需要选用另一种数据库,但是具体使用哪个?这可不是灵光一闪就能决定的,我们需要考虑所存储数据的特性:
-
数据的结构复杂吗?
不,很简单,我们仅需要存储玩家与物品栏的对应关系。玩家可以用 UUID 来表示,而 Bukkit 提供了将物品栏转换为字符串的方法。
-
数据的内容很多吗?
也许是,物品栏的内容不会少,但至多也就是几十到几百 MiB 大小的程度,不会大得离谱。
-
数据绝不能丢失吗?
也许不是,这个答案可能有点令人震惊,不过玩家背包数据相比区块存档和用户密码而言,还算是『不那么重要』的东西。换句话说,我们不希望这些数据丢失,但还没有重要到用另一台服务器来备份它的程度。
-
数据的存取频繁吗?
不,只是偶尔而已,我们只需要在玩家第一次打开背包时加载数据,并在玩家下线时存储数据。
-
数据要独立存放吗?
不,这些数据仅供我们的插件使用,我们希望使用集成式数据库,因为对于一个背包插件来说,要建立起一个 MySQL 多少是令人难以接受的。
这也就是说,我们需要一个容量较大、可靠性较高的集成式数据库,为此也许可以牺牲一些读写性能,而且它只需要存储键值关系(即 UUID 和字符串的对应关系)。根据这些条件,你可以去搜索对应的数据库,或者问问你的 AI。
在这个插件中,我们将使用 MapDB 作为我们的数据库。MapDB 能够以相对快的速度存储大量的数据,而且它非常容易使用,操作起来和直接操作 MutableMap 没什么区别,能够使用熟悉的 Kotlin 语法而不是复杂的 SQL 真是太好了!
那么,要怎么将 MapDB 加入到我们的项目中呢?这就是下一节要介绍的内容了 ——