基於 go 的規則引擎

【導讀】介紹 go 語言框架下開源規則引擎

1 引入

以一個電商運維場景爲例,我們需要對用戶註冊年限 p1、購買金額 p2、地域 p3 等條件給用戶進行發券,基於條件進行任意組合成不同規則。比如:

爲了解決這個問題,引入了規則引擎,從 if …else 中解放出來。Drools 是 java 語言的規則引擎,本文是針對 go 語言的規則引擎框架。

Snip2020110249

2 Go 開源

先說結論:比較了 govaluate、goengine、gorule,最終使用 govaluate。相比 gorule、goengine,govaluate 除了支持 in 操作、還支持正則表達式,而且表達式也不需要轉換成 DRL。

who043

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)
}

轉自: heartthinkdo.com/?p=3711

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/OH2KT9XiYrzjWnk4q81BGw