耀世娱乐:了解耀世注册平台中网络游戏架构的前世今生

耀世娱乐中有很多游戏,你知道这些游戏的数据库吗?

4.3 文档型数据库

前文提到,大多数游戏开发者会在关系型数据库如 MySQL 和键值型数据库如 Redis 之间做选择,这同样也是其他行业后端开发的通用选择。这些数据库是游戏后端数据库的最佳选择吗?还有没有其他数据库选项,能够更快地响应数据请求,更快地持久化数据呢?

文档型数据库非常契合游戏开发。包括 MongoDB、DynamoDB 在内的文档型数据库,越来越受到开发者的欢迎,在不少前沿应用程序中都有他们的身影。当我们封装好一个文档型数据库的调用库,并放入我们游戏项目的组件库中,会发现,开发者们会慢慢把原先存储在 MySQL 或是 Redis 中的数据,逐渐修改为使用文档型数据库进行存储,开发者完全不需要知道背后的原理,只需要知道使用它又快又好用就可以。


文档型数据库比 MySQL 更快!文档数据库作为后起之秀,技术层面上大多支持分布式集群存储,自动处理数据分片,相比于 MySQL 的本地存储,跨机器复制的方式更高效,扩展起来也更加方便。同步调用一次 MySQL 的接口,在正常表规模的情况下需要百毫秒的时间量级,而文档型数据库只需要十毫秒的时间量级,而且响应时间的抖动方差不大。

MySQL vs MongoDB

文档型数据库比 Redis 更适合存储对象!键值型数据库更适合存储一个个的键值对,而大部分需要保存的游戏数据都是一个个结构体,或者说是对象,这些存储对象的格式是松散的。很多时候我们在一次操作中只想修改一个或几个条目的值,而不想把整个对象都更新一遍。如果存储在键值型数据库中,那只能把这个存储对象通过特定的格式压缩存储,例如 json、bson 或是 protobuf。无论是读取还是修改,都需要整体进行,不断地加解码、转换格式,也不能对其中一个参数进行索引查询。而如果存储在文档型数据库中,则可以部分读取、部分写入,保留了对象的格式,方便程序使用,也节省了网络资源。

如果考虑到跨进程间的同步问题,使用 Redis 锁的消耗远比在文档型数据库中用乐观锁的消耗大。可以说,在大部分场景下,结构体更适合存储在文档型数据库中。

当然,文档型数据库也有很明显的劣势。文档型数据库对事务的支持不完全,存在性能上的缺陷。在流程环节较多时,某一个环节出问题需要整体失败并回滚,传统的关系型数据库更适合这一场景。例如货币、充值等需要强一致性的系统,关系型数据库是更好的选择。

复杂事务更适合使用关系型数据库

文档型数据库不支持复杂的跨表级联查询。幸运的是,游戏后端极少会遇到跨表查询的需求。很多时候需要跨表查询,是初期设计表结构时埋下的坑。所以这一点劣势在游戏后端并不明显。

-- 不存在下述场景SELECT account.name, trade.time FROM account,trade 
  WHERE account.id=trade.id;

文档型数据库生态支持欠缺。这一点才是文档型数据库没能广泛应用的主要原因。开发者对这类型数据库的掌握程度远不如传统数据库,出问题很难解决,这通常是实际项目中最现实的难题。现有的成熟框架,三方库,示例程序几乎都是基于传统数据库设计,如果借鉴甚至原版照抄一个框架引擎进行开发的话,自然不会构思应不应该使用文档型数据库。

如何解决这个问题?我们没能力改变生态环境,但有能力封装文档型数据库,以降低数据库的使用门槛,让大部分开发者只需要用封装好的库即可,而不需要学习文档型数据库的技术细节。从本地开发角度,为了开发阶段更好的测试,可以派生出纯内存的存储实现,生产环境再切换为正式的数据库。

// Storage 存储,提供增删改查能力type Storage interface {
    // Create 创建一个新的存储对象    // value 为符合格式的结构体    Create(value interface{}) error


    // Delete 删除一个存储对象    // value 为符合格式的结构体    Delete(value interface{}, hash interface{}, args ...interface{}) error


    // Save 保存一个存储对象    // value 为符合格式的结构体指针    Save(value interface{}) error


    // First 获取符合要求的第一个存储对象    // value 为符合格式的结构体指针    First(value interface{}, hash interface{}, args ...interface{}) error


    // Find 获取所有符合要求的对象,性能远低于 First,请慎重使用    // value 为符合格式的结构体切片地址 (注:&[]struct)    Find(value interface{}, limit int64, expr string, args ...interface{}) error}// 利用内存实现一个类,方便开发人员本地调试// 利用 MongoDB 实现一个类,生产环境应用// 利用 DynamoDB 实现一个类,生产环境应用// ……// 根据配置文件选择当前环境需要使用哪一个持久化存储

文档型数据库非常适合作为游戏数据持久化的默认存储。先考虑把需要持久化的数据放入文档型数据库中,遇到一些特殊情况,例如需要强一致性保证或事务功能,再考虑用关系型数据库。

有些游戏虽然没有大规模的应用文档型数据库,但会引入文档型数据库解决一些数据存储难题。例如某大逃杀类的游戏,使用 MongoDB 实时存储游戏战斗服的数据,可以做到故障恢复、实时回滚,以及后续助力开挂玩家鉴别。

MongoDB 持久化战斗服对局信息

虽然这类数据库尚未在游戏行业大规模使用,但我相信这是未来的一大趋势,目前缺乏的只是迈过这一步的动力。

4.4 列式数据库

上面我们横向对比了在不同场景下,应该选择什么类型的数据库。接下来我们思考一个问题,当数据量持续增大,服务于游戏分析的数据应该存储在什么类型的数据库中?


一开始,我们将部分游戏数据进行分析处理,储存在关系型数据库中,用作每日报表和实时数据展示。数据量小的时候这一切很美妙,我们能很快的完成开发,代码也能很快的分析处理数据,生成报表展示的速度也很快。但随着游戏持续运营,数据表越来越大,我们在分析处理数据上遭遇了瓶颈,查询数据太慢了!

很多游戏公司直接接入第三方的数据分析服务,没有这个烦恼。如果想要自己搭建一套游戏数据分析服务,出于更好的定制化和节省成本的目的,那下面的内容会有用。

很快我们找到了解决方案,使用列式数据库存储这些需要分析处理的数据。列式数据库是以列相关存储架构进行数据存储的数据,区别于 MySQL 这类行式数据库。

例如一张表存储玩家的姓名、年龄、注册时间等信息,如果我们把单个玩家的信息紧紧的放在一个存储块上,那就是行式存储,只要我们知道某个玩家的信息存储在哪个块上,就能检索到他的所有信息。但如果我们想统计一下游戏玩家的年龄分布,那我们需要遍历所有的存储块,获取整张表的信息,这太慢了。

而列式存储,是让每个项目按分区的方式存储在相同的存储块上。如果我们需要统计年龄分布,那就只需要检索年龄的相关存储块,极大地提升了性能效率。

行与列的区别


根据这一原理,我们将业务不需要访问的数据直接存储到列式数据库中,另一部分后端业务要用的数据,拷贝出来一份存储到列式数据库中,专门用于数据分析。

应用数据的分类存储

列式数据库非常适合批量数据处理,以及多行的及时查询。我们可以结合使用像 Clickhouse、Hive、HBase 这种分布式开源的数据库,或是采用一些商业数据仓库的解决方案。

游戏数据分析是一个大话题,这里只指出这一个方向。我接触过的大游戏公司的项目里,有专门的数据分析团队负责数据分析服务的开发和产出。

最后,我们将游戏数据库方案总结为如下这张图,横向为业务逻辑开发选型,纵向为游戏持续运营后数据分析场景下的数据库迁移。

业务数据库选型与数据分析选型
判宗TG 判宗聊天群 判宗待遇频道