场景题思考

实战

Posted by Ekko on November 8, 2025

[TOC]


百万级EXCEL读取

1、EasyExcel

基于POI的事件驱动模型,逐行解析,内存占用低

2、Apache POI (SAX模式)

事件驱动、流式读取

3、逐行读取后,及时批量处理,及时释放,因为等完全读取完整个excel的时候,内存也会爆炸


思考一:支持多线程读吗

理论上不支持,核心限制:EasyExcel的读取是顺序流式解析,XML解析器必须按顺序读取文件内容

其他场景可以考虑多线程,比如 多个 sheet 页面、读取出来后的数据处理


思考二:excel文件本身就很大,二进制流文件怎么处理的?内存不会溢出吗

EasyExcel:不加载整个XML到内存。边读边解析,遇到标签触发事件

1
2
3
4
5
6
7
8
9
物理文件 (磁盘)(文件流)
ZIP解压流 (xlsx本质是ZIP)(XML流)  
XML解析器 (SAX)(事件)
EasyExcel监听器
↓ (对象)
你的业务处理

思考三:excel不是本地文件,而是通过http请求过来的,内存如何处理

1
2
3
4
5
// 整个文件加载到内存! 不推荐
byte[] bytes = file.getBytes();

// 直接使用文件流,不加载到内存
InputStream inputStream = file.getInputStream();

有一个POST接口,接收一个MultipartFile(实际上Spring MVC在处理MultipartFile时,如果文件超过一定大小,会临时存储到磁盘,但可以通过配置使其不存盘,直接流式读取)

方式一:使用MultipartFile,但Spring默认会使用临时文件,所以对于大文件,我们确保临时文件存在,然后EasyExcel从临时文件读取,这样也不会内存溢出,但会有磁盘IO

方式二:完全流式解析,不保存到临时文件。我们可以通过自定义解析HttpServletRequest来实现,但这样就需要自己处理multipart/form-data的解析。不过,Spring提供了Streaming API

实际上,Spring MVC的MultipartFile接口默认实现是将文件存储到临时文件,所以我们可以直接使用MultipartFile,然后通过EasyExcel读取这个临时文件。但是,如果不想落盘,我们也可以直接读取MultipartFile的InputStream,因为MultipartFile的InputStream也是从临时文件读取的,所以不会内存溢出


思考四:反过来,java输出excel文件,文件太大,如何处理

不推荐:POI XSSFWorkbook

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// ❌ 危险:使用POI的XSSFWorkbook
@GetMapping("/export-oom")
public void exportOOM(HttpServletResponse response) {
    Workbook workbook = new XSSFWorkbook(); // 在内存中构建整个Excel
    Sheet sheet = workbook.createSheet("数据");
    
    // 写入100万行数据
    for (int i = 0; i < 1000000; i++) {
        Row row = sheet.createRow(i);
        for (int j = 0; j < 20; j++) {
            row.createCell(j).setCellValue("数据行-" + i + "-列-" + j);
        }
        // 每写入一行,内存占用就增加一点,直到OOM
    }
    
    // 永远执行不到这里...
    response.setContentType("application/vnd.ms-excel");
    workbook.write(response.getOutputStream());
}

推荐 POI SXSSF

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
@GetMapping("/export-safe")
public void exportSafe(HttpServletResponse response) throws IOException {
    response.setContentType("application/vnd.ms-excel");
    response.setHeader("Content-Disposition", "attachment; filename=large-file.xlsx");
    
    // 🎯 关键:使用SXSSFWorkbook,设置行内存窗口
    Workbook workbook = new SXSSFWorkbook(100); // 只在内存中保留100行
    Sheet sheet = workbook.createSheet("数据");
    
    try {
        // 写入表头
        Row headerRow = sheet.createRow(0);
        for (int i = 0; i < 20; i++) {
            headerRow.createCell(i).setCellValue("列头-" + i);
        }
        
        // 流式写入数据
        for (int rowNum = 1; rowNum <= 1000000; rowNum++) {
            Row row = sheet.createRow(rowNum);
            for (int colNum = 0; colNum < 20; colNum++) {
                row.createCell(colNum).setCellValue("数据-" + rowNum + "-" + colNum);
            }
            
            // 每1000行手动刷新一次(可选)
            if (rowNum % 1000 == 0) {
                ((SXSSFSheet) sheet).flushRows(100); // 刷新并保留100行在内存
            }
            
            // 进度跟踪
            if (rowNum % 10000 == 0) {
                log.info("已生成 {} 行数据", rowNum);
            }
        }
        
        workbook.write(response.getOutputStream());
        
    } finally {
        // 🎯 重要:清理临时文件
        if (workbook instanceof SXSSFWorkbook) {
            ((SXSSFWorkbook) workbook).dispose();
        }
        workbook.close();
    }
}

接口防刷

1、 分布式限流

redis + lua 脚本 实现计数器

令牌桶、固定窗口、滑动窗口

2、 设备指纹识别

3、 请求签名

4、 ip 黑名单


nacos宕机了,本地是否有缓存这个服务列表,是否还能正常远程调用

总结:只要服务生产者和消费者没有发生任何变动,即使 Nacos 一直宕机,服务之间的调用也完全正常

服务发现 (Naming) : 服务列表 (ServiceInfo) : nacos/naming 目录下 : 服务实例信息缓存与容灾

flowchart TD
    A[服务列表查询请求] --> B{内存缓存是否存在且有效?}
    B -- 是 --> C[直接使用内存缓存]
    B -- 否 --> D[查询Nacos服务器]
    D --> E{服务器查询成功?}
    E -- 是 --> F[更新内存与本地快照]
    E -- 否 --> G[读取本地快照文件]
    F --> C
    G --> C
    C --> H[返回服务列表]
    
    I[Nacos服务器] -- 推送变更 --> F

配置管理 (Config) : 配置参数 (如.properties, .yaml) : nacos/config 目录下 : 配置内容缓存与容灾

配置管理,Nacos客户端在获取配置时,通常会遵循 本地容灾文件 -> 本地快照文件 -> 内存缓存 -> Nacos服务器 的查询顺序,以最大程度保证自身的可用性


配置管理: 更侧重于数据可靠性和最终一致性。它会优先尝试读取你可能预先配置好的、非常重要的容灾文件,以防止配置完全丢失

服务列表: 查询则更侧重于速度和对服务状态变化的敏感度


Openfeign如何使用,A服务调用B服务发生了哪些事

总结:代理实现,拦截器帮助构造调用模版,发送http请求

flowchart TD
    A[A服务业务代码] --> B[Feign客户端接口调用]
    B --> C[Feign动态代理]
    C --> D[MethodHandler处理]
    D --> E[请求模板创建]
    E --> F[编码器处理参数]
    F --> G[服务发现与负载均衡]
    G --> H{使用服务发现?}
    H -- 是 --> I[从注册中心获取实例]
    H -- 否 --> J[使用直接URL]
    I --> K[负载均衡选择实例]
    J --> L[构造完整URL]
    K --> L
    L --> M[HTTP客户端发送请求]
    M --> N[服务B接收请求]
    N --> O[服务B处理业务]
    O --> P[服务B返回响应]
    P --> Q[HTTP客户端接收响应]
    Q --> R{响应状态}
    R -- 2xx成功 --> S[解码器处理响应]
    R -- 4xx/5xx错误 --> T[错误解码器处理]
    S --> U[返回结果对象]
    T --> V[抛出异常]
    U --> W[A服务继续执行]
    V --> W

加密后的数据如何模糊查询

类型 特点 常见算法
可逆加密 加密后可解密还原 AES、DES、RSA、SM4
不可逆加密 加密后不可还原 MD5、SHA、bcrypt、SM3

可逆加密算法(对称/非对称)

对称:加密解密使用相同密钥:AES、DES、SM4 非对称:公钥加密,私钥解密:RSA、ECC


一般采用 分词模糊搜索

注意:原数据按规则分词加密存储后,查询条件入参同样也要先分词,再去搜索

还一种思路:明文、密文 做映射表,但这样也失去了加密的初衷。但如果密文映射表,是独立的服务和数据库,从数据库权限上收口,也不是不行