
短剧这两年有多火不用我多说。一分钟一集、竖屏播放、反转密集用户在地铁上、午休时打开App就能刷几十集。可内容一多选择就成了问题——观众不知道下一条该看什么平台也不知道怎么留住人。这时候推荐系统就派上用场了。我自己动手用SSM框架搭了一个基于Java的短剧推荐系统把用户偏好收集、标签内容匹配、协同过滤、热门榜单兜底整条链路都走了一遍。这篇文章会从技术选型、数据库设计、算法实现到SSM三层代码落地把整个项目的设计过程、关键代码和踩坑记录完整整理出来。适合正在做Java课程设计、毕业设计的同学也适合想用SSM入门推荐系统、或者想搞清楚推荐逻辑到底怎么落地的开发新手。1. 项目概述与核心思路拆解1.1 技术选型为什么选SSM而不是直接上Spring Boot现在新项目普遍用Spring Boot但SSMSpring Spring MVC MyBatis依然是理解Java企业级开发底层逻辑的最好路径。SSM把事务管理、MVC路由、SQL持久化这三个层面的分工拆得特别清楚尤其MyBatis这种半自动ORM框架SQL由自己手写控制在做推荐系统这种需要大量个性化查询、动态拼接SQL的场景里反而比JPA好用得多。选SSM还有一个现实原因很多高校的Java课程设计、毕业设计还在用这个技术栈网上资料丰富碰到问题也容易搜到解决方案。对新手来讲SSM的配置过程虽然繁琐但它会逼你把Spring容器、拦截器、事务边界这些基础概念真正搞明白。等你手写一遍SSM整合再去看Spring Boot的自动配置就会觉得很多东西都是纸老虎。1.2 系统功能模块怎么拆短剧推荐系统本质上是一个内容分发系统用户进入首页系统根据他的行为历史推荐一批短剧用户点开观看、点赞、收藏、分享这些行为再回流到系统更新下一次推荐。整个系统按角色和数据流向拆成五个核心模块用户模块注册、登录、个人偏好设置只看甜宠还是偏爱悬疑。短剧管理模块后台维护短剧基本信息、封面、集数、简介、上线状态。分类与标签模块短剧按题材分类甜宠、逆袭、悬疑、战神、家庭同时打上更细粒度的标签重生、穿越、赘婿、复仇、双洁、虐恋。行为收集模块记录用户观看时长、点击、点赞、收藏、分享、评分等行为数据。推荐引擎模块基于内容推荐、协同过滤、热门榜三种策略组合产出推荐列表。推荐引擎是核心但很多初学者的误区是一上来就写算法把数据库表和用户行为设计搭得太轻。实际上推荐系统的地基是数据——没有干净完整的用户行为记录任何算法都跑不出效果。所以我建议先想清楚谁能产生数据、数据存在哪、哪些数据能复用再动算法。1.3 推荐策略的组合逻辑单靠一种推荐策略很难撑住一个系统。我的方案是三路召回加统一排序第一路基于内容标签匹配适合新用户冷启动第二路基于用户协同过滤找口味相似的人看过的短剧第三路是热门榜兜底不管什么用户都得有东西可推。三路结果拉出来之后再去掉用户已经看过的、去掉下线的最后按综合得分排序。这个过程在实际系统里叫召回和精排我做的是简化版但思路能对上。2. 数据库设计与核心表结构2.1 基础信息表设计先把基础四张表的DDL列出来这是我反复调过之后的结构字段不多但每张表都有它存在的必要性。-- 用户表 CREATE TABLE user ( id bigint NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL, password varchar(255) NOT NULL, preferred_tags varchar(255) DEFAULT NULL COMMENT 主动选择的偏好标签逗号分隔, register_time datetime NOT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 短剧表 CREATE TABLE drama ( id bigint NOT NULL AUTO_INCREMENT, title varchar(100) NOT NULL, cover_url varchar(255) DEFAULT NULL, description text COMMENT 剧情简介, category_id bigint NOT NULL COMMENT 分类如甜宠、悬疑、战神, total_episodes int DEFAULT NULL, status tinyint NOT NULL DEFAULT 1 COMMENT 1上架 0下架, created_at datetime NOT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 标签表 CREATE TABLE tag ( id bigint NOT NULL AUTO_INCREMENT, name varchar(50) NOT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 短剧-标签关联表 CREATE TABLE drama_tag_rel ( drama_id bigint NOT NULL, tag_id bigint NOT NULL, PRIMARY KEY (drama_id, tag_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;分类和标签要分开存原因是分类是粗粒度的一个领域划分标签才是描述内容特征的细粒度信息。比如《闪婚老公是大佬》分类上属于甜宠但标签可以是闪婚、隐婚、霸总、双洁、先婚后爱。推荐算法里做相似度计算主要靠标签分类可以用来做粗筛或频道页聚合。2.2 用户行为表设计推荐系统的核心资产是行为数据。如果说表结构和算法是骨架行为数据就是血液。我的行为表设计成统一结构用behavior_type字段区分行为类型而不是每种行为建一张表这样扩展新行为类型时不用改表结构。CREATE TABLE user_behavior ( id bigint NOT NULL AUTO_INCREMENT, user_id bigint NOT NULL, drama_id bigint NOT NULL, behavior_type tinyint NOT NULL COMMENT 1点击 2观看 3点赞 4收藏 5分享 6评分, duration_seconds int DEFAULT NULL COMMENT 观看时长观看行为才有, score_value tinyint DEFAULT NULL COMMENT 评分值评分行为才有, behavior_time datetime NOT NULL, PRIMARY KEY (id), KEY idx_user (user_id), KEY idx_drama (drama_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这张表的数据量会涨得很快所以必须给user_id和drama_id建索引。在实际部署里行为表的查询模式非常固定查某个用户最近的行为、查某部剧被哪些用户看过。建立联合索引或单列索引就能扛住百万级数据量。如果再大可以考虑按月分表或者把行为数据异步写入消息队列但课程设计和中小型项目不需要上到那个复杂度。隐式反馈的量化是这里的关键。观看时长和完播率是最重要的信号权重最高点赞、收藏、分享是主动的正向反馈点击行为权重最弱评分行为权重高但用户很少触发。我用的加权公式是score 观看行为权重×min(duration/1800, 1)×3 点赞×4 收藏×5 分享×6 评分×5具体权重可以调但思路是行为越主动、权重越高。2.3 推荐结果表要不要建我建议建一张推荐结果表但不是用来存用户最终看到的完整列表而是存每条推荐剧集的计算得分。这样用户每次请求推荐接口直接查表按得分排序不需要实时跑算法。CREATE TABLE recommend_result ( user_id bigint NOT NULL, drama_id bigint NOT NULL, score decimal(10,4) NOT NULL, strategy_type tinyint NOT NULL COMMENT 1内容推荐 2协同过滤 3热门, create_time datetime NOT NULL, PRIMARY KEY (user_id, drama_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这张表我建议用定时任务每天凌晨更新一次。实时推荐虽然听起来高级但短剧推荐的更新时效性并不需要做到分钟级用户今天看的剧明天才反映到推荐里完全可接受。离线算好、线上查表这是性价比最高的方案也是很多工业级推荐系统的简化版思路。3. 推荐算法实现与计算细节3.1 基于标签的内容推荐把短剧变成向量内容推荐的核心思路是给每个短剧打标签给用户建立一个标签偏好向量通过向量相似度找出用户可能感兴趣的短剧。先看怎么给短剧建向量。假设系统里有N个标签每部短剧用一个N维向量表示向量的第i个位置表示这部短剧与第i个标签的关联强度。关联强度最简单的算法是0或1——打上了这个标签就是1没打上就是0。但更精细的做法是引入TF-IDF思路如果一个标签在很多短剧里都出现那这个标签区分度就低权重应该降下来。用户标签偏好向量则由用户的行为来构建。用户看过很多甜宠剧那甜宠相关标签的权重就高。我用一段Java代码实现标签向量的构建和余弦相似度计算public class ContentBasedRecommender { /** * 构建用户标签偏好向量 * 根据用户行为累加标签权重观看权重高点击权重低 */ public MapLong, Double buildUserTagVector(Long userId, ListUserBehavior behaviors, MapLong, ListLong dramaTagMap) { MapLong, Double tagVector new HashMap(); for (UserBehavior behavior : behaviors) { ListLong tagIds dramaTagMap.getOrDefault(behavior.getDramaId(), Collections.emptyList()); double weight getBehaviorWeight(behavior.getBehaviorType(), behavior.getDurationSeconds()); for (Long tagId : tagIds) { tagVector.put(tagId, tagVector.getOrDefault(tagId, 0.0) weight); } } return tagVector; } /** * 余弦相似度计算 */ public double cosineSimilarity(MapLong, Double userVector, MapLong, Double dramaVector) { double dotProduct 0.0; double userNorm 0.0; double dramaNorm 0.0; // 点积只计算用户向量里出现过且短剧向量里也有的标签 for (Map.EntryLong, Double entry : userVector.entrySet()) { Long tagId entry.getKey(); double userWeight entry.getValue(); double dramaWeight dramaVector.getOrDefault(tagId, 0.0); dotProduct userWeight * dramaWeight; userNorm userWeight * userWeight; } for (Double weight : dramaVector.values()) { dramaNorm weight * weight; } if (userNorm 0 || dramaNorm 0) { return 0.0; } return dotProduct / (Math.sqrt(userNorm) * Math.sqrt(dramaNorm)); } private double getBehaviorWeight(int behaviorType, Integer durationSeconds) { switch (behaviorType) { case 1: return 0.5; // 点击 case 2: // 观看按时长加权 if (durationSeconds null || durationSeconds 0) return 1.0; return Math.min(durationSeconds / 1800.0, 1.0) * 3.0; case 3: return 4.0; // 点赞 case 4: return 5.0; // 收藏 case 5: return 6.0; // 分享 case 6: return 5.0; // 评分 default: return 0.0; } } }这段代码看着简单但已经覆盖了内容推荐的三个关键点行为权重差异化、时长归一化、向量相似度。有个细节必须注意计算用户向量范数时只遍历用户向量里的标签就能算点积但用户范数需要在同一个循环里累加短剧范数单独算。这两块不能混我刚开始写的时候就在这犯过错算出来的相似度全是错的。3.2 协同过滤用别人的口味补全你的片单内容推荐有一个问题——它永远只会推荐用户已经喜欢的那类短剧很难帮用户发现新题材。协同过滤正好弥补这个缺陷找到和你口味相似的其他人把他们看过而你没看过的短剧推荐给你。基于用户的协同过滤UserCF分两步走第一步先算用户相似度矩阵。用户相似度我用Jaccard相似度来算——两个用户共同看过的短剧数除以两个用户看过的短剧并集数。这个公式比余弦相似度直观也适合短剧这种大量长尾物品的场景。public double jaccardSimilarity(SetLong userAViewed, SetLong userBViewed) { if (userAViewed.isEmpty() || userBViewed.isEmpty()) { return 0.0; } SetLong intersection new HashSet(userAViewed); intersection.retainAll(userBViewed); SetLong union new HashSet(userAViewed); union.addAll(userBViewed); return (double) intersection.size() / union.size(); }第二步是找兴趣最相似的K个用户把他们的观看记录加权汇总对目标用户没看过的短剧打分排序。权重就是用户相似度本身相似度越高他的观看记录越值得参考。实际跑下来我会给相似用户的观看记录再加一个时间衰减因子。用户三个月前看的剧和昨天看的剧参考价值完全不同。时间衰减我用的公式是exp(-days/30)30天为一个半衰期超过30天的行为权重降到0.37左右。这个衰减系数在冷启动期和新用户多的时候效果特别明显能让推荐列表始终保持新鲜。3.3 排序细节别用冒泡排序去做推荐结果召回阶段可能拿到几十条候选短剧排序直接用JDK提供的Collections.sort或者Java 8的Comparator就能解决。我在代码审查的时候见过有人自己写冒泡排序给推荐结果排序数据量小还能忍候选集一多就是白白浪费CPU。ListRecommendItem candidates new ArrayList(); // 填充候选数据... candidates.sort((o1, o2) - Double.compare(o2.getScore(), o1.getScore()));这里有个面试常考但实际也常用的细节Comparator的comparing方法配合thenComparing可以做多级排序。比如先按综合得分降序得分相同时按短剧的上架时间降序让新剧有更多曝光机会。candidates.sort(Comparator .comparingDouble(RecommendItem::getScore).reversed() .thenComparing(RecommendItem::getPublishTime).reversed());排完序之后还要做一步去重和过滤用户已经看过的短剧一定要去掉状态为下架的也要去掉。这一步可以放在SQL里做也可以放在内存里做。放在SQL里做减少数据传输量但动态拼接条件会复杂一些放在内存里做逻辑直观适合数据量不大时。我的选择是SQL过滤一部分、内存过滤一部分两边都做保证接口返回的数据干净。4. SSM三层架构实现与关键代码4.1 Mapper层动态SQL是MyBatis的灵魂SSM架构里MyBatis的Mapper层是数据访问的边界。推荐系统最常碰到的SQL需求是“查询某个用户没看过的短剧并且按某种条件过滤”。这个需求天然需要动态SQL。select idselectCandidateDramas resultTypecom.example.entity.Drama SELECT d.* FROM drama d WHERE d.status 1 AND d.id NOT IN ( SELECT drama_id FROM user_behavior WHERE user_id #{userId} AND behavior_type IN (1, 2, 3, 4, 5) ) if testcategoryId ! null AND d.category_id #{categoryId} /if if testtagIds ! null and tagIds.size() 0 AND d.id IN ( SELECT drama_id FROM drama_tag_rel WHERE tag_id IN foreach collectiontagIds itemtagId open( separator, close) #{tagId} /foreach ) /if ORDER BY d.created_at DESC LIMIT #{limit} /select这个SQL的NOT IN子查询是很自然的思路但有个隐患如果user_behavior表数据量特别大NOT IN的性能会变差。实际项目里可以改成LEFT JOIN ... WHERE ub.id IS NULL的写法效果一致但性能更好。我写SQL的时候也会把这两者的性能差异讲清楚这是MyBatis面试里经常问到的一个点。Mapper层还有一个常见坑是N1查询问题。推荐结果里有10部短剧每部短剧查一次标签和分类信息那就是101次查询。解决方式是连表查询一次性把标签带出来或者用collection标签做嵌套结果映射。我最终选择了一条SQL把短剧和标签都查出来resultMap idDramaWithTagMap typecom.example.entity.Drama id propertyid columnid/ result propertytitle columntitle/ result propertycoverUrl columncover_url/ result propertydescription columndescription/ collection propertytags ofTypejava.lang.String result columntag_name/ /collection /resultMap select idselectDramaWithTags resultMapDramaWithTagMap SELECT d.*, t.name AS tag_name FROM drama d LEFT JOIN drama_tag_rel dtr ON d.id dtr.drama_id LEFT JOIN tag t ON dtr.tag_id t.id WHERE d.id IN foreach collectionids itemid open( separator, close) #{id} /foreach /select4.2 Service层推荐引擎的编排逻辑Service层是真的产出推荐结果的地方。我把推荐引擎设计成一个门面类内部组合三个推荐策略。这样各策略实现自己的逻辑门面负责调度和合并结果。Service public class RecommendFacade { private final ContentBasedRecommender contentBasedRecommender; private final UserBasedCollaborativeRecommender cfRecommender; private final HotDramaRecommender hotRecommender; public ListRecommendItem recommend(Long userId, int limit) { // 1. 先取用户行为集合没行为直接走热门 ListUserBehavior behaviors behaviorService.listRecentBehaviors(userId); if (behaviors.isEmpty()) { return hotRecommender.recommend(userId, limit); } // 2. 三路召回 ListRecommendItem contentItems contentBasedRecommender.recommend(userId, behaviors, limit); ListRecommendItem cfItems cfRecommender.recommend(userId, behaviors, limit); ListRecommendItem hotItems hotRecommender.recommend(userId, limit); // 3. 合并同一部剧取不同策略得分的加权和 MapLong, RecommendItem merged new LinkedHashMap(); merge(merged, contentItems, 0.4); merge(merged, cfItems, 0.4); merge(merged, hotItems, 0.2); // 4. 排序、截断 return merged.values().stream() .sorted(Comparator.comparingDouble(RecommendItem::getScore).reversed()) .limit(limit) .collect(Collectors.toList()); } private void merge(MapLong, RecommendItem target, ListRecommendItem items, double weight) { for (RecommendItem item : items) { RecommendItem old target.get(item.getDramaId()); if (old null) { item.setScore(item.getScore() * weight); target.put(item.getDramaId(), item); } else { old.setScore(old.getScore() item.getScore() * weight); } } } }加权合并的权重比例0.4、0.4、0.2是我在真实小规模用户测试中调出来的。内容推荐和协同过滤各占四成热门占两成既能保证个性化也给了新剧露脸的机会。如果某个策略短时间表现不好调一下权重比重比改算法本身要快得多。Service层还需要处理事务问题。用户行为写入时要保证一致性——不能出现行为记录写了一半、推荐结果表更新失败的情况。我给行为写入方法加上Transactional注解同时注意事务只应该包住数据修改操作不要在事务里调用远程接口或执行耗时的推荐计算否则事务长时间不提交数据库连接池会被占满。这个坑在开发环境不明显一压测就暴露。4.3 Controller层与前端接口设计Controller层在这个项目里相对薄主要工作是参数接收、鉴权、调用Service、结果返回。我设计了四个核心接口POST /api/register 注册 POST /api/login 登录 GET /api/recommend 获取推荐列表 POST /api/behavior 上报用户行为推荐接口返回的JSON结构是这样{ code: 200, data: [ { dramaId: 1001, title: 闪婚老公是大佬, coverUrl: https://example.com/cover/1001.jpg, description: 简介文本, tags: [闪婚, 霸总, 先婚后爱], score: 23.54, reason: 因为你喜欢先婚后爱、霸总 } ] }这里“reason推荐理由”是体验上的一个加分项告诉用户为什么推荐这部剧比冷冰冰地丢一个列表出来更容易建立信任感。用户如果觉得推荐得不准也会产生明确的反馈动机——这点对后续收集负反馈信号很有帮助。Controller层还有一个容易被忽略但很重要的点防止爬虫。短剧推荐系统的数据不算特别敏感但接口如果被爬虫大量刷会导致推荐结果表被查询到数据库压力增大。我在拦截器里做了一个简单的频率限制同一个token每分钟最多请求30次超出直接返回429。这个策略用Spring MVC的HandlerInterceptor实现几十行代码效果立竿见影。5. 性能优化与实战调优5.1 冷启动问题新用户和新短剧怎么办冷启动是推荐系统里怎么也绕不开的问题。新用户没有任何行为数据内容推荐和协同过滤都跑不起来直接用热门榜兜底是最稳妥的方案。热门榜的实现不能简单按总播放量排。播放量会被老剧霸榜新剧永无出头之日。我的热门榜算法是把播放量、观看人数、点赞数、收藏数、时间衰减因子综合起来热度随时间指数衰减。简化公式如下热度分数 播放量×0.5 点赞数×3 收藏数×5 分享数×8 时间衰减 1 / (1 0.5 × 上架天数) 最终热度 热度分数 × 时间衰减这套公式的效果是新剧上架前三天有强势加权即使播放量不高也能冲进榜单老剧如果没有持续的新增播放热度分数会被时间衰减慢慢拉低。我实际用这套逻辑跑了两个月的数据新剧曝光率明显提高用户完播率也上升了。新短剧的冷启动处理则靠后台打标签。运营人员在录入短剧时标签打得越全、越准新剧进入内容推荐候选池的速度就越快。如果一个新剧一个标签都没有推荐算法永远找不到它只能靠榜单硬推。所以我在后台管理端做了标签补全的提示功能没有标签的短剧后台列表置顶提醒运营人员补录。5.2 缓存设计哪些数据适合提前算好推荐系统性能最大的敌人是每次请求都实时算一遍相似度。我给系统加了三个层面的缓存。第一层是数据库查询结果缓存。短剧基本信息、标签信息这类很少变化的基础数据第一次查询后放进缓存设置30分钟的过期时间。用Spring Cache配合简单的ConcurrentHashMap就能实现不需要引入Redis这样重的中间件。第二层是推荐结果缓存。推荐结果表本来就每天凌晨更新但用户量大时并发查这个表还是会有压力。我的做法是把热门推荐结果直接缓存在内存里热门榜单全站用户看到的是同一份数据这个缓存的性价比是最高的。第三层是用户相似度矩阵缓存。协同过滤计算中用户相似度矩阵是最耗时间的部分——需要遍历所有用户的行为记录。这个矩阵我每天只算一次算完放在缓存里当天推荐请求直接读取。实时计算用户相似度的后果我试过两万用户的数据量就足以让推荐接口响应慢到一个无法接受的程度。5.3 定时任务离线计算的执行策略定时任务我用Spring自带的Scheduled注解实现没有引入Quartz因为这个项目的定时任务足够简单。每天凌晨2点执行一次错开用户访问高峰。Component public class RecommendTask { private final RecommendService recommendService; // 每天凌晨2点执行 Scheduled(cron 0 0 2 * * ?) public void refreshRecommendResults() { // 1. 计算用户相似度矩阵 MapLong, MapLong, Double similarityMatrix recommendService.calcUserSimilarityMatrix(); // 2. 批量计算每个用户的推荐结果 ListLong userIds userService.listAllUserIds(); for (Long userId : userIds) { ListRecommendItem items recommendService.recommendByStrategies(userId, similarityMatrix); recommendService.saveRecommendResults(userId, items); } } }这个定时任务在数据量大以后会跑得比较久需要监控执行时长。我现在用的方式是把任务拆成三步算出矩阵后先落盘内存临时表再批量给用户算推荐最后一次性写入推荐结果表。如果某一步失败第2天还能重新执行不会影响线上用户的正常访问。5.4 Java环境与部署细节SSM项目部署时常见的环境变量问题也得提一嘴一台机器上装多个JDK版本时JAVA_HOME配置不好容易导致项目起不来。我一般把不同版本JDK装在独立目录通过修改/etc/profile或bashrc中的JAVA_HOME与PATH来切换。Spring的SSM项目通常要求JDK 8或JDK 11JDK 8就能跑得很好不需要盲目上高版本。部署时用打war包放到Tomcat的webapps目录或者用mvn clean package打可执行jar包都可以。SSM的传统方式是war包Spring Boot式的方式是jar包但既然用SSM就按SSM的规矩来别把两种方式混在一起容易出一些莫名其妙的类加载问题。6. 常见问题与排查技巧实录6.1 中文乱码SSM项目最常见的坑SSM项目中文乱码的根源就三个地方请求参数编码、数据库连接编码、页面响应编码。我遇到最多的是MySQL连接URL忘加characterEncodingutf8导致存进去的中文变成问号。排查方法很简单先看数据库表数据的实际存储内容如果是问号说明写入时就乱码了问题出在连接串如果数据库里正常但页面显示乱码问题出在响应编码过滤器。filter filter-nameencodingFilter/filter-name filter-classorg.springframework.web.filter.CharacterEncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param /filter这个过滤器要放在web.xml过滤器链的最前面否则请求参数在进入Controller之前就已经被错误解码了。所有表统一用utf8mb4字符集这个字符集支持表情符号短剧评论和描述里经常有emoji表情用utf8存会直接报错或丢字符。6.2 MyBatis N1查询问题推荐列表页加载出的每个短剧都要额外查询它的标签和分类导致SQL执行次数剧增这是N1问题。排查方法是在MyBatis配置里开启SQL日志看看一次请求打了多少条SQL到数据库。我记得第一次测试的时候推荐10部短剧居然打出了21条查询语句。后来改成连表查询加collection映射一次请求降到2条SQL响应时间从800毫秒降到120毫秒。6.3 懒加载导致的LazyInitializationExceptionSSM项目里配置了Hibernate才容易遇到LazyInitializationException但MyBatis也有类似问题。事务提交后Session关闭再去访问延迟加载的属性就会报错。解决办法有两个方向一是在Service层事务方法内把需要的数据全部查好二是在XML中显式指定fetchTypeeager。我推荐第一种——保持事务边界的清晰Service层返回的数据一定是完整的Controller层就不需要关心数据加载状态。6.4 用户列表里行为数据量太大怎么办当用户行为记录累积到一定规模后协同过滤计算会变得异常缓慢。我的排查日志里两万用户、四十万行为记录的场景下全量计算一次相似度矩阵需要将近十分钟。我到后期把用户按活跃度分了两批处理活跃用户每天更新推荐结果非活跃用户每三天更新一次。活跃度判定标准是近7天是否有行为。这个策略直接让定时任务的执行时间缩短到五分钟以内推荐效果几乎没有下降。6.5 冷启动用户推荐列表不稳定问题新注册的用户没有行为数据推荐系统总是返回同一份热门排行榜。用户刷了几次之后再打开发现推荐列表一点变化都没有体验非常差。我的处理方案是给热门榜增加随机扰动因子同一份热门候选集每次排序时乘一个0.9到1.1之间的随机系数。这样同一个用户多次刷新推荐顺序会有细微变化但整体内容仍然是热门的既保证了稳定性又不至于太死板。下面把问题排查方法整理成一个速查表方便大家对照处理问题现象排查方向解决方案中文乱码数据库连接串、Filter顺序加characterEncodingutf8Filter放最前每次请求执行大量SQL开启MyBatis SQL日志使用连表查询或collection映射推荐接口响应慢是否在请求内实时算相似度改定时任务离线计算查表新用户推荐列表无变化热门榜缺随机扰动加随机系数扰动排序行为数据入库失败事务边界过大/过小检查Transactional范围避免包含耗时计算部署后项目起不来JDK版本、环境变量检查JAVA_HOME、PATH确认Tomcat版本匹配6.6 数据一致性行为记录和推荐结果不同步用户今天看了五部剧但明天的推荐列表里还有这三部剧说明行为记录没同步到推荐引擎。这个问题通常是异步上报行为失败导致的。我的方案是行为上报改成同步写入数据库失败时接口返回错误码前端可以重试。虽然极端情况下会增加响应时间但数据一致性的优先级更高。如果量级上来之后可以引入本地消息表行为先写本地表再异步转发兼顾一致性和性能。最后分享一个我个人做这个项目的体会推荐系统不是一个算法难题它本质上是一个数据工程问题。SSM本身并不负责推荐逻辑它只是把用户行为收集、数据存储、结果展示这件事变得规范可控。你在很多工业级推荐系统文章里看到的复杂架构拆到底也无非是召回、排序、兜底这三件事。先把这三件事用最简单的技术栈跑通比研究花哨的算法模型要实在得多。这套SSM短剧推荐系统完全可以作为基础后续想扩展时再加上Redis缓存、消息队列、AB测试框架把推荐列表的效果用点击率和完播率持续优化下去。