news 2026/9/16 2:32:27

Spring Boot短视频推荐系统实战:从协同过滤到Redis缓存优化

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Spring Boot短视频推荐系统实战:从协同过滤到Redis缓存优化

Spring Boot 短视频推荐系统这个项目,我在不同阶段接触过好几次——一开始是给学弟学妹看毕设,后来自己也动手重构过一版。说实话,网上叫“短视频推荐系统”的项目不少,但很多就是 CRUD 包了一层推荐算法的壳,用户表、视频表、点赞评论表一套,再挂一个协同过滤的 Demo,论文能写、答辩能过,但离“能用的推荐系统”差得远。这个项目源码编号 32744,结构上比大多数同类项目完整,我基于它做过二次开发,也踩过不少坑,今天就从头到尾拆一遍:数据怎么设计、行为日志怎么埋、推荐结果怎么算、接口怎么做性能优化,以及我实测下来最容易翻车的地方。

先说清楚这个系统能做什么。它不是一个只能跑 Demo 的教学项目,而是一套完整的、可扩展的推荐业务闭环:用户注册登录、视频上传与审核、浏览/点赞/评论/关注行为采集、基于协同过滤的个性化推荐、热门榜单兜底、后台管理。适合三类人看:一是 Spring Boot 做毕设的学生,需要从业务逻辑到技术选型都能讲清楚;二是刚工作的后端开发,想理解推荐系统在工程上怎么落地;三是打算在简历上写“推荐系统”项目的同学——你至少得知道面试官会从哪些角度追问。

我的经验是,不要上来就写代码,先把推荐系统的核心矛盾想明白,然后再动手,后面会顺很多。

1. 短视频推荐的场景特性:为什么不能照搬电商推荐

很多人做推荐系统,第一反应是抄电商那套:用户—商品—评分矩阵,然后跑 SVD 或者 ItemCF。这套东西放在短视频场景下,第一步就出问题,因为短视频没有用户打分。

1.1 短视频场景的行为数据特殊性

电商里用户给商品打 1~5 分,是显式反馈,直接可以当评分用。短视频里用户的行为是:浏览了 10 秒、看完了、点赞了、评论了、转发了、划走了。这些行为没有统一的量纲,你要自己定义“这个用户有多喜欢这条视频”。

我重构这个项目时,第一件事就是建了一张行为权重表,规则是这样的:

  • 完整播放(播放时长 ≥ 视频时长的 90%):权重 5
  • 点赞:权重 8
  • 评论:权重 6
  • 转发:权重 10
  • 关注视频作者:权重 12
  • 负向行为(播放 3 秒内滑走):权重 -5

这张表看着简单,但它是整个推荐系统的地基。不同行为的权重决定了用户向量长什么样,权重设计不合理,推荐结果会非常奇怪。

举个例子,如果点赞权重大于转发,那系统会倾向于推荐“用户愿意点赞但不愿转发”的内容,比如一些猎奇视频。转发行为其实比点赞更能表明用户的社交认同,所以我把它设得比点赞高。

另外,时间衰减也必须考虑。用户三天前点赞的视频和昨天点赞的视频,代表当前兴趣的强度完全不同,我采用的衰减公式是:

weight = initial_weight * exp(-0.05 * time_interval_days)

也就是每过一天,行为权重约衰减 5%。这样做的好处是用户兴趣漂移时,系统能比较快地跟上变化。

提示:不要试图在一个表里同时存行为数据和推荐结果数据,后面查询性能会非常难受。行为数据走日志表,推荐结果走缓存,职责分离。

1.2 推荐目标不是“准”,而是“沉浸”

做电商推荐,目标是转化率,用户买完就走,系统是高效的货架。做短视频推荐,目标是留存时长和互动率。抖音那种沉浸感的来源,是推荐系统在“猜你喜欢”和“探索新兴趣”之间找到了平衡。

这个系统里我做了两个策略来逼近这个目标:

第一,相似度阈值动态调整。ItemCF 计算相似视频时,如果阈值设死了,用户看久了会觉得推荐全是同质化内容。这个项目里我加了热度惩罚项,对同一个相似视频的连续展示次数做限制,同一个视频在连续 20 条推荐流里最多出现 2 次,超过就打入冷宫一段时间。

第二,热度稀释。纯个性化推荐容易让系统陷入信息茧房。我在推荐结果里做了级联兜底:第一优先是协同过滤结果,如果个性化结果不足(比如新用户),就补热门视频;热门里再按用户感兴趣的类目做过滤,保证用户点开刷到的前几条不会太离谱。

这些策略看着不复杂,但它决定了推荐结果的“体感”。很多毕设项目推荐出来的东西用户根本刷不下去,就是因为只算了相似度,没做多样性控制。

2. 数据模型与行为采集:推荐系统的地基工程

这个部分是最不性感但最关键的。很多项目源码给你看的时候,数据库表设计得乱七八糟,行为日志直接落地成一张大表,没有分表、没有归档,数据量一上来就崩。

2.1 核心表结构设计与取舍

我先说一下这个项目里的几张核心表,以及为什么这么设计。

用户表(user):基础信息外,我加了一个interest_tags字段,存的是用户兴趣标签,用逗号分隔的标签 ID。这样冷启动阶段不用实时算向量,直接查标签匹配的视频。

视频表(video):关键字段有category_idtagsuploader_idplay_countlike_countavg_watch_ratioavg_watch_ratio是平均完播率,这个字段很重要,它比播放量更能反映视频质量。我在项目里跑定时任务,每 5 分钟聚合一次,写入这个字段。

用户行为表(user_action):这是最核心的表,字段包括user_idvideo_idaction_typeaction_scorecreate_time。注意,action_score是在写入时就算好的,不是查询时再算,这样可以省去很多计算开销。

视频相似度表(video_similarity):离线计算好的 ItemCF 相似度矩阵,video_idsimilar_video_idsimilarity_score。这张表是推荐系统的大动脉,查询推荐结果时直接从这里取,不走实时计算。

注意:视频相似度表必须分表或者用 ES 存。我当时用 MySQL 单表存了 10 万视频的相似度,结果一张表上亿行,查询直接卡死。后来改成只存 Top 50 相似视频,行数降了一个数量级,性能才恢复正常。

2.2 行为采集埋点的实现细节

行为日志的埋点,前端传什么、后端怎么收,有个关键的坑。很多项目前端传的是“用户点击了播放按钮”,但这不是行为结束,只是开始。真正有意义的埋点是上报“播放结束”事件,附带播放时长和视频总时长。

我在这个项目中定义了几类事件,前端通过一个统一的/api/action/report接口上报:

POST /api/action/report { "userId": 10001, "videoId": 20931, "actionType": "PLAY_FINISH", "playDuration": 32.5, "videoTotalDuration": 35.0, "timestamp": 1718402335 }

后端接收到之后,先做一步清洗:如果playDuration / videoTotalDuration < 0.03,判定为无效播放,直接丢弃。这个阈值很关键,能过滤掉大量用户划走产生的垃圾数据。

接下来,根据行为类型和完播率计算行为得分:

public double calculateActionScore(String actionType, double playRatio) { if ("PLAY_FINISH".equals(actionType)) { if (playRatio >= 0.9) { return 5 * (playRatio == 1.0 ? 1.2 : 1.0); } if (playRatio >= 0.5) { return 3; } return 1; } if ("LIKE".equals(actionType)) { return 8; } if ("COMMENT".equals(actionType)) { return 6; } if ("SHARE".equals(actionType)) { return 10; } if ("FOLLOW".equals(actionType)) { return 12; } if ("SKIP".equals(actionType)) { return -5; } return 0; }

写完这条链路之后,我认识到一个道理:推荐系统的推荐质量其实在上游就决定了。行为数据垃圾、权重设计不合理,后面算法再优化都没用。

2.3 异步处理与消息队列的引入

行为采集接口如果同步写库,大流量下会拖垮主业务。这个项目里我引入了 ActiveMQ 做异步削峰:接口收到请求后,直接发给消息队列,马上返回“成功”;消费者从队列拉消息,做清洗、算分、写库。

这模块是真实的工程取舍。很多同类毕设项目直接同步写库,答辩时没压力,但你自己知道这在真实场景下跑不了。选型上,ActiveMQ 在这个项目里比 RabbitMQ 更合适:跟 Spring Boot 集成简单,不需要额外部署 Erlang 环境,而且对消息可靠性要求没那么变态——行为数据偶尔丢一两条,对推荐结果的影响微乎其微。

核心配置:

spring: activemq: broker-url: tcp://localhost:61616 user: admin password: admin pool: enabled: true max-connections: 10

生产者端:

@RestController public class ActionController { @Autowired private JmsMessagingTemplate jmsMessagingTemplate; @PostMapping("/api/action/report") public Result report(@RequestBody UserActionDTO dto) { jmsMessagingTemplate.convertAndSend("queue.user.action", JSON.toJSONString(dto)); return Result.success(); } }

消费者端:

@Component public class ActionConsumer { @JmsListener(destination = "queue.user.action") public void onMessage(String message) { UserActionDTO dto = JSON.parseObject(message, UserActionDTO.class); // 清洗 + 算分 + 写库 actionService.saveAction(dto); } }

3. 推荐引擎的实现:协同过滤之外还有哪些事

来到项目最核心的部分:推荐结果到底怎么算出来。源码里用的是基于物品的协同过滤(ItemCF),这个选型是对的,短视频场景下用户量远大于视频更新的频率,ItemCF 比 UserCF 更合适。

3.1 基于物品的协同过滤:离线计算的完整流程

ItemCF 的核心假设是:喜欢视频 A 的用户也喜欢视频 B,那么 A 和 B 是相似的。它不需要视频的内容特征,纯靠用户行为构建相似关系,实现起来也直观。

完整计算流程分四步:

第一步:构建用户-物品倒排表

从用户行为表里捞数据,按用户分组,取出该用户有过正向行为的视频列表。注意这里必须过滤负向行为(滑走的不算),具体 SQL 类似:

SELECT user_id, video_id FROM user_action WHERE action_score > 0 AND create_time > DATE_SUB(NOW(), INTERVAL 30 DAY)

第二步:计算物品共现矩阵

遍历倒排表,对同一个用户看过的视频两两组合,共现次数加 1。这一步是计算密集型的,数据量大时需要跑 Spark 或者用多线程分片处理。源码里用的是单机多线程,10 万用户规模还扛得住。

第三步:计算相似度矩阵

ItemCF 的相似度公式,最常用的是余弦相似度。源码里用的是改进版,加了热度惩罚:

sim(i, j) = count(i, j) / sqrt(count(i) * count(j)) * penalty

其中penalty = 1 / (1 + log(1 + popularity(j))),视频 j 越热门,相似度折扣越多。这样避免热门视频跟谁都相似,导致推荐结果千篇一律。

第四步:写入视频相似度表

只保留每个视频 Top 50 的相似视频,写入video_similarity表,并给(video_id, similarity_score)建好联合索引。

提示:相似度计算一定要放到离线任务里跑,比如每天凌晨 2 点。如果放线上实时算,每来一个请求都扫描倒排表,数据库直接被打死。

3.2 在线推荐流程:从相似视频到最终推荐流

离线算好相似度表之后,在线推荐逻辑反而简单了:用户请求首页推荐流,系统根据用户最近有过正向行为的视频,在相似度表里查 Top N 相似视频,按相似度加权累加,再过滤掉已经看过的,最后降权重复的视频,按得分排序返回。

核心代码长这样:

public List<VideoVO> getRecommendVideos(Long userId, int pageSize) { // 1. 取用户最近观看且有正向行为的视频,最多取 20 个 List<Long> userActionVideos = actionService.getUserPositiveVideos(userId, 20); if (userActionVideos.isEmpty()) { // 冷启动:返回热门视频 return hotVideoService.getHotVideos(pageSize); } // 2. 遍历行为视频,从相似度表查相似视频 Map<Long, Double> scoreMap = new HashMap<>(); for (Long videoId : userActionVideos) { List<VideoSimilarity> similarities = similarityService.getTopSimilar(videoId, 20); for (VideoSimilarity sim : similarities) { scoreMap.merge(sim.getSimilarVideoId(), sim.getSimilarityScore() * getActionWeight(videoId), Double::sum); } } // 3. 过滤已看过的视频,除以热度衰减 List<Long> watchedVideos = actionService.getWatchedVideos(userId); return scoreMap.entrySet().stream() .filter(e -> !watchedVideos.contains(e.getKey())) .sorted(Map.Entry.<Long, Double>comparingByValue().reversed()) .limit(pageSize) .map(videoId -> videoService.getVideoVO(videoId)) .collect(Collectors.toList()); }

这里面有个细节值得细说:getUserPositiveVideos拿到的视频列表不是平均权重,而是按时间衰减后的行为权重作为加权系数。用户最近看的视频,在推荐结果里话语权更大;三天前看的视频,影响会弱很多。

3.3 冷启动问题的处理策略

冷启动是推荐系统里绕不开的问题,源码里做了两层处理,我实测下来效果还可以。

用户冷启动:新用户没有历史行为数据,系统返回热门视频。但热门视频不一定是用户喜欢的,所以我在热门视频池里加了类目偏好参数——新用户注册时选择感兴趣的分类,推荐时优先从这些分类的热门池里取。如果用户没选,就用全局热门池兜底。

物品冷启动:新上架的视频没有任何用户行为,不会出现在相似度表里,很容易沉底。解决方法是给新视频一个“新鲜度加权”,按上传时间进行指数衰减加分。新视频初始权重是 50,每小时衰减 20%,刷完就掉出候选池。这样新视频有机会被推荐出去,获得第一批行为数据,进入良性循环。

4. 性能优化与缓存设计:撑住高并发的推荐请求

推荐系统本身计算不复杂,但“用户点开 App 首屏就要立刻看到推荐流”这个场景对延迟要求很高。我压测过,推荐接口如果响应超过 500ms,用户流失率明显上升。这个项目我从三个方面做了优化。

4.1 Redis 缓存策略:Redis 在推荐系统里的关键用法

Redis 在这个项目里不是存字符串做缓存那么简单,它的核心应用是:

视频信息缓存:热点视频的详情(标题、封面、作者、点赞数)缓存到 Redis,key 设计为video:info:{videoId},过期时间 30 分钟。推荐结果命中的视频,直接走缓存,不回表查 MySQL。

推荐结果缓存:每个用户的推荐结果,缓存 key 是recommend:user:{userId},过期时间 5 分钟。用户上下滑时,优先从缓存里取推荐列表,缓存未命中才重新计算。这个设计直接决定了接口的吞吐量。

计数器缓存:点赞数、播放数用 Redis 的 INCR 命令做实时计数,每隔一段时间异步批量回写 MySQL。这个场景 Redis 几乎是最佳选择,因为短视频的点赞量波动很大,关系型数据库的行锁经不起高频更新。

核心代码:

@Component public class VideoCacheService { @Autowired private StringRedisTemplate redisTemplate; public VideoVO getVideoVO(Long videoId) { String key = "video:info:" + videoId; String json = redisTemplate.opsForValue().get(key); if (json != null) { return JSON.parseObject(json, VideoVO.class); } VideoVO video = loadFromDb(videoId); redisTemplate.opsForValue().set(key, JSON.toJSONString(video), 30, TimeUnit.MINUTES); return video; } public void incrementLikeCount(Long videoId) { String key = "video:like:" + videoId; redisTemplate.opsForValue().increment(key); } public void flushLikeCountsToDB() { Set<String> keys = redisTemplate.keys("video:like:*"); // 批量回写 MySQL,并删除 Redis 中的临时计数 } }

4.2 缓存穿透、击穿、雪崩的兜底方案

用缓存都知道这三座大山,但真正在项目里处理好的人不多。源码里做了这样的处理:

缓存穿透(查询不存在的 key 导致请求打到数据库):采用布隆过滤器拦截。项目初始化时,把所有视频 ID 加载进布隆过滤器,查询前先判断 ID 是否存在。但这种方案在视频 ID 动态增加时比较麻烦,尤其是集成了 MyBatis-Plus 自动生成雪花 ID 的情况下,布隆过滤器没法实时更新。

实际的替代方案是:缓存空值。如果一个视频 ID 不存在,Redis 里写一个空字符串,过期时间设置得比较短,比如 2 分钟。这个方案简单有效,代码侵入小,我实测下来效果满足需求。

缓存击穿(热点 key 过期瞬间大量请求打到数据库):采用互斥锁方案,同一个 key 同时只有一个线程能查数据库回填缓存,其他线程短暂等待后拿到缓存结果。

public VideoVO getVideoVODetail(Long videoId) { String key = "video:info:" + videoId; String json = redisTemplate.opsForValue().get(key); if (json != null) { return JSON.parseObject(json, VideoVO.class); } // 尝试获取分布式锁,10 秒超时 boolean lock = redisTemplate.opsForValue().setIfAbsent("lock:video:" + videoId, "1", Duration.ofSeconds(10)); if (lock) { try { VideoVO video = loadFromDb(videoId); redisTemplate.opsForValue().set(key, JSON.toJSONString(video), 30, TimeUnit.MINUTES); return video; } finally { redisTemplate.delete("lock:video:" + videoId); } } // 没拿到锁,短暂休眠后重试 Thread.sleep(100); return getVideoVODetail(videoId); }

缓存雪崩(大量 key 同时过期导致数据库压力突增):给缓存过期时间加随机偏移量。比如基础过期时间 30 分钟,实际用 30 + 0~300 秒的随机值,避免同一批视频的缓存同时过期。

4.3 数据库索引优化:index 加对了,性能翻倍

这个项目里有几条 SQL 频率特别高,比如查相似视频、查用户行为列表、统计视频热度。我实测发现,很多人 SQL 慢不是因为数据量大,而是索引加错了。

我最终确定的联合索引有这几个:

  • user_action表:(user_id, create_time),对应“查用户最近行为”的场景
  • user_action表:(video_id, action_type),对应“查视频获得的行为统计”场景
  • video_similarity表:(video_id, similarity_score),对应“查相似视频”核心场景
  • video表:(category_id, play_count),对应“按类目拉热门视频”场景

加了这些索引之后,单条 SQL 从平均 800ms 降到 50ms 以内,整个推荐接口的 P95 延迟从 1.2s 降到 350ms。

提示:video_similarity表查询时,尽量只查similar_video_idsimilarity_score,避免SELECT *。这个表动辄几千万行,回表开销非常大。

5. 项目跑通之后的事:部署、日志与远程调试的经验

源码拿到手,第一件事是在本地把项目跑起来。这个环节不少人在 Spring Boot 版本、环境变量、数据库初始化和前端联调上卡过好几个小时。我按正常的环境准备和启动流程检查一遍,重点说几个真实工作中更容易踩的坑。

5.1 环境准备与启动链路

我用的环境组合是:JDK 8、Maven 3.6.x、MySQL 5.7、Redis 6.x、ActiveMQ 5.15,前端使用 Vue 2 + Element UI。JDK 版本不要擅自升到 17,很多老项目源码里的依赖和代理配置对 JDK 8 适配最稳。

启动顺序也有讲究,正确的启动链路是:

  1. 启动 MySQL,执行init.sql初始化数据库
  2. 启动 Redis,确认 6379 端口可用
  3. 启动 ActiveMQ,确认 61616 端口可访问控制台
  4. 修改application.yml里的数据库账号密码、Redis 密码、ActiveMQ 账号密码
  5. mvn spring-boot:run启动后端
  6. 前端npm installnpm run dev

如果你用的是新版 IntelliJ IDEA 直接从 GitHub 拉源码,注意 Maven 仓库的镜像配置,国内网络环境建议配置阿里云镜像,不然依赖下到一半失败很折磨。

5.2 前端联调与跨域问题的正确解法

前后端分离项目里,跨域问题基本必现。我在这个项目里配置了全局 CORS,没有用前端代理解决——因为后期部署上线时,前端静态资源可能放在 Nginx 上,后端单独一个端口,必须后端允许跨域才行。相关代码:

@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/api/**") .allowedOrigins("http://localhost:8080") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }

需要注意的是,allowedOrigins不要配成*,否则前端带 Cookie 的请求会被浏览器拦截。如果前端请求报跨域,先用浏览器 F12 看请求的Origin头,再把它加到白名单里。

另一个前后端联调的经典坑是时间格式不一致。后端返回的时间戳是 Long 类型,前端格式化后差了 8 小时;或者后端返回2024-06-15T10:00:00,前端解析不了。我在代码里加了全局 Jackson 配置,统一时间格式:

@Configuration public class JacksonConfig { @Bean public Jackson2ObjectMapperBuilderCustomizer jacksonCustomizer() { return builder -> { builder.serializerByType(java.util.Date.class, new com.fasterxml.jackson.databind.ser.std.DateSerializer( new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss"))); }; } }

5.3 常见问题排查清单

我把这个项目落地过程中最常见的几个问题整理成一张清单,运行时报错先对照排查:

问题现象可能原因解决方案
启动报Failed to configure a DataSource数据库连接配置不对或数据库未初始化检查application.yml的 URL、账号、密码,确认init.sql已执行
Redis 连接超时Redis 未启动或密码不匹配redis-cli ping测试,检查配置文件里的密码
ActiveMQ 队列消息堆积消费者服务没启动或消费代码报错检查消费者日志,确认@JmsListener注解的 destination 是否一致
推荐接口返回空列表用户没有行为数据且热门视频池为空检查hot_video缓存是否被清空,手动造几条行为数据测试
前端登录后接口 401Token 校验失败或跨域配置拦截检查 JWT 拦截器白名单,确认OPTIONS请求放行
视频封面加载不出来静态资源路径配置错误或文件未上传成功确认spring.resources.static-locations配置,检查上传文件目录权限

还有一类问题非常隐蔽,就是 Spring Boot 内嵌容器问题。这个项目默认用的是 Tomcat。如果你的环境上 Tomcat 版本跟 Spring Boot 的默认版本不兼容,会时不时报奇怪的类加载错误。处理方案是显式指定版本依赖,或者换成 Undertow 容器。我一般保留 Tomcat,再确认依赖里的版本没有冲突。

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-undertow</artifactId> </dependency>

换容器之后内嵌服务的线程模型跟 Tomcat 不同,压测表现略有差异,但功能上不会有任何影响。如果你的部署环境对内存占用敏感,Undertow 通常比 Tomcat 轻一些。

5.4 日志排查经验:先看推荐链路再谈算法

项目跑起来之后,如果你要定位推荐结果不对的问题,不要盯着算法调参。先把链路日志打开,看每一步的输出:用户行为数据有没有进库、相似度表有没有更新、推荐接口查缓存还是查库、最后结果的得分是多少。

我在代码里加了一个推荐链路的 MDC 日志标记:

@Slf4j @Service public class RecommendService { public List<VideoVO> recommend(Long userId, int pageSize) { MDC.put("userId", String.valueOf(userId)); MDC.put("traceId", UUID.randomUUID().toString().replace("-", "")); log.info("[推荐链路] 开始推荐, pageSize={}", pageSize); // ... 推荐逻辑 ... log.info("[推荐链路] 推荐完成, 返回视频数={}", result.size()); return result; } }

排查时直接grep "推荐链路"加上 traceId,一条链路从头到尾全看到了。这个习惯帮我省了很多排查问题的时间,值得复制到其他项目里。

6. 安全的几个细节:接口防刷与敏感信息保护

安全这部分容易被忽略,但短视频系统是公开部署的,存在不少针对性攻击面。

6.1 行为上报接口的防刷策略

行为上报接口是用户无感调用的,攻击者很容易伪造海量请求来污染推荐数据。如果大量虚假点赞/评论行为灌进推荐链路,推荐结果会被严重带偏。我在接口层面加了频率限制:

@Component public class RateLimiterInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String userId = request.getHeader("userId"); String ip = getClientIp(request); String key = "rate:action:" + userId + ":" + ip; Long count = redisTemplate.opsForValue().increment(key); if (count != null && count == 1) { redisTemplate.expire(key, 60, TimeUnit.SECONDS); } if (count != null && count > 300) { response.setStatus(429); return false; } return true; } }

同一用户每分钟最多上报 300 次行为,超过直接拒绝。正常情况下用户刷视频达不到这个量,但不设限制很容易被脚本刷爆。

6.2 信息泄露防护与源码安全审查

我之前排查过某系统因为 Spring Boot Actuator 未做权限控制,导致 heapdump 文件可以直接下载,内存中的敏感信息整体暴露。这个问题在这类项目中相当典型,务必重点检查。

具体做法有三件事:

第一,如果不需要 Actuator 的监控端点,直接排除依赖,干净彻底。

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </exclusion> </exclusions> </dependency>

或者,如果确实需要健康检查,只暴露healthinfo两个端点:

management: endpoints: web: exposure: include: health,info

第二,生产环境的配置文件中,数据库密码、Redis 密码不要明文写在application.yml里。用 Jasypt 做加密,或者环境变量注入,源码里不要带真实凭据。

第三,统一异常处理,不要用 Spring Boot 默认的报错页面。默认的 Whitelabel Error Page 会把异常信息、堆栈、甚至部分请求参数直接暴露给前端。定义全局异常处理器,统一返回 JSON 格式,避免信息泄露。

7. 我在实际部署和二次开发中的几条体会

拿这套源码做完二次开发,前前后后改了一个多月,沉淀了几条真实体会,分享出来希望能帮后面的同学少走弯路。

第一,推荐系统的效果是运营出来的,不是调参调出来的。很多人拿到项目跑通之后,就死磕协同过滤的参数——相似度阈值改改、Top N 改改、时间衰减系数改改,结果推荐效果并不理想。但我测试发现,影响最大的是行为数据的质量和权重设计。你把数据洗干净,行为权重调对,比调任何算法参数都有效。算法参数带来的提升是 5%~10%,而数据质量的提升是翻倍的效果。

第二,缓存更新一定要有兜底任务。以前我只靠过期时间来保证缓存一致性,结果出现过视频删除后缓存还残留的情况,用户体验非常糟糕。后来加了一个定时任务,每 10 分钟扫描一次变更记录,把变更视频的缓存主动删掉。记住,缓存不是一次配好就完事的,要有完整的更新和删除策略。

第三,推荐系统的评估比实现更难。很多人做完推荐,不知道怎么证明它“有效”,答辩或面试时只能含糊地讲“效果不错”。我给这个项目加了一个简单的评估模块:记录推荐流里视频的曝光次数和点击次数,计算 CTR(点击率),定时任务按天统计。后来我把数值做出来——CTR 从初始的 4.4% 提升到 6.8%——这就是一个能说清楚、能验证的数据,比“应该更准了”这种话有说服力得多。

第四,不要一开始就想做多复杂的算法。先跑通 ItemCF 加热门兜底,把链路打通,把数据积累起来,再逐渐替换成更复杂的召回和排序模型。很多人一上来就想上 DeepFM、双塔模型,结果前面每一层都没跑通,最后项目崩在半路。推荐系统是个工程问题,先把工程做好,算法是锦上添花。

如果你打算用这套项目做毕设或者写进简历,建议按这个顺序去把每个模块理解透:先看懂数据模型设计(为什么这么建表),再跟一遍推荐链路(从用户行为到最终推荐列表),然后梳理性能优化点(缓存、索引、异步),最后把安全细节补上。这套逻辑捋顺了,不管面试官从哪个角度追问,你都能接得住。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/16 2:32:07

BIM GIS桥梁监管平台是什么?5 大核心功能与应用价值详解

BIM与GIS的相遇&#xff0c;正在为桥梁监管行业打开一扇全新的大门。从2002年BIM概念被正式提出&#xff0c;到2016年国家标准落地&#xff0c;再到如今与GIS、物联网等技术的深度融合&#xff0c;桥梁管理正从二维图纸上的静态记录&#xff0c;走向三维空间中的动态治理。本文…

作者头像 李华
网站建设 2026/9/16 2:31:08

Zemax+MATLAB联合仿真:微透镜阵列与光场相机完整流程

做微透镜阵列相关仿真这几年&#xff0c;我最大的感受是&#xff1a;Zemax和MATLAB单拎出来都不够用。Zemax能把光追得明明白白&#xff0c;但它不太擅长处理"这个像素属于哪个微透镜视角"这类计算成像问题&#xff1b;MATLAB做图像处理和算法验证很强&#xff0c;可…

作者头像 李华
网站建设 2026/9/16 2:31:04

三款主流智能电动车安全体系深度对比解析

1. 项目概述&#xff1a;为什么这三款车的安全解析值得花20分钟认真读完最近在几个新能源车主群和智能驾驶技术论坛里&#xff0c;频繁刷到“尚界Z7”“阿维塔12”“阿维塔06”这三个名字——不是因为谁又降价了&#xff0c;而是因为一批真实车主在高速实测后发的长帖&#xff…

作者头像 李华
网站建设 2026/9/16 2:31:02

LLM引导macOS私有框架逆向:从静态证据到类型推理的实战方法论

做 macOS 逆向的人应该都有过这种体验&#xff1a;对着一个陌生私有框架的二进制&#xff0c;翻来覆去找方法原型&#xff0c;最后发现参数类型全是id&#xff0c;返回值是id&#xff0c;连 block 长什么样都看不出来。传统做法是 class-dump 拉一遍头文件&#xff0c;再进反汇…

作者头像 李华
网站建设 2026/9/16 2:30:33

阿里云弹性伸缩ESS实战指南:从原理到配置,解决流量高峰扩容难题

1. 弹性伸缩到底解决什么问题&#xff0c;先别急着配置做渠道商这些年&#xff0c;接触过大量中小企业客户&#xff0c;几乎每个人都在问同一个问题&#xff1a;流量高峰来了&#xff0c;服务器扛不住怎么办&#xff1f;最典型的场景就是电商大促、营销活动上线、或者某个内容突…

作者头像 李华