面试话术

EduAgentX 项目面试话术指南

一、项目整体介绍(30秒版本)

“我做的是一个智能教育管理系统 EduAgentX,核心功能包括 AI 智能题目生成高并发抢课系统RAG 知识库检索。技术栈是 Spring Boot 3 + Spring AI + Redis + RocketMQ,同时我还完成了从单体到微服务的架构演进,使用 Spring Cloud Alibaba + Dubbo + Nacos + Higress 网关。项目中我重点解决了三个技术难题:抢课系统的高并发防超卖AI 工作流的流式输出、以及 热点数据的缓存优化。”


零、八股文融合速查表

八股知识点 项目中的应用场景 引出话术
Redis 单线程模型 Lua 脚本原子性 “Lua 能保证原子性是因为 Redis 单线程…”
Redis 数据结构 Hash 存储库存/抢课状态 “我用 Hash 而不是 String,因为…”
Redis 持久化 抢课数据可靠性 “我配置了 AOF 持久化,防止宕机丢数据…”
线程池参数 异步工作流执行 “我自定义了线程池,核心线程数设置为…”
CAS 与原子类 HeavyKeeper 计数器 “我用 AtomicInteger 保证计数的线程安全…”
ConcurrentHashMap 热点 Key 存储 “我用 ConcurrentHashMap 而不是 HashMap…”
阻塞队列 消息缓冲区 “我用 ConcurrentLinkedQueue 无锁队列…”
AOP 原理 限流切面、性能监控 “我用 AOP 实现了限流,原理是动态代理…”
Spring 事务 批量写入数据库 “我用 TransactionTemplate 编程式事务…”
MySQL 索引 抢课记录查询优化 “我给 student_id + subject_id 建了联合索引…”
MySQL 锁 纯 MySQL 抢课方案 “我还实现了一版用悲观锁的方案…”
消息队列 异步削峰 “我用 RocketMQ 实现异步,选它是因为…”
设计模式 题目生成模块 “我用了策略模式、模板方法、工厂模式…”
JVM 内存模型 ThreadLocal 存储 SSE “我用 ThreadLocal 存储 Emitter,避免…”
序列化 Redis 序列化配置 “我配置了 Jackson 序列化,解决了…”
分布式锁 防止重复抢课 “除了 Lua,我还可以用 Redisson 分布式锁…”
CAP 理论 缓存一致性策略 “我选择了 AP,保证可用性,最终一致…”
限流算法 滑动窗口限流 “我实现了滑动窗口限流,用 Redis ZSet…”

二、核心亮点详细话术(融合八股版)

亮点一:高并发抢课系统(Redis + Lua + RocketMQ)

问题背景

“在抢课场景下,我们面临的核心问题是 高并发下的库存超卖数据库压力过大。比如一门课只有100个名额,但可能有1000个学生同时点击抢课,如果处理不当就会出现超卖,或者数据库直接被打挂。”

技术方案

“我的解决方案分三层:

第一层:Redis + Lua 脚本保证原子性

  • 把课程库存预热到 Redis 的 Hash 结构中
  • 使用 Lua 脚本实现原子性的「检查库存 → 扣减库存 → 记录抢课状态」
  • 这样即使万级并发,也不会出现超卖

第二层:RocketMQ 异步削峰

  • Redis 操作成功后,不直接写数据库,而是发送消息到 RocketMQ
  • 消费者批量消费(每批1000条或10秒),批量写入 MySQL
  • 这样把瞬时高并发转化为平稳的数据库写入

第三层:失败回滚机制

  • 如果消息发送失败,立即执行 Redis 回滚脚本
  • 保证 Redis 和最终数据库的数据一致性”

代码亮点(可以主动提)

“Lua 脚本的核心逻辑是这样的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
-- 1. 检查是否已抢课
if redis.call('HEXISTS', studentSnatchKey, subjectId) == 1 then
    return -1  -- 已抢课
end
-- 2. 检查并扣减库存(原子操作)
local capacity = redis.call('HINCRBY', capacityKey, subjectId, -1)
if capacity < 0 then
    redis.call('HINCRBY', capacityKey, subjectId, 1)  -- 回滚
    return -2  -- 库存不足
end
-- 3. 记录抢课状态
redis.call('HSET', studentSnatchKey, subjectId, 1)
return 1  -- 成功

整个过程在 Redis 单线程中执行,天然保证原子性。”

性能数据

“优化后,抢课接口的 QPS 从原来纯 MySQL 的 500 提升到 2000+,响应时间从 200ms 降到 50ms 以内。”

🎯 融合八股:Redis 单线程模型

面试官追问:为什么 Lua 脚本能保证原子性?

回答:“这要从 Redis 的线程模型说起。Redis 6.0 之前是纯单线程模型,所有命令都在一个线程中串行执行。虽然 6.0 引入了 IO 多线程(处理网络读写),但命令执行仍然是单线程的。

Lua 脚本在执行期间,Redis 不会执行其他命令,相当于把多个操作打包成一个原子操作。这和数据库事务不同——数据库是通过来保证隔离性,而 Redis 是通过单线程串行执行来保证原子性。

需要注意 Lua 脚本不能太长,否则会阻塞其他请求。我的脚本只有几行,执行时间在微秒级。”

🎯 融合八股:Redis 数据结构选择

面试官追问:为什么用 Hash 存储库存,而不是 String?

回答:“我用 Hash 有几个考虑:

  1. 内存效率:如果用 String,每个课程一个 Key(如 capacity:1capacity:2),会有大量的 Key 元数据开销。用 Hash(subject:capacity{1: 100, 2: 50}),多个字段共享一个 Key 的元数据,内存更省。

  2. 原子操作HINCRBY 可以原子性地对某个字段加减,配合 Lua 脚本很方便。

  3. 批量操作HGETALL 可以一次获取所有课程的库存,方便做缓存预热。

Redis 的 Hash 底层是 ziplist(小数据量)或 hashtable(大数据量),当字段数超过 hash-max-ziplist-entries(默认512)时会转换。我的场景课程数不多,用 ziplist 更省内存。”

🎯 融合八股:消息队列对比

面试官追问:为什么选 RocketMQ 而不是 Kafka?

回答:“我对比了几个主流消息队列:

特性 RocketMQ Kafka RabbitMQ
吞吐量 10万级 百万级 万级
延迟 ms级 ms级 us级
事务消息 ✅ 支持 ❌ 不支持 ❌ 不支持
延迟消息 ✅ 支持 ❌ 不支持 ✅ 插件支持
消息回溯 ✅ 支持 ✅ 支持 ❌ 不支持

选择 RocketMQ 的原因:

  1. 事务消息:可以保证本地事务和消息发送的一致性
  2. 延迟消息:未来可以做延迟退课提醒
  3. 消息回溯:出问题时可以重新消费历史消息
  4. 阿里生态:和 Spring Cloud Alibaba 集成更好”

🎯 融合八股:MySQL 悲观锁 vs 乐观锁

面试官追问:如果不用 Redis,纯 MySQL 怎么实现?

回答:“我其实还实现了一版纯 MySQL 的方案,用的是悲观锁

1
2
3
4
-- 悲观锁:SELECT FOR UPDATE
SELECT * FROM snatch_subject WHERE subject_id = ? FOR UPDATE;
-- 检查库存后更新
UPDATE snatch_subject SET stock_remain = stock_remain - 1 WHERE subject_id = ?;

悲观锁 vs 乐观锁的选择:

  • 悲观锁:适合写多读少、冲突概率高的场景,但会阻塞其他事务
  • 乐观锁:适合读多写少、冲突概率低的场景,通过版本号实现

抢课场景冲突概率高,所以用悲观锁更合适。但悲观锁的问题是性能差(QPS 只有 500),所以最终选择了 Redis 方案。”


亮点二:AI 工作流引擎(LangGraph4j + SSE 流式输出)

问题背景

“AI 生成题目是一个耗时操作,可能需要 10-30 秒。如果用传统的同步请求,用户体验很差,而且容易超时。另外,题目生成涉及多个步骤:知识检索 → 任务拆分 → 题目生成 → 质量检查,需要一个灵活的工作流来编排。”

技术方案

“我使用了 LangGraph4j 作为工作流引擎,配合 SSE(Server-Sent Events) 实现流式输出:

工作流设计

  1. RAG 知识检索节点 - 从向量数据库检索相关知识点
  2. 任务列表节点 - 根据题目数量创建生成任务队列
  3. 题目生成节点 - 调用大模型生成题目
  4. 质量检查节点 - 验证题目格式和逻辑
  5. 条件路由 - 质检不通过则重新生成,通过则继续下一题

流式输出实现

  • 使用 ThreadLocal 存储 SseEmitter,避免序列化问题
  • 工作流异步执行,每个节点完成后实时推送结果
  • 前端通过 EventSource 监听,实时展示生成进度”

代码亮点

“条件路由的实现是这样的:

1
2
3
4
5
6
7
.addConditionalEdges("ques_parse_check_node",
    edge_async(this::routeAfterCheck),
    Map.of(
        \"continue_generate\", \"ques_generate_node\",  // 继续生成下一题
        \"retry_generate\", \"ques_generate_node\",     // 重新生成当前题
        \"finish\", END                                 // 完成
    ))

这样就实现了一个带重试机制的循环工作流。”

设计模式应用

“在题目生成模块,我还应用了多种设计模式:

  • 策略模式:不同题型(选择题、填空题、大题)使用不同的生成策略
  • 模板方法模式:题目存储流程统一,但具体存储逻辑由子类实现
  • 工厂模式:根据题型创建对应的生成器
  • 门面模式:AIQuestionFacade 统一对外接口”

🎯 融合八股:ThreadLocal 原理与内存泄漏

面试官追问:为什么用 ThreadLocal 存储 SseEmitter?

回答:“因为 SseEmitter 不能序列化,不能放到工作流上下文中传递。我用 ThreadLocal 存储,每个线程有自己的副本。

1
2
3
4
5
6
7
public static final ThreadLocal<SseEmitter> SSE_EMITTER_HOLDER = new ThreadLocal<>();

// 异步执行前设置
SSE_EMITTER_HOLDER.set(emitter);

// 执行完毕后清理(重要!)
SSE_EMITTER_HOLDER.remove();

ThreadLocal 原理:每个 Thread 内部有一个 ThreadLocalMap,Key 是 ThreadLocal 对象(弱引用),Value 是存储的值。

内存泄漏问题:如果不调用 remove(),在线程池场景下,线程会被复用,ThreadLocalMap 中的 Entry 不会被清理,导致内存泄漏。所以我在 finally 块中一定会调用 remove()。”

🎯 融合八股:线程池参数配置

面试官追问:异步执行用的什么线程池?

回答:“我用的是 CompletableFuture.runAsync(),默认使用 ForkJoinPool.commonPool()。但在生产环境,我会自定义线程池:

1
2
3
4
5
6
7
8
ThreadPoolExecutor executor = new ThreadPoolExecutor(
    4,                      // 核心线程数 = CPU核心数
    8,                      // 最大线程数 = 2 * CPU核心数
    60, TimeUnit.SECONDS,   // 空闲线程存活时间
    new LinkedBlockingQueue<>(1000),  // 有界队列,防止OOM
    new ThreadFactoryBuilder().setNameFormat(\"ai-workflow-%d\").build(),
    new CallerRunsPolicy()  // 拒绝策略:调用者执行
);

参数设置原则

  • CPU 密集型:核心线程数 = CPU 核心数
  • IO 密集型:核心线程数 = CPU 核心数 * 2(或更多)
  • AI 生成是 IO 密集型(等待 API 响应),所以可以设置多一些线程

拒绝策略选择

  • AbortPolicy:直接抛异常(默认)
  • CallerRunsPolicy:调用者线程执行(我选这个,保证任务不丢失)
  • DiscardPolicy:静默丢弃
  • DiscardOldestPolicy:丢弃最老的任务”

🎯 融合八股:设计模式详解

面试官追问:能详细说说策略模式怎么用的吗?

回答:“以题目生成为例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// 1. 策略接口
public interface QuestionGenerator {
    Question generate(String knowledge, String difficulty);
}

// 2. 具体策略
@Component(\"multipleChoice\")
public class MultipleChoiceGenerator implements QuestionGenerator { ... }

@Component(\"fillBlank\")
public class FillBlankGenerator implements QuestionGenerator { ... }

// 3. 策略选择(配合 Spring 容器)
@Resource
private Map<String, QuestionGenerator> generatorMap;  // Spring 自动注入所有实现

public Question generate(String type, String knowledge) {
    QuestionGenerator generator = generatorMap.get(type);
    return generator.generate(knowledge, difficulty);
}

策略模式的好处

  1. 开闭原则:新增题型只需添加新的策略类,不修改原有代码
  2. 消除 if-else:避免大量的条件判断
  3. 易于测试:每个策略可以独立测试”

亮点三:热点数据缓存优化(HeavyKeeper + 二级缓存)

问题背景

“在抢课高峰期,某些热门课程会被大量访问,形成热点 Key。如果每次都访问 Redis,会造成 Redis 压力过大,响应时间从正常的 5ms 飙升到 200ms+。”

技术方案

“我实现了一套 热点检测 + 二级缓存 的方案:

热点检测(AsyncHeavyKeeper)

  • 使用 HeavyKeeper 算法实时检测 Top K 热点 Key
  • 核心优化:将耗时操作异步化,add() 方法耗时从 5ms 降到 <1ms
  • 使用 ConcurrentLinkedQueue 无锁队列,后台线程批量处理

二级缓存架构

  • L1 缓存:Caffeine 本地缓存,容量 10000,过期时间 60 秒
  • L2 缓存:Redis 分布式缓存
  • 读操作:先查 L1 → 未命中查 L2 → 写入 L1
  • 写操作:直接写 Redis(保证一致性),不走本地缓存”

关键设计决策

“这里有一个重要的设计决策:写操作不使用本地缓存

原因是:抢课是写操作,需要强一致性。如果用本地缓存,在分布式环境下会出现数据不一致。比如 A 服务器的本地缓存显示还有库存,但实际 Redis 中已经没了。

所以我的策略是:

  • 写操作(抢课/退课):直接走 Redis Lua 脚本
  • 读操作(查询库存/状态):走二级缓存,提升性能”

🎯 融合八股:ConcurrentHashMap 原理

面试官追问:热点 Key 存储为什么用 ConcurrentHashMap?

回答:“因为热点检测是多线程并发访问的场景。

HashMap 的问题

  • 非线程安全,多线程 put 可能导致死循环(JDK7 的头插法)或数据丢失

ConcurrentHashMap 的优势(JDK8+):

  • 使用 CAS + synchronized 保证线程安全
  • 锁粒度是单个 Node,而不是整个 Map
  • 读操作完全无锁(volatile 保证可见性)
1
2
3
4
5
6
// 我的代码中
private final ConcurrentHashMap<String, AtomicInteger> hotKeyMap;

// computeIfAbsent 是原子操作
AtomicInteger counter = hotKeyMap.computeIfAbsent(key, k -> new AtomicInteger(0));
counter.addAndGet(increment);  // AtomicInteger 也是线程安全的

为什么不用 Hashtable?

  • Hashtable 用 synchronized 锁整个表,性能差
  • ConcurrentHashMap 锁粒度更细,并发性能更好”

🎯 融合八股:CAS 与原子类

面试官追问:AtomicInteger 怎么保证线程安全的?

回答:“AtomicInteger 底层使用 CAS(Compare And Swap) 实现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// AtomicInteger.addAndGet 的底层实现
public final int addAndGet(int delta) {
    return U.getAndAddInt(this, VALUE, delta) + delta;
}

// Unsafe.getAndAddInt
public final int getAndAddInt(Object o, long offset, int delta) {
    int v;
    do {
        v = getIntVolatile(o, offset);  // 读取当前值
    } while (!compareAndSwapInt(o, offset, v, v + delta));  // CAS 更新
    return v;
}

CAS 原理:比较内存值和预期值,相等则更新,不相等则重试。

CAS 的问题

  1. ABA 问题:值从 A→B→A,CAS 认为没变化。解决:AtomicStampedReference(带版本号)
  2. 自旋开销:高并发时大量线程自旋,CPU 开销大
  3. 只能保证单个变量:多个变量需要用锁

在我的场景中,计数器更新冲突概率不高,CAS 很合适。”

🎯 融合八股:阻塞队列选择

面试官追问:为什么用 ConcurrentLinkedQueue 而不是 LinkedBlockingQueue?

回答:“两者的区别:

特性 ConcurrentLinkedQueue LinkedBlockingQueue
实现 CAS 无锁 ReentrantLock 加锁
阻塞 非阻塞 支持阻塞
容量 无界 可设置有界
性能 高并发下更好 中等

我选择 ConcurrentLinkedQueue 的原因:

  1. 生产者不阻塞:add() 方法要求极快返回,不能等待
  2. 高并发:无锁实现,性能更好
  3. 消费者轮询:后台线程用 poll() 非阻塞获取,配合 sleep 避免空转

如果需要阻塞等待,比如生产者-消费者模式,我会用 LinkedBlockingQueue。”


亮点四:微服务架构演进

问题背景

“随着业务发展,单体应用出现了几个问题:

  1. AI 生成任务占用大量资源,影响其他业务
  2. 抢课高并发场景无法独立扩展
  3. 部署时间长,发布风险高”

技术方案

“我完成了从单体到微服务的架构演进:

服务拆分(7个微服务):

  • User Service - 用户认证
  • Course Service - 课程管理
  • Snatch Service - 抢课服务(独立扩展)
  • Question Service - 题目管理
  • File Service - 文件存储
  • RAG Service - 知识库检索
  • AI-Workflow Service - AI 工作流(独立部署,可配 GPU)

技术选型

  • 服务注册与配置:Nacos
  • 服务间调用:Dubbo(Triple 协议,性能比 HTTP 高 10 倍)
  • API 网关:Higress(基于 Envoy,支持 Dubbo 协议转换)
  • 消息队列:RocketMQ
  • 分布式事务:Seata(Saga 模式)
  • Session 共享:Spring Session + Redis”

为什么选择 Dubbo 而不是 Feign?

“主要是性能考虑。Feign 基于 HTTP,每次调用都要经过 HTTP 协议栈,序列化/反序列化开销大。Dubbo 使用 Triple 协议(兼容 gRPC),基于 HTTP/2,支持多路复用,性能是 Feign 的 10 倍以上。

在抢课这种高并发场景下,服务间调用的性能差异会被放大,所以选择 Dubbo 更合适。”

🎯 融合八股:Spring 事务传播机制

面试官追问:批量写入数据库的事务怎么处理的?

回答:“我用的是 TransactionTemplate 编程式事务

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
@Resource
private TransactionTemplate transactionTemplate;

private void batchHandleSnatchEvents(List<SnatchEvent> events) {
    transactionTemplate.execute(status -> {
        try {
            // 1. 批量插入抢课记录
            snatchMapper.batchInsertSnatch(snatchList);
            // 2. 批量更新课程容量
            snatchSubjectMapper.decrementCapacityBatch(subjectId, count);
            return true;
        } catch (Exception e) {
            status.setRollbackOnly();  // 手动回滚
            throw e;
        }
    });
}

为什么用编程式事务而不是 @Transactional?

  1. 消费者方法不是 Spring 代理调用,@Transactional 可能失效
  2. 编程式事务更灵活,可以精确控制事务边界

事务传播机制(常问):

  • REQUIRED(默认):有事务就加入,没有就新建
  • REQUIRES_NEW:总是新建事务,挂起当前事务
  • NESTED:嵌套事务,可以独立回滚
  • SUPPORTS:有事务就加入,没有就非事务执行”

🎯 融合八股:分布式事务 Seata

面试官追问:跨服务的事务怎么处理?

回答:“我用的是 Seata 的 Saga 模式

Seata 的几种模式

模式 原理 优点 缺点
AT 自动补偿,基于 undo_log 无侵入 需要数据库支持
TCC Try-Confirm-Cancel 性能好 侵入性强,需要写三个方法
Saga 正向操作 + 补偿操作 长事务友好 最终一致性
XA 两阶段提交 强一致 性能差

我选择 Saga 的原因:

  1. 抢课流程是长事务(Redis → MQ → MySQL)
  2. 可以接受最终一致性
  3. 补偿逻辑简单(回滚 Redis 操作)

Saga 的实现

1
2
3
4
5
6
7
// 正向操作
redisService.decrStock();
mqService.sendMessage();

// 补偿操作(失败时调用)
redisService.incrStock();  // 回滚库存
```"

🎯 融合八股:CAP 理论

面试官追问:你的系统是 CP 还是 AP?

回答:“我的系统选择了 AP(可用性 + 分区容错性),牺牲强一致性,保证最终一致性。

CAP 理论

  • C(Consistency):所有节点数据一致
  • A(Availability):每个请求都能得到响应
  • P(Partition tolerance):网络分区时系统仍能工作

分布式系统必须保证 P,所以只能在 C 和 A 之间选择。

我的选择

  • 抢课场景对可用性要求高(用户不能等太久)
  • 可以接受最终一致性(Redis 和 MySQL 短暂不一致)
  • 通过补偿机制保证数据最终一致

如果选择 CP

  • 每次抢课都要等 MySQL 写入成功才返回
  • 性能会很差,用户体验不好”

三、常见追问及回答

Q1: Redis 和 MySQL 数据一致性怎么保证?

“我采用的是 最终一致性 方案:

  1. Redis 先行:抢课操作先在 Redis 完成,保证高性能
  2. 消息队列异步同步:成功后发送 RocketMQ 消息
  3. 批量写入 MySQL:消费者批量消费,写入数据库
  4. 失败回滚:消息发送失败时,立即回滚 Redis
  5. 补偿机制:定时任务对比 Redis 和 MySQL 数据,修复不一致

这样既保证了高性能,又保证了最终数据一致。”

Q2: 如果 RocketMQ 消息丢失怎么办?

“我做了多层保障:

  1. 生产者确认:使用同步发送,确保消息到达 Broker
  2. 消息持久化:RocketMQ 配置同步刷盘
  3. 消费者确认:处理成功后才 ACK,失败会重试
  4. 补偿任务:定时任务扫描 Redis 中的抢课记录,对比 MySQL,补偿漏掉的数据

即使极端情况下消息丢失,补偿任务也能保证数据最终一致。”

Q3: Lua 脚本为什么能保证原子性?

“Redis 是单线程模型,所有命令都是串行执行的。Lua 脚本在执行期间,不会被其他命令打断,相当于一个原子操作。

这和数据库的事务不同,数据库事务是通过锁来保证隔离性,而 Redis Lua 是通过单线程串行执行来保证原子性。

需要注意的是,Lua 脚本不能太长,否则会阻塞其他请求。我的脚本只有几行,执行时间在微秒级。”

Q4: 为什么用 SSE 而不是 WebSocket?

“SSE 和 WebSocket 的选择取决于场景:

  • SSE:单向通信(服务器 → 客户端),基于 HTTP,实现简单,自动重连
  • WebSocket:双向通信,需要额外的握手和心跳机制

AI 生成场景是典型的单向推送,用户发起请求后,服务器持续推送生成结果,不需要客户端再发消息。所以 SSE 更合适,实现也更简单。”

Q5: HeavyKeeper 算法原理是什么?

“HeavyKeeper 是一种概率数据结构,用于在有限内存下找出 Top K 热点元素。

核心思想是:

  1. 使用多层 Bucket 数组,每层用不同的 Hash 函数
  2. 每个 Bucket 存储一个 Key 的指纹和计数
  3. 新元素到来时,如果指纹匹配则计数+1,否则以一定概率衰减原计数
  4. 衰减到 0 时,用新元素替换

这样高频元素会稳定占据 Bucket,低频元素会被逐渐淘汰。

我的优化是把计数更新和清理操作异步化,主线程只做快速的 Bucket 更新,耗时操作放到后台线程。”

Q6: 微服务拆分的原则是什么?

“我遵循的原则是:

  1. 业务边界清晰:按领域划分,用户、课程、抢课各自独立
  2. 高内聚低耦合:服务内部高内聚,服务间通过接口通信
  3. 独立部署:每个服务可以独立部署和扩展
  4. 数据独立:每个服务有自己的数据库,避免跨库查询
  5. 渐进式拆分:先拆独立性高的服务(用户、文件),再拆有依赖的服务

比如抢课服务,它是高并发场景,需要独立扩展,所以单独拆出来。AI 服务是计算密集型,可能需要 GPU,也单独拆出来。”


四、项目难点与解决方案总结

难点 问题描述 解决方案 效果
高并发超卖 1000人同时抢100个名额 Redis Lua 原子操作 零超卖
数据库压力 瞬时高并发写入 RocketMQ 异步削峰 数据库 QPS 降低 90%
AI 生成超时 生成耗时 10-30 秒 SSE 流式输出 + 异步工作流 用户实时看到进度
热点 Key Redis 响应从 5ms 飙到 200ms HeavyKeeper + 二级缓存 响应稳定在 10ms
单体瓶颈 无法独立扩展 微服务拆分 抢课服务可独立扩容

五、面试加分项

1. 主动提及的技术深度

  • “我还研究了Redis 的 IO 多线程优化,在 Redis 6.0+ 可以配置 io-threads 提升性能”
  • “Lua 脚本我做了优化,把多次 Redis 操作合并,减少网络往返”
  • “消息队列我对比了 RocketMQ 和 Kafka,选择 RocketMQ 是因为它支持事务消息和延迟消息”

2. 可以展示的监控意识

  • “我在关键路径加了性能监控切面,记录每个方法的执行时间”
  • “Redis 慢查询我配置了告警,超过 50ms 就会记录日志”
  • “消息队列的消费延迟我也做了监控,防止消息堆积”

3. 可以提及的扩展思考

  • “如果并发量再大 10 倍,我会考虑 Redis Cluster 分片”
  • “如果要支持秒杀场景,可以加入令牌桶限流”
  • “未来可以考虑用 Serverless 部署 AI 服务,按需扩缩容”

六、更多八股融合场景

🎯 AOP 原理(限流切面)

项目应用:我用 AOP 实现了接口限流

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
@Aspect
@Component
public class RateLimitAspect {

    @Around("@annotation(rateLimit)")
    public Object around(ProceedingJoinPoint point, RateLimit rateLimit) {
        // 滑动窗口限流(Redis ZSet 实现)
        Long remaining = redisTemplate.execute(RATE_LIMIT_SCRIPT, ...);
        if (remaining < 0) {
            throw new BusinessException("操作过于频繁");
        }
        return point.proceed();
    }
}

AOP 原理

  • Spring AOP 基于动态代理实现
  • JDK 动态代理:目标类实现接口时使用,基于反射
  • CGLIB 代理:目标类没有接口时使用,基于字节码生成子类

@Around 的执行顺序

1
2
3
4
5
6
@Around 前置逻辑
  → @Before
    → 目标方法
  → @AfterReturning / @AfterThrowing
→ @Around 后置逻辑
→ @After

🎯 限流算法(滑动窗口)

项目应用:我实现了滑动窗口限流

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
-- Redis ZSet 实现滑动窗口
local key = KEYS[1]
local window = ARGV[1]  -- 窗口大小(秒)
local limit = ARGV[2]   -- 限制次数
local now = 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)  -- 记录本次请求
    return limit - count - 1  -- 返回剩余次数
else
    return -1  -- 限流
end

常见限流算法对比

算法 原理 优点 缺点
固定窗口 固定时间段计数 简单 临界问题(窗口边界突发)
滑动窗口 滑动时间段计数 平滑 内存占用大
漏桶 固定速率流出 平滑流量 无法应对突发
令牌桶 固定速率生成令牌 允许突发 实现复杂

我选择滑动窗口是因为它能平滑限流,避免固定窗口的临界问题。

🎯 MySQL 索引优化

项目应用:抢课记录查询优化

1
2
3
4
5
-- 查询某学生是否抢过某课程
SELECT * FROM snatch WHERE student_id = ? AND subject_id = ?;

-- 我建了联合索引
CREATE INDEX idx_student_subject ON snatch(student_id, subject_id);

索引原理(B+ 树):

  • 非叶子节点只存索引,叶子节点存数据
  • 叶子节点用链表连接,支持范围查询
  • 树高一般 3-4 层,查询复杂度 O(log n)

联合索引的最左前缀原则

  • (student_id, subject_id) 索引可以支持:
    • WHERE student_id = ?
    • WHERE student_id = ? AND subject_id = ?
    • WHERE subject_id = ? ❌(不走索引)

索引失效场景

  • 对索引列使用函数:WHERE YEAR(create_time) = 2024
  • 隐式类型转换:WHERE student_id = '123'(student_id 是 int)
  • LIKE 左模糊:WHERE name LIKE '%张'
  • OR 条件:WHERE student_id = 1 OR name = '张三'

🎯 Redis 序列化问题

项目应用:我配置了两个 RedisTemplate

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 1. 通用 RedisTemplate(Jackson 序列化)
@Bean
public RedisTemplate<String, Object> redisTemplate() {
    template.setValueSerializer(new Jackson2JsonRedisSerializer<>(...));
    return template;
}

// 2. Lua 专用 RedisTemplate(String 序列化)
@Bean
public RedisTemplate<String, String> luaRedisTemplate() {
    template.setValueSerializer(new StringRedisSerializer());
    return template;
}

为什么要两个?

  • Jackson 序列化会在值前面加类型信息,Lua 脚本处理不了
  • Lua 脚本需要纯字符串,所以用 StringRedisSerializer

常见序列化方式

  • JdkSerializationRedisSerializer:Java 原生序列化,可读性差
  • StringRedisSerializer:字符串,简单但只能存字符串
  • Jackson2JsonRedisSerializer:JSON,可读性好,需要配置类型信息
  • GenericJackson2JsonRedisSerializer:JSON + 类型信息,通用性好

🎯 Spring Bean 生命周期

项目应用:消费者启动和关闭

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
@Component
public class SnatchEventConsumer {

    @PostConstruct  // Bean 初始化后执行
    public void start() {
        // 启动消费线程
        new Thread(this::consumeLoop).start();
    }

    @PreDestroy  // Bean 销毁前执行
    public void shutdown() {
        // 优雅关闭:处理完剩余消息
        flushBatch();
        scheduler.shutdown();
    }
}

Bean 生命周期

  1. 实例化(new)
  2. 属性注入(@Autowired)
  3. Aware 接口回调(BeanNameAware、ApplicationContextAware)
  4. BeanPostProcessor.postProcessBeforeInitialization
  5. @PostConstruct
  6. InitializingBean.afterPropertiesSet
  7. init-method
  8. BeanPostProcessor.postProcessAfterInitialization
  9. 使用中…
  10. @PreDestroy
  11. DisposableBean.destroy
  12. destroy-method

🎯 JVM 内存模型与可见性

项目应用:后台线程的停止标志

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
private final AtomicBoolean running = new AtomicBoolean(true);

// 后台线程
private void processQueue() {
    while (running.get()) {  // 需要保证可见性
        // 处理逻辑
    }
}

// 关闭方法
public void shutdown() {
    running.set(false);  // 其他线程能立即看到
}

为什么用 AtomicBoolean 而不是普通 boolean?

  • 普通 boolean 没有可见性保证,其他线程可能看不到修改
  • AtomicBoolean 底层用 volatile,保证可见性

volatile 的作用

  1. 可见性:一个线程修改后,其他线程立即可见
  2. 禁止指令重排序:防止编译器和 CPU 优化导致的乱序

volatile 不能保证原子性

  • count++ 不是原子操作(读-改-写)
  • 需要原子性用 AtomicInteger 或 synchronized

七、一句话总结

“这个项目让我深入理解了 高并发系统设计(Redis + Lua + MQ)、AI 应用开发(LangGraph4j + RAG + SSE)、以及 微服务架构演进(Spring Cloud Alibaba + Dubbo)。最大的收获是学会了如何在 性能、一致性、可用性 之间做权衡。”


八、面试话术模板

开场白

“我做的是一个智能教育管理系统,主要有三个技术亮点:高并发抢课、AI 工作流、微服务架构。您想先听哪个?”

引出八股的过渡句

  • “说到这个,其实涉及到 Redis 的单线程模型…”
  • “这里我用了 ConcurrentHashMap,它的原理是…”
  • “为了保证线程安全,我用了 AtomicInteger,它底层是 CAS…”
  • “事务这块我用的是编程式事务,因为 @Transactional 有个坑…”

展示深度的句式

  • “我还对比了几种方案…”
  • “这里有个细节需要注意…”
  • “我踩过一个坑是…”
  • “如果并发量再大 10 倍,我会考虑…”

结束语

“这个项目让我对高并发和分布式有了更深的理解,也让我养成了从性能、一致性、可用性多角度思考问题的习惯。”


九、口语化面试话术(完整版)

公式:业务背景 → 技术选型原因 → 核心难点实现(手撕逻辑) → 遇到的坑与解决 → 未来优化方向


【话术一】高并发抢课系统

1️⃣ 业务背景(30秒)

“我们这个系统有一个抢课功能,就是学生选课的时候,热门课程可能几百上千人同时抢。

核心问题有两个

  • 第一是超卖,比如课程只有100个名额,结果抢了120个人,这肯定不行
  • 第二是数据库扛不住,如果每个请求都直接打到MySQL,瞬时1000个并发,数据库直接就挂了

所以我需要设计一个既能防超卖、又能扛住高并发的方案。”

2️⃣ 技术选型原因(1分钟)

“我调研了几种方案:

方案一:纯MySQL + 悲观锁

  • SELECT FOR UPDATE 锁住库存记录
  • 问题是性能太差,测下来只有500 QPS,而且锁竞争严重

方案二:MySQL + 乐观锁

  • 用版本号控制,UPDATE ... WHERE version = ?
  • 问题是高并发下大量重试,成功率低

方案三:Redis + Lua + 消息队列(我最终选的)

  • Redis 单线程,天然防并发问题
  • Lua 脚本保证原子性,不会超卖
  • 消息队列异步写数据库,削峰填谷

选 Redis 的核心原因是:它的单线程模型天然保证了操作的原子性,不需要加锁就能防止并发问题。”

3️⃣ 核心难点实现(2分钟,可手撕)

“核心就是这个 Lua 脚本,我给您讲一下逻辑:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
-- 第一步:检查这个学生是不是已经抢过了
local hasSnatch = redis.call('HEXISTS', studentKey, subjectId)
if hasSnatch == 1 then
    return -1  -- 已经抢过,直接返回
end

-- 第二步:扣减库存(原子操作)
local newCapacity = redis.call('HINCRBY', capacityKey, subjectId, -1)
if newCapacity < 0 then
    -- 库存不够,要回滚
    redis.call('HINCRBY', capacityKey, subjectId, 1)
    return -2  -- 库存不足
end

-- 第三步:记录抢课状态
redis.call('HSET', studentKey, subjectId, 1)
return 1  -- 成功

为什么能防超卖?

关键在于 Redis 是单线程执行命令的。这个 Lua 脚本在执行期间,不会有其他命令插进来。所以「检查库存 → 扣减库存 → 记录状态」这三步是一个原子操作,不可能出现两个人同时扣减最后一个库存的情况。

数据怎么落库?

Redis 操作成功后,我不是直接写 MySQL,而是发一条消息到 RocketMQ。消费者那边批量消费,比如攒够1000条或者等10秒,然后批量 INSERT。这样数据库的压力就从瞬时1000 QPS 变成了平稳的每秒几十次批量写入。”

4️⃣ 遇到的坑与解决(1分钟)

“我踩过几个坑:

坑一:Redis 序列化问题

一开始我用 Jackson 序列化,结果 Lua 脚本执行报错。因为 Jackson 会在值前面加类型信息,Lua 处理不了。

解决:我配了两个 RedisTemplate,一个用 Jackson 给业务用,一个用纯 String 序列化专门给 Lua 脚本用。

坑二:消息发送失败数据不一致

Redis 扣减成功了,但是消息发送失败了,这时候 Redis 和 MySQL 数据就不一致了。

解决:我加了回滚机制。消息发送失败时,立即执行一个回滚的 Lua 脚本,把库存加回去,把抢课状态删掉。

坑三:热点 Key 导致 Redis 响应变慢

压测的时候发现,热门课程的 Key 被大量访问,Redis 响应时间从5ms飙到200ms。

解决:我实现了一个热点检测 + 本地缓存的方案。用 HeavyKeeper 算法检测热点 Key,检测到之后把数据缓存到本地 Caffeine,减少 Redis 访问。”

5️⃣ 未来优化方向(30秒)

“如果并发量再大10倍,我会考虑:

  1. Redis Cluster 分片:把不同课程的库存分散到不同节点,避免单点瓶颈
  2. 令牌桶预热:抢课开始前,先发放令牌,没有令牌的请求直接拒绝,减少无效请求
  3. 本地预扣减:在应用层先做一次本地库存预扣减,过滤掉大部分请求,只有预扣减成功的才去访问 Redis”

【话术二】AI 智能题目生成

1️⃣ 业务背景(30秒)

“我们系统有一个 AI 出题功能,老师输入知识点和题目数量,系统自动生成选择题、填空题、大题。

核心问题

  • AI 生成很慢,一道题可能要5-10秒,生成10道题就要1-2分钟
  • 如果用传统的同步请求,用户要等很久,体验很差,而且容易超时
  • 生成的题目质量参差不齐,需要有质检和重试机制”

2️⃣ 技术选型原因(1分钟)

“我选了 LangGraph4j + SSE 流式输出 的方案:

为什么用 LangGraph4j?

  • 它是一个工作流引擎,可以把复杂的 AI 任务拆成多个节点
  • 支持条件路由,比如质检不通过可以自动重试
  • 节点之间可以传递上下文,方便管理状态

为什么用 SSE 而不是 WebSocket?

  • SSE 是单向通信,服务器推送给客户端,正好符合我的场景
  • 基于 HTTP,实现简单,不需要额外的握手和心跳
  • 自动重连,断了会自己连回来

为什么不用轮询?

  • 轮询会产生大量无效请求
  • 实时性差,用户体验不好”

3️⃣ 核心难点实现(2分钟)

“我设计了一个四节点的工作流:

1
2
3
开始 → RAG知识检索 → 任务拆分 → 题目生成 → 质量检查 → 结束
                                    ↑         ↓
                                    ←← 重试 ←←←

节点一:RAG 知识检索

  • 根据老师输入的知识点,从向量数据库检索相关的教学文档
  • 这样生成的题目更贴合教材内容

节点二:任务拆分

  • 把「生成10道选择题」拆成10个独立的任务
  • 每个任务生成一道题

节点三:题目生成

  • 调用大模型,传入知识点和检索到的文档
  • 生成题目、选项、答案、解析

节点四:质量检查

  • 检查 JSON 格式是否正确
  • 检查选项数量是否符合要求
  • 检查答案是否在选项中

条件路由的实现

1
2
3
4
5
6
7
.addConditionalEdges("质检节点",
    edge_async(this::routeAfterCheck),
    Map.of(
        "continue", "生成节点",  // 质检通过,继续下一题
        "retry", "生成节点",     // 质检失败,重新生成
        "finish", END            // 全部完成
    ))

SSE 流式输出

每生成一道题,就立即推送给前端,用户可以实时看到进度。我用 ThreadLocal 存储 SseEmitter,避免序列化问题。”

4️⃣ 遇到的坑与解决(1分钟)

坑一:ThreadLocal 内存泄漏

一开始我忘了清理 ThreadLocal,结果在线程池场景下,线程被复用,ThreadLocal 里的对象一直不释放,内存越来越大。

解决:在 finally 块里一定要调用 remove()

坑二:异步线程拿不到 ThreadLocal

工作流是异步执行的,但是异步线程和主线程不是同一个,拿不到主线程的 ThreadLocal。

解决:在异步任务开始时,重新 set 一次 SseEmitter。

坑三:大模型返回格式不稳定

有时候大模型返回的 JSON 格式不对,解析失败。

解决:我在 prompt 里加了严格的格式要求,并且在质检节点做了格式校验,不通过就重试,最多重试3次。”

5️⃣ 未来优化方向(30秒)

“1. 并行生成:现在是串行生成10道题,可以改成并行,开10个线程同时生成 2. 缓存相似题目:如果知识点相似,可以从缓存里取,不用每次都调大模型 3. 模型微调:用我们自己的题库数据微调模型,提高生成质量”


【话术三】微服务架构演进

1️⃣ 业务背景(30秒)

“项目一开始是单体架构,后来遇到了几个问题:

  1. 资源竞争:AI 生成任务很吃 CPU 和内存,一跑起来其他接口都变慢了
  2. 无法独立扩展:抢课高峰期,只有抢课模块需要扩容,但单体架构只能整体扩容,浪费资源
  3. 发布风险高:改一行代码要重新部署整个应用,万一出问题影响所有功能

所以我决定做微服务拆分。”

2️⃣ 技术选型原因(1分钟)

“我选的是 Spring Cloud Alibaba + Dubbo + Nacos + Higress 这套:

为什么选 Dubbo 而不是 Feign?

  • Feign 基于 HTTP,每次调用都要经过完整的 HTTP 协议栈,开销大
  • Dubbo 用的是 Triple 协议,基于 HTTP/2,支持多路复用,性能是 Feign 的10倍
  • 在抢课这种高并发场景,服务间调用的性能差异会被放大

为什么选 Nacos?

  • 同时支持服务注册和配置中心,不用部署两套
  • 和 Spring Cloud Alibaba 生态集成好
  • 性能比 Eureka 和 Consul 都好

为什么选 Higress 网关?

  • 基于 Envoy,性能很高
  • 原生支持 Dubbo 协议转换,外部 HTTP 请求可以直接转成 Dubbo 调用
  • 和 Nacos 无缝集成”

3️⃣ 核心难点实现(1分钟)

“我拆成了7个服务:

服务 职责 为什么单独拆
User 用户认证 基础服务,被所有服务依赖
Course 课程管理 业务独立
Snatch 抢课 高并发,需要独立扩展
Question 题目管理 业务独立
File 文件存储 IO 密集,独立部署
RAG 知识库检索 向量计算,资源隔离
AI-Workflow AI 工作流 CPU 密集,可能要 GPU

服务间调用:用 Dubbo RPC,定义了统一的接口模块 EduAgentX-Client,所有服务都依赖它。

Session 共享:用 Spring Session + Redis,所有服务共享同一个 Session 存储。”

4️⃣ 遇到的坑与解决(1分钟)

坑一:循环依赖

Course 服务要调用 User 服务获取教师信息,User 服务又要调用 Course 服务获取用户的课程列表,形成了循环依赖。

解决:重新梳理服务边界,把「用户的课程列表」这个功能放到 Course 服务,User 服务只负责用户基本信息。

坑二:分布式事务

抢课成功后要同时更新 Redis、发消息、写数据库,跨了多个服务。

解决:用 Seata 的 Saga 模式,定义正向操作和补偿操作。失败时自动执行补偿,保证最终一致性。

坑三:服务调用超时

AI 服务生成题目很慢,默认的 Dubbo 超时时间是3秒,经常超时。

解决:针对 AI 服务单独配置超时时间为60秒,其他服务保持3秒。”

5️⃣ 未来优化方向(30秒)

“1. 服务网格:引入 Istio,把服务治理下沉到基础设施层 2. 容器化:用 Kubernetes 部署,实现自动扩缩容 3. Serverless:AI 服务可以用 Serverless 部署,按调用量付费,降低成本”


【话术四】热点缓存优化

1️⃣ 业务背景(30秒)

“压测的时候发现一个问题:热门课程被大量访问,Redis 响应时间从正常的5ms飙升到200ms,严重影响了抢课接口的性能。

分析原因是:所有请求都打到 Redis 的同一个 Key 上,形成了热点 Key,Redis 单线程处理不过来。”

2️⃣ 技术选型原因(30秒)

“我的方案是 热点检测 + 本地缓存

  1. HeavyKeeper 算法 实时检测哪些 Key 是热点
  2. 检测到热点后,把数据缓存到本地 Caffeine
  3. 后续请求先查本地缓存,命中就不用访问 Redis 了

为什么用 HeavyKeeper?因为它是概率数据结构,内存占用小,可以在有限内存下找出 Top K 热点元素。”

3️⃣ 核心难点实现(1分钟)

“HeavyKeeper 的原理是:

  1. 维护一个多层的 Bucket 数组
  2. 每个 Bucket 存一个 Key 的指纹和计数
  3. 新请求来了,如果指纹匹配就计数+1
  4. 如果不匹配,以一定概率衰减原来的计数
  5. 衰减到0就用新 Key 替换

我做的优化

原始实现的 add() 方法要5ms,太慢了。我改成了异步版本:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public AddResult add(String key, int increment) {
    // 1. 快速更新 Bucket(分段锁,<1ms)
    int maxCount = updateBuckets(key, increment);

    // 2. 如果是热点,放入队列(不阻塞)
    if (maxCount >= threshold) {
        updateQueue.offer(new UpdateTask(key, maxCount));
    }

    return result;
}

耗时操作(清理过期数据、统计 Top K)都放到后台线程异步处理,主线程只做快速的计数更新。优化后 add() 耗时从5ms降到了0.1ms。”

4️⃣ 遇到的坑与解决(30秒)

坑:本地缓存数据不一致

本地缓存和 Redis 数据可能不一致,比如 A 服务器的本地缓存显示还有库存,但 Redis 里已经没了。

解决:我的策略是写操作不走本地缓存。抢课、退课这种写操作,直接走 Redis Lua 脚本。本地缓存只用于读操作(查询库存、查询状态),而且设置了60秒过期时间,保证最终一致性。”

5️⃣ 未来优化方向(30秒)

“1. 多级缓存:可以再加一层进程内缓存,形成 L1(进程内)→ L2(本地 Caffeine)→ L3(Redis)的三级缓存 2. 缓存预热:抢课开始前,提前把热门课程的数据加载到本地缓存 3. Redis Cluster:如果热点 Key 太多,可以用 Redis Cluster 分散到多个节点”


十、万能应对话术

当面试官问「还有什么要补充的吗」

“我想补充一点,这个项目让我最大的收获不是学会了某个技术,而是学会了怎么做技术选型

比如抢课系统,我一开始想用分布式锁,后来发现 Lua 脚本更简单高效;消息队列我对比了 Kafka 和 RocketMQ,最后选了 RocketMQ 因为它支持事务消息。

我觉得做技术选型最重要的是理解每个方案的优缺点和适用场景,而不是盲目追求新技术。”

当面试官问「这个项目有什么不足」

“有几个地方我觉得可以做得更好:

  1. 监控不够完善:目前只有基础的日志,缺少完整的链路追踪和性能监控大盘
  2. 测试覆盖率不够:单元测试写得比较少,主要靠手工测试
  3. 文档不够完善:接口文档有,但是架构设计文档和运维文档比较欠缺

如果有机会重新做,我会在项目初期就把这些基础设施搭建好。”

当面试官问「你在团队中的角色」

“我是这个项目的主要开发者,负责核心模块的设计和实现:

  • 抢课系统的高并发方案是我设计的
  • AI 工作流引擎是我从零搭建的
  • 微服务拆分的架构设计也是我主导的

遇到技术难题时,我会先自己调研,然后和团队讨论,最后形成方案文档,评审通过后再实施。”

当面试官深挖某个技术细节你不太确定时

“这个细节我不太确定,但是我的理解是…(说你的理解)。回去之后我会再深入研究一下,确认一下是不是这样。”

千万不要

  • 瞎编一个答案
  • 说「我不知道」然后沉默

十一、面试前 Checklist

必须能脱口而出的

  • 项目30秒介绍
  • 三个核心亮点的业务背景
  • Lua 脚本的核心逻辑(能手写)
  • 为什么选 Redis 不选 MySQL
  • 为什么选 RocketMQ 不选 Kafka
  • 为什么选 Dubbo 不选 Feign
  • ThreadLocal 内存泄漏怎么解决
  • 分布式事务用的什么方案

最好能说清楚的

  • Redis 单线程模型
  • ConcurrentHashMap 原理
  • 线程池参数怎么配置
  • AOP 的实现原理
  • Spring 事务传播机制
  • CAP 理论,你的系统是 CP 还是 AP

加分项

  • 性能优化的具体数据(QPS 从多少到多少)
  • 踩过的坑和解决方案
  • 未来的优化方向
  • 对比过哪些技术方案
Licensed under CC BY-NC-SA 4.0