論《資料落地》的方案總結

失足程式設計師發表於2020-08-27

序言

作為一個遊戲服務端研發人員,從業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,微不足道;

 

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

期待你的點評;

 

相關文章