这里是参考B站上的大佬做的面试题笔记。大家也可以去看视频讲解!!!
1.1、 什么是面向对象?
对比面向过程、是两种不同的处理问题的角度
面向过程更注重事情的没一个步骤及顺序,面向对象更注重事情有哪些参与者(对象),及各自需要做什么。
例如:洗衣机洗衣服
从以上例子可以看出:面向过程比较直观高效,而面向对象更易于复用,扩展和维护。
1.2 、面向对象
封装
封装的意义,在于明确标识出允许外部使用的所有成员函数和数据项,内部细节对外部调用透明,外部调用无需修改或者关心内部实现。
1、javabean的属性私有,提供get/set对外访问,因为属性的赋值或者获取逻辑只能由javabean本身决定,而不能由外部胡乱修改。
2、orm框架:操作数据库,我们不需要关心链接时如何建立的,sql时如何执行的,只需要引入mybatis,调方法即可。
继承
多态
无法调用子类特有的功能
JDK:
JRE:
JVM:
举例
@Test
void test(){
String str1 = "hello";
String str2 = new String("hello");
String str3 = str2;//引用传递
System.out.println("123");
System.out.println("(str1 == str2):"+(str1 == str2));//false
System.out.println("(str1 == str3):"+(str1 == str3));//false
System.out.println("(str2 == str3):"+(str2 == str3));//true
System.out.println("str1.equals(str2):"+str1.equals(str2));//true
System.out.println("str1.equals(str3):"+str1.equals(str3));//true
System.out.println("str2.equals(str3):"+str2.equals(str3));//true
}
4.1、简述final作用
最终的
(1) 、 修饰成员变量
(2) 、修饰局部变量
系统不会为局部变量进行初始化,局部变量必须由程序员显示初始化,因此使用final修饰局部变量时,即可以在定义时指定默认值(后面的代码不能对变量再赋值),也可以不指定默认值,而在后面的代码中对final变量赋初值(仅一次)
(3) 、修饰基本类型数据和引用类型数据
4.2 、为什么局部内部类和匿名内部类只能访问局部final变量?
首先需要知道的一点是:内部类和外部类是处于同一个级别的,内部类不会因为定义在方法中就会随着方法的执行完毕就被销毁。
这里就会产生问题:当外部类的方法结束时,局部变量就会被销毁,但是内部类对象可能还存在(只有没有人再引用它时,才会死亡)。这里就出现了一个矛盾:内部类对象访问了一个不存在的变量。为了解决这个问题,就将局部变量复制了一份作为内部类的成员变量,这样当局部变量死亡后,内部类仍可以访问它,实际访问的是局部变量的copy,这样好像就延长了局部变量的生命周期。
将局部变量复制为内部类的成员变量时,必须保证这两个变量是一样的,也就是如果我们再内部类中修改了成员变量,方法中的局部变量也得跟着改变,怎样解决问题呢?
就将局部变量设置为final、对它初始化之后,我就不让你再去修改这个变量,就保证了内部类的成员变量和方法的局部变量的一致性,这实际上也是一种妥协,使得局部变量与内部类内建立的拷贝保持一致性。
性能:StringBuilder > StringBuffer > String
场景:经常需要改变字符串内容时使用后面两个
优先使用StringBuilder,多线程使用共享变量时使用StringBuffer
接口的设计目的,是对类的行为进行约束(更准确的说是一种“有”约束,因为接口不能规定类不可以有什么行为),也就是提供一种机制,可以强制要求不同的类具有相同的行为,它只约束了行为的有无,但不对如何实现行为进行限制。
而抽象类的设计目的,是代码复用。当不同的类具有某些相同的行为(记为行为集合),且其中一部分行为的实现方式一致时(A的非真子集,记为B)可以让这些类都派生与一个抽象类,在这个抽象类中实现了B,避免让所有的子类来实现B,这就达到了代码复用的目的。而A减去B的部分,留给各个子类自己实现,正是因为A-B在这里没有实现,所以抽象类不允许实例化出来(否则当调用到A-B时,无法执行)。
抽象类是对类本质的抽象,表达的是is-a的关系,比如:BMW is a car 。抽象类包含并实现子类的通用特性,将子类存在差异化的特性进行抽象,交由子类去实现。
而接口是对行为的抽象,表达的是like a的关系,比如:bird like a aircraft(像飞行器一样飞),但其本质上 is a bird 。接口的核心是定义行为,至于实现主体是谁,是如何实现,接口并不关心。
使用场景:当你关注一个事物的本质的时候,用抽象类;当你关注一个操作的时候,用接口
抽象类的能力要远超过接口,但是,定义抽象类的代价高,因为高级语言来说(从实际设计上来说也是)每个类只能继承一个类,在这个类中,你必须继承或编写出其所有子类的所有共性。虽然接口在功能上会弱化许多,但是它只是针对一个动作的描述,而且你可以在一个类中同时实现多个接口,在设计阶段会降低难度
**铁汁们、铁汁们、铁汁们、都看到这里了、动动可爱的小手、点个赞呗、鼓励一下。哈哈哈**
hashcode介绍
hashcode()的作用是获取哈希码,也称为散列码;它实际上是返回一个int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashcode()定义在JDK的Object.java中,java中的任何类都包含有hashcode()函数。散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。这其中就利用到了散列码!(可以快速找到所需要的对象)
为什么要有hashcode
以“HashSet如何检查重复”为例子来说明为什么要有hashcode:
对象加入HashSet时,HashSet会先计算对象的hashcode值来判断对象加入的位置,看该位置是否有值,如果没有、HashSet会假设对象没有重复出现。但是如果发现有值,这时会调用equals()方法来检查两个对象是否真的相同。如果两者相同,HashSet就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。这样就大大减少了equals的次数,相应就大大提高了执行速度。
ArrayList:
LinkedList:
区别:
底层实现:数组+链表实现
jdk8开始链表高度到8,数组长度超过64,链表转变为红黑树,元素内部类Node节点存在
数组扩容
jdk7:
数据结构:ReentrantLock+Segment+HashEntry,一个Segment中包含一个HashEntry数组,每个HashEntry又是一个链表结构
元素查询:二次hash,第一次Hash定位到Segment,第二次hash定位到元素所在的链表的头部
锁:Segment分段锁 Segment继承了ReentrantLock,锁定操作的Segment,其他的Segment不受影响,并发度为Segment个数,可以通过构造函数指定,数组扩容不会影响到其他的segment
get方法无需加锁,volatile保证
jdk8:
1、配置文件配置包扫描路径
2、递归包扫描获取.class
文件
3、反射、确定需要交给IOC管理的类
4、对需要注入的类进行依赖注入
.class
结尾的文件添加到一个Set集合中进行存储java中的编译器和解释器
java中引入了虚拟机的概念,即在机器和编译程序之间加入了一层抽象的虚拟机的机器。这台虚拟机的机器在任何平台上都提供给编译程序一个共同的接口。
编译程序只需要面向虚拟机,生成虚拟机能够理解的代码,然后由解释器来将虚拟机代码转化为特定系统的机器码执行。在java中,这种提供虚拟机理解的代码叫做字节码(即扩展名为.class的文件),它不面向任何特定的处理器,只面向虚拟机。
每一种平台的解释器是不同的,但是实现的虚拟机是相同的。java源程序经过编译器编译后变成字节码,字节码由虚拟机解释执行,虚拟机将每一条要执行的字节码送给解释器,解释器将其翻译成特定机器上的机器码,然后再特定的机器上运行,这也就是解释了java的编译与解释并存的特点。
java源代码----->编译器------->jvm可执行的java字节码(即虚拟机指令)------>jvm------->jvm中的解释器-------->机器可以执行的二进制机器码------>程序运行。
采用字节码的好处:
java语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时保留了解释型语言可移植的特定。所以java程序运行时比较高效,而且,由于字节码并不专对一种特定的机器,因此,java程序无需重新编译便可在多种不同的计算机上运行。
JDK自带有三个类加载器:bootstrap ClassLoader、ExtClassLoader、AppClassLoader.
%JAVA_HOME%
下的jar包和class文件。%JAVA_HOME%/lib/ext
文件夹下的jar包和class类。双亲委派模型的好处:
铁汁们、铁汁们、铁汁们、都看到这里了。拜托、拜托、拜托、一键三连呗
引用计数法,可能会出现A引用lB,B又引用了A,这时候就算他们都不在使用了,但因为相互引用计数器=1、永远无法完成回收。
GC Toots的对象有哪些:
可达性算法中的不可达对象并不是并不是立即死亡的,对象拥有一次自我拯救的机会。对象被系统宣告死亡至少要经历两次标记过程:第一次是经过可达性分析发现没有GC Roots相连接的引用链,第二次是在由虚拟机自动建立的Finalizer队列中判断是否需要执行finalize()方法。
当对象变成(GC ROOts)不可达时,GC会判断该对象是否覆盖了finalize()方法,若未覆盖,则直接将其回收。否则,若对象未执行过finalize()方法,将其放入F-Queue队列,由一低优先级线程执行该队列中对象的finalize()方法。执行finalize()方法完毕后,GC会再次判断该对象是否可达,若不可达,则进行回收,否则,对象“复活"
每个对象只能触发一次finalize()方法
由于finalize()方法运行代价高昂,不确定性大,无法保证各个对象的调用顺序,不推荐大家使用,建议遗忘它
1、线程通常有五种状态,创建、就绪、运行、阻塞和死亡状态。
2、阻塞的情况又分为三种:
等待阻塞:运行的线程执行wait()方法,该线程会释放占用的所有资源,JVM会把该线程放入”等待池中“。进入这个状态后,是不能自动唤醒的,必须依靠其他线程调用notify或notifyAll方法才能被唤醒,wait是object类的方法
同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入”锁池“中
其他阻塞:运行的线程执行sleep或join方法,或者发出来I/O请求时,JVM会把该线程设置为阻塞状态。当sleep状态超时、join等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。sleep是Thread类的方法
1、新建状态(new):新建了一个线程对象
2、就绪状态(Runnable):线程对象创建后、其他线程调用了该对象的start()方法。该状态的线程位于
可运行线程池中,变得可运行,等待获取cpu的使用权。
3、运行状态(Runnbaleing):就绪状态的线程获取了CPU,执行程序代码。
4、阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU的使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。
5、死亡状态(Dead):线程执行完了或者因异常推出了run方法,该线程结束生命周期
1、锁池
所有需要竞争同步锁的线程都会放在锁池中,比如当前对象的锁已经被其中一个线程得到,则其他线程需要在这个锁池进行等待,当前面的线程释放同步锁后锁池中的线程区竞争同步锁,当某个线程得到后会进入就绪队列进行等待CPU资源分配。
2、等待池
当我们调用wait()方法后,线程会放到等待池当中,等待池的线程是不会去竞争同步锁的。只有调用了notify()或notifyAll()后等待池的线程才会去竞争锁,notify()是随机从等待池选出一个线程放到锁池,而notifyAll()是将等待池中的所有线程放到锁池中。
1、sleep是Thread类的静态本地方法,wait是object类的本地方法
2、sleep方法不会释放lock,但是wait会释放,而且会加入到等待队列中。
sleep就是把cpu的执行资格和执行权释放出去,不再运行此线程,当定时时间结束再取回cpu资源,参与cpu调度,获取到cpu资源后就可以继续运行了。而如果sleep时该线程有锁,那么sleep不会释放这个锁,而是把锁带着进入了冻结状态,也就是说其他需要这个锁的线程根本不可能获取到这个锁。也就是说无法执行程序。如果在睡眠期间其他线程调用了这个线程的interrupt方法,那么这个线程也就会抛出interruptexception异常返回,这点和wait是一样的
3、sleep方法不依赖于同步器synchronized,但是wait需要依赖synchronized关键字
4、sleep不需要被唤醒(休眠之后推出阻塞),但是wait需要(不指定时间需要被别人中断)。
5、sleep一般用于当前线程休眠,或者轮循暂停操作,wait则多用于多线程之间的通信
6、sleep会让出CPU执行时间且强制上下文切换,而wait则不一定,wait后可能还是有机会重新竞争到锁继续执行的。
yield()执行后线程直接进入就绪状态,马上释放了cpu的执行权,但是依然保留了cpu的执行资格,所以有可能cpu下次进行线程调度还会让这个线程获取到执行权继续执行。
join()执行后线程进入阻塞状态,例如在线程B中调用线程A的join(),那么线程B会进入到阻塞队列,直到线程A结束或中断线程
不是线程安全、应该是内存安全,堆是共享内存,可以被所有线程访问
当多个线程访问一个对象时,如果不用进行额外的同步控制或其他的协调操作,调用这个对象的行为都可以获得正确的结果,我们就说这个对象是线程安全的。
在java中,堆是java虚拟机所管理的内存中最大的一块,是所有线程共享的一块内存区域,在虚拟机启动时创建。堆所存在的内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。
目前主流操作系统都是多任务的,即多个进程同时运行。为了保证安全,每个进程只能访问分配给自己的内存空间,而不能访问别的进程的,这是由操作系统保障的。
在每个进程的内存空间中都会有一块特殊的公共区域,通常称为堆(内存)。进程的所有线程都可以访问到该区域,这就是造成问题的潜在原因。
Thread和Runnable的实质是继承关系,没有可比性。无论使用Runnable还是Thread,都会new Thread,然后执行run()方法。用法上,如果有复杂的线程操作需求,那就选择继承Thread,如果知识简单的执行一个任务,那就实现Runnable
守护线程:为所有非守护线程提供服务的线程;任何一个守护线程都是整个JVM中所有非守护线程的保姆;
守护线程类似于整个进程的一个默默无闻的小喽啰;它的生死无关重要,它却依赖整个进程而运行;哪天其他线程结束了,没有要执行的了,程序就结束了,理都没理守护线程,就把它中断了;
注意:由于守护线程的终止是自身无法控制的,因此千万不要把IO、File等重要操作逻辑分配给它;因为它不靠谱;
守护线程的作用是什么?
举例,GC垃圾回收线程:就是一个经典的守护线程,当我们的程序中不再有任何运行的Thread,程序就不会再产生垃圾,垃圾回收器也就无事可做,所以当垃圾回收线程是JVM上仅剩的线程时,垃圾回收线程会自动离开。它开始终再低级别的状态中运行,用于实时监控和管理系统中的可回收资源。
应用场景:
thread.setDaemon(true)
必须在thread.start()
之前设置,否则会抛出一个IllegalThreadException
异常。你不能把正在运行的常规线程设置为守护线程。
在Daemon线程中产生的新线程也是Daemon的。
守护线程不能用于去访问固有资源,比如读写操作或者计算逻辑。因为它会在任何时候甚至在一个操作的中间发生中断
java自带的多线程框架,比如ExecutorService,会将守护线程转换为用户线程,所以如果要使用后台线程就不能用java的线程池。
每一个Thread对象均含有一个ThreadLocalMap类型的成员变量threadLocals,它存储本线程中所有ThreadLocal对象及其对应的值
ThreadLocalMap由一个个Entry对象构成。
Entry
继承自WeakReference
,一个Entry
由ThreadLocal
对象和object
构成。由此可见,Entry
的key是ThreadLocal
对象,并且是一个弱引用,当没指向key的强引用后,该key就会被垃圾收集器回收。
当执行set方法时,ThreadLocal首先会获取当前线程对象,然后获取当前线程的ThreadLocalMap对象。再以当前ThreadLocal对象为key,将值存储进ThreadLocalMap对象中。
get方法执行过程类似,ThreadLocal首先会获取当前线程对象,然后获取当前线程的ThreadLocalMap对象。再以当前ThreadLocal对象为key,获取对应的value。
由于每一条线程均含有各自私有的ThreadLocalMap容器,这些容器相互独立互不影响,因此不会存在线程安全性问题,从而也无需使用同步机制来保证多条线程访问容器的互斥性。
使用场景:
内存泄漏为程序在申请内存后,无法释放已申请的内存空间,一次内存泄漏危害可以忽略,但内存泄漏堆积后果很严重,无论多少内存,迟早会被占光。
不再会被使用的对象或者变量占用的内存不能被回收,就是内存泄漏。
强引用:使用最普遍的引用(new),一个对象具有强引用,不会被垃圾回收器回收。当内存空间不足,java虚拟机宁愿抛出OutOfMemoryError
错误,使程序异常终止,也不回收这种对象。
如果想取消强引用和某个对象之间的关联,可以显式地将引用赋值为null,这样就可以使JVM在合适的时间就会回收该对象。
弱引用:JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在java中,用java.lang.ref.WeakReference
类来表示。可以在缓存中使用弱引用。
ThreadLocal的实现原理,每一个Thread维护一个ThreadLocalMap,key为使用弱引用的ThreadLocal实例,value为线程变量的副本
hreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal不存在外部强引用时,key(ThreadLocal)势必会被GC回收,这样就会导致ThreadLocalMap中key为null,而value还存在这强引用,只有thead线程退出以后value的强引用链条才会断掉,但如果当前现线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链(红色链条)
key使用强引用
当threadLocalMap的key为强引用回收ThreadLocal时,因为ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。
key的弱引用
当ThreadLocalMap的key为弱引用回收ThreadLocal时,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。当key为null,在下一次ThreadLocalMap调用set(),get(),remove()方法的时候会被清除value值
ThreadLocal正确的使用方法
原子性
可见性
有序性
1、降低资源消耗;提高线程利用率,降低创建和销毁线程的消耗
2、提高响应速度;任务来了,直接有线程可用可执行,而不是先创建线程,再执行。
3、提高线程的可管理性;线程是稀缺资源,使用线程池可以统一分配调优监控
corepoolsize
代表核心线程数,也就是正常情况下创建工作线程的线程数,这些线程创建后并不会消除,而是一种常驻线程。
maxinumpoolsize
代表的是最大的线程数,它与核心线程数相对应,表示最大允许被创建的线程数,比如当前任务较多,将核心线程数都用完了,还无法满足需求时,此时就会创建新的线程,但是线程池内线程总数不会超过最大线程数
keepAliveTime、unit
表示超出核心线程数之外的线程的空闲存活时间,也就是核心线程不会消除,但是超出核心线程的部分线程如果空闲一定的时间会被消除,我们可以通过setKeepAliveTime来设置空闲时间
workQueue
用来存放待执行的任务,假设我们现在核心线程都已被停用,还有任务进来则全部放入队列,直到整个队列被放满但任务还再持续进入则会开始创建新的线程。
ThreadFactory
实际上是一个线程工厂,用来生产线程执行任务。我们可以选择使用默认的创建工厂,产生的线程都会在同一个组内,拥有相同的优先级,且都不是守护线程。当然我们也可以选择自定义线程工厂,一般我们会根据业务来指定不同的线程工厂
handler任务拒绝策略,有两种情况,第一种是当我们调用shutdown等方法关闭线程池后,这时候即使线程池内部还有执行完的任务正在执行,但是由于线程池已经关闭,我们在继续向线程池提交任务就会遭到拒绝。另一种情况就是当达到最大线程数,线程池已经没有能力继续处理新提交的任务时,这也是拒绝
1、一般的队列只能保证作为一个有限长度的缓冲区,如果超出了缓冲长度,就无法保留当前的任务了,阻塞队列通过阻塞可以保留住当前想要继续入队的任务。
阻塞队列可以保证任务队列中没有任务时阻塞获取任务的线程,使得线程进入wait状态,释放cpu资源。
阻塞队列自带阻塞和唤醒功能,不需要额外处理,无任务执行时,线程池利用阻塞队列的take方法挂起,从而维持核心线程的存活,不至于一直占用cpu资源
2、在创建新线程的时候,是要获取全局锁的,这个时候其它的就得阻塞,影响了整体效率。
就好比一个企业里有10个(core)正式工的名额,最多招10个正式工,要是任务超过正式人工数(task>core)的情况下、工厂领导(线程池)不是首先扩招工人,还是这10个人,但是任务可以稍微积压一下,即先放到队列区(代价低)。10个正式工慢慢干,迟早会干完的。要是任务还在继续增加,超过正式工的加班忍耐极限了(队列满了),就的招外包帮忙了(注意是临时工)要是正式工加上外包还是不能完成任务,那新来的任务就会被领导拒绝了(线程池的拒绝策略)
线程池将线程和任务进行解耦,线程是线程,任务是任务,摆脱了之前通过Thread创建线程时的一个线程必须对应一个任务的限制。
在线程池中,同一个线程可以从阻塞队列中不断获取新任务来执行,其核心原理在于线程池对Thread进行了封装,并不是每次执行任务都会调用Thread.start()来创建线程,而是让每个线程去执行一个“循环任务”,在这个“循环任务”中不停检查是否有任务需要被执行,也就是调用任务中的run方法,将run方法当成一个普通的方法执行,通过这种方式只使用固定的线程就将所有任务的run方法串联起来
轻量级的开源的J2EE框架,它是一个容器框架,用来装javabean(java对象),中间层框架(万能胶)可以起一个连接作用,比如说把Structs和hibernate粘合在一起运用,可以让我们的企业开发更快,更简洁
Spring是一个轻量级的控制反转(IOC)和面向切面(AOP)的容器框架
从大小与开销两方面而言Spring都是轻量级的。
通过控制反转(IOC)的技术达到松耦合的目的。
提供了面向切面编程的丰富支持,允许通过分离应用的业务逻辑与系统级服务进行内聚性的开发。
包含并管理应用对象(Bean)的配置和生命周期,这个意义上是一个容器
将简单的组件配置,组合成为复杂的应用,这个意思上是一个框架。
容器概念、控制反转、依赖注入
IOC容器:
实际上就是个map(key,value),里面存的是各种对象(在xml里配置的bean节点,@repository、@service、@controller、@component),在项目启动时的时候会读取配置文件里面的bean节点,根据全限定类名使用反射创建对象放到map里,扫描到打上上述注解的类还是通过反射创建对象放到map里。
这个时候map里就有各种对象了,接下来我们在代码里需要用到里面的对象时,再通过DI注入(autowired、resource等注解,xml里面bean节点内的ref属性,项目启动时候会读取xml节点ref属性更具DI注入,也会扫描这些注解,根据类型或DI注入,id就是对象名)
控制反转
没有引入IOC容器之前,对象A依赖与对象B,那么对象A在初始化或者在运行到某一点的时候,自己必须主动去创建对象B或者使用已经创建的对象B。无论是创建还是使对象B,控制权都在自己手上。
引入IOC容器之后,对象A与对象B之间失去了直接关系,当对象A运行到需要对象B的时候,IOC容器会主动去创建一个对象B注入到对象A需要的地方
通过前后的对比,不难看出:对象A获得依赖对象B的过程由主动行为变为了被动行文,控制权颠倒过来了,这就是“控制反转”这个名称的由来
全部对象的控制权全部上缴给“第三方“IOC容器、所以,IOC容器成了整个系统的关键核心,它起到了一种类似“粘合剂” 的作用,把系统中的所有对象粘合在一起发挥作用,如果没有这个“粘合剂”,对象与对象之间会彼此失去联系,这就是有人把IOC容器比喻成“粘合剂”的由来
依赖注入
ApplicationContext是BeanFactory的子接口
ApplicationContext提供了更完整的功能:
继承了MessageSource,因此支持国际化
统一的资源文件访问方式
提供在监听器中注册bean的事件
同时加载多个配置文件
载入多个(有继承关系)上下文,使得每一个上下文都专注与一个特定的层次,比如应用的web层
BeanFactory
采用的是延迟加载形式来注入Bean的,即只有在使用到某个Bean时(调用getBean()),才对该Bean进行加载实例化。这样、我们就不能发现一些存在的Spring配置问题。如果Bean的某一个属性没有注入,BeanFactory加载后,直至第一次使用调用getBean()方法才会抛出异常
ApplicationContext
,它是在容器启动时,一次性创建了所有的Bean。这样,在容器启动时,我们就可以发现Spring中存在的配置错误,这样有利于检查所依赖属性是否注入。ApplicationContext
启动后预载入所有的单实例Bean,确保当你需要的时候,你就不用在等待,因为他们已经创建好了。
相对于基本的BeanFactory
,ApplicationContext
唯一的不足是占用内存空间。当应用程序配置Bean较多时,程序启动较慢。
BeanFactory
通常以编程的方式被创建,ApplicationContext
还能以声明的方式创建,如使用ContextLoader
BeanFactory
和ApplicationContext
都支持BeanPostProcessor
、BeanFactoryPostProcessor
的使用,但两者之间的区别是:BeanFactory
需要手动注册,而ApplicationContext
则是自动注册
BeanDefinition
@Autowired
注解的属性进行属性填充BeanNameAware
,BeanFactoryAware
BeanPostProcessor
的初始化前的方法BeanPostProcessor
的初始化后的方法,在这里进行AOPDisposableBean
中destory()方法singleton:默认,每个容器中只有一个bean的实例,单例的模式由BeanFactory自身来维护。该对象的生命周期是与Spring IOC容器一致的(但是第一次被注入时才会创建)
prototype:为每一个bean请求提供一个实例。在每次注入时都会创建一个新的对象
request:bean被定义为在每个HTTP请求中创建一个单例对象,也就是说在单个请求中都会复用这一个单例对象
session:与request范围类似,确保每个session中有一个bean的实例,在sesssion过期后,bean会随之失效
application:bean被定义为在servletContext的生命周期中复用一个单例对象。
websocket:bean被定义为在websocket的生命周期中复用一个单例对象。
global-session:全局作用域,global-session和Portlet应用相关。当你的应用部署在Portlet容器中工作时,它包含很多portlet。如果你想要声明让所有的portlet共用全局的存储变量的话,那么这全局变量需要存在global-session
中。全局作用域与servlet中的session作用域效果相同。
Spring中的Bean默认是单例模式的,框架并没有对bean进行多线程的封装处理。
如果Bean是有状态的 那就需要开发人员自己来进行线程安全的保证,最简单的方法就是改变bean的作用域 把”singleton"改为“protopyte"这样每次请求Bean就相当于是 new Bean()这样就可以保证线程安全了。
Dao会操作数据库Connection、Connection是带有状态的,比如说数据库事务、spring的事务管理器使用
Threadlocal为不同线程维护了一套独立的connection副本,保证线程之间不会相互影响(spring是如何保证事务获取同一个Connection的)
不要在bean中声明任何有状态的实例变量或类变量,如果必须如此,那么就使用Threadlocal把变量变为私有线程有的,如果bean的实例变量或类变量需要在多个线程之间共享,那么就只能使用synchronized、lock、CAS等这些实现线程同步的方法了。
简单工厂:由一个工厂类根据传入的参数,动态决定应该创建哪一个产品类
工厂方法:
单例模式:保证一个类仅有一个实例,并提供了一个访问它的全局访问点
适配器模式
装饰模式:动态地给一个对象添加一些额外的职责。就是增加功能来说,Decorator模式相比生成子类更为灵活。
wrapper
,另一种是类名中含有Decorator
。动态代理:
观察者模式:
策略模式:
在使用Spring框架时,可以有两种使用事务的方式,一种是=编程式的,一种是申明式的,@Transactiona注解就是申明式的。
首先,事务这个概念是数据库层面的,Spring只是基于数据库中的事务进行了扩展,以及提供一些能让程序员更加方便操作事务的方式。
比如我们可以提通过在某个方法上增加了@Transactionanl
注解,就可以开启事务,这个方法中所有的的sql都会在一个事务中执行,统一超过或失败。
在一个方法上加了@Transactional
注解后,Spring会基于这个类生成一个代理对象,会将这个代理对象作为bean,当在使用这个代理对象的方法时,如果这个方法存在@Transactional
注解,那么代理逻辑就会先把事务的自动提交设置为false,然后再去执行原本的业务逻辑方法,如果执行业务逻辑方法没有出现异常,那么代理逻辑中就会将事务进行提交,如果执行业务逻辑方法出现了异常,那么会将事务进行回滚。
当然,针对哪些异常回滚事务是可以配置的,可以利用@Transactional
注解中的rollbackFor属性进行配置,默认情况下会对RuntimeException
进行回滚。
spring事务隔离级别就是数据库的隔离级别:外加一个默认级别
数据库的配置隔离级别是 Read Committed,而spirng配置的隔离级别是Repeatable Read,请问这时隔离级别是以哪个为准?
以Spring配置的为准,如果spring设置的隔离级别数据库不支持,效果取决于数据库。
多个事务方法相互调用时,事务如何在这些方法间传播
方法A是一个事务的方法,方法A执行过程中调用了方法B,那么方法B有无事务以及方法B对事务的要求不同都会对方法A的事务具体执行造成影响,同时方法A的事务对方法B的事务执行也有影响,这种影响具体是什么就由两个方法所定义的事务传播类型所决定。
REQUIRED
(spring默认的事务传播类型):如果当前没有事务,则自己新建一个事务,如果当前存在事务,则加入这个事务
SUPPORTS
:当前存在事务,则加入当前事务,如果当前没有事务,就以非事务方法执行
MANDATORY
:当前存在事务,则加入当前事务,如果当前事务不存在,则抛出异常
REQUIRES_NEW
:创建一个新事务,如果存在当前事务,则挂起该事务
NOT_SUPPORTED
:以非事务方式执行,如果当前存在事务,则挂起当前事务
NEVER
:不适用事务,如果当前事务存在,则抛出异常
NESTED
:如果当前事务存在,则在嵌套事务中执行,否则REQUIRED的操作一样(开启一个事务)
和REQUIRES_NEW
的区别:
REQUIRES_NEW
是新建一个事务并且新开启的这个事务与原有事务无关,而NESTED
则是当前存在事务时(我们把当前事务称之为父事务)会开启一个嵌套事务(称之为一个子事务)。在NESTED
情况下父事务回滚时,子事务也会回滚,而在REQUIRES_NEW
情况下,原有事务回滚,不会影响新开启的事务和REQUIRED的区别
spring事务的原理AOP,进行了切面增强,那么失效的根本原因是这个AOP不起作用了!常见情况有如下几种
1 、 发生自调用,类里面使用this调用本类的方法(this通常省略),此时这个this对象不是代理类,而是UserService对象本身!
解决办法很简单:让那个this变成UserService的代理类即可。
2、方法不是public的
@Transactional 只能用于public 的方法上,否则事务不会失效,如果要用在非public方法上,可以开启Aspectj代理模式。
3 、数据库不支持事务
4 、没有被sprin管理
5 、异常被吃掉,事务不会回滚(或者抛出的异常没有被定义,默认为RuntimeException)
开启自动装配,只需要在xml配置文件
中定义“autowire"属性。
autowire属性有五种装配的方式:
手动装配:以value或ref的方式明确指定属性值都是手动装配
需要通过ref属性来连接bean
Customer的属性名称是person,Spring会将bean id为person的bean通过setter方法进行自动装配。
Cutomer的属性person的类型为Person,Spring会将Person类型通过setter方法进行自动装配。
Cutomer构造函数的参数person的类型为Person,Spring会将Person类型通过构造方法进行自动装配。
如果有默认的构造器,则通过constructor方式进行自动装配,否则使用byType方式进行自动装配。
@Autowired自动装配bean,可以在字段、setter方法,构造函数上使用。
spring是一个IOC容器,用来管理Bean,使用依赖注入实现控制反转,可以很方便的整合各种框架,提供AOP机制弥补OOP的代码重复问题、更方便将不同类不同方法中的共同处理抽取成切面、自动注入给方法执行,比如日志、异常等
springmvc是spring对web框架的一个解决方案,提供了一个总的前端控制器servlet,用来接受请求,然后定义了一套路由策略(url到handle的映射)及适配执行handle,将handle结果使用视图解析器技术生成视图展现给前端
springboot是spring提供的一个快速开发工具包,让程序员能更方便、更快速的开发spring+springmvc应用,简化了配置(约定了默认配置),整合了一些列的解决方案(starter机制)、redis、mongodb、es、可以开箱即用。
Handler:也就是处理器。它直接应对着MVC中的C也就是Controller层,它的具体表现形式有很多,可以是类,可以是方法。在Controller层中@RequestMapping
标注的所有方法都可以看成一个Handler,只要可以实际处理请求就可以是Handler。
1 、HandlerMapping
initHandlerMappings(context),处理器映射器,根据用户请求的资源url来查找Handler的,在springMVC中会有很多请求,每个请求都需要一个Handler处理,具体接收到一个请求之后使用哪个Handler进行,这个就是HandlerMapping需要做的事。
2 、HandlerAdapter
initHandlerAdapters(context),适配器。因为SpringMVC中的Handler可以是任意形式,只要能处理请求就ok,但是servlet需要的处理方法的结构却是固定的,都是以request和response为参数的方法。如何让固定的Servlet处理方法调用灵活的Handler来进行处理呢?这就是HandlerAdapter要做的事情。
Handler是用来干活的工具;HanderMapping用于根据需要干的活找到相应的工具;HandlerAdapter是使用工具干活的人。
3 、HandlerExceptionResolver
initHandlerExceptionResolvers(context),其它组件都是用来干活的。在干活的过程中难免会出现问题,出问题后咋办?这就需要有一个专门的角色对异常情况进行处理,在SpringMVC中就是HandlerExceptionResolver。具体来说,此组件的作用是根据异常设置ModelAndView,之后再交给rende方法进行渲染。
4 、ViewResolver
initViewResolver(context),ViewResolver用来将String类型的视图名和Local解析为View类型的视图。View是用来渲染页面的,也就是将程序返回的参数填入模板里,生成html(也可能是其他类型)文件。这里就有两个关键问题:使用哪个模板?用什么技术(规则)填入参数?这其实是ViewResolver主要做的工作,ViewResolver需要找到渲染所用的模板和所用的技术(也就是视图的类型)进行渲染,具体的渲染过程则交由不同的视图自己完成。
5 、RequestToViewNameTranslator
initRequestToViewNameTranslator(context),ViewResolver是根据ViewName查找View,但有的Handler处理完后并没有设置ViewName,这时就需要从request获取ViewName了,如何从request中获取ViewName就是RequesToViewNameTranslator要做的事情。RequestToViewNameTranslator再SpringMVC容器里只可以配置一个,所以所有rqeust到ViewName的转换规则都要在一个Translator里面全部实现。
6 、LocalResolver
initLocalResolver(context),解析视图需要两个参数:一是视图名,另一个是Locale。视图名是处理器返回的,Locale是从哪里来的?这就是LocaleResolver要做的事情。LocalResolver用于从reqeust解析出Locale,locale就是zh-cn之类,表示一个区域,有了这个就可以对不同的区域的用户显示不同的结果。SpringMVC主要有两个地方用到了Locale:一层是ViewResolver视图解析的时候,二是用到国际化资源或者主题的时候。
7 、ThemeResolver
iniThemeResolver(context),用于解析主题。springMVC中一个主题对应一个properties文件,里面存放着跟当前主题相关的所有资源、如图片、css样式等。SpringMVC的主题也支持国际化,同一个主题不同区域也可以显示不同的风格。SpringMVC中跟主题相关的类有ThemeResolver、ThemeSource和Theme。主题是通过一系列资源来具体体现的,要得到要给主题的资源,首先要得到资源的名称,这是ThemeResover的工作。然后通过主题名称找到对应的主题(可以理解为一个配置)文件,这是ThemeSource的工作。最后从主题中获取资源就可以了
8 、MultipartResolver
initMultipartResolver(context),用于处理上传请求。处理方法是将普通的rrequest包装成MultipartHttpServletRequest,后者可以直接调用getFile方法获取File,如果上传多个文件,还可以调用getFileMap得到FileName->File结构的Map.此组件中一共有三个方法,作用分别是判断是不是上传请求,将reqeust包装成MultipartHttpServletRequest、处理完后清理上传过程中产生的临时资源
9 、FlashMapManager
initFlashMapManager(context),用来管理FlashMap的,FlashMap主要用在redirect中传递参数。
@import+@Configuration+Spring spi
自动配置类由各个starter提供,使用@Configuration+@Bean定义配置类,放到META-INF/spring。factories下使用Spring spi 扫描META-INF/spring.factories下的配置类
使用@import导入自动装配类。
使用spring+springmvc使用,如果需要引入mybatis等框架,需要到xml中定义mybatis需要的bean
starter就是定义要给starter的jar包,写一个@Configuration配置类、将这些bean定义在里面,然后再starter包的META-INF/spring.factories中写入该配置类,springboot会按照约定来加载该配置类。
开发人员只需要将相应的starter包依赖进应用,进行相应的属性配置(使用默认配置时,不许要配置),就可以直接进行代码开发,使用对应的功能了,比如mybatis-spring-boot-starter,spring-boot-starter-redis
节省了下载安装tomcat,应用也不再需要打war包,然后放到webapp目录下再运行,只需要一个安装了java的虚拟机,就可以直接在上面部署应用程序了。springboot已经内置了tomcat.jar。运行main方法时会去启动tomcat,并利用tomcat的spi机制加载springmvc
优点:
缺点:
面向对象
SQL和ORM的争论,永远都不会终止
开发速度的对比:
开发工作量的对比:
sql优化方面:
对象管理的对比
缓存机制对比:
Mybatis的二级缓存配置都是在每个具体的表–对象映射中进行详细配置,这样针对不同的表可以自定义不同的缓存机制。并且Mybatis可以在命名空间中共享相同的缓存配置和实例,通过Cache-ref来实现。
两者比较:因为Hibernate对查询对象有着良好的管理机制,用户无需关心SQL,所以在使用二级缓存时如果出现脏数据,系统会报出错误并提示。
而mybatis在这一方面,使用二级缓存时需要特别小心。如果不能完全确定数据更新操作的波及范围,避免Cache的盲目使用。否则,脏数据的出现会给系统的正常运行带来很大的隐患。
Hibernate功能强大,数据库无关性好,O/R映射能力强,如果你对Hibernate相当精通,而且对Hibernate进行了适当的封装,那么你的整个项目持久性代码会相当简单,需要写的代码少,开发速度很快,很爽。
Hibernate的缺点就是学习门槛不低,要精通门槛更高,而且怎么设计O/R映射,在性能和对象模型之间如何权衡取得平衡,以及怎样用好Hibernate方面需要你的经验和能力都很强才行。
IBATIS入门简单,即学即用,提供了数据库查询的自动对象绑定功能,而且延续了很好的SQL使用经验,对于没有那么高的对象模型要求的项目来说,相当完美。
IBATIS的缺点就是框架还是比较简陋,功能尚有缺失,虽然简化了数据绑定代码,但是整个底层数据库查询实际还是要自己写的,工作量也比较大,而且不太容易适应快速数据库修改。
#{}
是预编译处理、是占位符,${}
是字符串替换,是拼接符#{}
时,会将sql中的#{}
替换为?,调用PrepareStatement
来赋值;${}
时,就是把${}
替换成变量的值,调用Statement
来赋值${}
的变量替换是在DBMS外,变量替换后,${}对应的变量不会加上单引号
Mybatis只支持针对ParameterHandler
、ResultSetHandler
、StatementHandler
、Executor
这四种接口的插件,Mybatis使用JDK的动态代理,为i需要拦截的接口生成代理对象以实现接口方法拦截功能,每当执行这四种接口对象的方法时,就会进入拦截方法,具体就是InvocationHandler的invoke()方法,拦截那些你指定需要拦截的方法。
编写插件:实现Mybatis的interceptor接口并复写Intercept()
方法,然后在给插件编写注解,指定要拦截哪一个接口的哪些方法即可,在配置文件中配置编写的插件
索引用来快速地寻找那些具有特定值的记录。如果没有索引,一般来说执行查询时遍历整张表。
索引的原理:就是把无序的数据变成有序的查询。
都是B+树的数据结构
优势:
劣势:
innoDB中一定有主键,主键一定是聚簇索引,不手动设置、则会使用unique索引,没有unique索引,则会使用数据库内部的一个行的隐藏id来当作主键索引。在聚簇索引之上创建的索引称之为辅助索引,辅助索引访问数据总是二次查找,非聚簇索引都是辅助索引,想复合索引、前缀索引、唯一索引、辅助索引叶子节点存储的不再是行的物理位置、而是主键值。
MyISM使用的是非聚簇索引,没有聚簇索引,非聚簇索引的两颗B+树上看上去没什么不同,节点的结构完全一致只是存储的内容不同而已,主键索引B+树的节点存储了主键,辅助索引B+树存储了辅助键。表数据存储在独立的地方,这两颗B+树的叶子节点都使用一个地址指向真正的表数据,对于表数据来说,这两个键没有任何差别。由于索引树是独立的,通过辅助键检索无需访问主键的索引树。
如果设计到大数据量的排序,全表扫描、count之类的操作的话,还是MyISAM占优势些,因为索引所占空间小,这些操作是需要在内存中完成的
索引的数据的数据结构和具体存储引擎的实现有关,在MySQL中使用较多的索引有hash索引,B+树索引等,innoDB存储引擎的默认索引实现为:B+树索引。对于哈希索引来说,底层的数据结构就是哈希表,因此在绝大多数需求为单条记录查询的时候,可以选择哈希索引,查询性能最快;其余大部分场景,建议选择BTree索引。
B+树:
哈希索引:
哈希索引就是采用一定的哈希算法,把键值换算成新的哈希值,检索时不需要类似B+树那样从根节点到叶子节点逐级查找,只需一次哈希算法即可立刻定位到相应的位置,速度非常快。
如果时等值查询,那么哈希索引明显有绝对优势,因为只需要经过一次算法即可找到相应的键值;前提是键值都是唯一的。如果键值不是唯一的,就需要先找到该键值所在位置,然后再根据链表往后扫描,直到找到相应的数据;
若果是范围查询检索,这时候哈希索引就毫无用武之地了,因为原先是有序的键值,经过哈希算法后,有可能变成不连续的了,就没办法再利用索引完成范围查询检索。
哈希索引也没办法利用索引完成排序,以及like‘xxx%’这样的部分模糊查询(这种部分模糊查询,其实质上也是范围查询);
哈希索引也不支持多列联合索引的最左匹配规则。
B+树索引的关键字检索效率比较平均,不像B树那样波动幅度大,在有大量重复键值情况下,哈希索引的效率也是极低的,因为存在哈希碰撞问题。
查询更快、占用空间小
不适合建立索引的
基于锁的属性分类:共享锁、排他锁
基于锁的粒度分类:行级锁(INNODB)、表级锁(INNODB、MYISAM)、页级锁(BDB引擎)、记录所、间隙锁、临建锁。
基于锁的状态分类:意向共享锁,意向排他锁。
共享锁又称读锁,简称S锁:当一个事务为数据加上读锁之后,其他事务只能对该锁加读锁,而不能对数据加写锁,直到所有的读锁释放之后其他事务才能对其进行加持写锁。共享锁的特性主要是为了支持并发的读取数据,读取数据的时候不支持修改,避免出现重复读的问题。
排他锁又称写锁,简称X锁:当一个事务为数据加上写锁时,其他请求将不再为数据加任何锁,直到该锁释放之后,其他事务才能对数据进行加锁。排他锁的目的是在数据修改时,不允许其他人同时修改,也不允许其他人读取,避免了出现脏数据和脏读的问题。
表锁是指上锁的时候锁住的是整个表,当下一个事务访问该表的时候,必须等前一个事务释放了锁才能进行对表进行访问:
特点:粒度大、加锁简单、容易冲突
行锁是指上锁的时候锁住的是表的某一行或多行记录,其他事务访问同一张表时,只有被锁住的记录不能访问,其他的记录可正常访问
特点:粒度小、加锁比表锁麻烦,不容易冲突,相比表锁支持的并发要高。
记录锁也属于行锁中的一种,只不过记录锁的范围只是表中的某一条记录,记录锁是说事务在加锁后锁住的只是表的某一条记录。
精准条件名中,并且名中的条件字段是唯一索引
加了记录锁之后数据可以避免数据在查询的时候被修改的重复读问题,也避免了在修改的事务未提交前被其他事务读取的脏读问题。
页锁
-页级锁是MySQL中锁定粒度介于行级锁和表级锁中间的一种锁。表级锁速度快,但冲突多,行级冲突少,但速度慢。所以取了折衷的页级,一次锁定相邻的一组记录。
特点:开销和加锁时间界于表锁和行锁之间;会出现死锁:锁定粒度界于表锁和行锁之间,并发读一般。
间隙锁(Gap Lock)
属于行锁中的一种,间隙锁是在事务加锁后其锁住的是表记录的某一个区间,当表的相邻ID之间出现空隙则会形成一个区间,遵循左开右闭原则。
范围查询并且查询未名中记录,查询条件必须名中索引、间隙锁只会出现在REPEATABLE_READ(重复读)的事务隔离级别中。
触发条件:防止幻读问题,事务并发的时候,如果没有间隙锁,就会发生如下的问题,在同一个事务里,A事务的两次查询出的结果会不一样。
比如表里面的数据ID为 1,4,5,7,10,那么会形成以下几个间隙区间-n-1区间,1-4区间、7-10区间、10-n区间(-n:代表负无穷大,n代表正无穷大)
也属于行锁的一种,并且它是INNODB的行锁默认算法,总结来说它就是记录锁和间隙锁的组合,临建锁会把查询出来的记录锁住,同时也会把该范围查询内的所有空隙空间也会锁住,再之它会把相邻的下一个区间也会锁住
触发条件:范围查询并命中,查询命中了索引。
结合记录锁和间隙锁的特性,临建锁避免了在范围查询时出现脏读、重复度、幻读问题。加了临建锁之后,在范围区间内数据不允许被修改和插入
如果当事务A加锁成功之后就设置一个状态告诉后面的人,已经有人对表里的行加了一个排他锁了,你们不能对整个表加共享锁或排它锁了,那么后面需要对整个表加锁的人只需要获取这个状态就知道自己是不是可以对表加锁,避免了对整个索引树的每个节点扫描是否加锁,而这个状态就是意向锁。
当一个事务试图对整个表进行加共享锁之前,首先需要获得这个表的意向共享锁。
当一个事务试图对整个表进行加排它锁之前,首先需要获得这个表的意向排它锁。
执行计划就是sql的执行查询顺序,以及如何使用索引查询,返回的结果集的行数
1 、 id:是一个有顺序的编号,是查询的顺序号,有几个select就显示几行。id的顺序是按select出现的顺序增长的。id列的值越大执行优先级越高越先执行,id列的值相同则从上往下执行,id列的值为NULL最后执行。
2 、selectType 表示查询中每个select子句的类型
3 、table:表示该语句查询的表
4 、type:优化sql的重要字段,也就是我们判断sql性能和优化程度的重要指标。他的取值类型范围:
执行效率 ALL < index < range < ref < eq_ref < const
5 、possible_key: 它表示MySQL在执行该sql语句的时候,可能用到的索引信息,仅仅是可能,实际不一定会用到。
6 、 key:此字段是mysql在当前查询时所真正使用到的索引。它是possible_keys的子集
事务的基本特性和隔离级别
原子性:指的是一个事务中的操作要么全部成功,要么全部失败。
一致性:指的是:数据库总是从一个一致性的状态转换到另外一个一致性的状态。比如A转账给B100块钱,假设A只有90块,支付之前我们数据库里的数据都是符合约束的,但是如果事务执行成功了,我们的数据库数据就破坏约束了,因此事务不能成功,这里我们说事务提供了一致性的保证。
隔离性:指的是一个事务的修改在最终提交前,对其他事务是不可见的。
持久性指的是:一旦事务提交,所做的修改就会永久保存到数据库中。
隔离性有四个隔离级别,分别是:
脏读(Drity Read):某个事务已更新一份数据,另一个事务在此时读取了同一份数据,由于某些原因,前一个RollBack了操作,则后一个事务所读取的数据就会是不正确的。
不可重复读(Non-repeatable read):在一个事务的两次查询之中数据不一致,这可能是两次查询过程中间插入了一个事务更细的原有的数据。
幻读(Phantom Read):在一个事务的两次查询中数据笔数不一致,例如有一个事务查询了几列(Row)数据,而另一个事务却在此时插入了新的几列数据,先前的事务在接下来的查询中,就会发现有几列数据是它先前所没有的。
关心过业务系统里面的sql耗时吗?统计过慢查询吗?对慢查询都怎么优化过?
在业务系统中,除了使用主键进行的查询,其他的都会在测试库上测试其耗时,慢查询的统计主要由运维在做,会定期将业务中的慢查询反馈给我们。
慢查询的优化首先要搞明白慢的原因是什么? 是查询条件没有名中索引? 是load了不需要的数据列?还是数据量太大?
所以优化也是针对这三个方向来的,
A原子性由undo log日志保证,它记录了需要回滚的日志信息,事务回滚撤销已经执行成功的sql
C一致性由其他三大特性保证、程序代码要保证业务上的一致性
I隔离性由MVCC来保证
D持久层由内存+redo log来保证,mysql修改数据同时在内存和redo log记录这次操作,宕机的时候可以从redo log恢复
InnoDB redo log 写盘,InnoDB 事务进入 prepare状态。
如果前面prepare 成功,binlog 写盘,再继续将事务日志持久化到binlog,如果持久化成功,那么 InnoDB 事务则进入commit状态 redo log里面写一个 commit 记录)
redolog的刷盘会在系统空闲时进行
多版本并发控制:读取数据数据时通过一种类似快照的方式将数据保存下来,这样读锁和写锁就不冲突了,不同的事务session会看到自己特定版本的数据,版本链
MVVC只在READ COMMITTED
和REPEATABLE READ
两个隔离级别下工作。其他两个隔离级别和MVCC不兼容,因为READ UNCOMMITTED9
总是读取最新的数据行,而不是符合当前事务版本的数据行。而SERIALIZABLE则会对所有读取的行都加锁。
聚簇索引记录中有两个必要的隐藏列:
trx_id:
用来存储每次对某条聚簇索引记录进行修改的时候的事务id。
roll_pointer:
每次对哪条聚簇索引记录有修改的时候,都会把老版本写入undo日志中。这个roll_pointer就是存了一个指针,它指向这条聚簇索引记录上一个版本的位置,通过它来获得上一个版本的记录信息。(注意插入操作的undo日志没有这个属性,因为它没有老版本。
已提交读和可重复读的区别就在于它们生成ReadView策略不同。
readview
,readView
维护当前活动的事务id,即未提交的事务id,排序成一个数组已提交读隔离级别下的事务在每次查询的开始都会生成一个独立的ReadView,而可重复读隔离级别则在第一次读的时候生成一个ReadView,之后的读都复用之前的ReadView。
这就是Mysql的MVVC,通过版本链,实现多版本,可并发读-写,写-读。通过ReadView生成策略的不同实现不同的隔离级别
MySQL主从同步的过程:
MySQL的主从复制中有三个线程:master(binlog dumo thread)、slave(I/0 thread、SQL thread),Master一条线程和Slave中的两条线程。
注意:从节点使用binlog文件+position偏移量来定位主从同步的位置,从节点会保存其已接收到的偏移量,如果从节点发生宕机重启,则会自动从position的位置发起同步。
由于mysql默认的复制方式是异步的,主库把日志发送给从库后不关心从库是否已经处理,这样会产生一个问题就是假设主库挂了,从库处理失败了,这时候从库上升为主库后,日志就丢失了。由此产生两个概念。
全同步复制
半同步复制
MyISAM:
innoDb:
普通索引:允许被索引的数据列包含重复的数据
唯一索引:可以保证数据记录的唯一性
主键:是一种特殊的唯一索引,在一张表中只能定义一个主键索引,主键用于唯一标识一条记录,使用关键字primary key来创建。
联合索引:索引可以覆盖多个数据列,如像INDEX(columnA,columnB)索引。
全文索引:通过建立倒排索引,可以极大的提升检索效率,解决判断字段是否包含的问题,是目前搜索引擎使用的一种关键技术。可以通过ALTER TABLE table_name ADD FULLTEXT(column);创建全文索引
优点:
索引可以极大的提高数据的查询速度。
通过使用索引,可以在查询的过程中,使用优化隐藏器,提高系统的性能。
缺点:
RDB:Redis DataBase
在指定的时间间隔内将内存中的数据集快照写入磁盘,实际操作过程就是fork一个子进程,先将数据集写入临时文件,写入成功后,再替换之前的文件,用二进制压缩存储。
优点:
缺点:
AOF:Append Only File
以日志的形式记录服务器所处理的每一个写、删除操作,查询操作不会记录,以文本的方式记录,可以打开文件看到详细的操作记录。
优点:
缺点:
AOF文件比RDB更新频率高,优先使用AOF还原数据
AOF比RDB更安全也更大
RDB性能比AOF好
如果两个都配置了优先加载AOF
Redis是key-value数据库,我们可以设置Redis中缓存的过期时间。Redis的过期策略就是指当Redis中缓存的key过期了,Redis如何处理。
(expires字典会保存所有设置了过期时间的key的过期时间数据,其中,key是指向键空间中的某个键的指针,value是改键的毫秒精度的UNIX时间戳表示过期的时间。键空间是指该Redis集群中保存的所有的键。)
Redis中同时使用了惰性过期和定期过期两种过期策略。
Redis基于Reactor模式开发了网路事件处理器,这个处理器叫做文件事件处理器 file event handler。这个文件事件处理器,它是单线程的,所以Redis才叫做单线程的模型,它采用IO多路复用机制来同时监听多个Socket,根据Socket上的事件类型来选择对应的事件处理器来处理这个事件。可以实现高性能网络通信模型,又可以跟内部其他单线程的模块进行对接,保证了Redis内部的线程模型的简单性。
文件事件处理器
的结构包含4个部分:多个Socket、IO多路复用程序、文件事件分配器以及事件处理器(命令请求处理器、命令回复处理器、连接应答处理器等)。
多个Socket可能并发的产生不同的操作,每个操作对应不同的文件事件,但是IO多路复用程序会监听多个Socket,会将Socket放入一个队列中排队,每次从队列中取出一个Socket给事件分派器,事件分派器把Socket给对应的事件处理器。
然后一个Socket的事件处理完之后,IO多路复用程序会将队列中的下一个Socket给事件分派器。文件事件分派器会根据每个Socket当前产生的事件,来选择对应的事件处理器来处理。
单线程快的原因:
缓存雪崩、缓存穿透、缓存击穿
缓存雪崩是指缓存同一时间大面积的失效,所以,后面的请求都会落到数据库上,造成数据库短时间内承受大量请求而崩掉。
解决方案:
缓存穿透是指缓存和数据库中都没有的数据,会导致所有的请求都落到数据库上,造成数据库短时间内承受大量请求而崩掉。
解决方法:
缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。和缓存雪崩不同的是,缓存击穿指并发查同一条数据,缓存雪崩式不同数据都过期了,很多数据都查不到从而查数据库。
解决方法:
1、事务开始
MULTI命令的执行,标识着一个事务的开始。MULTI命令会将客户端状态的flag属性中打开REDIS_MULTI标识来完成的。
2、命令入队
当一个客户端切换到事务状态后,服务器会根据这个客户端发送来的命令来执行不同的操作。如果客户端发送的命令为MULTI、EXEC、WATCH、DISCARD中的一个,立即执行这个命令,否则将命令放入一个事务队列里面,然后向客户端返回QUEUED回复。
事务队列是按照FIFO的方式保存入队的命令。
3、事务执行
客户端发送EXEC命令,服务器执行EXEC命令逻辑
redis不支持事务回滚机制,但是它会检查每一个事务中的命令是否错误。
Redis事务不支持检查哪些程序员自己的逻辑错误。例如对String类型的数据库执行对HashMap类型的操作!!!
主从
哨兵模式:
sentinel,哨兵是redis集群中非常重要的一个组件,主要有一下功能:
哨兵用于实现redis集群的高可用,本身也是分布式的,作为一个哨兵集群去运行,互相协同工作。
redis Cluster 是一种服务端Sharding技术,3.0版本开始正式提供。采用==slot(槽)==的概念,一共分成16384个槽。将请求发送到任意节点,接收到请求的节点会将查询请求发送到正确的节点上执行
方案说明:
在redis cluster架构下,每个redis要开放两个端口号,比如一个是6379,另外一个就是加1w的端口号,比如16379.
16379端口号是用来进行节点间通信的,也就是cluster bus的通信,用来进行故障检测、配置更新、故障转移授权。cluster bus用了另外一种二进制的协议,gossip协议,用于节点间进行高效的数据交换,占用更少的网络带宽和处理时间。
优点:
缺点:
Redis Sharding是Redis Cluster出来之前,业界普遍使用的多Redis实例集群方法。其主要思想是采用哈希算法将Redis数据的Key进行散列,通过hash函数,特定的key会映射到特定的Redis节点上。java redis客户端驱动jedis,支持Redis Sharing功能,即ShardedJedis以及结合缓存池的ShardegjedisPool
优点:
优势在于非常简单,服务端的redis实例彼此独立,相互无关联,每个Redis实例像单服务器一样运行,非常容易线性扩展,系统的灵活性很强。
缺点:
由于sharding处理放到客户端,规模进一步扩大时给运维带来挑战。
客户端sharding不支持动态增删改节点。服务端redis实例群拓扑结构有变化时,每个客户端都需要更新调整。连接不能共享,当应用规模增大时,资源浪费制约优化
通过执行slaveof命令或设置slaveof选项,让一个服务器去复制另一个服务器的数据。主数据库可以进行读写操作,当写操作导致数据变化时会自动将数据同步给从数据库。而从数据库一般只是只读的,并接受主数据库同步过来的数据。一个主数据库可以拥有多个从数据库,而一个从数据库只能拥有一个主数据库。
全量复制:
bgrewriteaof
,也会带来额外的消耗部分复制:
如果从节点保存的runid与主节点现在的runid相同,说明主从节点之前同步过,主节点会继续尝试使用部分复制(到底能不能部分复制还要看offset和复制积压缓冲区的情况)
如果从节点保存的runid与主节点现在的runid不同,说明从节点在断线前同步的Redis节点并不是当前的主节点,只能进行全量复制。
CAP理论
Consistency(一致性):
Availability(可用性):
Partition Tolerance(分区容错性):
CP和AP:分区容错是必须保证的,当发生网络分区的时候,如果要继续服务,那么强一致性和可用性只能2选1.
BASE是Basically Available(基本可用)、soft state(软状态)和Eventually consisten(最终一致性)
BASE理论是对CPA中一致性和可用性权衡的结果,其来源于对大规模互联网系统分布式实践的总结,是基于CAP定理逐步演化而来的。BASE理论的核心思想是:即使无法做到强一致性,但每个应用都可以根据自身业务特定,采用适当的方式来使系统达到最终一致性
基本可用:
软状态:数据同步允许一定的延迟
最终一致性:系统中所有的数据副本,在经过一段时间的同步后,最终能够达到一个一致的状态,不要求实时
1、轮循法
2、随机法
3、源地址哈希法
4、加权轮询法
5、加权随机法
6、随机连接数法
类型:
DNS方式实现负载均衡
硬件负载均衡:F5和A10
软件负载均衡:
Nginx、HAproxy、Lvs。其中的区别:
1、采用无服务状态,抛弃session
2、存入cookie(有安全风险)
3、服务器之间进行session同步,这样可以保证每个服务器上都有全部的session信息,不过当服务器数量比较多的时候,同步是会有延迟甚至同步失败;
4、IP绑定策略
使用Nginx(或其他复杂负载均衡硬件)中的IP绑定策略,同一个IP只能在指定的同一个机器访问,但是这样做失去了负载均衡的意义,当挂掉一台服务器的时候,会影响一批用户的使用,风险很大;
5、使用Redis存储
把Session放到Redis中存储,虽然架构上变得复杂,并且访问需要多访问一次Redis,但是这种方案带来的好处也是很大的:
RPC:在本地调用远程的函数,远程过程调用,可以跨语言实现, httpClient
RMI:远程方法调用,java中用于实现RPC的一种机制,RPC的java版本,是J2EE的网路调用机制,跨JVM调用对象的方法,面向对象的思维方式
直接或间接实现接口 java.rmi.Remove 成为存在于服务端的远程对象,供客户端访问并提供一顶的服务
远程对象必须实现java.rmi.server.UniCastRemoteObject类,这样才能保证客户端访问获得远程对象时,该远程对象将会把自身的一个拷贝以Socket的形式传输给客户端。此时客户端所获得的这个拷贝称为”存根“,而服务器端本身已存在的远程对象则称之为”骨架“,其实此时的存根是客户端的一个代理,用于服务器端的通信,而骨架也可以认位是服务器端的一个代理,用于接收客户端的请求之后调用远程方法来响应客户端的请求。
uuid
优点:代码简单,性能好(本地生成,没有网络消耗),保证唯一(相对而言,重复概率极低可以忽略)
缺点:
数据库自增序列
单机模式:
优点:
缺点:
优点:解决了ID生成的单点问题,同时平衡了负载。
缺点:
leaf-sagmnet
-采用每次获取一个ID区间段的方式来解决,区间段用完之后再去数据库获取新的号段,这样一来就可以大大减轻数据库的压力
核心字段:biz_tag, max_id ,step
biz_tag用来区分业务,max_id表示该biz_tag目前所被分配的ID号段的最大值,step表示每次分配的号段长度,原来每次获取ID都需要访问数据库,现在只需要把Step设置的足够合理如1000,那么现在可以在1000个ID用完之后再去访问数据库
优点:
缺点:
对buffer:将获取一个号段的方式优化成获取两个号段,在一个号段用完之后不用立马去更新号段,还有一个缓存号段备用,这样能够有效解决这种冲突问题,而且采用双buffer的方式,在当前号段消耗了10%的时候就去检查下一个号段有没有准备好,如果没有准备好就去更新下一个号段,当前号段用完了就切换到下一个已经缓存好的号段去使用,同时在下一个号段消耗到10%的时候,又去检测下一个号段有没有准备好,如此往复。
优点:
缺点
segment号段长度是固定的,业务量大时可能会频繁更新号段,因为原本分配的号段会一下用完,如果号段长度设置的过长,但凡缓存中有号段没有消耗完,其他节点重新获取的号段与之前相比可能跨度会很大,动态调整Step
基于redis、mongodb、zk等中间件生成
雪花算法:
生成一个64bit的整性数字
第一位符号位固定为0,41位时间戳,10位workid,12位序列号
位数可以有不同实现
优点
缺点
需要这个锁独立于每一个服务之外,而不是在服务里面。
数据库:利用主键冲突控制一次只有一个线程能获取锁,非阻塞、不可重入、单点、失效时间
Zookeeper分布式锁:
zk通过临时节点,解决了死锁的问题,一旦客户端获取到锁之后突然挂掉(Session连接断开),那么这个临时节点就会自动删除,其他客户端自动获取锁。临时顺序节点解决惊群效应。
Redis分布式锁:setNX,单线程处理网络请求,不需要考虑并发安全性
所有服务节点设置相同的key,返回为0,则锁获取失败
setnx问题:
删除锁:判断线程唯一标志,再删除
可重入性及锁续期没有 实现,通过redisson解决(类似AQS的实现,看门狗机制)
redlock:意思的机制都只操作单节点、即使Redis通过sentinel保证高可用,如果这个master节点由于某些原因发生了主从切换,那么就会出现锁丢失的情况(redis同步设置可能数据丢失)。redlock从多个节点申请锁,当一半以上节点获取成功,锁才算获取成功,redission有相应的实现。
XA规范:分布式事务规范,定义了分布式事务模型
四个角色:事务管理器(协调者TM)、资源管理器(参与者RM)、应用程序AP、通信资源管理器CRM
全局事务:一个横跨多个数据库的事务,要么全部提交、要么全部回滚
JTA事务时java对XA规范的实现,对应JDBC的单库事务。
两阶段协议:
第一阶段(prepare):每个参与者执行本地事务但不提交,进入ready状态,并通知协调者已经准备就绪。
第二阶段(commit)当协调者确认每个参与者都read后,通知参与者进行commit操作,如果有参与者fail,则发送rollback指令,各参与者做回滚。
问题:
三阶段协议:主要是针对两阶段的优化,解决了2PC单点故障的问题,但是性能问题和不一致问题仍然没有根本解决
引入了超时机制解决参与者阻塞的问题,超时后本地提交,2pc只有协调者有超时机制
第一阶段:CanCommit阶段,协调者询问事务参与者,是否有能力完成此次事务。
如果都返回yes,则进入第二阶段
有一个返回no或等待响应超时,则中断事务,并向所有参与者发送abort请求
第二阶段:precommit阶段,此时协调者会向所有的参与者发送precommit请求,参与者收到后开始执行事务操作。参与者执行完事务操作后(此时属于未提交事务的状态),就会向协调者反馈”Ack“表示我已经准备好提交了,并等待协调者的下一步指令。
第三阶段:DoCommit阶段,在阶段二中如果所有的参与者节点都返回Ack,那么协调者就会从”预提交状态“转变为”提交状态“。然后向所有的参与者节点发送”doCommit"请求,参与者节点在收到提交请求后就会各自执行事务提交操作,并向协调者节点反馈“Ack”消息,协调者收到所有参与者的Ack消息后完成事务。相反,如果有一个参与者节点未完成PreCommit的反馈或者反馈超时,那么协调者都会向所有的参与者节点发送abort请求,从而中断事务。
TCC(补偿事务):Try、Confirm、Cancel
TCC模型对业务的侵入性较强,改造的难度较大,每个操作都需要try、confirm、cancel三个接口实现confirm和cancel接口还必须实现幂等性。
消息队列的事务信息: