为啥是一首歌,因为新的需求是极端情况可以忍受玩家回档5分钟。哪些极端情况呢?redis完全不能用了,只能走mysql来落地。来保护玩家数据。
不过有个明显的问题是:mysql到底能不能抗此大责任?
介绍
首先我们得知道每个用户大概占多少内存。根据线上用户的统计来看,差不多可以认为上限为200kb
如果我们要保存10万个用户,差不多就是100,000x200Kb = 20G。这个对redis来说已经算比较大的压力了。所以自然考虑到了mysql。
mysql在这里主要扮演的是备份和还原的角色
- 备份:定时把内存中的用户信息写入mysql,对时间比较敏感,同时需要保证数据的完整性。
- 还原:回滚用户数据时候,停服将mysql里面数据加载到redis,对时间不是特别敏感。
做法
比较常规的做法是用一个循环套住insert或者update语句,如果10万个用户差不多需要跑10次。比较直接,但反复操作数据库连接太消耗,故没有考虑。
查询资料后,发现还有四种技巧:
- insert into ...on duplicate key update批量更新
- replace into 批量更新
- 创建临时表,先更新临时表,然后从临时表中update
- 使用mysql 自带的语句构建批量更新
进过研究,比较适合我这个项目的是方法2. 1和4主要是构建的sql语法比较麻烦,并且将业务逻辑下沉到数据库本身不是一种提倡的做法。3的开销主要是建立表,如果中间出现数据库问题,也是代价比较大。
迭代
由于内存限制,不大可能把10万个用户都加载到内存。所以准备加载1000个用户,然后循环100次。
第一个版本比较直接,就是将内存中所有用户全部构建成sql语句来执行。测试速度如下:
可以看到似乎不错,差不多10分钟内可以完成整个过程。但是离我们的目标5分钟还是有距离。
第二个版本:我固定了每次执行的用户数目,比如500个。这样改造后的速度:
已下子到了6分钟。看来批量定长是一个不错的方向。
第三个版本:由于数据本身200kb一个有压缩空间,所以我把每个用户数据进行了gzip压缩再存储。这次到达速度为:
结果不错,已经到达5分钟级别了。那么还能不能更快呢?我做了第四个版本是多线程的,但结果不是特别好:
这个版本基本和第三版本持平,唯一的好处做成一个异步的过程。当然也有可能是我实现得不对:)
如何进一步挖掘潜力
似乎单机10万写入速度已经到了极限,提升不大了。这个时候比较好的思路是拆表了,加入我把10万个用户拆成5个表,每个表只负责2万人。相信整体速度会有个更大的提升。
由于游戏的规模可能就是10万人同时在线,所以每台服务器内存很可能就是只负责差不多这10万人的一部分。那么为什么不直接用一张表来放呢?
其实这里基于两点考虑:
1.多个线程写一张表可能会出现竞争甚至死锁的问题。
2.然后玩家可能会在多个服务器都有记录,这个时候由于同步机制的问题,很可能老的数据反而覆盖了新的数据问题。
当然分成多个表带来的问题是如何识别重复数据哪份更新。最简单的做法是使用时间戳, 不过考虑到机器的时间差,其实用时间戳不是一个特别好的方案。还是需要使用redis或者zookeeper加锁机制来获取一个递增的版本号来管理。当发现两份数据的key值相同时候,通过对比版本号大小即可。
结语
这次是基于C#的Dapper库做的实验,其他的链接库应该差不多。主要是这一通操作的思想:尽量把相同的数据批处理,然后是定长,再配合压缩和异步的技术,那么这么大的数据也是可以写入mysql的。