@Pigmon
2017-01-01T22:01:29.000000Z
字数 4600
阅读 1321
袁胜 2016M8009073008
李文强 2016M8009073141
邓灵奇 2016M8009073056
魏永福 2016M8009073069
作业
弱联机形式,作为当前移动平台游戏产品的主要联机形式,其数据更新和交互操作对实时性和同时性几乎没有要求——联机的双方或多方不必同时在线,用户数据的改变不必实时广播等特点,决定了它更加适合非关系型的数据存储方式,尤其是Key-Value型数据存储形式;而从其数据读写时间对于单个用户来说十分集中,而不同用户之间数据读写的时间又很分散的性质考虑,尤其适合基于内存的Key-Value型数据存储形式。
本文首先介绍弱联机游戏服务器的特性,以及它为何更适合非关系型的数据存储方式;之后根据本文第一作者的实际项目——网络游戏《创世神》中应用的,基于MySQL和Memcached构建的类非关系型数据存储形式进行介绍;最后是本文4位作者一同进行的,在该项目的背景下,基于Redis的完全非关系型数据存储方式的设计思路。
用户和用户之间需要实时,同时在线进行交互的网络游戏形式,也是传统的网络游戏联机模式。
它的特点是对实时性要求比较高,在线用户操作带来的数据变化,需要立刻广播给特定范围内所有其他的在线用户;并且,用户之间会因为各自的操作给对方带来影响,即数据和状态的变化。
在智能手机普及之前,桌面联机游戏为主流的时代,强联机是主要的联机模式。这与桌面联机游戏用户的使用方式密切相关——用户需要在一段连续的时间内,坐在电脑前,专注于游戏内容。
弱联机网络游戏形式是智能手机普及后逐渐广泛出现的一种联机形式,当前用户在进行联机操作——即通过操作与另外一名远程用户进行交互时,对方可以是离线状态,服务器中的对应程序通过读取对方的数据,造成与当前用户同时在线操作的表现形式。即将离线用户数据托管给服务器AI程序并与当前在线用户进行联机操作。
这种形式主要是因为很多移动平台游戏的操作方式——碎片化时间密切相关——用户通常不会在一段连续的时间内持续在线,而是相隔比较长的时间后上线一次,并且持续时间很短,如此往复。
弱联机方式是相对于强联机方式来定义的,特点是不要求双方用户操作的实时性和同时性。主要原因在于大部分移动平台游戏用户碎片化在线时间的产品特点。
《创世神》是由喜佳森(北京)文化传播有限公司研发的一款“卡牌+X”形式的网络游戏,基于移动平台。其中程序开发工作中,客户端和服务器均由本文第一作者完成,数据管理平台由程欣欣开发;美术工作由刘赟,唐广权,雷朝俊完成;测试工作主要由党俊杰,汤胜完成。其他工作均由以上人员分担。
图1-1 《创世神》游戏截图。a 登录界面;b 关卡选择界面;c 战斗场景;d 抽奖(主要盈利途径);e 一些操作会强制用户之间的互动;f 内购界面
在《创世神》中,用户有很多基本操作是需要与其他实际的用户进行组队完成的,比如战斗,强化角色,进化角色,另外游戏内其中一种货币是直接与好友的数量和活跃程度挂钩的。组队后,服务器会记录好友用户被组队的信息,给予相应的奖励。
其他的绝大部分操作,都是与其他用户无关的,而这些与其他用户无关的数据变化非常频繁,比如用户帐号自身经验值,货币,关卡进度的变化;用户卡牌的获取,卖出,数值的改变等。一个正常的操作,比如进行一个关卡,会带来一系列上述的数据变化。
而传统的关系型数据库,如本项目中使用的MySQL数据库,基本思想是将所有用户的某一类信息存储到一个数据库表中,显然并不是此类问题的最佳解决方案。
由于上述的数据存取方式的特点——其数据读写时间对于单个用户来说十分集中,而不同用户之间数据读写的时间又很分散的性质考虑,理想中的数据存储模式如图2-1所示:
图2-1 根据《创世神》数据存取方式的特点而预期的理想数据存储形式——数据以用户为单位在存储媒介上进行划分
如图2-1所示,用户离线以后,对其他用户来说,唯一需要从服务器获取的就是该用户的队长信息,而这部分信息在该用户再次上线进行操作之前是不会改变的。所以由服务器维护一个简单的列表存储该类数据就可以了。另一方面,在线用户的所有数据都在频繁的更改。
所以,理想情况是,数据是以用户为单位进行分块的,这样数据频繁的读写可以集中在一小块存储介质上,而不是任意一个操作都要更新分散在多个数据库表中的数据。
用户的数据在MySQL数据库中以多个表的方式存储,一般来说,一个表的主键就是用户的唯一身份标识ID,其他字段是根据逻辑和修改频率来划分的各种数据,如,用户基本信息:
表中每一行是关于一个用户的基本信息。
这样,一个用户的信息就分散在多个数据库表中,当在线用户进行一个普通的操作,如完成一个关卡的时候,就会对多个表进行UPDATE操作,如图2-2所示
图2-2 用户频繁的普通操作,需要对多个表进行UPDATE甚至INSERT
针对上述的情况,我们在MySQL数据库和游戏服务器程序之间,增加了一层基于Memcached的存储结构,目的是实现图2-1中的组织形式,大体架构如图3-1所示
图3-1 《创世神》目前线上产品采用的数据存储形式,与真实货币有关的操作结果,会首先实时存入MySQL数据库,并在存入之后更新Memcached的内容;其他操作会首先更新Memcached,服务器定时一起同步到MySQL数据库中
早期的卡牌游戏,在生成一张卡牌实例数据的时候,就会在数据库中Insert一条数据,这样很快存储卡牌的表就会变的很大。一般的对策是达到一定的数量就会分表,保证查询和修改的效率。
因为本项目采用了频繁读写Memcached中的数据,定时以每个表一条SQL语句刷新的方式,所以采用了新建卡牌实例不增加数据库数据的方式。
举例来说,在最简单情况下,一个卡牌实例数据只包括基卡ID和卡牌的经验值数据(实际游戏一般都还有很多其他数值),卡牌的其他数值都可以根据这2个参数计算出来。
这样可以用一个长度12的16进制数字表示,其中前4位是实例ID,4-7位代表基本卡ID,后4位代表此卡牌实例的经验值,比如000100FF代表基于1号卡牌生成的实例,目前经验值是255。这样通过基本卡牌ID,可以知道这张卡在不同等级的各种数值,如HP,攻击力等;而根据目前的经验值可以知道这张卡牌的等级。
在数据库中建立多个表代表用户的卡包,表中每条记录是分成若干组的固定长度字符串,每12位代表一张卡牌实例,如果这个位置还没有卡牌,那么处理实例ID以外,后面8位都是0。新建卡牌实例的时候就会修改相应位置的字符串。
虽然这样运算量比较大,如果是每次新建实例都重写数据库,MySQL肯定支撑不住,不过如上所述,这部分数据只有在每1小时执行一次的把Memcached中的数据刷到MySQL时才会改写,而且是每个表一条SQL语句。
图3-2 卡包数据库表的一个片段(项目中实际一个卡牌实例的数据不止12长度,还有其他数据)
服务器在重启时,以及每天凌晨定时会刷新Memcached中存储的公用的,分散在多个表中,且需要经过计算,并且需要被频繁读取和写入的数据。
非用户独有数据不是本文讨论的重点,所以只做以上简要介绍。
Memcached中会维护一个Key-Value形式的大列表,列表项的Key为用户的ID,Value为按固定格式组织的多层结构,存储了该用户在MySQL数据库表中的数据。
图3-2 Memcached中用户数据的存储形式,是以用户ID为Key的Key-Value结构
在用户登录的时候,会一次性把MySQL表中,关于该用户的所有数据一并读取出来,并存储在 Memcached 相应的列表中。
图3-3 用户登录时,取出MySQL数据库中所有该用户的信息,存储到图3-2中所示的列表中
服务器每隔1小时对当前在线用户的数据进行一次更新。当前用户的领队角色也会更新到服务器的公用好友列表之中,供其他用户组队。用户主动退出游戏或没有任何操作半小时后,服务器将其标志为离线,在每隔1小时执行一次的程序中,服务器会将所有Memcached中的用户数据同步到MySQL数据库,并将标志为离线的用户从Memchached中删除。
程序中在这个步骤中,会对每一个MySQL表生成一个单句的SQL语句,将所有之前的在线用户数据进行更新,语句的形式如下:
UPDATE [TableName] SET [Colum] =
CASE
WHEN uid=用户ID1 THEN [Value1]
WHEN uid=用户ID2 THEN [Value2]
...
END
WHERE uid IN ([所有在上次更新后进行过操作的用户ID]);
使用Memcached+MySQL的模式,需要开发游戏服务器与Memcached之间的接口,Memcached与MySQL之间的接口。另外,Memcached没有数据持久化功能,需要单独开发。一旦数据库表或游戏逻辑发生变化,Memcached层都要修改。
Memcached在内存超出额定范围后,会自动抹掉前面的数据。
Memcached数据类型单一,只支持字符串的Key-Value结构的存储。
Memcached不支持事务。
- 自带持久化存储功能,没有必要配合关系数据库使用。
- 内存超出额定范围时,Redis会自动保存超出的数据,不会像Memcached那样直接丢弃。
- 数据类型丰富,其中Hash类型适合存储对象类型。
- 支持主从同步,方便开发过程的同步以及备份功能。
- Redis有简单的事务机制。
Redis的事务功能比较简单,只能保证事务过程中不插入其他的Client命令,并没有回滚等功能。
- Memcached的cas(checked and set)命令保证数据一致性问题。
- Memcached是多线程,非阻塞IO复用的网络模型;Redis是单线程的IO复用模型,对于单纯只有IO操作来说,单线程可以将速度优势发挥到最大,但是Redis也提供了一些简单的计算功能,比如排序、聚合等,对于这些操作,单线程模型实际会严重影响整体吞吐量,CPU计算过程中,整个IO调度都是被阻塞住的。
- Memcached使用预分配内存,可以省去内存申请释放的开销,且不容易产生内存碎片,但容易造成空间浪费;Redis是现场申请内存,会多出一定的开销,并且容易造成内存碎片。
Redis的持久化存储分为RDB(快照)和AOF(追加写操作)两种方式。根据本项目的需求特点,可以采用定时快照的方式。与真实货币有关的操作会实时存储到硬盘上。其他数据变化可以采用定时——如每1小时存储一次快照的方式。用作数据保障。
但无论采用哪种方式,都不方便直接读取解析,所以对Redis中的用户数据,最终决定还是自己编写成基于XML的持久化存储,这样在其基础之上做管理系统和服务器维护脚本等工作都很方便。
图4-1 基于Redis的设计方案