互联网架构学习方向

1. JVM

hashcode 和 equals 方法的联系

1、如果两个对象相同(即用equals比较返回true),那么它们的hashCode值一定要相同;
2、如果两个对象的hashCode相同,它们并不一定相同(即用equals比较返回false)

Java中接口和抽象类的区别?

1.抽象类继承Object ,接口不继承Object
2.抽象类有构造器 ,接口没有构造器
3.抽象类有普通成员变量和常量,接口不能定义变量,只能有常量,而且只能是public static final的(默认)还必须要赋初值。

4.抽象类可以有抽象方法和普通的方法,接口只能有抽象的方法并且修饰符只能是public abstract (默认)。
5.抽象类可以有final的方法,接口不能。
6.抽象类只能是单继承,多实现。接口可以多继承其他接口,但是不能实现接口,不能继承其他类
7.抽象类可以有静态方法,接口不可以。

String 类能不能被继承?为什么?

不能, final修饰的继承

1.双亲委派模式?

从上到下,分为启动类加载器、扩展类加载器、应用程序类加载器、自定义加载器。。

双亲委派模式:
类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,
如果父类加载器可以完成类加载任务,就成功返回;
只有父类加载器无法完成此加载任务时,才自己去加载

2.为什么要用这个模式?

目的是:防止内存中出现多份同样的字节码

简述 JVM 的内存模型 JVM 内存是如何对应到操作系统内存的?

在这里插入图片描述

3.java虚拟机的各个部分也存放什么东西

1)程序计数器:几乎不占内存,用于取下一条指令
2)堆:所有通过new创建的对象的内存都在堆中分配,堆被划分为新生代和老年代。
新生代有进一步划分为Eden和Survivor区,最后Survivor由FromSpace和ToSpace组成。
新建的对象都使用新生代分配内存,Eden空间不足,会把存活对象移植到Survivor中。
3)java虚拟机栈:每个线程执行每个方法的时候都会在栈中申请一个栈帧,每个栈帧
包括局部变量区和操作数栈,用于存放此次方法调用过程中的临时变量、参数和中间结果
4)本地方法栈:用于支持native方法的执行,存储每个native方法调用的状态
5)方法区:存放要加载的类信息、静态变量、常量、属性和方法信息

java四大引用类型

1.强引用 :强引用关联的对象,永远不会被GC回收。
2.软引用:系统在发生内存溢出前会对这类引用的对象进行回收。(缓存大对象,如图片)
3.弱引用:下一次GC的时候一定会被回收。(堆外内存,如NIO、ThreadLocal(内存泄漏))
4.虚引用:当发生GC的时候,虚引用也会被回收

JVM:运行时数据区、垃圾回收算法、垃圾回收器(CMS、G1)、常用配置参数、线上问题定位及解决。

Java 中垃圾回收机制中如何判断对象需要回收?常见的 GC 回收算法有哪些?

哪些垃圾可以被回收
1.引用计数法:通过引用计数来判断一个对象是否可以被回收,如果一个对象没有任何引用与之关联,则说明该对象基本不太可能在其他地方被使用到,那么这个对象就成为可被回收的对象了。

2.可达性分析法(GC Roots):通过一系列的“GC Roots”对象作为起点进行搜索,如果在“GC Roots”和一个对象之间没有可达路径,则称该对象是不可达的,不过要注意的是被判定为不可达的对象不一定就会成为可回收对象。被判定为不可达的对象要成为可回收对象必须至少经历两次标记过程

垃圾收集算法

1.标记-清除算法
2.复制算法
3.标记-整理算法
4.分代收集算法:新生代采取复制算法,老年代采取标记-整理算法

并行和并发的区别?

并发:一个处理器可以同时处理多个任务。这是逻辑上的同时发生。
并行:多个处理器同时处理多个不同的任务。这是物理上的同时发生。
有一个清晰地比喻:
并发:一个人同时吃三个苹果。并行:三个人同时吃三个苹果。

垃圾收集器(垃圾回收器)

1.Serial/Serial Old串行收集器 是最基本最古老的收集器,它是一个单线程收集器,并且在它进行垃圾收集时,必须暂停所有用户线程。Serial收集器是针对新生代的收集器,采用的是Copying算法,Serial Old收集器是针对老年代的收集器,采用的是Mark-Compact算法。它的优点是实现简单高效,但是缺点是会给用户带来停顿。

2.ParNew串行收集器 是Serial收集器的多线程版本,使用多个线程进行垃圾收集。

3.Parallel Scavenge并行收集器 是一个新生代的多线程收集器(并行收集器),它在回收期间不需要暂停其他用户线程,其采用的是Copying算法,该收集器与前两个收集器有所不同,它主要是为了达到一个可控的吞吐量。

4.Parallel Old并行收集器 是Parallel Scavenge收集器的老年代版本(并行收集器),使用多线程和Mark-Compact算法。

5.CMS(Current Mark Sweep)并发收集器 是一种以获取最短回收停顿时间为目标的收集器,它是一种并发收集器,采用的是标记清理算法。JDK1.7之前的默认垃圾回收算法,并发收集,停顿小。

优点:
  并发,低停顿
缺点:
  1、无法处理浮动垃圾:在最后一步并发清理过程中,用户县城执行也会产生垃圾,但是这部分垃圾是在标记之后,所以只有等到下一次gc的时候清理掉,这部分垃圾叫浮动垃圾
  2、CMS使用“标记-清理”法会产生大量的空间碎片,当碎片过多,将会给大对象空间的分配带来很大的麻烦,往往会出现老年代还有很大的空间但无法找到足够大的连续空间来分配当前对象,不得不提前触发一次FullGC
(导致频繁触发FullGC)

过程:
  1、初始标记:独占PUC,仅标记GCroots能直接关联的对象
  2、并发标记:可以和用户线程并行执行,标记所有可达对象
  3、重新标记:独占CPU(STW),对并发标记阶段用户线程运行产生的垃圾对象进行标记修正
  4、并发清理:可以和用户线程并行执行,清理垃圾
  在这里插入图片描述

CMS 出现FullGC的原因:
  1、年轻带晋升到老年带没有足够的连续空间,很有可能是内存碎片导致的
  2、在并发过程中JVM觉得在并发过程结束之前堆就会满,需要提前触发FullGC
  
6.G1并发收集器 它能充分利用多CPU、多核环境。是一款并行与并发收集器,并且它能建立可预测的停顿时间模型。G1算法JDK1.9之后默认回收算法,特点是保持高回收率的同时,减少停顿。是基于标记整理的垃圾回收器。

特点:
  1、并行于并发:G1能充分利用CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短stop-The-World停顿时间。部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让java程序继续执行。
  2、分代收集:分代概念在G1中依然得以保留。虽然G1可以不需要其它收集器配合就能独立管理整个GC堆,但它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效果。也就是说G1可以自己管理新生代和老年代了。
  3、空间整合:由于G1使用了独立区域(Region)概念,G1从整体来看是基于“标记-整理”算法实现收集,从局部(两个Region)上来看是基于“复制”算法实现的,但无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片。
  4、可预测的停顿:这是G1相对于CMS的另一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用这明确指定一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。

G1收集器的运作大致可划分为以下几个步骤:
1、初始标记(Initial Making)
2、并发标记(Concurrent Marking)
3、最终标记(Final Marking)
4、筛选回收(Live Data Counting and Evacuation)

在这里插入图片描述

Eden用于分配新的内存空间,当第一轮回收后剩下的对象被放到Survivor1,此时年龄为1。第二次剩下的对象则年龄为1,第一次的年龄为2,他们被复制到Survivor2。当再次回收的时候,又由Survivor2转换到Survivor1容器,他们两个反复替换。当对象的年龄成长到8以后,被移动到老年代。永久代又叫方法区。

Minor GC 年轻代的回收
Major GC/Full GC年老代的回收

jvm提供的年轻代回收算法属于复制算法,CMS、G1,ZGC属于标记清除算法。

看内存工具:jmap、jconsolne

线上问题定位及解决

1.先用top命令,找到cpu占用最高的进程 PID
2.再用ps -mp pid -o THREAD,tid,time 查询进程中,那个线程的cpu占用率高 记住TID
3.jstack TID >> xxx.log 打印出该进程下线程日志
4.sz xxx.log 将日志文件下载到本地
5.将查找到的 线程占用最高的 tid 上上上图中 29108 转成16进制 — 71b4
6.打开下载好的 xxx.log 通过 查找方式 找到 对应线程 进行排查

JVM 是怎么去调优的?简述过程和调优的结果

对JVM内存的系统级的调优主要的目的是减少GC的频率和Full GC的次数。
1.监控GC的状态
2.生成堆的dump文件(jmap)
3.分析dump文件
4.分析结果,判断是否需要优化
5.调整GC类型和内存分配
6.不断的分析和调整

当我们遇到了OutOfMemory等内存异常,GC停顿时间过长,Full GC次数频繁,或系统吞吐量与响应性能不高时,首先我们查找代码有没有问题,进行代码优化,如果代码没问题,我们就要考虑进行JVM调优.
首先我们要打印出GC日志,还有使用分析各种性能参数,判断是否需要优化,确定瓶颈问题.
然后我们需要确定JVM调优量化的目标;
然后我们根据他的历史参数来调整JVM的参数;现在一台机器上进行测试;
然后通过不断地分析和调整,直到找到合适的JVM参数配置;
最后我们将参数应用到所有的服务器,并进行后续的跟踪.

//一般我们在谈到JVM调优时会涉及三个指标: 内存占用量,系统延迟和系统吞吐量.
1,内存占用: 系统运行时,java虚拟机需要的内存.
2,延迟: 系统运行过程中由于垃圾收集引起的暂停时间.
3,吞吐量: 单位时间内完成的任务数量.
jvm调优的目的是追求更低的系统延迟和更高的系统吞吐量,衡量锡系统在稳定状态下所需要的最低内存占用量.

常用配置参数

JVM的堆的内存, 是通过下面面两个参数控制的
-Xms 初始堆大小 -Xms512M
-Xmx 最大堆大小 -Xms2G
-Xss:每个线程的堆栈大小 -Xmn512M
-Xmn:年轻代大小 -Xss512k
-XX:newSize -XX:MaxNewSize 新生代内存的初始大小 、最大大小

整个JVM内存大小=年轻代大小 + 年老代大小 + 持久代大小

1.针对JVM堆的设置,一般可以通过-Xms -Xmx限定其最小、最大值,为了防止垃圾收集器在最小、最大之间收缩堆而产生额外的时间,通常把最大、最小设置为相同的值;

2.年轻代和年老代将根据默认的比例(1:2)分配堆内存, 可以通过调整二者之间的比率NewRadio来调整二者之间的大小,也可以针对回收代。

年轻代大小选择:
1.响应时间优先的应用:尽可能设大,
2.吞吐量优先的应用:尽可能的设置大

年老代大小选择:
1.响应时间优先的应用:年老代使用并发收集器,所以其大小需要小心设置,一般要考虑并发会话率和会话持续时间等一些参数。如果堆设置小了,可以会造成内存碎片、高回收频率以及应用暂停而使用传统的标记清除方式;如果堆大了,则需要较长的收集时间。最优化的方案,一般需要参考以下数据获得:
1.并发垃圾收集信息
2.持久代并发收集次数
3.传统GC信息
4.花在年轻代和年老代回收上的时间比例
减少年轻代和年老代花费的时间,一般会提高应用的效率

简述 Java 的逃逸分析机制

逃逸分析的基本行为就是分析对象动态作用域:当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他地方中,称为方法逃逸。
逃逸分析:是一种可以有效减少java 程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。
方法逃逸:例如作为调用参数传递到其他方法中。
线程逃逸:有可能被外部线程访问到,譬如赋值给类变量或可以在其他线程中访问的实例变量

Java对象分配的过程

  1. 编译器通过逃逸分析,确定对象是在栈上分配还是在堆上分配。如果是在堆上分配,则进入2.
  2. 如果tlab_top + size <= tlab_end,则在在TLAB上直接分配对象并增加tlab_top 的值,如果现有的TLAB不足以存放当前对象则3.
  3. 重新申请一个TLAB,并再次尝试存放当前对象。如果放不下,则4。
  4. 在Eden区加锁(这个区是多线程共享的),如果eden_top + size <= eden_end则将对象存放在Eden区,增加eden_top 的值,如果Eden区不足以存放,则5。
  5. 执行一次Young GC(minor collection)
  6. 经过Young GC之后,如果Eden区任然不足以存放当前对象,则直接分配到老年代。

2. java.util.concurrnet 并发编程

并发编程
Synchroized,volatile,TheadPoolExecutor,Future,FutureTask,CountDownLatch、CyclicBarrier、Semaphore

HashTable在不指定容量的情况下的默认容量为11,而HashMap为16,Hashtable不要求底层数组的容量一定要为2的整数次幂,而HashMap则要求一定为2的整数次幂。
Hashtable扩容时,将容量变为原来的2倍加1,而HashMap扩容时,将容量变为原来的2倍。

hashmap在jdk7和jdk8的不同:

1.hashmap在jdk7是链表+数组,但是在jdk8超过链表长度超过阈值(长度超过8个,容量超过64)会变成 红黑树
2.新节点插入链表的顺序不同,jdk7是头插法,jdk8是尾插法
3.hash算法简化(hashcode 与 (长度-1 ))
4.resize逻辑修改 二字节的高位运算,大于0 是要移动位置(jdk7扩容时候容易出现死锁,jdk8不会)

ConcurrentHashmap在JDK1.7和1.8的版本改动比较大,

1.7使用Segment+HashEntry(16)分段锁的方式实现,
1.8则抛弃了Segment,改为使用CAS+synchronized+Node实现,同样也加入了红黑树,避免链表过长导致性能的问题。

集合类中的 List 和 Map 的线程安全版本是什么,如何保证线程安全的?

CopyOnWriteArrayList:重入锁
Collections.synchronizedList();加了synchronized

ConcurrentHashMap :底层是基于CAS+synchronized+Node实现,同样也加入了红黑树

HashMap 与 ConcurrentHashMap 的实现原理是怎样的?ConcurrentHashMap 是如何保证线程安全的?

HashMap :数组+链表+红黑树
ConcurrentHashMap :CAS+synchronized+Node实现,同样也加入了红黑树
ConcurrentHashMap 在1.7是用了分段锁,在1.8是通过synchronized、cas、头插法保证线程安全

1.JDK1.7版本的HashMap在并发情况下为什么会出现链表循环?

答:因为JDK1.7版本下,HashMap采用链表法来处理Hash冲突,结点的插入方式是采取头插法(也就是后面的结点插入到链表头部),
当进行resize(扩容)时,需要将原哈希桶中的中的数据搬到新的哈希桶数组中,因为是头插法,所以会出现相同链表上结点位置
互换的情况,头部变成尾部,尾部变成头部。在并发场景下,如果线程A搬迁key1指向ke2,而线程B搬迁后key2指向key1,那么就会出现链表循环。
JDK1.8是采用的尾插法,尾结点的next永远为null,可以保证不会出现链表循环

2. JDK1.8版本下HashMap为啥会有线程安全的问题?

JDK1.8版本下hashMap进行put操作时的流程是:先计算hashCode找到桶,然后遍历桶内的链表找到插入位置插入。即先查后写。没有进行加锁,并发情况下会覆盖数据,导致数据丢失。

3.比如预知可能会插入的元素有5个,应该设置的HashMap的初始容量多大合适呢?PS:装载因子不变

答:按照计算公式 thredload=initCapacity*loadFactor 计算 initCapacity=5/0.75=6.67,那么设置的initCapacity大小为7比较合适,保证不会扩容。
但是,我们设置初始大小为7后,容器的初始大小一定为7么,答案是否定的,因为,在JDK1.8中,容器的大小始终是2次幂,就是说大小只会是2的倍数。

synchronized原理

synchronized是java提供的原子性内置锁,这种内置的并且使用者看不到的锁也被称为监视器锁(monitor),依赖操作系统底层互斥锁实现,他的作用主要就是实现原子性操作和解决共享变量的内存可见性问题。
monitorenter、monitorexit

锁的优化机制

在这里插入图片描述

优化机制包括自适应锁、自旋锁、锁消除、锁粗化、轻量级锁和偏向锁。

自旋锁:由于大部分时候,锁被占用的时间很短,共享变量的锁定时间也很短,所有没有必要挂起线程,用户态和内核态的来回上下文切换严重影响性能。自旋的概念就是让线程执行一个忙循环,可以理解为就是啥也不干,防止从用户态转入内核态,自旋锁可以通过设置-XX:+UseSpining来开启,自旋的默认次数是10次,可以使用-XX:PreBlockSpin设置

自适应锁:自适应锁就是自适应的自旋锁,自旋的时间不是固定时间,而是由前一次在同一个锁上的自旋时间和锁的持有者状态来决定。

锁消除:锁消除指的是JVM检测到一些同步的代码块,完全不存在数据竞争的场景,也就是不需要加锁,就会进行锁消除。

锁粗化:锁粗化指的是有很多操作都是对同一个对象进行加锁,就会把锁的同步范围扩展到整个操作序列之外。

锁的状态

轻量级锁的底层是CAS自旋, 重量级锁的底层是操作系统互斥

无锁——开始有竞争——》偏向锁(第一次用)——多个线程竞争——》轻量级锁(自旋锁CAS)——竞争加剧——》重量级锁(自旋超过10次)

无锁 VS 偏向锁 VS 轻量级锁 VS 重量级锁,这四种锁是指锁的状态,专门针对synchronized的,
锁状态只能升级不能降级。
synchronized是悲观锁,在操作同步资源之前需要给同步资源先加锁,这把锁就是存在Java对象头里的
对象头主要包括两部分数据:Mark Word(标记字段)、Klass Pointer(类型指针)
Mark Word:默认存储对象的HashCode,分代年龄和锁标志位信息。
Klass Point:对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
synchronized通过底层的操作系统的Mutex Lock(互斥锁)来实现的线程同步。

无锁:无锁没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功,CAS原理及应用即是无锁的实现

偏向锁:偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价。当线程访问同步块获取锁时,会在对象头和栈帧中的锁记录里存储偏向锁的线程ID,之后
这个线程再次进入同步块时都不需要CAS来加锁和解锁了,偏向锁会永远偏向第一个获得锁的线程,如果后续没有其他线程获得过这个锁,持有锁的线程就永远不需要进行同步,反之,当有其他线程竞争偏向锁时,持有偏向锁的线程就会释放偏向锁。

轻量级锁:是指当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。JVM的对象的对象头中包含有一些锁的标志位,代码进入同步块的时候,JVM将会使用CAS方式来尝试获取锁,如果更新成功则会把对象头中的状态位标记为轻量级锁,如果更新失败,当前线程就尝试自旋来获得锁。

重量级锁: 升级为重量级锁时,锁标志的状态值变为“10”,此时Mark Word中存储的是指向重量级锁的指针,此时等待锁的线程都会进入阻塞状态。

synchronized 关键字底层是如何实现的?它与 Lock 相比优缺点分别是什么?

synchronized 通过竞争 monitor 实现
1.synchronized 用完不需要手动解锁,Lock需要unlock
2.synchronized 、lock默认都是非公平锁,lock可以手动改成公平锁
3.lock可以指定唤醒哪些线程,synchronized 只能随机或者全部唤醒线程

ReentrantLock原理?他和synchronized有什么区别?

1.等待可中断,当持有锁的线程长时间不释放锁的时候,等待中的线程可以选择放弃等待
2.公平锁:synchronized和ReentrantLock默认都是非公平锁,但是ReentrantLock可以通过构改变
3.绑定多个条件:ReentrantLock可以同时绑定多个Condition条件对象
4、lock是个接口,synchronized是关键字

ReentrantLock基于AQS(AbstractQueuedSynchronizer 抽象队列同步器)实现。

AQS: 队列同步器是用来构建锁或者其他同步组件的基础框架。AQS的主要作用是为Java中的并发同步组件提供统一的底层支持,例如ReentrantLock,CountdowLatch就是基于AQS实现的,用法是通过继承AQS实现其模版方法,然后将子类作为同步组件的内部类。

AQS内部维护一个state状态位,尝试加锁的时候通过CAS(CompareAndSwap)修改值,如果成功
设置为1,并且把当前线程ID赋值,则代表加锁成功,一旦获取到锁,其他的线程将会被阻塞进入
阻塞队列自旋,获得锁的线程释放锁的时候将会唤醒阻塞队列中的线程,释放锁的时候则会把state
重新置为0,同时当前线程ID置为空。

AQS提供了一种实现阻塞锁和一系列依赖FIFO等待队列的同步器的框架
AQS同时提供了独占模式和共享模式两种不同的同步逻辑。

state的访问方式有三种:
getState()
setState()
compareAndSetState()

AQS:
acquire(int arg)
tryAcquire(int arg) (独占)
tryRelease(int arg)(独占)
tryAcquireShared(共享)
tryReleaseShared(int arg) (共享)

重入锁(ReentrantLock)都可重入性基于AQS(独占)实现:公平锁、非公平锁,state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态的。
Semaphore/CountDownLatch 基于AQS(共享)实现,任务分为N个子线程去执行,state也初始化为N(注意N要与线程个数一致)。这N个子线程是并行执行的,每个子线程执行完后countDown()一次,state会CAS减1。等到所有子线程都执行完后(即state=0),会unpark()主调用线程,然后主调用线程就会从await()函数返回,继续后余动作。

CountDownLatch与CyclicBarrier区别

CountDownLatch 基于 AQS 的共享模式的使用,而 CyclicBarrier 基于 Condition 来实现的。
1.CountDownLatch一般用于一个或多个线程,等待其他线程执行完任务后,再才执行,CyclicBarrier一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行
2.CountDownLatch是减计数,计数减为0后不能重用;而CyclicBarrier是加计数,可置0后复用。

volatile 关键字解决了什么问题,它的实现原理是什么?

volatile本质是在告诉jvm当前变量在工作内存中的值是不确定的,需要从主存中读取;
1.保证线程可见性
2.阻止指令重排序(内存屏障)
volatile只能保证变量的可见性、有序性,但是不能保证原子性。

synchronized 和 volatile 区别

1.volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;
2. synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
3.volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的
4.volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性
5.volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
6.volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化

valatile: 1.线程可见性 2.阻止指令重排序
valatile内存屏障
1.StoreStore屏障,保证上面的普通写不和volatile写发生重排序
2.StoreLoad屏障,保证volatile写与后面可能的volatile读写不发生重排序
3.LoadLoad屏障,禁止volatile读与后面的普通读重排序
4.LoadStore屏障,禁止volatile读和后面的普通写重排序
在这里插入图片描述

那么说说你对JMM内存模型的理解?为什么需要JMM?

JMM内存模型是对多线程操作下的一系列规范约束,通过JMM我们才屏蔽了不同硬件和操作系统内存的访问差异,这样保证了Java程序在不同的平台下达到一致的内存访问效果,同时也是保证在高效并发的时候程序能够正确执行。
原子性、可见性、有序性、happen-before规则

简述 Java 的 happen before 原则

如果两个操作之间具有happen-before关系,那么前一个操作的结果就会对后面的一个操作可见。

1.程序的顺序性规则
2.volatile规则
3.传递性规则
4.锁规则
5.线程启动规则
6.线程中断规则
7.线程终结规则

ThreadLocal 实现原理是什么

ThreadLocal可以理解为线程本地变量,他会在每个线程都创建一个副本,那么在线程之间访问内部副本变量就行了,做到了线程之间互相隔离,相比于synchronized的做法是用空间来换时间。
底层是ThreadLocalMap,key是当前线程,val是设置的值,
ThreadLocal 是弱引用,每次gc的时候会被清理,val没有删掉会导致内存泄漏

3.java线程、多线程

进程和线程

进程是资源分配和调度的基本单位
线程是比进程更小的能独立运行的基本单位,他是进程的一个实体

java线程创建方式

1.继承thread类
2.实现runable接口,重写run方法,不带返回值
3.实现callable接口,重写call方法,带返回值

Java 线程和操作系统的线程是怎么对应的?Java线程是怎样进行调度的

Java的多线程本质是直接调用操作系统的多线程函数实现的

每个继承java.lang.Thread的类,调用start方法之后,都调用start0()的native方法;
start0()的native方法在openjdk里调用的是JVM_StartThread;
JVM_StartThread最终调用的是操作系统的pthread_create()函数,有四个参数,我们写的run方法就是该函数的第三个参数.

Java线程的调度策略决定上层多线程运行机制,JVM的线程调度器实现了抢占式调度,每条线程执行的时间由它分配管理,它将按照线程优先级的建议对线程执行的时间进行分配,优先级越高,可能得到CPU的时间则越长

守护线程和非守护线程

守护线程:只要当前JVM实例中尚存在任何一个非守护线程没有结束,守护线程就全部工作;只有当最后一个非守护线程结束时,守护线程随着JVM一同结束工作。,守护线程最典型的应用就是 GC (垃圾回收器)
非守护线程:用户线程

线程池原理知道吗?

ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit, BlockingQueue workQueue);
corePoolSize:核心池的大小
maximumPoolSize:线程池最大线程数
keepAliveTime:表示线程没有任务执行时最多保持多久时间会终止
unit:参数keepAliveTime的时间单位
workQueue:一个阻塞队列,用来存储等待执行的任务。一般来说,这里的阻塞队列有以下几种选择:
1.ArrayBlockingQueue:是一个有界缓存等待队列,可以指定缓存队列的大小。当ArrayBlockingQueue已满时,加入ArrayBlockingQueue失败,会开启新的线程去执行,当线程数已经达到最大的maximumPoolSizes时,再有新的元素尝试加入ArrayBlockingQueue时会报错。
2.LinkedBlockingQueue:一个无界缓存等待队列。当前执行的线程数量达到corePoolSize的数量时,剩余的元素会在阻塞队列里等待。
3.SynchronousQueue:没有容量,是无缓冲等待队列,是一个不存储元素的阻塞队列,会直接将任务交给消费者,必须等队列中的添加元素被消费后才能继续添加新的元素。
在这里插入图片描述

java线程池中的当前线程数 核心线程数 最大线程数 阻塞队列 拒绝策略 的基本使用
当新提交一个任务时:
(1)如果poolSize<corePoolSize,新增加一个线程处理新的任务。
(2)如果poolSize=corePoolSize,新任务会被放入阻塞队列等待。
(3)如果阻塞队列的容量达到上限,且这时poolSize<maximumPoolSize,新增线程来处理任务。
(4)如果阻塞队列满了,且poolSize=maximumPoolSize,那么线程池已经达到极限,会根据拒绝策略来处理该任务, 默认的处理方式是直接抛异常

常用的线程池:
public static ExecutorService newSingleThreadExecutor() 创建仅有一个线程工作的线程池,相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么将创建有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
public static ExecutorService newCachedThreadPool() 创建一个缓存线程池,如果线程池的大小超过了任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又动态添加新线程来处理任务。此线程池没有对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)所能够创建的最大线程大小。
public static ExecutorService newFixedThreadPool(int nThreads) 创建指定大小的线程池。每次提交一个任务就创建一个线程,直到线程数量达到线程池的最大值。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) 类似于newCachedThreadPool,创建一个支持定时以及周期性执行任务的缓存线程池。
public static ScheduledExecutorService newSingleThreadScheduledExecutor() 类似于newSingleThreadExecutor,创建一个支持定时以及周期性执行任务的单线程的线程池

拒绝策略
1.ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出异常。
2.ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
3.ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
4.ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务

4. 设计模式(7大原则,23种设计模式,饱汉、饿汉单例模式)

7大原则

1.单一职责原则
2.接口隔离原则
3.依赖倒转(倒置)原则
4.里氏替换原则
5.开闭原则
6.迪米特法则
7.合成复用原则

饱汉、饿汉单例模式

懒汉模式 不安全:
比较懒,在类加载时,不创建实例,因此类加载速度快,但运行时获取对象的速度慢

public class LazySingleDemo{
    private static LazySingleDemoinstance;
    private LazySingleDemo(){}
    public static LazySingleDemo getInstance(){
        if (instance == null) {
            instance = new LazySingleDemo();
        }
        return instance;
    }
}  

饿汉模式 安全:在类加载时就完成了初始化,所以类加载较慢,但获取对象的速度快

public class HungrySingleDemo{
    private static HungrySingleDemo instance = new SingletonDemo3();
    private SingletonDemo3(){}
    public static HungrySingleDemo getInstance(){
        return instance;
    }
}

双重校验锁 安全:

public class SingletonDemo7 {
    private volatile static SingletonDemo7 singletonDemo7;
    private SingletonDemo7(){}
    public static SingletonDemo7 getSingletonDemo7(){
        if (singletonDemo7 == null) {
            synchronized (SingletonDemo7.class) {
                if (singletonDemo7 == null) {
                    singletonDemo7 = new SingletonDemo7();
                }
            }
        }
        return singletonDemo7;
    }
}

5. mysql (sql优化, 索引原理,B+树,b树区别,

ACID, undo log, redo log, 间隙锁,临键锁

Mysql的四大特性

A: 原子性 (Atomicity)原子性是指一个事务是一个不可分割的工作单位,其中的操作要么都做,要么都不做。
C:一致性 (Consistency)
I:隔离性 (Isolation) 隔离性是指多个事务并发执行的时候,事务内部的操作与其他事务是隔离的,并发执行的各个事务之间不能互相干扰。
D:持久性 (Durability) 持久性是指事务一旦提交,它对数据库的改变就应该是永久性的。接下来的其他操作或故障不应该对其有任何影响。

Mysql怎么保证原子性的?怎么回滚的?

利用Innodb的undo log。
undo log名为回滚日志,是实现原子性的关键,当事务回滚时能够撤销所有已经成功执行的sql语句,他需要记录你要回滚的相应日志信息。
(1)当你delete一条数据的时候,就需要记录这条数据的信息,回滚的时候,insert这条旧数据
(2)当你update一条数据的时候,就需要记录之前的旧值,回滚的时候,根据旧值执行update操作
(3)当年insert一条数据的时候,就需要这条记录的主键,回滚的时候,根据主键执行delete操作
  undo log记录了这些回滚需要的信息,当事务执行失败或调用了rollback,导致事务需要回滚,便可以利用undo log中的信息将数据回滚到修改之前的样子。

Mysql怎么保证持久性的?

利用Innodb的redo log(重做日志)。
正如之前说的,Mysql是先把磁盘上的数据加载到内存中,在内存中对数据进行修改,再刷回磁盘上。如果此时突然宕机,内存中的数据就会丢失。
采用redo log解决上面的问题。当做数据修改的时候,不仅在内存中操作,还会在redo log中记录这次操作。当事务提交的时候,会将redo log日志进行刷盘(redo log一部分在内存中,一部分在磁盘上)。当数据库宕机重启的时候,会将redo log中的内容恢复到数据库中,再根据undo log和binlog内容决定回滚数据还是提交数据。

redo log与binlog

(1)作用不同:redo log是用于crash recovery的,保证MySQL宕机也不会影响持久性;binlog是用于point-in-time recovery的,保证服务器可以基于时间点恢复数据,此外binlog还用于主从复制。
(2)层次不同:redo log是InnoDB存储引擎实现的,而binlog是MySQL的服务器层(可以参考文章前面对MySQL逻辑架构的介绍)实现的,同时支持InnoDB和其他存储引擎。
(3)内容不同:redo log是物理日志,内容基于磁盘的Page;binlog的内容是二进制的,根据binlog_format参数的不同,可能基于sql语句、基于数据本身或者二者的混合。
(4)写入时机不同:binlog在事务提交时写入;redo log的写入时机相对多元:

Mysql怎么保证隔离性的?

利用的是锁和MVCC(多版本并发控制) 机制。
(一个事务)写操作对(另一个事务)写操作的影响:锁机制保证隔离性
(一个事务)写操作对(另一个事务)读操作的影响:MVCC保证隔离性
MVCC,即多版本并发控制(Multi Version Concurrency Control),一个行记录数据有多个版本对快照数据,这些快照数据在undo log中。 如果一个事务读取的行正在做DELELE或者UPDATE操作,读取操作不会等行上的锁释放,而是读取该行的快照版本。

在事务隔离级别为读已提交(Read Commited)时,一个事务能够读到另一个事务已经提交的数据,是不满足隔离性的。但是当事务隔离级别为可重复读(Repeateable Read)中,是满足隔离性的。

Mysql的事务,隔离级别,幻读是怎么解决的?

脏读、不可重复读和幻读

  1. 脏读:当前事务(A)中可以读到其他事务(B)未提交的数据(脏数据),这种现象是脏读。
  2. 不可重复读:在事务A中先后两次读取同一个数据,两次读取的结果不一样,这种现象称为不可重复读。脏读与不可重复读的区别在于:前者读到的是其他事务未提交的数据,后者读到的是其他事务已提交的数据。(MVVC )
  3. 幻读:在事务A中按照某个条件先后两次查询数据库,两次查询结果的条数不同,这种现象称为幻读。不可重复读与幻读的区别可以通俗的理解为:前者是数据变了,后者是数据的行数变了。(间隙锁)

为了解决不可重复读,或者为了实现可重复读,MySQL 采用了 MVVC (多版本并发控制) 的方式。

事务隔离级别

  1. 读未提交(READ UNCOMMITTED)
  2. 读提交 (READ COMMITTED)
  3. 可重复读 (REPEATABLE READ) mysql默认
  4. 串行化 (SERIALIZABLE)在这里插入图片描述

2. Mysql中锁的种类,行锁有哪些,分别怎么实现的?

行锁(记录锁、间隙锁、临键锁 )、表锁、叶锁

记录锁:顾名思义,记录锁就是为某行记录加锁,它封锁该行的索引记录:在通过 主键索引 与 唯一索引 对数据行进行 UPDATE 操作时,也会对该行数据加记录锁:

间隙锁:**间隙锁基于非唯一索引,它锁定一段范围内的索引记录。**间隙锁基于下面将会提到的Next-Key Locking 算法,请务必牢记:使用间隙锁锁住的是一个区间,而不仅仅是这个区间中的每一条数据。

临键锁:理解为一种特殊的间隙锁,通过临建锁可以解决幻读的问题。 每个数据行上的非唯一索引列上都会存在一把临键锁,当某个事务持有该数据行的临键锁时,会锁住一段左开右闭区间的数据。需要强调的一点是,InnoDB 中行级锁是基于索引实现的,临键锁只与非唯一索引列有关,在唯一索引列(包括主键列)上不存在临键锁。
在根据非唯一索引 对记录行进行 UPDATE \ FOR UPDATE \ LOCK IN SHARE MODE 操作时,InnoDB 会获取该记录行的 临键锁 ,并同时获取该记录行下一个区间的间隙锁。

总结:

  1. InnoDB 中的行锁的实现依赖于索引,一旦某个加锁操作没有使用到索引,那么该锁就会退化为表锁。
  2. 记录锁存在于包括主键索引在内的唯一索引中,锁定单条索引记录。
  3. 间隙锁存在于非唯一索引中,锁定开区间范围内的一段间隔,它是基于临键锁实现的。
  4. 临键锁存在于非唯一索引中,该类型的每条记录的索引上都存在这种锁,它是一种特殊的间隙锁,锁定一段左开右闭的索引区间。

索引是一种高效获取数据的存储结构,例:hash、 二叉、 红黑。
mysql索引是基于b+ tree

LBCC: 既然要保证前后两次读取数据一致,那么我读取数据的时候,锁定我要 操作的数据,不允许其他的事务修改就行了。这种方案我们叫做基于锁的并发控制 Lock Based Concurrency Control(LBCC)。

MVCC: 如果要让一个事务前后两次读取的数据保持一致, 那么我们可以在修改数据的时候给它建立一个备份或者叫快照,后面再来读取这个快照 就行了。这种方案我们叫做多版本的并发控制 Multi Version Concurrency Control (MVCC)。

B+Tree的特性

(1)单节点能存储更多数据,使得磁盘IO次数更少。
 (2)叶子节点形成有序链表,便于执行范围操作。
 (3)聚集索引中,叶子节点的data直接包含数据;非聚集索引中,叶子节点存储数据地址的指针。

3. Mysql的聚集索引和非聚集索引的区别?

InnoDB使用的是聚簇索引,将主键组织到一棵B+树中,而行数据就储存在叶子节点上
MyISM使用的是非聚簇索引,b+树最底层存放一个指向真正的表数据的地址

MyISAM引擎和InnoDB引擎的区别

MyISAM:支持全文索引;不支持事务;它是表级锁;会保存表的具体行数.适合查询、插入
InnoDB:5.6以后才有全文索引**;支持事务、外键、行锁**;不会保存表的具体行数.适合修改、删除
一般:不用事务的时候,count计算多的时候适合myisam引擎。对可靠性要求高就是用innodb引擎。推荐用InnoDB引擎.

4. B+树的高度

一个千万量级,且存储引擎是MyISAM或者InnoDB的表,其索引树的高度在3~5之间。

加了索引之后能够大幅度的提高查询速度,但是索引也不是越多越好,一方面它会占用存储空间,另一方面它会使得写操作变得很慢。通常我们对查询次数比较频繁,值比较多的列才建索引。

5. B+树的结构

  1. 中间节点每个元素不保存数据,只用来索引,所有数据都保存在叶子节点。
  2. 所有的叶子结点中包含了全部元素的信息,及指向含这些元素记录的指针,且叶子结点本身依关键字的大小自小而大顺序链接。
  3. 所有的中间节点元素都同时存在于子节点,在子节点元素中是最大(或最小)元素。

最左匹配原则

以最左边的为起点任何连续的索引都能匹配上。同时遇到范围查询(>、<、between、like)就会停止匹配。
最左匹配原则都是针对联合索引来说的,在a值相等的情况下,b值又是按顺序排列的,但是这种顺序是相对的,遇上范围查询就会停止,剩下的字段都无法使用索引。

4.如果不想使用消息队列怎么增加mysql的性能提升?

5.项目中的分库分表的实现原理?

分库方式:垂直拆分、水平切分 、
分库分表的策略: HASH取模、范围分片、地理位置分片、时间分片
分库分表中间件:Mycat

取模 1、中间变量 = user_id%(库数量*每个库的表数量);
2、库序号 = 取整(中间变量/每个库的表数量);
3、表序号 = 中间变量%每个库的表数量;
例如:数据库有256 个,每一个库中有1024个数据表,用户的user_id=262145,按照上述的路由策略,可得:

1、中间变量 = 262145%(256*1024)= 1;
2、库序号 = 取整(1/1024)= 0;
3、表序号 = 1%1024 = 1;
这样的话,对于user_id=262145,将被路由到第0个数据库的第1个表中。

读写分离:主从结构
1.当master正常时,读操作发生在两个slave上,写操作发生在master上。
master通过主从配置实时同步数据到两个slave上。

2.当master挂了,为了数据的一致性,和master为主从关系的slave2被停止访问。
这时slave1负责读和写,当master重新恢复后,slave1同步数据给master,然后再次恢复最初读写状态。

分库分表引入问题:

分布式事务问题(2PC\3PC\TCC\基于rabbitmq中间件最终消息一致性)、
跨库join的问题、
横向扩容的问题(预先准备好库)、
结果集合并、排序的问题(存入缓存)、

实现数据库的跨库join联表查询

方案一、字段冗余。也就是说把一部分信息重复存放
方案二、表复制和同步。也就是说把main_db里面的user_info表复制一份到log_db中,然后设置定时任务让这两个表进行同步。
方案三、链接表。就是在log_db里有一个user_info表,但这个表并没有存储数据,而是直接链接到了 main_db里的user_info表。

15.binlog日志

Mysql的binlog日志作用是用来记录mysql内部增删改查等对mysql数据库有更新的内容的记录(对数据库的改动),对数据库的查询select或show等不会被binlog日志记录;主要用于数据库的主从复制以及增量恢复

MySQL binlog的三种工作模式
(1)Row level
 日志中会记录每一行数据被修改的情况,然后在slave端对相同的数据进行修改。
(2)Statement level(默认)
  每一条被修改数据的sql都会记录到master的bin-log中,slave在复制的时候sql进程会解析成和原来master端执行过的相同的sql再次执行
(3)Mixed(混合模式)
  结合了Row level和Statement level的优点

16.MySQL 主从同步延时问题

全同步复制用来解决主从同步延时问题,指的是从库开启多个线程,并行读取 relay log 中不同库的日志,然后并行重放不同库的日志,这是库级别的并行。所有从库都要返回成功
半同步复制用来解决主库数据丢失问题,指的就是主库写入 binlog 日志之后,就会将强制此时立即将数据同步到从库,从库将日志写入自己本地的 relay log 之后,接着会返回一个 ack 给主库,主库接收到至少一个从库的 ack 之后才会认为写操作完成了。只要有一台返回就算成功

Mysql如何保证一致性和持久性

MySQL为了保证ACID中的一致性和持久性,使用了WAL(Write-Ahead Logging,先写日志再写磁盘)。Redo log就是一种WAL的应用。当数据库忽然掉电,再重新启动时,MySQL可以通过Redo log还原数据。也就是说,每次事务提交时,不用同步刷新磁盘数据文件,只需要同步刷新Redo log就足够了。

查询在什么时候不走(预期中的)索引

  1. 模糊查询 %like
  2. 索引列参与计算,使用了函数
  3. 非最左前缀顺序
  4. where对null判断
  5. where不等于
  6. or操作有至少一个字段没有索引
  7. 需要回表的查询结果集过大(超过配置的范围)

6. Redis

类型简介特性场景常用命令
StringRedis的字符串是动态字符串,是可以修改的字符串,它的内部表示就是一个字符数组,内部结构的实现类似于Java的ArrayList它的内部结构是一个带长度信息的字节数组可以包含任何数据,比如jpg图片或者序列化的对象,规定字符串的长度不得超过512MB。1、访问量统计:每次访问博客和文章使用 INCR 命令进行递增 2、将数据以二进制序列化的方式进行存储get、set、incr、decr
HashRedis的字典相当于Java语言里面的HashMap。字典结构内部包含了两个Hashtable,通常情况下只有一个Hashtable是有值的,但是在字典扩容缩容时候,需要重新分配新的Hashtable,然后进行渐进式搬迁,这时候两个Hashtable存储的分别是旧的Hashtable和新的Hashtable;待搬迁结束后,旧的Hashtable被删除,新的Hashtable取而代之适合存储对象,并且可以像数据库中update一个属性一样只修改某一项属性值(Memcached中需要取出整个字符串反序列化成对象修改完再序列化存回去)。存储、读取、修改对象属性,比如:用户(姓名、性别、爱好),文章(标题、发布时间、作者、内容)hget,hset
ListRedis的列表相当于Java的LinkedList。List的结构底层实现不是一个简单的LinkedList,而是快速链表(quicklist)。首先在列表元素较少的情况下,会使用一块连续的内存存储,这个结构是ziplist,即压缩列表。它将所有的元素彼此紧挨着一起存储,分配的是一块连续的内存;当数据量比较多的时候才会改成quicklist。增删快,提供了操作某一段元素的API。普通的链表需要的附加指针空间太大,会浪费空间,加重内存的碎片化。Redis将链表和ziplist结合起来组成了quicklist,也就是将多个ziplist使用双向指针串联起来使用,既满足了快速的插入删除性能,又不会出现太大的空间冗余1、最新消息排行等功能(比如朋友圈的时间线) 2、消息队列 3、抽奖名单Lpush、Lpop、Lrange、Lset
SetRedis的集合相当于Java语言里面的HashSet,内部的键值对是无须的、唯一的。Set的结构底层实现是字典,只不过所有的value都是NULL,其他的特性和字典一摸一样。1、添加、删除、查找的复杂度都是O(1) 2、为集合提供了求交集、并集、差集等操作 当set集合容纳的元素都是整数并且元素个数较少时,Redis会使用intset来存储集合元素。intset是紧凑的数组结构,同时支持16位,32位和64位整数1、共同好友(微信关注) 2、利用唯一性,统计访问网站的所有独立ip3、好友推荐时,根据tag求交集,大于某个阈值就可以推荐sadd 、smembers 、Sinter (交集)、Sdiff (差集)
ZsetRedis有序列表类似于Java的SortedSet和HashMap的结合体,一方面是一个set,保证内部value的唯一性,另一方面可以给每个value赋予一个score,代表这个value的排序权重。它的内部实现是一个Hash字典 + 一个跳表。数据插入集合时,已经进行天然排序Redis的跳表共有64层,能容纳2的64次方个元素。Redis之所以用跳表来实现有序集合1. 插入、删除、查找以及迭代输出有序序列这几个操作,红黑树都能完成,时间复杂度跟跳表是一样的。但是按照区间来查找数据,红黑树的效率就没有跳表高2. 跳表更容易代码实现,比起红黑树来说还是好懂、好写很多,可读性好,不容易出错3. 跳表更加灵活,可以通过改变索引构建策略,有效平衡执行效率和内存消耗1、排行榜,取TopN操作2、带权重的消息 (队列排序,微博排名)zadd、Zcount 、Zcard

多路复用,持久化,主众,哨兵, 集群、数据一致性

redis应用场景

  1. 缓存
  2. 共享Session
  3. 消息队列系统
  4. 分布式锁

单线程的Redis为什么快

  1. 纯内存操作
  2. 单线程操作,避免了频繁的上下文切换
  3. 合理高效的数据结构
  4. 采用了非阻塞I/O多路复用机制(有一个文件描述符同时监听多个文件描述符是否有数据到来)epoll

redis多路复用: I/O 多路复用

Redis 是跑在单线程中的,所有的操作都是按照顺序线性执行的,但是由于读写操作等待用户输入或输出都是阻塞的,所以 I/O 操作在一般情况下往往不能直接返回,这会导致某一文件的 I/O 阻塞导致整个进程无法对其它客户提供服务,而 I/O 多路复用就是为了解决这个问题而出现的。
select, poll, epoll 都是I/O多路复用的具体的实现epoll性能比其他几者要好
IO多路复用模型是建立在内核提供的多路分离函数select基础之上的,使用select函数可以避免同步非阻塞IO模型中轮询等待的问题。
使用select以后最大的优势是用户可以在一个线程内同时处理多个socket的I/O请求用户可以注册多个socket,然后不断地调用select读取被激活的socket,即可达到在同一个线程内同时处理多个I/O请求的目的。
总结
I/O 多路复用模型是利用select、poll、epoll可以同时监察多个流的 I/O 事件的能力,在空闲的时候,会把当前线程阻塞掉,当有一个或多个流有I/O事件时,就从阻塞态中唤醒,于是程序就会轮询一遍所有的流(epoll是只轮询那些真正发出了事件的流),依次顺序的处理就绪的流,这种做法就避免了大量的无用操作。这里“多路”指的是多个网络连接,“复用”指的是复用同一个线程。采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络IO的时间消耗),且Redis在内存中操作数据的速度非常快(内存内的操作不会成为这里的性能瓶颈),主要以上两点造就了Redis具有很高的吞吐量。

持久化(RDB、AOF)

所有的配置都是在redis.conf文件中,里面保存了RDB和AOF两种持久化机制的各种配置
RDB持久化是指在指定的时间间隔内将内存中的数据集快照写入磁盘。也是默认的持久化方式,这种方式是就是将内存中数据以快照的方式写入到二进制文件中,默认的文件名为dump.rdb。

①、优势
(1)RDB文件紧凑,全量备份,非常适合用于进行备份和灾难恢复。
(2)生成RDB文件的时候,redis主进程会fork()一个子进程来处理所有保存工作,主进程不需要进行任何磁盘IO操作。
(3)RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快
②、劣势
RDB快照是一次全量备份,存储的是内存数据的二进制序列化形式,存储上非常紧凑。当进行快照持久化时,会开启一个子进程专门负责快照持久化,子进程会拥有父进程的内存数据,父进程修改内存子进程不会反应出来,所以在快照持久化期间修改的数据不会被保存,可能丢失数据
AOF工作机制,redis将每一个收到的写命令都通过write函数追加到文件中。通俗的理解就是日志记录。
4、优点
(1)AOF可以更好的保护数据不丢失,一般AOF会每隔1秒,通过一个后台线程执行一次fsync操作,最多丢失1秒钟的数据。
(2)AOF日志文件没有任何磁盘寻址的开销,写入性能非常高,文件不容易破损
(3)AOF日志文件即使过大的时候,出现后台重写操作,也不会影响客户端的读写
(4)AOF日志文件的命令通过非常可读的方式进行记录,这个特性非常适合做灾难性的误删除的紧急恢复。比如某人不小心用flushall命令清空了所有数据,只要这个时候后台rewrite还没有发生,那么就可以立即拷贝AOF文件,将最后一条flushall命令给删了,然后再将该AOF文件放回去,就可以通过恢复机制,自动恢复所有数据
5、缺点
(1)对于同一份数据来说,AOF日志文件通常比RDB数据快照文件更大
(2)AOF开启后,支持的写QPS会比RDB支持的写QPS低,因为AOF一般会配置成每秒fsync一次日志文件,当然,每秒一次fsync,性能也还是很高的

redis集群

**Redis有三种集群模式,

1.主从模式**(弊端就是不具备高可用性,当master挂掉以后,Redis将不能再对外提供写入操作)
2.哨兵模式
1)Sentinel(哨兵) 进程是用于监控 Redis 集群中 Master 主服务器工作的状态
2)在 Master 主服务器发生故障的时候,可以实现 Master 和 Slave 服务器的切换,保证系统的高可用(High Availability)

哨兵进程的工作方式

  1. 每个 Sentinel(哨兵)进程以每秒钟一次的频率向整个集群中的 Master 主服务器,Slave 从服务器以及其他 Sentinel(哨兵)进程发送一个 PING 命令。(此处我们还没有讲到集群,下一章节就会讲到,这一点并不影响我们模拟哨兵机制)
  2. 如果一个实例(instance)距离最后一次有效回复 PING 命令的时间超过 down-after-milliseconds 选项所指定的值, 则这个实例会被 Sentinel(哨兵)进程标记为主观下线(SDOWN)。
  3. 如果一个 Master 主服务器被标记为主观下线(SDOWN),则正在监视这个 Master 主服务器的所有 Sentinel(哨兵)进程要以每秒一次的频率确认 Master 主服务器的确进入了主观下线状态。
  4. 当有足够数量的 Sentinel(哨兵)进程(大于等于配置文件指定的值)在指定的时间范围内确认 Master 主服务器进入了主观下线状态(SDOWN), 则 Master 主服务器会被标记为客观下线(ODOWN)。
  5. 在一般情况下, 每个 Sentinel(哨兵)进程会以每 10 秒一次的频率向集群中的所有Master 主服务器、Slave 从服务器发送 INFO 命令。
  6. 当 Master 主服务器被 Sentinel(哨兵)进程标记为客观下线(ODOWN)时,Sentinel(哨兵)进程向下线的 Master 主服务器的所有 Slave 从服务器发送 INFO 命令的频率会从 10 秒一次改为每秒一次。
  7. 若没有足够数量的 Sentinel(哨兵)进程同意 Master 主服务器下线, Master 主服务器的客观下线状态就会被移除。若 Master 主服务器重新向 Sentinel(哨兵)进程发送 PING 命令返回有效回复,Master 主服务器的主观下线状态就会被移除。

3.集群模式

主要为了解决哨兵模式单点读压力的
reids cluster模式最小得6个服务,每个服务之间通过ping-pong机制实现相互感知(注意:此模式没有单独的哨兵监控,集群内部完全自制)。redis集群数据存储类似于hashmap,其将数据划成16484个片区(又叫插槽,每个槽可以放n多个数据),通过对key进行一定的算法与16384取模,得到其在16384中间的一个片区中。如上图姑且认为1-4;2-5;3-6,两两结对,前者为master负责写(其实也具备读能力),后者slave负责读,而且是只读,防止数据出现不一致,同时slave从master同步数据。按照上述配对,1-4主从将只负责0-5500片区的读写;2~5主从只负责5501-11000片区的读写;3-6主从负责11001-16383片区的读写,从而实现了分摊了读写的压力。

redis和mysql数据一致性

写流程:先删除缓存,删除之后再更新DB,再异步将数据刷回缓存。
读流程:先读缓存,如果缓存没读到,则去读DB,之后再异步将数据刷回缓存。

分布式锁一般有三种实现方式:

  1. 数据库乐观锁;
  2. 基于Redis的分布式锁;setnx
  3. 基于ZooKeeper的分布式锁。

**redis缓存雪崩:

** 缓存雪崩是指 设置缓存时采用了相同的过期时间,导致缓存在某一个时刻同时失效, 或者缓存服务器宕机宕机导致缓存全面失效,请求全部转发到了DB层面,DB由于瞬间压力增大而导致崩溃。缓存失效导致的雪崩效应对底层系统的冲击是很大的。
解决方式:
1. 对缓存的访问,如果发现从缓存中取不到值,那么通过加锁(互斥锁)或者队列的方式保证缓存的单进程操作,从而避免失效时并并发请求全部落到底层的存储系统上;但是这种方式会带来性能上的损耗
2.** 将缓存失效的时间分散,降低每一个缓存过期时间的重复率**
3. 如果是因为缓存服务器故障导致的问题,一方面需要保证缓存服务器的高可用、另一方面,应用程序中可以采用多级缓存

缓存穿透: 缓存穿透是指缓存和数据库中都没有的数据
缓存击穿(缓存雪崩): 缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期)

redis缓存穿透:

缓存穿透是指查询一个根本不存在的数据,缓存和数据源都不会命中。出于容错的考虑,如果从数据层查不到数据则不写入缓存,即数据源返回值为 null 时,不缓存 null。缓存穿透问题可能会使后端数据源负载加大,由于很多后端数据源不具备高并发性,甚至可能造成后端数据源宕掉。
解决方式
1. 如果查询数据库也为空,直接设置一个默认值存放到缓存,这样第二次到缓冲中获取就有值了,而不会继续访问数据库,这种办法最简单粗暴。比如,”key” , “&&”。在返回这个&&值的时候,我们的应用就可以认为这是不存在的key,那我们的应用就可以决定是否继续等待继续访问,还是放弃掉这次操作。如果继续等待访问,过一个时间轮询点后,再次请求这个key,如果取到的值不再是&&,则可以认为这时候key有值了,从而避免了透传到数据库,从而把大量的类似请求挡在了缓存之中。
2. 根据缓存数据Key的设计规则,将不符合规则的key进行过滤采用布隆过滤器, 将所有可能存在的数据哈希到一个足够大的BitSet中,不存在的数据将会被拦截掉,从而避免了对底层存储系统的查询压力。

布隆过滤器:
  布隆过滤主要用来判断一个元素是否在集合中存在。Bloom Filter不适合那些“零错误”的应用场合。而在能容忍低错误率的应用场合下,Bloom Filter通过极少的错误换取了存储空间的极大节省。
BitMap就是用一个bit位来标记某个元素所对应的value,而key即是该元素,由于BitMap使用了bit位来存储数据,因此可以大大节省存储空间.

Redis 的过期策略

过期策略:
 Redis采用的是惰性删除和定期删除两种策略。
 惰性删除:不管键是否过期,只有每次取值的时候,才检查是否过期,过期就删除。
 定期删除:每隔一段时间,程序对数据库进行一次(随机)检查,过期的就删除。
 
内存淘汰机制:LRU,内存不足时,淘汰最近最少使用的key。

内存淘汰机制

1)voltile-lru:从已设置过期时间的数据集中挑选最近最少使用的数据淘汰:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的 Key。这种情况一般是把 Redis 既当缓存,又做持久化存储的时候才用。
2)volatile-ttl:从已设置过期时间的数据集中挑选将要过期的数据淘汰:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的 Key 优先移除。
3)volatile-random:从已设置过期时间的数据集任意选择数据淘汰:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个 Key。
4)allkeys-lru:从数据集中 挑选最近最少使用的数据淘汰:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 Key。(LRU推荐使用)
5)allkeys-random:从数据集任意选择数据淘汰:当内存不足以容纳新写入数据时,在键空间中,随机移除某个 Key。
6)no-enviction(驱逐):禁止驱逐数据: 当内存不足以容纳新写入数据时,新写入操作会报错。(Redis 默认策略)

12. redis处理大批量的数据: redis分片

Redis的分片(Sharding或者Partitioning)技术是指将数据分散到多个Redis实例中的方法,分片之后,每个redis拥有一部分原数据集的子集。在数据量非常大时,这种技术能够将数据量分散到若干主机的redis实例上,进而减轻单台redis实例的压力。分片技术能够以更易扩展的方式使用多台计算机的存储能力(这里主要指内存的存储能力)和计算能力:

redis利用集群的方式+槽位完成,分片的数据的定位和管理维护。

1.客户端分片:启动多个redis数据库节点,由客户端决定每个键交由哪个节点存储,下次客户端读取该键时直接到该节点读取。这样可以实现将整个数据分布存储在N个数据库节点中,每个节点只存放总数据量的1/N
2.通过代理服务器实现数据分片
客户端直接与代理联系,代理计算集群节点信息,并把请求发送到对应的集群节点。降低了客户端的复杂度,需要proxy收集集群节点信息。

预分片技术:具体来说是在节点部署初期,就提前考虑日后的存储规模,建立足够多的实例(如128个节点),初期时数据很少,所以每个节点存储的数据也非常少。由于节点轻量的特性,数据之外的内存开销并不大,这使得只需要很少的服务器即可运行这些实例。

通过一致性哈希分片算法来实现数据分片

5. redis分布式锁是怎么样的、分布式锁的缺点、怎么解决

原理:
使用setnx命令

setnx(key,1)当一个线程执行setnx返回1,说明key原本不存在,该线程成功得到了锁,当其他线程执行setnx返回0,说明key已经存在,该线程抢锁失败。
del(key)释放锁之后,其他线程就可以继续执行setnx命令来获得锁。
setnx的key必须设置一个超时时间,以保证即使没有被显式释放,这把锁也要在一定时间后自动释放,expire(key, 30)
缺点:

  1. setnx和expire的非原子性 ,解决:Redis 2.6.12以上版本为set指令增加了可选参数,伪代码如下:set(key,1,30,NX),这样就可以取代setnx指令。
  2. 超时后使用del 导致误删其他线程的锁,解决:可以在加锁的时候把当前的线程ID当做value,并在删除之前验证key对应的value是不是自己线程的ID。
  3. 过期时间设置的太短,程序还没有执行完,出现并发的可能性,解决:让获得锁的线程开启一个守护线程,用来给快要过期的锁“续航”,守护线程会执行expire指令,给锁续期。当线程A执行完任务,会显式关掉守护线程。

Redis中setnx不支持设置过期时间,做分布式锁时要想避免某一客户端中断导致死锁,需设置lock过期时间,在高并发时 setnx与 expire 不能实现原子操作,如果要用,得在程序代码上显示的加锁。使用SET代替SETNX ,相当于SETNX+EXPIRE实现了原子性,不必担心SETNX成功,EXPIRE失败的问题。

redis

Redis如何发现热点key

  1. 凭借经验,进行预估:例如提前知道了某个活动的开启,那么就将此Key作为热点Key。
  2. 服务端收集:在操作redis之前,加入一行代码进行数据统计。
  3. 抓包进行评估:Redis使用TCP协议与客户端进行通信,通信协议采用的是RESP,所以自己写程序 监听端口也能进行拦截包进行解析。
  4. 在proxy层,对每一个 redis 请求进行收集上报。
  5. Redis自带命令查询:Redis4.0.4版本提供了redis-cli –hotkeys就能找出热点Key。(如果要用Redis自带命令查询时,要注意需要先把内存逐出策略设置为allkeys-lfu或者volatile-lfu,否则会返回错误。进入Redis中使用config set maxmemory-policy allkeys-lfu即可。)

Redis的热点key解决方案

  1. 服务端缓存:即将热点数据缓存至服务端的内存中.(利用Redis自带的消息通知机制来保证Redis和服务端热点Key的数据一致性,对于热点Key客户端建立一个监听,当热点Key有更新操作的时候,服务端也随之更新。)
  2. 备份热点Key:即将热点Key+随机数,随机分配至Redis其他节点中。这样访问热点key的时候就不会全部命中到一台机器上了。

7. zookeeper. (选举算法。Watch机制、ZAB协议)

zookeeper一致性

2PC(1.预提交 2.ack 3.提交)
内存 dataTreee
集群启动
leader挂掉,没有过半服务器,就不能对外提供服务

脑裂问题

不会出现这个问题,因为依靠过半机制来解决

myid,zxid

ZAB协议保持数据一致性

ZAB协议包括两种基本的模式:崩溃恢复和消息广播。当整个 Zookeeper 集群刚刚启动或者Leader服务器宕机、重启或者网络故障导致不存在过半的服务器与 Leader 服务器保持正常通信时,所有服务器进入崩溃恢复模式,首先选举产生新的 Leader 服务器,然后集群中 Follower 服务器开始与新的 Leader 服务器进行数据同步。当集群中超过半数机器与该 Leader 服务器完成数据同步之后,退出恢复模式进入消息广播模式,Leader 服务器开始接收客户端的事务请求生成事物提案(超过半数同意)来进行事务请求处理。

ZAB协议分为广播模式和崩溃恢复模式

Zookeeper的ZAB协议与Paxos协议区别

  1. 两者的初衷或者说设计目标不一样
    Paxos算法用于构建一个分布式的一致性状态机系统
    ZAB算法用于构建一个高可用的分布式数据主备系统

  2. 流程上有区别
    Paxos算法,选举出一个新的Leader进程需要进行两个阶段。
    第一个阶段是读阶段,这个阶段中,这个新的主进程会通过和其他 所有其他进程进行通信的方式收集上一个主进程提出的提案,并将它们提交。
    第二个阶段是写阶段,这个阶段中,当前主进程Leader开始提出它自己的提案。
    ZAB算法存在三个阶段:发现阶段、同步阶段、广播阶段,其中发现阶段等同于Paxos的 读阶段,广播阶段等同于Paxos的写阶段。

ZooKeeper Watch 机制

其实原理应该是很简单的,四个步骤:

  1. 客户端注册Watcher到服务端;
  2. 服务端发生数据变更;
  3. 服务端通知客户端数据变更;
  4. 客户端回调Watcher处理变更应对逻辑;

client端会对某个znode建立一个watcher事件,当该znode发生变化时,这些client会收到zk的通知,然后client可以根据znode变化来做出业务上的改变等。

所有对ZooKeeper的读操作都可以附带一个Watch。一旦相应的数据有变化,该Watch即被触发。
Watch有以下特点:
主动推送:Watch被触发时,由ZooKeeper服务器主动将更新推送给客户端,而不需要客户端轮询。
一次性:数据变化时Watch只会被触发一次。如果客户端想得到后续更新的通知,必须要在Watch被触发后重新再注册一个Watch。
顺序性:如果多个更新触发了多个Watch,那Watch被触发的顺序与更新的顺序一致。
可见性:如果一个客户端在读请求中附带Watch,Watch被触发的同时再次读取数据,客户端在得到Watch消息之前肯定不可能看到更新后的数据。换句话说,更新通知先于更新结果。

选举流程详述 FastLeaderElection(默认提供的选举算法)

1.服务启动的时候

1.投自己
2.接受其他服务器的信息
3.pk,Serverid,Zxid、Epoch
4.我当前投给了谁:最厉害的服务器
5.投票箱
6.统计:超过一半人和我投同样的服务器

目前有5台服务器,每台服务器均没有数据,它们的编号分别是1,2,3,4,5,按编号依次启动,它们的选择举过程如下:

  1. 服务器1启动,给自己投票,然后发投票信息,由于其它机器还没有启动所以它收不到反馈信息,服务器1的状态一直属于Looking。
  2. 服务器2启动,给自己投票,同时与之前启动的服务器1交换结果,由于服务器2的编号大所以服务器2胜出,但此时投票数没有大于半数,所以两个服务器的状态依然是LOOKING。
  3. 服务器3启动,给自己投票,同时与之前启动的服务器1,2交换信息,由于服务器3的编号最大所以服务器3胜出,此时投票数正好大于半数,所以服务器3成为leader,服务器1,2成为follower。
  4. 服务器4启动,给自己投票,同时与之前启动的服务器1,2,3交换信息,尽管服务器4的编号大,但之前服务器3已经胜出,所以服务器4只能成为follower。
  5. 服务器5启动,后面的逻辑同服务器4成为follower。

zk实现分布式锁

zk实现分布式锁主要利用其临时顺序节点,实现分布式锁的步骤如下:

  1. 创建一个目录mylock
  2. 线程A想获取锁就在mylock目录下创建临时顺序节点
  3. 获取mylock目录下所有的子节点,然后获取比自己小的兄弟节点,如果不存在,则说明当前线程顺序号最小,获得锁
  4. 线程B获取所有节点,判断自己不是最小节点,设置监听比自己次小的节点
  5. 线程A处理完,删除自己的节点,线程B监听到变更事件,判断自己是不是最小的节点,如果是则获得锁

8. dubbo

dubbo架构:

  1. 服务容器负责启动,加载,运行服务提供者。
  2. 服务提供者(生产者)在启动时,向注册中心注册自己提供的服务。
  3. 服务消费者在启动时,向注册中心订阅自己所需的服务。
  4. 注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。
  5. 服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。
  6. 服务消费者和提供者,在内存中累计调用次数和调用时间,(异步通知)定时每分钟发送一次统计数据到监控中心

Dubbo提供了4种负载均衡机制:

1.权重随机算法:RandomLoadBalance
2.最少活跃调用数算法:LeastActiveLoadBalance
3.一致性哈希算法:ConsistentHashLoadBalance
4.加权轮询算法:RoundRobinLoadBalance

dubbo的服务暴露、服务引用和服务调用

服务暴露
创建invoke代理对象,使用export方法,通过dubbo协议,将服务注册到注册表中,并创建netty的客户端来进行端口的监听

怎么服务暴漏到本地,然后再通过netty暴漏到远程
创建invoke代理对象,使用export方法,通过injvm协议把服务暴漏到本地,然后通过netty把服务注册到zookeeper(注册中心)上

服务引用
就是@Reference的使用。在ReferenceConfig中进行消息的订阅,这个消息订阅就是引用注册表中的invoke,并且也创建了一个netty客户端用于交互

服务调用
调用这个invoker代理对象(就是自动上注入的service),在dubbo中客户端调用的service是被多次代理后的一个对象,这之中有个filter代理,作用就是使用dubbo的容错、并得到loadblance,来选择使用注册中心中的哪个服务,最终的话是在DubboInvoker对象中进行远程调用,这个对象来获取到通道并模拟出传输的参数,通过request来进行请求,得到结果之后进行解析并返回。

dubbo支持的通信协议

①dubbo://
②hessian://
③rmi://
④http://
⑤webservice://
⑥thrift://
⑦memcached://
⑧redis://

Dubbo使用之容错机制

1、Failover Cluster 失败自动切换,当出现失败,重试其它服务器(默认)
2.Failfast Cluster 这种是 快速失败,只发起一次调用,失败立即报错。通常用于非幂等性的写操作,比如新增记录。
3.Failsafe Cluster 失败安全,出现异常时,直接忽略。通常用于写入审计日志等操作。
4.Failback Cluster 失败自动恢复,后台记录失败请求,定时重发。通常用于消息通知操作。
5.Forking Cluster 广播 并行调用多个服务器,只要一个成功即返回。

9. netty, nio、bio、aio原理

1.BIO、NIO、AIO的区别

Java BIO : 同步并阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。
BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中

Java NIO : 同步非阻塞,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。(通道和缓冲区的形式来进行处理数据的。)
NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中

Java AIO(NIO.2) : 异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理,
AIO方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作

10. 源码(springboot,spring(ioc,aop),mybatis,jdk,dubbo)

spring循环依赖、spring三级缓存、Spring是如何解决的循环依赖

答:Spring通过三级缓存解决了循环依赖,其中一级缓存为单例池(singletonObjects),二级缓存为早期曝光对象earlySingletonObjects,三级缓存为早期曝光对象工厂(singletonFactories)。当A、B两个类发生循环引用时,在A完成实例化后,就使用实例化后的对象去创建一个对象工厂,并添加到三级缓存中,如果A被AOP代理,那么通过这个工厂获取到的就是A代理后的对象,如果A没有被AOP代理,那么这个工厂获取到的就是A实例化的对象。当A进行属性注入时,会去创建B,同时B又依赖了A,所以创建B的同时又会去调用getBean(a)来获取需要的依赖,此时的getBean(a)会从缓存中获取,第一步,先获取到三级缓存中的工厂;第二步,调用对象工工厂的getObject方法来获取到对应的对象,得到这个对象后将其注入到B中。紧接着B会走完它的生命周期流程,包括初始化、后置处理器等。当B创建完后,会将B再注入到A中,此时A再完成它的整个生命周期。至此,循环依赖结束!

1.singletonObjects 第一级缓存,存放可用的成品Bean。单例池 ConcurrentHashMap
2.earlySingletonObjects 第二级缓存,存放半成品的Bean半成品的Bean是已创建对象,但是未注入属性和初始化。用以解决循环依赖。 HashMap
3.singletonFactories 第三级缓存,存的是Bean工厂对象,用来生成半成品的Bean并放入到二级缓存中。用以解决循环依赖。HashMap

  1. 在构造Bean对象之后,将对象提前曝光到缓存中,这时候曝光的对象仅仅是构造完成,还没注入属性和初始化。
  2. 提前曝光的对象被放入 singletonFactories缓存中,这里并不是直接将Bean放入缓存,而是包装成ObjectFactory对象再放入(Spring就是在对象外面包一层ObjectFactory,提前曝光的是ObjectFactory对象,在被注入时才在ObjectFactory内实时生成代理对象,并将生成好的代理对象放入到第二级缓存 earlySingletonObjects。 为了防止对象在后面的初始化(init)时重复代理
  3. 注入属性和初始化:通过populateBean方法注入属性,在注入其他Bean对象时,会先去缓存里取,如果缓存没有,就创建该对象并注入。通过initializeBean方法初始化对象,包含创建代理。
  4. 放入已完成创建的单例缓存
    在经历了以下步骤之后,最终通过addSingleton方法将最终生成的可用的Bean放入到单例缓存里。

Spring循环依赖三级缓存是否可以减少为二级缓存?

如果要使用二级缓存解决循环依赖,意味着Bean在实例化后就创建代理对象,这样违背了Spring设计原则。如果没有出现循环依赖的情况下,设计之初就是让Bean在生命周期的最后一步完成代理而不是在实例化后就立马完成代理。

1.springMVC运行过程

1.用户发起请求到前端控制器(Controller)
2.前端控制器没有处理业务逻辑的能力,需要找到具体的模型对象处理(Handler),到处理器映射器(HandlerMapping)中查找Handler对象(Model)。
HandlerMapping返回执行链,包含了2部分内容: ① Handler对象、② 拦截器数组
3.前端处理器通过处理器适配器包装后执行Handler对象。
4.处理业务逻辑。
5.Handler处理完业务逻辑,返回ModelAndView对象,其中view是视图名称,不是真正的视图对象。
6.将ModelAndView返回给前端控制器
7.视图解析器(ViewResolver)返回真正的视图对象(View)。
(此时前端控制器中既有视图又有Model对象数据)前端控制器根据模型数据和视图对象,进行视图渲染。
8.返回渲染后的视图(html/json/xml)返回。
9.给用户产生响应。

在这里插入图片描述

Spring事务管理的方式有几种?

1.编程式事务:在代码中硬编码(不推荐使用)。
2.声明式事务:在配置文件中配置(推荐使用),分为基于XML的声明式事务和基于注解的声明式事务。

2.spring事务 隔离级别 5

ISOLATION_DEFAULT:默认隔离级别
ISOLATION_READ_UNCOMMITTED(未提交读):会出现脏读、不可重复读、幻读、
ISOLATION_READ_COMMITTED(提交读):造成不可重复读、幻读
ISOLATION_REPEATABLE_READ(可重复读):但会出现幻读
ISOLATION_SERIALIZABLE(序列化):防止脏读、不可重复读、幻读

Mysql 默认:可重复读
Oracle 默认:读已提交

4.spring事务 传播行为 7

支持当前事务的情况:
PROPAGATION_REQUIRED:支持当前事务,如当前没有事务,则新建一个。
PROPAGATION_SUPPORTS:支持当前事务,如当前没有事务,则已非事务性执行。
PROPAGATION_MANDATORY:支持当前事务,如当前没有事务,则抛出异常。
不支持当前事务的情况:
PROPAGATION_REQUIRES_NEW:始终新建一个事务,如当前原来有事务,则把原事务挂起。
PROPAGATION_NOT_SUPPORTED:不支持当前事务,始终已非事务性方式执行,如当前事务存在,挂起该事务。
PROPAGATION_NEVER:不支持当前事务;如果当前事务存在,则引发异常。
其他情况:
PROPAGATION_NESTED:如果当前事务存在,则在嵌套事务中执行,如果当前没有事务,则执行与 PROPAGATION_REQUIRED 类似的操作。

5.spring boot和spring区别

1.约定大于配置
2.自带tomcat
3.自动配置
4.继承大量的第三方配置

7.spring是什么?spring的ioc和aop

spring是ioc和aop的轻量级容器
ioc:控制反转,用依赖注入(反射机制),将实例的初始化交给spring容器来管理,
ioc的反射机制允许我们不重新编译代码,因为它的对象是动态生成的。
aop是面向切面编程,是那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,
便于减少系统的重复代码,降低模块之间的耦合度,并有利于未来的可操作性和可维护性。
用于权限、日志 (适配器模式)

Spring Bean的生命周期?

1.bean实例化
2.注入对象属性
3.调用aware方法(beannameaware、beanfactoryaware、applicationcontextaware)
4.beanpostprocessor前置处理
5.初始化bean 、初始化 方法
6.beanpostprocessor后置处理
7.bean使用
8.执行销毁方法

6.springIOC三种注入方式(工厂模式)

1.接口注入:如果采用接口注入一个Bean,那么通过注入的Bean就必须要实现这个接口(最差)
2.set方法注入:如果采用set注入一个Bean,那么只需要为Bean中所需要的一些组件提供set方法就可以,
通过set方法注入比较清晰,大家一看就知道(补充)
3.构造器注入:如果采用构造器注入方式,那么首先为这个Bean提供自定义的构造函数,
构造函数中需要的参数就是类中的组件实例。(最优)

简述 Java 的反射机制及其应用场景

在运行状态中,对于任意一个类,都能够获取到这个类的所有属性和方法,对于任意一个对象,都能够调用它的任意一个方法和属性(包括私有的方法和属性),这种动态获取的信息以及动态调用对象的方法的功能就称为java语言的反射机制
 
1.JDBC 的数据库的连接 class.forName()
2.Spring 通过 XML 配置模式装载 Bean 的过程

6.springAOP代理模式(代理模式、适配器模式)

AOP基于动态代理,动态代理分为JDK动态代理CGLIB动态代理

1、如果目标对象实现了接口,默认情况下会采用JDK的动态代理实现AOP,可以强制使用CGLIB实现AOP
2、如果目标对象没有实现了接口,必须采用CGLIB库,spring会自动在JDK动态代理和CGLIB之间转换
JDK动态代理:被代理类必须实现了接口,
CGLIB动态代理:被代理类必须可以被继承

mybatis二级缓存

①、一级缓存是SqlSession级别的缓存。在操作数据库时需要构造sqlSession对象,在对象中有一个数据结构(HashMap)用于存储缓存数据。不同的sqlSession之间的缓存数据区域(HashMap)是互相不影响的。
 一级缓存的作用域是SqlSession范围的,当在同一个SqlSession中执行两次相同的sql语句时,第一次执行完毕会将数据库中查询的数据写到缓存(内存)中,第二次查询时会从缓存中获取数据,不再去底层进行数据库查询,从而提高了查询效率。需要注意的是:如果SqlSession执行了DML操作(insert、update、delete),并执行commit()操作,mybatis则会清空SqlSession中的一级缓存,这样做的目的是为了保证缓存数据中存储的是最新的信息,避免出现脏读现象。

②、二级缓存是mapper级别的缓存,多个SqlSession去操作同一个Mapper的sql语句,多个SqlSession可以共用二级缓存,二级缓存是跨SqlSession的。
 二级缓存是多个SqlSession共享的,其作用域是mapper的同一个namespace,不同的SqlSession两次执行相同的namespace下的sql语句,且向sql中传递的参数也相同,即最终执行相同的sql语句,则第一次执行完毕会将数据库中查询的数据写到缓存(内存),第二次查询时会从缓存中获取数据,不再去底层数据库查询,从而提高查询效率。

Mybatis默认没有开启二级缓存,需要在setting全局参数中配置开启二级缓存。

简述一下Mybatis 的编程步骤

sqlSessionFactoryBuilder——》parse解析
Configuration——》build
SqlSessionFactory——》openSession
SqlSession——》query
Executor——》newStatementHander
StatementHandler——》handleResultSets
ResultSetHandler

A.创建 SqlSessionFactory
B.通过 SqlSessionFactory 创建 SqlSession
C.通过 sqlsession 执行数据库操作
D.调用 session.commit()提交事务
E.调用 session.close()关闭会话

Spring Boot 的核心注解是哪个?它主要由哪几个注解组成的?

启动类上面的注解是@SpringBootApplication,它也是 Spring Boot 的核心注解,主要组合包含了以下 3 个注解:

@SpringBootConfiguration:组合了 @Configuration 注解,实现配置文件的功能。

@EnableAutoConfiguration:打开自动配置的功能,也可以关闭某个自动配置的选项,如关闭数据源自动配置功能: @SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })。

@ComponentScan:Spring组件扫描。

17、Spring Boot 自动配置原理是什么?

@SpringBootApplication 里面 @EnableAutoConfiguration注解的自动配置功能 里面的@Import 会读取jar包类路径下的 META-INF 目录下的 spring.factories 文件,获得每个框架定义的需要自动配置的配置类,把它们添加在spring容器中

13. Fastdfs,hdfs

FastDFS与hadoop的HDFS区别
主要是定位和应用场合不一样。
hadoop的文件系统HDFS主要解决并行计算中分布式存储数据的问题。其单个数据文件通常很大,采用了分块(切分)存储的方式;

FastDFS主要用于大中网站,为小文件上传和下载提供在线服务。所以在负载均衡、动态扩容等方面都支持得比较好,FastDFS不会对文件进行分快(切分)存储。

14. nginx,lvs

1.负载均衡调度算法3:1.随机 2. 轮训 (可以加权重) 3. hash环

2.Nginx、HAProxy、LVS三者的优缺点
HAproxy和Nginx由于可以做七层的转发,所以URL和目录的转发都可以做
在很大并发量的时候我们就要选择LVS

LVS: (Linux虚拟服务器),工作在第四层网络层,主要做负载均衡。lvs就比较依赖于网络环境。LVS的通过控制IP来实现负载均衡。
LVS三种工作模式分为NAT模式、TUN模式、以及DR模式。
1.VS/NAT 的优点是服务器可以运行任何支持TCP/IP的操作系统,它只需要一个IP地址配置在调度器上,服务器组可以用私有的IP地址。缺点是它的伸缩能力有限,当服务器结点数目升到20时,调度器本身有可能成为系统的新瓶颈,因为在VS/NAT中请求和响应报文都需要通过负载调度器。
2.VS/TUN调度器可以调度上百台服务器,而它本身不会成为系统的瓶颈,可以用来构建高性能的超级服务器。VS/TUN技术对服务器有要求,即所有的服务器必须支持“IP Tunneling”或者“IP Encapsulation”协议。目前,VS/TUN的后端服务器主要运行Linux操作系统,
3.VS/DR没有IP隧道的开销,但是要求负载调度器与实际服务器都有一块网卡连在同一物理网段上,服务器网络设备(或者设备别名)不作ARP响应,或者能将报文重定向(Redirect)到本地的Socket端口上。
在这里插入图片描述

LVS十种调度算法:1.轮询调度 2.加权轮询调度 3.最小连接调度 4.加权最小连接调度 5.基于局部的最少连接 6.带复制的基于局部性的最少连接 7.目标地址散列调度 8.源地址散列调度 9.最短的期望的延迟10.最少队列调度

HAproxy: 是基于第四层网络层和第七层应用层的转发,是专业的代理服务器

Nginx: 工作在网络的第七层应用层,主要做反向代理,是WEB服务器,缓存服务器,但负载度和稳定度不及lvs。nginx对网络的依赖较小

15.自己做过的项目和写的业务重点及解决过的问题你要自己总结下

.解决过程:1.查看后台系统服务器日志发现,在2020-07-07 22:35到2020-07-08 4:30 CPU飙升,一些接口也出现OOM的现象。
2.查看服务器日志发现在22:34出现流量异常,服务器上行流量出现一个高峰。怀疑为:1).用户上传文件处理特殊逻辑导致 2).内部流量。
3.查看nginx日志,流量不高(排除以上第一个怀疑)。
4.查看数据库服务器流量在22:34出现异常,DBA定位22:34语句。

问题原因:写查询语句时逻辑不够严谨,前端发生变化,翻空页时出现,select语句无条件查询,查出了1000万+的数据,进行处理。

总结经验教训:1.写代码时,应该多考虑特殊情况,异常情况 2.加强CodeRevew 3.加强监控
怎么保证钱不会出错

2.钱 调用第三方申请订单接口报错了,然后的一些补偿机制
TCC做回滚
我们单独采用一张表,把调用失败的记录下来,用xxl-job单独扫描这张表,定时提交

16.架构和整个框架流程要能总结出来

现在项目中的zookeeper是什么用的
分布式锁、dubbo的注册服务器地址

17.分布式问题

1.分布式ID实现方式

1)数据库自增
2)雪花算法
3)号段模式(滴滴TinyId实现)

2.分布式事务 :TCC 、LCN

事务 具备原子性、一致性、隔离性和持久性,简称 ACID。
原子性(Atomicity),可以理解为一个事务内的所有操作要么都执行,要么都不执行。
一致性(Consistency),可以理解为数据是满足完整性约束的,也就是不会存在中间状态的数据,比如你账上有400,我账上有100,你给我打200块,此时你账上的钱应该是200,我账上的钱应该是300,不会存在我账上钱加了,你账上钱没扣的中间状态。
隔离性(Isolation),指的是多个事务并发执行的时候不会互相干扰,即一个事务内部的数据对于其他事务来说是隔离的。
持久性(Durability),指的是一个事务完成了之后数据就被永远保存下来,之后的其他操作或故障都不会对事务的结果产生影响。

1)两阶段提交方案(2PC)

两阶段提交,有一个事务管理器的概念,负责协调多个数据库(资源管理器)的事务,事务管理器先问问各个数据库你准备好了吗?如果每个数据库都回复 ok,那么就正式提交事务,在各个数据库上执行操作;如果任何其中一个数据库回答不 ok,那么就回滚事务。

这种分布式事务方案,比较适合单块应用里,跨多个库的分布式事务,而且因为严重依赖于数据库层面来搞定复杂的事务,效率很低,绝对不适合高并发的场景。如果要玩儿,那么基于 Spring + JTA 就可以搞定,自己随便搜个 demo 看看就知道了。
在这里插入图片描述
在这里插入图片描述
第二阶段提交失败的话呢?

第一种是第二阶段执行的是回滚事务操作,那么答案是不断重试,直到所有参与者都回滚了,不然那些在第一阶段准备成功的参与者会一直阻塞着。

第二种是第二阶段执行的是提交事务操作,那么答案也是不断重试,因为有可能一些参与者的事务已经提交成功了,这个时候只有一条路,就是头铁往前冲,不断的重试,直到提交成功,到最后真的不行只能人工介入处理。

2PC 是一种尽量保证强一致性的分布式事务,因此它是同步阻塞的,而同步阻塞就导致长久的资源锁定问题,总体而言效率低,并且存在单点故障问题,在极端条件下存在数据不一致的风险。
2PC 适用于数据库层面的分布式事务场景

2)3PC方案
3PC 包含了三个阶段,分别是准备阶段、预提交阶段和提交阶段,对应的英文就是:CanCommit、PreCommit 和 DoCommit。
在这里插入图片描述引入了超时机制,参与者就不会傻等了,如果是等待提交命令超时,那么参与者就会提交事务了,因为都到了这一阶段了大概率是提交的,如果是等待预提交命令超时,那该干啥就干啥了,反正本来啥也没干。

3)TCC 方案
2PC 和 3PC 都是数据库层面的,而 TCC 是业务层面的分布式事务

TCC 的全称是:Try、Confirm、Cancel。
Try 阶段:这个阶段说的是对各个服务的资源做检测以及对资源进行锁定或者预留。
Confirm 阶段:这个阶段说的是在各个服务中执行实际的操作。
Cancel 阶段:如果任何一个服务的业务方法执行出错,那么这里就需要进行补偿,就是执行已经执行成功的业务逻辑的回滚操作。(把那些执行成功的回滚)
这个事务回滚实际上是严重依赖于你自己写代码来回滚和补偿了,会造成补偿代码巨大,非常之恶心。
支付、交易相关的场景,我们会用 TCC,严格保证分布式事务要么全部成功,要么全部自动回滚,严格保证资金的正确性,保证在资金上不会出现问题。
在这里插入图片描述

4)可靠消息最终一致性方案
直接基于 MQ 来实现事务。比如阿里的 RocketMQ 就支持消息事务。
大概的意思就是:
1.A 系统先发送一个 prepared 消息到 mq,如果这个 prepared 消息发送失败那么就直接取消操作别执行了;
2.如果这个消息发送成功过了,那么接着执行本地事务,如果成功就告诉 mq 发送确认消息,如果失败就告诉 mq 回滚消息;
3.如果发送了确认消息,那么此时 B 系统会接收到确认消息,然后执行本地的事务;
4.mq 会自动定时轮询所有 prepared 消息回调你的接口,问你,这个消息是不是本地事务处理失败了,所有没发送确认的消息,是继续重试还是回滚?一般来说这里你就可以查下数据库看之前本地事务是否执行,如果回滚了,那么这里也回滚吧。这个就是避免可能本地事务执行成功了,而确认消息却发送失败了。
这个方案里,要是系统 B 的事务失败了咋办?重试咯,自动不断重试直到成功,如果实在是不行,要么就是针对重要的资金类业务进行回滚,比如 B 系统本地回滚后,想办法通知系统 A 也回滚;或者是发送报警由人工来手工回滚和补偿。。
在这里插入图片描述

阿里开源分布式事务框架:seata

3.分布式框架:CAP理论、BASE理论 、eureka、zookeeper、dubbo 、spring cloud、SEATA

8.CAP原则(zookeeper(cp)、eureka(ap))

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

CAP原则又称CAP定理,指的是在一个分布式系统中, Consistency(一致性)、 Availability(可用性)、
Partition tolerance(分区容错性),三者不可得兼,是NOSQL数据库的基石。

只能:
CP:放弃可用性,追求一致性和分区容错性 (zookeeper)

AP:放弃一致性,追求放弃容错性和可用性(eureka)

9.BASE 原则

BASE 是 Basically Available(基本可用)、Soft state(软状态)和 Eventually consistent (最终一致性)
三个短语的缩写,是对 CAP 中 AP 的一个扩展。

基本可用:分布式系统在出现故障时,允许损失部分可用功能,保证核心功能可用。

软状态:允许系统中存在中间状态,这个状态不影响系统可用性,这里指的是 CAP 中的不一致。

最终一致:最终一致是指经过一段时间后,所有节点数据都将会达到一致。

4.分布式session:

redis

5.分布式锁

1)基于数据库实现分布式锁 2)基于缓存redis实现分布式锁 (设置超时时间)3)基于Zookeeper实现分布式锁(临时节点)

6.LRU算法

LRU是一种缓存淘汰机制策略 用双向链表实现
LRU(Least recently used,最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。
最常见的实现是使用一个链表保存缓存数据,详细算法实现如下

在这里插入图片描述

新数据插入到链表头部;
每当缓存命中(即缓存数据被访问),则将数据移到链表头部;
当链表满的时候,将链表尾部的数据丢弃。
【命中率】
当存在热点数据时,LRU的效率很好,但偶发性的、周期性的批量操作会导致LRU命中率急剧下降,缓存污染情况比较严重。
【复杂度】
实现简单。
【代价】
命中时需要遍历链表,找到命中的数据块索引,然后需要将数据移到头部。

快速排序、插入排序、冒泡排序

快速排序:
1、先从数列中取出一个数作为基准数
2、分区过程,将比这个数大的数全放到它的右边,小于或等于它的数全放到它的左边
3、再对左右区间重复第二步,直到各区间只有一个数
概括来说为 挖坑填数+分治法

插入排序:
1.从数组的第二个数据开始往前比较,即一开始用第二个数和他前面的一个比较,如果 符合条件(比前面的大或者小,自定义),则让他们交换位置。
2.然后再用第三个数和第二个比较,符合则交换,但是此处还得继续往前比较,
3.重复步骤二,一直到数据全都排完。

冒泡排序:
1.比较相邻的元素。如果第一个比第二个大,就交换他们两个。
2.对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
3.针对所有的元素重复以上的步骤,除了最后一个。
4.持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。

11. Kafka,rocketmq,rabbitmq、activemq

1…消息中间件?消息中间件特点、应用场景?*

  1. 异步处理,用户注册
  2. 解耦,用户下单.
  3. 流量削峰,秒杀活动
  4. 日志处理 (kafka)
  5. 消息通讯,点对点,聊天室

activemq的几种通信方式

1.发布订阅模式
2.点对点

消息重复解决方案: 消息可以使用唯一id标识

生产者(ack=all 代表至少成功发送一次)
消费者 (offset手动提交,业务逻辑成功处理后,提交offset)
落表(主键或者唯一索引的方式,避免重复数据)

业务逻辑处理(选择唯一主键存储到Redis或者mongdb中,先查询是否存在,若存在则不处理;若不存在,先插入Redis或Mongdb,再进行业务逻辑处理)

RabbitMQ(erlang)

RabbitMQ接收到消息之后丢失了消息

confirm机制:
RabbitMQ可以开启 confirm 模式,在生产者那里设置开启 confirm 模式之后,生产者每次写的消息都会分配一个唯一的 id,如果消息成功写入 RabbitMQ 中,RabbitMQ 会给生产者回传一个 ack 消息,告诉你说这个消息 ok 了。如果 RabbitMQ 没能处理这个消息,会回调你的一个 nack 接口,告诉你这个消息接收失败,生产者可以发送。而且你可以结合这个机制自己在内存里维护每个消息 id 的状态,如果超过一定时间还没接收到这个消息的回调,那么可以重发。

Kafka(java)

producer:生产者
consumer:消费者
broker:机器
topic:队列
partition:分区(可以主从)生产者发送消费数据,消费者消费数据都是leader

1、Kafka在高并发的情况下,如何避免消息丢失和消息重复?

消息丢失解决方案:
首先对kafka进行限速, 其次启用重试机制,重试间隔时间设置长一些,最后Kafka设置acks=all,即需要相应的所有处于ISR的分区都确认收到该消息后,才算发送成功

2、kafka怎么保证数据消费一次且仅消费一次

  1. 幂等producer:保证发送单个分区的消息只会发送一次,不会出现重复消息
  2. 事务(transaction):保证原子性地写入到多个分区,即写入到多个分区的消息要么全部成功,要么全部回滚流处理EOS:流处理本质上可看成是“读取-处理-写入”的管道。此EOS保证整个过程的操作是原子性。注意,这只适用于Kafka Streams
  3. 流式EOS

kafka高性能高吞吐的原因

1.磁盘顺序读写:保证了消息的堆积
2.零拷贝:避免CPU将数据从一块存储拷贝到另一块存储的技术
3.分区分段+索引
4.批量压缩:多条数据一起压缩,降低带宽
5.批量读写
6.直接操作page cache,而不是jvm、避免gc耗时和对象创建耗时,读写数据快,重启不会数据丢失

kafka副本同步机制

partition —— offset
LEO
ISR

kafka高可靠消息解决方案(防止消息丢失)

生产端:
1.ack=all,等待isr同步后再返回
2.unclean.leader.election.enable=false,禁止选举isr以外的follower为leader
3.tries>1,重试次数
4.min.insync.replicas>1,同步副本数,没满足该值前,不通过读写服务、写操作异常
broker:事务机制、减小刷盘间隔
消费端:手工提交offset

kafka的rebalance机制

rocketMQ

12. Elasticserarch

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值