怒刷牛客JAVA面经(4)

本文题目来自于牛客上面的面经分享,会贴上来源帖子
本次题目来源感谢分享
个人想法,结合其他资料整理的,文章有问题希望大佬可以指出,目前正在准备春招,希望上岸🙏🏻

springcloud基本组件

  SpringCloud基本组件有很多,比如服务发现注册、配置中心、消息总线、负载均衡、断路器、数据监控等,其实也就是在SpringBoot的基础上在加组件让他变成一个微服务的框架,其中有五个核心组件需要知道:

  • 注册中心——Eureka:微服务是将一个系统拆分成多个子系统,如果子系统想要将他的服务提供给其他子系统使用,因为不是同一个项目,所以需要一个中介来帮忙,这就是注册中心的作用,将提供给外界的服务注册到注册中心,调用者则只需要告知注册中心调用的是哪个服务,其过程由调用中心去执行并返回结果,调用者不需要知道过程。
  • 负载均衡——Ribbon:因为一般的分布式项目会将一个服务部署多个实例,他可以帮助客户端找到注册中心上的服务实例,并根据一定的负载均衡策略,将请求分发到这些实例中的其中一个。他是集成在客户端,通过在客户端的请求中选择合适的服务实例来实现负载均衡,相比服务端负载均衡,这种方式更加灵活。
  • 熔断器——Hystrix:在调用其他服务的时候,可能服务会出现故障或延迟,这时候如果不去处理它并且还继续调用,可能会导致整个服务卡在这里,随意需要有相对应措施去预防这类情况,熔断器则提供了降级、熔断、限流等策略,防止服务故障在其他系统产生严重后果。
  • 服务调用——Feign:顾名思义,就是用来调用其他服务注册在注册中心的服务,简单来说他就是一个声明式的HTTP客户端。
  • 服务网关——Zuul:主要起到路由的作用,他可以根据请求的路径将请求路由到不同的后端服务,使得在微服务架构中可以更加灵活地管理服务之间的通信。



Eureka和Nacos有什么区别

  从以下三点来探讨(能想到的):

  • 开发商:Eureka是Netflix开发的,Nacos是阿里巴巴开发的,而且Eureka好像停止维护了,而Nacos还在维护。
  • 功能:Eureka主要提供服务注册和发现,而Nacos还可以用来当做配置中心。
  • 机制:
    • 服务提供者和注册中心:Eureka的客户端会主动发送心跳机制,如果短期内收不到心跳机制,则会直接剔除服务。nacos也会根据心跳机制去监听,但是他还会区分临时实例和非临时实例,二者间的区别是:临时实例短期内不发送心跳,则会直接剔除;非临时实例短期内不发送心跳机制,注册中心会主动询问等待,并不会直接删除。
    • 服务调用者和注册中心:Eureka需要客户端主动去拉取服务列表,注册中心不会主动同步;而Nacos会主动同步服务列表。

为什么选择微服务,微服务架构有哪些好处,有哪些缺点

  因为实习公司用的就是微服务,下面分别讲讲好处和缺点:

  • 好处:

    1. 更好的可维护性:由于将一个大的服务拆封成小的服务,每个服务相对来说会变得更加轻量化,维护起来会更加方便,因为只用维护自身的服务。
    2. 更高的容错性:由于每个服务都是单独部署、相对独立的,所以在某个服务宕机后,产生的影响波及小,就算对其他服务有印象,也可以通过熔断器来处理。
    3. 灵活性和可扩展性:由于被拆分成多个服务,各个服务可以按照自己的业务需求进行开发,引入自己需要的依赖,甚至是可以使用不同的语言。
  • 坏处:

    1. 分布式系统的复杂性:微服务架构引入了分布式系统的复杂性,包括服务间通信、服务发现、负载均衡、分布式事务等方面的挑战,使得整体架构不如单体架构简单。
    2. 部署和运维成本增加:由于服务都是单独部署的,所以运维的成本需要增加。
    3. 服务间通信的性能损耗:之前单体项目的时候都是在同一个项目中请求项目中的方法,微服务则是需要从其他服务中调用需要的方法,期间是需要进行网络连接和通信的,避免不了产生性能损耗。
    4. 数据一致性和事务管理:由于微服务架构中的服务是独立部署和运行的,因此在跨服务的操作中可能会面临数据一致性和分布式事务管理的挑战。



为什么要有分布式事务

  因为微服务是将一个大的项目拆分为多个小项目并且单独部署维护,服务之间的调用是需要考虑不确定因素的,例如调用服务的宕机、调用时网络的波动导致的调用失败等,如果不使用事务的话,可能会导致数据的不一致,因为在分布式系统中**,一个事务可能会涉及到多个数据库或者多个服务的操作**,这时候就需要分布式事务而不是普通事务来保障数据的一致性和准确性,只要调用过程中或者自己本身服务有问题,这整个事务都需要回滚。


Kafka有哪些应用场景

  这个最好根据实际项目答吧,使用这种消息中间件无非就是异步处理和流量削峰,建议根据具体项目答,这里提供几个场景:充当消息队列实现订单系统的异步操作、当大量请求将来时用来做流量削峰避免服务崩溃等。


Redis有哪些集群

  盲猜贴主的意思是想表达有哪些集群模式,Redis一共有三种常见的集群模式:主从模式、哨兵模式和集群模式,下面分别简要介绍一下:

  1. 主从模式:
      这个模式是由一个主节点和多个从节点构成,简称一主多从,主节点负责数据的读操作和写操作,而从节点只负责读操作,这样主节点就可以减少读操作,从而减轻节点的压力,把资源更多的放在写操作上。
      但是主从模式有两个缺点:第一就是从节点需要主节点发送写操作指令才会更新数据,如果这是在写操作的时候,用户想从节点索要数据,由于数据没更新完,读到的就可能是旧的数据;第二点就是主节点如果宕机了,需要人工手动处理恢复。
    在这里插入图片描述

图片来源:小林coding

  1. 哨兵模式:
      和主从模式的工作原理类似,但是这里会多一个哨兵节点,用于主节点宕机后,在从节点中选出一个新的主节点,这样就不用人工手动恢复了。
      简要概括一下工作流程:在主节点宕机后,会在从节点中选出一个节点成为新的主节点,一般是通过节点优先级选择,相同则考虑从主节点复制的数据量或者节点ID大小,选择完以后会将之前的主节点变为从节点,并告知其他从节点哪个是新的主节点。
    在这里插入图片描述

图片来源:小林coding大佬

  1. 切片集群模式:
      这个模式主要用在数据量过于庞大的时候,他会将一个集群拆分为 16384 个哈希槽,然后根据数据的key值计算出所属的哈希槽,然后再去访问这个哈希槽所处的具体节点,使用切片集群的节点是不会区分主从节点的,也就是所有节点众生平等,都会根据key值计算出的槽位去做读写操作。

Redis是单线程还是多线程

  经典面试题,直接秒了:
  首先,我们Redis整个程序是多线程的,Redis在执行异步删除(unlink)、持久化操作(AOF刷盘)等其他操作时,是会单独开启线程去执行的,我们说的Redis单线程是值的他的核心执行功能是单线程的,也就是读写操作和客户端的连接这些,也就是“接受客户端指令->解析指令->执行指令->返回客户端数据”这个操作是单线程的。


分片集群和主从集群的区别

  主要区别就是数据分布不同,一个是分主从节点,主节点负责读写操作,从节点只负责读操作,而且当主节点和从节点之间还要处理好数据的一致,需要从节点向主节点拷贝数据;而分片集群是将数据通过一致性哈希算法计算出所在的哈希槽,然后将数据存放在该哈希槽所在的节点中,每个节点都是平等的,部分主从,各自负责各自哈希槽的读写操作。


分片集群有哪些应用场景

  这个问题只能想到数据过于庞大和并发请求较多的时候,希望有大佬能够给出更多的场景🙏🏻


缓存穿透,缓存雪崩的产生原因以及解决方案

  经典三刺客,三个都给他答了:

  • 缓存穿透:
    • 原因:存在部分数据不存在于数据库,也不存在于Redis中,但依旧进行大量访问,使得每次请求都会去访问数据库,导致数据库崩溃的场景,就叫做缓存穿透,简单来说就是大量访问不存在的数据。
    • 解决方案:
      1. 将不存在的数据设置为null值存放在Redis中,这样就可以不用访问数据库,防止数据库崩溃,并且可以通过从Redis获取到null值做下一步处理,例如提醒该数据不存在。
      2. 可以使用布隆过滤器过滤的方式,将不存在的数据通过过滤器在访问数据库前就筛选出来,不存在的情况下就可以直接告诉前端数据不存在了。
      3. 无效方法:使用锁操作,因为数据库扛不住大量的并发请求,那么我们只要上锁,每次访问数据库都需要加锁,这样就不会击垮数据库,但是会击垮整个服务,所以用不得这个方法。
  • 缓存雪崩:
    • 原因:Redis中大量的数据在同一时间过期,这时又有大量的请求需要访问这些数据,由于不存在在Redis中,只能去访问数据库,大量的访问导致数据库崩溃。
    • 解决方案:
      1. 将数据的过期时间打散,不让他们在同一时间过期,这样子就不会出现缓存雪崩的情况。
      2. 使用多级缓存,比如可以多加一层本地缓存,当redis失效以后,会先去访问本地缓存,如果存在就返回本地缓存的数据,这样也可以避免直接访问数据库。
  • 缓存击穿:
    • 原因:Redis中某个热点数据过期了,但是就在这一时刻又有大量的请求来访问这个热点数据,由于Redis中以及将他删除了,所以会去访问数据库,导致数据库崩溃。
    • 解决方案:
      1. 对热点数据不设置过期时间,设置逻辑过期,也就是在这个数据中再设置一个属性,用来表示这个数据什么时候过期,当数据过期时,当前查询线程需要去多开一个线程更新数据,然后返回过期数据,这样子就能避免数据库的大量访问,但是可能会存在数据暂时为过期数据的情况。
      2. 使用锁,也就是查不到数据就上锁去添加。



http和https的区别

  简单来说,就是http是明文传输,不安全,而https在http的基础上,添加了SSL/TLS协议来对通信内容进行加密和认证,从而使数据传输变得安全,这一点是最主要的区别,其他细节就是二者端口不同,http默认是80,https默认是443。


讲一下非对称加密算法,对称加密和非对称加密算法分别有哪些

  这里就直接分对称和非对称来讲述:

  • 非对称加密算法:
      非对称加密算法是一种加密方式,使用了一对不同的密钥(公钥和私钥),其中一个用于加密,另一个用于解密。这种加密算法是非常安全的,因为即使知道了公钥,也很难通过公钥推导出私钥。
      主要的算法有RSA、ECC、DSA等,其中最常用的是RSA。
  • 对称加密算法:
      对称加密算法则使用相同的密钥来加密和解密数据,因此被称为“对称”。这意味着加密和解密双方必须共享同一个密钥。对称加密算法通常比非对称加密算法更快速,但是在密钥管理方面存在挑战。
      主要的算法有DES、3DES、AES等,其中最常用的是AES。


mysql事务的隔离级别,默认隔离级别是什么

  四种隔离级别:串行化、可重复读、读已提交、读未提交,下面讲讲各自的区别

  • 读未提交(READ_UNCOMMITTED,简称RU):允许一个事务读取另一个事务尚未提交的数据,这可能导致脏读、不可重复读和幻读问题
  • 读已提交(READ COMMITTED,简称RC):确保一个事务只能读取到已经提交的数据,避免了脏读问题,但仍然可能发生不可重复读和幻读问题
  • 可重复读(REPEATABLE READ,简称RR):确保在同一个事务中多次读取相同数据时,结果保持一致,避免了脏读和不可重复读问题,但仍然可能发生幻读问题。(但是mysql通过MVCC基本上防止了幻读问题)
  • 串行化(SERIALIZABLE):最高的隔离级别,确保事务彼此之间不会产生干扰,可以避免脏读、不可重复读和幻读问题,但会带来较高的性能开销。

  我们可以通过指令:**select @@transaction_isolation;**查询到数据库使用的隔离级别,默认使用的是可重复读
在这里插入图片描述


jdk和spring以及工作中常用的设计模式

  介绍几个最常见的:

  • 单例模式:指的是一个进程中某个类只存在一个对象,在Spring中默认的Bean作用域就是singleton单例的。
  • 代理模式:指的是通过创建一个代理对象来控制对另一个对象的访问,也就是说我们不是直接访问这个对象本身,而是用过代理对象这个中间人来间接访问他,在Spring中使用到的AOP就用到了动态代理。
  • 模板模式:指的是一个抽象类公开定义了执行它的方法的方式/模板。它的子类可以按需要重写方法实现,但调用将以抽象类中定义的方式进行。简单来说就是一个抽象类定义了一个模板,并且提供了一些供使用者定制的方法,使用者可以重写这些方法实现自己想要的效果,但是具体模板方法以及内部机制都是由提供的模板负责的。在Spring中我们可以看到类似JDBCTemplate这一类XXXXTemplate的都是使用的模板模式。
  • 工厂模式:指的是通过一个类似工厂的类,提供了一种创建对象的接口,但允许子类决定实例化的类,简单来说就是对象不由调用者主动去实例化创建,而是通过工厂类来创建。在Spring中使用到的BeanFactory 和 ApplicationContext就是用的工厂模式,创建Bean就是通过工厂,也就是我们说的容器去创建的。
  • 适配器模式:指的是将现成的对象通过适配变成我们需要的接口,让原本接口不兼容的类可以合作。例如在Spring中使用到的HandlerAdapter,他的作用就是将不同类型的处理器,如Controller类,@RequestMapping这些不同类型的处理器适配成能够让SPringMVC识别的处理器。
  • 装饰模式:指的是将对象封装在装饰器对象中,然后在运行时动态地添加功能,实现了对对象行为的扩展。例如我们使用的IO输入流,可以通过BufferedInputStream将普通的输入流装饰为带缓存的输入流。

  一共有23种设计模式,多多少少都有在JDK和Spring中使用到过,这里就不一一列举了。


什么是aop

  直接秒了,面向切面编程,英文全称Aspect-Oriented Programming,主要是用来分离横切关注点,也就是切面,并将其封装到单独的组件中,也就是常用的、普遍、和业务无关、可复用的功能,比如日志记录、事务控制等,就可以使用AOP。


单例模式有什么好处

  首先可以减少内存的开销,因为一个类只创建一个对象,那么占用的内存肯定就小(对比与创建多个相同类的情况下);其次他可以当做一个全局访问点,实现数据共享的功能。


SpringMvc执行流程

在这里插入图片描述

图片来源:javaguide

  下面讲讲具体流程:

  1. 前端发送http请求到后端被DispatcherServlet拦截请求。
  2. DispatcherServlet通过HandlerMapping,根据请求的方法和URL来找到对应的控制器,也就是handler,并会将请求涉及到的拦截器和控制器一起封装返回给DispatcherServlet。
  3. DispatcherServlet 调用 HandlerAdapter适配器执行 Handler (也就是上面提到的适配器模式)。
  4. 在执行完请求以后,会返回一个ModelAndView,简单来说就是数据和视图给到DispatcherServlet。(这里的视图一般都是返回视图的名称)
  5. DispatcherServlet 使用 ViewResolver 来将视图名称解析为实际的视图对象。ViewResolver 是用于解析视图的策略接口,它可以根据视图名称解析出对应的视图对象。
  6. 解析出实际的视图后会将Model,也就是数据渲染到实际视图上。
  7. 最后将视图通过http请求返回给前端。



ConcurrentHashMap底层原理

  其实ConcurrentHashMap就是一种线程安全的HashMap,其中大部分处理都和HashMap类似,只是需要保障线程安全,使用上了CAS和锁机制,我们从1.7和1.8以后的版本分别将将:

  • 1.7版本:
      底层是由Segment 数组结构和 HashEntry 数组结构组成的,HashEntry我们可以类似看成HashMap,而Segment可以看成一共可以让多少个线程同时访问ConcurrentHashMap,因为在1.7版本之前,上锁是给每个Segment节点上锁,那么我们初始化的时候设置了多少个Segment(默认16),后面是不是变的,会变的只会是每个Segment中的HashEntry。
    在这里插入图片描述

图片来源:javaguide

  • 1.8版本:
      底层抛弃了Segment数组+HashEntry数组,改成了Node 数组 + 链表 / 红黑树,保障线程安全是通过Node + CAS + synchronized保障的,锁的粒度降低,这样子就可以有更大的并发度,而不是一开始就定死。
    在这里插入图片描述

图片来源:javaguide



ConcurrentHashMap什么情况下会锁全表

  锁全表一般出现在jdk1.7版本,是在size方法中,通过源码来讲解:


	//在加锁前重试的次数  
	static final int RETRIES_BEFORE_LOCK = 2;  
    
    public int size() {
        final Segment<K,V>[] segments = this.segments;  // 获取Segment数组
        int size;   // 初始化size
        boolean overflow; // 是否溢出
        long sum;     // 计算modCount修改次数
        long last = 0L;   // 计算上一次的 sum,主要是防止并发状态下出现计算次数不同
        int retries = -1; // 重试次数
        try {
            for (;;) {
                // 如果超过了一定的重试次数,就同时把所有段都锁起来
                if (retries++ == RETRIES_BEFORE_LOCK) {
                    for (int j = 0; j < segments.length; ++j)
                        ensureSegment(j).lock(); // force creation
                }
                sum = 0L;
                size = 0;
                overflow = false;
                // 遍历每一个段
                for (int j = 0; j < segments.length; ++j) {
                    // 获取段
                    Segment<K,V> seg = segmentAt(segments, j);
                    // 计算每个段中的HashEntry个数
                    if (seg != null) {
                        sum += seg.modCount;
                        int c = seg.count;
                        if (c < 0 || (size += c) < 0)
                            overflow = true;
                    }
                }
                // 如果发现当前的modCount和上一次一样,说明没有线程安全问题
                if (sum == last)
                    break;  // 就可以结束了
                last = sum;   // 如果发现不一样,说明有线程修改了
            }
        } finally {
            // 解锁
            if (retries > RETRIES_BEFORE_LOCK) {
                for (int j = 0; j < segments.length; ++j)
                    segmentAt(segments, j).unlock();
            }
        }
        // 溢出那就是到达了最大长度,直接返回Integer.MAX_VALUE
        return overflow ? Integer.MAX_VALUE : size;
    }

  简单来说,就是默认情况下,他还是回向好的方面想,认为获取大小的时候不会有线程参与修改表数据,如果发现有改的话,超过两次就会锁全表。
  而在1.8中,由于抛弃了Segment,他在自身多维护了两个成员变量,baseCount(基本计数,也就是默认的数据个数)和countCells(CounterCell数组,在addCount中可能出现添加失败,因为使用的时CAS,这时就会使用这个数组统计),计数的时候最后会调用一下方法:

//CounterCell 结构
static final class CounterCell {
        volatile long value;
        CounterCell(long x) { value = x; }
}

final long sumCount() {
        CounterCell[] as = counterCells; CounterCell a;
        long sum = baseCount;
        // 如果计数数据还是为null,代表baseCount增加都成功了
        if (as != null) {
            for (int i = 0; i < as.length; ++i) {
                if ((a = as[i]) != null)
                    sum += a.value;
            }
        }
        return sum;
    }



ThreadLocal底层原理,如何保证线程安全

  ThreadLocal底层其实就是在每个线程中都会有一个类似于表结构的ThreadLocalMap,去维护ThreadLocal的数据,可以看一下图片:
在这里插入图片描述

图片来源:javaguide

  其中ThreadLocal会被当做key值,而具体每个线程的ThreadLocal的值就是业务代码中自己set的值。
  至于线程安全,是因为这个ThreadLocalMap本身就是每个线程私有的,ThreadLocal对于每个线程来说也是线程自己的一份副本变量,我们去调用get方法的时候也只会去get当前调用线程的表去查找,所以不存在线程安全问题。(但是会有内存泄露问题,当key是弱引用时)


线程池有什么作用

  线程池是一种管理和重用线程的机制,它可以有效地管理线程的生命周期、提高线程的利用率,并且可以避免不必要的线程创建和销毁开销


如何创建线程池

  最好是使用自定义方法创建,也就是使用ThreadPoolExecutor构造方法创建,或者使用Executors工具类提供的方法,但是提供的方法创建的线程池都有可能出现问题,例如OOM问题,所以只推荐使用构造方法,并且构造方法还可以设置线程池的名字,在出现问题的时候可以更好的定位问题。


线程池七大参数

  老生常谈的问题了:

  • 核心线程数——corePoolSize:指的是线程池中一直存货不会被销毁的线程。
  • 最大线程数——maximumPoolSize:指的是线程池能创建的最大线程数量,包括核心线程
  • 线程存活时间——keepAliveTime:指的是普通创建的线程在没被使用的时候,最长的存活时间。
  • 存活时间单位——unit :顾名思义,上面存活时间的单位
  • 工作队列——workQueue:这个和线程池的流程有关,如果核心线程全部被占用,那么新的任务会先被加入到队列中。
  • 线程工厂——threadFactory :创建线程的地方。
  • 拒绝策略——handler:指的是线程到达最大最大数量的时候,并且队列也满的时候,对于新任务的处理方式,有四种:
    1. AbortPolicy:丢弃任务并抛出 RejectedExecutionException 异常。(默认这种)
    2. DiscardPolicy:丢弃任务,但是不抛出异常
    3. DiscardOldestPolicy:丢弃队列最前面的任务,也就是最老的任务,然后重新尝试执行任务(重复此过程) 。
    4. CallerRunsPolicy:谁调用,谁处理。



线程池执行流程

在这里插入图片描述

图片来源:链接地址

  1. 判断核心线程池是否已满,如果不是,则创建线程执行任务
  2. 如果核心线程池满了,判断队列是否满了,如果队列没满,将任务放在队列中
  3. 如果队列满了,则判断线程池是否已满,如果没满,创建线程执行任务
  4. 如果线程池也满了,则按照拒绝策略对任务进行处理。



高并发秒杀如何设计

  看文章吧:秒杀场景

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值