17.如何设计一个不崩的点赞系统?
17.如何设计一个不崩的点赞系统?
大家好,我是牛哥。
用户对点赞的容忍度有多低?如果点击点赞后3秒没看到按钮变色,90%的用户会直接划走;如果同一内容点了两次赞却计了两次数,70%的用户会觉得“平台在刷数据”。
一个小小的点赞按钮,藏着社交平台的“生存密码”。今天我们就来拆解,如何设计一个既快又稳还省钱的点赞系统。
需求拆解
分析场景题的第一步,永远是把模糊的需求变成清晰的技术指标。点赞系统看起来简单,但要做好,这四个核心需求必须先想清楚:
- 快 — 秒级响应
用户点击点赞按钮后,必须秒响应。这里我们要看的是端到端延迟,从用户点击到界面反馈,超过 500ms 用户就会感知到卡顿。
你可能会说,500ms很宽松啊?但实际场景中,一次点赞要经过前端请求、网络传输、服务处理、缓存操作、数据库同步等多环节,每个环节都可能出问题,所以必须把目标定在200ms以内。

- 准 — 数据一致性
10万次点赞少算1个,用户可能不会发现;但如果重复统计,比如同一个用户点了两次赞却计了两次数,就会引发投诉。
这里的准不是强一致性,而是"不丢不重+最终一致"。用户点赞后,即使数据还没同步到数据库,至少缓存里要是对的;取消点赞时,也不能因为并发操作导致计数没减。
3. 稳 — 流量洪峰下的系统韧性
假设一条热门内容在 5 分钟内获得 100 万点赞,平均每秒就是 3333 次请求,高峰期可能达到每秒 1 万次。
这时候系统不能崩,不能丢数据,甚至不能出现明显的延迟抖动。要知道,点赞请求往往是突发的、无规律的,这对系统的弹性能力是极大考验。
4. 省 — 成本控制
这里的省不是偷工减料,而是合理利用资源:
热数据:像热门内容的实时点赞数这类高频读写的数据,必须放 Redis 里,保证用户点完赞立刻有反馈;
温数据:比如近 30 天的普通内容点赞记录,这类数据读写频率中等,不用一直占着 Redis 内存,可以存在 MySQL 的普通表里,需要时再查,兼顾性能和成本;
冷数据:超过 3 个月的历史点赞记录,基本只有后台统计、数据分析时才会用到,属于低频读写的冷数据,直接归档到低成本的存储服务里。
技术选型:3个核心工具
明确了需求,接下来要考虑的就是用什么技术栈支撑。点赞系统的技术选型,本质是对「高频读写」、「数据一致性」和「成本控制」这三个核心矛盾的权衡。
Redis:缓存的头号选手
你可能会问,为什么不用Memcached?或者干脆用本地缓存?这里我们要看的是点赞场景的核心诉求:高频读写+原子操作+数据结构丰富度。
Redis的优势太明显了:首先,它支持多种数据结构,Hash、Set、Sorted Set都能用上,不像 Memcached 只有简单的 Key-Value;
其次,Redis 的原子操作和 Lua 脚本支持,能完美解决并发下的状态判断和计数更新问题;
最后,它的持久化机制 RDB+AOF 虽然不是点赞场景的核心需求,但关键时刻能避免缓存全丢。
当然Redis也有缺点,比如内存成本比磁盘高,所以缓存策略必须做好,不能什么数据都往里塞。
消息队列:Kafka 还是 RabbitMQ?
异步化是解决高并发的万能钥匙,而消息队列就是异步化的核心工具。点赞系统里,点赞数更新和数据持久化必须解耦,毕竟用户点击点赞后,如果等数据库写完才返回,延迟肯定爆炸。
Kafka和RabbitMQ怎么选?
如果你的平台用户量极大,比如日活过亿,点赞消息吞吐量极高,每秒几万条,Kafka更合适,因为它的吞吐量和持久化能力更强;
如果是中小规模平台,RabbitMQ的易用性和丰富的路由策略更友好。比如通过死信队列处理失败消息,不用额外开发适配逻辑。
这里要注意,消息队列不是银弹,它会引入数据一致性风险。所以必须开启消息确认机制,并且定期对账,修复不一致的数据。
数据库:最后一道安全锁
既然 Redis 已经存了点赞数,为什么还要MySQL?这里我们要想的是数据的最终归宿。Redis是缓存,可能会丢数据,比如因为宕机导致数据没持久化,而用户的点赞记录是核心数据,必须持久化存储。
MySQL的作用就是存全量点赞记录:谁在什么时候给什么内容点了赞。这些数据不仅是为了恢复缓存,还能支撑后续的业务需求,比如拉取“我的点赞”列表、按点赞时间排序的评论等。
设计MySQL表时,要注意索引优化,比如给 user_id 和 content_id 建联合唯一索引,防止重复点赞:
CREATE TABLE `like_records` (
`id` bigint(20) NOTNULL AUTO_INCREMENT COMMENT '主键',
`user_id` bigint(20) NOTNULL COMMENT '用户ID',
`content_id` bigint(20) NOTNULL COMMENT '内容ID(如动态ID、评论ID)',
`content_type` varchar(20) NOTNULL COMMENT '内容类型(区分动态、评论等)',
`created_at` datetime NOTNULLDEFAULTCURRENT_TIMESTAMP COMMENT '点赞时间',
`updated_at` datetime NOTNULLDEFAULTCURRENT_TIMESTAMPONUPDATECURRENT_TIMESTAMP COMMENT '更新时间(取消点赞时更新)',
`is_canceled` tinyint(1) NOTNULLDEFAULT0 COMMENT '是否取消点赞(0-正常,1-取消)',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_user_content` (`user_id`,`content_id`,`content_type`) COMMENT '防止重复点赞',
KEY `idx_content` (`content_id`,`content_type`) COMMENT '按内容查点赞记录'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户点赞记录表';这个表结构里,uk_user_content 唯一索引能防止用户对同一内容重复点赞,idx_content 索引方便查询某内容的所有点赞记录。
架构拆解
需求和技术选型都明确了,接下来我们把这些组件拼成一个完整的架构。一个典型的高并发点赞系统,从用户点击到数据落地,要经过4层逻辑处理:
前端层:关键是用户体验
前端层的核心目标是提升用户体验,哪怕后端还在处理,也要让用户感觉很快。用户点击点赞按钮后,前端要立即做两件事:
- 本地反馈:按钮颜色立即变化,比如从灰色变成红色,然后显示“点赞+1”的动画,让用户直观感受到操作已生效;

- 异步请求:在动画播放的同时,悄悄发请求给后端,这时候即使后端稍微慢一点,用户也不会觉得卡顿。

这里有个细节:如果网络不好,请求失败了怎么办?不能让用户白点赞。前端需要把失败的点赞请求存到本地 localStorage,等网络恢复后自动重试,同时给用户一个“点赞已缓存,稍后同步”的提示。
服务层:业务逻辑的中央处理器
服务层是点赞逻辑的核心,主要做三件事:
- 参数校验:检查用户是否登录(未登录不让点赞)、content_id是否合法;
- 状态判断:从 Redis 查询用户对该内容是否已点赞;
- 操作执行:如果未点赞,就调用Redis接口点赞+1;如果已点赞,就点赞-1,同时发送消息到消息队列 Kafka,接着异步同步到 MySQL。
服务层还要注意幂等性 — 如果用户快速点击两次点赞按钮,前端可能会发两个请求,这时候服务层要能识别重复请求,避免重复处理。可以用请求ID+Redis分布式锁来实现:每个请求生成一个唯一requestId,处理前先抢锁,抢到锁才处理,处理完释放锁。
缓存层:存储两类核心数据
Redis是点赞系统的“心脏”,要存两类关键数据:
第一类:用户点赞了谁
这类数据用 Hash 还是 Set 存?这得看业务需求。如果只需要知道 “用户是否点赞”,Set 足够了,简单高效。
// 点赞:将用户ID加入集合
Redis.sAdd("like:status:123", "456");
// 判断用户是否已点赞,true表示已点赞,false表示未点赞
Redis.sIsMember("like:status:123", "456");但如果需要更多信息,比如 “按点赞时间排序”,Set 就不够了,这时候 Hash 更合适,这样不仅能判断状态,还能获取所有点赞用户及时间。
// 点赞:记录用户ID和点赞时间戳
Redis.hSet("like:status:123", "456", "1629260800000");
// 判断用户是否已点赞,true表示已点赞,false表示未点赞
Redis.hExists("like:status:123", "456");
// 获取该内容的所有点赞记录(用户ID+时间戳)
Redis.hGetAll("like:status:123");第二类:内容点赞数
直接用String类型存,key是 like:count:{contentId},value是点赞数,每次点赞用 INCR,取消点赞用 DECR,简单高效。
这里要注意缓存过期策略:比如点赞数超过10万的热点内容,缓存过期时间设置24小时;冷内容的缓存过期时间设置 1 小时,这样既能保证热点内容的缓存命中率,又能省内存。
持久层:数据落地的异步管道
持久层的核心是“异步化”,缓存更新后,不能忘了同步到数据库。但同步不能阻塞前端请求,所以用消息队列异步处理:
服务层更新Redis后,发一条类似“用户2001点赞了内容1001”的消息到Kafka,然后立即返回给前端。这样用户点完赞马上有反馈,主流程完全不卡。
后台启动一个消费者服务,从Kafka拉取消息,一条条处理:
如果是新点赞,就往数据库里插一条记录,带上用户 ID、内容 ID 和点赞时间;
如果是取消点赞,也不直接删记录,而是把is_canceled 字段改成 1,做个逻辑删除。
逻辑删除有两个好处:一是用户反复横跳,取消又点赞时,直接更新 is_canceled 为 0 比删了重插更高效;二是所有记录都留着,方便后续分析用户的点赞习惯、内容的互动热度。
关键细节:这些坑踩过才懂
架构搭好后,就到填坑环节了。点赞系统有两个经典的“坑”,必须提前规避:
Lua 脚本:并发操作的原子性保障
你可能会问,为什么一定要用Lua脚本?直接在代码里先查状态,再改计数不行吗?
还真不行。假设有个内容的点赞数是 1,现在 A 和 B 两个用户同时点了"取消点赞":
步骤 1:A查状态(已点赞),准备取消,执行计数 -1
步骤 2:B查状态(已点赞),准备取消,执行计数 -1
步骤 3:A先改计数(点赞-1),此时状态变为未点赞
步骤 4:B没察觉到状态变了,还是执行计数(点赞-1),最后点赞数就从 0 变成了 -1,显然不对
这就是并发脏数据,因为“查状态”和“改计数”是两个独立操作,中间可能被其他请求打断。而Lua脚本能解决这个问题,它会把这两个操作打包成一个原子操作,Redis 执行时会一口气跑完整个脚本,中间不会被打断,完美解决并发问题。
Lua 脚本示例:
-- 第一步:查用户是否已点赞
local isLiked = redis.call('HEXISTS', 'like:status:123', '456')
-- 第二步:只有已点赞,才执行取消操作(删状态+减计数)
if isLiked == 1 then
redis.call('HDEL', 'like:status:123', '456')
redis.call('DECR', 'like:count:123')
end
return 1缓存过期策略
Redis 缓存不能永久有效,否则内存会爆。但点赞数缓存过期了怎么办?直接删了让用户查数据库?那延迟就太高了。
可以这样设计缓存过期策略:
主动刷新:当用户查看某内容时,如果缓存不存在,就从 MySQL 查最新点赞数,同时更新到 Redis 并设置过期时间。
定期更新:给点赞数缓存设置一个较长的过期时间,比如24小时,同时用定时任务把Redis的点赞数同步到MySQL,并刷新缓存过期时间。这样既能保证缓存不过期,又能定期持久化数据。
缓存预热:新内容发布后,主动在Redis初始化点赞计数为0,避免第一次访问缓存miss。
这里要注意“缓存穿透”:如果有恶意用户不断请求不存在的 contentId,会导致大量请求穿透到 MySQL,给数据库带来没必要的压力,怎么解决呢?
有两个简单办法:
- 用布隆过滤器提前过滤掉那些肯定不存在的 contentId,让它们连 Redis 都到不了;

- 就算查询结果是空的,也把它缓存起来。比如查 contentId 为 999999 的点赞数,发现这内容根本不存在,但是也把这个键缓存起来,值设为 0,过期时间设短点 5 分钟。这样下次再有人查这个不存在的 contentId,也会直接从 Redis 拿结果。

优化技巧:从能用到好用
系统能跑起来不算本事,能扛住更大流量、更稳才算。点赞系统可以从这三个方向进一步提升性能和可靠性:
分级缓存:本地缓存 + Redis
如果你的平台用户量极大,比如日活过亿,即使Redis性能再强,每秒几十万的请求也会让它压力山大。这时候可以加一层本地缓存,比如Java的Caffeine、Go的 sync.Map,形成「本地缓存 + Redis」的二级缓存架构。
具体怎么做?把热门内容的点赞数缓存到应用服务器的本地缓存,用户请求先查本地缓存:
如果命中,直接返回,不查 Redis,减少 Redis 压力;
如果没命中,再查Redis,同时把结果更新到本地缓存,设置较短的过期时间,比如1分钟,避免本地缓存和Redis不一致。
有人可能担心热门内容点赞数变太快,本地缓存会不会不准?其实问题不大,就算点赞数在变,用户看到的也只是 “1 分钟内的近似值”,社交场景下这种延迟用户基本无感;但这能帮 Redis 挡掉 80% 以上请求,大大减轻它的压力。
当然,本地缓存也有缺点:每个应用实例的缓存是独立的,可能出现多实例数据不一致;而且服务器内存有限,只能存真正的热门内容。但对高并发场景来说,这样的取舍很值得。
读写分离:MySQL 主从架构
当点赞记录积累到千万甚至上亿条时,MySQL的查询压力会越来越大 — 比如用户查看“我的点赞列表”,需要从like_records 表查 userId=xxx 的所有记录,这时候单表查询会很慢。
解决方案是“读写分离+分库分表”:
读写分离:主库负责写入点赞记录,从库负责查询点赞列表、统计点赞数,把查询压力分散到从库;
分库分表:按content_id或userId哈希分表,比如分成16个表,like_records_0 到 like_records_15,查询时根据 content_id%16路由到对应表,减少单表数据量。
分表后的查询SQL示例(按content_id分表):
-- 查询contentId=12345的点赞记录(假设分16表,12345%16=9,路由到like_records_9)
SELECT user_id, created_at FROM like_records_9
WHERE content_id = 12345 AND is_canceled = 0
ORDER BY created_at DESC LIMIT 20;这里要注意,分库分表会增加架构复杂度,比如跨表联合查询、分布式事务等。所以中小规模平台可以先不着急分表,等单表数据量超过 1000 万再考虑。
限流熔断:突发流量下的系统保护罩
即使做了这么多优化,也架不住出现极端场景 — 比如某顶流明星官宣恋情,瞬间几千万用户同时点赞,这时候系统很可能被打垮。这时候就需要“限流熔断”来保护系统。
限流:限制单位时间内的请求数,比如某接口每秒最多处理1万次请求,超过的请求直接拒绝或排队。实现方式可以用 Redis+Lua 脚本做分布式限流,比如令牌桶算法,或者用 Sentinel、Hystrix 等限流组件。
熔断:当 Redis 或 MySQL 出现故障时,暂时熔断对这些组件的访问,直接返回降级结果。比如Redis宕机了,点赞接口可以暂时返回“点赞成功,稍后显示”,并把点赞请求缓存到本地,等Redis恢复后再同步。
限流熔断的核心思想是“先保系统,再保体验” — 宁可让部分用户暂时点赞失败,也不能让整个系统崩溃。
结语
最后牛哥想说,做系统设计别盯着技术栈看,而是要盯着用户的需求看:
用户要「快」,所以做前端异步+本地反馈;
用户要「准」,所以用Redis原子操作+Lua脚本;
系统要「稳」,所以用消息队列削峰+限流熔断;
成本要「省」,所以用Redis+MySQL分层存储。
其实不仅是点赞系统,所有高并发系统的设计思路都是一样的:
别追求最先进的技术,要追求最适合业务的技术。能用简单方案解决的问题,别搞复杂,毕竟稳定比什么都重要。
