大厂面试实录:Spring Boot源码深度解析+Redis缓存架构+RAG智能检索,谢飞机的AI电商面试之旅

大厂面试实录:Spring Boot源码深度解析+Redis缓存架构+RAG智能检索,谢飞机的AI电商面试之旅

前言

又是一年招聘季,某知名电商平台正在招聘高级Java开发工程师。谢飞机,一名自称"全栈工程师"的程序员,今天来到了面试现场。面试官是一位技术深厚的架构师,让我们来看看这场面试会发生什么有趣的故事...


第一轮:Spring Boot启动机制与自动配置基础

面试官:你好,谢飞机。先请你自我介绍一下,然后说说你理解的Spring Boot启动流程。

谢飞机:(挠挠头)您好!我啊,有三年的Java开发经验,精通Spring全家桶,做过电商、支付、社交等多个项目。至于Spring Boot启动嘛,就是有个@SpringBootApplication注解,然后run方法一跑,容器就启动了呗,很简单!

面试官:(微微点头)嗯,基础概念清楚。那我再问几个问题:

  1. @SpringBootApplication这个复合注解具体包含哪三个注解?各自的作用是什么?
  2. Spring Boot的自动配置是如何工作的?从源码角度讲讲@EnableAutoConfiguration的实现原理?
  3. 你在电商项目中如何使用Spring Boot的配置管理?多环境配置怎么实现的?

谢飞机:(有些紧张)三个注解嘛...@ComponentScan肯定有,还有...@SpringBootConfiguration,这个对吧?至于@EnableAutoConfiguration,它应该是通过读取spring.factories文件,然后加载一堆自动配置类...具体的源码实现,这个...

面试官:(温和地)前两个回答得不错,看来基础还在。第三个问题你提到了spring.factories文件,但Spring Boot 2.7以后这个文件的读取方式有变化,你知道吗?

谢飞机:(尴尬笑)变化?这个...应该还是读取配置文件吧,可能路径变了?

面试官:没关系,这算是个细节。我们继续下一部分。


第二轮:Redis缓存架构与分布式场景应用

面试官:你们电商平台在秒杀场景下是如何做缓存的?说说Redis在其中的应用。

谢飞机:(眼睛一亮)这个我熟!秒杀我们用Redis做库存预热,提前把库存加载到Redis里,然后用Lua脚本做原子扣减,防止超卖!

面试官:(满意点头)很好!那我深入问几个问题:

  1. Redis的String类型底层实现是什么?SDS和C语言的字符串有什么区别?

  2. 在秒杀场景下,Redis集群如何保证数据一致性?如果Redis主节点宕机,如何保证数据不丢失?

  3. 你们有没有遇到过缓存穿透、缓存击穿、缓存雪崩问题?怎么解决的?从源码角度分析Redis的过期策略。

谢飞机:(自信)缓存穿透我们用布隆过滤器,缓存击穿用互斥锁,缓存雪崩我们给过期时间加了随机值!至于Redis底层实现...String应该是用char数组吧?SDS...这个好像是什么简单动态字符串?

面试官:SDS全称是Simple Dynamic String,它比C字符串多了len和free属性,可以O(1)获取长度,并且避免缓冲区溢出。那Redis主从同步和持久化机制了解吗?

谢飞机:(开始冒汗)主从同步...大概是从节点发送SYNC命令,主节点生成RDB文件传过去...持久化就是RDB和AOF,RDB快一点,AOF数据更安全...

面试官:说得对,但不深入。我们换个方向,聊聊现在很火的AI技术吧。


第三轮:RAG检索增强生成与AI应用架构

面试官:我们平台最近在开发智能客服系统,基于RAG技术。你对RAG了解多少?

谢飞机:(愣住)RAG?R...Retrieval Augmented Generation?检索增强生成?我听过,大概是用向量数据库检索相关文档,然后用大模型生成回答...但具体怎么实现,没深入研究过。

面试官:理解基本概念。那我详细问几个问题:

  1. RAG系统的完整技术架构是怎样的?包括文档加载、文本分割、向量化、检索、生成等环节,每个环节的核心技术是什么?

  2. 向量数据库(如Milvus)的底层索引结构有哪些?HNSW索引和IVF索引的区别是什么?在什么场景下选择哪种?

  3. 如何解决RAG中的"幻觉"问题?从检索准确性和生成质量两个角度谈谈你的方案。

  4. Spring AI框架提供了哪些RAG相关的组件?如何与Milvus集成实现文档问答系统?

  5. 在企业文档问答场景下,如何处理大量PDF、Word等非结构化文档?文档向量化后如何支持多轮对话上下文记忆?

谢飞机:(彻底懵了)这个...向量数据库的索引...HNSW好像是基于图的?IVF是聚类的?至于幻觉...可以让模型回答更保守一点?Spring AI...我还没用过...

面试官:(温和地笑了)看来这个领域你需要加强学习了。AI确实是未来的方向,现在很多公司都在招懂AI应用开发的工程师。

谢飞机:(低下头)是的,我回去一定好好学习。

面试官:嗯,基础不错,Spring Boot和Redis这块理解还可以,但深度和源码层面的理解还需要加强。AI技术更是需要系统学习。今天的面试就到这里吧,你可以回去等通知了。

谢飞机:好的,谢谢面试官!再见!


答案详解

第一轮:Spring Boot启动机制与自动配置

1. @SpringBootApplication三个核心注解
java 复制代码
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
    excludeFilters = {@Filter(
    type = FilterType.CUSTOM,
    classes = {TypeExcludeFilter.class}
), @Filter(
    type = FilterType.CUSTOM,
    classes = {AutoConfigurationExcludeFilter.class}
)}
)
public @interface SpringBootApplication {
    // ...
}

@SpringBootConfiguration:标记为配置类,相当于@Configuration,允许在类中定义@Bean方法。

@EnableAutoConfiguration:开启自动配置,这是Spring Boot的核心。

@ComponentScan:组件扫描,默认扫描当前包及其子包下的所有@Component注解的类。

2. @EnableAutoConfiguration源码实现原理
java 复制代码
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
    String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
    Class<?>[] exclude() default {};
    String[] excludeName() default {};
}

核心原理

  1. @Import(AutoConfigurationImportSelector.class):导入AutoConfigurationImportSelector选择器

  2. AutoConfigurationImportSelector.selectImports()方法

java 复制代码
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
    if (!isEnabled(annotationMetadata)) {
        return NO_IMPORTS;
    }
    AutoConfigurationEntry autoConfigurationEntry = 
        getAutoConfigurationEntry(annotationMetadata);
    return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
  1. 加载自动配置类(Spring Boot 2.7+):
  • Spring Boot 2.7以前:读取META-INF/spring.factories文件
  • Spring Boot 2.7以后:读取META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
  1. 过滤机制:根据条件注解(@ConditionalOnClass、@ConditionalOnMissingBean等)筛选配置
3. 多环境配置实现

方式一:Profile文件

复制代码
application.properties          # 默认配置
application-dev.properties     # 开发环境
application-prod.properties    # 生产环境

方式二:YAML多文档

yaml 复制代码
spring:
  profiles:
    active: dev
---
spring:
  profiles: dev
  datasource:
    url: jdbc:mysql://localhost:3306/dev_db
---
spring:
  profiles: prod
  datasource:
    url: jdbc:mysql://prod-server:3306/prod_db

方式三:环境变量覆盖

java 复制代码
@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

第二轮:Redis缓存架构

1. String类型底层实现 - SDS
c 复制代码
struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t len;     // 已使用长度
    uint8_t alloc;   // 总分配空间
    unsigned char flags;  // 标志位
    char buf[];      // 字节数组
};

SDS vs C字符串区别

| 特性 | C字符串 | SDS | |------|---------|-----| | 获取长度 | O(n) | O(1) | | 缓冲区溢出 | 易发生 | 自动检查扩容 | | 修改次数 | 每次修改都需要重新分配内存 | 二进制安全,空间预分配 | | API安全性 | 不安全 | 安全 |

2. Redis集群数据一致性

主从复制流程

复制代码
1. 从节点发送SYNC命令
2. 主节点生成RDB快照文件
3. 主节点发送RDB文件给从节点
4. 主节点发送期间累积的写命令
5. 之后每秒发送命令增量

高可用方案 - Sentinel哨兵

yaml 复制代码
sentinel monitor mymaster 127.0.0.1 6379 2
sentinel down-after-milliseconds mymaster 30000
sentinel parallel-syncs mymaster 1
sentinel failover-timeout mymaster 180000

数据不丢失策略

  1. AOF持久化 + 每秒刷盘
  2. 主从复制 + 至少一个从节点
  3. Sentinel自动故障转移
3. 三大缓存问题解决方案

缓存穿透

java 复制代码
// 布隆过滤器实现
@Component
public class BloomFilterService {
    private BloomFilter<String> bloomFilter;
    
    @PostConstruct
    public void init() {
        bloomFilter = BloomFilter.create(
            Funnels.stringFunnel(Charset.defaultCharset()),
            1000000,
            0.01
        );
        // 预加载所有合法key
    }
    
    public boolean mightContain(String key) {
        return bloomFilter.mightContain(key);
    }
}

缓存击穿

java 复制代码
// 互斥锁实现
public String getWithLock(String key) {
    String value = redis.get(key);
    if (value == null) {
        String lockKey = "lock:" + key;
        try {
            // 尝试获取分布式锁
            if (redis.setnx(lockKey, "1", 10)) {
                value = db.query(key);
                redis.setex(key, value, 3600);
            } else {
                // 等待并重试
                Thread.sleep(100);
                return getWithLock(key);
            }
        } finally {
            redis.del(lockKey);
        }
    }
    return value;
}

缓存雪崩

java 复制代码
// 随机过期时间
public void setWithRandomExpire(String key, String value) {
    int baseExpire = 3600;
    int randomExpire = new Random().nextInt(600); // 0-600秒随机
    redis.setex(key, value, baseExpire + randomExpire);
}

Redis过期策略源码

c 复制代码
// 惰性删除 - 访问时检查
int expireIfNeeded(redisDb *db, robj *key) {
    if (!keyIsExpired(db, key)) return 0;
    if (server.lazyfree_lazy_expire) {
        freeObjectIfNeeded(key);
    } else {
        deleteExpiredKeyAndPropagate(db, key);
    }
    return 1;
}

// 定期删除 - 每秒执行10次
void activeExpireCycle(int type) {
    // 每次随机检查20个key
    // 删除过期key
    // 如果删除超过25%则继续
}

第三轮:RAG检索增强生成

1. RAG系统完整架构
复制代码
┌─────────────────────────────────────────────────────────────┐
│                        RAG系统架构                           │
├─────────────────────────────────────────────────────────────┤
│  文档层              │  处理层              │  检索层        │
│  ┌──────────┐       │  ┌──────────┐        │  ┌──────────┐  │
│  │  PDF     │ ────▶ │  │ 分割器   │ ─────▶ │  │ 向量化   │  │
│  │  Word    │       │  │ (Chunk)  │        │  │ Embedding│  │
│  │  Markdown│       │  └──────────┘        │  └──────────┘  │
│  └──────────┘       │  ┌──────────┐        │       │        │
│                     │  │ 清洗器   │        │       ▼        │
│                     │  └──────────┘        │  ┌──────────┐  │
│                     │                      │  │Milvus/Pine│  │
│  ┌──────────┐       │                      │  │  cone    │  │
│  │  用户    │       │                      │  └──────────┘  │
│  │  问题    │ ────▶ │  ┌──────────┐        │       │        │
│  └──────────┘       │  │ 问题向量化│ ──────┼───────┘        │
│                     │  └──────────┘        │                │
│                     │       │              │       ▼        │
│                     │       └──────────────▶│  相似度检索    │
├────────────────────┼───────────────────────┼────────────────┤
│  生成层              │  优化层               │  应用层        │
│  ┌──────────┐       │  ┌──────────┐         │  ┌──────────┐  │
│  │ LLM      │ ◀─────│  │ 重排序   │         │  │ 智能客服 │  │
│  │ GPT-4    │       │  │ Rerank   │         │  │ 企业问答 │  │
│  │ Claude   │       │  └──────────┘         │  └──────────┘  │
│  └──────────┘       │  ┌──────────┐         │                │
│        │           │  │上下文记忆│         │                │
│        └───────────▶│  │ChatMemory│         │                │
│                     │  └──────────┘         │                │
└─────────────────────┴───────────────────────┴────────────────┘
2. 向量数据库索引对比

HNSW(Hierarchical Navigable Small World)

python 复制代码
# 基于图的索引结构
# 时间复杂度:构建O(n log n),查询O(log n)
# 特点:高召回率,但内存占用大

index_params = {
    "metric_type": "COSINE",
    "index_type": "HNSW",
    "params": {
        "M": 16,              # 每个节点的最大连接数
        "efConstruction": 256 # 构建时的搜索宽度
    }
}

IVF(Inverted File Index)

python 复制代码
# 基于聚类的倒排索引
# 时间复杂度:构建O(nk),查询O(√n)
# 特点:内存占用小,但召回率略低

index_params = {
    "metric_type": "COSINE",
    "index_type": "IVF_FLAT",
    "params": {
        "nlist": 1024  # 聚类中心数量
    }
}

| 索引类型 | 构建速度 | 查询速度 | 内存占用 | 适用场景 | |---------|---------|---------|---------|---------| | HNSW | 慢 | 快 | 大 | 高精度要求场景 | | IVF | 快 | 中 | 小 | 大规模数据场景 | | FLAT | 快 | 慢 | 最大 | 小规模精确检索 |

3. RAG幻觉问题解决方案

检索层面优化

java 复制代码
@Service
public class RAGRetrievalService {
    
    // 多路召回
    public List<Document> multiPathRetrieve(String query) {
        // 1. 语义检索
        List<Document> semanticResults = vectorSearch(query);
        
        // 2. 关键词检索
        List<Document> keywordResults = keywordSearch(query);
        
        // 3. 混合检索
        return hybridMerge(semanticResults, keywordResults);
    }
    
    // 重排序
    public List<Document> rerank(String query, List<Document> documents) {
        // 使用Cross-Encoder进行精细排序
        documents.sort((d1, d2) -> {
            double score1 = crossEncoder.score(query, d1.content);
            double score2 = crossEncoder.score(query, d2.content);
            return Double.compare(score2, score1);
        });
        return documents;
    }
}

生成层面优化

java 复制代码
@Configuration
public class RAGGenerationConfig {
    
    @Bean
    public ChatClient ragChatClient(ChatModel chatModel) {
        return ChatClient.builder(chatModel)
            .defaultSystem("""
                你是一个专业的客服助手。请基于提供的上下文信息回答问题。
                
                重要规则:
                1. 如果上下文中没有答案,请明确说明"根据现有信息无法回答"
                2. 不要编造或猜测信息
                3. 回答时请引用相关的上下文来源
                4. 对不确定的信息使用"可能"、"据我所知"等措辞
                """)
            .defaultAdvisors(
                new QuestionAnswerAdvisor(vectorStore),
                new MessageChatMemoryAdvisor(chatMemory)
            )
            .build();
    }
}
4. Spring AI + Milvus集成
java 复制代码
@Configuration
public class MilvusVectorStoreConfig {
    
    @Bean
    public MilvusServiceClient milvusClient() {
        return new MilvusServiceClient(
            ConnectParam.newBuilder()
                .withHost("localhost")
                .withPort(19530)
                .build()
        );
    }
    
    @Bean
    public VectorStore vectorStore(
            MilvusServiceClient client, 
            EmbeddingModel embeddingModel) {
        
        return new MilvusVectorStore(
            client, 
            embeddingModel,
            MilvusVectorStoreConfig.builder()
                .withCollectionName("documents")
                .withDimension(1536)  // OpenAI embedding维度
                .build()
        );
    }
    
    @Bean
    public ChatClient ragChatClient(
            ChatModel chatModel, 
            VectorStore vectorStore) {
        
        return ChatClient.builder(chatModel)
            .defaultAdvisors(
                new QuestionAnswerAdvisor(vectorStore),
                new PromptChatMemoryAdvisor(chatMemory)
            )
            .build();
    }
}

// 文档加载与向量化
@Service
public class DocumentIngestionService {
    
    @Autowired
    private VectorStore vectorStore;
    
    public void ingestDocuments(String path) {
        // 1. 加载文档
        List<Document> documents = TextReader.from(path)
            .read();
        
        // 2. 分割文档
        TokenTextSplitter splitter = new TokenTextSplitter(
            500,  // chunk大小
            50,   // overlap大小
            5,    // 最小块数
            10000 // 最大块大小
        );
        
        List<Document> chunks = splitter.apply(documents);
        
        // 3. 存储到向量数据库
        vectorStore.add(chunks);
    }
}
5. 企业文档问答系统实现

文档处理流程

java 复制代码
@Service
public class EnterpriseDocumentQAService {
    
    // 支持多种文档格式
    public void processEnterpriseDocs(List<File> files) {
        for (File file : files) {
            String extension = getFileExtension(file);
            
            switch (extension) {
                case "pdf":
                    processPDF(file);
                    break;
                case "docx":
                    processWord(file);
                    break;
                case "md":
                    processMarkdown(file);
                    break;
                default:
                    processText(file);
            }
        }
    }
    
    private void processPDF(File file) {
        // 使用Apache PDFBox提取文本
        PDDocument document = PDDocument.load(file);
        PDFTextStripper stripper = new PDFTextStripper();
        String text = stripper.getText(document);
        
        // 分块处理
        List<Document> chunks = smartChunking(text);
        
        // 添加元数据
        chunks.forEach(chunk -> {
            chunk.getMetadata().put("source", file.getName());
            chunk.getMetadata().put("type", "PDF");
            chunk.getMetadata().put("page", extractPage(chunk));
        });
        
        // 向量化存储
        vectorStore.add(chunks);
        document.close();
    }
    
    // 智能分块 - 识别段落边界
    private List<Document> smartChunking(String text) {
        List<Document> chunks = new ArrayList<>();
        
        // 按段落分割
        String[] paragraphs = text.split("\n\s*\n");
        
        StringBuilder currentChunk = new StringBuilder();
        for (String para : paragraphs) {
            if (currentChunk.length() + para.length() > 500) {
                chunks.add(new Document(currentChunk.toString()));
                currentChunk = new StringBuilder(para);
            } else {
                currentChunk.append("\n\n").append(para);
            }
        }
        
        if (currentChunk.length() > 0) {
            chunks.add(new Document(currentChunk.toString()));
        }
        
        return chunks;
    }
}

多轮对话记忆

java 复制代码
@Service
public class ConversationalQAService {
    
    @Autowired
    private ChatMemory chatMemory;
    
    @Autowired
    private ChatClient chatClient;
    
    public String answer(String userId, String question) {
        // 1. 获取用户历史对话
        List<Message> history = chatMemory.get(userId);
        
        // 2. 构建提示词
        Prompt prompt = Prompt.builder()
            .user(question)
            .messages(history)
            .build();
        
        // 3. 生成回答
        String response = chatClient.call(prompt)
            .content();
        
        // 4. 更新对话记忆
        chatMemory.add(userId, new UserMessage(question));
        chatMemory.add(userId, new AssistantMessage(response));
        
        return response;
    }
}

// Redis实现的对话记忆
@Component
public class RedisChatMemory implements ChatMemory {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    private static final String KEY_PREFIX = "chat:memory:";
    
    @Override
    public void add(String conversationId, Message message) {
        String key = KEY_PREFIX + conversationId;
        redisTemplate.opsForList().rightPush(key, message);
        redisTemplate.expire(key, 1, TimeUnit.HOURS);
    }
    
    @Override
    public List<Message> get(String conversationId) {
        String key = KEY_PREFIX + conversationId;
        return (List<Message>) redisTemplate.opsForList().range(key, 0, -1);
    }
}

总结

通过这次面试,我们可以看到:

  1. Spring Boot:不仅要会用,还要理解自动配置、启动流程等底层原理
  2. Redis:从数据结构到底层实现,从单机到集群,从应用到源码,需要全面掌握
  3. AI技术:RAG、向量数据库、大模型应用是未来趋势,需要系统学习

给求职者的建议

  • 基础要扎实,源码要阅读
  • 理论结合实践,多写代码
  • 关注前沿技术,持续学习
  • 面试时诚实回答,不会就说不知道,但要有学习的态度

希望这篇文章能帮助到正在准备面试的同学们!加油!


参考文档

相关推荐
小猪配偶儿_oaken1 天前
SpringBoot实现单号生成功能(Java&若依)
java·spring boot·okhttp
KawYang1 天前
Spring Boot 使用 PropertiesLauncher + loader.path 实现外部 Jar 扩展启动
spring boot·后端·jar
用户8307196840821 天前
揭秘 Spring Boot 事务:动态增强的底层实现与核心组件
spring boot
DemonAvenger1 天前
深入Redis Stream:打造高效消息队列系统的实战指南
数据库·redis·性能优化
计算机程序设计小李同学1 天前
汽车4S店管理系统设计与实现
前端·spring boot·学习
laplace01231 天前
Part2.大模型RAG进阶多格式文档解析
笔记·语言模型·langchain·rag
钱多多_qdd1 天前
springboot注解(四)
java·spring boot·后端
Coder_Boy_1 天前
基于SpringAI的智能推荐影视平台设计和业务思路
大数据·人工智能·spring boot·spring cloud·langchain
姓蔡小朋友1 天前
Redisson
redis