技术方案

方案探索

面对上一节梳理的挑战,我开始研究各种技术方案。

技术选型

我考虑了几种方案:

方案 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 日志(实时)

数据库数据:
- 主从复制
- 每日备份

异步消息队列:
- 消息持久化
- 死信队列

效果评估

实施新方案后,我观察了一周的数据:

性能指标

指标优化前优化后提升
查询响应时间800ms5ms160 倍
数据库 QPS3200150降低 95%
Redis QPS-3000新增
缓存命中率60%99%+65%
并发处理能力100/秒5000/秒50 倍

可靠性指标

指标
数据一致性99.9%(通过对账)
系统可用性99.95%
故障恢复时间< 5 分钟

业务指标

指标优化前优化后
用户投诉3 条/周0 条/周
点赞成功率95%99.9%
页面加载速度3s0.5s

效果显著!

课后练习

练习 1

Redis 中 INCR 命令的时间复杂度是多少?为什么?

参考答案(3 个标签)
Redis时间复杂度性能

答案:O(1),常数时间复杂度。

原因

  1. Redis 是基于内存的存储
  2. 计数器存储为字符串类型,直接在内存中进行整数加减运算
  3. 不需要遍历数据结构,直接定位到 key 并执行操作
  4. 单线程执行,无锁竞争开销

对比 MySQL COUNT 查询

  • MySQL COUNT: O(n),需要扫描索引或表
  • Redis INCR: O(1),直接操作内存

这也是为什么 Redis 适合做计数器的原因。

练习 2

如何解决缓存和数据库的一致性问题?请列举至少三种方案。

参考答案(3 个标签)
Redis数据一致性系统设计

常见方案

方案一:Cache Aside Pattern(旁路缓存)

方案二:Write Through(写穿透)

方案三:Write Behind(异步写入)

方案四:双写一致性(分布式锁)

各方案对比

方案一致性性能实现复杂度
Cache Aside最终一致
Write Through强一致
Write Behind最终一致
分布式锁强一致

练习 3

设计一个点赞去重的方案,防止用户重复点赞。

参考答案(3 个标签)
Redis去重并发控制

方案一:数据库唯一索引

数据设计要点

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

优点:数据层保证唯一性 缺点:依赖数据库,性能较差

方案二:Redis SET 去重

优点:性能高,O(1) 复杂度 缺点:Redis 故障时可能失效

方案三:Redis + 数据库双重保证

优点:双重保证,最可靠 缺点:实现复杂

练习 4

如何实现”点赞排行榜”功能?列出 Top 10 点赞最多的文章。

参考答案(3 个标签)
Redis排行榜ZSet

方案:使用 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 次点赞按钮,如何处理?

参考答案(3 个标签)
Redis防抖并发控制

方案一:前端防抖

方案二:后端限流

方案三:幂等性设计

推荐方案

  • 前端防抖 + 后端幂等性设计
  • 限制 1 秒内只能点赞/取消点赞一次
  • 多次点击幂等处理

思考题

  1. 如果 Redis 内存不足,如何优化点赞数据的存储?

  2. 如何实现”点赞用户列表”功能?显示点赞过某篇文章的所有用户。

  3. 如果平台增长到千万级用户,如何设计计数系统的架构?

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