面试官:高并发场景下,如何设计一个不崩的点赞系统?

面试官:高并发场景下,如何设计一个不崩的点赞系统?

这时候系统不能崩,不能丢数据,甚至不能出现明显的延迟抖动。要知道,点赞请求往往是突发的、无规律的,这对系统的弹性能力是极大考验。

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 建联合唯一索引,防止重复点赞:

CREATETABLE`like_records` (

`id` bigint(20) NOTNULLAUTO_INCREMENT COMMENT '主键',

`user_id` bigint(20) NOTNULLCOMMENT '用户ID',

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

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

`created_at` datetime NOTNULLDEFAULTCURRENT_TIMESTAMPCOMMENT '点赞时间',

`updated_at` datetime NOTNULLDEFAULTCURRENT_TIMESTAMPONUPDATECURRENT_TIMESTAMPCOMMENT '更新时间(取消点赞时更新)',

`is_canceled` tinyint(1) NOTNULLDEFAULT0COMMENT '是否取消点赞(0-正常,1-取消)',

PRIMARYKEY (`id`),

UNIQUEKEY `uk_user_content` (`user_id`,`content_id`,`content_type`) COMMENT '防止重复点赞',

KEY `idx_content` (`content_id`,`content_type`) COMMENT '按内容查点赞记录'

) ENGINE=InnoDB DEFAULTCHARSET=utf8mb4 COMMENT='用户点赞记录表';

这个表结构里, uk_user_content 唯一索引能防止用户对同一内容重复点赞, idx_content 索引方便查询某内容的所有点赞记录。

架构拆解

需求和技术选型都明确了,接下来我们把这些组件拼成一个完整的架构。一个典型的高并发点赞系统,从用户点击到数据落地,要经过4层逻辑处理:

前端层:关键是用户体验

前端层的核心目标是提升用户体验,哪怕后端还在处理,也要让用户感觉很快。用户点击点赞按钮后,前端要立即做两件事:

1. 本地反馈:按钮颜色立即变化,比如从灰色变成红色,然后显示“点赞+1”的动画,让用户直观感受到操作已生效;

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

这里有个细节:如果网络不好,请求失败了怎么办?不能让用户白点赞。前端需要把失败的点赞请求存到本地 localStorage,等网络恢复后自动重试,同时给用户一个“点赞已缓存,稍后同步”的提示。

服务层:业务逻辑的中央处理器

服务层是点赞逻辑的核心,主要做三件事:

1. 参数校验:检查用户是否登录(未登录不让点赞)、content_id是否合法;

2. 状态判断:从 Redis 查询用户对该内容是否已点赞;

3. 操作执行:如果未点赞,就调用Redis接口点赞+1;如果已点赞,就点赞-1,同时发送消息到消息队列 Kafka,接着异步同步到 MySQL。

缓存层 — 存储两类核心数据

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

第一类:用户点赞了谁

这类数据用 Hash 还是 Set 存?这得看业务需求。 如果只需要知道 “用户是否点赞”,Set 足够了,简单高效。

// 点赞:将用户ID加入集合

Redis.sAdd("like:status:123", "456");

// 判断用户是否已点赞,true表示已点赞,false表示未点赞

Redis.sIsMember("like:status:123", "456");

但如果需要更多信息, 比如“按点赞时间排序”,Set 就不够了,这时候 Hash 更合适,这样不仅能判断状态,还能获取所有点赞用户及时间。

// 点赞:记录用户ID和点赞时间戳

// 判断用户是否已点赞,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 脚本示例:

-- 第一步:查用户是否已点赞

localisLiked = redis.call('HEXISTS', 'like:status:123', '456')

-- 第二步:只有已点赞,才执行取消操作(删状态+减计数)

ifisLiked == 1then

redis.call('HDEL', 'like:status:123', '456')

redis.call('DECR', 'like:count:123')

end

return1

缓存过期策略

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

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

主动刷新:当用户查看某内容时,如果缓存不存在,就从 MySQL 查最新点赞数,同时更新到 Redis 并设置过期时间。

定期更新:给点赞数缓存设置一个较长的过期时间,比如24小时,同时用定时任务把Redis的点赞数同步到MySQL,并刷新缓存过期时间。这样既能保证缓存不过期,又能定期持久化数据。

缓存预热:新内容发布后,主动在Redis初始化点赞计数为0,避免第一次访问缓存miss。

这里要注意“缓存穿透”:如果有恶意用户不断请求不存在的 contentId,会导致大量请求穿透到 MySQL,给数据库带来没必要的压力,怎么解决呢?

有两个简单办法:

一是用布隆过滤器提前过滤掉那些肯定不存在的 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)

SELECTuser_id, created_at FROMlike_records_9

WHEREcontent_id =12345ANDis_canceled =0

ORDERBYcreated_at DESCLIMIT 20;

这里要注意,分库分表会增加架构复杂度,比如跨表联合查询、分布式事务等。所以中小规模平台可以先不着急分表,等单表数据量超过 1000 万再考虑。

限流熔断:突发流量下的系统保护罩

即使做了这么多优化,也架不住出现极端场景 — 比如某顶流明星官宣恋情,瞬间几千万用户同时点赞,这时候系统很可能被打垮。这时候就需要“限流熔断”来保护系统。

限流:限制单位时间内的请求数,比如某接口每秒最多处理1万次请求,超过的请求直接拒绝或排队。实现方式可以用 Redis+Lua 脚本做分布式限流,比如令牌桶算法,或者用 Sentinel、Hystrix 等限流组件。

熔断:当 Redis 或 MySQL 出现故障时,暂时熔断对这些组件的访问,直接返回降级结果。比如Redis宕机了,点赞接口可以暂时返回“点赞成功,稍后显示”,并把点赞请求缓存到本地,等Redis恢复后再同步。

限流熔断的核心思想是“先保系统,再保体验” — 宁可让部分用户暂时点赞失败,也不能让整个系统崩溃。

结语

最后牛哥想说,做系统设计别盯着技术栈看,而是要盯着用户的需求看:

用户要「快」,所以做 前端异步+本地反馈;

用户要「准」,所以用 Redis原子操作+Lua脚本;

系统要「稳」,所以用 消息队列削峰+限流熔断;

成本要「省」,所以用 Redis+MySQL分层存储。

其实不仅是点赞系统,所有高并发系统的设计思路都是一样的:

别追求最先进的技术,要追求最适合业务的技术。能用简单方案解决的问题,别搞复杂,毕竟稳定比什么都重要。返回搜狐,查看更多

🎭 相关推荐 🎭

江湖X全门派加入地点方法 各门派武学介绍
KRW是什么货币,什么意思
bt365体育

KRW是什么货币,什么意思

📅 09-26 👀 9250
世界杯重大失误!官方:葡萄牙点球是误判,黄健翔:VAR毁了足球