3、数据预加载与预热:启动时预加载、基于用户画像的预热、定时任务刷新策略
说到特征获取的低延迟,很多人第一反应就是「把缓存搞快点」。但说实话,缓存只是防守,预加载才是进攻。你想想看,如果用户还没请求,数据就已经在内存里等着了,那延迟能不低吗?
这一章,我重点聊聊三种预加载策略:启动时预加载、基于用户画像的预热、定时任务刷新。这三种策略我都在线上实战过,各有各的适用场景,也各有各的坑。
3.1 启动时预加载:服务起来,数据先到位
启动时预加载,说白了就是服务进程启动的时候,主动把一批特征数据拉到本地内存里。这样第一个请求进来的时候,数据已经准备好了,不用临时去远程拉。
我在项目中遇到过这样一个场景:推荐系统的用户基础画像特征,比如性别、年龄、城市等级,这些数据变化频率很低,但每次请求都要用。如果等请求来了再查,光是网络开销就得加 2-3ms。后来我改成启动时一次性加载到本地 HashMap,查询直接变成 O(1) 内存访问,延迟直接降到微秒级。
3.1.1 实现方式
启动时预加载通常放在 Spring 的 @PostConstruct 或者 ApplicationRunner 里。我习惯用一个专门的 DataPreloader 组件来管理:
@Component
public class UserProfilePreloader {
private final Map<Long, UserProfile> profileCache = new ConcurrentHashMap<>();
@PostConstruct
public void preload() {
log.info("开始预加载用户画像数据...");
List<UserProfile> allProfiles = userProfileDao.selectAll();
for (UserProfile profile : allProfiles) {
profileCache.put(profile.getUserId(), profile);
}
log.info("预加载完成,共加载 {} 条记录", allProfiles.size());
}
public UserProfile getProfile(Long userId) {
return profileCache.get(userId);
}
}
3.1.2 避坑指南
我曾经踩过一个坑:预加载的数据量太大,导致服务启动时间从 10 秒飙到 3 分钟,健康检查都超时了。后来我加了分页加载和异步加载策略。
- 控制数据量: 单次预加载不要超过 100 万条,否则 GC 压力会很大
- 异步加载: 核心数据同步加载,非核心数据异步加载
- 失败重试: 预加载失败不能阻塞服务启动,要降级为懒加载
3.2 基于用户画像的预热:猜用户想要什么
启动时预加载解决的是「全局数据」的问题。但实际场景中,不同用户需要的特征是不一样的。比如一个北京的程序员和一个上海的美妆博主,他们的推荐特征完全不同。
这就引出了第二种策略:基于用户画像的预热。说白了,就是根据用户的历史行为、标签、活跃时段,提前把可能用到的特征加载到缓存里。
3.2.1 怎么做?
我一般分三步走:
- 画像建模: 给每个用户打标签,比如「高频用户」「夜间活跃」「美妆偏好」
- 特征关联: 每个标签关联一组特征 key,比如「美妆偏好」关联「美妆类目特征」「价格敏感度特征」
- 预热触发: 用户登录、刷新页面、切换 tab 时触发预热
3.2.2 代码示例
public class UserProfileWarmup {
private static final Map<String, List<String>> TAG_FEATURE_MAP = Map.of(
"beauty", List.of("category:beauty", "price:sensitive", "brand:premium"),
"tech", List.of("category:tech", "price:mid", "brand:xiaomi"),
"sports", List.of("category:sports", "price:budget", "season:summer")
);
public void warmup(Long userId, List<String> tags) {
for (String tag : tags) {
List<String> features = TAG_FEATURE_MAP.get(tag);
if (features == null) continue;
for (String featureKey : features) {
// 提前加载到本地缓存或 Redis
cacheClient.get(featureKey);
}
}
}
}
3.3 定时任务刷新策略:让数据保持新鲜
预加载和预热解决了「数据在不在」的问题,但解决不了「数据旧不旧」的问题。你想想看,用户的画像特征是会变的——昨天还是「单身」,今天可能就变成「已婚」了。如果数据不更新,那预加载反而成了负资产。
定时任务刷新,就是解决这个问题的。
3.3.1 刷新策略怎么选?
| 策略 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 全量刷新 | 数据量小(<10万) | 实现简单,一致性高 | 耗时长,资源消耗大 |
| 增量刷新 | 数据量大,变化率低 | 效率高,资源消耗小 | 需要记录变更日志 |
| 双缓冲切换 | 要求数据强一致 | 切换瞬间完成,无锁 | 内存占用翻倍 |
我个人习惯用增量刷新 + 双缓冲切换的组合。增量刷新保证效率,双缓冲切换保证数据一致性。
3.3.2 双缓冲切换实现
public class DoubleBufferCache<K, V> {
private volatile Map<K, V> current = new HashMap<>();
private volatile Map<K, V> backup = new HashMap<>();
public void refresh(Map<K, V> newData) {
// 1. 写入备份缓冲区
backup.putAll(newData);
// 2. 切换引用(原子操作)
Map<K, V> temp = current;
current = backup;
backup = temp;
// 3. 清空旧数据
backup.clear();
}
public V get(K key) {
return current.get(key);
}
}
3.4 三种策略如何配合?
这三种策略不是互斥的,而是互补的。我画了一张图来说明它们的关系:
实际落地时,我一般这样组合:
- 启动时: 加载全局静态特征(城市、类目、品牌等)
- 用户登录/活跃时: 触发用户画像预热,加载个性化特征
- 每 5 分钟: 定时任务增量刷新,更新变化的数据
- 每 30 分钟: 双缓冲全量切换,保证数据最终一致
3.5 总结一下
数据预加载和预热,核心就三句话:
- 启动时预加载:让服务一启动就有数据,适合全局静态特征
- 用户画像预热:猜用户想要什么,提前加载到缓存,适合个性化特征
- 定时任务刷新:让数据保持新鲜,避免「预加载了个寂寞」
这三种策略配合好了,特征获取延迟基本能控制在 1ms 以内。嗯,这就是我实战中反复验证过的方案,希望能帮到你。