当"牢大"遇上秒杀: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)    │
└─────────────┘    └──────────────┘    └──────┬──────┘
                                              │
                    ┌─────────────┐            │
                    │  账户服务     │◀───────────┘
                    │  扣款+回调    │
                    └─────────────┘

流程:

  1. 订单服务在同一本地事务中:创建订单 + 写入本地消息表(状态=待发送)
  2. 定时任务扫描本地消息表,发送到Kafka
  3. 账户服务消费消息,扣款成功→回调订单服务更新状态
  4. 扣款失败→重试N次→人工介入

方案二: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之间的距离。


本文纯属虚构,如有雷同,那你可能也遇到过老陈。

更多推荐