基于现有游戏服务端(java)的数据框架调整思路

一、
现状

1.现有架构

图片
这里写图片描述

2.基于现有部分实现的一些解读

现有的架构基本属于中规中矩型的,感觉比较适合业务不是特别复杂的情况。

优势:1.自我管理的变量写起来比较灵活,快速。有什么需要存的直接搞个map就完事。

  2.DBserver存在请求队列的设计,也有线程池和数据库连接池,分别由自己和hibernate来管理。能在一定程度上缓解数据更新请求的压力。java锁降低了出现脏读的概率。

  3.缓存和hibernate均使用对象的形式来交换数据,包括别的server传输也使用对象。基本做到了面向对象来处理大部分更新和查询问题。另外也做到了不受数据库类型制约。

二、问题

该框架目前存在的问题有以下几点(基于我的了解和理解):

1.hibernate相较于mybatis和一些直接操作JDBC的框架在性能上无太大优势,好处是面向对象,封装后常见的增删改查可以较为简单应对。但对象存取无法应对较为复杂的关联查询,这里说的只是较为复杂,特别复杂的关系表目前业务还没有需求(有的话根本无法应对),关系型数据库的查询优势没有利用起来(当然理论上也可以做hibernate的关系模型)。另外关于对象操作的api也相对来说较少,一些比较常见的方法目前也没有(比如说查个count),查询无法查or 的情况。

2.目前dbServer的缓存 所有访问都基于playerId进行,对PlayerData是单线程的,对全局是并发的。也就是说读的请求和写的请求会同时在队列中,有的人在读,有的人在写,在线程资源和连接资源饱和的情况下,有可能读的操作要等写的操作进行。而事实上写的很多数据,有一些数据常年是没有人在用的,比如log. 很多频繁读的数据,比如用户名又是基本不会发生变化的,(当然客户端也是可以做缓存策略,这个不展开说)。简单的说目前是没有实现读写分离的,性能无需求没事,性能有要求的话应该是撑不住了,在大规模请求到来时会不会崩,什么时候崩还有待进行性能测试,但是从理论上来说,慢应该是会的。也就是框架的并发处理能力和吞吐量并没有优化到最好。

3.服务器运行中不需要持久化又经常变化的数据(比如玩家的状态,好友的数量)没有规范化的管理,有理论上内存泄漏的风险。并且没办法做到全局通用,目前还不具备分布式共享的能力。

4.其他未知的问题。

三、改进方案(基于网上的一些论点经验和我个人的偏好,待讨论)

优化目的:

实现读写分离,提升服务器处理能力,为分布式做框架扩展准备。

实现读写分离常见的有两种手段:

l缓存框架 的介入

缓存框架是目前比较流行的做法,因为有成熟的开源框架和成功案例的先例。经过对市面上大部分的框架的对比。有以下几个候选:

ØMemcached:多线程、不做持久化。Key_value形式存储

ØRedis:单线程、持久化。多种存取形式。

ØMongoDB:海量数据的访问效率比较强劲。

Ø其他缓存框架如:Ehcache,轻量级缓存框架,易用,轻便,但有一定的局限性。不支持的功能较多。

结合以上看法,可以看出如果考虑到数据安全性持久化方面,Redis要更稳一些。另外性能也不比memcashed差多少。(实际上不到一定的量根本看不出差别)

而MongoDB更像是另外一种数据库,适合存储海量的数据。可以考虑战斗日志,部分业务数据在后期拆分过来。也可以考虑作为LoginServer的专属数据库来做验证。

另外后期处理一些高并发的数据:比如类似秒杀/聚划算邓。从网上看,直接使用mongodb作为主数据库的游戏后端案例还不少,应该都是看上了mongodb的海量处理能力。不过从目前我们的项目来讲,这个尝试可以放到下一步来进行。先不做深层次的研究。

其他的框架暂时不考虑。

l数据库本身层面的优化

包括SQLServer、mysql 和orcale都常用数据库都提供了主从复制的功能。如果优化的足够好的话甚至不需要其他的缓存框架。但是需要不断的调整和优化。另外对数据库本身的特性调优需要一定的经验。本身层面的优化有很多地方都可以提高,和使用缓存是不冲突的,这里不展开讨论。

值得一提的是如果后期采用了分表分库的做法来提升性能时,需要相应的对缓存框架进行处理,如果处理不当,工作量有可能会较大。

lRedis

ØRedis 的特点:单线程单核cpu(需要客户端来保障提交的一致性),很好的持久化能力,分为AOF和RDB.简单的说RDB性能更快,AOF更安全。我认为不同的数据形式适合不同的存储形式。(Redis可以创建多个库和多个实例来实现不同的缓存形式)。

Ø数据类型:字符串(string),列表(list),哈希(hash),集合(set)和有序集合(sorted set)

Ø提供了事务的管理

Ø管道(pipeline),简单的讲,在面临一大堆请求的时候,会合并请求一起返回结果给客户端。

比如客户端发起请求 a字段修改成 1,紧接着又发送请求 修改成 2,紧接着又发送请求修改成3

一般的处理会回复他3次结果,但pipeline形式会等他全部请求玩直接返回3给客户端。感觉这个功能比较适合处理count的累加。当然管道特性的使用与否要开发者自己控制。

l缓存处理的几种具体形式

参看http://coolshell.cn/articles/17416.html

Ø形式1:Cache Aside Pattern

最常用的形式,也是我们目前框架使用的形式。

失效:应用程序先从cache取数据,没有得到,则从数据库中取数据,成功后,放到缓存中。

命中:应用程序从cache中取数据,取到后返回。

更新:先把数据存到数据库中,成功后,再让缓存失效。

这里我认为网上的这个论点是有道理的即:

先删除缓存,然后再更新数据库,而后续的操作会把数据再装载的缓存中。然而,这个是逻辑是错误的。试想,两个并发操作,一个是更新操作,另一个是查询操作,更新操作删除缓存后,查询操作没有命中缓存,先把老数据读出来后放到缓存中,然后更新操作更新了数据库。于是,在缓存中的数据还是老的数据,导致缓存中的数据是脏的,而且还一直这样脏下去了。

Facebook就是这种策略。

但是值得一提的是网上一些其他的使用Cache Aside Pattern策略的支持貌似都无视了这个问题,我猜想有可能是这种问题发生的可能性太小了,或者可以通过别的手段来避免。因为这个问题相较于数据库和缓存的一致性处理方式显得不是那么重要了。

Ø形式2:Read/Write Through Pattern

Read Through 套路就是在查询操作中更新缓存,也就是说,当缓存失效的时候(过期或LRU换出),Cache Aside是由调用方负责把数据加载入缓存,而Read Through则用缓存服务自己来加载,从而对应用方是透明的。

Write Through 套路和Read Through相仿,不过是在更新数据时发生。当有数据更新的时候,如果没有命中缓存,直接更新数据库,然后返回。如果命中了缓存,则更新缓存,然后再由Cache自己更新数据库(这是一个同步操作)

这两种形式的意思我明白,但具体如何实现并没有太多的案例可循,因此初步考虑放弃。

Ø形式3:Write Behind Caching Pattern

Write Back套路,一句说就是,在更新数据的时候,只更新缓存,不更新数据库,而我们的缓存会异步地批量更新数据库。这个设计的好处就是让数据的I/O操作飞快无比(因为直接操作内存 ),因为异步,write backg还可以合并对同一个数据的多次操作,所以性能的提高是相当可观的。

但是,其带来的问题是,数据不是强一致性的,而且可能会丢失(我们知道Unix/Linux非正常关机会导致数据丢失,就是因为这个事)。

这种形式的具体实现需要花时间研究一下,初步可以考虑频繁的战斗日志可以放到这里来处理。由程序来读取缓存再异步的插入到数据库中。因为redis自有的持久化功能还是比较靠谱的,所以不用太担心数据的丢失。就算是崩了,缓存的数据还在,重启后重新接着插。或者做个定期去缓存拉数据的程序在后台运行。

Ø小总结:基于以上,我认为可以根据目前的数据类型来选择处理方式.

经过总结目前主要的数据有以下几类:

1.多写不读或少读:如战斗日志,操作日志等。直接选用 write behind caching pattern.先搞到缓存里慢慢写就是了。这部分的数据存在或多或少的延迟,缓存可以只考虑保存近3个月的数据,其他的数据需要放在数据库中并设置截止日期。定期去缓存拉数据的程序在后台运行。

3个月内读缓存,包含3个月外的数据直接读取数据库。

2.多读但少写:这类数据比较典型及数量众多,比如用户姓名/头像/等级/账户/密码。这个就使用模式Cache Aside Pattern。可以考虑开辟单独的库和其他手段来彻底释放Redis的性能。

可以考虑设置比较长的失效期。

3.多读多写:比如:击杀(真人)数/战斗完成次数/战斗胜利次数/战斗平均排名/乱斗最高连胜次数/经验值/当前货币信息(金币银币)。这类数据是Redis的主要用武之地。

货币/经济系统数据会单独拉出来一个库做。涉及到钱/装备等信息的数据考虑使用事务/piepine/等手段保证强一致性。

读:

查询:直接查询缓存。没有的话查询数据库。成功放入缓存

写:

方式1:

先更新缓存,后更新数据库。至于如何更新数据库和什么时候更新数据库可以根据情况做策略。

比如:战斗胜利次数,每胜利10次保存一次到数据库。(但这样做的话,相当于根据业务定制,如何做到很好的封装是个问题)

该方式适合对一致性要求不是很高的数据。

方式2:

先直接更新数据库,后将缓存失效。缓存设置较短的失效期。

该方式适合对一致性要求很高的数据。如金钱/装备。

方式3:

先直接更新数据库,同步更新至缓存。

该方式比较中规中矩,在大并发下会有脏数据的情况。但此种形式覆盖面会比较大。适合封装。

4.少读少写:

比如游戏崩溃次数.这种数据建议不放到缓存中。

5.多读不写(不需要持久化):玩家的状态/好友的数量/好友的状态, 临时组队的信息

此种数据可以根据需要放入缓存中。由于持久化又占用硬盘。但是各个变量的命名方式需要制定一些标准。

Ø关于上面说的第3点中Mysql往缓存同步的实现

目前查到有两种方式感觉较为靠谱:

1.通过UDF使mysql主动刷新redis缓存

使用的mysql的User Defined Function功能,mysql_udf_redis是有人实现的同步数据到Redis的功能,弊端:需要学习成本, 二来,第三方的插件不稳定。udf函数是C++写的。另外需要挨个表创建触发器,这个很蛋疼。感觉有点low.但是这个好处是较为底层一些,写法不用太关心应用层怎么弄了。

参考:http://blog.csdn.net/socho/article/details/52292064

2.使用阿里的canal

个人感觉这个更为靠谱一些,这东西有人维护,功能也比较强大。简单的说就是数据库一有变化就会被canal捕捉到,他直接解析mysql的binlog将变化更新到缓存。另外他的设计模式是消费者/生产者模式,一有变化他就放一个增量变化的更新放到那里,所有的客户端可以自由的来获取更新。这个可能更适合多个gameserver下的主从复制的分布式。扩展性更高。

这个的劣势是考虑到用久了有可能依赖性比较强,错误不好排查。如果挂了,很多缓存数据容易丢失。需要有一定的容错机制,这个我相信这个设计者都考虑到了,如果确定要用了待深入研究。

l缓存变量的初步命名规则:

Ø持久化通用数据:使用:做分隔符,比如一个人的名称

可以通过 player:1:name 来获取.

Ø持久化通用数据:可以使用方法名+参数的形式来做key比如

getPlayerList(155),这种形式的比较好记一些。

Ø持久化特殊数据:一些复杂查询的结果。使用sql对应的md5加密来作为key

比如查询一个人a的好友b的头像imgid

Select p.imgid from player p

inner join player_invitation pi on pi.sender_id=p.id

Where receiver_id= ‘b’

将以上字符串做md5加密后,key为2B6C2BD990C0E76EF1A0ECB8BA9A0B9E ,value 为一个结果集

Key 为 imgid,value为5

即2B6C2BD990C0E76EF1A0ECB8BA9A0B9E:2B6C2BD990C0E76EF1A0ECB8BA9A0B9E_1:imgid =5

考虑到每次写sql的轻微差异都将造成key的不一致从而导致重复无效数据,

java框架中将新增一个生成标准SQL的类来规范sql的写法,另外表名的缩写名将按照以下规范:

比如表明为 player_invitation ,缩写就是pi,全部用小写表示。

(上述例子不是太合适的例子,该种处理仅适用与比较复杂的SQL.比如查询一个人的好友中比他在服 务器排名高的好友头像)

Ø非持久化数据

      可以考虑使用服务类型:数据类型:变量名的形式,如在线玩家

Game:string:online_player:

数据为:
这里写图片描述

l关于数据库操作的方案

原有的方式是直接调用hibernate,由hibernate基于实体类生成SQL调用jdbc来进行对象的操作。

建议在现有的基础上补充SQL直连JDBC的形式来加快效率(开发效率和运行效率)。在传输上依然使用对象传输。包括线程池、数据库连接池、对象锁等等。这里只需扩展一个绕过hibernate直连JDBC的补充方法。

需要封装一个基于key_value的通用类 EntityObject。

需要封装一个生成SQL的通用类 SQLEntity。该通用类将根据数据库类型生成不同的语句.

生成一个直接调用JDBC的通用方法 SQLCommand。

效率比较案例(不考虑缓存介入):
这里写图片描述
需求:

原有做法,将我的所有战斗数据全部拉出来,然后使用java程序循环分析他的胜利、排名等等,使用变量累加的形式将各个数据拿出来。

现有做法:

直接编写SQL得到EntityObject对象,发送SQLEntity对象至DBServer,DBServer拿到SQLEntity对象中生成的SQL得到结果返回EntityObject。

这里写图片描述

从开发效率上少了遍历和很多变量的判断。从性能上直连JDBC ,速度也有保障。唯一不足的是不能利用hibernate的基于id的二级缓存机制。因此常见的单个对象查询还是可以使用hibernate。实际上我们原本也没有使用hibernate自身的缓存机制,而是自己写的Storge这个工具类。

四  新的示意图

图片

这里写图片描述

新的示意图

五、有可能出现或需要注意的问题

Redis是单线程阻塞的,因此会使用队列来工作。但是当某个发送过来的请求处理过慢时,会造成后续请求timeout.因此需要防止慢查询。

参考:http://carlosfu.iteye.com/blog/2254154

由于没有实践经验,考虑先从边缘业务入手缓存。再逐步介入核心业务。

要考虑将缓存和DB服务剥离开来,保证缓存框架down了之后依然可以正常运转,数据不丢。

缓存层数据 丢失/失效 后的数据同步恢复问题。

nosql层做好多节点分布式(一致性hash),以及节点失效后替代方案(多层hash寻找相邻替代节点),和数据震荡恢复了。

理论上如果在应用层和数据访问层之间加个缓存就需要改大量代码,那么说明原来写的就有问题。因为如果应用层只通过数据访问层访问数据的话,加缓存只需要修改数据访问层的实现,对上接口是不变的

对数据库做该有的优化,防止本末倒置。

后续上分布式可以考虑上Redis-Cluster

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值