数据库方案

最简单的方案

在上一章,我分析了各种技术方案。现在,让我从最简单的开始。

先用数据库实现,看看会发生什么。

数据库设计

表结构

我需要两张表:

数据设计要点

  • 核心是在 articles 里保存业务事实,而不是把规则散落在应用逻辑里。
  • 索引服务于高频查询,重点是缩小扫描范围,而不是堆更多字段。
  • 关键字段包括 idtitlecontentauthor_idlike_countcreated_atupdated_atarticle_id,它们决定后续查询和管理能力。

设计思路:

article_likes 表:
- 记录谁给哪篇文章点过赞
- UNIQUE KEY 保证一个用户对一篇文章只能点赞一次
- 用于查询"用户是否点赞"

articles.like_count 字段:
- 冗余字段,存储点赞总数
- 避免每次都 COUNT 查询
- 用于快速显示点赞数

为什么要有冗余字段?

没有冗余字段的方案:

数据设计要点

  • 查询目标是快速定位状态、任务或资源,避免在关键路径上做大范围扫描。

问题:

  • COUNT 查询慢(需要扫描索引)
  • 列表页显示 20 篇文章,需要 20 次 COUNT
  • 数据库压力大

有冗余字段的方案:

数据设计要点

  • 查询目标是快速定位状态、任务或资源,避免在关键路径上做大范围扫描。

好处:

  • 查询快,直接读取字段
  • 不需要 COUNT
  • 数据库压力小

代价:

  • 需要维护冗余字段的一致性
  • 点赞/取消点赞时需要更新

我选择了空间换时间。

点赞功能实现

点赞接口

取消点赞接口

查询文章详情

事务问题

场景:并发点赞

如果有两个用户同时对一篇文章点赞,会发生什么?

初始状态:article.id = 1, like_count = 100

用户 A 点赞:
1. INSERT INTO article_likes (article_id, user_id) VALUES (1, 1001)
2. UPDATE articles SET like_count = like_count + 1 WHERE id = 1

用户 B 点赞(同时):
1. INSERT INTO article_likes (article_id, user_id) VALUES (1, 1002)
2. UPDATE articles SET like_count = like_count + 1 WHERE id = 1

期望结果:like_count = 102
实际结果:like_count = 102 ✅

看起来没问题?

但如果使用事务呢?

使用事务

事务的好处:

  • 保证两个操作要么都成功,要么都失败
  • 避免数据不一致

事务的代价:

  • 锁定数据库资源
  • 降低并发性能

测试验证

单元测试

性能测试

我用 Apache Benchmark 测试了一下:

验证要点

  • 命令只用于验证系统状态,读者不需要记具体参数。

看起来还不错?

但是,这只是单机测试。如果用户量增长呢?

初步成果

上线一周后,数据如下:

文章数:1,500 篇
用户数:8,000 人
点赞总数:35,000 次
日均点赞:约 5,000 次

系统运行正常,没有出现明显问题。

我看了看数据库监控:

MySQL 状态:
- CPU:15-20%
- 连接数:30/200
- QPS:约 500
- 慢查询:0 条

article_likes 表:
- 记录数:35,000 条
- 索引大小:约 5MB
- 查询时间:< 1ms

一切看起来都很美好。

潜在问题

虽然目前运行正常,但我开始担心一些问题:

问题 1:数据一致性

解决方案:使用事务。

问题 2:并发更新

这是经典的”丢失更新”问题。

问题 3:性能瓶颈

当前:日均 5,000 次点赞,数据库 QPS 500

增长预测:
- 用户增长 10 倍 → 日均 50,000 次点赞
- 用户增长 100 倍 → 日均 500,000 次点赞
- 峰值可能达到 10 倍

数据库能承受吗?

我需要更深入地思考。

课后练习

练习 1

为什么需要在 article_likes 表上创建 (article_id, user_id) 的联合唯一索引?

参考答案(3 个标签)
MySQL索引唯一约束

原因 1:保证数据唯一性

联合唯一索引确保一个用户对一篇文章只能有一条点赞记录:

数据设计要点

  • 关键字段包括 INSERT,它们决定后续查询和管理能力。

原因 2:提升查询性能

查询”用户是否点赞”时,可以利用索引快速定位:

数据设计要点

  • 查询目标是快速定位状态、任务或资源,避免在关键路径上做大范围扫描。

原因 3:避免回表查询

联合索引包含了查询所需的所有字段,不需要回表:

联合索引结构:(article_id, user_id) → 主键 ID

查询:SELECT id FROM article_likes WHERE article_id = 1 AND user_id = 100
执行:直接在索引中查找,不需要回表

练习 2

如何解决并发更新导致的”丢失更新”问题?

参考答案(3 个标签)
并发控制事务MySQL

方案一:使用乐观锁

数据设计要点

  • 这是一次表结构演进:随着业务能力增加,把新状态、新时间点或新归属关系补进数据模型。

方案二:使用数据库原子操作

数据设计要点

  • 这里关注数据模型和约束关系,不需要记住具体语法。

这种方式不需要读取原值,直接在数据库层面完成计算。

方案三:使用 SELECT FOR UPDATE

数据设计要点

  • 查询目标是快速定位状态、任务或资源,避免在关键路径上做大范围扫描。

方案四:使用 Redis 计数器

推荐方案

对于点赞计数,推荐使用 方案二(原子操作)方案四(Redis)

  • 方案二简单高效,利用数据库原子性
  • 方案四性能最好,适合高并发场景

练习 3

设计一个接口,返回文章列表及每篇文章的点赞数和当前用户是否点赞。

要求:优化查询性能,避免 N+1 查询问题。

参考答案(3 个标签)
SQL优化N+1问题性能

问题分析

N+1 查询问题:

优化方案一:批量查询

数据设计要点

  • 查询目标是快速定位状态、任务或资源,避免在关键路径上做大范围扫描。

优化方案二:LEFT JOIN

数据设计要点

  • 查询目标是快速定位状态、任务或资源,避免在关键路径上做大范围扫描。

优化方案三:使用冗余字段 + 批量查询是否点赞

数据设计要点

  • 查询目标是快速定位状态、任务或资源,避免在关键路径上做大范围扫描。

性能对比

方案查询次数复杂度
N+1 查询1 + 2N简单
批量查询3中等
LEFT JOIN1复杂
冗余字段 + 批量2简单

推荐方案:使用 冗余字段 + 批量查询是否点赞,简单高效。

练习 4

如何处理”点赞失败但用户已经点击”的情况?

参考答案(3 个标签)
用户体验错误处理前端交互

问题场景

  • 用户点击点赞按钮
  • 前端立即显示点赞成功(乐观更新)
  • 后端返回失败(已点赞、网络错误等)
  • 前端状态与后端不一致

解决方案

方案一:悲观更新(先请求,后更新 UI)

优点:数据准确 缺点:用户体验稍差(等待响应)

方案二:乐观更新 + 回滚

优点:用户体验好 缺点:实现复杂,需要处理回滚

方案三:乐观更新 + 定期同步

优点:用户体验最好 缺点:可能有短暂的不一致

推荐方案

根据业务场景选择:

  • 严格要求一致:使用方案一(悲观更新)
  • 普通点赞功能:使用方案二(乐观更新 + 回滚)
  • 社交应用:使用方案三(乐观更新 + 定期同步)

练习 5

设计数据库表结构,支持”查看用户点赞过的所有文章”功能。

参考答案(3 个标签)
数据库设计索引查询优化

需求分析

  • 查询用户点赞过的所有文章
  • 支持分页
  • 按点赞时间倒序排列

表结构设计

数据设计要点

  • 核心是在 article_likes 里保存业务事实,而不是把规则散落在应用逻辑里。
  • 索引服务于高频查询,重点是缩小扫描范围,而不是堆更多字段。
  • 关键字段包括 idarticle_iduser_idcreated_at,它们决定后续查询和管理能力。

查询 SQL

数据设计要点

  • 查询目标是快速定位状态、任务或资源,避免在关键路径上做大范围扫描。

索引分析

索引 idx_user_created (user_id, created_at DESC)

查询:
WHERE user_id = ?
ORDER BY created_at DESC

执行计划:
- 使用索引 idx_user_created
- 索引扫描
- 避免文件排序(Using filesort)

优化建议

  1. 覆盖索引优化(如果只需要文章 ID):

数据设计要点

  • 查询目标是快速定位状态、任务或资源,避免在关键路径上做大范围扫描。
  1. 延迟关联(分页优化):

数据设计要点

  • 查询目标是快速定位状态、任务或资源,避免在关键路径上做大范围扫描。
  1. 缓存优化

思考题

  1. 如果用户在一篇文章上反复点赞、取消点赞,会对数据库产生什么影响?如何优化?

  2. 如何实现”点赞动画”效果?前后端如何配合?

  3. 如果需要统计”用户获得的点赞总数”(即用户所有文章的点赞数之和),如何设计?

💡 提示:这些问题没有标准答案,建议结合实际情况深入思考。