说到第一次接触的项目,可能要追溯到大学时代,但那时候做的所谓的·项目·实在是有点糊弄的意思,所以,我在这里将要讲述的是我工作中接触的第一个项目。
这是一个游戏项目,名叫《航空大亨》,是一款在IOS平台上运维的游戏,也是我接触到的第一个线上的项目,为了减少不必要的故事性描述,我将直接切入此文的核心点-- 我所遇到的问题。
忘了介绍自己的角色,和大家预想的一样,我是一名RD,准确的说,当时是一个刚刚接手线上项目的初级java编程菜鸟,让我逐个给给大家分享。好吧,开始了。
1.else if,越简单越出错
诚然,做为一只菜鸟,我还是很有犯错的觉悟,在经过一段时间的熟悉后,突然一天,一个非常简单的线上需求被产品在一顿饭的时间内想了出来, 做为该项目组的服务器端程序员,此需求无可非议的落在我的头上。
需求很简单,我们要对连续登录3天、7天的玩家,进行道具奖励,并给予系统邮件。这个真的很简单,我翻看了代码,发现添加奖励已经有了实现接口,我只需要按照 产品的需要,添加相应的奖品,就完成了该需求最主要的功能。但是发信这一功能,我发现没有统一的接口,需要自己来及时写(由于项目的历史问题,后已补充), 因为我们的游戏是世界服,所以有不同的语言需要区分,于是我看了看代码内的语言枚举(LOCALE_TYPE),当时一共有3个枚举ZH,EN,JA ,我小心翼翼的写
if(** ==LOCALE_TYPE.ZH)发中文邮件
else if(** ==LOCALE_TYPE.JA)发日文邮件
else if(** ==LOCALE_TYPE.EN)发英文邮件
一切都看似那么的 顺利 , 只是因为我们当时没有服务器部署平台和非常方便的 脚本,所以 从开始更新到重启到稳定花了1个小时多,经过日志监测,No Promble!
我大大的抒了一口气,心情舒畅,下午一直沉浸在第一次无失误的遐想中。
第二天,刚到公司,我的师傅(服务端负责人),就把我叫去了,我心中一惊,难道出什么问题了? "知道你昨天的代码有什么问题么?"师傅问,我仔细的想了想, 就区区30多行的代码,换做以前在学校,我连200行的代码,写完我都不检查就提交的,而昨天却是检查了又检查,确保万无一失。于是我很肯定的回答了师傅"不知道"。
师傅笑了笑,然后告诉我,从昨天凌晨后,陆续有国外的玩家发来信给客服说,他们收到了空白邮件,到目前为止,有接近5000多国外玩家出现了系统空白邮件的现象。
我一听就傻了,怎么可能,区区30行的代码,在反复检查了那么久,还有专门的测试人员测试,上线后一直运转良好,怎么就出这样的问题呢?我不能相信,"不可能, 为什么昨天下午就OK的,晚上就出事了呢?",随着情绪的激动,我的语气变得很倔强,很无理。师傅拍了拍我的肩膀,告诉我,你在代码里是不是用else if 来收尾的。
我想了想说,"是啊!","那如果有一些玩家,使用的设备语言不在你代码所包含的范围内怎么办?这些国外玩家,使用的语言就不是你代码里的那3种语言,
所以,你最后一个条件 else if 如果换做 else 那么,那么空白邮件不就都会显示为英文了么?
同样的,如果是用switch 来做筛选,一定不要忘记加default!
我们昨天下午更新的时候,他们正在睡觉,呵呵,明白了么?",师傅笑着继续说到,"虽然这是一个很小的错误,也没有造成什么大的损失,但是这是必须要重视的问题, 在线上,你的代码要尽量覆盖所有范围,特别是一些比较敏感的区域,要小心再小心!"听完师傅的一番话,我心中了然,不过依然垂头丧气的,感觉很失败,区区30行 代码,历时4小时的编码,检查,监控,都出现了错误。"你不用灰心,这都是经验的积累,时间久了自然就会好的。"师傅安慰我说到。
总之,这次的事,虽然说很小很小,但是让我看到了之前我没有看到的问题,那就是,无论代码的复杂与否,都需要我们细心的去理清它们的每一条出口,只有穷其所去, 才能避其所致!!!
2.物尽其用,慎用资源
时间很快,我觉得已经掌握了这个游戏的大部分功能和模块了,并且自己已经开始投身新版本的开发和设计,很充实,很快乐,也相信在下一次更版的时候,将不会再犯
那些低级的错误。
俗话说,瞌睡了就有人送枕头,好像是看出了我的自信,蛋疼的产品又一次在吃饭中,想到了一个给玩家造福的方法(谁知道呢?)。由于前一阵子,我们的代码问题,导致
了一部分玩家在免费获取游戏币的时候,出现了回档问题,很多玩家反馈,大概2W多吧,出现了金币丢失现象(金币需要RMB购买),而这一次的需求就是给这2W多个玩家补偿一定的金币。
这次的需求对当时的我来说,还是有点难度的,首先,这些玩家数量比较庞大,而这些玩家的判定是根据数据库中多表多字段共同判定的,在代码里没有办法在每一个玩家
发补偿逻辑的时候都去遍历数据库,否则数据库服务器肯定会死翘翘。其次,这补偿其实就是像上一次一样的奖励,而每次奖励的物品和类型都不一样,不可能产品一吃饭
,一蛋疼,一冒出新点子,我就得把一个类写的很长,再长,长长长吧。好吧,我承认我想了半天都没有什么好的想法,只能去找师傅了。
师傅听了我的描述后,想了不到1分钟(我真看表了啊),告诉我一个解决方案,那就是补偿模版,我要介绍一下设计吗? 不要了吧,5张数据表,其实就是类似于一个
provider分发补偿id,然后每一个补偿id对应多种补偿type ,比如你要补偿道具,还是金币,还是建筑什么的。然后玩家领到补偿后,根据补偿的id对其补偿字段做一定的'按位或'操作
(e: id1=1,id2=2 玩家领了这两个奖品后的步骤和结果如下:
long r =0;
r = 1 << (id1 - 1);//r=1 0001
player.x|r //player.x =1
r = 1 << (id2 - 1);//r=2 0010
player.x|r //player.x =3 0011
//看到了么 就根据上面的2进制'按位与'或者'按位或'来判断玩家是否已经领取过补偿,这段只是插曲的demo
)
//注意
用户的领奖信息,也就是上面的 player.x,我放在了数据库中,而补偿每次会在玩家登录的时候触发判断,为了减少访问数据库的开销,我们一直都把常用数据放在缓存(MemCached)中,于是,我将这2W个玩家的ID放在一个List里,然后将这个List放在缓存中,每次玩家登录的时候,我从缓存中取出这个List,再for循环判断。
//
String compensationKey = "compensationKey";
memCached.put(compensationKey,List)
...
...
终于,我写好了补偿模块,在经过测试人员的努力测试后,我很高兴,我的代码通过了检验,可以上线了,现在的时间是下午4点。
在一阵忙碌后,更新了,上线了,工作室的同事们忙用自己的设备查看线上补偿问题,结果是一切正常,玩家可以正常游戏,一切都灰常的和谐,这让我的心充满了成就感,一种叫做"扬眉吐气"的感觉油然而生。
但是,谁也没料到的是,下午6点,在距离下班还有半小时的时候,有玩家发信告诉我们,游戏上不去了,我们大惊,包括我师傅在内的4个人,围着师傅的电脑开始排查问题,不看很和谐,一看下4跳,我们在线上的4台服务器已经全线崩溃,玩家在这个下班高峰,下学高峰的时间发来流水一般的请求让我们向服务器输送一条指令都变的那么困难。好在经过一段时间的抢修,服务器重启成功了,又变的行云流水了,这时候师傅才有时间开始查看错误日志。
这一看,傻了,错误日志竟然是空的,所有服务器出奇的傻,自己都DOWN了,还帮我把错误自己忍下去吞了,丝毫没有显露出来。这也使得我的师傅深深的皱起了眉头,丝毫没有放松的他又排查了1个多小时,到8点钟了,线上一切正常,日志一切正常,玩家反馈一切正常。"奇怪,今天就先到这里吧!"师傅让我如负释重,原来不是我的错,我这样想着。
那晚,我很有成就感,因为我一手在缔造一款玩家喜爱的游戏,玩家在我的代码中如同有生命的数据,一遍遍冲击着我的逻辑,按照我指定的规则而存活着,这真是非常美妙的感觉,我很享受。
和你们料想的一样,故戏重演,第二天一早,就听到同事说,"昨晚服务器又挂了,在9点中,然后诗杨(我师傅)继续抢修,在经历了几次DOWN机折腾后,直到折腾到凌晨才解决问题!"
我很郁闷,为啥?这是肿么了,难道我真的灰常灰常菜么,这还让人活不活了啊。
后来我问了师傅怎么回事,没错误,为什么老死机?原来,我将2W多条数据组成的list,作为一个对象放入了缓存服务器中,这样一来,每当有一个用户登录的时候,都会从这台缓存服务器提取2W多用户的数据,然后并逐条遍历。但是语法和功能上又没有什么错误,这就导致了,我们的服务器在启动后流量,内存,CPU飙升,直到DOWN机。
解决方案很简单,把这2W多用户数据组成的List对象拆开,逐条放入缓存,这样每个用户获取数据时,不就只获取自己的那条了么?
- -,在这成长的路上,不错上那么几次,看来是不行啊,我到要看看,我还要碰到啥问题。
3.数据库,没索引没效率
又是一个重大事故,这次根据需求,要做的是一个每日任务系统,具体需求我就不说了,直接切入到我犯错的地方吧!
需求里有一个记录玩家当日活跃度的需求,每个玩家一条,当完成一条当日任务后改变该用户活跃度。在建表的时候。。。
//下面是我的建表语言
CREATE TABLE `daily_activity` (
`id` char(32) NOT NULL, //这个是主键
`player_id` char(32) NOT NULL, //这个是 用户ID
`mark` int(10) NOT NULL, //这个是积分
`last_open_time` datetime NOT NULL, //。。。
`have_get` char(32) NOT NULL,//。。。
`last_fresh_time` datetime NOT NULL,//。。。
`complete_time` datetime NOT NULL,//...
`high_get` int(10) NOT NULL DEFAULT '0',//,,,
PRIMARY KEY (`id`) //标识主键
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
而 我逻辑中 最常用的 一句SQL是 update daily_activity set ...=... where player_id ='**';
大家知道结果了么,有一天我给我师傅说了线上玩家反映每日任务很慢很卡,师傅顺手一查slow_log(慢查询日志),发现daily_activity时刻都存在着,然后查看了这张表的索引,我才知道原来,索引这么重要,特别是当你要频繁使用的时候,没有索引,意味着等死。。 后来我就给player_id加了索引,呵呵,哈哈。
4.transient,用过了没?没用过,看到了别乱删啊!
再说一个吧,这是最后一个,大家知道,我们不可能每次需要用户数据,都从数据库取,啥?有人就是那样取得?好吧,我们不是,就像上面说的,我们把这些大数据对象放在缓存里。为啥放缓存?好吧。。
在java里,想把一个对象写到一个认可该对象的地方咋办? 序列化!! 聪明啊!就是序列化。而下面的这个问题,就在这个序列化上。
有一天,我在用户bean里加了一个Map变量,用来存放一些临时的变量,当我这么做的时候,我发现在这个用户bean里面,很多其他的变量前都有一个很奇怪的修饰符
private transient int isRaffleFree;
private transient PlayerBuyInfo playerBuyInfo;
private Map<String,Object> laseSerializerMap; //这个是我加的
看到了么,transient ,这是什么,我当时删掉这个修饰符后,发现程序十分正常,于是就加了这个没有transient的Map。
程序上线咯,依旧是一切正常,不过在上线一天后,我的手机短信报警了(咱的系统运维帮我做的功能,服务器出事就给我发短信,听说他有一次连续收到300条。。),一看,服务器挂了,第二天查了日志,没错误,不过运维发来一封服务器性能邮件,里面说到,服务器内存在这一天中一直上涨,直到死机。
接下来,每天都一样,我们的4台服务器轮流着死机,都是内存上涨导致,后来运维没办法了,只好写了个上限内存自动切流量的脚本,才算稳定下来,然后强烈要求我寻找问题的所在。
经过我左思右想得出的结论,问题就处在这个没有transient修饰的Map上,果不其然,度娘告诉我“当一个对象被序列化的时候,transient型变量的值不包括在序列化的表示中”,我……%*%*,怎么不早说,此用户bean是我序列化最为平常的对象之一,这张临时map如果被序列化,我想象不出有多少垃圾信息一同被序列化了。
最后,我要说的是,这些问题都很小很小,我知道跟那些大牛,老牛,变态牛比,咱这更技术一词都差着远呢,但是我就是想表述一个思想,在编程的路上,任何一点看起来不起眼的地方,有可能才是解决问题的关键所在,当我们在编程中,工作中,生活中,不顺利了,不开心了,可以从一些细小的角度观察一下,改变一下,说不定有意想不到的效果哦!
还有,兄弟姐妹们,让我们一起加油!!!