被上海某小厂,严格拷打一小时!
The following article is from 小林coding Author 小林coding
文 | 小林coding
出品 | 小林coding(ID:CodingLin )
已获得原公众号的授权转载
之前分享很多大厂的面经,这次分享一家上海某小厂的 Java 岗位面试,面试的时间也挺长的,接近 1 个小时,无算法,全程抓着项目+mysql+redis+java这 4 个方向问。
问题记录
项目
介绍你的项目
这个项目是企业里面做的还是学校的项目?
说说你在这个项目负责了那些,应用了那些技术,你学到了什么?
你说你用到了 redis 分布式锁,为何采用 redis 分布式锁啊,redis 分布锁有啥好处?
你用过线程池吗? 谈谈线程池的好处?
在这个项目中线程池参数怎么配置的?
你这个项目是根据 io密集型 还是 CPU密集型 计算?
MySQL
说说mysql的事务隔离级别有哪几种?
读未提交,指一个事务还没提交时,它做的变更就能被其他事务看到; 读提交,指一个事务提交之后,它做的变更才能被其他事务看到; 可重复读,指一个事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的,MySQL InnoDB 引擎的默认隔离级别; 串行化;会对记录加上读写锁,在多个事务对这条记录进行读写操作时,如果发生了读写冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行;
按隔离水平高低排序如下:
说说 mvcc 机制?
我们需要了解两个知识:
Read View 中四个字段作用; 聚簇索引记录中两个跟事务有关的隐藏列;
Read View 有四个重要的字段:
m_ids :指的是在创建 Read View 时,当前数据库中「活跃事务」的事务 id 列表,注意是一个列表,“活跃事务”指的就是,启动了但还没提交的事务。 min_trx_id :指的是在创建 Read View 时,当前数据库中「活跃事务」中事务 id 最小的事务,也就是 m_ids 的最小值。 max_trx_id :这个并不是 m_ids 的最大值,而是创建 Read View 时当前数据库中应该给下一个事务的 id 值,也就是全局事务中最大的事务 id 值 + 1; creator_trx_id :指的是创建该 Read View 的事务的事务 id。
对于使用 InnoDB 存储引擎的数据库表,它的聚簇索引记录中都包含下面两个隐藏列:
trx_id,当一个事务对某条聚簇索引记录进行改动时,就会把该事务的事务 id 记录在 trx_id 隐藏列里; roll_pointer,每次对某条聚簇索引记录进行改动时,都会把旧版本的记录写入到 undo 日志中,然后这个隐藏列是个指针,指向每一个旧版本记录,于是就可以通过它找到修改前的记录。
在创建 Read View 后,我们可以将记录中的 trx_id 划分这三种情况:
一个事务去访问记录的时候,除了自己的更新记录总是可见之外,还有这几种情况:
如果记录的 trx_id 值小于 Read View 中的 min_trx_id
值,表示这个版本的记录是在创建 Read View 前已经提交的事务生成的,所以该版本的记录对当前事务可见。如果记录的 trx_id 值大于等于 Read View 中的 max_trx_id
值,表示这个版本的记录是在创建 Read View 后才启动的事务生成的,所以该版本的记录对当前事务不可见。如果记录的 trx_id 值在 Read View 的min_trx_id和max_trx_id之间,需要判断 trx_id 是否在 m_ids 列表中: 如果记录的 trx_id 在 m_ids
列表中,表示生成该版本记录的活跃事务依然活跃着(还没提交事务),所以该版本的记录对当前事务不可见。如果记录的 trx_id 不在 m_ids
列表中,表示生成该版本记录的活跃事务已经被提交,所以该版本的记录对当前事务可见。
这种通过「版本链」来控制并发事务访问同一个记录时的行为就叫 MVCC(多版本并发控制)。
mvcc 会加锁吗?
不会,快照读,
mvcc 解决了幻读吗?
MySQL InnoDB 引擎的默认隔离级别虽然是「可重复读」,但是它很大程度上避免幻读现象(并不是完全解决了),解决的方案有两种:
针对快照读(普通 select 语句),是通过 MVCC 方式解决了幻读,因为可重复读隔离级别下,事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的,即使中途有其他事务插入了一条数据,是查询不出来这条数据的,所以就很好了避免幻读问题。 针对当前读(select ... for update 等语句),是通过 next-key lock(记录锁+间隙锁)方式解决了幻读,因为当执行 select ... for update 语句的时候,会加上 next-key lock,如果有其他事务在 next-key lock 锁范围内插入了一条记录,那么这个插入语句就会被阻塞,无法成功插入,所以就很好了避免幻读问题。
这两个解决方案是很大程度上解决了幻读现象,但是还是有个别的情况造成的幻读现象是无法解决的。
比如这个场景:
在可重复读隔离级别下,事务 A 第一次执行普通的 select 语句时生成了一个 ReadView,之后事务 B 向表中新插入了一条 id = 5 的记录并提交。接着,事务 A 对 id = 5 这条记录进行了更新操作,在这个时刻,这条新记录的 trx_id 隐藏列的值就变成了事务 A 的事务 id,之后事务 A 再使用普通 select 语句去查询这条记录时就可以看到这条记录了,于是就发生了幻读。
因为这种特殊现象的存在,所以我们认为 MySQL Innodb 中的 MVCC 并不能完全避免幻读现象。
mysql 默认的隔离界别是可重复读,那是如何保证可重复读的呢?
可重复读隔离级别是启动事务时生成一个 Read View,然后整个事务期间都在用这个 Read View。
读已提交又是啥啊, 怎么保证的?
读提交隔离级别是在每次读取数据时,都会生成一个新的 Read View。也意味着,事务期间的多次读取同一条数据,前后两次读的数据可能会出现不一致,因为可能这期间另外一个事务修改了该记录,并提交了事务。
说说你对索引的理解?
索引是数据库中一种用于提高查询性能的数据结构。它类似于书籍的目录,可以帮助数据库系统快速地定位和访问数据。通过在数据库表的一个或多个列上创建索引,可以加快查询速度,减少数据扫描的时间和成本。
索引的作用是通过建立一个索引文件,将索引列的值与对应的数据记录进行映射关联。当进行查询时,数据库系统可以根据索引文件快速定位到符合查询条件的数据记录,而无需逐条扫描整个表。这样可以大大减少查询的时间复杂度,提高查询效率。
索引的创建需要占用额外的存储空间,并且在数据更新时需要维护索引结构,可能会增加写操作的开销。因此,索引的使用需要权衡查询频率和数据更新频率之间的平衡。对于经常进行查询的字段,可以考虑创建索引;而对于更新频率较高的字段,或者区分度较小的字段,创建索引可能效果有限,甚至会影响性能。
联合索引在b+树怎么表示的?在对数据排序时,什么时候会根据第二个字段排序?
通过将多个字段组合成一个索引,该索引就被称为联合索引。
比如,将商品表中的 product_no 和 name 字段组合成联合索引(product_no, name)
,创建联合索引的方式如下:
CREATE INDEX index_product_no_name ON product(product_no, name);
联合索引(product_no, name)
的 B+Tree 示意图如下(图中叶子节点之间我画了单向链表,但是实际上是双向链表,原图我找不到了,修改不了,偷个懒我不重画了,大家脑补成双向链表就行)。
可以看到,联合索引的非叶子节点用两个字段的值作为 B+Tree 的 key 值。当在联合索引查询数据时,先按 product_no 字段比较,在 product_no 相同的情况下再按 name 字段比较。
也就是说,联合索引查询的 B+Tree 是先按 product_no 进行排序,然后再 product_no 相同的情况再按 name 字段排序。
因此,使用联合索引时,存在最左匹配原则,也就是按照最左优先的方式进行索引的匹配。在使用联合索引进行查询的时候,如果不遵循「最左匹配原则」,联合索引会失效,这样就无法利用到索引快速查询的特性了。
索引底层数据是有序排序的,你知道它是怎样有序排序的原理吗?
二叉树会比较数据,从上至下以此分配和排序数据
一条sql语句执行很慢,你怎么排查?
开启慢查询日志,定位到出现问题的sql的语句
如果 SQL 和索引都没问题,查询还是很慢怎么办?
大查询改造为分批查询 数据库分表,降低数据库表的数量 引入redis缓存 ,减少 mysql 的访问
在 mybatis-plus 有封装好的分页配置类,如果是在mysql 的话,你怎么写分页语句的?
深分页如何优化?
如果查询出来的结果集,存在连续且递增的字段,可以基于有序字段来进行查询,例如 select xxx from book where 有序字段 >= 1 limit 100
舍弃limit关键字,如果查询出来的结果集存在连续且递增的字段,使用between and来进行范围结果集查询,例如 select xxx from book where 有序字段 between 10000000 and 1000100
采用MongoDB、ES搜索引擎优化深分页
导致索引失效有哪些?
当我们使用左或者左右模糊匹配的时候,也就是 like %xx
或者like %xx%
这两种方式都会造成索引失效;当我们在查询条件中对索引列使用函数,就会导致索引失效。 当我们在查询条件中对索引列进行表达式计算,也是无法走索引的。 MySQL 在遇到字符串和数字比较的时候,会自动把字符串转为数字,然后再进行比较。如果字符串是索引列,而条件语句中的输入参数是数字的话,那么索引列会发生隐式类型转换,由于隐式类型转换是通过 CAST 函数实现的,等同于对索引列使用了函数,所以就会导致索引失效。 联合索引要能正确使用需要遵循最左匹配原则,也就是按照最左优先的方式进行索引的匹配,否则就会导致索引失效。 在 WHERE 子句中,如果在 OR 前的条件列是索引列,而在 OR 后的条件列不是索引列,那么索引会失效。
or 一定会导致索引失效吗?,分析例如 where a = 1 or b = 1, 三种情况,a和b是联合索引,a是主键索引 b不是索引,a不是索引,b是主键索引
不一定,如果 or 左右两个字段都是索引,就能走索引。
如果 a 和b 是联合索引,会发生索引失效,对于联合索引(比如 bc),如果使用了 b =xxx or c=xxx,会走不了索引。
模糊查询是哪些情况导致索引失效啊?
左模糊和全模糊查询
为什么左模糊和全模糊查询会失效啊?
因为索引 B+ 树是按照「索引值」有序排列存储的,只能根据前缀进行比较。
举个例子,下面这张二级索引图(图中叶子节点之间我画了单向链表,但是实际上是双向链表,原图我找不到了,修改不了,偷个懒我不重画了,大家脑补成双向链表就行),是以 name 字段有序排列存储的。
假设我们要查询 name 字段前缀为「林」的数据,也就是 name like '林%'
,扫描索引的过程:
首节点查询比较:林这个字的拼音大小比首节点的第一个索引值中的陈字大,但是比首节点的第二个索引值中的周字小,所以选择去节点2继续查询; 节点 2 查询比较:节点2的第一个索引值中的陈字的拼音大小比林字小,所以继续看下一个索引值,发现节点2有与林字前缀匹配的索引值,于是就往叶子节点查询,即叶子节点4; 节点 4 查询比较:节点4的第一个索引值的前缀符合林字,于是就读取该行数据,接着继续往右匹配,直到匹配不到前缀为林的索引值。
如果使用 name like '%林'
方式来查询,因为查询的结果可能是「陈林、张林、周林」等之类的,所以不知道从哪个索引值开始比较,于是就只能通过全表扫描的方式来查询。
Redis
redis有哪些基本数据结构?
Redis 提供了丰富的数据类型,常见的有五种数据类型:String(字符串),Hash(哈希),List(列表),Set(集合)、Zset(有序集合)。
随着 Redis 版本的更新,后面又支持了四种数据类型:BitMap(2.2 版新增)、HyperLogLog(2.8 版新增)、GEO(3.2 版新增)、Stream(5.0 版新增)。Redis 五种数据类型的应用场景:
String 类型的应用场景:缓存对象、常规计数、分布式锁、共享 session 信息等。 List 类型的应用场景:消息队列(但是有两个问题:1. 生产者需要自行实现全局唯一 ID;2. 不能以消费组形式消费数据)等。 Hash 类型:缓存对象、购物车等。 Set 类型:聚合计算(并集、交集、差集)场景,比如点赞、共同关注、抽奖活动等。 Zset 类型:排序场景,比如排行榜、电话和姓名排序等。
Redis 后续版本又支持四种数据类型,它们的应用场景如下:
BitMap(2.2 版新增):二值状态统计的场景,比如签到、判断用户登陆状态、连续签到用户总数等; HyperLogLog(2.8 版新增):海量数据基数统计的场景,比如百万级网页 UV 计数等; GEO(3.2 版新增):存储地理位置信息的场景,比如滴滴叫车; Stream(5.0 版新增):消息队列,相比于基于 List 类型实现的消息队列,有这两个特有的特性:自动生成全局唯一消息ID,支持以消费组形式消费数据。
你了解过 redis 持久化机制?
Redis 的读写操作都是在内存中,所以 Redis 性能才会高,但是当 Redis 重启后,内存中的数据就会丢失,那为了保证内存中的数据不会丢失,Redis 实现了数据持久化的机制,这个机制会把数据存储到磁盘,这样在 Redis 重启就能够从磁盘中恢复原有的数据。
Redis 共有三种数据持久化的方式:
AOF 日志:每执行一条写操作命令,就把该命令以追加的方式写入到一个文件里; RDB 快照:将某一时刻的内存数据,以二进制的方式写入磁盘; 混合持久化方式:Redis 4.0 新增的方式,集成了 AOF 和 RBD 的优点;
说说缓存击穿?
我们的业务通常会有几个数据会被频繁地访问,比如秒杀活动,这类被频地访问的数据被称为热点数据。
如果缓存中的某个热点数据过期了,此时大量的请求访问了该热点数据,就无法从缓存中读取,直接访问数据库,数据库很容易就被高并发的请求冲垮,这就是缓存击穿的问题。
可以发现缓存击穿跟缓存雪崩很相似,你可以认为缓存击穿是缓存雪崩的一个子集。
应对缓存击穿可以采取前面说到两种方案:
互斥锁方案,保证同一时间只有一个业务线程更新缓存,未能获取互斥锁的请求,要么等待锁释放后重新读取缓存,要么就返回空值或者默认值。 不给热点数据设置过期时间,由后台异步更新缓存,或者在热点数据准备要过期前,提前通知后台线程更新缓存以及重新设置过期时间;
Java
你对 ArrayList 了解多少?
ArrayList是Java中的一个动态数组类,它实现了List接口。它的特点是可以根据需要自动扩展和收缩数组的大小,以便容纳不同数量的元素。
ArrayList可以存储任意类型的对象,包括基本类型的包装类。它提供了一系列方法来操作和访问数组中的元素,比如添加元素、删除元素、获取元素、遍历数组等。
ArrayList的内部实现是基于数组,当添加元素时,如果数组已满,它会自动创建一个更大的数组,并将原数组中的元素复制到新数组中。同样,当删除元素时,如果数组中的元素数量变得较少,它会自动缩小数组的大小,以节省内存空间。
由于ArrayList是基于数组实现的,所以它具有随机访问的特性,可以通过索引直接访问数组中的元素,时间复杂度为O(1)。但在插入和删除元素时,需要移动其他元素以保持数组的连续性,时间复杂度为O(n)。
说说 hash 函数?
哈希函数是一种将输入数据映射到固定大小值(哈希值)的函数。它将任意长度的输入数据转换为固定长度的输出,通常是一个较短的数字或字符串。
哈希函数具有以下特性:
输入相同的数据,得到的哈希值必定相同。 即使输入数据只有微小的改动,得到的哈希值也会完全不同。 哈希值的长度固定,不受输入数据的长度影响。
哈希函数常用于密码学、数据完整性校验、数据索引等领域。在密码学中,哈希函数常用于将用户密码进行加密存储,以保护用户的隐私。在数据完整性校验中,哈希函数可以用于验证数据是否被篡改。在数据索引中,哈希函数可以用于快速查找和比较数据。
常见的哈希函数包括MD5、SHA-1、SHA-256等。这些哈希函数具有较低的碰撞概率,保证了数据的唯一性和安全性。然而,随着计算能力的提升,一些传统的哈希函数可能存在安全性问题,因此在实际应用中,需要根据具体需求选择适当的哈希函数。
说说 hashmap 的底层?
HashMap 基于 Hash 算法实现的,我们通过 put(key,value)存储,get(key)来获取。当传入 key 时,HashMap 会根据 key. hashCode() 计算出 hash 值,根据 hash 值将 value 保存在 bucket 里。当计算出的 hash 值相同时,我们称之为 hash 冲突,HashMap 的做法是用链表和红黑树存储相同 hash 值的 value。当 hash 冲突的个数比较少时,使用链表否则使用红黑树。
hashmap 会有线程安全问题吗?
HashMap在多线程环境下会存在线程安全问题。HashMap是一种非线程安全的数据结构,当多个线程同时对HashMap进行修改时,可能会导致数据不一致或产生其他异常。
如果需要在多线程环境下使用HashMap,可以考虑使用ConcurrentHashMap代替。ConcurrentHashMap是Java提供的线程安全的哈希表实现,它采用了分段锁的机制,不同的线程可以同时访问不同的分段,从而提高了并发读写的能力。
concurrentHashMap 相对于 hashMap 好在哪里
ConcurrentHashMap是Java中的一个线程安全的哈希表实现,它可以在多线程环境下并发地进行读写操作,而不需要像传统的HashTable那样在读写时加锁。
ConcurrentHashMap的实现原理主要基于分段锁和CAS操作。它将整个哈希表分成了多个Segment(段),每个Segment都类似于一个小的HashMap,它拥有自己的数组和一个独立的锁。在ConcurrentHashMap中,读操作不需要锁,可以直接对Segment进行读取,而写操作则只需要锁定对应的Segment,而不是整个哈希表,这样可以大大提高并发性能。
SpringMVC 和 SpringBoot 的区别?
Spring MVC和Spring Boot是两个在Java开发中常用的框架,它们有以下几个主要区别:
目标和定位:Spring MVC是一个在Java企业级应用中构建Web应用程序的框架,它提供了一套完整的MVC(模型-视图-控制器)架构,用于处理HTTP请求和响应。而Spring Boot是一个快速构建独立、可运行的、生产级的Spring应用程序的框架,它简化了Spring应用程序的配置和部署过程。
配置方式:Spring MVC需要通过XML文件或Java配置类进行显式的配置,包括配置控制器、视图解析器、拦截器等。而Spring Boot采用约定大于配置的原则,提供了自动配置功能,可以根据应用程序的依赖和配置文件中的设置,自动完成大部分配置工作,简化了开发者的配置过程。
依赖管理:Spring MVC需要开发者手动管理依赖的版本和冲突。而Spring Boot使用了一个叫做"Starter"的概念,它提供了一组预定义的依赖,可以通过简单的引入Starter来管理依赖,减少了开发者的工作量。
SpringBoot 的自动装配原理?
Spring Boot的自动装配原理是基于条件注解和Spring的依赖注入机制实现的。
首先,Spring Boot会根据classpath下的依赖以及配置文件中的设置,自动扫描并加载相应的自动配置类。
其次,自动配置类中使用了条件注解,根据条件判断是否要进行自动装配。条件注解可以根据一些特定的条件,如某个类是否在classpath中、某个配置是否存在等,来决定是否要进行自动装配。
最后,当条件满足时,自动配置类会自动注册和配置相应的Bean,将它们添加到Spring容器中。这样,在应用程序启动时,Spring Boot就会自动完成大部分配置工作,开发者无需手动配置。
通过自动装配,Spring Boot可以根据应用程序的依赖和配置文件的设置,智能地选择合适的配置,并将其应用到应用程序中,从而简化了开发者的配置过程,提高了开发效率。
说说 MVC?
MVC(Model-View-Controller)是一种软件设计模式,用于将应用程序的逻辑分离为三个不同的组件:模型(Model)、视图(View)和控制器(Controller)。
模型(Model)表示应用程序的数据和业务逻辑。它负责处理数据的读取、存储、验证以及与数据库的交互等操作。
视图(View)是用户界面的呈现部分,负责展示模型中的数据给用户。它可以是一个网页、一个图形界面或者其他任何形式。
控制器(Controller)充当模型和视图之间的中介,负责处理用户的请求、更新模型的状态,以及决定要显示哪个视图。它接收用户的输入,调用相应的模型方法来处理请求,并最终将结果返回给视图进行显示。
通过MVC的分层结构,实现了应用程序的解耦和可维护性。模型、视图和控制器各司其职,彼此之间的依赖关系降低,使得应用程序的开发、测试和维护更加灵活和高效。
MVC模式被广泛应用于Web开发、桌面应用程序以及移动应用程序等领域,是一种重要的软件设计模式。
说说SpringMVC 的组件?
DispatcherServlet:前置控制器,负责接收 HTTP 请求并委托给 HandlerMapping、HandlerAdapter 和 ViewResolver 等组件处理。 Handler:处理器,完成具体的业务逻辑,相当于 Servlet 或 Action。HandlerMapping:负责将请求映射到对应的 Handler 即控制器(Controller)。 HandlerInterceptor:处理器拦截器,是一个接口,如果需要完成一些拦截处理,可以实现该接口。HandlerExecutionChain:处理器执行链,包括两部分内容:Handler 和 HandlerInterceptor(系统会有一个默认的 HandlerInterceptor,如果需要额外设置拦截,可以添加拦截器)。 HandlerAdapter:负责调用处理器方法并封装处理结果,将其传递给 DispatcherServlet。ModelAndView:装载了模型数据和视图信息,作为 Handler 的处理结果,返回给 DispatcherServlet。 ViewResolver:视图解析器,负责根据视图名称解析出对应的 View,最终将渲染结果响应给客户端。
了解过 SpringCloudAlibaba吗?
用过gateway做网关,nacos做注册中心和配置中心
面试总结
面试官问的很多,但是人很好,问的问题基本答出来了,说我理解的可以,就是说让我要多看看底层原理和源码, 聊了50多分钟。