面试官:如何实现亿级点赞系统
(牛哥带你拆解亿级架构背后的门道)
一、为什么需要点赞系统?别只看表面热闹
兄弟们,每次刷短视频、看文章时,是不是随手就点个赞?但你们知道吗,这一个小小的 “红心”“拇指” 背后,藏着一套能扛住亿级请求的复杂系统。今天牛哥就从根上聊聊,为啥咱们非得花大力气搞点赞系统,以及亿级规模下到底难在哪儿。
1.1 业务需求界定:点赞不只是 “我喜欢”,更是业务的 “连接器”
很多人觉得点赞就是用户表达喜好,其实远不止这么简单。从业务角度看,点赞是平台和用户、用户和内容之间的关键连接器,核心诉求至少有三个:
第一,用户情感表达的 “轻量化出口”。比起写评论,点赞成本极低,用户一秒就能完成互动。就像你刷到一条搞笑视频,不用打字,点个赞就等于告诉平台 “我喜欢这类内容”,平台也能顺着这个信号给你推更多同类内容,形成正向循环。
第二,内容价值的 “可视化标尺”。一条内容有多少赞,直接决定了它的曝光量。比如某公众号文章,点赞过万大概率会被平台推荐到首页;某短视频点赞破百万,能直接冲上热门。对创作者来说,点赞数也是成就感的来源,能激励他们持续产出 —— 这可是平台留住创作者的关键。
第三,平台数据的 “核心资产”。点赞数据里藏着大秘密:用户喜欢什么类型的内容?哪个时间段互动率高?甚至能通过点赞行为判断用户画像。这些数据能帮平台优化算法、调整运营策略,比如给新用户推荐高赞内容,快速留住他们。
所以啊,别小看点赞这个功能,它可是支撑平台用户活跃、内容分发、商业变现的 “隐形支柱”。
1.2 亿级规模下的技术挑战:高并发、高可用、数据一致性,一个都跑不了
当用户量从 “万级” 涨到 “亿级”,点赞系统就从 “小打小闹” 变成了 “生死考验”。牛哥给你们掰扯掰扯这三大绕不开的挑战:
首先是高并发。比如某明星发了条动态,瞬间几十万用户同时点赞,请求像潮水一样涌过来。如果系统扛不住,要么用户点了没反应,要么直接崩了 —— 这体验谁敢用?去年某平台演唱会直播,就因为点赞请求突增,导致部分用户页面卡顿,事后还上了热搜,损失可不小。
然后是高可用。不管是凌晨 3 点还是春节峰值,点赞功能必须 “随叫随到”。要是系统宕机,用户点不了赞,创作者看不到数据,平台的内容分发都会受影响。更麻烦的是,亿级系统涉及的组件多,比如缓存、数据库、消息队列,任何一个环节出问题,都可能导致整个系统 “掉链子”。
最后是数据一致性。这是最容易踩坑的地方。比如用户明明点了赞,刷新后却显示 “未点赞”;或者内容的点赞数一会儿多 100,一会儿少 50—— 这种数据错乱会严重影响用户信任。为啥会这样?因为亿级请求下,数据要在缓存和数据库之间同步,还要处理网络延迟、服务故障等问题,想让数据 “纹丝不动”,难度堪比在狂风中稳住风筝。
1.3 系统设计的核心指标:吞吐量、响应延迟、容错率,三个硬指标说了算
要搞定上面的挑战,设计系统时必须盯着三个硬指标,就像开车要盯着速度表、油表一样:
第一个是吞吐量。简单说就是系统每秒能处理多少个点赞请求。亿级平台的吞吐量至少得是 “万级 / 秒”,比如某短视频平台峰值时,每秒能处理 50 万 + 点赞请求。要是吞吐量不够,请求就会排队,用户就得等 —— 现在的用户可没耐心,等 1 秒就可能划走。
第二个是响应延迟。就是用户点完赞,到页面显示 “已点赞” 的时间。牛哥告诉你们,这个时间必须控制在 100 毫秒以内!超过 200 毫秒,用户就会觉得 “卡了”。比如你点了赞,等了半秒才显示红心,是不是会下意识再点一下?这反而会增加系统负担。
第三个是容错率。就是系统出小问题时,能不能 “自愈”。比如某台缓存服务器挂了,其他服务器能不能立刻顶上?某条点赞数据同步失败,能不能自动重试?容错率低的系统,就像纸糊的房子,一点小风小雨就塌了;容错率高的系统,才能在亿级请求下 “稳如老狗”。
二、亿级点赞系统的技术选型:别瞎选,适合的才是最好的
聊完需求和挑战,接下来就是实打实的技术选型了。很多新手一上来就跟风用 “最火的技术”,结果踩了大坑。牛哥今天就带你们理性分析,存储、缓存、消息队列该怎么选。
2.1 存储层选型:关系型数据库 vs 非关系型数据库,别被 “潮流” 带偏
存储点赞数据,首先要选对 “仓库”。现在主流的就是关系型数据库(比如 MySQL)和非关系型数据库(比如 Redis、MongoDB),两者各有优劣,不能一概而论。
先说说关系型数据库(MySQL)。优点很明显:支持事务,能保证数据一致性;结构清晰,比如用 “用户 ID + 内容 ID” 当主键,能轻松查询用户是否点赞过某内容。但缺点也致命:亿级数据下,读写性能会急剧下降。比如你要查某条内容的点赞数,得从几百万行数据里统计,慢得像蜗牛;而且 MySQL 抗并发能力弱,每秒几千个请求就可能扛不住。
再看非关系型数据库。牛哥重点说两个常用的:
第一个是Redis。这玩意儿简直是为点赞系统而生的!它是内存数据库,读写速度极快,每秒能处理几十万请求;支持 String、Hash 等数据结构,比如用 String 存内容点赞数,用 Hash 存用户点赞状态(key 是内容 ID,field 是用户 ID,value 是 1 表示已点赞),操作起来又快又方便。但 Redis 也有缺点:内存成本高,亿级数据全存在内存里,花费不小;而且默认不支持事务(虽然有简单的事务功能,但不如 MySQL 完善),数据一致性需要额外处理。
第二个是MongoDB。它适合存非结构化数据,比如点赞的时间、用户的设备信息等。但 MongoDB 的读写速度比 Redis 慢,而且在高并发场景下,性能不如 Redis 稳定。
所以牛哥的建议是:点赞数、用户点赞状态这种实时数据用Redis 存,历史数据(比如用户过去一个月的点赞记录)用 分库分表的 MySQL 存 —— 两者搭配,既保证性能,又能存大量数据。
总结一下:别迷信 “非关系型数据库一定比关系型好”,亿级点赞系统的存储层,讲究 “分工合作”,让每个数据库干自己擅长的事。
2.2 缓存策略选型:本地缓存 vs 分布式缓存,搞懂场景再选
缓存是提升系统性能的 “神器”,但缓存也分两种:本地缓存和分布式缓存,选错了反而会添乱。
先看本地缓存。就是把数据存在应用服务器的内存里,比如用 Java 的 Guava Cache。优点是:读写速度极快(不用走网络),没有网络延迟;而且不用依赖外部服务,稳定性高。但缺点也很明显:不支持分布式。比如你有 10 台应用服务器,每台服务器的本地缓存都是独立的,用户在 A 服务器点赞后,B 服务器的缓存里没有这个数据,就会出现 “数据不一致” 的问题。而且本地缓存的容量有限,存不了太多数据。
再看分布式缓存,比如 Redis Cluster(Redis 集群)。优点是:支持分布式,所有应用服务器共享一份缓存数据,不会出现数据不一致;而且容量可以横向扩展,加几台 Redis 服务器就能存更多数据。但缺点是:需要走网络请求,比本地缓存慢一点(不过也就几毫秒,用户感知不到);而且依赖 Redis 集群的稳定性,如果集群出问题,缓存就用不了了。
那亿级点赞系统该怎么选?牛哥的建议是:“本地缓存 + 分布式缓存” 结合用。比如把高频访问的数据(比如热门内容的点赞数)存在本地缓存,减少对分布式缓存的请求;低频访问的数据存在分布式缓存,保证数据一致性。这样既提升了性能,又避免了分布式缓存的压力过大。
举个例子:某平台的热门视频,每秒有 10 万次访问,把它的点赞数存在本地缓存,每台应用服务器自己读自己的缓存,不用每次都问 Redis;而普通视频的点赞数,存在 Redis 集群里,保证用户在任何服务器上点赞,数据都能同步。
2.3 消息队列选型:Kafka vs RabbitMQ,异步处理就靠它
亿级点赞系统里,消息队列是 “解耦神器”。比如用户点赞后,需要更新点赞数、记录点赞日志、给创作者发通知 —— 这些操作如果同步做,会让用户等很久。用消息队列把这些操作异步化,用户点完赞立刻返回,后面的操作让消息队列慢慢处理,体验会好很多。
现在主流的消息队列有 Kafka 和 RabbitMQ,该怎么选?
先说说Kafka。优点是:吞吐量极高,每秒能处理几十万条消息,特别适合亿级高并发场景;而且持久化性能好,消息存在磁盘里,不会丢;还支持横向扩展,加几台服务器就能提升吞吐量。缺点是:灵活性不如 RabbitMQ,不支持复杂的路由规则;而且在消息可靠性方面,需要额外配置(比如开启副本),否则可能丢消息。
再看RabbitMQ。优点是:功能强大,支持多种路由规则(比如 direct、topic、fanout),能满足复杂的业务需求;而且消息可靠性高,默认支持消息确认机制,不会丢消息;还有完善的管理界面,方便运维。但缺点是:吞吐量不如 Kafka,每秒只能处理几万条消息,在亿级峰值场景下,可能会有点吃力;而且集群扩展比较复杂,不如 Kafka 方便。
所以牛哥的建议是:如果你的点赞系统是亿级高并发,而且业务逻辑相对简单(比如只需要异步更新点赞数、记录日志),选 Kafka;如果你的业务逻辑复杂(比如需要根据用户类型、内容类型,发送不同的通知),而且并发量没那么极端,选 RabbitMQ。
比如某短视频平台,峰值时每秒有 50 万次点赞,用 Kafka 来异步处理点赞请求,既能扛住高并发,又能保证消息不丢;而某公众号平台,点赞并发量没那么高,但需要给创作者发 “点赞通知”“粉丝增长通知” 等不同消息,用 RabbitMQ 的路由功能,就能轻松实现。
三、亿级点赞系统核心模块设计:把复杂问题拆成 “小积木”
聊完技术选型,接下来就到了最关键的 “搭架子” 环节 —— 核心模块设计。很多兄弟一听到 “亿级系统” 就头大,觉得肯定特别复杂。其实不然,牛哥一直说,再大的系统都是由一个个小模块拼出来的。今天咱们就把亿级点赞系统拆成 4 个核心模块,逐个拆解,让你一看就懂。
3.1 点赞交互模块:前端请求与后端接口设计(只展开核心接口)
用户点下 “红心” 的那一刻,背后其实是前端和后端的一次 “快速对话”。这个模块的核心,就是让这次 “对话” 又快又稳,咱们重点说两个核心接口:点赞 / 取消点赞接口和点赞状态查询接口。
先看点赞 / 取消点赞接口。这是用户最常触发的操作,接口设计得好不好,直接影响用户体验。牛哥给你们说个经典的设计方案:用 POST 请求,接口路径设为/api/v1/like/toggle,请求参数只传三个关键信息 ——userId(用户 ID)、contentId(内容 ID)、likeType(点赞类型,比如 1 是点赞,0 是取消点赞)。为啥参数这么少?因为亿级系统讲究 “轻量化”,多余的参数会增加网络传输压力,还可能引发歧义。
后端处理这个接口时,有两个关键点要注意:一是参数校验,比如判断userId和contentId是否为空,likeType是不是合法值,避免无效请求打进来;二是快速响应,收到请求后,先更新 Redis 里的点赞状态和计数,再通过消息队列异步同步到数据库,然后立刻给前端返回 “操作成功”,别让用户等。举个例子:用户点了赞,后端 10 毫秒内就返回结果,前端马上把 “空心心” 变成 “红心”,用户根本感觉不到延迟 —— 这才是好体验。
再看点赞状态查询接口。用户打开一篇文章或一个视频,需要知道自己之前有没有点过赞,这就需要这个接口。设计方案也很简单:用 GET 请求,路径设为/api/v1/like/status,参数传userId和contentId。后端处理时,直接查 Redis 里的 Hash 结构(之前在存储选型里说过,用 Hash 存用户点赞状态),比如 key 是content:{contentId}:likes,field 是userId,如果能查到 value 是 1,就返回 “已点赞”,查不到就返回 “未点赞”。
这里有个优化小技巧:如果用户一次打开多个内容(比如短视频列表),别让前端发多次请求,而是设计一个批量查询接口/api/v1/like/status/batch,让前端传userId和contentIds(多个内容 ID 的列表),后端一次查完返回,这样能减少 80% 的接口调用量,大大减轻服务器压力。
3.2 数据存储模块:用户点赞状态与计数存储方案
数据存储是亿级点赞系统的 “地基”,地基打不稳,系统早晚要塌。这个模块要解决两个核心问题:一是实时存储用户点赞状态和内容点赞数,二是长期存储历史点赞数据。咱们分两部分说。
3.2.1 实时计数存储:Redis 的 String/Hash 结构应用
实时数据要求 “读写快、查得准”,Redis 的 String 和 Hash 结构正好能满足这个需求。牛哥给你们拆解一下具体怎么用:
首先是内容点赞数存储,用 Redis 的 String 结构。key 的命名要规范,比如content:{contentId}:likeCount,value 就是点赞数,比如 10000。更新点赞数时,不用先查再改,直接用 Redis 的INCR(加 1)或DECR(减 1)命令,这两个命令是原子操作,能避免并发更新导致的计数错乱。比如 1000 个用户同时给一个内容点赞,INCR命令能保证点赞数从 10000 准确涨到 11000,不会出现少算或多算的情况。
然后是用户点赞状态存储,用 Redis 的 Hash 结构。key 设为content:{contentId}:likes,field 是userId,value 是 1(表示已点赞)。为啥不用 String?因为如果用 String,每个用户对每个内容的点赞状态都要存一个 key,比如user:{userId}:content:{contentId}:likeStatus,亿级用户的话,key 的数量会爆炸,既占内存又不好管理。而用 Hash,一个内容的所有用户点赞状态都存在一个 key 里,管理起来特别方便。比如要查用户 A 是否给内容 B 点过赞,直接用HGET content:B:likes A,1 毫秒内就能出结果;要取消点赞,就用HDEL content:B:likes A,操作也快。
这里还有个内存优化技巧:对于那些点赞数很少、长时间没人互动的 “冷内容”,可以定期把它们的 Redis 数据迁移到数据库,释放 Redis 内存,让 Redis 只存 “热内容” 的数据 —— 毕竟 Redis 内存金贵,得用在刀刃上。
3.2.2 历史点赞数据存储:分库分表与冷热数据分离
实时数据存在 Redis 里,但用户的历史点赞记录(比如去年点赞过的文章)不能总存在 Redis 里,一来内存不够,二来访问频率也低。这时候就需要 “分库分表 + 冷热数据分离” 的方案。
先说说分库分表。如果把所有用户的历史点赞数据存在一个 MySQL 数据库的一张表里,亿级数据量下,查一次要几秒甚至几十秒,根本没法用。所以要把大表拆成小表,常用的拆分方式是 “按用户 ID 哈希分表”。比如把用户 ID 对 1024 取模,得到 0-1023 的结果,对应 1024 张表,表名比如like_history_0、like_history_1……like_history_1023。用户 A 的 ID 哈希后是 100,就存在like_history_100表里;用户 B 的 ID 哈希后是 200,就存在like_history_200表里。这样每张表的数据量只有几百万,查询速度能提升几十倍。
分库的逻辑也一样,如果 1024 张表存在一个数据库里,数据库压力还是大,就把表分到多个数据库里,比如用 4 个数据库,每个数据库存 256 张表。这样既能分散表的压力,又能分散数据库的压力,一举两得。
再说说冷热数据分离。历史数据也分 “热” 和 “冷”:比如近 3 个月的点赞记录,用户可能会经常查看(比如 “我的点赞” 页面),这是 “热数据”,存在 MySQL 分库分表里,保证查询速度;而 3 个月前的点赞记录,用户很少查看,这是 “冷数据”,可以迁移到低成本的存储里,比如阿里云的 OSS、腾讯云的 COS,或者专门的归档数据库(比如 ClickHouse)。这样既能保证热数据的查询性能,又能降低存储成本 —— 要知道,亿级冷数据存在 MySQL 里,一年的存储费用可不是小数目。
3.3 异步处理模块:消息队列实现点赞请求解耦
如果用户点个赞,后端要同步更新 Redis、MySQL、统计数据、发通知,那请求处理时间会特别长,用户得等好几秒,体验肯定差。这时候就需要消息队列来 “解耦”,把同步操作变成异步操作,让用户 “秒等” 变 “秒回”。
3.3.1 异步写入数据库:避免同步操作阻塞请求
前面说过,用户点赞后,后端会先更新 Redis,然后怎么同步到 MySQL 呢?答案是用消息队列异步处理。具体流程是这样的:
用户触发点赞操作,后端更新完 Redis 后,生成一条 “点赞消息”,消息里包含userId、contentId、likeType、operateTime(操作时间)等信息;
把这条消息发送到消息队列的 “点赞数据同步” 主题(比如 Kafka 的like-data-sync-topic);
后端启动一个 “消费者” 服务,专门监听这个主题,收到消息后,再把数据写入 MySQL 分库分表;
如果写入失败,消费者会自动重试(比如重试 3 次),还失败的话,就把消息存入 “死信队列”,后续人工处理,保证数据不丢失。
这样做的好处是,用户不用等数据库写入完成就能收到反馈,请求处理时间从几百毫秒降到几十毫秒;而且就算数据库暂时出问题,消息还在队列里,等数据库恢复后再同步,不会影响用户操作。去年某平台做活动,点赞请求突增到平时的 10 倍,就是靠这个方案,既保证了用户体验,又没丢一条数据。
3.3.2 异步更新统计数据:用户总点赞数、内容点赞数
除了存储点赞记录,平台还需要统计一些数据,比如 “用户总点赞数”(用户 A 一共点了多少赞)、“内容分类点赞总数”(美食类内容一共获赞多少)。这些统计数据如果同步更新,会严重拖慢请求速度,所以也要用消息队列异步处理。
牛哥给你们举个 “用户总点赞数” 的例子,流程如下:
用户点赞后,后端除了发送 “数据同步消息”,还会发送一条 “统计更新消息” 到消息队列的 “like-statistics-topic”;
统计服务作为消费者,监听这个主题,收到消息后,根据userId找到对应的统计记录,更新 “用户总点赞数”(点赞就加 1,取消点赞就减 1);
统计数据可以存在 Redis 里(实时展示用)和 MySQL 里(长期存储用),比如 Redis 里用 String 存user:{userId}:totalLikeCount,MySQL 里有张user_statistics表专门存用户统计数据。
对于 “内容分类点赞总数” 这种更复杂的统计,还可以结合分布式计算框架(比如 Spark),每天凌晨用 Spark 批量计算前一天的分类点赞总数,再把结果更新到数据库 —— 毕竟这种统计数据不需要实时更新,每天更新一次完全够用,还能减轻实时系统的压力。
3.4 计数一致性模块:解决缓存与数据库数据不一致问题
这是亿级点赞系统的 “老大难” 问题:Redis 缓存和 MySQL 数据库的数据,怎么保证一致?比如 Redis 里某内容的点赞数是 10000,数据库里却是 9999,用户看到的和实际数据对不上,这就麻烦了。这个模块就是要解决这个问题,咱们分两部分说。
3.4.1 缓存更新策略:Cache-Aside、Write-Through、Write-Behind
市面上有三种常见的缓存更新策略,牛哥给你们分析一下,哪种最适合亿级点赞系统:
第一种是Cache-Aside 策略(旁路缓存策略)。这是最常用的策略,逻辑很简单:读数据时,先查缓存,缓存有就返回,没有就查数据库,然后把数据库的数据更新到缓存;写数据时,先更数据库,再删缓存(不是更缓存)。为啥是删缓存而不是更缓存?因为如果有多个请求同时写数据,直接更缓存可能导致数据错乱,而删缓存后,下一次读请求会从数据库把最新数据加载到缓存,能保证一致性。
亿级点赞系统里,写数据(点赞 / 取消点赞)时,就是用的这个策略:先通过消息队列异步更数据库,数据库更新完后,再删除 Redis 里对应的缓存 key(比如content:{contentId}:likeCount),下一次用户查点赞数时,就会从数据库加载最新数据到 Redis。这个策略的优点是简单可靠,缺点是会有 “缓存穿透” 的风险(比如删了缓存后,还没等读请求加载新数据,又有写请求删缓存),不过可以通过加分布式锁解决。
第二种是Write-Through 策略(写透策略)。逻辑是:写数据时,先更缓存,再更数据库,两个操作都成功才算完成。这种策略的优点是缓存和数据库强一致,缺点是写操作要等两个步骤完成,速度慢,不适合亿级高并发场景 —— 点赞请求每秒几十万,每个请求都要等缓存和数据库都更新完,系统根本扛不住。
第三种是Write-Behind 策略(写回策略)。逻辑是:写数据时,只更缓存,不立刻更数据库,而是等缓存数据过期或达到一定数量后,再批量更数据库。这种策略的优点是写速度快,缺点是数据一致性最差,万一缓存宕机,没同步到数据库的数据就丢了 —— 亿级点赞系统可不能冒这个险,所以这种策略基本不用。
总结一下:亿级点赞系统首选Cache-Aside 策略,既保证了一致性,又能应对高并发。
3.4.2 分布式锁:Redis/ZooKeeper 保障并发更新安全
就算用了 Cache-Aside 策略,还是会有并发问题。比如两个用户同时给一个内容点赞,都触发了 “更数据库 + 删缓存” 的操作,第一个操作更完数据库、删了缓存,第二个操作还没更数据库,就有一个读请求把第一个操作的数据库数据加载到缓存,然后第二个操作才更数据库、删缓存 —— 这时候缓存里的数据就比数据库少 1,出现不一致。
解决这个问题,就得用分布式锁。分布式锁能保证同一时间,只有一个请求能执行 “更数据库 + 删缓存” 的操作,其他请求得排队等锁释放。亿级点赞系统里,常用的分布式锁有两种:Redis 锁和 ZooKeeper 锁。
先说说Redis 锁。用 Redis 的SET NX EX命令就能实现,比如要给content:{contentId}加锁,就执行SET lock:content:{contentId} 1 NX EX 10,意思是:如果这个 key 不存在,就设置它的值为 1,过期时间 10 秒;如果 key 已存在,就返回失败(获取锁失败)。请求获取到锁后,执行更数据库和删缓存的操作,操作完再用DEL命令删锁。
这里有个关键点:锁一定要设过期时间,防止请求获取锁后挂了,锁一直不释放,导致其他请求永远拿不到锁。而且过期时间要设得合理,比如亿级点赞系统里,更数据库和删缓存的操作最多 1 秒,所以过期时间设 10 秒足够了。
再说说ZooKeeper 锁。ZooKeeper 是通过创建临时有序节点来实现分布式锁的,逻辑比 Redis 锁复杂一点:每个请求都在 ZooKeeper 的某个节点下创建一个临时有序节点,然后判断自己是不是第一个节点,如果是,就获取到锁;如果不是,就监听前一个节点,前一个节点删除(锁释放)后,自己再尝试获取锁。
ZooKeeper 锁的优点是可靠性更高,能保证 “公平锁”(按请求顺序拿锁),缺点是性能不如 Redis 锁,因为 ZooKeeper 的节点操作需要网络通信,速度比 Redis 慢。亿级点赞系统里,因为并发量极高,所以更常用 Redis 锁 —— 毕竟性能更重要,而且 Redis 锁只要设计得当,可靠性也能满足需求。
牛哥给你们个小提醒:用 Redis 锁时,一定要避免 “死锁” 和 “锁误删” 的问题。比如可以给锁的 value 设为请求的唯一 ID(比如 UUID),删锁时先判断 value 是不是自己的唯一 ID,再删锁,防止删了别人的锁;还可以用 Redis 的 Redisson 客户端,它已经封装好了分布式锁的实现,能自动处理过期时间和重试逻辑,不用自己造轮子。
四、亿级点赞系统的性能优化策略:让系统跑起来 “飞” 一般
前面把系统的 “架子” 搭好了,但亿级场景下,光有架子还不够,得让系统跑得又快又稳。这就像给汽车做改装,从发动机到轮胎都得优化。今天牛哥就从缓存、数据库、网络、代码四个维度,给你们说说亿级点赞系统的性能优化秘籍。
4.1 缓存优化:多级缓存、缓存预热与缓存穿透 / 击穿 / 雪崩防护
缓存是系统性能的 “加速器”,但用不好反而会拖后腿。亿级点赞系统的缓存优化,核心要做好三件事:多级缓存、缓存预热,还有防住 “三兄弟”—— 穿透、击穿、雪崩。
先说说多级缓存。很多兄弟以为缓存只有 Redis,其实不然,多级缓存才是亿级系统的标配。牛哥给你们说个经典的多级缓存架构:“本地缓存 + Redis 分布式缓存 + 数据库缓存”。比如用户查某热门视频的点赞数,先查应用服务器的本地缓存(比如 Guava Cache),本地有就直接返回,没有再查 Redis;Redis 没有再查数据库,查完后把数据同步到 Redis 和本地缓存。这样一来,90% 以上的请求都能在本地缓存或 Redis 里搞定,根本不用碰数据库 —— 数据库压力小了,系统自然跑得更快。
再看缓存预热。如果系统刚启动,缓存里啥数据都没有,大量请求会直接打到数据库,很容易把数据库压垮,这就是 “缓存冷启动” 问题。缓存预热就是在系统启动前,提前把热门数据加载到缓存里。比如某平台每天早上 8 点用户量会激增,就可以在凌晨 5 点,用脚本把前一天点赞数 Top1000 的内容数据,批量加载到 Redis 和本地缓存里。这样用户一上来,就能直接从缓存里拿数据,不会出现 “冷启动” 的尴尬。
最后是缓存穿透、击穿、雪崩防护。这三个问题是缓存的 “噩梦”,亿级系统必须防住:
缓存穿透:用户查不存在的数据(比如查一个不存在的 contentId 的点赞数),缓存和数据库都没有,请求会一直打数据库。防护方案:一是用 “布隆过滤器”,把所有存在的 contentId 存到布隆过滤器里,请求过来先过一遍,不存在的直接返回;二是对不存在的数据,也在缓存里存一个 “空值”,比如content:999999:likeCount设为 0,过期时间设 5 分钟,避免重复请求打数据库。
缓存击穿:某热门数据的缓存过期了,大量请求同时打向数据库,把数据库搞崩。防护方案:一是给热门数据的缓存设 “永不过期”,比如点赞数 Top10 的内容,缓存永远不失效,后台定期更新缓存数据;二是用 “互斥锁”,缓存过期后,只让一个请求去查数据库,其他请求排队等这个请求把数据更新到缓存后,再从缓存里拿数据。
缓存雪崩:大量缓存同时过期,或者 Redis 集群宕机,请求全打到数据库,导致数据库崩溃。防护方案:一是给缓存设置 “随机过期时间”,比如原本想设 1 小时过期,就改成 50-70 分钟随机过期,避免大量缓存同时失效;二是 Redis 集群做 “主从复制 + 哨兵”,主节点挂了,从节点能立刻顶上;三是给系统加 “限流”,就算缓存雪崩了,也能限制打向数据库的请求数量,给数据库留口气。
4.2 数据库优化:索引设计、SQL 优化、读写分离
数据库是系统的 “粮仓”,粮仓要是出问题,整个系统都得瘫痪。亿级点赞系统的数据库优化,重点在三个方面:索引设计、SQL 优化、读写分离。
先看索引设计。没有索引的数据库,查数据就像在字典里瞎翻,慢得要命。亿级点赞系统的数据库表,必须设计好索引。比如like_history分表,常用查询是 “按 userId 查点赞记录” 和 “按 contentId 查点赞记录”,就可以给userId和contentId分别建普通索引;如果要按 “userId+operateTime” 排序查,就建联合索引(userId, operateTime)。但要注意,索引不是越多越好,建太多索引会让写入速度变慢 —— 每张表的索引控制在 5 个以内最好。
再说说SQL 优化。就算有索引,写得烂的 SQL 也会让数据库变慢。亿级系统的 SQL,要做到 “简单高效”。比如查用户 A 近 3 个月的点赞记录,别写SELECT * FROM like_history_100 WHERE userId=123,而是只查需要的字段:SELECT contentId, operateTime FROM like_history_100 WHERE userId=123 AND operateTime >= '2024-01-01';别用OR,改用UNION,因为OR可能会让索引失效;分页查询别用LIMIT 100000, 10,而是用 “游标分页”,比如WHERE id > 100000 LIMIT 10,这样能避免数据库扫描大量数据。
最后是读写分离。亿级点赞系统里,读请求远多于写请求(比如查点赞数的请求是点赞请求的 10 倍以上),把读写请求分开处理,能大大提升数据库性能。方案就是 “一主多从”:主库负责写操作(比如存点赞记录),从库负责读操作(比如查历史点赞记录);主库的数据通过 “binlog” 同步到从库,保证从库数据和主库一致。比如某平台用 1 主 3 从的架构,主库每秒处理 1 万次写请求,3 个从库各处理 3 万次读请求,数据库压力一下就分散了。但要注意,主从同步会有延迟(一般几十毫秒),如果业务对数据实时性要求极高(比如查刚点赞的记录),就直接查主库,别查从库。
4.3 网络优化:接口合并、CDN 加速、协议优化(HTTP/2)
网络是系统的 “高速公路”,公路要是堵了,数据再快也传不过来。亿级点赞系统的网络优化,主要靠三件事:接口合并、CDN 加速、HTTP/2 协议优化。
先看接口合并。用户一次操作可能要调用多个接口,比如打开短视频列表,要查每个视频的点赞数和自己的点赞状态,要是一个视频发一个请求,10 个视频就发 10 个请求,网络开销很大。接口合并就是把多个小请求合并成一个大请求,比如设计一个/api/v1/like/batchGet接口,一次传 10 个 contentId,后端一次返回 10 个视频的点赞数和用户点赞状态。这样一来,请求数减少 90%,网络传输时间也缩短了 —— 用户体验会明显提升。
再说说CDN 加速。CDN 是 “内容分发网络”,就像在全国建了很多 “仓库”,把热门内容存在离用户最近的仓库里,用户不用跑到总仓库拿数据。亿级点赞系统里,很多静态资源(比如点赞按钮的图标、点赞成功的动画)可以用 CDN 加速。比如用户在广州,要加载点赞图标,不用从北京的服务器拿,直接从广州的 CDN 节点拿,加载速度从几百毫秒降到几十毫秒。甚至可以把热门内容的点赞数,通过 CDN 缓存起来,用户查的时候直接从 CDN 拿,不用碰后端服务 —— 这能大大减轻后端压力。
最后是HTTP/2 协议优化。现在很多系统还用 HTTP/1.1 协议,它有个缺点:一个连接一次只能传一个请求,想传多个请求得建多个连接,效率低。HTTP/2 支持 “多路复用”,一个连接能同时传多个请求,还能压缩请求头,减少网络传输量。比如用户同时查点赞数、评论数、分享数,HTTP/1.1 要建 3 个连接,HTTP/2 一个连接就搞定,传输时间能减少 30% 以上。亿级系统把协议换成 HTTP/2,网络性能会有质的提升 —— 现在主流的浏览器和服务器都支持 HTTP/2,改起来也不复杂。
4.4 代码优化:避免锁竞争、减少对象创建、异步编程
代码是系统的 “发动机”,发动机要是有毛病,系统再强也跑不快。亿级点赞系统的代码优化,要关注三个点:避免锁竞争、减少对象创建、异步编程。
先看避免锁竞争。多线程环境下,锁用不好会导致 “锁竞争”,线程都在等锁,系统效率会很低。比如后端更新点赞数时,要是用synchronized锁整个方法,大量线程会排队等锁。优化方案:一是用 “细粒度锁”,只锁需要保护的代码块,别锁整个方法;二是用 “无锁编程”,比如用 Redis 的原子命令INCR,不用自己加锁,Redis 内部会保证线程安全;三是用 “并发容器”,比如用ConcurrentHashMap存本地缓存,比用HashMap加锁效率高得多。
再说说减少对象创建。Java 里创建对象会占用内存,频繁创建对象还会导致 GC(垃圾回收)频繁,GC 时系统会卡顿。亿级系统里,每秒处理几十万请求,要是每个请求都创建一堆对象,GC 会把系统拖垮。优化方案:一是用 “对象池”,比如把频繁创建的对象(比如点赞请求的 DTO)存在对象池里,用完放回池里,下次再用,不用每次都新建;二是用 “基本类型” 代替 “包装类型”,比如用int代替Integer,减少对象创建;三是避免在循环里创建对象,比如循环处理 1000 条点赞数据,别在循环里新建对象,而是在循环外新建一个对象反复用。
最后是异步编程。同步编程里,一个请求要等所有操作都完成才能返回,比如点赞后要更新缓存、发消息、写日志,同步处理要等这些操作都做完,用户得等很久。异步编程就是把不影响用户体验的操作异步化,比如用 Java 的 CompletableFuture,点赞后先更新缓存,然后异步发消息、写日志,更新完缓存就给用户返回结果。这样用户不用等后面的操作,请求处理时间能减少 60% 以上。但要注意,异步操作要做好 “异常处理”,别因为异步操作报错,导致数据不一致。
五、亿级点赞系统的可用性保障:让系统 “打不垮、摔不坏”
亿级系统不仅要跑得快,还要 “扛造”—— 就算遇到突发情况,也不能轻易崩。这就像给系统穿上 “防弹衣”,可用性保障就是这件 “防弹衣”。今天牛哥从服务降级、熔断、容灾备份、监控告警四个方面,给你们说说怎么让亿级点赞系统 “打不垮、摔不坏”。
5.1 服务降级:非核心功能降级策略
当系统压力达到极限时,比如点赞请求突然涨到平时的 20 倍,数据库和 Redis 快扛不住了,这时候就得 “丢车保帅”—— 把非核心功能关掉,保证核心功能能用,这就是服务降级。
亿级点赞系统的降级策略,要先分清 “核心功能” 和 “非核心功能”。比如 “点赞 / 取消点赞”“查点赞数” 是核心功能,必须保住;“查用户近一年点赞记录”“点赞排行榜实时更新” 是非核心功能,可以降级。具体降级方案有三种:
功能降级:直接关掉非核心功能。比如系统压力大时,用户点 “我的点赞 - 近一年记录”,直接返回 “当前访问人数过多,请稍后再试”;点赞排行榜从 “实时更新” 改成 “每 10 分钟更新一次”,减少系统压力。
数据降级:返回简化的数据。比如查某内容的点赞列表,平时返回前 100 条,降级时只返回前 10 条;查用户总点赞数,平时返回精确值,降级时返回近似值(比如 “1.2 万 +”),不用查数据库算精确数。
限流降级:限制非核心功能的请求量。比如用 Sentinel 给 “查历史点赞记录” 接口设限流阈值,每秒最多处理 1000 个请求,超过的请求直接返回降级提示。这样能把有限的资源留给核心功能,保证核心功能能用。
但要注意,降级不能 “一刀切”,要做好 “灰度降级”—— 先在小部分用户里试降级,没问题再扩大范围;还要做好 “降级通知”,让用户知道为啥功能用不了,别让用户以为系统崩了。
5.2 服务熔断:避免故障扩散(Sentinel/Hystrix)
如果某个服务出故障了,比如消息队列挂了,大量请求会一直等着这个服务响应,很容易导致 “故障扩散”—— 一个服务崩了,把整个系统拖垮。服务熔断就是当某个服务故障时,立刻 “切断” 对这个服务的请求,避免故障扩散,就像电路里的保险丝,过载了就熔断,保护电器。
亿级点赞系统常用的熔断组件是 Sentinel 或 Hystrix,原理都差不多:给服务设置 “熔断阈值”,比如 10 秒内请求错误率超过 50%,就触发熔断。熔断后,再有请求过来,直接返回预设的 “熔断结果”,不用等故障服务响应。比如消息队列挂了,点赞后异步写数据库的请求会失败,当错误率超过阈值,就触发熔断,暂时不往消息队列发消息,而是把消息存在本地文件里,等消息队列恢复后,再把文件里的消息同步过去。这样一来,消息队列的故障不会影响用户点赞,也不会让大量请求堵塞,避免故障扩散。
熔断还有个 “恢复机制”—— 熔断后不是一直断着,而是过一段时间(比如 5 秒),放少量请求试试故障服务恢复没,如果恢复了,就慢慢恢复正常请求;没恢复就继续熔断。这样既保证了系统稳定,又能在服务恢复后及时恢复功能。
5.3 容灾备份:数据多副本存储与异地容灾
数据是系统的 “命脉”,要是数据丢了,系统再稳定也没用。容灾备份就是给数据 “上保险”,就算遇到极端情况(比如机房着火、地震),数据也丢不了,系统也能快速恢复。
亿级点赞系统的容灾备份,核心是 “数据多副本 + 异地容灾”:
数据多副本存储:重要数据至少存 3 个副本。比如 Redis 集群用 “主从复制 + 哨兵”,主节点存 1 个副本,从节点存 2 个副本,就算主节点挂了,从节点能立刻顶上,数据也不会丢;MySQL 用 “主从复制”,主库存 1 个副本,从库存 2 个副本,就算主库崩了,从库也有完整数据。
异地容灾:在不同地区建容灾机房。比如主机房在上海,在广州建一个容灾机房,上海机房的所有数据,实时同步到广州机房。如果上海机房遇到地震、洪水等灾难,数据全丢了,广州机房能立刻接管业务,用户根本感觉不到异常。比如某大厂的点赞系统,就是 “上海主机房 + 广州容灾机房”,去年上海疫情期间,主机房没法维护,切换到广州机房,系统零中断,数据零丢失。
但要注意,容灾备份不是越复杂越好,要平衡 “成本” 和 “可用性”。比如小平台不用搞异地容灾,先做好数据多副本;大平台必须搞异地容灾,避免极端情况导致数据丢失。
5.4 监控告警:关键指标监控与异常告警(Prometheus/Grafana)
系统出问题不可怕,可怕的是出了问题没人知道,直到用户投诉才发现。监控告警就是系统的 “千里眼” 和 “顺风耳”,能实时盯着系统状态,一有异常就立刻通知运维人员,让问题在萌芽阶段就被解决。
亿级点赞系统的监控告警,要盯着 “四大类关键指标”,常用的监控工具是 Prometheus+Grafana:
业务指标:点赞请求量(每秒多少个)、点赞成功率(成功的请求占比)、取消点赞率、缓存命中率。比如点赞成功率突然从 99.9% 降到 90%,可能是系统出问题了,要立刻告警。
系统指标:CPU 使用率、内存使用率、磁盘使用率、网络带宽。比如 Redis 服务器的 CPU 使用率突然涨到 90%,可能是有慢查询,要及时排查。
组件指标:Redis 的连接数、命中率、主从同步延迟;MySQL 的 QPS(每秒查询数)、慢查询数、主从同步延迟。比如 MySQL 的慢查询数突然从 0 涨到 100,可能是 SQL 写得烂,要优化 SQL。
接口指标:每个接口的响应时间、调用量、错误率。比如点赞接口的响应时间突然从 10 毫秒涨到 100 毫秒,可能是缓存出问题了,要立刻检查缓存。
告警也很关键,要设置 “多级告警”:比如点赞成功率降到 95%,发短信通知初级运维;降到 90%,发电话通知高级运维;降到 80%,发邮件 + 电话通知技术负责人。这样能保证问题越大,越多人关注,解决得越快。
6.2 项目实践中的常见问题与解决方案
牛哥带团队做过 3 个亿级点赞相关项目,踩过的坑能装一箩筐。今天把最常见的 5 个问题拎出来,说说怎么解决,帮你们避坑:
6.2.1 问题 1:Redis 缓存与 MySQL 数据不一致,点赞数 “跳变”
场景:用户点赞后,Redis 里显示 1000 赞,刷新后变成 999,用户投诉 “点赞消失了”。
原因:异步同步时出了问题 —— 比如 Kafka 消息消费失败,或者 MySQL 更新成功但 Redis 没删缓存(Cache-Aside 策略没执行好)。
解决方案:
消息消费加 “重试 + 死信”:Kafka 消费者消费消息时,要是 MySQL 更新失败,重试 3 次,还失败就丢到死信队列,运维每天看死信队列,手动处理;
缓存加 “版本号”:给每个点赞数加版本号,比如 Redis 里存article:{articleId}:likeCount和article:{articleId}:version,同步到 MySQL 时,带着版本号更新,避免旧数据覆盖新数据;
定时校验:每天凌晨跑脚本,对比 Redis 和 MySQL 的热门文章点赞数,差值超过 5% 就告警,手动校准。
6.2.2 问题 2:Redis 集群扩容后,数据迁移导致短暂不可用
场景:Redis 从 8 分片扩到 16 分片,迁移数据时,部分点赞请求超时,用户点了没反应。
原因:传统的 Redis 数据迁移,会暂停旧分片的写操作,迁移完再切流量,中间有几秒到几十秒的不可用。
解决方案:
用 Redis Cluster 的 “在线迁移”:开启 Redis 的cluster-migration-barrier参数,让新分片先同步旧分片数据,同步完后,逐步把流量切到新分片,旧分片只读不写,直到数据完全一致,再下线旧分片 —— 整个过程零停机;
迁移期间加 “本地缓存兜底”:应用服务器本地缓存热门文章的点赞数,迁移时要是 Redis 查不到,先返回本地缓存数据,迁移完再更新本地缓存,用户感知不到异常。
6.2.3 问题 3:MySQL 分表后,查询 “用户所有点赞文章” 变慢
场景:用户点 “我的点赞”,要查自己所有点赞的文章,分表后要查 256 张表,响应时间从 50 毫秒涨到 500 毫秒。
原因:分表后,按用户 ID 查要跨多个表,就算加了索引,也得逐个表查,再合并结果,速度慢。
解决方案:
建 “用户点赞索引表”:单独建一张表,按用户 ID 分表,存用户点赞的文章 ID 和时间,比如user_like_index_{userIdMod32},每个用户的点赞记录都存在一张表里,查的时候直接查这张表,不用跨表;
加 “二级缓存”:用户查 “我的点赞” 时,先查 Redis 里的user:{userId}:likeArticles(存最近 100 条点赞记录),没有再查索引表,查完同步到 Redis,过期时间设 1 小时 ——90% 的查询都能在 Redis 里搞定。
6.2.4 问题 4:高峰期 Kafka 消息堆积,点赞数据同步延迟
场景:某爆款文章点赞峰值时,Kafka 的article-like-sync主题消息堆积了 10 万条,MySQL 同步延迟超过 5 分钟,用户查历史点赞记录时看不到最新数据。
原因:消费者处理速度跟不上生产速度,单个消费者每秒只能处理 1000 条消息,峰值时每秒生产 5000 条,自然堆积。
解决方案:
增加消费者分区:把 Kafka 主题的分区数从 8 个扩到 32 个,每个分区对应一个消费者实例,处理速度直接翻 4 倍,每秒能处理 4000 条;
消费者 “批量处理”:原来消费者一条消息处理一次 MySQL,改成批量拉取 100 条消息,批量更新 MySQL,减少数据库连接开销,处理速度再提升 2 倍;
消息 “分级”:把点赞消息分 “实时” 和 “非实时”,热门文章的点赞消息走高优先级分区,优先消费,普通文章的走普通分区,错峰处理 —— 保证用户关心的热门内容同步不延迟。
6.2.5 问题 5:用户 “反复点赞取消”,导致 Redis 频繁操作
场景:有些用户手欠,点了赞又取消,取消又点赞,1 分钟内操作 10 次,Redis 要频繁执行INCR、DECR、HSET、HDEL,增加 Redis 压力。
原因:没做前端限流,后端也没限制用户操作频率。
解决方案:
前端加 “防抖”:用户点击点赞按钮后,10 秒内不能再次点击,按钮置灰,从源头减少无效请求;
后端加 “频率限制”:用 Redis 的INCR做计数器,key 是user:{userId}:likeOperateLimit,用户每操作一次加 1,10 秒内超过 3 次就返回 “操作太频繁,请稍后再试”—— 既减少 Redis 压力,又避免恶意操作。
6.3 亿级系统设计的通用方法论
聊完案例和坑,牛哥给你们总结一套 “亿级系统设计通用方法论”,不管是做点赞、评论,还是订单、支付,这套思路都能用,核心就 4 句话:
6.3.1 第一句:“先扛住,再优化”—— 优先解决高并发问题
亿级系统的核心矛盾是 “高并发”,设计时先别纠结细节优化,先想清楚 “怎么扛住峰值请求”。比如刚开始做点赞系统,别先考虑怎么省内存,先把 Redis 集群搭起来,把 Kafka 异步流程跑通,保证峰值时请求不丢、系统不崩 —— 等系统稳定了,再回头优化缓存命中率、减少数据库连接数。
就像盖房子,先把框架搭起来,再装修;要是先纠结贴什么瓷砖,框架没搭稳,一阵风就吹倒了。
6.3.2 第二句:“分而治之”—— 把大问题拆成小模块
亿级系统不能搞 “monolith”(单体架构),必须拆成独立模块,每个模块干自己擅长的事。比如点赞系统拆成 “交互模块、存储模块、异步模块、一致性模块”,每个模块可以单独扩容、单独优化 —— 就算异步模块出问题,也不会影响用户点赞操作。
拆模块的关键是 “按职责拆分”,比如 “实时处理” 和 “异步处理” 必须分开,“缓存” 和 “数据库” 必须分开,别把所有逻辑揉在一个服务里。
6.3.3 第三句:“异步优先,同步兜底”—— 能用异步的绝不同步
亿级系统里,同步操作是 “性能杀手”,比如用户点赞后,同步写数据库、同步更新统计数据,请求会堵在那儿。所以设计时,先想 “哪些操作可以异步”—— 比如写历史数据、统计数据、发通知,这些都不影响用户实时体验,全用异步;只有 “更新缓存、返回操作结果” 这些必须实时的,才用同步。
但要注意,异步不是 “甩锅”,必须做好消息重试、死信处理,保证数据最终一致 —— 别以为异步了就万事大吉,丢数据的锅比同步慢的锅还难背。
6.3.4 第四句:“监控先行,预案在手”—— 别等故障发生才补救
亿级系统不怕出问题,怕的是出了问题没人知道,或者知道了不知道怎么解决。所以设计时,先把监控告警搭好,关键指标(比如请求量、响应时间、错误率)实时监控,异常时立刻告警;再针对可能的故障(比如 Redis 宕机、Kafka 堆积),提前写好预案,比如 “Redis 宕机后,怎么切换从节点”“Kafka 堆积后,怎么加消费者”—— 把预案写成脚本,甚至定期演练,真出问题时,按预案来,半小时就能恢复,不用手忙脚乱。
牛哥见过太多团队,系统搭好了,没监控没预案,出了故障半夜爬起来排查,折腾几小时才恢复 —— 这就是没做到 “监控先行”。
最后说两句
亿级点赞系统看起来复杂,但拆解开就是 “缓存扛并发、异步解耦、分库分表存数据、监控保障可用性” 这几件事。关键不是记住多少技术名词,而是理解 “为什么这么设计”—— 比如为什么用 Redis 不用 MySQL?因为 Redis 快,能扛高并发;为什么用异步?因为同步慢,影响用户体验。
以后你们碰到亿级系统设计,先想清楚业务需求和核心挑战,再用 “分而治之” 的思路拆模块,用 “异步优先” 的思路提性能,最后用 “监控预案” 保稳定 —— 按这个流程来,再复杂的系统也能搞定。
如果你们在项目里还碰到过其他坑,或者有不同的设计思路,欢迎在评论区留言,咱们一起交流 —— 技术就是在互相讨论中进步的!
