开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

很用心的为你写了 9 道 MySQL 面试题

发表于 2020-04-18

MySQL 一直是本人很薄弱的部分,后面会多输出 MySQL 的文章贡献给大家,毕竟 MySQL 涉及到数据存储、锁、磁盘寻道、分页等操作系统概念,而且互联网对 MySQL 的注重程度是不言而喻的,后面要加紧对 MySQL 的研究。写的如果不好,还请大家见谅。

非关系型数据库和关系型数据库区别,优势比较

非关系型数据库(感觉翻译不是很准确)称为 NoSQL,也就是 Not Only SQL,不仅仅是 SQL。非关系型数据库不需要写一些复杂的 SQL 语句,其内部存储方式是以 key-value 的形式存在可以把它想象成电话本的形式,每个人名(key)对应电话(value)。常见的非关系型数据库主要有 Hbase、Redis、MongoDB 等。非关系型数据库不需要经过 SQL 的重重解析,所以性能很高;非关系型数据库的可扩展性比较强,数据之间没有耦合性,遇见需要新加字段的需求,就直接增加一个 key-value 键值对即可。

关系型数据库以表格的形式存在,以行和列的形式存取数据,关系型数据库这一系列的行和列被称为表,无数张表组成了数据库,常见的关系型数据库有 Oracle、DB2、Microsoft SQL Server、MySQL等。关系型数据库能够支持复杂的 SQL 查询,能够体现出数据之间、表之间的关联关系;关系型数据库也支持事务,便于提交或者回滚。

它们之间的劣势都是基于对方的优势来满足的。

MySQL 事务四大特性

一说到 MySQL 事务,你肯定能想起来四大特性:原子性、一致性、隔离性、持久性,下面再对这事务的四大特性做一个描述

  • 原子性(Atomicity): 原子性指的就是 MySQL 中的包含事务的操作要么全部成功、要么全部失败回滚,因此事务的操作如果成功就必须要全部应用到数据库,如果操作失败则不能对数据库有任何影响。

这里涉及到一个概念,什么是 MySQL 中的事务?

事务是一组操作,组成这组操作的各个单元,要不全都成功要不全都失败,这个特性就是事务。

在 MySQL 中,事务是在引擎层实现的,只有使用 innodb 引擎的数据库或表才支持事务。

  • 一致性(Consistency):一致性指的是一个事务在执行前后其状态一致。比如 A 和 B 加起来的钱一共是 1000 元,那么不管 A 和 B 之间如何转账,转多少次,事务结束后两个用户的钱加起来还得是 1000,这就是事务的一致性。
  • 持久性(Durability): 持久性指的是一旦事务提交,那么发生的改变就是永久性的,即使数据库遇到特殊情况比如故障的时候也不会产生干扰。
  • 隔离性(Isolation):隔离性需要重点说一下,当多个事务同时进行时,就有可能出现脏读(dirty read)、不可重复读(non-repeatable read)、幻读(phantom read) 的情况,为了解决这些并发问题,提出了隔离性的概念。

脏读:事务 A 读取了事务 B 更新后的数据,但是事务 B 没有提交,然后事务 B 执行回滚操作,那么事务 A 读到的数据就是脏数据

不可重复读:事务 A 进行多次读取操作,事务 B 在事务 A 多次读取的过程中执行更新操作并提交,提交后事务 A 读到的数据不一致。

幻读:事务 A 将数据库中所有学生的成绩由 A -> B,此时事务 B 手动插入了一条成绩为 A 的记录,在事务 A 更改完毕后,发现还有一条记录没有修改,那么这种情况就叫做出现了幻读。

SQL的隔离级别有四种,它们分别是读未提交(read uncommitted)、读已提交(read committed)、可重复读(repetable read) 和 串行化(serializable)。下面分别来解释一下。

读未提交:读未提交指的是一个事务在提交之前,它所做的修改就能够被其他事务所看到。

读已提交:读已提交指的是一个事务在提交之后,它所做的变更才能够让其他事务看到。

可重复读:可重复读指的是一个事务在执行的过程中,看到的数据是和启动时看到的数据是一致的。未提交的变更对其他事务不可见。

串行化:顾名思义是对于同一行记录,写会加写锁,读会加读锁。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行。

这四个隔离级别可以解决脏读、不可重复读、幻象读这三类问题。总结如下

事务隔离级别 脏读 不可重复读 幻读
读未提交 允许 允许 允许
读已提交 不允许 允许 允许
可重复读 不允许 不允许 允许
串行化 不允许 不允许 不允许

其中隔离级别由低到高是:读未提交 < 读已提交 < 可重复读 < 串行化

隔离级别越高,越能够保证数据的完整性和一致性,但是对并发的性能影响越大。大多数数据库的默认级别是读已提交(Read committed),比如 Sql Server、Oracle ,但是 MySQL 的默认隔离级别是 可重复读(repeatable-read)。

MySQL 常见存储引擎的区别

MySQL 常见的存储引擎,可以使用

1
复制代码SHOW ENGINES

命令,来列出所有的存储引擎

可以看到,InnoDB 是 MySQL 默认支持的存储引擎,支持事务、行级锁定和外键。

MyISAM 存储引擎的特点

在 5.1 版本之前,MyISAM 是 MySQL 的默认存储引擎,MyISAM 并发性比较差,使用的场景比较少,主要特点是

  • 不支持事务操作,ACID 的特性也就不存在了,这一设计是为了性能和效率考虑的。
  • 不支持外键操作,如果强行增加外键,MySQL 不会报错,只不过外键不起作用。
  • MyISAM 默认的锁粒度是表级锁,所以并发性能比较差,加锁比较快,锁冲突比较少,不太容易发生死锁的情况。
  • MyISAM 会在磁盘上存储三个文件,文件名和表名相同,扩展名分别是 .frm(存储表定义)、.MYD(MYData,存储数据)、MYI(MyIndex,存储索引)。这里需要特别注意的是 MyISAM 只缓存索引文件,并不缓存数据文件。
  • MyISAM 支持的索引类型有 全局索引(Full-Text)、B-Tree 索引、R-Tree 索引

Full-Text 索引:它的出现是为了解决针对文本的模糊查询效率较低的问题。

B-Tree 索引:所有的索引节点都按照平衡树的数据结构来存储,所有的索引数据节点都在叶节点

R-Tree索引:它的存储方式和 B-Tree 索引有一些区别,主要设计用于存储空间和多维数据的字段做索引,目前的 MySQL 版本仅支持 geometry 类型的字段作索引,相对于 BTREE,RTREE 的优势在于范围查找。

  • 数据库所在主机如果宕机,MyISAM 的数据文件容易损坏,而且难以恢复。
  • 增删改查性能方面:SELECT 性能较高,适用于查询较多的情况

InnoDB 存储引擎的特点

自从 MySQL 5.1 之后,默认的存储引擎变成了 InnoDB 存储引擎,相对于 MyISAM,InnoDB 存储引擎有了较大的改变,它的主要特点是

  • 支持事务操作,具有事务 ACID 隔离特性,默认的隔离级别是可重复读(repetable-read)、通过MVCC(并发版本控制)来实现的。能够解决脏读和不可重复读的问题。
  • InnoDB 支持外键操作。
  • InnoDB 默认的锁粒度行级锁,并发性能比较好,会发生死锁的情况。
  • 和 MyISAM 一样的是,InnoDB 存储引擎也有 .frm文件存储表结构 定义,但是不同的是,InnoDB 的表数据与索引数据是存储在一起的,都位于 B+ 数的叶子节点上,而 MyISAM 的表数据和索引数据是分开的。
  • InnoDB 有安全的日志文件,这个日志文件用于恢复因数据库崩溃或其他情况导致的数据丢失问题,保证数据的一致性。
  • InnoDB 和 MyISAM 支持的索引类型相同,但具体实现因为文件结构的不同有很大差异。
  • 增删改查性能方面,果执行大量的增删改操作,推荐使用 InnoDB 存储引擎,它在删除操作时是对行删除,不会重建表。

MyISAM 和 InnoDB 存储引擎的对比

  • 锁粒度方面:由于锁粒度不同,InnoDB 比 MyISAM 支持更高的并发;InnoDB 的锁粒度为行锁、MyISAM 的锁粒度为表锁、行锁需要对每一行进行加锁,所以锁的开销更大,但是能解决脏读和不可重复读的问题,相对来说也更容易发生死锁
  • 可恢复性上:由于 InnoDB 是有事务日志的,所以在产生由于数据库崩溃等条件后,可以根据日志文件进行恢复。而 MyISAM 则没有事务日志。
  • 查询性能上:MyISAM 要优于 InnoDB,因为 InnoDB 在查询过程中,是需要维护数据缓存,而且查询过程是先定位到行所在的数据块,然后在从数据块中定位到要查找的行;而 MyISAM 可以直接定位到数据所在的内存地址,可以直接找到数据。
  • 表结构文件上: MyISAM 的表结构文件包括:.frm(表结构定义),.MYI(索引),.MYD(数据);而 InnoDB 的表数据文件为:.ibd和.frm(表结构定义);

MySQL 基础架构

这道题应该从 MySQL 架构来理解,我们可以把 MySQL 拆解成几个零件,如下图所示

大致上来说,MySQL 可以分为 Server层和 存储引擎层。

Server 层包括连接器、查询缓存、分析器、优化器、执行器,包括大多数 MySQL 中的核心功能,所有跨存储引擎的功能也在这一层实现,包括 存储过程、触发器、视图等。

存储引擎层包括 MySQL 常见的存储引擎,包括 MyISAM、InnoDB 和 Memory 等,最常用的是 InnoDB,也是现在 MySQL 的默认存储引擎。存储引擎也可以在创建表的时候手动指定,比如下面

1
复制代码CREATE TABLE t (i INT) ENGINE = <Storage Engine>;

然后我们就可以探讨 MySQL 的执行过程了

连接器

首先需要在 MySQL 客户端登陆才能使用,所以需要一个连接器来连接用户和 MySQL 数据库,我们一般是使用

1
复制代码mysql -u 用户名 -p 密码

来进行 MySQL 登陆,和服务端建立连接。在完成 TCP 握手 后,连接器会根据你输入的用户名和密码验证你的登录身份。如果用户名或者密码错误,MySQL 就会提示 Access denied for user,来结束执行。如果登录成功后,MySQL 会根据权限表中的记录来判定你的权限。

查询缓存

连接完成后,你就可以执行 SQL 语句了,这行逻辑就会来到第二步:查询缓存。

MySQL 在得到一个执行请求后,会首先去 查询缓存 中查找,是否执行过这条 SQL 语句,之前执行过的语句以及结果会以 key-value 对的形式,被直接放在内存中。key 是查询语句,value 是查询的结果。如果通过 key 能够查找到这条 SQL 语句,就直接返回 SQL 的执行结果。

如果语句不在查询缓存中,就会继续后面的执行阶段。执行完成后,执行结果就会被放入查询缓存中。可以看到,如果查询命中缓存,MySQL 不需要执行后面的复杂操作,就可以直接返回结果,效率会很高。

但是查询缓存不建议使用

为什么呢?因为只要在 MySQL 中对某一张表执行了更新操作,那么所有的查询缓存就会失效,对于更新频繁的数据库来说,查询缓存的命中率很低。

分析器

如果没有命中查询,就开始执行真正的 SQL 语句。

  • 首先,MySQL 会根据你写的 SQL 语句进行解析,分析器会先做 词法分析,你写的 SQL 就是由多个字符串和空格组成的一条 SQL 语句,MySQL 需要识别出里面的字符串是什么,代表什么。
  • 然后进行 语法分析,根据词法分析的结果, 语法分析器会根据语法规则,判断你输入的这个 SQL 语句是否满足 MySQL 语法。如果 SQL 语句不正确,就会提示 You have an error in your SQL syntax

优化器

经过分析器的词法分析和语法分析后,你这条 SQL 就合法了,MySQL 就知道你要做什么了。但是在执行前,还需要进行优化器的处理,优化器会判断你使用了哪种索引,使用了何种连接,优化器的作用就是确定效率最高的执行方案。

执行器

MySQL 通过分析器知道了你的 SQL 语句是否合法,你想要做什么操作,通过优化器知道了该怎么做效率最高,然后就进入了执行阶段,开始执行这条 SQL 语句

在执行阶段,MySQL 首先会判断你有没有执行这条语句的权限,没有权限的话,就会返回没有权限的错误。如果有权限,就打开表继续执行。打开表的时候,执行器就会根据表的引擎定义,去使用这个引擎提供的接口。对于有索引的表,执行的逻辑也差不多。

至此,MySQL 对于一条语句的执行过程也就完成了。

SQL 的执行顺序

我们在编写一个查询语句的时候

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码SELECT DISTINCT
< select_list >
FROM
< left_table > < join_type >
JOIN < right_table > ON < join_condition >
WHERE
< where_condition >
GROUP BY
< group_by_list >
HAVING
< having_condition >
ORDER BY
< order_by_condition >
LIMIT < limit_number >

它的执行顺序你知道吗?这道题就给你一个回答。

FROM 连接

首先,对 SELECT 语句执行查询时,对FROM 关键字两边的表执行连接,会形成笛卡尔积,这时候会产生一个虚表VT1(virtual table)

首先先来解释一下什么是笛卡尔积

现在我们有两个集合 A = {0,1} , B = {2,3,4}

那么,集合 A * B 得到的结果就是

A * B = {(0,2)、(1,2)、(0,3)、(1,3)、(0,4)、(1,4)};

B * A = {(2,0)、{2,1}、{3,0}、{3,1}、{4,0}、(4,1)};

上面 A * B 和 B * A 的结果就可以称为两个集合相乘的 笛卡尔积

我们可以得出结论,A 集合和 B 集合相乘,包含了集合 A 中的元素和集合 B 中元素之和,也就是 A 元素的个数 * B 元素的个数

再来解释一下什么是虚表

在 MySQL 中,有三种类型的表

一种是永久表,永久表就是创建以后用来长期保存数据的表

一种是临时表,临时表也有两类,一种是和永久表一样,只保存临时数据,但是能够长久存在的;还有一种是临时创建的,SQL 语句执行完成就会删除。

一种是虚表,虚表其实就是视图,数据可能会来自多张表的执行结果。

ON 过滤

然后对 FROM 连接的结果进行 ON 筛选,创建 VT2,把符合记录的条件存在 VT2 中。

JOIN 连接

第三步,如果是 OUTER JOIN(left join、right join) ,那么这一步就将添加外部行,如果是 left join 就把 ON 过滤条件的左表添加进来,如果是 right join ,就把右表添加进来,从而生成新的虚拟表 VT3。

WHERE 过滤

第四步,是执行 WHERE 过滤器,对上一步生产的虚拟表引用 WHERE 筛选,生成虚拟表 VT4。

WHERE 和 ON 的区别

  • 如果有外部列,ON 针对过滤的是关联表,主表(保留表)会返回所有的列;
  • 如果没有添加外部列,两者的效果是一样的;

应用

  • 对主表的过滤应该使用 WHERE;
  • 对于关联表,先条件查询后连接则用 ON,先连接后条件查询则用 WHERE;

GROUP BY

根据 group by 字句中的列,会对 VT4 中的记录进行分组操作,产生虚拟机表 VT5。果应用了group by,那么后面的所有步骤都只能得到的 VT5 的列或者是聚合函数(count、sum、avg等)。

HAVING

紧跟着 GROUP BY 字句后面的是 HAVING,使用 HAVING 过滤,会把符合条件的放在 VT6

SELECT

第七步才会执行 SELECT 语句,将 VT6 中的结果按照 SELECT 进行刷选,生成 VT7

DISTINCT

在第八步中,会对 TV7 生成的记录进行去重操作,生成 VT8。事实上如果应用了 group by 子句那么 distinct 是多余的,原因同样在于,分组的时候是将列中唯一的值分成一组,同时只为每一组返回一行记录,那么所以的记录都将是不相同的。

ORDER BY

应用 order by 子句。按照 order_by_condition 排序 VT8,此时返回的一个游标,而不是虚拟表。sql 是基于集合的理论的,集合不会预先对他的行排序,它只是成员的逻辑集合,成员的顺序是无关紧要的。

SQL 语句执行的过程如下

什么是临时表,何时删除临时表

什么是临时表?MySQL 在执行 SQL 语句的过程中,通常会临时创建一些存储中间结果集的表,临时表只对当前连接可见,在连接关闭时,临时表会被删除并释放所有表空间。

临时表分为两种:一种是内存临时表,一种是磁盘临时表,什么区别呢?内存临时表使用的是 MEMORY 存储引擎,而临时表采用的是 MyISAM 存储引擎。

MEMORY 存储引擎:memory 是 MySQL 中一类特殊的存储引擎,它使用存储在内容中的内容来创建表,而且数据全部放在内存中。每个基于 MEMORY 存储引擎的表实际对应一个磁盘文件。该文件的文件名与表名相同,类型为 frm 类型。而其数据文件,都是存储在内存中,这样有利于数据的快速处理,提高整个表的效率。MEMORY 用到的很少,因为它是把数据存到内存中,如果内存出现异常就会影响数据。如果重启或者关机,所有数据都会消失。因此,基于 MEMORY 的表的生命周期很短,一般是一次性的。

MySQL 会在下面这几种情况产生临时表

  • 使用 UNION 查询:UNION 有两种,一种是UNION ,一种是 UNION ALL ,它们都用于联合查询;区别是 使用 UNION 会去掉两个表中的重复数据,相当于对结果集做了一下去重(distinct)。使用 UNION ALL,则不会排重,返回所有的行。使用 UNION 查询会产生临时表。
  • 使用 TEMPTABLE 算法或者是 UNION 查询中的视图。TEMPTABLE 算法是一种创建临时表的算法,它是将结果放置到临时表中,意味这要 MySQL 要先创建好一个临时表,然后将结果放到临时表中去,然后再使用这个临时表进行相应的查询。
  • ORDER BY 和 GROUP BY 的子句不一样时也会产生临时表。
  • DISTINCT 查询并且加上 ORDER BY 时;
  • SQL中用到 SQL_SMALL_RESULT 选项时;如果查询结果比较小的时候,可以加上 SQL_SMALL_RESULT 来优化,产生临时表
  • FROM 中的子查询;
  • EXPLAIN 查看执行计划结果的 Extra 列中,如果使用 Using Temporary 就表示会用到临时表。

MySQL 常见索引类型

索引是存储在一张表中特定列上的数据结构,索引是在列上创建的。并且,索引是一种数据结构。

在 MySQL 中,主要有下面这几种索引

  • 全局索引(FULLTEXT):全局索引,目前只有 MyISAM 引擎支持全局索引,它的出现是为了解决针对文本的模糊查询效率较低的问题。
  • 哈希索引(HASH):哈希索引是 MySQL 中用到的唯一 key-value 键值对的数据结构,很适合作为索引。HASH 索引具有一次定位的好处,不需要像树那样逐个节点查找,但是这种查找适合应用于查找单个键的情况,对于范围查找,HASH 索引的性能就会很低。
  • B-Tree 索引:B 就是 Balance 的意思,BTree 是一种平衡树,它有很多变种,最常见的就是 B+ Tree,它被 MySQL 广泛使用。
  • R-Tree 索引:R-Tree 在 MySQL 很少使用,仅支持 geometry 数据类型,支持该类型的存储引擎只有MyISAM、BDb、InnoDb、NDb、Archive几种,相对于 B-Tree 来说,R-Tree 的优势在于范围查找。

varchar 和 char 的区别和使用场景

MySQL 中没有 nvarchar 数据类型,所以直接比较的是 varchar 和 char 的区别

char :表示的是定长的字符串,当你输入小于指定的数目,比如你指定的数目是 char(6),当你输入小于 6 个字符的时候,char 会在你最后一个字符后面补空值。当你输入超过指定允许最大长度后,MySQL 会报错

varchar: varchar 指的是长度为 n 个字节的可变长度,并且是非Unicode的字符数据。n 的值是介于 1 - 8000 之间的数值。存储大小为实际大小。

Unicode 是一种字符编码方案,它为每种语言中的每个字符都设定了统一唯一的二进制编码,以实现跨语言、跨平台进行文本转换、处理的要求

使用 char 存储定长的数据非常方便、char 检索效率高,无论你存储的数据是否到了 10 个字节,都要去占用 10 字节的空间

使用 varchar 可以存储变长的数据,但存储效率没有 char 高。

什么是 内连接、外连接、交叉连接、笛卡尔积

连接的方式主要有三种:外连接、内链接、交叉连接

  • 外连接(OUTER JOIN):外连接分为三种,分别是左外连接(LEFT OUTER JOIN 或 LEFT JOIN) 、右外连接(RIGHT OUTER JOIN 或 RIGHT JOIN) 、全外连接(FULL OUTER JOIN 或 FULL JOIN)

左外连接:又称为左连接,这种连接方式会显示左表不符合条件的数据行,右边不符合条件的数据行直接显示 NULL

右外连接:也被称为右连接,他与左连接相对,这种连接方式会显示右表不符合条件的数据行,左表不符合条件的数据行直接显示 NULL

MySQL 暂不支持全外连接

  • 内连接(INNER JOIN):结合两个表中相同的字段,返回关联字段相符的记录。

  • 笛卡尔积(Cartesian product): 我在上面提到了笛卡尔积,为了方便,下面再列出来一下。

现在我们有两个集合 A = {0,1} , B = {2,3,4}

那么,集合 A * B 得到的结果就是

A * B = {(0,2)、(1,2)、(0,3)、(1,3)、(0,4)、(1,4)};

B * A = {(2,0)、{2,1}、{3,0}、{3,1}、{4,0}、(4,1)};

上面 A * B 和 B * A 的结果就可以称为两个集合相乘的 笛卡尔积

我们可以得出结论,A 集合和 B 集合相乘,包含了集合 A 中的元素和集合 B 中元素之和,也就是 A 元素的个数 * B 元素的个数

  • 交叉连接的原文是Cross join ,就是笛卡尔积在 SQL 中的实现,SQL中使用关键字CROSS JOIN来表示交叉连接,在交叉连接中,随便增加一个表的字段,都会对结果造成很大的影响。
1
复制代码SELECT * FROM t_Class a CROSS JOIN t_Student b WHERE a.classid=b.classid

或者不用 CROSS JOIN,直接用 FROM 也能表示交叉连接的效果

1
复制代码SELECT * FROM t_Class a ,t_Student b WHERE a.classid=b.classid

如果表中字段比较多,不适宜用交叉连接,交叉连接的效率比较差。

  • 全连接:全连接也就是 full join,MySQL 中不支持全连接,但是可以使用其他连接查询来模拟全连接,可以使用 UNION 和 UNION ALL 进行模拟。例如
1
2
3
4
5
复制代码(select colum1,colum2...columN from tableA ) union (select colum1,colum2...columN from tableB )


或
(select colum1,colum2...columN from tableA ) union all (select colum1,colum2...columN from tableB );

使用 UNION 和 UNION ALL 的注意事项

通过 union 连接的 SQL 分别单独取出的列数必须相同

使用 union 时,多个相等的行将会被合并,由于合并比较耗时,一般不直接使用 union 进行合并,而是通常采用 union all 进行合并

谈谈 SQL 优化的经验

  • 查询语句无论是使用哪种判断条件 等于、小于、大于, WHERE 左侧的条件查询字段不要使用函数或者表达式
  • 使用 EXPLAIN 命令优化你的 SELECT 查询,对于复杂、效率低的 sql 语句,我们通常是使用 explain sql 来分析这条 sql 语句,这样方便我们分析,进行优化。
  • 当你的 SELECT 查询语句只需要使用一条记录时,要使用 LIMIT 1
  • 不要直接使用 SELECT *,而应该使用具体需要查询的表字段。
  • 为每一张表设置一个 ID 属性
  • 避免在 WHERE 字句中对字段进行 NULL 判断
  • 避免在 WHERE 中使用 != 或 <> 操作符
  • 使用 BETWEEN AND 替代 IN
  • 为搜索字段创建索引
  • 选择正确的存储引擎,InnoDB 、MyISAM 、MEMORY 等
  • 使用 LIKE %abc% 不会走索引,而使用 LIKE abc% 会走索引
  • 对于枚举类型的字段(即有固定罗列值的字段),建议使用ENUM而不是VARCHAR,如性别、星期、类型、类别等
  • 拆分大的 DELETE 或 INSERT 语句
  • 选择合适的字段类型,选择标准是 尽可能小、尽可能定长、尽可能使用整数。
  • 字段设计尽可能使用 NOT NULL
  • 进行水平切割或者垂直分割

水平分割:通过建立结构相同的几张表分别存储数据

垂直分割:将经常一起使用的字段放在一个单独的表中,分割后的表记录之间是一一对应关系。

文章参考:

www.cnblogs.com/sharpest/p/…

blog.csdn.net/yl2isoft/ar…

www.cnblogs.com/jinianjun/a…

www.cnblogs.com/huihuixi/p/…

www.php.cn/faq/418056.…

blog.csdn.net/w516162189/…

baike.baidu.com/item/聚集索引/1…

blog.csdn.net/riemann_/ar…

blog.csdn.net/qq_39101581…

blog.csdn.net/csdn_hklm/a…

zhidao.baidu.com/question/30…

www.zhihu.com/question/24…

baike.baidu.com/item/索引/571…

www.cnblogs.com/ghostwu/p/8…

www.cnblogs.com/yuxiuyan/p/…

www.jb51.net/article/147…

www.cnblogs.com/zhangchaoco…

baike.baidu.com/item/myisam…

segmentfault.com/a/119000001…

www.csdn.net/gather_2e/M…

《极客时间》- MySQL实战45讲

www.cnblogs.com/wyaokai/p/1…

www.cnblogs.com/hhhhuanzi/p…

zhidao.baidu.com/question/55…

www.cnblogs.com/limuzi1994/…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

这一次彻底搞懂拥塞控制

发表于 2020-04-17

这一次彻底搞懂拥塞控制

在计算机网络当中,我们会反复听到过一个名词-拥塞控制。那么到底什么是网络拥塞?有那些拥塞控制方法?读完这篇文章,希望能让你有一个大概的了解。

网络拥塞的概念

先引一段百度百科对于网络拥塞的解释:

网络拥塞(congestion)是指在分组交换网络中传送分组的数目太多时,由于存储转发节点的资源有限而造成网络传输性能下降的情况。当网络发生拥塞时,一般会出现数据丢失,时延增加,吞吐量下降,严重时甚至会导致“拥塞崩溃”(congestion collapse)。通常情况下,当网络中负载过度增加致使网络性能下降时,就会发生网络拥塞。下图则描述了在有无拥塞控制的干预下,网络吞吐量随输入负载的增加的变化情况。

网络拥塞控制图

通过以上的描述,我们大概可以得到以下信息:

  • 网络拥塞往往是由于对资源的请求超出了存储转发节点的能力而导致的。
  • 网络拥塞可能会导致数据丢失,时延增加,吞吐量下降等问题。
  • 若出现拥塞而不进行控制,有可能会使整个网络情况恶化,甚至网络吞吐降为0。
  • 网络的拥塞状况与当前网络负载是密切相关的。

拥塞控制算法

在描述拥塞控制算法之前,我想先和大家介绍以下拥塞控制算法和流量控制算法的区别。

异同点

流量控制和拥塞控制的最大异同点其实就是他们针对的对象和解决的问题不同。

首先对于流量控制而言,它所针对的对象是正在通信两个主机。通过滑动窗口的动态变化确保双方的接受能力能够与发送能力想匹配(接收方告知发送方自己的缓存大小,从此控制发送端发送窗口的大小,从而达到流量控制的目的—-接收方控制发送方),而不至于出现超出缓存,主动丢包的情况。(PS:【关键点】1.端到端通信。2.滑动窗口)

对于拥塞控制而言,需要解决的是一个全局性的问题,因为可能局部的网络拥塞最终会造成全网的数据交换阻塞,最终导致全网瘫痪,只能通过重启网络来解决。出现这种问题主要是因为对局部网络当中的资源的需求大于超出该存储转发节点的处理能力,导致该节点无法处理其他的存储转发请求。最终导致其他请求超时重发,由此可能导致有更多的数据被注入到网络当中最终造成网络瘫痪。因此,拥塞控制涉及到的是所有的主机,路由器以及降低网络性能相关的所有因素。(拥塞控制的对象是发送方,且它并不涉及通信双方缓存的大小。指的是某一源端数据流在一个RTT内最多发送的数据包数)

那么介绍完了流量控制和拥塞控制的不同点。接下来,我们需要介绍以下两个参数:

  • 拥塞窗口cwnd
  • 慢开始门限ssthresh

发送方需要维护一个叫做拥塞窗口cwnd的状态变量,其值取决于网络的拥塞程度并且会跟随网络的拥塞程度动态变化。拥塞窗口的维护原则:如果网络没有发生拥塞,拥塞窗口就会不断增大(具体如何增大与ssthresh有关);但只要网络一发生拥塞,这个窗口就会减小。而判断网络发生的依据就是:超时报文(没有按时收到回复的报文,需要超时重传)。

发送方窗口的上限值 = Min [ rwnd, cwnd ](PS:rwnd是接收窗口的大小)

ssthresh也是进行拥塞控制时需要维护的一个状态变量:

  • 当cwnd < ssthresh时,使用***慢开始算法***
  • 打cwnd >= ssthresh时,使用***拥塞避免算法***

慢开始

当主机开始发送数据时,如果一下子将所有数据注入到网络当中,有可能会引起网络的阻塞。这是因为在我们刚刚开始进行数据传输时,发送端并不清楚网络的实际负载情况。因此,在这种场景下最好的一种办法就是先探测一下当前的网络状况,然后根据情况,不断尝试将拥塞窗口增大。举个例子来讲就是,我们在行军打仗的时候,一般不会选择让大部队直接进入一片陌生的区域。而是会先派一部分侦查员去侦察,根据侦查员反馈的情况,在将部队慢慢的分批次进入。通常在刚刚开始发送报文段时,都会先把拥塞窗口cwnd设置为一个最大报文段(MSS)的值。每次收到一个对新的报文段的确认之后,拥塞窗口最多增加一个MSS的数值。

img
根据上图我们可以看出来,每经过一个传输轮次,拥塞窗口cwnd就会加倍(2的幂次倍数增加)。这里的传输轮次指的是一次报文的往返时间RTT,不过这里之所以指出是传输轮次,是为了强调:整个的传输过程是将cwnd中的数据全部发送出去,并收到了对已发送的最后一个字节的确认(代表之前发送的数据全部得到确认)。但是为了防止拥塞窗口增长的过大而造成网络拥塞,还需要通过ssthresh状态变量对其进行限制。即当cwnd > ssthresh时,会改用拥塞避免算法而停用慢开始算法。

拥塞避免

拥塞避免算法是为了减缓拥塞窗口的增大速度,以此控制数据的注入。即每经过一个传输轮次就把发送方的拥塞窗口cwnd加1,而不是加倍。这样拥塞窗口cwnd就会按照线性规律缓慢增长。

介绍完了慢开始算法和拥塞避免算法之后,我们来看一下它们是如何搭配使用的:无论是在慢开始阶段还是拥塞避免阶段,只要发送方判断网络拥塞(这里的判断条件是发送方在指定时间内没有收到确认报文—判断已经发生超时),就要把ssthresh设置为当前发生拥塞时cwnd的一半,然后把拥塞窗口cwnd重新设置为1,执行慢开始算法。这样做的目的就是为了迅速减少主机发送到网络中的数据,使得发生拥塞的路由器有足够的时间可以将自己的堆压分组处理完。

img

快速重传

快重传要求接收方在收到一个失序报文时立刻响应,而不能等到自己发送数据的时候才捎带确认(防止超时!!!)。按照快重传算法的规定,发送方只要一连收到三个对同一数据报文的重复ACK就应该立即重传对方没有收到过的报文段,而不必等待到重传计时器RTO超时(有时候并不是因为网络原因)。—-PS:使用快重传最主要的原因是:对于个别非网络原因丢失的报文,发送方不会出现超时重传,可以及早发现并立即重传,也就不会误认为网络发生了拥塞,通过快重传算法可以将网络的吞吐提高20%。

img

快恢复

在执行快重传算法时,发送方一旦收到3个重复的ACK,就知道现在只丢失了个别的数据报文。于是不启动慢开始算法(因为判断不是因为网络拥塞原因而丢失的报文,因为收到了ACK的回复)而是选择执行快恢复算法。快恢复算法的过程如下:

  • 发送方将慢开始门限ssthresh值和拥塞窗口cwnd均降低为原先的一半。
  • 执行拥塞避免算法。

接下来的一个例子将整个拥塞避免过程当中用到的四种算法都进行了展示,希望大家能够仔细将其看懂并且理解:

img

以上就是对TCP拥塞控制知识的总结,希望能够帮助大家。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

第一弹!安排!安利10个让你爽到爆的IDEA必备插件!

发表于 2020-04-16

大家好,我是Guide哥。上篇文章《「讨论」IntelliJ IDEA vs Eclipse:哪个更适合Java工程师?》中留言区大量评论表明IDEA更香,逃不过真香定律啊!

img

这篇文章中我会介绍10个非常不错的IDEA插件以及它们常见功能的使用方法。

这一期内容搞 Gif 动态图花了很久,很多Gif图片上传到微信还提示过大,所以很多地方重新又录制了一遍Gif图。

概览:

  • IDE Features Trainer—IDEA交互式教程
  • RestfulToolkit—RESTful服务开发
  • Key Promoter X—快捷键
  • Presentation Assistant—快捷键展示
  • Codota—代码智能提示
  • Alibaba Java Code Guidelines—阿里巴巴 Java 代码规范
  • GsonFormat+RoboPOJOGenerator—JSON转类对象
  • Statistic—项目信息统计
  • Translation-必备的翻译插件
  • CamelCase-多种命名格式之间切换

“

👉 注意:这只是第一弹,后面的文章中,我会继续推荐一些我在工作中必备的 IDEA 插件以及他们的使用方法。

IDE Features Trainer—IDEA交互式教程

有了这个插件之后,你可以在 IDE 中以交互方式学习IDEA最常用的快捷方式和最基本功能。 非常非常非常方便!强烈建议大家安装一个,尤其是刚开始使用IDEA的朋友。

当我们安装了这个插件之后,你会发现我们的IDEA 编辑器的右边多了一个“Learn”的选项,我们点击这个选项就可以看到如下界面。

img

我们选择“Editor Basics”进行,然后就可以看到如下界面,这样你就可以按照指示来练习了!非常不错!

img

RestfulToolkit—RESTful服务开发

专为 RESTful 服务开发而设计的插件,有了它之后,你可以:

1.根据 URL 直接跳转到对应的方法定义 (Windows: ctrl+\ or ctrl+alt+n Mac:command+\ or command+alt+n )并且提供了一个 Services tree 的可视化显示窗口。 如下图所示:

img

2.作为一个简单的 http 请求工具来使用。

img

4.在请求方法上添加了有用功能: 复制生成 URL、复制方法参数…

我们选中的某个请求对应的方法然后右击,你会发现多了这样几个选项。我们选择Generate & Copy Full URL,这样你就把整个请求的路径复制下来了:http://localhost:9333/api/users?pageNum=1&pageSize=1 。

img

5.其他功能: java 类上添加 Convert to JSON 功能,格式化 json 数据 ( Windows: Ctrl + Enter; Mac: Command + Enter )。

我们选中的某个类对应的方法然后右击,你会发现多了这样几个选项。

img

当我们选择Convert to JSON的话,你会得到:

1
2
3
4
5
复制代码{
"username": "demoData",
"password": "demoData",
"rememberMe": true
}

Key Promoter X—快捷键

相信我!这一定是IDEA必备的一个插件。它的功能主要是在一些你本可以使用快捷键操作的地方提醒你用快捷键操作。 比如我直接点击tab栏下的菜单打开 Version Control(版本控制) 的话,这个插件就会提示我说你可以用快捷键 command+9或者shift+command+9打开,如下图所示:

img

除了这个很棒的功能之外,它还有一个功能我觉得非常棒,那就是展示出哪些快捷键你使用的次数最多!超级赞!!!

Guide哥:快捷键真的很重要!入职之后,每次看着同事们花里胡哨的快捷键操作,咔咔咔很快就完成了某个操作,我才深深意识到它的重要性。不夸张的说,你用IDEA开发,常用的快捷键不熟悉的话,效率至少降低 30%。

img

小伙,你使用快捷键进行操作的时候,是帅啊!但是,你给别人演示的时候,别人可能根本不知道你进行了什么快捷键操作。这个时候 Presentation Assistant 这个插件就站出来了!

Presentation Assistant—快捷键展示

安装这个插件之后,你用键盘快捷键所做的操作都会被展示出来,非常适合自己在录制视频或者给别人展示代码的时候使用。比如我使用快捷键 command+9打开 Version Control ,使用了这个插件之后的效果如下图所示:

img

Codota—代码智能提示

Codota 这个插件用于智能代码补全,它基于数百万Java程序,能够根据程序上下文提示补全代码。相比于IDEA自带的智能提示来说,Codota 的提示更加全面一些,如下图所示。

我们创建线程池现在变成下面这样:

img

上面只是为了演示这个插件的强大,实际上创建线程池不推荐使用这种方式, 推荐使用 ThreadPoolExecutor 构造函数创建线程池。我下面要介绍的一个阿里巴巴的插件-Alibaba Java Code Guidelines 就检测出来了这个问题,所以,Executors下面用波浪线标记了出来。

除了,在写代码的时候智能提示之外。你还可以直接选中代码然后搜索相关代码示例。

img

Codota 还有一个在线网站,在这个网站上你可以根据代码关键字搜索相关代码示例,非常不错!我在工作中经常会用到,说实话确实给我带来了很大便利。网站地址:www.codota.com/code ,比如我们搜索 Files.readAllLines相关的代码,搜索出来的结果如下图所示:

img

Codota 插件的基础功能都是免费的。你的代码也不会被泄露,这点你不用担心。

Alibaba Java Code Guidelines—阿里巴巴 Java 代码规范

阿里巴巴 Java 代码规范,对应的Github地址为:github.com/alibaba/p3c 。非常推荐安装!

安装完成之后建议将与语言替换成中文,提示更加友好一点。

img

根据官方描述:

“

目前这个插件实现了开发手册中的的53条规则,大部分基于PMD实现,其中有4条规则基于IDEA实现,并且基于IDEA Inspection实现了实时检测功能。部分规则实现了Quick Fix功能,对于可以提供Quick Fix但没有提供的,我们会尽快实现,也欢迎有兴趣的同学加入进来一起努力。目前插件检测有两种模式:实时检测、手动触发。

上述提到的开发手册也就是在Java开发领域赫赫有名的《阿里巴巴Java开发手册》。

你还可以手动配置相关 inspection规则:

img

这个插件会实时检测出我们的代码不匹配它的规则的地方,并且会给出修改建议。比如我们按照下面的方式去创建线程池的话,这个插件就会帮我们检测出来,如下图所示。

img

img
这个可以对应上 《阿里巴巴Java开发手册》 这本书关于创建线程池的方式说明。

img

GsonFormat+RoboPOJOGenerator—JSON转类对象

这个插件可以根据Gson库使用的要求,将JSONObject格式的String 解析成实体类。

这个插件使用起来非常简单,我们新建一个类,然后在类中使用快捷键 option + s(Mac)或alt + s (win)调出操作窗口(必须在类中使用快捷键才有效),如下图所示。

img

这个插件是一个国人几年前写的,不过已经很久没有更新了,可能会因为IDEA的版本问题有一些小Bug。而且,这个插件无法将JSON转换为Kotlin(这个其实无关痛痒,IDEA自带的就有Java转Kotlin的功能)。

img

另外一个与之相似的插件是 :RoboPOJOGenerator ,这个插件的更新频率比较快。

1
复制代码File-> new -> Generate POJO from JSON

img

然后将JSON格式的数据粘贴进去之后,配置相关属性之后选择“Generate”

img

Statistic—项目信息统计

有了这个插件之后你可以非常直观地看到你的项目中所有类型的文件的信息比如数量、大小等等,可以帮助你更好地了解你们的项目。

img

你还可以使用它看所有类的总行数、有效代码行数、注释行数、以及有效代码比重等等这些东西。

img

Translation-必备的翻译插件

有了这个插件之后,你再也不用在编码的时候打开浏览器查找某个单词怎么拼写、某句英文注释什么意思了。

并且,这个插件支持多种翻译源:

  1. Google 翻译
  2. Youdao 翻译
  3. Baidu 翻译

除了翻译功能之外还提供了语音朗读、单词本等实用功能。这个插件的Github地址是:github.com/YiiGuxing/T… (貌似是国人开发的,很赞)。

使用方法很简单!选中你要翻译的单词或者句子,使用快捷键 command+ctrl+u(mac) / shift+ctrl+y(win/linux) (如果你忘记了快捷的话,鼠标右键操作即可!)

img

如果需要快速打开翻译框,使用快捷键command+ctrl+i(mac)/ctrl + shift + o(win/linux)

img

如果你需要将某个重要的单词添加到生词本的话,只需要点击单词旁边的收藏按钮即可!

CamelCase-多种命名格式之间切换

非常有用!这个插件可以实现包含6种常见命名格式之间的切换。并且,你还可以对转换格式进行相关配置(转换格式),如下图所示:

img

有了这个插件之后,你只需要使用快捷键 shift+option+u(mac) / shift+alt+u 对准你要修改的变量或者方法名字,就能实现在多种格式之间切换了,如下图所示:

img

如果你突然忘记快捷键的话,可以直接在IDEA的菜单栏的 Edit 部分找到。

img

使用这个插件对开发效率提升高吗?拿我之前项目组的情况举个例子:

我之前有一个项目组的测试名字是驼峰这种形式:ShouldReturnTicketWhenRobotSaveBagGiven1LockersWith2FreeSpace 。但是,使用驼峰形式命名测试方法的名字不太明显,一般建议用下划线_的形式:should_return_ticket_when_robot_save_bag_given_1_lockers_with_2_free_space

如果我们不用这个插件,而是手动去一个一个改的话,工作量想必会很大,而且正确率也会因为手工的原因降低。

“

👉 注意:这只是第一弹,后面的文章中,我会继续推荐一些我在工作中必备的 IDEA 插件以及他们的使用方法。

我的 75k Star 开源项目 JavaGuide 总结而成的PDF版本的**《JavaGuide面试突击版》**,公众号后台回复“面试突击”即可获取最新版本!安排!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

基于scrapy的可配置爬虫,大大提高工作效率

发表于 2020-04-15

原理说明

基于spalsh或者selenium的渲染后HTML,通过配置文件解析,入库。
提高了效率,一天可以写几十个配置dict,即完成几十个网站爬虫的编写。

配置文件说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制代码{
"industry_type": "政策", # 行业类别
"website_type": "央行", # 网站/微信公众号名称
"url_type": "中国人民银行-条法司-规范性文件", # 网站模块
"link": "http://www.pbc.gov.cn/tiaofasi/144941/3581332/index.html", # 访问链接
"article_rows_xpath": '//div[@id="r_con"]//table//tr/td/font[contains(@class, "newslist_style")]',
# 提取文章列表xpath对象
"title_xpath": "./a", # 提取标题
"title_parse": "./@title", # 提取标题
"title_link_xpath": "./a/@href", # 提取标题链接
"date_re_switch": "False", # 是否使用正则提取日期时间
"date_re_expression": "", # 日期时间正则表达式
"date_xpath": "./following-sibling::span[1]", # 提取日期时间
"date_parse": "./text()", # 提取日期时间
"content": '//*[@class="content"]', # 正文HTML xpath
"prefix": "http://www.pbc.gov.cn/", # link前缀
"config": "{'use_selenium':'False'}" # 其他配置:是否使用selenium(默认使用spalsh)
},

完整代码参考

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
复制代码# -*- coding: utf-8 -*-
'''
需求列表:使用任何资讯网站的抓取
央行:
http://www.pbc.gov.cn/tiaofasi/144941/3581332/index.html
http://www.pbc.gov.cn/tiaofasi/144941/144959/index.html

公安部:
https://www.mps.gov.cn/n2254314/n2254487/
https://www.mps.gov.cn/n2253534/n2253535/index.html
http://www.qth.gov.cn/xxsbxt/sxdw/gajxx/

'''
from risk_control_info.items import BIgFinanceNews
import dateparser

from w3lib.url import canonicalize_url
from urllib.parse import urljoin
import scrapy
from scrapy_splash import SplashRequest

from risk_control_info.utils import make_md5, generate_text, clean_string
import re

script = """
function main(splash, args)
splash.images_enabled = false
splash:set_user_agent("{ua}")
assert(splash:go(args.url))
assert(splash:wait(args.wait))
return splash:html()
end""".format(
ua="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.157 Safari/537.36")


class BigFinanceAllGovSpider(scrapy.Spider):
name = 'big_finance_all_gov'
custom_settings = {
'RANDOMIZE_DOWNLOAD_DELAY': True,
'DOWNLOAD_DELAY': 60 / 360.0,
'CONCURRENT_REQUESTS_PER_IP': 8,
'DOWNLOADER_MIDDLEWARES': {
'scrapy_splash.SplashCookiesMiddleware': 723,
'scrapy_splash.SplashMiddleware': 725,
'scrapy.downloadermiddlewares.httpcompression.HttpCompressionMiddleware': 810,
# 'risk_control_info.middlewares.SplashProxyMiddleware': 843, # 代理ip,此方法没成功
'risk_control_info.middlewares.RandomUserAgentMiddleware': 843,
'risk_control_info.middlewares.SeleniumMiddleware': 844
},
# 入库
'ITEM_PIPELINES': {
'risk_control_info.pipelines.RiskControlInfoPipeline': 401,
'risk_control_info.pipelines.MysqlPipeline': 402,
},
'SPIDER_MIDDLEWARES': {
'risk_control_info.middlewares.RiskControlInfoSpiderMiddleware': 543,
'scrapy_splash.SplashDeduplicateArgsMiddleware': 100,
},
}

def __init__(self, **kwargs):
super().__init__()
self.env = kwargs.get('env', 'online')

def start_requests(self):
for target in self.target_info():
if target.get('title_xpath') and target.get('title_link_xpath') \
and target.get('date_xpath') and target.get('article_rows_xpath'):
self.logger.info(f"目标网站可配置爬虫信息:{target}")
# 使用selenium
if target.get("config") and eval(eval(target.get("config")).get('use_selenium')):
self.logger.info(f"使用 Selenium 请求 {target['link']}")
yield scrapy.Request(url=target['link'],
meta={
"target": target,
"use_selenium": True
},
callback=self.parse,
)
else:
# 默认使用 Splash
self.logger.info(f"使用 Splash 请求 {target['link']}")
yield SplashRequest(url=target['link'],
meta={"target": target},
callback=self.parse,
# endpoint='execute',
# args={
# 'lua_source': script,
# 'wait': 8},
endpoint='render.json',
args={
# 'lua_source': script,
# 'proxy': f"http://{proxy_ip_dict['ip']}:{proxy_ip_dict['port']}",
'wait': 10,
'html': 1,
'png': 1
},
)

def parse(self, response):
target = response.meta['target']
article_rows = response.xpath(target['article_rows_xpath'])
# 遍历所有文字列表
for article_row in article_rows:
item = BIgFinanceNews()
# 处理标题
_article_row = article_row.xpath(target['title_xpath']) # 定位标题
item['title'] = clean_string(
generate_text(_article_row.xpath(target['title_parse']).extract_first().strip())) # 解析标题

# 处理链接
if target.get('prefix'):
item['title_link'] = urljoin(target['prefix'], article_row.xpath(
target['title_link_xpath']).extract_first())
else:
item['title_link'] = article_row.xpath(target['title_link_xpath']).extract_first()

# 处理发布日期
# 日期顺序规则
date_order = "YMD"
_title_time = article_row.xpath(target['date_xpath']) # 定位:发布时间
_date_str = clean_string(
generate_text(_title_time.xpath(target['date_parse']).extract_first())) # 解析:发布时间
if not eval(target.get('date_re_switch')):
item['title_time'] = dateparser.parse(_date_str, settings={'DATE_ORDER': date_order}).strftime(
"%Y-%m-%d")
else: # 使用正则提取时间字符串,存在默认正则表达式
date_re_expression = target.get('date_re_expression', None)
_expression = date_re_expression or r"(20\d{2}[-/]?\d{2}[-/]?\d{2})"
results = re.findall(r"%s" % _expression, _date_str, re.S)
self.logger.info(f"_date_str:{_date_str},results:{results} ")
if results:
item['title_time'] = dateparser.parse(results[0], settings={'DATE_ORDER': date_order}).strftime(
"%Y-%m-%d")
else:
item['title_time'] = None

# 以下写死的
item['bi_channel'] = "gov"
item['industry_type'] = f"{target['industry_type']}"
item['website_type'] = f"{target['website_type']}"
item['url_type'] = f"{target['url_type']}"
item['title_hour'] = 0 # 原网站没有发布时间,0代替
item['source_type'] = 0 # 数据来源,0 网站web, 1 微信公众号
item['redis_duplicate_key'] = make_md5(item['title'] + canonicalize_url(item['title_link']))

# 请求详情页
# 使用selenium
if target.get("config") and eval(eval(target.get("config")).get('use_selenium')):
self.logger.info(f"使用 Selenium 请求 {item['title_link']}")
yield scrapy.Request(url=item['title_link'],
meta={
"target": target,
"use_selenium": True,
"item": item
},
callback=self.parse_detail,
)
else:
# 使用 Splash
self.logger.info(f"使用 Splash 请求 {item['title_link']}")
yield SplashRequest(url=item['title_link'],
meta={
"target": target,
"item": item
},
callback=self.parse_detail,
# endpoint='execute',
# args={
# 'lua_source': script,
# 'wait': 8},
endpoint='render.json',
args={
# 'lua_source': script,
# 'proxy': f"http://{proxy_ip_dict['ip']}:{proxy_ip_dict['port']}",
'wait': 20,
'html': 1,
'png': 1
},
)

def parse_detail(self, response):
self.logger.info(f"处理详情页 {response.url}")
item = response.meta['item']
target = response.meta['target']
print(response.xpath(target['content']))
if response.xpath(target['content']):
item['content'] = generate_text(response.xpath(target['content']).extract_first())
else:
item['content'] = ""

yield item

@staticmethod
def target_info():
'''
返回目标网站信息
'''
target_list = [
{
"industry_type": "政策", # 行业类别
"website_type": "央行", # 网站/微信公众号名称
"url_type": "中国人民银行-条法司-规范性文件", # 网站模块
"link": "http://www.pbc.gov.cn/tiaofasi/144941/3581332/index.html", # 访问链接
"article_rows_xpath": '//div[@id="r_con"]//table//tr/td/font[contains(@class, "newslist_style")]',
# 提取文章列表xpath对象
"title_xpath": "./a", # 提取标题
"title_parse": "./@title", # 提取标题
"title_link_xpath": "./a/@href", # 提取标题链接
"date_re_switch": "False", # 是否使用正则提取日期时间
"date_re_expression": "", # 日期时间正则表达式
"date_xpath": "./following-sibling::span[1]", # 提取日期时间
"date_parse": "./text()", # 提取日期时间
"content": '//*[@class="content"]', # 正文HTML xpath
"prefix": "http://www.pbc.gov.cn/", # link前缀
"config": "{'use_selenium':'False'}" # 其他配置:是否使用selenium(默认使用spalsh)
},

{
"industry_type": "政策", # 行业类别
"website_type": "央行", # 网站/微信公众号名称
"url_type": "中国人民银行-条法司-其他文件", # 网站模块
"link": "http://www.pbc.gov.cn/tiaofasi/144941/144959/index.html", # 访问链接
"article_rows_xpath": '//div[@id="r_con"]//table//tr/td/font[contains(@class, "newslist_style")]',
"title_xpath": "./a",
"title_parse": "./@title",
"title_link_xpath": "./a/@href",
"date_re_switch": "False", # 是否使用正则提取日期时间
"date_re_expression": "", # 日期时间正则表达式
"date_xpath": "./following-sibling::span[1]",
"date_parse": "./text()",
"content": '//*[@class="content"]', # 正文HTML xpath
"prefix": "http://www.pbc.gov.cn/",
"config": "{'use_selenium':'False'}"
},

{
"industry_type": "政策", # 行业类别
"website_type": "公安部", # 网站/微信公众号名称
"url_type": "中华人民共和国公安部-规划计划", # 网站模块
"link": "https://www.mps.gov.cn/n2254314/n2254487/", # 访问链接
"article_rows_xpath": '//span/dl/dd',
"title_xpath": "./a",
"title_parse": "./text()",
"title_link_xpath": "./a/@href",
"date_re_switch": "True", # 是否使用正则提取日期时间 ( 2020-04-14 )
"date_re_expression": "", # 日期时间正则表达式
"date_xpath": "./span",
"date_parse": "./text()",
"content": '//*[@class="arcContent center"]', # 正文HTML xpath
"prefix": "https://www.mps.gov.cn/",
"config": "{'use_selenium':'True'}"
},

{
"industry_type": "政策", # 行业类别
"website_type": "公安部", # 网站/微信公众号名称
"url_type": "中华人民共和国公安部-公安要闻", # 网站模块
"link": "https://www.mps.gov.cn/n2253534/n2253535/index.html", # 访问链接
"article_rows_xpath": '//span/dl/dd',
"title_xpath": "./a",
"title_parse": "./text()",
"title_link_xpath": "./a/@href",
"date_re_switch": "True", # 是否使用正则提取日期时间 ( 2020-04-14 )
"date_re_expression": "", # 日期时间正则表达式
"date_xpath": "./span",
"date_parse": "./text()",
"content": '//*[@class="arcContent center"]', # 正文HTML xpath
"prefix": "https://www.mps.gov.cn/",
"config": "{'use_selenium':'True'}"
},

{
"industry_type": "政策", # 行业类别
"website_type": "公安部", # 网站/微信公众号名称
"url_type": "七台河市人民政府-信息上报系统-市辖单位-公安局", # 网站模块
"link": "http://www.qth.gov.cn/xxsbxt/sxdw/gajxx/", # 访问链接
"article_rows_xpath": '//td[contains(text(), "公安局")]/parent::tr/parent::tbody/parent::table/parent::td/parent::tr/following::tr[1]/td/table//tr/td/a/parent::td/parent::tr',
"title_xpath": "./td/a",
"title_parse": "./@title",
"title_link_xpath": "./td/a/@href",
"date_re_switch": "False", # 是否使用正则提取日期时间 ( 2020-04-14 )
"date_re_expression": "", # 日期时间正则表达式
"date_xpath": "./td[3]",
"date_parse": "./text()",
"content": '//*[@class="TRS_Editor"]', # 正文HTML xpath
"prefix": "http://www.qth.gov.cn/xxsbxt/sxdw/gajxx/",
"config": "{'use_selenium':'False'}"
},

]
for target in target_list:
yield target

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

《包你懂系列》Java 字符串常量池漫游指南(图文并茂)

发表于 2020-04-15

我是风筝,公众号「古时的风筝」,一个不只有技术的技术公众号,一个在程序圈混迹多年,主业 Java,另外 Python、React 也玩儿的 6 的斜杠开发者。
Spring Cloud 系列文章已经完成,可以到 我的 github 上查看系列完整内容。也可以在公众号内回复「pdf」获取我精心制作的 pdf 版完整教程。

字符串问题可谓是 Java 中经久不衰的问题,尤其是字符串常量池经常作为面试题出现。可即便是看似简单而又经常被提起的问题,还是有好多同学一知半解,看上去懂了,仔细分析起来却有发现不太明白。

背景说明

本文以 JDK 1.8 为讨论版本,虽然现在都已经 JDK 14了,奈何我们还是钟爱 1.8。

一个提问引起的讨论

为什么说到字符串常量呢,源于群里为数不多的一个程序员小姐姐的提问。


这本来和字符串常量没有关系,后来,一个同学说不只是 int ,换成 String 一样可以。


为什么会有”Java开发_北京”这么奇特的字符串乱入呢,因为提出问题的这位小姐姐的群昵称叫这个,所以群里的同学开玩笑说,以为她是某个房地产大佬,要来开发北京。


以上是开个玩笑,好了,收。

字符串用 == 比较也是 true,这就有意思了。马上有机灵的小伙伴说这和字符串常量池有关系。没错,就是因为字符串常量池的原因。

第一张图其实没什么好说的,在 JDK 1.8 之后已经不允许 Object 和 int 类型用 == 相比较了,编译直接报错。

第二张图中的代码才是重点要说的,我们可以把它简化成下面这段代码,用 == 符号比较字符串,之后的内容都从这几行代码出发。

1
2
3
4
复制代码public static void main(String[] args) {  
   String s1 = "古时的风筝";
   System.out.println(s1 == "古时的风筝");
}

当然,实际开发中强烈不推荐用 == 符号判断两个字符串是否相等,应该用 equals() 方法。

字符串常量池何许人也

为什么要有字符串常量池呢,像其他对象一样直接存在堆中不行吗,这就要问 Java 语言的设计者了,当然,这么做也并不是拍脑袋想出来的。

这就要从字符串说起。

首先对象的分配要付出时间和空间上的开销,字符串可以说是和 8 个基本类型一样常用的类型,甚至比 8 个基本类型更加常用,故而频繁的创建字符串对象,对性能的影响是非常大的,所以,用常量池的方式可以很大程度上降低对象创建、分配的次数,从而提升性能。

在 JDK 1.7 之后(包括1.7),字符串常量池已经从方法区移到了堆中。

字面量赋值

我们把上面的那个实例代码拿过来

1
复制代码String s1 = "古时的风筝";

这是我们平时声明字符串变量的最常用的方式,这种方式叫做字面量声明,也就用把字符串用双引号引起来,然后赋值给一个变量。

这种情况下会直接将字符串放到字符串常量池中,然后返回给变量。


那这是我再声明一个内容相同的字符串,会发现字符串常量池中已经存在了,那直接指向常量池中的地址即可。


例如上图所示,声明了 s1 和 s2,到最后都是指向同一个常量池的地址,所以 s1== s2 的结果是 true。

new String() 方式

与之对应的是用 new String() 的方式,但是基本上不建议这么用,除非有特殊的逻辑需要。

1
2
复制代码String a = "古时的";  
String s2 = new String(a + "风筝");

使用这种方式声明字符串变量的时候,会有两种情况发生。

第一种情况,字符串常量池之前已经存在相同字符串

比如在使用 new 之前,已经用字面量声明的方式声明了一个变量,此时字符串常量池中已经存在了相同内容的字符串常量。

  1. 首先会在堆中创建一个 s2 变量的对象引用;
  2. 然后将这个对象引用指向字符串常量池中的已经存在的常量;
第二种情况,字符串常量池中不存在相同内容的常量

之前没有任何地方用到了这个字符串,第一次声明这个字符串就用的是 new String() 的方式,这种情况下会直接在堆中创建一个字符串对象然后返回给变量。


我看到好多地方说,如果字符串常量池中不存在的话,就先把字符串先放进去,然后再引用字符串常量池的这个常量对象,这种说法是有问题的,只是 new String() 的话,如果池中没有也不会放一份进去。

基于 new String() 的这种特性,我们可以得出一个结论:

1
2
3
4
5
6
复制代码String s1 = "古时的风筝";  
String a = "古时的";
String s2 = new String(a + "风筝");
String s3 = new String(a + "风筝");
System.out.println(s1==s2); // false
System.out.println(s2==s3);  // false

以上代码,肯定输出的都是 false,因为 new String() 不管你常量池中有没有,我都会在堆中新建一个对象,新建出来的对象,当然不会和其他对象相等。

intern() 池化

那什么时候会放到字符串常量池呢,就是在使用 intern() 方法之后。

intern() 的定义:如果当前字符串内容存在于字符串常量池,存在的条件是使用 equas() 方法为ture,也就是内容是一样的,那直接返回此字符串在常量池的引用;如果之前不在字符串常量池中,那么在常量池创建一个引用并且指向堆中已存在的字符串,然后返回常量池中的地址。

第一种情况,准备池化的字符串与字符串常量池中的字符串有相同(equas()判断)
1
2
3
4
复制代码String s1 = "古时的风筝";  
String a = "古时的";
String s2 = new String(a + "风筝");
s2 = s2.intern();

这时,这个字符串常量已经在常量池存在了,这时,再 new 了一个新的对象 s2,并在堆中创建了一个相同字符串内容的对象。


这时,s1 == s2 会返回 fasle。然后我们调用 s2 = s2.intern(),将池化操作返回的结果赋值给 s2,就会发生如下的变化。


此时,再次判断 s1 == s2 ,就会返回 true,因为它们都指向了字符串常量池的同一个字符串。

第二种情况,字符串常量池中不存在相同内容的字符串

使用 new String() 在堆中创建了一个字符串对象


使用了 intern() 之后发生了什么呢,在常量池新增了一个对象,但是 并没有 将字符串复制一份到常量池,而是直接指向了之前已经存在于堆中的字符串对象。因为在 JDK 1.7 之后,字符串常量池不一定就是存字符串对象的,还有可能存储的是一个指向堆中地址的引用,现在说的就是这种情况,注意了,下图是只调用了 s2.intern(),并没有返回给一个变量。其中字符串常量池(0x88)指向堆中字符串对象(0x99)就是intern() 的过程。


只有当我们把 s2.intern() 的结果返回给 s2 时,s2 才真正的指向字符串常量池。

我明白了

通过以上的介绍,我们来看下面的一段代码返回的结果是什么

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
复制代码public class Test {  

    public static void main(String[] args) {
        String s1 = "古时的风筝";
        String s2 = "古时的风筝";
        String a = "古时的";
      
        String s3 = new String(a + "风筝");
        String s4 = new String(a + "风筝");
        System.out.println(s1 == s2); // 【1】 true
        System.out.println(s2 == s3); // 【2】 false
        System.out.println(s3 == s4); // 【3】 false
        s3.intern();
        System.out.println(s2 == s3); // 【4】 false
        s3 = s3.intern();
        System.out.println(s2 == s3); // 【5】 true
        s4 = s4.intern();
        System.out.println(s3 == s4); // 【6】 true
    }
}

【1】:s1 == s2 返回 ture,因为都是字面量声明,全都指向字符串常量池中同一字符串。

【2】: s2 == s3 返回 false,因为 new String() 是在堆中新建对象,所以和常量池的常量不相同。

【3】: s3 == s4 返回 false,都是在堆中新建对象,所以是两个对象,肯定不相同。

【4】: s2 == s3 返回 false,前面虽然调用了 intern() ,但是没有返回,不起作用。

【5】: s2 == s3 返回 ture,前面调用了 intern() ,并且返回给了 s3 ,此时 s2、s3 都直接指向常量池的同一个字符串。

【6】: s3 == s4 返回 true,和 s3 相同,都指向了常量池同一个字符串。

为啥我字符串就不可变

字符串常量池的基础就是字符串的不可变性,如果字符串是可变的,那想一想,常量池就没必要存在了。假设多个变量都指向字符串常量池的同一个字符串,然后呢,突然来了一行代码,不管三七二十一,直接把字符串给变了,那岂不是 jvm 世界大乱。

字符串不可变的根本原因应该是处于安全性考虑。

我们知道 jvm 类型加载的时候会用到类名,比如加载 java.lang.String 类型,如果字符串可变的话,那我替换成其他的字符,那岂不是很危险。

项目中会用到比如数据库连接串、账号、密码等字符串,只有不可变的连接串、用户名和密码才能保证安全性。

字符串在 Java 中的使用频率可谓高之又高,那在高并发的情况下不可变性也使得对字符串的读写操作不用考虑多线程竞争的情况。

还有就是 HashCode,HashCode 是判断两个对象是否完全相等的核心条件,另外,像 Set、Map 结构中的 key 值也需要用到 HashCode 来保证唯一性和一致性,因此不可变的 HashCode 才是安全可靠的。

最后一点就是上面提到的,字符串对象的频繁创建会带来性能上的开销,所以,利用不可变性才有了字符串常量池,使得性能得以保障。

后话

知其然,也要知所以然。一知半解才不是我们追求的目标。不知道图画的够不够清晰,希望能帮助到对字符串常量池不甚了解的同学。

创作不易,小小的赞,大大的暖,快来温暖我。赞我!一点也不要客气。

我是风筝,公众号「古时的风筝」,一个在程序圈混迹多年,主业 Java,另外 Python、React 也玩儿的很 6 的斜杠开发者。可以在公众号中加我好友,进群里小伙伴交流学习,好多大厂的同学也在群内呦。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【奇技淫巧】Android组件化不使用 Router 如何实

发表于 2020-04-15

前言

越来越多的项目使用了组件化,组件之间的通信是一个比较重要的问题。ARouter 等路由方案为我们提供了解决办法。那么如果不使用 Router 如何实现组件间的界面跳转呢?

万能的 setClassName

从一个 Activity 跳转到另一个Activity 的最直接方法如下:

1
2
复制代码val intent = Intent(this, TestActivity::class.java)
startActivity(intent)

但是,采用这种方法,当原 activity 位于一个 module(例如 FeatureA )中,而目标 activity 位于另一个 module (FeatureB)中时,该怎么办?

我们可以使用 Intent 的 setClassName 方法

1
2
3
复制代码val intent = Intent()
intent.setClassName(this, “com.flywith24.demo.TestActivity”)
startActivity(intent)

但是这种方式硬编码目标 activity 的完整类名,如果 activity 的类名被更改或者移动,而且没有更改硬编码,则编译可以通过,但是运行时崩溃

如果可以自动生成 activity 完整类名就好了

使用插件

我们知道 activity 作为 Android 的组件之一需要在 Manifest 文件中声明

1
2
3
复制代码<activity android:name=”com.flywith24.demo.MainActivity” />

<activity android:name=”com.flywith24.demo.TestActivity” />

如果我们的数据是从 Manifest 中获得的,那么就解决了硬编码的问题了

有这样一个插件 ,在 build 时会将所有在 Manifest 中声明的 activity 的完整类名以静态常量的形式罗列到一个静态类中

1
2
3
4
5
6
7
8
复制代码object QuadrantConstants {

const val MAIN_ACTIVITY: String = "com.gaelmarhic.quadrant.MainActivity"

const val SECONDARY_ACTIVITY: String = "com.gaelmarhic.quadrant.SecondaryActivity"

const val TERTIARY_ACTIVITY: String = "com.gaelmarhic.quadrant.TertiaryActivity"
}

这样在使用时就避免了硬编码

1
2
3
复制代码val intent = Intent()
intent.setClassName(context, QuadrantConstants.MAIN_ACTIVITY)
startActivity(intent)

使用依赖注入

组件化中 app module 会依赖所有的功能 module ,因此如果我们使用依赖注入在 app 中将所有的目标 activity 的完整类名声明出来,也能达到解决硬编码的问题

这里以 koin 为例

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码class MyApplication : Application() {
val myModule = module {
single { Feature2Activity::class.java.name }
}

override fun onCreate() {
super.onCreate()
startKoin {
androidContext(this@MyApplication)
modules(myModule)
}
}
}

这样通过 get() 方法即可拿到 Feature2Activity 的完整类名

1
2
3
4
复制代码val intent = Intent()
.setClassName(this@Feature1Activity, get())
.putExtra("key", "value")
startActivity(intent)

Demo

Demo 地址

各位有什么想法欢迎在评论区留言

关于我

我是 Flywith24,我的博客内容已经分类整理 在这里,点击右上角的 Watch 可以及时获取我的文章更新哦 😉

  • 掘金
  • 简书
  • Github

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Java8的Stream流真香,没体验过的永远不会知道

发表于 2020-04-15

原创:花括号MC(微信公众号:huakuohao-mc)。关注JAVA基础编程及大数据,注重经验分享及个人成长。

虽然现在Oacle官方发布的最新JDK版本已经到了JDK14。但我相信很多团队的生产系统上还是JDK8,甚至有的团队还是JDK7或者JDK6。即便很多团队已经将生产环境升级为JDK8,但是代码却还是老代码,也就是说根本没有使用JDK8提供的新特性。

JDK8 给程序员来带了很多便利,甚至可以让Java程序员跟Python,Ruby等程序员撕逼的时候,也能够扬眉吐气一把;因为JDK8终于开始支持“行为参数化了”,也就是大家经常说的,可以把一个函数当作参数传给另一个函数。

JDK8最明显的两个变化就是开始支持Lambda表达式,以及集合的Stream流式处理。这两个特性都可以让我们写的代码更优雅,也能让我们在实现某些功能的时候更轻松,特别是Stream,那简直是超级好用,性价比超级高,花上20分钟去学习一下,就能让你的代码质量提升一个档次。

下面我列举几个简单的小场景,让各位感受一下Java8提供的StreamAPI是多么的方便优雅,也算是抛砖引玉了。

日常编码的时候用的最多的应该就是集合了。比如从数据库里查询出一天卖出去多少本书,一般我们会这样写,List<Book> books = query.find(date); 然后针对这个集合会做各种各样的操作来满足产品层面的需求。

场景一:打印出每本书的详情。

JDK8之前

1
2
3
4
复制代码//打印出每本书的详情
for (Book book : books){
System.out.println(book.toString());
}

使用Stream之后

1
2
复制代码//打印每本书详情
books.stream().forEach(System.out::print);
场景二:选出价格高于20元的

JDK8之前

1
2
3
4
5
6
复制代码List<Book> highPriceBooks = new ArrayList<>();
for (Book book : books){
if (book.getPrice()>20){
highPriceBooks.add(book);
}
}

使用Stream之后

1
复制代码highPriceBooks = books.stream().filter(book -> book.getPrice()>20).collect(Collectors.toList());
场景三: 按照书价排序

JDK8之前

1
2
3
4
5
6
7
8
9
10
11
12
复制代码Collections.sort(books, new Comparator<Book>() {
@Override
public int compare(Book o1, Book o2) {
if (o1.getPrice() > o2.getPrice()) {
return 1;
} else if (o1.getPrice() < o2.getPrice()) {
return -1;
} else {
return 0;
}
}
});

使用Stream之后

1
复制代码sortBooks = books.stream().sorted(Comparator.comparing(Book::getPrice)).collect(Collectors.toList());

如果你喜欢可以直接通过reversed() 反转,像这样

1
复制代码sortBooks = books.stream().sorted(Comparator.comparing(Book::getPrice).reversed()).collect(Collectors.toList());
场景四:获取所有书名

JDK8之前

1
2
3
4
复制代码List<String> bookNames = new ArrayList<>();
for (Book book : books){
bookNames.add(book.getName());
}

使用Stream之后

1
复制代码bookNames = books.stream().map(Book::getName).collect(Collectors.toList());
场景五:获得所有书价格总和

JDK8之前

1
2
3
4
5
复制代码//计算一天当中卖出的所有书的价格总和。
int totalNum = 0;
for (Book book : books){
totalNum += book.getPrice();
}

使用Stream之后

1
复制代码totalNum = books.stream().map(Book::getPrice).reduce(0,(a,b)->a+b);
还有更多

你以为JDK8的Stream就这么点本事吗?它还可以完成链式处理,像这样

1
2
复制代码//选出价格高于20的两个元素。
books.stream().filter(book -> book.getPrice()> 20).limit(2).collect(Collectors.toList());

此外如果你想利用你多核的CPU并行处理集合以提高计算速度,在JDK8中只需要简单的调用一下parallelStream 方法。就像这样books.parallelStream().forEach(book -> book.toString()); JDK自动帮你并行处理,厉不厉害。

好了,就写这么多了,更多的使用技巧还得你自己去挖掘体会,我要去撸代码了,拜拜!
记住一句话,Java8提供的那些新东西,值得你花更多的精力去学习研究。


推荐阅读:

Java程序员必读核心书单—基础版

Javaer运维指令合集(快餐版)

手把手教你搭建一套ELK日志搜索运维平台

·END·

花括号MCJava·大数据·个人成长

微信号:huakuohao-mc

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

DDD 中的那些模式 — 领域事件

发表于 2020-04-14

严格的说事件驱动并不是一种模式,应该是一种架构风格或者编程范式。但是领域驱动设计中事件驱动所涵盖的范围没有那么大,往往只是作为整个系统解决方案的一部分,所以我还是把它归类在模式的范畴内。

事件无论对业务人员还是开发者都是非常熟悉且容易理解的概念,因此无论是在日常的需求沟通,还是系统设计中,事件都是建立领域模型时非常有用的工具。而在「事件风暴」这样的分析方法中,「领域事件」更是不可或缺的元素。在继续介绍领域驱动设计中的事件之前,我们先了解一下为什么要使用「事件」模式。

为什么需要「领域事件」

在之前介绍 Aggregation 聚合的文章中曾谈及,Aggregation 一个显著的特点或是限制条件就是每个事务应该只更新一个 Aggregation。无疑这对于系统设计提出了不小的挑战,如何设计一个粒度适中,又能符合业务要求的 Aggregation 并不是一件容易的事情。但是领域事件为我们提供了一种更为优雅的解决方案,在 Aggregation 完成更新后产生一个新的事件并广播出去,由其他订阅该事件的订阅者完成其他 Aggregation 的更新。这样就解除了 Aggregation 之间的耦合。

而另一个能让领域事件大显身手的地方是不同限界上下文之间的交互。现在较为流行的架构风格是将不同的限界上下文作为不同的微服务,微服务之间通过 API 的形式交互。但是 API 并不是唯一的解决方案,在某些场景下基于消息中间件的事件模型能够更好的降低耦合,提升系统的弹性。

如何使用「领域事件」

虽然事件的概念对于开发人员很好理解,但是在实际项目中真正使用事件驱动模式的却很少。一部分原因是事件驱动模式缺少框架的支持,往往需要手工处理许多包括异常,顺序发送等工作。另一个原因是事件驱动的编程模型与顺序编程模型差异很大,在发出事件后就将程序的控制逻辑交给了事件的订阅者,在开发与问题排查时不是那么的直观与方便。所以接下来的部分是我在一些事件驱动模式上的实践经验,希望能对大家有所帮助。

对领域事件建模

领域事件是一个对象,因此同样需要建模,定义它的数据结构。在开始定义我们的领域事件之前,还是先介绍一下业务场景。

当一个保险理赔申请提交,通过一系列的流程审核,确定理赔金额等数据无误后会有专人进行最后的二次审批,如果审批通过就可以支付给保险受益人相关的费用。从业务上看,理赔审批通过后,会有一连串的后台业务操作,首先是财务费用以及相关凭证的生产,然后是理赔通知书的生成与发送,如果保单由于理赔终止则需要对保单进行进一步的操作。这些业务行为无疑引入了数个 Aggregation 对象,肯定无法通过唯一的 Aggregation 在一个事务内完成,所以必须引入领域事件。以下是业务与事件的关系图:

领域事件由 Aggregation 生成,在我们的这个场景中,Aggregation 就是理赔案件对象 — ClaimCase。而领域事件的名称的格式一般为产生这个事件的 Aggregation 的名称 + 产生事件的动词的过去式,这里产生事件的行为是审批 — approve,所以我们可以把这个领域事件的名称定为: ClaimCaseApproved。这其实和事件风暴中的建议也一样。

确定了名字之后,我们看一下事件内部的数据结构。ClaimCaseApproved 内部数据结构一般与产生它的 Aggragation 很相似,都是相对重要的领域对象数据,在我们的业务场景中,会有理赔的案件号,保险单号,事故日期等。需要注意的是,对于领域事件,通常需要增加额外的两个属性,一个是事件的发生日期,还有一个是事件的唯一编号。这两项对于问题的排查与调试,以及订阅事件方的处理都是必需的。以下是事件的示例代码:

1
2
3
4
5
6
7
8
复制代码public class ClaimCaseApproved {  
  private String eventId;
  private LocalDateTime occuredOn;
  private long claimCaseId;
  private long policyId;
  private LocalDateTime accidentDate;
  ……
}

事件的生成,发送与订阅

有了数据模型之后,我们需要考虑的是在一个分层架构中,应该将事件相关的代码放置于何处。至今为止并没有一个统一的规则,所以我介绍之前项目中曾经尝试过的方法,其中有好的地方也有不方便的地方,具体选择何种,就留给你自己了。

一种做法是在领域服务中处理事件发送与订阅的逻辑,而事件的生成由领域对象,即 Aggregation 负责。我们先看一下示例代码:

1
2
3
4
5
复制代码public class ClaimCase {  
  public ClaimCaseApproved approve() {
    ……
  }
}

这里的代码很简单,ClaimCase 是一个 Aggregation 的领域对象,而 approve 方法执行的是审批的业务逻辑,它的返回结果就是它所产生的事件。接着看一下领域服务的代码:

1
2
3
4
5
6
7
8
9
10
复制代码public class ClaimCaseService {  
  private DomainEventPublisher publisher;
  ……
  public void approve() {
    ClaimCase claimCase = .....;
    ClaimCaseApproved claimCaseApproved = claimCase.approve();
    publisher.publish(claimCaseApproved);
    ……
  }
}

领域服务 ClaimCaseService 调用领域对象的 approve 方法获得生成的领域事件后进行发送,这里的 DomainEventPublisher 只是一个接口,具体的实现会依赖与基础设施层。这种做法的问题在于需要领域对象显式的返回事件对象,如果你的领域对象的这个方法正好需要返回值,而 Java 又是一门不支持多个返回值的语言,那么就有些尴尬了,比较直白的解决方案就是引入第三方库,返回一个类似 Tuple 的数据结构。

还有一种事件生成的可选方案是在领域对象内部保留一个数据结构存储产生的事件,然后在领域服务中调用特定的方法获取已经产生的事件,再发送,示例的代码如下:

1
2
3
4
5
6
7
复制代码public class ClaimCase implements DomainEventGenerator {  
  private Map<DomainEventType, List<DomainEvent>> registeredDomainEvents;
  public void approve() {
      ……
      registerDomainEvent(new ClaimCaseApproved());
  }
}

这次 ClaimCase 方法不再返回对应的领域事件,而是将事件保存在内部的 Map 中。接着看一下 ClaimCaseService 的变化:

1
2
3
4
5
6
7
8
复制代码public class ClaimCaseService {  
  public void approve() {
  …
      claimCase.approve();
      Map<DomainEventType, List<DomainEvent>> registeredDomainEvents = claimCase.getRegisteredDomainEvents();
      publisher.publish(registeredDomainEvents);
 }
}

领域服务 ClaimCaseService 在调用了 claimCase 的 approve 方法后,显式的调用了 claimCase.getRegisteredDomainEvents 方法,获取领域对象内部注册的领域事件,然后再发送。

另一种则是事件的发送,处理逻辑放在应用服务层,即 Application Service 中,具体的细节和领域服务中大同小异,我就不赘述了。但是有几点是需要牢记的:

  1. 领域事件是领域逻辑的一部分,所以在领域层不应该依赖某些底层的框架或是中间件,例如直接依赖某个消息中间件的 api。
  2. 事件的发送应该是异步非阻塞的,不应该阻塞当前处理的线程。
  3. 设计上避免事件链的产生,即一个事件被处理后又产生了另一个事件,第二个事件的处理又产生了第三个事件,在设计没有注意的情况会变成一个环。(别问我是怎么知道的~)
  4. 考虑最终一致性的解决方案,记好日志,以及事件丢失的处理与排查方案。

使用框架

无论上述何种方法你可能都需要通过「观察者」这样的模式实现事件驱动的整个架构,但是如果你是使用 Java 的,就可以使用 Spring 这样的框架,通过依赖注入将事件的订阅,发布从领域模型中剥离出去。下面让我们看看如何使用 Spring 实现之前的例子:

1
2
3
复制代码public class ClaimCaseApproved extends ApplicationEvent {  
  ……
}

我们的领域事件变化不大,只是继承了由 Spring 提供的 ApplicationEvent 基类。 ClaimCaseService 中的 publisher 可以通过 Spring 的注入 Spring 的 ApplicationPublisher:

1
2
复制代码@Autowired  
private ApplicationEventPublisher applicationEventPublisher;

Spring 中把 Subscriber 称之为 Listerner,这里我们可以定义自己的订阅者:

1
2
3
4
5
6
7
8
9
10
11
12
复制代码@Component  
public class FinanceFeeSubscriber {
  @Autowared
  private FinanceClaimFeeApplicationService financeClaimFeeApplicationService;

  @Async
  @EventListener
  public void handleClaimCaseApproved(ClaimCaseApproved event) {
      financeClaimFeeApplicationService.generateClaimFeeFor(…);
      ……
  }
}

上面的代码中我们借助 Spring 的能力注入了费用模块的应用层服务 FinanceClaimFeeApplicationService,然后通过 @Async 与 @EventListener 两个 annotation 声明了处理领域事件的异步方法,在方法中我们调用了应用层服务完成了由于理赔审批通过引起的费用相关处理逻辑。

这里需要注意的是要将方法声明为异步,这样处理事件的方法就会在一个独立的线程中运行,不会阻塞发布事件的线程。其次是这里业务上对事件处理的顺序没有要求,因此可以并行处理,按照上述的代码,可以再创建两个订阅者,负责理赔通知书生成和保单终止的业务处理,彼此没有影响。

如果你不想使用 Spring 提供能够的事件机制,可以考虑使用 Google Gauva 提供的 EventBus,它提供了类似的功能,使用起来也非常简单。

最后要注意的是,无论使用 Spring 还是 Guava,事件数据都是保存在内存中的,如果遇到服务重启很可能就会丢失未处理的数据,因此在项目中一定要记录日志并想好如何处理事件丢失的问题,必要时需要手工触发重发事件等机制。

小结

事件驱动是非常贴合人类思维习惯的一种架构模式,而领域事件也是分析领域模型的优秀工具。虽然使用事件驱动的编程模型需要考虑一些额外的问题,例如线上的调试,事件的容错,重发等,但是毋庸置疑的是领域事件为我们提供了更好的解除耦合的手段,能够将大量复杂的业务逻辑拆分到不同的事件订阅者中处理,而彼此之间又保持着松耦合的关系。在项目允许的情况下,我强烈推荐领域事件这种模式,有兴趣的你不妨尝试一下!

本文使用 mdnice 排版

欢迎关注我的微信号「且把金针度与人」,获取更多高质量文章

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Java集合面试题(总结最全面的面试题)

发表于 2020-04-13

Java面试总结汇总,整理了包括Java重点知识,以及常用开源框架,欢迎大家阅读。文章可能有错误的地方,因为个人知识有限,欢迎各位大佬指出!文章持续更新中……

ID 标题 地址
1 设计模式面试题(总结最全面的面试题) juejin.cn/post/684490…
2 Java基础知识面试题(总结最全面的面试题) juejin.cn/post/684490…
3 Java集合面试题(总结最全面的面试题) juejin.cn/post/684490…
4 JavaIO、BIO、NIO、AIO、Netty面试题(总结最全面的面试题) juejin.cn/post/684490…
5 Java并发编程面试题(总结最全面的面试题) juejin.cn/post/684490…
6 Java异常面试题(总结最全面的面试题) juejin.cn/post/684490…
7 Java虚拟机(JVM)面试题(总结最全面的面试题) juejin.cn/post/684490…
8 Spring面试题(总结最全面的面试题) juejin.cn/post/684490…
9 Spring MVC面试题(总结最全面的面试题) juejin.cn/post/684490…
10 Spring Boot面试题(总结最全面的面试题) juejin.cn/post/684490…
11 Spring Cloud面试题(总结最全面的面试题) juejin.cn/post/684490…
12 Redis面试题(总结最全面的面试题) juejin.cn/post/684490…
13 MyBatis面试题(总结最全面的面试题) juejin.cn/post/684490…
14 MySQL面试题(总结最全面的面试题) juejin.cn/post/684490…
15 TCP、UDP、Socket、HTTP面试题(总结最全面的面试题) juejin.cn/post/684490…
16 Nginx面试题(总结最全面的面试题) juejin.cn/post/684490…
17 ElasticSearch面试题
18 kafka面试题
19 RabbitMQ面试题(总结最全面的面试题) juejin.cn/post/684490…
20 Dubbo面试题(总结最全面的面试题) juejin.cn/post/684490…
21 ZooKeeper面试题(总结最全面的面试题) juejin.cn/post/684490…
22 Netty面试题(总结最全面的面试题)
23 Tomcat面试题(总结最全面的面试题) juejin.cn/post/684490…
24 Linux面试题(总结最全面的面试题) juejin.cn/post/684490…
25 互联网相关面试题(总结最全面的面试题)
26 互联网安全面试题(总结最全面的面试题)

集合容器概述

什么是集合

  • 集合就是一个放数据的容器,准确的说是放数据对象引用的容器
  • 集合类存放的都是对象的引用,而不是对象的本身
  • 集合类型主要有3种:set(集)、list(列表)和map(映射)。

集合的特点

  • 集合的特点主要有如下两点:
    • 集合用于存储对象的容器,对象是用来封装数据,对象多了也需要存储集中式管理。
    • 和数组对比对象的大小不确定。因为集合是可变长度的。数组需要提前定义大小

集合和数组的区别

  • 数组是固定长度的;集合可变长度的。
  • 数组可以存储基本数据类型,也可以存储引用数据类型;集合只能存储引用数据类型。
  • 数组存储的元素必须是同一个数据类型;集合存储的对象可以是不同数据类型。

使用集合框架的好处

  1. 容量自增长;
  2. 提供了高性能的数据结构和算法,使编码更轻松,提高了程序速度和质量;
  3. 可以方便地扩展或改写集合,提高代码复用性和可操作性。
  4. 通过使用JDK自带的集合类,可以降低代码维护和学习新API成本。

常用的集合类有哪些?

  • Map接口和Collection接口是所有集合框架的父接口:
  1. Collection接口的子接口包括:Set接口和List接口
  2. Map接口的实现类主要有:HashMap、TreeMap、Hashtable、ConcurrentHashMap以及Properties等
  3. Set接口的实现类主要有:HashSet、TreeSet、LinkedHashSet等
  4. List接口的实现类主要有:ArrayList、LinkedList、Stack以及Vector等

List,Set,Map三者的区别?

在这里插入图片描述

  • Java 容器分为 Collection 和 Map 两大类,Collection集合的子接口有Set、List、Queue三种子接口。我们比较常用的是Set、List,Map接口不是collection的子接口。
  • Collection集合主要有List和Set两大接口
+ List:一个有序(元素存入集合的顺序和取出的顺序一致)容器,元素可以重复,可以插入多个null元素,元素都有索引。常用的实现类有 ArrayList、LinkedList 和 Vector。
+ Set:一个无序(存入和取出顺序有可能不一致)容器,不可以存储重复元素,只允许存入一个null元素,必须保证元素唯一性。Set 接口常用实现类是 HashSet、LinkedHashSet 以及 TreeSet。
  • Map是一个键值对集合,存储键、值和之间的映射。 Key无序,唯一;value 不要求有序,允许重复。Map没有继承于Collection接口,从Map集合中检索元素时,只要给出键对象,就会返回对应的值对象。
+ Map 的常用实现类:HashMap、TreeMap、HashTable、LinkedHashMap、ConcurrentHashMap

集合框架底层数据结构

  • Collection
1. List


    + Arraylist: Object数组
    + Vector: Object数组
    + LinkedList: 双向循环链表
2. Set


    + HashSet(无序,唯一):基于 HashMap 实现的,底层采用 HashMap 来保存元素
    + LinkedHashSet: LinkedHashSet 继承与 HashSet,并且其内部是通过 LinkedHashMap 来实现的。有点类似于我们之前说的LinkedHashMap 其内部是基于 Hashmap 实现一样,不过还是有一点点区别的。
    + TreeSet(有序,唯一): 红黑树(自平衡的排序二叉树。)
  • Map
+ HashMap: JDK1.8之前HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突).JDK1.8以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间
+ LinkedHashMap:LinkedHashMap 继承自 HashMap,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap 在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。
+ HashTable: 数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的
+ TreeMap: 红黑树(自平衡的排序二叉树)

哪些集合类是线程安全的?

  • Vector:就比Arraylist多了个 synchronized (线程安全),因为效率较低,现在已经不太建议使用。
  • hashTable:就比hashMap多了个synchronized (线程安全),不建议使用。
  • ConcurrentHashMap:是Java5中支持高并发、高吞吐量的线程安全HashMap实现。它由Segment数组结构和HashEntry数组结构组成。Segment数组在ConcurrentHashMap里扮演锁的角色,HashEntry则用于存储键-值对数据。一个ConcurrentHashMap里包含一个Segment数组,Segment的结构和HashMap类似,是一种数组和链表结构;一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素;每个Segment守护着一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得它对应的Segment锁。(推荐使用)
  • …

Java集合的快速失败机制 “fail-fast”?

  • 是java集合的一种错误检测机制,当多个线程对集合进行结构上的改变的操作时,有可能会产生 fail-fast 机制。
  • 例如:假设存在两个线程(线程1、线程2),线程1通过Iterator在遍历集合A中的元素,在某个时候线程2修改了集合A的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这个时候程序就会抛出 ConcurrentModificationException 异常,从而产生fail-fast机制。
  • 原因:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变modCount的值。每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历。
  • 解决办法:
1. 在遍历过程中,所有涉及到改变modCount值得地方全部加上synchronized。
2. 使用CopyOnWriteArrayList来替换ArrayList

怎么确保一个集合不能被修改?

  • 可以使用 Collections. unmodifiableCollection(Collection c) 方法来创建一个只读集合,这样改变集合的任何操作都会抛出 Java. lang. UnsupportedOperationException 异常。
  • 示例代码如下:
1
2
3
4
5
复制代码List<String> list = new ArrayList<>();
list. add("x");
Collection<String> clist = Collections. unmodifiableCollection(list);
clist. add("y"); // 运行时此行报错
System. out. println(list. size());

Collection接口

List接口

迭代器 Iterator 是什么?

  • Iterator 接口提供遍历任何 Collection 的接口。我们可以从一个 Collection 中使用迭代器方法来获取迭代器实例。迭代器取代了 Java 集合框架中的 Enumeration,迭代器允许调用者在迭代过程中移除元素。
  • 因为所有Collection接继承了Iterator迭代器

在这里插入图片描述

Iterator 怎么使用?有什么特点?

  • Iterator 使用代码如下:
1
2
3
4
5
6
复制代码List<String> list = new ArrayList<>();
Iterator<String> it = list. iterator();
while(it. hasNext()){
String obj = it. next();
System. out. println(obj);
}
  • Iterator 的特点是只能单向遍历,但是更加安全,因为它可以确保,在当前遍历的集合元素被更改的时候,就会抛出 ConcurrentModificationException 异常。

如何边遍历边移除 Collection 中的元素?

  • 边遍历边修改 Collection 的唯一正确方式是使用 Iterator.remove() 方法,如下:
1
2
3
4
5
复制代码Iterator<Integer> it = list.iterator();
while(it.hasNext()){
*// do something*
it.remove();
}

一种最常见的错误代码如下:

1
2
3
复制代码for(Integer i : list){
list.remove(i)
}
  • 运行以上错误代码会报 ConcurrentModificationException 异常。这是因为当使用 foreach(for(Integer i : list)) 语句时,会自动生成一个iterator 来遍历该 list,但同时该 list 正在被 Iterator.remove() 修改。Java 一般不允许一个线程在遍历 Collection 时另一个线程修改它。

Iterator 和 ListIterator 有什么区别?

  • Iterator 可以遍历 Set 和 List 集合,而 ListIterator 只能遍历 List。
  • Iterator 只能单向遍历,而 ListIterator 可以双向遍历(向前/后遍历)。
  • ListIterator 实现 Iterator 接口,然后添加了一些额外的功能,比如添加一个元素、替换一个元素、获取前面或后面元素的索引位置。

遍历一个 List 有哪些不同的方式?每种方法的实现原理是什么?Java 中 List 遍历的最佳实践是什么?

  • 遍历方式有以下几种:
1. for 循环遍历,基于计数器。在集合外部维护一个计数器,然后依次读取每一个位置的元素,当读取到最后一个元素后停止。
2. 迭代器遍历,Iterator。Iterator 是面向对象的一个设计模式,目的是屏蔽不同数据集合的特点,统一遍历集合的接口。Java 在 Collections 中支持了 Iterator 模式。
3. foreach 循环遍历。foreach 内部也是采用了 Iterator 的方式实现,使用时不需要显式声明 Iterator 或计数器。优点是代码简洁,不易出错;缺点是只能做简单的遍历,不能在遍历过程中操作数据集合,例如删除、替换。
  • 最佳实践:Java Collections 框架中提供了一个 RandomAccess 接口,用来标记 List 实现是否支持 Random Access。
+ 如果一个数据集合实现了该接口,就意味着它支持 Random Access,按位置读取元素的平均时间复杂度为 O(1),如ArrayList。
+ 如果没有实现该接口,表示不支持 Random Access,如LinkedList。
+ 推荐的做法就是,支持 Random Access 的列表可用 for 循环遍历,否则建议用 Iterator 或 foreach 遍历。

说一下 ArrayList 的优缺点

  • ArrayList的优点如下:
+ ArrayList 底层以数组实现,是一种随机访问模式。ArrayList 实现了 RandomAccess 接口,因此查找的时候非常快。
+ ArrayList 在顺序添加一个元素的时候非常方便。
  • ArrayList 的缺点如下:
+ 删除元素的时候,需要做一次元素复制操作。如果要复制的元素很多,那么就会比较耗费性能。
+ 插入元素的时候,也需要做一次元素复制操作,缺点同上。
  • ArrayList 比较适合顺序添加、随机访问的场景。

如何实现数组和 List 之间的转换?

  • 数组转 List:使用 Arrays. asList(array) 进行转换。
  • List 转数组:使用 List 自带的 toArray() 方法。
  • 代码示例:
1
2
3
4
5
6
7
8
9
复制代码// list to array
List<String> list = new ArrayList<String>();
list.add("123");
list.add("456");
list.toArray();

// array to list
String[] array = new String[]{"123","456"};
Arrays.asList(array);

ArrayList 和 LinkedList 的区别是什么?

  • 数据结构实现:ArrayList 是动态数组的数据结构实现,而 LinkedList 是双向链表的数据结构实现。
  • 随机访问效率:ArrayList 比 LinkedList 在随机访问的时候效率要高,因为 LinkedList 是线性的数据存储方式,所以需要移动指针从前往后依次查找。
  • 增加和删除效率:在非首尾的增加和删除操作,LinkedList 要比 ArrayList 效率要高,因为 ArrayList 增删操作要影响数组内的其他数据的下标。
  • 内存空间占用:LinkedList 比 ArrayList 更占内存,因为 LinkedList 的节点除了存储数据,还存储了两个引用,一个指向前一个元素,一个指向后一个元素。
  • 线程安全:ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全;
  • 综合来说,在需要频繁读取集合中的元素时,更推荐使用 ArrayList,而在插入和删除操作较多时,更推荐使用 LinkedList。
  • LinkedList 的双向链表也叫双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。

ArrayList 和 Vector 的区别是什么?

  • 这两个类都实现了 List 接口(List 接口继承了 Collection 接口),他们都是有序集合
+ 线程安全:Vector 使用了 Synchronized 来实现线程同步,是线程安全的,而 ArrayList 是非线程安全的。
+ 性能:ArrayList 在性能方面要优于 Vector。
+ 扩容:ArrayList 和 Vector 都会根据实际的需要动态的调整容量,只不过在 Vector 扩容每次会增加 1 倍,而 ArrayList 只会增加 50%。
  • Vector类的所有方法都是同步的。可以由两个线程安全地访问一个Vector对象、但是一个线程访问Vector的话代码要在同步操作上耗费大量的时间。
  • Arraylist不是同步的,所以在不需要保证线程安全时时建议使用Arraylist。

插入数据时,ArrayList、LinkedList、Vector谁速度较快?阐述 ArrayList、Vector、LinkedList 的存储性能和特性?

  • ArrayList和Vector 底层的实现都是使用数组方式存储数据。数组元素数大于实际存储的数据以便增加和插入元素,它们都允许直接按序号索引元素,但是插入元素要涉及数组元素移动等内存操作,所以索引数据快而插入数据慢。
  • Vector 中的方法由于加了 synchronized 修饰,因此 Vector 是线程安全容器,但性能上较ArrayList差。
  • LinkedList 使用双向链表实现存储,按序号索引数据需要进行前向或后向遍历,但插入数据时只需要记录当前项的前后项即可,所以 LinkedList 插入速度较快。

多线程场景下如何使用 ArrayList?

  • ArrayList 不是线程安全的,如果遇到多线程场景,可以通过 Collections 的 synchronizedList 方法将其转换成线程安全的容器后再使用。例如像下面这样:
1
2
3
4
5
6
7
复制代码List<String> synchronizedList = Collections.synchronizedList(list);
synchronizedList.add("aaa");
synchronizedList.add("bbb");

for (int i = 0; i < synchronizedList.size(); i++) {
System.out.println(synchronizedList.get(i));
}

为什么 ArrayList 的 elementData 加上 transient 修饰?

  • ArrayList 中的数组定义如下:

private transient Object[] elementData;

  • 再看一下 ArrayList 的定义:
1
2
复制代码public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
  • 可以看到 ArrayList 实现了 Serializable 接口,这意味着 ArrayList 支持序列化。transient 的作用是说不希望 elementData 数组被序列化,重写了 writeObject 实现:
1
2
3
4
5
6
7
8
9
10
11
12
复制代码private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException{
*// Write out element count, and any hidden stuff*
int expectedModCount = modCount;
s.defaultWriteObject();
*// Write out array length*
s.writeInt(elementData.length);
*// Write out all elements in the proper order.*
for (int i=0; i<size; i++)
s.writeObject(elementData[i]);
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
  • 每次序列化时,先调用 defaultWriteObject() 方法序列化 ArrayList 中的非 transient 元素,然后遍历 elementData,只序列化已存入的元素,这样既加快了序列化的速度,又减小了序列化之后的文件大小。

List 和 Set 的区别

  • List , Set 都是继承自Collection 接口
  • List 特点:一个有序(元素存入集合的顺序和取出的顺序一致)容器,元素可以重复,可以插入多个null元素,元素都有索引。常用的实现类有 ArrayList、LinkedList 和 Vector。
  • Set 特点:一个无序(存入和取出顺序有可能不一致)容器,不可以存储重复元素,只允许存入一个null元素,必须保证元素唯一性。Set 接口常用实现类是 HashSet、LinkedHashSet 以及 TreeSet。
  • 另外 List 支持for循环,也就是通过下标来遍历,也可以用迭代器,但是set只能用迭代,因为他无序,无法用下标来取得想要的值。
  • Set和List对比
+ Set:检索元素效率低下,删除和插入效率高,插入和删除不会引起元素位置改变。
+ List:和数组类似,List可以动态增长,查找元素效率高,插入删除元素效率低,因为会引起其他元素位置改变

Set接口

说一下 HashSet 的实现原理?

  • HashSet 是基于 HashMap 实现的,HashSet的值存放于HashMap的key上,HashMap的value统一为present,因此 HashSet 的实现比较简单,相关 HashSet 的操作,基本上都是直接调用底层 HashMap 的相关方法来完成,HashSet 不允许重复的值。

HashSet如何检查重复?HashSet是如何保证数据不可重复的?

  • 向HashSet 中add ()元素时,判断元素是否存在的依据,不仅要比较hash值,同时还要结合equles 方法比较。
  • HashSet 中的add ()方法会使用HashMap 的put()方法。
  • HashMap 的 key 是唯一的,由源码可以看出 HashSet 添加进去的值就是作为HashMap 的key,并且在HashMap中如果K/V相同时,会用新的V覆盖掉旧的V,然后返回旧的V。所以不会重复( HashMap 比较key是否相等是先比较hashcode 再比较equals )。
  • 以下是HashSet 部分源码:
1
2
3
4
5
6
7
8
9
10
11
复制代码private static final Object PRESENT = new Object();
private transient HashMap<E,Object> map;

public HashSet() {
map = new HashMap<>();
}

public boolean add(E e) {
// 调用HashMap的put方法,PRESENT是一个至始至终都相同的虚值
return map.put(e, PRESENT)==null;
}

hashCode()与equals()的相关规定:

  1. 如果两个对象相等,则hashcode一定也是相同的
    • hashCode是jdk根据对象的地址或者字符串或者数字算出来的int类型的数值
  2. 两个对象相等,对两个equals方法返回true
  3. 两个对象有相同的hashcode值,它们也不一定是相等的
  4. 综上,equals方法被覆盖过,则hashCode方法也必须被覆盖
  5. hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写hashCode(),则该class的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)。

==与equals的区别

  1. ==是判断两个变量或实例是不是指向同一个内存空间 equals是判断两个变量或实例所指向的内存空间的值是不是相同
  2. ==是指对内存地址进行比较 equals()是对字符串的内容进行比较

HashSet与HashMap的区别

HashMap HashSet
实现了Map接口 实现Set接口
存储键值对 仅存储对象
调用put()向map中添加元素 调用add()方法向Set中添加元素
HashMap使用键(Key)计算Hashcode HashSet使用成员对象来计算hashcode值,对于两个对象来说hashcode可能相同,所以equals()方法用来判断对象的相等性,如果两个对象不同的话,那么返回false
HashMap相对于HashSet较快,因为它是使用唯一的键获取对象 HashSet较HashMap来说比较慢

Map接口

什么是Hash算法

  • 哈希算法是指把任意长度的二进制映射为固定长度的较小的二进制值,这个较小的二进制值叫做哈希值。

什么是链表

  • 链表是可以将物理地址上不连续的数据连接起来,通过指针来对物理地址进行操作,实现增删改查等功能。
  • 链表大致分为单链表和双向链表
1. 单链表:每个节点包含两部分,一部分存放数据变量的data,另一部分是指向下一节点的next指针


![在这里插入图片描述](https://gitee.com/songjianzaina/juejin_p15/raw/master/img/28427997f4960cd73ffa9ee8a2b609b0d5e2ce58d06c409ae67c92f1ef15c348)
2. 双向链表:除了包含单链表的部分,还增加的pre前一个节点的指针


![在这里插入图片描述](https://gitee.com/songjianzaina/juejin_p15/raw/master/img/532a3e5c431e3a22bde92b79b28de69aa2d21d4a45665c63f35a2c7d0659066a)
  • 链表的优点
+ 插入删除速度快(因为有next指针指向其下一个节点,通过改变指针的指向可以方便的增加删除元素)
+ 内存利用率高,不会浪费内存(可以使用内存中细小的不连续空间(大于node节点的大小),并且在需要空间的时候才创建空间)
+ 大小没有固定,拓展很灵活。
  • 链表的缺点
+ 不能随机查找,必须从第一个开始遍历,查找效率低

说一下HashMap的实现原理?

  • HashMap概述: HashMap是基于哈希表的Map接口的非同步实现。此实现提供所有可选的映射操作,并允许使用null值和null键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。
  • HashMap的数据结构: 在Java编程语言中,最基本的结构就是两种,一个是数组,另外一个是模拟指针(引用),所有的数据结构都可以用这两个基本结构来构造的,HashMap也不例外。HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体。
  • HashMap 基于 Hash 算法实现的
1. 当我们往HashMap中put元素时,利用key的hashCode重新hash计算出当前对象的元素在数组中的下标
2. 存储时,如果出现hash值相同的key,此时有两种情况。


​ (1)如果key相同,则覆盖原始值;


​ (2)如果key不同(出现冲突),则将当前的key-value放入链表中
3. 获取时,直接找到hash值对应的下标,在进一步判断key是否相同,从而找到对应值。
4. 理解了以上过程就不难明白HashMap是如何解决hash冲突的问题,核心就是使用了数组的存储方式,然后将冲突的key的对象放入链表中,一旦发现冲突就在链表中做进一步的对比。
  • 需要注意Jdk 1.8中对HashMap的实现做了优化,当链表中的节点数据超过八个之后,该链表会转为红黑树来提高查询效率,从原来的O(n)到O(logn)

HashMap在JDK1.7和JDK1.8中有哪些不同?HashMap的底层实现

  • 在Java中,保存数据有两种比较简单的数据结构:数组和链表。数组的特点是:寻址容易,插入和删除困难;链表的特点是:寻址困难,但插入和删除容易;所以我们将数组和链表结合在一起,发挥两者各自的优势,使用一种叫做拉链法的方式可以解决哈希冲突。

HashMap JDK1.8之前

  • JDK1.8之前采用的是拉链法。拉链法:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。

在这里插入图片描述

HashMap JDK1.8之后

  • 相比于之前的版本,jdk1.8在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。

在这里插入图片描述

JDK1.7 VS JDK1.8 比较

  • JDK1.8主要解决或优化了一下问题:
    1. resize 扩容优化
    2. 引入了红黑树,目的是避免单条链表过长而影响查询效率,红黑树算法请参考
    3. 解决了多线程死循环问题,但仍是非线程安全的,多线程时可能会造成数据丢失问题。
不同 JDK 1.7 JDK 1.8
存储结构 数组 + 链表 数组 + 链表 + 红黑树
初始化方式 单独函数:inflateTable() 直接集成到了扩容函数resize()中
hash值计算方式 扰动处理 = 9次扰动 = 4次位运算 + 5次异或运算 扰动处理 = 2次扰动 = 1次位运算 + 1次异或运算
存放数据的规则 无冲突时,存放数组;冲突时,存放链表 无冲突时,存放数组;冲突 & 链表长度 < 8:存放单链表;冲突 & 链表长度 > 8:树化并存放红黑树
插入数据方式 头插法(先讲原位置的数据移到后1位,再插入数据到该位置) 尾插法(直接插入到链表尾部/红黑树)
扩容后存储位置的计算方式 全部按照原来方法进行计算(即hashCode ->> 扰动函数 ->> (h&length-1)) 按照扩容后的规律计算(即扩容后的位置=原位置 or 原位置 + 旧容量)

什么是红黑树

说道红黑树先讲什么是二叉树

  • 二叉树简单来说就是 每一个节上可以关联俩个子节点
+ 
1
2
3
4
5
6
7
8
复制代码大概就是这样子:
a
/ \
b c
/ \ / \
d e f g
/ \ / \ / \ / \
h i j k l m n o

红黑树

  • 红黑树是一种特殊的二叉查找树。红黑树的每个结点上都有存储位表示结点的颜色,可以是红(Red)或黑(Black)。
  • 红黑树的每个结点是黑色或者红色。当是不管怎么样他的根结点是黑色。每个叶子结点(叶子结点代表终结、结尾的节点)也是黑色 [注意:这里叶子结点,是指为空(NIL或NULL)的叶子结点!]。
  • 如果一个结点是红色的,则它的子结点必须是黑色的。
  • 每个结点到叶子结点NIL所经过的黑色结点的个数一样的。[确保没有一条路径会比其他路径长出俩倍,所以红黑树是相对接近平衡的二叉树的!]
  • 红黑树的基本操作是添加、删除。在对红黑树进行添加或删除之后,都会用到旋转方法。为什么呢?道理很简单,添加或删除红黑树中的结点之后,红黑树的结构就发生了变化,可能不满足上面三条性质,也就不再是一颗红黑树了,而是一颗普通的树。而通过旋转和变色,可以使这颗树重新成为红黑树。简单点说,旋转和变色的目的是让树保持红黑树的特性。

HashMap的put方法的具体流程?

  • 当我们put的时候,首先计算 key的hash值,这里调用了 hash方法,hash方法实际是让key.hashCode()与key.hashCode()>>>16进行异或操作,高16bit补0,一个数和0异或不变,所以 hash 函数大概的作用就是:高16bit不变,低16bit和高16bit做了一个异或,目的是减少碰撞。按照函数注释,因为bucket数组大小是2的幂,计算下标index = (table.length - 1) & hash,如果不做 hash 处理,相当于散列生效的只有几个低 bit 位,为了减少散列的碰撞,设计者综合考虑了速度、作用、质量之后,使用高16bit和低16bit异或来简单处理减少碰撞,而且JDK8中用了复杂度 O(logn)的树结构来提升碰撞下的性能。
  • putVal方法执行流程图

在这里插入图片描述

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
复制代码public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}

static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

//实现Map.put和相关方法
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 步骤①:tab为空则创建
// table未初始化或者长度为0,进行扩容
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 步骤②:计算index,并对null做处理
// (n - 1) & hash 确定元素存放在哪个桶中,桶为空,新生成结点放入桶中(此时,这个结点是放在数组中)
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
// 桶中已经存在元素
else {
Node<K,V> e; K k;
// 步骤③:节点key存在,直接覆盖value
// 比较桶中第一个元素(数组中的结点)的hash值相等,key相等
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
// 将第一个元素赋值给e,用e来记录
e = p;
// 步骤④:判断该链为红黑树
// hash值不相等,即key不相等;为红黑树结点
// 如果当前元素类型为TreeNode,表示为红黑树,putTreeVal返回待存放的node, e可能为null
else if (p instanceof TreeNode)
// 放入树中
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// 步骤⑤:该链为链表
// 为链表结点
else {
// 在链表最末插入结点
for (int binCount = 0; ; ++binCount) {
// 到达链表的尾部

//判断该链表尾部指针是不是空的
if ((e = p.next) == null) {
// 在尾部插入新结点
p.next = newNode(hash, key, value, null);
//判断链表的长度是否达到转化红黑树的临界值,临界值为8
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
//链表结构转树形结构
treeifyBin(tab, hash);
// 跳出循环
break;
}
// 判断链表中结点的key值与插入的元素的key值是否相等
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
// 相等,跳出循环
break;
// 用于遍历桶中的链表,与前面的e = p.next组合,可以遍历链表
p = e;
}
}
//判断当前的key已经存在的情况下,再来一个相同的hash值、key值时,返回新来的value这个值
if (e != null) {
// 记录e的value
V oldValue = e.value;
// onlyIfAbsent为false或者旧值为null
if (!onlyIfAbsent || oldValue == null)
//用新值替换旧值
e.value = value;
// 访问后回调
afterNodeAccess(e);
// 返回旧值
return oldValue;
}
}
// 结构性修改
++modCount;
// 步骤⑥:超过最大容量就扩容
// 实际大小大于阈值则扩容
if (++size > threshold)
resize();
// 插入后回调
afterNodeInsertion(evict);
return null;
}
  1. 判断键值对数组table[i]是否为空或为null,否则执行resize()进行扩容;
  2. 根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向⑥,如果table[i]不为空,转向③;
  3. 判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向④,这里的相同指的是hashCode以及equals;
  4. 判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向5;
  5. 遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可;
  6. 插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold,如果超过,进行扩容。

HashMap的扩容操作是怎么实现的?

  1. 在jdk1.8中,resize方法是在hashmap中的键值对大于阀值时或者初始化时,就调用resize方法进行扩容;
  2. 每次扩展的时候,都是扩展2倍;
  3. 扩展后Node对象的位置要么在原位置,要么移动到原偏移量两倍的位置。
  • 在putVal()中,我们看到在这个函数里面使用到了2次resize()方法,resize()方法表示的在进行第一次初始化时会对其进行扩容,或者当该数组的实际大小大于其临界值值(第一次为12),这个时候在扩容的同时也会伴随的桶上面的元素进行重新分发,这也是JDK1.8版本的一个优化的地方,在1.7中,扩容之后需要重新去计算其Hash值,根据Hash值对其进行分发,但在1.8版本中,则是根据在同一个桶的位置中进行判断(e.hash & oldCap)是否为0,重新进行hash分配后,该元素的位置要么停留在原始位置,要么移动到原始位置+增加的数组大小这个位置上
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
复制代码final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;//oldTab指向hash桶数组
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {//如果oldCap不为空的话,就是hash桶数组不为空
if (oldCap >= MAXIMUM_CAPACITY) {//如果大于最大容量了,就赋值为整数最大的阀值
threshold = Integer.MAX_VALUE;
return oldTab;//返回
}//如果当前hash桶数组的长度在扩容后仍然小于最大容量 并且oldCap大于默认值16
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold 双倍扩容阀值threshold
}
// 旧的容量为0,但threshold大于零,代表有参构造有cap传入,threshold已经被初始化成最小2的n次幂
// 直接将该值赋给新的容量
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
// 无参构造创建的map,给出默认容量和threshold 16, 16*0.75
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 新的threshold = 新的cap * 0.75
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
// 计算出新的数组长度后赋给当前成员变量table
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];//新建hash桶数组
table = newTab;//将新数组的值复制给旧的hash桶数组
// 如果原先的数组没有初始化,那么resize的初始化工作到此结束,否则进入扩容元素重排逻辑,使其均匀的分散
if (oldTab != null) {
// 遍历新数组的所有桶下标
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
// 旧数组的桶下标赋给临时变量e,并且解除旧数组中的引用,否则就数组无法被GC回收
oldTab[j] = null;
// 如果e.next==null,代表桶中就一个元素,不存在链表或者红黑树
if (e.next == null)
// 用同样的hash映射算法把该元素加入新的数组
newTab[e.hash & (newCap - 1)] = e;
// 如果e是TreeNode并且e.next!=null,那么处理树中元素的重排
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
// e是链表的头并且e.next!=null,那么处理链表中元素重排
else { // preserve order
// loHead,loTail 代表扩容后不用变换下标,见注1
Node<K,V> loHead = null, loTail = null;
// hiHead,hiTail 代表扩容后变换下标,见注1
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
// 遍历链表
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
// 初始化head指向链表当前元素e,e不一定是链表的第一个元素,初始化后loHead
// 代表下标保持不变的链表的头元素
loHead = e;
else
// loTail.next指向当前e
loTail.next = e;
// loTail指向当前的元素e
// 初始化后,loTail和loHead指向相同的内存,所以当loTail.next指向下一个元素时,
// 底层数组中的元素的next引用也相应发生变化,造成lowHead.next.next.....
// 跟随loTail同步,使得lowHead可以链接到所有属于该链表的元素。
loTail = e;
}
else {
if (hiTail == null)
// 初始化head指向链表当前元素e, 初始化后hiHead代表下标更改的链表头元素
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
// 遍历结束, 将tail指向null,并把链表头放入新数组的相应下标,形成新的映射。
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}

HashMap是怎么解决哈希冲突的?

  • 答:在解决这个问题之前,我们首先需要知道什么是哈希冲突,而在了解哈希冲突之前我们还要知道什么是哈希才行;

什么是哈希?

  • Hash,一般翻译为“散列”,也有直接音译为“哈希”的, Hash就是指使用哈希算法是指把任意长度的二进制映射为固定长度的较小的二进制值,这个较小的二进制值叫做哈希值。

什么是哈希冲突?

  • 当两个不同的输入值,根据同一散列函数计算出相同的散列值的现象,我们就把它叫做碰撞(哈希碰撞)。

HashMap的数据结构

  • 在Java中,保存数据有两种比较简单的数据结构:数组和链表。
    • 数组的特点是:寻址容易,插入和删除困难;
    • 链表的特点是:寻址困难,但插入和删除容易;
  • 所以我们将数组和链表结合在一起,发挥两者各自的优势,就可以使用俩种方式:链地址法和开放地址法可以解决哈希冲突:

在这里插入图片描述

  • 链表法就是将相同hash值的对象组织成一个链表放在hash值对应的槽位;
  • 开放地址法是通过一个探测算法,当某个槽位已经被占据的情况下继续查找下一个可以使用的槽位。
  • 但相比于hashCode返回的int类型,我们HashMap初始的容量大小DEFAULT_INITIAL_CAPACITY = 1 << 4(即2的四次方16)要远小于int类型的范围,所以我们如果只是单纯的用hashCode取余来获取对应的bucket这将会大大增加哈希碰撞的概率,并且最坏情况下还会将HashMap变成一个单链表,所以我们还需要对hashCode作一定的优化

hash()函数

  • 上面提到的问题,主要是因为如果使用hashCode取余,那么相当于参与运算的只有hashCode的低位,高位是没有起到任何作用的,所以我们的思路就是让hashCode取值出的高位也参与运算,进一步降低hash碰撞的概率,使得数据分布更平均,我们把这样的操作称为扰动,在JDK 1.8中的hash()函数如下:
1
2
3
4
复制代码static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);// 与自己右移16位进行异或运算(高低位异或)
}
  • 这比在JDK 1.7中,更为简洁,相比在1.7中的4次位运算,5次异或运算(9次扰动),在1.8中,只进行了1次位运算和1次异或运算(2次扰动);

总结

  • 简单总结一下HashMap是使用了哪些方法来有效解决哈希冲突的:
    • 链表法就是将相同hash值的对象组织成一个链表放在hash值对应的槽位;
    • 开放地址法是通过一个探测算法,当某个槽位已经被占据的情况下继续查找下一个可以使用的槽位。

能否使用任何类作为 Map 的 key?

可以使用任何类作为 Map 的 key,然而在使用之前,需要考虑以下几点:

  • 如果类重写了 equals() 方法,也应该重写 hashCode() 方法。
  • 类的所有实例需要遵循与 equals() 和 hashCode() 相关的规则。
  • 如果一个类没有使用 equals(),不应该在 hashCode() 中使用它。
  • 用户自定义 Key 类最佳实践是使之为不可变的,这样 hashCode() 值可以被缓存起来,拥有更好的性能。不可变的类也可以确保 hashCode() 和 equals() 在未来不会改变,这样就会解决与可变相关的问题了。

为什么HashMap中String、Integer这样的包装类适合作为K?

  • 答:String、Integer等包装类的特性能够保证Hash值的不可更改性和计算准确性,能够有效的减少Hash碰撞的几率
    • 都是final类型,即不可变性,保证key的不可更改性,不会存在获取hash值不同的情况
    • 内部已重写了equals()、hashCode()等方法,遵守了HashMap内部的规范(不清楚可以去上面看看putValue的过程),不容易出现Hash值计算错误的情况;

如果使用Object作为HashMap的Key,应该怎么办呢?

  • 答:重写hashCode()和equals()方法
    1. 重写hashCode()是因为需要计算存储数据的存储位置,需要注意不要试图从散列码计算中排除掉一个对象的关键部分来提高性能,这样虽然能更快但可能会导致更多的Hash碰撞;
    2. 重写equals()方法,需要遵守自反性、对称性、传递性、一致性以及对于任何非null的引用值x,x.equals(null)必须返回false的这几个特性,目的是为了保证key在哈希表中的唯一性;

HashMap为什么不直接使用hashCode()处理后的哈希值直接作为table的下标?

  • 答:hashCode()方法返回的是int整数类型,其范围为-(2 ^ 31)(2 ^ 31 - 1),约有40亿个映射空间,而HashMap的容量范围是在16(初始化默认值)2 ^ 30,HashMap通常情况下是取不到最大值的,并且设备上也难以提供这么多的存储空间,从而导致通过hashCode()计算出的哈希值可能不在数组大小范围内,进而无法匹配存储位置;
  • 那怎么解决呢?
1. HashMap自己实现了自己的`hash()`方法,通过两次扰动使得它自己的哈希值高低位自行进行异或运算,降低哈希碰撞概率也使得数据分布更平均;
2. 在保证数组长度为2的幂次方的时候,使用`hash()`运算之后的值与运算(&)(数组长度 - 1)来获取数组下标的方式进行存储,这样一来是比取余操作更加有效率,二来也是因为只有当数组长度为2的幂次方时,h&(length-1)才等价于h%length,三来解决了“哈希值与数组大小范围不匹配”的问题;

HashMap 的长度为什么是2的幂次方

  • 为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀,每个链表/红黑树长度大致相同。这个实现就是把数据存到哪个链表/红黑树中的算法。
  • 这个算法应该如何设计呢?
+ 我们首先可能会想到采用%取余的操作来实现。但是,重点来了:“取余(%)操作中如果除数是2的幂次则等价于与其除数减一的与(&)操作(也就是说 hash%length==hash&(length-1)的前提是 length 是2的 n 次方;)。” 并且 采用二进制位操作 &,相对于%能够提高运算效率,这就解释了 HashMap 的长度为什么是2的幂次方。
  • 那为什么是两次扰动呢?
+ 答:这样就是加大哈希值低位的随机性,使得分布更均匀,从而提高对应数组存储下标位置的随机性&均匀性,最终减少Hash冲突,两次就够了,已经达到了高位低位同时参与运算的目的;

HashMap 与 HashTable 有什么区别?

  1. 线程安全: HashMap 是非线程安全的,HashTable 是线程安全的;HashTable 内部的方法基本都经过 synchronized 修饰。(如果你要保证线程安全的话就使用 ConcurrentHashMap );
  2. 效率: 因为线程安全的问题,HashMap 要比 HashTable 效率高一点。另外,HashTable 基本被淘汰,不要在代码中使用它;(如果你要保证线程安全的话就使用 ConcurrentHashMap );
  3. 对Null key 和Null value的支持: HashMap 中,null 可以作为键,这样的键只有一个,可以有一个或多个键所对应的值为 null。但是在 HashTable 中 put 进的键值只要有一个 null,直接抛NullPointerException。
  4. 初始容量大小和每次扩充容量大小的不同 :
    1. 创建时如果不指定容量初始值,Hashtable 默认的初始大小为11,之后每次扩充,容量变为原来的2n+1。HashMap 默认的初始化大小为16。之后每次扩充,容量变为原来的2倍。
    2. 创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为2的幂次方大小。也就是说 HashMap 总是使用2的幂作为哈希表的大小,后面会介绍到为什么是2的幂次方。
  5. 底层数据结构: JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。Hashtable 没有这样的机制。
  6. 推荐使用:在 Hashtable 的类注释可以看到,Hashtable 是保留类不建议使用,推荐在单线程环境下使用 HashMap 替代,如果需要多线程使用则用 ConcurrentHashMap 替代。

什么是TreeMap 简介

  • TreeMap 是一个有序的key-value集合,它是通过红黑树实现的。
  • TreeMap基于红黑树(Red-Black tree)实现。该映射根据其键的自然顺序进行排序,或者根据创建映射时提供的 Comparator 进行排序,具体取决于使用的构造方法。
  • TreeMap是线程非同步的。

如何决定使用 HashMap 还是 TreeMap?

  • 对于在Map中插入、删除和定位元素这类操作,HashMap是最好的选择。然而,假如你需要对一个有序的key集合进行遍历,TreeMap是更好的选择。基于你的collection的大小,也许向HashMap中添加元素会更快,将map换为TreeMap进行有序key的遍历。

HashMap 和 ConcurrentHashMap 的区别

  1. ConcurrentHashMap对整个桶数组进行了分割分段(Segment),然后在每一个分段上都用lock锁进行保护,相对于HashTable的synchronized锁的粒度更精细了一些,并发性能更好,而HashMap没有锁机制,不是线程安全的。(JDK1.8之后ConcurrentHashMap启用了一种全新的方式实现,利用CAS算法。)
  2. HashMap的键值对允许有null,但是ConCurrentHashMap都不允许。

ConcurrentHashMap 和 Hashtable 的区别?

  • ConcurrentHashMap 和 Hashtable 的区别主要体现在实现线程安全的方式上不同。
+ **底层数据结构**: JDK1.7的 ConcurrentHashMap 底层采用 **分段的数组+链表** 实现,JDK1.8 采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树。Hashtable 和 JDK1.8 之前的 HashMap 的底层数据结构类似都是采用 **数组+链表** 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的;
+ **实现线程安全的方式**:
    1. **在JDK1.7的时候,ConcurrentHashMap(分段锁)** 对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。(默认分配16个Segment,比Hashtable效率提高16倍。) **到了 JDK1.8 的时候已经摒弃了Segment的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。(JDK1.6以后 对 synchronized锁做了很多优化)** 整个看起来就像是优化过且线程安全的 HashMap,虽然在JDK1.8中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本;
    2. ② **Hashtable(同一把锁)** :使用 synchronized 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。
  • 两者的对比图:
1、HashTable:

在这里插入图片描述

2、 JDK1.7的ConcurrentHashMap:

在这里插入图片描述

3、JDK1.8的ConcurrentHashMap(TreeBin: 红黑二叉树节点 Node: 链表节点):

在这里插入图片描述

  • 答:ConcurrentHashMap 结合了 HashMap 和 HashTable 二者的优势。HashMap 没有考虑同步,HashTable 考虑了同步的问题使用了synchronized 关键字,所以 HashTable 在每次同步执行时都要锁住整个结构。 ConcurrentHashMap 锁的方式是稍微细粒度的。

ConcurrentHashMap 底层具体实现知道吗?实现原理是什么?

JDK1.7

  • 首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。
  • 在JDK1.7中,ConcurrentHashMap采用Segment + HashEntry的方式进行实现,结构如下:
  • 一个 ConcurrentHashMap 里包含一个 Segment 数组。Segment 的结构和HashMap类似,是一种数组和链表结构,一个 Segment 包含一个 HashEntry 数组,每个 HashEntry 是一个链表结构的元素,每个 Segment 守护着一个HashEntry数组里的元素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment的锁。

在这里插入图片描述

  1. 该类包含两个静态内部类 HashEntry 和 Segment ;前者用来封装映射表的键值对,后者用来充当锁的角色;
  2. Segment 是一种可重入的锁 ReentrantLock,每个 Segment 守护一个HashEntry 数组里得元素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment 锁。

JDK1.8

  • 在JDK1.8中,放弃了Segment臃肿的设计,取而代之的是采用Node + CAS + Synchronized来保证并发安全进行实现,synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发,效率又提升N倍。
  • 结构如下:

在这里插入图片描述

  • 附加源码,有需要的可以看看
  • 插入元素过程(建议去看看源码):
  • 如果相应位置的Node还没有初始化,则调用CAS插入相应的数据;
1
2
3
4
复制代码else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
  • 如果相应位置的Node不为空,且当前该节点不处于移动状态,则对该节点加synchronized锁,如果该节点的hash不小于0,则遍历链表更新节点或插入新节点;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
复制代码if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key, value, null);
break;
}
}
}
  1. 如果该节点是TreeBin类型的节点,说明是红黑树结构,则通过putTreeVal方法往红黑树中插入节点;如果binCount不为0,说明put操作对数据产生了影响,如果当前链表的个数达到8个,则通过treeifyBin方法转化为红黑树,如果oldVal不为空,说明是一次更新操作,没有对元素个数产生影响,则直接返回旧值;
  2. 如果插入的是一个新节点,则执行addCount()方法尝试更新元素个数baseCount;

辅助工具类

Array 和 ArrayList 有何区别?

  • Array 可以存储基本数据类型和对象,ArrayList 只能存储对象。
  • Array 是指定固定大小的,而 ArrayList 大小是自动扩展的。
  • Array 内置方法没有 ArrayList 多,比如 addAll、removeAll、iteration 等方法只有 ArrayList 有。

对于基本类型数据,集合使用自动装箱来减少编码工作量。但是,当处理固定大小的基本数据类型的时候,这种方式相对比较慢。

如何实现 Array 和 List 之间的转换?

  • Array 转 List: Arrays. asList(array) ;
  • List 转 Array:List 的 toArray() 方法。

comparable 和 comparator的区别?

  • comparable接口实际上是出自java.lang包,它有一个 compareTo(Object obj)方法用来排序
  • comparator接口实际上是出自 java.util 包,它有一个compare(Object obj1, Object obj2)方法用来排序
  • 一般我们需要对一个集合使用自定义排序时,我们就要重写compareTo方法或compare方法,当我们需要对某一个集合实现两种排序方式,比如一个song对象中的歌名和歌手名分别采用一种排序方法的话,我们可以重写compareTo方法和使用自制的Comparator方法或者以两个Comparator来实现歌名排序和歌星名排序,第二种代表我们只能使用两个参数版的Collections.sort().

Collection 和 Collections 有什么区别?

  • java.util.Collection 是一个集合接口(集合类的一个顶级接口)。它提供了对集合对象进行基本操作的通用接口方法。Collection接口在Java 类库中有很多具体的实现。Collection接口的意义是为各种具体的集合提供了最大化的统一操作方式,其直接继承接口有List与Set。
  • Collections则是集合类的一个工具类/帮助类,其中提供了一系列静态方法,用于对集合中元素进行排序、搜索以及线程安全等各种操作。

TreeMap 和 TreeSet 在排序时如何比较元素?Collections 工具类中的 sort()方法如何比较元素?

  • TreeSet 要求存放的对象所属的类必须实现 Comparable 接口,该接口提供了比较元素的 compareTo()方法,当插入元素时会回调该方法比较元素的大小。TreeMap 要求存放的键值对映射的键必须实现 Comparable 接口从而根据键对元素进 行排 序。
  • Collections 工具类的 sort 方法有两种重载的形式,
  • 第一种要求传入的待排序容器中存放的对象比较实现 Comparable 接口以实现元素的比较;

?

  • comparable接口实际上是出自java.lang包,它有一个 compareTo(Object obj)方法用来排序
  • comparator接口实际上是出自 java.util 包,它有一个compare(Object obj1, Object obj2)方法用来排序
  • 一般我们需要对一个集合使用自定义排序时,我们就要重写compareTo方法或compare方法,当我们需要对某一个集合实现两种排序方式,比如一个song对象中的歌名和歌手名分别采用一种排序方法的话,我们可以重写compareTo方法和使用自制的Comparator方法或者以两个Comparator来实现歌名排序和歌星名排序,第二种代表我们只能使用两个参数版的Collections.sort().

Collection 和 Collections 有什么区别?

  • java.util.Collection 是一个集合接口(集合类的一个顶级接口)。它提供了对集合对象进行基本操作的通用接口方法。Collection接口在Java 类库中有很多具体的实现。Collection接口的意义是为各种具体的集合提供了最大化的统一操作方式,其直接继承接口有List与Set。
  • Collections则是集合类的一个工具类/帮助类,其中提供了一系列静态方法,用于对集合中元素进行排序、搜索以及线程安全等各种操作。

TreeMap 和 TreeSet 在排序时如何比较元素?Collections 工具类中的 sort()方法如何比较元素?

  • TreeSet 要求存放的对象所属的类必须实现 Comparable 接口,该接口提供了比较元素的 compareTo()方法,当插入元素时会回调该方法比较元素的大小。TreeMap 要求存放的键值对映射的键必须实现 Comparable 接口从而根据键对元素进 行排 序。
  • Collections 工具类的 sort 方法有两种重载的形式,
  • 第一种要求传入的待排序容器中存放的对象比较实现 Comparable 接口以实现元素的比较;
  • 第二种不强制性的要求容器中的元素必须可比较,但是要求传入第二个参数,参数是Comparator 接口的子类型(需要重写 compare 方法实现元素的比较),相当于一个临时定义的排序规则,其实就是通过接口注入比较元素大小的算法,也是对回调模式的应用(Java 中对函数式编程的支持)。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

MyBatis面试题(总结最全面的面试题)

发表于 2020-04-13

Java面试总结汇总,整理了包括Java重点知识,以及常用开源框架,欢迎大家阅读。文章可能有错误的地方,因为个人知识有限,欢迎各位大佬指出!文章持续更新中……

ID 标题 地址
1 设计模式面试题(总结最全面的面试题) juejin.cn/post/684490…
2 Java基础知识面试题(总结最全面的面试题) juejin.cn/post/684490…
3 Java集合面试题(总结最全面的面试题) juejin.cn/post/684490…
4 JavaIO、BIO、NIO、AIO、Netty面试题(总结最全面的面试题) juejin.cn/post/684490…
5 Java并发编程面试题(总结最全面的面试题) juejin.cn/post/684490…
6 Java异常面试题(总结最全面的面试题) juejin.cn/post/684490…
7 Java虚拟机(JVM)面试题(总结最全面的面试题) juejin.cn/post/684490…
8 Spring面试题(总结最全面的面试题) juejin.cn/post/684490…
9 Spring MVC面试题(总结最全面的面试题) juejin.cn/post/684490…
10 Spring Boot面试题(总结最全面的面试题) juejin.cn/post/684490…
11 Spring Cloud面试题(总结最全面的面试题) juejin.cn/post/684490…
12 Redis面试题(总结最全面的面试题) juejin.cn/post/684490…
13 MyBatis面试题(总结最全面的面试题) juejin.cn/post/684490…
14 MySQL面试题(总结最全面的面试题) juejin.cn/post/684490…
15 TCP、UDP、Socket、HTTP面试题(总结最全面的面试题) juejin.cn/post/684490…
16 Nginx面试题(总结最全面的面试题) juejin.cn/post/684490…
17 ElasticSearch面试题
18 kafka面试题
19 RabbitMQ面试题(总结最全面的面试题) juejin.cn/post/684490…
20 Dubbo面试题(总结最全面的面试题) juejin.cn/post/684490…
21 ZooKeeper面试题(总结最全面的面试题) juejin.cn/post/684490…
22 Netty面试题(总结最全面的面试题)
23 Tomcat面试题(总结最全面的面试题) juejin.cn/post/684490…
24 Linux面试题(总结最全面的面试题) juejin.cn/post/684490…
25 互联网相关面试题(总结最全面的面试题)
26 互联网安全面试题(总结最全面的面试题)

MyBatis简介

MyBatis是什么?

  • Mybatis 是一个半 ORM(对象关系映射)框架,它内部封装了 JDBC,开发时只需要关注 SQL 语句本身,不需要花费精力去处理加载驱动、创建连接、创建statement 等繁杂的过程。程序员直接编写原生态 sql,可以严格控制 sql 执行性能,灵活度高。
  • MyBatis 可以使用 XML 或注解来配置和映射原生信息,将 POJO 映射成数据库中的记录,避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。

Mybatis优缺点

优点

与传统的数据库访问技术相比,ORM有以下优点:

  • 基于SQL语句编程,相当灵活,不会对应用程序或者数据库的现有设计造成任何影响,SQL写在XML里,解除sql与程序代码的耦合,便于统一管理;提供XML标签,支持编写动态SQL语句,并可重用
  • 与JDBC相比,减少了50%以上的代码量,消除了JDBC大量冗余的代码,不需要手动开关连接
  • 很好的与各种数据库兼容(因为MyBatis使用JDBC来连接数据库,所以只要JDBC支持的数据库MyBatis都支持)
  • 提供映射标签,支持对象与数据库的ORM字段关系映射;提供对象关系映射标签,支持对象关系组件维护
  • 能够与Spring很好的集成

缺点

  • SQL语句的编写工作量较大,尤其当字段多、关联表多时,对开发人员编写SQL语句的功底有一定要求
  • SQL语句依赖于数据库,导致数据库移植性差,不能随意更换数据库

Hibernate 和 MyBatis 的区别

相同点

  • 都是对jdbc的封装,都是持久层的框架,都用于dao层的开发。

不同点

  • 映射关系
    • MyBatis 是一个半自动映射的框架,配置Java对象与sql语句执行结果的对应关系,多表关联关系配置简单
    • Hibernate 是一个全表映射的框架,配置Java对象与数据库表的对应关系,多表关联关系配置复杂

SQL优化和移植性

  • Hibernate 对SQL语句封装,提供了日志、缓存、级联(级联比 MyBatis 强大)等特性,此外还提供 HQL(Hibernate Query Language)操作数据库,数据库无关性支持好,但会多消耗性能。如果项目需要支持多种数据库,代码开发量少,但SQL语句优化困难。
  • MyBatis 需要手动编写 SQL,支持动态 SQL、处理列表、动态生成表名、支持存储过程。开发工作量相对大些。直接使用SQL语句操作数据库,不支持数据库无关性,但sql语句优化容易。

ORM是什么

  • ORM(Object Relational Mapping),对象关系映射,是一种为了解决关系型数据库数据与简单Java对象(POJO)的映射关系的技术。简单的说,ORM是通过使用描述对象和数据库之间映射的元数据,将程序中的对象自动持久化到关系型数据库中。

为什么说Mybatis是半自动ORM映射工具?它与全自动的区别在哪里?

  • Hibernate属于全自动ORM映射工具,使用Hibernate查询关联对象或者关联集合对象时,可以根据对象关系模型直接获取,所以它是全自动的。
  • 而Mybatis在查询关联对象或关联集合对象时,需要手动编写sql来完成,所以,称之为半自动ORM映射工具。

传统JDBC开发存在什么问题?

  • 频繁创建数据库连接对象、释放,容易造成系统资源浪费,影响系统性能。可以使用连接池解决这个问题。但是使用jdbc需要自己实现连接池。
  • sql语句定义、参数设置、结果集处理存在硬编码。实际项目中sql语句变化的可能性较大,一旦发生变化,需要修改java代码,系统需要重新编译,重新发布。不好维护。
  • 使用preparedStatement向占有位符号传参数存在硬编码,因为sql语句的where条件不一定,可能多也可能少,修改sql还要修改代码,系统不易维护。
  • 结果集处理存在重复代码,处理麻烦。如果可以映射成Java对象会比较方便。

JDBC编程有哪些不足之处,MyBatis是如何解决的?

  • 1、数据库链接创建、释放频繁造成系统资源浪费从而影响系统性能,如果使用数据库连接池可解决此问题。
+ 解决:在mybatis-config.xml中配置数据链接池,使用连接池管理数据库连接。
  • 2、Sql语句写在代码中造成代码不易维护,实际应用sql变化的可能较大,sql变动需要改变java代码。-
+ 解决:将Sql语句配置在XXXXmapper.xml文件中与java代码分离。
  • 3、向sql语句传参数麻烦,因为sql语句的where条件不一定,可能多也可能少,占位符需要和参数一一对应。
+ 解决: Mybatis自动将java对象映射至sql语句。
  • 4、对结果集解析麻烦,sql变化导致解析代码变化,且解析前需要遍历,如果能将数据库记录封装成pojo对象解析比较方便。
+ 解决:Mybatis自动将sql执行结果映射至java对象。

MyBatis和Hibernate的适用场景?

  • MyBatis专注于SQL本身,是一个足够灵活的DAO层解决方案。
  • 对性能的要求很高,或者需求变化较多的项目,如互联网项目,MyBatis将是不错的选择。

开发难易程度和学习成本

  • Hibernate 是重量级框架,学习使用门槛高,适合于需求相对稳定,中小型的项目,比如:办公自动化系统
  • MyBatis 是轻量级框架,学习使用门槛低,适合于需求变化频繁,大型的项目,比如:互联网电子商务系统

总结

  • MyBatis 是一个小巧、方便、高效、简单、直接、半自动化的持久层框架,
  • Hibernate 是一个强大、方便、高效、复杂、间接、全自动化的持久层框架。

MyBatis的架构

MyBatis编程步骤是什么样的?

  • 1、 创建SqlSessionFactory
  • 2、 通过SqlSessionFactory创建SqlSession
  • 3、 通过sqlsession执行数据库操作
  • 4、 调用session.commit()提交事务
  • 5、 调用session.close()关闭会话

请说说MyBatis的工作原理

  • 在学习 MyBatis 程序之前,需要了解一下 MyBatis 工作原理,以便于理解程序。MyBatis 的工作原理如下图
  1. 读取 MyBatis 配置文件:mybatis-config.xml 为 MyBatis 的全局配置文件,配置了 MyBatis 的运行环境等信息,例如数据库连接信息。
  2. 加载映射文件。映射文件即 SQL 映射文件,该文件中配置了操作数据库的 SQL 语句,需要在 MyBatis 配置文件 mybatis-config.xml 中加载。mybatis-config.xml 文件可以加载多个映射文件,每个文件对应数据库中的一张表。
  3. 构造会话工厂:通过 MyBatis 的环境等配置信息构建会话工厂 SqlSessionFactory。
  4. 创建会话对象:由会话工厂创建 SqlSession 对象,该对象中包含了执行 SQL 语句的所有方法。
  5. Executor 执行器:MyBatis 底层定义了一个 Executor 接口来操作数据库,它将根据 SqlSession 传递的参数动态地生成需要执行的 SQL 语句,同时负责查询缓存的维护。
  6. MappedStatement 对象:在 Executor 接口的执行方法中有一个 MappedStatement 类型的参数,该参数是对映射信息的封装,用于存储要映射的 SQL 语句的 id、参数等信息。
  7. 输入参数映射:输入参数类型可以是 Map、List 等集合类型,也可以是基本数据类型和 POJO 类型。输入参数映射过程类似于 JDBC 对 preparedStatement 对象设置参数的过程。
  8. 输出结果映射:输出结果类型可以是 Map、 List 等集合类型,也可以是基本数据类型和 POJO 类型。输出结果映射过程类似于 JDBC 对结果集的解析过程。

MyBatis的功能架构是怎样的

在这里插入图片描述

  • 我们把Mybatis的功能架构分为三层:
    • API接口层:提供给外部使用的接口API,开发人员通过这些本地API来操纵数据库。接口层一接收到调用请求就会调用数据处理层来完成具体的数据处理。
    • 数据处理层:负责具体的SQL查找、SQL解析、SQL执行和执行结果映射处理等。它主要的目的是根据调用的请求完成一次数据库操作。
    • 基础支撑层:负责最基础的功能支撑,包括连接管理、事务管理、配置加载和缓存处理,这些都是共用的东西,将他们抽取出来作为最基础的组件。为上层的数据处理层提供最基础的支撑。

MyBatis的框架架构设计是怎么样的

在这里插入图片描述

  • 这张图从上往下看。MyBatis的初始化,会从mybatis-config.xml配置文件,解析构造成Configuration这个类,就是图中的红框。
  1. 加载配置:配置来源于两个地方,一处是配置文件,一处是Java代码的注解,将SQL的配置信息加载成为一个个MappedStatement对象(包括了传入参数映射配置、执行的SQL语句、结果映射配置),存储在内存中。
  2. SQL解析:当API接口层接收到调用请求时,会接收到传入SQL的ID和传入对象(可以是Map、JavaBean或者基本数据类型),Mybatis会根据SQL的ID找到对应的MappedStatement,然后根据传入参数对象对MappedStatement进行解析,解析后可以得到最终要执行的SQL语句和参数。
  3. SQL执行:将最终得到的SQL和参数拿到数据库进行执行,得到操作数据库的结果。
  4. 结果映射:将操作数据库的结果按照映射的配置进行转换,可以转换成HashMap、JavaBean或者基本数据类型,并将最终结果返回。

什么是DBMS

  • DBMS:数据库管理系统(database management system)是一种操纵和管理数据库的大型软件,用于建立、使用和维护数zd据库,简称dbms。它对数据库进行统一的管理和控制,以保证数据库的安全性和完整性。用户通过dbms访问数据库中的数据,数据库管理员也通过dbms进行数据库的维护工作。它可使多个应用程序和用户用不同的方法在同时版或不同时刻去建立,修改和询问数据库。DBMS提供数据定义语言DDL(Data Definition Language)与数据操作语言DML(Data Manipulation Language),供用户定义数据库的模式结构与权限约束,实现对数据的追加权、删除等操作。

为什么需要预编译

  • 定义:
+ SQL 预编译指的是数据库驱动在发送 SQL 语句和参数给 DBMS 之前对 SQL 语句进行编译,这样 DBMS 执行 SQL 时,就不需要重新编译。
  • 为什么需要预编译
+ JDBC 中使用对象 PreparedStatement 来抽象预编译语句,使用预编译。预编译阶段可以优化 SQL 的执行。预编译之后的 SQL 多数情况下可以直接执行,DBMS 不需要再次编译,越复杂的SQL,编译的复杂度将越大,预编译阶段可以合并多次操作为一个操作。同时预编译语句对象可以重复利用。把一个 SQL 预编译后产生的 PreparedStatement 对象缓存下来,下次对于同一个SQL,可以直接使用这个缓存的 PreparedState 对象。Mybatis默认情况下,将对所有的 SQL 进行预编译。
+ 还有一个重要的原因,复制SQL注入

Mybatis都有哪些Executor执行器?它们之间的区别是什么?

  • Mybatis有三种基本的Executor执行器,SimpleExecutor、ReuseExecutor、BatchExecutor。
  • SimpleExecutor:每执行一次update或select,就开启一个Statement对象,用完立刻关闭Statement对象。
  • ReuseExecutor:执行update或select,以sql作为key查找Statement对象,存在就使用,不存在就创建,用完后,不关闭Statement对象,而是放置于Map<String, Statement>内,供下一次使用。简言之,就是重复使用Statement对象。
  • BatchExecutor:执行update(没有select,JDBC批处理不支持select),将所有sql都添加到批处理中(addBatch()),等待统一执行(executeBatch()),它缓存了多个Statement对象,每个Statement对象都是addBatch()完毕后,等待逐一执行executeBatch()批处理。与JDBC批处理相同。

作用范围:Executor的这些特点,都严格限制在SqlSession生命周期范围内。

Mybatis中如何指定使用哪一种Executor执行器?

  • 在Mybatis配置文件中,在设置(settings)可以指定默认的ExecutorType执行器类型,也可以手动给DefaultSqlSessionFactory的创建SqlSession的方法传递ExecutorType类型参数,如SqlSession openSession(ExecutorType execType)。
  • 配置默认的执行器。SIMPLE 就是普通的执行器;REUSE 执行器会重用预处理语句(prepared statements); BATCH 执行器将重用语句并执行批量更新。

Mybatis是否支持延迟加载?如果支持,它的实现原理是什么?

  • Mybatis仅支持association关联对象和collection关联集合对象的延迟加载,association指的就是一对一,collection指的就是一对多查询。在Mybatis配置文件中,可以配置是否启用延迟加载lazyLoadingEnabled=true|false。
  • 它的原理是,使用CGLIB创建目标对象的代理对象,当调用目标方法时,进入拦截器方法,比如调用a.getB().getName(),拦截器invoke()方法发现a.getB()是null值,那么就会单独发送事先保存好的查询关联B对象的sql,把B查询上来,然后调用a.setB(b),于是a的对象b属性就有值了,接着完成a.getB().getName()方法的调用。这就是延迟加载的基本原理。
  • 当然了,不光是Mybatis,几乎所有的包括Hibernate,支持延迟加载的原理都是一样的。

映射器

#{}和${}的区别

  • #{}是占位符,预编译处理;${}是拼接符,字符串替换,没有预编译处理。
  • Mybatis在处理#{}时,#{}传入参数是以字符串传入,会将SQL中的#{}替换为?号,调用PreparedStatement的set方法来赋值。
  • #{} 可以有效的防止SQL注入,提高系统安全性;${} 不能防止SQL 注入
  • #{} 的变量替换是在DBMS 中;${} 的变量替换是在 DBMS 外

模糊查询like语句该怎么写

  • 1 ’%${question}%’ 可能引起SQL注入,不推荐
  • 2 “%”#{question}”%” 注意:因为#{…}解析成sql语句时候,会在变量外侧自动加单引号’ ‘,所以这里 % 需要使用双引号” “,不能使用单引号 ’ ‘,不然会查不到任何结果。
  • 3 CONCAT(’%’,#{question},’%’) 使用CONCAT()函数,(推荐)
  • 4 使用bind标签(不推荐)
1
2
3
4
复制代码<select id="listUserLikeUsername" resultType="com.jourwon.pojo.User">
&emsp;&emsp;<bind name="pattern" value="'%' + username + '%'" />
&emsp;&emsp;select id,sex,age,username,password from person where username LIKE #{pattern}
</select>

在mapper中如何传递多个参数

方法1:顺序传参法

1
2
3
4
5
6
复制代码public User selectUser(String name, int deptId);

<select id="selectUser" resultMap="UserResultMap">
select * from user
where user_name = #{0} and dept_id = #{1}
</select>
  • #{}里面的数字代表传入参数的顺序。
  • 这种方法不建议使用,sql层表达不直观,且一旦顺序调整容易出错。

方法2:@Param注解传参法

1
2
3
4
5
6
复制代码public User selectUser(@Param("userName") String name, int @Param("deptId") deptId);

<select id="selectUser" resultMap="UserResultMap">
select * from user
where user_name = #{userName} and dept_id = #{deptId}
</select>
  • #{}里面的名称对应的是注解@Param括号里面修饰的名称。
  • 这种方法在参数不多的情况还是比较直观的,(推荐使用)。

方法3:Map传参法

1
2
3
4
5
6
复制代码public User selectUser(Map<String, Object> params);

<select id="selectUser" parameterType="java.util.Map" resultMap="UserResultMap">
select * from user
where user_name = #{userName} and dept_id = #{deptId}
</select>
  • #{}里面的名称对应的是Map里面的key名称。
  • 这种方法适合传递多个参数,且参数易变能灵活传递的情况。(推荐使用)。

方法4:Java Bean传参法

1
2
3
4
5
6
复制代码public User selectUser(User user);

<select id="selectUser" parameterType="com.jourwon.pojo.User" resultMap="UserResultMap">
select * from user
where user_name = #{userName} and dept_id = #{deptId}
</select>
  • #{}里面的名称对应的是User类里面的成员属性。
  • 这种方法直观,需要建一个实体类,扩展不容易,需要加属性,但代码可读性强,业务逻辑处理方便,推荐使用。(推荐使用)。

Mybatis如何执行批量操作

  • 使用foreach标签

  • foreach的主要用在构建in条件中,它可以在SQL语句中进行迭代一个集合。foreach标签的属性主要有item,index,collection,open,separator,close。

    • item   表示集合中每一个元素进行迭代时的别名,随便起的变量名;
    • index   指定一个名字,用于表示在迭代过程中,每次迭代到的位置,不常用;
    • open   表示该语句以什么开始,常用“(”;
    • separator 表示在每次进行迭代之间以什么符号作为分隔符,常用“,”;
    • close   表示以什么结束,常用“)”。
  • 在使用foreach的时候最关键的也是最容易出错的就是collection属性,该属性是必须指定的,但是在不同情况下,该属性的值是不一样的,主要有一下3种情况:

    1. 如果传入的是单参数且参数类型是一个List的时候,collection属性值为list

    2. 如果传入的是单参数且参数类型是一个array数组的时候,collection的属性值为array

    3. 如果传入的参数是多个的时候,我们就需要把它们封装成一个Map了,当然单参数也可以封装成map,实际上如果你在传入参数的时候,在MyBatis里面也是会把它封装成一个Map的,

      map的key就是参数名,所以这个时候collection属性值就是传入的List或array对象在自己封装的map里面的key

  • 具体用法如下:

1
2
3
4
5
6
7
8
9
10
11
复制代码<!-- 批量保存(foreach插入多条数据两种方法)
int addEmpsBatch(@Param("emps") List<Employee> emps); -->
<!-- MySQL下批量保存,可以foreach遍历 mysql支持values(),(),()语法 --> //推荐使用

<insert id="addEmpsBatch">
INSERT INTO emp(ename,gender,email,did)
VALUES
<foreach collection="emps" item="emp" separator=",">
(#{emp.eName},#{emp.gender},#{emp.email},#{emp.dept.id})
</foreach>
</insert>
1
2
3
4
5
6
7
8
9
复制代码<!-- 这种方式需要数据库连接属性allowMutiQueries=true的支持
如jdbc.url=jdbc:mysql://localhost:3306/mybatis?allowMultiQueries=true -->

<insert id="addEmpsBatch">
<foreach collection="emps" item="emp" separator=";">
INSERT INTO emp(ename,gender,email,did)
VALUES(#{emp.eName},#{emp.gender},#{emp.email},#{emp.dept.id})
</foreach>
</insert>
  • 使用ExecutorType.BATCH
+ Mybatis内置的ExecutorType有3种,默认为simple,该模式下它为每个语句的执行创建一个新的预处理语句,单条提交sql;而batch模式重复使用已经预处理的语句,并且批量执行所有更新语句,显然batch性能将更优; 但batch模式也有自己的问题,比如在Insert操作时,在事务没有提交之前,是没有办法获取到自增的id,这在某型情形下是不符合业务要求的
+ 具体用法如下:



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
复制代码//批量保存方法测试
@Test
public void testBatch() throws IOException{
SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
//可以执行批量操作的sqlSession
SqlSession openSession = sqlSessionFactory.openSession(ExecutorType.BATCH);

//批量保存执行前时间
long start = System.currentTimeMillis();
try {
EmployeeMapper mapper = openSession.getMapper(EmployeeMapper.class);
for (int i = 0; i < 1000; i++) {
mapper.addEmp(new Employee(UUID.randomUUID().toString().substring(0, 5), "b", "1"));
}

openSession.commit();
long end = System.currentTimeMillis();
//批量保存执行后的时间
System.out.println("执行时长" + (end - start));
//批量 预编译sql一次==》设置参数==》10000次==》执行1次 677
//非批量 (预编译=设置参数=执行 )==》10000次 1121

} finally {
openSession.close();
}
}
+ mapper和mapper.xml如下
1
2
3
4
复制代码public interface EmployeeMapper {   
//批量保存员工
Long addEmp(Employee employee);
}
1
2
3
4
5
6
7
复制代码<mapper namespace="com.jourwon.mapper.EmployeeMapper"
<!--批量保存员工 -->
<insert id="addEmp">
insert into employee(lastName,email,gender)
values(#{lastName},#{email},#{gender})
</insert>
</mapper>

如何获取生成的主键

  • 新增标签中添加:keyProperty=” ID “ 即可
1
2
3
4
5
复制代码<insert id="insert" useGeneratedKeys="true" keyProperty="userId" >
insert into user(
user_name, user_password, create_time)
values(#{userName}, #{userPassword} , #{createTime, jdbcType= TIMESTAMP})
</insert>

在这里插入图片描述

当实体类中的属性名和表中的字段名不一样 ,怎么办

  • 第1种: 通过在查询的SQL语句中定义字段名的别名,让字段名的别名和实体类的属性名一致。
1
2
3
复制代码<select id="getOrder" parameterType="int" resultType="com.jourwon.pojo.Order">
select order_id id, order_no orderno ,order_price price form orders where order_id=#{id};
</select>
  • 第2种: 通过<resultMap>来映射字段名和实体类属性名的一一对应的关系。
1
2
3
4
5
6
7
8
9
10
11
12
复制代码<select id="getOrder" parameterType="int" resultMap="orderResultMap">
select * from orders where order_id=#{id}
</select>

<resultMap type="com.jourwon.pojo.Order" id="orderResultMap">
<!–用id属性来映射主键字段–>
<id property="id" column="order_id">

<!–用result属性来映射非主键字段,property为实体类属性名,column为数据库表中的属性–>
<result property ="orderno" column ="order_no"/>
<result property="price" column="order_price" />
</reslutMap>

Mapper 编写有哪几种方式?

  • 第一种:接口实现类继承 SqlSessionDaoSupport:使用此种方法需要编写mapper 接口,mapper 接口实现类、mapper.xml 文件。
1. 在 sqlMapConfig.xml 中配置 mapper.xml 的位置



1
2
3
4
复制代码<mappers>
<mapper resource="mapper.xml 文件的地址" />
<mapper resource="mapper.xml 文件的地址" />
</mappers>
2. 定义 mapper 接口 3. 实现类集成 SqlSessionDaoSupport mapper 方法中可以 this.getSqlSession()进行数据增删改查。 4. spring 配置
1
2
3
4
复制代码<bean id=" " class="mapper 接口的实现">
<property name="sqlSessionFactory"
ref="sqlSessionFactory"></property>
</bean>
  • 第二种:使用 org.mybatis.spring.mapper.MapperFactoryBean:
1. 在 sqlMapConfig.xml 中配置 mapper.xml 的位置,如果 mapper.xml 和mappre 接口的名称相同且在同一个目录,这里可以不用配置
2. 定义 mapper 接口:



1
2
3
4
复制代码<mappers>
<mapper resource="mapper.xml 文件的地址" />
<mapper resource="mapper.xml 文件的地址" />
</mappers>
3. mapper.xml 中的 namespace 为 mapper 接口的地址 4. mapper 接口中的方法名和 mapper.xml 中的定义的 statement 的 id 保持一致 5. Spring 中定义
1
2
3
4
复制代码<bean id="" class="org.mybatis.spring.mapper.MapperFactoryBean">
<property name="mapperInterface" value="mapper 接口地址" />
<property name="sqlSessionFactory" ref="sqlSessionFactory" />
</bean>
  • 第三种:使用 mapper 扫描器:
1. mapper.xml 文件编写:


mapper.xml 中的 namespace 为 mapper 接口的地址;


mapper 接口中的方法名和 mapper.xml 中的定义的 statement 的 id 保持一致;


如果将 mapper.xml 和 mapper 接口的名称保持一致则不用在 sqlMapConfig.xml中进行配置。
2. 定义 mapper 接口:


注意 mapper.xml 的文件名和 mapper 的接口名称保持一致,且放在同一个目录
3. 配置 mapper 扫描器:



1
2
3
4
5
6
复制代码<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="mapper 接口包地址
"></property>
<property name="sqlSessionFactoryBeanName"
value="sqlSessionFactory"/>
</bean>
4. 使用扫描器后从 spring 容器中获取 mapper 的实现对象。

什么是MyBatis的接口绑定?有哪些实现方式?

  • 接口绑定,就是在MyBatis中任意定义接口,然后把接口里面的方法和SQL语句绑定,我们直接调用接口方法就可以,这样比起原来了SqlSession提供的方法我们可以有更加灵活的选择和设置。
  • 接口绑定有两种实现方式
1. 通过注解绑定,就是在接口的方法上面加上 @Select、@Update等注解,里面包含Sql语句来绑定;
2. 通过xml里面写SQL来绑定, 在这种情况下,要指定xml映射文件里面的namespace必须为接口的全路径名。当Sql语句比较简单时候,用注解绑定, 当SQL语句比较复杂时候,用xml绑定,一般用xml绑定的比较多。

使用MyBatis的mapper接口调用时有哪些要求?

  1. Mapper接口方法名和mapper.xml中定义的每个sql的id相同。
  2. Mapper接口方法的输入参数类型和mapper.xml中定义的每个sql 的parameterType的类型相同。
  3. Mapper接口方法的输出参数类型和mapper.xml中定义的每个sql的resultType的类型相同。
  4. Mapper.xml文件中的namespace即是mapper接口的类路径。

这个Dao接口的工作原理是什么?Dao接口里的方法,参数不同时,方法能重载吗

  • Dao接口的工作原理是JDK动态代理,Mybatis运行时会使用JDK动态代理为Dao接口生成代理proxy对象,代理对象proxy会拦截接口方法,转而执行MappedStatement所代表的sql,然后将sql执行结果返回。
  • Dao接口里的方法,是不能重载的,因为是全限名+方法名的保存和寻找策略。

Mybatis的Xml映射文件中,不同的Xml映射文件,id是否可以重复?

  • 不同的Xml映射文件,如果配置了namespace,那么id可以重复;如果没有配置namespace,那么id不能重复;毕竟namespace不是必须的,只是最佳实践而已。
  • 原因就是namespace+id是作为Map<String, MappedStatement>的key使用的,如果没有namespace,就剩下id,那么,id重复会导致数据互相覆盖。有了namespace,自然id就可以重复,namespace不同,namespace+id自然也就不同。

简述Mybatis的Xml映射文件和Mybatis内部数据结构之间的映射关系?

  • 答:Mybatis将所有Xml配置信息都封装到All-In-One重量级对象Configuration内部。在Xml映射文件中,<parameterMap>标签会被解析为ParameterMap对象,其每个子元素会被解析为ParameterMapping对象。<resultMap>标签会被解析为ResultMap对象,其每个子元素会被解析为ResultMapping对象。每一个<select>、<insert>、<update>、<delete>标签均会被解析为MappedStatement对象,标签内的sql会被解析为BoundSql对象。

Mybatis是如何将sql执行结果封装为目标对象并返回的?都有哪些映射形式?

  • 第一种是使用<resultMap>标签,逐一定义列名和对象属性名之间的映射关系。
  • 第二种是使用sql列的别名功能,将列别名书写为对象属性名,比如T_NAME AS NAME,对象属性名一般是name,小写,但是列名不区分大小写,Mybatis会忽略列名大小写,智能找到与之对应对象属性名,你甚至可以写成T_NAME AS NaMe,Mybatis一样可以正常工作。

有了列名与属性名的映射关系后,Mybatis通过反射创建对象,同时使用反射给对象的属性逐一赋值并返回,那些找不到映射关系的属性,是无法完成赋值的。

Xml映射文件中,除了常见的select|insert|updae|delete标签之外,还有哪些标签?

  • 还有很多其他的标签,<resultMap>、<parameterMap>、<sql>、<include>、<selectKey>,加上动态sql的9个标签,trim|where|set|foreach|if|choose|when|otherwise|bind等,其中<sql>为sql片段标签,通过<include>标签引入sql片段,<selectKey>为不支持自增的主键生成策略标签。

Mybatis映射文件中,如果A标签通过include引用了B标签的内容,请问,B标签能否定义在A标签的后面,还是说必须定义在A标签的前面?

  • 虽然Mybatis解析Xml映射文件是按照顺序解析的,但是,被引用的B标签依然可以定义在任何地方,Mybatis都可以正确识别。
  • 原理是,Mybatis解析A标签,发现A标签引用了B标签,但是B标签尚未解析到,尚不存在,此时,Mybatis会将A标签标记为未解析状态,然后继续解析余下的标签,包含B标签,待所有标签解析完毕,Mybatis会重新解析那些被标记为未解析的标签,此时再解析A标签时,B标签已经存在,A标签也就可以正常解析完成了。

Mybatis能执行一对多,一对一的联系查询吗,有哪些实现方法

  • 能,不止可以一对多,一对一还可以多对多,一对多
  • 实现方式:
    1. 单独发送一个SQL去查询关联对象,赋给主对象,然后返回主对象
    2. 使用嵌套查询,似JOIN查询,一部分是A对象的属性值,另一部分是关联对 象 B的属性值,好处是只要发送一个属性值,就可以把主对象和关联对象查出来
    3. 子查询

Mybatis是否可以映射Enum枚举类?

  • Mybatis可以映射枚举类,不单可以映射枚举类,Mybatis可以映射任何对象到表的一列上。映射方式为自定义一个TypeHandler,实现TypeHandler的setParameter()和getResult()接口方法。
  • TypeHandler有两个作用,一是完成从javaType至jdbcType的转换,二是完成jdbcType至javaType的转换,体现为setParameter()和getResult()两个方法,分别代表设置sql问号占位符参数和获取列查询结果。

Mybatis动态sql是做什么的?都有哪些动态sql?能简述一下动态sql的执行原理吗?

  • Mybatis动态sql可以让我们在Xml映射文件内,以标签的形式编写动态sql,完成逻辑判断和动态拼接sql的功能,Mybatis提供了9种动态sql标签trim|where|set|foreach|if|choose|when|otherwise|bind。
  • 其执行原理为,使用OGNL从sql参数对象中计算表达式的值,根据表达式的值动态拼接sql,以此来完成动态sql的功能。

Mybatis是如何进行分页的?分页插件的原理是什么?

  • Mybatis使用RowBounds对象进行分页,它是针对ResultSet结果集执行的内存分页,而非物理分页,可以在sql内直接书写带有物理分页的参数来完成物理分页功能,也可以使用分页插件来完成物理分页。
  • 分页插件的基本原理是使用Mybatis提供的插件接口,实现自定义插件,在插件的拦截方法内拦截待执行的sql,然后重写sql,根据dialect方言,添加对应的物理分页语句和物理分页参数。
  • 举例:select * from student,拦截sql后重写为:select t.* from (select * from student) t limit 0, 10

简述Mybatis的插件运行原理,以及如何编写一个插件。

  • Mybatis仅可以编写针对ParameterHandler、ResultSetHandler、StatementHandler、Executor这4种接口的插件,Mybatis使用JDK的动态代理,为需要拦截的接口生成代理对象以实现接口方法拦截功能,每当执行这4种接口对象的方法时,就会进入拦截方法,具体就是InvocationHandler的invoke()方法,当然,只会拦截那些你指定需要拦截的方法。
  • 实现Mybatis的Interceptor接口并复写intercept()方法,然后在给插件编写注解,指定要拦截哪一个接口的哪些方法即可,记住,别忘了在配置文件中配置你编写的插件。

Mybatis的一级、二级缓存

  1. 一级缓存: 基于 PerpetualCache 的 HashMap 本地缓存,其存储作用域为 Session,当 Session flush 或 close 之后,该 Session 中的所有 Cache 就将清空,默认打开一级缓存。
  2. 二级缓存与一级缓存其机制相同,默认也是采用 PerpetualCache,HashMap 存储,不同在于其存储作用域为 Mapper(Namespace),并且可自定义存储源,如 Ehcache。默认不打开二级缓存,要开启二级缓存,使用二级缓存属性类需要实现Serializable序列化接口(可用来保存对象的状态),可在它的映射文件中配置<cache/>
  3. 对于缓存数据更新机制,当某一个作用域(一级缓存 Session/二级缓存Namespaces)的进行了C/U/D 操作后,默认该作用域下所有 select 中的缓存将被 clear。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…820821822…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%