写点什么

Drools 规则引擎探究以及在 IOT 的应用

  • 2020-09-05
  • 本文字数:8799 字

    阅读完需:约 29 分钟

Drools规则引擎探究以及在IOT的应用

前言:我上家公司是做物联网的,任职在 IOT 部门,业务上针对不同类型的燃气表,水表,报警器等有不同协议,其中又包含不同厂家的表和自己公司的表。针对不同的协议,如何解析不同的协议头帧,根据头帧进行不同的复杂业务处理,后来引入了 Drools 规则引擎,通过规则的逻辑和数据的分离以及可扩展解决问题。

目前所处为淘系技术部负责天猫奢品的业务,业务背景如下:业务包含天猫奢品频道,奢品折扣频道,天猫奢品官方直营旗舰店,魅力惠旗舰店,魅力惠 APP 等。基于业务场景下会员分为店铺会员,APP 会员,天猫奢品行业会员等,而业务需要进行会员精细化的运营,通过不同的会员等级享受不同的权益,而相同的等级还需要做到根据偏好做到千人多权,如何根据复杂的业务需求变化更加精准的进行匹配,考虑 Drools 规则的逻辑数据的分离和可扩展性,接下来也会在天猫奢品的相关的会员模块中和组内成员探讨是否适合引入。下面是一些基于业务场景的总结和分享。

引入

问题引入

天猫奢品业务方为了吸引更多的新客,和提高会员的活跃度,做了一期活动,通过购买天猫奢品频道内的任意商品就赠送特殊积分,积分可以直接兑换限量的奢品商品。假如业务方给的规则如下:



主刃同学一看,这需求果然简单呀,作为团队的核心开发,这不就是小 case,5 分钟搞定,送赠品的简单代码如下:


public void exchangeGift(Integer points) {        if (points < 100) {            System.out.println("无商品");        } else if (points > 100 && points <= 200) {            System.out.println("Dior限量口红");        } else if (points > 200 && points <= 300) {            System.out.println("TF限量口红");        } else if (points > 300 && points <= 400) {            System.out.println("SK-II套装");        } else if (points > 400 && points <= 500) {            System.out.println("MCM双肩包");        } else if (points > 500) {            System.out.println("RADO雷达限量表");        }    }
复制代码


活动进行了一天,运营发现 Dior 限量版口红被兑换的很多,即将超出预期,业务方根据实际情况想改变规则,规则要求在各个层级上各加 200 分从而促进更多的消费。



主刃一看改动不大,于是五分钟后修改完代码并经测试后发布上线。活动又进行了一天,运营人员通过后台监控发现提到 200 分后,用户达成比较少,想再现有基础上再降 100 分。没办法还的改不是,主刃同学这次把配置数据放入到了阿里集团的配置中心 diamond 中,当运营改策略的,只要改一下 Diamond 的值就可以了。


核心代码编程了这样:


public void exchangeGift(Integer points) {        if (points < CommDiamondProperty.getInstance().getLevelOne()) {            System.out.println("无商品");        } else if (points > CommDiamondProperty.getInstance().getLevelOne() && points <= CommDiamondProperty.getInstance().getLevelTwo()) {            System.out.println("Dior限量口红");        } else if (points > CommDiamondProperty.getInstance().getLevelTwo() && points <= CommDiamondProperty.getInstance().getLevelThree()) {            System.out.println("TF限量口红");        } else if (points > CommDiamondProperty.getInstance().getLevelThree() && points <= CommDiamondProperty.getInstance().getLevelFour()) {            System.out.println("SK-II套装");        } else if (points > CommDiamondProperty.getInstance().getLevelFour() && points <= CommDiamondProperty.getInstance().getLevelFive()) {            System.out.println("MCM双肩包");        } else if (points > CommDiamondProperty.getInstance().getLevelFive()) {            System.out.println("RADO雷达限量表");        }    }
复制代码


新功能上线后,运营侧提出这次的分层太少了,BD 到了更多的好的赠品,需要更加精细化的运营,由以前的 5 组变成 10 组,主刃此刻的心情:kao …


那么是否有什么技术可以将活动规则和代码解耦,不管规则如何变化,执行端不用动。那就是规则引擎,那么规则引擎到底是什么东西呢?我们来看看。

规则引擎

相关介绍

目前对领域模型的常见抽象方式是面向对象思想,即简单的来说把业务逻辑抽象为对象、对象的属性和对象的方法。这样规则跟整个系统耦合,修改规则需要走全 Case 的开发测试流程发布流程,对于频繁修改的规则效率比较低,为了解决这个问题就出现了规则引擎。


今天分享的 Drools,最早由 Jboss 开发,目前由 Redhat 开源的规则引擎,选择分享 Drools 的原因是它是 Redhat 的 KIE Group 中的组件之一,可以比较方便的跟另一个组件 JBPM 工作流配合用于管理复杂的规则流;同时 Drools 的推理策略算法在经典 Rete 算法以及其它算法的基础上做了多个版本的增强,目前支持结合正向推理和反向推理优点的混合推理。


规则引擎主要完成的就是将业务规则从代码中分离出来。 在规则引擎中,利用规则语言将规则定义为 if-then 的形式,if 中定义了规则的条件,then 中定义了规则的结果。规则引擎会基于数据对这些规则进行计算,找出匹配的规则。这样,当规则需要修改时,无需进行代码级的修改,只需要修改对应的规则,可以有效减少代码的开发量和维护量。


Java 开源的规则引擎有:Drools、Easy Rules、Mandarax、IBM ILOG。使用最为广泛并且开源的是 Drools。

规则引擎优点

Drools 是用 Java 语言编写的开放源码规则引擎,使用 Rete 算法对所编写的规则求值。Drools 允许使用声明方式表达业务逻辑。可以使用非 XML 的本地语言编写规则,从而便于学习和理解。并且,还可以将 Java 代码直接嵌入到规则文件中,这令 Drools 的学习更加吸引人。


  • 声明式编程

  • 使用规则的核心优势在于可以简化对于复杂问题的逻辑表述,并对这些逻辑进行验证(规则比编码具有更好的可阅读性)。规则机制可以解决很复杂的问题,提供一个如何解决问题的说明,并说明每个决策的是如何得出的。

  • 逻辑和数据分离

  • 将业务逻辑都放在规则里的好处是业务逻辑发生变化时,可以更加方便的进行维护。尤其是这个业务逻辑是一个跨域关联多个域的逻辑时。不像原先那样将业务逻辑分散在多个对象或控制器中,业务逻辑可以被组织在一个或多个清晰定义的规则文件中。

  • 数据位于“域对象”中,业务逻辑位于“规则”中。根据项目的种类,这种分离是非常有利的。

  • 速度和可扩展性

  • 网络算法(Rete algorithm),跳跃算法(Leaps algorithm),以及有它们派生出的 Drools 的 Reteoo 算法(以及跳跃算法),提供了非常高效的方式根据业务对象的数据匹配规则。

  • Drools 的 Rete OO 算法已经是一个成熟的算法。在 Drools 的帮助下,您的应用程序变得非常可扩展。如果频繁更改请求,可以添加新规则,而无需修改现有规则。

  • 知识集中化

  • 通过使用规则,您创建一个可执行的知识库(知识库)。这是商业政策的一个真理点。理想情况下,规则是可读的,它们也可以用作文档。

规则引擎缺点

  • 复杂性提高

  • 需要学习新的规则语法

  • 引入新组件的风险

规则引擎的实现

Drools 规则引擎的结构示意图:



Rete 算法

Rete 算法最初是由卡内基梅隆大学的 Charles L.Forgy 博士在 1974 年发表的论文中所阐述的算法 , 该算法提供了专家系统的一个高效实现。自 Rete 算法提出以后 , 它就被用到一些大型的规则系统中 , 像 ILog、Jess、JBoss Rules 等都是基于 RETE 算法的规则引擎。


Rete 在拉丁语中译为”net”,即网络。Rete 匹配算法是一种进行大量模式集合和大量对象集合间比较的高效方法,通过网络筛选的方法找出所有匹配各个模式的对象和规则。


其核心思想是将分离的匹配项根据内容动态构造匹配树,以达到显著降低计算量的效果。 Rete 算法可以被分为两个部分:规则编译和规则执行。 当 Rete 算法进行事实的断言时,包含三个阶段: 匹配选择执行 ,称做 match-select-act cycle。


Reta 网络节点图如下所示:



fact: 对象之间及对象属性之间的关系,例如 Product 类及其类目下的 points、gift 属性等 Root。


Node: 根节点,Rete 网络入口。


Type Node: 对应不同的 fact 对象,也就是规则用到的 POJO,每个 fact 就是一个 TypeNode 节点 Alpha Node :对应规则里的每个条件,例如规则条件 Product(points < 100)中,points<100 就是一个 AlphaNode


Beta Node: 用于组合两个 fact 的 alpha 条件或 BetaNode 与 fact 条件的组合。


LeftInputAdapter Node: 用来对 2 个规则队形进行比较,将一个 single Object 转化为一个单对象数组(因为 BetaNode 左边入口往往是一个 list 规则队形),传播到 JoinNode 节点。


Join Node : 用于聚合 BetaNode 节点的结果。


Drools 中的 Rete 算法被称为 ReteOO,表示 Drools 为面向对象系统(Object Oriented systems)增强并优化了 Rete 算法。在 Drools 中,规则被存 放在 Production Memory(规则库)中,推理机要匹配的 facts(事实)被存在 Working Memory(工作内存)中。当时事实被插入到工作内存中后,规则引擎会把事实和规则库里的模式进行匹配,对于匹配成功的规则再由 Agenda 负责具体执行推理算法中被激发规则的结论部分,同时 Agenda 通过冲突决策策略管理这些冲突规则的执行顺序,Drools 中规则冲突决策策略有:(1) 优先级策略 (2) 复杂度优先策略 (3) 简单性优先策略 (4) 广度策略 (5) 深度策略 (6) 装载序号策略 (7) 随机策略 。

Drools 规则引擎

Drools 介绍

官网:http://www.drools.org/


官方文档:http://www.drools.org/learn/documentation.html


Drools 是一个基于 Charles Forgy’s 的 RETE 算法的,易于访问企业策略、易于调整以及易于管理的开源业务规则引擎,符合业内标准,速度快、效率高。业务分析师人员或审核人员可以利用它轻松查看业务规则,从而检验是否已编码的规则执行了所需的业务规则。


Drools 是用 Java 语言编写的开放源码规则引擎,使用 Rete 算法对所编写的规则求值。Drools 允许使用声明方式表达业务逻辑。可以使用非 XML 的本地语言编写规则,从而便于学习和理解。并且,还可以将 Java 代码直接嵌入到规则文件中,这令 Drools 的学习更加吸引人。

Drools 相关概念

  • 事实(Fact): 对象之间及对象属性之间的关系

  • 规则(rule): 是由条件和结论构成的推理语句,一般表示为 if…Then。一个规则的 if 部分称为 LHS,then 部分称为 RHS。

  • 模式(module): 就是指 IF 语句的条件。这里 IF 条件可能是有几个更小的条件组成的大条件。模式就是指的不能在继续分割下去的最小的原子条件。


Drools 通过 事实、规则和模式相互组合来完成工作,drools 在开源规则引擎中使用率最广,但是在国内企业使用偏少,保险、支付行业使用稍多。

解决问题

Drools 有专门的规则语法 drl,就是专门描述活动的规则是如何执行的,按照主刃的需求规则如下:


rule.drl 文件内容如下:


package com.alibaba.rules
import com.alibaba.Order
rule "zero" no-loop true lock-on-active true salience 1 when $s : Order(point <= 300) then System.out.println("无赠品"); doSth($s);end
rule "giftOne" no-loop true lock-on-active true salience 1 when $s : Order(amout > 300 && amout <= 500) then System.out.println("Dior限量口红"); doSth($s);end
rule "giftTwo" no-loop true lock-on-active true salience 1 when $s : Order(amout > 500 && amout <= 700) then System.out.println("TF限量口红"); doSth($s);end
rule "giftThree" no-loop true lock-on-active true salience 1 when $s : Order(amout > 700 && amout <= 900) then System.out.println("SK-II套装"); doSth($s);end
rule "giftFour" no-loop true lock-on-active true salience 1 when $s : Order(amout > 900 && amout <= 1100) then System.out.println("MCM双肩包"); doSth($s);end
rule "giftFive" no-loop true lock-on-active true salience 1 when $s : Order(amout > 1100) then System.out.println("RADO雷达限量表"); doSth($s);end
复制代码


说明


  • package 与 Java 语言类似,drl 的头部需要有 package 和 import 的声明,package 不必和物理路径一致。

  • import 导出 java Bean 的完整路径,也可以将 Java 静态方法导入调用。

  • rule 规则名称,需要保持唯一 件,可以无限次执行。

  • no-loop 定义当前的规则是否不允许多次循环执行,默认是 false,也就是当前的规则只要满足条件,可以无限次执行。

  • lock-on-active 将 lock-on-active 属性的值设置为 true,可避免因某些 Fact 对象被修改而使已经执行过的规则再次被激活执行。

  • salience 用来设置规则执行的优先级,salience 属性的值是一个数字,数字越大执行优先级越高, 同时它的值可以是一个负数。默认情况下,规则的 salience 默认值为 0。如果不设置规则的 salience 属性,那么执行顺序是随机的。

  • when 条件语句,就是当到达什么条件的时候

  • then 根据条件的结果,来执行什么动作

  • end 规则结束


这个规则文件就是描述了,当符合什么条件的时候,应该去做什么事情,每当规则有变动的时候,我们只需要修改规则文件,然后重新加载即可生效。


这里需要有一个配置文件告诉代码规则文件 drl 在哪里,在 drools 中这个文件就是 kmodule.xml,放置到 resources/META-INF 目录下。


kmodule.xml 内容如下:


<kmodule xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"        xmlns="http://www.drools.org/xsd/kmodule">  <configuration>    <property key="drools.evaluator.supersetOf" value="org.mycompany.SupersetOfEvaluatorDefinition"/>  </configuration>  <kbase name="KBase1" default="true" eventProcessingMode="cloud" equalsBehavior="equality" declarativeAgenda="enabled" packages="org.domain.pkg1">    <ksession name="KSession2_1" type="stateful" default="true"/>    <ksession name="KSession2_2" type="stateless" default="false" beliefSystem="jtms"/>  </kbase>  <kbase name="KBase2" default="false" eventProcessingMode="stream" equalsBehavior="equality" declarativeAgenda="enabled" packages="org.domain.pkg2, org.domain.pkg3" includes="KBase1">    <ksession name="KSession3_1" type="stateful" default="false" clockType="realtime">      <fileLogger file="drools.log" threaded="true" interval="10"/>      <workItemHandlers>        <workItemHandler name="name" type="org.domain.WorkItemHandler"/>      </workItemHandlers>      <calendars>        <calendar name="monday" type="org.domain.Monday"/>      </calendars>      <listeners>        <ruleRuntimeEventListener type="org.domain.RuleRuntimeListener"/>        <agendaEventListener type="org.domain.FirstAgendaListener"/>        <agendaEventListener type="org.domain.SecondAgendaListener"/>        <processEventListener type="org.domain.ProcessListener"/>      </listeners>    </ksession>  </kbase></kmodule>
复制代码


说明


  • Kmodule 中可以包含一个到多个 kbase,分别对应 drl 的规则文件。

  • Kbase 需要一个唯一的 name,可以取任意字符串。

  • packages 为 drl 文件所在 resource 目录下的路径。注意区分 drl 文件中的 package 与此处的 package 不一定相同。多个包用逗号分隔。默认情况下会扫描 resources 目录下所有(包含子目录)规则文件。

  • kbase 的 default 属性,标示当前 KieBase 是不是默认的,如果是默认的则不用名称就可以查找到该 KieBase,但每个 module 最多只能有一个默认 KieBase。

  • kbase 下面可以有一个或多个 ksession,ksession 的 name 属性必须设置,且必须唯一。


rules.drl 中下方相关规则内容既可,如果活动规则动态的添加、减少也可以相应的去增加、减少规则文件既可,再也不用去动代码了。


rule "xxx"    no-loop true    lock-on-active true    salience 1    when        $s : Order(amout > yy)    then        System.out.println("xxxx");        doSth();end  
复制代码

规则中的条件操作符

Drools 提供了十二中类型比较操作符:< 、<=、>、>=、==、!=、contains、not contains、memberOf、not memberOf、matches、not matches,并且这些条件都可以组合使用。


  • 条件组合: 各种操作符可以组合使用

  • Fact 对象私有属性的操作: RHS 中对 Fact 对象 private 属性的操作必须使用 getter 和 setter 方法,而 RHS 中则必须要直接用.的方法去使用.

  • contains: 对比是否包含操作,操作的被包含目标可以是一个复杂对象也可以是一个简单的值。

  • matches: 正则表达式匹配,与 java 不同的是,不用考虑’/'的转义问题

  • memberOf: 判断某个 Fact 属性值是否在某个集合中,与 contains 不同的是他被比较的对象是一个集合,而 contains 被比较的对象是单个值或者对象。


注意:如果条件全部是【&&】可使用【,】来替代,但是两者不能混用。

规则中的结果部分

  • insert: 往当前 workingMemory 中插入一个新的 Fact 对象,会触发规则的再次执行,除非使用 no-loop 限定;

  • update: 更新

  • modify: 修改,与 update 语法不同,结果都是更新操作

  • retract: 删除

  • function: 定义一个方法,如:


function void console { System.out.println(); StringUtils.getId();// 调用外部静态方法,StringUtils 必须使用 import 导入,getId()必须是静态方法}


  • declare:可以在规则文件中定义一个 class,使用起来跟普通 java 对象相似,你可以在 RHS 部分中 new 一个并且使用 getter 和 setter 方法去操作其属性。


declare Address @author(dyzmj) // 元数据,仅用于描述信息 @createTime(2019-07-08) city : String @maxLengh(100) postno : intend


'@'是元数据定义,用于描述数据的数据~,没什么执行含义。


你可以在 RHS 部分中使用 Address address = new Address()的方法来定义一个对象。


更多语法规则可参考:Drools 规则引擎语法

Maven 中使用 Drools 的

工程结构目录

依赖文件 pom.xml

<!--规则引擎drools--><dependency>    <groupId>org.drools</groupId>    <artifactId>drools-core</artifactId>    <version>7.0.0.Final</version></dependency><dependency>    <groupId>org.drools</groupId>    <artifactId>drools-compiler</artifactId>    <version>7.0.0.Final</version></dependency><dependency>    <groupId>org.drools</groupId>    <artifactId>drools-decisiontables</artifactId>    <version>7.0.0.Final</version></dependency><dependency>    <groupId>org.drools</groupId>    <artifactId>drools-templates</artifactId>    <version>7.0.0.Final</version></dependency>
<dependency> <groupId>org.kie</groupId> <artifactId>kie-api</artifactId> <version>7.0.0.Final</version></dependency>
复制代码

配置文件 kmodule.drl

package com.rulesimport model.ProtocolTypedialect "java"
rule "jk16" when $protocol : ProtocolType(data matches "^1A12.*16$" ) then $protocol.setType("xx燃气表协议"); System.out.println("触发规则1:"+$protocol.getData()); end
rule "jkstd" when $protocol : ProtocolType(data matches "18.*26",length == 10) then $protocol.setType("xx水表标准协议"); System.out.println("触发规则2:"+$protocol.getData()); end
复制代码

测试程序 Main.java

@Slf4jpublic class Main {
@Test public void test() {
KieServices ks = KieServices.Factory.get(); KieContainer kContainer = ks.getKieClasspathContainer(); KieSession kSession = kContainer.newKieSession("ksession-rules");
ProtocolType protocolType = new ProtocolType(); protocolType.setData("1A1212345616"); protocolType.setLength(12); kSession.insert(protocolType); kSession.fireAllRules(); kSession.dispose(); System.out.println(protocolType);
log.info(protocolType.getType()); }
}
复制代码

运行结果

触发规则1:1A1212345616ProtocolType(data=1A1212345616,length=12,type=xx燃气表协议)2020-08-19 18:21:35 [main] [io.example.demo.Main.test(Main.java:35)] - [INFO] xx燃气表协议
Process finished with exit code 0
复制代码

Drools 中的坑

Drools 与 SpringBoot 集成时,与热部署工具 spring-boot-devtools 存在类加载器冲突的问题,会导致所有的规则失效。在 drools 的官方网站中有人提出了这个问题,并认为是个 bug,但是 drools 的开发者认为这不是一个 bug。


地址:https://issues.jboss.org/browse/DROOLS-1540



drools 用的是 lancher classloader,而 devtools 会把我们自己编写的 model 认为是会随时更改的类,所以采用的是 Restart ClassLoader 类加载器进行加载(为了快速进行热部署),devtools 会有两个类加载器,一个 Classloader 加载那些不会改变的类(第三方 Jar 包),另一个 ClassLoader 加载会更改的类,称为 Restart ClassLoader。当采用 Launcher ClassLoader 加载的 A 类与 Restart Class Loader 的 A 类进行对比时,发现不一致,所以 drools 引擎自然无法进行识别。


所以解决办法就是将 devtools 的 maven 依赖去掉即可,或者采用 drools 官网中说明的其它方法。


本文转载自公众号淘系技术(ID:AlibabaMTT)。


原文链接


Drools规则引擎探究以及在IOT的应用


2020-09-05 10:006040

评论 1 条评论

发布
用户头像
推荐一个新兴的规则/流程引擎-ice,新的思想解决复杂规则/流程问题,以及灵活变动问题~高性能,轻量级,接入成本低~也有配套的前端配置页面
http://waitmoon.com/docs
2022-01-06 12:56
回复
没有更多了
发现更多内容

一文带你认识CSS

未见花闻

6月月更

配置swagger

卢卡多多

swagger 6月月更

LabVIEW Arduino无线蓝牙遥控智能车(项目篇—2)

不脱发的程序猿

LabVIEW Arduino VISA 无线遥控智能小车 无线蓝牙遥控智能车

AntDB数据库与强网科技完成产品互认证,积极探索办公自动化领域

亚信AntDB数据库

音视频处理三剑客之 ANS:噪声产生原因及噪声抑制原理解析

ZEGO即构

音视频课程 噪声抑制 ANS

跟着官方文档学 Python 之:3.12 新变化

甜甜的白桃

Python python3.x 6月月更

LabVIEW Arduino ZigBee无线气象站(项目篇—3)

不脱发的程序猿

物联网 LabVIEW Arduino ZigBee无线气象站 无线传感器

如何设计BI平台

奔向架构师

数据仓库 商业智能 6月月更

挑战最全 Apache Doris 学习资料,你想要的都在这里了!

SelectDB

数据库 Doris apache doris 技术干货

InfoQ 极客传媒 15 周年庆征文|海王的鱼塘是怎样炼成的

知心宝贝

人工智能 大数据 运维 前端 InfoQ极客传媒15周年庆

Java—JVM

武师叔

6月月更

关于微服务通信的一些Tips

阿泽🧸

微服务 6月月更

HTTP接口性能测试中池化实践

FunTester

GetxController 生命周期详解

岛上码农

flutter ios 前端 安卓 6月月更

python逆序输出和进制转化(小白也能看懂)

写代码两年半

Python 6月月更

考试试卷存储方案

极客土豆

依图在实时音视频中语音处理的挑战丨RTC Dev Meetup

声网

音视频 RTC Dev Meetup 语音处理

2022-06微软漏洞通告

火绒安全

微软 漏洞 安全漏洞

力扣每日一练之二维数组下篇Day5

京与旧铺

6月月更

在 Pisa-Proxy 中,如何利用 Rust 实现 MySQL 代理

SphereEx

MySQL 数据库 rust

如何编写一份简单易用的在线产品手册

小炮

产品宣传手册 产品说明手册

GCC 为龙芯 CPU的预定义宏

mazhen

c++ RocksDB GCC 龙芯

vue生命周期

小恺

6月月更

数据库每日一题---第15天:未消费的顾客

知心宝贝

数据库 程序员 前端 后端 6月月更

leetcode 413. Arithmetic Slices 等差数列划分

okokabcd

LeetCode 算法与数据结构

5分钟了解红队如何搜索网络情报

穿过生命散发芬芳

6月月更 攻防演练

如何通过事件可视化分析?

清林情报分析师

数据分析 事件分析 可视化分析 时间分析

InfoQ 极客传媒 15 周年庆征文| 聊聊 Go 语言与云原生技术

宇宙之一粟

云原生 6月月更 InfoQ极客传媒15周年庆

从市场需求目标看数据分析演进方向

华为云开发者联盟

人工智能 华为云

python程序设计思想

左手の明天

Python 面向对象

倒计时1天,龙蜥社区走进Intel MeetUp 即将开播!直播大奖等你来拿

OpenAnolis小助手

开源 intel Meetup 龙蜥社区 线上直播

Drools规则引擎探究以及在IOT的应用_5G/IoT_刘欢(流欢)_InfoQ精选文章