点赞系统设计:从0到1拆解高并发场景的技术方案

点赞系统设计:从0到1拆解高并发场景的技术方案

点赞系统设计:从0到1拆解高并发场景的技术方案开篇:为什么点赞系统,成了社交平台的“生死线”?你有没有想过,一个小小的点赞按钮背后藏着多少技术门道?在这个注意力比黄金还宝贵的时代,用户对交互延迟的容忍度低到了极点——3秒打不开的点赞按钮,90%的用户会直接划走。这可不是夸张,社交平台的核心就是用户互动,而点赞作为最低成本的互动方式,其体验直接决定了用户留存。

普通点赞和“百万用户同时点赞”的差距,就像自行车和高铁的区别。前者可能一个单机数据库就能搞定,后者却需要一整套分布式架构支撑。比如某明星官宣动态,瞬间涌入的点赞请求能达到每秒数十万次,这时候系统如果扛不住,不仅会丢数据,更会让用户觉得“平台不行”。所以今天我们就来拆解,如何设计一个既快又稳还省钱的点赞系统。

需求拆解:高并发点赞系统要满足哪些“硬需求”?分析场景题的第一步,永远是把模糊的需求变成清晰的技术指标。点赞系统看起来简单,但要做好,这四个核心需求必须先想清楚:

快!—— 响应延迟是生命线 用户点击点赞按钮后,必须“秒响应”。这里我们想看的是端到端延迟,从用户点击到界面反馈,超过500ms用户就会感知到卡顿。你可能会说,500ms很宽松啊?但实际场景中,一次点赞要经过前端请求、网络传输、服务处理、缓存操作、数据库同步等多环节,每个环节都可能出幺蛾子,所以必须把目标定在200ms以内。

准!—— 数据一致性不能打折扣 10万次点赞少算1个,用户可能不会发现;但如果重复统计,比如同一个用户点了两次赞却计了两次数,就会引发投诉。这里的“准”不是强一致性,而是“最终一致+不丢不重”——用户点赞后,即使数据还没同步到数据库,至少缓存里要是对的;取消点赞时,也不能因为并发操作导致计数没减。

稳!—— 流量洪峰下的系统韧性 春运级并发是什么概念?假设一条热门内容在5分钟内获得100万点赞,平均每秒就是3333次请求,高峰期可能达到每秒1万次。这时候系统不能崩,不能丢数据,甚至不能出现明显的延迟抖动。要知道,点赞请求往往是突发的、无规律的,这对系统的弹性能力是极大考验。

省!—— 成本控制是工程师的基本素养 存千万条点赞记录,每月服务器账单不能爆表。这里的“省”不是偷工减料,而是合理利用资源:热点数据放缓存,冷数据归档,存储结构按需选择。比如用户点赞状态和点赞数,前者是高频读写,后者是高频读低频写,存储策略就得不一样。

技术选型:这3个“核心工具”缺一不可明确了需求,接下来要考虑的就是用什么技术栈支撑。点赞系统的技术选型,本质是对“高频读写”“数据一致性”“成本控制”这三个核心矛盾的权衡。

缓存选型:Redis为什么是“头号选手”?你可能会问,为什么不用Memcached?或者干脆用本地缓存?这里我们要看的是点赞场景的核心诉求:高频读写+原子操作+数据结构丰富度。

Redis的优势太明显了:首先,它支持多种数据结构,Hash、Set、Sorted Set都能用上,不像Memcached只有简单的Key-Value;其次,Redis的原子操作(比如INCR、HSETNX)和Lua脚本支持,能完美解决并发下的状态判断和计数更新问题;最后,它的持久化机制(RDB+AOF)虽然不是点赞场景的核心需求,但关键时刻能避免缓存全丢。

当然Redis也有缺点,比如内存成本比磁盘高,所以缓存策略必须做好,不能什么数据都往里塞。但对点赞这种“读多写多但数据量相对可控”的场景,Redis就是最优解。

消息队列:Kafka/RabbitMQ怎么选?异步化是解决高并发的万能钥匙,而消息队列就是异步化的核心工具。点赞系统里,“点赞数更新”和“数据持久化”必须解耦——用户点击点赞后,不能等数据库写完才返回,否则延迟肯定爆炸。

Kafka和RabbitMQ怎么选?如果你的平台用户量极大,比如日活过亿,点赞消息吞吐量极高(每秒几万条),Kafka更合适,它的吞吐量和持久化能力更强;如果是中小规模平台,RabbitMQ的易用性和丰富的路由策略(比如死信队列处理失败消息)更友好。

这里要注意,消息队列不是“银弹”,它会引入数据一致性风险——消息丢了怎么办?所以必须开启消息确认机制,并且定期对账,比如每天凌晨比对Redis和MySQL的点赞数,修复不一致的数据。

数据库:MySQL为什么是“最后一道安全锁”?有人可能会说,既然Redis已经存了点赞数,为什么还要MySQL?这里我们要想的是数据的“最终归宿”。Redis是缓存,可能会丢数据(比如宕机没持久化),而用户的点赞记录是核心数据,必须持久化存储。

MySQL的作用就是存全量点赞记录:谁在什么时候给什么内容点了赞。这些数据不仅是为了恢复缓存,还能支撑后续的业务需求,比如“我的点赞”列表、按点赞时间排序的评论等。设计MySQL表时,要注意索引优化,比如给user_id和content_id建联合唯一索引,防止重复点赞:

CREATE TABLE `like_records` (

`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',

`user_id` bigint(20) NOT NULL COMMENT '用户ID',

`content_id` bigint(20) NOT NULL COMMENT '内容ID(如动态ID、评论ID)',

`content_type` varchar(20) NOT NULL COMMENT '内容类型(区分动态、评论等)',

`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '点赞时间',

`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间(取消点赞时更新)',

`is_canceled` tinyint(1) NOT NULL DEFAULT 0 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层逻辑”需求和技术选型都明确了,接下来我们把这些组件拼成一个完整的架构。一个典型的高并发点赞系统,从用户点击到数据落地,要经过4层逻辑处理:

前端层:用户感知的“第一扇窗”前端层的核心目标是提升用户体验,哪怕后端还在处理,也要让用户“感觉很快”。具体怎么做?

用户点击点赞按钮后,前端要立即做两件事:

本地反馈:按钮颜色立即变化(比如从灰色变成红色),显示“点赞+1”的动画,让用户直观感受到操作已生效;异步请求:在动画播放的同时,悄悄发请求给后端,这时候即使后端稍微慢一点,用户也不会觉得卡顿。这里有个细节:如果网络不好,请求失败了怎么办?不能让用户白点赞。前端需要把失败的点赞请求存到本地(比如localStorage),等网络恢复后自动重试,同时给用户一个“点赞已缓存,稍后同步”的提示。

服务层:业务逻辑的“中央处理器”服务层是点赞逻辑的核心,主要做三件事:

参数校验:检查用户是否登录(未登录用户不能点赞)、content_id是否合法;状态判断:查询用户对该内容是否已点赞(从Redis查);操作执行:如果未点赞,就调用Redis接口点赞+1;如果已点赞,就取消点赞-1,同时发送消息到消息队列,异步同步到MySQL。服务层的代码要尽可能轻量,核心逻辑用伪代码表示大概是这样:

// 点赞接口

public LikeResponse like(LikeRequest request) {

Long userId = request.getUserId();

Long contentId = request.getContentId();

String contentType = request.getContentType();

// 1. 参数校验

if (userId == null || contentId == null) {

return LikeResponse.fail("参数错误");

}

// 2. 调用Redis操作(通过Lua脚本保证原子性)

String luaScript = "local statusKey = 'like:status:'..KEYS[2] " +

"local countKey = 'like:count:'..KEYS[2] " +

"local isLiked = redis.call('HEXISTS', statusKey, KEYS[1]) " +

"if isLiked == 1 then " +

" redis.call('HDEL', statusKey, KEYS[1]) " +

" redis.call('DECR', countKey) " +

" return 0 " + // 取消点赞

"else " +

" redis.call('HSET', statusKey, KEYS[1], ARGV[1]) " + // ARGV[1]是时间戳

" redis.call('INCR', countKey) " +

" return 1 " + // 点赞成功

"end";

// 执行Lua脚本

Long result = redisTemplate.execute(

new DefaultRedisScript<>(luaScript, Long.class),

Arrays.asList(userId.toString(), contentId.toString()), // KEYS[1]=userId, KEYS[2]=contentId

System.currentTimeMillis() + "" // ARGV[1]=时间戳

);

// 3. 发送消息到消息队列(异步同步到MySQL)

LikeMessage message = new LikeMessage(userId, contentId, contentType, result == 1 ? 1 : 0); // 1-点赞,0-取消

rabbitTemplate.convertAndSend("like-exchange", "like.key", message);

// 4. 返回结果

return LikeResponse.success(result == 1 ? "点赞成功" : "取消点赞成功");

}服务层还要注意幂等性——如果用户快速点击两次点赞按钮,前端可能会发两个请求,这时候服务层要能识别重复请求,避免重复处理。可以用请求ID+Redis分布式锁来实现:每个请求生成一个唯一requestId,处理前先抢锁,抢到锁才处理,处理完释放锁。

缓存层:Redis存储的“两类核心数据”Redis是点赞系统的“心脏”,要存两类关键数据:

第一类:用户点赞状态(谁点赞了谁) 用Hash还是Set存?这得看业务需求。如果只需要知道“用户是否点赞”,Set足够了,比如like:status:{contentId}作为key,value是点赞用户ID的集合,判断是否点赞用SISMEMBER key userId,简单高效。

但如果需要更多信息,比如“按点赞时间排序”,Set就不够了,这时候Hash更合适:key是like:status:{contentId},field是userId,value是点赞时间戳。这样不仅能判断状态,还能通过HGETALL获取所有点赞用户及时间,方便做“最近点赞的人”这样的功能。

第二类:内容点赞数(某内容有多少赞) 这个很简单,用String类型存,key是like:count:{contentId},value是点赞数,每次点赞INCR,取消点赞DECR。

这里有个问题:热点内容的点赞数缓存怎么更新?如果某条内容特别火,每秒有几千次点赞,总不能每次都更新缓存吧?可以结合“定期更新+主动刷新”:

定期更新:用定时任务(比如每5分钟)把Redis的点赞数同步到MySQL,同时更新缓存的过期时间;主动刷新:当用户查看某内容时,如果缓存过期了,就从MySQL查最新点赞数,更新到Redis,这样用户看到的永远是最新数据。持久层:消息队列异步同步的“非阻塞设计”持久层的核心是“异步化”——服务层把点赞消息发给消息队列后,就立即返回,不阻塞主流程。消息消费者收到消息后,再异步写入MySQL。

消费者的代码逻辑大概是这样:

// 消息消费者

@Component

public class LikeMessageConsumer {

@Autowired

private LikeRecordMapper likeRecordMapper;

@RabbitListener(queues = "like.queue")

public void handleLikeMessage(LikeMessage message) {

Long userId = message.getUserId();

Long contentId = message.getContentId();

String contentType = message.getContentType();

int action = message.getAction(); // 1-点赞,0-取消

if (action == 1) {

// 新增点赞记录

LikeRecord record = new LikeRecord();

record.setUserId(userId);

record.setContentId(contentId);

record.setContentType(contentType);

record.setCreatedAt(new Date());

likeRecordMapper.insert(record);

} else {

// 取消点赞(逻辑删除,保留记录用于数据分析)

likeRecordMapper.updateCanceled(userId, contentId, contentType);

}

}

}这里要注意“逻辑删除”——取消点赞时,不要直接删除记录,而是把is_canceled字段设为1。因为用户可能会“取消后又点赞”,这时候直接更新is_canceled为0比删了重插更高效,而且保留完整记录方便数据分析(比如用户点赞习惯)。

关键细节:这些“坑”踩过才懂Redis数据结构选择:Hash vs Set的场景对比前面提到Hash和Set都能存点赞状态,具体怎么选?我们用一张表对比一下:

数据结构优点缺点适用场景Set占用空间小,操作简单(SADD/SREM/SISMEMBER)只能存用户ID,无法附加信息(如时间)只需要判断“是否点赞”,无排序需求Hash可存额外信息(如时间戳),支持HGETALL批量获取占用空间比Set大(每个field-value对占更多字节)需要按时间排序、显示点赞时间等场景举个例子:微博的“点赞”按钮只需要显示是否点赞,用Set足够;而小红书的“点赞列表”需要显示“XX、YY等100人觉得很赞”,并按时间排序,这时候必须用Hash存时间戳,才能实现排序功能。

Lua脚本:保证“查状态+改计数”原子性的“杀手锏”你可能会问,为什么一定要用Lua脚本?直接在代码里先查状态,再改计数不行吗?

还真不行。假设A和B两个用户同时给同一内容点赞,正常逻辑是:

A查状态:未点赞B查状态:未点赞A改计数:+1B改计数:+1结果本来应该+2,没问题。但如果是“取消点赞”场景:

A查状态:已点赞(准备取消)B查状态:已点赞(准备取消)A改计数:-1(此时状态变为未点赞)B改计数:-1(此时状态已经是未点赞,却又-1,导致少算)这就是并发脏数据!因为“查状态”和“改计数”是两个独立操作,中间可能被其他请求打断。而Lua脚本能把这两个操作打包成一个原子操作,Redis会单线程执行脚本,中间不会被打断,完美解决并发问题。

一个完整的点赞Lua脚本示例(前面服务层已提到,这里再细化一下):

-- 点赞/取消点赞的Lua脚本

-- KEYS[1]:userId,KEYS[2]:contentId

-- ARGV[1]:当前时间戳(点赞时用)

local statusKey = "like:status:" .. KEYS[2] -- 用户点赞状态的key

local countKey = "like:count:" .. KEYS[2] -- 内容点赞数的key

-- 1. 检查用户是否已点赞

local isLiked = redis.call("HEXISTS", statusKey, KEYS[1])

if isLiked == 1 then

-- 2. 已点赞,执行取消点赞

redis.call("HDEL", statusKey, KEYS[1]) -- 删除用户点赞状态

redis.call("DECR", countKey) -- 点赞数-1

return 0 -- 返回0表示取消成功

else

-- 3. 未点赞,执行点赞

redis.call("HSET", statusKey, KEYS[1], ARGV[1]) -- 记录用户点赞状态及时间

redis.call("INCR", countKey) -- 点赞数+1

return 1 -- 返回1表示点赞成功

end这个脚本把“查状态”和“改计数”绑在一起,确保原子性,不管多少并发请求,都不会出现脏数据。

缓存过期:点赞数缓存的“更新策略”Redis缓存不能永久有效,否则内存会爆。但点赞数缓存过期了怎么办?直接删了让用户查数据库?那延迟就太高了。

可以这样设计缓存过期策略:

缓存不过期,定期更新:给点赞数缓存设置一个较长的过期时间(比如24小时),同时用定时任务(比如每小时)把Redis的点赞数同步到MySQL,并刷新缓存过期时间。这样既能保证缓存不过期,又能定期持久化数据。主动刷新触发:当用户查看某内容时,如果缓存不存在(比如Redis重启后),就从MySQL查最新点赞数,更新到Redis,并设置过期时间。这样用户访问时能立即触发缓存刷新,避免冷启动问题。这里要注意“缓存穿透”——如果有恶意用户不断请求不存在的contentId,会导致大量请求穿透到MySQL。可以用布隆过滤器过滤不存在的contentId,或者对空结果也缓存(比如缓存like:count:999999为0,过期时间设短一点,比如5分钟)。

优化技巧:从“能用”到“好用”的3个升级分级缓存:本地缓存+Redis的“双重保险”如果你的平台用户量极大,比如日活过亿,即使Redis性能再强,每秒几十万的请求也会让它压力山大。这时候可以加一层本地缓存(比如Java的Caffeine、Go的sync.Map),形成“本地缓存+Redis”的二级缓存架构。

具体怎么做?把热门内容的点赞数(比如点赞数超过10万的内容)缓存到应用服务器的本地缓存,用户请求先查本地缓存:

如果命中,直接返回,不查Redis,减少Redis压力;如果没命中,再查Redis,同时把结果更新到本地缓存(设置较短的过期时间,比如1分钟,避免本地缓存和Redis不一致)。本地缓存的优点是速度极快(内存级访问,微秒级响应),缺点是缓存空间有限(应用服务器内存不能太大),而且多实例间缓存不一致。所以只适合存“热点且变化不频繁”的数据,点赞数正好符合这个特点——热门内容的点赞数虽然在变,但短时间内变化不大,本地缓存1分钟完全可以接受。

读写分离: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恢复后再同步。

限流熔断的核心思想是“先保系统,再保体验”——宁可让部分用户暂时点赞失败,也不能让整个系统崩溃。当然,降级策略要提前设计好,比如返回友好提示、本地缓存重试等,尽量减少对用户体验的影响。

扩展思考:点赞系统的“技术演进”与“场景延伸”从技术演进的角度看,点赞系统会随着用户量增长经历几个阶段:

初级阶段:单机MySQL,直接读写数据库,适合几万用户的小平台;成长阶段:Redis缓存+MySQL+消息队列,支撑百万级用户,满足高并发需求;成熟阶段:分级缓存+读写分离+分库分表+限流熔断,支撑亿级用户,保证系统稳定性。从场景延伸来看,点赞系统的设计思路可以复用在很多类似场景:收藏、关注、评论等,它们都是“高频读写+数据量大+一致性要求不高”的场景。比如关注系统,“用户关注博主”和“点赞”逻辑几乎一样,只需要把“点赞状态”换成“关注状态”,“点赞数”换成“粉丝数”,架构可以直接复用。

设计系统时,要始终结合业务规模、用户量、成本预算来做取舍——小平台用复杂架构是“过度设计”,大平台用简单架构是“能力不足”。希望这篇分析能帮你掌握场景题的拆解方法,下次遇到类似问题时,能胸有成竹地给出技术方案!

相关推荐

outlook邮件怎么备份到本地?(邮件保存到本地教程)
365体育提现多久到账

outlook邮件怎么备份到本地?(邮件保存到本地教程)

📅 10-11 👁️ 9841
“()()不断” 的成语
beat365体育亚洲

“()()不断” 的成语

📅 10-29 👁️ 4027
一键启动的钥匙孔在哪里?一键启动怎么用钥匙发动车子
如何优雅地放一个无声的屁?
beat365体育亚洲

如何优雅地放一个无声的屁?

📅 07-06 👁️ 7300
攻略《雷神之锤3:竞技场》秘籍
beat365体育亚洲

攻略《雷神之锤3:竞技场》秘籍

📅 07-21 👁️ 599
百分比增长计算器
365bet娱乐登录

百分比增长计算器

📅 09-07 👁️ 4862
微信群赚钱,群主必看!教你如何在微信群中赚到钱
手把手教你安装黑苹果之CLOVER配置篇(基础四)
beat365体育亚洲

手把手教你安装黑苹果之CLOVER配置篇(基础四)

📅 07-09 👁️ 3446
线下自提iphone要很准时吗
365体育提现多久到账

线下自提iphone要很准时吗

📅 09-17 👁️ 8198