最近把之前做的一个基于SpringBoot图书推荐系统内部代号823完整重写了一遍从数据库设计到协同过滤算法落地再到SpringBoot接口实现和Docker部署整个过程几乎把Java后端日常开发的典型环节都走了一遍。这个系统核心是面向图书管理场景用户注册登录后可以浏览图书、打分、收藏系统会基于用户的历史行为给每个人生成个性化推荐列表。适合准备做类似毕设、课设或者想系统入门SpringBoot推荐算法实战的同学参考。我不打算只贴代码而是把每个关键选择背后的理由和踩过的坑一起记录下来。有些坑比如推荐结果冷启动为空、MyBatis分页插件在SpringBoot高版本下的兼容问题、缓存穿透导致数据库压力骤增网上资料虽然都有提及但只有真正在项目里撞上才知道多耽误时间。这篇文章尽量把这些经历写透能帮一个算一个。1. 系统定位与技术选型先解决“这个系统到底做了什么”1.1 项目结构与模块划分拿到这个需求的时候我第一反应不是急着写代码而是先想清楚这个系统到底是给谁用、要解决什么核心问题。图书推荐系统的核心用户是两类人普通读者和系统管理员。普通读者关心的是“我能不能快速找到想看的书”管理员关心的是“图书数据怎么维护、用户行为数据从哪来”。所以我把系统拆成了几个职责清晰的模块用户模块注册、登录、个人信息维护、密码加密存储图书模块图书信息增删改查、分类管理、封面上传行为模块用户打分、收藏、借阅记录这是推荐系统的数据来源推荐模块基于协同过滤算法生成推荐列表支持“猜你喜欢”和“相似图书”管理后台用户管理、图书审核、统计数据看板这个拆分不是凭空来的而是从发布、维护、测试三个维度倒推的。如果你把用户逻辑和图书逻辑混在一个Controller里短期内写起来很快但后面每次改动都要全局搜索风险极高。823项目里我最满意的一个决定就是坚持了模块化尽管它是单体应用但包结构上已经为将来拆分微服务留好了余量。1.2 为什么选SpringBoot而不是SSH或SSM很多教程一上来就直接说“SpringBoot简化了配置”但这个说法太笼统。我在对比过SSHStrutsSpringHibernate和SSMSpringSpringMVCMyBatis之后对SpringBoot最深的体会是它把“约定优于配置”落实到了工程层面。传统SSM搭建一个项目要先配置web.xml、spring-mvc.xml、mybatis-config.xml、数据源、事务管理器一整套下来至少半天而且每个配置文件之间的依赖关系非常容易出错。SpringBoot通过自动配置机制让大部分场景只需要引入对应的starter依赖就能直接使用。比如引入spring-boot-starter-web内嵌Tomcat、DispatcherServlet、Jackson这些就都自动配好了引入mybatis-spring-boot-starterSqlSessionFactory和数据源也能按约定自动装配。对图书推荐系统这种业务不算特别复杂的应用来说SpringBoot把开发者的注意力从“配置环境”释放到了“业务逻辑”上这才是核心价值。另外SpringBoot的生态非常成熟无论是做REST接口、定时任务、缓存还是消息队列都有对应的starters以后扩展功能不会卡在基础设施上。1.3 技术栈清单整个系统最终使用的技术栈如下组件选型说明开发框架Spring Boot 2.7.18稳定、资料多兼容JDK8持久层MyBatis mybatis-plus灵活写SQL分页方便数据库MySQL 8.0用户行为数据用InnoDB存储缓存Redis 6.x缓存推荐结果降低计算压力认证JWT 拦截器无状态登录前后端分离友好前端Vue 3 Element Plus管理后台和用户端页面推荐算法基于用户/物品的协同过滤自研实现不依赖第三方推荐引擎这里尤其想提醒一点选型不是越新越好而是要考虑资料完整度和踩坑成本。Spring Boot 3.x虽然性能更强但它是基于JDK17的如果你手头还是JDK8环境迁移成本不小。823项目我最终锁定了2.7.18这是2.x系列的最后一个版本既能用上大部分新特性资料又极其丰富遇到问题几乎都能搜到答案。2. 图书推荐系统的数据底座表结构设计和冷启动数据构建2.1 用户、图书、行为三张核心表推荐系统最讲究的是数据没有高质量的行为数据再好的算法都是空转。我在设计数据库时没有图省事而是老老实实建了5张核心表user用户表、book图书表、book_category图书分类表、user_book_rating用户评分表、user_book_favorite用户收藏表。用户表很简单除了常规的id、username、password我额外加了avatar和create_time。密码字段我用的是BCrypt加密后的密文长度设了64位防止后面用MD5时密码直接明文存储的尴尬。图书表是信息核心字段包括CREATE TABLE book ( id BIGINT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(200) NOT NULL COMMENT 书名, author VARCHAR(100) COMMENT 作者, publisher VARCHAR(150) COMMENT 出版社, isbn VARCHAR(20) UNIQUE COMMENT ISBN号, category_id BIGINT COMMENT 分类ID, cover_url VARCHAR(500) COMMENT 封面地址, description TEXT COMMENT 简介, publish_date DATE COMMENT 出版日期, rating_score DECIMAL(3,1) DEFAULT 0.0 COMMENT 平均评分, rating_count INT DEFAULT 0 COMMENT 评价人数, status TINYINT DEFAULT 1 COMMENT 上架状态 ) COMMENT 图书表;这里有个小优化值得提一下我没有在book表里冗余存“推荐指数”而是存了rating_score和rating_count这两个字段后面做排行榜和冷启动推荐时可以直接用这两个字段排序不用每次现算平均值数据库压力小很多。用户评分表是整个推荐系统的“燃料”CREATE TABLE user_book_rating ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, book_id BIGINT NOT NULL, rating TINYINT NOT NULL COMMENT 1-5分, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_user_book (user_id, book_id) ) COMMENT 用户-图书评分表;注意这里我加了UNIQUE KEY uk_user_book (user_id, book_id)这个唯一索引很重要它保证了同一用户对同一本书只能有一条评分记录后面做“更新还是插入”的逻辑时可以直接用INSERT ... ON DUPLICATE KEY UPDATE不用先查一遍再决定走update还是insert既省了一次查询又天然防住了重复数据。2.2 冷启动问题没有行为数据推荐系统怎么转起来这是所有推荐系统都绕不开的坎。823项目刚起步时数据库里只有几百本图书用户行为数据几乎为零协同过滤算法算不出一丁点推荐结果。这也是很多人在这个项目上最崩溃的一步——算法实现得再漂亮一旦输入矩阵是空的输出就是空列表。我实际的解法分成两层。第一层是让系统在“无行为数据”时也有推荐可展示新用户进入首页时直接推荐全局评分最高的热门图书也就是用rating_score和rating_count做一个加权公式来排序。这个公式我用了类似贝叶斯平均的思路避免只有一个人打了5分的小众图书碾压几万人打出4.8分的经典著作public double calculateHotScore(RatingStat stat) { double avgRating stat.getAvgRating(); int ratingCount stat.getRatingCount(); // C是全局平均评分m是最小评分人数阈值 double C 4.2; int m 10; return (avgRating * ratingCount C * m) / (ratingCount m); }第二层是在系统里埋好了“行为引导”用户在注册流程里选择感兴趣的图书分类系统把这个意图数据存入user_interest表即便新用户还没有评分行为也能基于分类偏好做粗粒度的内容推荐。这一层虽然是过渡方案但在真实场景里恰恰是用户愿意留下来继续使用系统的关键。2.3 种子数据的准备别小看这一步真实环境里图书数据可以通过爬虫或第三方API获取但开发阶段最忌讳的就是没有数据硬撸代码。我当时写了一个DataInitializer类利用SpringBoot的ApplicationRunner接口在系统启动时自动往库里灌入500本图书和一批模拟评分数据。Component public class DataInitializer implements ApplicationRunner { private static final Logger log LoggerFactory.getLogger(DataInitializer.class); Autowired private BookMapper bookMapper; Autowired private UserBookRatingMapper ratingMapper; Override public void run(ApplicationArguments args) { long bookCount bookMapper.selectCount(null); if (bookCount 0) { log.info(图书数据已存在跳过初始化); return; } // 使用Faker库生成图书信息和评分数据 Faker faker new Faker(); // 循环插入500本书、200个用户、2万条评分... } }模拟评分数据时要注意一个细节评分分布要接近真实场景。真实用户打分的分布通常是“两端多、中间少”也就是1分和5分相对多2、3、4分相对少而不是均匀分布。我生成数据时用了一个偏态分布函数这样后面调算法时评分矩阵的稀疏度和真实情况才比较接近调出来的参数才有参考价值。3. 推荐算法在SpringBoot服务里的落地方案3.1 基于用户的协同过滤UserCF推荐算法是823项目的灵魂我从最经典的协同过滤入手。UserCF的核心思想是“物以类聚、人以群分”如果一个用户A和用户B的历史评分记录高度相似那么A喜欢的书B很可能也喜欢。具体实现分三步第一步是构建“用户-图书”评分矩阵第二步是计算用户之间的相似度第三步是根据相似用户的评分加权预测当前用户对未读图书的评分。矩阵构建不需要复杂的数学库用HashMap就能搞定。关键在相似度计算我选用的是皮尔逊相关系数因为要消除不同用户打分尺度的差异。有些用户整体偏爱给高分有些用户普遍给低分如果直接用余弦相似度两个人的评分尺度差异会被误判为“不喜欢同一本书”。/** * 计算两个用户的皮尔逊相关系数 */ public double pearsonSimilarity(MapLong, Double user1Ratings, MapLong, Double user2Ratings) { ListDouble commonRatings1 new ArrayList(); ListDouble commonRatings2 new ArrayList(); for (Map.EntryLong, Double entry : user1Ratings.entrySet()) { Double rating2 user2Ratings.get(entry.getKey()); if (rating2 ! null) { commonRatings1.add(entry.getValue()); commonRatings2.add(rating2); } } if (commonRatings1.size() 5) { return 0.0; // 共同评分数太少相似度不可靠 } double avg1 commonRatings1.stream().mapToDouble(Double::doubleValue).average().orElse(0.0); double avg2 commonRatings2.stream().mapToDouble(Double::doubleValue).average().orElse(0.0); double numerator 0; double denominator1 0; double denominator2 0; for (int i 0; i commonRatings1.size(); i) { double diff1 commonRatings1.get(i) - avg1; double diff2 commonRatings2.get(i) - avg2; numerator diff1 * diff2; denominator1 Math.pow(diff1, 2); denominator2 Math.pow(diff2, 2); } if (denominator1 0 || denominator2 0) { return 0.0; } return numerator / (Math.sqrt(denominator1) * Math.sqrt(denominator2)); }代码里commonRatings1.size() 5这个判断是个重要经验如果两个用户只有一两本共同评分的书算出来的相关系数非常不可靠很可能因为偶然因素高达0.9以上反而误导推荐。加了这个门槛推荐的准确度会明显提升。3.2 基于物品的协同过滤ItemCF实际跑过UserCF之后会发现一个典型问题当用户量变大、行为数据变稀疏时用户之间的相似度计算准确率下降得很快而且实时计算代价很高。所以在823项目里我同时做了ItemCF作为辅助推荐。ItemCF的思路和UserCF刚好相反它计算的是图书之间的相似度。如果“读过A书的人也读了B书”那就认为A和B存在相似关系。这样做的好处是基于物品的相似度相对稳定不需要每次用户产生新行为就全部重算可以离线计算好存入Redis在线推荐时只做查询和排序。图书相似度计算我用了余弦相似度public double cosineSimilarity(MapLong, Double item1Ratings, MapLong, Double item2Ratings) { SetLong commonUsers new HashSet(item1Ratings.keySet()); commonUsers.retainAll(item2Ratings.keySet()); if (commonUsers.size() 3) { return 0.0; } double dotProduct 0; double norm1 0; double norm2 0; for (Long userId : commonUsers) { dotProduct item1Ratings.get(userId) * item2Ratings.get(userId); } for (Double rating : item1Ratings.values()) { norm1 rating * rating; } for (Double rating : item2Ratings.values()) { norm2 rating * rating; } return dotProduct / (Math.sqrt(norm1) * Math.sqrt(norm2)); }算完相似度之后我并不是把所有相似图书都一股脑塞给用户而是先过滤掉用户已经读过的书然后按“相似度×评分”做一个加权排序取前10本作为最终推荐结果。这一步过滤非常关键如果一个用户已经读过那本书还继续推荐体验会很差。3.3 推荐计算时机实时算还是离线算刚开始我犯了一个新手都会犯的错就是把推荐计算放在用户请求推荐接口的时候实时去做。结果第一次压测就暴露了问题当用户量到几百、图书量到几千时一次请求要现算用户相似度和后续的评分预测耗时经常在2秒以上接口直接超时。后来我调整了策略推荐结果分两级缓存。一级缓存把每个用户的“猜你喜欢”推荐列表在用户行为变化后异步重算计算结果存入Redis设置24小时过期。推荐接口直接读Redis压测下响应时间稳定在100ms以内。二级兜底如果Redis里没有用户的推荐缓存立即返回热门图书兜底同时触发一个异步任务去重算推荐重算完成后缓存起来。这个“先兜底、后异步补算”的策略在实际项目中救了我很多次既保证了接口速度又能让推荐结果在用户下次刷新时更新。SpringBoot里做异步任务非常简单只要在主类上加EnableAsync然后在推荐服务方法上加Async即可。4. SpringBoot服务端核心实现细节4.1 REST接口设计与参数校验前后端分离模式下接口设计直接影响开发效率和联调成本。我在设计REST接口时遵循了几个固定原则路径用复数名词、HTTP方法表达动作、状态码语义明确。例如图书模块方法路径说明GET/api/books分页查询图书列表GET/api/books/{id}获取图书详情POST/api/books新增图书PUT/api/books/{id}更新图书信息DELETE/api/books/{id}删除图书GET/api/books/{id}/similar获取相似图书参数校验是很多新手忽略但极其重要的一环。823项目里我用了Validated注解加上javax.validation规范在DTO上声明校验规则public class BookCreateRequest { NotBlank(message 书名不能为空) Size(max 200, message 书名长度不能超过200) private String title; NotNull(message 分类ID不能为空) private Long categoryId; DecimalMin(value 0.0, message 评分不能小于0) DecimalMax(value 10.0, message 评分不能大于10) private Double ratingScore; // getter/setter... }这样做的好处是所有参数错误都统一交给全局异常处理器返回不会出现那种“字段名拼错了查了半天都查不出来”的糟心事。全局异常处理器用SpringBoot里的RestControllerAdvice实现捕获MethodArgumentNotValidException后把具体错误的message字段返回给前端联调效率能提升一半。4.2 MyBatis分页插件的使用图书列表页翻页是图书系统最高频的操作之一。我用的是com.github.pagehelper.PageHelper这个分页插件集成方式非常友好在pom.xml里引入依赖然后在application.yml里配置好dialect即可。dependency groupIdcom.github.pagehelper/groupId artifactIdpagehelper-spring-boot-starter/artifactId version1.4.7/version /dependency使用时的核心用法是在要分页的查询语句前调用PageHelper.startPage(pageNum, pageSize)紧接着的第一条MyBatis查询就会被自动分页public PageResultBookVO pageBooks(int pageNum, int pageSize, String keyword) { PageHelper.startPage(pageNum, pageSize); ListBookVO bookList bookMapper.selectBookByCondition(keyword); // PageHelper会把查询结果包装成Page对象里面包含total总记录数 PageBookVO page (PageBookVO) bookList; return new PageResult(page.getTotal(), bookList); }这里有个严重的坑必须提醒PageHelper.startPage()只对紧随其后的第一条SQL语句生效。如果你在调用startPage之后又执行了其他数据库操作比如查分类列表分页就会被污染导致数据不准甚至报错。我在项目里就因为在一个service方法里先查了一下热门推荐再查图书列表折腾了整整一个下午才发现是这个原因。4.3 拦截器与Token登录态校验用户登录状态我用JWT来实现搭配SpringBoot拦截器做统一校验。流程很简单用户登录成功后服务端签发一个JWT返回给前端前端把这个Token放在请求头的Authorization字段里后端写一个HandlerInterceptor在preHandle方法里校验Token的合法性。Component public class AuthInterceptor implements HandlerInterceptor { Autowired private JwtUtil jwtUtil; Autowired private RedisTemplateString, String redisTemplate; Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行预检请求 if (OPTIONS.equalsIgnoreCase(request.getMethod())) { return true; } String authHeader request.getHeader(Authorization); if (authHeader null || !authHeader.startsWith(Bearer )) { response.setStatus(HttpStatus.UNAUTHORIZED.value()); response.getWriter().write(未登录或Token缺失); return false; } String token authHeader.substring(7); String userId jwtUtil.parseToken(token); if (userId null) { response.setStatus(HttpStatus.UNAUTHORIZED.value()); response.getWriter().write(Token无效或已过期); return false; } // 将userId放入请求上下文后续controller可以直接获取 request.setAttribute(currentUserId, userId); return true; } }注册拦截器时我用WebMvcConfigurer的addInterceptors方法精确控制拦截路径Configuration public class WebMvcConfig implements WebMvcConfigurer { Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(authInterceptor) .addPathPatterns(/api/**) .excludePathPatterns(/api/auth/login, /api/auth/register, /api/books/hot, /error); } }这里有个细节图书热门列表和登录注册接口必须放行否则用户还没登录连书都看不到。但“获取推荐列表”这个接口一定要拦截因为推荐本来就是个性化的不带用户身份就没有意义。4.4 Redis缓存的设计推荐结果缓存我用的是Rediskey设计成rec:user:{userId}value直接存JSON字符串过期时间24小时。除了推荐结果图书详情也加了缓存key是book:detail:{bookId}。引入Redis后要特别注意缓存穿透的问题。所谓缓存穿透就是查询一个不存在的图书ID缓存里没有数据库里也没有每次请求直接打到数据库。解决方式我用的是布隆过滤器拦截不存在的ID请求。但考虑到图书系统规模不算特别大更简单的办法是当缓存查不到、数据库也查不到时在Redis里存一个空值并设置5分钟过期至少能把恶意请求挡在缓存层之外。public BookVO getBookDetail(Long bookId) { String cacheKey book:detail: bookId; String cacheValue redisTemplate.opsForValue().get(cacheKey); if (cacheValue ! null) { if (EMPTY_MARK.equals(cacheValue)) { return null; } return JSON.parseObject(cacheValue, BookVO.class); } BookVO book bookMapper.selectBookDetail(bookId); if (book null) { redisTemplate.opsForValue().set(cacheKey, EMPTY_MARK, 5, TimeUnit.MINUTES); return null; } redisTemplate.opsForValue().set(cacheKey, JSON.toJSONString(book), 2, TimeUnit.HOURS); return book; }4.5 SpringBoot配置文件的几个关键项823项目里我把配置拆成了三个环境application-dev.yml、application-test.yml、application-prod.yml分别对应开发、测试和生成环境。核心配置如下spring: datasource: url: jdbc:mysql://localhost:3306/book_recommend?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: ****** driver-class-name: com.mysql.cj.jdbc.Driver hikari: maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 30000 redis: host: localhost port: 6379 database: 0 lettuce: pool: max-active: 16 max-idle: 8 mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.bookrec.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImplmap-underscore-to-camel-case这个配置很重要它让数据库的user_id字段自动映射到Java的userId属性不用写一堆resultMap大大减少了SQL映射的工作量。另外log-impl在开发阶段设为StdOutImpl能直接打印SQL调试时太有用了但生产环境一定要去掉否则日志会爆炸。5. 性能优化与踩坑实录5.1 用户相似度计算慢的根因定位项目做到一半时我遇到过一个特别头疼的性能问题推荐接口偶发性超时而且数据库CPU使用率居高不下。排查过程是这样的先看接口日志发现超时集中在用户行为数据更新之后。再看SQL日志发现后台定时任务正在执行全量用户相似度计算把数据库CPU吃满了。当时我的相似度计算是一次性把所有用户的评分数据从数据库查出来然后在内存里做全量比对用户数300多时还好到了800多时内存占用飙升计算耗时以指数级增长。后来我改用了一个更务实的做法不再对所有用户计算相似度而是只对“共同评分超过阈值”的用户候选集做计算。具体思路是先通过SQL筛出当前用户评分过的图书再查询这些图书都被哪些用户评分过形成一个候选用户集合只在这个集合内做相似度计算。这个剪枝策略让计算量从全量O(N²)降到了仅覆盖有些交互的活跃用户性能提升非常明显。5.2 PageHelper分页插件的“分页错乱”谜案前面提到过PageHelper.startPage()只对紧随其后的第一条SQL生效。这次踩坑的具体经历是我写了一个listNewBooksAndHotBooks方法是想一次查出最新上架图书和热门图书两个列表顺手做了分页结果热门图书的列表永远只有10条而且total显示的是最新图书的总数。定位方法很简单打开MyBatis的SQL日志发现PageHelper给两条查询语句都加了LIMIT它把“紧随其后”的语义扩展到了整个线程上下文里只要ThreadLocal里的分页参数没有消费完下一次查询还会继续被分页拦截。这种情况的解决办法有两个一是保证一个线程中startPage之后只执行一条查询二是在执行完分页查询后立即调用PageHelper.clearPage()清理上下文。我最终还是选择了前者因为代码可读性最好也不用担心忘记清理导致其他查询被误伤。5.3 SpringBoot 2.x升级到3.x的兼容性风险虽然823项目用的是2.7.18但我在调研阶段试图直接上Spring Boot 3.0结果遇到了一连串兼容性问题javax包名变成了jakarta很多老依赖不支持JDK17MyBatis插件也出了新版本。这些改动对于新人来说非常劝退。如果你是照着这篇文章来做自己的图书推荐系统我建议一开始就选Spring Boot 2.7.x JDK8这也是目前能找到最多参考资料的组合。等系统完全跑通了想升3.x再一步步升级尽量不要在项目初期就挑战最高版本那是给自己加戏。5.4 测试数据与脏数据清理开发过程中我还专门写了一个DataCleaner工具类用来清理测试产生的脏数据。比如测试用户注册会产生大量垃圾账号测试评分会产生大量随机评分如果不定期清理推荐列表会被污染得很厉害。我通过一个Scheduled(cron 0 0 3 * * ?)定时任务每天凌晨3点自动删除30天前创建且没有登录过的测试账号和它们的行为数据。不要小看这一步它对推荐算法的准确性影响巨大。脏数据会让用户相似度计算出现明显的偏斜比如一个测试账号给某本书打了20次分间接拉低这本书在所有人推荐列表里的权重整体推荐效果都会变差。6. 用Docker一键部署的完整流程6.1 多阶段构建镜像项目完成后部署方式我选择了Docker。这样可以避免“在我电脑上能跑”的尴尬也让后面换服务器变得轻量。我在根目录写了一个Dockerfile采用多阶段构建# 第一阶段编译 FROM maven:3.8-openjdk-8 AS builder WORKDIR /app COPY pom.xml . RUN mvn dependency:go-offline COPY src ./src RUN mvn clean package -DskipTests # 第二阶段运行 FROM openjdk:8-jre-slim WORKDIR /app COPY --frombuilder /app/target/book-recommendation.jar app.jar EXPOSE 8080 ENTRYPOINT [java, -jar, app.jar, --spring.profiles.activeprod]多阶段构建的好处是最终镜像只包含运行所需的JRE和应用jar包不含Maven和源码镜像从几百MB降到一百多MB部署传输都更快。6.2 Docker Compose串联依赖服务图书推荐系统依赖MySQL和Redis所以我用docker-compose.yml一次性编排三个服务version: 3.8 services: mysql: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: root123 MYSQL_DATABASE: book_recommend ports: - 3306:3306 volumes: - mysql-data:/var/lib/mysql networks: - book-net redis: image: redis:6-alpine ports: - 6379:6379 networks: - book-net app: build: . ports: - 8080:8080 depends_on: - mysql - redis environment: SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/book_recommend?useSSLfalseserverTimezoneAsia/Shanghai SPRING_REDIS_HOST: redis networks: - book-net volumes: mysql-data: networks: book-net:注意app服务连接数据库时要使用服务名mysql而不是localhost因为容器之间是通过Docker内部网络通信的。depends_on保证了启动顺序先启动MySQL和Redis再启动应用容器。第一次部署时我在这个坑里卡了很久应用一直报连接数据库超时后来才发现是数据库还没初始化完成就开始连了。解决办法是在应用启动命令里加上等待逻辑或者直接用depends_on配合healthcheck。最简单的做法就是重试几次SpringBoot默认的启动重试机制其实就够了只是需要点耐心。6.3 部署后的性能验证部署完毕后我用JMet启动压测把核心接口过了一遍重点看三个指标QPS、响应时间P99、错误率。热门图书列表接口在开启Redis缓存后QPS能到3000以上P99在80ms以内推荐列表接口在命中缓存时QPS也能上1500P99大概200ms。这个表现对图书推荐系统来说完全够用。压测过程中发现一个环境级别的问题应用容器的最大堆内存默认是物理内存的四分之一如果服务器内存不大会导致频繁GC。我在启动命令里明确了JVM参数-Xms256m -Xmx512m给应用设置了一个安全的内存边界这是部署时必须要注意的。7. 从“能跑”到“好用”我沉淀的几个心得如果说这篇博客只能留下一段话那我想说技术选型是服务于业务目标的图书推荐系统的核心价值不在于用了多前沿的框架而在于推荐结果能不能让用户产生“它懂我”的感觉。823项目做完之后我对推荐这个事的理解已经不只是算了多少分而是理解了“数据质量决定推荐质量”“冷启动要层层兜底”“缓存和异步是性能的发动机”。自己在实际摸索过程中还有几个小体会第一个体会是写推荐系统一定要先建立数据视角。很多新手一上来就写算法但真正应该先做的是数据埋点。用户什么时候看到书、点了多少次、停留了多久、最终有没有借阅这些行为数据比单纯的评分表更丰富将来做更复杂的推荐模型时都是基础。第二个体会是不要迷信精确的算法调参。我在调皮尔逊系数打分权重时花了很多时间后来发现对结果影响最大的不是那些参数而是候选集的召回范围和数据清洗的充分程度。把脏数据清干净、把召回集扩到合理范围之后推荐效果自然就上来了。第三个体会是单体应用也要认真缓存设计。SpringBoot项目做推荐系统很多人会觉得“规模小不用缓存”但缓存带来的不只是性能提升更是架构思维的训练。从什么时候该缓存、缓存Key怎么设计、缓存穿透和雪崩怎么防这套方法论在任何规模的项目里都通用。如果后续还有时间我会在这个系统上做两件事一是把用户行为数据埋点补全引入隐式反馈浏览时长、搜索关键词用加权的方式融合到评分里二是尝试做一个简单的基于内容的推荐通过图书简介的文本向量化来实现相似度计算弥补协同过滤在冷启动阶段的不足。这些都是已经验证可行的方向也算是给823项目预留好的进化路线。
企业数字化 ERP 产品动态
相关推荐
Kubernetes Label与Selector完全指南:从基础语法到发布调度实战 如果你已经跟着系列文章把 Pod、Deployment、Service 都跑通了,大概率会经历这样一个诡异时刻:Service 配好了,端口也通了,但访问死活不成功,最后排查半天发现是 selector 写错。这种问题我不止一次见过,因… · 2026/9/24 19:10:14
Spring Boot电影院购票系统实战:选座并发控制与订单闭环设计 1. 项目整体设计与技术选型1.1 核心需求解析电影院购票系统,看上去是个已经被写烂了的选题,课程设计里有、毕业设计里有、网上开源项目一抓一大把。但真敢说把这个项目做得能上线、能扛住并发、用户操作体验还顺手的,并不多。我当初选这个题目… · 2026/9/24 19:10:14
基于Python的简历智能推荐算法:从TF-IDF到余弦相似度的实践指南 简介:基于Python实现简历智能推荐算法,是一套面向课程设计、毕业设计以及NLP与机器学习初学者的完整项目资源。该项目聚焦招聘场景中的简历与职位描述自动匹配,涵盖文本清洗、去停用词、词形还原等NLP预处理步骤,以及TF-IDF、词嵌… · 2026/9/24 19:10:01
Dopamine 中的 DQN 与 Rainbow 智能体:从三大核心组件到可复现的 Atari 基准实验 强化学习机器学习深度学习 【免费下载链接】dopamine Dopamine is a research framework for fast prototyping of reinforcement learning algorithms. 项目地址: https://gitcode.com/gh_mirrors/dopami/dopamine 点击查看 免费下载 本文以仓库文档 docs/agents… · 2026/9/24 20:25:07
写了三年Vue代码还是一团糟?从病灶到重构的实战指南 写这篇文章的起因挺简单——我在一个技术社群里看到有人问:“写了三年 Vue,为什么每次回头改自己的代码还是想重写?”底下跟了几十条共鸣。我点进他的仓库看了几个文件,说实话,脸有点发烫,因为我刚工作头两… · 2026/9/24 20:25:01
基于Floyd与BP神经网络的轨道客流时空预测实战 简介:这是一份面向本科毕业设计场景的机器学习实战项目,围绕重庆轨道交通客流量开展时空分析与预测。项目将站点抽象为图,用弗洛伊德算法求解多源最短路径,累计各站点和线路的日均客流量;再针对客流最大的十个站点及主… · 2026/9/24 20:25:01
Express、Koa2、Nest.js 三大 Node.js 框架深度对比与选型指南 Node.js 做服务端,绕不开的一个问题就是框架选型。我这些年接手过不少项目,有从零起步的,也有中途接盘别人代码的,Express、Koa2、Nest.js 这三个框架基本都深度用过。说实话,每次有新项目要定技术栈,团队里… · 2026/9/24 20:25:01
SpringBoot+Vue互动课堂小程序:从需求到安全防护的完整实践 每年毕业设计选题季,"互动课堂"这类题目都是绝对的热门,光是标题就能看到「互动小课堂」「互动微课堂」「即时互动学堂」好几个版本。但说句实在话,我见过太多最终交付的成品——登录注册、课程列表、加一个聊天室,就敢… · 2026/9/24 20:25:01
国产大模型客户端深度测评:九大势力多模态与智能体能力对比 1. 国产大模型客户端测评的缘起与选型逻辑1.1 为什么我要做这轮客户端深度测评过去一年多,我一直在做AI应用落地相关的项目,从智能体搭建到多模态处理,从企业内部知识库到面向C端的对话产品,几乎把国内主流的大模型API都接了一遍。… · 2026/9/24 20:24:55
基于YOLOv8的渔船作业监控系统:从环境搭建到边缘部署全流程 简介:这是一套面向计算机、人工智能、自动化等专业学生与教师的毕业设计级项目资源,围绕YOLOv8实现渔船作业监控系统,可用于毕设、课程设计、大作业或项目立项演示。压缩包共97个文件,约24.21MB,以70个Python源码文件为… · 2026/9/24 0:00:13
1D-CNN时间序列建模实战:从Conv1d原理到工业落地 简介:面向时间序列数据建模的一维卷积神经网络完整实现,适合深度学习入门者及需要快速验证时序模型的研究者,能够从音频、文本、传感器或股价等序列中挖掘局部特征与时间依赖。压缩包体积很小,只有3KB,内含3个Python脚… · 2026/9/24 0:00:26
柔软的L:汉语语流中被忽视的舌肌张力控制 1. 这个“L”不是字母表里的L,而是舌尖上的L最近在几个方言群和语音教学社群里,反复看到有人发一句:“也说字母L:柔软的长舌”。初看以为是英语发音课笔记,点开才发现全是方言爱好者、播音系学生、语言康复师甚至戏曲演… · 2026/9/24 0:00:44