news 2026/10/20 7:27:11

百科系统开发实战:Spring Boot+MySQL构建仿百度百科全栈项目

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
百科系统开发实战:Spring Boot+MySQL构建仿百度百科全栈项目

简介:这套高度模仿百度百科的百科建站系统,面向站长、开发者和知识管理爱好者,用于快速搭建在线词库平台,复刻百度百科的浏览、搜索、词条编辑与审核体验,解决自主建站时内容管理不规范、审核机制缺失等问题。资源以开源的HDWiki系统为核心,将类百科的界面布局、多用户贡献机制、权威性审核流程以及分类检索模块整合在一个约1.95MB的压缩包内。包内主要包含安装说明类文本、程序源码文件以及使用与二次开发文档,可指导用户完成环境部署、数据库配置、站点初始化等操作,并帮助排查常见安装问题。已有384人学习下载,非常适合初学者按照说明从零构建百科网站,也适合中级开发者研究其目录结构、模板标签和扩展机制,进一步定制词条分类、编辑权限和搜索功能,搭建面向个人、团队或学校的知识共享平台。

1. 照抄页面只要三天,真正被卡住的是数据模型和搜索排序

把百度百科的首页、词条页、搜索页截图摆在一起,第一反应通常是:这不就是个带搜索框的内容管理系统吗?实际动手做这个"超级模仿"项目时才发现,UI 模仿是整条链路里最不值钱的部分。真正卡住人的是词条数据怎么建模、正文大字段放哪、搜索怎么模拟原版"名称精确命中排最前"的排序,以及多人编辑时怎么防止词条互相覆盖。这类"百科系统"前台仿搜索、详情、分类导航,后台管发布、编辑与版本回滚,适合想完整走一遍全栈的开发者拿去当课程设计或简历项目。下面按我自己的落地路径讲:先定架构和库表,再写搜索与编辑链路,最后是五个绕不开的坑。

2. 先定技术栈和模块边界:百科系统不是一堆页面堆出来的

好多人一上来就照着百度百科写页面,结果写了两周发现搜索、分类、编辑历史全都接不上,只能推倒重来。我习惯先用一小时把技术栈和模块边界画清楚。百科系统的本质是"多对多"关系很重的内容平台——分类挂词条、标签挂词条、词条挂多版本,选型时最怕的就是把骨架搭窄,后面每加一个功能都要动表结构。

2.1 技术选型:为什么我选 Spring Boot + MySQL 而不是纯前端方案

纯静态页面方案做仿站能看,浏览器里点着挺像那么回事,但撑不住"编辑留痕、搜索排序、分类聚合"这三件事。它们都需要服务端参与,前端静态页只能做到"看起来像",做不到"用起来像"。我用的组合是 Spring Boot 做后端、Thymeleaf 做服务端渲染、MyBatis 操作 MySQL,缓存按需加 Redis。选这个组合有三个理由:第一,百科页面本身是服务端渲染为主,搜索结果和词条详情都是整页刷新,用 Thymeleaf 模仿起来天然贴近;第二,Java 技术栈的课程设计和简历项目认可度最高,遇到问题能搜到的资料也最多;第三,MyBatis 写复杂排序 SQL 很直接,搜索排序这种逻辑放在 XML 里比写在 ORM 注解里好维护。

方案适合场景短板
Spring Boot + Thymeleaf课程设计、简历项目页面交互不如 SPA 顺滑
Vue/React + 后端 API想顺便练前后端分离两套工程,周期长一倍
Flask/Django 快速版单独练 Python 技术栈和主流 Java 岗位要求错位

如果你想同时练 Vue,把 Thymeleaf 换成 RESTful API + Vue 脚手架完全可行,本文后半部分的搜索接口和编辑接口设计照用就行,只是多一步 JSON 序列化。我的建议是:别在选型上花超过半天,百科系统的难点从来不在框架,而在数据模型和事务边界。

2.2 模块划分:前台、管理端与编辑流三条线

百科系统的页面不算多:首页、搜索页、分类导航、词条详情、编辑页,再加一个后台管理页。但页面少不代表结构简单。我习惯把整个工程拆成三条线:前台展示线负责能搜、能看;管理线负责词条上下架和分类维护;编辑流单独拎出来,因为它涉及版本表和内容表两次写入,必须在 service 层用事务包住,放 controller 里会导致事务失效。目录结构按这个思路组织:

baike-system/ ├── pom.xml └── src/main/ ├── java/com/example/baike/ │ ├── controller/ # 页面入口和 API │ │ ├── PortalController.java # 首页、搜索、分类 │ │ └── EntryController.java # 词条详情、编辑、回滚 │ ├── service/ # 业务逻辑(编辑事务、版本管理) │ ├── mapper/ # MyBatis 数据访问接口 │ ├── entity/ # 表和实体映射 │ └── config/ # 静态资源映射、编码拦截器 └── resources/ ├── mapper/ # MyBatis XML 查询(搜索排序在这里) ├── templates/ # Thymeleaf 页面 │ ├── index.html # 首页 │ ├── search.html # 搜索结果页 │ └── entry/ │ ├── detail.html │ └── edit.html └── application.yml

这个划分遵循一个原则:页面入口只管参数和视图,业务全在 service。你后面做版本回滚、做缓存清理,都在 service 层收口,controller 永远瘦瘦的。别小看这条线,我见过不少项目把业务逻辑写进 controller,最后加一个回滚功能要改五个页面入口。

2.3 最小骨架:application.yml 里三个必调的配置

初始化项目后,先不要急着写页面,把配置调对能省掉后面一整天的排查时间。我每次建这类项目,application.yml 里必调三个地方:

server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/baike_db?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai username: root password: 改成你自己的密码 driver-class-name: com.mysql.cj.jdbc.Driver thymeleaf: cache: false redis: host: localhost port: 6379 mybatis: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: true

第一个必调是数据源 URL 里的characterEncoding=utf8mb4,数据库侧字符集和 JDBC 侧字符集不一致,是最常见的"中文查不出来"来源,这个坑后面会专门展开。第二个是thymeleaf.cache: false,开发期改模板不用重启,改完刷新浏览器就能看到效果;上线前记得改回 true,不然流量一大模板编译会拖垮性能。第三个是 MyBatis 的map-underscore-to-camel-case,打开后库表里的update_time自动映射成实体里的updateTime,省掉一堆手写 ResultMap 的琐碎工作。

启动类也别忘加扫描注解:

@SpringBootApplication @MapperScan("com.example.baike.mapper") public class BaikeApplication { public static void main(String[] args) { SpringApplication.run(BaikeApplication.class, args); } }

@MapperScan的作用是让 MyBatis 自动注册所有 Mapper 接口,少了它每个 Mapper 都得手动加@Mapper注解,漏一个启动就报错。到这里骨架就立住了,下一步是设计数据库,这是整个项目的地基。

3. 数据库设计:词条、内容、版本三张表撑起整个百科

百科系统的库表设计是所有后续功能的底板。我见过很多翻车现场,都是因为把所有东西塞进一张大表:正文、标签、分类、阅读量全堆在 entry 表里,改着改着连索引都加不进去。我的做法是拆成三张核心表和两张关联表:entry 主表、entry_content 内容表、entry_revision 版本表,外挂 category 分类树和 tag 标签表。下面逐个说清楚为什么这么拆。

3.1 词条主表和内容表拆开:正文大字段为什么单独放

词条列表页只需要词条名、摘要、分类、浏览量和封面图,而正文是 LONGTEXT 级别的富文本 HTML,一条词条正文轻松几十 KB。如果正文和主表放同一张表,列表查询也得把这些大字段捞出来,虽然 InnoDB 按行读取,但行太大直接拖累缓冲池命中率,行数一多性能就崩。拆开后主表行短小,普通查询快得很,正文只有进详情页才查,这是内容型系统的通用分法。建表语句如下:

-- 词条主表:只放稳定、短小的信息 CREATE TABLE entry ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL COMMENT '词条名,唯一', summary TEXT COMMENT '词条摘要,列表页和搜索页展示', category_id BIGINT NOT NULL COMMENT '所属分类', view_count INT DEFAULT 0 COMMENT '浏览量', status TINYINT DEFAULT 1 COMMENT '1-已发布 0-草稿', cover_image VARCHAR(255) COMMENT '封面图地址', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_entry_name (name) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='词条主表'; -- 词条内容表:正文大字段单独放 CREATE TABLE entry_content ( id BIGINT PRIMARY KEY AUTO_INCREMENT, entry_id BIGINT NOT NULL COMMENT '关联词条主表', content LONGTEXT COMMENT '词条正文 HTML', source VARCHAR(255) COMMENT '参考资料来源', update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_entry_id (entry_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='词条内容表';

两个细节值得注意。第一,name加了唯一索引uk_entry_name,这是防止同名词条重复插入的第一道防线,后面并发章节还会用到它。第二,summary字段不能省,它承担列表页和搜索页的摘要展示,如果没有这个字段,每次列表页都要去 content 表里截取正文,性能差一大截。

3.2 分类和标签:两套挂载方式不能混成一锅

分类是树结构,比如"自然科学 → 物理 → 力学",一个词条只能挂一个主分类;标签是平铺的,一个词条可以挂多个,比如"相对论"可以同时挂"物理""爱因斯坦""时间"三个标签。把这两者混在一张表里,分类页 URL 做不出来,树遍历也做不了。分开设计:

-- 分类表:parent_id 实现树形结构 CREATE TABLE category ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL COMMENT '分类名', parent_id BIGINT DEFAULT 0 COMMENT '父分类 ID,0 表示顶级', sort_order INT DEFAULT 0 COMMENT '同层排序,越小越靠前', KEY idx_parent (parent_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='分类表'; -- 标签表:扁平结构 CREATE TABLE tag ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL UNIQUE COMMENT '标签名' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='标签表'; -- 词条与标签关联表:联合主键防重复挂载 CREATE TABLE entry_tag ( entry_id BIGINT NOT NULL, tag_id BIGINT NOT NULL, PRIMARY KEY (entry_id, tag_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='词条标签关联表';

分类树的便利在于:前台分类导航页只需要parent_id = 0查顶级分类,再按父 ID 逐层展开,天然支持无限层级。关联表用联合主键(entry_id, tag_id),同一个标签重复挂到同一个词条会直接报主键冲突,从数据库层面堵住脏数据。后面做"相关词条推荐"也是靠这张关联表做标签重叠度计算,它是省不掉的。

3.3 版本表:编辑留痕是百科类系统的底线

百科类产品最重要的信任点是"这个词条改没改过、改了什么地方、能不能回滚"。没有版本表,一次手滑的编辑就可能把整条词条内容毁掉且无法恢复。entry_revision 表的核心设计是:每次编辑都生成一份正文快照,revision 从 1 开始自增,回滚就是取出指定版本的 content 覆盖回当前内容。

CREATE TABLE entry_revision ( id BIGINT PRIMARY KEY AUTO_INCREMENT, entry_id BIGINT NOT NULL COMMENT '归属词条', revision INT NOT NULL COMMENT '版本号,从 1 开始递增', content LONGTEXT COMMENT '该版本的正文快照', editor VARCHAR(50) COMMENT '编辑者账号', edit_reason VARCHAR(200) COMMENT '编辑摘要,如"修正错别字"', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_entry_revision (entry_id, revision) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='词条版本表';

(entry_id, revision)联合唯一索引保证同一个词条下版本号不会重复。查询某词条所有历史版本就是WHERE entry_id = ? ORDER BY revision DESC。这里有个设计习惯:版本表只存正文快照,不存词条名和摘要,因为名称和摘要的变更频率远低于正文,而且主表里有 update_time 可以追溯;真要完整审计,可以再扩展;但作为课程设计和简历项目,快照到正文这一层足够撑起"回滚"功能。

4. 复刻百科核心体验:搜索命中、词条渲染与编辑发布

库表稳定之后,剩下的就是三个核心链路:搜索怎么命中、详情页怎么渲染、编辑怎么落库。这三个链路单独看不难,连起来跑通才体会到"模仿"的味道——搜索排序要像原版那样把精确命中放最前,详情页 URL 要像原版那样用词条名而不是数字 ID,编辑要能留痕能回滚。

4.1 搜索接口:名称优先加摘要兜底的排序 SQL

打开百度百科搜索框输入"力学",排在前面的一定是词条名精确叫"力学"的结果,然后是"力学XXX"这种前缀命中的,再往后才是正文或摘要里提到"力学"的。模拟这套排序,核心是 ORDER BY 里的 CASE WHEN 打分:

<select id="searchByKeyword" resultType="com.example.baike.vo.EntryVO"> SELECT e.id, e.name, e.summary, e.view_count, c.name AS categoryName FROM entry e LEFT JOIN category c ON e.category_id = c.id WHERE e.status = 1 AND ( e.name LIKE CONCAT('%', #{keyword}, '%') OR e.summary LIKE CONCAT('%', #{keyword}, '%')) ORDER BY CASE WHEN e.name = #{keyword} THEN 0 WHEN e.name LIKE CONCAT(#{keyword}, '%') THEN 1 ELSE 2 END, e.view_count DESC LIMIT #{offset}, #{pageSize} </select>

同样处理前缀匹配时,e.name LIKE CONCAT(#{keyword}, '%')不会命中词条名带空格或标点的场景,但作为第一版已经够用。三个关键点:第一,模糊匹配用CONCAT('%', #{keyword}, '%'),不要直接写'%${keyword}%',后者存在 SQL 注入风险;第二,CASE 打分把精确匹配定为 0、前缀匹配定为 1、其余定为 2,这就是"像不像原版"的核心;第三,同层级内用view_count DESC做热度排序,浏览量高的词条排前面,这和原版"热门优先"的体验一致。对应的 Mapper 接口:

@Mapper public interface EntryMapper { List<EntryVO> searchByKeyword(@Param("keyword") String keyword, @Param("offset") int offset, @Param("pageSize") int pageSize); }

分页参数直接用LIMIT #{offset}, #{pageSize},前端搜索页每页 20 条,第 N 页的偏移量就是(N - 1) * 20。这套 SQL 在数据量几万条时性能没有问题,但深翻页的坑会在避坑章展开。

4.2 词条详情页:目录式 URL 与模板渲染

百度百科的词条地址是/item/词条名这种目录式风格,而不是/detail?id=123。模仿做法是用@GetMapping("/entry/{name}")直接把词条名放进路径:

@GetMapping("/entry/{name}") public String detail(@PathVariable String name, Model model) { EntryVO entry = entryService.getByName(name); if (entry == null) { return "error/404"; } // 浏览量 +1 放异步或队列,别放在主链路里同步执行 entryService.incrementViewAsync(entry.getId()); model.addAttribute("entry", entry); return "entry/detail"; }

URL 上的中文词条名,浏览器请求时自动做百分号编码,Controller 拿到的name已经是解码后的中文,不需要额外处理。真正要注意的是词条名里如果含/字符,路径解析会出问题,我一般在上传词条时就把/替换成全角或过滤掉。详情页模板里最关键的渲染点是正文:

<!-- 正文是编辑器生成的富文本 HTML,必须用 utext 输出 --> <div class="entry-content" th:utext="${entry.content}"></div>

Thymeleaf 里th:text会转义 HTML 标签,th:utext才原样输出。正文必须用 utext,否则<p>、<img>全变成纯文本显示在页面上;但用了 utext 就意味着服务端必须做好 XSS 过滤,否则就是给攻击者留后门,这个坑在避坑章详细说。词条名、摘要这些普通文本字段一律用th:text,让模板自动转义。

4.3 编辑发布链路:事务里先写版本再更新正文

百科系统的编辑不是改一行 UPDATE 那么简单,它牵扯版本表、内容表、主表三处变更。我用的核心链路是"先插版本快照,再更新正文",整个操作包在一个事务里:

@Transactional(rollbackFor = Exception.class) public void publishEdit(EntryEditForm form) { // 1. 按词条名拿主记录,不存在直接抛业务异常 Entry entry = entryMapper.selectByName(form.getName()); if (entry == null) { throw new BizException("词条不存在,请先创建"); } // 2. 先插入一条新的版本快照,revision 在旧版本号基础上 +1 EntryRevision revision = new EntryRevision(); revision.setEntryId(entry.getId()); revision.setRevision(entry.getRevision() + 1); revision.setContent(form.getContent()); revision.setEditor(form.getEditor()); revision.setEditReason(form.getEditReason()); revisionMapper.insert(revision); // 3. 再更新内容表和主表的版本号 entryContentMapper.updateContent(entry.getId(), form.getContent()); entryMapper.updateRevision(entry.getId(), entry.getRevision() + 1); }

注意@Transactional必须写rollbackFor = Exception.class,因为 Spring 默认只对 RuntimeException 回滚,业务异常类 Exception 默认不回滚,忘了这个参数等于事务白开。顺序上为什么要先插版本再更新正文?如果先更新正文再写版本快照,第二步失败时事务虽然整体回滚,但日志里排查时会看到版本号和内容对不上;先插版本,正文更新失败时版本快照也一起回滚,两边始终一致。这个链路保证了每个已发布版本都有对应的正文快照,回滚功能就是从这里来的。

5. 百科系统避坑清单:五个绕不开的坑

下面这五条是我和周围同事做同类项目时真实踩过的,每一条都按"现象、原因、解决"写,你排查时可以直接照着定位。

5.1 中文搜索返回空:字符集没对齐

现象:词条表里用客户端工具看中文一切正常,但在搜索框输入中文关键词,查出来的结果永远为空,或者干脆报Illegal mix of collations异常。

原因:这是百科系统最典型的字符集问题,一般出在三个地方不一致——建库时用的默认字符集不是 utf8mb4、JDBC 连接 URL 没指定字符集、前端页面请求编码不对。查一下定位:

SHOW CREATE TABLE entry; SHOW VARIABLES LIKE 'character_set_server';

如果character_set_server显示 latin1 或 utf8(非 utf8mb4),那建表时继承的字符集就有问题。utf8 在 MySQL 里最多存 3 字节,生僻字和 emoji 会直接写入失败或变成问号。

解决:建库时强制指定DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci,JDBC URL 加上characterEncoding=utf8mb4,同时给项目加一个编码过滤器强制请求和响应都是 UTF-8。三个位置对齐后,中文搜索基本不再翻车。血泪经验是:客户端工具看着对不代表连接层对,一定要在应用日志里打印实际执行的 SQL 去核对。

5.2 词条被静默覆盖:并发编辑没有防线

现象:两个人同时编辑同一个词条,后提交的人把前一个人的改动整个覆盖掉了,且没有任何提示,等发现时数据已经丢了。

原因:编辑接口的 SQL 是无条件 UPDATE,不校验版本号;同时词条表如果没有唯一索引,还可能插入两条同名词条,搜索时出现诡异重复。这是我初版代码最典型的翻车点——功能能跑,但经不起两个人同时操作。

解决:第一道防线是entry表的uk_entry_name唯一索引,从数据库层堵住同名词条插入;第二道防线是乐观锁,编辑提交时带上页面里隐藏的当前版本号:

UPDATE entry SET summary = #{summary}, revision = revision + 1 WHERE id = #{id} AND revision = #{expectRevision};

执行后看返回的影响行数,如果是 0,说明这期间词条被别人改过,直接提示"词条已被其他人修改,请刷新后重试"。这就是后端处理并发最经典的乐观锁写法,实现成本低,效果立竿见影。

5.3 正文被脚本注入:百科类系统的 XSS 重灾区

现象:后台编辑器里粘贴了一段带<script>的内容,保存后在词条详情页弹出广告窗,或者页面被完整跳转到别的地址。

原因:富文本编辑器提交的是 HTML,服务端没做任何过滤直接入库;详情页为了显示富文本用了th:utext,脚本就原样执行了。XSS 在百科类系统里尤其危险,因为词条是公开内容,任何人编辑都可能带脚本。

解决:前端过滤不可靠,攻击者直接 POST 就能绕过,必须在服务端用白名单机制过滤。我用的方案是 Jsoup 的 Safelist:

public static String cleanContent(String html) { Safelist safelist = Safelist.relaxed() .addTags("img", "h1", "h2", "h3") .addAttributes("img", "src", "alt", "width", "height"); return Jsoup.clean(html, safelist); }
`relaxed()` 保留 p、br、strong、em 等常用标签,我再补上图片和标题标签;script、iframe、style 全被剔除。调用时机在保存编辑内容之前,存进库里的已经是过滤后的安全 HTML。

5.4 深翻页性能崩塌:LIMIT 偏移的代价

现象:搜索结果翻到 100 页之后明显变慢,接口耗时从 50 毫秒涨到 2 秒,越往后越慢。

原因:LIMIT 200000, 20看着只取 20 条,但 MySQL 要先扫描并丢弃前面 20 万行,这部分全都在空转。数据量到十万级以后,深翻页的消耗是线性的,翻到底直接把数据库拖垮。

解决:搜索场景限制最大翻页深度,比如 offset 超过某个阈值就不允许继续翻,提示用户换更精确的关键词,原版搜索引擎也是这么做的;列表类场景改用游标分页,记住上一页最后一条的 id:

SELECT ... WHERE id > #{lastId} ORDER BY id ASC LIMIT #{pageSize}

游标分页无论翻到多深,扫描行数都恒定等于页大小,这是深翻页的根治方案。但要注意它只能用于按 id 或时间排序的场景,搜索场景的排序带 CASE 打分,所以搜索页用"限深度"策略,列表页用"游标"策略。

5.5 上传图片 404:本地路径没有映射到 URL

现象:后台编辑器上传图片成功,返回的路径类似/upload/2025/xxx.jpg,但在详情页打开图片链接是 404。

原因:图片文件确实写到了服务器磁盘,但应用没有把磁盘路径映射成可访问的 URL。浏览器请求/upload/xxx.jpg时去 classpath 里找,自然找不到。

解决:加一个静态资源映射配置,把磁盘上传目录映射到 URL 路径:

@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { // Windows 用 file:/D:/data/baike/upload/,Linux 用 file:/data/baike/upload/ registry.addResourceHandler("/upload/**") .addResourceLocations("file:/data/baike/upload/"); } }

注意 Windows 和 Linux 的路径前缀写法不同,Windows 要写file:/盘符:/路径/,Linux 写file:/绝对路径/。这个本地映射只适合开发环境,部署上线后图片应当走对象存储,但作为百科系统的课程设计,本地映射足够,只要记住路径别写死在业务代码里,统一收口在配置类。

6. 把百科做"活":热榜聚合、相关推荐与缓存抖动

6.1 首页热榜与相关词条:两条能直接复用的查询

首页要像原版那样有"热门词条"板块,最简单可靠的做法不是每次实时查全表排序,而是用一个定时任务每小时把浏览量前 20 的词条写入热榜表,页面只查热榜表。相关词条推荐用标签重叠度算,关联表在这里派上用场:

SELECT t2.entry_id, COUNT(*) AS overlap FROM entry_tag t1 JOIN entry_tag t2 ON t1.tag_id = t2.tag_id WHERE t1.entry_id = #{entryId} AND t2.entry_id != #{entryId} GROUP BY t2.entry_id ORDER BY overlap DESC LIMIT 8;

这条 SQL 的逻辑是:找出和我共享标签最多的其他词条,共享的标签越多相关性越高。复杂度不高,但比随机的"猜你喜欢"有意义得多,是相关推荐里性价比最高的入门写法。

6.2 热词条缓存:过期时间别设死值

详情页是百科系统访问最集中的接口,一定要加缓存。读多写少、数据一致性要求不高的词条正文非常适合 Redis 缓存。但缓存过期时间设成一个固定值,比如统一的 30 分钟,流量大时所有 key 同时过期,数据库会被瞬间打穿,这就是缓存雪崩。我的习惯是过期时间加随机抖动:

public EntryVO getByName(String name) { String key = "entry:" + name; String json = redisTemplate.opsForValue().get(key); if (json != null) { return JSON.parseObject(json, EntryVO.class); } EntryVO entry = entryMapper.selectByName(name); if (entry != null) { // 基础 30 分钟,再加 1-10 分钟随机值,避免同时失效 int expire = 1800 + ThreadLocalRandom.current().nextInt(600); redisTemplate.opsForValue().set(key, JSON.toJSONString(entry), expire, TimeUnit.SECONDS); } return entry; }

编辑发布时记得删掉对应 key,下次访问重新回源数据库即可,代价几乎为零。验证这套做法很简单:压测详情页接口,对比开缓存前后的 QPS 和平均耗时,能看到数量级的差距。

做这类系统我的习惯是:每加一个查询先看执行计划,每上线一个页面先想"这页被刷爆怎么办"。百科这类系统最怕的不是功能少,而是功能堆了很多却扛不住一轮访问。这套流程做完之后,至少你能跟人说清楚每个表为什么这么拆、每次搜索为什么这么快、并发编辑为什么不会打架。希望帮到你。

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

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

人形机器人盲踩障碍物:足底触觉感知与柔顺步态控制解析

1. 案例定位&#xff1a;一句话说清楚“盲踩障碍物”到底做了什么先把这个项目讲明白&#xff1a;这是一台完整的人形机器人&#xff0c;在视觉传感器不参与决策的情况下&#xff0c;靠腿部关节的力觉、足底的压力触觉和躯干的惯性感知&#xff0c;主动走到障碍物前面&#xff…

作者头像 李华
网站建设 2026/10/20 7:26:10

MyBatis拦截器实现数据加解密:原理与实战

MyBatis 拦截器做数据加解密这个方向&#xff0c;我身边做后端的朋友问过很多次。一开始听到这个需求&#xff0c;很多人第一反应是“直接在 Service 层加密不就完了”&#xff0c;但实际上数据落库前后还有一堆隐性问题&#xff1a;分页查询、批量插入、模糊搜索、日志脱敏&am…

作者头像 李华
网站建设 2026/10/20 7:25:38

Photoshop影调调色全攻略:色阶、曲线与色彩工具实战

Photoshop教程三&#xff1a;影调&调色工具每次看到有人对着 Photoshop 界面问“曲线到底是什么、拉完色阶图怎么还是灰的、为什么照着网上的参数调色结果肤色像中毒”&#xff0c;我就知道这门《Photoshop教程三&#xff1a;影调&调色工具》是大多数人的刚需。调色不是…

作者头像 李华
网站建设 2026/10/20 7:25:27

FPGA实现HDMI收发:从TMDS解码到发送编码全流程解析

这块板子在我工作台上躺了快两周&#xff0c;终于把计划里的第二阶段调通了。上一篇文章写完了基础环境的搭建和方案选型&#xff0c;当时给自己留了两个目标&#xff1a;一是把外部输入的HDMI信号完整接收并解码成并行RGB数据&#xff0c;二是自己生成视频时序从FPGA侧发出去。…

作者头像 李华
网站建设 2026/10/20 7:24:47

H5开红包特效全解析:状态机、CSS动画与事件回调实践

简介&#xff1a;一份可直接运行的 H5 开红包特效完整代码包&#xff0c;面向需要为社交电商、营销活动增加互动趣味的前端开发者和初学者&#xff0c;帮助快速实现点击红包开启动画、随机金额展示等常见玩法。压缩包共 9 个文件&#xff0c;体积仅 130KB&#xff0c;包含 HTML…

作者头像 李华
网站建设 2026/10/20 7:23:51

AI芯片软硬件协同设计:从算力焦虑到性能落地的关键技术与实践

1. 从"算力焦虑"说起&#xff1a;为什么AI芯片的软硬件协同设计成了必答题这两年跟做芯片和做算法的朋友聊天&#xff0c;绕不开的一个词就是"算力焦虑"。模型参数从亿级飙到万亿级&#xff0c;训练集群的规模从几百张卡扩到几万张卡&#xff0c;可大家发现…

作者头像 李华