点赞系统设计

📅 2026/7/18 13:37:23
点赞系统设计
介绍点赞是一个典型的高频写和高频读并存的场景。如果每点一次赞都直接操作 MySQL 数据库数据库的行锁和磁盘 I/O 会瞬间成为系统瓶颈。架构为了支撑 QPS 十万甚至百万级的流量系统采用Redis 缓存异步写入 MQ 削峰填谷 MySQL 持久化的分层架构。写入链路点赞/取消赞客户端请求通过 API 网关进行限流、防刷和鉴权。Redis 状态校验与更新判断用户是否已点赞幂等性校验。利用 Redis 内存操作的高性能快速更新点赞状态和点赞总数。投递消息至 MQ更新完 Redis 后异步发送一条点赞事件消息到消息队列如 Kafka 或 RocketMQ直接返回给用户“成功”。异步消费落库消费服务批量拉取 MQ 中的消息通过批量更新Batch Update的方式合并写入 MySQL极大减轻数据库压力。读取链路是否点赞 点赞数优先读缓存聚合查询直接透传到 Redis。缓存回源若缓存失效或冷数据读取 MySQL 并回填缓存。核心数据结构设计点赞需要记录两个核心数据谁给什么点赞了明细 和 这个东西有多少人点赞计数。点赞状态明细数据结构Set 或 Bitmap设计策略方案 ASet。Key 为 like:detail:{business_type}:{entity_id}Value 为 user_id。优点判断 SISMEMBER 极快方便找出点赞的用户列表。缺点如果某个大 V 的文章有几千万点赞Set 会非常大BigKey 风险。business_type业务类型/ entity_id实体 ID / 被点赞对象 ID方案 BHash。Key 按照 entity_id 取模如 1000 个分片Field 为 entity_id:user_idValue 为 1点赞或 0取消赞。可以有效打散 BigKey。如果用常规的hash设计结构通常如下会导致BigKeyKeylike:detail:ARTICLE:10086一篇文章一个 KeyFielduser_id点赞的用户 IDValue1已赞或 0取消赞为了不让某一个 Key 变得无限大我们需要把一个大 Hash 拆分成 1000 个小 Hash让数据均匀分布。这就是“取模分片”。分片idshard_id entity_id mod 1000结构设计为Keylike:detail:ARTICLE:{shard_id}不再包含具体的 entity_id只包含分片号Fieldentity_id:user_id把文章 ID 和用户 ID 拼在一起作为 keyValue1 或 0方案 CBitmap适用于用户 ID 是连续递增的整数。Key 为 like:bitmap:{business_type}:{entity_id}Offset 为 user_id。极其节省内存。点赞总数计数数据结构Hash 或 String设计策略使用 Hash 存储。Key 为 like:count:{business_type}:{date_shard}Field 为 entity_idValue 为点赞数。使用 HINCRBY 进行原子原子加减。date_shard 为时间分片表示文章发布的时间MySQL 数据库设计由于数据最终要落库表结构要尽可能精简并建立合适的索引。点赞明细表以文章点赞为例CREATETABLEarticle_like_detail(idBIGINTAUTO_INCREMENTPRIMARYKEY,article_idBIGINTNOTNULLCOMMENT文章ID,user_idBIGINTNOTNULLCOMMENT用户ID,statusTINYINTNOTNULLDEFAULT1COMMENT1-点赞0-取消点赞,create_timeTIMESTAMPDEFAULTCURRENT_TIMESTAMP,update_timeTIMESTAMPDEFAULTCURRENT_TIMESTAMPONUPDATECURRENT_TIMESTAMP,UNIQUEKEYuk_article_user(article_id,user_id),INDEXidx_user_id(user_id))ENGINEInnoDBDEFAULTCHARSETutf8mb4;分库分表策略当数据量达到千万/亿级时按照 article_id 进行哈希分库分表保证同一文章的点赞数据落入同一张表方便批量聚合。高并发场景下的挑战挑战一Redis 缓存与 MQ 消息的一致性问题如果在 Redis 操作成功后发送 MQ 失败了或者服务器挂了会导致 Redis 里的数据和 MySQL 不一致少算或多算。架构师解决方案Lua 脚本保证 Redis 原子性将“检查是否已赞 改变状态 改变计数”写入 Lua 脚本确保 Redis 内部状态绝对一致。本地消息表 / 事务消息使用 RocketMQ 的事务消息。先发半消息Half MessageRedis 操作成功后确认提交消息。兜底策略定时对账通过 BinlogCanal或定时任务定期将 Redis 中的高频热点计数与 MySQL 进行校对以 Redis 为准异步修正 MySQL。挑战二极端热点事件名人爆款明星点赞问题某个超级大 V 发了一条微博瞬间产生几十万 QPS 的点赞Redis 单 Key 遭遇热点Hot Key击穿导致 Redis 代理或分片集群瘫痪。架构师解决方案本地多级缓存Caffeine在 Java 服务节点内存中缓存“点赞计数”。在秒级内直接在内存中做 LongAdder 计数累加每隔 500ms 批量同步一次 Redis。Key 散列分片将热点 entity_id 加上随机后缀如 entity_id_1, entity_id_2分散到不同的 Redis 节点上读取时再做聚合Reduce。挑战三MQ 消费堆积与 MySQL 写入瓶颈问题即使有 MQ 削峰如果消费端一条条插入 MySQL依然会把数据库拖垮。架构师解决方案批量聚合消费Batch Consumer消费端不要来一条消费一条。设置一个窗口比如每 200ms 或聚合满 1000 条在内存中利用 Map 进行去重和合并如果一个用户在 200ms 内点赞又取消直接抵消不用落库。批量写数据库合并后利用 INSERT INTO … ON DUPLICATE KEY UPDATE 进行批量提交将成千上万次数据库 I/O 变成一次 Batch 操作。