14-系统设计
设计一个秒杀系统?
原始问法:
- 设计一个秒杀系统?
来源题目:
SRC-14-132-430
面试先答
秒杀系统的核心挑战是高并发(瞬时流量)、超卖防护和低延迟。典型架构采用前端削峰 + 网关限流 + 异步下单 + 库存扣减四层架构。前端通过活动页加载、答题验证码等方式分流;网关层做限流和认证;核心层用 Redis 预扣库存保证性能,用 Lua 脚本保证原子性;下单通过消息队列异步处理,最终落库。关键是超卖防护(Redis + Lua + 数据库乐观锁三重保障)和请求削峰(减少数据库压力)。
核心结论
- 秒杀系统 = 前端削峰 + 网关限流 + Redis 预扣 + MQ 异步 + 最终落库。
- 超卖防护:Redis Lua 原子扣减 + 数据库乐观锁双重保障。
- 高并发处理:异步化 + 负载均衡 + 缓存预热。
1. 需求假设
- 秒杀商品:单 SKU,库存 10000 件。
- 并发用户:10 万 QPS,集中在 1-3 秒内。
- 目标:无超卖、无少卖、下单成功延迟 < 500ms。
2. 容量估算
| 指标 | 数值 | 说明 |
|---|---|---|
| QPS | 100,000 | 峰值并发 |
| 库存 | 10,000 | 单 SKU |
| 数据库写入 | ~10,000 | 最终下单数 |
| Redis QPS | 100,000+ | 预扣+查询 |
3. 架构设计
用户 → CDN(静态页) → 网关(Nginx/LB) → 秒杀服务集群
↓
Redis 集群(库存)
↓ Lua 原子操作
消息队列(RocketMQ)
↓
订单服务 → MySQL
关键流程:
- 活动预热:提前将库存加载到 Redis。
- 前端削峰:活动页加载、随机码校验。
- 网关限流:令牌桶/漏桶算法,超过则返回"稍后重试"。
- 库存预扣:Redis Lua 脚本原子扣减,返回秒杀结果。
- 异步下单:扣减成功发 MQ,消费者异步创建订单。
- 最终落库:订单写库,扣减数据库库存(乐观锁)。
4. 数据模型
-- 秒杀活动表
CREATE TABLE seckill_activity (
id BIGINT PRIMARY KEY,
product_id BIGINT,
total_stock INT,
start_time DATETIME,
end_time DATETIME
);
-- 秒杀订单表
CREATE TABLE seckill_order (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT,
activity_id BIGINT,
product_id BIGINT,
create_time DATETIME,
UNIQUE KEY uk_user_activity(user_id, activity_id)
);
-- Redis 数据结构
-- seckill:stock:{activityId} → 库存数
-- seckill:users:{activityId} → Set(已秒杀用户)
5. 关键流程
秒杀核心伪代码:
// 秒杀接口
public SeckillResult seckill(Long userId, Long activityId) {
// 1. 检查活动状态
if (!activity.isActive()) return fail("活动未开始");
// 2. 检查用户是否已参与(一人一单)
if (redis.sismember("seckill:users:" + activityId, userId))
return fail("已参与");
// 3. Redis Lua 原子扣减库存
String luaScript = """
local stock = redis.call('GET', KEYS[1])
if stock <= 0 then return -1 end
redis.call('DECR', KEYS[1])
redis.call('SADD', KEYS[2], ARGV[1])
return 1
""";
Long result = redis.eval(luaScript,
"seckill:stock:" + activityId,
"seckill:users:" + activityId,
userId);
if (result == -1) return fail("已售罄");
// 4. 发送 MQ 异步下单
mq.send("seckill_order", buildOrder(userId, activityId));
return success("秒杀成功,请等待结果");
}
6. 故障处理和取舍
| 风险 | 处理方案 |
|---|---|
| Redis 宕机 | 主从切换 + 降级到数据库(限流) |
| MQ 消息丢失 | 生产者确认 + 消费者幂等 + 数据库对账 |
| 超卖 | Redis Lua + 数据库乐观锁双重保障 |
| 恶意刷单 | IP 限流 + 用户限频 + 行为分析 |
| 数据库压力 | 分库分表 + 读写分离 + 热点数据缓存 |
取舍:
- 最终一致性 vs 强一致性:秒杀场景接受最终一致性,不追求实时强一致。
- 可用性 vs 一致性:宁可少量失败,不接受超卖。
7. 一句话总结
秒杀系统通过前端削峰 + Redis 原子预扣 + MQ 异步下单实现高并发下的无超卖秒杀。
设计一个抢票系统?
原始问法:
- 设计一个抢票系统?
来源题目:
SRC-14-132-431
面试先答
抢票系统(如火车票、演唱会门票)的核心挑战是座位级库存管理、并发一致性和性能。与秒杀系统的区别在于:抢票需要选座(或自动分配)、多票价等级、锁定-支付-确认三阶段流程。核心架构:Redis 存储座位状态(空闲/锁定/已售),通过分布式锁或原子操作保证并发安全;用户选座后进入锁定状态(15 分钟支付超时自动释放);支付成功后最终确认。关键是防超售和座位分配算法。
核心结论
- 抢票系统 = 座位状态管理 + 锁定-支付-确认三阶段 + 防超售。
- 座位状态:空闲 → 锁定 → 已售,超时自动释放。
- 核心挑战:高并发下的座位一致性。
1. 需求假设
- 演唱会门票:1 场演出,5000 座位,10 个票价等级。
- 火车票:1 趟列车,途经 N 站,M 个座位类型。
- 并发:5 万 QPS 抢票。
2. 容量估算
| 指标 | 数值 |
|---|---|
| 座位数 | 5,000 |
| 票价等级 | 10 |
| 并发 QPS | 50,000 |
| 支付超时 | 15 分钟 |
3. 架构设计
用户 → APP/Web → CDN → 网关 → 抢票服务
↓
Redis(座位状态+库存)
↓ 分布式锁/Lua
订单服务 → MySQL
↓
支付服务 → 第三方支付
座位管理方案:
- 方案一:每个座位一个 Redis Key(简单但 Key 过多)。
- 方案二:位图/布隆过滤器存储座位占用状态。
- 方案三:按行或区域分块锁(推荐)。
4. 数据模型
-- 演出/车次表
CREATE TABLE show_info (
id BIGINT PRIMARY KEY,
name VARCHAR(100),
total_seats INT,
start_time DATETIME
);
-- 票档表
CREATE TABLE ticket_level (
id BIGINT PRIMARY KEY,
show_id BIGINT,
level_name VARCHAR(50),
price DECIMAL(10,2),
total INT,
available INT
);
-- 订单表
CREATE TABLE ticket_order (
id BIGINT PRIMARY KEY,
user_id BIGINT,
show_id BIGINT,
seat_no VARCHAR(20),
level_id BIGINT,
status TINYINT, -- 0锁定/1已售/2已退
lock_expire_time DATETIME,
create_time DATETIME
);
-- Redis Key 设计
-- ticket:{showId}:{levelId} → 可用座位 Set
-- ticket:{showId}:locked → 锁定用户 Map
5. 关键流程
抢票流程:
- 用户选择场次和票价等级。
- Redis 原子检查可用座位。
- 分配座位(从 Set 中随机或顺序取一个)。
- 创建锁定订单(15 分钟超时)。
- 引导支付。
- 支付成功 → 订单状态改为已售。
- 支付超时 → 订单状态改为已退,座位归还。
火车票特殊逻辑:
- 座位按区间占用(A 站到 C 站经过 B 站,A-B 和 B-C 段都占用)。
- 需要维护每个区间的座位可用数。
6. 故障处理和取舍
| 风险 | 处理方案 |
|---|---|
| 重复购票 | 用户维度唯一约束 + 一人一单 |
| 支付超时 | 定时任务扫描释放锁定座位 |
| 座位冲突 | Redis 原子操作 + 分布式锁 |
| 缓存一致性 | Redis 与 DB 对账 + 最终一致性 |
取舍:
- 抢票 vs 秒杀:抢票更复杂,需座位分配和锁定超时。
- 强一致 vs 最终一致:锁定-支付-确认保证核心流程一致性。
7. 一句话总结
抢票系统通过Redis 管理座位状态 + 锁定-支付-确认三阶段实现高并发下的座位分配和防超售。
设计一个微信红包系统?
原始问法:
- 设计一个微信红包系统?
来源题目:
SRC-14-132-432
面试先答
微信红包系统的核心挑战是高并发下的原子性和防重复领取。红包流程分三步:发红包(预扣款+生成红包)、拆红包(领取+入账)、红包详情。关键设计:发红包时不实际扣款,而是创建红包记录并锁定金额;拆红包时采用二倍均值法(剩余金额/剩余人数 × 2 随机,保证每人至少 1 分);通过幂等控制防止重复领取。核心技术点:Redis 原子操作、分布式唯一 ID、幂等设计、最终一致性。
核心结论
- 发红包:创建红包记录,冻结金额,不直接扣款。
- 拆红包:二倍均值法随机金额,保证公平性。
- 防重复领取:红包记录状态机 + 幂等 Token。
1. 需求假设
- 红包类型:拼手气红包(随机金额)、普通红包(固定金额)。
- 并发:百万级 QPS(春节红包场景)。
- 可靠性:7×24 可用,零资金差错。
2. 容量估算
| 指标 | 数值 |
|---|---|
| QPS | 1,000,000+ |
| 单笔金额 | 0.01 - 200 元 |
| 红包记录 | 亿级/日 |
| 数据存储 | TB 级 |
3. 架构设计
发红包:用户 → 红包服务 → 账户服务(扣款) → 消息服务(通知)
拆红包:用户 → 红包服务 → Redis(原子拆) → 账户服务(入账) → DB(记录)
关键模块:
- 红包服务:处理发/拆核心逻辑。
- 账户服务:用户余额管理(扣/入账)。
- 通知服务:拆红包后推送通知。
- 对账服务:定期核对账户与红包记录。
4. 数据模型
-- 红包表
CREATE TABLE red_packet (
id BIGINT PRIMARY KEY,
send_id BIGINT, -- 发送者
receive_id BIGINT, -- 接收者(0为群红包)
total_amount DECIMAL(10,2),
total_count INT,
type TINYINT, -- 1拼手气/2普通
status TINYINT, -- 0待领取/1已领完/2已过期
create_time DATETIME,
expire_time DATETIME
);
-- 红包领取记录表
CREATE TABLE red_packet_record (
id BIGINT PRIMARY KEY,
packet_id BIGINT,
user_id BIGINT,
amount DECIMAL(10,2),
create_time DATETIME,
UNIQUE KEY uk_packet_user(packet_id, user_id)
);
-- Redis Key 设计
-- redpacket:{id}:remain → 剩余金额和剩余人数(Hash)
-- redpacket:{id}:received → 已领取用户 Set
5. 关键流程
发红包流程:
- 发送者扣款到红包冻结账户。
- 生成红包 ID(分布式 ID)。
- Redis 预存剩余金额和人数。
- 写入红包记录到数据库。
- 推送通知给接收者。
拆红包流程(核心):
// Lua 脚本实现原子拆红包
String lua = """
local remainAmount = tonumber(redis.call('HGET', KEYS[1], 'amount'))
local remainCount = tonumber(redis.call('HGET', KEYS[1], 'count'))
if remainCount <= 0 or remainAmount <= 0 then
return -1 -- 已领完
end
-- 二倍均值法:保证每人至少 0.01 分
local maxAmount = remainAmount / remainCount * 2
local amount = math.random(1, math.floor(maxAmount * 100)) / 100
if remainCount == 1 then
amount = remainAmount
end
-- 更新剩余金额和人数
redis.call('HSET', KEYS[1], 'amount', remainAmount - amount)
redis.call('HSET', KEYS[1], 'count', remainCount - 1)
redis.call('SADD', KEYS[2], ARGV[1])
return amount
""";
// 幂等检查
if (redis.sismember("redpacket:" + packetId + ":received", userId))
return fail("已领取");
Double amount = redis.eval(lua,
"redpacket:" + packetId + ":remain",
"redpacket:" + packetId + ":received",
userId);
if (amount < 0) return fail("已领完");
// 异步:入账 + 记录领取
mq.send("redpacket_receive", packetId, userId, amount);
6. 故障处理和取舍
| 风险 | 处理方案 |
|---|---|
| 重复领取 | 唯一索引 + Redis Set 双重幂等 |
| 超领 | Lua 原子操作保证不超发 |
| Redis 宕机 | 主从切换 + 降级到数据库 |
| 账户不一致 | 定期对账 + 资金核对 |
| 红包过期 | 定时任务自动退还未领金额 |
二倍均值法正确性:数学上可证明每人期望金额相同,且保证每人至少 1 分。
7. 一句话总结
微信红包通过Redis + Lua 原子操作 + 二倍均值法实现高并发下的安全拆红包,核心是幂等性和资金一致性。
设计一个延迟队列?
原始问法:
- 设计一个延迟队列?
来源题目:
SRC-14-132-433
面试先答
延迟队列(Delay Queue)用于在指定时间后触发任务,如订单超时自动取消、定时推送通知等。常见实现方案:1)RabbitMQ 延迟插件(死信队列 + TTL);2)RocketMQ 延迟消息;3)Redis 实现(ZSet + 定时扫描);4)时间轮算法(Netty 定时器原理)。核心原理:将任务与目标时间关联,到时间后触发。最常用的是基于 Redis ZSet + 定时任务扫描,或直接使用 MQ 的延迟消息功能。
核心结论
- 延迟队列方案:RabbitMQ 死信、RocketMQ 延迟消息、Redis ZSet、时间轮。
- Redis ZSet 方案最通用:score 存时间戳,定时扫描到期任务。
- 时间轮方案高效但实现复杂。
1. 需求假设
- 支持任意延迟时间(几秒到几天)。
- 精度:秒级。
- 任务量:百万级。
2. 容量估算
| 指标 | 数值 |
|---|---|
| 每分钟新增任务 | 10,000 |
| 待处理任务 | 1,000,000 |
| 扫描频率 | 每秒 |
3. 架构设计
方案一:RabbitMQ 死信队列
生产者 → 延迟队列(TTL) → 死信交换机 → 消费队列 → 消费者
↓
消息过期(TTL)自动进入死信交换机
方案二:Redis ZSet
生产者 → ZADD(delay_queue, task, target_time)
↓
定时任务(每秒扫描):ZRANGEBYSCORE(delay_queue, 0, now)
↓
取出到期任务 → 执行 → ZREM
方案三:时间轮
环形数组 + 指针
每个槽表示一个时间单位(如1秒)
任务存入对应槽
指针每 tick 前进一格
4. 数据模型
// Redis ZSet 实现
// Key: delay:queue:1 (topic)
// Member: taskId
// Score: 目标执行时间戳
public class DelayQueue {
private StringRedisTemplate redis;
// 添加延迟任务
public void addTask(String taskId, long delayMs) {
long executeTime = System.currentTimeMillis() + delayMs;
redis.opsForZSet().add("delay:queue", taskId, executeTime);
}
// 扫描到期任务(定时调用)
public List<String> pollExpiredTasks() {
long now = System.currentTimeMillis();
Set<ZSetOperations.TypedTuple<String>> tasks =
redis.opsForZSet().rangeByScore("delay:queue", 0, now);
List<String> result = new ArrayList<>();
for (ZSetOperations.TypedTuple<String> task : tasks) {
redis.opsForZSet().remove("delay:queue", task.getValue());
result.add(task.getValue());
}
return result;
}
}
5. 关键流程
Redis ZSet 方案:
- 生产者
ZADD任务到 ZSet,score 为目标时间。 - 定时任务每 1 秒执行
ZRANGEBYSCORE获取到期任务。 - 取出任务后
ZREM删除。 - 异步执行任务。
时间轮方案:
// 简化的时间轮实现
public class TimeWheel {
private int tickDuration; // 每个 tick 时长(ms)
private int wheelSize; // 槽数量
private List<List<TimerTask>> buckets;
private int currentIndex;
public void addTask(TimerTask task, long delayMs) {
int ticks = (int)(delayMs / tickDuration);
int bucketIndex = (currentIndex + ticks) % wheelSize;
buckets.get(bucketIndex).add(task);
}
// 每 tick 调用
public void tick() {
currentIndex = (currentIndex + 1) % wheelSize;
List<TimerTask> tasks = buckets.get(currentIndex);
// 执行所有到期任务
for (TimerTask task : tasks) task.execute();
tasks.clear();
}
}
6. 故障处理和取舍
| 方案 | 优点 | 缺点 |
|---|---|---|
| RabbitMQ 死信 | 可靠,无需额外组件 | TTL 精度不高,不支持动态延迟 |
| RocketMQ 延迟 | 原生支持,高可靠 | 需特定版本,延迟等级固定 |
| Redis ZSet | 灵活,精度高 | 需定时扫描,Redis 依赖 |
| 时间轮 | 高性能,O(1) 添加 | 实现复杂,内存占用 |
选型建议:
- 已用 MQ → 直接用 MQ 延迟消息。
- 未用 MQ → Redis ZSet 方案。
- 高性能场景 → 时间轮(Netty/ScheduledThreadPool)。
7. 一句话总结
延迟队列的核心是将任务与目标时间绑定,到期后触发执行,Redis ZSet 和 MQ 延迟消息是最常用实现。
设计一个分布式ID生成器?
原始问法:
- 设计一个分布式ID生成器?
来源题目:
SRC-14-132-434
面试先答
分布式 ID 生成器用于在多实例环境下生成全局唯一 ID。常见方案:1)UUID(简单但无序、太长);2)数据库号段(简单但 DB 压力大);3)雪花算法(Snowflake,高性能、有序);4)Leaf(美团,基于号段+雪花);5)Tinyid(滴滴,号段优化)。雪花算法是最主流的方案:64 位 = 1 位符号 + 41 位时间戳 + 10 位机器 ID + 12 位序列号。核心是时间戳保证趋势递增,机器 ID 保证分布式唯一,序列号支持同毫秒内 4096 个 ID。
核心结论
- 雪花算法最主流:时间戳 + 机器 ID + 序列号,高性能有序。
- 机器 ID 分配:Zookeeper / Redis / 数据库号段。
- 时钟回拨是雪花算法的最大挑战。
1. 需求假设
- QPS:每秒 100 万 ID。
- 要求:全局唯一、趋势递增、高性能。
- 延迟:< 1ms。
2. 容量估算
| 指标 | 雪花算法 |
|---|---|
| 时间戳位数 | 41 位(可用 69 年) |
| 机器 ID 位数 | 10 位(1024 台机器) |
| 序列号位数 | 12 位(每毫秒 4096 个 ID) |
| 峰值 QPS | 4096 × 1000 = 400 万/秒 |
3. 架构设计
ID 服务集群
↓
雪花算法生成器(每实例独立)
↓
实例 ID 分配(Zookeeper/Redis)
雪花算法 64 位结构:
| 符号位(1) | 时间戳(41) | 机器ID(10) | 序列号(12) |
| 0 固定 | 毫秒级时间 | 实例标识 | 同毫秒递增 |
4. 数据模型
public class SnowflakeIdGenerator {
private long workerId; // 机器ID (0-1023)
private long sequence = 0L; // 序列号
private long lastTimestamp = -1L;
private static final long WORKER_ID_BITS = 10L;
private static final long MAX_WORKER_ID = ~(-1L << WORKER_ID_BITS); // 1023
private static final long SEQUENCE_BITS = 12L;
private static final long WORKER_ID_SHIFT = SEQUENCE_BITS;
private static final long TIMESTAMP_LEFT_SHIFT =
SEQUENCE_BITS + WORKER_ID_BITS;
private static final long SEQUENCE_MASK = ~(-1L << SEQUENCE_BITS);
private static final long EPOCH = 1489111610226L; // 起始时间戳
public SnowflakeIdGenerator(long workerId) {
if (workerId > MAX_WORKER_ID || workerId < 0)
throw new IllegalArgumentException();
this.workerId = workerId;
}
public synchronized long nextId() {
long timestamp = System.currentTimeMillis();
if (timestamp < lastTimestamp) {
// 时钟回拨处理
long offset = lastTimestamp - timestamp;
if (offset <= 5) {
try { Thread.sleep(offset); }
catch (InterruptedException e) { e.printStackTrace(); }
timestamp = System.currentTimeMillis();
} else {
throw new RuntimeException("时钟回拨过大");
}
}
if (lastTimestamp == timestamp) {
sequence = (sequence + 1) & SEQUENCE_MASK;
if (sequence == 0) {
timestamp = tilNextMillis(lastTimestamp);
}
} else {
sequence = 0L;
}
lastTimestamp = timestamp;
return ((timestamp - EPOCH) << TIMESTAMP_LEFT_SHIFT)
| (workerId << WORKER_ID_SHIFT)
| sequence;
}
private long tilNextMillis(long lastTimestamp) {
long timestamp = System.currentTimeMillis();
while (timestamp <= lastTimestamp) timestamp = System.currentTimeMillis();
return timestamp;
}
}
5. 关键流程
- 机器 ID 分配:通过 Zookeeper 创建临时顺序节点,分配唯一 ID。
- ID 生成:每实例独立生成,无需网络调用。
- 时钟回拨处理:等待或抛异常。
6. 故障处理和取舍
| 方案 | 优点 | 缺点 |
|---|---|---|
| UUID | 简单,无中心化 | 无序,太长(36字符) |
| 数据库号段 | 简单有序 | DB 压力大,延迟高 |
| 雪花算法 | 高性能,有序 | 时钟回拨问题 |
| Leaf | 可靠,高性能 | 依赖 ZK + DB |
时钟回拨解决方案:
- 等待模式:回拨 < 5ms 时等待。
- 备用实例:主实例回拨时切备用实例。
- 逻辑时钟:用逻辑时间替代物理时间。
7. 一句话总结
分布式 ID 生成器的核心是在高性能和唯一性之间平衡,雪花算法是目前最主流的方案。
设计一个分布式锁?
原始问法:
- 设计一个分布式锁?
来源题目:
SRC-14-132-435
面试先答
分布式锁用于多进程/多机器间的互斥控制。常见实现方案:1)基于数据库(简单但性能差);2)基于 Redis(高性能,主流方案);3)基于 Zookeeper(强一致性,高可靠)。Redis 方案最常用:SET key value NX PX timeout 原子加锁,Lua 脚本保证解锁原子性,Redisson 框架封装了可重入锁、红锁、读写锁等。核心要素:互斥性、防死锁(自动过期)、可重入、原子解锁。
核心结论
- 分布式锁方案:Redis(高性能)、Zookeeper(强一致)、数据库(简单)。
- Redis 锁核心:SET NX + 过期时间 + Lua 解锁。
- Redisson 是生产级 Redis 分布式锁框架。
1. 需求假设
- 互斥:同一时刻只有一个客户端持有锁。
- 防死锁:锁有自动过期机制。
- 可重入:同一客户端可多次获取同一锁。
- 高可用:锁服务自身不能成为单点。
2. 容量估算
| 指标 | 数值 |
|---|---|
| 锁竞争 QPS | 10,000 |
| 锁持有时间 | 10ms - 10s |
| 客户端数 | 1,000 |
3. 架构设计
Redis 分布式锁:
客户端 → SET NX PX 加锁 → Redis
客户端 → Lua 脚本解锁 → Redis
Zookeeper 分布式锁:
客户端 → 创建临时节点 → Zookeeper
客户端 → 监听前驱节点 → 等待
4. 数据模型
// Redis 分布式锁实现
public class RedisDistributedLock {
private StringRedisTemplate redis;
public boolean tryLock(String lockKey, String requestId, long expireMs) {
Boolean result = redis.opsForValue()
.setIfAbsent(lockKey, requestId, expireMs, TimeUnit.MILLISECONDS);
return Boolean.TRUE.equals(result);
}
public boolean unlock(String lockKey, String requestId) {
// Lua 脚本保证原子解锁
String luaScript = """
if redis.call('GET', KEYS[1]) == ARGV[1] then
return redis.call('DEL', KEYS[1])
end
return 0
""";
Long result = redis.execute(
new DefaultRedisScript<>(luaScript, Long.class),
Collections.singletonList(lockKey), requestId);
return result == 1L;
}
}
Redisson 使用:
RLock lock = redissonClient.getLock("myLock");
lock.lock(10, TimeUnit.SECONDS);
try {
// 业务逻辑
} finally {
lock.unlock();
}
5. 关键流程
Redis 加锁流程:
- 客户端生成唯一
requestId(UUID)。 SET lockKey requestId NX PX 30000(设置 30s 过期)。- 返回成功则获取锁,失败则重试或排队。
Redis 解锁流程:
- Lua 脚本检查 value 是否等于 requestId。
- 相等则删除,防止误删他人锁。
- 不相等则返回失败(锁已过期或被他人持有)。
6. 故障处理和取舍
| 风险 | 处理方案 |
|---|---|
| 锁过期业务未完成 | 续期机制(Redisson Watchdog) |
| Redis 主从切换 | 红锁(RedLock)算法 |
| 客户端崩溃 | 自动过期释放锁 |
| 误删他人锁 | value 校验 + Lua 原子解锁 |
RedLock 算法:向多个独立 Redis 实例加锁,过半成功则认为获取锁。
Redis vs Zookeeper:
- Redis:高性能,弱一致,适合对一致性要求不高的场景。
- Zookeeper:强一致,性能低,适合对一致性要求高的场景。
7. 一句话总结
分布式锁的核心是在分布式环境下实现互斥,Redis 方案最常用,需注意原子性、可重入和高可用。
设计一个接口限流系统?
原始问法:
- 设计一个接口限流系统?
来源题目:
SRC-14-132-436
面试先答
接口限流是保护系统不被过载的重要手段。常见算法:1)固定窗口(实现简单但有临界突刺);2)滑动窗口(平滑但内存占用高);3)漏桶算法(匀速输出,应对突发能力差);4)令牌桶算法(允许突发,最常用);5)滑动日志(精度高)。生产级限流方案:Sentinel(阿里)、Guava RateLimiter(单机)、Redis + Lua(分布式)。核心是在公平性、平滑性、突发处理能力之间平衡。
核心结论
- 限流算法:固定窗口、滑动窗口、漏桶、令牌桶。
- 令牌桶最常用:允许突发,实现简单。
- Sentinel 是生产级限流框架,支持多种规则和降级。
1. 需求假设
- 限流维度:用户级、接口级、IP 级。
- 限流策略:QPS、并发数、熔断降级。
- 分布式:多实例全局限流。
2. 容量估算
| 指标 | 数值 |
|---|---|
| 限流规则数 | 100+ |
| 每秒检查次数 | 100,000 |
| 分布式实例数 | 10+ |
3. 架构设计
请求 → 限流层(Sentinel/自研) → 业务服务
↓
规则管理(控制台) → 推送到各节点
令牌桶算法:
桶容量: burst
令牌生成速率: rate tokens/sec
每来一个请求消耗 1 个令牌
桶空时拒绝请求
4. 数据模型
// 令牌桶限流器
public class TokenBucketRateLimiter {
private double tokens; // 当前令牌数
private double rate; // 令牌生成速率
private double burst; // 桶容量
private long lastTime; // 上次时间
public synchronized boolean tryAcquire() {
long now = System.currentTimeMillis();
double elapsed = (now - lastTime) / 1000.0;
tokens = Math.min(burst, tokens + elapsed * rate);
lastTime = now;
if (tokens >= 1.0) {
tokens -= 1.0;
return true;
}
return false;
}
}
// 基于 Redis 的分布式限流(Lua 滑动窗口)
public class DistributedRateLimiter {
public boolean tryAcquire(String key, int limit, int windowSec) {
String luaScript = """
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
redis.call('ZREMRANGEBYSCORE', key, 0, now - window * 1000)
local count = redis.call('ZCARD', key)
if count < limit then
redis.call('ZADD', key, now, now)
redis.call('EXPIRE', key, window)
return 1
end
return 0
""";
long now = System.currentTimeMillis();
Long result = redis.execute(
new DefaultRedisScript<>(luaScript, Long.class),
Collections.singletonList(key),
String.valueOf(limit),
String.valueOf(windowSec),
String.valueOf(now));
return result == 1L;
}
}
5. 关键流程
Sentinel 限流流程:
- 规则定义(QPS、并发数、降级策略)。
- 实时统计每个资源的请求数和响应时间。
- 基于规则和统计数据做限流判断。
- 触发限流时执行降级逻辑(快速失败、返回默认值等)。
6. 故障处理和取舍
| 算法 | 优点 | 缺点 |
|---|---|---|
| 固定窗口 | 实现简单 | 临界突刺 |
| 滑动窗口 | 平滑 | 内存占用高 |
| 漏桶 | 匀速输出 | 不允许突发 |
| 令牌桶 | 允许突发 | 实现略复杂 |
选型:
- 允许突发 → 令牌桶。
- 严格平滑 → 漏桶或滑动窗口。
- 生产环境 → Sentinel(综合策略)。
7. 一句话总结
接口限流通过令牌桶等算法控制请求速率,保护系统稳定性,Sentinel 是生产级首选。
设计一个评论系统?
原始问法:
- 设计一个评论系统?
来源题目:
SRC-14-132-437
面试先答
评论系统的核心挑战是层级结构存储、分页查询和高并发写入。三种存储方案:1)邻接表(parent_id 自关联,简单但多层查询慢);2)物化路径(存储完整路径,查询高效但更新复杂);3)闭包表(存储所有祖先关系,空间换时间)。热门评论需要分页+排序(时间/热度),楼中楼(回复评论的评论)需要特殊处理。关键:读写分离、缓存热点评论、异步写入。
核心结论
- 评论存储:邻接表(简单)、物化路径(高效查询)、闭包表(多层查询)。
- 热门评论:缓存 + 分页。
- 楼中楼:二级评论独立存储 + 展开模式。
1. 需求假设
- 支持:顶层评论、回复评论、点赞、分页。
- 量级:单篇文章 10 万评论。
- 并发:读多写少(读 90%,写 10%)。
2. 容量估算
| 指标 | 数值 |
|---|---|
| 评论 QPS(读) | 50,000 |
| 评论 QPS(写) | 5,000 |
| 单篇最大评论 | 100,000 |
| 点赞 QPS | 20,000 |
3. 架构设计
用户 → 网关 → 评论服务 → MySQL(存储)
↕
Redis(缓存热点)
↓
搜索引擎(ES)全文检索
架构要点:
- 读写分离:主库写、从库读。
- 热点评论缓存:Redis ZSet 按热度排序。
- 异步写入:MQ 削峰。
4. 数据模型
-- 评论表(邻接表)
CREATE TABLE comment (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
article_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
content VARCHAR(2000) NOT NULL,
parent_id BIGINT DEFAULT 0, -- 0为顶层评论
reply_to_user_id BIGINT DEFAULT 0,
like_count INT DEFAULT 0,
status TINYINT DEFAULT 1,
create_time DATETIME,
INDEX idx_article_parent(article_id, parent_id),
INDEX idx_article_time(article_id, create_time)
);
-- 点赞表
CREATE TABLE comment_like (
id BIGINT PRIMARY KEY,
comment_id BIGINT,
user_id BIGINT,
create_time DATETIME,
UNIQUE KEY uk_comment_user(comment_id, user_id)
);
-- Redis 缓存
-- comment:hot:{articleId} → ZSet(评论ID, 热度分)
-- comment:detail:{id} → Hash(评论详情)
-- comment:count:{articleId} → 评论总数
5. 关键流程
发表评论:
- 写入 MySQL(主库)。
- 增量更新 Redis 缓存。
- 发送通知(被回复者)。
查询评论列表:
- 查 Redis 缓存的评论 ID 列表。
- 批量查询评论详情(Pipeline)。
- 按分页返回。
点赞:
- Redis 原子操作
INCR点赞数。 - 异步落库。
6. 故障处理和取舍
| 风险 | 处理方案 |
|---|---|
| 评论过多 | 分表 + 分页加载更多 |
| 热点评论缓存穿透 | 布隆过滤器 + 空值缓存 |
| 敏感词 | 文本审核 + 关键词过滤 |
| 楼中楼展开 | 二级评论单独分页加载 |
取舍:
- 读多写少 → 缓存优先。
- 一致性 → 最终一致(点赞数允许延迟)。
7. 一句话总结
评论系统的核心是层级结构存储 + 高并发读写优化,通过邻接表存储、缓存热点、读写分离实现高效评论服务。
设计一个 feed 流系统?
原始问法:
- 设计一个 feed 流系统?
来源题目:
SRC-14-132-438
面试先答
Feed 流系统(信息流)是社交产品的核心,有三种实现模式:1)推模式(Push):用户发帖时推送到所有粉丝收件箱,简单但大 V 粉丝多时延迟高;2)拉模式(Pull):用户登录时拉取关注人的最新帖子,无推送延迟但需频繁拉取;3)推+拉结合(Push+Pull):热点推+冷点拉,主流方案。核心数据结构是收件箱(每个用户的 Feed 列表),用 Redis ZSet 按时间排序。关键优化:分页、预加载、短拉。
核心结论
- Feed 模式:推模式(实时)、拉模式(简单)、推+拉(主流)。
- 核心数据结构:Redis ZSet 存储每个用户的 Feed。
- 关键优化:预加载、分页、冷热分离。
1. 需求假设
- 用户数:1 亿。
- 大 V:1000 万粉丝。
- 帖子 QPS:10,000 发/秒。
- Feed 访问 QPS:1,000,000/秒。
2. 容量估算
| 指标 | 数值 |
|---|---|
| 用户数 | 100,000,000 |
| 大 V 粉丝 | 10,000,000 |
| 帖子 QPS | 10,000 |
| Feed 存储 | 100 亿条目 |
3. 架构设计
发帖 → 推送到粉丝收件箱(Redis ZSet) → 通知
↓
粉丝拉取 Feed → 分页返回
推模式流程:
- 用户发帖。
- 读取粉丝列表(Redis Set)。
- 将帖子 ID 推送到每个粉丝的收件箱(Redis ZAdd)。
- 截断到最大长度(如 500 条)。
拉模式流程:
- 用户登录。
- 从 Redis ZSet 拉取收件箱内容。
- 分页返回。
推+拉结合:
- 大 V 发帖 → 推送到活跃粉丝 + 冷数据走拉。
- 普通用户 → 全部推送。
4. 数据模型
-- 用户关系表
CREATE TABLE user_relation (
user_id BIGINT,
follow_id BIGINT,
create_time DATETIME,
PRIMARY KEY(user_id, follow_id)
);
-- 帖子表
CREATE TABLE post (
id BIGINT PRIMARY KEY,
user_id BIGINT,
content TEXT,
create_time DATETIME
);
-- Redis Key 设计
-- feed:{userId} → ZSet(postId, 时间戳) 收件箱
-- fans:{userId} → Set(粉丝ID) 粉丝列表
-- follow:{userId} → Set(关注ID) 关注列表
-- post:detail:{id} → Hash(帖子详情) 帖子缓存
5. 关键流程
发帖服务:
public void publishPost(Long userId, Post post) {
// 1. 保存帖子
postService.save(post);
// 2. 获取粉丝列表(Redis 缓存)
Set<Long> fans = redis.smembers("fans:" + userId);
// 3. 推送到粉丝收件箱
for (Long fanId : fans) {
redis.zadd("feed:" + fanId, post.getId(), post.getCreateTime());
// 截断收件箱到最大 500 条
redis.zremrangebyrank("feed:" + fanId, 0, -501);
}
// 4. 大 V 优化:异步推送 + 分批
// ...
}
6. 故障处理和取舍
| 风险 | 处理方案 |
|---|---|
| 大 V 推送延迟 | 异步批量推送 + 热点推送 |
| 收件箱过大 | 截断 + 分页拉取历史 |
| 推送失败 | 重试 + 最终一致性 |
| 缓存雪崩 | Redis 集群 + 预热 |
取舍:
- 推 vs 拉:推实时但写入量大,拉简单但延迟高。
- 存储:Redis 存活跃 Feed,MySQL 存历史。
7. 一句话总结
Feed 流系统通过推+拉结合模式实现高并发下的实时信息流,核心是收件箱的高效存储和分页。
弱网环境下上传1G视频,应该怎么设计?
原始问法:
- 弱网环境下上传1G视频,应该怎么设计?
来源题目:
SRC-14-132-439
面试先答
弱网环境上传大文件的核心是分片上传 + 断点续传 + 秒传。设计要点:1)将文件切分为固定大小的分片(如 5MB);2)分片并发上传(3-5 并发);3)支持断点续传(记录已上传分片);4)上传完成后服务端合并;5)弱网优化:自适应并发数、重传策略、压缩优化。关键技术:MD5 文件校验(秒传判断)、分片状态持久化(断点续传)、CDN 加速。
核心结论
- 大文件上传 = 分片 + 断点续传 + 秒传。
- 弱网优化:自适应并发、重传、压缩。
- 核心:分片状态管理和完整性校验。
1. 需求假设
- 文件大小:1GB 视频。
- 网络:波动大,带宽 100KB/s - 5MB/s。
- 要求:断点续传、秒传、完整性校验。
2. 容量估算
| 指标 | 数值 |
|---|---|
| 文件大小 | 1 GB |
| 分片大小 | 5 MB |
| 分片数 | 204 |
| 并发数 | 3-5 |
| 弱网传输时间 | 5-30 分钟 |
3. 架构设计
客户端 → 分片 → 并发上传 → CDN/OSS → 服务端合并
↓
上传状态存储(Redis/DB)
核心流程:
- 文件 MD5 校验:上传前计算整个文件的 MD5。
- 秒传判断:服务端检查 MD5 是否已存在,存在则直接完成。
- 分片上传:将文件切分为固定大小分片。
- 并发控制:3-5 个分片并发上传。
- 断点续传:记录已上传分片,续传时跳过已完成分片。
- 合并校验:所有分片上传完成后,服务端合并并校验。
4. 数据模型
-- 上传任务表
CREATE TABLE upload_task (
id BIGINT PRIMARY KEY,
user_id BIGINT,
file_name VARCHAR(200),
file_size BIGINT,
file_md5 VARCHAR(64),
total_chunks INT,
uploaded_chunks JSON, -- 已上传分片列表
status TINYINT, -- 0上传中/1已完成/2失败
create_time DATETIME
);
-- Redis 缓存
-- upload:{taskId} → Hash(已上传分片)
-- file:md5:{md5} → fileId(秒传)
5. 关键流程
// 分片上传核心逻辑
public class ChunkUploader {
private static final int CHUNK_SIZE = 5 * 1024 * 1024; // 5MB
private static final int MAX_CONCURRENT = 5;
public UploadResult upload(File file) {
// 1. 计算 MD5
String md5 = calculateMD5(file);
// 2. 秒传判断
if (isFileExists(md5)) {
return UploadResult.quickSuccess(getFileId(md5));
}
// 3. 创建上传任务
UploadTask task = createTask(file, md5);
// 4. 分片上传(并发)
List<Chunk> chunks = splitFile(file, CHUNK_SIZE);
Set<Integer> uploadedChunks = getUploadedChunks(task.getId());
// 过滤已上传分片
List<Chunk> remaining = chunks.stream()
.filter(c -> !uploadedChunks.contains(c.index))
.collect(Collectors.toList());
// 并发上传
ExecutorService executor = Executors.newFixedThreadPool(MAX_CONCURRENT);
List<Future<?>> futures = new ArrayList<>();
for (Chunk chunk : remaining) {
futures.add(executor.submit(() -> {
uploadChunk(task.getId(), chunk);
markChunkUploaded(task.getId(), chunk.index);
}));
}
// 等待所有分片完成
for (Future<?> f : futures) f.get();
// 5. 合并文件
return mergeChunks(task);
}
}
6. 故障处理和取舍
| 风险 | 处理方案 |
|---|---|
| 网络中断 | 断点续传:保存分片状态 |
| 上传超时 | 分片级重传,指数退避 |
| 并发过多 | 自适应:根据带宽调整并发数 |
| 分片丢失 | MD5 校验 + 补传 |
弱网优化策略:
- 自适应并发:监测带宽动态调整并发数。
- 压缩优化:视频压缩后上传。
- 增量上传:只上传修改部分。
- CDN 加速:就近节点上传。
7. 一句话总结
弱网大文件上传的核心是分片 + 断点续传 + 自适应优化,通过分而治之和状态持久化实现可靠传输。
100个进程,5个并发数的工作,应该怎么设计?
原始问法:
- 100个进程,5个并发数的工作,应该怎么设计?
来源题目:
SRC-14-132-440
面试先答
100 个进程,5 个并发的场景本质是限流 + 任务调度。核心有三种方案:1)线程池:固定 5 个工作线程处理 100 个任务;2)信号量:用 Semaphore(5) 控制并发数;3)消息队列:100 个任务入队,5 个消费者并发处理。关键是任务分发(轮询/随机/负载均衡)和结果收集(同步返回/异步通知)。推荐方案:阻塞队列 + 固定线程池,简单高效。
核心结论
- 并发控制方案:线程池、信号量、消息队列。
- 推荐:固定大小线程池(5 线程)+ 阻塞队列。
- 扩展:分布式场景用 MQ + 消费者组。
1. 需求假设
- 100 个独立任务,每个处理时间 100ms - 1s。
- 最大并发 5。
- 要求:任务不丢失、结果可追踪。
2. 容量估算
| 指标 | 数值 |
|---|---|
| 总任务数 | 100 |
| 并发数 | 5 |
| 单任务耗时 | 100ms - 1s |
| 总耗时 | 2s - 20s |
3. 架构设计
方案一:Java 线程池
// 固定大小线程池
ExecutorService executor = Executors.newFixedThreadPool(5);
List<Future<Result>> futures = new ArrayList<>();
for (int i = 0; i < 100; i++) {
futures.add(executor.submit(() -> {
return processTask(i);
}));
}
// 收集结果
for (Future<Result> f : futures) {
Result r = f.get(); // 阻塞等待
// 处理结果
}
executor.shutdown();
方案二:信号量
Semaphore semaphore = new Semaphore(5);
CountDownLatch latch = new CountDownLatch(100);
for (int i = 0; i < 100; i++) {
final int taskId = i;
new Thread(() -> {
try {
semaphore.acquire();
processTask(taskId);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release();
latch.countDown();
}
}).start();
}
latch.await(); // 等待所有任务完成
方案三:阻塞队列
BlockingQueue<Task> queue = new LinkedBlockingQueue<>(100);
for (int i = 0; i < 100; i++) queue.put(new Task(i));
// 5 个工作线程
for (int i = 0; i < 5; i++) {
new Thread(() -> {
while (true) {
Task task = queue.take(); // 阻塞获取
processTask(task);
}
}).start();
}
4. 关键流程
- 任务提交到队列。
- 最多 5 个工作线程并发执行。
- 完成后从队列取下一个任务。
- 所有任务完成后汇总结果。
5. 故障处理和取舍
| 方案 | 优点 | 缺点 |
|---|---|---|
| 线程池 | 简单,管理方便 | JVM 内有效 |
| 信号量 | 灵活,可动态调整 | 需手动创建线程 |
| 消息队列 | 分布式,解耦 | 运维成本高 |
选型:
- 单机 → 线程池。
- 分布式 → MQ + 消费者组。
6. 一句话总结
100 个任务 5 个并发的核心是限流调度,线程池是最简单高效的单机方案。
高并发场景下,如何解决数据库读写性能瓶颈?
原始问法:
- 高并发场景下,如何解决数据库读写性能瓶颈?
来源题目:
SRC-14-132-441
面试先答
高并发数据库瓶颈是系统设计的核心挑战。解决方案从五层架构入手:1)缓存层(Redis 热数据缓存、布隆过滤器防穿透);2)读写分离(主库写、从库读);3)分库分表(水平拆分、垂直拆分);4)异步化(MQ 削峰、异步落库);5)数据优化(索引、SQL 优化)。核心思路是减少数据库请求量和分散数据库压力。
核心结论
- 五层解决方案:缓存 → 读写分离 → 分库分表 → 异步化 → SQL 优化。
- 优先考虑缓存,再考虑架构调整。
- 分库分表是最后手段,复杂度高。
1. 需求假设
- 单表数据:1 亿+ 行。
- QPS:10,000+。
- 读多写少:90% 读,10% 写。
2. 架构设计
用户 → 应用层 → 缓存(Redis) → 数据库(读写分离+分库分表)
↓ ↓
缓存命中 主库写/从库读
方案一:缓存
- 热数据缓存:高频访问数据放 Redis。
- 缓存策略:Cache-Aside(旁路)、Write-Through(写穿透)。
- 防穿透:布隆过滤器 + 空值缓存。
- 防雪崩:随机过期时间 + 预热。
方案二:读写分离
- 一主多从:主库写,从库读。
- 半同步复制:保证主从延迟可接受。
- 应用层路由:读请求走从库,写请求走主库。
方案三:分库分表
- 垂直拆分:按业务模块分(用户库、订单库)。
- 水平拆分:按 ID 取模分(10 个库 × 10 个表)。
- 分片策略:取模、范围、一致性 Hash。
3. 数据模型
-- 垂直拆分示例
-- 用户库: user_{0-9}.user_info_{0-9}
-- 订单库: order_{0-9}.order_info_{0-9}
-- 分库分表路由
public class ShardingRouter {
// 分片键:user_id
public String getDatabase(Long userId) {
return "user_" + (userId % 10);
}
public String getTable(Long userId) {
return "user_info_" + (userId % 10);
}
}
4. 关键流程
请求处理流程:
- 查缓存 → 命中则返回。
- 缓存未命中 → 查从库。
- 从库未命中 → 查主库(保证一致性场景)。
- 写入 → 写主库 + 删缓存。
5. 故障处理和取舍
| 方案 | 优点 | 缺点 |
|---|---|---|
| 缓存 | 高性能 | 一致性问题、内存成本 |
| 读写分离 | 分散读压力 | 主从延迟、写仍是单点 |
| 分库分表 | 分散写压力 | 复杂度高、跨库 Join 难 |
| 异步化 | 削峰 | 最终一致性 |
选型顺序:缓存 → 读写分离 → 分库分表 → 异步化。
6. 一句话总结
高并发数据库优化的核心是减少请求量和分散压力,缓存是首选,分库分表是最后手段。
抛开MQ,自己实现延迟功能,你会怎么做?
原始问法:
- 抛开MQ,自己实现延迟功能,你会怎么做?
来源题目:
SRC-14-132-442
面试先答
抛开 MQ 实现延迟功能有四种主流方案:1)JDK ScheduledExecutorService(简单但单机);2)Redis ZSet + 定时扫描(分布式,推荐);3)时间轮算法(Netty 风格,高性能);4)数据库定时任务(最简单但效率低)。推荐 Redis ZSet 方案:将任务以目标时间戳为 score 存入 ZSet,定时任务每秒扫描到期任务并执行。时间复杂度:添加 O(log n),扫描 O(k)(k 为到期任务数)。
核心结论
- 无 MQ 延迟方案:ScheduledExecutor、Redis ZSet、时间轮、DB 定时。
- Redis ZSet 最通用,支持分布式。
- 时间轮最高性能,适合海量定时任务。
1. 需求假设
- 支持分布式部署。
- 精度:秒级。
- 任务量:百万级。
2. 架构设计
方案一:ScheduledExecutorService
ScheduledExecutorService executor = Executors.newScheduledThreadPool(5);
// 延迟执行
executor.schedule(() -> {
// 业务逻辑
}, 5, TimeUnit.SECONDS);
// 周期执行
executor.scheduleAtFixedRate(() -> {
// 周期任务
}, 0, 1, TimeUnit.SECONDS);
方案二:Redis ZSet
public class DelayTaskScheduler {
private StringRedisTemplate redis;
private static final String KEY = "delay:tasks";
// 添加延迟任务
public void addTask(String taskId, long delaySec) {
long executeTime = System.currentTimeMillis() + delaySec * 1000;
redis.opsForZSet().add(KEY, taskId, executeTime);
}
// 扫描到期任务(每秒调用)
@Scheduled(fixedRate = 1000)
public void scanAndExecute() {
long now = System.currentTimeMillis();
Set<ZSetOperations.TypedTuple<String>> tasks =
redis.opsForZSet().rangeByScore(KEY, 0, now);
for (ZSetOperations.TypedTuple<String> task : tasks) {
redis.opsForZSet().remove(KEY, task.getValue());
executeTask(task.getValue());
}
}
}
方案三:时间轮
public class TimingWheel {
private long tickMs; // 每 tick 时长
private int wheelSize; // 槽数量
private List<List<TimerTask>> buckets;
private int currentIndex;
private ScheduledExecutorService executor;
public void start() {
executor.scheduleAtFixedRate(() -> {
currentIndex = (currentIndex + 1) % wheelSize;
List<TimerTask> tasks = buckets.get(currentIndex);
for (TimerTask task : tasks) task.execute();
tasks.clear();
}, tickMs, tickMs, TimeUnit.MILLISECONDS);
}
public void addTask(TimerTask task, long delayMs) {
long ticks = delayMs / tickMs;
int slot = (currentIndex + (int)ticks) % wheelSize;
buckets.get(slot).add(task);
}
}
3. 关键流程
Redis ZSet 方案:
- 添加任务:
ZADD key taskId executeTime。 - 定时扫描:
ZRANGEBYSCORE key 0 now。 - 取出任务:
ZREM key taskId。 - 执行任务(异步)。
4. 故障处理和取舍
| 方案 | 优点 | 缺点 |
|---|---|---|
| ScheduledExecutor | 简单 | 单机,JVM 重启丢失 |
| Redis ZSet | 分布式,可靠 | 需定时扫描 |
| 时间轮 | O(1) 添加 | 实现复杂 |
| DB 定时 | 最简单 | 效率极低 |
选型:
- 单机 → ScheduledExecutor。
- 分布式 → Redis ZSet。
- 海量任务 → 时间轮。
5. 一句话总结
无 MQ 延迟实现的核心是用时间戳排序存储 + 定时扫描,Redis ZSet 是最通用的分布式方案。
设计一个RPC框架时,序列化模块最优先考虑的因素是什么?
原始问法:
- 设计一个RPC框架时,序列化模块最优先考虑的因素是什么?
来源题目:
SRC-14-132-443
面试先答
RPC 框架的序列化模块最优先考虑三个因素:性能(编码/解码速度)、兼容性(跨版本/跨语言)和体积(序列化后大小)。性能直接影响 RPC 调用延迟;兼容性决定框架的可扩展性;体积影响网络传输效率。主流序列化框架按优先级:Protobuf(高性能、强兼容、小体积,推荐)、Hessian(Java 专用、简洁)、JSON(通用但性能差)、Kryo(简单但兼容性弱)。Protobuf 是目前 RPC 框架的首选序列化方案(gRPC、Dubbo 默认)。
核心结论
- RPC 序列化三大优先级:性能 > 兼容性 > 体积。
- Protobuf 是主流 RPC 框架的首选。
- 其他因素:易用性、调试友好、跨语言。
1. 需求假设
- RPC 调用:毫秒级延迟。
- 跨语言:Java / Go / Python。
- 数据结构:复杂嵌套对象。
2. 主流序列化对比
| 框架 | 性能 | 体积 | 兼容性 | 跨语言 | 适用场景 |
|---|---|---|---|---|---|
| Protobuf | ★★★★★ | ★★★★★ | ★★★★★ | 支持 | RPC首选 |
| Avro | ★★★★ | ★★★★★ | ★★★★ | 支持 | 大数据 |
| Thrift | ★★★★ | ★★★★ | ★★★★ | 支持 | |
| Hessian | ★★★ | ★★★ | ★★★ | 不支持 | Java RPC |
| JSON | ★★ | ★★ | ★★★★★ | 支持 | 调试/低并发 |
| Kryo | ★★★★★ | ★★★★ | ★★ | 不支持 | 简单场景 |
| FST | ★★★★★ | ★★★★ | ★★ | 不支持 | 简单场景 |
3. Protobuf 示例
// user.proto
syntax = "proto3";
package com.example;
message UserRequest {
int64 user_id = 1;
string name = 2;
repeated string tags = 3;
}
message UserResponse {
int32 code = 1;
string message = 2;
}
service UserService {
rpc GetUser(UserRequest) returns (UserResponse);
}
Protobuf 序列化优势:
- 二进制编码,体积小。
- 字段编号兼容,支持版本演进。
- 跨语言支持。
- 自动生成代码,类型安全。
4. 关键流程
RPC 调用流程:
- 客户端 Stub 将参数 Protobuf 序列化。
- 通过网络传输到服务端。
- 服务端 Stub 反序列化后调用真实方法。
- 返回值序列化后传回客户端。
5. 故障处理和取舍
| 因素 | 说明 |
|---|---|
| 性能 | Protobuf 比 JSON 快 3-5 倍 |
| 兼容性 | 字段编号确保跨版本兼容 |
| 体积 | Protobuf 体积是 JSON 的 1/3-1/5 |
| 调试 | JSON 可读,Protobuf 需工具解析 |
选型建议:
- 生产 RPC → Protobuf。
- 调试/开发 → JSON。
- Java 内部 → Hessian 或 Kryo。
6. 一句话总结
RPC 序列化模块最优先考虑性能、兼容性和体积,Protobuf 是目前的最佳选择。
介绍docker代码沙箱的实现流程?
原始问法:
- 介绍docker代码沙箱的实现流程?
来源题目:
SRC-14-132-444
面试先答
Docker 代码沙箱是在线判题系统的核心,用于安全执行用户提交的代码。实现流程:1)代码接收:接收用户代码和语言类型;2)代码编译:在容器内编译(如 Java 编译为 .class);3)容器创建:基于语言镜像创建 Docker 容器;4)代码执行:在容器内运行编译后的代码;5)结果采集:采集输出、执行时间、内存使用;6)容器销毁:执行完毕后销毁容器。关键是资源限制(CPU/内存/时间)和安全隔离(防止恶意代码)。
核心结论
- Docker 沙箱流程:接收 → 编译 → 创建容器 → 执行 → 采集 → 销毁。
- 核心:资源限制(CPU/内存/超时)+ 安全隔离。
- 关键技术:Docker 资源限制、cgroup、namespace。
1. 需求假设
- 支持语言:Java、Python、C++。
- 并发判题:100 同时提交。
- 资源限制:CPU 1核、内存 256MB、超时 5s。
2. 容量估算
| 指标 | 数值 |
|---|---|
| 并发判题 | 100 |
| 单题超时 | 5s |
| 容器存活时间 | 10-30s |
| 资源限制 | CPU 1核, 内存 256MB |
3. 架构设计
用户提交 → 判题服务 → 任务队列
↓
沙箱调度器 → Docker 容器池
↓
编译 → 执行 → 采集结果
↓
结果返回 → 容器销毁
核心流程:
public class CodeSandbox {
public JudgeResult judge(SubmitRequest request) {
// 1. 选择或创建容器
String containerId = dockerClient.createContainer(
request.getLanguage(),
"1g", // 内存限制
"1", // CPU限制
5 // 超时时间
);
try {
// 2. 写入用户代码
dockerClient.copyToContainer(containerId,
request.getCodePath(), "/app/Main.java");
// 3. 编译代码
ExecResult compileResult = dockerClient.execInContainer(
containerId, "javac /app/Main.java");
if (compileResult.getExitCode() != 0) {
return JudgeResult.compileError(compileResult.getStderr());
}
// 4. 执行代码(带超时)
ExecResult runResult = dockerClient.execInContainer(
containerId, "timeout 5 java -Xmx256m -cp /app Main");
// 5. 采集结果
return JudgeResult.builder()
.output(runResult.getStdout())
.error(runResult.getStderr())
.time(runResult.getExecutionTime())
.memory(runResult.getMemoryUsage())
.build();
} finally {
// 6. 销毁容器
dockerClient.removeContainer(containerId);
}
}
}
4. 关键流程
- 代码编译:在容器内编译,生成可执行文件。
- 容器创建:基于语言基础镜像,设置资源限制。
- 代码执行:在容器内运行,采集输出和资源使用。
- 结果返回:将执行结果(输出、错误、时间、内存)返回。
- 容器清理:执行完毕后销毁容器,防止资源泄漏。
5. 故障处理和取舍
| 风险 | 处理方案 |
|---|---|
| 编译失败 | 返回编译错误信息 |
| 执行超时 | 强制终止容器 |
| 内存溢出 | 设置内存限制,OOM 时终止 |
| 恶意代码 | 容器隔离 + 资源限制 |
| 容器泄漏 | 定时清理 + 超时强制销毁 |
资源限制手段:
- Docker:
--memory 256m --cpus 1。 - cgroup:CPU、内存、IO 限制。
- 超时:
timeout命令或代码层超时。
6. 一句话总结
Docker 代码沙箱通过容器化隔离 + 资源限制实现安全可靠的用户代码执行环境。
代码沙箱如何防止恶意代码攻击(如死循环、占满内存、文件读写)?
原始问法:
- 代码沙箱如何防止恶意代码攻击(如死循环、占满内存、文件读写)?
来源题目:
SRC-14-132-445
面试先答
代码沙箱防恶意攻击的核心是多层防御体系:1)进程级隔离:每个用户代码在独立进程/容器中执行;2)资源限制:CPU 时间片、内存上限、磁盘配额;3)系统调用过滤:禁止危险系统调用(文件写入、网络、进程创建);4)代码静态检查:编译阶段检测恶意模式;5)运行时监控:实时监测资源使用并超限熔断。关键技术:Docker 容器隔离、Linux cgroup 资源限制、seccomp/apparmor 系统调用过滤、JVM SecurityManager。
核心结论
- 多层防御:容器隔离 + 资源限制 + 系统调用过滤 + 静态检查。
- 核心手段:cgroup(资源限制)+ namespace(隔离)+ seccomp(系统调用过滤)。
- JVM 环境:SecurityManager + 沙箱环境。
1. 需求假设
- 威胁模型:死循环、内存溢出、文件写入、网络攻击、进程 fork。
- 安全等级:中等(防已知攻击,不防 0-day)。
- 性能影响:< 20% 开销。
2. 架构设计
用户代码 → 静态检查 → 编译 → 容器执行
↓
cgroup(资源限制) + seccomp(系统调用过滤)
↓
运行时监控 → 超时/超限熔断
防御层详解:
第一层:容器隔离
# Docker 容器安全配置
docker run \
--security-opt no-new-privileges \ # 禁止提权
--cap-drop ALL \ # 丢弃所有 capabilities
--cap-add NET_BIND_SERVICE \ # 仅保留必要能力
--read-only \ # 只读根文件系统
--tmpfs /tmp:rw,noexec,nosuid,size=64m \ # 临时文件系统
pwn-sandbox:latest
第二层:资源限制(cgroup)
// Docker 资源限制配置
HostConfig hostConfig = new HostConfig()
.withMemory(256 * 1024 * 1024L) // 内存 256MB
.withMemorySwap(256 * 1024 * 1024L) // 禁用 swap
.withCpuShares(512) // CPU 权重
.withNanoCPUs(1000000000L) // 1核 CPU
.withBlkioWeight(100) // IO 权重
.withNetworkMode("none") // 禁用网络
.withReadonlyRootfs(true); // 只读根文件系统
第三层:系统调用过滤(seccomp)
{
"defaultAction": "SCMP_ACT_ERRNO",
"arch": ["SCMP_ARCH_X86_64"],
"syscalls": [
{"names": ["read", "write", "close", "fstat"], "action": "SCMP_ACT_ALLOW"},
{"names": ["brk", "mmap", "mprotect"], "action": "SCMP_ACT_ALLOW"},
{"names": ["exit", "exit_group"], "action": "SCMP_ACT_ALLOW"},
{"names": ["open", "openat", "mkdir"], "action": "SCMP_ACT_ERRNO"},
{"names": ["socket", "connect", "bind"], "action": "SCMP_ACT_ERRNO"},
{"names": ["fork", "clone", "execve"], "action": "SCMP_ACT_ERRNO"}
]
}
第四层:JVM 安全管理
// JVM SecurityManager 配置
Policy.setPolicy(new Policy() {
@Override
public boolean implies(ProtectionDomain d, Permission p) {
// 禁止文件写入
if (p instanceof FilePermission &&
((FilePermission) p).getActions().contains("write"))
return false;
// 禁止网络连接
if (p instanceof NetPermission) return false;
// 禁止进程创建
if (p instanceof RuntimePermission &&
((RuntimePermission) p).getName().startsWith("createProcess"))
return false;
return super.implies(d, p);
}
});
System.setSecurityManager(new SecurityManager());
3. 恶意代码防护
防死循环:
- CPU 时间限制(cgroup
cpu.cfs_period_us+cpu.cfs_quota_us)。 - 进程级超时(
timeout 5命令)。 - JVM 线程中断(
Thread.interrupt())。
防内存溢出:
- 容器内存限制(
--memory 256m)。 - JVM 堆内存限制(
-Xmx256m)。 - 主动检测内存使用并 OOM 时熔断。
防文件读写:
- Docker 只读根文件系统(
--read-only)。 - seccomp 禁止
open/mkdir等系统调用。 - JVM SecurityManager 禁止文件写权限。
防网络攻击:
- Docker 禁用网络(
--network none)。 - seccomp 禁止
socket/connect系统调用。
防进程 fork:
- seccomp 禁止
fork/clone系统调用。 - cgroup
pids.max限制进程数。
4. 关键流程
- 代码上传 → 静态检查(关键词过滤、语法检查)。
- 镜像构建 → 基于语言基础镜像,预装编译工具。
- 容器创建 → 应用安全配置(资源限制、seccomp、只读 FS)。
- 代码执行 → 超时监控 + 资源监控。
- 结果采集 → 输出、错误、时间、内存。
- 容器销毁 → 无论成功失败都销毁。
5. 故障处理和取舍
| 攻击类型 | 防护手段 |
|---|---|
| 死循环 | CPU 限制 + 超时 |
| 内存溢出 | 内存限制 + OOM 熔断 |
| 文件写入 | 只读 FS + seccomp |
| 网络攻击 | 禁用网络 + seccomp |
| 进程 fork | seccomp + pids 限制 |
取舍:
- 安全性 vs 灵活性:限制越强,可用 API 越少。
- 性能 vs 安全:seccomp 增加少量开销。
6. 一句话总结
代码沙箱通过容器隔离 + cgroup 资源限制 + seccomp 系统调用过滤的多层防御体系,全面防止恶意代码攻击。
Docker沙箱在高并发判题场景下,如何避免资源争抢与容器反复创建?
原始问法:
- Docker沙箱在高并发判题场景下,如何避免资源争抢与容器反复创建?
来源题目:
SRC-14-132-446
面试先答
高并发判题场景下 Docker 沙箱的核心问题是容器创建开销大(秒级)和资源争抢(CPU/内存竞争)。解决方案:1)容器池(预创建+复用,避免频繁创建销毁);2)Copy-on-Write(共享基础镜像,写时复制);3)资源隔离(cgroup 限制+调度优化);4)预加载(提前编译、预热);5)分级调度(按语言/资源分级排队)。关键是容器池技术,将容器创建延迟从秒级降到毫秒级。
核心结论
- 核心优化:容器池(预创建+复用)+ Copy-on-Write + 资源隔离。
- 避免反复创建:容器池复用 + 快照恢复。
- 避免资源争抢:cgroup 限制 + 分级调度。
1. 需求假设
- 并发判题:500 同时提交。
- 语言:Java、Python、C++。
- 容器创建延迟:< 100ms(目标)。
- 资源:32 核 CPU,128GB 内存。
2. 容量估算
| 指标 | 传统方式 | 容器池优化 |
|---|---|---|
| 容器创建延迟 | 3-5s | < 100ms |
| 500 并发处理时间 | 250s+ | 10s+ |
| 资源利用率 | 30% | 80% |
3. 架构设计
用户提交 → 调度器 → 容器池管理器
↓
┌── 就绪容器池 ──┐
│ Java: 20 个 │
│ Python: 15 个 │
│ C++: 10 个 │
└───────────────┘
↓
分配容器 → 执行 → 回收
容器池核心设计:
public class ContainerPool {
private Map<String, BlockingQueue<String>> pools;
private int poolSize;
public ContainerPool(int poolSize) {
this.poolSize = poolSize;
this.pools = new ConcurrentHashMap<>();
initPool("java", poolSize);
initPool("python", poolSize);
initPool("cpp", poolSize);
}
private void initPool(String language, int size) {
BlockingQueue<String> queue = new LinkedBlockingQueue<>();
for (int i = 0; i < size; i++) {
String containerId = createWarmContainer(language);
queue.offer(containerId);
}
pools.put(language, queue);
}
// 获取容器
public String acquire(String language) throws InterruptedException {
BlockingQueue<String> queue = pools.get(language);
String containerId = queue.poll(10, TimeUnit.SECONDS);
if (containerId == null) {
// 池空,动态创建
return createWarmContainer(language);
}
return containerId;
}
// 归还容器
public void release(String language, String containerId) {
// 重置容器状态(清空文件、重置环境)
resetContainer(containerId);
pools.get(language).offer(containerId);
}
// 创建预热容器
private String createWarmContainer(String language) {
// 基于语言镜像创建,预装编译环境
return dockerClient.createContainer(
getLanguageImage(language),
getSecurityConfig()
);
}
// 重置容器(快速恢复到干净状态)
private void resetContainer(String containerId) {
// 方案一:执行清理脚本
dockerClient.execInContainer(containerId, "rm -rf /app/*");
// 方案二:提交快照并恢复(更快)
dockerClient.commitContainer(containerId, "clean-snapshot");
}
}
4. 关键优化手段
1. 容器池 + 预热
启动时预创建 N 个容器 → 池化管理 → 按需分配 → 执行后回收
2. Copy-on-Write 优化
基础镜像(只读层) ← 所有容器共享
容器可写层 ← 每个容器独立
执行完毕丢弃可写层
3. 资源隔离与调度
按语言分级:
- Java 容器:256MB 内存,1 核 CPU
- Python 容器:128MB 内存,0.5 核 CPU
- C++ 容器:64MB 内存,0.5 核 CPU
调度策略:
- 优先分配轻量容器
- 按资源需求排队
4. 快照恢复(Fast Reset)
容器执行完毕 → 提交快照 → 恢复快照 → 快速复用
比重新创建快 10-50 倍
5. 故障处理和取舍
| 问题 | 解决方案 |
|---|---|
| 容器泄漏 | 超时强制回收 + 定期健康检查 |
| 池耗尽 | 动态扩容 + 排队等待 |
| 资源争抢 | cgroup 限制 + CPU 亲和性绑定 |
| 容器污染 | 快照恢复 + 只读根 FS |
关键取舍:
- 容器池大小:过大浪费资源,过小增加等待。需根据峰值 QPS 动态调整。
- 复用 vs 新建:复用快但需清理,新建慢但干净。用快照平衡。
6. 性能优化总结
| 优化点 | 效果 |
|---|---|
| 预创建容器 | 创建延迟 3s → 100ms |
| 容器复用 | 500 并发处理能力提升 10x |
| 快照恢复 | 容器重置时间从秒级 → 毫秒级 |
| 分级资源 | 资源利用率从 30% → 80% |
| Copy-on-Write | 磁盘占用降低 90% |
7. 一句话总结
高并发 Docker 沙箱通过容器池 + 快照复用 + 资源隔离实现毫秒级容器分配和高效资源利用。
如何设计一个日志系统?如何处理海量日志?
原始问法:
- 如何设计一个日志系统?如何处理海量日志?
来源题目:
SRC-14-132-447
面试先答
海量日志系统的核心是采集、传输、存储、检索四大环节。主流架构 ELK(Elasticsearch + Logstash + Kibana)或 EFK(Elasticsearch + Filebeat + Kibana)。设计要点:1)日志采集:轻量 Agent(Filebeat/Fluentd)从应用端采集;2)日志传输:Kafka/RabbitMQ 做消息队列缓冲;3)日志存储:Elasticsearch 做全文检索存储,冷热分离;4)日志检索:Kibana 提供查询 UI,支持复杂查询。海量处理关键:异步化、分层存储、索引优化、压缩。
核心结论
- 日志系统架构:采集 → MQ → 存储 → 检索。
- 主流方案:ELK/EFK 技术栈。
- 海量处理:异步化、冷热分离、索引优化、压缩。
1. 需求假设
- 日志量:100GB/天 → 30TB/月。
- 日志类型:应用日志、系统日志、访问日志。
- 检索需求:实时检索(秒级)、按条件查询、聚合分析。
- 保留周期:热数据 7 天,温数据 30 天,冷数据 180 天。
2. 容量估算
| 指标 | 数值 |
|---|---|
| 日志量 | 100GB/天 |
| 日志条目 | 10 亿/天 |
| 写入 QPS | 100,000 |
| 存储总量 | 30TB/月 |
| 检索延迟 | < 3s |
3. 架构设计
应用服务器 → Filebeat(轻量采集) → Kafka(消息队列) → Logstash(处理) → Elasticsearch(存储) → Kibana(检索)
↓
冷热分离(ILM)
↓
对象存储(S3/OSS)
核心组件:
| 组件 | 作用 | 技术选型 |
|---|---|---|
| 采集器 | 日志采集 | Filebeat, Fluentd, Logtail |
| 消息队列 | 缓冲削峰 | Kafka, RocketMQ |
| 处理器 | 清洗转换 | Logstash, Flink |
| 存储引擎 | 索引存储 | Elasticsearch, ClickHouse |
| 可视化 | 查询展示 | Kibana, Grafana |
4. 海量日志处理
1. 异步化
应用日志 → 异步写入本地文件 → Filebeat 采集 → Kafka → Logstash → ES
- 应用写日志不阻塞。
- Kafka 削峰,ES 不直接承受应用写入压力。
2. 冷热分离(ILM)
// Elasticsearch Index Lifecycle Management
PUT _ilm/policy/my-lifecycle
{
"policy": {
"phases": {
"hot": {
"min_age": "0ms",
"actions": {
"rollover": {
"max_primary_shard_size": "50gb"
},
"set_priority": {
"priority": 100
}
}
},
"warm": {
"min_age": "7d",
"actions": {
"forcemerge": {"max_num_segments": 1},
"shrink": {"number_of_shards": 1},
"allocate": {"require": {"data": "warm"}},
"set_priority": {"priority": 50}
}
},
"cold": {
"min_age": "30d",
"actions": {
"freeze": {},
"allocate": {"require": {"data": "cold"}}
}
},
"delete": {
"min_age": "180d",
"actions": {"delete": {}}
}
}
}
}
3. 索引优化
- 按天/按周创建索引,便于过期删除。
- 合理设置 shard 数(每个 shard 10-50GB)。
- 使用 mapping 优化:keyword(精确匹配)vs text(全文检索)。
- 关闭不需要的字段索引(doc_values、norms)。
- 使用 _source filtering 减少存储。
4. 压缩与降采样
- 日志压缩:gzip 压缩传输。
- 字段降采样:非关键字段降精度。
- 历史数据归档:冷数据压缩后存对象存储。
5. 数据模型
// Elasticsearch 索引模板
PUT _template/logs_template
{
"index_patterns": "logs-*",
"settings": {
"number_of_shards": 3,
"number_of_replicas": 1,
"refresh_interval": "30s",
"index.translog.durability": "async"
},
"mappings": {
"properties": {
"@timestamp": {"type": "date"},
"level": {"type": "keyword"},
"service": {"type": "keyword"},
"trace_id": {"type": "keyword"},
"user_id": {"type": "keyword"},
"message": {"type": "text", "analyzer": "ik_max_word"},
"stack_trace": {"type": "text", "index": false},
"host": {"type": "keyword"},
"ip": {"type": "ip"}
}
}
}
6. 故障处理和取舍
| 风险 | 处理方案 |
|---|---|
| 日志丢失 | Filebeat 本地缓存 + Kafka 持久化 |
| ES 过载 | 限流 + 批量写入 + 延迟刷新 |
| 磁盘满 | ILM 自动删除 + 告警 |
| 查询慢 | 索引优化 + 预热 + 缓存 |
| 数据迁移 | 冷热分离 + 快照恢复 |
关键取舍:
- 实时性 vs 吞吐量:调整 refresh_interval(默认 1s → 30s)。
- 存储成本 vs 查询性能:冷热分离,冷数据压缩归档。
- 精确查询 vs 模糊查询:keyword vs text。
7. 替代方案
| 方案 | 特点 | 适用场景 |
|---|---|---|
| ELK | 成熟生态,功能完善 | 通用场景 |
| EFK | Filebeat 更轻量 | 云原生 |
| ClickHouse | 列式存储,分析高效 | 日志分析 |
| Loki | 轻量,便宜 | Kubernetes 日志 |
| Splunk | 商业方案 | 企业级 |
| 自研(Kafka+PG) | 定制化 | 特殊需求 |
8. 一句话总结
海量日志系统的核心是异步采集 + 消息队列缓冲 + 分层存储 + 索引优化,ELK/EFK 是主流方案,ClickHouse 适合分析场景。