用规则引擎让你一天上线十个需求

作者: 程序员薯条 来源:薯条的编程修养

各位读者朋友大家好,我是薯条,好久没更文章,不知还有多少读者记得这个号,这篇文章写的有点精分,如果你有耐心看完本文,可以翻翻留言区,我会发个新年红包。

业务背景

如果是本号老读者,可能知道我是做数据系统的,作为一个在线数据服务组,我们这边承接的需求是小而多的。我在一家打车公司上班,运营大佬们认为不同用户在不同场景下有不同打车需求,设计出来很多子品类。于是我们组会承接这样一类需求:计算用户不同品类的各种实时单量,如:快车呼单量、拼车完单量。

这样的需求,一般处理流程是这样的:

描述一下这个图:用户在订单流转状态关键节点发生动作时,系统会发一个MQ消息让供其他系统消费。其他系统通过一个明确的据口径判断这条msg是否符合当前业务逻辑,进而存db或是丢弃。比如一个需求要计算:拼车完单量,一个靠谱的拼车rd告诉你口径是:

If aa.bb.cc == 1 // 说明是多车型发单

Unmarshal(bb.cc.ee)

看type是否为 4

else // 单车型发单

Unmarshal(bb.cc.ff)

看type是否为 4

(type = 4 的是拼车) 你对着这个口径,订阅mq,写数据提取和订单判断的逻辑,整个流程写代码1小时,自测一小时,由于你们机器太多,上线花了1整天,整体研发效率还行。

第二天,产品又给你提了个需求,想计算拼车的发单量,你又去找对应业务线的开发同学寻求一个取数口径,然后重复上面的过程。

第三天,产品上线了,效果不错,数据涨了3个点,老板非常开心,在周会上让PM讲两句他的心路历程,PM同学重点感谢了老板的栽培,然后轻描淡写的说了产品的底层逻辑和关键抓手。而他的几个精明的同事都get到了抓手是你,于是连夜赶PRD,要求你这个抓手把他们负责品类的各种实时单量全给抓出来。

第四天,你崩溃了,因为你收到4个PM并行的8个单量需求,正当你奋笔疾书再次准备重复上述流程时,你睿智的老板告诉你这样做有不妥之处:来一个坑填一个萝卜是小农时代的做法,现在都21世纪了,时代变了,让你想一种通用解决方案,让系统走向工业时代!

你似懂非懂点了点头,查了各种CSDN、博客园、知乎、github,又在技术交流群各种@群里大佬有没有遇到这种场景。一番折腾后终于有了头绪,于是你高兴的向老板汇报:老板,我懂了,这个场景可以用JPATH + Expression Eval来解决!这样一来,再来新的需求只需要写在db里插入俩表达式就可以了,20个需求提过来也不用怕。

你的老板微笑着点了点头,看了一眼自己手上的劳力士,有意无意的晃了晃,说:小伙子很上道,自己也琢磨出解法了,赶紧设计方案,争取本周上线,尽快拿到业务结果,到时候升职加薪少不了你的!

实现方案

这个系统的核心需求有两点:

数据提取规则判断数据提取即ETL,把mq的msg中关键信息提取出来,提取之后可能还需要简单处理一下(比如msg中事件时间是timestamp,你想转化为RFC3339格式) ,这里可以用JPATH 做数据提取 (如果你写过爬虫,一定知道用xpath去提取HTML中的node消息,jpath就是json数据的提取规则)。配置一个ETL rule,如图所示:

然后是数据规则判断,即题目中提到的规则引擎,我们这里使用 开源库govaluate,比如上面拼车完单的例子,我们可以配置这样的规则:

cc == 1 ? ( in(4, ee)? 1:0 ) : ( ff ==4 ? 1:0) govaluate会把这个表达式构建出一颗ast,然后输入参数进行求值(是不是回忆起来了编译原理?)。接下来让我们研究一下这个库~

govaluate介绍与使用注意

govaluate支持对C风格的算数/字符串的表达式进行求值。比如这些例子(例子来源于evaluation_test.go):

1. 100 ^ (23 * (2 | 5))

2. 5 < 10 && 1 < 5

3. (foo == true) || (bar == true) // foo、bar为变量

4. theft && period == 24 ? 60 // theft、period为变量 这个库几乎支持你能想象到的任何表达式,有兴趣可以去这个test文件。除此之外它支持拓展UDF, 你可以自己写一些函数支持你的定制业务逻辑,它还支持执行类方法,更多信息可以看README。

当我们需要使用它时,只需要这几行代码

expression, err := govaluate.NewEvaluableExpression("foo > 0");

parameters := make(map[string]interface{}, 8)

parameters["foo"] = -1;

result, err := expression.Evaluate(parameters);

// result is now set to "false", the bool value. 通过这个demo我们可以看到它的api被设计成了两步, 第一步NewEvaluableExpression的功能主要是把表达式拓展为一颗AST,Evaluate的主要功能是把用户参数填入ast求值。。举个例子:比如1 + foo + 4 * boo这个表达式,在两个阶段分别做的事情是这样:

那么生产项目代码中直接把这两步抄进去就可以了吗?显然不是。通过观察就可以发现, 第一步构造ast依赖的表达式其实是可以预先确定的,且表达式一般不会变化,没有必要用户每次传一个api就构造一颗ast然后求值。可以把表达式存入db,在项目启动or更新配置时加载到内存中, 比如搞一个map[string]*EvaluableExpression, 把不同表达式的ast进行cache,这样用户每次请求时只需遍历ast进行求值。

预编译所带来的收益很显著,尤其是在你表达式比较复杂的情况下。我对foo > 2? 1:0这个表达式分别做了现编译和预编译的benchmark,结果如下:

现编译

(现编译 构建ast占用62.3%的cpu开销,而eval只占2%)

预编译

(预编译省掉了构建ast的成本,节约了大量cpu资源) 建议大家如果使用这个库,有条件要用预编译版本。

govaluate 原理

看起来govaluate很有意思, 接下来让我们挖一下它的源码。首先来看第一阶段,把表达式拓展为AST时的逻辑,我简单画了一张图:

下面以1 + foo + 4 * boo为例,parserToken后,我们可以得到一堆token:

checkBalance没啥好说的,核心功能就看小括号是否成对出现:

而checkExpressionSyntax阶段主要是check token之间是否符合预设规则,核心是这个函数:

这个函数会check当前的token是否是上一个token的合法值,合法值是预设的,比如NUMERIC的合法值是后面这些:

接下来的 optimizeTokens 函数没啥好说的,主要就是编译一下正则。

比较有意思的是planStages这个步骤。planStages这个大步骤内部大概分成了planTokens、reorderStages、elideLiterals这三个小步骤,下面来一一介绍:

planTokens

这个函数写的让我大开眼界,首先它用func做不同运算符的优先级计算,原理是func接收struct作为参数,而参数中的next为这个函数连接的下一个优先级的func。

这个func优先级打印出来是这样的:

有了运算符优先级之后,对于具体的节点,会继续看节点类型,比如是func,accesser还是valueType,valueType的节点对于不同的详细类型也有不同策略,比如数字节点会构建一个Node,而小括号节点会直接parser下一个token来构建优先级更高的树。

对于不同的运算符,在这个函数链上会下沉构建出优先级比较高的节点,保证符合数学计算的规律。

reorderStages

这里主要把ast重排序,让ast由普通tree变成avl tree,树旋转的代码写的特别骚气,比如1 + foo + 4 * boo 这个表达式,planToken执行完后,会变成这样一颗树:

重排序的过程是把相同优先级的节点进行旋转,第一步是交换左右节点:

第二步是LL左旋:

这样就平衡了,一个非常骚气的算法。

elideLiterals

这个步骤是看叶子节点是否为LITERAL,比如这棵树:

在这个阶段,各个子节点会进行dfs计算直接变成:

至此第一阶段的逻辑梳理完毕。而第二个阶段Evaluate的主要功能是把用户参数填入ast,进行求值。这个过程比较简单,本文不在赘述。

govaluate 不足

govaluate 看起来很美好,真的是这样吗?其实不然,这个项目最后一次commit是2017年,距今已经6年了。我们在使用期间也发现了很多小bug和代码优美度欠缺的地方。下面来简单列举几个:

弱类型

govaluate所有数字类型都是被解析为float64进行计算的,这么玩写代码爽了,但是当你用1+2+9做表达式时,可能会得到一个类型为fload64的interface{}结果。

函数限制

govaluate的函数有的返回值无法继续做运算。比如这个case:

看起来没有任何问题,但是执行会报错:

参数会去除转义符

比如这段代码:

理论上结果应该含有转义符,实际上结果是:

实际上是这段代码搞的鬼,代码比较简单,就不解释了。

奇奇怪怪的代码

this关键字:这个就不举例子了,这个库里所有方法的接收者都是this,被官方建议熏陶过的我,看的我着实蛋疼...双重否定表肯定:token解析阶段有这样的代码,不知道作者为啥要搞个双重否定,我的话,会用一个isQuote代替。

govaluate改进

作为一个17年后就没更新过的项目, 也不知道作者还会不会维护。业务发展是不等人,govaluate对于我们服务来说并不能满足需求,很多时候用起来比较别扭,所以我基于我们的场景对于govaluate做了一些定制改造。我个人还是非常喜欢这个库的,于是把代码fork了一份,加了个eplus后缀,改造了上面那几个匪夷所思的问题。并加了个比较定制化的feature:type promotion。

这个听起来比较唬人, 其实就是支持更弱类型的表达式运算,比如我的库支持:'2' -1, '4' * 3,要支持这种功能,核心需要改两种地方:

第一种地方是typeCheck。比如subStage会check两个字节点必须是float64类型, 我们要支持string operator num, 可以把typeCheck扩大为可以是chek node是否为 floatOrStr。第二种地方是OpeatorOperate。前面我们把String类型也放进来让它支持计算了,但是在go里str和float终究是无法计算的, 所以到了计算阶段需要做一个type promotion,即把string类型转化为数字类型之后再计算。

总结与反思

总结

govaluate在我心中还是有一些不完美的地方,我们这里用它也是因为项目初期就引入了这个库,在大量的线上用例使用后要迁移这个库成本巨大, 对于用的不爽的地方只能改了。如果读者朋友有需求, 可以看一下市面上其他的表达式开源库, 比如gval。当然,如果你的场景比较复杂, 需要很多if else 或者for循环,那简单的规则引擎可能满足不了你的需求,此时可以考虑内嵌个更完整的脚本库或者嵌入lua, 不过这样就更复杂了, 慎重考虑把这样的东西直接放在db里面, 后期不好维护。

反思

govaluate这个库对我有很多启发,最主要就是表达式的预编译可以节省大量CPU开销,组内某个项目目前的运行方式是随着请求现编译,构建执行计划dag图,理论上如果能预编译,请求到来只是对于对应param访问存储,可以节省大量CPU开销。

跳脱出govaluate本身,我们系统选择JPATH + Expr做数据提取和条件描述做需求,本质上是因为这边的mq数据是JSON格式,JSON有一定的局限性,描述数据没啥问题,但是描述条件就比较困难了,理论上如果用XML这种技能描述条件,又能描述数据的交互形式,那我们可能会构建一个完全不同的系统。

最后稍微打个广告吧, 如果你也想使用govaluate,又有一些定制化的需求,欢迎star我的库然后提个issue, 我想试着维护一个开源库, 嘿嘿。

原文地址:https://www.toutiao.com/article/7051368877661684236/

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


所以很多人都会选择将这些生活琐事来交给智能化产品,在众多产品中,扫拖机器人所给我们带来的便利性最强,扫地、拖地全都一气呵成,不需要人为过多干预,不过目前大多数扫拖机器人对于双手解放得不彻底。而石头作为
“昨天的经历都将成为明天的力量。” 将绝大部分精力都奉献给璃月港的刻晴,可以说是《原神》中的高人气角色了,虽然是常驻角色,并且对于普通玩家来说……刻师傅别刮了……不过作为开服就存在的角色,还有独特的剑法
最近,全球知名的通信产业盛会MWC 2024(2024世界移动通信大会)正式召开,其中,联发科以“连接AI宇宙”(Connecting the AI-verse)为主题,为大众展示出一系列在AI和移动通信技术等领域的最新突破,吸引了大量行业
今年上半年有很多值得关注的机型,其中华为最新的影像旗舰华为P70 Art也自然受到了业界不少的关注目光,目前关于这款机型的轮廓图已经在网上曝光。
目前,2024世界移动通信大会(MWC)正在西班牙巴塞罗那举行,值得一提的是,此次大会参展中国厂商非常多,包括华为、中兴、小米、荣耀等等多家厂商均在其列。
就在去年,真我推出了11 Pro+,用一个2亿像素传感器和zoom变焦功能,开启了中端手机影像的长焦大战,而后友商才姗姗来迟的跟进了2亿像素传感器。
【手机之家新闻】一年一度的MWC已经于当地时间2月26日在巴塞罗那正式开展,在本次MWC2024上全球各大厂商齐聚一堂,展出自家最新的技术与产品,其中中兴就参展本次MWC2024,并且展出了诸多面向企业端的产品,而旗下的
近日,联发科在MWC 2024(2024 世界移动通信大会)上展出了一系列令人瞩目的AI和移动通信技术突破,以“连接AI宇宙”(Connecting the AI-verse)的展厅吸引了无数业界精英和媒体的目光。特别是其现场的生成式AI技术
虽然目前国内已经有不少厂商入局折叠屏产品,但是努比亚却迟迟没有入局。不过在近日举办的MWC 2024展会上,努比亚发布了自家首款折叠屏手机——努比亚Flip,预计国内很快也会上市。
MWC 2024正在西班牙巴塞罗那举办,和往年一样,荣耀这次依旧携众多新产品、新技术参会。荣耀Magic6 Pro、荣耀Magic V2 RSR保时捷设计的机型在海外正式发布,并且还展示了魔法大模型、任意门等诸多新技术。
MWC 2024正在西班牙巴塞罗那如火如荼地举行,其中小米也参加了今年的大会,在会上发布了在国内大受欢迎的小尺寸旗舰——小米14。值得一提的是,高通公司CEO安蒙甚至亲临发布会现场为这款机型助阵。
《原神》是一直以来在机圈深受欢迎的游戏,在充满幻想的提瓦特大陆上,你可以邂逅不少性格迥异、能力独特的伙伴。而一加Ace系列一直就拥有非常强烈的电竞属性,也是畅玩《原神》的热门机型,而在本月,一加Ace 3将推
有不少网友发现,今年新机的发布时间相对于往年大幅提前,很多厂商在春节之前密集发布了自己最新的中高端机型,给人一种年后没什么新机可发了的感觉。不过魅族全新的大杯机型——魅族21 PRO非常值得期待,魅族科技也
2022年7月,小米12S Ultra正式发布,这款产品率先将1英寸大底主摄引入到移动影像领域,同时凭借鲜明的徕卡影调给人留下深刻的印象,同时这款产品也被视为了影像旗舰地位的机型。如果从那时算起,到现在差不多已经快过
随着智能手机的日益普及和智能化进程的加速,智能穿戴设备成为了人们关注的焦点。各大智能手机厂商纷纷进军智能穿戴市场,试图在这一新兴领域抢占先机。
早在去年秋天,HyperOS操作系统发布的时候,小米便勾勒出了“人车家全生态”的美好蓝图,而在这其中,小米的多终端统一战略是核心,目前已经有不少小米产品预装或者接受到了HyperOS操作系统的推送,在过去几个月的时
今年雷军将把更多的精力放在小米汽车上,所以接下来的手机业务将由刚刚兼任小米品牌总经理卢伟冰接管。同时雷军也在微博上表示小米2024年开年旗舰——小米14 Ultra即将在近期发布,并且将有卢伟冰进行讲解。另外,卢
新的一年有龙则灵,有愿必达。自1月19日起,荣耀加码“新年荣耀,一起成龙”年货节,在全国荣耀线下门店上线了“新年许愿处”、“龙运当头”等趣味活动,吸引大批消费者到店打卡许愿,戴龙头迎好运。与此同时,为了回
小米在官网微博中已经透露了关于小米14 Ultra信息,所以新机上市应该不会太晚。根据德国莱茵的官方消息,目前小米14 Ultra(型号为24030PN60G)获得了莱茵无频闪认证,表明这款手机可以有效减轻屏幕给用户带来的视觉疲
2月22日,上海广播电视台与华为举办鸿蒙合作签约仪式,宣布其官方客户端看看新闻APP将基于HarmonyOS NEXT鸿蒙星河版启动鸿蒙原生应用开发,为用户提供更加极致的新闻资讯服务体验。此次合作标志着上海广播电视台成为全国