論《數據落地》的方案總結


序言

作為一個游戲服務端研發人員,從業10余年,被問及最多的話題是什么?

1,你們怎么處理高並發,
2,你們的吞吐量是多少?
3,你們數據怎么落地,服務器有狀態還是無狀態。
4,xxxxxxxxxxx

 做如此類的問題,我相信這幾個典型在被同行,領導,運營方,提出和問到最多的問題了。

 今天我們重點是講解數據落地方案。比如吞吐量啊,高並發啊在前面的文章也提到過,有興趣的小伙伴可以自行查看哦

如果有什么問題就提出來,

言歸正傳

在此我先描述一下,游戲服務器的有狀態和無狀態區別,這是本人的描述好理解或許和你們不太一樣別太介意就行;

我所說的無狀態是指類似http服務器一樣,沒有數據緩存,所有的數據操作流程是

-> read db -> use -> save db;

有狀態是指數據緩存在程序內部變量,第一次需要的時候發現緩沖池中沒有加載到

-> memory cache -> read cache -> use -> save db(異步定時落地) -> 長時間未使用 memory delect;

 本人在這么多年的游戲服務端研發中,都是做的有狀態服務,

其實不管是有狀態還是無狀態都會牽涉一個問題,那就是數據落地;

一般來講我們的數據落地都分為,同步落地和異步落地兩個大類,

同時還有兩個分支方案,就是全量落地和增量落地;

 也就是說分為:

同步全量落地,同步增量落地,

異步全量落地,異步增量落地,

具體方案其實都是根據你的業務需求來,如果要保證萬無一失,那么肯定是同步落地最為保險,比如TB,JD訂單系統,但是帶來的效果就是響應慢,

我們知道不管是秒殺還是雙十一的血拼搶購,你是不是總感覺搶不到?或者提交訂單慢的要死?《當然這不在本次討論的范圍》

我們今天講解的是在游戲內如何做到數據落地;

我們先來建立一個實體模型類

 1 package com.ty.backdata;
 2 
 3 import java.io.Serializable;
 4 
 5 /**
 6  * @program: com.ty.minigame
 7  * @description: 數據測試項
 8  * @author: Troy.Chen(失足程序員 , 15388152619)
 9  * @create: 2020-08-27 09:04
10  **/
11 public class DataModel implements Serializable {
12 
13     private static final long serialVersionUID = 1L;
14 
15     private long id;
16     private String name;
17     private int level;
18     private long exp;
19 
20     public long getId() {
21         return id;
22     }
23 
24     public void setId(long id) {
25         this.id = id;
26     }
27 
28     public String getName() {
29         return name;
30     }
31 
32     public void setName(String name) {
33         this.name = name;
34     }
35 
36     public int getLevel() {
37         return level;
38     }
39 
40     public void setLevel(int level) {
41         this.level = level;
42     }
43 
44     public long getExp() {
45         return exp;
46     }
47 
48     public void setExp(long exp) {
49         this.exp = exp;
50     }
51 
52     @Override
53     public String toString() {
54         return "DataModel{" +
55                 "id=" + id +
56                 ", name='" + name + '\'' +
57                 ", level=" + level +
58                 ", exp=" + exp +
59                 '}';
60     }
61 }

通常情況下我們怎么做數據落地

 通常情況下的同步全量更新

這就是說,每一次操作都需要把數據完全寫入到數據庫,不管屬性是否有變化;

這樣一來全量更新就有一個性能問題,如果我的模型有很多屬性(這里排除設計問題就是有很多屬性),而且某些屬性內容特別多,

然后這時候我們只是修改了其中一個不重要的數據,比方說

 玩家通過打怪獲得一點經驗值,修改了經驗值屬性之后,需要save data;

這里只能全量更新;這樣實際上浪費了很多 io 性能,因為數據根本沒變化但是依然 save to db;

那么我們在這個時候我們是否就應該考慮,如何拋棄掉沒有變化的屬性值呢?

這里我們就需要考慮如何做到增量更新方案;

首先我們在考慮一點,增量更新就得有數據標識狀態,

可能我們首先考慮到的第一方案是這樣的

我們修改一下datamodel類

首先我們新增一個Map 屬性對象來存儲有變化的值

 

 接下來是重點了,我們來修改屬性的set方法

改造后的模型類就是這樣的,

 1 package com.ty.backdata;
 2 
 3 import com.alibaba.fastjson.annotation.JSONField;
 4 
 5 import java.io.Serializable;
 6 import java.util.HashMap;
 7 import java.util.Map;
 8 
 9 /**
10  * @program: com.ty.minigame
11  * @description: 數據測試項
12  * @author: Troy.Chen(失足程序員 , 15388152619)
13  * @create: 2020-08-27 09:04
14  **/
15 public class DataModel implements Serializable {
16 
17     private static final long serialVersionUID = 1L;
18 
19     /*存儲有變化的屬性 由於這個字段屬性是不用落地到數據庫的 需要加入過濾標識*/
20     @JSONField(serialize = false, deserialize = false)
21     private transient Map<String, Object> updateFieldMap = new HashMap<>();
22 
23     /**
24      * 存儲有變化的屬性
25      *
26      * @return
27      */
28     public Map<String, Object> getUpdateFieldMap() {
29         return updateFieldMap;
30     }
31 
32     private long id;
33     private String name;
34     private int level;
35     private long exp;
36 
37     public long getId() {
38         return id;
39     }
40 
41     public void setId(long id) {
42         this.id = id;
43         /*我們考慮數據庫的屬性映射就用屬性名字做為映射名*/
44         this.updateFieldMap.put("id", id);
45     }
46 
47     public String getName() {
48         return name;
49     }
50 
51     public void setName(String name) {
52         this.name = name;
53         /*我們考慮數據庫的屬性映射就用屬性名字做為映射名*/
54         this.updateFieldMap.put("name", name);
55     }
56 
57     public int getLevel() {
58         return level;
59     }
60 
61     public void setLevel(int level) {
62         this.level = level;
63         /*我們考慮數據庫的屬性映射就用屬性名字做為映射名*/
64         this.updateFieldMap.put("level", level);
65     }
66 
67     public long getExp() {
68         return exp;
69     }
70 
71     public void setExp(long exp) {
72         this.exp = exp;
73         /*我們考慮數據庫的屬性映射就用屬性名字做為映射名*/
74         this.updateFieldMap.put("exp", exp);
75     }
76 
77     @Override
78     public String toString() {
79         return "DataModel{" +
80                 "id=" + id +
81                 ", name='" + name + '\'' +
82                 ", level=" + level +
83                 ", exp=" + exp +
84                 '}';
85     }
86 }
View Code

測試一下看看效果

    public static void main(String[] args) {
        DataModel dataModel = new DataModel(1, "失足程序員", 1, 1);

        System.out.println("查看屬性值1:" + JSON.toJSONString(dataModel));
        /*獲得一點經驗*/
        dataModel.setExp(dataModel.getExp() + 1);
        /*等級提示一級*/
        dataModel.setLevel(dataModel.getLevel() + 1);
        System.out.println("查看屬性值2:" + JSON.toJSONString(dataModel));
        System.out.println("查看有變化的屬性:" + JSON.toJSONString(dataModel.getUpdateFieldMap()));

//        /* 根據你選擇的 orm 框架 mysql mssql等等 具體操作不描述*/
//        orm.insert(dataModel) or orm.update(dataModel);
//        /* redis */
//        final String jsonString = JSON.toJSONString(dataModel);
//        jedis.set(rediskey, jsonString);
    }

輸出結果

 這樣我們通過更改set方法,得到更新的屬性字段來進行增量更新;

可能看到此處你是不是有疑問?這就完了?

 當然沒有,這樣的方案雖然能得到有變化的屬性值,

但是別忘記了一點,我們的程序可不止這一個數據模型,可不止這幾個字段,並且我們開發人員可以不止只有一個。

這樣的方案雖然可以解決問題,但是對研發規則苛刻。並且工作量非常大。

那么我們做架構的應該如何解決這樣的問題?

首先來講講,我們上面提到的異步定時落地,

我們再次改造一下 DataModel 類 把原始的map存儲改為 json 字符串 hashcode 值存儲,
其實你可以直接存字符串,但是如果數據比較大的話,全部存儲字符串比較耗內存,所有考慮hashcode
    /*存儲有變化的屬性 由於這個字段屬性是不用落地到數據庫的 需要加入過濾標識*/
    @JSONField(serialize = false, deserialize = false)
    private transient int oldJsonHashCode = 0;

    /**
     * 歷史json字符串 hash code
     *
     * @return
     */
    public int getOldJsonHashCode() {
        return oldJsonHashCode;
    }

    /**
     * 歷史json字符串 hash code
     *
     * @param oldJsonHashCode
     */
    public void setOldJsonHashCode(int oldJsonHashCode) {
        this.oldJsonHashCode = oldJsonHashCode;
    }

 修改測試方案

package com.ty.backdata;

import com.alibaba.fastjson.JSON;

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

/**
 * @program: com.ty.minigame
 * @description: 數據備份
 * @author: Troy.Chen(失足程序員 , 15388152619)
 * @create: 2020-08-27 09:03
 **/
public class BackDataMain {

    private static final long serialVersionUID = 1L;

    /*定義為緩存數據*/
    private static Map<Long, DataModel> cacheDataMap = new HashMap<>();

    public static void main(String[] args) {
        /*初始化測試數據*/
        initData();
        System.out.println("\n======================================================================\n");
        /*先進行一次檢查*/
        for (Map.Entry<Long, DataModel> modelEntry : cacheDataMap.entrySet()) {
            checkData(modelEntry.getValue());
        }
        System.out.println("\n======================================================================\n");
        /*獲取 id = 1 數據做修改*/
        DataModel cacheData = cacheDataMap.get(1L);
        /*獲得一點經驗*/
        cacheData.setExp(cacheData.getExp() + 1);
        /*等級提示一級*/
        cacheData.setLevel(cacheData.getLevel() + 1);
        /*先進行一次檢查*/
        for (Map.Entry<Long, DataModel> modelEntry : cacheDataMap.entrySet()) {
            checkData(modelEntry.getValue());
        }
//        /* 根據你選擇的 orm 框架 mysql mssql等等 具體操作不描述*/
//        orm.insert(dataModel) or orm.update(dataModel);
//        /* redis */
//        final String jsonString = JSON.toJSONString(dataModel);
//        jedis.set(rediskey, jsonString);
    }

    /*初始化測試數據*/
    public static void initData() {
        DataModel model1 = new DataModel(1, "失足程序員", 1, 1);
        String oldJsonString = JSON.toJSONString(model1);
        int code = Objects.hashCode(oldJsonString);
        model1.setOldJsonHashCode(code);
        System.out.println("原始:" + code + ", " + oldJsonString);
        cacheDataMap.put(model1.getId(), model1);

        DataModel model2 = new DataModel(2, "策划AA", 1, 1);
        oldJsonString = JSON.toJSONString(model2);
        code = Objects.hashCode(oldJsonString);
        model2.setOldJsonHashCode(code);
        System.out.println("原始:" + code + ", " + oldJsonString);
        cacheDataMap.put(model2.getId(), model2);
    }

    public static void checkData(DataModel model) {
        /*存儲原始 json 值*/
        String jsonString = JSON.toJSONString(model);
        int code = Objects.hashCode(jsonString);
        System.out.println("查看:" + code + ", " + jsonString);
        System.out.println("屬性對比是否有變化:" + (model.getOldJsonHashCode() != code));
        /*重新賦值hashcode*/
        model.setOldJsonHashCode(code);
    }

}

效驗一下輸出結果

清晰的看到,這樣,在這樣的架構下,對於研發人員的編碼格式要求就不在那么嚴謹;

也就是說不用怕他忘記修改set方法

但是我們可能依然發現其實這樣的依然不是你想要的,
可能會問有沒有更好的辦法,既能增量更新,也能對研發人員少一些苛刻的嚴謹需求;

 有當然有,既然你需求了,我們怎么能不滿足你呢?

那么最好的方案啥呢?

反射,通過反射初始化模型屬性為map對象,就和第一次的方案差不多類似;

但是這里是求差集;

也就是存儲一次原始的模型對象所有屬性的mao值,然后在下一次輪詢的時候在獲取一次屬性的map值,來對比屬性的值是否相等

繼續修改 DataModel

    /*存儲有變化的屬性 由於這個字段屬性是不用落地到數據庫的 需要加入過濾標識*/
    @FieldAnn(alligator = true)/*自定義的注解,標識反射的時候是忽律字段*/
    @JSONField(serialize = false, deserialize = false)
    private transient Map<String, String> oldFieldMap = new HashMap<>();

    public Map<String, String> getOldFieldMap() {
        return oldFieldMap;
    }

    public void setOldFieldMap(Map<String, String> oldFieldMap) {
        this.oldFieldMap = oldFieldMap;
    }

引用測試關鍵點在於反射獲取map對象,本文不標注,因為不是本文的重點,

 1 package com.ty.backdata;
 2 
 3 import com.alibaba.fastjson.JSON;
 4 import com.ty.tools.utils.FieldUtil;
 5 
 6 import java.util.HashMap;
 7 import java.util.Map;
 8 
 9 /**
10  * @program: com.ty.minigame
11  * @description: 數據備份
12  * @author: Troy.Chen(失足程序員 , 15388152619)
13  * @create: 2020-08-27 09:03
14  **/
15 public class BackDataMain {
16 
17     private static final long serialVersionUID = 1L;
18 
19     /*定義為緩存數據*/
20     private static Map<Long, DataModel> cacheDataMap = new HashMap<>();
21 
22     public static void main(String[] args) {
23         /*初始化測試數據*/
24         initData();
25         System.out.println("\n======================================================================\n");
26         /*先進行一次檢查*/
27         for (Map.Entry<Long, DataModel> modelEntry : cacheDataMap.entrySet()) {
28             checkData(modelEntry.getValue());
29         }
30         System.out.println("\n======================================================================\n");
31         /*獲取 id = 1 數據做修改*/
32         DataModel cacheData = cacheDataMap.get(1L);
33         /*獲得一點經驗*/
34         cacheData.setExp(cacheData.getExp() + 1);
35         /*等級提示一級*/
36         cacheData.setLevel(cacheData.getLevel() + 1);
37         /*先進行一次檢查*/
38         for (Map.Entry<Long, DataModel> modelEntry : cacheDataMap.entrySet()) {
39             checkData(modelEntry.getValue());
40         }
41 //        /* 根據你選擇的 orm 框架 mysql mssql等等 具體操作不描述*/
42 //        orm.insert(dataModel) or orm.update(dataModel);
43 //        /* redis */
44 //        final String jsonString = JSON.toJSONString(dataModel);
45 //        jedis.set(rediskey, jsonString);
46         System.exit(0);
47     }
48 
49     /*初始化測試數據*/
50     public static void initData() {
51         DataModel model1 = new DataModel(1, "失足程序員", 1, 1);
52         Map<String, String> objectFieldMap = FieldUtil.getObjectFieldMap(model1);
53         model1.setOldFieldMap(objectFieldMap);
54         System.out.println("原始:" + JSON.toJSONString(objectFieldMap));
55         cacheDataMap.put(model1.getId(), model1);
56 
57         DataModel model2 = new DataModel(2, "策划AA", 1, 1);
58         objectFieldMap = FieldUtil.getObjectFieldMap(model2);
59         model2.setOldFieldMap(objectFieldMap);
60         System.out.println("原始:" + JSON.toJSONString(objectFieldMap));
61         cacheDataMap.put(model2.getId(), model2);
62     }
63 
64     public static void checkData(DataModel model) {
65         /*存儲原始 json 值*/
66         Map<String, String> objectFieldMap = FieldUtil.getObjectFieldMap(model);
67 
68         final Map<String, String> oldFieldMap = model.getOldFieldMap();
69         Map<String, String> tmp = new HashMap<>();
70         /*求出差集*/
71         for (Map.Entry<String, String> stringStringEntry : objectFieldMap.entrySet()) {
72             final String key = stringStringEntry.getKey();
73             final String value = stringStringEntry.getValue();
74             final String oldValue = oldFieldMap.get(key);
75             if (oldValue == null || !value.equals(oldValue)) {
76                 /*如果原來沒有這個屬性值 或者屬性發生變更*/
77                 tmp.put(key, value);
78             }
79         }
80         System.out.println("變化:" + JSON.toJSONString(tmp));
81         System.out.println("屬性對比是否有變化:" + (tmp.size() > 0));
82         /*重新賦值最新的*/
83         model.setOldFieldMap(objectFieldMap);
84     }
85 
86 }
View Code

重點代碼是下面的求差集獲取map增量更新代碼

    public static void checkData(DataModel model) {
        /*存儲原始 json 值*/
        Map<String, String> objectFieldMap = FieldUtil.getObjectFieldMap(model);

        final Map<String, String> oldFieldMap = model.getOldFieldMap();
        Map<String, String> tmp = new HashMap<>();
        /*求出差集*/
        for (Map.Entry<String, String> stringStringEntry : objectFieldMap.entrySet()) {
            final String key = stringStringEntry.getKey();
            final String value = stringStringEntry.getValue();
            final String oldValue = oldFieldMap.get(key);
            if (oldValue == null || !value.equals(oldValue)) {
                /*如果原來沒有這個屬性值 或者屬性發生變更*/
                tmp.put(key, value);
            }
        }
        System.out.println("變化:" + JSON.toJSONString(tmp));
        System.out.println("屬性對比是否有變化:" + (tmp.size() > 0));
        /*重新賦值最新的*/
        model.setOldFieldMap(objectFieldMap);
    }

輸出結果

總結

本文提供了四種落地方案,

全量落地和增量落地

不同實現的四種方案,

第一種全量更新

  優點就是代碼少,坑也少,
  缺點就是性能不是很高;

第二種全量更新

  優點:提升了落地性能,也不用考慮開發人員的行為規范問題,
   缺點:在架構初期就要考慮進去,代碼實現量有所增加。

第一種增量更新

  優點:解決了性能消耗問題,不用反射也不用第三方格式化判斷等,
  缺點:對開發人員的行為規范要求比較嚴格,如果遺漏了很可能出現數據問題;

第二種增量更新

  優點:不考慮開發人員的行為規范,也實現了增量更新,減少數據交付導致的io瓶頸
  缺點:增加了代碼量和判斷量,但是這樣的量對比數據交互io,微不足道;

 

不知道各位是否還有其他更加優化的方案!!!!

期待你的點評;

 


免責聲明!

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



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