规则引擎 | 青训营笔记

109 阅读4分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 9 天

规则引擎是一种嵌入在应用服务中的组件,可以将灵活多变的业务决策从服务代码中分离出来。通过使用预定义的语义模块来编写业务逻辑规则。在执行时接受数据输入、解释业务规则,并做出决策。规则引擎能大大提高系统的灵活性和扩展性。

在字节跳动,规则引擎已经在风控识别、活动运营、配置下发等场景得到了广泛的应用。开发人员可以将业务逻辑与服务代码解耦,实现灵活、高效的业务策略发布。目前公司内部基于规则引擎的动态决策系统已经承接了千万级别QPS的决策请求。

规则引擎的实现需要在满足大容量、高请求、低延迟的基础上尽可能做到简单易上手。本次课程将会带领大家实现一个简单版的规则引擎。

组成部分

  • 数据输入

    • 支持接受使用预定义的语义编写的规则作为策略集。比如“price>500”,接受业务的数据作为执行过程中的参数,比如价格、标签等。
  • 规则理解

    • 能够按照预定义的词法、语法、优先级、运算符等正确理解业务规则所表达的语义。
  • 规则执行

    • 根据执行时输入的参数对策略集中的规则进行正确的解释和执行。同时对规则执行过程中的数据类型进行检查,确保执行结果正确。

编译原理基本概念

  • 理解

    • 词法分析、语法分析

      • 词法分析:把源代码字符串转换为词法单元(Token)这个过程
      • 语法分析:在词法分析的基础上识别出表达式的语法结构
  • 执行

    • 表达式抽象语法结构的树状表示,对于一个表达式,抽象语法树一定是唯一确定的
  • 输入输出

    • 参数注入、类型检查

      • 类型检查:验证执行的结果是否为合适的数据类型。在抽象语法树中,通常会验证某节点的子节点的数据类型是否合法
      • 参数注入:在规则执行过程中,使用输入的参数值来计算语法树中的标识符节点值的过程

词法分析

词法分析就是把源代码字符串转换为词法单元(Token)这个过程

如何识别Token有限自动机(Finite-State Automaton)

  • 有限自动机就是一个状态机,它的状态数量是有限的。该状态机在任何一个状态,基于输入的字符,都能做一个确定的状态转换。

词法分析

语法分析就是在词法分析的基础上识别出表达式的语法结构

抽象语法树

表达式的语法结构可以用树来表示,其每个节点(子树)是一个语法单元,这个单元的构成规则就叫“语法”。每个节点还可以有下级节点。

  • 上下文无关语法 Context-Free Grammar

    • 语言句子无需考虑上下文,就可以判断正确性。可以使用巴科斯范式(BNF)来表达
  • 递归下降算法 Recursive Descent Parsing

    • 递归下降算法就是自顶向下构造语法树
    • 不断的对 Token 进行语法展开(下降),展开过程中可能遇到递归的情况。

类型检查

类型综合

根据子表达式的类型构造出父表达式的类型。例如,表达式 A+B 的类型是根据 A 和 B 的类型定义的

编译时检查 & 运行时检查

类型检查可以发生在表达式的编译阶段,即在构造语法树的阶段;也可以发生在执行时的阶段

  • 编译时:需要提前声明参数的类型,在构建语法树的过程中进行类型检查
  • 执行时:可以根据执行时的参数输入的值类型,在执行过程中进行类型检查

设计规则引擎

设计目标

  • 设计一个规则引擎,支持特定的词法、运算符、数据类型和优先级。并且支持基于以上预定义语法的规则表达式的编译和执行。

词法(合法Token)

  • 参数:由字母数字下划线组成 eg: _ab2、user_name
  • 布尔值:true、false
  • 字符串:“abcd”、‘abcd’、abcd
  • 十进制int:1234
  • 十进制float:123.5
  • 预定义运算符:+ -

运算符

  • 一元运算符:+ -
  • 二元运算符:+ - * / % > < >= <= == !=
  • 逻辑操作符:&& || !
  • 括号:()

数据类型

  • 字符串
  • 布尔值
  • 十进制int
  • 十进制float

优先级

优先级运算符
0
1&&
2! - +
3> >= < <= == !=
4+ -
5* /
6()

词法分析

expr: logOr EOF;
logOr: logOr '||' logAnd | logAnd;
logAnd: logAnd '&&' logNot | logNot;
logNot: '!' logNot | cmp;
cmp: cmp '>' add | cmd '>=' add | cmp '<' add | cmp '<=' add | cmp '==' add | cmp '!=' add | add;
add: add '+' mul | add '-' mul | mul;
mul: mul '*' pri | mul '/' pri | mul '%' pri | pri;
pri: BooleanLiteral | IntegerLiteral | FloatLiteral | Stringliteral | Identifier | '(' expr ')';

优先级的表达

type precedence struct {
    validSymbols []Symbol       // 当前优先级支持的运算符类型
    nextPrecedence *precedence  // 更高优先级的
    planner planner             // 当前优先级的处理函数
}

语法树

  • 一元运算符:左子树为空,右子树为右操作符
  • 二元运算符:左子树为左操作符,右子树为右操作符
  • 括号:左子树为空,右子树为内部表达式的AST

语法树执行与类型检查

语法树执行

预先定义好每种操作符的执行逻辑。

对抽象语法树进行后续遍历执行,即:

  • 先执行左子树,得到左节点的值。
  • 再执行右子树,得到右节点的值。
  • 最后根据根节点的操作符执行得到根节点的值。

类型检查

检查时机:执行时检查

检查方法:在一个节点的左右子节点执行完成后,分别校验左右子节点的类型是否符合对应操作符的类型检查预设规则

  • ‘>’符号要求左右子节点的值都存在且为intfloat
  • ‘!’符号要求左节点为空且右节点的值为bool

young_engine github.com/qimengxingy…