[关闭]
@yexiaoqi 2023-06-07T06:57:29.000000Z 字数 6678 阅读 90

近期整理

面试


数据结构

MongoDB为啥用B树而不是B+树

  1. B-树所有节点都存储数据,因此在查询单条数据的时候,B树的查询效率不固定,最好的情况是O(1)。我们可以认为在做单一数据查询的时候,B树的平均性能更好,但不适合做一些数据遍历操作。
  2. B+树的数据只出现在叶子节点上,因此查询单条数据的时候,查询速度非常稳定,但是在单一数据查询上平均性能不如B树。因为叶子节点有指针相连,适合做范围查询
  3. MonogoDB是nosql,对遍历的需求没有关系型数据库强烈

网络

TCP 四次挥手过程,为什么是四次?

TCP四次挥手过程

客户端第一次发送 FIN 报文之后,只是代表客户端不再发送数据给服务端,但此时客户端还是有接收数据的能力。服务端收到 FIN 报文的时候,可能还有数据要传输给客户端,所以只能先回复 ACK 给客户端。等到服务端不在有数据发送给客户端时,才发送 FIN 报文给客户端,表示可以关闭了。

四次挥手中有TIME_WAITCLOSE_WAIT状态,是出现在哪一方的?

TIME_WAIT:主动断开连接方
CLOSE_WAIT:被动断开连接方

假设 TIME_WAIT 状态过多会有什么危害,怎么解决?

危害:占用内存资源和端口,可以通过修改 Linux 内核参数解决。
TCP建立连接就是交换双方的状态

为什么TCP4次挥手时等待为2MSL?

第一个MSL是为了等自己发出去的最后一个ACK从网络中消失,而第二MSL是为了等在对端收到ACK之前的一刹那可能重传的FIN报文从网络中消失。等待2MSL时间,A就可以放心地释放TCP占用的资源、端口号,此时可以使用该端口号连接任何服务器。

如果不等,释放的端口可能会重连刚断开的服务器端口,这样依然存活在网络里的老的TCP报文可能与新TCP连接报文冲突,造成数据冲突,为避免此种情况,需要耐心等待网络老的TCP连接的活跃报文全部死翘翘,2MSL时间可以满足这个需求

nginx是四层负载还是七层负载?

四层负载均衡(基于IP+端口的负载均衡)

在三层负载均衡基础上,通过发布的VIP,然后加上四层的端口号,来决定哪些流量需要做负载均衡,对需要的流量进行NAT处理,转发给后台服务器,并记录下这个TCP/UDP的流量是由哪台服务器处理的,后续这个连接的所有流量转发到同一台服务器。

四层负载均衡的实现有:
- F5:硬件负载均衡器,性能强大,成本高
- LVS:重量级四层负载均衡软件
- Haproxy:模拟四层、七层转发,较灵活

七层负载均衡(基于虚拟的URL或主机IP的负载均衡)

主要通过报文中真正有意义的应用层内容,再加上负载均衡设备设置的服务器选择策略,决定最终的内部服务器。在四层负载均衡基础上,再考虑应用层特征,比如同一个Web服务器的负载均衡可以根据 IP+端口、url、浏览器类别、语言来决定是否要进行负载均衡。

先代理最终服务器与客户端三次握手后,才可能接触到真正的应用层报文。这种情况下类似于一个代理服务器,负载均衡和前端客户端、后端服务器会分别建立TCP连接。因此七层负载均衡明显对设备要求更高,更灵活,吞吐量低于四层。

七层负载均衡实现:
- Haproxy:全面支持四层、七层会话代理,会话保持,标记,路径转移
- nginx:只在http和mail协议上功能比较好,性能与haproxy相当

为什么常用的端口最大65535?

系统通过一个四元组{local ip, local port, remote ip, remote port}来唯一标识一条TCP连接。对于IPv4,系统理论上最多管理2^(32+16+32+16) -> 2^96个连接,如果考虑协议号,则是五元组。

TCP协议中 port 大小为 16bit,所以不能超过 2^16-1=65565

IO

IO模型

数据库

MySQL锁

在 InnoDB 引擎下,按照锁的粒度,可以简单分为行锁表锁

行锁是作用在索引上的,SQL命中了索引,锁住的就是命中条件内的索引节点(行锁)。如果没有命中索引,那锁住的就是整个索引树(表锁)。

行锁可以简单分为读锁写锁。读锁是共享的,多个事务可以读取同一个资源,但不允许其他事务修改。写锁是排他的,写锁会阻塞其他的写锁和读锁。

事务的四大特性

MySQL隔离级别

MVCC原理

  1. 获取事务自己的版本号,即事务ID
  2. 获取 Read View
  3. 查询到的数据,然后 Read View 中的事务版本号进行比较
  4. 如果不符合 Read View 的可见性规则,就需要 undo log 中历史快照
  5. 最后返回符合规则的数据

Java基础

面向对象三大特性

继承、封装、多态

Object类常用方法,equals & hashcode,在hashmap里哪里用到了

hashcode方法是为了加快比较效率,如果hashcode相同再用equals比较。同一个对象hashCode必须相同,hashCode相同不一定是同一个对象。
如果重写equals的时候没有重写hashCode,会违反Object.hashCode的通用约定,导致该类无法结合所有基于散列的集合一起工作,包括HashMap、HashSet、HashTable等。

wait、notify等方法为什么定义在Object中而不是Thread类中?

synchronized 锁升级过程

synchronized 有三种使用方式:

Java 对象结构分为 对象头、对象体、对齐字节。对象头包含三部分:

锁升级过程按照无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁方向升级,无法进行锁降级。

  1. 线程A在进入同步代码块前,先检查 MarkWord 中的线程 ID 是否与当前线程 ID 一致,如果一致,则直接无需通过 CAS 来加锁、解锁
  2. 如果不一致,再检查是否为偏向锁,如果不是,自适应自旋等待锁释放
  3. 如果是,在检查对应线程是否存在,如果不在,则设置线程 ID 为线程 A 的 ID,还是偏向锁
  4. 如果在,撤销偏向锁,升级为轻量级锁。线程 A 自旋等待锁释放
  5. 如果自旋到了阈值,锁还没释放,又有一个线程来竞争锁,此时升级为重量级锁。其他竞争线程都被阻塞,防止CPU空转
  6. 锁被释放,唤醒所有阻塞线程,重新竞争锁

线程池中线程抛异常了,如何处理?

  1. public class ThreadPoolException {
  2. public static void main(String[] args) {
  3. //创建一个线程池
  4. ExecutorService executorService=Executors.newFixedThreadPool(1);
  5. //当线程池抛出异常后submit无提示,其他线程继续执行
  6. executorService.submit(new task());
  7. //当线程池抛出异常后execute抛出异常,其他线程继续执行新任务
  8. executorService.execute(new task());
  9. }
  10. }
  11. //任务类
  12. class task implements Runnable{
  13. @Override
  14. public void run() {
  15. System.out.println("进入了task方法!!!");
  16. int i=1/0;
  17. }
  18. }

任务提交方式中,execute会打印异常信息,submit不打印异常,要想获取异常信息就必须使用get()

  1. //当线程池抛出异常后submit无提示,其他线程继续执行
  2. Future<?> submit = executorService.submit(new task());
  3. submit.get();

方案一:使用try-catch
方案二:使用Thread.setDefaultUncaughtExceptionHandler方法捕获异常。
重写线程工厂方法,在线程工厂创建线程的时候,赋予UncaughtExceptionHandler处理器对象
方案三:重写afterExecute进行异常处理

  1. public class ThreadPoolException3 {
  2. public static void main(String[] args) throws InterruptedException,ExecutionException {
  3. //1.创建一个自己定义的线程池
  4. ExecutorService executorService = new ThreadPoolExecutor(2,3,0,TimeUnit.MILLISECONDS,new LinkedBlockingQueue(10)) {
  5. //重写afterExecute方法
  6. @Override
  7. protected void afterExecute(Runnable r, Throwable t) {
  8. //这个是excute提交的时候
  9. if (t != null) {
  10. System.out.println("获取到excute提交的异常信息,处理异常"+t.getMessage());
  11. }
  12. //如果r的实际类型是FutureTask那么是submit提交的,所以可以在里面get到异常
  13. if (r instanceof FutureTask) {
  14. try {
  15. Future<?> future = (Future<?>) r;
  16. //get获取异常
  17. future.get();
  18. } catch (Exception e) {
  19. System.out.println("获取到submit提交的异常信息,处理异常" + e);
  20. }
  21. }
  22. }
  23. };
  24. //当线程池抛出异常后execute
  25. executorService.execute(new task());
  26. //当线程池抛出异常后submit
  27. executorService.submit(new task());
  28. }
  29. }
  30. class task3 implements Runnable {
  31. @Override
  32. public void run() {
  33. System.out.println("进入了task方法!!!");
  34. int i = 1 / 0;
  35. }
  36. }

JVM

jvm加载类的时候的阶段、static成员变量在各个阶段中发生了什么变化

源码到代码执行过程:编译 -> 加载 -> 解释 -> 执行
编译阶段:语法分析 -> 语义分析 -> 注解处理 -> class文件
加载阶段:装载 -> 连接 -> 初始化

装载阶段:查找并加载类的二进制数据,在 JVM 堆中创建一个 java.lang.Class类的对象,并将类相关的信息存储在 JVM 方法区中。
连接阶段:对 class 的信息进行验证、为类变量分配内存空间并对其赋默认值
1. 验证:验证类是否符合 java 规范和 JVM 规范
2. 准备:为类的静态变量分配内存,初始化为系统默认值
3. 解析:将符号引用转为直接引用
初始化:为类的静态变量赋予正确的初始值

解释阶段:把字节码转换为操作系统识别的指令
JVM 会检测热点代码,超过阈值会触发即时编译,生成机器码保存起来,下次直接执行
执行阶段:调用操作系统执行指令

框架

SpringBoot中如何定义一个starter

  1. 新建两个模块,命名规范:xxx-spring-boot-starter
    • xxx-spring-boot-autoconfigure:自定配置核心代码
    • xxx-spring-boot-starter:管理依赖
  2. 在 xxx-spring-boot-autoconfigure 项目中
    1. 引入 maven 依赖
    2. 创建自定义的 XXXProperties 类:类中的属性要出现在配置文件中
    3. 创建自定义类,实现自定义功能
    4. 创建自定义的 XXXAutoConfigure 类:用于自动配置时的一些逻辑,需将上方自定义类进行 Bean 对象创建,同时让 XXXProperties 类生效
    5. 创建自定义的spring.factories文件:在resource/META-INF创建一个 spring.factories文件和spring-configuration-metadata.json,分别用于导入自动配置类(必须有)、用于在填写配置文件时的智能提示(可以没有)。
  3. 在 xxx-spring-boot-starter 项目中引入 xxx-spring-boot-autoconfigure 依赖,其他项目使用时只需依赖 xxx-spring-boot-starter 即可

分布式

redis分布式锁

RedLock算法是 Redis 官方支持的分布式锁算法。

最普通的实现方式,是使用SET key value [EX seconds|PX milliseconds] NX创建一个key,就算加锁成功

  1. -- 删除锁的时候,找到key对应的value,与传过去的value做比较,如果是一样的才删除
  2. if redis.call("get",KEYS[1]) == ARGV[1] then
  3. return redis.call("del",KEYS[1])
  4. else
  5. return 0
  6. end

考虑到 Redis 单实例会出现单点故障风险;或者普通主从异步复制,如果主节点挂了,key 还没有同步到从节点,此时从节点切换为主节点,别人就可以 set key,从而拿到锁。

RedLock 算法
假设有一个 Redis cluster,有5个 Redis master 实例。然后执行如下步骤获取一把锁:
1. 获取当前的时间戳,单位是毫秒
2. 轮流尝试在每个 master 节点创建锁,超时时间较短(客户端为了获取锁使用的超时时间比自动释放锁的总时间要小)
3. 尝试在大多数节点上建立一个锁,比如5个节点就要求3个节点
4. 客户端计算加锁的时间,如果建立锁的时间小于超时时间,就算建立成功了
5. 要是加锁失败,就依次之前建立过的锁删除
6. 只要别人建立了一把分布式锁,你就地不断轮询去尝试获取锁

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注