特征计算加速:实时特征存储选型与优化
特征计算,说白了就是风控系统的「心脏」。你规则写得再漂亮,模型训练得再准,特征算不出来或者算得太慢,一切都白搭。我在做实时风控系统的时候,踩过最多的坑就在这一块。今天咱们就聊聊特征存储选型、预计算策略、滑动窗口优化和特征降维这四件事。
一、实时特征存储选型:Redis vs Aerospike
先说说存储。特征数据要快,要能扛住高并发,还得支持一些简单的聚合操作。市面上最常用的两个方案就是 Redis 和 Aerospike。我两个都用过,各有各的脾气。
Redis:简单粗暴,但内存是硬伤
Redis 的好处不用我多说——快,真的快。单机 QPS 能到 10 万+,而且数据结构丰富。做实时特征存储,我一般用 Hash 结构来存用户的多维特征,用 Sorted Set 来做滑动窗口。
# 存储用户特征(Hash)
HSET user:10001:features age 28 income 15000 credit_score 720
# 滑动窗口计数(Sorted Set)
ZADD user:10001:events:1h 1680000000 "login"
ZADD user:10001:events:1h 1680000100 "payment"
ZREMRANGEBYSCORE user:10001:events:1h 0 1679996400
ZCARD user:10001:events:1h
但 Redis 有个致命问题——内存太贵了。你想想看,一个用户存几十个特征,几百万用户就是几十 GB。我见过一个项目,Redis 内存飙到 200GB,一个月云账单看得老板直皱眉。
Aerospike:为实时特征而生
Aerospike 是我后来才接触的。说实话,第一次用的时候有点不习惯,但用顺手之后真香。它的设计理念就是「闪存优先」,数据可以存在 SSD 上,但访问速度依然很快。
| 对比维度 | Redis | Aerospike |
|---|---|---|
| 存储介质 | 纯内存 | 内存+SSD |
| 单节点 QPS | 10万+ | 100万+ |
| 数据持久化 | RDB/AOF(有性能损耗) | 原生支持,无额外开销 |
| 成本 | 高(全内存) | 低(SSD为主) |
| 数据结构 | 丰富(Hash, List, Sorted Set等) | 简单(KV + Map) |
我个人的习惯是:如果特征数据量在 100GB 以内,用 Redis 就够了,开发成本低。如果超过 500GB,或者对成本敏感,果断上 Aerospike。中间那一段,看团队技术栈。
二、特征预计算策略
实时计算最大的敌人是什么?是延迟。每次请求都从头算一遍特征,那肯定慢。预计算就是「把功夫下在平时」。
我常用的预计算策略有三种:
- 离线预计算 + 在线缓存:每天凌晨用 Spark 批量算好基础特征,写入特征存储。在线请求时直接读取,几乎零延迟。
- 增量预计算:用户每产生一个事件,就更新一次特征值。比如用户完成一笔交易,立即更新「近1小时交易次数」这个特征。
- 分层预计算:把特征分成高频、中频、低频三层。高频特征(如登录次数)实时更新,低频特征(如用户画像)每天更新一次。
// 增量预计算示例(伪代码)
public void onEvent(UserEvent event) {
String key = "user:" + event.getUserId() + ":features";
// 原子更新:近1小时交易次数 +1
featureStore.hincrBy(key, "txn_count_1h", 1);
// 更新最近交易时间
featureStore.hset(key, "last_txn_time", System.currentTimeMillis());
// 更新滑动窗口
featureStore.zadd("user:" + event.getUserId() + ":txn_timestamps",
System.currentTimeMillis(), UUID.randomUUID().toString());
}
嗯,这里要注意:增量预计算虽然快,但容易出错。比如事件重复消费、时序错乱,都会导致特征值不准。我建议加上「幂等性校验」,或者用事件时间(Event Time)而不是处理时间(Processing Time)。
三、滑动窗口优化
滑动窗口是实时风控里最常用的特征类型。比如「过去5分钟登录次数」「过去1小时交易金额」。但滑动窗口也是最容易出性能问题的地方。
为什么会这样?因为每个请求都要查窗口内的数据,如果窗口很大(比如7天),数据量就很大,查询自然慢。
我分享几个优化思路:
- 用计数器代替全量存储:如果只需要计数,不要存所有事件的时间戳。用多个小窗口(比如每分钟一个计数器)来近似滑动窗口。
- 预聚合 + 时间桶:把时间切成桶(比如1分钟一个桶),每个桶里存聚合值。查询时只需要读最近N个桶的数据。
- 本地缓存 + 异步刷新:在应用层缓存最近几秒的窗口结果,用异步线程去更新。这样大部分请求直接走缓存,只有缓存过期才去查存储。
// 时间桶滑动窗口实现
public class TimeBucketWindow {
private static final int BUCKET_SIZE_MS = 60_000; // 1分钟一个桶
private static final int WINDOW_BUCKETS = 60; // 1小时 = 60个桶
private final AtomicLongArray buckets = new AtomicLongArray(WINDOW_BUCKETS);
public void add(long timestamp) {
int index = (int)((timestamp / BUCKET_SIZE_MS) % WINDOW_BUCKETS);
buckets.incrementAndGet(index);
}
public long getCount(long currentTime) {
long sum = 0;
long currentBucket = currentTime / BUCKET_SIZE_MS;
for (int i = 0; i < WINDOW_BUCKETS; i++) {
long bucketTime = currentBucket - i;
if (bucketTime < 0) break;
int index = (int)(bucketTime % WINDOW_BUCKETS);
sum += buckets.get(index);
}
return sum;
}
}
四、特征降维
最后聊聊特征降维。很多团队做实时风控,恨不得把几百个特征全用上。结果呢?计算慢、存储贵、模型还容易过拟合。
我见过最夸张的一个项目,一个用户存了 200 多个特征,每次请求要算 30 秒。后来我们做了降维,只保留 20 个核心特征,效果反而更好了。
常用的降维方法:
- 业务理解驱动:跟业务方聊,哪些特征是真的有用的?哪些是「我觉得可能有用」的?砍掉后者。
- 特征重要性排序:用随机森林或 XGBoost 跑一遍,看每个特征的重要性分数。低于阈值的直接去掉。
- 特征交叉与组合:把多个弱特征组合成一个强特征。比如「近1小时登录次数」和「近1小时支付次数」可以组合成「近1小时活跃度」。
- Embedding 化:对于高基数类别特征(如设备ID、IP地址),用 Embedding 降维到 16 维或 32 维向量。
我曾经在一个支付风控项目里,把特征从 150 个降到了 25 个。存储成本降了 80%,计算延迟从 200ms 降到了 20ms,AUC 反而从 0.85 升到了 0.88。你想想看,有时候「少即是多」。
知识体系总览
下面这张图是我对本章内容的总结。你可以看到,特征计算加速不是单一的技术选型,而是一套组合拳。
说白了,特征计算加速没有银弹。你要根据业务场景、数据量、成本预算来综合选型。我个人的经验是:先做降维,再选存储,最后优化计算逻辑。顺序搞反了,容易白费功夫。
好了,这一章的内容就到这里。记住:特征计算是风控系统的「心脏」,心脏跳得快不快,就看你这四件事做得怎么样。
公众号:蓝海资料掘金营,微信deep3321