目录

16-项目经验

发表于
2 79.9~102.7 分钟 35958

注意:以下所有项目案例均为虚构示例,面试时必须替换成自己的真实项目经历。

介绍一下你的项目架构?

原始问法:

    1. 介绍一下你的项目架构?

来源题目:SRC-16-156-498

面试先答

**(虚构示例,需替换)**我之前做过一个 企业级智能客服 Agent 平台,面向内部 1000+ 客服人员,日处理会话量 5 万+。整体架构采用 分层微服务 + Agent 中台 的设计:接入层(API Gateway + Nginx 负载均衡)→ 业务层(会话服务、知识库服务、Agent 编排服务、工具服务)→ 数据层(MySQL 主从、Redis 集群、ElasticSearch、Milvus 向量库)→ AI 层(大模型服务、RAG 检索服务、Prompt 管理服务)。核心是 Agent 编排服务,通过 ReAct 循环驱动大模型进行意图识别 → 工具调用 → 结果生成,结合 RAG 增强和 Prompt 工程优化,实现 85%+ 的问题自动解决率。

核心结论
  • (虚构)架构风格:分层微服务 + Agent 中台
  • 核心模块:接入层 → 业务层 → 数据层 → AI 层
  • 技术亮点:ReAct 循环 + RAG + Prompt 工程
1. 架构分层图(虚构示例)
┌─────────────────────────────────────────────────┐
│                  接入层                          │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐      │
│  │  Nginx   │  │API Gateway│  │  负载均衡  │      │
│  └──────────┘  └──────────┘  └──────────┘      │
└─────────────────────┬───────────────────────────┘
                      ↓
┌─────────────────────────────────────────────────┐
│                  业务层                          │
│  ┌─────────────┐  ┌──────────────┐              │
│  │  会话服务    │  │  权限服务     │              │
│  └─────────────┘  └──────────────┘              │
│  ┌─────────────┐  ┌──────────────┐              │
│  │  Agent编排   │  │  工具服务     │              │
│  └─────────────┘  └──────────────┘              │
│  ┌─────────────┐  ┌──────────────┐              │
│  │  知识库服务   │  │  消息通知     │              │
│  └─────────────┘  └──────────────┘              │
└─────────────────────┬───────────────────────────┘
                      ↓
┌─────────────────────────────────────────────────┐
│                  AI 层                           │
│  ┌─────────────┐  ┌──────────────┐              │
│  │ 大模型服务   │  │ RAG 检索服务  │              │
│  └─────────────┘  └──────────────┘              │
│  ┌─────────────┐  ┌──────────────┐              │
│  │ Prompt管理  │  │ 可观测平台    │              │
│  └─────────────┘  └──────────────┘              │
└─────────────────────┬───────────────────────────┘
                      ↓
┌─────────────────────────────────────────────────┐
│                  数据层                          │
│  ┌─────────────┐  ┌──────────────┐              │
│  │ MySQL 主从  │  │ Redis 集群    │              │
│  └─────────────┘  └──────────────┘              │
│  ┌─────────────┐  ┌──────────────┐              │
│  │  ElasticSearch│  │ Milvus 向量库 │              │
│  └─────────────┘  └──────────────┘              │
└─────────────────────────────────────────────────┘
2. 核心模块说明(虚构示例)
模块 技术栈 核心职责
会话服务 Spring Boot + WebSocket 管理客服与用户的实时会话
Agent 编排服务 Spring Boot + LangChain4j ReAct 循环驱动、工具调度
RAG 检索服务 Milvus + ES 向量检索 + 关键词检索 + 重排序
大模型服务 GPT-4o API 意图识别、文本生成
知识库服务 MySQL + ES + Milvus 知识文档管理、向量化
工具服务 Spring Boot 订单查询、工单创建、数据统计
3. 面试回答要点
  • 架构分层清晰:接入/业务/AI/数据四层,职责明确
  • 技术选型有理有据:为什么用微服务、为什么选 Milvus
  • 突出自己负责的模块:重点讲自己主导的 Agent 编排或 RAG 模块
  • 量化成果:85% 自动解决率、5 万+ 日会话量

项目中最有挑战/出彩的地方是什么?

原始问法:

    1. 项目中最有挑战/出彩的地方是什么?

来源题目:SRC-16-156-499

面试先答

**(虚构示例,需替换)**我认为项目中最出彩的是 RAG 检索效果优化 这块。项目初期 RAG 的 Top 5 召回率只有 72%,回答经常答非所问。我主导做了 系统性的四维度优化Chunk 语义切分(替代固定长度切分,召回率 +8%)、混合检索融合(BM25 + 向量 + RRF 融合,召回率 +12%)、Cross-Encoder 重排序(精排 Top K 结果,准确率 +5%)、Query 动态扩展(同义词扩展 + 子问题分解,召回率 +3%)。最终 Top 5 召回率从 72% 提升到 93%,用户满意度从 68% 提升到 89%。

核心结论
  • (虚构)出彩点:RAG 检索效果系统性优化
  • 优化手段:Chunk 切分 + 混合检索 + 重排序 + Query 扩展
  • 成果量化:召回率 72%→93%,满意度 68%→89%
1. STAR 结构回答框架
S(Situation)- 背景
├── 项目初期 RAG 效果差
├── 用户反馈回答不准确
├── 数据:Top 5 召回率 72%
└── 影响:客服效率低,用户投诉多

T(Task)- 任务
├── 目标:将 Top 5 召回率提升到 90%+
├── 约束:不改业务逻辑,纯优化
└── 时间:2 周

A(Action)- 行动
├── 1. 构建评测数据集(2000 条标注)
├── 2. Chunk 语义切分(语义边界 + 50% 重叠)
├── 3. 混合检索融合(BM25 + 向量 + RRF)
├── 4. Cross-Encoder 重排序
└── 5. Query 动态扩展

R(Result)- 结果
├── Top 5 召回率:72% → 93%
├── 用户满意度:68% → 89%
├── 客服平均处理时间:8 分钟 → 3 分钟
└── 获得季度项目之星
2. 关键技术实现(虚构示例)
// 混合检索核心实现
public class HybridRetrievalEngine {

    private final BM25Search bm25;
    private final VectorSearch vectorSearch;
    private final CrossEncoderReranker reranker;

    public RetrievalResult retrieve(String query) {
        // Step 1: Query 扩展
        String expandedQuery = queryExpander.expand(query);

        // Step 2: 双路召回
        List<ScoredDoc> bm25Results = bm25.search(expandedQuery, 50);
        List<ScoredDoc> vectorResults = vectorSearch.search(expandedQuery, 50);

        // Step 3: RRF 融合
        List<ScoredDoc> fused = rrfFusion(bm25Results, vectorResults);

        // Step 4: Cross-Encoder 重排序
        List<ScoredDoc> reranked = reranker.rerank(expandedQuery, fused, 20);

        return new RetrievalResult(reranked);
    }

    // Reciprocal Rank Fusion
    private List<ScoredDoc> rrfFusion(List<ScoredDoc> list1,
            List<ScoredDoc> list2) {
        Map<String, Double> rrfScores = new HashMap<>();
        int k = 60;

        for (int i = 0; i < list1.size(); i++) {
            rrfScores.merge(list1.get(i).getId(),
                1.0 / (k + i + 1), Double::sum);
        }
        for (int i = 0; i < list2.size(); i++) {
            rrfScores.merge(list2.get(i).getId(),
                1.0 / (k + i + 1), Double::sum);
        }

        return rrfScores.entrySet().stream()
            .sorted(Map.Entry.<String, Double>comparingByValue().reversed())
            .map(e -> new ScoredDoc(e.getKey(), e.getValue()))
            .collect(Collectors.toList());
    }
}
3. 面试回答要点
  • 用数据说话:72% → 93%,用户满意度 68% → 89%
  • 展示思考过程:为什么选这些优化手段,每一步的决策依据
  • 体现主人翁精神:主动发现问题、主导优化方案、推动落地
  • 总结复盘:优化过程中的经验教训

项目中遇到最难的问题是什么?怎么解决的?

原始问法:

    1. 项目中遇到最难的问题是什么?怎么解决的?

来源题目:SRC-16-156-500

面试先答

**(虚构示例,需替换)**项目中最难的问题是 生产环境 MySQL 数据库连接池突然耗尽 的线上故障。现象:高峰期(每小时整点)大量请求超时,数据库连接数飙升到 maxActive=200 上限,应用报错 "Too many connections"。排查过程:先用 Arthas 观察到连接获取后未及时释放 → 定位到 Agent 工具调用的数据库操作 → 发现是 Spring 的 @Transactional 在异步线程中没有正确传播导致事务未提交、连接未释放。解决方案:一是将数据库操作从 Agent 工具中剥离到独立服务;二是使用 TransactionTemplate 编程式事务确保连接正确释放;三是增加连接池监控告警。结果:连接池峰值从 198 降到 45,零超时。

核心结论
  • (虚构)问题:MySQL 连接池耗尽导致的性能故障
  • 根因:@Transactional 在异步线程中未正确传播
  • 解决方案:架构调整 + 编程式事务 + 监控告警
  • 教训:异步场景下的事务管理需要特别注意
1. 问题排查过程(虚构示例)
故障现象:
├── 高峰期整点(每小时)请求超时
├── 数据库连接数达 200 上限
└── 应用日志:Too many connections

排查步骤:
Step 1: 确认时间规律
├── 观察到每小时整点触发
├── 整点有定时任务 + 业务高峰
└── 初步怀疑:定时任务或高并发导致

Step 2: Arthas 现场诊断
├── thread -n 3: 观察到大量线程卡在 getConnection()
├── trace DataSource.getConnection: 连接获取耗时超长
└── 定位到 Agent 工具模块的数据库操作

Step 3: 代码审查
├── Agent 工具用 @Transactional 注解
├── 调用链经过异步线程(@Async)
├── @Transactional 默认不传播到异步线程
└── 事务未提交 → 连接未释放 → 连接池耗尽

Step 4: 验证假设
├── 手动触发 Agent 工具调用
├── 监控连接池变化
└── 确认:每次调用增加 3-5 个未释放连接
2. 解决方案(虚构示例)
// 方案一:使用编程式事务(确保事务在当前线程提交)
@Service
public class ToolDataService {

    private final TransactionTemplate transactionTemplate;

    public void executeInTransaction(Runnable operation) {
        transactionTemplate.execute(status -> {
            operation.run();
            return null;
        });
    }
}

// 方案二:架构调整 —— 将数据库操作剥离到独立服务
@Service
public class OrderToolService {

    private final OrderRepository orderRepository;
    private final ToolDataService toolDataService;

    public ToolResult queryOrder(String orderId) {
        return toolDataService.executeInTransaction(() -> {
            Order order = orderRepository.findById(orderId)
                .orElseThrow(() -> new BusinessException("订单不存在"));
            return ToolResult.success(order);
        });
    }
}

// 方案三:连接池监控告警
@Configuration
public class ConnectionPoolMonitor {

    @Scheduled(fixedRate = 5000) // 每 5 秒检查
    public void monitor() {
        HikariPoolMXBean pool = getHikariPoolMXBean();
        int active = pool.getActiveConnections();
        int idle = pool.getIdleConnections();
        int waiting = pool.getThreadsAwaitingConnection();

        if (waiting > 0 || active > 150) {
            alertService.alert("数据库连接池告警",
                String.format("活跃:%d 空闲:%d 等待:%d", active, idle, waiting));
        }
    }
}
3. 面试回答要点
  • 遵循 STAR 结构:现象 → 排查 → 根因 → 解决方案 → 效果
  • 展示排查能力:如何一步步定位问题(Arthas、日志、代码审查)
  • 体现深度思考:为什么选择这个方案,有哪些备选方案
  • 总结经验教训:异步场景、事务管理、连接池监控

项目的技术选型是怎么考虑的?

原始问法:

    1. 项目的技术选型是怎么考虑的?

来源题目:SRC-16-156-501

面试先答

**(虚构示例,需替换)**我们的技术选型遵循 "成熟稳定优先、团队能力匹配、扩展性考虑、社区活跃" 四大原则。关键选型决策包括:大模型选 GPT-4o(效果最好、Function Call 稳定)、向量数据库选 Milvus(开源、支持超大规模、社区活跃)、Agent 框架选 LangChain4j(Java 生态最成熟、Spring Boot 集成好)、缓存选 Redis Cluster(成熟稳定、性能满足)、消息队列选 RocketMQ(金融级可靠性、国内生态好)。没有为了追新而选技术,所有选型都经过 技术预研 + POC 验证 + 团队培训 三步流程。

核心结论
  • 选型原则:成熟稳定 > 团队匹配 > 扩展性 > 社区活跃
  • 关键决策:GPT-4o(模型)、Milvus(向量库)、LangChain4j(框架)
  • 决策流程:预研 → POC → 培训 → 落地
1. 选型决策矩阵(虚构示例)
决策项 候选方案 最终选择 决策理由
大模型 GPT-4o / Claude 3.5 / 千问 GPT-4o Function Call 稳定、效果最好
向量数据库 Milvus / Weaviate / Qdrant Milvus 开源、支持亿级、Java SDK 完善
Agent 框架 LangChain4j / Spring AI / 自研 LangChain4j Java 生态成熟、Spring Boot 集成
缓存 Redis / Caffeine / Hazelcast Redis Cluster 成熟稳定、集群方案成熟
消息队列 RocketMQ / Kafka / Pulsar RocketMQ 金融级可靠、事务消息
搜索引擎 ElasticSearch / OpenSearch ElasticSearch 团队熟悉、生态完善
前端 React / Vue / Angular React + TypeScript 组件库丰富、类型安全
2. 选型决策流程
需求分析
    ↓
┌─────────────────────┐
│ 1. 明确选型标准      │
│    - 核心需求        │
│    - 团队能力        │
│    - 运维成本        │
│    - 社区生态        │
└─────────┬───────────┘
          ↓
┌─────────────────────┐
│ 2. 候选方案调研      │
│    - 技术文档阅读    │
│    - 社区口碑调研    │
│    - 同类项目参考    │
└─────────┬───────────┘
          ↓
┌─────────────────────┐
│ 3. POC 验证          │
│    - 搭建 Demo       │
│    - 性能测试        │
│    - 集成测试        │
│    - 团队技术评估    │
└─────────┬───────────┘
          ↓
┌─────────────────────┐
│ 4. 决策与落地        │
│    - 技术评审会      │
│    - 团队培训        │
│    - 灰度使用        │
│    - 全量推广        │
└─────────────────────┘
3. 面试回答要点
  • 展示决策依据:不是拍脑袋选的,有数据和调研支撑
  • 体现权衡能力:每个选型都有利弊,说明为什么选这个
  • 展示务实态度:不追新、不炫技,适合团队的才是最好的
  • 提及踩过的坑:如有选型不当的经历,展示复盘能力

项目中你负责的模块是什么?核心业务场景是什么?

原始问法:

    1. 项目中你负责的模块是什么?核心业务场景是什么?

来源题目:SRC-16-156-502

面试先答

**(虚构示例,需替换)**我在智能客服 Agent 平台中负责 两个核心模块Agent 编排服务RAG 检索服务Agent 编排服务 是整个系统的大脑,负责接收用户消息 → 调用大模型进行意图识别 → 调度工具(订单查询、工单创建等)→ 生成回复。核心业务场景包括:用户咨询订单状态(高频、需实时)、申请退款/换货(涉及多系统调用)、业务规则查询(依赖知识库 RAG 检索)。RAG 检索服务 负责构建企业知识库的语义检索能力,支撑 Agent 回答准确率。我主导了这两个模块的架构设计、核心代码实现和性能优化。

核心结论
  • (虚构)负责模块:Agent 编排服务 + RAG 检索服务
  • 核心场景:订单查询、退款申请、业务规则咨询
  • 贡献:架构设计、核心实现、性能优化
1. 负责模块详解(虚构示例)

模块一:Agent 编排服务

@Service
public class AgentOrchestrationService {

    private final ChatModel chatModel;
    private final ToolRegistry toolRegistry;
    private final RAGRetrievalService ragService;
    private final ContextManager contextManager;

    public AgentResponse processUserMessage(Session session, String userMessage) {
        // Step 1: 构建上下文
        List<Message> messages = contextManager.buildContext(session);
        messages.add(new UserMessage(userMessage));

        // Step 2: RAG 知识检索
        List<Document> knowledge = ragService.retrieve(userMessage);
        if (!knowledge.isEmpty()) {
            messages.add(new SystemMessage(
                "参考以下知识回答:" + formatDocuments(knowledge)
            ));
        }

        // Step 3: 调用大模型(ReAct 循环)
        ChatResponse response;
        int maxIterations = 5;

        for (int i = 0; i < maxIterations; i++) {
            response = chatModel.chat(messages);

            if (response.isToolCall()) {
                // 工具调用
                ToolResult toolResult = executeToolCall(response.getToolCall());
                messages.add(new AssistantMessage(response.getText()));
                messages.add(new ToolMessage(toolResult.toString()));
            } else {
                // 最终回复
                break;
            }
        }

        // Step 4: 保存上下文
        contextManager.saveContext(session, messages);

        return AgentResponse.from(response);
    }
}

模块二:RAG 检索服务

@Service
public class RAGRetrievalService {

    private final BM25Search bm25Search;
    private final VectorSearch vectorSearch;
    private final CrossEncoderReranker reranker;

    public List<Document> retrieve(String query) {
        // 双路召回
        List<ScoredDoc> bm25Results = bm25Search.search(query, 50);
        List<ScoredDoc> vectorResults = vectorSearch.search(query, 50);

        // RRF 融合
        List<ScoredDoc> fused = rrfFusion(bm25Results, vectorResults);

        // Cross-Encoder 重排序
        return reranker.rerank(query, fused, 5);
    }

    public void indexDocument(String docId, String content) {
        ChunkService chunkService = new ChunkService();
        List<Chunk> chunks = chunkService.semanticChunk(content);

        for (Chunk chunk : chunks) {
            // 同时写入 ES 和 Milvus
            bm25Search.index(docId, chunk.getText());
            vectorSearch.index(docId, chunk.getText());
        }
    }
}
2. 核心业务场景(虚构示例)
场景 频率 核心流程 技术要点
订单查询 高频 意图识别→工具调用→返回结果 Function Call 稳定性
退款申请 中频 多工具编排→事务处理→状态流转 多 Agent 协作
业务规则咨询 中频 RAG 检索→知识注入→生成回答 检索准确率
投诉工单创建 低频 意图识别→表单生成→工单创建 多轮对话理解
3. 面试回答要点
  • 明确职责边界:清晰说明自己负责的模块
  • 量化贡献:优化了什么、提升了多少
  • 展示技术深度:核心模块的关键设计和实现
  • 体现业务理解:知道模块在整个业务中的位置和价值

项目压测过吗?并发量多少?QPS/TPS/RT是多少?

原始问法:

    1. 项目压测过吗?并发量多少?QPS/TPS/RT是多少?

来源题目:SRC-16-156-503

面试先答

**(虚构示例,需替换)**项目做过完整的压力测试。压测场景:模拟客服高峰期会话量(整点集中涌入),使用 JMeter + 阿里云 PTS 进行。核心指标并发量:稳定 5000 QPS,峰值 8000 QPS;TPS:平均 3500 TPS(按订单查询为基准);RT:P99 响应时间 < 500ms,平均 < 100ms;错误率:< 0.1%。压测过程发现的问题:压测初期 Redis 热点 Key 导致部分缓存节点过载,通过 热点 Key 散列 + 本地缓存二级缓存 优化解决;MySQL 慢查询导致 RT 突增,通过 索引优化 + 读写分离 解决。最终所有指标满足 SLA 要求。

核心结论
  • (虚构)压测指标:5000 QPS 稳定,P99 RT < 500ms,错误率 < 0.1%
  • 发现并解决:Redis 热点 Key、MySQL 慢查询
  • 优化后满足 SLA 要求
1. 压测方案(虚构示例)

压测工具与场景

工具 用途 版本
JMeter 接口压测 5.6
阿里云 PTS 全链路压测 -
JConsole JVM 监控 -
SkyWalking 链路追踪 9.x

压测场景设计

场景 比例 描述
订单查询 60% 查询订单详情
RAG 问答 25% 知识库问答
工单创建 10% 创建投诉工单
退款申请 5% 申请退款
2. 压测结果(虚构示例)
指标 压测前 压测后 SLA 目标
QPS(稳定) 2000 5000 ≥ 3000
QPS(峰值) 3500 8000 ≥ 5000
P99 RT 1200ms 480ms < 500ms
平均 RT 350ms 95ms < 200ms
错误率 2.5% 0.08% < 0.1%
3. 压测中发现的问题与优化(虚构示例)

问题一:Redis 热点 Key

// 热点 Key 散列
public class HotKeyDispatcher {

    // 将热点 Key 散列到不同 Key,避免单 Key 过热
    public String disperseKey(String originalKey) {
        int slot = Math.abs(originalKey.hashCode()) % 16;
        return originalKey + ":slot" + slot;
    }

    // 二级缓存:本地缓存 + Redis
    @Cacheable(cacheNames = "local", key = "#root.args[0]")
    public Product getProduct(String productId) {
        return productRepository.findById(productId);
    }
}

问题二:MySQL 慢查询

-- 添加复合索引优化查询
ALTER TABLE orders
ADD INDEX idx_user_status_create (user_id, status, create_time);

-- 使用读写分离
@DataSource("slave")  // 读操作走从库
public List<Order> queryByUserId(Long userId) {
    return orderRepository.findByUserId(userId);
}
4. 面试回答要点
  • 要有具体数字:不要说"很高",要说"5000 QPS"
  • 展示压测方法论:场景设计、工具选择、指标体系
  • 体现优化能力:压测发现问题 → 分析 → 解决 → 验证
  • 诚实面对不足:如果有指标未达标,说明改进计划

项目中做过哪些性能优化?

原始问法:

    1. 项目中做过哪些性能优化?

来源题目:SRC-16-156-504

面试先答

**(虚构示例,需替换)**我在项目中做过 四层性能优化应用层(对象池复用、异步化改造、本地缓存)→ 数据库层(索引优化、SQL 调优、读写分离、分库分表)→ 缓存层(Redis Cluster、多级缓存、热点 Key 处理)→ 架构层(CDN 静态化、消息队列削峰、服务降级)。最有效的三项优化是:MySQL 分库分表(单表 500 万→按用户 ID 分 10 库 100 表,查询 RT 从 800ms 降到 50ms)、Redis 多级缓存(本地缓存 + Redis + DB,热点数据命中率 99%+)、异步化改造(核心接口同步→异步 CompletableFuture,吞吐量 +200%)。

核心结论
  • 四层优化:应用层 → 数据库层 → 缓存层 → 架构层
  • 关键优化:分库分表、多级缓存、异步化
  • 量化成果:查询 RT 降 94%,吞吐量 +200%
1. 优化全景图(虚构示例)
┌─────────────────────────────────────────────────┐
│              架构层优化                          │
│  CDN 静态化 | MQ 削峰 | 服务降级                 │
└─────────────────────┬───────────────────────────┘
                      ↓
┌─────────────────────────────────────────────────┐
│              应用层优化                          │
│  对象池复用 | 异步化 | 本地缓存 | 代码优化       │
└─────────────────────┬───────────────────────────┘
                      ↓
┌─────────────────────────────────────────────────┐
│              缓存层优化                          │
│  Redis Cluster | 多级缓存 | 热点 Key | 穿透防护  │
└─────────────────────┬───────────────────────────┘
                      ↓
┌─────────────────────────────────────────────────┐
│              数据库层优化                        │
│  分库分表 | 索引优化 | SQL 调优 | 读写分离       │
└─────────────────────────────────────────────────┘
2. 关键优化实现(虚构示例)

数据库分库分表

// ShardingSphere 分库分表配置
@Configuration
public class ShardingConfig {

    @Bean
    public ShardingRuleConfiguration shardingRule() {
        ShardingRuleConfiguration rule = new ShardingRuleConfiguration();
        rule.setTableShardingStrategyConfigs(Arrays.asList(
            new TableShardingStrategyConfiguration(
                "t_order",
                "user_id",
                "mod"
            )
        ));
        // 按 user_id 分 10 库 100 表
        rule.setDatabaseShardingStrategyConfigs(...);
        return rule;
    }
}

多级缓存

@Service
public class MultiLevelCacheService {

    private final CacheManager localCacheManager;  // Caffeine 本地缓存
    private final RedisTemplate redisTemplate;      // Redis 分布式缓存

    public Object getData(String key) {
        // L1: 本地缓存(Caffeine,10 分钟过期)
        Cache localCache = localCacheManager.getCache("local");
        Object value = localCache.get(key);
        if (value != null) return value;

        // L2: Redis 缓存(1 小时过期)
        value = redisTemplate.opsForValue().get(key);
        if (value != null) {
            localCache.put(key, value);  // 回填本地缓存
            return value;
        }

        // L3: 数据库查询
        value = queryFromDatabase(key);

        // 写入两级缓存
        redisTemplate.opsForValue().set(key, value, 1, TimeUnit.HOURS);
        localCache.put(key, value);

        return value;
    }
}

异步化改造

// 核心接口异步化
@Service
public class OrderService {

    @Async("orderExecutor")
    public CompletableFuture<OrderResult> createOrderAsync(OrderDTO dto) {
        // 同步逻辑改为异步执行
        Order order = orderRepository.save(convertToEntity(dto));

        // 异步发送通知
        notificationService.sendOrderCreatedNotification(order);

        return CompletableFuture.completedFuture(
            new OrderResult(order.getId(), "SUCCESS")
        );
    }

    // 批量并行处理
    public List<CompletableFuture<OrderResult>> batchCreateOrders(
            List<OrderDTO> dtos) {
        return dtos.stream()
            .map(this::createOrderAsync)
            .collect(Collectors.toList());
    }
}
3. 面试回答要点
  • 分层展示:从底层到上层,展示系统性的优化思路
  • 量化效果:每个优化都有数据支撑
  • 展示取舍:说明哪些优化没做、为什么
  • 体现体系化思维:不是零散的优化,而是系统性规划

项目中如何保证接口的幂等性?

原始问法:

    1. 项目中如何保证接口的幂等性?

来源题目:SRC-16-156-505

面试先答

**(虚构示例,需替换)**我们项目中对 创建型接口(创建订单、支付、退款等)实现了严格的幂等性保证。核心方案是 唯一 Token + Redis 状态锁 + 数据库唯一索引 三重保障:第一重:客户端请求前先获取唯一幂等 Token(UUID),请求时携带;第二重:服务端收到请求后先通过 Token 检查 Redis 中是否存在,存在则直接返回之前的结果;第三重:数据库层通过唯一索引(如 order_no + user_id)防止重复插入。同时用 状态机校验 确保状态流转的幂等性(如只有待支付状态才能支付,重复支付请求直接返回当前状态)。最终实现了所有关键接口的严格幂等。

核心结论
  • 三重保障:唯一 Token + Redis 锁 + 数据库唯一索引
  • 状态机校验:确保状态流转幂等
  • 覆盖场景:创建型、支付型、状态变更型接口
1. 幂等方案实现(虚构示例)

幂等 Token 机制

// 幂等注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
    String keyPrefix() default "idempotent:";
    int expireSeconds() default 300;
}

// 幂等切面
@Aspect
@Component
public class IdempotentAspect {

    private final RedisTemplate redisTemplate;

    @Around("@annotation(idempotent)")
    public Object around(ProceedingJoinPoint pjp, Idempotent idempotent)
            throws Throwable {
        String token = extractToken(pjp);  // 从请求头获取
        String key = idempotent.keyPrefix() + token;

        // 检查是否已处理
        Boolean exists = redisTemplate.hasKey(key);
        if (Boolean.TRUE.equals(exists)) {
            // 已处理,直接返回缓存的结果
            return redisTemplate.opsForValue().get(key + ":result");
        }

        // 加锁并执行
        Boolean locked = redisTemplate.opsForValue()
            .setIfAbsent(key, "processing", 10, TimeUnit.SECONDS);
        if (Boolean.FALSE.equals(locked)) {
            throw new BusinessException("请求正在处理中,请稍后重试");
        }

        try {
            Object result = pjp.proceed();
            // 缓存结果
            redisTemplate.opsForValue().set(key + ":result", result,
                idempotent.expireSeconds(), TimeUnit.SECONDS);
            return result;
        } finally {
            redisTemplate.delete(key);  // 释放锁
        }
    }
}

数据库唯一索引

-- 创建型接口的唯一索引
ALTER TABLE orders
ADD UNIQUE KEY uk_order_user (order_no, user_id);

-- 状态机校验
@Service
public class OrderService {

    @Idempotent(keyPrefix = "order:create:")
    public OrderResult createOrder(OrderCreateDTO dto) {
        // 数据库唯一索引防止重复创建
        try {
            Order order = orderRepository.save(convertToEntity(dto));
            return OrderResult.success(order.getId());
        } catch (DuplicateKeyException e) {
            // 唯一索引冲突,返回已存在的订单
            Order existing = orderRepository.findByOrderNo(dto.getOrderNo());
            return OrderResult.success(existing.getId());
        }
    }
}

状态机校验

// 支付接口的状态机幂等
@Service
public class PaymentService {

    private final Map<OrderStatus, Set<OrderStatus>> transitions = Map.of(
        OrderStatus.PENDING_PAYMENT, Set.of(OrderStatus.PAID),
        OrderStatus.PAID, Set.of(),  // 已支付,不再流转
        OrderStatus.CANCELLED, Set.of()  // 已取消
    );

    @Idempotent(keyPrefix = "payment:")
    public PaymentResult payOrder(String orderId, PaymentDTO dto) {
        Order order = orderRepository.findById(orderId);

        // 状态机校验
        if (!transitions.getOrDefault(order.getStatus(), Set.of())
                .contains(OrderStatus.PAID)) {
            // 状态不允许变更,返回当前状态
            return PaymentResult.currentStatus(order.getStatus());
        }

        // 执行支付...
        order.setStatus(OrderStatus.PAID);
        orderRepository.save(order);

        return PaymentResult.success();
    }
}
2. 面试回答要点
  • 展示系统性思路:不是单一方案,而是多层保障
  • 覆盖所有场景:创建、支付、状态变更等不同类型的幂等
  • 说明 trade-off:幂等会增加一些复杂度,说明是否值得
  • 提及边界情况:网络超时重试、并发请求等极端情况的处理

分享一个你印象最深的Bug?当时的现象、定位过程、解决方案、学到的东西?

原始问法:

    1. 分享一个你印象最深的Bug?当时的现象、定位过程、解决方案、学到的东西?

来源题目:SRC-16-156-506

面试先答

**(虚构示例,需替换)**印象最深的是一个 Agent 并发会话上下文串扰 的 Bug。现象:客服 A 和用户对话时,偶尔会出现回复中混入客服 B 的对话内容。排查过程:首先怀疑是大模型的问题,排除后发现是会话存储的 Key 冲突。原来我们用 sessionId 作为 Redis Key,但并发场景下 sessionId 的生成逻辑有缺陷——用户快速连续发送消息时,两次请求用了相同的 sessionId 片段根因:sessionId 用 userId + timestamp 拼接,毫秒级时间戳在高并发下会重复。解决方案:改用 UUID.randomUUID() 生成唯一 sessionId + Redis 的 SETNX 确保唯一性。学到的教训:分布式系统中任何 ID 生成逻辑都需要考虑并发场景,不能依赖时间戳的唯一性。

核心结论
  • (虚构)Bug:并发场景下 sessionId 重复导致会话串扰
  • 根因:时间戳拼接 ID 在高并发下不唯一
  • 解决:改用 UUID + SETNX 唯一性保证
  • 教训:分布式 ID 不能依赖时间戳
1. Bug 详情(虚构示例)

现象

客服 A 看到的对话:
  用户:我的订单什么时候能发货?
  Agent:您的订单【ORD12345】预计明天发货
  用户:能帮我查一下物流吗?
  Agent:(错误回复)您的订单【ORD67890】已签收  ← 这是客服 B 的订单!

排查过程

Step 1: 怀疑大模型问题
├── 检查是否大模型返回错误
├── 对比正确/错误请求的大模型输入
└── 发现:大模型输入中包含了错误的上下文 → 不是大模型的问题

Step 2: 检查上下文存储
├── 查看 Redis 中 sessionId 对应的数据
├── 发现:有时两个不同的会话指向同一个 Redis Key
└── 定位:sessionId 生成逻辑有问题

Step 3: 分析 sessionId 生成
├── 原代码:
└── String sessionId = userId + "_" + System.currentTimeMillis();

Step 4: 验证假设
├── 高并发下,同一毫秒内多个请求
├── System.currentTimeMillis() 返回相同值
├── 导致 sessionId 重复 → 上下文串扰
└── 确认根因

解决方案

// 修复前:
public String generateSessionId(Long userId) {
    return userId + "_" + System.currentTimeMillis();
    // 高并发下毫秒级重复!
}

// 修复后:
public String generateSessionId(Long userId) {
    return userId + "_" + UUID.randomUUID().toString().replace("-", "");
    // UUID 保证全局唯一
}

// 额外保障:Redis SETNX 确保唯一性
public void saveSession(String sessionId, SessionData data) {
    Boolean success = redisTemplate.opsForValue()
        .setIfAbsent(sessionId, serialize(data), 24, TimeUnit.HOURS);
    if (Boolean.FALSE.equals(success)) {
        log.warn("Session ID冲突: {}", sessionId);
        // 重新生成
        sessionId = generateSessionId(userId);
    }
}
2. 面试回答要点
  • 按照 STAR + Bug 结构回答:现象 → 排查 → 根因 → 解决 → 教训
  • 展示排查能力:如何一步步排除干扰、定位根因
  • 体现反思能力:从 Bug 中总结的经验和改进措施
  • 诚实面对:不要回避自己的错误,重点展示从中学到的东西

项目中使用消息队列有没有遇到什么问题?怎么解决的?

原始问法:

    1. 项目中使用消息队列有没有遇到什么问题?怎么解决的?

来源题目:SRC-16-156-507

面试先答

**(虚构示例,需替换)**项目中使用 RocketMQ 遇到过 三个典型问题消息重复消费消息积压消息顺序性重复消费:消费者超时重试导致重复执行,通过 消费去重(Redis 记录消费 ID + 数据库唯一索引)解决。消息积压:双十一促销导致订单消息积压 10 万+,通过 扩容消费者 + 临时降级(非核心消息丢弃或批量处理)解决,2 小时内清空积压。消息顺序性:同一订单的状态变更消息需要严格有序,通过 MessageQueueSelector 按订单 ID 选择队列 保证同一订单的消息进入同一个队列,配合 单线程消费者 保证顺序。

核心结论
  • 三大问题:重复消费、消息积压、消息顺序性
  • 解决方案:消费去重、扩容+降级、队列路由
  • 关键能力:MQ 问题预防 + 快速处理
1. 问题与解决方案(虚构示例)

问题一:消息重复消费

// 消费者去重
@RocketMQMessageListener(topic = "order_topic",
                         consumerGroup = "order_group")
public class OrderConsumer implements MessageListener {

    private final RedisTemplate redisTemplate;
    private final OrderRepository orderRepository;

    @Override
    public void onMessage(Message msg) {
        String msgId = msg.getMsgId();
        String dedupKey = "order:consumed:" + msgId;

        // Redis 去重(SETNX)
        Boolean isFirst = redisTemplate.opsForValue()
            .setIfAbsent(dedupKey, "1", 24, TimeUnit.HOURS);

        if (Boolean.FALSE.equals(isFirst)) {
            log.warn("消息已消费,跳过: {}", msgId);
            return;
        }

        // 数据库唯一索引二次保障
        try {
            processMessage(msg);
        } catch (DuplicateKeyException e) {
            log.warn("重复消息,跳过: {}", msgId);
        }
    }
}

问题二:消息积压

// 积压处理方案
@Service
public class MessageBacklogHandler {

    // 1. 临时扩容消费者(通过 API 动态调整实例数)
    public void scaleUpConsumers(int targetInstances) {
        // 调用 K8s API 增加消费者 Pod 数量
        k8sClient.scaleDeployment("order-consumer", targetInstances);
    }

    // 2. 非核心消息降级
    public void processWithPriority(List<Message> messages) {
        List<Message> critical = messages.stream()
            .filter(m -> m.getPriority() == Priority.HIGH)
            .collect(Collectors.toList());

        List<Message> normal = messages.stream()
            .filter(m -> m.getPriority() == Priority.NORMAL)
            .collect(Collectors.toList());

        // 优先处理高优先级消息
        critical.forEach(this::processMessage);

        // 普通消息批量处理
        processBatch(normal);
    }

    // 3. 批量处理减少开销
    private void processBatch(List<Message> messages) {
        List<String> orderIds = messages.stream()
            .map(Message::getOrderId)
            .collect(Collectors.toList());

        // 批量数据库操作
        orderRepository.batchUpdateStatus(orderIds, Status.PROCESSING);
    }
}

问题三:消息顺序性

// 生产者:按订单 ID 选择队列
public void sendOrderMessage(OrderEvent event) {
    rocketMQTemplate.asyncSend(
        "order_topic",
        MessageBuilder.withPayload(event)
            .setHeader("orderId", event.getOrderId())
            .build(),
        new SendCallback() { ... },
        // 关键:按 orderId 选择 MessageQueue
        new MessageQueueSelector() {
            @Override
            public MessageQueue select(List<MessageQueue> mqs,
                    Message msg, Object arg) {
                String orderId = (String) arg;
                int index = Math.abs(orderId.hashCode()) % mqs.size();
                return mqs.get(index);
            }
        },
        event.getOrderId()
    );
}

// 消费者:单线程消费保证顺序
@RocketMQMessageListener(
    topic = "order_topic",
    consumerGroup = "order_orderly_group",
    // 消费模式:顺序消费
    messageModel = MessageModel.CLUSTERING
)
public class OrderOrderlyConsumer implements MessageListenerOrderly {

    @Override
    public boolean consumeOrderly(MessageExt msg,
            ConsumeOrderlyContext context) {
        // 单线程处理,天然保证同一队列内消息顺序
        processMessage(msg);
        return true;
    }
}
2. 面试回答要点
  • 覆盖 MQ 核心问题:重复、积压、顺序、丢失
  • 展示实战经验:不是理论,是真实遇到并解决的问题
  • 体现预案意识:如何预防这些问题(监控、告警、预案)
  • 量化效果:积压处理用了多久、恢复效果如何

项目中权限控制模块,你是如何设计数据权限和功能权限的隔离逻辑的?

原始问法:

    1. 项目中权限控制模块,你是如何设计数据权限和功能权限的隔离逻辑的?

来源题目:SRC-16-156-508

面试先答

**(虚构示例,需替换)**权限控制模块采用 RBAC + 数据权限注解 的双层设计。功能权限:基于经典 RBAC 模型(用户→角色→权限),通过 Spring Security + JWT + 注解 实现,在 Controller 方法上标注 @PreAuthorize("hasAuthority('order:query')") 控制接口访问。数据权限:通过 自定义注解 @DataScope + AOP 切面 实现,在 Service 层根据用户角色自动拼接数据过滤条件(如部门经理只能看本部门数据,普通员工只能看自己的数据)。隔离逻辑:功能权限在入口层拦截,数据权限在数据层过滤,两者独立生效、互不干扰。核心是 认证和授权分离、功能和数据权限分离 的设计思想。

核心结论
  • 功能权限:RBAC + Spring Security + JWT(入口层拦截)
  • 数据权限:自定义注解 + AOP 切面(数据层过滤)
  • 设计原则:认证/授权分离、功能/数据权限分离
1. 功能权限设计(虚构示例)
// RBAC 数据模型
@Entity
public class SysUser {
    private Long id;
    private String username;
    private String password;
}

@Entity
public class SysRole {
    private Long id;
    private String name;
    private String code;
}

@Entity
public class SysPermission {
    private Long id;
    private String code;  // 如 "order:query"
    private String type;  // MENU / BUTTON / API
}

// Spring Security 配置
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) {
        http
            .addFilterBefore(jwtAuthFilter(), UsernamePasswordAuthenticationFilter.class)
            .authorizeRequests()
            .antMatchers("/api/admin/**").hasRole("ADMIN")
            .antMatchers("/api/order/**").hasAuthority("order:query")
            .anyRequest().authenticated();
    }
}
2. 数据权限设计(虚构示例)
// 数据权限注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DataScope {
    String deptAlias() default "d";   // 部门表别名
    String userAlias() default "u";   // 用户表别名
}

// 数据权限切面
@Aspect
@Component
public class DataScopeAspect {

    @Before("@annotation(dataScope)")
    public void doBefore(JoinPoint point, DataScope dataScope) {
        LoginUser user = SecurityUtils.getCurrentUser();

        // 构建数据权限 SQL 条件
        StringBuilder sql = new StringBuilder(" WHERE 1=1 ");

        if (user.isAdmin()) {
            sql.append(" ");  // 管理员看全部
        } else if (user.isDeptManager()) {
            // 部门经理看本部门
            sql.append(" AND dept_id = ").append(user.getDeptId());
        } else {
            // 普通员工看自己
            sql.append(" AND create_by = ").append(user.getUserId());
        }

        // 将条件放入 ThreadLocal,供后续 SQL 使用
        DataScopeHolder.set(sql.toString());
    }
}

// Service 使用示例
@Service
public class OrderService {

    @DataScope(deptAlias = "o", userAlias = "u")
    public List<Order> queryOrders(OrderQueryDTO dto) {
        MybatisPlusWrapper<Order> wrapper = new MybatisPlusWrapper<>();
        // 自动拼接数据权限条件
        String dataScopeSql = DataScopeHolder.get();
        wrapper.apply(dataScopeSql);
        return orderMapper.selectList(wrapper);
    }
}
3. 权限隔离逻辑
请求到达
    ↓
┌─────────────────────────────┐
│  Layer 1: 认证(Authentication)│
│  JWT Token 验证              │
│  获取用户身份信息             │
└─────────────┬───────────────┘
              ↓
┌─────────────────────────────┐
│  Layer 2: 功能权限(RBAC)    │
│  @PreAuthorize 注解校验       │
│  是否有接口访问权限           │
│  → Controller 层拦截          │
└─────────────┬───────────────┘
              ↓
┌─────────────────────────────┐
│  Layer 3: 数据权限(DataScope)│
│  @DataScope 注解 + AOP        │
│  拼接数据过滤条件             │
│  → Service/Mapper 层过滤     │
└─────────────────────────────┘
4. 面试回答要点
  • 展示分层设计思想:认证 → 功能权限 → 数据权限,层层递进
  • 体现实战经验:不是纸上谈兵,有具体的注解和切面实现
  • 说明扩展性:如何支持更复杂的权限需求(如行级权限、字段级权限)
  • 提及安全考虑:防止越权访问的措施

项目中如果抛出自定义异常,你是如何保证异常信息能准确定位问题的?

原始问法:

    1. 项目中如果抛出自定义异常,你是如何保证异常信息能准确定位问题的?

来源题目:SRC-16-156-509

面试先答

**(虚构示例,需替换)**我们通过 结构化异常 + 全局异常处理 + 唯一追踪 ID + 分级日志 四件套保证异常的可定位性。结构化异常:自定义 BusinessException 携带错误码、错误信息、模块名、操作提示,每个错误码对应唯一问题类型。全局异常处理@ControllerAdvice 统一捕获,记录完整堆栈 + 上下文信息(Trace ID、用户 ID、请求参数)。唯一追踪 ID:每次请求生成 Trace ID,贯穿日志和异常,排查时可以通过 Trace ID 串联整条调用链。分级日志:ERROR 级记录堆栈,WARN 级记录上下文,INFO 级记录关键节点。最终实现 "一错误码一定位" 的快速排查能力。

核心结论
  • 四件套:结构化异常 → 全局处理 → Trace ID → 分级日志
  • 核心思想:每个异常都有唯一标识和完整上下文
  • 效果:快速定位问题,从分钟级到秒级
1. 结构化异常设计(虚构示例)
// 错误码体系
public enum ErrorCode {
    // 订单相关(1000-1999)
    ORDER_NOT_FOUND(1001, "订单不存在"),
    ORDER_STATUS_INVALID(1002, "订单状态不正确"),

    // 支付相关(2000-2999)
    PAYMENT_FAILED(2001, "支付失败"),
    PAYMENT_TIMEOUT(2002, "支付超时"),

    // Agent 相关(3000-3999)
    AGENT_TOOL_ERROR(3001, "工具调用失败"),
    AGENT_RAG_ERROR(3002, "RAG 检索异常");

    private final int code;
    private final String message;
}

// 自定义业务异常
public class BusinessException extends RuntimeException {
    private final ErrorCode errorCode;
    private final String traceId;       // 追踪 ID
    private final String module;        // 模块名
    private final Map<String, Object> context;  // 上下文参数

    public BusinessException(ErrorCode errorCode, String traceId,
            String module, Map<String, Object> context) {
        super(errorCode.getMessage());
        this.errorCode = errorCode;
        this.traceId = traceId;
        this.module = module;
        this.context = context;
    }
}
2. 全局异常处理(虚构示例)
@RestControllerAdvice
public class GlobalExceptionHandler {

    private final Logger log = LoggerFactory.getLogger(this.getClass());

    @ExceptionHandler(BusinessException.class)
    public ResponseEntity<?> handleBusinessException(
            BusinessException ex, HttpServletRequest request) {

        // 记录完整异常信息
        log.error("业务异常 [TraceId:{}] [模块:{}] [错误码:{}] [用户:{}] " +
            "[路径:{}] [参数:{}] [堆栈:{}]",
            ex.getTraceId(),
            ex.getModule(),
            ex.getErrorCode().getCode(),
            getCurrentUserId(),
            request.getRequestURI(),
            ex.getContext(),
            ex.getStackTrace());

        // 返回结构化响应
        ErrorResponse response = ErrorResponse.builder()
            .code(ex.getErrorCode().getCode())
            .message(ex.getErrorCode().getMessage())
            .traceId(ex.getTraceId())
            .timestamp(LocalDateTime.now())
            .build();

        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response);
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<?> handleUnknownException(
            Exception ex, HttpServletRequest request) {

        String traceId = request.getHeader("X-Trace-Id");

        log.error("未知异常 [TraceId:{}] [路径:{}]", traceId,
            request.getRequestURI(), ex);

        ErrorResponse response = ErrorResponse.builder()
            .code(500)
            .message("系统内部错误")
            .traceId(traceId)
            .timestamp(LocalDateTime.now())
            .build();

        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
            .body(response);
    }
}
3. Trace ID 全链路追踪(虚构示例)
// Trace ID 过滤器
@Component
public class TraceIdFilter implements Filter {

    private static final String TRACE_ID_HEADER = "X-Trace-Id";
    private static final ThreadLocal<String> TRACE_ID_HOLDER = new ThreadLocal<>();

    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
            FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;

        // 生成或获取 Trace ID
        String traceId = httpRequest.getHeader(TRACE_ID_HEADER);
        if (traceId == null || traceId.isEmpty()) {
            traceId = UUID.randomUUID().toString().replace("-", "");
        }

        TRACE_ID_HOLDER.set(traceId);
        response.setHeader(TRACE_ID_HEADER, traceId);

        try {
            chain.doFilter(request, response);
        } finally {
            TRACE_ID_HOLDER.remove();
        }
    }

    public static String getCurrentTraceId() {
        return TRACE_ID_HOLDER.get();
    }
}
4. 面试回答要点
  • 展示体系化设计:不是简单的 try-catch,而是完整的异常管理体系
  • 体现可观测性:Trace ID + 结构化日志,支持快速排查
  • 关注用户体验:对用户返回友好的错误信息,不暴露技术细节
  • 提及监控告警:异常监控大盘、告警规则

是否部署过项目?具体怎么部署的?

原始问法:

    1. 是否部署过项目?具体怎么部署的?

来源题目:SRC-16-156-510

面试先答

**(虚构示例,需替换)**项目采用 Docker + Kubernetes + CI/CD 的容器化部署方案。部署流程:开发提交代码 → GitLab CI 触发 → 代码检查(SonarQube)→ 单元测试 → 构建 Docker 镜像 → 推送到 Harbor 镜像仓库 → 部署到 Kubernetes(Dev → Test → Staging → Production 四环境)。K8s 核心配置:Deployment(副本数、健康检查、滚动更新)、Service(负载均衡)、ConfigMap/Secret(配置和密钥管理)、HPA(自动扩缩容,CPU > 70% 自动扩容)、Ingress(域名路由)。发布策略:灰度发布(金丝雀部署,先部署 10% 流量,观察 30 分钟后全量)+ 快速回滚。

核心结论
  • 部署方案:Docker + Kubernetes + CI/CD
  • 部署流程:代码提交 → CI 检查 → 镜像构建 → K8s 部署
  • 发布策略:灰度发布 + 快速回滚 + HPA 自动扩缩
1. CI/CD 流程(虚构示例)
开发者提交代码
    ↓
┌─────────────────────────────┐
│ GitLab CI Pipeline           │
│ 1. 代码检查(SonarQube)     │
│ 2. 单元测试(JUnit)         │
│ 3. 构建打包(Maven)         │
│ 4. 构建 Docker 镜像          │
│ 5. 推送到 Harbor 仓库        │
└─────────────┬───────────────┘
              ↓
┌─────────────────────────────┐
│ Kubernetes 部署              │
│ 1. Dev 环境自动部署          │
│ 2. Test 环境手动触发         │
│ 3. Staging 环境审批后部署    │
│ 4. Production 灰度发布       │
└─────────────────────────────┘
2. K8s 核心配置(虚构示例)
# Deployment 配置
apiVersion: apps/v1
kind: Deployment
metadata:
  name: agent-service
  namespace: production
spec:
  replicas: 5
  selector:
    matchLabels:
      app: agent-service
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 25%
      maxUnavailable: 0  # 零宕机
  template:
    spec:
      containers:
      - name: agent-service
        image: harbor.example.com/agent-service:latest
        ports:
        - containerPort: 8080
        env:
        - name: SPRING_PROFILES_ACTIVE
          value: "prod"
        envFrom:
        - configMapRef:
            name: agent-config
        - secretRef:
            name: agent-secrets
        resources:
          requests:
            cpu: "500m"
            memory: "1Gi"
          limits:
            cpu: "2000m"
            memory: "4Gi"
        livenessProbe:
          httpGet:
            path: /actuator/health
            port: 8080
          initialDelaySeconds: 30
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /actuator/ready
            port: 8080
          initialDelaySeconds: 10
          periodSeconds: 5
        lifecycle:
          preStop:
            exec:
              command: ["sh", "-c", "sleep 30"]  # 优雅关闭
---
# HPA 自动扩缩容
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: agent-service-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: agent-service
  minReplicas: 3
  maxReplicas: 20
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70
3. 灰度发布策略(虚构示例)
# Ingress 灰度配置
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: agent-ingress
  annotations:
    nginx.ingress.kubernetes.io/canary-by-header: "canary"
    nginx.ingress.kubernetes.io/canary-weight: "10"  # 10% 流量
spec:
  rules:
  - host: agent.example.com
    http:
      paths:
      - path: /
        backend:
          service:
            name: agent-service-canary
            port:
              number: 8080
---
apiVersion: v1
kind: Service
metadata:
  name: agent-service-canary
spec:
  selector:
    app: agent-service
    version: v2  # 新版本
  ports:
  - port: 8080
4. 面试回答要点
  • 展示完整流程:从代码提交到生产部署的完整链路
  • 体现工程化能力:CI/CD、Dockerfile、K8s YAML 等
  • 关注高可用:健康检查、滚动更新、HPA、优雅关闭
  • 提及发布策略:灰度、回滚、监控

项目中如何保证数据库和ES数据同步?

原始问法:

    1. 项目中如何保证数据库和ES数据同步?

来源题目:SRC-16-156-511

面试先答

**(虚构示例,需替换)**数据库和 ES 的数据同步采用 "增量实时 + 全量定期 + 最终一致性校验" 的三重保障方案。增量实时同步:通过 Canal 监听 MySQL Binlog,捕获数据变更事件(INSERT/UPDATE/DELETE),发送到 RocketMQ,消费者消费后同步到 ES。全量定期同步:每天凌晨定时任务全量扫描 MySQL 数据,重建 ES 索引,保证数据最终一致性。一致性校验:每小时对比 MySQL 和 ES 的数据量,不一致时触发增量补偿。同时通过 版本号机制(每次更新版本号,ES 只保留最新版本)解决并发更新冲突。

核心结论
  • 三重保障:Binlog 增量 + 定时全量 + 周期校验
  • 核心机制:Canal + MQ + 版本号控制
  • 一致性:最终一致性,秒级延迟
1. 同步架构(虚构示例)
┌──────────┐    Binlog     ┌──────────┐    MQ     ┌──────────┐
│  MySQL   │──────────────→│  Canal   │────────→│ RocketMQ │
│  (主库)  │               │ (监听)   │         │ (消息)   │
└──────────┘               └──────────┘         └────┬─────┘
                                                     ↓
┌──────────┐                                    ┌──────────┐
│  业务服务 │                                    │  ES Consumer
│  (写DB)  │                                    │ (消费MQ) │
└────┬─────┘                                    └────┬─────┘
     │                                               ↓
     │ 定期全量扫描                              ┌──────────┐
     └──────────────────────────────────────────→│  ElasticSearch
                                                 │ (索引)   │
                                                 └──────────┘
                                                      ↑
                                                      │ 每小时校验
                                                 ┌──────────┐
                                                 │ 数据校验  │
                                                 │ 定时任务  │
                                                 └──────────┘
2. 核心实现(虚构示例)

Canal 配置

# canal 配置实例.properties
canal.instance.master.address=mysql-master:3306
canal.instance.dbUsername=canal
canal.instance.dbPassword=canal_pwd
canal.instance.filter.regex=order_db\\..*
canal.mq.exchange=canal.exchange
canal.mq.routingKey=canal.routing.key

MQ 消费者

@RocketMQMessageListener(
    topic = "canal_order_topic",
    consumerGroup = "es_sync_group"
)
public class ESSyncConsumer implements MessageListener {

    private final RestHighLevelClient esClient;

    @Override
    public void onMessage(Message msg) {
        CanalMessage canalMsg = JSON.parseObject(
            new String(msg.getBody()), CanalMessage.class
        );

        String tableName = canalMsg.getTable();
        String eventType = canalMsg.getType();  // INSERT/UPDATE/DELETE

        switch (eventType) {
            case "INSERT":
            case "UPDATE":
                // 同步到 ES(upsert 语义)
                esClient.index(IndexRequest.of(
                    i -> i.index(tableName)
                        .id(canalMsg.getId())
                        .document(canalMsg.getData())
                ));
                break;
            case "DELETE":
                // 删除 ES 文档
                esClient.delete(DeleteRequest.of(
                    tableName, canalMsg.getId()
                ));
                break;
        }
    }
}

版本号控制

// 解决并发更新冲突
@Entity
public class Order {
    @Version  // JPA 乐观锁
    private Long version;

    private Long id;
    private String status;
    private LocalDateTime updatedAt;
}

// ES 索引中存储版本号
public void syncToES(Order order) {
    esClient.index(IndexRequest.of(i -> {
        try {
            return i.index("orders")
                .id(String.valueOf(order.getId()))
                .document(Map.of(
                    "order_id", order.getId(),
                    "status", order.getStatus(),
                    "version", order.getVersion(),  // 版本号
                    "updated_at", order.getUpdatedAt()
                ))
                .ifSeqNoIsPresent()  // 仅当版本号更大时才更新
                .set("seq_no_primary_term",
                    getCurrentESVersion(order.getId()));
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }));
}

全量同步

@Scheduled(cron = "0 0 2 * * ?")  // 每天凌晨 2 点
public void fullSync() {
    List<Order> allOrders = orderRepository.findAll();

    // 分批处理,每批 500 条
    Lists.partition(allOrders, 500).forEach(batch -> {
        esClient.bulk(BulkRequest.of(b -> {
            for (Order order : batch) {
                b.index(IndexRequest.of(i ->
                    i.index("orders")
                        .id(String.valueOf(order.getId()))
                        .document(convertToDocument(order))
                ));
            }
            return b;
        }));
    });

    log.info("全量同步完成,共 {} 条", allOrders.size());
}
3. 面试回答要点
  • 展示完整性:增量 + 全量 + 校验的三重保障
  • 体现技术深度:Canal、Binlog、版本号控制
  • 说明权衡:为什么选 Canal 而不是同步双写、优缺点
  • 提及一致性:说明是最终一致性,允许短暂不一致

项目中Token过期机制是怎么设计的?

原始问法:

    1. 项目中Token过期机制是怎么设计的?

来源题目:SRC-16-156-512

面试先答

**(虚构示例,需替换)**Token 过期机制采用 Access Token + Refresh Token 双令牌 设计。Access Token:短期有效(30 分钟),用于日常接口访问,存放在 Redis 中,Key 为 token:{userId},过期自动清除。Refresh Token:长期有效(7 天),用于刷新 Access Token,存放在 Redis 中,Key 为 refresh:{userId}刷新流程:Access Token 过期后,前端用 Refresh Token 调用 /api/auth/refresh → 后端校验 Refresh Token → 生成新的 Access Token 和 Refresh Token(旧 Refresh Token 立即失效,防止 token 重放)。退出登录:删除 Redis 中的两个 Token。安全增强:Token 绑定设备指纹 + 异常登录告警 + 强制下线功能。

核心结论
  • 双令牌设计:Access Token(30分钟)+ Refresh Token(7天)
  • 安全机制:Token 旋转、设备绑定、异常告警、强制下线
  • 存储方案:Redis 存储,自动过期
1. Token 生命周期(虚构示例)
用户登录
    ↓
┌─────────────────────────────┐
│ 生成 Access Token (30min)   │
│ 生成 Refresh Token (7day)   │
│ 存入 Redis                   │
│ 返回两个 Token 给前端         │
└─────────────┬───────────────┘
              ↓
┌─────────────────────────────┐
│ 正常使用阶段                 │
│ 前端带 Access Token 请求     │
│ 后端校验 → 返回数据           │
│ Access Token 即将过期时      │
│ 前端自动用 Refresh Token 刷新│
└─────────────┬───────────────┘
              ↓
┌─────────────────────────────┐
│ 刷新阶段                     │
│ 校验 Refresh Token           │
│ 生成新的 Access + Refresh    │
│ 旧 Refresh Token 立即失效    │
│ 返回新的两个 Token            │
└─────────────┬───────────────┘
              ↓
┌─────────────────────────────┐
│ 安全机制                     │
│ 设备指纹绑定校验             │
│ 异常登录告警                 │
│ 管理员可强制下线              │
│ Refresh Token 过期 → 需重新登录│
└─────────────────────────────┘
2. 核心实现(虚构示例)
// Token 服务
@Service
public class TokenService {

    private static final long ACCESS_TOKEN_EXPIRE = 30 * 60;     // 30 分钟
    private static final long REFRESH_TOKEN_EXPIRE = 7 * 24 * 3600; // 7 天

    private final JwtUtil jwtUtil;
    private final RedisTemplate redisTemplate;

    // 登录时生成双 Token
    public TokenPair generateTokenPair(Long userId, String deviceFingerprint) {
        String accessToken = jwtUtil.generate(userId, ACCESS_TOKEN_EXPIRE);
        String refreshToken = jwtUtil.generate(
            userId + ":refresh", REFRESH_TOKEN_EXPIRE
        );

        // 存入 Redis
        redisTemplate.opsForValue().set(
            "token:" + userId,
            accessToken,
            ACCESS_TOKEN_EXPIRE, TimeUnit.SECONDS
        );
        redisTemplate.opsForValue().set(
            "refresh:" + userId,
            refreshToken + ":" + deviceFingerprint,
            REFRESH_TOKEN_EXPIRE, TimeUnit.SECONDS
        );

        return new TokenPair(accessToken, refreshToken);
    }

    // 刷新 Token(Token 旋转)
    public TokenPair refresh(String refreshToken, String deviceFingerprint) {
        // 1. 校验 Refresh Token
        Long userId = jwtUtil.parseUserId(refreshToken);
        String stored = redisTemplate.opsForValue()
            .get("refresh:" + userId);

        if (stored == null) {
            throw new AuthException("Refresh Token 已失效");
        }

        // 2. 设备指纹校验
        String storedFingerprint = stored.split(":")[1];
        if (!storedFingerprint.equals(deviceFingerprint)) {
            // 设备不一致,告警
            alertService.alertAbnormalLogin(userId);
            throw new AuthException("设备不一致");
        }

        // 3. 旧 Refresh Token 立即失效(防止重放)
        redisTemplate.delete("refresh:" + userId);

        // 4. 生成新 Token 对
        return generateTokenPair(userId, deviceFingerprint);
    }

    // 强制下线
    public void forceLogout(Long userId) {
        redisTemplate.delete("token:" + userId);
        redisTemplate.delete("refresh:" + userId);
    }
}
3. 面试回答要点
  • 展示完整的 Token 生命周期管理
  • 体现安全性:Token 旋转、设备绑定、异常告警
  • 说明权衡:为什么用双令牌而不是单令牌
  • 提及扩展:如何支持多设备登录、单点登录(SSO)

JWT鉴权流程是怎样的?

原始问法:

    1. JWT鉴权流程是怎样的?

来源题目:SRC-16-156-513

面试先答

**(虚构示例,需替换)**JWT(JSON Web Token)鉴权流程分为 五步第一步:用户登录 → 客户端发送用户名密码到 /api/auth/login第二步:服务端验证 → 校验密码,生成 JWT(包含用户 ID、角色、过期时间等声明);第三步:返回 Token → 服务端返回 JWT(Header.Payload.Signature 三段式),客户端存储在 localStorage 或 Cookie;第四步:携带 Token 请求 → 客户端每次请求在 HTTP Header 中携带 Authorization: Bearer <JWT>第五步:服务端校验 → 解析 JWT、验证签名、检查过期时间、提取用户信息进行权限判断。我们项目中使用 JJWT 库 实现,配合 Redis 存储 Token 版本号 支持强制下线,Access Token 有效期 30 分钟,Refresh Token 7 天。

核心结论
  • JWT 五步流程:登录→验证→返回→携带→校验
  • JWT 结构:Header(算法类型).Payload(声明数据).Signature(签名)
  • 安全增强:Token 版本号、黑名单、Refresh Token 旋转
1. JWT 结构详解
JWT Token 结构:
┌─────────────────────────────────────────────────┐
│ Header.Payload.Signature                        │
├─────────────────────────────────────────────────┤
│                                                  │
│ Header(Header):                                │
│ {                                                │
│   "alg": "HS256",    // 签名算法                │
│   "typ": "JWT"       // Token 类型              │
│ }                                                │
│                                                  │
│ Payload(载荷):                                 │
│ {                                                │
│   "sub": "1234567890",  // 主题(用户ID)        │
│   "name": "John Doe",   // 用户名               │
│   "iat": 1516239022,    // 签发时间             │
│   "exp": 1516240000     // 过期时间             │
│ }                                                │
│                                                  │
│ Signature(签名):                              │
│ HMACSHA256(                                     │
│   base64(Header) + "." + base64(Payload),       │
│   secretKey                                     │
│ )                                                │
│                                                  │
└─────────────────────────────────────────────────┘
2. 完整鉴权流程(虚构示例)
客户端                          服务端
  │                              │
  │ 1. POST /api/auth/login      │
  │    {username, password}      │
  │─────────────────────────────→│
  │                              │
  │                              │ 2. 验证用户名密码
  │                              │    - 查询用户
  │                              │    - BCrypt 校验密码
  │                              │    - 检查账号状态
  │                              │
  │                              │ 3. 生成 JWT
  │                              │    - Header: {"alg":"HS256","typ":"JWT"}
  │                              │    - Payload: {sub, userId, role, exp}
  │                              │    - Signature: HMACSHA256(header+payload)
  │                              │
  │ 4. 返回 Token                │
  │    {accessToken, refreshToken}│
  │←─────────────────────────────│
  │                              │
  │ 5. GET /api/orders           │
  │    Authorization: Bearer <JWT>│
  │─────────────────────────────→│
  │                              │
  │                              │ 6. 校验 JWT
  │                              │    - 解析 Header.Payload.Signature
  │                              │    - 验证签名(确保未篡改)
  │                              │    - 检查 exp 是否过期
  │                              │    - 检查 Redis 黑名单
  │                              │    - 提取 userId、role
  │                              │
  │                              │ 7. 权限校验
  │                              │    - 检查角色是否有权访问
  │                              │    - 数据权限过滤
  │                              │
  │ 8. 返回数据                  │
  │←─────────────────────────────│
3. 核心代码实现(虚构示例)
// JWT 工具类
@Component
public class JwtUtil {

    private static final String SECRET_KEY = "your-256-bit-secret-key-here";
    private static final long ACCESS_TOKEN_EXPIRE = 30 * 60;     // 30 分钟
    private static final long REFRESH_TOKEN_EXPIRE = 7 * 24 * 3600; // 7 天

    // 生成 Token
    public String generateAccessToken(Long userId, String role) {
        return Jwts.builder()
            .setHeaderParam("alg", "HS256")
            .setHeaderParam("typ", "JWT")
            .setSubject(String.valueOf(userId))
            .claim("role", role)
            .claim("type", "access")
            .setIssuedAt(new Date())
            .setExpiration(new Date(System.currentTimeMillis()
                + ACCESS_TOKEN_EXPIRE * 1000))
            .signWith(Keys.hmacShaKeyFor(SECRET_KEY.getBytes()),
                SignatureAlgorithm.HS256)
            .compact();
    }

    // 解析和验证 Token
    public Claims parseToken(String token) {
        return Jwts.parserBuilder()
            .setSigningKey(Keys.hmacShaKeyFor(SECRET_KEY.getBytes()))
            .build()
            .parseClaimsJws(token)
            .getBody();
    }

    // 验证 Token 并提取用户信息
    public UserInfo validateAndGetUser(String token) {
        Claims claims = parseToken(token);

        // 检查过期
        if (claims.getExpiration().before(new Date())) {
            throw new JwtException("Token 已过期");
        }

        return new UserInfo(
            Long.parseLong(claims.getSubject()),
            claims.get("role", String.class),
            claims.get("type", String.class)
        );
    }
}

JWT 过滤器

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final JwtUtil jwtUtil;
    private final UserDetailsService userDetailsService;

    @Override
    protected void doFilterInternal(HttpServletRequest request,
            HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {

        String authHeader = request.getHeader("Authorization");

        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
            filterChain.doFilter(request, response);
            return;
        }

        String token = authHeader.substring(7);

        try {
            UserInfo userInfo = jwtUtil.validateAndGetUser(token);

            UsernamePasswordAuthenticationToken auth =
                new UsernamePasswordAuthenticationToken(
                    userInfo, null,
                    getAuthorities(userInfo.getRole())
                );

            SecurityContextHolder.getContext().setAuthentication(auth);

        } catch (JwtException e) {
            // Token 无效,返回 401
            response.setStatus(HttpStatus.UNAUTHORIZED.value());
            response.setContentType("application/json");
            response.getWriter().write(
                "{\"error\":\"Invalid or expired token\"}"
            );
            return;
        }

        filterChain.doFilter(request, response);
    }
}
4. 安全增强(虚构示例)
// JWT 黑名单 + Token 版本号
@Service
public class JwtSecurityService {

    private final RedisTemplate redisTemplate;

    // 强制下线:将 Token 加入黑名单
    public void blacklistToken(String token, long remainingTime) {
        redisTemplate.opsForValue().set(
            "jwt:blacklist:" + token,
            "1",
            remainingTime, TimeUnit.SECONDS
        );
    }

    // 检查 Token 是否在黑名单
    public boolean isBlacklisted(String token) {
        return Boolean.TRUE.equals(
            redisTemplate.hasKey("jwt:blacklist:" + token)
        );
    }

    // Token 版本号:支持修改密码后所有 Token 失效
    public void incrementTokenVersion(Long userId) {
        String key = "jwt:version:" + userId;
        redisTemplate.opsForValue().increment(key);
    }

    // 验证 Token 版本号
    public boolean verifyTokenVersion(Long userId, long tokenVersion) {
        String key = "jwt:version:" + userId;
        String currentVersion = redisTemplate.opsForValue().get(key);
        if (currentVersion == null) return true; // 无版本号,允许访问
        return Long.parseLong(currentVersion) == tokenVersion;
    }
}
5. 常见追问
  • 追问 1:JWT 和 Session 的区别?
    • JWT 是无状态的,服务端不需要存储;Session 是有状态的,服务端需要存储。JWT 适合分布式,Session 适合传统单体
  • 追问 2:JWT 的缺点是什么?
    • 无法主动失效(需配合黑名单)、Payload 信息不宜过大、需要 HTTPS 传输
  • 追问 3:如何防止 JWT 被窃取?
    • HTTPS 加密传输、HttpOnly Cookie 存储、短有效期、绑定设备指纹
6. 面试回答要点
  • 展示完整流程:从登录到鉴权的每一步
  • 体现技术细节:JWT 三段结构、签名算法、代码实现
  • 关注安全性:黑名单、版本号、设备绑定等增强措施
  • 说明项目实践:结合项目说明具体的配置和策略
一句话总结

JWT 鉴权通过登录验证→生成 Token→携带 Token 请求→服务端校验的流程实现无状态认证,配合黑名单和版本号等增强机制保证安全性,适用于分布式系统。


推荐文章

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