|
大家好,我是库森。* i* W& V" p3 A* j4 `4 d
海康威视作为一家体面厂,我们来看看今年海康威视的校招薪资开了多少?8 d: F( G! g2 }/ [
我根据一些同学的反馈,整理了海康威视软件开发岗位的校招薪资,在目前的就业背景下,海康威视的校招薪资还是很体面的。
1 z) d" \" u" T. c14k x 15 = 21w(本科 985,武汉)15k x 15 = 22.5w(硕士双一流,杭州)16 x 15 = 24w (本硕 211,杭州)19 x 15 = 28.5w (本硕 211,杭州)那海康威视的面试难度如何呢?. Q% }- Y' v* a3 J
我也找了一位今年秋招面海康威视同学的面经,给大家做做参考参考,总共 1 轮技术面 + 1 轮 HR 面,3-5 个工作日出结果。& o, ^; }# y5 K0 N
一面是技术面,问的问题不算多,主要拷打了 Java、MySQL、Redis 方面的八股文,都属于经典的面试问题,不算难。) V% K V8 `5 ~3 z1 S7 h
fmvmujcputf64078576854.jpg
. M' T6 O) A0 p$ P) S# N" z
Java 介绍一下 Spring Boot 整体的启动流程?首先从main找到run()方法,在执行run()方法之前new一个SpringApplication对象进入run()方法,创建应用监听器SpringApplicationRunListeners开始监听然后加载SpringBoot配置环境(ConfigurableEnvironment),然后把配置环境(Environment)加入监听对象中然后加载应用上下文(ConfigurableApplicationContext),当做run方法的返回对象最后创建Spring容器,refreshContext(context),实现starter自动化配置和bean的实例化等工作。[/ol]说一说 Spring MVC 整体的执行流程?* }: Y4 S* D5 C: j
cxzzckxp1pl64078576954.jpg
3 R! d" k" C b6 U' h
流程图步骤详解:发送请求:用户发送的所有请求都会到前端控制器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对象ViewResolver通过调用createView方法尝试将视图名解析成View,如果无法解析会返回Null(注: 如果ViewResolver是派生自AbstractCachingViewResolver则在调用createView方法前会先尝试根据viewName和Iocale从缓存中查找对应的视图对象)DispatcherServlet调用View的render方法进行渲染视图 (即将模型数据填充至request域)DispatcherServlet响应用户[/ol]MySQL MySQL 索引的机制,类型有哪些?MySQL可以按照四个角度来分类索引。
- h) Q% m7 o* ~! Z) l7 Z! C按「数据结构」分类:B+tree索引、Hash索引、Full-text索引。按「物理存储」分类:聚簇索引(主键索引)、二级索引(辅助索引)。按「字段特性」分类:主键索引、唯一索引、普通索引、前缀索引。按「字段个数」分类:单列索引、联合索引。接下来,按照这些角度来说说各类索引的特点。7 ~$ X$ [* f" _ a! O* H
按数据结构分类
+ W' E$ q$ L4 C+ I- f* e从数据结构的角度来看,MySQL 常见索引有 B+Tree 索引、HASH 索引、Full-Text 索引。+ U0 P3 {# g5 u4 E9 H' g
每一种存储引擎支持的索引类型不一定相同,我在表中总结了 MySQL 常见的存储引擎 InnoDB、MyISAM 和 Memory 分别支持的索引类型。+ ^) {1 I; ~1 d# y
zjro0uixxki64078577054.jpg
' p! Z1 |$ H: K) |) X! @. nInnoDB 是在 MySQL 5.5 之后成为默认的 MySQL 存储引擎,B+Tree 索引类型也是 MySQL 存储引擎采用最多的索引类型。2 S: ^' z3 C( `# p5 y, D; Y% N
在创建表时,InnoDB 存储引擎会根据不同的场景选择不同的列作为索引:
# X% r( c& y) m6 T8 Q2 j9 [如果有主键,默认会使用主键作为聚簇索引的索引键(key);如果没有主键,就选择第一个不包含 NULL 值的唯一列作为聚簇索引的索引键(key);在上面两个都没有的情况下,InnoDB 将自动生成一个隐式自增 id 列作为聚簇索引的索引键(key);其它索引都属于辅助索引(Secondary Index),也被称为二级索引或非聚簇索引。创建的主键索引和二级索引默认使用的是 B+Tree 索引。
4 i- Q+ L; V" O& `# [9 F: a按物理存储分类" h9 a2 D. d6 o! {" Z5 E$ n
从物理存储的角度来看,索引分为聚簇索引(主键索引)、二级索引(辅助索引)。: E r' U; _7 G5 q
这两个区别在前面也提到了:: x$ K& \- a; X4 C4 r. ?7 N$ \. q
主键索引的 B+Tree 的叶子节点存放的是实际数据,所有完整的用户记录都存放在主键索引的 B+Tree 的叶子节点里;二级索引的 B+Tree 的叶子节点存放的是主键值,而不是实际数据。所以,在查询时使用了二级索引,如果查询的数据能在二级索引里查询的到,那么就不需要回表,这个过程就是覆盖索引。如果查询的数据不在二级索引里,就会先检索二级索引,找到对应的叶子节点,获取到主键值后,然后再检索主键索引,就能查询到数据了,这个过程就是回表。( |$ C5 t& S# a
按字段特性分类! \: t3 H4 i& x2 z' k/ `- T
从字段特性的角度来看,索引分为主键索引、唯一索引、普通索引、前缀索引。0 `4 [% M1 b7 b/ p9 K: _# x
主键索引主键索引就是建立在主键字段上的索引,通常在创建表的时候一起创建,一张表最多只有一个主键索引,索引列的值不允许有空值。
9 m# c1 f3 Z, T在创建表时,创建主键索引的方式如下:, d# ~: T. m1 M Q
CREATE TABLE table_name (6 E ]- m, b' h$ w
....
$ k, t; X: V4 T$ [! x. T: }) v: T PRIMARY KEY (index_column_1) USING BTREE
6 y+ s' t+ E D$ A- d' R8 a4 D);
0 M- f! r& V) T, q6 h3 J" ^唯一索引唯一索引建立在 UNIQUE 字段上的索引,一张表可以有多个唯一索引,索引列的值必须唯一,但是允许有空值。& \% \. D2 o' c G f3 b( z
在创建表时,创建唯一索引的方式如下:
! R0 v7 u4 r" oCREATE TABLE table_name (9 L6 T9 Z, {9 _% v! A7 O2 c% [
....
, C; S- {! T3 ]& U UNIQUE KEY(index_column_1,index_column_2,...) - Q( q2 L6 G# {
);
) j3 }# W) D/ h* w建表后,如果要创建唯一索引,可以使用这面这条命令:0 N$ q D* ^, D1 Q; b
CREATE UNIQUE INDEX index_name5 u0 g f$ m9 h6 ^
ON table_name(index_column_1,index_column_2,...);4 E4 Y* k, A' [! B+ \
普通索引普通索引就是建立在普通字段上的索引,既不要求字段为主键,也不要求字段为 UNIQUE。
4 x, A, k* m. ]6 |) c在创建表时,创建普通索引的方式如下:
. h) C/ Z& n, @! GCREATE TABLE table_name (
& k3 M9 o6 z) ~* T ....
1 C2 n: A/ k" K6 g5 S; a INDEX(index_column_1,index_column_2,...)
$ b: q+ F4 y) C5 W9 P);9 O& e' t) z% @# b$ i$ u
建表后,如果要创建普通索引,可以使用这面这条命令:" T; O- Y+ f# y; f1 t; _: z
CREATE INDEX index_name- l+ A! S O" X: C4 u
ON table_name(index_column_1,index_column_2,...);3 _# q {4 ^% L6 _
前缀索引前缀索引是指对字符类型字段的前几个字符建立的索引,而不是在整个字段上建立的索引,前缀索引可以建立在字段类型为 char、 varchar、binary、varbinary 的列上。
4 p; b& T1 J$ `4 C使用前缀索引的目的是为了减少索引占用的存储空间,提升查询效率。; [) I) k3 g8 {9 s) Z
在创建表时,创建前缀索引的方式如下:' Z6 ^& {6 c) J0 R# v/ z
CREATE TABLE table_name(
- M4 z; \" `: i( @9 O J/ r- k s column_list,
; N( q) o" n7 z5 v INDEX(column_name(length))
! _; Q# t" ^& u( L* ^' ?. Q);
8 j" k7 l% w* d/ B% T! [建表后,如果要创建前缀索引,可以使用这面这条命令:
/ ]! Q* n, Z" e$ }7 I7 ?8 e4 J: |CREATE INDEX index_name' D+ r# ~4 p8 t/ B3 ~* y7 k
ON table_name(column_name(length));3 @; F P* c# w; i' V. |
按字段个数分类1 i M4 V; |0 ~' |; S2 I
从字段个数的角度来看,索引分为单列索引、联合索引(复合索引)。
9 z% s8 U% w# ?1 |1 B1 k( z _: k建立在单列上的索引称为单列索引,比如主键索引;建立在多列上的索引称为联合索引;通过将多个字段组合成一个索引,该索引就被称为联合索引。
' ?( ?. B' M* ~5 L* D比如,将商品表中的 product_no 和 name 字段组合成联合索引(product_no, name),创建联合索引的方式如下:# G2 m9 L8 g8 Q& ?* c! u
CREATE INDEX index_product_no_name ON product(product_no, name);
+ |9 Q5 J. d2 G" B. \联合索引(product_no, name) 的 B+Tree 示意图如下(图中叶子节点之间我画了单向链表,但是实际上是双向链表,原图我找不到了,修改不了,偷个懒我不重画了,大家脑补成双向链表就行)。& L# F) i# C, o; j7 F5 [/ u
ibea4h5cthi64078577154.jpg
7 V: ?" ]! D* U c: J$ m) u9 K
可以看到,联合索引的非叶子节点用两个字段的值作为 B+Tree 的 key 值。当在联合索引查询数据时,先按 product_no 字段比较,在 product_no 相同的情况下再按 name 字段比较。
0 j- \6 Y5 ~) J& y也就是说,联合索引查询的 B+Tree 是先按 product_no 进行排序,然后再 product_no 相同的情况再按 name 字段排序。. X, V+ s$ r$ Q# h/ L! \# K- S: m
因此,使用联合索引时,存在最左匹配原则,也就是按照最左优先的方式进行索引的匹配。在使用联合索引进行查询的时候,如果不遵循「最左匹配原则」,联合索引会失效,这样就无法利用到索引快速查询的特性了。1 B6 D, G* g& C) c
比如,如果创建了一个 (a, b, c) 联合索引,如果查询条件是以下这几种,就可以匹配上联合索引:0 P; Y+ B0 H% A5 u7 ^2 w* m/ ~3 Y) c
where a=1;where a=1 and b=2 and c=3;where a=1 and b=2;需要注意的是,因为有查询优化器,所以 a 字段在 where 子句的顺序并不重要。: W2 B0 j9 A3 |; U0 M& G7 j* u
但是,如果查询条件是以下这几种,因为不符合最左匹配原则,所以就无法匹配上联合索引,联合索引就会失效:7 ^+ n$ O2 u# B7 P [( {7 B
where b=2;where c=3;where b=2 and c=3;上面这些查询条件之所以会失效,是因为(a, b, c) 联合索引,是先按 a 排序,在 a 相同的情况再按 b 排序,在 b 相同的情况再按 c 排序。所以,b 和 c 是全局无序,局部相对有序的,这样在没有遵循最左匹配原则的情况下,是无法利用到索引的。9 J7 j6 c p2 l, c! n2 v
联合索引有一些特殊情况,并不是查询过程使用了联合索引查询,就代表联合索引中的所有字段都用到了联合索引进行索引查询,也就是可能存在部分字段用到联合索引的 B+Tree,部分字段没有用到联合索引的 B+Tree 的情况。+ T3 u% G& s6 G, | V
这种特殊情况就发生在范围查询。联合索引的最左匹配原则会一直向右匹配直到遇到「范围查询」就会停止匹配。也就是范围查询的字段可以用到联合索引,但是在范围查询字段的后面的字段无法用到联合索引。9 C3 H" {" l7 r1 D% I$ {
有无排查索引失效的经验,展开讲讲?可以使用 EXPLAIN 来查看 SQL 的执行计划,判断SQL是否走了索引,如果没有走索引,就代表索引发生失效了。( {; e" p) |( G7 P: l
如下图,就是一个没有使用索引,并且是一个全表扫描的查询语句。
3 p# S: U! w6 ^' z
53a05fgp4c264078577254.jpg
6 a9 G1 m1 y( b0 ?. j( u对于执行计划,参数有:
, g' `& z. e; S9 u6 ]9 @possible_keys 字段表示可能用到的索引;key 字段表示实际用的索引,如果这一项为 NULL,说明没有使用索引;key_len 表示索引的长度;rows 表示扫描的数据行数。type 表示数据扫描类型,我们需要重点看这个。type 字段就是描述了找到所需数据时使用的扫描方式是什么,常见扫描类型的执行效率从低到高的顺序为:1 Y6 F6 I b9 F5 b4 I; U, N
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 显示的结果,这里说几个重要的参考指标:" I2 G. t+ s$ {% n2 P+ o1 [
Using filesort :当查询语句中包含 group by 操作,而且无法利用索引完成排序操作的时候, 这时不得不选择相应的排序算法进行,甚至可能会通过文件排序,效率是很低的,所以要避免这种问题的出现。Using temporary:使了用临时表保存中间结果,MySQL 在对查询结果排序时使用临时表,常见于排序 order by 和分组查询 group by。效率低,要避免这种问题的出现。Using index:所需数据只需在索引即可全部获得,不须要再到表中取数据,也就是使用了覆盖索引,避免了回表操作,效率不错。索引失效的场景有哪些?会发生索引失效的情况:
% j' F* s% W% g* F. ^当我们使用左或者左右模糊匹配的时候,也就是 like %xx 或者 like %xx%这两种方式都会造成索引失效;当我们在查询条件中对索引列使用函数,就会导致索引失效。当我们在查询条件中对索引列进行表达式计算,也是无法走索引的。MySQL 在遇到字符串和数字比较的时候,会自动把字符串转为数字,然后再进行比较。如果字符串是索引列,而条件语句中的输入参数是数字的话,那么索引列会发生隐式类型转换,由于隐式类型转换是通过 CAST 函数实现的,等同于对索引列使用了函数,所以就会导致索引失效。联合索引要能正确使用需要遵循最左匹配原则,也就是按照最左优先的方式进行索引的匹配,否则就会导致索引失效。在 WHERE 子句中,如果在 OR 前的条件列是索引列,而在 OR 后的条件列不是索引列,那么索引会失效。Redis Redis 为什么这么快?官方使用基准测试的结果是,单线程的 Redis 吞吐量可以达到 10W/每秒,如下图所示:1 M* Y' L8 v- Y
h1twxfhtg3w64078577354.jpg
. q, J8 b- {% r; u. n: S0 F6 e之所以 Redis 采用单线程(网络 I/O 和执行命令)那么快,有如下几个原因:
6 n( p8 _8 D+ f& d; i* oRedis 的大部分操作都在内存中完成,并且采用了高效的数据结构,因此 Redis 瓶颈可能是机器的内存或者网络带宽,而并非 CPU,既然 CPU 不是瓶颈,那么自然就采用单线程的解决方案了;Redis 采用单线程模型可以避免了多线程之间的竞争,省去了多线程切换带来的时间和性能上的开销,而且也不会导致死锁问题。Redis 采用了 I/O 多路复用机制处理大量的客户端 Socket 请求,IO 多路复用机制是指一个线程处理多个 IO 流,就是我们经常听到的 select/epoll 机制。简单来说,在 Redis 只运行单线程的情况下,该机制允许内核中,同时存在多个监听 Socket 和已连接 Socket。内核会一直监听这些 Socket 上的连接请求或数据请求。一旦有请求到达,就会交给 Redis 线程处理,这就实现了一个 Redis 线程处理多个 IO 流的效果。Redis 6.0 之后为什么引入了多线程?Redis 单线程指的是「接收客户端请求->解析请求 ->进行数据读写等操作->发送数据给客户端」这个过程是由一个线程(主线程)来完成的,这也是我们常说 Redis 是单线程的原因。
, c3 T R4 Y4 m( D但是,Redis 程序并不是单线程的,Redis 在启动的时候,是会启动后台线程(BIO)的:7 x2 n/ D( f2 v2 M- B
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 主线程就很容易发生阻塞,这样就无法处理后续的请求了。2 c/ K- V1 A2 J) W! h" ~1 o
后台线程相当于一个消费者,生产者把耗时任务丢到任务队列中,消费者(BIO)不停轮询这个队列,拿出任务就去执行对应的方法即可。
5 Q* n. }3 ]; {4 {
znfflvyj2bj64078577454.jpg
7 } b5 t8 L# e8 |7 r
虽然 Redis 的主要工作(网络 I/O 和执行命令)一直是单线程模型,但是在 Redis 6.0 版本之后,也采用了多个 I/O 线程来处理网络请求,这是因为随着网络硬件的性能提升,Redis 的性能瓶颈有时会出现在网络 I/O 的处理上。
# _$ h3 V8 d# e. E3 ]所以为了提高网络 I/O 的并行度,Redis 6.0 对于网络 I/O 采用多线程来处理。但是对于命令的执行,Redis 仍然使用单线程来处理,所以大家不要误解Redis 有多线程同时执行命令。
* l# Z. r- o5 p) SRedis 官方表示,Redis 6.0 版本引入的多线程 I/O 特性对性能提升至少是一倍以上。
1 m$ X0 W; o/ ^: z- fRedis 6.0 版本支持的 I/O 多线程特性,默认情况下 I/O 多线程只针对发送响应数据(write client socket),并不会以多线程的方式处理读请求(read client socket)。要想开启多线程处理客户端读请求,就需要把 Redis.conf 配置文件中的 io-threads-do-reads 配置项设为 yes。
+ k! V: l6 N1 K0 N//读请求也使用io多线程* O! F# X' t7 ?. i" J. P
io-threads-do-reads yes
5 f7 _% c' o' G) h+ q* I1 J* p同时, Redis.conf 配置文件中提供了 IO 多线程个数的配置项。
6 h2 e* j3 {/ @5 P5 d) ?3 f6 }// io-threads N,表示启用 N-1 个 I/O 多线程(主线程也算一个 I/O 线程)1 z3 E# Q+ |: i# Z& B
io-threads 48 v u# P) @2 C) {0 j! L
关于线程数的设置,官方的建议是如果为 4 核的 CPU,建议线程数设置为 2 或 3,如果为 8 核 CPU 建议线程数设置为 6,线程数一定要小于机器核数,线程数并不是越大越好。
+ j z5 z$ a2 i0 ]& k1 h因此, Redis 6.0 版本之后,Redis 在启动的时候,默认情况下会额外创建 6 个线程(这里的线程数不包括主线程):- i/ N7 I0 x7 `
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,同一时间只能有一个客户端拿到锁,其他客户端会陷入无限的等待来尝试获取那个锁,只有获取到锁的客户端才能执行下面的业务逻辑。0 F" A7 d; P4 y3 u0 N
比如说,用户要一次性买 10 台手机,那么避免超卖的流程如下:4 ^" ~* e6 n* j
只有一个订单系统实例可以成功加分布式锁,然后只有他一个实例可以查库存、判断库存是否充足、下单扣减库存,接着释放锁。释放锁之后,另外一个订单系统实例才能加锁,接着查库存,一下发现库存只有 2 个了,库存不足,无法购买,下单失败,不会将库存扣减为-8的,就避免超卖的问题。这种方案的缺点是同一个商品在多用户同时下单的情况下,会基于分布式锁串行化处理,导致没法同时处理同一个商品的大量下单的请求。 |
|