基于SpringBoot和Vue的H5知识共享平台开发实践
2026/9/16 14:08:09 网站建设 项目流程

简介:基于Spring Boot与Vue构建的H5知识共享平台源码包,采用前后端分离架构,前端为Vue+Element UI的H5页面,后端以Spring Boot、MyBatis、MySQL、ES、Redis、MQ为核心,并区分用户端与管理端。平台支持用户注册登录、发布图文或视频、后台审核、浏览点赞评价,并借助MQ异步发送邮件通知,完整呈现内容发布到审核上线的业务闭环;项目特意关闭转发分享功能,代码中已预留实现,便于需要时开启。压缩包约60MB,共2000个文件,以js、json、ts、java、vue等前后端工程文件为主,也包含HTML样式与文档等辅助内容,目录结构清晰。已有222人学习下载,适合具备一定Java与Vue基础的学生用于课程设计、毕业设计参考或个人网站二次开发。

1. H5知识共享平台为什么选 SpringBoot + Vue 这套组合

一个几十人规模的技术团队要做内部知识库,需求通常很具体:文档能上传、视频能播放、内容能按类目检索、不同角色能看到不同范围的内容,而且要在手机浏览器、微信公众号、企业微信甚至App内嵌的WebView里都能正常打开。如果直接做原生App,双端开发和发版周期都太长;如果只做静态页面,权限和文件存储又无从谈起。最后基本都会落到“前后端分离的H5”这个形态上。

SpringBoot把接口、鉴权、文件存储和搜索结果的管理收敛在一个工程里,Vue负责页面渲染和路由控制,H5则让一套代码同时被微信生态和App容器复用。这套组合真正的价值不是技术栈新颖,而是团队人力资源最省:后端用SpringBoot快速出接口,前端用Vue只管用户界面,两个角色只要谈拢JSON结构就能并行。适合企业内部知识库、中小团队内部文档站,以及课程设计级别的移动端共享平台。

2. 先用 IDEA 把 SpringBoot 后端工程跑起来:目录、配置与第一个接口

2.1 用 IDEA 创建 SpringBoot 项目时,依赖勾选和版本选择

后端工程用 IDEA 的 Spring Initializr 生成最快。打开 IDEA,选择 New Project,左侧选 Spring Initializr,Group 填com.example,Artifact 填knowledge-platform,Type 选 Maven,Java 版本按本机环境选 17 或 21 均可。

依赖这一步不要全勾,够用即可:Spring Web 提供 MVC 能力,Validation 负责参数校验,MySQL Driver 连数据库,Lombok 减少实体类样板代码。后续需要操作 Redis 时再手动加spring-boot-starter-data-redis,不需要在一开始就把全家桶塞进工程里。版本选择上,SpringBoot 3.x 是目前的主流,但要注意 3.x 的包名从javax换成了jakarta,网上很多旧教程的import javax.servlet会直接编译失败;如果团队里其他成员更熟悉 SpringBoot 2.7,继续用 2.7 也没问题,不需要盲目追新。

pom.xml 里核心依赖如下:

<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.2.5</version> <relativePath/> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies>

这段配置里最需要注意的是没有显式写版本号,全部交给spring-boot-starter-parent做依赖管理,避免自己维护一堆版本互相冲突。如果项目里需要 MyBatis-Plus,追加mybatis-plus-spring-boot3-starter,注意选择适配 SpringBoot3 的mybatis-plus版本,否则启动时会因为被 Spring 容器扫描到不兼容的自动配置类而直接报错。创建完工程后先跑一次mvn spring-boot:run,确认空的 SpringBoot 应用能启动,再继续往下写代码。

2.2 application.yml 多环境配置与连接池参数

SpringBoot 配置里面最容易出问题的是数据库连接。开发、测试、生产环境的数据源地址不一样,建议拆成application.ymlapplication-dev.ymlapplication-prod.yml三份文件。主配置里只放公共项,环境相关的内容放到对应的配置文件中,启动时通过SPRING_PROFILES_ACTIVE环境变量指定当前环境。

以下是一份开发环境的 yml 配置:

server: port: 8080 servlet: context-path: /api spring: datasource: url: jdbc:mysql://localhost:3306/knowledge_platform?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: ${DB_PASSWORD:root} hikari: minimum-idle: 5 maximum-pool-size: 20 connection-timeout: 30000 idle-timeout: 600000 servlet: multipart: max-file-size: 100MB max-request-size: 200MB mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0

配置项里几个点需要单独说明。context-path: /api表示所有接口都挂在/api前缀下,前端联调时不用在代码里写死环境地址,也方便后续在网关层统一转发。数据库密码用${DB_PASSWORD:root}这种占位符写法,形如“SpringBoot yml 密文”的诉求,生产环境建议把密码放到环境变量或配置中心,不要明文提交到 Git。HikariCP 连接池的maximum-pool-size不是越大越好,对 MySQL 单库来说 20 已经足够,过大会增加数据库端线程切换开销。

map-underscore-to-camel-case开启后,数据表的create_time会自动映射到 Java 字段createTime,少写很多@TableField注解。多文件上传的限制写在spring.servlet.multipart里,知识平台里通常会有视频和 PDF,max-file-size直接给到 100MB。

2.3 文章列表接口的 Controller、参数与统一返回体

后端第一个要写的接口是文章分页列表。这里用 MyBatis-Plus 的BaseMapper简化数据访问,实体类Article对应article表,字段包括idtitlecategory_idauthor_idcover_urlcontentcreate_timedeleted。Controller 层代码尽量保持薄,只做参数接收和结果包装,业务判断放到 Service 中。

@RestController @RequestMapping("/article") @RequiredArgsConstructor public class ArticleController { private final ArticleService articleService; @GetMapping("/page") public Result<PageResult<ArticleVO>> page(@Valid ArticleQuery query) { PageResult<ArticleVO> page = articleService.pageByCondition(query); return Result.ok(page); } }
@Data public class ArticleQuery { private long page = 1; private long size = 10; private Long categoryId; private String keyword; }

查询参数用ArticleQuery对象接收,前端传参时直接使用GET /api/article/page?page=1&size=10&categoryId=3&keyword=SpringBoot。分页参数page从 1 开始,size默认 10,避免未传参时查全表。categoryIdkeyword都是可空字段,在 Service 拼条件时需要用if判断是否为 null,不能直接把空值拼进 SQL。Result<T>是统一返回体,包含codemessagedata三个字段,前端 Axios 拦截器只需要判断code === 200就能确定业务是否成功。

这个接口参数对应的具体含义见下表。

参数名类型必填说明
pagelong页码,默认 1
sizelong每页条数,默认 10,最大建议不超过 50
categoryIdLong类目 ID,为空时查询全部分类
keywordString标题模糊搜索关键词

3. Vue3 前端与后端联调:路由、状态、Axios 请求封装

3.1 用 Vite 初始化 Vue3 工程并按模块划分目录

前端工程使用 Vite 创建,命令是npm create vite@latest knowledge-h5 -- --template vue。这个模板生成的是 Vue3 组合式 API 的基础结构,不包含路由和状态管理,还需要额外安装依赖。

cd knowledge-h5 npm install npm install vue-router@4 pinia axios element-plus @element-plus/icons-vue npm run dev

依赖安装完毕后,在src下按功能划分目录:api放接口定义,router放路由配置,store放 Pinia 状态,views放页面组件,layout放底部导航和头部布局。如果开发机没有外网访问 npm 源,可以先执行npm config set registry切换到镜像源再执行 install,这一步是 Vue 安装依赖最常见的卡点之一。

3.2 vue-router 路由配置与 H5 页面的懒加载

H5 页面的路由设计和 PC 后台不同,底部 Tab 栏通常只有首页、知识库、我的三个入口,其余详情页、上传页都从 Tab 页面通过路由跳转进入。路由配置使用懒加载,把页面组件的 import 写进一个函数里,在浏览器访问对应路径时才加载对应资源,减少首屏加载时间。

import { createRouter, createWebHashHistory } from 'vue-router' const routes = [ { path: '/', component: () => import('@/layout/H5Layout.vue'), children: [ { path: '', redirect: '/home' }, { path: '/home', name: 'Home', component: () => import('@/views/HomePage.vue') }, { path: '/library', name: 'Library', component: () => import('@/views/LibraryPage.vue') } ] }, { path: '/article/detail', name: 'ArticleDetail', component: () => import('@/views/ArticleDetail.vue') } ] const router = createRouter({ history: createWebHashHistory(), routes }) export default router

这里选择createWebHashHistory而不是createWebHistory,是因为 H5 页面会被嵌入到微信、钉钉或原生 App 的 WebView 中,这些容器刷新页面时不一定能正确回传 URL 给后端服务,哈希路由能够保证任意页面刷新都不会出现 404。路由参数最常见的问题是混淆queryparamsquery是 URL 上的问号参数?id=3,刷新后仍然在地址栏里;params只能在push时通过 name 方式传递,刷新后参数不在了。详情页跳转建议用query,因为 H5 端分享链接时需要完整 URL,只有params的话别人拿到链接会打不开。

3.3 Axios 实例的拦截器与 Token 失效处理

联调阶段最简单的是直接把http://localhost:8080/api写进代码,但这在真机和微信里会直接失败,因为手机访问不了电脑的 localhost。前端工程一般建立统一的 Axios 实例,用环境变量管理后端地址。

import axios from 'axios' import { ElMessage } from 'element-plus' const service = axios.create({ baseURL: import.meta.env.VITE_API_BASE, timeout: 15000 }) service.interceptors.request.use(config => { const token = localStorage.getItem('access_token') if (token) { config.headers['Authorization'] = 'Bearer ' + token } return config }) service.interceptors.response.use( response => { const res = response.data if (res.code !== 200) { ElMessage.error(res.message) return Promise.reject(new Error(res.message)) } return res.data }, error => { if (error.response && error.response.status === 401) { localStorage.removeItem('access_token') window.location.href = '/login' } return Promise.reject(error) } ) export default service

请求拦截器的作用是在每次 HTTP 请求发出前,从 localStorage 取出登录时保存的 token,拼到Authorization请求头里。后端的鉴权过滤器会校验这个头,识别当前登录用户是谁。响应拦截器做两件事:业务码code !== 200时统一弹错误信息,避免每个页面重复写处理逻辑;HTTP 状态码 401 表示 token 过期或被吊销,此时清理本地存储并跳回登录页。这样封装后,页面里的调用就变得很干净,只需要关注业务成功后的data

4. 知识内容的上传、存储与全文检索在前后端是如何落地的

4.1 用 MultipartFile 接收文档上传,并生成访问链接

知识平台的文件上传不能只传文本,需要支持用户上传 PDF、Word、图片和视频。后端接口用MultipartFile接收文件流,把文件写到本地磁盘目录,然后在数据库里保存一个/files/xxx.pdf的相对路径,最终由 Nginx 或者网关层对/files/**做静态资源映射。如果公司有对象存储服务,这一层可以替换为把流上传到对象存储,然后回传一个内网 URL,接口结构不用改。

上传接口的常见写法如下:

@PostMapping("/upload") public Result<FileUploadVO> upload(@RequestParam("file") MultipartFile file) { String originalFilename = file.getOriginalFilename(); String ext = StringUtils.getFilenameExtension(originalFilename); String fileName = UUID.randomUUID().toString().replace("-", "") + "." + ext; Path path = Paths.get(uploadDir, fileName); file.transferTo(path); FileUploadVO vo = new FileUploadVO(); vo.setUrl("/files/" + fileName); vo.setSize(file.getSize()); return Result.ok(vo); }

UUID.randomUUID()生成文件名,是为了避免用户上传重名文件时互相覆盖,也防止文件名里带中文和特殊字符导致访问链接异常。file.transferTo是 Spring 提供的方法,可以接受一个File或者Path对象,内部处理临时文件的复制。这里必须手动校验文件后缀白名单,只允许jpg/png/mp4/pdf/docx,否则恶意用户上传一个.jsp或者.html,一旦被当作脚本执行,整个 H5 页面都会受影响。uploadDir在配置文件里指定,生产环境不要放在src/main/resources下面,否则打成的 jar 包里的路径是只读的。

4.2 文章分页查询与类目过滤的条件拼接

列表页通常需要在类目、关键词、时间三个维度过滤。MyBatis-Plus 的LambdaQueryWrapper可以用链式方法拼条件,代码写起来比 XML 里的动态 SQL 直观不少。为了统一处理分页和过滤,Service 里定义一个pageByCondition方法。

public PageResult<ArticleVO> pageByCondition(ArticleQuery query) { Page<Article> page = new Page<>(query.getPage(), query.getSize()); LambdaQueryWrapper<Article> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(query.getCategoryId() != null, Article::getCategoryId, query.getCategoryId()) .like(StringUtils.hasText(query.getKeyword()), Article::getTitle, query.getKeyword()) .orderByDesc(Article::getCreateTime); Page<Article> result = articleMapper.selectPage(page, wrapper); return PageResult.of(result.getRecords(), result.getTotal(), query.getPage(), query.getSize()); }

LambdaQueryWrapper的第一个参数是布尔表达式,条件为 true 时才会把对应条件拼到 SQL 上,这样categoryId为空时就不生成WHERE category_id = null这种无效语句。like方法做的是%keyword%模糊匹配,只适合对小数据量的标题字段做快速查询。这里有一个性能隐患需要提前知道:selectPage返回的 total 会额外执行一条count(*),当数据量超过几十万时这条 count 查询会成为瓶颈,届时要手动改造为只统计 ID 数量或者使用 ES 的 total 关系。

类目过滤的典型路由参数结构如下表。

场景路径参数示例
全部文章/article/pagepage=1&size=10
按分类筛选/article/pagecategoryId=java
标题搜索/article/pagekeyword=SpringBoot

4.3 搜索从 LIKE 到全文索引的升级节奏

当知识平台的文档数量增长到几千篇时,LIKE '%关键字%'依然可用,但一旦文章内容也参与搜索,用户搜索的就不是“标题里包含什么”,而是“正文里提到过什么”,这个场景下标准的LIKE就不再适合了。

MySQL 提供了FULLTEXT全文索引,可以在文章标题和正文上建立全文索引,查询时使用MATCH ... AGAINST。在数据表上执行的建索引语句如下:

ALTER TABLE article ADD FULLTEXT INDEX ft_article_content (title, content) WITH PARSER ngram;

WITH PARSER ngram是 MySQL 5.7 以上对中文全文检索的支持方式,ngram 分词器把连续的中文文本切成固定长度的字符序列,解决中文没有空格分词的问题。查询时不再使用LIKE,改为:

SELECT id, title FROM article WHERE MATCH(title, content) AGAINST('SpringBoot' IN NATURAL LANGUAGE MODE);

这个方案的边界很清晰:MySQL 全文索引不支持中文同义词、拼音纠错、相关度排序权重调整,公司内部知识库如果只是搜标题和简短摘要,在数据量 10 万以内够用。如果搜索词包含英文缩写、版本号(比如 JDK 17)、中英混合,全文索引的召回率会明显下降,这时候再迁移到 Elasticsearch 或者 MeiliSearch 不迟。我一般建议小团队先上 MySQL 全文索引,把content字段存一份到独立的搜索表里,后续迁 ES 时不用改业务表的 schema。

5. H5 页面在微信、钉钉、App 内嵌环境下的适配与踩坑

5.1 viewport 与底部安全区域的适配

H5 页面和 PC 页面最大的差别在于视口尺寸。Vue 页面默认的 HTML 结构在移动端会出现横向滚动和字体缩放的问题。在index.html里把 viewport 写全,是最基础但也是最容易漏掉的适配。

<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no" />

width=device-width让布局视口等于设备宽度,maximum-scale=1user-scalable=no禁止用户双指缩放页面。对于底部有操作栏的页面,还需要考虑带有 Home 指示条的全面屏手机,CSS 里加一句安全区域适配:

.bottom-bar { padding-bottom: env(safe-area-inset-bottom); }

env(safe-area-inset-bottom)是 iOS 实际高度,Android 的 WebView 对手指操作区的处理不太统一,需要配合viewport-fit=cover一起用,否则这个变量可能为 0。App 内嵌 H5 时,外层 Android 的 WebView 默认会启用夜间模式和系统字体缩放,如果页面字体异常变大,可以在 WebView 侧关闭 textZoom 并将 forceDark 关掉,这些问题在前端 CSS 里是调不回来的。

5.2 微信公众号内获取地理位置的鉴权流程

知识共享平台有一个常见需求是根据用户位置推荐附近的文档,这在 H5 端不能直接调用浏览器navigator.geolocation,微信浏览器必须走微信 JS-SDK 的wx.getLocation。调用前第一步是在后端生成签名串,前端再执行wx.config

public WxJsConfig createJsConfig(String url) { String nonceStr = UUID.randomUUID().toString().substring(0, 16); long timestamp = System.currentTimeMillis() / 1000; String sign = sha1("jsapi_ticket=" + ticket + "&noncestr=" + nonceStr + "&timestamp=" + timestamp + "&url=" + url); WxJsConfig config = new WxJsConfig(); config.setAppId(appId); config.setNonceStr(nonceStr); config.setTimestamp(timestamp); config.setSignature(sign); return config; }

signature的生成算法固定,参与签名的四个参数jsapi_ticketnoncestrtimestampurl顺序不能错,url必须是当前页面完整的location.href去掉 hash 部分后的地址。jsapi_ticket的有效期也是 7200 秒,获取后要在后端做缓存,不能每次请求都去微信接口拉取。前端拿到签名后调用:

wx.config({ debug: false, appId: res.appId, timestamp: res.timestamp, nonceStr: res.nonceStr, signature: res.signature, jsApiList: ['getLocation'] })

wx.config只是注入配置,实际触发定位是后面调用wx.getLocation,这个 API 会先弹窗询问用户授权,用户拒绝后会在fail回调里返回错误。调试时看到的错误码 40163 表示 code 已过期,3301 表示签名不对,优先检查参与签名的 URL 和当前页面的 URL 是否完全一致,这是最常见的问题,多见于 H5 在 WebView 里被二次跳转后自行改了地址。

5.3 Vue 播放 m3u8 视频流:video.js + hls.js 的开箱组合

知识共享平台里常见一类需求是上传的讲课视频在 H5 页面直接播放。早期 PC 时代用 Flash 播放 m3u8,现在浏览器和 WebView 都不支持,要靠 Media Source Extensions 实现流式播放。目前方案成熟的是video.jshls.js插件,先安装依赖:

npm install video.js hls.js

Vue 组件里需要手动初始化播放器,代码片段如下:

<template> <video ref="videoEl" class="video-js vjs-big-play-centered" controls playsinline></video> </template> <script setup> import { onMounted, ref } from 'vue' import videojs from 'video.js' import 'video.js/dist/video-js.css' const props = defineProps({ src: String }) const videoEl = ref(null) onMounted(() => { const player = videojs(videoEl.value, { sources: [{ src: props.src, type: 'application/x-mpegURL' }] }) }) </script>

playsinline这个属性在 iOS Safari 里很关键,不加它视频会自动进入全屏播放模式。type必须指定为application/x-mpegURL,video.js 和 hls.js 才知道这个地址按 HLS 协议去解析。生产环境常见的问题不是播放器本身,而是后端返回 m3u8 文件时要加响应头Access-Control-Allow-Origin: *,HLS 的 ts 分片会发起独立的 HTTP 请求,如果静态文件服务没有配置 CORS,浏览器控制台会报跨域错误,但页面不白屏,不容易定位。另外注意 m3u8 的视频源如果是在内网里,H5 页面部署在公网,播放请求也需要走内网穿透或专线,不要在业务接口里用HttpURLConnection去拉流再吐给前端,这样内存占用会直接拖垮后端服务。

5.4 小程序内嵌 H5 的返回按钮与 App 内嵌时的 token 注入

知识平台经常被嵌入到微信小程序和钉钉的工作台中。小程序里用web-view的 H5 页面,左上角默认有一个返回箭头,但如果 H5 内部有history.go(-1)或者页面 JS 调用了浏览器历史相关 API,返回箭头会消失或行为错乱。H5 页面不应主动管理历史栈,尽量让用户通过页面内的按钮跳回首页。

App 内嵌 H5 的场景里,token 不能出现在 URL 参数上,因为 URL 会被系统 WebView 缓存,泄露风险很高。常见做法是原生 App 通过 WebView 的JSBridge注入一个方法,H5 端通过window.xxxSDK.getToken()拿到登录态。如果原生端没有提供接口,可以在首次加载 expect 时通过 sessionStorage 传递,但这种方式在页面刷新后就会失效,只能作为临时联调方案。

不同容器的差异化表现见下表。

容器token 传递方式定位能力常见问题
微信浏览器公众号网页授权需 JS-SDK 签名签名 URL 不一致
微信小程序 web-viewURL/JSBridge小程序组件提供H5 返回箭头消失
钉钉工作台钉钉免登 code需调用钉钉 JSAPI权限配置不当报无权限
原生 App WebView原生注入对象原生定位接口Android 6.0 以上需要动态申请权限

钉钉环境里如果 H5 调用了录音或定位能力,控制台会报类似no permission info for action:device.audio.startrecord的错误,这是钉钉后台的权限配置没打开,不是代码问题,通知管理员在应用权限管理里勾选对应权限即可。

6. 用 SpringCache 和 Redis 把 H5 文章列表接口从 800ms 压到 50ms

H5 知识库的首页打开时,前端会一次请求分类、推荐文章、公告三个接口,如果每次请求都打到 MySQL,数据库压力非常大。我常用的做法是给列表接口加一层 SpringCache 缓存,用 Redis 作为缓存存储,把热点数据在缓存里放 5 分钟,后端代码改动量非常小。

第一步在启动类上开启缓存支持:

@SpringBootApplication @EnableCaching public class KnowledgeApplication { public static void main(String[] args) { SpringApplication.run(KnowledgeApplication.class, args); } }

然后改造 Service 里的分页查询方法,加一个@Cacheable注解,如图所示核心改动只有注解:

@Override @Cacheable(value = "articlePage", key = "#query.page + '_' + #query.size + '_' + #query.categoryId + '_' + #query.keyword") public PageResult<ArticleVO> pageByCondition(ArticleQuery query) { // 原有 MyBatis-Plus 查询逻辑不变 }

@Cacheable的执行顺序是第一次调用时先查数据库,把结果存进 Redis;第二次及以后调用,如果 key 存在就直接返回缓存,不执行方法体。key 由分页参数、类目 ID、关键词拼接而成,这意味着淘宝首页那种大而全的列表页会被分成几百个缓存键,命中率取决于真实场景的查询分布。对于内部知识平台,用户通常只集中在几个默认分类和热门关键词,这样的缓存设计命中率很高。

更新文章内容时,要主动删除对应的缓存,否则修改不生效:

@CacheEvict(value = "articlePage", key = "#query.page + '_' + #query.size + '_' + #query.categoryId + '_' + #query.keyword")

@CacheEvict放在更新方法和删除方法上,执行时机是在方法执行成功后清除缓存。这里的 key 表达式必须与@Cacheable的 key 完全一致,否则删的是不存在的 key,旧缓存永远不被清理。更稳妥的方式是使用CacheManager.clear("articlePage")在文章增删改时把整个分页缓存清空,虽然会短暂丢失热点数据,但能保证一致性不复杂。

压测验证直接用本机的ab命令完成,不需要复杂工具:

ab -n 5000 -c 50 -H "Authorization: Bearer $TOKEN" \ "http://localhost:8080/api/article/page?page=1&size=10"

压测命令里-n 5000表示总共发送 5000 个请求,-c 50表示同时 50 个并发。压测结果重点看Requests per second(吞吐量)和Percentage of requests served within a certain time(响应时间分布)。优化前 5000 个请求平均耗时通常在 500~800 毫秒,因为每次请求都要执行count(*)和分页查询;加缓存后的第一次请求仍然是几百毫秒,之后的请求响应时间会下降到 30 到 50 毫秒,吞吐量从每秒几十提升到几百,数据库端可以看到慢查询日志里不再出现高频的分页 SQL。

缓存只解决读多写少的场景。知识库的文章一旦频繁被用户评论、收藏,每篇文章内容被修改的概率并不高,5 分钟过期时间是业务可接受的值。如果以后加了搜索功能,这类列表缓存的思路依然适用,只是缓存 key 要加一个新的维度query字符串,并监控 Redis 内存使用量。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询