Skip to content

如何设计一个点赞系统? ​

  • 标签:系统设计、场景题

🏛 业务架构师 ​

点赞是典型的高并发写 + 弱一致容忍场景,我的分层设计:

1. 写路径:先扛住,再落库

  • 去重:SET like:{内容ID}:{用户ID} 或 Bitmap(SETBIT like:{内容ID} {用户ID偏移} 1)——一人一赞的业务规则在存储结构上直接表达;
  • 计数:ZSet/hash 的 incr 计数,计数与去重双写要原子(Lua 打包);
  • 点赞事件进 MQ,异步落库——用户操作只等 Redis 返回(毫秒级),持久化排队做。

2. 同步落库:批量合并

消费者攒批(每秒/每千条)update 点赞表与计数表——1 万次点赞合并为几十条 SQL,DB 压力降两个数量级;落库失败重试 + 对账补偿。

3. 读路径

  • 计数读 Redis(显示赞数);
  • "我是否赞过"用 Bitmap/Redis 判断;
  • 榜单类(获赞最多)ZSet 承载。

4. 业务治理(比架构更重要)

  • 防刷:频次限制(单用户每分钟 N 赞)、设备指纹、异常增长告警——刷赞是黑产刚需,安全预算要预留;
  • 冷热分离:热门内容活跃计数走上述链路;长尾内容定期归档,计数快照化;
  • 降级预案:Redis 故障时"点赞暂不可用"比"点赞打挂全站"好——非核心功能的牺牲优先级要提前声明。

一致性立场:计数短暂不精确(展示 1.2 万 vs 实际 1.2001 万)业务完全可接受——用最终一致(对账定时校准)换高并发,这是典型的"一致性换吞吐"定价。唯一不能错的是去重(一人一赞是规则)——它有一致性保障(唯一键兜底)。

一句话:点赞系统 = 去重强一致 + 计数最终一致 + 读写分层削峰。

🏗 系统架构师 ​

点赞系统是典型的高并发写 + 实时统计场景,我按"写链路、读链路、一致性、防刷"四块设计:

1. 数据模型:点赞关系表 like(user_id, target_id, target_type, create_time),唯一索引 (user_id, target_id) 天然防重复点赞;计数表 like_count(target_id, count) 或 Redis 承载。

2. 写链路(削峰聚合):用户点赞 → 先写 Redis Set(like:{targetId} 存 user)或 Bitmap(user_id 连续时 Bitmap 省 10 倍内存)做实时判重 + ZSet 记录近期点赞 → 异步写 MQ → 消费端批量聚合落库(合并同一 target 的 N 次点赞为一次 update count = count + N)。DB 只承受聚合后的低频写入,扛住大促洪峰。

3. 读链路:计数读 Redis(like:count:{targetId} 缓存 + 定时与 DB 校准),列表读(谁赞过我)走 DB 分页或 ES。热点内容的计数用本地缓存兜一层。

4. 一致性与容灾:Redis 与 DB 最终一致——点赞结果先落 Redis 立即返回,MQ 投递失败落本地消息表补发;Redis 故障降级为"先查 DB 判重";对账任务按天校验 Redis/DB 计数差异并修复。

5. 防刷与体验:单用户单目标频控、单用户每日点赞上限、设备/黑产特征拦截;取消点赞 = 删 Set 成员 + 计数减一(同样幂等);"先点赞后取消"的抖动在聚合层做窗口合并。

核心架构思想:关系(判重)用 Set/DB 唯一约束,计数用 Redis 热路径 + 异步聚合落库,读写分离、峰值化渐。

💻 全栈开发专家 ​

点赞看着简单,难在高并发写 + 防重 + 数据量。我的设计:

1. 存储模型(防重是核心)

sql
CREATE TABLE user_like (
  id BIGINT PRIMARY KEY,
  user_id BIGINT NOT NULL,
  target_id BIGINT NOT NULL,      -- 被赞对象(帖子/评论)
  target_type TINYINT NOT NULL,   -- 类型:1帖子 2评论
  create_time DATETIME,
  UNIQUE KEY uk_user_target (user_id, target_type, target_id)  -- 唯一索引防重复点赞
);

2. 高并发写:Redis 承接 + 异步落库

java
// 点赞:Set 判重 + 计数器,DB 不直接扛写
Boolean first = redis.opsForSet().add("liked:" + targetType + ":" + targetId, userId);
if (Boolean.TRUE.equals(first)) {
    redis.opsForValue().increment("like:cnt:" + targetType + ":" + targetId);
    mq.send(new LikeMessage(userId, targetId, type));   // 异步落库
} else {
    redis.opsForSet().remove(...);  // 再点一次=取消,同时计数-1、发取消消息
}

3. 消费端幂等落库:唯一索引兜底(重复消息撞索引忽略),计数表 like_count(target_id, cnt) upsert。

4. 一致性与对账:Redis 计数是准实时,DB 是最终一致;定时对账(DB count vs Redis 计数)纠正漂移;Redis 挂了从 DB 重建(Set 太大可只重建计数,明细靠 DB)。

5. 大 V 场景:单条微博百万赞,Set 太大就不存明细只存计数(是否点过赞容忍"重赞一次"或落 DB 查询);计数展示可缓存 + 10 秒刷新,业务上不要求绝对实数。

关键取舍:点赞是"可丢失可重算"的数据,用最终一致换写入性能——这类判断比技术细节更值得讲。