25年11月它思科技 Java开发 二面
#JAVA##JAVA面经##JAVA内推#
1. 项目中你负责的模块如何保证接口的幂等性?
回答思路
- 核心锚定:幂等性核心是「同一请求多次执行结果一致」,需按请求类型+业务场景选择方案,核心逻辑是「唯一标识+状态校验+防重拦截」;
- 分层拆解方案(按场景适配):
- 读接口:天然幂等,无需额外处理;
- 写接口(如订单创建、支付回调):
- 方案1:唯一请求ID(幂等号)+ Redis防重锁
- 流程:前端/网关生成唯一requestId,接口接收后先通过
SET requestId 1 NX EX 300加锁,成功则执行业务,失败则返回重复请求;- 适用场景:短链接、支付回调等一次性请求;
- 项目落地:订单创建接口,requestId关联用户ID+订单业务号,Redis锁有效期覆盖业务最大处理时长;
- 核心:NX保证只有一次执行,EX防止死锁;
- 写接口(如库存扣减):
- 方案2:业务唯一键+数据库唯一索引
- 流程:以「用户ID+商品ID+活动ID」为唯一键创建数据库唯一索引,重复插入时触发主键冲突,捕获异常并返回“操作已执行”;
- 适用场景:有明确业务唯一标识的场景;
- 状态机控制:
- 流程:针对有状态流转的业务(如订单:待支付→已支付→已完成),执行业务前校验当前状态,仅允许合法状态流转(如已支付订单不允许重复扣减库存);
- 核心结论:核心是「唯一标识拦截重复请求 + 业务状态校验防止重复处理」,按场景选择Redis防重锁/数据库唯一索引/状态机。
标准答案
我负责的订单模块按场景分层保证幂等:① 支付回调接口:用网关生成的唯一requestId作为幂等号,接口层先通过Redis
SET requestId 1 NX EX 300加防重锁,成功则执行业务,失败则返回重复请求;② 订单创建接口:以「用户ID+商品ID+活动ID」为业务唯一键,创建数据库唯一索引,重复插入触发主键冲突时,捕获异常并返回“订单已创建”;③ 库存扣减接口:增加状态机校验,仅当订单处于“待支付”状态时允许扣减,已支付/已取消状态直接拒绝。核心逻辑是「唯一标识拦截重复请求 + 业务状态校验防止重复处理」,确保同一请求多次执行结果一致。
2. 高并发场景下,如何解决数据库读写性能瓶颈?
回答思路
- 核心锚定:数据库瓶颈核心是「读多写少/写多读少」,解决方案围绕「读写分离+缓存+分库分表+SQL优化」,按“先缓存→再分离→最后分库分表”的优先级落地;
- 分层拆解方案:
- 读性能优化(核心:减轻数据库读压力):
- 缓存层:热点数据(如商品详情、用户信息)接入Redis,设置合理过期时间+缓存预热+缓存击穿/穿透/雪崩防护;
- 读写分离:主库写、从库读,通过中间件(MyCat/Sharding-JDBC)实现读写路由,从库可水平扩容;
- 只读实例:针对超高频读场景,部署多个只读实例,分摊读压力;
- 写性能优化(核心:减少写冲突+提升写效率):
- 分库分表:按用户ID/订单ID哈希分片,拆分大表为小表,减少单表数据量和锁竞争;
- 批量写入:将高频小批量写入(如日志、埋点)改为批量提交,减少事务次数和IO;
- 异步写入:非核心数据(如订单日志)通过MQ异步写入,降低接口响应时间;
- 基础优化(兜底):
- SQL优化:避免大表全扫、优化JOIN、控制返回字段;
- 数据库参数调优:增大InnoDB Buffer Pool、优化日志刷盘策略;
- 核心结论:先通过缓存扛读压力,再读写分离扩展读能力,最后分库分表解决写瓶颈,配合基础优化兜底。
标准答案
高并发下数据库读写瓶颈按“先轻量后重度”解决:① 读瓶颈:热点数据接入Redis缓存(设置过期时间+布隆过滤器防穿透),主从分离(主库写、多从库读),通过MyCat实现读写路由;② 写瓶颈:按用户ID哈希分库分表拆分大表,高频小写入改为批量提交,非核心数据通过MQ异步写入;③ 基础优化:优化慢SQL(避免全表扫描、控制JOIN表数),调大InnoDB Buffer Pool提升缓存命中率。核心逻辑是“缓存扛读、分库分表扛写、读写分离扩容量”,优先用轻量方案(缓存),再用架构方案(分库分表)。
3. JDK 动态代理与 CGLIB 动态代理的底层实现差异是什么?
回答思路
- 核心锚定:差异核心是「代理对象创建方式+目标类依赖」,JDK基于接口,CGLIB基于继承;
- 分层拆解差异:
维度 JDK 动态代理 CGLIB 动态代理 底层原理 反射机制,生成实现目标接口的代理类 ASM字节码框架,生成目标类的子类 目标类要求 必须实现接口 无需实现接口,不能代理final类/方法 代理对象创建 Proxy.newProxyInstance() 创建代理实例 Enhancer.create() 创建子类实例 方法调用 通过InvocationHandler.invoke() 拦截 通过MethodInterceptor.intercept() 拦截 性能 反射调用,创建快、执行稍慢 字节码生成,创建慢、执行稍快 - 核心结论:JDK代理依赖接口,基于反射;CGLIB代理基于继承,基于ASM字节码,两者拦截方式和性能特性不同。
标准答案
JDK动态代理与CGLIB的核心差异:① 底层原理:JDK基于反射,生成实现目标接口的代理类;CGLIB基于ASM字节码框架,生成目标类的子类;② 目标类要求:JDK要求目标类必须实现接口,CGLIB无需接口但不能代理final类/方法;③ 调用方式:JDK通过InvocationHandler.invoke()拦截方法,CGLIB通过MethodInterceptor.intercept()拦截;④ 性能:JDK创建代理快、执行稍慢,CGLIB反之。Spring AOP默认优先用JDK代理,目标类无接口时用CGLIB。
4. Spring AOP 失效除了方法权限问题,还有哪些底层原因?
回答思路
- 核心锚定:AOP失效核心是「代理对象未被调用」,底层原因围绕「代理创建失败+方法调用绕开代理」;
- 分层拆解原因:
- 代理创建失败:
- 目标类未被Spring管理:如手动new的对象(非Bean),无代理实例;
- 目标类是final类:CGLIB无法生成子类,代理创建失败;
- 切面切入点配置错误:如切入点表达式(@Pointcut)匹配不到目标方法;
- 方法调用绕开代理:
- 内部方法调用:代理对象内部调用自身方法(如A方法调用本类B方法),未经过代理拦截;
- 静态方法/私有方法:静态方法无法被代理,私有方法JDK/CGLIB均无法拦截;
- 容器初始化问题:
- 切面类未被扫描:切面类未加@Aspect/@Component,Spring无法识别;
- 代理顺序问题:多个切面冲突,导致目标切面未生效;
- 核心结论:失效本质是代理未创建或调用未走代理,需排查Bean管理、调用方式、切入点配置。
标准答案
Spring AOP失效除方法权限外的核心原因:① 代理创建失败:目标类未被Spring管理(手动new)、目标类是final类(CGLIB无法代理)、切入点表达式匹配错误;② 方法调用绕开代理:代理对象内部调用自身方法(如A调本类B)、调用静态方法/私有方法(无法被拦截);③ 容器初始化问题:切面类未加@Aspect/@Component、多切面顺序冲突。核心是「代理未创建或调用未走代理」,可通过暴露代理(@EnableAspectJAutoProxy(exposeProxy=true))解决内部调用问题。
5. MySQL 中 InnoDB 事务原子性、持久性分别依靠什么机制保证?
回答思路
- 核心锚定:原子性靠「undo log」,持久性靠「redo log + 双写缓冲区」;
- 分层拆解机制:
- 原子性(要么全执行,要么全回滚):
- 依赖undo log(回滚日志):事务执行时,InnoDB会记录数据修改前的状态到undo log;若事务执行失败/回滚,通过undo log恢复数据到修改前的状态,保证操作全部撤销;
- 举例:事务修改A=1、B=2,若B修改失败,通过undo log将A恢复为原值,保证原子性;
- 持久性(提交后数据不丢失):
- 核心:redo log(重做日志)+ 双写缓冲区;
- redo log:事务执行时,先将修改写入redo log(内存+磁盘),提交时通过
innodb_flush_log_at_trx_commit=1强制刷盘,宕机后可通过redo log恢复已提交数据;- 双写缓冲区:防止数据页部分写(如IO中断),数据页刷盘前先写入双写缓冲区,保证数据页完整性;
- 核心结论:原子性靠undo log回滚,持久性靠redo log保证提交后可恢复+双写缓冲区保证数据页完整。
标准答案
InnoDB事务原子性和持久性的保证机制:① 原子性:依赖undo log(回滚日志),事务执行时记录数据修改前的状态,失败/回滚时通过undo log恢复数据,保证操作要么全执行、要么全回滚
6. MVCC 在可重复读隔离级别下是如何实现一致性视图的?
思路
- 抓住核心:Read View 生成时机 + 可见性规则
- RR:事务第一次 SELECT 时生成一个全局唯一 Read View,之后全程复用
- RC:每次 SELECT 都生成新的 Read View
答案 在可重复读(RR)级别下:
- 事务第一次执行查询时生成一个 Read View,之后整个事务期间不再更新;
- 通过 Read View 中的活跃事务ID列表、最小/最大事务ID,判断数据版本是否可见;
- 即使其他事务提交了修改,当前事务依然使用第一次生成的视图,保证重复读取结果一致。 一句话:RR 是全程共用一个视图,RC 是每次查询新视图。
7. 分布式环境下,本地事务失效如何解决?
思路
- 本地事务只保证单库原子性,分布式必须用分布式事务
- 分强一致 / 最终一致
答案
- 强一致性:使用 Seata AT 模式(二阶段提交),TM 协调多个 RM,保证所有库要么全提交,要么全回滚。
- 最终一致性:
- 本地消息表 + MQ
- 可靠消息(RocketMQ 事务消息)
- 业务长流程:Saga 模式,拆分子事务 + 补偿机制。
8. 如何设计一个支持高并发的分布式 ID 生成方案?
思路
- 要求:全局唯一、趋势递增、高可用、高吞吐
- 排除:UUID(太长无序)、自增ID(单点)
答案 成熟方案:雪花算法(Snowflake)+ 双 buffer 优化 结构:
- 1bit 符号位
- 41bit 时间戳
- 10bit 机器ID
- 12bit 序列号
优化:
- 机房/业务标识放机器位
- 本地缓存一段ID,减少远程调用
- 时钟回拨处理:拒绝/等待/切换机器ID
- 高可用:多实例部署,无中心化
9. Redis 与数据库双写一致性如何保证?
思路
- 核心:先更库,再删缓存;不更新缓存
- 避免并发脏数据
答案 标准方案:
- 查询:先查 Redis → 不存在查库 → 写回 Redis
- 更新/删除:
- 第一步:更新数据库
- 第二步:删除 Redis 缓存(不做 set 更新)
- 兜底:
- 缓存过期时间
- 异步补偿重试(MQ)
10. 线上 JVM 内存泄漏如何定位与分析?
思路
- 标准流程:dump → 分析 → 定位 → 修复
- 工具:jmap、jstack、MAT、Arthas
答案
- 保留现场:jmap -dump 生成堆转储文件
- 分析工具:用 MAT 查看:
- 大对象
- GC Roots 引用链
- 线程栈
- 常见泄漏点:
- ThreadLocal 没 remove
- 静态集合缓存对象不清理
- 流/连接未关闭
- 内部类持有外部类引用
- 修复:切断引用、及时释放、加池化管理。
11. 线程池参数在高并发场景下如何合理设计?
思路
- CPU 密集型 / IO 密集型
- 队列、拒绝策略
答案
- CPU 密集型 corePoolSize = CPU核心数 + 1
- IO 密集型(接口、RPC、DB) corePoolSize = CPU核心数 * 2
- maximumPoolSize:不要过大,防止线程爆炸
- 队列:
- 高并发用有界队列,防止OOM
- 拒绝策略:
- 生产用 CallerRunsPolicy 或自定义抛异常
12. 线上服务频繁 FullGC 如何排查与优化?
思路
- 先看现象:频繁 / 慢 / 卡顿
- 定位:堆、元空间、直接内存
答案 排查步骤:
- jstat -gc 观察:YGC / FGC 次数、耗时
- 看 dump:
- 大对象
- 内存泄漏
- 常见原因:
- 堆太小
- 内存泄漏
- 永久代/元空间满
- 直接内存泄漏(Netty)
- 优化:
- 调 Xmx/Xms
- 修复泄漏
- 优化大对象、池化
13. 如何设计一个可水平扩展的高可用后端架构?
思路
- 无状态 → 可水平扩
- 分层、解耦、限流、降级、熔断
答案
- 应用无状态化:不存本地会话,session 放 Redis
- 集群化:多实例部署,Nginx/LVS 负载均衡
- 缓存层:Redis 集群/哨兵
- 数据层:
- 主从 + 读写分离
- 分库分表
- 高可用保障:
- 限流、降级、熔断(Sentinel/Sentinel)
- 异步解耦 MQ
- 监控告警 + 自动扩缩容
查看17道真题和解析