我经常遇到需要查询MySQL表中大量数据的情形。在游戏行业,一次性对一张表进行很多数据的查询一般意味都着不怎么好的情况发生了(一般是出bug需要捞线上数据进行补偿、修复等操作)。这种时候采用什么手段是有一点点的讲究的(尤其是游戏还没有停服的时候)。要求:不影响线上服务器的性能又快速查出数据。
下面我用自己遇到的一个场景来说明一下。
实际案例:
线上出了一个bug:参与集福气活动的很多玩家奖励少发了。
现在我们能取到的相关数据:
-
能计算出玩家真实可获得奖励数量的:表A(表A中有数据的玩家也有可能是正常玩家,主键是userId);
-
参与集福气活动真实发放的奖励数量:实际奖励发放表(可能存在少发奖励);
要补发给玩家的奖励可以通过表A和奖励发放表计算得出(需要补发的奖励 = 表A - 实际奖励发放表)。
现在的做法是写个job把这份补发奖励的名单拉取来(没错不等关服维护了,直接在线跑job)。这里要强调一点:只有在“实际奖励发放表”存在的玩家才有可能需要补发奖励。
这里我给了3个方案来确定所有需要补发奖励的名单:
- WHERE IN:从“实际奖励发放表”获得所有可能要补发奖励的userId列表,然后对表A进行WHERE IN找出这些玩家在表A中的数据,最后计算出补发奖励列表。代码如下:
// 实际奖励发放表数据加载
Map<Long, UserRealGainRewardInfo> map = loadUserRealGainRewardInfos();
// 获得所有可能要补发奖励的userId列表
Collection<Long> effectUserIds = map.keys();
// SELECT * FROM A WHERE userId IN(effectUserIds...)
Map<Long, A_TableInfo> map1 = DbManager.selectATbaleInfosWhereIn(effectUserIds);
// 根据 表A 和 实际奖励发放表 补发奖励
sendMail(map, map1);
- 范围查询:采用范围加载的方式把表A的数据先加内存中,然后通过实际奖励发放表确定每个问题玩家需要补发的物品数量。伪代码如下:
// 实际奖励发放表数据加载
Map<Long, UserRealGainRewardInfo> map = loadUserRealGainRewardInfos();
// 循环取一定范围userId的玩家不断处理: SELECT * FROM A WHERE userId >= 0 AND userId < ?
List<A_TableInfo> list = DbManager.selectATableInfos();
sendMail(list, map);
- for循环:伪代码如下:
// 实际奖励发放表数据加载
List<UserRealGainRewardInfo> list = loadUserRealGainRewardInfos();
for (UserRealGainRewardInfo info : list) {
// SELECT * FROM A WHERE userId = ?
A_TableInfo aInfo = DbManager.selectATableInfo(info.getUserId());
sendMail(aInfo, map);
}
第三个方法是我用来凑数的,请各位千万不要这么做。除非实际奖励发放表中玩家人数只有个位数。那到底是用第一个还是第二个呢?这个问题主要看两点:
-
实际奖励发放表的数据量;
-
表A的数据量;
涉及的知识:
-
MySQL是如何利用索引查找数据的,MySQL的主键索引如下图所示。
image.png
当我们是用 SELECT * FROM A WHERE userId 查询时会搜索索引然后找到满足条件的数据加载出来。 -
WHERE IN的执行逻辑:把IN中的列表数据逐个从MySQL查询出来;
-
数据项之间的链表结构如下图:
image.png
当我们用:SELECT * FROM A WHERE userId>0 AND userId<? 进行数据查找时,是不需要再遍历索引的,只需要一个接一个读下去知道索引不满足where条件为止。
我们的实际情况:
- 玩家名单list涉及几万个玩家;
- 表A每个分库的数据大概在1-3万条数据;
- 在线跑job
我们最后决定使用的方式:
采用方法二:范围查询的方式把数据加到内存中然后再筛选
原因:
需要查询的MySQL数据很多(玩家名单list有几万个),使用范围查找能加快查询速度并且即使将表A的数据全部加载在内存中job服务器也是没有压力。
网友评论