文章版权归腾讯GAD所有,禁止匿名转载;禁止商业使用;禁止个人使用。
前言
上文介绍了我们的SLG手游的服务器架构设计以及网络通信部分,本文介绍数据管理部分,在数据存储方面,我选择了Mysql、Memcache和Redis,利用这三种数据库各自的优势,各自发挥所长,在项目中有着不同的应用。
游戏数据分析
前文已经对游戏数据做了大概的分析,如下图所示:
这是我个人对游戏中的数据进行的一种划分。其中,游戏数据直接使用代码将表文件中的数据加载到内存,然后从内存中读取数据即可。玩家数据则分为了热数据和冷数据两部分分别进行存储,最大程度利用Mysql和Redis各自的优势。
游戏静态数据
在我们的这款游戏中,我们的静态数据配置在CSV表文件中,在服务器启动时,我读取所有的表文件,然后取出数据放到Map中,一般以一个标志性列(如ID)为key,一个JavaBean作为value放入到Map中。
读取CSV表文件步骤如下:
1.读取CSV文件
2.按一定格式对文件内容进行解析
3.将内容封装到JavaBean
4.存放数据到Map中
读取完了之后,在代码中如果需要,只需通过key在Map中来读取即可。
在项目中,我将整个过程进行了封装,封装成了dataConfig.xml文件、*.csv文件、JavaBean、CsvLoader.java、CsvParser.java和TempletService.java,添加一个CSV文件的步骤如下:
1.在dataConfig.xml中添加csv文件路径
2.创建一个和CSV文件中结构一模一样的JavaBean
3.服务器启动时调用CsvLoader的load()方法来加载所有的CSV文件
4.调用TempletService.listAll()方法,并传入Javabean的simpleName来加载CSV文件内容到List中
5.将List中的内容按一定结构存储(我一般都存为Map结构)
dataConfig.xml
dataConfig.xml中存储所有CSV表的路径,在CsvLoader.java中直接对这个xml表中的路径下的CSV文件进行读取加载。
1 |
|
CSV文件
CSV文件中存储具体的游戏数据,这个数据表一般是由数值策划来进行配置。CSV表的本质就是按逗号进行分割的数据,如下是卡牌表的前两行数据。
1 | 卡牌ID,卡牌名称,英雄动画名称,卡牌兵种name,卡牌兵种类型,卡牌兵种美术资源ID,品质编号,所属势力,英雄等级,英雄升星等级,技能id,统,勇,智,初始兵力,初始攻击力,兵力,攻击力,克制系数,被克制系数,移动速度,爆击率,爆击倍数,普攻伤害加深,普攻伤害减免,技能伤害加深,技能伤害减免,普攻伤害点数,普攻免伤点数,技能伤害点数,技能减伤点数,KPI,技能名称,技能描述:目标 目标个数(范围)效果数值buff描述,英雄定位 |
JavaBean
添加了CSV文件之后,我们需要创建一个和CSV表结构一模一样的JavaBean,如下是卡牌表对应的JavaBean。
1 | package com.kidbear._36.template; |
CsvDataLoader.java
CsvDataLoader封装了对CSV数据的载入,包括使用SAXReader对dataConfig.xml文件的读取,以及对其中的CSV文件的内容的读取,代码如下:
1 | package com.kidbear._36.util.csv; |
CsvParser
CsvDataLoader中用到的CsvParser是具体对CSV文件按逗号分割的格式的解析的类,代码如下:
1 | package com.kidbear._36.util.csv; |
TempletService.java
服务器启动时,调用CsvDataLoader的load()方法,以完成对CSV文件的加载,之后就需要使用TempletService的listAll方法来讲数据加载到List中,TempletService根据JavaBean的simpleName来对数据进行加载,代码如下:
1 | package com.kidbear._36.util.csv; |
使用静态数据
在完成了添加和加载等一系列操作之后,就可以在代码中调用CSV表中加载进来的数据了,例如上文提到的卡牌数据表,加载代码如下:
1 | // 卡牌数据表 |
使用时只需要根据卡牌的Id,就可以取到这张卡牌的所有数据。
Mysql存储数据
我们使用Mysql作为冷数据的存储数据库,并使用Druid和Hibernate来创建数据库的连接以及增删改查的操作。在游戏数据中,我对游戏中的冷数据做了一个总结,如下图所示:
完成要存储的游戏数据的分析之后,我们就可以进行具体建模建表的工作,完成对数据的设计。由于在游戏服务器的数据存储中,数据库基本上只是一个游戏数据临时存放的地方,所以游戏数据中的关联性并不是特别强,所以不需要严密的数据库设计,只需简单的将玩家所有的数据按照一个userid进行关联即可,在使用Hibernate的时候,我们使用了Hibernate4,使用了它注解JavaBean自动建表的功能,我们只需将需要存储的Model写成JavaBean,并写上注解,在启动时,Hibernate扫描到JavaBean会自动为我们创建或更新表。
Druid数据库连接池
游戏服务器运行中经常是多个玩家同时在线的,可想而知,如果同时进行某一项涉及数据库的操作时,也会并发请求数据库,多个数据库请求就需要我们对多个数据库连接进行有效的管理,当然,我们可以自己写一个数据库卡连接池来进行数据库管理,但好在以后前辈为我们做足了工作,有很多成型的开源数据库连接池可供我们选择,常见的有c3p0、dbcp、proxool和driud等,这里我们使用阿里巴巴公司的开源产品Druid,这是我个人认为最好用的数据库连接池,它不仅提供了数据库连接池应有的功能,更是提供了良好的数据库监控性能,这是我们作为开发人员在遇到性能瓶颈时最需要的东西,感兴趣的朋友可以参考下官方github,根据官方wiki配置一个Druid的数据监控系统,通过系统可以查看数据库的各种性能指标。
Druid在github中的地址是:https://github.com/alibaba/druid
在项目中使用druid,首先我们需要导入druid所需jar包以及Mysql的驱动jar包,由于我们是maven项目,我们就直接添加pom依赖,代码如下:
1 | <!--Mysql驱动--> |
在spring的xml中对druid进行配置
1 | <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" |
然后需要在web.xml中再对druid的filter进行配置:
1 | <filter> |
至此为止,Druid的配置就算是完成,启动工程之后我们还能通过/druid路径来访问Druid提供的监控系统,更多关于Druid的使用可以参照github中的wiki介绍,了解更多Druid配置及参数设置。
Hibernate
使用Hibernate作为Mysql数据库的ORM框架,主要是因为其良好的封装,首先我个人认为Hibernate的性能是不足与和原生JDBC以及MyBatis这样的框架所匹敌的,封装的更好却带来了更多的性能损失,但我使用他也是看中他良好的封装性,因为我对性能的需求还没有达到很高的级别;其次,Hibernate很难写出复杂的SQL查询,而MyBatis却可以写出一些复杂的SQL,但在我的设计中,我不需要太复杂的查询,基本上我所有的SQL语句的where条件都是”where userid=?”,因此在性能需求上以及易用的对比上,我选择了Hibernate。
我使用的版本你是Hibernate4,因为Hibernate4提供了注解自动创建表的功能,Hibernate集成在spring的配置的xml代码如下:
1 | <!-- 定义事务管理 --> |
配置完了之后,我们需要对我们需要进行存储的数据进行注解,如君主信息的Model如下:
1 | package com.kidbear._36.manager.junzhu; |
以上代码中,@Entity和@Table(name = “JunZhu”)就可以使Hibernate在启动时自动创建一个JunZhu表,@Id的属性即设为主键的字段。创建好Model之后,就可以使用Hibernate的session进行数据库操作,这里我将数据库的操作封装为一个工具类HibernateUtil,这个工具类大家可以拿去直接使用,具体代码如下:
1 | package com.kidbear._36.util.hibernate; |
以上代码中,除了调用session的数据库操作API之外,我还使用了Memcache进行结果集的缓存,具体关系Memcache和Mysql的集合使用,在下文中在进行讲解。上文代码中首先在服务器启动时,需要构建SessionFactory,然后通过操作session开始事务,通过session调用CRUD方法进行操作,之后再调用commit方法提交并结束事务,中间如果发生异常则进行rollback操作回滚事务。
Redis存储数据
游戏中的热数据的存储我选用了Redis,Redis不仅是运行在内存上的内存数据库,并且它的数据存储结构也是很丰富的,包括String,Set,List,Sorted Set和Hash五种数据结构,我对游戏数据的热数据进行了分析,如下图:
使用Redis首先得了解Redis的五种基本数据类型,每一种数据类型都对应不同的Redis操作API,在Java中使用Redis可以使用官方提供的Jedis客户端,Jedis客户端中包含了各种数据类型的操作,我将所有的Redis操作都封装在了Redis类中,启动时调用init方法进行Redis连接,使用时通过getInstance获取实例,再调用相应的API即可完成相关的Redis操作,在init方法中,我是通过调用JedisSentinelPool去获取Redis的连接,因为我在服务器对Redis做了Sentinel的集群部署,大家可以直接拿这个Redis工具类去使用,Redis类的方法如下:
1 | package com.kidbear._36.util.redis; |
Memcache数据结果集缓存
上文在介绍Hibernate中说到了Memcache对Mysql结果集的缓存,Memcache作为一种内存数据库,经常用作应用系统的缓存系统,我也将Memcache引入到项目作为Mysql数据结果集的缓存系统,其实在实现Memcache对Mysql查询的缓存的过程中,我曾进行了多种尝试,具体有以下几种缓存模型:
1.无缓存
这种方式不使用Memcache缓存,游戏服务器的操作直接穿透到Mysql中,这种方式在高并发环境下容易引起Mysql服务器高负载情况,如下图所示:
2.查询使用缓存,更新穿透到数据库,数据库同步数据到缓存
这种方式在客户端表现来看可以提供一部分速度,因为查询操作都是基于缓存的,但实际上Mysql的负担反而加大了,因为每一个更新请求,都需要Mysql同步最新的查询结果集给Memcache,因为每一个更新操作都会带来一个查询操作,当然这个同步过程可以使异步,但是就算我们感受不到这个同步的过程,但在实际上也是加大了数据库的负载,如下图所示:
3.更新和查询都使用缓存,缓存按策略与数据库进行同步
这种方式是比较好的方式,因为客户端的所有操作都是被缓存给拦截下来了,所有操作均是基于缓存,不会穿透到数据库,而缓存与数据库之间可以按照一定策略进行同步,如每5分钟同步一次数据到数据库等,具体同步策略可根据情况具体调整,当然这种方式的缺陷就是一旦服务器宕机,那么在上次同步到宕机这段时间之间的数据都会丢失,如下图所示:
4.更新和查询都是用缓存,更新操作同时穿透到数据库,数据库同步缓存的查询
这种方式是我最终使用的方式,虽然更新操作穿透到数据库,但是我可以在保证查询效率的同时,也保证数据的安全稳定性,因为每一步更新操作都是要进行数据库存储的,并且所有的查询操作可以直接在缓存中进行,如下图所示:
需要支持缓存的类需实现MCSupport接口:
1 | package com.kidbear._36.util.cache; |
Memcache的工具类MemcacheCRUD实现了Memcache的连接,以及add,update,delete等操作,具体代码如下:
1 | package com.kidbear._36.util.memcached; |
使用Memcache进行缓存,则需要封装一个缓存的操作工具类MC,封装各种缓存操作方法,具体代码如下:
1 | package com.kidbear._36.util.cache; |
以上代码中,find方法的缓存和list方法的缓存需要分别实现,find方法缓存只需要将类名和id作为key,对象作为value即可,而list的缓存不仅需要缓存所有的结果集,还需要缓存所有的where查询条件,根据类型查询出where条件,然后根据where条件分别进行缓存。
HibernateUtil中使用缓存部分的java代码如下,其中注释的方法为上面第二种缓存模型的实现(现已被我淘汰):
1 | public static void synMC4Insert(String tName, Object o, String id) { |
总结
以上是本文对我们这款游戏中的数据管理的介绍,游戏服务器中的各种数据是多种多样的,我们应该根据各种数据的各种性质,合理利用进行存取,以保证不管什么类型的游戏数据,在我们的游戏服务器中都可以安全稳定的运行,数据安全,玩家才会放心!