1. 项目概述:基于协同过滤的跳蚤市场推荐系统实战
去年在帮母校升级校园二手交易平台时,我发现学生们最头疼的问题不是找不到商品,而是被海量重复的二手信息淹没。一个计算机系的学弟甚至抱怨:"每次找教材都要翻20多个同款帖子,比期末考试还费劲。"这促使我设计了这个基于协同过滤算法的商品推荐系统,通过分析用户行为数据,实现千人千面的个性化推荐。
这个系统采用SpringBoot+Vue的全栈架构,核心在于用协同过滤算法挖掘用户潜在兴趣。与普通电商推荐不同,跳蚤市场的特殊性在于:
- 商品生命周期短(平均在线7天)
- 用户行为稀疏(人均每月交易不足3次)
- 冷启动问题严重(60%用户只交易1次)
针对这些痛点,我们设计了混合推荐策略:初期用热度榜解决冷启动,积累数据后切换为协同过滤,最终使推荐商品点击率提升47%。下面从技术实现角度详细解析这个项目。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术架构设计
2.1 整体架构设计
系统采用典型的前后端分离架构:
code复制[前端] Vue 2.x + Element UI
↑ axios ↓
[网关] Nginx反向代理
↑ HTTP/HTTPS ↓
[后端] SpringBoot 2.7 + MyBatis-Plus
↑ ↓
[数据层] MySQL 8.0(主业务) + Redis 7.0(缓存)
选择这套技术栈主要基于:
- 开发效率:SpringBoot的starter机制能快速集成Redis、MyBatis等组件
- 性能考量:Redis缓存用户行为数据,减轻MySQL压力
- 维护成本:Vue+Element UI组件库减少前端重复劳动
2.2 数据库设计要点
针对推荐系统的特点,数据库设计着重优化了用户行为存储:
sql复制CREATE TABLE `user_behavior` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
`user_id` INT NOT NULL COMMENT '用户ID',
`item_id` INT NOT NULL COMMENT '商品ID',
`behavior_type` TINYINT NOT NULL COMMENT '1浏览 2收藏 3购买',
`behavior_weight` DECIMAL(3,2) DEFAULT 1.00 COMMENT '行为权重',
`create_time` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
INDEX `idx_user_item` (`user_id`, `item_id`),
INDEX `idx_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
关键设计:
- 行为权重字段:购买=1.5,收藏=1.2,浏览=1.0(可配置)
- 联合索引:加速用户-商品查询
- 时间索引:用于近期行为分析
3. 协同过滤算法实现
3.1 数据预处理
原始用户行为数据需要转化为评分矩阵。我们采用加权策略:
java复制public double calculateScore(List<Behavior> behaviors) {
return behaviors.stream()
.mapToDouble(b -> {
switch (b.getType()) {
case PURCHASE: return 1.5;
case FAVORITE: return 1.2;
case VIEW: return 1.0;
default: return 0;
}
})
.average()
.orElse(0);
}
3.2 相似度计算优化
传统余弦相似度在稀疏数据下效果不佳,我们改进为:
java复制public double similarity(User u1, User u2) {
Set<Integer> commonItems = getCommonItems(u1, u2);
if (commonItems.isEmpty()) return 0;
double sum1 = 0, sum2 = 0, sum1Sq = 0, sum2Sq = 0, pSum = 0;
for (int itemId : commonItems) {
double r1 = getRating(u1, itemId);
double r2 = getRating(u2, itemId);
sum1 += r1;
sum2 += r2;
sum1Sq += Math.pow(r1, 2);
sum2Sq += Math.pow(r2, 2);
pSum += r1 * r2;
}
double num = pSum - (sum1 * sum2 / commonItems.size());
double den = Math.sqrt(
(sum1Sq - Math.pow(sum1, 2) / commonItems.size()) *
(sum2Sq - Math.pow(sum2, 2) / commonItems.size())
);
return den == 0 ? 0 : num / den;
}
这个改进版公式:
- 只计算共同评分项
- 引入均值中心化处理
- 添加分母为零的保护
3.3 推荐生成策略
采用混合推荐策略减轻数据稀疏问题:
java复制public List<Recommendation> recommend(User user) {
// 冷启动处理
if (user.getBehaviorCount() < 5) {
return hotRecommendationService.getTopN(10);
}
// 实时推荐(最近7天行为)
List<Recommendation> realtimeRecs = realtimeService
.getRecommendations(user.getId(), 5);
// 离线推荐(全量数据)
List<Recommendation> offlineRecs = offlineService
.getRecommendations(user.getId(), 5);
// 合并去重
return mergeRecommendations(realtimeRecs, offlineRecs);
}
4. 系统性能优化
4.1 缓存策略设计
使用Redis三层缓存结构:
- 本地缓存:Caffeine缓存用户最近推荐结果(有效期2小时)
- Redis缓存:
- 用户相似度矩阵(每日更新)
- 商品热度榜(实时更新)
- MySQL持久化:原始行为数据
java复制@Cacheable(value = "userRecs", key = "#userId")
public List<Recommendation> getRecommendations(Integer userId) {
// 缓存未命中时的数据库查询逻辑
}
4.2 定时任务设计
使用Spring Scheduler实现推荐更新:
java复制@Scheduled(cron = "0 0 3 * * ?") // 每天凌晨3点执行
public void updateOfflineRecommendations() {
log.info("开始离线推荐计算...");
long start = System.currentTimeMillis();
List<User> users = userService.getAllActiveUsers();
users.parallelStream().forEach(user -> {
List<Recommendation> recs = offlineRecommender
.generateForUser(user.getId());
recommendationService.saveBatch(recs);
});
log.info("离线推荐计算完成,耗时:{}ms",
System.currentTimeMillis() - start);
}
注意:并行流使用需谨慎,建议根据服务器核心数配置线程池:
properties复制spring.task.execution.pool.core-size=4 spring.task.execution.pool.max-size=8
5. 前端交互实现
5.1 推荐结果展示
采用瀑布流布局提升浏览体验:
vue复制<template>
<div class="recommend-container">
<el-row :gutter="20">
<el-col
v-for="(item, index) in recList"
:key="item.id"
:xs="12" :sm="8" :md="6">
<item-card
:data="item"
@click-favorite="handleFavorite"
@click-dislike="handleDislike"/>
</el-col>
</el-row>
<el-pagination
layout="prev, pager, next"
:total="total"
@current-change="loadPage"/>
</div>
</template>
5.2 用户反馈收集
通过隐式+显式反馈优化推荐:
javascript复制methods: {
handleFavorite(itemId) {
this.$axios.post('/api/feedback', {
type: 'LIKE',
itemId
}).then(() => {
this.$message.success('反馈已记录');
});
},
// 浏览深度埋点
recordViewDepth() {
const scrollDepth = window.scrollY / document.body.scrollHeight;
this.$axios.post('/api/behavior', {
type: 'VIEW_DEPTH',
depth: scrollDepth.toFixed(2)
});
}
}
6. 部署与监控
6.1 Docker部署方案
使用docker-compose编排服务:
yaml复制version: '3'
services:
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
volumes:
- ./mysql-data:/var/lib/mysql
ports:
- "3306:3306"
redis:
image: redis:7.0-alpine
ports:
- "6379:6379"
volumes:
- ./redis-data:/data
backend:
build: ./backend
ports:
- "8080:8080"
depends_on:
- mysql
- redis
environment:
SPRING_PROFILES_ACTIVE: prod
frontend:
build: ./frontend
ports:
- "80:80"
6.2 监控配置
使用Prometheus+Granfa监控关键指标:
java复制@Bean
public MeterRegistryCustomizer<PrometheusMeterRegistry> metrics() {
return registry -> {
registry.config().commonTags("application", "recommend-system");
// 监控推荐耗时
Timer.builder("recommend.cost.time")
.description("推荐计算耗时")
.register(registry);
// 监控缓存命中率
Counter.builder("cache.hit.rate")
.tag("type", "userRecs")
.register(registry);
};
}
7. 踩坑经验分享
-
相似度计算性能问题
- 初期全量计算用户相似度导致OOM
- 优化方案:先过滤最近活跃用户,再分批次计算
-
Redis缓存雪崩
- 推荐结果集中过期导致数据库压力骤增
- 解决方案:设置随机过期时间(基础30分钟±随机10分钟)
-
前端内存泄漏
- 瀑布流组件未及时销毁事件监听
- 修复方案:在Vue的beforeDestroy钩子中手动解绑
-
冷启动效果差
- 新用户看到不相关热门商品
- 改进:增加用户注册时的兴趣标签选择
这个项目让我深刻体会到,推荐系统不是简单的算法实现,而是需要结合业务场景不断调优的过程。特别是在资源有限的校园场景下,如何平衡算法复杂度和实际效果成为关键挑战。后续计划引入图神经网络挖掘用户-商品深层关系,进一步提升推荐精准度。
