简介:基于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.yml、application-dev.yml、application-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表,字段包括id、title、category_id、author_id、cover_url、content、create_time、deleted。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,避免未传参时查全表。categoryId和keyword都是可空字段,在 Service 拼条件时需要用if判断是否为 null,不能直接把空值拼进 SQL。Result<T>是统一返回体,包含code、message、data三个字段,前端 Axios 拦截器只需要判断code === 200就能确定业务是否成功。
这个接口参数对应的具体含义见下表。
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| page | long | 否 | 页码,默认 1 |
| size | long | 否 | 每页条数,默认 10,最大建议不超过 50 |
| categoryId | Long | 否 | 类目 ID,为空时查询全部分类 |
| keyword | String | 否 | 标题模糊搜索关键词 |
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。路由参数最常见的问题是混淆query和params:query是 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/page | page=1&size=10 |
| 按分类筛选 | /article/page | categoryId=java |
| 标题搜索 | /article/page | keyword=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=1和user-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 + "×tamp=" + timestamp + "&url=" + url); WxJsConfig config = new WxJsConfig(); config.setAppId(appId); config.setNonceStr(nonceStr); config.setTimestamp(timestamp); config.setSignature(sign); return config; }signature的生成算法固定,参与签名的四个参数jsapi_ticket、noncestr、timestamp、url顺序不能错,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.js配hls.js插件,先安装依赖:
npm install video.js hls.jsVue 组件里需要手动初始化播放器,代码片段如下:
<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-view | URL/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 内存使用量。
本文还有配套的精品资源,点击获取