高并发点赞系统架构设计:从数据库行锁到 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
- 视频来源与背景 📺 / Video Source & Background → 说明文档来源与核心问题
- 数据库行锁的串行化灾难 🔒 / Row Lock Serialization Disaster → 为什么直接 UPDATE 会崩
- 第一层:前端批量合并削峰 📱 / Frontend Batch Merging → 从源头减少请求量
- 第二层:Redis 原子计数器 ⚡ / Redis Atomic Counter → 内存级高并发累加
- 第三层:异步持久化策略 💾 / Async Persistence Strategy → Write-Behind 模式落地
- 第四层:分库分表打散热点 🗂️ / Database Sharding → 彻底消除单点瓶颈
- 架构取舍与最终一致性 ⚖️ / Architecture Trade-offs → 性能 vs 一致性的务实选择
- 完整架构总结 📝 / 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):
当大量并发请求同时尝试修改数据库中的 同一行数据 (如某直播间的点赞总数)时,数据库的行级锁机制会强制将这些请求 串行化执行,导致:
- ⏱️ 请求大量超时:后到的请求必须等待前面的事务完成
- 🔗 连接池耗尽:大量等待的请求占用数据库连接
- 💥 系统雪崩:数据库锁死,整个服务不可用
这个问题不仅出现在点赞场景,秒杀扣减库存、热门商品计数器、实时排行榜等场景都面临同样的灾难。
2. 数据库行锁的串行化灾难 🔒 / Row Lock Serialization Disaster
🔒 Note: 本章详解为什么直接
UPDATE会在高并发下导致系统崩溃 / This chapter explains why directUPDATEcauses system crash under high concurrency.
2.1 行级锁的工作原理
在关系型数据库(如 MySQL、PostgreSQL)中,当执行以下 SQL 语句时:
sql
UPDATE live_room SET like_count = like_count + 1 WHERE room_id = 12345;
数据库会执行以下操作:📋
- 定位目标行 🔍:通过索引或全表扫描找到
room_id = 12345的记录 - 施加行级锁 🔒:对该行数据加写锁(Write Lock / Exclusive Lock)
- 执行更新 ✏️:将
like_count字段值加 1 - 记录事务日志 📝:写入 Redo Log 和 Undo Log,保证 ACID 特性
- 提交事务并释放锁 ✅:事务提交后,释放行锁,允许其他事务访问
关键问题 ⚠️:在步骤 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
其中:📝
- Tlast 是最后一个请求的开始时间
- N 是并发请求数量(10,000)
- Ttxn 是单个事务执行时间(50ms)
计算结果:
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 小时
- 大多数人等不及,直接放弃了(请求超时)
这就是行锁串行化导致的雪崩效应。
参考资料:
- MySQL热点行优化技术- 越哥聊AI -- 博客园 ⭐值得阅读
- OceanBase热点更新优化方案 -- OceanBase ⭐值得阅读
- 行锁功过:怎么减少行锁对性能的影响? -- 极客时间 ⭐值得阅读
- 热点数据行UPDATE操作的性能优化 -- 阿里云
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 收益与代价
收益 ✅:
- 请求量锐减 📉:从单用户的 N 次请求降为 1 次(N 倍减少)
- 网络开销降低 🌐:减少 HTTP 连接建立、TLS 握手、请求头传输等开销
- 后端压力骤降 💪:后端只需处理 1/50 的请求量
代价 ⚖️:
- 实时性降低 ⏱️:点赞从"每次点击立即更新"变为"秒级批量更新"
- 数据丢失风险 ⚠️:如果用户在发送前关闭 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 的核心优势 🌟:
- 纯内存操作 💾:数据存储在内存中,读写速度极快(微秒级)
- 单线程模型 🧵:避免多线程锁竞争,天然保证操作的原子性
- 丰富的数据结构 📦:支持 String、Hash、Set、Sorted Set 等
- 高性能 🚀:单机可支撑 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)(常数时间),无论值多大,执行时间不变- 对比数据库
UPDATE: 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 台并行,所有人几乎同时完成操作
参考资料:
- Redis原子计数器incr,防止并发请求 -- 腾讯云 ⭐值得阅读
- 基于Redis INCR命令的高可用原子计数器实现方案 -- 文心快码 ⭐值得阅读
- Redis 实现高并发场景下的计数器设计 -- 稀土掘金 ⭐值得阅读
- 面试官:高并发场景下,如何设计一个不崩的点赞系统? -- 知乎 ⭐值得阅读
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 次点赞:
最大丢失量=每秒点赞数×持久化间隔
最大丢失量=10,000×5=50,000次点赞
业务可接受吗? 🤔
- 对于 1,000,000 次点赞的直播间,丢失 50,000 次 = 5% 误差
- 用户看到的点赞数:1,000,000 vs 1,050,000(实际)
- 用户无感知:量级正确,不影响体验
直观类比 🎰:
想象赌场里的老虎机:
- 每秒有几百人投币
- 不需要每次投币都更新总账本
- 每 5 分钟统计一次总金额就够了
- 即使老虎机突然断电,丢失几分钟的数据也不影响大局
这就是最终一致性(Eventual Consistency) 的务实选择。
参考资料:
- 探秘Redis读写策略:CacheAside、读写穿透、异步写入 -- 阿里云 ⭐值得阅读
- 3种常用的缓存读写策略详解 -- JavaGuide ⭐值得阅读
- 缓存与数据库的一致性方案 -- 博客园 ⭐值得阅读
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 定理 📐:
分布式系统无法同时满足以下三个特性:
- 一致性(Consistency) - 所有节点在同一时间看到相同数据
- 可用性(Availability) - 每个请求都能得到响应
- 分区容错性(Partition Tolerance) - 网络分区时系统仍能运行
点赞系统的选择 🎯:
objectivec
CAP 选择:AP(可用性 + 分区容错性)
↓
牺牲强一致性,换取高性能和高可用
↓
通过最终一致性保证数据最终正确
数学公式 📊:
假设系统每秒处理点赞请求:
吞吐量(强一致性)=Tlock+Tdisk1
吞吐量(最终一致性)=Tmemory1
其中:📝
- Tlock 是锁等待时间(~50ms)
- Tdisk 是磁盘写入时间(~10ms)
- Tmemory 是内存操作时间(~0.1ms)
计算结果:
吞吐量(强一致性)=60ms1≈16次/秒
吞吐量(最终一致性)=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
- 行锁串行化是热点更新的最大杀手 🔒:理解数据库锁机制是系统设计的基础
- 合并在源头最有效 📱:前端批量合并能从源头消除 90%+ 的并发压力
- Redis 原子操作是高并发计数器的标配 ⚡:INCRBY + 单线程模型 = 完美解决方案
- Write-Behind 是性能与持久化的最佳平衡 💾:用可接受的数据丢失风险换取极高吞吐
- 分库分表是终极武器 🗂️:当单机遇到瓶颈时,水平扩展是唯一出路
- 架构设计没有银弹 ⚖️:根据业务特点(如一致性要求)做出务实判断,才是真正的高手
参考资料:
文档来源 :本文档基于抖音视频 扛住千万人点赞的4层架构设计 进行二次解读与技术深化,所有架构设计均为原创实现。
最后更新时间:2026-06-26