起因

这段时间我们游戏在持续推广,每天数万的玩家注册,服务器压力增加得很快。虽然我们服务器进程可以多开,redis用的是集群版,也可以水平扩展,但数据库不是,一直是只有一个数据库在支撑。即便大部分玩家数据都存放在redis中,只有redis中的数据过期后才会去请求数据库进行查询,但一样顶不住人多,现在每天日活跃玩家将近20万,同时在线接近2万,数据库压力还是很大,所以扩展成多个数据库支持是唯一选择

思考

原来只有一个数据库,现在要扩展多个数据库,把新玩家哈希到不同的数据库上,同时还要兼容原来的老玩家,依然能正确读取,更新其旧数据,不能出错

第一种方法

再买2个数据库(当然也可以买多个),加上原来的数据库,这样一共有3个数据库,新注册的玩家根据playerID对3进行取余,这样把新玩家的数据均匀地哈希到3个数据库上,以做负载均衡。当读取玩家数据时,按照playerID对3进行取余的算法,得到数据库ID,先尝试从这个数据库里面读取,如果读取不到,说明是旧玩家,那么再到旧数据库里面读取。修改更麻烦一些,因为要先判断在哪个数据库上,然后再进行更新。

看似也没什么问题,既能哈希式地存储,也能哈希式地读取

请仔细想一下,这种方法有什么问题吗?

如果后面数据库压力越来越大,3个数据库也不够了怎么办呢?只能再买几个数据库,比如5个,这样一共有8个数据库,进行新的哈希算法,新注册的玩家根据playerID对8进行取余,把新玩家的数据均匀地哈希到8个数据库上。读取的时候就麻烦了,因为有三类不同的玩家了,先对8取余得到一个数据库进行查询,没有的话再对3进行取余进行查询,仍然没有的话再到最旧的数据库上进行查询。读取更新玩家数据的话就更痛苦了,要进行多次哈希算法库判断到底在哪个数据库上,找到后才能进行更新,数据库操作需要多次

看到了吧,每扩展一次数据库,读取玩家数据的时候就要多尝试一次DB

想一想,读取,更新玩家数据库的地方有很多,每次都这么麻烦的判断,累也累死了

有个优化点,我们可以记录下来分界点玩家ID,归于哪个时间段生成的,在这个时间段内的玩家都是用一种算法,比如playerID在1~M之间的玩家直接到旧的数据库上查询,playerID在M~N之间的玩家直接对3进行取余后到对应的数据库上进行操作,playerID在N之后的玩家对8进行取余后到对应的数据库上进行操作。这个M,N可以提前在代码里面定好,玩家注册的时候也根据M,N进行不同的哈希算法。但是缺点也有,那就是M,N的预估不能出错,而且没法测试,一旦启用了这个规则,玩家数据就会被自动归类,出错就完蛋了。而且再扩展一次数据库的话要在所有选择数据库的地方修改一遍,吐血到死)

问题在哪里呢?

答案:扩展数据后,读取更新玩家数据时的算法严重依赖生成数据时的算法,二者必须一样也就是高耦合,因为有旧数据,只能多次哈希后判断在不在当前数据库上

所以我们要找到一个读取更新数据时,不依赖于生成数据时算法的办法,也就是下面方法2

第二种方法

再买N个数据库,比如2个,加上原来的一个旧数据库,一共3个。仍然采用对3取余的哈希办法,把新玩家均衡的撒在3个数据库上,但是同时记录下新玩家所在的数据库号,建议记录在redis中,这样最高效。读取更新玩家数据时,先从redis中查到这个玩家所在的数据库号,再到对应的数据库上进行操作,如果redis中不存在,说明是旧玩家那就到旧数据库中。由于redis的效率超高,每秒达到10万级别,相对于数据库的操作,这点儿耗时可以说忽略不计了

这个办法有

优点

1. 查询更新玩家数据时的算法永不需改变,只用先从redis中查找所在数据库号,再到对应的数据库操作即可,永远只有一次数据库操作。redis中查询不到说明是当初没有记,直接到旧数据库查询即可

2. 代码改动比较小,只需新玩家记录下数据库ID号,读取更新数据库时根据redis的值选择对应的数据库连接即可

3. 以后再扩展数据库的时候,只需更改玩家注册时的哈希算法。读取,更新时找数据库的算法不需更新

缺点

1. 需要额外引入redis,且这些数据在redis中永久存在

2. 若redis出问题导致数据丢了,那这些玩家就找不到准确的归属数据库了。不过现在云redis的集群模式下,数据安全是有保证的。如果你仍然不放心,也可以先到数据库中保存一份玩家的归属数据库ID,再加载到redis中做长期保存,这样更保险,即使redis中丢了,再到数据库中查询导入即可。由于我们绝大多数都是查询redis,数据库里面只是用来做备份保险用,性能不用担心