• Java面试题-0919


    集合篇

    为了满足hashmap集合的不重复存储,为什么要重写hashcode和equals方法?
    首先理解一下hashmap的插入元素的前提:
    hashmap会根据元素的hashcode取模进行比较,当hashcode相等时,会再次去比较元素之间的内容值,当内容值也相等时就代表元素重复。
    所以当元素与元素之间的hashcode值内容值相同时,hashmap就会认为元素重复

    • 重写hashCode是为了让两个具有相同值的对象的Hash值相等。

    • 重写equals方法是为了比较两个不同对象的值是否相等。

    • 同时重写hashCode()与equals()是为了满足HashSet、HashMap等此类集合的相同对象的不重复存储。

    当前有两个不同对象,他们的内容是相同的,但是在hashmap看来他们就是重复的,所以我们重写hashcode方法,保证相同值的两个对象的hashcode相同,重写equals方式是比较两个不同对象的值是否相同。

    equals和hashCode

    基础篇

    == 与 equals的区别
    在这里插入图片描述

    默认情况下 equals方法也是比较的两个对象之间的内存地址是否相同,但是我们可以重写equals方法达到不同的效果,如String类就重写equals方法,String类的equals方法会先去比较两个对象的内存是否相同,相同就返回true,如果不相同也不会立马放回false,而是会再次比较两个String对象的数值是否相同。

    多线程

    java 多线程 面试题整理(更新…)

    • interrupt()方法
      interrupt方法用于中断线程,需要注意的是,只是将线程的状态设置为“中断”状态,并没有真正的停止这个线程;需要线程自己去监视(interrupted、isinterrupted)线程的状态为并做处理

    通常与interrupted()、isinterrupted()配合使用,从而达到停止一个线程。
    interrupted()、isinterrupted()都是监视当前线程的中断状态,当这两个方法返回的中断状态为true,可以使用return或者抛出异常来结束线程方法。
    代码示例如下:

    public class IsinterruptedTest {
    
       public static void main(String[] args)  {
           Runnable runnable = () ->{
               Thread thisThread = Thread.currentThread();
               int num =0;
               while (true){
                   // 检查当前中断标志是否为true
                   if (thisThread.isInterrupted()){
                       System.out.println("当前线程任务已被中断....");
                       return;
                   }
                   System.out.println(num++);
               }
           };
    
           Thread thread =new Thread(runnable);
           // 启动线程
           thread.start();
    
           // 让主线程休眠2ms,之后再去中断子线程
           try {
               Thread.sleep(2);
           } catch (InterruptedException e) {
               e.printStackTrace();
           }
    
           // 中断子线程
           thread.interrupt();
       }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • interrupted、isinterrupted的区别?
    • 为何stop()和suspend()方法不推荐使用
      stop方法: stop()是立即终止一个线程,并立刻释放被它锁住的所有对象的锁。当线程执行到一般时突然被终止后,可能会导致资源没有被正确释放,也会导致数据损坏、数据不一致的问题。如一个线程任务中做了两件事(增加订单、减少库存),而这时线程执行一半就被强制终止了,就导致订单增加了,库存却没有减少,出现数据不一致问题
      需要注意的是,通过stop()终止线程,finally代码块中的代码也不会被执行;finally代码块通常用于资源的释放或者清理操作,所以会导致资源没有被正确释放

    suspend():作用是挂起/暂停某个线程直到调用resume方法来恢复该线程,但是调用了suspend方法后并不会释放线程获取到的锁,容易造成死锁

    • 线程的几种状态

    • sleep方法:放弃cpu使用权,使当前线程暂停一段时间,当前暂停时间结束后,会重新进入就绪状态并与其它线程等待cpu调度。sleep()会释放cpu资源,但是不会释放同步锁(类锁、对象锁)

    • yield方法:与sleep方法相似,暂停线程,放弃cpu使用权,并马上进入就绪状态,等待cpu调度。不会释放同步锁(类锁、同步锁);需要注意的是:yield方法可能会不起作用,因为cpu调度是不可控制,我们无法控制cpu去调用指定的线程,所以可能会导致出现,当前线程调用了yield()放弃cpu使用权进入就绪状态后,cpu下次调用的线程还是当前线程

    • Java中join()方法原理及使用教程

    • 锁池与等待池的区别:
      每个对象都有一个同步锁/内置锁(互斥锁),同时也会锁池和等待池
      锁池是用来存放那些想要获取对象锁,但是还没有拿到锁的线程。当拿到锁资源后,线程会进入就绪状态。
      等待池存放的是那些主动释放(wait)锁去成全其它线程的线程。当等待池中的某一个线程被notfy、notfyall方法唤醒后,会进入锁池重新争夺锁,之后再从中断处继续执行任务。

    • wait方法 与 notify方法

    线程池

    线程池的好处

    • 降低资源的消耗:利用线程池中已存在的线程重复执行任务,这样就可以不用每次都创建、销毁线程,有效的降低了资源的损耗
    • 提高响应速度:当线程池已存在空闲线程,可以直接执行任务,不用等待线程的创建
    • 有效管理线程:线程是稀缺资源,放入线程池中可有效管理线程,不用每次创建后就销毁,降低资源损耗。

    线程池的七大参数

    • maximumPoolSize:核心线程数,默认情况下,这些线程数被创建后不会被销毁的,除非设置了allowCoreThreadTimeOut。
    • maximumPoolSize:线程池中最大线程数量,核心线程数也包含在里面
    • keepAliveTime :空闲(非核心)线程存活时间,空闲线程会在指定时间内销毁
    • unit :空闲时间单位
    • workQueue :阻塞队列,异步任务基本都会放入阻塞队列中等待线程调用执行,注意是基本不是全部。
    • threadFactory :线程工厂,线程池创建一个新线程时使用的工厂,可以用来设定线程名、是否为daemon线程等等
    • handler :拒绝策略,当阻塞队列&线程数都达到最大限制,会采用传入的拒绝策略。jdk也为我们提供了四种拒绝策略

    常用阻塞队列

    • ArrayBlockingQueue
      基于数组有界阻塞队列队列先进先出
    • LinkedBlockingQueue
      基于链表的阻塞队列,可以说是无界的队列,当未指定容量时,则等于Integer.MAX.VALUE(2^31-1)
    • DelayQueue
      是一个无界的阻塞队列,可以实现延迟获取元素,所以添加进入队列的元素必须实现Delayed接口(指定延迟时间),在延迟期满后元素才被提取。调用put之类的方法加入元素时,会触发Delayed接口中的compareTo方法进行排序,也就是说队列中元素的顺序是按到期时间排序的,而非它们进入队列的顺序

    常用拒绝策略

    • AbortPolicy:中止策略。默认的拒绝策略,直接抛出 RejectedExecutionException。调用者可以捕获这个异常,然后根据需求编写自己的处理代码。
    • DiscardPolicy:抛弃策略。什么都不做,直接抛弃被拒绝的任务。
    • DiscardOldestPolicy:抛弃最老策略。抛弃阻塞队列中最老的任务,相当于就是队列中下一个将要被执行的任务,然后重新提交被拒绝的任务。
    • CallerRunsPolicy:调用者运行策略。该策略既不会抛弃任务,也不会抛出异常,而是将任务回退到调用者(调用线程池执行任务的主线程),由调用者线程执行该任务。

    线程池的执行流程

    在这里插入图片描述

    1. 先查看当前线程池的线程数量是否小于核心线程数大小,小于则新建线程并直接执行任务
    2. 大于则放入阻塞队列中,加入阻塞后,任务在未来某个时间点被一个空闲的线程取出执行,但是在加入到阻塞队列后,还会检查当前是否有线程存在(因为当核心线程数量设置为0的时候,程序也会执行到一步,但是会出现一个情况:阻塞队列里的任务无法被提取执行,因为当前线程池并没有线程,所有这里会创建一个线程)
    3. 当阻塞队列也满了,则会新建线程并立即执行任务
    4. 若线程数量也达到最大限制就会执行对应的拒绝策略

    线程池的最大线程数量如何设计

    • CPU密集型任务: CPU数+1,这是为了防止线程在执行任务时发生突发情况导致线程暂停,从而使CPU空闲,这时候多加一个线程就可以利用CPU的空闲时间去完成任务。
    • IO密集型任务:
      CPU数*2,程序在执行IO操作时,是不会用到CPU的,此时的CPU是空闲,所以我们可以多设置一些线程去利用CPU的空闲时间。
    • 混合型任务:
      (线程等待时间+CPU使用时间)/CPU使用时间*CPU数,如当前的任务都是1.5s的IO时间,0.5s的CPU使用时间,CPU核数是4,那最大的线程量应该为:(1.5+0.5)/0.5*4=4;

    线程池中的空闲线程能成为核心线程吗?

    有一定概率;我们常说的空闲、核心线程只是一个概念,在线程池实际概念中并没有标识哪个是核心、空闲线程,线程池只会保留核心线程数大小的线程,其它线程就会被销毁,保留下来的就是核心线程。且销毁是随机的,那可能这次保留下来的核心线程,在下一次销毁时,核心线程是有可能被销毁,而它的位置就空闲线程替代。

    线程只能在任务到达时才启动吗?

    默认情况下,即使是核心线程也只能在新任务到达时才创建和启动。但是我们可以使用 prestartCoreThread(启动一个核心线程)或 prestartAllCoreThreads(启动全部核心线程)方法来提前启动核心线程

    使用队列有什么需要注意的吗?

    使用有界队列时,需要注意线程池满了后,被拒绝的任务如何处理。
    使用无界队列时,需要注意如果任务的提交速度大于线程池的处理速度,可能会导致内存溢出

    线程池如何关闭

    • shutdown():将线程池状态变成SHUTDOWN状态,此时不能再往线程池中添加任务,否则抛出异常。此时线程池不会立即关闭,而是等待线程池中所有任务完成后才关闭。
    • shutdownNow():将线程池状态立即变成STOP方法,并尝试停止正在执行的任务,注意这里也并不是立即关闭任务,然后将未执行的任务都返回。

    线程池的五种状态

    1. RUNNING:运行
      状态说明:线程池处于RUNNING状态时,能够接收新任务以及对已添加的任务进行处理。
      状态切换:线程池的初始状态为RUNNING。换句话说线程池一旦被创建,就处于RUNNING状态,且线程池中的任务数为0
    2. SHUTDOWN:关闭
    • 状态说明:线程池处于SHUTDOWN状态时,不接收新任务,但能处理已添加的任务
    • 状态切换:调用线程池的shutdown()接口时,线程池由RUNNING->SHUTDOWN
    1. STOP:停止
    • 状态说明:线程池处于STOP状态时,不接收新任务,不处理已添加的任务,并且会中断正在处理的任务
    • 状态切换:调用线程池的shutdownNow()接口时,线程池由(RUNNING)或者(SHUTDOWN)->STOP
    1. TIDYING:调整
    • 状态说明:当所有的任务已终止,队列任务数为0,线程池的状态会变为TIDYING状态;当线程池的状态变为TIDYING状态时,会调用钩子函数terminated(),该方法在ThreadPoolExecutor中是空的,若用户想在线程池变为TIDYING时进行相应的处理,就需要重载terminated()函数实现
    • 状态切换:当线程池状态为SHUTDOWN时,阻塞队列为空并且线程池中执行的任务也为空时,就会由SHUTDOWN->TIDYING
      当线程池为STOP时,线程池中执行的任务为空时,就会又STOP->TIDYING
    1. TERMINATED:终止
    • 状态说明:线程池彻底终止,就会变成TERMINATED状态
    • 状态切换:线程池处于TIDYING状态时,调用terminated()就会由TIDYING->TERMINATED

    Spring

    Spring是什么

    • 是一个轻量级的开源的JavaEE(企业开发)框架
    • 主要是为代码解耦,降低代码间的耦合度,让对象与对象(模块和模块)之间的关系不是由代码进行说明,而是用配置来说明
    SpringIOC(控制反装)

    解释:就是将对象创建和对象之间的调用过程,交给Spring管理,不用开发人员手动创建对象,减小开发人员的工作量,主要作用也是为了解耦。

    解耦如下:
    没有引入IOC容器之前

    对象A依赖于对象B,那么对象A在初始化或者运行到某一点的时候, 自己必须主动去创建对象B或者使用已经创建的对象B,无论是创建还是使用对象B,控制权都在自己手上

    在这里插入图片描述

    引入IOC容器之后

    对象A与对象B之间失去了直接联系,当对象A运行到需要对象B的时候,IOC容器会主动创建一个对象B注入到对象A需要的地方

    在这里插入图片描述

    通过前后的对比,不难看出来:对象A获得依赖对象B的过程,由主动行为变为了被动行为,控制权颠倒过来了,这就是"控制反转"这个名称的由来

    ”由主动行为变为了被动行为“:对象A之前需要自己创建B对象,此时对象A就依赖与对象B,对象A、B之间就耦合了,而此时引入IOC之后,对象A就不需要主动创建对象B,对象A等待Spring容器创建好之后去调用即可,对象A也不用关心对象B是怎么创建的。这里就弱化了对象A与对象B的直接联系,这里其实就起到了松耦合的作用,转而加强了对IOC的联系。

    虽然是加强了对IOC的联系,但是Spring的主旨就是这么干的: 主要是为代码解耦,降低代码间的耦合度,让对象与对象(模块和模块)之间的关系不是由代码进行说明,而是用配置来说明

    如果你还是不太理解IOC,那么举个例子就比如说人饿了想要吃饭。如果不使用IOC的话,你就得自己去菜市场买菜、做饭才能吃上饭。用了IOC以后,你可以到一家饭店,想吃什么菜你点好就可以了,具体怎么做你不用关心,饭店做好了,服务员端上来你负责吃就可以了,其它的交给饭店来做

    控制反转IoC(Inversion of Control),是一种设计思想,DI(依赖注入)是实现IoC的一种方法

    DI(依赖注入)的实现方式

    • 构造方法注入,使用配置文件

    以下是一个使用XML配置实现Spring依赖注入的示例。假设你有一个名为UserService的服务类,它依赖于UserRepository:

    public class UserRepository {
        // UserRepository的实现
    }
    
    public class UserService {
        private UserRepository userRepository;
    
        // 构造函数注入
        public UserService(UserRepository userRepository) {
            this.userRepository = userRepository;
        }
    
        public void getUserInfo() {
            // 使用userRepository获取用户信息
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    这时我们需要创建一个配置文件-applicationConytext.xml,这个配置文件包含了对Bean的定义与依赖注入的篇日志,我们将UserService与UserRepository对象在配置文件定义好,并声明UserRepository对象可以构造方法的形式注入到UserService对象中

    <beans xmlns="http://www.springframework.org/schema/beans"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd">
    
        <!-- 配置UserRepository Bean -->
        <bean id="userRepository" class="com.example.UserRepository" />
    
        <!-- 配置UserService Bean,并注入UserRepository -->
        <bean id="userService" class="com.example.UserService">
        	// 这行代码是关键
            <constructor-arg ref="userRepository" />
        </bean>
    </beans>
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    主要注意这一行: ,这是构造方法注入的关键,通过constructor-arg标签将对象userRepository通过构造方法的形式注入到userService中

    • setter方法注入,使用配置文件
      业务代码
    public class UserRepository {
        // UserRepository的实现
    }
    
    public class UserService {
        private UserRepository userRepository;
    
        // 使用setter方法注入依赖
        public void setUserRepository(UserRepository userRepository) {
            this.userRepository = userRepository;
        }
    
        public void getUserInfo() {
            // 使用userRepository获取用户信息
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    配置文件:

    <beans xmlns="http://www.springframework.org/schema/beans"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd">
    
        <!-- 配置UserRepository Bean -->
        <bean id="userRepository" class="com.example.UserRepository" />
    
        <!-- 配置UserService Bean,并使用setter方法注入UserRepository -->
        <bean id="userService" class="com.example.UserService">
        	// 这行代码是关键
            <property name="userRepository" ref="userRepository" />
        </bean>
    </beans>
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    主要注意这一行:,这是构造方法注入的关键,通过property标签将对象userRepository通过setter方法的形式注入到userService中

    • 注解注入

    假设你有一个名为UserService的服务类,它依赖于UserRepository

    public class UserRepository {
        // UserRepository的实现
    }
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    
    @Service
    public class UserService {
        private UserRepository userRepository;
    
        @Autowired
        public UserService(UserRepository userRepository) {
            this.userRepository = userRepository;
        }
    
        public void getUserInfo() {
            // 使用userRepository获取用户信息
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    接下来,你需要配置Spring框架以扫描注解并创建Bean。通常,这可以通过配置Spring的扫描包来实现。以下是一个示例Spring配置文件(applicationContext.xml):

    <beans xmlns="http://www.springframework.org/schema/beans"
        xmlns:context="http://www.springframework.org/schema/context"
        xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd">
    
        <context:component-scan base-package="com.example" />
    
    </beans>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    在上述配置中, 标签用于指示Spring扫描指定包下的类,并自动创建Bean。这样,Spring会扫描 com.example 包下的所有类,包括UserService和UserRepository,并根据@Autowired和@Service注解来处理依赖注入

    当然你也可以使用@Component注解将对象注册为Spring容器的bean。这样就可以不使用配置文件的 标签。区别就是context:component-scan 标签可以将指定包下的所有类注册为bean,放入到Spring容器中,而@Component注解只能将该注解修饰下的一个类注册为bean并放入到容器中。

    Spring注解

    在使用注解注入的时候,先理解几个注解的意思。

    • @Component:将一个类标识为Spring容器管理的Bean。

    • @Autowired:自动装配,通过 @Autowired 注解,Spring容器(IOC)会自动将匹配的Bean注入到标记了 @Autowired 的字段、方法参数或构造函数参数中。

    • @Service:标识一个类为服务层的Bean。与@Component相似,但提供了更明确的语义

    • @Controller:标识一个类为Spring MVC控制器

    • @Repository:标识一个类为数据访问层的Bean

    • @ComponentScan:会自动扫描包路径下面的所有被@Controller、@Service、@Repository、@Component 注解标识的类,然后装配到Spring容器中

    • @RequestMapping:用于映射web请求,包括访问路径和参数

    • @ResponseBody:将返回值转换为json格式并放入到response中。

    • @RequestBody:接收请求体中的json数据。

    • @PathVariable:用于接收路径参数,比如@RequestMapping(“/hello/{name}”)声明的路径,将注解放在参数前,即可获取该值,通常作为Restful的接口实现方法。

    • @RestController:该注解为一个组合注解,相当于@Controller和@ResponseBody的组合

    • @Configuration:标识一个类为配置类,可替换xml配置文件。通常与@bean注解配合使用

    • @Bean:定义一个bean,并放入到Spring容器中。
      通常是与configuration一起使用,若是在非Spring管理的类中定义bean,则需要将该类添加到Spring容器中。如下:

    public class Test{
    	@Bean
    	public MyBean myBean(){
    		return new MyBean();
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    这时你需要将Test类注册进入Spring容器中,MyBean这个bean才能被注册进入Spring容器中。
    可以使用@Component注解,如下

    @Component
    public class Test{}
    
    • 1
    • 2

    bean的作用域

    面试:Bean 作用域是什么?它有几种类型

    总结:作用域session、request,在Spring中每次用户获取到的bean都是不同的,也就多例的

    • 作用域singleton(单例)与application的区别
      singleton与application在整个应用程序共享同一个Bean实例也就是说都是单例的,区别就是:
      “singleton”作用域是基于Spring容器的,“application”作用域是基于Servlet上下文(ServletContext)的

    bean的生命周期

    1. 实例化bean:当客户向容器请求一个尚未初始化的bean时,容器就会调用doCreateBean()方法进行实例化,实际上就是通过反射的方式创建出一个bean对象
    2. Bean属性填充:注入这个Bean依赖的其它Bean对象
    3. 初始化Bean:Bean对象被创建后,Spring容器会执行一些额外的操作来准备Bean对象,使其可以被使用。我们通常通过@PostConstruct注解或配置init-method执行自定义初始化方法,Spring将在初始化阶段中调用该方法
    4. 销毁Bean:当Bean不再需要时,就会进行销毁操作。

    参考链接:
    Spring中bean的生命周期

    Spring AOP(面向切面)

    AOP(面向切面):就是在不修改源代码(业务代码)的情况下对程序(方法)添加额外的功能,主要场景有日志记录、权限管理、异常处理等方面。

    AOP术语

    • joinpoint 连接点:就是指那些潜在的可以被拦截(增强)的方法,方法一旦被拦截就转为切点,切点的候选。这里可以理解成 所有方法都可以时连接点
    • pointcut 切入点:在那些方法上切入。即被拦截的连接点。
    • advice 通知:指拦截到连接点之后要做的事,即对切入点增强的内容,通知有很多种,如下

    befor(前置通知):通知方法在目标方法(切入点)调用之前执行
    after-returing(返回后通知):通知方法在目标方法返回后执行
    after-throwing(抛出异常通知):通知方法在目标方法抛出异常后执行
    after(后置通知):通知方法在目标方法返回或异常后执行
    around(环绕通知):

    • Target(目标对象):切入点的对象(被拦截的方法的对象)。
    • Weaving(织入)将切面的通知代码插入到目标对象的过程
    • Proxy(代理)在织入之后,代理对象会被创建。代理对象包装了目标对象,并包含了切面的通知代码。客户端代码通常与代理对象交互,因此客户端对目标对象的调用实际上是通过代理对象进行的

    静态代理和动态代理

    静态代理:
    优点:在编译时创建代理类,代理类编译后存在,与目标对象的关系在编译时已经确定。代码易于理解和实现
    缺点:不灵活:静态代理需要为每个被代理的类创建一个代理类,导致类增加。
    动态代理:
    优点:在运行时创建代理类,代理类在程序运行期间动态生成,减少了代码的重复和冗余
    缺点:相对于静态代理,动态代理的实现更加复杂,需要深入理解反射机制和代理类的生成。

    静态、动态代理示例代码可以参考下面这篇文章:
    静态代理和动态代理

    JDK动态代理与CGLIB动态代理

    JDK动态代理:
    优点:Java本身支持,随着版本稳定升级
    缺点:目标类必须实现某个接口,没有实现接口的类型不能生成代理对象;代理的方法必须申明在接口中,否则无法代理执行速度性能相对于cglib较低

    CGLIB动态代理:
    优点:目标类无需实现接口;但是会针对目标类生成一个子类,覆盖其中的所有方法;执行速度性能会比JDK代理高
    缺点:若是目标类和目标类中的方法被final修饰,则无法代理动态创建代理对象的代价比较高

    可以参考下面这篇文章:
    谁与争锋,JDK动态代理大战CGLib动态代理

    文章中JDK、CGLIB代理都没有明确使用通知(Advice)方法,而是仅演示了如何使用代理创建对象和调用方法。这里只是演示如何创建的,在实际开发中JDK、CGLIB动态代理是不用我们自己实现的,Spring会自己创建。在Spring中默认动态代理策略是智能的,若是目标类没有实现任何接口,Spring会尝试使用CGLIB动态代理来创建代理对象,反之则使用JDK动态代理。

    实际开发中使用AOP可以参考下面链接
    基于springboot实现一个简单的aop

    SpringBoot

    SpringBoot是Spring开源组织下的子项目,主要简化了Spring的难度,减去了繁重的配置,是开发者能快速上手。

    BeanDeFinition是什么

    在Spring框架中,“BeanDefinition”是一个很重要的概念,它描述了一个Bean实例的基本信息,每个被Spring容器管理的Bean都有一个对应的“BeanDefinition”,其中包含了它的类名、作用域、构造函数参数、属性值、Bean之间的依赖关系、初始方法和销毁方法。

    约定大约配置是什么意思

    约定大于配置是一种开发原则,就是为了减少人为的配置数量,能使用默认的配置就使用默认的;默认配置就是所谓的“约定”;当存在特殊需求的时候,也可以自定义配置覆盖默认配置。,如我们需要在SpringBoot使用redis,我们需要去连接redis,需要用到URL、端口、密码等,SpringBoot其实就有默认的约定,默认连接本地端口6379的redis,没有密码

    SpringBoot的核心注解

    • @SpringBootApplication:标识该类为SpringBoot应用程序的主配置类;该类的main方法用于启动SpringBoot程序,在main方法中写入代码SpringApplication.run(xxx.class,args)可启动Spring容器。它是一个组合注解,包含了@SpringBootConfiguration、@EnableAutoConfiguration、和@ComponentScan。
    • @SpringBootConfiguration:标识这是一个配置类;被@configuration修饰。二者功能一致。
    • @ComponentScan:开启自动配置。

    SpringBoot支持什么前端模板

    thymeleaf、freemarker、jsp

    SpringBoot实现热部署有哪几种方式?

    热部署:就是程序检测到代码改动后会自动重新启动SpringBoot项目,程序员就不用手动的重启项目了。
    主要有两种方式

    1. Spring Loaded
    2. Spring-boot-devtools

    SpringBoot事务的使用

    首先使用注解@EnableTransactionManagement开启事务管理,然后在对应的方法添加注解@Transactional即可

    SpringBoot有哪几种读取配置的方式

    Spring Boot 可以通过 @PropertySource,@Value, @ConfigurationPropertie注
    解来读取配置并赋值。

    bootstrap.properties 和application.properties 有何区别 ?

    1. 记载顺序:bootstrap(.yml、properties)会比application配置文件先加载
    2. 属性覆盖:如果同一个属性在bootstrap和application中都有定义,bootstrap 中的属性将会覆盖 application 中的属性。这意味着 bootstrap 具有更高的优先级

    Spring Profiles

    Spring Profiles (Spring配置文件)是Spring框架中的一种机制,可以让程序在不同环境下使用不用的配置。对于开发、测试、生产环境之间的配置切换非常有用。

    当前有三个配置文件,application.yml、application-dev.yml、application-test.yml,你可以在application.yml指定使用哪套配置文件,被指定的配置文件与application.yml会组合成一个配置文件。
    applicatiom.yml:

    spring:
      profiles:
        active: dev // 指定配置文件
    
    • 1
    • 2
    • 3

    application-dev.yml

    my:
     applicationName: dev
    
    • 1
    • 2

    application-test.yml

    my:
     applicationName: test
    
    • 1
    • 2

    SpringBoot如何实现跨域

    跨域是指定客户端在使用浏览器/app发生ajax请求时,会触发同源策略(协议、ip
    、端口必须相同),当发现请求的服务器地址不同源时就会出现跨域
    解决跨域常用有两种方式

    1. jsonp:前端通过jsonp来解决,但是jsonp只能发生get请求。弊端很明显
    2. CROS

    常见的解决方案有 实现WebMvcConfigurer接口并重写addCorsMappings方法解决跨域问题

    @Configuration
    public class CorsConfig implements WebMvcConfigurer {
    	@Override
    	public void addCorsMappings(CorsRegistry registry) {
    		registry.addMapping("/**")
    		.allowedOrigins("*")
    		.allowCredentials(true)
    		.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
    		.maxAge(3600);
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    干货满满~如何解决跨域!

    如何使用 Spring Boot 实现全局异常处理

    可以结合注解 @RestControllerAdvice、@ExceptionHandler 一起使用实现全局异常处理

    1. @RestControllerAdvice:可以用于定于是一个组合注解,內部包括注解@ResponseBody、@ControllerAdvice
    2. @ControllerAdvice:可以对整个应用的控制器(所有的controller中的所有方法)进行一致性的处理。常用于定义全局异常处理、全局数据绑定、全局数据预处理等功能。
    3. @ExceptionHandler:用于在控制器中处理异常。当控制器中的方法抛出指定类型异常时,该注解修饰的方法就会被调用。

    Spring Boot 中如何实现定时任务 ?

    @Scheduled注解:用于创建定时任务;指定方法在特定的时间间隔或时间点执行。

    MYSQL

    MYSQL的锁
    全局锁

    使用全局锁之后,数据库的所有数据就处于只读状态,后续对于数据的CRUD操作都会被阻塞

    一般是用来进行数据备份的。对于MYISAM搜索引擎,它不支持事务,因此在备份数据的时候就需要使用全局锁来处理了。

    表锁

    表锁分为以下三种

    1. 表锁

    表锁:作用是对于某个特定的表加锁,支持读锁与写锁。

    • 表共享读锁:允许其它线程/事务读表,但不能写
    • 表独占写锁:写锁只存在一个。其它线程/事务就不能再对这个表加任何读锁或写锁。

    读锁与写锁之间:读读共享,读写互斥,写写互斥

    1. 元数据锁(MDL)

    MDL加锁过程是系统控制,也就是说对于用户而言是不透明的。MDL锁主要是为了解决DDL操作与DML操作之间的数据一致性问题。內部也是有写锁与读锁

    • 读锁:当我们对一个表的数据进行CRUD(DML)操作时,会自动加MDL读锁。
    • 写锁:当我们对一个表的结构(删除字段、更改字段的数据类型)进行更改时(DDL操作),会自动加MDL写锁,拿到写锁也是可以对表的数据进行DML操作的,但是需要注意的是 MDL写锁也只会存在一个

    其中 读读共享,读写互斥,写写互斥
    参考:MySQL(十二)MDL锁介绍

    1. Autu-INC锁

    Auto-INC锁是InnoDB存储引擎用来管理自增长列(主键自增)的一个锁。该锁确保每个事务获取的自增值是唯一的且是按照递增的顺序进行分配的。 其中auto-inc锁分为以下三种模式

    1. 传统模式

    innodb_autoinc_lock_mode=0;在传统模式中,事务结束后才会释放锁。事务中如果有对表进行添加操作添加1/n条数据,该事务就会获取该表的auto-inc锁,其它事务若想得到该表的auto-inc锁则会阻塞,直到该事务提交。这种模式保证了在事务中进行了多次添加操作,都可以保证该事务中添加的数据的自增值都是连续的。但有一个很大的弊端:在高并发的情况下,所有事务都需要排队获取auto-inc锁,性能与并发度不是很高

    1. 连续模式

    innodb_autoinc_lock_mode=0;截止到现在,mysql默认的auto-inc锁是连续模式;,连续模式就是在性能和自增值连续性之间进行一个折中选择;某种情况下放弃连续性保证性能。 连续模式针对单条/批量插入语句会出现以下两种情况。

    单条插入:对于A事务中单行的插入语句,mysql在生成自增值后就会立即释放auto-inc锁。这时其它事务不用等待事务/插入语句结束才能拿到auto-inc锁。完全放弃了连续性保并发性能。实例如下:

    当前有事务A、B,事务中有两条插入语句,事务B插入一条数据
    流程:事务A插入第一条数据并释放锁 此时事务A需要做其它表的DML操作导致事务B拿到auto-inc锁,事务B获取到锁插入一条数据,事务A再次获取锁插入一条数据。锁的资源占用顺序为:A->B->A 来看看事务A中的添加的两条数据自增值是否是连续的

    事务A:
     begin
     insert into auto_inc_test(name) 
     value ('A1');
    接着事务A需要执行其它表的DML操作。
    
    • 1
    • 2
    • 3
    • 4
    • 5

    事务A开启事务并添加第一条数据。insert执行完后释放了auto-inc锁,接着事务A需要执行其它表的DML操作。导致事务B拿到了该表的auto-inc锁。

    事务B:
     begin
     insert into auto_inc_test(name) 
     value ('B');
     commit
    
    • 1
    • 2
    • 3
    • 4
    • 5

    事务B开启事务,获取到auto-inc锁添加了一条数据并释放锁,同时提交事务

    事务A:
    insert into auto_inc_test(name) 
     value ('A2');
     
     commit
    
    • 1
    • 2
    • 3
    • 4
    • 5

    事务A再次获取auto-inc锁,并添加数据及提交事务
    此时我们查看auto_inc_test表中的数据:
    在这里插入图片描述
    可以发现在事务A中插入的两条数据的自增值并不是连续的,所以在连续模式中针对单条的insert语句,是完全放弃了连续性保证性能。

    批量插入:对于批量插入操作,事务会一直持有auto-inc锁直到语句结束。在语句结束后批量插入的数据的自增值是连续的,一定程度上放弃了连续性并保证性能。

    当前有事务A、B,事务中有两条批量插入语句,事务B一条批量插入语句。
    流程:事务A执行第一条批量插入并释放锁,此时事务A需要做其它表的DML操作导致事务B拿到auto-inc锁,事务B获取到锁执行批量插入,事务A再次获取锁执行批量数据。

    事务A:
    begin
     insert into auto_inc_test(name) values
    ('A1'),
    ('A2')
    
    接着事务A需要执行其它表的DML操作。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    事务A获取auto-inc锁,执行第一条批量插入语句,语句执行完成后释放锁。

    事务B:
     begin
     
     insert into auto_inc_test(name) values
    	('B1'),
    	('B2')
    
    COMMIT
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    事务B获取auto-inc锁,执行批量插入语句后释放锁并提交事务

    insert into auto_inc_test(name) values
    ('A3'),
    ('A4')
    
    COMMIT
    
    • 1
    • 2
    • 3
    • 4
    • 5

    事务A再次获取auto-inc锁,执行完第二条插入语句后释放锁并提交事务

    我们此时来看看事务A中批量插入的数据的自增值是否是连续,两条批量插入的数据的自增值是否是连续的

    在这里插入图片描述

    可以看到单条批量插入的数据是连续的,但是在同一个事务中多条批量插入语句之间数据的自增值并不连续。也就是该情况下 放弃一定程度的连续性保证性能。

    1. 交错模式

    innodb_autoinc_lock_mode=2;它完全放弃了自增值的连续,从而提高并发插入的性能。无论是单条/批量插入时,锁都是在生成自增值后立即释放。与连续模式中的批量插入不同的是,连续模式是批量插入语句执行完成之后才释放,也就是说在交错模式下可能会出现以下情况。

    当前事务A、B都同时执行了批量插入语句。那他们的自增值可能会出现下面的情况。

    事务A:
     begin 
     inert into auto_inc_test(name) values
     ('A1'),
     ('A2')
     commit
    
    事务B:
     begin
     insert into auto_inc_test(name) values
      ('B1'),
      ('B2')
      commit
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    在这里插入图片描述

    仔细看自增值的连续性是完成没办法保证的,而连续模式的批量插入一定程度上是可以保证单次批量插入语句的数据的自增值是连续的。但交错模式的并发性能肯定是会比连续模式好的。

    行锁

    作用:是InnoDB引擎锁定数据表中的特定行,是最细粒度的锁,可以最大程度的支持并发处理,但是也很容易造成死锁。 支持共享锁及排它锁。

    • 共享锁:加锁语句:LOCK IN SHARE MODE ,对于读操作(如SELECT table where … LOCK IN SHARE MODE查询),行锁通常在读取操作完成后隐式释放;需要注意的是:普通的SELECT语句是不会加共享锁的,必须显示拼接加锁语句(LOCK IN SHARE MODE)
    • 排它锁:加锁语句:FOR UPDATE;对于写操作(如INSERT、UPDATE、DELETE),行锁通常在对数据行的修改操作完成后隐式释放INSERT、UPDATE、DELETE语句会默认加上排它锁,不用显示拼接加锁语句

    注意:

    • 锁与锁之间的关系为:读读共享、读写阻塞。锁的获取与释放都是隐式的,通常在对应的操作完成后锁会隐式释放,但是对于事务,行锁就必须要等待事务提交后才会释放,这是因为事务的一致性要求,需要确保整个事务内的操作是原子的,要么全部执行成功,要么全部回滚(auto-inc锁除外)。这就是为什么说行锁很容易造成死锁。

    在这里插入图片描述

    在这种情况下,就很容易造成死锁。

    当事务B发现获取锁的资源被事务A占用,而事务A刚好也在等待事务B的锁资源,MYSQL就会立马判定这是死锁,会回滚某一事务从而解决死锁。

    如果事务获取锁的等待时间超过时间限制就会被认为是死锁的一部分。

    当MYSQL检测到死锁的存在,会自动中止/回滚某一个事务来解决死锁。但是对于回滚事务也是会占用系统资源(CPU)的,所以我们应尽量避免死锁。

    如何避免死锁?

    1. 让两个线程/事务的获取锁的顺序一致,这样就可以避免死锁。
    2. 可以限制锁的持有时间,或者是完成锁的操作后立即释放锁,减少死锁的风险。

    如何解决死锁?

    1. 在mysql中会自动处理死锁,当死锁产生时,会终止/回滚一个或者多个事务来解决死锁。
    2. 在Java中是需要手动处理死锁的,也就是说我们需要自己找到死锁的线程,从而终止线程来解决线程,但是要找到死锁的线程是非常难的,所以在java中没有很好的办法去解决已经产生的死锁,所以编写代码时就需要去思考避免死锁。
    意向锁

    意向锁的粒度也可以看成是表级别的锁;支持共享锁与排它锁。

    • 意向共享锁:表明事务打算在表中的行上设置共享锁。
    • 意向排它锁:表明事务打算在表中的行上设置共享锁。

    它的作用:

    • 加快上层资源对下层资源是否被占用的检查时间。
    • 实现了表锁与行锁共存。

    场景:在innoDB引擎中一个事务A正在对某个表T的第r行加了写锁,另一个事务B尝试去对整个表做操作,B尝试去对整个表加一个写锁。
    那它此时就要检查这张表是否有行锁,不管是有行写锁还是行读锁都是不允许对这个表加入表的写锁的。

    那mysql如何判断表中是否有行锁?

    未引入意向锁前:一行一行查询是否有行锁,如果都没加,就代表可以锁表。但这样的效率太低,数据表的数据可能是百万级别的。检索起来非常麻烦。

    引入意向锁后:事务A想对某一个加行锁,会先对表加上意向锁,之后再对具体行加上行锁。
    事务A加行读锁:事务A—> 加意向共享锁---->加行共享锁。
    事务A加行写锁:事务A----> 加意向排它锁---->加行排它锁。

    这样就可以快速检测到这个表是否有行锁锁定。

    在这里插入图片描述
    注意:意向锁之间是相互兼容的

    为什么是都是兼容的?

    事务A加了表的IX锁,或者IS锁,只代表事务A已锁定一行或者将要锁定一行。事务B当然也可以锁定其他的行,所以事务B肯定也是可以获得表的IS锁或者IX锁的

    这样也说明了 innoDB引擎支持表锁与行锁共存。

    意向锁与表锁之间的兼容关系如下:

    在这里插入图片描述
    这里的S锁和X锁是表级别的,意向锁不会与行级别的S锁和X锁冲突

    参考:意向锁的理解

    索引

    索引是帮助MYSQL高效获取数据数据结构。简单来说,数据库索引就像是书前面的目录,能加快数据库的查询速度

    索引会占据磁盘空间,索引虽然会提高查询速度,但是会降低更新表的效率。比如每次对表进行增删改操作,MYSQL不仅要保存数据,还要保存或更新对应的索引文件。

    常见的索引

    • 唯一索引:索引列中的所有值必须是唯一的,允许有空值。
    • 主键索引:是一种特殊的唯一索引,每个表只能有一个主键索引,不允许有控制。
    • 普通索引:是最基本的索引类型,没有唯一性或空值的限制。
    • 组合索引:将多个列组合在一起形成的索引,可以加速多列的查询。但要遵循最左前缀原则。
    • 全文索引:只有在MyISAM引擎上才能使用,只能在CHAR,VARCHAR,TEXT类型字段上使用,通过关键字找到记录行。

    B+tree指的是三层结构的树状结构类型,相对于二叉树而言,会降低IO成本。

    在这里插入图片描述
    在B+tree中叶子节点指的是最底层的节点其它层都叫非叶子节点,根节点也可以被称为非叶子节点,B+tree中的根节点可以是由多个节点组成的

    在B+tree中,叶子节点之间使用双向指针连接,最底层的叶子节点形成了一个双向有序链表。这样做的好处是使用范围查询时,不用每次都需要回到根节点重新遍历查找,而是从叶子节点中往后遍历即可。

    B树:其它都与B+tree相同,但是叶子节点之间是没有双向指针相连的。当MYSQL使用范围查询时,B树的查询效率相对于B+tree而言就慢许多。

    在MyISAM存储引擎中,叶子节点存储的数据是内存地址,而InnoDB引擎,叶子节点存储的数据为行记录。

    聚簇索引:也叫主键索引,在InnoDB中,如果表中没有设置主键索引,MYSQL会创建一个6字节的长整型的列作为主键索引。

    辅助索引:除聚簇(主键)索引之外的所有索引都称为辅助索引,InnoDB的辅助索引只会存储主键值而非行记录。

    最左前缀法则

    我们在使用组合索引时,最好遵循最左前缀法则,否则在查询时MYSQL会放弃索引查询,而去选择全表查询,这时效率就会低效。

    最左前缀法则:如果你创建一个多列索引,查询中的条件必须从索引的最左侧的列开始,并且不能跳过前面的列,按照一定顺序命中索引。这样的查询才能充分利用索引。

    这里可以参考:原创:史上最全最通俗易懂的,索引最左前缀匹配原则(认真脸)

    其中关于使用EXPLAIN语句中出现type列的解释如下

    • type=index:表示索引的全值匹配或前缀匹配;mysql使用索引查询,从索引文件中的第一个节点数据查找到最后一个节点数据,直到找到符合判断条件的某个索引
    • ref:表示索引的部分匹配;只是用索引文件的部分节点
    • range:表示 MySQL 在索引的一个范围内进行扫描
    • all:表示表执行全表扫描,不使用索引。查找表中每一行及每一列数据,效率最慢。

    索引失效的几种情况:

    • 语句前后没有同时使用索引。当 or 左右查询字段只有一个是索引,该索引失效,只有左右查询字段均为索引时,才会生效;
    • 数据类型出现隐式转化。如 varchar 不加单引号的话可能会自动转换为 int 型,使索引无效,产生全表扫描;
    • 在索引字段上使用not,<>,!=。不等于操作符是永远不会用到索引的
    • 当 MySQL 觉得全表扫描更快时(数据少);

    具体使用全表扫描还是索引查询,取决于查询优化器的决策。

    索引篇参考文章如下:

    存储引擎

    InnoDB存储引擎

    • 支持事务提供ACID(原子性、一致性、隔离性、持久性)事务特性,使InnoDB可以更好的处理复杂的业务逻辑保证数据的一致性
    • 支持行锁:锁定粒度更小,有助于提高并发性能
    • 支持行锁与表锁并存:在特定的场景下,支持意向锁和行锁共存
    • 自动增长列聚簇(主键)索引,方便插入新数据生成唯一的值。
    • 支持外键:可以定义和管理表之间的关系,确保数据的完整性。
    • 崩溃恢复:通过事务日志(redo log)实现崩溃恢复功能,可以在数据库崩溃后还原未完成的事务

    InnoDB适用于需要事务支持和高并发的场景。可以更好的确保数据的一致性。

    MyISAM存储引擎

    • 支持表锁:用表级锁定,这意味着在对数据进行并发访问和修改时,会锁定整个表,可能导致并发性能下降
    • 全文索引:支持全文索引,适用于对文本(CHAR,VARCHAR,TEXT)类型进行搜索的场景。如倒排索引

    MyISAM在读取密集型的场景中可能更有优势,MyISAM不支持事务,可能在某些情况下无法保证数据一致性。

    为什么说读取会比InnoDB引擎快,这是因为在使用辅助索引进行查询时,不会再出现回表的现象。因为辅助索引的叶子节点存储的也是这行数据的内存地址。

    事务

    一个事务由一组DML语句组成的,事务内的DML语句要么同时成功、要么同时失败。

    事务特性

    • 原子性(Atomicity): 原子性关注事务的执行单元(DML语句),确保它们同时成功/失败;即使事务中的一步操作失败,整个事务也会被回滚;确保数据的一致性,起到承上启下的作用。
    • 持久性:一旦事务被提交它对数据库的改变就是永久的(写入到磁盘中),即使系统崩溃,也能回复到事务提交后的状态。
    • 隔离性事务之间相互隔离。这意味着一个事务在执行过程中对数据的修改对其他事务是不可见的,直到事务被提交。例如读未提交(Read Uncommitted)、读已提交(Read Committed)、可重复读(Repeatable Read)和串行化(Serializable)
    • 一致性::事务在执行前后,数据库的状态应该保持一致。如数据在事务执行前后都是一致的。

    注:一致性建立在原子性、持久性、隔离性的基础上。

    MYSQL是如何保证事务原子性的?

    • 当事务开始时:MYSQL会为事务生成一个唯一的事务ID;
    • 在事务过程中:DML操作对数据的修改都会生成相应的Undo Log 记录,存储在Undo Log表中;
    • 事务提交:在事务执行提交后,MYSQL会将数据持久化到磁盘中,Undo Log的记录也会被刷新到磁盘中。
    • 事务回滚:如果事务执行过程中发生错误,MYSQL根据Undo Log表回滚数据。

    MYSQL如何保证事务持久性

    在事务中的DML除了会写入Undo Log日志中,也会被写入Redo Log日志中,但它们的用途不同,Undo Log 主要用于回滚,而Redo Log日志用来保证持久性。

    当系统崩溃后异常关闭,MYSQL在重新启动时会检查系统崩溃的情况,会根据Redo Log日志来恢复数据。

    • 回滚未提交的事务:在系统崩溃前未提交的事务,MYSQL会根据Redo Log日志记录来回滚这些事务,确保数据库回到一致的状态。
    • 重做已提交的事务:对于系统崩溃前已提交的事务,MYSQL会重新执行这些事务,确保数据库持久化到磁盘中(防止事务在提交后持久化到磁盘中时,持久化一半就突然断电)。

    事务的隔离级别

    1. 读未提交(Read Uncommitted):

      • 特点: 事务可以读取其他未提交的事务所做的修改。
      • 可能带来的问题: 脏读(读到其他事务未提交的数据)、不可重复读、幻读。
    2. 读已提交(Read Committed):

      • 特点: 一个事务只能读取已经提交的其他事务的数据修改。
      • 可能带来的问题: 不可重复读、幻读。
    3. 可重复读(Repeatable Read):

      • 特点: 事务在执行期间看到的数据是一致的,即使其他事务提交了修改。
      • 可能带来的问题: 幻读。
    4. 串行化(Serializable):

      • 特点: 所有事务按照严格的顺序依次执行,完全隔离事务之间的影响。
      • 可能带来的问题: 性能开销高,可能导致事务的等待时间增加。

    脏读:一个事务读取了另一个事务未提交的数据
    不可重复读:在一个事务内,相同的查询返回不同的结果。主要是同一行数据的修改。
    幻读:在一个事务内,相同的查询返回不同的行数。查询的数据行数与前一次查询发生改变。
    写偏斜:两个事务同时修改同一行数据,并最终只有一个事务的修改生效。发生在读已提交和可重复读隔离级别下。

    不可重复读和幻读有什么区别区别

    MVCC

    mvcc(多版本并发控制)提高数据库的并发访问,实现读-写不冲突。

    • 当前读
      像 select lock in share mode (共享锁), select for update; update; insert; delete (排他锁)这些操作都是一种当前读,为什么叫当前读?就是它读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁
    • 快照读
      像不加锁的 select 操作就是快照读,即不加锁的非阻塞读;快照读的前提是隔离级别不是串行级别,串行级别下的快照读会退化成当前读;避免了加锁操作,降低了开销;既然是基于多版本,即快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本

    MVCC 就是为了实现读-写冲突不加锁,而这个读指的就是快照读, 而非当前读,当前读实际上是一种加锁的操作,是悲观锁的实现

    当前读,快照读和MVCC的关系

    • MVCC 多版本并发控制是 「维持一个数据的多个版本,使得读写操作没有冲突」 的概念,只是一个抽象概念,并非实现
    • 因为 MVCC 只是一个抽象概念,要实现这么一个概念,MySQL 就需要提供具体的功能去实现它,「快照读就是 MySQL 实现 MVCC 理想模型的其中一个非阻塞读功能」。而相对而言,当前读就是悲观锁的具体功能实现
    • 要说的再细致一些,快照读本身也是一个抽象概念,再深入研究。MVCC 模型在 MySQL 中的具体实现则是由 3 个隐式字段,undo 日志 , Read View 等去完成的,具体可以看下面的 MVCC 实现原理

    三个隐式字段

    每行记录除了我们自定义的字段外,还有数据库隐式定义的 DB_TRX_ID, DB_ROLL_PTR, DB_ROW_ID 等字段

    • DB_TRX_ID
      6 byte,最近修改(修改/插入)事务 ID:记录创建这条记录/最后一次修改该记录的事务 ID
    • DB_ROLL_PTR
      7 byte,回滚指针,指向这条记录的上一个版本(存储于 rollback segment 里)
    • DB_ROW_ID
      6 byte,隐含的自增 ID(隐藏主键),如果数据表没有主键,InnoDB 会自动以DB_ROW_ID产生一个聚簇索引

    undo log日志

    记录类型主要分为两种

    • insert undo log:代表事务再insert新纪录时产生的undo log日志,只在事务回滚时需要,当事务提交后立即删除丢弃
    • update undo log:事务再进行updatedelete时产生的undo log日志;不仅在事务回滚时需要,在快照时也需要;所以不能随便删除。

    undo log产生的日志记录是以单链表形式存储。其中头链表是最新的修改记录。如下:
    在这里插入图片描述

    注:undo log日志只会事务中DML操作后的记录,如果DML操作没有在事务中执行,undo log是不会记录的,这是为了方便事务回滚操作。

    Read View(读试图)

    什么是 Read View,说白了 Read View 就是事务进行快照读操作的时候生产的读视图 (Read View),在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的 ID (当每个事务开启时,都会被分配一个 ID , 这个 ID 是递增的,所以最新的事务,ID 值越大)

    Read View遵循一个可见性算法,主要是将要被修改的数据的最新记录中的 DB_TRX_ID(即当前事务 ID )取出来,与系统当前其他活跃事务的 ID 去对比(由 Read View 维护),如果 DB_TRX_ID 跟 Read View 的属性做了某些比较,不符合可见性,那就通过 DB_ROLL_PTR 回滚指针去取出Undo Log中的 DB_TRX_ID 再比较,即遍历链表的 DB_TRX_ID(从链首到链尾,即从最近的一次修改查起),直到找到满足特定条件的 DB_TRX_ID , 那么这个 DB_TRX_ID 所在的旧记录就是当前事务能看见的最新老版本

    Read View的三个全局属性

    • trx_list(随意取的):读试图生成时刻,系统中正在活跃的事务ID列表
    • up_limit_id:是trx_list列表中最小的事务ID
    • low_limit_id:ReadView 生成时刻系统尚未分配的下一个事务 ID ,也就是 目前已出现过的事务 ID 的最大值 + 1

    可见性算法如下:

    • 首先比较 DB_TRX_ID < up_limit_id ,这个DB_TRX_ID指的是快照读的记录中的事务ID, 如果小于,则说明当前的记录的事务在快照生成时刻就已经提交了,则当前事务能看到 DB_TRX_ID 所在的记录,如果大于等于进入下一个判断
    • 接下来判断 DB_TRX_ID >= low_limit_id , 如果大于等于则代表 DB_TRX_ID 所在的记录在 Read View 生成后才出现的,那对当前事务肯定不可见,如果小于则进入下一个判断
    • 判断 DB_TRX_ID 是否在活跃事务列表之中,trx_list.contains (DB_TRX_ID),如果不在,则说明,你这个事务在 Read View 生成之前就已经 Commit 了,你修改的结果,我当前事务是能看见的;如果在,则代表我 Read View 生成时刻,你这个事务还在活跃,还没有 Commit,你修改的数据,我当前事务也是看不见的,此时就会去undo log中找到前一版本的记录,重新执行可见性算法,直到找到满足条件的记录。

    RC , RR 级别下的 InnoDB 快照读有什么不同?

    RC(读已提交) 隔离级别下,是每个快照读都会生成并获取最新的 Read View;而在 RR (可重复读)隔离级别下,则是同一个事务中的第一个快照读才会创建 Read View, 之后的快照读获取的都是同一个 Read View。

    快照命中的是某个时间点的所有数据,在RR模式下事务将一直使用这个一致的数据视图。

    参考文章:【MySQL笔记】正确的理解MySQL的MVCC及实现原理

    redis篇

    八大数据类型

    String(字符串)

    数据类型:key value

    • 存储:set key value
    • 取出:get key
    • 删除一个或多个键:del key[key key…]

    哈希(hash)

    数据类型:key field value1,其中field1,value可以看出是key的value之一,也就是说hash类型是集合,在这个key中也会有field2 value2等。

    • 存储:hset key field value1
    • 取出:hget key field
    • 删除一个或多个指定字段的值:hdel key field[field field…]
    • 获取哈希keu中所有字段:hkeys key

    列表(list)

    数据类型:key values
    该数据类型是有序,根据你插入的顺序进行排序

    • 存储:lpush key value[value value …];往队头插入,最先插入的元素会往队尾靠近。
    • 存储:rpush key value;往队尾插入,最先插入的元素会往对头靠近。
    • 取出:lindex key index(下标):每次插入的元素都有对应下标。
    • 范围取出:lrange key index index;lrange key 0 -1 取出全部
    • 删除:lrem key count value:count为要删除的元素个数,当count>0时:从列表头部开始向列表尾部移除指定元素,移除数量为 count;count<0:从列表尾部开始向列表头部移除指定元素,移除数量为 count 的绝对值;count=0:移除所有与指定元素相等的元素。
    • 从对头删除第一个元素:Lpop key
    • 从队尾删除第一个元素:Rpop key

    集合(set)

    数据类型:key values ,其中內部元素是无序并且不允许重复元素

    • 存储:Sadd key value [value value …]
    • 全部取出:Smembers key
    • 随机取出:srandmember key num;num为要随机取出多少个元素。
    • 删除:Srem key value[value value…]:删除一个或多个成员元素,value为具体元素内容,无法一次删除集合中所有元素,只有元素都被删除了,这个集合才会被自动删除。

    有序集合(Zset)

    数据类型:key values;其中內部元素有序且不允许重复元素,其中value被拆分为 score(分数) member(成员名称,也可以将这个看成value)

    • 存储:Zadd key score member[score member score member …];score为自定义的分数,分数的数据类型必须为int类型,Zset会根据分数对这个集合的元素进行排序。
    • 范围取出:Zrange key score score:元素按分数从小到大排序返回
    • 范围取出:Zrevrange key start end:元素按分数从大到小排序返回
    • 删除:Zrem key member[member member…]:删除一个或多个元素

    地理空间数据类型(GEO)

    数据类型:key values,其中value被拆分为 longitude(经度) latitude(纬度) member(位置名称),常用于存储地理位置信息。

    • 存储一个或多个地理空间:GEOADD key longitude latitude member [longitude latitude member …]
      在这里插入图片描述

    • 取出key中指定成员的地理位置:GEOpos key member[member …]

    • 范围取出:Zrange key start end

    • 获取两个成员之间的距离:GEODIST key member1 member2 [unit];unit可以不填,默认单位为m

    • 在指定的键中,查找给定成员周围一定范围内地临近成员:GEORADIUSBYMEMBER key member radius m|km|mi|ft [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key]
      在这里插入图片描述

    • 删除指定成员:ZREM key member[member …];与Zset数据类型的删除指令一样

    基数统计(Hyperloglog)

    基数:不重复的元素
    Hyperloglog常用来统计一个网站的浏览量,统计出来的数据是去重的,但是估算值不是特别准确。但在大多数实际应用场景中,其对基数估算的性能和内存效率都是相当可观的

    数据类型:key values

    • 存储一个或多个元素:PFADD key element[element …]
    • 返回估算值:PFCOUNT key[key …],这里需要注意的是,估算的数值是去重的
    • 合并多个HyperLogLog:PFMERGE destkey sourcekey[sourcekey]:destkey为合并后的key,数据也都会出现在这里,但这些数据是去重过的。可以看成是无重复的并集。

    位存储(Bitmap)

    应用场景,统计网站中登录和未登录的用户人数,这时候我们知道用户都是一个个user对象,统计起来也是特别麻烦,这时候可以使用Bitmaps,它是位图
    它是一种数据结构,都是操作二进制来进行记录,就只有0和1两个状态

    模拟一个用户该周的七天登录情况
    在这里插入图片描述
    语法:setbit key offset(偏移量) value(0/1)
    zhangsan:泛指单个用户
    0:本周的第一天
    1:登录 0 :未登录

    这样该周的打卡情况就出来了

    查看某一天有没有登录
    在这里插入图片描述
    getbit zhangsan 0:可以看成是该用户周一有没有登录,

    统计打卡的天数

    在这里插入图片描述
    语法:bitcount key
    统计key为zhangsan的value为1的总和,看成是总打卡量

    发布订阅

    缺点:消息无法持久化,PubSub的消息是不会持久化的,redis宕机就相当于一个消费者都没有,所有消息直接丢弃。如果开始有三个消费者,一个消费者突然挂掉了,生产者会继续发送消息,另外两个消费者可以持续收到消息。但是挂掉的消费者重新连上的时候,这断连期间生产者发送的消息,对于这个消费者来说就是彻底丢失了。

    参考链接:Redis发布订阅以及应用场景介绍

    redis事务

    redis事务的本质是一组命令的集合。

    • 它没有隔离性的概念,一组命令在发送EXEC(提交)命令前会放入队列缓存中,并不会立马执行,而是在提交后才执行,也就不存在在事务查看具体的数据。在事务执行期间,其它客户端仍然可以读写事务锁定的键。
    • 不保证原子性:在redis中单条命令是原子性的,但事务不保证原子性,且没有回滚,事务中任意命令执行失败(出现语法错误),其余的命令仍会执行。但是事务内的命令出现命令级(错误指令-setter key)错误时,事务内所有命令都不会执行
    • 在redis中,同一个客户端的事务是不能同时提交的,也就是说同一客户端的事务是串行执行的,来自不同客户端的事务可以在同一时间内并发执行。

    参考链接:Redis之Redis事务

    watch监视器

    watch监视器通常与事务一起使用,用来实现乐观锁。

    watch用来监视一个或多个键,如果被监视的任何一个键被其他客户端修改,当前客户端的事务就会被打断,EXEC 命令将返回 nil,表示事务执行失败。

    WATCH 命令必须在事务开始前使用,通常是在 MULTI 命令之后,EXEC 命令之前。

    主要使用场景为:

    • 防止并发修改:当多个事务需要使用同一个键时,可以使用watch命令确保在同一时间只有一个事务执行成功。
  • 相关阅读:
    Vue3项目创建+组合式API使用+组件通信+渲染微博热搜+打包上线
    LISTAGG () 和STRING_AGG () 函数的区别与简单使用
    Java 将Excel转为UOS
    Qiskit系列(1)---Qiskit安装
    canal+es+kibana+springboot
    经典文献阅读之--EGO-Planner(无ESDF的四旋翼局部规划器)
    蓝桥杯算法题汇总
    深度学习-吴恩达 作业 Tensorflow环境部署
    @Autowired和@Resource的区别
    Java版工程行业管理系统源码-专业的工程管理软件- 工程项目各模块及其功能点清单
  • 原文地址:https://blog.csdn.net/qq_60264381/article/details/133030254