这是 Beta 探索课程,内容结构、实验步骤和示例可能会继续调整。
核心挑战
增长的烦恼
三个月后,平台开始增长。
用户数: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:可靠性
- 缓存故障怎么办?
- 数据会不会丢失?
- 解决方案:持久化、容灾
这些问题,我需要在实践中一个个解决。
