面试题
1、说一下ArrayList和LinkedList区别
- 首先,他们的底层数据结构不同,ArrayList底层是 基于数组 实现的,LinkedList底层是 基于链表 实现的。
- 由于底层数据结构不同,他们所适用的场景也不同,AayList更适合随机查找,Linkedlist更适合删除和添加,查询、添加、删除的时间复杂度不同。=
- 另外Arraylist和Linkedist 都实现了List接口,但是LinkedList还额外实现了 Deque接口 ,所以LinkedList还可以当做队列来使用。
其实,ArrayList 和 LinkedList 并没有严格意义上的谁好谁坏,具体情况得具体分析。
1、如果是指定下标查询查询的情况下
ArrayList的速度要快于LinkedList的速度,直接使用下标索引即可;但查询首尾两个元素,Linkedlist的速度也很快,因为LinkedList存在两个属性,记录首尾元素。
2、添加元素
ArrayList添加一个元素可能会涉及到数组扩容,以及元素的移动。
LinkedList添加一个元素直接插入到链表最后;如果是添加到到指定位置则会涉及到链表的遍历,速度较慢。
2、说一下HashMap的Put方法
先说HashMap的Put方法的大体流程:
- 根据Key通过哈希算法(hashcode)与与运算得出数组下标。
- 如果数组下标位置元素为空,则将key和value封装为Entry对象、(JDK1.7中是Entry对象,JDK1.8中是Node对象)并放入该位置。
- 如果数组下标位置元素不为空,则要分情况讨论:
- 如果是JDK1.7,则先判断是否需要扩容,如果要扩容就进行扩容,如果不用扩容就生成Entry对象,并使用头插法添加到当前位
置的键表中。
- 如果是JDK1.8,则会先判断当前位置上的Node的类型,看是红黑树Node,还是链表Node,
- 如果是红黑树Node,则将key和value封装为一个红黑树节点并添加到红黑树中去,在这个过程中会判断红黑树中是否存在当前key,如果存在则更新value;
- 如果此位置上的Node对象是链表节点,则将key和value封装为一个链表Node并通过尾插法插入到链表的最后位置去,因为是尾插法,所以需要遍历链表,在遍历链表的过程中会判断是否存在当前key,如果存在则更新value,当遍历完链表后,将新链表Node插入到链表中插入到链表后,会看当前链表的节点个数,如果超过了8,那么则会将该链表转成红黑树;
- 将key和value封装位Node插入到链表或红黑树中后,再判断是否需要进行扩容,如果需要就扩容,如果不需要就介绍PUT方
法。
3、ThreadLocal
ThreadLocal可以实现 【资源对象】的线程隔离 ,让每个线程各用各的【资源对象】,避免争用引发的线程安全问题。ThreadLocal同时实现了 线程内的资源共享 。
- ThreadLocal是Java中所提供的线程本地存储机制,可以利用该机制将数据缓存在某个线程内部,该线程可以在任意时刻任意方法中获取缓存的数据。
- ThreadLocal底层是通过 ThreadLocalMap 来实现的,每个Thread对象 (注意不是ThreadLocal对象)中都存在一个ThreadLocalMap ,Map的key为ThreadLocal对象,Map的value为需要缓存的值。
- 如果在线程池中使用ThreadLocal会造成内存泄漏,因为当Threadlocal对象使用完之后,应该要把设置的key和value,也就是Entry对象进行回收,但线程池中的线程不会回收,而线程对象是通过强引用指向ThreadLocalMap,ThreadLocalMap也是通过强引用指向Enty对象,线程不被回收,Enty对象也就不会被回收,从而出现内存泄漏,解决办法是,在使用了ThreadLocal对象之后,手动调用ThreadLocal的remove方法,手动清楚Entry对象。
- ThreadLocal经典的应用场景就是连接管理(一个线程持有一个连接,该连接对象可以在不同的方法之间进行传递,线程之间不共享同一个连接)。
其原理是,每个线程内有一个ThreadLocalMap类型的成员变量,用来存储资源对象。
①调用 set方法,就是以ThreadLocal自己作为key,资源对象作为value,放入当前线程的 ThreadLocalMap集合中。
②调用 get 方法,就是以ThreadLocal自己作为key,到当前线程中查找关联的资源值。
③调用 remove方法,就是以ThreadLocal自己作为key,移除当前线程关联的资源值。
为什么ThreadLocalMap中的key (即ThreadLocal )要设计为弱引用?
① Thread可能需要长时间运行(如线程池中的线程),如果key不再使用,需要在内存不足(GC)时释放其占用的内存。
② 但GC仅是让key的内存释放,后续还要根据key是否为null来进一步释放值的内存,释放时机有:
- 获取key 发现null key 。
- set key时,会使用启发式扫描,清除临近的null key,启发次数与元素个数,是否发现 null key有关。
- remove时(推荐),因为一般使用ThreadLocal时都把它作为静态变量,因此GC无法回收。
4、说一下JVM中,哪些是共享区,哪些可以作为gc root?
堆和方法区是所有线程共享的;栈、本地方法栈、程序计数器是每个线程独有的。
Q:什么是gc root?
JVM在进行垃圾回收时,需要找到垃圾”对象,也就是没有被引用的对象,但是直接找“垃圾”对象是比较耗时的,所以反过来,先找“非垃圾”对象,也就是正常对象,那么就需要从某些“根”开始去找,根据这些“根”的引用路径找到正常对象,而这些“根”有一个特征,就是它 只会引用其他对象,而不会被其他对象引用,例如:栈中的本地变量、方法区中的静态变量、本地方法栈中的变量、正在运行的线程等可以作为gc root。
5、如何排查项目中遇到的JVM问题?
对于还在正常运行的系统:
- 可以使用 jmap 来查看JVM中各个区域的使用情况;
- 可以通过 jstack 来查看线程的运行情况,比如哪些线程阻塞、是否出现了死锁;
- 可以通过 jstat 命令来查看垃圾回收的情况,特别是fullgc,如果发现fullgc比较频繁,那么就得进行调优了;
- 通过各个命令的结果,或者 jvisualvm 等工具来进行分析;
- 首先,初步猜测频繁发送fullgc的原因,如果频繁发生fullgc但是又一直没有出现内存,那么表示fullgc实际上是回收了很多对象了,所以这些对象最好能在年轻代垃圾回收过程中就直接回收掉,避免这些对象进入到老年代,对于这种情况,就要考虑这些存活时间不长的对象是不是比较大,导致年轻代放不下,直接进入到了老年代,尝试加大年轻代的大小,如果改完之后,fullgc减少,则证明修改有效;
- 同时,还可以找到占用CPU最多的线程,定位到具体的方法,优化这个方法的执行,看是否能避免某些对象的创建,从而节省内存。
对于已经发生了00M的系统:
- 一般生产系统中都会设置当系统发生了00M时,生成当时的dump文件((-XX (+HeapDumpOnOutOfMemory)Error -XHeapDumpPath=/usr/loca/base);
- 我们可以利用jsisualvm等工具来分析dump文件;
- 根据dump文件找到异常的实例对象,和异常的线程(占用CPU高),定位到具体的代码;
- 然后再进行详细的分析和调试。
总之,调优不是一蹴而就的,需要分析、推理、实践、总结、再分析,最终定位到具体的问题。
6、如何查看线程死锁?
- 可以通过jstack 命令来进行查看,jstack命令中会显示发生了死锁的线程;
- 或者两个线程去操作数据库时,数据库发生了死锁,这是可以查询数据库的死锁情况。
7、线程之间如何进行通讯的?
- 线程之间可以通过 共享内存或基于网络 来进行通信;
- 如果是通过共享内存来进行通信,则需要考虑并发问题,什么时候阻塞,什么时候唤醒;
- 像Java中的wait(. notify0就是阻塞和唤醒;
- 通过网络就比较简单了,通过网络连接将通信数据发送给对方,当然也要考虑到并发问题,处理方式就是加锁等方式。
8、介绍一下Spring,读过源码介绍一下大致流程
Spring两大核心机制: IoC(工厂模式)和AOP(代理模式)
- Spring是一个快速开发框架,Spring帮助程序员来管理对象;
- Spring的源码实现的是非常优秀的,设计模式的应用、并发安全的实现、面向接口的设计等;
- 在创建Spring容器,也就是启动Spring时:
a.首先会进行扫描,扫描得到所有的BeanDefinition对象,并存在一个Map中;
b.然后筛选出非懒加载的单例BeanDefnition进行创建Bean,对于多例Bean不需要在启动过程中去进行创建,对于多例Bean会在每次获取Bean时利用BeanDefmntion去创建;
c,利用BeanDefinition创建Bean就是Bean的创建生命周期,这期间包括了合并BeanDefinition、推断构造方法、实例化、属性填充.初始化前、初始化、初始化后等步骤,其中AOP(代理模式)就是发生在初始化后这一步骤中;
- 单例Bean创建完了之后,Spring会发布一个容器启动事件;
- Spring启动结束;
- 在源码中会更复杂,比如源码中会提供一些模板方法,让子类来实现,比如源码中还涉及到一些BeanFactoryPotProcesso和BeanPostProceso的注册,Spring的扫描就是通过BenafactoryPostProcessor来实现的,依赖注入就是通过BeanPostProcessor来实现的;
- 在spring启动过程中还会去处理@Import等注解。
9、Spring的事务机制
- Spring事务底层是 基于数据库事务和AOP机制的;
- 首先对于使用了@Transactional注解的Bean,Spring会创建一个代理对象作为Bean;
- 当调用代理对象的方法时,会先判断该方法上是否加了@Transactional注解;
- 如果加了,那么则利用事努管理器创建一个数据库连接;
- 并且修改数据库连接的autocommit属性为false,禁止此连接的自动提交,这是实现Spring事务非常重要的一步;
- 然后执行当前方法,方法中会执行sql;
- 执行完当前方法后,如果没有出现异常就直接提交事务;
- 如果出现了异常,并且这个异常是需要回滚的就会回滚事务,否则仍然提交事务;
- Spring事务的隔离级别对应的就是 数据库的隔离级别;
- Spring事务的传播机制是Spring事务自己实现的,也是Spring事务中最复杂的;
- ping事务的传播机制是基于数据库连接来做的,一个数据库连接一个事务,如果传播机制配置为需要新开一个事务,那么实际上就是先建立一个数据库连接,在此新数据库连接上执行sql。
10、@Transactional 什么时候失效?
因为 Spring事务是基于代理来实现的 ,所以某个加了@Transactonal的方法只有是被代理对象调用时,那么这个注解才会生效,所以如果是被代理对象来调用这个方法,那么@Transactional是不会生效的。
同时如果某个方法是provite的,那么@Transactional也会关效,因为底层cglib是基于父子类来实现的。子类是不能重载父类的private方法的,所以无法很好的利用代理,也会导致@Transactianal失效。
11、Dubbo是如何做系统交互的?
Dubbo底层是通过RPC来完成服务和服务之间的调用的,Dubbo支持很多协议,比如默认的dubbo协议,比如http协议、比如rest等都是支持的,他们的底层所使用的技术是不太一样的,比如dubbo协议底层使用的是netty,也可以使用mina,http协议底层使用的tomcat或jetty。
服务消费者在调用某个服务时,会将当前所调用的服务接口信息、当前方法信息、执行方法所传入的入参信息等组装为一个Invocation对象,然后不同的协议通过不同的数据组织方式和传输方式将这个对象传送给服务提供者,提供者接收到这个对象后,找到对应的服务实现,利用反射执行对应的方法,得到方法结果后再通过网络响应给服务消费者。
当然,Dubbo在这个调用过程中还做很多其他的设计,比如服务容错、负载均衡、Filter机制、动态路由机制等等,让Dubbo能处理更多企业中的需求。
12、Dubbo的负载均衡策略
Dubbo目前支持:
- 平衡加权轮询算法
- 加权随机算法
- —致性哈希算法
- 最小活跃数算法
13、Jdk1.7到Jdk1.8 HashMap发生了什么变化(底层)?
- 1.7中底层是 数组+链表,1.8中底层是 数组+链表+红黑树,加红黑树的目的是提高HashMap插入和查询整体效率;
- 1.7中链表插入使用的是 头插法,1.8中链表插入使用的是尾插法,因为1.8中插入key和value时需要判断链表元素个数,所以需要遍历链表统计链表元素个数,所以正好就直接使用尾插法;
- 1.7中哈希算法比较复杂,存在各种右移与异或运算,1.8中进行了简化,因为负载的哈希算法的目的就是提高散列性,来提供HashMap的整体效率,而1.8中新增了红黑树,所以可以适当的简化哈希算法,节省CPU资源。
14、Jdk1.7到Jdk1 .8 java虚拟机发生了什么变化?
1.7中存在永久代,1.8中没有永久代,替换它的是元空间,元空间所占的内存不是在虚拟机内部,而是本地内存空间,这么做的原因是,不管是永久代还是元空间,他们都是方法区的具体实现,之所以元空间所占的内存改成本地内存,官方的说法是为了和Rockit统一,不过额外还有一些原因,比如方法区所存储的类信息通常是比较难确定的,所以对于方法区的大小是比较难指定的,太小了容易出现方法区溢出,太大了又会占用了太多虚拟机的内存空间,而转移到本地内存后则不会影响虚拟机所占用的内存。
15、如何实现AOP,你在哪里用到了AOP?
利用 动态代理 技术来实现AOP,比如JDK动态代理或Cglib动态代理,利用动态代理技术,可以针对某个类生成代理对象,当调用代理对象的某个方法时,可以任意控制该方法的执行,比如可以先打印执行时间,再执行该方法,并且该方法执行完成后,再次打印执行时间。
项目中,比如事务、权限控制、方法执行时长日志都是通过AOP技术来实现的,凡是需要对某些方法做统一处理的都可以用AOP来实现,利用AOP可以做到业务无侵入。
16、Spring中后置处理器的作用?
Spring中的后置处理器分为Beanfactony后置处理器和Bean后置处理器,它们是Spring底层源码架构设计中非常重要的一种机制,同时开发者也可以利用这两种后置处理器来进行扩展。BeanFacory后置处理器表示针对BeanFactory的处理器,spring启动过程中,会先创建出Beanfactoy实例,然后利用BeanFacory处理器来加工Beanfactary,比如Spring的扫描就是基于Beanfactony后置处理器来实现的,而Bean后置处理器也类似,Spring在创建一个Bean的过程中,首先会实例化得到一个对象,然后再利用Bean后置处理器来对该实例对象进行加工,比如我们常说的依赖注入就是基于一个Bean后置处理器来实现的,通过该Bean后置处理器来给实例对象中加了 @Autowired 注解的属性自动赋值,还比如我们常说的AOP,也是利用一个Bean后置处理器来实现的,基于原实例对象,判断是否需要进行AOP,如果需要,那么就基于原实例对象进行动态代理,生成一个代理对象。
17、SpringBoot注解,及其实现
- @SpringBootApplication注解:这个注解标识了一个SpringBoot工程,它实际上是另外三个注解的组合,这三个注解是:
@SpringBootConfiguration: 这个注解实际就是一个@Configuration,表示启动类也是一个配置类;
@CEnableAutoConfiguration: 向Spring容器中导入了一个Selector,用来加载(ClassPath下SpringFactories中所定义的自动配置类,将这些自动加载为配置Bean;
@ComponentScan: 标识扫描路径,因为默认是没有配置实际扫描路径,所以SpringBoot扫描的路径是启动类所在的当前目录;
- @Bean注解:用来定义Bean,类似于XML中的标签,Spring在启动时,会对加了@Bean注解的方法进行解析,将方法的名字做为beanName,并通过执行方法得到bean对象;
- @Controller、@Service、@ResponseBody、@Autowired都可以说。
Q:SpringBoot中配置文件的加载顺序是怎样的?
优先级从高到低,高优先级的配置覆盖低优先级的配置,所有配置会形成互补配置。
- 命令行参数。所有的配置都可以在命令行上进行指定;
- Java系统属性(System.getProperties0) ;
- 操作系统环境变量;
- jar包外部的application-fprofile}.properties或application.yml(带spring.profile)配置文件;
- jar包内部的application-fprofile}.properties或application.yml(带spring.profile)配置文件再来加载不带profile;
- jar包外部的application.properties或application.yml(不带spring.profile)配置文件;
- jar包内部的application.properties或application.yml(不带spring.profile)配置文件;8. @Configuration注解类上的@PropertySource。
18、分布式锁实现?
分布式锁所要解决的问题的本质是:能够对分布在多台机器中的线程对共享资源的互斥访问。在这个原理上可以有很多的实现方式:
- 基于Mysql,分布式环境中的线程连接同一个数据库,利用数据库中的行锁来达到互斥访问,但是Mysql的加锁和释放锁的性能会比较低,不适合真正的实际生产环境;
- 基于Zookeeper,Zookeeper中的数据是存在内存的,所以相对于Mysql性能上是适合实际环境的,并且基于Zookeeper的顺序节点、临时节点. Watch机制能非常好的来实现的分布式锁;
- 基于Redis,Redis中的数据也是在内存,基于Redis的消费订阅功能、数据超时时间,lua脚本等功能,也能很好的实现的分布式锁。
19、Redis的数据结构及使用场景
Redis的数据结构有:
- 字符串: 可以用来做最简单的数据缓存,可以缓存某个简单的字符串,也可以缓存某个yson格式的字符串,Redis分布式锁的实现就利用了这种数据结构,还包括可以实现计数器、Session共享、分布式ID;
- 哈希表: 可以用来存储一些key-value对,更适合用来存储对象;
- 列表: Redis的列表通过命令的组合,既可以当做栈,也可以当做队列来使用,可以用来缓存类似微信公众号、微博等消息流数据;
- 集合: 和列表类似,也可以存储多个元素,但是不能重复,集合可以进行交集、并集、差集操作,从而可以实现类似,我和某人共同关注的人、朋友圈点赞等功能;
- 有序集合: 集合是无序的,有序集合可以设置顺序,可以用来实现排行榜功能。
20、Redis集群策略
Redis提供了三种集群策略:
- 主从模式: 这种模式比较简单,主库可以读写,并且会和从库进行数据同步,这种模式下,客户端直接连主库或某个从库,但是当主库或从库宕机后,客户端需要手动修改IP,另外,这种模式也比较难进行扩容,整个集群所能存储的数据受到某台机器的内存容量,所以不可能支持特大数据量;
- 哨兵模式: 这种模式在主从的基础上新增了哨兵节点,但主库节点宕机后,哨兵会发现主库节点宕机,然后在从库中选择一个库作为进的主库,另外哨兵也可以做集群,从而可以保证但某一个哨兵节点宕机后,还有其他哨兵节点可以继续工作,这种模式可以比较好的保证Redis集群的高可用,但是仍然不能很好的解决Redis的容量上限问题。
- Cluster模式: Cluster模式是用得比较多的模式,它支持多主多从,这种模式会按照key进行槽位的分配,可以使得不同的key分散到不同的主节点上,利用这种模式可以使得整个集群支持更大的数据容量,同时每个主节点可以拥有自己的多个从节点,如果该主节点宕机,会从它的从节点中选举一个新的主节点。
对于这三种模式,如果Redis要存的数据量不大,可以选择哨兵模式,如果Redis要存的数据量大,并且需要持续的扩容,那么选择Cluster模式。
21、Mysql数据库中,什么情况下设置了索引但无法使用?
- 没有符合最左前缀原则。
- 字段进行了隐式数据类型转化。
- 走索引没有全表扫描效率高。
22、Innodb是如何实现事务的?
Innodb通过Buffer Pool,LogBuffer,Redo Log,Undo Log来实现事务,以一个update语句为例:
- Innodb在收到一个update语句后,会先根据条件找到数据所在的页,并将该页缓存在Buffer Pool中;
- 执行update语句,修改Buffer Pool中的数据,也就是内存中的数据;
- 针对update语句生成一个RedoLog对象,并存入LogBuffer中;
- 针对update语句生成undolog日志,用于事务回滚;
- 如果事务提交,那么则把RedoLog对象进行持久化,后续还有其他机制将Buffer Pool中所修改的数据页持久化到磁盘中;6、如果事务回滚,则利用undolog日志进行回滚。
23、遇到过哪些设计模式?
在学习一些框架或中间件的底层源码的时候遇到过一些设计模式:
- 代理模式: Mybatis中用到JDK动态代理来生成Mapper的代理对象,在执行代理对象的方法时会去执行SQL,Spring中AOP、包括Configuration注解的底层实现也都用到了代理模式;
- 责任链模式: Tomcat中的Pipeline实现,以及Dubbo中的Filter机制都使用了麦任链模式;
- 工厂模式: Spring中的BeanFactory就是一种工厂模式的实现;
- 适配器模式: Spring中的Bean销毁的生命周期中用到了适配器模式,用来适配各种Bean销毁逻辑的执行方式;
- 外观模式: Tomcat中的Request和RequestFacade之间体现的就是外观模式;
- 模板方法模式: Spring中的refresh方法中就提供了给子类继承重写的方法,就用到了模板方法模式。
24、Java死锁如何避免?
造成死锁的几个原因:
- 一个资源每次只能被一个线程使用;
- 一个线程在阻塞等待某个资源时,不释放已占有资源;
- —个线程已经获得的资源,在未使用完之前,不能被强行剥夺;
- 若千线程形成头尾相接的循环等待资源关系;
这是造成死锁必须要达到的4个条件,如果要避免死锁,只需要不满足其中某一个条件即可。而其中前3个条件是作为锁要符合的条件,所以要避免死锁就需要打破第4个条件,不出现循环等待锁的关系。
在开发过程中:
- 要注意加锁顺序,保证每个线程按同样的顺序进行加锁;
- 要注意加锁时限,可以针对锁设置一个超时时间;
- 要注意死锁检查,这是—种预防机制,确保在第一时间发现死锁并进行解决。
25、深拷贝和浅拷贝
深拷贝和浅拷贝就是指对象的拷贝,一个对象中存在两种类型的属性,一种是基本数据类型,一种是实例对象的引用。
- 浅拷贝 是指,只会拷贝基本数据类型的值,以及实例对象的引用地址,并不会复制一份引用地址所指向的对象,也就是浅拷贝出来的对象,内部的类属性指向的是同一个对象(同一个对象)。
2、深拷贝 是指,既会拷贝基本数据类型的值,也会针对实例对象的引用地址所指向的对象进行复制,深拷贝出来的对象,内部的类执行指向的不是同一个对象(两个不同的对象)。
26、提交任务时,线程池队列已满,这时会发生什么?
- 如果使用的无界队列,那么可以继续提交任努时没关系的;
- 如果使用的有界队列,提交任努时,如果队列满了,如果核心线程数没有达到上限,那么则增加线程,如果线程数已经达到了最大值,则使用拒绝策略进行拒绝。
27、concurrentHashMap的扩容机制
1.7版本
- 1.7版本的ConcurrentHashMap是基于Segment分段实现的;
- 每个Segment相对于一个小型的HashMap;
- 每个Segment内部会进行扩容,和HashMap的扩容逻辑类似;
- 先生成新的数组,然后转移元素到新数组中;
- 扩容的判断也是每个Segment内部单独判断的,判断是否超过阈值。
1.8版本
- 1.8版本的ConcurrentHashMap不再基于Segment实现;
- 当某个线程进行put时,如果发现ConcurrentHashMap正在进行扩容那么该线程一起进行扩容;
- 如果某个线程put时,发现没有正在进行扩容,则将key-value添加到ConcurrentHashMap中,然后判断是否超过阈值,超过了则进行扩容;
- ConcurrentHashMap是支持多个线程同时扩容的;
- 扩容之前也先生成一个新的数组;
- 在转移元素时,先将原数组分组,将每组分给不同的线程来进行元素的转移,每个线程负责一组或多组的元素转移工作。
28、Spring中Bean是线程安全的吗?
Spring本身并没有针对Bean做线程安全的处理,所以:
- 如果Bean是无状态的,那么Bean则是线程安全的;
- 如果Bean是有状态的,那么Bean则不是线程安全的。
另外,Bean是不是线程安全,跟Bean的作用域没有关系,Bean的作用域只是表示Bean的生命周期范围,对于任何生命周期的Bean都是一个对象,这个对象是不是线程安全的,还是得看这个Bean对象本身。
29、Linux中常用的命令
参加前面内容 【Linux常用命令】
30、Maven中Package和Install的区别
- Package是打包,打成JareWar;
- Install表示将Jar或war安装到本地仓库中。
31、Springcloud各组件功能,与Dubbo的区别
- Eureka: 注册中心,用来进行服务的自动注册和发现。
- Ribbon: 负载均衡组件,用来在消费者调用服务时进行负载均衡。
- Feign: 基于接口的申明式的服务调用客户端,让调用变得更简单。
- Hystrix: 断路器,负责服务容错。
- Zuul: 服务网关,可以进行服务路由、服务降级.负载均衡等。
- Nacos: 分布式配置中心以及注册中心。
- Sentinel: 服务的熔断降级,包括限流。
- Seata: 分布式事务。
- **Spring Cloud Config:**分布式配置中心。
- Spring Cloud Bus: 消息总线。
Spring Cloud是一个微服务框架,提供了微服务领域中的很多功能组件,Dubo一开始是一个RPC调用框架,核心是解决服务调用间的问题,Ssping Cloud是一个大而全的框架,Dubo则更则重于服务调用,所以Dubo所提供的功能没有Sspring Cloud全面,但是Dubbo的服务调用性能比Spring Cloud高,不过Spring Cloud和Dubo并不是对立的,是可以结合起来一起使用的。
32、类加载器双亲委派模型
JVM中存在三个默认的类加载器:
- BootstrapClassLoader
- ExtClassLoaHer
- AppClassLoader
AppClassLoader的父加载器是ExtClassLoader,ExtClassLoader的父加裁器是BootstrapClassLoader。
JVM在加载一个类时,会调用AppClassloader的loadClass方法来加载这个类,不过在这个方法中,会先使用ExtClassLoader的loadClss方法来加载类,同样ExtClassLoader的loadClass方法中会先使用BootstrapClassloader来加载类,如果BootstrapClassLoader和加数到了就直接成功,如果BootstrapClassloader没有加载到,那么ExtClassloader就会自己尝试加载该类,如果没有加载到,那么则会由AppClassLoader来加载这个类。
所以,双亲委派指得是,JVM在加载类时,会委派给Ext和Bootstrap进行加载,如果没加载到才由自己进行加载。
33、泛型中extends和super的区别
- extends T> 表示包括T在内的任何T的子类;
- super T> 表示包括T在内的任何T的父类。
34、并发编程三要素?
- 原子性: 不可分割的操作,多个步骤要保证同时成功或同时失败;
- 有序性: 程序执行的顺序和代码的顺序保持—致;
- 可用性: —个线程对共享变量的修改,另一个线程能立马看到。
35、CAP理论
CAP理论是分布式领域非常重要的一个理论,很多分布式中间件在实现时都需要遵守这个理论,其中:
- C表示一致性: 指的的是分布式系统中的数据的—致性。
- A表示可用性: 表示分布式系统是否正常可用。
- P表示分区容器性: 表示分布式系统出现网络问题时的容错性。
CAP理论是指,在分布式系统中不能同时保证C和A,也就是说在分布式系统中要么保证CP,要么保证AP,也就是一致性和可用性只能取其一,如果想要数据的一致性,那么就需要损失系统的可用性,如果需要系统高可用,那么就要损失系统的数据一致性,特指强一致性。
CAP理论太过严格,在实际生产环境中更多的是使用BASE理论,BASE理论是指分布式系统不需要保证数据的强一致,只要做到最终一致,也不需要保证一直可用,保证基本可用即可。
Q: 什么是BASE理论
由于不能同时满足CAP,所以出现了BASE理论:
- BA:Basically Available,表示基本可用,表示可以允许一定程度的不可用,比如由于系统故障,请求时间变长,或者由于系统故障导致部分非核心功能不可用,都是允许的。
- S:Soft state:表示分布式系统可以处于一种中间状态,比如数据正在同步。
- E:Eventually consistent,表示最终一致性,不要求分布式系统数据实时达到一致,允许在经过一段时间后再达到一致,在达到一致过程中,系统也是可用的。
36、图的深度遍历和广度遍历
图的深度优先遍历 是指,从一个节点出发,一直沿着边向下深入去找节点,如果找不到了则返回上一层找其他节点。
图的广度优先遍历 是指,从一个阶段出发,向下先把第一层的节点遍历完,再去遍历第二层的阶段,直到遍历到最后一层。
37、快排算法
快速排序算法底层采用了 分治法 。
基本思想是:
- 先取出数列中的第一个数作为基准数。
- 将数列中比基准数大的数全部放在它的右边,比基准数小的数全部放在它的左边。
- 然后在对左右两部分重复第二步,直到各区间只有一个数。
38、TCP的三次握手和四次挥手
TCP协议是7层网络协议中的传输层协议,负责数据的可靠传输。
在建立TCP连接时,需要通过三次握手来建立, 过程是:
- 客户端向服努端发送一个SYN;
- 服务端接收到SYN后,给客户端发送一个SYN_ACK ;
- 客户端接收到SYN_ACK后,再给服务端发送一个ACK。
在断开TCP连接时,需要通过四次挥手来端口,过程是:
- 客户端向服务端发送FIN ;
- 服务端接收FIN后,向客户端发送ACK,表示我接收到了断开连接的请求,客户端你可以不发数据了,不过服务端这边可能还有数据正在处理;
- 服务端处理完所有数据后,向客户端发送FIN,表示服务端现在可以断开连接;
- 客户端收到服努端的FIN,向服务端发送ACK,表示客户端也会断开连接了。
39、消息队列如何保证消息可靠传输
消息可靠传输代表了两层意思,既不能多也不能少。
-
为了保证消息不多,也就是消息不能重复,也就是生产者不能重复生产消息,或者消费者不能重复消费消息。
**a.**首先要确保消息不多发,这个不常出现,也比较难控制,因为如果出现了多发,很大的原因是生产者自己的原因,如果要避免出
现问题,就需要在消费端做控制;
**b.**要避免不重复消费,最保险的机制就是消费者实现幂等性,保征就算重复消费,也不会有问题,通过幂等性,也能解决生产者重
复发送消息的问题。
-
消息不能少,意思就是消息不能丢失,生产者发送的消息,消费者一定要能消费到,对于这个问题,就要考虑两个方面。
**a.**生产者发送消息时,要确认Abroker确实收到并持久化了这条消息,比如RabbitMQ的confim机制,Kafka的ack机制都可以保证
生产者能正确的将消息发送给broker ;
b. broker要等待消费者真正确认消费到了消息时才删除掉消息,这里通常就是消费端ack机制,消费者接收到一条消息后,如果确
认没问题了,就可以给broker发送一个ack,broker接收到ack后才会删除消息。
40、二叉搜索树和平衡二叉树有什么关系?
平衡二叉树也叫做平衡二叉搜索树,是二叉搜索树的升级版,二叉搜索树是指节点左边的所有节点都比该节点小,节点右边的节点都比该节点大,而平衡二叉搜索树是在二叉搜索的基础上还规定了节点左右两边的子树高度差的绝对值不能超过1。
41、强平衡二叉树和弱平衡二叉树有什么区别?
强平衡二叉树AVL树,弱平衡二叉树就是我们说的红黑树。
- AVL树比红黑树对于平衡的程度更加严格,在相同节点的情况下,AVL树的高度低于红黑树;
- 红黑树中增加了一个节点颜色的概念;
- AVL树的旋转操作比红黑树的旋转操作更耗时。
42、B树和B+树的区别,为什么Mysql使用B+树?
B树的特点:
- 节点排序;
- —个节点可以存多个元素,多个元素也排序了。
B+树的特点:
- 拥有B树的特点;
- 叶子节点之间有指针;
- 非叶子节点上的元素在叶子节点上都冗余了,也就是叶子节点中存储了所有的元素,并且是排好顺序的。
Mysql索引使用的是B+树,因为索引是用来加快查询的,而B+树通过时数据进行排序所以是可以提高查询速度的,然后通过一个节点中可以存储多个元素,从而可以使得B+树的高度不会太高,在Mysq中一个Innodb页就是一个B+树节点,一个Innodb页默认16kb,所以一般情况下一颗两层的B+树可以存200万行左右的数据,然后通过利用B+树叶子节点存储了所有数据并且进行了排序,并且叶子节点之间有指针,可以很好的支持全表扫描,范围查找等SQL语句。
43、epoll和poll的区别
- select模型,使用的是数组来存储Socket连接文件描述符,容量是固定的,需要通过轮询来判断是否发生了IO事件;
- poll模型,使用的是链表来存储Socket连接文件描述符,容量是不固定的,同样需要通过轮询来判断是否发生了I0事件;
- epoll模型,epoll和poll是完全不同的,epoll是一种事件通知模型,当发生了10事件时,应用程序才进行IO操作,不需要像poll模型那样主动去轮询。
44、线程池原理,FixedThreadPool用的阻塞队列是什么?
线程池内部是通过 队列+线程 实现的,当我们利用线程池执行任务时:
- 如果此时线程池中的数量 小于corePoolSize,即使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务。
- 如果此时线程池中的数量 等于corePoolSize,但是缓冲队列workQueue未满,那么任务被放入缓冲队列。
- 如果此时线程池中的数量 大于等于corePoolSize,缓冲队列workQueue满,并且线程池中的数量小于maximumPoolSize,则创建新的线程来处理被添加的任务。
- 如果此时线程池中的数量 大于corePoolSize,缓中队列workQueue满,并且线程池中的数量等于maximumPoolSize,那么通过handler所指定的策略来处理此任努。
- 当线程池中的线程数量 大于corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止。这样,线程池可以动态的调整池中的线程数。
FixedThreadPool代表定长线程池,底层用的LinkedBlockingQueue,表示无界的阻塞队列。
45、sychronized和ReentrantLock的区别?
- sychronized是一个关键字,ReentrantLock是一个类。
- sychronized会自动的加锁与释放锁,ReentrantLock需要程序员手动加锁与释放锁。
- sychronized的底层是JVM层面的锁,ReentrantLock是API层面的锁。
- sychronized是非公平锁,ReentrantLock可以选择公平锁或非公平锁。
- sychronized锁的是对象,锁信息保存在对象头中,ReentrantLock所的是线程,通过代码中int类型的state标识来标识锁的状态。
- sychronized底层有一个锁升级的过程。
ReentrantLock中的公平锁和非公平锁的底层实现
首先不管是公平锁和非公平锁,它们的底层实现都会使用AQS来进行排队,它们的区别在于: 线程在使用lock)方法加锁时,如果是 公平锁 ,会先检查AQS队列中是否存在线程在排队,如果有线程在排队,则当前线程也进行排队;如果是 非公平锁 ,则不会去检查是否有线程在排队,而是直接竞争锁。
不管是公平锁还是非公平锁,一旦没竞争到锁,都会进行排队,当锁释放时,都是唤醒排在最前面的线程,所以非平锁只是体现在了线程加锁阶段,而没有体现在线程被唤醒阶段。
另外,ReentrantLock是可重入锁,不管是公平锁还是非公平锁都是可重入的。
46、sychronized的自旋锁、偏向锁、轻量级锁、重量级锁
- 偏向锁: 在锁对象的对象头中记录一下当前获取到该锁的线程ID,该线程下次如果又来获取该锁就可以直接获取到了。
- 轻量级锁: 由偏向锁升级而来,当一个线程获取到锁后,此时这把锁是偏向锁,此时如果有第二个线程来竞争锁,偏向锁就会升级为轻量级锁,之所以叫轻量级锁,是为了和重量级锁区分开来,轻量级锁底层是通过自旋来实现的,并不会阻塞线程。
- 如果自旋次数过多仍然没有获取到锁,则会升级为重量级锁,重量级锁会导致线程阻塞。
- 自旋锁: 自旋锁就是线程在获取锁的过程中,不会去阻塞线程,也就无所谓唤醒线程,阻塞和唤醒这两个步骡都是需要操作系统去进行的,比较消耗时间,自旋锁是线程通过CAS获取预期的一个标记,如果没有获取到,则继续循环获取,如果获取到了则表示获取到了锁,这个过程线程一直在运行中,相对而言没有使用太多的操作系统资源,比较轻量。
47、HTTPS是如何保证安全传输的?
Https通过使用 对称加密、非对称加密、数字证书等 方式来保证数据的安全传输。
- 客户端向服务端发送数据之前,需要先建立TCP连接,所以需要先建立TCP连接,建立完TCP连接后,服务端会先给客户端发送公钥,客户端拿到公钥后就可以用来加密数据了,服务端到时候接收到数据就可以用私钥解密数据,这种就是通过非对称加密来传输数据。
- 不过非对称加密比对称加密要慢,所以不能直接使用非对称加密来传输请求数据,所以可以通过非对称加密的方式来传输对称加密的秘钥,之后就可以使用对称机加密来传输请求数据了。
- 但是仅仅通过非对称加密+对称加密还不足以能保证数据传输的绝对安全,因为服务端向客户端发送公钥时,可能会被截取。
- 所以为了安全的传输公钥,需要用到数字证书,数字证书是具有公信力、大家都认可的,服务端向客户端发送公钥时,可以把公钥和服务端相关信息通过Hash算法生成消息摘要,再通过数字证书提供的私钥对消息摘要进行加密生成数字签名,在把没进行Hash算法之前的信息和数字签名一起形成数字证书,最后把数字证书发送给客户端,客户端收到数字证书后,就会通过数字证书提供的公钥来解密数字证书,从而得到非对称加密要用到的公钥。
- 在这个过程中,就算有中间人拦截到服务端发出来的数字证书,虽然它可以解密得到非对称加密要使用的公钥,但是中间人是办法伪造数字证书发给客户端的,因为客户端上内嵌的数字证书是全球具有公信力的,某个网站如果要支持https,都是需要申请数字证书的私钥的,中间人如果要生成能被客户端解析的数字证书,也是要申请私钥的,所以是比较安全了。
48、设计模式的分类?
- 创建型
- 结构型
- 行为型
49、volatile关键字,它是如何保证可见性、有序性的?
在并发领域中,存在三大特性:原子性、有序性、可见性。volatile关键字用来修饰对象的属性,在并发环境下可以保证这个屠性的可见性,对于加了volatile关键字的属性,在对这个属性进行修改时,会直接将CPU高级缓存中的数据写回到主内存,对这个变量的读取也会直接从主内存中读取,从而保证了可见性,底层是通过操作系统的内存屏障来实现的,由于使用了内存屏障,所以会禁止指令重排,所以同时也就保证了有序性,在很多并发场景下,如果用好volatile关键字可以很好的提高执行效率。
对于加了volatle关键字的成员变量,在对这个变量进行修改时,余直接将CPU高级缓存中的数据写回到主内存,对这个变量的读取也会直接从主内存中读取,从而保证了可见性
在对volatile修饰的成员变量进行读写时,会插入内存屏障,而内存屏障可以达到禁止重排序的效果,从而可以保证有序性。
50、Java的内存结构,堆分为哪几部分,默认年龄多大进入老年代?
年轻代
- Eden区
- From Survivor区
- To Survivor区
老年代
默认对象的年龄达到15后,就会进入老年代。
51、Mysql的锁你了解哪些?
按锁粒度分类:
行锁: 锁某行数据,锁粒度最小,并发度高。
表销: 锁整张表,锁粒度最大,并发度低。
间隙锁: 锁的是一个区间。
还可以分为:
共享锁: 也就是读锁,一个事务给某行数据加了读锁,其他事务也可以读,但是不能写。
排它锁: 也就是写锁,一个事务给某行数据加了写锁,其他事务不能读,也不能写。
还可以分为:
乐观锁: 并不会真正的去锁某行记录,而是通过一个版本号来实现的。
**悲观锁:**上面所的行锁、表锁等都是悲观锁。
在事务的隔离级别实现中,就需要利用锁来解决幻读。
52、ConcurrentHashMap 如何保证线程安全,jdk1.8有什么变化?
53、讲一下00M以及遇到这种情况怎么处理的,是否使用过日志分析工具?
54、Mysql索引原理?
55、介绍一下亮点的项目?
56、项目的并发大概有多高,Redis的瓶颈是多少?
57、项目中遇到线上问题怎么处理的,说一下印象最深刻的?
58、String
不改变字符串的引用,更改字符串的值,如何实现? 反射
59、String.StringBuffer、StringBuilder的区别
- String是不可变的,如果尝试去修改,会新生成一个字符串对象,StringBuffer(线程安全)和StringBuilder(线程不安全)是可变的。
- StringBuffer是线程安全的,StringBuilder是线程不安全的,所以在单线程环境下StringBuilder效率会更高。
60、ArrayList和LinkedList区别
- 首先,他们的底层数据结构不同,ArrayList底层是基于数组实现的,LinkedList底层是基于链表实现的。
- 由于底层数据结构不同,他们所适用的场景也不同,Arraylist更适合随机查找,Linkedist更适合删除和添加,查询、添加、删除的时间复杂度不同。
- 另外ArrayList和LinkedList都实现了list接口,但是LinkedList还额外实现了Deque接口,所以LinkedList还可以当做队列来使用。
61、copyonWriteArrayList的底层原理是怎样的
- 首先CopyOnWriteArraylist内部也是用过数组来实现的,在向CopyOnWriteArraylist(线程安全,而ArrayList是非线程安全的)添加元素时,会复制一个新的数组写操作在新数组上进行,读操作在原数组上进行。
- 并且,写操作会加锁,防止出现并发写入丢失数据的问题。
- 写操作结束之后会把原数组指向新数组。
- CopyOnWriteArrayList允许在写操作时来读取数据,大大提高了读的性能,因此适合读多写少的应用场景,但是CopyOnWriteArraylist会比较占内存,同时可能读到的数据不是实时最新的数据,所以不适合实时性要求很高的场景。
62、HashMap的扩容机制
1.7版本
- 先生成新数组。
- 遍历老数组中的每个位置上的链表上的每个元素。
- 取每个元素的key,并基于新数组长度,计算出每个元素在新数组中的下标。
- 将元素添加到新数组中去。
- 所有元素转移完了之后,将新数组赋值给HashMap对象的table属性。
1.8版本
-
先生成新数组。
-
遍历老数组中的每个位置上的链表或红黑树。
-
如果是链表,则直接将链表中的每个元素重新计算下标,并添加到新数组中去。
-
如果是红黑树,则先遍历红黑树,先计算出红黑树中每个元素对应在新数组中的下标位置。
a. 统计每个下标位置的元素个教。
b. 如果该位置下的元素个数超过了8,则生成一个新的红黑树,并将根节点的添加到新数组的对应位置。
c. 如果该位置下的元素个数没有超过8,那么则生成一个链表,并将链表的头节点添加到新数组的对应位置。
-
所有元素转移完了之后,将新数组赋值给HashMap对象的table属性。
63、concurrentHashMap的扩容机制
1.7版本
- 1.7版本的ConcurrentHashMap是基于Segment分段实现的。
- 每个Segment相对于一个小型的HashMap。
- 每个Segment内部会进行扩容,和HashMap的扩容逻辑类似。
- 先生成新的数组,然后转移元素到新数组中。
- 扩容的判断也是每个Segment内部单独判断的,判断是否超过阈值。
1.8版本
- 1.8版本的ConcurrentHashMap不再基于Segment实现。
- 当某个线程进行put时,如果发现ConcurrentHashMap正在进行扩容那么该线程一起进行扩容。
- 如果某个线程put时,发现没有正在进行扩容,则将key-value添加到concurrentHashMap中,然后判断是否超过阈值,超过了则进行扩容。
- ConcurrentHashMap是支持多个线程同时扩容的。·扩容之前也先生成一个新的数组。
- 在转移元素时,先将原数组分组,将每组分给不同的线程来进行元素的转移,每个线程负麦一组或多组的元素转移工作。
64、Tomcat中为什么要使用自定义类加载器
一个Tomcat中可以部署多个应用,而每个应用中都存在很多类,并且各个应用中的类是独立的,全类名是可以相同的,比如一个订单系统中可能存在com.zhouy.User类,一个库存系统中可能也存在com.zhouyu.User类,一个Tomcat,不管内部部署了多少应用,Tomcat启动之后就是一个Java进程,也就是一个JVM,所以如果Tomcat中只存在一个类加载器,比如默认的AppCGlassLoader,那么就只能加载一个com.zhouyu.User类,这是有问题的,而在Tomcat中,会为部署的每个应用都生成一个类加载器,名字叫做webAppClssloader,这样Tomcat中每个应用就可以使用自己的类加载器去加载自己的类,从而达到应用之间的类隔离,不出现冲突。另外Tomcat还利用自定义加载器实现了热加载功能。
65、Tomcat如何进行优化?
对于Tomcat调优,可以从两个方面来进行调整:内存和线程。
首先启动Tomcat,实际上就是启动了一个VM,所以可以按JVM调优的方式来进行调整,从而达到Tomcat优化的目的。
另外Tomcat中设计了一些缓存区,比如appReadBufSize、bufferPoolSize等缓存区来提高吞吐量。
还可以调整Tomcat的线程,比如调整minSpareThreads参数来改变Tomcat空闲时的线程数,调整maxThreads参数来设置Tomcat处理连接的最大线程数。并且还可以调整IO模型,比如使用NIO、APR这种相比于BIO更加高效的IO模型。
66、浏览器发出一个请求到收到响应经历了哪些步骡?
- 浏览器解析用户输入的URL,生成一个HTTP格式的请求。
- 先根据URL域名从本地hosts文件查找是否有映射IP,如果没有就将域名发送给电脑所配置的DNS进行域名解析,得到IP地址。
- 浏览器通过操作系统将请求通过四层网络协议发送出去。
- 途中可能会经过各种路由器、交换机,最终到达服务器。
- 服务器搜到请求后,根据请求所指定的端口,将请求传递给绑定了该端口的应用程序,比如8080被tomcat占用了。
- tomcat接收到请求数据后,按照http协议的格式进行解析,解析得到所要访问的servlet。
- 然后servlet来处理这个请求,如果是SpringMVC中的DispatcherServlet,那么则会找到对应的Controller中的方法,并执行该方法得到结果。
- Tomcat得到响应结果后封装成HTTP响应的格式,并再次通过网络发送给浏览器所在的服务器
- 浏览器所在的服努器拿到结果后再传递给浏览器,浏览器则负责解析并渲染。
67、跨域请求是什么?有什么问题?怎么解决?
跨域是指浏览器在发起网络请求时,会检查该请求所对应的协议、域名、端口和当前网页是否一致,如果不一致则浏览器会进行限制,比如在www.baidu.com的某个网页中,如果使用ajax去访问wwwjd.com是不行的,但是如果是img、iframe、script等标签的src属性去访问则是可以的,之所以浏览器要做这层限制,是为了用户信息安全。但是如果开发者想要绕过这层限制也是可以的:
- response添加header,比如resp.setHeader(“Access-Control-Allow-Origin” “*”)表示可以访问所有网站,不受是否同源的限制;
- jsonp的方式,该技术底层就是基于script标签来实现的,因为script标签是可以跨域的;
- 后台自己控制,先访问同域名下的接口,然后在接口中再去使用HTTPClient等工具去调用目标接口;
- 网关,和第三种方式类似,都是交给后台服努来进行跨域访问。
68、spring中的Bean创建的生命周期有哪些步骤
Spring中一个Bean的创建大概分为以下几个步骤:
- 推断构造方法
- 实例化
- 填充属性,也就是依赖注入
- 处理Aware回调
- 初始化前,处理@PostConstruct注解
- 初始化,处理lnitializingBean接口
- 初始化后,进行AOP
69、ApplicationContext和BeanFactory有什么区别
BeanFactory是Spring中非常核心的组件,表示Bean工厂,可以生成Bean,维护Bean,而ApplicationContext继承了BeanFactory,所以ApplicationContext拥有BeanFactory所有的特点,也是一个Bean工厂,但是ApplicationContext除了继承BeanFactory之外还继承了诸如EnvironmentCapable、MessageSource、ApplicationEventPublisher等接口,从而ApplicationContexti还有获取系统环境变量、国际化、事件发布等功能,这是BeanFactory所不具备的。
70、SpringMVC的底层工作流程
- 用户发送请求至前端控制器DispatcherServlet。
- DispatcherServlet收到请求调用HandlerMapping处理器映射器。
- 处理器映射器找到具体的处理器(可以根据xml配置、注解进行查找),生成处理器及处理器拦截器(如果有则生成)一并返回给DispatcherServlet。
- DispatcherServlet调用HandlerAdapter处理器适配器。
- HandlerAdapter经过适配调用具体的处理器(Controller,也叫后端控制器)。
- Controller执行完成返回ModelAndView。
- HandlerAdapter将controller执行结果ModelAndView返回给DispatcherServlet。
- DispatcherServlet将 ModelAndView传给ViewReslover视图解析器。
- ViewReslover解析后返回具体View。
- DispatcherServlet根据View进行渲染视图(即将模型数据填充至视图中)。
- DispatcherServlet响应用户。
71、SpringBoot是如何启动Tomcat的?
- 首先,SpringBoot在启动时会 先创建一个Spring容器。
- 在创建Spring容器过程中,会利用@ConditionalonClass技术来 判断当前classpath中是否存在Tomcat依赖 ,如果存在则会生成一个启动Tomcat的Bean。
- Spring容器创建完之后,就会 获取启动Tomcat的Bean,并创建Tomcat对象,并绑定端口等,然后启动Tomcat 。
72、Mybatis存在哪些优点和缺点
优点:
- 基于SQL语句编程,相当灵活,不会对应用程序或者数据库的现有设计造成任何影响,SQL单独写,解除sql与程序代码的耦合,便于统一管理;
- 与JDBC相比,减少了50%以上的代码量,消除了JDBC大量冗余的代码,不需要手动开关连接;
- 很好的与各种数据库兼容(因为MyBatis使用JDBC来连接数据库,所以只要JDBC支持的数据库MyBatis都支持);
- 能够与spring很好的集成;
- 提供映射标签,支持对象与数据库的ORM字段关系映射;提供对象关系映射标签,支持对象关系组件维护。
缺点:
- SQL语句的编写工作量较大,尤其当字段多、关联表多时,对开发人员编写SQL语句的功底有一定要求。
- SQL语句依赖于数据库,导致数据库移植性差,不能随意更换数据库。
73、Mybatis中#{}和${}的区别是什么?
- #{} 是 预编译处理、是占位符 ,${} 是 字符串替换、是拼接符 。
- Mybatis在处理 #{} 时,会将sql中的#{}替换为?号,调用PreparedStatement来赋值。
- Mybatis在处理 ${} 时,就是把 ${} 替换成变量的值,调用Statement来赋值。
- 使用 #{} 可以有效的防止SQL注入,提高系统安全性。
74、什么是RPC?
RPC,表示 远程过程调用 ,对于Java这种面向对象语言,也可以理解为远程方法调用,RPC调用和HTTP调用是有区别的,RPC表示的是一种调用远程方法的方式,可以使用HTTP协议、或直接基于TCP协议来实现RPC,在Java中,我们可以通过直接使用某个服务接口的代理对象来执行方法,而底层则通过构造HTTP请求来调用远端的方法,所以,有一种说法是RPC协议是HTTP协议之上的一种协议,也是可以理解的。
75、分布式ID是什么?有哪些解决方案?
在开发中,我们通常会需要一个唯一ID来标识数据,如果是单体架构,我们可以通过数据库的主键,或直接在内存中维护一个自增数字来作为ID都是可以的,但对于一个分布式系统,就会有可能会出现ID冲突,此时有一下解决方案:
- uuid,这种方案复杂度最低,但是会影响存储空间和性能。
- 利用单机数据库的自增主键,作为分布式ID的生成器,复杂度适中,ID长度较之uuid更短,但是受到单机数据库性能的限制,并发量大的时候,此方案也不是最优方案。
- 利用redis、zookeeper的特性来生成id,比如redis的自增命令、zookeeper的顺序节点,这种方案和单机数据库(mysq/)相比,性能有所提高,可以适当选用。
- 雪花算法,一切问题如果能直接用算法解决,那就是最合适的,利用雪花算法也可以生成分布式ID,底层原理就是通过某台机器在某一毫秒内对某一个数字自增,这种方案也能保证分布式架构中的系统id唯一,但是只能保证趋势递增。业界存在tinyid、leaf等开源中间件实现了雪花算法。
76、分布式锁的使用场景是什么?有哪些实现方案?
在单体架构中,多个线程都是属于同一个进程的,所以在线程并发执行时,遇到资源竞争时,可以利用Reentrantlock、synchronized等技术来作为锁,来控制共享资源的使用。
而在分布式架构中,多个线程是可能处于不同进程中的,而这些线程并发执行遇到资源竞争时,利用ReentrantLock、synchronized等技术是没办法来控制多个进程中的线程的,所以需要分布式锁,意思就是,需要一个分布式锁生成器,分布式系统中的应用程序都可以来使用这个生成器所提供的锁,从而达到多个进程中的线程使用同一把锁。
目前主流的分布式锁的实现方案有两种:
- zookeeper: 利用的是zookeeper的临时节点、顺序节点、watch机制来实现的,zookeeper分布式锁的特点是高一致性,因为zookeeper保证的是CP,所以由它实现的分布式锁更可靠,不会出现混乱。
- redis: 利用redis的setnx.lua脚本、消费订阅等机制来实现的,redis分布式锁的特点是高可用,因为redis保证的是AP,所以由它实现的分布式锁可能不可靠,不稳定(一旦redis中的数据出现了不一致),可能会出现多个客户端同时加到锁的情况。
77、什么是分布式事务?有哪些实现方案?
在分布式系统中,一次业务处理可能需要多个应用来实现,比如用户发送一次下单请求,就涉及到订单系统创建订单、库存系统减库存,而对于一次下单,订单创建与减库存应该是要同时成功或同时失败的,但在分布式系统中,如果不做处理,就很有可能出现订单创建成功,但是减库存失败,那么解决这类问题,就需要用到分布式事务。常用解决方案有:
-
本地消息表: 创建订单时,将减库存消息加入在本地事务中,一起提交到数据库存入本地消息表,然后调用库存系统,如果调用成功则修改本地消息状态为成功,如果调用库存系统失败,则由后台定时任务从本地消息表中取出未成功的消息,重试调用库存系统。
-
消息队列: 目前RocketMQ中支持事务消息,它的工作原理是:
a. 生产者订单系统先发送一条half消息到Broker,half消息对消费者而言是不可见的。
b. 再创建订单,根据创建订单成功与否,向Broker发送commit或rollback。
c. 并且生产者订单系统还可以提供Broker回调接口,当Broker发现一段时间half消息没有收到任何操作命令,则会主动调此接口来查询订单是否创建成功。
d. 一旦half消息commit了,消费者库存系统就会来消费,如果消费成功,则消息销毁,分布式事务成功结束。
e. 如果消费失败,则根据重试策略进行重试,最后还失败则进入死信队列,等待进一步处理。
-
Seata: 阿里开源的分布式事务框架,支持AT、TCC等多种模式,底层都是基于两阶段提交理论来实现的。
78、什么是ZAB协议?
ZAB协议是Zookeeper用来实现一致性的原子广播协议,该协议描述了Zookeeper是如何实现一致性的,分为三个阶段:
- 领导者选举阶段: 从Zookeeper集群中选出一个节点作为Leader,所有的写请求都会由Leader节点来处理。
- 数据同步阶段: 集群中所有节点中的数据要和Leader节点保持一致,如果不一致则要进行同步。
- 请求广播阶段: 当Leader节点接收到写请求时,会利用两阶段提交来广播该写请求,使得写请求像事务一样在其他节点上执行,达到节点上的数据实时一致。
但值得注意的是,Zookeeper只是尽量的在达到强一致性,实际上仍然只是最终一致性的。
79、为什么zookeeper可以用来作为注册中心?
可以利用 Zookeeper的临时节点和watch机制 来实现注册中心的自动注册和发现,另外Zookeeper中的数据都是存在内存中的并且Zookeeper底层采用了nio,多线程模型,所以Zookeeper的性能也是比较高的,所以可以用来作为注册中心,但是如果考虑到注册中心应该是注册可用性的话,那么Zookeeper则不太合适,因为Zookeeper是CP的,它注重的是一致性,所以集群数据不一致时,集群将不可用,所以用Redis、Eureka、Nacos来作为注册中心将更合适。
80、zookeeper中的领导者选举的流程是怎样的?
对于Zookeeper集群,整个集群需要从集群节点中选出一个节点作为Leader,大体流程如下:
- 集群中各个节点首先都是观望状态,一开始都会投票给自己,认为自己比较适合作为leader。
- 然后相互交互投票,每个节点会收到其他节点发过来的选票,然后pk,先比较zxid,zxid大者获胜,zxid如果相等则比较myid,myid大者获胜。
- 一个节点收到其他节点发过来的选票,经过PK后,如果PK输了,则改票,此节点就会投给zxid或myid更大的节点,并将选票放入自己的投票箱中,并将新的选票发送给其他节点。
- 如果pk是平局则将接收到的选票放入自己的投票箱中。
- 如果pk赢了,则忽略所接收到的选票。
- 当然一个节点将一张选票放入到自己的投票箱之后,就会从投票箱中统计票数,看是否超过一半的节点都和自己所投的节点是一样的,如果超过半数,那么则认为当前自己所投的节点是leader。
- 集群中每个节点都会经过同样的流程,pk的规则也是一样的,一旦改票就会告诉给其他服务器,所以最终各个节点中的投票箱中的选票也将是一样的,所以各个节点最终选出来的leader也是一样的,这样集群的leader就选举出来了。
81、zookeeper集群中节点之间数据是如何同步的?
- 首先集群启动时,会先进行领导者选举,确定哪个节点是Leader,哪些节点是Follower和Observer。
- 然后Leader会和其他节点进行数据同步,采用发送快照和发送Diff日志的方式。
- 集群在工作过程中,所有的写请求都会交给Leader节点来进行处理,从节点只能处理读请求。
- Leader节点收到一个写请求时,会通过两阶段机制来处理。
- Leader节点会将该写请求对应的日志发送给其他Follower节点,并等待Follower节点持久化日志成功。
- Follower节点收到日志后会进行持久化,如果持久化成功则发送一AAck给Leader节点。
- 当Leader节点收到半数以上的Ack后,就会开始提交,先更新Leader节点本地的内存数据。
- 然后发送commit命令给Follower节点,Follower节点收到commit命令后就会更新各自本地内存数据。
- 同时Leader节点还是将当前写请求直接发送给Observer节点,Observer节点收到Leader发过来的写请求后直接执行更新本地内存数据。
- 最后Leader节点返回客户端写请求响应成功。
- 通过 同步机制 和 两阶段提交机制 来达到集群中节点数据一致。
82、Dubbo支持哪些负载均衡策略
- 随机: 从多个服务提供者随机选择一个来处理本次请求,调用量越大则分布越均匀,并支持按权重设置随机概率。
- 轮询: 依次选择服务提供者来处理请求,并支持按权重进行轮询,底层采用的是平滑加权轮询算法。
- 最小活跃调用数: 统计服务提供者当前正在处理的请求,下次请求过来则交给活跃数最小的服务器来处理。
- —致性哈希: 相同参数的请求总是发到同一个服务提供者。
83、Dubbo是如何完成服务导出的?
- 首先Dubbo会将程序员所使用的@DubboService注解或@Service注解进行解析得到程序员所定义的服务参数,包括定义的服务名、服务接口、服务超时时间、服务协议等等,得到一个ServiceBean。
- 然后调用serviceBean的export方法进行服务导出。
- 然后将服务信息注册到注册中心,如果有多个协议,多个注册中心,那就将服务按单个协议,单个注册中心进行注册。
- 将服务信息注册到注册中心后,还会绑定一些监听器,监听动态配置中心的变更。
- 还会根据服务协议启动对应的Web服务器或网络框架,比如Tomcat、Netty等。
84、Dubbo是如何完成服务引入的?
- 当程序员使用 @Reference注解 来引入一个服务时,Dubbo会将注解和服务的信息解析出来,得到当前所引用的服努名、服务接口是什么。
- 然后从注册中心进行查询服务信息,得到服务的提供者信息,并存在消费端的服务目录中。
- 并绑定一些监听器用来监听动态配置中心的变更。
- 然后根据查询得到的服务提供者信息生成一个服务接口的代理对象,并放入Spring容器中作为Bean。
85、Dubbo的架构设计是怎样的?
Dubbo中的架构设计是非常优秀的,分为了很多层次,并且每层都是可以扩展的,比如:
- Proxy服务代理层,支持JDK动态代理、javassist等代理机制。
- Registry注册中心层,支持Zookeeper.Redis等作为注册中心。
- Protocol远程调用层,支持Dubbo.Htpp等调用协议。
- Transport网络传输层,支持netty.mina等网络传输框架。
- Serialize数据序列化层,支持JSON、Hessian等序列化机制。
86、Spring cloud有哪些常用组件,作用是什么?
- Eureka:注册中心
- Nacos:注册中心、配置中心
- Consul:注册中心、配置中心
- Spring Cloud Config:配置中心
- Feign/OpenFeign:RPC调用
- Kong:服努网关
- Zuul:服务网关
- Spring Cloud Gateway:服务网关
- Ribbon:负载均衡
- Spring CLoud Sleuth:链路追踪
- Zipkin:链路追踪
- Seata:分布式事务
- Dubbo:RPC调用
- Sentinel:服务熔断
- Hystrix:服务熔断
87、Spring Cloud和Dubbo有哪些区别?
Spring Cloud是一个微服务框架,提供了微服务领域中的很多功能组件,Dubbo一开始是一个RPC调用框架,核心是解决服务调用间的问题,Spring Cloud是一个大而全的框架,Dubbo则更侧重于服务调用,所以Dubbo所提供的功能没有SpringCloud全面,但是Dubbo的服务调用性能比Spring Cloud高,不过Spring Cloud和Dubbo并不是对立的,是可以结合起来一起使用的。
88、什么是服务雪崩?什么是服务限流?
- 当服务A调用服务B,服务B调用C,此时大量请求突然请求服务A,假如服务A本身能抗住这些请求,但是如果服务C抗不住,导致服务C请求堆积,从而服务B请求堆积,从而服务A不可用,这就是服务雪崩,解决方式就是服务降级和服务熔断。
- 服务限流 是指在高并发请求下,为了保护系统,可以对访问服务的请求进行数量上的限制,从而防止系统不被大量请求压垮,在秒杀中,限流是非常重要的。
89、什么是服务熔断?什么是服务降级?区别是什么?
- 服务熔断 是指,当服务A调用的某个服务B不可用时,上游服务A为了保证自己不受影响,从而不再调用服务B,直接返回一个结果,减轻服务A和服务B的压力,直到服务B恢复。
- 服务降级 是指,当发现系统压力过载时,可以通过关闭某个服务,或限流某个服务来减轻系统压力,这就是服务降级。
相同点:
- 都是为了防止系统崩溃
- 都让用户体验到某些功能暂时不可用
不同点: 熔断是下游服务故障触发的,降级是为了降低系统负载。
90、SOA、分布式、微服务之间有什么关系和区别?
- 分布式架构是指将单体架构中的各个部分拆分,然后部署不同的机器或进程中去,SOA和微服务基本上都是分布式架构的。
- SOA是一种 面向服务的架构,系统的所有服务都注册在总线上,当调用服务时,从总线上查找服务信息,然后调用。
- 微服务是一种 更彻底的面向服务的架构 ,将系统中各个功能个体抽成一个个小的应用程序,基本保持一个应用对应的一个服务的架构。
91、BIO、NIO、AIO分别是什么?
- BIO:同步阻塞lO,使用BIO读取数据时,线程会阻塞住,并且需要线程主动去查询是否有数据可读,并且需要处理完一个Socket之后才能处理下一个Socket。
- NIO:同步非阻塞lO,使用NIO读取数据时,线程不会阻塞,但需要线程主动的去查询是否有IO事件
- AlO:也叫做NIO 2.0,异步非阻塞lo,使用AlO读取数据时,线程不会阻塞,并且当有数据可读时会通知给线程,不需要线程主动去查询。
92、零拷贝是什么?
零拷贝 指的是,应用程序在需要把内核中的一块区域数据转移到另外一块内核区域去时,不需要经过先复制到用户空间,再转移到目标内核区域去了,而直接实现转移。
93、Netty是什么?和Tomcat有什么区别?特点是什么?
Netty是一个 基于NIO的异步网络通信框架 ,性能高,封装了原生NIO编码的复杂度,开发者可以直接使用Netty来开发高效率的各种网络服务器,并且编码简单。
Tomcat是一个Web服务器,是一个Servlet容器,基本上Tomcat内部只会运行Servlet程序,并处理HTTP请求,而Netty封装的是底层IO模型,关注的是网络数据的传输,而不关心具体的协议,可定制性更高。
Netty的特点:
- 异步、NIO的网络通信框架
- 高性能
- 高扩展,高定制性
- 易用性
94、Netty的线程模型是怎么样的?
Netty同时支持Reactor单线程模型、Reactor多线程模型和Reactor主从多线程模型,用户可根据启动参数配置在这三种模型之间切换。
服务端启动时,通常会创建两个NioEventLoopGroup实例,对应了两个独立的Reactor线程池, bossGroup负责处理客户端的连接请求,workerGroup负责处理I/O相关的操作,执行系统Task、定时任务Task等。用户可根据服务端引导类ServerBootstrap配置参数选择Reactor线程模型,进而最大限度地满足用户的定制化需求。
95、Netty的高性能体现在哪些方面?
- NIO模型, 用最少的资源做更多的事情。
- 内存零拷贝, 尽量减少不必要的内存拷贝,实现了更高效效的传输。
- 内存池设计, 申请的内存可以重用,主要指直接内存。内部实现是用一颗二叉查找树管理内存分配情况.
- 串行化处理读写: 避免使用锁带来的性能开销。即消息的处理尽可能再同一个线程内完成,期间不进行线程切换,这样就避免了多线程竞争和同步锁。表面上看,串行化设计似乎CPU利用率不高,并发程度不够。但是,通过调整NO线程池的线程参数,可以同时启动多个串行化的线程并行运行,这种局部无锁化的串行线程设计相比一个队里-多个工作线程模型性能更优。
- 高性能序列化协议: 支持protobuf等高性能序列化协议。
- 高效并发编程的体现: volatile的大量、正确使用;CAS和原子类的广泛使用;线程安全容器的使用;通过读写锁提升并发性能。
96、Redis分布式锁底层是如何实现的?
- 首先利用setnx来保证:如果key不存在才能获取到锁,如果key存在,则获取不到锁。
- 然后还要利用lua脚本来保证多个redis操作的原子性。
- 同时还要考虑到锁过期,所以需要额外的一个看门狗定时任务来监听锁是否需要续约。
- 同时还要考虑到redis节点挂掉后的情况,所以需要采用红锁的方式来同时向N/2+1个节点申请锁,都申请到了才证明获取锁成功,这样就算其中某个redis节点挂掉了,锁也不能被其他客户端获取到。
97、Redis主从复制的核心原理
Redis的主从复制是提高Redis的可靠性的有效措施,主从复制的流程如下:
- 集群启动时,主从库间会先建立连接,为全量复制做准备。
- 主库将所有数据同步给从库。从库收到数据后,在本地完成数据加载,这个过程依赖于内存快照RDB。
- 在主库将数据同步给从库的过程中,主库不会阻塞,仍然可以正常接收请求。否则,redis的服务就被中断了。但是,这些请求中的写操作并没有记录到刚刚生成的RDB文件中。为了保证主从库的数据一致性,主库会在内存中用专门的replication buffer,记录RDB文件生成收到的所有写操作。
- 最后,也就是第三个阶段,主库会把第二阶段执行过程中新收到的写命令,再发送给从库。具体的操作是,当主库完成RDB文件发送后,就会把此时replocation buffer中修改操作发送给从库,从库再执行这些操作。这样一来,主从库就实现同步了。
- 后续主库和从库都可以处理客户端读操作,写操作只能交给主库处理,主库接收到写操作后,还会将写操作发送给从库,实现增量同步。
98、缓存穿透、缓存击穿、缓存雪崩分别是什么?
缓存中存放的大多都是热点数据,目的就是防止请求可以直接从缓存中获取到数据,而不用访问Mysql。
- 缓存雪崩: 如果缓存中某一时刻大批热点数据同时过期,那么就可能导致大量请求直接访问Mysql了,解决办法就是在过期时间上增加一点随机值,另外如果搭建一个高可用的Redis集群也是防止缓存雪崩的有效手段。
- 缓存击穿: 和缓存雪崩类似,缓存雪崩是大批热点数据失效,而缓存击穿是指某一个热点key突然失效,也导致了大量请求直接访问Mysql数据库,这就是缓存击穿,解决方案就是考虑这个热点key不设过期时间。
- 缓存穿透: 假如某一时刻访问redis的大量key都在redis中不存在(比如黑客故意伪造一些乱七八糟的key),那么也会给数据造成压力,这就是缓存穿透,解决方案是使用布隆过滤器,它的作用就是如果它认为一个key不存在,那么这个key就肯定不存在,所以可以在缓存之前加一层布隆过滤器来拦截不存在的key。
99、Redis和Mysql如何保证数据一致?
- 先更新Mysql,再更新Redis,如果更新Redis失败,可能仍然不一致。
- 先删除Redis缓存数据,再更新Mysql,再次查询的时候在将数据添加到缓存中,这种方案能解决1方案的问题,但是在高并发下性能较低,而且仍然会出现数据不一致的问题,比如线程1删除了Redis缓存数据,正在更新Mysql,此时另外一个查询再查询,那么就会把Mysql中老数据又查到Redis中。
- 延时双删,步骤是:先删除Redis缓存数据,再更新Mysql,延迟几百毫秒再删除Redis缓存数据,这样就算在更新Mysql时,有其他线程读了Mysql,把老数据读到了Redis中,那么也会被删除掉,从而把数据保持一致。
100、Explain语句结果中各个字段分别标识什么?
101、索引覆盖是什么?
索引覆盖就是一个SQL在执行时,可以利用索引来快速查找,并且此SQL所要查询的字段在当前索引对应的字段中都包含了,那么就表示此SQL走完索引后不用回表了,所需要的字段都在当前索引的叶子节点上存在,可以直接作为结果返回了。
101、最左前缀原则是什么?
当一个SQL想要利用索引时,就一定要提供该索引所对应的字段中最左边的字段,也就是排在最前面的字段,比如针对a,b, c三个字段建立了一个联合索引,那么在写一个sql时就一定要提供a字段的条件,这样才能用到联合索引,这是由于在建立a,b, c三个字段的联合索引时,底层的B+树是按照a, b, c三个字段从左往右去比较大小进行排序的,所以如果想要利用B+树进行快速查找也得符合这个规则。
103、Mysql慢查询该如何优化?
- 检查是否走了索引,如果没有则优化SQL利用索引。
- 检查所利用的索引,是否是最优索引。
- 检查所查字段是否都是必须的,是否查询了过多字段,查出了多余数据。
- 检查表中数据是否过多,是否应该进行分库分表了。
- 检查数据库实例所在机器的性能配置,是否太低,是否可以适当增加资源。
104、消息队列有哪些作用?
- 解耦: 使用消息队列来作为两个系统直接的通讯方式,两个系统不需要相互依赖了。
- 异步: 系统A给消费队列发送完消息之后,就可以继续做其他事情了。
- 流量削峰: 如果使用消息队列的方式来调用某个系统,那么消息将在队列中排队,有消费者自己控制消费速度。
105、死信队列是什么?延时队列是什么?
- 死信队列也是一个消息队列,它是用来 存放那些没有成功消费的消息的 ,通常可以用来作为消息重试。
- 延时队列就是用来 存放需要在指定时间被处理的元素的队列 ,通常可以用来处理一些具有过期性操作的业务,比如十分钟内未支付则取消订单。
106、Kafka为什么比RocketMQ的吞吐量要高?
Kafka的生产者采用的是 异步发送消息机制 ,当发送一条消息时,消息并没有发送到Broker而是缓存起来,然后直接向业务返回成功,当缓存的消息达到一定数量时再批量发送给Broker。这种做法 减少了网络IO,从而提高了消息发送的吞吐量 ,但是如果消息生产者宕机,会导致消息丢失,业务出错,所以理论上kafka利用此机制提高了性能却降低了可靠性。
107、Kafka的Pull和Push分别有什么优缺点?
- pull表示消费者主动拉取,可以批量拉取,也可以单条拉取,所以 pull可以由消费者自己控制 ,根据自己的消息处理能力来进行控制,但是消费者不能及时知道是否有消息,可能会拉到的消息为空。
- push表示Broker主动给消费者推送消息,所以肯定是有消息时才会推送,但是 消费者不能按自己的能力来消费消息,推过来多少消息,消费者就得消费多少消息 ,所以可能会造成网络堵塞,消费者压力大等问题。
108、RocketMQ的底层实现原理
RocketMQ由 NameServer集群、Producer集群、Consumer集群、Broker集群组成,消息生产和消费的大致原理如下:
- Broker在启动的时候向所有的NameServer注册,并保持长连接,每30s发送一次心跳。
- Producer在发送消息的时候从NameServer获取Broker服务器地址,根据负载均衡算法选择一台服务器来发送消息。
- Conusmer消费消息的时候同样从NameServer获取Broker地址,然后主动拉取消息来消费。