恒生电子 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圣经

全部评论

相关推荐

不愿透露姓名的神秘牛友
昨天 10:38
实力求职者:真的绷不住了,第一张霸总人设,第二张求生欲拉满
点赞 评论 收藏
分享
查看34道真题和解析
点赞 评论 收藏
分享
面试时长:37分钟,二面27分钟,hr面10分钟1.问了点个人,学校信息和家庭信息试探稳定性。2.职业规划是什么3.一个成绩表,有学生姓名,课程成绩,课程分数三个列,比如:张三,语文,70;张三,数学,80;写一个sql,去对学生总成绩进行排序。完整复述下你的sql。(昨天湖北信航一模一样的面试题,差点没憋住笑)4.sum()和count()的区别?(一个记录数据行数,一个记录某列数值的总和)5.我看你实习经历有优化线上慢查询的经历,如果你遇到了慢查询,你的排查逻辑是什么?(答的是,先看sql是否需要优化,再看接口逻辑是否需要优化,最后看要不要加中间件,比如mq限流)6.假设有一个字典表,后端要频繁访问字典表并字段转换,你应该怎么优化?(其实就是数据库压力大的问题,加redis缓存数据,减少数据库压力,其实我想再答个mq,但没敢答,不知道对不对)7.使用redis时要注意什么?(八股吟唱了redis基本数据结构的不同场景用法,和缓存三件套的解决思路)8.redis的大key问题怎么解决?(没背,瞎答一通给面试官整笑了,反正不让问题落地上这一块)9.如何保证rabbitmq的不重复消费呢?(我答的是从队列方面,采用消费者确认机制。接口方面,业务要做幂等性处理,并八股吟唱)10.如果我才用ack模式的话,并发场景下如何控制它的不重复消费?(没背,答的是给topic下每个队列绑定对应的消费者,生产者根据消息类型送往对应的队列)11.说下自己的优缺点。12.期望薪资。13.你觉得你和同校同龄人对比是怎样的。-------------------------过了几分钟二面面试官把技术负责人叫来现场hr面。14.说说自己优缺点。15.觉得自己ai用的怎么样。16.说说自己用ai的规范。17.对使用ai的态度是什么样的。18.能接受写前端吗。19.反问环节公司环境不错,至少不是什么野鸡公司,人也挺多的。但是要求进去全栈,且实习生也要加班到9点。二面面试官倒是挺好的,答的好就给予肯定,答偏或者答错了现场引导或者解答
查看16道真题和解析
点赞 评论 收藏
分享
评论
点赞
4
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务