策略引擎的首要目标是实现业务逻辑的快速变更. 对于部分业务场景, 产品需要根据实际情况快速新增或调整业务逻辑. 传统的开发模式需要经历需求排期, 需求宣讲, 业务方案设计, 业务方案评审, 实际业务逻辑开发, 功能测试, 灰度发布等一连串的流程. 以风控场景为例, 其往往需要在一天甚至几小时内完成业务逻辑调整, 传统流程即便去除所有非核心环节, 仅开发和部署就可能需要数小时时间, 完全无法满足快速变更的需求.

策略引擎将常见的信息(业务特征字段, 因子信息等)固化到入参之中, 并配置各种处置手段, 使得需求开发的逻辑变为仅调整策略文本. 同时策略变动采用热更新的方式可以在分钟级生效和回滚, 从而满足快速部署的需求. 得益于此能力, 产品甚至可以将原本先定需求再开发的模式改为一边开发一边观察效果一边改进的滚动模式, 从而大幅的减少无效需求产生的人力浪费, 并能够根据实际情况更快的进行调整.

基于以上分析, 任何一种支持热更新的脚本语言似乎都可以满足需求, 但实际的策略引擎还需要考虑两个问题:

  1. 在编写策略的过程中, 在语法层面上应该尽量只需要考虑决策逻辑, 而不存在其他需要额外理解的概念或者特殊的语法元素, 也不会因为语法过于简单而导致需要的功能无法实现.
  2. 语法层面上不应该轻松产生死循环, 高CPU开销, 扩散型RPC调用等可以轻易将运行策略引擎的机器性能耗尽的操作. 策略文件本质上是由产品和开发共同维护的, 而产品通常都并非专业程序员.

因此策略引擎通常采用领域特定语言, 从而进行针对性的语法开发和功能性限制.

策略文件的载体

策略引擎需要执行预先配置好的规则, 因此需要有一种文件格式来承载这些规则. 常见的思路是使用JSON文件存储, 其具有语义明确解析简单的优点, 因此通常都算是比较适中的方案. 不过只要意识到这些规则本质上属于是配置, 那么使用YAML格式就显得非常顺理成章了.

YAML格式相较于JSON格式有几个好处:

  1. YAML在设计上就是优先人类可读, 而JSON在设计上是优先机器可读, 因此只要这些规则是需要人写的, 那么人类可读就总是优于机器可读. 例如YAML原生支持注释语法, 而JSON并不支持.
  2. YAML格式的表达能力与JSON相等, 需要使用JSON的场景(例如返回给前端系统渲染展示)时, 可以简单无损的转换为目标格式
  3. YAML格式天然支持字符串等数据类型, 而JSON中如果需要进行类似表达, 则通常需要单引号嵌套双引号之类的操作, 增加语法噪声

例如以本文中涉及的策略语法, 分别以YAML和JSON时, 内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
{
"Meta": {
"ID": 2602212147,
"Name": "实例策略",
"Enabled": true,
"GrayCount": 10000,
"Priority": 100,
"Owner": "lizec",
"Description": "描述文本可以多行吗?\n这里可以写多行描述"
},
"Filter": [
{
"Name": "判断事件",
"When": "Req.GetEvent() == 'user_login_check'",
"ThenList": [
"Sys.Log('User Login Check')"
]
}
],
"Action": [
{
"Name": "加载角色信息",
"When": true,
"ThenList": [
"Sys.SetVar('VarRole', Req.Attrs['role'])"
]
},
{
"Name": "输出",
"WhenAll": [
"VarRole == 'admin'",
"Req.UserId == 'alan turing'"
],
"ThenList": [
"Rsp.SetCode(200)",
"Rsp.SetMsg('policy_result', 'welcome turing')"
]
}
]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
Meta:
ID: 2602212147
Name: 实例策略
Enabled: true
GrayCount: 10000
Priority: 100
Owner: lizec
Description: |
描述文本可以多行吗?
这里可以写多行描述
Filter:
- Name: 判断事件
When: Req.GetEvent() == 'user_login_check'
ThenList:
- Sys.Log('User Login Check')
Action:
- Name: 加载角色信息
When: true
ThenList:
- Sys.SetVar('VarRole', Req.Attrs['role'])
- Name: 输出
WhenAll:
- VarRole == 'admin'
- Req.UserId == 'alan turing'
ThenList:
- Rsp.SetCode(200)
- Rsp.SetMsg('policy_result', 'welcome turing')

从这个对比不难看出, YAML格式无论是在长度, 可读性, 语法简洁性上都显然的优于JSON格式.

策略文件的语法结构

对于一个单一的策略文件, 其核心逻辑可以概括为一个When->Then的结构, 即满足某个条件时执行特定的动作. 但在实际使用过程中, 往往需要先进行多个条件的判断, 满足全部条件后再执行对应的逻辑. 因此首先将文件分割为Filter和Action两个部分, 两者都是数组结构, 但区别在于

  1. 对于Filter中的When条件, 如果满足则执行对应的Then语句. 如果条件不满足, 则退出当前策略的执行.
  2. 对于Action中的When条件, 如果满足则执行对应的Then语句. 如果条件不满足, 则顺序执行下一个元素中的逻辑.

因此正如其名称所表达的一样, Filter主要用于过滤, 而Action主要用于执行业务逻辑.


对于Filter和Action中的每一个元素, 其可以分为三个部分, Name是用于表示该元素作用的说明文字, 相当于注释信息. When->Then结构支持多种细分的用法, 具体为

名称 含义
When 接受一个布尔表达式, 返回为真时执行Then语句
WhenAll 接受一组布尔表达式, 返回均为真时执行Then语句
WhenAny 接受一组布尔表达式, 至少1个语句返回真时执行Then语句
ThenList 接受一组表达式, 顺序执行每一行的内容
ThenGroup 接受一组表达式, 并发执行每一行的内容, 并等待所有语句执行完毕

通常情况下, 如果条件较为简单, 则可以单独使用When条件, 从而使得策略看起来更简短. 而对于复杂的逻辑条件, 则可以拆分为多个元素, 依次进行条件判断. 对于耗时不敏感的场景, 通常使用ThenList语句顺序执行, 从而避免产生线程安全问题. 而对于需要控制耗时的场景, 则也可以在谨慎地确保所有语句均处理好了并发问题的前提下使用ThenGroup语句并发执行.


每个策略文件还应该包含一些元信息, 例如策略文件的名称, 是否启用, 灰度比例, 负责人等.

策略引擎的入参与出参

策略引擎的核心参数是入参Req和出参Rsp. 两个参数应该是抽象的, 以便于能尽可能的适配不同的场景. 通常来说, 两者具有如下的一些抽象方法

入参函数名 含义 出参函数名 含义
GetEvent() 获取本次决策的事件名称 SetCode(200) 设置返回码
GetAttr(‘key’) 获取指定名称的参数 SetMsg(‘key’, ‘value’) 设置返回消息

事件名是决策系统中最重要的参数, 直接决定了要执行哪些逻辑. 一个决策系统中通常都会接入几百个不同的事件, 因此策略的组织也必然需要按照事件名的维度进行分割, 从而避免不同场景相互耦合导致复杂度爆炸.

由于入参需要覆盖所有的场景, 而不同的场景之间差异很大, 因此无法实现定义一套固定的字段, 或者说即便强行枚举了所有字段, 每次实现时也只有一小部分是有值的. 因此搞清楚每个场景中Req到底有哪些值是一个实际开发过程中的痛点. 针对这个问题的最佳解决方案是和可观测系统联动, 将所有请求的数据落库, 在需要的时候直接去数据库查询实际的数据分布. 一般认为每一次接入都需要有对应的文档, 但文档作为一个人类手写且为了人类可读目的设计的东西, 天然存在时效性低和检索困难的问题. 最后通常都是消耗大量人力写文档, 等到要用的时候发现一点用没有.

在出参中放置本次决策的结果, 它可能是拒绝某个操作的执行, 弹出特定的提示, 或者要求业务方执行特定的额外逻辑, 因此一个返回码通常无法满足灵活多变的业务需求. 将返回码和一个键值对列表结合就可以方便的进行扩展, 业务方根据键值对的取值执行不同的逻辑. 与入参存在同样的问题, 返回键值对的枚举值情况同样难以确定, 此时最准确的方案是直接查询业务方的代码实现逻辑. 相较于入参可以通过数据库检索, 出参的问题在于业务方可以在不知会的情况下修改实现逻辑. 为了避免业务方的此类BUG, 最稳妥的方式只能是直接查看业务方的实现逻辑.

策略引擎的上下文信息

系统函数 执行流控制

策略的组织结构

策略组织结构 主策略,分组策略

数据清洗

赋值与线程安全

可代谢插件系统

事件分裂与转发

执行链路与可观测性

  • 熔断与告警
  • 执行情况统计与上报
  • 基于落库数据统计输入字段信息

文档化与系统化

使用文档存储还是使用系统存储信息? 使用系统存储就会存在不一致性, 就如同注释和函数不一致. 因此正如最好的代码无需注释, 最好的系统也无需信息

代码支持系统

(语法检查与补全插件, 策略存储仓库)

  • 性能优化

  • 高性能内存名单系统

策略文件分发系统

原子弹最大的秘密是原子弹可以被制造

策略文件分发系统最大的秘密是使用传统Mysql+Redis足以支撑几千台规模机器的拉取数据需求. 采用传统的MySQL数据库+定时轮询即可满足中小规模的需求, 只需要确保定时轮询操作设置了随机的偏移值, 避免所有机器在同一时刻拉取数据即可. 使用这一模式在机器数量达到3000+台之前都可以抗住, 仅需要根据机器数量适当的调整云数据库规则即可.

小规模业务对于决策部分来说, 30台机器可能就已经很多了

只有机器数量大于3000+台, 逐渐逼近10000台时, 才需要考虑进一步优化架构. 此时也仅需要引入一个简单的代理层即可, 代理层定时拉取数据库并缓存到本机内存中, 其他机器查询代理层. 这一模式几乎没有上限, 可以承担超过3000万用户同时使用.


内存名单库的最大问题是确定使用场景, 没有银弹可以解决所有问题. 根据使用场景进行适当的优化

微服务设计

微服务设计从逻辑拆分到一体化服务

AI辅助开发

  • AI与架构设计
  • AI开发与CC: 全自动不可取, AI辅助驾驶更好

最后更新: 2026年10月01日 13:46

版权声明:本文为原创文章,转载请注明出处

原始链接: https://lizec.top/2026/07/20/%E5%A6%82%E4%BD%95%E8%AE%BE%E8%AE%A1%E5%86%B3%E7%AD%96%E5%BC%95%E6%93%8E%E7%B3%BB%E7%BB%9F/