Skip to content

文章模块面经文档

项目:黑马点评(类 Yelp 本地生活服务平台) 技术栈:Spring Boot · MySQL · Redis · MyBatis-Plus


1. 模块概览

文章模块的核心设计亮点:将文章表拆分为信息表和内容表,通过解耦两类访问特性完全不同的字段,加速多种业务场景下的查询,同时降低单表的并发压力。

这是一个典型的数据库垂直分表设计,面试时常被追问「为什么要这么设计」「如果不拆表会怎样」「MySQL 里大字段对性能有什么影响」。


2. 表结构设计

2.1 原始单表设计(问题版)

sql
CREATE TABLE article (
    article_id   BIGINT PRIMARY KEY,  -- 雪花算法
    user_id      BIGINT,              -- 作者ID
    title        VARCHAR(128),        -- 文章标题
    cover_image  VARCHAR(256),        -- 封面图地址
    status       TINYINT,             -- 状态(0草稿/1发布/2下架)
    create_time  DATETIME,            -- 发布时间
    update_time  DATETIME,            -- 更新时间
    -- ⚠️ 问题字段
    content      MEDIUMTEXT,          -- 文章正文(可达几MB)
    image_count  INT,                 -- 图片数量
    image_paths  TEXT                 -- 所有图片路径(JSON)
);

单表的问题:文章正文(content)、图片路径(image_paths)属于大字段(可达几百KB乃至数MB),而标题、封面、状态等元信息字段只有几十字节。绝大多数业务场景只需要元信息,却不得不把大字段一起加载进内存,造成大量 IO 浪费。

2.2 垂直分表后的设计(本项目)

sql
-- 文章信息表:轻量,适合列表查询、状态查询
CREATE TABLE article (
    article_id   BIGINT PRIMARY KEY,
    user_id      BIGINT,
    title        VARCHAR(128),
    cover_image  VARCHAR(256),
    status       TINYINT,
    create_time  DATETIME,
    update_time  DATETIME
);

-- 文章内容表:大字段,只在需要时按需加载
CREATE TABLE article_content (
    article_id   BIGINT PRIMARY KEY,  -- 与 article 表 1:1 关联
    image_count  INT,
    content      MEDIUMTEXT,          -- 正文内容
    image_paths  TEXT                 -- 图片路径(JSON)
);

3. 架构图:不同场景的查询路径

                    用户请求

        ┌──────────────┼──────────────┐
        │              │              │
   首页/推荐列表     用户主页列表    文章详情页
        │              │              │
   只查 article     只查 article    先查 article
   表(轻量)        表(轻量)     再查 article_content
        │              │              │
   返回标题/封面    返回标题/封面   拼装完整 ArticleVO
   /发布时间         /发布时间      返回给客户端

核心收益:列表类查询(并发量最大)只扫描 article 小表,完全不碰大字段;只有用户点进详情页时,才额外查一次 article_content,按需加载。


4. 技术原理深挖

4.1 MySQL 中大字段为什么影响性能?

InnoDB 的行存储格式:InnoDB 默认使用 COMPACTDYNAMIC 行格式。对于 TEXTMEDIUMTEXT 等大字段:

  • 若字段长度小于等于 768 字节,数据存储在行内(In-Page)
  • 若超过 768 字节,溢出部分存储在独立的「溢出页」(Off-Page),行内只保留 20 字节的指针

性能影响链路

查询包含大字段的行


InnoDB 读取数据页(16KB 一个页)


大字段触发额外的「溢出页」随机 IO


Buffer Pool 被大量大字段占满
    ├── 挤占其他热点数据页的缓存空间
    └── 缓存命中率下降 → 更多磁盘 IO

垂直分表后,article 表的每行只有约 100 字节,一个 16KB 的数据页可以存放约 160 行,Buffer Pool 利用率大幅提升,列表查询的缓存命中率显著提高。

4.2 索引效率的提升

MySQL 的二级索引叶子节点存储的是「索引值 + 主键」,回表时需要根据主键到聚簇索引读取完整行数据。

-- 场景:按用户ID查询其发布的所有文章列表
SELECT article_id, title, cover_image, create_time
FROM article
WHERE user_id = ? AND status = 1
ORDER BY create_time DESC;

若存在 (user_id, status, create_time) 联合索引,且表中不含大字段:

  • 每个索引页能存储更多索引记录(行短则数据页内可放更多行)
  • 回表读取主表行时,每行小,IO 代价低
  • 整个查询可能只需几次 IO

若大字段和元信息在同一表中,回表代价翻倍甚至更高。

4.3 为什么用 1:1 关联而不是两个独立表?

article_content 的主键与 article 主键相同(都是 article_id),这是严格的 1:1 关系。设计原则:

  • 生命周期一致:文章删除时内容也应删除
  • 主键相同:JOIN 查询代价最低(等值连接走主键索引,O(log n))
  • 没有外键约束(性能考量):由应用层保证一致性
java
// 查询文章详情:先查信息表,再按需查内容表
Article article = articleService.getById(articleId);
ArticleContent content = articleContentService.getById(articleId); // 同一主键
ArticleVO vo = new ArticleVO();
BeanUtils.copyProperties(article, vo);
BeanUtils.copyProperties(content, vo);
return vo;

4.4 MySQL B+ 树与大字段的关系

B+ 树索引的效率依赖于树的高度,高度取决于每个节点能存放多少个 key,而节点大小固定(InnoDB 默认 16KB)。行越短,每页能存的行越多,树越矮,IO 次数越少:

单表(含大字段):平均行长 5000 字节
    每页约 3 行 → B+ 树较高 → 查询需更多层次的 IO

分表后(article 表):平均行长 100 字节
    每页约 160 行 → B+ 树更矮 → 查询 IO 次数减少

5. 各业务场景的查询分析

5.1 首页推荐 / 列表展示(最高频)

sql
-- 只查轻量信息表,不碰 content
SELECT article_id, title, cover_image, create_time
FROM article
WHERE status = 1
ORDER BY create_time DESC
LIMIT 20;

特点:高并发(每个用户刷新首页都会触发),但数据量小(只需元信息),通过 Redis 缓存热点列表进一步减少 DB 压力。

5.2 用户主页(中频)

sql
-- 查某作者的所有已发布文章
SELECT article_id, title, cover_image, create_time
FROM article
WHERE user_id = ? AND status = 1
ORDER BY create_time DESC;

(user_id, status) 联合索引覆盖此查询,只访问 article 小表。

5.3 文章详情页(低频但数据量大)

java
// 两次查询,按需组装
Article article = articleService.getById(articleId);   // 查 article 表
ArticleContent content = articleContentService.getById(articleId); // 查 content 表

详情页访问频率远低于列表页,且大字段只在用户主动点进时才加载,不影响列表查询性能。


6. 方案对比

6.1 垂直分表 vs 单表

维度单表垂直分表(本项目)
列表查询性能低(扫描含大字段的宽表)高(只扫小表)
Buffer Pool 利用率低(大字段占用缓存)高(小表行短,缓存更多)
详情页查询一次查询两次查询(JOIN 或分两步)
开发复杂度中(需维护两表一致性)
扩展性差(大表难以分库分表)好(大小表可独立扩展)

6.2 垂直分表 vs 水平分表

维度垂直分表水平分表
解决的问题字段过多/大字段性能数据量过大(亿级以上)
表数量固定(按字段类型分)动态增长(按数据量分)
查询变化多表 JOIN路由到指定分片查询
适用场景宽表优化海量数据存储

本项目数据量有限,用垂直分表即可;若文章量达到亿级,还需在此基础上加水平分表(如按 user_id 分片)。


7. 索引设计

sql
-- article 表索引
CREATE INDEX idx_user_status ON article(user_id, status);          -- 用户主页查询
CREATE INDEX idx_status_time  ON article(status, create_time DESC); -- 首页列表查询

-- article_content 表
-- 主键即 article_id,1:1 关联,无需额外索引

联合索引字段顺序原则(最左前缀):

  • (user_id, status):先按 user_id 过滤缩小范围,再按 status 筛选,符合查询条件的顺序
  • (status, create_time):先过滤 status=1 的已发布文章,再按时间排序

8. 极端场景与容灾

场景一:文章表数据量激增,查询变慢

垂直分表后 article 表仍会随文章数量增长:

  • 短期:加索引、优化查询、Redis 缓存热点数据
  • 中期:按 user_id 水平分表,将不同作者的文章分散到不同分片
  • 长期:冷热数据分离(近 6 个月的文章在热库,更早的归档到冷库),配合推荐算法只推新文章,冷数据基本无流量

场景二:article 和 article_content 数据不一致

不使用外键约束(性能原因),由应用层保证:发布文章时,在 @Transactional 注解下同时写两张表,任一失败则全部回滚:

java
@Transactional
public void publishArticle(ArticleVO vo) {
    Article article = new Article();
    BeanUtils.copyProperties(vo, article);
    articleService.save(article);          // 写 article 表

    ArticleContent content = new ArticleContent();
    BeanUtils.copyProperties(vo, content);
    content.setArticleId(article.getArticleId());
    articleContentService.save(content);   // 写 article_content 表
    // 任一失败,事务回滚,两表保持一致
}

场景三:content 字段内容超大(如几十 MB 的长文)

MEDIUMTEXT 最大支持 16MB,足够大多数场景。若确实有极大内容(如视频脚本、长篇小说),可考虑:

  • 将内容存储到对象存储(OSS/S3),数据库只存 URL
  • 对内容进行分段存储(将一篇文章拆为多段落存储)

9. 面试题清单

9.1 基础实现层

题目考察点
你的文章表是怎么设计的?为什么要拆成两张表?垂直分表动机
信息表和内容表是什么关系?怎么关联的?1:1 关联设计
文章详情查询需要查两次 DB,比单表多一次,这不是性能更差吗?权衡思维:低频牺牲换高频收益
发布文章时如何保证两张表的数据一致性?事务

9.2 原理深挖层

题目考察点
MySQL 中大字段(TEXT)对性能有什么影响?InnoDB 行存储,溢出页,Buffer Pool
为什么表中行越短,查询越快?B+树高度,数据页利用率
垂直分表和水平分表有什么区别,分别解决什么问题?分表策略理解
联合索引 (user_id, status)(status, user_id) 有什么区别?最左前缀原则,区分度

9.3 方案对比层

题目考察点
如果不做垂直分表,有没有其他办法优化大字段的影响?覆盖索引、懒加载等
文章数据量很大时,除了垂直分表还需要什么优化?水平分表,冷热分离,归档
为什么不用外键约束来保证两表一致性?外键对性能的影响,应用层事务替代

9.4 极端场景层

题目考察点
article 写入成功但 article_content 写入失败,怎么处理?事务回滚
文章内容特别长(几十 MB),该怎么存储?对象存储,内容分段
文章表有 1 亿条数据了,查询变慢了,怎么优化?水平分表,冷热分离,读写分离

10. 一句话总结(面试开场白模板)

文章表采用垂直分表设计,将文章元信息(标题、封面、状态、发布时间)和文章内容(正文 TEXT、图片路径)拆分成两张表,通过相同的 article_id 做 1:1 关联。这么设计的核心原因是:首页列表、用户主页等高并发场景只需要元信息,不应该因为大字段拖累查询效率。MySQL 中 TEXT 类型大字段会触发溢出页的额外 IO,还会占用大量 Buffer Pool 缓存空间,挤压其他热点数据;垂直分表后 article 信息表行长约 100 字节,单个数据页可存 160 行,缓存命中率显著提升。代价是详情页需要两次查询,但详情页访问频率远低于列表页,这是合理的权衡。