当"牢大"遇上秒杀:Java面试官连环追问Redis、Kafka与微服务架构,水货程序员高并发场景翻车实录
一、前情提要
杭州某互联网大厂,下午两点,6号会议室。
面试官老陈——工号0087,司龄9年,双11核心架构师,眼镜片厚如瓶底,表情比GitLab的CI流水线还冷。今天他要面一个号称"5年电商经验"的Java开发——候选人花名"牢大",简历上写着"精通高并发、熟悉分布式架构"。
牢大推门进来,穿着皱巴巴的格子衫,背上印着"代码改变世界",手里捏着一杯瑞幸,脸上挂着自信又有点油滑的微笑。
"坐。"老陈指了指对面的椅子,屏幕上的八股题库已经打开。
二、第一轮:Java基础 + 数据库 + 缓存(热身试探)
场景切入:某电商平台"双11"秒杀活动,SKU限量1000件,瞬时并发10万+
面试官(老陈):「先聊点基础的。咱们以双11秒杀为背景——1000件商品,10万人同时抢。你负责订单模块,第一步你打算怎么写?先说说你用Java怎么保证内存里的数据不出问题。」
牢大:(放下咖啡,搓搓手)「这个我熟!Java里面肯定用HashMap缓存商品库存嘛,但HashMap线程不安全,多线程put容易死循环。所以得用ConcurrentHashMap,分段锁,JDK8之后用CAS+synchronized,性能比HashTable好多了。」
老陈:(微微点头)「不错,那你说说ConcurrentHashMap在JDK8里具体怎么实现线程安全的?size()方法怎么统计的?」
牢大:(眼神飘了一下)「呃……JDK8去掉了分段锁,改成了……那个……节点上加synchronized,配合CAS。size的话,它有个baseCount,然后每个线程有CounterCell,最后求和……具体源码记不太清了,但大概就是这么个意思!」
老陈:(不置可否)「行。回到秒杀场景,库存1000件你怎么存?数据库还是缓存?」
牢大:(来了精神)「肯定先放Redis!数据库扛不住10万并发。用Redis的String类型存库存,decr原子扣减。然后做缓存预热,提前把商品信息、库存加载到Redis,设置合理的过期策略——不过秒杀期间不能设过期,得持久化。」
老陈:「Redis单线程,decr操作串行化,10万QPS扛得住吗?如果库存只剩1件,1000个请求同时decr,会发生什么?」
牢大:(挠头)「这个……Redis单线程其实不怕,它是顺序执行的,不会超卖。但你说的10万QPS,单机Redis确实可能打满CPU,得用集群或者哨兵模式。配合Lua脚本把"判断库存+扣减"做成原子操作,减少网络开销。」
老陈:(嘴角微动,表示认可)「Lua脚本这个思路可以。那接下来,扣减成功之后,订单落库怎么处理?」
三、第二轮:消息队列 + 微服务 + 分布式事务(逐步加压)
场景延伸:秒杀成功后需要异步创建订单、扣减账户余额、发送通知
老陈:「Redis扣减库存成功后,你要创建订单、扣余额、发短信。这三步是一个事务吗?你打算怎么设计?」
牢大:(开始有点紧张)「这个……肯定不能同步做,不然接口响应太慢。用消息队列异步解耦!比如Redis扣减成功后,发一条消息到Kafka,订单服务消费消息创建订单,然后通知账户服务扣款,最后发短信。」
老陈:「你用Kafka,那消息丢失怎么办?Producer发出去,Broker挂了,你订单就丢了?」
牢大:(语速变快)「Kafka可以配置acks=all,所有ISR副本都确认了才算成功。Producer这边开启重试机制retries,配合幂等性enable.idempotence=true。消费端手动提交offset,处理完业务再commit。」
老陈:「好,如果订单服务消费成功、创建了订单,但账户服务扣款失败——订单已经创建了,钱没扣成,你怎么处理?」
牢大:(额头冒汗)「这个……这个是分布式事务问题。可以用……呃……Seata的AT模式?或者用本地消息表?订单服务创建订单的时候,同时写一条"待扣款"记录,账户服务轮询处理,保证最终一致性……」
老陈:(身体前倾)「你说到最终一致性了——如果用户秒杀成功后马上查看订单,订单是"待支付"状态,但库存已经扣了。3分钟后订单超时取消,库存怎么回补?Redis加回去?数据库也加回去?这两个操作的一致性怎么保证?」
牢大:(咖啡已经凉了,手有点抖)「这个……可以用RocketMQ的事务消息。先发半消息,执行本地事务(比如标记订单超时),然后根据本地事务结果决定commit还是rollback。库存回补的话……Redis和DB都加回去,用定时任务对账……」
老陈:「定时任务对账——间隔多久?对账期间库存不一致,用户看到Redis有库存但DB没库存,下单又失败,你不怕被投诉?」
牢大:(彻底慌了)「这……这个确实……要综合考虑……可能用Redis做唯一库存源,DB做备份……或者……呃……」
四、第三轮:架构设计 + 云原生 + 监控(终极考验)
场景升级:大促期间全链路压测、灰度发布、故障自愈
老陈:(面无表情)「前面聊的都是单点。现在假设你负责整个秒杀系统——画一下你的架构图,从网关到数据库,每一层都说清楚。」
牢大:(开始在白板上画,手有点抖)「首先是CDN……把静态资源缓存到边缘节点。然后Nginx做反向代理和限流,用漏桶算法……网关层用Spring Cloud Gateway,配合Sentinel做熔断降级……」
老陈:「为什么选Spring Cloud Gateway而不是Zuul?」
牢大:「Gateway是响应式编程,基于WebFlux和Netty,非阻塞IO,性能比Zuul 1.x好。Zuul 1.x是同步阻塞的,虽然Zuul 2也用了Netty但社区没Gateway活跃。」
老陈:(点头)「继续。」
牢大:「网关后面是业务服务——商品服务、订单服务、库存服务、用户服务。用Nacos做注册中心和配置中心,OpenFeign做服务调用,Resilience4j做熔断……」
老陈:「Resilience4j的熔断器有三种状态,哪三种?半开状态怎么工作的?」
牢大:(咽了口唾沫)「关闭、打开、半开……半开状态就是放一部分请求试探,如果成功率达标就关闭熔断器恢复正常,不达标就继续打开……具体阈值配置我一般用默认的……」
老陈:「默认的Ring Buffer大小是多少?滑动窗口类型有哪两种?」
牢大:(沉默三秒)「……这个……记不太清了,平时都是用Spring Boot Starter自动配置的……」
老陈:(推了下眼镜)「最后几个问题。你的服务部署在K8s上,Pod怎么保证健康检查?你用什么做CI/CD?」
牢大:「K8s用livenessProbe和readinessProbe,HTTP GET检查/actuator/health。CI/CD用Jenkins Pipeline,代码推到GitLab自动触发构建,Docker镜像打tag推到Harbor,然后kubectl rollout滚动更新。」
老陈:「蓝绿部署和金丝雀发布有什么区别?灰度发布时,如果新版本有问题,怎么做到用户无感知回滚?」
牢大:(已经快撑不住了)「蓝绿部署是两套完整环境切换……金丝雀是小比例流量切到新版本……回滚的话……用Istio做流量管理,配合Prometheus监控指标,发现错误率上升就自动回滚……」
老陈:(合上电脑,表情依然严肃)「好,今天的面试就到这里。感谢你的时间,回去等通知吧。」
牢大:(愣住,经典台词来了)「好的好的……那个……大概多久有消息?」
老陈:「HR会在一周内联系你。」(起身,没有握手)
五、尾声
牢大走出会议室,瑞幸杯子已经空空如也。他掏出手机,打开Boss直聘,默默把简历里的"精通高并发"改成了"熟悉高并发",然后又改成了"了解高并发"。
电梯里,他收到了HR的消息:「您好,面试官反馈技术广度不错,但深度还需加强,本次暂不匹配。简历已入库,后续有机会再联系。」
牢大叹了口气,在"码农互助群"里发了一条:「兄弟们,今天被问麻了。面试官让我画秒杀架构图,我画到一半他说'回去等通知'……有没有面经分享一波?」
群里秒回:「常规操作,坐下坐下。」「你就说你要多少钱吧。」「把题发出来让大家学习学习。」
六、技术解析与答案详解(小白学习专区)
以下是面试中涉及的核心技术点详解,按面试顺序整理。
6.1 ConcurrentHashMap 线程安全机制(JDK8)
业务场景:秒杀场景中,商品库存数据在JVM内存中缓存时,多个线程同时读写需要保证线程安全。
JDK8实现原理:
JDK8废弃了JDK7的Segment分段锁设计,改用了更细粒度的Node节点锁 + CAS。
// JDK8 putVal 核心逻辑简化
final V putVal(K key, V value, boolean onlyIfAbsent) {
// 1. 计算hash值
int hash = spread(key.hashCode());
// 2. 无限循环 + CAS
for (Node<K,V>[] tab = table;;) {
// 如果数组为空,CAS初始化
if (tab == null) {
// CAS 设置 table
}
// 如果桶位置为空,CAS 直接放入
else if ((f = tabAt(tab, i = (n-1) & hash)) == null) {
if (casTabAt(tab, i, null, new Node<>(hash, key, value)))
break;
}
// 如果正在扩容,帮助扩容
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
// 桶位置有数据,对头节点加synchronized锁
synchronized (f) {
// 链表或红黑树插入
}
}
}
}
- CAS:无锁操作,用于初始化数组、在空桶位置插入节点
- synchronized:锁住桶的头节点,仅锁冲突桶,不影响其他桶的并发读写
- 数据访问:transient volatile Node<K,V>[] table,volatile保证可见性
size() 计数机制:
- 使用baseCount + CounterCell[]数组
- 无竞争时CAS更新baseCount
- 有竞争时每个线程分配到不同的CounterCell
- size()求和:baseCount + 所有CounterCell.value
6.2 Redis 秒杀库存扣减
为什么用Redis:Redis单线程模型,所有命令顺序执行,天然避免并发超卖。
三种扣减方案对比:
| 方案 | 实现 | 优点 | 缺点 |
|——|——|——|——|
| decr 原子操作 | decr stock:sku001 | 简单高效 | 无法做复杂判断 |
| Lua脚本 | 判断+扣减原子化 | 灵活安全 | 脚本复杂度 |
| 分布式锁 | SETNX + 业务逻辑 | 通用性强 | 性能损耗 |
Lua脚本示例(推荐):
— 原子判断库存并扣减
local key = KEYS[1] — 库存key
local requestCount = tonumber(ARGV[1]) — 请求购买数量
local stock = tonumber(redis.call('get', key))
if stock == nil then
return -1 — key不存在
end
if stock < requestCount then
return 0 — 库存不足
end
redis.call('decrby', key, requestCount)
return 1 — 扣减成功
Java调用:
String script = "…"; // 上面的Lua脚本
Long result = redisTemplate.execute(
new DefaultRedisScript<>(script, Long.class),
Collections.singletonList("stock:sku001"),
"1"
);
if (result == 1) {
// 扣减成功,发消息创建订单
}
缓存预热:大促前将商品信息、库存从DB加载到Redis,避免冷启动缓存击穿。
6.3 Kafka 消息可靠性保障
Producer端:
# 所有ISR副本确认
acks=all # 或 acks=-1
# 开启幂等性(防止重复)
enable.idempotence=true
# 重试次数
retries=3
# 每个连接最多未确认请求数
max.in.flight.requests.per.connection=5
Broker端:
- min.insync.replicas=2:最少同步副本数
- unclean.leader.election.enable=false:不允许落后副本竞选Leader
Consumer端:
// 手动提交offset(处理完再提交)
props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);
while (true) {
ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100));
for (ConsumerRecord<String, String> record : records) {
processOrder(record.value()); // 处理订单
}
consumer.commitSync(); // 处理完再提交
}
6.4 分布式事务:最终一致性方案
问题:订单创建成功 + 库存扣减成功 + 账户扣款失败 → 需要回滚
方案一:本地消息表(推荐用于订单场景)
┌─────────────┐ ┌──────────────┐ ┌─────────────┐
│ 订单服务 │───▶│ 本地消息表 │───▶│ 消息队列 │
│ 创建订单 │ │ order_msg │ │ (Kafka) │
└─────────────┘ └──────────────┘ └──────┬──────┘
│
┌─────────────┐ │
│ 账户服务 │◀───────────┘
│ 扣款+回调 │
└─────────────┘
流程:
方案二:RocketMQ事务消息
// 发送事务消息
transactionMQProducer.sendMessageInTransaction(msg, new TransactionListener() {
@Override
public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
try {
// 执行本地事务:创建订单
orderService.createOrder();
return LocalTransactionState.COMMIT_MESSAGE;
} catch (Exception e) {
return LocalTransactionState.ROLLBACK_MESSAGE;
}
}
@Override
public LocalTransactionState checkLocalTransaction(MessageExt msg) {
// 回查本地事务状态
return orderService.checkOrderStatus(msg.getTransactionId());
}
});
6.5 Spring Cloud Gateway vs Zuul
| 对比维度 | Spring Cloud Gateway | Zuul 1.x | Zuul 2.x |
|———-|———————|———-|———-|
| IO模型 | 非阻塞(Netty) | 阻塞(Servlet) | 非阻塞(Netty) |
| 编程模型 | 响应式(WebFlux) | 命令式 | 响应式 |
| 性能 | 高(Reactor) | 低 | 高 |
| 社区活跃度 | 高(Spring官方) | 停更 | 低 |
| 限流 | 内置RequestRateLimiter | 需扩展 | 内置 |
Gateway路由配置示例:
spring:
cloud:
gateway:
routes:
– id: order-service
uri: lb://order-service
predicates:
– Path=/api/orders/**
filters:
– name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 100
redis-rate-limiter.burstCapacity: 200
– name: CircuitBreaker
args:
name: orderCircuitBreaker
fallbackUri: forward:/fallback
6.6 Resilience4j 熔断器
三种状态:
┌──────────┐
│ CLOSED │ ← 正常状态,请求全部放行
└────┬─────┘
│ 失败率 ≥ 阈值
▼
┌──────────┐
│ OPEN │ ← 熔断打开,拒绝所有请求
└────┬─────┘
│ 等待时间到
▼
┌──────────┐
│ HALF_OPEN│ ← 半开状态,放部分请求试探
└────┬─────┘
│
成功率高 失败率高
│ │
▼ ▼
CLOSED OPEN
配置示例:
resilience4j:
circuitbreaker:
instances:
orderService:
sliding-window-type: COUNT_BASED # 基于计数
sliding-window-size: 10 # 窗口大小10次
failure-rate-threshold: 50 # 失败率50%触发熔断
wait-duration-in-open-state: 10s # OPEN状态等待10s
permitted-number-of-calls-in-half-open-state: 3 # 半开状态放行3个请求
6.7 Kubernetes 健康检查与CI/CD
健康检查:
apiVersion: v1
kind: Pod
spec:
containers:
– name: order-service
image: order-service:1.0.0
livenessProbe: # 存活探针:失败则重启Pod
httpGet:
path: /actuator/health/liveness
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe: # 就绪探针:失败则从Service移除
httpGet:
path: /actuator/health/readiness
port: 8080
initialDelaySeconds: 10
periodSeconds: 5
CI/CD流水线(Jenkins + K8s):
Git Push → GitLab Webhook → Jenkins Pipeline
├── mvn clean package
├── docker build -t order-service:v1.0.1 .
├── docker push harbor.example.com/order-service:v1.0.1
├── kubectl set image deployment/order-service order-service=harbor.example.com/order-service:v1.0.1
└── kubectl rollout status deployment/order-service
蓝绿部署 vs 金丝雀发布:
| 策略 | 做法 | 回滚速度 | 风险 |
|——|——|———-|——|
| 蓝绿 | 两套完整环境,流量一次性切换 | 秒级 | 资源翻倍 |
| 金丝雀 | 逐步增加新版本流量比例(5%→20%→100%) | 分钟级 | 发现问题早 |
6.8 秒杀系统整体架构图
┌──────────────┐
│ 用户请求 │
└──────┬───────┘
│
┌──────▼───────┐
│ CDN/缓存 │ ← 静态资源
└──────┬───────┘
│
┌──────▼───────┐
│ Nginx (限流) │ ← 漏桶/令牌桶
└──────┬───────┘
│
┌──────▼───────┐
│ Gateway + │ ← Sentinel熔断
│ Sentinel │
└──────┬───────┘
│
┌───────────────┼───────────────┐
│ │ │
┌──────▼──────┐ ┌─────▼─────┐ ┌──────▼──────┐
│ 秒杀服务 │ │ 订单服务 │ │ 商品服务 │
│ Redis扣库存 │ │ Kafka消费 │ │ 缓存预热 │
└──────┬──────┘ └─────┬─────┘ └─────────────┘
│ │
┌──────▼──────┐ ┌─────▼─────┐
│ Redis │ │ MySQL │
│ 库存 │ │ 订单持久化 │
└─────────────┘ └───────────┘
│
┌──────▼──────┐
│ Elasticsearch│ ← 订单检索
└─────────────┘
监控层:Prometheus + Grafana + ELK + Jaeger(链路追踪)
6.9 扩展思考:面试官到底在考察什么?
| 轮次 | 考察维度 | 核心考点 |
|——|———-|———-|
| 第一轮 | 基础扎实度 | JUC、集合源码、Redis原子操作 |
| 第二轮 | 架构设计力 | 异步解耦、消息可靠性、分布式事务 |
| 第三轮 | 全局视野 | 全链路架构、容器化、可观测性 |
面试官老陈并不是要一个完美的答案,而是在观察候选人的技术深度和遇到未知问题的反应。牢大虽然广度不错,但每个深挖点都暴露出"会用但不懂原理"的问题——这才是大厂面试最忌讳的。
本文总结:从一场模拟的Java高并发面试出发,覆盖了ConcurrentHashMap源码、Redis秒杀方案、Kafka可靠性、分布式事务、微服务网关、熔断器、K8s部署等核心知识点。希望能帮助正在准备面试的同学,不仅要"会用",更要"懂原理"。毕竟——面试官的一句"回去等通知",往往就是你和offer之间的距离。
本文纯属虚构,如有雷同,那你可能也遇到过老陈。


