核心挑战

增长的烦恼

三个月后,平台开始增长。

用户数:482 → 5,200(增长 10 倍)
文章数:156 → 1,300(增长 8 倍)
日均点赞:150 → 3,000(增长 20 倍)

看起来是好消息,对吧?

但噩梦开始了。

第一次报警

那是一个周三的上午,我正在咖啡店工作。

突然,手机收到一条报警短信:

Inbox
From:监控系统
To:
Time:周三 10:23
Subject: 【监控报警】数据库 CPU 使用率超过 90%
  • 当前值:92%
  • 阈值:80%
  • 持续时间:已持续 5 分钟
  • 影响范围:全部数据库查询

我打开电脑,连接服务器,查看数据库监控:

MySQL 状态:
- CPU: 92%
- 连接数: 180/200
- 慢查询: 45 条/分钟
- QPS: 3,200

数据库快扛不住了!

问题定位

我打开慢查询日志,看到了一堆类似的查询:

数据设计要点

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

原来都是 COUNT 查询!

我分析了一下:

article_likes 表数据量:
- 当前记录数:约 180,000 条
- 每天新增:约 3,000 条

COUNT 查询:
- 列表页:20 篇文章 → 20 次 COUNT 查询
- 用户刷 10 页 → 200 次 COUNT 查询
- 1000 个活跃用户 → 200,000 次 COUNT 查询/天

COUNT 性能:
- 扫描全表索引:180,000 条记录
- 单次查询时间:约 800ms
- 高峰期并发查询:导致数据库 CPU 飙升

问题找到了:COUNT 查询太慢!

为什么 COUNT 慢?

我深入研究了 MySQL 的 COUNT 查询机制:

MyISAM vs InnoDB

MyISAM 存储引擎:
- 表级锁,不适合高并发
- COUNT(*) 很快(存储了总数)

InnoDB 存储引擎:
- 行级锁,适合高并发
- COUNT(*) 较慢(需要扫描索引)

我用的是 InnoDB,所以每次 COUNT 都要扫描索引。

索引结构

article_likes 表索引:
- PRIMARY KEY: id
- UNIQUE KEY: (article_id, user_id)

COUNT 查询:
SELECT COUNT(*) FROM article_likes WHERE article_id = 1234;

执行过程:
1. 使用联合索引 (article_id, user_id)
2. 找到 article_id = 1234 的所有记录
3. 扫描并计数
4. 返回结果

问题:每篇文章的点赞数不同,索引扫描的行数也不同。

热门文章:
- 点赞数:1000+
- COUNT 扫描:1000 行
- 查询时间:约 10ms

超级热门文章:
- 点赞数:10000+
- COUNT 扫描:10000 行
- 查询时间:约 100ms

爆款文章:
- 点赞数:100000+
- COUNT 扫描:100000 行
- 查询时间:约 1s+

尝试优化

方案 1:缓存 COUNT 结果

我想:既然 COUNT 慢,为什么不缓存结果?

效果:

  • 缓存命中时:约 1ms ✅
  • 缓存未命中时:仍然慢 ❌
  • 用户点赞后:缓存不更新,数据不一致 ❌

问题:用户点赞后,点赞数不会立即更新!

方案 2:主动更新缓存

效果:

  • 缓存始终是新的 ✅
  • 不再需要 COUNT 查询 ✅

但是,我忽略了缓存初始化的问题:

如果用户访问了一篇冷门文章?

冷门文章:
- 点赞数:0
- 从未被访问过

突然访问:
1. 缓存不存在
2. 查询数据库(COUNT)
3. 如果有大量冷门文章被同时访问...
4. 数据库压力还是很大!

方案 3:预热缓存

我在想:能不能提前把所有文章的点赞数都缓存起来?

问题:

  • 1300 篇文章 → 1300 次 COUNT 查询
  • 每次新文章发布都要预热
  • 系统启动时间变长

这个方案太重了。

更深层的问题

在思考缓存方案时,我突然意识到一个更严重的问题:

如果用户快速点赞/取消点赞会怎样?

看起来没问题,但如果网络延迟呢?

时间线:
T0: 用户点击点赞 → 发送请求 1
T1: 请求 1 到达服务器
T2: 用户再次点击 → 发送请求 2(取消点赞)
T3: 请求 2 到达服务器
T4: 请求 1 开始处理(网络慢)
T5: 请求 2 开始处理(先到先处理)

结果:
- 请求 2 先处理(取消点赞)→ 数据库删除记录,缓存 -1
- 请求 1 后处理(点赞)→ 数据库插入记录,缓存 +1
- 最终状态:点赞了
- 但用户看到的是:点击后没反应

并发问题出现了!

数据一致性挑战

我画了一张图,分析数据一致性问题:

场景:用户点赞

方案 A:先更新数据库,再更新缓存
┌─────────────┐
│ 更新数据库   │ ← 成功
└──────┬──────┘


┌─────────────┐
│ 更新缓存     │ ← 如果失败?
└─────────────┘

问题:数据库有记录,缓存没更新 → 数据不一致

方案 B:先更新缓存,再更新数据库
┌─────────────┐
│ 更新缓存     │ ← 成功
└──────┬──────┘


┌─────────────┐
│ 更新数据库   │ ← 如果失败?
└─────────────┘

问题:缓存更新了,数据库没记录 → 数据不一致

方案 C:先删缓存,再更新数据库
┌─────────────┐
│ 删除缓存     │ ← 成功
└──────┬──────┘


┌─────────────┐
│ 更新数据库   │ ← 如果失败?
└─────────────┘

问题:缓存删除了,数据库没更新 → 下次查询重新加载旧数据

无论哪种方案,都有问题!

高并发挑战

除了数据一致性,还有并发性能问题:

场景:热门文章被大量点赞

1000 个用户同时点赞同一篇文章:

方案 1:数据库直接插入
→ 1000 个 INSERT 操作
→ 数据库压力巨大

方案 2:Redis 计数
→ 1000 个 INCR 操作
→ Redis 轻松应对
→ 但如何同步到数据库?

方案 3:异步写入
→ Redis 立即响应
→ 后台任务批量写入数据库
→ 但如果 Redis 宕机?数据丢失?

我需要一个既快又可靠的方案!

总结挑战

那天晚上,我整理了所有挑战:

核心挑战 1:查询性能
- COUNT 查询慢(扫描大量索引)
- 解决方案:缓存

核心挑战 2:数据一致性
- 缓存和数据库的一致性
- 并发场景下的竞态条件
- 解决方案:需要更精细的设计

核心挑战 3:高并发写入
- 热门文章大量点赞
- 数据库压力大
- 解决方案:异步写入?消息队列?

核心挑战 4:可靠性
- 缓存故障怎么办?
- 数据会不会丢失?
- 解决方案:持久化、容灾

这些问题,我需要在实践中一个个解决。