温馨提示:距离2024年结束还剩18天,剩余约为4.92%...

转载

Java开发高频面试题(下)

21. 延迟队列最后一秒钟跳转支付界面然后库存进行回滚用户已经支付的问题?

微服支付文档:

  1. 统一下单 传入订单失效时间(下单时生成二维码时,把失效时间一起传给微信)
  2. 延迟队列,比失效时间多10秒后再处理(取消订单前,查询订单获取订单最新支付的状态,再决定是否更改订单状态)调用支付统一下单接口,传入的主要参数(订单号,商品名称,回调地址,价格,订单失效时间time_expire),我们自己的失效时间可以延迟10秒,如果没有接收的支付平台的消息,则调用查询订单API,主要参数为商户id、子商户id、微信支付订单号。

22. Spring的三级缓存与循环依赖的解决?

22.1 三级缓存

名称 源码名称 数据类型 作用
一级缓存 singleObjects ConcurrentHashMap(256) 单例池,缓存已经经历生命周期初始化完成的bean对象
二级缓存 earlySingletonObjects HashMap(16) 缓存早期的bean对象(生命周期还没走完),如:只完成构造方法
三级缓存 singletonFactories ConcurrentHashMap(16) 缓存ObjectFactory,用来创建某个对象

22.2 Spring是如何解决循环依赖的?

spring循环依赖是指两个或更多个Bean之间存在相互依赖的情况,例如A依赖于B,而B依赖于A

  1. 实例化A,得到原始对象A,并且同时生成一个原始对象A对应的ObjectFactory对象
  2. 将ObjectFactory对象存储到三级缓存中
  3. 需要注入B,发现B对象在一级缓存和二级缓存都不存在,并且三级缓存中也不存在B对象所对应的ObjectFactory对象
  4. 实例化B,得到原始对象B,并且同时生成一个原始对象B对应的ObjectFactory对象,然后将该ObjectFactory对象也存储到三级缓存中
  5. 需要注入A,发现A对象在一级缓存和二级缓存都不存在,但是三级缓存中存在A对象所对应的ObjectFactory对象
  6. 通过A对象所对应的ObjectFactory对象创建A对象的代理对象
  7. 将A对象的代理对象存储到二级缓存中,删除三级缓存中的A的ObejectFactory对象
  8. 将A对象的代理对象注入给B,B对象执行后面的生命周期阶段,最终B对象创建成功
  9. 将B对象存储到一级缓存中,同时删除三级缓存中B的ObejctFactory对象
  10. 将B对象注入给A,A对象执行后面的生命周期阶段,最终A对象创建成功,将二级缓存的A的代理对象存储到一级缓存中

注意:

  1. 后面的生命周期阶段会按照本身的逻辑进行AOP, 在进行AOP之前会判断是否已经进行了AOP,如果已经进行了AOP就不会进行AOP操作了。
  2. singletonFactories : 缓存的是一个ObjectFactory,主要用来去生成原始对象进行了AOP之后得到的代理对象,在每个Bean的生成过程中,都会提前暴露一个工厂,这个工厂可能用到,也可能用不到,如果没有出现循环依赖依赖本bean,那么这个工厂无用,本bean按照自己的生命周期执行,执行完后直接把本bean放入singletonObjects中即可,如果出现了循环依赖依赖了本bean,则另外那个bean执行ObjectFactory提交得到一个AOP之后的代理对象(如果没有AOP,则直接得到一个原始对象)。

22.3 解决循环依赖的方案

三种解决方案:

  1. 通过构造函数注入解决循环依赖:当Bean之间有循环依赖时,使用构造函数注入可以解决问题。因为Spring容器可以通过构造函数来创建对象,并将它们注入到依赖项中。这种方式需要确保依赖关系是通过构造函数来注入的,而不是通过Setter方法。
  2. 通过Setter方法注入解决循环依赖:另一种解决方案是使用Setter方法注入。在这种情况下,Spring容器会先创建所有Bean的实例,然后通过Setter方法注入依赖项。如果循环依赖,容器将返回一个代理对象,稍后再将实际对象注入到代理中。
  3. 使用@Lazy注解解决循环依赖:@Lazy注解可以用于延迟注入依赖项。当一个Bean被标记为延迟注入时,Spring容器会在需要时才创建它。这种方式可以解决循环依赖的问题,因为每个Bean都是在需要时才被创建的,而不是在启动时一次性创建所有Bean。

23. 继承?封装?多态?接口和抽象类的区别?

23.1 面向对象编程的3大特性

  1. 封装是指将对象的状态和行为包装在一起,对外部隐藏对象的实现细节,只暴露出一些公共的接口,通过这些接口访问对象的属性和方法。封装的好处是增强了对象的安全性和可靠性,同时也方便了对象的使用和维护。
  2. 继承是指一个类(称为子类或派生类)可以继承另一个类(称为父类或基类)的属性和方法。子类可以通过继承获得父类的属性和方法,并且还可以在此基础上增加自己的属性和方法。
  3. 多态是指同一种行为具有多种不同的表现形式或状态。具体来说,多态性可以通过方法的重载和覆盖、接口和抽象类等机制实现。多态性的好处是增强了程序的灵活性和可扩展性,同时也方便了代码的复用和维护。

23.2 接口和抽象类区别

  1. 接口和抽象类都是用来实现多态性的机制。
  2. 接口是一种纯抽象的类型,只包含方法声明而没有实现。
  3. 通过实现接口,一个类可以具备接口中声明的方法,从而实现多态性。
  4. 抽象类是一种包含抽象方法的类,抽象方法只有声明而没有实现。
  5. 通过继承抽象类,子类必须实现父类的抽象方法,从而实现多态性。
  6. 接口和抽象类的区别在于:接口只包含方法的声明而没有实现,而抽象类包含抽象方法和具体方法;一个类可以实现多个接口,但只能继承一个抽象类。

使用场景

  1. 抽象类用于表示一类事物,抽象概念产品描述,通过用于封装工具,父类封装好,子类直接用。AQS(AbstractQueuedSynchronizer 锁的始祖)
  2. 接口表示一种能力、规范、约束,实现接口表示具有某种能力。JDK1.8有默认方法有方法体(方法声明前加default)。

24. 单例模式?(其它设计模式)

24.1 单例模式

懒汉式,饿汉式、双检锁(DCL)、内部类、枚举

24.2 四大原则

构造方法私有、以静态方法返回或枚举返回实例、确保只有一个实例、确保反序列时不会重新构建对象。

24.3 为什么使用单例模式

  1. 节省资源:在某些情况下,创建对象的开销比较大,如果每次都创建新的对象,会消耗大量的系统资源。使用单例模式可以在应用程序中只创建一个实例,避免了重复创建对象的开销,从而节省了系统资源。
  2. 简化代码:如果某个类需要在应用程序中频繁使用,那么在每个地方都创建该类的实例会导致代码变得复杂。使用单例模式可以将类的实例化过程封装在一个单独的类中,并提供一个全局访问点,从而简化了代码结构。
  3. 维护全局状态:在某些情况下,需要在应用程序中维护一个全局状态,例如日志对象或配置文件对象。使用单例模式可以保证该类只有一个实例,从而保证了全局状态的一致性。
  4. 保证线程安全:在多线程环境下,如果多个线程同时访问一个对象,并且该对象没有进行任何线程安全的处理,那么可能会出现数据竞争和其他线程安全问题。使用单例模式可以保证该类只有一个实例,并且提供了一个全局访问点,从而避免了线程安全问题。
  5. 使用单例主要保证:
    • 懒加载
    • 线程安全
    • 可以使用反射破坏

25. stream的常用API

  1. filter:过滤集合中不符合条件的元素。
  2. map:将集合中的每个元素映射为另一个元素。
  3. flatMap:将集合中的每个元素映射为多个元素,并将结果合并成一个流。
  4. sorted:对集合进行排序。
  5. distinct:去除集合中的重复元素。
  6. limit:限制集合中元素的数量。
  7. skip:跳过集合中的前几个元素。
  8. reduce:将集合中的元素逐个聚合成一个结果。
  9. collect:将集合中的元素收集到一个集合中。
  10. forEach:对集合中的每个元素执行一个操作。
  11. count:计算集合中元素的数量。
  12. anyMatch:判断集合中是否存在满足条件的元素。
  13. allMatch:判断集合中所有元素是否都满足条件。
  14. noneMatch:判断集合中是否没有元素满足条件。
  15. findFirst:查找集合中第一个符合条件的元素。
  16. findAny:查找集合中任意一个符合条件的元素。

26. 死锁的解决方法

死锁是指两个或多个进程或线程无限期地等待彼此释放所占用的资源 避免循环依赖:死锁通常是由于多个线程对资源的互相依赖导致的。因此,可以尝试在设计时避免循环依赖,从而避免死锁的发生。

  1. 统一获取锁的顺序:当多个线程需要获取多个锁时,可以规定所有线程必须按照同样的顺序获取锁,从而避免不同线程之间的锁竞争导致的死锁。
  2. 设置超时时间:如果一个线程在一定时间内无法获取所需的锁,就会自动放弃该锁并退出,从而避免死锁的发生。这个方法可以通过设置锁的超时时间来实现。
  3. 使用资源分配图:资源分配图可以帮助我们理解资源之间的依赖关系,并且可以通过检查图中的环路来检测是否有死锁的情况发生。
  4. 引入抢占机制:抢占机制可以中断正在运行的线程并强制释放它所占用的资源,从而避免死锁的发生。但是需要注意,这种方法可能会导致程序执行的不确定性。

27. Spring中有哪些设计模式?你的项目中用了什么设计模式?

27.1 Spring中的设计模式

设计模式 在哪里用到 描述
工厂模式 BeanFactory和ApplicationContext
单例模式 Spring容器中的Bean
代理模式 Spring AOP 基于反射实现动态代码
模板方法模式 Spring中以Template结尾的类 使用继承的方式实现
观察者模式 Spring事件驱动模型(发布订阅事件)
适配器模式 AOP中的AdvisorAdapter MVC中的HandlerAdapter
装饰器模式 Spring中的含有Wrapper和Decorator的类
策略模式 资源访问Resource接口
链式模式 springMVC中的拦截器

28. 如何防止重复提交

使用拦截器|过滤器+Redis来解决

  1. 获取请求对象Request,获取请求路径与请求参数,获取登录用户id
  2. 以请求路径+参数+登录用户id为key,使用字符串的incr命令,自增长加1,根据操作后的返回值判断
  3. 如果返回1,则通过expire命令设置这个key的有效时间为15s(不允许15s内重复提交)
  4. 如果返回值>1,则代表之前用户已经提交过了,则拦截用户请求

使用字符串的incr,由于单次incr的操作具有原子性,每次操作后的值一定会不一样,则是自增长的,就可以通过是否>1来判断它增长了几次,也就是提交了几次。 expire超时失效:如果超过15S,key会自动消失,那时用户又可以提交请求了。

29. 如何解决消息重复消费问题

消息重复消费无可避免,它是一种消息可靠性的保证。因此当消息重复消费时,只要做幂等处理即可

  1. 发送时,给每个消息一个唯一标识,当要开始处理消息时,先执行保存到redis或数据库中。redis中setnx如果数据已存在会返回0,不存在返回1。数据库则使用唯一索引,如果数据存在表中,则插入数据时报错
  2. 给要更新的数据,设置更新的条件(乐观锁方式)。如:update set version=version+1, ... where version=查询时的版本。或更新时使用update table set status=1 where status=0。看返回的affected row的值,如果是1代表没处理过,返回0代表处理过了。
  3. 给1个队列的1个消费者消费,消费前先查询判断是否处理过,如是处理过则不处理(status!=0)。处理后马上更新状态为其它值。即只处理某种状态的数据,其它不处理.

30. 如何优化接口

则检查接口经过的每个环节所花费的时长,看哪个环节花的时间最长。

  • 如果是业务代码:则看是否能使用异步处理(异步线程,或异步mq),异步多线程合并批量处理

  • 如果是数据库响应过长:就把sql语句拿来做 执行计划分析。采用sql优化方案进行优化。

    • sql优化:索引优化:是否使用了索引列,索引是否正确使用。如果没索引,则创建相应索引,优先使复合索引(索引覆盖,索引下推,减少回表可能)
    • 分表法:
      • 水平拆:如果表中数据近千万级别,则拆用分表
      • 垂直拆:如果表中列过多,数据量不大,但查询慢,则根据字段使用频率拆分。
    • 只查询需要的列
    • 注意深度查询问题,限制页码大小,每页大小

31. Redis主从复制原理

31.1 全量同步

  1. slave节点向master请求增量同步(replId与offset)
  2. master节点判断replid,发现不一致,拒绝增量同步
  3. master将完整内存数据生成RDB,发送RDB到slave
  4. slave清空本地数据,加载master的RDB
  5. master将RDB期间的命令记录在repl_baklog,并持续将log中的命令发送给slave
  6. slave执行接收到的命令,保持与master之间的同步

31.2 增量同步

  1. slave节点向master请求增量同步(replId与offset)
  2. master节点判断replid,如果不是第一次则回复Continue
  3. master去repl_baklog中获取slave发来的offset之后的数据,将offset之后的命令发送给slave,slave执行命令完成同步

31.3 repl_backlog原理

repl_backlog是一个环形数组,大小16KB,master将新产生的数据存放在内存中,并且也存在repl_backlog中,并且每次更新都给它打上一个offset标记,每次slave同步都同步到最新的offset,在同步的间隔期间,会产生新的数据,offset会变大,salve每次同步的数据都是原来的数据与新的offset之间的数据

当master产生的新数据超过16kb时,会从头开始覆盖原来的数据,如果salve集群宕机过久,导致主从数据的offset(从小到大一直增长)差值过大(超过了16K的数据),master会拒绝增量同步,转而变为全量同步;反之则执行增量同步时。

  • 作者:CZC(关于作者)
  • 发表时间:2024-10-20 10:50
  • 版权声明
  • 评论区:

    留言