移动建模平台元数据存储架构演进

源宝导读:明源云天际-移动建模平台是一个快速生成多端移动应用的PaaS平台,元数据是移动应用设计与运行的核心数据结构,本文将从元数据存储这个视角分享我们的技术思考与实践。

一、什么是元数据(Metadata)?

    这个问题要先从移动建模平台的定位说起。移动建模平台是一个高效的应用搭建、管理平台,用户可以通过拖拉拽的方式,自定义快速生成多端移动应用的PaaS平台。

    目前主流的移动应用开发大都是基于H5为主的前端技术,元数据是对移动应用内部结构的一种数据抽象,用于描述应用所使用的组件和配置,是整个移动应用设计阶段和运行阶段的核心数据,也是移动建模平台生成的重要产物之一。本文主要从元数据这个视角去讨论移动建模平台在元数据存储方面的一些实践。

    如果把移动建模平台比作一个汽车生产线的话,那么移动应用就好比这条生产线生产的汽车,元数据就好比汽车的配置,消费者可以基于汽车的原厂配置进行个性化改装,也就有了个性化元数据,改装完成最终验车上牌也就有了运行时元数据。

    设计阶段通过一个Web版的在线设计器,设计器初始化会加载元数据进行页面渲染,元数据数据结构如下:

    设计器加载完成后可以通过设计器进行应用的设计和页面配置,保存设计就会产生新的元数据:

二、元数据存储架构演进过程概览

    移动建模平台元数据存储的演进过程大致可以分为三个阶段:

2.1、单体应用阶段

    这个阶段元数据表和其他业务数据表在同一个数据库中,按照上图的逻辑结构主要分成四张表来存储:

    在项目初期数据量并不大,这种结构也是最容易实现和最容易想到的。但随着业务的发展,各种组件越来越丰富,单个应用的元数据也由最初的普遍几十KB发展到几M,同时伴随着页面增多,页面之间的拷贝、复制、更新等操作也变得越来越缓慢。

    从上图的表结构可以看出,metadata字段是使用字符串来存储json的,并且设计时和运行时元数据存储在同一张表中。很快这种设计方案的弊端就显现出来,主要有几方面问题:

  1. 元数据可能会有几M,对元数据的每次读写操作都需要对元数据进行序列化和反序列化,网络IO和内存消耗大,程序执行时间过长

  2. 即使要修改元数据中很小的一部分内容也必须将元数据全部取出,修改后再序列化为字符串存入数据库。由于元数据的特殊性,缓存方案也无法使用

  3. 页面、文件夹数量比较多时对页面的复制、删除等操作需要涉及到多表事务,事务执行效率低。一个租户升级操作可能需要十几分钟。

  4. 当PHP按照数组方式来处理后导致空对象和空数组转换问题,会导致元数据损坏无法还原,前端页面渲染出错

  5. 由于涉及到多张表的操作,多表查询会让业务逻辑变得极其复杂,程序很难维护

2.2、服务拆分阶段

针对以上出现的一些问题,开始采取一些局部的优化手段,主要有几下几方面:

  1. 采用服务化的方式将原有的元数据操作相关的逻辑从单体应用中剥离出来,有了元数据服务。

  2. 数据表结构增加了一些冗余字段,并针对索引进行了相应调整,提高了查询性能。

  3. 在写入操作比较多的地方将以前的单条insert改为了一次性多条insert插入,优化写入性能。

  4. 对元数据的结构进行优化,精简冗余部分,减小元数据的体积。

  5. PHP操作元数据禁止使用数组方式来处理,统一转为对象。

    这个阶段采用了以上一些优化方法,虽然性能得到一些改善,但是都没有从根本上解决问题,根本问题出在存储层,团队也有讨论过使用NOSQL,比如MongoDB,但是由于元数据和其他模块严重耦合,数据层的拆分难度很大。加之如果改为MongoDB,新的数据模型如何设计,旧的数据如何迁移等问题还没最佳实践。所以这个阶段的一些改进仅限于应用层的拆分,不过对于后续重构提供了参考。

2.3、微服务化阶段

    这个阶段也是移动PaaS2.0阶段,在2.0中元数据相关的能力完全抽离出来成为单独的服务,并且使用golang进行重构,数据库也独立出来,使用MongoDB进行重新建模设计。为什么要选用MongoDB来作为数据库存储,主要基于以下几个方面:

  1. 元数据本来就是json结构,而MongoDB的使用BSON作为数据交换格式,以文档方式组织数据,非常符合元数据的结构特点。

  2. MongoDB4.0之后同样支持事务操作,在一些需要事务的场景下依然能够保证数据的一致性。

  3. 通过性能对比,MongoDB在读写性能上有明显优势。

  4. JSON 格式存储最接近真实对象模型,对开发者友好,方便快速开发迭代。对于测试人员来说,可以直观的看到元数据的数据结构,对测试更加友好。

  5. 能够极大的简化目前的应用层开发,减少大量的多表查询操作。

  6. 可以按需修改元数据文档的某个节点,而不需要读取整个元数据文档。

  7. 高可用复制集满足数据高可靠、服务高可用的需求,运维简单,故障自动切换。

  8. 可扩展分片集群,面对未来海量元数据存储,可以很方便的支持水平扩展。

  9. 强大的aggregation & mapreduce,可以将复杂的查询分解为一个个小的步骤。

    下图是在4核8G的同一台虚拟机上做的一个MySQL和MongoDB的性能对比测试,可以看出随着插入元数据的数量增加,MySQL和MongoDB所花费的时间的差距也越来越大。

    使用MongoDB重新设计后的元数据结构:

{"_id": ObjectId("5f3de7507cda70000e433ca2"),"workspaceId": "26043287605354496","common": {"style": {"globalBgColor": "#FFFFFF","primaryColor": "#FF543D","secondaryColor": "#FF6954"},"body": {"header": {"hide": false}}},"configs": [{"_id": ObjectId("5f3de7507cda70000e433ca3"),"type": "role","name": "游客","alias": "default","isGuest": true,"remark": "用户未登录时所使用...","position": 1.59789243277115e+18,"viewIds": [ObjectId("5f3e45c62ef1d50013b3303e")],"metadata": {"tabs": {"items": [{"isDefault": true,"text": "123","activeIcon": "appicon-house","href": {"name": "bde68663-6f93-2206-0b29-cf910711f71e"},"icon": "appicon-house","iconClass": "appicon"}]}}},{"_id": ObjectId("5f3de7507cda70000e433ca4"),"type": "role","name": "已登录用户","alias": "default-login","isGuest": false,"remark": "用户登录时所使用...","position": 1.59789243277116e+18,"viewIds": [ ],"metadata": { }},{"_id": ObjectId("5f470ced59221f0014d2a144"),"type": "page","ancestors": [ObjectId("5f3de7507cda70000e433ca4")],"name": "login","routeName": "ef214890-b3e6-9a24-9dd8-80d12343f76c","routePath": "/ef214890-b3e6-9a24-9dd8-80d12343f76c","remark": "","design": { },"metadata": {"name": "ef214890-b3e6-9a24-9dd8-80d12343f76c","path": "/ef214890-b3e6-9a24-9dd8-80d12343f76c","body": {"header": {"title": "login","items": [ ]},"content": {"items": [ ]}}},"position": 1.59849188522867e+18,"viewIds": [ ]}],"createdAt": ISODate("2020-08-20T03:00:32Z"),"updatedAt": ISODate("2020-08-20T03:00:32Z")
}

    从新的结构可以看出之前的元数据中的配置变成了一个内嵌数组configs,configs下包含了角色配置、文件夹、页面。三者之间的关系由以前的层次关系被打平后变成了并列关系。那么如何实现他们之前的那种上下级关系呢?仔细看就能发现configs中的每一个对象里都有一个ancestors字段,这个字段用于记录祖先节点,也就是通过这个节点就可以轻松找到当前项有几个上级,只需要增加一个索引字段就可以高效的得到一个树状结构。根据ancestors创建索引:

xxxxxxxxxx
db.metadata.createIndex({"configs.ancestors": 1
})

    如图所示,在1.0中,如果想要按照箭头所指的方向移动往往需要配合数据库中的

    这两个字段,更新这两个字段来标注页面的位置。

    在新的数据库当中,由于页面、文件夹、配置是平等关系,所以只需要一个 "position": 1.59849188522867e+18字段来记录就行了,当需要移动上下页面时候只需要取相邻两个元素的position的平均值,最后结果按照position来排序,性能得到很大提升。

    通过前后数据结构的对比,可以很明显发现,在使用MySQL存储时,为了要保证元数据节点之间的关系,往往需要设计多张表,而使用MongoDB后,只要一个集合就能搞定设计时元数据的存储,这样带来的直接好处就是性能提升和应用程序开发的简化。

    元数据服务端使用了golang代替之前的php,其实也是为了方便元数据的操作和提升性能,由于配置、文件夹和页面的差异被抹平,三者被统一抽象为配置,于是就很方便的提供统一的元数据操作API,golang结构体可以完美的将元数据的结构映射到MongoDB的文档模型中,开发者可以清楚的看到数据库中元数据结构和代码中是完全一致的,这对新人理解元数据结构会有很大帮助。

//元数据结构体
type Metadata struct {Model       `bson:"-"`Id          bson.ObjectId `bson:"_id" json:"id"`WorkspaceId string        `bson:"workspaceId" comment:"工作区ID"`Common      bson.M        `bson:"common" comment:"公共配置"`Configs     []Config      `bson:"configs" comment:"配置信息"`IsPublished bool          `bson:"isPublished" comment:"是否发布"`CreatedAt   time.Time     `bson:"createdAt"`UpdatedAt   time.Time     `bson:"updatedAt"`
}type Config interface {Add(metadataId string, data interface{}) errorEdit(metadataId, configId string, data interface{}) errorDelete(metadataId, configId string) errorGetType() string
}

    解决了存储问题后,需要返回树状结构给前端,这就需要应用端重新组装数据。

type PageListResponse []TreeNode//统一定义菜单树的数据结构
type TreeNode struct {Id        string                 `json:"id"`                  //节点idParentId  string                 `json:"-"`                   //父idType      string                 `json:"type"`                //类型Name      string                 `json:"name"`                //节点名字RouteName string                 `json:"routeName,omitempty"` //标识RoutePath string                 `json:"routePath,omitempty"` //路径Leaf      bool                   `json:"leaf,omitempty"`      //叶子节点IsGuest   bool                   `json:"isGuest,omitempty"`   //是否是游客配置IsLogin   bool                   `json:"isLogin,omitempty"`   //是否是登录页面Ancestors []string               `json:"ancestors,omitempty"` //祖先节点Remark    string                 `json:"remark,omitempty"`    //备注Position  string                 `json:"position"`            //位置Design    map[string]interface{} `json:"design,omitempty"`    //组件属性Metadata  map[string]interface{} `json:"metadata,omitempty"`  //元数据Children  []TreeNode             `json:"children,omitempty"`  //子节点
}// GenerateTree 自定义的结构体实现 TreeNode 接口后调用此方法生成树结构
// nodes 需要生成树的节点
func GenerateTree(nodes []TreeNode) (trees []TreeNode) {trees = []TreeNode{}// 定义顶层根和子节点var roots, childs []TreeNodefor _, v := range nodes {if len(v.ParentId) <= 0 {// 判断顶层根节点roots = append(roots, v)}childs = append(childs, v)}for _, v := range roots {childTree := &v// 递归recursiveTree(childTree, childs)// 递归之后,根据子节确认是否是叶子节点childTree.Leaf = (len(childTree.Children) == 0)trees = append(trees, *childTree)}return
}// recursiveTree 递归生成树结构
// tree 递归的树对象
// nodes 递归的节点
func recursiveTree(tree *TreeNode, nodes []TreeNode) {for _, v := range nodes {if len(v.ParentId) <= 0 {// 如果当前节点是顶层根节点就跳过continue}if tree.Id == v.ParentId {childTree := &vrecursiveTree(childTree, nodes)// 递归之后,根据子节确认是否是叶子节点childTree.Leaf = (len(childTree.Children) == 0)tree.Children = append(tree.Children, *childTree)}}
}

    这个阶段golang结构体处理json的便利性凸显出来,omitempty可以将空的节点数据忽略掉,这就有效的降低了元数据的体积,降低了网络I/O。

    设计时的元数据存储性能和逻辑复杂的问题解决了,剩下的就是运行时元数据的问题了。元数据在运行时阶段其实是不会变动的,在1.0当中,移动应用在运行时需要动态请求元数据的服务,从元数据服务接口中拉取运行时元数据来渲染页面,显然如果访问量大后元数据服务会成为性能的瓶颈。针对这个问题结合元数据的业务特点,最终运行时元数据就采用静态json文件的方式存储在OSS上,不仅消除了后端服务访问压力问题,同时也提高了运行时元数据加载的稳定性。最终生成的路径其实访问的是一个真实存在的json问题。

xxxxxxxxxx
https://xxxxxx.com/_assets/mobile_three/demo/exp/1.0.12/meta/default.json

三、总结

    好了,以上就是本次分享的移动建模平台元数据存储的演进过程,当然实际演进过程远比本次讲述的要复杂得多,分享的内容也是挑选几个比较重要的场景展开,后续可以分享一些MongoDB设计模式方面的内容,总结一下从开发选型角度大致有以下几点实践经验:

  1. 使用MySQL和MongoDB同时进行数据建模,对比两者之间的优劣,在表关系比较复杂时可能涉及到多表关联查询较多的场景下利用MongoDB内嵌文档、内嵌数组等灵活的文档数据结构往往能设计出结构更清晰、性能更好的存储方案。

  2. 小心MongoDB单个文档16M的存储限制,对于那种可能无限增长的数据不适合直接使用内嵌方式存储,可改为内嵌引用方式。

  3. 尽量不要使用ORM框架来操作MongoDB,往往会误把MongoDB当成MySQL来使用,同时不能很好的使用MongoDB强大的API。

  4. Golang和MongoDB的结合能在提升性能的同时,带来开发上的便利。

  5. MongoDB 4.0以后已经支持多文档事务,扩展了MongoDB的使用场景,越来越多的场景其实是可以使用MongoDB代替MySQL。如果没有特别的必要和限制,采用MongoDB往往会给程序设计带来更大的灵活性,提高数据库开发效率,更好的满足快速迭代开发的需求。

  6. MongoDB不能简单理解为一个json文档存储所有数据,同时要结合具体的业务场景考虑读写操作是否方便来设计文档模型。

------ END ------

作者简介

段同学: 研发工程师,目前负责天际-移动平台产品的研发工作。

也许您还想看

基于 Go 的微服务运行情况监控实践

在明源云客,一个全新的服务会经历什么?

云客后台优化的“前世今生”(一)

云客后台优化的“前世今生”(二)

回归统计在DMP中的实战应用

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/306616.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

浅谈AsyncLocal,我们应该知道的那些事儿

【导读】最近查看有关框架源码&#xff0c;发现AsyncLocal这玩意水还挺深&#xff0c;于是花了一点功夫去研究&#xff0c;同时对比ThreadLocal说明二者区别以及在何时场景下使用AsyncLocal或ThreadLocalThreadLocal相信很多童鞋用过&#xff0c;但AsyncLocal具体使用包括我在内…

算法设计与分析——动态规划——数字三角形问题

数字三角形问题 1.题目描述&#xff1a;给定一个由n行数字组成的数字三角形&#xff0c;如图3-7所示。设计一个算法&#xff0c;计算出从三角形的顶至底的一条路径&#xff0c;使该路径经过的数字总和最大。 算法设计:对于给定的由n行数字组成的数字三角形&#xff0c;计算从三…

如何在 Asp.Net Core MVC 中处理 null 值

译文链接&#xff1a;https://www.infoworld.com/article/3434624/how-to-handle-null-values-in-aspnet-core-mvc.html传统的 asp.net mvc 对应着 .netcore 中的 asp.net core mvc&#xff0c;可以利用 asp.net core mvc 去构建跨平台&#xff0c;可扩展&#xff0c;高性能的w…

算法设计与分析——动态规划——最长公共子序列

#include<iostream> #include<stdio.h> #include<string.h> #include<bits/stdc.h> #define MAXLEN 50 using namespace std;void LCSlength(int m,int n,char *x,char *y,int c[][MAXLEN],int b[][MAXLEN]) {for(int i0;i<m;i)//m为字符个数 {c[i]…

程序员过关斩将--论系统设计的高可扩展性

“此文仅仅代表个人意见&#xff0c;并非行业标准“MQ是万能的高扩展方式&#xff1f;“面向接口是万能的高扩展方式&#xff1f;说到系统设计的三高&#xff0c;每一高都是一个很庞大的话题&#xff0c;甚至可以用一本书甚至N本书来详细阐述。其中高可扩展性是系统架构的众多目…

Docker Vs Podman

翻译自 Chetansingh 2020年4月24日的博文《Docker Vs Podman》 [1]容器化的一场全新革命是从 Docker 开始的&#xff0c;Docker 的守护进程管理着所有的事情&#xff0c;并成为最受欢迎和广泛使用的容器管理系统之一。但是&#xff0c;请稍等&#xff01;您真的会假设 Docker 是…

算法设计与分析——动态规划——01背包问题

#include<iostream> #include<iomanip> using namespace std; //前i个物品装入容量为j的背包中获得的最大价值//0-1背包动态规划算法 构造二维表 int knapsack_problem( int n,int *weight,int *value,int capacity,int **m,int *flag) {for(int i0;i<capaci…

让 CefSharp.WinForms 应用程序同时支持32位(x86)和64位(x64)的解决方案

当我们为基于 .NET Framework 的 WinForm 程序增加 CefSharp.WinForms 依赖后&#xff0c;可能会遇到以下报错信息&#xff1a;CefSharp.Common is unable to proceeed as your current Platform is ‘AnyCPU’. To target AnyCPU please read https://github.com/cefsharp/Cef…

算法设计与分析——贪心算法——活动安排问题

问题描述&#xff1a;设有n个活动的集合E{1,2,…,n}&#xff0c;其中每个活动都要求使用同一资源&#xff0c;如演讲会场等&#xff0c;而在同一时间内只有一个活动能使用这一资源。 每个活动i都有一个要求使用该资源的起始时间si和一个结束时间fi,且si<fi 。 如果选择了活动…

c语言——刷新控制台界面与返回上一级

#include<iostream> #include<stdlib.h> using namespace std;int main() {while(1){system("cls");//刷新控制台程序界面 cout<<"请输入1&#xff1a;进入下一级程序A:"<<endl;cout<<"请输入2&#xff1a;进入下一级程…

C# :异步编程的注意点

在上一篇《C#&#xff1a;异步编程中的 async 和 await》 中简单介绍了在 C# 中的异步编程以及 async 和 await 编程模型&#xff0c;本文介绍下异步编程的注意事项&#xff0c;主要有以下几个方面。同步中调用异步在同步代码中调用异步代码&#xff0c;容易导致死锁&#xff0…

makefile 打印变量_[Makefile] 缩进与空格--记录踩过的坑

今天折腾了好久&#xff0c;就为了debug两个makefile的bug。虽然最后找到原因了&#xff0c;但是&#xff0c;怎么说呢&#xff0c;用现在流行的话来说&#xff0c;实在是意难平啊&#xff01;必须写一篇记录一下&#xff01;第一个问题&#xff0c;是个语法高亮问题。今天观察…

算法设计与分析——贪心算法——背包问题

0-1背包问题&#xff1a; 前提&#xff1a;给定n种物品和一个背包。物品i的重量是Wi&#xff0c;其价值为Vi&#xff0c;背包的容量为C。 问题&#xff1a;应如何选择装入背包的物品&#xff0c;使得装入背包中物品的总价值最大? 背包问题&#xff1a; 与0-1背包问题类似&…

企业级精致 Blazor 套件 BootstrapBlazor 介绍

BootstrapBlazor1、前言 Blazor 作为一种 Web 开发的新技术已经发展有一段时间了&#xff0c;有些人标称 无 JS 无 TS&#xff0c;我觉得有点误导新人的意味&#xff0c;也有人文章大肆宣传 Blazor 是 JavaScript 的终结者&#xff0c;是为了替代 JavaScript 而生的&#xff0c…

算法设计与分析——贪心算法——最优装载问题

有一批集装箱要装上一艘载重量为c的轮船。其中集装箱i的重量为Wi。最优装载问题要求确定在装载体积不受限制的情况下&#xff0c;将尽可能多的集装箱装上轮船。 #include<iostream> #include<algorithm> #include<cstring> using namespace std;typedef str…

AgileConfig-轻量级配置中心 1.1.0 发布,支持应用间配置继承

AgileConfig轻量级配置中心自第一个版本发布不知不觉已经半年了。在并未进行什么推广的情况下收到了250个star&#xff0c;对我有很大的鼓舞&#xff0c;并且也有不少同学试用&#xff0c;并且给出了宝贵的意见&#xff0c;非常感谢他们。其中有一些意见非常好&#xff0c;但是…

如何在 C# 中使用 Dapper ORM

译文链接&#xff1a;https://www.infoworld.com/article/3025784/how-to-use-the-dapper-orm-in-c.html?nsdrtrue对象关系映射&#xff08;ORM&#xff09;这个概念已经存在很长时间了&#xff0c;ORM的作用就是用来解决 编程领域的 object model 和关系数据库中的 data mode…

GraphQL:简单开开始一个查询

GraphQL 既是一种用于 API 的查询语言也是一个满足你数据查询的运行时。GraphQL 对你的 API 中的数据提供了一套易于理解的完整描述&#xff0c;使得客户端能够准确地获得它需要的数据&#xff0c;而且没有任何冗余&#xff0c;也让 API 更容易地随着时间推移而演进&#xff0c…

算法设计与分析——回溯法——批处理作业调度

问题描述&#xff1a;给定n个作业的集合{J1,J2,…,Jn}。每个作业必须先由机器1处理&#xff0c;然后由机器2处理。作业Ji需要机器j的处理时间为tji。对于一个确定的作业调度&#xff0c;设Fji是作业i在机器j上完成处理的时间。所有作业在机器2上完成处理的时间和称为该作业调度…

探索.NET平台中的SIMD内在函数Vector

概述Vector&#xff08;向量&#xff09;是一种序列式容器&#xff0c;事实上和数组差不多&#xff0c;但它比数组更优越。一般来说数组不能动态拓展&#xff0c;因此在程序运行的时候不是浪费内存&#xff0c;就是造成越界。而Vector刚好弥补了这个缺陷&#xff0c;它的特征是…