推荐的几个八股文网站

JAVA面试

面向对象和面向过程

面向过程编程是以过程为中心的编程思想,将问题拆解为一系列的过程和步骤或者函数来解决问题,它的代码重用很差,但是效率较高。面向对象编程是以对象为中心,通过将问题拆解为对象的行为和属性和方法,通过不同对象的交互来解决问题,适合复杂和模块化的问题,代码重用性高。

Stream流

它是java8以后引入的新特性,它可以对数据源比如集合 数组 io进行一系列的操作序列,像一个流水线一样将数据源一步一步的操作,它支持并行计算,大大提高了对数据源的处理效率。

final关键字介绍一下

final修饰类的话 类不能被继承,修饰方法,方法不能被重写,修饰变量,变量只能赋值一次,一般修饰的时候就赋值了。

finally在try catch

操作一些需要手动释放的资源时,保证这些资源能被正确关闭。即便在try块中发生异常,或者try块中的代码执行了return语句,finally块中的代码依然会执行。

StringBuffer与StringBuilder

StringBuffer:可变字符串、效率低、线程安全;适合多线程,每个线程都加了synchronized

StringBuilder:可变字符序列、效率高、线程不安全;适合单线程

几种变量的区别

变量类型 定义位置 作用域 生命周期 初始化 共享性
局部变量 方法 / 代码块内部 声明它的方法 / 代码块内 方法 / 代码块执行期间 必须手动初始化 不共享
全局变量 类外部(Java 通过static模拟) 整个程序 程序运行期间 必须手动初始化 所有类共享
成员变量 类内部,无static 整个类内部 对象创建到销毁 可默认初始化 每个对象独立副本
静态成员变量 类内部,有static 整个类内部 类加载到卸载 可默认初始化 所有对象共享

线程池里面需要配置哪些主要参数

线程池(ThreadPoolExecutor)的主要配置参数,有核心线程数,最大线程数,线程空闲时间,时间单位,任务队列,线程工程和拒绝策略。线程创建的过程首先是提交任务,调用线程池的时候会判断当前任务的线程数是否小于核心线程数,如果小于,就创建核心线程,当运行的线程数等于核心线程数的时候,将任务放入任务队列中,如果还有新的任务来,任务队列满了,并且当前核心线程数小于最大线程数,会创建非核心线程数。如果达到最大线程数并且任务队列已满,会执行你设置的拒绝策略,最后任务执行完了,会销毁线程。

创建线程

线程的创建底层其实都是通过Thread类去实现,他有两个核心方法,一个start方法,一个run方法,start方法是启动线程,此时线程会等待cpu调度资源,当线程被调度以后会回调到线程,然后再执行run方法,对任务逻辑进一个执行。所以启动线程的方式只有thread的start方法,我们可以通过以下几种方式写我们的任务逻辑,在java中我们可以通过继承thread类,并且重写run方法,写我们的任务逻辑执行任务。还有通过实现runnable接口的run方法,会通过thread的构造函数,传入进去。也可以去结合futureTask和callable(有返回值)接口,实现异步任务的执行。

线程池如果实现复用

线程池的复用也是线程池的核心思想,它并不是线程使用完以后再拿回来,它的实现是通过线程池里面维护的阻塞队列实现的,当线程执行完一个任务以后,不会立刻结束,而是通过自旋的方式,它会从阻塞队列调用take或者poll方法,获取新的任务。如果队列中有任务,那线程就执行新的任务,如果队列中任务为空,那就要分几种情况,根据核心线程数,最大线程数,超时时间,是否允许回收。比如当前线程数没有达到核心线程数,那它会进入等待状态,等待阻塞队列来新的任务,如果超过核心线程数,并且允许回收,并且没任务了,就会回收,这样的逻辑实现线程的复用。

线程池为什么不能使用无界队列

阻塞队列什么情况会阻塞

一般用于多线程之间实现线程安全通讯和协调

  • 阻塞操作
    • 当队列已满时,向队列中添加元素的操作(如put)会被阻塞,直到队列有空闲位置。
    • 当队列为空时,从队列中获取元素的操作(如take)会被阻塞,直到队列中有元素可用。
  • 线程安全:内部实现了锁机制(如ReentrantLock),确保多线程环境下操作的安全性。

线程的状态

在JAVA中封装了线程的六种状态,第一种是new,当前线程被创建了,但是没有执行start方法,第二个就是runnable,此时线程已经执行了或者等待执行,此时CPU会调度资源分配任务给该线程,第三个是blocked,线程中有某个线程拿到了锁,其他线程会进去blocked阻塞状态,等待锁释放,第四个是waiting,线程等待其他线程执行完,这里我们一般是手动的去wait或者park,第五个是现在wait park加时间参数,线程会进入Time waiting状态,sleep同样会进去该状态。最后一个就是终止状态,执行完run方法以后进入终止状态。

wait和blocked区别

wait和blocked触发条件不一样,首先wait是用户通过wait或者park手动主动去触发,然后需要通过notify进行一个手动唤醒。但是blocked是被动去触发,它是当遇到其他线程拿到锁以后,该线程会进入阻塞状态,直到线程拿到锁以后在会去执行任务。wait在执行的时候会主动的去释放锁,而blocked是要拿到监视器锁以后等待执行完再去释放锁。

ThreadLocal

threadlocal是单线程的副本变量,它是属于当前线程的,线程之间互相不影响,保证了线程变量的安全。它的结构是在thread里面有一个threadlocalmap是kv的集合,当我给threadlocal set的时候会将当前线程threadlocal对象当key 值为value。get也是一样,通过当前threadlocal在threadlocalmap中获取value。存在一个内存泄露问题 threadlocal和thread引用在栈中,对象在堆中,产生关键,thread里面还有一个threadlocalmap,他也是在堆中,并且和thread关联,同时它的key是弱引用,并且和threadlocal对象关联,这样在垃圾回收的时候,就回收只有弱引用关联的threadlocal,又因为value是强引用,thread存在value就存在。不销毁就一直存在,所以map会有一个key为null,value存在的情况,在线程池的情况下,线程又不会销毁,而是一直复用,那就会导致一直添加空key又有value导致内存溢出。所以我们需要在每次回收的时候手动remove将强引用的value删除。

synchronized和ReentrantLock锁

synchronized是java中的一个关键字,而ReentrantLock是jdk提供的一个类,synchronized是jvm层面的,它是非公平锁,底层像悲观锁,自动加锁和自动释放。而ReentrantLock是api层面的锁,它可以实现非公平锁和公平锁,底层像乐观锁,加锁释放锁需要通过lock 和 unlock。

CAS 悲观锁和乐观锁

CAS是比较和交换,它的作用就是当几个线程同时执行一个任务的时候,线程会有一个old value 和 一个new value,它可以比较任务的状态值,一般状态值会有0,1两个状态,线程也会有0和1两个状态值,如果某个线程执行任务的时候,发现任务的状态值和自己old value一样,就会把任务的状态值改成new value,这样其他线程来的时候发现任务的状态值和自己的oldvalue不一样,就进入自旋,重复cas操作,直到任务状态和自己的oldvalue一样。那悲观锁就是线程悲观的认为线程一定会发生冲突,就会提前加锁防止其他线程同时并发操作。而乐观锁就是乐观的认为线程不会出现冲突,它就会进行一个检查,检查资源是否被修改了,若未被修改则成功提交,否则重试或放弃。

饿汉式和懒汉式

都是单例设计模式,饿汉式就是在类加载的时候就会迫不及待的创建实例对象,懒汉式是当类被加载的时候并不会实例化对象,而是在第一次执行getinstance方法中实例化对象,一般会在方法中加锁保证线程安全。饿汉式的加载模式实现简单,因为是jvm保证天然线程安全,但是会浪费资源的占用,因为有时候实例没有被使用就会资源浪费,而懒汉式实现相对复杂,第一次获取实例的速度比较慢,但是节约内存因为是按需创建的。

nginx.confsites-available 目录下创建配置文件,示例:

1
2
3
4
5
6
7
8
9
10
server {
listen 80; # 监听端口
server_name example.com; # 域名或IP

location / {
proxy_pass http://backend_server; # 后端服务器地址
proxy_set_header Host $host; # 传递原始请求头
proxy_set_header X-Real-IP $remote_addr; # 传递真实IP
}
}

关键配置参数说明

  • proxy_pass:后端服务器地址(支持 HTTP/HTTPS)
  • proxy_set_header:传递客户端请求头信息
    • Host:保留原始请求的域名
    • X-Real-IP:传递客户端真实 IP
    • X-Forwarded-For:记录完整代理链 IP
    • X-Forwarded-Proto:传递请求协议(HTTP/HTTPS)

简述一下JAVA三大特性

Java 的三大特性:

  1. 封装:将数据和操作数据的方法绑定在一起,隐藏内部实现细节,通过公共接口访问。
  2. 继承:子类继承父类的属性和方法,实现代码复用和扩展。
  3. 多态:同一方法可以根据对象类型的不同表现出不同的行为,通过继承和接口实现。

集合

Java 集合主要分为两大接口:

  1. Collection:存储单个元素的集合。
    • List:有序可重复,如ArrayListLinkedList
    • Set:无序不可重复,如HashSetTreeSet
    • Queue:队列,如LinkedListPriorityQueue
  2. Map:存储键值对的集合,如HashMapTreeMapConcurrentHashMap

linkedlist和 ArrayList 的对比

linkedlist底层是双向链表存储的非连续内存,而arraylist是动态数组,它是连续内存存储的。那他们的扩容机制也是不一样的这也和他们底层实现是有关系,arraylist底层是动态数组,所以容量不足的时候,会创建新数组,再把元素复制,那一般新数组长度是原来的1.5倍,那linkedlist因为是链表的关系,它在插入时候是动态创建新节点。他们的共同点就是线程都不是很安全,需要synchronizedList包装。或者用copyonwirtearraylist。选择的话看插入数据的时候,如果是在尾端或者经常需要索引值查元素,这个时候选用arraylist,如果需要元素放头部或者中间,并且插入操作比较多,就选择linkedlist。

hashMap底层实现

hashmap底层分java8之前和之后,因为在8的时候对它进行了一个优化,在8之前是采用一个链表+数组的方式去存储数据,那么在8以后是加了一个红黑树的方式存储,当数组长度大于64,链表超过8的时候,会从链表转成红黑树,提高了数据量大的时候的一个查询效率。因为都是数组+链表或者红黑树来实现,其实就是插入数据的时候会根据一个hash值进行数组的插入,也就是哈希桶么。如果发生hash冲突了,那么会在同样的hash值下面创建一个链表,是这样来存储数据的。通过 (n - 1) & hash 确定元素位置,如果要存100个元素,容量设为多大,100/0.75么,负载因子是0.75,再加1,然后因为容量要是2的幂所以需要向上找2的幂。

ConcurrentHashMap和hashmap的区别

最大的区别其实就是在多线程的情况下,hashmap是线程不安全的,因为底层原理是链表+数组么,并没有加锁,所以在高并发的情况下肯定会导致数据不一致,或者链表成环导致死循环的问题。而concurrenthashmap底层在以前是分布段锁+CAS比较时交换,那在JAVA8以后是使用CAS加synchronized锁,它会对链表的头结点加锁,这样在其他线程修改值的时候就会等待 它的特点是对数据在读操作不加锁,在写入操作先通过CAS插入,失败的话就加synchronized锁,提高并发效率。

ConcurrentHashMap 为何禁止 null 作为 key?

  • 并发场景下,null 会导致 逻辑歧义get(null) 返回 null,无法区分 “key 不存在” 和 “value 为 null”;
  • 设计上保持与 Hashtable(线程安全且禁止 null key)的一致性,避免并发判断复杂,保障线程安全逻辑清晰。

如何创建线程

Java 创建线程的三种方式:

  1. 继承 Thread 类

    1
    2
    3
    4
    5
    6
    7
    // 使用:new MyThread().start();
    public class MyThread extends Thread {
    @Override
    public void run() {
    System.out.println("Thread running");
    }
    }
  2. 实现 Runnable 接口

    1
    2
    3
    4
    5
    6
    7
    // 使用:new Thread(new MyRunnable()).start();
    public class MyRunnable implements Runnable {
    @Override
    public void run() {
    System.out.println("Runnable running");
    }
    }
  3. 实现 Callable 接口(支持返回值):

    1
    2
    3
    4
    5
    6
    7
    // 使用:通过FutureTask和Thread执行
    public class MyCallable implements Callable<String> {
    @Override
    public String call() {
    return "Result";
    }
    }

介绍Mybatis

半自动 ORM(对象关系映射)框架,通过 XML 或注解将 SQL 语句与 Java 对象绑定,一般用#{name}的形式占位

  • XML 配置:通过 <select><insert> 等标签编写 SQL。
  • 注解支持:使用 @Select@Insert 等注解直接在接口中定义 SQL。

mybatis和mybatis-plus区别

mybatis是一般是用xml和注解手动编写SQL,mybatis-plus是内置了很多UURD的方法,基础的SQL编写代码会少很多。这样的话mybatis就比较适合一些复杂的业务逻辑,多表查询还要分组统计什么的。mybatis-plus的话复杂查询就需Lambda 表达式或 Wrapper 条件构造器来查询多表。分页是使用PageHelper(页码,每页显示) 。

如何防止SQL注入

优先使用 #{} 占位符将参数作为预编译语句的占位符(?)处理,底层使用 PreparedStatement,完全避免 SQL 注入。参数值(如 1' OR '1'='1)被自动转义为字符串,作为数据而非 SQL 代码处理。mybatis-plus的话是 Wrapper 条件构造器会自动使用预编译。使用lambda时候,参数类型会严格校验,防止非法 SQL,内置的很多插件都会防止sql注入。

properties和yml和yaml的加载顺序和优先级以及区别

properties是键值对,yml和yaml是树形结构么,Spring Boot 会按properties,yaml,yml加载同一目录下的配置文件,后加载的覆盖先加载的。特定环境配置(如 application-dev.properties)会覆盖默认(application.properties)。

重载和重写的区别

定义位置不同:

  • 重载发生在同一个类中,是多个同名方法因参数列表不同(类型、数量、顺序)而共存的现象;
  • 重写发生在子类与父类之间,子类通过相同参数列表覆盖父类的方法实现。

多态:动态编译看左运行看右,静态编译看左运行看左

重载属于编译时多态(静态绑定)编译器根据调用时的参数类型直接确定方法版本。

1
2
3
void test(int i) { ... }  // 方法1
void test(String s) { ... } // 方法2
test(10); // 编译时确定调用方法1

重写属于

运行时多态(动态绑定)JVM 根据对象实际类型决定执行哪个方法

1
2
Parent p = new Child();
p.method(); // 运行时调用Child的method()

==和equals区别

  • “==”是运算符,如果是基本数据类型,则比较存储的值;如果是引用数据类型,则比较所指向对象的地址值
  • equals是Object的方法,比较的是所指向的对象的地址值,一般情况下,重写之后比较的是对象的值。一般情况我们创建的类都会对equals方法进行重写
1
2
3
4
5
6
7
8
9
String s1 = "hello";
String s2 = "hello";
System.out.println(s1 == s2); //true,比较地址值:内容相同,因为常量池中只有一个“hello”,所以它们的地址值相同
System.out.println(s1.equals(s2));//true,比较内容:内容相同,因为常量池中只有一个“hello”,所以它们的地址值相同
System.out.println(s1.equals("hello")); //true
String s3 = new String("hello");
String s4 = new String("hello");
System.out.println(s3 == s4); //false,比较地址值:s3和s4在堆内存中的地址值不同
System.out.println(s3.equals(s4)); //true,比较内容:内容相同

重写 equals()必并重写 hashCode

Java 规范要求:equals 相等的对象,hashCode 必须相等。若仅重写 equals,不重写 hashCode,会导致 哈希集合(如 HashMapHashSet)逻辑错误(比如两个 equals 相等的对象因 hashCode 不同,被哈希表判定为不同键 / 元素,破坏唯一性)

缓存与数据库如何保证数据一致性

  • Cache-Aside 模式:更新数据库后,删除缓存(而非更新,避免并发冲突);
  • 延时双删:更新数据库 → 删缓存 → 短暂休眠(避免脏读)→ 再删缓存(处理并发更新延迟);
  • 最终一致性:结合消息队列异步同步,保证最终数据一致。

JVM

jvm jre jdk

JVM是java的虚拟机,帮助Java 实现 “一次编写,到处运行”,同时也是java跨平台使用的依靠,jre是运行java的环境,它包含了jvm,如果只运行编译好的jar包或者class文件,那么只需要有jre就行,但是如果要开发java,少不了需要编译,这时候就需要有jdk,jdk包含了jre和jvm,提供了很多java 的开发工具。编译器(javac),打包工具jar

JUC

Spring面试

IOC和AOP

是spring的两大重要特性,IOC就是控制反转、注入依赖么。他就是Spring将我们的类通过的方式,将他的创建和生命周期都放到Spring容器中统一管理,这样我们其他的类如果需要调用某个类就可以直接到Spring的容器工厂中去找。AOP就是IOC的一个扩展,他是面向切面编程,其实就是一个动态代理,通常情况他的作用就是能实现我们非业务的代码,不去影响实际的业务,其实就是在不动已经完成的业务代码,想要在加上非业务的代码的时候会用到AOP思想,比如要看日志,算接口时间,事务管理等。AOP有五大通知,前置 后置 环绕 异常 最终,添加和业务无关的逻辑。

Bean的生命周期

IOC的加载其实就是一个Bean的创建么,Bean从创建到销毁就是它的生命周期,分为实例化,它是通过反射去找构造函数实例化的。再属性赋值,解析自动装配的,一般用@autowired或者@resource,解决了循环依赖的问题。接着是初始化,初始化的时候调用初始化生命周期回调,这个时候就可以使用该类了,最后容器关闭的时候,调用销毁生命周期回调,将Bean进行一个销毁处理。

Spring 的自动装配过程如何识别要装配的类?会扫描全部类吗?

Spring 识别要装配的类主要通过 组件扫描(Component Scan),而非扫描项目中的全部类,具体逻辑如下:他会扫描spring用注解标记的一些类,将标记的类注册为 Bean。开发者需在配置中指定扫描的包路径(如@ComponentScan("com.example")),Spring 仅扫描该路径下的类,不会扫描所有类,以此提升效率。

Spring Boot 自动配置原理?

  1. 注解驱动@SpringBootApplication 包含 @EnableAutoConfiguration,触发自动配置;
  2. SPI 机制:Spring 扫描 META-INF/spring.factories,加载所有 AutoConfiguration 类;
  3. 条件装配:通过 @Conditional(如 @ConditionalOnClass@ConditionalOnBean)判断环境是否满足,满足则自动创建 Bean。

@autowired和@resource

autowired是按照类进行注入是Spring自带的,resource是按照对象名进行注入,对象名找不到就找类,它属于jdk

SSM

Spring + Spring MVC + MyBatis 三个框架的组合,是 Java 企业级开发中经典的 Web 应用架构。这三个框架各司其职,共同构成了一个完整的分层开发体系:

  • Spring:负责全局管理(IoC 容器、AOP 切面)和组件协调
  • Spring MVC:作为表现层框架,处理 HTTP 请求和响应;
  • MyBatis:作为数据访问层框架,简化数据库操作。

TODO JDK动态代理和CGLIB动态代理

拦截器的使用场景

拦截器其实就是一个AOP的思想,最常见的就是用户登录的时候进行身份验证和权限校验,还有日志的记录和监控,还有对传参出参的数据进行校验,看是否符合业务需求,还有前端跨域请求也有用到。

拦截器和过滤器的区别

来源不同过滤器来自servlet而拦截器是spring,他们也是实现了不同的类,进行方法的实现,过滤器实现了filter接口,底层是通过一个filterchain类进行链的操作实现过滤的,而拦截器是实现handlerInterceptor接口。拦截器底层是通过反射实现的。他们在项目中所在位置也不一样,拦截器更接近controller层,所以经常用来做登录校验,过滤器就用来做编码规则等功能。

Spring事务

Spring事务的创建

用一个注解@Transactional,在service层使用该注解设置隔离级别 传播级别 异常处理。复杂逻辑通常使用TransactionTemplate,核心都是保持业务操作数据时的原子性。

Spring事务的隔离级别

默认状态和数据库的隔离级别保持一致,还有读未提交,读已提交,可重复读和串行化。

Spring事务的失效

第一个就是对非public方法,使用事务的时候会失效,因为事务其实基于AOP的,那AOP源码里面有写使用注解如果该方法为非public则会返回一个null值,第二个就是同一个类中,非事务方法中调用了事务方法也会导致失效,第三个就是事务在异常处理的时候,事务内部没有对异常抛出或者进行处理,导致事务无法回滚,第四个就是数据库不支持事务的话也会失效。

Spring事务的传播行为

其实是一个事务调用另一个事务的情况,该事务的几种执行情况。具体不了解。

Spring 中两个 id 相同的 bean 会报错吗?

会报错。Spring 容器中,id 是 bean 的唯一标识。若配置重复 id,容器初始化时会抛出 BeanDefinitionStoreException(冲突检测失败)。

MYSQL面试

关系型数据库和非关系型数据库的区别

关系型数据库有固定的表结构像mysql这种表和表之间存在关联,非关系型数据库没有固定的表结构,所以比较灵活。

DDL和DML

DDL是对数据库表进行的操作,比如修改表结构,创建表,删除表。DML是对表数据的操作,比如插入数据,删除数据修改数据。

笛卡尔积

其实就是多表连接的时候,无连接条件的指定,就会导致查询的结果的行数是每个表行数相乘。所以要小表驱动大表,可以减少外层的循环次数。

MySQL的连接方式

内连接 左连接 右连接 全连接 左外连接 右外连接 交叉连接

COUNT(*) COUNT(1) COUNT(列)

一般使用COUNT(),其实在MySQL中count()和count1的查询效率是差不多的,如果列名为主键的情况下,count(列名)会比较快,他们的效率在不同的场景下是不同的,那么区别就是*是执行的时候包括了所有的列,计算出来是行数,即使是null也会记录,1是常量,忽略了所有的列,代表代码行,所以也不会忽略null,但是列名的话会去除null值,不统计。

MySQL索引的理解

MySQL的实现是选择的innodb,那innodb索引数据存储结构是使用的b+树,B+树是叶子节点存储数据,非叶子节点存储键值,同时非叶子节点父节点的元素都在子节点的元素中,并且是最大的一个,叶子节点有一个指向相邻链表的指针,方便范围查询。聚簇索引存储整行的数据和键值,非聚簇索引存储索引列的值和指向聚簇索引的键值。所以我们如果使用非聚簇索引,它会先找到索引列的值,再去找聚簇索引的键值,再通过聚簇索引的键值找到行数据,这样就会有一个回表的操作,进行两次查询的IO。所以我们可以使用组合索引来避免回表的操作,将所需要使用的列都设置为组合索引,这样查询字段的时候因为组合索引中已经有了对应的字段,就不会再去找聚簇索引了,可以直接找数据行。

聚簇索引和非聚簇索引

在存储方式上不太一样,聚簇索引是索引结构和数据行一起存储的,而非聚簇索引是分开存储,一个表只能有一个聚簇索引而可以有多个非聚簇索引,效率上面聚簇索引效率高比较适合范围性的查找,而非聚簇索引效率比较低,查询会做两次IO。

数据库慢查询和优化

首先慢查询其实是MySQL能记录sql执行时间多长的语句的一个手段,它会把一些执行时间超过我们设置的慢查询阈值的语句写入慢查询日志中,我们要在MySQL中开启慢查询,设置slow_query_log设置为1 时间是long_query_time中设置,出现慢查询的sql语句都需要优化。首先是对sql语句的优化,这种一般根据我们的常识来优化,比如避免使用select*,模糊查询的时候百分号放到了前面,使用or,考虑使用union或者unionall,使用了不等于可以使用in,用了子查询with as,可以使用join,隐式转换,非正确函数处理字段。除了这些可以考虑使用索引来优化,我们可以使用explan关键字,查询有没有走索引,有没有全表扫描,有没有回表。我们会首先考虑给经常需要查询的列增加索引,和要连接条件的列建立所以同时我们可以使用覆盖索,可以给字段添加覆盖索引这样该列查询得时候就会在辅助索引树上面,可以有效地减小回表操作。我们可以适当使用前缀索引,并且正确使用联合索引避免过多的列使用覆盖索引。同时要避免范围查询数据因为mysql他走索引的话会从辅助索引宿舍找完再回主键找数据,所以找如果你的覆盖范围过大会导致他他还不如进行一遍全面扫描那因为mysql是自带一个优化器的那他可能就会走全表扫描。必要的时候可以考虑使用分批查询或者mysql的缓存大小或者分库分表读写分离。

怎么优化SQL效率

首先是对sql语句的优化,肯定要开启分析慢查询日志,筛选出效率比较低的sql语句,然后开始分析,是IO数据访问导致的还是CPU数据运算导致的,这时候会使用到explain分析出常见的sql语句中的问题。比如IO导致,可以索引优化,添加索引的覆盖范围,或者有多表操作,临时表过多,这类的问题,那么像索引优化我们应该遵循最左匹配么,防止sql语句筛选会多次全表查找,可以在字段匹配的时候注意数据类型一致,而且少用模糊匹配,如果是CPU数据运算导致的,那要看分组操作和排序是不是有问题,并且要减少函数的操作,因为底层都可能会涉及到全表查询,必要的时候也可以分批查询。再后面就是对数据库的配置了,比如缓存池的大小之类的,或者分库分表 读写分离的操作。

索引添加的时候优先考虑基数大的,或者经常作为查询条件的字段作为索引,就是唯一性的比如用户id,或者按照最左前缀匹配添加复合索引等等。同时要避免函数列和状态列。避免给经常更新的字段添加索引,或者唯一性 太差的字段添加索引。

用explain分析的时候可以查看type和一些信息,当发现全表查询或者全索引查询的时候,或者key为null,扫描行数过长,或者信息上说有临时表等等。像where,join,group by这些方法使用的时候都需要添加索引了。

索引失效的场景

索引列参与了sql查询的计算或使用了函数,like匹配用%开头,where匹配的时候存在隐式转换的问题,索引列进行了or连接,表非常小,全表扫描比索引查找还快的时候,还有就是使用group分组或者order排序的时候分组字段和索引顺序不一致。

MySQL的事务

MySQL的事务就是数据处理的时候的一些场景,需要保证一组SQL语句要么全部执行成功要么执行失败。事务有四大特性,首先是原子性,原子性就是比如当A给B转账,A扣钱,B加钱,A扣钱了B没收到钱,或者B收到,A没扣,这种情况MySQL会将数据回滚。第二个是一致性,事务执行前后,保持一致性,数据不被破坏。第三个是隔离性,当多个事务并发执行的时候,事务之间互不影响。最后一个是持久性,当数据库出现断电 崩溃等问题的时候,数据不会丢失,在MySQL里是通过redo log记录提高的修改,出现崩溃的情况时,会通过日志恢复数据。

事务隔离级别

有四大隔离级别,分别是读未提交,这个是一个事务读取了另一个事务没有提交的数据,会出现脏读的问题,接着就是读已提交,它只会读事务已经提交的数据,它解决了脏读的问题,提高了数据一致性,但是出现了不可重复读的问题,多次读取的结果不一样,然后就是RR可重复读,解决了脏读和不可重复读的问题。还有一个串行化,这个基本就是把事务进行串行处理,就不会出现并发的时候出现的各种事务问题。

MVCC

MVCC实现了读已提交和可重复读两个事务隔离级别,MVCC是多版本并发控制,就和它的名字一样,是通过多版本的形式解决并发问题,它是一个无锁的机制,因为mysql经常要读,如果因为其他线程的写操作,上锁了导致读操作需要阻塞等待,这样效率非常差,所以出现了MVCC,当事务快照读的时候,将数据进行一个快照,会生成一个readview视图,MVCC会在线程修改删除数据的时候,会生成一个undo log,方便回滚的数据,如果有线程执行查询数据的时候,会按照规则判断数据有没有被其他线程更改,在查数据的过程中被修改了,MySQL会进行一个id的判断,他会从快照中找,当时它执行查询命令的时候的数据,这样就将多线程并发问题导致数据不一致问题进行了解决,并且不会影响性能。

MySQL的锁(未解决)

SQL 分页 limit 500000,10和 limit 10 速度一致?

不一致。limit50000 10 需要跳过前面50w行,再取数据,数据库需扫描 500010 行,再丢弃前 500000 行,耗时远高于 直接取前 10 行。偏移量越大,性能越差。

数据库中索引类型有哪些

1. B-Tree 索引

  • 特点:最常用的索引类型,基于 B + 树结构实现,支持等值查询(WHERE col = value)、范围查询(WHERE col BETWEEN a AND b)和排序(ORDER BY)。
  • 适用场景:几乎所有场景,特别是范围查询。
  • 支持引擎:InnoDB、MyISAM、Memory 等。

2. 哈希索引

  • 特点:基于哈希表实现,仅支持等值查询(=IN()),不支持范围查询。
  • 适用场景:内存表(Memory 引擎默认使用)或 InnoDB 的自适应哈希索引(由引擎自动创建)。
  • 支持引擎:Memory、InnoDB(自适应哈希索引)。

3. 全文索引

  • 特点:专门用于文本搜索,支持MATCH AGAINST语法。

  • 适用场景:文章内容、评论等大文本字段的搜索。

  • 支持引擎:InnoDB(MySQL 5.6+)、MyISAM。

  • 示例

    1
    SELECT * FROM articles WHERE MATCH(content) AGAINST('关键词' IN NATURAL LANGUAGE MODE);

4. 空间索引

  • 特点:用于存储和查询空间数据(如地理位置、几何形状)。
  • 适用场景:地图应用、地理信息系统(GIS)。
  • 支持引擎:InnoDB、MyISAM。
  • 注意:需要使用SPATIAL关键字创建,且字段类型必须为GEOMETRYPOINT等空间类型。

5. 聚簇索引(Clustered Index)

  • 特点:InnoDB 存储引擎特有的索引类型,数据行直接存储在索引的叶子节点中。
  • 主键即聚簇索引:每个表只能有一个聚簇索引,通常是主键。如果没有定义主键,InnoDB 会选择唯一非空索引或生成隐藏的聚簇索引。

6. 辅助索引(Secondary Index)

  • 特点:除聚簇索引外的所有索引(如普通索引、唯一索引),叶子节点存储的是主键值(而非数据行)。
  • 回表查询:通过辅助索引查询数据时,可能需要先找到主键,再通过主键查询完整数据行。

7. 组合索引(复合索引)

  • 特点:基于多个列创建的索引,遵循 “最左前缀原则”。

  • 示例

    1
    2
    3
    4
    CREATE INDEX idx_name_age ON users (name, age);
    -- 支持查询:WHERE name = '张三' AND age = 20
    -- 支持查询:WHERE name = '张三'(最左前缀)
    -- 不支持查询:WHERE age = 20(跳过了name列)

8. 唯一索引(Unique Index)

  • 特点:确保索引列的值唯一,允许NULL(但每个NULL视为不同值)。

  • 示例

    1
    CREATE UNIQUE INDEX idx_email ON users (email);

9. 主键索引(Primary Key)

  • 特点:特殊的唯一索引,不允许NULL,每个表只能有一个主键。
  • 聚簇索引关联:InnoDB 中主键即聚簇索引,决定数据的物理存储顺序。

10. 覆盖索引(Covering Index)

  • 特点:索引包含所有查询需要的字段,无需回表查询数据。

  • 示例

    1
    2
    SELECT name, age FROM users WHERE name = '张三';
    -- 若索引包含name和age,则直接通过索引返回结果,无需访问数据行。

11. 函数索引(Functional Index)

  • 特点:基于表达式或函数创建的索引。

  • 支持版本:MySQL 5.7+。

  • 示例

    1
    2
    CREATE INDEX idx_upper_name ON users (UPPER(name));
    -- 支持查询:WHERE UPPER(name) = 'ZHANGSAN;

Redis

redis和mq的机制和原理

  • Redis:以 内存存储 + 单线程模型 为基础,通过高效数据结构、持久化和集群方案,实现高速缓存与分布式能力。
  • MQ:以 生产者 - 队列 - 消费者 为模型,通过异步解耦、流量控制和持久化,解决分布式系统的通信协作问题。
    两者常配合(如 Redis 缓存加速,MQ 异步解耦),构建高性能、高可靠的分布式架构。

配置Redis时候要序列化配置

因为如果不配置序列化器,RedisTemplate会使用默认的序列化器(JdkSerializationRedisSerializer)。
默认序列化器的问题:序列化后的数据是Java特有的二进制格式,不可读且不通用。在RedisConfig类中,使用了以下序列化器:StringRedisSerializer

Redis的缓存一致性

redis和MySQL的数据一致性问题,主要要围绕着写操作,因为在更新和删除的时候会产生一致性问题,一般是用旁路缓存模式,它在读操作的时候会先查看缓存中数据,缓存命中的话返回,如果没有就查数据库,再将数据写入缓存中,那写操作它是先更新数据库再将缓存删除。这样就保证了一致性。

Redis的二级缓存

分为本地缓存和分布式缓存,本地缓存就是放在本地存储key value,分布式缓存一般是用于解决本地缓存无法跨实例共享缓存的一致性和缓存穿透 击穿 雪崩的问题。

讲一下缓存穿透,缓存击穿,缓存雪崩

缓存穿透就是因为一般设计业务的时候,会优先访问缓存中数据是否存在,如果不存在访问数据库,攻击者就是可以用这一点构造恶意请求,多次访问数据库,导致数据库压力过大。这种情况可以对访问请求进行合法性检查,过滤非法字符或者使用布隆过滤器过滤,再决定是否访问数据库。或者给redis设置null值或者空字符串。

缓存击穿就是当缓存数据过期或者失效的时候,攻击者并发访问失效数据,这样会直接访问数据库,导致高并发,给数据库造成压力。解决方式可以在缓存失效或者过期前,进行预更新或者延迟更新,让攻击者不知道更新的时间。第二个就是解决并发攻击,这里可以使用锁,互斥锁和分布式锁,过期或者失效的时候有线程没有访问到缓存中数据,则给该线程一个锁, 这个时候就可以增加一个判断如果有锁,则查询数据,同时释放锁,并将数据更新到缓存,如果没有锁就线程等待。或者逻辑过期,添加逻辑时间字段,当现在请求接口的时候,先判断当前时间是否在过期时间之前,如果未过期,将数据直接返回,如果过期了,进行缓存重建,加上互斥锁,重新查找一次数据库,封装新的过期时间,将数据放入缓存中。那么在过期时间以后的所有线程,只有拿到锁的线程进行了缓存修改,后面线程发现时间都没过期,就拿修改后的数据。所以会有一个数据一致性的问题,在过期时间内的数据都是旧数据。

缓存雪崩是缓存中大量的数据全部失效,导致非常多的请求直接访问数据库,导致数据库压力剧增。最简单的是给每个key的TTL增加随机值,缓存预热的时候给缓存数据的设置过期时间TTL的时候定义一个范围,追加该范围的随机数。这种情况一般使用分布式集群提高可靠性或者限流,要么多级缓存。

Redis的分布式锁

通过SetNx命令实现,当客户端尝试使用SETNX命令设置一个特定的key,值可以是任意唯一标识客户端的字符串,会返回一个true表示加锁成功,这样其他的客户端就不能再访问这个key了。当该客户端使用完或者过期时间到了,会删除这个key这个锁就释放了。过期时间的设置会用一个看门狗的机制,在java中使用redisson的锁实现,会自动处理锁的续期、可重入等问题,还支持公平锁、联锁、红锁等更复杂的分布式锁场景。

三种分布式锁的区别

哨兵机制

场景题

用户登录的流程开发

用户通过前端输入用户名和密码,或者扫码,手机号等方式进行登陆。后端接受到数据,进行验证,验证成功会生成一个token,那一般我们为了防止泄漏,我们会将token返回,同时对token进行一个加密,生成我们所谓的jwt令牌。我会选择设置jwt令牌的过期时间,放在redis中,后续用户进行一系列操作的时候,我会设置一个拦截器,将用户持有的token和jwt令牌进行校验,校验通过正常操作,不通过返回token过期,重新登录。token过期这里看业务需求,如果甲方要求,用户在操作的时候不要过期,那么就在用户带着token访问其他网页的时候,进行一个jwt令牌的过期时间刷新,当长时间在一个页面没操作的时候,过期时间到了,就跳转登录界面重新登录,或者回到不用登录的首页。

Token如何续费

可以通过刷新令牌的方式行续费,当用户的登录的时候,我们后端拿到一个访问令牌和刷新令牌,当访问令牌失效了,达到我们设置的失效时间了,还要请求访问的时候,就可以通过刷新令牌,请求新的访问令牌给用户。

项目难点亮点

execl导入

execl导出

用了execlwrite类,我先进行了分批查询数据库,构造不同的sheet页,最后导出数据到sheet页中,中间加同步锁。然后把刚刚的异步任务放到线程池里面,等任务执行完刷新流并关闭流。实现多线程导出优化导出效率。

多端操作同一个数据

JVM调优

心跳机制

MySQL死锁

拦截器和过滤器

内存溢出问题

spring

spring bean

ioc aop

事务管理

springboot

业务类

PO/DO、DTO、VO、BO

一个DO类对应的数据库的表结构,字段名和表名对应,一般是类通过驼峰命名法。DTO是前端返回给后端的数据封装的类,用于接受前端数据,VO则是返回前端所需要的类。BO是当我们正常编写代码,经常需要自己存储一些相同的数据的时候,我们可以选则自己新建一个类,用于存放一些常用参数,可以优化代码。

大数据

Hadoop怎么配置流程 需要改什么文件

  1. 基础环境准备
    • 安装 Java 并配置JAVA_HOME环境变量。
    • 配置 SSH 无密码登录。
  2. 核心配置文件修改(位于$HADOOP_HOME/etc/hadoop目录):
    • core-site.xml:配置 Hadoop 核心参数,如 HDFS 的 NameNode 地址。
    • hdfs-site.xml:配置 HDFS 参数,如副本数、数据存储位置。
    • mapred-site.xml:配置 MapReduce 参数,指定资源调度框架。
    • yarn-site.xml:配置 YARN 参数,如 ResourceManager 和 NodeManager 地址。
  3. 环境变量配置
    • hadoop-env.sh:设置 Hadoop 运行环境,如 Java 路径。

Hive怎么配置并和mysql连接

  1. 基础配置
    • 修改hive-site.xml,配置元数据存储位置:
  2. 依赖准备
    • 将 MySQL JDBC驱动(如mysql-connector-java.jar)放入 Hive 的lib目录。
  3. 初始化元数据库
    • 执行schematool -initSchema -dbType mysql初始化 MySQL 元数据库。

部署

为什么使用docker

因为我们之前这个项目提交给甲方的时候,他是分配给我们一个云服务器,他在验收的时候会重启所有的服务器,那这里就需要考虑到一个部署持久化的问题,所以我们使用了docker,将我们本地服务器的依赖和环境打镜像,迁移到甲方的云服务器,写了脚本在服务器启动后就自动运行我们的项目,jar包。正常使用docker部署的原因,是它的部署效率高并且方便维护,还有持久化啊,数据迁移都比较方便,支持的技术栈和兼容性也比较好。