推荐系统_runRetainRecommend_如何防止重复插入retain推荐数据表
你已经触及“推荐任务幂等性与重复推荐控制”的核心。
我们一步步看下 为什么会重复、怎么防、最佳实践是什么 👇
🧩 一、问题出现的原因
你的定时任务 runRetainRecommend()(每天执行) 会:
遍历所有活跃用户;
计算 retain 推荐结果;
批量插入到
recommend_log_location。
如果任务中断(例如服务重启、数据库锁、网络异常),
下次再执行时就可能:
重新计算了同一天的推荐;
重复插入相同
(uuid, region_id, loc_uid, strategy_type)组合。
⚙️ 二、典型“重复推荐”风险场景
| 场景 | 结果 |
|---|---|
| 任务中断后重启 | 同一个用户重复推荐相同场地 |
| 定时任务多实例并行 | 多次执行相同逻辑 |
| 异常重试 / 手动补跑 | 同样数据插入多次 |
✅ 三、最佳实践方案(推荐策略)
可以从三个层面控制:逻辑层 + 数据层 + 执行层。
方案 1:逻辑层幂等控制(最推荐)
在插入 recommend_log_location 之前,
先判断当日是否已经存在相同推荐,若有则跳过或更新。
public void insertRetainRecommendations(Long userId, Long regionId, List<UserLocationBehaviorStatsDTO> rankedStats) {LocalDate today = LocalDate.now();for (UserLocationBehaviorStatsDTO stat : rankedStats) {boolean exists = recommendLogLocationMapper.existsRecommendForToday(userId, regionId, stat.getId(), "retain", today);if (!exists) {RecommendLogLocation rec = new RecommendLogLocation();rec.setUuid(userId);rec.setRegionId(regionId);rec.setLocUid(stat.getId());rec.setStrategyType("retain");rec.setRecommendScore(stat.getScore());rec.setRecommendedTs(LocalDateTime.now());rec.setIsInteracted(false);recommendLogLocationMapper.insert(rec);}}
}
对应 SQL(MyBatis):
<select id="existsRecommendForToday" resultType="boolean">SELECT COUNT(*) > 0FROM recommend_log_locationWHERE uuid = #{userId}AND region_id = #{regionId}AND loc_uid = #{locUid}AND strategy_type = #{strategyType}AND DATE(recommended_ts) = #{today}
</select>
📍特点:
简单易懂;
幂等;
可在失败重跑后自动去重。
方案 2:数据库层唯一索引防重
给表加一个唯一索引:
ALTER TABLE recommend_log_location
ADD UNIQUE KEY uniq_user_loc_day (uuid, region_id, loc_uid, strategy_type, DATE(recommended_ts));
然后使用:
try {recommendLogLocationMapper.insert(rec);
} catch (DuplicateKeyException e) {// 已存在则忽略
}
📍特点:
强一致性;
防止并发插入;
不怕多实例并行。
📉缺点:
MySQL 不支持函数索引中的
DATE(),需要在表中额外添加一列recommend_date(日期字段):ALTER TABLE recommend_log_location ADD COLUMN recommend_date DATE GENERATED ALWAYS AS (DATE(recommended_ts)) STORED; ALTER TABLE recommend_log_location ADD UNIQUE KEY uniq_user_loc_day (uuid, region_id, loc_uid, strategy_type, recommend_date);
方案 3:执行层锁 / 状态标识控制
在任务开始时:
记录任务执行状态(如 Redis key 或数据库任务表);
如果同日期已有“成功执行”记录,则跳过;
如果中断,可重新执行未完成的部分。
例:
if (redisTemplate.hasKey("retain_task:" + LocalDate.now())) {return; // 已执行
}
redisTemplate.opsForValue().set("retain_task:" + LocalDate.now(), "running", 2, TimeUnit.HOURS);
📍特点:
防止同一时刻多实例并行;
可用于分布式环境(微服务架构)。
🧠 四、推荐组合策略(最佳实践)
| 层级 | 推荐做法 |
|---|---|
| 逻辑层 | 查询是否已有推荐记录,幂等插入 |
| 数据层 | 添加 recommend_date + 唯一约束 |
| 执行层 | Redis / DB 锁防并发执行 |
| 重跑机制 | 可安全重试,已有数据自动跳过 |
🧩 五、可选优化
记录任务表
recommend_task_log,存放任务日期、执行时间、状态(成功/失败),便于重跑;若使用批量插入,可通过
INSERT IGNORE或ON DUPLICATE KEY UPDATE避免重复。
💬 总结一句话:
“推荐任务”必须具备幂等性,最佳实践是 ——
逻辑层先查是否存在 + 数据层唯一约束 + 执行层分布式锁。
