恒生电子 Java 二面 面经
恒生电子 Java开发二面
1. 详细讲讲你负责的项目,系统架构是怎么设计的?(30min)
回答框架:
项目背景(5min)
- 业务场景和用户规模
- 技术选型和团队规模
- 你在项目中的角色和职责
系统架构(15min)
- 整体架构图(分层架构、微服务架构)
- 核心模块划分和职责
- 技术栈选择的理由
- 数据库设计(分库分表、读写分离)
- 缓存策略(Redis集群、缓存更新)
- 消息队列使用场景
技术难点(10min)
- 遇到的核心技术挑战
- 如何分析和解决问题
- 方案对比和选择依据
- 最终效果和数据指标
面试官可能深挖:
- 为什么不用XX技术方案?
- 系统的QPS和TPS是多少?
- 如何保证高可用和容灾?
- 遇到过线上故障吗?怎么处理的?
- 如果让你重新设计会改什么?
2. MySQL的事务隔离级别,MVCC的实现原理
四种隔离级别:
READ UNCOMMITTED |
可能 |
可能 |
可能 |
READ COMMITTED |
不可能 |
可能 |
可能 |
REPEATABLE READ(默认) |
不可能 |
不可能 |
可能 |
SERIALIZABLE |
不可能 |
不可能 |
不可能 |
问题说明:
- 脏读:读到未提交的数据
- 不可重复读:同一事务内多次读取结果不同(UPDATE)
- 幻读:同一事务内多次查询记录数不同(INSERT/DELETE)
MVCC(多版本并发控制)原理:
核心组件:
1. 隐藏字段
-- 每行记录包含隐藏字段 DB_TRX_ID -- 最后修改该行的事务ID DB_ROLL_PTR -- 回滚指针,指向undo log DB_ROW_ID -- 隐藏主键(无主键时)
2. Undo Log(版本链)
当前版本: id=1, name='张三', trx_id=100
↓ (回滚指针)
旧版本1: id=1, name='李四', trx_id=90
↓
旧版本2: id=1, name='王五', trx_id=80
3. Read View(读视图)
class ReadView {
long m_low_limit_id; // 当前最大事务ID+1
long m_up_limit_id; // 活跃事务最小ID
List<Long> m_ids; // 创建ReadView时的活跃事务列表
long m_creator_trx_id; // 创建ReadView的事务ID
}
可见性判断规则:
boolean isVisible(long trx_id, ReadView view) {
// 1. 当前事务自己的修改,可见
if (trx_id == view.m_creator_trx_id) return true;
// 2. 事务ID小于最小活跃ID,已提交,可见
if (trx_id < view.m_up_limit_id) return true;
// 3. 事务ID大于等于最大ID+1,未提交,不可见
if (trx_id >= view.m_low_limit_id) return false;
// 4. 在活跃事务列表中,未提交,不可见
if (view.m_ids.contains(trx_id)) return false;
// 5. 不在活跃列表,已提交,可见
return true;
}
RC vs RR的区别:
- RC(READ COMMITTED):每次SELECT都生成新的ReadView
- RR(REPEATABLE READ):事务开始时生成ReadView,之后复用
示例:
-- 事务A(trx_id=100) BEGIN; SELECT * FROM user WHERE id=1; -- 生成ReadView -- 此时活跃事务:[100, 101] -- 事务B(trx_id=101) UPDATE user SET name='李四' WHERE id=1; COMMIT; -- 事务A SELECT * FROM user WHERE id=1; -- RC:生成新ReadView,能看到李四 -- RR:复用旧ReadView,看到原值
MVCC优点:
- 读不加锁,写不阻塞读
- 提高并发性能
- 解决不可重复读问题
3. JVM调优经验,如何排查内存泄漏和CPU飙高问题?
JVM调优参数:
堆内存设置:
-Xms2g # 初始堆大小 -Xmx2g # 最大堆大小(建议与Xms相同) -Xmn800m # 新生代大小 -XX:SurvivorRatio=8 # Eden:Survivor = 8:1
垃圾收集器选择:
# G1收集器(推荐) -XX:+UseG1GC -XX:MaxGCPauseMillis=200 # 最大停顿时间 -XX:G1HeapRegionSize=4m # CMS收集器 -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=70 # ZGC(JDK 11+) -XX:+UseZGC
GC日志:
-Xlog:gc*:file=/var/log/gc.log:time,uptime,level,tags -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/heapdump.hprof
排查内存泄漏:
1. 观察现象
# 查看堆内存使用 jmap -heap <pid> # 查看对象统计 jmap -histo:live <pid> | head -20
2. 生成堆转储
# 手动dump jmap -dump:live,format=b,file=heap.hprof <pid> # 或者等OOM自动dump -XX:+HeapDumpOnOutOfMemoryError
3. 分析堆转储
# 使用MAT(Memory Analyzer Tool) # 查看: # - Leak Suspects(泄漏嫌疑) # - Dominator Tree(支配树) # - Top Consumers(最大对象)
常见内存泄漏场景:
- ThreadLocal未清理
- 集合类持有大量对象
- 监听器未注销
- 数据库连接未关闭
- 缓存无限增长
排查CPU飙高:
1. 定位进程
top # 找到CPU高的Java进程PID
2. 定位线程
# 查看线程CPU使用 top -Hp <pid> # 记录CPU高的线程ID(TID) # 转换为16进制 printf "%x\n" <tid>
3. 查看线程堆栈
# 生成线程dump jstack <pid> > thread.dump # 搜索16进制线程ID grep -A 50 "0x线程ID" thread.dump
4. 分析原因
- 死循环:代码逻辑问题
- 频繁GC:内存不足,对象创建过多
- 正则表达式:复杂正则回溯
- 大量计算:业务逻辑问题
实战案例:
问题:线上CPU突然100% 排查: 1. top找到Java进程 2. top -Hp找到占用高的线程 3. jstack查看堆栈,发现在执行正则匹配 4. 代码review发现正则表达式有灾难性回溯 解决:优化正则表达式,CPU恢复正常
4. Redis的缓存穿透、缓存击穿、缓存雪崩,如何解决?
三大缓存问题:
1. 缓存穿透
问题描述:
- 查询不存在的数据
- 缓存和数据库都没有
- 大量请求打到数据库
解决方案:
方案1:布隆过滤器
@Autowired
private RedissonClient redisson;
// 初始化布隆过滤器
RBloomFilter<String> bloomFilter = redisson.getBloomFilter("user:bloom");
bloomFilter.tryInit(100000L, 0.01); // 预计元素数,误判率
// 添加数据
bloomFilter.add("user:1001");
// 查询前先判断
public User getUser(String userId) {
if (!bloomFilter.contains("user:" + userId)) {
return null; // 一定不存在
}
// 查缓存和数据库
return getUserFromCacheOrDB(userId);
}
方案2:缓存空值
public User getUser(String userId) {
// 查缓存
String cacheKey = "user:" + userId;
User user = redis.get(cacheKey);
if (user != null) {
return user;
}
// 查数据库
user = userMapper.selectById(userId);
if (user == null) {
// 缓存空值,设置短过期时间
redis.setex(cacheKey, 60, "NULL");
return null;
}
redis.setex(cacheKey, 3600, user);
return user;
}
2. 缓存击穿
问题描述:
- 热点key突然过期
- 大量并发请求同时查数据库
- 数据库压力瞬间增大
解决方案:
方案1:互斥锁
public User getUser(String userId) {
String cacheKey = "user:" + userId;
User user = redis.get(cacheKey);
if (user == null) {
String lockKey = "lock:user:" + userId;
// 尝试获取锁
if (redis.setnx(lockKey, "1", 10)) {
try {
// 双重检查
user = redis.get(cacheKey);
if (user != null) return user;
// 查数据库
user = userMapper.selectById(userId);
redis.setex(cacheKey, 3600, user);
} finally {
redis.del(lockKey);
}
} else {
// 等待后重试
Thread.sleep(50);
return getUser(userId);
}
}
return user;
}
方案2:热点数据永不过期
// 逻辑过期
class CacheData {
Object data;
long expireTime;
}
public User getUser(String userId) {
CacheData cache = redis.get("user:" + userId);
if (cache == null || System.currentTimeMillis() > cache.expireTime) {
// 异步更新
threadPool.execute(() -> {
User user = userMapper.selectById(userId);
CacheData newCache = new CacheData(user,
System.currentTimeMillis() + 3600000);
redis.set("user:" + userId, newCache);
});
}
return cache != null ? (User)cache.data : null;
}
3. 缓存雪崩
问题描述:
- 大量key同时过期
- 或Redis宕机
- 所有请求打到数据库
解决方案:
方案1:过期时间加随机值
// 避免同时过期 int expireTime = 3600 + new Random().nextInt(300); // 3600-3900秒 redis.setex(key, expireTime, value);
方案2:Redis高可用
# 主从复制 + 哨兵模式 # 或Redis Cluster集群
方案3:限流降级
@Sentin
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
Java面试圣经 文章被收录于专栏
Java面试圣经,带你练透java圣经
查看34道真题和解析