面试题整合(2023)

192 阅读20分钟

前言:记录遇到/见到的一些面试题,自行查阅总结的答案,不一定完全准确,欢迎指正。


Memcached

Memcached是一种高性能、分布式的内存对象缓存系统,将数据存储在内存中, 便于快速访问和检索数据,从而提高Web应用程序的性能和响应速度,支持跨平台。

集群下可能存在问题和解决

  1. 单点宕机或故障:一个节点出现宕机,导致整个系统集群可用性受影响;
    1. 可以通过增加备份节点来解决
  2. 数据不一致问题:因为Memcached是分布式系统的,写入数据可能存在数据不一致的问题。
    1. 可以将读写限制在单节点上,避免并发写操作导致不一致
    2. 可以使用复制、同步机制来保证一致性
  3. 性能瓶颈:可以对节点分片、数据压缩等操作。

微服务系统下可能存在问题和解决

  1. 数据不一致问题:多个服务拥有自己的缓存副本,可能导致数据不一致
    1. 使用事务、分布式锁、版本号来保证数据一致性
    2. 也可以使用复制、同步等方式
  2. 单节点并发瓶颈问题:并发请求过大,集中式单节点存储瓶颈
    1. 可以使用分布式缓存,分散到多个节点上,提高性能
    2. 缓存雪崩或缓存穿透问题:缓存大量同时失效或缓存未命中,导致请求直接访问数据库。
    3. 可以使用缓存预热、缓存错误数据等方式

Shiro

简介

Shiro是一个Java的安全框架,提供身份认证、授权、加密和会话管理等功能,可以帮助开发人员快速实现安全性较高的应用程序。

核心组件以及作用

核心组件包含:

  1. Subject:主体,代表当前操作用户
  2. SecurityManager:安全管理器,Shiro的核心,主要通过安全管理器来管理内部的组件,通过它来提供安全管理的各种服务
  3. Realm:领域,充当Shiro和应用数据之间的桥梁。当对用户进行身份认证、授权时,Shiro会从应用配置的Realm中查找用户及其权限信息

认证过程、授权过程

认证过程:

  1. 用户提交登录请求,Shiro会将用户名和密码封装成一个AuthenticationToken对象
  2. SecurityManager接收到AuthenticationToken对象后,会根据其中的用户名查找对应的Realm
  3. Realm用于从数据源(例如数据库)中获取用户信息,并进行身份认证。如果认证成功,就返回一个表示该用户身份的AuthenticationInfo对象。
  4. SecurityManager将AuthenticationToken和AuthenticationInfo对象交给Authenticator进行验证,Authenticator根据其规则对两个对象进行比较。如果匹配成功,则认为该用户身份验证通过。

授权过程:

  1. 当需要进行授权操作时,Subject会提交一个代表自己的Principal(即用户名或者其他唯一标识符)
  2. SecurityManager接收到Principal后,会根据其中的用户名查找对应的Realm,并从Realm中获取与该用户相关的角色和权限信息。
  3. 如果角色和权限信息与请求操作相匹配,则表示该用户有权执行该操作,授权成功;否则授权失败。

资源过滤类型

  • anno:不需要认证即可请求
  • authc:必须认证才能请求
  • user:身份认证通过后并且选择记住我,可以请求
  • perms:必须有资源权限才能请求
  • role:必须有角色权限才能请求

Spring、SpringBoot、SpringMVC

请求从前端到后端的过程

  1. 域名解析得到IP地址
  2. 请求IP地址服务器,经过网关/负载均衡到达具体的应用服务器
  3. 前端控制器接受到请求,转发到处理器映射器
  4. 处理器映射器根据请求找到处理请求的controller信息返回给前端控制器
  5. 前端控制器拿到返回的信息转发到处理器适配器,将请求转发到具体的Controller进行执行
  6. 执行业务逻辑后,返回ModelAndView给前端控制器
  7. 前端控制器将ModelAndView传给视图解析器进行解析返回View
  8. 前端控制器将View渲染返回给前端客户端

静态属性注入

  1. 在静态属性的set方法上使用@Value注解
  2. 使用@ConfigurationProperties注解指定配置文件的前缀,同时静态属性需要有set方法
  3. 在@PostConstruct注解的方法内对静态属性进行赋值

Bean的作用域

  • Singleton
  • prototype
  • request
  • session
  • global session

AOP通知类型

  1. 前置通知:@Before,在目标方法之前执行
  2. 最终通知:@After,在目标方法之后执行,异常也会执行
  3. 后置通知:@AfterReturnning,在目标方法之后执行,异常不会执行
  4. 异常通知:@AfterThrowing,在目标方法抛出异常之后执行
  5. 环绕通知:@Around,在目标方法调用之前和之后执行

SpringBoot配置文件执行顺序

  • bootstrap>application
  • properties>yaml
  • 项目同级目录config目录>项目同级目录>项目resource目录下的config目录>项目resource目录

事务失效情况

  1. 数据库引擎不支持事务
  2. 注解所在的类没有被SpringIOC容器所管理
  3. 出现了自调用
  4. 手动try catch了异常
  5. 方法是非public修饰
  6. 捕获的异常类型不对(使用rollbackFor=Exception.class)

Spring中的设计模式

  • 单例模式:Bean
  • 工厂模式:Bean工厂
  • 代理模式:AOP
  • 适配器模式
  • 模板模式:JDBCTemplate

SpringBoot

SpringBoot是Spring组件一站式的解决方案,集成很多开发框架,使开发者可以快速搭建Spring项目。简化了Spring的大量配置文件问题,可以快速创建一键运行的Spring应用。

内置Tomcat服务器,不需要单独部署war包;

提供各种Starter启动器,开箱即用;

自动配置

常用注解:

@SpringBootApplication

@Component、@Service、@Controller、@Mapper、@Repository、@ResponseBody、@RequestBody、@RestController、@Autowired、@Resource、@Qulifier、@RequestMapping、@GetMapping、@PostMapping、@Value、@ConfigurationProperties、@Import、@Configuration、@Bean


SpringCloud

组件及作用

  1. Nacos--服务发现与注册--注册中心、配置中心
  2. Ribbon--负载均衡
  3. Feign--声明式的HTTP客户端--远程调用
  4. Sentinel--服务容错(限流、降级、熔断)
  5. GateWay--API网关
  6. Sleuth--调用链监控
  7. Seata--分布式事务.

Feign的原理

Feign底层依赖于Java的动态代理机制,声明式的http Client相对于编程式http代码逻辑更简洁;集中管理HTTP请求方法,代码边界清晰;Feign是一个声明式的、基于注解的HTTP客户端库,用于简化使用RestFul服务的调用。

  1. 声明式接口定义:使用Feign,首先需要定义一个接口,用于描述要调用的远程服务的API。接口中的方法使用注解来标识请求的URL、HTTP方法、请求参数等信息。
  2. 生成动态代理:在应用启动时,Feign会根据定义的接口生成一个动态代理对象。代理对象实现接口中的方法,并将方法调用转发到底层的HTTP请求。
  3. 请求发送和编码:当调用接口的方法时,Feign根据方法上的注解信息,构建HTTP请求,并将请求发送到目标服务。同时,Feign负责将Java对象序列化为请求内容(JSON格式)
  4. 请求的执行和响应解码:目标服务接收到请求后,执行响应的业务逻辑,并响应返回。Feign负责接收响应,并将响应内容解码为Java对象。
  5. 错误处理和异常传播:如果请求发生错误,feign会进行适当的错误处理。根据配置和响应的内容,feign可以抛出相应的异常,供调用方进行处理。

工作原理是基于Java的动态代理和注解处理技术。通过使用注解描述API接口和请求信息,Feign可以在运行时生成代理对象,并将方法调用转化为HTTP请求。

服务熔断、服务降级、服务限流

熔断:当服务的错误率超过一定的阈值时,熔断器会自动断开服务的调用,防止错误的服务继续对系统造成负载压力,从而保证整个系统的可用性。如A服务调用B服务,B服务卡机导致时间超长,直接断路B,不再请求B。

限流:是一种控制流量的手段,通过设置最大并发数、最大请求数等方式,保证系统在高并发场景下不会被过多的请求拖垮。流量控制,使服务能够承受不超过自己能力的流量压力。

降级:指通过切换到备用方案来保证服务可用,例如使用缓存或使用降级接口等方式。如流量高峰期,服务器压力剧增,对一些服务和页面做策略的降级,所有调用直接返回降级数据。

熔断:Hystrix,通过维护一个计数器,记录服务的错误率,当错误率超过一定阈值时,熔断器会自动断开服务的调用,防止错误的服务继续对系统造成负载压力,从而保证整个系统的可用性。

限流:Sentinel

降级:Spring Cloud Circuit Breaker、Netflix Hystrix

熔断和降级的区别:

相同:都是为了保证集群大部分服务的可用性和可靠性,防止崩溃;用户最终体验都是某个功能不可用

不同:熔断是被调用方故障,触发的系统主动规则;降级是基于全局考虑,停止一些正常服务,释放资源。

RPC和HTTP的区别

http是指从客户端到服务器端的请求消息,rpc是远程过程调用协议,HTTP请求是使用具有标准语义的通用的接口定向到资源的,这些语义能够被中间组件和提供服务的来源机器进行解析。

Rpc的机制是根据语言的API来定义,而不是根据基于网络的应用来定义的。

RPCHTTP
传输协议可以基于TCP,也可以HTTP基于HTTP协议
传输效率使用自定义的TCP协议,可以让请求报文体积更小;使用HTTP2协议,可以很多的减少报文体积,提高传输效率基于HTTP1.1的协议,请求会包含很多无用的内容;基于HTTP2,简单的封装一下可以作为RPC来使用
性能消耗可以基于thrift实现高效的二进制传输大部分通过json来实现,字节大小和序列化耗时
负载均衡自带需要配置Nginx
服务治理自动通知,不影响上游需要事先通知
场景内部的服务调用,性能消耗低,传输效率高,服务治理方便对外的异构环境,浏览器接口调用,APP接口调用,第三方接口调用

Redis

使用Redis存一个有效时间值

@Autowired
private StringRedisTemplate redisTemplate;

redisTemplate.opsForValue().set(K key, V value, long timeout,TimeUnit unit)

分布式锁

Redis分布式锁是一种基于Redis实现的分布式锁,可以保证多个进程或线程在同时访问共享资源时只有一个能够获得锁。

实现方式:

  1. setNX +过期删除
  2. Redisson
  3. 数据库唯一索引做分布式锁

数据类型

  • 字符串String
  • 列表List
  • 集合Set
  • 哈希Hash
  • 有序集合Zset
  • 基数统计HyperLogLogs
  • 位图BitMaps
  • 地理位置Geospatial

内存淘汰策略

  • 从已设置过期时间结果集中选择最少使用的数据
  • 从已设置过期时间结果集中选择将要过期的数据
  • 从已设置过期时间结果集中随机选择
  • 移除最少使用的
  • 随机移除
  • 内存满了,直接拒绝写入

持久化机制

  • RDB:占用空间小、存储速度慢、恢复速度快、会丢失数据、启动优先级低

将内存中的数据写入到磁盘中,通过fork一个子进程来生成RDB文件

  • AOF:占用空间大、存储速度快、恢复速度慢、启动优先级高

以独立日志的方法记录每次写命令,重启时会重新执行AOF文件中的命令达到恢复数据目的

为什么快

  • 纯内存数据库,比磁盘IO读取存储要快
  • 使用的是非阻塞IO、IO多路复用,使用单线程来轮询描述符,将数据库的开、关、读、写都转换成了事件,减少了线程切换时上下文的切换和竞争
  • 采用单线程的模型,保证操作的原子性,减少了线程的上下文切换和竞争
  • 避免了多线程的锁消耗

Java基础

Java8新特性

  • lambda表达式:是一种匿名函数,可以将其作为参数传递,也可以作为方法返回值
  • Steam API:提供一种新的处理集合数据的方式,可以方便地进行过滤、映射、排序等操作。
  • 允许在接口中定义默认方法和静态方法,可以在实现类中重写
  • Optional类
  • 新的时间和日期API

动态代理

  1. JDK动态代理

需要实现接口(invocationHanlder),在invoke方法定义增加内容和调用目标对象的方法,通过Proxy.newProxyInstance(目标对象.classLoder,目的对象.interfaces,代理对象)来创建代理对象实例。

  1. CGLIB动态代理

需要引入第三方依赖,不能用于增强声明final的类或方法,因为底层是使用字节码技术生成被代理类的子类。实现MethodInterceptor接口,通过intercept方法来增强。通过CglibProxy().getProxy(目标对象.class)来获取代理对象。

HashMap

jdk1.8-jdk1.8+
存储结构数组+链表数组+链表+红黑树
初始化方法单独的方法:inflateTable()集成到扩容函数:resize()
Hash值计算4次位运算+5次异或运算一次位运算+一次异或运算
存储数据无冲突直接存数组;冲突存链表无冲突,直接存数组; 有冲突并且链表长度小于8,存链表; 有冲突并且长度大于8时,转红黑树存入。
插入数据头插法尾插法
扩容后存储位置全部按照原来的方式重新计算按照扩容后的规律,扩容后位置=原位置/原位置+旧容量
实现采用拉链法,将链表和数组结合。创建一个链表数组,数组每一格都是一个链表,遇到冲突将元素加到链表中。主要在解决冲突上有了变化。当链表的长度大于8,元素个数大于64,会转为红黑树,以减少搜索时间。
Put()计算Key的hashCode,HashCode高低位异或运算得到hash值,再通过hash&(len -1)得到存入的下标(索引); 如果该索引不存在元素,直接存入;...
Get()通过hash()计算得到具体的索引位置,遍历链表/红黑树,找到和键值相等的元素位置。
扩容机制空参数的构造函数:以默认容量、负载因子、阈值初始化数组,内部是空数组。 有参构造函数:按照指定的参数。第一次Put时会初始化数组,其容量不小于指定容量的2 的幂数,根据负载因子计算阈值。扩容是2n空参数的构造函数:实例化的HashMap默认为null,即没有实例化。第一次调用put才会进行扩容,默认16。 有参构造:按照指定的参数。第一次Put时会初始化数组,其容量不小于指定容量的2 的幂数,根据负载因子计算阈值。扩容是2n 首次Put,先触发扩容(初始化),存入数据后判断是否需要扩容

Synchronized和volatile

Volatile是线程同步的轻量级实现,性能比synchronized要好,只能应用于变量

多线程访问volatile不会发生阻塞,volatile主要解决变量在多线程之间的可见性

只可以保证数据的可见性,不能保证数据的原子性

Synchronized可以应用变量、方法、代码块

Synchronized解决的是多线程之间访问资源的同步性

可以保证可见性和原子性

锁的类型

  • 悲观锁:synchronized、接口Lock的实现类
  • 乐观锁:CAS、版本号控制
  • 自旋锁:自选锁即是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待(不放弃CPU 资源),然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环,此时获取锁的线程一直处于活跃状态(而非阻塞)
  • 读写锁
  • 可重入锁

Java引用类型

  • 强引用:jvm宁愿oom也不会回收的对象
  • 软引用:内存不足时会尝试回收
  • 弱引用:会被垃圾收集器直接回收,最常见的就是ThrealLocal.
  • 虚引用:可以用来判断对象是否被回收

为什么不建议使用Executors来创建线程池

Executors创建线程池,用的是new ThreadPoolExecutor来创建,SingleThreadExecutor和FixedThreadPool时用的无界队列,如果任务过多一致往队列里放,占用的内存增多,最终可能导致内存溢出。同时不能自定义线程名字,不方便排查问题,应该使用new ThreadPoolExecutor来定义。

线程池的拒绝策略

  1. 直接抛出异常,阻止系统正常运行,默认策略
  2. 直接将任务交给调用者线程进行执行,性能可能造成下降
  3. 丢弃最老的一个请求任务,并尝试重新提交当前任务
  4. 默默丢弃,不做任何响应,允许丢失时可用。

序列化和反序列化

序列化:把对象转换为字节序列的过程

反序列化:把字节序列恢复为对象的过程

序列化针对对象而言,static属性属于类,因此不会被序列化。

只要我们对内存中的对象进行持久化或网络传输,就需要序列化和反序列化。

JSON格式实际上就是将一个对象转化为字符串,服务器和浏览器交互的数据格式JSON就是字符串,而String类实现了Serializable接口,并显示指定SerialVersionUID,而持久化数据库的实体属性也是同样是实现了接口。没有实现,进行序列化可能抛出异常NotSerializableException。

不显式指定UID,JVM在序列化时会根据属性自动生成一个,然后和属性一起序列化,再进行持久化或网络传输;反序列化时,JVM再根据数据生成一个新的ID进行和旧的比较,一致则反序列化成功。如果显式指定,JVM在序列化和反序列化依然会生成,但是是我们指定的值,自然就一致了。


消息队列

如何保证消息的可靠性传输

  • 生产者:使用Confirm机制,实现ConfirmCallBack接口
  • RabbitMQ:将队列和消息设置为持久化
  • 消费者:手动ACK确认机制

MySQL

in和exist的区别

exist子查询不直接返回结果集,返回true或false

in直接返回结果集,主查询再去结果集中查询符合条件的进行输出

查询慢以及优化

慢的可能原因:

  • 没有索引或者没有使用到索引
  • I/O吞吐量小
  • 内存不足,网络速度慢
  • 查询的数据量大
  • 锁或者死锁
  • 返回不必要的行和列

解决方案:

  • 纵向、横向的分割表,减小表的尺寸
  • 升级硬件
  • 根据查询条件,建立索引,优化索引,限制访问结果集的数据量
  • 优化SQL语句
    • SELECT避免*
    • 使用Where代替having
    • 多表连接使用别名
    • 用exist代替in、not exist代替 not in
    • 用exist代替distinct
    • 避免索引列使用NOT、函数、运算
    • 用>=代替>
    • 用in代替or
    • 避免索引列使用IS NULL或IS NOT NULL
    • 避免使用<> 和 !=
    • 为搜索字段建立索引

索引

  • 哈希索引:采用一定的哈希算法,把键值换算成新的哈希值,检索时不需要像B+树从根节点开始遍历,只需要一次哈希算法即可,是无序的。优势在于等值查询,但是如果重复键较多时,哈希冲突比较严重。不适合需要范围查询、排序查询的场景。
  • B树索引:B树是一种多路平衡树,使用这种存储结构来存储大量数据,整个高度比二叉树要矮。树的高度能决定磁盘IO的次数,磁盘IO次数越少,性能的提升也越大,因此比二叉树更适合。
  • B+树索引:所有数据都存储在叶子节点上,非叶子节点只存储索引。叶子结点中的数据使用双向链表的方式进行关联。升序排序
    • B+树的非叶子节点不存储数据,所以树的每一层就能够存储更多的索引(键值)数量,也就是说B+树在层高相同的情况下,比B树存储的数据量更多,间接地减少磁盘IO次数。
    • 针对范围查询的情况,B+树遍历全部数据只需要遍历叶子节点,而B树则需要遍历整棵树,因此范围查询/全表查询B+树更高效率。
    • 适合范围查询、支持联合索引的最左侧原则、支持order by排序、可以使用like模糊查询,以上哈希都不行

行级锁

优点:只锁定行,多线程访问冲突概率小;回滚更改的数据也少;可以长时间锁定单一的行

缺点:加锁速度慢;占用更多的内存

千万级表优化

  • 读写分离
  • 水平拆分
  • 垂直拆分
  • 性能优化
    • 硬件和操作系统层面:CPU、内存大小、磁盘读写速度、网络带宽
    • 架构设计层面:主从集群、读写分离、分库分表、热点数据使用缓存数据库
    • SQL优化

CPU飙升

可能是因为创建过多的线程,有线程一直占用CPU资源无法被释放,如死循环

死锁

使用jstack导出dump日志,分析日志定位具体死锁的代码。

死锁条件:互斥、不可剥夺、循环等待、请求和保持

避免死锁:

  • 设置事务等待锁的超时时间(innodb_lock_wait_timeout)
  • 开启主动死锁检查(innodb_deadlock_detect)
  • 对于更新频繁的字段,采用唯一索引

磁盘打满如何处理

  • 备份
  • 清理日志文件
  • 压缩表格
  • 删除不必要的数据
  • 增加磁盘空间

Linux

常用命令

文件和目录:cd\pwd\ls\cp\mv\rm\mkdir\

查看文件:cat\head\tail

搜索:find -name \whereis

文件权限:chmod\chown\chgrp

文本处理:grep

打包解压:tar

系统和关机:shutdown\reboot\logout

进程相关:top\jps\ps\kill\killall


ElasticSearch

底层原理

倒排索引,将数据按分词器进行拆分多个词语,将数据Id和词语做关联关系。