java常见面试题(后续可能一直更新)

该面试题的答案不保证一定对

一、Java基础

1、多线程开发的时候,有哪些并发的容器?或者说哪些是线程安全的?

StringBuffer、List中的Vector、CopyOnWriteArrayList、CopyOnWriteArraySet、hashtable、Atomicxxx-包装类的线程安全类、Concurrentxxx

2、hashmap的原理

通过 HashSet 存储对象,会根据对象的 hashCode() 生成 hash 值,根据 hash 生成索引,根据索引找到hash 表中的位置;判断位置上是否有对象,没有对象直接插入,有对象则通过 equals() 判断内容是否相同,如果相同则不插入,不同则根据链表往下检索,直到重复或者链表没有下一个对象,准备插入最后的时候判断链表是否超过 8 个对象,当链表超过 8 个对象时,总存储的对象大于等于 64 则把链表转换成红黑树当 hash 表总存储的对象大于负载因子值(0.75),则会进行数组的扩容,扩容为原本的 2 倍。

3、线程池的最大的线程数如何设计

江湖谣传是:
IO 密集型: 2 * CPU核心数
计算密集型: CPU核心数 + 1

但私以为下面这个更可信:
理论公式:cpu核心数 * cpu利用率 * (1+ 线程等待时间 / 线程计算时间)
但是理论公式仅供参考,实际使用还需要不断的细微调节。

二、Jvm相关

1、JVM垃圾回收器有哪些?

Serial收集器(新生代、复制算法)
ParNew收集器(新生代、复制算法)
Parallel Scavenge收集器(新生代、复制算法)
Serial Old收集器(老年代、整理算法)
Parallel Old收集器(老年代、整理算法)
CMS收集器(Concurrent Mark Sweep)
G1回收器(Garbage First)

2、JVM的优化配置哪些参数

参考网址:https://blog.csdn.net/wyn_365/article/details/120381440

-Xms10g :JVM启动时申请的初始堆内存值
-Xmx20G :JVM可申请的最大堆内存值
-Xmn3g : 新生代大小,一般设置为堆空间的1/3 1/4左右,新生代大则老年代小
-Xss :Java每个线程的栈内存大小
-XX:PermSize :持久代(方法区)的初始内存大小
-XX:MaxPermSize : 持久代(方法区)的最大内存大小
-XX:SurvivorRatio : 设置新生代eden空间和from/to空间的比例关系,关系(eden/from=eden/to)
-XX:NewRatio : 设置新生代和老年代的比例老年代/新生代

三、Spring相关

1、spring里面除了单例,工厂,代理模式,哪些功能还用到了什么模式?

a、工厂设计模式(Spring 框架中 BeanFactory 和 ApplicationContext 类使用工厂模式创建 Bean 对象)

(1)BeanFactory:延迟注入,即使用到某个Bean时才会进行注入,和 ApplicationContext 相比会占用更少的内存,程序启动速度更快。

(2)ApplicationContext:容器启动时就创建所有的Bean,和 BeanFactory 相比 ,BeanFactory 仅提供了最基本的依赖注入支持 . ApplicationContext 扩展了 BeanFactory, 除了 BeanFactory 的功能外还包含其余更多的功能,通常使用ApplicationContext 创建 Bean。

(3)ApplicationContext的三个实现类:
ClassPathXmlApplication: 将上下文文件作为类路径资源。
FileSystemXmlApplication: 从文件系统中的 XML 文件中载入上下文定义信息。
XmlWebApplicationContext: 从Web系统中的 XML 文件中载入上下文定义信息。

b、单例设计模式(Spring 中的 Bean 的作用域默认就是单例 Singleton 的)

Spring底层通过ConcurrentHashMap实现单例注册表来实现单例模式。

c、模板方法模式

模板方法模式是一种行为型模式,基于继承的代码复用、定义一个操作的算法骨架,将一些实现步骤延迟到子类中、模板方法使得子类可以不改变一个算法结构的情况下即可重新定义算法的某些特定步骤的实现方式

Spring 中以 Template 结尾的类,比如 jdbcTemplate 等,都是使用了模板方法模式。
1、通常情况下,都是使用继承来实现模板模式。
2、在 Spring 中,使用了 Callback 与模板方法相结合的方式,既达到了代码复用的效果,又增加了系统的灵活性

d、观察者模式

观察者模式表示的是一种对象和对象之间具有依赖关系,当一个对象发生改变,依赖于这个对象的对象也会发生改变。

Spring事件驱动模型就是基于观察者模式实现的,包含三种角色:事件Event角色、事件监听者Listener角色、事件发布者Publisher角色

(1)事件角色Event:

ApplicationEvent 事件角色抽象类,继承了 Event 并实现 Serializable 接口。
	Spring中默认存在以下事件,都是继承自 ApplicationContext 事件角色抽象类: 
		ContextStartedEvent:ApplicationContext 启动后触发的事件。
		ContextStoppedEvent:ApplicationContext 停止后触发的事件。
		ContextRefreshedEvent:ApplicationContext 初始化或者刷新后触发的事件。
		ContextClosedEvent:ApplicationContext 关闭后触发的事件。

(2)事件监听者角色Listener:
ApplicationListener(事件监听者角色,ApplicationListener 接口中定义了一个 onApplicationEvent() 方法来处理ApplicationEvent。只要实现 onApplicationEvent() 方法即可完成监听事件)

(3)事件发布者角色Publisher:
ApplicationEventPublisher(事件发布者角色,ApplicationEventPublisher 接口中定义了 publishEvent() 方法来发布事件,这个方法在 AbstractApplicationContext 中实现,在 AbstractApplicationContext 中,事件是通过ApplicationEventMulticaster 广播的)

Spring的事件发布流程:
定义一个事件(实现一个继承自ApplicationEvent的事件类,并写出相应的构造函数)——>
定义一个事件监听者(实现ApplicationListener接口、重写onApplicationEvent()方法)——>
使用事件发布者发布消息(使用ApplicationEventPublisher的publishEvent() 方法、重写publishEvent() 方法发布消息)

e、AOP中的适配器模式

适配器模式:将一个接口转换为调用方需要的接口,适配器使得接口不兼容的类之间可以一起工作.适配器又被称为包装器Wrapper

Spring AOP中的增强和通知Advice使用了适配器模式,接口是AdvisorAdapter。

每个通知Advice都有对应的拦截器:
BeforeAdvice - MethodBeforeAdviceInterceptor
AfterAdvice - MethodAfterAdviceInterceptor
AfterReturningAdvice - MethodAfterReturningAdviceInterceptor

Spring中预定义的通知要通过对应的适配器,适配成为MethodInterceptor接口类型的对象

f、Spring MVC中的适配器模式

Spring MVC中 ,DispatchServlet 根据请求信息调用 HanlderMapping, 解析请求对应的 Handler, 解析到对应的Handler 后,开始由 HandlerAdapter 适配器进行处理。
HandlerAdapter 作为期望接口,具体的适配器实现类对具体目标类进行适配 。controller 作为需要适配的类。
通过使用适配器 AdapterHandler 可以对 Spring MVC 中众多类型的 Controller 通过不同的方法对请求进行处理。

g、装饰器模式

装饰器模式:动态地给对象添加一些额外的属性或者行为,和继承相比,装饰器模式更加灵活

使用场景:当需要修改原有的功能,但是不想直接修改原有的代码,就可以设计一个装饰器 Decorator 类在原有的代码的外面,这样可以在不修改原有的类的基础上扩展新的功能。

Spring 中配置 DataSource 时 ,DataSource 可以是不同的数据库和数据源.为了在少修改原有类的代码下动态切换不同的数据源,这时就用到了装饰器模式。
Spring 中含有 Wrapper 和含有 Decorator 的类都用到了装饰器模式,都是动态地给一个对象添加一些额外的属性或者功能。

2、spring中事务没有回滚是什么原因造成的?

a.方法访问权限 public 修饰。
b.方法被 final 修饰,这种情况代理类无法重写该方法,无法添加事务功能、static 也是。
c.方法自调用、
d.未被 Spring 管理、
e.多线程调用:可能存在两个方法不在同一个线程中、获取到的数据库链接不一样,从而是两个不同的事务。

3、Bean的初始化流程

在这里插入图片描述
在这里插入图片描述

4、Spring如何解决循环依赖的

循环依赖就是:多个bean之间相互依赖,形成了一个闭环。 比如:A依赖于B、B依赖于c、c依赖于A。

a、三级缓存名词解释

第一级缓存〈也叫单例池)singletonObjects:存放已经经历了完整生命周期的Bean对象。
第二级缓存:earlySingletonObjects,存放早期暴露出来的Bean对象,Bean的生命周期未结束(属性还未填充完整)
第三级缓存:Map<String, ObiectFactory<?>> singletonFactories,存放可以生成Bean的工厂。

b、解决循环依赖具体说明

(1)A创建过程中需要B,于是A将自己放到三级缓存里面,去实例化B。
(2)B实例化的时候发现需要A,于是B先查一级缓存,没有,再查二级缓存,还是没有,再查三级缓存,找到了A然后把三级缓存里面的这个A放到二级缓存里面,并删除三级缓存里面的A。
(3)B顺利初始化完毕,将自己放到一级缓存里面(此时B里面的A依然是创建中状态)然后回来接着创建A,此时B已经创建结束,直接从一级缓存里面拿到B,然后完成创建,并将A放到一级缓存中。

c、为什么使用三级缓存?二级缓存能解决循环依赖吗?

如果没有 AOP 代理,二级缓存可以解决问题,但是有 AOP 代理的情况下,只用二级缓存就意味着所有 Bean 在实例化后就要完成 AOP 代理,这样违背了 Spring 设计的原则,Spring 在设计之初就是通过AnnotationAwareAspectJAutoProxyCreator 这个后置处理器来在 Bean 生命周期的最后一步来完成 AOP 代理,而不是在实例化后就立马进行 AOP 代理。

d、B 中提前注入了一个没有经过初始化的 A 类型对象不会有问题吗?

虽然在创建 B 时会提前给 B 注入了一个还未初始化的 A 对象,但是在创建 A 的流程中一直使用的是注入到 B 中的 A 对象的引用,之后会根据这个引用对 A 进行初始化,所以这是没有问题的。

5、AOP 的增强过程

在这里插入图片描述

在这里插入图片描述

6、BeanFactory 和 ApplicationContext 的区别

BeanFactory 和 ApplicationContext 都是接口,并且 ApplicationContext 是 BeanFactory 的子接口。

BeanFactory 是 Spring 中最底层的接口,提供了最简单的容器的功能,只提供了实例化对象和拿对象的功能。而ApplicationContext 是 Spring 的一个更高级的容器,提供了更多的有用的功能。

ApplicationContext 提供的额外的功能:国际化的功能、消息发送、响应机制、统一加载资源的功能、强大的事件机制、对 Web 应用的支持等等。

加载方式的区别:BeanFactory 采用的是延迟加载的形式来注入 Bean;ApplicationContext 则相反的,它是在 IOC 启动时就一次性创建所有的 Bean,好处是可以马上发现 Spring 配置文件中的错误,坏处是造成浪费。

7、@Resource和@Autowired的区别

1、@Resource 默认是按照名称(id)来装配注入的,只有当找不到与名称匹配的 bean 才会按照类型(class)来装配注入;
2、@Autowired 默认是按照类型装配注入的,如果想按照名称来注入,则需要结合 @Qualifier 一起使用;
3、@Resource 注解是由 J2EE 提供,而 @Autowired 是由 Spring 提供;

四、SpringBoot相关

1、Spring Boot 怎么完成自动化配置的?

结论: SpringBoot不需要写配置文件的原因是,SpringBoot所有配置都是在启动的时候进行扫描并加载,SpringBoot的所有自动配置类都在Spring.factories里面,但是不一定会生效,生效前要判断条件是否成立,只要导入了对应的start,就有对应的启动器,有了启动器就能帮我们进行自动配置类

以前我们需要自己配置的东西,自动配置类帮我们做了:
1、SpringBoot在启动的时候从类路径下的 META-INF/spring.factories 中获取 EnableAutoConfiguration 指定的值。
2、将这些值作为自动配置类导入容器 , 自动配置类就生效 , 帮我们进行自动配置工作;
3、整个J2EE的整体解决方案和自动配置都在 springboot-autoconfigure 的 jar 包中;
4、它会给容器中导入非常多的自动配置类 (xxxAutoConfiguration), 就是给容器中导入这个场景需要的所有组件 , 并配置好这些组件 ;

有了自动配置类 , 免去了我们手动编写配置注入功能组件等的工作;

Spring的自动装配原理:
Spring Boot启动的时候会通过 @EnableAutoConfiguration 注解找到 META-INF/spring.factories 配置文件中的所有自动配置类,并对其进行加载,这些自动配置类都是以 AutoConfiguration 结尾来命名的,它实际上就是一个 JavaConfig 形式的 Spring 容器配置类,通过 @Bean 导入到 Spring 容器中,以 Properties 结尾命名的类是和配置文件进行绑定的。它能通过这些以 Properties 结尾命名的类中取得在全局配置文件中配置的属性,我们可以通过修改配置文件对应的属性来修改自动配置的默认值,来完成自定义配置。

额外说一句:
run 方法的作用:
1、推断应用的类型是普通的项目还是Web项目

2、查找并加载所有可用初始化器 , 设置到initializers属性中

3、找出所有的应用程序监听器,设置到listeners属性中

4、推断并设置main方法的定义类,找到运行的主类

2、SpringBoot 初始化时的启动扩展点

a、监听容器刷新完成扩展点 ApplicationListener

容器刷新成功意味着所有的 Bean 初始化已经完成,当容器刷新之后 Spring 将会调用容器内所有实现了ApplicationListener 的 Bean 的 onApplicationEvent 方法,应用程序可以以此达到监听容器初始化完成事件的目的。

易错的点这个扩展点用在 web 容器中的时候需要额外注意,在 web 项目中(例如 spring mvc),系统会存在两个容器,一个是 root application context,另一个就是我们自己的 context(作为 root application context 的子容器)。如果按照上面这种写法,就会造成 onApplicationEvent 方法被执行两次。

解决此问题的方法如下:
自定义事件,可以借助 Spring 以最小成本实现一个观察者模式:

先自定义一个事件,注册一个事件监听器,发布事件,执行单元测试可以看到邮件的地址和内容都被打印出来了。

b、SpringBoot 的 CommandLineRunner 接口

c、@PostConstruct 注解

@PostConstruct 注解一般放在 Bean 的方法上,被 @PostConstruct 修饰的方法会在 Bean 初始化后马上调用。

d、InitializingBean 接口

InitializingBean 的用法基本上与 @PostConstruct 一致,只不过相应的 Bean 需要实现 afterPropertiesSet 方法

e、@Bean 注解中定义初始化方法(销毁方法也一样)

在创建对象的时候里面可以有多种方法,假设其中两个方法分别为 init() 和 destory();那么就可以通过 @Bean 注解来指定初始化方法和销毁方法:
@Bean(initMethod=“init”, destroyMethod=“destroy”)

bean 的销毁方法时在容器关闭的时候被调用的,applicationContext.close()。

bean对象的初始化方法调用的时机:
对象创建完成,如果对象中存在一些属性,并且这些属性也都已经赋值了,那么就会调用bean的初始化方法。对于单实例bean来说,在Spring容器创建完成后,Spring容器会自动调用bean的初始化方法;对应多实例bean来说,在每次获取bean对象的时候,调用bean的初始化方法。

bean对象的销毁方法调用的时机:
对于单实例bean来说,在容器关闭的时候,会调用bean的销毁方法;对于多实例bean来说,Spring容器只负责创建bean不会管理这些bean,也就不会自动调用这个bean的销毁方法了。小伙伴只能手动调用多实例bean的销毁方法了。

初始化、销毁方法的使用场景:
一个典型的场景就是对于数据源的管理。例如,在配置数据源时,在初始化的时候,会对很多的数据源的属性进行赋值操作;在销毁的时候,我们需要对数据源的连接等信息进行关闭和清理。这个时候,我们就可以在自定义的初始化和销毁方法中自定义操作逻辑。

五、SpringCloud(微服务)相关

1、分布式和集群有什么区别呢?

a、集群是个物理形态,分布式是个工作方式。集群一般是物理集中、统一管理的;一个程序或系统,只要运行在不同的机器上,就可以叫分布式。

b、分布式是相对中心化而来,强调的是任务在多个物理隔离的节点上进行,除了解决部分中心化问题,也倾向于分散负载,但分布式会带来很多的其他问题,最主要的就是一致性。集群就是逻辑上处理同一任务的机器集合。

c、分布式:一个业务分拆多个子业务,部署在不同的服务器上;集群:同一个业务,部署在多个服务器上。

d、集群是解决高可用的;而分布式是解决高性能、高并发的。

2、nacos里面要配置 namespace 和 group id 这两个的作用是什么?

group 是分组,namespace 是命名空间。一般 namespace 区分环境,group区分项目。

namespace:不同的命名空间下,可以存在相同的 Group 或 Data ID 的配置。
group:通过一个有意义的字符串(如 Buy 或 Trade )对配置集进行分组,从而区分 Data ID 相同的配置集。

3、nacos 的配置变化了是服务器主动推送的还是微服务每次循环去拉取的?

(1)Nacos 采用的是客户端主动拉 pull 模型,应用长轮询(Long Polling)的方式来获取配置数据。

(2)Nacos 获取配置数据的逻辑比较简单,先取本地快照文件中的配置,如果本地文件不存在或者内容为空,则再通过HTTP 请求从远端拉取对应 dataId 配置数据,并保存到本地快照中,请求默认重试3次,超时时间 3s。获取配置有getConfig() 和 getConfigAndSignListener() 这两个接口,但 getConfig() 只是发送普通的 HTTP 请求,而getConfigAndSignListener() 则多了发起长轮询和对dataId数据变更注册监听的操作addTenantListenersWithContent()。

(3)注册监听:
客户端注册监听,先从 cacheMap 中拿到 dataId 对应的 CacheData 对象。如没有则向服务端发起长轮询请求获取配置,默认的 Timeout 时间为30s,并把返回的配置数据回填至 CacheData 对象的 content 字段,同时用 content 生成MD5 值;再通过 addListener() 注册监听器。其中 listeners 是对 dataId 所注册的所有监听器集合,其中的ManagerListenerWrap 对象除了持有 Listener 监听类,还有一个 lastCallMd5 字段,这个属性很关键,它是判断服务端数据是否更变的重要条件。在添加监听的同时会将 CacheData 对象当前最新的 md5 值赋值给ManagerListenerWrap 对象的 lastCallMd5 属性。

(4)变更通知:
客户端又是如何感知服务端数据已变更呢?
我们还是从头看,NacosConfigService 类的构造器中初始化了一个 ClientWorker,而在 ClientWorker 类的构造器中又启动了一个线程池来轮询 cacheMap。而在 executeConfigListen() 方法中有这么一段逻辑,检查 cacheMap 中 dataId的 CacheData 对象内,MD5 字段与注册的监听 listener 内的l astCallMd5 值,不相同表示配置数据变更则触发safeNotifyListener 方法,发送数据变更通知。safeNotifyListener() 方法单独起线程,向所有对 dataId 注册过监听的客户端推送变更后的数据内容。客户端接收通知,直接实现 receiveConfigInfo() 方法接收回调数据,处理自身业务就可以了。

4、RestTemplate 和 Feign 的区别

a.请求方式:
	RestTemplate需要每个请求都拼接url+参数+类文件,灵活性高但是消息封装臃肿。
	feign可以伪装成类似SpringMVC的controller一样,将rest的请求进行隐藏,不用再自己拼接url和参数,可以便捷优雅地调用HTTP API。
	
b.底层实现:
	RestTemplate在拼接url的时候,可以直接指定ip地址+端口号,不需要经过服务注册中心就可以直接请求接口;也可以指定服务名,
	请求先到服务注册中心(如nacos)获取对应服务的ip地址+端口号,然后经过HTTP转发请求到对应的服务接口
	(注意:这时候的restTemplate需要添加@LoadBalanced注解,进行负载均衡)。

	Feign的底层实现是动态代理,如果对某个接口进行了@FeignClient注解的声明,Feign就会针对这个接口创建一个动态代理的对象,
	在调用这个接口的时候,其实就是调用这个接口的代理对象,代理对象根据@FeignClient注解中name的值在服务注册中心找到对应的服务,
	然后再根据@RequestMapping等其他注解的映射路径构造出请求的地址,针对这个地址,再从本地实现HTTP的远程调用。

5、docker怎么解决容器间的通信

docker network 来创建一个桥接网络,在 docker run 的时候将容器指定到新创建的桥接网络中,这样同一桥接网络中的容器就可以通过这个桥接网络互相访问。

六、MySQL 相关

1、mybatis的缓存(一级缓存二级缓存那些)

a. 一级缓存

SqlSession 级别的缓存,缓存的数据只在 SqlSession 内有效。
默认开启;一级缓存是 sqlSession 级别的缓存。在操作数据库时需要构造 sqlSession 对象,在对象中有一个基于 PerpetualCache 的 HashMap 本地缓存数据结构,用于缓存数据。不同的 sqlSession 之间的缓存数据区域(HashMap)是互不影响的。一个SqlSession 结束后那么它里面的一级缓存也就不存在了。

b.二级缓存

mapper 级别的缓存,同一个 namespace 公用这一个缓存,所以对 SqlSession 是共享的。二级缓存需要我们手动开启。(全局级别)二级缓存是 mapper 级别的缓存,多个 sqlSession 去操作同一个Mapper 的 sql 语句,多个 sqlSession可以共用二级缓存,二级缓存是跨 sqlSession 的。二级缓存是基于映射文件的缓存(namespace),缓存范围比一级缓存更大,不同的 SQLSession 可以访问二级缓存的内容。哪些数据放入二级缓存需要自己指定

c.一级缓存原理

(1)查询数据时,会先到缓存中查询是否有,如果没有则到数据库中查找,找到后存到缓存中。
(2)如果 sqlSession 去执行 commit 操作(插入、更新、删除),清空 sqlSession 中的一级缓存,保证缓存中始终保存的是最新的信息,避免脏读。
(3)两次查询须在同一个 sqlsession 中完成,否则将不会走 mybatis 的一级缓存。在 mybatis 与 spring 进行整合开发时,事务控制在 service 中进行,重复调用两次 servcie 将不会走一级缓存,因为在第二次调用时 session 方法结束,SqlSession 就关闭了

d.二级缓存原理

(1)二级缓存与一级缓存原理相同,默认也是采用 PerpetualCache,HashMap 存储,不同在于其存储作用域为 Mapper ( Namespace ),并且可自定义存储源,如 Ehcache。作用域为 namespance 是指对该 namespance 对应的配置文件中所有的 select 操作结果都缓存,这样不同线程之间就可以共用二级缓存。

e.二级缓存设置对象策略

(1)readOnly=“true”(只读):MyBatis 认为所有从缓存中获取数据的操作都是只读操作,不会修改数据。MyBatis 为了加快获取数据,直接就会将数据在缓存中的引用交给用户 。不安全,速度快。
(2)readOnly=“false”(读写,默认):MyBatis 觉得获取的数据可能会被修改,MyBatis 会利用序列化或反序列化的技术克隆一份新的数据。安全,速度相对慢。

f.二级缓存注意事项

(1)如果 SqlSession 执行了 DML 操作(insert、update、delete),并 commit 了,那么 mybatis 就会清空当前mapper 缓存中的所有缓存数据,这样可以保证缓存中的存的数据永远和数据库中一致,避免出现脏读。
(2)mybatis 的缓存是基于[ namespace:sql语句:参数 ]来进行缓存的,意思就是,SqlSession 的 HashMap 存储缓存数据时,是使用[ namespace:sql:参数 ]作为key,查询返回的语句作为value保存的。

g.开启二级缓存

通过application.yml配置二级缓存开启:mybatis.configuration.cache-enabled=true

2、mysql数据库的 innodb 和 myisam 的区别?

a、InnoDB支持事务,MyISAM不支持

b、MyISAM适合纯读或者纯写为主,InnoDB适合频繁修改以及涉及到安全性较高的应用

c、InnoDB支持外键,MyISAM不支持

d、从MySQL5.5.5以后,InnoDB是默认引擎

e、InnoDB不支持FULLTEXT类型的索引

g、InnoDB中不保存表的行数,如select count() from table时,InnoDB需要扫描一遍整个表来计算有多少行,但是MyISAM只要简单的读出保存好的行数即可。但是当count()语句包含where条件时MyISAM也需要扫描整个表。

h、对于自增长的字段,InnoDB中必须包含只有该字段的索引,但是在MyISAM表中可以和其他字段一起建立联合索引。

i、清空整个表时,InnoDB是一行一行的删除,效率非常慢。MyISAM则会重建表。

j、innodb默认表锁,使用索引检索条件时是行锁,而myisam是表锁(每次更新增加删除都会锁住表)。

k、innodb和myisam的索引都是基于b+树,但他们具体实现不一样,innodb的b+树的叶子节点是存放数据的,myisam的b+树的叶子节点是存放指针的。

l、innodb是聚簇索引,必须要有主键,一定会基于主键查询,但是辅助索引就会查询两次,myisam是非聚簇索引,索引和数据是分离的,索引里保存的是数据地址的指针,主键索引和辅助索引是分开的。

3、innodb支持几种索引?

参考网址:https://www.cnblogs.com/aluna/p/15850620.html

主键索引(B+树)、普通索引、唯一索引、全文索引(5.6.4版本以后。char、varchar、text的列才支持)、哈希索引

聚簇索引(是一种数据存储方式,所有的用户记录都存储在了叶子节点,也就是所谓的索引即数据,数据即索引),特点:
	使用记录主键值的大小进行记录和页的排序:
		1、页内的记录是按照主键的大小顺序排成一个单向链表
		2、各个存放用户记录的页也是根据页中用户记录的主键大小顺序排成一个双向链表。
		3、.存放目录项记录的页分为不同的层次,在同一层次中的页也是根据页中目录项记录的主键大小顺序排成一个双向链表。
		
	B+树的叶子节点存储的是完整的数据记录:
		所谓完整的用户记录,就是指这个记录中存储了所有列的值(包括隐藏列)。
			
	优点:
		数据访问更快,因为聚簇索引将索引和数据保存在同一个B+树中,
		聚簇索引对于主键的排序查找和范围查找速度非常快,
		按照聚簇索引排列顺序,查询显示一定范围数据的时候,由于数据都是紧密相连,数据库不用从多个数据块中提取数据,
		所以节省了大量的IO操作。
			
	缺点:
		插入速度严重依赖于插入顺序,按照主键的顺序插入是最快的方式,否则将会出现页分裂,严重影响性能。因此,
		对于InnoDB表,我们一般都会定义一个自增的ID列为主键。
		
		更新主键的代价很高,因为将会导致被更新的行移动。因此,对于InnoDB表,我们一般定义主键为不可更新。
		二级索引访问需要两次索引查找,第一次找到主键值,第二次根据主键值找到行数据。


索引的代价:
	空间上的代价:
		每建立一个索引都要为它建立一棵B+树,每一棵B+树的每一个节点都是一个数据页,一个页默认会占用16KB的存储空间,
		一棵很大的B+树由许多数据页组成,那就是很大的一片存储空间。
			
	时间上的代价:
		每次对表中的数据进行增、删、改操作时,都需要去修改各个B+树索引。增、删、改操作可能会对节点和记录的排序造成破坏,
		所以存储引擎需要额外的时间进行一些记录移位,页面分裂、页面回收等操作来维护好节点和记录的排序。如果建了许多索引,
		每个索引对应的B+树都要进行相关的维护操作,会给性能拖后腿。
		
	一个表上索引建的越多,就会占用越多的存储空间,在增删改记录的时候性能就越差。 

4、对sql优化的理解

1、sql语句中IN包含的值不应过多(MySQL对于IN做了相应的优化,即将IN中的常量全部存储在一个数组里面,而且这个数组是排好序的。
   但是如果数值较多,产生的消耗也是比较大的。再例如:select id from t where num in(1,2,3) 对于连续的数值,能用between就不要用in了;
   再或者使用连接来替换。)

2、数据分页 limit
3、当只需要一条数据的时候,使用limit 1(这是为了使EXPLAIN中type列达到const类型,如果加上limit1,查找到就不用继续往后找了)
4、如果排序字段没有用到索引,就尽量少排序,可以在程序中排序。
5、如果限制条件中其他字段没有索引,尽量少用or。(or两边的字段中,如果有一个不是索引字段,而其他条件也不是索引字段,会造成该查询不走索引的情况。
   很多时候使用union all或者是union(必要的时候)的方式来代替“or”会得到更好的效果。)
	
6、尽量用union all代替union。(union和union all的差异主要是前者需要将结果集合并后再进行唯一性过滤操作,这就会涉及到排序,
   增加大量的CPU运算,加大资源消耗及延迟。当然,union all的前提条件是两个结果集没有重复数据。)
	
7、小表驱动大表(in 的话里面驱动外面,in适合子查询是小表;exist 的话外面驱动里面,适合外面是小表)
8、用链接查询代替子查询(子查询缺点:需要创建临时表,查询完成后再删除临时表,有一些额外开销。)
9、控制索引的数量(一般不超过五个)
10、选择合理的字段类型
11、提升group by的效率
12、避免用 select * (浪费数据库资源,内存,cpu查出来的数据多,通过网络IO传输过程中也会增加传输时间)
13、批量插入(mybatis plus 的insertBatch,当然一次插入量也不能太大,可以分批插入。)
14、join表不宜过多,不宜超过3个。(如果join太多,MySQL在选择索引时会非常复杂,很容易选错索引)
15、join的注意事项:
		inner join:mysql会自动选择小表驱动
		left join:左边的表驱动右边的表
		
16、选择合理的字段类型
		char:固定字符串类型,该类型在的字段在存储空间上是固定的,固定长度的可以用
		varchar:变长字符串类型。
		能用数字类型就不用字符串,字符串处理速度比数字类型慢。
		尽量用小类型,比如:用bit存布尔值,用tinyint存枚举值等。
		
17、提升group by效率(先过滤数据,减少数据,再分组)

5、你说说死锁怎么解决

死锁(DeadLock)指的是两个或两个以上的运算单元(进程、线程或协程),都在等待对方释放资源,但没有一方提起释放资源,从而造成了一种阻塞的现象就称为死锁。

死锁产生的四个必要条件:
1、互斥:一个资源一次只能被一个进程使用,当该进程使用该资源的时候,其他进程就不能使用,具有独占性;

2、请求与保持:一个进程要请求新的资源,但同时对已获得的资源不释放,要等待其他进程释放资源;

3、循环等待:若干进程都要申请资源,但是都对已获得的资源不释放,都要等待其他进程释放资源,若干进程陷入循环等待资源;

4、不可剥夺:进程已获得的资源,在未使用完之前,不能被强行剥夺。

只要上述条件之一不满足,就不会发生死锁:
比如当一个运算单元获取到锁的时候其他就等待,或者给锁加一个时间,时间到了强制释放锁。

6、resultMap 标签干什么用的?

sql代码的复用,类似于java的继承。

7、一个 sql 查询变得很慢,你会怎么去分析这个 sql

1、检查 SQL 是否走索引,或者检查是否出现索引失效的情况。如果是单条 sql,则使用 explain 进行相关分析。
2、单表数据量数据过多,导致查询瓶颈:可以考虑对表进行切分,或者分库。
3、网络原因或者机器负载过高:可以进行读写分离。

8、什么样的情况下不会走索引

1、用 != 或者 <> 导致索引失效
2、字段编码类型不一致进行关联查询不走索引;如:a表和b表,一个utf8编码,一个utf8mb4编码
3、函数导致的索引失效
4、运算符导致的索引失效
5、OR引起的索引失效。比如:or链接同一个字段走索引,不同就不走索引。或者两个字段都有索引才走索引
6、模糊搜索导致的索引失效,如 like的百分号放左边不走索引,或者两边都有
7、NOT IN、NOT EXISTS导致索引失效
8、is null,is not null也无法使用索引,不走索引!
9、数据类型隐式转换,字符串列与数字直接比较,不走索引

9、mybatis的加载流程是怎样的?

启动程序,通过 springfactoryBuilder 去加载 mybatis 全局配置文件,生成 sqlSessionFactory,通过这个去生成 sqlSession,sqlSession 代表的是一个会话,通过这个去获取 mapper,获取到代理类执行即可找到对应的 mapper.xml文件,扫描 mapper 接口。生成 mapperProxyFactory 生成代理类,再执行具体的增删改查。

七、Redis 相关

1、redis的数据类型以及你们公司具体的应用

1、String(动态字符串):说是字符串但它的内部结构更像是一个 ArrayList,内部维护着一个字节数组,并且在其内部预分配了一定的空间,以减少内存的频繁分配:
	redis的内存分配机制:
		(1)当字符串的长度小于 1MB时,每次扩容都是加倍现有的空间。
		(2)如果字符串长度超过 1MB时,每次扩容时只会扩展 1MB 的空间。
		(3)字符串最大长度为 512MB。
		
	应用场景:计数器:string类型的incr和decr命令的作用是将key中储存的数字值加一/减一,这两个操作具有原子性,总能安全地进行加减操作,
	         因此可以用string类型进行计数,如微博的评论数、点赞数、分享数,抖音作品的收藏数,京东商品的销售量、评价数等。
	         
2、List(一个列表最多可以存储2^32 - 1个元素。):
	可以对列表两端插入(push)和弹出(pop),还可以获取指定范围的元素列表、获取指定索引下标的元素等。列表是一种比较灵活的数据结构,
	可以充当栈和队列的角色。
		列表类型有以下特点:
			(1)列表中的元素是有序的,即可以通过索引下标获取某个元素或者某个范围内的元素列表。
			(2)列表中的元素可以是重复的。
			(3)redis中的list底层可不是一个双向链表那么简单。当数据量较少的时候它的底层存储结构为一块连续内存,称之为ziplist(压缩列表),
			     它将所有的元素紧挨着一起存储,分配的是一块连续的内存;当数据量较多的时候将会变成quicklist(快速链表)结构。
			     
	应用场景:
		(1)消息队列:
			Redis的lpush + brpop命令组合即可实现阻塞队列,生产者客户端使用 lpush 从列表左侧插入元素,多个消费者客户端使用 brpop 命令阻塞式的
			争抢列表尾部的元素,多个客户端保证了消费的负载均衡和高可用;
		(2)最新列表:
			list类型的lpush命令和lrange命令能实现最新列表的功能,每次通过lpush命令往列表里插入新的元素,然后通过lrange命令读取最新的元素列表,
			如朋友圈的点赞列表、评论列表。	
			
3、hash(字典:Redis 中的 Hash和 Java的 HashMap 更加相似,都是数组+链表的结构,当发生 hash 碰撞时将会把元素追加到链表上,
        值得注意的是在 Redis 的 Hash 中 value 只能是字符串):
        
	应用场景:
		购物车:hset [key] [field] [value] 命令, 可以实现以用户Id,商品Id为field,商品数量为value,恰好构成了购物车的3个要素。
		存储对象:hash类型的(key, field, value)的结构与对象的(对象id, 属性, 值)的结构相似,也可以用来存储对象。
		
4、set(集合:Redis中的 set和Java中的HashSet 有些类似,它内部的键值对是无序的、唯一 的。它的内部实现相当于一个特殊的字典,
       字典中所有的value都是一个值 NULL。当集合中最后一个元素被移除之后,数据结构被自动删除,内存被回收。)
       
	应用场景:
		好友、关注、粉丝、感兴趣的人集合:
			(1)sinter命令可以获得A和B两个用户的共同好友;
			(2)sismember命令可以判断A是否是B的好友;
			(3)scard命令可以获取好友数量;
			(4)关注时,smove命令可以将B从A的粉丝集合转移到A的好友集合
		首页展示随机:美团首页有很多推荐商家,但是并不能全部展示,set类型适合存放所有需要展示的内容,而 srandmember 命令则可以从中随机获取几个。
		
		存储某活动中中奖的用户ID :因为有去重功能,可以保证同一个用户不会中奖两次。
		
5、zset(有序集合:zset也叫SortedSet一方面它是个 set ,保证了内部 value 的唯一性,另方面它可以给每个 value 赋予一个score,代表这个value的排序权重。
		它的内部实现用的是一种叫作“跳跃列表”的数据结构。)
		
	应用场景:
		zset 可以用做排行榜,但是和list不同的是zset它能够实现动态的排序,例如: 可以用来存储粉丝列表,value 值是粉丝的用户 ID,score 是关注时间,
		我们可以对粉丝列表按关注时间进行排序。
		zset 还可以用来存储学生的成绩, value 值是学生的 ID, score 是他的考试成绩。 我们对成绩按分数进行排序就可以得到他的名次。

2、redission的分布式锁原理

参考网址:https://www.cnblogs.com/crazymakercircle/p/14731826.html#autoid-h3-5-0-0

watchdog 的具体思路是:加锁时,默认加锁 30秒,每10秒钟检查一次,如果存在就重新设置 过期时间为30秒。

底层主要是通过 lua 脚本去完成原子性的命令操作。

注意:
(1)watchDog 只有在未显示指定加锁时间时才会生效。
(2)lockWatchdogTimeout设定的时间不要太小 ,比如我之前设置的是 100毫秒,由于网络直接导致加锁完后,watchdog去延期时,这个key在redis中已经被删除了。

3、redis为什么这么快?

1、redis是内存级操作。
2、redis是单线程,相比于多线程,CPU避免了上下文切换和锁的竞争消耗,以及IO多路复用和虚拟内存机制(冷热数据分离,冷数据从内存交到磁盘中,腾出内存处理热数据)

八、消息队列相关

九、Linux 相关

1、linux查看端口命令

netstat -ntlp——查看当前所有tcp端⼝
netstat -ntulp | grep 80——查看所有80端⼝使⽤情况

2、linux常用指令

cp
midir
cat xxx | grep “xx” | more
tail -f
kill
docker
sh xx
倒叙日志:tac

等等

3、linux杀死进程的指令、-9跟-15的区别

-9是强制杀死进程,立马停止;-15是通知程序安全干净的退出。

十、场景题

1、有没有遇到过超卖的情况,怎么解决?

参考网站:http://www.pingtaimeng.com/article/detail/id/2143325

商城设计的过程中,必然会考虑到一个库存扣除的问题,超卖将会带来一定的损失和麻烦,在某一个时间段的瞬间的流量会造成库存的并发性操作带来库存超额扣除。

1、流量较少时,可以采用锁定最后库存+乐观锁解决方案。但是缺点就是操作的时间内只能有一个用户操作成功,如果执行一个200ms,则其他用户等待时间会很长。

2、redis的队列来实现。将要促销的商品数量以队列的方式存入redis中,每当用户抢到一件促销商品则从队列中删除一个数据,确保商品不会超卖。这个操作起来很方便,而且效率极高。

3、分阶段排队下单方案(将提交操作变成两段式):
第一阶段申请,申请预减减库,申请成功之后,进入消息队列;
第二阶段确认,从消息队列消费申请令牌,然后完成下单操作。 查库存 -> 创建订单 ->扣减库存。通过分段锁保障解决多个provider实例并发下单产生的超卖问题。

3-1、申请阶段:
将存库从MySQL前移到Redis中,所有的预减库存的操作放到内存中,由于Redis中不存在锁故不会出现互相等待,并且由于Redis的写性能和读性能都远高于MySQL,这就解决了高并发下的性能问题。(通过redis自增或者自减操作来实现原子性)

3-2、确认阶段:
然后通过队列等异步手段,将变化的数据异步写入到DB中。引入队列,然后数据通过队列排序,按照次序更新到DB中,完全串行处理。当达到库存阀值的时候就不在消费队列,并关闭购买功能。这就解决了超卖问题。

但是下单阶段的性能比较差,如何提升性能呢?

可以使用Redis 分布式锁。为了达到每秒600个订单,可以将锁分成 600 /5 =120 个段,每个段负责5个订单,600个订单,在第二个阶段1秒钟下单完成。

该方案优点:解决超卖问题,库存读写都在内存中,故同时解决性能问题。
该方案缺点:数据不一致。由于异步写入DB,可能存在数据不一致,存在某一时刻DB和Redis中数据不一致的风险。可能存在少买,也就是如果拿到号的人不真正下订单,可能库存减为0,但是订单数并没有达到库存阀值。

2、sku跟spu的区别

计量单位不同、描述特性不同、产品分类不同。

1、计量单位不同:
SPU = Standard Product Unit (标准产品单位),SPU是商品信息聚合的最小单位;

SKU=stock keeping unit(库存量单位),SKU即库存进出计量的单位。

2、描述特性不同:
SPU是一组可复用、易检索的标准化信息的集合,该集合描述了一个产品的特性;

SKU的计量可以是以件、盒、托盘等为单体,就是物理上不可分割的最小存货单元。

3、产品分类不同:

SPU描述的就是属性值、特性相同的商品。例如:iphone4就是一个SPU,与商家,与颜色、款式、套餐都无关。

SKU在使用时要根据不同业态,不同管理模式来处理。在服装、鞋类商品中使用最多最普遍。例如:纺织品中一个SKU通常表示:规格、颜色、款式。

3、假设现在需要调用第三方接口,这个接口限制每秒请求5次,我们的系统需求是对这个接口调用每秒100次,你怎么解决这个问题?

redis令牌桶或者mq队列

4、假设现在有一万个数,你要对这些数进行累加,每个加法耗时0.5秒,你怎么样以最小的时间把累加的值算出来?

这里考的是线程如何分配最优

多线程,由于是cpu计算密集型,cpu核心数 * cpu利用率 * (1+ 线程等待时间/线程计算时间)

5、假设接到一个项目,某些部分的接口,比如 controller,在 jar 包里面,我们只有class,没有其他,要对他原有的功能进行修改,你怎么做?

通过反编译软件 jd-gui 反编译得到反编译文件,然后去修改文件。

还有种非反编译操作:
新建一个包,使其满足该文件所需的包名路径。编写修改好的java文件–>编译该java文件为class文件–>解压jar包–>找打待修改文件的class文件并将其替换–>压缩源码文件,并改为jar后缀格式。随后解压该jar包,替换目标class文件,再次压缩回为jar文件即可!大功告成!

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值