net.sz.framework 框架 ORM 消消樂超過億條數據排行榜分析 天王蓋地虎


序言

 

天王蓋地虎,

 

老婆馬上生孩子了,在家待產,老婆喜歡玩消消樂類似的休閑游戲,閑置狀態,無聊的分析一下消消樂游戲的一些技術問題;

由於我主要是服務器研發,客戶端屬於半吊子,所以就分析一下消消樂排行榜問題;

第一章

消消樂排行榜大致分為好友排行榜和全國排行榜;

好友排行榜和全國排行榜的其實是重合的只是需要從全國排行榜中提取出來而已;

那么就需要記錄所有玩家的通關記錄已進行查詢;

也許你說全國排行榜只顯示前xxx名就好;但是你的好友記錄必須要的吧?你的好友不可能全部進入全國排行榜吧;

而好友排行榜基本都是要去全部顯示出來排名;

所有那么問題來了:

我們加入400萬用戶,那么每一關卡都會有400萬記錄;

目前消消樂關卡開始1200關,那么就是400萬 x 1200 = 48億條數據;這他媽的嚇死人啊;

消消樂游戲,最大的技術關鍵是排行榜查詢問題,反而寫入速度,和頻率卻不高;

還有重要的一點是每一關卡的玩家流失率大約:0.xx%;

由於我在家休息中,家里開發環境限制所以設定數據存在是sqlite、mysql數據庫,其他數據庫有待研究;如果redis 牽涉排序問題,搜索問題,么有想到好的方案;

 

第二章

我首先設計通關記錄存儲表結構模型;

需要,玩家id,通關關卡,通關星級,通關積分,通關時間

 1 class TopList extends DataBaseModel implements Serializable, Cloneable {
 2 
 3     /*玩家id*/
 4     private long pid;
 5     /*關卡*/
 6     private int point;
 7     /*星級*/
 8     private int star;
 9     /*通關時間*/
10     private long time;
11     /*積分*/
12     private int integral;
13 
14     public long getPid() {
15         return pid;
16     }
17 
18     public void setPid(long pid) {
19         this.pid = pid;
20     }
21 
22     public int getPoint() {
23         return point;
24     }
25 
26     public void setPoint(int point) {
27         this.point = point;
28     }
29 
30     public int getStar() {
31         return star;
32     }
33 
34     public void setStar(int star) {
35         this.star = star;
36     }
37 
38     public long getTime() {
39         return time;
40     }
41 
42     public void setTime(long time) {
43         this.time = time;
44     }
45 
46     public int getIntegral() {
47         return integral;
48     }
49 
50     public void setIntegral(int integral) {
51         this.integral = integral;
52     }
53 
54     @Override
55     public Object clone() {
56         try {
57             return super.clone(); //To change body of generated methods, choose Tools | Templates.
58         } catch (CloneNotSupportedException ex) {
59         }
60         return null;
61     }
62 
63 }
View Code

測試代碼

 1     public static void main(String[] args) throws Exception {
 2 
 3         SqliteDaoImpl sdi = new SqliteDaoImpl("/home/toplist.db");
 4         TopList topList = new TopList();
 5 
 6         CUDThread cudt = new CUDThread(sdi, "top-list-thread");
 7         /*設置異步操作的緩沖容量*/
 8         cudt.setMaxTaskCount(500000);
 9         /*設置單次寫入的數據量*/
10         cudt.setGetTaskMax(5000);
11         /*創建表*/
12         sdi.createTable(topList);
13 
14         /*id生成器*/
15         LongId0 longId0 = new LongId0();
16 
17         /*模擬5萬個玩家*/
18         for (int i = 0; i < 50000; i++) {
19             long id = longId0.getId();
20             /*模擬500關卡*/
21             for (int j = 1; j <= 500; j++) {
22 
23                 TopList clone = (TopList) topList.clone();
24                 clone.setPid(id);
25                 clone.setTime(System.currentTimeMillis());
26                 clone.setPoint(j);
27                 clone.setStar(3);
28                 /*隨機積分*/
29                 clone.setIntegral(RandomUtils.random(20000, 400000));
30 
31                 cudt.insert_Sync(clone);
32             }
33         }
34 
35     }

 

sqlite插入速度非常快,

[07-25 11:28:20:368:DEBUG:CUDThread.run():219] 新增數據插入影響行數:5000 耗時:115
[07-25 11:28:20:368:DEBUG:CUDThread.run():246] 當前待處理剩余數量:7257
[07-25 11:28:20:524:DEBUG:CUDThread.run():219] 新增數據插入影響行數:5000 耗時:155
[07-25 11:28:20:524:DEBUG:CUDThread.run():246] 當前待處理剩余數量:8342
[07-25 11:28:20:696:DEBUG:CUDThread.run():219] 新增數據插入影響行數:5000 耗時:172
[07-25 11:28:20:696:DEBUG:CUDThread.run():246] 當前待處理剩余數量:9129
[07-25 11:28:20:818:DEBUG:CUDThread.run():219] 新增數據插入影響行數:5000 耗時:122
[07-25 11:28:20:818:DEBUG:CUDThread.run():246] 當前待處理剩余數量:8188
[07-25 11:28:20:973:DEBUG:CUDThread.run():219] 新增數據插入影響行數:5000 耗時:154
[07-25 11:28:20:973:DEBUG:CUDThread.run():246] 當前待處理剩余數量:8424

 

我們查詢一下數據庫;

 

總數據是560多萬行數據;

查詢一下關卡數據

 

查詢關卡數據很快;

但是我需要排序

 這些就看出對比了吧,這僅僅只有不到600萬條數據呢;而且我們僅僅是查詢了全國總排行,還么有牽涉好友排行榜,多條件搜索;

 

第三章

數據庫可以增加索引的,加入索引后,查詢會快很多;

那么接下來我們修改模型,測試

 1 class TopList extends DataBaseModel implements Serializable, Cloneable {
 2 
 3     /*玩家id*/
 4     @AttColumn(index = true)
 5     private long pid;
 6     /*關卡*/
 7     @AttColumn(index = true)
 8     private int point;
 9     /*積分*/
10     @AttColumn(index = true)
11     private int integral;
12     /*星級*/
13     private int star;
14     /*通關時間*/
15     private long time;
16 
17     public long getPid() {
18         return pid;
19     }
20 
21     public void setPid(long pid) {
22         this.pid = pid;
23     }
24 
25     public int getPoint() {
26         return point;
27     }
28 
29     public void setPoint(int point) {
30         this.point = point;
31     }
32 
33     public int getStar() {
34         return star;
35     }
36 
37     public void setStar(int star) {
38         this.star = star;
39     }
40 
41     public long getTime() {
42         return time;
43     }
44 
45     public void setTime(long time) {
46         this.time = time;
47     }
48 
49     public int getIntegral() {
50         return integral;
51     }
52 
53     public void setIntegral(int integral) {
54         this.integral = integral;
55     }
56 
57     @Override
58     public Object clone() {
59         try {
60             return super.clone(); //To change body of generated methods, choose Tools | Templates.
61         } catch (CloneNotSupportedException ex) {
62         }
63         return null;
64     }
65 
66 }
View Code

 

修改模型,在玩家id,分數,關卡,這三個地方加入索引;

 1 [07-25 13:11:04:759:DEBUG:CUDThread.run():219] 新增數據插入影響行數:5000 耗時:662
 2 [07-25 13:11:04:760:DEBUG:CUDThread.run():246] 當前待處理剩余數量:22525
 3 [07-25 13:11:05:549:DEBUG:CUDThread.run():219] 新增數據插入影響行數:5000 耗時:787
 4 [07-25 13:11:05:550:DEBUG:CUDThread.run():246] 當前待處理剩余數量:24975
 5 [07-25 13:11:06:437:DEBUG:CUDThread.run():219] 新增數據插入影響行數:5000 耗時:885
 6 [07-25 13:11:06:438:DEBUG:CUDThread.run():246] 當前待處理剩余數量:27030
 7 [07-25 13:11:07:198:DEBUG:CUDThread.run():219] 新增數據插入影響行數:5000 耗時:759
 8 [07-25 13:11:07:199:DEBUG:CUDThread.run():246] 當前待處理剩余數量:27454
 9 [07-25 13:11:08:023:DEBUG:CUDThread.run():219] 新增數據插入影響行數:5000 耗時:823
10 [07-25 13:11:08:027:DEBUG:CUDThread.run():246] 當前待處理剩余數量:27449
11 [07-25 13:11:08:966:DEBUG:CUDThread.run():219] 新增數據插入影響行數:5000 耗時:936
12 [07-25 13:11:08:967:DEBUG:CUDThread.run():246] 當前待處理剩余數量:27900
13 [07-25 13:11:09:945:DEBUG:CUDThread.run():219] 新增數據插入影響行數:5000 耗時:977

 

 數據庫數據越來越多的時候,插入速度就會越來越慢;看下圖,是不是很嚇人?

1 [07-25 13:15:06:511:DEBUG:CUDThread.run():246] 當前待處理剩余數量:23473
2 [07-25 13:15:10:948:DEBUG:CUDThread.run():219] 新增數據插入影響行數:5000 耗時:4434
3 [07-25 13:15:10:950:DEBUG:CUDThread.run():246] 當前待處理剩余數量:42028
4 [07-25 13:15:14:666:DEBUG:CUDThread.run():219] 新增數據插入影響行數:5000 耗時:3715
5 [07-25 13:15:14:668:DEBUG:CUDThread.run():246] 當前待處理剩余數量:49338

 

也許與我當前筆記本硬盤有關系;電腦性能占用問題;

 插入速度是慢了,我們來看看查詢速度吧,

 

我們可以看到插入速度快很多;

當然此時的數據量確實么有之前的多,因為測試問題,插入的數據的確實很慢,

如果是mysql 數據庫,還需要加入聯合索引查詢才會翻倍提升性能;mysql插入速度會比sqlite好一點,因為sqlie本身就不適用於大型數據集合;

第四章

其實以上工作只是做到了索引優化問題;但是也抵不住越來越多的數據;對不對?

所以還是的尋求其他方式來解決問題;

我們先來分析情況

 

可以建立分區表形式;但是建立分區表,數據也只是在一個表內大hash集合,然后區分的小hash集合,然后就算mysql這種官方建議的分區表都是200萬條數據;

所以沒有嘗試過;有興趣的同學可以研究研究;

 

我想到的第二種方式,分表,之前在做游戲運營后台,接受來之多個游戲的運營日志數據,就采用了這種方式,分表,每一天一張表,來划分;

所以我很快又想到了這種方式,我以每一個關卡划分通關數據記錄,

可是仔細一想肯定不行啊,因為按照現在消消樂這種游戲來說1200關,1000多張表,維護都得忙死;而且越往后越更新越多,依然不可行!

 

任何游戲只要在生存周期內,就有有人加入,有人流失;

那么消消樂這種關卡類的游戲,那么數據量永遠都在前面關卡,越往后越少,就跟我前面說的一樣,每一關卡都會流失0.xxx%;

那么我們可以設計30張表,30張總能接受了吧?

起碼我是能接受了;

可是如何優雅的利用30張表數據的呢?

我們分析可得越靠前的關卡,數據越多,

那么我們設計前800關卡的數據存入20張表;

后面的所以表記錄到剩余的10張表中;

創建表,獲取表名的簡易算法;

 1     /**
 2      * 獲取表名
 3      *
 4      * @param point
 5      * @return
 6      */
 7     static String getTableName(int point) {
 8         int tableId = 0;
 9         if (point < 300) {
10             /*第一段前300關存入15張表*/
11             tableId = 1000 + point % 15;
12         } else if (point < 800) {
13             /*第二段后500關卡存入5張表*/
14             tableId = 2000 + point % 5;
15         } else {
16             /*800關卡以后的數據存入剩余10張表*/
17             tableId = 3000 + point % 10;
18         }
19 
20         return TopList.class.getSimpleName().toLowerCase() + tableId;
21     }

 

創建表

1         int points = 1200;
2 
3         TopList topList = new TopList();
4         for (int i = 1; i <= points; i++) {
5             topList.setDataTableName(getTableName(i));
6             /*創建表*/
7             sdi.createTable(topList);
8         }

表自動創建完成;

 1         /*模擬5萬個玩家*/
 2         for (int i = 1; i <= 50000; i++) {
 3             long id = longId0.getId();
 4             /*模擬關卡*/
 5             int j = 1;
 6             for (; j <= points; j++) {
 7 
 8                 TopList clone = (TopList) topList.clone();
 9                 clone.setDataTableName(getTableName(j));
10                 clone.setPid(id);
11                 clone.setTime(System.currentTimeMillis());
12                 clone.setPoint(j);
13                 clone.setStar(3);
14                 /*隨機積分*/
15                 clone.setIntegral(RandomUtils.random(20000, 400000));
16 
17                 cudt.insert_Sync(clone);
18             }
19             log.info("總共寫入數據量:" + (i * (j - 1)));
20             Thread.sleep(1000);
21         }

 

寫入速度,已經得到提升了,

 1 [07-25 14:24:12:293:DEBUG:CUDThread.run():219] 新增數據插入影響行數:3000 耗時:2750
 2 [07-25 14:24:12:294:DEBUG:CUDThread.run():246] 當前待處理剩余數量:3600
 3 [07-25 14:24:12:605:INFO :TopTest.main():68] 總共寫入數據量:222185
 4 [07-25 14:24:13:619:INFO :TopTest.main():68] 總共寫入數據量:223386
 5 [07-25 14:24:14:647:INFO :TopTest.main():68] 總共寫入數據量:224587
 6 [07-25 14:24:14:757:DEBUG:CUDThread.run():219] 新增數據插入影響行數:3000 耗時:2463
 7 [07-25 14:24:14:758:DEBUG:CUDThread.run():246] 當前待處理剩余數量:4200
 8 [07-25 14:24:15:690:INFO :TopTest.main():68] 總共寫入數據量:225788
 9 [07-25 14:24:16:709:INFO :TopTest.main():68] 總共寫入數據量:226989
10 [07-25 14:24:17:502:DEBUG:CUDThread.run():219] 新增數據插入影響行數:3000 耗時:2744
11 [07-25 14:24:17:502:DEBUG:CUDThread.run():246] 當前待處理剩余數量:3600
12 [07-25 14:24:17:743:INFO :TopTest.main():68] 總共寫入數據量:228190
13 [07-25 14:24:18:755:INFO :TopTest.main():68] 總共寫入數據量:229391
14 [07-25 14:24:19:783:INFO :TopTest.main():68] 總共寫入數據量:230592
15 [07-25 14:24:20:250:DEBUG:CUDThread.run():219] 新增數據插入影響行數:3000 耗時:2748
16 [07-25 14:24:20:250:DEBUG:CUDThread.run():246] 當前待處理剩余數量:4200
17 [07-25 14:24:20:824:INFO :TopTest.main():68] 總共寫入數據量:231793
18 [07-25 14:24:21:843:INFO :TopTest.main():68] 總共寫入數據量:232994

 

查詢速度在第三章已經驗證了,就不在驗證;

 

總結

像消消樂這類型游戲重點就在於排行榜數據存儲和讀取,然后寫入和讀取相比,寫入的需求遠遠小於讀取的需求;

我做的是實時數據排行榜區分;

 

當然我們還可以利用mysql的主從關系,提供讀寫分離情況,做非實時排行榜數據;

也可以利用非滑動緩存來做非實時排行榜,解決寫入和讀取的性能平衡問題,緩存可以設置比如每5分鍾或者每10分鍾更新一次排行榜數據來完成;

以上分析就不再做代碼測試;

這是我分析和我的解決方案,不知道屌大的園友們還要更好的解決方案嗎?

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM