# 如何设计一个点赞系统?
9 min read
Table of Contents
如何设计一个点赞系统?
如何设计一个点赞系统?
【核心定义】
点赞系统是一个高并发、高可用、最终一致性的计数与状态管理服务,其核心本质是处理海量“写-读”请求下的数据一致性、性能与扩展性平衡。
【关键要点】
- 数据结构设计:采用“计数+状态”分离存储,计数用 Redis String/哈希存储聚合值,用户点赞状态用 Redis Set/Hash 记录防重复。
- 核心算法:基于异步合并+批量写入的最终一致性模型,通过消息队列缓冲写请求,定时同步到持久化数据库。
- 扩容/保护机制:读写分离、缓存分层(本地缓存+分布式缓存)、服务限流与降级、数据库分库分表。
- 线程安全:通过 Redis 原子操作(INCR/DECR、SADD/SREM)保证单点操作的原子性,分布式锁仅用于极端并发控制。
- 版本差异:V1.0 同步写数据库 → V2.0 引入 Redis 缓存 → V3.0 引入异步消息队列与最终一致性。
【深度推导/细节】
1. 数据结构与存储设计
-
计数存储:
- Redis Key:
like:count:{target_type}:{target_id}(如like:count:post:123) - 值类型:String(直接存储整数)或 Hash(存储多维度计数,如点赞、收藏)。
- 设计理由:Redis 单线程内存操作,INCR/DECR 原子性保证计数准确,O(1) 时间复杂度。
- Redis Key:
-
用户状态存储:
- Redis Key:
like:status:{target_type}:{target_id} - 值类型:Set(存储用户 ID,自动去重)或 Bitmap(海量用户时节省内存)。
- 设计理由:快速判断用户是否已点赞(SISMEMBER O(1)),避免重复点赞。
- Redis Key:
2. 核心流程与算法
Step 1:用户点赞请求
- 前端发送
POST /like,参数:target_type,target_id,user_id。 - 网关层进行身份验证与限流(如令牌桶算法,限制每秒请求数)。
Step 2:业务逻辑处理
def like(target_type, target_id, user_id): # 1. 检查是否已点赞(防重复) if redis.sismember(f"like:status:{target_type}:{target_id}", user_id): return "already_liked"
# 2. 原子性执行:计数+状态更新 with redis.pipeline() as pipe: pipe.incr(f"like:count:{target_type}:{target_id}") # 计数+1 pipe.sadd(f"like:status:{target_type}:{target_id}", user_id) # 记录状态 pipe.execute()
# 3. 发送异步消息到队列(用于持久化与业务解耦) mq.send("like_events", {"target_type": target_type, "target_id": target_id, "user_id": user_id, "action": "like"})
return "success"Step 3:异步持久化
- 消费者从消息队列拉取点赞事件,批量合并后写入 MySQL。
- 批量策略:每 1000 条或每 5 秒聚合一次,减少数据库写入压力。
- 数据库表设计:
-- 计数表(按 target 分片)CREATE TABLE like_count (target_type VARCHAR(32),target_id BIGINT,count INT,PRIMARY KEY(target_type, target_id)) ENGINE=InnoDB;-- 状态表(按 user_id 分片)CREATE TABLE like_status (user_id BIGINT,target_type VARCHAR(32),target_id BIGINT,created_at TIMESTAMP,PRIMARY KEY(user_id, target_type, target_id)) ENGINE=InnoDB;
3. 扩容与保护机制
- 读写分离:读请求直接走 Redis(99% 场景),写请求走异步队列。
- 缓存分层:
- L1:本地缓存(Guava Cache,缓存热点内容点赞数,TTL=10s)。
- L2:Redis 集群(分片存储,采用一致性哈希避免扩容抖动)。
- 限流降级:
- 入口层:Nginx 限流,防止恶意刷赞。
- 服务层:Sentinel 或 Hystrix 实现熔断,当 Redis 不可用时降级为直接写队列。
- 数据库分库分表:
- 按
target_id哈希分片(如 1024 个分片),解决单表数据量过大问题。
- 按
4. 线程安全与一致性
- 原子操作:Redis INCR 和 SADD 是单线程原子操作,无需额外锁。
- 极端场景:当需要同时更新多个关联资源时,使用分布式锁(RedLock)保证强一致性,但会牺牲性能(一般避免)。
- 最终一致性模型:
- 用户看到点赞成功 → 计数已更新(Redis)。
- 后台异步同步到 MySQL(延迟 1-5 秒)。
- 通过定时对账任务修复数据不一致(每小时扫描差异并修复)。
【关联/对比】
- vs. 传统同步写数据库:传统方式无法承受高并发(如明星发微博瞬间百万点赞),本设计通过缓存+异步解耦,QPS 可从 1000 提升到 10 万+。
- vs. 纯内存方案:纯内存(如 HashMap)无法持久化且无法分布式扩展,本设计结合了内存速度与磁盘持久化。
- vs. 其他计数系统:点赞系统更注重“状态防重复”与“实时性”,而分享计数可能更注重去重算法(Bloom Filter)。
『面试官追问』
-
Q:如果消息队列积压导致数据同步延迟,用户看到计数不一致怎么办?
- A:采用“缓存为主,数据库为辅”的读取策略。读时优先读 Redis,如果 Redis 故障则读数据库+重建缓存。延迟在秒级可接受,同时前端可设计“软计数”(点赞后本地直接+1,避免等待)。
-
Q:Redis 内存爆了怎么办?
- A:分层存储策略。热点数据放 Redis,全量数据放 MySQL。对冷数据(如 3 个月前的帖子点赞状态)从 Redis 迁移到 MySQL,Redis 只保留最近活跃数据。
-
Q:如何防止刷赞?
- A:多维度风控。① IP 限流:每个 IP 每分钟最多点赞 100 次。② 用户行为分析:检测异常模式(如瞬间点赞不同帖子 1000 次)。③ 验证码挑战:对可疑请求弹出验证码。
-
Q:Bitmap 和 Set 如何选择?
- A:Bitmap 内存占用固定(每个目标 1.2MB 可支持 1000 万用户),适合海量用户场景(如微博大 V)。Set 内存随用户数增长,适合中小规模。临界点:当用户数 > 100 万时,Bitmap 优势明显。
-
Q:分库分片键为什么用 target_id 而不是 user_id?
- A:因为查询模式多为“查看某个内容的点赞数”,按 target_id 分片可保证同一目标的数据落在同一分片,避免跨分片查询。用户维度的查询(如“我赞过的帖子”)可通过冗余表或 ES 解决。
【直击痛点:关键数字与设计合理性】
- Redis 批量写入间隔:5 秒/1000 条:平衡数据延迟与数据库压力,太短则数据库负载高,太长则故障时丢失数据多。
- 本地缓存 TTL:10 秒:保证数据相对新鲜,同时避免缓存穿透击穿数据库。
- 分片数量:1024:保证足够分散,且后续扩容可翻倍(一致性哈希)。
- 限流阈值:单 IP 100 次/分钟:基于正常人类操作频率(每秒 < 2 次),防止脚本刷赞。
总结:一个工业级点赞系统设计需围绕“高性能、高可用、可扩展”三大原则,通过缓存原子操作+异步持久化+分层限流的核心架构,平衡并发性能与数据一致性。关键是要根据业务规模(用户量、QPS)灵活选择数据结构(Set/Bitmap)与同步策略(实时/延迟)。