Java

位运算

在 Java 中,整数通常使用补码表示。正数的补码表示和其原码相同,而负数的补码是其原码的反码加1。

推荐文章:Java 位运算(移位、位与、或、异或、非)

  • 左移( << ):无论正数还是负数,都是在低位补0
  • 右移( >> ):正数在高位补0,对于负数在高位补1
  • 无符号右移( >>> ):无论正数还是负数,都在高位插入0
  • 位与( & ) :第一个操作数的的第n位于第二个操作数的第n位如果都是1,那么结果的第n为也为1,否则为0
  • 位或( | ):第一个操作数的的第n位于第二个操作数的第n位只要有一个是1,那么结果的第n为也为1,否则为0
  • 位异或( ^ ):第一个操作数的的第n位于第二个操作数的第n位相反,那么结果的第n为也为1,否则为0
  • 位非( ~ ):操作数的第n位为1,那么结果的第n位为0,反之。

注意:位非是一元操作符,其他是二元操作符

抽象类和接口的区别

  1. 抽象类和接口都不能直接实例化。

  2. 抽象类可以有main方法,接口则不可以有。

  3. 抽象类可以有构造方法,接口则不可以有。

  4. 一个类可以通过implements实现多个接口,但是只能通过extends继承一个抽象类。

  5. 抽象类的成员变量可以是变量和常量;接口的成员变量只能是常量,默认修饰符是:public static final

  6. 抽象类的成员方法可以是抽象方法(不能用privatestaticfinal修饰)和非抽象方法;

  7. 接口的成员方法在 JDK 1.7 时只能定义抽象方法,JDK 1.8 时可以定义默认方法和静态方法,JDK 1.9时可以定义私有方法。默认修饰符是:public

上转型对象和下转型对象

推荐文章:Java对象类型向上转型和向下转型

上转型:父类引用指向子类对象

  • 父类引用变量可以访问子类中属于父类的属性和方法(如果子类重写了父类的方法,则调用子类的方法),但是不能访问子类独有的属性和方法。
  • 作用:减少重复的代码,对象实例化的时候可以根据不同需求实例化不同的对象。

下转型:子类引用指向父类对象

  • 只有当这个父类对象原本就是子类对象通过向上转型得到的时候才能够成功转型。
  • 作用:向上转型时 ,会丢失子类独有的属性和方法;可以通过向下转型再重新访问

访问权限控制符有哪些

访问权限控制符 本类 包内 包外子类 任何地方
public
protected ×
无(default) × ×
private × × ×

重写与重载

推荐文章:Java—重写与重载的区别

区别:

  1. 重写实现的是运行时的多态,而重载实现的是编译时的多态。
  2. 重写的方法参数列表必须相同;而重载的方法参数列表必须不同(参数类型不同、参数个数不同或者二者都不同)。
  3. 重写的方法的返回值类型只能是父类类型或者父类类型的子类,而重载的方法对返回值类型没有要求。
重载 重写
范围 一般在一个类中 父类与子类之间
方法名 相同 相同
访问权限 可以不同 可以不同(但是子类的可见性不能比父类的低)
方法返回值 可以不同 相同
参数类型 不同(充分条件) 相同
参数个数 不同(充分条件) 相同

注意:

  • 子类中重写方法的访问修饰符的限制 >= 父类中被重写方法的访问修饰符(public > protected > default > private)
  • 重写方法一定不能抛出新的检查异常或者比被重写方法申明更加宽泛的检查型异常

子类初始化的顺序

推荐文章:Java类的初始化顺序

① 父类静态代码块和静态变量。

② 子类静态代码块和静态变量。

③ 父类普通代码块和普通变量。

④ 父类构造方法。

⑤ 子类普通代码块和普通变量。

⑥ 子类构造方法。

扩展:

  • 静态代码块和静态变量只加载一次

  • 静态加载完后如果有main方法会先执行main方法

  • 静态变量和静态代码块之间,普通变量和普通代码块之间的顺序取决于它们在类中出现的先后顺序。

ArrayList

自动扩容机制:

推荐文章:ArrayList扩容机制(基于jdk1.8)

  1. 使用无参构造方法创建 ArrayList 时,实际上初始化赋值的是一个空数组。当向数组中添加第一个元素时,数组容量扩容为10;使用有参构造方法创建 ArrayList 时,容量是size。
  2. 如果最小扩容量大于数组的容量则需要调用grow方法进行扩容。
  3. 按照当前容量的1.5倍左右进行扩容(oldCapacity为偶数就是1.5倍,否则是1.5倍左右)
  4. 扩容是通过Arrays.copyOf(elementData, newCapacity)方法将原数组的内容复制到更大容量的新数组中,底层实际调用的是System.arraycopy(...)方法。

ArrayList和LinkedList的区别:

  1. ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全;
  2. ArrayList 底层是 Object 动态数组;LinkedList 底层是 双向链表
  3. 插入和删除受元素位置的影响:ArrayList需要考虑元素的位置移动和扩容,LinkedList则需要遍历到指定位置再操作;
  4. 当随机访问元素(get和set操作)时,ArrayList比LinkedList的效率更高,因为LinkedList是线性的数据存储方式,所以需要移动指针遍历链表,而 ArrayList可以通过数组下标索引

ArrayList和Vector的区别:

  1. ArrayList 是 List 的主要实现类,Vector 是 List 的古老实现类,两者几乎是完全相同的。

  2. Vector 是同步类(synchronized),线程安全的,而 ArrayList 不是,因此开销比 ArrayList 要大,效率也不如ArrayList。

  3. Vector 默认扩容是2倍,如果有指定增长因子,则新容量=之前的容量+增长因子,而ArrayList 扩容是1.5倍。

  4. ArrayList 存储数据的 Object 数组使用了 transient 关键字,而 Vector 的 Object 数组没有。(大概意思:被 transient 修饰的成员变量,在序列化的时候其值会被忽略,在被反序列化后, transient 变量的值被设为初始值, 如 int 型的是 0,对象型的是 null。)

  5. ArrayList 调用无参构造器创建对象时,是在调用 add() 方法才对 ArrayList 的默认容量进行初始化,而Vector 在调用构造器时就对容量进行了初始化。

HashMap(待)

推荐文章:

HashMap 的底层结构和原理

老生常谈,HashMap的死循环【基于JDK1.7】

【Java集合】HashMap的tableSizeFor()源码详解

HashMap面试,看这一篇就够了

【精讲】深入剖析HashMap的底层原理

HashMap源码研究——源码一行一行的注释

红黑树,扩容机制,2的幂次方的原因

基本概念:

  1. HashMap线程不安全的,效率高;可以存储null的key和value。
  2. 桶(bucket)是存储元素的容器。哈希表通过哈希函数将键映射到一个特定的位置,该位置通常被称为桶。每个桶可以包含一个元素,也可以包含多个元素,桶内的数据结构可以是链表或红黑树结构。

JDK 7 和 JDK 8 的区别:

  • 数据结构:
    • JDK 7是数组+链表(即为链地址法),内部数据节点是Entry类
    • JDK 8是数组+链表+红黑树(链表长度>=8),内部数据节点是Node类
  • 插入位置:
    • JDK 7:新添加的元素作为链表的头结点,新元素指向旧元素(多线程下扩容后可能导致循环链表的问题)
    • JDK 8:新添加的元素作为链表的尾结点或树的叶子结点,旧元素指向新元素(多线程下插入时可能导致数据丢失的问题)
  • 扩容机制:
    • JDK 7扩容条件:元素个数 > 容量(16) * 加载因子 (0.75) && 插入的数组位置有元素存在
    • JDK 8扩容条件 :元素个数 > 容量 (16) * 加载因子(0.75)

数组长度是2的幂次方的原因及实现原理:

实现原理tableSizeFor()方法:

  • 2的整数幂用二进制表示都是最高有效位为1,其余全是0
  • 该方法的核心思想是,先将最高有效位以及其后的位都变为1,最后再+1,这样就会得到最高有效位的前一位是1,后面全是0
  • 开始移位前先将容量-1,是为了避免给定容量已经是8,16这样2的幂次方时,容量变成两倍
  • 最高有效位右移一位再或运算,可以得到最高位连续的两个1,右移两位再或运算,有四位变成了1,右移八位和十六位后可以保证32位的int类型整数最高有效位之后的位都能变为1
  • 最后再+1就能得到大于等于大于等于initialCapacity的第一个2的幂次方

HashMap的默认负载因子:

默认负载因子是0.75。

  • 负载因子越大密度越大,发生碰撞的几率越高,数组中的链表越容易长,造成查询或插入时的比较次数增多,性能会下降。
  • 负载因子越小,就越容易触发扩容,数据密度也越小,意味着发生碰撞的几率越小,数组中的链表也就越短,查询和插入时比较的次数也越小,性能会更高。但是会浪费一定的内存空间。而且经常扩容也会影响性能。
  • 选择0.75作为默认的负载因子是时间和空间成本上寻求的一种折中选择。

扩容机制:

https://blog.csdn.net/zhuzhianing/article/details/123962898

HashMap为什么线程不安全:
推荐文章:老生常谈,HashMap的死循环【基于JDK1.7】
https://blog.csdn.net/zzu_seu/article/details/106669757
https://blog.csdn.net/swpu_ocean/article/details/88917958

ConcurrentHashMap(待)

https://blog.csdn.net/wuhuayangs/article/details/126049472
https://blog.csdn.net/zycxnanwang/article/details/105424734
https://blog.csdn.net/yunzhaji3762/article/details/113623168
https://blog.csdn.net/wwj17647590781/article/details/118151008

线程池

推荐文章:线程池原理

讲一下ThreadPoolExecutor的几个重要参数:

  • corePoolSize: 核心线程数,定义了最小可以同时运行的线程数量。
  • workQueue: 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在任务缓存队列中。
  • maximumPoolSize: 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。
  • keepAliveTime: 当线程池中的线程数量大于 corePoolSize 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 keepAliveTime才会被回收销毁;
  • unit: keepAliveTime 参数的时间单位。
  • threadFactory: 线程工厂,主要用来创建线程。
  • handler: 任务拒绝策略。

线程池的执行流程:

  • 如果当前线程池中的线程数目小于corePoolSize,则每来一个任务,就会创建一个线程去执行这个任务;
  • 如果当前线程池中的线程数目>=corePoolSize,则每来一个任务,会尝试将其添加到任务缓存队列当中,若添加成功,则该任务会等待空闲线程将其取出去执行;若添加失败(一般来说是任务缓存队列已满),则会尝试创建新的线程去执行这个任务(添加失败的任务);
  • 如果当前线程池中的线程数目达到maximumPoolSize,则会采取任务拒绝策略进行处理;
  • 如果线程池中的线程数量大于 corePoolSize时,如果非线程空闲时间超过keepAliveTime,线程将被终止,直至线程池中的线程数目不大于corePoolSize;如果允许为核心池中的线程设置存活时间,那么核心池中的线程空闲时间超过keepAliveTime,线程也会被终止。

有哪几种任务缓存队列:

推荐文章:线程池的三种缓存队列

  • **ArrayBlockingQueue**:基于数组的先进先出队列,此队列创建时必须指定大小;
  • **LinkedBlockingQueue**:基于链表的先进先出队列,如果创建时没有指定此队列大小,则默认为Integer.MAX_VALUE;
  • SynchronousQueue :它不会保存提交的任务,而是将直接新建一个线程来执行新来的任务。

有哪几种任务拒绝策略:

  • ThreadPoolExecutor.AbortPolicy 丢弃任务并抛出 RejectedExecutionException异常。默认

  • ThreadPoolExecutor.DiscardPolicy 也是丢弃任务,但是不抛出异常。

  • ThreadPoolExecutor.DiscardOldestPolicy 丢弃队列最前面的任务,然后重新尝试执行任务

  • ThreadPoolExecutor.CallerRunsPolicy 由调用execute方法的线程运行被拒绝的任务

线程池有哪些:

推荐文章:四种线程池

  • CachedThreadPool没有核心线程,非核心线程是无界的,空闲线程等待新任务的最长时间是60s,任务队列采用SynchronousQueue,适用于处理大量、耗时少的任务

  • SecudleThreadPool:按照某种特定的计划执行线程中的任务,有核心线程,但也有非核心线程,非核心线程是无界的。任务队列采用DelayedWorkQueue,会将任务排序,适用于执行周期性的任务。

  • SingleThreadPool只有一个核心线程来执行任务,适用于有顺序的任务的应用场景。任务队列采用LinkedBlockingQueue

  • FixedThreadPool:只有核心线程且数量固定,没有非核心线程。任务队列采用LinkedBlockingQueue

如何合理配置线程池的大小:

最佳线程数目 = (线程等待时间与线程CPU时间之比 + 1)* CPU核数

线程等待时间(比如IO时间)所占比例越高,需要越多线程。线程CPU时间所占比例越高,需要越少线程,减少线程上下文的切换。

可以通过控制台输入jconsole,打开图形监控界面,监控线程运行情况。

动态代理

推荐文章:
Java中的代理模式——静态代理以及分析静态代理的缺点
Java中动态代理的两种方式JDK动态代理和cglib动态代理以及区别

三种代理方式之间对比:

  • 静态代理:
    • 实现:手写代理类,一是通过继承委托类的方式实现其子类,重写父类方法;二是与委托类实现共同的一个接口
    • 特点:硬编码效率低,代码重用性不强
  • JDK动态代理
    • 实现:代理类与委托类实现同一接口,主要是通过代理类实现InvocationHandler接口并重写invoke方法来进行动态代理的,在invoke方法中将对方法进行增强处理。
    • 创建动态代理对象:
      • Proxy.newProxyInstance(ClassLoader loader,Class<?>[] instance, InvocationHandler h)
    • 特点:底层使用反射机制进行方法的调用,代码复用率高。只能够代理实现了接口的委托类同时继承了Proxy类。
  • CGLIB动态代理
    • 实现:代理类将被委托类作为自己的父类并为其中的非final委托方法创建两个方法,一个是与委托方法签名相同的方法,它在方法中会通过super调用委托方法;另一个是代理类独有的方法,在代理方法中,它会判断是否存在实现了MethodInterceptor接口的对象,若存在则将调用intercept方法对委托方法进行代理。
    • 创建动态代理对象:
      • Enhancer enhancer = new Enhancer();
      • 设置被代理类:enhancer.setSuperclass(Car.class);
      • 设置回调函数:enhancer.setCallback(new MyMethodInterceptor());
      • 创建代理对象:Car car = (Car) enhancer.create();
    • 特点:底层方法的调用并不是通过反射来完成的,而是将方法全部存入一个数组中,通过数组索引直接进行方法调用(FastClass机制),可以在运行时对类或者是接口进行增强操作,且被代理类无需实现接口。

动态代理不能对final类以及final、static、private方法进行代理,default和public方法可以。

Spring底层是如何选择哪种动态代理方式:

Spring会根据具体的Bean是否有实现接口去选择动态代理方式,如果有实现接口,使用的是JDK动态代理的方式,如果没有实现接口,使用的是CGLIB动态代理的方式。

Spring

Spring的IoC理解

IoC (Inversion of Control)就是控制反转,我们将对象的控制权转移给Spring,由 Spring 来负责控制对象的生命周期(比如创建、销毁)和对象间的依赖关系。

在IoC容器运行时,动态地为某个对象提供它所需要的外部依赖,是通过 DI (Dependency Injection)依赖注入来实现的。而 Spring 的 DI 是通过反射实现的。

Spring的AOP理解

推荐文章:【SpringBoot-3】切面AOP实现权限校验:实例演示与注解全解

AOP(Aspect Oriented Programming)是面向切面编程,在切面(Aspect)将相应通知(Advice)织入(Weaving)切点(Pointcut)指定的连接点(Join point)处。主要是通过动态代理来实现的。通过AOP可以帮我们实现到像事务、权限、日志和安全等方便的扩展。

Spring基于xml依赖注入四种方式

推荐文章:Spring中bean的注入方式

  • 构造器注入

  • set方法注入

  • 静态工厂注入

  • 实例工厂注入

注意:一般答前两种

Spring的自动装配

推荐文章:Spring中的@Autowired自动装配

自动装配也就是 Spring 会在容器中自动的查找,并自动的给 bean 装配及其关联的属性

Spring 基于XML提供了 5 种自动装配策略:

  • no:默认的方式是不进行自动装配的,通过手工设置ref属性来进行装配bean。
  • byName:通过bean的名称进行自动装配。
  • byType:通过参数的数据类型进行自动装配。
  • constructor:利用构造函数进行装配,并且构造函数的参数通过byType进行装配。
  • autodetect:自动探测,如果有构造方法,通过 construct的方式自动装配,否则使用byType的方式自动装配。 Spring 3.0 之后已经被标记为 @Deprecated

Spring基于注解的自动装配方式:

使用@Autowired、@Resourc或者@Inject注解来自动装配指定的bean。

  • @Autowired默认是byType 装配注入的,默认情况下它要求依赖对象必须存在(可以设置它required属性为false)。如果匹配到类型的多个实例,再通过 byName 来确定 bean。可以使用@Qualifier来指定注入哪个beanName的bean。

  • @Resource默认是 byName 来装配注入的,只有当找不到与名称匹配的bean才会byType 来装配注入。

  • @Inject也是byType 来查找bean注入的,如果需要指定名称beanName,则可以结合使用@Named注解,而@Autowired是结合@Qualifier注解来指定名称beanName。

Spring中的通知类型

  • Before(前置通知):@Before 指定的方法在切面切入目标方法之前执行
  • After(后置通知): @After 指定的方法在切面切入目标方法之后执行
  • After Retuning(返回通知): @AfterReturning 可以用来捕获切入方法执行完之后的返回值,对返回值进行业务逻辑上的增强处理
  • After Throwing(异常通知): 当被切方法执行过程中抛出异常时,会进入 @AfterThrowing 的方法中执行,在该方法中可以做一些异常的处理逻辑
  • Around(环绕通知):如果需要目标方法执行之前和之后共享某种状态数据,则应该考虑使用@Around。尤其是需要使用增强处理阻止目标的执行,或需要改变目标方法的返回值时,则只能使用@Around增强处理了。

执行顺序:

不抛异常:@Around @Before @AfterReturning @After @Around

抛异常:@Around @Before @AfterThrowing @After

bean的作用域

推荐文章:Spring Bean的作用域

  • singleton:在spring IoC容器仅存在一个Bean实例,Bean以单例方式存在,bean作用域范围的默认值。
  • prototype:每次从容器中调用Bean时,都返回一个新的实例,即每次调用getBean()时,相当于执行newXxxBean()。
  • request:每次HTTP请求都会创建一个新的Bean,该作用域仅适用于web的Spring WebApplicationContext环境。
  • session:同一个HTTP Session共享一个Bean,不同Session使用不同的Bean。该作用域仅适用于web的Spring WebApplicationContext环境。
  • application:限定一个Bean的作用域为ServletContext的生命周期。该作用域仅适用于web的Spring WebApplicationContext环境。

bean的生命周期(待)

推荐文章:

(1)实例化Bean:

  • 对于BeanFactory容器,当客户向容器请求一个尚未初始化的bean时,或初始化bean的时候需要注入另一个尚未初始化的依赖时,容器就会调用createBean进行实例化。

  • 对于ApplicationContext容器,当容器启动结束后,通过获取BeanDefinition对象中的信息,实例化所有的bean。

(2)设置对象属性(依赖注入):实例化后的对象被封装在BeanWrapper对象中,紧接着,Spring根据BeanDefinition中的信息以及通过BeanWrapper提供的设置属性的接口完成属性设置与依赖注入。

(3)处理Aware接口:Spring会检测该对象是否实现了xxxAware接口,通过Aware类型的接口,可以让我们拿到Spring容器的一些资源:

  • 如果这个Bean实现了BeanNameAware接口,会调用它实现的setBeanName(String beanId)方法,传入Bean的名字;
  • 如果这个Bean实现了BeanClassLoaderAware接口,调用setBeanClassLoader()方法,传入ClassLoader对象的实例。
  • 如果这个Bean实现了BeanFactoryAware接口,会调用它实现的setBeanFactory()方法,传递的是Spring工厂自身。
  • 如果这个Bean实现了ApplicationContextAware接口,会调用setApplicationContext(ApplicationContext)方法,传入Spring上下文;

(4)BeanPostProcessor前置处理:如果想对Bean进行一些自定义的前置处理,那么可以让Bean实现了BeanPostProcessor接口,那将会调用postProcessBeforeInitialization(Object obj, String s)方法。

(5)InitializingBean:如果Bean实现了InitializingBean接口,执行afeterPropertiesSet()方法。

(6)init-method:如果Bean在Spring配置文件中配置了 init-method 属性,则会自动调用其配置的初始化方法。

(7)BeanPostProcessor后置处理:如果这个Bean实现了BeanPostProcessor接口,将会调用postProcessAfterInitialization(Object obj, String s)方法;由于这个方法是在Bean初始化结束时调用的,所以可以被应用于内存或缓存技术;

以上几个步骤完成后,Bean就已经被正确创建了,之后就可以使用这个Bean了。

(8)DisposableBean:当Bean不再需要时,会经过清理阶段,如果Bean实现了DisposableBean这个接口,会调用其实现的destroy()方法;

(9)destroy-method:最后,如果这个Bean的Spring配置中配置了destroy-method属性,会自动调用其配置的销毁方法。

循环依赖(待)

推荐文章:

什么情况下循环依赖可以被处理?

  • 出现循环依赖的Bean必须要是单例(singleton)。
  • 依赖注入的方式不能全是构造器注入的方式,且beanName字母序在前的不能是构造器注入(只能解决setter方法的循环依赖,这是错误的)

Spring如何解决循环依赖的

实例化 -> 属性注入 -> 初始化

Spring通过三级缓存解决了循环依赖

  • 一级缓存为singletonObjects,存放所有已经完全初始化的单例Bean
  • 二级缓存为earlySingletonObjects,存放所有仅完成实例化,但还未进行属性注入和初始化的 Bean
  • 三级缓存为singletonFactories,存放单例对象的工厂实例,用于创建对象,然后放入到二级缓存中
  1. 首先,获取单例 Bean 的时候会通过 BeanName 先去一级缓存查找有无完整的 Bean,如果找到则直接返回
  2. 否则会去看对应的 Bean 是否在创建中,如果不在创建中直接返回 null
  3. 如果在创建中则去二级缓存查找 Bean,如果找到则返回
  4. 否则会去三级缓存通过 BeanName 查找到对应的工厂,如果存在工厂则通过工厂创建 Bean ,并且放置到二级缓存中,然后删除对应的工厂
  5. 如果三个缓存都没找到,则返回 null
  6. 返回 null 之后,说明这个 Bean 还未创建,这个时候会标记这个 Bean 正在创建中,然后再调用 createBean 来创建 Bean,而实际创建是调用方法 doCreateBean(执行实例化、属性注入和初始化)。
  7. 在实例化 Bean 之后,会往三级缓存塞入对应的工厂,而调用这个工厂的 getObject 方法就能得到这个 Bean

为什么要使用三级缓存呢?二级缓存能解决循环依赖吗

如果要使用二级缓存解决循环依赖,意味着所有Bean在实例化后就要完成AOP代理,这样违背了Spring设计的原则,Spring在设计之初就是通过AnnotationAwareAspectJAutoProxyCreator这个后置处理器来在Bean生命周期的最后一步来完成AOP代理,而不是在实例化后就立马进行AOP代理。

Spring事务

Spring事务的传播机制:

推荐文章:Spring事务传播机制详解

  1. TransactionDefinition.PROPAGATION_REQUIRED:”如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。这是默认值。”
  2. TransactionDefinition.PROPAGATION_REQUIRES_NEW:”创建一个新的事务,如果当前存在事务,则把当前事务挂起。”
  3. TransactionDefinition.PROPAGATION_SUPPORTS:”如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。”
  4. TransactionDefinition.PROPAGATION_NOT_SUPPORTED:”以非事务方式运行,如果当前存在事务,则把当前事务挂起。”
  5. TransactionDefinition.PROPAGATION_NEVER:”以非事务方式运行,如果当前存在事务,则抛出异常。”
  6. TransactionDefinition.PROPAGATION_MANDATORY:”如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。”
  7. TransactionDefinition.PROPAGATION_NESTED:”如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则执行类似于TransactionDefinition.PROPAGATION_REQUIRED的操作。”

Spring事务的隔离级别:

  1. Isolation.DEFAULT:使用底层数据库默认的隔离级别。ORACLE(读已提交)SQL SERVER(读已提交)MySQL(可重复读)。
  2. Isolation.READ_UNCOMMITTED:最低的隔离级别,有脏读、不可重复读和幻读的风险。
  3. Isolation.READ_COMMITTED:解决脏读的隔离级别,但有幻读以及不可重复读风险。
  4. Isolation.REPEATABLE_READ:解决不可重复读的隔离级别,但还是有幻读风险。
  5. Isolation.SERIALIZABLE:最高的事务隔离级别,解决了脏读、不可重复读和幻读的问题了。

Spring事务的失效场景:

推荐文章:Spring事务失效常见场景

  1. 事务方法访问修饰符非public,或者事务被static、final修饰。【可以开启 AspectJ 代理模式】
  2. 默认情况下,RuntimeException和Error才会回滚。【可使用rollbackFor指定自定义异常】
  3. 数据库本身不支持事务。【如MySQL使用存储引擎是MyISAM则不支持事务,可使用InnoDB存储引擎】
  4. 事务方法所在的类没有被Spring管理。【事务所在类没有加@Service、@Component注解等】
  5. catch捕获异常后没有再次抛出异常。【重新抛出RuntimeException】
  6. 事务方法被类内部方法(this)调用。【调用方法和事务方法放在不同类中;注入事务所在类再通过注入类调用;通过代理类调用自己类的方法;将事务注解搬到调用方法上】
  7. 数据源没有配置事务管理器。【SpringBoot默认开启事务管理器DataSourceTransactionManagerAutoConfiguration
  8. 事务传播机制设置错误。
  9. 多线程由于数据库连接不一样导致事务失效。

SpringMVC

SpringMVC 的执行流程

  1. 用户发送请求至前端控制器DispatcherServlet
  2. 前端控制器收到请求后,调用处理器映射器HandlerMapping,请求获取Handler处理器(也可以称为controller);
  3. 处理器映射器根据请求url找到具体的处理器,生成处理器对象及处理器拦截器(如果有则生成)一并返回给前端控制器DispatcherServlet;
  4. 前端控制器通过处理器适配器HandlerAdapter调用处理器Handler;
  5. 处理器适配器执行处理器;
  6. 处理器执行完成给适配器返回ModelAndView;
  7. 处理器适配器将ModelAndView返回给前端控制器;
  8. 前端控制器将ModelAndView传给视图解析器ViewReslover进行解析;
  9. 视图解析器解析后返回具体View;
  10. 前端控制器对View进行渲染视图(即将模型数据填充至视图中)
  11. 前端控制器向用户响应结果。

Mybatis

什么是SqlSession

SqlSession是mybatis框架中的一个对象,类似JDBC的Connection对象,是Java程序端和数据库之间的会话。
框架底层通过sqlSession对象去执行sql语句,帮助我们实现增删改查操作

#{}和${}的区别

#{}底层采用的是?占位符赋值的方式,调用PreparedStatement的set方法来赋值,可以对SQL语句预编译

${}底层采用的是字符串拼接sql的方式,容易产生sql注入,如果需要一些字段名或者关键字(比如order by)可以用${}

MyBatis的缓存机制

作用:减少web应用和数据库(磁盘)访问次数,提高查询效率,减轻数据库访问压力。

一级缓存(默认级别):

SqlSession级别的缓存,作用域是同一个SqlSession,在同一个sqlSession中两次执行相同的sql语句(包括参数相同),第一次执行完毕会将数据库中查询结果写到缓存,第二次会直接从缓存中获取数据,从而提高查询效率。

当SqlSession执行了DML操作(增删改),并且提交到数据库,MyBatis则会清空SqlSession中的一级缓存,避免出现脏读。当一个 SqlSession结束后,该SqlSession中的一级缓存也就不存在了。

二级缓存:

mapper级别的缓存,其作用域是mapper的同一个namespace,两次执行相同namespace下的sql语句(包括参数相同),第一次执行完毕会将数据库中查询结果写到缓存,第二次会从缓存中获取数据将不再从数据库查询,从而提高查询效率。

mapper文件中增加标签,实体类实现序列化。
当执行了增删改操作,清空当前sql所对应的namespace的缓存。

MyBatis和Mybatis-Plus的区别

Mybatis-Plus是无侵入的,只对Mybatis做了增强不做改变。

  • 内置通用 Mapper、Service,仅通过少量配置即可实现单表大部分 CRUD 操作,更有强大的条件构造器,满足各类使用需求

  • 支持 Lambda 形式调用:通过 Lambda 表达式,方便的编写各类查询条件,无需再担心字段写错。

  • 支持主键自动生成:支持多达 4 种主键策略(内含分布式唯一 ID 生成器 - Sequence),可自由配置,完美解决主键问题。

  • 内置代码生成器、内置分页插件

  • 内置性能分析插件:可输出 Sql 语句以及其执行时间,建议开发测试时启用该功能,能快速揪出慢查询。

  • 内置全局拦截插件:提供全表 delete 、 update 操作智能分析阻断,也可自定义拦截规则,预防误操作。

Mysql

https://blog.csdn.net/ThinkWon/article/details/104778621

基本组成模块

推荐文章:MySQL实战第一讲 - 一条SQL查询语句是如何执行的?

20200419204847494

Server 层:包括连接器、查询缓存(Mysql8已移除)、分析器、优化器、执行器等,涵盖 MySQL 的大多数核心服务功能,以及所有的内置函数(如日期、时间、数学和加密函数等),所有跨存储引擎的功能都在这一层实现,比如存储过程、触发器、视图等。

存储引擎层:负责数据的存储和提取。其架构模式是插件式的,支持 InnoDB、MyISAM、Memory 等多个存储引擎。现在最常用的存储引擎是 InnoDB。

数据库三大范式

推荐文章:数据库三大范式是什么?(3NF详解)

第一范式:属性不可分割

第二范式:在第一范式的基础上,非主键列完全依赖于主键,而不能是依赖于主键的一部分

第三范式:在第二范式的基础上,非主键列只依赖于主键,不依赖于其他非主键。

脏读、不可重复读、幻读

推荐文章:数据库事务隔离级别(脏读、幻读、不可重复读)

  • 脏读(Dirty read): 当一个事务正在访问数据并且对数据进行了修改,而这种修改还没有提交到数据库中,这时另外一个事务也访问了这个数据,然后使用了这个数据。因为这个数据是还没有提交的数据,那么另外一个事务读到的这个数据是“脏数据”,依据“脏数据”所做的操作可能是不正确的。
  • 不可重复读(Unrepeatable read): 指在一个事务内多次读同一数据。在这个事务还没有结束时,另一个事务也访问该数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改导致第一个事务两次读取的数据可能不太一样。这就发生了在一个事务内两次读到的数据是不一样的情况,因此称为不可重复读。
  • 幻读(Phantom read): 幻读与不可重复读类似。它发生在一个事务(T1)读取了几行数据,接着另一个并发事务(T2)插入了一些数据时。在随后的查询中,第一个事务(T1)就会发现多了一些原本不存在的记录,就好像发生了幻觉一样,所以称为幻读。

事务的隔离级别

√表示会有该风险

隔离级别 脏读 不可重复读 幻读
READ-UNCOMMITTED(读未提交)
READ-COMMITTED(读已提交) ×
REPEATABLE-READ(可重复读) × ×
SERIALIZABLE(可串行化) × × ×

MyISAM与InnoDB区别

MyISAM Innodb
文件格式 数据和索引是分别存储的,数据.MYD,索引.MYI 数据和索引是集中存储的.ibd
记录存储顺序 按记录插入顺序保存 按主键大小有序插入
外键 不支持 支持
事务 不支持 支持
SELECT、INSERT、UPDATE、DELETE SELECT更优 INSERT、UPDATE、DELETE更优
表的行数 存储 不存储
锁支持 表锁 行锁、表锁
主键索引类型 非聚簇索引 聚簇索引

B树与B+树的区别

推荐文章:

结构区别:

  • 在B树中,可以将键和值存放在非叶子节点和叶子节点;但在B+树中,非叶子节点都是只有键没有值,叶子节点同时存放键和值,所有叶子节点都在同一层。
  • B树的叶子节点各自独立;但B+树的叶子节点增加了顺序访问指针,指向相邻叶子节点。

性能区别:

  • 同一盘块中,B+树容纳的关字键更多,使得查询的IO次数更少。
  • B+树查询效率更加稳定,所有关键字查询的路径长度相同。
  • B+树便于范围查询,只需遍历链表,而B树需要中序遍历。

索引类型

推荐文章:

按数据的物理存储分类,可分为聚簇索引和非聚簇索引

按索引字段特性分类,可分主键索引、唯一索引、普通索引、全文索引

聚簇索引:

  • 按照每张表的主键构造一颗B+树,叶子节点中存储索引值行数据。也将聚集索引的叶子节点称为数据页。
  • 每张表只能拥有一个聚簇索引。Innodb的聚簇索引一般都是主键索引。如果没有定义主键,Innodb会选择非空的唯一索引代替。如果没有这样的索引,Innodb会隐式定义一个主键来作为聚簇索引。

非聚簇索引(辅助索引):

  • 非聚簇索引的叶子节点中存储索引值主键值(Innodb)或数据行的地址(MyISAM)。
  • 一张表可以有多个辅助索引。
  • Innodb的辅助索引访问数据总是需要二次查找。拿到主键再去聚簇索引上查找也称之为回表查询。如果查询语句所要求的字段全部命中了索引,那么就不必再进行回表查询,即索引覆盖

最左匹配原则

推荐文章:Mysql最左匹配原则

创建一个(a,b)的联合索引,在B+树中a的值是有顺序的,在a值相等的情况下,b值又是按顺序排列的。

最左匹配原则:联合索引中以最左边的为起点任何连续的索引都能匹配上,遇到范围查询(>、<、between、like)就会停止匹配。

  • where子句的搜索条件调换顺序不会影响查询结果,因为Mysql中有查询优化器,会自动优化查询顺序
  • 模糊查询时:like ‘As%’走索引,like ‘%As’全表扫描, like ‘%As%’全表扫描
  • order by子句后面的顺序需与索引列的顺序一致才走索引

比如a = 1 and b = 2 and c > 3 and d = 4,如果建立(a,b,c,d)顺序的索引,d是用不到索引的;如果建立(a,b,d,c)的索引则都可以用到

索引下推

推荐文章:五分钟搞懂MySQL索引下推

索引下推下推其实就是指将部分上层(server层)负责的事情,交给了下层(存储引擎层)去处理。目的是为了减少回表次数,也就是要减少IO操作,提高查询效率。

没有使用ICP的情况下,MySQL的查询:

  1. 存储引擎读取索引记录;
  2. 根据索引中的主键值,定位并读取完整的行记录;
  3. 存储引擎把行记录交给Server层去检测该记录是否满足WHERE条件。

使用ICP的情况下,查询过程:

  1. 存储引擎读取索引记录(不是完整的行记录);

  2. 判断WHERE条件部分能否用索引中的列来做检查,条件不满足,则处理下一行索引记录;

条件满足,使用索引中的主键去定位并读取完整的行记录(就是所谓的回表);

  1. 存储引擎把记录交给Server层,Server层检测该记录是否满足WHERE条件的其余部分。

使用条件

  • 只能用于执行计划中type是range、 ref、 eq_ref、ref_or_null访问方法;
  • 只能用于InnoDB和 MyISAM存储引擎及其分区表;
  • 对InnoDB存储引擎来说,索引下推只适用于二级索引(也叫辅助索引);
  • 引用了子查询的条件不能下推;
  • 引用了存储过程或函数的条件不能下推,因为这些都是在server层实现。

MVCC(待)

Reidis

缓存穿透

缓存穿透指一个一定不存在的数据,由于缓存未命中这条数据,就会去查询数据库,数据库也没有这条数据,所以返回结果是 null。如果并发量大且缓存都没有命中,每次查询都请求数据库时,缓存就失去了保护后端持久层的意义,这会给持久层数据库造成很大的压力。

解决方案:

  • 缓存空对象:是指在持久层没有命中的情况下,对key进行set (key,null),缓存空对象会有两个问题:
  • value为null 不代表不占用内存空间,空值做了缓存,意味着缓存层中存了更多的键,需要更多的内存空间,比较有效的方法是针对这类数据设置一个较短的过期时间,让其自动剔除。
  • 缓存层和持久层的数据会有一段时间窗口的不一致,可能会对业务有一定影响。例如过期时间设置为5分钟,如果此时存储层添加了这个数据,那此段时间就会出现缓存层和持久层数据的不一致,此时可以利用消息系统或者其他方式清除掉缓存层中的空对象。
  • 布隆过滤器拦截
  • 在访问缓存层和持久层之前,将存在的key用布隆过滤器提前保存起来,做第一层拦截,当收到一个对key请求时先用布隆过滤器验证是key否存在,如果存在再进入缓存层、持久层。可以使用bitmap做布隆过滤器。这种方法适用于数据命中不高、数据相对固定、实时性低的应用场景,代码维护较为复杂,但是缓存空间占用少。
  • 布隆过滤器实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和 删除困难。

缓存击穿

当某个key在过期的瞬间,有大量的请求并发访问,这类数据一般是热点数据,由于缓存过期,会同时访问数据库来查询最新数据,数据库有数据并且回写缓存,会导使数据库瞬间压力过大。

解决方案:

  • 分布式互斥锁,只允许一个线程重建缓存,其他线程等待重建缓存的线程执行完,重新从缓存获取数据即可。
  • 这种方案思路比较简单,但是存在一定的隐患,如果在查询数据库 + 和 重建缓存(key失效后进行了大量的计算)时间过长,也可能会存在死锁和线程池阻塞的风险,高并发情景下吞吐量会大大降低!但是这种方法能够较好地降低后端存储负载,并在一致性上做得比较好。
  • 热点Key永不过期,从功能层面来看,为每个value设置一个逻辑过期时间,当发现超过逻辑过期时间后,会使用单独的线程去更新缓存。
  • 这种方案由于没有设置真正的过期时间,实际上已经不存在热点key产生的一系列危害,但是会存在数据不一致的情况,同时代码复杂度会增大。

缓存雪崩

由于缓存层承载着大量请求,有效地保护了持久层,但是如果缓存层由于某些原因不可用(宕机)或者大量缓存由于超时时间相同在同一时间段失效(大批key失效/热点数据失效),大量请求直接到达存储层,存储层压力过大导致系统雪崩。

解决方案:

  • 缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。
  • 可以把缓存层设计成高可用的,即使个别节点、个别机器、甚至是机房宕掉,依然可以提供服务。利用sentinel或cluster实现。
  • 采用多级缓存,本地进程作为一级缓存,redis作为二级缓存,不同级别的缓存设置的超时时间不同,即使某级缓存过期了,也有其他级别缓存兜底。
  • 数据加热,在正式部署之前先把可能的数据先预先访问一遍,这样部分可能大量访问的数据就会加载到缓存中。

过期的数据的删除策略

如果假设你设置了一批 key 只能存活 1 分钟,那么 1 分钟后,Redis 是怎么对这批 key 进行删除的呢?

常用的过期数据的删除策略就两个:

  • 惰性删除 :只会在取出 key 的时候才对数据进行过期检查。这样对 CPU 最友好,但是可能会造成太多过期 key 没有被删除。

  • 定期删除 : 每隔一段时间抽取一批 key 执行删除过期 key 操作。并且,Redis 底层会通过限制删除操作执行的时长和频率来减少删除操作对 CPU 时间的影响。

定期删除对内存更加友好,惰性删除对 CPU 更加友好。两者各有千秋,所以 Redis 采用的是 定期删除+惰性/懒汉式删除

但是,仅仅通过给 key 设置过期时间还是有问题的。因为还是可能存在定期删除和惰性删除漏掉了很多过期 key 的情况。这样就导致大量过期 key 堆积在内存里,然后就 Out of memory 了。

怎么解决这个问题呢?答案就是:Redis 内存淘汰机制。

Redis 内存淘汰机制

Redis一开始有6种数据淘汰策略,4.0版本后增加2种(7和8),总共8种

  1. volatile-lru(least recently used):从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
  2. volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
  3. volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
  4. allkeys-lru(least recently used):当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 key(这个是最常用的)
  5. allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
  6. no-eviction:禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错。这个应该没人使用吧!
  7. volatile-lfu(least frequently used):从已设置过期时间的数据集(server.db[i].expires)中挑选最不经常使用的数据淘汰
  8. allkeys-lfu(least frequently used):当内存不足以容纳新写入数据时,在键空间中移除最不经常使用的 key