我正在开发一款回合制休闲MMORPG游戏服务器。
处理网络的低级引擎(不是由我们编写的), 多线程,计时器,服务器间通信,主游戏循环等,是 由C ++编写。高级游戏逻辑由Python编写。
我的问题是我们游戏中的数据模型设计。
首先,我们只是尝试将播放器的所有数据加载到RAM和共享数据中 当客户端登录并安排定时器定期刷新数据时,缓存服务器 数据缓存服务器和数据缓存服务器将数据保存到数据库中。
但我们发现这种方法存在一些问题
1)需要立即保存或检查某些数据,例如任务进度, 升级,项目&赚钱等等。
2)根据游戏逻辑,有时我们需要查询一些离线玩家 数据
3)一些全球游戏世界数据需要在不同游戏之间共享 可能在不同主机上运行的实例或在其上的不同进程上运行的实例 同一主持人。这是我们需要数据缓存服务器位于游戏之间的主要原因 逻辑服务器和数据库。4)玩家需要在游戏实例之间自由切换。
以下是我们过去遇到的困难:
1)所有数据访问操作都应该异步,以避免网络I / O. 阻止主游戏逻辑线程。我们必须发送消息到数据库或 缓存服务器,然后处理回调函数中的数据回复消息 继续进行游戏逻辑。写一些温和的东西很快就会变得痛苦 需要与db和游戏逻辑多次交谈的复杂游戏逻辑 分散在许多回调函数中使得它很难理解和 维护。
2)ad-hoc数据缓存服务器使事情变得更复杂,我们很难维护 数据一致性并有效地更新/加载/刷新数据。
3)游戏中数据查询效率低且繁琐,游戏逻辑需要查询 一些信息,如库存,项目信息,头像状态等 例如,如果一步失败了整个过程,也需要交易机制 操作应该回滚。我们尝试在RAM中设计一个好的数据模型系统, 构建了大量复杂的索引,以缓解众多信息查询,添加 事务支持等我很快意识到我们正在构建的是内存中 数据库系统,我们正在重新发明轮子......最后我转向无堆栈python,我们删除了缓存服务器。所有数据 保存在数据库中。游戏逻辑服务器直接查询数据库无堆叠 python的微任务和通道,我们可以在同步中编写游戏逻辑 办法。它更容易编写和理解,并且大大提高了工作效率 改善。
实际上,底层数据库访问也是异步的:一个客户端tasklet 向另一个专用DB I / O工作线程和tasklet发出请求 阻止在一个频道上,但整个主游戏逻辑没有被阻止,其他 客户端的tasklet将被安排并自由运行。当DB数据回复时 阻止的tasklet将被唤醒并继续运行'break 点“(续?)。
通过以上设计,我有一些问题:
1)数据库访问将比以前的缓存解决方案更频繁 DB可以支持高频率的查询/更新操作吗?有些成熟的缓存 在不久的将来需要像redis,memcached这样的解决方案吗?
2)我的设计中是否有任何严重的缺陷?你们能给我一些更好的东西吗? 建议,尤其是游戏内数据管理模式。
任何建议都将不胜感激,谢谢。
答案 0 :(得分:6)
我使用过一种MMO引擎,它以类似的方式运行。它是用Java编写的,而不是Python。
关于你的第一组要点:
1)异步数据库访问我们实际上走了另一条路线,并避免拥有“主游戏逻辑线程”。所有游戏逻辑任务作为新线程产生。与I / O相比,在本底噪声中完全丢失了线程创建和销毁的开销。这也保留了将每个“任务”作为一种相当简单的方法的语义,而不是一个令人抓狂的回调链接,否则最终会出现这种情况(尽管仍有这种情况。)它还意味着所有游戏代码必须是并发的,我们越来越依赖于带有时间戳的不可变数据对象。
2) ad-hoc缓存我们使用了很多WeakReference对象(我相信Python有类似的概念?),并且还使用了数据对象之间的分割,例如: “播放器”和“加载器”(实际上是数据库访问方法),例如“PlayerSQLLoader;”实例保留了一个指向其Loader的指针,并且Loaders由一个全局“工厂”类调用,该类将处理缓存查找与网络或SQL加载。数据类中的每个“Setter”方法都会调用方法changed
,这是myLoader.changed (this);
的继承样板
为了处理来自其他活动服务器的加载对象,我们使用了使用相同数据类的“代理”对象(又称“播放器”),但我们关联的Loader类是一个网络代理(同步) ,但在千兆位本地网络上)更新该对象在另一台服务器上的“主”副本;反过来,“主”副本会调用changed
本身。
我们的SQL UPDATE
逻辑有一个计时器。如果后端数据库在最后($ n)秒内收到了UPDATE
对象(我们通常将其保持在5左右),则会将对象添加到“脏列表”。后台计时器任务将定期唤醒并尝试异步地将仍在“脏列表”上的任何对象刷新到数据库后端。
由于全局工厂将WeakReferences维护到所有内核对象,并且会在任何实时服务器上查找给定游戏对象的单个实例化副本,因此我们永远不会尝试实例化由a支持的一个游戏对象的第二个副本单个DB记录,因此游戏的RAM内状态可能与其中的SQL图像一次最多5或10秒不同,这一点无关紧要。
我们整个SQL系统在RAM中运行(是的,一个很多的RAM)作为镜像到另一台尝试写入光盘的服务器。 (由于“老年”,这台糟糕的机器平均每3-4个月烧掉一次RAID驱动器.RAID很好。)
值得注意的是,当从缓存中移除对象时,必须将对象刷新到数据库,例如,由于超出缓存RAM限额。
3)内存数据库 ...我没有碰到这种精确的情况。我们确实有“类似事务”的逻辑,但这一切都发生在Java getter / setter的层面上。
关于你的后几点:
1)是的,PostgreSQL和MySQL特别适合这一点,特别是当您使用数据库的RAMdisk镜像来尝试最小化实际的HDD磨损时。根据我的经验,MMO确实倾向于锤击数据库,而不是严格必要的。我们的“5秒规则”*是专门为避免必须“正确解决”而设立的。我们的每个人都会拨打changed
。在我们的使用模式中,我们发现一个对象通常有1个字段已更改,然后在一段时间内没有活动,或者发生了更新的“风暴”,其中许多字段连续更改。构建正确的事务(例如,通知对象它即将接受许多写入,并且应该等待片刻然后将其自身保存到DB)将涉及系统的更多计划,逻辑和主要重写;所以,相反,我们绕过了这种情况。
2)嗯,上面有我的设计: - )
事实上,我目前正在开发的MMO引擎甚至使用更多依赖于RAM内SQL数据库,并且(我希望)会做得更好一些。但是,该系统是使用Entity-Component-System模型构建的,而不是我上面描述的OOP模型。
如果您已经基于OOP模式,转换到ECS是一个相当典型的转变,如果您可以为您的目的使OOP工作,那么坚持您的团队已经知道的东西可能更好。
* - “5秒规则”是一种口语化的美国“民间信仰”,在将食物丢弃在地板上之后,如果你在5秒内捡起食物,它仍然可以吃。
答案 1 :(得分:2)
如果不对软件有更深入的了解,很难对整个设计/数据模型发表评论,但听起来您的应用程序可能会受益于内存数据库。*将这些数据库备份到磁盘(相对而言)是一种廉价的操作。我发现它通常更快:
A)创建一个内存数据库,创建一个表,在给定的表中插入一百万**行,然后将整个数据库备份到磁盘
比
B)在磁盘绑定数据库的表中插入一百万**行。
显然,单个记录插入/更新/删除也会在内存中运行得更快。我已经成功地将JavaDB / Apache Derby用于内存数据库。
*请注意,数据库无需嵌入您的游戏服务器。 **对于这个例子,一百万可能不是理想的大小。