今天看到网易社招Java岗位的面试题,大致浏览了下,发现还没有答案出来,所以自己就搜索整理下,将答案分享出来,由于水平有限,如发现错误或者疑问,欢迎斧正和讨论,大家一起进步
1. redis有哪几种数据结构?给你一个key怎么知道是用的哪种结构?
考察对redis的数据结构的了解,以及是否在工作中是否能熟练的运用这些数据结构来解决优化问题
Redis是一个内存中的数据结构存储系统,可以用作数据库、缓存和消息中间件, 它支持多种类型的数据结构,如 字符串(strings), 散列(hashes), 列表(lists), 集合(sets), 有序集合(sorted sets) 与范围查询, bitmaps, hyperloglogs 和 地理空间(geospatial) 索引半径查询,在Redis5版本中,新增了流数据类型(Stream data type);
对于存储的key用什么数据结构,这个需要根据业务的需要来设计,选择适合的数据结构对于开发和性能的帮助都是巨大的,比如说以前开发过系统签到功能,在满足高并发的场景要求下,还需要考虑操作时间复杂度,我们这里可以选择hashes类型,并且这里我们还可以根据业务的需求继续优化我们的数据结构,在代码设计复杂度简单的情况下,尽可能减少key对应的个数,比如这里的签到,如果我们为每一个员工都创建一个hash类型的key,则key的数量非常多,但是我们可以选择为每个部门创建一个key,对应的value我们这里还是可以选择使用hashes来存储部门下的员工的相关信息,其中key是员工的信息,value则是签到的状态,这样做有什么好处呢?第一就是减少了key的数据,也就是减少了内存的消耗,第二:由于我们可以根据部门来查询对应的员工,这样的话就减少了循环遍历的次数,性能上面也有了不小的提升。具体的可以参考我的博文《Redis场景应用实例》
2. 怎么查看所有的key?redis怎么切换库?怎么清数据?
重点是考察大数据量的情况下,如果查询所有的key,以及一些常用的管理操作
一般情况下,我们使用keys *
来查询所有的key,也可以匹配查询keys apple*
来查询,dbsize
来查看当前数据库的key的数量;使用flushall
来清空所有数据库的key;
但是当redis中的数据量越来越大的时候,keys命令执行越慢,而且最重要的会阻塞服务器,对单线程的redis来说,简直是灾难,这是我们可以使用scan
命令来查询,scan
是增量式的检索所有的key,同时提供查询一定数量的key(scan 0 count 100
)、匹配查询一定数量的key(scan 0 match CMD* count 100
)等等;
redis默认有16个库,下标从0到15,如果我们切换到指定的数据库,则需要执行select x
x是指定数据库的的下标;也可以通过修改redis.conf来设置数据库的数量database 32
:设置redis的数据库的数量为32个
3. 描述下redis淘汰策略?如果没有数据可以淘汰或者没有配置淘汰策略读请求可以正常执行吗?
考察对淘汰策略的了解,是否在项目中去思考如何配置淘汰策略
首先,为什么redis需要淘汰策略,或者redis淘汰策略出现的原因,我们知道redis是基于内存的,如果一个key存储到了redis中,使用的频率很少,但是一直没有释放,会占据一定的内存,很容易造成内存空间存储瓶颈,此时淘汰无用数据来释放空间,存储新数据的就变得尤为重要了。
那么redis如何需要淘汰数据,采用何种方式来淘汰数据呢?
首先Redis中配置文件中设置一个参数maxmemory
来限制内存大小,当实际存储的内容超过这个大小,redis来开始淘汰数据了,redis采用了以下几种淘汰策略,(这里以redis4为例,redis4目前广泛使用),淘汰的策略可以在redis.conf文件中为maxmemory-policy
赋值:值为几下几种
策略名称 | 说明 |
---|---|
volatile-lru | 根据最近最少使用算法,淘汰带有 有效期 属性的key及其数据。是4.0版本之前最常选用的策略 |
allkeys-lru | 同样根据最近最少使用算法,但是淘汰范围的key是所有的key |
volatile-lfu | 根据最不经常使用算法,淘汰带有 有效期 属性的key及其数据。是4.0版本新增的淘汰机制,个人觉得这种策略会与第1种策略成为两种最佳的选择 |
allkeys-lfu | 与第二种的淘汰范围相同,不过使用的算法是最不经常使用算法。同样是4.0版本新增的淘汰机制 |
volatile-random | 随机淘汰带有 有效期 属性的key及其数据 (不推荐使用) |
allkeys-random | 所有key都随机淘汰 (不推荐使用) |
volatile-ttl | 淘汰有效期属性最少的key及其数据,ttl是 Time To Live的缩写 |
如果没有数据可以淘汰,或者没有配置淘汰策略,则只要内存中可用内存还有的话,请求依旧是可以执行的,直到内存全部被占用,但是相应的当Redis内存超出物理内存限制时,内存数据就会与磁盘产生频繁交换,使Redis性能急剧下降。
4. 你们项目里redis是单节点的吗?如果多节点怎么同步?
考察redis的复制,以及复制的主要的步骤和原理
一般上线的数据都不是使用单节点的,主要是redis是基于内存的,如果服务器发送不可预测的因素导致重启,则redis中的数据会发生丢失(采用持久化可以尽量的避免这点),而且系统上线时,如果单节点的redis服务不能正常提供服务,这样会导致整个系统不可用,风险太大,所以一般都是redis集群来搭建高可用环境,将数据备份到其他服务节点上,这样当该节点服务不可用时,其他节点可以继续提供服务;
redis一般采用主从配置的模式来搭建集群,master配置来写数据,slave配置来读数据,redis内置提供复制功能来实现各个节点间数据的同步;
一旦redis各节点配置了主从关系时,便开始进行数据的同步,从库向主库发送psync
命令,主库接收到命令后会判断是否进行增量同步还是全量同步,然后再根据同步策略来同步数据;
当主库有消息操作时,主库会根据心跳来检查从库是否在线,从库则提供自己的复制偏移量,主库根据偏移量来发送未同步的数据
redis采用量乐观复制策略,容忍在一定时间内主从数据内容是不同的,但是两者的数据最终会同步
5. 项目里用redis存哪些数据?为什么用redis?和memcache本地缓存有什么区别?
考察实际项目中是否有使用redis,观察是否有独立思考为什么要用redis,有什么优点和缺点
redis的官网中已经给出了redis的应用场景:
Redis is an open source (BSD licensed), in-memory data structure store, used as a database, cache and message broker.
Redis 是一个开源(BSD许可)的,内存中的数据结构存储系统,它可以用作数据库、缓存和消息中间件。
一般情况下很少作为数据库,常见的是作为缓存使用,也可以用作分布式锁;除了这些,一些小的功能场景也可以使用redis很方便的解决:
- 排行榜
利用Redis的SortSet(有序集合)数据结构能够简单的搞定
- 计算器
利用Redis中原子性的自增操作,我们可以统计类似用户点赞数、用户访问数等,这类操作如果用MySQL,频繁的读写会带来相当大的压力;限速器比较典型的使用场景是限制某个用户访问某个API的频率,常用的有抢购时,防止用户疯狂点击带来不必要的压力;
- 好友关系
利用集合的一些命令,比如求交集、并集、差集等。可以方便搞定一些共同好友、共同爱好之类的功能;
- session共享
这个在分布式架构中应该是应用最广泛的,无论用户落在那台机器上都能够获取到对应的Session信息。
- 速度快: redis是基于内存的,内存处理速度非常快,这也决定了redis的性能非常的好
- 数据类型丰富:支持String、Hash、Set、Sort Set、bitmaps, hyperloglogs 和 geospatial
- 能持久化存储:RDB和AOF持久化机制来确保数据的不丢失
- 支持事务:操作都是原子性
当然redis也有缺点:
- 内存虽然性能很高,但是同时有受限于内存的大小
- 数据持久化生成文件
- 修改配置文件需要重启redis服务
redis和memcache存在同样的特点:
- 都是将数据放置在内存中,都是内存数据库
- 都存在过期设置
- 都支持分布式集群
但是两者之间又有差异:
- memcache可以缓存图片和视频
- redis支持的数据类型比memcache丰富
- redis支持数据持久化
- redis数据重启可以恢复,而memcache不可
- memcache支持多线程,redis仅支持单线程
6. 你用过哪些开源框架?最熟悉的是哪个?(这里我说了spring,所以后边的问题都是围绕spring的),你常用哪一种注入方式?BeanFactory和ApplicationContext有什么区别?你们项目里用的哪个?说一下spring bean的生命周期。 AOP实现原理是什么?两种动态代理实现原理?JDK动态代理为什么要实现接口?
考察对Spring的了解,对于框架,我们不仅仅需要会用,还需要知道内部的联系和原理
- Spring的优点:
- 降低了组件之间的耦合性 ,实现了软件各层之间的解耦
- 可以使用容易提供的众多服务,如事务管理,消息服务等
- 容器提供单例模式支持
- 容器提供了AOP技术,利用它很容易实现如权限拦截,运行期监控等功能
- 容器提供了众多的辅助类,能加快应用的开发
- spring对于主流的应用框架提供了集成支持,如hibernate,JPA,Struts等
- spring属于低侵入式设计,代码的污染极低
- 独立于各种应用服务器
- spring的DI机制降低了业务对象替换的复杂性
- Spring的高度开放性,并不强制应用完全依赖于Spring,开发者可以自由选择spring的部分或全部
- Spring目前提供三种注入方式:setter/getter方法注入,构造器注入和接口注入;
这里需要说明下@Autowired
和@Resource
的区别
- 首先
@Autowired
是Spring自定义的注解,而@Resource
是Java再带的注解; -
@Autowired
与@Resource
都可以用来装配bean. 都可以写在字段上,或写在setter方法上。 -
@Autowired
默认是按照类型装配的,如果想按照名称来装配可以结合注解@Qualifier
来使用 -
@Resource
默认是按照名称来装配的,也支持按照类型来装配,有两个重要的属性name
和type
- BeanFactory和ApplicationContext的区别
BeanFactory是最顶层的接口 ,提供容器功能,只提供实例化对象和获取对象的功能;ApplicationContext是应用上下文,该接口继承BeanFactory接口,是更高一层的容器,提供更多有用的功能:国际化、访问资源、AOP拦截等
两者都可以装载Bean,但是还是有区别的:
- BeanFactory在启动的时候不会去实例化Bean,只有当获取对象时采取加载Bean,而ApplicationContext在启动的时候就去实例化所有的Bean,还是配置Bean懒加载(
lazy-init=true
)
我们一般使用ApplicationContext,他有如下的优点:
- 启动的时候去实例化所有的Bean,这样系统运行的速度就会非常快,同时在启动的过程中我们就可以发现系统中的配置问题
- ApplicationContext可以通过实现
ApplicationContextAware
来获取应用上下文,然后对Bean进行获取操作等
- Spring bean的生命周期
完整的生命周期分为以下步骤:
- 实例化一个Bean--也就是我们常说的new;
- 按照Spring上下文对实例化的Bean进行配置--也就是IOC注入;
- 如果这个Bean已经实现了BeanNameAware接口,会调用它实现的setBeanName(String)方法,此处传递的就是Spring配置文件中Bean的id值
- 如果这个Bean已经实现了BeanFactoryAware接口,会调用它实现的setBeanFactory(setBeanFactory(BeanFactory)传递的是Spring工厂自身(可以用这个方式来获取其它Bean,只需在Spring配置文件中配置一个普通的Bean就可以);
- 如果这个Bean已经实现了ApplicationContextAware接口,会调用setApplicationContext(ApplicationContext)方法,传入Spring上下文(同样这个方式也可以实现步骤4的内容,但比4更好,因为ApplicationContext是BeanFactory的子接口,有更多的实现方法);
- 如果这个Bean关联了BeanPostProcessor接口,将会调用postProcessBeforeInitialization(Object obj, String s)方法,BeanPostProcessor经常被用作是Bean内容的更改,并且由于这个是在Bean初始化结束时调用那个的方法,也可以被应用于内存或缓存技术;
- 如果Bean在Spring配置文件中配置了init-method属性会自动调用其配置的初始化方法。
- 如果这个Bean关联了BeanPostProcessor接口,将会调用postProcessAfterInitialization(Object obj, String s)方法、;
注:以上工作完成以后就可以应用这个Bean了,那这个Bean是一个Singleton的,所以一般情况下我们调用同一个id的Bean会是在内容地址相同的实例,当然在Spring配置文件中也可以配置非Singleton - 当Bean不再需要时,会经过清理阶段,如果Bean实现了DisposableBean这个接口,会调用那个其实现的destroy()方法;
- 最后,如果这个Bean的Spring配置中配置了destroy-method属性,会自动调用其配置的销毁方法。
- AOP实现的原理
所谓AOP是面向切面编程,通过预编译方式和运行时动态代理实现程序功能的统一维护的一种技术,我们利用AOP结束可以对程序进行解耦,从而使业务逻辑的各部分之间的耦合度降低,提高程序的可重用性,提升开发效率
常见的应用场景有:权限控制、异常处理、缓存、事务管理、日志记录以及数据校验等
Spring框架提供了@AspectJ 注解方法和基于XML架构的方法来实现AOP;主要的原理如下:
通过配置切入点来拦截关注点方法,运用Java动态代理的方法来生成代理类,增强对目标方法的处理。
需要了解几个概念:
- 方面(Aspect):一个关注点的模块化,这个关注点实现可能另外横切多个对象。事务管理是J2EE应用中一个很好的横切关注点例子。方面用Spring的Advisor或拦截器实现。
- 切入点(Pointcut):指定一个通知将被引发的一系列连接点的集合。
- 连接点(Joinpoint):程序执行过程中明确的点,如方法的调用或特定的异常被抛出。
- 通知(Advice):在特定的连接点,AOP框架执行的动作。
- 目标对象(Target Object):包含连接点的对象,也被称作被通知或被代理对象。
- AOP代理(AOP Proxy):AOP框架创建的对象,包含通知。在Spring中,AOP代理可以是JDK动态代理或CGLIB代理。
- 引入(Introduction):添加方法或字段到被通知的类。Spring允许引入新的接口到任何被通知的对象。
- 编织(Weaving):组装方面来创建一个被通知对象。
Spring提供了两种方式来生成代理对象: JDKProxy和Cglib,具体使用哪种方式生成由AopProxyFactory根据AdvisedSupport对象的配置来决定。默认的策略是如果目标类是接口,则使用JDK动态代理技术,否则使用Cglib来生成代理
- 动态代理以及原理
上面说到的SpringAOP是基于动态代理实现的,那么什么是动态代理呢?我们编写程序知道代理模式:
为其他对象提供一个代理以控制对某个对象的访问。代理类主要负责为委托了(真实对象)预处理消息、过滤消息、传递消息给委托类,代理类不现实具体服务,而是利用委托类来完成服务,并将执行结果封装处理
这个其实就是代理(代理分为静态代理和动态代理),而这里的动态代理就是利用反射机制在运行时创建代理类,由代理类为被代理类预处理消息、过滤消息并在此之后将消息转发给被代理类,之后还能进行消息的后置处理。代理类和被代理类通常会存在关联关系(即上面提到的持有的被带离对象的引用),代理类本身不实现服务,而是通过调用被代理类中的方法来提供服务;
动态代理有两种方式:
- 基于接口的JDK动态代理技术
主要是通过Proxy
的静态方法newProxyInstance
返回一个接口的代理实例。针对不同的代理类,传入相应的代理程序控制器InvocationHandler,其底层实现如下:
1. 通过实现 InvocationHandler 接口创建自己的调用处理器;
2. 通过为 Proxy 类指定 ClassLoader 对象和一组 interface 来创建动态代理类;
3. 通过反射机制获得动态代理类的构造函数,其唯一参数类型是调用处理器接口类型;
4. 通过构造函数创建动态代理类实例,构造时调用处理器对象作为参数被传入。
- 基于类的cglib代理
是一个开源项目,强大的高性能的代码生成包,CGLIB包的底层是通过使用一个小而快的字节码处理框架ASM,来转换字节码并生成新的类;
cglib是通过拦截被代理类生成代理类,被代理类直接继承代理类,就可以反射出委托类接口中的所有方法,父类中的所有方法,自身定义的所有方法,完成这些方法的代理就完成了对委托类所有方法的代理
cglib无法代理final修饰的方法
主要原理如下:
- 是经过一系列操作实例化出了
Enhance
对象,并设置了所需要的参数然后enhancer.create()
成功创建出来了代理对象 - 调用代理对象的方法,会进入到方法拦截器的
intercept()
方法,在这个方法中会调用proxy.invokeSuper(obj, args)
方法 -
invokeSuper
中,通过FastClass机制调用目标类的方法
7. 描述下spring的ioc和aop
考察Spring的IOC和AOP
- IOC
Inversion of Control
所谓 IOC ,就是由 Spring IOC 容器来负责对象的生命周期和对象之间的关系
抛除Spring框架,如果我们使用一个对象的话,势必是需要我们自己动手来实例化这个对象的,这样的话就会造成我们的对象和所依赖的对象耦合在一起,我们需要的是所依赖对象提供的服务,但是不是这个对象;
最好是我们需要的依赖对象提供服务的时候,他能够及时提供服务即可,至于是谁实例化这个对象,其实我们并不关心的,Spring正是基于这点,通过Spring容器来实例化Bean对象,然后存储在Bean容器中,那个组件需要这个Bean,只需要将这个Bean注入到对象内,即可调用这个Bean提供的服务。
这样的话,原来需要我们手动来实例化的操作交给了Spring容器来实现,控制Bean实例化的操作权交给了Spring容器,这就是控制翻转
一般注入Bean的方式有三种:setter方法注入、构造器注入、接口注入
- AOP
见面试题6中的关于AOP的讲解
8. HTTP 1.1版本增加了哪些内容?有哪几种请求方式?
考察网络请求协议相关知识
提到Http1.1版本,我们不得不提下 HTTP1.0版本,WEB站点收到大量的请求,为了提高效率,HTTP 1.0规定浏览器与服务器只保持短暂的连接,浏览器的每次请求都需要与服务器建立一个TCP连接,服务器完成请求处理后立即断开TCP连接,服务器不跟踪每个客户也不记录过去的请求。但是,这也造成了一些性能上的缺陷,例如,一个包含有许多图像的网页文件中并没有包含真正的图像数据内容,而只是指明了这些图像的URL地址,当WEB浏览器访问这个网页文件时,浏览器首先要发出针对该网页文件的请求,当浏览器解析WEB服务器返回的该网页文档中的HTML内容时,发现其中的<img>图像标签后,浏览器将根据<img>标签中的src属性所指定的URL地址再次向服务器发出下载图像数据的请求,这样的话,如果一个网页中包含多个图片文件时,就会多次请求和响应,这样对性能造成很大的影响,
HTTP1.1版本解决了上传的问题 ,同时新增了如下的特性:
- 默认持久链接
在一个TCP连接上可以传送多个HTTP请求和响应,减少了建立和关闭连接的消耗和延迟
- 管线化
客户端可以同时发送多个HTTP请求,而不用一个个等待响应
- 断点续传原理
客户端记录本次需要续传的片断,并在需要续传时通知服务器本次需要下载的内容片断,实现断点续传功能
说了HTTP1.0和HTTP1.1,我们在来说下HTTP2.0有了哪些优化
- HTTP2.0采用了二进制格式,而非文本格式
二进制解析起来更高效,更紧凑
- HTTP2.0采用了多路复用,而非有序并阻塞
不采用有序阻塞,主要是为了提高请求和相应的效率,能同时处理多个消息的请求和响应; 甚至可以在传输过程中将一个消息跟另外一个掺杂在一起。所以客户端只需要一个连接就能加载一个页面
- HTTP2.0报文头消息压缩
节省数据的开销,提升请求和响应的效率
- HTTP2.0可以让服务器主动将响应推送到客户端的缓存中
当浏览器请求一个网页时,服务器将会发回HTML,在服务器开始发送JavaScript、图片和CSS前,服务器需要等待浏览器解析HTML和发送所有内嵌资源的请求。服务器推送服务通过“推送”那些它认为客户端将会需要的内容到客户端的缓存中,以此来避免往返的延迟
请求方式有如下:
请求方式 | 备注说明 |
---|---|
GET | 请求指定的页面信息,并返回实体主体 |
HEAD | 类似GET请求,只是返回的信息中没有请求的主体内容,用于获取报头 |
POST | 向指定资源提交数据进行处理请求 |
PUT | 从客户端向服务端传送的数据取代指定的文档的内容 |
DELETE | 请求服务器删除指定界面 |
CONNECT | HTTP1.1协议中预留给能够将连接改为管理方式的代理服务器 |
OPTIONS | 允许客户端查看服务器的性能 |
TRACE | 回显服务器收到的请求,主要用于测试或诊断 |
9. 描述下HTTP三次握手和四次挥手过程?为什么需要四次挥手?为什么TIME_WAIT状态需要经过两个最大报文段生存时间才能到close状态?
何为三次握手:
- A机器发送一个数据包将并SYN置为1,表示希望建立连接。这个包中的序列假设是x
- B机器收到A机器发送的数据包后,通过SYN得知这是一个建立连接的请求,于是发送一个响应包并将SYN和ACK标记置为1,假设这个包中的序列号是y,而确认序列号必须是x+1,表示A收到了SYN,在TCP中,SYN被当作数据部分的一个字节
- A收到B的响应后,需要进行消息确认, ,确认包中将ACK置为1,并将序列号置为y+1,表示收到了来自B的SYN。
三此握手有两个目的:
- 信息对等
- 防止超时
建立连接,需要三次握手,但是释放链接则需要四次握手
- A机器想要关闭链接,则待本方数据发送完毕之后,传递FIN信号给B机器
- B机器应答ACK,告诉A机器可以断开,但是需要等B机器处理完数据,在主动给A发送FIN信号,这时A处于半关闭状态(FIN_WAIT_2),无法发送新的数据
- B做好链接关闭前的准备工作后,发送FIN给A,此时B也进入半关闭状态(CLOSE_WAIT)
- A发送针对B的FIN的ACK后,进入TIME_WAIT状态,经过2MSL后,没有收到B传送回来的报文,则确定B机器已经收到A最后发送的ACK命令,此时TCP链接正式释放
为什么TIME_WAIT状态需要经过两个最大报文段生存时间才能到close状态?
原因有以下几点:
- 确认被动关闭方能够顺利进入CLOSED状态
如上图所示:假如最后一个ACK由于网络原因导致无法到达B机器,处于LAST_ACK状态的B机器会以为对方没有收到自己的FIN+ACK报文,所以会重发,A机器收到第二次的FIN+ACK报文,会重发一次ACK,并且重新计时,如果A机器收到B机器的FIN+ACK报文后, 发送一个ACK给B机器,就进入CLOSED状态,可能会导致B机器无法去报收到最后的ACK命令,也无法进入CLOSED状态
- 防止失效请求
防止已经失效链接的请求数据包与正常链接的请求数据包混淆而发生异常
10. 浏览器发起一个请求到收到响应中间经历了哪些过程?
考察网络相关知识,数据传输的流程
- 浏览器首先去找本地的hosts文件,检查在该文件中是否有相应的域名、IP对应关系,如果有,则向其IP地址发送请求,如果没有就会将domain(域)发送给 dns(域名服务器)进行解析(解析如下图),将域名解析成对应的服务器IP地址,发回给浏览器
- 拿到了服务器IP,接下来就是网络通信
- 应用层客户端发送HTTP请求
- 传输层TCP传输报文
- 网络层IP协议查询MAC地址
- 数据到达数据链路层
- 服务器接收数据
- 服务器响应请求
- 服务器返回相应文件
- 关闭TCP连接
- 页面的渲染阶段
- 解析HTML生成DOM树。
- 解析CSS生成CSSOM规则树。
- 将DOM树与CSSOM规则树合并在一起生成渲染树。
- 遍历渲染树开始布局,计算每个节点的位置大小信息。
- 将渲染树每个节点绘制到屏幕。
11. spring task是怎么实现的?
考察Spring task的原理
Spring Task的作用是处理定时任务。Spring中为定时任务提供TaskExecutor
、TaskScheduler
两个接口。
TaskExecutor
继承了jdk的Executor
,为定时任务的执行提供线程池的支持:
public interface TaskExecutor extends Executor {
void execute(Runnable var1);
}
TaskScheduler提供定时器支持,即定时滴执行任务:
scheduler.schedule(task, new CronTrigger("30 * * * * ?"));
TaskScheduler需要传入一个Runnable的任务做为参数,并指定需要周期执行的时间或者触发器,这样Runnable任务就可以周期性执行了
Spring中实现定时任务有两种方式:
- xml配置
<!-- 配置注解扫描 -->
<context:component-scan base-package="需要扫描的包路径"/>
<task:scheduler id="taskScheduler" pool-size="100" />
<task:scheduled-tasks scheduler="taskScheduler">
<!-- 每半分钟触发任务 -->
<task:scheduled ref="bean组件名" method="方法名" cron="30 * * * * ?"/>
</task:scheduled-tasks>
- 注解
基于SpringBoot,在启动类上添加@EnableScheduling
@Scheduled(cron="* * * * * ?")
public void test(){}
12. spring事务你是怎么用的?加了@Transcational注解spring都做了哪些工作?怎么知道事务执行成功了?
考察Spring的事务
Spring有基于XML和注解的两种事务配置方式
- xml配置
首先定义好数据源的bean,其次实例化sessionFactory,最后配置切面来管理事务,拦截指定路径下的类
<aop:config proxy-target-class="true">
<!-- expression(*)执行的实现类 -->
<aop:pointcut id="serviceMethods" expression="execution(* com.spring.jdbc.service.impl.UserServiceImpl.*(..))"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="serviceMethods"/>
</aop:config>
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="insert*" propagation="REQUIRED"/>
<tx:method name="get*" propagation="SUPPORTS"/>
<tx:method name="*" propagation="SUPPORTS" read-only="true"/>
</tx:attributes>
</tx:advice>
- 注解配置
在Spring中,基于注解的事务管理和xml配置原理是一样的,都是需要配置数据源、实例化sessionFactory,不同的是,基于注解则需要在开启扫描组件,并且在需要开启事务的代码中添加@Transactional
注解
Spring事务的本质是对数据库事务的封装支持,没有数据库对事务的支持,Spring本身无法提供事务管理功能。对于用JDBC操作数据库想要用到事务,必须经过获取连接——》开启事务——》执行CRUD操作——》提交/回滚事务——》关闭连接几部分操作。使用Spring管理事务后,可以省掉自己写代码开启、提交/回滚事务的操作
Spring事务通过AOP动态代理实现,使用上通常要先在配置文件中开启事务,然后通过xml文件或注解配置要执行注解的类方法,然后在调用对应类实例方法时,Spring会自动生成代理,在调用前设置事务操作、调用方法后进行事务回滚或提交操作
下面以SpringBoot为例说明,Spring的事务是如何工作的
在SpringBoot中,如果想要添加@Transactional
并且开启事务功能,则需要添加注解@EnableTransactionManagement
,该注解的主要作用是开启基于注解的事务管理功能,自动配置TransactionManager
,Spring容器在启动时,会将所有的bean加载到ApplicationContext中,在执行含有@Transactional
的类的方法时,会使用AOP来拦截该方法,在该方法调用前开启事务,执行完该方法后,没有异常则提交事务,存在异常则回滚事务。
我们
13. nginx有哪些模块?你比较熟悉哪个?
考察nginx相关知识
- 性能相关配置
- 时间驱动events相关的配置
- http核心模块相关配置(ngx_http_core_module)
- 访问控制模块(ngx_http_access_module)
- 用户认证模块(ngx_http_auth_basic_module)
- 状态查看模块(ngx_http_stub_status_module)
- 日志记录模块(ngx_http_log_module)
- 压缩相关选项(ngx_http_gzip_module)
- httpsssl模块(ngx_http_ssl_module)
- 重定向模块(ngx_http_rewrite_module)
- 引用模块(ngx_http_referer_module)
- 反向代理模块(ngx_http_proxy_module)
- 代理模块(ngx_http_upstream_module)
- ngx_stream_proxy_module模块
14. proxy_cache你是怎么配置的?缓存是存在哪里?具体是怎么命中缓存的? 简历里有写nginx,结果问得几个问题我都没答好,面试官就没再多问了,囧~事务隔离级别?mysql默认级别是什么?事务传播属性?spring默认是什么?嵌套事务子事务什么时候commit?
事务隔离级别:
事务具有四性:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)
而事务的隔离性就是指,多个并发的事务同时访问一个数据库时,一个事务不应该被另一个事务所干扰,每个并发的事务间要相互进行隔离
事务有四种隔离级别,由低到高:
- Read uncommitted
就是一个事务可以读取另一个未提交事务的数据,容易造成脏读
- Read committed
就是一个事务要等另一个事务提交后才能读取数据
一个事务范围内两个相同的查询却返回了不同数据,这就是不可重复读
- Repeatable read
重复读,就是在开始读取数据(事务开启)时,不再允许修改操作
不可重复读对应的是修改,即UPDATE操作。但是可能还会有幻读问题。因为幻读问题对应的是插入INSERT操作,而不是UPDATE操作
- Serializable
Serializable 是最高的事务隔离级别,在该级别下,事务串行化顺序执行,可以避免脏读、不可重复读与幻读。但是这种事务隔离级别效率低下,比较耗数据库性能,一般不使用
大多数数据库默认的事务隔离级别是Read committed,比如Sql Server , Oracle。Mysql的默认隔离级别是Repeatable read
Spring中存在事务传播,所谓的传播属性就是定义在存在多个事务同时存在的时候,spring应该如何处理这些事务的行为;
- Propagation.REQUIRED:支持当前事务,如果当前有事务, 那么加入事务, 如果当前没有事务则新建一个(默认情况)
- Propagation.NOT_SUPPORTED(not_supported) : 以非事务方式执行操作,如果当前存在事务就把当前事务挂起,执行完后恢复事务(忽略当前事务);
- Propagation.SUPPORTS (supports) :如果当前有事务则加入,如果没有则不用事务
- Propagation.MANDATORY (mandatory) :支持当前事务,如果当前没有事务,则抛出异常。(当前必须有事务)
- PROPAGATION_NEVER (never) :以非事务方式执行,如果当前存在事务,则抛出异常。(当前必须不能有事务)
- Propagation.REQUIRES_NEW (requires_new) :支持当前事务,如果当前有事务,则挂起当前事务,然后新创建一个事务,如果当前没有事务,则自己创建一个事务。
- Propagation.NESTED (nested 嵌套事务) :如果当前存在事务,则嵌套在当前事务中。如果当前没有事务,则新建一个事务自己执行(和required一样)。嵌套的事务使用保存点作为回滚点,当内部事务回滚时不会影响外部事物的提交;但是外部回滚会把内部事务一起回滚回去。(这个和新建一个事务的区别)
嵌套事务提交的情况:
- Propagation.REQUIRED+Propagation.REQUIRES_NEW
// ServiceA
@Autowired
private ServiceB serviceB;
@Override
@Transactional(propagation = Propagation.REQUIRED, readOnly = false)
public void methodA() {
this.methodA();
serviceB.methodB();
}
// ServiceB
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW, readOnly = false)
public void methodB() {
System.out.println("methodB");
}
这种情况下, 因为 ServiceB#methodB 的事务属性为 PROPAGATION_REQUIRES_NEW,ServiceB是一个独立的事务,与外层事务没有任何关系。如果ServiceB执行失败,ServiceA的调用出会抛出异常,导致ServiceA的事务回滚
- Propagation.REQUIRED+Propagation.REQUIRED
//ServiceA
@Autowired
ServiceB serviceB;
@Override
@Transactional(propagation = Propagation.REQUIRED, readOnly = false)
public void methodA() {
serviceB.methodB();
}
//ServiceB
@Override
@Transactional(propagation = Propagation.REQUIRED, readOnly = false)
public void methodB() {
}
B 如果发生异常导致事务回滚,则A的事务也会回滚
- Propagation.REQUIRED+无事务注解
//ServiceA
@Autowired
ServiceB serviceB;
@Override
@Transactional(propagation = Propagation.REQUIRED, readOnly = false)
public void methodA() {
serviceB.methodB();
}
//ServiceB
@Override
public void methodB() {
}
B 如果发生异常导致事务回滚,则A的事务也会回滚
- 内层事务被try-catch、
- trycatch+Propagation.REQUIRED+Propagation.REQUIRED
//ServiceA
@Autowired
ServiceB serviceB;
@Override
@Transactional(propagation = Propagation.REQUIRED, readOnly = false)
public void methodA() {
try {
serviceB.methodB(id);
} catch (Exception e) {
System.out.println("内层事务出错啦。");
}
}
//ServiceB
@Override
@Transactional(propagation = Propagation.REQUIRED, readOnly = false)
public void methodB(String id) {
}
事务设置为Propagation.REQUIRED时,如果内层方法抛出Exception,外层方法中捕获Exception但是并没有继续向外抛出,最后出现“Transaction rolled back because it has been marked as rollback-only”的错误。外层的方法也将会回滚。
其原因是:内层方法抛异常返回时,transacation被设置为rollback-only了,但是外层方法将异常消化掉,没有继续向外抛,那么外层方法正常结束时,transaction会执行commit操作,但是transaction已经被设置为rollback-only了。所以,出现“Transaction rolled back because it has been marked as rollback-only”错误。
- trycatch+Propagation.REQUIRED+Propagation.NESTED
//ServiceA
@Autowired
ServiceB serviceB;
@Override
@Transactional(propagation = Propagation.REQUIRED, readOnly = false)
public void methodA() {
try {
serviceB.methodB(id);
} catch (Exception e) {
System.out.println("内层事务出错啦。");
}
}
//ServiceB
@Override
@Transactional(propagation = Propagation.NESTED, readOnly = false)
public void methodB(String id) {
}
当内层配置成 PROPAGATION_NESTED, 此时两者之间又将如何协作呢? 从 Juergen Hoeller 的原话中我们可以找到答案, ServiceB#methodB 如果 rollback, 那么内部事务(即 ServiceB#methodB) 将回滚到它执行前的 SavePoint(注意, 这是本文中第一次提到它, 潜套事务中最核心的概念), 而外部事务(即 ServiceA#methodA) 可以有以下两种处理方式:
- 内层失败,外层调用其它分支,这种方式也是潜套事务最有价值的地方, 它起到了分支执行的效果, 如果 ServiceB.methodB 失败, 那么执行 ServiceC.methodC(), 而 ServiceB.methodB 已经回滚到它执行之前的 SavePoint, 所以不会产生脏数据(相当于此方法从未执行过), 这种特性可以用在某些特殊的业务中, 而 PROPAGATION_REQUIRED 和 PROPAGATION_REQUIRES_NEW 都没有办法做到这一点。
- 代码不做任何修改, 那么如果内部事务(即 ServiceB#methodB) rollback, 那么首先 ServiceB.methodB 回滚到它执行之前的 SavePoint(在任何情况下都会如此), 外部事务(即 ServiceA#methodA) 将根据具体的配置决定自己是 commit 还是 rollback。
15. spring和springMVC是什么关系?有没有用过JdbcTemplate?
考察SpringMVC和Spring的区别
Spring是IOC和AOP的容器框架,SpringMVC是基于Spring功能之上添加的Web框架,想用SpringMVC必须先依赖Spring。
Spring框架有如下的特征:
- 方便解耦,简化开发
- AOP编程的支持
- 声明事事务的支持
- 方便集成各种优秀框架
SpringMVC是一个MVC模式的WEB开发框架;Spring有的优点他都有,他们之间的区别SpringMVC是web框架,是基于Spring框架演化而来的,主要用来简化web开发的,而Spring是一个轻量级的通用解决方案。
Java操作数据库,底层还是使用JDBC技术来进行的,而Spring也针对JDBC进行了封装,提供了许多使用JDBC的模板和驱动模块,为Spring应用操作关系数据库提供了更大的便利。
我们首先配置数据源,然后将数据源注入到jdbcTemplate中,这样jdbcTemplate就能操作数据库了,jdbcTemplate中封装了操作数据的常用方法execute
、query
、update
等方法,方便直接调用
16. springMVC中对整个请求的处理流程是怎样的?返回json的话是用哪个view?
考察SpringMVC的原理,以及@ResponseBody注解的使用
- 客户端(浏览器)发送请求,直接请求到DispatcherServlet。
- DispatcherServlet根据请求信息调用HandlerMapping,解析请求对应的Handler。
- 解析到对应的Handler后,开始由HandlerAdapter适配器处理。
- HandlerAdapter会根据Handler来调用真正的处理器开处理请求,并处理相应的业务逻辑。
- 处理器处理完业务后,会返回一个ModelAndView对象,Model是返回的数据对象,View是个逻辑上的View。
- ViewResolver会根据逻辑View查找实际的View。
- DispaterServlet把返回的Model传给View。
- 通过View返回给请求者(浏览器)
后端返回json的话,则需要使用的@ResponseBody
注解,
- 如果在Controller头上添加
@RestController
注解则 该类中的所有的含有@RequestMapping
注解的方法返回的都是json格式的 - 如果在方法上添加
@ResponseBody
的话,也是返回json格式,但是每个方法都必须添加,比较麻烦,对于前后端分离的项目来说,使用第一种方案最方面
17. 怎么查看某个进程中的线程?
考察常用的jvm的命令
- jps -lvm
jps -lvm 用于查看当前机器上运行的java进程。
- top -Hp pid
可以查看某个进程的线程信息
- jstack -l pid
查看此进程下线程的堆栈信息
18. 怎么批量替换一个文件夹下所有文件中的一个字符?(sed命令)
考察linux命令
- sed -i ‘s/oldstring/newstring/g’ *
批量替换当前目录下所有文件中oldstring为newstring
- sed -i “s/old_string/new_string/g”
grep old_string -rl /home
该命令批量将/home下的所有文件里面包含old_string的替换成new_string
grep和/home旁边的符号为反引号
19. 有没有用过jps jmap jstack jstat 命令,分别说下有哪些常用参数,
考察jvm命令的使用方法
- jps
java提供的一个显示当前所有java进程pid的命令
常用的命令参数有
# 查看当前所有java进程
jps
# 输出应用程序main class的完整package名或者应用程序的jar文件完整路径名
jps -l
- jmap
命令jmap是一个多功能的命令。它可以生成 java 程序的 dump 文件, 也可以查看堆内对象示例的统计信息、查看 ClassLoader 的信息以及 finalizer 队列
常用的命令参数有:
# 查看进程的内存映像信息
jmap pid
# 显示Java堆详细信息
jmap heap pid
# 产生核心的dump的java可执行文件
jmap executable
# 显示堆中对象的统计信息
jmap -histo:live pid
# 打印类加载器信息
jmap -clstats pid
# 生成堆转储快照dump文件
jmap -dump:format=b file=heapdump.phrof pid
- jstack
jstack是jdk自带的线程堆栈分析工具,使用该命令可以查看或导出 Java 应用程序中线程堆栈信息
jstack用于生成java虚拟机当前时刻的线程快照
常用的命令参数有:
# 长列表. 打印关于锁的附加信息
jstack -l pid
- jstat
利用JVM内建的指令对Java应用程序的资源和性能进行实时的命令行的监控,包括了对Heap size和垃圾回收状况的监控
常用的命令参数有:
# 显示加载class的数量,及所占空间等信息。
jstat -class pid
# 显示VM实时编译(JIT)的数量等信息
jstat -compiler pid
# 显示gc相关的堆信息,查看gc的次数,及时间
jstat -gc pid
# VM内存中三代(young,old,perm)对象的使用和占用大小
jstat -gccapacity pid
# 统计gc信息
jstat -gcutil pid
20. 定义Integer x=20 Integer y=200 在内存里是个什么过程?
考察自动装箱等基础知识
首先我们需要明确一点,Integer是一个Object对象类型,但是Java8中针对Integer进行了一些处理,我们先来看下代码:
int i1=20;
Integer i2 = 20;
Integer i3 = 128;
Integer i4 = 128;
int i5 = 128;
Integer i6 = 100;
Integer i7 = 100;
System.out.println(i1==i2); // true
System.out.println(i3==i4); // false
System.out.println(i4==i5); // true
System.out.println(i6==i7); // true
Integer i8 = new Integer(10);
Integer i9 = new Integer(10);
System.out.println(i8==i9); // false
其中 ,为什么i6==i7就返回ture,而i3==i4返回false呢,这里其实需要说明下:
我们使用Integer i=100
,其实调用的是Integer.valueO()
方法,我们先来扒下源码
/**
* 如果i在 -128~127之间,则对象从缓存中获取
* 否则就创建一个新的对象
*/
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
上面的注释说的很明白了,如果一个数字不在-128~127之间,则就会重新创建一个对象,对象与对象使用 ==
比较的话,则肯定是返回false的,而 new Integer()
肯定是创建一个对象的,所以返回false
21. volatile关键字的原理?它能保证原子性吗?AtomicInteger底层怎么实现的?
考察 volatile
volatile常用于保持内存可见性和防止指令重排序;
什么是内存可见性呢?
所有线程都能看到共享内存的最新状态就是内存可见性
我们都知道Java的内存模型,分为主内存和工作内存,其中主内存就是主线程的内存,而工作内存就是每个子线程的内存,每当创建一个子线程执行,则会将主内存中的数据备份到工作内存中,在子线程中操作的也是对应工作内存中的数据,然后等操作的结果同步到主线程中的内存中,这个过程很容易出现多个子线程同时同步数据到主内存中,造成数据混乱,出现线程安全问题;
volatile还能防止指令重排,我们的JVM编译器在编译程序时,为了性能考虑, 编译器和CPU可能会对指令重新排序,很容易造成执行的结果并不是我们想要的,
此volatile关键字就显现他的作用了,对了votaile修饰的变量,线程1中对变量V的修改,在线程2中是可见的,这样就能避免线程同步时,线程安全的问题
volatile能防止指令重排,主要是通过内存屏障来实现的
这里需要明确一点,volatile是变量对内存可见,同时也能防止指令重排,但是不能保证原子性,主要体现在:
如果对声明了volatile的变量进行写操作,JVM就会向处理器发送一条指令,将这个变量所在缓存行的数据写回到系统内存。但是,就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题
对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性
AtomicInteger是对int类型的一个封装,提供原子性的访问和更新操作,其原子性操作的实现是基于CAS(compare-and-swap)技术。
所谓CAS,表现为一组指令,当利用CAS执行试图进行一些更新操作时。会首先比较当前数值,如果数值未变,代表没有其它线程进行并发修改,则成功更新。如果数值改变,则可能出现不同的选择,要么进行重试,要么就返回是否成功。也就是所谓的“乐观锁”。
从AtomicInteger的内部属性可以看出,它依赖于Unsafe提供的一些底层能力,进行底层操作;以volatile的value字段,记录数值,以保证可见性
22. hashMap与concurrentHashMap原理和区别? hashMap什么情况下会出现循环链表?concurrentHashMap写的时候用什么锁?RenteenLock底层是怎么保证线程安全的?
经典面试题,如果具体的话能用一个长篇博客来表达,此处给出关于面试题的解答
- hashMap与concurrentHashMap的原理和区别
JDK1.8之前:
HashMap 里面是一个数组,然后数组中每个元素是一个单向链表,查找的时间复杂度为O(N),N为链表的长度;
数组和链表中的每个元素和节点都是嵌套类Entry的实例,Entry包括四个属性:key,value,hash,和用于单向链表的next;
static class Entry<K,V> implements Map.Entry<K,V>{
Final K key;
V value;
Entry<K,V>next;
int hash;
}
JDK 1.8之后
HashMap结构:数组+链表+红黑树,查找的时间复杂度降低为O(logN).
Java7 中使用Entry来代表每个HashMap中的数据节点,Java8中使用Node,基本没有区别,都是 key,value,hash 和 next 这四个属性,不过,Node 只能用于链表的情况,红黑树的情况需要使用 TreeNode。
我们根据数组元素中,第一个节点数据类型是 Node 还是 TreeNode 来判断该位置下是链表还是红黑树的。
另外,和 Java7 稍微有点不一样的地方就是,Java7 是先扩容后插入新值的,Java8 先插值再扩容
他们之间的区别:
- HashMap不支持并发操作,没有同步方法,ConcurrentHashMap支持并发操作,通过继承 ReentrantLock(JDK1.7重入锁)/CAS和synchronized(JDK1.8内置锁)来进行加锁(分段锁),每次需要加锁的操作锁住的是一个 segment,这样只要保证每个 Segment 是线程安全的,也就实现了全局的线程安全。
- JDK1.8之前HashMap的结构为数组+链表,JDK1.8之后HashMap的结构为数组+链表+红黑树;JDK1.8之前ConcurrentHashMap的结构为segment数组+数组+链表,JDK1.8之后ConcurrentHashMap的结构为数组+链表+红黑树。
HashMap在多线程的情况下会出现循环链表
为什么会出现循环链表呢? 这个得从HashMap的扩容来说,扩容的过程如下:
当HashMap中的元素个数超过数组大小(数组总大小length,不是数组中个数size)loadFactor时,就会进行数组扩容,loadFactor的默认值为0.75,这是一个折中的取值。也就是说,默认情况下,数组大小为16,那么当HashMap中元素个数超过160.75=12(这个值就是代码中的threshold值,也叫做临界值)的时候,就把数组的大小扩展为 2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。
这个也是我们的代码规范中,为什么规范在创建HashMap对象时需要制定Map的大小。
当多个线程同时对这个HashMap进行put操作,而察觉到内存容量不够,需要进行扩容时,多个线程会同时执行resize操作,而这就出现问题了:
- 在HashMap扩容时,会改变链表中的元素的顺序,将元素从链表头部插入
以下模拟2两个线程同时扩容:
当前hashmap的空间为2(临界值为1),hashcode分别为0和1,在散列地址0处有元素A和B,这时候要添加元素C,C经过hash运算,得到散列地址为1,这时候由于超过了临界值,空间不够,需要调用resize方法进行扩容,那么在多线程条件下,会出现条件竞争,模拟过程如下:
线程一:读取到当前的hashmap情况,在准备扩容时,线程二介入
线程二:读取hashmap,进行扩容
线程一:继续执行
这个过程为,先将A复制到新的hash表中,然后接着复制B到链头(A的前边:B.next=A),本来B.next=null,到此也就结束了(跟线程二一样的过程),但是,由于线程二扩容的原因,将B.next=A,所以,这里继续复制A,让A.next=B,由此,环形链表出现:B.next=A; A.next=B
ConncurrentHash写的时候,使用的是分段锁:
ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。Segment是一种可重入锁ReentrantLock,在ConcurrentHashMap里扮演锁的角色,HashEntry则用于存储键值对数据。一个ConcurrentHashMap里包含一个Segment数组,Segment的结构和HashMap类似,是一种数组和链表结构, 一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素, 每个Segment守护者一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得它对应的Segment锁
ReentrantLock与Synchronized相似,都是加锁方式同步,而且都是阻塞式的同步;不同的是:它是java语言的关键字,是原生语法层面的互斥,需要jvm实现。而ReentrantLock它是JDK 1.5之后提供的API层面的互斥锁,需要lock()和unlock()方法配合try/finally语句块来完成
其内部实现线程安全的原理可以使用一句话总结:内部使用了自旋锁的方式来解决多线程下的线程安全问题
什么是自旋锁?通过循环调用CAS操作来实现加锁;
它的性能比较好也是因为避免了使线程进入内核态的阻塞状态
23. mysql索引是怎么实现的?b+树有哪些特点?真实的数据存在哪里?
考察索引原理相关知识
索引(Index)是帮助MySQL高效获取数据的数据结构,索引实际上也是一张表,保存了主键和索引字段,并指向实体表的记录;
我们知道数据库的数据持久化到了磁盘中,每次的数据查询,其实就是扫描磁盘,而磁盘IO的操作是非常耗费资源的;
那么我们就需要尽量减少磁盘的IO次数,我们可以时间分区思想来思考问题,如果总共有数据1000条,我们将50条数据分为一块,那么查询序号为300的数据,我们需要定位到第6分区的数据就能查到,这样就极大的减少了磁盘IO的次数。
其实上面的思想就是运用到搜索树的功能,其平均复杂度是lgN,具有不错的查询性能;
这里我们引入B+Tree的数据结构,那么什么是B+Tree?
在了解B+Tree之间,我们还得介绍下BTree;
BTree即二叉搜索树:
其特点如下:
- 度(Degree)-节点的数据存储个数,一旦存储数据超过度值就要进行节点分裂
- 所有叶子节点具有相同的深度
- 叶子节点的指针为空
- 叶子节点中的数据key从左到右递增排列
BTree数据结构通过节点的横向扩展,从而压低整个Btree的高度,减少了节点io读取的次数。通常百万级别的数据会被压到3-5层的高度。
但是BTree也有缺点:
BTree里面的节点采用了key-value的基本存储结构,key是索引的数值,value是存储的data数值。由于我们对于BTree进行节点 比较的时候是基于内存进行数据比较,先从磁盘进行io读取数据,读取到cpu缓存中进行比对。
如果我们将节点的度设置到极致,例如说将度设置到100W,那么BTree的高度就会降低,查询的次数就会大大减少,是否可行?
这种想法是不可行的,节点会变得过大。每次进行节点数据读取的时候都需要将磁盘的数据加载到操作系统自身的缓存中。假设将度值设置过大,io一次读取的大小还是有限,过大的节点还是需要进行多次的io读取。
此时我们就需要B+Tree了,那么什么是B+Tree呢?
具有如下的特点:
- 非叶子节点不存储data,只存储key,可以增大度的值。
- 叶子节点不存储指针。
仔细观察图中树结构的同学不知道是否有发现,B+Tree里面对于索引数据进行了适当的冗余存储,但是这一点相比于度大小的增加而言,并不会带来太多的性能影响。由于非叶子结点只存储key,并没有存储data数据,因此所有的非叶子结点的度可以增加地更大, 使得一次磁盘IO读取的数据更多,从磁盘读取到操作系统内存中的数据也大大增加。
仔细观察B+Tree里面的数据结构,会发现叶子节点里面有相应的顺序访问指针。
B+Tree的叶子节点之间的顺序访问指针的作用可以提高范围查询的效率。
具体的可以阅读如下的博文:
Mysql索引原理
24. 哪些情况下建索引?解释下最左匹配原则? 现在一个表有三列a b c,组合索引(a,b,c)查询的时候where a like ? and b=? and c=?能用到这个组合索引吗?为什么?
考察索引使用的相关知识
我们通常在以下几种情况时创建索引:
- 主键自动建立唯一索引。
- 频繁作为查询条件的字段应该创建索引。
- 查询外键关系建立索引。
- 查询排序、分组字段建立索引
索引的底层是一颗B+树,那么联合索引当然还是一颗B+树,只不过联合索引的健值数量不是一个,而是多个。构建一颗B+树只能根据一个值来构建,因此数据库依据联合索引最左的字段来构建B+树;
最左匹配原则:最左优先,以最左边的为起点任何连续的索引都能匹配上。同时遇到范围查询(>、<、between、like)就会停止匹配;
举个例子:
建立联合索引(a,b,c),其索引匹配有以下几种情况:
- 全值匹配查询
select * from table_name where a = '1' and b = '2' and c = '3'
select * from table_name where b = '2' and a = '1' and c = '3'
select * from table_name where c = '3' and b = '2' and a = '1'
用到了索引,where子句几个搜索条件顺序调换不影响查询结果,因为Mysql中有查询优化器,会自动优化查询顺序
- 匹配左边的列时
select * from table_name where a = '1'
select * from table_name where a = '1' and b = '2'
select * from table_name where a = '1' and b = '2' and c = '3'
都从最左边开始连续匹配,用到了索引,如果查询调价那种没有a的确定值匹配(不是范围匹配)则没有用到索引,是全表扫描的
如果不连续的话,比如:
select * from table_name where a = '1' and c = '3'
如果不连续时,只用到了a列的索引,b列和c列都没有用到
- 匹配列前缀
如果a是字符类型,那么前缀匹配用的是索引,后缀和中缀只能全表扫描了
select * from table_name where a like 'As%'; //前缀都是排好序的,走索引查询
select * from table_name where a like '%As'//全表查询
select * from table_name where a like '%As%'//全表查询
- 匹配范围值
select * from table_name where a > 1 and a < 3
可以对最左边的列进行范围查询
select * from table_name where a > 1 and a < 3 and b > 1;
多个列同时进行范围查找时,只有对索引最左边的那个列进行范围查找才用到B+树索引,也就是只有a用到索引,在1<a<3的范围内b是无序的,不能用索引,找到1<a<3的记录后,只能根据条件 b > 1继续逐条过滤
- 精确匹配某一列并范围匹配另外一列
select * from table_name where a = 1 and b > 3;
如果左边的列是精确查找的,右边的列可以进行范围查找, a=1的情况下b是有序的,进行范围查找走的是联合索引
- 排序
一般情况下,我们只能把记录加载到内存中,再用一些排序算法,比如快速排序,归并排序等在内存中对这些记录进行排序,有时候查询的结果集太大不能在内存中进行排序的话,还可能暂时借助磁盘空间存放中间结果,排序操作完成后再把排好序的结果返回客户端。Mysql中把这种再内存中或磁盘上进行排序的方式统称为文件排序。文件排序非常慢,但如果order子句用到了索引列,就有可能省去文件排序的步骤
select * from table_name order by a,b,c limit 10;
因为b+树索引本身就是按照上述规则排序的,所以可以直接从索引中提取数据,然后进行回表操作取出该索引中不包含的列就好了
order by的子句后面的顺序也必须按照索引列的顺序给出,比如
select * from table_name order by b,c,a limit 10;
这种颠倒顺序的没有用到索引
select * from table_name order by a limit 10;
select * from table_name order by a,b limit 10;
这种用到部分索引
select * from table_name where a =1 order by b,c limit 10;
联合索引左边列为常量,后边的列排序可以用到索引
那么题目中的 现在一个表有三列a b c,组合索引(a,b,c)查询的时候where a like ? and b=? and c=?能用到这个组合索引吗?
我们就有了答案了:
这里要区分下: a的范围匹配 是否是前缀匹配,如果是前缀匹配则使用了联合索引,如果不是前缀匹配,则走的是全表扫描
25. explain执行计划看过没有?其中type字段都有哪些值?分别代表什么?
考察sql执行计划
当我定位查询缓慢的sql语句时,通过我们会在sql的执行语句之前添加explain,查询的结果会出现10列:
column | remark |
---|---|
id | 选择标识符 |
select_type | 表示查询的类型 |
table | 输出结果集的表 |
partitions | 匹配的分区 |
type | 表示表的连接类型 |
possible_keys | 表示查询时,可能使用的索引 |
ref | 列与索引的比较 |
key | 表示实际使用的索引 |
filtered | 按表条件过滤的行百分比 |
Extra | 执行情况的描述和说明 |
下面我们来重点说明下:select_type
和 type
值的含义
其中select_type
如下:
value | remark |
---|---|
SIMPLE | 简单SELECT,不使用UNION或子查询等 |
PRIMARY | 子查询中最外层查询,查询中若包含任何复杂的子部分,最外层的select被标记为PRIMARY |
UNION | UNION中的第二个或后面的SELECT语句 |
DEPENDENT UNION | UNION中的第二个或后面的SELECT语句,取决于外面的查询 |
UNION RESULT | UNION的结果,union语句中第二个select开始后面所有select |
SUBQUERY | 子查询中的第一个SELECT,结果不依赖于外部查询 |
DEPENDENT SUBQUERY | 子查询中的第一个SELECT,依赖于外部查询 |
DERIVED | 派生表的SELECT, FROM子句的子查询 |
UNCACHEABLE SUBQUERY | 一个子查询的结果不能被缓存,必须重新评估外链接的第一行 |
type
对应的列值:
value | remark |
---|---|
ALL | Full Table Scan, MySQL将遍历全表以找到匹配的行 |
index | Full Index Scan,index与ALL区别为index类型只遍历索引树 |
range | 只检索给定范围的行,使用一个索引来选择行 |
ref | 表示上述表的连接匹配条件,即哪些列或常量被用于查找索引列上的值 |
eq_ref | 类似ref,区别就在使用的索引是唯一索引,对于每个索引键值,表中只有一条记录匹配,简单来说,就是多表连接中使用primary key或者 unique key作为关联条件 |
const/system | 当MySQL对查询某部分进行优化,并转换为一个常量时,使用这些类型访问。如将主键置于where列表中,MySQL就能将该查询转换为一个常量,system是const类型的特例,当查询的表只有一行的情况下,使用system |
NULL | MySQL在优化过程中分解语句,执行时甚至不用访问表或索引,例如从一个索引列里选取最小值可以通过单独索引查找完成 |
26. 你有哪些sql调优经验?
考察实际的项目经验
结合实际情况说明下,一般就以下几种情况:
- sql语句设计不合理
- 没有索引或者索引设计不合理
- 表数据过大,没有有效的分区设计
- 网络问题
- 服务器内存不够
27. 反射能获取到父类的私有方法吗?怎么防止反射破坏单例模式?
考察反射相关知识,以及单例模式的特性
反射可以获取父类的私有方法,代码如下:
public class MockData extends Parent{
private String name;
public static void main(String[] args) throws Exception{
Class parentClass = MockData.class.getSuperclass();
Method method = parentClass.getDeclaredMethod("saySomething", null);
method.setAccessible(true);
method.invoke(parentClass.newInstance(), null);
}
}
class Parent{
private void saySomething() {
System.out.println("parent's method");
}
}
首先单例模式的常用实现方式是将构造器私有化,这样外部就不能通过new的方式来创建对象,但是这个方法可以通过反射来创建新的对象,让实例不唯一,单例模式就遭到了破坏。
解决的方法其实很简单,就是在构造器中添加判断,如果对象已经存在,就直接抛出异常
//volatile防止指令重排序,内存可见(缓存中的变化及时刷到主存,并且其他的内存失效,必须从主存获取)
public static volatile DoubleLock doubleLock = null;
private DoubleLock(){
//构造器必须私有 不然直接new就可以创建
//构造器判断,防止反射攻击
if(doubleLock != null){
throw new IllegalStateException();
}
}
28. 描述下JVM内存模型。每个区的作用是什么?堆内存的工作原理,为什么需要两个幸存区?只有一个行不行?老年代是用什么垃圾回收算法?
考察JVM内存方面的相关知识,
首先这里的内存模型描述有点问题,其实考官主要是考察内存结构方面的知识点:
- 程序计数器
每个线程都有一个程序计算器,就是一个指针,指向方法区中的方法字节码(下一个将要执行的指令代码),由执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不记
- 本地方法栈
本地方法栈则是为Native方法服务
- 方法区
用于存储虚拟机加载的:静态变量+常量+类信息+运行时常量池(类信息:类的版本、字段、方法、接口、构造函数等描述信息 ),默认最小值为16MB,最大值为64MB,可以通过-XX:PermSize 和 -XX:MaxPermSize 参数限制方法区的大小
- 堆
所有的对象实例以及数组都要在堆上分配,此内存区域的唯一目的就是存放对象实例
堆是Java 虚拟机所管理的内存中最大的一块。Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建
- 虚拟机栈
编译器可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(引用指针,并非对象本身)
栈是java 方法执行的内存模型
每个方法被执行的时候 都会创建一个“栈帧”用于存储局部变量表(包括参数)、操作栈、方法出口等信息
每个方法被调用到执行完的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程
栈的生命期是跟随线程的生命期,线程创建时创建,线程结束栈内存也就释放,是线程私有的
由于堆是JavaGC的主要区域,所以我们这里讲解下堆内部的结构:新生代(Eden区+2个Survivor区) 老年代 永久代(HotSpot有)
- 新生代
新创建的对象——>Eden区
GC之后,存活的对象由Eden区 Survivor区0进入Survivor区1
再次GC,存活的对象由Eden区 Survivor区1进入Survivor区0
这里为什么需要两个S区呢?
我们假设一下,如果没有Survivor区,新生代只有Eden区。
当Eden区装满后,Minor GC进行垃圾回收,幸存的对象会直接放入老年代,可以想到,要不了多久老年代就会装满,便会进行Major GC且连带Minor GC也就是Full GC,每次Full GC都会消耗大量的时间。
Survivor具有预筛选保证,只有对象到一定岁数才会送往老年代,Survivor区可以减少被送到老年代的对象,进而减少Full GC发生
至于为什么使用两个S区?
我们知道新生代使用复制回收算法,我们设想一下只有一个Survivor区会发生什么情况:
当Eden区填满后,Minor GC进行垃圾回收,幸存的对象会移动到Survivor区,这样循环往复。此时,Survivor区被装满了,也会进行Minor GC,将一些对象kill掉,幸存的对象只能保存在原来的位置,这样就会出现大量的内存碎片(被占用内存不连续)
内存碎片化是严重影响性能的,可以设想当有一个稍大一点的对象从Eden区存活转入Survivor区,发现空闲内存断断续续,没有他能落脚的地方,就只能直接存到老年代了,如此反复,老年代会出现我们第一部分的问题。
如果有两个Survivor区,便可以保证一个为空,另一个是非空且无碎片保存的
- 老年代
对象如果在新生代存活了足够长的时间而没有被清理掉(即在几次Young GC后存活了下来),则会被复制到老年代
如果新创建对象比较大(比如长字符串或大数组),新生代空间不足,则大对象会直接分配到老年代上(大对象可能触发提前GC,应少用,更应避免使用短命的大对象)
老年代的空间一般比新生代大,能存放更多的对象,在老年代上发生的GC次数也比年轻代少
我们常用的垃圾回收算法有:
- 标记清除算法
- 标记整理算法
- 复制回收算法
而老年代使用的就是 标记整理算法
- 永久代
可以简单理解为方法区(本质上两者并不等价)
Jdk1.6及之前:常量池分配在永久代
Jdk1.7:有,但已经逐步“去永久代”
Jdk1.8及之后:没有永久代(java.lang.OutOfMemoryError: PermGen space,这种错误将不会出现在JDK1.8中)
29. threadLocal关键字有用过吗?如果没有重写initialValue方法就直接get会怎样?
ThreadLoal 变量,线程局部变量,同一个 ThreadLocal 所包含的对象,在不同的 Thread 中有不同的副本。这里有几点需要注意:
- 因为每个 Thread 内有自己的实例副本,且该副本只能由当前 Thread 使用。这是也是 ThreadLocal 命名的由来。
- 既然每个 Thread 有自己的实例副本,且其它 Thread 不可访问,那就不存在多线程间共享的问题。
ThreadLocal是如何为每个线程创建变量的副本的:
- 在每个线程Thread内部有一个ThreadLocal.ThreadLocalMap类型的成员变量threadLocals,这个threadLocals就是用来存储实际的变量副本的,键值为当前ThreadLocal变量,value为变量副本(即T类型的变量)
- 在Thread里面,threadLocals为空,当通过ThreadLocal变量调用get()方法或者set()方法,就会对Thread类中的threadLocals进行初始化,并且以当前ThreadLocal变量为键值,以ThreadLocal要保存的副本变量为value,存到threadLocals
- 然后在当前线程里面,如果要使用副本变量,就可以通过get方法在threadLocals里面查找
- 实际的通过ThreadLocal创建的副本是存储在每个线程自己的threadLocals中的
- 为何threadLocals的类型ThreadLocalMap的键值为ThreadLocal对象,因为每个线程中可有多个threadLocal变量,就像上面代码中的longLocal和stringLocal
- 在进行get之前,必须先set,否则会报空指针异常
如果想在get之前不需要调用set就能正常访问的话,必须重写initialValue()方法。
如果没有先set的话,即在map中查找不到对应的存储,则会通过调用setInitialValue方法返回i,而在setInitialValue方法中,有一个语句是T value = initialValue(), 而默认情况下,initialValue方法返回的是null,不重写的话,如果查询不到会报空指针异常
30. 描述下多线程原理。怎么开启一个线程?start和run方法有什么区别? 怎么创建一个线程池,传入的参数分别什么含义?线程池是怎么实现维持核心线程数的?怎么实现一个自定义的拒绝策略?
常用的创建线程有两种方法:extends Thread和 implements Runnable,start方法是开启一个线程,run方法是执行类中的方法,此时没有开启线程。
创建线程和销毁线程的花销是很大的,如果在业务逻辑中创建和销毁线程,这样很消耗系统资源,降低系统性能,使用线程池可以减少线程创建和销毁的过程,线程池有如下的优点:
- 提高效率,预先创建好线程放在线程池中,需要的时候从线程池中获取,结束后回归线程池,这样能减少线程的创建和销毁时间和资源,能提供系统系统
- 方便管理,可以统一编写线程池管理代码,管理线程的分配以及排队等候等
常见的线程池
- newSingleThreadExecutor
创建单线程的线程池,主要是通过一个线程池来实现执行的顺序化
缺点:堆积的请求处理队列,可能会消耗大量的内存,引起OOM异常
- newFixedThreadPoo
创建定长线程,控制线程最大并发数,超出的线程会在队列中等待
缺点:堆积的请求处理队列,可能会消耗大量的内存,引起OOM异常
- newCachedThreadPool
创建可缓存线程池,如果线程池长度超过处理的需求,可灵活的回收空闲线程池,若不可回收,则创建新的线程
缺点:线程池最大数是Integer.MAX_VALUE,创建很多的线程导致OOM
- newScheduledThreadPool
创建定长线程池,支持周期性任务执行
缺点:线程池最大数是Integer.MAX_VALUE,创建很多的线程导致OOM
线程池几个重要参数说明
- corePoolSize
核心池的大小,线程池刚创建时,默认是没有线程的,只有当任务来临时才创建线程,如果创建的线程多于corePoolSize的大小,则再次创建的线程就会存储在缓存队列中等待执行
- maximumPoolSize
线程池最大线程数,线程池中最多能创建多个线程
- keepAliveTime
线程没有执行任务时,最终保持多长时间终止,默认情况下,只有当线程池中的线程树大于corePoolSize才会生效
- unit
keepAliveTime的单位
- workQueue
阻塞队列,用来存储等待执行的任务
- threadFactory
用于创建线程的工厂,对创建出来的线程进行管理
- handler
表示拒绝处理任务时的策略
线程池有四种拒绝策略:
- AbortPolicy
是线程池默认的拒绝策略,在任务不能再提交的时候,抛出异常,及时反馈程序运行状态
- DiscardPolicy
丢弃任务,但是不抛出异常。如果线程队列已满,则后续提交的任务都会被丢弃,且是静默丢弃
- DiscardOldestPolicy
丢弃队列最前面的任务,然后重新提交被拒绝的任务
- CallerRunsPolicy
由调用线程处理该任务
我们也可以自己实现RejectedExecutionHandler
接口,来自定义拒绝策略