掘金风格后端数据库设计实战:从用户表到分布式架构,一文搞定
本文基于一个真实的博客/社区后端项目,带你从零理解表设计、索引优化、外键约束、分布式部署、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 = ?极快。name加UNIQUE KEY------ 既是唯一约束 (防止重名),又是普通索引 (加速WHERE name = ?查询,比如登录)。- 没有存
email、phone、bio、avatar_url等 ------ 这些放在附属表 ,通过userId关联查询。好处:- 行记录小,单页可存更多行,减少 I/O。
- 缓存命中率高。
- 分库分表时更轻量(后续扩展容易)。
2.2 密码安全
明文存储是大忌,实际项目会用 bcrypt 或 scrypt 加盐哈希,这里只做示范。
三、头像表:静态资源与 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;
解析:
content用longtext,可存最大 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 更新(极少发生),子评论自动更新。
- 索引覆盖:
postId、userId、parentId都加索引,因为查询"某文章的所有评论"、"某用户的所有评论"、"某评论的子评论"都是常见操作。
七、标签与多对多关系
标签表:
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),因为文件可能属于用户头像或其他地方。 - 索引只加在
postId和userId上,满足查询需求。
九、索引的底层原理与选择
9.1 B+树索引
MySQL InnoDB 默认使用 B+树 索引。特点:
- 叶子节点存储数据(聚簇索引)或主键值(二级索引)。
- 范围查询快(如
BETWEEN、>)。 - 高度一般 3
4 层,千万级数据查询只需 23 次磁盘 I/O。
9.2 什么时候加索引?
- 高频查询字段 :如
WHERE、JOIN、ORDER BY中的列。 - 选择性高的列 :如
userId比gender更适合索引。 - 覆盖索引:如果查询的所有字段都在索引中,避免回表。
9.3 什么时候避免索引?
- 表很小(全表扫描更快)。
- 频繁更新的列(维护索引代价高)。
- 选择性很低的列(如布尔值)。
9.4 复合索引与最左前缀
联合索引 (a, b, c) 能支持 a、a,b、a,b,c 的查询,但不能单独支持 b 或 c。所以建索引时列顺序很重要。
十、分布式与部署架构
10.1 从 DNS 到负载均衡
用户访问 juejin.cn 时,背后的流程:
-
DNS 解析 :浏览器发起域名查询,依次查本地缓存 → 路由器缓存 → ISP 缓存 → 根域名服务器 → 顶级域名服务器(.com) → 权威 DNS(掘金的 DNS 服务商)。最终拿到一个 IP 地址。
-
这个 IP 不是应用服务器的 IP ,而是 Nginx 负载均衡器 的 IP。
为什么?
- 掘金后端是一个集群,有多台服务器(比如 10 台 Web 容器)。
- Nginx 作为反向代理,根据负载策略(轮询、最少连接、IP Hash 等)将请求分发给某台健康的后端服务器。
-
就近访问 :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)**等,但作为基础设计,这些表结构已经足够支撑一个中小型社区。