Java实习面试记录
JMM 中的三大特性
可见性
多个线程访问同一变量时,一个线程修改的值,其他线程能否及时看到?
volatile
能保证可见性:
volatile boolean flag = false;public void stopThread() {flag = true;
}public void run() {while (!flag) {// do something}
}
原子性
一个操作是否不可中断?
i++
不是原子操作,需要使用AtomicInteger
或synchronized
保证原子性。
有序性
编译器/CPU 可能会指令重排。volatile
禁止重排序,synchronized
同样具有顺序性保障。
ReentrantLock vs synchronized
对比项 | synchronized | ReentrantLock |
---|---|---|
可重入性 | ✅ | ✅ |
公平锁 | ❌ | ✅ 可选 |
可中断 | ❌ | ✅ |
读写锁支持 | ❌ | ✅ 可搭配 ReadWriteLock |
性能 | JDK1.6后优化较好 | 控制更灵活但稍复杂 |
场景:当需要响应中断、超时控制或公平锁机制时,优先使用 ReentrantLock
。
高并发线程安全计数器设计
场景:库存扣减、秒杀扣量
- 方案:
AtomicInteger
或LongAdder
(高并发推荐)配合 Redis + Lua 保证分布式原子性。 - 如果涉及分布式系统:
- Redis 原子扣减 + 库存回滚
- DB乐观锁(version字段)+ 唯一索引幂等性 + 异步 MQ 补偿机制
2. Spring框架与设计模式
三级缓存解决循环依赖
三级缓存结构:
1. 一级缓存:singletonObjects (成品)
2. 二级缓存:earlySingletonObjects(提前暴露的半成品)
3. 三级缓存:singletonFactories(用于生成早期代理对象)
// 如果构造函数注入(如 @Autowired 构造器)发生循环依赖,无法提前暴露半成品对象,三级缓存失效。
解决方案:推荐使用字段注入或 @PostConstruct
避免构造器注入的循环依赖。
🔹 JDK动态代理 vs CGLIB
特性 | JDK Proxy | CGLIB |
---|---|---|
是否需要接口 | ✅ | ❌ |
代理机制 | 基于接口,反射生成代理类 | 继承目标类,字节码增强 |
性能 | 接口多时快 | 复杂类时略慢 |
Spring选择 | 有接口 → JDK,否则 → CGLIB |
责任链模式优惠券参数校验(伪代码)
public interface Handler {void setNext(Handler handler);void handle(Request request);
}public class AmountCheckHandler implements Handler {public void handle(Request req) {if (req.amount < 0) throw new IllegalArgumentException();next.handle(req);}
}
优势:
- 将每个校验逻辑解耦
- 动态组合校验链(更灵活)
- 新增规则无需修改原有代码,符合开放封闭原则
3. MySQL优化与高并发设计
SQL慢查优化流程
- 确认慢SQL
EXPLAIN
查看执行计划:type
:最佳是const
>ref
>range
>ALL
key
:是否命中索引rows
:预估扫描行数extra
:是否出现Using filesort
,Using temporary
- 优化手段:
- 添加合适索引
- 覆盖索引
- 拆分大表
- LIMIT 分页优化
分库分表后的跨分片查询
- 手段:
- 业务规避
- 异步聚合(如日志、报表)
- 中间件支持(如 ShardingSphere 分布式 SQL 解析 + 路由 + 聚合)
唯一索引 + 乐观锁幂等机制
插入前判断记录是否存在(幂等键),或插入失败即跳过。
INSERT INTO coupon_log(user_id, coupon_id, request_id)
VALUES (...) ON DUPLICATE KEY UPDATE ...
- 乐观锁:基于
version
字段,失败重试 - 悲观锁:如
SELECT ... FOR UPDATE
,加锁范围大,吞吐低
对比 | 乐观锁 | 悲观锁 |
---|---|---|
并发性能 | 高 | 低 |
死锁风险 | 小 | 存在 |
应用场景 | 读多写少 | 严格强一致性 |
4. Redis与高可用架构
为什么用 Lua 实现原子性?
- 所有命令作为一个事务性脚本执行
- 单线程模型天然避免并发冲突
- 与
MULTI/EXEC
不同,Lua 是“不可打断+打包执行”
Redis 脑裂问题与防范
脑裂后主写成功,主挂掉无法同步从节点 → 数据丢失。
防止写入孤立主:
min-slaves-to-write: 2
min-slaves-max-lag: 10
表示只有在有 ≥2 个从节点延迟 <10s 时才允许主节点写入。
布隆过滤器误判率优化
误判 = 说有但其实没有
- 调整参数:
expectedInsertions
、falseProbability
- 多哈希函数,合理内存分配
- 将误判项记录到数据库(如灰度机制)
- 定期重建布隆过滤器
5. 分布式系统与消息中间件
RocketMQ事务消息两阶段提交
- Half Message:消息先存储为“准备状态”
- 执行本地事务
- 事务回查:
- 成功 → Commit
- 失败 → Rollback
- 超时 → Broker 定期回查生产者状态
消费端幂等性方案
- 通过唯一
msg_id/request_id
入库前判断(Redis Set / 去重表 + 唯一索引) - 如果高并发瓶颈:
- Redis 先判断,再异步入库(最终一致)
- 可设置 TTL 或滑动窗口做清理
MQ堆积延迟排查
- 查看消费速率和消费端堆积量
- 检查消费者是否:
- 死循环/业务异常
- 并发度设置过低
- 手段:
- 扩容消费者
- 提升消费能力
- 消息分区优化(或分topic)
6. 短链接系统设计
短链生成算法对比
方法 | 优点 | 缺点 |
---|---|---|
Hash(MD5, SHA) | 简单、可分布式 | 存在哈希冲突,需重试 |
Snowflake | 唯一性强、时间有序 | 不适合短码(太长) |
数据库自增 | 简单 | 分布式不方便 |
base62编码 | 将ID压缩为短字符串 | 需考虑冲突与排序性 |
推荐组合:Snowflake生成ID + Base62编码
短链唯一性与哈希冲突处理
- 哈希后检查数据库是否存在
- 存在则重新哈希/加盐或加冲突计数
- 记录
原始URL+生成短码
的映射表
QPS 10万级别缓存策略
- Redis 做短链跳转缓存(Key: shortCode → URL)
- 热点数据加入本地 Caffeine 缓存
- 缓存穿透 → 布隆过滤器 + 缓存空值
- 异步写DB,日志落盘异步归档
7. 开放性问题
单元测试发现Bug案例
在 Mini-Spring 实现 AOP 时,某个切面注入出现空指针。测试时通过 mock bean 提前暴露出代理对象未生效的问题,进而修复为通过三级缓存暴露代理后的对象。
技术选型分歧如何协调
- 收集团队观点 + 论证维度(性能、生态、学习成本)
- 快速 POC 小范围验证
- 从场景适配角度做最终决策,而非“好恶”
- 若有博弈,保留少数意见作为 Plan B,确保应变能力
深入秒杀系统项目技术细节
以下针对“优惠券秒杀系统”的核心技术点进行深度解析与场景化问答:
1. 责任链模式在参数校验中的实现
Q:责任链模式如何保证优惠券创建流程的扩展性?如果新增一个“黑名单用户校验”节点,如何最小化代码改动?
- 答案:
核心实现:
- 定义统一的校验接口,每个校验器实现该接口并持有下一个节点的引用。
- 校验失败时抛出异常,成功则调用链的下一个节点。
public interface CouponValidator {void validate(CouponRequest request) throws ValidationException; } public class ParamValidator implements CouponValidator {private CouponValidator next;public void validate(CouponRequest request) {if (request.getAmount() <= 0) throw new ValidationException("金额无效");if (next != null) next.validate(request);}// 设置下一个校验器public void setNext(CouponValidator next) { this.next = next; } }
扩展性:新增校验器只需实现
CouponValidator
接口,并通过setNext
插入到责任链中。例如新增BlacklistValidator
:CouponValidator chain = new ParamValidator(); chain.setNext(new InventoryValidator()); chain.setNext(new BlacklistValidator()); // 新增节点
优势:无需修改已有校验逻辑,符合开闭原则(OCP)。
2. 执行点记录与断点续发机制
Q:如何设计“执行点记录”机制来支持故障恢复?如果发放优惠券时系统宕机,重启后如何快速恢复?
- 答案:
数据结构设计: 在数据库中记录发放批次的关键信息:
CREATE TABLE coupon_batch (batch_id VARCHAR(64) PRIMARY KEY,total_count INT,sent_count INT, -- 已发放数量last_user_id VARCHAR(64) -- 最后处理的用户ID );
恢复流程:
- 系统重启后,查询
coupon_batch
表获取未完成的批次。 - 根据
last_user_id
从断点处继续遍历用户列表,批量扣减库存并更新sent_count
。
- 系统重启后,查询
性能优化:
- 批量处理:每次发放100条优惠券,减少数据库写入压力。
- 异步日志:通过RocketMQ异步记录发放进度,降低主流程延迟。
3. 幂等性保障方案
Q:如何通过“唯一索引+乐观自旋”实现RocketMQ消费端的幂等性?如果并发极高,自旋重试可能导致CPU飙升,如何优化?
- 答案:
实现逻辑:
- 消费消息前,尝试插入去重表(唯一索引为消息ID):
INSERT INTO msg_dedup (msg_id) VALUES ('msg_001') ON DUPLICATE KEY UPDATE msg_id=msg_id;
- 若插入成功,继续处理;若触发唯一索引冲突,判定为重复消息,直接丢弃。
自旋优化:
- 限制重试次数:例如最多重试3次,超过则记录日志并人工介入。
- 退避策略:每次重试前增加等待时间(如指数退避:100ms → 200ms → 400ms)。
4. Redis ZSet在优惠券列表展示中的应用
Q:为什么选择ZSet存储用户已领取的优惠券?如果需支持按过期时间排序,如何设计Score?
- 答案:
- ZSet优势:
- 天然支持按Score排序(如领取时间),查询时直接使用
ZREVRANGE
倒序获取。 - 时间复杂度低:插入和范围查询均为O(log N)。
- 天然支持按Score排序(如领取时间),查询时直接使用
- 过期时间排序:
将Score设置为优惠券过期时间戳(而非领取时间),查询时按Score范围过滤:
ZADD user:coupons:1001 1672502400000 "coupon_001" -- 2023-01-01过期 ZRANGEBYSCORE user:coupons:1001 1672502400000 +inf -- 查询未过期优惠券
- ZSet优势:
5. Redis Lua脚本与原子性校验
Q:请写出资格校验的Lua脚本伪代码,并说明为何不直接用Redis事务(MULTI/EXEC)?
- 答案:
Lua脚本示例:
-- KEYS[1]: 库存键, KEYS[2]: 已领取用户集合, ARGV[1]: 用户ID local stock = tonumber(redis.call('GET', KEYS[1])) if stock <= 0 thenreturn 0 -- 库存不足 end if redis.call('SISMEMBER', KEYS[2], ARGV[1]) == 1 thenreturn -1 -- 已领取 end redis.call('DECR', KEYS[1]) redis.call('SADD', KEYS[2], ARGV[1]) return 1 -- 领取成功
Lua vs 事务:
- 原子性:Lua脚本执行期间不会被其他命令打断,事务中的命令可能被其他客户端插入执行。
- 灵活性:Lua支持复杂逻辑(如条件判断、循环),事务仅支持简单命令队列。
6. 高并发下缓存穿透与击穿应对
Q:布隆过滤器如何解决缓存穿透?如果误判率较高,如何优化?
- 答案:
- 布隆过滤器原理:
- 通过多个哈希函数将元素映射到位数组,查询时若任意一位为0,则元素一定不存在。
- 误判率优化:
- 增加哈希函数数量:降低冲突概率(但会增加计算开销)。
- 扩大位数组容量:减少哈希碰撞。
- 兜底策略:缓存空值(
key:null
)并设置短TTL,拦截重复查询。
- 布隆过滤器原理:
7. RocketMQ消息堆积处理
Q:如果消费端处理速度远低于生产端,导致消息大量堆积,如何快速解决?
- 答案:
- 应急方案:
- 扩容消费者:增加消费者实例或线程池大小。
- 批量消费:调整
consumeMessageBatchMaxSize
参数,一次处理多条消息。 - 降级非核心逻辑:例如先缓存领取记录,异步持久化到数据库。
- 根本解决:
- 优化消费逻辑:避免同步阻塞操作(如远程调用),改用异步或并行处理。
- 限流保护:通过Sentinel对消费者限流,避免雪崩。
- 应急方案:
总结:面试回答技巧
- 细节具体化:结合代码、数据结构和流程图说明实现(如“三级缓存解决循环依赖”)。
- 场景绑定:强调设计背后的业务需求(如“秒杀场景要求原子性,因此选择Lua脚本”)。
- 问题反思:主动提及遇到的挑战(如“初期未考虑构造器注入循环依赖,通过单元测试发现并修复”)。
- 性能数据:若有压测结果,可量化说明优化效果(如“引入Redis后,QPS从1k提升至10k”)。