高并发系统设计 - 从零到精通
课程目标
学完这套课程,你将能够:
- 理解高并发的核心瓶颈和解决思路
- 掌握高并发架构设计模式
- 熟练使用各种缓存、消息队列、数据库优化技术
- 能够设计QPS 10万+的系统
- 掌握限流、降级、熔断等保护机制
- 面试时自信回答各种高并发问题
第一部分:高并发基础概念
1.1 什么是高并发?
核心概念:
- QPS(Queries Per Second): 每秒查询数
- TPS(Transactions Per Second): 每秒事务数
- 并发数: 同时处理的请求数
- RT(Response Time): 响应时间
| 并发量级参考: | 并发级别 | QPS | 场景示例 |
|---|---|---|---|
| 小型 | 100-1000 | 个人博客、小公司官网 | |
| 中型 | 1000-10000 | 中型企业应用 | |
| 大型 | 1万-10万 | 电商平台、社交应用 | |
| 超大型 | 10万+ | 双11大促、秒杀系统 |
核心公式(面试必背!):
系统容量 = 并发数 × 平均响应时间
QPS = 并发数 / 平均响应时间
举例:
- 100个并发用户,平均响应时间100ms
- QPS = 100 / 0.1 = 1000 QPS
1.2 高并发的三大瓶颈
面试必答:
"高并发系统主要面临三大瓶颈:1)CPU瓶颈,大量计算消耗CPU;2)I/O瓶颈,包括磁盘I/O和网络I/O;3)内存瓶颈,内存不足导致频繁GC或swap。解决思路是:CPU密集型任务并行化,I/O密集型任务异步化,内存不足使用缓存或增加服务器。"
瓶颈一:CPU瓶颈
场景: 复杂计算、加解密、图片处理
解决方法:
- 算法优化:减少不必要的计算
- 缓存:缓存计算结果
- 并行计算:利用多核CPU
- 异步处理:将耗时操作放到后台
// 原始:CPU密集型计算
public Result heavyCalculation(Input input) {
// 复杂计算,耗时长
return doComplexCalculation(input);
}
// 优化:异步计算
public CompletableFuture<Result> heavyCalculationAsync(Input input) {
return CompletableFuture.supplyAsync(() ->
doComplexCalculation(input),
threadPool
);
}
瓶颈二:I/O瓶颈
场景: 数据库查询、文件读写、网络请求
解决方法:
- 使用缓存:Redis、Memcached
- 异步I/O:NIO、Netty
- 批量操作:减少I/O次数
- 连接池:复用连接
// 原始:同步I/O,阻塞
public User getUser(Long userId) {
return repository.findById(userId); // 阻塞等待
}
// 优化:缓存优先
public User getUser(Long userId) {
// 先查缓存
User user = redisTemplate.opsForValue().get("user:" + userId);
if (user != null) {
return user;
}
// 缓存未命中查数据库
user = repository.findById(userId);
// 写入缓存
redisTemplate.opsForValue().set("user:" + userId, user, 3600);
return user;
}
瓶颈三:内存瓶颈
场景: 大对象、缓存过多、内存泄漏
解决方法:
- 对象池:复用对象
- 缓存淘汰策略:LRU、LFU
- 分级缓存:本地缓存+分布式缓存
- 优化数据结构:减少内存占用
1.3 性能测试方法
工具:
- JMeter: 压力测试
- ab (Apache Bench): 简单压测
- wrk: 高性能压测工具
- Gatling: 更强大的压测工具
使用wrk压测:
# 安装wrk
sudo apt install wrk
# 基础压测
wrk -t12 -c400 -d30s http://localhost:8080/api/test
# 参数说明:
# -t12: 12个线程
# -c400: 400个并发连接
# -d30s: 持续30秒
# 输出示例:
# Running 30s test @ http://localhost:8080/api/test
# 12 threads and 400 connections
# Thread Stats Avg Stdev Max +/- Stdev
# Latency 45.32ms 12.45ms 200.34ms 87.23%
# Req/Sec 8.50k 1.23k 10.00k 68.92%
# 3056780 requests in 30.00s, 2.35GB read
# Requests/sec: 101892.60
# Transfer/sec: 80.15MB
第二部分:缓存策略(并发加速器)
2.1 缓存架构设计
三级缓存架构:
请求
↓
本地缓存(Caffeine/Guava) ← 命中返回
↓ 未命中
分布式缓存(Redis/Cluster)
↓ 未命中
数据库(MySQL)
↓
回写缓存
为什么需要多级缓存?
- 本地缓存:速度快(内存),不占用网络带宽
- 分布式缓存:数据一致性好,容量大
- 数据库:持久化存储
2.2 本地缓存(Caffeine/Guava)
Caffeine(推荐):
// Caffeine缓存配置
Cache<String, User> userCache = Caffeine.newBuilder()
.maximumSize(10000) // 最大10000条
.expireAfterWrite(10, TimeUnit.MINUTES) // 写入10分钟过期
.refreshAfterWrite(5, TimeUnit.MINUTES) // 5分钟刷新
.recordStats() // 记录统计信息
.build(key -> loadUserFromDB(key)); // 缓存加载器
// 使用
User user = userCache.get("user:123");
// 缓存统计
CacheStats stats = userCache.stats();
System.out.println("命中率: " + stats.hitRate());
System.out.println("加载数: " + stats.loadCount());
面试问题:本地缓存的缺点?
回答:
"本地缓存的缺点:1)容量受限,受限于服务器内存;2)数据不一致,多台服务器缓存可能不同;3)无法主动失效,需要等待过期;4)内存占用过高影响应用稳定性。因此通常作为一级缓存,配合分布式缓存使用。"
2.3 分布式缓存(Redis)
Redis数据类型和使用场景
| 数据类型 | 适用场景 | 示例 |
|---|---|---|
| String | 简单KV、计数器 | 用户信息、点赞数 |
| Hash | 对象存储 | 商品详情、用户画像 |
| List | 消息队列、列表 | 评论列表、待办任务 |
| Set | 去重、交集差集 | 标签、共同关注 |
| ZSet | 排行榜、延迟队列 | 排行榜、定时任务 |
| Bitmap | 位统计 | 用户签到、在线状态 |
| HyperLogLog | 基数统计 | UV统计(精准度99%) |
Redis集群方案
方案一:主从复制 + 哨兵
Master
↓ 写入
Slave1 ← 复制
Slave2 ← 复制
↓ 读取
Sentinel 监控
配置示例:
# redis.conf - Master
port 6379
requirepass yourpassword
# redis.conf - Slave
port 6380
replicaof master_ip 6379
masterauth yourpassword
# sentinel.conf - Sentinel
port 26379
sentinel monitor mymaster master_ip 6379 2
sentinel auth-pass mymaster yourpassword
sentinel down-after-milliseconds mymaster 5000
方案二:Redis Cluster(分片集群)
客户端
↓
Slot分片 (0-16383)
↓
┌─────┬─────┬─────┬─────┬─────┬─────┐
│Slot0│Slot1│Slot2│Slot3│Slot...│Slot │
│ - │ - │ - │ - │ -27 │16383│
└──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┘
↓ ↓ ↓ ↓ ↓ ↓
Node1 Node2 Node3 Node1 Node2 Node3
面试回答:
"Redis集群有两种方案:主从复制+哨兵适合中小规模,读写分离,高可用;Redis Cluster适合大规模,自动分片,无中心节点。对于高并发场景,我选择Redis Cluster,因为它支持水平扩展,故障时自动迁移数据。同时配合本地缓存减少Redis压力。"
2.4 缓存穿透、击穿、雪崩(面试三剑客!)
问题一:缓存穿透
现象: 查询不存在的数据,请求全部打到数据库
请求查询 user_id=-1
↓
缓存不存在
↓
数据库也不存在
↓
再次请求,继续查数据库
↓
数据库被打爆!
解决方案:
方案1:缓存空值
public User getUser(Long userId) {
String key = "user:" + userId;
// 先查缓存
User user = redisTemplate.opsForValue().get(key);
if (user != null) {
return user; // 返回空值或真实值
}
// 查数据库
user = userRepository.findById(userId);
// 即使是null也缓存
redisTemplate.opsForValue().set(key, user, 300, TimeUnit.SECONDS);
return user;
}
方案2:布隆过滤器
// 初始化布隆过滤器
BloomFilter<Long> bloomFilter = BloomFilter.create(
Funnels.longFunnel(),
1000000, // 预计数据量
0.01 // 误判率1%
);
// 加载所有存在的ID到布隆过滤器
for (Long userId : existingUserIds) {
bloomFilter.put(userId);
}
// 查询时先检查
public User getUser(Long userId) {
// 布隆过滤器判断不存在,直接返回
if (!bloomFilter.mightContain(userId)) {
return null;
}
// 可能存在,继续查缓存和数据库
return getUserFromCacheOrDB(userId);
}
面试回答:
"缓存穿透的解决方案:1)缓存空值,将查询为空的结果也缓存,设置较短过期时间;2)布隆过滤器,将所有可能存在的key预加载到布隆过滤器,查询时先过滤肯定不存在的key。布隆过滤器有误判率但性能极高,适合海量数据场景。"
问题二:缓存击穿
现象: 热点key过期,大量请求同时打到数据库
时间点T: 热点key过期
↓
10000个并发请求同时到达
↓
全部查数据库
↓
数据库压力暴增
解决方案:
方案1:互斥锁(分布式锁)
public User getUser(Long userId) {
String key = "user:" + userId;
// 先查缓存
User user = redisTemplate.opsForValue().get(key);
if (user != null) {
return user;
}
// 获取分布式锁
String lockKey = "lock: user:" + userId;
try {
boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
if (locked) {
// 获取锁成功,查数据库
user = userRepository.findById(userId);
// 写入缓存,设置过期时间
redisTemplate.opsForValue().set(key, user, 3600, TimeUnit.SECONDS);
} else {
// 获取锁失败,等待一下再查缓存
Thread.sleep(100);
return getUser(userId);
}
} finally {
// 释放锁
redisTemplate.delete(lockKey);
}
return user;
}
方案2:逻辑过期(热点永不过期)
public User getUser(Long userId) {
String key = "user:" + userId;
// 查缓存
User user = redisTemplate.opsForValue().get(key);
// 判断是否逻辑过期
if (user != null && !isExpired(user)) {
return user;
}
// 异步刷新缓存
if (user == null || isExpired(user)) {
CompletableFuture.runAsync(() -> refreshCache(userId));
}
// 返回旧数据
return user;
}
面试回答:
"缓存击穿是热点key过期导致的数据库压力暴增。解决方案:1)互斥锁,只允许一个线程查数据库重建缓存,其他线程等待;2)逻辑过期,缓存永不过期,通过后台线程定期刷新,返回旧数据的同时异步刷新。互斥锁保证一致性但性能较低,逻辑过期性能好但数据有短暂延迟。"
问题三:缓存雪崩
现象: 大量key同时过期,数据库压力暴增
时间点T: 10000个key同时过期
↓
所有请求查数据库
↓
数据库崩溃
解决方案:
方案1:随机过期时间
public void setCache(String key, Object value) {
// 基础过期时间1小时
int baseExpire = 3600;
// 随机增加0-30分钟
int randomExpire = (int) (Math.random() * 1800);
redisTemplate.opsForValue().set(
key,
value,
baseExpire + randomExpire,
TimeUnit.SECONDS
);
}
方案2:缓存预热
@Scheduled(cron = "0 0 2 * * ?") // 每天凌晨2点
public void warmUpCache() {
// 加载热点数据到缓存
List<User> hotUsers = userRepository.findHotUsers();
for (User user : hotUsers) {
redisTemplate.opsForValue().set(
"user:" + user.getId(),
user,
3600,
TimeUnit.SECONDS
);
}
}
方案3:多级缓存 + 限流保护
// 一级Caffeine本地缓存
// 二级Redis分布式缓存
// 查询时先查本地,再查Redis,最后查DB
// 并在DB前加限流
面试回答:
"缓存雪崩是大量key同时过期导致的。解决方案:1)设置随机过期时间,避免同时过期;2)缓存预热,在系统低峰期提前加载热点数据;3)多级缓存+限流,本地缓存作为第一道防线,配合限流保护数据库。实际项目中我们组合使用多种方案,确保系统稳定性。"
2.5 缓存更新策略(面试必问!)
策略一:Cache Aside(旁路缓存,最常用)
读缓存:先查缓存,未命中查数据库,回写缓存
写数据:先更新数据库,再删除缓存
// 读
public User getUser(Long id) {
User user = cache.get(id);
if (user == null) {
user = db.findById(id);
cache.put(id, user);
}
return user;
}
// 写
public void updateUser(User user) {
db.update(user); // 先更新DB
cache.remove(user.getId()); // 再删除缓存
}
为什么是删除而不是更新?
"删除缓存是因为:1)并发写时可能导致数据不一致;2)删除开销小;3)下次读取时自动加载最新数据。只有在缓存更新计算非常昂贵时才考虑更新缓存。"
策略二:Write Through(写穿透)
写数据:同时写入缓存和数据库
读缓存:直接读缓存
public void updateUser(User user) {
db.update(user); // 写数据库
cache.put(user.getId(), user); // 写缓存
}
策略三:Write Behind(异步写回)
写数据:只写缓存,异步写数据库
读缓存:直接读缓存
适用场景: 写入频繁、一致性要求不高
第三部分:消息队列(异步解耦神器)
3.1 为什么需要消息队列?
核心作用:
- 异步处理: 耗时操作异步执行
- 削峰填谷: 平滑流量峰值
- 解耦: 生产者和消费者解耦
场景对比:
场景1:同步处理(慢)
用户下单
↓
创建订单 (50ms)
↓
扣减库存 (50ms)
↓
发送短信 (100ms) ← 耗时操作
↓
发送通知 (100ms) ← 耗时操作
↓
返回 (300ms) ← 响应慢
场景2:异步处理(快)
用户下单
↓
创建订单 (50ms)
↓
扣减库存 (50ms)
↓
发送到MQ (10ms)
↓
返回 (110ms) ← 响应快
MQ消费者异步:
↓
发送短信
发送通知
3.2 消息队列选型
| MQ | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| RabbitMQ | 功能丰富、可靠性强 | 性能一般、资源占用高 | 复杂业务、可靠投递 |
| Kafka | 吞吐量极高、持久化好 | 实时性一般、消费复杂 | 日志收集、大数据 |
| RocketMQ | 功能全面、阿里系 | 学习曲线陡峭 | 电商、金融 |
| Pulsar | 云原生、存算分离 | 较新、生态一般 | 云架构 |
面试回答:
"我们选择RabbitMQ作为消息队列,因为它:1)支持多种消息模式(直连、主题、RPC、广播);2)消息可靠性高,支持持久化、ACK机制、死信队列;3)管理界面友好,运维方便;4)社区活跃,文档完善。如果需要超大数据吞吐,会选择Kafka。"
3.3 RabbitMQ实战
消息模型
1. 直连模式(Direct)
// 生产者
rabbitTemplate.convertAndSend(
"order-exchange",
"create",
orderMessage
);
// 消费者
@RabbitListener(
queues = "order.create.queue",
queueBinding = @QueueBinding(
value = @Queue(value = "order.create.queue"),
exchange = @Exchange(value = "order-exchange", type = "direct"),
key = "create"
)
)
public void handleOrderCreate(OrderMessage message) {
// 处理订单创建
}
2. 主题模式(Topic)
// 生产者
rabbitTemplate.convertAndSend(
"user-exchange",
"user.created", // 通配符匹配 user.*
userMessage
);
// 消费者
@RabbitListener(
bindings = @QueueBinding(
value = @Queue(value = "user-event.queue"),
exchange = @Exchange(value = "user-exchange", type = "topic"),
key = "user.*" // 匹配所有用户事件
)
)
public void handleUserEvent(UserMessage message) {
// 处理用户事件
}
3. 延迟队列
// 发送延迟消息(5分钟后执行)
Message message = MessageBuilder
.withPayload(task)
.setHeader("x-delay", 300000) // 5分钟延迟
.build();
rabbitTemplate.send("delay-exchange", "delay-key", message);
消息可靠性保证
1. 消息持久化
# application.yml
spring:
rabbitmq:
publisher-confirm-type: correlated # 发送确认
publisher-returns: true # 退回确认
listener:
simple:
acknowledge-mode: manual # 手动ACK
2. 死信队列(处理失败消息)
@Bean
public Queue deadLetterQueue() {
return QueueBuilder.durable("order.dead-letter.queue").build();
}
@Bean
public Queue orderQueue() {
return QueueBuilder.durable("order.queue")
.withArgument("x-dead-letter-exchange", "dead-letter-exchange")
.withArgument("x-dead-letter-routing-key", "dead-letter")
.build();
}
@RabbitListener(queues = "order.dead-letter.queue")
public void handleDeadLetterMessage(Message message) {
// 处理失败消息,记录日志或重试
log.error("处理失败消息: {}", message);
}
3. 消息幂等性(防止重复消费)
@RabbitListener(queues = "order.queue")
public void handleOrder(OrderMessage message) {
// 使用唯一ID保证幂等性
String messageId = message.getId();
// 检查是否已处理
if (redisTemplate.hasKey("processed:" + messageId)) {
return; // 已处理,跳过
}
try {
// 处理消息
processOrder(message);
// 标记已处理
redisTemplate.opsForValue().set(
"processed:" + messageId,
"1",
24, TimeUnit.HOURS
);
} catch (Exception e) {
// 处理失败,抛出异常触发重试
throw new RuntimeException(e);
}
}
面试回答:
"消息可靠性保证从三个方面实现:1)生产端,开启confirm机制确保消息发送成功,开启return机制处理路由失败;2)服务端,消息持久化到磁盘,集群部署保证高可用;3)消费端,手动ACK确认,失败消息进入死信队列。同时使用唯一ID实现幂等性,防止重复消费。"
3.4 消息堆积处理(面试高频)
现象: 消费速度 < 生产速度,队列堆积
解决方案:
方案1:增加消费者
# application.yml - 增加并发消费
spring:
rabbitmq:
listener:
simple:
concurrency: 10 # 最小并发数
max-concurrency: 50 # 最大并发数
方案2:批量消费
@RabbitListener(
queues = "order.queue",
containerFactory = "batchContainerFactory"
)
public void handleBatchOrders(List<OrderMessage> messages) {
// 批量处理,减少数据库操作
batchProcessOrders(messages);
}
@Bean
public SimpleRabbitListenerContainerFactory batchContainerFactory() {
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
factory.setConnectionFactory(connectionFactory);
factory.setBatchListener(true); // 开启批量
factory.setBatchSize(100); // 每次100条
factory.setConsumerBatchEnabled(true);
return factory;
}
方案3:临时扩容队列
// 1. 创建临时队列,增加分区
// 2. 停止原消费者
// 3. 使用脚本将堆积消息转移到临时队列
// 4. 启动更多消费者消费临时队列
方案4:丢弃非关键消息
// 监控队列长度
if (queueSize > threshold) {
// 删除低优先级消息
rabbitTemplate.purgeQueue("low-priority.queue");
}
面试回答:
"消息堆积处理:1)紧急情况下增加消费者数量;2)使用批量消费减少网络开销和数据库操作;3)监控队列长度,超过阈值时告警;4)非关键消息可丢弃,保护核心业务;5)长期方案是优化消费者性能,增加分区。实际项目中我们设置监控告警,队列堆积超过1万条时自动扩容消费者。"
第四部分:数据库优化(并发瓶颈突破)
4.1 读写分离
架构:
应用
↓
主库(写)
↓ 复制
从库1(读)
从库2(读)
从库3(读)
实现方式:
方案1:Spring动态数据源
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDataSourceType();
}
}
// 读操作
@Service
public class UserService {
@DataSource("slave")
public User getUser(Long id) {
return userRepository.findById(id);
}
}
// 写操作
@Service
public class UserService {
@DataSource("master")
public void saveUser(User user) {
userRepository.save(user);
}
}
方案2:ShardingSphere(推荐)
# application.yml
spring:
shardingsphere:
datasource:
names: master,slave1,slave2
master:
type: com.zaxxer.hikari.HikariDataSource
url: jdbc:mysql://master:3306/db
slave1:
type: com.zaxxer.hikari.HikariDataSource
url: jdbc:mysql://slave1:3306/db
slave2:
type: com.zaxxer.hikari.HikariDataSource
url: jdbc:mysql://slave2:3306/db
rules:
readwrite-splitting:
data-sources:
readwrite_ds:
write-data-source-name: master
read-data-source-names: slave1,slave2
load-balancer-name: round_robin
props:
load-balance-algorithm-type: round_robin
面试问题:读写分离的数据一致性问题?
回答:
"读写分离存在数据一致性问题:主库写入后,从库可能有延迟。解决方案:1)强制读主库,关键业务读写都走主库;2)缓存兜底,读取时先查缓存;3)接受短暂不一致,如用户更新头像后几秒内看不到新头像;4)使用半同步复制,主库等待至少一个从库确认才返回。实际项目中我们结合业务场景选择合适的方案。"
4.2 分库分表
为什么需要分库分表?
- 单表数据量超过1000万,查询变慢
- 单机存储空间不足
- 单机CPU/内存瓶颈
分表策略:
策略1:范围分表
-- 按ID范围分表
orders_0: 0 - 100万
orders_1: 100万 - 200万
orders_2: 200万 - 300万
...
// 路由算法
public String getTable(Long orderId) {
// 假设每张表100万条
int tableIndex = (int)(orderId / 1000000);
return "orders_" + tableIndex;
}
策略2:Hash分表(推荐)
// 哈希分表
public String getTable(Long orderId) {
// 对订单ID取模,均匀分布
int tableCount = 10;
int tableIndex = (int)(orderId % tableCount);
return "orders_" + tableIndex;
}
ShardingSphere配置:
spring:
shardingsphere:
rules:
sharding:
tables:
orders:
actual-data-nodes: ds_${0..1}.orders_${0..9}
table-strategy:
standard:
sharding-column: order_id
sharding-algorithm-name: order_mod
sharding-algorithms:
order_mod:
type: MOD
props:
sharding-count: 10
面试问题:分库分表后的join问题?
回答:
"分库分表后跨表join确实是个问题。解决方案:1)将关联数据放在同一个分片,使用相同的分片键;2)应用层join,先查询主表获取关联ID,再分别查询子表;3)使用ES或宽表,将关联数据冗余存储;4)对于不需要实时性的报表,使用数据仓库定时聚合。实际项目中我们优先设计表结构避免跨分片join。"
4.3 索引优化
索引设计原则:
- 最左前缀原则: 联合索引从左向右匹配
- 选择性高的列在前: 区分度高的列优先索引
- 避免覆盖索引失效: 不要在索引列上做运算
正确示例:
-- 好的索引
CREATE INDEX idx_user_time ON orders(user_id, create_time);
-- 好的查询
SELECT * FROM orders
WHERE user_id = 123
AND create_time > '2024-01-01';
-- 坏的查询(索引失效)
SELECT * FROM orders
WHERE create_time > '2024-01-01'; -- 跳过最左列
索引优化案例:
-- 问题查询:慢
SELECT * FROM orders
WHERE user_id = 123
AND status = 'PAID'
AND create_time > '2024-01-01'
ORDER BY create_time DESC
LIMIT 10;
-- 优化:添加覆盖索引
CREATE INDEX idx_user_status_time ON orders(user_id, status, create_time)
INCLUDE (id, amount); -- 覆盖索引
-- 查询可以直接从索引获取数据,无需回表
面试回答:
"索引优化原则:1)区分度高的列优先建索引;2)联合索引遵循最左前缀原则;3)避免在索引列上做运算导致索引失效;4)使用覆盖索引减少回表;5)定期分析慢查询,使用EXPLAIN分析执行计划。实际项目中,我们监控慢查询日志,定期优化索引。"
4.4 连接池优化
HikariCP(推荐):
spring:
datasource:
hikari:
minimum-idle: 10 # 最小空闲连接
maximum-pool-size: 100 # 最大连接数
connection-timeout: 30000 # 连接超时
idle-timeout: 600000 # 空闲超时
max-lifetime: 1800000 # 连接最大生命周期
leak-detection-threshold: 60000 # 连接泄露检测
连接池配置计算:
最大连接数 = (CPU核心数 * 2) + 有效磁盘数
例如:
- 8核CPU,1个磁盘
- 最大连接数 = 8 * 2 + 1 = 17
但实际配置要结合应用场景:
- 读多写少:可以增加连接数
- CPU密集型:减少连接数避免上下文切换
第五部分:限流、降级、熔断(系统保护三剑客)
5.1 限流
限流算法:
算法1:计数器(固定窗口)
public class RateLimiter {
private final AtomicInteger counter = new AtomicInteger(0);
private final long interval;
private final int limit;
public boolean allow() {
if (counter.get() >= limit) {
return false;
}
counter.incrementAndGet();
return true;
}
}
缺点: 边界问题,2倍流量可能通过
算法2:滑动窗口(推荐)
public class SlidingWindowRateLimiter {
private final ConcurrentLinkedDeque<Long> timestamps = new ConcurrentLinkedDeque<>();
private final long windowSize; // 窗口大小(毫秒)
private final int limit; // 限制数量
Q public boolean allow() {
long now = System.currentTimeMillis();
long windowStart = now - windowSize;
// 清理过期请求
while (!timestamps.isEmpty() && timestamps.peek() < windowStart) {
timestamps.poll();
}
// 检查是否超过限制
if (timestamps.size() >= limit) {
return false;
}
timestamps.add(now);
return true;
}
}
算法3:令牌桶(生产消费模型)
public class TokenBucketRateLimiter {
private final long capacity; // 桶容量
private final long rate; // 令牌生成速率(个/毫秒)
private long tokens; // 当前令牌数
private long lastRefillTime; // 上次填充时间
public synchronized boolean allow() {
long now = System.currentTimeMillis();
long elapsed = now - lastRefillTime;
// 填充令牌
tokens = Math.min(capacity, tokens + elapsed * rate);
lastRefillTime = now;
;
if (tokens >= 1) {
tokens--;
return true;
}
return false;
}
}
算法4:漏桶(恒定输出)
public class LeakyBucketRateLimiter {
private final long capacity; // 桶容量
private final long rate; // 漏水速率(个/毫秒)
private long water; // 当前水量
private long lastLeakTime; // 上次漏水时间
public synchronized boolean allow() {
long now = System.currentTimeMillis();
long elapsed = now - lastLeakTime;
// 漏水
water = Math.max(0, water - elapsed * rate);
lastLeakTime = now;
if (water < capacity) {
water++;
return true;
}
return false;
}
}
实战:Sentinel限流
# application.yml
spring:
cloud:
sentinel:
datasource:
flow:
nacos:
server-addr: localhost:8848
data-id: sentinel-flow-rules
rule-type: flow
Nacos配置规则:
[{
"resource": "/api/order/create",
"limitApp": "default",
"grade": 1,
"count": 1000,
"strategy": 0,
"controlBehavior": 0,
"clusterMode": false
}]
Java代码:
@RestController
public class OrderController {
@GetMapping("/api/order/create")
public Result createOrder(@RequestParam Long userId) {
try (Entry entry = SentinelResourceUtil.enter("createOrder")) {
// 业务逻辑
return Result.success(orderService.create(userId));
} catch (BlockException e) {
// 限流处理
返回 Result.error("系统繁忙,请稍后重试");
}
}
}
面试回答:
"限流我们使用阿里Sentinel,支持QPS限流、并发限流、Warm Up预热、匀速排队等多种模式。针对不同接口设置不同限流规则:核心接口严格限流保证稳定性,非核心接口可以适当放行。限流后返回友好提示,引导用户重试。通过Nacos动态配置规则,无需重启服务。"
5.2 降级
降级策略:
| 降级类型 | 适用场景 | 处理方式 |
|---|---|---|
| 自动降级 | 超时、异常、限流 | 返回默认值、缓存值 |
| 手动降级 | 运维开关 | 关闭非核心功能 |
| 兜底数据 | 数据获取失败 | 返回历史数据 |
实现方式:
方式1:注解降级
@RestController
public class UserController {
@HystrixCommand(
fallbackMethod = "getUserFallback",
commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "3000")
}
)
@GetMapping("/user/{id}")
public User getUser(@PathVariable Long id) {
return userService.getUser(id);
}
// 降级方法
public User getUserFallback(@PathVariable Long id) {
// 返回缓存数据或默认值
return cacheService.getUserFromCache(id);
}
}
方式2:Sentinel降级
@SentinelResource(value = "getUser", fallback = "getUserFallback")
public User getUser(Long id) {
return userRepository.findById(id);
}
// 降级方法
public User getUserFallback(Long id, Throwable ex) {
log.warn("getUser降级, id={}, ex={}", id, ex.getMessage());
return User.builder().id(id).name("默认用户").build();
}
降级策略配置:
// 配置降级规则
List<DegradeRule> rules = new ArrayList<>();
DegradeRule rule = new DegradeRule();
rule.setResource("getUser");
rule.setGrade(RuleConstant.DEGRADE_GRADE_EXCEPTION_RATIO); // 异常比例
rule.setCount(0.5); // 异常比例超过50%
rule.setTimeWindow(10); // 统计窗口10秒
rule.setMinRequestAmount(5); // 最小请求数5
rules.add(rule);
DegradeRuleManager.loadRules(rules);
面试回答:
"降级策略包括:1)超时降级,超过3秒未响应自动降级;2)异常降级,异常比例超过50%触发降级;3)自动降级,结合监控指标自动触发;4)手动降级,运维人员开关控制。降级后返回缓存数据或默认值,保证核心功能可用。非核心功能如推荐、评论优先降级。"
5.3 熔断
熔断机制(电路熔断器):
正常状态 ──(失败率>阈值)──> 熔断状态
↑ ↓
└────────(半开尝试)─────────┘
熔断状态流转:
- Closed(关闭): 正常请求通过,统计失败率
- Open(打开): 熔断器打开,请求直接失败
- Half-Open(半开): 尝试恢复,允许少量请求通过
Resilience4j熔断:
// 配置熔断器
CircuitBreakerConfig config = CircuitBreakerConfig.custom()
.failureRateThreshold(50) // 失败率50%
.waitDurationInOpenState(Duration.ofSeconds(10)) // 熔断10时间
.slidingWindowSize(10) // 滑动窗口大小
.build();
CircuitBreaker circuitBreaker = CircuitBreaker.of("userService", config);
// 使用熔断器
Supplier<User> supplier = CircuitBreaker.decorateSupplier(
circuitBreaker,
() -> userService.getUser(id)
);
try {
User user = supplier.get();
} catch (CallNotPermittedException e) {
// 熔断打开,执行降级
user = getUserFromCache(id);
} catch (Exception e) {
// 其他异常
throw e;
}
面试回答:
"熔断使用Resilience4j实现,配置失败率阈值50%、熔断时间10秒。当失败率超过阈值时熔断器打开,直接拒绝请求;熔断时间后进入半开状态,允许少量请求通过尝试恢复;如果恢复成功则关闭熔断器。熔断和降级配合使用,保护系统不被雪崩拖垮。"
第六部分:异步处理(提升并发能力)
6.1 异步方案对比
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 线程池 | 简单直接 | 资源有限、阻塞 | 简单异步任务 |
| CompletableFuture | 链式调用、组合灵活 | 学习曲线 | 复杂异步编排 |
| 消息队列 | 解耦、削峰 | 系统复杂 | 跨服务异步 |
| 事件驱动 | 松耦合、扩展性好 | 调试困难 | 复杂业务流 |
6.2 CompletableFuture(推荐)
基础使用:
// 异步执行任务
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
// 耗时操作
Thread.sleep(1000);
return "Hello";
});
// 获取结果
String result = future.get(); // 阻塞等待
String result = future.get(2, TimeUnit.SECONDS); // 带超时
链式调用:
CompletableFuture.supplyAsync(() -> {
// 第一步:获取用户
return userService.getUser(userId);
}, executor)
.thenApplyAsync(user -> {
// 第二步:获取用户订单
return orderService.getOrders(user.getId());
}, executor)
.thenAcceptAsync(orders -> {
// 第三步:发送通知
notificationService.send(orders);
}, executor)
.exceptionally(ex -> {
// 异常处理
log.error("异步任务失败", ex);
return null;
});
组合多个Future:
// 场景:同时获取用户、商品、优惠券,再创建订单
CompletableFuture<User> userFuture = CompletableFuture.supplyAsync(
() -> userService.getUser(userId)
);
CompletableFuture<Product> productFuture = CompletableFuture.supplyAsync(
() -> productService.get(productId)
);
CompletableFuture<Coupon> couponFuture = CompletableFuture.supplyAsync(
() -> couponService.getCoupon(couponId)
);
// 等待所有完成
CompletableFuture.allOf(userFuture, productFuture, couponFuture)
.thenRun(() -> {
// 创建订单
Order order = orderService.create(
userFuture.join(),
productFuture.join(),
couponFuture.join()
);
});
面试回答:
"异步处理我们使用CompletableFuture,它支持链式调用和组合操作。将耗时操作如发送通知、计算积分等异步执行,提升接口响应速度。同时配置独立的线程池,避免影响核心业务。对于跨服务异步,使用消息队列解耦。通过异步化,接口响应时间从500ms降低到100ms。"
6.3 异步事件(Spring Event)
// 定义事件
public class OrderCreatedEvent extends ApplicationEvent {
private final Order order;
public OrderCreatedEvent(Object source, Order order) {
super(source);
this.order = order;
}
}
// 发布事件
@Service
public class OrderService {
@Autowired
private ApplicationEventPublisher eventPublisher;
public void createOrder(Order order) {
// 创建订单
orderRepository.save(order);
// 发布事件(异步)
eventPublisher.publishEvent(new OrderCreatedEvent(this, order));
}
}
// 监听事件
@Service
public class OrderEventListener {
@EventListener
@Async // 异步处理
public void handleOrderCreated(OrderCreatedEvent event) {
Order order = event.getOrder();
// 发送通知
notificationService.send(order);
// 更新积分
pointService.add(order.getUserId(), order.getAmount());
}
}
第七部分:实战案例(面试题库)
案例1:秒杀系统设计
需求:
- QPS 10万+
- 不能超卖
- 高可用
架构设计:
用户
↓
[限流层] Sentinel限流
↓
[缓存层] Redis预扣库存
↓
[消息层] MQ异步下单
↓
[服务层) 下单、扣减库存
↓
[数据层] MySQL持久化
核心代码:
Redis预扣库存(原子操作):
@Service
public class SeckillService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
public boolean preDeductStock(Long productId, Long userId) {
String key = "seckill:stock:" + productId;
// 使用Lua脚本保证原子性
String script = "local stock = redis.call('get', KEYS[1]) " +
"if stock and tonumber(stock) > 0 then " +
" redis.call('decr', KEYS[1]) " +
" return 1 " +
"else " +
" return 0 " +
"end";
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
redisScript.setScriptText(script);
redisScript.setResultType(Long.class);
Long result = redisTemplate.execute(
redisScript,
Collections.singletonList(key)
);
return result == 1;
}
}
Redisson分布式锁:
@Service
public class SeckillService {
@Autowired
private RedissonClient redissonClient;
public boolean seckill(Long productId, Long userId) {
String lockKey = "seckill:lock:" + productId;
RLock lock = redissonClient.getLock(lockKey);
try {
// 尝试获取锁,等待3秒,锁10秒自动释放
if (lock.tryLock(3, 10, TimeUnit.SECONDS)) {
// 检查是否已购买
if (hasPurchased(productId, userId)) {
return false;
}
// 预扣库存
if (preDeductStock(productId, userId)) {
// 发送消息异步下单
sendToMQ(productId, userId);
return true;
}
}
return false;
} finally {
lock.unlock();
}
}
}
MySQL防超卖(乐观锁):
@Service
public class OrderService {
@Transactional
public void createOrder(Long productId, Long userId) {
// 乐观锁扣减库存
int updated = productRepository.deductStock(productId, 1);
if (updated == 0) {
throw new BusinessException("库存不足");
}
// 创建订单
Order order = Order.builder()
.productId(productId)
.userId(userId)
.status("PENDING")
.build();
orderRepository.save(order);
}
}
// Repository
@Modifying
@Query("UPDATE Product p SET p.stock = p.stock - :count " +
"WHERE p.id = :productId AND p.stock >= :count")
int deductStock(@Param("productId") Long productId,
@Param("count") Integer count);
面试回答:
"秒杀系统核心是防止超卖和高并发。架构上采用:1)限流层,SentinelQPS限流;2)缓存层,Redis预扣库存,使用Lua脚本保证原子性;3)消息层,MQ异步处理下单;4)数据层,MySQL乐观锁扣库存。通过多级防护:Redis保证高并发预扣,MySQL保证数据一致性,MQ异步处理削峰。同时使用分布式锁防止重复购买。"
案例2:评论系统设计
需求:
- 文章评论树形结构
- 支持无限嵌套
- 高并发读写
表设计:
CREATE TABLE comments (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
article_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
content TEXT NOT NULL,
parent_id BIGINT DEFAULT 0, -- 父评论ID,0表示顶级评论
root_id BIGINT DEFAULT 0, -- 根评论ID,用于查询整个评论树
level INT DEFAULT 0, -- 评论层级
reply_to BIGINT DEFAULT 0, -- 回复的用户ID
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
is_deleted TINYINT DEFAULT 0,
INDEX idx_article (article_id),
INDEX idx_root (root_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
查询方案:
方案1:递归查询(少量数据)
-- MySQL 8.0+ 使用CTE递归查询
WITH RECURSIVE comment_tree AS (
-- 查询顶级评论
SELECT * FROM comments
WHERE article_id = 123 AND parent_id = 0
UNION ALL
-- 递归查询子评论
SELECT c.* FROM comments c
JOIN comment_tree ct ON c.parent_id = ct.id
)
SELECT * FROM comment_tree ORDER BY create_time;
方案2:闭包表(大量数据)
-- 闭包表存储所有祖先-后代关系
CREATE TABLE comment_closure (
ancestor_id BIGINT NOT NULL,
descendant_id BIGINT NOT NULL,
depth INT NOT NULL,
PRIMARY KEY (ancestor_id, descendant_id)
);
-- 查询某个评论的所有子孙
SELECT c.*
FROM comments c
JOIN comment_closure cc ON c.id = cc.descendant_id
WHERE cc.ancestor_id = 123
ORDER BY c.create_time;
-- 查询某个评论的所有祖先
SELECT c.*
FROM comments c
JOIN comment_closure cc ON c.id = cc.ancestor_id
WHERE cc.descendant_id = 456
ORDER BY cc.depth DESC;
方案3:物化路径(推荐)
-- 添加path字段存储路径
ALTER TABLE comments ADD COLUMN path VARCHAR(500);
-- 插入数据
-- 顶级评论:path = '1'
-- 二级评论:path = '1.2'
-- 三级评论:path = '1.2.3'
-- 查询某个评论的所有子孙
SELECT * FROM comments
WHERE article_id = 123
AND path LIKE '1.%' -- 查询评论1的所有子孙
ORDER BY path;
-- 查询某个评论的所有祖先
SELECT * FROM comments
WHERE article_id = 123
AND FIND_IN_SET('1', REPLACE(path, '.', ',')) > 0
ORDER BY path;
缓存优化:
@Service
public class CommentService {
@Cacheable(value = "comment:tree", key = "#articleId")
public List<Comment> getCommentTree(Long articleId) {
// 查询评论树
return queryCommentTree(articleId);
}
@CacheEvict(value = "comment:tree", key = "#comment.articleId")
public Comment createComment(Comment comment) {
// 创建评论
Comment saved = commentRepository.save(comment);
// 更新path
updateCommentPath(saved);
return saved;
}
}
面试回答:
"评论系统设计考虑树形结构和并发性能。表设计存储parent_id和path,path使用物化路径方案存储'1.2.3'格式,便于查询所有子孙和祖先。查询时使用path LIKE '1.%'快速获取子树。使用Redis缓存整个评论树,增删改时失效缓存。对于超高并发场景,使用ES存储评论,支持全文搜索和复杂查询。"
案例3:点赞系统设计
需求:
- 用户可以点赞/取消点赞
- 实时获取点赞数
- 查询用户是否点赞
存储方案:
Redis方案(推荐):
@Service
public class LikeService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
private static final String LIKE_COUNT_KEY = "like:count:";
private static final String USER_LIKED_KEY = "user:liked:";
// 点赞
public boolean like(Long userId, Long targetId) {
String userLikedKey = USER_LIKED_KEY + userId + ":" + targetId;
// 检查是否已点赞
Boolean isLiked = redisTemplate.opsForValue().setIfAbsent(userLikedKey, "1", 30, TimeUnit.DAYS);
if (Boolean.TRUE.equals(isLiked)) {
// 增加点赞数
redisTemplate.opsForValue().increment(LIKE_COUNT_KEY + targetId);
return true;
}
return false;
}
// 取消点赞
public boolean unlike(Long userId, Long targetId) {
String userLikedKey = USER_LIKED_KEY + userId + ":" + targetId;
// 检查是否点赞
String value = (String) redisTemplate.opsForValue().get(userLikedKey);
if (value != null) {
// 删除点赞记录
redisTemplate.delete(userLikedKey);
// 减少点赞数
redisTemplate.opsForValue().decrement(LIKE_COUNT_KEY + targetId);
return true;
}
return false;
}
// 获取点赞数
public Long getLikeCount(Long targetId) {
Long count = (Long) redisTemplate.opsForValue().get(LIKE_COUNT_KEY + targetId);
return count != null ? count : 0L;
}
// 检查用户是否点赞
public boolean isLiked(Long userId, Long targetId) {
String userLikedKey = USER_LIKED_KEY + userId + ":" + targetId;
return redisTemplate.hasKey(userLikedKey);
}
}
持久化方案:
// 定时将Redis数据同步到MySQL
@Scheduled(cron = "0 0 2 * * ?") // 每天凌晨2点
public void syncToMySQL() {
// 1. 查询Redis所有点赞数据
Set<String> keys = redisTemplate.keys("user:liked:*");
// 2. 批量处理
for (String key : keys) {
// 解析key
String[] parts = key.split(":");
Long userId = Long.parseLong(parts[2]);
Long targetId = Long.parseLong(parts[3]);
// 更新MySQL
likeRepository.upsert(userId, targetId);
}
// 3. 删除已处理的数据(或设置标志)
}
MySQL表设计:
CREATE TABLE likes (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL,
target_id BIGINT NOT NULL,
target_type VARCHAR(20) NOT NULL, -- POST, COMMENT, USER
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uk_user_target (user_id, target_id, target_type),
INDEX idx_target (target_id, target_type)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
BitMap方案(用户维度):
// 使用BitMap记录用户是否点赞某篇文章
// 每个bit代表一个用户,1表示点赞,0表示未点赞
public void setLikeBit(Long articleId, Long userId) {
long bitIndex = userId % (Long.MAX_VALUE - 1);
redisTemplate.opsForValue().setBit("article:likes:" + articleId, bitIndex, true);
}
public boolean isLiked(Long articleId, Long userId) {
long bitIndex = userId % (Long.MAX_VALUE - 1);
return redisTemplate.opsForValue().getBit("article:likes:" + articleId, bitIndex);
}
public long getLikeCount(Long articleId) {
// 统计bit数
return redisTemplate.execute((RedisCallback<Long>) connection -> {
return (long) connection.bitCount("article:likes:" + articleId.getBytes());
});
}
面试回答:
"点赞系统采用Redis+MySQL双层存储。Redis使用String存储点赞数,使用user:liked:userId:targetId记录点赞关系,操作简单快速。定时任务将数据同步到MySQL持久化。对于用户维度的点赞状态,可以使用BitMap进一步优化,一个bit代表一个用户,内存占用极小。实际项目中我们支持Post、Comment、User等多种点赞类型,使用target_type区分。"
案例4:排行榜系统设计
需求:
- 实时排行榜
- 支持分页查询
- 支持按时间范围查询
方案设计:
方案1:Redis ZSet(推荐)
@Service
public class LeaderboardService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
private static final String LEADERBOARD_KEY = "leaderboard:";
// 增加分数
public void addScore(Long userId, double score) {
redisTemplate.opsForZSet().incrementScore(
LEADERBOARD_KEY + "all",
userId,
score
);
}
// 获取Top N
public List<RankItem> getTopN(int n) {
Set<ZSetOperations.TypedTuple<Object>> set =
redisTemplate.opsForZSet().reverseRangeWithScores(
LEADERBOARD_KEY + "all", 0, n - 1
);
return set.stream()
.map(tuple -> RankItem.builder()
.userId((Long) tuple.getValue())
.score(tuple.getScore())
.build())
.collect(Collectors.toList());
}
// 获取用户排名
public Long getUserRank(Long userId) {
Long rank = redisTemplate.opsForZSet().reverseRank(
LEADERBOARD_KEY + "all", userId
);
return rank != null ? rank + 1 : null;
}
// 分页查询
public List<RankItem> getRange(int page, int size) {
long start = (page - 1) * size;
long end = start + size - 1;
Set<ZSetOperations.TypedTuple<Object>> set =
redisTemplate.opsForZSet().reverseRangeWithScores(
LEADERBOARD_KEY + "all", start, end
);
return set.stream()
.map(tuple -> RankItem.builder()
.userId((Long) tuple.getValue())
.score(tuple.getScore())
.rank(start++)
.build())
.collect(Collectors.toList());
}
}
定时排行榜(周榜、月榜):
@Service
public class LeaderboardScheduler {
@Autowired
private LeaderboardService leaderboardService;
// 每天创建新key
@Scheduled(cron = "0 0 0 * * ?")
public void createDailyLeaderboard() {
String date = LocalDate.now().format(DateTimeFormatter.ISO_DATE);
// 当天的key
String dailyKey = "leaderboard:daily:" + date;
}
// 每周汇总周榜
@Scheduled(cron = "0 0 0 ? * MON")
public void createWeeklyLeaderboard() {
LocalDate today = LocalDate.now();
LocalDate weekStart = today.minusDays(6);
// 合并一周的数据
String weeklyKey = "leaderboard:weekly:" + today;
for (int i = 0; i < 7; i++) {
LocalDate date = weekStart.plusDays(i);
String dailyKey = "leaderboard:daily:" + date;
// 合并到周榜
mergeToWeekly(dailyKey, weeklyKey);
}
}
}
持久化到MySQL:
CREATE TABLE leaderboard (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL,
score DECIMAL(10, 2) NOT NULL,
rank INT NOT NULL,
date DATE NOT NULL,
UNIQUE KEY uk_user_date (user_id, date),
INDEX idx_date_rank (date, rank)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
@Scheduled(cron = "0 0 1 * * ?") // 每天凌晨1点
public void syncLeaderboard() {
// 1. 获取Redis排行榜
List<RankItem> ranks = leaderboardService.getTopN(1000);
// 2. 转换并插入MySQL
LocalDate today = LocalDate.now();
for (int i = 0; i < ranks.size(); i++) {
LeaderboardEntity entity = LeaderboardEntity.builder()
.userId(ranks.get(i).getUserId())
.score(ranks.get(i).getScore())
.rank(i + 1)
.date(today)
.build();
leaderboardRepository.save(entity);
}
}
面试回答:
"排行榜系统使用Redis ZSet实现,支持实时分数更新、排名查询、分页获取。对于多时间维度的排行榜(日榜、周榜、月榜),使用定时任务创建不同key分别存储。定时任务将排行榜数据持久化到MySQL,用于历史查询和数据统计。ZSet的时间复杂度是O(log N),支持千万级数据的高性能查询。对于更复杂的场景,可以结合MySQL做二次查询。"
第八部分:面试题库(逐题分析)
面试题1:如何设计一个高并发系统?
回答模板(完整版):
"设计高并发系统从以下几个层面考虑:
1. 架构层面:
- 使用微服务拆分,按业务领域拆分服务
- 读写分离,减轻主库压力
- 分库分表,水平扩展存储能力
- CDN加速静态资源
2. 缓存层面:
- 多级缓存:本地缓存+分布式缓存
- 缓存预热,提前加载热点数据
- 处理缓存穿透、击穿、雪崩
3. 异步处理:
- 使用消息队列异步处理耗时操作
- CompletableFuture实现本地异步
- 事件驱动架构解耦
4. 数据库优化:
- 读写分离,主库写从库读
- 分库分表,水平扩展
- 索引优化,合理使用索引
- 连接池调优,HikariCP
5. 保护机制:
- 限流:Sentinel QPS限流
- 降级:非核心功能降级
- 熔断:防止雪崩
6. 监控告警:
- 监控系统指标:QPS、RT、错误率
- 监控资源指标:CPU、内存、磁盘、网络
- 设置告警阈值,及时发现问题
通过多维度优化,系统可支撑10万+ QPS。"
面试题2:如何处理热点数据?
回答模板:
"热点数据处理策略:
1. 识别热点:
- 监控各key的访问频率
- 采样分析找出热点key
2. 缓存热点:
- 本地缓存热点数据,减少Redis压力
- 设置更长的过期时间
3. 多级缓存:
- 应用层本地缓存
- 分布式缓存
- 数据库
4. 动态限流:
- 热点key单独限流
- 非热点key正常限流
5. 数据迁移:
- 热点数据可以单独存储
- 使用更高性能的存储
实际项目中,我们通过监控识别热点key,动态调整缓存策略和限流规则。"
面试题3:消息队列如何保证顺序性?
回答模板:
"消息顺序性保证:
1. 全局有序:
- 使用单分区Topic,所有消息按顺序写入
- 单消费者消费
2. 分区有序:
- 相同key的消息发到同一分区
- Kafka通过partitionKey保证
- 每个分区内部有序
3. 消费端保证:
- 单线程消费保证顺序
- 多线程消费需要额外设计
实际项目中,大多数场景只需要分区有序,通过设计合理的分区键实现。全局有序会牺牲吞吐量,谨慎使用。"
面试题4:分布式锁怎么实现?
回答模板:
"分布式锁实现方案:
方案1:Redis SETNX + 过期时间(常用)
boolean locked = redisTemplate.opsForValue() .setIfAbsent(lockKey, value, timeout, TimeUnit.SECONDS);方案2:Redisson(推荐,自动续约)
RLock lock = redissonClient.getLock(lockKey); lock.lock(); // 自动续约(看门狗) try { // 业务逻辑 } finally { lock.unlock(); }方案3:Zookeeper临时节点
- 创建临时有序节点
- 最小节点获取锁
Redis方案性能好,Zookeeper方案可靠性高。实际项目中我们使用Redisson,它实现了Redis分布式锁,支持可重入、自动续约、公平锁。"
面试题5:如何保证消息不丢失?
回答模板:
"消息不丢失从三个环节保证:
1. 生产端:
- 开启confirm机制,等待Broker确认
- 发送失败重试,最多3次
- 持久化到本地,发送成功后删除
2. 服务端:
- 消息持久化到磁盘
- 集群部署,多副本
- 同步复制副本再返回确认
3. 消费端:
- 手动ACK确认
- 消费失败不ACK,重新投递
- 业务逻辑在事务内执行
通过全链路保障,可以实现消息不丢失。对于金融等关键业务,可以配合数据库事务,实现最终一致性。"
面试题6:如何设计一个短链接系统?
回答模板:
"短链接系统设计:
1. 短链接生成算法:
- 自增ID转62进制:a-z, A-Z, 0-9
- 哈希+冲突检测
- 雪花算法
2. 存储设计:
CREATE TABLE short_urls ( id BIGINT PRIMARY KEY AUTO_INCREMENT, short_code VARCHAR(10) UNIQUE, long_url VARCHAR(2048), create_time DATETIME, expire_time DATETIME );3. 缓存优化:
- Redis缓存长短映射
- LRU淘汰冷数据
4. 防刷机制:
- 限流防止单用户刷
- BloomFilter过滤已访问
5. 分布式ID:
- 使用雪花算法生成唯一ID
实际项目中,我们使用Redis自增ID+62进制生成短链接,Redis缓存热点映射,MySQL持久化存储。通过限流和BloomFilter防止恶意访问。"
面试题7:如何设计一个限流系统?
回答模板:
"限流系统设计:
1. 限流维度:
- 全局限流:保护系统整体
- 用户限流:防止单用户刷
- IP限流:防止恶意攻击
2. 限流算法:
- 固定窗口:简单但边界问题
- 滑动窗口:精确但内存占用大
- 令牌桶:适合流量整形
- 漏桶:适合恒定输出
3. 实现方案:
- Sentinel:功能全面,支持多种规则
- Guava RateLimiter:令牌桶实现
- Redis+Lua:分布式限流
4. 配置管理:
- 通过配置中心动态调整
- 按接口配置不同规则
实际项目中我们使用Sentinel,支持QPS限流、并发限流、Warm Up预热,通过Nacos动态配置规则。"
面试题8:如何优化慢查询?
回答模板:
"慢查询优化步骤:
1. 识别慢查询:
- 开启慢查询日志
- 使用explain分析执行计划
2. 优化索引:
- 为WHERE、JOIN、ORDER BY列建索引
- 使用覆盖索引减少回表
- 遵循最左前缀原则
3. 优化SQL:
- 避免SELECT *,只查需要的列
- 避免子查询,改用JOIN
- 分页查询优化,使用游标
4. 架构优化:
- 读写分离
- 分库分表
- 使用缓存
5. 代码优化:
- 批量操作减少数据库调用
- 使用连接池复用连接
通过多维度优化,慢查询从秒级降低到毫秒级。"
面试题9:如何实现分布式事务?
回答模板:
"分布式事务实现方案:
方案1:2PC/XA(强一致性)
- 两阶段提交:准备阶段+提交阶段
- 缺点:性能差,单点故障
方案2:TCC(补偿机制)
- Try:预留资源
- Confirm:确认提交
- Cancel:取消补偿
- 适用:业务逻辑可控的场景
方案3:Saga(长事务)
- 每步都有补偿操作
- 失败时反向补偿
- 适用:长流程业务
方案4:本地消息表(最终一致性)
- 业务+消息在同一事务
- 定时任务投递消息
- 适用:对实时性要求不高的场景
实际项目中,我们根据业务场景选择:强一致性用TCC,最终一致性用本地消息表。"
面试题10:如何设计一个即时通讯系统?
回答模板:
"即时通讯系统设计:
1. 消息推送:
- 长连接:WebSocket
- 推送服务:Netty
- 心跳保活
2. 消息存储:
- 聊天记录:MySQL持久化
- 未读消息:Redis计数
- 消息索引:ES搜索
3. 用户在线状态:
- Redis Set存储在线用户
- 过期时间+心跳更新
4. 离线消息:
- 离线时消息存储到Redis
- 上线后推送
5. 群聊优化:
- 使用消息队列异步处理
- 批量推送
实际项目中,我们使用WebSocket建立长连接,Netty作为服务端,消息存储MySQL,未读状态Redis,离线消息MQ异步推送。通过分片策略支持千万级在线用户。"
第九部分:快速记忆卡片
核心概念速记
【高并发三大瓶颈】
CPU瓶颈、I/O瓶颈、内存瓶颈
【缓存三剑客】
缓存穿透(布隆过滤器)、缓存击穿(互斥锁)、缓存雪崩(随机过期)
【缓存更新策略】
Cache Aside(最常用)、Write Through、Write Behind
【限流算法】
固定窗口、滑动窗口、令牌桶、漏桶
【系统保护三剑客】
限流、降级、熔断
【QPS计算】
QPS = 并发数 / 平均响应时间
技术选型决策树
需要缓存?
├─ 单机 → Caffeine/Guava
└─ 分布式 → Redis
需要消息队列?
├─ 高吞吐 → Kafka
├─ 功能丰富 → RabbitMQ
└─ 阿里系 → RocketMQ
需要限流?
├─ Java应用 → Sentinel
├─ 网关层 → Nginx
└─ 基础设施 → Redis
需要分布式锁?
├─ 性能优先 → Redis
└─ 可靠性优先 → Zookeeper
第十部分:学习路线图
阶段1:基础理解(1周)
- [ ] 理解并发核心概念(QPS、TPS、并发数)
- [ ] 理解三大瓶颈(CPU、I/O、内存)
- [ ] 掌握性能测试方法
- [ ] 理解多线程基础
阶段2:缓存技术(2周)
- [ ] 本地缓存(Caffeine、Guava)
- [ ] 分布式缓存(Redis)
- [ ] 缓存穿透、击穿、雪崩处理
- [ ] 缓存更新策略
阶段3:消息队列(1周)
- [ ] RabbitMQ基础
- [ ] Kafka基础
- [ ] 消息可靠性保证
- [ ] 消息堆积处理
阶段4:数据库优化(1周)
- [ ] 读写分离
- [ ] 分库分表
- [ ] 索引优化
- [ ] 连接池优化
阶段5:系统保护(1周)
- [ ] 限流(Sentinel)
- [ ] 降级
- [ ] 熔断(Resilience4j)
- [ ] 异步处理
阶段6:实战项目(2周)
- [ ] 秒杀系统设计
- [ ] 评论系统设计
- [ ] 点赞系统设计
- [ ] 排行榜系统设计
阶段7:面试准备(1周)
- [ ] 背诵10个核心面试题回答
- [ ] 练习设计3个完整方案
- [ ] 模拟面试
结语
记住几个关键点:
- 核心思想: 异步化、缓存、解耦
- 架构原则: 分层、拆分、冗余
- 优化思路: 找瓶颈、定策略、监控效果
- 保护机制: 限流、降级、熔断
面试时,先说架构图,再说具体实现,最后说优化效果。你不需要做过10万QPS的系统,只要理解设计思路和原理,就能给出令人信服的答案!
最重要的是:自信! 高并发系统设计的核心是理解瓶颈并合理运用各种技术解决,这不是神秘的东西,而是工程经验的总结。