規(guī)則引擎 Drools 執(zhí)行流程淺析

什么是規(guī)則引擎

規(guī)則引擎是處理復(fù)雜規(guī)則集合的引擎苛吱。通過(guò)輸入一些基礎(chǔ)事件,以推演或者歸納等方式器瘪,得到最終的執(zhí)行結(jié)果翠储。規(guī)則引擎的核心作用在于將復(fù)雜、易變的規(guī)則從系統(tǒng)中抽離出來(lái)橡疼,由靈活可變的規(guī)則來(lái)描述業(yè)務(wù)需求

Drools 簡(jiǎn)介

Drools 是 Java 編寫(xiě)的一款開(kāi)源規(guī)則引擎援所。Drools 的核心算法基于 Rete。早些版本中衰齐,Drools 使用的是基于 Rete 二次開(kāi)發(fā)的 ReteOO 算法任斋。在 7.x 版本的 Drools 中,其內(nèi)部算法已經(jīng)改為使用 Phreak。Phreak 也是Drools 團(tuán)隊(duì)自研的算法废酷,雖然網(wǎng)上關(guān)于該算法的資料很少瘟檩,但是總體來(lái)說(shuō)與 Rete 算法相似。閱讀本文之前可以先了解下 Rete 算法

編寫(xiě)一個(gè)簡(jiǎn)單的規(guī)則

使用 Drools 需要我們將原有的代碼抽象成:Rule(規(guī)則) + Fact(事實(shí))

首先我們先來(lái)編寫(xiě)一個(gè)簡(jiǎn)單的 demo 用于后文的原理學(xué)習(xí)

  1. 引入 pom 依賴
    <properties>
        <drools.version>7.62.0.Final</drools.version>
    </properties>
...
        <dependency>
            <groupId>org.drools</groupId>
            <artifactId>drools-compiler</artifactId>
            <version>${drools.version}</version>
        </dependency>

        <dependency>
            <groupId>org.drools</groupId>
            <artifactId>drools-mvel</artifactId>
            <version>${drools.version}</version>
        </dependency>
  1. resource 目錄下新建 order.drl
// 包名用于邏輯上區(qū)分 rule
package com.example.drools.order;

import com.example.drools.demo.HelloDrools.Order
import com.example.drools.demo.HelloDrools.User
import java.util.ArrayList;

global java.util.List list

// 指定方言為 java
dialect  "java"

// 規(guī)則的組成包括澈蟆,條件(when 部分)和動(dòng)作(then 部分)
// 當(dāng)滿足 when 時(shí)墨辛,會(huì)執(zhí)行 then 的邏輯
rule "order can pay"
    when
        // 要求插入的 fact 必須有一個(gè) User 對(duì)象
        // 并且 Order fact 必須滿足 price < $user.price
        $user: User()
        $order: Order(price < $user.price)
    then
        System.out.println("username:" + $user.getName() + ", order price:" + $order.getPrice());
end

rule "calculate member point"
    when
        $user: User(level > 0)
        $order: Order()
    then
        Double point = $user.getPoint();
        if ($user.getLevel() > 10) {
            $user.setPoint(point + $order.getPrice());
        } else {
            $user.setPoint(point + $order.getPrice() * 0.5);
        }
        System.out.println("previous point:" + point + ", present point:" + $user.getPoint());
end

rule "user age > 18"
    when
        $user: User(age > 18)
    then
        System.out.println("user age > 18");
end

resource 下新建 META-INF\kmodule.xml

<?xml version="1.0" encoding="UTF-8"?>
<kmodule xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns="http://www.drools.org/xsd/kmodule">
</kmodule>
  1. java 代碼部分
package com.example.drools.demo;

import lombok.Data;
import org.kie.api.KieServices;
import org.kie.api.runtime.KieContainer;
import org.kie.api.runtime.KieSession;


/**
 * @author tianwen.yin
 */
public class HelloDrools {

    public static void main(String[] args) {
        // 初始化
        KieServices kieServices = KieServices.Factory.get();
        KieContainer kieContainer = kieServices.newKieClasspathContainer();
        KieSession kieSession = kieContainer.newKieSession();
        // 構(gòu)建 fact
        User user = new User();
        user.setName("taven");
        user.setPoint(10D);
        user.setLevel(5);
        user.setPrice(100D);
        user.setAge(19);

        Order order = new Order();
        order.setPrice(58D);
        // insert fact
        kieSession.insert(user);
        kieSession.insert(order);
        // 觸發(fā)所有規(guī)則
        int fireCount = kieSession.fireAllRules();
        System.out.println("fireRuleCount:" + fireCount);
        kieSession.dispose();
    }

    @Data
    public static class Order {
        private Double price;
    }

    @Data
    public static class User {
        private String name;
        private Integer age;
        private Double price;
        private Integer level;
        private Double point;
    }

}
  1. 執(zhí)行結(jié)果如下
username:taven, order price:58.0
previous point:10.0, present point:39.0
user age > 18
fireRuleCount:3

Drools 執(zhí)行流程淺析

Drools 的使用看起來(lái)還是比較簡(jiǎn)單的,但是實(shí)際上真正落地使用還是需要詳讀官方文檔的趴俘,不是本文重點(diǎn)睹簇,就不多贅述了。接下來(lái)我們進(jìn)入正題寥闪,分析下執(zhí)行流程

上述的圖太惠,是我結(jié)合源碼總結(jié)的 Drools 執(zhí)行流程圖,最終目的就是根據(jù)插入的 fact 進(jìn)行推演疲憋,如果能走到最后的 Terminal 節(jié)點(diǎn)則代表規(guī)則會(huì)被執(zhí)行

我們先來(lái)了解一下上圖中的所有節(jié)點(diǎn)

  • Object Type Node:簡(jiǎn)稱 OTN凿渊,fact 會(huì)根據(jù)類型流轉(zhuǎn)到不同的 OTN

  • Alpha Node:也被稱為單輸入節(jié)點(diǎn),所有單對(duì)象的約束條件都會(huì)被構(gòu)建為 Alpha 節(jié)點(diǎn)缚柳,例如 "age > 18"埃脏,"leve > 0"

  • Beta Node:雙輸入節(jié)點(diǎn),不同對(duì)象之間的約束會(huì)被構(gòu)建為 Beta 節(jié)點(diǎn)秋忙,例如 "order.price > user.price"彩掐;當(dāng)一個(gè)節(jié)點(diǎn)需要同時(shí)滿足多個(gè)單對(duì)象約束時(shí)也是 Beta 節(jié)點(diǎn);一個(gè)節(jié)點(diǎn)有超過(guò)兩個(gè)條件約束時(shí)灰追,會(huì)構(gòu)建為多個(gè) Beta 節(jié)點(diǎn)相連

    Beta 節(jié)點(diǎn)又分為 Join堵幽,Not,Exist 等弹澎,本文主要以 Join 節(jié)點(diǎn)為例進(jìn)行說(shuō)明谐檀。對(duì)于其他節(jié)點(diǎn)來(lái)說(shuō)流程也是一樣的,只不過(guò)某些具體細(xì)節(jié)的實(shí)現(xiàn)不同

    補(bǔ)充一張多 Beta 節(jié)點(diǎn)相連的圖


  • LeftInputAdapterNode:左輸入節(jié)點(diǎn)裁奇,這個(gè)節(jié)點(diǎn)的作用我最開(kāi)始也很迷惑。后來(lái)在反復(fù) Debug 后終于頓悟了麦撵,Beta 節(jié)點(diǎn)被設(shè)計(jì)成只存儲(chǔ)右側(cè)進(jìn)入的 fact刽肠,左側(cè)的數(shù)據(jù)來(lái)自 LeftInputAdapterNode 或者另一個(gè) Beta 節(jié)點(diǎn)(可能理解不了,請(qǐng)繼續(xù)往下讀)

  • Rule Terminal:當(dāng)一個(gè) fact 流轉(zhuǎn)到 Terminal 時(shí)免胃,代表當(dāng)前 fact 會(huì)觸發(fā)該規(guī)則

  • 內(nèi)存結(jié)構(gòu):關(guān)于 Drools 內(nèi)存結(jié)構(gòu)這塊音五,與傳統(tǒng) RETE 算法不太一樣,我也沒(méi)有太仔細(xì)的研究這塊羔沙,上圖中只是把會(huì)存儲(chǔ) fact 的位置標(biāo)識(shí)了出來(lái)

實(shí)際上 Drools 的源碼非常復(fù)雜躺涝,其中包含的節(jié)點(diǎn)遠(yuǎn)不止提到的這些,我這里僅是基于 RETE 算法的核心內(nèi)容來(lái)刨析下 Drools 原理

注:這里我補(bǔ)充下扼雏,Beta 節(jié)點(diǎn)的右側(cè)分支坚嗜,在進(jìn)入到 Beta 之前夯膀,也是可以有 Alpha 節(jié)點(diǎn)的。并且當(dāng)多個(gè) rule 中包含相同條件時(shí)也會(huì)共用分支苍蔬。改圖和編 demo 實(shí)在太麻煩了

準(zhǔn)備環(huán)節(jié)

  1. 在解析規(guī)則文件時(shí)诱建,應(yīng)該就已經(jīng)創(chuàng)建了類似上圖的節(jié)點(diǎn)關(guān)系(這個(gè)具體源碼沒(méi)有閱讀)

  2. 上述示例中,kieSession.insert(user); 會(huì)將 fact 插入到 PropagationList

  3. 調(diào)用 kieSession.fireAllRules(); 后碟绑,進(jìn)入到規(guī)則引擎核心環(huán)節(jié)

fireAllRules

字面意思已經(jīng)很明顯了俺猿,觸發(fā) Session 中的所有規(guī)則

flush 階段

傳播 PropagationList 中所有 fact,對(duì)照上圖中 flush格仲,OTN 下游的所有分支都會(huì)遍歷訪問(wèn)

  1. 如果某條分支全部都是 Alpha 節(jié)點(diǎn)的話押袍,可以直接傳播到 Terminal 節(jié)點(diǎn)
  2. 如果 fact 流轉(zhuǎn)到 LeftInputAdapterNode 的話,會(huì)將 fact 存儲(chǔ)在 LeftInputAdapterNode 對(duì)應(yīng)的內(nèi)存中
  3. 如果 fact 流轉(zhuǎn)到 Beta 節(jié)點(diǎn)右側(cè)的話凯肋,會(huì)將 fact 存儲(chǔ)在 Beta 節(jié)點(diǎn)的右側(cè)

當(dāng)分支走到 Alpha Terminal 節(jié)點(diǎn)時(shí)谊惭,構(gòu)建一個(gè) RuleAgendaItem 插入到 InternalAgendaGroup 中。這個(gè)動(dòng)作代表當(dāng)前規(guī)則需要進(jìn)行下一個(gè)階段 evaluateAndFire

Beta 節(jié)點(diǎn)的邏輯是否过,當(dāng)所有的分支入口都存儲(chǔ)了數(shù)據(jù)時(shí)午笛,插入 InternalAgendaGroup(這句話可能不太好理解,當(dāng)僅有一個(gè) Beta 節(jié)點(diǎn)時(shí)苗桂,左右都存儲(chǔ)了數(shù)據(jù)药磺,就會(huì)插入 InternalAgendaGroup。如果是多個(gè) Beta 節(jié)點(diǎn)相連的話煤伟,必須要滿足第一個(gè) Beta 的左右癌佩,以及下游所有 Beta 的右節(jié)點(diǎn)都有數(shù)據(jù)時(shí)才會(huì)插入 InternalAgendaGroup)。

evaluate(評(píng)估)

純 Alpha 節(jié)點(diǎn)的分支便锨,是沒(méi)有這個(gè)步驟的

以 Beta 節(jié)點(diǎn)為例围辙,evaluate 就是基于左右內(nèi)存進(jìn)行匹配,找到所有配對(duì)成功的數(shù)據(jù)放入一個(gè)集合放案,將這個(gè)集合繼續(xù)帶入到下一個(gè)節(jié)點(diǎn)姚建,下個(gè)節(jié)點(diǎn)又可能是 Beta 節(jié)點(diǎn)或者 Terminal 節(jié)點(diǎn)。

  • 如果是 Beta 節(jié)點(diǎn)的話吱殉,則繼續(xù)進(jìn)行匹配掸冤,配對(duì)成功的集合帶入到下一個(gè)節(jié)點(diǎn)...
  • 如果是 Terminal 節(jié)點(diǎn)的話,會(huì)將數(shù)據(jù)插入到 RuleExecutor 的 tupleList 中友雳。這步又是啥意思呢稿湿,tupleList 的數(shù)據(jù)代表,這些數(shù)據(jù)會(huì)真正的代入到規(guī)則執(zhí)行當(dāng)中去(Alpha Terminal 也會(huì)執(zhí)行這個(gè)操作)

Beta 節(jié)點(diǎn)這里還有一個(gè)細(xì)節(jié)押赊,就是在進(jìn)行左右配對(duì)的時(shí)候误澳,并不只是遍歷查找礼殊,而是在條件允許的情況下斥杜,Drools 在存儲(chǔ)這些數(shù)據(jù)的時(shí)候會(huì)建立索引。上述示例的話罗丰,并沒(méi)有建立索引,隨便把條件改成 xx.a = yy.b 這種條件的話咽袜,就會(huì)建立索引丸卷。具體索引的實(shí)現(xiàn)也很簡(jiǎn)單,Drools 實(shí)現(xiàn)了一個(gè)類似 HashMap 的結(jié)構(gòu)來(lái)管理索引询刹,感興趣的同學(xué)可以自己打個(gè)斷點(diǎn) debug 下谜嫉。

斷點(diǎn) Class:PhreakJoinNode

注:上圖中兩個(gè)位置,只有一處會(huì)被執(zhí)行

fire

這里會(huì)遍歷 RuleExecutor 的 tupleList 執(zhí)行這些規(guī)則凹联。我們的規(guī)則文件在 Drools 運(yùn)行時(shí)會(huì)被編譯成字節(jié)碼動(dòng)態(tài)執(zhí)行沐兰,具體這塊具體用啥實(shí)現(xiàn)的沒(méi)研究。

fire 階段還有一個(gè)細(xì)節(jié)就是蔽挠,我們的規(guī)則文件內(nèi)部是可以調(diào)用 insert modify 這些函數(shù)的住闯,這些 fact 同樣會(huì)被插入到 PropagationList 中,內(nèi)部也會(huì)再執(zhí)行一次 PropagationList flush 操作澳淑。整個(gè) fireAllRules 方法內(nèi)部是一個(gè)循環(huán)比原,如果 fire 內(nèi)部的 fact 命中了規(guī)則的話,在 fire 結(jié)束后還會(huì)繼續(xù)執(zhí)行 evaluateAndFire 直到全部觸發(fā)完為止(所以在規(guī)則編寫(xiě)錯(cuò)誤的情況下杠巡,Drools 可能進(jìn)入死循環(huán))

Conflict resolution

沖突解決簡(jiǎn)單來(lái)說(shuō)就是量窘,當(dāng)我們知道了要執(zhí)行的規(guī)則都有哪些時(shí),還需要明確這些規(guī)則執(zhí)行的順序氢拥。

Drools 這里如何解決順序問(wèn)題的呢蚌铜?回顧一下上面提到的 flush 階段。RuleAgendaItem 插入到 InternalAgendaGroup 中這一步嫩海,InternalAgendaGroup 的默認(rèn)實(shí)現(xiàn)為 AgendaGroupQueueImpl冬殃,AgendaGroupQueueImpl 中使用了 BinaryHeapQueue(二叉堆隊(duì)列)來(lái)存儲(chǔ)元素

通過(guò)二叉堆算法保證每次隊(duì)列彈出優(yōu)先級(jí)最高的規(guī)則,優(yōu)先級(jí)的計(jì)算通過(guò) PhreakConflictResolver 來(lái)完成

PhreakConflictResolver 從兩個(gè)方面來(lái)判斷優(yōu)先級(jí)

  1. 規(guī)則是否聲明 salience(salience 越大叁怪,優(yōu)先級(jí)越高)
  2. 無(wú)法通過(guò) salience 來(lái)計(jì)算的話审葬,則通過(guò)規(guī)則 loadOrder 來(lái)決定優(yōu)先級(jí)(規(guī)則在文件中越靠前則 loadOrder 就越高)

總結(jié)下

Drools 這種算法邏輯有什么好處呢?下述結(jié)論參考了 https://en.wikipedia.org/wiki/Rete_algorithm

  1. 通過(guò)共享節(jié)點(diǎn)來(lái)減少節(jié)點(diǎn)的冗余(如果多個(gè) Rule 中有相同的條件奕谭,不會(huì)重復(fù)計(jì)算)

  2. fact 的變化耳璧,不需要完全重新評(píng)估,只需要進(jìn)行增量評(píng)估(只需要對(duì) fact 對(duì)應(yīng)的 OTN 重新評(píng)估就可以)

  3. 支持高效的刪除 fact (從 Drools 的角度來(lái)看這句話展箱,fact 存儲(chǔ)時(shí)會(huì)建立一個(gè)雙向關(guān)聯(lián),也就是 fact 知道自己被哪些節(jié)點(diǎn)存儲(chǔ)了蹬昌,所以可以高效的刪除)

本文介紹了 Drools 的執(zhí)行流程混驰,由于網(wǎng)上沒(méi)有找到太多參考資料,大多數(shù)結(jié)論都是我自己總結(jié)出來(lái)的,如果有寫(xiě)錯(cuò)的地方歡迎各位指正栖榨。

最后

如果覺(jué)得我的文章對(duì)你有幫助昆汹,歡迎點(diǎn)贊,關(guān)注婴栽,轉(zhuǎn)發(fā)满粗。你的支持是對(duì)我最大的幫助

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市愚争,隨后出現(xiàn)的幾起案子映皆,更是在濱河造成了極大的恐慌,老刑警劉巖轰枝,帶你破解...
    沈念sama閱讀 217,542評(píng)論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件捅彻,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡鞍陨,警方通過(guò)查閱死者的電腦和手機(jī)步淹,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,822評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)诚撵,“玉大人缭裆,你說(shuō)我怎么就攤上這事∈傺蹋” “怎么了澈驼?”我有些...
    開(kāi)封第一講書(shū)人閱讀 163,912評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)韧衣。 經(jīng)常有香客問(wèn)我盅藻,道長(zhǎng),這世上最難降的妖魔是什么畅铭? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,449評(píng)論 1 293
  • 正文 為了忘掉前任氏淑,我火速辦了婚禮,結(jié)果婚禮上硕噩,老公的妹妹穿的比我還像新娘假残。我一直安慰自己,他們只是感情好炉擅,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,500評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布辉懒。 她就那樣靜靜地躺著,像睡著了一般谍失。 火紅的嫁衣襯著肌膚如雪眶俩。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,370評(píng)論 1 302
  • 那天快鱼,我揣著相機(jī)與錄音颠印,去河邊找鬼纲岭。 笑死,一個(gè)胖子當(dāng)著我的面吹牛线罕,可吹牛的內(nèi)容都是我干的止潮。 我是一名探鬼主播,決...
    沈念sama閱讀 40,193評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼钞楼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼喇闸!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起询件,我...
    開(kāi)封第一講書(shū)人閱讀 39,074評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤燃乍,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后雳殊,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體橘沥,經(jīng)...
    沈念sama閱讀 45,505評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,722評(píng)論 3 335
  • 正文 我和宋清朗相戀三年夯秃,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了座咆。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,841評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡仓洼,死狀恐怖介陶,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情色建,我是刑警寧澤哺呜,帶...
    沈念sama閱讀 35,569評(píng)論 5 345
  • 正文 年R本政府宣布,位于F島的核電站箕戳,受9級(jí)特大地震影響某残,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜陵吸,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,168評(píng)論 3 328
  • 文/蒙蒙 一玻墅、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧壮虫,春花似錦澳厢、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,783評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至饶唤,卻和暖如春徐伐,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背募狂。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,918評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工办素, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留魏保,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,962評(píng)論 2 370
  • 正文 我出身青樓摸屠,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親粱哼。 傳聞我的和親對(duì)象是個(gè)殘疾皇子季二,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,781評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容