资讯动态

基于SSM框架的短剧推荐系统设计与协同过滤实践

发布时间:2026/10/9 6:54:49 来源:尧图企业网站定制
做推荐系统项目很多人第一反应就是上Python、上Spring Boot、上微服务但我这次偏偏用了一套“老伙计”SSMSpring SpringMVC MyBatis把短剧推荐系统从零到一完整落地了。短剧这个赛道跟长视频不一样单集时长一两分钟用户刷起来完全是“短平快”的节奏推荐逻辑如果不贴合这个场景做出来就是花架子。这篇文章我把整个项目的设计思路、算法选型、SSM整合细节、以及那些踩过的坑都梳理出来不讲空理论全部是可复现的实操内容适合正在做Java毕设、或者刚入职想拿推荐系统练手的朋友参考。1. 项目概述与核心设计思路1.1 为什么选SSM而不是Spring Boot或Python项目的核心诉求是“短剧推荐”关键词拆开就是三个维度SSM框架、Java语言、推荐系统。很多同学一看到SSM就觉得过时了其实这个想法要辩证看。SSM里的Spring负责对象管理和事务SpringMVC负责路由分发MyBatis负责数据持久化这套组合的职责边界非常清晰尤其适合中小型Web项目。短剧推荐系统的数据量级说实话根本到不了需要分布式计算的级别单机MySQL加上合适的索引和缓存支撑几千甚至上万用户完全没问题。我选SSM还有一个实际原因面试和毕设场景下SSM能展示更多“手动造轮子”的能力。比如Spring Boot帮你自动配置了事务管理器、视图解析器但SSM里这些都需要自己显式声明这个过程反而能让你把框架的底层逻辑吃透。而且SSM的部署方式非常灵活一个普通的Tomcat就能跑不依赖内嵌容器迁移到云服务器或者学校机房的老机器都没压力。另外推荐算法部分我用Java而不是Python核心考量是工程一致性。虽然Python写协同过滤很舒服但一个Java Web项目里硬塞一个Python算法服务要么走HTTP接口通信要么走消息队列整体复杂度会上升一个级别。实际上基于物品的协同过滤用Java实现并不复杂核心就是矩阵运算和相似度计算Java的Stream API和集合框架完全够用。1.2 短剧场景与长视频推荐的本质差异短剧和长视频的推荐逻辑有非常大的区别。长视频用户可能花40分钟看一集行为信号非常稀疏但强度很高短剧一集只有1到3分钟用户一天可能刷几十部行为数据密集但单次行为的“粘性”偏低。这意味着播放完成度比播放次数更有价值。用户点开一部短剧但5秒就退出说明推荐结果不匹配这个信号要降权。连续观看多集是强正反馈。短剧是按“部”推荐但用户是否连续追完是衡量推荐质量的关键指标。时间衰减要设置得更激进。用户昨天的兴趣和今天的兴趣可能截然不同短剧用户很容易“上头”某一种题材但下个星期就完全换口味。所以我在设计推荐权重时没有套用通用的“浏览收藏点赞评论”加权公式而是重新定义了一套面向短剧场景的权重体系。具体怎么设计下一节详细说。1.3 推荐系统整体架构组成整个系统的架构我把它拆成四层来设计。最底层是数据层MySQL负责存储用户、短剧、行为日志和推荐结果Redis负责缓存热门前缀和实时榜单。上面一层是算法层主要是两个协同过滤引擎和一个热门榜兜底策略。再往上是业务层也就是SSM里的Service层负责把算法层算出的结果组装成业务对象比如过滤掉用户已经看过的短剧、做分页、记录曝光日志等。最顶层是接口层SpringMVC的Controller暴露给前端调用。这套架构没有使用独立的大数据组件所有计算都跑在应用内存里。我当时的设计原则是在数据量有限的情况下优先保证系统可运行、逻辑可解释、代码可维护。比如基于物品的协同过滤如果短剧数量在1万部以内物品相似度矩阵完全可以在启动时加载到内存用HashMap存储每次推荐直接查内存性能比跑数据库快几个数量级。2. 数据库设计与用户行为建模2.1 核心表结构设计与关系梳理数据库是推荐系统的地基表设计不好后面写算法全是坑。我总共设计了六张核心表用户表、短剧信息表、行为记录表、收藏表、评分/反馈表、推荐结果日志表。下面把关键字段列出来直接照着建就行。用户表其实没什么特殊的就是标准的id、username、password、nickname、avatar、create_time。但短剧信息表需要多花心思。短剧除了基本的title、cover_url、description、director、actor、category_id之外我还加了total_episodes总集数和avg_duration平均单集时长这两个字段。为什么因为推荐算法的相似度计算需要用到短剧的属性向量这两个字段是重要的数值型特征。行为记录表是整个推荐系统的灵魂。我设计的字段是id、user_id、drama_id、behavior_type、watch_seconds、watch_total_episodes、create_time。behavior_type我用int存储1代表点击2代表播完一集3代表连续看完超过10集4代表点赞5代表分享。这里有个很关键的细节watch_seconds是用户实际观看的秒数而不是播放器报告的自然秒数因为用户可能拖动进度条。评分表我一开始本来想省掉的后来发现它对协同过滤的质量影响极大。短剧没有像电影那样明确的星级评分所以我做了一个隐式评分映射把行为记录转化为5分制的分数。这个转化逻辑不是固定的我会根据行为组合动态算下面单独讲。2.2 行为权重计算方案与评分映射我之前说过短剧场景不能直接套用通用权重公式。我实际用的是这样一套打分逻辑单次行为的基础分点击算1分播完一集算2分连续追完5集以上算5分点赞算3分分享算4分。然后乘以一个时间衰减因子衰减公式我用的是半衰期为7天的指数衰减score base_score * 0.5^(days/7)。比如一个用户7天前分享了一部短剧那这条行为的实际得分就是4 * 0.5 2分。但光有基础分还不够。短剧用户的行为是连续性的所以我在计算用户对某部剧的最终偏好分时还叠加了一个“连续观看加成”。规则是如果用户在同一天连续观看了同一部短剧的多个单集每多一集额外加0.3分。这个逻辑可以通过SQL先group by user_id和drama_id统计每天的观看集数再乘以0.3。这套权重方案跑下来之后我明显发现一个好处那些“点开马上关掉”的无效行为会被自然压下去因为点击只有1分而且如果用户只看了一集就放弃不会触发连续观看加成得分很难超过那些完播率高的短剧。2.3 冷启动与降级策略冷启动是推荐系统绕不开的坑。新用户没有任何行为记录协同过滤直接哑火新短剧刚上线没有任何用户行为也不会被推荐出来。我的处理方式分两条线走。新用户冷启动默认走热门榜兜底。热门榜的计算不能只按播放总量排而是按“近7天活跃指数”排公式是热门指数 近7天完播数 * 0.5 近7天点赞数 * 0.3 近7天分享数 * 0.2。这个榜在Redis里维护一个有序集合每天凌晨定时任务重算一次。新用户注册后首先看到的就是这个榜单同时我会给他随机推荐3到5部不同题材的热门短剧观察他的点击反馈。新短剧冷启动给每部新剧打一个“新品加权”标签在推荐结果中插队展示但展示位不超过总推荐数的20%。同时设置观察期如果新剧上线48小时内的点击率低于某个阈值就自动降低加权等级。这套策略帮我解决了内容生态的冷启动问题新剧不至于石沉大海。3. 推荐算法核心实现与参数调优3.1 基于用户的协同过滤实现步骤基于用户的协同过滤UserCF的基本思想是找到和你兴趣相似的一群用户把他们喜欢的但你还没看过的短剧推荐给你。听起来简单但实际实现时有几个容易出问题的地方。第一步是构建“用户-物品”评分矩阵。我直接用2.2节算出来的偏好分作为矩阵元素存储在内存里使用Map实现MapLong, MapLong, Double外层key是userId内层key是dramaIdvalue是偏好分。第二步是计算用户之间的相似度。这里我没有用皮尔逊相关系数而是用余弦相似度。原因是短剧场景下用户行为矩阵非常稀疏皮尔逊在稀疏矩阵上计算会失真而余弦相似度更稳定。计算两个用户相似度时只取两个用户都产生过行为的短剧集合如果交集为空相似度直接为0。第三步是生成推荐列表。对目标用户的每个相似用户把他们喜欢的高分短剧按相似度加权累加排除目标用户已经看过的短剧最终按分数排序取TopN。N我设置的是20前端展示时再根据刷新位置分页。这个算法我用了一个小优化在计算相似度时只保留相似度大于0.3的用户关系把关系矩阵做了剪枝不然每次给用户做推荐时都要遍历全量用户性能会很差。3.2 基于物品的协同过滤与相似度计算细节ItemCF是我在项目里实际的主力算法。短剧场景和电商场景有个重要区别用户的兴趣变化快但短剧的“内容属性”稳定所以物品之间的相似度比用户之间的相似度更可靠。物品相似度计算我采用了两种特征混合的方式。第一种是基于行为共现即同时喜欢短剧A和短剧B的用户越多A和B越相似。公式是经典的余弦相似度similarity(A,B) |同时喜欢A和B的用户数| / sqrt(|喜欢A的用户数| * |喜欢B的用户数|)。第二种是基于内容属性的相似度我取的是题材、导演、主演这三个维度。题材用独热编码导演和主演用标签集合的Jaccard相似度计算。比如两部短剧都没有相同题材但导演相同就说明它们有一定的相似度。最终相似度是行为相似度和内容相似度的加权融合sim 0.7 * 行为相似度 0.3 * 内容相似度。实际实现时物品相似度矩阵我提前在项目启动阶段计算好存到一个ConcurrentHashMap里面避免每次请求都现场算。这里有个细节矩阵计算用到了Java的并行流短剧数量几千部时双层循环加并行流几秒钟就能跑完不需要依赖外部计算框架。3.3 混合推荐策略与实时降级方案只靠单一算法是不行的。我在项目里做了一个简单的加权混合策略推荐结果由三部分构成60%来自ItemCF算出的相似剧推荐30%来自UserCF算出的热门好友推荐10%来自热门榜兜底但这个比例不是固定的。我在Service层做了动态调整逻辑如果用户近7天行为记录很少就调低ItemCF的比例调高热门兜底比例如果用户行为密度很高说明他已经进入“上头”状态就提高UserCF的比例给他挖一些同好圈层里口碑好的剧打破信息茧房。降级方案也很重要。如果Redis挂了推荐接口不能跟着挂。我把热门榜在应用内存里也保留了一份副本每5分钟同步一次。Redis不可用时代码直接切到内存副本保证推荐接口的高可用。这种“本地冗余”的思路虽然土但在SSM这种单体架构下非常可靠。4. SSM框架整合与MyBatis高级应用4.1 Spring SpringMVC MyBatis配置实践要点SSM整合的配置文件当时花了我不少时间踩了几个隐形的坑这里把关键配置思路分享出来。Spring的配置文件管数据源、事务管理器、MyBatis的SqlSessionFactorySpringMVC的配置文件管Controller扫描和视图解析器MyBatis的Mapper接口用MapperScan注解扫描进Spring容器。一个我特别想提醒的点是事务配置。短剧推荐系统里行为记录写入、评分更新、推荐日志记录这几个操作涉及多表写入必须保持事务一致。我在Spring配置文件里启用了注解驱动的事务管理tx:annotation-driven transaction-managertransactionManager /然后在Service层的关键方法上加了Transactional(rollbackFor Exception.class)。rollbackFor一定要声明否则默认只回滚RuntimeException检查异常导致的事务不一致问题排查起来很痛苦。MyBatis的驼峰映射配置也容易被忽略。数据库字段是下划线风格比如create_timeJava属性是createTime。如果不在mybatis-config.xml里开启mapUnderscoreToCamelCase每次查询结果映射都会因为字段对不上而返回null而且这种Bug不会报错只会让你在调试时百思不得其解。4.2 MyBatis动态SQL处理推荐场景的复杂查询推荐系统里有几个复杂的查询场景用MyBatis动态SQL处理最合适。最典型的是分页推荐查询需要同时满足过滤用户已看过的剧、按照推荐优先级排序、按分组去重这三个条件。我写了一个比较经典的多条件动态SQL这里直接给出核心片段select idselectRecommendList resultTypeDramaVO SELECT d.id, d.title, d.cover_url, d.description, d.total_episodes, d.category_id, r.score FROM ( SELECT drama_id, MAX(score) AS score FROM ( SELECT drama_id, score FROM drama_similarity WHERE user_id #{userId} UNION ALL SELECT drama_id, score FROM hot_drama_rank ) AS combined_score GROUP BY drama_id ) AS r INNER JOIN drama_info d ON d.id r.drama_id LEFT JOIN user_behavior ub ON ub.user_id #{userId} AND ub.drama_id d.id where ub.id IS NULL if testcategoryId ! null AND d.category_id #{categoryId} /if if testexcludeDramaIds ! null and excludeDramaIds.size() 0 AND d.id NOT IN foreach collectionexcludeDramaIds itemdid open( separator, close) #{did} /foreach /if /where ORDER BY r.score DESC LIMIT #{offset}, #{pageSize} /select这个SQL的核心思路是先用子查询把多路推荐源的分数合并取最大值再LEFT JOIN用户行为表用ub.id IS NULL过滤掉用户已看过的短剧。动态SQL里的IF标签和FOREACH标签用来处理可选条件代码层面的判断逻辑就可以少写很多。4.3 Redis缓存与本地会话级缓存搭配推荐系统的缓存设计我分了两层。第一层是Redis用来缓存热门榜、用户推荐列表、短剧详情基础信息。key的设计遵循“业务前缀:方法名:参数”的规范比如用户推荐列表的key是rec:user:list:123:1:10其中123是userId1是页码10是页大小。热门榜的key是rec:hot:daily每天定时任务生成后写入过期时间设置为24小时加一个随机偏移量。第二层是Guava的本地缓存用来缓存算法层计算出来的中间结果。比如同一用户在一分钟内多次刷新推荐页如果每次都去计算相似度做排序纯属浪费性能。我用Guava的CacheBuilder构建了一个带过期时间的本地缓存最大容量设置10000条过期时间5分钟。代码实现如下CacheLong, ListDramaVO recCache CacheBuilder.newBuilder() .maximumSize(10000) .expireAfterWrite(5, TimeUnit.MINUTES) .build(); public ListDramaVO getRecommendList(Long userId) { ListDramaVO cached recCache.getIfPresent(userId); if (cached ! null) { return cached; } ListDramaVO result doComputeRecommend(userId); recCache.put(userId, result); return result; }两层缓存有个分工Redis缓存是面向多实例共享的但SSM项目通常只有一个实例所以Guava缓存反而用得更顺手。如果以后项目水平扩展成多实例只需要把Guava缓存那层去掉Redis缓存顶上即可。5. 前端展示与管理后台设计要点5.1 推荐结果展示策略与前端交互细节推荐系统的最终价值要落到前端展示上。短剧的推荐页我设计成双列瀑布流卡片每张卡片展示封面、标题、集数和题材标签。这里有个细节卡片上的“推荐理由”很重要比如显示“因为你看过《XXX》”这个功能能明显提升点击率。实现方式是在Service层组装数据时把推荐来源短剧的标题拼接到返回对象里。前端接口设计上我用了三个接口来支撑推荐页获取推荐列表、上报曝光、上报点击。上报曝光是前端在卡片出现在可视区域时调用的点击事件则是在用户真正点击卡片时上报。这两个接口的数据最终会进入行为记录表成为算法重算的原始材料。曝光和点击分开上报是为了后续计算点击率CTR做数据回收。交互细节上我做了下拉刷新加载更多和滑动自动加载两种模式。短剧用户刷得很快分页加载如果用传统“下一页”按钮交互就太傻了用无限滚动配合下拉刷新用户粘性会高很多。后端分页接口返回时除了返回本页数据我还返回了一个hasMore字段前端根据这个字段控制是否继续发请求。5.2 管理后台的推荐规则配置模块管理后台的作用不只是展示数据更重要的是给运营人员一个“干预推荐”的入口。我的后台里做了一个简单的规则配置页面可以配置三件事推荐算法权重、热门榜计算周期、推荐数量上限。算法权重配置是重中之重。我给每个算法分配了一个权重字段存储在数据库的suggestion_config表里。运营人员在页面上把ItemCF权重从60调到40点保存后后台会发一个请求强制刷新所有用户的本地推荐缓存让改动立即生效。这个功能实现起来不复杂但业务价值很大因为短剧运营经常会根据热播情况调整推荐策略。另外后台还做了简单的数据看板Top10短剧排行、用户增长曲线、各题材点击分布。这些数据通过定时任务聚合到一张汇总表页面直接查汇总表避免每次都全表扫描行为记录表导致慢查询。6. 常见问题排查与性能优化实录6.1 线上实际问题排查记录我必须要说这个项目上线后遇到的真正“高光时刻”基本都是Bug排查带来的教训。最经典的一个问题是推荐列表偶尔会出现空指针异常。排查下来发现问题出在用户行为记录为空时ItemCF的评分矩阵虽然正确初始化了但UserCF的相似用户计算返回了一个不可变空集合后续在遍历这个空集合时调用了某个方法触发了NPE。修复方式是判断集合为空时直接降级到热门榜。另一个典型问题是SQL慢查询。推荐页的SQL初版写法用了多个IN查询和子查询嵌套短剧数据量到3000部时单次查询耗时达到了2.8秒。后来通过EXPLAIN分析发现联合索引没有被正确命中。修复方式是在drama_similarity表的(user_id, score)字段上建联合索引在user_behavior表的(user_id, drama_id)字段上建唯一索引并把一个多层嵌套子查询重写成JOIN查询时间从2.8秒降到了180毫秒。6.2 内存管理与推荐矩阵计算的性能优化协同过滤的相似度矩阵是内存消耗大户。1万部短剧的相似度矩阵如果你存全量稠密矩阵就是1万乘以1万乘以8个字节算下来800MB服务器直接爆内存。我的优化方案是只存储每个短剧的Top50相似短剧其他相似度较低的条目直接丢弃。这样矩阵规模从1亿条缩减到50万条内存占用降到几十MB。计算阶段的优化我用了两个技巧。第一用Java Feature的并行流把双层循环拆成多个线程并行计算四核CPU下计算时间缩短了一半。第二响应式数据在计算时使用了原始类型数组double[]而不是Double包装类避免自动装箱带来的GC压力。说实话用原始类型数组写起来丑了一些但在这个场景下性能红利非常明显。6.3 高频问题速查表我把项目期间遇到的高频问题整理了一下方便后来人少走弯路。问题现象可能原因解决思路推荐列表固定不变本地缓存过期时间设置过长调短expireAfterWrite时间新入库短剧无法被推荐物品相似度矩阵没有重算在短剧新增后触发矩阵增量更新推荐结果全是热门剧UserCF和ItemCF权重失衡检查用户行为数据量增加冷启动策略权重行为记录重复写入唯一索引缺失在behavior表加(user_id, drama_id, behavior_type, create_time)联合唯一索引用户注销后推荐崩溃算法层未处理用户消失场景算法计算前先校验用户状态异常时降级热门榜浏览量高但完播率低推荐内容与用户兴趣匹配度不足调低点击权重、调高完播权重最后一个问题值得多说两句。很多推荐系统把点击率当成北极星指标但对短剧来说完播率才是真正的质量信号。点击只能说明封面和标题吸引人完播才能说明内容本身对味。我在后台数据看板里单独拉了一条“完播榜”和“播放榜”并列展示运营看到两个榜单的差异后反而能更快地发现哪些剧是“标题党”哪些是“口碑剧”。6.4 从SSM迁移到Spring Boot的扩展思路这个项目跑稳定之后我又对后续演进做了一些规划。如果并发量上来SSM单体架构撑不住了最平滑的迁移路径是拆成两个微服务一个是内容管理服务一个是推荐算法服务。推荐算法服务可以单独打包成Spring Boot应用部署独立实例通过Feign或RestTemplate与内容管理服务通信。迁移过程中要注意原来的本地Guava缓存要改为分布式缓存推荐结果从Redis里读取。算法重算的定时任务要加分布式锁防止多实例重复计算。数据库连接池参数要根据微服务实例数重新估算避免连接数打满。不过说实话如果不是业务体量真到了那一步SSM这套架构完全扛得住短剧推荐这个量级。框架没有绝对的好坏只有适不适合当前的业务场景。硬上的微服务反而会让一个简单项目变得复杂难维护这一点在面试聊项目的时候也是加分项。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑