前一篇文章讲过,游戏服务器的设计哲学是在一个封闭的环境内,针对各个环节优化以达到最大的性能。如何处置数据是达到高性能的关键问题,一般成熟的游戏公司,其实很少面临数据处理方面的压力(现在开始流行的全球同服游戏可能是个挑战),这是由于他们都有一套很成熟的技术方案了。
我们这个团队由于一开始是做web系统的,在这个基础问题上很是挣扎了一番。下面,就我们的经验大体讲述一下我们对游戏数据的分类及处置方案。
1、静态配置数据
静态配置数据就是指那些在运行过程中基本不会发生变化的数据,比如角色的技能、道具属性等,静态配置数据推荐使用静态文件来存储,我们的做法是由策划将这些数据编写成excel文件,技术人员通过工具脚本导出两份,一份给客户端使用,一份给服务器使用。具体的导出格式并不重要:我们的项目中,前端导出的是lua脚本,后端导出的是json文件。
这种存储方式的优点:
1)读取效率高,技术实现简单:
就是一段读取本地文件的代码而已,简单意味着不容易出错;
2)策划、前端、后端对数据模型达成高度的一致性:
这点对一个复杂的游戏来说是很重要的,游戏试图刻画一个虚拟的世界,再简单的游戏,它内部创造的概念名词以及这些概念之间的关系,也比其他互联网应用多得多。一组配置文件对这个世界建立了一个静态模型,如果大家对这个模型认识、表达不一致,结果是灾难性的。相比长篇大论的需求文档,一组配置文件要精确太多了;
3)维护修改简单:
游戏一般包含大量的静态配置数据,这个数据是由策划人员维护的,在产品的生命周期内,策划更愿意编辑静态文件而不是一堆数据库表;
4)方便灰度发布
静态文件伴随代码一起部署,线上环境和灰度环境互不影响。
缺点:不支持在线修改
我们通过技术手段实际上做到了可以在线修改:方式是通过推送修改后的文件到服务器,再通知服务器重新加载这个文件;这个过程需要技术来执行,显然不是一个常规操作。但静态数据之所是静态数据,是由于他的本质特性决定的,试想一下角色技能应该被经常修改吗?用户玩游戏的过程中突然多出一种从未见过技能是什么感受?在游戏这个业务里是说不通的,会破坏游戏世界的一致性和完整性。实际的情况下,静态数据需要在线调整的原因是:发现某个数值配置错误而进行微调,这不是一种常规的运营手段,所以“不那么方便”是可以接受的。
2、动态配置数据
配置数据总体上来说是很少发生变化的,但是我们仍然期望某些数据在需要时可以方便地修改(不需要技术人员的干预)。由于这种数据可以从外部修改,所以就必须要放在数据库里面了,但是由于他很少变化,所每次都从数据库或者缓存里面去拉取就显得效率太低。我们的系统采取的是一种3级存储的方案:数据库+redis+服务器内存。原始数据存储在数据库表里面,运营人员可以通过GM工具对数据库表里面的数据进行修改,然后通过一个指令来告诉主服务器”数据发生了变化“,主服务器把数据库表的数据加载到redis里面,再发出一个广播通知,所有的游戏服务器接受到这个通知,重新从redis里面拉取数据到内存里。
动态配置数据和静态配置数据最大的区别是:对它的修改是一种常规的运营需求,比如一个充值活动的起止时间,一个抽奖系统的概率干涉等等;两者之间有时并没有明显的界限,决策依据取决于对需求的理解,以及开发和运营之间的共识。还是拿抽奖系统来举例,策划准备了两套概率配置,希望运营依据运行的具体情况进行调整;那么这两套概率都是静态配置,那个切换的”开关“是动态配置。
这类数据按理不会太多,否则说明策划的想法有问题。
3、全局状态数据
有些数据是全局共享,并且变化也比较频繁,代表游戏运行某种全局状态,比如博彩游戏的总奖池状态或者棋牌游戏的房间列表,这些数据必须放到redis缓存系统,并进行实时的同步访问,幸好这样的数据一般不多。这些数据一般不需要持久化,在服务器重启的时候适当地初始化就好。
4、用户数据
用户数据包括基本属性(昵称、头像)、财富数据(金币、道具)、角色数据、任务状态数据等,可以说游戏里面用户相关的数据远远多于web系统。
这些数据又多又读写频繁,如果每次操作都访问数据库或缓存系统肯定是妨碍性能的,因此一般采取的策略是将数据加载到服务器内存,在需要的时候才同步到缓存中心或数据库。
这种延时同步的策略必然存在一致性风险:
1)不同服务器之间数据不同步:用户进入不同游戏场景会被分配到不同的服务器实例,系统要确保各服务器之间的用户数据是同步的。用户数据在某个时间段会驻留在特定的服务器实例上,要保证在切换服务器之前数据做了同步。
2)服务器内存与数据库的不同步:数据库存储的数据状态总是滞后的,这种滞后性并不是特别大的问题(参见“游戏系统的封闭性”),为了减少这种滞后性对运营造成的困扰,可以加上定时同步(周期不要太长)的机制。
3)服务器宕机导致数据丢失:定时同步能减少这种风险,在某些特定时机(比如用户获得特别大额的金币)进行立即同步也能减少风险。此外就只能依靠服务器本地日志了,通过将数据变化同步地写入日志文件,可以在宕机时用于恢复数据。实际上,在宕机时发生小量的数据丢失,给用户一个统一的小额补偿往往就够了,说到底游戏数据不是金融数据。
有很多游戏系统将用户相关的数据分成几块,每块打包成json或二进制数据块,作为一个字段放在数据表里面。这种完全不同于web系统的数据存储方案是有道理的:
1)游戏里面数据更新虽然频繁,但并不会实时入库;需要入库时,写入效率更佳;
2)游戏里面涉及的数据结构特别复杂,如果采用分散字段的方式来存储,造成表结构复杂,后期的表结构调整也是个麻烦事
3)数据库里面的数据可以直接抛给客户端;
5)数据备份更容易。
这样的数据字段没法在外部通过sql进行读写和统计,你得提供一套完整的运营工具来做这些事情。有些游戏系统会做一个折中方案,某些重要的字段独立存储(比如用户ID,账号、金币),其他的打包存储(我们的游戏系统也在往这个方向发展)。
由于历史原因,目前我们的用户数据存储方案设计有点偏web系统,数据库表比较多,大概可分成3类
A、用户属性数据和财产数据:诸如昵称、金币、道具等数据
这些数据是最重要的数据,需要定期做数据库备份,一旦出现重大漏洞和运营事故,要做数据回档。
B、有时效的数据:比如任务、签到等
这一类数据一般在一个固定的期限内只有一条有效记录,可以按时间戳来生成新的记录,老的记录由dba定期删除就好(比如保留最近7天),这样程序逻辑会比较简单,也能保留一定的历史记录。
C、日志数据:比如各种战斗记录,金币流水等
这一类数据写入量比较大,一般单独一个数据库来存放,然后做定期的删除(比如保留最近20天)。此类数据可以用来辅助客服工作,以及在出现小的bug和运营事故时作为补偿用户的依据。
5、流失用户数据备份:
游戏运行久了,就有很多的流失用户,这些用户的信息停留在系统里面,会慢慢影响数据库的性能,对不分服的棋牌游戏来说,这个问题必须要处理。对于分服的游戏来说,为了提高硬件的利用效率,在一组服务器用户流失多了以后,也会考虑合并多组服务器的活跃用户数据以节省硬件资源。
我们做的是棋牌游戏,所以是不分服的,解决垃圾用户数据问题的方案是:定期将不活跃的用户(比如半年没有登录)数据,放到一个静默用户数据表里面,当用户登录的时候先去活跃表里面查找,如果找不到,就去静默表查找;如果在静默表里面找到用户数据,就将他恢复到在线表里面来。按照上面的分类定义,只有A类数据需要转移,所以需要转移的数据实际上并不太多。
这样,对活跃用户来说,他的登录时间是比较快的;对于新用户来说,第一次登陆会额外增加一次在静默表查找用户的时间;对于流失又回来的用户,会增加一个恢复数据的时间。为了防止静默表越来越大,可以设定一个规则,如果用户数据在静默表又待了1年,那么可以考虑将它的数据直接删除,另外对于身上没有什么财富的流失玩家,其实可以更早一点将他删除,比如半年甚至更短。
只有上面所述的A类数据是需要备份的,所有有时效的数据,我们都不备份(我们倾向于相信,一个静默用户的这类数据早就失效了)。由于用户的数据被存储到多个数据表,对所有这些数据库表分别做备份,非常繁琐,而且一旦数据库表调整,对应备份表的也要作调整,非常麻烦。为了解决这个问题,我们在备份流失用户的数据时,将用户所有需要备份的数据打包成一个json,存到备份表的一个字段(上面说了,有很多游戏的用户数据本来就是这么存储的,备份就很easy了)。在恢复备份数据时候,就解析这个数据块并分别写入不同的数据库表。