规则引擎-实时计算特征篇

先谈谈自己工作的经历吧,我原来在某企业管理风控系统,日常工作最多的就是业务接入,然后就是成批几百个统计类特征的开发工作,策略人员小手一滑啦,Excel中几个维度两两组合,就出现几百个特征需要开发。在此家之前另一家用的决策引擎是Drools, 这个估计很多中小企业都还在用,Drools规则引擎看似强大,实则风控研发的噩梦,一切依赖开发人员,策略长期与规则隔离,很容易出现跟孩子不熟的问题,就好比把大舅家孩子认成姑家的。 好了回归正题实时特征计算,网络很多文章都在说如何用flink,如何实时计算,文章读取,视频也看过。说实话,干货不多,不成体系,存在遮遮掩掩的情况,也大有吹牛皮的存在。 本文将从生产实践方面解读为何需要实时特征计算平台,注意是平台化解决思路。回到开篇场景,业务部门需要推广一项新业务,需要接入风控,然后策略给出一批几百个特征,如果让你开发,你需要多久?

在工作期间在特征这一块领域,我有自己的一套理解。因为我真实参与了累计10000+的统计类特征开发,从SQL统计到三方数据源解析。

先说传统的SQL统计实现模式:前端交互设计一套页面,选中数据源,然后脚本输入SQL语句,动态传入参数。然后直接调度使用。这套方案对某些实时性要求不高的场景(如信贷领域)影响不大,但是对支付风控存在致命的隐患,主要体现在2方面:部署效率和执行耗时;

一个新业务进来,需要开发500个特征,按SQL统计或者代码开发起码要耗时5天,业务验证还要花费更多耗时,策略部署需要等着特征开发完毕才能开始。这样悄悄的过去了半个月甚至一个月,在日益激烈的竞争下,公司业务能等一个月吗?特征的执行耗时严重影响决策的响应时间,当前决策响应耗时多少?工作期间经过重新设计决策引擎和特征计算,我们支付风控达到了200ms以内的决策耗时。

我的设计是拿实时特征计算代替所有的SQL统计,抛开所谓的实时数据和离线数据概念划分,单从支付领域来说,我理解支付交易需要的特征数据大概分为三类:

  1. 超高实时的,具体到包含当笔交易的那种;
  2. 允许轻微延迟的,比如延迟几秒的。
  3. 允许天级延迟的;

如果技术允许,我建议避免第三类的出现,因为支付交易的超高交易量问题,秒级的延迟也存在很高的风险。 我的整体设计思路就是实时计算代替全部的SQL统计,底层均采用增量式计算模式,技术框架采用redis, flink, kafka, hbase; 一类数据用自定义算法或redis自增; 二三类数据用flink; 为保障查询效率,计算结果均存储redis, 这样查询方面耗时控制在毫秒以内。但此方案存在短板,过度依赖redis , 假如redis不可用,所以需要hbase做个备份,一旦出现redis不可用,还可以从hbase查询。特征计算流程按照以下模式

因为计算平台预定义存储是存放redis中,固查询是可以直接从redis获取结果数据,毫秒级响应。针对支付风控,尤其是涉及跨境支付时,因为部分地区存在数据出境安全要求的缘故,此处需要变更一下方案

方案一:集中管理,初始化阶段,针对所有要求不允许出境的数据进行不可逆转化,如转Md5值,以达到合规要求。送往中央集群计算,各地区决策中心统一访问中央集群redis, 因为网络和机房原因,耗时较高;好处就是架构简单,容易维护。

方案二:分区计算存储,中央调配,各地区决策系统本地部署实施特征计算平台,然后同步中央集群,各地集群从中央同步增量;好处就是各地区独立,决策响应超快,缺点是结构复杂,部分增量存在秒级延迟。我倾向方案二

以上都是属于设计篇,下面直接送上具体细节设计

一、模块概述

1.1 模块定位

ttd-feature 是 TTD 智能决策引擎的特征累计模块,专门用于处理和计算业务场景中的特征值(Feature)。特征是指在特定时间窗口内,基于主维度和从维度对业务事件进行累计统计的特征值,广泛应用于风控、反欺诈、交易监控等场景。

1.2 核心能力

  • 多维度累计:支持主维度(如用户ID)和从维度(如设备ID、商户ID)的二维累计
  • 多时间窗口:支持 TTL(滑动窗口)、自然日、自然月、自然年、永久累计五种时间模式
  • 多计算方式:支持求和(SUM)、计数(COUNT)、平均值(AVG)、最小值(MIN)、最大值(MAX)
  • 高性能存储:基于 Redis 的高速缓存,支持毫秒级读写
  • 异步处理:采用线程池异步处理,提升系统吞吐量
  • 幂等控制:基于 Redis 的幂等判断,防止重复累计
  • 监控追溯:完整的监控数据落库,支持数据审计和问题排查

二、核心概念

2.1 特征(Feature)

特征是一种特殊的业务特征,用于统计某个主体在特定时间范围内的行为频率或累计值。

示例场景:

  • 用户在过去 24 小时内的登录次数
  • 用户在近 7 天的交易总金额
  • 同一设备在今日的下单次数
  • 商户在本月的退款总金额

2.2 核心概念定义

概念 说明 示例
特征编码(featureCode) 特征的唯一标识 user_login_count_24h
主维度(masterValue) 累计的主要维度 用户ID:user_12345
从维度(slaveValue) 累计的次要维度(可选) 设备ID:device_abc
特征值(featureValue) 每次累计的值 1(计数)、100.50(金额)
累计方式(cumType) 时间窗口模式 TTL、CD(自然日)、CM(自然月)等
计算方式(calateType) 聚合计算方式 SUM、COUNT、AVG、MIN、MAX

2.3 累计方式枚举(FeatureCumTypeEnum)

枚举值 类型代码 说明 应用场景
TTL ttl 滑动时间窗口 过去24小时、过去7天
CURRENT_DAY cd 自然日 今日累计、昨日清零
CURRENT_MONTH cm 自然月 本月累计、月初清零
CURRENT_YEAR cy 自然年 今年累计、年初清零
FOREVER fvr 永久累计 历史总累计、不清零

三、架构设计

3.1 整体架构图

yaml 复制代码
┌─────────────────────────────────────────────────────────────┐
│                     业务系统(调用方)                          │
└──────────────────┬──────────────────────────────────────────┘
                   │  writeFeature(TxnFeatureRequest)
                   ▼
┌─────────────────────────────────────────────────────────────┐
│                TxnFeatureService(服务接口)                   │
│                     TxnFeatureServiceImpl                  │
└──────────────────┬──────────────────────────────────────────┘
                   │
                   ▼
┌─────────────────────────────────────────────────────────────┐
│           FeatureDataSunkTask(数据处理任务)                  │
│  ┌──────────────────────────────────────────────────────┐  │
│  │  1. 幂等判断(Redis)                                   │  │
│  │  2. 异步并发处理(ThreadPoolExecutor)                  │  │
│  │  3. 路由到具体累计服务(FeatureDataSunkFactory)            │  │
│  │  4. Redis 缓存更新                                     │  │
│  │  5. 监控数据落库                                       │  │
│  └──────────────────────────────────────────────────────┘  │
└──────────────────┬──────────────────────────────────────────┘
                   │
        ┌──────────┴──────────┐
        ▼                     ▼
┌───────────────┐    ┌──────────────────┐
│  FeatureData  │    │ 监控数据落库服务   │
│   SunkFactory │    │FeatureMonitor    │
│ (策略工厂)   │    │  SinkService     │
└───────┬───────┘    └────────┬─────────┘
        │                     │
        ▼                     ▼
┌──────────────────┐  ┌──────────────────────────────────┐
│ 累计策略实现类    │  │ 监控数据存储                        │
│                  │  │  ├─ 事件流数据(FeatureEventData)     │
│ ├─ TTL模式       │  │  ├─ 主维度值记录                   │
│ ├─ 自然日模式    │  │  ├─ 历史快照数据                   │
│ ├─ 自然月模式    │  │  ├─ 查询快照数据                   │
│ ├─ 自然年模式    │  │  └─ 日监控数据                     │
│ └─ 永久累计模式  │  └──────────────────────────────────┘
└──────────────────┘
        │
        ▼
┌─────────────────────────────────────────────────────────────┐
│                    Redis(高速缓存层)                         │
│  ┌─────────────────────────────────────────────┐           │
│  │ Key: feature:{featureCode}:{masterValue}    │           │
│  │ Value: Map<slaveValue, List<ValueItem>>     │           │
│  │ TTL: 动态过期时间                             │           │
│  └─────────────────────────────────────────────┘           │
└─────────────────────────────────────────────────────────────┘

3.2 核心组件说明

3.2.1 服务层

TxnFeatureService(特征累计服务接口)

提供特征累计的统一入口方法 writeFeature(TxnFeatureRequest request),接收业务请求后触发异步处理流程。

TxnFeatureRequest(请求对象)

包含以下核心字段:

  • txnId:交易流水号(必填)
  • txnTime:交易时间(可选)
  • eventDataList:事件数据列表(必填),包含多个 FeatureEventDataDto 对象

3.2.2 数据处理层

FeatureDataSunkTask(数据处理任务)

核心处理流程分为以下步骤:

  1. 校验事件数据:检查事件数据列表是否为空,为空则抛出异常
  2. 保存事件流数据:将原始事件数据异步保存到监控存储,用于数据追溯
  3. 并发处理事件 :遍历事件列表,对每个事件执行以下操作:
    • 幂等判断:基于 Redis 检查该交易是否已处理(Key 格式:pay:rcs:feature:handle:{txnId}:{featureCode}),已处理则跳过
    • 异步处理:提交任务到线程池(payRcsFeatureSunkTp),使用 CompletableFuture 实现并发
    • 策略路由:通过 FeatureDataSunkFactory 根据 cumType 获取对应的累计策略服务
    • 执行算法:调用策略服务的 getFeatureData 方法计算累计值
    • 写入缓存:构建 Redis Key 和过期时间,将处理后数据写入 Redis
    • 保存监控:记录主维度值和历史快照数据
    • 设置幂等标记:在 Redis 中设置处理标记,5 分钟过期
  4. 等待完成:使用 CompletableFuture.allOf().join() 等待所有异步任务完成

3.2.3 策略模式层

AbstractFeatureDataSunkService(抽象策略基类)

定义策略接口和公共方法:

  • getCumType():返回累计类型标识(策略路由键)
  • getFeatureData(dto):抽象方法,由子类实现具体的累计算法
  • getCacheItemFromRedis(redisKey):公共方法,从 Redis 读取并反序列化缓存数据

FeatureDataSunkFactory(策略工厂)

采用 Spring 自动注册机制:

  • 使用 @PostConstruct 在启动时扫描所有 AbstractFeatureDataSunkService 类型的 Bean
  • 将每个策略实例以其 getCumType() 返回值作为 Key 存入 serviceMap
  • 提供静态方法 getService(cumType) 根据累计类型获取对应策略

设计优势:

  • ✅ 开闭原则:新增累计方式只需添加新策略类,无需修改现有代码
  • ✅ 自动注册:基于 Spring 的自动发现机制,零配置
  • ✅ 策略路由:根据 cumType 动态路由到对应实现

3.2.4 策略实现类

五种累计策略实现:

策略类 累计类型 核心逻辑
FeatureDataSunkByTTLService TTL 基于滑动时间窗口,过滤过期数据
FeatureDataSunkByCalendarDayService 自然日 按自然日累计,每日清零
FeatureDataSunkByCalendarMonthService 自然月 按自然月累计,每月清零
FeatureDataSunkByCalendarYearService 自然年 按自然年累计,每年清零
FeatureDataSunkByPersistService 永久累计 永久累计,不清零

TTL 策略实现示例(FeatureDataSunkByTTLService)

核心算法流程:

  1. 从 Redis 读取缓存数据(Map<从维度, 值项列表>)
  2. 计算时间窗口边界:windowMinTime = txnTime - timeWindow
  3. 过滤过期数据:移除时间戳小于窗口边界的记录
  4. 清理空值:移除累计值为空的从维度
  5. 保存处理前数据快照(用于监控对比)
  6. 追加新数据:在对应从维度下添加新的值项(包含 txnId、时间戳、值)
  7. 首次创建:若缓存不存在,则创建新的数据结构
  8. 返回 FeatureWindowDataDto,包含处理前后的数据快照

3.2.5 辅助工具类

FeatureRedisKeyBuilder(Redis Key 构建器)

构建特征数据项 Key,格式为 feature:{featureCode}:{masterValue},例如 feature:user_login_count_24h:user_12345

FeatureTimeOutBuilder(过期时间构建器)

提供三个核心方法:

  • getCacheTime(dto):根据 cumType 和 expireNum/expireType 计算缓存过期时间(秒)
  • getWindowMinTime(dto):计算时间窗口最小时间戳
  • getWindowExpireTime(dto):计算窗口过期时间

3.2.6 监控层

FeatureMonitorSinkService(监控数据落库服务)

监控数据类型:

监控数据 DTO 类 用途
事件流数据 FeatureEventDataDto 记录原始事件,用于数据追溯
主维度值记录 FeatureMasterValueRecord 记录主维度的缓存大小和字节数
历史快照数据 FeatureHistorySnapshotDto 记录写入前后的数据快照
查询快照数据 FeatureReadSnapshotDto 记录查询时的数据快照
日监控数据 FeatureDailyMonitorDto 每日统计汇总

监控数据流转:

vbnet 复制代码
事件写入
   ↓
保存事件流数据(FeatureEventData)
   ↓
计算累计值
   ↓
保存主维度值记录(FeatureMasterValueRecord)
   ├─ Redis Key
   ├─ 缓存大小(条目数)
   └─ 缓存字节数
   ↓
保存历史快照数据(FeatureHistorySnapshotDto)
   ├─ 写入前数据(beforeData)
   └─ 写入后数据(afterData)
   ↓
异步落库(Kafka / 数据库)

四、数据模型

4.1 核心数据对象

FeatureEventDataDto(事件流数据)

字段 类型 说明
txnId String 交易流水号
txnTime Date 交易时间
featureCode String 特征编码
masterValue String 主维度值
slaveValue String 从维度值
featureValue BigDecimal 特征值
cumType String 累计方式
calateType String 计算方式
expireNum Integer 过期时间数
expireType String 过期时间单位
expireTime Date 过期时间

FeatureWindowDataDto(窗口数据)

字段 类型 说明
featureCode String 特征编码
masterValue String 主维度值
txnId String 交易流水号
redisKey String Redis Key
beforeData Map<String, List<FeatureValueItem>> 处理前数据
afterData Map<String, List<FeatureValueItem>> 处理后数据
expireTime Date 过期时间

FeatureValueItem(值项)

字段 类型 说明
txnId String 交易流水号
ts Long 时间戳
value BigDecimal

4.2 Redis 数据结构

Key 格式:

css 复制代码
feature:{featureCode}:{masterValue}

示例:

makefile 复制代码
feature:user_login_count_24h:user_12345

Value 结构(JSON):

json 复制代码
{
  "device_001": [
    {"txnId": "txn001", "ts": 1716000000000, "value": 1},
    {"txnId": "txn002", "ts": 1716003600000, "value": 1}
  ],
  "device_002": [
    {"txnId": "txn003", "ts": 1716007200000, "value": 1}
  ]
}

说明:

  • Key:从维度值(如设备ID)
  • Value:值项列表(包含交易ID、时间戳、值)

五、核心流程

5.1 速率累计流程

markdown 复制代码
1. 业务系统调用 writeFeature(TxnFeatureRequest)
   ↓
2. TxnFeatureServiceImpl 接收请求
   ↓
3. FeatureDataSunkTask.execute() 执行处理
   ↓
4. 校验事件数据列表非空
   ↓
5. 异步保存事件流数据到监控存储
   ↓
6. 遍历事件列表,对每个事件:
   ├─ 6.1 幂等判断(Redis)
   │    └─ 已处理 → 跳过
   │    └─ 未处理 → 继续
   ├─ 6.2 提交异步任务到线程池
   │    ├─ 获取累计策略服务(FeatureDataSunkFactory)
   │    ├─ 执行策略算法(getFeatureData)
   │    │   ├─ 从 Redis 读取缓存数据
   │    │   ├─ 过滤过期数据
   │    │   ├─ 追加新数据
   │    │   └─ 返回前后数据快照
   │    ├─ 写入 Redis(带过期时间)
   │    ├─ 保存主维度值监控记录
   │    ├─ 保存历史快照数据
   │    └─ 设置幂等标记(5分钟)
   │
7. 等待所有异步任务完成(CompletableFuture.allOf)
   ↓
8. 返回(异步处理,立即返回)

## 六、设计模式

### 6.1 策略模式(Strategy Pattern)

**应用场景:** 不同累计方式的算法实现

AbstractFeatureDataSunkService(抽象策略) ↑ ├── FeatureDataSunkByTTLService(具体策略:TTL) ├── FeatureDataSunkByCalendarDayService(具体策略:自然日) ├── FeatureDataSunkByCalendarMonthService(具体策略:自然月) ├── FeatureDataSunkByCalendarYearService(具体策略:自然年) └── FeatureDataSunkByPersistService(具体策略:永久累计)

FeatureDataSunkFactory(上下文/工厂) └─ 根据 cumType 选择具体策略

arduino 复制代码
**优势:**
- 算法独立变化,互不影响
- 易于扩展新的累计方式
- 符合开闭原则

### 6.2 工厂模式(Factory Pattern)

**应用场景:** 策略实例的创建和路由

```java
@PostConstruct
public void init() {
    // 自动发现并注册所有策略实现
    Map<String, AbstractFeatureDataSunkService> map = 
        context.getBeansOfType(AbstractFeatureDataSunkService.class);
    map.forEach((key, value) -> serviceMap.put(value.getCumType(), value));
}

优势:

  • 自动注册,无需手动配置
  • 集中管理策略实例
  • 支持动态扩展

6.3 模板方法模式(Template Method Pattern)

应用场景: AbstractFeatureDataSunkService 定义算法骨架

java 复制代码
// 抽象基类定义公共方法
protected Map<String, List<FeatureValueItem>> getCacheItemFromRedis(String redisKey) {
    // 公共的 Redis 读取逻辑
}

// 子类实现特定算法
public abstract FeatureWindowDataDto getFeatureData(FeatureEventDataDto dto);

七、性能优化

7.1 并发处理

线程池配置:

  • payRcsFeatureSunkTp:数据处理线程池,用于并发处理特征累计任务
  • payRcsFeatureMonitorTp:监控数据线程池,用于异步保存监控数据

并发级别:

  • 事件级别并发:每个事件独立处理,互不阻塞
  • 使用 CompletableFuture 实现异步编排
  • allOf().join() 等待所有任务完成后返回

7.2 幂等控制

机制:

  • 基于 Redis 实现,Key 格式为 pay:rcs:feature:handle:{txnId}:{featureCode}
  • 处理前检查 Key 是否存在,存在则跳过
  • 处理完成后设置 Key 值为 "1",TTL 设为 300 秒(5 分钟)
  • Redis 原子操作保证并发安全

优势:

  • 防止重复累计,保证数据一致性
  • 自动过期,避免内存泄漏
  • 轻量级判断,性能开销极小

八、扩展性设计

8.1 新增累计方式

步骤:

  1. 创建策略实现类,继承 AbstractFeatureDataSunkService
  2. 实现 getCumType() 方法,返回自定义累计类型标识(如 "custom")
  3. 实现 getFeatureData(dto) 方法,编写自定义累计算法
  4. 添加 @Service 注解,Spring 自动扫描注册
  5. 在 FeatureCumTypeEnum 中添加对应枚举值

优势: 无需修改工厂类和现有代码,系统启动时自动发现并注册新策略。

8.2 新增计算方式

当前支持: SUM、COUNT、AVG、MIN、MAX

扩展方式:

  • 在策略实现中根据 calateType 分支处理
  • 或创建新的策略类专门处理

九、监控与运维

9.1 监控指标

指标 说明 监控方式
写入 QPS 每秒写入事件数 日志统计
写入延迟 从接收到写入完成的耗时 时间戳对比
Redis 命中率 缓存命中比例 Redis 监控
缓存大小 每个主维度的缓存条目数和字节数 FeatureMasterValueRecord
异常率 处理失败的比例 异常日志

9.2 日志规范

关键日志点: 警告日志: 记录已处理的重复请求(包含 txnId、featureCode)

  • 错误日志:记录处理失败的异常信息(包含 txnId、eventData、异常堆栈)
  • 业务日志:记录处理流程和关键节点(包含 pointCode、txnId 等业务标识)

9.3 数据追溯

追溯链路:

markdown 复制代码
txnId → FeatureEventDataDto(事件流数据)
      → FeatureHistorySnapshotDto(历史快照)
      → Redis Key(当前缓存数据)
      → FeatureMasterValueRecord(主维度记录)

十、最佳实践

10.1 特征设计原则

  1. 主维度选择

    • 选择高频查询的维度作为主维度
    • 常用:用户ID、设备ID、IP地址
  2. 从维度选择

    • 需要细分统计的维度作为从维度
    • 常用:商户ID、商品ID、地区
  3. 时间窗口设置

    • TTL 模式:根据业务场景设置合理窗口
    • 自然日/月/年:适合周期性统计
  4. 计算方式选择

    • COUNT:统计次数(如登录次数)
    • SUM:累计金额(如交易总额)
    • AVG:平均值(如平均订单金额)

10.2 性能调优建议

Redis 配置:

  • 启用持久化(AOF + RDB),保证数据安全
  • 合理设置内存上限(maxmemory)
  • 配置淘汰策略(volatile-ttl,优先淘汰带 TTL 的 Key)

线程池调优:

  • 核心线程数:CPU 核心数 × 2
  • 队列容量:根据业务峰值设置(建议 1000-5000)
  • 拒绝策略:CallerRunsPolicy(调用者运行)

批量处理:

  • 批量写入 Redis(Pipeline),减少网络开销
  • 批量落库监控数据,降低数据库压力

十一、总结

11.1 架构优势

  1. 高性能:基于 Redis 的毫秒级读写,支持高并发场景
  2. 高可用:异步处理 + 异常隔离,单个事件失败不影响整体
  3. 易扩展:策略模式 + 工厂模式,新增累计方式零改造
  4. 可追溯:完整的监控数据落库,支持数据审计
  5. 幂等安全:Redis 幂等标记,防止重复累计

11.2 适用场景

  • ✅ 风控反欺诈(高频交易检测)
  • ✅ 用户行为分析(登录频率、操作频率)
  • ✅ 交易监控(交易限额、频次控制)
  • ✅ 营销活动(参与次数限制)
  • ✅ 资源配额(API 调用频次限制)

11.3 技术栈

技术 用途
Redis 高速缓存,存储特征数据
kafka/flink kafka高并发削峰处理,flink异步实时增量计算
CompletableFuture 异步并发处理
Spring IoC 容器,策略自动注册
FastJSON JSON 序列化/反序列化
策略模式 多累计方式实现
工厂模式 策略实例管理

特征管理平台配置篇: 想要把系统交出去,就要懂业务人员会干什么,每个人都会填写表单和excel表格,所以针对这个场景,增强交互,条件组动态增加各种前置条件,内置表达式转换算法。将表单数据转为groovy脚本, 主从维度值也是脚本获取。变量就是特征元数据(本参考页面是将特征平台集成到决策管理系统,二者公用同一套元数据,单独特征管理平台时需要增加元数据管理模块)

相关推荐
hxttd4 小时前
如何从0到1设计企业级风控决策平台?
后端
血小溅4 小时前
Git Submodule 实战指南:从基础概念到 AI-Native 项目落地
后端
用户21991679703914 小时前
基于.Net的NetCoreKevin框架中AgentFramework实现AI智能体Skill和工具动态管理和加载
后端
日月云棠4 小时前
6 高级配置:Spring Boot整合、泛化调用与配置指南
java·后端
SE_NAK4 小时前
go-zero 两个限流器都踩了坑,最后自行实现了一个分布式令牌桶
后端
苏三说技术4 小时前
Durid和HikariCP,哪个连接池更好?
后端
思考着亮4 小时前
1.DDL(数据定义语言)
后端
她的男孩4 小时前
Spring Boot 3 后台框架的自动配置设计:少写配置,多做组合
后端