序言
作為一個遊戲服務端研發人員,從業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 }
測試一下看看效果
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 }
重點程式碼是下面的求差集獲取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,微不足道;
不知道各位是否還有其他更加優化的方案!!!!
期待你的點評;