Q01-高并发点赞系统架构设计

高并发点赞系统架构设计:从数据库行锁到 Redis 四层架构 🏗️

本文档基于抖音面试案例视频,深入解析高并发点赞系统的四层架构设计。从数据库行级锁的串行化瓶颈出发,逐层讲解前端批量合并、Redis 原子计数器、异步持久化到分库分表打散热点的完整解决方案,揭示如何在性能与一致性之间取得务实平衡 🎯

This document analyzes a four-layer architecture for high-concurrency like systems based on a Douyin interview case. From database row-lock serialization bottlenecks, it progressively explains frontend batch merging, Redis atomic counters, async persistence, and sharding strategies, revealing how to achieve pragmatic balance between performance and consistency 🎯


术语表 / Terminology

术语 / Term 说明 / Description
Row Lock Database 对单行 Data 施加的 Lock,确保 Concurrent Update 的一致性
Serialization 多个 Concurrent Requests 被迫按顺序执行的现象
Atomic Operation 不可分割的 Operation,要么完全执行,要么完全不执行
INCRBY Redis Command,将 Key 的 Value 增加指定 Integer
Write-Behind 先写 Cache,再 Async Batch 持久化到 Database 的策略
Eventual Consistency System 保证在一段时间后 Data 达到一致状态,不强求即时一致
Sharding 将 Data 分散到多个 Database Instances 或 Tables,打散 Write Hotspot
Idempotency 同一 Operation 执行多次与执行一次的结果相同

章节阅读路线图 🗺️ / Chapter Reading Roadmap

  1. 视频来源与背景 📺 / Video Source & Background → 说明文档来源与核心问题
  2. 数据库行锁的串行化灾难 🔒 / Row Lock Serialization Disaster → 为什么直接 UPDATE 会崩
  3. 第一层:前端批量合并削峰 📱 / Frontend Batch Merging → 从源头减少请求量
  4. 第二层:Redis 原子计数器 ⚡ / Redis Atomic Counter → 内存级高并发累加
  5. 第三层:异步持久化策略 💾 / Async Persistence Strategy → Write-Behind 模式落地
  6. 第四层:分库分表打散热点 🗂️ / Database Sharding → 彻底消除单点瓶颈
  7. 架构取舍与最终一致性 ⚖️ / Architecture Trade-offs → 性能 vs 一致性的务实选择
  8. 完整架构总结 📝 / Complete Architecture Summary → 四层架构全景回顾

1. 视频来源与背景 📺 / Video Source & Background

📺 Note: 本文档基于抖音视频内容二次解读 / This document is a secondary interpretation based on a Douyin video.

1.1 视频信息

视频标题:扛住千万人点赞的4层架构设计 🎬

视频作者:简简日常(抖音账号)

视频链接抖音 - 扛住千万人点赞的4层架构设计

内容摘要 :本文档基于该视频中的面试案例,从"如何设计一个点赞系统"这一经典面试题出发,深入解析了从 naive 的数据库直写到四层高并发架构的完整演进过程。视频通过真实的面试对话场景,揭示了为什么简单的 UPDATE 语句在大并发场景下会导致系统崩溃,并逐步展示了如何通过前端合并、Redis 缓存、异步持久化和分库分表四层策略,构建一个能支撑千万级并发的点赞系统。


1.2 面试场景还原

面试官提问 🎤:

"如果让你设计一个点赞功能,你会怎么做?"

候选人初始回答 💭:

"我觉得可以写一个 UPDATE 语句,用户点一次赞,我们就在数据库里把对应文章或者视频的点赞数加一,这样最直接。"

面试官追问 🔥:

"那如果这是个大主播的直播间,同时有上万人疯狂点赞,所有人都去更新数据库里同一行记录。你觉得会发生什么?"

候选人卡壳 😰:

"这会触发数据库的行级锁...(后面的机制不清楚)"

面试结果 ❌:到这里差不多就结束了。


1.3 核心问题定义

这个面试案例揭示了一个经典的热点数据并发更新问题(Hotspot Data Concurrent Update Problem)

当大量并发请求同时尝试修改数据库中的 同一行数据 (如某直播间的点赞总数)时,数据库的行级锁机制会强制将这些请求 串行化执行,导致:

  1. ⏱️ 请求大量超时:后到的请求必须等待前面的事务完成
  2. 🔗 连接池耗尽:大量等待的请求占用数据库连接
  3. 💥 系统雪崩:数据库锁死,整个服务不可用

这个问题不仅出现在点赞场景,秒杀扣减库存、热门商品计数器、实时排行榜等场景都面临同样的灾难。


2. 数据库行锁的串行化灾难 🔒 / Row Lock Serialization Disaster

🔒 Note: 本章详解为什么直接 UPDATE 会在高并发下导致系统崩溃 / This chapter explains why direct UPDATE causes system crash under high concurrency.

2.1 行级锁的工作原理

在关系型数据库(如 MySQL、PostgreSQL)中,当执行以下 SQL 语句时:

sql 复制代码
UPDATE live_room SET like_count = like_count + 1 WHERE room_id = 12345;

数据库会执行以下操作:📋

  1. 定位目标行 🔍:通过索引或全表扫描找到 room_id = 12345 的记录
  2. 施加行级锁 🔒:对该行数据加写锁(Write Lock / Exclusive Lock)
  3. 执行更新 ✏️:将 like_count 字段值加 1
  4. 记录事务日志 📝:写入 Redo Log 和 Undo Log,保证 ACID 特性
  5. 提交事务并释放锁 ✅:事务提交后,释放行锁,允许其他事务访问

关键问题 ⚠️:在步骤 2 到步骤 5 之间, 其他所有尝试更新同一行的事务都必须排队等待


2.2 串行化机制与雪崩效应

什么是串行化? 🤔

假设有 10,000 个用户同时点赞同一个直播间,每个 UPDATE 事务执行需要 50ms(包括锁获取、磁盘写入、日志记录等):

  • 第 1 个请求:0ms 开始,50ms 完成 ✅
  • 第 2 个请求:50ms 开始,100ms 完成 ✅
  • 第 3 个请求:100ms 开始,150ms 完成 ✅
  • ...
  • 第 10,000 个请求: 499,950ms(约 8.3 分钟) 才开始执行 ⏱️

数学公式 📐:
Tlast=(N−1)×Ttxn T_{\text{last}} = (N - 1) \times T_{\text{txn}} Tlast=(N−1)×Ttxn

其中:📝

  • Tlast T_{\text{last}} Tlast 是最后一个请求的开始时间
  • NN N 是并发请求数量(10,000)
  • Ttxn T_{\text{txn}} Ttxn 是单个事务执行时间(50ms)

计算结果:
Tlast=(10000−1)×50ms=499,950ms≈8.3分钟 T_{\text{last}} = (10000 - 1) \times 50\text{ms} = 499,950\text{ms} \approx 8.3\text{分钟} Tlast=(10000−1)×50ms=499,950ms≈8.3分钟

这意味着最后一个用户点完赞后,要等 8 分多钟数据库才开始处理他的请求!而大多数系统的请求超时时间只有 3-5 秒。


2.3 连接池耗尽与系统雪崩

数据库连接池(Connection Pool) 是什么? 🏊

数据库连接池是预先创建好的一批数据库连接(如 100 个),应用通过这些连接与数据库通信,避免频繁创建和销毁连接的开销。

雪崩过程 💥:

markdown 复制代码
1. 10,000 个并发请求到来
   ↓
2. 每个请求占用一个数据库连接,执行 UPDATE
   ↓
3. 第 101 个请求发现连接池已满(假设池大小为 100)
   ↓
4. 第 101-10,000 个请求进入等待队列
   ↓
5. 等待超时(通常 3-5 秒),抛出 Connection Timeout 异常
   ↓
6. 应用层捕获异常,返回 500 错误给用户
   ↓
7. 如果应用层有重试机制,会再次发起请求,加剧问题
   ↓
8. 数据库连接被长时间占用,其他业务(如查询、登录)也受影响
   ↓
9. 整个系统不可用,形成雪崩 🌨️

直观类比 🌉:

想象一座独木桥(数据库的某一行):

  • 正常情况下,每次只有 1 个人过桥,很顺畅
  • 突然 10,000 个人要同时过桥
  • 但桥每次只能过 1 个人,且每个人需要 50 秒才能走完
  • 后面的人只能排队等待,等到最后一个人时,已经过了 8 小时
  • 大多数人等不及,直接放弃了(请求超时)

这就是行锁串行化导致的雪崩效应


参考资料:


3. 第一层:前端批量合并削峰 📱 / Frontend Batch Merging

📱 Note: 本章讲解如何在前端层面减少请求数量 / This chapter explains how to reduce request volume at the frontend level.

3.1 核心思路:本地累积 + 批量提交

问题分析 🔍:

如果用户每秒狂点 50 次赞,客户端真的需要发 50 个请求到后端吗?

答案是否定的 ❌。点赞业务的本质是 计数累加,用户并不关心每一次点击是否立即生效,只关心最终的点赞总数是否正确。

解决方案 💡:

在前端(App 或 Web)做一层拦截和打包

复制代码
用户点击 → 本地计数器 +1 → 累积 1 秒 → 批量发送(如 50 次) → 后端接收

3.2 实现机制

前端伪代码 📝:

javascript 复制代码
// 前端点赞计数器(本地内存)
let likeCounter = 0;                              // 本地累积计数器,初始为 0
let lastSendTime = Date.now();                    // 上次发送时间戳

// 用户每次点击赞时触发
function onLikeClick() {
    likeCounter++;                                // 本地计数器 +1,不立即发请求
}

// 定时器:每 1 秒检查一次
setInterval(() => {
    if (likeCounter > 0) {                        // 如果有累积的点赞
        sendLikeRequest(likeCounter);             // 批量发送,如 {userId: 123, count: 50}
        likeCounter = 0;                          // 重置本地计数器
        lastSendTime = Date.now();                // 更新发送时间
    }
}, 1000);                                         // 每 1000ms(1秒)执行一次

数据流动 🔄:

ini 复制代码
用户点击 50 次
   ↓
前端本地累积:likeCounter = 50
   ↓
1 秒后,发送单个请求:POST /api/likes {room_id: 12345, count: 50}
   ↓
后端接收:1 个请求(而非 50 个)

3.3 收益与代价

收益 ✅:

  1. 请求量锐减 📉:从单用户的 N 次请求降为 1 次(N 倍减少)
  2. 网络开销降低 🌐:减少 HTTP 连接建立、TLS 握手、请求头传输等开销
  3. 后端压力骤降 💪:后端只需处理 1/50 的请求量

代价 ⚖️:

  1. 实时性降低 ⏱️:点赞从"每次点击立即更新"变为"秒级批量更新"
  2. 数据丢失风险 ⚠️:如果用户在发送前关闭 App,本地累积的点赞可能丢失

为什么代价可接受? 🤔

  • 点赞业务不要求强实时性:用户不会盯着点赞数看每一秒的变化
  • 数据丢失风险极低:1 秒间隔很短,且用户通常不会频繁关闭 App
  • 用户体验无感知:前端可以立即更新 UI 显示(乐观更新),后台异步同步

直观类比 🚌:

想象公交车站:

  • 不合并:每个人打车单独走 → 100 个人需要 100 辆车,交通拥堵
  • 合并后:大家等一辆大巴车一起走 → 100 个人只需 1 辆车,高效顺畅

前端批量合并就是这个道理。


参考资料:


4. 第二层:Redis 原子计数器 ⚡ / Redis Atomic Counter

Note: 本章详解如何使用 Redis 的原子操作解决高并发累加问题 / This chapter explains how to use Redis atomic operations to solve high-concurrency accumulation.

4.1 为什么选择 Redis?

Redis 的核心优势 🌟:

  1. 纯内存操作 💾:数据存储在内存中,读写速度极快(微秒级)
  2. 单线程模型 🧵:避免多线程锁竞争,天然保证操作的原子性
  3. 丰富的数据结构 📦:支持 String、Hash、Set、Sorted Set 等
  4. 高性能 🚀:单机可支撑 10 万+ QPS(Queries Per Second)

对比数据库 ⚔️:

特性 关系型数据库(MySQL) Redis
存储介质 磁盘(Disk) 💿 内存(Memory) ⚡
单次写入延迟 毫秒级(ms) 微秒级(μs)
并发能力 数百~数千 QPS 十万+ QPS
锁机制 行锁/表锁,需等待 🔒 单线程,无需锁 ✅
适用场景 持久化存储、复杂查询 缓存、计数器、会话

4.2 INCRBY 命令详解

什么是 INCRBY 🤔

INCRBY 是 Redis 的原子增量命令,语法如下:

redis 复制代码
INCRBY key increment

功能 :将键 key 的整数值增加 increment,返回增加后的新值。

示例 📝:

redis 复制代码
# 初始状态:live:room:12345:likes = 1000

INCRBY live:room:12345:likes 50                 # 增加 50
# 返回:1050

INCRBY live:room:12345:likes 1                  # 增加 1
# 返回:1051

GET live:room:12345:likes                       # 获取当前值
# 返回:"1051"

原子性保证 ✅:

Redis 是单线程执行模型 (Single-Threaded Execution Model),所有命令按顺序排队执行,不存在并发冲突。即使有 10,000 个客户端同时发送 INCRBY 命令,Redis 也会按接收顺序逐个执行,每个命令都是原子的。


4.3 后端实现代码

Java + Spring Boot 示例 💻:

java 复制代码
import org.springframework.data.redis.core.StringRedisTemplate;         // 导入 Redis 操作模板
import org.springframework.stereotype.Service;                          // 导入 Spring 服务注解
import javax.annotation.Resource;                                       // 导入资源注入注解

/**点赞服务实现类 🎯 / Like Service Implementation

功能:通过 Redis 原子操作处理高并发点赞 / Handle high-concurrency likes via Redis atomic operations
*/
@Service
public class LikeService {
    
    @Resource                                                           // 注入 Redis 操作模板
    private StringRedisTemplate redisTemplate;                          // Redis 模板,用于执行 Redis 命令
    
    /**处理批量点赞请求 ⚡ / Process batch like request
    
    参数 / Args:
        roomId: 直播间 ID / Live room ID
        count: 本次点赞数量(前端合并后的批量值)/ Like count from frontend batch
        
    返回 / Returns:
        currentLikes: 当前总点赞数 / Current total like count
    */
    public Long processBatchLikes(Long roomId, Integer count) {
        String key = "live:room:" + roomId + ":likes";                  // 构造 Redis 键,如 "live:room:12345:likes"
        
        // 执行原子增量操作,时间复杂度 O(1) / Execute atomic increment, O(1) complexity
        // 数据流动:key="live:room:12345:likes", count=50 → Redis 内存中累加 → 返回新值 1050
        Long currentLikes = redisTemplate.opsForValue().increment(key, count);
        
        return currentLikes;                                            // 返回当前总点赞数
    }
    
    /**获取当前点赞数 👁️ / Get current like count
    
    参数 / Args:
        roomId: 直播间 ID / Live room ID
        
    返回 / Returns:
        likes: 当前点赞数 / Current like count
    */
    public Long getCurrentLikes(Long roomId) {
        String key = "live:room:" + roomId + ":likes";                  // 构造 Redis 键
        String value = redisTemplate.opsForValue().get(key);            // 获取键值,数据流动:key → "1050"
        return value != null ? Long.parseLong(value) : 0L;              // 转换为 Long,若为空则返回 0
    }
}

4.4 性能分析

时间复杂度 ⏱️:

  • INCRBY 命令: O(1)O(1) O(1)(常数时间),无论值多大,执行时间不变
  • 对比数据库 UPDATE O(log⁡N)O(\log N) O(logN)(索引查找)+ 磁盘 I/O + 锁等待

吞吐量估算 📊:

场景 数据库(MySQL) Redis
单次写入延迟 ~50ms ~0.1ms(100μs)
每秒处理能力 ~1,000 次 ~100,000 次
10,000 并发耗时 ~500 秒(串行) ~0.1 秒(并行)

关键洞察 💡:

Redis 将 10,000 次并发写入的处理时间从 500 秒 压缩到 0.1 秒 ,性能提升 5000 倍

直观类比 🏪:

  • 数据库:像银行柜台,每次只能服务 1 个客户,其他人排队
  • Redis:像自助取款机 + 100 台并行,所有人几乎同时完成操作

参考资料:


5. 第三层:异步持久化策略 💾 / Async Persistence Strategy

💾 Note: 本章讲解 Write-Behind 模式,如何平衡性能与数据持久化 / This chapter explains the Write-Behind pattern, balancing performance and data persistence.

5.1 问题引入:Redis 宕机数据丢失

风险场景 ⚠️:

所有点赞数据都存储在 Redis 内存中,如果:

  • Redis 服务器突然宕机 💥
  • 机房断电 🔌
  • 网络分区导致 Redis 不可用 🌐

后果 :自上次持久化以来的所有点赞增量 全部丢失

举例 📝:

  • 12:00:00 - Redis 值 = 10,000(已持久化到数据库)
  • 12:00:01 ~ 12:00:04 - 用户疯狂点赞,Redis 值增长到 15,000
  • 12:00:05 - Redis 宕机 💥
  • 重启后,Redis 值回退到 10,000,丢失 5,000 次点赞

5.2 Write-Behind 模式

什么是 Write-Behind? 🤔

Write-Behind(异步回写 / 延迟写入)是一种缓存写入策略:

markdown 复制代码
客户端请求 → 写入缓存(Redis) → 立即返回成功 ✅
                    ↓
            后台定时任务(如每 5 秒)
                    ↓
            批量读取缓存数据 → 批量写入数据库 💾

与其他缓存策略对比 ⚔️:

策略 写入时机 性能 数据安全性 适用场景
Cache Aside 先写数据库,再删缓存 中等 读多写少
Write Through 同时写缓存和数据库 最高 强一致性要求
Write Behind 先写缓存,异步写数据库 最高 中等(可能丢失几秒数据) 点赞、计数器等

为什么点赞场景适合 Write-Behind? 🎯

点赞业务的一致性要求较低

  • 用户不关心点赞数是否精确到个位数
  • 丢失几秒的数据(如几千次点赞)对百万级直播间无影响
  • 最终一致性即可满足需求

5.3 定时任务实现

Spring Boot 定时任务示例 💻:

java 复制代码
import org.springframework.scheduling.annotation.Scheduled;             // 导入定时任务注解
import org.springframework.data.redis.core.StringRedisTemplate;         // 导入 Redis 操作模板
import org.springframework.jdbc.core.JdbcTemplate;                      // 导入 JDBC 模板
import org.springframework.stereotype.Service;                          // 导入 Spring 服务注解
import javax.annotation.Resource;                                       // 导入资源注入注解
import java.util.List;                                                  // 导入列表类型
import java.util.Map;                                                   // 导入 Map 类型

/**异步持久化服务 💾 / Async Persistence Service

功能:定时将 Redis 中的点赞数据批量写入数据库 / Periodically batch-write Redis like data to database
*/
@Service
public class LikePersistenceService {
    
    @Resource                                                           // 注入 Redis 操作模板
    private StringRedisTemplate redisTemplate;                          // Redis 模板
    
    @Resource                                                           // 注入 JDBC 模板
    private JdbcTemplate jdbcTemplate;                                  // JDBC 模板,用于执行 SQL
    
    /**定时任务:每 5 秒执行一次 ⏰ / Scheduled task: execute every 5 seconds
    
    参数 / Args:
        无 / None
        
    返回 / Returns:
        无 / None
    */
    @Scheduled(fixedRate = 5000)                                        // 每 5000ms(5秒)执行一次
    public void persistLikesToDatabase() {
        // 1️⃣ 扫描所有直播间点赞键(简化示例,实际可用 SCAN 命令)
        // 数据流动:Redis → keys=["live:room:12345:likes", "live:room:67890:likes", ...]
        List<String> keys = scanLikeKeys();                             // 获取所有点赞键
        
        if (keys.isEmpty()) {                                           // 如果没有待持久化的数据
            return;                                                     // 直接返回,不做任何操作
        }
        
        // 2️⃣ 批量读取 Redis 值
        // 数据流动:keys → values={"live:room:12345:likes": "15000", "live:room:67890:likes": "8000", ...}
        Map<String, String> likeCounts = redisTemplate.opsForValue().multiGet(keys);
        
        // 3️⃣ 批量写入数据库(使用 INSERT ... ON DUPLICATE KEY UPDATE)
        // 数据流动:likeCounts → SQL 批量执行 → 数据库持久化
        for (Map.Entry<String, String> entry : likeCounts.entrySet()) {
            String key = entry.getKey();                                // 如 "live:room:12345:likes"
            String countStr = entry.getValue();                         // 如 "15000"
            
            if (countStr != null) {                                     // 如果值不为空
                Long roomId = extractRoomId(key);                       // 提取直播间 ID,如 12345
                Long count = Long.parseLong(countStr);                  // 解析点赞数,如 15000
                
                // 执行 SQL:如果记录存在则更新,不存在则插入
                // 数据流动:roomId=12345, count=15000 → UPDATE/INSERT live_room SET like_count=15000
                jdbcTemplate.execute(
                    "INSERT INTO live_room (room_id, like_count) " +
                    "VALUES (" + roomId + ", " + count + ") " +
                    "ON DUPLICATE KEY UPDATE like_count = " + count     // 幂等更新
                );
            }
        }
        
        // 4️⃣ 持久化成功后,可选:删除 Redis 中已持久化的键(或用新值覆盖)
        // redisTemplate.delete(keys);                                  // 根据业务需求决定是否删除
    }
    
    /**扫描所有点赞键 🔍 / Scan all like keys
    
    返回 / Returns:
        keys: 所有点赞键列表 / List of all like keys
    */
    private List<String> scanLikeKeys() {
        // 简化实现:实际应使用 Redis SCAN 命令避免阻塞
        // 示例:SCAN 0 MATCH live:room:*:likes COUNT 1000
        return List.of("live:room:12345:likes", "live:room:67890:likes"); // 示例数据
    }
    
    /**从 Redis 键提取直播间 ID 🔑 / Extract room ID from Redis key
    
    参数 / Args:
        key: Redis 键,如 "live:room:12345:likes" / Redis key
        
    返回 / Returns:
        roomId: 直播间 ID / Room ID
    */
    private Long extractRoomId(String key) {
        // 解析: "live:room:12345:likes" → 12345
        String[] parts = key.split(":");                                // 按 ":" 分割 → ["live", "room", "12345", "likes"]
        return Long.parseLong(parts[2]);                                // 取第 3 个元素(索引 2),转换为 Long
    }
}

5.4 数据丢失风险评估

丢失窗口 ⏱️:

如果定时任务每 5 秒执行一次,最坏情况下丢失 5 秒内 的点赞数据。

数学公式 📐:

假设某直播间每秒收到 10,000 次点赞:
最大丢失量=每秒点赞数×持久化间隔\text{最大丢失量} = \text{每秒点赞数} \times \text{持久化间隔} 最大丢失量=每秒点赞数×持久化间隔
最大丢失量=10,000×5=50,000次点赞\text{最大丢失量} = 10,000 \times 5 = 50,000 \text{次点赞} 最大丢失量=10,000×5=50,000次点赞

业务可接受吗? 🤔

  • 对于 1,000,000 次点赞的直播间,丢失 50,000 次 = 5% 误差
  • 用户看到的点赞数:1,000,000 vs 1,050,000(实际)
  • 用户无感知:量级正确,不影响体验

直观类比 🎰:

想象赌场里的老虎机:

  • 每秒有几百人投币
  • 不需要每次投币都更新总账本
  • 每 5 分钟统计一次总金额就够了
  • 即使老虎机突然断电,丢失几分钟的数据也不影响大局

这就是最终一致性(Eventual Consistency) 的务实选择。


参考资料:


6. 第四层:分库分表打散热点 🗂️ / Database Sharding

🗂️ Note: 本章讲解如何通过水平拆分彻底消除单点写入瓶颈 / This chapter explains how to eliminate single-point write bottlenecks through horizontal sharding.

6.1 为什么需要分库分表?

问题场景 🎯:

即使有了 Redis 和异步持久化,如果某个直播间超级火爆(如全网级活动),单台数据库在批量写入时仍可能存在压力:

  • 定时任务每 5 秒批量写入 1000 个直播间的数据
  • 单个数据库的写入吞吐量达到上限(如磁盘 I/O 瓶颈)
  • 数据库 CPU 占用率 100%,写入延迟增加

解决方案 💡:

通过水平拆分(Horizontal Sharding),将数据分散到多个数据库实例,彻底打散写热点。


6.2 按地区分片策略

分片规则 📋:

根据用户所在地区,将点赞数据写入不同的数据库:

复制代码
用户点赞 → 判断用户地区 → 写入对应地区数据库
   ↓
华北用户 → 华北库(DB-North)
华南用户 → 华南库(DB-South)
华东用户 → 华东库(DB-East)

数据库表结构 📊:

每个地区数据库都有相同的表结构:

sql 复制代码
CREATE TABLE live_room_likes (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,             -- 主键 ID
    room_id BIGINT NOT NULL,                          -- 直播间 ID
    user_region VARCHAR(20) NOT NULL,                 -- 用户地区(如 "north", "south")
    like_count BIGINT DEFAULT 0,                      -- 该地区点赞数
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP    -- 更新时间
);

6.3 写入流程

分片写入示例 💻:

java 复制代码
/**根据用户地区写入点赞数据 🗺️ / Write like data based on user region

参数 / Args:
    roomId: 直播间 ID / Room ID
    userRegion: 用户地区("north", "south", "east")/ User region
    count: 点赞数量 / Like count
    
返回 / Returns:
    无 / None
*/
public void writeLikeByRegion(Long roomId, String userRegion, Long count) {
    // 1️⃣ 根据地区选择数据库
    // 数据流动:userRegion="north" → 选择 DB-North
    String dataSource = getDataSourceByRegion(userRegion);
    
    // 2️⃣ 执行插入或更新
    // 数据流动:roomId=12345, count=50 → INSERT/UPDATE DB-North.live_room_likes
    jdbcTemplate.execute(
        "INSERT INTO live_room_likes (room_id, user_region, like_count) " +
        "VALUES (" + roomId + ", '" + userRegion + "', " + count + ") " +
        "ON DUPLICATE KEY UPDATE like_count = like_count + " + count    // 累加更新
    );
}

/**根据地区获取数据源 🔀 / Get data source by region

参数 / Args:
    region: 地区标识 / Region identifier
    
返回 / Returns:
    dataSource: 数据源名称 / Data source name
*/
private String getDataSourceByRegion(String region) {
    switch (region) {                                                   // 根据地区返回对应数据源
        case "north": return "db-north";                                // 华北用户 → 华北库
        case "south": return "db-south";                                // 华南用户 → 华南库
        case "east": return "db-east";                                  // 华东用户 → 华东库
        default: return "db-default";                                   // 其他地区 → 默认库
    }
}

数据流动示意 🔄:

复制代码
10,000 个并发点赞请求
   ↓
按地区分流:
   ├── 华北用户(3,000 个)→ DB-North
   ├── 华南用户(4,000 个)→ DB-South
   └── 华东用户(3,000 个)→ DB-East
   ↓
每个数据库只处理 3,000~4,000 次写入(而非 10,000 次)

6.4 查询聚合

问题:点赞数分散在多个数据库中,如何获取总点赞数? 🤔

方案 1:后端聚合查询 🖥️

java 复制代码
/**查询直播间总点赞数(后端聚合) 🔍 / Query total likes (backend aggregation)

参数 / Args:
    roomId: 直播间 ID / Room ID
    
返回 / Returns:
    totalLikes: 总点赞数 / Total like count
*/
public Long getTotalLikes(Long roomId) {
    // 1️⃣ 并发查询所有地区数据库
    Long northLikes = queryRegionLikes(roomId, "db-north");             // 查询华北库,如 3,000
    Long southLikes = queryRegionLikes(roomId, "db-south");             // 查询华南库,如 4,000
    Long eastLikes = queryRegionLikes(roomId, "db-east");               // 查询华东库,如 3,000
    
    // 2️⃣ 累加返回
    // 数据流动:3,000 + 4,000 + 3,000 → 10,000
    return northLikes + southLikes + eastLikes;                         // 返回总点赞数
}

方案 2:前端并发请求 📱

前端同时请求多个接口,在客户端累加:

javascript 复制代码
// 前端并发查询
const [north, south, east] = await Promise.all([
    fetch('/api/likes/north?roomId=12345'),                             // 请求华北数据,如 {count: 3000}
    fetch('/api/likes/south?roomId=12345'),                             // 请求华南数据,如 {count: 4000}
    fetch('/api/likes/east?roomId=12345')                               // 请求华东数据,如 {count: 3000}
]);

const totalLikes = north.count + south.count + east.count;              // 客户端累加:3000+4000+3000=10000

6.5 分片效果

性能提升 📊:

指标 单库 三分库
单库写入压力 10,000 次/批 3,333 次/批
磁盘 I/O 占用 100%(瓶颈) 33%(健康)
写入延迟 高(排队) 低(并行)
可扩展性 无法扩展 可增加更多分片

直观类比 🏬:

想象一个大型商场:

  • 单库:所有顾客都从一个入口进入 → 入口拥堵
  • 分库:开设东、南、西三个入口 → 顾客分散进入,畅通无阻

分库分表就是这个道理。


参考资料:


7. 架构取舍与最终一致性 ⚖️ / Architecture Trade-offs

⚖️ Note: 本章分析架构设计中的性能与一致性权衡 / This chapter analyzes the trade-off between performance and consistency in architecture design.

7.1 一致性模型对比

强一致性(Strong Consistency) 🔒:

  • 所有用户在任何时刻看到的数据都相同
  • 写入完成后,所有读操作立即返回最新值
  • 代价:性能低,需要锁机制,不适合高并发场景

最终一致性(Eventual Consistency) ✅:

  • 写入完成后,数据会在一段时间后达到一致
  • 不同用户可能在短时间内看到不同的值
  • 优势:性能高,适合高并发、低一致性要求的场景

点赞业务的一致性要求 🎯:

业务场景 一致性要求 原因
银行转账 强一致性 🔒 金额必须精确,不能丢失或重复
库存扣减 强一致性 🔒 超卖会导致严重业务问题
点赞计数 最终一致性 ✅ 量级正确即可,几秒延迟无感知
浏览量统计 最终一致性 ✅ 近似值即可,不需要精确到个位

7.2 性能与一致性的权衡

CAP 定理 📐:

分布式系统无法同时满足以下三个特性:

  1. 一致性(Consistency) - 所有节点在同一时间看到相同数据
  2. 可用性(Availability) - 每个请求都能得到响应
  3. 分区容错性(Partition Tolerance) - 网络分区时系统仍能运行

点赞系统的选择 🎯:

objectivec 复制代码
CAP 选择:AP(可用性 + 分区容错性)
   ↓
牺牲强一致性,换取高性能和高可用
   ↓
通过最终一致性保证数据最终正确

数学公式 📊:

假设系统每秒处理点赞请求:
吞吐量(强一致性)= 1 Tlock+Tdisk \text{吞吐量(强一致性)} = \frac{1}{T_{\text{lock}} + T_{\text{disk}}} 吞吐量(强一致性)=Tlock+Tdisk1
吞吐量(最终一致性)= 1Tmemory \text{吞吐量(最终一致性)} = \frac{1}{T_{\text{memory}}} 吞吐量(最终一致性)=Tmemory1

其中:📝

  • Tlock T_{\text{lock}} Tlock 是锁等待时间(~50ms)
  • Tdisk T_{\text{disk}} Tdisk 是磁盘写入时间(~10ms)
  • Tmemory T_{\text{memory}} Tmemory 是内存操作时间(~0.1ms)

计算结果:
吞吐量(强一致性)= 160ms ≈16次/秒 \text{吞吐量(强一致性)} = \frac{1}{60\text{ms}} \approx 16 \text{次/秒} 吞吐量(强一致性)=60ms1≈16次/秒
吞吐量(最终一致性)= 10.1ms =10,000次/秒 \text{吞吐量(最终一致性)} = \frac{1}{0.1\text{ms}} = 10,000 \text{次/秒} 吞吐量(最终一致性)=0.1ms1=10,000次/秒

性能差距625 倍


7.3 业务视角的务实判断

面试官的灵魂拷问 🎤:

"数据全放在 Redis 内存里,万一服务器宕机了,点赞数丢了怎么办?"

正确回答 💡:

"点赞业务对数据一致性的要求并不严苛。对于一个几百万热度的直播间,丢失一两秒的数据(几千次点赞)完全可接受。用户不会在意点赞数是 1,000,000 还是 1,005,000,量级正确就足够了。我们牺牲了几秒钟的强一致性,换取了系统的高吞吐能力,这是一种务实的架构取舍。"

核心洞察 🎯:

优秀的架构师不是追求技术上的完美,而是根据业务特点做出最务实的判断。


参考资料:


8. 完整架构总结 📝 / Complete Architecture Summary

📝 Note: 本章回顾四层架构的完整设计 / This chapter reviews the complete four-layer architecture design.

8.1 四层架构全景

scss 复制代码
┌─────────────────────────────────────────────────────────────┐
│                    用户点赞请求                               │
│                  (10,000 并发/秒)                             │
└─────────────────────────────────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────────┐
│  第一层:前端批量合并 📱                                      │
│  • 本地累积点击,1秒批量提交                                    │
│  • 请求量减少 50 倍                                            │
│  • 代价:秒级延迟(用户无感知)                                   │
└─────────────────────────────────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────────┐
│  第二层:Redis 原子计数器 ⚡                                   │
│  • INCRBY 命令,O(1) 时间复杂度                                │
│  • 单线程模型,无锁竞争                                         │
│  • 性能:10 万+ QPS                                            │
└─────────────────────────────────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────────┐
│  第三层:异步持久化 💾                                         │
│  • Write-Behind 模式                                          │
│  • 每 5 秒批量写入数据库                                        │
│  • 最终一致性,容忍秒级数据丢失                                   │
└─────────────────────────────────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────────┐
│  第四层:分库分表 🗂️                                          │
│  • 按地区/用户维度水平拆分                                      │
│  • 打散写热点,消除单点瓶颈                                      │
│  • 查询时聚合各分片数据                                         │
└─────────────────────────────────────────────────────────────┘

8.2 核心设计原则

原则 说明 应用层
合并(Merge) 减少请求数量 前端批量提交
异步(Async) 提升吞吐量 Redis 缓存 + Write-Behind
打散(Shard) 消除单点瓶颈 分库分表
务实(Pragmatic) 根据业务特点取舍 最终一致性 vs 强一致性

8.3 性能对比

方案 并发能力 延迟 数据安全性 复杂度
直接 UPDATE 数据库 ~1,000 QPS 高(串行) 最高
前端合并 + Redis ~100,000 QPS 极低 中等(可能丢失几秒) 中等
四层完整架构 ~500,000+ QPS 极低 中等

8.4 关键 Takeaways

  1. 行锁串行化是热点更新的最大杀手 🔒:理解数据库锁机制是系统设计的基础
  2. 合并在源头最有效 📱:前端批量合并能从源头消除 90%+ 的并发压力
  3. Redis 原子操作是高并发计数器的标配 ⚡:INCRBY + 单线程模型 = 完美解决方案
  4. Write-Behind 是性能与持久化的最佳平衡 💾:用可接受的数据丢失风险换取极高吞吐
  5. 分库分表是终极武器 🗂️:当单机遇到瓶颈时,水平扩展是唯一出路
  6. 架构设计没有银弹 ⚖️:根据业务特点(如一致性要求)做出务实判断,才是真正的高手

参考资料:


文档来源 :本文档基于抖音视频 扛住千万人点赞的4层架构设计 进行二次解读与技术深化,所有架构设计均为原创实现。

最后更新时间:2026-06-26

相关推荐
笨鸟飞不快5 小时前
从 MVC 到 DDD:一次真实的渐进式迁移实录
后端·架构
这个DBA有点耶1 天前
GROUP BY优化全解:如何写出既不丢数据又飞快的分组查询
数据库·mysql·架构
锋行天下1 天前
我试图优化 Vite 的拆包,结果首屏慢了 10 倍
前端·vue.js·架构
小鼻子的猫1 天前
独立开发 30 天:2.5 万行代码,23 个 Bug,5 次重构——一个 AI 社区的诞生
架构
咖啡八杯1 天前
GoF设计模式——命令模式
java·设计模式·架构
candyTong1 天前
阿里开源 AI Code Review 工具:ocr review 的执行链路解析
javascript·后端·架构
doiito2 天前
【Agent Harness】TPS的“自工程完结”教会了我一件事:别把Bug留给下一道工序
架构·rust
烬羽2 天前
中英文 token 数量差一倍?两段 JS 代码搞懂 LLM 底层是怎么"读"文字的
javascript·程序员·架构