Java面试题总结2023

Java面试题总结2023

  • 基础
    • String中常用的方法
    • == 与 equals的区别
    • 值传递和引用传递
    • 数组和集合的区别
    • 成员变量和局部变量的区别
    • final和finally和finalize的区别
    • Cookie和Session的的区别
    • 接口分类
    • 接口和抽象类的区别
    • 说说你对抽象类的理解
    • String/StringBuffer/StringBuilder
    • jdk1.8的新特性
    • Java的基本数据类型
    • java中异常分为那些种类
  • 集合
    • java中的集合有哪些?
    • Java中ArrayList和Linkedlist区别?
    • HashMap底层原理
    • hashmap和hashtable的区别
    • hashmap传送门
  • JVM
    • 介绍一下JVM
    • Java类的加载机制
    • 垃圾回收机制
    • java 的GC 什么时候回收拦击
    • jvm的内存模型
    • 内存泄漏
  • 多线程
    • 线程和进程
    • 线程的创建方式
    • 线程创建方式的区别
    • wait(),yield和sleep()的区别
    • 说说你对线程池的理解
    • 线程池的应用场景
    • 线程池的优势
    • 线程池的使用
    • 线程池的执行原理
    • Java线程具有五中基本状态是那些?
  • Mysql
    • sql执行慢的原因
    • 表设计三范式
    • mysql的数据类型
    • mysql搜索引擎
    • 数据库的隔离级别
    • mysql的优化
    • 分库分表
      • 搭建mycat的核心配置文件
      • 主从复制,读写分离.
      • 主从实现方式
      • 读写分离的实现方式
    • 索引
    • 放弃索引优化
    • 数据库为什么使用B+树而不是B树
    • 索引设计的原则
    • 事务的隔离级别
    • mysql的锁有哪些
    • 什么是乐观锁和悲观锁?
    • 主键索引和唯一索引有啥区别?
  • Redis
    • Redis缓存预热
    • Redis 是单线程的么?线程安全么?
    • redis传送门
  • MQ
    • MQ消息中间件的作用?为什么要用消息中间件?
    • 如何保证MQ消息不丢失?如何解决数据丢失的问题?
    • 如何确保消息正确的发送到MQ?
    • 如何避免消息重复投递或重复消费?
    • 使用MQ的优点和缺点是什么?
    • 使用MQ的场景能举例说一些么?
    • activeMQ和rabbitMQ有什么区别?
    • RabbitMQ 宕机了怎么处理
  • Spring
    • Springmvc 的运行原理
    • Spring中的设计模式有哪些
    • 简单介绍一下Spring bean 的生命周期
    • spring有哪些核心组件
    • #{}和${}的区别是什么?
    • IOC/DI 的区别
    • 分布式ID生成解决方案
  • ES
    • 为什么要使用Elasticsearch?
    • ES的应用场景
  • 微服务
    • Spring是一个生态体系
    • Spring的常用注解
    • Springboot注解
    • Springboot自动装配
    • Nacos
    • Ribbon(负载均衡)
    • Spring Cloud项目中用到哪些组件?
    • Feign的工作原理?
  • 服务部署
    • 说下Linux常用命令

基础

String中常用的方法

   split():分割
   toString():获取出字符串的内容
   charAt():获取指定索引处的字符
   equals():判断两个字符串内容是否相等
   substring():截取
   getBytes():将字符串转成字节数组
   valueOf():将数据转成字符串                  
   trim():去除两端空格
   compareTo():按照字典顺序比较字符串

== 与 equals的区别

==比较基本数据类型的值是否相等,比较引用数据类型的地址值是否相等, equals本来是比较引用数据类型的地址,可以重写此方法比较对象的属性是否相等。

值传递和引用传递

值传递就是传递的过程中,传递的是值,对值操作之后,不会影响原有变量的值
引用传递就是传递的过程中,传递的是引用,操作引用之后,会影响原有变量的值

数组和集合的区别

长度区别:
数组的长度是固定的而集合的长度是可变的
·存储数据类型的区别:
数组可以存储基本数据类型 , 也可以存储引用数据类型; 而集合只能存储引用数据型
内容区别:
数组只能存储同种数据类型的元素 ,集合可以存储不同类型的元素

成员变量和局部变量的区别

在类中的位置不同
成员变量:在类中方法外
局部变量:在方法定义中或者方法声明上
在内存中的位置不同
成员变量:在堆内存
局部变量:在栈内存
·生命周期不同
成员变量:随着对象的创建而存在,随着对象的消失而消失
局部变量:随着方法的调用而存在,随着方法的调用完毕而消失
初始化值不同
成员变量:有默认初始化值
局部变量:没有默认初始化值,必须定义,赋值,然后才能使用。

final和finally和finalize的区别

final
是最终的,不可变的。修饰类,该类不能被继承,修饰
方法,方法不能被重写,修饰变量,变量变成常量。
finally
try…catch的异常处理中,里面是必定会执行的代码,一般用于资源释放。
finalize
是Object类中的一个方法,当垃圾回收器确定不存在, 对该对象的更多引用时,由对象的垃圾回收器调用此方法。

Cookie和Session的的区别

1、数据存储位置:cookie数据存放在客户的浏览器上,session数据放在服务器上。
2、安全性:cookie不是很安全,别人可以分析存放在本地的cookie并进行cookie欺骗,考虑到安全应
当使用session。
3、服务器性能:session会在一定时间内保存在服务器上。当访问增多,会比较占用你服务器的性能,
考虑到减轻服务器性能方面,应当使用cookie。
4、数据大小:单个cookie保存的数据不能超过4K,很多浏览器都限制一个站点最多保存20个cookie。
5、信息重要程度:可以考虑将登陆信息等重要信息存放为session,其他信息如果需要保留,可以放在
cookie中。

接口分类

1,普通接口
2,函数式接口:接口内部有 且只有一个抽象方法。
3,标记型接口:接口内没有方法,它的目的就是为了声明某些东西,例如serializable

接口和抽象类的区别

构造方法:
接口:没有构造方法
抽象类:有构造方法,用于子类访问父类成员时给父类成员初始化。
成员方法:
接口:只能有抽象方法,1.8以后可以有默认方法
抽象类:可以由抽象方法和普通方法
成员变量:
接口:只能是静态常量
抽象类:可以是常量,也可以是变量。

说说你对抽象类的理解

1. 抽象类大部分使用场景和普通类并没有太大区别,比如成员变量、成员方法,构造方法,
2. 抽象类通过abstract修饰,抽象类中不一定有抽象方法,但有抽象方法的类一定是抽象类
3. 抽象类不能实例化(可以通过子类继承抽象类,实例化子类)
4. 抽象类主要用于公共方法的抽取,便于子类继承父类后,重写这些公共的方法
5.子类继承抽象类(或实现接口),必须重写父类(或接口中)的抽象方法,除非子类也是抽象类

String/StringBuffer/StringBuilder

String是不可变的,stringbuffer 和builder 是字符串可变。StringBuffer和StringBuilder都继承了AbstractStringBuilder, buff 方法中加了同步锁,线程是安全的,builder 没有加同步锁,线程是不安全的,在内存中的存储方式与String相同,都是以一个有序的字符序列(char类型的数组,在jdk9当中将char 数组换成了 byte 数组)进行存储,不同点是StringBuffer/StringBuilder对象的值是可以改变的,并且值改变以后,对象引用不会发生改变 ,操作少量的字符串用String ,单线程的情况下操作的大量的字符串用Stringbuilder,在多线程的情况下用StringBuffer。

jdk1.8的新特性

1.新增lamda表达式这块,遍历集合以及定义匿名函数
2.switch中的变量可以是string类型,之前只能是基本数据类型
3.增加流式编程,让我们用stream的方式,可以非常方便的对集合里的数据操作
4.新的时间类,LocalDate、LocalTime、LocalDateTime这几个类,让我们操作日期的时候非常方便
5.Java 8允许我们给接口添加一个非抽象的方法实现,只需要使用 default关键字就行了

Java的基本数据类型

Java的类型分为四类八种
四类为:整型,浮点型,布尔型,字符型
基本数据类型有8种,分别是byte、short、int、long 、float、double、char、boolean
对应包类Byte,Short,Integer,Long,Float,Double,Character,Boolean

java中异常分为那些种类

1、Error:重大的问题,我们处理不了。也不需要编写代码处理。比如说内存溢出。
2、Exception:一般性的错误,是需要我们编写代码进行处理的。
(1)RuntimeException运行时异常。
(2)NullPointerException空指针异常,最常遇到
(3)ArrayIndexOutOfBoundsException 下标越界异常
(4)IndexOutOfBoundsException 索引越界异常。当访问某个序列的索引值小于0或大于等于序列大小时,抛出该异常。
(5) SQLException sql异常
(6)检查异常/编译异常 、 对于检查的异常必须处理,可以采用捕获或抛出式解决
(7)ClassNotFoundException 类找不到异常
(8)FileNotFound 文件找不到异常,在文件操作的时候,一不小心路径写错了,或者是windows切换linux的时候,因为路径格式不一致,经常会有这个错误
(9)ParseException解析异常,一般当日期字符串解析时与指定格式不一致就是出现这个问题

集合

java中的集合有哪些?

List中有:
ArrayList:底层实现是数组,查询快,增删慢,线程不安全,效率高,每次扩容为当前容量的1.5倍+1;
Vector:底层实现是数组,查询快,增删慢,线程安全,效率低,扩容机制为翻倍;
LinkedList:底层实现是双向链表,增删快,查询慢,线程不安全,效率高;
Set 中有:
HashSet:无序不可重复,底层使用Hash表实现,存取速度快。
LinkedHashSet:采用hash表存储,使用双向链表记录插入顺序
Map中有:
HashMap、key重复则覆盖原有的value,底层使用数组+链表实现,jdk1.8以后加入了红黑树,线程不安全,key和value都允许为null
HashTable:key重复则覆盖原有的value,底层使用数组+链表实现,jdk1.8以后加入了红黑树,线程安全,key和value都不允许为null

Java中ArrayList和Linkedlist区别?

相同点:
1、都是List 接口的实现类,具有元素可重复,存取有序特点;
2、都是线程不安全,效率高;
不同点:
1、数据结构:ArrayList底层是动态数组,LinkedList底层是双向链表;
2、随机访问效率:ArrayList效率优先于LinkedList ,因为LinkedList 是线性的数据存储方式,指针从前往后依次查询。
3、增加和删除效率:LinkedList效率优先于ArrayList ,因为ArrayList 增删操作要影响数组内的其他数据的下标。

HashMap底层原理

(1)在jdk1.8中 底层是数组 链表 红黑树实现
在Hashmap中,初始化长度为16,当用put方法存储数据时,传入key及value值,
此时将key值进行hash运算后的hash值作为entry键值对在数组中的索引位置,确定位置之后,首先判断该位置是否为空,如果为空,就将entry值放在该位置,否则将entry以链表的方式存在数组中,当链表长度超过8位之后,会将链表转换为红黑树继续存储entry。
(2)在jdk1.8之前,底层是通过数组+链表实现的,当我们创建hashmap时会先创建一个数组。
当我们用put方法存数据时,先根据key的hashcode值计算出hash值,然后用这个哈希值确定在数组中的位置,再把value值放进去,如果这个位置本来没放东西,就会直接放进去;如果之前就有,就会生成一个链表,把新放入的值放在头部。
当用get方法取值时,会先根据key的hashcode值计算出hash值,确定位置,再根据equals方法从该位置上的链表中取出该value值。
当容量超过当前容量的0.75倍之后,就会自动扩容为原来容量的2倍。这个0.75就是负载因子。
但是在jdk1.8之后,最大的不同就是其由 数组+链表+ 红黑树组成。因为在1.7的时候,这个链表的长度不固定,所以如果key的hashcode重复之后,
那么对应的链表的数据的长度就无法控制了,get数据的时间复杂度就取决于链表的长度了,为了提高这一部分的性能,
加入了红黑树,如果链表的长度超过8位之后,会将链表转换为红黑树,极大的降低了时间复杂度

HashMap 线程不安全,有多个线程同时 HashMap,可能会导致数据的不一致。如果需要满足线程安全,可以使用 ConcurrentHashMap。

hashmap和hashtable的区别

相同点:
1、都是key-value 的双列集合;
2、都是数组+链表的底层原理;
3.都实现了Map 接口。
不同点:
1、继承的父类不同
Hashtable 继承Dictionary类,而HashMap 继承Abstract Map类。
2、线程安全性不同
hashMap 允许null键和null值为空,线程不同步,不安全,效率高,hashtable 不允许null键和null 值,线程同步,安全,效率低。
在java 开发中常用的是HashMap 类,比如ConcurrentHashMap,可以实现线程安全,Hashtable 和vector 一样成为了废弃类。

hashmap传送门

JVM

介绍一下JVM

VM主要包括:类加载器、执行引擎、本地接口、运行时数据区
类加载器:加载类文件到内存。只管加载,只要符合文件结构就加载
执行引擎:负责解释命令
本地接口:本地接口的作用是融合不同的语言为java所用。
JVM的运行时数据区分为五个区域:堆、栈、本地方法栈、方法区、计数器。
计数器:这里记录了线程执行的字节码的行号。
栈:每个方法执行的时候都会创建一个栈,用于存放 局部变量表、动态链接。
本地方法栈:与栈类似,是执行本地方法。
堆:堆就是存放对象实例,几乎所有的对象实例都在这里分配内存。
方法区:用于存储Java虚拟机加载的类信息、常量、静态变量、以及编译器编译后的代码等数据。

Java类的加载机制

加载
Classloader通过完全限定名查找字节码文件,并利用字节码文件创建一个class对象.
验证
确保class文件所包含的字节流信息符合当前虚拟机的要求,不会对虚拟机造成自身安全,主要包括四种验证 字节码 元数据 符号引用 文件格式验证
准备
在方法区中为类变量分配内存并设置初始值。实例变量不会在这里分配初始化,而是随着对象分配到Java堆中。给静态变量分配内存空间
解析
将常量池中的符号引用替换成直接引用
初始化
如果有父类就对父类进行初始化 给静态变量和静态代码库进行初始化工作。

(内存优化以及垃圾回收)

垃圾回收机制

垃圾收集器一般必须完成两件事:检测出垃圾;回收垃圾。
检测垃圾一般有以下几种方法:引用计数法,可达性分析算法。引用计数法:给一个对象引用计数,为0则当作垃圾处理。可达性分析算法:以根集对象为起始点进行搜索,如果有对象不可达的话,即是垃圾对象。
处理垃圾有四种算法,1标记-清除(Mark-sweep)分为两个阶段:标记和清除。标记所有需要回收的对象,然后统一回收。2复制(Copying)把内存空间划为两个相等的区域,每次只使用其中一个区域。垃圾回收时,遍历当前使用区域,把正在使用中的对象复制到另外一个区域中。3 标记-整理(Mark-Compact)结合了“标记-清除”和“复制”两个算法的优点。也是分两阶段,第一阶段从根节点开始标记所有被引用对象,第二阶段遍历整个堆,清除未标记对象,并且把存活对象“压缩”到堆的其中一块,按顺序排放。 4分代收集算法(当今最常用的方法) 分代的垃圾回收策略,对不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。

java 的GC 什么时候回收拦击

1.执行到System.gc 的时候。
2.新生代对象晋升到老年代对象的时候,老年代剩余的 空间低于新生代晋升老年代的速效率,会触发老年代的回收,
3.new 一个对象的时候,新生代放不下,直接发到老年代,空间不够,触发fullgc.
对象划分
年轻代:是所有新对象产生的地方
年老代:在年轻代中经历了N次回收后仍然没有被清除的对象,就会被放到年老代中
持久代:用于存放静态文件,比如java类、方法等

jvm的内存模型

Java堆(Heap),是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,我们创建的所有对象,都在堆中
方法区(Method Area),各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量
程序计数器(Program Counter Register),是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器。
JVM栈(JVM Stacks),也是线程私有的,它的生命周期与线程相同。
本地方法栈(Native Method Stacks)为虚拟机使用到的Native方法服务。
内存优化(1)
内存溢出(oom)
程序运行时占用的内存超出系统分配的内存,就会出现内存溢出
引发原因:递归,死循环,大量内存泄露

内存泄漏

当对象销毁后,gc在回收该实例的时候,发现该实例的被其他对象持有引用,导致不能被回收,出现内存泄漏
1, 内部类引发的内存泄漏
因为java当中,内部类默认持有外部类的引用,当外部类销毁后,一旦gc回收该实例,发现内部类持有他的引用而导致不能回收该实例,出现内存泄漏的情况。
解决方法:将内部类改为静态内部类,因为静态内部类生命周期和进程一样长,且不持有外部类,不会影响外部类对象的回收
2, 资源未关闭
Cursor,stream,database比如这些东西在使用完成后,需要进行close或者Unbind处理,以节省内存
3, 线程导致的内存泄漏:一般使用异步线程都会创建一个内部类对象,而创建线程一般执行耗时任务,所以这个内部类默认持有外部类的引用,如果耗时任务在外部类销毁的时候未执行完成,会因为持有外部类引用导致外部类不能被回收
内存抖动
内存抖动是由于短时间内有大量对象创建和销毁,它伴随着频繁的GC。gc会频繁抢占cup资源,影响其他线程的执行效率(对象的销毁和创建操作的是堆内存,堆内存也叫动态内存,是一块不连续的内存区域,如果频繁的创建和销毁,会造成大量的内存碎片,消耗程序性能)
解决方案:
· 尽量避免在循环体内创建对象,或者在循环内进行字符串拼接。
· 对于能够复用的对象,可以使用对象池将它们缓存起来。

多线程

线程和进程

进程:系统进行资源分配和调度的一个独立单位
线程: 是比进程更小的可独立运行的基本单位,一般处于进程当中,可以看做是轻量级的进程

线程的创建方式

继承thread,重写run方法
实现runnable接口,重写run方法
实现callable接口,重写call方法,相对runnable,可以声明返回类型。

线程创建方式的区别

1, thread方式,最为简单,可以通过this获取当前线程,但是因为继承了thread类,不能在继承其他类了
2, runnable方式,子类实现这个接口,可以继承其他的类,多个线程可以共享同一个target对象,没有返回值。

wait(),yield和sleep()的区别

sleep:属于thread类,指定休眠时间,不会释放锁,到时间后会自动执行
yield:不指定时间,礼让优先级高的线程执行,也不会释放锁,当别的线程执行完毕后,如果没有优先级更高的线程需要执行,继续执行当前线程,否则继续抢占CPU资源。
Wait:属于object类,一般不指定时间,释放锁,需要通过调用notify或notifyall方法唤醒,才能继续执行(如果给wait设置了等待时间,如果到时间还没有唤醒,则会自动唤醒。

说说你对线程池的理解

线程池就是提前创建好若干个线程,如果有任务需要进行处理的话,线程池里面的线程就会去处理任务,处理完成之后,线程就不会消失,而是等待一下一个任务,由于创建和销毁的线程是消耗系统资源的,所以当你频繁的创建和销毁的时候就需要考虑线程池来提升系统的性能。

线程池的应用场景

线程池的我做的第一个项目,当时用线程池做过文件上传,最近一次用线程池,就是最近做过一个电网类项目,我们对用户的内容搜索历史做了保存,当用户搜索的内容为新内容时,需要添加这个搜索记录,但是搜索的记录如果之前是存在的,需要对之前保存的记录进行更新,而如果用户根据搜索历史再次搜索,我们在更新记录的同时,不能影响搜索结果的查询,所以当时我们对记录新增及更新这个接口放到了线程池当中,这个线程池是使用Java原生的threadPoolExcutor来实现的,并对核心线程数、最大线程及维护时间进行了自定义。

线程池的优势

1.降低系统资源消耗,通过重用已存在的线程,降低线程创建和销毁造成的消耗;
2.提高系统响应速度,当有任务到达时,无需等待新线程的创建便能立即执行;
3.方便线程并发数的管控,线程若是无限制的创建,不仅会额外消耗大量系统资源,更是占用过多资源而阻塞系统或oom等状况,从而降低4.系统的稳定性。线程池能有效管控线程,统一分配、调优,提高资源使用率;
5.更强大的功能,线程池提供了定时、定期以及可控线程数等功能的线程池,使用方便简单。

线程池的使用

创建线程池

ExecutorService service = new ThreadPoolExecutor(…)
核心参数
corePoolSize:核心线程数,默认情况下,核心线程一直存活在线程池中,即便他们在线程池中处于闲置状态。除非我们将ThreadPoolExecutor的allowCoreThreadTimeOut属性设为true的时候,这时候处于闲置的核心线程在等待新任务到来时会有超时策略,这个超时时间由keepAliveTime来指定。一旦超过所设置的超时时间,闲置的核心线程就会被终止
maximumPoolSize:线程池中所容纳的最大线程数,如果活动的线程达到这个数值以后,后续的新任务将会被阻塞。包含核心线程数+非核心线程数。
keepAliveTime:非核心线程闲置时的超时时长,对于非核心线程,闲置时间超过这个时间,非核心线程就会被回收。只有对ThreadPoolExecutor的allowCoreThreadTimeOut属性设为true的时候,这个超时时间才会对核心线程产生效果
unit:时间单位
workQueue:线程池中保存等待执行的任务的阻塞队列
threadFactory:为线程池提供新线程的创建
handler:无法处理新任务的时候抛出异常(拒绝策略)

线程池的执行原理

①如果在线程池中的线程数量没有达到核心的线程数量,这时候就回启动一个核心线程来执行任务。
②如果线程池中的线程数量已经超过核心线程数,这时候任务就会被插入到任务队列中排队等待执行。
③由于任务队列已满,会立即启动一个非核心线程来执行任务(当然线程池里边目前的线程数量不能超过最大线程数)
④如果线程池中的数量达到了所规定的最大值,那么就会拒绝执行此任务,这时候就会调用RejectedExecutionHandler中的rejectedExecution方法来通知调用者。

Java线程具有五中基本状态是那些?

新建状态
使用new关键字或thread类和子类创建一个线程对象后,该线程对象处于新建状态,并保持这个状态直到程序start这个线程
就绪状态
当线程对象调用了start方法之后 线程进入就绪状态 在就绪队列中等待jvm线程调度器的调度
运行状态
如果就绪状态线程获得cpu资源后 就可以执行run方法 然后线程就进入运行状态 处于运行状态的线程及其复杂 它可以变为 阻塞状态 死亡状态 就绪状态
阻塞状态
如果一个线程执行了sleep suspend方法 失去所有资源后 线程进入阻塞状态 时间过了之后 获得设备资源的线程可重新进入就绪状态
死亡状态
一个运行状态的线程完成任务或终止条件发生 线程进入死亡状态 terminated执行完成。

Mysql

sql执行慢的原因

1.硬件问题。如网络速度慢,内存不足,I/O吞吐量小,磁盘空间满了等。
2.没有索引或者索引失效。
3.数据过多(可以通过分库分表解决)

表设计三范式

第一范式:每个列都不可以再拆分。
第二范式:在第一范式的基础上,非主键列完全依赖于主键,而不能是依赖于主键的一部分。
第三范式:在第二范式的基础上,非主键列只依赖于主键,不依赖于其他非主键。
在设计数据库结构的时候,要尽量遵守三范式,如果不遵守,必须有足够的理由。比如性能。事实上我
们经常会为了性能而妥协数据库的设计。

mysql的数据类型

varchar,double,int,tynyint,smallint,mediumint,date,time,datetime,timestamp,text,longtext

mysql搜索引擎

MyISAM、InnoDB、BDB、MEMORY等,对于 MySQL 5.5 及更高版本,默认的存储引擎是 InnoDB。在 5.5 版本之前,MySQL 的默认存储引擎是 MyISAM

• InnoDB 存储引擎:
o 支持自增长列(auto_increment),自增长列的值不能为空,如果在使用的时候为空的话就会从现有的最大值自动+1,如果有但是比现在的还大,则就保存这个值。
o 支持外键(foreignkey),外键所在的表称为子表而所依赖的表称为父表。
o 支持事务,回滚以及系统崩溃的修复能力,并且支持多版本并发控制的事务安全。
o 支持mvcc(多版本并发控制)的行级锁,就是通过多版本控制来实现的乐观锁
o 索引使用的是B+Tree
优缺点:InnoDB的优势在于提供了良好的事务处理、崩溃修复能力和并发控制。缺点是读写效率较差,占用的数据空间相对较大。
• MyISAM 存储引擎
不支持事务、支持表级锁
支持全文搜索
缓冲池只缓存索引文件,不缓存数据文件
MyISAM 存储引擎表由数据文件(MYD)和索引文件( MYI)组成
我们项目中常用到的是innoDB,InnoDB存储引擎提供了具有提交、回滚和崩溃恢复能力的事务安全,但是对比Myisam的存储引擎InnoDB写的处理效率差一些并且会占用更多的磁盘空间以保留数据和索引。

数据库的隔离级别

读未提交,读已提交,可重复读,串行化四个
默认是可重复读,我们在项目中一般用读已提交(Read Commited)这个隔离级别

mysql的优化

1,sql语句添加索引
2,将数据库的数据预热到redis中
3, 主从复制,读写分离
4,分库分表
5,分页查询

分库分表

垂直拆分:是将表按列拆分成多个表,且每个表中要包含主键,尽量把长度较短,访问频率较高的属性放在主表里。
水平拆分:是将一个表,通过 取模、按照日期范围 等等拆分成若干个表。
垂直分库如果没有按照合理的业务逻辑去拆分,后期会带来跨库join,分布式事务等;
跨库join会导致查询性能低下,分布式事务下会导致因数据不一致造成很多脏数据的存在。
(怎么去 分的)

搭建mycat的核心配置文件

schema.xml,配置逻辑库表,分片和读写分离
rule.xml,具体的分片规则和分片算法
server.xml,配置默认的数据库和用户,表权限

主从复制,读写分离.

主从复制,是用来建立一个和主数据库完全一样的数据库环境,称为从数据库;

读写分离就是在主服务器上修改,数据会同步到从服务器,从服务器只能提供读取数据,不能写入,实现备份的同时也实现了数据库性能的优化,以及提升了服务器安全。
主从原理
数据库有个bin-log二进制文件,记录了所有的sql语句。

只需要把主数据库的bin-log文件中的sql语句复制。

让其从数据的relay-log重做日志文件中在执行一次这些sql语句即可。

主从实现方式

同步复制:所谓的同步复制,意思是master的变化,必须等待slave-1,slave-2,…,slave-n完成后才能返回。
异步复制:如同AJAX请求一样。master只需要完成自己的数据库操作即可。至于slaves是否收到二进制日志,是否完成操作,不用关心。MYSQL的默认设置。
半同步复制:master只保证slaves中的一个操作成功,就返回,其他slave不管。

读写分离的实现方式

1)基于程序代码内部实现
在代码中根据 select 、insert 进行路由分类,这类方法也是目前生产环境下应用最广泛的。优点是性能较好,因为程序在代码中实现,不需要增加额外的硬件开支,缺点是需要开发人员来实现,运维人员无从下手。
2) 基于中间代理层实现
代理一般介于应用服务器和数据库服务器之间,代理数据库服务器接收到应用服务器的请求后根据判断后转发到。

索引

分类
按照逻辑分类,索引可分为:
主键索引:一张表只能有一个主键索引,不允许重复,不允许为null;
唯一索引:数据列不允许重复,允许为NULL值,一张表可有多个唯一索引,但是一个唯一索引只能包含
一列,比如身份证号码,卡号都可以作为唯一索引;
普通索引:一张表可以创建多个普通索引,一个普通索引可以包含多个字段,允许数据重复,允许NULL值插入;
全文索引:让搜索关键词更高效的一种索引;
按照物理分类,索引可分为:
聚集索引:一般是表中的主键索引,如果表中没有显示指定主键,则会选择表中的第一个不允许为NULL的唯一索引,如果还是没有,就采用Innodb存储引擎为每行数据内置的6字节rowid作为聚集索引。每张表只有一个聚集索引,因为聚集索引的兼职的逻辑顺序决定了表中相应行的物理顺序。聚集索引在精确查找和范围查找方面有良好的性能表现(相对于普通索引和全表扫描),聚集索引就显得弥足珍贵,聚集索引选择还是要慎重(一般不会让没有语义的自增id充当聚集索引);
非聚集索引:该索引中索引的逻辑顺序与磁盘上行的物理存储顺序不同(非主键的那一列),一个表中可以拥有多个非聚集索引;

放弃索引优化

1,对查询进行优化,应尽量避免全表扫描,首先应考虑在 where 及 order by 涉及的列上建立索引。
2, 应尽量避免在 where 子句中使用!=或<>操作符
3,应尽量避免在 where 子句中对字段进行 null值判断。如: select id from t where num is null 可以在 num 上设置默认值 0,确保表中 num 列没有 null 值,然后这样查询:selectid from t where num=0
4, 应尽量避免在 where 子句中使用 or 来连接条件:
如:select id from t where num=10 or num=20 ,可以使用可以这样查询: select id from t where num=10 union all select id from t where num=20
5,以%开头的模糊查询也会导致全表扫描:
select id from t where name like ‘%abc%’,如果要提高效率的话,可以考虑全文检索来解决。
6,in 和 not in
如: select id from t where num in(1,2,3) 对于连续的数值,能用 between 就不要用 in 了: select id from t where num between 1 and 3
7,应尽量避免在 where 子句中对字段进行表达式操作
如:selectid from t where num/2=100应改为: select id from t where num=1002*
8,应尽量避免在 where 子句中对字段进行函数操作
以上将导致引擎放弃使用索引而 进行全表扫描。

数据库为什么使用B+树而不是B树

1,B树只适合随机检索,而B+树同时支持随机检索和顺序检索,比较符合实际需求;
2,B+树空间利用率更高,由于内部节点只存键,文件小且数据命中的概率高,可减少I/O次数,磁盘读写代价更低
3,B+树的查询效率更加稳定。随机查询时都必须走一条从根节点到叶节点的路,查询效率略低,而B树查询靠近根节点的数据时,效率会更高
4,数据库中基于范围的查询是非常频繁的,B+树的叶子节点使用指针顺序连接在一起,只要遍历叶子节点就可以实现整棵树的遍历,而B树不支持这样的操作
5,B+树的叶子节点包含所有关键字,并以有序的链表结构存储,增删快

索引设计的原则

1.where子句中查询条件对应的列去添加索引
2.基数较小的类,索引效果较差,没有必要在此列建立索引
3.尽量使用短索引,如果对长字符串列进行索引,应该指定一个前缀长度*(检索字段的前面的若干个字符)*,这样能够节省大量索引空间
4.不要过度添加索引。索引需要额外的磁盘空间,降低操作的性能。因为在修改表内容的时候,索引会进行更新甚至重构,索引列越多,这个时间就会越长。所以只保持需要的索引有利于查询即可。 5.数据量小的表最好也不使用索引,因为数据量较少的话,可能查询全部数据比查询索引还要快,索引就可能不会产生优化的效果了 6.text、image和bit的数据类型的列不要建立索引 7.定义有外键的数据列一定要建立索引。 8.尽量的扩展索引,不要新建索引。比如表中已经有a的索引,现在要加(a,b)的索引,那么只需要修改原来的索引即可。

事务的隔离级别

● 读未提交:允许另外一个事务可以看到这个事务未提交的数据,最低级别,任何情况都无法保证。
● 读已提交:保证一个事务修改的数据提交后才能被另一事务读取,而且能看到该事务对已有记录的更新,可避免脏读的发生。
● 可重复读:保证一个事务修改的数据提交后才能被另一事务读取,但是不能看到该事务对已有记录的更新,可避免脏读、不可重复读的发生。
● 串行化:一个事务在执行的过程中完全看不到其他事务对数据库所做的更新,可避免脏读、不可重复读、幻读的发生。

mysql的锁有哪些

按照锁的粒度

行级锁:给表的每一行加锁,开销大,加锁慢;会出现死锁;粒度小,冲突概率低,并发度高

表级锁:给表加锁,开销小,加锁快;不会出现死锁;粒度大,冲突概率高,并发度低。

页级锁:开销和加锁时间、锁定粒度界于表锁和行锁之间;会出现死锁,并发度一般
锁的类别上分
共享锁(读锁):就是读的时候加共享锁,可以加多个,在读的时候,不能写操作
排他锁(写锁):只能加一个,就是写的时候不能受其他事务的干扰

什么是死锁?怎么解决?
死锁是指多个事务在同一资源上相互占用,并请求锁定对方的资源,从而导致恶性循环的现象。
常见的解决死锁的方法
1、如果不同程序会并发存取多个表,尽量约定以相同的顺序访问表,可以大大降低死锁机会。
2、在同一个事务中,尽可能做到一次锁定所需要的所有资源,减少死锁产生概率;
3、对于非常容易产生死锁的业务部分,可以尝试使用升级锁定颗粒度,通过表级锁定来减少死锁产生的
概率;

什么是乐观锁和悲观锁?

1、乐观锁的话就是比较乐观,每次去拿数据的时候,认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制或者CAS 算法实现。乐观锁在读操作比较多的场景比较适用,这样可以提高吞吐量,就像数据库提供的write_condition机制,其实都是乐观锁
2、悲观锁的话就是每次去拿数据的时候,也认为别人会修改数据,这个时候就会加上锁,这就导致其他线程想拿数据的话,就会阻塞,直到这个线程修改完成才会释放锁,让其他线程获取数据。在数据库里的行级锁、表级锁都是在操作之前就先锁住数据再操作数据 ,都属于悲观锁。Java中的 synchronized和 ReentrantLock 等独占锁就是悲观锁思想的实现。

主键索引和唯一索引有啥区别?

键是一种约束,唯一索引是一种索引,两者在本质上是不同的。
主键创建后一定包含一个唯一索引,唯一索引并不一定就是主键。
主键列在创建时,已经默认为空值 + 唯一索引了。
主键可以被其他表引用为外键,而唯一索引不能。
主键更适合那些不容易更改的唯一标识,如自动递增列、身份证号等。
一个表最多只能创建一个主键,但可以创建多个唯一索引
在 RBO 模式下,主键的执行优先级要高于唯一索引。两者可以提高查询的速度
唯一索引列允许空值,而主键列不允许为空值。

Redis

Redis缓存预热

缓存预热就是系统上线后,提前将相关的缓存数据直接加载到缓存系统。
避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!
用户直接查询事先被预热的缓存数据!
缓存预热解决方案:
(1)直接写个缓存刷新页面,上线时手工操作下。
(2)数据量不大,可以在项目启动的时候自动进行加载。

Redis 是单线程的么?线程安全么?

redis 是单线程,线程安全。
因为 Redis 是基于内存的操作,CPU 不是 Redis 的瓶颈,Redis 的瓶颈最有可能是机器内存的大小或者网络宽带。
redis 实际上是采用了线程封闭的观念,把任务封闭在一个线程,自然避免了线程安全问题。
不过对于需要依赖多个 redis 操作的复合操作来说,依然需要锁,而且有可能是分布式锁。

redis传送门

MQ

MQ消息中间件的作用?为什么要用消息中间件?

1、异步处理、有些业务不需要立即执行的,就可以把消息放入队列,然后在处理,这样可以不影响主线程运行
2、应用解耦
降低模块之间的耦合度,提升项目的扩展性。
3、流量削峰
应用可能会因为流量过大导致挂掉,所以一般这类前端请求都会加入消息队列。在秒杀场景中经常见到.

如何保证MQ消息不丢失?如何解决数据丢失的问题?

1、持久化设置、 设置交换机持久化、队列持久化、 消息持久化
2、ACK确认机制、是消费端消费完成要通知服务端,服务端才把消息从内存删除。
3、设置集群镜像模式
4、消息补偿机制、根据状态字段进行补偿,发送完成和接收完成都要对状态做出变更,定时任务检测超时没有接收的或者接收失败的,重新发送。

如何确保消息正确的发送到MQ?

1、rabbitmq中提供了事物和confirm(肯否儿)的机制,事物就类似于数据库的事物,开启,执行,提交,如果过程中发生任何异常,就会触发回滚机制,我们可以在回滚中加入一些逻辑处理,重新发送或者日志记录
2、配置生产者确认的机制,就是在消息发送之后,该消息会被指定唯一的ID,如果有消息成功被交换机转发到队列之后,mq会给生产者发送ack确认,如果没有队列接收消息,那么会发送错误回执消息给生产者,生产者可以尝试重试发送,或者日志记录.

如何避免消息重复投递或重复消费?

1、在消息生产时,MQ 内部针对每条生产者发送的消息生成一个 消息id,作为去重的依据,避免重复的消息进入队列;
2、在消息消费时,要求消息体中必须要有一个业务id作为去重的依据,避免同一条消息被重复消费。

使用MQ的优点和缺点是什么?

优点: 解耦、异步、削峰
缺点:
1、需要保证高可用、MQ若是挂了,容易引起整个服务挂掉
2、系统复杂性增加、需要保证让消息可靠的传递、消息正确的被消费等问题

使用MQ的场景能举例说一些么?

调用短信通知、邮件通知.

activeMQ和rabbitMQ有什么区别?

ActiveMQ基于jms协议,java开发,强在MQ领域所有想要的功能,基本都有,开箱即用。
rabbitMQ基于AMQP协议开发的,使用erlang语言开发,重在基于内存,性能很高。

RabbitMQ 宕机了怎么处理

RabbitMQ 提供了持久化的机制,将内存中的消息持久化到硬盘上,即使重启 RabbitMQ,消息也不会丢失。持久化队列和非持久化队列的区别是,持久化队列会被保存在磁盘中,固定并持久的存储,当 Rabbit 服务重启后,该队列会保持原来的状态在RabbitMQ 中被管理,而非持久化队列不会被保存在磁盘中,Rabbit 服务重启后队列就会消失。非持久化比持久化的优势就是,由于非持久化不需要保存在磁盘中,所以使用速度就比持久化队列快。即是非持久化的性能要高于持久化。而持久化的优点就是消息会一直存在,不会随服务的重启或服务器的宕机而消失。使用的时候需要根据实际需求来判断具体如何使用。

Spring

Springmvc 的运行原理

1.客户端请求提交到DispatcherServlet(前端控制器)
2.由DispatcherServlet控制器查询一个或多个HandlerMapping(处理器映射器),找到处理请求的Controller
3.DispatcherServlet将请求提交到Controller(也称为Handler)
4.Controller调用业务逻辑处理后,返回ModelAndView(模型视图数据)
5.DispatcherServlet查询一个或多个ViewResoler(视图解析器),找到ModelAndView指定的视图
视图负责将结果显示到客户端

Spring中的设计模式有哪些

工厂模式:BeanFactory 就是简单工厂模式的体现,用来创建对象的实例;
单例模式:Bean 默认为单例模式。
模板方法:用来解决代码重复的问题。
代理模式:Spring 的 AOP 功能用到了 JDK 的动态代理和 CGLIB 字节码生成术;
观察者模式:定义对象键一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都会得到通知被制动更新.

简单介绍一下Spring bean 的生命周期

就是从创建到销毁的过程,通过构造方法或者工厂方法,实例化bean对象,并通过依赖注入,设置对象的属性。将Bean实例传递给Bean的前置处理器,调用Bean的初始化方法,再将Bean实例传递给Bean的后置处理器的,然后使用Bean。容器关闭之前,调用Bean的销毁方法销毁实例

spring有哪些核心组件

Spring Core(核心):核心类库,提供 IOC 服务;
Spring Context 提供了框架式的bean的访问方式以及企业级功能如JNDI定时任务等。
Spring AOP:AOP 服务;
Spring ORM: 对现有ORM框架的支持。
Spring DAO:对 JDBC 的抽象,简化了数据访问异常的处理;
Spring Web:提供了基本的面向 Web 的特性 例如多方文件上传;
Spring MVC:提供面向 Web 应用的 Model-View-Controller 实现。

#{}和${}的区别是什么?

#{}是占位符,KaTeX parse error: Expected 'EOF', got '#' at position 19: …连接符。 Mybatis在处理#̲{}时,会将sql中的#{}替…{}时,就是把${}替换成变量的值。
使用#{}可以有效的防止SQL注入,提高系统安全性.

IOC/DI 的区别

控制反转,把对象创建和对象之间的调用过程,交给Spring进行管理,传统应用程序都是由我们在类内部主动创建依赖对象,从而导致类与类之间高耦合,有了IoC容器后,把创建和查找依赖对象的控制权交给了容器,由容器进行注入组合对象,所以对象与对象之间是松散耦合,使得程序的整个体系结构变得非常灵活。
IoC的一个重点是在系统运行中,动态的向某个对象提供它所需要的其他对象。这一点是通过DI来实现的。比如对象A需要操作数据库,以前我们总是要在A中自己编写代码来获得一个Connection对象,有了 spring我们就只需要告诉spring,A中需要一个Connection,至于这个Connection怎么构造,何时构造,A不需要知道。在系统运行时,spring会在适当的时候制造一个Connection,注入到ioc中,这样就完成了对各个对象之间关系的控制。A需要依赖 Connection才能正常运行,而这个Connection是由spring注入到A中的。依赖注入的底层是通过反射来实现的,它允许程序在运行的时候动态的生成对象、执行对象的方法、改变对象的属性,spring就是通过反射来实现注入的。

分布式ID生成解决方案

● UUID
优点:
简单,代码方便;性能好,全球唯一
缺点:
没有排序,无法保证趋势递增。
UUID是36位字符串,查询的效率比较低,存储空间比较大
● Redis生成ID
优点:
不依赖于数据库,灵活方便,且性能优于数据库。
数字ID天然排序,对分页或者需要排序的结果很有帮助。
缺点:
如果系统中没有Redis,还需要引入Redis,增加系统复杂度。
需要编码和配置的工作量比较大。
● 雪花算法snowflake
snowflake是Twitter开源的分布式ID生成算法,结果是一个long型的ID。
其核心思想是:使用41bit作为毫秒数,10bit作为机器的ID,12bit作为毫秒内的流水号,最后还有一个符号位,永远是0,性能极好,使用简单。

ES

为什么要使用Elasticsearch?

1、横向可扩展性好、只需要增加服务器,简单配置,启动进程就可以并入集群;
2、提供更好的分布式特性、同一个索引分成多个分片(sharding),分而治提升处理效率
3、扩展性很强,各种规模的公司都可以选用,根据自己的数据规模选择集群的大小,并且有合理的分布式架构,单个计算节点宕机不会造成整体系统的崩溃
4、可以根据不同的需求方便地定制各类查询,使用简单或者高级的ranking策略;

ES的应用场景

结合项目进行解答

微服务

Spring是一个生态体系

Spring Framework是整个spring生态的**基石,针对于开发的WEB层(springMVC)、业务层(IoC)、持久层(jdbcTemplate)等都提供了多种配置解决方案
Spring Boot默认集成了很多第三方包,将过去繁杂的配置改为注解和Java代码实现
Spring Cloud是一整套基于Spring Boot的微服务解决方案,包括
配置管理、注册中心、负载均衡、限流、网关等。

Spring的常用注解

bean,controller,restcontroller,requestbody,responsebody,service,autowire,recourse,requestmapping(get,post,put),requestparam,param,Component,componentscan,value,configuration,Transactional

Springboot注解

springbootapplication,configurationproperties,EnableAutoConfiguration,autoconfiguration,enablediscoveryclient,enablefeignclients,feignclient,SpringBootConfiguration

Springboot自动装配

Springboot项目在启动的时候,因为我们在启动类上添加了@springbootapplication注解,这个注解内部封装了三个核心注解,第一个是@springbootconfiguration,他申明了boot项目在启动的时候自动装配的功能,第二个是@enableautoconfiguration,他内部封装了boot项目启动时的自动装配内容,底层通过java的反射调用meta-info中的spring-factories文件中的具体配置信息,在项目启动时,就将部分内容注入到spring ioc容器中,当然一些不常用的他只是做了内部配置,需要使用的时候自己通过注解调用就行,第三个注解是@componentscan,这个注解通过映射内部的配置路劲,去自动扫描,个别需要手动配置的路劲,在项目启动时,开发者额外申明即可。
另外main方法中调用了springapplication的run方法,调用时将当前类的class传给了boot底层,因为考虑多进程问题,将传入的class封装到了一个数组当中,然后底层有一个类叫stopwatch,确定我们当前传如的springapplication没有启动的情况下,调用start方法启动该boot项目,启动方法中对启动项目的标识名称和启动时间做了记录。

Nacos

因为我们最近这个项目使用的时boot+cloud,注册中心这块使用的是nanos,就类似于dubbo框架里边,咱们一般用的zk,主要实现微服务架构下,各模块的相互访问,另外也用到了他配置中心的功能,因为咱们项目启动的时候会读取配置信息,而很多配置的话是动态的,比如环境切换,正常来讲当我们修改配置后需要重启服务,对于正式环境来讲,重新打包重启服务器的代价是相当高的,所以通过配置中心,我们可以再客户端修改对应配置后,将新的配置自动同步到已部署好的代码当中,避免了重新部署、重启等问题(通过是通过refreshscope注解实现的,底层使用代理模式使用反射动态更新)
p r e f i x − {prefix}-prefix−{spring.profiles.active}.${file-extension} serverId-dev.yaml
支持配置信息的持久化,支持集群。

Ribbon(负载均衡)

之前项目上负载均衡这块儿用过ribbon,ribbon负载均衡的策略总共有七种,之前我们主要通过三种策略来实现,轮询、随机、权重,默认使用的是轮询。
轮询底层每次记录当前服务的位置,当有新的任务分配的时候,以当前位置加一作为即将接受任务的角标,然后根据该角标从集群服务的列表中拿到对应的服务,然后将这个任务分配给该服务。
随机其实就是拿到一个不超过集群数量的随机值,然后使用该随机值作为下标去集群服务的列表中获取对应的服务,然后将当前任务交给他执行(这种算法策略在少量请求时,不能做到均衡分配,但如果请求量大的时候,每台集群的服务器接受的任务趋于均衡)
权重策略在初始化的时候会启动一个定时任务,每隔30S重新计算一次,具体的计算方式就是通过集群服务器的平均响应时间减去当前服务器的响应时间,作为分配给当前服务器的权重比例(当前服务器压力越大,响应越慢,响应时间越长,算出来的结果越小,权重越低)。

Feign的工作原理
启动类添加@EnableFeignClients注解,Spring会扫描标记了@FeignClient注解的接口,并生成此接口的代理对象,
@FeignClient内部指定了被调用服务的标识,将接口注入到Spring IOC容器中,Feign会从注册中心获取到服务标识,当消费者访问时,会根据请求路劲,识别注册中心服务对应的接口。
网络调用的时候,通过代理模式为每个接口方法创建一个RequestTemplate,而requsttemplate内部封装了httpurlconnection,httpclient,okhttp等五种请求方式,调用时可以根据yml配置,指定对应的请求方式。

Spring Cloud项目中用到哪些组件?

Nacos:注册中心,配置中心
Ribbon:负载均衡
Feign:远程调用
Sentinel:限流,熔断降级,服务容错
Gateway:网关(路由,过滤,限流)

Feign的工作原理?

启动类添加@EnableFeignClients注解,Spring会扫描标记了@FeignClient注解的接口,并生成此接口的代理对象,
@FeignClient内部指定了被调用服务的标识标,将接口注入到Spring IOC容器中,Feign会从注册中心获取到服务标识,当消费者访问时,会根据请求路劲,识别注册中心服务对应的接口。
网络调用的时候,通过代理模式为每个接口方法创建一个RequestTemplate,而requsttemplate内部封装了httpurlconnection,httpclient,okhttp等五种请求方式,调用时可以根据yml配置,指定对应的请求方式。
Gateway的作用?
路由:请求时,所有请求先回通过网关的配置路劲访问到网关,然后网关经过校验,将任务分配给具体的服务。
过滤:每次网络请求执行到网关的时候,一般过滤request对象携带的参数、头信息、cookie,根据前后端的校验规则,来确认是否是本应用客户端发起的请求,从而决定是否放行。
限流:gateway有限流的功能,但我们公司限流用的是sentinel,gateway的限流主要通过令牌桶算法来实现,说白了就是在一个容器内,我们设定了一个定时写入令牌的规则,当请求发起时,必须携带一个令牌,后台校验令牌通过才可以访问,如果没有令牌或者是无效的令牌,就拒绝访问,因为容器设置了最大令牌的限量,如果桶满,新生成的令牌会被丢弃,这样如果同一时间大量请求来临,只有少部分请求能获取到令牌,让其余没有令牌的请求原路返回,以此实现限流的功能。

服务部署

说下Linux常用命令

cd:进入某个目录
pwd:查看当前目录
ls:查看当前目录下有哪些文件
mkdir:创建一个目录
cp:复制文件
mv: 文件移动
rm -rf:递归强制删除
tar -xzvf:解压缩
tail -200f 文件名:动态输出日志最后200行
kill -9 进程pid:强制杀死进程
ps -ef|grep tomcat:查看tomcat进程pid

持续更新…
今天的分享就到这里啦,若本文对你有所帮助,请点赞,收藏,就是对我最大的支持。

你可能感兴趣的:(笔记,面试,java,jvm,mybatis,spring,cloud)