开始这个话题前,离上篇开发笔记已经有一周多了。我是打算一直把开发笔记写下去的,而开发过程当中必定不会一路顺风,各类技术的抉择,放弃,均可能有反复。公开记录这个历程,便是对思路的持久化,又是一种自我督促。不轻易陷入到技术细节中而丢失了产品开发进度。并且有一天,当咱们的项目完成了后,我能够对全部人说,看,咱们的东西就是这样一步步作出来的。每一个点滴都凝聚了叫得上名字的开发人员这么多个月的心血。 html
技术方案的争议在咱们几我的内部是很激烈的。让本身的想法说服每一个人是很困难的。有下面这个话题,是源于咱们将来的服务器的数据流究竟是怎样的。 算法
我但愿数据和逻辑能够分离,有物理上独立的点能够存取数据。而且有单独的 agent 实体为每一个外部链接服务。这使得进程间通信的代价变得很频繁。对于一个及时战斗的游戏,咱们又但愿对象实体之间的交互速度足够快。因此对于这个看似挺漂亮的方案,可能面临实现出来性能不达要求的结果。这也是争议的焦点之一。 数据库
我我的比较有信心解决高性能的进程间数据共享问题。上一篇 谈的其实也是这个问题,只是此次更进一步。 api
核心问题在于,每一个 PC (玩家) 以及有可能的话也包括 NPC 相互在不一样的实体中(我没有有进程,由于不想被理解成 OS 的进程),他们在互动时,逻辑代码会读写别的对象的数据。最终有一个实体来保有和维护一个对象的全部数据,它提供一个 RPC 接口来操控数据当然是必须的。由于整个虚拟世界会搭建在多台物理机上,因此 RPC 是惟一的途径。这里能够理解成,每一个实体是一个数据库,保存了实体的全部数据,开放一个 RPC 接口让外部来读写内部的这些数据。 服务器
可是,在高频的热点数据交互时,不管怎么优化协议和实现,可能都很难把性能提高到须要的水平。至少很难达到让这些数据都在一个进程中处理的性能。 数据结构
这样,除了 RPC 接口,我但愿再提供一个更直接的 api 采用共享状态的方式来操控数据。若是咱们认为两个实体的数据交互很频繁,就能够想办法把这两个实体的运行流程迁移到同一台物理机上,让同时处理这两个对象的进程能够同时用共享内存的方式读写二者的数据,性能能够作到理论上的上限。 性能
ok, 这就涉及到了,如何让一块带结构的数据被多个进程共享访问的问题。结构化是其中的难点。 优化
方案以下: google
咱们认为,须要交互和共享的数据,就是最终须要持久化到外存中的数据。总体上看,它好像一个小型内存数据库。它必定能够经过相似 google protocol buffers 的协议来序列化为二进制流。它和内存数据结构是有区别的。主要是一些约束条件,让这件事情能够简单点解决,又能知足能想到的各类需求。 lua
数据类型是有限的:
以上 6 种类型足够描述全部的需求,这在 lua 中已获得了证明。不过这里把 lua 的 table 拆分为 map 和 array 是对 protobuf 的一种借鉴。这里的 map 是有 data scheme 的,而不是随意的字典。即 key 必定是事先定义好的原子,在储存上实际上是一个整数 id ,而 value 则能够是其它全部类型。
array 则必定是同类型数据的简单集合,且不存在 array 的 array 。这种方式的可行性在 protobuf 的应用中也获得了证明。
本质上,任何一个实体的全部数据,均可以描述为一个 map 。也就是若干 key-value 对的集合。array 只是相同 key 的重复(至关于 protobuf 里的 repeated)。
这里能够看出,除了 string 外,全部的 value 均可以是等长的,适合在 C 里统一储存。每一个条目就是 id - type - value - brother 的一组记录而已。
其中 map 用二叉树的方式储存就能够知足节点的定长需求,左子树是它的第一个儿子,右子树是它的兄弟。
咱们用一个固定内存块来保存整块数据,里面都是等长的记录,map 的记录中,左右子树都用保存着全局记录序号。
string 须要单独储存,全部的 string 都额外保存在另外一片内存中(也能够是同一片内存的另外一端)。在记录表中,记录 string 内容在 string pool 中的位置。
这样作有什么好处?
因为数据有 scheme(能够直接用 protobuf 格式描述),因此数据在每一个层次上的规模是能够预估的,数据都是以等长记录保存的,对整个数据块的修改均可以当作是对局部数据的修改或是对总体的追加。这两个操做恰巧均可以作成无锁的操做。
换句话说,每次对整颗树具体一个节点的修改,都绝对不会损坏其它节点的数据。
有了这块组织好的数据结构有什么用呢?首先持久化问题就不是问题,但这只是一个附带的好处。这块数据虽然能完整的记录各类复杂的结构数据,但不利于快速检索。咱们须要在对这颗树的访问点,制做一个索引结构。若是导入到 lua 中,就是一个索引表。当咱们第一次须要访问这颗树的特定节点:体现为读写 xxx.yyy.zzz 的形式,咱们遍历这颗树,能够方便的找到节点的位置。大约时间复杂度是 O(N^2) :要遍历 N 个层次,每一个层次上要遍历 M 个节点。固然这里 N 和 M 都很小。
一旦找到节点的位置,咱们就能够在 lua 中记录下这个绝对位置。由于每一个节点一旦生成出来,就不会改变位置了。下次访问时,能够经过这个位置直接读写上面的数据了。
string 怎么办呢?个人想法是开一个 double buffer 来保存 string 。string 和节点是一一对应的关系。当节点上的 string 修改时,就新增长一个 string 到 pool 里,并改变引用关系。当一个 string pool 满后,能够很轻易的扫描整个 string pool ,找到那些正在引用的 string ,copy 到另外一个 string pool 中。这个过程比通常的 GC 算法要简单的多。
最后就是考虑读写锁的问题了。只有一些关键的地方须要加锁,而大部分状况下均可以无锁处理。甚至在特定条件下,整个设计均可以是无锁的。
btw, 这一周剩下来的时间就是实现了。多说无益,快速实现出来最有说服力。按照惯例,应该会开源。