美文网首页Screeps 游戏指南
Screeps 实现数据层与业务层分离

Screeps 实现数据层与业务层分离

作者: masterkeze | 来源:发表于2021-07-23 14:17 被阅读0次

    基础知识

    阅读本文前,请先了解一下 Screeps存储基础知识,关系型数据库(聚集索引与非聚集索引),ORM的概念。另外,笔者使用Typescript,因此对象有强类型的概念。

    可以先翻到最下面,看下最终实现的效果。

    前言

    Screeps中有许多跨tick存储数据的方法:Memory, Global, InterShardMemory, RawMemory, 闭包等等。笔者作为后端开发,在直接使用上面的方法书写业务代码(涉及操作游戏对象的代码)的时候,产生了强烈的违和感。这种违和感来源于业务层与数据层之间的强耦合性,最常见的情况是,业务代码没几行,数据读写操作写了一大堆(初始化,空值校验,缓存,查询等)。如果能够将业务层与数据层分离,业务层只关心业务,读写层只关心读写,将会大大提高开发的效率,降低系统的耦合性

    业务层的需求

    业务层在常规读写数据时,有些共同的需求:

    1. 增删改查对象——类似数据库的引擎

    2. 查询往往通过单个或多个维度(按房间,按职责,按任务)——索引

    3. 强类型的对象——ORM对象映射

    分层设计

    分层设计.JPG

    考虑到Screeps和常规后端应用的差异,在数据存储上做了很大程度的简化(没有并发,没有事务,不需要锁),只满足了业务层需求的几个功能点,后续可以扩展。

    对象集合管理器

    功能:

    1. 创建对象集合及索引(默认自带主键的聚集索引,支持添加聚集或非聚集的单列索引,可以中途添加索引)

    2. 向集合添加对象(同时修改索引)

    3. 修改集合中的对象(同时修改索引)

    4. 从集合删除对象(同时修改索引)

    5. 通过id获取对象

    6. 通过属性获取对象(有索引时优先使用索引)

    7. (开发中) 指定存储介质(global, Memory等),以及重启策略

    注意事项:

    1. 对象集合必须包含主键列,id

    2. 对象集合管理器中,不存储集合的元数据(描述这个集合的数据)

    3. 对象集合管理器接受及返回的对象,均为包含了{id: string} 的对象,不做其他类型校验

    代码示例:

    // DatasetManager.ts
    import { Logger } from "./Logger";
    
    function EnsureCreated(route: string): boolean {
        if (!Memory.datasets) Memory.datasets = {};
        if (!Memory.datasets[route]) {
            Memory.datasets[route] = {};
            return false;
        } else {
            return true;
        }
    }
    
    export class DatasetManager {
        /**
         * 在Memory中创建数据集
         * @param  {string} route 路径
         * @param  {Entity[]} entities 数据
         * @param  {IndexConfig[]} indexConfigs? 索引结构
         * @param  {boolean} reset? 是否清空重建
         */
        public static Create(route: string, entities: Entity[], indexConfigs?: IndexConfig[], reset?: boolean) {
            let created = EnsureCreated(route);
            if (!created || reset) {
                // 主键默认聚集索引
                Memory.datasets[route]["id"] = {
                    clusterd: true,
                    data: {}
                };
                if (indexConfigs) {
                    for (const indexConfig of indexConfigs) {
                        Memory.datasets[route][indexConfig.indexName] = {
                            clusterd: indexConfig.clustered,
                            data: {}
                        };
                    }
                }
                Logger.info(`Dataset:${route} created.`, "DatasetManager", "Create");
            } else {
                if (indexConfigs) {
                    let currentIndexes = Object.keys(Memory.datasets[route]);
                    let newIndexConfigs = indexConfigs.filter(config => !currentIndexes.includes(config.indexName));
                    if (newIndexConfigs.length > 0) {
                        const entities = _.flattenDeep(Object.values(Memory.datasets[route]["id"].data));
                        for (const indexConfig of newIndexConfigs) {
                            Memory.datasets[route][indexConfig.indexName] = {
                                clusterd: indexConfig.clustered,
                                data: {}
                            };
                        }
                        for (const entity of entities) {
                            for (const indexConfig of newIndexConfigs) {
                                const indexName = indexConfig.indexName;
                                const value = (entity as any)[indexName];
                                if (value == undefined) {
                                    continue;
                                }
                                const index = Memory.datasets[route][indexName];
                                if (index.clusterd) {
                                    // 聚集索引全量存储
                                    if (index.data[value]) {
                                        index.data[value].push(entity);
                                    } else {
                                        index.data[value] = [entity];
                                    }
                                } else {
                                    // 非聚集只存储主键列
                                    if (index.data[value]) {
                                        index.data[value].push(entity.id);
                                    } else {
                                        index.data[value] = [entity.id];
                                    }
                                }
                            }
                        }
                    }
                }
            }
            // 插入数据
            for (const entity of entities) {
                this.Add(route, entity);
            }
        }
        /**
         * 向数据集添加数据
         * @param  {string} route
         * @param  {Entity} entity
         */
        public static Add(route: string, entity: Entity) {
            let created = EnsureCreated(route);
            if (!created) {
                Logger.error(`Adding ${JSON.stringify(entity)} to a non-existing dataset:${route}`, "DatasetManager", "Add");
                return;
            }
            const indexNames = Object.keys(Memory.datasets[route]);
            for (const indexName of indexNames) {
                const value = (entity as any)[indexName];
                if (value == undefined) {
                    continue;
                }
                const index = Memory.datasets[route][indexName];
                if (index.clusterd) {
                    // 聚集索引全量存储
                    if (index.data[value]) {
                        index.data[value].push(entity);
                    } else {
                        index.data[value] = [entity];
                    }
                } else {
                    // 非聚集只存储主键列
                    if (index.data[value]) {
                        index.data[value].push(entity.id);
                    } else {
                        index.data[value] = [entity.id];
                    }
                }
            }
        }
    
        public static Remove(route: string, entity: Entity) {
            let created = EnsureCreated(route);
            if (!created) {
                Logger.error(`Removing ${JSON.stringify(entity)} from a non-existing dataset:${route}`, "DatasetManager", "Remove");
                return;
            }
            const indexNames = Object.keys(Memory.datasets[route]);
            for (const indexName of indexNames) {
                const value = (entity as any)[indexName];
                const index = Memory.datasets[route][indexName];
                // 空值或者数据不存在
                if (!value || !index.data[value]) {
                    continue;
                }
                if (indexName == "id") {
                    // 主键直接删除记录
                    delete index.data[value];
                } else {
                    //index.data[value] = _.filter(index.data[value],(id)=>{id != entity.id})
                    _.remove(index.data[value], (x) => (x == entity.id));
                    if (index.data[value].length == 0) delete index.data[value];
                }
    
            }
        }
    
        public static Update(route: string, entity: Entity) {
            let created = EnsureCreated(route);
            if (!created) {
                Logger.error(`Updating ${JSON.stringify(entity)} from a non-existing dataset:${route}`, "DatasetManager", "Update");
                return;
            }
            const indexNames = Object.keys(Memory.datasets[route]);
            for (const indexName of indexNames) {
                const value = (entity as any)[indexName];
                const index = Memory.datasets[route][indexName];
                // 空值或者数据不存在
                if (!value || !index.data[value]) {
                    continue;
                }
                // 只更新聚集索引
                if (!index.clusterd) {
                    continue;
                }
                if (indexName == "id") {
                    index.data[value] = [entity]
                } else {
                    index.data[value] = _.remove(index.data[value], (x) => (x.id == entity.id));
                    index.data[value].push(entity);
                }
            }
        }
        public static GetById<T>(route: string, id: string): T | undefined {
            EnsureCreated(route);
            if (Memory.datasets[route]["id"]) {
                let result = Memory.datasets[route]["id"].data[id];
                return result[0] as T;
            }
            return undefined;
        }
        public static GetByProperty<T>(route: string, property: string, value: any): T[] {
            EnsureCreated(route);
            const index = Memory.datasets[route][property];
            if (index) {
                let result = Memory.datasets[route][property].data[value];
                if (!result) return [];
                // 聚集的直接返回
                if (index.clusterd) {
                    return result as T[];
                } else {
                    let lookup = [];
                    // 非聚集联查id索引
                    for (const id of result as string[]) {
                        let entity = Memory.datasets[route]["id"].data[id][0];
                        lookup.push(entity);
                    }
                    return lookup as T[];
                }
            } else {
                if (Memory.datasets[route]["id"]) {
                    let data = _.flattenDeep(Object.values(Memory.datasets[route]["id"].data));
                    return data.filter(e => e[property] == value) as T[];
                } else {
                    return [] as T[];
                }
            }
        }
        public static GetByProperties<T>(route: string, pairs: { property: string, value: any }[]): T[] {
            // 多条件联查,贼复杂,先不整了。
            EnsureCreated(route);
            if (pairs.length == 0) return [] as T[];
            if (Memory.datasets[route]["id"]) {
                let result = this.GetByProperty<T>(route, pairs[0].property, pairs[1].property) as any[];
                for (let i = 1; i < pairs.length; i++) {
                    const pair = pairs[i];
                    result = result.filter((e) => e[pair.property] == pair.value);
                }
                return result as T[];
            } else {
                return [] as T[];
            }
        }
    }
    
    

    业务上下文

    功能:

    1. 定义对象集合的数据结构,索引

    2. 初始化对象集合

    3. 封装增删改查接口,出入参设置为强类型对象(这步相当于ORM)

    4. 索引列单独设置查询方法

    5. 强类型对象的快捷创建方法

    代码示例:

    // StoreInTransitContext.ts
    import { DatasetManager } from "../utils/DatasetManager";
    import { ContextBase } from "./ContextBase";
    
    interface StoreInTransit extends Entity {
        gameObjectId: string,
        taskId: string,
        direction: "in" | "out",
        resourceType: ResourceConstant,
        amount: number,
        createTick: number
    }
    
    export class StoreInTransitContext extends ContextBase {
        static route: string = "storeInTransit";
        public static Initialize() {
            DatasetManager.Create(this.route, [], [{
                // 查询储量必定使用,高频率,直接聚集索引
                clustered: true,
                indexName: "gameObjectId"
            }, {
                clustered: false,
                indexName: "taskId"
            }, {
                clustered: false,
                indexName: "createTick"
            }], false);
        }
    
        public static Get(id: string) {
            return DatasetManager.GetById<StoreInTransit>(this.route, id);
        }
    
        public static Add(entity: StoreInTransit) {
            DatasetManager.Add(this.route, entity);
        }
    
        public static Remove(entity: StoreInTransit) {
            DatasetManager.Remove(this.route, entity);
        }
    
        public static Update(entity: StoreInTransit) {
            DatasetManager.Update(this.route, entity);
        }
    
        public static GetByGameObjectId(gameObjectId: string) {
            return DatasetManager.GetByProperty<StoreInTransit>(this.route, "gameObjectId", gameObjectId);
        }
    
        public static GetByTaskId(taskId: string) {
            return DatasetManager.GetByProperty<StoreInTransit>(this.route, "taskId", taskId);
        }
    }
    
    

    哪些列需要索引?如何选择聚集索引与非聚集索引?

    索引本质上是空间换取时间的策略,索引能够加速查询,但是会影响增删改的效率。一般情况下,数据的查询次数远大于数据修改的次数,所以对于常用的查询维度,都推荐使用索引。

    这些常用的维度中,必定使用到的维度,可以直接建立聚集索引,而频率较低的维度,使用非聚集索引。值得注意的一点是,如果使用了Memory作为存储介质,Memory每tick都会序列化和反序列化,如果使用了聚集索引,对象集合的大小会增加一倍,序列化反序列化的时间也会增加,在总的cpu消耗上是否划算,还需要后续的测试。如果使用的是global则不太需要考虑数据量的问题,但是需要考虑数据丢失重启的问题。

    业务层调用业务上下文方法

    代码示例:

    // test.ts
    // 初始化上下文(建表建索引,新增索引)
    StoreInTransitContext.Initialize();
    const mySpawns = GameContext.mySpawns;
    if (mySpawns.length >= 2) {
        let spawn1 = mySpawns[0];
        let spawn2 = mySpawns[1];
        // 创建对象
        let store1 = StoreInTransitContext.Create(spawn1.id, "task1", "out", "energy", 300);
        let store2 = StoreInTransitContext.Create(spawn2.id, "task2", "out", "energy", 100);
        StoreInTransitContext.Add(store1);
        StoreInTransitContext.Add(store2);
        // 获取对象
        let store3 = StoreInTransitContext.Get(store1.id);
        if (store3) {
            // 修改对象
            store3.taskId = "task1";
            store3.amount = 500;
            StoreInTransitContext.Update(store3);
    
        }
        // 通过索引获取对象,这里会返回[store2,store3]
        let stores = StoreInTransitContext.GetByTaskId("task1");
        // 移除对象
        StoreInTransitContext.Remove(store2);
    }
    

    从上面的代码,可以看出一些直观的好处:

    1. 索引可配置,可以中途添加,非常灵活。

    2. 对象增删改时,索引自动更新,不需要任何额外处理。

    3. 可以统一做查询优化(类似于 query optimizer),针对多维度的查询,可以使用更优的索引组合。

    4. 实现了业务层和数据层之间的解耦合。

    对本文有任何意见或者建议,或者有任何处理数据层的心得,都欢迎评论留言,互相交流探讨。

    相关文章

      网友评论

        本文标题:Screeps 实现数据层与业务层分离

        本文链接:https://www.haomeiwen.com/subject/cluumltx.html