Java实战面试题(一)

目录

[1. 怎么优化慢 SQL?](#1. 怎么优化慢 SQL?)

[2. 大文件分片上传](#2. 大文件分片上传)

[3. 消息队列发送一条消息 A 到 B 怎么阻塞它?](#3. 消息队列发送一条消息 A 到 B 怎么阻塞它?)

[4. 怎么解决缓存数据一致性?](#4. 怎么解决缓存数据一致性?)

[5. 跨域问题怎么解决?](#5. 跨域问题怎么解决?)

[6. 线程池了解吗?](#6. 线程池了解吗?)

[7. 延迟双删怎么实现的?](#7. 延迟双删怎么实现的?)

[8. RabbitMQ 执行流程](#8. RabbitMQ 执行流程)

[9. 分布式事务怎么实现的?](#9. 分布式事务怎么实现的?)


1. 怎么优化慢 SQL?

回答思路:定位慢 SQL → 分析执行计划 → 针对性优化。

  • 开启慢查询日志 :设置 long_query_timeslow_query_log,找出耗时 SQL。

  • 使用 EXPLAIN 分析执行计划 :重点关注 type(至少达到 range,最好 ref/const)、rowsExtra(避免 Using filesortUsing temporary)。

  • 常见优化手段

    • 索引优化:为 WHERE、ORDER BY、GROUP BY、JOIN 的字段建索引,遵循最左前缀原则,避免索引失效(如对列进行函数操作、隐式类型转换)。

    • SQL 改写 :用 JOIN 替代子查询;分页时用游标分页替代深分页 OFFSET;避免 SELECT *;将大事务拆小。

    • 表结构优化:字段选择合适类型,垂直/水平分表,适当冗余减少关联。

    • 数据库参数调优:调整 InnoDB Buffer Pool 大小,合理设置连接数。

  • 兜底方案:读写分离、缓存、ES 异构等。


2. 大文件分片上传

实现原理:前端将文件切成多个小块(Chunk),按序上传,后端校验、暂存,全部传完后合并。

关键步骤

  1. 前端 :用 File.slice() 切块,每片带上 fileIdchunkIndextotalChunkschunkHash

  2. 后端接收 :校验 fileId 对应的文件元数据,将分片存临时目录(如以 fileId/chunkIndex 命名)。

  3. 合并 :当接收到的分片数量等于 totalChunks 时,按索引顺序合并为一个完整文件,并校验 MD5。

  4. 优化点

    • 秒传:上传前先传文件 MD5,后端若已存在则直接返回成功。

    • 断点续传:客户端询问后端已接收的分片索引列表,只上传缺失的分片。

    • 并发控制:前端可并行上传数片,但后端要保证线程安全的计数和存储。

简历话术:可描述为"基于分片上传与 MD5 校验实现文件断点续传与秒传功能,将大文件上传成功率从 X% 提升至 Y%"。


3. 消息队列发送一条消息 A 到 B 怎么阻塞它?

这道题的核心是如何让消费者在收到消息 A 后阻塞,直到 B 消息到达,实现两条消息的"等待-继续"模式。下面用 RabbitMQ 举例,但思路通用。

解法1:服务端伪延迟消息(推荐)

  • 消息 A 被消费时,检查关联 B 是否就绪(比如查 Redis/DB 的状态)。如果 B 未到,则不确认消息 A ,而是将其重新投递(basicNackrequeue=true),并让消费者短暂休眠后重试。直到 B 已处理完,再确认 A 并继续。

  • 更优雅的做法:A 到达时,若 B 未到,将 A 的 payload 存入 Redis,然后确认 A(移出队列)。当 B 被消费并处理完毕后,主动触发 A 的后续逻辑(或发送另一个通知消息)。

解法2:客户端阻塞器(CountDownLatch)

  • 消费者本地维护一个 ConcurrentHashMap<CorrelationId, CountDownLatch>

  • 消费 A 时,发现 B 未到,创建一个 CountDownLatch(1) 并存入 Map,然后 await(timeout)。消费 B 的线程在处理完 B 后,根据关联 ID 获取对应的 Latch 并 countDown(),唤醒 A 的消费线程。

  • 注意:要求 A 和 B 的消费在同一应用内,且顺序敏感,否则跨实例无法协作。

解法3:Redis 分布式锁/状态标记

  • A 来了,设置 key-A-{orderId} 状态为 WAITING_B,然后循环查询 key-B-{orderId} 状态(或使用 Redis 的发布订阅)。B 处理完后设置状态为 B_DONE,并通知 A。A 收到通知后继续执行。

4. 怎么解决缓存数据一致性?

核心原则先更新数据库,再删除缓存。这是避免缓存旧数据最朴素的方案。

主要方案

  • Cache Aside(旁路缓存)

    • :先读缓存,命中则返回;未命中查数据库,写入缓存,返回。

    • :先更新数据库,然后删除缓存(不建议更新缓存,尤其复杂计算场景)。

    • 为何删除而不是更新:并发写时,更新缓存顺序不可控,可能造成缓存与 DB 永久不一致。删除缓存是懒加载,下次读时重建。

  • 为什么先操作 DB 再删缓存:若反过来,删缓存后、更新 DB 前有读请求,可能把旧数据写回缓存,造成脏数据。

  • 极端情况下的不一致 :DB 更新后,缓存删除前,有读请求并发读,会把旧数据写入缓存。针对小概率事件,可以用延迟双删:写完 DB 后删一次缓存,延迟几百毫秒再删一次,确保第二次能删掉并发读写入的旧数据。

  • 最终一致性方案

    • Canal + MQ 异步:监听 MySQL binlog,发送变更消息到 MQ,缓存服务消费后删除/更新缓存。

    • 缓存设置合理过期时间:兜底,即使有短暂不一致,过期后自动校准。


5. 跨域问题怎么解决?

本质 :浏览器的同源策略(协议、域名、端口三者相同)阻止了跨域请求。解决是从服务端或前端绕过该限制。

常见方案

  • CORS(跨域资源共享)------ 标准方案

    • 服务端在响应头添加 Access-Control-Allow-Origin(指定域名或 *)。

    • 复杂请求(如 Content-Type: application/json)会先发 OPTIONS 预检请求,服务端需响应 Access-Control-Allow-MethodsAccess-Control-Allow-Headers 等。

    • Spring Boot 可全局配置 CorsRegistry@CrossOrigin

  • 反向代理(Nginx)------ 生产最常用

    • 配置 Nginx 将 /api 请求代理到后端服务,域名和端口一致,浏览器视为同源。
  • JSONP :利用 <script> 标签无跨域限制,仅支持 GET,过时。

  • WebSocket:不受同源策略限制,但需服务端配合。

  • 其他 :iframe + postMessage、服务器间请求(后端代理)。


6. 线程池了解吗?

见前文 ThreadPoolExecutor 的详细解答。核心回答点:

  • 核心参数corePoolSizemaxPoolSizekeepAliveTimeworkQueuethreadFactoryhandler

  • 提交流程:核心 → 队列 → 最大线程 → 拒绝策略。

  • 拒绝策略AbortPolicy(抛异常)、CallerRunsPolicy(交给提交线程)、DiscardPolicyDiscardOldestPolicy

  • 线程池配置经验 :CPU 密集 N+1,IO 密集 2NN*(1+WT/ST)

  • 常用实现Executors 的四种快捷方式及其弊端(newFixedThreadPool 队列无界可能 OOM),建议手动 new ThreadPoolExecutor


7. 延迟双删怎么实现的?

目的:解决 Cache Aside 模式下,写操作后因并发读导致缓存中短期旧数据的极端问题。

步骤

  1. 先删除缓存(可选,有些做法只做后两次删)。

  2. 更新数据库

  3. 等待短暂时间 (比如 200~500ms,覆盖一次读操作完成重建的时间),然后再次删除缓存

实现方式

  • 简单版:开启一个独立线程(或线程池)睡眠后调用 Redis 删除。缺点:睡眠时间不好把握,强耦合业务。

  • 消息队列版 :更新 DB 后,发送一条延迟消息到 RocketMQ(指定延迟级别),消费者收到后再执行删缓存操作。这是更可靠的方案,利用 MQ 的延迟投递能力,解耦业务,且保证至少执行一次。

  • Timer/ ScheduledExecutorService:本地定时,简单但不推荐分布式场景。

注意 :延迟双删是最终一致性的妥协方案,不能完全消除不一致窗口,只能缩短。核心思想是第二次删除将并发读误写的脏数据清掉。


8. RabbitMQ 执行流程

生产者 → 交换机 → 队列 → 消费者 为主线:

  1. 生产者 :建立连接,创建信道(Channel),声明 Exchange 和 Queue,并通过 Routing Key 将消息发送到指定 Exchange。

  2. 交换机(Exchange) :根据类型(direct、topic、fanout、headers)和 Routing Key 将消息路由到一个或多个 Queue。

  3. 队列(Queue):存储消息,等待消费者。

  4. 消费者 :监听 Queue,主动拉取(basicGet)或被动推送(basicConsume)。收到消息后处理,并发送 ACK 确认。

    • ACK 机制 :自动 ACK(消费即确认,可能丢失消息)或手动 ACK(处理成功 basicAck,失败 basicNackbasicReject,可重新入队或丢弃)。

    • QoS 预取 :通过 basicQos 设置消费者未确认消息的上限,实现流量控制,避免消费者过载。

  5. 持久化 :消息可设为持久化(delivery_mode=2),配合持久化的队列,落盘后重启不丢失,但性能会下降。

  6. 死信与延迟:消息被拒绝、过期、或队列满时进入死信交换机(DLX),可借此实现延迟队列。


9. 分布式事务怎么实现的?

目标:保证多个独立的服务数据库之间的数据一致性。

常见方案

  • 1. 两阶段提交(2PC,XA 协议):强一致,但性能极差,单点故障,不适合高并发(如 Seata XA 模式)。

  • 2. TCC(Try-Confirm-Cancel)

    • Try 阶段预留资源,Confirm 提交,Cancel 释放。开发者需要实现这三个接口,对代码侵入大,但性能好。

    • 举例:转账,Try 冻结金额,Confirm 划扣,Cancel 解冻。

    • 常用框架:Seata TCC、ByteTCC。

  • 3. 可靠消息最终一致性(异步确保型)

    • 本地消息表 :一方服务将业务和消息写在同一本地事务中,通过定时任务轮询消息表发送到 MQ,另一方消费 MQ 并处理,通过 ACK + 重试保证最终一致。可使用 RocketMQ 的事务消息简化实现(半消息+回查)。

    • 流程:A 执行本地事务,同时发送 MQ 事务消息(半消息,暂不可消费)。A 的本地事务提交后,MQ 二次确认,B 才能消费。若 A 本地回滚,MQ 丢弃半消息。

  • 4. Saga 长事务:将一个长事务拆成多个本地事务,每个本地事务有对应的补偿操作。适用于流程长、不能长期锁资源的场景。Seata Saga 模式提供状态机编排。

  • 5. AT 模式(Seata 默认,基于两阶段思想的改进):自动记录回滚日志(undo log),无业务侵入,适用于简单 SQL 的分布式事务,通过全局锁避免脏写。

相关推荐
好家伙VCC1 小时前
动态因子图谱+滚动SHAP重构量化模型可解释性
java·人工智能·重构
椰椰椰耶1 小时前
[SpringCloud][11] Nacos 负载均衡,服务下线、权重配置、同集群优先访问
java·spring cloud·负载均衡
Miss roro1 小时前
通用OA能不能替代专业法务系统?钉钉飞书和律杏法务云的实测对比
java·钉钉·飞书·法律科技·企业诉讼管理·法务管理系统
不是光头 强1 小时前
feign-list-param-crash-cpp
java·数据结构·list
Chase_______1 小时前
【Java基础 | 10】异常处理入门:Throwable、try-catch-finally 与异常调用栈一次讲清
java
雪的季节1 小时前
C++ 运行时多态 vs 编译时多态
开发语言
chushiyunen1 小时前
php笔记、下载安装等
开发语言·笔记·php
Xin_ye100861 小时前
C# 零基础到精通教程 - WPF 深度专题:自定义布局与性能优化
开发语言·c#·wpf
努力努力再努力wz1 小时前
【C++高阶数据结构系列】:跳表 SkipList 详解:多层索引、随机晋升与C++ 完整实现(附跳表实现的源码)
开发语言·数据结构·数据库·c++·redis·缓存·skiplist