前景提要
目前工作7年,基本问的多的是跟项目相关的问题,基础的也会带一点,这里一并做了汇总,部分问题跟前一篇相同,只是搬过来记录一下。
一、Java基础
1、Java几种集合基本实现与应用场景
1、List集合
- ArrayList是一个可变大小的数组实现,适用于需要频繁访问元素的场景,有序。
- LinkedList是一个双向链表实现,适用于需要频繁插入和删除元素的场景,有序
- Vector与ArrayList相似,但是线程安全,适用于并发环境。
2、Set集合
- HashSet使用哈希表实现,适用于不允许重复元素的场景,无序。
- TreeSet使用红黑树实现,适用于需要元素自动排序的场景,有序。
3、Map集合
- HashMap是基于哈希表的键值对实现,适用于需要快速查找和存储键值对的场景,无序。
- TreeMap基于红黑树实现,适用于需要按键自动排序的场景,有序。
2、Java的内存模型
Java虚拟机所管理的内存将会包括以下几个运行时数据区域:程序计数器,Java虚拟机栈,本地方法栈,Java堆,方法区。
1)程序计数器(Program Counter Register)
程序计数器程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完成。
2)Java虚拟机栈(Java Virtual Machine Stacks)
Java Virtual Machine Stacks,也是线程私有的,它的生命周期与线程相同。
虚拟机栈描述的是Java方法执行的内存模型(非native方法)。
每个方法在执行的同时都会创建一个栈帧用于存储局部变量表,操作数栈,动态链接,方法出口等信息。
每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程,当方法被调用则入栈,一旦完成调用则出栈。所有的栈帧都出栈后,线程就结束了。
局部变量表存放了编译器可知的各种基本数据类型、对象引用、returnAddress类型。局部变量表所需的内存空间在编译器完成分配。当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。
基本类型:boolean, byte, char, short, int, float, long, double,其中long和double占用2个局部变量空间slot其余的占用1个;
对象引用:reference类型,它不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置;
returnAddress类型:指向了一条字节码指令的地址;
在Java虚拟机规范中,对这个区域规定了两种异常:线程请求的栈深度大于虚拟机所允许的深度,抛出StackOverflowError
异常;如果虚拟机栈可以动态扩展(目前大部分的Java虚拟机都可动态扩展,只不过Java虚拟机规范中也允许固定长度的虚拟机栈),如果扩展时无法申请到足够的内存,抛出OutOfMemoryError
异常。
3)本地方法栈(Native Method Stack)
Native Method Stack与虚拟机栈的作用非常相似,区别是:虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法。
本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息。
备注:HotSpot直接把本地方法栈和虚拟机栈合二为一。本地方法栈区域也会抛出StackOverflowError
和OutOfMemoryError
异常。
4)Java堆(Java Heap)
Java Heap是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此区域的唯一目的就是存放对象实例(Java虚拟机规范中的描述时:所有的对象实例以及数组都要在堆上分配)。
Java堆是GC的主要区域,因此很多时候也被称为GC堆。
从内存分配的角度来看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer, TLAB)
从内存回收的角度来看,由于现在收集器基本都采用分代收集算法,所以Java堆中还可以细分为:新生代和老年代,在细致一点的有Eden空间,From Survivor空间,To Survivor空间等。
备注:有OOM异常:
1.对dump出来的堆快照分析出内存泄漏还是内存溢出
2.内存泄漏: 查看泄漏对象的GCRoots的引用链,通过怎样的路径与GCRoots相关联导致垃圾收集器无法自动回收
3 不存在泄漏,则对象必须存活,则检查-Xmx与-Xms 及代码检查对象生命周期
5)方法区(Method Area)
Method Area是各个线程共享内存区域,用于存储已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据。这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载。
运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本,字段,方法,接口等描述信息,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性。
3、NIO和BIO的区别
概念不同。NIO是java1.4中引入的,被称为new I/O,也有说是non-blocking I/O,NIO被成为同步非阻塞的IO1。BIO是面向流的,NIO是面向块(缓冲区)的1。
处理方式不同。NIO是同步非阻塞的,而BIO是直接让线程等待1。
功能不同。NIO是可以双向的,BIO只能单向1。
性能不同。BIO以流的方式处理数据,块I/O的效率比流I/O高很多2。
此外,NIO有选择器,而BIO没有。
4、 反射的应用
java反射的作用有以下3点:
- 获取已装载类的成员变量信息;
- 获取已装载类的方法;
- 获取已装载类的构造方法信息。
通过”获取已装载类的成员变量信息”功能可以获取JVM中的Class对象,获取了对象之后可以实现很多功能,比方说IOC容器通过反射创建对象、或者是动态代理。
5、java悲观锁和乐观锁的区别以及应用
悲观锁
悲观锁的思想是,在访问数据时,假定其他线程会修改数据,因此每次访问数据之前都会先获取锁,确保其他线程不能同时访问数据。这种方式对并发性能有一定的影响,因为它会导致线程之间的竞争和等待。
常见的悲观锁实现方式包括:
- 数据库中的排他锁(如SELECT … FOR UPDATE)。
- Java中的synchronized关键字。
- Java中的ReentrantLock类。
适用场景:
- 当写操作非常频繁,读操作较少时,可以使用悲观锁,确保数据的一致性和安全性。
- 当资源竞争较为严重,需要精确控制并发操作时,悲观锁可以提供更细粒度的控制。
乐观锁
乐观锁的思想是,认为在绝大多数情况下,数据的冲突是很少发生的,因此允许多个线程并发地访问数据,但在实际更新数据时会检查是否发生冲突。如果检测到冲突,会进行回滚或重试操作。
常见的乐观锁实现方式包括:
- 数据库中的版本控制字段,如使用版本号或时间戳。
- Java中的CAS(Compare and Swap)操作,例如Atomic类和java.util.concurrent包中的一些工具类。
适用场景:
- 当读操作远远多于写操作时,使用乐观锁可以减少锁竞争,提高并发性能。
- 当冲突发生的概率较低,且冲突发生时的处理代价较小时,适合使用乐观锁。
6、JDK8和JDK11有什么区别
7、Arrays.sort用什么排序?各个时间复杂度和空间复杂度?稳定性如何?快排的思想?
在Java中,Arrays.sort() 方法可以用来对数组进行排序。它根据传入的数组元素的类型使用不同的排序算法:
-
基本数据类型数组:
对于 byte[], short[], int[], long[], float[], double[],Arrays.sort() 使用双轴快速排序(dual-pivot quicksort)算法。
对象数组(包括包装类数组): -
对于 Object[],Arrays.sort() 使用一种稳定的归并排序(merge sort)算法,它确保了相同元素的相对顺序不会改变。
-
char[] 数组:
对于 char[],Arrays.sort() 使用一种特殊的插入排序(insertion sort)算法。 -
其他数组类型:
如果数组元素类型是其他非基本数据类型(例如自定义类),则要求该类实现 Comparable 接口,并在类中重写 compareTo() 方法,以便 Arrays.sort() 能够进行排序。
总结起来,Arrays.sort() 根据数组元素的类型选择合适的排序算法,大多数情况下使用的是快速排序或归并排序。
各个时间复杂度和空间复杂度
稳定性?
快排的思想?
二、并发
1、线程是什么
线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。
进程进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。在 Java 中,当我们启动 main 函数时其实就是启动了一个 JVM 的进程,而 main 函数所在的线程就是这个进程中的一个线程,也称主线程。
2、线程池的几个主要的参数
ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
1)corePoolSize:核心线程数(设置多少合适)
核心线程会一直存活,及时没有任务需要执行
当线程数小于核心线程数时,即使有线程空闲,线程池也会优先创建新线程处理
设置allowCoreThreadTimeout=true
(默认false)时,核心线程会超时关闭
1.5)核心线程数设置多少合适
一个计算为主的程序(CPU密集型程序),多线程跑的时候,可以充分利用起所有的 CPU 核心数,比如说 8 个核心的CPU ,开8 个线程的时候,可以同时跑 8 个线程的运算任务,此时是最大效率。但是如果线程远远超出 CPU 核心数量,反而会使得任务效率下降,因为频繁的切换线程也是要消耗时间的。因此对于 CPU 密集型的任务来说,线程数等于 CPU 数是最好的了。
如果是一个磁盘或网络为主的程序(IO密集型程序),一个线程处在 IO 等待的时候,另一个线程还可以在 CPU 里面跑,有时候 CPU 闲着没事干,所有的线程都在等着 IO,这时候他们就是同时的了,而单线程的话此时还是在一个一个等待的。我们都知道 IO 的速度比起 CPU 来是很慢的。此时线程数等于CPU核心数的两倍是最佳的。
2)maximumPoolSize:最大允许线程数量
线程池内部线程数量已经达到核心线程数量,即corePoolSize
,并且任务队列已满,此时如果继续有任务被提交,将判断线程池内部线程总数是否达到maximumPoolSize
,如果小于maximumPoolSize
,将继续使用线程工厂创建新线程。如果线程池内线程数量等于maximumPoolSize
,就不会继续创建线程,将触发拒绝策略RejectedExecutionHandler
。新创建的同样是一个Work对象,并最终放入workers
集合中。
3)keepAliveTime、unit:超出线程的存活时间
当线程池内部的线程数量大于corePoolSize
,则多出来的线程会在keepAliveTime
时间之后销毁。
如果allowCoreThreadTimeout=true
,则会直到线程数量=0
4)workQueue:任务队列
被提交但未执行的任务队列,它是一个BlockingQueue接口的对象,仅用于存放Runnable对象。ThreadPoolExecutor
的构造函数中,可使用以下几种BlockingQueue
,通常有固定数量的ArrayBlockingQueue
,无限制的LinkedBlockingQueue
。
1、直接提交队列。
即SynchronousQueue
,这是一个比较特殊的BlockKingQueue
, SynchronousQueue
没有容量,每一个插入操作都要等待对应的删除操作,反之 一个删除操作都要等待对应的插入操作。 也就是如果使用SynchronousQueue
,提交的任务不会被真实保存,而是将新任务交给空闲线程执行,如果没有空闲线程,则创建线程,如果线程数都已经大于最大线程数,则执行拒绝策略。使用这种队列,需要将maximumPoolSize
设置的非常大,不然容易执行拒绝策略。比如说
没有最大线程数限制的newCachedThreadPool()
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
但是这个在大量任务的时候,会启用等量的线程去处理,有风险造成系统资源不足。
2、有界任务队列。
有界任务队列可以使用ArrayBlockingQueue
实现。需要给一个容量参数表示该队列的最大值。当有新任务进来时,如果当前线程数小于corePoolSize
,则会创建新线程执行任务。如果大于,则会将任务放到任务队列中,如果任务队列满了,在当前线程小于将maximumPoolSize
的情况下,将会创建新线程,如果大于maximumPoolSize
,则执行拒绝策略。
也就是,一阶段,当线程数小于coresize
的时候,创建线程;二阶段,当线程任务数大于coresize
的时候,放入到队列中;三阶段,队列满,但是还没大于maxsize
的时候,创建新线程。 四阶段,队列满,线程数也大于了maxsize
, 则执行拒绝策略。
可以发现,有界任务队列,会大概率将任务保持在coresize
上,只有队列满了,也就是任务非常繁忙的时候,会到达maxsize
。
3、无界任务队列。
使LinkedBlockingQueue
实现,队列最大长度限制为Integer.MAX
。无界任务队列,不存在任务入队失败的情况, 当任务过来时候,如果线程数小于coresize
,则创建线程,如果大于,则放入到任务队列里面。也就是,线程数几乎会一直维持在coresize
大小。FixedThreadPool
和SingleThreadPool
即是如此。 风险在于,如果任务队列里面任务堆积过多,可能导致内存不足。
4、优先级任务队列。
使用PrioriBlockingQueue
,特殊的无界队列,和普通的先进先出队列不同,它是优先级高的先出。
5)threadFactory:线程工厂
线程池内初初始没有线程,任务来了之后,会使用线程工厂创建线程。
6)rejectedExecutionHandler:任务拒绝处理器
两种情况会拒绝处理任务:
- 当线程数已经达到
maxPoolSize
,切队列已满,会拒绝新任务 - 当线程池被调用
shutdown()
后,会等待线程池里的任务执行完毕,再shutdown
。如果在调用shutdown()
和线程池真正shutdown
之间提交任务,会拒绝新任务
线程池会调用rejectedExecutionHandler
来处理这个任务。如果没有设置默认是AbortPolicy
,会抛出异常
ThreadPoolExecutor
类有几个内部实现类来处理这类情况:
AbortPolicy
丢弃任务,抛运行时异常
-CallerRunsPolicy
执行任务
-DiscardPolicy
忽视,什么都不会发生
-DiscardOldestPolicy
从队列中踢出最先进入队列(最后一个执行)的任务
实现RejectedExecutionHandler
接口,可自定义处理器
参考:
JAVA线程池参数详解
Java线程池,知道这些就够了
线程池各个参数详解以及如何自定义线程池
深入理解 Java 线程池:ThreadPoolExecutor
3、线程池的应用
- 异步发送邮件通知,发送一个任务,然后注入到线程池中异步发送。
- 异步送券
- 心跳请求任务,创建一个任务,然后定时发送请求到线程池中
三、Spring相关
1、Spring事务
当方法A有事务,方法B没有事务,但是方法B调用了方法A,问在controller层分别调用方法A和方法B的时候有什么区别?
主要取决于事务的传播行为和控制方式,
- 调用方法A(有事务):
- 假设方法A在执行期间涉及数据库操作,并且使用了事务管理。这意味着,如果方法A执行失败或者抛出异常,事务会回滚,之前对数据库所做的更改将被撤销,保持数据库的一致性。
- 如果控制器直接调用方法A,并且方法A在事务内执行,那么无论方法B是否涉及事务,方法A的事务会在方法A执行结束后进行提交。这可能导致方法B的操作受到方法A事务的影响。
- 调用方法B(无事务)调用了方法A(有事务):
- 如果方法B没有涉及事务,那么它不会影响数据库的事务处理。即使方法A内部使用了事务,方法B的调用不会触发方法A事务的提交或回滚。这意味着方法A事务的影响仅限于方法A自身的操作。
- 调用方法B后,如果方法B内部调用了方法A,方法A的事务会独立于方法B的事务进行管理。方法B不会影响方法A事务的提交或回滚。
四、Spring Cloud相关
1、Spring Cloud组件用过哪些
openfeign、eureka、Hystrix、Ribbon、sidecar
2、简述一下openfeign的原理
OpenFeign是基于注解和动态代理技术的Java HTTP客户端框架,通过接口定义和注解配置,将远程HTTP请求映射为本地方法(RequestTemplate对象)调用,简化了调用远程服务的过程
3、Ribbon几种负载均衡算法
负载均衡,不管 Nginx 还是 Ribbon 都需要其算法的支持,如果我没记错的话 Nginx 使用的是 轮询和加权轮询算法。而在 Ribbon 中有更多的负载均衡调度算法,其默认是使用的 RoundRobinRule
轮询策略。
RoundRobinRule
:轮询策略。Ribbon 默认采用的策略。若经过一轮轮询没有找到可用的provider
,其最多轮询 10 轮。若最终还没有找到,则返回 null。RandomRule
: 随机策略,从所有可用的provider
中随机选择一个。RetryRule
: 重试策略。先按照RoundRobinRule
策略获取provider
,若获取失败,则在指定的时限内重试。默认的时限为 500 毫秒。
更换默认的负载均衡算法,只需要在配置文件中做出修改就行。
providerName:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
当然,在 Ribbon 中你还可以自定义负载均衡算法,你只需要实现 IRule
接口,然后修改配置文件或者自定义 Java Config
类。
五、数据库语言相关
1、mysql
1 描述一下索引的数据结构,以及为什么要用这个数据结构
。MySQL就普遍使用B+Tree实现其索引结构。
B+的特性:
- 所有关键字都出现在叶子结点的链表中(稠密索引),且链表中的关键字恰好是有序的;
- 不可能在非叶子结点命中;
- 非叶子结点相当于是叶子结点的索引(稀疏索引),叶子结点相当于是存储(关键字)数据的数据层;
- 更适合文件索引系统;
B+tree的优点:
- B±tree的磁盘读写代价更低
B+ tree的内部结点并没有指向关键字具体信息的指针。因此其内部结点相对B 树更小。如果把所有同一内部结点的关键字存放在同一盘块中,那么盘块所能容纳的关键字数量也越多。一次性读入内存中的需要查找的关键字也就越多。相对来说IO读写次数也就降低了。
举个例子,假设磁盘中的一个盘块容纳16bytes,而一个关键字2bytes,一个关键字具体信息指针2bytes。一棵9阶B-tree(一个结点最多8个关键字)的内部结点需要2个盘快。而**B+树内部结点只需要1个盘快。当需要把内部结点读入内存中的时候,B 树就比B+ **树多一次盘块查找时间(在磁盘中就是盘片旋转的时间)。 - B±tree的查询效率更加稳定
由于非终结点并不是最终指向文件内容的结点,而只是叶子结点中关键字的索引。所以任何关键字的查找必须走一条从根结点到叶子结点的路。所有关键字查询的路径长度相同,导致每一个数据的查询效率相当。
2 索引怎么设计 有什么指导意见
1、表的主键、外键必须有索引;
2、数据量超过300的表应该有索引;
3、经常与其他表进行连接的表,在连接字段上应该建立索引;
4、经常出现在Where子句中的字段,特别是大表的字段,应该建立索引;
5、索引应该建在选择性高的字段上;
6、索引应该建在小字段上,对于大的文本字段甚至超长字段,不要建索引;
7、复合索引的建立需要进行仔细分析;尽量考虑用单字段索引代替:
A、正确选择复合索引中的主列字段,一般是选择性较好的字段;
B、复合索引的几个字段是否经常同时以AND方式出现在Where子句中?单字段查询是否极少甚至没有?如果是,则可以建立复合索引;否则考虑单字段索引;
C、如果复合索引中包含的字段经常单独出现在Where子句中,则分解为多个单字段索引;
D、如果复合索引所包含的字段超过3个,那么仔细考虑其必要性,考虑减少复合的字段;
E、如果既有单字段索引,又有这几个字段上的复合索引,一般可以删除复合索引;
8、频繁进行数据操作的表,不要建立太多的索引;
9、删除无用的索引,避免对执行计划造成负面影响;
3 了解前缀索引吗?
前缀索引:对文本的前几个字符建立索引(具体是几个字符在建立索引时去指定),比如以产品名称的前 10 位来建索引,这样建立起来的索引更小,查询效率更快!
但是mysql中无法使用前缀索引进行 ORDER BY 和 GROUP BY,也无法用来进行覆盖扫描。
这样设置解决什么问题:
- 能有效的减小索引文件的大小,让每个索引页可以保存更多的索引值,从而提高了索引查询的速度;
- 节省索引存储空间。
4 慢sql怎么调优?
1)找到慢查询 SQL
首先需要找到慢查询 SQL,可以通过使用 MySQL 自带的慢查询日志(slow query log)来实现。在 MySQL 配置文件中启用慢查询日志,然后将执行时间超过阈值的 SQL 查询语句记录到慢查询日志中。通过分析慢查询日志,可以找到执行时间较长的 SQL 查询语句。
- 打开MySQL配置文件 my.cnf。该文件通常位于
/etc/mysql
目录下。 - 找到
[mysqld]
段落. - 添加或修改以下行:
slow_query_log = 1 # 启用慢查询日志
slow_query_log_file = /var/log/mysql/mysql-slow.log # 指定慢查询日志文件路径
long_query_time = 10 # 指定查询执行时间超过多少秒时被认为是慢查询
注意:上述示例中,慢查询日志文件路径为 /var/log/mysql/mysql-slow.log,您可以根据需要更改为其他路径。
-
保存并关闭文件。
-
重新启动MySQL服务以使更改生效。
-
确认慢查询日志是否在指定的路径中生成。
要查看慢查询日志,您可以使用以下命令:
sudo tail -f /var/log/mysql/mysql-slow.log
2)分析慢查询 SQL
找到慢查询 SQL 后,需要对 SQL 进行分析,了解 SQL 查询语句的执行计划、索引使用情况等信息。可以通过 MySQL 自带的 Explain 命令来查看 SQL 的执行计划,以及哪些索引被使用了。根据执行计划和索引使用情况,可以确定 SQL 查询语句的瓶颈所在。
1 获取SQL执行计划
MySQL 为我们提供了 EXPLAIN 命令,来获取执行计划的相关信息。需要注意的是,EXPLAIN 语句并不会真的去执行相关的语句,而是通过查询优化器对语句进行分析,找出最优的查询方案,并显示对应的信息。EXPLAIN 执行计划支持 SELECT、DELETE、INSERT、REPLACE 以及 UPDATE 语句。我们一般多用于分析 SELECT 查询语句,使用起来非常简单,语法如下:
EXPLAIN + SELECT 查询语句;
例子:
mysql> explain SELECT * FROM dept_emp WHERE emp_no IN (SELECT emp_no FROM dept_emp GROUP BY emp_no HAVING COUNT(emp_no)>1);
+----+-------------+----------+------------+-------+-----------------+---------+---------+------+--------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+----------+------------+-------+-----------------+---------+---------+------+--------+----------+-------------+
| 1 | PRIMARY | dept_emp | NULL | ALL | NULL | NULL | NULL | NULL | 331143 | 100.00 | Using where |
| 2 | SUBQUERY | dept_emp | NULL | index | PRIMARY,dept_no | PRIMARY | 16 | NULL | 331143 | 100.00 | Using index |
+----+-------------+----------+------------+-------+-----------------+---------+---------+------+--------+----------+-------------+
可以看到,执行计划结果中共有 12 列,各列代表的含义总结如下表:
列名 | 含义 |
---|---|
id | SELECT 查询的序列标识符 |
select_type | SELECT 关键字对应的查询类型 |
table | 用到的表名 |
partitions | 匹配的分区,对于未分区的表,值为 NULL |
type | 表的访问方法 |
possible_keys | 可能用到的索引 |
key | 实际用到的索引 |
key_len | 所选索引的长度 |
ref | 当使用索引等值查询时,与索引作比较的列或常量 |
rows | 预计要读取的行数 |
filtered | 按表条件过滤后,留存的记录数的百分比 |
Extra | 附加信息 |
2 如何分析 EXPLAIN 结果
id
SELECT 标识符,是查询中 SELECT 的序号,用来标识整个查询中 SELELCT 语句的顺序。id 如果相同,从上往下依次执行。id 不同,id 值越大,执行优先级越高,如果行引用其他行的并集结果,则该值可以为 NULL。
select_type
查询的类型,主要用于区分普通查询、联合查询、子查询等复杂的查询,常见的值有:
- SIMPLE:简单查询,不包含 UNION 或者子查询。
- PRIMARY:查询中如果包含子查询或其他部分,外层的 SELECT 将被标记为 PRIMARY。
- SUBQUERY:子查询中的第一个 SELECT。
- UNION:在 UNION 语句中,UNION 之后出现的 SELECT。
- DERIVED:在 FROM 中出现的子查询将被标记为 DERIVED。UNION RESULT:UNION 查询的结果。
table
查询用到的表名,每行都有对应的表名,表名除了正常的表之外,也可能是以下列出的值:<unionM,N>
: 本行引用了 id 为 M 和 N 的行的 UNION 结果;<derivedN>
: 本行引用了 id 为 N 的表所产生的的派生表结果。派生表有可能产生自 FROM 语句中的子查询。 <subqueryN>
:本行引用了 id 为 N 的表所产生的的物化子查询结果。
type(重要)
查询执行的类型,描述了查询是如何执行的。所有值的顺序从最优到最差排序为:system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL
常见的几种类型具体含义如下:
- system:如果表使用的引擎对于表行数统计是精确的(如:MyISAM),且表中只有一行记录的情况下,访问方法是 system ,是 const 的一种特例。
- const:表中最多只有一行匹配的记录,一次查询就可以找到,常用于使用主键或唯一索引的所有字段作为查询条件。
- eq_ref:当连表查询时,前一张表的行在当前这张表中只有一行与之对应。是除了 system 与 const 之外最好的 join 方式,常用于使用主键或唯一索引的所有字段作为连表条件。
- ref:使用普通索引作为查询条件,查询结果可能找到多个符合条件的行。
- index_merge:当查询条件使用了多个索引时,表示开启了 Index Merge 优化,此时执行计划中的 key 列列出了使用到的索引。
- range:对索引列进行范围查询,执行计划中的 key 列表示哪个索引被使用了。
- index:查询遍历了整棵索引树,与 ALL 类似,只不过扫描的是索引,而索引一般在内存中,速度更快。
- ALL:全表扫描。
possible_keys
possible_keys 列表示 MySQL 执行查询时可能用到的索引。如果这一列为 NULL ,则表示没有可能用到的索引;这种情况下,需要检查 WHERE 语句中所使用的的列,看是否可以通过给这些列中某个或多个添加索引的方法来提高查询性能。
key(重要)
key 列表示 MySQL 实际使用到的索引。如果为 NULL,则表示未用到索引。
key_len
key_len 列表示 MySQL 实际使用的索引的最大长度;当使用到联合索引时,有可能是多个列的长度和。在满足需求的前提下越短越好。如果 key 列显示 NULL ,则 key_len 列也显示 NULL 。
rows
rows 列表示根据表统计信息及选用情况,大致估算出找到所需的记录或所需读取的行数,数值越小越好。
Extra(重要)
这列包含了 MySQL 解析查询的额外信息,通过这些信息,可以更准确的理解 MySQL 到底是如何执行查询的。常见的值如下:
- Using filesort:在排序时使用了外部的索引排序,没有用到表内索引进行排序。
- Using temporary:MySQL 需要创建临时表来存储查询的结果,常见于 ORDER BY 和 GROUP BY。
- Using index:表明查询使用了覆盖索引,不用回表,查询效率非常高。
- Using index condition:表示查询优化器选择使用了索引条件下推这个特性。
- Using where:表明查询使用了 WHERE 子句进行条件过滤。一般在没有使用到索引的时候会出现。
- Using join buffer (Block Nested Loop):连表查询的方式,表示当被驱动表的没有使用索引的时候,MySQL 会先将驱动表读出来放到 join buffer 中,再遍历被驱动表与驱动表进行查询。这里提醒下,当 Extra 列包含 Using filesort 或 Using temporary 时,MySQL 的性能可能会存在问题,需要尽可能避免。
参考:
MySQL执行计划分析
3) 优化 SQL 查询语句
根据 SQL 查询语句的瓶颈所在,可以采取不同的优化方法。下面列举几种常见的优化方法:
-
添加索引:如果 SQL 查询语句没有使用索引或者索引使用不当,可以通过添加索引来优化 SQL 查询语句的性能。需要注意的是,添加索引也有可能降低性能,因此需要权衡索引的使用。
-
优化查询条件:如果 SQL 查询语句的查询条件不合理,可以通过优化查询条件来提高 SQL 查询语句的性能。例如,可以使用更合适的数据类型、避免使用函数等。
-
优化 SQL 查询语句结构:如果 SQL 查询语句的结构不合理,可以通过重构 SQL 查询语句来提高性能。例如,可以使用子查询、使用 Join 语句等。
-
减少数据扫描次数:如果 SQL 查询语句需要扫描大量数据,可以通过减少数据扫描次数来提高性能。例如,可以使用分页查询、避免使用 like 模糊查询等。
5 有用过分库分表吗?是怎么实现的?
用sharding-jdbc
当Sharding-JDBC接受到一条SQL语句时,会陆续执行 SQL解析 => 查询优化 => SQL路由 => SQL改写 => SQL执行 =>结果归并 ,最终返回执行结果
参考:
sharding-jdbc分库分表-使用及原理
6 mysql是如何锁住一条数据的
场景是:经典的就是操作银行卡, 用户A有一张cardA,现在在取钱,同时用户B在往用户A的cardA里打钱,如何保证数据一致性:
答:在MySQL中,当多个事务同时访问数据库中的同一条数据时,就会出现并发控制的问题。为了解决这个问题,MySQL提供了多种锁机制,其中最常用的是行级锁。
行级锁是MySQL中最细粒度的锁,它可以在对数据进行读取或修改时,对该数据行进行锁定,避免其他事务对该数据行进行操作。使用MySQL锁定一条数据可分为以下步骤:
1. 开始一个事务
START TRANSACTION;
2. 锁定数据行
SELECT * FROM table WHERE id = 1 FOR UPDATE;
3. 对数据行进行操作
UPDATE table SET column = value WHERE id = 1;
4. 提交事务
COMMIT;
在这个过程中,使用SELECT … FOR UPDATE语句可以锁定一条数据行,当其他事务尝试访问该数据行时,将会被阻塞或等待。在修改完数据后,必须提交事务,这样才能释放锁,其他事务才能进行操作。
除了行级锁外,MySQL还提供了表级锁和页级锁等多种锁机制,程序开发人员应该根据具体应用场景选择合适的锁机制来实现数据并发控制。
参考:
mysql 锁定一条数据
7 MongoDB和mysql有什么区别?
1、数据模型,MongoDB是一种面向文档的数据库,使用BSON(Binary JSON)格式存储文档,文档可以包含任意数量和类型的字段。而MySQL是一种关系型数据库,使用表格来存储数据,每个表格由多个行和列组成。
2、数据查询,MongoDB使用一种称为MongoDB Query Language(MQL)的语言来查询数据,它可以轻松地查询嵌套文档和数组。而MySQL使用结构化查询语言(SQL)来查询数据,它可以对多个表格进行复杂的联合查询;
3、扩展性,MongoDB可以很容易地扩展到多个服务器,它支持分片和复制来实现高可用性和可伸缩性。而MySQL也可以扩展到多个服务器,但通常需要更多的配置和管理。
4、数据处理,MongoDB内置了一些数据处理功能,如地理空间索引和聚合管道,这些功能可用于大型和复杂的数据集。而MySQL需要使用第三方库或自定义函数来实现类似的功能。
8 幻读和不可重复读有什区别?幻读是怎么产生的
脏读是指一个事务读取到了其他事务没有提交的数据,如果其他事务失败回滚了,就那就是读的脏数据了。
不可重复读是指在同一个事务内,同一条SQL语句的多次查询的同一条记录的值不一致。
可重复读即相反。
幻读是它发生在一个事务读取了几行数据,接着另一个并发事务插入了一些数据时。在随后的查询中,第一个事务就会发现多了一些原本不存在的记录,就好像发生了幻觉一样。
幻读和不可重复读区别:
- 不可重复读的重点是内容修改或者记录减少比如多次读取一条记录发现其中某些记录的值被修改;
- 幻读的重点在于记录新增比如多次执行同一条查询语句(DQL)时,发现查到的记录增加了。
幻读产生的原因:
行锁只能锁住行、但是新插入记录这个动作、要更新的是记录之间的间隙。
如何解决幻读:
引入了间隙锁(Gap Lock)
事务隔离级别脏读、幻读、不可重复读的关系
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
READ UNCOMMITED(读未提交) | 允许 | 允许 | 允许 |
READ COMMITED(读已提交) | 不允许 | 允许 | 允许 |
REPEATABLE READ(可重复读) | 不允许 | 不允许 | 允许 |
SERIALIZABLE(串行化) | 不允许 | 不允许 | 不允许 |
9 在mysql默认的隔离级别下会发生幻读吗
会的,见上表
10 mysql使用b+树的原因
- 提高索引查询时的磁盘IO效率。还可以提高范围查询的效率
- 所有查询都要查找叶子节点,查询性能稳定
- B+树里的元素也都是有序的
2、国产化数据库(达梦、人大金仓)
1 mysql和达梦的语法有哪些区别
- 创建表的时候,不支持在列的后面直接加 comment 注释,使用 COMMENT ON IS 代替
- 不支持 case-when-then-else
- 不支持 if。
- 不支持 “”(双引号使用),只支持’’(单引号使用)
- 不支持 longtext、TINYBLOB、MEDIUMBLOB、LONGBLOB类型,可用 CLOB 代替
- 不支持 date_format 函数,它有三种代替方法:
a: 使用 datepart 代替:语法:datepart(datepart, date),返回代表日期的指定部分的整数,
b: 使用 date_part 代替,功能和 datepart 一样,写法不同,参数顺序颠倒,且都要加引号,
c: 使用 extract 代替,
语法:extract(dtfield from date),从日期类型date中抽取dtfield对应的值
dtfield 可以是 year,month,day,hour,minute,second - 不支持 substring_index 函数, 使用 substr / substring 代替,
- 不支持 group_concat 函数,使用 wm_concat 代替,
- 不支持 from_unixtime 函数,使用 round 代替
- current_timestamp 的返回值带有时区
等等。。。
3、oracle
1 oracle有哪些函数
1、字符串函数,包括ASCII()、CONCAT()等;
2、数字函数,包括ABS()、COS()等;
3、日期函数,包括EXTRACT()、ROUND()等;
4、转换函数,包括TO_CHAR()、TO_DATE()等。
六、Redis相关
1、Redis的数据结构
2、Zset的用法
3、如何实现分布式锁
4、如何实现数据(缓存数据和数据库数据)的一致性
-
读写时更新缓存:
在数据写入数据库时,先更新数据库,然后再更新或删除缓存中的对应数据。这确保了数据库数据的更新优先,保持数据一致性。 -
缓存失效策略:
设置合适的缓存失效策略,确保缓存中的数据不会过期太快或太慢。短期内的频繁读取可以使用短暂的缓存,长期有效的数据可以使用长时间的缓存。 -
数据库事件监听:
监听数据库的变化事件(如插入、更新、删除操作),一旦发生数据变动,立即更新缓存中的对应数据。这可以通过数据库触发器、消息队列等方式来实现。 -
版本控制:
在缓存中存储数据的同时,存储一个版本号或时间戳。在读取数据时,先检查版本号或时间戳,确保缓存中的数据是最新的。如果数据有更新,同时更新版本号或时间戳。 -
写后更新缓存:
当有写操作发生时,先更新数据库,然后再更新或删除缓存中的对应数据。这种方式可以减少并发写操作对数据一致性的影响。 -
定时刷新缓存:
定时刷新缓存,例如每隔一段时间或在特定时间点,强制刷新缓存中的数据,以确保数据的及时更新。 -
使用缓存中间件:
一些缓存中间件或数据库中间件提供了自动的缓存刷新机制,可以在数据库更新时自动更新缓存中的数据。 -
双写策略:
在写操作时,同时更新数据库和缓存。这可以确保写入数据库后,缓存中的数据也会得到更新。
七、消息队列相关的问题
1、RabbitMQ
1、RabbitMQ怎么实现的延迟消息队列
Rabbitmq可以通过以下两种方式实现延时消息:
① Rabbitmq插件rabbitmq_delayed_message_exchange 安装这个插件后,可以使用x-delayed-message类型的exchange来实现延时消息。
具体步骤如下:
- 安装插件:rabbitmq-plugins enable rabbitmq_delayed_message_exchange
- 创建exchange时指定类型为x-delayed-message:arguments = {“x-delayed-type”: “direct”}
- 发送消息时,在消息头中设置延迟时间:headers = {“x-delay”: 5000} (单位为毫秒)
② 建立延时队列并设置TTL 在Rabbitmq中,可以创建一个队列,并设置队列的TTL(生存时间)。当消息被发送到该队列时,如果没有被消费者消费,在TTL过期后,该消息将会被自动删除。
具体步骤如下:
- 创建一个队列,设置队列的TTL:arguments = {“x-message-ttl”: 5000} (单位为毫秒)
- 发送消息到该队列
- 消费该队列时,需要设置auto_ack为false,并在处理完消息后手动确认ack
以上两种方式都可以实现延时消息,选择哪种方式取决于项目的实际需求和使用场景。