Java
海康威视 Java ⾯试
海康威视作为⼀家体⾯⼚,我们来看看 今年海康威视的校招薪资开了多少?
我根据⼀些同学的反馈,整理了海康威视软件开发岗位的校招薪资,在⽬前的就业背景下,海康
威视的校招薪资还是很体⾯的。
14k x 15 = 21w (本科 985 ,武汉)15k x 15 = 22.5w (硕⼠双⼀流,杭州)
16 x 15 = 24w (本硕 211 ,杭州)
19 x 15 = 28.5w (本硕 211 ,杭州)那海康威视的⾯试难度如何呢?
我也找了⼀位今年秋招⾯海康威视同学的⾯经,给⼤家做做 参考参考,总共 1 轮技术⾯ + 1 轮 HR
⾯,3-5 个⼯作⽇出结果。
⼀⾯是技术⾯,问的问题不算多,主要拷打了 Java 、MySQL 、Redis ⽅⾯的⼋股⽂,都属于经典的
⾯试问题,不算难。

岗位:应⽤软件开发⼯程师
timeline :9.18 ⼀⾯,9.27 ⼆⾯
为什么 会选杭州这个城市
Spring Boot 整体的启动流程
Spring MVC 整体的执⾏流程
MySQL 索引的机制,类型有哪些
有⽆排查索引实效的经验,展开讲讲
索引实效的场景有哪些
Redis 为什么 这么快
Redis 6.0 之后为什么 引⼊了多线程
Redis 分布式锁怎么解决超卖问题的
反问
部⻔:国际业务,⾯向海外政府单位,提供整体的软件安防解决⽅案
1 轮技术⾯ + 1 轮 HR ⾯,3-5 个⼯作⽇出结果
后续:已约 10.16 线下通⽤素质⾯试
Java
介绍⼀下 Spring Boot 整体的启动流程?
1. ⾸先从main 找到run() ⽅法,在执⾏run() ⽅法之前new ⼀个SpringApplication 对象
2. 进⼊run() ⽅法,创建应⽤监听器SpringApplicationRunListeners 开始监听
3. 然后加载SpringBoot 配置环境(ConfigurableEnvironment) ,然后把配置环境(Environment) 加⼊监听对象中
4. 然后加载应⽤上下 ⽂(ConfigurableApplicationContext) ,当做run ⽅法的返回对象
5. 最后创建Spring 容器, refreshContext(context) ,实现starter ⾃动化配置和bean 的实例化等⼯作。
说⼀说 Spring MVC 整体的执⾏流程?

流程图步骤详解:
发送请求:⽤⼾发送的所有请求都会到前 端控制器DispatcherServlet
请求查找Handler :DispatcherServlet 收到请求会调⽤HandlerMapping (处理器映射器) 查找
Handler
- 返回Handler :处理器映射器根据url 返回具体的处理器, ⽣成HandlerExecutionChain 对象,其
中包含了⽬标Handler 和若⼲拦截 器( 可能没有)
请求调⽤Handler :DispatcherServlet 通过Handler 寻找匹配到HandlerAdapter
执⾏Handler :HandlerAdapter 调⽤Handler
返回结果:Handler 执⾏完成,返回⼀个ModelAndView 对象
返回结果给DispatcherServlet :HandlerAdapter 将Handler 执⾏结果ModelAndView 返回给
DispatcherServlet
- 如果Handler 返回的View 是逻辑视图名称⽽不是真正的View 对象,DispatcherServlet 调⽤
resolveViewName ⽅法在配置的所有视图解析器(ViewResolver) 中,寻找合适的,最终通过ViewResolver 将逻辑视图名解析成真正的View 对象
9. ViewResolver 通过调⽤createView ⽅法尝试将视图名解析成View ,如果⽆法解析会返回(注: 如果ViewResolver 是派⽣⾃AbstractCachingViewResolver 则在调⽤createView ⽅法前会先尝试根据
viewName 和Iocale 从缓存中查找对应的视图对象)DispatcherServlet 调⽤View 的render ⽅法进⾏渲染视图 (即将模型数据填充⾄request 域)
DispatcherServlet 响应⽤⼾
MySQL
MySQL 索引的机制,类型有哪些?
MySQL 可以按照四个⻆度来分类索引。
按「数据结构」分类:B+tree 索引、Hash 索引、Full-text 索引。
按「物理存储」分类:聚簇索引(主键索引)、⼆级索引(辅助索引)。
按「字段特性」分类:主键索引、唯⼀索引、普通索引、前缀索引。
按「字段个数」分类:单列索引、联合索引。
接下来,按照这些⻆度来说说 各类索引的特点。
按数据结构分类
从数据结构的⻆度来看,MySQL 常⻅索引有 B+Tree 索引、HASH 索引、Full-Text 索引。
每⼀种存储引擎⽀持的索引类型不⼀定相同,我在表中总结了 MySQL 常⻅的存储引擎 InnoDB 、
MyISAM 和 Memory 分别 ⽀持的索引类型。

InnoDB 是在 MySQL 5.5 之后成为默认的 MySQL 存储引擎,B+Tree 索引类型也是 MySQL 存储引
擎采⽤最多的索引类型。
在创建表时,InnoDB 存储引擎会根据不同的场景选择不同的列作为索引:
如果有 主键,默认会使 ⽤主键作为聚簇索引的索引键(key );
如果没有主键,就选择第⼀个不 包含 值的唯⼀列作为聚簇索引的索引键(key );
在上⾯两个 都没有的情况下,InnoDB 将⾃动⽣成⼀个隐式⾃增 id 列作为聚簇索引的索引键
(key );其它索引都属于辅助索引(Secondary Index ),也被称为⼆级索引或⾮聚簇索引。创建的主键索引
和⼆级索引默认使⽤的是 B+Tree 索引。
按物理存储分类
从物理存储的⻆度来看,索引分为聚簇索引(主键索引)、⼆级索引(辅助索引)。
这两个 区别在前⾯也提到了:
主键索引的 B+Tree 的叶⼦节点存放的是实际数据,所有完整的⽤⼾记录都存放在主键索引的
B+Tree 的叶⼦节点⾥;
⼆级索引的 B+Tree 的叶⼦节点存放的是主键值,⽽不是实际数据。
所以,在查询时使⽤了⼆级索引,如果查 询的数据能在⼆级索引⾥查询的到,那么就不需要回
表,这个过程就是覆盖索引。如果查 询的数据不在⼆级索引⾥,就会先检索⼆级索引,找到对应
的叶⼦节点,获取到主键值后,然后再检索主键索引,就能查询到数据了,这个过程就是回表。
按字段特性分类
从字段特性的⻆度来看,索引分为主 键索引、唯⼀索引、普通索引、前缀索引。
主键索引
主键索引就是建⽴在主键字段上的索引,通常在创建表的时候⼀起创建,⼀张表最多只有⼀个主
键索引,索引列的值不允许有空值。
在创建表时,创建主键索引的⽅式如下:
CREATE TABLE table_name (
....
PRIMARY KEY (index_column_1 ) USING BTREE
);唯⼀索引
唯⼀索引建 ⽴在 UNIQUE 字段上的索引,⼀张表可以有多个唯⼀索引,索引列的值必须唯⼀,但
是允许有空值。
在创建表时,创建唯⼀索引的⽅式如下:
CREATE TABLE table_name (
....
UNIQUE KEY(index_column_1,index_column_2,...)
);建表后,如果要创建唯⼀索引,可以使 ⽤这⾯这条命令:
CREATE UNIQUE INDEX index_name
ON table_name(index_column_1,index_column_2,...);普通索引
普通索引就是建⽴在普通字段上的索引,既不要求字段为主 键,也不 要求字段为 UNIQUE 。
在创建表时,创建普通索引的⽅式如下:
CREATE TABLE table_name (
....
INDEX(index_column_1,index_column_2,...)
);建表后,如果要创建普通索引,可以使 ⽤这⾯这条命令:
CREATE INDEX index_name
ON table_name(index_column_1,index_column_2,...);前缀索引
前缀索引是指对字符类型字段的前⼏个字符建⽴的索引,⽽不是在整个字段上建⽴的索引,前缀
索引可以建⽴在字段类型为 char 、 varchar 、binary 、varbinary 的列上。
使⽤前缀索引的⽬的是为了 减少索引占⽤的存储空间,提升查询效率。
在创建表时,创建前缀索引的⽅式如下:
mn_list,
INDEX(column_name(length))
);建表后,如果要创建前缀索引,可以使 ⽤这⾯这条命令:
CREATE INDEX index_name
ON table_name(column_name(length));按字段个数分类
从字段个数的⻆度来看,索引分为单列索引、联合索引(复合索引)。
建⽴在单列上的索引称为单列索引,⽐如主键索引;
建⽴在多列上的索引称为联合索引;
通过将多个字段组合成⼀个索引,该索引就被称为联合索引。
⽐如,将商品表中的 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 字段排序。
因此,使⽤联合索引时,存在最左匹配原则,也就是按照最左优先的⽅式进⾏索引的匹配。在使
⽤联合索引进⾏查询的时候,如果不遵循「最左匹配原则」,联合索引会失效,这样就⽆法利⽤到
索引快 速查询的特性了。
⽐如,如果创建了⼀个 (a, b, c) 联合索引,如果查 询条件是以下这⼏种,就可以匹配上联合索引:
where a=1 ;
where a=1 and b=2 and c=3 ;
where a=1 and b=2 ;
需要注意的是,因为有查 询优化器, 所以 a 字段在 where ⼦句的顺序并 不重要。但是,如果查 询条件是以下这⼏种,因为不 符合最左匹配原则,所以就⽆法匹配上联合索引,联
合索引就会失效:
where b=2 ;
where c=3 ;
where b=2 and c=3 ;
上⾯这些查询条件之所以会 失效,是因为(a, b, c) 联合索引,是先按 a 排序,在 a 相同的情况再 按b 排序,在 b 相同的情况再 按 c 排序。所以,b 和 c 是全局⽆序,局部相对有序的,这样在没有遵
循最左匹配原则的情况下,是⽆法利⽤到索引的。
联合索引有⼀些特殊情况,并不是查询过程使⽤了联合索引查询,就代表联合索引中的所有字段
都⽤到了联合索引进⾏索引查询,也就是可能存在部分字段⽤到联合索引的 B+Tree ,部分字段没
有⽤到联合索引的 B+Tree 的情况。
这种特殊情况就发⽣在范围查询。联合索引的最左匹配原则会⼀直向右 匹配直到遇到「范围查询」
就会停⽌匹配。也就是范围查询的字段可以⽤到联合索引,但是在范围查询字段的后⾯的字段⽆
法⽤到联合索引。
有⽆排查索引失效的经验,展开讲讲 ?
可以使 ⽤ EXPLAIN 来查 看 SQL 的执⾏计划,判断SQL 是否⾛了索引,如果没有⾛索引,就代表索
引发⽣失效了。
如下图,就是⼀个没有使⽤索引,并且是⼀个全表扫描的查询语句。

对于执⾏计划,参数有:
possible_keys 字段表⽰可能⽤到的索引;
key 字段表⽰实际⽤的索引,如果这⼀项为 ,说明没有使⽤索引;
key_len 表⽰索引的⻓度;rows 表⽰扫描的数据⾏数。
type 表⽰数据扫描类型,我们需要重点看这个。
type 字段就是描述了找到所需数据时使⽤的扫描⽅式是什么 ,常⻅扫描类型的执⾏效率从低到⾼
的顺序为:
All (全表扫描):在这些情况⾥,all 是最坏的情况,因为采⽤了全表扫描的⽅式。
index (全索引扫描):index 和 all 差不多,只不过 index 对索引表进⾏全扫描,这样做的好处
是不再需要对数据进⾏排序,但是开销依然很⼤。所以,要尽量避免全 表扫描和全索引扫描。
range (索引范围扫描):range 表⽰采⽤了索引范围扫描,⼀般在 where ⼦句中使⽤ < 、>、in 、between 等关键词,只检索给定范围的⾏,属于范围查找。从这⼀级别开始,索引的作⽤
会越来越明显 ,因此我们需要尽量让 SQL 查询可以使 ⽤到 range 这⼀级别及以上的 type 访问
⽅式。
ref (⾮唯⼀索引扫描):ref 类型表⽰采⽤了⾮唯⼀索引,或者是唯⼀索引的⾮唯⼀性前缀,返
回数据返回可能是多条。因为虽然使⽤了索引,但该索引列的值并不唯⼀,有重复。这样即使
使⽤索引快 速查找到了第⼀条数据,仍然不能停⽌,要进⾏⽬标值附近的⼩范围扫描。但它的
好处 是它并不需要扫全表,因为索引是有序的,即便有重复值,也是在⼀个⾮常⼩的范围内扫
描。
eq_ref (唯⼀索引扫描):eq_ref 类型是使⽤主键或唯⼀索引时产⽣的访问⽅式,通常使⽤在多
表联查中。⽐如,对两张表进⾏联查,关联条件是两张表的 user_id 相等,且 user_id 是唯⼀索
引,那么使⽤ EXPLAIN 进⾏执⾏计划查看的时候,type 就会显⽰ eq_ref 。const (结果只有⼀条的主键或唯⼀索引扫描):const 类型表⽰使⽤了主 键或者唯⼀索引与常量
值进⾏⽐较,⽐如 select name from product where id=1 。需要说明的是 const 类型和 eq_ref都使⽤了主 键或唯⼀索引,不过这 两个 类型有所区别,const 是与常量进⾏⽐较,查询效率会更
快,⽽ eq_ref 通常⽤于多表联查中。
extra 显⽰的结果,这⾥说⼏个重要的参考指标:
Using filesort :当查询语句中包含 group by 操作,⽽且⽆法利⽤索引完成排序操作的时候,
这时不得不选择相应的排序算法进⾏,甚⾄可能会通过⽂件排序,效率是很低的,所以要避免
这种问题的出现。
Using temporary :使了⽤临时表保存中间结果,MySQL 在对查询结果排序时使⽤临时表,常
⻅于排序 order by 和分组查询 group by 。效率低,要避免这种问题的出现。
Using index :所需数据只需在索引即可全部获得,不须要再到表中取数据,也就是使⽤了覆盖
索引,避免了回表操作,效率不错。
索引失效的场景有哪些?
6 种会发⽣索引失效的情况:
当我们使 ⽤左或者左右模糊匹配的时候,也就是 like %xx 或者 like %xx% 这两种⽅式都会造成索
引失效;
当我们在查询条件中对索引列使⽤函数,就会导致索引失效。
当我们在查询条件中对索引列进⾏表达式计算,也是⽆法⾛索引的。
MySQL 在遇到字符串和数字⽐较的时候,会⾃动把字符串转为数字,然后再进⾏⽐较。如果字
符串是索引列,⽽条件语句中的输⼊参数是数字的话,那么索引列会发⽣隐式类型转换,由于
隐式类型转换是通过 CAST 函数实现的,等同于对索引列使⽤了函数,所以就会导致索引失
效。
联合索引要能正确使⽤需要遵循最左匹配原则,也就是按照最左优先的⽅式进⾏索引的匹配,
否则就会导致索引失效。
在 WHERE ⼦句中,如果在 OR 前的条件列是索引列,⽽在 OR 后的条件列不是索引列,那么索
引会失效。
Redis
Redis 为什么 这么快?
官⽅使⽤基准测试的结果是,单线程的 Redis 吞吐 量可以达到 10W/ 每秒,如下图所⽰:

之所以 Redis 采⽤单线程(⽹络 I/O 和执⾏命令)那么快,有如下⼏个原因:
Redis 的⼤部分操作都在内存中完成,并且采⽤了⾼效的数据结构,因此 Redis 瓶颈可能是机器
的内存或者⽹络带宽,⽽并⾮ CPU ,既然 CPU 不是瓶颈,那么⾃然就采⽤单线程的解决⽅案
了;
Redis 采⽤单线程模型可以避免了多线程之间的竞争,省去了多线程切换带来的时间和性能上的
开销,⽽且也不 会导致死锁问题。
Redis 采⽤了 I/O 多路复⽤机制处理⼤量的客⼾端 Socket 请求,IO 多路复⽤机制是指⼀个线程
处理多个 IO 流,就是我们经常听到的 select/epoll 机制。简单来说,在 Redis 只运⾏单线程的
情况下,该机制允许内核中,同时存在多个监听 Socket 和已连接 Socket 。内核会⼀直监听这些
Socket 上的连接请求或数据请求。⼀旦有请求到达,就会交给 Redis 线程处理,这就实现了⼀
个 Redis 线程处理多个 IO 流的效果。
Redis 6.0 之后为什么 引⼊了多线程?
Redis 单线程指的是「接收客⼾端请求-> 解析请求 -> 进⾏数据读写等操作-> 发送数据给客⼾端」
这个过程是由⼀个线程(主线程)来完成的,这也是我们常说 Redis 是单线程的原因。
但是,Redis 程序并 不是单线程的,Redis 在启动的时候,是会启动后台 线程(BIO )的:
Redis 在 2.6 版本,会启动 2 个后台 线程,分别 处理关闭⽂件、AOF 刷盘这两个 任务;
Redis 在 4.0 版本之后,新增了⼀个新的后台 线程,⽤来异步释放 Redis 内存,也就是 lazyfree
线程。例如执⾏ unlink key / flushdb async / flushall async 等命令,会把这些删除操作交给后台
线程来执⾏,好处 是不会导致 Redis 主线程卡顿。因此,当我们要删除⼀个⼤ key 的时候,不
要使⽤ del 命令删除,因为 del 是在主线程处理的,这样会导致 Redis 主线程卡顿,因此我们应
该使⽤ unlink 命令来异步删除⼤key 。
之所以 Redis 为「关闭⽂件、AOF 刷盘、释放内存」这些任务创 建单独的线程来处理,是因为这
些任务的操作都是很耗时的,如果把这些任务都放在主线程来处理,那么 Redis 主线程就很容易发
⽣阻塞,这样就⽆法处理后续的请求了。
后台 线程相当于⼀个消费者,⽣产者把耗时任务丢到任务队列中,消费者(BIO )不停轮询这个队
列,拿出任务就去执⾏对应的⽅法即可。

虽然 Redis 的主要⼯作(⽹络 I/O 和执⾏命令)⼀直是单线程模型,但是在 Redis 6.0 版本之后,
也采⽤了多个 I/O 线程来处理⽹络请求,这是因为随着⽹络硬件的性能提升,Redis 的性能瓶颈有
时会出现在⽹络 I/O 的处理上。
所以为了 提⾼⽹络 I/O 的并⾏度,Redis 6.0 对于⽹络 I/O 采⽤多线程来处理。但是对于命令的执
⾏,Redis 仍然使⽤单线程来处理,所以⼤家不要误解Redis 有多线程同时执⾏命令。
Redis 官⽅表⽰,Redis 6.0 版本引⼊的多线程 I/O 特性对性能提升⾄少是⼀倍以上。
Redis 6.0 版本⽀持的 I/O 多线程特性,默认情况下 I/O 多线程只针对发送响应数据(write client socket ),并不会以 多线程的⽅式处理读请 求(read client socket )。要想开启多线程处理客⼾端读请求,就需要把 Redis.conf 配置⽂件中的 io-threads-do-reads 配置项设为 yes 。
//读请求也使用io多线程io-threads-do-reads yes
同时, Redis.conf 配置⽂件中提供了 IO 多线程个数的配置项。
// io-threads N,表示启用 N-1 个 I/O 多线程(主线程也算一个 I/O 线程)io-threads 4
关于线程数的设置,官⽅的建议是如果为 4 核的 CPU ,建议线程数设置为 2 或 3,如果为 8 核
CPU 建议线程数设置为 6,线程数⼀定要⼩于机器核数,线程数并不是越⼤越好。
因此, Redis 6.0 版本之后,Redis 在启动的时候,默认情况下会额外创建 6 个线程(
这⾥的线程
数不包括主线程):
Redis-server : Redis 的主线程,主要负责 执⾏命令;
bio_close_file 、bio_aof_fsync 、bio_lazy_free :三个 后台 线程,分别 异步处理关闭⽂件任 务、
AOF 刷盘任务、释放内存任务;
io_thd_1 、io_thd_2 、io_thd_3 :三个 I/O 线程,io-threads 默认是 4 ,所以会 启动 3(4-1 )个
I/O 多线程,⽤来分担 Redis ⽹络 I/O 的压⼒。
Redis 分布式锁怎么解决超卖问题的?
同⼀个锁key ,同⼀时间只能有⼀个客⼾端拿到锁,其他客⼾端会陷⼊⽆限的等待来尝试获取那个
锁,只有获取到锁的客⼾端才能执⾏下⾯的业务逻辑。
⽐如说,⽤⼾要⼀次性买 10 台⼿机,那么避免超卖的流程如下:
只有⼀个订单系统实例可以成功加分 布式锁,然后只 有他⼀个实例可以查库存、判断库存是否
充⾜、下单扣减库存,接着释放锁。
释放锁之后,另外⼀个订单系统实例才能加锁,接着查库存,⼀下发现库存只有 2 个了 ,库存
不⾜,⽆法购买,下单失败,不会将库存扣减为-8 的,就避免超卖的问题。
这种⽅案的缺点是同⼀个商品在多⽤⼾同时下单的情况下,会基于分布式锁串⾏化处理,导致没法同时处理同⼀个商品的⼤量下单的请求。
