25年10月智晟未来信息科技 Java开发实习生 二面
#JAVA##JAVA面经##JAVA内推#
1. 分享一个你印象最深的 Bug 吧~当时现象是啥?怎么一步步定位到根因的?最后怎么解决的?从中学到了啥?
回答:
现象:电商项目中,用户支付完成后偶发出现“订单状态已支付,但库存未扣减”的情况,且仅在高峰期(每秒超200笔支付)出现,低峰期完全正常。 定位过程:
- 先查日志:支付回调接口日志显示“扣减库存成功”,但库存表数据未变,排除接口未执行的问题;
- 查数据库事务:支付流程是“扣库存→改订单状态→记录流水”,事务配置为默认的
REPEATABLE READ,但发现扣库存的SQL用了UPDATE 库存表 SET 数量=数量-1 WHERE 商品ID=?,未加行锁范围限制;- 查数据库锁等待:通过
show engine innodb status发现高峰期存在大量行锁等待,部分事务因锁超时(innodb_lock_wait_timeout默认5秒)回滚,但订单状态更新因提前提交(代码逻辑错误,拆分了事务)未回滚;- 根因定位:代码中将“扣库存”和“改订单状态”拆分为两个独立事务,高峰期扣库存事务因锁等待超时回滚,但改订单状态事务已提交,导致数据不一致。
解决方式:
- 合并事务:将“扣库存→改订单状态→记录流水”纳入同一个数据库事务,保证原子性;
- 优化锁机制:扣库存SQL改为
UPDATE 库存表 SET 数量=数量-1 WHERE 商品ID=? AND 数量>=1,并添加行锁提示FOR UPDATE;- 调整锁超时:将
innodb_lock_wait_timeout调整为10秒,同时添加重试机制(最多3次)。学到的经验:
- 事务必须保证原子性,核心业务流程不能拆分事务;
- 高峰期的并发问题无法通过“表面排查”发现,需结合数据库锁、事务日志分析;
- 并发场景下,更新操作必须加“行锁范围限制”,避免全表锁或无效更新。
2. 多表联合查询(比如 LEFT JOIN)变慢了,你一般会从哪些地方入手排查和优化?有实际处理过的例子吗?
回答:
排查方向:
- 索引层面:
- 检查关联字段(JOIN ON 后的字段)是否加索引,LEFT JOIN 中左表关联字段必须加索引,右表关联字段建议加索引;
- 检查查询字段是否包含“回表字段”(非索引字段),是否可通过覆盖索引减少回表;
- SQL 层面:
- 检查是否有不必要的 JOIN 表(比如关联了不需要的表);
- 检查 WHERE 条件是否过滤了大量数据,是否将过滤条件前置(先过滤再 JOIN);
- 检查是否使用了 SELECT *,是否只查询需要的字段;
- 数据库层面:
- 执行
EXPLAIN分析执行计划,看是否出现“全表扫描(type=ALL)”“临时表(Using temporary)”“文件排序(Using filesort)”;- 检查表数据量,是否有大表(千万级以上)未分库分表;
- 数据层面:
- 检查是否有大量 NULL 值(LEFT JOIN 右表NULL值多会增加计算量);
- 检查表碎片,是否需要
OPTIMIZE TABLE整理碎片。实际案例: 某物流项目中,
LEFT JOIN查询“订单表(order)+ 物流表(logistics)+ 用户表(user)”时,响应时间从50ms涨到3s。
- 排查:执行
EXPLAIN发现物流表的关联字段order_id无索引,且查询字段包含logistics表的非索引字段(物流详情),出现全表扫描;- 优化:
- 给
logistics.order_id添加普通索引;- 将查询字段改为仅需的
order.id, logistics.status, user.name,并创建覆盖索引idx_order_id_status (order_id, status);- 将 WHERE 条件
logistics.create_time > '2025-01-01'前置到子查询,先过滤物流表数据再 JOIN:SELECT o.id, l.status, u.name FROM `order` o LEFT JOIN (SELECT order_id, status FROM logistics WHERE create_time > '2025-01-01') l ON o.id = l.order_id LEFT JOIN `user` u ON o.user_id = u.id WHERE o.status = 1;- 效果:响应时间降至80ms以内。
3. Redis 哨兵和 Cluster 集群,你觉得它们核心区别在哪?平时项目里怎么选?(比如数据量大小、高可用要求)
回答:
维度 Redis 哨兵(Sentinel) Redis Cluster(集群) 核心定位 主从高可用(故障自动切换) 分布式存储+高可用(分片+故障切换) 数据分片 无,所有节点数据全量复制(主从同步) 有,按槽位(16384个)分片,每个节点存部分数据 节点角色 主节点+从节点+哨兵节点(监控/决策) 主节点+从节点(无专门哨兵,自身实现故障检测) 扩容能力 弱(全量数据,扩容仅提升读性能) 强(分片扩容,可线性提升存储和性能) 故障切换 哨兵集群投票决定主节点切换 集群节点投票,自动将从节点提升为主节点 适用数据量 中小数据量(单节点能承载,如GB级) 大数据量(TB级,需分片存储)
选型策略:
- 选哨兵的场景:
- 数据量小(单Redis节点可承载,如≤10GB);
- 核心诉求是“高可用”(避免主节点宕机导致服务不可用);
- 业务逻辑简单,无需分片,比如缓存热点数据、计数器、会话存储。 例:某后台管理系统,缓存用户权限数据(总量≤5GB),用哨兵保证主从切换,满足99.9%可用即可。
- 选Cluster的场景:
- 数据量大(单节点无法承载,如≥50GB);
- 高并发读写(如每秒万级请求),需要分片分摊压力;
- 核心诉求是“分布式存储+高可用”,比如电商商品缓存、订单缓存、秒杀库存。 例:某电商平台,商品缓存总量超100GB,QPS峰值5万,用Cluster分片为6个主节点+6个从节点,既满足存储需求,又保证高可用。
4. 小实操:怎么用 Linux 命令找出当前目录下所有 .log 文件里包含 "error" 的行,并存到 error_log.txt?(说说你会用哪些命令组合)
回答:
核心命令组合:
grep(匹配关键词)+find(查找文件),推荐两种常用方式:方式1:基础版(适合新手,逻辑清晰)
# 1. find 查找当前目录下所有 .log 文件 # 2. xargs 传递文件列表给 grep # 3. grep 匹配包含 "error" 的行(忽略大小写用 -i,显示行号用 -n) # 4. > 重定向到 error_log.txt(覆盖写入),>> 是追加写入 find ./ -name "*.log" | xargs grep "error" > error_log.txt方式2:进阶版(更健壮,处理特殊文件名如含空格)
# -print0 和 -0 配合,处理文件名含空格/特殊字符的情况 # -i:忽略大小写(可选,根据需求) # -n:显示行号(可选,便于定位) find ./ -name "*.log" -type f -print0 | xargs -0 grep -in "error" > error_log.txt参数说明:
./:当前目录(可替换为绝对路径,如 /var/log/);-name "*.log":匹配后缀为 .log 的文件;-type f:仅匹配文件(排除目录);-print0/-0:解决文件名含空格、换行等特殊字符的问题;-i:忽略大小写(比如匹配 Error/ERROR/error);-n:显示匹配行的行号(便于排查问题);>:覆盖写入 error_log.txt,若需追加用>>。验证:执行后可通过
cat error_log.txt查看结果,确认是否包含所有含“error”的行。
5. 项目里遇到过数据库并发导致的数据不一致吗?怎么解决的?如果没遇到过,你觉得可以用哪些方案预防?(比如加锁、事务隔离级别)
回答:
实际案例: 某秒杀项目中,并发抢购同一商品时,出现“超卖”(库存为负数),核心原因是高并发下多个请求同时读取到“库存>0”,并执行扣减操作,导致数据不一致。
解决方式:
- 数据库层面:
- 事务隔离级别调整:将默认的
REPEATABLE READ改为READ COMMITTED,减少幻读;- 悲观锁:扣库存时用
SELECT ... FOR UPDATE加行锁,保证同一时间只有一个请求能修改库存:BEGIN; -- 加行锁,锁定该商品的库存行 SELECT stock FROM product WHERE id = 1 FOR UPDATE; -- 扣减库存(需判断库存>0) UPDATE product SET stock = stock - 1 WHERE id = 1 AND stock > 0; -- 检查影响行数,若为0则回滚 IF ROW_COUNT() = 0 THEN ROLLBACK; ELSE COMMIT; END IF;- 应用层面:
- 乐观锁:基于版本号或库存字段实现,避免长事务锁:
(若更新失败,应用层重试3次,仍失败则提示“库存不足”);UPDATE product SET stock = stock - 1 WHERE id = 1 AND stock = #{currentStock};- 分布式锁:用Redis的
SETNX实现分布式锁,保证同一商品只有一个请求进入扣库存逻辑。预防方案(未遇到时):
- 锁机制:
- 悲观锁:适合写多读少、数据一致性要求极高的场景(如金融交易);
- 乐观锁:适合读多写少、并发高的场景(如电商秒杀);
- 分布式锁:跨服务/跨数据库的并发场景。
- 事务优化:
- 缩短事务时长(避免长事务占用锁);
- 合理设置事务隔离级别(如
READ COMMITTED避免不必要的锁等待)。- 业务层面:
- 限流:对高并发接口做限流(如每秒最多1000请求),避免数据库压垮;
- 预扣库存:提前将库存缓存到Redis,先扣Redis库存,再异步同步到数据库。
6. 前端要渲染大量数据时页面卡顿,你会从前端和后端分别怎么优化?(比如分页、虚拟滚动、懒加载)
回答:
前端优化(核心:减少DOM渲染量、降低重绘重排):
- 虚拟滚动(核心方案):
- 原理:只渲染可视区域内的DOM节点,滚动时动态替换内容,比如10万条数据仅渲染20条可视节点;
- 实现:用成熟组件(如Vue的
vue-virtual-scroller、React的react-window),避免手写复杂逻辑。- 分页加载:
- 基础分页:按页码/条数加载(如每页20条),配合“下一页”“上一页”;
- 无限滚动:滚动到底部自动加载下一页(结合节流函数,避免频繁请求)。
- 懒加载:
- 非核心数据(如列表项的详情、图片)延迟加载,仅当用户点击/滚动到该位置时加载;
- 图片懒加载:用
loading="lazy"或自定义指令,先加载占位图。- 渲染优化:
- 减少DOM嵌套,避免复杂样式(如
position: fixed);- 用
requestAnimationFrame处理数据渲染,避免同步渲染阻塞主线程;- 大数据计算(如排序、过滤)放到Web Worker,不占用主线程。
后端优化(核心:减少数据传输量、提升接口响应速度):
- 数据分页查询:
- 用
LIMIT offset, size(MySQL)或ROWNUM(Oracle)实现分页,避免全表查询;- 优化分页SQL:加索引,避免
SELECT *,只返回前端需要的字段。- 数据预处理:
- 后端提前过滤、排序、聚合数据(如统计总数、计算百分比),避免前端处理大量数据;
- 缓存分页结果:用Redis缓存热门分页数据(如前10页),减少数据库查询。
- 接口优化:
- 压缩数据:开启Gzip压缩,减少传输体积;
- 批量接口合并:将多个小接口合并为一个,减少HTTP请求数;
- 异步返回:若数据处理耗时,返回“任务ID”,前端轮询获取结果(避免长连接阻塞)。
案例:某后台系统渲染10万条订单数据,前端卡顿严重。
- 前端:用
vue-virtual-scroller实现虚拟滚动,仅渲染可视区域(约30条);- 后端:分页查询(每页50条),并缓存前20页数据,接口响应时间从2s降至100ms;
- 效果:页面流畅无卡顿,滚动时无明显延迟。
7. 数据库
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
【八股真解】精炼最新高频面经 文章被收录于专栏
本专栏在精不在多,内容分为八股文、大厂真实面经,面试通过后将offer和面试题私发给我,可退还专栏的收益部分费用。欢迎大家共建专栏
查看22道真题和解析