1000万并发点赞,如何设计一个高可用的点赞系统?
最近抖音 🔥 的有点杂,从鸡排哥到云南 BIGBANG,突如其来的流量,动不动直播间就是 10w+,点赞上千万到亿,手指轻点屏幕——「❤️+1」。这个再简单不过的操作,背后却藏着一套精巧的技术设计。当千万用户在同一时刻为同一个直播间疯狂点赞时,如何保证每一次点击都被准确记录?这就是我们今天要聊的面试场景题。
从最简单的方案说起
很多刚入行的程序员第一次做点赞功能时,思路都很直接:收到点赞请求,就往数据库执行一条更新语句。
-- 最简单的点赞实现
UPDATE post
SET like_count = like_count + 1
WHERE id = #{postId};简单粗暴,逻辑清晰,在用户量不大的时候确实没什么问题。
image
但这种方案有个致命缺陷——高并发场景下的性能瓶颈。想象一下李佳琦直播间,几十万人同时在线,每秒可能有上万次点赞。每次点赞都要锁表、写磁盘、更新索引,数据库瞬间就会被打垮。热门内容的点赞数据会成为整个系统的"热点",引发行锁竞争,响应时间从毫秒级飙升到秒级,用户体验直线下降。
Redis 登场:用缓存扛住流量冲击
既然数据库扛不住,那就把计数操作放到内存里——这就是 Redis 的用武之地。Redis 的 INCR 命令天生就是为计数而生的,它是原子操作,单线程模型保证了并发安全,而且速度快到飞起,轻松支撑每秒十万级的操作。
// 使用 Redis 处理点赞
@Service
public class LikeService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
public void addLike(Long roomId) {
String key = "like:room:" + roomId;
// Redis 原子自增操作
redisTemplate.opsForValue().increment(key, 1);
}
public Long getLikeCount(Long roomId) {
String key = "like:room:" + roomId;
String count = redisTemplate.opsForValue().get(key);
return count != null ? Long.parseLong(count) : 0L;
}
}具体怎么做呢?当用户点赞时,不再直接写数据库,而是执行 INCR like:room:123,把计数累加到 Redis 里。前端展示点赞数时,也直接从 Redis 读取。这样一来,数据库的压力瞬间降低了 99%。
image
但新的问题又来了:Redis 里的数据总不能一直放着不管吧?服务器重启了怎么办?Redis 挂了数据不就丢了?这就需要引入「持久化机制」——定期把 Redis 的计数同步回数据库。
// 定时同步 Redis 数据到 MySQL
@Scheduled(cron = "0 */5 * * * ?") // 每5分钟执行一次
public void syncLikeCountToDB() {
Set<String> keys = redisTemplate.keys("like:room:*");
for (String key : keys) {
Long roomId = extractRoomId(key);
Long redisCount = getLikeCount(roomId);
// 批量更新数据库
postMapper.updateLikeCount(roomId, redisCount);
// 同步后清零或删除 Redis 键
redisTemplate.delete(key);
}
}可以用定时任务,每隔几分钟批量写入一次,既保证了数据最终会落盘,又避免了频繁写库的性能开销。
前端优化:别让每次点击都成为请求
到这里还没完。如果用户手速够快,一秒点十几次屏幕,难道要发十几个网络请求吗?这不仅浪费带宽,服务器也受不了。
聪明的做法是在客户端做「请求合并」。前端可以设置一个缓冲区,比如每 3 秒或者累积 10 次点赞后,才批量上报一次总数。
// 前端批量点赞实现
class LikeManager {
constructor() {
this.likeCount = 0;
this.roomId = null;
this.timer = null;
}
// 用户点击点赞按钮
onLikeClick(roomId) {
this.roomId = roomId;
this.likeCount++;
// 立即更新UI显示
this.updateUI();
// 防抖:3秒内的点赞合并成一次请求
clearTimeout(this.timer);
this.timer = setTimeout(() => {
this.sendBatchLike();
}, 3000);
}
// 批量发送点赞请求
async sendBatchLike() {
if (this.likeCount > 0) {
await fetch('/api/like/batch', {
method: 'POST',
body: JSON.stringify({
roomId: this.roomId,
count: this.likeCount
})
});
this.likeCount = 0;
}
}
}用户看到的点赞动画依然流畅即时,但实际发送的网络请求大大减少。这个小优化能让服务端压力再降一个数量级。
魔鬼藏在细节里:一致性与幂等性
当系统变复杂后,一些边界问题就会浮现出来。比如用户重复点赞怎么办?网络抖动导致请求发了两次怎么办?
防止重复点赞可以用 Redis 的 Set 结构,存储已点赞的用户 ID:
public boolean addLikeWithCheck(Long roomId, Long userId) {
String countKey = "like:room:" + roomId;
String userSetKey = "like:room:" + roomId + ":users";
// 检查用户是否已点赞
Boolean isNewMember = redisTemplate.opsForSet()
.add(userSetKey, userId.toString());
if (Boolean.TRUE.equals(isNewMember)) {
// 首次点赞,增加计数
redisTemplate.opsForValue().increment(countKey, 1);
return true;
}
return false; // 已经点过赞了
}至于数据一致性问题,需要承认一个现实:在分布式系统中,强一致性往往意味着牺牲性能。对于点赞这种场景,我们追求的是「最终一致性」——用户点赞后可能有几秒钟的延迟,但最终 Redis 和数据库的数据一定会对齐。
架构演进:走向分布式
当业务继续膨胀,单机 Redis 也开始吃紧时,就该考虑分布式架构了。Redis Cluster 可以把不同直播间的点赞数据分散到不同的节点上,每个节点只处理一部分流量。
image
整套系统的数据流大概是这样:用户端批量上报点赞 → 负载均衡分发请求 → 应用服务做幂等校验 → Redis Cluster 累加计数 → 定时任务批量同步到 MySQL 分库 → 监控系统检测数据偏差并告警。每个环节各司其职,环环相扣。
方案对比总结
| 实现方式 | QPS | 一致性 | 复杂度 | 适用场景 |
|---|---|---|---|---|
| 数据库直接更新 | < 1000 | 强一致 | ⭐ | 博客文章点赞 |
| Redis 缓存计数 | 10 万+ | 最终一致 | ⭐⭐ | 视频点赞 |
| Redis + 批量同步 | 50 万+ | 最终一致 | ⭐⭐⭐ | 直播间点赞 |
| 分布式集群 | 百万+ | 最终一致 | ⭐⭐⭐⭐ | 大型直播平台 |
总结
从最初的一条 SQL 语句,到后来的 Redis 缓存,再到分布式集群,点赞系统的演进史其实就是互联网高并发架构的缩影。它告诉我们:没有银弹,只有权衡。简单场景用简单方案,复杂场景才上复杂架构。过度设计和设计不足都是问题,关键是要理解业务特点,找到性能、成本和可维护性之间的平衡点。
下次当你在刷短视频疯狂点赞时,不妨想想屏幕背后那套精密运转的系统。那些看不见的技术细节,正是我们这个时代数字生活顺滑体验的基石。
