友邦咨询(成都)- Java开发 二面 面经

1. 详细介绍一下你最有成就感的项目,技术架构是怎样的?

我参与开发过一个在线教育平台的后端系统。这个项目采用微服务架构,主要技术栈包括:

技术架构:

  • 后端框架:Spring Boot + Spring Cloud
  • 服务注册与发现:Nacos
  • 网关:Spring Cloud Gateway
  • 负载均衡:Ribbon
  • 熔断降级:Sentinel
  • 远程调用:OpenFeign
  • 数据库:MySQL 8.0(主从复制)+ Redis
  • 消息队列:RabbitMQ
  • 对象存储:阿里云OSS
  • 搜索引擎:Elasticsearch

我主要负责课程管理模块和订单支付模块。课程管理涉及课程的增删改查、分类管理、课程搜索等功能。订单支付模块对接了支付宝和微信支付,需要处理订单创建、支付回调、订单状态更新等流程。

最有挑战的是处理支付回调的幂等性问题,因为第三方支付平台可能会重复发送回调通知。我使用Redis的分布式锁和订单状态机来保证幂等性,确保订单不会被重复处理。

这个项目让我对微服务架构有了深入理解,也积累了处理分布式场景下常见问题的经验。

2. HashMap的底层实现原理,JDK 1.7和1.8有什么区别?

HashMap底层是数组+链表+红黑树的结构。

基本原理:

  • 通过key的hashCode计算hash值,再通过hash值确定在数组中的位置
  • 如果该位置没有元素,直接放入
  • 如果有元素(hash冲突),采用链表或红黑树存储
  • 当链表长度超过8且数组长度大于64时,链表转为红黑树
  • 当红黑树节点少于6时,退化为链表

JDK 1.7和1.8的主要区别:

数据结构:1.7是数组+链表,1.8引入了红黑树,优化了hash冲突严重时的查询性能,从O(n)提升到O(log n)。

插入方式:1.7使用头插法,多线程环境下扩容可能形成环形链表导致死循环;1.8使用尾插法,避免了这个问题。

扩容时机:1.7是先扩容再插入,1.8是先插入再扩容。

hash算法:1.8的hash算法更简单,只做了一次异或运算,性能更好。

扩容优化:1.7需要重新计算所有元素的位置;1.8通过高位运算,元素要么在原位置,要么在原位置+旧容量的位置,不需要重新计算hash。

3. ConcurrentHashMap的实现原理,1.7和1.8有什么区别?

ConcurrentHashMap是线程安全的HashMap,性能比Hashtable好很多。

JDK 1.7实现:

  • 采用分段锁(Segment)机制,每个Segment继承自ReentrantLock
  • 默认16个Segment,理论上支持16个线程并发写
  • 每个Segment内部是一个类似HashMap的结构
  • 读操作不加锁(使用volatile保证可见性),写操作只锁当前Segment
  • size()方法需要遍历所有Segment,先尝试不加锁统计,如果发现有修改则加锁重新统计

JDK 1.8实现:

  • 取消了Segment分段锁,采用CAS + synchronized实现更细粒度的锁
  • 锁的粒度是数组的每个位置(Node),并发度更高
  • 数据结构改为数组+链表+红黑树,与HashMap 1.8一致
  • 使用CAS操作进行无锁的插入和更新
  • 只有在hash冲突时才使用synchronized锁住链表或红黑树的头节点
  • size()方法使用baseCount + counterCells数组来统计,性能更好

1.8的实现更加高效,锁的粒度更细,并发性能更好,代码也更简洁。

4. 说说MySQL的索引优化策略和最佳实践

索引优化策略:

选择合适的字段建索引:

  • where、order by、group by、join on的字段
  • 区分度高的字段(重复值少)
  • 字段长度小的优先

联合索引设计:

  • 遵循最左前缀原则
  • 区分度高的字段放在前面
  • 范围查询的字段放在最后

避免索引失效:

  • 不在索引列上使用函数或表达式
  • 避免隐式类型转换
  • like查询不以%开头
  • 避免使用!=、<>、not in
  • 注意or条件,确保所有字段都有索引

覆盖索引:查询的字段都在索引中,避免回表,性能最优。

索引下推:MySQL 5.6引入,在索引遍历过程中就过滤掉不符合条件的记录,减少回表次数。

前缀索引:对于长字符串字段,可以只索引前几个字符,节省空间。

索引维护:

  • 定期分析表(ANALYZE TABLE)更新索引统计信息
  • 删除冗余和重复的索引
  • 监控慢查询日志,针对性优化

最佳实践:

  • 单表索引数量不要太多(一般不超过5个),影响写入性能
  • 小表不需要建索引,全表扫描更快
  • 对于频繁更新的字段,谨慎建索引
  • 使用EXPLAIN分析执行计划,确保索引生效

5. 介绍一下Spring Bean的生命周期

Spring Bean的生命周期主要包括以下阶段:

实例化(Instantiation):

  • Spring容器通过反射调用Bean的构造方法创建对象

属性赋值(Populate):

  • 通过依赖注入,为Bean的属性赋值
  • 包括@Autowired、@Value等注解的处理

初始化前(BeanPostProcessor.postProcessBeforeInitialization):

  • 执行BeanPostProcessor的前置处理方法
  • 比如@PostConstruct注解的方法就在这个阶段执行

初始化(Initialization):

  • 如果Bean实现了InitializingBean接口,调用afterPropertiesSet方法
  • 如果配置了init-method,调用指定的初始化方法

初始化后(BeanPostProcessor.postProcessAfterInitialization):

  • 执行BeanPostProcessor的后置处理方法
  • AOP代理就是在这个阶段创建的

使用(In Use):

  • Bean已经准备就绪,可以被应用程序使用

销毁前(@PreDestroy):

  • 容器关闭前,执行@PreDestroy注解的方法

销毁(Destruction):

  • 如果Bean实现了DisposableBean接口,调用destroy方法
  • 如果配置了destroy-method,调用指定的销毁方法

关键扩展点:

  • BeanFactoryPostProcessor:在Bean实例化之前修改Bean定义
  • BeanPostProcessor:在Bean初始化前后进行增强
  • Aware接口:让Bean感知到Spring容器(如BeanNameAware、ApplicationContextAware)

了解Bean的生命周期有助于在合适的时机进行扩展和定制。

6. 分布式锁有哪些实现方式?各有什么优缺点?

常见的分布式锁实现方式:

基于Redis实现:

方案一:SETNX + EXPIRE

  • 使用SETNX设置key,成功则获得锁
  • 设置过期时间防止死锁
  • 问题:SETNX和EXPIRE不是原子操作,可能导致死锁

方案二:SET key value NX EX seconds

  • Redis 2.6.12后支持,原子操作
  • value设置为唯一标识(如UUID),释放锁时校验,防止误删
  • 使用Lua脚本保证释放锁的原子性
  • 问题:主从复制异步,主节点宕机可能导致锁丢失

方案三:Redisson

  • 封装了分布式锁的实现,支持可重入锁
  • 使用watchdog机制自动续期,防止业务执行时间过长导致锁过期
  • 支持RedLock算法,解决主从切换问题
  • 推荐使用

基于Zookeeper实现:

  • 利用临时顺序节点实现
  • 客户端创建临时顺序节点,序号最小的获得锁
  • 其他客户端监听前一个节点,前一个节点删除时获得锁
  • 优点:可靠性高,不会因为网络问题导致锁丢失;支持阻塞等待
  • 缺点:性能不如Redis;依赖Zookeeper集群

基于数据库实现:

  • 方案一:使用唯一索引,插入成功则获得锁
  • 方案二:使用for update行锁
  • 优点:实现简单,不需要额外组件
  • 缺点:性能差,不适合高并发场景;可能有死锁风险

选择建议:

  • 性能要求高:Redis + Redisson
  • 可靠性要求高:Zookeeper
  • 简单场景:数据库实现

7. 如何保证消息队列的消息不丢失?

消息丢失可能发生在三个阶段,需要分别处理:

生产者端丢失:

问题:消息发送到MQ前,网络故障或MQ宕机导致消息丢失。

解决方案:

  • 开启生产者确认机制(Publisher Confirm)
  • RabbitMQ:使用confirm模式或事务模式
  • R

剩余60%内容,订阅专栏后可继续查看/也可单篇购买

Java面试圣经 文章被收录于专栏

Java面试圣经,带你练透java圣经

全部评论

相关推荐

评论
2
3
分享

创作者周榜

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