目录

14-系统设计

发表于
2 82.1~105.6 分钟 36950

设计一个秒杀系统?

原始问法:

    1. 设计一个秒杀系统?

来源题目: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

关键流程

  1. 活动预热:提前将库存加载到 Redis。
  2. 前端削峰:活动页加载、随机码校验。
  3. 网关限流:令牌桶/漏桶算法,超过则返回"稍后重试"。
  4. 库存预扣:Redis Lua 脚本原子扣减,返回秒杀结果。
  5. 异步下单:扣减成功发 MQ,消费者异步创建订单。
  6. 最终落库:订单写库,扣减数据库库存(乐观锁)。
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 异步下单实现高并发下的无超卖秒杀。


设计一个抢票系统?

原始问法:

    1. 设计一个抢票系统?

来源题目: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. 关键流程

抢票流程

  1. 用户选择场次和票价等级。
  2. Redis 原子检查可用座位。
  3. 分配座位(从 Set 中随机或顺序取一个)。
  4. 创建锁定订单(15 分钟超时)。
  5. 引导支付。
  6. 支付成功 → 订单状态改为已售。
  7. 支付超时 → 订单状态改为已退,座位归还。

火车票特殊逻辑

  • 座位按区间占用(A 站到 C 站经过 B 站,A-B 和 B-C 段都占用)。
  • 需要维护每个区间的座位可用数。
6. 故障处理和取舍
风险 处理方案
重复购票 用户维度唯一约束 + 一人一单
支付超时 定时任务扫描释放锁定座位
座位冲突 Redis 原子操作 + 分布式锁
缓存一致性 Redis 与 DB 对账 + 最终一致性

取舍

  • 抢票 vs 秒杀:抢票更复杂,需座位分配和锁定超时。
  • 强一致 vs 最终一致:锁定-支付-确认保证核心流程一致性。
7. 一句话总结

抢票系统通过Redis 管理座位状态 + 锁定-支付-确认三阶段实现高并发下的座位分配和防超售。


设计一个微信红包系统?

原始问法:

    1. 设计一个微信红包系统?

来源题目: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. 关键流程

发红包流程

  1. 发送者扣款到红包冻结账户。
  2. 生成红包 ID(分布式 ID)。
  3. Redis 预存剩余金额和人数。
  4. 写入红包记录到数据库。
  5. 推送通知给接收者。

拆红包流程(核心):

// 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 原子操作 + 二倍均值法实现高并发下的安全拆红包,核心是幂等性和资金一致性。


设计一个延迟队列?

原始问法:

    1. 设计一个延迟队列?

来源题目: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 方案

  1. 生产者 ZADD 任务到 ZSet,score 为目标时间。
  2. 定时任务每 1 秒执行 ZRANGEBYSCORE 获取到期任务。
  3. 取出任务后 ZREM 删除。
  4. 异步执行任务。

时间轮方案

// 简化的时间轮实现
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生成器?

原始问法:

    1. 设计一个分布式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. 关键流程
  1. 机器 ID 分配:通过 Zookeeper 创建临时顺序节点,分配唯一 ID。
  2. ID 生成:每实例独立生成,无需网络调用。
  3. 时钟回拨处理:等待或抛异常。
6. 故障处理和取舍
方案 优点 缺点
UUID 简单,无中心化 无序,太长(36字符)
数据库号段 简单有序 DB 压力大,延迟高
雪花算法 高性能,有序 时钟回拨问题
Leaf 可靠,高性能 依赖 ZK + DB

时钟回拨解决方案

  1. 等待模式:回拨 < 5ms 时等待。
  2. 备用实例:主实例回拨时切备用实例。
  3. 逻辑时钟:用逻辑时间替代物理时间。
7. 一句话总结

分布式 ID 生成器的核心是在高性能和唯一性之间平衡,雪花算法是目前最主流的方案。


设计一个分布式锁?

原始问法:

    1. 设计一个分布式锁?

来源题目: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 加锁流程

  1. 客户端生成唯一 requestId(UUID)。
  2. SET lockKey requestId NX PX 30000(设置 30s 过期)。
  3. 返回成功则获取锁,失败则重试或排队。

Redis 解锁流程

  1. Lua 脚本检查 value 是否等于 requestId。
  2. 相等则删除,防止误删他人锁。
  3. 不相等则返回失败(锁已过期或被他人持有)。
6. 故障处理和取舍
风险 处理方案
锁过期业务未完成 续期机制(Redisson Watchdog)
Redis 主从切换 红锁(RedLock)算法
客户端崩溃 自动过期释放锁
误删他人锁 value 校验 + Lua 原子解锁

RedLock 算法:向多个独立 Redis 实例加锁,过半成功则认为获取锁。

Redis vs Zookeeper

  • Redis:高性能,弱一致,适合对一致性要求不高的场景。
  • Zookeeper:强一致,性能低,适合对一致性要求高的场景。
7. 一句话总结

分布式锁的核心是在分布式环境下实现互斥,Redis 方案最常用,需注意原子性、可重入和高可用。


设计一个接口限流系统?

原始问法:

    1. 设计一个接口限流系统?

来源题目: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 限流流程

  1. 规则定义(QPS、并发数、降级策略)。
  2. 实时统计每个资源的请求数和响应时间。
  3. 基于规则和统计数据做限流判断。
  4. 触发限流时执行降级逻辑(快速失败、返回默认值等)。
6. 故障处理和取舍
算法 优点 缺点
固定窗口 实现简单 临界突刺
滑动窗口 平滑 内存占用高
漏桶 匀速输出 不允许突发
令牌桶 允许突发 实现略复杂

选型

  • 允许突发 → 令牌桶。
  • 严格平滑 → 漏桶或滑动窗口。
  • 生产环境 → Sentinel(综合策略)。
7. 一句话总结

接口限流通过令牌桶等算法控制请求速率,保护系统稳定性,Sentinel 是生产级首选。


设计一个评论系统?

原始问法:

    1. 设计一个评论系统?

来源题目: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. 关键流程

发表评论

  1. 写入 MySQL(主库)。
  2. 增量更新 Redis 缓存。
  3. 发送通知(被回复者)。

查询评论列表

  1. 查 Redis 缓存的评论 ID 列表。
  2. 批量查询评论详情(Pipeline)。
  3. 按分页返回。

点赞

  1. Redis 原子操作 INCR 点赞数。
  2. 异步落库。
6. 故障处理和取舍
风险 处理方案
评论过多 分表 + 分页加载更多
热点评论缓存穿透 布隆过滤器 + 空值缓存
敏感词 文本审核 + 关键词过滤
楼中楼展开 二级评论单独分页加载

取舍

  • 读多写少 → 缓存优先。
  • 一致性 → 最终一致(点赞数允许延迟)。
7. 一句话总结

评论系统的核心是层级结构存储 + 高并发读写优化,通过邻接表存储、缓存热点、读写分离实现高效评论服务。


设计一个 feed 流系统?

原始问法:

    1. 设计一个 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 → 分页返回

推模式流程

  1. 用户发帖。
  2. 读取粉丝列表(Redis Set)。
  3. 将帖子 ID 推送到每个粉丝的收件箱(Redis ZAdd)。
  4. 截断到最大长度(如 500 条)。

拉模式流程

  1. 用户登录。
  2. 从 Redis ZSet 拉取收件箱内容。
  3. 分页返回。

推+拉结合

  • 大 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视频,应该怎么设计?

原始问法:

    1. 弱网环境下上传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)

核心流程

  1. 文件 MD5 校验:上传前计算整个文件的 MD5。
  2. 秒传判断:服务端检查 MD5 是否已存在,存在则直接完成。
  3. 分片上传:将文件切分为固定大小分片。
  4. 并发控制:3-5 个分片并发上传。
  5. 断点续传:记录已上传分片,续传时跳过已完成分片。
  6. 合并校验:所有分片上传完成后,服务端合并并校验。
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 校验 + 补传

弱网优化策略

  1. 自适应并发:监测带宽动态调整并发数。
  2. 压缩优化:视频压缩后上传。
  3. 增量上传:只上传修改部分。
  4. CDN 加速:就近节点上传。
7. 一句话总结

弱网大文件上传的核心是分片 + 断点续传 + 自适应优化,通过分而治之和状态持久化实现可靠传输。


100个进程,5个并发数的工作,应该怎么设计?

原始问法:

    1. 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. 关键流程
  1. 任务提交到队列。
  2. 最多 5 个工作线程并发执行。
  3. 完成后从队列取下一个任务。
  4. 所有任务完成后汇总结果。
5. 故障处理和取舍
方案 优点 缺点
线程池 简单,管理方便 JVM 内有效
信号量 灵活,可动态调整 需手动创建线程
消息队列 分布式,解耦 运维成本高

选型

  • 单机 → 线程池。
  • 分布式 → MQ + 消费者组。
6. 一句话总结

100 个任务 5 个并发的核心是限流调度,线程池是最简单高效的单机方案。


高并发场景下,如何解决数据库读写性能瓶颈?

原始问法:

    1. 高并发场景下,如何解决数据库读写性能瓶颈?

来源题目: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. 关键流程

请求处理流程

  1. 查缓存 → 命中则返回。
  2. 缓存未命中 → 查从库。
  3. 从库未命中 → 查主库(保证一致性场景)。
  4. 写入 → 写主库 + 删缓存。
5. 故障处理和取舍
方案 优点 缺点
缓存 高性能 一致性问题、内存成本
读写分离 分散读压力 主从延迟、写仍是单点
分库分表 分散写压力 复杂度高、跨库 Join 难
异步化 削峰 最终一致性

选型顺序:缓存 → 读写分离 → 分库分表 → 异步化。

6. 一句话总结

高并发数据库优化的核心是减少请求量和分散压力,缓存是首选,分库分表是最后手段。


抛开MQ,自己实现延迟功能,你会怎么做?

原始问法:

    1. 抛开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 方案:

  1. 添加任务:ZADD key taskId executeTime
  2. 定时扫描:ZRANGEBYSCORE key 0 now
  3. 取出任务:ZREM key taskId
  4. 执行任务(异步)。
4. 故障处理和取舍
方案 优点 缺点
ScheduledExecutor 简单 单机,JVM 重启丢失
Redis ZSet 分布式,可靠 需定时扫描
时间轮 O(1) 添加 实现复杂
DB 定时 最简单 效率极低

选型

  • 单机 → ScheduledExecutor。
  • 分布式 → Redis ZSet。
  • 海量任务 → 时间轮。
5. 一句话总结

无 MQ 延迟实现的核心是用时间戳排序存储 + 定时扫描,Redis ZSet 是最通用的分布式方案。


设计一个RPC框架时,序列化模块最优先考虑的因素是什么?

原始问法:

    1. 设计一个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 ★★★★ ★★★★ ★★★★ 支持 Facebook
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 调用流程:

  1. 客户端 Stub 将参数 Protobuf 序列化。
  2. 通过网络传输到服务端。
  3. 服务端 Stub 反序列化后调用真实方法。
  4. 返回值序列化后传回客户端。
5. 故障处理和取舍
因素 说明
性能 Protobuf 比 JSON 快 3-5 倍
兼容性 字段编号确保跨版本兼容
体积 Protobuf 体积是 JSON 的 1/3-1/5
调试 JSON 可读,Protobuf 需工具解析

选型建议

  • 生产 RPC → Protobuf。
  • 调试/开发 → JSON。
  • Java 内部 → Hessian 或 Kryo。
6. 一句话总结

RPC 序列化模块最优先考虑性能、兼容性和体积,Protobuf 是目前的最佳选择。


介绍docker代码沙箱的实现流程?

原始问法:

    1. 介绍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. 关键流程
  1. 代码编译:在容器内编译,生成可执行文件。
  2. 容器创建:基于语言基础镜像,设置资源限制。
  3. 代码执行:在容器内运行,采集输出和资源使用。
  4. 结果返回:将执行结果(输出、错误、时间、内存)返回。
  5. 容器清理:执行完毕后销毁容器,防止资源泄漏。
5. 故障处理和取舍
风险 处理方案
编译失败 返回编译错误信息
执行超时 强制终止容器
内存溢出 设置内存限制,OOM 时终止
恶意代码 容器隔离 + 资源限制
容器泄漏 定时清理 + 超时强制销毁

资源限制手段

  • Docker:--memory 256m --cpus 1
  • cgroup:CPU、内存、IO 限制。
  • 超时:timeout 命令或代码层超时。
6. 一句话总结

Docker 代码沙箱通过容器化隔离 + 资源限制实现安全可靠的用户代码执行环境。


代码沙箱如何防止恶意代码攻击(如死循环、占满内存、文件读写)?

原始问法:

    1. 代码沙箱如何防止恶意代码攻击(如死循环、占满内存、文件读写)?

来源题目: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. 关键流程
  1. 代码上传 → 静态检查(关键词过滤、语法检查)。
  2. 镜像构建 → 基于语言基础镜像,预装编译工具。
  3. 容器创建 → 应用安全配置(资源限制、seccomp、只读 FS)。
  4. 代码执行 → 超时监控 + 资源监控。
  5. 结果采集 → 输出、错误、时间、内存。
  6. 容器销毁 → 无论成功失败都销毁。
5. 故障处理和取舍
攻击类型 防护手段
死循环 CPU 限制 + 超时
内存溢出 内存限制 + OOM 熔断
文件写入 只读 FS + seccomp
网络攻击 禁用网络 + seccomp
进程 fork seccomp + pids 限制

取舍

  • 安全性 vs 灵活性:限制越强,可用 API 越少。
  • 性能 vs 安全:seccomp 增加少量开销。
6. 一句话总结

代码沙箱通过容器隔离 + cgroup 资源限制 + seccomp 系统调用过滤的多层防御体系,全面防止恶意代码攻击。


Docker沙箱在高并发判题场景下,如何避免资源争抢与容器反复创建?

原始问法:

    1. 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 沙箱通过容器池 + 快照复用 + 资源隔离实现毫秒级容器分配和高效资源利用。


如何设计一个日志系统?如何处理海量日志?

原始问法:

    1. 如何设计一个日志系统?如何处理海量日志?

来源题目: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 适合分析场景。


推荐文章

02-Java集合
19-HR与软技能
18-Git
下一篇 15-AI与智能体