网络游戏中玩家数据的处理

背景

网络游戏中最重要的数据莫过于玩家在游戏的过程当中产生的数据。git

能够简单的分红两类:github

  1. 存档数据
  2. 过程记录数据

第一类数据主要是相似角色『基础』信息,背包、技能、任务,以及全部(或者部分)玩家共有的王国、地图、联盟等信息。
第二类主要是相似『日志』信息同样的,好比「某个地方某角色使用了某道具」这样的操做记录。golang

这篇文章中咱们主要讨论第一种数据的处理,关于后一种用做记录和分析的数据,可能会在后面写一个专门的Blog介绍。redis

存储结构

首先咱们须要决定数据的形态,和描述的方法。
这里有一个选择:以『条目格式』为核心,仍是以『文档格式』为核心。算法

『条目格式』的优势:看起来较为平坦,方便实时落地,和各类数据库存储模式搭配方便,外部工具修改方便,便于批量处理。
『文档格式』的有点:一般是树形结构,和大部分脑中的角色模型更加匹配,在内存中处理的时候,一般来讲效率更高。数据库

采用不一样的数据结构一般会影响到项目使用的技术,甚至直接影响到总体架构和处理模型。若是使用条目,可能就会用memcache/redis存储,使用更多的无状态业务逻辑处理方式。json

咱们团队习惯的,是以 struct 为核心,配合 bson/protobuf 实现接口和序列化,主要操做都在内存中处理的方式。
使用的时候,内存中的数据结构为 Go 的结构体;落地时使用MongoDB数据库的保存的bson格式的文档(Document);在和移动端交互以及某些内部RPC接口调用的时候,使用protobuf做为传输格式;配置方面,使用etcd同步的csv条目数据。服务器

不管是bsonprotobuf仍是 csv 条目数据,均可以使用的是 Go 里面的扩展性极强的 struct tag 来完成自动转换。
1550139780426.jpg网络

数据兼容和升级

在长时间的线上业务维护和开发过程当中,玩家的数据结构老是会修改、更新的。
若是每次都中止系统,把几千万甚至上亿的存放在数据库中的数据都升级准备好确定是不可能的。一般状况下会在数据载入的过程当中进行数据兼容检查和更新。数据结构

此时,在go程序里面使用bson做为数据的最终存储方式会给咱们带来很是多的便利。bson unmarshal的自动兼容处理能够把大部分『新增』、『删除』操做都完美处理好,对于那些须要修改的字段或者复合结构,咱们彻底可使用新增操做来替代,升级以后删除旧数据便可。

使用二进制数据的那种痛苦 v0.01 -> v0.02 -> v0.0N 升级过程不再须要见到了。而咱们也不须要向使用条目存储数据的项目同样,等待一个巨大的库执行 alter table 的操做。

序列化的效率

有不少人都作过测试,有不一样的结果。咱们实际测试的结果是:

gob 最好,bson,protobuf 和 json 较差。

LRU 的选择

上面提到,须要操做的数据的,都是以 struct 的形式存放在内存中的。天然不能将全部的数据都载入,一般咱们须要维护一个数据结构,将那些好久没有使用过的数据逐步淘汰出内存。这里使用一个LRU表就能够恰当且方便的完成。
Go的一个LRU实现

获取到须要淘汰的数据,一般会将其持久化到数据库中,线上咱们遇到过这样的一个场景:

  1. 更新启动服务器以后,停服期间玩家完成的大量操做在启动以后统一处理(建筑、战斗、科研等),致使大量玩家载入内存。同时触发联盟操做,致使大量联盟数据也载入到相应服务进城的内存中。
  2. 因为大部分须要持久化的数据都是使用的一个接口,里面定义了相同的存盘间隔(或者LRU阈值)。
  3. 半小时后,巨量在启动的时候载入的玩家和联盟数据超过存盘阈值,被LRU算法筛选出来。
  4. 并发的序列化以及db操做致使MongoDB卡死……

因此,作了LRU,最好配合限流或者hash操做,防止同时间大量数据一块儿淘汰。

内存数据的丢失

数据放在内存中,老是给人一种『不靠谱』的感受,至少是有很多人给我提起过的。

然而游戏的业务中最重要的是什么?我我的认为是响应速度和可用性。

实际上,每一个半年左右总会有一些云主机无辜挂掉(该死的阿里云),实践证实,经过一些常规的手段,能够在上层避免掉软硬件crash带来的灾难,至于内存中较新的未持久化的数据,丢了就丢了吧

  • 使用适当的保底存盘策略(每一个玩家半小时至少存盘一次),加上登出或者某些特殊操做(例如充值)的即时存盘,保证数据绝大部分数据的正确。
  • 敏感操做所有记录track日志(经过kafka一类的方式保存),必要的时候从日志中恢复丢失的操做。
  • 使用云服务器自带的备份功能,将全部数据库作1小时间隔的备份。
  • 多处引用的数据,只在一个地方保存(或者仲裁),防止部分节点数据损坏带来的一致性问题(那怕出错也不要冲突)。

动态读写内存

Go的动态特性(其实就是Reflect啦),给咱们带来了一些运行时调试的便利。起码有两种方式能够实现不中止服务器修改数据:

  • 本身实现一套get/set/del的操做,读写内存中的struct object。
  • 引入lua,动态的载入自定义lua script读写内存中的数据。

因为Go到目前为止(1.11)仍然没有靠谱的动态更新方案,咱们开始考虑,在某些运营业务为主的服务中,尝试用lua来编写业务逻辑,这样能够达到不停机修复bug甚至更新程序。

相关文章
相关标签/搜索