二、MySQL
2.1 MySQL的事务和引擎等问题
事务是一种机制,一个操作序列,包含一组数据库操作命令并且把所有命令作为一个整体统一向系统提交或撤销操作请求,即这一组数据库命令要么都执行,要么都不执行。
事务的特点的话就是常说的 ACID
- A(原子性): 要不成功,要不失败
- C(一致性):在事务开始之前和事务结束以后,数据库的完整性约束没有被破坏。
- I(隔离性):在并发环境中,当不同的事务同时操纵相同的数据时,每个事务都有各自的完整数据空间
- D(持久性):在事务完成以后,该事务所对数据库所作的更改便持久的保存在数据库之中,并不会被回滚。
事务的隔离级别
- Read uncommitted(读未提交):事务中的修改即使没有提交对其他事务都是可见的,也可以称为脏读,这个级别会导致很多问题,从性能上来说不会比其他隔离级别好太多,但缺乏其他隔离级别的很多好处。除非真的有特定的需求,一般很少用
- Read committed(读提交):大多数数据库默认的都是read committed,但是MySQL默认的不是这个!一个事务从执行到提交前,其他事务都是不可见的,有时候也可以叫不可重复读,因为两次执行同样的查询可能会得到不一样的查询结果
- Repeatable read(可重复读):repeatable read解决了read committed脏读的问题,这个隔离级别也是MySQL默认的隔离级别。该级别保证了同一个事务多次执行可以读取同样的数据,但是有个缺陷就是存在幻读!幻读就是当事务在某个范围内读取数据时,这时另一个事务在这个范围插入了数据,当读取的事务再次读取该范围时会产生幻行。通过多版本并发控制(MVCC)解决了幻读的问题。
- Serializable(串行化):这是最高的隔离级别,它通过强制事务在从串行上执行,避免了前面说的幻读问题,简单来说就在在读取数据时加一个锁,这就暴露了另一个问题,大量的加锁会导致出现争锁超时的问题。只有特定的需求情况下或者可以接收没有并发的情况下才考虑这种隔离级别。
MySQL常用的引擎主要有两种 MyISAM 和 InnoDB
MyISAM
MyISAM不支持事务,也不支持外键约束,只支持全文索引,数据文件和索引文件是分开保存的。访问速度快,对事务完整性没有要求。MyISAM适合查询、插入为主的应用场景
MyISAM在磁盘上存储成三个文件,文件名和表名都相同,但是扩展名分别为:
- 文件存储表结构的定义.frm
- 数据文件的扩展名为.MYD (MYData)
- 索引文件的扩展名是.MYI(MYIndex)
表级锁定形式,数据在更新时锁定整个表数据库在读写过程中相互阻塞(串行操作,按照顺序操作,每次在读或写的时候会把全表锁起来)会在数据写入的过程阻塞用户数据的读取也会在数据读取的过程中阻塞用户的数据写入 特性:数据单独写入或读取,速度过程较快且占用资源相对少
MyISAM表支持三种不同的存储格式
- 静态(固定长度)表: 静态表是默认的存储格式。静态表中的字段都是非可变字段,这样每个记录都是固定长度的,这种存储方式的优点是存储非常迅速,容易缓存,出现故障容易恢复;缺点是占用的空间通常比动态表多。
- 动态表: 动态表包含可变字段(varchar),记录不是固定长度的,这样存储的优点是占用空间较少,但是频繁的更新、删除记录会产生碎片,需要定期执行OPTIMIZE TABLE 语句或myisamchk -r 命令来改善性能,并且出现故障的时候恢复相对比较困难。
- 压缩表: 压缩表由 myisamchk工具创建,占据非常小的空间,因为每条记录都是被单独压缩的,所以只有非常小的访问开支
MyISAM适应场景
- 公司业务不需要事务的支持
- 单方面读取或写入数据比较多的业务
- MyISAM存储引擎数据读写都比较频繁场景不适合
- 使用读写并发访问相对较低的业务
- 数据修改相对较少的业务
- 对数据业务一致性要求不是非常高的业务服务器硬件资源相对比较差
InnoDB
InnoDB支持事务,外键,InnoDB不支持FULLTEXT类型的索引
InnoDB适合频繁修改以及涉及到安全性较高的应用。
InnoDB中不保存表的行数,如select count(*) from table时,InnoDB需要扫描一遍整个表来计算有多少行
清空整个表时,InnoDB是一行一行的删除,效率非常慢。MyISAM则会重建表
InnoDB支持行锁(某些情况下还是锁整表,如 update table set a=1 where user like '%lee%'
在磁盘存储:基于磁盘的资源是InnoDB表空间数据文件和它的日志文件,InnoDB 表的大小只受限于操作系统文件的大小,一般为 2GB
2.2 MySQL的调优看过吗?有调优经验吗?
- 当查询一条数据时,加上
limit 1
- 为搜素字段,经常查询的字段加索引 (一定不要给区分度不高的字段加索引,比如订单状态)
- 作关联查询时,一定要保证两个字段类型相同,索引相同。
- 查询肯定是避免
select *
- 字段值一定要设置 not null,减少存储空间而且进行比较时程序会更复杂。
- 设计表时,考虑固定长度的表会更快(表中没有如下类型的字段: VARCHAR,TEXT,BLOB)
- 越小的列越快(根据字段长度的大小设置对应的字段长度)
- in , not in,!=,<> 要慎用,否则会导致全表扫描
- 对于大表来说,使用偏移量查询数据可以采用子查询limit 嵌套大查询limit方式
- 谨慎使用order by 字段
- 禁用Query Cache
- 如果单表数据量较大的话,则进行分表,分表的单表数据量应该维持在1000万以内。
- 如果单库出现瓶颈,则进行分库,读写分离。
- 如果物理机器性能瓶颈,可以增加多个数据库节点,搭建分布式集群。主从库等等。
- 根据需求进行合理的存储。(比如身份证信息,如果要取后六位信息进行根据地区统计的话,如果按照正常存储,那就要建立18位的索引,索引越大占用空间就越大,数据页放的索引就越少,搜索效率就会越低。所以如果我们倒叙存储的话,我们建立索引时就可以直接建立一个6个字节。)
- 这里可以从innodb_buffer_pool_size,innodb_flush_log_at_trx_commit ,innodb_io_capactiy 这类参数入手,奈何我技术实力还不够,后续会慢慢深入
2.3 MySQL的索引失效有哪些场景?
- like
- 计算函数、函数处理
- 类型不匹配,隐式类型转换
- or
- <>
- OR语句前后没有同时使用索引
- 联合索引ABC问题
2.4 你在上文提到了慎用order by,可以聊聊原理吗?
-- 表定义 CREATE TABLE `t` ( `id` int(11) NOT NULL, `city` varchar(16) NOT NULL, `name` varchar(16) NOT NULL, `age` int(11) NOT NULL, `addr` varchar(128) DEFAULT NULL, PRIMARY KEY (`id`), KEY `city` (`city`) -- 为了避免全表扫描,我们给city加了索引,这样先去找 `city = '常州'` 的时 候就不需要全表了。 ) ENGINE=InnoDB; -- 查询语句 select city,name,age from t where city='常州' order by name limit 1000;
首先order by 有两种排序算法 全字段排序 和 rowid 排序
全字段排序
- 因为是order by语句,一开始会初始化一个sort_buffer 然后放入
city,name,age
这三个字段 - 先从索引树上找到第一个符合
city= '常州'
的数据的id (city,id都是索引,所以通过city是可以拿到当前city=常州
对应的id) - 去主键id索引取出整行数据,也就是select用到的
city,name,age
存入sort_buffer
- 再回到第二步,第三步重复操作,直至完成所有数据。
- 查完之后对
sort_buffer
的数据按照字段name快速排序 - 按照排序后的结果取出前1000行的数据
第五步中,有一个按照字段name快速排序,这里是有一个参数控制的。
sort_buffer_size:MySQL 为排序开辟的内存(sort_buffer)的大小。如果要排序的数据量小于 sort_buffer_size,排序就在内存中完成。但如果排序数据量太大,内存放不下,则不得不利用磁盘临时文件辅助排序。
外部: 当借助外部排序时,一般使用的是 归并排序算法。他会把整个sort_buffer分成多个临时文件,每一份临时文件单独进行排序,然后再把12个有序文件再合并成一个有序的大文件。
如果 sort_buffer_size 超过了需要排序的数据量的大小,number_of_tmp_files 就是 0,表示排序可以直接在内存中完成。
否则就需要放在临时文件中排序。sort_buffer_size 越小,需要分成的份数越多,number_of_tmp_files 的值就越大。
rowid排序
上面算法中,采用的是把数据统一写入sort_buffer或者临时中,但是这个算法有个问题,如果返回的字段很多的话,sort_buffer里放的字段数太多,导致内存中的行数太少。要分成很多个临时文件,排序的性能就会很差。
除了全字段排序,MySQL还引用了另一种排序算法, rowid排序
rowid排序需要先设置一个参数 SET max_length_for_sort_data = 16;
max_length_for_sort_data,是 MySQL 中专门控制用于排序的行数据的长度的一个参数。它的意思是,如果单行的长度超过这个值,MySQL 就认为单行太大,要换一个算法
根据city,name,age 的字段长度计算得知是 16+16+11(4) = 36,我们把 max_length_for_sort_data
设置为16。新的算法放入 sort_buffer 的字段,只有要排序的列(即 name 字段)和主键 id。但这时,排序的结果就因为少了 city 和 age 字段的值,不能直接返回了,整个执行流程就变成如下所示的样子:
- 初始化
sort_buffer
,确定放入两个字段,name
和id
- 从索引city上找到第一个满足杭州的数据,也就是主键id
- 再去主键索引上取出整行,也就是
name
,id
存入sort_buffer
- 重复第二步和第三步直至不满足条件为止。
- 为
sort_buffer
按照name排序 - 遍历排序结果,取前1000行,并按照id的值回到原表取出这个id 对应的city,name,age三个字段返回给客户端
rowid比全字段排序多了最后一步回表的过程。实际上MySQL服务器端从排序后的sort_buffer取出id,然后去原表查city,name,age字段时,不需要在服务端再耗费内存存储结果,是直接返回给客户端的。
从number_of_tmp_files结果可以得知。
number_of_tmp_files变小了,扫描的数据是不变的,但是字每一行变小了,因此需要排序的总数据量就变小了,需要的临时文件也相应地变少了。
排序算法分析
两种算法各有各的好处吧,实际的实战中我们应该如何选择呢?
- 如果 MySQL 实在是担心排序内存太小,会影响排序效率,才会采用 rowid 排序算法,这样排序过程中一次可以排序更多行,但是需要再回到原表去取数据。
- 如果 MySQL 认为内存足够大,会优先选择全字段排序,把需要的字段都放到 sort_buffer 中,这样排序后就会直接从内存里面返回查询结果了,不用再回到原表去取数据。
这也体现了MySQL的一个设计思想:如果内存够,就多用内存,减少磁盘的访问。
其实我们可以换一个思路解决,如果输出的字段不多,我们可以通过建立联合索引 + 插入时就按照那个字段排序 这样就不需要使用order by,sort_buffer了。
2.5 基本语句,如何增加列?
在一个表加一个列的话,首先应该看是大表还是小表。如果是小表的话,我感觉随意操作。大表的话就要留意一下加字段的时候会涉及到锁表操作。一旦锁表就会暂停这个表的所有业务。
我们可以通过创建一个中间表,然后转移那个表的索引信息。
最后通过insert into user (X,Y,Z) select X,Y,Z from user
转移真实数据。
最好在脱机的情况下执行,以免在迁移数据的时候,有新数据写入,导致新表数据流失不完整。
2.6 数据库主从复制原理
什么是主从复制?
主从复制是指一个MySQL数据库服务器的主节点复制到一个或多个从节点。默认采用异步复制方式,这样从节点不用一直访问主服务器来更新自己的数据,数据的更新可以在远程连接上进行,从节点可以复制主数据库中的所有数据库或特定的数据库,特定的数据表。
为什么需要主从复制?
- 防止在做写操作时,发生锁表,导致读请求无法响应。
- 数据备份,一旦主库挂了或者被删了,也可以通过从库做数据恢复
- 数据量变大之后,IO访问率过高,单机无法满足。通过多台机器降低磁盘IO访问的评率,提升单台机器的IO性能。
主从复制原理?
- master服务器将数据的改变记录记录二进制binlog日志,当maser上的数据发生改变时,则将其写入二进制日志文件中。
- slave服务器会在一定时间间隔内对master二进制日志进行探测其是否发生改变,如果发生改变,则开始一个IOThread请求master二进制事件
- 同时主节点为每个IO线程启动一个dump线程,用于向其发送二进制事件,并保存至从节点本地的relay log中,从节点将启动SQL线程从relay log中读取二进制日志,在本地重放,使得其数据和主节点的保持一致,最后IOThread和SQLThread将进入睡眠状态,等待下一次被唤醒。
如何保证主从数据一致?
在主从复制原理的基础上可以再深聊一下,主从数据是如何做到一致性的!主从数据同步主要是通过binlog日志
- 从库上通过
change master
命令,设置主库的IP,端口,用户名,密码以及日志文件名和偏移量。 - 在从库上执行
start slave
命令,这个时候从库会启动两个线程。一个是io_thread
和sql_thread
其中io_thread
主要负责与主库建立连接。 - 主库校验完用户名,密码,开始按照从库传过来的位置,从本地读取binlog发送给从库。
- 从库拿到主库传过来的binlog后,写到本地文件,这个本地文件就是常说中继文件(relay log)
sql_thread
读取中转日志,解析出日志里的命令,并执行。这样就达到了主从数据一致性的要求。
binlog是啥
上面聊了很多binlog,这里我们细聊一下binlog是啥。面试的时候肯定会细问的,毕竟是阿里面试,不可能问的那么粗浅。
binlog他是mysql的二进制日志,它记录了所有的DDL和DML语句。它是以事件形式记录的,还会包含所执行的消耗时间。binlog的主要目的是 复制 和 恢复
binlog主要有三种格式 row ,statement ,mixed
binlog_format = statement
- begin
- 在执行真实的SQL语句之前,会有一个
use 数据库名
命令。这条命令不是我们主动执行的,而是 MySQL 根据当前要操作的表所在的数据库,自行添加的。这样做可以保证日志传到备库去执行的时候,不论当前的工作线程在哪个库里,都能够正确地更新到 test 库的表 t - 到了中间部分就是完整的记录一个SQL的所有信息,连注释都会一起记录。
- 最后部分是COMMIT 。提交
这个参数是设置当前binlog 按照哪种格式存储的 binlog_format
binlog_format = row
- begin
- 有两个 event:Table_map 和 Delete_rows。
- commit,提交
Table_map event,用于说明接下来要操作的表是 test 库的表 t;
Delete_rows event,用于定义删除的行为。
通过以上信息是无法定位的,需要借助mysqlbinlog 工具才可以。
row格式的优点就是binlog日志记录是真实删除行的主键id,这样binlog日志传到备库的时候就会肯定删除对应的id。
statement的格式会有一个误操作。比如举个例子
-- a字段是一个索引 delete from user where a = 1 limit 1
如果是上述数据的话,有可能根据不同库的索引不同,导致删除不同的数据,最终导致主从库大量数据不一致。
binlog_format = mixed
之所以有mixed这种格式主要取决于三点。
- 因为有些 statement 格式的 binlog 可能会导致主备不一致,所以要使用 row 格式。
- 但 row 格式的缺点是,很占空间。比如你用一个 delete 语句删掉 10 万行数据,用 statement 的话就是一个 SQL 语句被记录到 binlog 中,占用几十个字节的空间。但如果用 row 格式的 binlog,就要把这 10 万条记录都写到 binlog 中。这样做,不仅会占用更大的空间,同时写 binlog 也要耗费 IO 资源,影响执行速度
- 所以,MySQL 就取了个折中方案,也就是有了 mixed 格式的 binlog。mixed 格式的意思是,MySQL 自己会判断这条 SQL 语句是否可能引起主备不一致,如果有可能,就用 row 格式,否则就用 statement 格式。(比较重要)
2.7 MySQL中都有哪些锁?
行锁与表锁
只有明确指定主键,才会执行行锁,否则执行表锁。
无锁
-- 当前这个表不存在主键 select * from user where id = -1 for update;
行锁
select * from user where id = -1 for update; select * from user where id = -1 and name = 'kkkk' for update;
表锁
-- 主键不明确 select * from user where name = 'kkk' for update; select * from user where id <>
锁算法(机制)
行锁算法
Record Lock(普通行锁)
- 键值在条件范围内
- 记录存在
GapLock(间隙锁)
- 对于键值不存在条件范围内,叫做 "间隙" (GAP),引擎就会对这个间隙加锁,这种机制就是Gap机制
Next-Key Lock(行&间隙锁)
- 在键值范围条件内,同时键值又不存在条件范围内
表锁算法
意向锁(升级机制)
- 当一个事务带着表锁去访问一个被加了行锁的资源,那么,此时这个行锁就会升级成意向锁,将表锁住。
-- 事务A 与 事务B 当前id=10这条数据中, name的值为kkk 就会升级 select * from user where id = 10 for update select * from user where name like 'kkk%' for update
自增锁
- 事务插入自增类型的列时,获取自增锁
如果一个事务正在往表中插入自增记录,其他事务都必须等待
实现
行锁和表锁其实是粒度的概念,共享锁和排它锁是他们的具体实现
共享锁
- 允许一个事务去读一行,组织其他事务去获取该行的排它锁
- 一般理解:能读,不能写
排它锁
- 允许持有排它锁的事务读写数据,阻止其他事务获取该资源的共享锁和排它锁。
- 不能获取任何锁,不代表不能读
注意点
- 某个事务获取数据的排它锁,其他事务不能获取该数据的任何锁,并不代表其他事务不能无锁读取该数据
乐观锁
一般通过版本号进行更新操作
update user set name = 'kkk' where id = 1
悲观锁
每次获取数据时,对该记录加排它锁,期间其他用户阻塞等待访问该记录。悲观锁适合写入频繁的场景。
select * from user where id = 1 for update; update user set buy_sum = buy_sum - 1 where id =1;
总结
- InnoDB行锁是通过给索引上的索引项加锁来实现的,只有通过索引条件检索数据,InnoDB才使用行级锁,否则,InnoDB将使用表锁。
- 由于MySQL的行锁是针对索引加的锁,不是针对记录加的锁,所以虽然是访问不同行的记录,但是如果是使用相同的索引键,是会出现锁冲突的。应用设计的时候要注意这一点。
- 当表有多个索引的时候,不同的事务可以使用不同的索引锁定不同的行,另外,不论是使用主键索引、唯一索引或普通索引,InnoDB都会使用行锁来对数据加锁。
- 即便在条件中使用了索引字段,但是否使用索引来检索数据是由MySQL通过判断不同执行计划的代价来决定的,如果MySQL认为全表扫描效率更高,比如对一些很小的表,它就不会使用索引,这种情况下InnoDB将使用表锁,而不是行锁。因此,在分析锁冲突时,别忘了检查SQL的执行计划,以确认是否真正使用了索引。
- 检索值的数据类型与索引字段不同,虽然MySQL能够进行数据类型转换,但却不会使用索引,从而导致InnoDB使用表锁。通过用explain检查两条SQL的执行计划,我们可以清楚地看到了这一点。
2.8 InnoDB引擎为什么使用B+树?
索引结构是主要分五块
- 哈希:只支持单值查询,不支持范围查询,所以innodb引擎无法以哈希结构为主结构
- 链表:体量增大后,对查询的时间复杂度还是比较鸡肋的,O(n)。
- 二叉树:使用二叉树时,如果当前插入的值的确是持续递增的就会出现树节点一边倒的情况,这样的话就有点类似于链表了,查询性能不好。
- 红黑树:在一定程序上的确解决了平衡问题,但是还没有完成解决。出现了层级较多这个问题。层级较多会影响查询性能
- B+树:在B树的基础上作了优化,也是红黑树之后的一个进化版。主要优化点就是数据节点的自旋。在插入时,当节点树大于某一个限制后会自动自旋,变成另一个节点树。而且具有排序的功能。节点与节点之间有连接关系,这是对查询非常有利的。
综合每个结构的优缺点进行权衡,最终选择B+树
2.9 如何发现慢SQL
- 首先要检查是否开启慢查询日志
slow_query_log 默认是off关闭的,使用时,需要改为on 打开
slow_query_log_file 记录的是慢日志的记录文件
long_query_time 默认是10S,每次执行的sql达到这个时长,就会被记录
- 查看慢查询状态
Slow_queries 记录的是慢查询数量 当有一条sql执行一次比较慢时,这个vlue就是1 (记录的是本次会话的慢sql条数)
如果显示非0,那么就是慢查询了,我们就可以根据具体的SQL去进行explain,检查执行计划。进行相应的SQL优化。
不走慢查询的话也可以通过,在编写SQL时进行习惯性的explain调优一下。
慢查询工具的话可以借助 mysqldumpslow 。优化策略 可以参考上面的索引,调优那个板块。
三、Spring
3.1 对Spring的理解?
概念
spring是Java企业级应用的开源开发框架
好处
轻量:Spring是轻量的,基本的版本大约2MB
控制反转:Spring通过控制反转实现了松散耦合,对象们给出它们的依赖,而不是创建或查找依赖的对象们
面向切面的编程(AOP):Spring支持面向切面的编程,并且把应用业务逻辑和系统服务分开
容器:Spring包含并管理应用中对象的生命周期和配置
MVC框架:Spring的WEB框架是个精心设计的框架,是Web框架的一个很好的替代品
事务管理:Spring提供一个持续的事务管理接口,可以扩展到上至本地事务下至全局事务(JTA)
异常处理:Spring提供方便的API把具体技术相关的异常(比如由JDBC,HibernateorJDO抛出的)转化为一致的unchecked异常
3.2 spring 和 springboot的区别?
springboot是spring家族中的一员,我觉得这两个都不在同一个纬度。说句心里话这道面试题我怀疑不是阿里面试真题。
3.3 说一下Bean的生命周期?注入过程?
1、解析xml配置或注解配置的类,得到BeanDefinition;
2、通过BeanDefinition反射创建Bean对象;
3、对Bean对象进行属性填充;
4、回调实现了Aware接口的方法,如BeanNameAware;
5、调用BeanPostProcessor的初始化前方法;
6、调用init初始化方法;
7、调用BeanPostProcessor的初始化后方法,此处会进行AOP;
8、将创建的Bean对象放入一个Map中;
9、业务使用Bean对象;或着是通过洼解配置的
10、Spring容器关闭时调用DisposableBean的destory)方法;
3.5 Spring Boot启动类注解是什么,有什么作用,自动装配原理?
// 目的是开启springboot的自动配置 @SpringBootApplication
由源码我们得知@SpringBootApplication 包含下列七种注解
@Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @SpringBootConfiguration @EnableAutoConfiguration @ComponentScan() public @interface SpringBootApplication { }
@Target(ElementType.TYPE) :说明了Annotation(注解)所修饰的对象范围
1.CONSTRUCTOR:用于描述构造器 2.FIELD:用于描述域 3.LOCAL_VARIABLE:用于描述局部变量 4.METHOD:用于描述方法 5.PACKAGE:用于描述包 6.PARAMETER:用于描述参数 7.TYPE:用于描述类、接口(包括注解类型) 或enum声明
@Retention(RetentionPolicy.RUNTIME): 注解按生命周期来划分可分为3类:
1、RetentionPolicy.SOURCE:注解只保留在源文件,当Java文件编译成class文件的时候,注解被遗弃; 2、RetentionPolicy.CLASS:注解被保留到class文件,但jvm加载class文件时候被遗弃,这是默认的生命周期; 3、RetentionPolicy.RUNTIME:注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在;
@Documented: 这个注解只是用来标注生成javadoc的时候是否会被记录。
@Inherited: 是一个标识,用来修饰注解,自定义注解当中会用到
@SpringBootConfiguration: 标注在某个类上,表示这是一个Spring Boot的配置类。(这个注解也是一个自定义注解)
@EnableAutoConfiguration: 需要配置的东西,Spring Boot会帮我们自动配置;
@EnableAutoConfiguration告诉SpringBoot开启自 动配置功能;这样自动配置才能生效;
点进去会发现@Import,说白了他就是借助@Import的支持,收集和注册特定场景相关的bean定义。
@Import作用:用于导入其他的配置类。而@EnableAutoConfiguration也是借助@Import的帮助,将所有符合自动配置条件的bean定义加载到IoC容器,仅此而已!
EnableAutoConfigurationImportSelector:导入哪些组件的选择器;会给容器中导入非常多的自动配置类(xxxAutoConfiguration);
大概的流程:
Spring Boot在启动的时候,通过EnableAutoConfigurationImportSelector类,从类路径下的 META-INF/spring.factories中获取EnableAutoConfiguration指定的值(就是上方截图), 以全类名反射的创建方式,将这些值作为自动配置类导入到容器中,自动配置类就生效, 帮我们进行自动配置工作;
以前我们需要自己配置的东西,自动配置类都帮我们配置好了,这也就是使用springboot在使用spring,springmvc不用配置视图解析器、数据库连接池、事务 等配置的原因。直接开箱即用。
当然springboot也给我提供了修改配置的方法,那就是通过yml或者propertie文件来进行修改springboot为我们配置好的配置默认值。
@ComponentScan: 用于通过注解指定spring在创建容器时要扫描的包
我们可以通过basePackages等属性来细粒度的定制@ComponentScan自动扫描的范围,如果不指定,则默认Spring框架实现会从声明@ComponentScan所在类的package进行扫描。
3.6 定时框架用过吗?
我了解的定时框架大概有三种
quartz
- 调用API的的方式操作任务,不人性化;
- 调度逻辑和QuartzJobBean耦合在同一个项目中,这将导致一个问题,在调度任务数量逐渐增多,同时调度任务逻辑逐渐加重的情况加,此时调度系统的性能将大大受限于业务;
- Quartz关注点在于定时任务而非数据,并无一套根据数据处理而定制化的流程。虽然Quartz可以基于数据库实现作业的高可用,但缺少分布式并行调度的功能。
- 支持分布式高可用,我们需要某个定时任务在多个节点中只有某个节点可以执行时,就需要Quartz来实现,否则使用@Scheduled等方式会造成所有节点都执行一遍。
- 支持持久化,Quartz有专门的数据表来实现定时任务的持久化。
- 支持多任务调度和管理,Quartz可以在数据库中存储多个定时任务进行作业调度,可以实现定时任务的增删改查等管理。
xxl-job
- 侧重的业务实现的简单和管理的方便,学习成本简单,失败策略和路由策略丰富。推荐使用在“用户基数相对少,服务器数量在一定范围内”的情景下使用。
elastic-job
- 关注的是数据,增加了弹性扩容和数据分片的思路,以便于更大限度的利用分布式服务器的资源。但是学习成本相对高些,推荐在“数据量庞大,且部署服务器数量较多”时使用。
这里我常用的是 Quartz,Quartz由三部分组成:
- 任务:JobDetail
- 触发器:Trigger(分为SimpleTrigger和CronTrigger)
- 调度器:Scheduler
JobDetail主要由JobKey(job的名字name和分组group)、JobClass、JobDataMap(任务相关的数据)、JobBuilder组成。常用的是前几个。
Trigger规定触发执行Job实现类,主要有SimpleTrigger和CronTrigger两个实现类。Trigger由以下部分组成:
- TriggerKey(job的名字name和分组group)
- JobDataMap(Trigger相关的数据,同JobDetail中JobDataMap,存相同key,若value不同,会覆盖前者。)
- ScheduleBuilder(有CronScheduleBuilder、SimpleScheduleBuilder、CalendarIntervalScheduleBuilder、DailyTimeIntervalScheduleBuilder常用前2种。)
Scheduler 调度器就是为了读取触发器Trigger从而触发定时任务JobDetail。可以通过SchedulerFactory进行创建调度器,分为
StdSchedulerFactory(常用)和DirectSchedulerFactory
两种。
- StdSchedulerFactory使用一组属性(放在配置文件中)创建和初始化调度器,然后通过
getScheduler()
方法生成调度程序。- DirectSchedulerFactory不常用,容易硬编码。
3.7 Spring IOC和AOP讲讲你的理解?
IOC
IOC(Inversion of Control) ,即控制反转,不是具体的技术,而是一种思想,IOC意味着将你设计好的对象交给Spring容器来管理,而不是传统的在你的对象内部直接控制。对于spring框架来说,就是由spring来负责控制对象的生命周期和对象间的关系。
IOC或DI(Dependency Injection,依赖注入)把应用的代码量降到最低。它使应用容易测试,单元测试不再需要单例和JNDI查找机制。最小的代价和最小的侵入性使松散耦合得以实现。IOC容器支持加载服务时的饿汉式初始化和懒加载。
Spring IOC负责创建对象、管理对象,通过依赖注入(DI),装配对象,配置对象,并且管理这些对象的整个生命周期。
IOC主要有两种注入方式
- 构造器依赖注入:构造器依赖注入通过容器触发一个类的构造器来实现的,该类有一系列参数,每个参数代表一个对其他类的依赖。
- Setter方法注入:Setter方法注入是容器通过调用无参构造器或无参static工厂方法实例化bean之后,调用该bean的setter方法,即实现了基于setter的依赖注入。
AOP
如果说 IoC 是 Spring 的核心,那么面向切面编程就是 Spring 最为重要的功能之一了,在数据库事务中切面编程被广泛使用。
AOP模块用于发给我们的Spring应用做面向切面的开发,这样就确保了Spring和其他AOP框架的共通性。
AOP能够将那些与业务无关,却为业务模块所共同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可拓展性和可维护性。
例如 日志 拦截器 过滤器 事务管理 性能统计 都是采用AOP面向切面编程
四、Mybatis
4.1 MyBatis框架的优缺点及其适用的场合
优点
- 与JDBC相比,减少了50%以上的代码量。
- MyBatis是易学的持久层框架,小巧并且简单易学。
- MyBatis相当灵活,不会对应用程序或者数据库的现有设计强加任何影响,SQL写在XML文件里,从程序代码中彻底分离,降低耦合度,便于统一的管理和优化,并可重用。
- 提供XML标签,支持编写动态的SQL,满足不同的业务需求。
- 提供映射标签,支持对象与数据库的ORM字段关系映射。
缺点
- SQL语句的编写工作量较大,对开发人员编写SQL的能力有一定的要求。
- SQL语句依赖于数据库,导致数据库不具有好的移植性,不可以随便更换数据库。
适用场景
- MyBatis专注于SQL自身,是一个足够灵活的DAO层解决方案。对性能的要求很高,或者需求变化较多的项目,例如Web项目,那么MyBatis是不二的选择。
4.2 mapper接口与xml关联原理?
mapper接口是不能重载的。而且必须做到全限名。不能存在多个文件。
mapper接口的工作原理就是通过jdk动态代理的形式。mybatis运行时,会使用动态代理的形式生成对象MapperProxy,代理对象会拦截接口方法,也就是执行invoke函数,invoke函数内操作也就是直接操作sqlsession。
继续利用statement(执行接口所对于的xml内函数的ID)拿到mapperstatement对象,通过执行器Executor去执行具体的SQL执行结果返回
4.3 如何进行分页?分页原理?
使用RowBouds对象进行分页。它是针对ResultSet的内存分页的。mybatis分页原理的核心点是内存分页,而不是物理分页。所以性能是比较快的。
分页插件的实现原理就是一切还是老规矩执行,只是在插件内会有一个拦截的操作。拦截之后会重写SQL ,根据dialect方言,添加对应的物理分页语句和物理分页参数。
4.4 延迟加载(懒加载)
使用CGLIB创建目标对象的代理对象,当调用目标方法时,进入拦截器方法,比如调用a.getB().getName(),拦截器invoke()方法发现a.getB()是null值,那么就会单独发送事先保存好的查询关联B对象的sql,把B查询上来,然后调用a.setB(b),于是a的对象b属性就有值了,接着完成a.getB().getName()方法的调用。这就是延迟加载的基本原理。
4.5 一级,二级缓存?
一级缓存是存在session中,如果经过flush或者close,Cache将会被清空。close会释放PerpetualCache对象。导致对象不可用。clearCache会清空缓存但是PerpetualCache对象还是可用的。执行任何一个insert,update,delete,都会清空一级缓存
二级缓存是Application级别的缓存,它可以提高对数据库查询的效率,以提高应用的性能。默认情况是不开启二级缓存的,二级缓存是全局缓存。
何时命中缓存?
传入statementId,查询时要求的结果集中的结果范围,传递给java.sql.Statement要设置的参数值,sql字符串(执行过程中,如果满足以上条件,则命中缓存,虽然是多次执行SQL 但是只会执行一遍,第一遍过后的数据会从缓存中取)
执行流程?
一级缓存:client=>Executor=>Database 这三方进行通信,利用Executor与Local Cache进行数据缓存处理
二级缓存:client=>CachingExcutor=>Executor=>Database 这四方通信,利用CachingExcutor与缓存集群Configuration进行通信,缓存子集是Mapper namespace
4.6 不同的映射文件中,ID是否可以重复?原理?
不同的xml映射文件的寻址方式就是 namespace + id。存储的格式是Map。如果namespace相同的话,只根据id无法判断是哪个键值对,所以多个xml文件不同的命名空间是可以允许相同的id的,反之不允许
五、计算机基础与网络
5.1 TCP和HTTP的区别
TCP是 传输层协议,定义数据传输和连接方式的规范。握手过程中传送的包里不包含数据,三次握手完毕后,客户端与服务器才正式开始传送数据。
HTTP 超文本传送协议(Hypertext Transfer Protocol )是 应用层协议,定义的是传输数据的内容的规范。
HTTP协议中的数据是利用TCP协议传输的,特点是客户端发送的每次请求都需要服务器回送响应,它是TCP协议族中的一种,默认使用 TCP 80端口。
好比网络是路,TCP是跑在路上的车,HTTP是车上的人。每个网站内容不一样,就像车上的每个人有不同的故事一样。
5.2 是否了解操作系统,进程和线程的区别,资源争端怎办?
进程和线程都不是一个东西,无法办法进行比较。
这里可以举一个例子,进程就是QQ,线程就是QQ中的一个发送模块。
一个线程只可以属于一个进程,但是一个进程可以包含多个线程
资源争端也可以说是 死锁
死锁,是指多个进程在运行过程中因争夺资源而造成的一种僵局,当进程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进。
例如:系统中只有一台打印机,可供进程 A 使用,假定 A 已占用了打印机,若 B 继续要求打印机打印将被阻塞。
系统中的资源可以分为两类:
- 可剥夺资源:是指某进程在获得这类资源后,该资源可以再被其他进程或系统剥夺,CPU 和主存均属于可剥夺性资源;
- 不可剥夺资源,当系统把这类资源分配给某进程后,再不能强行收回,只能在进程用完后自行释放,如磁带机、打印机等。
如何解决死锁?
- 资源剥夺:挂起某些死锁进程,并抢占它的资源,将这些资源分配给其他死锁进程(但应该防止被挂起的进程长时间得不到资源);
- 撤销进程:强制撤销部分、甚至全部死锁进程并剥夺这些进程的资源(撤销的原则可以按进程优先级和撤销进程代价的高低进行);
- 进程回退:让一个或多个进程回退到足以避免死锁的地步。进程回退时自愿释放资源而不是被剥夺。要求系统保持进程的历史信息,设置还原点。
5.3 TCP三次握手,丢包如何解决?
TCP三次握手在发送数据包时,如果出现异常,主要分下面三种情况
- A发给B的syn中途被丢,没有到达B
- B发给A的syn+ack中途被丢,没有到达A
- A发给B的ack中途被丢,没有到达B
A发给B的syn中途被丢,没有到达B
A会周期性超时重传,直到收到B的确认
B发给A的syn+ack中途被丢,没有到达A
B会周期性的超时重传,直到收到A的确认
A发给B的ack中途被丢,没有到达B
A发完ack,单方面会认为TCP为Established状态,而B显然认为TCP为Active状态:
- 假定此时双方都没有数据发送,B会周期性超时重传,直到收到A的确认,收到之后B的TCP连接也会Established状态,双向可以发包
- 假定此时A有数据发送,B收到A的Data+ACK,自然会切换为Established状态,并接受A的Data
- 假定B有数据发送,数据发送不了,会周期性超时重传syn+ack,直到收到A的确认才可以发送数据。
客户端syn包超时重传的最大次数 是由 tcp_syn_retries 决定的。 默认为5次。
5.4 TCP拥塞控制详细说一下?
原因是有可能整个网络环境特别差,容易丢包,那么发送端就应该注意了。
主要用三种方法:
- 慢启动阈值 + 拥塞避免
- 快速重传
- 快速恢复
慢启动阈值 + 拥塞避免
对于拥塞控制来说,TCP 主要维护两个核心状态:
- 拥塞窗口(cwnd)
- 慢启动阈值(ssthresh)
在发送端使用拥塞窗口来控制发送窗口的大小。
然后采用一种比较保守的慢启动算法来慢慢适应这个网络,在开始传输的一段时间,发送端和接收端会首先通过三次握手建立连接,确定各自接收窗口大小,然后初始化双方的拥塞窗口,接着每经过一轮 RTT(收发时延),拥塞窗口大小翻倍,直到达到慢启动阈值。
然后开始进行拥塞避免,拥塞避免具体的做法就是之前每一轮 RTT,拥塞窗口翻倍,现在每一轮就加一个。
快速重传
在 TCP 传输过程中,如果发生了丢包,接收端就会发送之前重复 ACK,比如 第 5 个包丢了,6、7 达到,然后接收端会为 5,6,7 都发送第四个包的 ACK,这个时候发送端受到了 3 个重复的 ACK,意识到丢包了,就会马上进行重传,而不用等到 RTO (超时重传的时间)
选择性重传:报文首部可选性中加入 SACK 属性,通过 left edge 和 right edge 标志那些包到了,然后重传没到的包
快速恢复
如果发送端收到了 3 个重复的 ACK,发现了丢包,觉得现在的网络状况已经进入拥塞状态了,那么就会进入快速恢复阶段:
- 会将拥塞阈值降低为 拥塞窗口的一半
- 然后拥塞窗口大小变为拥塞阈值
- 接着 拥塞窗口再进行线性增加,以适应网络状况