去哪⼉⼀⾯
去哪⼉ Java ⾯试
正好,今天看到「去哪⼉」开奖了,去哪⼉虽然属于互 联⽹中⼚,但是薪资开的也是⽐较给⼒
的,⽬前看到开奖有 2 个档次,年薪也是在 35w 左右。

22k x 16 = 35w
21k x 16 = 33w现在能看到爆料薪资⽐较少,sp 、ssp 的档位薪资还不清楚。
去哪⼉的校招⾯试的流程是⼀⼆三⾯⼀起的,⼀⾯完了就在会议⾥⾯⼀直等待,等到⼆⾯⾯试官
来,最后也是等HR 来,整个流程就算完成,⼀共会持续⼤概三个 多⼩时。
⼀⾯的⾯试⻛格是偏向⼋股,⽐如Java 、数据库、计算机基础等,会要求写⼀个算法
⼆⾯的⾯试⻛格是偏向项⽬和场景题,⽐如项⽬中有什么 难点,也有可能也会写⼀个算法
三⾯是 HR ⾯,考察⼀些通⽤性问题,⽐如职业规划,优缺点,最⼤困难这些
这次,跟⼤家分享今年去哪⼉秋招的后端开发的⾯经,是⼀⾯⾯经,以技术拷打为主 。
去哪⼉⼀⾯
Java 线程状态有哪些?

源⾃《Java 并发编程艺术》 java.lang.Thread.State 枚举类中定义了 六种线程的状态,可以调⽤线程


Thread 中的getState() ⽅法获取当前线程的状态。hashmap 底层结构说⼀下?
在 JDK 1.7 版本之前, HashMap 数据结构是数组和链表,HashMap 通过哈希算法将元素的键
(Key )映射到数组中的槽位(Bucket )。如果多个键映射到同⼀个槽位,它们会以 链表的形式 存
储在同⼀个槽位上,因为链表的查询时间是O(n) ,所以冲突很严重,⼀个索引上的链表⾮常⻓,效率就很低了。

所以在 JDK 1.8 版本的时候做 了优化,当⼀个链表的⻓度超过8的时候就转换数据结构,不再使⽤
链表存储,⽽是使⽤红⿊树,查找时使⽤红⿊树,时间复杂度O(log n ),可以提⾼查询性能,但
是在数量较少时,即数量⼩于6时,会将红⿊树转换回链表。

hashmap 在并发时怎么去解决?
hashmap 不是线程安全的,hashmap 在多线程会存在下⾯的问题:
JDK 1.7 HashMap 采⽤数组 + 链表的数据结构,多线程背景下,在数组扩容的时候,存在 Entry
链死循环和数据丢失问题。
JDK 1.8 HashMap 采⽤数组 + 链表 + 红⿊⼆叉树的数据结构,优化了 1.7 中数组扩容的⽅案,
解决了 Entry 链死循环和数据丢失问题。但是多线程背景下,put ⽅法存在数据覆盖的 问题。
如果要保证线程安全,可以通过这 些⽅法来保证:
多线程环境可以使 ⽤Collections.synchronizedMap 同步加锁的⽅式,还可以使 ⽤HashTable ,但
是同步的⽅式显然性能不达标,⽽ConurrentHashMap 更适合⾼并发场景使⽤。
ConcurrentHashmap 在JDK1.7 和1.8 的版本改动⽐较⼤,1.7 使⽤Segment+HashEntry 分段锁的⽅
式实现,1.8 则抛弃了Segment ,改为使⽤CAS+synchronized+Node 实现,同样也加⼊了红⿊
树,避免链表过⻓导致性能的问题。
HTTP 的⼏个版本的区别?
HTTP/2 相⽐ HTTP/1.1 性能上的改进:
头部压缩:HTTP/2 会压缩头(Header )如果你同时发出多个请求,他们的头是⼀样的或是相
似的,那么,协议会帮你消除重复的部分。这就是所谓的 HPACK 算法:在客⼾端和服务器同时
维护⼀张头信息表,所有字段都会存⼊这个表,⽣成⼀个索引号,以后就不发送同样字段了,
只发送索引号,这样就提⾼速度了。
⼆进制格式:HTTP/2 不再像 HTTP/1.1 ⾥的纯⽂本形式 的报⽂,⽽是全⾯采⽤了⼆进制格式,
头信息和数据体都是⼆进制,并且统称为帧(frame ):头信息帧(Headers Frame )和数据帧
(Data Frame )。这样虽然对⼈不友好,但是对计算机⾮常友好,因为计算机只懂⼆进制,那么
收到报⽂后,⽆需再将明⽂的报⽂转成⼆进制,⽽是直接解析⼆进制报⽂,这增加了数据传输
的效率。
并发传输:引出了 Stream 概念,多个 Stream 复⽤在⼀条 TCP 连接。解决了HTTP/1.1 队头阻
塞的问题:
服务器主动推送资源:HTTP/2 还在⼀定程度上改善了传统的「请求 - 应答」⼯作模式,服务端
不再是被动地响应,可以主动向客⼾端发送消息。
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 ,⽀持以消费组形式 消费数据。
Mysql 索引了解吗?
MySQL InnoDB 引擎是⽤了B+ 树作为了 索引的数据结构。
B+Tree 是⼀种多叉树,叶⼦节点才存放数 据,⾮叶⼦节点只存放索引,⽽且每个节点⾥的数据是
按主键顺序存放的。每⼀层⽗节点的索引值都会出现在下层⼦节点的索引值中,因此在叶⼦节点
中,包括了所有的索引值信息,并且每⼀个叶⼦节点都有两个 指针,分别 指向下⼀个叶⼦节点和
上⼀个叶⼦节点,形成⼀个双向链表。
主键索引的 B+Tree 如图所⽰:

⽐如,我们执⾏了下 ⾯这条查 询语句:
select * from product where id = 5;这条语句使⽤了主 键索引查询 id 号为 5 的商品。查询过程是这样的,B+Tree 会⾃顶向下逐层进⾏
查找:
将 5 与根节点的索引数据 (1 ,10 ,20) ⽐较,5 在 1 和 10 之间,所以根据 B+Tree 的搜索逻
辑,找到第⼆层的索引数据 (1 ,4,7) ;
在第⼆层的索引数据 (1 ,4,7) 中进⾏查找,因为 5 在 4 和 7 之间,所以找到第三层的索引数
据(4,5,6);在叶⼦节点的索引数据(4,5,6)中进⾏查找,然后我们找到了索引值为 5 的⾏数据。
数据库的索引和数据都是存储在硬盘的 ,我们可以把读取⼀个节点当作⼀次磁盘 I/O 操作。那么上⾯的整个查询过程⼀共经历了 3 个节点,也就是进⾏了 3 次 I/O 操作。
B+Tree 存储千万级的数据只需要 3-4 层⾼度就可以满⾜,这意味着从千万级的表查询⽬标数据最
多需要 3-4 次磁盘 I/O ,所以B+Tree 相⽐于 B 树和⼆叉树来说,最⼤的优势在于查询效率很⾼,
因为即使在数据量很⼤的情况,查询⼀个数据的磁盘 I/O 依然维持在 3-4 次。
MVCC 原理介绍⼀下?
MVCC 允许多个事 务同时读取同⼀⾏数据,⽽不会彼此阻塞,每个事 务看到的数据版本是该事务开
始时的数据版本。这意味着,如果其他事 务在此期间修改了数据,正在运⾏的事务仍然看到的是
它开始时的数据状态,从⽽实现了⾮阻塞读操作。
对于「读提交」和「可重复读」隔离级别的事务来说,它们是通过 Read View 来实现的,它们的
区别在于创建 Read View 的时机不同,⼤家可以把 Read View 理解成⼀个数据快照,就像相机拍
照那样,定格某⼀时刻的⻛景。
「读提交」隔离级别是在「每个select 语句执⾏前」都会重新⽣成⼀个 Read View ;
「可重复读」隔离级别是执⾏第⼀条select 时,⽣成⼀个 Read View ,然后整个事 务期间都在⽤
这个 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 (多版本并发控制)。
⼿撕
算法:两两交 换链表中的节点
