面试题总结5

面试问题总结

1、fail-safe和fail-fast机制

Fail-fast

表示快速失败,在集合遍历过程中,一旦发现容器中的数据被修改了,会 立刻抛出ConcurrentModificationException 异常,从而导致遍历失败

java.util 包下的集合类都是快速失败机制的, 常见的的使用 fail-fast 方式遍历的容器有 HashMap 和 ArrayList 等。

在这里插入图片描述
修改代码示例

import java.util.ArrayList;
import java.util.List;
import java.util.ListIterator;

public class ModifyListExample {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("A");
        list.add("B");
        list.add("C");

        ListIterator<String> iterator = list.listIterator();

        while (iterator.hasNext()) {
            String element = iterator.next();
            System.out.println(element);

            // 修改元素
            iterator.set(element + "-Modified");
        }

        System.out.println("Modified List: " + list);
    }
}

删除代码示例

import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

public class ModifySetExample {
    public static void main(String[] args) {
        Set<String> set = new HashSet<>();
        set.add("A");
        set.add("B");
        set.add("C");

        Iterator<String> iterator = set.iterator();

        while (iterator.hasNext()) {
            String element = iterator.next();
            System.out.println(element);

            // 修改元素
            iterator.remove();
        }

        System.out.println("Modified Set: " + set);
    }
}

fail-safe

表示失败安全,也就是在这种机制下,出现集合元素的修改,不会抛出 ConcurrentModificationException。

原因是采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先 复制原有集合内容,在拷贝的集合上进行遍历。由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中 对原集合所作的修改并不能被迭代器检测到

比如这种情况(贴下面这个图) , 定义了一个 CopyOnWriteArrayList,在对这个集 合遍历过程中,对集合元素做修改后,不会抛出异常,但同时也不会打印出增加的元素。

在这里插入图片描述
(但同时也不会打印出增加的元素。) (但同时也不会打印出增加的元素。) (但同时也不会打印出增加的元素。)这句话的意思是:对于 CopyOnWriteArrayList,迭代器是不可变的,也就是说一旦创建迭代器,它将保留创建时刻集合的快照,后续对集合的修改不会反映在迭代器上。

java.util.concurrent 包下的容器都是安全失败的,可以在多线程下并发使用,并发修改。 常见的的使用 fail-safe 方式遍历的容器有 ConcerrentHashMap 和 CopyOnWriteArrayList 等。

2、谈谈你对 Seata 的理解

历史

在微服务架构下,由于数据库和应用服务的拆分,导致原本一个事务单元中的多个 DML 操作, 变成了跨进程或者跨数据库的多个事务单元的多个 DML 操作, 而传统的数据库事务无法解决这类的问题,所以就引出了分布式事务的概念。

市面上有很多针对这些理论模型实现的分布式事务框架,我们可以在应用中集成这 些框架来实现分布式事务。 而 Seata 就是其中一种,它是阿里开源的分布式事务解决方案,提供了高性能且简单易 用的分布式事务服务。

数据一致性问题

分布式事务本质上要解决的就是跨网络节点的多个事务的数据一致性问题,业内常 见的解决方法有两种

  1. 强一致性
    就是所有的事务参与者要么全部成功,要么全部失败,全局事务协 调者需要知道每个事务参与者的执行状态,再根据状态来决定数据的提交或者 回滚!基于 XA 协议下的二阶段提 交。
  2. 最终一致性
    也叫弱一致性,也就是多个网络节点的数据允许出现不一致的情 况,但是在最终的某个时间点会达成数据一致。基于 TCC 事务模型、可靠性消息模型。

基于 CAP 定理我们可以知道,强一致性方案对于应用的性能和可用性会有影响,所以 对于数据一致性要求不高的场景,就会采用最终一致性算法。

Seata 中封装了四种分布式事务模式

AT 模式

是一种基于本地事务+二阶段协议来实现的最终数据一致性方案,也是 Seata 默认的解决方案

在这里插入图片描述

TCC 模式

TCC 事务是 Try、Confirm、Cancel 三个词语的缩写,简单理解就是 把一个完整的业务逻辑拆分成三个阶段,然后通过事务管理器在业务逻辑层面根据 每个分支事务的执行情况分别调用该业务的 Confirm 或者 Cacel 方法。
在这里插入图片描述

Saga 模式

Saga 模式是 SEATA 提供的长事务解决方案,在 Saga 模式中,业务 流程中每个参与者都提交本地事务,当出现某一个参与者失败则补偿前面已经成功 的参与者。

在这里插入图片描述

XA 模式

XA 可以认为是一种强一致性的事务解决方法,它利用事务资源(数据库、 消息服务等)对 XA 协议的支持,以 XA 协议的机制来管理分支事务的一种 事务 模式。

在这里插入图片描述

3、Spring Boot 的约定优于配置,你的理解是什么?

  1. 首先, 约定优于配置是一种软件设计的范式,它的核心思想是减少软件开发人员 对于配置项的维护,从而让开发人员更加聚焦在业务逻辑上。
  2. 在 Spring Boot 中,我们不需要再去做这些繁琐的配置,Spring Boot 已经自动帮我 们完成了,这就是约定由于配置思想的体现。
  3. 类似于 Spring 框架下的一 个脚手架,通过 Spring Boot,我们可以快速开发基于 Spring 生态下的应用程序。
  4. Spring Boot 约定由于配置的体现有很多,比如
    • Spring Boot Starter 启动依赖,它能帮我们管理所有 jar 包版本
    • 内置 Tomcat 容器来运行 web 应用,我们不需要再去单独做应用部署
    • Spring Boot 的自动装配机制的实现中,通过扫描约定路径下的 spring.factories 文件来识别配置类,实现 Bean 的自动装配。
    • 默认加载的配置文件 application.properties

4、kafka 的零拷贝原理?

在实际应用中,如果我们需要把磁盘中的某个文件内容发送到远程服务器上,那么它必须要经过几个拷贝的过程,如图
在这里插入图片描述

  1. 从磁盘中读取目标文件内容拷贝到内核缓冲区
  2. CPU 控制器再把内核缓冲区的数据赋值到用户空间的缓冲区中
  3. 接着在应用程序中,调用 write()方法,把用户空间缓冲区中的数据拷贝到内核下 的 Socket Buffer 中。
  4. 最后,把在内核模式下的 SocketBuffer 中的数据赋值到网卡缓冲区(NIC Buffer)
  5. 网卡缓冲区再把数据传输到目标服务器上。

在这个过程中我们可以发现,数据从磁盘到最终发送出去,要经历 4 次拷贝,而在这四 次拷贝过程中,有两次拷贝是浪费的,分别是:

  1. 从内核空间赋值到用户空间
  2. 从用户空间再次复制到内核空间

除此之外,由于用户空间和内核空间的切换会带来 CPU 的上线文切换,对于 CPU 性能 也会造成性能影响。

而零拷贝,就是把这两次多于的拷贝省略掉,应用程序可以直接把磁盘中的数据从内核 中直接传输给 Socket,而不需要再经过应用程序所在的用户空间,如下图所示。
在这里插入图片描述
所以,所谓零拷贝,并不是完全没有数据赋值,只是相对于用户空间来说,不再需要进 行数据拷贝。对于前面说的整个流程来说,零拷贝只是减少了不必要的拷贝次数而已。

在程序中如何实现零拷贝呢?

  • 在 Linux 中,零拷贝技术依赖于底层的 sendfile()方法实现
  • 在 Java 中,FileChannal.transferTo() 方法的底层实现就是 sendfile() 方法。

mmap 文件映射机制

它的原理是:将磁盘文件映射到内存, 用户通过修改内存就能修改磁盘文件。使用这种 方式可以获取很大的 I/O 提升,省去了用户空间到内核空间复制的开销。

5、innoDB 如何解决幻读

什么是幻读

幻读是指在同一个事务中,前后两次查询相同的范围时,得到的结果不一致(我们来看 这个图)
在这里插入图片描述

  • 第一个事务里面我们执行了一个范围查询,这个时候满足条件的数据只有一条
  • 第二个事务里面,它插入了一行数据,并且提交了
  • 接着第一个事务再去查询的时候,得到的结果比第一查询的结果多出来了一条数 据。

所以,幻读会带来数据一致性问题。

Mysql 的事务隔离级别

Mysql 有四种事务隔离级别,这四种隔离级别代表当存在多个事务并发冲突时,可能出 现的脏读、不可重复读、幻读的问题。 其中 InnoDB 在 RR(默认) 的隔离级别下,解决了幻读的问题。

在这里插入图片描述

InnoDB 如何解决幻读的问题

InnoDB 引入了间隙锁和 next-key Lock 机制来解决幻读问题,为了更清晰的说明这两 种锁,我举一个例子:
假设现在存在这样(图片)这样一个 B+ Tree 的索引结构,这个结构中有四个索引元 素分别是:1、4、7、10。
在这里插入图片描述
当我们通过主键索引查询一条记录,并且对这条记录通过 for update 加锁(请看这个 图片)
在这里插入图片描述
这个时候,会产生一个记录锁,也就是行锁,锁定 id=1 这个索引(请看这个图片)。
在这里插入图片描述
被锁定的记录在锁释放之前,其他事务无法对这条记录做任何操作。 前面我说过对幻读的定义: 幻读是指在同一个事务中,前后两次查询相同的范围时, 得到的结果不一致! 注意,这里强调的是范围查询, 也就是说,InnoDB 引擎要解决幻读问题,必须要保证一个点,就是如果一个事务通过 这样一条语句(如图)进行锁定时。
在这里插入图片描述
另外一个事务再执行这样一条(显示图片)insert 语句,需要被阻塞,直到前面获得锁 的事务释放。
在这里插入图片描述
所以,在 InnoDB 中设计了一种间隙锁,它的主要功能是锁定一段范围内的索引记录(如 图)
在这里插入图片描述
当对查询范围 id>4 and id <7 加锁的时候,会针对 B+树中(4,7)这个开区间范围 的索引加间隙锁。 意味着在这种情况下,其他事务对这个区间的数据进行插入、更新、删除都会被锁住。

但是,还有另外一种情况,比如像这样(图片)
在这里插入图片描述
这条查询语句是针对 id>4 这个条件加锁,那么它需要锁定多个索引区间,所以在这种 情况下 InnoDB 引入了 next-key Lock 机制。 next-key Lock 相当于间隙锁和记录锁的合集,记录锁锁定存在的记录行,间隙锁锁住 记录行之间的间隙,而 next-key Lock 锁住的是两者之和。(如图所示)
在这里插入图片描述
每个数据行上的非唯一索引列上都会存在一把 next-key lock,当某个事务持有该数据 行的 next-key lock 时,会锁住一段左开右闭区间的数据。 因此,当通过 id>4 这样一种范围查询加锁时,会加 next-key Lock,锁定的区间范围 是:(4, 7] , (7,10],(10,+∞]
在这里插入图片描述
间隙锁和 next-key Lock 的区别在于加锁的范围,间隙锁只锁定两个索引之间的引用间 隙,而 next-key Lock 会锁定多个索引区间,它包含记录锁和间隙锁。

6、CPU 飙高系统反应慢怎么排查?

CPU 是整个电脑的核心计算资源,对于一个应用进程来说,CPU 的最小执行单元 是线程。

导致 CPU 飙高的原因有几个方面

  • CPU 上下文切换过多
    在 Java 中,文件 IO、网络 IO、锁等待、线程阻塞等操作都会造成线程阻塞从而触发 上下文切换

  • CPU 资源过度消耗
    在程序中创建了大量的线程,或者有线程一直占用 CPU 资源无法被释放,比如死循环!

  • 访问量较大
    最后有可能定位的结果是程序正常,只是在 CPU 飙高的那一刻,用户访问量较大, 导致系统资源不够。

CPU 利用率过高之后,导致应用中的线程无法获得 CPU 的调度,从而影响程序的执行 效率!

排查步骤

  1. 以通过 top 命令,找到 CPU 利用率较高的进程
  2. 再通过 Shift+H 切换线程模式,找到进程中 CPU 消耗过高的线程
    • CPU 利用率过高的线程一直是同一个
      说明程序中存在线程长期占用 CPU 没 有释放的情况,这种情况直接通过 jstack 获得线程的 Dump 日志,定位到线 程日志后就可以找到问题的代码。
    • CPU 利用率过高的线程 id 不断变化
      说明线程创建过多,需要挑选几个线程 id,通过 jstack 去线程 dump 日志中排查。

最后有可能定位的结果是程序正常,只是在 CPU 飙高的那一刻,用户访问量较大, 导致系统资源不够。

7、lock 和 synchronized 区别

  1. Synchronized 是 Java 中的同步关键字,Lock 是 J.U.C 包中提供的接口,这 个接口有很多实现类,其中就包括 ReentrantLock 重入锁
  2. synchronized常见的是修饰方法或者代码块上,如果锁对象是静态对象或者类对象,那么这个锁就是全局锁。 如果锁对象是普通实例对象,那这个锁的范围取决于这个实例的声明周期。Lock 锁的粒度是通过它里面提供的 lock()和 unlock()方法决定的,包裹在这 两个方法之间的代码能够保证线程安全性。而锁的作用域取决于 Lock 实例的生命周期。
  3. Lock 比 Synchronized 的灵活性更高,Lock 可以自主决定什么时候加锁,什 么时候释放锁,只需要调用 lock()和 unlock()这两个方法就行,同时 Lock 还 提供了非阻塞的竞争锁方法 tryLock()方法,这个方法通过返回 true/false 来 告诉当前线程是否已经有其他线程正在使用锁。Synchronized 由于是关键字,所以它无法实现非阻塞竞争锁的方法,另外, Synchronized 锁的释放是被动的,就是当 Synchronized 同步代码块执行完以后或者 代码出现异常时才会释放。
  4. Lock 提供了公平锁和非公平锁的机制,公平锁是指线程竞争锁资源时,如果 已经有其他线程正在排队等待锁释放,那么当前竞争锁资源的线程无法插队。 而非公平锁,就是不管是否有线程在排队等待锁,它都会尝试去竞争一次锁。 Synchronized 只提供了一种非公平锁的实现。
  5. 从性能方面来看,Synchronized 和 Lock 在性能方面相差不大,在实现上会有一 些区别,Synchronized 引入了偏向锁、轻量级锁、重量级锁以及锁升级的方式来 优化加锁的性能,而 Lock 中则用到了自旋锁的方式来实现性能优化。

8、线程池如何知道一个线程的任务已经执行完成

  1. 线程中的run方法正常结束,意味着任务完成
  2. 通过线程的isTerminated方法,判断线程的运行状态。一旦线 程池的运行状态是 Terminated,意味着线程池中的所有任务都已经执行完了。(不实用)
  3. 通 过 Future.get()方法来获得任务的执行结果,当线程池中的任务没执行完之前, future.get()方法会一直阻塞,直到任务执行结束。因此,只要 future.get() 方法正常返回,也就意味着传入到线程池中的任务已经执行完成了!
  4. 引入一个 CountDownLatch 计数器,它可以通过初始化指定一个计数器 进行倒计时,其中有两个方法分别是 await()阻塞线程,以及 countDown() 进行倒计时,一旦倒计时归零,所以被阻塞在 await()方法的线程都会被释放。

总结

不管是线程池内部还是外部,要想知道线程是否 执行结束,我们必须要获取线程执行结束后的状态,而线程本身没有返回值,所以 只能通过阻塞-唤醒的方式来实现,future.get 和 CountDownLatch 都是这样一个 原理。

9、HashMap 是怎么解决哈希冲突的?

  1. 开放定址法

开放定址法,也称为线性探测法,就是从发生冲突的那个位置开始,按照 一定的次序从 hash 表中找到一个空闲的位置,然后把发生冲突的元素存 入到这个空闲位置中。ThreadLocal 就用到了线性探测法来解决 hash 冲 突的。

在 hash 表索引 1 的位置存了一个 key=name,当再次添加 key=hobby 时,hash 计算得到的索引也是 1,这个就是 hash 冲突。而开放定址法, 就是按顺序向前找到一个空闲的位置来存储冲突的 key。
在这里插入图片描述

  1. 链式寻址法

链式寻址法,这是一种非常常见的方法,简单理解就是把存在 hash 冲突 的 key,以单向链表的方式来存储,比如 HashMap 就是采用链式寻址法 来实现的。

存在冲突的 key 直接以单向链表的方式进行存储。

在这里插入图片描述

  1. 再 hash 法

再 hash 法,就是当通过某个 hash 函数计算的 key 存在冲突时,再用另 外一个 hash 函数对这个 key 做 hash,一直运算直到不再产生冲突。这种 方式会增加计算时间,性能影响较大

  1. 建立公共溢出区

建立公共溢出区, 就是把 hash 表分为基本表和溢出表两个部分,凡事存 在冲突的元素,一律放入到溢出表中。

10、什么叫做阻塞队列的有界和无界

阻塞队列

  • 当队列为空的时候,获取队列中元素的消费者线程会被阻塞,同时唤醒生产者 线程。
  • 当队列满了的时候,向队列中添加元素的生产者线程被阻塞,同时唤醒消费者 线程。

阻塞队列中能够容纳的元素个数,通常情况下是有界的,比如我们实例化一 个 ArrayBlockingList,可以在构造方法中传入一个整形的数字,表示这个基于数 组的阻塞队列中能够容纳的元素个数。这种就是有界队列。

而无界队列,就是没有设置固定大小的队列,不过它并不是像我们理解的那种元素 没有任何限制,而是它的元素存储量很大,像 LinkedBlockingQueue,它的默认 队列长度是 Integer.Max_Value,所以我们感知不到它的长度限制。

无界队列存在比较大的潜在风险,如果在并发量较大的情况下,线程池中可以几乎 无限制的添加任务,容易导致内存溢出的问题!

11、Dubbo 的服务请求失败怎么处理?

默认提供了重试的容错机制,也就是说,如果基 于 Dubbo 进行服务间通信出现异常,服务消费者会对服务提供者集群中其他的节点发 起重试,确保这次请求成功,默认的额外重试次数是 2 次。

  • 快速失败策略
  • 广播调用策略
  • 并行调用多个服务策略
  • 失败自动恢复策略

12、ConcurrentHashMap 底层具体实现知道吗? 实现原理是什么?

ConcurrentHashMap 的整体架构

在这里插入图片描述

ConcurrentHashMap 在 JDK1.8 中的存储结构,它是由数组、 单向链表、红黑树组成。
当我们初始化一个ConcurrentHashMap实例时,默认会初始化一个长度为16的数组。 由于 ConcurrentHashMap 它的核心仍然是 hash 表,所以必然会存在 hash 冲突问题。 ConcurrentHashMap 采用链式寻址法来解决 hash 冲突。
当 hash 冲突比较多的时候,会造成链表长度较长,这种情况会使得 ConcurrentHashMap 中数据元素的查询复杂度变成 O(n)。因此在 JDK1.8 中,引入了 红黑树的机制。
当数组长度大于 64 并且链表长度大于等于 8 的时候,单项链表就会转换为红黑树。 另外,随着 ConcurrentHashMap 的动态扩容,一旦链表长度小于 8,红黑树会退化 成单向链表。

ConcurrentHashMap 的基本功能

并发安全的HashMap。并发安全的主要实现是通过对指定的 Node 节点加锁,来保证数据更新的安全性(如图 所示)。
在这里插入图片描述

ConcurrentHashMap 在性能方面的优化

- 在 JDK1.8 中,ConcurrentHashMap 锁的粒度是数组中的某一个节点,而在 JDK1.7,锁定的是 Segment,锁的范围要更大,因此性能上会更低。
- 引入红黑树,降低了数据查询的时间复杂度,红黑树的时间复杂度是 $O(log_n)$。
- 当数组长度不够时,ConcurrentHashMap 需要对数组进行扩 容,在扩容的实现上,ConcurrentHashMap 引入了多线程并发扩容的机制, 简单来说就是多个线程对原始数组进行分片后,每个线程负责一个分片的数据 迁移,从而提升了扩容过程中数据迁移的效率。

在这里插入图片描述

13、b 树和 b+树的理解

这个是 B 树的存储结构,从 B 树上可以看到每个节点会存储数据。
在这里插入图片描述

这个是 B+树,B+树的所有数据是存储在叶子节点,并且叶子节点的数据 是用双向链表关联的。
在这里插入图片描述

B+树,其实是在 B 树的基础上做的增强,最大的区别有两个:

  • B 树的数据存储在每个节点上,而 B+树中的数据是存储在叶子节点,并且通 过链表的方式把叶子节点中的数据进行连接。
  • B+树的子路数量等于关键字数

为什么用 B 树或者 B+树来做索引结构?

原因是 AVL 树的高度要比 B 树的高度要 高,而高度就意味着磁盘 IO 的数量。所以为了减少磁盘 IO 的次数,文件系统或者 数据库才会采用 B 树或者 B+树。

14、能谈一下 CAS 机制吗?

CAS 是 Java 中 Unsafe 类里面的方法,它的全称是CompareAndSwap,比较并交换 的意思。它的主要功能是能够保证在多线程环境下,对于共享变量的修改的原子性。

CAS 主要用在并发场景中,比较典型的使用场景有两个。

  • 第一个是 J.U.C 里面 Atomic 的原子实现,比如 AtomicInteger,AtomicLong。
  • 第二个是实现多线程对共享资源竞争的互斥性质,比如在 AQS、 ConcurrentHashMap、ConcurrentLinkedQueue 等都有用到。

15、请说一下网络四元组

四元组,简单理解就是在 TCP 协议中,去确定一个客户端连接的组成要素,它包括源 IP 地址、目标 IP 地址、源端口号、目标端口号。

16、什么是服务网格

目前对于大部分企业来说,仍然是 处在第二代微服务架构下。

在第二代微服务架构中,负责业务开发的小伙伴不仅仅需要关注业务逻辑,还需要花大 量精力去处理微服务中的一些基础性配置工作,虽然 Spring Cloud 已经尽可能去完成 了这些事情,但对于开发人员来说,学习 Spring Cloud,以及针对 Spring Cloud 的 配置和维护,仍然存在较大的挑战。另外呢,也增加了整个微服务的复杂性。

实际上,在我看来,“微服务中所有的这些服务注册、容错、重试、安全等工作,都是 为了保证服务之间通信的可靠性”。
于是,就有了第三代微服务架构,Service Mesh。

原本模块化到微服务框架里的微服务基础能力,被进一步的从一个 SDK 中演 进成了一个独立的代理进程-SideCar
在这里插入图片描述
SideCar 的主要职责就是负责各个微服务之间的通信,承载了原本第二代微服务架构中 的服务发现、调用容错、服务治理等功能。使得微服务基础能力和业务逻辑迭代彻底解 耦。

之所以我们称 Service Mesh 为服务网格,是因为在大规模微服务架构中,每个服务的 通信都是由 SideCar 来代理的,各个服务之间的通信拓扑图,看起来就像一个网格形状

在这里插入图片描述

17、Redis 和 Mysql 如何保证数据一致性

消息不一致如何产生

一份数据,同时保存在数据库和 Redis 里面,当数据发生变化的时候,需要同时更新 Redis 和 Mysql,由于更新是有先 后顺序的,并且它不像 Mysql 中的多表事务操作,可以满足 ACID 特性。所以就会出 现数据一致性问题。

在这种情况下,能够选择的方法只有几种。

  1. 先更新数据库,再更新缓存
    如果先更新数据库,再更新缓存,如果缓存更新失败,就会导致数据库和 Redis 中的数 据不一致。
  2. 先删除缓存,再更新数据库
    理想情况是应用下次访问 Redis 的时候,发现 Redis 里面的数据是空的,就从数据库加载保存到 Redis 里面,那么数据是一致的。但是在极 端情况下,由于删除 Redis 和更新数据库这两个操作并不是原子的,所以这个过程如果 有其他线程来访问,还是会存在数据不一致问题

最终一致性方案

RocketMQ可靠性消息通信

  • 异步重试
    在这里插入图片描述

  • Canal监听
    在这里插入图片描述
    因为这里是基于最终一致性来实现的,如果业务场景不能接受数据的短期不一致性,那 就不能使用这个方案来做。

18、Spring Boot 中自动装配机制的原理

简单来说就是自动把第三方组件的 Bean 装载到 Spring IOC 器里面,不需 要开发人员再去写 Bean 的装配配置。

在 Spring Boot 应用里面,只需要在启动类加上@SpringBootApplication 注解就可 以实现自动装配。

@SpringBootApplication 是一个复合注解,真正实现自动装配的注解是 @EnableAutoConfiguration。

自动装配的实现主要依靠三个核心关键技术。

  1. 引入 Starter 启动依赖组件的时候,这个组件里面必须要包含@Configuration 配 置类,在这个配置类里面通过@Bean 注解声明需要装配到 IOC 容器的 Bean 对象。
  2. 这个配置类是放在第三方的 jar 包里面,然后通过 SpringBoot 中的约定优于配置 思想,把这个配置类的全路径放在 classpath:/META-INF/spring.factories 文件 中。这样 SpringBoot 就可以知道第三方 jar 包里面的配置类的位置,这个步骤主 要是用到了 Spring 里面的 SpringFactoriesLoader 来完成的。
  3. SpringBoot 拿到所第三方 jar 包里面声明的配置类以后,再通过 Spring 提供的 ImportSelector 接口,实现对这些配置类的动态加载。

在这里插入图片描述

19、死锁的发生原因和怎么避免

死锁,简单来说就是两个或者两个以上的线程在执行的过程中,争夺同一个共 享资源造成的相互等待的现象。

在这里插入图片描述
导致死锁之后,只能通过人工干预来解决,比如重启服务,或者杀掉某个线程。

20、dubbo负载均衡的几种策略

  • 轮询
  • 加权
  • 随机
  • 最小活跃数
  • 一致性hash

21、dubbo的工作原理

provider、register、consumer

  1. 服务启动的时候,provider 和 consumer 根据配置信息,连接到注册中心 register, 分别向注册中心注册和订阅服务
  2. register 根据服务订阅关系,返回 provider 信息到 consumer,同时 consumer 会 把 provider 信息缓存到本地。如果信息有变更,consumer 会收到来自 register 的推 送
  3. consumer 生成代理对象,同时根据负载均衡策略,选择一台 provider,同时定时向 monitor 记录接口的调用次数和时间信息
  4. 拿到代理对象之后,consumer 通过代理对象发起接口调用
  5. provider 收到请求后对数据进行反序列化,然后通过代理调用具体的接口实现

22、dubbo和spring cloud的区别

首先Dubbo 它的关注点主要在于服务的调用,流量分发、流量监控 和熔断。而 Spring Cloud 诞生于微服务架构时代,考虑的是微服务治理, 另外由于依托了 Spirng、Spirng Boot 的优势之上,两个框架在开始目标就不一致, Dubbo 定位服务治理、Spirng Cloud 是一个生态。

两者最大的区别,Dubbo 底层是使用 Netty 这样的 NIO 框架,是基于 TCP 协议传 输的,配合以 Hession 序列化完成 RPC 通信。而 SpringCloud 是基于 Http 协议 + Rest 接口调用远程过程的通信,相对来说,Http 请求会有更大的报文,占的带宽也 会更多。但是 REST 相比 RPC 更为灵活,服务提供方和调用方的依赖只依靠一纸契约, 不存在代码级别的强依赖。

23、Spring Bean 生命周期的执行流程

24、Spring 是如何解决循环依赖问题的?

在这里插入图片描述

三级缓存是用来存储代理 Bean,当调用 getBean()方法时,发现目标 Bean 需要通过 代理工厂来创建,此时会将创建好的实例保存到三级缓存,最终也会将赋值好的 Bean 同步到一级缓存中

25、zookeeper和redis分布锁的区别

  • redis
  1. 通过redis指令set key value nx
  2. 基于redission,提供了分布式锁的封装方法
    - redission所有指令都通过lua脚本执行并支持lua脚本原子性执行
    - redission中有一个watchdog(看门狗)的概念,当你获得锁之后,每隔10秒帮你把key的超时时间设为30s,就算一直持有锁也不会出现key过期了。看门狗的逻辑保证了没有死锁发生。
  • zookeeper

对于 redis 的分布式锁而言,它有以下缺点:

  • 它获取锁的方式简单粗暴,如果获取不到锁,会不断尝试获取锁,比较消耗性能。
  • Redis 是 AP 模型,在集群模式中由于数据的一致性会导致锁出现问题,即便使用 Redlock 算法来实现,在某些复杂场景下,也无法保证其实现 100%的可靠性。 不过在实际开发中使用 Redis 实现分布式锁还是比较常见,而且大部分场情况下不会遇 到”极端复杂的场景“,更重要的是 Redis 性能很高,在高并发场景中比较合适。

对于 zk 分布式锁而言:

  • zookeeper 天生设计定位就是分布式协调,强一致性。锁的模型健壮、简单易用、 适合做分布式锁。
  • 如果获取不到锁,只需要添加一个监听器就可以了,不用一直轮询,性能消耗较小。

26、关于索引的底层实现,为什么选择 B+Tree 而不是 红黑树

  • 减少IO次数

举个例子 对于 31 个节点的树来说 ,一个 5 阶 B+Tree 的高度是 3 一个红黑树的 最小高度是 5,树的高度基本决定了磁盘的 IO 次数 ,所以使用 B+Tree 性能要高 很多

  • 数据相邻

B+Tree 有个特点是相邻的数据在物理上也是相邻的,因为 B+Tree 的 node 的大小设
为一个页,而一个节点上存有多个相邻的关键字和分支信息,每个节点只需要一次 IO 就能完全载入,相当于一次 IO
载入了多个相邻的关键字和分支,而红黑树不具有这个 特性,红黑树中大小相邻的数据,在物理结构上可能距离相差很大。由于程序的局部性
原理,如果我们在索引中采用了预加载的技术,每次磁盘访问的时候除了将访问到的页
加载到磁盘,我们还可以基于局部性原理加载,几页相邻的数据到内存中,而这个加载 是不需要消耗多余磁盘 IO 时间的。

27、为什么要用 Dcoker,它具有哪些优势

  • 资源占用小
  • 启动速度快
  • 迁移更轻松
  • 维护和拓展更轻松
  • 运行环境一致
  • 持续交付和部署

28、docker的核心组件

  • 仓库
  • 镜像
  • 容器

29、什么是容器编排技术

通过一个入口来管理多个服务器中的docker容器

30、对 Netty 的认识

31、请你说一下 Mysql 中的性能调优方法

  1. 表结构与索引
    分库分表、读写分离、为字段选择合适的数据类型、适当的反范式设计,适当冗余设计、 为查询操作创建必要的索引但是要避免索引滥用、尽可能使用 Not Null。
  2. sql语句优化
    通过慢查询分析需要优化的 SQL 进行合理优化、利用 explain、profile 等工具分析 SQL 执行计划、避免使用 SELECT *查询。 尽可能使用索引扫描来排序。
  3. mysql参数优化
    主要可以设置 Buffer_pool 的大小,建议占总内存的 70%左右。 设置刷盘策略,平衡好数据安全性和性能的关系等
  4. 硬件及系统配置
    主要是 CPU 核数、磁盘的读写性能(减小寻道时间、旋转时间、传输 时间),可以选择 SSD、网卡、内存等方面。

32、扫码登录到底是怎么实现的?

  1. 首先,在网页端打开登录页面,展示一个二维码,这个二维码有一个唯一编号是服 务端生成的。然后浏览器定时轮询这个二维码的状态
  2. 接着,APP 扫描这个二维码,把 APP 的 token 信息、二维码 ID 发送给 Server 端, Server 收到请求后修改二维码的扫码状态,并生成一个临时 token
  3. 此时,网页端展示的二维码状态会提示已扫码,待确认。 而 APP 端扫码之后,会 提示确认授权的操作。
  4. 于是,用户确认登录后,携带临时 token 给到 server,server 端修改二维码状态 并为网页端生成授权 token
  5. 最后,网页端轮询到状态变化并获取到 token,从而完成扫码授权。

33、为什么阿里巴巴的Java开发手册不建议使用Java 自带的线程池

  1. Executors 里面默认提供的几个线程池是有一些弊端的,如果是不懂多线程、或者是新 手直接盲目使用,就可能会造成比较严重的生产事故
    • 第一个,FixedThreadPool 和 SingleThreadPool 中,阻塞队列长度是 Integer.Max_Value,一旦请求量增加,就会堆积大量请求阻塞在队列中,可能会 造成内存溢出的问题
    • 第二个,CachedThreadPool 和 ScheduledThreadPool 中最大线程数量是 Integer.Max_value,一旦请求量增加,导致创建大量的线程,使得处理性能下降。 甚至可能会出现宕机的问题.

为了避免这类问题出现,我们可以直接实例化 ThreadPoolExecutor,然后自己设置参 数的值,从而确保线程池的可控性

34、Redis 为什么这么快?

  1. 网络
    在网络层面,redis采用多路复用的设计,提高了并发处理的连接数,
  2. CPU
    从CPU的层面,redis只需要采用单线程即可,单线程的好处:
    • 不需要加锁,没有线程切换影响redis的性能
    • 不会消耗太多CPU
  3. 内存
    从内存层面来看,redis本身就是一个数据库,内存的IO速度本身就很快,所以内存的瓶颈只是受限于内存大小。

最后redis本身的数据结构也做了很多的优化,比如压缩表、跳跃表等方式来降低时间复杂度,同时提供了不同时间复杂度的数据类型。
压缩表:哈希表压缩。当哈希表中删除了一些键值对后,Redis可能会定期对哈希表进行稀疏压缩,以释放废弃的内存。这有助于确保哈希表在删除键值对后不会浪费太多内存。
跳跃表:

35、订单超时自动取消功能如何设计

利用MQ中的延时消息功能,消息发送到broker上以后不会立即消费,而是根据消息设置的延时时间去投递,我们只需要把订单和超时时间发送到MQ即可。

36、Mysql 中的 RR 隔离级别,到底有没有解决幻读问题?

Mysql 中的 RR 事务隔离级别,在特定的情况下会出现幻读的问题。
在这里插入图片描述
来看这样一种情况,在事务 1 里面通过 update 语句触发当前读的情况下,就 会导致在该事务中的前后两次查询的数据行数不一致,从而出现幻读的现象。

导致幻读的根本原因是,update 触发的当前读操作,绕过了快照读,从而导致 MVCC 机制在当前场景下失效。 最终读取到了事务 2 中已经提交的数据。

为了避免出现这类的情况,我们可以通过 for update 语句加锁。

37、如何排查线上的oom问题

什么情况下会导致oom

  • jvm内存分配太小,实际业务消耗太多
  • Java应用存在内存泄漏的问题,或者应用中有大量占用内存的对象没办法及时释放

内存泄漏
申请使用完的内存没有释放,导致虚拟机不能再次使用该内存,此时这段内存就泄漏了
内存溢出
申请的内存超过了jvm所能提供的内存大小

常见的oom异常

  • OutOfMemoryError: Java heap space:堆内存溢出(常见)
    一般由于内存泄露或者堆的大小设置不当引起,堆大小可以通过虚拟机参数 -Xms,-Xmx 来修改
  • OutOfMemoryError: PermGen space:方法区溢出
  • OutOfMemoryError:MetaSpace:元空间溢出
    一般 出现在大量 Class、或者采用 cglib 等反射机制的情况,因为这些情况会产生大量 的 Class 信息存储于方法区,使用类似-XX:PermSize=64m -XX:MaxPermSize=256m 的形式修改

排查步骤

  1. 先获取内存的 Dump 文件
    • 通过配置 JVM 启动参数,当触发了 OOM 异常的时候自动生成
    • 使用 jmap 工具来生成
  2. 使用 MAT 工具来分析 Dump 文件
    • 如果是普通的内存溢出,确实有很多占用内存的对象,那就只需要提升堆内存空间 即可。
    • 如果是内存泄漏,可进一步通过工具查看泄漏对象到 GC Roots 的引用链。掌握了 泄漏对象的类信息和 GC Roots 引用链的信息,就可以比较准确地定位泄漏代码的 位置。

38、怎么理解接口幂等,项目中如何保证的接口幂等

幂等
简单来说,就是一个接口,使用相同的参数重复执行的情况下,对数据造成的改变只发 生一次。
比如支付操作,如果支付接口被重复调了 N 次,那资金的扣减只发生一次,这就是幂 等。

解决方案

  1. 在数据库中根据消息的唯一标识判断
  2. redis
  3. 状态机
    在很多的业务场景中,都会存在业务状态的流转, 并且这些状态流转只会前进,所以我们在对数据进行修改的时候,只需要在条件里 面带上状态,就能避免数据被重复修改的问题。

39、布隆过滤器到底是什么东西?它有什么用

如果这些点有任何一个 0,则被检查的元素一定不在;如果都是 1,则被检查的元素很 可能存在。

40、什么是聚集索引和非聚集索引

  1. 简单来说,聚簇索引就是基于主键创建的索引,除了主键索引以外的其它索引,称为非聚簇索引,也叫二级索引。
  2. 在innodb中,聚簇索引就是根据每张表的主键来构建b+树,然后叶子结点里面存储了这个表的每一行数据记录。
  3. 在innodb里面,主键索引表示的是一种数据存储结构,所以如果是 基于非聚集索引来查询一条完整的记录,最终还是需要访问主键索引来检索。
    在这里插入图片描述
  • 一般情况是建议使用自 增 id 作为主键,这样的话 id 本身具有连续性使得对应的数据也会按照顺序存储在 磁盘上,写入性能和检索性能都很高。否则,如果使用 uuid 这种随机 id,那么在 频繁插入数据的时候,就会导致随机磁盘 IO,从而导致性能较低
  • InnoDB 里面只能存在一个聚集索引,原因很简单,如果存在多个 聚集索引,那么意味着这个表里面的数据存在多个副本,造成磁盘空间的浪费,以 及数据维护的困难。

41、说一下你对双亲委派的理解

类的加载机制
类的加载机制,就是我们自己写的java源文件到运行必须要经过编译和类加载两个阶段。
编译:Java—>class
类加载:将class装载到jvm内存中,装载完成后就会得到一个class对象,我们就可以使用new关键字来实例化这个对象。

而类的加载过程,需要涉及到类加载器。

JVM 在运行的时候,会产生 3 个类加载器,这三个类加载器组成了一个层级关系 每个类加载器分别去加载不同作用范围的 jar 包

  • Bootstrap ClassLoader,主要是负责 Java 核心类库的加载
  • Extension ClassLoader,主要负责%{JDK_HOME}\lib\ext 目录下的 jar 包和 class 文件
  • Application ClassLoader,主要负责当前应用里面的 classpath 下的所有 jar 包和 类文件

除了系统自己提供的类加载器以外,还可以通过 ClassLoader 类实现自定义加载器,去 满足一些特殊场景的需求。
在这里插入图片描述

双亲委派模型
就是按照类加载器的层级关系,逐层进行委派。比如当需要执行一个String.class文件的时候,首先会把这个class的查询和加载委派给父加载器去执行,如果父加载器都无法加载,再尝试自己来加载这个class。
在这里插入图片描述
好处

  1. 安全性
    对于核心类库中的类,就没办法去破坏,比如 自己写一个 java.lang.String,最终还是会交给启动类加载器
  2. 避免重复加载
    如果父 加载器已经加载过了,那么子类就没必要去加载了。

42、说一说 Mybatis 里面的缓存机制

Mybatis 里面设计的二级缓存是用来提升数据的检索效率,避免每次数据的访 问都需要去查询数据库。

  • 一级缓存
    SqlSession 级别的缓存,也叫本地缓存。每个用户在执行查询的时 候都需要使用 SqlSession 来执行,为了避免每次都去查数据库,Mybatis 把查询出来的数据保存到 SqlSession 的本地缓 存中,后续的 SQL 如果命中缓存,就可以直接从本地缓存读取了。
  • 二级缓存
    在查询数据的时候,只有有任何一个 SqlSession 拿到了数据就会放入到二级缓存里面, 其他的 SqlSession 就可以从二级缓存加载数据。

一级缓存的实现原理
在 SqlSession 里面持有一个 Executor,每个 Executor 中有一个 LocalCache 对象。 当用户发起查询的时候,Mybatis 会根据执行语句在 Local Cache 里面查询,如果没命 中,再去查询数据库并写入到 LocalCache,否则直接返回。 所以,以及缓存的生命周期是 SqlSessiion,而且在多个 Sqlsession 或者分布式环境下, 可能会导致数据库写操作出现脏数据。
在这里插入图片描述
二级缓存的实现原理
使用 CachingExecutor 装饰了 Executor,所以在进入一级缓存的查询流程之前,会先 通过 CachingExecutor 进行二级缓存的查询。
开启二级缓存以后,会被多个 SqlSession 共享,所以它是一个全局缓存。因此它的查 询流程是先查二级缓存,再查一级缓存,最后再查数据库。 另外,MyBatis 的二级缓存相对于一级缓存来说,实现了 SqlSession 之间缓存数据 的共享,同时缓存粒度也能够到 namespace 级别,并且还可以通过 Cache 接口实 现类不同的组合,对 Cache 的可控性也更强。

在这里插入图片描述

43、Redis 存在线程安全问题吗?

  1. Redis Server 本身是一个线程安全的 K-V 数据库,也就是说在 Redis Server 上执行的 指令,不需要任何同步机制,不会存在线程安全问题。
  2. 但是如果多个redis客户端同 时执行多个指令的时候,就无法保证原子性。
    假设两个 redis client 同时获取 Redis Server 上的 key1, 同时进行修改和写入,因为 多线程环境下的原子性无法被保障,以及多进程情况下的共享资源访问的竞争问题,使 得数据的安全性无法得到保障。

解决办法

  • 尽可能的使用 Redis 里面的原子指令
  • 访问加锁
  • 通过 Lua 脚本来实现多个指令的操作

44、RDB 和 AOF 的实现原理以及优缺点

  1. RDB快照
  • 执行bgsave触发异步快照
  • 执行save命令触发同步快照(阻塞客户端的执行命令)
  • 修改redis.conf配置,自动触发
  • 主从复制的时候触发
    在这里插入图片描述
  1. AOF日志
    近乎实时的方式来完成持久化的,就是客户端执行一个 数据变更的操作,Redis Server 就会把这个命令追加到 aof 缓冲区的末尾, 然后再把缓冲区的数据写入到磁盘的 AOF 文件里面,至于最终什么时候真正持久化到 磁盘,是根据刷盘的策略来决定的。

在这里插入图片描述

为了避免追加的方式导致 AOF 文件过大的问题,Redis 提供了 AOF 重写机制,也就是说当 AOF 文件的大小达到某个阈值的时候,就会把这个文件里面相同的指令进 行压缩。
在这里插入图片描述

  1. 优缺点分析
  • RDB数据安全性低
  • RDB 文件默认采用压缩的方式持久化,AOF 存储的是执行指令,所以 RDB 在数据 恢复的时候性能比 AOF 要好

45、什么是守护线程,它有什么特点

举个例子,JVM 垃圾回收线程就是一个典型的守护线程,它存在的意义是不断的处理 用户线程运行过程中产生的内存垃圾。

一旦用户线程全部结束了,那垃圾回收线程也就没有存在的意义了。

所以它它适合用在一些后台的通用服务场景里面。

46、AbstractQueuedSynchronized 为什么采用双 向链表

双向链表的优势

  • 双向链表提供了双向指针,可以在任何一个节点方便向前或向后进行遍历,这种对于有反向遍历需求的场景来说非常有用。
  • 双向链表可以在任意节点位置实现数据的插入和删除,并且这些操作的时间复杂度都是 O(1),不受链表长度的影响。这对于需要频繁对链表进行增删操作的场景非 常有用。

AQS采用双向链表的原因

  • 存储在双向链表中的线程,有可能这个线程出现异常不再需要竞争锁,所以需要把 这些异常节点从链表中删除,而删除操作需要找到这个节点的前驱结点,如果不采 用双向链表,就必须要从头节点开始遍历,时间复杂度就变成了 O(n)。
    在这里插入图片描述

  • 新加入到链表中的线程,在进入到阻塞状态之前,需要判断前驱节点的状态,只有 前驱节点是 Sign 状态的时候才会让当前线程阻塞,所以这里也会涉及到前驱节点 的查找,采用双向链表能够更好的提升查找效率

  • 线程在加入到链表中后,会通过自旋的方式去尝试竞争锁来提升性能,在自旋竞争 锁的时候为了保证锁竞争的公平性,需要先判断当前线程所在节点的前驱节点是否 是头节点。这个判断也需要获取当前节点的前驱节点,同样采用双向链表能提高查 找效率。

47、limit 1000000,10 加载很慢该怎么优化

  1. 如果 id 是连续的,可以直接使用这样的方式。
select id,name from user where id>1000000 limit10

这种方式其实就是先对数据做过滤,然后再 limit,可以有效提升查询效率

  1. 通过 order by+索引来解决
select id,name from user order by id limit 100000010

需要注意 id 是索引列,通过索引排序后再 limit,同样减少了计算次数

48、Mysql 主从集群同步延迟问题怎么解决

  • 主从复制的工作原理
    在这里插入图片描述
    复制过程分为几个步骤
  1. 主库的更新事件(update、insert、delete)被写到binlog
  2. 从库发起连接,连接到主库
  3. 此时主库创建一个 binlog dump thread,把 binlog 的内容发送到从库
  4. 从库启动之后,创建一个 I/O 线程,读取主库传过来的 binlog 内容并写入到 relay log
  5. 从库还会创建一个 SQL 线程,从 relay log 里面读取内容,从 Exec_Master_Log_Pos 位置开始执行读取到的更新事件,将更新内容写入到slave 的 db

主从数据同步涉及到网络数据传输,由于网络通信的延迟以及从库数据处理的效率问 题,就会导致主从数据同步延迟的情况。

解决办法

  • 一主多从来分担从库压力,减少延迟
  • 从库设置sleep阻塞等待固定时间后再次查询
  • 通过并行复制解决从库复制延迟的问题
  • 如果对数据一致性要求高,在从库存在延迟的情况下,可以强制走主库查询数据(没必要)

49、Mysql 的 binlog 有几种格式?分别有什么区别

  • statement:记录的是 SQL 的原文
  • row:仅保存哪条记录被修改
  • mixed:普通操作使用 statement 记录,当无法使用 statement 的时候使用 row

50、索引有哪些缺点以及具体有哪些索引类型

缺点

  1. 创建、维护索引需要时间
  2. 占物理空间
  3. 对数据进行修改、删除、新增时,会动态维护索引,造成性能影响

索引类型

  1. 主键索引
  2. 普通索引
  3. 唯一索引
  4. 覆盖索引
  5. 组合索引
  6. 全文索引

51、MySQL 数据库 cpu 飙升的话,要怎么处理呢?

52、会员批量过期的方案怎么实现

有一张 200W 数据量的会员表,每个会员会有长短不一的到期时间,现在想在快到 期之前发送邮件通知提醒续费,如何实现

  1. 200w的数据量
  2. 如何快速筛选快过期的会员

解决办法

  1. 系统不主动轮询,而是等到用户登陆到系统之后,触发一次检查。如果发现会员的过期时间小于设定的阈值,就触发一次弹窗和邮件提醒。这种方式规避了轮询问题,不会对数据库和后端应用程序造成任何压力。缺点是,如果用户一直不登陆,就一直无法实现会员过期,并且也无法提前去根据运营 策略发送续期的提醒消息。
  2. 使用搜索引擎,比如Elasticsearch,把会员表里面的会员 id 和会员到期时间存储一份到搜索引擎中。搜索引擎的优势在于大数据量的快速检索,并且具有高可扩展性和高可靠性,非常适合 大规模数据的处理。
  3. 使用redis来实现,用户开通会员以后,在 Redis 里面存储这个会员 id,以及设置这个 id 的过期时间。 然后可以使用 redis 的过期提醒功能,把配置项 notify-keyspace-events 改为 notify-keyspace-events “Ex”。当 Redis 里面的 key 过期以后,会触发一个 key 过期事件,我们可以在应用程序中监 听这个事件来处理。
  4. 使用MQ 。当用户开通会员以后,直接计算这个会 员的过期时间,然后发送一个延迟消息到 MQ 上,一旦消息达到过期时间,消费者就 可以消费这个消息来触发会员过期的提醒。

53、binlog 和 redolog 有什么区别

在这里插入图片描述

  1. binlog 主要用来做数据备份、数据恢复、以及主从集群的数据同步;Redo Log 主要用来实现 Mysql 数据库的事务恢复,保证事务的 ACID 特性。
  2. binlog 是在执行 SQL 语句的时候,在主线程中生成逻辑变化写 入到磁盘中,所以它是语句级别的记录方式;RedoLog 是在 InnoDB 存储引擎层 面的操作,它是在 Mysql 后台线程中生成并写入到磁盘中的,所以它是事务级别 的记录方式,一个事务操作完成以后才会被写入到 redo log 中

54、说说 Spring MVC 的执行流程

在这里插入图片描述

55、谈谈你对 AQS 的理解

56、JDK 动态代理为什么只能代理有接口的类

还是取决于 JDK 动态代理的底层实现。
JDK 动态代理会在程序运行期间动态生成一个代理类$Proxy0,这个动态生成的代理类 会继承 java.lang.reflect.Proxy 类,同时还会实现被代理类的接口 IHelloService。 在 Java 中,是不支持多重继承的。而每个动态代理类都会继承 Proxy 类(这也是 JDK 动态代理的实现规范),所以就导致 JDK 里面的动态代理只能代理接口,而不能代理 实现类。
在这里插入图片描述

57、Redis 的内存淘汰算法和原理是什么

Redis 里面的内存淘汰策略,是指内存的使用率达到 maxmemory 上限的时候的一种 内存释放的行为。

  1. Random 算法,随机移除某个 key
  2. TTL 算法 ,在设置了过期时间的键中,把更早过期时间的 key 有限移除
  3. LRU 算法,移除最近很少使用的 key
  4. LFU 算法,移除最近很少使用的 key

58、Kafka 如何保证消息不丢失

59、说一下你对分布式锁的理解,以及分布式锁的实现

60、Redis 锁超时怎么办?

61、Redis 主从切换导致锁失效怎么办?

Redlock实现
简单来说,就是利用多个的主节点,在超过半数以上的主节点获取锁成功,才算成功;否则算失败,回滚–删除之前在所有节点上获取的锁。

62、说说缓存雪崩和缓存穿透的理解,以及如何避免

63、Dubbo 是如何动态感知服务下线的

在这里插入图片描述
Dubbo 服务消费端会使用 Zookeeper 里面的 Watch 来针对 Zookeeper Server 端的/providers 节点注册监听,一旦这个节点下的子节点发生变化,Zookeeper Server 就会发送一个事件通知 Dubbo Client 端。Dubbo Client 端收到事件以后,就会把本地缓存的这个服务地址删除,这样后续就不 会把请求发送到失败的节点上,完成服务下线感知。

64、Zookeeper 中的 Watch 机制的原理

Zookeeper 提供了一个 Watch 机制,可以让客户端感知到 Zookeeper Server 上存储的数据变化,这样一种机制可以让 Zookeeper 实现很多的场景,比如配置中心、 注册中心等。

Watch 机制采用了 Push 的方式来实现,也就是说客户端和 Zookeeper Server 会建立 一个长连接,一旦监听的指定节点发生了变化,就会通过这个长连接把变化的事件推送 给客户端。

65、Spring中 BeanFactory和FactoryBean的区别

在这里插入图片描述

  • 看源码的话beanfactory主要就提供了一个getBean方法,从容器中获取bean实例
  • 提供了DI,依赖注入

在这里插入图片描述

  • FactoryBean是一个工厂bean,它是一个接口,我们可以自定义一个bean并且加载到容器中。getObject就是用来实现动态构建bean的过程。(Spring Cloud 里面的 OpenFeign 组件,客户端的代理类,就是使用了 FactoryBean 来实现的。)

66、什么是可重入,什么是可重入锁? 它用来解决什么 问题?

67、请说一下 ReentrantLock 的实现原理

68、数据库连接池有什么用?它有哪些关键参数

连接池的核心思想,就是应用程序在启动的时候提前初始化一部分连接保存 到连接池里面,当应用需要使用连接的时候,直接从连接池获取一个已经建立好的链接。 连接池的设计,避免了每次连接的建立和释放带来的开销

  • 初始化连接数,表示启动的时候初始多少个连接保存到连接池里面
  • 最大连接数,表示同时最多能支持多少连接,如果连接数不够,后续要获取连接的 线程会阻塞
  • 最大空闲连接数,表示没有请求的时候,连接池中要保留的最大空闲连接
  • 最小空闲连接,当连接数小于这个值的时候,连接池需要再创建连接来补充到这个 值
  • 最大等待时间,就是连接池里面的连接用完了以后,新的请求要等待的时间,超过 这个时间就会提示超时异常
  • 无效连接清除, 清理连接池里面的无效连接,避免使用这个连接操作的时候出现 错误

69、简述一下你对线程池的理解

70、如何理解 Spring Boot 中的 Starter

71、IO 和 NIO 有什么区别

72、常见的限流算法有哪些

滑动窗口限流,本质上也是一种计数器,只是通过以时间为维度的可滑动 窗口设计,来减少了临界值带来的并发超过阈值的问题
每次进行数据统计的时候,只需要统计这个窗口内每个时间刻度的访问量就可以了。
Spring Cloud里面的熔断框架Hystrix ,以及Spring Cloud Alibaba里面的Sentinel 都采用了滑动窗口来做数据统计
在这里插入图片描述

73、TCP 协议为什么要设计三次握手

TCP 协议,是一种可靠的,基于字节流的,面向连接的传输层协议

  • TCP 是可靠性通信协议。
  • TCP 协议需要在一个不可靠的网络环境下实现可靠的数据传输,意味着通信双方必须要通过某种手段来实现一个可靠的数据传输通道。而三次通信是建立这样一个通道的最小值。当然还可以四次、五次,只是没必要浪费这个资源。
  • 防止历史的重复连接初始化造成的混乱问题。比如说在网络比较差的情况下,客户端连续多次发送建立连接的请求,假设只有两次握手,那么服务端只能选择接受或者拒绝这个连接请求,但是服务端不知道这次请求是不是之前因为网络堵塞而过期的请求,也就是说服务端不知道当前客户端的连接是有效还是无效。

74、Spring 中有哪些方式可以把Bean 注入到IOC 容器

  1. 使用xml 的方式来声明Bean 的定义,Spring 容器在启动的时候会加载并解析这个xml,把bean 装载到IOC 容器中。
  2. 声明了@Controller、@Service、@Repository、@Component 注解的类, 使用@CompontScan 注解来扫描
  3. 使用@Bean 注解实现Bean 的定义,使用@Configuration 注解声明配置类. 这种事xml方式的演变,迈入无配置化的里程碑
  4. 使用FactoryBean 工厂bean,动态构建一个Bean 实例,Spring Cloud OpenFeign 里面的动态代理实例就是使用FactoryBean 来实现的。
  5. 实现ImportBeanDefinitionRegistrar 接口,这个在Spring Boot 里面的启动注解有用到
  6. 实现ImportSelector 接口,Spring Boot 里面的自动装配机制里面有用到

75、为什么引入偏向锁、轻量级锁,介绍下升级流程

偏向锁
就是直接把当前锁偏向于某个线程,简单来说就是通过CAS 修改偏向锁标记,这种锁适合同一个线程多次去申请同一个锁资源并且没有其他线程竞争的场景。

轻量级锁也可以称为自旋锁
基于自适应自旋的机制,通过多次自旋重试去竞争锁。自旋锁优点在于它避免避免了用户态到内核态的切换带来的性能开销。

76、介绍下Spring IoC 的工作流程

  1. 第一个阶段,就是IOC 容器的初始化

在这里插入图片描述

  1. 第二个阶段,完成Bean 初始化及依赖注入
  2. 第三个阶段,Bean 的使用
    通常我们会通过@Autowired 或者BeanFactory.getBean()从IOC 容器中获
    取指定的bean 实例。

77、@Resource 和@Autowired 的区别

78、Spring 中,有两个id 相同的bean,会报错吗,如果会报错,在哪个阶段报错

id 这个属性表示一个Bean 的唯一标志符号,所以Spring 在启动的时候会去验证id 的唯一性,一旦发现重复就会报错

这个错误发生Spring 对XML 文件进行解析转化为BeanDefinition 的阶段。

但是在两个不同的Spring 配置文件里面,可以存在id 相同的两个bean。IOC 容器在加载Bean 的时候,默认会多个相同id的bean 进行覆盖。

在Spring3.x 版本以后,这个问题发生了变化我们知道Spring3.x 里面提供@Configuration 注解去声明一个配置类,然后使用
@Bean 注解实现Bean 的声明,这种方式完全取代了XMl。在这种情况下,如果我们在同一个配置类里面声明多个相同名字的bean,在Spring IOC 容器中只会注册第一个声明的Bean 的实例。后续重复名字的Bean 就不会再注册了。
像这样一段代码,在Spring IOC 容器里面,只会保存UserService01 这个实例,后续相同名字的实例不会再加载。

如果使用@Autowired 注解根据类型实现依赖注入,因为IOC 容器只有UserService01 的实例,所以启动的时候会提示找不到UserService02 这个实例。

如果使用@Resource 注解根据名词实现依赖注入,在IOC 容器里面得到的实例对象是UserService01,于是Spring 把UserService01 这个实例赋值给UserService02,就会提示类型不匹配错误。

这个错误,是在Spring IOC 容器里面的Bean 初始化之后的依赖注入阶段发生的。

79、什么是 ISR,为什么需要引入 ISR

Kafka 为了保证 Parititon 的可靠性,提供了 Paritition 的副本机制。生产者发送过来的消息,会先存到 Leader Partition 里面,然后再把消息复制到 Follower Partition,这样设计的好处就是一旦 Leader Partition 所在的节点挂了,可以重新从剩余的 Partition 副本里面选举出新的 Leader。然后消费者可以继续从新的 Leader Partition 里面获取未消费的数据。

在 Partition 多副本设计的方案里面,有两个很关键的需求。

  • 副本数据的同步
  • 新leader的选举

这两个需求都需要涉及到网络通信,Kafka 为了避免网络通信延迟带来的性能问题, 以及尽可能的保证新选举出来的 Leader Partition 里面的数据是最新的,所以设计了ISR这样一个方案。

它是一个集合列表,里面保存的是和 Leader Parition 节 点数据最接近的 Follower Partition。如果某个 Follower Partition 里面的数据落后 Leader 太多,就会被剔除 ISR 列表。简单来说,ISR 列表里面的节点,同步的数据一定是最新的,所以后续的 Leader 选举, 只需要从 ISR 列表里面筛选就行了。

好处

  1. 尽可能的保证数据同步的效率
  2. 避免数据的丢失

80、简单说一下你对序列化和反序列化的理解

首先,之所以需要序列化,核心目的是为了解决网络通信之间的对象传输问题。也就是说如何把当前jvm进程里面的一个对象,跨网络传输到另一个jvm进程里面。

而序列化就是把内存中的对象转化为字节流,用来实现存储或者传输。那么反序列化就是根据从文件或者网络上获取到的对象的字节流,根据字节流里面保存的对象信息重新构建一个对象。

81、说一下你对 CompletableFuture 的理解

CompletableFuture 是 JDK1.8 里面引入的一个基于事件驱动的异步回调类。简单来说,就是当使用异步线程去执行一个任务的时候,我们希望在任务结束以后触发 一个后续的动作。而 CompletableFuture 就可以实现这个功能。

举个简单的例子:
先查询到订单以后,再针对这个订单发起 支付,支付成功以后再发送邮件通知。

  • 如果是同步流程设计的话,导致这个方法的执行性能比较慢
  • 可以直接使用 CompletableFuture,把查询订单的逻 辑放在一个异步线程池里面去处理。然后基于 CompletableFuture 的事件回调机制的特性,可以配置查询订单结束后自动 触发支付,支付结束后自动触发邮件通知。

在这里插入图片描述

CompletableFuture 提供了 5 种不同的方式,把多个异步任务组成一个具有先后关系 的处理链,然后基于事件驱动任务链的执行。

  1. thenCombine:把两个任务组合在一起,当两个任务都执行结 束以后触发事件回调
    在这里插入图片描述

  2. thenCompose:把两个任务组合在一起,这两个任务串行执行, 也就是第一个任务执行完以后自动触发执行第二个任务。
    在这里插入图片描述

  3. thenAccept:第一个任务执行结束后触发第二个任务, 并且第一个任务的执行结果作为第二个任务的参数,这个方法是纯粹接受上一个任务的 结果,不返回新的计算值。

在这里插入图片描述

  1. thenApply:和 thenAccept 一样,但是它有返回值
    在这里插入图片描述

  2. thenRun:就是第一个任务执行完成后触发执行一个实现了 Runnable 接口的任务。

在这里插入图片描述

82、Spring 里面的事务和分布式事务的使用如何区 分,以及这两个事务之间有什么关联?

Spring 里面的事务,本质上就是数据库层面的事务。
在 Spring 里面并没有提供事务,它只是提供了对数据库事务管理的封装。 通过声明式的事务配置,使得开发人员可以从一些复杂的事务处理中得到解脱, 我们不再需要关心连接的获取、连接的关闭、事务提交、事务回滚这些操作。 更加聚焦在业务开发层面

Spring 事务和分布式事务在使用 上并没有直接的关联性。

但是我们可以使用一些主流的事务解决框架,比如 Seata,集成到 Spring 生态里面, 去解决分布式事务的问题。

83、Cookie 和 Session 的区别

  1. Cookie
    它是客户端浏览器用来保存服务端数据的一种机制。当通过浏览器进行网页访问的时候,服务器可以把某一些状态数据以 key-value 的方式 写入到 Cookie 里面存储到客户端浏览器。然后客户端下一次再访问服务器的时候,就可以携带这些状态数据发送到服务器端,服 务端可以根据 Cookie 里面携带的内容来识别使用者。

  2. Session
    Session 表示一个会话,它是属于服务器端的容器对象,默认情况下,针对每一个浏览器的请求。Servlet 容器都会分配一个 Session。Session 本质上是一个 ConcurrentHashMap,可以存储当前会话产生的一些状态数 据。
    我们都知道,Http 协议本身是一个无状态协议,也就是服务器并不知道客户端发送过 来的多次请求是属于同一个用户。所以 Session 是用来弥补 Http 无状态的不足,简单来说,服务器端可以利用 session 来存储客户端在同一个会话里面的多次请求记录。

84、线程状态,BLOCKED 和 WAITING 有什么区别

  • BLOCKED: 没有竞争到锁资源的线程。在线程的整个生命周期里面,只有 Synchronized 同步锁等待才会存在这个状态。
  • WAITING:可以使用 Object.wait()、Object.join()、LockSupport.park() 这些方法 使得线程进入到 WAITING 状态。在这个状态下,必须要等待特定的方法来唤醒, 比如 Object.notify 方法可以唤醒 Object.wait()方法阻塞的线程 LockSupport.unpark()可以唤醒 LockSupport.park()方法阻塞的线程。

85、Kafka 如何保证消息消费的顺序性

一般的解决办法就是自定义消息分区路由的算法,然后把指定的 key 都发送到同一个 Partition 里面。 接着指定一个消费者专门来消费某个分区的数据,这样就能保证消息的顺序消费了。

86、如果让你设计一个秒杀系统,怎么设计?

秒杀系统的核心:

  1. 过滤掉90%的无效请求
  2. 库存超卖

87、为什么两个 Integer 的对象不能用==号来判断?

88、JVM 分代年龄为什么是 15 次?可以 25 次吗?

一个 Java 对象在 JVM 内存中的布局由三个部分组成, 分别是对象头、实例数据、对齐填充。而对象头里面有 4 个 bit 位来存储 GC 年龄。

而 4 个 bit 位能够存储的最大数值是 15,所以从这个角度来说,JVM 分代年龄之所以 设置成 15 次是因为它最大能够存储的数值就是 15。

89、可以讲一下 ArrayList 的自动扩容机制吗

ArrayList 是一个数组结构的存储容器,默认情况下,数组的长度是 10. 当然我们也可以在构建 ArrayList 对象的时候自己指定初始长度。 随着在程序里面不断的往 ArrayList 中添加数据,当添加的数据达到 10 个的时候, ArrayList 就没有多余容量可以存储后续的数据。 这个时候 ArrayList 会自动触发扩容。 扩容的具体流程很简单, 1. 首先,创建一个新的数组,这个新数组的长度是原来数组长度的 1.5 倍。 2. 然后使用 Arrays.copyOf 方法把老数组里面的数据拷贝到新的数组里面。 扩容完成后再把当前要添加的元素加入到新的数组里面,从而完成动态扩容的过程。

90、Eureka server 数据同步原理能说下吗

Eureka 是一个服务注册中心,在 Eureka 的设计里面,为了保证 Eureka 的高可用性, 提供了集群的部署方式。

Eureka 的集群部署采用的是两两相互注册的方式来实现,也就是说每个 Eureka Server 节点都需要发现集群中的其他节点并建立连接,然后通过心跳的方式来维持这个连接的状态。

在 Eureka Server 集群中,不存在所谓主从节点,任何一个节点都可以接收 或者写入数据。 一旦集群中的任意一个节点接收到了数据的变更,就直接同步到其他节点上。

这种无中心化节点的数据同步,需要考虑到一个数据同步死循环的问题,也就是需要区 分 Eureka Server 收到的数据 是属于客户端传递来的数据还是集群中其他节点发过来的同步数据。 Eureka 使用了一个时间戳的标记来实现类似于数据的版本号来解决这个问题。

另外,从 Eureka 的数据同步方案来看,Eureka 集群采用的是 AP 模型,也就是只提供 高可用保障,而不提供数据强一致性保障。 之所以采用 AP,我认为注册中心它只是维护服务之间的通信地址,数据是否一致对于 服务之间的通信影响并不大。 而注册中心对 Eureka 的高可用性要求会比较高,不能出现因为 Eureka 的故障导致服 务之间无法通信的问题。

91、请说一下你对分布式和微服务的理解

其实微服务架构本身就是一种分布式架构,它强调的是对部署在各个计算机上的应用服 务的粒度。

拆分的好处是使得程序的扩展性更强,开发迭代效率更高。 对于一些大型的互联网项目来说,微服务能够在不影响用户使用的情况下非常方便的实 现产品功能的创新和上线。

92、请你说一下你对滑动窗口算法的理解

简单来说,就是在一个大的数组上,定义一个固定长度的滑动窗口,然后这个 窗口在数组上进行滑动。
在这里插入图片描述

在窗口滑动的过程中,左边会出一个元素,右边会进一个元素, 最后,我们根据当前 窗口内记录的数据进行计算。 从而达到数据统计的目的。

  1. 在 Hystrix 中,用到了滑动窗口来实现熔断触发的数据统计。
  2. 在 Sentinel 限流框架中,也使用了滑动窗口来实现限流的数据统计。

93、什么是深拷贝和浅拷贝

94、ConcurrentHashMap 的 size()方法是线程安全 的吗?为什么

ConcurrentHashMap 的 size()方法是非线程安全的。

size()方法是一个非同步方法。put()方法和 size()方法并没有实现同步锁。

这里用到了 CAS 的方式解决了并发更新问题,因此从 size()方法本身来看,它的整个计算过程是线程安全的。但是站在 ConcurrentHashMap 全局角度来看,put()方法和 size()方法之间的数据是 不一致的,因此也就不是线程安全的。

之所以不像 HashTable 那样,直接在方法级别加同步锁。在我看来有两个考虑点。

  1. 直接在 size()方法加锁,就会造成数据写入的并发冲突,对性能造成影响
  2. ConcurrentHashMap 并发集合中,对于 size()数量的一致性需求并不大,并发集 合更多的是去保证数据存储的安全性。

95、Nacos 配置更新的工作流程

在这里插入图片描述

Nacos 是采用长轮训的方式向 Nacos Server 端发起配置更新查询的功能。 所谓长轮训,就是客户端发起一次轮训请求到服务端,当服务端配置没有任何 变更的时候,这个连接一直打开。 直到服务端有配置或者连接超时后返回。

Nacos Client 端需要获取服务端变更的配置,前提是要有一个比较, 也就是拿客户端本地的配置信息和服务端的配置信息进行比较。 一旦发现和服务端的配置有差异,就表示服务端配置有更新,于是把更新的配置拉到本 地。

在这个过程中,有可能因为客户端配置比较多,导致比较的时间较长,使得配置同步较 慢的问题。于是 Nacos 针对这个场景,做了两个方面的优化。

  1. 减少网络通信的数据量,客户端把需要进行比较的配置进行分片,每一个分片大小 是 3000, 也就是说,每次最多拿 3000 个配置去 Nacos Server 端进行比较。
  2. 分阶段进行比较和更新,先比较配置文件中的key+value的md5值和服务端的是否一致,再循环逐步调用服务获取需要更新key的value(核心思想就是,增加网络通信次数,减少网络通信数据包的大小。最后,再采用长连接这种方式,既减少了 pull 轮询次数,又利用了长连接的优势,很 好的实现了配置的动态更新同步功能。)

96、RabbitMQ 的消息如何实现路由

在这里插入图片描述

AMQP 的具体工作机制是,生产者把消息发送到 RabbitMQ Broker 上的 Exchange 交换机上。 Exchange 交换机把收到的消息根据路由规则发给绑定的队列(Queue)。 最后再把消息投递给订阅了这个队列的消费者,从而完成消息的异步通讯。

其中,Exchange 是一个消息交换机,它里面定义了消息路由的规则,也就是这个消息 路由到那个队列。

然后 Queue 表示消息的载体,每个消息可以根据路由规则路由到一个或者多个队列里 面。

而关于消息的路由机制,核心的组件是 Exchange。它负责接收生产者的消息然后把消息路由到消息队列,而消息的路由规则由 ExchangeType 和 Binding 决定。

Binding 表示建立 Queue 和 Exchange 之间的绑定关系,每一个绑定关系会 存在一个 BindingKey。

生产者发送消息的时候,需要声明一个 routingKey(路由键),Exchange 拿到 routingKey 之后, 根据 RoutingKey 和路由表里面的 BindingKey 进行匹配,而匹配的规则是通过 ExchangeType 来决定的。

在这里插入图片描述
在 RabbitMQ 中,有三种类型的 Exchange:direct ,fanout 和 topic。

  • direct: 完整匹配方式,也就是 Routing key 和 Binding Key 完全一致,相当于 点对点的发送。
  • fanout: 广播机制,这种方式不会基于 Routing key 来匹配,而是把消息广播给 绑定到当前 Exchange 上的所有队列上。
  • topic: 正则表达式匹配,根据 Routing Key 使用正则表达式进行匹配,符合匹配 规则的 Queue 都会收到这个消息

97、如何保证 RabbitMQ 的消息可靠传输

首先,在 RabbitMQ 的整个消息传递过程中,有三种情况会存在丢失。

  • 生产者把消息发送到 RabbitMQ Server 的过程中丢失
  • RabbitMQ Server 收到消息后在持久化之前宕机导致数据丢失
  • 消费端收到消息还没来得及处理宕机,导致 RabbitMQ Server 认为这个消息已签收

生产者发送消息的角度
RabbitMQ 提供了一个 Confirm(消息确认)机制, 生产者发送消息到 Server 端以后,如果消息处理成功,Server 端会返回一个 ack 消息。 客户端可以根据消息的处理结果来决定是否要做消息的重新发送,从而确保消息一定到 达 RabbitMQ Server 上。

RabbitMQ Server角度
开启消息的持久化机制。

消费端的角度
自动确认机制修改成手动确认。这种方式可能会造成重复消费问题,所以这里需要考虑到幂等性的设计。

98、请说一下 Netty 中 Reactor 模式的理解

Reactor 其实是在 NIO 多路复用的基础上提出的一个高性能 IO 设计模式。核心思想是把响应 IO 事件和业务处理进行分离,通过一个或者多个线程来处理 IO 事件。然后把就绪的事件分发给业务线程进行异步处理。

99、HashMap 中的 hash 方法为什么要右移 16 位异 或?

核心目的是为了让 hash 值的散列 度更高, 尽可能减少 hash 表的 hash 冲突,从而提升数据查找的性能

100、DCL 单例模式设计为什么需要 volatile 修饰实例 对象

101、说一下你对行锁、临键锁、间隙锁的理解

  1. 行锁,也称为记录锁。当我们针对主键或者唯一索引加锁的时候,Mysql 默认会对查询的这一行数据加行锁, 避免其他事务对这一行数据进行修改。
  2. 间隙锁,顾名思义,就是锁定一个索引区间。在普通索引或者唯一索引列上,由于索引是基于 B+树的结构存储,所以默认会存在一 个索引区间。在基于索引列的范围查询,无论是否是唯一索引,都会自动触发间隙锁。 比如基于 between 的范围查询,就会产生一个左右开区间的间隙锁。
  3. 临键锁,它相当于行锁+间隙锁的组合,也就是它的锁定范围既包 含了索引记录,也包含了索引区间 它会锁定一个左开右闭区间的数据范围。假设我们使用非唯一索引列进行查询的时候,默认会加一个临键锁,锁定一个 左开右闭区间的范围。

最终目的是为了解决 幻读的问题。

102、生产环境服务器变慢,如何诊断处理?

  • CPU

CPU 利用率过高或者 CPU 利用率过低,都会影响程序的处理效率。

  1. 过高。说明当前服务器要处理的指令比较多,当 CPU 忙不过来的时候,指令的 运算效率自然就会下降。反馈在用户上的感受就是程序响应变慢了。针对这个问题,我们可以使用 top 命令查询当前系统中占用 CPU 过高的进程,以及定 位到这个进程中比较活跃的线程。 再通过 jstack 命令打印当前虚拟机的线程快照,然后根据快照日志排查问题代码。
  2. 过低。说明程序资源使用不够,可以增加线程数量提升程序性能。
  • 内存
    内存使用率比较高的时候, 可以 dump 出 JVM 堆内存,然后借助 MAT 工具进行 分析, 查出大对象或者占用最多的对象,以及排查是否存在内存泄漏的问题。如果 dump 出的堆内存文件正常,此时可以考虑堆外内存被大量使用导致出现问题, 需要借助操作系统指令 pmap 查出进程的内存分配情况。如果 CPU 和 内存使用率都很正常,那就需要进一步开启 GC 日志,分析用户线程暂 停的时间、 各部分内存区域 GC 次数和时间等指标,可以借助 jstat 或可视化工具 GCeasy 等, 如果问题出在 GC 上面的话,考虑是否是内存不够、根据垃圾对象的特点进行参数调 优、使用更适合的垃圾收集器; 分析 jstack 出来的各个线程状态。如果问题实在比较隐蔽,考虑是否可以开启 jmx, 使用 visualmv 等可视化工具远程监控与分析。
  • 磁盘IO
  1. 使用 iostat 命令查看磁盘IO较高的应用
  • 借助缓存,减少IO次数

103、HashMap 啥时候扩容,为什么扩容?

loadFactor 的默认值是 0.75,capacity 的默认值是 16,也就是元素个数达到 12 的时 候触发扩容。 扩容后的大小是原来的 2 倍。

104、线程池的线程回收

默认情况下,线程池只会回收非核心线程,非核心线程是为了解决任务过多的时候临时增加的,所以当任务处理完成后, 工作线程处于空闲状态的时候,就需要回收。

因为所有工作线程都是从阻塞队列中去获取要执行的任务,所以只要在一定时间内, 阻塞队列没有任何可以处理的任务,那这个线程就可以结束了。 这个功能是通过阻塞队列里面的 poll 方法来完成的。这个方法提供了超时时间和超时 时间单位这两个参数 当超过指定时间没有获取到任务的时候,poll 方法返回 null,从而终止当前线程完成线 程回收。

如果希望核心线程也要回收,可以 设置 allowCoreThreadTimeOut 这个属性为 true,一般情况下我们不会去回收核心线 程。因为线程池本身就是实现线程的复用,而且这些核心线程在没有任务要处理的时候是处 于阻塞状态 并没有占用 CPU 资源。

105、索引什么时候失效?

  1. 索引列做运算
  2. 类型转换
  3. 组合索引中,需要按照最左匹配原则
  4. 在索引列使用不等于号、not 查询的时候,由于索引数据的检索效率非常低,因此 Mysql 引擎会判断不走索引。
  5. like
  6. or 语句前后没有同时使用索引,那么索引会失效

106、怎么防止缓存击穿

缓存击穿,表示请求因为某些原因全部打到了数据库,缓存并没有起到流量缓冲的作用。 我认为有 2 种情况会导致缓存击穿。

  • 在 Redis 里面保存的热点 key,在缓存过期的瞬间,有大量请求进来,导致请求全 部打在数据库上
  • 客户端恶意发起大量不存在的 key 的请求,由于访问的 key 对应的数据本身就不存 在, 所以每次必然都会穿透到数据库,导致缓存成为了摆设

解决办法

  1. 对于热点数据,我们可以不设置过期时间,或者在访问数据的时候对数据过期时间 进行续期。
  2. 对于访问量较高的缓存数据,我们可以设计多级缓存,尽量减少后端存储设备的压 力。
  3. 使用分布式锁,当发现缓存失效的时候,不是先从数据库加载,而是先获取分布式 锁,获得分布式锁的线程从数据库查询数据后写回到缓存里面。 后续没有获得锁的线程就只需要等待和重试即可。
  4. 对于恶意攻击类的场景,可以使用布隆过滤器,应用启动的时候把存在的数据缓存 到布隆过滤器里面。 每一次请求进来的时候先访问布隆过滤器, 如果不存在,则说明这个数据一定没有在数据库里面,就没必要再去访问数据库了。

107、什么是 IO 的多路复用机制

IO 多路复用机制,核心思想是让单个线程去监视多个连接,一旦某个连接就绪,也就 是触发了读/写事件。 就通知应用程序,去获取这个就绪的连接进行读写操作。 也就是在应用程序里面可以使用单个线程同时处理多个客户端连接, 在对系统资源消耗较少的情况下提升服务端的链接处理数量。 在 IO 多路复用机制的实现原理中, 客户端请求到服务端后,此时客户端在传输数据过程中(如图), 为了避免 Server 端在 read 客户端数据过程中阻塞,服务端会把该请求注册到 Selector 复路器上, 服务端此时不需要等待,只需要启动一个线程, 通过 selector.select()阻塞轮询复路器上就绪的 channel 即可, 也就是说,如果某个客户端连接数据传输完成, 那么 select()方法会返回就绪的 channel,然后执行相关的处理就可以了。

108、Java 有几种文件拷贝方式,哪一种效率最高?

  1. 使用 java.io 包下的库,使用 FileInputStream 读取,再使用 FileOutputStream 写出。
  2. 利用 java.nio 包下的库,使用 transferTo 或 transfFrom 方法实现。
  3. Java 标准类库本身已经提供了 Files.copy 的实现

NIO 里面提供的 NIO transferTo 和 transfFrom 方法,也就是常说的零拷贝实现。 它能够利用现代操作系统底层机制,避免不必要拷贝和上下文切换,因此在性能上表现 比较好。

109、线程池是如何实现线程复用的

在这里插入图片描述
基于阻塞队列的特性,使得阻塞队列中如果没有任务的时候,这些工作线程就会阻塞等 待。直到又有新的任务进来,这些工作线程再次被唤醒。

110、可以说下阻塞队列被异步消费怎么保持顺序吗

对于阻塞队列的消费过程

  • 如果阻塞队列里面已经包含了很多任务,这个时候启动多个消费者去消费 的时候, 它的有序性保证是通过加锁来实现的,也就是每个消费者线程去阻塞队列获取任务的时 候必须要先获得排他锁。
  • 如果阻塞队列中没有任务的时候,消费者线程这个时候也是会被放到一个阻塞队列中去的,当有新的任务加入,那消费者线程队列会严格按照FIFO的顺序来唤醒,来保证消费的顺序。

111、当任务数超过线程池的核心线程数时,如何让它不 进入队列,而是直接启用最大线程数

SynchronousQueue

112、SimpleDateFormat 是线程安全的吗? 为什 么?

SimpleDateFormat 不是线程安全的

SimpleDateFormat 类内部有一个 Calendar 对象引用, 它用来储存和这个 SimpleDateFormat 相关的日期信息。多个线程对于同一个 Calendar 的操作,会出现数据脏读现象导致一些不可预料的错误。

解决办法

  1. 在 Java8 里面引入了一些线程安全的日期 API,比如 LocalDateTimer、 DateTimeFormatter 等
  2. 使用ThreadLocal
  3. 同步锁,同一时刻只允许一个线程操作 SimpleDateFormat
  4. 把 SimpleDateFormat 定义成局部变量,每个线程调用的时候都创建一个新的实例

113、Http 协议和 RPC 协议有什么区别?

从定位上来说,

  • http 是一个属于应用层的超文本传输协议,是万维网数据通信的基础,主要服务在 网页端和服务端的数据传输上。
  • RPC 是一个远程过程调用协议,它的定位是实现不同计算机应用之间的数据通信, 屏蔽通信底层的复杂性,让开发者就像调用本地服务一样完成远程服务的调用。

因此,这两个协议在定位层面就完全不同。

其次,从原理上来说,

  • http 协议定义了通信的报文格 式 Request Body 和 Request Header,以及 Response Body 和 Response Header。也就是说,符合这样一个协议特征的通信协议,才是 http 协议。
  • RPC协议没有具体的实现,在实现RPC框架的时候,可以自定义报文通信的协议规范、自定义序列化方式,包括自定义网络通信协议的类型等

最后,从应用层面来说

  • http和RPC协议底层都是以tcp协议作为通信基础,RPC 的网络通信层也可以使用 HTTP 协议来实现,比如 gRPC、OpenFeign 底层都采用了 http 协议。

114、如何解决 TCC 中的悬挂问题

在 TCC 这个事务解决方案里面,除了悬挂问题以外,还有空回滚、幂等性需 要考虑。

什么是TCC
所谓 TCC,其实就是(Try-Confirm-Cancel),也就是把一个事务拆分成两个阶段, 类似于传统的 XA 事务模型。

  • Try 这个阶段,是实现业务的检查,预留必要的业务资源。
  • Confirm,真正执行业务逻辑,只需要使用 try 阶段预留的业务资源进行处理就行。
  • Cancel,如果事务执行失败,就通过 cancel 方法释放 try 阶段预留的资源。

在这里插入图片描述
很显然,这是一个最终一致性的实现方案,因此当 Try 执行成功,就必须确保 Confirm 执行成功。 当 Try 执行失败,就必须确保 Cancel 实现资源释放。

悬挂问题
指的是 TCC 执行 Try 接口出现网络超时时候,使得 TCC 触 发 Cancel 接口回滚,但可能在回滚之后,这个超时的 Try 接口才被真正执行,也就导致 Cancel 接口比 Try 接口先执行。从而造成 Try 接口预留的资源一直无法释放,这种情况就是悬挂。

解决办法
只需要保证 Cancel 接口执行完以后,Try 接口不允许在执行就 可以了。所以,我们可以在 Try 接口里面,先判断 Cancel 接口有没有执行过,如果已经执行过, 就不再执行。

是否执行过的这个判断,可以在事务控制表里面插入一条事务控制记录来标记这个事务 的回滚状态。然后在 Try 接口中只需要读取这个状态来判断就行了。

115、什么是令牌桶限流算法

令牌桶是一种能够处理突发流量的限流算法,系统以恒定速率向令牌桶 里面添加令牌,然后每个请求都需要从令牌桶去获取令牌才能访问,如果获取不到,就会触发限流。
在这里插入图片描述

116、Java 常见的垃圾收集器有哪些

  • CMS:(jdk9已经废弃)
    基于标记-清除算法,设计目标是尽量减少停顿时间,这一点对于web等反应时间敏感的应用非常重要,有很多系统使用CMS GC。但是,CMS采用的标记-清除算法,存在着内存碎片化的问题,所以难以避免在长时间运行情况下发生full gc。
    并且这款GC强调了并发,会占用更多的CPU资源,并和用户线程争抢。
  • G1:(值得深入)
    兼顾吞吐量和停顿时间的GC实现,jdk9以后默认的GC。G1 GC整体上是标记-整理算法,可以有效的避免内存碎片的问题,尤其是Java堆非常大的时候,G1的优势更加明显。

117、RabbitMQ 如何实现高可用

在这里插入图片描述

118、为什么 ConcurrentHashMap 中 key 不允许为 null

为了避免在多线程并发场景下的歧义问题。当一个线程从 ConcurrentHashMap 获取某个 key,如果返回的结果是 null 的时候。 这个线程无法确认,这个 null 表示的是确实不存在这个 key,还是说存在 key,但是 value 为空。 这种不确定性会造成线程安全性问题,而 ConcurrentHashMap 本身又是一个线程安 全的集合。

119、ThreadLocal 如何解决内存泄漏的问题

  1. 每次使用完 ThreadLocal 以后,主动调用 remove()方法移除数据
  2. 把 ThreadLocal 声明称全局变量,使得它无法被回收

120、在 Java 中实现单例模式有哪些方法

  1. 基于双检锁
  2. 通过静态内部类
  3. 枚举,既是线程安全,又能防止反序列化导致的破坏单例

多线程、克隆、反序列化、反射,都有可能会造成单例的破坏。 而我认为,通过枚举的方式实现单例,是能够解决所有可能被破坏的情况

121、请简要说明 Mysql 中 MyISAM 和 InnoDB 引擎 的区别

MyISAm 和 InnoDB 的区别有 4 个,

  1. 数据存储的方式不同,MyISAM 中的数据和索引是分开存储的, 而 InnoDB 是把索引和数据存储在同一个文件里面
  2. 对于事务的支持不同,MyISAM 不支持事务,而 InnoDB 支持 ACID 特性 的事务处理
  3. 对于锁的支持不同,MyISAM 只支持表锁,而 InnoDB 可以根据不同的 情况,支持行锁,表锁,间隙锁,临键锁
  4. MyISAM 不支持外键,InnoDB 支持外键

122、存储 MD5 的值应该用 VARCHAR 还是 CHAR

我认为应该使用 Char 类型,原因是: char 类型是固定长度的字符串,varchar 是可变长度字符串。 而 MD5 是一个固定长度的字符,不管数据怎么修改,长度不变,这个点很符合 char 类型。 另外,由于是固定长度,所以在数据变更的时候,不需要去调整存储空间大小,在效率 上会比 varchar 好。

123、Redis 哨兵机制和集群有什么区别(Redis 哨兵集群和 Redis Cluster 的区别。)

  1. Redis 哨兵集群是基于主从复制来实现的,所以它可以实现读写分离,分担 Redis 读操作的压力 而 Redis Cluster 集群的 Slave 节点只是实现冷备机制,它只有在 Master 宕机之后才 会工作。
  2. Redis 哨兵集群无法在线扩容,所以它的并发压力受限于单个服务器的资源配置。 Redis Cluster 提供了基于 Slot 槽的数据分片机制,可以实现在线扩容提升写数据的性 能
  3. 从集群架构上来说,Redis 哨兵集群是一主多从, 而 Redis Cluster 是多主多从

124、@Conditional 注解有什么用

@Conditional 注解的作用是为 Bean 的装载提供了一个条件判断。只有满足条件的情况下,Spring 才会把当前 Bean 装载到 IOC 容器中。 这个条件的实现逻辑,我们可以实现 Condition 接口并重写 matches 方法自己去实现。 所以@Conditional 注解增加了 Bean 装载的灵活性。

在 Spring Boot 里面,对@Conditional 注解做了更进一步的扩展,比如增加了 @ConditionalOnClass、@ConditionalOnBean
等注解,使得我们在使用的过程中不再需要去写条件的逻辑。

125、Redis 主从复制的原理

Redis 主从复制包括全量复制和增量复制。
全量复制
全量复制是发生在初始化阶段,从节点会主动向主节点发起一个同步请求,主节点收到 请求后会 会生成一份当前数据的快照发送给从节点,从节点收到数据进行加载后完成全量复制。 增量复制是发生在每次 Master 数据发生变化的过程中,会把变化的数据同步给所有的 从节点。

增量复制
增量复制是通过维护 Offset 这个复制偏移量来实现的

126、介绍下策略模式和观察者模式?

策略模式和观察者模式属于行为型模式。

策略模式主要是用在根据上下文动态控制类的行为的场景,

  • 一方面可以解决多个 if…else 判断带来的代码复杂性和维护性问题
  • 另一方面,把类的不同行为进行封装,使得程序可以进行动态的扩展和替换,增加 了程序的灵活性。

观察者模式主要用在一对多的对象依赖关系的中,实现某一个对象状态变更之后的感知 的场景

  • 一方面可以降低对象依赖关系的耦合度,弱化依赖关系
  • 另一方面,通过这种状态通知机制,可以保证这些依赖对象之间的状态协同。

127、Java 反射的优缺点

Java 反射的优点:  增加程序的灵活性,可以在运行的过程中动态对类进行修改和操作  提高代码的复用率,比如动态代理,就是用到了反射来实现  可以在运行时轻松获取任意一个类的方法、属性,并且还能通过反射进行动态调用 Java 反射的缺点:  反射会涉及到动态类型的解析,所以 JVM 无法对这些代码进行优化,导致性能要 比非反射调用更低。  使用反射以后,代码的可读性会下降  反射可以绕过一些限制访问的属性或者方法,可能会导致破坏了代码本身的抽象性。

128、如何处理消息队列的消息积压问题

  • 系统bug导致
  1. 首先解决系统bug,然后临时做紧急扩容来完成大量消息的消费。
  2. 把现在的所有消费端停止,然后新建一个topic,把partition分区数量调整成原来的10倍
  3. 接着写一个用来实现数据分发的consumer程序,这个程序专门消费现在积压的数据,消费后不做处理,而是直接把这些数据写入临时建立的topic的10个partition中
  4. 然后临时增加10倍的消费者结点来部署consumer,专门来消费临时的partition分区数据
  5. 等积压的消息处理完,再把恢复成原来的部署架构,把临时的topic和临时申请的机器释放掉。
  • 非系统bug导致
  1. 优化消费端的逻辑,比如通过异步的方式来处理消息、或者通过批量处理的方式来消费。
  2. 如果通过这两种优化方式还没有缓解,可以考虑对消费端进行水平扩容,从而扩大消费 端的消费能力。

129、对称加密与非对称加密有什么区别

对称加密
指加密和解密使用同一密钥,优点是运算速度较快,常见的对称加密算 法有:DES、AES 等
在这里插入图片描述

非对称加密
指的是加密和解密使用不同的密钥,也称为公钥和私钥。公钥与私钥是成 对存在的,如果用公钥对数据进行加密,只有对应的私钥才能解密。反之,如果采用私 钥加密,只能使用对应的公钥才能解密,RSA 是比较常见的非对称加密算法。
在这里插入图片描述

130、select 和 epoll 的区别

  • select 是基于轮询的机制
  • epoll 是基于事件通知的机制

131、在 2G 大小的文件中,找出高频 top100 的单词

  1. 把 2G 的文件进行分割成大小为 512KB 小文件,总共得到 2048 个小文件,避免一 次性读入整个文件造成内存不足。
  2. 定义一个长度为 2048 的 hash 表数组,用来统计每个小文件中单词出现的频率。
  3. 使用多线程并行遍历 2048 个小文件,针对每个单词进行 hash 取模运算分别存储 到长度为 2048 的 hash 表数组中
  4. 接着再遍历这 2048 个 hash 表,把频率前 100 的单词存入小顶堆中
  5. 最后,小顶堆中最终得到的 100 个单词,就是 top 100 了

132、说一下 Kafka 中 Partition 分区副本的 Leader 选 举算法

在 Kafka 中,除了 Partition 分区副本的 Leader 选举以外,还有 Kafka 集群本身 的 Leader 选举,这两个不是同一个东西

Kafka 首先会选择一个具有最新数据的副本作为新的 Leader,也就是 ISR 集合中的副 本。其中,ISR(In-Sync Replica)是指与 Leader 同步的副本集合,它们的数据同步状态 与 Leader 最接近, 并且它们与 Leader 副本的网络通信延迟最小。 如果 ISR 集合中没有可用的副本,Kafka 会从所有副本中选择一个具有最新数据的副本 作为新的 Leader。 在这种情况下选举出来的 Leader,由于和原来老的 Leader 节点的数据存在较大的延 迟,会造成数据丢失的情况

133、数据量达到多少的时候要开始分库分表

  1. 单表数据量:如果单个表的数据量已经非常大,例如超过了百万级别,就需要开始 考虑分表。
  2. 数据库性能:当单个数据库的性能无法满足业务需求时,就需要考虑分库
  3. 数据访问频率:如果某些表的数据访问频率非常高,单个数据库节点无法满足高并 发请求,就需要考虑将这些表分到不同的库或表中,以提高性能
  4. 业务拆分:当系统的业务逻辑越来越复杂,不同的业务之间的数据耦合度越来越低, 就需要考虑对系统进行拆分,以方便管理和扩展

134、表数据量大的时候,影响查询效率的主要原因有哪 些

  1. 索引失效
  2. 分页
  3. 锁竞争
  4. 磁盘IO
    数据量大意味着需要从磁盘中读取更多的数据,而磁盘 IO 速度是相对较 慢的,因此会影响查询效率。

135、MySQL update 是锁行还是锁表

  • 如果 update 语句中的 where 条件包含了索引列,并且只更新一条数据,那这个时 候就加行锁。如果 where 条件中不包含索引列,这个时候会加表锁。

136、SpringBoot 如何解决跨域问题

CORS

它的工作原理很简单。 如果一个网站需要访问另一个网站的资源,浏览器会先发送一个 OPTIONS 请求, 根据服务器返回的 Access-Control-Allow-Origin 头信息,决定是否允许跨域访问。 所以,我们只需要在服务器端配置 Access-Control-Allow-Origin 属性,并配置允许 哪些域名支持跨域请求即可。

137、过滤器和拦截器有什么区别?

在这里插入图片描述

  1. 运行顺序不同,过滤器是在 Servlet 容器接收到请求之后,但在 Servlet 被调用之前运行的;而拦截器则是在 Servlet 被调用之后,但在响应被发送到客户 端之前运行的
  2. 配置方式不同,过滤器是在 web.xml 中进行配置;而拦截器的配置则是在 Spring 的配置文件中进行配置,或者使用注解进行配置。
  3. Filter 依赖于 Servlet 容器,而 Interceptor 不依赖于 Servlet 容器
  4. Filter 在过滤是只能对 request 和 response 进行操作,而 interceptor 可以对 request、response、handler、modelAndView、exception 进行操作。

138、什么情况下不建索引

  1. 数据量太小的情况
  2. 存在函数操作
  3. 频繁变更的表,比如经常需要更新、删除或插入记录

139、Spring 中的 Bean 是线程安全的吗

在 Spring 中,只有有状态的单例 Bean 才会存在线程安全 问题。

处理有状态单例 Bean 的线程安全问题有以下三种方法:
1、将 Bean 的作用域由 “singleton” 单例 改为 “prototype” 多例。
2、在 Bean 对象中避免定义可变的成员变量,当然,这样做不太现实,就当我没说。
3、在类中定义 ThreadLocal 的成员变量,并将需要的可变成员变量保存在 ThreadLocal 中,ThreadLocal 本身就具备线程隔离的特性,这就相当于为每个线 程提供了一个独立的变量副本,每个线程只需要操作自己的线程副本变量,从而解决线 程安全问题

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值