论《数据落地》的方案总结

序言

作为一个游戏服务端研发人员,从业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,微不足道;

 

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

期待你的点评;

 

posted on 2020-08-27 12:12  無心道(失足程序员)  阅读(1631)  评论(0编辑  收藏  举报