掘金风格后端数据库设计实战:从用户表到分布式架构,一文搞定

掘金风格后端数据库设计实战:从用户表到分布式架构,一文搞定

本文基于一个真实的博客/社区后端项目,带你从零理解表设计、索引优化、外键约束、分布式部署、DNS解析、CDN加速等核心知识。全程结合 SQL 代码与底层原理解析,适合初中级开发者进阶。


一、业务概览:我们需要几张表?

一个典型的 UGC 社区(类似掘金),核心功能包括:用户注册登录、发布文章、点赞、收藏、评论、关注、标签、文件上传

后端业务涉及的表有:

  • user(用户)
  • avatar(头像)
  • post(文章)
  • user_like_post(点赞关系)
  • user_collect_post(收藏关系,本文略,结构与点赞类似)
  • comment(评论,支持嵌套)
  • tag(标签)
  • post_tag(文章-标签多对多)
  • file(文件资源)

那么问题来了:怎么建表?怎么建索引?怎么建约束?

下面我们逐一拆解,并深入背后的性能、扩展性、分布式考量。


二、用户表:小而美的核心

2.1 为什么用户表要"瘦"?

用户表是系统的访问热点 ,登录、鉴权、个人信息查询都离不开它。

为了应对海量用户(千万级) ,我们遵循一个原则:核心表只存最常用字段

sql 复制代码
CREATE TABLE `user`(
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
  `password` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `name` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4_unicode_ci;

解析:

  • id 自增主键 ------ 聚簇索引 ,数据按主键顺序存储,查询 WHERE id = ? 极快。
  • nameUNIQUE KEY ------ 既是唯一约束 (防止重名),又是普通索引 (加速 WHERE name = ? 查询,比如登录)。
  • 没有存 emailphonebioavatar_url 等 ------ 这些放在附属表 ,通过 userId 关联查询。好处:
    • 行记录小,单页可存更多行,减少 I/O。
    • 缓存命中率高
    • 分库分表时更轻量(后续扩展容易)。

2.2 密码安全

明文存储是大忌,实际项目会用 bcryptscrypt 加盐哈希,这里只做示范。


三、头像表:静态资源与 CDN 加速

3.1 表结构

sql 复制代码
CREATE TABLE `avatar` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `mimetype` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
  `filename` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
  `size` int(11) NOT NULL,
  `userId` int(11) NOT NULL,
  PRIMARY KEY (`id`),
  KEY `userId` (`userId`),   -- 普通索引,用于根据用户查头像
  CONSTRAINT `avatar_ibfk_1` FOREIGN KEY (`userId`) REFERENCES `user` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4_unicode_ci;

关键点:

  • userId外键约束 ,保证数据一致性(删除用户时,头像也被级联删除,但这里没写 ON DELETE CASCADE,视业务需求)。
  • KEY userId (userId)普通索引 ,因为业务上经常通过 userId 查询头像,加索引避免全表扫描。

3.2 静态资源为什么不存数据库?

头像文件(图片)实际存储在独立的静态资源服务器上,比如:

  • 云 OSS(阿里云/腾讯云对象存储)
  • 或者自建 CDN

数据库中只存元数据 (文件名、大小、类型、所属用户)。

前端直接访问静态 URL,例如:

bash 复制代码
https://p6-passport.byteacctimg.com/img/user-avatar/11174122cb6102aca29ac7599f4e08b4~40x40.awebp

这样做的好处:

  • 解耦:数据库压力小,文件读写不占用 DB 连接。
  • CDN 加速:用户就近获取资源,降低延迟。

四、文章表:核心内容存储

sql 复制代码
CREATE TABLE `post` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `title` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
  `content` longtext COLLATE utf8mb4_unicode_ci,
  `userId` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `userId` (`userId`),
  CONSTRAINT `post_ibfk_1` FOREIGN KEY (`userId`) REFERENCES `user` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4_unicode_ci;

解析:

  • contentlongtext,可存最大 4GB 的文本(Markdown 或 HTML)。
  • userId 外键关联用户,并加普通索引 ------ 因为"查询某用户的所有文章"是高频操作。
  • 为什么不把 content 单独拎出去? 如果文章列表页只展示标题和摘要,可能考虑分表,但这里为了简单,直接放在一起。实际大数据量下,content 可能用对象存储或专门的文档数据库。

五、点赞表:多对多关系的最佳实践

sql 复制代码
CREATE TABLE `user_like_post`(
    `userId` int(11) NOT NULL,
    `postId` int(11) NOT NULL,
    PRIMARY KEY (`userId`,`postId`),   -- 联合主键,同时保证唯一性
    KEY `postId` (`postId`),           -- 额外索引,用于反向查询
    CONSTRAINT `user_like_post_ibfk_1` FOREIGN KEY (`userId`) REFERENCES `user` (`id`),
    CONSTRAINT `user_like_post_ibfk_2` FOREIGN KEY (`postId`) REFERENCES `post` (`id`)
);

5.1 为什么联合主键是 (userId, postId)

  • 唯一性:同一用户不能重复点赞同一文章。
  • 覆盖索引:查询"用户点赞了哪些文章"时,只用扫描这个索引,无需回表。
  • 查询方向
    • WHERE userId = ? 走联合主键(左前缀),快。
    • WHERE postId = ? 我们单独建了 KEY postId,所以反查"这篇文章被谁点赞"也快。

5.2 为什么不单独加 userId 索引?

联合主键 (userId, postId) 本身已经是 复合索引 ,最左匹配原则下,userId 作为第一列,查询 userId 已经能用到该索引。再加一个单独的 KEY userId冗余,浪费空间。


六、评论表:支持嵌套回复(树形结构)

sql 复制代码
CREATE TABLE `comment` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `content` longtext COLLATE utf8mb4_unicode_ci,
  `postId` int(11) NOT NULL,
  `userId` int(11) NOT NULL,
  `parentId` int(11) DEFAULT NULL,   -- 父评论ID,NULL表示一级评论
  PRIMARY KEY (`id`),
  KEY `postId` (`postId`),
  KEY `userId` (`userId`),
  KEY `parentId` (`parentId`),
  CONSTRAINT `comment_ibfk_1` FOREIGN KEY (`userId`) REFERENCES `user` (`id`),
  CONSTRAINT `comment_ibfk_2` FOREIGN KEY (`parentId`) REFERENCES `comment` (`id`) ON DELETE SET NULL ON UPDATE CASCADE,
  CONSTRAINT `comment_ibfk_3` FOREIGN KEY (`postId`) REFERENCES `post` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
);

重点解析:

  • parentId 指向自身表 comment.id,实现无限级树状评论
  • 外键约束:
    • ON DELETE SET NULL:删除父评论时,子评论的 parentId 置为 NULL(避免孤儿记录),而不是级联删除子评论(保留内容)。
    • ON UPDATE CASCADE:如果父评论 ID 更新(极少发生),子评论自动更新。
  • 索引覆盖:postIduserIdparentId 都加索引,因为查询"某文章的所有评论"、"某用户的所有评论"、"某评论的子评论"都是常见操作。

七、标签与多对多关系

标签表:

sql 复制代码
CREATE TABLE `tag` (
 `id` int(11) NOT NULL AUTO_INCREMENT,
 `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
 PRIMARY KEY (`id`),
 UNIQUE KEY `name` (`name`)   -- 标签名唯一
) ENGINE=InnoDB;

关联表(文章-标签):

sql 复制代码
CREATE TABLE `post_tag`(
    `postId` int(11) NOT NULL,
    `tagId` int(11) NOT NULL,
    PRIMARY KEY (`postId`,`tagId`),
    KEY `tagId` (`tagId`),
    CONSTRAINT `post_tag_ibfk_1` FOREIGN KEY (`postId`) REFERENCES `post` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
    CONSTRAINT `post_tag_ibfk_2` FOREIGN KEY (`tagId`) REFERENCES `tag` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
);

设计原则与点赞表类似:

  • 联合主键保证唯一。
  • 两个外键均设置 ON DELETE CASCADE,删除文章或标签时自动清理关联关系。

八、文件表:存储图片等资源元数据

sql 复制代码
CREATE TABLE `file`(
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `originalname` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
  `mimetype` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
  `filename` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
  `size` int(11) NOT NULL,
  `postId` int(11) DEFAULT NULL,
  `userId` int(11) NOT NULL,
  `width` int(11) DEFAULT NULL,
  `height` int(11) DEFAULT NULL,
  `metadata` json DEFAULT NULL,   -- 存储额外信息,如图片 EXIF
  KEY `postId` (`postId`),
  KEY `userId` (`userId`),
  CONSTRAINT `file_ibfk_1` FOREIGN KEY (`userId`) REFERENCES `user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
);

亮点:

  • metadata 用 JSON 类型(MySQL 5.7+),灵活存储扩展字段。
  • 外键只关联 userId,未强制关联 postId(可为 NULL),因为文件可能属于用户头像或其他地方。
  • 索引只加在 postIduserId 上,满足查询需求。

九、索引的底层原理与选择

9.1 B+树索引

MySQL InnoDB 默认使用 B+树 索引。特点:

  • 叶子节点存储数据(聚簇索引)或主键值(二级索引)。
  • 范围查询快(如 BETWEEN>)。
  • 高度一般 34 层,千万级数据查询只需 23 次磁盘 I/O。

9.2 什么时候加索引?

  • 高频查询字段 :如 WHEREJOINORDER BY 中的列。
  • 选择性高的列 :如 userIdgender 更适合索引。
  • 覆盖索引:如果查询的所有字段都在索引中,避免回表。

9.3 什么时候避免索引?

  • 表很小(全表扫描更快)。
  • 频繁更新的列(维护索引代价高)。
  • 选择性很低的列(如布尔值)。

9.4 复合索引与最左前缀

联合索引 (a, b, c) 能支持 aa,ba,b,c 的查询,但不能单独支持 bc。所以建索引时列顺序很重要。


十、分布式与部署架构

10.1 从 DNS 到负载均衡

用户访问 juejin.cn 时,背后的流程:

  1. DNS 解析 :浏览器发起域名查询,依次查本地缓存 → 路由器缓存 → ISP 缓存 → 根域名服务器 → 顶级域名服务器(.com) → 权威 DNS(掘金的 DNS 服务商)。最终拿到一个 IP 地址

  2. 这个 IP 不是应用服务器的 IP ,而是 Nginx 负载均衡器 的 IP。

    为什么?

    • 掘金后端是一个集群,有多台服务器(比如 10 台 Web 容器)。
    • Nginx 作为反向代理,根据负载策略(轮询、最少连接、IP Hash 等)将请求分发给某台健康的后端服务器。
  3. 就近访问 :DNS 可以根据用户地理位置,返回最近区域 的负载均衡器 IP(如华东、华北),这就是智能 DNS

10.2 静态资源与 CDN

图片、CSS、JS 等静态资源不走应用服务器,而是上传到 OSS ,然后通过 CDN(Content Delivery Network) 加速。

  • CDN 在全国(全球)部署边缘节点。
  • 用户请求资源时,DNS 解析到最近的 CDN 节点。
  • 如果节点有缓存,直接返回;否则回源到 OSS 拉取。

这样大大减轻了源站压力,也提升了用户访问速度。


十一、总结:设计背后的权衡

核心索引 外键 设计要点
user PRIMARY(id), UNIQUE(name) 瘦核心,便于扩展
avatar PRIMARY(id), KEY(userId) userId → user.id 存元数据,文件放 OSS
post PRIMARY(id), KEY(userId) userId → user.id content 用 longtext
user_like_post PRIMARY(userId,postId), KEY(postId) 双外键 联合主键防重,冗余索引反查
comment PRIMARY(id), KEY(postId,userId,parentId) 自引用外键 树形结构,级联策略精细
tag PRIMARY(id), UNIQUE(name) 标签名唯一
post_tag PRIMARY(postId,tagId), KEY(tagId) 双外键 多对多标准设计
file KEY(postId), KEY(userId) userId → user.id JSON 元数据灵活

核心思想

  • 索引是双刃剑:加速查询,但占用空间、拖慢写入。
  • 外键保证一致性,但在高并发下可能影响性能,有时在应用层维护。
  • 分布式设计:DNS 调度、负载均衡、CDN 各司其职,让系统可扩展。

本文所有 SQL 基于 MySQL 5.7+,字符集 utf8mb4_unicode_ci 支持 emoji 和所有 Unicode 字符。

实际生产环境还要考虑**读写分离、分库分表、缓存(Redis)**等,但作为基础设计,这些表结构已经足够支撑一个中小型社区。

如果你觉得有用,欢迎点赞、收藏、评论!

相关推荐
呆呆小孩1 小时前
零基础学 MySQL(上):从建库建表到增删改查,一篇搭好 SQL 基础
android·sql·mysql
拾光师1 小时前
Hadoop 三种运行模式:从单机调试到生产集群,一路搭过来
后端
进阶的小名2 小时前
Spring AI 2.0 探索:多 OpenAI-Compatible 模型接入,以及下一代 Session 记忆管理
java·人工智能·后端·gpt·spring·ai·chatgpt
卷无止境2 小时前
FastAPI 的 Metadata 到底是什么,又牵动了哪些核心概念
后端·python
卷无止境2 小时前
FastAPI 调试实战,从断点到生产环境的排错心法
后端·python
敢敢のwings2 小时前
智元 GO-2 与 AgiBot-World 深度解读
开发语言·后端·golang
考虑考虑3 小时前
Java三元表达式注意
java·后端·java ee
stark张宇3 小时前
Go语言runtime全景图:从编译到GC,带你彻底吃透Go的底层血脉
后端·go
IT_陈寒3 小时前
Redis的DEL命令居然没删干净数据?这个坑我爬了半天
前端·人工智能·后端