低代码平台探讨-MetaStore元数据缓存

这篇具有很好参考价值的文章主要介绍了低代码平台探讨-MetaStore元数据缓存。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

背景及需求

之前提到我们模型驱动的实现选择的是解释型,需要模型的元数据信息,在接到请求后动态处理逻辑.

此外,应用的通用能力中还包括:页面dsl查询,菜单查询等.

而且后期加入触发器,用户自定义api后,这些元数据也需要提供查询服务.

所以我们需要一个元数据模块,需要提供两个基础功能:加载元数据和提供元数据查询服务.

特殊说明:最开始的时候我们支持两种源:本地和远程,后期防止单独部署网络隔离问题把远程逻辑去掉了.

第一版迭代处理的元数据有:模型,页面dsl及菜单,后期加入触发器,用户自定义api,拦截器等,我们今天按照第一版迭代来讨论设计及实现.

模型元数据的需求是缓存一批模型元数据,可以根据模型name获取模型的具体信息.

页面dsl的需求是缓存一批页面dsl,根据dslId获取页面dsl信息.

菜单的需求比较简单,缓存菜单列表和获取菜单列表.

上边说的是功能性需求,接着说下非功能性需求:

  • 性能,元数据的查询特别频繁,必须保证高性能,通常会使用缓存,这也是我们这个模块的核心价值之一.
  • 数据要准确,从MetaStore获取的数据不能有问题和差异.
  • 易于扩展,首先元数据不仅仅有根据id获取的需求,可能还有其他查询需求;其次后期加入其他元数据存储的时候要改动小.

初版设计

设计思路

上边说了需求,下边我们开始正式的设计,先选一个具体场景来说:模型元数据.

为了高性能肯定要使用缓存,开发中常用缓存方式有两种:远程和本地.

远程通常使用NoSql的中间件,如Redis和MemCache,在这种场景下肯定不适合.

该场景最适合的方式是使用内存缓存,查询逻辑简单:根据name或者id获取,所以直接使用map的数据结构即可.

元数据是在应用启动时加载,不会再有变动(后期热部署此处需要重构),利用spring的启动机制,也不用考虑线程安全问题,直接使用HashMap就可以,这里应该是利用的jvm的Happens-before原则.

到此,我们确定了缓存的数据结构及接口:

包含一个内部变量cache是HashMAP类型,一个内部方法去加载数据,对外一个接口方法getByKey,功能比较内聚,类设计是没问题的,下边我们看下具体的逻辑.

详细逻辑

getByKey的逻辑是从缓存变量cache中获取,直接使用map的get方法,不用赘述(此处有坑,下文会有说明),主要看下load加载数据的方法

主要逻辑:

  1. 读取指定的目录,可以从配置中获取,获取不到使用默认值:models,读取出目录下所有文件.

  2. 开始循环处理每个文件,详细步骤:

  3. 读取出文件内容:json格式.

  4. json数据转换成Model对象.

  5. 把Model对象放入到cache中,key是modelName,value是Model对象.

抽象

当具体方案确定后,如上边所述的逻辑实现起来并不复杂,甚至可以说是简单.

但我们在整体看下会发现:模型和页面dsl的元数据缓存逻辑相似度特别高!

再回头看下上边的逻辑流程图,紫色的部分都是完全相同的,差异只存在与红色的两块逻辑:"读取指定目录"和"解析成对象",我们可以把公共的逻辑抽象出去,做成抽象的父类让子类去继承,利用了继承的代码复用的场景,这是一个典型的模板模式的应用场景.

简单说下模板模式的定义:在一个方法中定义一个算法骨架,并将某些步骤推迟到子类中实现,让子类在不改变算法整体结构的情况下,重新定义算法中的某些步骤.

结合我们的场景来说下:算法骨架就是整体的加载数据流程和获取元数据方法,子类(ModelMetaStore和PageMetaModel)需要实现骨架中的两个扩展方法:"读取指定目录"和"解析成对象".

第一版设计重构后,类图如下

说明:

  • 模型数据文件和页面dsl文件都放到各自的目录:models和dsls下,上边两个目录是默认的,但可自定义配置.
  • 模型数据目录下的文件类型都是json,文件名是模型名称.
  • 页面dsl目录下的文件类型也都是json,文件名是page的id.
  • 抽象出一个父类AbstractDataStore,泛型是子类的对象类型,包含核心逻辑和骨架.内部有一个map类型的cache变量和一个load私有函数用于加载数据,此外提供了两个抽象方法,一个是directoryName:获取文件所在目录,另一个方式是parse:根据文件内容解析成单个对象,这两个抽象方法需要子类实现,最后还有一个根据key获取元数据的公共方法.
  • ModelMetaStore和PageDataStore继承了父类AbstractDataStore,泛型分别是ModelEntity和PageEntity,实现上述的两个抽象方法:directoryName和parse,两个方法的逻辑都特别简单,第一个直接返回自己的目录,第二个利用fastjson把字符串解析成指定对象.

组合

上面把模型和页面的元数据缓存设计完成了,还有一个菜单的元数据缓存.

菜单的元数据结构与上边的两个差异很大,每个应用中只会有一个菜单的元数据文件,所以无法使用上边的模板模式,我们需要单独去处理它的逻辑.

实际上菜单的元数据加载逻辑和其他两个(更准确的说应该是抽象类)有一部分重叠:读取文件的内容,这里面需要判断文件是否为空,读取内容字符串及io读取报错等.

这部分逻辑可以复制出来放到菜单的元数据缓存类中,但实际违背了DRY 原则:不要写重复的代码.

那么该如何解决这块重复代码的问题呢?有两个方案:

第一个方案:使用继承.

在AbstractDataStore上再加一层抽象类,里面只包括一个方法:读取文件内容,新的菜单元数据缓存也继承这个抽象类,把读取文件内容的方法移到最上层,类结构图如下:

这个方案有两个问题:

  1. 继承层次太深,逻辑有点乱
  2. 最上层的抽象类很难起名

AbstractDataStore和MenuDataCache继承LoadFileData从代码上是可行的,但逻辑上不太合理,抽象继承是is-a的关系,这里需要给LoadFileData一个合适的名字才满足设计规范,而这个名字并不太好起.

第一个方案:使用组合.

这个方案实际上更合理一些,面向对象设计有一个原则:组合优于继承.

可以把读取文件内容的逻辑独立出去作为一个新的帮助类DataLoadSupport,AbstractDataStore和MenuDataCache引入它就可以解决代码重复问题.

初始化加载

上一步几乎完成了所有逻辑,但是少了数据加载的触发,这个需要放到服务启动的时候触发.

我们利用了spring的ApplicationListener,有一个细节问题要确定:如何实现ApplicationListener,是每一个类中单独实现,还是统一实现.

为了逻辑的统一,还有后期其他服务需要启动时加载的考虑,我选择了统一实现,具体逻辑和类图如下:

具体逻辑:

  • 新增一个接口StartLoadListener:启动加载监听,启动时需要被触发的操作类实现该接口.
  • AbstractDataStore和MenuDataCache实现StartLoadListener接口,标记需要在应用启动时触发,应用启动后触发自己的load方法.
  • 新增StartLoadListenerManager类,里面会自动注入所有实现StartLoadListener接口的对象,该类同时实现了spring的ApplicationListener接口,在spring启动后被触发,触发后调用所有的StartLoadListener的startLoad方法,完成所有需要启动时触发的操作.

类图:

第二版设计

如文章开头所说,从元数据缓存中使用key从map中获取信息后直接返回是有问题的,开发完的时候也意识到了一些,但没有去处理,直到某天雷真的炸了.

先说下问题出现的过程,初期功能简单,低代码平台生成的应用都跑的好好的,迭代数次版本后,新增了权限能力:支持简单的数据权限和菜单权限.

菜单权限的处理逻辑大体如下:

  • 从菜单缓存中直接获取缓存的数据.
  • 如果菜单列表为空,直接返回给客户端空的列表,结束菜单查询.
  • 查询用户有权限的菜单编码列表(set),数据源头是科技权限系统,初期比较粗暴代码耦合到菜单权限代码中,但这不是今天讨论的重点,后边的文章会讲到权限模块的重构.
  • 如果用户有权限的菜单编码列表(set)为空,直接返回给客户端空的列表,结束菜单查询.
  • 递归处理获取的缓存菜单,如果编码在权限系统返回的编码列表(set)中,递归处理子节点列表(菜单是树形的),否则直接删除该节点,返回到上级递归.
  • 循环上步操作,直到所有菜单节点校验完成.

逻辑并不太复杂,开发的时候特意重点关注了递归和权限系统查询,本地测试没什么问题,部署到测试环境也没发现什么问题.

但最后要上线前测试发现了一个问题:切换账号后,菜单不对,有权限的菜单变少了!!!

定位问题的时候,先排查权限系统的返回,结果没问题;在把数据拿到本地走mock测试也没有问题;在最后debug的时候发现了真正的问题,也就是之前意识到但没有解决的问题:在菜单权限逻辑中操作的菜单列表和菜单元数据缓存的菜单列表是同一个对象!

在某个用户处理完自己的权限菜单逻辑后,可能会移除一些节点,下一个用户在获取菜单的时候他的菜单元数据可能就是被处理过(在权限处理中被移除)的了.

元数据存储实际上类似原型模型,原型模式的定义:如果对象的创建成本比较大,而同一个类的不同对象之间差别不大(大部分字段都相同),在这种情况下,我们可以利用对已有对象进行复制(或者叫拷贝)的方式来创建新对象,以达到节省创建时间的目的.

我的实现只做了缓存,但没有实现复制,使得同一对象被多个场景操作.最后导致数据的错误及混乱.

而复制一般也叫拷贝,分为两类:

  • 浅拷贝:只会拷贝对象中的基本数据类型的数据(比如,int、long),以及引用对象的内存地址,不会递归地拷贝引用对象本身.
  • 深拷贝:不仅仅会复制索引,还会复制数据本身

我们肯定要选择深拷贝,但出现问题的时候已经有几个元数据存储了,特别是菜单数据的结构复杂(树形),短时间没法完成深度复制,市面上的工具类通常只能复制一层,无法自动完成深层次的复制.

当时的解决方案比较粗暴:问题出现在菜单,那只在菜单权限处理的时候做手脚:定义一个新的菜单列表,有权限的菜单深度复制当前对象后加入到list;递归处理子节点.继续重新定义一个list设置为上层节点的子节点,有权限的子节点放入到新的list,递归循环上述步骤.

表象的bug解决了,但实际问题还没有解决,只是被遮盖了,就拿菜单来说,我是在权限服务那处理了,但如果在其他地方使用呢,一样会出现数据缺失的错误.

更严重的是在触发器中提供了获取模型元数据的接口,开发者获取模型元数据后如果进行了修改,导致的错误会更加严重,也更加的难已排查,因为模型元数据的变更会导致模型通用接口逻辑的缺失或混乱,所以深度复制的功能一定要实现.

深度复制的实现一般有两种:

第一种序列化后反序列化,比如把数据序列化成json,在反序列化回来.该方案有两个问题:(1)继承的子类多态容易出问题(2)序列化和反序列化是有性能消耗的,在此处方案并不适合.

第二种是递归处理:遇到Collection和Map及javabean类型,进行递归的深度拷贝.该方案开发成本稍高,大量的递归需要特殊注意,此外一些特殊的bean也无法复制:如内部变量是final类型,或者只支持构造函数传入.

我们使用的是第二种,这种方法纯粹是算法类,不再细说,在中间碰到了一个坑:我有一个习惯遇到一些没有数据需要返回空的list的时候我会直接使用Collections.emptyList(),在深度复制的时候根据构造函数新建对象会直接报错,修复方案是:如果是list或者map不是常见的对象类型直接使用常见的对象ArrayList和HashMap.

实际上还有一种方案能解决数据修改问题,使用不变对象,不变对象就是对象在创建后,不可以被修改:没有set方法,且内部变量不会暴露,需要注意的是,内部变量也要进行保护:不变或者深拷贝.

作者:京东科技 吴籽良

来源:京东云开发者社区 转载请注明来源文章来源地址https://www.toymoban.com/news/detail-710888.html

到了这里,关于低代码平台探讨-MetaStore元数据缓存的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 探讨:是否可以不见用户,利用AI做用户洞察/需求分析(附上可用的GPTs)

    这个探讨的缘起呢,是很多企业内部的产品经理,因为各种原因,无法接触用户。 首先,做产品不接触用户当然不对,但这就是现状,所以探索一下有没有可能借力AI把这个事情的危害减少,哪怕一点点。 为什么可以试试? 第一,因为AI相比任何人,有着更广泛的数据/知识

    2024年02月03日
    浏览(42)
  • 深入探讨Eureka的三级缓存架构与缓存运行原理

    在当今的软件开发领域,分布式系统已经成为了必 不可少的一部分。而在分布式系统中,服务的注册与发现是其中的重要组成部分之一。Netflix开源的Eureka便是一款优秀的服务发现框架,它采用了三级缓存架构来提供高效的服务发现与注册功能。本文将深入探讨Eureka的三级缓

    2024年02月11日
    浏览(39)
  • 深入探讨 Presto 中的缓存

    【squids.cn】 全网zui低价RDS,免费的迁移工具DBMotion、数据库备份工具DBTwin、SQL开发工具等 Presto是一种流行的开源分布式SQL引擎,使组织能够在多个数据源上大规模运行交互式分析查询。缓存是一种典型的提高 Presto 查询性能的优化技术。它为 Presto 平台提供了显着的性能和效

    2024年02月07日
    浏览(64)
  • 探讨Redis缓存问题及解决方案:缓存穿透、缓存击穿、缓存雪崩与缓存预热(如何解决Redis缓存中的常见问题并提高应用性能)

    Redis是一种非常流行的开源缓存系统,用于缓存数据以提高应用程序性能。但是,如果我们不注意一些缓存问题,Redis也可能会导致一些性能问题。在本文中,我们将探讨Redis中的一些常见缓存问题,并提供解决方案。 缓存穿透指的是当一个请求尝试访问一个不存在于缓存中的

    2024年02月03日
    浏览(89)
  • 高效协作处理缓存清理需求:生产者-消费者模式助力多模块缓存管理

    在现代应用系统中,缓存是提高性能和减少数据库负载的重要手段之一。然而,缓存的数据在某些情况下可能会过期或者变得无效,因此需要及时进行清理。在复杂的应用系统中,可能有多个系统、多个模块产生缓存清理需求,而这些系统、模块之间的清理任务需要高效的协

    2024年02月15日
    浏览(44)
  • 【大数据之Hive】六、Hive之metastore服务部署

      metastore为Hive CLI或Hiveserver2提供元数据访问接口。   metastore运行模式有两种,嵌入式模式和独立服务模式。 (1)嵌入式模式   将metastore看作一个依赖嵌入到Hiveserver2和每一个HiveCLI客户端进程,使得Hiveserver2和HiveCLI客户端直接连接访问数据库。 (2)独立服务模式  

    2024年02月03日
    浏览(43)
  • 访问 Hive 的元数据存储(MetaStore)的API方式

    访问 Hive 的元数据存储(MetaStore)的API方式 访问 Hive 的元数据存储(MetaStore)是通过 Hive 的 Thrift API 来实现的。Thrift 是一个跨语言的远程服务调用框架,它可以让不同编程语言之间进行跨语言的远程过程调用(RPC)。Hive 的元数据存储的 Thrift API 允许你通过编程语言(如 J

    2024年02月14日
    浏览(44)
  • 【文章+代码】2023妈妈杯大数据B题分享 mathorcup 电商零售商家需求预测

    本次的妈妈杯大数据B题我们也将持续陪跑,目前已经完成了大部分的代码,和第一版文章。 下面进行文章摘要和其他部分的分享 基于时间序列的电商零售商家预测模型 摘要 在电子商务平台上,通常有数以千计的零售商家,它们将其商品存放在该电子商务平台提供的仓库中

    2024年02月05日
    浏览(38)
  • 【2023Mathorcup大数据】B题 电商零售商家需求预测及库存优化问题 python代码解析

    2023 年MathorCup 高校数学建模挑战赛——大数据竞赛赛道B:电商零售商家需求预测及库存优化问题电商平台存在着上千个商家,他们会将商品货物放在电商配套的仓库,电商平台会对这些货物进行统一管理。通过科学的管理手段和智能决策,大数据智能驱动的供应链可以显著降

    2024年02月08日
    浏览(62)
  • 【代码思路】2023mathorcup 大数据数学建模B题 电商零售商家需求预测及库存优化问题

    各位同学们好,我们之前已经发布了第一问的思路视频,然后我们现在会详细的进行代码和结果的一个讲解,然后同时我们之后还会录制其他小问更详细的思路以及代码的手把手教学。 大家我们先看一下代码这一部分,我们采用的软件是Jupyter,大家可以下载Anaconda,然后选择

    2024年02月08日
    浏览(49)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包