朋友圈点赞,看似简单实则藏着“技术坑”
朋友圈点赞,看似简单实则藏着“技术坑”
大家好,我是牛哥。
前阵子面一个社招同学,问他“如何设计朋友圈点赞功能”,他笑了:“这还不简单?用户点个赞,后端存条记录,前端显示个数呗。”我追问:“1000个人同时给一条朋友圈点赞,怎么保证不超算?共同好友点赞只展示彼此,怎么实现高效过滤?用户取消点赞后,点赞数要立刻减1,怎么避免并发问题?”他当场卡壳——这就是典型的“把简单功能想简单了”。
朋友圈点赞看似是“小功能”,实则是高并发+社交关系+实时性的综合考验:它要支撑“一条热门动态被10万人点赞”的高并发写入,要处理“只展示共同好友点赞”的社交关系计算,还要保证“点赞瞬间数更新、取消瞬间变灰”的实时体验。面试官爱问这个题,正是因为它能看出你“把小功能做扎实”的设计能力。
今天牛哥就带你拆解朋友圈点赞功能的设计思路:从“点击点赞按钮到数字+1”的全流程,到“共同好友过滤”的性能优化,再到“缓存与数据库一致性”的坑点处理,手把手教你把这个“小功能”做出大厂水准。
功能拆解:一个完整的点赞功能要包含这些
拿到“朋友圈点赞”需求,别急着写代码,先想清楚“用户到底需要什么”。一个完整的点赞功能,远不止“点一下加1”那么简单,至少要包含四大核心能力:
基础操作:点赞/取消点赞的“无缝切换”
用户点第一次是“点赞”(按钮变红,数字+1),点第二次是“取消点赞”(按钮变灰,数字-1),这个切换要瞬间完成——不能点了赞等2秒数字才变,更不能出现“点了取消但数字没减”的尴尬。这背后需要解决“快速判断用户是否已点赞”“点赞状态实时同步”两个问题。
实时展示:数字变、头像出,体验要“跟手”
点赞后,不仅要点赞数实时更新,还要显示“谁赞了”——比如显示前3个点赞人的头像,hover时展开完整列表。这里的关键是**“数据实时+展示高效”**:数字不能延迟,头像列表不能卡顿(尤其当点赞人数超过100时)。
社交互动:只展示共同好友,隐私保护是底线
朋友圈的核心是“社交关系”,点赞列表也得体现这一点:你刷到好友的动态,点赞列表里只能看到“你和好友的共同好友”,非共同好友会被隐藏(比如你好友发了条动态,他的同事点赞了,但你不认识他同事,就看不到这个同事的点赞)。这个“共同好友过滤”功能,直接关系到用户隐私,实现时既要准确又要高效(总不能每次加载点赞列表都查一遍你的好友关系吧?)。
消息通知:被赞了要“即时提醒”
用户发完动态后,有人点赞得收到通知:“XX赞了你的朋友圈”。通知要及时但不骚扰——不能延迟半小时才收到,也不能同一时间多条点赞通知刷屏(得合并提醒,比如“XX、YY和另外3人赞了你的朋友圈”)。
技术选型:这3个工具足够应对
需求明确后,该选“趁手的工具”了。朋友圈点赞功能的技术选型,核心要解决“高并发读写”“社交关系计算”“实时通知”三大问题。牛哥帮你拆解3个核心工具,以及为什么非它们不可:
Redis:高并发场景的“点赞状态机”
为什么选Redis? 因为点赞场景的核心诉求是“高频读写+快速判断”,而Redis的Set(集合) 和Sorted Set(有序集合) 简直是为点赞量身定做的:
- 判断“是否已点赞”:用Set存“某动态的点赞用户ID”(Key:
like:moment:{动态ID},Value:用户ID集合),通过SISMEMBER key userID判断用户是否点赞,O(1)复杂度,1秒能处理百万次查询; - 点赞列表按时间排序:用Sorted Set存“点赞用户ID+点赞时间戳”(Key:
like:moment:{动态ID}:sorted,Score:时间戳,Member:用户ID),ZREVRANGE按时间倒序取前N个用户,支持分页加载; - 点赞数实时更新:用
SCARD(Set的元素个数)或ZCARD(Sorted Set的元素个数)直接取点赞数,或者单独用一个Key(like:count:{动态ID})存计数,INCR/DECR原子操作更新,毫秒级响应。
反问一下:“为什么不用本地缓存(比如HashMap)存点赞状态?”——本地缓存只存在单台服务器,而点赞服务是集群部署的(多台服务器处理请求),用户A的点赞可能存在服务器1的本地缓存,用户B查询时访问服务器2,就会出现“点赞状态不一致”。Redis是分布式缓存,所有服务器共享数据,天然解决这个问题。
MySQL:点赞记录的“保险箱”
Redis虽好,但数据存在内存里,万一宕机可能丢失(比如Redis没开持久化,或持久化文件损坏)。所以需要MySQL持久化存储点赞记录,用于数据恢复和历史查询(比如“查看3个月前谁赞了我”)。
MySQL表设计很简单,核心字段:id(主键)、user_id(点赞用户ID)、moment_id(动态ID)、created_at(点赞时间)、status(点赞状态:1-点赞,0-取消)。为了加速查询,建联合索引idx_moment_user (moment_id, user_id)(查某动态的某用户点赞状态)和idx_user_moment (user_id, moment_id)(查某用户赞过的动态)。
消息队列:通知流程的“解耦器”
用户点赞后,发通知的流程(比如推送到被赞用户的消息列表、更新未读计数)如果同步执行,会阻塞“点赞-更新数”的主流程(用户点了赞要等通知发完才能看到数字+1,体验差)。这时候需要消息队列(如RabbitMQ/Kafka) 异步处理:
- 主流程:用户点赞→Redis更新状态和计数→返回成功;
- 异步流程:主流程成功后,往消息队列丢一条“点赞通知”消息,消费者(通知服务)异步拉取消息,处理“推送通知”“合并提醒”等非核心流程,不阻塞主流程。
核心设计:从点击到展示的全流程
选好工具后,该把“点赞从点击到展示”的全流程串起来了。牛哥画了张架构图,清晰展示数据怎么流、每个环节做什么:
点赞操作:从“点击”到“状态更新”的原子性保证
用户点击点赞按钮后,最核心的是“判断状态→更新状态→更新计数”这三步要原子执行,否则会出现“重复点赞”“计数不准”的问题。
- 前端防重复:用户点击后,按钮立刻置灰2秒(用
setTimeout实现),同时用防抖(debounce)处理快速点击,避免1秒内发3次请求; - 后端判断状态:点赞服务先查Redis的Set集合(
SISMEMBER like:moment:{动态ID} {用户ID}),如果返回1(已点赞),执行“取消点赞”;返回0(未点赞),执行“点赞”; - 原子更新状态+计数:用Redis的
MULTI事务或Lua脚本保证“状态更新”和“计数更新”原子性。比如点赞时,先SADD添加用户ID到Set,再INCR点赞数;取消时,先SREM移除用户ID,再DECR点赞数。
举个Lua脚本的例子(点赞操作):
-- KEYS[1]:Set集合Key(like:moment:123)
-- KEYS[2]:计数Key(like:count:123)
-- ARGV[1]:用户ID(user:456)
if redis.call('SISMEMBER', KEYS[1], ARGV[1]) == 0 then
redis.call('SADD', KEYS[1], ARGV[1])
redis.call('INCR', KEYS[2])
-- 同时添加到Sorted Set(记录点赞时间)
redis.call('ZADD', KEYS[1]..':sorted', ARGV[2], ARGV[1]) -- ARGV[2]是时间戳
return 1 -- 点赞成功
else
return 0 -- 已点赞,无需操作
end用Lua脚本的好处是“一次发送,批量执行”,避免网络往返导致的“判断和更新之间被其他请求插队”(比如两个请求同时判断“未点赞”,都执行点赞,导致重复点赞)。
计数更新:Redis实时+MySQL异步,平衡实时性与可靠性
点赞数要“实时更新”,但数据不能丢——这就需要**“Redis实时计数+MySQL异步持久化”**的组合策略:
- 实时计数:Redis的
INCR/DECR是原子操作,点赞数更新后立刻生效,前端能实时看到数字变化; - 异步持久化:点赞服务更新Redis后,发一条消息到消息队列(如“点赞事件:用户456赞了动态123,时间戳xxx”),数据持久化服务异步消费消息,往MySQL插入/更新记录(如果是取消点赞,就更新
status=0)。
为什么不实时写MySQL? 因为MySQL的写性能比Redis差100倍以上(Redis单节点每秒10万+写,MySQL单表每秒几千写),如果10万人同时点赞,直接写MySQL会导致“请求排队超时”,用户体验崩了。异步写MySQL虽然会有“短暂的数据不一致”(比如Redis计数100,MySQL还没同步完,显示98),但朋友圈点赞场景允许“最终一致”(5秒内同步完成即可),用户感知不到。
列表展示:共同好友过滤的“高效计算”
用户点开点赞列表,要显示“头像+昵称”,且只展示共同好友——这个“共同好友过滤”是性能难点,直接查“用户好友列表”和“点赞用户列表”的交集,数据量大时会很慢。
- 存储点赞用户+时间:用Redis Sorted Set存“点赞用户ID+时间戳”(Key:
like:moment:{动态ID}:sorted),ZREVRANGE 0 99取最近100个点赞用户(朋友圈点赞列表通常只显示前100,更多需分页加载); - 共同好友快速计算:社交关系服务提前把“用户的好友ID”缓存到Redis Set(Key:
friend:user:{用户ID}),拿到点赞用户列表后,用SINTER命令计算“点赞用户Set”和“好友Set”的交集,得到共同好友ID列表; - 头像昵称本地缓存:前端拿到共同好友ID后,先查本地缓存(用户基本信息,如头像、昵称),没缓存的再调用户服务查询,避免重复请求。
避坑指南:这些细节最容易出问题
点赞功能看似流程顺畅,但实际落地时,这些细节没处理好,很容易“上线崩”:
并发问题:Redis事务 vs Lua脚本,选哪个?
坑:用Redis的MULTI事务处理点赞时,可能出现“中间命令失败,部分执行”的情况。比如MULTI后先SADD添加用户ID,再INCR计数,结果INCR失败,导致“用户已在Set里,但计数没+1”,数据不一致。
解决方案:用Lua脚本替代MULTI事务。Lua脚本在Redis中是“单线程执行”的,要么全部成功,要么全部失败,天然保证原子性。而且Lua脚本可以包含复杂逻辑(比如判断、分支),比MULTI事务更灵活。
数据一致性:缓存与数据库的同步策略
坑:Redis计数更新后,消息队列异步同步到MySQL,如果消息队列丢消息,会导致“Redis显示点赞数100,MySQL只有90条记录”,数据永久不一致。
解决方案:“消息队列持久化+定时对账”双重保障:
- 消息队列持久化:Kafka/RabbitMQ开启消息持久化(消息写入磁盘),即使服务宕机,重启后消息不丢;
- 定时对账:每天凌晨跑定时任务(如XXL-Job),对比Redis计数和MySQL中
status=1的记录数,发现差异后以MySQL为准修复Redis计数(因为MySQL是持久化存储,数据更可靠)。
性能优化:点赞列表的分页加载与热点控制
坑:一条热门动态被10万人点赞,用户点开点赞列表时,ZREVRANGE 0 9999取前1万条,Redis会阻塞几十毫秒,拖慢接口响应。
解决方案:
- 分页加载:前端默认只加载前20条,用户滑动到底部时,再加载下20条(
ZREVRANGE 20 39),避免一次性返回过多数据; - 热点动态降级:对点赞数超10万的热点动态,点赞列表只显示前500条,超过部分提示“还有99500人点赞”,避免Redis处理超大Sorted Set的性能损耗。
总结:小功能的“大设计”思维
朋友圈点赞功能虽小,但设计时要兼顾“高并发、社交关系、实时性”三大核心,牛哥帮你提炼3个关键思维,面试时能说清这些,offer基本稳了:
1. “先缓存后持久”:实时性与可靠性的平衡
用Redis扛高并发读写,保证点赞状态和计数实时更新;用MySQL异步持久化,保证数据不丢。牺牲“强一致性”换“高可用”,这是互联网场景的典型取舍。
2. “原子操作+异步解耦”:性能优化的核心
点赞状态更新用Lua脚本保证原子性,避免并发问题;通知、持久化等非核心流程用消息队列异步处理,不阻塞主流程,让用户“点了就有反馈”。
3. “社交关系预计算”:复杂逻辑的前置处理
共同好友过滤的关键是“提前缓存用户好友列表”,用Redis Set的交集计算替代“查库关联”,把复杂计算前置,查询时只需O(1)操作,性能才能跟得上。
最后想说:大厂面试爱问“朋友圈点赞”这类小功能,不是考你会不会用Redis,而是看你能不能把“简单需求”拆解成“复杂场景”,用“工程化思维”把小功能做扎实——毕竟,能把点赞做好的人,做复杂系统也差不了。
下次面试官再问你,试试用这个思路答,保准让他眼前一亮!
