1

106 阅读1小时+

1.JAVA基础部分

HASHMAP

零散的基础知识汇总

  1. 负载因子为0.75 为啥是0.75 泊松分布计算 大了不利于查找 小了频繁扩容 资源得不到更好的利用
  2. 解决哈希冲突的方法有三个,拉链法(hashMap),再哈希法,开放定址法
  3. HashMap的默认的初始大小为16
  4. 链表的最大长度为8 当大于8同时hashmap的容量大于64时转化为红黑树

put流程

首先判断当前table是否为空,若为空,则初始化,若不为空,则根据key的hash计算得到插入位置,再判断该位置是否有元素,若无元素,则直接插入,若有元素,则判断原位置数据的hash值与待插入数据的hash值是否相同,若相同,则继续比较值,若值不同,则创建一个新的Node节点,并使用尾插法将其插入到原数据的节点后面形成链表,若值相同,则采用待插入数据的值覆盖原数据的值,并返回原数据的值

扩容流程

美团:tech.meituan.com/2016/06/24/…

线程安全的集合

一、早期线程安全的集合 我们先从早期的线程安全的集合说起,它们是Vector和HashTable

  1. Vector Vector和ArrayList类似,是长度可变的数组,与ArrayList不同的是,Vector是线程安全的,它给几乎所有的public方法都加上了synchronized关键字。由于加锁导致性能降低,在不需要并发访问同一对象时,这种强制性的同步机制就显得多余,所以现在Vector已被弃用
  2. HashTable HashTable和HashMap类似,不同点是HashTable是线程安全的,它给几乎所有public方法都加上了synchronized关键字,还有一个不同点是HashTable的K,V都不能是null,但HashMap可以,它现在也因为性能原因被弃用了

二、Collections包装方法 Vector和HashTable被弃用后,它们被ArrayList和HashMap代替,但它们不是线程安全的,所以Collections工具类中提供了相应的包装方法把它们包装成线程安全的集合

​
List<E> synArrayList = Collections.synchronizedList(new ArrayList<E>());
​
Set<E> synHashSet = Collections.synchronizedSet(new HashSet<E>());
​
Map<K,V> synHashMap = Collections.synchronizedMap(new HashMap<K,V>());

Collections针对每种集合都声明了一个线程安全的包装类,在原集合的基础上添加了锁对象,集合中的每个方法都通过这个锁对象实现同步

三、java.util.concurrent包中的集合

  1. ConcurrentHashMap ConcurrentHashMap和HashTable都是线程安全的集合,它们的不同主要是加锁粒度上的不同。HashTable的加锁方法是给每个方法加上synchronized关键字,这样锁住的是整个Table对象。而ConcurrentHashMap是更细粒度的加锁 在JDK1.8之前,ConcurrentHashMap加的是分段锁,也就是Segment锁,每个Segment含有整个table的一部分,这样不同分段之间的并发操作就互不影响 JDK1.8对此做了进一步的改进,它取消了Segment字段,直接在table元素上加锁,实现对每一行进行加锁,进一步减小了并发冲突的概率
  2. CopyOnWriteArrayList和CopyOnWriteArraySet 它们是加了写锁的ArrayList和ArraySet,锁住的是整个对象,但读操作可以并发执行
  3. 除此之外还有ConcurrentSkipListMap、ConcurrentSkipListSet、ConcurrentLinkedQueue、ConcurrentLinkedDeque等,至于为什么没有ConcurrentArrayList,原因是无法设计一个通用的而且可以规避ArrayList的并发瓶颈的线程安全的集合类,只能锁住整个list,这用Collections里的包装类就能办到

cmap

推荐阅读:blog.csdn.net/a745233700/…

补充:1.7与1.8的cmap主要区别就是更加粒度化 加强了写的效率

补充问题

红黑树 二叉平衡树的区别

2.JAVA进阶部分

JVM

组成

  1. 线程共享的区域

    1. 方法区
  2. 线程不共享的区域

    1. 虚拟机栈
    2. 本地方法栈
    3. 程序计数器

栈帧包含

  1. 局部变量表
  2. 操作数栈
  3. 动态链接
  4. 方法返回地址

垃圾处理器分类

年轻代老年代
serialserial old
paw newcms
parallel scarengeparallel old
g1g1

CMS垃圾处理器

  1. 初始标记(stw)扫描old区选取gcroot覆盖的对象
  2. 并发标记 全部扫描,并发标记gcroot有关联的old对象
  3. 重新标记(stw)修正上述并发标记没有完善的old对象
  4. 标记清除算法

G1的执行流程

rset是G1处理器中当前区域包含其他区域应用的集合

  1. 初始标记(stw)扫描old区选取gcroot覆盖的对象 并且确定gcroot所在的region 标记为rootregion
  2. rootregin与rset取交集
  3. 并发标记 并发标记gcroot无关联的old对象,范围变小
  4. 重新标记(stw)修正上述并发标记没有完善的old对象
  5. 标记整理算法(stw)

类加载机制

类加载的加载器

  1. bootstrap类加载器
  2. 扩展类加载器
  3. 应用程序加载器
  4. 用户自定义的加载器

介绍

java默认的类加载机制

主要流程是双亲委派机制

加载一个类的时候先去先问服务时候加载过,层层向上问,询问到最顶层都没加载便需要向下进行加载操作。

为啥需要双亲委派机制那

  1. 避免类的重复加载
  2. 保护java核心代码不被篡改

打破双亲委派机制的例子

  1. tomcat的web容器
  2. spi机制

SPI 的调用方和接口定义方很可能都在 Java 的核心类库之中,而实现类交由开发者实现,然而实现类并不会被启动类加载器所加载,基于双亲委派的可见性原则,SPI 调用方无法拿到实现类。

SPI ServiceLoader 通过线程上下文获取能够加载实现类的ClassLoader,一般情况下是 Application ClassLoader,绕过了这层限制,逻辑上打破了双亲委派原则。

如何将两个全类名相同的路径引入进来

SPI机制

服务发现机制,是基于接口编程+策略模式+配置文件的综合应用。

java的时通过类的全类名进行加载

dubbo spi 机制是javaspi的升级版 通过自定义确定文件的数据地址 可以减少类加载的冗余问题 加快服务启动速度 更高的灵活性

JUC

image-20230203144049219

介绍一下cas

cas是一种比较交换的技术,自旋转,通过锁实现并发情况下的线程的安全

cas在java中由底层的Unsafe实现支持,使用unsafe的cas实质是直接操作变量的内存地址来实现的

JMM

JMM 是一个抽象概念,由于 CPU 多核多级缓存、为了优化代码会发生指令重排的原因,JMM 为了屏蔽细节,定义了一套规范,保证最终的并发安全。它抽象出了工作内存于主内存的概念,并且通过八个原子操作以及内存屏障保证了原子性、内存可见性、防止指令重排,使得 volatile 能保证内存可见性并防止指令重排、synchronised 保证了内存可见性、原子性、防止指令重排导致的线程安全问题,JMM 是并发编程的基础。

介绍一下volatile

volatile关键字主要保证了变量的可见性与防止指令重排

  1. 保证可见性时通过JMM的Happen-before之前变量,一个变量的前操作后操作可以看到
  2. 防止指令重排是为了抵制编译时候进行的操作,为了性能采取的行为,而volatile可以防止指令的重排

这里需要注意i++不是原子操作,i++从字节码层次来看是多个指令的,不具备变量改变的原子性操作,所以volatile修饰的i++不具备原子性

介绍一下 synchronized 锁升级的过程

锁升级主要包括是个阶段,从无锁到偏向锁再到自选锁最后就是重锁

  1. synchronized 首先是无锁 不添加当然是无锁了
  2. 加入 synchronized mark word中记录了当前线程的 ThreadId 偏向锁状态
  3. 当有线程竞争的时候进入自旋锁状态
  4. 线程竞争到达一定程度的时候进入重量级锁

Synchronized与ReentrantLock的区别

  1. sync能作用与代码块,方法,类,变量,而lock只能作用于代码快
  2. sync1.6之后的执行效率才赶上ReentrantLock
  3. ReentrantLock需要代码手动释放,而sync不需要手动释放
  4. ReentrantLock相对于sync有更多高级的特性

yield() 、sleep()、wait()、notify()

sleep yield让出资源不释放锁

wait让出资源并且释放锁

wait()和notify(),notifyAll()是Object类的方法,sleep()和yield()是Thread类的方法

yield()方法称为“退让”

void wait(long timeout)在其他线程调用此对象的notify() 方法 或者 notifyAll()方法,或者超过指定的时间量前,导致当前线程等待。

AQS的使用

原理

AQS底层实现原理用一句话总结就是:volatile + CAS + 一个虚拟的FIFO双向队列(CLH队列)

为啥使用双向链表而不是单向链表

AQS双向队列CLH的使用

AQS独享锁的加锁流程

1. 通过acquire来进行获取锁的操作
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&  //try 为自己实现的具体逻辑
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
​
2. 在reentrantLock中 
final boolean nonfairTryAcquire(int acquires) {
      final Thread current = Thread.currentThread();
      int c = getState();
      if (c == 0) {
            if (compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true;
            }
       }
       else if (current == getExclusiveOwnerThread()) {
            int nextc = c + acquires;
            if (nextc < 0) // overflow
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
       }
   return false;
}
​
3. 添加不符合的节点进入队列进行自旋转等待
private Node addWaiter(Node mode) {
        Node node = new Node(mode);
​
        for (;;) {
            Node oldTail = tail;
            if (oldTail != null) {
                node.setPrevRelaxed(oldTail);
                if (compareAndSetTail(oldTail, node)) {
                    oldTail.next = node;
                    return node;
                }
            } else {
                initializeSyncQueue();
            }
        }
    }
​
4. 自旋转判断时候可以获取资源
final boolean acquireQueued(final Node node, int arg) {
  // 标记是否成功拿到资源
  boolean failed = true;
  try {
    // 标记等待过程中是否中断过
    boolean interrupted = false;
    // 开始自旋,要么获取锁,要么中断
    for (;;) {
      // 获取当前节点的前驱节点
      final Node p = node.predecessor();
      // 如果p是头结点,说明当前节点在真实数据队列的首部,就尝试获取锁(别忘了头结点是虚节点)
      if (p == head && tryAcquire(arg)) {
        // 获取锁成功,头指针移动到当前node
        setHead(node);
        p.next = null; // help GC
        failed = false;
        return interrupted;
      }
      // 说明p为头节点且当前没有获取到锁(可能是非公平锁被抢占了)或者是p不为头结点,这个时候就要判断当前node是否要被阻塞(被阻塞条件:前驱节点的waitStatus为-1),防止无限循环浪费资源。具体两个方法下面细细分析
      if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
        interrupted = true;
    }
  } finally {
    if (failed)
      cancelAcquire(node);
  }
}
​
// 其中这个方法很重要
// 靠前驱节点判断当前线程是否应该被阻塞
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
  // 获取头结点的节点状态
  int ws = pred.waitStatus;
  // 说明头结点处于唤醒状态
  if (ws == Node.SIGNAL)
    return true; 
  // 通过枚举值我们知道waitStatus>0是取消状态
  if (ws > 0) {
    do {
      // 循环向前查找取消节点,把取消节点从队列中剔除
      node.prev = pred = pred.prev;
    } while (pred.waitStatus > 0);
    pred.next = node;
  } else {
    // 设置前任节点等待状态为SIGNAL
    compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
  }
  return false;
}
​
// 同时要注意取消节点的生成
private void cancelAcquire(Node node) {
  // 将无效节点过滤
  if (node == null)
    return;
  // 设置该节点不关联任何线程,也就是虚节点
  node.thread = null;
  Node pred = node.prev;
  // 通过前驱节点,跳过取消状态的node
  while (pred.waitStatus > 0)
    node.prev = pred = pred.prev;
  // 获取过滤后的前驱节点的后继节点
  Node predNext = pred.next;
  // 把当前node的状态设置为CANCELLED
  node.waitStatus = Node.CANCELLED;
  // 如果当前节点是尾节点,将从后往前的第一个非取消状态的节点设置为尾节点
  // 更新失败的话,则进入else,如果更新成功,将tail的后继节点设置为null
  if (node == tail && compareAndSetTail(node, pred)) {
    compareAndSetNext(pred, predNext, null);
  } else {
    int ws;
    // 如果当前节点不是head的后继节点,1:判断当前节点前驱节点的是否为SIGNAL,2:如果不是,则把前驱节点设置为SINGAL看是否成功
    // 如果1和2中有一个为true,再判断当前节点的线程是否为null
    // 如果上述条件都满足,把当前节点的前驱节点的后继指针指向当前节点的后继节点
    if (pred != head && ((ws = pred.waitStatus) == Node.SIGNAL || (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) && pred.thread != null) {
      Node next = node.next;
      if (next != null && next.waitStatus <= 0)
        compareAndSetNext(pred, predNext, next);
    } else {
      // 如果当前节点是head的后继节点,或者上述条件不满足,那就唤醒当前节点的后继节点
      unparkSuccessor(node);
    }
    node.next = node; // help GC
  }
}

在取消节点的生成的时候可以看到只对next节点做了处理 在shouldParkAfterFailedAcquire 会进行node.prev = pred = pred.prev;

线程池

线程池的优点

  • 降低资源消耗:通过池化技术重复利用已创建的线程,降低线程创建和销毁造成的损耗。
  • 提高响应速度:任务到达时,无需等待线程创建即可立即执行。
  • 提高线程的可管理性:线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,还会因为线程的不合理分布导致资源调度失衡,降低系统的稳定性。使用线程池可以进行统一的分配、调优和监控。
  • 提供更多更强大的功能:线程池具备可拓展性,允许开发人员向其中增加更多的功能。比如延时定时线程池ScheduledThreadPoolExecutor,就允许任务延期执行或定期执行。

线程池的执行流程

image-20230203144135212

任务调度

任务调度是线程池的主要入口,当用户提交了一个任务,接下来这个任务将如何执行都是由这个阶段决定的。了解这部分就相当于了解了线程池的核心运行机制。

首先,所有任务的调度都是由execute方法完成的,这部分完成的工作是:检查现在线程池的运行状态、运行线程数、运行策略,决定接下来执行的流程,是直接申请线程执行,或是缓冲到队列中执行,亦或是直接拒绝该任务。其执行过程如下:

  1. 首先检测线程池运行状态,如果不是RUNNING,则直接拒绝,线程池要保证在RUNNING的状态下执行任务。
  2. 如果workerCount < corePoolSize,则创建并启动一个线程来执行新提交的任务。
  3. 如果workerCount >= corePoolSize,且线程池内的阻塞队列未满,则将任务添加到该阻塞队列中。
  4. 如果workerCount >= corePoolSize && workerCount < maximumPoolSize,且线程池内的阻塞队列已满,则创建并启动一个线程来执行新提交的任务。
  5. 如果workerCount >= maximumPoolSize,并且线程池内的阻塞队列已满, 则根据拒绝策略来处理该任务, 默认的处理方式是直接抛异常。

其执行流程如下图所示

image-20230203144154619

常见线程队列

image-20230203144211680

常见拒绝策略

image-20230203144225835

3.MYSQL

查询语句执行流程

基础知识

三大范式

  1. 列是原子的
  2. 列属性依赖于主键
  3. 每列和主键直接相关而不是间接相关

索引详细介绍(索引的分类 MYSQL数据的组成部分)

为啥使用索引

  • 更快的获取查询的速度,提高查询的效率
  • 降低数据库的IO次数

使用索引的分类

  • 主键索引 :唯一且不能为空
  • 普通索引 :不唯一,可为空
  • 唯一索引 :唯一,可以空
  • 全文索引 :MyISam所带,5.6之后InnoDB也开始使用

储存索引分类

  • 聚簇索引 :数据索引共处
  • 非聚簇索引 :叶子结点的索引为指向索引

索引算法

  • hash索引
  • 二叉树索引
  • B树索引

索引失效的情况

  1. 模糊查询
  2. 索引参加了运算
  3. 使用了函数
  4. 不遵循左前缀原则

解释一下回表

通俗的讲就是,如果索引的列在 select 所需获得的列中(因为在 mysql 中索引是根据索引列的值进行排序的,所以索引节点中存在该列中的部分值)或者根据一次索引查询就能获得记录就不需要回表,如果 select 所需获得列中有大量的非索引列,索引就需要到表中找到相应的列的信息,这就叫回表。

ACID(四大特性)

  1. 原子性(Atomicity): 事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用;
  2. 一致性(Consistency): 是指事务执行的结果必须是使数据库从一个一致性状态变到另一个一致性状态。保证数据库一致性是指当事务完成时,必须使所有数据都具有一致的状态;【Mysql是怎样运行的:如果数据库中的数据全部符合现实世界中的约束(all defined rules),我们说这些数据就是一致的,或者说符合一致性的。】
  3. 隔离性(Isolation): 并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间是独立的;
  4. 持久性(Durability): 事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失

ACID这4个特性MYSQL如何保证实现的

  • 保证一致性:

    • 从数据库层面,数据库通过原子性、隔离性、持久性来保证一致性。也就是说ACID四大特性之中,C(一致性)是目的,A(原子性)、I(隔离性)、D(持久性)是手段,是为了保证一致性,数据库提供的手段
    • 从应用层面,通过代码判断数据库数据是否有效,然后决定回滚还是提交数据!
  • 保证原子性:是利用Innodb的undo log,undo log名为回滚日志,是实现原子性的关键。undo log记录了这些回滚需要的信息,当事务执行失败或调用了rollback,导致事务需要回滚,便可以利用undo log中的信息将数据回滚到修改之前的样子。

  • 保证持久性:是利用Innodb的redo log。当做数据修改的时候,不仅在内存中操作,还会在redo log中记录这次操作。当事务提交的时候,会将redo log日志进行刷盘。当数据库宕机重启的时候,会将redo log中的内容恢复到数据库中,再根据undo log和binlog内容决定回滚数据还是提交数据

    • 采用redo log的好处?其实好处就是将redo log进行刷盘比对数据页刷盘效率高,具体表现如下

      • redo log体积小,毕竟只记录了哪一页修改了啥,因此体积小,刷盘快。
      • redo log是一直往末尾进行追加,属于顺序IO。效率显然比随机IO来的快。
  • 保证隔离性:利用的是锁和MVCC机制

三大日志的作用

推荐阅读:www.cnblogs.com/xiaolincodi…

  1. redo log :

    • 最先执行 在写buffer pool之前进行日志的记录
    • 主要用于数据库的持久化存储
    • 保证数据的快速恢复,物理页内容
  2. undo log :

    • 主要用户事务的回滚操作
  3. bin log :

    • 主要用于数据库的主从架构
    • 逻辑日志 保证数据的全部存在
  4. 为啥在redo log之前要有一层buffer pool 脏刷

    • 脏刷是为了解决io的瓶颈问题在缓存中存储相应的page信息
    • redo log是顺序io 在redolog 写的效率大于在buffer pool中的记录

数据库搜索引擎的区别

  1. MyISam只有表锁 InnoDB有行锁也有表锁
  2. InnoDB支持事务 MyISam不支持事务

除了知道myisam、innodb还知道什么数据库引擎

答曰:Memory 类似于 redis 内存关联存储数据库引擎 hash索引

Innodb锁

按照细度进行分类
  1. 行锁 :清楚行锁

    1. 普通行锁
    2. 间隙锁
    3. Next-key Lock
  2. 表锁 :模糊表锁

    1. 意向锁
    2. 自增锁
具体实现

行锁和表锁是锁粒度的概念,共享锁和排它锁是他们的具体实现

  1. 共享锁(S):读锁

    • 允许一个事务去读一行,阻止其他事务获取该行的排它锁。
    • 多事务时,只能加共享读锁,不能加排他写锁;单事务时,可以加任何锁。 一般理解为:能读,不能写。
  2. 排它锁(X):写锁

    • 允许持有排它锁的事务读写数据,阻止其他事物获取该数据的共享锁和排它锁。
    • 其他事务不能获取该数据的任何锁,直到排它锁持有者释放。
    • 不能获取任何锁,不代表不能无锁读取

进阶知识

幻读的解决

幻读是什么

幻读指的是一个事务在前后两次查询同一个范围的时候,后一次查询看到了前一次查询没有看到的行。

如何解决幻读

添加间隙锁(Gap Lock)将修改区间进行加锁防止幻读的发生

  • 在RR可重复读的情况下通过加锁是通过间隙锁或者是next-key Lock 来进行解决幻读
  • 间隙锁之间互不影响,都是为了防止插入

在RR隔离级别下只要不出现快照读和当前读的切换,其实就能保证不会出现幻读

数据库的优化

为啥用B+树而不用B树跟红黑树

缓冲池

作用:用来提升数据库的性能

mysql也存在缓存淘汰机制

LRU的应用

mysql的内部XA事件

两阶段提交的主要用意是:为了保证redolog和binlog数据的安全一致性。只有在这两个日志文件逻辑上高度一致了。你才能放心的使用redolog帮你将数据库中的状态恢复成crash之前的状态,使用binlog实现数据备份、恢复、以及主从复制。而两阶段提交的机制可以保证这两个日志文件的逻辑是高度一致的。没有错误、没有冲突

实现:

当MySQL写完redolog并将它标记为prepare状态时,并且会在redolog中记录一个XID,它全局唯一的标识着这个事务。而当你设置sync_binlog=1时,做完了上面第一阶段写redolog后,mysql就会对应binlog并且会直接将其刷新到磁盘中。

下图就是磁盘上的row格式的binlog记录。binlog结束的位置上也有一个XID。

只要这个XID和redolog中记录的XID是一致的,MySQL就会认为binlog和redolog逻辑上一致。就上面的场景来说就会commit,而如果仅仅是rodolog中记录了XID,binlog中没有,MySQL就会RollBack

MVCC 软件事物内存STM

4.REDIS

基础

数据结构

分类
  1. 字符串
  2. 列表list
  3. 字典map
  4. set
  5. zset
  6. HepyLogLog

淘汰策略

LRU

LFU

持久化策略

RDB :体积小,恢复速度快,数据稳定度低 生成耗时长 需要fork一个子进程进行生成

AOF :体积大,服务速度慢,但是数据稳定

混合模式 :通过RDB与AOF进行混合使用,通过规划AOF实现数据的可靠速度快

缓存穿透 缓存雪崩 缓存击穿

  1. 缓存穿透

    • 原因

      • 查询一个不存在的数据,返回为null,一般情况下会进行db操作,流量大的db操作会使系统直接挂掉。
    • 解决思路

      • 参数校验
      • 将空值进行包装,非法访问取值时取到的是包装后的值
      • redis提供布隆过滤器,可以高效的查询你所查找的key是否在redis中存储,如果存储但是为null,你可以在进行db操作返回
  2. 缓存雪崩

    • 原因

      • 设置过期时间统一,在某一时间段,热点key全部失效,大量访问请求,导致db负载过大,使系统宕机
    • 解决思路

      • 将过期时间进行随机数赋值
      • 事中:通过setennel进行限流降级处理避免db被打死
      • redis持久化配置
  3. 缓存击穿

    • 原因

      • 热点key在某一时间段,受到大量请求,系统为分布式系统时,请求都会落地db,缓存击穿
    • 解决思路

      • 热点数据为不更新数据可以将数据进行持久化缓存
      • 热点数据不频繁更新,可以采用分布式锁的方式进行解决
      • 频繁更新的数据应该主动添加过期时间,保证请求可以访问到缓存或者是db

缓存预热

解决缓存击穿的办法,热点的数据的提前缓存

方式

  1. 数据量少,启动时候通过init进行加载
  2. 脚本批量刷新 实现canal的方式

缓存同步

Redis高可用集群的探索

持久性

单机的情况下,防止Redis的宕机的情况下,启动redis的持久化缓存保证数据不丢失。但是还是不行,单机的访问上限总归是最低的。出现问题的可能性大。

主从结构

经过单机的摸索,于是出现了主从的模式,主redis服务器负责写,从服务进行读取操作,降低了redis的访问瓶颈,主从通过持久化文件RDB与AOF进行数据的通过

  1. 全量备份通过RDB,在搭建主从架构的时候,启动时候先通过RDB全量数据数据的同步,耗时短,文件小
  2. 增量备份通过AOF,主服务的数据改变了不会通过RDB进行再次的全量备份,耗时长,浪费大量的资源,于是通过AOF进行增量的同步,记录第一次同步的指针offset,再次同步的时候通过比较指针来确定同步范围,进行高效的同步

主从结构的出现,增加了redis服务的可用性,但是还是不是那么的完美,例如当主服务宕机时候还会产生一些不可用的情况,我们需要自己进行手动的切换

对于这种情况,我们需要一个「故障自动切换」机制,这就是我们经常听到的「哨兵」所具备的能力

哨兵

image-20230203144347610

主从的进阶版于是就出现了,通过增加哨兵机制来完善redis集群的可用性

对于集群:

  1. 当master宕机,通过 slave-priority配置 > 数据完整性 > runid较小者 进行备份master的选举,提升为master其他为slave
  2. 哨兵也需要自己进行选举,一致性算法近似Raft
分片集群

主要 :数据分片,不同数据放入不同的redis进行高效的索引

进阶

Redis过期数据的删除流程

  1. 固定删除+惰性删除

    • 固定删除 :一段时间进行某些key的抽取,过期就删除。这样就会造成一部分key过期了但是还存留在内存中没有被删除。
    • 惰性删除 :上述删除残留的key value 获取的时候判断是否已经删除,过期即删除不反回
  2. 内存淘汰机制

    • noeviction:当内存不足以容纳新写入数据时,新写入操作会报错,这个一般没人用吧,实在是太恶心
    • allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key(这个是最常用的)
    • allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key,一般没人用,为啥要随机,肯定是把最近最少使用的key给干掉啊
    • volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key
    • volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key(一般不太合适)
    • volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除

redis为啥快

  1. 内存操作
  2. 单线程避免了上下文线程切换的消耗
  3. 事件驱动消息线程模型
  4. 多路复用epoll机制

redis的线程模型

  1. 事件管理器是整个事件驱动的核心,管理着文件事件和时间事件;
  2. 事件管理器的单线程负责循环从epoll里面拉取就绪事件,然后同步执行。
  3. 事件管理器的单线程中也会遍历时间事件列表,同步执行过期事件。

文件事件

image-20230203144253232

时间时间

负责redis过期时间的统计进行

两张图应付面试

image-20230203144312726

image-20230203144325906

布隆过滤器

布隆过滤器的原理是,当一个元素被加入集合时,通过 K 个散列函数将这个元素映射成一个位数组(Bit array)中的 K 个点,把它们置为 1 。检索时,只要看看这些点是不是都是1就知道元素是否在集合中;如果这些点有任何一个 0,则被检元素一定不在;如果都是1,则被检元素很可能在(之所以说“可能”是误差的存在)。

redis实现分布式锁

想要实现分布式锁,必须要求 Redis 有「互斥」的能力,我们可以使用 SETNX 命令,这个命令表示SET if Not eXists,即如果 key 不存在,才会设置它的值,否则什么也不做。

防止死锁的思路可以通过实现对setnx进行ttl操作

SET lock 1 EX 10 NX nx互斥 ex过期时间选择 expire

5.消息队列 ES

KAFKA

Kafka 如何保证高可用?

Kafka 的基本架构组成是:由多个 broker 组成一个集群,每个 broker 是一个节点;当创建一个 topic 时,这个 topic 会被划分为多个 partition,每个 partition 可以存在于不同的 broker 上,每个 partition 只存放一部分数据。 这就是天然的分布式消息队列,就是说一个 topic 的数据,是分散放在多个机器上的,每个机器 就放一部分数据。 在 Kafka 0.8 版本之前,是没有 HA 机制的,当任何一个 broker 所在节点宕机了,这个 broker 上 的 partition 就无法提供读写服务,所以这个版本之前, Kafka 没有什么高可用性可言。 在 Kafka 0.8 以后,提供了 HA 机制,就是 replica 副本机制。每个 partition 上的数据都会同步 到其它机器,形成自己的多个 replica 副本。所有 replica 会选举一个 leader 出来,消息的生产 者和消费者都跟这个 leader 打交道,其他 replica 作为 follower。写的时候, leader 会负责把 数据同步到所有 follower 上去,读的时候就直接读 leader 上的数据即可。 Kafka 负责均匀的将一 个 partition 的所有 replica 分布在不同的机器上,这样才可以提高容错性。

Kafka 与传统消息系统之间的区别(对比)

Kafka 持久化日志,这些日志可以被重复读取和无限期保留 Kafka 是一个分布式系统:它以集群的方式运行,可以灵活伸缩,在内部通过复制数据提升容错能 力和高可用性 Kafka 支持实时的流式处理

解释下Kafka中位移(offset)的作用

在Kafka中,每个主题分区下的每条消息都被赋予了一个唯一的ID数值,用于标识它在分区中的位置。这 个ID数值,就被称为位移,或者叫偏移量。一旦消息被写入到分区日志,它的位移值将不能被修改。

kafka 为什么那么快(重要:缓存 顺序写 零拷贝 批量发送 )

  • Cache Filesystem Cache PageCache缓存
  • 顺序写:由于现代的操作系统提供了预读和写技术,磁盘的顺序写大多数情况下比随机写内存还要 快。
  • Zero-copy:零拷技术减少拷贝次数
  • Batching of Messages:批量量处理。合并小的请求,然后以流的方式进行交互,直顶网络上 限。

解决kafka不丢失消息

主要从三个方面来看待kafka消息丢失的问题

  1. 生成者将消息发送到broker的过程

    解决方式

    1. ACKS机制 设置ack=-1 or all
    2. 配置retries 重试次数
    3. callback机制 网络抖动可以添加重试机制
  2. broker之间的传播过程以及写入磁盘的过程

    解决方式

    1. 由于没有提供 「同步刷盘」策略,因此 Kafka 是通过「多分区多副本」的方式来最大限度的保证数据不丢失。
  3. 消费者消费broker消息的过程

    解决方式

    1. 正确的做法是:拉取数据、业务逻辑处理、提交消费 Offset 位移信息。
    2. enable.auto.commit = false, 采用手动提交位移的方式。

ES

es倒叙插排(重点但是我没被问过)

zhuanlan.zhihu.com/p/33671444

es集群可用问题

www.modb.pro/db/115009

es写入流程详细版本(非重点)

1)数据先写入内存buffer,在写入buffer的同时将数据写入translog日志文件,注意:此时数据还没有被成功es索引记录,因此无法搜索到对应数据;

2)如果buffer快满了或者到一定时间,es就会将buffer数据refresh到一个新的segment file中,但是此时数据不是直接进入segment file的磁盘文件,而是先进入os cache的。这个过程就是refresh。 每隔1秒钟,es将buffer中的数据写入一个新的segment file,因此每秒钟会产生一个新的磁盘文件segment file,这个segment file中就存储最近1秒内buffer中写入的数据。

操作系统中,磁盘文件其实都有一个操作系统缓存os cache,因此数据写入磁盘文件之前,会先进入操作系统级别的内存缓存os cache中。

一旦buffer中的数据被refresh操作,刷入os cache中,就代表这个数据就可以被搜索到了。

这就是为什么es被称为准实时(NRT,near real-time):因为写入的数据默认每隔1秒refresh一次,也就是数据每隔一秒才能被 es 搜索到,之后才能被看到,所以称为准实时。

只要数据被输入os cache中,buffer就会被清空,并且数据在translog日志文件里面持久化到磁盘了一份,此时就可以让这个segment file的数据对外提供搜索了。

3)重复1~2步骤,新的数据不断进入buffer和translog,不断将buffer数据写入一个又一个新的segment file中去,每次refresh完,buffer就会被清空,同时translog保留一份日志数据。随着这个过程推进,translog文件会不断变大。当translog文件达到一定程度时,就会执行commit操作。

4)commit操作发生第一步,就是将buffer中现有数据refresh到os cache中去,清空buffer。

5)将一个 commit point 写入磁盘文件,里面标识着这个 commit point 对应的所有 segment file,同时强行将 os cache 中目前所有的数据都 fsync 到磁盘文件中去。

6)将现有的translog清空,然后再次重启启用一个translog,此时commit操作完成。

translog日志文件的作用是什么?

在你执行commit操作之前,数据要么是停留在buffer中,要么是停留在os cache中,无论是buffer还是os cache都是内存,一旦这台机器死了,内存中的数据就全丢了。

因此需要将数据对应的操作写入一个专门的日志文件,也就是translog日志文件,一旦此时机器宕机,再次重启的时候,es会自动读取translog日志文件中的数据,恢复到内存buffer和os cache中去。

综上可以看出: es是准实时的,因此数据写入1秒后才可以搜索到。 如果translog是异步写入的话,es可能会丢失数据:有n秒的数据停留在buffer、translog的os cache、segment file的os cache中,也就是这n秒的数据不在磁盘上,此时如果宕机,会导致n秒的数据丢失。

6.SPRING

bean的生命周期

<简而言之的回答:生命周期主要是四个 实例化 属性赋值 初始化 销毁,其中初始化是最复杂的节点 其中包括一个bean的众多复杂操作>

  1. 流程

    实例化->属性赋值->初始化->销毁
    

Spring的生命周期

img转存失败,建议直接上传图片文件

  1. 初始化解释
/**
   * Actually create the specified bean. Pre-creation processing has already happened
   * at this point, e.g. checking {@code postProcessBeforeInstantiation} callbacks.
   * <p>Differentiates between default bean instantiation, use of a
   * factory method, and autowiring a constructor.
   * 实际创建指定的bean。此时已经进行了预创建处理,例如检查实例化回调之前的后处理。
   * 区分默认 bean 实例化、使用工厂方法和自动装配构造函数。
   *
   * @param beanName the name of the bean
   * @param mbd      the merged bean definition for the bean
   * @param args     explicit arguments to use for constructor or factory method invocation
   * @return a new instance of the bean
   * @throws BeanCreationException if the bean could not be created
   * @see #instantiateBean
   * @see #instantiateUsingFactoryMethod
   * @see #autowireConstructor
   */
  protected Object doCreateBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args)
      throws BeanCreationException {
​
    // Instantiate the bean.
    BeanWrapper instanceWrapper = null;
    if (mbd.isSingleton()) {
      instanceWrapper = this.factoryBeanInstanceCache.remove(beanName);
    }
    if (instanceWrapper == null) {
      // 创建bean实例
      instanceWrapper = createBeanInstance(beanName, mbd, args);
    }
    Object bean = instanceWrapper.getWrappedInstance();
    Class<?> beanType = instanceWrapper.getWrappedClass();
    if (beanType != NullBean.class) {
      mbd.resolvedTargetType = beanType;
    }
​
    // Allow post-processors to modify the merged bean definition.
    synchronized (mbd.postProcessingLock) {
      if (!mbd.postProcessed) {
        try {
          applyMergedBeanDefinitionPostProcessors(mbd, beanType, beanName);
        } catch (Throwable ex) {
          throw new BeanCreationException(mbd.getResourceDescription(), beanName,
              "Post-processing of merged bean definition failed", ex);
        }
        mbd.postProcessed = true;
      }
    }
​
    // Eagerly cache singletons to be able to resolve circular references
    // even when triggered by lifecycle interfaces like BeanFactoryAware.
    boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
        isSingletonCurrentlyInCreation(beanName));
    if (earlySingletonExposure) {
      if (logger.isTraceEnabled()) {
        logger.trace("Eagerly caching bean '" + beanName +
            "' to allow for resolving potential circular references");
      }
      addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
    }
​
    // Initialize the bean instance.
    Object exposedObject = bean;
    try {
      //bean属性赋值
      populateBean(beanName, mbd, instanceWrapper);
      // 初始化
      exposedObject = initializeBean(beanName, exposedObject, mbd);
    } catch (Throwable ex) {
      if (ex instanceof BeanCreationException && beanName.equals(((BeanCreationException) ex).getBeanName())) {
        throw (BeanCreationException) ex;
      } else {
        throw new BeanCreationException(
            mbd.getResourceDescription(), beanName, "Initialization of bean failed", ex);
      }
    }
​
    if (earlySingletonExposure) {
      Object earlySingletonReference = getSingleton(beanName, false);
      if (earlySingletonReference != null) {
        if (exposedObject == bean) {
          exposedObject = earlySingletonReference;
        } else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) {
          String[] dependentBeans = getDependentBeans(beanName);
          Set<String> actualDependentBeans = new LinkedHashSet<>(dependentBeans.length);
          for (String dependentBean : dependentBeans) {
            if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) {
              actualDependentBeans.add(dependentBean);
            }
          }
          if (!actualDependentBeans.isEmpty()) {
            throw new BeanCurrentlyInCreationException(beanName,
                "Bean with name '" + beanName + "' has been injected into other beans [" +
                    StringUtils.collectionToCommaDelimitedString(actualDependentBeans) +
                    "] in its raw version as part of a circular reference, but has eventually been " +
                    "wrapped. This means that said other beans do not use the final version of the " +
                    "bean. This is often the result of over-eager type matching - consider using " +
                    "'getBeanNamesForType' with the 'allowEagerInit' flag turned off, for example.");
          }
        }
      }
    }
​
    // Register bean as disposable.
    try {
      registerDisposableBeanIfNecessary(beanName, bean, mbd);
    } catch (BeanDefinitionValidationException ex) {
      throw new BeanCreationException(
          mbd.getResourceDescription(), beanName, "Invalid destruction signature", ex);
    }
​
    return exposedObject;
  }
  /**
   * Initialize the given bean instance, applying factory callbacks
   * as well as init methods and bean post processors.
   * <p>Called from {@link #createBean} for traditionally defined beans,
   * and from {@link #initializeBean} for existing bean instances.
   *
   * @param beanName the bean name in the factory (for debugging purposes)
   * @param bean     the new bean instance we may need to initialize
   * @param mbd      the bean definition that the bean was created with
   *                 (can also be {@code null}, if given an existing bean instance)
   * @return the initialized bean instance (potentially wrapped)
   * @see BeanNameAware
   * @see BeanClassLoaderAware
   * @see BeanFactoryAware
   * @see #applyBeanPostProcessorsBeforeInitialization
   * @see #invokeInitMethods
   * @see #applyBeanPostProcessorsAfterInitialization
   */
//初始化
  protected Object initializeBean(String beanName, Object bean, @Nullable RootBeanDefinition mbd) {
    //健康判断
    if (System.getSecurityManager() != null) {
      AccessController.doPrivileged((PrivilegedAction<Object>) () -> {
        //初始化Aware接口
        invokeAwareMethods(beanName, bean);
        return null;
      }, getAccessControlContext());
    } else {
      invokeAwareMethods(beanName, bean);
    }
​
    Object wrappedBean = bean;
    if (mbd == null || !mbd.isSynthetic()) {
      wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
    }
    //后置处理
    try {
      invokeInitMethods(beanName, wrappedBean, mbd);
    } catch (Throwable ex) {
      throw new BeanCreationException(
          (mbd != null ? mbd.getResourceDescription() : null),
          beanName, "Invocation of init method failed", ex);
    }
    if (mbd == null || !mbd.isSynthetic()) {
      wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);
    }
​
    return wrappedBean;
  }

3. 扩展节点

若 Spring 检测到 bean 实现了 Aware 接口,则会为其注入相应的依赖。所以通过让bean 实现 Aware 接口,则能在 bean 中获得相应的 Spring 容器资源

  1. IOC知识点

「org.springframework.context.support.AbstractApplicationContext#prepareBeanFactory」属性填充

「org.springframework.context.support.AbstractApplicationContext#finishBeanFactoryInitialization」初始化了非懒加载的bean单例

无状态bean和有状态bean

  1. 有状态就是有数据存储功能。有状态对象(Stateful Bean),就是有实例变量的对象,可以保存数 据,是非线程安全的。在不同方法调用间不保留任何状态。
  2. 无状态就是一次操作,不能保存数据。无状态对象(Stateless Bean),就是没有实例变量的对象 .不 能保存数据,是不变类,是线程安全的。
  3. 在spring中无状态的Bean适合用不变模式,就是单例模式,这样可以共享实例提高性能。有状态的 Bean在多线程环境下不安全,适合用Prototype原型模式。
  4. Spring使用ThreadLocal解决线程安全问题。如果你的Bean有多种状态的话(比如 View Model 对 象),就需要自行保证线程安全 。

spring事物

事务主要通过aop实现 在添加事务注解时 在beanpostprocesser解析的时候判断是否需要进行事物(通过事务注解进行判断)

image-20220219174745490

spring AOP原理

springAOP实现原理其实很简单,就是通过动态代理实现的。如果我们为Spring的某个bean配置了切面,那么Spring在创建这个bean的时候,实际上创建的是这个bean的一个代理对象,我们后续对bean中方法的调用,实际上调用的是代理类重写的代理方法。而SpringAOP使用了两种动态代理,分别是JDK的动态代理,以及CGLib的动态代理

spring自动装配的解释

  1. 主要注解为 @SpringBootApplication 其中主要的注解为 @EnableAutoConfiguration 开启了自动装配
  2. @Import(AutoConfigurationImportSelector.class) Import注解可以将当前类或者制定类加载到spring容器中去
  3. 通过继承 selectImports 接口实现 selectImports 方法实现自动装配
  4. 开启自动装配即可在META-INF中创建配置文件 spring.factories 中的全类名批量添加Bean
  5. 自动装配阶段在 BeanPostProcess 前置处理阶段之前通过BeanFactoryPostProcessor进行修改,具体实现为 ConfigurationClassPostProcessor

spring缓存实现 如何解决循环依赖的

  • 三个缓存名称

    singletonObjects一级缓存

    earlySingletonObjects二级缓存

    singletonFactories三级缓存

  1. Bean解决循环依赖主要是通过三级缓存的方式

  2. 首先要从Bean的生命周期开始分析

    1. 生命周期中存在一个属性的填充,在填充时因为都不存在的原因无法打破循环
    2. 为啥earlySingletonObjects解决不了Bean的循环依赖,是因为Aop等的动态代理的原因
  3. 需要一个点来破开循环,所以earlySingletonObjects打破了这个平衡

  4. 而存在事务等需要代理的类时,我们放入最后单例容器的Bean不是我们new原生的Bean而是通过动态代理代理出来的类,当我们使用earlySingletonObjects缓存存放半成品的时候,不是我们所需的代理类。

  5. 因此出来了三级缓存,代理单例模式流程为下(AB循环依赖,为代理对象)

    1. A创建
    2. 无论需要不需要都将A对象无参构造的函数存入singletonFactories 如果需要动态代理,取动态代理的类构造类
    3. 填充B,创建B执行前面步骤,发现出现循环依赖的情况,先去 earlySingletonObjects 寻找是否存在A,不存在三级缓存取出创建方式,创建填充,完善生命周期

springboot的启动流程

img

7.分布式理论

cap原理及注册中心选型

三者不可以兼得,分布式系统需要保证系统的分区容错性所以一般选择ap或者cp

  • C:一致性
  • A:可用性
  • P:分期容错性

为啥选择nacos为注册中心、配置中心

  1. 技术比较新,社区活跃度比较高
  2. 项目设计采用了阿里巴巴全家桶,选择nacos有利于问题的解决
  3. nacos支持apcp的切换
  4. nacos不仅是注册中心还是配置中心,可以减少系统的复杂度

分布式事务

随着单体应用向分布式应用的扩展,数据库基本事务已经不能保证数据的一致性,因此为了分布式发展数据一致性的保证,提出分布式事物的概念。分布式事物主要作用就是来实现cap中一致性为先,分区容错,同时高可用的状态。分布式事物概念复杂,逻辑繁琐,不能完全保证数据库事物四大特性acid的完全落地,但是极大层面上按照此思路进行(原子性,持久性严格遵守,最终保证数据的一致性,隔离性在传递过程中可见)。通过seata可以更好的提供分布式事物的落地。

  1. TCC(try-confirm-cancel)

    • 悬挂挂问题
    • 幂等问题
    • 空回滚问题
  2. XA

    • 在XA模式下,需要有一个[全局]协调器,每一个数据库事务完成后,进行第一阶段预提交,并通知协调器,把结果给协调器。协调器等所有分支事务操作完成、都预提交后,进行第二步;第二步:协调器通知每个数据库进行逐个commit/rollback。
  3. SAGA

    • Saga 是一种补偿协议,在 Saga 模式下,分布式事务内有多个参与者,每一个参与者都是一个冲正补偿服务,需要用户根据业务场景实现其正向操作和逆向回滚操作。
  4. AT

    • AT 模式的一阶段、二阶段提交和回滚均由 Seata 框架自动生成,用户只需编写“业务 SQL”,便能轻松接入分布式事务,AT 模式是一种对业务无任何侵入的分布式事务解决方案。

image-20220301210408064

  • AT 模式是无侵入的分布式事务解决方案,适用于不希望对业务进行改造的场景,几乎0学习成本。
  • TCC 模式是高性能分布式事务解决方案,适用于核心系统等对性能有很高要求的场景。
  • Saga 模式是长事务解决方案,适用于业务流程长且需要保证事务最终一致性的业务系统,Saga 模式一阶段就会提交本地事务,无锁,长流程情况下可以保证性能,多用于渠道层、集成层业务系统。事务参与者可能是其它公司的服务或者是遗留系统的服务,无法进行改造和提供 TCC 要求的接口,也可以使用 Saga 模式。
  • XA模式是分布式强一致性的解决方案,但性能低而使用较少

3.分布式锁

jvm内部锁仅支持单机应用的互斥控制,随着业务向分布式系统的过度,资源服务需要进行并发策略的更新,因此分布式锁实现这种理念。

4.一致性哈希算法(负载均衡算法)

解决分布式查询命中问题

public static void main(String[] args) {
    TreeMap<Long, String> map = new TreeMap<>();
    map.put(111L, "127.0.0.1");
    map.put(222L, "127.0.0.2");
    map.put(333L, "127.0.0.3");
    map.put(444L, "127.0.0.4");
    SortedMap<Long, String> maps = map.tailMap(201L);
    if (maps.isEmpty()) {
        System.out.println(map.firstEntry().getValue());
    } else {
        System.out.println(maps.get(maps.firstKey()));
    }
}

5.轮询与随机

大量随机的结果趋向于轮询,轮询因为木桶的最短板原理,轮训会造成性能的损耗

式事务的幂等

多次相同的提交,与一次执行的结果是一样的。

分布式调用时会遇到重试机制,两次相同操作需要实现幂等,防止多服务产生问题

8.OS 计网

OS

进程间的通信方式

  • 管道:管道这种通讯方式有两种限制,一是半双工的通信,数据只能单向流动,二是只能在具有亲 缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。 管道可以分为两类:匿名管道和命名管道。匿名管道是单向的,只能在有亲缘关系的进程间通信; 命名管道以磁盘文件的方式存在,可以实现本机任意两个进程通信。
  • 信号 : 信号是一种比较复杂的通信方式,信号可以在任何时候发给某一进程,而无需知道该进程 的状态。
  • 信号量:信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机 制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进 程内不同线程之间的同步手段。
  • 消息队列:消息队列是消息的链接表,包括Posix消息队列和System V消息队列。有足够权限的进 程可以向队列中添加消息,被赋予读权限的进程则可以读走队列中的消息。消息队列克服了信号承 载信息量少,管道只能承载无格式字节流以及缓冲区大小受限等缺点。
  • 共享内存:共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但 多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专 门设计的。它往往与其他通信机制,如信号量,配合使用,来实现进程间的同步和通信。
  • Socket:与其他通信机制不同的是,它可用于不同机器间的进程通信。

进程通信的优缺点:

  • 管道:速度慢,容量有限;
  • Socket:任何进程间都能通讯,但速度慢;
  • 消息队列:容量受到系统限制,且要注意第一次读的时候,要考虑上一次没有读完数据的问题;
  • 信号量:不能传递复杂消息,只能用来同步;
  • 共享内存区:能够很容易控制容量,速度快,但要保持同步,比如一个进程在写的时候,另一个进 程要注意读写的问题,相当于线程中的线程安全,当然,共享内存区同样可以用作线程间通讯,不 过没这个必要,线程间本来就已经共享了同一进程内的一块内存。

讲一讲IO多路复用

IO多路复用是指内核一旦发现进程指定的一个或者多个IO条件准备读取,它就通知该进程。IO多 路复用适用如下场合:

  • 当客户处理多个描述字时(一般是交互式输入和网络套接口),必须使用I/O复用。
  • 当一个客户同时处理多个套接口时,而这种情况是可能的,但很少出现。
  • 如果一个TCP服务器既要处理监听套接口,又要处理已连接套接口,一般也要用到I/O复用。
  • 如果一个服务器即要处理TCP,又要处理UDP,一般要使用I/O复用。
  • 如果一个服务器要处理多个服务或多个协议,一般要使用I/O复用。
  • 与多进程和多线程技术相比,I/O多路复用技术的最大优势是系统开销小,系统不必创建进程/线 程,也不必维护这些进程/线程,从而大大减小了系统的开销。

select、poll 和 epoll

  1. select:时间复杂度 O(n) select 仅仅知道有 I/O 事件发生,但并不知道是哪几个流,所以只能无差别轮询所有流,找出能读出数 据或者写入数据的流,并对其进行操作。所以 select 具有 O(n) 的无差别轮询复杂度,同时处理的流越 多,无差别轮询时间就越长。
  2. poll:时间复杂度 O(n) poll 本质上和 select 没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个 fd 对应的设备状 态, 但是它没有最大连接数的限制,原因是它是基于链表来存储的。
  3. epoll:时间复杂度 O(1) epoll 可以理解为 event poll,不同于忙轮询和无差别轮询,epoll 会把哪个流发生了怎样的 I/O 事件通 知我们。所以说 epoll 实际上是事件驱动(每个事件关联上 fd)的。

死锁必须具备以下四个条件:

互斥条件:该资源任意一个时刻只由一个线程占用。 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。 不剥夺条件:线程已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释 放资源。 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

如何避免线程死锁?

只要破坏产生死锁的四个条件

  1. 破坏互斥条件 这个条件我们没有办法破坏,因为我们用锁本来就是想让他们互斥的(临界资源需要互斥访问)
  2. 破坏请求与保持条件 一次性申请所有的资源。
  3. 破坏不剥夺条件 占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
  4. 破坏循环等待条件 靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。
  5. 锁排序法: 指定获取锁的顺序,比如某个线程只有获得A锁和B锁,才能对某资源进行操作,在多线程条件下, 如何避免死锁? 通过指定锁的获取顺序,比如规定,只有获得A锁的线程才有资格获取B锁,按顺序获取锁就可以避 免死锁。这通常被认为是解决死锁很好的一种方法。 使用显式锁中的ReentrantLock.try(long,TimeUnit)来申请锁

计算机网络

ps:3挥4握必问

计算机网络的各层协议及作用?

计算机网络体系可以大致分为一下三种,OSI七层模型、TCP/IP四层模型和五层模型。

  • OSI七层模型:大而全,但是比较复杂、而且是先有了理论模型,没有实际应用。
  • TCP/IP四层模型:是由实际应用发展总结出来的,从实质上讲,TCP/IP只有最上面三层,最下面一 层没有什么具体内容,TCP/IP参考模型没有真正描述这一层的实现。
  • 五层模型:五层模型只出现在计算机网络教学过程中,这是对七层模型和四层模型的一个折中,既 简洁又能将概念阐述清楚。

image-20230203033104400

七层网络体系结构各层的主要功能:

  • 应用层:为应用程序提供交互服务。在互联网中的应用层协议很多,如域名系统DNS,支持万维网 应用的HTTP协议,支持电子邮件的SMTP协议等。

  • 表示层:主要负责数据格式的转换,如加密解密、转换翻译、压缩解压缩等。

  • 会话层:负责在网络中的两节点之间建立、维持和终止通信,如服务器验证用户登录便是由会话层 完成的。

  • 运输层:有时也译为传输层,向主机进程提供通用的数据传输服务。该层主要有以下两种协议:

    • TCP:提供面向连接的、可靠的数据传输服务;
    • UDP:提供无连接的、尽最大努力的数据传输服务,但不保证数据传输的可靠性。
  • 网络层:选择合适的路由和交换结点,确保数据及时传送。主要包括IP协议。

  • 数据链路层:数据链路层通常简称为链路层。将网络层传下来的IP数据包组装成帧,并再相邻节点 的链路上传送帧。

TCP和UDP的区别?

image-20230203033334402转存失败,建议直接上传图片文件

为啥需要三次握手而不是两次握手

主要有三个原因:

  1. 防止已过期的连接请求报文突然又传送到服务器,因而产生错误和资源浪费。 在双方两次握手即可建立连接的情况下,假设客户端发送 A 报文段请求建立连接,由于网络原因造 成 A 暂时无法到达服务器,服务器接收不到请求报文段就不会返回确认报文段。 客户端在长时间得不到应答的情况下重新发送请求报文段 B,这次 B 顺利到达服务器,服务器随即 返回确认报文并进入 ESTABLISHED 状态,客户端在收到 确认报文后也进入 ESTABLISHED 状态, 双方建立连接并传输数据,之后正常断开连接。 此时姗姗来迟的 A 报文段才到达服务器,服务器随即返回确认报文并进入 ESTABLISHED 状态,但 是已经进入 CLOSED 状态的客户端无法再接受确认报文段,更无法进入 ESTABLISHED 状态,这将 导致服务器长时间单方面等待,造成资源浪费。
  2. 三次握手才能让双方均确认自己和对方的发送和接收能力都正常。 第一次握手:客户端只是发送处请求报文段,什么都无法确认,而服务器可以确认自己的接收能力 和对方的发送能力正常; 第二次握手:客户端可以确认自己发送能力和接收能力正常,对方发送能力和接收能力正常; 第三次握手:服务器可以确认自己发送能力和接收能力正常,对方发送能力和接收能力正常; 可见三次握手才能让双方都确认自己和对方的发送和接收能力全部正常,这样就可以愉快地进行通 信了。
  3. 告知对方自己的初始序号值,并确认收到对方的初始序号值。 TCP 实现了可靠的数据传输,原因之一就是 TCP 报文段中维护了序号字段和确认序号字段,通过 这两个字段双方都可以知道在自己发出的数据中,哪些是已经被对方确认接收的。这两个字段的值 会在初始序号值得基础递增,如果是两次握手,只有发起方的初始序号可以得到确认,而另一方的 初始序号则得不到确认。

TCP协议如何保证可靠性

TCP主要提供了检验和、序列号/确认应答、超时重传、滑动窗口、拥塞控制和 流量控制等方法实现了可 靠性传输。

  • 检验和:通过检验和的方式,接收端可以检测出来数据是否有差错和异常,假如有差错就会直接丢 弃TCP段,重新发送。

  • 序列号/确认应答:

    序列号的作用不仅仅是应答的作用,有了序列号能够将接收到的数据根据序列号排序,并且去掉重 复序列号的数据。

    TCP传输的过程中,每次接收方收到数据后,都会对传输方进行确认应答。也就是发送ACK报文, 这个ACK报文当中带有对应的确认序列号,告诉发送方,接收到了哪些数据,下一次的数据从哪里 发。

  • 滑动窗口:滑动窗口既提高了报文传输的效率,也避免了发送方发送过多的数据而导致接收方无法 正常处理的异常。

  • 超时重传:超时重传是指发送出去的数据包到接收到确认包之间的时间,如果超过了这个时间会被 认为是丢包了,需要重传。最大超时时间是动态计算的。

  • 拥塞控制:在数据传输过程中,可能由于网络状态的问题,造成网络拥堵,此时引入拥塞控制机 制,在保证TCP可靠性的同时,提高性能。

  • 流量控制:如果主机A 一直向主机B发送数据,不考虑主机B的接受能力,则可能导致主机B的接受 缓冲区满了而无法再接受数据,从而会导致大量的数据丢包,引发重传机制。而在重传的过程中, 若主机B的接收缓冲区情况仍未好转,则会将大量的时间浪费在重传数据上,降低传送数据的效 率。所以引入流量控制机制,主机B通过告诉主机A自己接收缓冲区的大小,来使主机A控制发送的 数据量。流量控制与TCP协议报头中的窗口大小有

HTTP常见的状态码有哪些?

常见状态码:

  • 200:服务器已成功处理了请求。 通常,这表示服务器提供了请求的网页。
  • 301 : (永久移动) 请求的网页已永久移动到新位置。 服务器返回此响应(对 GET 或 HEAD 请求的响 应)时,会自动将请求者转到新位置。
  • 302:(临时移动) 服务器目前从不同位置的网页响应请求,但请求者应继续使用原有位置来进行以 后的请求。
  • 400 :客户端请求有语法错误,不能被服务器所理解。
  • 403 :服务器收到请求,但是拒绝提供服务。
  • 404 :(未找到) 服务器找不到请求的网页。
  • 500: (服务器内部错误) 服务器遇到错误,无法完成请求。

HTTPS原理 看图

对称加密非对称加密同时使用

preview转存失败,建议直接上传图片文件

Cookie和Session的区别?

  • 作用范围不同,Cookie 保存在客户端(浏览器),Session 保存在服务器端。

  • 存取方式的不同,Cookie 只能保存 ASCII,Session 可以存任意数据类型,一般情况下我们可以在

    Session 中保持一些常用变量信息,比如说 UserId 等。

  • 有效期不同,Cookie 可设置为长时间保持,比如我们经常使用的默认登录功能,Session 一般失效 时间较短,客户端关闭或者 Session 超时都会失效。

  • 隐私策略不同,Cookie 存储在客户端,比较容易遭到不法获取,早期有人将用户的登录名和密码 存储在 Cookie 中导致信息被窃取;Session 存储在服务端,安全性相对 Cookie 要好一些。

  • 存储大小不同, 单个 Cookie 保存的数据不能超过 4K,Session 可存储数据远高于 Cookie。

什么是XSS攻击?

XSS也称 cross-site scripting,跨站脚本。这种攻击是由于服务器将攻击者存储的数据原原本本地显 示给其他用户所致的。比如一个存在XSS漏洞的论坛,用户发帖时就可以引入带有<script>标签的 代码,导致恶意代码的执行。 预防措施有:

  • 前端:过滤。
  • 后端:转义,比如go自带的处理器就具有转义功能。

9.项目知识汇总

RPC

1. 什么是rpc

  1. 自己的理解

    远程调用的框架,现实中分布式微服务已经成为主流,拥有两个服务A,B,我想要通过A服务调用B服务的方法可以使用HTTP进行访问,通过也可以使用rpc进行调用,rpc就是为了这种情况而应运而生的。

    远程服务端的方法可以在本地服务优雅的进行调用

  2. rpc框架简要流程 (架构)

    sequenceDiagram
      participant a AS client 
      participant b AS RPC
      participant c AS server
     
      
      a->>+b:调用远程接口
      alt 传输失败
        b->>+c:通过socket传输
      else 传输失败
        b-->>c:失败重传
      end
      c->>-b:成功返回数据
      b->>-a:返回需要的结果
      
       
    

2. 为啥选择rpc框架

为什么用 RPC,不用 HTTP

首先需要指正,这两个并不是并行概念。RPC 是一种设计,就是为了解决不同服务之间的调用问题,完整的 RPC 实现一般会包含有 传输协议序列化协议 这两个。

而 HTTP 是一种传输协议,RPC 框架完全可以使用 HTTP 作为传输协议,也可以直接使用 TCP,使用不同的协议一般也是为了适应不同的场景。

使用 TCP 和使用 HTTP 各有优势:

传输效率

  • TCP,通常自定义上层协议,可以让请求报文体积更小
  • HTTP:如果是基于HTTP 1.1 的协议,请求中会包含很多无用的内容

性能消耗,主要在于序列化和反序列化的耗时

  • TCP,可以基于各种序列化框架进行,效率比较高
  • HTTP,大部分是通过 json 来实现的,字节大小和序列化耗时都要更消耗性能

跨平台

  • TCP:通常要求客户端和服务器为统一平台
  • HTTP:可以在各种异构系统上运行

总结:   RPC 的 TCP 方式主要用于公司内部的服务调用,性能消耗低,传输效率高。HTTP主要用于对外的异构环境,浏览器接口调用,APP接口调用,第三方接口调用等。

3. 现在主流的rpc框架有啥

  1. dubbo 阿里的开源rpc框架

    dubbo的缺点 :无法跨语言 dubbo的线程模型相比netty的线程模型有点弱

  2. grpc 谷歌的开源rpc框架

  3. hession 老牌

  4. open feign 不算rpc 通过 http进行远程调用 可以实现跨平台 调用 但是相对来说http请求体中的多数内容比较冗余

4. 手写rpc框架的原因

  1. 起先接触的rpc框架为openfeign,通过学习了解,openfeign严格来说不算rpc框架,相对dubbo这种真正的rpc框架有很大的出入,因此在项目下面微服务项目开展的同时,进行了仿照dubbo的重写操作,为了自己更好的了解dubbo,更好的了解rpc框架
  2. 同时在编写rpc框架的同时巩固自己对基础编码的能力,完善代码结构。

4. 当前项目的架构

call
获取注册
注册
服务端
注册中心
客户端

Netty

img

netty是干嘛的

  1. 基于java的nio封装的一套框架,里面有很多可以组件,简化了开发者java网络编程的复杂度

  2. netty可以实现im通信、rpc框架、消息推送系统等一系例产品的开发

    使用简单,功能强大,扩展性强,性能好

为什么Netty性能高

  • IO 线程模型:同步非阻塞,用最少的资源做更多的事。

  • 内存零拷贝:尽量减少不必要的内存拷贝,实现了更高效率的传输。

  • 内存池设计:申请的内存可以重用,主要指直接内存。内部实现是用一颗二叉查找树管理内存分配情况。

  • 串行化处理读写:避免使用锁带来的性能开销。

  • 高性能序列化协议:支持 protobuf 等高性能序列化协议。

    简单介绍一下BIO,NIO,AIO

    1. BIO同步阻塞io,一个连接一个线程
    2. NIO同步非阻塞io,一个请求一个线程,连接占用一个线程,实际请求占用多线程处理,io多路复用
    3. AIO异步非阻塞io,一个有效请求一个线程,先通过系统进行完成再分配线程进行通知

netty分层

1. Core 核心层

Core 核心层是 Netty 最精华的内容,它提供了底层网络通信的通用抽象和实现,包括可扩展的事件模型、通用的通信 API、支持零拷贝的 ByteBuf 等。

2. Protocol Support 协议支持层

协议支持层基本上覆盖了主流协议的编解码实现,如 HTTP、SSL、Protobuf、压缩、大文件传输、WebSocket、文 本、二进制等主流协议,此外 Netty 还支持自定义应用层协议。Netty 丰富的协议支持降低了用户的开发成本,基于 Netty 我们可以快速开发 HTTP、WebSocket 等服务。

3. Transport Service 传输服务层

传输服务层提供了网络传输能力的定义和实现方法。它支持 Socket、HTTP 隧道、虚拟机管道等传输方式。Netty 对 TCP、UDP 等数据传输做了抽象和封装,用户可以更聚焦在业务逻辑实现上,而不必关系底层数据传输的细节。

netty的组件

  • Channel:代表了一个链接,与EventLoop一起用来参与IO处理。
  • ChannelHandler:为了支持各种协议和处理数据的方式,便诞生了Handler组件。Handler主要用来处理各种事件,这里的事件很广泛,比如可以是连接、数据接收、异常、数据转换等。
  • ChannelPipeline:提供了 ChannelHandler 链的容器,并定义了用于在该链上传播入站 和出站事件流的 API。
  • EventLoop:Channel处理IO操作,一个EventLoop可以为多个Channel服务。
  • EventLoopGroup:会包含多个EventLoop。

netty的常用的线程模型

  1. netty 存在两种线程 boss 线程和 worker 线程 ,boss线程默认给一个负责请求的连接,而worker线程存在cpu+1个,通过boos线程分发多个任务给线程给worker线程处理。

  2. 自己实现的rpc仅实现了direct模式 worker 工人执行到底的模式

  3. dubbo线程模型分析

    • 默认是all:所有消息都派发到线程池,包括请求,响应,连接事件,断开事件,心跳等。 即worker线程接收到事件后,将该事件提交到业务线程池中,自己再去处理其他事。
    • direct:worker线程接收到事件后,由worker执行到底。
    • message:只有请求响应消息派发到线程池,其它连接断开事件,心跳等消息,直接在 IO线程上
    • execution:只请求消息派发到线程池,不含响应(客户端线程池),响应和其它连接断开事件,心跳等消息,直接在 IO 线程上执行
    • connection:在 IO 线程上,将连接断开事件放入队列,有序逐个执行,其它消息派发到线程池。
  4. dubbo流程

    1. 客户端的主线程发出一个请求后获得future,在执行get时进行阻塞等待;
    2. 服务端使用worker线程(netty通信模型)接收到请求后,将请求提交到server线程池中进行处理
    3. server线程处理完成之后,将相应结果返回给客户端的worker线程池(netty通信模型),最后,worker线程将响应结果提交到client线程池进行处理
    4. client线程将响应结果填充到future中,然后唤醒等待的主线程,主线程获取结果,返回给客户端
  5. reactor线程模型

    Reactor三种模式形象比喻 餐厅一般有接待员和服务员,接待员负责在门口接待顾客,服务员负责全程服务顾客 Reactor的三种线程模型可以用接待员和服务员类比

    1. 单Reactor单线程模型:接待员和服务员是同一个人,一直为顾客服务。客流量较少适合

    2. 单Reactor多线程模型:一个接待员,多个服务员。客流量大,一个人忙不过来,由专门的接待员在门口接待顾客,然后安排好桌子后,由一个服务员一直服务,一般每个服务员负责一片中的几张桌子

    3. 多Reactor多线程模型:多个接待员,多个服务员。这种就是客流量太大了,一个接待员忙不过来了

      补充redis的线程模型 侧面证明了redis为什么快

      1. 基于reactor模型实现的文件事件处理器
      2. 分为以下四个部分 套接字 i/o多路复用程序 文件时间分派器 事件处理器

      image-20220328181656149

netty实现自己的传输协议

image-20220327180300799

/**
 * 自定义协议解码器
 * <pre>
 *   0     1     2     3     4        5     6     7     8         9          10      11      12   13   14   15
 *   +-----+-----+-----+-----+--------+----+----+----+------+-----------+-------+----- --+-----+-----+-------+
 *   |   magic   code        |version | full length         | messageType| codec|compress|    RequestId      |
 *   +-----------------------+--------+---------------------+-----------+-----------+-----------+------------+
 *   |                                                                                                       |
 *   |                                         body                                                          |
 *   |                                                                                                       |
 *   |                                        ... ...                                                        |
 *   +-------------------------------------------------------------------------------------------------------+
 * 4B  magic code(魔法数)   1B version(版本)   4B full length(消息长度)    1B messageType(消息类型)
 * 1B compress(压缩类型) 1B codec(序列化类型)    4B  requestId(请求的Id)
 * body(object类型数据)
 * </pre>
 */

常询问的netty问题

  1. netty零拷贝问题 延伸到 用户态与内核态的转化问题

    • 实现一个文件通过sokect发往另一台主机的过程,传统发送需要将数据读取到java的内存中再进行传递

    • 其中先是用户态转变为内核态,读取硬盘中的数据到缓存区,然后将缓存区的数据读取到java内存中转变回用户态,再将数据转化到sokect中的缓存区,再拷贝给发送引擎进行发送

    • linux零拷贝

      • 用户态发起发送指令,进入用户态,在内核态进行磁盘的转化,最后通过拷贝发送
    • netty零拷贝 优化 进阶

      • 堆外内存,避免 JVM 堆内存到堆外内存的数据拷贝。

      • CompositeByteBuf 类,可以组合多个 Buffer 对象合并成一个逻辑上的对象,避免通过传统内存拷贝的方式将几个 Buffer 合并成一个大的 Buffer。

      • 通过 Unpooled.wrappedBuffer 可以将 byte 数组包装成 ByteBuf 对象,包装过程中不会产生内存拷贝。

      • ByteBuf.slice 操作与 Unpooled.wrappedBuffer 相反,slice 操作可以将一个 ByteBuf 对象切分成多个 ByteBuf 对象,切分过程中不会产生内存拷贝,底层共享一个 byte 数组的存储空间。

      • Netty 使用 FileRegion 实现文件传输,FileRegion 底层封装了 FileChannel#transferTo() 方法,可以将文件缓冲区的数据直接传输到目标 Channel,避免内核缓冲区和用户态缓冲区之间的数据拷贝,这属于操作系统级别的零拷贝。

    延伸问题

    用户态内核态是啥:

    • 内核态:内校态还行的程宁可以访问计算机的任向数据和资源,不受限制,包括外用设备:比如网卡、硬林等。处于内校态的CPU 可以从一个程序切换到号外一-个程厅,并且占用CPU 不会发生抢古情况
    • 用户态:用户态运行的程序只能受限地访问内存,只能直接读取用户程序的数据,并且不允许访问外国设备,用户态下的 CPU 不允许独占,也就是说 CPU 能够被其他程序获取
  2. netty如何解决的半包粘包问题

    • 设置固定大小的消息长度,如果长度不足则使用空字符弥补,它的缺点比较明显,比较消耗网络流量,因此不建议使用;
    • 使用分隔符来确定消息的边界,从而避免粘包和半包问题的产生;
    • 将消息分为消息头和消息体,在头部中保存有当前整个消息的长度,只有在读取到足够长度的消息之后才算是读到了一个完整的消息。
  3. BIO和NIO的区别

    • BIO以流的方式处理数据,NIO以块的方式处理数据,块IO的效率比流IO高很多。(比如说流IO他是一个流,你必须时刻去接着他,不然一些流就会丢失造成数据丢失,所以处理这个请求的线程就阻塞了他无法去处理别的请求,他必须时刻盯着这个请求防止数据丢失。而块IO就不一样了,线程可以等他的数据全部写入到缓冲区中形成一个数据块然后再去处理他,在这期间该线程可以去处理其他请求)PS 重点缓冲区
    • BIO是阻塞的,NIO是非阻塞的
    • BIO基于字节流和字符流进行操作的,而NIO基于Channel(通道)和Buffer(缓冲区)进行操作的,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择器)用于监听多个通道事件,因此使用单个线程就可以监听多个客户端通道

序列化算法介绍

什么事序列化什么是反序列化

序列化是将对象转化为二进制流的过程,反序列化是将二进制流转化为对象的过程。

在java中语言自带序列化接口,通过实现Serializable接口实现序列化

json

  1. 优点可读性强
  2. 缺点占用内存偏大,序列化反序列化时间长

hessian

  1. 实现原理 简单来说 hassian将序列化的简单对象进行缩减序列化,对引用对象进行map处理进行序列化 hessian着重于数据的序列化
  2. hessian 跨平台 多平台可以进行使用 支持多种语言

protobuf

  1. 实现原理 转化为二进制流
  2. 序列化后的对象 体积很小

kryo

Kryo 是一个快速高效的 Java 序列化框架,旨在提供快速、高效和易用的 API。无论文件、数据库或网络数据 Kryo 都 可以随时完成序列化。 Kryo 还可以执行自动深拷贝、浅拷贝。这是对象到对象的直接拷贝,而不是对象->字节->对 象的拷贝。kryo 速度较快,序列化后体积较小,但是跨语言支持较复杂。

fst

Fast Serializition Tool

负载均衡算法介绍

一致性哈希

  1. 哈希环 将服务端通过hash映射到hash环上通过
  2. 请求访问时通过请求的hash判断请求寻找到哪台服务器进行处理
  3. 当添加服务 或者删除服务的时候只会影响部分机器的工作状态 不会对系统产生巨大损失

轮询算法 随机算法

大量随机的结果趋向于轮询,轮询因为木桶的最短板原理,轮训会造成性能的损耗

注册中心分析

cap理论 base理论

  1. cap分别是一致性,可用性,分区容错性

  2. base 理论

    实际上,不是为了P(分区容错性),必须在C(一致性)和A(可用性)之间任选其一。分区的情况很少出现,CAP在大多时间能够同时满足C和A。

    • 类别: 基本可用(Basically Available)软状态(Soft State)最终一致性(Eventually Consistent)

    • 基本可用

      什么是基本可用呢?假设系统,出现了不可预知的故障,但还是能用,相比较正常的系统而言:

      响应时间上的损失:正常情况下的搜索引擎0.5秒即返回给用户结果,而基本可用的搜索引擎可以在2秒作用返回结果。功能上的损失:在一个电商网站上,正常情况下,用户可以顺利完成每一笔订单。但是到了大促期间,为了保护购物系统的稳定性,部分消费者可能会被引导到一个降级页面。

    • 软状态

      什么是软状态呢?相对于原子性而言,要求多个节点的数据副本都是一致的,这是一种“硬状态”。

      软状态指的是:允许系统中的数据存在中间状态,并认为该状态不影响系统的整体可用性,即允许系统在多个不同节点的数据副本存在数据延时。

    • 最终一致性

      上面说软状态,然后不可能一直是软状态,必须有个时间期限。在期限过后,应当保证所有副本保持数据一致性,从而达到数据的最终一致性。这个时间期限取决于网络延时、系统负载、数据复制方案设计等等因素。

基于 DNSRPC 是两个方向 :

openfeign是通过 DNS 寻找服务端进行消费的

如今的注册中心有:

nacos ap + cp

zookeeper cp

eureka ap

举个例子:在双十一或者京东618进行高强度的线上活动时,需要保持可用的状态,因此ap为最佳选择,允许数据的暂时不一致,之后通过补偿进行完善数据的一致性

image-20220326181309303

项目整合SPRING的步骤

分为三步

  1. 构建注解
  2. 扫描注解 添加bean
  3. bean生成时增强
  1. 构建注解

    简单创建三个注解 实现client server扫包 client server分别的服务版本注解

  2. 扫包

    通过继承ImportBeanDefinitionRegistrar 将bean的definition信息存储到要加载的bean中

  3. beanPostProcess

    通过前置处理将需要将需要注册到注册中心的方法进行注册

    后置处理将客户端代理对象进行注册

SPI机制

  • Java SPI就是提供这样的一个服务发现机制:为某个接口寻找服务实现的机制。有点类似IOC的思想,就是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要。所以SPI的核心思想就是解耦。Java SPI 实际上是“基于接口的编程+策略模式+配置文件”组合实现的动态加载机制。

  • dubbo通过增强SPI机制

    1. java的spi时通过遍历MATEINF中的所有包名进行加载,也就是加载了全部放到一个LInkedList中
    2. 并发的情况下java spi是线程不安全的
    3. 我们不需要全部加载,希望通过按需求加载,同时重写spi简化了配置文件的复杂度

收集到的问题汇总

  1. 如何优雅的关闭dubbo 可以延伸到如何关闭rpc

    ShutdownHook进行优雅的线程关闭

     public void clearAll(byte type) {
            log.info("addShutdownHook for clearAll");
            Runtime.getRuntime().addShutdownHook(new Thread(() -> {
                try {
                    InetSocketAddress inetSocketAddress = new InetSocketAddress(InetAddress.getLocalHost().getHostAddress(), NettyRpcServer.PORT);
                    if (Objects.equals(RegisterAndDiscoveryTypeEnum.getName(type), "nacos")) {
                        NacosUtil.clearRegistry(NacosUtil.getNacosClient());
                    }
                    CuratorUtil.clearRegistry(CuratorUtil.getZkClient(), inetSocketAddress);
                } catch (UnknownHostException ignored) {
                }
                ThreadPoolFactoryUtil.shutDownAllThreadPool();
            }));
        }
    
  2. 服务启动消费者是如何感知的

    主要分为四个核心流程:

    • 服务提供者启动服务,并暴露服务端口;
    • 启动时扫描需要对外发布的服务,并将服务元数据信息发布到注册中心;
    • 接收 RPC 请求,解码后得到请求消息;
    • 提交请求至自定义线程池进行处理,并将处理结果写回客户端。
  3. dubbo的需要改进的地方

  4. 先序列化还是先编吗

    答:先进行序列化在进行编码,编码发送后先进行解码,之后进行反序列化。

RAFT

介绍

Raft算法由leader节点来处理一致性问题。leader节点接收来自客户端的请求日志数据,然后同步到集群中其它节点进行复制,当日志已经同步到超过半数以上节点的时候,leader节点再通知集群中其它节点哪些日志已经被复制成功,可以提交到raft状态机中执行。

通过以上方式,Raft算法将要解决的一致性问题分为了以下几个子问题。

  • leader选举:集群中必须存在一个leader节点。
  • 日志复制:leader节点接收来自客户端的请求然后将这些请求序列化成日志数据再同步到集群中其它节点。
  • 安全性:如果某个节点已经将一条提交过的数据输入raft状态机执行了,那么其它节点不可能再将相同索引 的另一条日志数据输入到raft状态机中执行。

什么是MultiRaft

简单来说,MultiRaft 是在整个系统中,把所管理的数据按照一定的方式切片,每一个切片的数据都有自己的副本,这些副本之间的数据使用 Raft 来保证数据的一致性,在全局来看整个系统中同时存在多个 Raft-Group,就像这个样子:

为啥要使用Rocksdb

  • LSM Nosql的推荐

    HBase RocksDb LevelDb

B+数适合顺序写

LSM更加适合于高频写入随机写入

主要涉及到三个步骤

  1. 写入memtable
  2. 通过only-read memtable 压缩 flush 入sst文件
  3. SST文件 压缩 megre LSM树
  • compaction style

    • minor Compaction,就是把memtable中的数据导出到SSTable文件中;
    • major compaction就是合并不同层级的SSTable文件

1)冗余存储,对于某个key,实际上除了最新的那条记录外,其他的记录都是冗余无用的,但是仍然占用了存储空间。因此需要进行Compact操作(合并多个SSTable)来清除冗余的记录。

2)读取时需要从最新的倒着查询,直到找到某个key的记录。最坏情况需要查询完所有的SSTable,这里可以通过前面提到的索引/布隆过滤器来优化查找速度。

选举

https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/f24d8a93dc1e40b7a37522742e9da7ba~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyo5a2m5b6I6I-c:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzk4NTA5MjYzMzY5MDY5NiJ9&rk3s=e9ecf3d6&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1727776786&x-orig-sign=qoXzELAKFq61SuwRvnQy5xJLipE%3D

https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/e164d471bddd437daee1e51cfe266c22~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyo5a2m5b6I6I-c:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzk4NTA5MjYzMzY5MDY5NiJ9&rk3s=e9ecf3d6&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1727776786&x-orig-sign=qwoZ9SolVcQugrkqIhIHda0w%2FbI%3D

使用 **心跳超时heartbeat timeout**机制来触发 Leader 选举:

  • 节点启动时默认处于 Follower 状态,如果 Follower 超时未收到 Leader 心跳信息,会转换为 Candidate 并向其他节点发起 RequestVote 请求。
  • 当 Candidate 收到半数以上的选票之后成为 Leader,开始定时向其他节点发起 AppendEntries 请求以维持其 Leader 的地位。
  • Leader 失效之后停止发送心跳,Follower 的心跳超时机制又会被触发,开始新一轮的选举。

复制

未提交日志 已提交日志

https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/a459dc2f85a148eabb41f50944493a5d~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyo5a2m5b6I6I-c:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzk4NTA5MjYzMzY5MDY5NiJ9&rk3s=e9ecf3d6&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1727776786&x-orig-sign=oE9EkYoaxWMU1WOFDvhsDCS2Ru0%3D

集群中只有 Leader 对外提供服务:

客户端与 Leader 进行通信时,每个请求包含一条可以被状态机执行的命令。

当 Leader 在接收到命令之后,首先会将命令转换为一条对应的 日志记录log entry,并追加到本地的日志中。然后调用 AppendEntries 将这条日志复制到其他节点的日志中。

当日志被复制到过半数节点上时,Leader 会将这条日志中包含的命令 **提交commit**状态机执行,最后将执行结果告知客户端。

网络分区

https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/4db9488822e24c5d94cee414b41f406e~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyo5a2m5b6I6I-c:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzk4NTA5MjYzMzY5MDY5NiJ9&rk3s=e9ecf3d6&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1727776787&x-orig-sign=rZ3F%2Fp0LJHaeDzZ8OH5oD3Kv%2FgU%3D

使用 **过半数majority**机制来处理脑裂:

发生网络分区后,集群中可能同时出现多个 Leader,复制机制保证了最多只有一个 Leader 能够正常对外提供服务。如果日志无法复制到多数节点,Leader 会拒绝提交这些日志,当网络分区消失后,集群会自动恢复到一致的状态。

安全性保证

选举时…

保证新的 Leader 拥有所有已经提交的日志

  • 每个 Follower 节点在投票时会检查 Candidate 的日志索引,并拒绝为日志不完整的 Candidate 投赞成票
  • 半数以上的 Follower 节点都投了赞成票,意味着 Candidate 中包含了所有可能已经被提交的日志
提交日志时…

Leader 只主动提交自己任期内产生的日志

  • 如果记录是当前 Leader 所创建的,那么当这条记录被复制到大多数节点上时,Leader 就可以提交这条记录以及之前的记录
  • 如果记录是之前 Leader 所创建的,则只有当前 Leader 创建的记录被提交后,才能提交这些由之前 Leader 创建的日志

总结

一致性算法的本质:一致性与可用性之间的权衡

Raft 的优点:Single Leader 的架构简化日志管理

所有日志都由 Leader 流向其他节点,无需与其他节点进行协商。其他节点只需要记录并应用 Leader 发送过来日志内容即可,将原来的两阶段请求优化为一次 RPC 调用。

Raft 的缺点:对日志的连续性有较高要求

为了简化日志管理,Raft 的日志不允许存在空隙,限制了并发性。某些应用场景下,需要通过Multi-Raft的模式对无关的业务进行解耦,从而提高系统的并发度。

日志问题

  1. 同步流程

image-20230203144657113问题:为啥跟随着的日志提交是异步的

在修正流程中保证了日志的一致性

  1. 修正流程

image-20230203144640123

image-20230203144623809

RocksDB 通过以下几种策略来解决写放大和读放大问题:

解决写放大的策略

  1. 写入缓冲(MemTable) : RocksDB 首先将写入操作存储在内存中的 MemTable,减少频繁的磁盘写入。
  2. 合并与压缩: 使用 LSM-tree 结构,定期合并 SST 文件以减少数据重复,减少后续写入操作的写放大。
  3. 压缩策略: 提供多种压缩算法(如 Snappy、Zlib),通过选择适当的压缩策略,降低存储空间和写入次数。
  4. 批量写入: 通过 WriteBatch 进行批量写入,合并多个写操作,降低每次写入对磁盘的影响。

解决读放大的策略

  1. 内存索引: 使用内存中的索引来快速定位数据,减少对磁盘的读取次数。
  2. Bloom Filters: 使用布隆过滤器来快速判断某个键是否存在,避免不必要的读取。
  3. 合并与压缩: 类似于写放大,定期合并 SST 文件,减少文件数量,从而加快读取性能。
  4. 增量读取: 通过增量读取和缓存机制,提高读操作的效率,减少对存储介质的访问次数。

在 LSM(Log-Structured Merge)树中,如果对某个值进行修改,通常会导致写放大的问题,但通过以下几种机制可以有效解决或减少写放大的影响:

解决写放大的策略

  1. 更新策略:

    • 写入新值: 修改操作不会直接更新原有值,而是将新值插入到 MemTable 中。这意味着旧值仍然存在,只有在合并或压缩时才会被清理。
    • 标记删除: 对于被修改的键,系统可以先插入一个删除标记(tombstone),然后再插入新值。这样,在读取时,系统会忽略被标记的旧值。
  2. 合并与压缩:

    • 定期执行合并和压缩操作,清理旧的值和删除标记。这可以减少存储中冗余的数据,从而降低写放大。
  3. 使用增量合并:

    • 在进行合并操作时,LSM 树只会处理最新的有效数据,忽略已被标记删除的旧数据。这可以有效减少在合并过程中的写放大。
  4. MemTable 切换:

    • 当 MemTable 满了并刷新到磁盘时,系统会将旧的 MemTable 标记为不可写,并创建新的 MemTable。通过这种方式,新值会被添加到新的 MemTable 中,减少对旧数据的频繁更新。
  5. 调优参数:

    • 根据应用需求调节 MemTable 大小、SST 文件大小、压缩策略等参数,以优化写入性能,减少写放大。

算法

快排

    public static void quickSort(int[] a, int l, int r) {

        if (l < r) {
            int i,j,x;

            i = l;
            j = r;
            x = a[i];
            while (i < j) {
                while(i < j && a[j] > x)
                    j--; // 从右向左找第一个小于x的数
                if(i < j)
                    a[i++] = a[j];
                while(i < j && a[i] < x)
                    i++; // 从左向右找第一个大于x的数
                if(i < j)
                    a[j--] = a[i];
            }
            a[i] = x;
            quickSort(a, l, i-1); /* 递归调用 */
            quickSort(a, i+1, r); /* 递归调用 */
        }
    }

    public static void main(String[] args) {
        int i;
        int a[] = {30,40,60,10,20,50};

        System.out.printf("before sort:");
        for (i=0; i<a.length; i++)
            System.out.printf("%d ", a[i]);
        System.out.printf("\n");

        quickSort(a, 0, a.length-1);

        System.out.printf("after  sort:");
        for (i=0; i<a.length; i++)
            System.out.printf("%d ", a[i]);
        System.out.printf("\n");
    }

没有重复的最长字符串

class Solution {
    public int lengthOfLongestSubstring(String s) {
        char[] chars = s.toCharArray();
        Map<Character, Integer> map = new HashMap<>();
        int left = 0, maxlen = 0;
        for (int i = 0; i < chars.length; i++) {
            if (map.containsKey(chars[i])) {
                left = Math.max(left,map.get(chars[i])+1); //滑动窗口永不回头!!!
                           }
            map.put(chars[i], i);
            maxlen=Math.max(maxlen,i-left+1);
        }
        return maxlen;
    }
}

数组中的第K个最大元素

class Solution {
    int quickselect(int[] nums, int l, int r, int k) {
        if (l == r) return nums[k];
        int x = nums[l], i = l - 1, j = r + 1;
        while (i < j) {
            do i++; while (nums[i] < x);
            do j--; while (nums[j] > x);
            if (i < j){
                int tmp = nums[i];
                nums[i] = nums[j];
                nums[j] = tmp;
            }
        }
        if (k <= j) return quickselect(nums, l, j, k);
        else return quickselect(nums, j + 1, r, k);
    }
    public int findKthLargest(int[] _nums, int k) {
        int n = _nums.length;
        return quickselect(_nums, 0, n - 1, n - k);
    }
}

合并区间

public int[][] merge(int[][] intervals) {
        LinkedList<int[]> list = new LinkedList();
        int m = intervals.length;
        if (m == 1) return intervals;
        if (m == 0) return new int[0][0];
        int n = intervals[0].length;
        Arrays.sort(intervals, (a, b) -> a[0] - b[0]);
        list.offer(intervals[0]);
        for (int i = 1; i < m; i++) {
            int[] temp = list.getLast();
            int right = temp[1];
            int left = intervals[i][0];
            if(right>=left){
                list.removeLast();
                list.add(new int[]{temp[0],Math.max(temp[1],intervals[i][1])});
            }else list.offer(intervals[i]);
        }
        return list.toArray(new int[list.size()][]);
    }

最长回文

class Solution {
  public String longestPalindrome(String s) {
        // StringBuilder string = new StringBuilder();
        String string="";
        int index = 0;
        for (int i = 0; i < 2*s.length()-1; i++) {
            int l= i/2;
            int r= l+i%2;
            while(l>=0 && r<s.length() && s.charAt(l)==s.charAt(r)){
                if(index==0||r-l>index){
                    index=r-l;
                    string=s.substring(l,r+1);
                }
                l--;
                r++;
            } 
        }
        return string.toString();
    }
}

零钱兑换

class Solution {
 int dp[]; //状态数组,用来存放amount金额需要的硬币数量
    public int coinChange(int[] nums, int amount){
        dp = new int[amount + 1];
        Arrays.fill(dp, amount + 1); //将数组中的每一个置为amount + 1,因为硬币的最小金额是1,所以显然不可能需要amount + 1个硬币
        return dp(nums, amount);
    }
    public int dp(int[] nums, int amount){
        dp[0] = 0;
        for (int i = 0; i < dp.length; i++) {//for循环,dp数组中的每个值都要填充
            for (int coin : nums) {
                if (i - coin < 0){
                    continue;
                }
                dp[i] = Math.min(dp[i], 1 + dp[i - coin]);//状态转移方程
            }
        }
        return dp[amount] == amount + 1? -1 : dp[amount];
    }

}

比较版本号

class Solution {
    public int compareVersion(String version1, String version2) {
        int n = version1.length(), m = version2.length();
        int i = 0, j = 0;
        while (i < n || j < m) {
            int x = 0;
            for (; i < n && version1.charAt(i) != '.'; ++i) {
                x = x * 10 + version1.charAt(i) - '0';
            }
            ++i; // 跳过点号
            int y = 0;
            for (; j < m && version2.charAt(j) != '.'; ++j) {
                y = y * 10 + version2.charAt(j) - '0';
            }
            ++j; // 跳过点号
            if (x != y) {
                return x > y ? 1 : -1;
            }
        }
        return 0;
    }
}

全排列

class Solution {
    List<List<Integer>> res = new ArrayList<>();
    List<Integer> list = new ArrayList<>();
    public List<List<Integer>> permute(int[] nums) {
        
        backtrack(nums,0);
        return res;        
    }
    
    public void backtrack( int[] nums,int n) {
       if(n==nums.length-1){
           for(int x:nums){
               list.add(x);
            }
           res.add(new ArrayList(list));
           list.clear();
       }
       for (int i = n; i <nums.length ; i++) {
           swap(nums,i,n);
            backtrack(nums,n+1);
            swap(nums,i,n);
       }
    }
    void swap(int[] nums,int i,int j ){
        int temp=nums[i];
        nums[i]=nums[j];
        nums[j]=temp;
    }
}

LRU

class LRUCache {
    private int cap;
    private Map<Integer,Node> map;
    private Node head;
    private Node tail;
     class Node{
        int key;
        int value;
        Node pre;
        Node next;
        public Node(){}
        public Node(int key,int val){
            this.key = key;
            this.value = val; 
        }
    }

    public void addTail(Node node){
            Node tmp = tail.pre;
            tail.pre = node;
            tmp.next = node;
            node.next = tail;
            node.pre = tmp;
        }

        public void delNode(Node node){
            node.next.pre= node.pre;
            node.pre.next=node.next;
        }

        public void selectToTail(Node node){
            delNode(node);
            addTail(node);
        }

        public Node delHead(){
            Node tmp = head.next;
            head.next= tmp.next;
            tmp.next.pre = head; 
            return tmp;
        }
        
    public LRUCache(int capacity) {
        this.cap = capacity;
        this.map = new HashMap<>();
        head = new Node();
        tail = new Node();
        tail.pre = head;
        head.next = tail;     
    }
    
    public int get(int key) {
        if(map.containsKey(key)){
            Node temp = map.get(key);
            selectToTail(temp);
            return temp.value;
        }
        return -1;
    }
    
    public void put(int key, int value) {
        Node tmp = new Node(key,value);
        if(map.containsKey(key)){
            delNode(map.get(key));
            map.remove(key);
        }
        if(map.size()>=cap){
          Node del = delHead();  
          map.remove(del.key);
        }
        addTail(tmp);
        map.put(tmp.key,tmp);
    }
}

螺旋数组

class Solution {
    public List<Integer> spiralOrder(int[][] matrix) {
        List<Integer> res = new ArrayList<>();
        int mf = 0, nf = 0;
        int ml = matrix.length - 1;
        int nl = matrix[0].length - 1;
        while (true) {
            for (int i = nf; i <= nl; i++) res.add(matrix[mf][i]);
            if (++mf > ml) break;
            for (int i = mf ; i <= ml ;i++)res.add(matrix[i][nl]);
            if (--nl < nf) break;
            for (int i = nl ; i >= nf ;i--)res.add(matrix[ml][i]);
            if (--ml < mf) break;
            for (int i = ml ; i >= mf ;i--)res.add(matrix[i][nf]);
            if (++nf > nl) break;
        }
        return res;


    }
}

反转链表

    public ListNode reverseList(ListNode head) {
        ListNode node = head;
        ListNode cur= null;
        while(node!=null){
            ListNode tmp = node.next;
            node.next = cur;
            cur = node;
            node = tmp;
        }
        return cur;
    }
   

二叉树遍历

/* 层序遍历 */
List<Integer> levelOrder(TreeNode root) {
    // 初始化队列,加入根节点
    Queue<TreeNode> queue = new LinkedList<>();
    queue.add(root);
    // 初始化一个列表,用于保存遍历序列
    List<Integer> list = new ArrayList<>();
    while (!queue.isEmpty()) {
        TreeNode node = queue.poll(); // 队列出队
        list.add(node.val);           // 保存节点值
        if (node.left != null)
            queue.offer(node.left);   // 左子节点入队
        if (node.right != null)
            queue.offer(node.right);  // 右子节点入队
    }
    return list;
}
        
前序遍历
public void preOrderTraverse2(TreeNode root) {
		LinkedList<TreeNode> stack = new LinkedList<>();
		TreeNode pNode = root;
		while (pNode != null || !stack.isEmpty()) {
			if (pNode != null) {
				System.out.print(pNode.val+"  ");
				stack.push(pNode);
				pNode = pNode.left;
			} else { //pNode == null && !stack.isEmpty()
				TreeNode node = stack.pop();
				pNode = node.right;
			}
		}
	}
        中旭遍历
public void inOrderTraverse2(TreeNode root) {
		LinkedList<TreeNode> stack = new LinkedList<>();
		TreeNode pNode = root;
		while (pNode != null || !stack.isEmpty()) {
			if (pNode != null) {
				stack.push(pNode);
				pNode = pNode.left;
			} else { //pNode == null && !stack.isEmpty()
				TreeNode node = stack.pop();
				System.out.print(node.val+"  ");
				pNode = node.right;
			}
		}
	}
        后续
        public void postOrderTraverse(TreeNode root) {
        TreeNode cur, pre = null;

        Stack<TreeNode> stack = new Stack<>();
        stack.push(root);

        while (!stack.empty()) {
            cur = stack.peek();
            if ((cur.left == null && cur.right == null) || (pre != null && (pre == cur.left || pre == cur.right))) {
                System.out.print(cur.val + "->");
                stack.pop();
                pre = cur;
            } else {
                if (cur.right != null)
                    stack.push(cur.right);
                if (cur.left != null)
                    stack.push(cur.left);
            }
        }
    }
    
    最大宽
    class Solution {
    Map<Integer, Integer> levelMin = new HashMap<Integer, Integer>();

    public int widthOfBinaryTree(TreeNode root) {
        return dfs(root, 1, 1);
    }

    public int dfs(TreeNode node, int depth, int index) {
        if (node == null) {
            return 0;
        }
        levelMin.putIfAbsent(depth, index); // 每一层最先访问到的节点会是最左边的节点,即每一层编号的最小值
        return Math.max(index - levelMin.get(depth) + 1, Math.max(dfs(node.left, depth + 1, index * 2), dfs(node.right, depth + 1, index * 2 + 1)));
    }
}

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
class Solution {
    public TreeNode mirrorTree(TreeNode root) {
        helper(root);
        return root;
    }

    void helper(TreeNode node){
        //截止条件
        if(node==null) return;
        swap(node);
        helper(node.left);
        helper(node.right);
    }

    void swap(TreeNode node){
        TreeNode temp= node.left;
        node.left=node.right;
        node.right=temp;
    }
}