news 2026/10/9 10:54:31

Spring Boot美食分享平台实战:技术选型、数据库设计与部署优化

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Spring Boot美食分享平台实战:技术选型、数据库设计与部署优化

1. 美食分享平台项目:为什么我会选 Spring Boot 来做

聊到个人博客、内容社区这类项目,我见过太多人一上来就选很重的方案:微服务先拆四个服务、数据库直接上分库分表、消息队列先挂上。结果往往是开发周期拖到三个月,连用户登录都还没做完。这次做美食分享平台,我一开始就定了个调子:项目定位是社区型内容平台,不是互联网大厂的高并发系统,单体 Spring Boot 就是最务实的答案。

当初这个项目立项时,需求其实非常明确:用户能发图文混排的美食菜谱,能按分类找菜,能收藏、点赞、评论,最好还能关注喜欢的美食作者。说实话,这些功能听起来不少,但仔细拆开之后,都是经典的 CRUD + 关联查询 + 排序分页,没有任何一个需要分布式事务或者大数据量处理。用微服务架构去做这种项目,等于拿航空母舰去跑小区门口的快递三轮车线路,治理成本比业务本身还高。

Spring Boot 在这里的价值不只是"约定优于配置"那几个字。它真正帮我省时间的地方在于:

  • 自动配置机制:引入spring-boot-starter-web就拿到了内嵌 Tomcat 和完整的 MVC 能力,引入spring-boot-starter-data-jpa就有了数据访问层,我只需要写业务代码,不用花两天去拼 ssh 时代的 XML 配置文件。
  • 起步依赖:Redis、MyBatis、验证框架、Swagger,全都是一行依赖的事,版本冲突也由 Spring Boot 的 BOM 统一管理。
  • 生态成熟:菜谱图片上传、全文搜索、定时任务这些需求,在 Spring Boot 生态里都有非常成熟的落地方案,不用自己造轮子。
  • 部署友好:最终打出一个可执行 jar 包,扔到服务器上就能跑,后续如果要上 Docker 也很顺。

搜"springboot"相关热词的时候,我看到很多人在纠结 Spring Boot 版本选取、项目结构规范这些东西。我的经验是:这类内容型项目,别选太新的版本,选稳定版。当时我用的 Spring Boot 2.7.x,不是最新的,但它兼容性最好,遇到坑时网上的解决方案最多。等我把核心功能全部跑通、部署上线、压测完成之后,再去考虑升不升级,这才是合理的节奏。

下面我把整个项目的解剖过程写出来,从功能规划到表设计,从前端接口到部署上线,全部是真实跑过的路径,适合也想做类似内容社区项目的同学参考。

2. 平台功能模块拆解:用户能干什么,系统要撑住什么

2.1 前台用户端:围绕"发现和分享美食"设计功能路径

美食分享平台的核心是内容。我先把用户的典型行为路径画出来——内容生产、内容消费、互动反馈。这三条路径撑起来了整个站点的功能骨架。

内容生产路径:用户注册登录后,可以发布菜谱。菜谱不是简单的文字+图片,我拆成了几个独立字段:标题、封面图、食材清单、步骤说明、分类归属、自定义标签。食材清单我用 JSON 格式存储,因为每道菜的食材数量和单位都不同,关系表反而啰嗦;步骤说明则是按顺序排列的文本列表。发布之后,系统自动生成一条动态推送到"美食广场",同时同步到作者的"个人主页"。

内容消费路径:首页美食广场按时间流展示最新内容和热点内容;分类页按八大菜系加"烘焙""轻食""家常菜"等场景分类;搜索框支持按菜名、食材、作者三种维度查找。用户点进详情页,能看到完整菜谱、作者信息、相关推荐。

互动反馈路径:收藏、点赞、评论、关注。这些功能单独看都很简单,但合在一起就是社区活跃度的发动机。我在详情页放了"收藏数、点赞数、评论数、浏览数"四个指标,用户能直观感受到内容的热度,作者也有动力持续更新。

2.2 后台管理端:内容审核和数据统计一个都不能少

作为平台方,后台管理是所有内容社区项目的刚需。我做了两个大块:

  • 内容管理:菜谱审核(新发布的菜谱先进入待审核状态,管理员可以一键通过或驳回)、分类管理(增删改查分类)、标签管理、热门推荐位设置、置顶内容管理。
  • 用户管理:用户列表、状态管理(封号/解封)、作者认证标记。后台首页放了核心指标:今日新增用户、今日发布菜谱数、总菜谱数、总用户数,让运营者一眼看到平台状态。

这里要特别提醒一点:内容审核不要不做,也不要过度做。我见过很多个人项目完全没有审核环节,结果被人灌了一堆垃圾内容还毫不知情。美食平台虽然内容风险低,但图片资源如果被人当图床用,服务器带宽分分钟被打爆。所以我设计了"先发后审"——用户发布后立刻可见,但管理员后台能看到所有内容并按需处理,这样既不伤害用户体验,又能守住底线。

2.3 系统支撑能力:文件存储、权限控制、定时任务

除了业务功能,还有几个横切面能力是必须提前规划的:

  • 认证授权:用户端用 JWT 做无状态认证,管理员端走独立校验逻辑,两个角色权限分开。
  • 图片处理:用户上传的美食图片,系统要压缩出多尺寸版本(缩略图、列表图、原图),并且做格式统一。
  • 定时任务:每日热点榜计算、数据统计报表、失效 token 清理、未登录访客的临时数据回收。
  • 搜索索引:菜谱的标题、食材、标签需要建立高效检索能力。

这些能力虽然不直接在前台页面上展示,但它们是平台能不能撑住的底层地基。我在做技术选型的时候,盯着这几个横切面能力反复对比,最终确定了下面的技术栈。

3. 技术选型对比:为什么是 MyBatis-Plus 而不是 JPA,为什么 Redis 不能省

技术选型这一步,我花了不少时间,最终确定的核心技术栈是:Spring Boot 2.7 + MyBatis-Plus + MySQL 8.0 + Redis + JWT + MinIO。下面逐个说理由,这些理由都是我在实际编码过程中真实体会到的。

MyBatis-Plus 和 Spring Data JPA 之争。说实话,JPA 的自动建表和一二级缓存机制很适合快速开发,但在做内容社区这种大量自定义查询、多表关联、动态条件分页的场景下,JPA 的复杂查询写起来非常憋屈——不是不能写,而是要么拼接 Specification,要么写 JPQL,调试成本高。MyBatis-Plus 的好处是:简单的单表 CRUD 直接继承BaseMapper就用,复杂的多表查询可以用 XML 自己写 SQL,而且它内置了分页插件,对"列表页+条件筛选"这种需求简直是量身定做。美食广场需要支持"按分类、按标签、按关键词、按时间范围"组合筛选,这种 SQL 很容易写,但用 JPA 能把自己绕晕。另外,MyBatis-Plus 的逻辑删除、自动填充(创建时间、更新时间)也帮我省掉了非常多的样板代码。

MySQL 8.0 的选型。从 5.7 切到 8.0,最直接的感受是:窗口函数真好用。比如"某人发布的所有菜谱中,按收藏数排名",一条 SQL 就能算出来,不需要复杂子查询。8.0 的默认字符集是utf8mb4,对 emoji 支持不需要额外配置——美食分享平台上用户评论里出现 emoji 的概率极高,这个细节不能忽略。

Redis 不是锦上添花,而是必需品。我有三个核心使用场景:点赞数和浏览数的秒级更新(先写 Redis 再异步落库);热门菜谱榜单的实时计算;用户 Session 级别的临时状态。另外,首页的"今日热点"如果每次请求都查 MySQL,高峰期压力很大,我用 Redis 缓存了热点列表,每 5 分钟刷新一次,QPS 从几百降到几十。

MinIO 和云存储的选择。很多教程喜欢直接用七牛云/阿里云 OSS,但在内网部署或个人服务器场景下,MinIO 是完全兼容 S3 API 的开源对象存储,装在服务器上就能提供图片上传下载服务。我选择 MinIO 的理由是可控:不依赖外部厂商,环境隔离、数据私有,以后想迁云也很方便,因为 API 是标准化的。菜谱图片统一走 MinIO,配合 Nginx 做静态资源代理,实际体验下来很稳定。

做技术选型对比时,我整理过一个表格,大家可以直接抄:

技术组件选型结果选型理由
Web 框架Spring Boot 2.7生态成熟、自动配置、稳定版
ORMMyBatis-Plus复杂查询灵活、内置分页、较少样板代码
数据库MySQL 8.0窗口函数、utf8mb4、社区资源多
缓存Redis热点榜单、计数、异步落库
搜索MySQL LIKE + 全文索引中小体量够用,上 ES 是后面的事
文件存储MinIOS3 协议、私有化部署、易迁移
认证JWT无状态、易扩展
接口文档Knife4j在线调试方便,前后端协作神器
构建Maven上手门槛低,和 Jenkins/Docker 都好集成

有个东西我没上——搜索引擎 Elasticsearch。我知道很多内容平台都用 ES 做搜索,但说实话,在数据量没有到几十万条的时候,MySQL 的LIKE模糊查询配合ngram全文索引,性能完全够用,而且省掉了一套独立的集群运维成本。我在搜索设计里做了个折中:菜名和食材字段建了FULLTEXT索引,用MATCH...AGAINST做评分排序,数据量过 10 万条再考虑迁 ES。

4. 数据库设计:从用户表到菜谱详情,12 张核心表的设计思路

4.1 用户、内容、互动三大域建模

数据库设计是我在整个项目里最谨慎的一部分。表结构一旦定了,后面改起来痛苦无比。我按"用户域、内容域、互动域"三个域来建模,一共设计了 12 张核心表。

用户域:

  • user:用户主表,username、password(BCrypt 加密)、nickname、avatar、bio、role(USER/ADMIN)、status、create_time。
  • user_follow:关注关系表,user_id、follow_user_id。关注关系用单独的表而不在用户表里存"关注数",是因为关注数是个频繁更新的聚合值,放主表里会锁行,单独建表后关注数用 Redis 统计即可。

内容域:

  • category:分类表,用于菜谱分类(如川菜、粤菜、烘焙、轻食)。
  • recipe:菜谱主表,这是整个平台的核心表。字段包括:title、cover_image、user_id(作者)、category_id、description、ingredients(JSON 数组,存食材清单)、status(PENDING待审核/PUBLISHED已发布/REJECTED已驳回)、view_count、like_count、favorite_count、comment_count、create_time、update_time。
  • recipe_step:菜谱步骤表,recipe_id 关联菜谱、step_no 步骤序号、content 步骤文本、image_url 步骤图。步骤拆成子表而不是拼成一个长文本,好处是前端可以按步骤渲染出清晰的流程卡片,后续如果要做"只看步骤"的打印版也很方便。
  • tag和recipe_tag:标签表和菜谱标签关联表,多对多关系。

互动域:

  • recipe_like:点赞记录表,recipe_id、user_id、create_time。
  • recipe_favorite:收藏记录表,recipe_id、user_id、create_time。
  • comment:评论表,recipe_id、user_id、content、parent_id(支持楼中楼)。
  • recipe_view:浏览记录表,recipe_id、user_id(未登录为 null)、view_time。

4.2 索引、冗余字段和一些取舍

建表的时候有几条是我反复强调的原则:

  • 读多写少的数据,加冗余字段不是坏味道。菜谱表里的like_count、favorite_count、comment_count这些计数,虽然可以通过 count 查询实时算出来,但列表页一次要展示 20 条菜谱,每条都去 count 一下,数据库直接被打垮至于吗?直接实时 count 必然导致性能灾难,所以我在recipe主表里冗余了这几个计数字段。计数更新通过 Redis 异步做,保证最终一致。

  • 所有关联字段都建索引。这是最基础也最容易漏的——recipe.user_id、recipe.category_id、recipe_step.recipe_id、recipe_tag.recipe_id、recipe_tag.tag_id、comment.recipe_id这些外键字段全部建索引。很多新手只给主键加索引,等数据量大了分页变慢才发现问题,那时候加索引可能已经要锁表了。

  • JSON 字段要克制。食材清单我用 JSON 存储,是因为食材这个对象在业务上没有独立的查询维度,不太需要"按食材搜菜谱"以外的场景。但如果你觉得以后要"按某个食材筛选所有菜谱"这种功能,那还是老老实实拆出ingredient子表,否则 SQL 里查 JSON 字段会非常痛苦。我的项目里搜索食材用的是全文索引,数据存在 JSON 字段里也能被全文索引覆盖到,所以够用。

最终的数据模型长这样:

user ├── recipe (user 发布 recipe) │ ├── recipe_step (recipe 的步骤) │ ├── recipe_tag (recipe 和 tag 关联) │ └── category (recipe 属于分类) ├── recipe_like (用户给菜谱点赞) ├── recipe_favorite (用户收藏菜谱) ├── comment (用户评论菜谱) └── recipe_view (用户浏览菜谱)

这套模型跑下来的感受是:整体够用、扩展性均衡。将来要加"菜谱合集(合集里包含多道菜)"或"活动页(固定一批菜谱参与活动)",只需要在合集表和菜谱之间加一张关联表,不需要动现有表结构。

5. 后端核心实现:认证、上传、搜索、点赞,四个最难啃的点

5.1 JWT 认证和用户权限:Spring Security 配置别硬抄

Spring Security 是所有 Spring Boot 项目里最容易让人翻车的地方,因为网上的教程版本差异极大。我这次统一了思路:用 Spring Security + JWT 做无状态认证,自定义 Filter 完成 token 校验,不碰 OAuth2 那套重东西。

核心逻辑是这样的:

  1. 用户登录成功,后端生成 JWT 返回给前端,前端存到 localStorage。
  2. 前端每次请求在 Header 里带Authorization: Bearer <token>。
  3. 后端自定义一个JwtAuthenticationFilter,继承OncePerRequestFilter,在过滤链里解析 token、校验签名、把用户信息塞进SecurityContext。
  4. 请求进入 Controller 时,通过@AuthenticationPrincipal拿到当前用户。

这里有个容易踩的坑:很多人直接把 Security 配置类从网上抄下来,结果发现WebSecurityConfigurerAdapter在 Spring Boot 2.7 里还能用,在 3.x 里直接废弃了。虽然我用的 2.7 还能用,但建议新项目直接使用SecurityFilterChain的 Bean 配置方式,避免后续升级痛苦。

我贴一下配置的关键代码,大家可以参考这个思路:

@Configuration @EnableWebSecurity public class SecurityConfig { private final JwtAuthenticationFilter jwtAuthenticationFilter; public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter) { this.jwtAuthenticationFilter = jwtAuthenticationFilter; } @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.csrf(csrf -> csrf.disable()) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth -> auth .requestMatchers("/api/auth/**", "/api/recipes/list", "/api/recipes/detail/**").permitAll() .requestMatchers("/api/admin/**").hasRole("ADMIN") .anyRequest().authenticated() ) .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) .exceptionHandling(ex -> ex .authenticationEntryPoint((req, res, e) -> { res.setStatus(401); res.setContentType("application/json;charset=UTF-8"); res.getWriter().write("{\"code\":401,\"msg\":\"未登录或登录已过期\"}"); }) ); return http.build(); } }

注意几个细节:/api/auth/**和公开的菜谱列表接口要放行;后台管理接口单独用hasRole("ADMIN")保护;未登录的拦截要返回 JSON,不能跳转 login 页面,因为这是前后端分离项目。JWT token 的有效期我设成 7 天,用户端每次请求时刷新一次过期时间,管理员台 token 有效期设成 2 小时,权限边界越严越好。

5.2 图片上传:MinIO 集成与压缩处理

美食分享平台的核心资产就是图片,一个精致的菜谱封面或步骤图,能直接影响用户的浏览意愿。图片上传这个功能我踩过不少坑,总结下来最关键的是三点:限制大小、压缩处理、安全校验。

首先,MinIO 的集成非常简单。引入依赖后,配置一个MinioClient的 Bean,上传时调用putObject就完成:

@Service public class FileStorageService { private final MinioClient minioClient; public FileStorageService(MinioClient minioClient) { this.minioClient = minioClient; } public String upload(MultipartFile file, String bucket, String objectName) { try { // 检查 bucket 是否已创建 boolean exists = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucket).build()); if (!exists) { minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucket).build()); } // 上传文件 minioClient.putObject(PutObjectArgs.builder() .bucket(bucket) .object(objectName) .stream(file.getInputStream(), file.getSize(), -1) .contentType(file.getContentType()) .build()); // 拼接访问路径 return "/api/files/" + bucket + "/" + objectName; } catch (Exception e) { throw new RuntimeException("文件上传失败", e); } } }

但直接存原图有两个问题:带宽浪费和加载速度慢。所以我开发时做了三件事:

  • 限制上传文件类型,只接受jpg、jpeg、png、webp,前端和后端双重校验。
  • 按需求压缩:封面图统一压缩到宽度 800px;步骤图压缩到宽度 600px;头像压缩到 200x200。
  • 缩略图单独生成,列表页加载缩略图,详情页加载原图。

这里我推荐一个工具:Thumbnailator,几行代码就能完成图片缩放和格式转换:

BufferedImage image = ImageIO.read(file.getInputStream()); Thumbnails.of(image) .width(800) .outputFormat("jpg") .outputQuality(0.85) .toOutputStream(outputStream);

实测下来,一张 5MB 的相机原图,压缩后大概 300KB 左右,加载速度提升非常明显。还有一个细节:图片文件名不能用用户上传的原始文件名,因为客户端传的文件名可能包含中文、特殊字符甚至路径注入攻击。我用UUID + 时间戳 + 后缀重新生成对象名,从根上杜绝安全问题。

5.3 搜索功能:MySQL FULLTEXT + 标题/食材/标签加权

做搜索功能之前,我认真考虑过要不要上 Elasticsearch,最后决定先在 MySQL 上做。上面说了,目前的数据量根本不值得引入一套 ES,而且 MySQL 8.0 的全文索引对中文支持已经不错了,处理中文需要配置ngram分词插件,但比我预想的简单——只要建表时指定全文索引,查询时用MATCH...AGAINST即可。

具体做法:

CREATE FULLTEXT INDEX ft_recipe_search ON recipe(title, description, ingredients) WITH PARSER ngram;

查询的时候,我用加权搜索让结果更符合"美食平台"的用户预期——标题命中的权重最高,食材命中的次之,描述的权重最低。实现方式是写一条动态 SQL,在 MyBatis-Plus XML 里拼接不同字段的MATCH条件,score 相加后排序。

除了全文搜索,我还额外做了标签预筛选。热搜词里大家搜"炸鸡"或"家常菜",近义词和场景词不好用关键词索引直接覆盖,我把标签作为一种结构化的筛选条件挂到了搜搜接口上——用户搜索后,外层再按标签和时间做二次过滤。这样即使用户输入的关键词比较泛,也能通过标签精确锚定。

如果大家以后真的要把搜索换成 ES,核心索引结构大概这样:recipe_index存菜谱 id、标题、食谱描述、食材列表、标签列表,从 MySQL 同步过去,查询的时候走 ES,然后拿recipeId回表查详情。这个演进路径很清晰,但现阶段还是老老实实用 MySQL。

5.4 点赞、收藏和计数:先写 Redis 再异步落库

点赞/收藏/浏览数这类高频写操作,如果每次都直接写 MySQL,数据库的锁竞争和磁盘 IO 会很密集。我采用的模式是Redis 计数 + 延迟异步落库。

具体逻辑:

  1. 用户点收藏,前端调POST /api/recipe/{id}/favorite。
  2. 后端先把userId写入 Redis 的 Set 集合favorite:recipe:{id}。
  3. 接着把计数递增到 Redis 的 Hash 里,然后返回"收藏成功"给前端。
  4. 每隔一段时间(比如 5 分钟),一个定时任务把 Redis 里的增量同步回 MySQL 的favorite_count字段。

这套模式好处很明显:用户侧响应极快,数据库压力平摊。但坏处也同样明显:万一 Redis 数据丢了,计数会不准确。不过对美食分享平台这种业务来说,计数偏差个位数完全无感知,这属于典型的可用性和一致性的权衡。我的兜底方案是:每晚凌晨跑一次全量统计任务,把每道菜的真实点赞收藏数刷回 MySQL,修正白天可能产生的偏差。

有个并发细节要注意:不可重复点赞。用户点赞之前,要判断Redis Set里有没有这个 userId,没有才执行SADD,这道判断保证了一个用户对一个内容最多只能点一次赞。用关系型数据库做同样的事情需要在user_id + recipe_id上建唯一索引,两条路都可以,Redis 的做法更快,也不产生随机的数据库索引竞争。

6. 前后端对接:接口设计规范、跨域和文件访问代理

6.1 RESTful 接口约定和统一返回结构

前后端分离的项目,接口设计直接影响开发效率和联调体验。我一开始就定下规范:统一返回对象Result<T>,无论成功失败,响应 JSON 结构都一致。

public class Result<T> { private int code; // 200 成功,400 参数错误,401 未登录,500 服务异常 private String msg; private T data; }

接口路径按资源命名:

  • POST /api/auth/register:注册
  • POST /api/auth/login:登录
  • GET /api/recipes/list?page=1&size=10&categoryId=xx&tagId=xx&keyword=xx:菜谱列表
  • GET /api/recipes/detail/{id}:菜谱详情
  • POST /api/recipes:发菜谱
  • POST /api/recipes/{id}/like:点赞
  • POST /api/recipes/{id}/favorite:收藏
  • GET /api/users/{id}/profile:用户主页
  • GET /api/users/{id}/followers:粉丝列表

我特别建议在开发阶段就接上Knife4j(Swagger 的增强版)。它能在浏览器里直接测试所有接口,对调试 JWT 这类带认证的接口非常方便——在文档页面填好 token,点一下就能调用,比用 Postman 手动拼参数效率高不少。

6.2 跨域处理与文件访问映射

前后端分离部署时,跨域问题是首要拦路虎。前端跑在http://localhost:5173(Vite 默认端口),后端跑在http://localhost:8080,前端直接发 Ajax 请求会被浏览器拦截。我的处理方式是后端配置 CORS:

@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }

注意:allowCredentials(true)时allowedOrigins("*")在allowedOriginPatterns("*")是可以配合的。实际生产环境建议把allowedOriginPatterns配成真实前端域名,不要用*或字符串匹配,减少恶意站点的跨域请求。

另外,MinIO 里存的文件不能直接暴露给前端,否则会出现另一个跨域问题,而且 MinIO 的外网访问端口如果直接开放也有安全隐患。我的方案是通过 Nginx 反代/api/files/**路径到 MinIO 端口,前端看到的 URL 永远是后端域名:

location /api/files/ { proxy_pass http://127.0.0.1:9000/; proxy_set_header Host $host; }

这样图片的访问路径和 API 域名保持一致,后端在生成访问 URL 时也不需要硬编码 MinIO 的内网端口。

7. 部署上线与性能调优:宝塔面板 + Docker,以及压测后的三个教训

7.1 服务器部署方案

搜索热词里有不少"宝塔docker部署springboot"和"springboot 阿里云构建地址",可见大家对部署环节的关注度非常高。我的部署方案很简单:一台 2C4G 云服务器 + 宝塔面板 + Docker Compose。主要跑四个容器:MySQL、Redis、MinIO、后端 jar 包。前端打包成静态文件后丢到 Nginx 的html目录,反代后端 API。

Docker Compose 文件的核心片段:

version: "3.8" services: mysql: image: mysql:8.0 container_name: food-mysql environment: MYSQL_ROOT_PASSWORD: yourpassword MYSQL_DATABASE: food_share ports: - "3306:3306" volumes: - ./mysql-data:/var/lib/mysql redis: image: redis:7-alpine container_name: food-redis ports: - "6379:6379" minio: image: minio/minio container_name: food-minio command: server /data --console-address ":9001" ports: - "9000:9000" - "9001:9001" environment: MINIO_ROOT_USER: minioadmin MINIO_ROOT_PASSWORD: minioadmin123 volumes: - ./minio-data:/data backend: build: . container_name: food-backend depends_on: - mysql - redis - minio ports: - "8080:8080"

用 Docker 部署的好处是环境隔离、迁移方便。如果想精简,也可以不用 Docker,直接在宝塔面板里安装 MySQL、Redis、MinIO,后端 jar 用系统服务进程守护,效果差不多,看个人习惯。

7.2 压测后做的三个优化

项目上线前,我用 JMeter 做了基础压测,模拟 100 个并发用户持续操作 10 分钟。发现的问题和优化办法如下:

第一个教训:首页热点接口响应时间从 50ms 涨到 1200ms,原因是每次请求都在实时计算热度值。问题定位后,我做了俩改动:热点数据 Redis 缓存(5 分钟过期),热度值从"浏览量+收藏量3+点赞量2"的加权积分中取值,计算结果直接存 Redis。优化后接口 P99 稳定在 80ms 左右。

第二个教训:图片懒加载要配占位图,否则首屏图片加载数量过多。菜谱列表页一页 20 道菜,每道菜一张封面缩略图,如果用户网速一般,页面会白屏很久。我把封面图全部换成 WebP 格式,加载时先显示后端返回的低清占位图,目标图加载完再替换,体感改善非常明显。

第三个教训:MySQL 连接池别用默认配置。默认的connectionTimeout=30000、maximumPoolSize=10在高并发下不够用。我调成了maximum-pool-size: 30,minimum-idle: 5,connection-timeout: 5000(防止长时间排队)。同时在application.yml里开启了 MySQL 的连接验证和空闲回收,避免长时间空闲连接被数据库服务端断开后,应用还拿着死连接去访问。

还有一点,网上搜"springboot定时任务"经常能搜到@Scheduled的写法,我在项目里确实也用了这个注解做每日榜单刷新。不过要提醒一下:@Scheduled只适合单实例部署。如果以后项目扩到两台机器同时跑后端,定时任务会被触发两次,这时候就要引入分布式锁或独立的调度服务了。单体阶段用@Scheduled完全合理,但要心里有数这是后续架构升级时要处理的点。

8. 整个项目做完后,我最想分享的几个心得

项目从立项到上线稳定运行,前前后后折腾了三周多。如果让我重新做一遍,有几个决策我会坚持,有几个时间会被我省下来。

坚持用 MyBatis-Plus 而不是 JPA。不是因为 JPA 不好,而是内容型社区项目的查询绝大多数是定制化的列表查询,MyBatis-Plus 在这种场景下写 SQL 的直观性和可控性远超 JPA。团队协作也更简单,新同事接手时看 XML 里的 SQL 就能立刻理解业务逻辑。

坚持把用户认证和权限前置设计。我见过太多项目先做业务后补安全,最后业务代码里到处散落着判断当前用户的逻辑。我在第一个迭代就把 JWT 认证、角色校验、接口权限全部搭好,后面的所有业务开发都在这个安全框架内进行,省掉了大量返工。

坚持做接口文档而不是口头约定。Knife4j 在开发期和联调期的价值被严重低估了。前后端并行开发时,后端把接口定义好,注解写清楚,前端根本不用追着问参数,直接在文档页面看示例、调试接口。这个习惯保持到项目后期,能让协作效率提升一半。

从热搜词反复出现的"springboot项目结构""springboot配置""springboot 自定义自动配置"来看,现在很多初学者还在跟配置搏斗。我建议这类同学不要沉迷于从零手写配置,先跑通一个完整的业务闭环,再回头琢磨底层原理。我这款美食分享平台做完,最大的成长不在于学会某个框架的 API,而在于理解了"一个内容平台的完整生命周期是怎样的"——这个理解,比具体的技术栈选择更值钱。

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

Vue+ECharts动态地图实战:数据驱动着色与下钻联动

1. 项目背景与核心需求拆解1.1 为什么要在Vue项目里做动态地图做过数据大屏或者后台管理系统的朋友应该都有体会&#xff0c;静态地图早就满足不了业务需求了。所谓动态地图&#xff0c;核心诉求无非这么几类&#xff1a;地图区域能根据数据变化自动着色、点击某个省份能下钻到…

作者头像 李华
网站建设 2026/10/9 10:54:14

小波分解+BP神经网络风电功率预测实战指南

简介&#xff1a;本资源是一份面向电力系统、新能源预测及人工智能应用方向的科研与工程实践者的技术文档&#xff0c;聚焦风电功率不确定性带来的电网调度难题&#xff0c;提出融合小波分析与BP神经网络的高精度短期预测方法。文档系统阐述了小波分解&#xff08;DB4四层&…

作者头像 李华
网站建设 2026/10/9 10:53:31

kubeadm集群升级全攻略:从控制平面到worker节点的安全实践

1. 升级前的准备工作与版本策略1.1 先想清楚&#xff1a;为什么升级 kubeadm 集群要卡版本kubeadm 集群升级&#xff0c;最容易踩的坑就是不看版本路线&#xff0c;直接往新版冲。Kubernetes 官方的支持策略是&#xff1a;kubeadm 只能从一个次版本升级到相邻的下一个次版本&am…

作者头像 李华
网站建设 2026/10/9 10:53:31

Java Web轻量级供求信息网源码解析与实战部署

简介&#xff1a;这是一套基于Java Web技术栈开发的都市供求信息网完整项目源码&#xff0c;面向Java初学者与Web开发入门者&#xff0c;适用于课程设计、毕业设计及小型信息发布平台实践。项目采用JSPServletMySQL架构&#xff0c;前后台功能完备&#xff1a;前台支持信息列表…

作者头像 李华
网站建设 2026/10/9 10:53:15

Agent Skills,一篇就够了:用TaoToken统一Key跑通SKILL.md与MCP上下文

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/9 10:50:30

注意力机制中文聊天机器人:模型加载与直接运行实战指南

简介&#xff1a;面向机器学习与自然语言处理初学者的中文聊天机器人项目&#xff0c;是大学生课程设计作品&#xff0c;基于注意力机制与序列模型构建&#xff0c;能够理解中文语境并生成自然回复。项目已提供预训练模型&#xff08;.h5&#xff09;&#xff0c;下载后无需重新…

作者头像 李华