前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >拒绝代码臃肿,这套计算引擎设计方法值得一看!

拒绝代码臃肿,这套计算引擎设计方法值得一看!

作者头像
腾讯云开发者
发布2021-08-19 10:11:17
5940
发布2021-08-19 10:11:17
举报
文章被收录于专栏:【腾讯云开发者】

导语 | 在庞大的数据系统中,往往会有大量的计算需求。传统的方式便是直接在代码写各种计算逻辑判断,这导致了代码非常臃肿,计算维护的成本变大。所以想着编写一套DSL,定义专用的语法去实现对数据的计算,并将其独立成为底层基础服务。

一、DSL 设计

(一)何为 DSL

领域特定语言(英语:domain-specific language、DSL)指的是专注于某个应用程序领域的计算机语言。不同于普通的跨领域通用计算机语言(GPL),领域特定语言只用在某些特定的领域。

简单来说,就是利用DSL,通过抽象构建模型,抽取公共的代码,以达到提高开发效率,减少重复的劳动的目的,比如经常使用的SQL。

同样的思路,我们要将复杂的逻辑判断与计算规则抽象化,构建计算DSL。

(二)如何通用化设计计算 DSL

值得庆幸的是,办公中经常使用的Excel就包含了许多计算规则。

让我直接举一个例子来说明,比如:要计算实际支出超出预算的金额,由于超出金额不可能为负数,所以逻辑条件为:如果实际支出大于预算,则结果为实际支出减预算,反之则取0。对应的Excel计算公式为:

代码语言:javascript
复制
IF (C2 > B2, IMSUB (C2, B2), 0)(C2 代表实际支出,B2 代表预算,IMSUB 代表减法)

有了一个这么专业的例子,那么对应我们的计算DSL就是:

代码语言:javascript
复制
IF ({budget} > {actual_expenses}, IMSUB ({budget}, {actual_expenses}), 0)({}用于标示具体字段,budget、actual_expenses 代表数据库中对应的预算、实际支出字段)

(三)DSL 设计的优势

  • 与Excel计算规则相似,减少用户学习成本。
  • 按照专业的规则来定义,使计算DSL更规范。
  • 由于规范的设计,更有利于后期扩展。

二、计算引擎的实现

(一)DSL 解析

对于这种有关键字并且无限嵌套的DSL,应该没有比堆栈更合适的方法来解析了。下面是具体例子的部分解析代码:

代码语言:javascript
复制
$dsl = 'IF({budget}>={actual_expenses},IMSUB({budget},{actual_expenses}),0,1)';
$stack               = []; // 堆栈$result              = []; // 结果$comparisonOperators = ['<', '>', '&', '|', '=']; // 比较运算符$placeholders        = [',', '(', ')']; // 占位符for ($index = 0; $index < strlen($dsl); $index++) {    $key = $dsl[$index];    switch ($key) {        // 解析变量        case '}' :            $variable = '';            while (true) {                $item = array_pop($stack); // 出栈                if ($item === '{') {                    break;                }                $variable = $item . $variable;            }            $result[] = $this->getVariable($variable); // 获取真实变量值            break;        // 解析方法        case  '(' :            $method = '';            while (true) {                $item = array_pop($stack); // 出栈                if (is_null($item)) {                    break;                }                $method = $item . $method;            }            $result[] = $method;            $result[] = $key;            break;        // 存储占位符,清空栈内变量(常量)        case in_array($key, $placeholders) :            $variable = '';            while (true) {                $item = array_pop($stack); // 出栈                if (is_null($item)) {                    break;                }                $variable = $item . $variable;            }            $variable != '' && $result[] = $variable;            $result[] = $key;            break;        // 解析比较运算符        case in_array($key, $comparisonOperators) :            if ($dsl[$index + 1] == '=') { // 兼容 >=、<=                $result[] = $key . '=';                $index++;            } else {                $result[] = $key;            }            break;        // 入栈        default :            $stack[] = $key;            break;    }}

(二)数据结构化

通过DSL解析可以得到“未赋值”的结构,再根据预先存储的数据模型对变量进行赋值,我们便可以得到如下结构:

这样一来,DSL就变成了机器所能识别的数据,将参数带入到指定的函数中便能得到计算结果。

(三)递归计算

从上图的结构中,我们可以分析出:每一个计算都包含了计算函数、占位符(开始符、分割符、结束符)以及函数对应的多个参数。其中参数可以是比较运算(IF函数第一个参数必为比较运算),也可以是另一个函数。这时候我们只需要使用递归的方式去不断往下运算便能得出结果。

代码语言:javascript
复制
/** * IF 函数核心计算逻辑 */public function calculate(){    // 计算比较结果    if ($this->getComparativeResult()) {        return $this->getResult($this->params[1]); // 返回真    } else {        return $this->getResult($this->params[2]); // 返回假    }}/** * 获取计算结果 */public function getResult($params){    // 如果是函数,则继续计算    if (is_array($params)) {        return (new Calculate($params))->calculate(); // 递归计算    }        // 非函数,直接返回结果    return $params;}

(四)架构梳理

首先对输入的DSL进行校验、解析并结构化数据;然后启动多个计算引擎同时并行处理;最终输出计算结果。

三、项目接入

(一)架构设计

整体架构分为五层,上层应用层提供给具体应用接入;通讯层负责对接收应用层的数据,及对支持应用层轮询获取计算结果;DSL解析层负责DSL校验、DSL解析以及数据结构化;处理完之后再到核心计算层,进行具体的计算执行;最后再将结果入库并将结果发送到消息队列中。

其中,DSL 解析层和核心计算层共同组成计算引擎

四、问题与思考

(一)计算提升效率缓慢

在完成项目接入后,为了提升计算效率,采用并行执行的方式来执行计算。期望的效果便是:随着并行的数量增加,效率也随之增加。

但事实总是事与愿违,即使扩大计算的并行数量也无法成倍提升计算效率,并且当并行数达到一定量时,效率提升越不明显

(二)计算依赖 

在经过仔细的问题排查之后,发现数据计算之间是有依赖关系的。让我们直接看下图的例子:当同时计算A、B、C三个字段时,不管如何并行执行,B的计算永远依赖A计算的结果;同理,C的计算也永远依赖A和B的计算结果。总而言之,就是说计算效率是有瓶颈的。

那么,如何能够用最少的资源达到整体计算的最佳效率呢?

五、解决方案:寻找最优解

(一)策略优先算法

对于每个计算字段来说,我们是知道具体依赖的程度的:

  • 对于A、D,只依赖常数,所以他们依赖程度为0。
  • 对于B、E,分别依赖A、B,那么他们的依赖应该分别在A、B的基础上+1,所以他们的依赖程度为1。
  • 对于C,同时依赖A、B,那么他的依赖程度应该为A+B+1=2。

所以,我们将每个字段排了优先级,对于同一优先级的字段并行计算,依次进行,便能以最少的资源达到整体计算的最佳效率

(二)计算速度不一致

在实际的计算中,每个字段计算的速度是不一样的。比如:在第一优先级中的A需要不断的累加才能得出结果,需要比同一优先级的D花费更长的时间。假如此时D已经计算完成,那么E其实已经不需要再依赖其他计算了,应该立即被执行。但由于第一优先级还未算完,所以只能继续等待。这样一来,对于计算结果的反馈非常的不友好。

(三)更进一步:动态策略优先算法

为了能快速的响应计算结果,我们需要在计算的同时,对计算完成的字段触发完成事件,对依赖该字段的其他优先级字段,重新分配优先级,当获得第一优先级时,立即执行

比如:在D计算完成后,去修改E的优先级,因为E只依赖D,而D已经计算完成,所以应该获得第一优先级,立即执行。

六、总结

(一)架构完善

在动态策略优先算法的思路下,我们在原先的结构中引入策略分配层。在DSL解析之后将数据传入到策略分配层中进行策略计算;然后,依次对各个优先级的字段进行计算任务调度;在计算完成后对事件进行处理,再依次进行任务调度;最终在完成整个计算后将数据入库。

其中,DSL解析层、策略分配层和核心计算层共同组成计算引擎

(二)下一步:引入监控

在完成了一系列的开发与项目接入工作之后。对于整个底层计算服务来说,并不是已经无懈可击了。

  • 在DSL的解析中需要实时监控解析结果,及时对错误进行拦截与记录,避免影响下层计算。
  • 在策略分配层中,也需要对每一次的策略计算、任务调度、事件处理进行监控,因为每一次错误都将影响整个模型的计算结果。
  • 在最终入库之前,还需要监控每个字段的计算结果是否符合预期。及时对错误结果进行修正。

 作者简介

林楨淵

腾讯 CDC 团队应用开发工程师

腾讯 CDC 团队应用开发工程师,毕业于广东工业大学,负责腾讯投资决策信息平台开发。致力于低代码开发平台(包括流程引擎、表单配置、计算引擎等等)的架构设计与持续优化。在投资领域开发有着丰富的落地经验。

推荐阅读

程序员如何把你关注的内容推送到你眼前?揭秘信息流推荐背后的系统设计

在Exception的影响下,如何才能写出更高质量的C++代码?

自动的内存管理系统实操手册——Java和Golang对比篇


本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-08-17,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 腾讯云开发者 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档