制作企业网站方案,网站手机模板的特点,青海省教育厅门户网站登录,平面设计公司属于什么行业源宝导读#xff1a;明源云天际-移动建模平台是一个快速生成多端移动应用的PaaS平台#xff0c;元数据是移动应用设计与运行的核心数据结构#xff0c;本文将从元数据存储这个视角分享我们的技术思考与实践。一、什么是元数据#xff08;Metadata#xff09;#xff1f;这… 源宝导读明源云天际-移动建模平台是一个快速生成多端移动应用的PaaS平台元数据是移动应用设计与运行的核心数据结构本文将从元数据存储这个视角分享我们的技术思考与实践。一、什么是元数据Metadata 这个问题要先从移动建模平台的定位说起。移动建模平台是一个高效的应用搭建、管理平台用户可以通过拖拉拽的方式自定义快速生成多端移动应用的PaaS平台。 目前主流的移动应用开发大都是基于H5为主的前端技术元数据是对移动应用内部结构的一种数据抽象用于描述应用所使用的组件和配置是整个移动应用设计阶段和运行阶段的核心数据也是移动建模平台生成的重要产物之一。本文主要从元数据这个视角去讨论移动建模平台在元数据存储方面的一些实践。 如果把移动建模平台比作一个汽车生产线的话那么移动应用就好比这条生产线生产的汽车元数据就好比汽车的配置消费者可以基于汽车的原厂配置进行个性化改装也就有了个性化元数据改装完成最终验车上牌也就有了运行时元数据。 设计阶段通过一个Web版的在线设计器设计器初始化会加载元数据进行页面渲染元数据数据结构如下 设计器加载完成后可以通过设计器进行应用的设计和页面配置保存设计就会产生新的元数据二、元数据存储架构演进过程概览 移动建模平台元数据存储的演进过程大致可以分为三个阶段2.1、单体应用阶段 这个阶段元数据表和其他业务数据表在同一个数据库中按照上图的逻辑结构主要分成四张表来存储 在项目初期数据量并不大这种结构也是最容易实现和最容易想到的。但随着业务的发展各种组件越来越丰富单个应用的元数据也由最初的普遍几十KB发展到几M同时伴随着页面增多页面之间的拷贝、复制、更新等操作也变得越来越缓慢。 从上图的表结构可以看出metadata字段是使用字符串来存储json的并且设计时和运行时元数据存储在同一张表中。很快这种设计方案的弊端就显现出来主要有几方面问题元数据可能会有几M对元数据的每次读写操作都需要对元数据进行序列化和反序列化网络IO和内存消耗大程序执行时间过长。即使要修改元数据中很小的一部分内容也必须将元数据全部取出修改后再序列化为字符串存入数据库。由于元数据的特殊性缓存方案也无法使用。页面、文件夹数量比较多时对页面的复制、删除等操作需要涉及到多表事务事务执行效率低。一个租户升级操作可能需要十几分钟。当PHP按照数组方式来处理后导致空对象和空数组转换问题会导致元数据损坏无法还原前端页面渲染出错。由于涉及到多张表的操作多表查询会让业务逻辑变得极其复杂程序很难维护。2.2、服务拆分阶段针对以上出现的一些问题开始采取一些局部的优化手段主要有几下几方面采用服务化的方式将原有的元数据操作相关的逻辑从单体应用中剥离出来有了元数据服务。数据表结构增加了一些冗余字段并针对索引进行了相应调整提高了查询性能。在写入操作比较多的地方将以前的单条insert改为了一次性多条insert插入优化写入性能。对元数据的结构进行优化精简冗余部分减小元数据的体积。PHP操作元数据禁止使用数组方式来处理统一转为对象。 这个阶段采用了以上一些优化方法虽然性能得到一些改善但是都没有从根本上解决问题根本问题出在存储层团队也有讨论过使用NOSQL比如MongoDB但是由于元数据和其他模块严重耦合数据层的拆分难度很大。加之如果改为MongoDB新的数据模型如何设计旧的数据如何迁移等问题还没最佳实践。所以这个阶段的一些改进仅限于应用层的拆分不过对于后续重构提供了参考。2.3、微服务化阶段 这个阶段也是移动PaaS2.0阶段在2.0中元数据相关的能力完全抽离出来成为单独的服务并且使用golang进行重构数据库也独立出来使用MongoDB进行重新建模设计。为什么要选用MongoDB来作为数据库存储主要基于以下几个方面元数据本来就是json结构而MongoDB的使用BSON作为数据交换格式以文档方式组织数据非常符合元数据的结构特点。MongoDB4.0之后同样支持事务操作在一些需要事务的场景下依然能够保证数据的一致性。通过性能对比MongoDB在读写性能上有明显优势。JSON 格式存储最接近真实对象模型对开发者友好方便快速开发迭代。对于测试人员来说可以直观的看到元数据的数据结构对测试更加友好。能够极大的简化目前的应用层开发减少大量的多表查询操作。可以按需修改元数据文档的某个节点而不需要读取整个元数据文档。高可用复制集满足数据高可靠、服务高可用的需求运维简单故障自动切换。可扩展分片集群面对未来海量元数据存储可以很方便的支持水平扩展。强大的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.59789243277115e18,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.59789243277116e18,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.59849188522867e18,viewIds: [ ]}],createdAt: ISODate(2020-08-20T03:00:32Z),updatedAt: ISODate(2020-08-20T03:00:32Z)
}从新的结构可以看出之前的元数据中的配置变成了一个内嵌数组configsconfigs下包含了角色配置、文件夹、页面。三者之间的关系由以前的层次关系被打平后变成了并列关系。那么如何实现他们之前的那种上下级关系呢仔细看就能发现configs中的每一个对象里都有一个ancestors字段这个字段用于记录祖先节点也就是通过这个节点就可以轻松找到当前项有几个上级只需要增加一个索引字段就可以高效的得到一个树状结构。根据ancestors创建索引xxxxxxxxxx
db.metadata.createIndex({configs.ancestors: 1
})如图所示在1.0中如果想要按照箭头所指的方向移动往往需要配合数据库中的 这两个字段更新这两个字段来标注页面的位置。 在新的数据库当中由于页面、文件夹、配置是平等关系所以只需要一个 position: 1.59849188522867e18字段来记录就行了当需要移动上下页面时候只需要取相邻两个元素的position的平均值最后结果按照position来排序性能得到很大提升。 通过前后数据结构的对比可以很明显发现在使用MySQL存储时为了要保证元数据节点之间的关系往往需要设计多张表而使用MongoDB后只要一个集合就能搞定设计时元数据的存储这样带来的直接好处就是性能提升和应用程序开发的简化。 元数据服务端使用了golang代替之前的php其实也是为了方便元数据的操作和提升性能由于配置、文件夹和页面的差异被抹平三者被统一抽象为配置于是就很方便的提供统一的元数据操作APIgolang结构体可以完美的将元数据的结构映射到MongoDB的文档模型中开发者可以清楚的看到数据库中元数据结构和代码中是完全一致的这对新人理解元数据结构会有很大帮助。//元数据结构体
type Metadata struct {Model bson:-Id bson.ObjectId bson:_id json:idWorkspaceId string bson:workspaceId comment:工作区IDCommon bson.M bson:common comment:公共配置Configs []Config bson:configs comment:配置信息IsPublished bool bson:isPublished comment:是否发布CreatedAt time.Time bson:createdAtUpdatedAt 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设计模式方面的内容总结一下从开发选型角度大致有以下几点实践经验使用MySQL和MongoDB同时进行数据建模对比两者之间的优劣在表关系比较复杂时可能涉及到多表关联查询较多的场景下利用MongoDB内嵌文档、内嵌数组等灵活的文档数据结构往往能设计出结构更清晰、性能更好的存储方案。小心MongoDB单个文档16M的存储限制对于那种可能无限增长的数据不适合直接使用内嵌方式存储可改为内嵌引用方式。尽量不要使用ORM框架来操作MongoDB往往会误把MongoDB当成MySQL来使用同时不能很好的使用MongoDB强大的API。Golang和MongoDB的结合能在提升性能的同时带来开发上的便利。MongoDB 4.0以后已经支持多文档事务扩展了MongoDB的使用场景越来越多的场景其实是可以使用MongoDB代替MySQL。如果没有特别的必要和限制采用MongoDB往往会给程序设计带来更大的灵活性提高数据库开发效率更好的满足快速迭代开发的需求。MongoDB不能简单理解为一个json文档存储所有数据同时要结合具体的业务场景考虑读写操作是否方便来设计文档模型。------ END ------作者简介段同学 研发工程师目前负责天际-移动平台产品的研发工作。也许您还想看基于 Go 的微服务运行情况监控实践在明源云客一个全新的服务会经历什么云客后台优化的“前世今生”(一)云客后台优化的“前世今生”(二)回归统计在DMP中的实战应用