为啥是一首歌,因为新的需求是极端情况可以忍受玩家回档5分钟。哪些极端情况呢?redis完全不能用了,只能走mysql来落地。来保护玩家数据。

不过有个明显的问题是:mysql到底能不能抗此大责任?

介绍

首先我们得知道每个用户大概占多少内存。根据线上用户的统计来看,差不多可以认为上限为200kb

如果我们要保存10万个用户,差不多就是100,000x200Kb = 20G。这个对redis来说已经算比较大的压力了。所以自然考虑到了mysql。

mysql在这里主要扮演的是备份和还原的角色

  • 备份:定时把内存中的用户信息写入mysql,对时间比较敏感,同时需要保证数据的完整性。
  • 还原:回滚用户数据时候,停服将mysql里面数据加载到redis,对时间不是特别敏感。

做法

比较常规的做法是用一个循环套住insert或者update语句,如果10万个用户差不多需要跑10次。比较直接,但反复操作数据库连接太消耗,故没有考虑。

查询资料后,发现还有四种技巧:

  1. insert into ...on duplicate key update批量更新
  2. replace into 批量更新
  3. 创建临时表,先更新临时表,然后从临时表中update
  4. 使用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的。