拓冰建站拓冰建站
首页 / 资讯中心 / 正文

如何设计一个高并发点赞系统?从 Redis、MQ、幂等到最终一致性

如何设计一个高并发点赞系统从 Redis、MQ、幂等到最终一致性一个看似简单的点赞按钮背后串联着缓存、消息队列、数据库、幂等设计和数据一致性。本文从三个高频面试问题出发完整拆解高并发点赞系统的设计思路。前言在后端开发面试中“设计一个高并发点赞系统”是一道非常经典的系统设计题。面试官通常会继续追问大量用户同时点赞如何避免把数据库打垮如何防止一个用户重复点赞同时支持取消点赞Redis、MQ 和 MySQL 之间出现数据不一致怎么办表面上是在讨论点赞功能实际上是在考察我们能否把高并发抗压、幂等去重、异步削峰和最终一致性串成一套完整方案。面试问题核心知识点高并发点赞如何抗住Redis、MQ、削峰填谷、批量写、热点 Key、限流降级、分库分表点赞如何去重、取消幂等、状态机、Redis Set/Bitmap、Lua 原子操作、唯一索引如何保证最终一致可靠消息、消费幂等、重试、死信队列、补偿、对账接下来让我们跟着图片中的点赞红心走完一次完整的“高并发之旅”。一、整体架构一次点赞会经过哪些组件一个常见的点赞链路可以抽象为用户点击点赞 ↓ 点赞服务 ↓ Redis 原子更新 ↓ 发送点赞事件 ↓ MQ ↓ 消费者聚合处理 ↓ MySQL各组件的职责如下组件主要职责点赞服务接收请求、鉴权、限流和参数校验Redis承接实时高并发维护点赞状态和计数MQ缓冲瞬时流量将同步写库改为异步写库消费者幂等消费、聚合增量、批量落库MySQL持久化点赞关系和点赞统计数据这套架构的核心思想是Redis 负责快MQ 负责缓冲MySQL 负责最终持久化。二、问题一10 万用户同时点赞数据库怎么扛2.1 为什么不能直接更新 MySQL最直接的实现是每收到一次点赞请求就执行UPDATEvideoSETlike_countlike_count1WHEREid10001;低并发时这样做没有问题。但面对热门视频、明星动态或大型直播间大量请求会同时更新同一行数据导致MySQL QPS 突然升高热点行锁竞争严重数据库连接池被占满请求延迟增加甚至引发级联故障。因此高并发点赞系统的第一原则是不要让每一次点赞请求都同步更新数据库。2.2 使用 Redis 承接实时流量点赞请求先写入 Redis例如like:count:video:10001 9527计数可以使用INCR like:count:video:10001Redis 基于内存处理请求适合承接高频读写。于是原来的10 万个请求 → 10 万次 MySQL UPDATE变成10 万个请求 → Redis 快速响应 → 后台异步落库2.3 使用 MQ 削峰填谷Redis 更新完成后系统发布点赞事件到 MQ用户点赞 → Redis → MQ → Consumer → MySQL如果瞬间进入 10 万条点赞请求MySQL 不需要同时处理 10 万次写操作。MQ 会把流量暂存下来消费者按照数据库能够承受的速度逐步处理。这就是消息队列的重要作用削峰填谷和异步解耦。2.4 消费端聚合后批量写库消费者不一定要“一条消息执行一次 UPDATE”。可以在一个时间窗口内按target_id聚合增量视频 A800 视频 B120 视频 C80再批量更新数据库。这样1000 条点赞事件可能被压缩成几次数据库操作进一步降低写压力。需要注意关系明细和聚合计数是两类数据。关系明细通常需要幂等写入聚合计数可以按窗口批量更新但必须有对账或重算能力。2.5 超级热门内容的热点 Key 问题如果几百万用户同时操作like:count:video:666即使 Redis 整体性能足够单个 Key 仍可能成为热点。常见做法是把计数拆成多个分片like:count:video:666:0 like:count:video:666:1 ... like:count:video:666:15根据用户 ID 选择分片intslotMath.floorMod(Long.hashCode(userId),16);读取总点赞数时再汇总 16 个分片。为了避免每次读取都实时求和也可以定期聚合并缓存展示值。这一部分考察的是Redis、高并发架构、MQ、削峰填谷、批量写入、热点治理、限流降级与水平扩展。三、问题二如何防止重复点赞还能取消点赞3.1 点赞本质上是状态变更点赞不能简单理解为每次请求都执行count 1而应该看成状态转换原状态用户操作新状态计数变化未点赞点赞已点赞1已点赞点赞已点赞不变已点赞取消未点赞-1未点赞取消未点赞不变只有状态真正发生变化时才能修改点赞计数。这就是幂等设计的核心。3.2 使用 Redis Set 去重可以为内容维护已点赞用户集合like:users:video:10001 {1001, 1002, 1003}用户点赞时执行SADD like:users:video:10001 1001SADD的返回值可以直接判断状态是否改变返回1用户此前没有点赞本次需要将计数加一返回0用户已经点赞本次请求属于重复操作。取消点赞时执行SREM like:users:video:10001 1001只有SREM真正删除成员时才将计数减一。3.3 使用 Lua 保证 Redis 内部原子性如果业务先执行SADD再执行INCR两条命令之间服务发生故障就可能出现点赞关系已写入但点赞数没有增加因此需要用 Lua 把“修改关系”和“修改计数”放在一次原子执行中。点赞脚本示例localaddedredis.call(SADD,KEYS[1],ARGV[1])ifadded1thenredis.call(INCR,KEYS[2])return1endreturn0取消点赞脚本示例localremovedredis.call(SREM,KEYS[1],ARGV[1])ifremoved1thenlocalcounttonumber(redis.call(GET,KEYS[2])or0)ifcount0thenredis.call(DECR,KEYS[2])endreturn1endreturn0这里的原子性只覆盖同一个 Redis 实例内的操作并不自动保证 Redis、MQ 和 MySQL 的跨组件事务。3.4 数据库使用唯一索引兜底点赞关系表可以这样设计CREATETABLEuser_like(idBIGINTPRIMARYKEYAUTO_INCREMENT,user_idBIGINTNOTNULL,target_idBIGINTNOTNULL,statusTINYINTNOTNULLCOMMENT1点赞0取消,versionBIGINTNOTNULLDEFAULT0,update_timeDATETIMENOTNULL,UNIQUEKEYuk_user_target(user_id,target_id));user_id target_id唯一索引保证同一个用户对同一个对象最多只有一条关系记录是数据库层面的幂等兜底。不过唯一索引只能防止重复记录不能独自解决消息乱序。例如用户先点赞再取消但“取消”消息先落库“点赞”消息后落库最终状态仍可能错误。生产环境通常还需要为状态变更携带单调递增的版本号数据库只接受比当前版本更新的事件或者让同一个user_id target_id的消息进入同一有序分区。3.5 Redis Set 可以存一亿个用户吗Redis Set 便于理解和实现但超级热门内容可能产生 Big Key并带来较高内存成本。数据规模较大时可以根据业务特点选择按用户 ID 对 Set 分片使用 Bitmap但需要可映射且相对连续的用户编号数据库保存完整关系Redis 只缓存热点状态使用布隆过滤器辅助判断“不存在”但不能仅靠它完成精确点赞状态判断。这一部分考察的是幂等性、状态机、Redis 数据结构、Lua 原子操作、数据库唯一约束、事件顺序和 Big Key 治理。四、问题三Redis、MQ、MySQL 如何保证最终一致4.1 为什么不追求每一毫秒都完全一致点赞通常不是余额扣款或支付交易。短时间内出现Redis 点赞数 10001 MySQL 点赞数 10000往往可以接受但系统必须确保数据最后能够收敛到正确状态。因此点赞场景通常采用最终一致性模型。4.2 消费失败后的恢复链路消费者写 MySQL 失败时常见处理流程是消费失败 ↓ 有限次数重试 ↓ 仍然失败 ↓ 进入死信队列 ↓ 补偿任务处理 ↓ 定时对账修复完整机制包括MQ 消息持久化和生产确认消费成功后再 ACK消费失败进行退避重试超过阈值进入死信队列人工或自动补偿定时对账发现并修复 Redis 与数据库差异。4.3 MQ 重复投递怎么办很多 MQ 提供的是At Least Once语义消息至少会被消费一次但可能重复。假设数据库已经写入成功但消费者 ACK 丢失MQ 会再次投递同一条消息。如果消费者每次都无条件执行like_count 1计数就会错误。因此消费者必须幂等。常见方案包括使用业务唯一键约束点赞关系给事件分配唯一event_id记录已处理事件通过版本号判断事件是否过期根据关系状态是否真正变化决定是否修改计数。换句话说消费一次消息不等于无条件执行一次加一。4.4 Redis 成功、MQ 发送失败怎么办这是面试中非常关键的深挖点。如果代码只是顺序执行更新 Redis → 发送 MQ那么 Redis 更新成功但 MQ 发送失败时MySQL 永远收不到这次变更。可以根据业务和基础设施选择生产端确认、失败重试并将失败事件写入可恢复存储使用事务消息使用本地消息表或 Outbox再由后台任务可靠投递记录可追溯的变更日志通过定时扫描补发用对账任务作为最终兜底。这里不存在“加一个 MQ 就天然一致”。真正要回答的是任何一步失败后系统如何发现、重试和修复。4.5 对账应该以谁为准对账前必须明确可信数据源。如果点赞关系表保存了每个用户的最终状态那么它通常比一个累计计数更适合作为事实依据。可以定期按点赞关系表重新聚合SELECTtarget_id,COUNT(*)FROMuser_likeWHEREstatus1GROUPBYtarget_id;然后与统计表、Redis 展示计数比较对异常数据执行重算或补偿。超大数据量下不会每次全表扫描而会采用分片扫描、增量校验、抽样校验和离线任务。这一部分考察的是最终一致性、可靠消息、消费幂等、异常重试、死信队列、补偿机制、数据对账和可恢复性设计。五、一套更完整的生产级方案把前面的设计串起来可以得到如下流程用户点赞/取消 ↓ 鉴权、限流、参数校验 ↓ Redis Lua 原子修改关系状态与实时计数 ↓ 可靠地产生状态变更事件 ↓ MQ 持久化与削峰 ↓ 消费者按业务键有序、幂等消费 ↓ 更新点赞关系表 ↓ 聚合并批量更新点赞统计表 ↓ 重试 → 死信 → 补偿 → 对账 ↓ 数据最终一致系统设计时还应关注以下非功能性能力限流与降级高峰期保护服务、Redis 和数据库监控告警关注请求成功率、Redis 延迟、MQ 堆积、消费失败率和对账差异容量规划估算点赞关系规模、Redis 内存、消息吞吐和数据库写入能力防刷与风控限制异常账户、设备和 IP 的高频操作可观测性通过event_id、用户 ID 和目标 ID 串联完整处理链路。六、面试时可以怎样回答如果面试时间有限可以先给出下面这段主干回答我不会让每一次点赞请求都直接更新 MySQL而会使用 Redis 承接实时高并发再通过 MQ 异步削峰。消费者可以按内容聚合点赞增量并批量更新数据库从而减少热点行竞争和数据库写入次数。点赞去重方面我会把点赞看作用户与内容之间的状态变更。可以使用 Redis Set 的 SADD、SREM 判断状态是否真正改变并通过 Lua 将关系修改和计数修改原子执行。数据库使用user_id target_id唯一索引兜底同时通过版本号或有序消息处理点赞与取消的乱序问题。Redis、MQ 和 MySQL 之间采用最终一致性。生产端保证消息可恢复消费端保证幂等并通过重试、死信队列、补偿任务和定时对账处理异常。如果出现超级热门内容还需要处理 Redis 热点 Key 和 Big Key可以通过 Key 分片等方式分散压力。这段回答已经覆盖了题目的三条主线高并发问题看吞吐 重复点赞问题看幂等 跨组件问题看一致性七、面试官可能继续追问什么1. Redis 挂了点赞功能应该如何降级Redis 挂了我会避免缓存雪崩式穿透数据库。读请求可以通过本地缓存、旧值或者兜底值降级写请求优先限流和熔断如果 MQ 仍可用可以将点赞事件临时写入 MQ后续恢复 Redis如果 MQ 和 Redis 都异常则宁可临时关闭点赞也不能把流量全部打到数据库。2. Redis 更新成功但 MQ 消息发送失败怎么办使用生产确认和有限重试并把待发送事件记录到可恢复的消息表、Outbox 或变更日志中由后台任务补发同时通过对账任务兜底避免事件永久丢失。3. MQ 为什么会重复投递消费者如何做到幂等消费者处理成功但 ACK 丢失时MQ 可能再次投递消息。可以使用唯一event_id、数据库唯一索引、已消费记录和状态变更判断确保同一事件重复执行不会重复计数。4. 点赞与取消消息乱序怎么办将同一个user_id target_id的消息路由到同一有序分区并为事件携带版本号或操作序号数据库只接受比当前版本更新的状态。5. Redis Set 存放上亿用户会产生什么问题会消耗大量内存并形成 Big Key影响删除、迁移和主从同步。可以采用分片 Set、Bitmap或者由数据库保存完整关系、Redis 只缓存热点数据。6. 热点 Key 拆分后如何高效读取总点赞数后台定期汇总各个分片将结果写入一个只读的总计数缓存。查询时直接读取聚合结果允许展示值存在秒级延迟。7. 点赞关系表和点赞统计表不一致时以谁为准通常以保存用户最终点赞状态的关系表作为事实依据重新聚合生成统计值再修复统计表和 Redis 缓存。8. 如何设计对账、补偿和告警机制定时分片扫描关系数据与统计数据的差异对异常记录自动重算或补发事件当 MQ 堆积、重试次数、死信数量或数据差异超过阈值时及时告警。9. 点赞消息积压时应该扩容消费者还是直接丢弃先定位瓶颈在数据库承载范围内扩容消费者或提高批量处理能力。点赞状态属于业务数据通常不能直接丢弃只有明确允许丢失且能够通过关系数据重建的统计消息才可以合并或舍弃。10. 如何进行容量评估和压测根据峰值 QPS、点赞比例、单条关系大小、Redis 内存、MQ 吞吐量和数据库批量写能力估算容量压测时还要覆盖热点 Key、重复请求、组件故障和消息积压等场景。总结一个小小的点赞按钮背后可能经历Redis 抗流量 ↓ 幂等去重 ↓ Lua 保证原子性 ↓ MQ 异步削峰 ↓ 消费者聚合、批量落库 ↓ MySQL 持久化 ↓ 重试、死信、补偿和对账 ↓ 最终一致所以面试官问“如何设计一个高并发点赞系统”时不要只回答一句“用 Redis”。这道题真正考察的是能否围绕一个高并发业务把缓存、数据库、消息队列、幂等、顺序性和数据一致性完整地串起来。推荐标签Redis、消息队列、MySQL、后端、系统设计、分布式
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门