适合游戏服务器开发的ORM

ORM对象关系映射 (Object–relational mapping),一般是用来映射逻辑数据结构和数据库的,用于修改数据结构后自动生成 SQL 来操作数据库。但是这不适合游戏开发,游戏中修改内存数据一般都不是立即写入数据库的,因为修改内存数据后立即写入数据库的话就太频繁了,都会选择定时写入数据库,而且修改玩家的部分数据后不是完整的写入整个玩家的数据,而是只写入修改的部分。

定时写入修改的数据是比较容易实现的,但是只写入差异的数据就不是很容易,一般的游戏框架都只提供了接口用于手动进行脏标记哪块数据修改了,定时器到了就写入有脏标记的数据。如何让脏标记变得自动呢?之前写过一篇文章 golang 脏数据模块 是用于 golang 里实现自动标记数据变脏的,不过没有实现部分层级变脏,而是直接把 root 节点置为脏。

这一次想要实现的是在 Lua 中做一套内存数据和 MongoDB 数据库数据映射,内存数据修改后,通过元表的 _newindex_ 来实现在对字段赋值时自动把数据标记为脏。然后定时把差异数据生成 MongoDB 的格式,类似这样:

{
  $set = {
    hash = {
      x = x,
    },
    arr.1 = 11,
    a = aa,
  },
  $unset = {
    arr.3 = {
    },
  },
}

这样就做到了只更新差异数据的效果,而不是修改内存数据后完整的更新所有字段。这个实现方案的原理见 跟踪数据结构的变更 ,代码实现是使用了 jojo59516 的 Fork 版本 jojo59516/tracedoc ,原因是云风觉得他的实现有道理 cloudwu/tracedoc#8 ,当然,我也觉得有道理。。。

不过我删除了合并数据差异的代码,因为在我这里用不上这块,只需要生成差异给 MongoDB 执行,数据初始化是从 MongoDB 里加载的。修改后的代码见 hanxi/lua-dirty-mongo ,关键就是 commit_mongo 函数的实现。

Lua 中的内存数据可以使用 proto3 定义结构来定义,这样可以防止写入未定义的字段,比如常见的一个例子:字段的某个字母写错造成读取和写入用的不是同一个字段。如果不用 proto3 来定义数据结构的话,就需要搞一种新 DSL 来定义数据结构了,这里只是为了介绍整套 ORM 想要的效果,选用 proto3 只是为了快速实现 demo 效果。

proto3 支持 map ,proto2 不支持。

用下面的图表示数据流转的样子:
Diagram 2

假设玩家的数据结构定义如下:

message User {
	uint32 uid = 1; // 玩家id
	map <uint32,Item> items = 2; // 道具列表
}
message Item {
	uint32 id = 1; // 道具id
	map<uint32,uint32> props = 2; // 道具属性
}

可以生成类似下面这样的 Lua 对象:

function MAP(key, value)
	return {
		key = key,
		value = value,
	}
end
local Item = {
	id = UINT32,
	props = MAP(uint32, uint32),
}
local User = {
	uid = UINT32,
	items = MAP(uint32, Item),
}
return User

这样就可以给这些 Lua 对象加上元表,控制字段的写入和读取,访问未定义的字段就可以报错,字段类型不一样也可以报错。

这一块还没有实现,预计是用 starwing/lua-protobuf 来实现对 proto 文件的解析,然后生成 Lua 对象。等这块全部实现后在去我的 skynet-demo 里写个使用示例。


已经完成了 schema.lua 的基础结构格式: https://github.com/hanxi/lua-dirty-mongo/blob/main/schema.lua

目前只有表和子表结构,未来有空再加上 array , map 。最后再加上从 proto3 自动生成就算结束了。


最终的开发游戏逻辑的效果应该是这样的:

在框架层面做好数据的加载和写入,比如首次访问数据直接从 MongoDB 里加载完整数据,然后用 dirtydoc 包裹起来给到玩法逻辑,玩法逻辑操作 dirtydoc 对象,框架每 5 分钟定时检查 dirtydoc 是否有差异,有差异就把差异写入 MongoDB。

这样写玩法逻辑就不用关心数据是怎么从数据库中加载出来的和落地的。


点击进入评论 ...