文 | 小林coding
出品 | 小林coding(ID:CodingLin )
已获得原公众号的授权转载
今天分享一位读者的腾讯春招实习面经,岗位Java后端,主要问了MySQL、Java、网络这三大块。
他觉得这场面试非常有收获,虽然都是基础问题,但是往下深挖根茎叶脉全部相连。反问环节面试官给了我很多建议,包括面试、策略、基础、算法等等,是一次宝贵的学习经历。
索引可以帮助我们快速搜索数据,innodb 存储引擎用的是 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 字段排序。
因此,使用联合索引时,存在最左匹配原则,也就是按照最左优先的方式进行索引的匹配。在使用联合索引进行查询的时候,如果不遵循「最左匹配原则」,联合索引会失效,这样就无法利用到索引快速查询的特性了。
聚簇索引的 B+Tree 的叶子节点存放的是实际数据,所有完整的用户记录都存放在主键索引的 B+Tree 的叶子节点里。
在查询时使用了二级索引,如果查询的数据能在二级索引里查询的到,那么就不需要回表,这个过程就是覆盖索引。如果查询的数据不在二级索引里,就会先检索二级索引,找到对应的叶子节点,获取到主键值后,然后再检索主键索引,就能查询到数据了,这个过程就是回表。
InnoDB 里的 B+ 树中的每个节点都是一个数据页,结构示意图如下:
B+ 树如何实现快速查找主键为 6 的记录,以上图为例子:
可以看到,在定位记录所在哪一个页时,也是通过二分法快速定位到包含该记录的页。定位到该页后,又会在该页内进行二分法快速定位记录所在的分组(槽号),最后在分组内进行遍历查找。
对于「读提交」和「可重复读」隔离级别的事务来说,它们是通过 Read View 来实现的,它们的区别在于创建 Read View 的时机不同,大家可以把 Read View 理解成一个数据快照,就像相机拍照那样,定格某一时刻的风景。
Read View 有四个重要的字段:
对于使用 InnoDB 存储引擎的数据库表,它的聚簇索引记录中都包含下面两个隐藏列:
在创建 Read View 后,我们可以将记录中的 trx_id 划分这三种情况:
一个事务去访问记录的时候,除了自己的更新记录总是可见之外,还有这几种情况:
min_trx_id
值,表示这个版本的记录是在创建 Read View 前已经提交的事务生成的,所以该版本的记录对当前事务可见。max_trx_id
值,表示这个版本的记录是在创建 Read View 后才启动的事务生成的,所以该版本的记录对当前事务不可见。m_ids
列表中,表示生成该版本记录的活跃事务依然活跃着(还没提交事务),所以该版本的记录对当前事务不可见。m_ids
列表中,表示生成该版本记录的活跃事务已经被提交,所以该版本的记录对当前事务可见。这种通过「版本链」来控制并发事务访问同一个记录时的行为就叫 MVCC(多版本并发控制)。
Next-Key Lock 称为临键锁,是 Record Lock + Gap Lock 的组合,锁定一个范围,并且锁定记录本身。
假设,表中有一个范围 id 为(3,5] 的 next-key lock,那么其他事务即不能插入 id = 4 记录,也不能修改 id = 5 这条记录。
所以,next-key lock 即能保护该记录,又能阻止其他事务将新纪录插入到被保护记录前面的间隙中。
like %xx
或者 like %xx%
这两种方式都会造成索引失效;具体更新一条记录 UPDATE t_user SET name = 'xiaolin' WHERE id = 1;
的流程如下:
JVM的内存结构主要分为以下几个部分:
程序计数器(Program Counter Register):每个线程都有一个程序计数器。当线程执行 Java 方法时,程序计数器保存当前执行指令的地址,以便在 JVM 调用其他方法或恢复线程执行时重新回到正确的位置。
Java 虚拟机栈(Java Virtual Machine Stacks):每个线程都有一个虚拟机栈。虚拟机栈保存着方法执行期间的局部变量、操作数栈、方法出口等信息。线程每调用一个 Java 方法时,会创建一个栈帧(Stack Frame),栈帧包含着该方法的局部变量、操作数栈、方法返回地址等信息。栈帧在方法执行结束后会被弹出。
本地方法栈(Native Method Stack):与 Java 虚拟机栈类似,但是为本地方法服务。
Java 堆(Java Heap):Java 堆是 Java 虚拟机中最大的一块内存区域,用于存储各种类型的对象实例,也是垃圾收集器的主要工作区域。Java 堆是所有线程共享的部分。
方法区(Method Area):方法区也是所有线程共享的部分,它用于存储类的加载信息、静态变量、常量池、方法字节码等数据。在 Java 8 及以前的版本中,方法区被实现为永久代(Permanent Generation),在 Java 8 中被改为元空间(Metaspace)。
String 保存在字符串常量池中,不同于其他对象,它的值是不可变的,且可以被多个引用共享。
Java 7 新特性:钻石操作符、try-with-resource语句、支持动态类型语言、Fork/Join框架等。
Java 8 新特性:Lambda 表达式、Stream API、新的 Date/Time API、Nashorn JavaScript 引擎等。
Java中的垃圾回收机制通过判断对象是否可达来确定哪些对象可以被回收。当一个对象没有任何引用指向它时,它就可以被回收,这个过程由JVM的垃圾回收器自动完成。
JVM使用可达性分析算法来判断对象是否可达。从GC Roots对象开始,通过一系列的引用链来遍历所有的对象,如果一个对象不可达,则说明它已经死亡,可以被回收了。
在Java中,有4种引用类型:强引用、软引用、弱引用和虚引用。其中,强引用是最常见的引用类型,只要强引用存在,垃圾回收器就不会回收该对象。软引用、弱引用和虚引用则分别表示对对象的软引用、弱引用和虚引用,当垃圾回收器进行垃圾回收时,会根据不同的引用类型来决定是否回收对象。
Serial收集器:单线程的垃圾回收器,使用标记-复制算法,适合小型应用程序或客户端应用程序。
Parallel收集器:多线程的垃圾回收器,使用标记-复制算法,适合在后台运行的中型应用程序。
CMS收集器:并发垃圾回收器,使用标记-清除算法,适合对响应时间有要求的中型应用程序。
G1收集器:并发垃圾回收器,使用标记-整理算法,适合对响应时间有要求且堆内存较大的应用程序。
其中,Serial收集器和Parallel收集器是新生代收集器,CMS和G1是老年代收集器。
HashMap底层是基于数组和链表实现的。简单来说,HashMap将key通过hash算法映射到数组中,然后在对应的链表中查找value。当多个key的hash值相同时,会在同一个数组位置上使用链表来存储这些key-value。但是,当链表长度太长时,会影响HashMap的性能,因此在JDK1.8中,当链表长度超过阈值时,会将链表转换为红黑树,以提高查找效率。
HashMap不是线程安全的,因为多个线程同时访问HashMap时可能会导致数据不一致的问题。可以使用ConcurrentHashMap来实现线程安全的Map。
红黑树是一种自平衡的二叉查找树,可以保证在最坏情况下基本动态操作的时间复杂度为O(log n)。红黑树中的每个节点都有一个颜色属性,可以是红色或黑色。红黑树满足以下5个性质:
通过这些性质,红黑树可以保证在插入和删除节点时,自动调整树的结构,以保持树的平衡和性质的满足。相比于普通的二叉查找树,红黑树的平衡性更好,查找、插入和删除都具有更稳定的时间复杂度,因此在很多场景下被广泛应用。
公平锁和非公平锁是针对锁的获取方式而言的。
公平锁是指多个线程按照申请锁的顺序来获取锁,即先到先得的原则。当线程A释放锁后,线程B、C、D依次获取锁,如果此时线程E申请锁,则它需要等待B、C、D依次获取到锁并释放锁后才能获取锁。
非公平锁是指多个线程获取锁的顺序是随机的,不保证公平性。当线程A释放锁后,线程B、C、D等线程都可以通过竞争获取到锁,而此时线程E也可以通过竞争获取到锁。
在实际应用中,公平锁可以避免饥饿现象,但是由于需要维护线程队列,因此效率相对较低。而非公平锁由于不需要维护线程队列,因此效率相对较高,但是可能会导致某些线程长时间无法获取锁。
ThreadLocal是Java中的一个线程封闭技术,它可以让每个线程都拥有自己单独的变量副本,从而保证线程安全。ThreadLocal提供了一种线程本地存储的机制,为每个线程提供一个独立的变量副本,使得每个线程中的变量互不干扰。
三次握手的过程:
CLOSE
状态。先是服务端主动监听某个端口,处于 LISTEN
状态client_isn
),将此序号置于 TCP 首部的「序号」字段中,同时把 SYN
标志位置为 1
,表示 SYN
报文。接着把第一个 SYN 报文发送给服务端,表示向服务端发起连接,该报文不包含应用层数据,之后客户端处于 SYN-SENT
状态。SYN
报文后,首先服务端也随机初始化自己的序号(server_isn
),将此序号填入 TCP 首部的「序号」字段中,其次把 TCP 首部的「确认应答号」字段填入 client_isn + 1
, 接着把 SYN
和 ACK
标志位置为 1
。最后把该报文发给客户端,该报文也不包含应用层数据,之后服务端处于 SYN-RCVD
状态。ACK
标志位置为 1
,其次「确认应答号」字段填入 server_isn + 1
,最后把报文发送给服务端,这次报文可以携带客户到服务端的数据,之后客户端处于 ESTABLISHED
状态。ESTABLISHED
状态。四次挥手的过程:
FIN
标志位被置为 1
的报文,也即 FIN
报文,之后客户端进入 FIN_WAIT_1
状态。ACK
应答报文,接着服务端进入 CLOSE_WAIT
状态。ACK
应答报文后,之后进入 FIN_WAIT_2
状态。FIN
报文,之后服务端进入 LAST_ACK
状态。FIN
报文后,回一个 ACK
应答报文,之后进入 TIME_WAIT
状态ACK
应答报文后,就进入了 CLOSE
状态,至此服务端已经完成连接的关闭。2MSL
一段时间后,自动进入 CLOSE
状态,至此客户端也完成连接的关闭。三次握手的原因:
四次次挥手的原因:
阻塞IO(Blocking IO):应用程序在进行IO操作时,会一直阻塞等待IO完成,期间无法进行其他操作。
非阻塞IO(Non-blocking IO):应用程序在进行IO操作时,会立即返回,无论IO操作是否完成,应用程序都可以进行其他操作。需要通过轮询的方式来判断IO是否完成,因此效率较低。
IO多路复用(IO Multiplexing):通过select、poll、epoll等系统调用,在一个进程中可以同时监控多个文件描述符,当有任何一个文件描述符就绪时,就可以进行IO操作。
信号驱动IO(Signal Driven IO):应用程序在进行IO操作时,向内核注册一个信号处理函数,内核在IO完成时会向应用程序发送一个信号,应用程序收到信号后再进行数据处理。
异步IO(Asynchronous IO):应用程序进行IO操作时,可以立即返回,内核负责将数据读取到指定的缓冲区中,并在完成后通知应用程序,应用程序可以继续进行其他操作。异步IO需要操作系统和硬件的支持,目前主要应用于高性能IO场景。
select 和 poll 并没有本质区别,它们内部都是使用「线性结构」来存储进程关注的 Socket 集合。
在使用的时候,首先需要把关注的 Socket 集合通过 select/poll 系统调用从用户态拷贝到内核态,然后由内核检测事件,当有网络事件产生时,内核需要遍历进程关注 Socket 集合,找到对应的 Socket,并设置其状态为可读/可写,然后把整个 Socket 集合从内核态拷贝到用户态,用户态还要继续遍历整个 Socket 集合找到可读/可写的 Socket,然后对其处理。
很明显发现,select 和 poll 的缺陷在于,当客户端越多,也就是 Socket 集合越大,Socket 集合的遍历和拷贝会带来很大的开销,因此也很难应对 C10K。
epoll 是解决 C10K 问题的利器,通过两个方面解决了 select/poll 的问题。
Java中,BIO和NIO都是IO模型,它们的主要区别在于:
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
if (headA == null || headB == null) return null;
ListNode pA = headA, pB = headB;
while (pA != pB) {
pA = pA == null ? headB : pA.next;
pB = pB == null ? headA : pB.next;
}
return pA;
}
非常有收获的一次面试,值得之后自己单独写一写文章去记录一下。基础不牢地动山摇,虽然都是基础问题,但是往下深挖根茎叶脉全部相连,这些问题面试题里面都有解,但是自己真的是只知道表面,浅浅看个大概就上战场了,还有就是非常感谢面试官,反问环节给了我很多建议,包括面试、策略、基础、算法等等,是一次宝贵的学习经历,说不遗憾是假的,但我很开心,有所收获就好。
基础不够扎实,很多只知其表不知其里,而且面试官经验丰富,非常敏锐,当时压力也很大,被问倒了心态也不稳,有的可以答出来的也没想起来怎么说。
没有几个答的特别好的,稍微说一点就会被问深直到不会,这里就不贴当时回答了,说的都很浅,下来每一个问题都要仔细研究,要将知识连接成网。
自己给自己构造一个场景,顺着把用到的技术全部梳理一遍,能讲出这个整体过程,有头有尾,才能真的理解,比如hashmap插入数据的过程、threadlocal创建释放的过程、String怎么实现的字符串拼接、SpringBoot框架搭建的每一步等等。
<END> 程序员专属T恤
商品直购链接 👇
推荐阅读:
文章引用微信公众号"脚本之家",如有侵权,请联系管理员删除!