技术方案
方案探索
面对上一节梳理的挑战,我开始研究各种技术方案。
技术选型
我考虑了几种方案:
方案 A:纯数据库
优点:数据强一致性,实现简单
缺点:性能差,扛不住高并发
结论:放弃
方案 B:数据库 + 缓存
优点:查询性能好,实现相对简单
缺点:一致性问题,缓存更新策略复杂
结论:备选
方案 C:Redis 计数 + 异步同步到数据库
优点:性能最好,能扛高并发
缺点:实现复杂,需要考虑可靠性
结论:主力方案
我选择了方案 C:Redis 计数 + 异步同步。
架构设计
我画出了整体架构:
┌─────────────┐
│ 用户请求 │
└──────┬──────┘
│
▼
┌─────────────────────────┐
│ 应用服务器 │
│ - 处理点赞/取消点赞 │
│ - 查询点赞状态 │
│ - 返回点赞数 │
└──────┬─────────┬────────┘
│ │
│ │
▼ ▼
┌──────────┐ ┌──────────────┐
│ Redis │ │ 数据库 │
│ - 计数器│ │ - 点赞记录 │
│ - 集合 │ │ - 用户数据 │
└────┬─────┘ └───────┬──────┘
│ │
└────────┬───────┘
│
▼
┌───────────────┐
│ 异步同步任务 │
│ - 定时同步 │
│ - 数据对账 │
└───────────────┘
数据结构设计
我在 Redis 中设计了两种数据结构:
1. 计数器(String)
Key: article:{article_id}:like_count
Value: 点赞数(整数)
操作: INCR / DECR / GET
示例:
article:123:like_count = 1000
2. 点赞集合(Set)
Key: article:{article_id}:liked_users
Value: 点赞过的用户 ID 集合
操作: SADD / SREM / SISMEMBER
示例:
article:123:liked_users = {1, 2, 3, 5, 8, 13, ...}
为什么需要两种结构?
计数器:
- 快速获取点赞总数
- O(1) 时间复杂度
点赞集合:
- 判断用户是否点赞
- O(1) 时间复杂度
- 支持集合操作
核心流程
点赞流程:
取消点赞流程:
查询点赞数流程:
查询是否点赞:
异步同步设计
为什么要异步同步到数据库?
原因:
1. Redis 数据在内存中,需要持久化
2. 数据库用于数据备份和复杂查询
3. 可以降低数据库写入压力
方式:
- 消息队列(RabbitMQ / Kafka)
- 定时任务(Celery)
异步同步流程:
数据一致性方案
问题:缓存和数据库不一致
场景:用户点赞
时序:
1. Redis 更新成功(INCR)
2. 发送异步消息
3. 消费者处理消息失败(数据库宕机)
结果:
- Redis 显示已点赞
- 数据库没有记录
→ 数据不一致!
解决方案 1:重试机制
解决方案 2:数据对账
解决方案 3:最终一致性
一致性级别:
强一致性:
- 用户体验最好
- 性能最差
- 实现复杂
弱一致性:
- 性能最好
- 用户体验差
- 数据可能丢失
最终一致性:
- 性能好
- 用户体验可接受
- 数据最终会一致
我选择了最终一致性。
原因:
- 点赞数稍微延迟更新,用户可以接受
- 通过对账机制,保证数据最终一致
- 性能和用户体验的平衡
性能优化
批量查询
性能提升:
- 优化前:20 次网络往返
- 优化后:2 次网络往返(pipeline)
- 性能提升:10 倍
本地缓存
注意:本地缓存可能导致数据延迟,需要权衡。
可靠性设计
Redis 故障处理
场景:Redis 宕机
降级策略:
1. 尝试连接 Redis
2. 如果连接失败,直接查询数据库
3. 返回结果,但性能下降
代码示例:
def get_like_count_with_fallback(article_id):
try:
return redis.get(f"article:{article_id}:like_count")
except RedisConnectionError:
# 降级:查询数据库
return db.query(
"SELECT COUNT(*) FROM article_likes WHERE article_id = ?",
article_id
)[0]['count']
数据备份策略
Redis 数据:
- RDB 快照(每小时)
- AOF 日志(实时)
数据库数据:
- 主从复制
- 每日备份
异步消息队列:
- 消息持久化
- 死信队列
效果评估
实施新方案后,我观察了一周的数据:
性能指标
| 指标 | 优化前 | 优化后 | 提升 |
|---|---|---|---|
| 查询响应时间 | 800ms | 5ms | 160 倍 |
| 数据库 QPS | 3200 | 150 | 降低 95% |
| Redis QPS | - | 3000 | 新增 |
| 缓存命中率 | 60% | 99% | +65% |
| 并发处理能力 | 100/秒 | 5000/秒 | 50 倍 |
可靠性指标
| 指标 | 值 |
|---|---|
| 数据一致性 | 99.9%(通过对账) |
| 系统可用性 | 99.95% |
| 故障恢复时间 | < 5 分钟 |
业务指标
| 指标 | 优化前 | 优化后 |
|---|---|---|
| 用户投诉 | 3 条/周 | 0 条/周 |
| 点赞成功率 | 95% | 99.9% |
| 页面加载速度 | 3s | 0.5s |
效果显著!
课后练习
练习 1
Redis 中 INCR 命令的时间复杂度是多少?为什么?
答案:O(1),常数时间复杂度。
原因:
- Redis 是基于内存的存储
- 计数器存储为字符串类型,直接在内存中进行整数加减运算
- 不需要遍历数据结构,直接定位到 key 并执行操作
- 单线程执行,无锁竞争开销
对比 MySQL COUNT 查询:
- MySQL COUNT: O(n),需要扫描索引或表
- Redis INCR: O(1),直接操作内存
这也是为什么 Redis 适合做计数器的原因。
练习 2
如何解决缓存和数据库的一致性问题?请列举至少三种方案。
常见方案:
方案一:Cache Aside Pattern(旁路缓存)
方案二:Write Through(写穿透)
方案三:Write Behind(异步写入)
方案四:双写一致性(分布式锁)
各方案对比:
| 方案 | 一致性 | 性能 | 实现复杂度 |
|---|---|---|---|
| Cache Aside | 最终一致 | 中 | 低 |
| Write Through | 强一致 | 低 | 中 |
| Write Behind | 最终一致 | 高 | 高 |
| 分布式锁 | 强一致 | 低 | 高 |
练习 3
设计一个点赞去重的方案,防止用户重复点赞。
方案一:数据库唯一索引
数据设计要点
- 核心是在
article_likes里保存业务事实,而不是把规则散落在应用逻辑里。- 索引服务于高频查询,重点是缩小扫描范围,而不是堆更多字段。
- 关键字段包括
article_id、user_id、created_at,它们决定后续查询和管理能力。
优点:数据层保证唯一性 缺点:依赖数据库,性能较差
方案二:Redis SET 去重
优点:性能高,O(1) 复杂度 缺点:Redis 故障时可能失效
方案三:Redis + 数据库双重保证
优点:双重保证,最可靠 缺点:实现复杂
练习 4
如何实现”点赞排行榜”功能?列出 Top 10 点赞最多的文章。
方案:使用 Redis Sorted Set(有序集合)
Redis Sorted Set 特性:
- 自动按分数排序
- O(log N) 插入和更新复杂度
- O(log N) 查询复杂度
优化建议:
- 只保留 Top N,定期清理:
ZREMRANGEBYRANK article:like_ranking 0 -1001 - 使用定时任务更新排行榜,而不是实时更新
- 考虑分页查询:
ZREVRANGE key start stop
练习 5
如果用户在 1 秒内快速点击 10 次点赞按钮,如何处理?
方案一:前端防抖
方案二:后端限流
方案三:幂等性设计
推荐方案:
- 前端防抖 + 后端幂等性设计
- 限制 1 秒内只能点赞/取消点赞一次
- 多次点击幂等处理
思考题
-
如果 Redis 内存不足,如何优化点赞数据的存储?
-
如何实现”点赞用户列表”功能?显示点赞过某篇文章的所有用户。
-
如果平台增长到千万级用户,如何设计计数系统的架构?
💡 提示:这些问题没有标准答案,建议结合实际情况深入思考。
