介绍go语言框架下开源规则引擎

1 引入

以一个电商运维场景为例,我们需要对用户注册年限p1、购买金额p2、地域p3等条件给用户进行发券,基于条件进行任意组合成不同规则。比如:

p1 > 2 && p2 > 10 000 & p3 in (‘beijng’,’shanghai’)p1<1

为了解决这个问题,引入了规则引擎,从if …else中解放出来。Drools是java语言的规则引擎,本文是针对go语言的规则引擎框架。

2 Go开源

先说结论:比较了govaluate、goengine、gorule,最终使用govaluate。相比 gorule、goengine,govaluate除了支持in操作、还支持正则表达式,而且表达式也不需要转换成DRL。

  • 支持string类型的 ==操作
  • 支持in操作
  • 支持计算逻辑表达式和算数表达式
  • 支持正则
框架功能基准测试
1、直接使用表达式。不需要转IDL2、支持算数表达式和逻辑表达式。3、支持string的判断。4、支持in 操作。- 1 in (1,2,3)- 字符串 in 。‘code1’ in (‘cod1′,’code2’)5、支持正则测试3个逻辑表达式条件,如下:每次执行op需要15us
gengie(B站开源)GitHub - rencalo770/gengine: Rule Engine for GolangStar: 193规则表达式类似于一个DRL。规则脚本具有if..else等语法,后续支持扩展灵活。
grueGitHub - hyperjumptech/grule-rule-engine: Rule engine implementation in Golangstar:525规则表达式需要生成生成一个 DRL。
go实现执行js脚本(类似于java 执行groovy )这里不关注表达式,只是通过这引擎可以执行js脚本代码。goja、gengine、grule都是基于脚本的,只是gojar是支持js,grule和gengine是自定义的语法脚本。

2.1 govaluate

demo代码,需要生成一个map传递变量的值。如下是计算算数公式和逻辑表达式例子。

func TestGoValueate() {
   // 支持多个逻辑表达式
   expr, err := govaluate.NewEvaluableExpression("(10 > 0) && (2.1 == 2.1) && 'service is ok' == 'service is ok'" +
      " && 1 in (1,2) && 'code1' in ('code3','code2',1)")
   if err != nil {
      log.Fatal("syntax error:", err)
   }
   result, err := expr.Evaluate(nil)
   if err != nil {
      log.Fatal("evaluate error:", err)
   }
   fmt.Println(result)

   // 逻辑表达式包含变量
   expression, err := govaluate.NewEvaluableExpression("http_response_body == 'service is ok'")
   parameters := make(map[string]interface{}, 8)
   parameters["http_response_body"] = "service is ok"
   res, _ := expression.Evaluate(parameters)
   fmt.Println(res)

   // 算数表达式包含变量
   expression1, _ := govaluate.NewEvaluableExpression("requests_made * requests_succeeded / 100")
   parameters1 := make(map[string]interface{}, 8)
   parameters1["requests_made"] = 100
   parameters1["requests_succeeded"] = 80
   result1, _ := expression1.Evaluate(parameters1)
   fmt.Println(result1)

}

2、基准测试

func BenchmarkNewEvaluableExpression(b *testing.B) {
   for i := 0; i < b.N; i++ {
      _, err := govaluate.NewEvaluableExpression("(10 > 0) && (100 > 20) && 'code1' in ('code3','code2',1)")
      if err != nil {
         log.Fatal("syntax error:", err)
      }
   }
}

func BenchmarkEvaluate(b *testing.B) {
   parameters1 := make(map[string]interface{}, 8)
   parameters1["gmv"] = 100
   parameters1["customerId"] = "80"
   parameters1["stayLength"] = 20
   for i := 0; i < b.N; i++ {
      _, err := govaluate.NewEvaluableExpression("(gmv > 0) && (stayLength > 20) && customerId in ('80','code2','code3')")
      if err != nil {
         log.Fatal("syntax error:", err)
      }
   }
}

在测试go文件目录下执行:

test go test  -bench=.    -benchmem

测试结果如下,每次执行op需要15ms、9KB、需要内存分片次数140次。

goos: darwin
goarch: amd64
pkg: helloWord/test
BenchmarkNewEvaluableExpression-8          74413             15341 ns/op            8680 B/op        139 allocs/op
BenchmarkEvaluate-8

2.2 goengine代码

demo如下:

import (
   "fmt"
   "gengine/builder"
   "gengine/context"
   "gengine/engine"
   "github.com/google/martian/log"
   "time"
)

func PrintName(name string) {
   fmt.Println(name)
}

/**
use '@name',you can get rule name in rule content
*/
const atname_rule = `
rule "测试规则名称1" "rule desc"
begin
  va = @name
  PrintName(va)
  PrintName(@name)
end
rule "rule name" "rule desc"
begin
  va = @name
  PrintName(va)
  PrintName(@name)
end
`

func TestGEngine() {
   start1 := time.Now().UnixNano()
   // context data
   dataContext := context.NewDataContext()
   dataContext.Add("PrintName", PrintName)
   // init rule engine
   ruleBuilder := builder.NewRuleBuilder(dataContext)
   // resolve rules from string
   err := ruleBuilder.BuildRuleFromString(atname_rule)

   end1 := time.Now().UnixNano()

   fmt.Println("rules num:%d, load rules cost time:%d ns", len(ruleBuilder.Kc.RuleEntities), end1-start1)

   if err != nil {
      log.Errorf("err:%s ", err)
   } else {
      eng := engine.NewGengine()

      start := time.Now().UnixNano()
      // true: means when there are many rules, if one rule execute error,continue to execute rules after the occur error rule
      err := eng.Execute(ruleBuilder, true)
      end := time.Now().UnixNano()
      if err != nil {
         log.Errorf("execute rule error: %v", err)
      }
      log.Infof("execute rule cost %d ns", end-start)
   }
}

2.3 gorule

demo如下:

type RuleConditionContext struct {
   NetAmount float32
   Distance  int32
   Duration  int32
   Result    bool
}

// DRL的规则
const duplicateRulesWithDiffSalience = `
rule  DuplicateRule1  "Duplicate Rule 1"  salience 5 {
when
(RuleConditionContext.Distance > 5000  &&   RuleConditionContext.Duration > 120) && (RuleConditionContext.Result == false)
Then
   RuleConditionContext.Result=true;
}
`

// 理想的规则引擎应该是解析一个规则+contextData

// 1.目前规则是:
// (1)执行步骤
//       step1:加载所有规则
//    step2: 执行一个数据
// (2) 问题
// 问题1: 执行效率
// 问题2: 如果需要提前加载所有规则,那么就需要考虑:规则量、动态改变规则,比如规则被修改或者新增规则场景。

func TestGruleEngine() {
   //Given
   ruleCondition := &RuleConditionContext{
      Distance: 6000,
      Duration: 121,
      Result:   false,
   }

   lib := ast.NewKnowledgeLibrary()

   ruleBuiler := builder.NewRuleBuilder(lib)
   ruleBuiler.BuildRuleFromResource("rule1", "1.0", pkg.NewBytesResource([]byte(duplicateRulesWithDiffSalience)))

   kb := lib.NewKnowledgeBaseInstance("rule1", "1.0")
   eng := engine.NewGruleEngine()

   // 2.对于一个对象进行判断是否满足规则
   // 2.1 构建一个规则条件对象
   dctx := ast.NewDataContext()
   dctx.Add("RuleConditionContext", ruleCondition)
   eng.Execute(dctx, kb)

   fmt.Println(ruleCondition.Result)

}

2.4 goja

demo如下:

import (
 "fmt"
 "github.com/dop251/goja"
)

func TestGoja() {
 const SCRIPT = `
 var hasX = false;
 var hasY = false;
 for (var key in o) {
  switch (key) {
  case "x":
   if (hasX) {
    throw "Already have x";
   }
   hasX = true;
   delete o.y;
   break;
  case "y":
   if (hasY) {
    throw "Already have y";
   }
   hasY = true;
   delete o.x;
   break;
  default:
   throw "Unexpected property: " + key;
  }
 }
 
 hasX && !hasY || hasY && !hasX;
 `
 r := goja.New()
 r.Set("o", map[string]interface{}{
  "x": 40,
  "y": 2,
 })
 v, err := r.RunString(SCRIPT)
 if err != nil {
  fmt.Println(err)
 }

 fmt.Println(v)
}