我經常遇到需要查詢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服務器也是沒有壓力。