diff --git a/DB.md b/DB.md index 579e96a..7fd57da 100644 --- a/DB.md +++ b/DB.md @@ -4,7 +4,7 @@ ### 数据库 -数据库:DataBase,简称 DB,用于存储和管理数据的仓库,它的存储空间很大,可以存放百万上亿条数据。 +数据库:DataBase,简称 DB,存储和管理数据的仓库 数据库的优势: @@ -22,19 +22,18 @@ - 数据表 - 数据库最重要的组成部分之一 - - 它由纵向的列和横向的行组成(类似 excel 表格) + - 由纵向的列和横向的行组成(类似 excel 表格) - 可以指定列名、数据类型、约束等 - 一个表中可以存储多条数据 - 数据:想要永久化存储的数据 - 参考视频:https://www.bilibili.com/video/BV1zJ411M7TB -参考文章:https://time.geekbang.org/column/intro/139 +参考专栏:https://time.geekbang.org/column/intro/139 参考书籍:https://book.douban.com/subject/35231266/ @@ -46,13 +45,11 @@ ### MySQL -MySQL 数据库是一个最流行的关系型数据库管理系统之一 - -关系型数据库是将数据保存在不同的数据表中,而且表与表之间可以有关联关系,提高了灵活性。 +MySQL 数据库是一个最流行的关系型数据库管理系统之一,关系型数据库是将数据保存在不同的数据表中,而且表与表之间可以有关联关系,提高了灵活性 缺点:数据存储在磁盘中,导致读写性能差,而且数据关系复杂,扩展性差 -MySQL 所使用的SQL语句是用于访问数据库最常用的标准化语言。 +MySQL 所使用的 SQL 语句是用于访问数据库最常用的标准化语言 MySQL 配置: @@ -118,160 +115,56 @@ MySQL 配置: cd /etc/mysql/mysql.conf.d sudo chmod 666 mysqld.cnf vim mysqld.cnf - #bind-address = 127.0.0.1注释该行 + # bind-address = 127.0.0.1注释该行 ``` * 关闭 Linux 防火墙 ```shell systemctl stop firewalld.service - 放行3306端口 + # 放行3306端口 ``` -*** - - - -### 常用工具 - -#### mysql - -mysql 不是指 mysql 服务,而是指 mysql 的客户端工具 - -```sh -mysql [options] [database] -``` - -* -u --user=name:指定用户名 -* -p --password[=name]:指定密码 -* -h --host=name:指定服务器IP或域名 -* -P --port=#:指定连接端口 -* -e --execute=name:执行SQL语句并退出,在控制台执行SQL语句,而不用连接到数据库执行 - -示例: - -```sh -mysql -h 127.0.0.1 -P 3306 -u root -p -mysql -uroot -p2143 db01 -e "select * from tb_book"; -``` - - - -*** - - - -#### admin - -mysqladmin 是一个执行管理操作的客户端程序,用来检查服务器的配置和当前状态、创建并删除数据库等 - -通过 `mysqladmin --help` 指令查看帮助文档 - -```sh -mysqladmin -uroot -p2143 create 'test01'; -``` - - - -*** - - - -#### binlog - -服务器生成的日志文件以二进制格式保存,如果需要检查这些文本,就要使用 mysqlbinlog 日志管理工具 - -```sh -mysqlbinlog [options] log-files1 log-files2 ... -``` - -* -d --database=name:指定数据库名称,只列出指定的数据库相关操作 - -* -o --offset=#:忽略掉日志中的前n行命令。 - -* -r --result-file=name:将输出的文本格式日志输出到指定文件。 - -* -s --short-form:显示简单格式, 省略掉一些信息。 - -* --start-datatime=date1 --stop-datetime=date2:指定日期间隔内的所有日志。 - -* --start-position=pos1 --stop-position=pos2:指定位置间隔内的所有日志。 - - - -*** - - - -#### dump - -##### 命令介绍 - -mysqldump 客户端工具用来备份数据库或在不同数据库之间进行数据迁移,备份内容包含创建表,及插入表的SQL语句 - -```sh -mysqldump [options] db_name [tables] -mysqldump [options] --database/-B db1 [db2 db3...] -mysqldump [options] --all-databases/-A -``` - -连接选项: - -* -u --user=name:指定用户名 -* -p --password[=name]:指定密码 -* -h --host=name:指定服务器IP或域名 -* -P --port=#:指定连接端口 - -输出内容选项: - -* --add-drop-database:在每个数据库创建语句前加上 Drop database 语句 -* --add-drop-table:在每个表创建语句前加上 Drop table 语句 , 默认开启 ; 不开启 (--skip-add-drop-table) -* -n --no-create-db:不包含数据库的创建语句 -* -t --no-create-info:不包含数据表的创建语句 -* -d --no-data:不包含数据 -* -T, --tab=name:自动生成两个文件:一个.sql文件,创建表结构的语句;一个.txt文件,数据文件,相当于select into outfile - -示例: - -```sh -mysqldump -uroot -p2143 db01 tb_book --add-drop-database --add-drop-table > a -mysqldump -uroot -p2143 -T /tmp test city -``` - *** -##### 数据备份 - -命令行方式: - -* 备份命令:mysqldump -u root -p 数据库名称 > 文件保存路径 - -* 恢复 - 1. 登录MySQL数据库:`mysql -u root p` - 2. 删除已经备份的数据库 - 3. 重新创建与备份数据库名称相同的数据库 - 4. 使用该数据库 - 5. 导入文件执行:`source 备份文件全路径` - - -图形化界面: -* 备份 +## 体系架构 - ![图形化界面备份](https://gitee.com/seazean/images/raw/master/DB/图形化界面备份.png) +### 整体架构 -* 恢复 +体系结构详解: - ![图形化界面恢复](https://gitee.com/seazean/images/raw/master/DB/图形化界面恢复.png) +* 第一层:网络连接层 + * 一些客户端和链接服务,包含本地 Socket 通信和大多数基于客户端/服务端工具实现的 TCP/IP 通信,主要完成一些类似于连接处理、授权认证、及相关的安全方案 + * 在该层上引入了**连接池** Connection Pool 的概念,管理缓冲用户连接,线程处理等需要缓存的需求 + * 在该层上实现基于 SSL 的安全链接,服务器也会为安全接入的每个客户端验证它所具有的操作权限 +- 第二层:核心服务层 + * 查询缓存、分析器、优化器、执行器等,涵盖 MySQL 的大多数核心服务功能,所有的内置函数(日期、数学、加密函数等) + * Management Serveices & Utilities:系统管理和控制工具,备份、安全、复制、集群等 + * SQL Interface:接受用户的 SQL 命令,并且返回用户需要查询的结果 + * Parser:SQL 语句分析器 + * Optimizer:查询优化器 + * Caches & Buffers:查询缓存,服务器会查询内部的缓存,如果缓存空间足够大,可以在大量读操作的环境中提升系统性能 + * 所有**跨存储引擎的功能**在这一层实现,如存储过程、触发器、视图等 + * 在该层服务器会解析查询并创建相应的内部解析树,并对其完成相应的优化如确定表的查询顺序,是否利用索引等, 最后生成相应的执行操作 + * MySQL 中服务器层不管理事务,**事务是由存储引擎实现的** +- 第三层:存储引擎层 + - Pluggable Storage Engines:存储引擎接口,MySQL 区别于其他数据库的重要特点就是其存储引擎的架构模式是插件式的(存储引擎是基于表的,而不是数据库) + - 存储引擎**真正的负责了 MySQL 中数据的存储和提取**,服务器通过 API 和存储引擎进行通信 + - 不同的存储引擎具有不同的功能,共用一个 Server 层,可以根据开发的需要,来选取合适的存储引擎 +- 第四层:系统文件层 + - 数据存储层,主要是将数据存储在文件系统之上,并完成与存储引擎的交互 + - File System:文件系统,保存配置文件、数据文件、日志文件、错误文件、二进制文件等 +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-体系结构.png) @@ -279,25 +172,19 @@ mysqldump -uroot -p2143 -T /tmp test city -#### import +### 建立连接 -mysqlimport 是客户端数据导入工具,用来导入mysqldump 加 -T 参数后导出的文本文件 +#### 连接器 -```sh -mysqlimport [options] db_name textfile1 [textfile2...] -``` +池化技术:对于访问数据库来说,建立连接的代价是比较昂贵的,因为每个连接对应一个用来交互的线程,频繁的创建关闭连接比较耗费资源,有必要建立数据库连接池,以提高访问的性能 -示例: +连接建立 TCP 以后需要做**权限验证**,验证成功后可以进行执行 SQL。如果这时管理员账号对这个用户的权限做了修改,也不会影响已经存在连接的权限,只有再新建的连接才会使用新的权限设置 -```sh -mysqlimport -uroot -p2143 test /tmp/city.txt -``` +MySQL 服务器可以同时和多个客户端进行交互,所以要保证每个连接会话的隔离性(事务机制部分详解) -导入 sql 文件,可以使用 MySQL 中的 source 指令 : +整体的执行流程: -```mysql -source 文件全路径 -``` + @@ -305,87 +192,43 @@ source 文件全路径 -#### show - -mysqlshow 客户端对象查找工具,用来很快地查找存在哪些数据库、数据库中的表、表中的列或者索引 - -```sh -mysqlshow [options] [db_name [table_name [col_name]]] -``` - -* --count:显示数据库及表的统计信息(数据库,表 均可以不指定) - -* -i:显示指定数据库或者指定表的状态信息 - -示例: - -```sh -#查询每个数据库的表的数量及表中记录的数量 -mysqlshow -uroot -p1234 --count -#查询test库中每个表中的字段书,及行数 -mysqlshow -uroot -p1234 test --count -#查询test库中book表的详细情况 -mysqlshow -uroot -p1234 test book --count -``` - - - - - -*** +#### 权限信息 +grant 语句会同时修改数据表和内存,判断权限的时候使用的是内存数据 +flush privileges 语句本身会用数据表(磁盘)的数据重建一份内存权限数据,所以在权限数据可能存在不一致的情况下使用,这种不一致往往是由于直接用 DML 语句操作系统权限表导致的,所以尽量不要使用这类语句 +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-权限范围.png) -## 体系结构 -### 整体架构 -体系结构详解: -* 第一层:网络连接层 - * 一些客户端和链接服务,包含本地 socket 通信和大多数基于客户端/服务端工具实现的 TCP/IP 通信,主要完成一些类似于连接处理、授权认证、及相关的安全方案 - * 在该层上引入了连接池 Connection Pool 的概念,管理缓冲用户连接,线程处理等需要缓存的需求 - * 在该层上实现基于 SSL 的安全链接,服务器也会为安全接入的每个客户端验证它所具有的操作权限 -- 第二层:核心服务层 - * 查询缓存、分析器、优化器、执行器等,涵盖 MySQL 的大多数核心服务功能,所有的内置函数(日期、数学、加密函数等) - * Management Serveices & Utilities:系统管理和控制工具,备份、安全、复制、集群等 - * SQL Interface:接受用户的 SQL 命令,并且返回用户需要查询的结果 - * Parser:SQL 语句分析器 - * Optimizer:查询优化器 - * Caches & Buffers:查询缓存,服务器会查询内部的缓存,如果缓存空间足够大,可以在大量读操作的环境中提升系统性能 - * 所有**跨存储引擎的功能**在这一层实现,如存储过程、触发器、视图等 - * 在该层服务器会解析查询并创建相应的内部解析树,并对其完成相应的优化如确定表的查询顺序,是否利用索引等, 最后生成相应的执行操作 - * MySQL 中服务器层不管理事务,**事务是由存储引擎实现的** -- 第三层:存储引擎层 - - Pluggable Storage Engines:存储引擎接口,MySQL 区别于其他数据库的重要特点就是其存储引擎的架构模式是插件式的(存储引擎是基于表的,而不是数据库) - - 存储引擎**真正的负责了 MySQL 中数据的存储和提取**,服务器通过 API 和存储引擎进行通信 - - 不同的存储引擎具有不同的功能,共用一个 Server 层,可以根据开发的需要,来选取合适的存储引擎 -- 第四层:系统文件层 - - 数据存储层,主要是将数据存储在文件系统之上,并完成与存储引擎的交互 - - File System:文件系统,保存配置文件、数据文件、日志文件、错误文件、二进制文件等 -![](https://gitee.com/seazean/images/raw/master/DB/MySQL-体系结构.png) +**** -*** +#### 连接状态 +客户端如果长时间没有操作,连接器就会自动断开,时间是由参数 wait_timeout 控制的,默认值是 8 小时。如果在连接被断开之后,客户端**再次发送请求**的话,就会收到一个错误提醒:`Lost connection to MySQL server during query` +数据库里面,长连接是指连接成功后,如果客户端持续有请求,则一直使用同一个连接;短连接则是指每次执行完很少的几次查询就断开连接,下次查询再重新建立一个 -### 执行流程 +为了减少连接的创建,推荐使用长连接,但是**过多的长连接会造成 OOM**,解决方案: -#### 连接器 +* 定期断开长连接,使用一段时间,或者程序里面判断执行过一个占用内存的大查询后,断开连接,之后要查询再重连 -池化技术:对于访问数据库来说,建立连接的代价是比较昂贵的,频繁的创建关闭连接比较耗费资源,有必要建立数据库连接池,以提高访问的性能 + ```mysql + KILL CONNECTION id + ``` -首先连接到数据库上,这时连接器发挥作用,连接完成后如果没有后续的动作,这个连接就处于空闲状态,通过指令查看连接状态 +* MySQL 5.7 版本,可以在每次执行一个比较大的操作后,通过执行 mysql_reset_connection 来重新初始化连接资源,这个过程不需要重连和重新做权限验证,但是会将连接恢复到刚刚创建完时的状态 SHOW PROCESSLIST:查看当前 MySQL 在进行的线程,可以实时地查看 SQL 的执行情况,其中的 Command 列显示为 Sleep 的这一行,就表示现在系统里面有一个空闲连接 -![](https://gitee.com/seazean/images/raw/master/DB/MySQL-SHOW_PROCESSLIST命令.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-SHOW_PROCESSLIST命令.png) | 参数 | 含义 | | ------- | ------------------------------------------------------------ | @@ -398,16 +241,7 @@ SHOW PROCESSLIST:查看当前 MySQL 在进行的线程,可以实时地查看 | State | 显示使用当前连接的 sql 语句的状态,以查询为例,需要经过 copying to tmp table、sorting result、sending data等状态才可以完成 | | Info | 显示执行的 sql 语句,是判断问题语句的一个重要依据 | -连接建立 TCP 以后需要做**权限验证**,验证成功后可以进行执行 SQL。如果这时管理员账号对这个用户的权限做了修改,也不会影响已经存在连接的权限,只有再新建的连接才会使用新的权限设置 - -客户端如果太长时间没动静,连接器就会自动断开,这个时间是由参数 wait_timeout 控制的,默认值是 8 小时。如果在连接被断开之后,客户端再次发送请求的话,就会收到一个错误提醒:`Lost connection to MySQL server during query` - -数据库里面,长连接是指连接成功后,如果客户端持续有请求,则一直使用同一个连接;短连接则是指每次执行完很少的几次查询就断开连接,下次查询再重新建立一个。为了减少连接的创建,推荐使用长连接,但是过多的长连接会造成 OOM,解决方案: - -* 定期断开长连接,使用一段时间,或者程序里面判断执行过一个占用内存的大查询后,断开连接,之后要查询再重连。 -* MySQL 5.7 版本,可以在每次执行一个比较大的操作后,通过执行 mysql_reset_connection 来重新初始化连接资源,这个过程不需要重连和重新做权限验证,但是会将连接恢复到刚刚创建完时的状态 - - +**Sending data 状态**表示 MySQL 线程开始访问数据行并把结果返回给客户端,而不仅仅只是返回给客户端,是处于执行器过程中的任意阶段。由于在 Sending data 状态下,MySQL 线程需要做大量磁盘读取操作,所以是整个查询中耗时最长的状态 @@ -417,6 +251,8 @@ SHOW PROCESSLIST:查看当前 MySQL 在进行的线程,可以实时地查看 +### 执行流程 + #### 查询缓存 ##### 工作流程 @@ -428,7 +264,7 @@ SHOW PROCESSLIST:查看当前 MySQL 在进行的线程,可以实时地查看 1. 客户端发送一条查询给服务器 2. 服务器先会检查查询缓存,如果命中了缓存,则立即返回存储在缓存中的结果(一般是 K-V 键值对),否则进入下一阶段 3. 分析器进行 SQL 分析,再由优化器生成对应的执行计划 -4. MySQL 根据优化器生成的执行计划,调用存储引擎的 API 来执行查询 +4. 执行器根据优化器生成的执行计划,调用存储引擎的 API 来执行查询 5. 将结果返回给客户端 大多数情况下不建议使用查询缓存,因为查询缓存往往弊大于利 @@ -444,13 +280,13 @@ SHOW PROCESSLIST:查看当前 MySQL 在进行的线程,可以实时地查看 ##### 缓存配置 -1. 查看当前的 MySQL 数据库是否支持查询缓存: +1. 查看当前 MySQL 数据库是否支持查询缓存: ```mysql SHOW VARIABLES LIKE 'have_query_cache'; -- YES ``` -2. 查看当前MySQL是否开启了查询缓存: +2. 查看当前 MySQL 是否开启了查询缓存: ```mysql SHOW VARIABLES LIKE 'query_cache_type'; -- OFF @@ -481,7 +317,7 @@ SHOW PROCESSLIST:查看当前 MySQL 在进行的线程,可以实时地查看 SHOW STATUS LIKE 'Qcache%'; ``` - + | 参数 | 含义 | | ----------------------- | ------------------------------------------------------------ | @@ -515,7 +351,7 @@ SHOW PROCESSLIST:查看当前 MySQL 在进行的线程,可以实时地查看 查询缓存失效的情况: -* SQL 语句不一致的情况,要想命中查询缓存,查询的 SQL 语句必须一致 +* SQL 语句不一致,要想命中查询缓存,查询的 SQL 语句必须一致,因为**缓存中 key 是查询的语句**,value 是查询结构 ```mysql select count(*) from tb_item; @@ -542,9 +378,9 @@ SHOW PROCESSLIST:查看当前 MySQL 在进行的线程,可以实时地查看 SELECT * FROM information_schema.engines; ``` -* 在跨存储引擎的存储过程、触发器或存储函数的主体内执行的查询,缓存失效 +* 在**跨存储引擎**的存储过程、触发器或存储函数的主体内执行的查询,缓存失效 -* 如果表更改,则使用该表的所有高速缓存查询都将变为无效并从高速缓存中删除,包括使用 MERGE 映射到已更改表的表的查询,比如:INSERT、UPDATE、DELETE、ALTER TABLE、DROP TABLE、DROP DATABASE +* 如果表更改,则使用该表的**所有高速缓存查询都将变为无效**并从高速缓存中删除,包括使用 MERGE 映射到已更改表的表的查询,比如:INSERT、UPDATE、DELETE、ALTER TABLE、DROP TABLE、DROP DATABASE @@ -560,8 +396,12 @@ SHOW PROCESSLIST:查看当前 MySQL 在进行的线程,可以实时地查看 SELECT * FROM t WHERE id = 1; ``` -* 先做**词法分析**,输入的是由多个字符串和空格组成的一条 SQL 语句,MySQL 需要识别出里面的字符串分别是什么,代表什么。从输入的 select 这个关键字识别出来这是一个查询语句;把字符串 t 识别成 表名 t,把字符串 id 识别成列 id -* 然后做**语法分析**,根据词法分析的结果,语法分析器会根据语法规则,判断你输入的这个 SQL 语句是否满足 MySQL 语法。如果你的语句不对,就会收到 `You have an error in your SQL syntax` 的错误提醒 +解析器:处理语法和解析查询,生成一课对应的解析树 + +* 先做**词法分析**,输入的是由多个字符串和空格组成的一条 SQL 语句,MySQL 需要识别出里面的字符串分别是什么代表什么。从输入的 select 这个关键字识别出来这是一个查询语句;把字符串 t 识别成 表名 t,把字符串 id 识别成列 id +* 然后做**语法分析**,根据词法分析的结果,语法分析器会根据语法规则,判断你输入的这个 SQL 语句是否满足 MySQL 语法。如果语句不对,就会收到 `You have an error in your SQL syntax` 的错误提醒 + +预处理器:进一步检查解析树的合法性,比如数据表和数据列是否存在、别名是否有歧义等 @@ -571,27 +411,44 @@ SELECT * FROM t WHERE id = 1; #### 优化器 -##### 扫描行数 +##### 成本分析 -优化器是在表里面有多个索引的时候,决定使用哪个索引;或者在一个语句有多表关联(join)的时候,决定各个表的连接顺序。 +优化器是在表里面有多个索引的时候,决定使用哪个索引;或者在一个语句有多表关联(join)的时候,决定各个表的连接顺序 * 根据搜索条件找出所有可能的使用的索引 -* 计算全表扫描的代价 -* 计算使用不同索引执行 SQL 的的代价 +* 成本分析,执行成本由 I/O 成本和 CPU 成本组成,计算全表扫描和使用不同索引执行 SQL 的代价 * 找到一个最优的执行方案,用最小的代价去执行语句 在数据库里面,扫描行数是影响执行代价的因素之一,扫描的行数越少意味着访问磁盘的次数越少,消耗的 CPU 资源越少,优化器还会结合是否使用临时表、是否排序等因素进行综合判断 -MySQL 在真正执行语句之前,并不能精确地知道满足条件的记录有多少条,只能根据统计信息来估算记录,统计信息就是索引的区分度,一个索引上不同的值的个数(比如性别只能是男女,就是 2 ),称之为基数(cardinality),基数越大说明区分度越好 -* 通过**采样统计**来获取基数,InnoDB 默认会选择 N 个数据页,统计这些页面上的不同值得到一个平均值,然后乘以这个索引的页面数,就得到了这个索引的基数 -* 数据表是会持续更新的,索引统计信息也不会固定不变,当变更的数据行数超过 1/ M 的时候,会自动触发重新做一次索引统计 -* 在 MySQL 中,有两种存储索引统计的方式,可以通过设置参数 innodb_stats_persistent 的值来选择: - * 设置为 on 时,表示统计信息会持久化存储,这时默认的 N 是 20,M 是 10 - * 设置为 off 时,表示统计信息只存储在内存,这时默认的 N 是 8,M 是 16 +*** + + + +##### 统计数据 + +MySQL 中保存着两种统计数据: + +* innodb_table_stats 存储了表的统计数据,每一条记录对应着一个表的统计数据 +* innodb_index_stats 存储了索引的统计数据,每一条记录对应着一个索引的一个统计项的数据 + +MySQL 在真正执行语句之前,并不能精确地知道满足条件的记录有多少条,只能根据统计信息来估算记录,统计信息就是索引的区分度,一个索引上不同的值的个数(比如性别只能是男女,就是 2 ),称之为基数(cardinality),**基数越大说明区分度越好** + +通过**采样统计**来获取基数,InnoDB 默认会选择 N 个数据页,统计这些页面上的不同值得到一个平均值,然后乘以这个索引的页面数,就得到了这个索引的基数 -EXPLAIN 执行计划在优化器阶段生成,如果发现 explain 的结果预估的 rows 值跟实际情况差距比较大,可以执行 `analyze table t ` 重新修正信息,只是对表的索引信息做重新统计(不是重建表),没有修改数据,这个过程中加了 MDL 读锁 +在 MySQL 中,有两种存储统计数据的方式,可以通过设置参数 `innodb_stats_persistent` 的值来选择: + +* ON:表示统计信息会持久化存储(默认),采样页数 N 默认为 20,可以通过 `innodb_stats_persistent_sample_pages` 指定,页数越多统计的数据越准确,但消耗的资源更大 +* OFF:表示统计信息只存储在内存,采样页数 N 默认为 8,也可以通过系统变量设置(不推荐,每次重新计算浪费资源) + +数据表是会持续更新的,两种统计信息的更新方式: + +* 设置 `innodb_stats_auto_recalc` 为 1,当发生变动的记录数量超过表大小的 10% 时,自动触发重新计算,不过是**异步进行** +* 调用 `ANALYZE TABLE t` 手动更新统计信息,只对信息做**重新统计**(不是重建表),没有修改数据,这个过程中加了 MDL 读锁并且是同步进行,所以会暂时阻塞系统 + +**EXPLAIN 执行计划在优化器阶段生成**,如果 explain 的结果预估的 rows 值跟实际情况差距比较大,可以执行 analyze 命令重新修正信息 @@ -601,7 +458,7 @@ EXPLAIN 执行计划在优化器阶段生成,如果发现 explain 的结果预 ##### 错选索引 -扫描行数本身是估算数据,或者 SQL 语句中的字段选择有问题时,可能导致 MySQL 没有选择正确的执行索引 +采样统计本身是估算数据,或者 SQL 语句中的字段选择有问题时,可能导致 MySQL 没有选择正确的执行索引 解决方法: @@ -623,99 +480,256 @@ EXPLAIN 执行计划在优化器阶段生成,如果发现 explain 的结果预 #### 执行器 -开始执行的时候,要先判断一下当前连接对表有没有执行查询的权限,如果没有就会返回没有权限的错误,在工程实现上,如果命中查询缓存,会在查询缓存返回结果的时候,做权限验证。如果有权限,就打开表继续执行,执行器就会根据表的引擎定义,去使用这个引擎提供的接口 +开始执行的时候,要先判断一下当前连接对表有没有**执行查询的权限**,如果没有就会返回没有权限的错误,在工程实现上,如果命中查询缓存,会在查询缓存返回结果的时候,做权限验证。如果有权限,就打开表继续执行,执行器就会根据表的引擎定义,去使用这个引擎提供的接口 -**** +*** -### 数据空间 +#### 引擎层 -#### 数据存储 +Server 层和存储引擎层的交互是**以记录为单位的**,存储引擎会将单条记录返回给 Server 层做进一步处理,并不是直接返回所有的记录 -系统表空间是用来放系统信息的,比如数据字典什么的,对应的磁盘文件是 ibdata,数据表空间是一个个的表数据文件,对应的磁盘文件就是表名.ibd +工作流程: -表数据既可以存在共享表空间里,也可以是单独的文件,这个行为是由参数 innodb_file_per_table 控制的: +* 首先根据二级索引选择扫描范围,获取第一条符合二级索引条件的记录,进行回表查询,将聚簇索引的记录返回 Server 层,由 Server 判断记录是否符合要求 +* 然后在二级索引上继续扫描下一个符合条件的记录 -* OFF:表示表的数据放在系统共享表空间,也就是跟数据字典放在一起 -* ON :表示每个 InnoDB 表数据存储在一个以 .ibd 为后缀的文件中(默认) -一个表单独存储为一个文件更容易管理,在不需要这个表时通过 drop table 命令,系统就会直接删除这个文件;如果是放在共享表空间中,即使表删掉了,空间也是不会回收的 +推荐阅读:https://mp.weixin.qq.com/s/YZ-LckObephrP1f15mzHpA -*** -#### 数据删除 +*** -MySQL 的数据删除就是移除掉某个记录后,该位置就被标记为可复用,如果有符合范围条件的数据可以插入到这里。符合范围条件的意思是假设删除记录 R4,之后要再插入一个 ID 在 300 和 600 之间的记录时,就会复用这个位置 - -InnoDB 的数据是按页存储的如果删掉了一个数据页上的所有记录,整个数据页就可以被复用了,如果相邻的两个数据页利用率都很小,系统就会把这两个页上的数据合到其中一个页上,另外一个数据页就被标记为可复用 +### 终止流程 -删除命令其实只是把记录的位置,或者**数据页标记为了可复用,但磁盘文件的大小是不会变的**,这些可以复用还没有被使用的空间,看起来就像是空洞,造成数据库的稀疏,因此需要进行紧凑处理 +#### 终止语句 +终止线程中正在执行的语句: +```mysql +KILL QUERY thread_id +``` -*** +KILL 不是马上终止的意思,而是告诉执行线程这条语句已经不需要继续执行,可以开始执行停止的逻辑(类似于打断)。因为对表做增删改查操作,会在表上加 MDL 读锁,如果线程被 KILL 时就直接终止,那这个 MDL 读锁就没机会被释放了 +命令 `KILL QUERYthread_id_A` 的执行流程: +* 把 session A 的运行状态改成 THD::KILL_QUERY(将变量 killed 赋值为 THD::KILL_QUERY) +* 给 session A 的执行线程发一个信号,让 session A 来处理这个 THD::KILL_QUERY 状态 -#### 空间收缩 +会话处于等待状态(锁阻塞),必须满足是一个可以被唤醒的等待,必须有机会去**判断线程的状态**,如果不满足就会造成 KILL 失败 -重建表就是按照主键 ID 递增的顺序,把数据一行一行地从旧表中读出来再插入到新表中,重建表时 MySQL 会自动完成转存数据、交换表名、删除旧表的操作,重建命令: +典型场景:innodb_thread_concurrency 为 2,代表并发线程上限数设置为 2 -```sql -ALTER TABLE A ENGINE=InnoDB -``` +* session A 执行事务,session B 执行事务,达到线程上限;此时 session C 执行事务会阻塞等待,session D 执行 kill query C 无效 +* C 的逻辑是每 10 毫秒判断是否可以进入 InnoDB 执行,如果不行就调用 nanosleep 函数进入 sleep 状态,没有去判断线程状态 -工作流程:新建临时表 tmp_table B(在 Server 层创建的),把表 A 中的数据导入到表 B 中,操作完成后用表 B 替换表 A,完成重建 +补充:执行 Ctrl+C 的时候,是 MySQL 客户端另外启动一个连接,然后发送一个 KILL QUERY 命令 -重建表的步骤需要 DDL 不是 Online 的,因为在导入数据的过程有新的数据要写入到表 A 的话,就会造成数据丢失 -MySQL 5.6 版本开始引入的 Online DDL,重建表的命令默认执行此步骤: -* 建立一个临时文件 tmp_file(InnoDB 创建),扫描表 A 主键的所有数据页 -* 用数据页中表 A 的记录生成 B+ 树,存储到临时文件中 -* 生成临时文件的过程中,将所有对 A 的操作记录在一个日志文件(row log)中,对应的是图中 state2 的状态 -* 临时文件生成后,将日志文件中的操作应用到临时文件,得到一个逻辑数据上与表 A 相同的数据文件,对应的就是图中 state3 -* 用临时文件替换表 A 的数据文件 +*** - -Online DDL 操作会先获取 MDL 写锁,再退化成 MDL 读锁。但 MDL 写锁持有时间比较短,所以可以称为 Online; 而 MDL 读锁,不阻止数据增删查改,但会阻止其它线程修改表结构 -问题:想要收缩表空间,执行指令后整体占用空间增大 +#### 终止连接 -原因:在重建表后 InnoDB 不会把整张表占满,每个页留了 1/16 给后续的更新使用。表在未整理之前页已经占用 15/16 以上,收缩之后需要保持数据占用空间在 15/16,所以文件占用空间更大才能保持 +断开线程的连接: -注意:临时文件也要占用空间,如果空间不足会重建失败 +```mysql +KILL CONNECTION id +``` +断开连接后执行 SHOW PROCESSLIST 命令,如果这条语句的 Command 列显示 Killed,代表线程的状态是 KILL_CONNECTION,说明这个线程有语句正在执行,当前状态是停止语句执行中,终止逻辑耗时较长 +* 超大事务执行期间被 KILL,这时回滚操作需要对事务执行期间生成的所有新数据版本做回收操作,耗时很长 +* 大查询回滚,如果查询过程中生成了比较大的临时文件,删除临时文件可能需要等待 IO 资源,导致耗时较长 +* DDL 命令执行到最后阶段被 KILL,需要删除中间过程的临时文件,也可能受 IO 资源影响耗时较久 -**** +总结:KILL CONNECTION 本质上只是把客户端的 SQL 连接断开,后面的终止流程还是要走 KILL QUERY +一个事务被 KILL 之后,持续处于回滚状态,不应该强行重启整个 MySQL 进程,应该等待事务自己执行完成,因为重启后依然继续做回滚操作的逻辑 -#### inplace -DDL 中的临时表 tmp_table 是在 Server 层创建的,Online DDL 中的临时文件 tmp_file 是 InnoDB 在内部创建出来的,整个 DDL 过程都在 InnoDB 内部完成,对于 Server 层来说,没有把数据挪动到临时表,是一个原地操作,这就是 inplace -两者的关系: -* DDL 过程如果是 Online 的,就一定是 inplace 的 -* inplace 的 DDL,有可能不是 Online 的,截止到 MySQL 8.0,全文索引(FULLTEXT)和空间索引 (SPATIAL ) 属于这种情况 +*** + + + +### 常用工具 + +#### mysql + +mysql 不是指 mysql 服务,而是指 mysql 的客户端工具 + +```sh +mysql [options] [database] +``` + +* -u --user=name:指定用户名 +* -p --password[=name]:指定密码 +* -h --host=name:指定服务器IP或域名 +* -P --port=#:指定连接端口 +* -e --execute=name:执行SQL语句并退出,在控制台执行SQL语句,而不用连接到数据库执行 + +示例: + +```sh +mysql -h 127.0.0.1 -P 3306 -u root -p +mysql -uroot -p2143 db01 -e "select * from tb_book"; +``` + + + +*** + + + +#### admin + +mysqladmin 是一个执行管理操作的客户端程序,用来检查服务器的配置和当前状态、创建并删除数据库等 + +通过 `mysqladmin --help` 指令查看帮助文档 + +```sh +mysqladmin -uroot -p2143 create 'test01'; +``` + + + +*** + + + +#### binlog + +服务器生成的日志文件以二进制格式保存,如果需要检查这些文本,就要使用 mysqlbinlog 日志管理工具 + +```sh +mysqlbinlog [options] log-files1 log-files2 ... +``` + +* -d --database=name:指定数据库名称,只列出指定的数据库相关操作 + +* -o --offset=#:忽略掉日志中的前 n 行命令。 + +* -r --result-file=name:将输出的文本格式日志输出到指定文件。 + +* -s --short-form:显示简单格式,省略掉一些信息。 + +* --start-datatime=date1 --stop-datetime=date2:指定日期间隔内的所有日志 + +* --start-position=pos1 --stop-position=pos2:指定位置间隔内的所有日志 + + + +*** + + + +#### dump + +##### 命令介绍 + +mysqldump 客户端工具用来备份数据库或在不同数据库之间进行数据迁移,备份内容包含创建表,及插入表的 SQL 语句 + +```sh +mysqldump [options] db_name [tables] +mysqldump [options] --database/-B db1 [db2 db3...] +mysqldump [options] --all-databases/-A +``` + +连接选项: + +* -u --user=name:指定用户名 +* -p --password[=name]:指定密码 +* -h --host=name:指定服务器 IP 或域名 +* -P --port=#:指定连接端口 + +输出内容选项: + +* --add-drop-database:在每个数据库创建语句前加上 Drop database 语句 +* --add-drop-table:在每个表创建语句前加上 Drop table 语句 , 默认开启,不开启 (--skip-add-drop-table) +* -n --no-create-db:不包含数据库的创建语句 +* -t --no-create-info:不包含数据表的创建语句 +* -d --no-data:不包含数据 +* -T, --tab=name:自动生成两个文件:一个 .sql 文件,创建表结构的语句;一个 .txt 文件,数据文件,相当于 select into outfile + +示例: + +```sh +mysqldump -uroot -p2143 db01 tb_book --add-drop-database --add-drop-table > a +mysqldump -uroot -p2143 -T /tmp test city +``` + + + +*** + + + +##### 数据备份 + +命令行方式: + +* 备份命令:mysqldump -u root -p 数据库名称 > 文件保存路径 +* 恢复 + 1. 登录MySQL数据库:`mysql -u root p` + 2. 删除已经备份的数据库 + 3. 重新创建与备份数据库名称相同的数据库 + 4. 使用该数据库 + 5. 导入文件执行:`source 备份文件全路径` + +更多方式参考:https://time.geekbang.org/column/article/81925 + +图形化界面: + +* 备份 + + ![图形化界面备份](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/图形化界面备份.png) + +* 恢复 + + ![图形化界面恢复](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/图形化界面恢复.png) + +*** + + + +#### import + +mysqlimport 是客户端数据导入工具,用来导入mysqldump 加 -T 参数后导出的文本文件 + +```sh +mysqlimport [options] db_name textfile1 [textfile2...] +``` + +示例: + +```sh +mysqlimport -uroot -p2143 test /tmp/city.txt +``` + +导入 sql 文件,可以使用 MySQL 中的 source 指令 : -参考文章:https://time.geekbang.org/column/article/72388 +```mysql +source 文件全路径 +``` @@ -723,6 +737,39 @@ DDL 中的临时表 tmp_table 是在 Server 层创建的,Online DDL 中的临 +#### show + +mysqlshow 客户端对象查找工具,用来很快地查找存在哪些数据库、数据库中的表、表中的列或者索引 + +```sh +mysqlshow [options] [db_name [table_name [col_name]]] +``` + +* --count:显示数据库及表的统计信息(数据库,表 均可以不指定) + +* -i:显示指定数据库或者指定表的状态信息 + +示例: + +```sh +#查询每个数据库的表的数量及表中记录的数量 +mysqlshow -uroot -p1234 --count +#查询test库中每个表中的字段书,及行数 +mysqlshow -uroot -p1234 test --count +#查询test库中book表的详细情况 +mysqlshow -uroot -p1234 test book --count +``` + + + + + + + +**** + + + ## 单表操作 @@ -740,7 +787,7 @@ DDL 中的临时表 tmp_table 是在 Server 层创建的,Online DDL 中的临 - 可使用空格和缩进来增强语句的可读性。 - MySQL 数据库的 SQL 语句不区分大小写,**关键字建议使用大写**。 - 数据库的注释: - - 单行注释:-- 注释内容 #注释内容(mysql特有) + - 单行注释:-- 注释内容 #注释内容(MySQL 特有) - 多行注释:/* 注释内容 */ - SQL 分类 @@ -761,7 +808,7 @@ DDL 中的临时表 tmp_table 是在 Server 层创建的,Online DDL 中的临 - 用来定义数据库的访问权限和安全级别,及创建用户。关键字:grant, revoke等 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-SQL分类.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-SQL分类.png) @@ -941,11 +988,12 @@ DDL 中的临时表 tmp_table 是在 Server 层创建的,Online DDL 中的临 | DOUBLE | 小数类型 | | DATE | 日期,只包含年月日:yyyy-MM-dd | | DATETIME | 日期,包含年月日时分秒:yyyy-MM-dd HH:mm:ss | - | TIMESTAMP | 时间戳类型,包含年月日时分秒:yyyy-MM-dd HH:mm:ss
如果不给这个字段赋值或赋值为null,则默认使用当前的系统时间 | - | VARCHAR | 字符串
name varchar(20):姓名最大20个字符:zhangsan8个字符,张三2个字符 | - + | TIMESTAMP | 时间戳类型,包含年月日时分秒:yyyy-MM-dd HH:mm:ss
如果不给这个字段赋值或赋值为 NULL,则默认使用当前的系统时间 | + | CHAR | 字符串,定长类型 | + | VARCHAR | 字符串,**变长类型**
name varchar(20) 代表姓名最大 20 个字符:zhangsan 8 个字符,张三 2 个字符 | + `INT(n)`:n 代表位数 - + * 3:int(9)显示结果为 000000010 * 3:int(3)显示结果为 010 @@ -1136,7 +1184,7 @@ SELECT DISTINCT FROM JOIN - ON + ON -- 连接查询在多表查询部分详解 WHERE GROUP BY @@ -1195,7 +1243,7 @@ LIMIT SELECT 列名1,列名2,... FROM 表名; ``` -* 去除重复查询:只有值全部重复的才可以去除,需要创建临时表辅助查询 +* **去除重复查询**:只有值全部重复的才可以去除,需要创建临时表辅助查询 ```mysql SELECT DISTINCT 列名1,列名2,... FROM 表名; @@ -1269,8 +1317,8 @@ LIMIT | AND 或 && | 并且 | | OR 或 \|\| | 或者 | | NOT 或 ! | 非,不是 | - | UNION | 对两个结果集进行并集操作,不包括重复行,同时进行默认规则的排序 | - | UNION ALL | 对两个结果集进行并集操作,包括重复行,不进行排序 | + | UNION | 对两个结果集进行**并集操作并进行去重,同时进行默认规则的排序** | + | UNION ALL | 对两个结果集进行并集操作不进行去重,不进行排序 | * 例如: @@ -1307,7 +1355,7 @@ LIMIT SELECT * FROM product WHERE NAME LIKE '%电脑%'; ``` - + @@ -1525,6 +1573,8 @@ SELECT * FROM emp WHERE name REGEXP '[uvw]';-- 匹配包含 uvw 的name值 #### 分组查询 +分组查询会进行去重 + * 分组查询语法 ````mysql @@ -1581,7 +1631,7 @@ SELECT * FROM emp WHERE name REGEXP '[uvw]';-- 匹配包含 uvw 的name值 SELECT * FROM product LIMIT 6,2; -- 第四页 开始索引=(4-1) * 2 ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-DQL分页查询图解.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-DQL分页查询图解.png) @@ -1594,11 +1644,13 @@ SELECT * FROM emp WHERE name REGEXP '[uvw]';-- 匹配包含 uvw 的name值 -## 约束操作 +## 多表操作 ### 约束分类 -约束:对表中的数据进行限定,保证数据的正确性、有效性、完整性! +#### 约束介绍 + +约束:对表中的数据进行限定,保证数据的正确性、有效性、完整性 约束的分类: @@ -1618,7 +1670,7 @@ SELECT * FROM emp WHERE name REGEXP '[uvw]';-- 匹配包含 uvw 的name值 -### 主键约束 +#### 主键约束 * 主键约束特点: @@ -1670,9 +1722,9 @@ SELECT * FROM emp WHERE name REGEXP '[uvw]';-- 匹配包含 uvw 的name值 -### 主键自增 +#### 主键自增 -主键自增约束可以为空,并自动增长。删除某条数据不影响自增的下一个数值,依然按照前一个值自增。 +主键自增约束可以为空,并自动增长。删除某条数据不影响自增的下一个数值,依然按照前一个值自增 * 建表时添加主键自增约束 @@ -1716,7 +1768,7 @@ SELECT * FROM emp WHERE name REGEXP '[uvw]';-- 匹配包含 uvw 的name值 -### 唯一约束 +#### 唯一约束 唯一约束:约束不能有重复的数据 @@ -1748,7 +1800,7 @@ SELECT * FROM emp WHERE name REGEXP '[uvw]';-- 匹配包含 uvw 的name值 -### 非空约束 +#### 非空约束 * 建表时添加非空约束 @@ -1778,9 +1830,9 @@ SELECT * FROM emp WHERE name REGEXP '[uvw]';-- 匹配包含 uvw 的name值 -### 外键约束 +#### 外键约束 - 外键约束:让表和表之间产生关系,从而保证数据的准确性! + 外键约束:让表和表之间产生关系,从而保证数据的准确性 * 建表时添加外键约束 @@ -1833,9 +1885,14 @@ SELECT * FROM emp WHERE name REGEXP '[uvw]';-- 匹配包含 uvw 的name值 DELETE FROM USER WHERE NAME='王五'; ``` - -### 外键级联 + + +*** + + + +#### 外键级联 级联操作:当把主表中的数据进行删除或更新时,从表中有关联的数据的相应操作,包括 RESTRICT、CASCADE、SET NULL 和 NO ACTION @@ -1875,16 +1932,12 @@ SELECT * FROM emp WHERE name REGEXP '[uvw]';-- 匹配包含 uvw 的name值 -## 多表操作 - ### 多表设计 #### 一对一 多表:有多张数据表,而表与表之间有一定的关联关系,通过外键约束实现,分为一对一、一对多、多对多三类 - - 举例:人和身份证 实现原则:在任意一个表建立外键,去关联另外一个表的主键 @@ -1909,7 +1962,11 @@ CREATE TABLE card( INSERT INTO card VALUES (NULL,'12345',1),(NULL,'56789',2); ``` -![](https://gitee.com/seazean/images/raw/master/DB/多表设计一对一.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/多表设计一对一.png) + + + +*** @@ -1939,7 +1996,11 @@ CREATE TABLE orderlist( INSERT INTO orderlist VALUES (NULL,'hm001',1),(NULL,'hm002',1),(NULL,'hm003',2),(NULL,'hm004',2); ``` -![多表设计一对多](https://gitee.com/seazean/images/raw/master/DB/多表设计一对多.png) +![多表设计一对多](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/多表设计一对多.png) + + + +*** @@ -1978,7 +2039,7 @@ CREATE TABLE stu_course( INSERT INTO stu_course VALUES (NULL,1,1),(NULL,1,2),(NULL,2,1),(NULL,2,2); ``` -![](https://gitee.com/seazean/images/raw/master/DB/多表设计多对多.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/多表设计多对多.png) @@ -1986,50 +2047,30 @@ INSERT INTO stu_course VALUES (NULL,1,1),(NULL,1,2),(NULL,2,1),(NULL,2,2); -### 多表查询 - -#### 查询格式 - -多表查询分类: - -* 内连接查询 -* 外连接查询 -* 子查询 -* 自关联查询 - -多表查询格式:(笛卡儿积) - -```mysql -SELECT - 列名列表 -FROM - 表名列表 -WHERE - 条件... -``` - - - -*** +### 连接查询 +#### 内外连接 +##### 内连接 -#### 内连接 +连接查询的是两张表有交集的部分数据,两张表分为**驱动表和被驱动表**,如果结果集中的每条记录都是两个表相互匹配的组合,则称这样的结果集为笛卡尔积 -查询原理:内连接查询的是两张表有交集的部分数据,分为驱动表和被驱动表,首先查询驱动表得到结果集,然后根据结果集中的每一条记录都分别到被驱动表中查找匹配 +内连接查询,若驱动表中的记录在被驱动表中找不到匹配的记录时,则该记录不会加到最后的结果集 -* 显式内连接 +* 显式内连接: ```mysql SELECT 列名 FROM 表名1 [INNER] JOIN 表名2 ON 条件; ``` -* 隐式内连接 +* 隐式内连接:内连接中 WHERE 子句和 ON 子句是等价的 ```mysql SELECT 列名 FROM 表名1,表名2 WHERE 条件; ``` +STRAIGHT_JOIN与 JOIN 类似,只不过左表始终在右表之前读取,只适用于内连接 + @@ -2037,21 +2078,25 @@ WHERE -#### 外连接 +##### 外连接 + +外连接查询,若驱动表中的记录在被驱动表中找不到匹配的记录时,则该记录也会加到最后的结果集,只是对于被驱动表中**不匹配过滤条件**的记录,各个字段使用 NULL 填充 -* 左外连接:查询左表的全部数据,和左右两张表有交集部分的数据 +应用实例:查学生成绩,也想展示出缺考的人的成绩 + +* 左外连接:选择左侧的表为驱动表,查询左表的全部数据,和左右两张表有交集部分的数据 ```mysql SELECT 列名 FROM 表名1 LEFT [OUTER] JOIN 表名2 ON 条件; ``` -* 右外连接:查询右表的全部数据,和左右两张表有交集部分的数据 +* 右外连接:选择右侧的表为驱动表,查询右表的全部数据,和左右两张表有交集部分的数据 ```mysql SELECT 列名 FROM 表名1 RIGHT [OUTER] JOIN 表名2 ON 条件; ``` -![](https://gitee.com/seazean/images/raw/master/DB/MySQL-JOIN查询图.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-JOIN查询图.png) @@ -2060,72 +2105,28 @@ WHERE -#### 子查询 -子查询概念:查询语句中嵌套了查询语句,**将嵌套查询称为子查询** -* 结果是单行单列:可以将查询的结果作为另一条语句的查询条件,使用运算符判断 +#### 关联查询 - ```mysql - SELECT 列名 FROM 表名 WHERE 列名=(SELECT 列名/聚合函数(列名) FROM 表名 [WHERE 条件]); - ``` +自关联查询:同一张表中有数据关联,可以多次查询这同一个表 -* 结果是多行单列:可以作为条件,使用运算符in或not in进行判断 +* 数据准备 ```mysql - SELECT 列名 FROM 表名 WHERE 列名 [NOT] IN (SELECT 列名 FROM 表名 [WHERE 条件]); + -- 创建员工表 + CREATE TABLE employee( + id INT PRIMARY KEY AUTO_INCREMENT, -- 员工编号 + NAME VARCHAR(20), -- 员工姓名 + mgr INT, -- 上级编号 + salary DOUBLE -- 员工工资 + ); + -- 添加数据 + INSERT INTO employee VALUES (1001,'孙悟空',1005,9000.00),..,(1009,'宋江',NULL,16000.00); ``` - -* 结果是多行多列:查询的结果可以作为一张虚拟表参与查询 - - ```mysql - SELECT 列名 FROM 表名 [别名],(SELECT 列名 FROM 表名 [WHERE 条件]) [别名] [WHERE 条件]; - -- 查询订单表orderlist中id大于4的订单信息和所属用户USER信息 - SELECT - * - FROM - USER u, - (SELECT * FROM orderlist WHERE id>4) o - WHERE - u.id=o.uid; - ``` - - - - -*** - - - -#### 自关联 - -自关联查询:同一张表中有数据关联,可以多次查询这同一个表 - -* 数据准备 - - ```mysql - -- 创建员工表 - CREATE TABLE employee( - id INT PRIMARY KEY AUTO_INCREMENT, -- 员工编号 - NAME VARCHAR(20), -- 员工姓名 - mgr INT, -- 上级编号 - salary DOUBLE -- 员工工资 - ); - -- 添加数据 - INSERT INTO employee VALUES (1001,'孙悟空',1005,9000.00), - (1002,'猪八戒',1005,8000.00), - (1003,'沙和尚',1005,8500.00), - (1004,'小白龙',1005,7900.00), - (1005,'唐僧',NULL,15000.00), - (1006,'武松',1009,7600.00), - (1007,'李逵',1009,7400.00), - (1008,'林冲',1009,8100.00), - (1009,'宋江',NULL,16000.00); - ``` - - ![](https://gitee.com/seazean/images/raw/master/DB/自关联查询数据准备.png) - + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/自关联查询数据准备.png) + * 数据查询 ```mysql @@ -2165,15 +2166,208 @@ WHERE 1009 宋江 NULL NULL NULL ``` + + +*** + + + +#### 连接原理 + +Index Nested-Loop Join 算法:查询驱动表得到**数据集**,然后根据数据集中的每一条记录的**关联字段再分别**到被驱动表中查找匹配(**走索引**),所以驱动表只需要访问一次,被驱动表要访问多次 + +MySQL 将查询驱动表后得到的记录成为驱动表的扇出,连接查询的成本:单次访问驱动表的成本 + 扇出值 * 单次访问被驱动表的成本,优化器会选择成本最小的表连接顺序(确定谁是驱动表,谁是被驱动表)生成执行计划,进行连接查询,优化方式: + +* 减少驱动表的扇出(让数据量小的表来做驱动表) +* 降低访问被驱动表的成本 + +说明:STRAIGHT_JOIN 是查一条驱动表,然后根据关联字段去查被驱动表,要访问多次驱动表,所以需要优化为 INL 算法 + +Block Nested-Loop Join 算法:一种**空间换时间**的优化方式,基于块的循环连接,执行连接查询前申请一块固定大小的内存作为连接缓冲区 Join Buffer,先把若干条驱动表中的扇出暂存在缓冲区,每一条被驱动表中的记录一次性的与 Buffer 中多条记录进行匹配(扫描全部数据,一条一条的匹配),因为是在内存中完成,所以速度快,并且降低了 I/O 成本 + +Join Buffer 可以通过参数 `join_buffer_size` 进行配置,默认大小是 256 KB + +在成本分析时,对于很多张表的连接查询,连接顺序有非常多,MySQL 如果挨着进行遍历计算成本,会消耗很多资源 + +* 提前结束某种连接顺序的成本评估:维护一个全局变量记录当前成本最小的连接方式,如果一种顺序只计算了一部分就已经超过了最小成本,可以提前结束计算 +* 系统变量 optimizer_search_depth:如果连接表的个数小于该变量,就继续穷举分析每一种连接数量,反之只对数量与 depth 值相同的表进行分析,该值越大成本分析的越精确 + +* 系统变量 optimizer_prune_level:控制启发式规则的启用,这些规则就是根据以往经验指定的,不满足规则的连接顺序不分析成本 + + + +*** + + + +#### 连接优化 + +##### BKA + +Batched Key Access 算法是对 NLJ 算法的优化,在读取被驱动表的记录时使用顺序 IO,Extra 信息中会有 Batched Key Access 信息 + +使用 BKA 的表的 JOIN 过程如下: + +* 连接驱动表将满足条件的记录放入 Join Buffer,并将两表连接的字段放入一个 DYNAMIC_ARRAY ranges 中 +* 在进行表的过接过程中,会将 ranges 相关的信息传入 Buffer 中,进行被驱动表主建的查找及排序操作 +* 调用步骤 2 中产生的有序主建,**顺序读取被驱动表的数据** +* 当缓冲区的数据被读完后,会重复进行步骤 2、3,直到记录被读取完 + +使用 BKA 优化需要设进行设置: + +```mysql +SET optimizer_switch='mrr=on,mrr_cost_based=off,batched_key_access=on'; +``` + +说明:前两个参数的作用是启用 MRR,因为 BKA 算法的优化要依赖于 MRR(系统优化 → 内存优化 → Read 详解) + + + +*** + + + +##### BNL + +###### 问题 + +BNL 即 Block Nested-Loop Join 算法,由于要访问多次被驱动表,会产生两个问题: + +* Join 语句多次扫描一个冷表,并且语句执行时间小于 1 秒,就会在再次扫描冷表时,把冷表的数据页移到 LRU 链表头部,导致热数据被淘汰,影响业务的正常运行 + + 这种情况冷表的数据量要小于整个 Buffer Pool 的 old 区域,能够完全放入 old 区,才会再次被读时加到 young,否则读取下一段时就已经把上一段淘汰 + +* Join 语句在循环读磁盘和淘汰内存页,进入 old 区域的数据页很可能在 1 秒之内就被淘汰,就会导致 MySQL 实例的 Buffer Pool 在这段时间内 young 区域的数据页没有被合理地淘汰 + +大表 Join 操作虽然对 IO 有影响,但是在语句执行结束后对 IO 的影响随之结束。但是对 Buffer Pool 的影响就是持续性的,需要依靠后续的查询请求慢慢恢复内存命中率 + + + +###### 优化 + +将 BNL 算法转成 BKA 算法,优化方向: + +* 在被驱动表上建索引,这样就可以根据索引进行顺序 IO +* 使用临时表,**在临时表上建立索引**,将被驱动表和临时表进行连接查询 + +驱动表 t1,被驱动表 t2,使用临时表的工作流程: + +* 把表 t1 中满足条件的数据放在临时表 tmp_t 中 +* 给临时表 tmp_t 的关联字段加上索引,使用 BKA 算法 +* 让表 t2 和 tmp_t 做 Join 操作(临时表是被驱动表) + +补充:MySQL 8.0 支持 hash join,join_buffer 维护的不再是一个无序数组,而是一个哈希表,查询效率更高,执行效率比临时表更高 + + + + + + +*** + + + +### 嵌套查询 + +#### 查询分类 + +查询语句中嵌套了查询语句,**将嵌套查询称为子查询**,FROM 子句后面的子查询的结果集称为派生表 + +根据结果分类: + +* 结果是单行单列:可以将查询的结果作为另一条语句的查询条件,使用运算符判断 + + ```mysql + SELECT 列名 FROM 表名 WHERE 列名=(SELECT 列名/聚合函数(列名) FROM 表名 [WHERE 条件]); + ``` + +* 结果是多行单列:可以作为条件,使用运算符 IN 或 NOT IN 进行判断 + + ```mysql + SELECT 列名 FROM 表名 WHERE 列名 [NOT] IN (SELECT 列名 FROM 表名 [WHERE 条件]); + ``` + +* 结果是多行多列:查询的结果可以作为一张虚拟表参与查询 + + ```mysql + SELECT 列名 FROM 表名 [别名],(SELECT 列名 FROM 表名 [WHERE 条件]) [别名] [WHERE 条件]; + -- 查询订单表orderlist中id大于4的订单信息和所属用户USER信息 + SELECT + * + FROM + USER u, + (SELECT * FROM orderlist WHERE id>4) o + WHERE + u.id=o.uid; + ``` + +相关性分类: + +* 不相关子查询:子查询不依赖外层查询的值,可以单独运行出结果 +* 相关子查询:子查询的执行需要依赖外层查询的值 + + + +**** + + + +#### 查询优化 + +不相关子查询的结果集会被写入一个临时表,并且在写入时**去重**,该过程称为**物化**,存储结果集的临时表称为物化表 + +系统变量 tmp_table_size 或者 max_heap_table_size 为表的最值 + +* 小于系统变量时,内存中可以保存,会为建立**基于内存**的 MEMORY 存储引擎的临时表,并建立哈希索引 +* 大于任意一个系统变量时,物化表会使用**基于磁盘**的 InnoDB 存储引擎来保存结果集中的记录,索引类型为 B+ 树 + +物化后,嵌套查询就相当于外层查询的表和物化表进行内连接查询,然后经过优化器选择成本最小的表连接顺序执行查询 + +子查询物化会产生建立临时表的成本,但是将子查询转化为连接查询可以充分发挥优化器的作用,所以引入:半连接 + +* t1 和 t2 表进行半连接,对于 t1 表中的某条记录,只需要关心在 t2 表中是否存在,而不需要关心有多少条记录与之匹配,最终结果集只保留 t1 的记录 +* 半连接只是执行子查询的一种方式,MySQL 并没有提供面向用户的半连接语法 + + + +参考书籍:https://book.douban.com/subject/35231266/ + + *** -### 多表练习 +#### 联合查询 + +UNION 是取这两个子查询结果的并集,并进行去重,同时进行默认规则的排序(union 是行加起来,join 是列加起来) + +UNION ALL 是对两个结果集进行并集操作不进行去重,不进行排序 + +```mysql +(select 1000 as f) union (select id from t1 order by id desc limit 2); #t1表中包含id 为 1-1000 的数据 +``` + +语句的执行流程: + +* 创建一个内存临时表,这个临时表只有一个整型字段 f,并且 f 是主键字段 +* 执行第一个子查询,得到 1000 这个值,并存入临时表中 +* 执行第二个子查询,拿到第一行 id=1000,试图插入临时表中,但由于 1000 这个值已经存在于临时表了,违反了唯一性约束,所以插入失败,然后继续执行 +* 取到第二行 id=999,插入临时表成功 +* 从临时表中按行取出数据,返回结果并删除临时表,结果中包含两行数据分别是 1000 和 999 + + -#### 数据准备 + + +**** + + + +### 查询练习 + +数据准备: ```mysql -- 创建db4数据库 @@ -2220,17 +2414,18 @@ CREATE TABLE us_pro( ); ``` -![多表练习架构设计](https://gitee.com/seazean/images/raw/master/DB/多表练习架构设计.png) +![多表练习架构设计](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/多表练习架构设计.png) -#### 数据查询 - -1. 查询用户的编号、姓名、年龄、订单编号。 - 分析: - 数据:用户的编号、姓名、年龄在user表,订单编号在orderlist表 - 条件:user.id = orderlist.uid +**数据查询:** +1. 查询用户的编号、姓名、年龄、订单编号 + + 数据:用户的编号、姓名、年龄在 user 表,订单编号在 orderlist 表 + + 条件:user.id = orderlist.uid + ```mysql SELECT u.*, @@ -2241,7 +2436,7 @@ CREATE TABLE us_pro( WHERE u.id = o.uid; ``` - + 2. 查询所有的用户,显示用户的编号、姓名、年龄、订单编号。 ```mysql @@ -2256,7 +2451,7 @@ CREATE TABLE us_pro( u.id = o.uid; ``` -3. 查询用户年龄大于 23 岁的信息,显示用户的编号、姓名、年龄、订单编号。 +3. 查询用户年龄大于 23 岁的信息,显示用户的编号、姓名、年龄、订单编号 ```mysql SELECT @@ -2297,8 +2492,10 @@ CREATE TABLE us_pro( u.name IN ('张三','李四'); ```` -5. 查询所有的用户和该用户能查看的所有的商品,显示用户的编号、姓名、年龄、商品名称。 - 数据:用户的编号、姓名、年龄在user表,商品名称在product表,中间表 us_pro +5. 查询所有的用户和该用户能查看的所有的商品,显示用户的编号、姓名、年龄、商品名称 + + 数据:用户的编号、姓名、年龄在 user 表,商品名称在 product 表,中间表 us_pro + 条件:us_pro.uid = user.id AND us_pro.pid = product.id ```mysql @@ -2343,190 +2540,198 @@ CREATE TABLE us_pro( -**** - - - - +*** -## 事务机制 -### 事务介绍 -事务(Transaction)是访问和更新数据库的程序执行单元;事务中可能包含一个或多个 sql 语句,这些语句要么都执行,要么都不执行。作为一个关系型数据库,MySQL 支持事务。 -单元中的每条 SQL 语句都相互依赖,形成一个整体 -* 如果某条 SQL 语句执行失败或者出现错误,那么整个单元就会回滚,撤回到事务最初的状态 +## 高级结构 -* 如果单元中所有的 SQL 语句都执行成功,则事务就顺利执行 +### 视图 +#### 基本介绍 +视图概念:视图是一种虚拟存在的数据表,这个虚拟的表并不在数据库中实际存在 -*** +本质:将一条 SELECT 查询语句的结果封装到了一个虚拟表中,所以在创建视图的时候,工作重心要放在这条 SELECT 查询语句上 +作用:将一些比较复杂的查询语句的结果,封装到一个虚拟表中,再有相同查询需求时,直接查询该虚拟表 +优点: -### 管理事务 +* 简单:使用视图的用户不需要关心表的结构、关联条件和筛选条件,因为虚拟表中已经是过滤好的结果集 +* 安全:使用视图的用户只能访问查询的结果集,对表的权限管理并不能限制到某个行某个列 -管理事务的三个步骤 +* 数据独立,一旦视图的结构确定,可以屏蔽表结构变化对用户的影响,源表增加列对视图没有影响;源表修改列名,则可以通过修改视图来解决,不会造成对访问者的影响 -1. 开启事务:记录回滚点,并通知服务器,将要执行一组操作,要么同时成功、要么同时失败 -2. 执行 SQL 语句:执行具体的一条或多条 SQL 语句 -3. 结束事务(提交|回滚) +*** - - 提交:没出现问题,数据进行更新 - - 回滚:出现问题,数据恢复到开启事务时的状态 -事务操作: +#### 视图创建 -* 开启事务 +* 创建视图 ```mysql - START TRANSACTION; + CREATE [OR REPLACE] + VIEW 视图名称 [(列名列表)] + AS 查询语句 + [WITH [CASCADED | LOCAL] CHECK OPTION]; ``` -* 回滚事务 + `WITH [CASCADED | LOCAL] CHECK OPTION` 决定了是否允许更新数据使记录不再满足视图的条件: - ```mysql - ROLLBACK; - ``` + * LOCAL:只要满足本视图的条件就可以更新 + * CASCADED:必须满足所有针对该视图的所有视图的条件才可以更新, 默认值 -* 提交事务,显示执行是手动提交,MySQL 默认为自动提交 +* 例如 ```mysql - COMMIT; + -- 数据准备 city + id NAME cid + 1 深圳 1 + 2 上海 1 + 3 纽约 2 + 4 莫斯科 3 + + -- 数据准备 country + id NAME + 1 中国 + 2 美国 + 3 俄罗斯 + + -- 创建city_country视图,保存城市和国家的信息(使用指定列名) + CREATE + VIEW + city_country (city_id,city_name,country_name) + AS + SELECT + c1.id, + c1.name, + c2.name + FROM + city c1, + country c2 + WHERE + c1.cid=c2.id; ``` - 工作原理: + - * 自动提交模式下,如果没有 start transaction 显式地开始一个事务,那么**每个 SQL 语句都会被当做一个事务执行提交操作** - * 手动提交模式下,所有的 SQL 语句都在一个事务中,直到执行了 commit 或 rollback,该事务结束的同时开启另外一个事务 +*** - * 存在一些特殊的命令,在事务中执行了这些命令会马上强制执行 COMMIT 提交事务,如 DDL 语句 (create/drop/alter/table)、lock tables 语句等 - 提交方式语法: - - 查看事务提交方式 +#### 视图查询 - ```mysql - SELECT @@AUTOCOMMIT; -- 1 代表自动提交 0 代表手动提交 - ``` +* 查询所有数据表,视图也会查询出来 - - 修改事务提交方式 + ```mysql + SHOW TABLES; + SHOW TABLE STATUS [\G]; + ``` - ```mysql - SET @@AUTOCOMMIT=数字; -- 系统 - SET AUTOCOMMIT=数字; -- 会话 - ``` - - - 系统变量的操作: - - ```sql - SET [GLOBAL|SESSION] 变量名 = 值; -- 默认是会话 - SET @@[(GLOBAL|SESSION).]变量名 = 值; -- 默认是系统 - ``` - - ```sql - SHOW [GLOBAL|SESSION] VARIABLES [LIKE '变量%']; -- 默认查看会话内系统变量值 - ``` +* 查询视图 - + ```mysql + SELECT * FROM 视图名称; + ``` -* 管理实务演示 +* 查询某个视图创建 ```mysql - -- 开启事务 - START TRANSACTION; - - -- 张三给李四转账500元 - -- 1.张三账户-500 - UPDATE account SET money=money-500 WHERE NAME='张三'; - -- 2.李四账户+500 - UPDATE account SET money=money+500 WHERE NAME='李四'; - - -- 回滚事务(出现问题) - ROLLBACK; - - -- 提交事务(没出现问题) - COMMIT; + SHOW CREATE VIEW 视图名称; ``` - - *** -### 四大特征 - -#### ACID - -事务的四大特征:ACID - -- 原子性 (atomicity) -- 一致性 (consistency) -- 隔离性 (isolaction) -- 持久性 (durability) +#### 视图修改 +视图表数据修改,会**自动修改源表中的数据**,因为更新的是视图中的基表中的数据 +* 修改视图表中的数据 -*** + ```mysql + UPDATE 视图名称 SET 列名 = 值 WHERE 条件; + ``` +* 修改视图的结构 + ```mysql + ALTER [ALGORITHM = {UNDEFINED | MERGE | TEMPTABLE}] + VIEW 视图名称 [(列名列表)] + AS 查询语句 + [WITH [CASCADED | LOCAL] CHECK OPTION] + + -- 将视图中的country_name修改为name + ALTER + VIEW + city_country (city_id,city_name,name) + AS + SELECT + c1.id, + c1.name, + c2.name + FROM + city c1, + country c2 + WHERE + c1.cid=c2.id; + ``` -#### 原子性 -原子性是指事务是一个不可分割的工作单位,事务的操作如果成功就必须要完全应用到数据库,失败则不能对数据库有任何影响。比如事务中一个 SQL 语句执行失败,则已执行的语句也必须回滚,数据库退回到事务前的状态 -InnoDB 存储引擎提供了两种事务日志:redo log(重做日志)和 undo log(回滚日志) +*** -* redo log 用于保证事务持久性 -* undo log 用于保证事务原子性和隔离性 -undo log 属于逻辑日志,根据每行操作进行记录,记录了 SQL 执行相关的信息,用来回滚行记录到某个版本 -当事务对数据库进行修改时,InnoDB 会先记录对应的 undo log,如果事务执行失败或调用了 rollback 导致事务回滚,InnoDB 会根据 undo log 的内容**做与之前相反的操作**: +#### 视图删除 -* 对于每个 insert,回滚时会执行 delete +* 删除视图 -* 对于每个 delete,回滚时会执行 insert + ```mysql + DROP VIEW 视图名称; + ``` -* 对于每个 update,回滚时会执行一个相反的 update,把数据修改回去 +* 如果存在则删除 -undo log 是采用段(segment)的方式来记录,每个 undo 操作在记录的时候占用一个 undo log segment + ```mysql + DROP VIEW IF EXISTS 视图名称; + ``` -rollback segment 称为回滚段,每个回滚段中有 1024 个 undo log segment -* 在以前老版本,只支持 1 个 rollback segment,只能记录 1024 个 undo log segment -* MySQL5.5 开始支持 128 个 rollback segment,支持 128*1024 个 undo 操作 -参考文章:https://www.cnblogs.com/kismetv/p/10331633.html +*** -*** +### 存储过程 +#### 基本介绍 -#### 一致性 +存储过程和函数:存储过程和函数是事先经过编译并存储在数据库中的一段 SQL 语句的集合 -一致性是指事务执行前后,数据库的完整性约束没有被破坏,事务执行的前后都是合法的数据状态。 +存储过程和函数的好处: -数据库的完整性约束包括但不限于:实体完整性(如行的主键存在且唯一)、列完整性(如字段的类型、大小、长度要符合要求)、外键约束、用户自定义完整性(如转账前后,两个账户余额的和应该不变) +* 提高代码的复用性 +* 减少数据在数据库和应用服务器之间的传输,提高传输效率 +* 减少代码层面的业务处理 +* **一次编译永久有效** -实现一致性的措施: +存储过程和函数的区别: -- 保证原子性、持久性和隔离性,如果这些特性无法保证,事务的一致性也无法保证 -- 数据库本身提供保障,例如不允许向整形列插入字符串值、字符串长度不能超过列的限制等 -- 应用层面进行保障,例如如果转账操作只扣除转账者的余额,而没有增加接收者的余额,无论数据库实现的多么完美,也无法保证状态的一致 +* 存储函数必须有返回值 +* 存储过程可以没有返回值 @@ -2534,80 +2739,138 @@ rollback segment 称为回滚段,每个回滚段中有 1024 个 undo log segme -#### 隔离性 - -隔离性是指,事务内部的操作与其他事务是隔离的,多个并发事务之间要相互隔离,不能互相干扰 +#### 基本操作 -* 严格的隔离性,对应了事务隔离级别中的 serializable,实际应用中对性能考虑很少使用可串行化 +DELIMITER: -* 与原子性、持久性侧重于研究事务本身不同,隔离性研究的是**不同事务**之间的相互影响 +* DELIMITER 关键字用来声明 sql 语句的分隔符,告诉 MySQL 该段命令已经结束 -隔离性让并发情形下的事务之间互不干扰: +* MySQL 语句默认的分隔符是分号,但是有时需要一条功能 sql 语句中包含分号,但是并不作为结束标识,这时使用 DELIMITER 来指定分隔符: -- 一个事务的写操作对另一个事务的写操作(写写):锁机制保证隔离性 -- 一个事务的写操作对另一个事务的读操作(读写):MVCC 保证隔离性 + ```mysql + DELIMITER 分隔符 + ``` -锁机制:事务在修改数据之前,需要先获得相应的锁,获得锁之后,事务便可以修改数据;该事务操作期间,这部分数据是锁定的,其他事务如果需要修改数据,需要等待当前事务提交或回滚后释放锁(详解见锁机制) +存储过程的创建调用查看和删除: +* 创建存储过程 + ```mysql + -- 修改分隔符为$ + DELIMITER $ + + -- 标准语法 + CREATE PROCEDURE 存储过程名称(参数...) + BEGIN + sql语句; + END$ + + -- 修改分隔符为分号 + DELIMITER ; + ``` -*** +* 调用存储过程 + ```mysql + CALL 存储过程名称(实际参数); + ``` +* 查看存储过程 -#### 持久性 + ```mysql + SELECT * FROM mysql.proc WHERE db='数据库名称'; + ``` -##### 实现原理 +* 删除存储过程 -持久性是指一个事务一旦被提交了,那么对数据库中数据的改变就是永久性的,接下来的其他操作或故障不应该对其有任何影响。 + ```mysql + DROP PROCEDURE [IF EXISTS] 存储过程名称; + ``` -Buffer Pool 是一片内存空间,可以通过 innodb_buffer_pool_size 来控制 Buffer Pool 的大小(内存优化部分会详解参数) +练习: -* Change Buffer 是 Buffer Pool 里的内存,不能无限增大,用来对增删改操作提供缓存 -* Change Buffer 的大小可以通过参数 innodb_change_buffer_max_size 来动态设置,设置为 50 时表示 Change Buffer 的大小最多只能占用 Buffer Pool 的 50% -* 补充知识:**唯一索引的更新不能使用 Buffer**,一般只有普通索引可以使用,直接写入 Buffer 就结束 +* 数据准备 -InnoDB 的数据是按数据页为单位来读写,每个数据页的大小默认是 16KB。数据是存放在磁盘中,每次读写数据都需要磁盘 IO,效率会很低。InnoDB 提供了缓存 Change Buffer,Buffer 中包含了磁盘中部分数据页的映射,作为访问数据库的缓冲: + ```mysql + id NAME age gender score + 1 张三 23 男 95 + 2 李四 24 男 98 + 3 王五 25 女 100 + 4 赵六 26 女 90 + ``` -* 从数据库读取数据时,会首先从缓存中读取,如果缓存中没有,则从磁盘读取后放入 Buffer Pool -* 向数据库写入数据时,会首先写入缓存,缓存中修改的数据会**定期刷新**到磁盘,这一过程称为刷脏 +* 创建 stu_group() 存储过程,封装分组查询总成绩,并按照总成绩升序排序的功能 + ```mysql + DELIMITER $ + + CREATE PROCEDURE stu_group() + BEGIN + SELECT gender,SUM(score) getSum FROM student GROUP BY gender ORDER BY getSum ASC; + END$ + + DELIMITER ; + + -- 调用存储过程 + CALL stu_group(); + -- 删除存储过程 + DROP PROCEDURE IF EXISTS stu_group; + ``` + *** -##### 数据恢复 - -Buffer Pool 的使用提高了读写数据的效率,但是也带了新的问题:如果 MySQL 宕机,此时 Buffer Pool 中修改的数据还没有刷新到磁盘,就会导致数据的丢失,事务的持久性无法保证,所以引入 redo log - -* 当数据修改时,先修改 Change Buffer 中的数据,然后在 redo log buffer 记录这次操作 -* 如果 MySQL 宕机,InnoDB 判断一个数据页在崩溃恢复时丢失了更新,就会将它读到内存,然后根据 redo log 内容更新内存,更新完成后,内存页变成脏页,然后进行刷脏(buffer pool 的任务) +#### 存储语法 -redo log **记录数据页的物理修改**,而不是某一行或某几行的修改,用来恢复提交后的物理数据页,且只能恢复到最后一次提交的位置 +##### 变量使用 -redo log 采用的是 WAL(Write-ahead logging,**预写式日志**),所有修改要先写入日志,再更新到磁盘,保证了数据不会因 MySQL 宕机而丢失,从而满足了持久性要求 +存储过程是可以进行编程的,意味着可以使用变量、表达式、条件控制语句等,来完成比较复杂的功能 -redo log 也需要在事务提交时将日志写入磁盘,但是比将内存中的 Buffer Pool 修改的数据写入磁盘的速度快: +* 定义变量:DECLARE 定义的是局部变量,只能用在 BEGIN END 范围之内 + + ```mysql + DECLARE 变量名 数据类型 [DEFAULT 默认值]; + ``` + +* 变量的赋值 -* 刷脏是随机 IO,因为每次修改的数据位置随机,但写 redo log 是尾部追加操作,属于顺序 IO -* 刷脏是以数据页(Page)为单位的,一个页上的一个小修改都要整页写入,而 redo log 中只包含真正需要写入的部分,减少无效 IO + ```mysql + SET 变量名 = 变量值; + SELECT 列名 INTO 变量名 FROM 表名 [WHERE 条件]; + ``` -InnoDB 引擎会在适当的时候,把内存中 redo log buffer 持久化到磁盘,具体的刷盘策略: +* 数据准备:表 student -* 通过修改参数 `innodb_flush_log_at_trx_commit` 设置: - * 0:表示当提交事务时,并不将缓冲区的 redo 日志写入磁盘,而是等待主线程每秒刷新一次 - * 1:在事务提交时将缓冲区的 redo 日志**同步写入**到磁盘,保证一定会写入成功 - * 2:在事务提交时将缓冲区的 redo 日志异步写入到磁盘,不能保证提交时肯定会写入,只是有这个动作 -* 如果写入 redo log buffer 的日志已经占据了 redo log buffer 总容量的一半了,此时就会刷入到磁盘文件,这时会影响执行效率,所以开发中应该**避免大事务** + ```mysql + id NAME age gender score + 1 张三 23 男 95 + 2 李四 24 男 98 + 3 王五 25 女 100 + 4 赵六 26 女 90 + ``` -刷脏策略: +* 定义两个 int 变量,用于存储男女同学的总分数 -* redo log 文件是固定大小的,如果写满了就要擦除以前的记录,在擦除之前需要把旧记录更新到磁盘中的数据文件中 -* Buffer Pool 内存不足,需要淘汰部分数据页,如果淘汰的是脏页,就要先将脏页写到磁盘(大事务) -* 系统空闲时,后台线程会自动进行刷脏 -* MySQL 正常关闭时,会把内存的脏页都 flush 到磁盘上 + ```mysql + DELIMITER $ + CREATE PROCEDURE pro_test3() + BEGIN + -- 定义两个变量 + DECLARE men,women INT; + -- 查询男同学的总分数,为men赋值 + SELECT SUM(score) INTO men FROM student WHERE gender='男'; + -- 查询女同学的总分数,为women赋值 + SELECT SUM(score) INTO women FROM student WHERE gender='女'; + -- 使用变量 + SELECT men,women; + END$ + DELIMITER ; + -- 调用存储过程 + CALL pro_test3(); + ``` @@ -2615,186 +2878,414 @@ InnoDB 引擎会在适当的时候,把内存中 redo log buffer 持久化到 -##### 工作流程 - -MySQL中还存在 binlog(二进制日志)也可以记录写操作并用于数据的恢复,**保证数据不丢失**,二者的区别是: - -* 作用不同:redo log 是用于 crash recovery (故障恢复),保证 MySQL 宕机也不会影响持久性;binlog 是用于 point-in-time recovery 的,保证服务器可以基于时间点恢复数据,此外 binlog 还用于主从复制 - -* 层次不同:redo log 是 InnoDB 存储引擎实现的,而 binlog 是MySQL的服务器层实现的,同时支持 InnoDB 和其他存储引擎 - -* 内容不同:redo log 是物理日志,内容基于磁盘的 Page;binlog 的内容是二进制的,根据 binlog_format 参数的不同,可能基于SQL 语句、基于数据本身或者二者的混合(日志部分详解) - -* 写入时机不同:binlog 在事务提交时一次写入;redo log 的写入时机相对多元 +##### IF语句 -两种日志在 update 更新数据的**作用时机**: +* if 语句标准语法 -```sql -update T set c=c+1 where ID=2; -``` + ```mysql + IF 判断条件1 THEN 执行的sql语句1; + [ELSEIF 判断条件2 THEN 执行的sql语句2;] + ... + [ELSE 执行的sql语句n;] + END IF; + ``` - +* 数据准备:表 student -流程说明:执行引擎将这行新数据更新到内存中(Buffer Pool)后,然后会将这个更新操作记录到 redo log buffer 里,此时 redo log 处于 prepare 状态,代表执行完成随时可以提交事务,然后执行器生成这个操作的 binlog 并**把 binlog 写入磁盘**,在提交事务后 **redo log 也持久化到磁盘** + ```mysql + id NAME age gender score + 1 张三 23 男 95 + 2 李四 24 男 98 + 3 王五 25 女 100 + 4 赵六 26 女 90 + ``` + +* 根据总成绩判断:全班 380 分及以上学习优秀、320 ~ 380 学习良好、320 以下学习一般 -redo log 和 binlog 都可以用于表示事务的提交状态,而**两阶段提交就是让这两个状态保持逻辑上的一致**,也有利于主从复制,更好的保持主从数据的一致性 + ```mysql + DELIMITER $ + CREATE PROCEDURE pro_test4() + BEGIN + DECLARE total INT; -- 定义总分数变量 + DECLARE description VARCHAR(10); -- 定义分数描述变量 + SELECT SUM(score) INTO total FROM student; -- 为总分数变量赋值 + -- 判断总分数 + IF total >= 380 THEN + SET description = '学习优秀'; + ELSEIF total >=320 AND total < 380 THEN + SET description = '学习良好'; + ELSE + SET description = '学习一般'; + END IF; + END$ + DELIMITER ; + -- 调用pro_test4存储过程 + CALL pro_test4(); + ``` -故障恢复数据: -* 如果在时刻 A 发生了崩溃(crash),由于此时 binlog 还没写,redo log 也没提交,所以数据恢复的时候这个事务会回滚 -* 如果在时刻 B 发生了崩溃,redo log 和 binlog 有一个共同的数据字段叫 XID,崩溃恢复的时候,会按顺序扫描 redo log: - * 如果 redo log 里面的事务是完整的,也就是已经有了 commit 标识,说明 binlog 也已经记录完整,直接从 redo log 恢复数据 - * 如果 redo log 里面的事务只有 prepare,就根据 XID 去 binlog 中判断对应的事务是否存在并完整,如果完整可以从 binlog 恢复 redo log 的信息,进而恢复数据,提交事务 -判断一个事务的 binlog 是否完整的方法: +*** -* statement 格式的 binlog,最后会有 COMMIT -* row 格式的 binlog,最后会有一个 XID event -* MySQL 5.6.2 版本以后,引入了 binlog-checksum 参数用来验证 binlog 内容的正确性 +##### 参数传递 -参考文章:https://time.geekbang.org/column/article/73161 +* 参数传递的语法 + IN:代表输入参数,需要由调用者传递实际数据,默认的 + OUT:代表输出参数,该参数可以作为返回值 + INOUT:代表既可以作为输入参数,也可以作为输出参数 + ```mysql + DELIMITER $ + + -- 标准语法 + CREATE PROCEDURE 存储过程名称([IN|OUT|INOUT] 参数名 数据类型) + BEGIN + 执行的sql语句; + END$ + + DELIMITER ; + ``` -*** +* 输入总成绩变量,代表学生总成绩,输出分数描述变量,代表学生总成绩的描述 + ```mysql + DELIMITER $ + + CREATE PROCEDURE pro_test6(IN total INT, OUT description VARCHAR(10)) + BEGIN + -- 判断总分数 + IF total >= 380 THEN + SET description = '学习优秀'; + ELSEIF total >= 320 AND total < 380 THEN + SET description = '学习不错'; + ELSE + SET description = '学习一般'; + END IF; + END$ + + DELIMITER ; + -- 调用pro_test6存储过程 + CALL pro_test6(310,@description); + CALL pro_test6((SELECT SUM(score) FROM student), @description); + -- 查询总成绩描述 + SELECT @description; + ``` +* 查看参数方法 -##### 系统优化 + * @变量名 : **用户会话变量**,代表整个会话过程他都是有作用的,类似于全局变量 + * @@变量名 : **系统变量** -系统在进行刷脏时会占用一部分系统资源,会影响系统的性能,产生系统抖动 + -* 一个查询要淘汰的脏页个数太多,会导致查询的响应时间明显变长 -* 日志写满,更新全部堵住,写性能跌为 0,这种情况对敏感业务来说,是不能接受的 +*** -InnoDB 刷脏页的控制策略: -* `innodb_io_capacity` 参数代表磁盘的读写能力,建议设置成磁盘的 IOPS(每秒的 IO 次数) -* 刷脏速度参考两个因素:脏页比例和 redo log 写盘速度 - * 参数 `innodb_max_dirty_pages_pct` 是脏页比例上限,默认值是 75%,InnoDB 会根据当前的脏页比例,算出一个范围在 0 到 100 之间的数字 - * InnoDB 每次写入的日志都有一个序号,当前写入的序号跟 checkpoint 对应的序号之间的差值,InnoDB 根据差值算出一个范围在 0 到 100 之间的数字 - * 两者较大的值记为 R,执行引擎按照 innodb_io_capacity 定义的能力乘以 R% 来控制刷脏页的速度 +##### CASE -* `innodb_flush_neighbors` 参数置为 1 代表控制刷脏时检查相邻的数据页,如果也是脏页就一起刷脏,并检查邻居的邻居,这个行为会一直蔓延直到不是脏页,在 MySQL 8.0 中该值的默认值是 0,不建议开启此功能 +* 标准语法 1 + ```mysql + CASE 表达式 + WHEN 值1 THEN 执行sql语句1; + [WHEN 值2 THEN 执行sql语句2;] + ... + [ELSE 执行sql语句n;] + END CASE; + ``` +* 标准语法 2 + ```mysql + sCASE + WHEN 判断条件1 THEN 执行sql语句1; + [WHEN 判断条件2 THEN 执行sql语句2;] + ... + [ELSE 执行sql语句n;] + END CASE; + ``` +* 演示 -*** + ```mysql + DELIMITER $ + CREATE PROCEDURE pro_test7(IN total INT) + BEGIN + -- 定义变量 + DECLARE description VARCHAR(10); + -- 使用case判断 + CASE + WHEN total >= 380 THEN + SET description = '学习优秀'; + WHEN total >= 320 AND total < 380 THEN + SET description = '学习不错'; + ELSE + SET description = '学习一般'; + END CASE; + + -- 查询分数描述信息 + SELECT description; + END$ + DELIMITER ; + -- 调用pro_test7存储过程 + CALL pro_test7(390); + CALL pro_test7((SELECT SUM(score) FROM student)); + ``` -### 隔离级别 +*** -事务的隔离级别:多个客户端操作时,各个客户端的事务之间应该是隔离的,**不同的事务之间不该互相影响**,而如果多个事务操作同一批数据时,则需要设置不同的隔离级别,否则就会产生问题。 -隔离级别分类: -| 隔离级别 | 名称 | 会引发的问题 | 数据库默认隔离级别 | -| ---------------- | -------- | -------------------------------- | ------------------- | -| read uncommitted | 读未提交 | 脏读、不可重复读、幻读 | | -| read committed | 读已提交 | 不可重复读、幻读 | Oracle / SQL Server | -| repeatable read | 可重复读 | 幻读 | MySQL | -| serializable | 串行化 | 无(因为写会加写锁,读会加读锁) | | +##### WHILE -一般来说,隔离级别越低,系统开销越低,可支持的并发越高,但隔离性也越差 +* while 循环语法 + + ```mysql + WHILE 条件判断语句 DO + 循环体语句; + 条件控制语句; + END WHILE; + ``` + +* 计算 1~100 之间的偶数和 + + ```mysql + DELIMITER $ + CREATE PROCEDURE pro_test6() + BEGIN + -- 定义求和变量 + DECLARE result INT DEFAULT 0; + -- 定义初始化变量 + DECLARE num INT DEFAULT 1; + -- while循环 + WHILE num <= 100 DO + IF num % 2 = 0 THEN + SET result = result + num; + END IF; + SET num = num + 1; + END WHILE; + -- 查询求和结果 + SELECT result; + END$ + DELIMITER ; + + -- 调用pro_test6存储过程 + CALL pro_test6(); + ``` -* 丢失更新 (Lost Update):当两个或多个事务选择同一行,最初的事务修改的值,被后面事务修改的值覆盖,所有的隔离级别都可以避免丢失更新(行锁) -* 脏读 (Dirty Reads):在事务中先后两次读取同一个数据,两次读取的结果不一样,原因是在一个事务处理过程中读取了另一个**未提交**的事务中的数据 -* 不可重复读 (Non-Repeatable Reads):在事务中先后两次读取同一个数据,两次读取的结果不一样,原因是在一个事务处理过程中读取了另一个事务中修改并**已提交**的数据 +*** - > 可重复读的意思是不管读几次,结果都一样,可以重复的读,可以理解为快照读,要读的数据集不会发生变化 -* 幻读 (Phantom Reads):在事务中按某个条件先后两次查询数据库,后一次查询查到了前一次查询没有查到的行,**数据条目**发生了变化。比如查询某数据不存在,准备插入此记录,但执行插入时发现此记录已存在,无法插入 -**隔离级别操作语法:** +##### REPEAT -* 查询数据库隔离级别 +* repeat 循环标准语法 ```mysql - SELECT @@TX_ISOLATION; - SHOW VARIABLES LIKE 'tx_isolation'; + 初始化语句; + REPEAT + 循环体语句; + 条件控制语句; + UNTIL 条件判断语句 + END REPEAT; ``` -* 修改数据库隔离级别 +* 计算 1~10 之间的和 ```mysql - SET GLOBAL TRANSACTION ISOLATION LEVEL 级别字符串; + DELIMITER $ + CREATE PROCEDURE pro_test9() + BEGIN + -- 定义求和变量 + DECLARE result INT DEFAULT 0; + -- 定义初始化变量 + DECLARE num INT DEFAULT 1; + -- repeat循环 + REPEAT + -- 累加 + SET result = result + num; + -- 让num+1 + SET num = num + 1; + -- 停止循环 + UNTIL num > 10 + END REPEAT; + -- 查询求和结果 + SELECT result; + END$ + + DELIMITER ; + -- 调用pro_test9存储过程 + CALL pro_test9(); ``` -*** +*** -### 并发控制 -#### MVCC +##### LOOP -MVCC 全称 Multi-Version Concurrency Control,即多版本并发控制,用来**解决读写冲突的无锁并发控制** +LOOP 实现简单的循环,退出循环的条件需要使用其他的语句定义,通常可以使用 LEAVE 语句实现,如果不加退出循环的语句,那么就变成了死循环 -MVCC 处理读写请求,可以做到在发生读写请求冲突时不用加锁,这个读是指的快照读,而不是当前读 +* loop 循环标准语法 -* 快照读:实现基于 MVCC,因为是多版本并发,所以快照读读到的数据不一定是当前最新的数据,有可能是历史版本的数据 -* 当前读:读取数据库记录是当前最新的版本(产生幻读、不可重复读),可以对读取的数据进行加锁,防止其他事务修改数据,是悲观锁的一种操作,读写操作加共享锁或者排他锁和串行化事务的隔离级别都是当前读 + ```mysql + [循环名称:] LOOP + 条件判断语句 + [LEAVE 循环名称;] + 循环体语句; + 条件控制语句; + END LOOP 循环名称; + ``` + +* 计算 1~10 之间的和 -数据库并发场景: + ```mysql + DELIMITER $ + CREATE PROCEDURE pro_test10() + BEGIN + -- 定义求和变量 + DECLARE result INT DEFAULT 0; + -- 定义初始化变量 + DECLARE num INT DEFAULT 1; + -- loop循环 + l:LOOP + -- 条件成立,停止循环 + IF num > 10 THEN + LEAVE l; + END IF; + -- 累加 + SET result = result + num; + -- 让num+1 + SET num = num + 1; + END LOOP l; + -- 查询求和结果 + SELECT result; + END$ + DELIMITER ; + -- 调用pro_test10存储过程 + CALL pro_test10(); + ``` -* 读-读:不存在任何问题,也不需要并发控制 -* 读-写:有线程安全问题,可能会造成事务隔离性问题,可能遇到脏读,幻读,不可重复读 -* 写-写:有线程安全问题,可能会存在丢失更新问题 +*** -MVCC 的优点: -* 在并发读写数据库时,做到在读操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了并发读写的性能 -* 可以解决脏读,不可重复读等事务隔离问题(加锁也能解决),但不能解决更新丢失问题 - -提高读写和写写的并发性能: - -* MVCC + 悲观锁:MVCC 解决读写冲突,悲观锁解决写写冲突 -* MVCC + 乐观锁:MVCC 解决读写冲突,乐观锁解决写写冲突 +##### 游标 +游标是用来存储查询结果集的数据类型,在存储过程和函数中可以使用光标对结果集进行循环的处理 +* 游标可以遍历返回的多行结果,每次拿到一整行数据 +* 简单来说游标就类似于集合的迭代器遍历 +* MySQL 中的游标只能用在存储过程和函数中 -参考文章:https://www.jianshu.com/p/8845ddca3b23 +游标的语法 +* 创建游标 + ```mysql + DECLARE 游标名称 CURSOR FOR 查询sql语句; + ``` -*** +* 打开游标 + ```mysql + OPEN 游标名称; + ``` +* 使用游标获取数据 -#### 原理 + ```mysql + FETCH 游标名称 INTO 变量名1,变量名2,...; + ``` -##### 隐藏字段 +* 关闭游标 -实现原理主要是隐藏字段,undo日志,Read View 来实现的 + ```mysql + CLOSE 游标名称; + ``` -数据库中的每行数据,除了自定义的字段,还有数据库隐式定义的字段: +* Mysql 通过一个 Error handler 声明来判断指针是否到尾部,并且必须和创建游标的 SQL 语句声明在一起: -* DB_TRX_ID:6byte,最近修改事务ID,记录创建该数据或最后一次修改(修改/插入)该数据的事务ID。当每个事务开启时,都会被分配一个ID,这个 ID 是递增的 + ```mysql + DECLARE EXIT HANDLER FOR NOT FOUND (do some action,一般是设置标志变量) + ``` -* DB_ROLL_PTR:7byte,回滚指针,配合 undo 日志,指向上一个旧版本(存储在 rollback segment) + -* DB_ROW_ID:6byte,隐含的自增ID(**隐藏主键**),如果数据表没有主键,InnoDB 会自动以 DB_ROW_ID 作为聚簇索引 +游标的基本使用 -* DELETED_BIT:删除标志的隐藏字段,记录被更新或删除并不代表真的删除,而是删除位变了 +* 数据准备:表 student -![](https://gitee.com/seazean/images/raw/master/DB/MySQL-MVCC版本链隐藏字段.png) + ```mysql + id NAME age gender score + 1 张三 23 男 95 + 2 李四 24 男 98 + 3 王五 25 女 100 + 4 赵六 26 女 90 + ``` +* 创建 stu_score 表 + ```mysql + CREATE TABLE stu_score( + id INT PRIMARY KEY AUTO_INCREMENT, + score INT + ); + ``` +* 将student表中所有的成绩保存到stu_score表中 + ```mysql + DELIMITER $ + + CREATE PROCEDURE pro_test12() + BEGIN + -- 定义成绩变量 + DECLARE s_score INT; + -- 定义标记变量 + DECLARE flag INT DEFAULT 0; + + -- 创建游标,查询所有学生成绩数据 + DECLARE stu_result CURSOR FOR SELECT score FROM student; + -- 游标结束后,将标记变量改为1 这两个必须声明在一起 + DECLARE EXIT HANDLER FOR NOT FOUND SET flag = 1; + + -- 开启游标 + OPEN stu_result; + -- 循环使用游标 + REPEAT + -- 使用游标,遍历结果,拿到数据 + FETCH stu_result INTO s_score; + -- 将数据保存到stu_score表中 + INSERT INTO stu_score VALUES (NULL,s_score); + UNTIL flag=1 + END REPEAT; + -- 关闭游标 + CLOSE stu_result; + END$ + + DELIMITER ; + + -- 调用pro_test12存储过程 + CALL pro_test12(); + -- 查询stu_score表 + SELECT * FROM stu_score; + ``` + + @@ -2802,34 +3293,59 @@ MVCC 的优点: -##### undo +#### 存储函数 -undo log 是逻辑日志,记录的是每个事务对数据执行的操作,而不是记录的全部数据,需要根据 undo log 逆推出以往事务的数据 +存储函数和存储过程是非常相似的,存储函数可以做的事情,存储过程也可以做到 -undo log 的作用: +存储函数有返回值,存储过程没有返回值(参数的 out 其实也相当于是返回数据了) -* 保证事务进行 rollback 时的原子性和一致性,当事务进行回滚的时候可以用 undo log 的数据进行恢复 -* 用于 MVCC 快照读,通过读取 undo log 的历史版本数据可以实现不同事务版本号都拥有自己独立的快照数据版本 +* 创建存储函数 -undo log 主要分为两种: + ```mysql + DELIMITER $ + -- 标准语法 + CREATE FUNCTION 函数名称(参数 数据类型) + RETURNS 返回值类型 + BEGIN + 执行的sql语句; + RETURN 结果; + END$ + + DELIMITER ; + ``` -* insert undo log:事务在 insert 新记录时产生的 undo log,只在事务回滚时需要,并且在事务提交后可以被立即丢弃 +* 调用存储函数,因为有返回值,所以使用 SELECT 调用 -* update undo log:事务在进行 update 或 delete 时产生的 undo log,在事务回滚时需要,在快照读时也需要。不能随意删除,只有在快速读或事务回滚不涉及该日志时,对应的日志才会被 purge 线程统一清除 + ```mysql + SELECT 函数名称(实际参数); + ``` -每次对数据库记录进行改动,都会将旧值放到一条 undo 日志中,算是该记录的一个旧版本,随着更新次数的增多,所有的版本都会被 roll_pointer 属性连接成一个链表,把这个链表称之为**版本链**,版本链的头节点就是当前记录最新的值,链尾就是最早的旧记录 +* 删除存储函数 - + ```mysql + DROP FUNCTION 函数名称; + ``` -* 有个事务插入 persion 表一条新记录,name 为 Jerry,age 为 24 +* 定义存储函数,获取学生表中成绩大于95分的学生数量 -* 事务 1 修改该行数据时,数据库会先对该行加排他锁,然后先记录 undo log,然后修改该行 name 为 Tom,并且修改隐藏字段的事务 ID 为当前事务 1 的 ID(默认为 1 之后递增),回滚指针指向拷贝到 undo log 的副本记录,事务提交后,释放锁 -* 以此类推 + ```mysql + DELIMITER $ + CREATE FUNCTION fun_test() + RETURN INT + BEGIN + -- 定义统计变量 + DECLARE result INT; + -- 查询成绩大于95分的学生数量,给统计变量赋值 + SELECT COUNT(score) INTO result FROM student WHERE score > 95; + -- 返回统计结果 + SELECT result; + END + DELIMITER ; + -- 调用fun_test存储函数 + SELECT fun_test(); + ``` -补充知识:purge 线程 -* 为了实现 InnoDB 的 MVCC 机制,更新或者删除操作都只是设置一下老记录的 deleted_bit,并不真正将过时的记录删除,为了节省磁盘空间,InnoDB 有专门的 purge 线程来清理 deleted_bit 为 true 的记录 -* purge 线程维护了一个 Read view(这个 Read view 相当于系统中最老活跃事务的 Read view),如果某个记录的 deleted_bit 为 true,并且 DB_TRX_ID 相对于 purge 线程的 Read view 可见,那么这条记录一定是可以被安全清除的 @@ -2837,30 +3353,22 @@ undo log 主要分为两种: -##### 读视图 - -Read View 是事务进行**快照读**操作时产生的读视图,该事务执行快照读的那一刻会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的 ID,用来做可见性判断,根据视图判断当前事务能够看到哪个版本的数据 - -注意:这里的快照并不是把所有的数据拷贝一份副本,而是由 undo log 记录的逻辑日志,根据库中的数据进行计算出历史数据 - -工作流程:将版本链的头节点的事务 ID(最新数据事务 ID)DB_TRX_ID 取出来,与系统当前活跃事务的 ID 对比,如果 DB_TRX_ID 不符合可见性,通过 DB_ROLL_PTR 回滚指针去取出 undo log 中的下一个 DB_TRX_ID 比较,直到找到最近的满足特定条件的 DB_TRX_ID,该事务 ID 所在的旧记录就是当前事务能看见的最新的记录 +### 触发器 -Read View 几个属性: +#### 基本介绍 -- m_ids:生成 Read View 时当前系统中活跃的事务 id 列表(未提交的事务集合,当前事务也在其中) -- up_limit_id:生成 Read View 时当前系统中活跃的最小的事务 id,也就是 m_ids 中的最小值(已提交的事务集合) -- low_limit_id:生成 Read View 时系统应该分配给下一个事务的 id 值,m_ids 中的最大值加 1(未开始事务) -- creator_trx_id:生成该 Read View 的事务的事务 id,就是判断该 id 的事务能读到什么数据 +触发器是与表有关的数据库对象,在 insert/update/delete 之前或之后触发并执行触发器中定义的 SQL 语句 -creator 创建一个 Read View,进行可见性算法分析:(解决了读未提交) +* 触发器的这种特性可以协助应用在数据库端确保数据的完整性 、日志记录 、数据校验等操作 -* db_trx_id == creator_trx_id:表示这个数据就是当前事务自己生成的,自己生成的数据自己肯定能看见,所以这种情况下此数据对 creator 是可见的 -* db_trx_id < up_limit_id:该版本对应的事务 ID 小于 Read view 中的最小活跃事务 ID,则这个事务在当前事务之前就已经被提交了,对 creator 可见 +- 使用别名 NEW 和 OLD 来引用触发器中发生变化的记录内容,这与其他的数据库是相似的 +- 现在触发器还只支持行级触发,不支持语句级触发 -* db_trx_id >= low_limit_id:该版本对应的事务 ID 大于 Read view 中当前系统的最大事务 ID,则说明该数据是在当前 Read view 创建之后才产生的,对 creator 不可见 -* up_limit_id <= db_trx_id < low_limit_id:判断 db_trx_id 是否在活跃事务列表 m_ids 中 - * 在列表中,说明该版本对应的事务正在运行,数据不能显示(**不能读到未提交的数据**) - * 不在列表中,说明该版本对应的事务已经被提交,数据可以显示(**可以读到已经提交的数据**) +| 触发器类型 | OLD的含义 | NEW的含义 | +| --------------- | ------------------------------ | ------------------------------ | +| INSERT 型触发器 | 无 (因为插入前状态无数据) | NEW 表示将要或者已经新增的数据 | +| UPDATE 型触发器 | OLD 表示修改之前的数据 | NEW 表示将要或已经修改后的数据 | +| DELETE 型触发器 | OLD 表示将要或者已经删除的数据 | 无 (因为删除后状态无数据) | @@ -2868,245 +3376,317 @@ creator 创建一个 Read View,进行可见性算法分析:(解决了读 -##### 工作流程 - -表 user 数据 - -```sh -id name age -1 张三 18 -``` - -Transaction 20: - -```mysql -START TRANSACTION; -- 开启事务 -UPDATE user SET name = '李四' WHERE id = 1; -UPDATE user SET name = '王五' WHERE id = 1; -``` - -Transaction 60: - -```mysql -START TRANSACTION; -- 开启事务 --- 操作表的其他数据 -``` - -![](https://gitee.com/seazean/images/raw/master/DB/MySQL-MVCC工作流程1.png) - -ID 为 0 的事务创建 Read View: - -* m_ids:20、60 -* up_limit_id:20 -* low_limit_id:61 -* creator_trx_id:0 +#### 基本操作 -![](https://gitee.com/seazean/images/raw/master/DB/MySQL-MVCC工作流程2.png) +* 创建触发器 -只有红框部分才复合条件,所以只有张三对应的版本的数据可以被看到 + ```mysql + DELIMITER $ + + CREATE TRIGGER 触发器名称 + BEFORE|AFTER INSERT|UPDATE|DELETE + ON 表名 + [FOR EACH ROW] -- 行级触发器 + BEGIN + 触发器要执行的功能; + END$ + + DELIMITER ; + ``` +* 查看触发器的状态、语法等信息 + ```mysql + SHOW TRIGGERS; + ``` -参考视频:https://www.bilibili.com/video/BV1t5411u7Fg +* 删除触发器,如果没有指定 schema_name,默认为当前数据库 + ```mysql + DROP TRIGGER [schema_name.]trigger_name; + ``` + *** -#### RC RR +#### 触发演示 -Read View 用于支持 RC(Read Committed,读已提交)和 RR(Repeatable Read,可重复读)隔离级别的实现 +通过触发器记录账户表的数据变更日志。包含:增加、修改、删除 -RR、RC 生成时机: +* 数据准备 -- RC 隔离级别下,每个快照读都会生成并获取最新的 Read View -- RR 隔离级别下,则是同一个事务中的第一个快照读才会创建 Read View + ```mysql + -- 创建db9数据库 + CREATE DATABASE db9; + -- 使用db9数据库 + USE db9; + ``` -RC、RR 级别下的 InnoDB 快照读区别 + ```mysql + -- 创建账户表account + CREATE TABLE account( + id INT PRIMARY KEY AUTO_INCREMENT, -- 账户id + NAME VARCHAR(20), -- 姓名 + money DOUBLE -- 余额 + ); + -- 添加数据 + INSERT INTO account VALUES (NULL,'张三',1000),(NULL,'李四',2000); + ``` -- RC 级别下的,事务中每次快照读都会新生成一个 Read View,这就是在 RC 级别下的事务中可以看到别的事务提交的更新的原因 + ```mysql + -- 创建日志表account_log + CREATE TABLE account_log( + id INT PRIMARY KEY AUTO_INCREMENT, -- 日志id + operation VARCHAR(20), -- 操作类型 (insert update delete) + operation_time DATETIME, -- 操作时间 + operation_id INT, -- 操作表的id + operation_params VARCHAR(200) -- 操作参数 + ); + ``` -- RR 级别下的某个事务的对某条记录的第一次快照读会创建一个 Read View, 将当前系统活跃的其他事务记录起来,此后在调用快照读的时候,使用的是同一个Read View,所以一个事务的查询结果每次都是相同的 +* 创建 INSERT 型触发器 - 当前事务在其他事务提交之前使用过快照读,那么以后其他事务对数据的修改都是不可见的,就算以后其他事务提交了数据也不可见;早于 Read View 创建的事务所做的修改并提交的均是可见的 + ```mysql + DELIMITER $ + + CREATE TRIGGER account_insert + AFTER INSERT + ON account + FOR EACH ROW + BEGIN + INSERT INTO account_log VALUES (NULL,'INSERT',NOW(),new.id,CONCAT('插入后{id=',new.id,',name=',new.name,',money=',new.money,'}')); + END$ + + DELIMITER ; + ``` -解决幻读问题: + ```mysql + -- 向account表添加记录 + INSERT INTO account VALUES (NULL,'王五',3000); + + -- 查询日志表 + SELECT * FROM account_log; + /* + id operation operation_time operation_id operation_params + 1 INSERT 2021-01-26 19:51:11 3 插入后{id=3,name=王五money=2000} + */ + ``` -- 快照读:通过 MVCC 来进行控制的,在可重复读隔离级别下,普通查询是快照读,是不会看到别的事务插入的数据的,但是**并不能完全避免幻读** + - 场景:RR 级别,T1 事务开启,创建 Read View,此时 T2 去 INSERT 新的一行然后提交,然后 T1去 UPDATE 该行会发现更新成功,因为 Read View 并不能阻止事务去更新数据,并且把这条新记录的 trx_id 给变为当前的事务 id,对当前事务就是可见的了 -- 当前读:通过 next-key 锁(行锁 + 间隙锁)来解决问题 +* 创建 UPDATE 型触发器 + ```mysql + DELIMITER $ + + CREATE TRIGGER account_update + AFTER UPDATE + ON account + FOR EACH ROW + BEGIN + INSERT INTO account_log VALUES (NULL,'UPDATE',NOW(),new.id,CONCAT('修改前{id=',old.id,',name=',old.name,',money=',old.money,'}','修改后{id=',new.id,',name=',new.name,',money=',new.money,'}')); + END$ + + DELIMITER ; + ``` + ```mysql + -- 修改account表 + UPDATE account SET money=3500 WHERE id=3; + + -- 查询日志表 + SELECT * FROM account_log; + /* + id operation operation_time operation_id operation_params + 2 UPDATE 2021-01-26 19:58:54 2 更新前{id=2,name=李四money=1000} + 更新后{id=2,name=李四money=200} + */ + ``` + +* 创建 DELETE 型触发器 -*** + ```mysql + DELIMITER $ + + CREATE TRIGGER account_delete + AFTER DELETE + ON account + FOR EACH ROW + BEGIN + INSERT INTO account_log VALUES (NULL,'DELETE',NOW(),old.id,CONCAT('删除前{id=',old.id,',name=',old.name,',money=',old.money,'}')); + END$ + + DELIMITER ; + ``` + ```mysql + -- 删除account表数据 + DELETE FROM account WHERE id=3; + + -- 查询日志表 + SELECT * FROM account_log; + /* + id operation operation_time operation_id operation_params + 3 DELETE 2021-01-26 20:02:48 3 删除前{id=3,name=王五money=2000} + */ + ``` -## 存储结构 -### 视图 -#### 基本介绍 -视图概念:视图是一种虚拟存在的数据表,这个虚拟的表并不在数据库中实际存在 +*** -本质:将一条 SELECT 查询语句的结果封装到了一个虚拟表中,所以在创建视图的时候,工作重心要放在这条 SELECT 查询语句上 -作用:将一些比较复杂的查询语句的结果,封装到一个虚拟表中,再有相同查询需求时,直接查询该虚拟表 -优点: -* 简单:使用视图的用户不需要关心表的结构、关联条件和筛选条件,因为虚拟表中已经是过滤好的结果集 -* 安全:使用视图的用户只能访问查询的结果集,对表的权限管理并不能限制到某个行某个列 -* 数据独立,一旦视图的结构确定,可以屏蔽表结构变化对用户的影响,源表增加列对视图没有影响;源表修改列名,则可以通过修改视图来解决,不会造成对访问者的影响 +## 存储引擎 +### 基本介绍 +对比其他数据库,MySQL 的架构可以在不同场景应用并发挥良好作用,主要体现在存储引擎,插件式的存储引擎架构将查询处理和其他的系统任务以及数据的存储提取分离,可以针对不同的存储需求可以选择最优的存储引擎 -*** +存储引擎的介绍: +- MySQL 数据库使用不同的机制存取表文件 , 机制的差别在于不同的存储方式、索引技巧、锁定水平等不同的功能和能力,在 MySQL 中,将这些不同的技术及配套的功能称为存储引擎 +- Oracle、SqlServer 等数据库只有一种存储引擎,MySQL **提供了插件式的存储引擎架构**,所以 MySQL 存在多种存储引擎 , 就会让数据库采取了不同的处理数据的方式和扩展功能 +- 在关系型数据库中数据的存储是以表的形式存进行,所以存储引擎也称为表类型(存储和操作此表的类型) +- 通过选择不同的引擎,能够获取最佳的方案, 也能够获得额外的速度或者功能,提高程序的整体效果。 +MySQL 支持的存储引擎: -#### 视图创建 +- MySQL 支持的引擎包括:InnoDB、MyISAM、MEMORY、Archive、Federate、CSV、BLACKHOLE 等 +- MySQL5.5 之前的默认存储引擎是 MyISAM,5.5 之后就改为了 InnoDB -* 创建视图 - ```mysql - CREATE [OR REPLACE] - VIEW 视图名称 [(列名列表)] - AS 查询语句 - [WITH [CASCADED | LOCAL] CHECK OPTION]; - ``` - `WITH [CASCADED | LOCAL] CHECK OPTION` 决定了是否允许更新数据使记录不再满足视图的条件: +**** - * LOCAL:只要满足本视图的条件就可以更新 - * CASCADED:必须满足所有针对该视图的所有视图的条件才可以更新, 默认值 -* 例如 - ```mysql - -- 数据准备 city - id NAME cid - 1 深圳 1 - 2 上海 1 - 3 纽约 2 - 4 莫斯科 3 - - -- 数据准备 country - id NAME - 1 中国 - 2 美国 - 3 俄罗斯 - - -- 创建city_country视图,保存城市和国家的信息(使用指定列名) - CREATE - VIEW - city_country (city_id,city_name,country_name) - AS - SELECT - c1.id, - c1.name, - c2.name - FROM - city c1, - country c2 - WHERE - c1.cid=c2.id; - ``` +### 引擎对比 - +MyISAM 存储引擎: -*** +* 特点:不支持事务和外键,读取速度快,节约资源 +* 应用场景:**适用于读多写少的场景**,对事务的完整性要求不高,比如一些数仓、离线数据、支付宝的年度总结之类的场景,业务进行只读操作,查询起来会更快 +* 存储方式: + * 每个 MyISAM 在磁盘上存储成 3 个文件,其文件名都和表名相同,拓展名不同 + * 表的定义保存在 .frm 文件,表数据保存在 .MYD (MYData) 文件中,索引保存在 .MYI (MYIndex) 文件中 +InnoDB 存储引擎:(MySQL5.5 版本后默认的存储引擎) +- 特点:**支持事务**和外键操作,支持并发控制。对比 MyISAM 的存储引擎,InnoDB 写的处理效率差一些,并且会占用更多的磁盘空间以保留数据和索引 +- 应用场景:对事务的完整性有比较高的要求,在并发条件下要求数据的一致性,读写频繁的操作 +- 存储方式: + - 使用共享表空间存储, 这种方式创建的表的表结构保存在 .frm 文件中, 数据和索引保存在 innodb_data_home_dir 和 innodb_data_file_path 定义的表空间中,可以是多个文件 + - 使用多表空间存储,创建的表的表结构存在 .frm 文件中,每个表的数据和索引单独保存在 .ibd 中 -#### 视图查询 +MEMORY 存储引擎: -* 查询所有数据表,视图也会查询出来 +- 特点:每个 MEMORY 表实际对应一个磁盘文件 ,该文件中只存储表的结构,表数据保存在内存中,且默认**使用 HASH 索引**,所以数据默认就是无序的,但是在需要快速定位记录可以提供更快的访问,**服务一旦关闭,表中的数据就会丢失**,存储不安全 +- 应用场景:**缓存型存储引擎**,通常用于更新不太频繁的小表,用以快速得到访问结果 +- 存储方式:表结构保存在 .frm 中 - ```mysql - SHOW TABLES; - SHOW TABLE STATUS [\G]; - ``` +MERGE 存储引擎: -* 查询视图 +* 特点: - ```mysql - SELECT * FROM 视图名称; - ``` + * 是一组 MyISAM 表的组合,这些 MyISAM 表必须结构完全相同,通过将不同的表分布在多个磁盘上 + * MERGE 表本身并没有存储数据,对 MERGE 类型的表可以进行查询、更新、删除操作,这些操作实际上是对内部的 MyISAM 表进行的 -* 查询某个视图创建 +* 应用场景:将一系列等同的 MyISAM 表以逻辑方式组合在一起,并作为一个对象引用他们,适合做数据仓库 + +* 操作方式: + + * 插入操作是通过 INSERT_METHOD 子句定义插入的表,使用 FIRST 或 LAST 值使得插入操作被相应地作用在第一或者最后一个表上;不定义这个子句或者定义为 NO,表示不能对 MERGE 表执行插入操作 + * 对 MERGE 表进行 DROP 操作,但是这个操作只是删除 MERGE 表的定义,对内部的表是没有任何影响的 ```mysql - SHOW CREATE VIEW 视图名称; + CREATE TABLE order_1( + )ENGINE = MyISAM DEFAULT CHARSET=utf8; + + CREATE TABLE order_2( + )ENGINE = MyISAM DEFAULT CHARSET=utf8; + + CREATE TABLE order_all( + -- 结构与MyISAM表相同 + )ENGINE = MERGE UNION = (order_1,order_2) INSERT_METHOD=LAST DEFAULT CHARSET=utf8; ``` + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-MERGE.png) +| 特性 | MyISAM | InnoDB | MEMORY | +| ------------ | ------------------------------ | ------------- | -------------------- | +| 存储限制 | 有(平台对文件系统大小的限制) | 64TB | 有(平台的内存限制) | +| **事务安全** | **不支持** | **支持** | **不支持** | +| **锁机制** | **表锁** | **表锁/行锁** | **表锁** | +| B+Tree 索引 | 支持 | 支持 | 支持 | +| 哈希索引 | 不支持 | 不支持 | 支持 | +| 全文索引 | 支持 | 支持 | 不支持 | +| 集群索引 | 不支持 | 支持 | 不支持 | +| 数据索引 | 不支持 | 支持 | 支持 | +| 数据缓存 | 不支持 | 支持 | N/A | +| 索引缓存 | 支持 | 支持 | N/A | +| 数据可压缩 | 支持 | 不支持 | 不支持 | +| 空间使用 | 低 | 高 | N/A | +| 内存使用 | 低 | 高 | 中等 | +| 批量插入速度 | 高 | 低 | 高 | +| **外键** | **不支持** | **支持** | **不支持** | -*** +只读场景 MyISAM 比 InnoDB 更快: +* 底层存储结构有差别,MyISAM 是非聚簇索引,叶子节点保存的是数据的具体地址,不用回表查询 +* InnoDB 每次查询需要维护 MVCC 版本状态,保证并发状态下的读写冲突问题 -#### 视图修改 -视图表数据修改,会**自动修改源表中的数据**,因为更新的是视图中的基表中的数据 +*** -* 修改视图表中的数据 - ```mysql - UPDATE 视图名称 SET 列名 = 值 WHERE 条件; - ``` -* 修改视图的结构 +### 引擎操作 + +* 查询数据库支持的存储引擎 ```mysql - ALTER [ALGORITHM = {UNDEFINED | MERGE | TEMPTABLE}] - VIEW 视图名称 [(列名列表)] - AS 查询语句 - [WITH [CASCADED | LOCAL] CHECK OPTION] - - -- 将视图中的country_name修改为name - ALTER - VIEW - city_country (city_id,city_name,name) - AS - SELECT - c1.id, - c1.name, - c2.name - FROM - city c1, - country c2 - WHERE - c1.cid=c2.id; + SHOW ENGINES; + SHOW VARIABLES LIKE '%storage_engine%'; -- 查看Mysql数据库默认的存储引擎 ``` +* 查询某个数据库中所有数据表的存储引擎 + ```mysql + SHOW TABLE STATUS FROM 数据库名称; + ``` -*** - - +* 查询某个数据库中某个数据表的存储引擎 -#### 视图删除 + ```mysql + SHOW TABLE STATUS FROM 数据库名称 WHERE NAME = '数据表名称'; + ``` -* 删除视图 +* 创建数据表,指定存储引擎 ```mysql - DROP VIEW 视图名称; + CREATE TABLE 表名( + 列名,数据类型, + ... + )ENGINE = 引擎名称; ``` -* 如果存在则删除 +* 修改数据表的存储引擎 ```mysql - DROP VIEW IF EXISTS 视图名称; + ALTER TABLE 表名 ENGINE = 引擎名称; ``` @@ -3114,166 +3694,171 @@ RC、RR 级别下的 InnoDB 快照读区别 -*** +*** -### 存储过程 -#### 基本介绍 -存储过程和函数:存储过程和函数是事先经过编译并存储在数据库中的一段 SQL 语句的集合 -存储过程和函数的好处: -* 提高代码的复用性 -* 减少数据在数据库和应用服务器之间的传输,提高传输效率 -* 减少代码层面的业务处理 -* **一次编译永久有效** +## 索引机制 -存储过程和函数的区别: +### 索引介绍 -* 存储函数必须有返回值 -* 存储过程可以没有返回值 +#### 基本介绍 +MySQL 官方对索引的定义为:索引(index)是帮助 MySQL 高效获取数据的一种数据结构,**本质是排好序的快速查找数据结构。**在表数据之外,数据库系统还维护着满足特定查找算法的数据结构,这些数据结构以某种方式指向数据, 这样就可以在这些数据结构上实现高级查找算法,这种数据结构就是索引 +**索引是在存储引擎层实现的**,所以并没有统一的索引标准,即不同存储引擎的索引的工作方式并不一样 -*** +索引使用:一张数据表,用于保存数据;一个索引配置文件,用于保存索引;每个索引都指向了某一个数据 +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-索引的介绍.png) +左边是数据表,一共有两列七条记录,最左边的是数据记录的物理地址(注意逻辑上相邻的记录在磁盘上也并不是一定物理相邻的)。为了加快 Col2 的查找,可以维护一个右边所示的二叉查找树,每个节点分别包含索引键值和一个指向对应数据的物理地址的指针,这样就可以运用二叉查找快速获取到相应数据 +索引的优点: -#### 基本操作 +* 类似于书籍的目录索引,提高数据检索的效率,降低数据库的 IO 成本 +* 通过索引列对数据进行排序,降低数据排序的成本,降低 CPU 的消耗 -DELIMITER: +索引的缺点: -* DELIMITER 关键字用来声明 sql 语句的分隔符,告诉 MySQL 该段命令已经结束 +* 一般来说索引本身也很大,不可能全部存储在内存中,因此索引往往以索引文件的形式**存储在磁盘**上 +* 虽然索引大大提高了查询效率,同时却也降低更新表的速度。对表进行 INSERT、UPDATE、DELETE 操作,MySQL 不仅要保存数据,还要保存一下索引文件每次更新添加了索引列的字段,还会调整因为更新所带来的键值变化后的索引信息,**但是更新数据也需要先从数据库中获取**,索引加快了获取速度,所以可以相互抵消一下。 +* 索引会影响到 WHERE 的查询条件和排序 ORDER BY 两大功能 -* MySQL 语句默认的分隔符是分号,但是有时需要一条功能 sql 语句中包含分号,但是并不作为结束标识,这时使用 DELIMITER 来指定分隔符: - ```mysql - DELIMITER 分隔符 - ``` -存储过程的创建调用查看和删除: +*** -* 创建存储过程 - ```mysql - -- 修改分隔符为$ - DELIMITER $ + +#### 索引分类 + +索引一般的分类如下: + +- 功能分类 + - 主键索引:一种特殊的唯一索引,不允许有空值,一般在建表时同时创建主键索引 + - 单列索引:一个索引只包含单个列,一个表可以有多个单列索引(普通索引) + - 联合索引:顾名思义,就是将单列索引进行组合 + - 唯一索引:索引列的值必须唯一,**允许有空值**,如果是联合索引,则列值组合必须唯一 + * NULL 值可以出现多次,因为两个 NULL 比较的结果既不相等,也不不等,结果仍然是未知 + * 可以声明不允许存储 NULL 值的非空唯一索引 + - 外键索引:只有 InnoDB 引擎支持外键索引,用来保证数据的一致性、完整性和实现级联操作 - -- 标准语法 - CREATE PROCEDURE 存储过程名称(参数...) - BEGIN - sql语句; - END$ +- 结构分类 + - BTree 索引:MySQL 使用最频繁的一个索引数据结构,是 InnoDB 和 MyISAM 存储引擎默认的索引类型,底层基于 B+Tree + - Hash 索引:MySQL中 Memory 存储引擎默认支持的索引类型 + - R-tree 索引(空间索引):空间索引是 MyISAM 引擎的一个特殊索引类型,主要用于地理空间数据类型 + - Full-text 索引(全文索引):快速匹配全部文档的方式。MyISAM 支持, InnoDB 不支持 FULLTEXT 类型的索引,但是 InnoDB 可以使用 sphinx 插件支持全文索引,MEMORY 引擎不支持 - -- 修改分隔符为分号 - DELIMITER ; + | 索引 | InnoDB | MyISAM | Memory | + | --------- | ---------------- | ------ | ------ | + | BTREE | 支持 | 支持 | 支持 | + | HASH | 不支持 | 不支持 | 支持 | + | R-tree | 不支持 | 支持 | 不支持 | + | Full-text | 5.6 版本之后支持 | 支持 | 不支持 | + +联合索引图示:根据身高年龄建立的组合索引(height、age) + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-组合索引图.png) + + + + + +*** + + + +### 索引操作 + +索引在创建表的时候可以同时创建, 也可以随时增加新的索引 + +* 创建索引:如果一个表中有一列是主键,那么会**默认为其创建主键索引**(主键列不需要单独创建索引) + + ```mysql + CREATE [UNIQUE|FULLTEXT] INDEX 索引名称 [USING 索引类型] ON 表名(列名...); + -- 索引类型默认是 B+TREE ``` -* 调用存储过程 +* 查看索引 ```mysql - CALL 存储过程名称(实际参数); + SHOW INDEX FROM 表名; ``` -* 查看存储过程 +* 添加索引 ```mysql - SELECT * FROM mysql.proc WHERE db='数据库名称'; + -- 单列索引 + ALTER TABLE 表名 ADD INDEX 索引名称(列名); + + -- 组合索引 + ALTER TABLE 表名 ADD INDEX 索引名称(列名1,列名2,...); + + -- 主键索引 + ALTER TABLE 表名 ADD PRIMARY KEY(主键列名); + + -- 外键索引(添加外键约束,就是外键索引) + ALTER TABLE 表名 ADD CONSTRAINT 外键名 FOREIGN KEY (本表外键列名) REFERENCES 主表名(主键列名); + + -- 唯一索引 + ALTER TABLE 表名 ADD UNIQUE 索引名称(列名); + + -- 全文索引(mysql只支持文本类型) + ALTER TABLE 表名 ADD FULLTEXT 索引名称(列名); ``` -* 删除存储过程 +* 删除索引 ```mysql - DROP PROCEDURE [IF EXISTS] 存储过程名称; + DROP INDEX 索引名称 ON 表名; ``` -练习: +* 案例练习 -* 数据准备 + 数据准备:student ```mysql - id NAME age gender score - 1 张三 23 男 95 - 2 李四 24 男 98 - 3 王五 25 女 100 - 4 赵六 26 女 90 + id NAME age score + 1 张三 23 99 + 2 李四 24 95 + 3 王五 25 98 + 4 赵六 26 97 ``` -* 创建 stu_group() 存储过程,封装分组查询总成绩,并按照总成绩升序排序的功能 + 索引操作: ```mysql - DELIMITER $ - - CREATE PROCEDURE stu_group() - BEGIN - SELECT gender,SUM(score) getSum FROM student GROUP BY gender ORDER BY getSum ASC; - END$ - - DELIMITER ; + -- 为student表中姓名列创建一个普通索引 + CREATE INDEX idx_name ON student(NAME); - -- 调用存储过程 - CALL stu_group(); - -- 删除存储过程 - DROP PROCEDURE IF EXISTS stu_group; + -- 为student表中年龄列创建一个唯一索引 + CREATE UNIQUE INDEX idx_age ON student(age); ``` - -*** -#### 存储语法 -##### 变量使用 +*** -存储过程是可以进行编程的,意味着可以使用变量、表达式、条件控制语句等,来完成比较复杂的功能 -* 定义变量:DECLARE 定义的是局部变量,只能用在 BEGIN END 范围之内 - - ```mysql - DECLARE 变量名 数据类型 [DEFAULT 默认值]; - ``` - -* 变量的赋值 - ```mysql - SET 变量名 = 变量值; - SELECT 列名 INTO 变量名 FROM 表名 [WHERE 条件]; - ``` +### 聚簇索引 -* 数据准备:表 student +#### 索引对比 - ```mysql - id NAME age gender score - 1 张三 23 男 95 - 2 李四 24 男 98 - 3 王五 25 女 100 - 4 赵六 26 女 90 - ``` +聚簇索引是一种数据存储方式,并不是一种单独的索引类型 -* 定义两个 int 变量,用于存储男女同学的总分数 +* 聚簇索引的叶子节点存放的是主键值和数据行,支持覆盖索引 - ```mysql - DELIMITER $ - CREATE PROCEDURE pro_test3() - BEGIN - -- 定义两个变量 - DECLARE men,women INT; - -- 查询男同学的总分数,为men赋值 - SELECT SUM(score) INTO men FROM student WHERE gender='男'; - -- 查询女同学的总分数,为women赋值 - SELECT SUM(score) INTO women FROM student WHERE gender='女'; - -- 使用变量 - SELECT men,women; - END$ - DELIMITER ; - -- 调用存储过程 - CALL pro_test3(); - ``` +* 非聚簇索引的叶子节点存放的是主键值或指向数据行的指针(由存储引擎决定) + +在 Innodb 下主键索引是聚簇索引,在 MyISAM 下主键索引是非聚簇索引 @@ -3281,51 +3866,29 @@ DELIMITER: -##### IF语句 +#### Innodb -* if 语句标准语法 +##### 聚簇索引 - ```mysql - IF 判断条件1 THEN 执行的sql语句1; - [ELSEIF 判断条件2 THEN 执行的sql语句2;] - ... - [ELSE 执行的sql语句n;] - END IF; - ``` +在 Innodb 存储引擎,B+ 树索引可以分为聚簇索引(也称聚集索引、clustered index)和辅助索引(也称非聚簇索引或二级索引、secondary index、non-clustered index) -* 数据准备:表 student +InnoDB 中,聚簇索引是按照每张表的主键构造一颗 B+ 树,叶子节点中存放的就是整张表的数据,将聚簇索引的叶子节点称为数据页 - ```mysql - id NAME age gender score - 1 张三 23 男 95 - 2 李四 24 男 98 - 3 王五 25 女 100 - 4 赵六 26 女 90 - ``` - -* 根据总成绩判断:全班 380 分及以上学习优秀、320 ~ 380 学习良好、320 以下学习一般 +* 这个特性决定了**数据也是索引的一部分**,所以一张表只能有一个聚簇索引 +* 辅助索引的存在不影响聚簇索引中数据的组织,所以一张表可以有多个辅助索引 - ```mysql - DELIMITER $ - CREATE PROCEDURE pro_test4() - BEGIN - DECLARE total INT; -- 定义总分数变量 - DECLARE description VARCHAR(10); -- 定义分数描述变量 - SELECT SUM(score) INTO total FROM student; -- 为总分数变量赋值 - -- 判断总分数 - IF total >= 380 THEN - SET description = '学习优秀'; - ELSEIF total >=320 AND total < 380 THEN - SET description = '学习良好'; - ELSE - SET description = '学习一般'; - END IF; - END$ - DELIMITER ; - -- 调用pro_test4存储过程 - CALL pro_test4(); - ``` +聚簇索引的优点: + +* 数据访问更快,聚簇索引将索引和数据保存在同一个 B+ 树中,因此从聚簇索引中获取数据比非聚簇索引更快 +* 聚簇索引对于主键的排序查找和范围查找速度非常快 + +聚簇索引的缺点: + +* 插入速度严重依赖于插入顺序,按照主键的顺序(递增)插入是最快的方式,否则将会出现页分裂,严重影响性能,所以对于 InnoDB 表,一般都会定义一个自增的 ID 列为主键 + +* 更新主键的代价很高,将会导致被更新的行移动,所以对于 InnoDB 表,一般定义主键为不可更新 +* 二级索引访问需要两次索引查找,第一次找到主键值,第二次根据主键值找到行数据 @@ -3333,420 +3896,4810 @@ DELIMITER: -##### 参数传递 +##### 辅助索引 -* 参数传递的语法 +在聚簇索引之上创建的索引称之为辅助索引,非聚簇索引都是辅助索引,像复合索引、前缀索引、唯一索引等 - IN:代表输入参数,需要由调用者传递实际数据,默认的 - OUT:代表输出参数,该参数可以作为返回值 - INOUT:代表既可以作为输入参数,也可以作为输出参数 +辅助索引叶子节点存储的是主键值,而不是数据的物理地址,所以访问数据需要二次查找,推荐使用覆盖索引,可以减少回表查询 - ```mysql - DELIMITER $ - - -- 标准语法 - CREATE PROCEDURE 存储过程名称([IN|OUT|INOUT] 参数名 数据类型) - BEGIN - 执行的sql语句; - END$ - - DELIMITER ; - ``` +**检索过程**:辅助索引找到主键值,再通过聚簇索引(二分)找到数据页,最后通过数据页中的 Page Directory(二分)找到对应的数据分组,遍历组内所所有的数据找到数据行 -* 输入总成绩变量,代表学生总成绩,输出分数描述变量,代表学生总成绩的描述 +补充:无索引走全表查询,查到数据页后和上述步骤一致 - ```mysql - DELIMITER $ + + +*** + + + +##### 索引实现 + +InnoDB 使用 B+Tree 作为索引结构,并且 InnoDB 一定有索引 + +主键索引: + +* 在 InnoDB 中,表数据文件本身就是按 B+Tree 组织的一个索引结构,这个索引的 key 是数据表的主键,叶子节点 data 域保存了完整的数据记录 + +* InnoDB 的表数据文件**通过主键聚集数据**,如果没有定义主键,会选择非空唯一索引代替,如果也没有这样的列,MySQL 会自动为 InnoDB 表生成一个**隐含字段 row_id** 作为主键,这个字段长度为 6 个字节,类型为长整形 + +辅助索引: + +* InnoDB 的所有辅助索引(二级索引)都引用主键作为 data 域 + +* InnoDB 表是基于聚簇索引建立的,因此 InnoDB 的索引能提供一种非常快速的主键查找性能。不过辅助索引也会包含主键列,所以不建议使用过长的字段作为主键,**过长的主索引会令辅助索引变得过大** + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-InnoDB聚簇和辅助索引结构.png) + + + +*** + + + +#### MyISAM + +##### 非聚簇 + +MyISAM 的主键索引使用的是非聚簇索引,索引文件和数据文件是分离的,**索引文件仅保存数据的地址** + +* 主键索引 B+ 树的节点存储了主键,辅助键索引 B+ 树存储了辅助键,表数据存储在独立的地方,这两颗 B+ 树的叶子节点都使用一个地址指向真正的表数据,对于表数据来说,这两个键没有任何差别 +* 由于索引树是独立的,通过辅助索引检索**无需回表查询**访问主键的索引树 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-聚簇索引和辅助索引检锁数据图.jpg) + + + +*** + + + +##### 索引实现 + +MyISAM 的索引方式也叫做非聚集的,之所以这么称呼是为了与 InnoDB 的聚集索引区分 + +主键索引:MyISAM 引擎使用 B+Tree 作为索引结构,叶节点的 data 域存放的是数据记录的地址 + +辅助索引:MyISAM 中主索引和辅助索引(Secondary key)在结构上没有任何区别,只是主索引要求 key 是唯一的,而辅助索引的 key 可以重复 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-MyISAM主键和辅助索引结构.png) + + + + + +参考文章:https://blog.csdn.net/lm1060891265/article/details/81482136 + + + +*** + + + +### 索引结构 + +#### 数据页 + +文件系统的最小单元是块(block),一个块的大小是 4K,系统从磁盘读取数据到内存时是以磁盘块为基本单位的,位于同一个磁盘块中的数据会被一次性读取出来,而不是需要什么取什么 + +InnoDB 存储引擎中有页(Page)的概念,页是 MySQL 磁盘管理的最小单位 + +* **InnoDB 存储引擎中默认每个页的大小为 16KB,索引中一个节点就是一个数据页**,所以会一次性读取 16KB 的数据到内存 +* InnoDB 引擎将若干个地址连接磁盘块,以此来达到页的大小 16KB +* 在查询数据时如果一个页中的每条数据都能有助于定位数据记录的位置,这将会减少磁盘 I/O 次数,提高查询效率 + +超过 16KB 的一条记录,主键索引页只会存储部分数据和指向**溢出页**的指针,剩余数据都会分散存储在溢出页中 + +数据页物理结构,从上到下: + +* File Header:上一页和下一页的指针、该页的类型(索引页、数据页、日志页等)、**校验和**、LSN(最近一次修改当前页面时的系统 lsn 值,事务持久性部分详解)等信息 +* Page Header:记录状态信息 +* Infimum + Supremum:当前页的最小记录和最大记录(头尾指针),Infimum 所在分组只有一条记录,Supremum 所在分组可以有 1 ~ 8 条记录,剩余的分组可以有 4 ~ 8 条记录 +* User Records:存储数据的记录 +* Free Space:尚未使用的存储空间 +* Page Directory:分组的目录,可以通过目录快速定位(二分法)数据的分组 +* File Trailer:检验和字段,在刷脏过程中,页首和页尾的校验和一致才能说明页面刷新成功,二者不同说明刷新期间发生了错误;LSN 字段,也是用来校验页面的完整性 + +数据页中包含数据行,数据的存储是基于数据行的,数据行有 next_record 属性指向下一个行数据,所以是可以遍历的,但是一组数据至多 8 个行,通过 Page Directory 先定位到组,然后遍历获取所需的数据行即可 + +数据行中有三个隐藏字段:trx_id、roll_pointer、row_id(在事务章节会详细介绍它们的作用) + + + +*** + + + +#### BTree + +BTree 的索引类型是基于 B+Tree 树型数据结构的,B+Tree 又是 BTree 数据结构的变种,用在数据库和操作系统中的文件系统,特点是能够保持数据稳定有序 + +BTree 又叫多路平衡搜索树,一颗 m 叉的 BTree 特性如下: + +- 树中每个节点最多包含 m 个孩子 +- 除根节点与叶子节点外,每个节点至少有 [ceil(m/2)] 个孩子 +- 若根节点不是叶子节点,则至少有两个孩子 +- 所有的叶子节点都在同一层 +- 每个非叶子节点由 n 个 key 与 n+1 个指针组成,其中 [ceil(m/2)-1] <= n <= m-1 + +5 叉,key 的数量 [ceil(m/2)-1] <= n <= m-1 为 2 <= n <=4 ,当 n>4 时中间节点分裂到父节点,两边节点分裂 + +插入 C N G A H E K Q M F W L T Z D P R X Y S 数据的工作流程: + +* 插入前 4 个字母 C N G A + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-BTree工作流程1.png) + +* 插入 H,n>4,中间元素 G 字母向上分裂到新的节点 + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-BTree工作流程2.png) + +* 插入 E、K、Q 不需要分裂 + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-BTree工作流程3.png) + +* 插入 M,中间元素 M 字母向上分裂到父节点 G + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-BTree工作流程4.png) + +* 插入 F,W,L,T 不需要分裂 + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-BTree工作流程5.png) + +* 插入 Z,中间元素 T 向上分裂到父节点中 + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-BTree工作流程6.png) + +* 插入 D,中间元素 D 向上分裂到父节点中,然后插入 P,R,X,Y 不需要分裂 + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-BTree工作流程7.png) + +* 最后插入 S,NPQR 节点 n>5,中间节点 Q 向上分裂,但分裂后父节点 DGMT 的 n>5,中间节点 M 向上分裂 + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-BTree工作流程8.png) + +BTree 树就已经构建完成了,BTree 树和二叉树相比, 查询数据的效率更高, 因为对于相同的数据量来说,**BTree 的层级结构比二叉树少**,所以搜索速度快 + +BTree 结构的数据可以让系统高效的找到数据所在的磁盘块,定义一条记录为一个二元组 [key, data] ,key 为记录的键值,对应表中的主键值,data 为一行记录中除主键外的数据。对于不同的记录,key 值互不相同,BTree 中的每个节点根据实际情况可以包含大量的关键字信息和分支 +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/索引的原理1.png) + +缺点:当进行范围查找时会出现回旋查找 + + + +*** + + + +#### B+Tree + +##### 数据结构 + +BTree 数据结构中每个节点中不仅包含数据的 key 值,还有 data 值。磁盘中每一页的存储空间是有限的,如果 data 数据较大时将会导致每个节点(即一个页)能存储的 key 的数量很小,当存储的数据量很大时同样会导致 B-Tree 的深度较大,增大查询时的磁盘 I/O 次数,进而影响查询效率,所以引入 B+Tree + +B+Tree 为 BTree 的变种,B+Tree 与 BTree 的区别为: + +* n 叉 B+Tree 最多含有 n 个 key(哈希值),而 BTree 最多含有 n-1 个 key + +- 所有**非叶子节点只存储键值 key** 信息,只进行数据索引,使每个非叶子节点所能保存的关键字大大增加 +- 所有**数据都存储在叶子节点**,所以每次数据查询的次数都一样 +- **叶子节点按照 key 大小顺序排列,左边结尾数据都会保存右边节点开始数据的指针,形成一个链表** +- 所有节点中的 key 在叶子节点中也存在(比如 5),**key 允许重复**,B 树不同节点不存在重复的 key + + + +B* 树:是 B+ 树的变体,在 B+ 树的非根和非叶子结点再增加指向兄弟的指针 + + + +*** + + + +##### 优化结构 + +MySQL 索引数据结构对经典的 B+Tree 进行了优化,在原 B+Tree 的基础上,增加一个指向相邻叶子节点的链表指针,就形成了带有顺序指针的 B+Tree,**提高区间访问的性能,防止回旋查找** + +区间访问的意思是访问索引为 5 - 15 的数据,可以直接根据相邻节点的指针遍历 + +B+ 树的**叶子节点是数据页**(page),一个页里面可以存多个数据行 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/索引的原理2.png) + +通常在 B+Tree 上有两个头指针,**一个指向根节点,另一个指向关键字最小的叶子节点**,而且所有叶子节点(即数据节点)之间是一种链式环结构。可以对 B+Tree 进行两种查找运算: + +- 有范围:对于主键的范围查找和分页查找 +- 有顺序:从根节点开始,进行随机查找,顺序查找 + +InnoDB 中每个数据页的大小默认是 16KB, + +* 索引行:一般表的主键类型为 INT(4 字节)或 BIGINT(8 字节),指针大小在 InnoDB 中设置为 6 字节节,也就是说一个页大概存储 16KB/(8B+6B)=1K 个键值(估值)。则一个深度为 3 的 B+Tree 索引可以维护 `10^3 * 10^3 * 10^3 = 10亿` 条记录 +* 数据行:一行数据的大小可能是 1k,一个数据页可以存储 16 行 + +实际情况中每个节点可能不能填充满,因此在数据库中,B+Tree 的高度一般都在 2-4 层。MySQL 的 InnoDB 存储引擎在设计时是**将根节点常驻内存的**,也就是说查找某一键值的行记录时最多只需要 1~3 次磁盘 I/O 操作 + +B+Tree 优点:提高查询速度,减少磁盘的 IO 次数,树形结构较小 + + + +*** + + + +##### 索引维护 + +B+ 树为了保持索引的有序性,在插入新值的时候需要做相应的维护 + +每个索引中每个块存储在磁盘页中,可能会出现以下两种情况: + +* 如果所在的数据页已经满了,这时候需要申请一个新的数据页,然后挪动部分数据过去,这个过程称为**页分裂**,原本放在一个页的数据现在分到两个页中,降低了空间利用率 +* 当相邻两个页由于删除了数据,利用率很低之后,会将数据页做**页合并**,合并的过程可以认为是分裂过程的逆过程 +* 这两个情况都是由 B+ 树的结构决定的 + +一般选用数据小的字段做索引,字段长度越小,普通索引的叶子节点就越小,普通索引占用的空间也就越小 + +自增主键的插入数据模式,可以让主键索引尽量地保持递增顺序插入,不涉及到挪动其他记录,**避免了页分裂**,页分裂的目的就是保证后一个数据页中的所有行主键值比前一个数据页中主键值大 + + + +参考文章:https://developer.aliyun.com/article/919861 + + + +*** + + + +### 设计原则 + +索引的设计可以遵循一些已有的原则,创建索引的时候请尽量考虑符合这些原则,便于提升索引的使用效率 + +创建索引时的原则: +- 对查询频次较高,且数据量比较大的表建立索引 +- 使用唯一索引,区分度越高,使用索引的效率越高 +- 索引字段的选择,最佳候选列应当从 where 子句的条件中提取,使用覆盖索引 +- 使用短索引,索引创建之后也是使用硬盘来存储的,因此提升索引访问的 I/O 效率,也可以提升总体的访问效率。假如构成索引的字段总长度比较短,那么在给定大小的存储块内可以存储更多的索引值,相应的可以有效的提升 MySQL 访问索引的 I/O 效率 +- 索引可以有效的提升查询数据的效率,但索引数量不是多多益善,索引越多,维护索引的代价越高。对于插入、更新、删除等 DML 操作比较频繁的表来说,索引过多,会引入相当高的维护代价,降低 DML 操作的效率,增加相应操作的时间消耗;另外索引过多的话,MySQL 也会犯选择困难病,虽然最终仍然会找到一个可用的索引,但提高了选择的代价 + +* MySQL 建立联合索引时会遵守**最左前缀匹配原则**,即最左优先,在检索数据时从联合索引的最左边开始匹配 - CREATE PROCEDURE pro_test6(IN total INT, OUT description VARCHAR(10)) - BEGIN - -- 判断总分数 - IF total >= 380 THEN - SET description = '学习优秀'; - ELSEIF total >= 320 AND total < 380 THEN - SET description = '学习不错'; - ELSE - SET description = '学习一般'; - END IF; - END$ + N 个列组合而成的组合索引,相当于创建了 N 个索引,如果查询时 where 句中使用了组成该索引的**前**几个字段,那么这条查询 SQL 可以利用组合索引来提升查询效率 - DELIMITER ; - -- 调用pro_test6存储过程 - CALL pro_test6(310,@description); - CALL pro_test6((SELECT SUM(score) FROM student), @description); - -- 查询总成绩描述 - SELECT @description; + ```mysql + -- 对name、address、phone列建一个联合索引 + ALTER TABLE user ADD INDEX index_three(name,address,phone); + -- 查询语句执行时会依照最左前缀匹配原则,检索时分别会使用索引进行数据匹配。 + (name,address,phone) + (name,address) + (name,phone) -- 只有name字段走了索引 + (name) + + -- 索引的字段可以是任意顺序的,优化器会帮助我们调整顺序,下面的SQL语句可以命中索引 + SELECT * FROM user WHERE address = '北京' AND phone = '12345' AND name = '张三'; + ``` + + ```mysql + -- 如果联合索引中最左边的列不包含在条件查询中,SQL语句就不会命中索引,比如: + SELECT * FROM user WHERE address = '北京' AND phone = '12345'; ``` -* 查看参数方法 +哪些情况不要建立索引: + +* 记录太少的表 +* 经常增删改的表 +* 频繁更新的字段不适合创建索引 +* where 条件里用不到的字段不创建索引 + + + +*** + + + +### 索引优化 + +#### 覆盖索引 + +覆盖索引:包含所有满足查询需要的数据的索引(SELECT 后面的字段刚好是索引字段),可以利用该索引返回 SELECT 列表的字段,而不必根据索引去聚簇索引上读取数据文件 + +回表查询:要查找的字段不在非主键索引树上时,需要通过叶子节点的主键值去主键索引上获取对应的行数据 + +使用覆盖索引,防止回表查询: + +* 表 user 主键为 id,普通索引为 age,查询语句: + + ```mysql + SELECT * FROM user WHERE age = 30; + ``` + + 查询过程:先通过普通索引 age=30 定位到主键值 id=1,再通过聚集索引 id=1 定位到行记录数据,需要两次扫描 B+ 树 + +* 使用覆盖索引: + + ```mysql + DROP INDEX idx_age ON user; + CREATE INDEX idx_age_name ON user(age,name); + SELECT id,age FROM user WHERE age = 30; + ``` + + 在一棵索引树上就能获取查询所需的数据,无需回表速度更快 + +使用覆盖索引,要注意 SELECT 列表中只取出需要的列,不可用 SELECT *,所有字段一起做索引会导致索引文件过大,查询性能下降 + + + +*** + + + +#### 索引下推 + +索引条件下推优化(Index Condition Pushdown,ICP)是 MySQL5.6 添加,可以在索引遍历过程中,对索引中包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表次数 + +索引下推充分利用了索引中的数据,在查询出整行数据之前过滤掉无效的数据,再去主键索引树上查找 + +* 不使用索引下推优化时存储引擎通过索引检索到数据,然后回表查询记录返回给 Server 层,**服务器判断数据是否符合条件** + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-不使用索引下推.png) +* 使用索引下推优化时,如果**存在某些被索引的列的判断条件**时,由存储引擎在索引遍历的过程中判断数据是否符合传递的条件,将符合条件的数据进行回表,检索出来返回给服务器,由此减少 IO 次数 + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-使用索引下推.png) + +**适用条件**: + +* 需要存储引擎将索引中的数据与条件进行判断(所以**条件列必须都在同一个索引中**),所以优化是基于存储引擎的,只有特定引擎可以使用,适用于 InnoDB 和 MyISAM +* 存储引擎没有调用跨存储引擎的能力,跨存储引擎的功能有存储过程、触发器、视图,所以调用这些功能的不可以进行索引下推优化 +* 对于 InnoDB 引擎只适用于二级索引,InnoDB 的聚簇索引会将整行数据读到缓冲区,不再需要去回表查询了 + +工作过程:用户表 user,(name, age) 是联合索引 + +```mysql +SELECT * FROM user WHERE name LIKE '张%' AND age = 10; -- 头部模糊匹配会造成索引失效 +``` + +* 优化前:在非主键索引树上找到满足第一个条件的行,然后通过叶子节点记录的主键值再回到主键索引树上查找到对应的行数据,再对比 AND 后的条件是否符合,符合返回数据,需要 4 次回表 + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-索引下推优化1.png) + +* 优化后:检查索引中存储的列信息是否符合索引条件,然后交由存储引擎用剩余的判断条件判断此行数据是否符合要求,**不满足条件的不去读取表中的数据**,满足下推条件的就根据主键值进行回表查询,2 次回表 + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-索引下推优化2.png) + +当使用 EXPLAIN 进行分析时,如果使用了索引条件下推,Extra 会显示 Using index condition + + + +参考文章:https://blog.csdn.net/sinat_29774479/article/details/103470244 + +参考文章:https://time.geekbang.org/column/article/69636 + + + +*** + + + +#### 前缀索引 + +当要索引的列字符很多时,索引会变大变慢,可以只索引列开始的部分字符串,节约索引空间,提高索引效率 + +注意:使用前缀索引就系统就忽略覆盖索引对查询性能的优化了 + +优化原则:**降低重复的索引值** + +比如地区表: + +```mysql +area gdp code +chinaShanghai 100 aaa +chinaDalian 200 bbb +usaNewYork 300 ccc +chinaFuxin 400 ddd +chinaBeijing 500 eee +``` + +发现 area 字段很多都是以 china 开头的,那么如果以前 1-5 位字符做前缀索引就会出现大量索引值重复的情况,索引值重复性越低,查询效率也就越高,所以需要建立前 6 位字符的索引: + +```mysql +CREATE INDEX idx_area ON table_name(area(7)); +``` + +场景:存储身份证 + +* 直接创建完整索引,这样可能比较占用空间 +* 创建前缀索引,节省空间,但会增加查询扫描次数,并且不能使用覆盖索引 +* 倒序存储,再创建前缀索引,用于绕过字符串本身前缀的区分度不够的问题(前 6 位相同的很多) +* 创建 hash 字段索引,查询性能稳定,有额外的存储和计算消耗,跟第三种方式一样,都不支持范围扫描 + + + +**** + + + +#### 索引合并 + +使用多个索引来完成一次查询的执行方法叫做索引合并 index merge + +* Intersection 索引合并: + + ```sql + SELECT * FROM table_test WHERE key1 = 'a' AND key3 = 'b'; # key1 和 key3 列都是单列索引、二级索引 + ``` + + 从不同索引中扫描到的记录的 id 值取**交集**(相同 id),然后执行回表操作,要求从每个二级索引获取到的记录都是按照主键值排序 + +* Union 索引合并: + + ```sql + SELECT * FROM table_test WHERE key1 = 'a' OR key3 = 'b'; + ``` + + 从不同索引中扫描到的记录的 id 值取**并集**,然后执行回表操作,要求从每个二级索引获取到的记录都是按照主键值排序 + +* Sort-Union 索引合并 + + ```sql + SELECT * FROM table_test WHERE key1 < 'a' OR key3 > 'b'; + ``` + + 先将从不同索引中扫描到的记录的主键值进行排序,再按照 Union 索引合并的方式进行查询 + +索引合并算法的效率并不好,通过将其中的一个索引改成联合索引会优化效率 + + + + + +*** + + + + + +## 系统优化 + +### 表优化 + +#### 分区表 + +##### 基本介绍 + +分区表是将大表的数据按分区字段分成许多小的子集,建立一个以 ftime 年份为分区的表: + +```mysql +CREATE TABLE `t` ( + `ftime` datetime NOT NULL, + `c` int(11) DEFAULT NULL, + KEY (`ftime`) +) ENGINE=InnoDB DEFAULT CHARSET=latin1 +PARTITION BY RANGE (YEAR(ftime)) +(PARTITION p_2017 VALUES LESS THAN (2017) ENGINE = InnoDB, + PARTITION p_2018 VALUES LESS THAN (2018) ENGINE = InnoDB, + PARTITION p_2019 VALUES LESS THAN (2019) ENGINE = InnoDB, + PARTITION p_others VALUES LESS THAN MAXVALUE ENGINE = InnoDB); +INSERT INTO t VALUES('2017-4-1',1),('2018-4-1',1);-- 这两行记录分别落在 p_2018 和 p_2019 这两个分区上 +``` + +这个表包含了一个.frm 文件和 4 个.ibd 文件,每个分区对应一个.ibd 文件 + +* 对于引擎层来说,这是 4 个表,针对每个分区表的操作不会相互影响 +* 对于 Server 层来说,这是 1 个表 + + + +*** + + + +##### 分区策略 + +打开表行为:第一次访问一个分区表时,MySQL 需要**把所有的分区都访问一遍**,如果分区表的数量很多,超过了 open_files_limit 参数(默认值 1024),那么就会在访问这个表时打开所有的文件,导致打开表文件的个数超过了上限而报错 + +通用分区策略:MyISAM 分区表使用的分区策略,每次访问分区都由 Server 层控制,在文件管理、表管理的实现上很粗糙,因此有比较严重的性能问题 + +本地分区策略:从 MySQL 5.7.9 开始,InnoDB 引擎内部自己管理打开分区的行为,InnoDB 引擎打开文件超过 innodb_open_files 时就会**关掉一些之前打开的文件**,所以即使分区个数大于 open_files_limit,也不会报错 + +从 MySQL 8.0 版本开始,就不允许创建 MyISAM 分区表,只允许创建已经实现了本地分区策略的引擎,目前只有 InnoDB 和 NDB 这两个引擎支持了本地分区策略 + + + +*** + + + +##### Server 层 + +从 Server 层看一个分区表就只是一个表 + +* Session A: + + ```mysql + SELECT * FROM t WHERE ftime = '2018-4-1'; + ``` + +* Session B: + + ```mysql + ALTER TABLE t TRUNCATE PARTITION p_2017; -- blocked + ``` + +现象:Session B 只操作 p_2017 分区,但是由于 Session A 持有整个表 t 的 MDL 读锁,就导致 B 的 ALTER 语句获取 MDL 写锁阻塞 + +分区表的特点: + +* 第一次访问的时候需要访问所有分区 +* 在 Server 层认为这是同一张表,因此**所有分区共用同一个 MDL 锁** +* 在引擎层认为这是不同的表,因此 MDL 锁之后的执行过程,会根据分区表规则,只访问需要的分区 + + + +*** + + + +##### 应用场景 + +分区表的优点: + +* 对业务透明,相对于用户分表来说,使用分区表的业务代码更简洁 + +* 分区表可以很方便的清理历史数据。按照时间分区的分区表,就可以直接通过 `alter table t drop partition` 这个语法直接删除分区文件,从而删掉过期的历史数据,与使用 drop 语句删除数据相比,优势是速度快、对系统影响小 + +使用分区表,不建议创建太多的分区,注意事项: + +* 分区并不是越细越好,单表或者单分区的数据一千万行,只要没有特别大的索引,对于现在的硬件能力来说都已经是小表 +* 分区不要提前预留太多,在使用之前预先创建即可。比如是按月分区,每年年底时再把下一年度的 12 个新分区创建上即可,并且对于没有数据的历史分区,要及时的 drop 掉 + + + +参考文档:https://time.geekbang.org/column/article/82560 + + + +*** + + + +#### 临时表 + +##### 基本介绍 + +临时表分为内部临时表和用户临时表 + +* 内部临时表:系统执行 SQL 语句优化时产生的表,例如 Join 连接查询、去重查询等 + +* 用户临时表:用户主动创建的临时表 + + ```mysql + CREATE TEMPORARY TABLE temp_t like table_1; + ``` + +临时表可以是内存表,也可以是磁盘表(多表操作 → 嵌套查询章节提及) + +* 内存表指的是使用 Memory 引擎的表,建立哈希索引,建表语法是 `create table … engine=memory`,这种表的数据都保存在内存里,系统重启时会被清空,但是表结构还在 +* 磁盘表是使用 InnoDB 引擎或者 MyISAM 引擎的临时表,建立 B+ 树索引,写数据的时候是写到磁盘上的 + +临时表的特点: + +* 一个临时表只能被创建它的 session 访问,对其他线程不可见,所以不同 session 的临时表是**可以重名**的 +* 临时表可以与普通表同名,会话内有同名的临时表和普通表时,执行 show create 语句以及增删改查语句访问的都是临时表 +* show tables 命令不显示临时表 +* 数据库发生异常重启不需要担心数据删除问题,临时表会**自动回收** + + + +*** + + + +##### 重名原理 + +执行创建临时表的 SQL: + +```mysql +create temporary table temp_t(id int primary key)engine=innodb; +``` + +MySQL 给 InnoDB 表创建一个 frm 文件保存表结构定义,在 ibd 保存表数据。frm 文件放在临时文件目录下,文件名的后缀是 .frm,**前缀是** `#sql{进程 id}_{线程 id}_ 序列号`,使用 `select @@tmpdir` 命令,来显示实例的临时文件目录 + +MySQL 维护数据表,除了物理磁盘上的文件外,内存里也有一套机制区别不同的表,每个表都对应一个 table_def_key + +* 一个普通表的 table_def_key 的值是由 `库名 + 表名` 得到的,所以如果在同一个库下创建两个同名的普通表,创建第二个表的过程中就会发现 table_def_key 已经存在了 +* 对于临时表,table_def_key 在 `库名 + 表名` 基础上,又加入了 `server_id + thread_id`,所以不同线程之间,临时表可以重名 + +实现原理:每个线程都维护了自己的临时表链表,每次 session 内操作表时,先遍历链表,检查是否有这个名字的临时表,如果有就**优先操作临时表**,如果没有再操作普通表;在 session 结束时对链表里的每个临时表,执行 `DROP TEMPORARY TABLE + 表名` 操作 + +执行 rename table 语句无法修改临时表,因为会按照 `库名 / 表名.frm` 的规则去磁盘找文件,但是临时表文件名的规则是 `#sql{进程 id}_{线程 id}_ 序列号.frm`,因此会报找不到文件名的错误 + + + +**** + + + +##### 主备复制 + +创建临时表的语句会传到备库执行,因此备库的同步线程就会创建这个临时表。主库在线程退出时会自动删除临时表,但备库同步线程是持续在运行的并不会退出,所以这时就需要在主库上再写一个 DROP TEMPORARY TABLE 传给备库执行 + +binlog 日志写入规则: + +* binlog_format=row,跟临时表有关的语句就不会记录到 binlog +* binlog_format=statment/mixed,binlog 中才会记录临时表的操作,也就会记录 `DROP TEMPORARY TABLE` 这条命令 + +主库上不同的线程创建同名的临时表是不冲突的,但是备库只有一个执行线程,所以 MySQL 在记录 binlog 时会把主库执行这个语句的线程 id 写到 binlog 中,在备库的应用线程就可以获取执行每个语句的主库线程 id,并利用这个线程 id 来构造临时表的 table_def_key + +* session A 的临时表 t1,在备库的 table_def_key 就是:`库名 + t1 +“M 的 serverid" + "session A 的 thread_id”` +* session B 的临时表 t1,在备库的 table_def_key 就是 :`库名 + t1 +"M 的 serverid" + "session B 的 thread_id"` + +MySQL 在记录 binlog 的时不论是 create table 还是 alter table 语句都是原样记录,但是如果执行 drop table,系统记录 binlog 就会被服务端改写 + +```mysql +DROP TABLE `t_normal` /* generated by server */ +``` + + + +*** + + + +##### 跨库查询 + +分库分表系统的跨库查询使用临时表不用担心线程之间的重名冲突,分库分表就是要把一个逻辑上的大表分散到不同的数据库实例上 + +比如将一个大表 ht,按照字段 f,拆分成 1024 个分表,分布到 32 个数据库实例上,一般情况下都有一个中间层 proxy 解析 SQL 语句,通过分库规则通过分表规则(比如 N%1024)确定将这条语句路由到哪个分表做查询 + +```mysql +select v from ht where f=N; +``` + +如果这个表上还有另外一个索引 k,并且查询语句: + +```mysql +select v from ht where k >= M order by t_modified desc limit 100; +``` + +查询条件里面没有用到分区字段 f,只能**到所有的分区**中去查找满足条件的所有行,然后统一做 order by 操作,两种方式: + +* 在 proxy 层的进程代码中实现排序,拿到分库的数据以后,直接在内存中参与计算,但是对 proxy 端的压力比较大,很容易出现内存不够用和 CPU 瓶颈问题 +* 把各个分库拿到的数据,汇总到一个 MySQL 实例的一个表中,然后在这个汇总实例上做逻辑操作,执行流程: + * 在汇总库上创建一个临时表 temp_ht,表里包含三个字段 v、k、t_modified + * 在各个分库执行:`select v,k,t_modified from ht_x where k >= M order by t_modified desc limit 100` + * 把分库执行的结果插入到 temp_ht 表中 + * 在临时表上执行:`select v from temp_ht order by t_modified desc limit 100` + + + + + +*** + + + +### 优化步骤 + +#### 执行频率 + +MySQL 客户端连接成功后,查询服务器状态信息: + +```mysql +SHOW [SESSION|GLOBAL] STATUS LIKE ''; +-- SESSION: 显示当前会话连接的统计结果,默认参数 +-- GLOBAL: 显示自数据库上次启动至今的统计结果 +``` + +* 查看 SQL 执行频率: + + ```mysql + SHOW STATUS LIKE 'Com_____'; + ``` + + Com_xxx 表示每种语句执行的次数 + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-SQL语句执行频率.png) + +* 查询 SQL 语句影响的行数: + + ```mysql + SHOW STATUS LIKE 'Innodb_rows_%'; + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-SQL语句影响的行数.png) + +Com_xxxx:这些参数对于所有存储引擎的表操作都会进行累计 + +Innodb_xxxx:这几个参数只是针对 InnoDB 存储引擎的,累加的算法也略有不同 + +| 参数 | 含义 | +| :------------------- | ------------------------------------------------------------ | +| Com_select | 执行 SELECT 操作的次数,一次查询只累加 1 | +| Com_insert | 执行 INSERT 操作的次数,对于批量插入的 INSERT 操作,只累加一次 | +| Com_update | 执行 UPDATE 操作的次数 | +| Com_delete | 执行 DELETE 操作的次数 | +| Innodb_rows_read | 执行 SELECT 查询返回的行数 | +| Innodb_rows_inserted | 执行 INSERT 操作插入的行数 | +| Innodb_rows_updated | 执行 UPDATE 操作更新的行数 | +| Innodb_rows_deleted | 执行 DELETE 操作删除的行数 | +| Connections | 试图连接 MySQL 服务器的次数 | +| Uptime | 服务器工作时间 | +| Slow_queries | 慢查询的次数 | + + + +**** + + + +#### 定位低效 + +SQL 执行慢有两种情况: + +* 偶尔慢:DB 在刷新脏页(学完事务就懂了) + * redo log 写满了 + * 内存不够用,要从 LRU 链表中淘汰 + * MySQL 认为系统空闲的时候 + * MySQL 关闭时 +* 一直慢的原因:索引没有设计好、SQL 语句没写好、MySQL 选错了索引 + +通过以下两种方式定位执行效率较低的 SQL 语句 + +* 慢日志查询: 慢查询日志在查询结束以后才记录,执行效率出现问题时查询日志并不能定位问题 + + 配置文件修改:修改 .cnf 文件 `vim /etc/mysql/my.cnf`,重启 MySQL 服务器 + + ```sh + slow_query_log=ON + slow_query_log_file=/usr/local/mysql/var/localhost-slow.log + long_query_time=1 #记录超过long_query_time秒的SQL语句的日志 + log-queries-not-using-indexes = 1 + ``` + + 使用命令配置: + + ```mysql + mysql> SET slow_query_log=ON; + mysql> SET GLOBAL slow_query_log=ON; + ``` + + 查看是否配置成功: + + ```mysql + SHOW VARIABLES LIKE '%query%' + ``` + +* SHOW PROCESSLIST:**实时查看**当前 MySQL 在进行的连接线程,包括线程的状态、是否锁表、SQL 的执行情况,同时对一些锁表操作进行优化 + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-SHOW_PROCESSLIST命令.png) + + + + + + +*** + + + +#### EXPLAIN + +##### 执行计划 + +通过 EXPLAIN 命令获取执行 SQL 语句的信息,包括在 SELECT 语句执行过程中如何连接和连接的顺序,执行计划在优化器优化完成后、执行器之前生成,然后执行器会调用存储引擎检索数据 + +查询 SQL 语句的执行计划: + +```mysql +EXPLAIN SELECT * FROM table_1 WHERE id = 1; +``` + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-explain查询SQL语句的执行计划.png) + +| 字段 | 含义 | +| ------------- | ------------------------------------------------------------ | +| id | SELECT 的序列号 | +| select_type | 表示 SELECT 的类型 | +| table | 访问数据库中表名称,有时可能是简称或者临时表名称() | +| type | 表示表的连接类型 | +| possible_keys | 表示查询时,可能使用的索引 | +| key | 表示实际使用的索引 | +| key_len | 索引字段的长度 | +| ref | 表示与索引列进行等值匹配的对象,常数、某个列、函数等,type 必须在(range, const] 之间,左闭右开 | +| rows | 扫描出的行数,表示 MySQL 根据表统计信息及索引选用情况,**估算**的找到所需的记录扫描的行数 | +| filtered | 条件过滤的行百分比,单表查询没意义,用于连接查询中对驱动表的扇出进行过滤,查询优化器预测所有扇出值满足剩余查询条件的百分比,相乘以后表示多表查询中还要对被驱动执行查询的次数 | +| extra | 执行情况的说明和描述 | + +MySQL **执行计划的局限**: + +* 只是计划,不是执行 SQL 语句,可以随着底层优化器输入的更改而更改 +* EXPLAIN 不会告诉显示关于触发器、存储过程的信息对查询的影响情况, 不考虑各种 Cache +* EXPLAIN 不能显示 MySQL 在执行查询时的动态,因为执行计划在执行**查询之前生成** +* EXPALIN 只能解释 SELECT 操作,其他操作要重写为 SELECT 后查看执行计划 +* EXPLAIN PLAN 显示的是在解释语句时数据库将如何运行 SQL 语句,由于执行环境和 EXPLAIN PLAN 环境的不同,此计划可能与 SQL 语句**实际的执行计划不同**,部分统计信息是估算的,并非精确值 + +SHOW WARINGS:在使用 EXPALIN 命令后执行该语句,可以查询与执行计划相关的拓展信息,展示出 Level、Code、Message 三个字段,当 Code 为 1003 时,Message 字段展示的信息类似于将查询语句重写后的信息,但是不是等价,不能执行复制过来运行 + +环境准备: + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-执行计划环境准备.png) + + + + + +*** + + + +##### id + +id 代表 SQL 执行的顺序的标识,每个 SELECT 关键字对应一个唯一 id,所以在同一个 SELECT 关键字中的表的 id 都是相同的。SELECT 后的 FROM 可以跟随多个表,每个表都会对应一条记录,这些记录的 id 都是相同的, + +* id 相同时,执行顺序由上至下。连接查询的执行计划,记录的 id 值都是相同的,出现在前面的表为驱动表,后面为被驱动表 + + ```mysql + EXPLAIN SELECT * FROM t_role r, t_user u, user_role ur WHERE r.id = ur.role_id AND u.id = ur.user_id ; + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-explain之id相同.png) + +* id 不同时,id 值越大优先级越高,越先被执行 + + ```mysql + EXPLAIN SELECT * FROM t_role WHERE id = (SELECT role_id FROM user_role WHERE user_id = (SELECT id FROM t_user WHERE username = 'stu1')) + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-explain之id不同.png) + +* id 有相同也有不同时,id 相同的可以认为是一组,从上往下顺序执行;在所有的组中,id 的值越大的组,优先级越高,越先执行 + + ```mysql + EXPLAIN SELECT * FROM t_role r , (SELECT * FROM user_role ur WHERE ur.`user_id` = '2') a WHERE r.id = a.role_id ; + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-explain之id相同和不同.png) + +* id 为 NULL 时代表的是临时表 + + + +*** + + + +##### select + +表示查询中每个 select 子句的类型(简单 OR 复杂) + +| select_type | 含义 | +| ------------------ | ------------------------------------------------------------ | +| SIMPLE | 简单的 SELECT 查询,查询中不包含子查询或者 UNION | +| PRIMARY | 查询中若包含任何复杂的子查询,最外层(也就是最左侧)查询标记为该标识 | +| UNION | 对于 UNION 或者 UNION ALL 的复杂查询,除了最左侧的查询,其余的小查询都是 UNION | +| UNION RESULT | UNION 需要使用临时表进行去重,临时表的是 UNION RESULT | +| DEPENDENT UNION | 对于 UNION 或者 UNION ALL 的复杂查询,如果各个小查询都依赖外层查询,是相关子查询,除了最左侧的小查询为 DEPENDENT SUBQUERY,其余都是 DEPENDENT UNION | +| SUBQUERY | 子查询不是相关子查询,该子查询第一个 SELECT 代表的查询就是这种类型,会进行物化(该子查询只需要执行一次) | +| DEPENDENT SUBQUERY | 子查询是相关子查询,该子查询第一个 SELECT 代表的查询就是这种类型,不会物化(该子查询需要执行多次) | +| DERIVED | 在 FROM 列表中包含的子查询,被标记为 DERIVED(衍生),也就是生成物化派生表的这个子查询 | +| MATERIALIZED | 将子查询物化后与与外层进行连接查询,生成物化表的子查询 | + +子查询为 DERIVED:`SELECT * FROM (SELECT key1 FROM t1) AS derived_1 WHERE key1 > 10` + +子查询为 MATERIALIZED:`SELECT * FROM t1 WHERE key1 IN (SELECT key1 FROM t2)` + + + +**** + + + +##### type + +对表的访问方式,表示 MySQL 在表中找到所需行的方式,又称访问类型 + +| type | 含义 | +| --------------- | ------------------------------------------------------------ | +| ALL | 全表扫描,如果是 InnoDB 引擎是扫描聚簇索引 | +| index | 可以使用覆盖索引,但需要扫描全部索引 | +| range | 索引范围扫描,常见于 between、<、> 等的查询 | +| index_subquery | 子查询可以普通索引,则子查询的 type 为 index_subquery | +| unique_subquery | 子查询可以使用主键或唯一二级索引,则子查询的 type 为 index_subquery | +| index_merge | 索引合并 | +| ref_or_null | 非唯一性索引(普通二级索引)并且可以存储 NULL,进行等值匹配 | +| ref | 非唯一性索引与常量等值匹配 | +| eq_ref | 唯一性索引(主键或不存储 NULL 的唯一二级索引)进行等值匹配,如果二级索引是联合索引,那么所有联合的列都要进行等值匹配 | +| const | 通过主键或者唯一二级索引与常量进行等值匹配 | +| system | system 是 const 类型的特例,当查询的表只有一条记录的情况下,使用 system | +| NULL | MySQL 在优化过程中分解语句,执行时甚至不用访问表或索引 | + +从上到下,性能从差到好,一般来说需要保证查询至少达到 range 级别, 最好达到 ref + + + +*** + + + +##### key + +possible_keys: + +* 指出 MySQL 能使用哪个索引在表中找到记录,查询涉及到的字段上若存在索引,则该索引将被列出,但不一定被查询使用 +* 如果该列是 NULL,则没有相关的索引 + +key: + +* 显示 MySQL 在查询中实际使用的索引,若没有使用索引,显示为 NULL +* 查询中若使用了**覆盖索引**,则该索引可能出现在 key 列表,不出现在 possible_keys + +key_len: + +* 表示索引中使用的字节数,可通过该列计算查询中使用的索引的长度 +* key_len 显示的值为索引字段的最大可能长度,并非实际使用长度,即 key_len 是根据表定义计算而得,不是通过表内检索出的 +* 在不损失精确性的前提下,长度越短越好 + + + +*** + + + +##### Extra + +其他的额外的执行计划信息,在该列展示: + +* No tables used:查询语句中使用 FROM dual 或者没有 FROM 语句 +* Impossible WHERE:查询语句中的 WHERE 子句条件永远为 FALSE,会导致没有符合条件的行 +* Using index:该值表示相应的 SELECT 操作中使用了**覆盖索引**(Covering Index) +* Using index condition:第一种情况是搜索条件中虽然出现了索引列,但是部分条件无法形成扫描区间(**索引失效**),会根据可用索引的条件先搜索一遍再匹配无法使用索引的条件,回表查询数据;第二种是使用了**索引条件下推**优化 +* Using where:搜索的数据需要在 Server 层判断,无法使用索引下推 +* Using join buffer:连接查询被驱动表无法利用索引,需要连接缓冲区来存储中间结果 +* Using filesort:无法利用索引完成排序(优化方向),需要对数据使用外部排序算法,将取得的数据在内存或磁盘中进行排序 +* Using temporary:表示 MySQL 需要使用临时表来存储结果集,常见于**排序、去重(UNION)、分组**等场景 +* Select tables optimized away:说明仅通过使用索引,优化器可能仅从聚合函数结果中返回一行 +* No tables used:Query 语句中使用 from dual 或不含任何 from 子句 + + + +参考文章:https://www.cnblogs.com/ggjucheng/archive/2012/11/11/2765237.html + + + +**** + + + +#### PROFILES + +SHOW PROFILES 能够在做 SQL 优化时分析当前会话中语句执行的**资源消耗**情况 + +* 通过 have_profiling 参数,能够看到当前 MySQL 是否支持 profile: + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-have_profiling.png) + +* 默认 profiling 是关闭的,可以通过 set 语句在 Session 级别开启 profiling: + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-profiling.png) + + ```mysql + SET profiling=1; #开启profiling 开关; + ``` + +* 执行 SHOW PROFILES 指令, 来查看 SQL 语句执行的耗时: + + ```mysql + SHOW PROFILES; + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-查看SQL语句执行耗时.png) + +* 查看到该 SQL 执行过程中每个线程的状态和消耗的时间: + + ```mysql + SHOW PROFILE FOR QUERY query_id; + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-SQL执行每个状态消耗的时间.png) + +* 在获取到最消耗时间的线程状态后,MySQL 支持选择 all、cpu、block io 、context switch、page faults 等类型查看 MySQL 在使用什么资源上耗费了过高的时间。例如,选择查看 CPU 的耗费时间: + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-SQL执行每个状态消耗的CPU.png) + + * Status:SQL 语句执行的状态 + * Durationsql:执行过程中每一个步骤的耗时 + * CPU_user:当前用户占有的 CPU + * CPU_system:系统占有的 CPU + + + +*** + + + +#### TRACE + +MySQL 提供了对 SQL 的跟踪, 通过 trace 文件可以查看优化器**生成执行计划的过程** + +* 打开 trace 功能,设置格式为 JSON,并设置 trace 的最大使用内存,避免解析过程中因默认内存过小而不能够完整展示 + + ```mysql + SET optimizer_trace="enabled=on",end_markers_in_json=ON; -- 会话内有效 + SET optimizer_trace_max_mem_size=1000000; + ``` + +* 执行 SQL 语句: + + ```mysql + SELECT * FROM tb_item WHERE id < 4; + ``` + +* 检查 information_schema.optimizer_trace: + + ```mysql + SELECT * FROM information_schema.optimizer_trace \G; -- \G代表竖列展示 + ``` + + 执行信息主要有三个阶段:prepare 阶段、optimize 阶段(成本分析)、execute 阶段(执行) + + + + + +**** + + + +### 索引优化 + +#### 创建索引 + +索引是数据库优化最重要的手段之一,通过索引通常可以帮助用户解决大多数的 MySQL 的性能优化问题 + +```mysql +CREATE TABLE `tb_seller` ( + `sellerid` varchar (100), + `name` varchar (100), + `nickname` varchar (50), + `password` varchar (60), + `status` varchar (1), + `address` varchar (100), + `createtime` datetime, + PRIMARY KEY(`sellerid`) +)ENGINE=INNODB DEFAULT CHARSET=utf8mb4; +INSERT INTO `tb_seller` (`sellerid`, `name`, `nickname`, `password`, `status`, `address`, `createtime`) values('xiaomi','小米科技','小米官方旗舰店','e10adc3949ba59abbe56e057f20f883e','1','西安市','2088-01-01 12:00:00'); +CREATE INDEX idx_seller_name_sta_addr ON tb_seller(name, status, address); # 联合索引 +``` + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL使用索引环境准备.png) + + + +**** + + + +#### 避免失效 + +##### 语句错误 + +* 全值匹配:对索引中所有列都指定具体值,这种情况索引生效,执行效率高 + + ```mysql + EXPLAIN SELECT * FROM tb_seller WHERE name='小米科技' AND status='1' AND address='西安市'; + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL使用索引1.png) + +* **最左前缀法则**:联合索引遵守最左前缀法则 + + 匹配最左前缀法则,走索引: + + ```mysql + EXPLAIN SELECT * FROM tb_seller WHERE name='小米科技'; + EXPLAIN SELECT * FROM tb_seller WHERE name='小米科技' AND status='1'; + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL使用索引2.png) + + 违法最左前缀法则 , 索引失效: + + ```mysql + EXPLAIN SELECT * FROM tb_seller WHERE status='1'; + EXPLAIN SELECT * FROM tb_seller WHERE status='1' AND address='西安市'; + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL使用索引3.png) + + 如果符合最左法则,但是出现跳跃某一列,只有最左列索引生效: + + ```mysql + EXPLAIN SELECT * FROM tb_seller WHERE name='小米科技' AND address='西安市'; + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL使用索引4.png) + + 虽然索引列失效,但是系统会**使用了索引下推进行了优化** + +* **范围查询**右边的列,不能使用索引: + + ```mysql + EXPLAIN SELECT * FROM tb_seller WHERE name='小米科技' AND status>'1' AND address='西安市'; + ``` + + 根据前面的两个字段 name , status 查询是走索引的, 但是最后一个条件 address 没有用到索引,使用了索引下推 + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL使用索引5.png) + +* 在索引列上**函数或者运算(+ - 数值)操作**, 索引将失效:会破坏索引值的有序性 + + ```mysql + EXPLAIN SELECT * FROM tb_seller WHERE SUBSTRING(name,3,2) = '科技'; + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL使用索引6.png) + +* **字符串不加单引号**,造成索引失效:隐式类型转换,当字符串和数字比较时会**把字符串转化为数字** + + 没有对字符串加单引号,查询优化器会调用 CAST 函数将 status 转换为 int 进行比较,造成索引失效 + + ```mysql + EXPLAIN SELECT * FROM tb_seller WHERE name='小米科技' AND status = 1; + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL使用索引7.png) + + 如果 status 是 int 类型,SQL 为 `SELECT * FROM tb_seller WHERE status = '1' ` 并不会造成索引失效,因为会将 `'1'` 转换为 `1`,并**不会对索引列产生操作** + +* 多表连接查询时,如果两张表的**字符集不同**,会造成索引失效,因为会进行类型转换 + + 解决方法:CONVERT 函数是加在输入参数上、修改表的字符集 + +* **用 OR 分割条件,索引失效**,导致全表查询: + + OR 前的条件中的列有索引而后面的列中没有索引或 OR 前后两个列是同一个复合索引,都造成索引失效 + + ```mysql + EXPLAIN SELECT * FROM tb_seller WHERE name='阿里巴巴' OR createtime = '2088-01-01 12:00:00'; + EXPLAIN SELECT * FROM tb_seller WHERE name='小米科技' OR status='1'; + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL使用索引10.png) + + **AND 分割的条件不影响**: + + ```mysql + EXPLAIN SELECT * FROM tb_seller WHERE name='阿里巴巴' AND createtime = '2088-01-01 12:00:00'; + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL使用索引11.png) + +* **以 % 开头的 LIKE 模糊查询**,索引失效: + + 如果是尾部模糊匹配,索引不会失效;如果是头部模糊匹配,索引失效 + + ```mysql + EXPLAIN SELECT * FROM tb_seller WHERE name like '%科技%'; + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL使用索引12.png) + + 解决方案:通过覆盖索引来解决 + + ```mysql + EXPLAIN SELECT sellerid,name,status FROM tb_seller WHERE name like '%科技%'; + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL使用索引13.png) + + 原因:在覆盖索引的这棵 B+ 数上只需要进行 like 的匹配,或者是基于覆盖索引查询再进行 WHERE 的判断就可以获得结果 + + + +*** + + + +##### 系统优化 + +系统优化为全表扫描: + +* 如果 MySQL 评估使用索引比全表更慢,则不使用索引,索引失效: + + ```mysql + CREATE INDEX idx_address ON tb_seller(address); + EXPLAIN SELECT * FROM tb_seller WHERE address='西安市'; + EXPLAIN SELECT * FROM tb_seller WHERE address='北京市'; + ``` + + 北京市的键值占 9/10(区分度低),所以优化为全表扫描,type = ALL + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL使用索引14.png) + +* IS NULL、IS NOT NULL **有时**索引失效: + + ```mysql + EXPLAIN SELECT * FROM tb_seller WHERE name IS NULL; + EXPLAIN SELECT * FROM tb_seller WHERE name IS NOT NULL; + ``` + + NOT NULL 失效的原因是 name 列全部不是 null,优化为全表扫描,当 NULL 过多时,IS NULL 失效 + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL使用索引15.png) + +* IN 肯定会走索引,但是当 IN 的取值范围较大时会导致索引失效,走全表扫描: + + ```mysql + EXPLAIN SELECT * FROM tb_seller WHERE sellerId IN ('alibaba','huawei');-- 都走索引 + EXPLAIN SELECT * FROM tb_seller WHERE sellerId NOT IN ('alibaba','huawei'); + ``` + +* [MySQL 实战 45 讲](https://time.geekbang.org/column/article/74687)该章节最后提出了一种慢查询场景,获取到数据以后 Server 层还会做判断 + + + +*** + + + +#### 底层原理 + +索引失效一般是针对联合索引,联合索引一般由几个字段组成,排序方式是先按照第一个字段进行排序,然后排序第二个,依此类推,图示(a, b)索引,**a 相等的情况下 b 是有序的** + + + +* 最左前缀法则:当不匹配前面的字段的时候,后面的字段都是无序的。这种无序不仅体现在叶子节点,也会**导致查询时扫描的非叶子节点也是无序的**,因为索引树相当于忽略的第一个字段,就无法使用二分查找 + +* 范围查询右边的列,不能使用索引,比如语句: `WHERE a > 1 AND b = 1 `,在 a 大于 1 的时候,b 是无序的,a > 1 是扫描时有序的,但是找到以后进行寻找 b 时,索引树就不是有序的了 + + + +* 以 % 开头的 LIKE 模糊查询,索引失效,比如语句:`WHERE a LIKE '%d'`,前面的不确定,导致不符合最左匹配,直接去索引中搜索以 d 结尾的节点,所以没有顺序 + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-索引失效底层原理3.png) + + + +参考文章:https://mp.weixin.qq.com/s/B_M09dzLe9w7cT46rdGIeQ + + + +*** + + + +#### 查看索引 + +```mysql +SHOW STATUS LIKE 'Handler_read%'; +SHOW GLOBAL STATUS LIKE 'Handler_read%'; +``` + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL查看索引使用情况.png) + +* Handler_read_first:索引中第一条被读的次数,如果较高,表示服务器正执行大量全索引扫描(这个值越低越好) + +* Handler_read_key:如果索引正在工作,这个值代表一个行被索引值读的次数,值越低表示索引不经常使用(这个值越高越好) + +* Handler_read_next:按照键顺序读下一行的请求数,如果范围约束或执行索引扫描来查询索引列,值增加 + +* Handler_read_prev:按照键顺序读前一行的请求数,该读方法主要用于优化 ORDER BY ... DESC + +* Handler_read_rnd:根据固定位置读一行的请求数,如果执行大量查询并对结果进行排序则该值较高,可能是使用了大量需要 MySQL 扫描整个表的查询或连接,这个值较高意味着运行效率低,应该建立索引来解决 + +* Handler_read_rnd_next:在数据文件中读下一行的请求数,如果正进行大量的表扫描,该值较高,说明表索引不正确或写入的查询没有利用索引 + + + + + +*** + + + +### SQL 优化 + +#### 自增主键 + +##### 自增机制 + +自增主键可以让主键索引尽量地保持在数据页中递增顺序插入,不自增需要寻找其他页插入,导致随机 IO 和页分裂的情况 + +表的结构定义存放在后缀名为.frm 的文件中,但是并不会保存自增值,不同的引擎对于自增值的保存策略不同: + +* MyISAM 引擎的自增值保存在数据文件中 +* InnoDB 引擎的自增值保存在了内存里,每次打开表都会去找自增值的最大值 max(id),然后将 max(id)+1 作为当前的自增值;8.0 版本后,才有了自增值持久化的能力,将自增值的变更记录在了 redo log 中,重启的时候依靠 redo log 恢复重启之前的值 + +在插入一行数据的时候,自增值的行为如下: + +* 如果插入数据时 id 字段指定为 0、null 或未指定值,那么就把这个表当前的 AUTO_INCREMENT 值填到自增字段 +* 如果插入数据时 id 字段指定了具体的值,比如某次要插入的值是 X,当前的自增值是 Y + * 如果 X 优化为: + SELECT id,name,statu FROM tb_book; + ``` + +* 插入数据: + + ```mysql + INSERT INTO tb_test VALUES(1,'Tom'); + INSERT INTO tb_test VALUES(2,'Cat'); + INSERT INTO tb_test VALUES(3,'Jerry'); -- 连接三次数据库 + -- >优化为 + INSERT INTO tb_test VALUES(1,'Tom'),(2,'Cat'),(3,'Jerry'); -- 连接一次 + ``` + +* 在事务中进行数据插入: + + ```mysql + start transaction; + INSERT INTO tb_test VALUES(1,'Tom'); + INSERT INTO tb_test VALUES(2,'Cat'); + INSERT INTO tb_test VALUES(3,'Jerry'); + commit; -- 手动提交,分段提交 + ``` + +* 数据有序插入: + + ```mysql + INSERT INTO tb_test VALUES(1,'Tom'); + INSERT INTO tb_test VALUES(2,'Cat'); + INSERT INTO tb_test VALUES(3,'Jerry'); + ``` + +增加 cache 层:在应用中增加缓存层来达到减轻数据库负担的目的。可以部分数据从数据库中抽取出来放到应用端以文本方式存储,或者使用框架(Mybatis)提供的一级缓存 / 二级缓存,或者使用 Redis 数据库来缓存数据 + + + +*** + + + +#### 数据插入 + +当使用 load 命令导入数据的时候,适当的设置可以提高导入的效率: + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL load data.png) + +```mysql +LOAD DATA LOCAL INFILE = '/home/seazean/sql1.log' INTO TABLE `tb_user_1` FIELD TERMINATED BY ',' LINES TERMINATED BY '\n'; -- 文件格式如上图 +``` + +对于 InnoDB 类型的表,有以下几种方式可以提高导入的效率: + +1. **主键顺序插入**:因为 InnoDB 类型的表是按照主键的顺序保存的,所以将导入的数据按照主键的顺序排列,可以有效的提高导入数据的效率,如果 InnoDB 表没有主键,那么系统会自动默认创建一个内部列作为主键 + + 主键是否连续对性能影响不大,只要是递增的就可以,比如雪花算法产生的 ID 不是连续的,但是是递增的,因为递增可以让主键索引尽量地保持顺序插入,**避免了页分裂**,因此索引更紧凑 + + * 插入 ID 顺序排列数据: + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL插入ID顺序排列数据.png) + + * 插入 ID 无序排列数据: + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL插入ID无序排列数据.png) + +2. **关闭唯一性校验**:在导入数据前执行 `SET UNIQUE_CHECKS=0`,关闭唯一性校验;导入结束后执行 `SET UNIQUE_CHECKS=1`,恢复唯一性校验,可以提高导入的效率。 + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL插入数据关闭唯一性校验.png) + +3. **手动提交事务**:如果应用使用自动提交的方式,建议在导入前执行`SET AUTOCOMMIT=0`,关闭自动提交;导入结束后再打开自动提交,可以提高导入的效率。 + + 事务需要控制大小,事务太大可能会影响执行的效率。MySQL 有 innodb_log_buffer_size 配置项,超过这个值的日志会写入磁盘数据,效率会下降,所以在事务大小达到配置项数据级前进行事务提交可以提高效率 + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL插入数据手动提交事务.png) + + + +**** + + + +#### 分组排序 + +##### ORDER + +数据准备: + +```mysql +CREATE TABLE `emp` ( + `id` INT(11) NOT NULL AUTO_INCREMENT, + `name` VARCHAR(100) NOT NULL, + `age` INT(3) NOT NULL, + `salary` INT(11) DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=INNODB DEFAULT CHARSET=utf8mb4; +INSERT INTO `emp` (`id`, `name`, `age`, `salary`) VALUES('1','Tom','25','2300');-- ... +CREATE INDEX idx_emp_age_salary ON emp(age, salary); +``` + +* 第一种是通过对返回数据进行排序,所有不通过索引直接返回结果的排序都叫 FileSort 排序,会在内存中重新排序 + + ```mysql + EXPLAIN SELECT * FROM emp ORDER BY age DESC; -- 年龄降序 + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL ORDER BY排序1.png) + +* 第二种通过有序索引顺序扫描直接返回**有序数据**,这种情况为 Using index,不需要额外排序,操作效率高 + + ```mysql + EXPLAIN SELECT id, age, salary FROM emp ORDER BY age DESC; + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL ORDER BY排序2.png) + +* 多字段排序: + + ```mysql + EXPLAIN SELECT id,age,salary FROM emp ORDER BY age DESC, salary DESC; + EXPLAIN SELECT id,age,salary FROM emp ORDER BY salary DESC, age DESC; + EXPLAIN SELECT id,age,salary FROM emp ORDER BY age DESC, salary ASC; + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL ORDER BY排序3.png) + + 尽量减少额外的排序,通过索引直接返回有序数据。**需要满足 Order by 使用相同的索引、Order By 的顺序和索引顺序相同、Order by 的字段都是升序或都是降序**,否则需要额外的操作,就会出现 FileSort + +* ORDER BY RAND() 命令用来进行随机排序,会使用了临时内存表,临时内存表排序的时使用 rowid 排序方法 + +优化方式:创建合适的索引能够减少 Filesort 的出现,但是某些情况下条件限制不能让 Filesort 消失,就要加快 Filesort 的排序操作 + +内存临时表,MySQL 有两种 Filesort 排序算法: + +* rowid 排序:首先根据条件取出排序字段和信息,然后在**排序区 sort buffer(Server 层)**中排序,如果 sort buffer 不够,则在临时表 temporary table 中存储排序结果。完成排序后再根据行指针**回表读取记录**,该操作可能会导致大量随机 I/O 操作 + + 说明:对于临时内存表,回表过程只是简单地根据数据行的位置,直接访问内存得到数据,不会导致多访问磁盘,优先选择该方式 + +* 全字段排序:一次性取出满足条件的所有数据,需要回表,然后在排序区 sort buffer 中排序后直接输出结果集。排序时内存开销较大,但是排序效率比两次扫描算法高 + +具体的选择方式: + +* MySQL 通过比较系统变量 max_length_for_sort_data 的大小和 Query 语句取出的字段的大小,来判定使用哪种排序算法。如果前者大,则说明 sort buffer 空间足够,使用第二种优化之后的算法,否则使用第一种。 + +* 可以适当提高 sort_buffer_size 和 max_length_for_sort_data 系统变量,来增大排序区的大小,提高排序的效率 + + ```mysql + SET @@max_length_for_sort_data = 10000; -- 设置全局变量 + SET max_length_for_sort_data = 10240; -- 设置会话变量 + SHOW VARIABLES LIKE 'max_length_for_sort_data'; -- 默认1024 + SHOW VARIABLES LIKE 'sort_buffer_size'; -- 默认262114 + ``` + +磁盘临时表:排序使用优先队列(堆)的方式 + + + +*** + + + +##### GROUP + +GROUP BY 也会进行排序操作,与 ORDER BY 相比,GROUP BY 主要只是多了排序之后的分组操作,所以在 GROUP BY 的实现过程中,与 ORDER BY 一样也可以利用到索引 + +* 分组查询: + + ```mysql + DROP INDEX idx_emp_age_salary ON emp; + EXPLAIN SELECT age,COUNT(*) FROM emp GROUP BY age; + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL GROUP BY排序1.png) + + Using temporary:表示 MySQL 需要使用临时表(不是 sort buffer)来存储结果集,常见于排序和分组查询 + +* 查询包含 GROUP BY 但是用户想要避免排序结果的消耗, 则可以执行 ORDER BY NULL 禁止排序: + + ```mysql + EXPLAIN SELECT age,COUNT(*) FROM emp GROUP BY age ORDER BY NULL; + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL GROUP BY排序2.png) + +* 创建索引:索引本身有序,不需要临时表,也不需要再额外排序 + + ```mysql + CREATE INDEX idx_emp_age_salary ON emp(age, salary); + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL GROUP BY排序3.png) + +* 数据量很大时,使用 SQL_BIG_RESULT 提示优化器直接使用直接用磁盘临时表 + + + +*** + + + +#### 联合查询 + +对于包含 OR 的查询子句,如果要利用索引,则 OR 之间的**每个条件列都必须用到索引,而且不能使用到条件之间的复合索引**,如果没有索引,则应该考虑增加索引 + +* 执行查询语句: + + ```mysql + EXPLAIN SELECT * FROM emp WHERE id = 1 OR age = 30; -- 两个索引,并且不是复合索引 + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL OR条件查询1.png) + + ```sh + Extra: Using sort_union(idx_emp_age_salary,PRIMARY); Using where + ``` + +* 使用 UNION 替换 OR,求并集: + + 注意:该优化只针对多个索引列有效,如果有列没有被索引,查询效率可能会因为没有选择 OR 而降低 + + ```mysql + EXPLAIN SELECT * FROM emp WHERE id = 1 UNION SELECT * FROM emp WHERE age = 30; + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL OR条件查询2.png) + +* UNION 要优于 OR 的原因: + + * UNION 语句的 type 值为 ref,OR 语句的 type 值为 range + * UNION 语句的 ref 值为 const,OR 语句的 ref 值为 null,const 表示是常量值引用,非常快 + + + +**** + + + +#### 嵌套查询 + +MySQL 4.1 版本之后,开始支持 SQL 的子查询 + +* 可以使用 SELECT 语句来创建一个单列的查询结果,然后把结果作为过滤条件用在另一个查询中 +* 使用子查询可以一次性的完成逻辑上需要多个步骤才能完成的 SQL 操作,同时也可以避免事务或者表锁死 +* 在有些情况下,**子查询是可以被更高效的连接(JOIN)替代** + +例如查找有角色的所有的用户信息: + +* 执行计划: + + ```mysql + EXPLAIN SELECT * FROM t_user WHERE id IN (SELECT user_id FROM user_role); + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL嵌套查询1.png) + +* 优化后: + + ```mysql + EXPLAIN SELECT * FROM t_user u , user_role ur WHERE u.id = ur.user_id; + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL嵌套查询2.png) + + 连接查询之所以效率更高 ,是因为**不需要在内存中创建临时表**来完成逻辑上需要两个步骤的查询工作 + + + + + +*** + + + +#### 分页查询 + +一般分页查询时,通过创建覆盖索引能够比较好地提高性能 + +一个常见的问题是 `LIMIT 200000,10`,此时需要 MySQL 扫描前 200010 记录,仅仅返回 200000 - 200010 之间的记录,其他记录丢弃,查询排序的代价非常大 + +* 分页查询: + + ```mysql + EXPLAIN SELECT * FROM tb_user_1 LIMIT 200000,10; + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL分页查询1.png) + +* 优化方式一:内连接查询,在索引列 id 上完成排序分页操作,最后根据主键关联回原表查询所需要的其他列内容 + + ```mysql + EXPLAIN SELECT * FROM tb_user_1 t,(SELECT id FROM tb_user_1 ORDER BY id LIMIT 200000,10) a WHERE t.id = a.id; + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL分页查询2.png) + +* 优化方式二:方案适用于主键自增的表,可以把 LIMIT 查询转换成某个位置的查询 + + ```mysql + EXPLAIN SELECT * FROM tb_user_1 WHERE id > 200000 LIMIT 10; -- 写法 1 + EXPLAIN SELECT * FROM tb_user_1 WHERE id BETWEEN 200000 and 200010; -- 写法 2 + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL分页查询3.png) + + + +**** + + + +#### 使用提示 + +SQL 提示,是优化数据库的一个重要手段,就是在 SQL 语句中加入一些提示来达到优化操作的目的 + +* USE INDEX:在查询语句中表名的后面添加 USE INDEX 来提供 MySQL 去参考的索引列表,可以让 MySQL 不再考虑其他可用的索引 + + ```mysql + CREATE INDEX idx_seller_name ON tb_seller(name); + EXPLAIN SELECT * FROM tb_seller USE INDEX(idx_seller_name) WHERE name='小米科技'; + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL使用提示1.png) + +* IGNORE INDEX:让 MySQL 忽略一个或者多个索引,则可以使用 IGNORE INDEX 作为提示 + + ```mysql + EXPLAIN SELECT * FROM tb_seller IGNORE INDEX(idx_seller_name) WHERE name = '小米科技'; + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL使用提示2.png) + +* FORCE INDEX:强制 MySQL 使用一个特定的索引 + + ```mysql + EXPLAIN SELECT * FROM tb_seller FORCE INDEX(idx_seller_name_sta_addr) WHERE NAME='小米科技'; + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL使用提示3.png) + + + + + +*** + + + +#### 统计计数 + +在不同的 MySQL 引擎中,count(*) 有不同的实现方式: + +* MyISAM 引擎把一个表的总行数存在了磁盘上,因此执行 count(*) 的时候会直接返回这个数,效率很高,但不支持事务 +* show table status 命令通过采样估算可以快速获取,但是不准确 +* InnoDB 表执行 count(*) 会遍历全表,虽然结果准确,但会导致性能问题 + +解决方案: + +* 计数保存在 Redis 中,但是更新 MySQL 和 Redis 的操作不是原子的,会存在数据一致性的问题 + +* 计数直接放到数据库里单独的一张计数表中,利用事务解决计数精确问题: + + + + 会话 B 的读操作在 T3 执行的,这时更新事务还没有提交,所以计数值加 1 这个操作对会话 B 还不可见,因此会话 B 查询的计数值和最近 100 条记录,返回的结果逻辑上就是一致的 + + 并发系统性能的角度考虑,应该先插入操作记录再更新计数表,因为更新计数表涉及到行锁的竞争,**先插入再更新能最大程度地减少事务之间的锁等待,提升并发度** + +count 函数的按照效率排序:`count(字段) < count(主键id) < count(1) ≈ count(*)`,所以建议尽量使用 count(*) + +* count(主键 id):InnoDB 引擎会遍历整张表,把每一行的 id 值都取出来返回给 Server 层,Server 判断 id 不为空就按行累加 +* count(1):InnoDB 引擎遍历整张表但不取值,Server 层对于返回的每一行,放一个数字 1 进去,判断不为空就按行累加 +* count(字段):如果这个字段是定义为 not null 的话,一行行地从记录里面读出这个字段,判断不能为 null,按行累加;如果这个字段定义允许为 null,那么执行的时候,判断到有可能是 null,还要把值取出来再判断一下,不是 null 才累加 +* count(*):不取值,按行累加 + + + +参考文章:https://time.geekbang.org/column/article/72775 + + + + + +*** + + + +### 缓冲优化 + +#### 优化原则 + +三个原则: + +* 将尽量多的内存分配给 MySQL 做缓存,但也要给操作系统和其他程序预留足够内存 +* MyISAM 存储引擎的数据文件读取依赖于操作系统自身的 IO 缓存,如果有 MyISAM 表,就要预留更多的内存给操作系统做 IO 缓存 +* 排序区、连接区等缓存是分配给每个数据库会话(Session)专用的,值的设置要根据最大连接数合理分配,如果设置太大,不但浪费资源,而且在并发数较高时会导致物理内存耗尽 + + + +*** + + + +#### 缓冲内存 + +Buffer Pool 本质上是 InnoDB 向操作系统申请的一段连续的内存空间。InnoDB 的数据是按数据页为单位来读写,每个数据页的大小默认是 16KB。数据是存放在磁盘中,每次读写数据都需要进行磁盘 IO 将数据读入内存进行操作,效率会很低,所以提供了 Buffer Pool 来暂存这些数据页,缓存中的这些页又叫缓冲页 + +工作原理: + +* 从数据库读取数据时,会首先从缓存中读取,如果缓存中没有,则从磁盘读取后放入 Buffer Pool +* 向数据库写入数据时,会写入缓存,缓存中修改的数据会**定期刷新**到磁盘,这一过程称为刷脏 + +Buffer Pool 中每个缓冲页都有对应的控制信息,包括表空间编号、页号、偏移量、链表信息等,控制信息存放在占用的内存称为控制块,控制块与缓冲页是一一对应的,但并不是物理上相连的,都在缓冲池中 + +MySQL 提供了缓冲页的快速查找方式:**哈希表**,使用表空间号和页号作为 Key,缓冲页控制块的地址作为 Value 创建一个哈希表,获取数据页时根据 Key 进行哈希寻址: + +* 如果不存在对应的缓存页,就从 free 链表中选一个空闲缓冲页,把磁盘中的对应页加载到该位置 +* 如果存在对应的缓存页,直接获取使用,提高查询数据的效率 + +当内存数据页跟磁盘数据页内容不一致时,称这个内存页为脏页;内存数据写入磁盘后,内存和磁盘上的数据页一致,称为干净页 + + + +*** + + + +#### 内存管理 + +##### Free 链表 + +MySQL 启动时完成对 Buffer Pool 的初始化,先向操作系统申请连续的内存空间,然后将内存划分为若干对控制块和缓冲页。为了区分空闲和已占用的数据页,将所有空闲缓冲页对应的**控制块作为一个节点**放入一个链表中,就是 Free 链表(**空闲链表**) + + + +基节点:是一块单独申请的内存空间(占 40 字节),并不在 Buffer Pool 的那一大片连续内存空间里 + +磁盘加载页的流程: + +* 从 Free 链表中取出一个空闲的缓冲页 +* 把缓冲页对应的控制块的信息填上(页所在的表空间、页号之类的信息) +* 把缓冲页对应的 Free 链表节点(控制块)从链表中移除,表示该缓冲页已经被使用 + + + +参考文章:https://blog.csdn.net/li1325169021/article/details/121124440 + + + +**** + + + +##### Flush 链表 + +Flush 链表是一个用来**存储脏页**的链表,对于已经修改过的缓冲脏页,第一次修改后加入到**链表头部**,以后每次修改都不会重新加入,只修改部分控制信息,出于性能考虑并不是直接更新到磁盘,而是在未来的某个时间进行刷脏 + + + +**后台有专门的线程每隔一段时间把脏页刷新到磁盘**: + +* 从 Flush 链表中刷新一部分页面到磁盘: + * **后台线程定时**从 Flush 链表刷脏,根据系统的繁忙程度来决定刷新速率,这种方式称为 BUF_FLUSH_LIST + * 线程刷脏的比较慢,导致用户线程加载一个新的数据页时发现没有空闲缓冲页,此时会尝试从 LRU 链表尾部寻找缓冲页直接释放,如果该页面是已经修改过的脏页就**同步刷新**到磁盘,速度较慢,这种方式称为 BUF_FLUSH_SINGLE_PAGE +* 从 LRU 链表的冷数据中刷新一部分页面到磁盘,即:BUF_FLUSH_LRU + * 后台线程会定时从 LRU 链表的尾部开始扫描一些页面,扫描的页面数量可以通过系统变量 `innodb_lru_scan_depth` 指定,如果在 LRU 链表中发现脏页,则把它们刷新到磁盘,这种方式称为 BUF_FLUSH_LRU + * 控制块里会存储该缓冲页是否被修改的信息,所以可以很容易的获取到某个缓冲页是否是脏页 + + + +参考文章:https://blog.csdn.net/li1325169021/article/details/121125765 + + + +*** + + + +##### LRU 链表 + +Buffer Pool 需要保证缓存的命中率,所以 MySQL 创建了一个 LRU 链表,当访问某个页时: + +* 如果该页不在 Buffer Pool 中,把该页从磁盘加载进来后会将该缓冲页对应的控制块作为节点放入 **LRU 链表的头部**,保证热点数据在链表头 +* 如果该页在 Buffer Pool 中,则直接把该页对应的控制块移动到 LRU 链表的头部,所以 LRU 链表尾部就是最近最少使用的缓冲页 + +MySQL 基于局部性原理提供了预读功能: + +* 线性预读:系统变量 `innodb_read_ahead_threshold`,如果顺序访问某个区(extent:16 KB 的页,连续 64 个形成一个区,一个区默认 1MB 大小)的页面数超过了该系统变量值,就会触发一次**异步读取**下一个区中全部的页面到 Buffer Pool 中 +* 随机预读:如果某个区 13 个连续的页面都被加载到 Buffer Pool,无论这些页面是否是顺序读取,都会触发一次**异步读取**本区所有的其他页面到 Buffer Pool 中 + +预读会造成加载太多用不到的数据页,造成那些使用频率很高的数据页被挤到 LRU 链表尾部,所以 InnoDB 将 LRU 链表分成两段,**冷热数据隔离**: + +* 一部分存储使用频率很高的数据页,这部分链表也叫热数据,young 区,靠近链表头部的区域 +* 一部分存储使用频率不高的冷数据,old 区,靠近链表尾部,默认占 37%,可以通过系统变量 `innodb_old_blocks_pct` 指定 + +当磁盘上的某数据页被初次加载到 Buffer Pool 中会被放入 old 区,淘汰时优先淘汰 old 区 + +* 当对 old 区的数据进行访问时,会在控制块记录下访问时间,等待后续的访问时间与第一次访问的时间是否在某个时间间隔内,通过系统变量 `innodb_old_blocks_time` 指定时间间隔,默认 1000ms,成立就**移动到 young 区的链表头部** +* `innodb_old_blocks_time` 为 0 时,每次访问一个页面都会放入 young 区的头部 + + + +*** + + + +#### 参数优化 + +InnoDB 用一块内存区做 IO 缓存池,该缓存池不仅用来缓存 InnoDB 的索引块,也用来缓存 InnoDB 的数据块,可以通过下面的指令查看 Buffer Pool 的状态信息: + +```mysql +SHOW ENGINE INNODB STATUS\G +``` + +`Buffer pool hit rate` 字段代表**内存命中率**,表示 Buffer Pool 对查询的加速效果 + +核心参数: + +* `innodb_buffer_pool_size`:该变量决定了 Innodb 存储引擎表数据和索引数据的最大缓存区大小,默认 128M + + ```mysql + SHOW VARIABLES LIKE 'innodb_buffer_pool_size'; + ``` + + 在保证操作系统及其他程序有足够内存可用的情况下,`innodb_buffer_pool_size` 的值越大,缓存命中率越高,建议设置成可用物理内存的 60%~80% + + ```sh + innodb_buffer_pool_size=512M + ``` + +* `innodb_log_buffer_size`:该值决定了 Innodb 日志缓冲区的大小,保存要写入磁盘上的日志文件数据 + + 对于可能产生大量更新记录的大事务,增加该值的大小,可以避免 Innodb 在事务提交前就执行不必要的日志写入磁盘操作,影响执行效率,通过配置文件修改: + + ```sh + innodb_log_buffer_size=10M + ``` + +在多线程下,访问 Buffer Pool 中的各种链表都需要加锁,所以将 Buffer Pool 拆成若干个小实例,**每个线程对应一个实例**,独立管理内存空间和各种链表(类似 ThreadLocal),多线程访问各自实例互不影响,提高了并发能力 + +MySQL 5.7.5 之前 `innodb_buffer_pool_size` 只支持在系统启动时修改,现在已经支持运行时修改 Buffer Pool 的大小,但是每次调整参数都会重新向操作系统申请一块连续的内存空间,**将旧的缓冲池的内容拷贝到新空间**非常耗时,所以 MySQL 开始以一个 chunk 为单位向操作系统申请内存,所以一个 Buffer Pool 实例可以由多个 chunk 组成 + +* 在系统启动时设置系统变量 `innodb_buffer_pool_instance` 可以指定 Buffer Pool 实例的个数,但是当 Buffer Pool 小于 1GB 时,设置多个实例时无效的 +* 指定系统变量 `innodb_buffer_pool_chunk_size` 来改变 chunk 的大小,只能在启动时修改,运行中不能修改,而且该变量并不包含缓冲页的控制块的内存大小 +* `innodb_buffer_pool_size` 必须是 `innodb_buffer_pool_chunk_size × innodb_buffer_pool_instance` 的倍数,默认值是 `128M × 16 = 2G`,Buffer Pool 必须是 2G 的整数倍,如果指定 5G,会自动调整成 6G +* 如果启动时 `chunk × instances` > `pool_size`,那么 chunk 的值会自动设置为 `pool_size ÷ instances` + + + +*** + + + +### 内存优化 + +#### Change + +InnoDB 管理的 Buffer Pool 中有一块内存叫 Change Buffer 用来对**增删改操作**提供缓存,可以通过参数来动态设置,设置为 50 时表示 Change Buffer 的大小最多占用 Buffer Pool 的 50% + +* 唯一索引的更新不能使用 Change Buffer,需要将数据页读入内存,判断没有冲突在写入 +* 普通索引可以使用 Change Buffer,**直接写入 Buffer 就结束**,不用校验唯一性 + +Change Buffer 并不是数据页,只是对操作的缓存,所以需要将 Change Buffer 中的操作应用到旧数据页,得到新的数据页(脏页)的过程称为 Merge + +* 触发时机:访问数据页时会触发 Merge、后台有定时线程进行 Merge、在数据库正常关闭(shutdown)的过程中也会触发 +* 工作流程:首先从磁盘读入数据页到内存(因为 Buffer Pool 中不一定存在对应的数据页),从 Change Buffer 中找到对应的操作应用到数据页,得到新的数据页即为脏页,然后写入 redo log,等待刷脏即可 + +说明:Change Buffer 中的记录,在事务提交时也会写入 redo log,所以是可以保证不丢失的 + +业务场景: + +* 对于**写多读少**的业务来说,页面在写完以后马上被访问到的概率比较小,此时 Change Buffer 的使用效果最好,常见的就是账单类、日志类的系统 + +* 一个业务的更新模式是写入后马上做查询,那么即使满足了条件,将更新先记录在 Change Buffer,但之后由于马上要访问这个数据页,会立即触发 Merge 过程,这样随机访问 IO 的次数不会减少,并且增加了 Change Buffer 的维护代价 + +补充:Change Buffer 的前身是 Insert Buffer,只能对 Insert 操作优化,后来增加了 Update/Delete 的支持,改为 Change Buffer + + + +*** + + + +#### Net + +Server 层针对优化**查询**的内存为 Net Buffer,内存的大小是由参数 `net_buffer_length`定义,默认 16k,实现流程: + +* 获取一行数据写入 Net Buffer,重复获取直到 Net Buffer 写满,调用网络接口发出去 +* 若发送成功就清空 Net Buffer,然后继续取下一行;若发送函数返回 EAGAIN 或 WSAEWOULDBLOCK,表示本地网络栈 `socket send buffer` 写满了,**进入等待**,直到网络栈重新可写再继续发送 + +MySQL 采用的是边读边发的逻辑,因此对于数据量很大的查询来说,不会在 Server 端保存完整的结果集,如果客户端读结果不及时,会堵住 MySQL 的查询过程,但是**不会把内存打爆导致 OOM** + + + +SHOW PROCESSLIST 获取线程信息后,处于 Sending to client 状态代表服务器端的网络栈写满,等待客户端接收数据 + +假设有一个业务的逻辑比较复杂,每读一行数据以后要处理很久的逻辑,就会导致客户端要过很久才会去取下一行数据,导致 MySQL 的阻塞,一直处于 Sending to client 的状态 + +解决方法:如果一个查询的返回结果很是很多,建议使用 mysql_store_result 这个接口,直接把查询结果保存到本地内存 + + + +参考文章:https://blog.csdn.net/qq_33589510/article/details/117673449 + + + +*** + + + +#### Read + +read_rnd_buffer 是 MySQL 的随机读缓冲区,当按任意顺序读取记录行时将分配一个随机读取缓冲区,进行排序查询时,MySQL 会首先扫描一遍该缓冲,以避免磁盘搜索,提高查询速度,大小是由 read_rnd_buffer_size 参数控制的 + +Multi-Range Read 优化,**将随机 IO 转化为顺序 IO** 以降低查询过程中 IO 开销,因为大多数的数据都是按照主键递增顺序插入得到,所以按照主键的递增顺序查询的话,对磁盘的读比较接近顺序读,能够提升读性能 + +二级索引为 a,聚簇索引为 id,优化回表流程: + +* 根据索引 a,定位到满足条件的记录,将 id 值放入 read_rnd_buffer 中 +* 将 read_rnd_buffer 中的 id 进行**递增排序** +* 排序后的 id 数组,依次回表到主键 id 索引中查记录,并作为结果返回 + +说明:如果步骤 1 中 read_rnd_buffer 放满了,就会先执行步骤 2 和 3,然后清空 read_rnd_buffer,之后继续找索引 a 的下个记录 + +使用 MRR 优化需要设进行设置: + +```mysql +SET optimizer_switch='mrr_cost_based=off' +``` + + + +*** + + + +#### Key + +MyISAM 存储引擎使用 key_buffer 缓存索引块,加速 MyISAM 索引的读写速度。对于 MyISAM 表的数据块没有特别的缓存机制,完全依赖于操作系统的 IO 缓存 + +* key_buffer_size:该变量决定 MyISAM 索引块缓存区的大小,直接影响到 MyISAM 表的存取效率 + + ```mysql + SHOW VARIABLES LIKE 'key_buffer_size'; -- 单位是字节 + ``` + + 在 MySQL 配置文件中设置该值,建议至少将1/4可用内存分配给 key_buffer_size: + + ```sh + vim /etc/mysql/my.cnf + key_buffer_size=1024M + ``` + +* read_buffer_size:如果需要经常顺序扫描 MyISAM 表,可以通过增大 read_buffer_size 的值来改善性能。但 read_buffer_size 是每个 Session 独占的,如果默认值设置太大,并发环境就会造成内存浪费 + +* read_rnd_buffer_size:对于需要做排序的 MyISAM 表的查询,如带有 ORDER BY 子句的语句,适当增加该的值,可以改善此类的 SQL 的性能,但是 read_rnd_buffer_size 是每个 Session 独占的,如果默认值设置太大,就会造成内存浪费 + + + + + +*** + + + + + +### 存储优化 + +#### 数据存储 + +系统表空间是用来放系统信息的,比如数据字典什么的,对应的磁盘文件是 ibdata,数据表空间是一个个的表数据文件,对应的磁盘文件就是表名.ibd + +表数据既可以存在共享表空间里,也可以是单独的文件,这个行为是由参数 innodb_file_per_table 控制的: + +* OFF:表示表的数据放在系统共享表空间,也就是跟数据字典放在一起 +* ON :表示每个 InnoDB 表数据存储在一个以 .ibd 为后缀的文件中(默认) + +一个表单独存储为一个文件更容易管理,在不需要这个表时通过 drop table 命令,系统就会直接删除这个文件;如果是放在共享表空间中,即使表删掉了,空间也是不会回收的 + + + + + +*** + + + +#### 数据删除 + +MySQL 的数据删除就是移除掉某个记录后,该位置就被标记为**可复用**,如果有符合范围条件的数据可以插入到这里。符合范围条件的意思是假设删除记录 R4,之后要再插入一个 ID 在 300 和 600 之间的记录时,就会复用这个位置 + + + +InnoDB 的数据是按页存储的如果删掉了一个数据页上的所有记录,整个数据页就可以被复用了,如果相邻的两个数据页利用率都很小,系统就会把这两个页上的数据合到其中一个页上,另外一个数据页就被标记为可复用 + +删除命令其实只是把记录的位置,或者**数据页标记为了可复用,但磁盘文件的大小是不会变的**,这些可以复用还没有被使用的空间,看起来就像是空洞,造成数据库的稀疏,因此需要进行紧凑处理 + + + +*** + + + +#### 重建数据 + +重建表就是按照主键 ID 递增的顺序,把数据一行一行地从旧表中读出来再插入到新表中,让数据更加紧凑。重建表时 MySQL 会自动完成转存数据、交换表名、删除旧表的操作,线上操作会阻塞大量的线程增删改查的操作 + +重建命令: + +```sql +ALTER TABLE A ENGINE=InnoDB +``` + +工作流程:新建临时表 tmp_table B(在 Server 层创建的),把表 A 中的数据导入到表 B 中,操作完成后用表 B 替换表 A,完成重建 + +重建表的步骤需要 DDL 不是 Online 的,因为在导入数据的过程有新的数据要写入到表 A 的话,就会造成数据丢失 + +MySQL 5.6 版本开始引入的 **Online DDL**,重建表的命令默认执行此步骤: + +* 建立一个临时文件 tmp_file(InnoDB 创建),扫描表 A 主键的所有数据页 +* 用数据页中表 A 的记录生成 B+ 树,存储到临时文件中 +* 生成临时文件的过程中,将所有对 A 的操作记录在一个日志文件(row log)中,对应的是图中 state2 的状态 +* 临时文件生成后,将日志文件中的操作应用到临时文件,得到一个逻辑数据上与表 A 相同的数据文件,对应的就是图中 state3 +* 用临时文件替换表 A 的数据文件 + + + +Online DDL 操作会先获取 MDL 写锁,再退化成 MDL 读锁。但 MDL 写锁持有时间比较短,所以可以称为 Online; 而 MDL 读锁,不阻止数据增删查改,但会阻止其它线程修改表结构(可以对比 `ANALYZE TABLE t` 命令) + +问题:重建表可以收缩表空间,但是执行指令后整体占用空间增大 + +原因:在重建表后 InnoDB 不会把整张表占满,每个页留了 1/16 给后续的更新使用。表在未整理之前页已经占用 15/16 以上,收缩之后需要保持数据占用空间在 15/16,所以文件占用空间更大才能保持 + +注意:临时文件也要占用空间,如果空间不足会重建失败 + + + +**** + + + +#### 原地置换 + +DDL 中的临时表 tmp_table 是在 Server 层创建的,Online DDL 中的临时文件 tmp_file 是 InnoDB 在内部创建出来的,整个 DDL 过程都在 InnoDB 内部完成,对于 Server 层来说,没有把数据挪动到临时表,是一个原地操作,这就是 inplace + +两者的关系: + +* DDL 过程如果是 Online 的,就一定是 inplace 的 +* inplace 的 DDL,有可能不是 Online 的,截止到 MySQL 8.0,全文索引(FULLTEXT)和空间索引(SPATIAL)属于这种情况 + + + + + +*** + + + +### 并发优化 + +MySQL Server 是多线程结构,包括后台线程和客户服务线程。多线程可以有效利用服务器资源,提高数据库的并发性能。在 MySQL 中,控制并发连接和线程的主要参数: + +* max_connections:控制允许连接到 MySQL 数据库的最大连接数,默认值是 151 + + 如果状态变量 connection_errors_max_connections 不为零,并且一直增长,则说明不断有连接请求因数据库连接数已达到允许最大值而失败,这时可以考虑增大 max_connections 的值 + + MySQL 最大可支持的连接数取决于很多因素,包括操作系统平台的线程库的质量、内存大小、每个连接的负荷、CPU的处理速度、期望的响应时间等。在 Linux 平台下,性能好的服务器,可以支持 500-1000 个连接,需要根据服务器性能进行评估设定 + +* innodb_thread_concurrency:并发线程数,代表系统内同时运行的线程数量(已经被移除) + +* back_log:控制 MySQL 监听 TCP 端口时的积压请求栈的大小 + + 如果 Mysql 的连接数达到 max_connections 时,新来的请求将会被存在堆栈中,以等待某一连接释放资源,该堆栈的数量即 back_log。如果等待连接的数量超过 back_log,将不被授予连接资源直接报错 + + 5.6.6 版本之前默认值为 50,之后的版本默认为 `50 + (max_connections/5)`,但最大不超过900,如果需要数据库在较短的时间内处理大量连接请求, 可以考虑适当增大 back_log 的值 + +* table_open_cache:控制所有 SQL 语句执行线程可打开表缓存的数量 + + 在执行 SQL 语句时,每个执行线程至少要打开1个表缓存,该参数的值应该根据设置的最大连接数以及每个连接执行关联查询中涉及的表的最大数量来设定:`max_connections * N` + +* thread_cache_size:可控制 MySQL 缓存客户服务线程的数量 + + 为了加快连接数据库的速度,MySQL 会缓存一定数量的客户服务线程以备重用,池化思想 + +* innodb_lock_wait_timeout:设置 InnoDB 事务等待行锁的时间,默认值是 50ms + + 对于需要快速反馈的业务系统,可以将行锁的等待时间调小,以避免事务被长时间挂起; 对于后台运行的批量处理程序来说,可以将行锁的等待时间调大,以避免发生大的回滚操作 + + + + + +*** + + + + + +## 事务机制 + +### 基本介绍 + +事务(Transaction)是访问和更新数据库的程序执行单元;事务中可能包含一个或多个 SQL 语句,这些语句要么都执行,要么都不执行,作为一个关系型数据库,MySQL 支持事务。 + +单元中的每条 SQL 语句都相互依赖,形成一个整体 + +* 如果某条 SQL 语句执行失败或者出现错误,那么整个单元就会回滚,撤回到事务最初的状态 + +* 如果单元中所有的 SQL 语句都执行成功,则事务就顺利执行 + +事务的四大特征:ACID + +- 原子性 (atomicity) +- 一致性 (consistency) +- 隔离性 (isolaction) +- 持久性 (durability) + +事务的几种状态: + +* 活动的(active):事务对应的数据库操作正在执行中 +* 部分提交的(partially committed):事务的最后一个操作执行完,但是内存还没刷新至磁盘 +* 失败的(failed):当事务处于活动状态或部分提交状态时,如果数据库遇到了错误或刷脏失败,或者用户主动停止当前的事务 +* 中止的(aborted):失败状态的事务回滚完成后的状态 +* 提交的(committed):当处于部分提交状态的事务刷脏成功,就处于提交状态 + + + + + +*** + + + +### 事务管理 + +#### 基本操作 + +事务管理的三个步骤 + +1. 开启事务:记录回滚点,并通知服务器,将要执行一组操作,要么同时成功、要么同时失败 + +2. 执行 SQL 语句:执行具体的一条或多条 SQL 语句 + +3. 结束事务(提交|回滚) + + - 提交:没出现问题,数据进行更新 + - 回滚:出现问题,数据恢复到开启事务时的状态 + + +事务操作: + +* 显式开启事务 + + ```mysql + START TRANSACTION [READ ONLY|READ WRITE|WITH CONSISTENT SNAPSHOT]; #可以跟一个或多个状态,最后的是一致性读 + BEGIN [WORK]; + ``` + + 说明:不填状态默认是读写事务 + +* 回滚事务,用来手动中止事务 + + ```mysql + ROLLBACK; + ``` + +* 提交事务,显示执行是手动提交,MySQL 默认为自动提交 + + ```mysql + COMMIT; + ``` + +* 保存点:在事务的执行过程中设置的还原点,调用 ROLLBACK 时可以指定回滚到哪个点 + + ```mysql + SAVEPOINT point_name; #设置保存点 + RELEASE point_name #删除保存点 + ROLLBACK [WORK] TO [SAVEPOINT] point_name #回滚至某个保存点,不填默认回滚到事务执行之前的状态 + ``` + +* 操作演示 + + ```mysql + -- 开启事务 + START TRANSACTION; + + -- 张三给李四转账500元 + -- 1.张三账户-500 + UPDATE account SET money=money-500 WHERE NAME='张三'; + -- 2.李四账户+500 + UPDATE account SET money=money+500 WHERE NAME='李四'; + + -- 回滚事务(出现问题) + ROLLBACK; + + -- 提交事务(没出现问题) + COMMIT; + ``` + + + +*** + + + +#### 提交方式 + +提交方式的相关语法: + +- 查看事务提交方式 + + ```mysql + SELECT @@AUTOCOMMIT; -- 会话,1 代表自动提交 0 代表手动提交 + SELECT @@GLOBAL.AUTOCOMMIT; -- 系统 + ``` + +- 修改事务提交方式 + + ```mysql + SET @@AUTOCOMMIT=数字; -- 系统 + SET AUTOCOMMIT=数字; -- 会话 + ``` + +- **系统变量的操作**: + + ```sql + SET [GLOBAL|SESSION] 变量名 = 值; -- 默认是会话 + SET @@[(GLOBAL|SESSION).]变量名 = 值; -- 默认是系统 + ``` + + ```sql + SHOW [GLOBAL|SESSION] VARIABLES [LIKE '变量%']; -- 默认查看会话内系统变量值 + ``` + +工作原理: + +* 自动提交:如果没有 START TRANSACTION 显式地开始一个事务,那么**每条 SQL 语句都会被当做一个事务执行提交操作**;显式开启事务后,会在本次事务结束(提交或回滚)前暂时关闭自动提交 +* 手动提交:不需要显式的开启事务,所有的 SQL 语句都在一个事务中,直到执行了提交或回滚,然后进入下一个事务 +* 隐式提交:存在一些特殊的命令,在事务中执行了这些命令会马上**强制执行 COMMIT 提交事务** + * **DDL 语句** (CREATE/DROP/ALTER)、LOCK TABLES 语句、LOAD DATA 导入数据语句、主从复制语句等 + * 当一个事务还没提交或回滚,显式的开启一个事务会隐式的提交上一个事务 + + + +**** + + + +#### 事务 ID + +事务在执行过程中对某个表执行了**增删改操作或者创建表**,就会为当前事务分配一个独一无二的事务 ID(对临时表并不会分配 ID),如果当前事务没有被分配 ID,默认是 0 + +说明:只读事务不能对普通的表进行增删改操作,但是可以对临时表增删改,读写事务可以对数据表执行增删改查操作 + +事务 ID 本质上就是一个数字,服务器在内存中维护一个全局变量: + +* 每当需要为某个事务分配 ID,就会把全局变量的值赋值给事务 ID,然后变量自增 1 +* 每当变量值为 256 的倍数时,就将该变量的值刷新到系统表空间的 Max Trx ID 属性中,该属性占 8 字节 +* 系统再次启动后,会读取表空间的 Max Trx ID 属性到内存,加上 256 后赋值给全局变量,因为关机时的事务 ID 可能并不是 256 的倍数,会比 Max Trx ID 大,所以需要加上 256 保持事务 ID 是一个**递增的数字** + +**聚簇索引**的行记录除了完整的数据,还会自动添加 trx_id、roll_pointer 隐藏列,如果表中没有主键并且没有非空唯一索引,也会添加一个 row_id 的隐藏列作为聚簇索引 + + + + +*** + + + +### 隔离级别 + +#### 四种级别 + +事务的隔离级别:多个客户端操作时,各个客户端的事务之间应该是隔离的,**不同的事务之间不该互相影响**,而如果多个事务操作同一批数据时,则需要设置不同的隔离级别,否则就会产生问题。 + +隔离级别分类: + +| 隔离级别 | 名称 | 会引发的问题 | 数据库默认隔离级别 | +| ---------------- | -------- | ---------------------- | ------------------- | +| Read Uncommitted | 读未提交 | 脏读、不可重复读、幻读 | | +| Read Committed | 读已提交 | 不可重复读、幻读 | Oracle / SQL Server | +| Repeatable Read | 可重复读 | 幻读 | MySQL | +| Serializable | 可串行化 | 无 | | + +一般来说,隔离级别越低,系统开销越低,可支持的并发越高,但隔离性也越差 + +* 脏写 (Dirty Write):当两个或多个事务选择同一行,最初的事务修改的值被后面事务修改的值覆盖,所有的隔离级别都可以避免脏写(又叫丢失更新),因为有行锁 + +* 脏读 (Dirty Reads):在一个事务处理过程中读取了另一个**未提交**的事务中修改过的数据 + +* 不可重复读 (Non-Repeatable Reads):在一个事务处理过程中读取了另一个事务中修改并**已提交**的数据 + + > 可重复读的意思是不管读几次,结果都一样,可以重复的读,可以理解为快照读,要读的数据集不会发生变化 + +* 幻读 (Phantom Reads):在事务中按某个条件先后两次查询数据库,后一次查询查到了前一次查询没有查到的行,**数据条目**发生了变化。比如查询某数据不存在,准备插入此记录,但执行插入时发现此记录已存在,无法插入 + +隔离级别操作语法: + +* 查询数据库隔离级别 + + ```mysql + SELECT @@TX_ISOLATION; -- 会话 + SELECT @@GLOBAL.TX_ISOLATION; -- 系统 + ``` + +* 修改数据库隔离级别 + + ```mysql + SET GLOBAL TRANSACTION ISOLATION LEVEL 级别字符串; + ``` + + + +*** + + + +#### 加锁分析 + +InnoDB 存储引擎支持事务,所以加锁分析是基于该存储引擎 + +* Read Uncommitted 级别,任何操作都不会加锁 + +* Read Committed 级别,增删改操作会加写锁(行锁),读操作不加锁 + + 在 Server 层过滤条件时发现不满足的记录会调用 unlock_row 方法释放该记录的行锁,保证最后只有满足条件的记录加锁,但是扫表过程中每条记录的**加锁操作不能省略**。所以对数据量很大的表做批量修改时,如果无法使用相应的索引(全表扫描),在 Server 过滤数据时就会特别慢,出现虽然没有修改某些行的数据,但是还是被锁住了的现象(锁表),这种情况同样适用于 RR + +* Repeatable Read 级别,增删改操作会加写锁,读操作不加锁。因为读写锁不兼容,**加了读锁后其他事务就无法修改数据**,影响了并发性能,为了保证隔离性和并发性,MySQL 通过 MVCC 解决了读写冲突。RR 级别下的锁有很多种,锁机制章节详解 + +* Serializable 级别,读加共享锁,写加排他锁,读写互斥,使用的悲观锁的理论,实现简单,数据更加安全,但是并发能力非常差 + + * 串行化:让所有事务按顺序单独执行,写操作会加写锁,读操作会加读锁 + * 可串行化:让所有操作相同数据的事务顺序执行,通过加锁实现 + + + +参考文章:https://tech.meituan.com/2014/08/20/innodb-lock.html + + + +*** + + + +### 原子特性 + +#### 实现方式 + +原子性是指事务是一个不可分割的工作单位,事务的操作如果成功就必须要完全应用到数据库,失败则不能对数据库有任何影响。比如事务中一个 SQL 语句执行失败,则已执行的语句也必须回滚,数据库退回到事务前的状态 + +InnoDB 存储引擎提供了两种事务日志:redo log(重做日志)和 undo log(回滚日志) + +* redo log 用于保证事务持久性 +* undo log 用于保证事务原子性和隔离性 + +undo log 属于**逻辑日志**,根据每行操作进行记录,记录了 SQL 执行相关的信息,用来回滚行记录到某个版本 + +当事务对数据库进行修改时,InnoDB 会先记录对应的 undo log,如果事务执行失败或调用了 rollback 导致事务回滚,InnoDB 会根据 undo log 的内容**做与之前相反的操作**: + +* 对于每个 insert,回滚时会执行 delete + +* 对于每个 delete,回滚时会执行 insert + +* 对于每个 update,回滚时会执行一个相反的 update,把数据修改回去 + + + +参考文章:https://www.cnblogs.com/kismetv/p/10331633.html + + + +*** + + + +#### DML 解析 + +##### INSERT + +乐观插入:当前数据页的剩余空间充足,直接将数据进行插入 + +悲观插入:当前数据页的剩余空间不足,需要进行页分裂,申请一个新的页面来插入数据,会造成更多的 redo log,undo log 影响不大 + +当向某个表插入一条记录,实际上需要向聚簇索引和所有二级索引都插入一条记录,但是 undo log **只针对聚簇索引记录**,在回滚时会根据聚簇索引去所有的二级索引进行回滚操作 + +roll_pointer 是一个指针,**指向记录对应的 undo log 日志**,一条记录就是一个数据行,行格式中的 roll_pointer 就指向 undo log + + + +*** + + + +##### DELETE + +插入到页面中的记录会根据 next_record 属性组成一个单向链表,这个链表称为正常链表,被删除的记录也会通过 next_record 组成一个垃圾链表,该链表中所占用的存储空间可以被重新利用,并不会直接清除数据 + +在页面 Page Header 中,PAGE_FREE 属性指向垃圾链表的头节点,删除的工作过程: + +* 将要删除的记录的 delete_flag 位置为 1,其他不做修改,这个过程叫 **delete mark** + +* 在事务提交前,delete_flag = 1 的记录一直都会处于中间状态 + +* 事务提交后,有专门的线程将 delete_flag = 1 的记录从正常链表移除并加入垃圾链表,这个过程叫 **purge** + + purge 线程在执行删除操作时会创建一个 ReadView,根据事务的可见性移除数据(隔离特性部分详解) + +当有新插入的记录时,首先判断 PAGE_FREE 指向的头节点是否足够容纳新纪录: + +* 如果可以容纳新纪录,就会直接重用已删除的记录的存储空间,然后让 PAGE_FREE 指向垃圾链表的下一个节点 +* 如果不能容纳新纪录,就直接向页面申请新的空间存储,并不会遍历垃圾链表 + +重用已删除的记录空间,可能会造成空间碎片,当数据页容纳不了一条记录时,会判断将碎片空间加起来是否可以容纳,判断为真就会重新组织页内的记录: + +* 开辟一个临时页面,将页内记录一次插入到临时页面,此时临时页面时没有碎片的 +* 把临时页面的内容复制到本页,这样就解放出了内存碎片,但是会耗费很大的性能资源 + + + +**** + + + +##### UPDATE + +执行 UPDATE 语句,对于更新主键和不更新主键有两种不同的处理方式 + +不更新主键的情况: + +* 就地更新(in-place update),如果更新后的列和更新前的列占用的存储空间一样大,就可以直接在原记录上修改 + +* 先删除旧纪录,再插入新纪录,这里的删除不是 delete mark,而是直接将记录加入垃圾链表,并且修改页面的相应的控制信息,执行删除的线程不是 purge,是执行更新的用户线程,插入新记录时可能造成页空间不足,从而导致页分裂 + + +更新主键的情况: + +* 将旧纪录进行 delete mark,在更新语句提交后由 purge 线程移入垃圾链表 +* 根据更新的各列的值创建一条新纪录,插入到聚簇索引中 + +在对一条记录修改前会**将记录的隐藏列 trx_id 和 roll_pointer 的旧值记录到当前 undo log 对应的属性中**,这样当前记录的 roll_pointer 指向当前 undo log 记录,当前 undo log 记录的 roll_pointer 指向旧的 undo log 记录,**形成一个版本链** + +UPDATE、DELETE 操作产生的 undo 日志会用于其他事务的 MVCC 操作,所以不能立即删除,INSERT 可以删除的原因是 MVCC 是对现有数据的快照 + + + +*** + + + +#### 回滚日志 + +undo log 是采用段的方式来记录,Rollback Segement 称为回滚段,本质上就是一个类型是 Rollback Segement Header 的页面 + +每个回滚段中有 1024 个 undo slot,每个 slot 存放 undo 链表页面的头节点页号,每个链表对应一个叫 undo log segment 的段 + +* 在以前老版本,只支持 1 个 Rollback Segement,只能记录 1024 个 undo log segment +* MySQL5.5 开始支持 128 个 Rollback Segement,支持 128*1024 个 undo 操作 + +工作流程: + +* 事务执行前需要到系统表空间第 5 号页面中分配一个回滚段(页),获取一个 Rollback Segement Header 页面的地址 +* 回滚段页面有 1024 个 undo slot,首先去回滚段的两个 cached 链表获取缓存的 slot,缓存中没有就在回滚段页面中找一个可用的 undo slot 分配给当前事务 +* 如果是缓存中获取的 slot,则该 slot 对应的 undo log segment 已经分配了,需要重新分配,然后从 undo log segment 中申请一个页面作为日志链表的头节点,并填入对应的 slot 中 +* 每个事务 undo 日志在记录的时候**占用两个 undo 页面的组成链表**,分别为 insert undo 链表和 update undo 链表,链表的头节点页面为 first undo page 会包含一些管理信息,其他页面为 normal undo page + + 说明:事务执行过程的临时表也需要两个 undo 链表,不和普通表共用,这些链表并不是事务开始就分配,而是按需分配 + + + + + +*** + + + +### 隔离特性 + +#### 实现方式 + +隔离性是指,事务内部的操作与其他事务是隔离的,多个并发事务之间要相互隔离,不能互相干扰 + +* 严格的隔离性,对应了事务隔离级别中的 serializable,实际应用中对性能考虑很少使用可串行化 + +* 与原子性、持久性侧重于研究事务本身不同,隔离性研究的是**不同事务**之间的相互影响 + +隔离性让并发情形下的事务之间互不干扰: + +- 一个事务的写操作对另一个事务的写操作(写写):锁机制保证隔离性 +- 一个事务的写操作对另一个事务的读操作(读写):MVCC 保证隔离性 + +锁机制:事务在修改数据之前,需要先获得相应的锁,获得锁之后,事务便可以修改数据;该事务操作期间,这部分数据是锁定的,其他事务如果需要修改数据,需要等待当前事务提交或回滚后释放锁(详解见锁机制) + + + +*** + + + +#### 并发控制 + +MVCC 全称 Multi-Version Concurrency Control,即多版本并发控制,用来**解决读写冲突的无锁并发控制**,可以在发生读写请求冲突时不用加锁解决,这个读是指的快照读(也叫一致性读或一致性无锁读),而不是当前读: + +* 快照读:实现基于 MVCC,因为是多版本并发,所以快照读读到的数据不一定是当前最新的数据,有可能是历史版本的数据 +* 当前读:又叫加锁读,读取数据库记录是当前**最新的版本**(产生幻读、不可重复读),可以对读取的数据进行加锁,防止其他事务修改数据,是悲观锁的一种操作,读写操作加共享锁或者排他锁和串行化事务的隔离级别都是当前读 + +数据库并发场景: + +* 读-读:不存在任何问题,也不需要并发控制 + +* 读-写:有线程安全问题,可能会造成事务隔离性问题,可能遇到脏读,幻读,不可重复读 + +* 写-写:有线程安全问题,可能会存在脏写(丢失更新)问题 + +MVCC 的优点: + +* 在并发读写数据库时,做到在读操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了并发读写的性能 +* 可以解决脏读,不可重复读等事务隔离问题(加锁也能解决),但不能解决更新丢失问题(写锁会解决) + +提高读写和写写的并发性能: + +* MVCC + 悲观锁:MVCC 解决读写冲突,悲观锁解决写写冲突 +* MVCC + 乐观锁:MVCC 解决读写冲突,乐观锁解决写写冲突 + + + +参考文章:https://www.jianshu.com/p/8845ddca3b23 + + + +*** + + + +#### 实现原理 + +##### 隐藏字段 + +实现原理主要是隐藏字段,undo日志,Read View 来实现的 + +InnoDB 存储引擎,数据库中的**聚簇索引**每行数据,除了自定义的字段,还有数据库隐式定义的字段: + +* DB_TRX_ID:最近修改事务 ID,记录创建该数据或最后一次修改该数据的事务 ID +* DB_ROLL_PTR:回滚指针,**指向记录对应的 undo log 日志**,undo log 中又指向上一个旧版本的 undo log +* DB_ROW_ID:隐含的自增 ID(**隐藏主键**),如果数据表没有主键,InnoDB 会自动以 DB_ROW_ID 作为聚簇索引 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-MVCC版本链隐藏字段.png) + + + + + +*** + + + +##### 版本链 + +undo log 是逻辑日志,记录的是每个事务对数据执行的操作,而不是记录的全部数据,要**根据 undo log 逆推出以往事务的数据** + +undo log 的作用: + +* 保证事务进行 rollback 时的原子性和一致性,当事务进行回滚的时候可以用 undo log 的数据进行恢复 +* 用于 MVCC 快照读,通过读取 undo log 的历史版本数据可以实现不同事务版本号都拥有自己独立的快照数据 + +undo log 主要分为两种: + +* insert undo log:事务在 insert 新记录时产生的 undo log,只在事务回滚时需要,并且在事务提交后可以被立即丢弃 + +* update undo log:事务在进行 update 或 delete 时产生的 undo log,在事务回滚时需要,在快照读时也需要。不能随意删除,只有在当前读或事务回滚不涉及该日志时,对应的日志才会被 purge 线程统一清除 + +每次对数据库记录进行改动,都会产生的新版本的 undo log,随着更新次数的增多,所有的版本都会被 roll_pointer 属性连接成一个链表,把这个链表称之为**版本链**,版本链的头节点就是当前的最新的 undo log,链尾就是最早的旧 undo log + +说明:因为 DELETE 删除记录,都是移动到垃圾链表中,不是真正的删除,所以才可以通过版本链访问原始数据 + + + +注意:undo 是逻辑日志,这里只是直观的展示出来 + +工作流程: + +* 有个事务插入 persion 表一条新记录,name 为 Jerry,age 为 24 +* 事务 1 修改该行数据时,数据库会先对该行加排他锁,然后先记录 undo log,然后修改该行 name 为 Tom,并且修改隐藏字段的事务 ID 为当前事务 1 的 ID(默认为 1 之后递增),回滚指针指向拷贝到 undo log 的副本记录,事务提交后,释放锁 +* 以此类推 + + + +*** + + + +##### 读视图 + +Read View 是事务进行读数据操作时产生的读视图,该事务执行快照读的那一刻会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的 ID,用来做可见性判断,根据视图判断当前事务能够看到哪个版本的数据 + +注意:这里的快照并不是把所有的数据拷贝一份副本,而是由 undo log 记录的逻辑日志,根据库中的数据进行计算出历史数据 + +工作流程:将版本链的头节点的事务 ID(最新数据事务 ID,大概率不是当前线程)DB_TRX_ID 取出来,与系统当前活跃事务的 ID 对比进行可见性分析,不可见就通过 DB_ROLL_PTR 回滚指针去取出 undo log 中的下一个 DB_TRX_ID 比较,直到找到最近的满足可见性的 DB_TRX_ID,该事务 ID 所在的旧记录就是当前事务能看见的最新的记录 + +Read View 几个属性: + +- m_ids:生成 Read View 时当前系统中活跃的事务 id 列表(未提交的事务集合,当前事务也在其中) +- min_trx_id:生成 Read View 时当前系统中活跃的最小的事务 id,也就是 m_ids 中的最小值(已提交的事务集合) +- max_trx_id:生成 Read View 时当前系统应该分配给下一个事务的 id 值,m_ids 中的最大值加 1(未开始事务) +- creator_trx_id:生成该 Read View 的事务的事务 id,就是判断该 id 的事务能读到什么数据 + +creator 创建一个 Read View,进行可见性算法分析:(解决了读未提交) + +* db_trx_id == creator_trx_id:表示这个数据就是当前事务自己生成的,自己生成的数据自己肯定能看见,所以此数据对 creator 是可见的 +* db_trx_id < min_trx_id:该版本对应的事务 ID 小于 Read view 中的最小活跃事务 ID,则这个事务在当前事务之前就已经被提交了,对 creator 可见(因为比已提交的最大事务 ID 小的并不一定已经提交,所以应该判断是否在活跃事务列表) + +* db_trx_id >= max_trx_id:该版本对应的事务 ID 大于 Read view 中当前系统的最大事务 ID,则说明该数据是在当前 Read view 创建之后才产生的,对 creator 不可见 +* min_trx_id<= db_trx_id < max_trx_id:判断 db_trx_id 是否在活跃事务列表 m_ids 中 + * 在列表中,说明该版本对应的事务正在运行,数据不能显示(**不能读到未提交的数据**) + * 不在列表中,说明该版本对应的事务已经被提交,数据可以显示(**可以读到已经提交的数据**) + + + +*** + + + +##### 工作流程 + +表 user 数据 + +```sh +id name age +1 张三 18 +``` + +Transaction 20: + +```mysql +START TRANSACTION; -- 开启事务 +UPDATE user SET name = '李四' WHERE id = 1; +UPDATE user SET name = '王五' WHERE id = 1; +``` + +Transaction 60: + +```mysql +START TRANSACTION; -- 开启事务 +-- 操作表的其他数据 +``` + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-MVCC工作流程1.png) + +ID 为 0 的事务创建 Read View: + +* m_ids:20、60 +* min_trx_id:20 +* max_trx_id:61 +* creator_trx_id:0 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-MVCC工作流程2.png) + +只有红框部分才复合条件,所以只有张三对应的版本的数据可以被看到 + + + +参考视频:https://www.bilibili.com/video/BV1t5411u7Fg + + + +*** + + + +##### 二级索引 + +只有在聚簇索引中才有 trx_id 和 roll_pointer 的隐藏列,对于二级索引判断可见性的方式: + +* 二级索引页面的 Page Header 中有一个 PAGE_MAX_TRX_ID 属性,代表修改当前页面的最大的事务 ID,SELECT 语句访问某个二级索引时会判断 ReadView 的 min_trx_id 是否大于该属性,大于说明该页面的所有属性对 ReadView 可见 +* 如果属性判断不可见,就需要利用二级索引获取主键值进行**回表操作**,得到聚簇索引后按照聚簇索引的可见性判断的方法操作 + + + +*** + + + +#### RC RR + +Read View 用于支持 RC(Read Committed,读已提交)和 RR(Repeatable Read,可重复读)隔离级别的实现,所以 **SELECT 在 RC 和 RR 隔离级别使用 MVCC 读取记录** + +RR、RC 生成时机: + +- RC 隔离级别下,每次读取数据前都会生成最新的 Read View(当前读) +- RR 隔离级别下,在第一次数据读取时才会创建 Read View(快照读) + +RC、RR 级别下的 InnoDB 快照读区别 + +- RC 级别下,事务中每次快照读都会新生成一个 Read View,这就是在 RC 级别下的事务中可以看到别的事务提交的更新的原因 + +- RR 级别下,某个事务的对某条记录的**第一次快照读**会创建一个 Read View, 将当前系统活跃的其他事务记录起来,此后在调用快照读的时候,使用的是同一个 Read View,所以一个事务的查询结果每次都是相同的 + + RR 级别下,通过 `START TRANSACTION WITH CONSISTENT SNAPSHOT` 开启事务,会在执行该语句后立刻生成一个 Read View,不是在执行第一条 SELECT 语句时生成(所以说 `START TRANSACTION` 并不是事务的起点,执行第一条语句才算起点) + +解决幻读问题: + +- 快照读:通过 MVCC 来进行控制的,在可重复读隔离级别下,普通查询是快照读,是不会看到别的事务插入的数据的,但是**并不能完全避免幻读** + + 场景:RR 级别,T1 事务开启,创建 Read View,此时 T2 去 INSERT 新的一行然后提交,然后 T1 去 UPDATE 该行会发现更新成功,并且把这条新记录的 trx_id 变为当前的事务 id,所以对当前事务就是可见的。因为 **Read View 并不能阻止事务去更新数据,更新数据都是先读后写并且是当前读**,读取到的是最新版本的数据 + +- 当前读:通过 next-key 锁(行锁 + 间隙锁)来解决问题 + + + + + +*** + + + +### 持久特性 + +#### 实现方式 + +持久性是指一个事务一旦被提交了,那么对数据库中数据的改变就是永久性的,接下来的其他操作或故障不应该对其有任何影响。 + +Buffer Pool 的使用提高了读写数据的效率,但是如果 MySQL 宕机,此时 Buffer Pool 中修改的数据还没有刷新到磁盘,就会导致数据的丢失,事务的持久性无法保证,所以引入了 redo log 日志: + +* redo log **记录数据页的物理修改**,而不是某一行或某几行的修改,用来恢复提交后的数据页,只能**恢复到最后一次提交**的位置 +* redo log 采用的是 WAL(Write-ahead logging,**预写式日志**),所有修改要先写入日志,再更新到磁盘,保证了数据不会因 MySQL 宕机而丢失,从而满足了持久性要求 +* 简单的 redo log 是纯粹的物理日志,复杂的 redo log 会存在物理日志和逻辑日志 + +工作过程:MySQL 发生了宕机,InnoDB 会判断一个数据页在崩溃恢复时丢失了更新,就会将它读到内存,然后根据 redo log 内容更新内存,更新完成后,内存页变成脏页,然后进行刷脏 + +缓冲池的**刷脏策略**: + +* redo log 文件是固定大小的,如果写满了就要擦除以前的记录,在擦除之前需要把对应的更新持久化到磁盘中 +* Buffer Pool 内存不足,需要淘汰部分数据页(LRU 链表尾部),如果淘汰的是脏页,就要先将脏页写到磁盘(要避免大事务) +* 系统空闲时,后台线程会自动进行刷脏(Flush 链表部分已经详解) +* MySQL 正常关闭时,会把内存的脏页都刷新到磁盘上 + + + +**** + + + +#### 重做日志 + +##### 日志缓冲 + +服务器启动时会向操作系统申请一片连续内存空间作为 redo log buffer(重做日志缓冲区),可以通过 `innodb_log_buffer_size` 系统变量指定 redo log buffer 的大小,默认是 16MB + +log buffer 被划分为若干 redo log block(块,类似数据页的概念),每个默认大小 512 字节,每个 block 由 12 字节的 log block head、496 字节的 log block body、4 字节的 log block trailer 组成 + +* 当数据修改时,先修改 Change Buffer 中的数据,然后在 redo log buffer 记录这次操作,写入 log buffer 的过程是**顺序写入**的(先写入前面的 block,写满后继续写下一个) +* log buffer 中有一个指针 buf_free,来标识该位置之前都是填满的 block,该位置之后都是空闲区域 + +MySQL 规定对底层页面的一次原子访问称为一个 Mini-Transaction(MTR),比如在 B+ 树上插入一条数据就算一个 MTR + +* 一个事务包含若干个 MTR,一个 MTR 对应一组若干条 redo log,一组 redo log 是不可分割的,在进行数据恢复时也把一组 redo log 当作一个不可分割的整体处理 + +* 不是每生成一条 redo 日志就将其插入到 log buffer 中,而是一个 MTR 结束后**将一组 redo 日志写入** + +InnoDB 的 redo log 是**固定大小**的,redo 日志在磁盘中以文件组的形式存储,同一组中的每个文件大小一样格式一样 + +* `innodb_log_group_home_dir` 代表磁盘存储 redo log 的文件目录,默认是当前数据目录 +* `innodb_log_file_size` 代表文件大小,默认 48M,`innodb_log_files_in_group` 代表文件个数,默认 2 最大 100,所以日志的文件大小为 `innodb_log_file_size * innodb_log_files_in_group` + +redo 日志文件也是由若干个 512 字节的 block 组成,日志文件的前 2048 个字节(前 4 个 block)用来存储一些管理信息,以后的用来存储 log buffer 中的 block 镜像 + +注意:block 并不代表一组 redo log,一组日志可能占用不到一个 block 或者几个 block,依赖于 MTR 的大小 + + + +*** + + + +##### 日志刷盘 + +redo log 需要在事务提交时将日志写入磁盘,但是比 Buffer Pool 修改的数据写入磁盘的速度快,原因: + +* 刷脏是随机 IO,因为每次修改的数据位置随机;redo log 和 binlog 都是**顺序写**,磁盘的顺序 IO 比随机 IO 速度要快 +* 刷脏是以数据页(Page)为单位的,一个页上的一个小修改都要整页写入;redo log 中只包含真正需要写入的部分,好几页的数据修改可能只记录在一个 redo log 页中,减少无效 IO +* **组提交机制**,可以大幅度降低磁盘的 IO 消耗 + +InnoDB 引擎会在适当的时候,把内存中 redo log buffer 持久化(fsync)到磁盘,具体的**刷盘策略**: + +* 在事务提交时需要进行刷盘,通过修改参数 `innodb_flush_log_at_trx_commit` 设置: + * 0:表示当提交事务时,并不将缓冲区的 redo 日志写入磁盘,而是等待**后台线程每秒刷新一次** + * 1:在事务提交时将缓冲区的 redo 日志**同步写入**到磁盘,保证一定会写入成功(默认值) + * 2:在事务提交时将缓冲区的 redo 日志异步写入到磁盘,不能保证提交时肯定会写入,只是有这个动作。日志已经在操作系统的缓存,如果操作系统没有宕机而 MySQL 宕机,也是可以恢复数据的 +* 写入 redo log buffer 的日志超过了总容量的一半,就会将日志刷入到磁盘文件,这会影响执行效率,所以开发中应**避免大事务** +* 服务器关闭时 +* 并行的事务提交(组提交)时,会将将其他事务的 redo log 持久化到磁盘。假设事务 A 已经写入 redo log buffer 中,这时另外一个线程的事务 B 提交,如果 innodb_flush_log_at_trx_commit 设置的是 1,那么事务 B 要把 redo log buffer 里的日志全部持久化到磁盘,**因为多个事务共用一个 redo log buffer**,所以一次 fsync 可以刷盘多个事务的 redo log,提升了并发量 + +服务器启动后 redo 磁盘空间不变,所以 redo 磁盘中的日志文件是被**循环使用**的,采用循环写数据的方式,写完尾部重新写头部,所以要确保头部 log 对应的修改已经持久化到磁盘 + + + +*** + + + +##### 日志序号 + +lsn (log sequence number) 代表已经写入的 redo 日志量、flushed_to_disk_lsn 指刷新到磁盘中的 redo 日志量,两者都是**全局变量**,如果两者的值相同,说明 log buffer 中所有的 redo 日志都已经持久化到磁盘 + +工作过程:写入 log buffer 数据时,buf_free 会进行偏移,偏移量就会加到 lsn 上 + +MTR 的执行过程中修改过的页对应的控制块会加到 Buffer Pool 的 flush 链表中,链表中脏页是按照第一次修改的时间进行排序的(头插),控制块中有两个指针用来记录脏页被修改的时间: + +* oldest_modification:第一次修改 Buffer Pool 中某个缓冲页时,将修改该页的 MTR **开始时**对应的 lsn 值写入这个属性 +* newest_modification:每次修改页面,都将 MTR 结束时全局的 lsn 值写入这个属性,所以该值是该页面最后一次修改后的 lsn 值 + +全局变量 checkpoint_lsn 表示**当前系统可以被覆盖的 redo 日志总量**,当 redo 日志对应的脏页已经被刷新到磁盘后,该文件空间就可以被覆盖重用,此时执行一次 checkpoint 来更新 checkpoint_lsn 的值存入管理信息(刷脏和执行一次 checkpoint 并不是同一个线程),该值的增量就代表磁盘文件中当前位置向后可以被覆盖的文件的量,所以该值是一直增大的 + +**checkpoint**:从 flush 链表尾部中找出还未刷脏的页面,该页面是当前系统中最早被修改的脏页,该页面之前产生的脏页都已经刷脏,然后将该页 oldest_modification 值赋值给 checkpoint_lsn,因为 lsn 小于该值时产生的 redo 日志都可以被覆盖了 + +但是在系统忙碌时,后台线程的刷脏操作不能将脏页快速刷出,导致系统无法及时执行 checkpoint ,这时需要用户线程从 flush 链表中把最早修改的脏页刷新到磁盘中,然后执行 checkpoint + +```java +write pos ------- checkpoint_lsn // 两值之间的部分表示可以写入的日志量,当 pos 追赶上 lsn 时必须执行 checkpoint +``` + +使用命令可以查看当前 InnoDB 存储引擎各种 lsn 的值: + +```mysql +SHOW ENGINE INNODB STATUS\G +``` + + + +**** + + + +##### 崩溃恢复 + +恢复的起点:在从 redo 日志文件组的管理信息中获取最近发生 checkpoint 的信息,**从 checkpoint_lsn 对应的日志文件开始恢复** + +恢复的终点:扫描日志文件的 block,block 的头部记录着当前 block 使用了多少字节,填满的 block 总是 512 字节, 如果某个 block 不是 512 字节,说明该 block 就是需要恢复的最后一个 block + +恢复的过程:按照 redo log 依次执行恢复数据,优化方式 + +* 使用哈希表:根据 redo log 的 space id 和 page number 属性计算出哈希值,将对同一页面的修改放入同一个槽里,可以一次性完成对某页的恢复,**避免了随机 IO** +* 跳过已经刷新到磁盘中的页面:数据页的 File Header 中的 FILE_PAGE_LSN 属性(类似 newest_modification)表示最近一次修改页面时的 lsn 值,数据页被刷新到磁盘中,那么该页 lsn 属性肯定大于 checkpoint_lsn + + + +参考书籍:https://book.douban.com/subject/35231266/ + + + +*** + + + +#### 工作流程 + +##### 日志对比 + +MySQL 中还存在 binlog(二进制日志)也可以记录写操作并用于数据的恢复,**保证数据不丢失**,二者的区别是: + +* 作用不同:redo log 是用于 crash recovery (故障恢复),保证 MySQL 宕机也不会影响持久性;binlog 是用于 point-in-time recovery 的,保证服务器可以基于时间点恢复数据,此外 binlog 还用于主从复制 +* 层次不同:redo log 是 InnoDB 存储引擎实现的,而 binlog 是MySQL的 Server 层实现的,同时支持 InnoDB 和其他存储引擎 +* 内容不同:redo log 是物理日志,内容基于磁盘的 Page;binlog 的内容是二进制的,根据 binlog_format 参数的不同,可能基于SQL 语句、基于数据本身或者二者的混合(日志部分详解) +* 写入时机不同:binlog 在事务提交时一次写入;redo log 的写入时机相对多元 + +binlog 为什么不支持崩溃恢复? + +* binlog 记录的是语句,并不记录数据页级的数据(哪个页改了哪些地方),所以没有能力恢复数据页 +* binlog 是追加写,保存全量的日志,没有标志确定从哪个点开始的数据是已经刷盘了,而 redo log 只要在 checkpoint_lsn 后面的就是没有刷盘的 + + + +*** + + + +##### 更新记录 + +更新一条记录的过程:写之前一定先读 + +* 在 B+ 树中定位到该记录,如果该记录所在的页面不在 Buffer Pool 里,先将其加载进内存 + +* 首先更新该记录对应的聚簇索引,更新聚簇索引记录时: + * 更新记录前向 undo 页面写 undo 日志,由于这是更改页面,所以需要记录一下相应的 redo 日志 + + 注意:修改 undo 页面也是在**修改页面**,事务只要修改页面就需要先记录相应的 redo 日志 + + * 然后**记录对应的 redo 日志**(等待 MTR 提交后写入 redo log buffer),**最后进行真正的更新记录** + +* 更新其他的二级索引记录,不会再记录 undo log,只记录 redo log 到 buffer 中 + +* 在一条更新语句执行完成后(也就是将所有待更新记录都更新完了),就会开始记录该语句对应的 binlog 日志,此时记录的 binlog 并没有刷新到硬盘上,还在内存中,在事务提交时才会统一将该事务运行过程中的所有 binlog 日志刷新到硬盘 + +假设表中有字段 id 和 a,存在一条 `id = 1, a = 2` 的记录,此时执行更新语句: + +```sql +update table set a=2 where id=1; +``` + +InnoDB 会真正的去执行把值修改成 (1,2) 这个操作,先加行锁,在去更新,并不会提前判断相同就不修改了 + + + +参考文章:https://mp.weixin.qq.com/s/wcJ2KisSaMnfP4nH5NYaQA + + + +*** + + + +##### 两段提交 + +当客户端执行 COMMIT 语句或者在自动提交的情况下,MySQL 内部开启一个 XA 事务,分两阶段来完成 XA 事务的提交: + +```sql +update T set c=c+1 where ID=2; +``` + + + +流程说明:执行引擎将这行新数据读入到内存中(Buffer Pool)后,先将此次更新操作记录到 redo log buffer 里,然后更新记录。最后将 redo log 刷盘后事务处于 prepare 状态,执行器会生成这个操作的 binlog,并**把 binlog 写入磁盘**,完成提交 + +两阶段: + +* Prepare 阶段:存储引擎将该事务的 **redo 日志刷盘**,并且将本事务的状态设置为 PREPARE,代表执行完成随时可以提交事务 +* Commit 阶段:先将事务执行过程中产生的 binlog 刷新到硬盘,再执行存储引擎的提交工作,引擎把 redo log 改成提交状态 + +存储引擎层的 redo log 和 server 层的 binlog 可以认为是一个分布式事务, 都可以用于表示事务的提交状态,而**两阶段提交就是让这两个状态保持逻辑上的一致**,也有利于主从复制,更好的保持主从数据的一致性 + + + +*** + + + +##### 数据恢复 + +系统崩溃前没有提交的事务的 redo log 可能已经刷盘(定时线程或者 checkpoint),怎么处理崩溃恢复? + +工作流程:获取 undo 链表首节点页面的 undo segement header 中的 TRX_UNDO_STATE 属性,表示当前链表的事务属性,**事务状态是活跃(未提交)的就全部回滚**,如果是 PREPARE 状态,就需要根据 binlog 的状态进行判断: + +* 如果在时刻 A 发生了崩溃(crash),由于此时 binlog 还没完成,所以需要进行回滚 +* 如果在时刻 B 发生了崩溃,redo log 和 binlog 有一个共**同的数据字段叫 XID**,崩溃恢复的时候,会按顺序扫描 redo log: + * 如果 redo log 里面的事务是完整的,也就是已经有了 commit 标识,说明 binlog 也已经记录完整,直接从 redo log 恢复数据 + * 如果 redo log 里面的事务只有 prepare,就根据 XID 去 binlog 中判断对应的事务是否存在并完整,如果完整可以恢复数据 + + +判断一个事务的 binlog 是否完整的方法: + +* statement 格式的 binlog,最后会有 COMMIT +* row 格式的 binlog,最后会有一个 XID event +* MySQL 5.6.2 版本以后,引入了 binlog-checksum 参数用来验证 binlog 内容的正确性(可能日志中间出错) + + + +参考文章:https://time.geekbang.org/column/article/73161 + + + +*** + + + +#### 刷脏优化 + +系统在进行刷脏时会占用一部分系统资源,会影响系统的性能,**产生系统抖动** + +* 一个查询要淘汰的脏页个数太多,会导致查询的响应时间明显变长 +* 日志写满,更新全部堵住,写性能跌为 0,这种情况对敏感业务来说,是不能接受的 + +InnoDB 刷脏页的控制策略: + +* `innodb_io_capacity` 参数代表磁盘的读写能力,建议设置成磁盘的 IOPS(每秒的 IO 次数) +* 刷脏速度参考两个因素:脏页比例和 redo log 写盘速度 + * 参数 `innodb_max_dirty_pages_pct` 是脏页比例上限,默认值是 75%,InnoDB 会根据当前的脏页比例,算出一个范围在 0 到 100 之间的数字 + * InnoDB 每次写入的日志都有一个序号,当前写入的序号跟 checkpoint 对应的序号之间的差值,InnoDB 根据差值算出一个范围在 0 到 100 之间的数字 + * 两者较大的值记为 R,执行引擎按照 innodb_io_capacity 定义的能力乘以 R% 来控制刷脏页的速度 +* `innodb_flush_neighbors` 参数置为 1 代表控制刷脏时检查相邻的数据页,如果也是脏页就一起刷脏,并检查邻居的邻居,这个行为会一直蔓延直到不是脏页,在 MySQL 8.0 中该值的默认值是 0,不建议开启此功能 + + + + + +**** + + + +### 一致特性 + +一致性是指事务执行前后,数据库的完整性约束没有被破坏,事务执行的前后都是合法的数据状态。 + +数据库的完整性约束包括但不限于:实体完整性(如行的主键存在且唯一)、列完整性(如字段的类型、大小、长度要符合要求)、外键约束、用户自定义完整性(如转账前后,两个账户余额的和应该不变) + +实现一致性的措施: + +- 保证原子性、持久性和隔离性,如果这些特性无法保证,事务的一致性也无法保证 +- 数据库本身提供保障,例如不允许向整形列插入字符串值、字符串长度不能超过列的限制等 +- 应用层面进行保障,例如如果转账操作只扣除转账者的余额,而没有增加接收者的余额,无论数据库实现的多么完美,也无法保证状态的一致 + + + + + +**** + + + + + +## 锁机制 + +### 基本介绍 + +锁机制:数据库为了保证数据的一致性,在共享的资源被并发访问时变得安全有序所设计的一种规则 + +利用 MVCC 性质进行读取的操作叫**一致性读**,读取数据前加锁的操作叫**锁定读** + +锁的分类: + +- 按操作分类: + - 共享锁:也叫读锁。对同一份数据,多个事务读操作可以同时加锁而不互相影响 ,但不能修改数据 + - 排他锁:也叫写锁。当前的操作没有完成前,会阻断其他操作的读取和写入 +- 按粒度分类: + - 表级锁:会锁定整个表,开销小,加锁快;不会出现死锁;锁定力度大,发生锁冲突概率高,并发度最低,偏向 MyISAM + - 行级锁:会锁定当前操作行,开销大,加锁慢;会出现死锁;锁定力度小,发生锁冲突概率低,并发度高,偏向 InnoDB + - 页级锁:锁的力度、发生冲突的概率和加锁开销介于表锁和行锁之间,会出现死锁,并发性能一般 +- 按使用方式分类: + - 悲观锁:每次查询数据时都认为别人会修改,很悲观,所以查询时加锁 + - 乐观锁:每次查询数据时都认为别人不会修改,很乐观,但是更新时会判断一下在此期间别人有没有去更新这个数据 + +* 不同存储引擎支持的锁 + + | 存储引擎 | 表级锁 | 行级锁 | 页级锁 | + | -------- | -------- | -------- | ------ | + | MyISAM | 支持 | 不支持 | 不支持 | + | InnoDB | **支持** | **支持** | 不支持 | + | MEMORY | 支持 | 不支持 | 不支持 | + | BDB | 支持 | 不支持 | 支持 | + +从锁的角度来说:表级锁更适合于以查询为主,只有少量按索引条件更新数据的应用,如 Web 应用;而行级锁则更适合于有大量按索引条件并发更新少量不同数据,同时又有并查询的应用,如一些在线事务处理系统 + + + +*** + + + +### 内存结构 + +对一条记录加锁的本质就是**在内存中**创建一个锁结构与之关联,结构包括 + +* 事务信息:锁对应的事务信息,一个锁属于一个事务 +* 索引信息:对于行级锁,需要记录加锁的记录属于哪个索引 +* 表锁和行锁信息:表锁记录着锁定的表,行锁记录了 Space ID 所在表空间、Page Number 所在的页号、n_bits 使用了多少比特 +* type_mode:一个 32 比特的数,被分成 lock_mode、lock_type、rec_lock_type 三个部分 + * lock_mode:锁模式,记录是共享锁、排他锁、意向锁之类 + * lock_type:代表表级锁还是行级锁 + * rec_lock_type:代表行锁的具体类型和 is_waiting 属性,is_waiting = true 时表示当前事务尚未获取到锁,处于等待状态。事务获取锁后的锁结构是 is_waiting 为 false,释放锁时会检查是否与当前记录关联的锁结构,如果有就唤醒对应事务的线程 + +一个事务可能操作多条记录,为了节省内存,满足下面条件的锁使用同一个锁结构: + +* 在同一个事务中的加锁操作 +* 被加锁的记录在同一个页面中 +* 加锁的类型是一样的 +* 加锁的状态是一样的 + + + + + +**** + + + +### Server + +MySQL 里面表级别的锁有两种:一种是表锁,一种是元数据锁(meta data lock,MDL) + +MDL 叫元数据锁,主要用来保护 MySQL 内部对象的元数据,保证数据读写的正确性,**当对一个表做增删改查的时候,加 MDL 读锁;当要对表做结构变更操作 DDL 的时候,加 MDL 写锁**,两种锁不相互兼容,所以可以保证 DDL、DML、DQL 操作的安全 + +说明:DDL 操作执行前会隐式提交当前会话的事务,因为 DDL 一般会在若干个特殊事务中完成,开启特殊事务前需要提交到其他事务 + +MDL 锁的特性: + +* MDL 锁不需要显式使用,在访问一个表的时候会被自动加上,在事务开始时申请,整个事务提交后释放(执行完单条语句不释放) + +* MDL 锁是在 Server 中实现,不是 InnoDB 存储引擎层能直接实现的锁 + +* MDL 锁还能实现其他粒度级别的锁,比如全局锁、库级别的锁、表空间级别的锁 + +FLUSH TABLES WITH READ LOCK 简称(FTWRL),全局读锁,让整个库处于只读状态,DDL DML 都被阻塞,工作流程: + +1. 上全局读锁(lock_global_read_lock) +2. 清理表缓存(close_cached_tables) +3. 上全局 COMMIT 锁(make_global_read_lock_block_commit) + +该命令主要用于备份工具做**一致性备份**,由于 FTWRL 需要持有两把全局的 MDL 锁,并且还要关闭所有表对象,因此杀伤性很大 + + + +*** + + + +### MyISAM + +#### 表级锁 + +MyISAM 存储引擎只支持表锁,这也是 MySQL 开始几个版本中唯一支持的锁类型 + +MyISAM 引擎在执行查询语句之前,会**自动**给涉及到的所有表加读锁,在执行增删改之前,会**自动**给涉及的表加写锁,这个过程并不需要用户干预,所以用户一般不需要直接用 LOCK TABLE 命令给 MyISAM 表显式加锁 + +* 加锁命令:(对 InnoDB 存储引擎也适用) + + 读锁:所有连接只能读取数据,不能修改 + + 写锁:其他连接不能查询和修改数据 + + ```mysql + -- 读锁 + LOCK TABLE table_name READ; + + -- 写锁 + LOCK TABLE table_name WRITE; + ``` + +* 解锁命令: + + ```mysql + -- 将当前会话所有的表进行解锁 + UNLOCK TABLES; + ``` + +锁的兼容性: + +* 对 MyISAM 表的读操作,不会阻塞其他用户对同一表的读请求,但会阻塞对同一表的写请求 +* 对 MyISAM 表的写操作,则会阻塞其他用户对同一表的读和写操作 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-MyISAM 锁的兼容性.png) + +锁调度:**MyISAM 的读写锁调度是写优先**,因为写锁后其他线程不能做任何操作,大量的更新会使查询很难得到锁,从而造成永远阻塞,所以 MyISAM 不适合做写为主的表的存储引擎 + + + +*** + + + +#### 锁操作 + +##### 读锁 + +两个客户端操作 Client 1和 Client 2,简化为 C1、C2 + +* 数据准备: + + ```mysql + CREATE TABLE `tb_book` ( + `id` INT(11) AUTO_INCREMENT, + `name` VARCHAR(50) DEFAULT NULL, + `publish_time` DATE DEFAULT NULL, + `status` CHAR(1) DEFAULT NULL, + PRIMARY KEY (`id`) + ) ENGINE=MYISAM DEFAULT CHARSET=utf8 ; + + INSERT INTO tb_book (id, NAME, publish_time, STATUS) VALUES(NULL,'java编程思想','2088-08-01','1'); + INSERT INTO tb_book (id, NAME, publish_time, STATUS) VALUES(NULL,'mysql编程思想','2088-08-08','0'); + ``` + +* C1、C2 加读锁,同时查询可以正常查询出数据 + + ```mysql + LOCK TABLE tb_book READ; -- C1、C2 + SELECT * FROM tb_book; -- C1、C2 + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-MyISAM 读锁1.png) + +* C1 加读锁,C1、C2 查询未锁定的表,C1 报错,C2 正常查询 + + ```mysql + LOCK TABLE tb_book READ; -- C1 + SELECT * FROM tb_user; -- C1、C2 + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-MyISAM 读锁2.png) + + C1、C2 执行插入操作,C1 报错,C2 等待获取 + + ```mysql + INSERT INTO tb_book VALUES(NULL,'Spring高级','2088-01-01','1'); -- C1、C2 + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-MyISAM 读锁3.png) + + 当在 C1 中释放锁指令 UNLOCK TABLES,C2 中的 INSERT 语句立即执行 + + + +*** + + + +##### 写锁 + +两个客户端操作 Client 1和 Client 2,简化为 C1、C2 + +* C1 加写锁,C1、C2查询表,C1 正常查询,C2 需要等待 + + ```mysql + LOCK TABLE tb_book WRITE; -- C1 + SELECT * FROM tb_book; -- C1、C2 + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-MyISAM 写锁1.png) + + 当在 C1 中释放锁指令 UNLOCK TABLES,C2 中的 SELECT 语句立即执行 + +* C1、C2 同时加写锁 + + ```mysql + LOCK TABLE tb_book WRITE; + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-MyISAM 写锁2.png) + +* C1 加写锁,C1、C2查询未锁定的表,C1 报错,C2 正常查询 + + + +*** + + + +#### 锁状态 + +* 查看锁竞争: + + ```mysql + SHOW OPEN TABLES; + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-锁争用情况查看1.png) + + In_user:表当前被查询使用的次数,如果该数为零,则表是打开的,但是当前没有被使用 + + Name_locked:表名称是否被锁定,名称锁定用于取消表或对表进行重命名等操作 + + ```mysql + LOCK TABLE tb_book READ; -- 执行命令 + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-锁争用情况查看2.png) + +* 查看锁状态: + + ```mysql + SHOW STATUS LIKE 'Table_locks%'; + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-MyISAM 锁状态.png) + + Table_locks_immediate:指的是能立即获得表级锁的次数,每立即获取锁,值加 1 + + Table_locks_waited:指的是不能立即获取表级锁而需要等待的次数,每等待一次,该值加 1,此值高说明存在着较为严重的表级锁争用情况 + + + +*** + + + +### InnoDB + +#### 行级锁 + +##### 记录锁 + +InnoDB 与 MyISAM 的最大不同有两点:一是支持事务;二是采用了行级锁,**InnoDB 同时支持表锁和行锁** + +行级锁,也称为记录锁(Record Lock),InnoDB 实现了以下两种类型的行锁: + +- 共享锁 (S):又称为读锁,简称 S 锁,多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改 +- 排他锁 (X):又称为写锁,简称 X 锁,不能与其他锁并存,获取排他锁的事务是可以对数据读取和修改 + +RR 隔离界别下,对于 UPDATE、DELETE 和 INSERT 语句,InnoDB 会**自动给涉及数据集加排他锁**(行锁),在 commit 时自动释放;对于普通 SELECT 语句,不会加任何锁(只是针对 InnoDB 层来说的,因为在 Server 层会**加 MDL 读锁**),通过 MVCC 防止并发冲突 + +在事务中加的锁,并不是不需要了就释放,而是在事务中止或提交时自动释放,这个就是**两阶段锁协议**。所以一般将更新共享资源(并发高)的 SQL 放到事务的最后执行,可以让其他线程尽量的减少等待时间 + +锁的兼容性: + +- 共享锁和共享锁 兼容 +- 共享锁和排他锁 冲突 +- 排他锁和排他锁 冲突 +- 排他锁和共享锁 冲突 + +显式给数据集加共享锁或排他锁:**加锁读就是当前读,读取的是最新数据** + +```mysql +SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE -- 共享锁 +SELECT * FROM table_name WHERE ... FOR UPDATE -- 排他锁 +``` + +注意:**锁默认会锁聚簇索引(锁就是加在索引上)**,但是当使用覆盖索引时,加共享锁只锁二级索引,不锁聚簇索引 + + + +*** + + + +##### 锁操作 + +两个客户端操作 Client 1和 Client 2,简化为 C1、C2 + +* 环境准备 + + ```mysql + CREATE TABLE test_innodb_lock( + id INT(11), + name VARCHAR(16), + sex VARCHAR(1) + )ENGINE = INNODB DEFAULT CHARSET=utf8; + + INSERT INTO test_innodb_lock VALUES(1,'100','1'); + -- .......... + + CREATE INDEX idx_test_innodb_lock_id ON test_innodb_lock(id); + CREATE INDEX idx_test_innodb_lock_name ON test_innodb_lock(name); + ``` + +* 关闭自动提交功能: + + ```mysql + SET AUTOCOMMIT=0; -- C1、C2 + ``` + + 正常查询数据: + + ```mysql + SELECT * FROM test_innodb_lock; -- C1、C2 + ``` + +* 查询 id 为 3 的数据,正常查询: + + ```mysql + SELECT * FROM test_innodb_lock WHERE id=3; -- C1、C2 + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-InnoDB 锁操作1.png) + +* C1 更新 id 为 3 的数据,但不提交: + + ```mysql + UPDATE test_innodb_lock SET name='300' WHERE id=3; -- C1 + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-InnoDB 锁操作2.png) + + C2 查询不到 C1 修改的数据,因为隔离界别为 REPEATABLE READ,C1 提交事务,C2 查询: + + ```mysql + COMMIT; -- C1 + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-InnoDB 锁操作3.png) + + 提交后仍然查询不到 C1 修改的数据,因为隔离级别可以防止脏读、不可重复读,所以 C2 需要提交才可以查询到其他事务对数据的修改: + + ```mysql + COMMIT; -- C2 + SELECT * FROM test_innodb_lock WHERE id=3; -- C2 + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-InnoDB 锁操作4.png) + +* C1 更新 id 为 3 的数据,但不提交,C2 也更新 id 为 3 的数据: + + ```mysql + UPDATE test_innodb_lock SET name='3' WHERE id=3; -- C1 + UPDATE test_innodb_lock SET name='30' WHERE id=3; -- C2 + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-InnoDB 锁操作5.png) + + 当 C1 提交,C2 直接解除阻塞,直接更新 + +* 操作不同行的数据: + + ```mysql + UPDATE test_innodb_lock SET name='10' WHERE id=1; -- C1 + UPDATE test_innodb_lock SET name='30' WHERE id=3; -- C2 + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-InnoDB 锁操作6.png) + + 由于 C1、C2 操作的不同行,获取不同的行锁,所以都可以正常获取行锁 + + + + + +**** + + + +#### 锁分类 + +##### 间隙锁 + +InnoDB 会对间隙(GAP)进行加锁,就是间隙锁 (RR 隔离级别下才有该锁)。间隙锁之间不存在冲突关系,**多个事务可以同时对一个间隙加锁**,但是间隙锁会阻止往这个间隙中插入一个记录的操作 + +InnoDB 加锁的基本单位是 next-key lock,该锁是行锁和 gap lock 的组合(X or S 锁),但是加锁过程是分为间隙锁和行锁两段执行 + +* 可以**保护当前记录和前面的间隙**,遵循左开右闭原则,单纯的间隙锁是左开右开 +* 假设有 10、11、13,那么可能的间隙锁包括:(负无穷,10]、(10,11]、(11,13]、(13,正无穷) + +几种索引的加锁情况: + +* 唯一索引加锁在值存在时是行锁,next-key lock 会退化为行锁,值不存在会变成间隙锁 +* 普通索引加锁会继续向右遍历到不满足条件的值为止,next-key lock 退化为间隙锁 +* 范围查询无论是否是唯一索引,都需要访问到不满足条件的第一个值为止 +* 对于联合索引且是唯一索引,如果 where 条件只包括联合索引的一部分,那么会加间隙锁 + +间隙锁优点:RR 级别下间隙锁可以**解决事务的一部分的幻读问题**,通过对间隙加锁,可以防止读取过程中数据条目发生变化。一部分的意思是不会对全部间隙加锁,只能加锁一部分的间隙 + +间隙锁危害: + +* 当锁定一个范围的键值后,即使某些不存在的键值也会被无辜的锁定,造成在锁定的时候无法插入锁定键值范围内的任何数据,在某些场景下这可能会对性能造成很大的危害,影响并发度 +* 事务 A B 同时锁住一个间隙后,A 往当前间隙插入数据时会被 B 的间隙锁阻塞,B 也执行插入间隙数据的操作时就会**产生死锁** + +现场演示: + +* 关闭自动提交功能: + + ```mysql + SET AUTOCOMMIT=0; -- C1、C2 + ``` + +* 查询数据表: + + ```mysql + SELECT * FROM test_innodb_lock; + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-InnoDB 间隙锁1.png) + +* C1 根据 id 范围更新数据,C2 插入数据: + + ```mysql + UPDATE test_innodb_lock SET name='8888' WHERE id < 4; -- C1 + INSERT INTO test_innodb_lock VALUES(2,'200','2'); -- C2 + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-InnoDB 间隙锁2.png) + + 出现间隙锁,C2 被阻塞,等待 C1 提交事务后才能更新 + + + +**** + + + +##### 意向锁 + +InnoDB 为了支持多粒度的加锁,允许行锁和表锁同时存在,支持在不同粒度上的加锁操作,InnoDB 增加了意向锁(Intention Lock) + +意向锁是将锁定的对象分为多个层次,意向锁意味着事务希望在更细粒度上进行加锁,意向锁分为两种: + +* 意向共享锁(IS):事务有意向对表加共享锁 +* 意向排他锁(IX):事务有意向对表加排他锁 + +**IX,IS 是表级锁**,不会和行级的 X,S 锁发生冲突,意向锁是在加表级锁之前添加,为了在加表级锁时可以快速判断表中是否有记录被上锁,比如向一个表添加表级 X 锁的时: + +- 没有意向锁,则需要遍历整个表判断是否有锁定的记录 +- 有了意向锁,首先判断是否存在意向锁,然后判断该意向锁与即将添加的表级锁是否兼容即可,因为意向锁的存在代表有表级锁的存在或者即将有表级锁的存在 + +兼容性如下所示: + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-意向锁兼容性.png) + +**插入意向锁** Insert Intention Lock 是在插入一行记录操作之前设置的一种间隙锁,是行级锁 + +插入意向锁释放了一种插入信号,即多个事务在相同的索引间隙插入时如果不是插入相同的间隙位置就不需要互相等待。假设某列有索引,只要两个事务插入位置不同,如事务 A 插入 3,事务 B 插入 4,那么就可以同时插入 + + + +*** + + + +##### 自增锁 + +系统会自动给 AUTO_INCREMENT 修饰的列进行递增赋值,实现方式: + +* AUTO_INC 锁:表级锁,执行插入语句时会自动添加,在该语句执行完成后释放,并不是事务结束 +* 轻量级锁:为插入语句生成 AUTO_INCREMENT 修饰的列时获取该锁,生成以后释放掉,不需要等到插入语句执行完后释放 + +系统变量 `innodb_autoinc_lock_mode` 控制采取哪种方式: + +* 0:全部采用 AUTO_INC 锁 +* 1:全部采用轻量级锁 +* 2:混合使用,在插入记录的数量确定时采用轻量级锁,不确定时采用 AUTO_INC 锁 + + + +**** + + + +##### 隐式锁 + +一般情况下 INSERT 语句是不需要在内存中生成锁结构的,会进行隐式的加锁,保护的是插入后的安全 + +注意:如果插入的间隙被其他事务加了间隙锁,此次插入会被阻塞,并在该间隙插入一个插入意向锁 + +* 聚簇索引:索引记录有 trx_id 隐藏列,表示最后改动该记录的事务 id,插入数据后事务 id 就是当前事务。其他事务想获取该记录的锁时会判断当前记录的事务 id 是否是活跃的,如果不是就可以正常加锁;如果是就创建一个 X 的锁结构,该锁的 is_waiting 是 false,为自己的事务创建一个锁结构,is_waiting 是 true(类似 Java 中的锁升级) +* 二级索引:获取数据页 Page Header 中的 PAGE_MAX_TRX_ID 属性,代表修改当前页面的最大的事务 ID,如果小于当前活跃的最小事务 id,就证明插入该数据的事务已经提交,否则就需要获取到主键值进行回表操作 + +隐式锁起到了延迟生成锁的效果,如果其他事务与隐式锁没有冲突,就可以避免锁结构的生成,节省了内存资源 + +INSERT 在两种情况下会生成锁结构: + +* 重复键:在插入主键或唯一二级索引时遇到重复的键值会报错,在报错前需要对对应的聚簇索引进行加锁 + * 隔离级别 <= Read Uncommitted,加 S 型 Record Lock + * 隔离级别 >= Repeatable Read,加 S 型 next_key 锁 + +* 外键检查:如果待插入的记录在父表中可以找到,会对父表的记录加 S 型 Record Lock。如果待插入的记录在父表中找不到 + * 隔离级别 <= Read Committed,不加锁 + * 隔离级别 >= Repeatable Read,加间隙锁 + + + + + +*** + + + +#### 锁优化 + +##### 优化锁 + +InnoDB 存储引擎实现了行级锁定,虽然在锁定机制的实现方面带来了性能损耗可能比表锁会更高,但是在整体并发处理能力方面要远远优于 MyISAM 的表锁,当系统并发量较高的时候,InnoDB 的整体性能远远好于 MyISAM + +但是使用不当可能会让 InnoDB 的整体性能表现不仅不能比 MyISAM 高,甚至可能会更差 + +优化建议: + +- 尽可能让所有数据检索都能通过索引来完成,避免无索引行锁升级为表锁 +- 合理设计索引,尽量缩小锁的范围 +- 尽可能减少索引条件及索引范围,避免间隙锁 +- 尽量控制事务大小,减少锁定资源量和时间长度 +- 尽可使用低级别事务隔离(需要业务层面满足需求) + + + +**** + + + +##### 锁升级 + +索引失效造成**行锁升级为表锁**,不通过索引检索数据,全局扫描的过程中 InnoDB 会将对表中的所有记录加锁,实际效果和**表锁**一样,实际开发过程应避免出现索引失效的状况 + +* 查看当前表的索引: + + ```mysql + SHOW INDEX FROM test_innodb_lock; + ``` + +* 关闭自动提交功能: + + ```mysql + SET AUTOCOMMIT=0; -- C1、C2 + ``` + +* 执行更新语句: + + ```mysql + UPDATE test_innodb_lock SET sex='2' WHERE name=10; -- C1 + UPDATE test_innodb_lock SET sex='2' WHERE id=3; -- C2 + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-InnoDB 锁升级.png) + + 索引失效:执行更新时 name 字段为 varchar 类型,造成索引失效,最终行锁变为表锁 + + + +*** + + + +##### 死锁 + +不同事务由于互相持有对方需要的锁而导致事务都无法继续执行的情况称为死锁 + +死锁情况:线程 A 修改了 id = 1 的数据,请求修改 id = 2 的数据,线程 B 修改了 id = 2 的数据,请求修改 id = 1 的数据,产生死锁 + +解决策略: + +* 直接进入等待直到超时,超时时间可以通过参数 innodb_lock_wait_timeout 来设置,默认 50 秒,但是时间的设置不好控制,超时可能不是因为死锁,而是因为事务处理比较慢,所以一般不采取该方式 + +* 主动死锁检测,发现死锁后**主动回滚死锁链条中较小的一个事务**,让其他事务得以继续执行,将参数 `innodb_deadlock_detect` 设置为 on,表示开启该功能(事务较小的意思就是事务执行过程中插入、删除、更新的记录条数) + + 死锁检测并不是每个语句都要检测,只有在加锁访问的行上已经有锁时,当前事务被阻塞了才会检测,也是从当前事务开始进行检测 + +通过执行 `SHOW ENGINE INNODB STATUS` 可以查看最近发生的一次死循环,全局系统变量 `innodb_print_all_deadlocks` 设置为 on,就可以将每个死锁信息都记录在 MySQL 错误日志中 + +死锁一般是行级锁,当表锁发生死锁时,会在事务中访问其他表时**直接报错**,破坏了持有并等待的死锁条件 + + + +*** + + + +#### 锁状态 + +查看锁信息 + +```mysql +SHOW STATUS LIKE 'innodb_row_lock%'; +``` + + + +参数说明: + +* Innodb_row_lock_current_waits:当前正在等待锁定的数量 + +* Innodb_row_lock_time:从系统启动到现在锁定总时间长度 + +* Innodb_row_lock_time_avg:每次等待所花平均时长 + +* Innodb_row_lock_time_max:从系统启动到现在等待最长的一次所花的时间 + +* Innodb_row_lock_waits:系统启动后到现在总共等待的次数 + +当等待的次数很高,而且每次等待的时长也不短的时候,就需要分析系统中为什么会有如此多的等待,然后根据分析结果制定优化计划 + +查看锁状态: + +```mysql +SELECT * FROM information_schema.innodb_locks; #锁的概况 +SHOW ENGINE INNODB STATUS\G; #InnoDB整体状态,其中包括锁的情况 +``` + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-InnoDB查看锁状态.png) + +lock_id 是锁 id;lock_trx_id 为事务 id;lock_mode 为 X 代表排它锁(写锁);lock_type 为 RECORD 代表锁为行锁(记录锁) + + + + + +*** + + + +### 乐观锁 + +悲观锁:在整个数据处理过程中,将数据处于锁定状态,为了保证事务的隔离性,就需要一致性锁定读。读取数据时给加锁,其它事务无法修改这些数据,修改删除数据时也加锁,其它事务同样无法读取这些数据 + +悲观锁和乐观锁使用前提: + +- 对于读的操作远多于写的操作的时候,一个更新操作加锁会阻塞所有的读取操作,降低了吞吐量,最后需要释放锁,锁是需要一些开销的,这时候可以选择乐观锁 +- 如果是读写比例差距不是非常大或者系统没有响应不及时,吞吐量瓶颈的问题,那就不要去使用乐观锁,它增加了复杂度,也带来了业务额外的风险,这时候可以选择悲观锁 + +乐观锁的实现方式:就是 CAS,比较并交换 + +* 版本号 + + 1. 给数据表中添加一个 version 列,每次更新后都将这个列的值加 1 + + 2. 读取数据时,将版本号读取出来,在执行更新的时候,比较版本号 + + 3. 如果相同则执行更新,如果不相同,说明此条数据已经发生了变化 + + 4. 用户自行根据这个通知来决定怎么处理,比如重新开始一遍,或者放弃本次更新 + + ```mysql + -- 创建city表 + CREATE TABLE city( + id INT PRIMARY KEY AUTO_INCREMENT, -- 城市id + NAME VARCHAR(20), -- 城市名称 + VERSION INT -- 版本号 + ); + + -- 添加数据 + INSERT INTO city VALUES (NULL,'北京',1),(NULL,'上海',1),(NULL,'广州',1),(NULL,'深圳',1); + + -- 修改北京为北京市 + -- 1.查询北京的version + SELECT VERSION FROM city WHERE NAME='北京'; + -- 2.修改北京为北京市,版本号+1。并对比版本号 + UPDATE city SET NAME='北京市',VERSION=VERSION+1 WHERE NAME='北京' AND VERSION=1; + ``` + +* 时间戳 + + - 和版本号方式基本一样,给数据表中添加一个列,名称无所谓,数据类型需要是 **timestamp** + - 每次更新后都将最新时间插入到此列 + - 读取数据时,将时间读取出来,在执行更新的时候,比较时间 + - 如果相同则执行更新,如果不相同,说明此条数据已经发生了变化 + +乐观锁的异常情况:如果 version 被其他事务抢先更新,则在当前事务中更新失败,trx_id 没有变成当前事务的 ID,当前事务再次查询还是旧值,就会出现**值没变但是更新不了**的现象(anomaly) + +解决方案:每次 CAS 更新不管成功失败,就结束当前事务;如果失败则重新起一个事务进行查询更新 + + + + + +*** + + + + + +## 主从 + +### 基本介绍 + +主从复制是指将主数据库的 DDL 和 DML 操作通过二进制日志传到从库服务器中,然后在从库上对这些日志重新执行(也叫重做),从而使得从库和主库的数据保持同步 + +MySQL 支持一台主库同时向多台从库进行复制,从库同时也可以作为其他从服务器的主库,实现链状复制 + +MySQL 复制的优点主要包含以下三个方面: + +- 主库出现问题,可以快速切换到从库提供服务 + +- 可以在从库上执行查询操作,从主库中更新,实现读写分离 + +- 可以在从库中执行备份,以避免备份期间影响主库的服务(备份时会加全局读锁) + + + +*** + + + +### 主从复制 + +#### 主从结构 + +MySQL 的主从之间维持了一个**长连接**。主库内部有一个线程,专门用于服务从库的长连接,连接过程: + +* 从库执行 change master 命令,设置主库的 IP、端口、用户名、密码以及要从哪个位置开始请求 binlog,这个位置包含文件名和日志偏移量 +* 从库执行 start slave 命令,这时从库会启动两个线程,就是图中的 io_thread 和 sql_thread,其中 io_thread 负责与主库建立连接 +* 主库校验完用户名、密码后,开始按照从传过来的位置,从本地读取 binlog 发给从库,开始主从复制 + +主从复制原理图: + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-主从复制原理图.jpg) + +主从复制主要依赖的是 binlog,MySQL 默认是异步复制,需要三个线程: + +- binlog thread:在主库事务提交时,把数据变更记录在日志文件 binlog 中,并通知 slave 有数据更新 +- I/O thread:负责从主服务器上**拉取二进制日志**,并将 binlog 日志内容依次写到 relay log 中转日志的最末端,并将新的 binlog 文件名和 offset 记录到 master-info 文件中,以便下一次读取日志时从指定 binlog 日志文件及位置开始读取新的 binlog 日志内容 +- SQL thread:监测本地 relay log 中新增了日志内容,读取中继日志并重做其中的 SQL 语句,从库在 relay-log.info 中记录当前应用中继日志的文件名和位点以便下一次执行 + +同步与异步: + +* 异步复制有数据丢失风险,例如数据还未同步到从库,主库就给客户端响应,然后主库挂了,此时从库晋升为主库的话数据是缺失的 +* 同步复制,主库需要将 binlog 复制到所有从库,等所有从库响应了之后主库才进行其他逻辑,这样的话性能很差,一般不会选择 +* MySQL 5.7 之后出现了半同步复制,有参数可以选择成功同步几个从库就返回响应 + + + +**** + + + +#### 主主结构 + +主主结构就是两个数据库之间总是互为主从关系,这样在切换的时候就不用再修改主从关系 + +循环复制:在库 A 上更新了一条语句,然后把生成的 binlog 发给库 B,库 B 执行完这条更新语句后也会生成 binlog,会再发给 A + +解决方法: + +* 两个库的 server id 必须不同,如果相同则它们之间不能设定为主主关系 +* 一个库接到 binlog 并在重放的过程中,生成与原 binlog 的 server id 相同的新的 binlog +* 每个库在收到从主库发过来的日志后,先判断 server id,如果跟自己的相同,表示这个日志是自己生成的,就直接丢弃这个日志 + + + +*** + + + +### 主从延迟 + +#### 延迟原因 + +正常情况主库执行更新生成的所有 binlog,都可以传到从库并被正确地执行,从库就能达到跟主库一致的状态,这就是最终一致性 + +主从延迟是主从之间是存在一定时间的数据不一致,就是同一个事务在从库执行完成的时间和主库执行完成的时间的差值,即 T2-T1 + +- 主库 A 执行完成一个事务,写入 binlog,该时刻记为 T1 +- 日志传给从库 B,从库 B 执行完这个事务,该时刻记为 T2 + +通过在从库执行 `show slave status` 命令,返回结果会显示 seconds_behind_master 表示当前从库延迟了多少秒 + +- 每一个事务的 binlog 都有一个时间字段,用于记录主库上写入的时间 +- 从库取出当前正在执行的事务的时间字段,跟系统的时间进行相减,得到的就是 seconds_behind_master + +主从延迟的原因: + +* 从库的机器性能比主库的差,导致从库的复制能力弱 +* 从库的查询压力大,建立一主多从的结构 +* 大事务的执行,主库必须要等到事务完成之后才会写入 binlog,导致从节点出现应用 binlog 延迟 +* 主库的 DDL,从库与主库的 DDL 同步是串行进行,DDL 在主库执行时间很长,那么从库也会消耗同样的时间 +* 锁冲突问题也可能导致从节点的 SQL 线程执行慢 + +主从同步问题永远都是**一致性和性能的权衡**,需要根据实际的应用场景,可以采取下面的办法: + +* 优化 SQL,避免慢 SQL,减少批量操作 +* 降低多线程大事务并发的概率,优化业务逻辑 +* 业务中大多数情况查询操作要比更新操作更多,搭建**一主多从**结构,让这些从库来分担读的压力 + +* 尽量采用短的链路,主库和从库服务器的距离尽量要短,提升端口带宽,减少 binlog 传输的网络延时 +* 实时性要求高的业务读强制走主库,从库只做备份 + + + +*** + + + +#### 并行复制 + +##### MySQL5.6 + +高并发情况下,主库的会产生大量的 binlog,在从库中有两个线程 IO Thread 和 SQL Thread 单线程执行,会导致主库延迟变大。为了改善复制延迟问题,MySQL 5.6 版本增加了并行复制功能,以采用多线程机制来促进执行 + +coordinator 就是原来的 SQL Thread,并行复制中它不再直接更新数据,**只负责读取中转日志和分发事务**: + +* 线程分配完成并不是立即执行,为了防止造成更新覆盖,更新同一 DB 的两个事务必须被分发到同一个工作线程 +* 同一个事务不能被拆开,必须放到同一个工作线程 + +MySQL 5.6 版本的策略:每个线程对应一个 hash 表,用于保存当前这个线程的执行队列里的事务所涉及的表,hash 表的 key 是数据库名,value 是一个数字,表示队列中有多少个事务修改这个库,适用于主库上有多个 DB 的情况 + +每个事务在分发的时候,跟线程的**冲突**(事务操作的是同一个库)关系包括以下三种情况: + +* 如果跟所有线程都不冲突,coordinator 线程就会把这个事务分配给最空闲的线程 +* 如果只跟一个线程冲突,coordinator 线程就会把这个事务分配给这个存在冲突关系的线程 +* 如果跟多于一个线程冲突,coordinator 线程就进入等待状态,直到和这个事务存在冲突关系的线程只剩下 1 个 + +优缺点: + +* 构造 hash 值的时候很快,只需要库名,而且一个实例上 DB 数也不会很多,不会出现需要构造很多项的情况 +* 不要求 binlog 的格式,statement 格式的 binlog 也可以很容易拿到库名(日志章节详解了 binlog) +* 主库上的表都放在同一个 DB 里面,这个策略就没有效果了;或者不同 DB 的热点不同,比如一个是业务逻辑库,一个是系统配置库,那也起不到并行的效果,需要**把相同热度的表均匀分到这些不同的 DB 中**,才可以使用这个策略 + + + +*** + + + +##### MySQL5.7 + +MySQL 5.7 由参数 slave-parallel-type 来控制并行复制策略: + +* 配置为 DATABASE,表示使用 MySQL 5.6 版本的**按库(DB)并行策略** +* 配置为 LOGICAL_CLOCK,表示的**按提交状态并行**执行 + +按提交状态并行复制策略的思想是: + +* 所有处于 commit 状态的事务可以并行执行;同时处于 prepare 状态的事务,在从库执行时是可以并行的 +* 处于 prepare 状态的事务,与处于 commit 状态的事务之间,在从库执行时也是可以并行的 + +MySQL 5.7.22 版本里,MySQL 增加了一个新的并行复制策略,基于 WRITESET 的并行复制,新增了一个参数 binlog-transaction-dependency-tracking,用来控制是否启用这个新策略: + +* COMMIT_ORDER:表示根据同时进入 prepare 和 commit 来判断是否可以并行的策略 + +* WRITESET:表示的是对于每个事务涉及更新的每一行,计算出这一行的 hash 值,组成该事务的 writeset 集合,如果两个事务没有操作相同的行,也就是说它们的 writeset 没有交集,就可以并行(**按行并行**) + + 为了唯一标识,这个 hash 表的值是通过 `库名 + 表名 + 索引名 + 值`(表示的是某一行)计算出来的 + +* WRITESET_SESSION:是在 WRITESET 的基础上多了一个约束,即在主库上同一个线程先后执行的两个事务,在备库执行的时候,要保证相同的先后顺序 + + +MySQL 5.7.22 按行并发的优势: + +* writeset 是在主库生成后直接写入到 binlog 里面的,这样在备库执行的时候,不需要解析 binlog 内容,节省了计算量 +* 不需要把整个事务的 binlog 都扫一遍才能决定分发到哪个线程,更省内存 +* 从库的分发策略不依赖于 binlog 内容,所以 binlog 是 statement 格式也可以,更节约内存(因为 row 才记录更改的行) + +MySQL 5.7.22 的并行复制策略在通用性上是有保证的,但是对于表上没主键、唯一和外键约束的场景,WRITESET 策略也是没法并行的,也会暂时退化为单线程模型 + + + +参考文章:https://time.geekbang.org/column/article/77083 + + + +*** + + + +### 读写分离 + +#### 读写延迟 + +读写分离:可以降低主库的访问压力,提高系统的并发能力 + +* 主库不建查询的索引,从库建查询的索引。因为索引需要维护的,比如插入一条数据,不仅要在聚簇索引上面插入,对应的二级索引也得插入 +* 将读操作分到从库了之后,可以在主库把查询要用的索引删了,减少写操作对主库的影响 + +读写分离产生了读写延迟,造成数据的不一致性。假如客户端执行完一个更新事务后马上发起查询,如果查询选择的是从库的话,可能读到的还是以前的数据,叫过期读 + +解决方案: + +* 强制将写之后**立刻读的操作转移到主库**,比如刚注册的用户,直接登录从库查询可能查询不到,先走主库登录 +* **二次查询**,如果从库查不到数据,则再去主库查一遍,由 API 封装,比较简单,但导致主库压力大 +* 更新主库后,读从库之前先 sleep 一下,类似于执行一条 `select sleep(1)` 命令,大多数情况下主备延迟在 1 秒之内 + + + +*** + + + +#### 确保机制 + +##### 无延迟 + +确保主备无延迟的方法: + +* 每次从库执行查询请求前,先判断 seconds_behind_master 是否已经等于 0,如果不等于那就等到参数变为 0 执行查询请求 +* 对比位点,Master_Log_File 和 Read_Master_Log_Pos 表示的是读到的主库的最新位点,Relay_Master_Log_File 和 Exec_Master_Log_Pos 表示的是备库执行的最新位点,这两组值完全相同就说明接收到的日志已经同步完成 +* 对比 GTID 集合,Retrieved_Gtid_Set 是备库收到的所有日志的 GTID 集合,Executed_Gtid_Set 是备库所有已经执行完成的 GTID 集合,如果这两个集合相同也表示备库接收到的日志都已经同步完成 + + + +*** + + + +##### 半同步 + +半同步复制就是 semi-sync replication,适用于一主一备的场景,工作流程: + +* 事务提交的时候,主库把 binlog 发给从库 +* 从库收到 binlog 以后,发回给主库一个 ack,表示收到了 +* 主库收到这个 ack 以后,才能给客户端返回事务完成的确认 + +在一主多从场景中,主库只要等到一个从库的 ack,就开始给客户端返回确认,这时在从库上执行查询请求,有两种情况: + +* 如果查询是落在这个响应了 ack 的从库上,是能够确保读到最新数据 +* 如果查询落到其他从库上,它们可能还没有收到最新的日志,就会产生过期读的问题 + +在业务更新的高峰期,主库的位点或者 GTID 集合更新很快,导致从库来不及处理,那么两个位点等值判断就会一直不成立,很可能出现从库上迟迟无法响应查询请求的情况 + + + +**** + + + +##### 等位点 + +在**从库执行判断位点**的命令,参数 file 和 pos 指的是主库上的文件名和位置,timeout 可选,设置为正整数 N 表示最多等待 N 秒 + +```mysql +SELECT master_pos_wait(file, pos[, timeout]); +``` + +命令正常返回的结果是一个正整数 M,表示从命令开始执行,到应用完 file 和 pos 表示的 binlog 位置,执行了多少事务 + +* 如果执行期间,备库同步线程发生异常,则返回 NULL +* 如果等待超过 N 秒,就返回 -1 +* 如果刚开始执行的时候,就发现已经执行过这个位置了,则返回 0 + +工作流程:先执行 trx1,再执行一个查询请求的逻辑,要**保证能够查到正确的数据** + +* trx1 事务更新完成后,马上执行 `show master status` 得到当前主库执行到的 File 和 Position +* 选定一个从库执行判断位点语句,如果返回值是 >=0 的正整数,说明从库已经同步完事务,可以在这个从库执行查询语句 +* 如果出现其他情况,需要到主库执行查询语句 + +注意:如果所有的从库都延迟超过 timeout 秒,查询压力就都跑到主库上,所以需要进行权衡 + + + +*** + + + +##### 等GTID + +数据库开启了 GTID 模式,MySQL 提供了判断 GTID 的命令 + +```mysql +SELECT wait_for_executed_gtid_set(gtid_set [, timeout]) +``` + +* 等待直到这个库执行的事务中包含传入的 gtid_set,返回 0 +* 超时返回 1 + +工作流程:先执行 trx1,再执行一个查询请求的逻辑,要保证能够查到正确的数据 + +* trx1 事务更新完成后,从返回包直接获取这个事务的 GTID,记为 gtid +* 选定一个从库执行查询语句,如果返回值是 0,则在这个从库执行查询语句,否则到主库执行查询语句 + +对比等待位点方法,减少了一次 `show master status` 的方法,将参数 session_track_gtids 设置为 OWN_GTID,然后通过 API 接口 mysql_session_track_get_first 从返回包解析出 GTID 的值即可 + +总结:所有的等待无延迟的方法,都需要根据具体的业务场景去判断实施 + + + +参考文章:https://time.geekbang.org/column/article/77636 + + + +*** + + + +#### 负载均衡 + +负载均衡是应用中使用非常普遍的一种优化方法,机制就是利用某种均衡算法,将固定的负载量分布到不同的服务器上,以此来降低单台服务器的负载,达到优化的效果 + +* 分流查询:通过 MySQL 的主从复制,实现读写分离,使增删改操作走主节点,查询操作走从节点,从而可以降低单台服务器的读写压力 + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-负载均衡主从复制.jpg) + +* 分布式数据库架构:适合大数据量、负载高的情况,具有良好的拓展性和高可用性。通过在多台服务器之间分布数据,可以实现在多台服务器之间的负载均衡,提高访问效率 + + + +**** + + + +### 主从搭建 + +#### master + +1. 在master 的配置文件(/etc/mysql/my.cnf)中,配置如下内容: + + ```sh + #mysql 服务ID,保证整个集群环境中唯一 + server-id=1 + + #mysql binlog 日志的存储路径和文件名 + log-bin=/var/lib/mysql/mysqlbin + + #错误日志,默认已经开启 + #log-err + + #mysql的安装目录 + #basedir + + #mysql的临时目录 + #tmpdir + + #mysql的数据存放目录 + #datadir + + #是否只读,1 代表只读, 0 代表读写 + read-only=0 + + #忽略的数据, 指不需要同步的数据库 + binlog-ignore-db=mysql + + #指定同步的数据库 + #binlog-do-db=db01 + ``` + +2. 执行完毕之后,需要重启 MySQL + +3. 创建同步数据的账户,并且进行授权操作: + + ```mysql + GRANT REPLICATION SLAVE ON *.* TO 'seazean'@'192.168.0.137' IDENTIFIED BY '123456'; + FLUSH PRIVILEGES; + ``` + +4. 查看 master 状态: + + ```mysql + SHOW MASTER STATUS; + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-查看master状态.jpg) + + * File:从哪个日志文件开始推送日志文件 + * Position:从哪个位置开始推送日志 + * Binlog_Ignore_DB:指定不需要同步的数据库 + + + +*** + + + +#### slave + +1. 在 slave 端配置文件中,配置如下内容: + + ```sh + #mysql服务端ID,唯一 + server-id=2 + + #指定binlog日志 + log-bin=/var/lib/mysql/mysqlbin + ``` + +2. 执行完毕之后,需要重启 MySQL + +3. 指定当前从库对应的主库的IP地址、用户名、密码,从哪个日志文件开始的那个位置开始同步推送日志 + + ```mysql + CHANGE MASTER TO MASTER_HOST= '192.168.0.138', MASTER_USER='seazean', MASTER_PASSWORD='seazean', MASTER_LOG_FILE='mysqlbin.000001', MASTER_LOG_POS=413; + ``` + +4. 开启同步操作: + + ```mysql + START SLAVE; + SHOW SLAVE STATUS; + ``` + +5. 停止同步操作: + + ```mysql + STOP SLAVE; + ``` + + + +*** + + + +#### 验证 + +1. 在主库中创建数据库,创建表并插入数据: + + ```mysql + CREATE DATABASE db01; + USE db01; + CREATE TABLE user( + id INT(11) NOT NULL AUTO_INCREMENT, + name VARCHAR(50) NOT NULL, + sex VARCHAR(1), + PRIMARY KEY (id) + )ENGINE=INNODB DEFAULT CHARSET=utf8; + + INSERT INTO user(id,NAME,sex) VALUES(NULL,'Tom','1'); + INSERT INTO user(id,NAME,sex) VALUES(NULL,'Trigger','0'); + INSERT INTO user(id,NAME,sex) VALUES(NULL,'Dawn','1'); + ``` + +2. 在从库中查询数据,进行验证: + + 在从库中,可以查看到刚才创建的数据库: + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-主从复制验证1.jpg) + + 在该数据库中,查询表中的数据: + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-主从复制验证2.jpg) + + + +*** + + + +### 主从切换 + +#### 正常切换 + +正常切换步骤: + +* 在开始切换之前先对主库进行锁表 `flush tables with read lock`,然后等待所有语句执行完成,切换完成后可以释放锁 + +* 检查 slave 同步状态,在 slave 执行 `show processlist` + +* 停止 slave io 线程,执行命令 `STOP SLAVE IO_THREAD` + +* 提升 slave 为 master + + ```sql + Stop slave; + Reset master; + Reset slave all; + set global read_only=off; -- 设置为可更新状态 + ``` + +* 将原来 master 变为 slave(参考搭建流程中的 slave 方法) + +**可靠性优先策略**: + +* 判断备库 B 现在的 seconds_behind_master,如果小于某个值(比如 5 秒)继续下一步,否则持续重试这一步 +* 把主库 A 改成只读状态,即把 readonly 设置为 true +* 判断备库 B 的 seconds_behind_master 的值,直到这个值变成 0 为止(该步骤比较耗时,所以步骤 1 中要尽量等待该值变小) +* 把备库 B 改成可读写状态,也就是把 readonly 设置为 false +* 把业务请求切到备库 B + +可用性优先策略:先做最后两步,会造成主备数据不一致的问题 + + + +参考文章:https://time.geekbang.org/column/article/76795 + + + +*** + + + +#### 健康检测 + +主库发生故障后从库会上位,**其他从库指向新的主库**,所以需要一个健康检测的机制来判断主库是否宕机 + +* select 1 判断,但是高并发下检测不出线程的锁等待的阻塞问题 + +* 查表判断,在系统库(mysql 库)里创建一个表,比如命名为 health_check,里面只放一行数据,然后定期执行。但是当 binlog 所在磁盘的空间占用率达到 100%,所有的更新和事务提交语句都被阻塞,查询语句可以继续运行 + +* 更新判断,在健康检测表中放一个 timestamp 字段,用来表示最后一次执行检测的时间 + + ```mysql + UPDATE mysql.health_check SET t_modified=now(); + ``` + + 节点可用性的检测都应该包含主库和备库,为了让主备之间的更新不产生冲突,可以在 mysql.health_check 表上存入多行数据,并用主备的 server_id 做主键,保证主、备库各自的检测命令不会发生冲突 + + + +*** + + + + + +#### 基于位点 + +主库上位后,从库 B 执行 CHANGE MASTER TO 命令,指定 MASTER_LOG_FILE、MASTER_LOG_POS 表示从新主库 A 的哪个文件的哪个位点开始同步,这个位置就是**同步位点**,对应主库的文件名和日志偏移量 + +寻找位点需要找一个稍微往前的,然后再通过判断跳过那些在从库 B 上已经执行过的事务,获取位点方法: + +* 等待新主库 A 把中转日志(relay log)全部同步完成 +* 在 A 上执行 show master status 命令,得到当前 A 上最新的 File 和 Position +* 取原主库故障的时刻 T,用 mysqlbinlog 工具解析新主库 A 的 File,得到 T 时刻的位点 + +通常情况下该值并不准确,在切换的过程中会发生错误,所以要先主动跳过这些错误: + +* 切换过程中,可能会重复执行一个事务,所以需要主动跳过所有重复的事务 + + ```mysql + SET GLOBAL sql_slave_skip_counter=1; + START SLAVE; + ``` + +* 设置 slave_skip_errors 参数,直接设置跳过指定的错误,保证主从切换的正常进行 + + * 1062 错误是插入数据时唯一键冲突 + * 1032 错误是删除数据时找不到行 + + 该方法针对的是主备切换时,由于找不到精确的同步位点,只能采用这种方法来创建从库和新主库的主备关系。等到主备间的同步关系建立完成并稳定执行一段时间后,还需要把这个参数设置为空,以免真的出现了主从数据不一致也跳过了 + + + +**** + + + +#### 基于GTID + +##### GTID + +GTID 的全称是 Global Transaction Identifier,全局事务 ID,是一个事务**在提交时生成**的,是这个事务的唯一标识,组成: + +```mysql +GTID=source_id:transaction_id +``` + +* source_id:是一个实例第一次启动时自动生成的,是一个全局唯一的值 +* transaction_id:初始值是 1,每次提交事务的时候分配给这个事务,并加 1,是连续的(区分事务 ID,事务 ID 是在执行时生成) + +启动 MySQL 实例时,加上参数 `gtid_mode=on` 和 `enforce_gtid_consistency=on` 就可以启动 GTID 模式,每个事务都会和一个 GTID 一一对应,每个 MySQL 实例都维护了一个 GTID 集合,用来存储当前实例**执行过的所有事务** + +GTID 有两种生成方式,使用哪种方式取决于 session 变量 gtid_next: + +* `gtid_next=automatic`:使用默认值,把 source_id:transaction_id (递增)分配给这个事务,然后加入本实例的 GTID 集合 + + ```mysql + @@SESSION.GTID_NEXT = 'source_id:transaction_id'; + ``` + +* `gtid_next=GTID`:指定的 GTID 的值,如果该值已经存在于实例的 GTID 集合中,接下来执行的事务会直接被系统忽略;反之就将该值分配给接下来要执行的事务,系统不需要给这个事务生成新的 GTID,也不用加 1 + + 注意:一个 GTID 只能给一个事务使用,所以执行下一个事务,要把 gtid_next 设置成另外一个 GTID 或者 automatic + +业务场景: + +* 主库 X 和从库 Y 执行一条相同的指令后进行事务同步 + + ```mysql + INSERT INTO t VALUES(1,1); + ``` + +* 当 Y 同步 X 时,会出现主键冲突,导致实例 X 的同步线程停止,解决方法: + + ```mysql + SET gtid_next='(这里是主库 X 的 GTID 值)'; + BEGIN; + COMMIT; + SET gtid_next=automatic; + START SLAVE; + ``` + + 前三条语句通过**提交一个空事务**,把 X 的 GTID 加到实例 Y 的 GTID 集合中,实例 Y 就会直接跳过这个事务 + + + +**** + + + +##### 切换 + +在 GTID 模式下,CHANGE MASTER TO 不需要指定日志名和日志偏移量,指定 `master_auto_position=1` 代表使用 GTID 模式 + +新主库实例 A 的 GTID 集合记为 set_a,从库实例 B 的 GTID 集合记为 set_b,主备切换逻辑: + +* 实例 B 指定主库 A,基于主备协议建立连接,实例 B 并把 set_b 发给主库 A +* 实例 A 算出 set_a 与 set_b 的差集,就是所有存在于 set_a 但不存在于 set_b 的 GTID 的集合,判断 A 本地是否包含了这个**差集**需要的所有 binlog 事务 + * 如果不包含,表示 A 已经把实例 B 需要的 binlog 给删掉了,直接返回错误 + * 如果确认全部包含,A 从自己的 binlog 文件里面,找出第一个不在 set_b 的事务,发给 B +* 实例 A 之后就从这个事务开始,往后读文件,按顺序取 binlog 发给 B 去执行 + + + +参考文章:https://time.geekbang.org/column/article/77427 + + + +*** + + + + + +## 日志 + +### 日志分类 + +在任何一种数据库中,都会有各种各样的日志,记录着数据库工作的过程,可以帮助数据库管理员追踪数据库曾经发生过的各种事件 + +MySQL日志主要包括六种: + +1. 重做日志(redo log) +2. 回滚日志(undo log) +3. 归档日志(binlog)(二进制日志) +4. 错误日志(errorlog) +5. 慢查询日志(slow query log) +6. 一般查询日志(general log) +7. 中继日志(relay log) + + + +*** + + + +### 错误日志 + +错误日志是 MySQL 中最重要的日志之一,记录了当 mysqld 启动和停止时,以及服务器在运行过程中发生任何严重错误时的相关信息。当数据库出现任何故障导致无法正常使用时,可以首先查看此日志 + +该日志是默认开启的,默认位置是:`/var/log/mysql/error.log` + +查看指令: + +```mysql +SHOW VARIABLES LIKE 'log_error%'; +``` + +查看日志内容: + +```sh +tail -f /var/log/mysql/error.log +``` + + + +*** + + + +### 归档日志 + +#### 基本介绍 + +归档日志(BINLOG)也叫二进制日志,是因为采用二进制进行存储,记录了所有的 DDL(数据定义语言)语句和 DML(数据操作语言)语句,但**不包括数据查询语句,在事务提交前的最后阶段写入** + +作用:**灾难时的数据恢复和 MySQL 的主从复制** + +归档日志默认情况下是没有开启的,需要在 MySQL 配置文件中开启,并配置 MySQL 日志的格式: + +```sh +cd /etc/mysql +vim my.cnf + +# 配置开启binlog日志, 日志的文件前缀为 mysqlbin -----> 生成的文件名如: mysqlbin.000001 +log_bin=mysqlbin +# 配置二进制日志的格式 +binlog_format=STATEMENT +``` + +日志存放位置:配置时给定了文件名但是没有指定路径,日志默认写入MySQL 的数据目录 + +日志格式: + +* STATEMENT:该日志格式在日志文件中记录的都是 **SQL 语句**,每一条对数据进行修改的 SQL 都会记录在日志文件中,通过 mysqlbinlog 工具,可以查看到每条语句的文本。主从复制时,从库会将日志解析为原语句,并在从库重新执行一遍 + + 缺点:可能会导致主备不一致,因为记录的 SQL 在不同的环境中可能选择的索引不同,导致结果不同 +* ROW:该日志格式在日志文件中记录的是每一行的**数据变更**,而不是记录 SQL 语句。比如执行 SQL 语句 `update tb_book set status='1'`,如果是 STATEMENT,在日志中会记录一行 SQL 语句; 如果是 ROW,由于是对全表进行更新,就是每一行记录都会发生变更,ROW 格式的日志中会记录每一行的数据变更 + + 缺点:记录的数据比较多,占用很多的存储空间 + +* MIXED:这是 MySQL 默认的日志格式,混合了STATEMENT 和 ROW 两种格式,MIXED 格式能尽量利用两种模式的优点,而避开它们的缺点 + + + +*** + + + +#### 日志刷盘 + +事务执行过程中,先将日志写(write)到 binlog cache,事务提交时再把 binlog cache 写(fsync)到 binlog 文件中,一个事务的 binlog 是不能被拆开的,所以不论这个事务多大也要确保一次性写入 + +事务提交时执行器把 binlog cache 里的完整事务写入到 binlog 中,并清空 binlog cache + +write 和 fsync 的时机由参数 sync_binlog 控制的: + +* sync_binlog=0:表示每次提交事务都只 write,不 fsync +* sync_binlog=1:表示每次提交事务都会执行 fsync +* sync_binlog=N(N>1):表示每次提交事务都 write,但累积 N 个事务后才 fsync,但是如果主机发生异常重启,会丢失最近 N 个事务的 binlog 日志 - * @变量名 : **用户会话变量**,代表整个会话过程他都是有作用的,类似于全局变量 - * @@变量名 : **系统变量** - *** -##### CASE +#### 日志读取 -* 标准语法 1 +日志文件存储位置:/var/lib/mysql - ```mysql - CASE 表达式 - WHEN 值1 THEN 执行sql语句1; - [WHEN 值2 THEN 执行sql语句2;] - ... - [ELSE 执行sql语句n;] - END CASE; - ``` +由于日志以二进制方式存储,不能直接读取,需要用 mysqlbinlog 工具来查看,语法如下: -* 标准语法 2 +```sh +mysqlbinlog log-file; +``` + +查看 STATEMENT 格式日志: + +* 执行插入语句: ```mysql - sCASE - WHEN 判断条件1 THEN 执行sql语句1; - [WHEN 判断条件2 THEN 执行sql语句2;] - ... - [ELSE 执行sql语句n;] - END CASE; + INSERT INTO tb_book VALUES(NULL,'Lucene','2088-05-01','0'); ``` -* 演示 +* `cd /var/lib/mysql`: - ```mysql - DELIMITER $ - CREATE PROCEDURE pro_test7(IN total INT) - BEGIN - -- 定义变量 - DECLARE description VARCHAR(10); - -- 使用case判断 - CASE - WHEN total >= 380 THEN - SET description = '学习优秀'; - WHEN total >= 320 AND total < 380 THEN - SET description = '学习不错'; - ELSE - SET description = '学习一般'; - END CASE; - - -- 查询分数描述信息 - SELECT description; - END$ - DELIMITER ; - -- 调用pro_test7存储过程 - CALL pro_test7(390); - CALL pro_test7((SELECT SUM(score) FROM student)); + ```sh + -rw-r----- 1 mysql mysql 177 5月 23 21:08 mysqlbin.000001 + -rw-r----- 1 mysql mysql 18 5月 23 21:04 mysqlbin.index ``` + mysqlbin.index:该文件是日志索引文件 , 记录日志的文件名; + + mysqlbing.000001:日志文件 +* 查看日志内容: -*** + ```sh + mysqlbinlog mysqlbing.000001; + ``` + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-日志读取1.png) + + 日志结尾有 COMMIT +查看 ROW 格式日志: -##### WHILE +* 修改配置: -* while 循环语法 + ```sh + # 配置二进制日志的格式 + binlog_format=ROW + ``` + +* 插入数据: ```mysql - WHILE 条件判断语句 DO - 循环体语句; - 条件控制语句; - END WHILE; + INSERT INTO tb_book VALUES(NULL,'SpringCloud实战','2088-05-05','0'); ``` - -* 计算 1~100 之间的偶数和 + +* 查看日志内容:日志格式 ROW,直接查看数据是乱码,可以在 mysqlbinlog 后面加上参数 -vv ```mysql - DELIMITER $ - CREATE PROCEDURE pro_test6() - BEGIN - -- 定义求和变量 - DECLARE result INT DEFAULT 0; - -- 定义初始化变量 - DECLARE num INT DEFAULT 1; - -- while循环 - WHILE num <= 100 DO - IF num % 2 = 0 THEN - SET result = result + num; - END IF; - SET num = num + 1; - END WHILE; - -- 查询求和结果 - SELECT result; - END$ - DELIMITER ; - - -- 调用pro_test6存储过程 - CALL pro_test6(); + mysqlbinlog -vv mysqlbin.000002 ``` + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-日志读取2.png) + + + *** -##### REPEAT +#### 日志删除 -* repeat 循环标准语法 +对于比较繁忙的系统,生成日志量大,这些日志如果长时间不清除,将会占用大量的磁盘空间,需要删除日志 + +* Reset Master 指令删除全部 binlog 日志,删除之后,日志编号将从 xxxx.000001重新开始 ```mysql - 初始化语句; - REPEAT - 循环体语句; - 条件控制语句; - UNTIL 条件判断语句 - END REPEAT; + Reset Master -- MySQL指令 ``` -* 计算 1~10 之间的和 +* 执行指令 `PURGE MASTER LOGS TO 'mysqlbin.***`,该命令将删除 ` ***` 编号之前的所有日志 - ```mysql - DELIMITER $ - CREATE PROCEDURE pro_test9() - BEGIN - -- 定义求和变量 - DECLARE result INT DEFAULT 0; - -- 定义初始化变量 - DECLARE num INT DEFAULT 1; - -- repeat循环 - REPEAT - -- 累加 - SET result = result + num; - -- 让num+1 - SET num = num + 1; - -- 停止循环 - UNTIL num > 10 - END REPEAT; - -- 查询求和结果 - SELECT result; - END$ - - DELIMITER ; - -- 调用pro_test9存储过程 - CALL pro_test9(); +* 执行指令 `PURGE MASTER LOGS BEFORE 'yyyy-mm-dd hh:mm:ss'` ,该命令将删除日志为 `yyyy-mm-dd hh:mm:ss` 之前产生的日志 + +* 设置参数 `--expire_logs_days=#`,此参数的含义是设置日志的过期天数,过了指定的天数后日志将会被自动删除,这样做有利于减少管理日志的工作量,配置 my.cnf 文件: + + ```sh + log_bin=mysqlbin + binlog_format=ROW + --expire_logs_days=3 ``` + +**** + + + +#### 数据恢复 + +误删库或者表时,需要根据 binlog 进行数据恢复 + +一般情况下数据库有定时的全量备份,假如每天 0 点定时备份,12 点误删了库,恢复流程: + +* 取最近一次全量备份,用备份恢复出一个临时库 +* 从日志文件中取出凌晨 0 点之后的日志 +* 把除了误删除数据的语句外日志,全部应用到临时库 + +跳过误删除语句日志的方法: + +* 如果原实例没有使用 GTID 模式,只能在应用到包含 12 点的 binlog 文件的时候,先用 –stop-position 参数执行到误操作之前的日志,然后再用 –start-position 从误操作之后的日志继续执行 +* 如果实例使用了 GTID 模式,假设误操作命令的 GTID 是 gtid1,那么只需要提交一个空事务先将这个 GTID 加到临时实例的 GTID 集合,之后按顺序执行 binlog 的时就会自动跳过误操作的语句 + + + *** -##### LOOP +### 查询日志 -LOOP 实现简单的循环,退出循环的条件需要使用其他的语句定义,通常可以使用 LEAVE 语句实现,如果不加退出循环的语句,那么就变成了死循环 +查询日志中记录了客户端的所有操作语句,而二进制日志不包含查询数据的 SQL 语句 -* loop 循环标准语法 +默认情况下,查询日志是未开启的。如果需要开启查询日志,配置 my.cnf: - ```mysql - [循环名称:] LOOP - 条件判断语句 - [LEAVE 循环名称;] - 循环体语句; - 条件控制语句; - END LOOP 循环名称; - ``` - -* 计算 1~10 之间的和 +```sh +# 该选项用来开启查询日志,可选值0或者1,0代表关闭,1代表开启 +general_log=1 +# 设置日志的文件名,如果没有指定,默认的文件名为host_name.log,存放在/var/lib/mysql +general_log_file=mysql_query.log +``` - ```mysql - DELIMITER $ - CREATE PROCEDURE pro_test10() - BEGIN - -- 定义求和变量 - DECLARE result INT DEFAULT 0; - -- 定义初始化变量 - DECLARE num INT DEFAULT 1; - -- loop循环 - l:LOOP - -- 条件成立,停止循环 - IF num > 10 THEN - LEAVE l; - END IF; - -- 累加 - SET result = result + num; - -- 让num+1 - SET num = num + 1; - END LOOP l; - -- 查询求和结果 - SELECT result; - END$ - DELIMITER ; - -- 调用pro_test10存储过程 - CALL pro_test10(); +配置完毕之后,在数据库执行以下操作: + +```mysql +SELECT * FROM tb_book; +SELECT * FROM tb_book WHERE id = 1; +UPDATE tb_book SET name = 'lucene入门指南' WHERE id = 5; +SELECT * FROM tb_book WHERE id < 8 +``` + +执行完毕之后, 再次来查询日志文件: + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-查询日志.png) + + + +*** + + + +### 慢日志 + +慢查询日志记录所有执行时间超过 long_query_time 并且扫描记录数不小于 min_examined_row_limit 的所有的 SQL 语句的日志long_query_time 默认为 10 秒,最小为 0, 精度到微秒 + +慢查询日志默认是关闭的,可以通过两个参数来控制慢查询日志,配置文件 `/etc/mysql/my.cnf`: + +```sh +# 该参数用来控制慢查询日志是否开启,可选值0或者1,0代表关闭,1代表开启 +slow_query_log=1 + +# 该参数用来指定慢查询日志的文件名,存放在 /var/lib/mysql +slow_query_log_file=slow_query.log + +# 该选项用来配置查询的时间限制,超过这个时间将认为值慢查询,将需要进行日志记录,默认10s +long_query_time=10 +``` + +日志读取: + +* 直接通过 cat 指令查询该日志文件: + + ```sh + cat slow_query.log ``` + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-慢日志读取1.png) +* 如果慢查询日志内容很多,直接查看文件比较繁琐,可以借助 mysql 自带的 mysqldumpslow 工具对慢查询日志进行分类汇总: -*** + ```sh + mysqldumpslow slow_query.log + ``` + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-慢日志读取2.png) -##### 游标 -游标是用来存储查询结果集的数据类型,在存储过程和函数中可以使用光标对结果集进行循环的处理 -* 游标可以遍历返回的多行结果,每次拿到一整行数据 -* 简单来说游标就类似于集合的迭代器遍历 -* MySQL 中的游标只能用在存储过程和函数中 -游标的语法 -* 创建游标 +*** - ```mysql - DECLARE 游标名称 CURSOR FOR 查询sql语句; - ``` -* 打开游标 - ```mysql - OPEN 游标名称; - ``` +## 范式 -* 使用游标获取数据 +### 第一范式 - ```mysql - FETCH 游标名称 INTO 变量名1,变量名2,...; - ``` +建立科学的,**规范的数据表**就需要满足一些规则来优化数据的设计和存储,这些规则就称为范式 -* 关闭游标 +**1NF:**数据库表的每一列都是不可分割的原子数据项,不能是集合、数组等非原子数据项。即表中的某个列有多个值时,必须拆分为不同的列。简而言之,**第一范式每一列不可再拆分,称为原子性** - ```mysql - CLOSE 游标名称; - ``` +基本表: -* Mysql 通过一个 Error handler 声明来判断指针是否到尾部,并且必须和创建游标的 SQL 语句声明在一起: +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/普通表.png) + - ```mysql - DECLARE EXIT HANDLER FOR NOT FOUND (do some action,一般是设置标志变量) - ``` +第一范式表: - +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/第一范式.png) -游标的基本使用 -* 数据准备:表 student - ```mysql - id NAME age gender score - 1 张三 23 男 95 - 2 李四 24 男 98 - 3 王五 25 女 100 - 4 赵六 26 女 90 - ``` -* 创建 stu_score 表 - ```mysql - CREATE TABLE stu_score( - id INT PRIMARY KEY AUTO_INCREMENT, - score INT - ); - ``` +**** -* 将student表中所有的成绩保存到stu_score表中 - ```mysql - DELIMITER $ - - CREATE PROCEDURE pro_test12() - BEGIN - -- 定义成绩变量 - DECLARE s_score INT; - -- 定义标记变量 - DECLARE flag INT DEFAULT 0; - - -- 创建游标,查询所有学生成绩数据 - DECLARE stu_result CURSOR FOR SELECT score FROM student; - -- 游标结束后,将标记变量改为1 这两个必须声明在一起 - DECLARE EXIT HANDLER FOR NOT FOUND SET flag = 1; - - -- 开启游标 - OPEN stu_result; - -- 循环使用游标 - REPEAT - -- 使用游标,遍历结果,拿到数据 - FETCH stu_result INTO s_score; - -- 将数据保存到stu_score表中 - INSERT INTO stu_score VALUES (NULL,s_score); - UNTIL flag=1 - END REPEAT; - -- 关闭游标 - CLOSE stu_result; - END$ - - DELIMITER ; - - -- 调用pro_test12存储过程 - CALL pro_test12(); - -- 查询stu_score表 - SELECT * FROM stu_score; - ``` - - +### 第二范式 +**2NF:**在满足第一范式的基础上,非主属性完全依赖于主码(主关键字、主键),消除非主属性对主码的部分函数依赖。简而言之,**表中的每一个字段 (所有列)都完全依赖于主键,记录的唯一性** -*** +作用:遵守第二范式减少数据冗余,通过主键区分相同数据。 +1. 函数依赖:A → B,如果通过 A 属性(属性组)的值,可以确定唯一 B 属性的值,则称 B 依赖于 A + * 学号 → 姓名;(学号,课程名称) → 分数 +2. 完全函数依赖:A → B,如果A是一个属性组,则 B 属性值的确定需要依赖于 A 属性组的所有属性值 + * (学号,课程名称) → 分数 +3. 部分函数依赖:A → B,如果 A 是一个属性组,则 B 属性值的确定只需要依赖于 A 属性组的某些属性值 + * (学号,课程名称) → 姓名 +4. 传递函数依赖:A → B,B → C,如果通过A属性(属性组)的值,可以确定唯一 B 属性的值,在通过 B 属性(属性组)的值,可以确定唯一 C 属性的值,则称 C 传递函数依赖于 A + * 学号 → 系名,系名 → 系主任 +5. 码:如果在一张表中,一个属性或属性组,被其他所有属性所完全依赖,则称这个属性(属性组)为该表的码 + * 该表中的码:(学号,课程名称) + * 主属性:码属性组中的所有属性 + * 非主属性:除码属性组以外的属性 +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/第二范式.png) -#### 存储函数 -存储函数和存储过程是非常相似的,存储函数可以做的事情,存储过程也可以做到 -存储函数有返回值,存储过程没有返回值(参数的 out 其实也相当于是返回数据了) -* 创建存储函数 - ```mysql - DELIMITER $ - -- 标准语法 - CREATE FUNCTION 函数名称(参数 数据类型) - RETURNS 返回值类型 - BEGIN - 执行的sql语句; - RETURN 结果; - END$ - - DELIMITER ; - ``` +**** -* 调用存储函数,因为有返回值,所以使用 SELECT 调用 - ```mysql - SELECT 函数名称(实际参数); - ``` -* 删除存储函数 +### 第三范式 - ```mysql - DROP FUNCTION 函数名称; - ``` +**3NF:**在满足第二范式的基础上,表中的任何属性不依赖于其它非主属性,消除传递依赖。简而言之,**非主键都直接依赖于主键,而不是通过其它的键来间接依赖于主键**。 + +作用:可以通过主键 id 区分相同数据,修改数据的时候只需要修改一张表(方便修改),反之需要修改多表。 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/第三范式.png) -* 定义存储函数,获取学生表中成绩大于95分的学生数量 - ```mysql - DELIMITER $ - CREATE FUNCTION fun_test() - RETURN INT - BEGIN - -- 定义统计变量 - DECLARE result INT; - -- 查询成绩大于95分的学生数量,给统计变量赋值 - SELECT COUNT(score) INTO result FROM student WHERE score > 95; - -- 返回统计结果 - SELECT result; - END - DELIMITER ; - -- 调用fun_test存储函数 - SELECT fun_test(); - ``` @@ -3756,22 +8709,14 @@ LOOP 实现简单的循环,退出循环的条件需要使用其他的语句定 -### 触发器 +### 总结 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/三大范式.png) -#### 基本介绍 -触发器是与表有关的数据库对象,在 insert/update/delete 之前或之后触发并执行触发器中定义的 SQL 语句 -* 触发器的这种特性可以协助应用在数据库端确保数据的完整性 、日志记录 、数据校验等操作 -- 使用别名 NEW 和 OLD 来引用触发器中发生变化的记录内容,这与其他的数据库是相似的 -- 现在触发器还只支持行级触发,不支持语句级触发 -| 触发器类型 | OLD的含义 | NEW的含义 | -| --------------- | ------------------------------ | ------------------------------ | -| INSERT 型触发器 | 无 (因为插入前状态无数据) | NEW 表示将要或者已经新增的数据 | -| UPDATE 型触发器 | OLD 表示修改之前的数据 | NEW 表示将要或已经修改后的数据 | -| DELETE 型触发器 | OLD 表示将要或者已经删除的数据 | 无 (因为删除后状态无数据) | @@ -3779,167 +8724,106 @@ LOOP 实现简单的循环,退出循环的条件需要使用其他的语句定 -#### 基本操作 -* 创建触发器 - ```mysql - DELIMITER $ - - CREATE TRIGGER 触发器名称 - BEFORE|AFTER INSERT|UPDATE|DELETE - ON 表名 - [FOR EACH ROW] -- 行级触发器 - BEGIN - 触发器要执行的功能; - END$ - - DELIMITER ; - ``` +# Redis -* 查看触发器的状态、语法等信息 +## NoSQL - ```mysql - SHOW TRIGGERS; - ``` +### 概述 -* 删除触发器,如果没有指定 schema_name,默认为当前数据库 +NoSQL(Not-Only SQL):泛指非关系型的数据库,作为关系型数据库的补充 - ```mysql - DROP TRIGGER [schema_name.]trigger_name; - ``` +MySQL 支持 ACID 特性,保证可靠性和持久性,读取性能不高,因此需要缓存的来减缓数据库的访问压力 - +作用:应对基于海量用户和海量数据前提下的数据处理问题 -*** +特征: +* 可扩容,可伸缩,SQL 数据关系过于复杂,Nosql 不存关系,只存数据 +* 大数据量下高性能,数据不存取在磁盘 IO,存取在内存 +* 灵活的数据模型,设计了一些数据存储格式,能保证效率上的提高 +* 高可用,集群 +常见的 NoSQL:Redis、memcache、HBase、MongoDB -#### 触发演示 -通过触发器记录账户表的数据变更日志。包含:增加、修改、删除 -* 数据准备 +参考书籍:https://book.douban.com/subject/25900156/ - ```mysql - -- 创建db9数据库 - CREATE DATABASE db9; - -- 使用db9数据库 - USE db9; - ``` +参考视频:https://www.bilibili.com/video/BV1CJ411m7Gc - ```mysql - -- 创建账户表account - CREATE TABLE account( - id INT PRIMARY KEY AUTO_INCREMENT, -- 账户id - NAME VARCHAR(20), -- 姓名 - money DOUBLE -- 余额 - ); - -- 添加数据 - INSERT INTO account VALUES (NULL,'张三',1000),(NULL,'李四',2000); - ``` - ```mysql - -- 创建日志表account_log - CREATE TABLE account_log( - id INT PRIMARY KEY AUTO_INCREMENT, -- 日志id - operation VARCHAR(20), -- 操作类型 (insert update delete) - operation_time DATETIME, -- 操作时间 - operation_id INT, -- 操作表的id - operation_params VARCHAR(200) -- 操作参数 - ); - ``` -* 创建 INSERT 型触发器 +*** + + + +### Redis + +Redis (REmote DIctionary Server) :用 C 语言开发的一个开源的高性能键值对(key-value)数据库 + +特征: + +* 数据间没有必然的关联关系,**不存关系,只存数据** +* 数据**存储在内存**,存取速度快,解决了磁盘 IO 速度慢的问题 +* 内部采用**单线程**机制进行工作 +* 高性能,官方测试数据,50 个并发执行 100000 个请求,读的速度是 110000 次/s,写的速度是 81000 次/s +* 多数据类型支持 + * 字符串类型:string(String) + * 列表类型:list(LinkedList) + * 散列类型:hash(HashMap) + * 集合类型:set(HashSet) + * 有序集合类型:zset/sorted_set(TreeSet) +* 支持持久化,可以进行数据灾难恢复 + - ```mysql - DELIMITER $ - - CREATE TRIGGER account_insert - AFTER INSERT - ON account - FOR EACH ROW - BEGIN - INSERT INTO account_log VALUES (NULL,'INSERT',NOW(),new.id,CONCAT('插入后{id=',new.id,',name=',new.name,',money=',new.money,'}')); - END$ - - DELIMITER ; - ``` - ```mysql - -- 向account表添加记录 - INSERT INTO account VALUES (NULL,'王五',3000); - - -- 查询日志表 - SELECT * FROM account_log; - /* - id operation operation_time operation_id operation_params - 1 INSERT 2021-01-26 19:51:11 3 插入后{id=3,name=王五money=2000} - */ - ``` +*** - -* 创建 UPDATE 型触发器 - ```mysql - DELIMITER $ - - CREATE TRIGGER account_update - AFTER UPDATE - ON account - FOR EACH ROW - BEGIN - INSERT INTO account_log VALUES (NULL,'UPDATE',NOW(),new.id,CONCAT('修改前{id=',old.id,',name=',old.name,',money=',old.money,'}','修改后{id=',new.id,',name=',new.name,',money=',new.money,'}')); - END$ - - DELIMITER ; +### 安装启动 + +安装: + +* Redis 5.0 被包含在默认的 Ubuntu 20.04 软件源中 + + ```sh + sudo apt update + sudo apt install redis-server ``` - ```mysql - -- 修改account表 - UPDATE account SET money=3500 WHERE id=3; - - -- 查询日志表 - SELECT * FROM account_log; - /* - id operation operation_time operation_id operation_params - 2 UPDATE 2021-01-26 19:58:54 2 更新前{id=2,name=李四money=1000} - 更新后{id=2,name=李四money=200} - */ +* 检查 Redis 状态 + + ```sh + sudo systemctl status redis-server ``` - +启动: -* 创建 DELETE 型触发器 +* 启动服务器——参数启动 - ```mysql - DELIMITER $ - - CREATE TRIGGER account_delete - AFTER DELETE - ON account - FOR EACH ROW - BEGIN - INSERT INTO account_log VALUES (NULL,'DELETE',NOW(),old.id,CONCAT('删除前{id=',old.id,',name=',old.name,',money=',old.money,'}')); - END$ - - DELIMITER ; + ```sh + redis-server [--port port] + #redis-server --port 6379 ``` - ```mysql - -- 删除account表数据 - DELETE FROM account WHERE id=3; - - -- 查询日志表 - SELECT * FROM account_log; - /* - id operation operation_time operation_id operation_params - 3 DELETE 2021-01-26 20:02:48 3 删除前{id=3,name=王五money=2000} - */ +* 启动服务器——配置文件启动 + + ```sh + redis-server config_file_name + #redis-server /etc/redis/conf/redis-6397.conf ``` +* 启动客户端: + + ```sh + redis-cli [-h host] [-p port] + #redis-cli -h 192.168.2.185 -p 6397 + ``` + 注意:服务器启动指定端口使用的是--port,客户端启动指定端口使用的是-p @@ -3947,273 +8831,270 @@ LOOP 实现简单的循环,退出循环的条件需要使用其他的语句定 +### 基本配置 +#### 系统目录 -## 存储引擎 +1. 创建文件结构 -### 基本介绍 + 创建配置文件存储目录 -对比其他数据库,MySQL 的架构可以在不同场景应用并发挥良好作用,主要体现在存储引擎,插件式的存储引擎架构将查询处理和其他的系统任务以及数据的存储提取分离,可以针对不同的存储需求可以选择最优的存储引擎 + ```sh + mkdir conf + ``` -存储引擎的介绍: + 创建服务器文件存储目录(包含日志、数据、临时配置文件等) -- MySQL 数据库使用不同的机制存取表文件 , 机制的差别在于不同的存储方式、索引技巧、锁定水平等不同的功能和能力,在 MySQL 中,将这些不同的技术及配套的功能称为存储引擎 -- Oracle、SqlServer 等数据库只有一种存储引擎,MySQL **提供了插件式的存储引擎架构**,所以 MySQL 存在多种存储引擎 , 就会让数据库采取了不同的处理数据的方式和扩展功能 -- 在关系型数据库中数据的存储是以表的形式存进行,所以存储引擎也称为表类型(存储和操作此表的类型) -- 通过选择不同的引擎,能够获取最佳的方案, 也能够获得额外的速度或者功能,提高程序的整体效果。 + ```sh + mkdir data + ``` -MySQL 支持的存储引擎: +2. 创建配置文件副本放入 conf 目录,Ubuntu 系统配置文件 redis.conf 在目录 `/etc/redis` 中 -- MySQL 支持的引擎包括:InnoDB、MyISAM、MEMORY、Archive、Federate、CSV、BLACKHOLE 等 -- MySQL5.5 之前的默认存储引擎是 MyISAM,5.5 之后就改为了 InnoDB + ```sh + cat redis.conf | grep -v "#" | grep -v "^$" -> /conf/redis-6379.conf + ``` + + 去除配置文件的注释和空格,输出到新的文件,命令方式采用 redis-port.conf -**** +*** -### 引擎对比 +#### 服务器 -MyISAM 存储引擎: +* 设置服务器以守护进程的方式运行,关闭后服务器控制台中将打印服务器运行信息(同日志内容相同): -* 特点:不支持事务和外键,读取速度快,节约资源 -* 应用场景:查询和插入操作为主,只有很少更新和删除操作,并对事务的完整性、并发性要求不高 -* 存储方式: - * 每个 MyISAM 在磁盘上存储成 3 个文件,其文件名都和表名相同,拓展名不同 - * 表的定义保存在 .frm 文件,表数据保存在 .MYD (MYData) 文件中,索引保存在 .MYI (MYIndex) 文件中 + ```sh + daemonize yes|no + ``` -InnoDB 存储引擎:(MySQL5.5 版本后默认的存储引擎) +* 绑定主机地址,绑定本地IP地址,否则SSH无法访问: -- 特点:**支持事务**和外键操作,支持并发控制。对比 MyISAM 的存储引擎,InnoDB 写的处理效率差一些,并且会占用更多的磁盘空间以保留数据和索引 -- 应用场景:对事务的完整性有比较高的要求,在并发条件下要求数据的一致性,读写频繁的操作 -- 存储方式: - - 使用共享表空间存储, 这种方式创建的表的表结构保存在 .frm 文件中, 数据和索引保存在 innodb_data_home_dir 和 innodb_data_file_path 定义的表空间中,可以是多个文件 - - 使用多表空间存储,创建的表的表结构存在 .frm 文件中,每个表的数据和索引单独保存在 .ibd 中 + ```sh + bind ip + ``` -MEMORY 存储引擎: +* 设置服务器端口: -- 特点:每个 MEMORY 表实际对应一个磁盘文件 ,该文件中只存储表的结构,表数据保存在内存中,且默认使用 HASH 索引,这样有利于数据的快速处理,在需要快速定位记录可以提供更快的访问,但是服务一旦关闭,表中的数据就会丢失,数据存储不安全 -- 应用场景:通常用于更新不太频繁的小表,用以快速得到访问结果,类似缓存 -- 存储方式:表结构保存在 .frm 中 + ```sh + port port + ``` -MERGE存储引擎: +* 设置服务器文件保存地址: -* 特点: + ```sh + dir path + ``` - * 是一组 MyISAM 表的组合,这些 MyISAM 表必须结构完全相同,通过将不同的表分布在多个磁盘上 - * MERGE 表本身并没有存储数据,对 MERGE 类型的表可以进行查询、更新、删除操作,这些操作实际上是对内部的 MyISAM 表进行的 +* 设置数据库的数量: -* 应用场景:将一系列等同的 MyISAM 表以逻辑方式组合在一起,并作为一个对象引用他们,适合做数据仓库 + ```sh + databases 16 + ``` -* 操作方式: +* 多服务器快捷配置: - * 插入操作是通过 INSERT_METHOD 子句定义插入的表,使用 FIRST 或 LAST 值使得插入操作被相应地作用在第一或者最后一个表上;不定义这个子句或者定义为 NO,表示不能对 MERGE 表执行插入操作 - * 对 MERGE 表进行 DROP 操作,但是这个操作只是删除 MERGE 表的定义,对内部的表是没有任何影响的 + 导入并加载指定配置文件信息,用于快速创建 redis 公共配置较多的 redis 实例配置文件,便于维护 - ```mysql - CREATE TABLE order_1( - )ENGINE = MyISAM DEFAULT CHARSET=utf8; - - CREATE TABLE order_2( - )ENGINE = MyISAM DEFAULT CHARSET=utf8; - - CREATE TABLE order_all( - -- 结构与MyISAM表相同 - )ENGINE = MERGE UNION = (order_1,order_2) INSERT_METHOD=LAST DEFAULT CHARSET=utf8; + ```sh + include /path/conf_name.conf ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-MERGE.png) - -| 特性 | MyISAM | InnoDB | MEMORY | -| ------------ | ---------------------------- | ------------- | ------------------ | -| 存储限制 | 有(平台对文件系统大小的限制) | 64TB | 有(平台的内存限制) | -| **事务安全** | **不支持** | **支持** | **不支持** | -| **锁机制** | **表锁** | **表锁/行锁** | **表锁** | -| B+Tree索引 | 支持 | 支持 | 支持 | -| 哈希索引 | 不支持 | 不支持 | 支持 | -| 全文索引 | 支持 | 支持 | 不支持 | -| 集群索引 | 不支持 | 支持 | 不支持 | -| 数据索引 | 不支持 | 支持 | 支持 | -| 数据缓存 | 不支持 | 支持 | N/A | -| 索引缓存 | 支持 | 支持 | N/A | -| 数据可压缩 | 支持 | 不支持 | 不支持 | -| 空间使用 | 低 | 高 | N/A | -| 内存使用 | 低 | 高 | 中等 | -| 批量插入速度 | 高 | 低 | 高 | -| **外键** | **不支持** | **支持** | **不支持** | + -面试问题:MyIsam 和 InnoDB 的区别? +*** -* 事务:InnoDB 支持事务,MyISAM 不支持事务 -* 外键:InnoDB 支持外键,MyISAM 不支持外键 -* 索引:InnoDB 是聚集(聚簇)索引,MyISAM 是非聚集(非聚簇)索引 -* 锁粒度:InnoDB 最小的锁粒度是行锁,MyISAM 最小的锁粒度是表锁 -* 存储结构:参考本节上半部分 +#### 客户端 +* 服务器允许客户端连接最大数量,默认 0,表示无限制,当客户端连接到达上限后,Redis 会拒绝新的连接: -*** + ```sh + maxclients count + ``` +* 客户端闲置等待最大时长,达到最大值后关闭对应连接,如需关闭该功能,设置为 0: + ```sh + timeout seconds + ``` -### 引擎操作 -* 查询数据库支持的存储引擎 - ```mysql - SHOW ENGINES; - SHOW VARIABLES LIKE '%storage_engine%'; -- 查看Mysql数据库默认的存储引擎 - ``` +*** -* 查询某个数据库中所有数据表的存储引擎 - ```mysql - SHOW TABLE STATUS FROM 数据库名称; - ``` -* 查询某个数据库中某个数据表的存储引擎 +#### 日志配置 - ```mysql - SHOW TABLE STATUS FROM 数据库名称 WHERE NAME = '数据表名称'; - ``` +设置日志记录 -* 创建数据表,指定存储引擎 +* 设置服务器以指定日志记录级别 - ```mysql - CREATE TABLE 表名( - 列名,数据类型, - ... - )ENGINE = 引擎名称; + ```sh + loglevel debug|verbose|notice|warning ``` -* 修改数据表的存储引擎 +* 日志记录文件名 - ```mysql - ALTER TABLE 表名 ENGINE = 引擎名称; + ```sh + logfile filename ``` +注意:日志级别开发期设置为 verbose 即可,生产环境中配置为 notice,简化日志输出量,降低写日志 IO 的频度 +**配置文件:** +```sh +bind 192.168.2.185 +port 6379 +#timeout 0 +daemonize no +logfile /etc/redis/data/redis-6379.log +dir /etc/redis/data +dbfilename "dump-6379.rdb" +``` -*** +*** -## 索引机制 +#### 基本指令 -### 索引介绍 +帮助信息: -#### 基本介绍 +* 获取命令帮助文档 -MySQL 官方对索引的定义为:索引(index)是帮助 MySQL 高效获取数据的一种数据结构,**本质是排好序的快速查找数据结构。**在表数据之外,数据库系统还维护着满足特定查找算法的数据结构,这些数据结构以某种方式指向数据, 这样就可以在这些数据结构上实现高级查找算法,这种数据结构就是索引。 + ```sh + help [command] + #help set + ``` -**索引是在存储引擎层实现的**,所以并没有统一的索引标准,即不同存储引擎的索引的工作方式并不一样 +* 获取组中所有命令信息名称 -索引使用:一张数据表,用于保存数据;一个索引配置文件,用于保存索引;每个索引都指向了某一个数据 -![](https://gitee.com/seazean/images/raw/master/DB/MySQL-索引的介绍.png) + ```sh + help [@group-name] + #help @string + ``` -左边是数据表,一共有两列七条记录,最左边的是数据记录的物理地址(注意逻辑上相邻的记录在磁盘上也并不是一定物理相邻的)。为了加快 Col2 的查找,可以维护一个右边所示的二叉查找树,每个节点分别包含索引键值和一个指向对应数据的物理地址的指针,这样就可以运用二叉查找快速获取到相应数据 +退出服务 -索引的优点: +* 退出客户端: -* 类似于书籍的目录索引,提高数据检索的效率,降低数据库的 IO 成本 -* 通过索引列对数据进行排序,降低数据排序的成本,降低 CPU 的消耗 + ```sh + quit + exit + ``` -索引的缺点: +* 退出客户端服务器快捷键: -* 一般来说索引本身也很大,不可能全部存储在内存中,因此索引往往以索引文件的形式**存储在磁盘**上 -* 虽然索引大大提高了查询效率,同时却也降低更新表的速度。对表进行 INSERT、UPDATE、DELETE 操作,MySQL 不仅要保存数据,还要保存一下索引文件每次更新添加了索引列的字段,还会调整因为更新所带来的键值变化后的索引信息,**但是更新数据也需要先从数据库中获取**,索引加快了获取速度,所以可以相互抵消一下。 -* 索引会影响到 WHERE 的查询条件和排序 ORDER BY 两大功能 + ```sh + Ctrl+C + ``` -*** -#### 索引分类 -索引一般的分类如下: -- 功能分类 - - 单列索引:一个索引只包含单个列,一个表可以有多个单列索引(普通索引) - - 联合索引:顾名思义,就是将单列索引进行组合 - - 唯一索引:索引列的值必须唯一,允许有空值。如果是联合索引,则列值组合必须唯一 - - 主键索引:一种特殊的唯一索引,不允许有空值,一般在建表时同时创建主键索引 - - 外键索引:只有 InnoDB 引擎支持外键索引,用来保证数据的一致性、完整性和实现级联操作 +*** -- 结构分类 - - BTree 索引:MySQL 使用最频繁的一个索引数据结构,是 InnoDB 和 MyISAM 存储引擎默认的索引类型,底层基于 B+Tree - - Hash 索引:MySQL中 Memory 存储引擎默认支持的索引类型 - - R-tree 索引(空间索引):空间索引是 MyISAM 引擎的一个特殊索引类型,主要用于地理空间数据类型 - - Full-text 索引(全文索引):快速匹配全部文档的方式。MyISAM 支持, InnoDB 不支持 FULLTEXT 类型的索引,但是 InnoDB 可以使用 sphinx 插件支持全文索引,MEMORY 引擎不支持 - - | 索引 | InnoDB | MyISAM | Memory | - | --------- | ---------------- | ------ | ------ | - | BTREE | 支持 | 支持 | 支持 | - | HASH | 不支持 | 不支持 | 支持 | - | R-tree | 不支持 | 支持 | 不支持 | - | Full-text | 5.6 版本之后支持 | 支持 | 不支持 | -联合索引图示:根据身高年龄建立的组合索引(height,age) -![](https://gitee.com/seazean/images/raw/master/DB/MySQL-组合索引图.png) +## 数据库 +### 服务器 +Redis 服务器将所有数据库保存在**服务器状态 redisServer 结构**的 db 数组中,数组的每一项都是 redisDb 结构,代表一个数据库,每个数据库之间相互独立,**共用 **Redis 内存,不区分大小。在初始化服务器时,根据 dbnum 属性决定创建数据库的数量,该属性由服务器配置的 database 选项决定,默认 16 -*** +```c +struct redisServer { + // 保存服务器所有的数据库 + redisDB *db; + + // 服务器数据库的数量 + int dbnum; +}; +``` + +**在服务器内部**,客户端状态 redisClient 结构的 db 属性记录了目标数据库,是一个指向 redisDb 结构的指针 -### 聚簇索引 +```c +struct redisClient { + // 记录客户端正在使用的数据库,指向 redisServer.db 数组中的某一个 db + redisDB *db; +}; +``` -#### 索引对比 +每个 Redis 客户端都有目标数据库,执行数据库读写命令时目标数据库就会成为这些命令的操作对象,默认情况下 Redis 客户端的目标数据库为 0 号数据库,客户端可以执行 SELECT 命令切换目标数据库,原理是通过修改 redisClient.db 指针指向服务器中不同数据库 -聚簇索引是一种数据存储方式,并不是一种单独的索引类型 +命令操作: -* 聚簇索引的叶子节点存放的是主键值和数据行,支持覆盖索引 +```sh +select index #切换数据库,index从0-15取值 +move key db #数据移动到指定数据库,db是数据库编号 +ping #测试数据库是否连接正常,返回PONG +echo message #控制台输出信息 +``` -* 非聚簇索引的叶子节点存放的是主键值或指向数据行的指针(由存储引擎决定) +Redis 没有可以返回客户端目标数据库的命令,但是 redis-cli 客户端旁边会提示当前所使用的目标数据库 -在 Innodb 下主键索引是聚簇索引,在 MyISAM 下主键索引是非聚簇索引 +```sh +redis> SELECT 1 +OK +redis[1]> +``` -*** +*** -#### Innodb -##### 聚簇索引 -在 Innodb 存储引擎,B+ 树索引可以分为聚簇索引(也称聚集索引、clustered index)和辅助索引(也称非聚簇索引或二级索引、secondary index、non-clustered index) +### 键空间 -InnoDB 中,聚簇索引是按照每张表的主键构造一颗 B+ 树,叶子节点中存放的就是整张表的数据,将聚簇索引的叶子节点称为数据页 +#### key space -* 这个特性决定了**数据也是索引的一部分**,所以一张表只能有一个聚簇索引 -* 辅助索引的存在不影响聚簇索引中数据的组织,所以一张表可以有多个辅助索引 +Redis 是一个键值对(key-value pair)数据库服务器,每个数据库都由一个 redisDb 结构表示,redisDb.dict **字典中保存了数据库的所有键值对**,将这个字典称为键空间(key space) -聚簇索引的优点: +```c +typedef struct redisDB { + // 数据库键空间,保存所有键值对 + dict *dict +} redisDB; +``` -* 数据访问更快,聚簇索引将索引和数据保存在同一个 B+ 树中,因此从聚簇索引中获取数据比非聚簇索引更快 -* 聚簇索引对于主键的排序查找和范围查找速度非常快 +键空间和用户所见的数据库是直接对应的: -聚簇索引的缺点: +* 键空间的键就是数据库的键,每个键都是一个字符串对象 +* 键空间的值就是数据库的值,每个值可以是任意一种 Redis 对象 -* 插入速度严重依赖于插入顺序,按照主键的顺序(递增)插入是最快的方式,否则将会出现页分裂,严重影响性能,所以对于 InnoDB 表,一般都会定义一个自增的 ID 列为主键 +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-数据库键空间.png) -* 更新主键的代价很高,将会导致被更新的行移动,所以对于 InnoDB 表,一般定义主键为不可更新 +当使用 Redis 命令对数据库进行读写时,服务器不仅会对键空间执行指定的读写操作,还会**进行一些维护操作**: -* 二级索引访问需要两次索引查找,第一次找到主键值,第二次根据主键值找到行数据 +* 在读取一个键后(读操作和写操作都要对键进行读取),服务器会根据键是否存在来更新服务器的键空间命中 hit 次数或键空间不命中 miss 次数,这两个值可以在 `INFO stats` 命令的 keyspace_hits 属性和 keyspace_misses 属性中查看 +* 更新键的 LRU(最后使用)时间,该值可以用于计算键的闲置时间,使用 `OBJECT idletime key` 查看键 key 的闲置时间 +* 如果在读取一个键时发现该键已经过期,服务器会**先删除过期键**,再执行其他操作 +* 如果客户端使用 WATCH 命令监视了某个键,那么服务器在对被监视的键进行修改之后,会将这个键标记为脏(dirty),从而让事务注意到这个键已经被修改过 +* 服务器每次修改一个键之后,都会对 dirty 键计数器的值增1,该计数器会触发服务器的持久化以及复制操作 +* 如果服务器开启了数据库通知功能,那么在对键进行修改之后,服务器将按配置发送相应的数据库通知 @@ -4221,39 +9102,65 @@ InnoDB 中,聚簇索引是按照每张表的主键构造一颗 B+ 树,叶子 -##### 辅助索引 - -在聚簇索引之上创建的索引称之为辅助索引,非聚簇索引都是辅助索引,像复合索引、前缀索引、唯一索引等 +#### 读写指令 -辅助索引叶子节点存储的是主键值,而不是数据的物理地址,所以访问数据需要二次查找,推荐使用覆盖索引,可以减少回表查询 +常见键操作指令: -**检索过程**:辅助索引找到主键值,再通过聚簇索引(二分)找到数据页,最后通过数据页中的 Page Directory(二分)找到对应的数据分组,遍历组内所所有的数据找到数据行 +* 增加指令 -补充:无索引走全表查询,查到数据页后和上述步骤一致 + ```sh + set key value #添加一个字符串类型的键值对 +* 删除指令 + ```sh + del key #删除指定key + unlink key #非阻塞删除key,真正的删除会在后续异步操作 + ``` -*** +* 更新指令 + ```sh + rename key newkey #改名 + renamenx key newkey #改名 + ``` + 值得更新需要参看具体得 Redis 对象得操作方式,比如字符串对象执行 `SET key value` 就可以完成修改 -##### 索引实现 +* 查询指令 -InnoDB 使用 B+Tree 作为索引结构,并且 InnoDB 一定有索引 + ```sh + exists key #获取key是否存在 + randomkey #随机返回一个键 + keys pattern #查询key + ``` -主键索引: + KEYS 命令需要**遍历存储的键值对**,操作延时高,一般不被建议用于生产环境中 -* 在 InnoDB 中,表数据文件本身就是按 B+Tree 组织的一个索引结构,这个索引的 key 是数据表的主键,叶子节点 data 域保存了完整的数据记录 + 查询模式规则:* 匹配任意数量的任意符号、? 配合一个任意符号、[] 匹配一个指定符号 + + ```sh + keys * #查询所有key + keys aa* #查询所有以aa开头 + keys *bb #查询所有以bb结尾 + keys ??cc #查询所有前面两个字符任意,后面以cc结尾 + keys user:? #查询所有以user:开头,最后一个字符任意 + keys u[st]er:1 #查询所有以u开头,以er:1结尾,中间包含一个字母,s或t + ``` -* InnoDB 的表数据文件**通过主键聚集数据**,如果没有定义主键,会选择非空唯一索引代替,如果也没有这样的列,MySQL 会自动为 InnoDB 表生成一个**隐含字段**作为主键,这个字段长度为 6 个字节,类型为长整形(MVCC 部分的笔记提及) -辅助索引: +* 其他指令 -* InnoDB 的所有辅助索引(二级索引)都引用主键作为 data 域 + ```sh + type key #获取key的类型 + dbsize #获取当前数据库的数据总量,即key的个数 + flushdb #清除当前数据库的所有数据(慎用) + flushall #清除所有数据(慎用) + ``` + + 在执行 FLUSHDB 这样的危险命令之前,最好先执行一个 SELECT 命令,保证当前所操作的数据库是目标数据库 -* InnoDB 表是基于聚簇索引建立的,因此 InnoDB 的索引能提供一种非常快速的主键查找性能。不过辅助索引也会包含主键列,所以不建议使用过长的字段作为主键,**过长的主索引会令辅助索引变得过大** -![](https://gitee.com/seazean/images/raw/master/DB/MySQL-InnoDB聚簇和辅助索引结构.png) @@ -4261,453 +9168,473 @@ InnoDB 使用 B+Tree 作为索引结构,并且 InnoDB 一定有索引 -#### MyISAM +#### 时效设置 -##### 非聚簇 +客户端可以以秒或毫秒的精度为数据库中的某个键设置生存时间(TimeTo Live, TTL),在经过指定时间之后,服务器就会自动删除生存时间为 0 的键;也可以以 UNIX 时间戳的方式设置过期时间(expire time),当键的过期时间到达,服务器会自动删除这个键 -MyISAM 的主键索引使用的是非聚簇索引,索引文件和数据文件是分离的,**索引文件仅保存数据的地址** +```sh +expire key seconds #为指定key设置生存时间,单位为秒 +pexpire key milliseconds #为指定key设置生存时间,单位为毫秒 +expireat key timestamp #为指定key设置过期时间,单位为时间戳 +pexpireat key mil-timestamp #为指定key设置过期时间,单位为毫秒时间戳 +``` -* 主键索引 B+ 树的节点存储了主键,辅助键索引 B+ 树存储了辅助键,表数据存储在独立的地方,这两颗 B+ 树的叶子节点都使用一个地址指向真正的表数据,对于表数据来说,这两个键没有任何差别 -* 由于索引树是独立的,通过辅助索引检索无需访问主键的索引树回表查询 +* 实际上 EXPIRE、EXPIRE、EXPIREAT 三个命令**底层都是转换为 PEXPIREAT 命令**来实现的 +* SETEX 命令可以在设置一个字符串键的同时为键设置过期时间,但是该命令是一个类型限定命令 -![](https://gitee.com/seazean/images/raw/master/DB/MySQL-聚簇索引和辅助索引检锁数据图.jpg) +redisDb 结构的 expires 字典保存了数据库中所有键的过期时间,字典称为过期字典: +* 键是一个指针,指向键空间中的某个键对象(复用键空间的对象,不会产生内存浪费) +* 值是一个 long long 类型的整数,保存了键的过期时间,是一个毫秒精度的 UNIX 时间戳 +```c +typedef struct redisDB { + // 过期字典,保存所有键的过期时间 + dict *expires +} redisDB; +``` -*** +客户端执行 PEXPIREAT 命令,服务器会在数据库的过期字典中关联给定的数据库键和过期时间: + +```python +def PEXPIREAT(key, expire_time_in_ms): + # 如果给定的键不存在于键空间,那么不能设置过期时间 + if key not in redisDb.dict: + return 0 + + # 在过期字典中关联键和过期时间 + redisDB.expires[key] = expire_time_in_ms + + # 过期时间设置成功 + return 1 +``` -##### 索引实现 +**** -MyISAM 的索引方式也叫做非聚集的,之所以这么称呼是为了与 InnoDB 的聚集索引区分 -主键索引:MyISAM 引擎使用 B+Tree 作为索引结构,叶节点的 data 域存放的是数据记录的地址 -辅助索引:MyISAM 中主索引和辅助索引(Secondary key)在结构上没有任何区别,只是主索引要求 key 是唯一的,而辅助索引的 key 可以重复 +#### 时效状态 -![](https://gitee.com/seazean/images/raw/master/DB/MySQL-MyISAM主键和辅助索引结构.png) +TTL 和 PTTL 命令通过计算键的过期时间和当前时间之间的差,返回这个键的剩余生存时间 +* 返回正数代表该数据在内存中还能存活的时间 +* 返回 -1 代表永久性,返回 -2 代表键不存在 +```sh +ttl key #获取key的剩余时间,每次获取会自动变化(减小),类似于倒计时 +pttl key #获取key的剩余时间,单位是毫秒,每次获取会自动变化(减小) +``` +PERSIST 是 PEXPIREAT 命令的反操作,在过期字典中查找给定的键,并解除键和值(过期时间)在过期字典中的关联 +```sh +persist key #切换key从时效性转换为永久性 +``` -参考文章:https://blog.csdn.net/lm1060891265/article/details/81482136 +Redis 通过过期字典可以检查一个给定键是否过期: +* 检查给定键是否存在于过期字典:如果存在,那么取得键的过期时间 +* 检查当前 UNIX 时间戳是否大于键的过期时间:如果是那么键已经过期,否则键未过期 +补充:AOF、RDB 和复制功能对过期键的处理 -*** +* RDB : + * 生成 RDB 文件,程序会对数据库中的键进行检查,已过期的键不会被保存到新创建的 RDB 文件中 + * 载入 RDB 文件,如果服务器以主服务器模式运行,那么在载入时会对键进行检查,过期键会被忽略;如果服务器以从服务器模式运行,会载入所有键,包括过期键,但是主从服务器进行数据同步时就会删除这些键 +* AOF: + * 写入 AOF 文件,如果数据库中的某个键已经过期,但还没有被删除,那么 AOF 文件不会因为这个过期键而产生任何影响;当该过期键被删除,程序会向 AOF 文件追加一条 DEL 命令,显式的删除该键 + * AOF 重写,会对数据库中的键进行检查,忽略已经过期的键 +* 复制:当服务器运行在复制模式下时,从服务器的过期键删除动作由主服务器控制 + * 主服务器在删除一个过期键之后,会显式地向所有从服务器发送一个 DEL 命令,告知从服务器删除这个过期键 + * 从服务器在执行客户端发送的读命令时,即使碰到过期键也不会将过期键删除,会当作未过期键处理,只有在接到主服务器发来的 DEL 命令之后,才会删除过期键(数据不一致) -### 索引结构 -#### BTree -磁盘存储: +**** -* 系统从磁盘读取数据到内存时是以磁盘块(block)为基本单位的,位于同一个磁盘块中的数据会被一次性读取出来,而不是需要什么取什么 -- InnoDB 存储引擎中有页(Page)的概念,页是其磁盘管理的最小单位,InnoDB 存储引擎中默认每个页的大小为 16KB -- InnoDB 引擎将若干个地址连接磁盘块,以此来达到页的大小 16KB,在查询数据时如果一个页中的每条数据都能有助于定位数据记录的位置,这将会减少磁盘 I/O 次数,提高查询效率 -BTree 的索引类型是基于 B+Tree 树型数据结构的,B+Tree 又是 BTree 数据结构的变种,用在数据库和操作系统中的文件系统,特点是能够保持数据稳定有序 +### 过期删除 -BTree 又叫多路平衡搜索树,一颗 m 叉的 BTree 特性如下: +#### 删除策略 -- 树中每个节点最多包含 m 个孩子 -- 除根节点与叶子节点外,每个节点至少有 [ceil(m/2)] 个孩子 -- 若根节点不是叶子节点,则至少有两个孩子 -- 所有的叶子节点都在同一层 -- 每个非叶子节点由 n 个key与 n+1 个指针组成,其中 [ceil(m/2)-1] <= n <= m-1 +删除策略就是**针对已过期数据的处理策略**,已过期的数据不一定被立即删除,在不同的场景下使用不同的删除方式会有不同效果,在内存占用与 CPU 占用之间寻找一种平衡,顾此失彼都会造成整体 Redis 性能的下降,甚至引发服务器宕机或内存泄露 -5 叉,key 的数量 [ceil(m/2)-1] <= n <= m-1 为 2 <= n <=4 ,当 n>4 时中间节点分裂到父节点,两边节点分裂 +针对过期数据有三种删除策略: -插入 C N G A H E K Q M F W L T Z D P R X Y S 数据的工作流程: +- 定时删除 +- 惰性删除(被动删除) +- 定期删除 -* 插入前4个字母 C N G A +Redis 采用惰性删除和定期删除策略的结合使用 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-BTree工作流程1.png) -* 插入H,n>4,中间元素G字母向上分裂到新的节点 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-BTree工作流程2.png) +*** -* 插入E,K,Q不需要分裂 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-BTree工作流程3.png) -* 插入M,中间元素M字母向上分裂到父节点G +#### 定时删除 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-BTree工作流程4.png) +在设置键的过期时间的同时,创建一个定时器(timer),让定时器在键的过期时间到达时,立即执行对键的删除操作 -* 插入F,W,L,T 不需要分裂 +- 优点:节约内存,到时就删除,快速释放掉不必要的内存占用 +- 缺点:对 CPU 不友好,无论 CPU 此时负载多高均占用 CPU,会影响 Redis 服务器响应时间和指令吞吐量 +- 总结:用处理器性能换取存储空间(拿时间换空间) - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-BTree工作流程5.png) +创建一个定时器需要用到 Redis 服务器中的时间事件,而时间事件的实现方式是无序链表,查找一个事件的时间复杂度为 O(N),并不能高效地处理大量时间事件,所以采用这种方式并不现实 -* 插入Z,中间元素T向上分裂到父节点中 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-BTree工作流程6.png) -* 插入D,中间元素D向上分裂到父节点中,然后插入P,R,X,Y不需要分裂 +*** - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-BTree工作流程7.png) -* 最后插入S,NPQR节点n>5,中间节点Q向上分裂,但分裂后父节点DGMT的n>5,中间节点M向上分裂 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-BTree工作流程8.png) +#### 惰性删除 -BTree 树就已经构建完成了,BTree 树和二叉树相比, 查询数据的效率更高, 因为对于相同的数据量来说,**BTree 的层级结构比二叉树小**,所以搜索速度快 +数据到达过期时间不做处理,等下次访问到该数据时执行 **expireIfNeeded()** 判断: -BTree 结构的数据可以让系统高效的找到数据所在的磁盘块,定义一条记录为一个二元组 [key, data] ,key 为记录的键值,对应表中的主键值,data 为一行记录中除主键外的数据。对于不同的记录,key 值互不相同,BTree 中的每个节点根据实际情况可以包含大量的关键字信息和分支 -![](https://gitee.com/seazean/images/raw/master/DB/索引的原理-BTree.png) +* 如果输入键已经过期,那么 expireIfNeeded 函数将输入键从数据库中删除,接着访问就会返回空 +* 如果输入键未过期,那么 expireIfNeeded 函数不做动作 -缺点:当进行范围查找时会出现回旋查找 +所有的 Redis 读写命令在执行前都会调用 expireIfNeeded 函数进行检查,该函数就像一个过滤器,在命令真正执行之前过滤掉过期键 +惰性删除的特点: +* 优点:节约 CPU 性能,删除的目标仅限于当前处理的键,不会在删除其他无关的过期键上花费任何 CPU 时间 +* 缺点:内存压力很大,出现长期占用内存的数据,如果过期键永远不被访问,这种情况相当于内存泄漏 +* 总结:用存储空间换取处理器性能(拿空间换时间) -*** +*** -#### B+Tree -##### 数据结构 -BTree 数据结构中每个节点中不仅包含数据的 key 值,还有 data 值。磁盘中每一页的存储空间是有限的,如果 data 数据较大时将会导致每个节点(即一个页)能存储的 key 的数量很小,当存储的数据量很大时同样会导致 B-Tree 的深度较大,增大查询时的磁盘 I/O 次数,进而影响查询效率,所以引入 B+Tree +#### 定期删除 -B+Tree 为 BTree 的变种,B+Tree 与 BTree 的区别为: +定期删除策略是每隔一段时间执行一次删除过期键操作,并通过限制删除操作执行的时长和频率来减少删除操作对 CPU 时间的影响 -* n 叉 B+Tree 最多含有 n 个 key(哈希值),而 BTree 最多含有 n-1 个 key +* 如果删除操作执行得太频繁,或者执行时间太长,就会退化成定时删除策略,将 CPU 时间过多地消耗在删除过期键上 +* 如果删除操作执行得太少,或者执行时间太短,定期删除策略又会和惰性删除策略一样,出现浪费内存的情况 -- 所有**非叶子节点只存储键值 key** 信息,只进行数据索引,使每个非叶子节点所能保存的关键字大大增加 -- 所有**数据都存储在叶子节点**,所以每次数据查询的次数都一样 -- **叶子节点按照 key 大小顺序排列,左边结尾数据都会保存右边节点开始数据的指针,形成一个链表** -- 所有节点中的 key 在叶子节点中也存在(比如 5),key 允许重复,B 树不同节点不存在重复的 key +定期删除是**周期性轮询 Redis 库中的时效性**数据,从过期字典中随机抽取一部分键检查,利用过期数据占比的方式控制删除频度 - +- Redis 启动服务器初始化时,读取配置 server.hz 的值,默认为 10,执行指令 info server 可以查看,每秒钟执行 server.hz 次 `serverCron() → activeExpireCycle()` -B* 树:是 B+ 树的变体,在 B+ 树的非根和非叶子结点再增加指向兄弟的指针 +- activeExpireCycle() 对某个数据库中的每个 expires 进行检测,工作模式: + * 轮询每个数据库,从数据库中取出一定数量的随机键进行检查,并删除其中的过期键,如果过期 key 的比例超过了 25%,则继续重复此过程,直到过期 key 的比例下降到 25% 以下,或者这次任务的执行耗时超过了 25 毫秒 + * 全局变量 current_db 用于记录 activeExpireCycle() 的检查进度(哪一个数据库),下一次调用时接着该进度处理 + * 随着函数的不断执行,服务器中的所有数据库都会被检查一遍,这时将 current_db 重置为 0,然后再次开始新一轮的检查 -*** +定期删除特点: +- CPU 性能占用设置有峰值,检测频度可自定义设置 +- 内存压力不是很大,长期占用内存的**冷数据会被持续清理** +- 周期性抽查存储空间(随机抽查,重点抽查) -##### 优化结构 -MySQL 索引数据结构对经典的 B+Tree 进行了优化,在原 B+Tree 的基础上,增加一个指向相邻叶子节点的链表指针,就形成了带有顺序指针的 B+Tree,**提高区间访问的性能,防止回旋查找** -区间访问的意思是访问索引为 5 - 15 的数据,可以直接根据相邻节点的指针遍历 -B+ 树的叶子节点是数据页(page),一个页里面可以存多个数据行 +*** -![](https://gitee.com/seazean/images/raw/master/DB/索引的原理-B+Tree.png) -通常在 B+Tree 上有两个头指针,**一个指向根节点,另一个指向关键字最小的叶子节点**,而且所有叶子节点(即数据节点)之间是一种链式环结构。可以对 B+Tree 进行两种查找运算: -- 有范围:对于主键的范围查找和分页查找 -- 有顺序:从根节点开始,进行随机查找,顺序查找 +### 数据淘汰 -InnoDB 中每个数据页的大小默认是 16KB,一般表的主键类型为 INT(4 字节)或 BIGINT(8 字节),指针类型也一般为 4 或 8 个字节,也就是说一个页(B+Tree 中的**一个节点**)中大概存储 16KB/(8B+8B)=1K 个键值(估值)。则一个深度为 3 的 B+Tree 索引可以维护 `10^3 * 10^3 * 10^3 = 10亿` 条记录 +#### 逐出算法 -实际情况中每个节点可能不能填充满,因此在数据库中,B+Tree 的高度一般都在 2-4 层。MySQL 的 InnoDB 存储引擎在设计时是**将根节点常驻内存的**,也就是说查找某一键值的行记录时最多只需要 1~3 次磁盘 I/O 操作 +数据淘汰策略:当新数据进入 Redis 时,在执行每一个命令前,会调用 **freeMemoryIfNeeded()** 检测内存是否充足。如果内存不满足新加入数据的最低存储要求,Redis 要临时删除一些数据为当前指令清理存储空间,清理数据的策略称为**逐出算法** -B+Tree 优点:提高查询速度,减少磁盘的 IO 次数,树形结构较小 +逐出数据的过程不是 100% 能够清理出足够的可使用的内存空间,如果不成功则反复执行,当对所有数据尝试完毕,如不能达到内存清理的要求,**出现 Redis 内存打满异常**: +```sh +(error) OOM command not allowed when used memory >'maxmemory' +``` -*** +**** -##### 索引维护 -B+ 树为了保持索引的有序性,在插入新值的时候需要做相应的维护 +#### 策略配置 -每个索引中每个块存储在磁盘页中,可能会出现以下两种情况: +Redis 如果不设置最大内存大小或者设置最大内存大小为 0,在 64 位操作系统下不限制内存大小,在 32 位操作系统默认为 3GB 内存,一般推荐设置 Redis 内存为最大物理内存的四分之三 -* 如果所在的数据页已经满了,这时候需要申请一个新的数据页,然后挪动部分数据过去,这个过程称为**页分裂** -* 当相邻两个页由于删除了数据,利用率很低之后,会将数据页做**页合并**,合并的过程可以认为是分裂过程的逆过程 -* 这两个情况都是由 B+ 树的结构决定的 +内存配置方式: -一般选用数据小的字段做索引,字段长度越小,普通索引的叶子节点就越小,普通索引占用的空间也就越小 +* 通过修改文件配置(永久生效):修改配置文件 maxmemory 字段,单位为字节 +* 通过命令修改(重启失效): + * `config set maxmemory 104857600`:设置 Redis 最大占用内存为 100MB + * `config get maxmemory`:获取 Redis 最大占用内存 -*** + * `info` :可以查看 Redis 内存使用情况,`used_memory_human` 字段表示实际已经占用的内存,`maxmemory` 表示最大占用内存 +影响数据淘汰的相关配置如下,配置 conf 文件: +* 每次选取待删除数据的个数,采用随机获取数据的方式作为待检测删除数据,防止全库扫描,导致严重的性能消耗,降低读写性能 -### 索引操作 + ```sh + maxmemory-samples count + ``` -索引在创建表的时候可以同时创建, 也可以随时增加新的索引 +* 达到最大内存后的,对被挑选出来的数据进行删除的策略 -* 创建索引:如果一个表中有一列是主键,那么会**默认为其创建主键索引**(主键列不需要单独创建索引) - - ```mysql - CREATE [UNIQUE|FULLTEXT] INDEX 索引名称 [USING 索引类型] ON 表名(列名...); - -- 索引类型默认是 B+TREE + ```sh + maxmemory-policy policy ``` - -* 查看索引 - ```mysql - SHOW INDEX FROM 表名; - ``` + 数据删除的策略 policy:3 类 8 种 -* 添加索引 + 第一类:检测易失数据(可能会过期的数据集 server.db[i].expires): - ```mysql - -- 单列索引 - ALTER TABLE 表名 ADD INDEX 索引名称(列名); - - -- 组合索引 - ALTER TABLE 表名 ADD INDEX 索引名称(列名1,列名2,...); - - -- 主键索引 - ALTER TABLE 表名 ADD PRIMARY KEY(主键列名); - - -- 外键索引(添加外键约束,就是外键索引) - ALTER TABLE 表名 ADD CONSTRAINT 外键名 FOREIGN KEY (本表外键列名) REFERENCES 主表名(主键列名); - - -- 唯一索引 - ALTER TABLE 表名 ADD UNIQUE 索引名称(列名); - - -- 全文索引(mysql只支持文本类型) - ALTER TABLE 表名 ADD FULLTEXT 索引名称(列名); + ```sh + volatile-lru # 对设置了过期时间的 key 选择最近最久未使用使用的数据淘汰 + volatile-lfu # 对设置了过期时间的 key 选择最近使用次数最少的数据淘汰 + volatile-ttl # 对设置了过期时间的 key 选择将要过期的数据淘汰 + volatile-random # 对设置了过期时间的 key 选择任意数据淘汰 ``` -* 删除索引 + 第二类:检测全库数据(所有数据集 server.db[i].dict ): - ```mysql - DROP INDEX 索引名称 ON 表名; + ```sh + allkeys-lru # 对所有 key 选择最近最少使用的数据淘汰 + allkeLyRs-lfu # 对所有 key 选择最近使用次数最少的数据淘汰 + allkeys-random # 对所有 key 选择任意数据淘汰,相当于随机 ``` -* 案例练习 - - 数据准备:student - - ```mysql - id NAME age score - 1 张三 23 99 - 2 李四 24 95 - 3 王五 25 98 - 4 赵六 26 97 - ``` - - 索引操作: - - ```mysql - -- 为student表中姓名列创建一个普通索引 - CREATE INDEX idx_name ON student(NAME); - - -- 为student表中年龄列创建一个唯一索引 - CREATE UNIQUE INDEX idx_age ON student(age); + 第三类:放弃数据驱逐 + + ```sh + no-enviction #禁止驱逐数据(redis4.0中默认策略),会引发OOM(Out Of Memory) ``` - - -*** +数据淘汰策略配置依据:使用 INFO 命令输出监控信息,查询缓存 hit 和 miss 的次数,根据需求调优 Redis 配置 -### 设计原则 -索引的设计可以遵循一些已有的原则,创建索引的时候请尽量考虑符合这些原则,便于提升索引的使用效率 -创建索引时的原则: -- 对查询频次较高,且数据量比较大的表建立索引 -- 使用唯一索引,区分度越高,使用索引的效率越高 -- 索引字段的选择,最佳候选列应当从 where 子句的条件中提取,使用覆盖索引 -- 使用短索引,索引创建之后也是使用硬盘来存储的,因此提升索引访问的 I/O 效率,也可以提升总体的访问效率。假如构成索引的字段总长度比较短,那么在给定大小的存储块内可以存储更多的索引值,相应的可以有效的提升 MySQL 访问索引的 I/O 效率 -- 索引可以有效的提升查询数据的效率,但索引数量不是多多益善,索引越多,维护索引的代价越高。对于插入、更新、删除等 DML 操作比较频繁的表来说,索引过多,会引入相当高的维护代价,降低 DML 操作的效率,增加相应操作的时间消耗;另外索引过多的话,MySQL 也会犯选择困难病,虽然最终仍然会找到一个可用的索引,但提高了选择的代价 +*** -* MySQL 建立联合索引时会遵守**最左前缀匹配原则**,即最左优先,在检索数据时从联合索引的最左边开始匹配 - - N 个列组合而成的组合索引,相当于创建了 N 个索引,如果查询时 where 句中使用了组成该索引的**前**几个字段,那么这条查询 SQL 可以利用组合索引来提升查询效率 - - ```mysql - -- 对name、address、phone列建一个联合索引 - ALTER TABLE user ADD INDEX index_three(name,address,phone); - -- 查询语句执行时会依照最左前缀匹配原则,检索时分别会使用索引进行数据匹配。 - (name,address,phone) - (name,address) - (name,phone) -- 只有name字段走了索引 - (name) - - -- 索引的字段可以是任意顺序的,优化器会帮助我们调整顺序,下面的SQL语句可以命中索引 - SELECT * FROM user WHERE address = '北京' AND phone = '12345' AND name = '张三'; - ``` - - ```mysql - -- 如果联合索引中最左边的列不包含在条件查询中,SQL语句就不会命中索引,比如: - SELECT * FROM user WHERE address = '北京' AND phone = '12345'; - ``` -哪些情况不要建立索引: -* 记录太少的表 -* 经常增删改的表 -* 频繁更新的字段不适合创建索引 -* where 条件里用不到的字段不创建索引 +### 排序机制 +#### 基本介绍 +Redis 的 SORT 命令可以对列表键、集合键或者有序集合键的值进行排序,并不更改集合中的数据位置,只是查询 -*** +```sh +SORT key [ASC/DESC] #对key中数据排序,默认对数字排序,并不更改集合中的数据位置,只是查询 +SORT key ALPHA #对key中字母排序,按照字典序 +``` -### 索引优化 -#### 覆盖索引 -覆盖索引:包含所有满足查询需要的数据的索引(SELECT 后面的字段刚好是索引字段),可以利用该索引返回 SELECT 列表的字段,而不必根据索引去聚簇索引上读取数据文件 +*** -回表查询:要查找的字段不在非主键索引树上时,需要通过叶子节点的主键值去主键索引上获取对应的行数据 -使用覆盖索引,防止回表查询: -* 表 user 主键为 id,普通索引为 age,查询语句: +#### SORT - ```mysql - SELECT * FROM user WHERE age = 30; - ``` +`SORT ` 命令可以对一个包含数字值的键 key 进行排序 - 查询过程:先通过普通索引 age=30 定位到主键值 id=1,再通过聚集索引 id=1 定位到行记录数据,需要两次扫描 B+ 树 +假设 `RPUSH numbers 3 1 2`,执行 `SORT numbers` 的详细步骤: -* 使用覆盖索引: +* 创建一个和 key 列表长度相同的数组,数组每项都是 redisSortObject 结构 - ```mysql - DROP INDEX idx_age ON user; - CREATE INDEX idx_age_name ON user(age,name); - SELECT id,age FROM user WHERE age = 30; + ```c + typedef struct redisSortObject { + // 被排序键的值 + robj *obj; + + // 权重 + union { + // 排序数字值时使用 + double score; + // 排序带有 BY 选项的字符串 + robj *cmpobj; + } u; + } ``` - 在一棵索引树上就能获取查询所需的数据,无需回表速度更快 +* 遍历数组,将各个数组项的 obj 指针分别指向 numbers 列表的各个项 -使用覆盖索引,要注意 SELECT 列表中只取出需要的列,不可用 SELECT *,所有字段一起做索引会导致索引文件过大,查询性能下降 +* 遍历数组,将 obj 指针所指向的列表项转换成一个 double 类型的浮点数,并将浮点数保存在对应数组项的 u.score 属性里 +* 根据数组项 u.score 属性的值,对数组进行数字值排序,排序后的数组项按 u.score 属性的值**从小到大排列** +* 遍历数组,将各个数组项的 obj 指针所指向的值作为排序结果返回给客户端,程序首先访问数组的索引 0,依次向后访问 -*** +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-sort排序.png) +对于 `SORT key [ASC/DESC]` 函数: +* 在执行升序排序时,排序算法使用的对比函数产生升序对比结果 +* 在执行降序排序时,排序算法使用的对比函数产生降序对比结果 -#### 索引下推 -索引条件下推优化(Index Condition Pushdown)是 MySQL5.6 添加,可以在索引遍历过程中,对索引中包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表次数 -索引下推充分利用了索引中的数据,在查询出整行数据之前过滤掉无效的数据,再去主键索引树上查找 +**** -* 不使用索引下推优化时存储引擎通过索引检索到数据返回给 MySQL 服务器,服务器判断数据是否符合条件,符合条件的数据去聚簇索引回表查询,获取完整的数据 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-不使用索引下推.png) -* 使用索引下推优化时,如果**存在某些被索引的列的判断条件**时,MySQL 服务器将这一部分判断条件传递给存储引擎,由存储引擎在索引遍历的过程中判断数据是否符合传递的条件,将符合条件的数据进行回表,检索出来返回给服务器,由此减少 IO 次数 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-使用索引下推.png) +#### BY -**适用条件**: +SORT 命令默认使用被排序键中包含的元素作为排序的权重,元素本身决定了元素在排序之后所处的位置,通过使用 BY 选项,SORT 命令可以指定某些字符串键,或者某个哈希键所包含的某些域(field)来作为元素的权重,对一个键进行排序 -* 需要存储引擎将索引中的数据与条件进行判断(所以条件列必须都在同一个索引中),所以优化是基于存储引擎的,只有特定引擎可以使用,适用于 InnoDB 和 MyISAM -* 存储引擎没有调用跨存储引擎的能力,跨存储引擎的功能有存储过程、触发器、视图,所以调用这些功能的不可以进行索引下推优化 -* 对于 InnoDB 引擎只适用于二级索引,InnoDB 的聚簇索引会将整行数据读到缓冲区,不再需要去回表查询了,索引下推的目的减少 IO 次数也就失去了意义 +```sh +SORT BY # 数值 +SORT BY ALPHA # 字符 +``` -工作过程:用户表 user,(name, age) 是联合索引 +```sh +redis> SADD fruits "apple" "banana" "cherry" +(integer) 3 +redis> SORT fruits ALPHA +1) "apple" +2) "banana" +3) "cherry" +``` -```mysql -SELECT * FROM user WHERE name LIKE '张%' AND age = 10; -- 头部模糊匹配会造成索引失效 +```sh +redis> MSET apple-price 8 banana-price 5.5 cherry-price 7 +OK +# 使用水果的价钱进行排序 +redis> SORT fruits BY *-price +1) "banana" +2) "cherry" +3) "apple" ``` -* 优化前:在非主键索引树上找到满足第一个条件的行,然后通过叶子节点记录的主键值再回到主键索引树上查找到对应的行数据,再对比 AND 后的条件是否符合,符合返回数据,需要 4 次回表 +实现原理:排序时的 u.score 属性就会被设置为对应的权重 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-索引下推优化1.png) -* 优化后:检查索引中存储的列信息是否符合索引条件,然后交由存储引擎用剩余的判断条件判断此行数据是否符合要求,**不满足条件的不去读取表中的数据**,满足下推条件的就根据主键值进行回表查询,2 次回表 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-索引下推优化2.png) -当使用 EXPLAIN 进行分析时,如果使用了索引条件下推,Extra 会显示 Using index condition +*** -参考文章:https://blog.csdn.net/sinat_29774479/article/details/103470244 -参考文章:https://time.geekbang.org/column/article/69636 +#### LIMIT +SORT 命令默认会将排序后的所有元素都返回给客户端,通过 LIMIT 选项可以让 SORT 命令只返回其中一部分已排序的元素 -*** +```sh +LIMIT +``` +* offset 参数表示要跳过的已排序元素数量 +* count 参数表示跳过给定数量的元素后,要返回的已排序元素数量 +```sh +# 对应 a b c d e f g +redis> SORT alphabet ALPHA LIMIT 2 3 +1) "c" +2) "d" +3) "e" +``` -#### 前缀索引 +实现原理:在排序后的 redisSortObject 结构数组中,将指针移动到数组的索引 2 上,依次访问 array[2]、array[3]、array[4] 这 3 个数组项,并将数组项的 obj 指针所指向的元素返回给客户端 -当要索引的列字符很多时,索引会变大变慢,可以只索引列开始的部分字符串,节约索引空间,提高索引效率 -注意:使用前缀索引就系统就忽略覆盖索引对查询性能的优化了 -优化原则:**降低重复的索引值** -比如地区表: -```mysql -area gdp code -chinaShanghai 100 aaa -chinaDalian 200 bbb -usaNewYork 300 ccc -chinaFuxin 400 ddd -chinaBeijing 500 eee +*** + + + +#### GET + +SORT 命令默认在对键进行排序后,返回被排序键本身所包含的元素,通过使用 GET 选项, 可以在对键进行排序后,根据被排序的元素以及 GET 选项所指定的模式,查找并返回某些键的值 + +```sh +SORT GET ``` -发现 area 字段很多都是以 china 开头的,那么如果以前 1-5 位字符做前缀索引就会出现大量索引值重复的情况,索引值重复性越低,查询效率也就越高,所以需要建立前 6 位字符的索引: +```sh +redis> SADD students "tom" "jack" "sea" +#设置全名 +redis> SET tom-name "Tom Li" +OK +redis> SET jack-name "Jack Wang" +OK +redis> SET sea-name "Sea Zhang" +OK +``` -```mysql -CREATE INDEX idx_area ON table_name(area(7)); +```sh +redis> SORT students ALPHA GET *-name +1) "Jack Wang" +2) "Sea Zhang" +3) "Tom Li" ``` -场景:存储身份证 +实现原理:对 students 进行排序后,对于 jack 元素和 *-name 模式,查找程序返回键 jack-name,然后获取 jack-name 键对应的值 -* 直接创建完整索引,这样可能比较占用空间 -* 创建前缀索引,节省空间,但会增加查询扫描次数,并且不能使用覆盖索引 -* 倒序存储,再创建前缀索引,用于绕过字符串本身前缀的区分度不够的问题(前 6 位相同的很多) -* 创建 hash 字段索引,查询性能稳定,有额外的存储和计算消耗,跟第三种方式一样,都不支持范围扫描 -**** +*** -#### 索引合并 -使用多个索引来完成一次查询的执行方法叫做索引合并 index merge +#### STORE -* Intersection 索引合并: +SORT 命令默认只向客户端返回排序结果,而不保存排序结果,通过使用 STORE 选项可以将排序结果保存在指定的键里面 - ```sql - SELECT * FROM table_test WHERE key1 = 'a' AND key3 = 'b'; # key1 和 key3 列都是单列索引、二级索引 - ``` +```sh +SORT STORE +``` - 从不同索引中扫描到的记录的 id 值取交集(相同 id),然后执行回表操作,要求从每个二级索引获取到的记录都是按照主键值排序 +```sh +redis> SADD students "tom" "jack" "sea" +(integer) 3 +redis> SORT students ALPHA STORE sorted_students +(integer) 3 +``` -* Union 索引合并: +实现原理:排序后,检查 sorted_students 键是否存在,如果存在就删除该键,设置 sorted_students 为空白的列表键,遍历排序数组将元素依次放入 - ```sql - SELECT * FROM table_test WHERE key1 = 'a' OR key3 = 'b'; - ``` - 从不同索引中扫描到的记录的 id 值取并集,然后执行回表操作,要求从每个二级索引获取到的记录都是按照主键值排序 -* Sort-Union 索引合并 - ```sql - SELECT * FROM table_test WHERE key1 < 'a' OR key3 > 'b'; - ``` - 先将从不同索引中扫描到的记录的主键值进行排序,再按照 Union 索引合并的方式进行查询 +*** + +#### 执行顺序 + +调用 SORT 命令,除了 GET 选项之外,改变其他选项的摆放顺序并不会影响命令执行选项的顺序 + +```sh +SORT ALPHA [ASC/DESC] BY LIMIT GET STORE +``` + +执行顺序: + +* 排序:命令会使用 ALPHA 、ASC 或 DESC、BY 这几个选项,对输入键进行排序,并得到一个排序结果集 +* 限制排序结果集的长度:使用 LIMIT 选项,对排序结果集的长度进行限制 +* 获取外部键:根据排序结果集中的元素以及 GET 选项指定的模式,查找并获取指定键的值,并用这些值来作为新的排序结果集 +* 保存排序结果集:使用 STORE 选项,将排序结果集保存到指定的键上面去 +* 向客户端返回排序结果集:最后一步命令遍历排序结果集,并依次向客户端返回排序结果集中的元素 @@ -4717,109 +9644,105 @@ CREATE INDEX idx_area ON table_name(area(7)); +### 通知机制 +数据库通知是可以让客户端通过订阅给定的频道或者模式,来获知数据库中键的变化,以及数据库中命令的执行情况 -## 系统优化 +* 关注某个键执行了什么命令的通知称为键空间通知(key-space notification) +* 关注某个命令被什么键执行的通知称为键事件通知(key-event notification) -### 优化步骤 +图示订阅 0 号数据库 message 键: -#### 执行频率 + -随着生产数据量的急剧增长,很多 SQL 语句逐渐显露出性能问题,对生产的影响也越来越大,此时有问题的 SQL 语句就成为整个系统性能的瓶颈,因此必须要进行优化 +服务器配置的 notify-keyspace-events 选项决定了服务器所发送通知的类型 -MySQL 客户端连接成功后,查询服务器状态信息: +* AKE 代表服务器发送所有类型的键空间通知和键事件通知 +* AK 代表服务器发送所有类型的键空间通知 +* AE 代表服务器发送所有类型的键事件通知 +* K$ 代表服务器只发送和字符串键有关的键空间通知 +* EL 代表服务器只发送和列表键有关的键事件通知 +* ..... -```mysql -SHOW [SESSION|GLOBAL] STATUS LIKE ''; --- SESSION: 显示当前会话连接的统计结果,默认参数 --- GLOBAL: 显示自数据库上次启动至今的统计结果 -``` +发送数据库通知的功能是由 notifyKeyspaceEvent 函数实现的: -* 查看SQL执行频率: +* 如果给定的通知类型 type 不是服务器允许发送的通知类型,那么函数会直接返回 +* 如果给定的通知是服务器允许发送的通知 + * 检测服务器是否允许发送键空间通知,允许就会构建并发送事件通知 + * 检测服务器是否允许发送键事件通知,允许就会构建并发送事件通知 - ```mysql - SHOW STATUS LIKE 'Com_____'; - ``` - Com_xxx 表示每种语句执行的次数 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-SQL语句执行频率.png) -* 查询 SQL 语句影响的行数: - ```mysql - SHOW STATUS LIKE 'Innodb_rows_%'; - ``` +*** - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-SQL语句影响的行数.png) -Com_xxxx:这些参数对于所有存储引擎的表操作都会进行累计 -Innodb_xxxx:这几个参数只是针对InnoDB 存储引擎的,累加的算法也略有不同 -| 参数 | 含义 | -| :------------------- | ------------------------------------------------------------ | -| Com_select | 执行 SELECT 操作的次数,一次查询只累加 1 | -| Com_insert | 执行 INSERT 操作的次数,对于批量插入的 INSERT 操作,只累加一次 | -| Com_update | 执行 UPDATE 操作的次数 | -| Com_delete | 执行 DELETE 操作的次数 | -| Innodb_rows_read | 执行 SELECT 查询返回的行数 | -| Innodb_rows_inserted | 执行 INSERT 操作插入的行数 | -| Innodb_rows_updated | 执行 UPDATE 操作更新的行数 | -| Innodb_rows_deleted | 执行 DELETE 操作删除的行数 | -| Connections | 试图连接 MySQL 服务器的次数 | -| Uptime | 服务器工作时间 | -| Slow_queries | 慢查询的次数 | +## 体系架构 +### 事件驱动 -**** +#### 基本介绍 +Redis 服务器是一个事件驱动程序,服务器需要处理两类事件 +* 文件事件 (file event):服务器通过套接字与客户端(或其他 Redis 服务器)进行连接,而文件事件就是服务器对套接字操作的抽象。服务器与客户端的通信会产生相应的文件事件,服务器通过监听并处理这些事件完成一系列网络通信操作 +* 时间事件 (time event):Redis 服务器中的一些操作(比如 serverCron 函数)需要在指定时间执行,而时间事件就是服务器对这类定时操作的抽象 -#### 定位低效 -SQL 执行慢有两种情况: -* 偶尔慢:DB 在刷新脏页 - * redo log 写满了 - * 内存不够用,要从 LRU 链表中淘汰 - * MySQL 认为系统空闲的时候 - * MySQL 关闭时 -* 一直慢的原因:索引没有设计好、SQL 语句没写好、MySQL 选错了索引 -通过以下两种方式定位执行效率较低的 SQL 语句 -* 慢日志查询: 慢查询日志在查询结束以后才记录,执行效率出现问题时查询日志并不能定位问题 +*** - 配置文件修改:修改 .cnf 文件 `vim /etc/mysql/my.cnf`,重启 MySQL 服务器 - ```sh - slow_query_log=ON - slow_query_log_file=/usr/local/mysql/var/localhost-slow.log - long_query_time=1 #记录超过long_query_time秒的SQL语句的日志 - log-queries-not-using-indexes = 1 - ``` - 使用命令配置: +#### 文件事件 - ```mysql - mysql> SET slow_query_log=ON; - mysql> SET GLOBAL slow_query_log=ON; - ``` +##### 基本组成 + +Redis 基于 Reactor 模式开发了网络事件处理器,这个处理器被称为文件事件处理器 (file event handler) + +* 使用 I/O 多路复用 (multiplexing) 程序来同时监听多个套接字,并根据套接字执行的任务来为套接字关联不同的事件处理器 + +* 当被监听的套接字准备好执行连接应答 (accept)、 读取 (read)、 写入 (write)、 关闭 (close) 等操作时,与操作相对应的文件事件就会产生,这时文件事件分派器会调用套接字关联好的事件处理器来处理事件 + +文件事件处理器**以单线程方式运行**,但通过使用 I/O 多路复用程序来监听多个套接字, 既实现了高性能的网络通信模型,又可以很好地与 Redis 服务器中其他同样以单线程方式运行的模块进行对接,保持了 Redis 内部单线程设计的简单性 + +文件事件处理器的组成结构: + + + +I/O 多路复用程序将所有产生事件的套接字处理请求放入一个**单线程的执行队列**中,通过队列有序、同步的向文件事件分派器传送套接字,上一个套接字产生的事件处理完后,才会继续向分派器传送下一个 + + + +Redis 单线程也能高效的原因: + +* 纯内存操作 +* 核心是基于非阻塞的 IO 多路复用机制,单线程可以高效处理多个请求 +* 底层使用 C 语言实现,C 语言实现的程序距离操作系统更近,执行速度相对会更快 +* 单线程同时也**避免了多线程的上下文频繁切换问题**,预防了多线程可能产生的竞争问题 + + + +**** - 查看是否配置成功: - ```mysql - SHOW VARIABLES LIKE '%query%' - ``` -* SHOW PROCESSLIST:**实时查看**当前 MySQL 在进行的连接线程,包括线程的状态、是否锁表、SQL 的执行情况,同时对一些锁表操作进行优化 +##### 多路复用 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-SHOW PROCESSLIST命令.png) +Redis 的 I/O 多路复用程序的所有功能都是通过包装常见的 select 、epoll、 evport 和 kqueue 这些函数库来实现的,Redis 在 I/O 多路复用程序的实现源码中用 #include 宏定义了相应的规则,编译时自动选择系统中**性能最高的多路复用函数**来作为底层实现 +I/O 多路复用程序监听多个套接字的 AE_READABLE 事件和 AE_WRITABLE 事件,这两类事件和套接字操作之间的对应关系如下: +* 当套接字变得**可读**时(客户端对套接字执行 write 操作或者 close 操作),或者有新的**可应答**(acceptable)套接字出现时(客户端对服务器的监听套接字执行 connect 连接操作),套接字产生 AE_READABLE 事件 +* 当套接字变得可写时(客户端对套接字执行 read 操作,对于服务器来说就是可以写了),套接字产生 AE_WRITABLE 事件 +I/O 多路复用程序允许服务器同时监听套接字的 AE_READABLE 和 AE_WRITABLE 事件, 如果一个套接字同时产生了这两种事件,那么文件事件分派器会优先处理 AE_READABLE 事件, 等 AE_READABLE 事件处理完之后才处理 AE_WRITABLE 事件 @@ -4827,224 +9750,237 @@ SQL 执行慢有两种情况: -#### EXPLAIN +##### 处理器 -##### 执行计划 +Redis 为文件事件编写了多个处理器,这些事件处理器分别用于实现不同的网络通信需求: -通过 EXPLAIN 命令获取执行 SQL 语句的信息,包括在 SELECT 语句执行过程中如何连接和连接的顺序,执行计划在优化器优化完成后、执行器之前生成,然后执行器会调用存储引擎检索数据 +* 连接应答处理器,用于对连接服务器的各个客户端进行应答,Redis 服务器初始化时将该处理器与 AE_READABLE 事件关联 +* 命令请求处理器,用于接收客户端传来的命令请求,执行套接字的读入操作,与 AE_READABLE 事件关联 +* 命令回复处理器,用于向客户端返回命令的执行结果,执行套接字的写入操作,与 AE_WRITABLE 事件关联 +* 复制处理器,当主服务器和从服务器进行复制操作时,主从服务器都需要关联该处理器 -查询 SQL 语句的执行计划: +Redis 客户端与服务器进行连接并发送命令的整个过程: -```mysql -EXPLAIN SELECT * FROM table_1 WHERE id = 1; -``` +* Redis 服务器正在运作监听套接字的 AE_READABLE 事件,关联连接应答处理器 +* 当 Redis 客户端向服务器发起连接,监听套接字将产生 AE_READABLE 事件,触发连接应答处理器执行,对客户端的连接请求进行应答,创建客户端套接字以及客户端状态,并将客户端套接字的 **AE_READABLE 事件与命令请求处理器**进行关联 +* 客户端向服务器发送命令请求,客户端套接字产生 AE_READABLE 事件,引发命令请求处理器执行,读取客户端的命令内容传给相关程序去执行 +* 执行命令会产生相应的命令回复,为了将这些命令回复传送回客户端,服务器会将客户端套接字的 **AE_WRITABLE 事件与命令回复处理器**进行关联 +* 当客户端尝试读取命令回复时,客户端套接字产生 AE_WRITABLE 事件,触发命令回复处理器执行,在命令回复全部写入套接字后,服务器就会解除客户端套接字的 AE_WRITABLE 事件与命令回复处理器之间的关联 -![](https://gitee.com/seazean/images/raw/master/DB/MySQL-explain查询SQL语句的执行计划.png) -| 字段 | 含义 | -| ------------- | ------------------------------------------------------------ | -| id | select查询的序列号,表示查询中执行select子句或操作表的顺序 | -| select_type | 表示 SELECT 的类型 | -| table | 输出结果集的表,显示这一步所访问数据库中表名称,有时不是真实的表名字,可能是简称 | -| type | 表示表的连接类型 | -| possible_keys | 表示查询时,可能使用的索引 | -| key | 表示实际使用的索引 | -| key_len | 索引字段的长度 | -| ref | 列与索引的比较,表示表的连接匹配条件,即哪些列或常量被用于查找索引列上的值 | -| rows | 扫描出的行数,表示 MySQL 根据表统计信息及索引选用情况,**估算**的找到所需的记录扫描的行数 | -| filtered | 按表条件过滤的行百分比 | -| extra | 执行情况的说明和描述 | -MySQL 执行计划的局限: -* 只是计划,不是执行 SQL 语句,可以随着底层优化器输入的更改而更改 -* EXPLAIN 不会告诉显示关于触发器、存储过程的信息对查询的影响情况 -* EXPLAIN 不考虑各种 Cache -* EXPLAIN 不能显示 MySQL 在执行查询时的动态,因为执行计划在执行查询之前生成 -* EXPALIN 部分统计信息是估算的,并非精确值 -* EXPALIN 只能解释 SELECT 操作,其他操作要重写为 SELECT 后查看执行计划 -* EXPLAIN PLAN 显示的是在解释语句时数据库将如何运行 SQL 语句,由于执行环境和 EXPLAIN PLAN 环境的不同,此计划可能与 SQL 语句实际的执行计划不同 -环境准备: +*** -![](https://gitee.com/seazean/images/raw/master/DB/MySQL-执行计划环境准备.png) +#### 时间事件 +Redis 的时间事件分为以下两类: +* 定时事件:在指定的时间之后执行一次(Redis 中暂时未使用) +* 周期事件:每隔指定时间就执行一次 -*** +一个时间事件主要由以下三个属性组成: +* id:服务器为时间事件创建的全局唯一 ID(标识号),从小到大顺序递增,新事件的 ID 比旧事件的 ID 号要大 +* when:毫秒精度的 UNIX 时间戳,记录了时间事件的到达(arrive)时间 +* timeProc:时间事件处理器,当时间事件到达时,服务器就会调用相应的处理器来处理事件 +时间事件是定时事件还是周期性事件取决于时间事件处理器的返回值: -##### id +* 定时事件:事件处理器返回 AE_NOMORE,该事件在到达一次后就会被删除 +* 周期事件:事件处理器返回非 AE_NOMORE 的整数值,服务器根据该值对事件的 when 属性更新,让该事件在一段时间后再次交付 -SQL 执行的顺序的标识,SQL 从大到小的执行 +服务器将所有时间事件都放在一个**无序链表**中,新的时间事件插入到链表的表头: -* id 相同时,执行顺序由上至下 + - ```mysql - EXPLAIN SELECT * FROM t_role r, t_user u, user_role ur WHERE r.id = ur.role_id AND u.id = ur.user_id ; - ``` +无序链表指是链表不按 when 属性的大小排序,每当时间事件执行器运行时就必须遍历整个链表,查找所有已到达的时间事件,并调用相应的事件处理器处理 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-explain之id相同.png) +无序链表并不影响时间事件处理器的性能,因为正常模式下的 Redis 服务器**只使用 serverCron 一个时间事件**,在 benchmark 模式下服务器也只使用两个时间事件,所以无序链表不会影响服务器的性能,几乎可以按照一个指针处理 -* id 不同时,id 值越大优先级越高,越先被执行 - ```mysql - EXPLAIN SELECT * FROM t_role WHERE id = (SELECT role_id FROM user_role WHERE user_id = (SELECT id FROM t_user WHERE username = 'stu1')) - ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-explain之id不同.png) +*** -* id 有相同也有不同时,id 相同的可以认为是一组,从上往下顺序执行;在所有的组中,id 的值越大的组,优先级越高,越先执行 - ```mysql - EXPLAIN SELECT * FROM t_role r , (SELECT * FROM user_role ur WHERE ur.`user_id` = '2') a WHERE r.id = a.role_id ; - ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-explain之id相同和不同.png) +#### 事件调度 +服务器中同时存在文件事件和时间事件两种事件类型,调度伪代码: +```python +# 事件调度伪代码 +def aeProcessEvents(): + # 获取到达时间离当前时间最接近的时间事件 + time_event = aeSearchNearestTime() + + # 计算最接近的时间事件距离到达还有多少亳秒 + remaind_ms = time_event.when - unix_ts_now() + # 如果事件已到达,那么 remaind_ms 的值可能为负数,设置为 0 + if remaind_ms < 0: + remaind_ms = 0 + + # 根据 remaind_ms 的值,创建 timeval 结构 + timeval = create_timeval_with_ms(remaind_ms) + # 【阻塞并等待文件事件】产生,最大阻塞时间由传入的timeval结构决定,remaind_ms的值为0时调用后马上返回,不阻塞 + aeApiPoll(timeval) + + # 处理所有已产生的文件事件 + processFileEvents() + # 处理所有已到达的时间事件 + processTimeEvents() +``` -*** +事件的调度和执行规则: +* aeApiPoll 函数的最大阻塞时间由到达时间最接近当前时间的时间事件决定,可以避免服务器对时间事件进行频繁的轮询(忙等待),也可以确保 aeApiPoll 函数不会阻塞过长时间 +* 对文件事件和时间事件的处理都是**同步、有序、原子地执行**,服务器不会中途中断事件处理,也不会对事件进行抢占,所以两种处理器都要尽可地减少程序的阻塞时间,并在有需要时**主动让出执行权**,从而降低事件饥饿的可能性 + * 命令回复处理器在写入字节数超过了某个预设常量,就会主动用 break 跳出写入循环,将余下的数据留到下次再写 + * 时间事件也会将非常耗时的持久化操作放到子线程或者子进程执行 +* 时间事件在文件事件之后执行,并且事件之间不会出现抢占,所以时间事件的实际处理时间通常会比设定的到达时间稍晚 -##### select -表示查询中每个 select 子句的类型(简单 OR 复杂) -| select_type | 含义 | -| ------------------ | ------------------------------------------------------------ | -| SIMPLE | 简单的 SELECT 查询,查询中不包含子查询或者 UNION | -| PRIMARY | 查询中若包含任何复杂的子查询,最外层查询标记为该标识 | -| SUBQUERY | 在 SELECT 或 WHERE 中包含子查询,该子查询被标记为:SUBQUERY | -| DEPENDENT SUBQUERY | 在 SUBQUERY 基础上,子查询中的第一个SELECT,取决于外部的查询 | -| DERIVED | 在 FROM 列表中包含的子查询,被标记为 DERIVED(衍生),MYSQL会递归执行这些子查询,把结果放在临时表中 | -| UNION | UNION 中的第二个或后面的 SELECT 语句,则标记为UNION ; 若 UNION 包含在 FROM 子句的子查询中,外层 SELECT 将被标记为:DERIVED | -| DEPENDENT UNION | UNION 中的第二个或后面的SELECT语句,取决于外面的查询 | -| UNION RESULT | UNION 的结果,UNION 语句中第二个 SELECT 开始后面所有 SELECT | +**** -**** +#### 多线程 +Redis6.0 引入多线程主要是为了提高网络 IO 读写性能,因为这是 Redis 的一个性能瓶颈(Redis 的瓶颈主要受限于内存和网络),多线程只是用来**处理网络数据的读写和协议解析**, 执行命令仍然是单线程顺序执行,因此不需要担心线程安全问题。 -##### type +Redis6.0 的多线程默认是禁用的,只使用主线程。如需开启需要修改 redis 配置文件 `redis.conf` : -对表的访问方式,表示 MySQL 在表中找到所需行的方式,又称访问类型 +```sh +io-threads-do-reads yesCopy to clipboardErrorCopied +``` -| type | 含义 | -| ------ | ------------------------------------------------------------ | -| ALL | Full Table Scan,MySQL 将遍历全表以找到匹配的行,全表扫描,如果是 InnoDB 引擎是扫描聚簇索引 | -| index | Full Index Scan,index 与 ALL 区别为 index 类型只遍历索引树 | -| range | 索引范围扫描,常见于 between、<、> 等的查询 | -| ref | 非唯一性索引扫描,返回匹配某个单独值的所有记录,本质上也是一种索引访问 | -| eq_ref | 唯一性索引扫描,对于每个索引键,表中只有一条记录与之匹配,常见于主键或唯一索引扫描 | -| const | 通过主键或者唯一索引来定位一条记录 | -| system | system 是 const 类型的特例,当查询的表只有一行的情况下,使用 system | -| NULL | MySQL 在优化过程中分解语句,执行时甚至不用访问表或索引 | +开启多线程后,还需要设置线程数,否则是不生效的,同样需要修改 redis 配置文件 : -从上到下,性能从差到好,一般来说需要保证查询至少达到 range 级别, 最好达到 ref +```sh +io-threads 4 #官网建议4核的机器建议设置为2或3个线程,8核的建议设置为6个线程 +``` + -*** +参考文章:https://mp.weixin.qq.com/s/dqmiR0ECf4lB6Y2OyK-dyA -##### key -possible_keys: -* 指出 MySQL 能使用哪个索引在表中找到记录,查询涉及到的字段上若存在索引,则该索引将被列出,但不一定被查询使用 -* 如果该列是 NULL,则没有相关的索引 -key: +**** -* 显示MySQL在查询中实际使用的索引,若没有使用索引,显示为 NULL -* 查询中若使用了**覆盖索引**,则该索引可能出现在 key 列表,不出现在 possible_keys -key_len: -* 表示索引中使用的字节数,可通过该列计算查询中使用的索引的长度 -* key_len 显示的值为索引字段的最大可能长度,并非实际使用长度,即 key_len 是根据表定义计算而得,不是通过表内检索出的 -* 在不损失精确性的前提下,长度越短越好 +### 客户端 +#### 基本介绍 +Redis 服务器是典型的一对多程序,一个服务器可以与多个客户端建立网络连接,服务器对每个连接的客户端建立了相应的 redisClient 结构(客户端状态,**在服务器端的存储结构**),保存了客户端当前的状态信息,以及执行相关功能时需要用到的数据结构 -*** +Redis 服务器状态结构的 clients 属性是一个链表,这个链表保存了所有与服务器连接的客户端的状态结构: + +```c +struct redisServer { + // 一个链表,保存了所有客户端状态 + list *clients; + + //... +}; +``` +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-服务器clients链表.png) -##### Extra -其他的额外的执行计划信息,在该列展示: -* Using index:该值表示相应的 SELECT 操作中使用了**覆盖索引**(Covering Index) -* Using index condition:第一种情况是搜索条件中虽然出现了索引列,但是有部分条件无法使用索引,会根据能用索引的条件先搜索一遍再匹配无法使用索引的条件,回表查询数据;第二种是使用了索引下推 -* Using where:表示存储引擎收到记录后进行后过滤(Post-filter),如果查询操作未能使用索引,Using where 的作用是提醒我们 MySQL 将用 where 子句来过滤结果集,即需要回表查询 -* Using temporary:表示 MySQL 需要使用临时表来存储结果集,常见于排序和分组查询 -* Using filesort:对数据使用外部排序算法,将取得的数据在内存中进行排序,这种无法利用索引完成的排序操作称为文件排序 -* Using join buffer:说明在获取连接条件时没有使用索引,并且需要连接缓冲区来存储中间结果 -* Impossible where:说明 where 语句会导致没有符合条件的行,通过收集统计信息不可能存在结果 -* Select tables optimized away:说明仅通过使用索引,优化器可能仅从聚合函数结果中返回一行 -* No tables used:Query 语句中使用 from dual 或不含任何 from 子句 +*** -参考文章:https://www.cnblogs.com/ggjucheng/archive/2012/11/11/2765237.html +#### 数据结构 +##### redisClient +客户端的数据结构: -**** +```c +typedef struct redisClient { + //... + + // 套接字 + int fd; + // 名字 + robj *name; + // 标志 + int flags; + + // 输入缓冲区 + sds querybuf; + // 输出缓冲区 buf 数组 + char buf[REDIS_REPLY_CHUNK_BYTES]; + // 记录了 buf 数组目前已使用的字节数量 + int bufpos; + // 可变大小的输出缓冲区,链表 + 字符串对象 + list *reply; + + // 命令数组 + rboj **argv; + // 命令数组的长度 + int argc; + // 命令的信息 + struct redisCommand *cmd; + + // 是否通过身份验证 + int authenticated; + + // 创建客户端的时间 + time_t ctime; + // 客户端与服务器最后一次进行交互的时间 + time_t lastinteraction; + // 输出缓冲区第一次到达软性限制 (soft limit) 的时间 + time_t obuf_soft_limit_reached_time; +} +``` +客户端状态包括两类属性 +* 一类是比较通用的属性,这些属性很少与特定功能相关,无论客户端执行的是什么工作,都要用到这些属性 +* 另一类是和特定功能相关的属性,比如操作数据库时用到的 db 属性和 dict id 属性,执行事务时用到的 mstate 属性,以及执行 WATCH 命令时用到的 watched_keys 属性等,代码中没有列出 -#### PROFILES -SHOW PROFILES 能够在做 SQL 优化时分析当前会话中语句执行的**资源消耗**情况 -* 通过 have_profiling 参数,能够看到当前 MySQL 是否支持 profile: - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-have_profiling.png) +*** -* 默认 profiling 是关闭的,可以通过 set 语句在 Session 级别开启 profiling: - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-profiling.png) - ```mysql - SET profiling=1; #开启profiling 开关; - ``` +##### 套接字 -* 执行 SHOW PROFILES 指令, 来查看 SQL 语句执行的耗时: +客户端状态的 fd 属性记录了客户端正在使用的套接字描述符,根据客户端类型的不同,fd 属性的值可以是 -1 或者大于 -1 的整数: - ```mysql - SHOW PROFILES; - ``` +* 伪客户端 (fake client) 的 fd 属性的值为 -1,命令请求来源于 AOF 文件或者 Lua 脚本,而不是网络,所以不需要套接字连接 +* 普通客户端的 fd 属性的值为大于 -1 的整数,因为合法的套接字描述符不能是 -1 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-查看SQL语句执行耗时.png) +执行 `CLIENT list` 命令可以列出目前所有连接到服务器的普通客户端,不包括伪客户端 -* 查看到该 SQL 执行过程中每个线程的状态和消耗的时间: - ```mysql - SHOW PROFILE FOR QUERY query_id; - ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-SQL执行每个状态消耗的时间.png) +*** - **Sending data 状态**表示 MySQL 线程开始访问数据行并把结果返回给客户端,而不仅仅是返回给客户端。由于在 Sending data 状态下,MySQL 线程需要做大量磁盘读取操作,所以是整个查询中耗时最长的状态。 -* 在获取到最消耗时间的线程状态后,MySQL 支持选择 all、cpu、block io 、context switch、page faults 等类型查看 MySQL 在使用什么资源上耗费了过高的时间。例如,选择查看 CPU 的耗费时间: - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-SQL执行每个状态消耗的CPU.png) +##### 名字 - * Status:SQL 语句执行的状态 - * Durationsql:执行过程中每一个步骤的耗时 - * CPU_user:当前用户占有的 CPU - * CPU_system:系统占有的 CPU +在默认情况下,一个连接到服务器的客户端是没有名字的,使用 `CLIENT setname` 命令可以为客户端设置一个名字 @@ -5052,28 +9988,27 @@ SHOW PROFILES 能够在做 SQL 优化时分析当前会话中语句执行的** -#### trace +##### 标志 -MySQL 提供了对 SQL 的跟踪, 通过 trace 文件能够进一步了解执行过程。 +客户端的标志属性 flags 记录了客户端的角色以及客户端目前所处的状态,每个标志使用一个常量表示 -* 打开 trace,设置格式为 JSON,并设置 trace 最大能够使用的内存大小,避免解析过程中因为默认内存过小而不能够完整展示 +* flags 的值可以是单个标志:`flags = ` +* flags 的值可以是多个标志的二进制:`flags = | | ... ` - ```mysql - SET optimizer_trace="enabled=on",end_markers_in_json=ON; -- 会话内有效 - SET optimizer_trace_max_mem_size=1000000; - ``` +一部分标志记录**客户端的角色**: -* 执行 SQL 语句: - - ```mysql - SELECT * FROM tb_item WHERE id < 4; - ``` +* REDIS_MASTER 表示客户端是一个从服务器,REDIS_SLAVE 表示客户端是一个从服务器,在主从复制时使用 +* REDIS_PRE_PSYNC 表示客户端是一个版本低于 Redis2.8 的从服务器,主服务器不能使用 PSYNC 命令与该从服务器进行同步,这个标志只能在 REDIS_ SLAVE 标志处于打开状态时使用 +* REDIS_LUA_CLIENT 表示客户端是专门用于处理 Lua 脚本里面包含的 Redis 命令的伪客户端 -* 检查 information_schema.optimizer_trace: +一部分标志记录目前**客户端所处的状态**: - ```mysql - SELECT * FROM information_schema.optimizer_trace \G; -- \G代表竖列展示 - ``` +* REDIS_UNIX_SOCKET 表示服务器使用 UNIX 套接字来连接客户端 +* REDIS_BLOCKED 表示客户端正在被 BRPOP、BLPOP 等命令阻塞 +* REDIS_UNBLOCKED 表示客户端已经从 REDIS_BLOCKED 所表示的阻塞状态脱离,在 REDIS_BLOCKED 标志打开的情况下使用 +* REDIS_MULTI 标志表示客户端正在执行事务 +* REDIS_DIRTY_CAS 表示事务使用 WATCH 命令监视的数据库键已经被修改 +* ..... @@ -5083,407 +10018,407 @@ MySQL 提供了对 SQL 的跟踪, 通过 trace 文件能够进一步了解执 -### 索引失效 - -#### 创建索引 +##### 缓冲区 -索引是数据库优化最重要的手段之一,通过索引通常可以帮助用户解决大多数的 MySQL 的性能优化问题 +客户端状态的输入缓冲区用于保存客户端发送的命令请求,输入缓冲区的大小会根据输入内容动态地缩小或者扩大,但最大大小不能超过 1GB,否则服务器将关闭这个客户端,比如执行 `SET key value `,那么缓冲区 querybuf 的内容: -```mysql -CREATE TABLE `tb_seller` ( - `sellerid` varchar (100), - `name` varchar (100), - `nickname` varchar (50), - `password` varchar (60), - `status` varchar (1), - `address` varchar (100), - `createtime` datetime, - PRIMARY KEY(`sellerid`) -)ENGINE=INNODB DEFAULT CHARSET=utf8mb4; -INSERT INTO `tb_seller` (`sellerid`, `name`, `nickname`, `password`, `status`, `address`, `createtime`) values('xiaomi','小米科技','小米官方旗舰店','e10adc3949ba59abbe56e057f20f883e','1','西安市','2088-01-01 12:00:00'); -CREATE INDEX idx_seller_name_sta_addr ON tb_seller(name, status, address); +```sh +*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue\r\n # ``` -![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引环境准备.png) +输出缓冲区是服务器用于保存执行客户端命令所得的命令回复,每个客户端都有两个输出缓冲区可用: +* 一个是固定大小的缓冲区,保存长度比较小的回复,比如 OK、简短的字符串值、整数值、错误回复等 +* 一个是可变大小的缓冲区,保存那些长度比较大的回复, 比如一个非常长的字符串值或者一个包含了很多元素的集合等 +buf 是一个大小为 REDIS_REPLY_CHUNK_BYTES (常量默认 16*1024 = 16KB) 字节的字节数组,bufpos 属性记录了 buf 数组目前已使用的字节数量,当 buf 数组的空间已经用完或者回复数据太大无法放进 buf 数组里,服务器就会开始使用可变大小的缓冲区 -**** +通过使用 reply 链表连接多个字符串对象,可以为客户端保存一个非常长的命令回复,而不必受到固定大小缓冲区 16KB 大小的限制 +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-可变输出缓冲区.png) -#### 避免失效 -索引失效的情况: -* 全值匹配:对索引中所有列都指定具体值,这种情况索引生效,执行效率高 - ```mysql - EXPLAIN SELECT * FROM tb_seller WHERE name='小米科技' AND status='1' AND address='西安市'; - ``` +*** - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引1.png) -* **最左前缀法则**:联合索引遵守最左前缀法则 - 匹配最左前缀法则,走索引: +##### 命令 - ```mysql - EXPLAIN SELECT * FROM tb_seller WHERE name='小米科技'; - EXPLAIN SELECT * FROM tb_seller WHERE name='小米科技' AND status='1'; - ``` +服务器对 querybuf 中的命令请求的内容进行分析,得出的命令参数以及参数的数量分别保存到客户端状态的 argv 和 argc 属性 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引2.png) +* argv 属性是一个数组,数组中的每项都是字符串对象,其中 argv[0] 是要执行的命令,而之后的其他项则是命令的参数 +* argc 属性负责记录 argv 数组的长度 - 违法最左前缀法则 , 索引失效: + - ```mysql - EXPLAIN SELECT * FROM tb_seller WHERE status='1'; - EXPLAIN SELECT * FROM tb_seller WHERE status='1' AND address='西安市'; - ``` +服务器将根据项 argv[0] 的值,在命令表中查找命令所对应的命令的 redisCommand,将客户端状态的 cmd 指向该结构 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引3.png) +命令表是一个字典结构,键是 SDS 结构保存命令的名字;值是命令所对应的 redisCommand 结构,保存了命令的实现函数、命令标志、 命令应该给定的参数个数、命令的总执行次数和总消耗时长等统计信息 - 如果符合最左法则,但是出现跳跃某一列,只有最左列索引生效: + - ```mysql - EXPLAIN SELECT * FROM tb_seller WHERE name='小米科技' AND address='西安市'; - ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引4.png) - 虽然索引列失效,但是系统**使用了索引下推进行了优化** +**** -* **范围查询**右边的列,不能使用索引: - ```mysql - EXPLAIN SELECT * FROM tb_seller WHERE name='小米科技' AND status>'1' AND address='西安市'; - ``` - 根据前面的两个字段 name , status 查询是走索引的, 但是最后一个条件 address 没有用到索引,使用了索引下推 +##### 验证 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引5.png) +客户端状态的 authenticated 属性用于记录客户端是否通过了身份验证 -* 在索引列上进行**运算操作**, 索引将失效: +* authenticated 值为 0,表示客户端未通过身份验证 +* authenticated 值为 1,表示客户端已通过身份验证 - ```mysql - EXPLAIN SELECT * FROM tb_seller WHERE SUBSTRING(name,3,2) = '科技'; - ``` +当客户端 authenticated = 0 时,除了 AUTH 命令之外, 客户端发送的所有其他命令都会被服务器拒绝执行 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引6.png) +```sh +redis> PING +(error) NOAUTH Authentication required. +redis> AUTH 123321 +OK +redis> PING +PONG +``` -* **字符串不加单引号**,造成索引失效: - 在查询时,没有对字符串加单引号,MySQL 的查询优化器,会自动的进行类型转换,造成索引失效 - ```mysql - EXPLAIN SELECT * FROM tb_seller WHERE name='小米科技' AND status=1; - ``` +*** - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引7.png) -* **用 OR 分割条件,索引失效**,导致全表查询: - OR 前的条件中的列有索引而后面的列中没有索引或 OR 前后两个列是同一个复合索引,都造成索引失效 +##### 时间 - ```mysql - EXPLAIN SELECT * FROM tb_seller WHERE name='阿里巴巴' OR createtime = '2088-01-01 12:00:00'; - EXPLAIN SELECT * FROM tb_seller WHERE name='小米科技' OR status='1'; - ``` +ctime 属性记录了创建客户端的时间,这个时间可以用来计算客户端与服务器已经连接了多少秒,`CLIENT list` 命令的 age 域记录了这个秒数 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引10.png) +lastinteraction 属性记录了客户端与服务器最后一次进行互动 (interaction) 的时间,互动可以是客户端向服务器发送命令请求,也可以是服务器向客户端发送命令回复。该属性可以用来计算客户端的空转 (idle) 时长, 就是距离客户端与服务器最后一次进行互动已经过去了多少秒,`CLIENT list` 命令的 idle 域记录了这个秒数 - **AND 分割的条件不影响**: +obuf_soft_limit_reached_time 属性记录了**输出缓冲区第一次到达软性限制** (soft limit) 的时间 - ```mysql - EXPLAIN SELECT * FROM tb_seller WHERE name='阿里巴巴' AND createtime = '2088-01-01 12:00:00'; - ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引11.png) -* **以 % 开头的 LIKE 模糊查询**,索引失效: - 如果是尾部模糊匹配,索引不会失效;如果是头部模糊匹配,索引失效。 - ```mysql - EXPLAIN SELECT * FROM tb_seller WHERE name like '%科技%'; - ``` +*** - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引12.png) - 解决方案:通过覆盖索引来解决 - ```mysql - EXPLAIN SELECT sellerid,name,status FROM tb_seller WHERE name like '%科技%'; - ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引13.png) - 原因:在覆盖索引的这棵 B+ 数上只需要进行 like 的匹配,或者是基于覆盖索引查询再进行 WHERE 的判断就可以获得结果 +#### 生命周期 -系统优化为全表扫描: +##### 创建 -* 如果 MySQL 评估使用索引比全表更慢,则不使用索引,索引失效: +服务器使用不同的方式来创建和关闭不同类型的客户端 - ```mysql - CREATE INDEX idx_address ON tb_seller(address); - EXPLAIN SELECT * FROM tb_seller WHERE address='西安市'; - EXPLAIN SELECT * FROM tb_seller WHERE address='北京市'; - ``` +如果客户端是通过网络连接与服务器进行连接的普通客户端,那么在客户端使用 connect 函数连接到服务器时,服务器就会调用连接应答处理器为客户端创建相应的客户端状态,并将这个新的客户端状态添加到服务器状态结构 clients 链表的末尾 - 北京市的键值占 9/10(区分度低),所以优化为全表扫描,type = ALL +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-服务器clients链表.png) - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引14.png) +服务器会在初始化时创建负责执行 Lua 脚本中包含的 Redis 命令的伪客户端,并将伪客户端关联在服务器状态的 lua_client 属性 -* IS NULL、IS NOT NULL **有时**索引失效: +```c +struct redisServer { + // 保存伪客户端 + redisClient *lua_client; + + //... +}; +``` - ```mysql - EXPLAIN SELECT * FROM tb_seller WHERE name IS NULL; - EXPLAIN SELECT * FROM tb_seller WHERE name IS NOT NULL; - ``` +lua_client 伪客户端在服务器运行的整个生命周期会一直存在,只有服务器被关闭时,这个客户端才会被关闭 - NOT NULL 失效的原因是 name 列全部不是 null,优化为全表扫描,当 NULL 过多时,IS NULL 失效 +载入 AOF 文件时, 服务器会创建用于执行 AOF 文件包含的 Redis 命令的伪客户端,并在载入完成之后,关闭这个伪客户端 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引15.png) -* IN 肯定会走索引,但是当 IN 的取值范围较大时会导致索引失效,走全表扫描: - ```mysql - EXPLAIN SELECT * FROM tb_seller WHERE sellerId IN ('alibaba','huawei');-- 都走索引 - EXPLAIN SELECT * FROM tb_seller WHERE sellerId NOT IN ('alibaba','huawei'); - ``` +**** -*** +##### 关闭 +一个普通客户端可以因为多种原因而被关闭: +* 客户端进程退出或者被杀死,那么客户端与服务器之间的网络连接将被关闭,从而造成客户端被关闭 +* 客户端向服务器发送了带有不符合协议格式的命令请求,那么这个客户端会**被服务器关闭** +* 客户端是 `CLIENT KILL` 命令的目标 +* 如果用户为服务器设置了 timeout 配置选项,那么当客户端的空转时间超过该值时将被关闭,特殊情况不会被关闭: + * 客户端是主服务器(REDIS_MASTER )或者从服务器(打开了 REDIS_SLAVE 标志) + * 正在被 BLPOP 等命令阻塞(REDIS_BLOCKED) + * 正在执行 SUBSCRIBE、PSUBSCRIBE 等订阅命令 +* 客户端发送的命令请求的大小超过了输入缓冲区的限制大小(默认为 1GB) +* 发送给客户端的命令回复的大小超过了输出缓冲区的限制大小 -#### 底层原理 +理论上来说,可变缓冲区可以保存任意长的命令回复,但是为了回复过大占用过多的服务器资源,服务器会时刻检查客户端的输出缓冲区的大小,并在缓冲区的大小超出范围时,执行相应的限制操作: -索引失效一般是针对联合索引,联合索引一般由几个字段组成,排序方式是先按照第一个字段进行排序,然后排序第二个,依此类推,图示(a, b)索引,**a 相等的情况下 b 是有序的** +* 硬性限制 (hard limit):输出缓冲区的大小超过了硬性限制所设置的大小,那么服务器会关闭客户端(serverCron 函数中执行),积存在输出缓冲区中的所有内容会被**直接释放**,不会返回给客户端 +* 软性限制 (soft limit):输出缓冲区的大小超过了软性限制所设置的大小,小于硬性限制的大小,服务器的操作: + * 用属性 obuf_soft_limit_reached_time 记录下客户端到达软性限制的起始时间,继续监视客户端 + * 如果输出缓冲区的大小一直超出软性限制,并且持续时间超过服务器设定的时长,那么服务器将关闭客户端 + * 如果在指定时间内不再超出软性限制,那么客户端就不会被关闭,并且 o_s_l_r_t 属性清零 - +使用 client-output-buffer-limit 选项可以为普通客户端、从服务器客户端、执行发布与订阅功能的客户端分别设置不同的软性限制和硬性限制,格式: -* 最左前缀法则:当不匹配前面的字段的时候,后面的字段都是无序的。这种无序不仅体现在叶子节点,也会**导致查询时扫描的非叶子节点也是无序的**,因为索引树相当于忽略的第一个字段,就无法使用二分查找 +```sh +client-output-buffer-limit -* 范围查询右边的列,不能使用索引,比如语句: `WHERE a > 1 AND b = 1 `,在 a 大于 1 的时候,b 是无序的,a > 1 是扫描时有序的,但是找到以后进行寻找 b 时,索引树就不是有序的了 +client-output-buffer-limit normal 0 0 0 +client-output-buffer-limit slave 256mb 64mb 60 +client-output-buffer-limit pubsub 32mb 8mb 60 +``` - +* 第一行:将普通客户端的硬性限制和软性限制都设置为 0,表示不限制客户端的输出缓冲区大小 +* 第二行:将从服务器客户端的硬性限制设置为 256MB,软性限制设置为 64MB,软性限制的时长为 60 秒 +* 第三行:将执行发布与订阅功能的客户端的硬性限制设置为 32MB,软性限制设置为 8MB,软性限制的时长为 60 秒 -* 以 % 开头的 LIKE 模糊查询,索引失效,比如语句:`WHERE a LIKE '%d'`,前面的不确定,导致不符合最左匹配,直接去索引中搜索以 d 结尾的节点,所以没有顺序 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-索引失效底层原理3.png) -*** +**** -#### 查看索引 -```mysql -SHOW STATUS LIKE 'Handler_read%'; -SHOW GLOBAL STATUS LIKE 'Handler_read%'; +### 服务器 + +#### 执行流程 + +Redis 服务器与多个客户端建立网络连接,处理客户端发送的命令请求,在数据库中保存客户端执行命令所产生的数据,并通过资源管理来维持服务器自身的运转,所以一个命令请求从发送到获得回复的过程中,客户端和服务器需要完成一系列操作 + + + +##### 命令请求 + +Redis 服务器的命令请求来自 Redis 客户端,当用户在客户端中键入一个命令请求时,客户端会将这个命令请求转换成协议格式,通过连接到服务器的套接字,将协议格式的命令请求发送给服务器 + +```sh +SET KEY VALUE -> # 命令 +*3\r\nS3\r\nSET\r\n$3\r\nKEY\r\n$5\r\nVALUE\r\n # 协议格式 ``` -![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL查看索引使用情况.png) +当客户端与服务器之间的连接套接字因为客户端的写入而变得可读,服务器调用**命令请求处理器**来执行以下操作: -* Handler_read_first:索引中第一条被读的次数,如果较高,表示服务器正执行大量全索引扫描(这个值越低越好) +* 读取套接字中协议格式的命令请求,并将其保存到客户端状态的输入缓冲区里面 +* 对输入缓冲区中的命令请求进行分析,提取出命令请求中包含的命令参数以及命令参数的个数,然后分别将参数和参数个数保存到客户端状态的 argv 属性和 argc 属性里 +* 调用命令执行器,执行客户端指定的命令 -* Handler_read_key:如果索引正在工作,这个值代表一个行被索引值读的次数,值越低表示索引不经常使用(这个值越高越好) +最后客户端接收到协议格式的命令回复之后,会将这些回复转换成用户可读的格式打印给用户观看,至此整体流程结束 -* Handler_read_next:按照键顺序读下一行的请求数,如果范围约束或执行索引扫描来查询索引列,值增加 -* Handler_read_prev:按照键顺序读前一行的请求数,该读方法主要用于优化 ORDER BY ... DESC -* Handler_read_rnd:根据固定位置读一行的请求数,如果执行大量查询并对结果进行排序则该值较高,可能是使用了大量需要 MySQL 扫描整个表的查询或连接,这个值较高意味着运行效率低,应该建立索引来解决 +**** + -* Handler_read_rnd_next:在数据文件中读下一行的请求数,如果正进行大量的表扫描,该值较高,说明表索引不正确或写入的查询没有利用索引 +##### 命令执行 +命令执行器开始对命令操作: -*** +* 查找命令:首先根据客户端状态的 argv[0] 参数,在**命令表 (command table)** 中查找参数所指定的命令,并将找到的命令保存到客户端状态的 cmd 属性里面,是一个 redisCommand 结构 + 命令查找算法与字母的大小写无关,所以命令名字的大小写不影响命令表的查找结果 +* 执行预备操作: -### SQL优化 + * 检查客户端状态的 cmd 指针是否指向 NULL,根据 redisCommand 检查请求参数的数量是否正确 + * 检查客户端是否通过身份验证 + * 如果服务器打开了 maxmemory 功能,执行命令之前要先检查服务器的内存占用,在有需要时进行内存回收(**逐出算法**) + * 如果服务器上一次执行 BGSAVE 命令出错,并且服务器打开了 stop-writes-on-bgsave-error 功能,那么如果本次执行的是写命令,服务会拒绝执行,并返回错误 + * 如果客户端当前正在用 SUBSCRIBE 或 PSUBSCRIBE 命令订阅频道,那么服务器会拒绝除了 SUBSCRIBE、SUBSCRIBE、 UNSUBSCRIBE、PUNSUBSCRIBE 之外的其他命令 + * 如果服务器正在进行载入数据,只有 sflags 带有 1 标识(比如 INFO、SHUTDOWN、PUBLISH等)的命令才会被执行 + * 如果服务器执行 Lua 脚本而超时并进入阻塞状态,那么只会执行客户端发来的 SHUTDOWN nosave 和 SCRIPT KILL 命令 + * 如果客户端正在执行事务,那么服务器只会执行客户端发来的 EXEC、DISCARD、MULTI、WATCH 四个命令,其他命令都会被**放进事务队列**中 + * 如果服务器打开了监视器功能,那么会将要执行的命令和参数等信息发送给监视器 -#### 覆盖索引 +* 调用命令的实现函数:被调用的函数会执行指定的操作并产生相应的命令回复,回复会被保存在客户端状态的输出缓冲区里面(buf 和 reply 属性),然后实现函数还会**为客户端的套接字关联命令回复处理器**,这个处理器负责将命令回复返回给客户端 -复合索引叶子节点不仅保存了复合索引的值,还有主键索引,所以使用覆盖索引的时候,加上主键也会用到索引 +* 执行后续工作: -尽量使用覆盖索引,避免 SELECT *: + * 如果服务器开启了慢查询日志功能,那么慢查询日志模块会检查是否需要为刚刚执行完的命令请求添加一条新的慢查询日志 + * 根据执行命令所耗费的时长,更新命令的 redisCommand 结构的 milliseconds 属性,并将命令 calls 计数器的值增一 + * 如果服务器开启了 AOF 持久化功能,那么 AOF 持久化模块会将执行的命令请求写入到 AOF 缓冲区里面 + * 如果有其他从服务器正在复制当前这个服务器,那么服务器会将执行的命令传播给所有从服务器 -```mysql -EXPLAIN SELECT name,status,address FROM tb_seller WHERE name='小米科技' AND status='1' AND address='西安市'; -``` +* 将命令回复发送给客户端:客户端**套接字变为可写状态**时,服务器就会执行命令回复处理器,将客户端输出缓冲区中的命令回复发送给客户端,发送完毕之后回复处理器会清空客户端状态的输出缓冲区,为处理下一个命令请求做好准备 -![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引8.png) -如果查询列,超出索引列,也会降低性能: -```mysql -EXPLAIN SELECT name,status,address,password FROM tb_seller WHERE name='小米科技' AND status='1' AND address='西安市'; -``` +**** -![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引9.png) +##### Command -**** +每个 redisCommand 结构记录了一个Redis 命令的实现信息,主要属性 +```c +struct redisCommand { + // 命令的名字,比如"set" + char *name; + + // 函数指针,指向命令的实现函数,比如setCommand + // redisCommandProc 类型的定义为 typedef void redisCommandProc(redisClient *c) + redisCommandProc *proc; + + // 命令参数的个数,用于检查命令请求的格式是否正确。如果这个值为负数-N, 那么表示参数的数量大于等于N。 + // 注意命令的名字本身也是一个参数,比如 SET msg "hello",命令的参数是"SET"、"msg"、"hello" 三个 + int arity; + + // 字符串形式的标识值,这个值记录了命令的属性,, + // 比如这个命令是写命令还是读命令,这个命令是否允许在载入数据时使用,是否允许在Lua脚本中使用等等 + char *sflags; + + // 对sflags标识进行分析得出的二进制标识,由程序自动生成。服务器对命令标识进行检查时使用的都是 flags 属性 + // 而不是sflags属性,因为对二进制标识的检查可以方便地通过& ^ ~ 等操作来完成 + int flags; + + // 服务器总共执行了多少次这个命令 + long long calls; + + // 服务器执行这个命令所耗费的总时长 + long long milliseconds; +}; +``` -#### 减少访问 -避免对数据进行重复检索:能够一次连接就获取到结果的,就不用两次连接,这样可以大大减少对数据库无用的重复请求 -* 查询数据: - ```mysql - SELECT id,name FROM tb_book; - SELECT id,status FROM tb_book; -- 向数据库提交两次请求,数据库就要做两次查询操作 - -- > 优化为: - SELECT id,name,statu FROM tb_book; - ``` +**** -* 插入数据: - ```mysql - INSERT INTO tb_test VALUES(1,'Tom'); - INSERT INTO tb_test VALUES(2,'Cat'); - INSERT INTO tb_test VALUES(3,'Jerry'); -- 连接三次数据库 - -- >优化为 - INSERT INTO tb_test VALUES(1,'Tom'),(2,'Cat'),(3,'Jerry'); -- 连接一次 - ``` - -* 在事务中进行数据插入: - ```mysql - start transaction; - INSERT INTO tb_test VALUES(1,'Tom'); - INSERT INTO tb_test VALUES(2,'Cat'); - INSERT INTO tb_test VALUES(3,'Jerry'); - commit; -- 手动提交,分段提交 - ``` +#### serverCron -* 数据有序插入: +##### 基本介绍 - ```mysql - INSERT INTO tb_test VALUES(1,'Tom'); - INSERT INTO tb_test VALUES(2,'Cat'); - INSERT INTO tb_test VALUES(3,'Jerry'); - ``` +Redis 服务器以周期性事件的方式来运行 serverCron 函数,服务器初始化时读取配置 server.hz 的值,默认为 10,代表每秒钟执行 10 次,即**每隔 100 毫秒执行一次**,执行指令 info server 可以查看 -增加 cache 层:在应用中增加缓存层来达到减轻数据库负担的目的。可以部分数据从数据库中抽取出来放到应用端以文本方式存储,或者使用框架(Mybatis)提供的一级缓存 / 二级缓存,或者使用 Redis 数据库来缓存数据 +serverCron 函数负责定期对自身的资源和状态进行检查和调整,从而确保服务器可以长期、稳定地运行 +* 更新服务器的各类统计信息,比如时间、内存占用、 数据库占用情况等 +* 清理数据库中的过期键值对 +* 关闭和清理连接失效的客户端 +* 进行 AOF 或 RDB 持久化操作 +* 如果服务器是主服务器,那么对从服务器进行定期同步 +* 如果处于集群模式,对集群进行定期同步和连接测试 -*** +**** -#### 数据插入 -当使用 load 命令导入数据的时候,适当的设置可以提高导入的效率: +##### 时间缓存 -![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL load data.png) +Redis 服务器中有很多功能需要获取系统的当前时间,而每次获取系统的当前时间都需要执行一次系统调用,为了减少系统调用的执行次数,服务器状态中的 unixtime 属性和 mstime 属性被用作当前时间的缓存 -```mysql -LOAD DATA LOCAL INFILE = '/home/seazean/sql1.log' INTO TABLE `tb_user_1` FIELD TERMINATED BY ',' LINES TERMINATED BY '\n'; -- 文件格式如上图 +```c +struct redisServer { + // 保存了秒级精度的系统当前UNIX时间戳 + time_t unixtime; + // 保存了毫秒级精度的系统当前UNIX时间戳 + long long mstime; + +}; ``` -对于 InnoDB 类型的表,有以下几种方式可以提高导入的效率: +serverCron 函数默认以每 100 毫秒一次的频率更新两个属性,所以属性记录的时间的精确度并不高 -1. **主键顺序插入**:因为 InnoDB 类型的表是按照主键的顺序保存的,所以将导入的数据按照主键的顺序排列,可以有效的提高导入数据的效率,如果 InnoDB 表没有主键,那么系统会自动默认创建一个内部列作为主键。 +* 服务器只会在打印日志、更新服务器的 LRU 时钟、决定是否执行持久化任务、计算服务器上线时间(uptime)这类对时间精确度要求不高的功能上 +* 对于为键设置过期时间、添加慢查询日志这种需要高精确度时间的功能来说,服务器还是会再次执行系统调用,从而获得最准确的系统当前时间 - **主键是否连续对性能影响不大,只要是递增的就可以**,比如雪花算法产生的 ID 不是连续的,但是是递增的 - * 插入 ID 顺序排列数据: - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL插入ID顺序排列数据.png) +*** - * 插入 ID 无序排列数据: - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL插入ID无序排列数据.png) -2. **关闭唯一性校验**:在导入数据前执行 `SET UNIQUE_CHECKS=0`,关闭唯一性校验;导入结束后执行 `SET UNIQUE_CHECKS=1`,恢复唯一性校验,可以提高导入的效率。 +##### LRU 时钟 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL插入数据关闭唯一性校验.png) +服务器状态中的 lruclock 属性保存了服务器的 LRU 时钟 -3. **手动提交事务**:如果应用使用自动提交的方式,建议在导入前执行`SET AUTOCOMMIT=0`,关闭自动提交;导入结束后再打开自动提交,可以提高导入的效率。 +```c +struct redisServer { + // 默认每10秒更新一次的时钟缓存,用于计算键的空转(idle)时长。 + unsigned lruclock:22; +}; +``` - 事务需要控制大小,事务太大可能会影响执行的效率。MySQL 有 innodb_log_buffer_size 配置项,超过这个值的日志会写入磁盘数据,效率会下降,所以在事务大小达到配置项数据级前进行事务提交可以提高效率 +每个 Redis 对象都会有一个 lru 属性, 这个 lru 属性保存了对象最后一次被命令访问的时间 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL插入数据手动提交事务.png) +```c +typedef struct redisObiect { + unsigned lru:22; +} robj; +``` +当服务器要计算一个数据库键的空转时间(即数据库键对应的值对象的空转时间),程序会用服务器的 lruclock 属性记录的时间减去对象的 lru 属性记录的时间 +serverCron 函数默认以每 100 毫秒一次的频率更新这个属性,所以得出的空转时间也是模糊的 -**** +*** -#### ORDER BY -数据准备: -```mysql -CREATE TABLE `emp` ( - `id` INT(11) NOT NULL AUTO_INCREMENT, - `name` VARCHAR(100) NOT NULL, - `age` INT(3) NOT NULL, - `salary` INT(11) DEFAULT NULL, - PRIMARY KEY (`id`) -) ENGINE=INNODB DEFAULT CHARSET=utf8mb4; -INSERT INTO `emp` (`id`, `name`, `age`, `salary`) VALUES('1','Tom','25','2300');-- ... -CREATE INDEX idx_emp_age_salary ON emp(age,salary); -``` +##### 命令次数 -* 第一种是通过对返回数据进行排序,所有不通过索引直接返回结果的排序都叫 FileSort 排序,会在内存中重新排序 +serverCron 中的 trackOperationsPerSecond 函数以每 100 毫秒一次的频率执行,函数功能是以**抽样计算**的方式,估算并记录服务器在最近一秒钟处理的命令请求数量,这个值可以通过 INFO status 命令的 instantaneous_ops_per_sec 域查看: - ```mysql - EXPLAIN SELECT * FROM emp ORDER BY age DESC; -- 年龄降序 - ``` +```sh +redis> INFO stats +# Stats +instantaneous_ops_per_sec:6 +``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL ORDER BY排序1.png) +根据上一次抽样时间 ops_sec_last_sample_time 和当前系统时间,以及上一次已执行的命令数 ops_sec_last_sample_ops 和服务器当前已经执行的命令数,计算出两次函数调用期间,服务器平均每毫秒处理了多少个命令请求,该值乘以 1000 得到每秒内的执行命令的估计值,放入 ops_sec_samples 环形数组里 -* 第二种通过有序索引顺序扫描直接返回**有序数据**,这种情况为 Using index,不需要额外排序,操作效率高 +```c +struct redisServer { + // 上一次进行抽样的时间 + long long ops_sec_last_sample_time; + // 上一次抽样时,服务器已执行命令的数量 + long long ops_sec_last_sample_ops; + // REDIS_OPS_SEC_SAMPLES 大小(默认值为16)的环形数组,数组的每一项记录一次的抽样结果 + long long ops_sec_samples[REDIS_OPS_SEC_SAMPLES]; + // ops_sec_samples数组的索引值,每次抽样后将值自增一,值为16时重置为0,让数组成为一个环形数组 + int ops_sec_idx; +}; +``` - ```mysql - EXPLAIN SELECT id, age, salary FROM emp ORDER BY age DESC; - ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL ORDER BY排序2.png) -* 多字段排序: - ```mysql - EXPLAIN SELECT id,age,salary FROM emp ORDER BY age DESC, salary DESC; - EXPLAIN SELECT id,age,salary FROM emp ORDER BY salary DESC, age DESC; - EXPLAIN SELECT id,age,salary FROM emp ORDER BY age DESC, salary ASC; - ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL ORDER BY排序3.png) +*** - 尽量减少额外的排序,通过索引直接返回有序数据。**需要满足 Order by 使用相同的索引、Order By 的顺序和索引顺序相同、Order by 的字段都是升序或都是降序**,否则需要额外的操作,就会出现 FileSort -优化:通过创建合适的索引能够减少 Filesort 的出现,但是某些情况下条件限制不能让 Filesort 消失,就要加快 Filesort 的排序操作 -对于 Filesort , MySQL 有两种排序算法: +##### 内存峰值 -* 两次扫描算法:MySQL4.1 之前,使用该方式排序。首先根据条件取出排序字段和行指针信息,然后在排序区 sort buffer 中排序,如果 sort buffer 不够,则在临时表 temporary table 中存储排序结果。完成排序后再根据行指针**回表读取记录**,该操作可能会导致大量随机 I/O 操作 -* 一次扫描算法:一次性取出满足条件的所有数据,需要回表,然后在排序区 sort buffer 中排序后直接输出结果集。排序时内存开销较大,但是排序效率比两次扫描算法高 +服务器状态里的 stat_peak_memory 属性记录了服务器内存峰值大小,循环函数每次执行时都会查看服务器当前使用的内存数量,并与 stat_peak_memory 保存的数值进行比较,设置为较大的值 -MySQL 通过比较系统变量 max_length_for_sort_data 的大小和 Query 语句取出的字段的大小,来判定使用哪种排序算法。如果前者大,则说明 sort buffer 空间足够,使用第二种优化之后的算法,否则使用第一种。 +```c +struct redisServer { + // 已使用内存峰值 + size_t stat_peak_memory; +}; +``` -可以适当提高 sort_buffer_size 和 max_length_for_sort_data 系统变量,来增大排序区的大小,提高排序的效率 +INFO memory 命令的 used_memory_peak 和 used_memory_peak_human 两个域分别以两种格式记录了服务器的内存峰值: -```mysql -SET @@max_length_for_sort_data = 10000; -- 设置全局变量 -SET max_length_for_sort_data = 10240; -- 设置会话变量 -SHOW VARIABLES LIKE 'max_length_for_sort_data'; -- 默认1024 -SHOW VARIABLES LIKE 'sort_buffer_size'; -- 默认262114 +```sh +redis> INFO memory +# Memory +... +used_memory_peak:501824 +used_memory_peak_human:490.06K ``` @@ -5492,146 +10427,129 @@ SHOW VARIABLES LIKE 'sort_buffer_size'; -- 默认262114 -#### GROUP BY - -GROUP BY 也会进行排序操作,与 ORDER BY 相比,GROUP BY 主要只是多了排序之后的分组操作,所以在 GROUP BY 的实现过程中,与 ORDER BY 一样也可以利用到索引 +##### SIGTERM -* 分组查询: +服务器启动时,Redis 会为服务器进程的 SIGTERM 信号关联处理器 sigtermHandler 函数,该信号处理器负责在服务器接到 SIGTERM 信号时,打开服务器状态的 shutdown_asap 标识 - ```mysql - DROP INDEX idx_emp_age_salary ON emp; - EXPLAIN SELECT age,COUNT(*) FROM emp GROUP BY age; - ``` +```c +struct redisServer { + // 关闭服务器的标识:值为1时关闭服务器,值为0时不做操作 + int shutdown_asap; +}; +``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL GROUP BY排序1.png) +每次 serverCron 函数运行时,程序都会对服务器状态的 shutdown_asap 属性进行检查,并根据属性的值决定是否关闭服务器 - Using temporary:表示 MySQL 需要使用临时表来存储结果集,常见于排序和分组查询 +服务器在接到 SIGTERM 信号之后,关闭服务器并打印相关日志的过程: -* 查询包含 GROUP BY 但是用户想要避免排序结果的消耗, 则可以执行 ORDER BY NULL 禁止排序: +```sh +[6794 | signal handler] (1384435690) Received SIGTERM, scheduling shutdown ... +[6794] 14 Nov 21:28:10.108 # User requested shutdown ... +[6794] 14 Nov 21:28:10.108 * Saving the final RDB snapshot before exiting. +[6794) 14 Nov 21:28:10.161 * DB saved on disk +[6794) 14 Nov 21:28:10.161 # Redisis now ready to exit, bye bye ... +``` - ```mysql - EXPLAIN SELECT age,COUNT(*) FROM emp GROUP BY age ORDER BY NULL; - ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL GROUP BY排序2.png) -* 创建索引: +*** - ```mysql - CREATE INDEX idx_emp_age_salary ON emp(age,salary); - ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL GROUP BY排序3.png) +##### 管理资源 +serverCron 函数每次执行都会调用 clientsCron 和 databasesCron 函数,进行管理客户端资源和数据库资源 -*** +clientsCron 函数对一定数量的客户端进行以下两个检查: +* 如果客户端与服务器之间的连接巳经超时(很长一段时间客户端和服务器都没有互动),那么程序释放这个客户端 +* 如果客户端在上一次执行命令请求之后,输入缓冲区的大小超过了一定的长度,那么程序会释放客户端当前的输入缓冲区,并重新创建一个默认大小的输入缓冲区,从而防止客户端的输入缓冲区耗费了过多的内存 +databasesCron 函数会对服务器中的一部分数据库进行检查,删除其中的过期键,并在有需要时对字典进行收缩操作 -#### OR -对于包含 OR 的查询子句,如果要利用索引,则 OR 之间的**每个条件列都必须用到索引,而且不能使用到条件之间的复合索引**,如果没有索引,则应该考虑增加索引 -* 执行查询语句: +*** - ```mysql - EXPLAIN SELECT * FROM emp WHERE id = 1 OR age = 30; -- 两个索引,并且不是复合索引 - ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL OR条件查询1.png) - ```sh - Extra: Using sort_union(idx_emp_age_salary,PRIMARY); Using where - ``` +##### 持久状态 -* 使用 UNION 替换 OR,求并集: - - 注意:该优化只针对多个索引列有效,如果有列没有被索引,查询效率可能会因为没有选择 OR 而降低 - - ```mysql - EXPLAIN SELECT * FROM emp WHERE id = 1 UNION SELECT * FROM emp WHERE age = 30; - ``` - - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL OR条件查询2.png) - -* UNION 要优于 OR 的原因: +服务器状态中记录执行 BGSAVE 命令和 BGREWRITEAOF 命令的子进程的 ID - * UNION 语句的 type 值为 ref,OR 语句的 type 值为 range - * UNION 语句的 ref 值为 const,OR 语句的 ref 值为 null,const 表示是常量值引用,非常快 +```c +struct redisServer { + // 记录执行BGSAVE命令的子进程的ID,如果服务器没有在执行BGSAVE,那么这个属性的值为-1 + pid_t rdb_child_pid; + // 记录执行BGREWRITEAOF命令的子进程的ID,如果服务器没有在执行那么这个属性的值为-1 + pid_t aof_child_pid +}; +``` +serverCron 函数执行时,会检查两个属性的值,只要其中一个属性的值不为 -1,程序就会执行一次 wait3 函数,检查子进程是否有信号发来服务器进程: +* 如果有信号到达,那么表示新的 RDB 文件已经生成或者 AOF 重写完毕,服务器需要进行相应命令的后续操作,比如用新的 RDB 文件替换现有的 RDB 文件,用重写后的 AOF 文件替换现有的 AOF 文件 +* 如果没有信号到达,那么表示持久化操作未完成,程序不做动作 -**** +如果两个属性的值都为 -1,表示服务器没有进行持久化操作 +* 查看是否有 BGREWRITEAOF 被延迟,然后执行 AOF 后台重写 +* 查看服务器的自动保存条件是否已经被满足,并且服务器没有在进行持久化,就开始一次新的 BGSAVE 操作 -#### 嵌套查询 + 因为条件 1 可能会引发一次 AOF,所以在这个检查中会再次确认服务器是否已经在执行持久化操作 -MySQL 4.1 版本之后,开始支持 SQL 的子查询 +* 检查服务器设置的 AOF 重写条件是否满足,条件满足并且服务器没有进行持久化,就进行一次 AOF 重写 -* 可以使用 SELECT 语句来创建一个单列的查询结果,然后把结果作为过滤条件用在另一个查询中 -* 使用子查询可以一次性的完成逻辑上需要多个步骤才能完成的 SQL 操作,同时也可以避免事务或者表锁死 -* 在有些情况下,子查询是可以被更高效的连接(JOIN)替代 +如果服务器开启了 AOF 持久化功能,并且 AOF 缓冲区里还有待写入的数据, 那么 serverCron 函数会调用相应的程序,将 AOF 缓冲区中的内容写入到 AOF 文件里 -例如查找有角色的所有的用户信息: -* 执行计划: - ```mysql - EXPLAIN SELECT * FROM t_user WHERE id IN (SELECT user_id FROM user_role); - ``` +*** - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL嵌套查询1.png) -* 优化后: - ```mysql - EXPLAIN SELECT * FROM t_user u , user_role ur WHERE u.id = ur.user_id; - ``` +##### 延迟执行 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL嵌套查询2.png) +在服务器执行 BGSAVE 命令的期间,如果客户端发送 BGREWRITEAOF 命令,那么服务器会将 BGREWRITEAOF 命令的执行时间延迟到 BGSAVE 命令执行完毕之后,用服务器状态的 aof_rewrite_scheduled 属性标识延迟与否 - 连接查询之所以效率更高 ,是因为不需要在内存中创建临时表来完成逻辑上需要两个步骤的查询工作 +```c +struct redisServer { + // 如果值为1,那么表示有 BGREWRITEAOF命令被延迟了 + int aof_rewrite_scheduled; +}; +``` +serverCron 函数会检查 BGSAVE 或者 BGREWRITEAOF 命令是否正在执行,如果这两个命令都没在执行,并且 aof_rewrite_scheduled 属性的值为 1,那么服务器就会执行之前被推延的 BGREWRITEAOF 命令 +**** -*** +##### 执行次数 -#### 分页查询 +服务器状态的 cronloops 属性记录了 serverCron 函数执行的次数 -一般分页查询时,通过创建覆盖索引能够比较好地提高性能 +```c +struct redisServer { + // serverCron 函数每执行一次,这个属性的值就增 1 + int cronloops; +}; +``` -一个常见的问题是 `LIMIT 200000,10`,此时需要 MySQL 扫描前 200010 记录,仅仅返回 200000 - 200010 之间的记录,其他记录丢弃,查询排序的代价非常大 -* 分页查询: - ```mysql - EXPLAIN SELECT * FROM tb_user_1 LIMIT 200000,10; - ``` +**** - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL分页查询1.png) -* 优化方式一:子查询,在索引列 id 上完成排序分页操作,最后根据主键关联回原表查询所需要的其他列内容 - ```mysql - EXPLAIN SELECT * FROM tb_user_1 t,(SELECT id FROM tb_user_1 ORDER BY id LIMIT 200000,10) a WHERE t.id = a.id; - ``` +##### 缓冲限制 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL分页查询2.png) +服务器会关闭那些输入或者输出**缓冲区大小超出限制**的客户端 -* 优化方式二:方案适用于主键自增的表,可以把 LIMIT 查询转换成某个位置的查询 - ```mysql - EXPLAIN SELECT * FROM tb_user_1 WHERE id > 200000 LIMIT 10; -- 写法 1 - EXPLAIN SELECT * FROM tb_user_1 WHERE id BETWEEN 200000 and 200010; -- 写法 2 - ``` - - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL分页查询3.png) @@ -5639,36 +10557,36 @@ MySQL 4.1 版本之后,开始支持 SQL 的子查询 -#### 使用提示 - -SQL 提示,是优化数据库的一个重要手段,就是在 SQL 语句中加入一些提示来达到优化操作的目的 - -* USE INDEX:在查询语句中表名的后面添加 USE INDEX 来提供 MySQL 去参考的索引列表,可以让 MySQL 不再考虑其他可用的索引 +#### 初始化 - ```mysql - CREATE INDEX idx_seller_name ON tb_seller(name); - EXPLAIN SELECT * FROM tb_seller USE INDEX(idx_seller_name) WHERE name='小米科技'; - ``` +##### 初始结构 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用提示1.png) +一个 Redis 服务器从启动到能够接受客户端的命令请求,需要经过一系列的初始化和设置过程 -* IGNORE INDEX:让 MySQL 忽略一个或者多个索引,则可以使用 IGNORE INDEX 作为提示 +第一步:创建一个 redisServer 类型的实例变量 server 作为服务器的状态,并为结构中的各个属性设置默认值,由 initServerConfig 函数进行初始化一般属性: - ```mysql - EXPLAIN SELECT * FROM tb_seller IGNORE INDEX(idx_seller_name) WHERE name = '小米科技'; - ``` +* 设置服务器的运行 ID、默认运行频率、默认配置文件路径、默认端口号、默认 RDB 持久化条件和 AOF 持久化条件 +* 初始化服务器的 LRU 时钟,创建命令表 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用提示2.png) +第二步:载入配置选项,用户可以通过给定配置参数或者指定配置文件,对 server 变量相关属性的默认值进行修改 -* FORCE INDEX:强制 MySQL 使用一个特定的索引 +第三步:初始化服务器数据结构(除了命令表之外),因为服务器**必须先载入用户指定的配置选项才能正确地对数据结构进行初始化**,所以载入配置完成后才进性数据结构的初始化,服务器将调用 initServer 函数: - ```mysql - EXPLAIN SELECT * FROM tb_seller FORCE INDEX(idx_seller_name_sta_addr) WHERE NAME='小米科技'; - ``` +* server.clients 链表,记录了的客户端的状态结构;server.db 数组,包含了服务器的所有数据库 +* 用于保存频道订阅信息的 server.pubsub_channels 字典, 以及保存模式订阅信息的 server.pubsub_patterns 链表 +* 用于执行 Lua 脚本的 Lua 环境 server.lua +* 保存慢查询日志的 server.slowlog 属性 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用提示3.png) +initServer 还进行了非常重要的设置操作: +* 为服务器设置进程信号处理器 +* 创建共享对象,包含 OK、ERR、**整数 1 到 10000 的字符串对象**等 +* **打开服务器的监听端口** +* **为 serverCron 函数创建时间事件**, 等待服务器正式运行时执行 serverCron 函数 +* 如果 AOF 持久化功能已经打开,那么打开现有的 AOF 文件,如果 AOF 文件不存在,那么创建并打开一个新的 AOF 文件 ,为 AOF 写入做好准备 +* **初始化服务器的后台 I/O 模块**(BIO), 为将来的 I/O 操作做好准备 +当 initServer 函数执行完毕之后, 服务器将用 ASCII 字符在日志中打印出 Redis 的图标, 以及 Redis 的版本号信息 @@ -5676,111 +10594,126 @@ SQL 提示,是优化数据库的一个重要手段,就是在 SQL 语句中 -#### 统计计数 - -在不同的 MySQL 引擎中,count(*) 有不同的实现方式: +##### 还原状态 -* MyISAM 引擎把一个表的总行数存在了磁盘上,因此执行 count(*) 的时候会直接返回这个数,效率很高,但不支持事务 -* show table status 命令通过采样估算可以快速获取,但是不准确 -* InnoDB 表执行 count(*) 会遍历全表,虽然结果准确,但会导致性能问题 +在完成了对服务器状态的初始化之后,服务器需要载入RDB文件或者AOF 文件, 并根据文件记录的内容来还原服务器的数据库状态: -解决方案: +* 如果服务器启用了 AOF 持久化功能,那么服务器使用 AOF 文件来还原数据库状态 +* 如果服务器没有启用 AOF 持久化功能,那么服务器使用 RDB 文件来还原数据库状态 -* 计数保存在 Redis 中,但是更新 MySQL 和 Redis 的操作不是原子的,会存在数据一致性的问题 +当服务器完成数据库状态还原工作之后,服务器将在日志中打印出载入文件并还原数据库状态所耗费的时长 -* 计数直接放到数据库里单独的一张计数表中,利用事务解决计数精确问题: +```sh +[7171] 22 Nov 22:43:49.084 * DB loaded from disk: 0.071 seconds +``` - - 会话 B 的读操作在 T3 执行的,这时更新事务还没有提交,所以计数值加 1 这个操作对会话 B 还不可见,因此会话 B 查询的计数值和最近 100 条记录,返回的结果逻辑上就是一致的 - 并发系统性能的角度考虑,应该先插入操作记录再更新计数表,因为更新计数表涉及到行锁的竞争,**先插入再更新能最大程度地减少事务之间的锁等待,提升并发度** +*** -count 函数的按照效率排序:`count(字段) < count(主键id) < count(1) ≈ count(*)`,所以建议尽量使用 count(*) -* count(主键 id):InnoDB 引擎会遍历整张表,把每一行的 id 值都取出来返回给 Server 层,Server 判断 id 不为空就按行累加 -* count(1):InnoDB 引擎遍历整张表但不取值,Server 层对于返回的每一行,放一个数字 1 进去,判断不为空就按行累加 -* count(字段):如果这个字段是定义为 not null 的话,一行行地从记录里面读出这个字段,判断不能为 null,按行累加;如果这个字段定义允许为 null,那么执行的时候,判断到有可能是 null,还要把值取出来再判断一下,不是 null 才累加 +##### 驱动循环 +在初始化的最后一步,服务器将打印出以下日志,并开始**执行服务器的事件循环**(loop) +```c +[7171] 22 Nov 22:43:49.084 * The server is now ready to accept connections on pert 6379 +``` -参考文章:https://time.geekbang.org/column/article/72775 +服务器现在开始可以接受客户端的连接请求,并处理客户端发来的命令请求了 -*** +***** -### 内存优化 +### 慢日志 -#### 优化原则 +#### 基本介绍 -三个原则: +Redis 的慢查询日志功能用于记录执行时间超过给定时长的命令请求,通过产生的日志来监视和优化查询速度 -* 将尽量多的内存分配给 MySQL 做缓存,但也要给操作系统和其他程序预留足够内存 -* MyISAM 存储引擎的数据文件读取依赖于操作系统自身的 IO 缓存,如果有 MyISAM 表,就要预留更多的内存给操作系统做 IO 缓存 -* 排序区、连接区等缓存是分配给每个数据库会话(Session)专用的,值的设置要根据最大连接数合理分配,如果设置太大,不但浪费资源,而且在并发数较高时会导致物理内存耗尽 +服务器配置有两个和慢查询日志相关的选项: +* slowlog-log-slower-than 选项指定执行时间超过多少微秒的命令请求会被记录到日志上 +* slowlog-max-len 选项指定服务器最多保存多少条慢查询日志 +服务器使用先进先出 FIFO 的方式保存多条慢查询日志,当服务器存储的慢查询日志数量等于 slowlog-max-len 选项的值时,在添加一条新的慢查询日志之前,会先将最旧的一条慢查询日志删除 -*** +配置选项可以通过 CONFIG SET option value 命令进行设置 +常用命令: +```sh +SLOWLOG GET [n] # 查看 n 条服务器保存的慢日志 +SLOWLOG LEN # 查看日志数量 +SLOWLOG RESET # 清除所有慢查询日志 +``` -#### MyISAM -MyISAM 存储引擎使用 key_buffer 缓存索引块,加速 MyISAM 索引的读写速度。对于 MyISAM 表的数据块没有特别的缓存机制,完全依赖于操作系统的 IO 缓存 -* key_buffer_size:该变量决定 MyISAM 索引块缓存区的大小,直接影响到 MyISAM 表的存取效率 +*** - ```mysql - SHOW VARIABLES LIKE 'key_buffer_size'; -- 单位是字节 - ``` - 在 MySQL 配置文件中设置该值,建议至少将1/4可用内存分配给 key_buffer_size: - ```sh - vim /etc/mysql/my.cnf - key_buffer_size=1024M - ``` +#### 日志保存 -* read_buffer_size:如果需要经常顺序扫描 MyISAM 表,可以通过增大 read_buffer_size 的值来改善性能。但 read_buffer_size 是每个 Session 独占的,如果默认值设置太大,并发环境就会造成内存浪费 +服务器状态中包含了慢查询日志功能有关的属性: -* read_rnd_buffer_size:对于需要做排序的 MyISAM 表的查询,如带有 ORDER BY 子句的语句,适当增加该的值,可以改善此类的 SQL 的性能,但是 read_rnd_buffer_size 是每个 Session 独占的,如果默认值设置太大,就会造成内存浪费 +```c +struct redisServer { + // 下一条慢查询日志的ID + long long slowlog_entry_id; + + // 保存了所有慢查询日志的链表 + list *slowlog; + + // 服务器配置选项的值 + long long slowlog-log-slower-than; + // 服务器配置选项的值 + unsigned long slowlog_max_len; +} +``` +slowlog_entry_id 属性的初始值为 0,每当创建一条新的慢查询日志时,这个属性就会用作新日志的 id 值,之后该属性增一 +slowlog 链表保存了服务器中的所有慢查询日志,链表中的每个节点是一个 slowlogEntry 结构, 代表一条慢查询日志: -*** +```c +typedef struct slowlogEntry { + // 唯一标识符 + long long id; + // 命令执行时的时间,格式为UNIX时间戳 + time_t time; + // 执行命令消耗的时间,以微秒为单位 + long long duration; + // 命令与命令参数 + robj **argv; + // 命令与命令参数的数量 + int argc; +} +``` -#### InnoDB -Innodb 用一块内存区做 IO 缓存池,该缓存池不仅用来缓存 Innodb 的索引块,也用来缓存 Innodb 的数据块 -* innodb_buffer_pool_size:该变量决定了 Innodb 存储引擎表数据和索引数据的最大缓存区大小 +*** - ```mysql - SHOW VARIABLES LIKE 'innodb_buffer_pool_size'; - ``` - 在保证操作系统及其他程序有足够内存可用的情况下,innodb_buffer_pool_size 的值越大,缓存命中率越高 - ```sh - innodb_buffer_pool_size=512M - ``` +#### 添加日志 -* innodb_log_buffer_size:该值决定了 Innodb 日志缓冲区的大小,保存要写入磁盘上的日志文件数据 +在每次执行命令的前后,程序都会记录微秒格式的当前 UNIX 时间戳,两个时间之差就是执行命令所耗费的时长,函数会检查命令的执行时长是否超过 slowlog-log-slower-than 选项所设置: - 对于可能产生大量更新记录的大事务,增加 innodb_log_buffer_size 的大小,可以避免 Innodb 在事务提交前就执行不必要的日志写入磁盘操作,影响执行效率。通过配置文件修改: +* 如果是的话,就为命令创建一个新的日志,并将新日志添加到 slowlog 链表的表头 +* 检查慢查询日志的长度是否超过 slowlog-max-len 选项所设置的长度,如果是将多出来的日志从 slowlog 链表中删除掉 - ```sh - innodb_log_buffer_size=10M - ``` +* 将 redisServer. slowlog_entry_id 的值增 1 @@ -5790,148 +10723,205 @@ Innodb 用一块内存区做 IO 缓存池,该缓存池不仅用来缓存 Innod -### 并发优化 -MySQL Server 是多线程结构,包括后台线程和客户服务线程。多线程可以有效利用服务器资源,提高数据库的并发性能。在 MySQL 中,控制并发连接和线程的主要参数: -* max_connections:控制允许连接到 MySQL 数据库的最大连接数,默认值是 151 +## 数据结构 - 如果状态变量 connection_errors_max_connections 不为零,并且一直增长,则说明不断有连接请求因数据库连接数已达到允许最大值而失败,这时可以考虑增大max_connections 的值 +### 字符串 - Mysql 最大可支持的连接数取决于很多因素,包括操作系统平台的线程库的质量、内存大小、每个连接的负荷、CPU的处理速度、期望的响应时间等。在 Linux 平台下,性能好的服务器,可以支持 500-1000 个连接,需要根据服务器性能进行评估设定 +#### SDS -* back_log:控制 MySQL 监听 TCP 端口时的积压请求栈的大小 +Redis 构建了简单动态字符串(SDS)的数据类型,作为 Redis 的默认字符串表示,包含字符串的键值对在底层都是由 SDS 实现 - 如果 Mysql 的连接数达到 max_connections 时,新来的请求将会被存在堆栈中,以等待某一连接释放资源,该堆栈的数量即 back_log。如果等待连接的数量超过 back_log,将不被授予连接资源直接报错 +```c +struct sdshdr { + // 记录buf数组中已使用字节的数量,等于 SDS 所保存字符串的长度 + int len; + + // 记录buf数组中未使用字节的数量 + int free; + + // 【字节】数组,用于保存字符串(不是字符数组) + char buf[]; +}; +``` - 5.6.6 版本之前默认值为 50,之后的版本默认为 `50 + (max_connections/5)`,但最大不超过900,如果需要数据库在较短的时间内处理大量连接请求, 可以考虑适当增大 back_log 的值 +SDS 遵循 C 字符串**以空字符结尾**的惯例,保存空字符的 1 字节不计算在 len 属性,SDS 会自动为空字符分配额外的 1 字节空间和添加空字符到字符串末尾,所以空字符对于 SDS 的使用者来说是完全透明的 -* table_open_cache:控制所有 SQL 语句执行线程可打开表缓存的数量 +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-SDS底层结构.png) - 在执行 SQL 语句时,每个执行线程至少要打开1个表缓存,该参数的值应该根据设置的最大连接数以及每个连接执行关联查询中涉及的表的最大数量来设定:`max_connections * N` -* thread_cache_size:可控制 MySQL 缓存客户服务线程的数量 - 为了加快连接数据库的速度,MySQL 会缓存一定数量的客户服务线程以备重用,池化思想 +*** -* innodb_lock_wait_timeout:设置 InnoDB 事务等待行锁的时间,默认值是 50ms - 对于需要快速反馈的业务系统,可以将行锁的等待时间调小,以避免事务被长时间挂起; 对于后台运行的批量处理程序来说,可以将行锁的等待时间调大,以避免发生大的回滚操作 +#### 对比 +常数复杂度获取字符串长度: +* C 字符串不记录自身的长度,获取时需要遍历整个字符串,遇到空字符串为止,时间复杂度为 O(N) +* SDS 获取字符串长度的时间复杂度为 O(1),设置和更新 SDS 长度由函数底层自动完成 +杜绝缓冲区溢出: -*** +* C 字符串调用 strcat 函数拼接字符串时,如果字符串内存不够容纳目标字符串,就会造成缓冲区溢出(Buffer Overflow) + s1 和 s2 是内存中相邻的字符串,执行 `strcat(s1, " Cluster")`(有空格): + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-内存溢出问题.png) +* SDS 空间分配策略:当对 SDS 进行修改时,首先检查 SDS 的空间是否满足修改所需的要求, 如果不满足会自动将 SDS 的空间扩展至执行修改所需的大小,然后执行实际的修改操作, 避免了缓冲区溢出的问题 +二进制安全: -## 主从复制 +* C 字符串中的字符必须符合某种编码(比如 ASCII)方式,除了字符串末尾以外其他位置不能包含空字符,否则会被误认为是字符串的结尾,所以只能保存文本数据 +* SDS 的 API 都是二进制安全的,使用字节数组 buf 保存一系列的二进制数据,**使用 len 属性来判断数据的结尾**,所以可以保存图片、视频、压缩文件等二进制数据 -### 基本介绍 +兼容 C 字符串的函数:SDS 会在为 buf 数组分配空间时多分配一个字节来保存空字符,所以可以重用一部分 C 字符串函数库的函数 -复制是指将主数据库的 DDL 和 DML 操作通过二进制日志传到从库服务器中,然后在从库上对这些日志重新执行(也叫重做),从而使得从库和主库的数据保持同步 -MySQL 支持一台主库同时向多台从库进行复制,从库同时也可以作为其他从服务器的主库,实现链状复制 -MySQL 复制的优点主要包含以下三个方面: +*** -- 主库出现问题,可以快速切换到从库提供服务 -- 可以在从库上执行查询操作,从主库中更新,实现读写分离 -- 可以在从库中执行备份,以避免备份期间影响主库的服务 +#### 内存 +C 字符串**每次**增长或者缩短都会进行一次内存重分配,拼接操作通过重分配扩展底层数组空间,截断操作通过重分配释放不使用的内存空间,防止出现内存泄露 +SDS 通过未使用空间解除了字符串长度和底层数组长度之间的关联,在 SDS 中 buf 数组的长度不一定就是字符数量加一, 数组里面可以包含未使用的字节,字节的数量由 free 属性记录 -*** +内存重分配涉及复杂的算法,需要执行**系统调用**,是一个比较耗时的操作,SDS 的两种优化策略: +* 空间预分配:当 SDS 需要进行空间扩展时,程序不仅会为 SDS 分配修改所必需的空间, 还会为 SDS 分配额外的未使用空间 + * 对 SDS 修改之后,SDS 的长度(len 属性)小于 1MB,程序分配和 len 属性同样大小的未使用空间,此时 len 和 free 相等 -### 复制原理 + s 为 Redis,执行 `sdscat(s, " Cluster")` 后,len 变为 13 字节,所以也分配了 13 字节的 free 空间,总长度变为 27 字节(额外的一字节保存空字符,13 + 13 + 1 = 27) -#### 主从结构 + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-SDS内存预分配.png) -MySQL 的主从之间维持了一个长连接。主库内部有一个线程,专门用于服务从库的长连接,连接过程: + * 对 SDS 修改之后,SDS 的长度大于等于 1MB,程序会分配 1MB 的未使用空间 -* 从库执行 change master 命令,设置主库的 IP、端口、用户名、密码以及要从哪个位置开始请求 binlog,这个位置包含文件名和日志偏移量 -* 从库执行 start slave 命令,这时从库会启动两个线程,就是图中的 io_thread 和 sql_thread,其中 io_thread 负责与主库建立连接 -* 主库校验完用户名、密码后,开始按照从传过来的位置,从本地读取 binlog 发给从库,开始主从复制 + 在扩展 SDS 空间前,API 会先检查 free 空间是否足够,如果足够就无需执行内存重分配,所以通过预分配策略,SDS 将连续增长 N 次字符串所需内存的重分配次数从**必定 N 次降低为最多 N 次** -主从复制原理图: +* 惰性空间释放:当 SDS 缩短字符串时,程序并不立即使用内存重分配来回收缩短后多出来的字节,而是使用 free 属性将这些字节的数量记录起来,并等待将来复用 -![](https://gitee.com/seazean/images/raw/master/DB/MySQL-主从复制原理图.jpg) + SDS 提供了相应的 API 来真正释放 SDS 的未使用空间,所以不用担心空间惰性释放策略造成的内存浪费问题 -主从复制主要依赖的是 binlog,MySQL 默认是异步复制,需要三个线程: -- binlog dump thread:在主库事务提交时,负责把数据变更记录在二进制日志文件 binlog 中,并通知 slave 有数据更新 -- I/O thread:负责从主服务器上拉取二进制日志,并将 binlog 日志内容依次写到 relay log 中转日志的最末端,并将新的 binlog 文件名和 offset 记录到 master-info 文件中,以便下一次读取日志时从指定 binlog 日志文件及位置开始读取新的 binlog 日志内容 -- SQL thread:监测本地 relay log 中新增了日志内容,读取中继日志并重做其中的 SQL 语句,从库在 relay-log.info 中记录当前应用中继日志的文件名和位置点以便下一次执行 -同步与异步: -* 异步复制有数据丢失风险,例如数据还未同步到从库,主库就给客户端响应,然后主库挂了,此时从库晋升为主库的话数据是缺失的 -* 同步复制,主库需要将 binlog 复制到所有从库,等所有从库响应了之后主库才进行其他逻辑,这样的话性能很差,一般不会选择 -* MySQL 5.7 之出现了半同步复制,有参数可以选择成功同步几个从库就返回响应 +**** -**** +### 链表 +链表提供了高效的节点重排能力,C 语言并没有内置这种数据结构,所以 Redis 构建了链表数据类型 -#### 主主结构 +链表节点: -主主结构就是两个数据库之间总是互为主从关系,这样在切换的时候就不用再修改主从关系 +```c +typedef struct listNode { + // 前置节点 + struct listNode *prev; + + // 后置节点 + struct listNode *next; + + // 节点的值 + void *value +} listNode; +``` -循环复制:在库 A 上更新了一条语句,然后把生成的 binlog 发给库 B,库 B 执行完这条更新语句后也会生成 binlog,会再发给 A +多个 listNode 通过 prev 和 next 指针组成**双端链表**: -解决方法: +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-链表节点底层结构.png) -* 两个库的 server id 必须不同,如果相同则它们之间不能设定为主主关系 -* 一个库接到 binlog 并在重放的过程中,生成与原 binlog 的 server id 相同的新的 binlog -* 每个库在收到从主库发过来的日志后,先判断 server id,如果跟自己的相同,表示这个日志是自己生成的,就直接丢弃这个日志 +list 链表结构:提供了表头指针 head 、表尾指针 tail 以及链表长度计数器 len +```c +typedef struct list { + // 表头节点 + listNode *head; + // 表尾节点 + listNode *tail; + + // 链表所包含的节点数量 + unsigned long len; + + // 节点值复制函数,用于复制链表节点所保存的值 + void *(*dup) (void *ptr); + // 节点值释放函数,用于释放链表节点所保存的值 + void (*free) (void *ptr); + // 节点值对比函数,用于对比链表节点所保存的值和另一个输入值是否相等 + int (*match) (void *ptr, void *key); +} list; +``` +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-链表底层结构.png) -*** +Redis 链表的特性: +* 双端:链表节点带有 prev 和 next 指针,获取某个节点的前置节点和后置节点的时间复杂度都是 O(1) +* 无环:表头节点的 prev 指针和表尾节点的 next 指针都指向 NULL,对链表的访问以 NULL 为终点 +* 带表头指针和表尾指针: 通过 list 结构的 head 指针和 tail 指针,获取链表的表头节点和表尾节点的时间复杂度为 O(1) +* 带链表长度计数器:使用 len 属性来对 list 持有的链表节点进行计数,获取链表中节点数量的时间复杂度为 O(1) +* 多态:链表节点使用 void * 指针来保存节点值, 并且可以通过 dup、free 、match 三个属性为节点值设置类型特定函数,所以链表可以保存各种**不同类型的值** -### 主从延迟 -#### 延迟原因 -正常情况主库执行更新生成的所有 binlog,都可以传到从库并被正确地执行,从库就能达到跟主库一致的状态,这就是最终一致性 -主从延迟是主从之间是存在一定时间的数据不一致,就是同一个事务在从库执行完成的时间和主库执行完成的时间的差值,即 T2-T1 +**** -- 主库 A 执行完成一个事务,写入 binlog,该时刻记为 T1 -- 日志传给从库 B,从库 B 执行完这个事务,该时刻记为 T2 -通过在从库执行 `show slave status` 命令,返回结果会显示 seconds_behind_master 表示当前从库延迟了多少秒 -- 每一个事务的 binlog 都有一个时间字段,用于记录主库上**写入**的时间 -- 从库取出当前正在**执行**的事务的时间字段,跟系统的时间进行相减,得到的就是 seconds_behind_master +### 字典 -主从延迟的原因: +#### 哈希表 -* 从库的查询压力大 -* 大事务的执行,主库必须要等到事务完成之后才会写入 binlog,导致从节点出现应用 binlog 延迟 -* 主库的 DDL,从库与主库的 DDL 同步是串行进行,DDL 在主库执行时间很长,那么从库也会消耗同样的时间 -* 锁冲突问题也可能导致从节点的 SQL 线程执行慢 -* 从库的机器性能比主库的差,导致从库的复制能力弱 +Redis 字典使用的哈希表结构: -主从同步问题永远都是**一致性和性能的权衡**,需要根据实际的应用场景,可以采取下面的办法: +```c +typedef struct dictht { + // 哈希表数组,数组中每个元素指向 dictEntry 结构 + dictEntry **table; + + // 哈希表大小,数组的长度 + unsigned long size; + + // 哈希表大小掩码,用于计算索引值,总是等于 【size-1】 + unsigned long sizemask; + + // 该哈希表已有节点的数量 + unsigned long used; +} dictht; +``` -* 优化 SQL,避免慢 SQL,减少批量操作 -* 降低多线程大事务并发的概率,优化业务逻辑 -* 业务中大多数情况查询操作要比更新操作更多,搭建一主多从结构,让这些从库来分担读的压力 +哈希表节点结构: -* 尽量采用短的链路,主库和从库服务器的距离尽量要短,提升端口带宽,减少 binlog 传输的网络延时 -* 实时性要求高的业务读强制走主库,从库只做备份 +```c +typedef struct dictEntry { + // 键 + void *key; + + // 值,可以是一个指针,或者整数 + union { + void *val; // 指针 + uint64_t u64; + int64_t s64; + } + + // 指向下个哈希表节点,形成链表,用来解决冲突问题 + struct dictEntry *next; +} dictEntry; +``` + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-哈希表底层结构.png) @@ -5939,350 +10929,346 @@ MySQL 的主从之间维持了一个长连接。主库内部有一个线程, -#### 并行复制 +#### 字典结构 -##### MySQL5.6 +字典,又称为符号表、关联数组、映射(Map),用于保存键值对的数据结构,字典中的每个键都是独一无二的。底层采用哈希表实现,一个哈希表包含多个哈希表节点,每个节点保存一个键值对 -高并发情况下,主库的会产生大量的 binlog,在从库中有两个线程 IO Thread 和 SQL Thread 单线程执行,会导致主库延迟变大。为了改善复制延迟问题,MySQL 5.6 版本增加了并行复制功能,以采用多线程机制来促进执行 +```c +typedef struct dict { + // 类型特定函数 + dictType *type; + + // 私有数据 + void *privdata; + + // 哈希表,数组中的每个项都是一个dictht哈希表, + // 一般情况下字典只使用 ht[0] 哈希表, ht[1] 哈希表只会在对 ht[0] 哈希表进行 rehash 时使用 + dictht ht[2]; + + // rehash 索引,当 rehash 不在进行时,值为 -1 + int rehashidx; +} dict; +``` -coordinator 就是原来的 sql_thread,并行复制中它不再直接更新数据,只**负责读取中转日志和分发事务**: +type 属性和 privdata 属性是针对不同类型的键值对, 为创建多态字典而设置的: -* 线程分配完成并不是立即执行,为了防止造成更新覆盖,更新同一 DB 的两个事务必须被分发到同一个工作线程 -* 同一个事务不能被拆开,必须放到同一个工作线程 +* type 属性是指向 dictType 结构的指针, 每个 dictType 结构保存了一簇用于操作特定类型键值对的函数, Redis 会为用途不同的字典设置不同的类型特定函数 +* privdata 属性保存了需要传给那些类型特定函数的可选参数 -MySQL 5.6 版本的策略:每个线程对应一个 hash 表,用于保存当前这个线程的执行队列里的事务所涉及的表,hash 表的 key 是数据库 名,value 是一个数字,表示队列中有多少个事务修改这个库,适用于主库上有多个 DB 的情况 +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-字典底层结构.png) -每个事务在分发的时候,跟线程的冲突(事务操作的是同一个 DB)关系包括以下三种情况: -* 如果跟所有线程都不冲突,coordinator 线程就会把这个事务分配给最空闲的线程 -* 如果只跟一个线程冲突,coordinator 线程就会把这个事务分配给这个存在冲突关系的线程 -* 如果跟多于一个线程冲突,coordinator 线程就进入等待状态,直到和这个事务存在冲突关系的线程只剩下 1 个 -优缺点: +**** -* 构造 hash 值的时候很快,只需要库名,而且一个实例上 DB 数也不会很多,不会出现需要构造很多个项的情况 -* 不要求 binlog 的格式,statement 格式的 binlog 也可以很容易拿到库名(日志章节详解了 binlog) -* 主库上的表都放在同一个 DB 里面,这个策略就没有效果了;或者不同 DB 的热点不同,比如一个是业务逻辑库,一个是系统配置库,那也起不到并行的效果,需要**把相同热度的表均匀分到这些不同的 DB 中**,才可以使用这个策略 +#### 哈希冲突 -*** +Redis 使用 MurmurHash 算法来计算键的哈希值,这种算法的优点在于,即使输入的键是有规律的,算法仍能给出一个很好的随机分布性,并且算法的计算速度也非常快 +将一个新的键值对添加到字典里,需要先根据键 key 计算出哈希值,然后进行取模运算(取余): +```c +index = hash & dict->ht[x].sizemask +``` -##### MySQL5.7 +当有两个或以上数量的键被分配到了哈希表数组的同一个索引上时,就称这些键发生了哈希冲突(collision) -MySQL 5.7 并行复制策略的思想是: +Redis 的哈希表使用链地址法(separate chaining)来解决键哈希冲突, 每个哈希表节点都有一个 next 指针,多个节点通过 next 指针构成一个单向链表,被分配到同一个索引上的多个节点可以用这个单向链表连接起来,这就解决了键冲突的问题 -* 所有处于 commit 状态的事务可以并行执行 -* 同时处于 prepare 状态的事务,在从库执行时是可以并行的 -* 处于 prepare 状态的事务,与处于 commit 状态的事务之间,在从库执行时也是可以并行的 +dictEntry 节点组成的链表没有指向链表表尾的指针,为了速度考虑,程序总是将新节点添加到链表的表头位置(**头插法**),时间复杂度为 O(1) -MySQL 5.7 由参数 slave-parallel-type 来控制并行复制策略: +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-字典解决哈希冲突.png) -* 配置为 DATABASE,表示使用 MySQL 5.6 版本的**按库(DB)并行策略** -* 配置为 LOGICAL_CLOCK,表示的**按提交状态并行**执行 -MySQL 5.7.22 版本里,MySQL 增加了一个新的并行复制策略,基于 WRITESET 的并行复制。新增了一个参数 binlog-transaction-dependency-tracking,用来控制是否启用这个新策略: -* COMMIT_ORDER:表示根据同时进入 prepare 和 commit 来判断是否可以并行的策略 +**** -* WRITESET:表示的是对于每个事务涉及更新的每一行,计算出这一行的 hash 值,组成该事务的 writeset 集合,如果两个事务没有操作相同的行,也就是说它们的 writeset 没有交集,就可以并行(**按行并行**) -* WRITESET_SESSION:是在 WRITESET 的基础上多了一个约束,即在主库上同一个线程先后执行的两个事务,在备库执行的时候,要保证相同的先后顺序 - 为了唯一标识,这个 hash 表的值是通过 `库名 + 表名 + 索引名 + 值` (表示的是某一行)计算出来的 +#### 负载因子 -MySQL 5.7.22 按行并发的优势: +负载因子的计算方式:哈希表中的**节点数量** / 哈希表的大小(**长度**) -* writeset 是在主库生成后直接写入到 binlog 里面的,这样在备库执行的时候,不需要解析 binlog 内容,节省了计算量 -* 不需要把整个事务的 binlog 都扫一遍才能决定分发到哪个线程,更省内存 -* 从库的分发策略不依赖于 binlog 内容,所以 binlog 是 statement 格式也可以,更节约内存(因为 row 才记录更改的行) +```c +load_factor = ht[0].used / ht[0].size +``` -MySQL 5.7.22 的并行复制策略在通用性上是有保证的,但是对于表上没主键、唯一和外键约束的场景,WRITESET 策略也是没法并行的,也会暂时退化为单线程模型 +为了让哈希表的负载因子(load factor)维持在一个合理的范围之内,当哈希表保存的键值对数量太多或者太少时 ,程序会自动对哈希表的大小进行相应的扩展或者收缩 +哈希表执行扩容的条件: +* 服务器没有执行 BGSAVE 或者 BGREWRITEAOF 命令,哈希表的负载因子大于等于 1 -参考文章:https://time.geekbang.org/column/article/77083 +* 服务器正在执行 BGSAVE 或者 BGREWRITEAOF 命令,哈希表的负载因子大于等于 5 + 原因:执行该命令的过程中,Redis 需要创建当前服务器进程的子进程,而大多数操作系统都采用写时复制(copy-on­-write)技术来优化子进程的使用效率,通过提高执行扩展操作的负载因子,尽可能地避免在子进程存在期间进行哈希表扩展操作,可以避免不必要的内存写入操作,最大限度地节约内存 +哈希表执行收缩的条件:负载因子小于 0.1(自动执行,servreCron 中检测) -*** +*** -#### 读写分离 -读写分离:可以降低主库的访问压力,提高系统的并发能力 -* 主库不建查询的索引,从库建查询的索引。因为索引需要维护的,比如插入一条数据,不仅要在聚簇索引上面插入,对应的二级索引也得插入 -* 将读操作分到从库了之后,可以在主库把查询要用的索引删了,减少写操作对主库的影响 +#### 重新散列 -读写分离产生了读写延迟,造成数据的不一致性。假如客户端执行完一个更新事务后马上发起查询,如果查询选择的是从库的话,可能读到的还是以前的数据,叫过期读 +扩展和收缩哈希表的操作通过 rehash(重新散列)来完成,步骤如下: -解决方案: +* 为字典的 ht[1] 哈希表分配空间,空间大小的分配情况: + * 如果执行的是扩展操作,ht[1] 的大小为第一个大于等于 $ht[0].used * 2$ 的 $2^n$ + * 如果执行的是收缩操作,ht[1] 的大小为第一个大于等于 $ht[0].used$ 的 $2^n$ +* 将保存在 ht[0] 中所有的键值对重新计算哈希值和索引值,迁移到 ht[1] 上 +* 当 ht[0] 包含的所有键值对都迁移到了 ht[1] 之后(ht[0] 变为空表),释放 ht[0],将 ht[1] 设置为 ht[0],并在 ht[1] 创建一个新的空白哈希表,为下一次 rehash 做准备 -* 强制将写之后**立刻读的操作转移到主库**,比如刚注册的用户,直接登录从库查询可能查询不到,先走主库登录 +如果哈希表里保存的键值对数量很少,rehash 就可以在瞬间完成,但是如果哈希表里数据很多,那么要一次性将这些键值对全部 rehash 到 ht[1] 需要大量计算,可能会导致服务器在一段时间内停止服务 -* **二次查询**,如果从库查不到数据,则再去主库查一遍,由 API 封装,比较简单,但导致主库压力大 +Redis 对 rehash 做了优化,使 rehash 的动作并不是一次性、集中式的完成,而是分多次,渐进式的完成,又叫**渐进式 rehash** -* 更新主库后,读从库之前先 sleep 一下,类似于执行一条 `select sleep(1)` 命令 -* 确保主备无延迟的方法,每次从库执行查询请求前,先判断 seconds_behind_master 是否已经等于 0,如果不等于那就等到这个参数变为 0 才能执行查询请求 +* 为 ht[1] 分配空间,此时字典同时持有 ht[0] 和 ht[1] 两个哈希表 +* 在字典中维护了一个索引计数器变量 rehashidx,并将变量的值设为 0,表示 rehash 正式开始 +* 在 rehash 进行期间,每次对字典执行增删改查操作时,程序除了执行指定的操作以外,还会顺带将 ht[0] 哈希表在 rehashidx 索引上的所有键值对 rehash 到 ht[1],rehash 完成之后**将 rehashidx 属性的值增一** +* 随着字典操作的不断执行,最终在某个时间点 ht[0] 的所有键值对都被 rehash 至 ht[1],将 rehashidx 属性的值设为 -1 +渐进式 rehash 采用**分而治之**的方式,将 rehash 键值对所需的计算工作均摊到对字典的每个添加、删除、查找和更新操作上,从而避免了集中式 rehash 带来的庞大计算量 +渐进式 rehash 期间的哈希表操作: +* 字典的查找、删除、更新操作会在两个哈希表上进行,比如查找一个键会先在 ht[0] 上查找,查找不到就去 ht[1] 继续查找 +* 字典的添加操作会直接在 ht[1] 上添加,不在 ht[0] 上进行任何添加 -*** -### 负载均衡 +**** -负载均衡是应用中使用非常普遍的一种优化方法,机制就是利用某种均衡算法,将固定的负载量分布到不同的服务器上,以此来降低单台服务器的负载,达到优化的效果 -* 分流查询:通过 MySQL 的主从复制,实现读写分离,使增删改操作走主节点,查询操作走从节点,从而可以降低单台服务器的读写压力 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-负载均衡主从复制.jpg) +### 跳跃表 -* 分布式数据库架构:适合大数据量、负载高的情况,具有良好的拓展性和高可用性。通过在多台服务器之间分布数据,可以实现在多台服务器之间的负载均衡,提高访问效率 +#### 底层结构 +跳跃表(skiplist)是一种有序(**默认升序**)的数据结构,在链表的基础上**增加了多级索引以提升查找的效率**,索引是占内存的,所以是一个**空间换时间**的方案,跳表平均 O(logN)、最坏 O(N) 复杂度的节点查找,效率与平衡树相当但是实现更简单 +原始链表中存储的有可能是很大的对象,而索引结点只需要存储关键值和几个指针,并不需要存储对象,因此当节点本身比较大或者元素数量比较多的时候,其优势可以被放大,而缺点(占内存)则可以忽略 -**** +Redis 只在两个地方应用了跳跃表,一个是实现有序集合键,另一个是在集群节点中用作内部数据结构 +```c +typedef struct zskiplist { + // 表头节点和表尾节点,O(1) 的时间复杂度定位头尾节点 + struct skiplistNode *head, *tail; + + // 表的长度,也就是表内的节点数量 (表头节点不计算在内) + unsigned long length; + + // 表中层数最大的节点的层数 (表头节点的层高不计算在内) + int level +} zskiplist; +``` +```c +typedef struct zskiplistNode { + // 层 + struct zskiplistLevel { + // 前进指针 + struct zskiplistNode *forward; + // 跨度 + unsigned int span; + } level[]; + + // 后退指针 + struct zskiplistNode *backward; + + // 分值 + double score; + + // 成员对象 + robj *obj; +} zskiplistNode; +``` -### 主从搭建 +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-跳表底层结构.png) -#### 搭建流程 -##### master -1. 在master 的配置文件(/etc/mysql/my.cnf)中,配置如下内容: +*** - ```sh - #mysql 服务ID,保证整个集群环境中唯一 - server-id=1 - - #mysql binlog 日志的存储路径和文件名 - log-bin=/var/lib/mysql/mysqlbin - - #错误日志,默认已经开启 - #log-err - - #mysql的安装目录 - #basedir - - #mysql的临时目录 - #tmpdir - - #mysql的数据存放目录 - #datadir - - #是否只读,1 代表只读, 0 代表读写 - read-only=0 - - #忽略的数据, 指不需要同步的数据库 - binlog-ignore-db=mysql - - #指定同步的数据库 - #binlog-do-db=db01 - ``` -2. 执行完毕之后,需要重启 MySQL -3. 创建同步数据的账户,并且进行授权操作: +#### 属性分析 - ```mysql - GRANT REPLICATION SLAVE ON *.* TO 'seazean'@'192.168.0.137' IDENTIFIED BY '123456'; - FLUSH PRIVILEGES; - ``` +层:level 数组包含多个元素,每个元素包含指向其他节点的指针。根据幕次定律(power law,越大的数出现的概率越小)**随机**生成一个介于 1 和 32 之间的值(Redis5 之后最大为 64)作为 level 数组的大小,这个大小就是层的高度,节点的第一层是 level[0] = L1 -4. 查看 master 状态: +前进指针:forward 用于从表头到表尾方向**正序(升序)遍历节点**,遇到 NULL 停止遍历 - ```mysql - SHOW MASTER STATUS; - ``` +跨度:span 用于记录两个节点之间的距离,用来计算排位(rank): - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-查看master状态.jpg) +* 两个节点之间的跨度越大相距的就越远,指向 NULL 的所有前进指针的跨度都为 0 - * File:从哪个日志文件开始推送日志文件 - * Position:从哪个位置开始推送日志 - * Binlog_Ignore_DB:指定不需要同步的数据库 +* 在查找某个节点的过程中,**将沿途访问过的所有层的跨度累计起来,结果就是目标节点在跳跃表中的排位**,按照上图所示: + 查找分值为 3.0 的节点,沿途经历的层:查找的过程只经过了一个层,并且层的跨度为 3,所以目标节点在跳跃表中的排位为 3 + 查找分值为 2.0 的节点,沿途经历的层:经过了两个跨度为 1 的节点,因此可以计算出目标节点在跳跃表中的排位为 2 -*** +后退指针:backward 用于从表尾到表头方向**逆序(降序)遍历节点** +分值:score 属性一个 double 类型的浮点数,跳跃表中的所有节点都按分值从小到大来排序 +成员对象:obj 属性是一个指针,指向一个 SDS 字符串对象。同一个跳跃表中,各个节点保存的**成员对象必须是唯一的**,但是多个节点保存的分值可以是相同的,分值相同的节点将按照成员对象在字典序中的大小来进行排序(从小到大) -##### slave -1. 在 slave 端配置文件中,配置如下内容: - ```sh - #mysql服务端ID,唯一 - server-id=2 - - #指定binlog日志 - log-bin=/var/lib/mysql/mysqlbin - ``` +个人笔记:JUC → 并发包 → ConcurrentSkipListMap 详解跳跃表 -2. 执行完毕之后,需要重启 MySQL -3. 指定当前从库对应的主库的IP地址、用户名、密码,从哪个日志文件开始的那个位置开始同步推送日志 - ```mysql - CHANGE MASTER TO MASTER_HOST= '192.168.0.138', MASTER_USER='seazean', MASTER_PASSWORD='seazean', MASTER_LOG_FILE='mysqlbin.000001', MASTER_LOG_POS=413; - ``` +**** -4. 开启同步操作: - ```mysql - START SLAVE; - SHOW SLAVE STATUS; - ``` -5. 停止同步操作: +### 整数集合 - ```mysql - STOP SLAVE; - ``` +#### 底层结构 +整数集合(intset)是用于保存整数值的集合数据结构,是 Redis 集合键的底层实现之一 +```c +typedef struct intset { + // 编码方式 + uint32_t encoding; + + // 集合包含的元素数量,也就是 contents 数组的长度 + uint32_t length; + + // 保存元素的数组 + int8_t contents[]; +} intset; +``` -*** +encoding 取值为三种:INTSET_ENC_INT16、INTSET_ENC_INT32、INTSET_ENC_INT64 +整数集合的每个元素都是 contents 数组的一个数组项(item),在数组中按值的大小从小到大**有序排列**,并且数组中**不包含任何重复项**。虽然 contents 属性声明为 int8_t 类型,但实际上数组并不保存任何 int8_t 类型的值, 真正类型取决于 encoding 属性 +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-整数集合底层结构.png) -##### 验证 +说明:底层存储结构是数组,所以为了保证有序性和不重复性,每次添加一个元素的时间复杂度是 O(N) -1. 在主库中创建数据库,创建表并插入数据: - ```mysql - CREATE DATABASE db01; - USE db01; - CREATE TABLE user( - id INT(11) NOT NULL AUTO_INCREMENT, - name VARCHAR(50) NOT NULL, - sex VARCHAR(1), - PRIMARY KEY (id) - )ENGINE=INNODB DEFAULT CHARSET=utf8; - - INSERT INTO user(id,NAME,sex) VALUES(NULL,'Tom','1'); - INSERT INTO user(id,NAME,sex) VALUES(NULL,'Trigger','0'); - INSERT INTO user(id,NAME,sex) VALUES(NULL,'Dawn','1'); - ``` -2. 在从库中查询数据,进行验证: +**** + - 在从库中,可以查看到刚才创建的数据库: - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-主从复制验证1.jpg) +#### 类型升级 - 在该数据库中,查询表中的数据: +整数集合添加的新元素的类型比集合现有所有元素的类型都要长时,需要先进行升级(upgrade),升级流程: - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-主从复制验证2.jpg) +* 根据新元素的类型长度以及集合元素的数量(包括新元素在内),扩展整数集合底层数组的空间大小 +* 将底层数组现有的所有元素都转换成与新元素相同的类型,并将转换后的元素放入正确的位置,放置过程保证数组的有序性 + 图示 32 * 4 = 128 位,首先将 3 放入索引 2(64 位 - 95 位),然后将 2 放置索引 1,将 1 放置在索引 0,从后向前依次放置在对应的区间,最后放置 65535 元素到索引 3(96 位- 127 位),修改 length 属性为 4 -*** +* 将新元素添加到底层数组里 +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-整数集合升级.png) +每次向整数集合添加新元素都可能会引起升级,而每次升级都需要对底层数组中的所有元素进行类型转换,所以向整数集合添加新元素的时间复杂度为 O(N) -#### 主从切换 +引发升级的新元素的长度总是比整数集合现有所有元素的长度都大,所以这个新元素的值要么就大于所有现有元素,要么就小于所有现有元素,升级之后新元素的摆放位置: -正常切换步骤: +* 在新元素小于所有现有元素的情况下,新元素会被放置在底层数组的最开头(索引 0) +* 在新元素大于所有现有元素的情况下,新元素会被放置在底层数组的最末尾(索引 length-1) -* 在开始切换之前先对主库进行锁表 `flush tables with read lock`,然后等待所有语句执行完成,切换完成后可以释放锁 +整数集合升级策略的优点: -* 检查 slave 同步状态,在 slave 执行 `show processlist` +* 提升整数集合的灵活性:C 语言是静态类型语言,为了避免类型错误通常不会将两种不同类型的值放在同一个数据结构里面,整数集合可以自动升级底层数组来适应新元素,所以可以随意的添加整数 -* 停止 slave io 线程,执行命令 `STOP SLAVE IO_THREAD` +* 节约内存:要让数组可以同时保存 int16、int32、int64 三种类型的值,可以直接使用 int64_t 类型的数组作为整数集合的底层实现,但是会造成内存浪费,整数集合可以确保升级操作只会在有需要的时候进行,尽量节省内存 -* 提升 slave 为 master +整数集合**不支持降级操作**,一旦对数组进行了升级,编码就会一直保持升级后的状态 - ```sql - Stop slave; - Reset master; - Reset slave all; - set global read_only=off; -- 设置为可更新状态 - ``` -* 将原来 master 变为 slave(参考搭建流程中的 slave 方法) -主库发生故障,从库会进行上位,其他从库指向新的主库 +***** -**** +### 压缩列表 +#### 底层结构 +压缩列表(ziplist)是 Redis 为了节约内存而开发的,是列表键和哈希键的底层实现之一。是由一系列特殊编码的连续内存块组成的顺序型(sequential)数据结构,一个压缩列表可以包含任意多个节点(entry),每个节点可以保存一个字节数组或者一个整数值 +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-压缩列表底层结构.png) +* zlbytes:uint32_t 类型 4 字节,记录整个压缩列表占用的内存字节数,在对压缩列表进行内存重分配或者计算 zlend 的位置时使用 +* zltail:uint32_t 类型 4 字节,记录压缩列表表尾节点距离起始地址有多少字节,通过这个偏移量程序无须遍历整个压缩列表就可以确定表尾节点的地址 +* zllen:uint16_t 类型 2 字节,记录了压缩列表包含的节点数量,当该属性的值小于 UINT16_MAX (65535) 时,该值就是压缩列表中节点的数量;当这个值等于 UINT16_MAX 时节点的真实数量需要遍历整个压缩列表才能计算得出 +* entryX:列表节点,压缩列表中的各个节点,**节点的长度由节点保存的内容决定** +* zlend:uint8_t 类型 1 字节,是一个特殊值 0xFF (255),用于标记压缩列表的末端 -## 锁机制 +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-压缩列表示例.png) -### 基本介绍 +列表 zlbytes 属性的值为 0x50 (十进制 80),表示压缩列表的总长为 80 字节,列表 zltail 属性的值为 0x3c (十进制 60),假设表的起始地址为 p,计算得出表尾节点 entry3 的地址 p + 60 -锁机制:数据库为了保证数据的一致性,在共享的资源被并发访问时变得安全有序所设计的一种规则 -作用:锁机制类似于多线程中的同步,可以保证数据的一致性和安全性 -锁的分类: +**** -- 按操作分类: - - 共享锁:也叫读锁。对同一份数据,多个事务读操作可以同时加锁而不互相影响 ,但不能修改数据 - - 排他锁:也叫写锁。当前的操作没有完成前,会阻断其他操作的读取和写入 -- 按粒度分类: - - 表级锁:会锁定整个表,开销小,加锁快;不会出现死锁;锁定力度大,发生锁冲突概率高,并发度最低,偏向 MyISAM - - 行级锁:会锁定当前操作行,开销大,加锁慢;会出现死锁;锁定力度小,发生锁冲突概率低,并发度高,偏向 InnoDB - - 页级锁:锁的力度、发生冲突的概率和加锁开销介于表锁和行锁之间,会出现死锁,并发性能一般 -- 按使用方式分类: - - 悲观锁:每次查询数据时都认为别人会修改,很悲观,所以查询时加锁 - - 乐观锁:每次查询数据时都认为别人不会修改,很乐观,但是更新时会判断一下在此期间别人有没有去更新这个数据 -* 不同存储引擎支持的锁 - | 存储引擎 | 表级锁 | 行级锁 | 页级锁 | - | -------- | -------- | -------- | ------ | - | MyISAM | 支持 | 不支持 | 不支持 | - | InnoDB | **支持** | **支持** | 不支持 | - | MEMORY | 支持 | 不支持 | 不支持 | - | BDB | 支持 | 不支持 | 支持 | +#### 列表节点 -从锁的角度来说:表级锁更适合于以查询为主,只有少量按索引条件更新数据的应用,如 Web 应用;而行级锁则更适合于有大量按索引条件并发更新少量不同数据,同时又有并查询的应用,如一些在线事务处理系统 +列表节点 entry 的数据结构: +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-压缩列表节点.png) +previous_entry_length:以字节为单位记录了压缩列表中前一个节点的长度,程序可以通过指针运算,根据当前节点的起始地址来计算出前一个节点的起始地址,完成**从表尾向表头遍历**操作 -*** +* 如果前一节点的长度小于 254 字节,该属性的长度为 1 字节,前一节点的长度就保存在这一个字节里 +* 如果前一节点的长度大于等于 254 字节,该属性的长度为 5 字节,其中第一字节会被设置为 0xFE(十进制 254),之后的四个字节则用于保存前一节点的长度 +encoding:记录了节点的 content 属性所保存的数据类型和长度 +* **长度为 1 字节、2 字节或者 5 字节**,值的最高位为 00、01 或者 10 的是字节数组编码,数组的长度由编码除去最高两位之后的其他位记录,下划线 `_` 表示留空,而 `b`、`x` 等变量则代表实际的二进制数据 -### Server + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-压缩列表字节数组编码.png) -FLUSH TABLES WITH READ LOCK 简称(FTWRL),全局读锁,让整个库处于只读状态,工作流程: +* 长度为 1 字节,值的最高位为 11 的是整数编码,整数值的类型和长度由编码除去最高两位之后的其他位记录 -1. 上全局读锁(lock_global_read_lock) -2. 清理表缓存(close_cached_tables) -3. 上全局 COMMIT 锁(make_global_read_lock_block_commit) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-压缩列表整数编码.png) -该命令主要用于备份工具做一致性备份,由于 FTWRL 需要持有两把全局的 MDL 锁,并且还要关闭所有表对象,因此杀伤性很大 +content:每个压缩列表节点可以保存一个字节数组或者一个整数值 -MySQL 里面表级别的锁有两种:一种是表锁,一种是元数据锁(meta data lock,MDL) +* 字节数组可以是以下三种长度的其中一种: -MDL 叫元数据锁,主要用来保护 MySQL内部对象的元数据,保证数据读写的正确性,通过 MDL 机制保证 DDL、DML、DQL 操作的并发,**当对一个表做增删改查操作的时候,加 MDL 读锁;当要对表做结构变更操作的时候,加 MDL 写锁** + * 长度小于等于 $63 (2^6-1)$ 字节的字节数组 -* MDL 锁不需要显式使用,在访问一个表的时候会被自动加上,事务中的 MDL 锁,在语句执行开始时申请,在整个事务提交后释放 + * 长度小于等于 $16383(2^{14}-1)$ 字节的字节数组 -* MDL 锁是在 Server 中实现,不是 InnoDB 存储引擎层不能直接实现的锁 + * 长度小于等于 $4294967295(2^{32}-1)$ 字节的字节数组 -* MDL 锁还能实现其他粒度级别的锁,比如全局锁、库级别的锁、表空间级别的锁 +* 整数值则可以是以下六种长度的其中一种: + * 4 位长,介于 0 至 12 之间的无符号整数 + * 1 字节长的有符号整数 + + * 3 字节长的有符号整数 + + * int16_t 类型整数 + + * int32_t 类型整数 + + * int64_t 类型整数 @@ -6290,98 +11276,80 @@ MDL 叫元数据锁,主要用来保护 MySQL内部对象的元数据,保证 -### MyISAM +#### 连锁更新 -#### 表级锁 +Redis 将在特殊情况下产生的连续多次空间扩展操作称之为连锁更新(cascade update) -MyISAM 存储引擎只支持表锁,这也是 MySQL 开始几个版本中唯一支持的锁类型 +假设在一个压缩列表中,有多个连续的、长度介于 250 到 253 字节之间的节点 e1 至 eN。将一个长度大于等于 254 字节的新节点 new 设置为压缩列表的头节点,new 就成为 e1 的前置节点。e1 的 previous_entry_length 属性仅为 1 字节,无法保存新节点 new 的长度,所以要对压缩列表执行空间重分配操作,并将 e1 节点的 previous_entry_length 属性从 1 字节长扩展为 5 字节长。由于 e1 原本的长度介于 250 至 253 字节之间,所以扩展后 e1 的长度就变成了 254 至 257 字节之间,导致 e2 的 previous_entry_length 属性无法保存 e1 的长度,程序需要不断地对压缩列表执行空间重分配操作,直到 eN 为止 -MyISAM 引擎在执行查询语句之前,会**自动**给涉及到的所有表加读锁,在执行增删改之前,会**自动**给涉及的表加写锁,这个过程并不需要用户干预,所以用户一般不需要直接用 LOCK TABLE 命令给 MyISAM 表显式加锁 +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-压缩列表连锁更新1.png) -* 加锁命令: + 删除节点也可能会引发连锁更新,big.length >= 254,small.length < 254,删除 small 节点 - 读锁:所有连接只能读取数据,不能修改 +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-压缩列表连锁更新2.png) - 写锁:其他连接不能查询和修改数据 +连锁更新在最坏情况下需要对压缩列表执行 N 次空间重分配,每次重分配的最坏复杂度为 O(N),所以连锁更新的最坏复杂度为 O(N^2) - ```mysql - -- 读锁 - LOCK TABLE table_name READ; - - -- 写锁 - LOCK TABLE table_name WRITE; - ``` +说明:尽管连锁更新的复杂度较高,但出现的记录是非常低的,即使出现只要被更新的节点数量不多,就不会对性能造成影响 -* 解锁命令: - ```mysql - -- 将当前会话所有的表进行解锁 - UNLOCK TABLES; - ``` -锁的兼容性: -* 对 MyISAM 表的读操作,不会阻塞其他用户对同一表的读请求,但会阻塞对同一表的写请求 -* 对 MyISAM 表的写操作,则会阻塞其他用户对同一表的读和写操作 -![](https://gitee.com/seazean/images/raw/master/DB/MySQL-MyISAM 锁的兼容性.png) +**** -锁调度:MyISAM 的读写锁调度是写优先,因为写锁后其他线程不能做任何操作,大量的更新会使查询很难得到锁,从而造成永远阻塞,所以 MyISAM 不适合做写为主的表的存储引擎 -*** +## 数据类型 +### redisObj -#### 锁操作 +#### 对象系统 -##### 读锁 +Redis 使用对象来表示数据库中的键和值,当在 Redis 数据库中新创建一个键值对时至少会创建两个对象,一个对象用作键值对的键(**键对象**),另一个对象用作键值对的值(**值对象**) -两个客户端操作 Client 1和 Client 2,简化为 C1、C2 +Redis 中对象由一个 redisObject 结构表示,该结构中和保存数据有关的三个属性分别是 type、 encoding、ptr: -* 数据准备: +```c +typedef struct redisObiect { + // 类型 + unsigned type:4; + // 编码 + unsigned encoding:4; + // 指向底层数据结构的指针 + void *ptr; + + // .... +} robj; +``` - ```mysql - CREATE TABLE `tb_book` ( - `id` INT(11) AUTO_INCREMENT, - `name` VARCHAR(50) DEFAULT NULL, - `publish_time` DATE DEFAULT NULL, - `status` CHAR(1) DEFAULT NULL, - PRIMARY KEY (`id`) - ) ENGINE=MYISAM DEFAULT CHARSET=utf8 ; - - INSERT INTO tb_book (id, NAME, publish_time, STATUS) VALUES(NULL,'java编程思想','2088-08-01','1'); - INSERT INTO tb_book (id, NAME, publish_time, STATUS) VALUES(NULL,'mysql编程思想','2088-08-08','0'); - ``` +Redis 并没有直接使用数据结构来实现键值对数据库,而是基于这些数据结构创建了一个对象系统,包含字符串对象、列表对象、哈希对象、集合对象和有序集合对象这五种类型的对象,而每种对象又通过不同的编码映射到不同的底层数据结构 -* C1、C2 加读锁,同时查询可以正常查询出数据 +Redis 是一个 Map 类型,其中所有的数据都是采用 key : value 的形式存储,**键对象都是字符串对象**,而值对象有五种基本类型和三种高级类型对象 - ```mysql - LOCK TABLE tb_book READ; -- C1、C2 - SELECT * FROM tb_book; -- C1、C2 - ``` +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-对象编码.png) - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-MyISAM 读锁1.png) +* 对一个数据库键执行 TYPE 命令,返回的结果为数据库键对应的值对象的类型,而不是键对象的类型 +* 对一个数据库键执行 OBJECT ENCODING 命令,查看数据库键对应的值对象的编码 -* C1 加读锁,C1、C2查询未锁定的表,C1 报错,C2 正常查询 - ```mysql - LOCK TABLE tb_book READ; -- C1 - SELECT * FROM tb_user; -- C1、C2 - ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-MyISAM 读锁2.png) +**** - C1、C2 执行插入操作,C1 报错,C2 等待获取 - ```mysql - INSERT INTO tb_book VALUES(NULL,'Spring高级','2088-01-01','1'); -- C1、C2 - ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-MyISAM 读锁3.png) +#### 命令多态 - 当在 C1 中释放锁指令 UNLOCK TABLES,C2 中的 INSERT 语句立即执行 +Redis 中用于操作键的命令分为两种类型: + +* 一种命令可以对任何类型的键执行,比如说 DEL 、EXPIRE、RENAME、 TYPE 等(基于类型的多态) +* 只能对特定类型的键执行,比如 SET 只能对字符串键执行、HSET 对哈希键执行、SADD 对集合键执行,如果类型步匹配会报类型错误: `(error) WRONGTYPE Operation against a key holding the wrong kind of value` + +Redis 为了确保只有指定类型的键可以执行某些特定的命令,在执行类型特定的命令之前,先通过值对象 redisObject 结构 type 属性检查操作类型是否正确,然后再决定是否执行指定的命令 + +对于多态命令,比如列表对象有 ziplist 和 linkedlist 两种实现方式,通过 redisObject 结构 encoding 属性确定具体的编码类型,底层调用对应的 API 实现具体的操作(基于编码的多态) @@ -6389,103 +11357,108 @@ MyISAM 引擎在执行查询语句之前,会**自动**给涉及到的所有表 -##### 写锁 +#### 内存回收 -两个客户端操作 Client 1和 Client 2,简化为 C1、C2 +对象的整个生命周期可以划分为创建对象、 操作对象、 释放对象三个阶段 -* C1 加写锁,C1、C2查询表,C1 正常查询,C2 需要等待 +C 语言没有自动回收内存的功能,所以 Redis 在对象系统中构建了引用计数(reference counting)技术实现的内存回收机制,程序可以跟踪对象的引用计数信息,在适当的时候自动释放对象并进行内存回收 - ```mysql - LOCK TABLE tb_book WRITE; -- C1 - SELECT * FROM tb_book; -- C1、C2 - ``` +```c +typedef struct redisObiect { + // 引用计数 + int refcount; +} robj; +``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-MyISAM 写锁1.png) +对象的引用计数信息会随着对象的使用状态而不断变化,创建时引用计数 refcount 初始化为 1,每次被一个新程序使用时引用计数加 1,当对象不再被一个程序使用时引用计数值会被减 1,当对象的引用计数值变为 0 时,对象所占用的内存会被释放 - 当在 C1 中释放锁指令 UNLOCK TABLES,C2 中的 SELECT 语句立即执行 -* C1、C2 同时加写锁 - ```mysql - LOCK TABLE tb_book WRITE; - ``` +*** - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-MyISAM 写锁2.png) -* C1 加写锁,C1、C2查询未锁定的表,C1 报错,C2 正常查询 +#### 对象共享 +对象的引用计数属性带有对象共享的作用,共享对象机制更节约内存,数据库中保存的相同值对象越多,节约的内存就越多 -*** +让多个键共享一个对象的步骤: +* 将数据库键的值指针指向一个现有的值对象 +* 将被共享的值对象的引用计数增一 -#### 锁状态 + -* 查看锁竞争: +Redis 在初始化服务器时创建一万个(配置文件可以修改)字符串对象,包含了**从 0 到 9999 的所有整数值**,当服务器需要用到值为 0 到 9999 的字符串对象时,服务器就会使用这些共享对象,而不是新创建对象 - ```mysql - SHOW OPEN TABLES; - ``` +比如创建一个值为 100 的键 A,并使用 OBJECT REFCOUNT 命令查看键 A 的值对象的引用计数,会发现值对象的引用计数为 2,引用这个值对象的两个程序分别是持有这个值对象的服务器程序,以及共享这个值对象的键 A - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-锁争用情况查看1.png) +共享对象在嵌套了字符串对象的对象(linkedlist 编码的列表、hashtable 编码的哈希、zset 编码的有序集合)中也能使用 - In_user:表当前被查询使用的次数,如果该数为零,则表是打开的,但是当前没有被使用 +Redis 不共享包含字符串对象的原因:验证共享对象和目标对象是否相同的复杂度越高,消耗的 CPU 时间也会越多 - Name_locked:表名称是否被锁定,名称锁定用于取消表或对表进行重命名等操作 +* 整数值的字符串对象, 验证操作的复杂度为 O(1) +* 字符串值的字符串对象, 验证操作的复杂度为 O(N) +* 如果共享对象是包含了多个值(或者对象的)对象,比如列表对象或者哈希对象,验证操作的复杂度为 O(N^2) - ```mysql - LOCK TABLE tb_book READ; -- 执行命令 - ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-锁争用情况查看2.png) -* 查看锁状态: +**** - ```mysql - SHOW STATUS LIKE 'Table_locks%'; - ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-MyISAM 锁状态.png) - Table_locks_immediate:指的是能立即获得表级锁的次数,每立即获取锁,值加 1 +#### 空转时长 - Table_locks_waited:指的是不能立即获取表级锁而需要等待的次数,每等待一次,该值加 1,此值高说明存在着较为严重的表级锁争用情况 +redisObject 结构包含一个 lru 属性,该属性记录了对象最后一次被命令程序访问的时间 +```c +typedef struct redisObiect { + unsigned lru:22; +} robj; +``` +OBJECT IDLETIME 命令可以打印出给定键的空转时长,该值就是通过将当前时间减去键的值对象的 lru 时间计算得出的,这个命令在访问键的值对象时,不会修改值对象的 lru 属性 -*** +```sh +redis> OBJECT IDLETIME msg +(integer) 10 +# 等待一分钟 +redis> OBJECT IDLETIME msg +(integer) 70 +# 访问 msg +redis> GET msg +"hello world" +# 键处于活跃状态,空转时长为 0 +redis> OBJECT IDLETIME msg +(integer) 0 +``` + +空转时长的作用:如果服务器开启 maxmemory 选项,并且回收内存的算法为 volatile-lru 或者 allkeys-lru,那么当服务器占用的内存数超过了 maxmemory 所设置的上限值时,空转时长较高的那部分键会优先被服务器释放,从而回收内存(LRU 算法) -### InnoDB -#### 行级锁 -InnoDB 与 MyISAM 的**最大不同**有两点:一是支持事务;二是采用了行级锁,InnoDB 同时支持表锁和行锁 +*** -InnoDB 实现了以下两种类型的行锁: -- 共享锁 (S):又称为读锁,简称 S 锁,就是多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改 -- 排他锁 (X):又称为写锁,简称 X 锁,就是不能与其他锁并存,如一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁,包括共享锁和排他锁,只有获取排他锁的事务是可以对数据读取和修改 -对于 UPDATE、DELETE 和 INSERT 语句,InnoDB 会**自动给涉及数据集加排他锁**(行锁),在 commit 的时候会自动释放;对于普通 SELECT 语句,不会加任何锁 +### string -锁的兼容性: +#### 简介 -- 共享锁和共享锁 兼容 -- 共享锁和排他锁 冲突 -- 排他锁和排他锁 冲突 -- 排他锁和共享锁 冲突 +存储的数据:单个数据,最简单的数据存储类型,也是最常用的数据存储类型,实质上是存一个字符串,string 类型是二进制安全的,可以包含任何数据,比如图片或者序列化的对象 -可以通过以下语句显式给数据集加共享锁或排他锁: +存储数据的格式:一个存储空间保存一个数据,每一个空间中只能保存一个字符串信息 -```mysql -SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE -- 共享锁 -SELECT * FROM table_name WHERE ... FOR UPDATE -- 排他锁 -``` +存储内容:通常使用字符串,如果字符串以整数的形式展示,可以作为数字操作使用 + + +Redis 所有操作都是**原子性**的,采用**单线程**机制,命令是单个顺序执行,无需考虑并发带来影响,原子性就是有一个失败则都失败 +字符串对象可以是 int、raw、embstr 三种实现方式 @@ -6493,143 +11466,166 @@ SELECT * FROM table_name WHERE ... FOR UPDATE -- 排他锁 -#### 锁操作 +#### 操作 -两个客户端操作 Client 1和 Client 2,简化为 C1、C2 +指令操作: -* 环境准备 +* 数据操作: - ```mysql - CREATE TABLE test_innodb_lock( - id INT(11), - name VARCHAR(16), - sex VARCHAR(1) - )ENGINE = INNODB DEFAULT CHARSET=utf8; - - INSERT INTO test_innodb_lock VALUES(1,'100','1'); - -- .......... - - CREATE INDEX idx_test_innodb_lock_id ON test_innodb_lock(id); - CREATE INDEX idx_test_innodb_lock_name ON test_innodb_lock(name); + ```sh + set key value #添加/修改数据添加/修改数据 + del key #删除数据 + setnx key value #判定性添加数据,键值为空则设添加 + mset k1 v1 k2 v2... #添加/修改多个数据,m:Multiple + append key value #追加信息到原始信息后部(如果原始信息存在就追加,否则新建) ``` -* 关闭自动提交功能: +* 查询操作 - ```mysql - SET AUTOCOMMIT=0; -- C1、C2 + ```sh + get key #获取数据,如果不存在,返回空(nil) + mget key1 key2... #获取多个数据 + strlen key #获取数据字符个数(字符串长度) ``` - 正常查询数据: +* 设置数值数据增加/减少指定范围的值 - ```mysql - SELECT * FROM test_innodb_lock; -- C1、C2 + ```sh + incr key #key++ + incrby key increment #key+increment + incrbyfloat key increment #对小数操作 + decr key #key-- + decrby key increment #key-increment ``` -* 查询 id 为 3 的数据,正常查询: +* 设置数据具有指定的生命周期 - ```mysql - SELECT * FROM test_innodb_lock WHERE id=3; -- C1、C2 + ```sh + setex key seconds value #设置key-value存活时间,seconds单位是秒 + psetex key milliseconds value #毫秒级 ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-InnoDB 锁操作1.png) +注意事项: -* C1 更新 id 为 3 的数据,但不提交: +1. 数据操作不成功的反馈与数据正常操作之间的差异 - ```mysql - UPDATE test_innodb_lock SET name='300' WHERE id=3; -- C1 - ``` + * 表示运行结果是否成功 + + * (integer) 0 → false ,失败 + + * (integer) 1 → true,成功 + + * 表示运行结果值 + + * (integer) 3 → 3 个 + + * (integer) 1 → 1 个 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-InnoDB 锁操作2.png) +2. 数据未获取到时,对应的数据为(nil),等同于null + +3. **数据最大存储量**:512MB + +4. string 在 Redis 内部存储默认就是一个字符串,当遇到增减类操作 incr,decr 时**会转成数值型**进行计算 + +5. 按数值进行操作的数据,如果原始数据不能转成数值,或超越了Redis 数值上限范围,将报错 + 9223372036854775807(java 中 Long 型数据最大值,Long.MAX_VALUE) + +6. Redis 可用于控制数据库表主键 ID,为数据库表主键提供生成策略,保障数据库表的主键唯一性 + + +单数据和多数据的选择: + +* 单数据执行 3 条指令的过程:3 次发送 + 3 次处理 + 3 次返回 +* 多数据执行 1 条指令的过程:1 次发送 + 3 次处理 + 1 次返回(发送和返回的事件略高于单数据) + + + + + + + +*** - C2 查询不到 C1 修改的数据,因为隔离界别为 REPEATABLE READ,C1 提交事务,C2 查询: - ```mysql - COMMIT; -- C1 - ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-InnoDB 锁操作3.png) +#### 实现 - 提交后仍然查询不到 C1 修改的数据,因为隔离级别可以防止脏读、不可重复读,所以 C2 需要提交才可以查询到其他事务对数据的修改: +字符串对象的编码可以是 int、raw、embstr 三种 - ```mysql - COMMIT; -- C2 - SELECT * FROM test_innodb_lock WHERE id=3; -- C2 - ``` +* int:字符串对象保存的是**整数值**,并且整数值可以用 long 类型来表示,那么对象会将整数值保存在字符串对象结构的 ptr 属性面(将 void * 转换成 long),并将字符串对象的编码设置为 int(浮点数用另外两种方式) - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-InnoDB 锁操作4.png) + -* C1 更新 id 为 3 的数据,但不提交,C2 也更新 id 为 3 的数据: +* raw:字符串对象保存的是一个字符串值,并且值的长度大于 39 字节,那么对象将使用简单动态字符串(SDS)来保存该值,并将对象的编码设置为 raw - ```mysql - UPDATE test_innodb_lock SET name='3' WHERE id=3; -- C1 - UPDATE test_innodb_lock SET name='30' WHERE id=3; -- C2 - ``` + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-字符串对象raw编码.png) - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-InnoDB 锁操作5.png) +* embstr:字符串对象保存的是一个字符串值,并且值的长度小于等于 39 字节,那么对象将使用 embstr 编码的方式来保存这个字符串值,并将对象的编码设置为 embstr - 当 C1 提交,C2 直接解除阻塞,直接更新 + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-字符串对象embstr编码.png) -* 操作不同行的数据: + 上图所示,embstr 与 raw 都使用了 redisObject 和 sdshdr 来表示字符串对象,但是 raw 需要调用两次内存分配函数分别创建两种结构,embstr 只需要一次内存分配来分配一块**连续的空间** - ```mysql - UPDATE test_innodb_lock SET name='10' WHERE id=1; -- C1 - UPDATE test_innodb_lock SET name='30' WHERE id=3; -- C2 - ``` +embstr 是用于保存短字符串的一种编码方式,对比 raw 的优点: - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-InnoDB 锁操作6.png) +* 内存分配次数从两次降低为一次,同样释放内存的次数也从两次变为一次 +* embstr 编码的字符串对象的数据都保存在同一块连续内存,所以比 raw 编码能够更好地利用缓存优势(局部性原理) - 由于C1、C2 操作的不同行,获取不同的行锁,所以都可以正常获取行锁 +int 和 embstr 编码的字符串对象在条件满足的情况下,会被转换为 raw 编码的字符串对象: +* int 编码的整数值,执行 APPEND 命令追加一个字符串值,先将整数值转为字符串然后追加,最后得到一个 raw 编码的对象 +* Redis 没有为 embstr 编码的字符串对象编写任何相应的修改程序,所以 embstr 对象实际上**是只读的**,执行修改命令会将对象的编码从 embstr 转换成 raw,操作完成后得到一个 raw 编码的对象 +某些情况下,程序会将字符串对象里面的字符串值转换回浮点数值,执行某些操作后再将浮点数值转换回字符串值: -​ +```sh +redis> SET pi 3.14 +OK +redis> OBJECT ENCODING pi +"embstr" +redis> INCRBYFLOAT pi 2.0 # 转为浮点数执行增加的操作 +"5. 14" +redis> OBJECT ENCODING pi +"embstr" +``` -*** -#### 锁分类 -##### 间隙锁 -当使用范围条件检索数据,并请求共享或排他锁时,InnoDB 会给符合条件的已有数据进行加锁,对于键值在条件范围内但并不存在的记录,叫做间隙(GAP), InnoDB 会对间隙进行加锁,就是间隙锁 -* 唯一索引加锁只有在值存在时才是行锁,值不存在会变成间隙锁,所以范围查询时容易出现间隙锁 -* 对于联合索引且是唯一索引,如果 where 条件只包括联合索引的一部分,那么会加间隙锁 +**** -加锁的基本单位是 next-key lock,该锁是行锁和这条记录前面的 gap lock 的组合,就是行锁加间隙锁 -* 加锁遵循前开后闭原则 -* 假设有索引值 10、11、13,那么可能的间隙锁包括:(负无穷,10]、(10,11]、(11,13]、(13,20,正无穷),锁住索引 11 会同时对间隙 (10,11]、(11,13] 加锁 -间隙锁优点:RR 级别下间隙锁可以解决事务的**幻读问题**,通过对间隙加锁,防止读取过程中数据条目发生变化 +#### 应用 -间隙锁危害:当锁定一个范围的键值后,即使某些不存在的键值也会被无辜的锁定,造成在锁定的时候无法插入锁定键值范围内的任何数据,在某些场景下这可能会对性能造成很大的危害 +主页高频访问信息显示控制,例如新浪微博大 V 主页显示粉丝数与微博数量 -* 关闭自动提交功能: +* 在 Redis 中为大 V 用户设定用户信息,以用户主键和属性值作为 key,后台设定定时刷新策略 - ```mysql - SET AUTOCOMMIT=0; -- C1、C2 + ```sh + set user:id:3506728370:fans 12210947 + set user:id:3506728370:blogs 6164 + set user:id:3506728370:focuses 83 ``` -* 查询数据表: +* 使用 JSON 格式保存数据 - ```mysql - SELECT * FROM test_innodb_lock; + ```sh + user:id:3506728370 → {"fans":12210947,"blogs":6164,"focuses":83} ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-InnoDB 间隙锁1.png) - -* C1 根据 id 范围更新数据,C2 插入数据: +* key的设置约定:表名 : 主键名 : 主键值 : 字段名 - ```mysql - UPDATE test_innodb_lock SET name='8888' WHERE id < 4; -- C1 - INSERT INTO test_innodb_lock VALUES(2,'200','2'); -- C2 - ``` + | 表名 | 主键名 | 主键值 | 字段名 | + | ----- | ------ | --------- | ------ | + | order | id | 29437595 | name | + | equip | id | 390472345 | type | + | news | id | 202004150 | title | - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-InnoDB 间隙锁2.png) - 出现间隙锁,C2 被阻塞,等待 C1 提交事务后才能更新 @@ -6637,21 +11633,24 @@ SELECT * FROM table_name WHERE ... FOR UPDATE -- 排他锁 -##### 意向锁 +### hash -InnoDB 为了支持多粒度的加锁,允许行锁和表锁同时存在,支持在不同粒度上的加锁操作,InnoDB 增加了意向锁(Intention Lock ) +#### 简介 -意向锁是将锁定的对象分为多个层次,意向锁意味着事务希望在更细粒度上进行加锁,意向锁分为两种: +数据存储需求:对一系列存储的数据进行编组,方便管理,典型应用存储对象信息 + +数据存储结构:一个存储空间保存多个键值对数据 -* 意向共享锁(IS):事务有意向对表中的某些行加共享锁 +hash 类型:底层使用**哈希表**结构实现数据存储 -* 意向排他锁(IX):事务有意向对表中的某些行加排他锁 + -InnoDB 存储引擎支持的是行级别的锁,因此意向锁不会阻塞除全表扫描以外的任何请求,表级意向锁与行级锁的兼容性如下所示: +Redis 中的 hash 类似于 Java 中的 `Map>`,左边是 key,右边是值,中间叫 field 字段,本质上 **hash 存了一个 key-value 的存储空间** -![](https://gitee.com/seazean/images/raw/master/DB/MySQL-意向锁兼容性.png) +hash 是指的一个数据类型,并不是一个数据 -插入意向锁是在插入一行记录操作之前设置的一种间隙锁,这个锁释放了一种插入方式的信号,即多个事务在相同的索引间隙插入时如果不是插入间隙中相同的位置就不需要互相等待。假设某列有索引值2,6,只要两个事务插入位置不同,如事务 A 插入 3,事务 B 插入 4,那么就可以同时插入 +* 如果 field 数量较少,存储结构优化为**压缩列表结构**(有序) +* 如果 field 数量较多,存储结构使用 HashMap 结构(无序) @@ -6659,51 +11658,77 @@ InnoDB 存储引擎支持的是行级别的锁,因此意向锁不会阻塞除 -##### 死锁 +#### 操作 -当并发系统中不同线程出现循环资源依赖,线程都在等待别的线程释放资源时,就会导致这几个线程都进入无限等待的状态,称为死锁 +指令操作: -死锁情况:线程 A 修改了 id = 1 的数据,请求修改 id = 2 的数据,线程 B 修改了 id = 2 的数据,请求修改 id = 1 的数据,产生死锁 +* 数据操作 -解决策略: + ```sh + hset key field value #添加/修改数据 + hdel key field1 [field2] #删除数据,[]代表可选 + hsetnx key field value #设置field的值,如果该field存在则不做任何操作 + hmset key f1 v1 f2 v2... #添加/修改多个数据 + ``` -* 直接进入等待直到超时,超时时间可以通过参数 innodb_lock_wait_timeout 来设置,但是时间的设置不好控制,超时可能不是因为死锁,而是因为事务处理比较慢,所以一般不采取该方式 -* 主动死锁检测,发现死锁后**主动回滚死锁链条中的某一个事务**,让其他事务得以继续执行,将参数 innodb_deadlock_detect 设置为 on,表示开启这个逻辑 +* 查询操作 + + ```sh + hget key field #获取指定field对应数据 + hgetall key #获取指定key所有数据 + hmget key field1 field2... #获取多个数据 + hexists key field #获取哈希表中是否存在指定的字段 + hlen key #获取哈希表中字段的数量 + ``` +* 获取哈希表中所有的字段名或字段值 + ```sh + hkeys key #获取所有的field + hvals key #获取所有的value + ``` -**** +* 设置指定字段的数值数据增加指定范围的值 + ```sh + hincrby key field increment #指定字段的数值数据增加指定的值,increment为负数则减少 + hincrbyfloat key field increment#操作小数 + ``` -#### 锁优化 +注意事项 -##### 锁升级 +1. hash 类型中 value 只能存储字符串,不允许存储其他数据类型,不存在嵌套现象,如果数据未获取到,对应的值为(nil) +2. 每个 hash 可以存储 2^32 - 1 个键值对 +3. hash 类型和对象的数据存储形式相似,并且可以灵活添加删除对象属性。但 hash 设计初衷不是为了存储大量对象而设计的,不可滥用,不可将 hash 作为对象列表使用 +4. hgetall 操作可以获取全部属性,如果内部 field 过多,遍历整体数据效率就很会低,有可能成为数据访问瓶颈 -索引失效造成行锁升级为表锁,不通过索引检索数据,InnoDB 会将对表中的所有记录加锁,实际效果和**表锁**一样实际开发过程应避免出现索引失效的状况 -* 查看当前表的索引: - ```mysql - SHOW INDEX FROM test_innodb_lock; - ``` +*** -* 关闭自动提交功能: - ```mysql - SET AUTOCOMMIT=0; -- C1、C2 - ``` -* 执行更新语句: +#### 实现 - ```mysql - UPDATE test_innodb_lock SET sex='2' WHERE name=10; -- C1 - UPDATE test_innodb_lock SET sex='2' WHERE id=3; -- C2 - ``` +哈希对象的内部编码有两种:ziplist(压缩列表)、hashtable(哈希表、字典) - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-InnoDB 锁升级.png) +* 压缩列表实现哈希对象:同一键值对的节点总是挨在一起,保存键的节点在前,保存值的节点在后 - 索引失效:执行更新时 name 字段为 varchar 类型,造成索引失效,最终行锁变为表锁 + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-哈希对象ziplist.png) + +* 字典实现哈希对象:字典的每一个键都是一个字符串对象,每个值也是 + + + +当存储的数据量比较小的情况下,Redis 才使用压缩列表来实现字典类型,具体需要满足两个条件: + +- 当键值对数量小于 hash-max-ziplist-entries 配置(默认 512 个) +- 所有键和值的长度都小于 hash-max-ziplist-value 配置(默认 64 字节) + +以上两个条件的上限值是可以通过配置文件修改的,当两个条件的任意一个不能被满足时,对象的编码转换操作就会被执行 + +ziplist 使用更加紧凑的结构实现多个元素的连续存储,所以在节省内存方面比 hashtable 更加优秀,当 ziplist 无法满足哈希类型时,Redis 会使用 hashtable 作为哈希的内部实现,因为此时 ziplist 的读写效率会下降,而 hashtable 的读写时间复杂度为 O(1) @@ -6711,19 +11736,19 @@ InnoDB 存储引擎支持的是行级别的锁,因此意向锁不会阻塞除 -##### 优化锁 +#### 应用 -InnoDB 存储引擎实现了行级锁定,虽然在锁定机制的实现方面带来了性能损耗可能比表锁会更高,但是在整体并发处理能力方面要远远优于 MyISAM 的表锁,当系统并发量较高的时候,InnoDB 的整体性能远远好于 MyISAM +```sh +user:id:3506728370 → {"name":"春晚","fans":12210862,"blogs":83} +``` -但是使用不当可能会让InnoDB 的整体性能表现不仅不能比 MyISAM 高,甚至可能会更差 +对于以上数据,使用单条去存的话,存的条数会很多。但如果用 json 格式,存一条数据就够了。 -优化建议: +假如现在粉丝数量发生了变化,要把整个值都改变,但是用单条存就不存在这个问题,只需要改其中一个就可以 -- 尽可能让所有数据检索都能通过索引来完成,避免无索引行锁升级为表锁 -- 合理设计索引,尽量缩小锁的范围 -- 尽可能减少索引条件及索引范围,避免间隙锁 -- 尽量控制事务大小,减少锁定资源量和时间长度 -- 尽可使用低级别事务隔离(需要业务层面满足需求) + + +可以实现购物车的功能,key 对应着每个用户,存储空间存储购物车的信息 @@ -6733,140 +11758,136 @@ InnoDB 存储引擎实现了行级锁定,虽然在锁定机制的实现方面 -#### 锁状态 +### list -```mysql -SHOW STATUS LIKE 'innodb_row_lock%'; -``` +#### 简介 - +数据存储需求:存储多个数据,并对数据进入存储空间的顺序进行区分 -参数说明: +数据存储结构:一个存储空间保存多个数据,且通过数据可以体现进入顺序,允许重复元素 -* Innodb_row_lock_current_waits:当前正在等待锁定的数量 +list 类型:保存多个数据,底层使用**双向链表**存储结构实现,类似于 LinkedList -* Innodb_row_lock_time:从系统启动到现在锁定总时间长度 + -* Innodb_row_lock_time_avg:每次等待所花平均时长 +如果两端都能存取数据的话,这就是双端队列,如果只能从一端进一端出,这个模型叫栈 -* Innodb_row_lock_time_max:从系统启动到现在等待最长的一次所花的时间 -* Innodb_row_lock_waits:系统启动后到现在总共等待的次数 -当等待的次数很高,而且每次等待的时长也不短的时候,就需要分析系统中为什么会有如此多的等待,然后根据分析结果制定优化计划 +*** -查看锁状态: -```mysql -SELECT * FROM information_schema.innodb_locks; #锁的概况 -SHOW ENGINE INNODB STATUS; #InnoDB整体状态,其中包括锁的情况 -``` -![](https://gitee.com/seazean/images/raw/master/DB/MySQL-InnoDB查看锁状态.png) +#### 操作 -lock_id 是锁 id;lock_trx_id 为事务 id;lock_mode 为 X 代表排它锁(写锁);lock_type 为 RECORD 代表锁为行锁(记录锁) +指令操作: +* 数据操作 + + ```sh + lpush key value1 [value2]...#从左边添加/修改数据(表头) + rpush key value1 [value2]...#从右边添加/修改数据(表尾) + lpop key #从左边获取并移除第一个数据,类似于出栈/出队 + rpop key #从右边获取并移除第一个数据 + lrem key count value #删除指定数据,count=2删除2个,该value可能有多个(重复数据) + ``` +* 查询操作 + ```sh + lrange key start stop #从左边遍历数据并指定开始和结束索引,0是第一个索引,-1是终索引 + lindex key index #获取指定索引数据,没有则为nil,没有索引越界 + llen key #list中数据长度/个数 + ``` +* 规定时间内获取并移除数据 -*** + ```sh + b #代表阻塞 + blpop key1 [key2] timeout #在指定时间内获取指定key(可以多个)的数据,超时则为(nil) + #可以从其他客户端写数据,当前客户端阻塞读取数据 + brpop key1 [key2] timeout #从右边操作 + ``` + +* 复制操作 + ```sh + brpoplpush source destination timeout #从source获取数据放入destination,假如在指定时间内没有任何元素被弹出,则返回一个nil和等待时长。反之,返回一个含有两个元素的列表,第一个元素是被弹出元素的值,第二个元素是等待时长 + ``` +注意事项 -### 乐观锁 +1. list 中保存的数据都是 string 类型的,数据总容量是有限的,最多 2^32 - 1 个元素(4294967295) +2. list 具有索引的概念,但操作数据时通常以队列的形式进行入队出队,或以栈的形式进行入栈出栈 +3. 获取全部数据操作结束索引设置为 -1 +4. list 可以对数据进行分页操作,通常第一页的信息来自于 list,第 2 页及更多的信息通过数据库的形式加载 -悲观锁和乐观锁使用前提: -- 对于读的操作远多于写的操作的时候,一个更新操作加锁会阻塞所有的读取操作,降低了吞吐量,最后需要释放锁,锁是需要一些开销的,这时候可以选择乐观锁 -- 如果是读写比例差距不是非常大或者系统没有响应不及时,吞吐量瓶颈的问题,那就不要去使用乐观锁,它增加了复杂度,也带来了业务额外的风险,这时候可以选择悲观锁 -乐观锁的现方式: +**** -* 版本号 - 1. 给数据表中添加一个 version 列,每次更新后都将这个列的值加 1 - 2. 读取数据时,将版本号读取出来,在执行更新的时候,比较版本号 +#### 实现 - 3. 如果相同则执行更新,如果不相同,说明此条数据已经发生了变化 +在 Redis3.2 版本以前列表对象的内部编码有两种:ziplist(压缩列表)和 linkedlist(链表) - 4. 用户自行根据这个通知来决定怎么处理,比如重新开始一遍,或者放弃本次更新 +* 压缩列表实现的列表对象:PUSH 1、three、5 三个元素 - ```mysql - -- 创建city表 - CREATE TABLE city( - id INT PRIMARY KEY AUTO_INCREMENT, -- 城市id - NAME VARCHAR(20), -- 城市名称 - VERSION INT -- 版本号 - ); - - -- 添加数据 - INSERT INTO city VALUES (NULL,'北京',1),(NULL,'上海',1),(NULL,'广州',1),(NULL,'深圳',1); - - -- 修改北京为北京市 - -- 1.查询北京的version - SELECT VERSION FROM city WHERE NAME='北京'; - -- 2.修改北京为北京市,版本号+1。并对比版本号 - UPDATE city SET NAME='北京市',VERSION=VERSION+1 WHERE NAME='北京' AND VERSION=1; - ``` + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-列表对象ziplist.png) -* 时间戳 +* 链表实现的列表对象:为了简化字符串对象的表示,使用了 StringObject 的结构,底层其实是 sdshdr 结构 - - 和版本号方式基本一样,给数据表中添加一个列,名称无所谓,数据类型需要是 **timestamp** - - 每次更新后都将最新时间插入到此列 - - 读取数据时,将时间读取出来,在执行更新的时候,比较时间 - - 如果相同则执行更新,如果不相同,说明此条数据已经发生了变化 + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-列表对象linkedlist.png) +列表中存储的数据量比较小的时候,列表就会使用一块连续的内存存储,采用压缩列表的方式实现的条件: +* 列表对象保存的所有字符串元素的长度都小于 64 字节 +* 列表对象保存的元素数量小于 512 个 +以上两个条件的上限值是可以通过配置文件修改的,当两个条件的任意一个不能被满足时,对象的编码转换操作就会被执行 +在 Redis3.2 版本 以后对列表数据结构进行了改造,使用 **quicklist(快速列表)**代替了 linkedlist,quicklist 实际上是 ziplist 和 linkedlist 的混合体,将 linkedlist 按段切分,每一段使用 ziplist 来紧凑存储,多个 ziplist 之间使用双向指针串接起来,既满足了快速的插入删除性能,又不会出现太大的空间冗余 -*** + +*** -## 日志 -### 日志分类 +#### 应用 -在任何一种数据库中,都会有各种各样的日志,记录着数据库工作的过程,可以帮助数据库管理员追踪数据库曾经发生过的各种事件。 +企业运营过程中,系统将产生出大量的运营数据,如何保障多台服务器操作日志的统一顺序输出? -MySQL日志主要包括六种: +* 依赖 list 的数据具有顺序的特征对信息进行管理,右进左查或者左近左查 +* 使用队列模型解决多路信息汇总合并的问题 +* 使用栈模型解决最新消息的问题 -1. 重做日志(redo log) -2. 回滚日志(undo log) -3. 归档日志(binlog)(二进制日志) -4. 错误日志(errorlog) -5. 慢查询日志(slow query log) -6. 一般查询日志(general log) -7. 中继日志(relay log) +微信文章订阅公众号: + +* 比如订阅了两个公众号,它们发布了两篇文章,文章 ID 分别为 666 和 888,可以通过执行 `LPUSH key 666 888` 命令推送给我 -*** +*** -### 错误日志 -错误日志是 MySQL 中最重要的日志之一,记录了当 mysqld 启动和停止时,以及服务器在运行过程中发生任何严重错误时的相关信息。当数据库出现任何故障导致无法正常使用时,可以首先查看此日志 -该日志是默认开启的,默认位置是:`/var/log/mysql/error.log` +### set -查看指令: +#### 简介 -```mysql -SHOW VARIABLES LIKE 'log_error%'; -``` +数据存储需求:存储大量的数据,在查询方面提供更高的效率 -查看日志内容: +数据存储结构:能够保存大量的数据,高效的内部存储机制,便于查询 -```sh -tail -f /var/log/mysql/error.log -``` +set 类型:与 hash 存储结构哈希表完全相同,只是仅存储键不存储值(nil),所以添加,删除,查找的复杂度都是 O(1),并且**值是不允许重复且无序的** + + @@ -6874,106 +11895,99 @@ tail -f /var/log/mysql/error.log -### 归档日志 +#### 操作 -#### 基本介绍 +指令操作: -归档日志(BINLOG)也叫二进制日志,是因为采用二进制进行存储,记录了所有的 DDL(数据定义语言)语句和 DML(数据操作语言)语句,但**不包括数据查询语句,在事务提交前的最后阶段写入** +* 数据操作 -作用:**灾难时的数据恢复和 MySQL 的主从复制** + ```sh + sadd key member1 [member2] #添加数据 + srem key member1 [member2] #删除数据 + ``` + +* 查询操作 -归档日志默认情况下是没有开启的,需要在 MySQL 配置文件中开启,并配置 MySQL 日志的格式: + ```sh + smembers key #获取全部数据 + scard key #获取集合数据总量 + sismember key member #判断集合中是否包含指定数据 + ``` -```sh -cd /etc/mysql -vim my.cnf +* 随机操作 -# 配置开启binlog日志, 日志的文件前缀为 mysqlbin -----> 生成的文件名如: mysqlbin.000001 -log_bin=mysqlbin -# 配置二进制日志的格式 -binlog_format=STATEMENT -``` + ```sh + spop key [count] #随机获取集中的某个数据并将该数据移除集合 + srandmember key [count] #随机获取集合中指定(数量)的数据 -日志存放位置:配置时给定了文件名但是没有指定路径,日志默认写入MySQL 的数据目录 +* 集合的交、并、差 -日志格式: + ```sh + sinter key1 [key2...] #两个集合的交集,不存在为(empty list or set) + sunion key1 [key2...] #两个集合的并集 + sdiff key1 [key2...] #两个集合的差集 + + sinterstore destination key1 [key2...] #两个集合的交集并存储到指定集合中 + sunionstore destination key1 [key2...] #两个集合的并集并存储到指定集合中 + sdiffstore destination key1 [key2...] #两个集合的差集并存储到指定集合中 + ``` -* STATEMENT:该日志格式在日志文件中记录的都是 SQL 语句,每一条对数据进行修改的 SQL 都会记录在日志文件中,通过 mysqlbinlog 工具,可以查看到每条语句的文本。主从复制时,从库会将日志解析为原语句,并在从库重新执行一遍 +* 复制 - 缺点:可能会导致主备不一致,因为记录的 SQL 在不同的环境中可能选择的索引不同,导致结果不同 -* ROW:该日志格式在日志文件中记录的是每一行的数据变更,而不是记录 SQL 语句。比如执行 SQL 语句 `update tb_book set status='1'`,如果是 STATEMENT,在日志中会记录一行 SQL 语句; 如果是 ROW,由于是对全表进行更新,就是每一行记录都会发生变更,ROW 格式的日志中会记录每一行的数据变更 + ```sh + smove source destination member #将指定数据从原始集合中移动到目标集合中 + ``` - 缺点:记录的数据比较多,占用很多的存储空间 -* MIXED:这是 MySQL 默认的日志格式,混合了STATEMENT 和 ROW 两种格式。MIXED 格式能尽量利用两种模式的优点,而避开它们的缺点 +注意事项 +1. set 类型不允许数据重复,如果添加的数据在 set 中已经存在,将只保留一份 +2. set 虽然与 hash 的存储结构相同,但是无法启用 hash 中存储值的空间 -*** +*** -#### 日志读取 -日志文件存储位置:/var/lib/mysql +#### 实现 -由于日志以二进制方式存储,不能直接读取,需要用 mysqlbinlog 工具来查看,语法如下: +集合对象的内部编码有两种:intset(整数集合)、hashtable(哈希表、字典) -```sh -mysqlbinlog log-file; -``` +* 整数集合实现的集合对象: -查看 STATEMENT 格式日志: + -* 执行插入语句: +* 字典实现的集合对象:键值对的值为 NULL - ```mysql - INSERT INTO tb_book VALUES(NULL,'Lucene','2088-05-01','0'); - ``` + -* `cd /var/lib/mysql`: +当集合对象可以同时满足以下两个条件时,对象使用 intset 编码: - ```sh - -rw-r----- 1 mysql mysql 177 5月 23 21:08 mysqlbin.000001 - -rw-r----- 1 mysql mysql 18 5月 23 21:04 mysqlbin.index - ``` +* 集合中的元素都是整数值 +* 集合中的元素数量小于 set-maxintset-entries配置(默认 512 个) - mysqlbin.index:该文件是日志索引文件 , 记录日志的文件名; +以上两个条件的上限值是可以通过配置文件修改的 - mysqlbing.000001:日志文件 -* 查看日志内容: - ```sh - mysqlbinlog mysqlbing.000001; - ``` +**** - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-日志读取1.png) - - 日志结尾有 COMMIT -查看 ROW 格式日志: -* 修改配置: +#### 应用 - ```sh - # 配置二进制日志的格式 - binlog_format=ROW - ``` +应用场景: -* 插入数据: +1. 黑名单:资讯类信息类网站追求高访问量,但是由于其信息的价值,往往容易被不法分子利用,通过爬虫技术,快速获取信息,个别特种行业网站信息通过爬虫获取分析后,可以转换成商业机密。 - ```mysql - INSERT INTO tb_book VALUES(NULL,'SpringCloud实战','2088-05-05','0'); - ``` + 注意:爬虫不一定做摧毁性的工作,有些小型网站需要爬虫为其带来一些流量。 -* 查看日志内容:日志格式 ROW,直接查看数据是乱码,可以在 mysqlbinlog 后面加上参数 -vv +2. 白名单:对于安全性更高的应用访问,仅仅靠黑名单是不能解决安全问题的,此时需要设定可访问的用户群体, 依赖白名单做更为苛刻的访问验证 - ```mysql - mysqlbinlog -vv mysqlbin.000002 - ``` +3. 随机操作可以实现抽奖功能 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-日志读取2.png) +4. 集合的交并补可以实现微博共同关注的查看,可以根据共同关注或者共同喜欢推荐相关内容 @@ -6983,100 +11997,120 @@ mysqlbinlog log-file; -#### 日志删除 +### zset -对于比较繁忙的系统,生成日志量大,这些日志如果长时间不清除,将会占用大量的磁盘空间,需要删除日志 +#### 简介 -* Reset Master 指令删除全部 binlog 日志,删除之后,日志编号将从 xxxx.000001重新开始 +数据存储需求:数据排序有利于数据的有效展示,需要提供一种可以根据自身特征进行排序的方式 - ```mysql - Reset Master -- MySQL指令 - ``` +数据存储结构:新的存储模型,可以保存可排序的数据 -* 执行指令 `PURGE MASTER LOGS TO 'mysqlbin.***`,该命令将删除 ` ***` 编号之前的所有日志 -* 执行指令 `PURGE MASTER LOGS BEFORE 'yyyy-mm-dd hh:mm:ss'` ,该命令将删除日志为 `yyyy-mm-dd hh:mm:ss` 之前产生的日志 -* 设置参数 `--expire_logs_days=#`,此参数的含义是设置日志的过期天数,过了指定的天数后日志将会被自动删除,这样做有利于减少管理日志的工作量,配置 my.cnf 文件: +**** + + + +#### 操作 + +指令操作: + +* 数据操作 ```sh - log_bin=mysqlbin - binlog_format=ROW - --expire_logs_days=3 + zadd key score1 member1 [score2 member2] #添加数据 + zrem key member [member ...] #删除数据 + zremrangebyrank key start stop #删除指定索引范围的数据 + zremrangebyscore key min max #删除指定分数区间内的数据 + zscore key member #获取指定值的分数 + zincrby key increment member #指定值的分数增加increment ``` +* 查询操作 + ```sh + zrange key start stop [WITHSCORES] #获取指定范围的数据,升序,WITHSCORES 代表显示分数 + zrevrange key start stop [WITHSCORES] #获取指定范围的数据,降序 + + zrangebyscore key min max [WITHSCORES] [LIMIT offset count] #按条件获取数据,从小到大 + zrevrangebyscore key max min [WITHSCORES] [...] #从大到小 + + zcard key #获取集合数据的总量 + zcount key min max #获取指定分数区间内的数据总量 + zrank key member #获取数据对应的索引(排名)升序 + zrevrank key member #获取数据对应的索引(排名)降序 + ``` -*** + * min 与 max 用于限定搜索查询的条件 + * start 与 stop 用于限定查询范围,作用于索引,表示开始和结束索引 + * offset 与 count 用于限定查询范围,作用于查询结果,表示开始位置和数据总量 +* 集合的交、并操作 + ```sh + zinterstore destination numkeys key [key ...] #两个集合的交集并存储到指定集合中 + zunionstore destination numkeys key [key ...] #两个集合的并集并存储到指定集合中 + ``` -### 查询日志 +注意事项: -查询日志中记录了客户端的所有操作语句,而二进制日志不包含查询数据的 SQL 语句 +1. score 保存的数据存储空间是 64 位,如果是整数范围是 -9007199254740992~9007199254740992 +2. score 保存的数据也可以是一个双精度的 double 值,基于双精度浮点数的特征可能会丢失精度,慎重使用 +3. sorted_set 底层存储还是基于 set 结构的,因此数据不能重复,如果重复添加相同的数据,score 值将被反复覆盖,保留最后一次修改的结果 -默认情况下,查询日志是未开启的。如果需要开启查询日志,配置 my.cnf: -```sh -# 该选项用来开启查询日志,可选值0或者1,0代表关闭,1代表开启 -general_log=1 -# 设置日志的文件名,如果没有指定,默认的文件名为host_name.log,存放在/var/lib/mysql -general_log_file=mysql_query.log -``` -配置完毕之后,在数据库执行以下操作: +*** -```mysql -SELECT * FROM tb_book; -SELECT * FROM tb_book WHERE id = 1; -UPDATE tb_book SET name = 'lucene入门指南' WHERE id = 5; -SELECT * FROM tb_book WHERE id < 8 -``` -执行完毕之后, 再次来查询日志文件: -![](https://gitee.com/seazean/images/raw/master/DB/MySQL-查询日志.png) +#### 实现 +有序集合对象的内部编码有两种:ziplist(压缩列表)和 skiplist(跳跃表) +* 压缩列表实现有序集合对象:ziplist 本身是有序、不可重复的,符合有序集合的特性 -*** + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-有序集合对象ziplist.png) +* 跳跃表实现有序集合对象:**底层是 zset 结构,zset 同时包含字典和跳跃表的结构**,图示字典和跳跃表中重复展示了各个元素的成员和分值,但实际上两者会**通过指针来共享相同元素的成员和分值**,不会产生空间浪费 + ```c + typedef struct zset { + zskiplist *zsl; + dict *dict; + } zset; + ``` -### 慢日志 + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-有序集合对象zset.png) -慢查询日志记录所有执行时间超过 long_query_time 并且扫描记录数不小于 min_examined_row_limit 的所有的 SQL 语句的日志。long_query_time 默认为 10 秒,最小为 0, 精度到微秒 +使用字典加跳跃表的优势: -慢查询日志默认是关闭的,可以通过两个参数来控制慢查询日志,配置文件 `/etc/mysql/my.cnf`: +* 字典为有序集合创建了一个**从成员到分值的映射**,用 O(1) 复杂度查找给定成员的分值 +* **排序操作使用跳跃表完成**,节省每次重新排序带来的时间成本和空间成本 -```sh -# 该参数用来控制慢查询日志是否开启,可选值0或者1,0代表关闭,1代表开启 -slow_query_log=1 +使用 ziplist 格式存储需要满足以下两个条件: -# 该参数用来指定慢查询日志的文件名,存放在 /var/lib/mysql -slow_query_log_file=slow_query.log +- 有序集合保存的元素个数要小于 128 个; +- 有序集合保存的所有元素大小都小于 64 字节 -# 该选项用来配置查询的时间限制,超过这个时间将认为值慢查询,将需要进行日志记录,默认10s -long_query_time=10 -``` +当元素比较多时,此时 ziplist 的读写效率会下降,时间复杂度是 O(n),跳表的时间复杂度是 O(logn) -日志读取: +为什么用跳表而不用平衡树? -* 直接通过 cat 指令查询该日志文件: +* 在做范围查找的时候,跳表操作简单(前进指针或后退指针),平衡树需要回旋查找 +* 跳表比平衡树实现简单,平衡树的插入和删除操作可能引发子树的旋转调整,而跳表的插入和删除只需要修改相邻节点的指针 - ```sh - cat slow_query.log - ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-慢日志读取1.png) -* 如果慢查询日志内容很多,直接查看文件比较繁琐,可以借助 mysql 自带的 mysqldumpslow 工具对慢查询日志进行分类汇总: +*** - ```sh - mysqldumpslow slow_query.log - ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-慢日志读取2.png) + +#### 应用 + +* 排行榜 +* 对于基于时间线限定的任务处理,将处理时间记录为 score 值,利用排序功能区分处理的先后顺序 +* 当任务或者消息待处理,形成了任务队列或消息队列时,对于高优先级的任务要保障对其优先处理,采用 score 记录权重 @@ -7086,103 +12120,127 @@ long_query_time=10 -## 范式 +### Bitmaps -### 第一范式 +#### 基本操作 -建立科学的,**规范的数据表**就需要满足一些规则来优化数据的设计和存储,这些规则就称为范式 +Bitmaps 是二进制位数组(bit array),底层使用 SDS 字符串表示,因为 SDS 是二进制安全的 -**1NF:**数据库表的每一列都是不可分割的原子数据项,不能是集合、数组等非原子数据项。即表中的某个列有多个值时,必须拆分为不同的列。简而言之,**第一范式每一列不可再拆分,称为原子性** +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-位数组结构.png) -基本表: +buf 数组的每个字节用一行表示,buf[1] 是 `'\0'`,保存位数组的顺序和书写位数组的顺序是完全相反的,图示的位数组 0100 1101 -![](https://gitee.com/seazean/images/raw/master/DB/普通表.png) - +数据结构的详解查看 Java → Algorithm → 位图 -第一范式表: -![](https://gitee.com/seazean/images/raw/master/DB/第一范式.png) +*** -**** +#### 命令实现 +##### GETBIT -### 第二范式 +GETBIT 命令获取位数组 bitarray 在 offset 偏移量上的二进制位的值 -**2NF:**在满足第一范式的基础上,非主属性完全依赖于主码(主关键字、主键),消除非主属性对主码的部分函数依赖。简而言之,**表中的每一个字段 (所有列)都完全依赖于主键,记录的唯一性** +```sh +GETBIT +``` -作用:遵守第二范式减少数据冗余,通过主键区分相同数据。 +执行过程: -1. 函数依赖:A → B,如果通过 A 属性(属性组)的值,可以确定唯一 B 属性的值,则称 B 依赖于 A - * 学号 → 姓名;(学号,课程名称) → 分数 -2. 完全函数依赖:A → B,如果A是一个属性组,则 B 属性值的确定需要依赖于 A 属性组的所有属性值 - * (学号,课程名称) → 分数 -3. 部分函数依赖:A → B,如果 A 是一个属性组,则 B 属性值的确定只需要依赖于 A 属性组的某些属性值 - * (学号,课程名称) → 姓名 -4. 传递函数依赖:A → B,B → C,如果通过A属性(属性组)的值,可以确定唯一 B 属性的值,在通过 B 属性(属性组)的值,可以确定唯一 C 属性的值,则称 C 传递函数依赖于 A - * 学号 → 系名,系名 → 系主任 -5. 码:如果在一张表中,一个属性或属性组,被其他所有属性所完全依赖,则称这个属性(属性组)为该表的码 - * 该表中的码:(学号,课程名称) - * 主属性:码属性组中的所有属性 - * 非主属性:除码属性组以外的属性 +* 计算 `byte = offset/8`(向下取整), byte 值记录数据保存在位数组中的索引 +* 计算 `bit = (offset mod 8) + 1`,bit 值记录数据在位数组中的第几个二进制位 +* 根据 byte 和 bit 值,在位数组 bitarray 中定位 offset 偏移量指定的二进制位,并返回这个位的值 + +GETBIT 命令执行的所有操作都可以在常数时间内完成,所以时间复杂度为 O(1) -![](https://gitee.com/seazean/images/raw/master/DB/第二范式.png) +*** -**** +##### SETBIT +SETBIT 将位数组 bitarray 在 offset 偏移量上的二进制位的值设置为 value,并向客户端返回二进制位的旧值 +```sh +SETBIT +``` -### 第三范式 +执行过程: -**3NF:**在满足第二范式的基础上,表中的任何属性不依赖于其它非主属性,消除传递依赖。简而言之,**非主键都直接依赖于主键,而不是通过其它的键来间接依赖于主键**。 +* 计算 `len = offset/8 + 1`,len 值记录了保存该数据至少需要多少个字节 +* 检查 bitarray 键保存的位数组的长度是否小于 len,成立就会将 SDS 扩展为 len 字节(注意空间预分配机制),所有新扩展空间的二进制位的值置为 0 +* 计算 `byte = offset/8`(向下取整), byte 值记录数据保存在位数组中的索引 +* 计算 `bit = (offset mod 8) + 1`,bit 值记录数据在位数组中的第几个二进制位 +* 根据 byte 和 bit 值,在位数组 bitarray 中定位 offset 偏移量指定的二进制位,首先将指定位现存的值保存在 oldvalue 变量,然后将新值 value 设置为这个二进制位的值 +* 向客户端返回 oldvalue 变量的值 -作用:可以通过主键 id 区分相同数据,修改数据的时候只需要修改一张表(方便修改),反之需要修改多表。 -![](https://gitee.com/seazean/images/raw/master/DB/第三范式.png) +*** +##### BITCOUNT +BITCOUNT 命令用于统计给定位数组中,值为 1 的二进制位的数量 +```sh +BITCOUNT [start end] +``` -*** +二进制位统计算法: +* 遍历法:遍历位数组中的每个二进制位 +* 查表算法:读取每个字节(8 位)的数据,查表获取数值对应的二进制中有几个 1 +* variable-precision SWAR算法:计算汉明距离 +* Redis 实现: + * 如果二进制位的数量大于等于 128 位, 那么使用 variable-precision SWAR 算法来计算二进制位的汉明重量 + * 如果二进制位的数量小于 128 位,那么使用查表算法来计算二进制位的汉明重量 -### 总结 -![](https://gitee.com/seazean/images/raw/master/DB/三大范式.png) +**** +##### BITOP +BITOP 命令对指定 key 按位进行交、并、非、异或操作,并将结果保存到指定的键中 +```sh +BITOP OPTION destKey key1 [key2...] +``` +OPTION 有 AND(与)、OR(或)、 XOR(异或)和 NOT(非)四个选项 -**** +AND、OR、XOR 三个命令可以接受多个位数组作为输入,需要遍历输入的每个位数组的每个字节来进行计算,所以命令的复杂度为 O(n^2);与此相反,NOT 命令只接受一个位数组输入,所以时间复杂度为 O(n) +*** -# JDBC -## 概述 +#### 应用场景 -JDBC(Java DataBase Connectivity,java数据库连接)是一种用于执行 SQL 语句的 Java API,可以为多种关系型数据库提供统一访问,是由一组用 Java 语言编写的类和接口组成的。 +- **解决 Redis 缓存穿透**,判断给定数据是否存在, 防止缓存穿透 + + + +- 垃圾邮件过滤,对每一个发送邮件的地址进行判断是否在布隆的黑名单中,如果在就判断为垃圾邮件 + +- 爬虫去重,爬给定网址的时候对已经爬取过的 URL 去重 + +- 信息状态统计 -JDBC 是 Java 官方提供的一套规范(接口),用于帮助开发人员快速实现不同关系型数据库的连接 -使用 JDBC 需要导包 @@ -7190,40 +12248,43 @@ JDBC 是 Java 官方提供的一套规范(接口),用于帮助开发人员 -## 功能类 +### Hyper -### DriverManager +基数是数据集去重后元素个数,HyperLogLog 是用来做基数统计的,运用了 LogLog 的算法 -DriverManager:驱动管理对象 +```java +{1, 3, 5, 7, 5, 7, 8} 基数集: {1, 3, 5 ,7, 8} 基数:5 +{1, 1, 1, 1, 1, 7, 1} 基数集: {1,7} 基数:2 +``` -* 注册驱动 - * 注册给定的驱动:`public static void registerDriver(Driver driver)` +相关指令: - * 代码实现语法:`Class.forName("com.mysql.jdbc.Driver)` +* 添加数据 - * com.mysql.jdbc.Driver 中存在静态代码块 + ```sh + pfadd key element [element ...] + ``` - ```java - static { - try { - DriverManager.registerDriver(new Driver()); - } catch (SQLException var1) { - throw new RuntimeException("Can't register driver!"); - } - } - ``` +* 统计数据 - * 不需要通过 DriverManager 调用静态方法 registerDriver,因为 Driver 类被使用,则自动执行静态代码块完成注册驱动 + ```sh + pfcount key [key ...] + ``` - * jar 包中 META-INF 目录下存在一个 java.sql.Driver 配置文件,文件中指定了 com.mysql.jdbc.Driver +* 合并数据 -* 获取数据库连接并返回连接对象 + ```sh + pfmerge destkey sourcekey [sourcekey...] + ``` - `public static Connection getConnection(String url, String user, String password)` +应用场景: - * url:指定连接的路径。语法:`jdbc:mysql://ip地址(域名):端口号/数据库名称` - * user:用户名 - * password:密码 +* 用于进行基数统计,不是集合不保存数据,只记录数量而不是具体数据,比如网站的访问量 +* 核心是基数估算算法,最终数值存在一定误差 +* 误差范围:基数估计的结果是一个带有 0.81% 标准错误的近似值 +* 耗空间极小,每个 hyperloglog key 占用了12K的内存用于标记基数 +* pfadd 命令不是一次性分配12K内存使用,会随着基数的增加内存逐渐增大 +* Pfmerge 命令合并后占用的存储空间为12K,无论合并之前数据量多少 @@ -7231,122 +12292,86 @@ DriverManager:驱动管理对象 -### Connection +### GEO -Connection:数据库连接对象 +GeoHash 是一种地址编码方法,把二维的空间经纬度数据编码成一个字符串 -- 获取执行者对象 - - 获取普通执行者对象:`Statement createStatement()` - - 获取预编译执行者对象:`PreparedStatement prepareStatement(String sql)` -- 管理事务 - - 开启事务:`setAutoCommit(boolean autoCommit)`,false 开启事务,true 自动提交模式(默认) - - 提交事务:`void commit()` - - 回滚事务:`void rollback()` -- 释放资源 - - 释放此 Connection 对象的数据库和 JDBC 资源:`void close()` +* 添加坐标点 + ```sh + geoadd key longitude latitude member [longitude latitude member ...] + georadius key longitude latitude radius m|km|ft|mi [withcoord] [withdist] [withhash] [count count] + ``` +* 获取坐标点 -*** + ```sh + geopos key member [member ...] + georadiusbymember key member radius m|km|ft|mi [withcoord] [withdist] [withhash] [count count] + ``` +* 计算距离 + ```sh + geodist key member1 member2 [unit] #计算坐标点距离 + geohash key member [member ...] #计算经纬度 + ``` -### Statement +Redis 应用于地理位置计算 -Statement:执行 sql 语句的对象 -- 执行 DML 语句:`int executeUpdate(String sql)` - - 返回值 int:返回影响的行数 - - 参数 sql:可以执行 insert、update、delete 语句 -- 执行 DQL 语句:`ResultSet executeQuery(String sql)` - - 返回值 ResultSet:封装查询的结果 - - 参数 sql:可以执行 select 语句 -- 释放资源 - - 释放此 Statement 对象的数据库和 JDBC 资源:`void close()` -*** +**** -### ResultSet -ResultSet:结果集对象,ResultSet 对象维护了一个游标,指向当前的数据行,初始在第一行 -- 判断结果集中是否有数据:`boolean next()` - - 有数据返回 true,并将索引**向下移动一行** - - 没有数据返回 false -- 获取结果集中**当前行**的数据:`XXX getXxx("列名")` - - XXX 代表数据类型(要获取某列数据,这一列的数据类型) - - 例如:String getString("name"); int getInt("age"); -- 释放资源 - - 释放 ResultSet 对象的数据库和 JDBC 资源:`void close()` +## 持久机制 +### 概述 +持久化:利用永久性存储介质将数据进行保存,在特定的时间将保存的数据进行恢复的工作机制称为持久化 -*** +作用:持久化用于防止数据的意外丢失,确保数据安全性,因为 Redis 是内存级,所以需要持久化到磁盘 +计算机中的数据全部都是二进制,保存一组数据有两种方式 + +RDB:将当前数据状态进行保存,快照形式,存储数据结果,存储格式简单 -### 代码实现 +AOF:将数据的操作过程进行保存,日志形式,存储操作过程,存储格式复杂 -数据准备 -```mysql --- 创建db14数据库 -CREATE DATABASE db14; --- 使用db14数据库 -USE db14; +*** --- 创建student表 -CREATE TABLE student( - sid INT PRIMARY KEY AUTO_INCREMENT, -- 学生id - NAME VARCHAR(20), -- 学生姓名 - age INT, -- 学生年龄 - birthday DATE, -- 学生生日 -); --- 添加数据 -INSERT INTO student VALUES (NULL,'张三',23,'1999-09-23'),(NULL,'李四',24,'1998-08-10'), -(NULL,'王五',25,'1996-06-06'),(NULL,'赵六',26,'1994-10-20'); -``` -JDBC 连接代码: +### RDB -```java -public class JDBCDemo01 { - public static void main(String[] args) throws Exception{ - //1.导入jar包 - //2.注册驱动 - Class.forName("com.mysql.jdbc.Driver"); +#### 文件创建 - //3.获取连接 - Connection con = DriverManager.getConnection("jdbc:mysql://192.168.2.184:3306/db2","root","123456"); +RDB 持久化功能所生成的 RDB 文件是一个经过压缩的紧凑二进制文件,通过该文件可以还原生成 RDB 文件时的数据库状态,有两个 Redis 命令可以生成 RDB 文件,一个是 SAVE,另一个是 BGSAVE - //4.获取执行者对象 - Statement stat = con.createStatement(); - //5.执行sql语句,并且接收结果 - String sql = "SELECT * FROM user"; - ResultSet rs = stat.executeQuery(sql); - //6.处理结果 - while(rs.next()) { - System.out.println(rs.getInt("id") + "\t" + rs.getString("name")); - } +##### SAVE - //7.释放资源 - con.close(); - stat.close(); - con.close(); - } -} +SAVE 指令:手动执行一次保存操作,该指令的执行会阻塞当前 Redis 服务器,客户端发送的所有命令请求都会被拒绝,直到当前 RDB 过程完成为止,有可能会造成长时间阻塞,线上环境不建议使用 -``` +工作原理:Redis 是个**单线程的工作模式**,会创建一个任务队列,所有的命令都会进到这个队列排队执行。当某个指令在执行的时候,队列后面的指令都要等待,所以这种执行方式会非常耗时 +配置 redis.conf: +```sh +dir path #设置存储.rdb文件的路径,通常设置成存储空间较大的目录中,目录名称data +dbfilename "x.rdb" #设置本地数据库文件名,默认值为dump.rdb,通常设置为dump-端口号.rdb +rdbcompression yes|no #设置存储至本地数据库时是否压缩数据,默认yes,设置为no节省CPU运行时间 +rdbchecksum yes|no #设置读写文件过程是否进行RDB格式校验,默认yes +``` @@ -7354,379 +12379,206 @@ public class JDBCDemo01 { -## 工具类 - -* 配置文件(在 src 下创建 config.properties) +##### BGSAVE - ```properties - driverClass=com.mysql.jdbc.Driver - url=jdbc:mysql://192.168.2.184:3306/db14 - username=root - password=123456 - ``` +BGSAVE:bg 是 background,代表后台执行,命令的完成需要两个进程,**进程之间不相互影响**,所以持久化期间 Redis 正常工作 -* 工具类 +工作原理: - ```java - public class JDBCUtils { - //1.私有构造方法 - private JDBCUtils(){ - }; - - //2.声明配置信息变量 - private static String driverClass; - private static String url; - private static String username; - private static String password; - private static Connection con; - - //3.静态代码块中实现加载配置文件和注册驱动 - static{ - try{ - //通过类加载器返回配置文件的字节流 - InputStream is = JDBCUtils.class.getClassLoader(). - getResourceAsStream("config.properties"); - - //创建Properties集合,加载流对象的信息 - Properties prop = new Properties(); - prop.load(is); - - //获取信息为变量赋值 - driverClass = prop.getProperty("driverClass"); - url = prop.getProperty("url"); - username = prop.getProperty("username"); - password = prop.getProperty("password"); - - //注册驱动 - Class.forName(driverClass); - - } catch (Exception e) { - e.printStackTrace(); - } - } - - //4.获取数据库连接的方法 - public static Connection getConnection() { - try { - con = DriverManager.getConnection(url,username,password); - } catch (SQLException e) { - e.printStackTrace(); - } - return con; - } - - //5.释放资源的方法 - public static void close(Connection con, Statement stat, ResultSet rs) { - if(con != null) { - try { - con.close(); - } catch (SQLException e) { - e.printStackTrace(); - } - } - - if(stat != null) { - try { - stat.close(); - } catch (SQLException e) { - e.printStackTrace(); - } - } - - if(rs != null) { - try { - rs.close(); - } catch (SQLException e) { - e.printStackTrace(); - } - } - } - //方法重载,可能没有返回值对象 - public static void close(Connection con, Statement stat) { - close(con,stat,null); - } - } - ``` + + +流程:客户端发出 BGSAVE 指令,Redis 服务器使用 fork 函数创建一个子进程,然后响应后台已经开始执行的信息给客户端。子进程会异步执行持久化的操作,持久化过程是先将数据写入到一个临时文件中,持久化操作结束再用这个临时文件**替换**上次持久化的文件 + +```python +# 创建子进程 +pid = fork() +if pid == 0: + # 子进程负责创建 RDB 文件 + rdbSave() + # 完成之后向父进程发送信号 + signal_parent() +elif pid > 0: + # 父进程继续处理命令请求,并通过轮询等待子进程的信号 + handle_request_and_wait_signal() +else: + # 处理出错恃况 + handle_fork_error() +``` - +配置 redis.conf +```sh +stop-writes-on-bgsave-error yes|no #后台存储过程中如果出现错误,是否停止保存操作,默认yes +dbfilename filename +dir path +rdbcompression yes|no +rdbchecksum yes|no +``` -**** +注意:BGSAVE 命令是针对 SAVE 阻塞问题做的优化,Redis 内部所有涉及到 RDB 操作都采用 BGSAVE 的方式,SAVE 命令放弃使用 +在 BGSAVE 命令执行期间,服务器处理 SAVE、BGSAVE、BGREWRITEAOF 三个命令的方式会和平时有所不同 +* SAVE 命令会被服务器拒绝,服务器禁止 SAVE 和 BGSAVE 命令同时执行是为了避免父进程(服务器进程)和子进程同时执行两个 rdbSave 调用,产生竞争条件 +* BGSAVE 命令也会被服务器拒绝,也会产生竞争条件 +* BGREWRITEAOF 和 BGSAVE 两个命令不能同时执行 + * 如果 BGSAVE 命令正在执行,那么 BGREWRITEAOF 命令会被**延迟**到 BGSAVE 命令执行完毕之后执行 + * 如果 BGREWRITEAOF 命令正在执行,那么 BGSAVE 命令会被服务器拒绝 -## 数据封装 -从数据库读取数据并封装成Student对象,需要: -- Student 类成员变量对应表中的列 +*** -- 所有的基本数据类型需要使用包装类,**以防 null 值无法赋值** - ```java - public class Student { - private Integer sid; - private String name; - private Integer age; - private Date birthday; - ........ -- 数据准备 +##### 特殊指令 - ```mysql - -- 创建db14数据库 - CREATE DATABASE db14; - - -- 使用db14数据库 - USE db14; - - -- 创建student表 - CREATE TABLE student( - sid INT PRIMARY KEY AUTO_INCREMENT, -- 学生id - NAME VARCHAR(20), -- 学生姓名 - age INT, -- 学生年龄 - birthday DATE -- 学生生日 - ); - - -- 添加数据 - INSERT INTO student VALUES (NULL,'张三',23,'1999-09-23'),(NULL,'李四',24,'1998-08-10'),(NULL,'王五',25,'1996-06-06'),(NULL,'赵六',26,'1994-10-20'); - ``` +RDB 特殊启动形式的指令(客户端输入) -- 操作数据库 +* 服务器运行过程中重启 - ```java - public class StudentDaoImpl{ - //查询所有学生信息 - @Override - public ArrayList findAll() { - //1. - ArrayList list = new ArrayList<>(); - Connection con = null; - Statement stat = null; - ResultSet rs = null; - try{ - //2.获取数据库连接 - con = JDBCUtils.getConnection(); - - //3.获取执行者对象 - stat = con.createStatement(); - - //4.执行sql语句,并且接收返回的结果集 - String sql = "SELECT * FROM student"; - rs = stat.executeQuery(sql); - - //5.处理结果集 - while(rs.next()) { - Integer sid = rs.getInt("sid"); - String name = rs.getString("name"); - Integer age = rs.getInt("age"); - Date birthday = rs.getDate("birthday"); - - //封装Student对象 - Student stu = new Student(sid,name,age,birthday); - //将student对象保存到集合中 - list.add(stu); - } - } catch(Exception e) { - e.printStackTrace(); - } finally { - //6.释放资源 - JDBCUtils.close(con,stat,rs); - } - //将集合对象返回 - return list; - } - - //添加学生信息 - @Override - public int insert(Student stu) { - Connection con = null; - Statement stat = null; - int result = 0; - try{ - con = JDBCUtils.getConnection(); - - //3.获取执行者对象 - stat = con.createStatement(); - - //4.执行sql语句,并且接收返回的结果集 - Date d = stu.getBirthday(); - SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); - String birthday = sdf.format(d); - String sql = "INSERT INTO student VALUES ('"+stu.getSid()+"','"+stu.getName()+"','"+stu.getAge()+"','"+birthday+"')"; - result = stat.executeUpdate(sql); - - } catch(Exception e) { - e.printStackTrace(); - } finally { - //6.释放资源 - JDBCUtils.close(con,stat); - } - //将结果返回 - return result; - } - } + ```sh + debug reload ``` +* 关闭服务器时指定保存数据 + ```sh + shutdown save + ``` + 默认情况下执行 shutdown 命令时,自动执行 bgsave(如果没有开启 AOF 持久化功能) +* 全量复制:主从复制部分详解 -*** -## 注入攻击 -### 攻击演示 +*** -SQL注入攻击演示 -* 在登录界面,输入一个错误的用户名或密码,也可以登录成功 - ![](https://gitee.com/seazean/images/raw/master/DB/SQL注入攻击演示.png) +#### 文件载入 -* 原理:我们在密码处输入的所有内容,都应该认为是密码的组成,但是 Statement 对象在执行 sql 语句时,将一部分内容当做查询条件来执行 +RDB 文件的载入工作是在服务器启动时自动执行,期间 Redis 会一直处于阻塞状态,直到载入完成 - ```mysql - SELECT * FROM user WHERE loginname='aaa' AND password='aaa' OR '1'='1'; - ``` +Redis 并没有专门用于载入 RDB 文件的命令,只要服务器在启动时检测到 RDB 文件存在,就会自动载入 RDB 文件 +```sh +[7379] 30 Aug 21:07:01.289 * DB loaded from disk: 0.018 seconds # 服务器在成功载入 RDB 文件之后打印 +``` +AOF 文件的更新频率通常比 RDB 文件的更新频率高: +* 如果服务器开启了 AOF 持久化功能,那么会优先使用 AOF 文件来还原数据库状态 +* 只有在 AOF 持久化功能处于关闭状态时,服务器才会使用 RDB 文件来还原数据库状态 -*** -### 攻击解决 -PreparedStatement:预编译 sql 语句的执行者对象,继承 `PreparedStatement extends Statement` +**** -* 在执行 sql 语句之前,将 sql 语句进行提前编译。明确 sql 语句的格式,剩余的内容都会认为是参数 -* sql 语句中的参数使用 ? 作为占位符 -为 ? 占位符赋值的方法:`setXxx(int parameterIndex, xxx data)` -- 参数1:? 的位置编号(编号从 1 开始) +#### 自动保存 -- 参数2:? 的实际参数 +##### 配置文件 - ```java - String sql = "SELECT * FROM user WHERE loginname=? AND password=?"; - pst = con.prepareStatement(sql); - pst.setString(1,loginName); - pst.setString(2,password); - ``` +Redis 支持通过配置服务器的 save 选项,让服务器每隔一段时间自动执行一次 BGSAVE 命令 -执行 sql 语句的方法 +配置 redis.conf: -- 执行 insert、update、delete 语句:`int executeUpdate()` -- 执行 select 语句:`ResultSet executeQuery()` +```sh +save second changes #设置自动持久化条件,满足限定时间范围内key的变化数量就进行持久化(bgsave) +``` +* second:监控时间范围 +* changes:监控 key 的变化量 +默认三个条件: +```sh +save 900 1 # 900s内1个key发生变化就进行持久化 +save 300 10 +save 60 10000 +``` +判定 key 变化的依据: -**** +* 对数据产生了影响,不包括查询 +* 不进行数据比对,比如 name 键存在,重新 set name seazean 也算一次变化 +save 配置要根据实际业务情况进行设置,频度过高或过低都会出现性能问题,结果可能是灾难性的 -## 连接池 -### 概念 +*** -数据库连接背景:数据库连接是一种关键的、有限的、昂贵的资源,这一点在多用户的网页应用程序中体现得尤为突出。对数据库连接的管理能显著影响到整个应用程序的伸缩性和健壮性,影响到程序的性能指标。 -数据库连接池:**数据库连接池负责分配、管理和释放数据库连接**,它允许应用程序**重复使用**一个现有的数据库连接,而不是再重新建立一个,这项技术能明显提高对数据库操作的性能。 -数据库连接池原理 +##### 自动原理 -![](https://gitee.com/seazean/images/raw/master/DB/数据库连接池原理图解.png) +服务器状态相关的属性: +```c +struct redisServer { + // 记录了保存条件的数组 + struct saveparam *saveparams; + + // 修改计数器 + long long dirty; + + // 上一次执行保存的时间 + time_t lastsave; +}; +``` +* Redis 服务器启动时,可以通过指定配置文件或者传入启动参数的方式设置 save 选项, 如果没有自定义就设置为三个默认值(上节提及),设置服务器状态 redisServe.saveparams 属性,该数组每一项为一个 saveparam 结构,代表 save 的选项设置 + ```c + struct saveparam { + // 秒数 + time_t seconds + // 修改数 + int changes; + }; + ``` + +* dirty 计数器记录距离上一次成功执行 SAVE 或者 BGSAVE 命令之后,服务器中的所有数据库进行了多少次修改(包括写入、删除、更新等操作),当服务器成功执行一个修改指令,该命令修改了多少次数据库, dirty 的值就增加多少 +* lastsave 属性是一个 UNIX 时间戳,记录了服务器上一次成功执行 SAVE 或者 BGSAVE 命令的时间 -**** +Redis 的服务器周期性操作函数 serverCron 默认每隔 100 毫秒就会执行一次,该函数用于对正在运行的服务器进行维护 +serverCron 函数的其中一项工作是检查 save 选项所设置的保存条件是否满足,会遍历 saveparams 数组中的**所有保存条件**,只要有任意一个条件被满足服务器就会执行 BGSAVE 命令 +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-BGSAVE执行原理.png) -### 自定义池 -DataSource 接口概述: -* java.sql.DataSource 接口:数据源(数据库连接池) -* Java 中 DataSource 是一个标准的数据源接口,官方提供的数据库连接池规范,连接池类实现该接口 -* 获取数据库连接对象:`Connection getConnection()` -自定义连接池: -```java -public class MyDataSource implements DataSource{ - //1.定义集合容器,用于保存多个数据库连接对象 - private static List pool = Collections.synchronizedList(new ArrayList()); +*** - //2.静态代码块,生成10个数据库连接保存到集合中 - static { - for (int i = 0; i < 10; i++) { - Connection con = JDBCUtils.getConnection(); - pool.add(con); - } - } - //3.返回连接池的大小 - public int getSize() { - return pool.size(); - } - //4.从池中返回一个数据库连接 - @Override - public Connection getConnection() { - if(pool.size() > 0) { - //从池中获取数据库连接 - return pool.remove(0); - }else { - throw new RuntimeException("连接数量已用尽"); - } - } -} -``` -测试连接池功能: +#### 文件结构 -```java -public class MyDataSourceTest { - public static void main(String[] args) throws Exception{ - //创建数据库连接池对象 - MyDataSource dataSource = new MyDataSource(); +RDB 的存储结构:图示全大写单词标示常量,用全小写单词标示变量和数据 - System.out.println("使用之前连接池数量:" + dataSource.getSize());//10 - - //获取数据库连接对象 - Connection con = dataSource.getConnection(); - System.out.println(con.getClass());// JDBC4Connection +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-RDB文件结构.png) - //查询学生表全部信息 - String sql = "SELECT * FROM student"; - PreparedStatement pst = con.prepareStatement(sql); - ResultSet rs = pst.executeQuery(); +* REDIS:长度为 5 字节,保存着 `REDIS` 五个字符,是 RDB 文件的开头,在载入文件时可以快速检查所载入的文件是否 RDB 文件 +* db_version:长度为 4 字节,是一个用字符串表示的整数,记录 RDB 的版本号 +* database:包含着零个或任意多个数据库,以及各个数据库中的键值对数据 +* EOF:长度为 1 字节的常量,标志着 RDB 文件正文内容的结束,当读入遇到这个值时,代表所有数据库的键值对都已经载入完毕 +* check_sum:长度为 8 字节的无符号整数,保存着一个校验和,该值是通过 REDIS、db_version、databases、EOF 四个部分的内容进行计算得出。服务器在载入 RDB 文件时,会将载入数据所计算出的校验和与 check_sum 所记录的校验和进行对比,来检查 RDB 文件是否有出错或者损坏 - while(rs.next()) { - System.out.println(rs.getInt("sid") + "\t" + rs.getString("name") + "\t" + rs.getInt("age") + "\t" + rs.getDate("birthday")); - } - - //释放资源 - rs.close(); - pst.close(); - //目前的连接对象close方法,是直接关闭连接,而不是将连接归还池中 - con.close(); +Redis 本身带有 RDB 文件检查工具 redis-check-dump - System.out.println("使用之后连接池数量:" + dataSource.getSize());//9 - } -} -``` -结论:释放资源并没有把连接归还给连接池 @@ -7734,478 +12586,190 @@ public class MyDataSourceTest { -### 归还连接 - -归还数据库连接的方式:继承方式、装饰者设计者模式、适配器设计模式、动态代理方式 +### AOF -#### 继承方式 +#### 基本概述 -继承(无法解决) +AOF(append only file)持久化:以独立日志的方式记录每次写命令(不记录读)来记录数据库状态,**增量保存**只许追加文件但不可以改写文件,**与 RDB 相比可以理解为由记录数据改为记录数据的变化** -- 通过打印连接对象,发现 DriverManager 获取的连接实现类是 JDBC4Connection -- 自定义一个类,继承 JDBC4Connection 这个类,重写 close() 方法 -- 通过查看 JDBC 工具类获取连接的方法我们发现:我们虽然自定义了一个子类,完成了归还连接的操作。但是DriverManager 获取的还是 JDBC4Connection 这个对象,并不是我们的子类对象。 +AOF 主要作用是解决了**数据持久化的实时性**,目前已经是 Redis 持久化的主流方式 -代码实现 +AOF 写数据过程: -* 自定义继承连接类 + - ```java - //1.定义一个类,继承JDBC4Connection - public class MyConnection1 extends JDBC4Connection{ - //2.定义Connection连接对象和容器对象的成员变量 - private Connection con; - private List pool; - - //3.通过有参构造方法为成员变量赋值 - public MyConnection1(String hostToConnectTo, int portToConnectTo, Properties info, String databaseToConnectTo, String url,Connection con,List pool) throws SQLException { - super(hostToConnectTo, portToConnectTo, info, databaseToConnectTo, url); - this.con = con; - this.pool = pool; - } - - //4.重写close方法,完成归还连接 - @Override - public void close() throws SQLException { - pool.add(con); - } - } - ``` +Redis 只会将对数据库进行了修改的命令写入到 AOF 文件,并复制到各个从服务器,但是 PUBSUB 和 SCRIPT LOAD 命令例外: -* 自定义连接池类 +* PUBSUB 命令虽然没有修改数据库,但 PUBSUB 命令向频道的所有订阅者发送消息这一行为带有副作用,接收到消息的所有客户端的状态都会因为这个命令而改变,所以服务器需要使用 REDIS_FORCE_AOF 标志强制将这个命令写入 AOF 文件。这样在将来载入 AOF 文件时,服务器就可以再次执行相同的 PUBSUB 命令,并产生相同的副作用 +* SCRIPT LOAD 命令虽然没有修改数据库,但它修改了服务器状态,所以也是一个带有副作用的命令,需要使用 REDIS_FORCE_AOF - ```java - //将之前的连接对象换成自定义的子类对象 - private static MyConnection1 con; - - //4.获取数据库连接的方法 - public static Connection getConnection() { - try { - //等效于:MyConnection1 con = new JDBC4Connection(); 语法错误! - con = DriverManager.getConnection(url,username,password); - } catch (SQLException e) { - e.printStackTrace(); - } - - return con; - } - ``` - *** -#### 装饰者 +#### 持久实现 -自定义类实现 Connection 接口,通过装饰设计模式,实现和 mysql 驱动包中的 Connection 实现类相同的功能 +AOF 持久化功能的实现可以分为命令追加(append)、文件写入、文件同步(sync)三个步骤 -在实现类对每个获取的 Connection 进行装饰:把连接和连接池参数传递进行包装 -特点:通过装饰设计模式连接类我们发现,有很多需要重写的方法,代码太繁琐 -* 装饰设计模式类 +##### 命令追加 - ```java - //1.定义一个类,实现Connection接口 - public class MyConnection2 implements Connection { - //2.定义Connection连接对象和连接池容器对象的变量 - private Connection con; - private List pool; - - //3.提供有参构造方法,接收连接对象和连接池对象,对变量赋值 - public MyConnection2(Connection con,List pool) { - this.con = con; - this.pool = pool; - } - - //4.在close()方法中,完成连接的归还 - @Override - public void close() throws SQLException { - pool.add(con); - } - //5.剩余方法,只需要调用mysql驱动包的连接对象完成即可 - @Override - public Statement createStatement() throws SQLException { - return con.createStatement(); - } - .......... - } - ``` +启动 AOF 的基本配置: -* 自定义连接池类 +```sh +appendonly yes|no #开启AOF持久化功能,默认no,即不开启状态 +appendfilename filename #AOF持久化文件名,默认appendonly.aof,建议设置appendonly-端口号.aof +dir #AOF持久化文件保存路径,与RDB持久化文件路径保持一致即可 +``` - ```java - @Override - public Connection getConnection() { - if(pool.size() > 0) { - //从池中获取数据库连接 - Connection con = pool.remove(0); - //通过自定义连接对象进行包装 - MyConnection2 mycon = new MyConnection2(con,pool); - //返回包装后的连接对象 - return mycon; - }else { - throw new RuntimeException("连接数量已用尽"); - } - } - ``` +当 AOF 持久化功能处于打开状态时,服务器在执行完一个写命令之后,会以协议格式将被执行的写命令**追加**到服务器状态的 aof_buf 缓冲区的末尾 - +```c +struct redisServer { + // AOF 缓冲区 + sds aof_buf; +}; +``` -*** +*** -#### 适配器 -使用适配器设计模式改进,提供一个适配器类,实现 Connection 接口,将所有功能进行实现(除了 close 方法),自定义连接类只需要继承这个适配器类,重写需要改进的 close() 方法即可。 -特点:自定义连接类中很简洁。剩余所有的方法抽取到了适配器类中,但是适配器这个类还是我们自己编写。 +##### 文件写入 -* 适配器类 +服务器在处理文件事件时会执行**写命令,追加一些内容到 aof_buf 缓冲区**里,所以服务器每次结束一个事件循环之前,就会执行 flushAppendOnlyFile 函数,判断是否需要**将 aof_buf 缓冲区中的内容写入和保存到 AOF 文件**里 - ```java - public abstract class MyAdapter implements Connection { - - // 定义数据库连接对象的变量 - private Connection con; - - // 通过构造方法赋值 - public MyAdapter(Connection con) { - this.con = con; - } - - // 所有的方法,均调用mysql的连接对象实现 - @Override - public Statement createStatement() throws SQLException { - return con.createStatement(); - } - } - ``` +flushAppendOnlyFile 函数的行为由服务器配置的 appendfsync 选项的值来决定 -* 自定义连接类 +```sh +appendfsync always|everysec|no #AOF写数据策略:默认为everysec +``` - ```java - public class MyConnection3 extends MyAdapter { - //2.定义Connection连接对象和连接池容器对象的变量 - private Connection con; - private List pool; - - //3.提供有参构造方法,接收连接对象和连接池对象,对变量赋值 - public MyConnection3(Connection con,List pool) { - super(con); // 将接收的数据库连接对象给适配器父类传递 - this.con = con; - this.pool = pool; - } - - //4.在close()方法中,完成连接的归还 - @Override - public void close() throws SQLException { - pool.add(con); - } - } - ``` +- always:每次写入操作都将 aof_buf 缓冲区中的所有内容**写入并同步**到 AOF 文件 -* 自定义连接池类 + 特点:安全性最高,数据零误差,但是性能较低,不建议使用 - ```java - //从池中返回一个数据库连接 - @Override - public Connection getConnection() { - if(pool.size() > 0) { - //从池中获取数据库连接 - Connection con = pool.remove(0); - //通过自定义连接对象进行包装 - MyConnection3 mycon = new MyConnection3(con,pool); - //返回包装后的连接对象 - return mycon; - }else { - throw new RuntimeException("连接数量已用尽"); - } - } - ``` - +- everysec:先将 aof_buf 缓冲区中的内容写入到操作系统缓存,判断上次同步 AOF 文件的时间距离现在超过一秒钟,再次进行同步 fsync,这个同步操作是由一个(子)线程专门负责执行的 -*** + 特点:在系统突然宕机的情况下丢失 1 秒内的数据,准确性较高,性能较高,建议使用,也是默认配置 +- no:将 aof_buf 缓冲区中的内容写入到操作系统缓存,但并不进行同步,何时同步由操作系统来决定 -#### 动态代理 + 特点:**整体不可控**,服务器宕机会丢失上次同步 AOF 后的所有写指令 -使用动态代理的方式来改进 -自定义数据库连接池类: -```java -public class MyDataSource implements DataSource { - //1.准备一个容器。用于保存多个数据库连接对象 - private static List pool = Collections.synchronizedList(new ArrayList<>()); +**** - //2.定义静态代码块,获取多个连接对象保存到容器中 - static{ - for(int i = 1; i <= 10; i++) { - Connection con = JDBCUtils.getConnection(); - pool.add(con); - } - } - //3.提供一个获取连接池大小的方法 - public int getSize() { - return pool.size(); - } - //动态代理方式 - @Override - public Connection getConnection() throws SQLException { - if(pool.size() > 0) { - Connection con = pool.remove(0); - Connection proxyCon = (Connection) Proxy.newProxyInstance( - con.getClass().getClassLoader(), new Class[]{Connection.class}, - new InvocationHandler() { - /* - 执行Connection实现类连接对象所有的方法都会经过invoke - 如果是close方法,归还连接 - 如果不是,直接执行连接对象原有的功能即可 - */ - @Override - public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { - if(method.getName().equals("close")) { - //归还连接 - pool.add(con); - return null; - }else { - return method.invoke(con,args); - } - } - }); - return proxyCon; - }else { - throw new RuntimeException("连接数量已用尽"); - } - } -} -``` +##### 文件同步 +在现代操作系统中,当用户调用 write 函数将数据写入文件时,操作系统通常会将写入数据暂时保存在一个内存缓冲区空间,等到缓冲区**写满或者到达特定时间周期**,才真正地将缓冲区中的数据写入到磁盘里面(刷脏) +* 优点:提高文件的写入效率 +* 缺点:为写入数据带来了安全问题,如果计算机发生停机,那么保存在内存缓冲区里面的写入数据将会丢失 -*** +系统提供了 fsync 和 fdatasync 两个同步函数做**强制硬盘同步**,可以让操作系统立即将缓冲区中的数据写入到硬盘里面,函数会阻塞到写入硬盘完成后返回,保证了数据持久化 +异常恢复:AOF 文件损坏,通过 redis-check-aof--fix appendonly.aof 进行恢复,重启 Redis,然后重新加载 -### 开源项目 -#### C3P0 -使用 C3P0 连接池: -* 配置文件名称:c3p0-config.xml,必须放在 src 目录下 +*** - ```xml - - - - - com.mysql.jdbc.Driver - jdbc:mysql://192.168.2.184:3306/db14 - root - 123456 - - - - 5 - - 10 - - 3000 - - - - - - - - ``` - -* 代码演示 - ```java - public class C3P0Test1 { - public static void main(String[] args) throws Exception{ - //1.创建c3p0的数据库连接池对象 - DataSource dataSource = new ComboPooledDataSource(); - - //2.通过连接池对象获取数据库连接 - Connection con = dataSource.getConnection(); - - //3.执行操作 - String sql = "SELECT * FROM student"; - PreparedStatement pst = con.prepareStatement(sql); - - //4.执行sql语句,接收结果集 - ResultSet rs = pst.executeQuery(); - - //5.处理结果集 - while(rs.next()) { - System.out.println(rs.getInt("sid") + "\t" + rs.getString("name") + "\t" + rs.getInt("age") + "\t" + rs.getDate("birthday")); - } - - //6.释放资源 - rs.close(); pst.close(); con.close(); - } - } - ``` - +#### 文件载入 -#### Druid +AOF 文件里包含了重建数据库状态所需的所有写命令,所以服务器只要读入并重新执行一遍 AOF 文件里的命令,就还原服务器关闭之前的数据库状态,服务器在启动时,还原数据库状态打印的日志: -Druid 连接池: +```sh +[8321] 05 Sep 11:58:50.449 * DB loaded from append only file: 0.000 seconds +``` -* 配置文件:druid.properties,必须放在src目录下 +AOF 文件里面除了用于指定数据库的 SELECT 命令是服务器自动添加的,其他都是通过客户端发送的命令 - ```properties - driverClassName=com.mysql.jdbc.Driver - url=jdbc:mysql://192.168.2.184:3306/db14 - username=root - password=123456 - initialSize=5 - maxActive=10 - maxWait=3000 - ``` +```sh +* 2\r\n$6\r\nSELECT\r\n$1\r\n0\r\n # 服务器自动添加 +* 3\r\n$3\r\nSET\r\n$3\r\nmsg\r\n$5\r\nhello\r\n +* 5\r\n$4\r\nSADD\r\n$6\r\nfruits\r\n$5\r\napple\r\n$6\r\nbanana\r\n$6\r\ncherry\r\n +``` -* 代码演示 +Redis 读取 AOF 文件并还原数据库状态的步骤: + +* 创建一个**不带网络连接的伪客户端**(fake client)执行命令,因为 Redis 的命令只能在客户端上下文中执行, 而载入 AOF 文件时所使用的命令来源于本地 AOF 文件而不是网络连接 +* 从 AOF 文件分析并读取一条写命令 +* 使用伪客户端执行被读出的写命令,然后重复上述步骤 - ```java - public class DruidTest1 { - public static void main(String[] args) throws Exception{ - //获取配置文件的流对象 - InputStream is = DruidTest1.class.getClassLoader().getResourceAsStream("druid.properties"); - - //1.通过Properties集合,加载配置文件 - Properties prop = new Properties(); - prop.load(is); - - //2.通过Druid连接池工厂类获取数据库连接池对象 - DataSource dataSource = DruidDataSourceFactory.createDataSource(prop); - - //3.通过连接池对象获取数据库连接进行使用 - Connection con = dataSource.getConnection(); - - //4.执行sql语句,接收结果集 - String sql = "SELECT * FROM student"; - PreparedStatement pst = con.prepareStatement(sql); - ResultSet rs = pst.executeQuery(); - - //5.处理结果集 - while(rs.next()) { - System.out.println(rs.getInt("sid") + "\t" + rs.getString("name") + "\t" + rs.getInt("age") + "\t" + rs.getDate("birthday")); - } - - //6.释放资源 - rs.close(); pst.close(); con.close(); - } - } - - ``` -### 工具类 -数据库连接池的工具类: +**** -```java -public class DataSourceUtils { - //1.私有构造方法 - private DataSourceUtils(){} - //2.声明数据源变量 - private static DataSource dataSource; - //3.提供静态代码块,完成配置文件的加载和获取数据库连接池对象 - static{ - try{ - //完成配置文件的加载 - InputStream is = DataSourceUtils.class.getClassLoader().getResourceAsStream("druid.properties"); - Properties prop = new Properties(); - prop.load(is); - - //获取数据库连接池对象 - dataSource = DruidDataSourceFactory.createDataSource(prop); - } catch (Exception e) { - e.printStackTrace(); - } - } +#### 重写实现 - //4.提供一个获取数据库连接的方法 - public static Connection getConnection() { - Connection con = null; - try { - con = dataSource.getConnection(); - } catch (SQLException e) { - e.printStackTrace(); - } - return con; - } +##### 重写策略 - //5.提供一个获取数据库连接池对象的方法 - public static DataSource getDataSource() { - return dataSource; - } +AOF 重写:读取服务器当前的数据库状态,**生成新 AOF 文件来替换旧 AOF 文件**,不会对现有的 AOF 文件进行任何读取、分析或者写入操作,而是直接原子替换。新 AOF 文件不会包含任何浪费空间的冗余命令,所以体积通常会比旧 AOF 文件小得多 - //6.释放资源 - public static void close(Connection con, Statement stat, ResultSet rs) { - if(con != null) { - try { - con.close(); - } catch (SQLException e) { - e.printStackTrace(); - } - } +AOF 重写规则: - if(stat != null) { - try { - stat.close(); - } catch (SQLException e) { - e.printStackTrace(); - } - } +- 进程内具有时效性的数据,并且数据已超时将不再写入文件 - if(rs != null) { - try { - rs.close(); - } catch (SQLException e) { - e.printStackTrace(); - } - } - } - //方法重载 - public static void close(Connection con, Statement stat) { - if(con != null) { - try { - con.close(); - } catch (SQLException e) { - e.printStackTrace(); - } - } - if(stat != null) { - try { - stat.close(); - } catch (SQLException e) { - e.printStackTrace(); - } - } - } -} +- 对同一数据的多条写命令合并为一条命令,因为会读取当前的状态,所以直接将当前状态转换为一条命令即可。为防止数据量过大造成客户端缓冲区溢出,对 list、set、hash、zset 等集合类型,**单条指令**最多写入 64 个元素 + + 如 lpushlist1 a、lpush list1 b、lpush list1 c 可以转化为:lpush list1 a b c + +- 非写入类的无效指令将被忽略,只保留最终数据的写入命令,但是 select 指令虽然不更改数据,但是更改了数据的存储位置,此类命令同样需要记录 + +AOF 重写作用: + +- 降低磁盘占用量,提高磁盘利用率 +- 提高持久化效率,降低持久化写时间,提高 IO 性能 +- 降低数据恢复的用时,提高数据恢复效率 + + + +*** + + +##### 重写原理 + +AOF 重写程序 aof_rewrite 函数可以创建一个新 AOF 文件, 但是该函数会进行大量的写入操作,调用这个函数的线程将被长时间阻塞,所以 Redis 将 AOF 重写程序放到 fork 的子进程里执行,不会阻塞父进程,重写命令: + +```sh +bgrewriteaof ``` +* 子进程进行 AOF 重写期间,服务器进程(父进程)可以继续处理命令请求 + +* 子进程带有服务器进程的数据副本,使用子进程而不是线程,可以在避免使用锁的情况下, 保证数据安全性 + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-AOF手动重写原理.png) + +子进程在进行 AOF 重写期间,服务器进程还需要继续处理命令请求,而新命令可能会对现有的数据库状态进行修改,从而使得服务器当前的数据库状态和重写后的 AOF 文件所保存的数据库状态不一致,所以 Redis 设置了 AOF 重写缓冲区 +工作流程: +* Redis 服务器执行完一个写命令,会同时将该命令追加到 AOF 缓冲区和 AOF 重写缓冲区(从创建子进程后才开始写入) +* 当子进程完成 AOF 重写工作之后,会向父进程发送一个信号,父进程在接到该信号之后, 会调用一个信号处理函数,该函数执行时会**对服务器进程(父进程)造成阻塞**(影响很小,类似 JVM STW),主要工作: + * 将 AOF 重写缓冲区中的所有内容写入到新 AOF 文件中, 这时新 AOF 文件所保存的状态将和服务器当前的数据库状态一致 + * 对新的 AOF 文件进行改名,**原子地(atomic)覆盖**现有的 AOF 文件,完成新旧两个 AOF 文件的替换 @@ -8215,36 +12779,73 @@ public class DataSourceUtils { +##### 自动重写 +触发时机:Redis 会记录上次重写时的 AOF 大小,默认配置是当 AOF 文件大小是上次重写后大小的一倍且文件大于 64M 时触发 -# Redis +```sh +auto-aof-rewrite-min-size size #设置重写的基准值,最小文件 64MB,达到这个值开始重写 +auto-aof-rewrite-percentage percent #触发AOF文件执行重写的增长率,当前AOF文件大小超过上一次重写的AOF文件大小的百分之多少才会重写,比如文件达到 100% 时开始重写就是两倍时触发 +``` -## NoSQL +自动重写触发比对参数( 运行指令 `info Persistence` 获取具体信息 ): -### 概述 +```sh +aof_current_size #AOF文件当前尺寸大小(单位:字节) +aof_base_size #AOF文件上次启动和重写时的尺寸大小(单位:字节) +``` -NoSQL(Not-Only SQL):泛指非关系型的数据库,作为关系型数据库的补充。 +自动重写触发条件公式: -MySQL 支持 ACID 特性,保证可靠性和持久性,读取性能不高,因此需要缓存的来减缓数据库的访问压力。 +- aof_current_size > auto-aof-rewrite-min-size +- (aof_current_size - aof_base_size) / aof_base_size >= auto-aof-rewrite-percentage -作用:应对基于海量用户和海量数据前提下的数据处理问题 -特征: -* 可扩容,可伸缩,SQL 数据关系过于复杂,Nosql 不存关系,只存数据 -* 大数据量下高性能,数据不存取在磁盘 IO,存取在内存 -* 灵活的数据模型,设计了一些数据存储格式,能保证效率上的提高 -* 高可用,集群 -常见的 Nosql:Redis、memcache、HBase、MongoDB -![](https://gitee.com/seazean/images/raw/master/DB/电商场景解决方案.png) +**** -参考视频:https://www.bilibili.com/video/BV1CJ411m7Gc +### 对比 + +RDB 的特点 + +* RDB 优点: + - RDB 是一个紧凑压缩的二进制文件,存储效率较高,但存储数据量较大时,存储效率较低 + - RDB 内部存储的是 Redis 在某个时间点的数据快照,非常**适合用于数据备份,全量复制、灾难恢复** + - RDB 恢复数据的速度要比 AOF 快很多,因为是快照,直接恢复 +* RDB 缺点: + + - BGSAVE 指令每次运行要执行 fork 操作创建子进程,会牺牲一些性能 + - RDB 方式无论是执行指令还是利用配置,无法做到实时持久化,具有丢失数据的可能性,最后一次持久化后的数据可能丢失 + - Redis 的众多版本中未进行 RDB 文件格式的版本统一,可能出现各版本之间数据格式无法兼容 + +AOF 特点: -参考视频:https://www.bilibili.com/video/BV1Rv41177Af +* AOF 的优点:数据持久化有**较好的实时性**,通过 AOF 重写可以降低文件的体积 +* AOF 的缺点:文件较大时恢复较慢 + +AOF 和 RDB 同时开启,系统默认取 AOF 的数据(数据不会存在丢失) + +应用场景: + +- 对数据**非常敏感**,建议使用默认的 AOF 持久化方案,AOF 持久化策略使用 everysecond,每秒钟 fsync 一次,该策略 Redis 仍可以保持很好的处理性能 + + 注意:AOF 文件存储体积较大,恢复速度较慢,因为要执行每条指令 + +- 数据呈现**阶段有效性**,建议使用 RDB 持久化方案,可以做到阶段内无丢失,且恢复速度较快 + + 注意:利用 RDB 实现紧凑的数据持久化,存储数据量较大时,存储效率较低 + +综合对比: + +- RDB 与 AOF 的选择实际上是在做一种权衡,每种都有利有弊 +- 灾难恢复选用 RDB +- 如不能承受数分钟以内的数据丢失,对业务数据非常敏感,选用 AOF;如能承受数分钟以内的数据丢失,且追求大数据集的恢复速度,选用 RDB +- 双保险策略,同时开启 RDB 和 AOF,重启后 Redis 优先使用 AOF 来恢复数据,降低丢失数据的量 +- 不建议单独用 AOF,因为可能会出现 Bug,如果只是做纯内存缓存,可以都不用 @@ -8252,34 +12853,33 @@ MySQL 支持 ACID 特性,保证可靠性和持久性,读取性能不高, -### Redis +### fork -Redis (REmote DIctionary Server) :用 C 语言开发的一个开源的高性能键值对(key-value)数据库。 +#### 介绍 -特征: +fork() 函数创建一个子进程,子进程与父进程几乎是完全相同的进程,系统先给子进程分配资源,然后把父进程的所有数据都复制到子进程中,只有少数值与父进程的值不同,相当于克隆了一个进程 -* 数据间没有必然的关联关系,**不存关系,只存数据** -* 数据**存储在内存**,存取速度快,解决了磁盘 IO 速度慢的问题 -* 内部采用**单线程**机制进行工作 -* 高性能,官方测试数据,50个并发执行100000 个请求,读的速度是110000 次/s,写的速度是81000次/s -* 多数据类型支持 - * 字符串类型:string(String) - * 列表类型:list(LinkedList) - * 散列类型:hash(HashMap) - * 集合类型:set(HashSet) - * 有序集合类型:zset/sorted_set(TreeSet) -* 支持持久化,可以进行数据灾难恢复 +在完成对其调用之后,会产生 2 个进程,且每个进程都会**从 fork() 的返回处开始执行**,这两个进程将执行相同的程序段,但是拥有各自不同的堆段,栈段,数据段,每个子进程都可修改各自的数据段,堆段,和栈段 -应用: +```c +#include +pid_t fork(void); +// 父进程返回子进程的pid,子进程返回0,错误返回负值,根据返回值的不同进行对应的逻辑处理 +``` + +fork 调用一次,却能够**返回两次**,可能有三种不同的返回值: -* 为热点数据加速查询(主要场景),如热点商品、热点新闻、热点资讯、推广类等高访问量信息等 +* 在父进程中,fork 返回新创建子进程的进程 ID +* 在子进程中,fork 返回 0 +* 如果出现错误,fork 返回一个负值,错误原因: + * 当前的进程数已经达到了系统规定的上限,这时 errno 的值被设置为 EAGAIN + * 系统内存不足,这时 errno 的值被设置为 ENOMEM -* 即时信息查询,如排行榜、网站访问统计、公交到站信息、在线人数(聊天室、网站)、设备信号等 +fpid 的值在父子进程中不同:进程形成了链表,父进程的 fpid 指向子进程的进程 id,因为子进程没有子进程,所以其 fpid 为0 -* 时效性信息控制,如验证码控制、投票控制等 +创建新进程成功后,系统中出现两个基本完全相同的进程,这两个进程执行没有固定的先后顺序,哪个进程先执行要看系统的调度策略 -* 分布式数据共享,如分布式集群架构中的 session 分离 -* 消息队列 +每个进程都有一个独特(互不相同)的进程标识符 process ID,可以通过 getpid() 函数获得;还有一个记录父进程 pid 的变量,可以通过 getppid() 函数获得变量的值 @@ -8287,97 +12887,128 @@ Redis (REmote DIctionary Server) :用 C 语言开发的一个开源的高性 -### 安装启动 +#### 使用 + +基本使用: + +```c +#include +#include +int main () +{ + pid_t fpid; // fpid表示fork函数返回的值 + int count = 0; + fpid = fork(); + if (fpid < 0) + printf("error in fork!"); + else if (fpid == 0) { + printf("i am the child process, my process id is %d/n", getpid()); + count++; + } + else { + printf("i am the parent process, my process id is %d/n", getpid()); + count++; + } + printf("count: %d/n",count);// 1 + return 0; +} +/* 输出内容: + i am the child process, my process id is 5574 + count: 1 + i am the parent process, my process id is 5573 + count: 1 +*/ +``` -#### CentOS +进阶使用: -1. 下载 Redis +```c +#include +#include +int main(void) +{ + int i = 0; + // ppid 指当前进程的父进程pid + // pid 指当前进程的pid, + // fpid 指fork返回给当前进程的值,在这可以表示子进程 + for(i = 0; i < 2; i++){ + pid_t fpid = fork(); + if(fpid == 0) + printf("%d child %4d %4d %4d/n",i, getppid(), getpid(), fpid); + else + printf("%d parent %4d %4d %4d/n",i, getppid(), getpid(),fpid); + } + return 0; +} +/*输出内容: + i 父id id 子id + 0 parent 2043 3224 3225 + 0 child 3224 3225 0 + 1 parent 2043 3224 3226 + 1 parent 3224 3225 3227 + 1 child 1 3227 0 + 1 child 1 3226 0 +*/ +``` - 下载安装包: + - ```sh - wget http://download.redis.io/releases/redis-5.0.0.tar.gz - ``` +在 p3224 和 p3225 执行完第二个循环后,main 函数退出,进程死亡。所以 p3226,p3227 就没有父进程了,成为孤儿进程,所以 p3226 和 p3227 的父进程就被置为 ID 为 1 的 init 进程(笔记 Tool → Linux → 进程管理详解) - 解压安装包: +参考文章:https://blog.csdn.net/love_gaohz/article/details/41727415 - ```sh - tar –xvf redis-5.0.0.tar.gz - ``` - 编译(在解压的目录中执行): - ```sh - make - ``` +*** - 安装(在解压的目录中执行): - ```sh - make install - ``` - +#### 内存 -2. 安装 Redis +fork() 调用之后父子进程的内存关系 - redis-server,服务器启动命令 客户端启动命令 +早期 Linux 的 fork() 实现时,就是全部复制,这种方法效率太低,而且造成了很大的内存浪费,现在 Linux 实现采用了两种方法: - redis-cli,redis核心配置文件 +* 父子进程的代码段是相同的,所以代码段是没必要复制的,只需内核将代码段标记为只读,父子进程就共享此代码段。fork() 之后在进程创建代码段时,子进程的进程级页表项都指向和父进程相同的物理页帧 - redis.conf,RDB文件检查工具(快照持久化文件) + - redis-check-dump,AOF文件修复工具 +* 对于父进程的数据段,堆段,栈段中的各页,由于父子进程相互独立,采用**写时复制 COW** 的技术,来提高内存以及内核的利用率 - redis-check-aof + 在 fork 之后两个进程用的是相同的物理空间(内存区),子进程的代码段、数据段、堆栈都是指向父进程的物理空间,**两者的虚拟空间不同,但其对应的物理空间是同一个**,当父子进程中有更改相应段的行为发生时,再为子进程相应的段分配物理空间。如果两者的代码完全相同,代码段继续共享父进程的物理空间;而如果两者执行的代码不同,子进程的代码段也会分配单独的物理空间。 + fork 之后内核会将子进程放在队列的前面,让子进程先执行,以免父进程执行导致写时复制,而后子进程再执行,因无意义的复制而造成效率的下降 + -*** +补充知识: +vfork(虚拟内存 fork virtual memory fork):调用 vfork() 父进程被挂起,子进程使用父进程的地址空间。不采用写时复制,如果子进程修改父地址空间的任何页面,这些修改过的页面对于恢复的父进程是可见的 -#### Ubuntu -安装: +参考文章:https://blog.csdn.net/Shreck66/article/details/47039937 -* Redis 5.0 被包含在默认的 Ubuntu 20.04 软件源中 - ```sh - sudo apt update - sudo apt install redis-server - ``` -* 检查Redis状态 - ```sh - sudo systemctl status redis-server - ``` -启动: +**** -* 启动服务器——参数启动 - ```sh - redis-server [--port port] - #redis-server --port 6379 - ``` -* 启动服务器——配置文件启动 - ```sh - redis-server config_file_name - #redis-server /etc/redis/conf/redis-6397.conf - ``` -* 启动客户端: +## 事务机制 + +### 事务特征 + +Redis 事务就是将多个命令请求打包,然后**一次性、按顺序**地执行多个命令的机制,并且在事务执行期间,服务器不会中断事务去执行其他的命令请求,会将事务中的所有命令都执行完毕,然后才去处理其他客户端的命令请求,Redis 事务的特性: + +* Redis 事务**没有隔离级别**的概念,队列中的命令在事务没有提交之前都不会实际被执行 +* Redis 单条命令式保存原子性的,但是事务**不保证原子性**,事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚 - ```sh - redis-cli [-h host] [-p port] - #redis-cli -h 192.168.2.185 -p 6397 - ``` - 注意:服务器启动指定端口使用的是--port,客户端启动指定端口使用的是-p @@ -8385,31 +13016,53 @@ Redis (REmote DIctionary Server) :用 C 语言开发的一个开源的高性 -### 基本配置 +### 工作流程 -#### 系统目录 +事务的执行流程分为三个阶段: -1. 创建文件结构 +* 事务开始:MULTI 命令的执行标志着事务的开始,通过在客户端状态的 flags 属性中打开 REDIS_MULTI 标识,将执行该命令的客户端从非事务状态切换至事务状态 - 创建配置文件存储目录 + ```sh + MULTI # 设定事务的开启位置,此指令执行后,后续的所有指令均加入到事务中 + ``` - ```sh - mkdir conf - ``` +* 命令入队:事务队列以先进先出(FIFO)的方式保存入队的命令,每个 Redis 客户端都有事务状态,包含着事务队列: - 创建服务器文件存储目录(包含日志、数据、临时配置文件等) + ```c + typedef struct redisClient { + // 事务状态 + multiState mstate; /* MULTI/EXEC state */ + } + + typedef struct multiState { + // 事务队列,FIFO顺序 + multiCmd *commands; + + // 已入队命令计数 + int count; + } + ``` - ```sh - mkdir data - ``` + * 如果命令为 EXEC、DISCARD、WATCH、MULTI 四个命中的一个,那么服务器立即执行这个命令 + * 其他命令服务器不执行,而是将命令放入一个事务队列里面,然后向客户端返回 QUEUED 回复 + +* 事务执行:EXEC 提交事务给服务器执行,服务器会遍历这个客户端的事务队列,执行队列中的命令并将执行结果返回 + + ```sh + EXEC # Commit 提交,执行事务,与multi成对出现,成对使用 + ``` + +事务取消的方法: + +* 取消事务: + + ```sh + DISCARD # 终止当前事务的定义,发生在multi之后,exec之前 + ``` + + 一般用于事务执行过程中输入了错误的指令,直接取消这次事务,类似于回滚 -2. 创建配置文件副本放入 conf 目录,Ubuntu系统配置文件 redis.conf 在目录 `/etc/redis` 中 - ```sh - cat redis.conf | grep -v "#" | grep -v "^$" -> /conf/redis-6379.conf - ``` - - 去除配置文件的注释和空格,输出到新的文件,命令方式采用 redis-port.conf @@ -8417,138 +13070,116 @@ Redis (REmote DIctionary Server) :用 C 语言开发的一个开源的高性 -#### 服务器 +### WATCH + +#### 监视机制 + +WATCH 命令是一个乐观锁(optimistic locking),可以在 EXEC 命令执行之前,监视任意数量的数据库键,并在 EXEC 命令执行时,检查被监视的键是否至少有一个已经被修改过了,如果是服务器将拒绝执行事务,并向客户端返回代表事务执行失败的空回复 -* 设置服务器以守护进程的方式运行,关闭后服务器控制台中将打印服务器运行信息(同日志内容相同): +* 添加监控锁 ```sh - daemonize yes|no + WATCH key1 [key2……] #可以监控一个或者多个key ``` -* 绑定主机地址,绑定本地IP地址,否则SSH无法访问: +* 取消对所有 key 的监视 ```sh - bind ip + UNWATCH ``` -* 设置服务器端口: - ```sh - port port - ``` -* 设置服务器文件保存地址: +*** - ```sh - dir path - ``` -* 设置数据库的数量: - ```sh - databases 16 - ``` +#### 实现原理 -* 多服务器快捷配置: +每个 Redis 数据库都保存着一个 watched_keys 字典,键是某个被 WATCH 监视的数据库键,值则是一个链表,记录了所有监视相应数据库键的客户端: - 导入并加载指定配置文件信息,用于快速创建 redis 公共配置较多的 redis 实例配置文件,便于维护 +```c +typedef struct redisDb { + // 正在被 WATCH 命令监视的键 + dict *watched_keys; +} +``` - ```sh - include /path/conf_name.conf - ``` +所有对数据库进行修改的命令,在执行后都会调用 `multi.c/touchWatchKey` 函数对 watched_keys 字典进行检查,是否有客户端正在监视刚被命令修改过的数据库键,如果有的话函数会将监视被修改键的客户端的 REDIS_DIRTY_CAS 标识打开,表示该客户端的事务安全性已经被破坏 - +服务器接收到个客户端 EXEC 命令时,会根据这个客户端是否打开了 REDIS_DIRTY_CAS 标识,如果打开了说明客户端提交事务不安全,服务器会拒绝执行 -*** -#### 客户端 -* 服务器允许客户端连接最大数量,默认0,表示无限制,当客户端连接到达上限后,Redis会拒绝新的连接: +**** - ```sh - maxclients count - ``` -* 客户端闲置等待最大时长,达到最大值后关闭对应连接,如需关闭该功能,设置为 0: - ```sh - timeout seconds - ``` +### ACID +#### 原子性 +事务具有原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability) -*** +原子性指事务队列中的命令要么就全部都执行,要么一个都不执行,但是在命令执行出错时,不会保证原子性(下一节详解) +Redis 不支持事务回滚机制(rollback),即使事务队列中的某个命令在执行期间出现了错误,整个事务也会继续执行下去,直到将事务队列中的所有命令都执行完毕为止 +回滚需要程序员在代码中实现,应该尽可能避免: -#### 日志配置 +* 事务操作之前记录数据的状态 -* 设置服务器以指定日志记录级别: + * 单数据:string - ```sh - loglevel debug|verbose|notice|warning - ``` + * 多数据:hash、list、set、zset -* 日志记录文件名 - ```sh - logfile filename - ``` +* 设置指令恢复所有的被修改的项 -注意:日志级别开发期设置为 verbose 即可,生产环境中配置为 notice,简化日志输出量,降低写日志IO的频度 + * 单数据:直接 set(注意周边属性,例如时效) + * 多数据:修改对应值或整体克隆复制 -**配置文件:** -```sh -bind 192.168.2.185 -port 6379 -#timeout 0 -daemonize no -logfile /etc/redis/data/redis-6379.log -dir /etc/redis/data -dbfilename "dump-6379.rdb" -``` +*** +#### 一致性 +事务具有一致性指的是,数据库在执行事务之前是一致的,那么在事务执行之后,无论事务是否执行成功,数据库也应该仍然是一致的 -*** +一致是数据符合数据库的定义和要求,没有包含非法或者无效的错误数据,Redis 通过错误检测和简单的设计来保证事务的一致性: +* 入队错误:命令格式输入错误,出现语法错误造成,**整体事务中所有命令均不会执行**,包括那些语法正确的命令 + -## 体系结构 +* 执行错误:命令执行出现错误,例如对字符串进行 incr 操作,事务中正确的命令会被执行,运行错误的命令不会被执行 -### 存储对象 + -Redis 使用对象来表示数据库中的键和值,当在 Redis 数据库中新创建一个键值对时至少会创建两个对象,一个对象用作键值对的键(键对象),另一个对象用作键值对的值(值对象) +* 服务器停机: -Redis 中对象由一个 redisObject 结构表示,该结构中和保存数据有关的三个属性分别是 type、 encoding、ptr: + * 如果服务器运行在无持久化的内存模式下,那么重启之后的数据库将是空白的,因此数据库是一致的 + * 如果服务器运行在持久化模式下,重启之后将数据库还原到一致的状态 -```c -typedef struct redisObiect{ - //类型 - unsigned type:4; - //编码 - unsigned encoding:4; - //指向底层数据结构的指针 - void *ptr; -} -``` -Redis 中主要数据结构有:简单动态字符串(SDS)、双端链表、字典、压缩列表、整数集合、跳跃表 -Redis 并没有直接使用数据结构来实现键值对数据库,而是基于这些数据结构创建了一个对象系统,包含字符串对象、列表对象、哈希对象、集合对象和有序集合对象这五种类型的对象,而每种对象又通过不同的编码映射到不同的底层数据结构 -Redis 自身是一个 Map,其中所有的数据都是采用 key : value 的形式存储,**键对象都是字符串对象**,而值对象有五种基本类型和三种高级类型对象 +*** + -![](https://gitee.com/seazean/images/raw/master/DB/Redis-对象模型.png) +#### 隔离性 + +Redis 是一个单线程的执行原理,所以对于隔离性,分以下两种情况: +* 并发操作在 EXEC 命令前执行,隔离性的保证要使用 WATCH 机制来实现,否则隔离性无法保证 +* 并发操作在 EXEC 命令后执行,隔离性可以保证 @@ -8556,52 +13187,58 @@ Redis 自身是一个 Map,其中所有的数据都是采用 key : value 的形 -### 线程模型 +#### 持久性 -Redis 基于 Reactor 模式开发了网络事件处理器,这个处理器被称为文件事件处理器(file event handler),这个文件事件处理器是单线程的,所以 Redis 叫做单线程的模型 +Redis 并没有为事务提供任何额外的持久化功能,事务的持久性由 Redis 所使用的持久化模式决定 -文件事件处理器以单线程方式运行,但是使用 I/O 多路复用程序来监听多个套接字,既实现了高性能的网络通信模型,又很好地与 Redis 服务器中其他同样以单线程方式运行的模块进行对接,保持了 Redis 单线程设计的简单性 +配置选项 `no-appendfsync-on-rewrite` 可以配合 appendfsync 选项在 AOF 持久化模式使用: -工作原理: +* 选项打开时在执行 BGSAVE 或者 BGREWRITEAOF 期间,服务器会暂时停止对 AOF 文件进行同步,从而尽可能地减少 I/O 阻塞 +* 选项打开时运行在 always 模式的 AOF 持久化,事务也不具有持久性,所以该选项默认关闭 -* 文件事件处理器使用 I/O 多路复用(multiplexing)程序来同时监听多个套接字,并根据套接字目前执行的任务来为套接字关联不同的事件处理器 +在一个事务的最后加上 SAVE 命令总可以保证事务的耐久性 -* 当被监听的套接字准备好执行连接应答 (accept)、读取 (read)、写入 (write)、关闭 (close) 等操作时,与操作相对应的文件事件就会产生,这时文件事件处理器会将处理请求放入**单线程的执行队列**中,等待调用套接字关联好的事件处理器来处理事件 -**Redis 单线程也能高效的原因**: -* 纯内存操作 -* 核心是基于非阻塞的 IO 多路复用机制,单线程可以高效处理多个请求 -* 底层使用 C 语言实现,C 语言实现的程序距离操作系统更近,执行速度相对会更快 -* 单线程同时也**避免了多线程的上下文频繁切换问题**,预防了多线程可能产生的竞争问题 +*** -**** +## Lua 脚本 -### 多线程 +### 环境创建 -Redis6.0 引入多线程主要是为了提高网络 IO 读写性能,因为这是 Redis 的一个性能瓶颈(Redis 的瓶颈主要受限于内存和网络),多线程只是用来**处理网络数据的读写和协议解析**, 执行命令仍然是单线程顺序执行,因此不需要担心线程安全问题。 +#### 基本介绍 -Redis6.0 的多线程默认是禁用的,只使用主线程。如需开启需要修改 redis 配置文件 `redis.conf` : +Redis 对 Lua 脚本支持,通过在服务器中嵌入 Lua 环境,客户端可以使用 Lua 脚本直接在服务器端**原子地执行**多个命令 ```sh -io-threads-do-reads yesCopy to clipboardErrorCopied +EVAL + +``` + +web.xml配置:请求响应章节请求中的web.xml配置 + +```xml +CharacterEncodingFilter + DispatcherServlet +``` + +spring-mvc.xml: + +```xml + + + +``` + + + +**** + + + +### 响应数据 + +注解:@ResponseBody + +作用:将 Java 对象转为 json 格式的数据 + +方法返回值为 POJO 时,自动封装数据成 Json 对象数据: + +```java +@RequestMapping("/ajaxReturnJson") +@ResponseBody +public User ajaxReturnJson(){ + System.out.println("controller return json pojo..."); + User user = new User("Jockme",40); + return user; +} +``` + +方法返回值为 List 时,自动封装数据成 json 对象数组数据: + +```java +@RequestMapping("/ajaxReturnJsonList") +@ResponseBody +//基于jackon技术,使用@ResponseBody注解可以将返回的保存POJO对象的集合转成json数组格式数据 +public List ajaxReturnJsonList(){ + System.out.println("controller return json list..."); + User user1 = new User("Tom",3); + User user2 = new User("Jerry",5); + + ArrayList al = new ArrayList(); + al.add(user1); + al.add(user2); + return al; +} +``` + +AJAX 文件: + +```js +//为id="testAjaxReturnString"的组件绑定点击事件 +$("#testAjaxReturnString").click(function(){ + //发送异步调用 + $.ajax({ + type:"POST", + url:"ajaxReturnString", + //回调函数 + success:function(data){ + //打印返回结果 + alert(data); + } + }); +}); + +//为id="testAjaxReturnJson"的组件绑定点击事件 +$("#testAjaxReturnJson").click(function(){ + $.ajax({ + type:"POST", + url:"ajaxReturnJson", + success:function(data){ + alert(data['name']+" , "+data['age']); + } + }); +}); + +//为id="testAjaxReturnJsonList"的组件绑定点击事件 +$("#testAjaxReturnJsonList").click(function(){ + $.ajax({ + type:"POST", + url:"ajaxReturnJsonList", + success:function(data){ + alert(data); + alert(data[0]["name"]); + alert(data[1]["age"]); + } + }); +}); +``` + + + +**** + + + +### 跨域访问 + +跨域访问:当通过域名 A 下的操作访问域名 B 下的资源时,称为跨域访问,跨域访问时,会出现无法访问的现象 + +环境搭建: + +* 为当前主机添加备用域名 + * 修改 windows 安装目录中的 host 文件 + * 格式: ip 域名 +* 动态刷新 DNS + * 命令: ipconfig /displaydns + * 命令: ipconfig /flushdns + +跨域访问支持: + +* 名称:@CrossOrigin +* 类型:方法注解 、 类注解 +* 位置:处理器类中的方法上方或类上方 +* 作用:设置当前处理器方法 / 处理器类中所有方法支持跨域访问 +* 范例: + +```java +@RequestMapping("/cross") +@ResponseBody +//使用@CrossOrigin开启跨域访问 +//标注在处理器方法上方表示该方法支持跨域访问 +//标注在处理器类上方表示该处理器类中的所有处理器方法均支持跨域访问 +@CrossOrigin +public User cross(HttpServletRequest request){ + System.out.println("controller cross..." + request.getRequestURL()); + User user = new User("Jockme",36); + return user; +} +``` + +* jsp 文件 + +```html +跨域访问
+ + +``` + + + + + +*** + + + + + +## 拦截器 + +### 基本介绍 + +拦截器(Interceptor)是一种动态拦截方法调用的机制 + +作用: + +1. 在指定的方法调用前后执行预先设定后的的代码 +2. 阻止原始方法的执行 + +核心原理:AOP 思想 + +拦截器链:多个拦截器按照一定的顺序,对原始被调用功能进行增强 + +拦截器和过滤器对比: + +1. 归属不同: Filter 属于 Servlet 技术, Interceptor 属于 SpringMVC 技术 + +2. 拦截内容不同: Filter 对所有访问进行增强, Interceptor 仅针对 SpringMVC 的访问进行增强 + + + + + +*** + + + +### 处理方法 + +#### 前置处理 + +原始方法之前运行: + +```java +public boolean preHandle(HttpServletRequest request, + HttpServletResponse response, + Object handler) throws Exception { + System.out.println("preHandle"); + return true; +} +``` + +* 参数: + * request:请求对象 + * response:响应对象 + * handler:被调用的处理器对象,本质上是一个方法对象,对反射中的Method对象进行了再包装 + * handler:public String controller.InterceptorController.handleRun + * handler.getClass():org.springframework.web.method.HandlerMethod +* 返回值: + * 返回值为 false,被拦截的处理器将不执行 + + + +*** + + + +#### 后置处理 + +原始方法运行后运行,如果原始方法被拦截,则不执行: + +```java +public void postHandle(HttpServletRequest request, + HttpServletResponse response, + Object handler, + ModelAndView modelAndView) throws Exception { + System.out.println("postHandle"); +} +``` + +参数: + +* modelAndView:如果处理器执行完成具有返回结果,可以读取到对应数据与页面信息,并进行调整 + + + +*** + + + +#### 异常处理 + +拦截器最后执行的方法,无论原始方法是否执行: + +```java +public void afterCompletion(HttpServletRequest request, + HttpServletResponse response, + Object handler, + Exception ex) throws Exception { + System.out.println("afterCompletion"); +} +``` + +参数: + +* ex:如果处理器执行过程中出现异常对象,可以针对异常情况进行单独处理 + + + +*** + + + +### 拦截配置 + +拦截路径: + +* `/**`:表示拦截所有映射 +* `/* `:表示拦截所有/开头的映射 +* `/user/*`:表示拦截所有 /user/ 开头的映射 +* `/user/add*`:表示拦截所有 /user/ 开头,且具体映射名称以 add 开头的映射 +* `/user/*All`:表示拦截所有 /user/ 开头,且具体映射名称以 All 结尾的映射 + +```xml + + + + + + + + + + + +``` + + + +*** + + + +### 拦截器链 + +**责任链模式**:责任链模式是一种行为模式 + +特点:沿着一条预先设定的任务链顺序执行,每个节点具有独立的工作任务 +优势: + +* 独立性:只关注当前节点的任务,对其他任务直接放行到下一节点 +* 隔离性:具备链式传递特征,无需知晓整体链路结构,只需等待请求到达后进行处理即可 +* 灵活性:可以任意修改链路结构动态新增或删减整体链路责任 +* 解耦:将动态任务与原始任务解耦 + +缺点: + +* 链路过长时,处理效率低下 +* 可能存在节点上的循环引用现象,造成死循环,导致系统崩溃 + + + + + +*** + + + +### 源码解析 + +DispatcherServlet#doDispatch 方法中: + +```java +protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { + try { + // 获取映射器以及映射器的所有拦截器(运行原理部分详解了源码) + mappedHandler = getHandler(processedRequest); + // 前置处理,返回 false 代表条件成立 + if (!mappedHandler.applyPreHandle(processedRequest, response)) { + //请求从这里直接结束 + return; + } + //所有拦截器都返回 true,执行目标方法 + mv = ha.handle(processedRequest, response, mappedHandler.getHandler()) + // 倒序执行所有拦截器的后置处理方法 + mappedHandler.applyPostHandle(processedRequest, response, mv); + } catch (Exception ex) { + //异常处理机制 + triggerAfterCompletion(processedRequest, response, mappedHandler, ex); + } +} +``` + +HandlerExecutionChain#applyPreHandle:前置处理 + +```java +boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception { + //遍历所有的拦截器 + for (int i = 0; i < this.interceptorList.size(); i++) { + HandlerInterceptor interceptor = this.interceptorList.get(i); + //执行前置处理,如果拦截器返回 false,则条件成立,不在执行其他的拦截器,直接返回 false,请求直接结束 + if (!interceptor.preHandle(request, response, this.handler)) { + triggerAfterCompletion(request, response, null); + return false; + } + this.interceptorIndex = i; + } + return true; +} +``` + +HandlerExecutionChain#applyPostHandle:后置处理 + +```java +void applyPostHandle(HttpServletRequest request, HttpServletResponse response, @Nullable ModelAndView mv) + throws Exception { + //倒序遍历 + for (int i = this.interceptorList.size() - 1; i >= 0; i--) { + HandlerInterceptor interceptor = this.interceptorList.get(i); + interceptor.postHandle(request, response, this.handler, mv); + } +} +``` + +DispatcherServlet#triggerAfterCompletion 底层调用 HandlerExecutionChain#triggerAfterCompletion: + +* 前面的步骤有任何异常都会直接倒序触发 afterCompletion + +* 页面成功渲染有异常,也会倒序触发 afterCompletion + +```java +void triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response, @Nullable Exception ex) { + //倒序遍历 + for (int i = this.interceptorIndex; i >= 0; i--) { + HandlerInterceptor interceptor = this.interceptorList.get(i); + try { + //执行异常处理的方法 + interceptor.afterCompletion(request, response, this.handler, ex); + } + catch (Throwable ex2) { + logger.error("HandlerInterceptor.afterCompletion threw exception", ex2); + } + } +} +``` + + + +拦截器的执行流程: + + + + + +参考文章:https://www.yuque.com/atguigu/springboot/vgzmgh#wtPLU + + + +*** + + + +### 自定义 + +* Contoller层 + + ```java + @Controller + public class InterceptorController { + @RequestMapping("/handleRun") + public String handleRun() { + System.out.println("业务处理器运行------------main"); + return "page.jsp"; + } + } + ``` + +* 自定义拦截器需要实现 HandleInterceptor 接口 + + ```java + //自定义拦截器需要实现HandleInterceptor接口 + public class MyInterceptor implements HandlerInterceptor { + //处理器运行之前执行 + @Override + public boolean preHandle(HttpServletRequest request, + HttpServletResponse response, + Object handler) throws Exception { + System.out.println("前置运行----a1"); + //返回值为false将拦截原始处理器的运行 + //如果配置多拦截器,返回值为false将终止当前拦截器后面配置的拦截器的运行 + return true; + } + + //处理器运行之后执行 + @Override + public void postHandle(HttpServletRequest request, + HttpServletResponse response, + Object handler, + ModelAndView modelAndView) throws Exception { + System.out.println("后置运行----b1"); + } + + //所有拦截器的后置执行全部结束后,执行该操作 + @Override + public void afterCompletion(HttpServletRequest request, + HttpServletResponse response, + Object handler, + Exception ex) throws Exception { + System.out.println("完成运行----c1"); + } + } + ``` + + 说明:三个方法的运行顺序为 preHandle → postHandle → afterCompletion,如果 preHandle 返回值为 false,三个方法仅运行preHandle + +* web.xml: + + ```xml + CharacterEncodingFilter + DispatcherServlet + ``` + +* 配置拦截器:spring-mvc.xml + + ```xml + + + + + + + + + ``` + + 注意:配置顺序为**先配置执行位置,后配置执行类** + + + + + +*** + + + + + +## 异常处理 + +### 处理器 + +异常处理器: **HandlerExceptionResolver** 接口 + +类继承该接口的以后,当开发出现异常后会执行指定的功能 + +```java +@Component +public class ExceptionResolver implements HandlerExceptionResolver { + @Override + public ModelAndView resolveException(HttpServletRequest request, + HttpServletResponse response, + Object handler, + Exception ex) { + System.out.println("异常处理器正在执行中"); + ModelAndView modelAndView = new ModelAndView(); + //定义异常现象出现后,反馈给用户查看的信息 + modelAndView.addObject("msg","出错啦! "); + //定义异常现象出现后,反馈给用户查看的页面 + modelAndView.setViewName("error.jsp"); + return modelAndView; + } +} +``` + +根据异常的种类不同,进行分门别类的管理,返回不同的信息: + +```java +public class ExceptionResolver implements HandlerExceptionResolver { + @Override + public ModelAndView resolveException(HttpServletRequest request, + HttpServletResponse response, + Object handler, + Exception ex) { + System.out.println("my exception is running ...." + ex); + ModelAndView modelAndView = new ModelAndView(); + if( ex instanceof NullPointerException){ + modelAndView.addObject("msg","空指针异常"); + }else if ( ex instanceof ArithmeticException){ + modelAndView.addObject("msg","算数运算异常"); + }else{ + modelAndView.addObject("msg","未知的异常"); + } + modelAndView.setViewName("error.jsp"); + return modelAndView; + } +} +``` + +模拟错误: + +```java +@Controller +public class UserController { + @RequestMapping("/save") + @ResponseBody + public String save(@RequestBody String name) { + //模拟业务层发起调用产生了异常 +// int i = 1/0; +// String str = null; +// str.length(); + + return "error.jsp"; + } +``` + + + +*** + + + +### 注解开发 + +使用注解实现异常分类管理,开发异常处理器 + +@ControllerAdvice 注解: + +* 类型:类注解 + +* 位置:异常处理器类上方 + +* 作用:设置当前类为异常处理器类 + +* 格式: + + ```java + @Component + //声明该类是一个Controller的通知类,声明后该类就会被加载成异常处理器 + @ControllerAdvice + public class ExceptionAdvice { + } + ``` + +@ExceptionHandler 注解: + +* 类型:方法注解 + +* 位置:异常处理器类中针对指定异常进行处理的方法上方 + +* 作用:设置指定异常的处理方式 + +* 说明:处理器方法可以设定多个 + +* 格式: + + ```java + @Component + @ControllerAdvice + public class ExceptionAdvice { + //类中定义的方法携带@ExceptionHandler注解的会被作为异常处理器,后面添加实际处理的异常类型 + @ExceptionHandler(NullPointerException.class) + @ResponseBody + public String doNullException(Exception ex){ + return "空指针异常"; + } + + @ExceptionHandler(Exception.class) + @ResponseBody + public String doException(Exception ex){ + return "all Exception"; + } + } + ``` + + +@ResponseStatus 注解: + +* 类型:类注解、方法注解 + +* 位置:异常处理器类、方法上方 + +* 参数: + + value:出现错误指定返回状态码 + + reason:出现错误返回的错误信息 + + + + + +*** + + + +### 解决方案 + +* web.xml ```java - //本例中的泛型填写的是String,Date,最终出现字符串转日期时,该类型转换器生效 - public class MyDateConverter implements Converter { - //重写接口的抽象方法,参数由泛型决定 - public Date convert(String source) { - DateFormat df = new SimpleDateFormat("yyyy-MM-dd"); - Date date = null; - //类型转换器无法预计使用过程中出现的异常,因此必须在类型转换器内部捕获, - //不允许抛出,框架无法预计此类异常如何处理 - try { - date = df.parse(source); - } catch (ParseException e) { - e.printStackTrace(); + DispatcherServlet + CharacterEncodingFilter + ``` + +* ajax.jsp + + ```jsp + <%@page pageEncoding="UTF-8" language="java" contentType="text/html;UTF-8" %> + + 点击
+ + + + ``` + +* spring-mvc.xml + + ```xml + + + + ``` + +* java / controller / UserController + + ```java + @Controller + public class UserController { + @RequestMapping("/save") + @ResponseBody + public List save(@RequestBody User user) { + System.out.println("user controller save is running ..."); + //对用户的非法操作进行判定,并包装成异常对象进行处理,便于统一管理 + if(user.getName().trim().length() < 8){ + throw new BusinessException("对不起,用户名长度不满足要求,请重新输入!"); } - return date; + if(user.getAge() < 0){ + throw new BusinessException("对不起,年龄必须是0到100之间的数字!"); + } + if(user.getAge() > 100){ + throw new SystemException("服务器连接失败,请尽快检查处理!"); + } + + User u1 = new User("Tom",3); + User u2 = new User("Jerry",5); + ArrayList al = new ArrayList(); + al.add(u1);al.add(u2); + return al; } } ``` - 配置 resources / spring-mvc.xml,注册自定义转换器,将功能加入到 SpringMVC 转换服务 ConverterService 中 +* 自定义异常 - ```xml - - - - - - - - - - - - - - - - + ```java + //自定义异常继承RuntimeException,覆盖父类所有的构造方法 + public class BusinessException extends RuntimeException {覆盖父类所有的构造方法} ``` -* 使用转换器 + ```java + public class SystemException extends RuntimeException {} + ``` + +* 通过自定义异常将所有的异常现象进行分类管理,以统一的格式对外呈现异常消息 ```java - @RequestMapping("/requestParam12") - public String requestParam12(Date date){ - System.out.println(date); - return "page.jsp"; + @Component + @ControllerAdvice + public class ProjectExceptionAdvice { + @ExceptionHandler(BusinessException.class) + public String doBusinessException(Exception ex, Model m){ + //使用参数Model将要保存的数据传递到页面上,功能等同于ModelAndView + //业务异常出现的消息要发送给用户查看 + m.addAttribute("msg",ex.getMessage()); + return "error.jsp"; + } + + @ExceptionHandler(SystemException.class) + public String doSystemException(Exception ex, Model m){ + //系统异常出现的消息不要发送给用户查看,发送统一的信息给用户看 + m.addAttribute("msg","服务器出现问题,请联系管理员!"); + return "error.jsp"; + } + + @ExceptionHandler(Exception.class) + public String doException(Exception ex, Model m){ + m.addAttribute("msg",ex.getMessage()); + //将ex对象保存起来 + return "error.jsp"; + } + } ``` @@ -10153,412 +13224,354 @@ SpringMVC 对接收的数据进行自动类型转换,该工作通过 Converter -### 响应处理 -#### 页面跳转 -请求转发和重定向: +## 文件传输 -* 请求转发: +### 上传下载 - ```java - @Controller - public class UserController { - @RequestMapping("/showPage1") - public String showPage1() { - System.out.println("user mvc controller is running ..."); - return "forward:/WEB-INF/page/page.jsp; - } - } - ``` +上传文件过程: -* 请求重定向: +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/SpringMVC-上传文件过程分析.png) - ```java - @RequestMapping("/showPage2") - public String showPage2() { - System.out.println("user mvc controller is running ..."); - return "redirect:/WEB-INF/page/page.jsp";//不能访问WEB-INF下的资源 - } - ``` -页面访问快捷设定(InternalResourceViewResolver): +MultipartResolver接口: -* 展示页面的保存位置通常固定且结构相似,可以设定通用的访问路径简化页面配置,配置 spring-mvc.xml: +* MultipartResolver 接口定义了文件上传过程中的相关操作,并对通用性操作进行了封装 +* MultipartResolver 接口底层实现类 CommonsMultipartResovler +* CommonsMultipartResovler 并未自主实现文件上传下载对应的功能,而是调用了 apache 文件上传下载组件 + +文件上传下载实现: + +* 导入坐标 ```xml - - - - + + commons-fileupload + commons-fileupload + 1.4 + ``` -* 简化 +* 页面表单 fileupload.jsp - ```java - @RequestMapping("/showPage3") - public String showPage3() { - System.out.println("user mvc controller is running..."); - return "page"; - } - @RequestMapping("/showPage4") - public String showPage4() { - System.out.println("user mvc controller is running..."); - return "forward:page"; - } + ```html +
+
+ +
+ ``` - @RequestMapping("/showPage5") - public String showPage5() { - System.out.println("user mvc controller is running..."); - return "redirect:page"; - } +* web.xml + + ```xml + DispatcherServlet + CharacterEncodingFilter ``` -* 如果未设定了返回值,使用 void 类型,则默认使用访问路径作页面地址的前缀后缀 +* 控制器 ```java - //最简页面配置方式,使用访问路径作为页面名称,省略返回值 - @RequestMapping("/showPage6") - public void showPage6() { - System.out.println("user mvc controller is running ..."); + @PostMapping("/upload") + public String upload(@RequestParam("email") String email, + @RequestParam("username") String username, + @RequestPart("headerImg") MultipartFile headerImg) throws IOException { + + if(!headerImg.isEmpty()){ + //保存到文件服务器,OSS服务器 + String originalFilename = headerImg.getOriginalFilename(); + headerImg.transferTo(new File("H:\\cache\\" + originalFilename)); + } + return "main"; } ``` + + *** -#### 数据跳转 +### 名称问题 -ModelAndView 是 SpringMVC 提供的一个对象,该对象可以用作控制器方法的返回值(Model 同),实现携带数据跳转 +MultipartFile 参数中封装了上传的文件的相关信息。 -作用: +1. 文件命名问题, 获取上传文件名,并解析文件名与扩展名 -+ 设置数据,向请求域对象中存储数据 -+ 设置视图,逻辑视图 + ```java + file.getOriginalFilename(); + ``` -代码实现: +2. 文件名过长问题 -* 使用 HttpServletRequest 类型形参进行数据传递 +3. 文件保存路径 - ```java - @Controller - public class BookController { - @RequestMapping("/showPageAndData1") - public String showPageAndData1(HttpServletRequest request) { - request.setAttribute("name","seazean"); - return "page"; - } - } - ``` + ```java + ServletContext context = request.getServletContext(); + String realPath = context.getRealPath("/uploads"); + File file = new File(realPath + "/"); + if(!file.exists()) file.mkdirs(); + ``` -* 使用 Model 类型形参进行数据传递 +4. 重名问题 - ```java - @RequestMapping("/showPageAndData2") - public String showPageAndData2(Model model) { - model.addAttribute("name","seazean"); - Book book = new Book(); - book.setName("SpringMVC入门实战"); - book.setPrice(66.6d); - //添加数据的方式,key对value - model.addAttribute("book",book); - return "page"; - } - ``` + ```java + String uuid = UUID.randomUUID.toString().replace("-", "").toUpperCase(); + ``` - ```java - public class Book { - private String name; - private Double price; - } - ``` +```java +@Controller +public class FileUploadController { + @RequestMapping(value = "/fileupload") + //参数中定义MultipartFile参数,用于接收页面提交的type=file类型的表单,表单名称与参数名相同 + public String fileupload(MultipartFile file,MultipartFile file1,MultipartFile file2, HttpServletRequest request) throws IOException { + System.out.println("file upload is running ..."+file); +// MultipartFile参数中封装了上传的文件的相关信息 +// System.out.println(file.getSize()); +// System.out.println(file.getBytes().length); +// System.out.println(file.getContentType()); +// System.out.println(file.getName()); +// System.out.println(file.getOriginalFilename()); +// System.out.println(file.isEmpty()); + //首先判断是否是空文件,也就是存储空间占用为0的文件 + if(!file.isEmpty()){ + //如果大小在范围要求内正常处理,否则抛出自定义异常告知用户(未实现) + //获取原始上传的文件名,可以作为当前文件的真实名称保存到数据库中备用 + String fileName = file.getOriginalFilename(); + //设置保存的路径 + String realPath = request.getServletContext().getRealPath("/images"); + //保存文件的方法,通常文件名使用随机生成策略产生,避免文件名冲突问题 + file.transferTo(new File(realPath,file.getOriginalFilename())); + } + //测试一次性上传多个文件 + if(!file1.isEmpty()){ + String fileName = file1.getOriginalFilename(); + //可以根据需要,对不同种类的文件做不同的存储路径的区分,修改对应的保存位置即可 + String realPath = request.getServletContext().getRealPath("/images"); + file1.transferTo(new File(realPath,file1.getOriginalFilename())); + } + if(!file2.isEmpty()){ + String fileName = file2.getOriginalFilename(); + String realPath = request.getServletContext().getRealPath("/images"); + file2.transferTo(new File(realPath,file2.getOriginalFilename())); + } + return "page.jsp"; + } +} +``` -* 使用 ModelAndView 类型形参进行数据传递,将该对象作为返回值传递给调用者 - ```java - @RequestMapping("/showPageAndData3") - public ModelAndView showPageAndData3(ModelAndView modelAndView) { - //ModelAndView mav = new ModelAndView(); 替换形参中的参数 - Book book = new Book(); - book.setName("SpringMVC入门案例"); - book.setPrice(66.66d); - - //添加数据的方式,key对value - modelAndView.addObject("book",book); - modelAndView.addObject("name","Jockme"); - //设置页面的方式,该方法最后一次执行的结果生效 - modelAndView.setViewName("page"); - //返回值设定成ModelAndView对象 - return modelAndView; - } - ``` -* ModelAndView 扩展 +**** + + + +### 源码解析 + +StandardServletMultipartResolver 是文件上传解析器 + +DispatcherServlet#doDispatch: + +```java +protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { + // 判断当前请求是不是文件上传请求 + processedRequest = checkMultipart(request); + // 文件上传请求会对 request 进行包装,导致两者不相等,此处赋值为 true,代表已经被解析 + multipartRequestParsed = (processedRequest != request); +} +``` + +DispatcherServlet#checkMultipart: + +* `if (this.multipartResolver != null && this.multipartResolver.isMultipart(request))`:判断是否是文件请求 + * `StandardServletMultipartResolver#isMultipart`:根据开头是否符合 multipart/form-data 或者 multipart/ +* `return this.multipartResolver.resolveMultipart(request)`:把请求封装成 StandardMultipartHttpServletRequest 对象 + +开始执行 ha.handle() 目标方法进行数据的解析 + +* RequestPartMethodArgumentResolver#supportsParameter:支持解析文件上传数据 ```java - //ModelAndView对象支持转发的手工设定,该设定不会启用前缀后缀的页面拼接格式 - @RequestMapping("/showPageAndData4") - public ModelAndView showPageAndData4(ModelAndView modelAndView) { - modelAndView.setViewName("forward:/WEB-INF/page/page.jsp"); - return modelAndView; - } - - //ModelAndView对象支持重定向的手工设定,该设定不会启用前缀后缀的页面拼接格式 - @RequestMapping("/showPageAndData5") - public ModelAndView showPageAndData6(ModelAndView modelAndView) { - modelAndView.setViewName("redirect:page.jsp"); - return modelAndView; + public boolean supportsParameter(MethodParameter parameter) { + // 参数上有 @RequestPart 注解 + if (parameter.hasParameterAnnotation(RequestPart.class)) { + return true; + } } ``` +* RequestPartMethodArgumentResolver#resolveArgument:解析参数数据,封装成 MultipartFile 对象 + * `RequestPart requestPart = parameter.getParameterAnnotation(RequestPart.class)`:获取注解的相关信息 + * `String name = getPartName(parameter, requestPart)`:获取上传文件的名字 + * `Object mpArg = MultipartResolutionDelegate.resolveMultipartArgument()`:解析参数 + * `List files = multipartRequest.getFiles(name)`:获取文件的所有数据 -*** +* `return doInvoke(args)`:解析完成执行自定义的方法,完成上传功能 -#### JSON -注解:@ResponseBody -作用:将 Controller 的方法返回的对象通过适当的转换器转换为指定的格式之后,写入到 Response 的 body 区。如果返回值是字符串,那么直接将字符串返回客户端;如果是一个对象,会**将对象转化为 Json**,返回客户端 +*** -注意:当方法上面没有写 ResponseBody,底层会将方法的返回值封装为 ModelAndView 对象 -* 使用 HttpServletResponse 对象响应数据 - ```java - @Controller - public class AccountController { - @RequestMapping("/showData1") - public void showData1(HttpServletResponse response) throws IOException { - response.getWriter().write("message"); - } - } - ``` +## 实用技术 -* 使用 **@ResponseBody 将返回的结果作为响应内容**(页面显示),而非响应的页面名称 +### 校验框架 - ```java - @RequestMapping("/showData2") - @ResponseBody - public String showData2(){ - return "{'name':'Jock'}"; - } - ``` +#### 校验概述 -* 使用 jackson 进行 json 数据格式转化 +表单校验保障了数据有效性、安全性 - 导入坐标: +校验分类:客户端校验和服务端校验 - ```xml - - - com.fasterxml.jackson.core - jackson-core - 2.9.0 - - - - com.fasterxml.jackson.core - jackson-databind - 2.9.0 - - - - com.fasterxml.jackson.core - jackson-annotations - 2.9.0 - - ``` +* 格式校验 + * 客户端:使用 js 技术,利用正则表达式校验 + * 服务端:使用校验框架 +* 逻辑校验 + * 客户端:使用ajax发送要校验的数据,在服务端完成逻辑校验,返回校验结果 + * 服务端:接收到完整的请求后,在执行业务操作前,完成逻辑校验 - ```java - @RequestMapping("/showData3") - @ResponseBody - public String showData3() throws JsonProcessingException { - Book book = new Book(); - book.setName("SpringMVC入门案例"); - book.setPrice(66.66d); - - ObjectMapper om = new ObjectMapper(); - return om.writeValueAsString(book); - } - ``` +表单校验框架: -* 使用 SpringMVC 提供的消息类型转换器将对象与集合数据自动转换为 JSON 数据 +* JSR(Java Specification Requests):Java 规范提案 - ```java - //使用SpringMVC注解驱动,对标注@ResponseBody注解的控制器方法进行结果转换,由于返回值为引用类型,自动调用jackson提供的类型转换器进行格式转换 - @RequestMapping("/showData4") - @ResponseBody - public Book showData4() { - Book book = new Book(); - book.setName("SpringMVC入门案例"); - book.setPrice(66.66d); - return book; - } - ``` +* 303:提供bean属性相关校验规则 - * 手工添加信息类型转换器 +* JCP(Java Community Process):Java社区 - ```xml - - - - - - - - - ``` + ```xml + + + javax.validation + validation-api + 2.0.1.Final + + + + org.hibernate + hibernate-validator + 6.1.0.Final + + ``` -* 转换集合类型数据 +**注意:** - ```java - @RequestMapping("/showData5") - @ResponseBody - public List showData5() { - Book book1 = new Book(); - book1.setName("SpringMVC入门案例"); - book1.setPrice(66.66d); - - Book book2 = new Book(); - book2.setName("SpringMVC入门案例"); - book2.setPrice(66.66d); - - ArrayList al = new ArrayList(); - al.add(book1); - al.add(book2); - return al; - } - ``` +* tomcat7:搭配 hibernate-validator 版本 5.*.*.Final +* tomcat8.5:搭配 hibernate-validator 版本 6.*.*.Final + +*** -**** +#### 基本使用 -### Restful +##### 开启校验 -#### 基本介绍 +名称:@Valid、@Validated -Rest(REpresentational State Transfer):表现层状态转化,定义了**资源”在网络传输中以某种“表现形式”进行“状态转移**,即网络资源的访问方式 +类型:形参注解 -* 资源:把真实的对象数据称为资源,一个资源既可以是一个集合,也可以是单个个体;每一种资源都有特定的 URI(统一资源标识符)与之对应,如果获取这个资源,访问这个 URI 就可以,比如获取特定的班级`/class/12`;资源也可以包含子资源,比如 `/classes/classId/teachers`某个指定班级的所有老师 -* 表现形式:"资源"是一种信息实体,它可以有多种外在表现形式,把"资源"具体呈现出来的形式比如 json、xml、image、txt 等等叫做它的"表现层/表现形式" -* 状态转移:描述的服务器端资源的状态,比如增删改查(通过 HTTP 动词实现)引起资源状态的改变,互联网通信协议 HTTP 协议,是一个**无状态协议**,所有的资源状态都保存在服务器端 +位置:处理器类中的实体类类型的方法形参前方 +作用:设定对当前实体类类型参数进行校验 +范例: -*** +```java +@RequestMapping(value = "/addemployee") +public String addEmployee(@Valid Employee employee) { + System.out.println(employee); +} +``` -#### 访问方式 +##### 校验规则 -Restful 是按照 Rest 风格访问网络资源 +名称:@NotNull -* 传统风格访问路径:http://localhost/user/get?id=1 -* Rest 风格访问路径:http://localhost/user/1 +类型:属性注解等 -优点:隐藏资源的访问行为,通过地址无法得知做的是何种操作,书写简化 +位置:实体类属性上方 -Restful 请求路径简化配置方式:`@RestController = @Controller + @ResponseBody` +作用:设定当前属性校验规则 -相关注解:@GetMapping 注解是 @RequestMapping 注解的衍生,所以效果是一样的,建议使用 @GetMapping +范例:每个校验规则所携带的参数不同,根据校验规则进行相应的调整,具体的校验规则查看对应的校验框架进行获取 -* `@GetMapping("/poll")` = `@RequestMapping(value = "/poll",method = RequestMethod.GET)` +```java +public class Employee{ + @NotNull(message = "姓名不能为空") + private String name;//员工姓名 +} +``` - ```java - @RequestMapping(method = RequestMethod.GET) // @GetMapping 就拥有了 @RequestMapping 的功能 - public @interface GetMapping { - @AliasFor(annotation = RequestMapping.class) // 与 RequestMapping 相通 - String name() default ""; - } - ``` -* `@PostMapping("/push")` = `@RequestMapping(value = "/push",method = RequestMethod.POST)` +##### 错误信息 -过滤器:HiddenHttpMethodFilter 是 SpringMVC 对 Restful 风格的访问支持的过滤器 +```java +@RequestMapping(value = "/addemployee") +//Errors对象用于封装校验结果,如果不满足校验规则,对应的校验结果封装到该对象中,包含校验的属性名和校验不通过返回的消息 +public String addEmployee(@Valid Employee employee, Errors errors, Model model){ + System.out.println(employee); + //判定Errors对象中是否存在未通过校验的字段 + if(errors.hasErrors()){ + for(FieldError error : errors.getFieldErrors()){ + //将校验结果添加到Model对象中,用于页面显示,返回json数据即可 + model.addAttribute(error.getField(),error.getDefaultMessage()); + } + //当出现未通过校验的字段时,跳转页面到原始页面,进行数据回显 + return "addemployee.jsp"; + } + return "success.jsp"; +} +``` -代码实现: +通过形参Errors获取校验结果数据,通过Model接口将数据封装后传递到页面显示,页面获取后台封装的校验结果信息 -* restful.jsp: +```html +
+ 员工姓名:${name}
+ 员工年龄:${age}
+ +
+``` - * 页面表单**使用隐藏域提交请求类型**,参数名称固定为 _method,必须配合提交类型 method=post 使用 - * GET 请求通过地址栏可以发送,也可以通过设置 form 的请求方式提交 - * POST 请求必须通过 form 的请求方式提交 - ```html -

restful风格请求表单

- -
- - - -
- ``` +**** -* java / controller / UserController - ```java - @RestController //设置rest风格的控制器 - @RequestMapping("/user/") //设置公共访问路径,配合下方访问路径使用 - public class UserController { - @GetMapping("/user") - //@RequestMapping(value = "/user",method = RequestMethod.GET) - public String getUser(){ - return "GET-张三"; - } - - @PostMapping("/user") - //@RequestMapping(value = "/user",method = RequestMethod.POST) - public String saveUser(){ - return "POST-张三"; - } - - @PutMapping("/user") - //@RequestMapping(value = "/user",method = RequestMethod.PUT) - public String putUser(){ - return "PUT-张三"; - } + +#### 多规则校验 + +* 同一个属性可以添加多个校验器 + + ```java + public class Employee{ + @NotBlank(message = "姓名不能为空") + private String name;//员工姓名 - @DeleteMapping("/user") - //@RequestMapping(value = "/user",method = RequestMethod.DELETE) - public String deleteUser(){ - return "DELETE-张三"; - } + @NotNull(message = "请输入年龄") + @Max(value = 60,message = "年龄最大值60") + @Min(value = 18,message = "年龄最小值18") + private Integer age;//员工年龄 } ``` -* 配置拦截器 web.xml - - ```xml - - - HiddenHttpMethodFilter - org.springframework.web.filter.HiddenHttpMethodFilter - - - HiddenHttpMethodFilter - DispatcherServlet - - ``` +* 三种判定空校验器的区别 + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/SpringMVC-三种判定空检验器的区别.png) @@ -10566,67 +13579,113 @@ Restful 请求路径简化配置方式:`@RestController = @Controller + @Respo -#### 参数注解 +#### 嵌套校验 -Restful 开发中的参数注解 +名称:@Valid + +类型:属性注解 + +位置:实体类中的引用类型属性上方 + +作用:设定当前应用类型属性中的属性开启校验 + +范例: ```java -@GetMapping("{id}") -public String getMessage(@PathVariable("id") Integer id){ +public class Employee { + //实体类中的引用类型通过标注@Valid注解,设定开启当前引用类型字段中的属性参与校验 + @Valid + private Address address; } ``` -使用 @PathVariable 注解获取路径上配置的具名变量,一般在有多个参数的时候添加 +注意:开启嵌套校验后,被校验对象内部需要添加对应的校验规则 -其他注解: +```java +//嵌套校验的实体中,对每个属性正常添加校验规则即可 +public class Address implements Serializable { + @NotBlank(message = "请输入省份名称") + private String provinceName;//省份名称 -* @RequestHeader:获取请求头 -* @RequestParam:获取请求参数(指问号后的参数,url?a=1&b=2) -* @CookieValue:获取 Cookie 值 -* @RequestAttribute:获取 request 域属性 -* @RequestBody:获取请求体 [POST] -* @MatrixVariable:矩阵变量 -* @ModelAttribute:自定义类型变量 + @NotBlank(message = "请输入邮政编码") + @Size(max = 6,min = 6,message = "邮政编码由6位组成") + private String zipCode;//邮政编码 +} +``` + + + +*** + + + +#### 分组校验 + +分组校验的介绍 + +* 同一个模块,根据执行的业务不同,需要校验的属性会有不同 + * 新增用户 + * 修改用户 +* 对不同种类的属性进行分组,在校验时可以指定参与校验的字段所属的组类别 + * 定义组(通用) + * 为属性设置所属组,可以设置多个 + * 开启组校验 + +domain: ```java -@RestController -@RequestMapping("/user/") -public class UserController { - //rest风格访问路径简化书写方式,配合类注解@RequestMapping使用 - @RequestMapping("{id}") - public String restLocation2(@PathVariable Integer id){ - System.out.println("restful is running ....get:" + id); - return "success.jsp"; - } +//用于设定分组校验中的组名,当前接口仅提供字节码,用于识别 +public interface GroupOne { +} +``` - //@RequestMapping(value = "{id}",method = RequestMethod.GET) - @GetMapping("{id}") - public String get(@PathVariable Integer id){ - System.out.println("restful is running ....get:" + id); - return "success.jsp"; - } +```java +public class Employee{ + @NotBlank(message = "姓名不能为空",groups = {GroupA.class}) + private String name;//员工姓名 - @PostMapping("{id}") - public String post(@PathVariable Integer id){ - System.out.println("restful is running ....post:" + id); - return "success.jsp"; - } + @NotNull(message = "请输入年龄",groups = {GroupA.class}) + @Max(value = 60,message = "年龄最大值60")//不加Group的校验不生效 + @Min(value = 18,message = "年龄最小值18") + private Integer age;//员工年龄 - @PutMapping("{id}") - public String put(@PathVariable Integer id){ - System.out.println("restful is running ....put:" + id); - return "success.jsp"; - } + @Valid + private Address address; + //...... +} +``` - @DeleteMapping("{id}") - public String delete(@PathVariable Integer id){ - System.out.println("restful is running ....delete:" + id); +controller: + +```java +@Controller +public class EmployeeController { + @RequestMapping(value = "/addemployee") + public String addEmployee(@Validated({GroupA.class}) Employee employee, Errors errors, Model m){ + if(errors.hasErrors()){ + List fieldErrors = errors.getFieldErrors(); + System.out.println(fieldErrors.size()); + for(FieldError error : fieldErrors){ + m.addAttribute(error.getField(),error.getDefaultMessage()); + } + return "addemployee.jsp"; + } return "success.jsp"; } } ``` +jsp: +```html +
<%--页面使用${}获取后台传递的校验信息--%> + 员工姓名:${name}
+ 员工年龄:${age}
+ <%--注意,引用类型的校验未通过信息不是通过对象进行封装的,直接使用对象名.属性名的格式作为整体属性字符串进行保存的,和使用者的属性传递方式有关,不具有通用性,仅适用于本案例--%> + 省:${requestScope['address.provinceName']}
+ +/form> +``` @@ -10634,193 +13693,76 @@ public class UserController { -#### 识别原理 +### Lombok -表单提交要使用 REST 时,会带上 `_method=PUT`,请求过来被 `HiddenHttpMethodFilter` 拦截,进行过滤操作 +Lombok 用标签方式代替构造器、getter/setter、toString() 等方法 -org.springframework.web.filter.HiddenHttpMethodFilter.doFilterInternal(): +引入依赖: -```java -public class HiddenHttpMethodFilter extends OncePerRequestFilter { - // 兼容的请求 PUT、DELETE、PATCH - private static final List ALLOWED_METHODS = - Collections.unmodifiableList(Arrays.asList(HttpMethod.PUT.name(), - HttpMethod.DELETE.name(), HttpMethod.PATCH.name())); - // 隐藏域的名字 - public static final String DEFAULT_METHOD_PARAM = "_method"; +```xml + + org.projectlombok + lombok + +``` - private String methodParam = DEFAULT_METHOD_PARAM; - @Override - protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, - FilterChain filterChain) throws ServletException, IOException { +下载插件:IDEA 中 File → Settings → Plugins,搜索安装 Lombok 插件 - HttpServletRequest requestToUse = request; - // 请求必须是 POST, - if ("POST".equals(request.getMethod()) && request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) == null) { - // 获取标签中 name="_method" 的 value 值 - String paramValue = request.getParameter(this.methodParam); - if (StringUtils.hasLength(paramValue)) { - // 转成大写 - String method = paramValue.toUpperCase(Locale.ENGLISH); - // 兼容的请求方式 - if (ALLOWED_METHODS.contains(method)) { - // 包装请求 - requestToUse = new HttpMethodRequestWrapper(request, method); - } - } - } - // 过滤器链放行的时候用wrapper。以后的方法调用getMethod是调用requesWrapper的 - filterChain.doFilter(requestToUse, response); - } -} -``` +常用注解: -Rest 使用客户端工具,如 PostMan 可直接发送 put、delete 等方式请求不被过滤 +```java +@NoArgsConstructor // 无参构造 +@AllArgsConstructor // 全参构造 +@Data // set + get +@ToString // toString +@EqualsAndHashCode // hashConde + equals +``` -改变默认的 `_method` 的方式: +简化日志: ```java -@Configuration(proxyBeanMethods = false) -public class WebConfig{ - //自定义filter - @Bean - public HiddenHttpMethodFilter hiddenHttpMethodFilter(){ - HiddenHttpMethodFilter methodFilter = new HiddenHttpMethodFilter(); - //通过set 方法自定义 - methodFilter.setMethodParam("_m"); - return methodFilter; - } +@Slf4j +@RestController +public class HelloController { + @RequestMapping("/hello") + public String handle01(@RequestParam("name") String name){ + log.info("请求进来了...."); + return "Hello, Spring!" + "你好:" + name; + } } ``` -*** -### Servlet -SpringMVC 提供访问原始 Servlet 接口的功能 +**** -* SpringMVC 提供访问原始 Servlet 接口 API 的功能,通过形参声明即可 - ```java - @RequestMapping("/servletApi") - public String servletApi(HttpServletRequest request, - HttpServletResponse response, HttpSession session){ - System.out.println(request); - System.out.println(response); - System.out.println(session); - request.setAttribute("name","seazean"); - System.out.println(request.getAttribute("name")); - return "page.jsp"; - } - ``` -* Head 数据获取快捷操作方式 - 名称:@RequestHeader - 类型:形参注解 - 位置:处理器类中的方法形参前方 - 作用:绑定请求头数据与对应处理方法形参间的关系 - 范例: - ```java - 快捷操作方式@RequestMapping("/headApi") - public String headApi(@RequestHeader("Accept-Language") String headMsg){ - System.out.println(headMsg); - return "page"; - } - ``` -* Cookie 数据获取快捷操作方式 - 名称:@CookieValue - 类型:形参注解 - 位置:处理器类中的方法形参前方 - 作用:绑定请求 Cookie 数据与对应处理方法形参间的关系 - 范例: +# Boot - ```java - @RequestMapping("/cookieApi") - public String cookieApi(@CookieValue("JSESSIONID") String jsessionid){ - System.out.println(jsessionid); - return "page"; - } - ``` +## 基本介绍 -* Session 数据获取 - 名称:@SessionAttribute - 类型:形参注解 - 位置:处理器类中的方法形参前方 - 作用:绑定请求Session数据与对应处理方法形参间的关系 - 范例: +### Boot介绍 - ```java - @RequestMapping("/sessionApi") - public String sessionApi(@SessionAttribute("name") String name){ - System.out.println(name); - return "page.jsp"; - } - //用于在session中放入数据 - @RequestMapping("/setSessionData") - public String setSessionData(HttpSession session){ - session.setAttribute("name","seazean"); - return "page"; - } - ``` +SpringBoot 提供了一种快速使用 Spring 的方式,基于约定优于配置的思想,可以让开发人员不必在配置与逻辑业务之间进行思维的切换,全身心的投入到逻辑业务的代码编写中,从而大大提高了开发的效率 -* Session 数据设置 - 名称:@SessionAttributes - 类型:类注解 - 位置:处理器类上方 - 作用:声明放入session范围的变量名称,适用于Model类型数据传参 - 范例: +SpringBoot 功能: - ```java - @Controller - //设定当前类中名称为age和gender的变量放入session范围,不常用 - @SessionAttributes(names = {"age","gender"}) - public class ServletController { - //将数据放入session存储范围,Model对象实现数据set,@SessionAttributes注解实现范围设定 - @RequestMapping("/setSessionData2") - public String setSessionDate2(Model model) { - model.addAttribute("age",39); - model.addAttribute("gender","男"); - return "page"; - } - - @RequestMapping("/sessionApi") - public String sessionApi(@SessionAttribute("age") int age, - @SessionAttribute("gender") String gender){ - System.out.println(name); - System.out.println(age); - return "page"; - } - } - ``` +* 自动配置,自动配置是一个运行时(更准确地说,是应用程序启动时)的过程,考虑了众多因素选择使用哪个配置,该过程是SpringBoot 自动完成的 -* spring-mvc.xml 配置 +* 起步依赖,起步依赖本质上是一个 Maven 项目对象模型(Project Object Model,POM),定义了对其他库的传递依赖,这些东西加在一起即支持某项功能。简单的说,起步依赖就是将具备某种功能的坐标打包到一起,并提供一些默认的功能 - ```xml - - - - - - - - - - - ``` +* 辅助功能,提供了一些大型项目中常见的非功能性特性,如内嵌 web 服务器、安全、指标,健康检测、外部配置等 - + + +参考视频:https://www.bilibili.com/video/BV19K4y1L7MT @@ -10829,251 +13771,246 @@ SpringMVC 提供访问原始 Servlet 接口的功能 -## 运行原理 +### 构建工程 -### 技术架构 +普通构建: -#### 组件介绍 +1. 创建 Maven 项目 -* DispatcherServlet:核心控制器, 是 SpringMVC 的核心,整体流程控制的中心,所有的请求第一步都先到达这里,由其调用其它组件处理用户的请求,它就是在 web.xml 配置的核心 Servlet,有效的降低了组件间的耦合性 +2. 导入 SpringBoot 起步依赖 -* HandlerMapping:处理器映射器, 负责根据请求找到对应具体的 Handler 处理器,SpringMVC 中针对配置文件方式、注解方式等提供了不同的映射器来处理 + ```xml + + + org.springframework.boot + spring-boot-starter-parent + 2.1.8.RELEASE + + + + + + org.springframework.boot + spring-boot-starter-web + + + ``` -* Handler:处理器,其实就是 Controller,业务处理的核心类,通常由开发者编写,并且必须遵守 Controller 开发的规则,这样适配器才能正确的执行。例如实现 Controller 接口,将 Controller 注册到 IOC 容器中等 +3. 定义 Controller -* HandlAdapter:处理器适配器,根据映射器中找到的 Handler,通过 HandlerAdapter 去执行 Handler,这是适配器模式的应用 + ```java + @RestController + public class HelloController { + @RequestMapping("/hello") + public String hello(){ + return " hello Spring Boot !"; + } + } + ``` -* View Resolver:视图解析器, 将 Handler 中返回的逻辑视图(ModelAndView)解析为一个具体的视图(View)对象 +4. 编写引导类 -* View:视图, View 最后对页面进行渲染将结果返回给用户。SpringMVC 框架提供了很多的 View 视图类型,包括:jstlView、freemarkerView、pdfView 等 + ```java + // 引导类,SpringBoot项目的入口 + @SpringBootApplication + public class HelloApplication { + public static void main(String[] args) { + SpringApplication.run(HelloApplication.class, args); + } + } + ``` - ![](https://gitee.com/seazean/images/raw/master/Frame/SpringMVC-技术架构.png) +快速构建: +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/SpringBoot-IDEA构建工程.png) -**** -#### 工作原理 -在 Spring 容器初始化时会建立所有的 URL 和 Controller 的对应关系,保存到 Map 中,这样 request 就能快速根据 URL 定位到 Controller: -* 在 Spring IOC 容器初始化完所有单例 bean 后 -* SpringMVC 会遍历所有的 bean,获取 controller 中对应的 URL(这里获取 URL 的实现类有多个,用于处理不同形式配置的 Controller) -* 将每一个 URL 对应一个 controller 存入 Map 中 +*** -注意:将 @Controller 注解换成 @Component,启动时不会报错,但是在浏览器中输入路径时会出现 404,说明 Spring 没有对所有的 bean 进行 URL 映射 -**一个 Request 来了:** -* 监听端口,获得请求:Tomcat 监听 8080 端口的请求处理,根据路径调用了 web.xml 中配置的核心控制器 DispatcherServlet,`DispatcherServlet#doDispatch` 是**核心调度方法** -* **首先根据 URI 获取 HandlerMapping 处理器映射器**,RequestMappingHandlerMapping 用来处理 @RequestMapping 注解的映射规则,其中保存了所有 handler 的映射规则,最后包装成一个拦截器链返回,拦截器链对象持有 HandlerMapping。如果没有合适的处理请求的 HandlerMapping,说明请求处理失败,设置响应码 404 返回 -* 根据映射器获取当前 handler,**处理器适配器执行处理方法**,适配器根据请求的 URL 去 handler 中寻找对应的处理方法: - * 创建 ModelAndViewContainer (mav) 对象,用来填充数据,然后通过不同的**参数**解析器去解析 URL 中的参数,完成数据解析绑定,然后执行真正的 Controller 方法,完成 handle 处理 - * 方法执行完对**返回值**进行处理,没添加 @ResponseBody 注解的返回值使用视图处理器处理,把视图名称设置进入 mav 中 - * 对添加了 @ResponseBody 注解的 Controller 的按照普通的返回值进行处理,首先进行内容协商,找到一种浏览器可以接受(请求头 Accept)的并且服务器可以生成的数据类型,选择合适数据转换器,设置响应头中的数据类型,然后写出数据 - * 最后把 ModelAndViewContainer 和 ModelMap 中的数据**封装到 ModelAndView 对象**返回 -* **视图解析**,根据返回值创建视图,请求转发 View 实例为 InternalResourceView,重定向 View 实例为 RedirectView。最后调用 view.render 进行页面渲染,结果派发 - * 请求转发时请求域中的数据不丢失,会把 ModelAndView 的数据设置到请求域中,获取 Servlet 原生的 RequestDispatcher,调用 `RequestDispatcher#forward` 实现转发 - * 重定向会造成请求域中的数据丢失,使用 Servlet 原生方式实现重定向 `HttpServletResponse#sendRedirect` +## 自动装配 -**** +### 依赖管理 +在 spring-boot-starter-parent 中定义了各种技术的版本信息,组合了一套最优搭配的技术版本。在各种 starter 中,定义了完成该功能需要的坐标合集,其中大部分版本信息来自于父工程。工程继承 parent,引入 starter 后,通过依赖传递,就可以简单方便获得需要的 jar 包,并且不会存在版本冲突,自动版本仲裁机制 -### 调度函数 -请求进入原生的 HttpServlet 的 doGet() 方法处理,调用子类 FrameworkServlet 的 doGet() 方法,最终调用 DispatcherServlet 的 doService() 方法,为请求设置相关属性后调用 doDispatch(),请求和响应的以参数的形式传入 +*** -![](https://gitee.com/seazean/images/raw/master/Frame/SpringMVC-请求相应的原理.png) -```java -// request 和 response 为 Java 原生的类 -protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { - HttpServletRequest processedRequest = request; - HandlerExecutionChain mappedHandler = null; - // 文件上传请求 - boolean multipartRequestParsed = false; - // 异步管理器 - WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request); - try { - ModelAndView mv = null; - Exception dispatchException = null; +### 底层注解 - try { - // 文件上传相关请求 - processedRequest = checkMultipart(request); - multipartRequestParsed = (processedRequest != request); +#### SpringBoot - // 找到当前请求使用哪个 HandlerMapping (Controller 的方法)处理,返回执行链 - mappedHandler = getHandler(processedRequest); - // 没有合适的处理请求的方式 HandlerMapping,请求失败,直接返回 404 - if (mappedHandler == null) { - noHandlerFound(processedRequest, response); - return; - } +@SpringBootApplication:启动注解,实现 SpringBoot 的自动部署 - // 根据映射器获取当前 handler 处理器适配器,用来【处理当前的请求】 - HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); - // 获取发出此次请求的方式 - String method = request.getMethod(); - // 判断请求是不是 GET 方法 - boolean isGet = HttpMethod.GET.matches(method); - if (isGet || HttpMethod.HEAD.matches(method)) { - long lastModified = ha.getLastModified(request, mappedHandler.getHandler()); - if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) { - return; - } - } - // 拦截器链的前置处理 - if (!mappedHandler.applyPreHandle(processedRequest, response)) { - return; - } - // 执行处理方法,返回的是 ModelAndView 对象,封装了所有的返回值数据 - mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); +* 参数 scanBasePackages:可以指定扫描范围 +* 默认扫描当前引导类所在包及其子包 - if (asyncManager.isConcurrentHandlingStarted()) { - return; - } - // 设置视图名字 - applyDefaultViewName(processedRequest, mv); - // 执行拦截器链中的后置处理方法 - mappedHandler.applyPostHandle(processedRequest, response, mv); - } catch (Exception ex) { - dispatchException = ex; - } - - // 处理程序调用的结果,进行结果派发 - processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException); +假如所在包为 com.example.springbootenable,扫描配置包 com.example.config 的信息,三种解决办法: + +1. 使用 @ComponentScan 扫描 com.example.config 包 + +2. 使用 @Import 注解加载类,这些类都会被 Spring 创建并放入 ioc 容器,默认组件的名字就是**全类名** + +3. 对 @Import 注解进行封装 + +```java +//1.@ComponentScan("com.example.config") +//2.@Import(UserConfig.class) +@EnableUser +@SpringBootApplication +public class SpringbootEnableApplication { + + public static void main(String[] args) { + ConfigurableApplicationContext context = SpringApplication.run(SpringbootEnableApplication.class, args); + //获取Bean + Object user = context.getBean("user"); + System.out.println(user); + + } +} +``` + +UserConfig: + +```java +@Configuration +public class UserConfig { + @Bean + public User user() { + return new User(); } - //.... } ``` +EnableUser 注解类: - -笔记参考视频:https://www.bilibili.com/video/BV19K4y1L7MT - - - -*** - +```java +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Import(UserConfig.class)//@Import注解实现Bean的动态加载 +public @interface EnableUser { +} +``` -### 请求映射 -#### 映射器 -doDispatch() 中调用 getHandler 方法获取所有的映射器 -总体流程: +*** -* 所有的请求映射都在 HandlerMapping 中,**RequestMappingHandlerMapping 处理 @RequestMapping 注解的映射规则** -* 遍历所有的 HandlerMapping 看是否可以匹配当前请求,匹配成功后返回,匹配失败设置 HTTP 404 响应码 -* 用户可以自定义的映射处理,也可以给容器中放入自定义 HandlerMapping -访问 URL:http://localhost:8080/user +#### Configuration -```java -@GetMapping("/user") -public String getUser(){ - return "GET"; -} -@PostMapping("/user") -public String postUser(){ - return "POST"; -} -//。。。。。 -``` +@Configuration:设置当前类为 SpringBoot 的配置类 -HandlerMapping 处理器映射器,**保存了所有 `@RequestMapping` 和 `handler` 的映射规则** +* proxyBeanMethods = true:Full 全模式,每个 @Bean 方法被调用多少次返回的组件都是单实例的,默认值,类组件之间**有依赖关系**,方法会被调用得到之前单实例组件 +* proxyBeanMethods = false:Lite 轻量级模式,每个 @Bean 方法被调用多少次返回的组件都是新创建的,类组件之间**无依赖关系**用 Lite 模式加速容器启动过程 ```java -protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { - if (this.handlerMappings != null) { - // 遍历所有的 HandlerMapping - for (HandlerMapping mapping : this.handlerMappings) { - // 尝试去每个 HandlerMapping 中匹配当前请求的处理 - HandlerExecutionChain handler = mapping.getHandler(request); - if (handler != null) { - return handler; - } - } +@Configuration(proxyBeanMethods = true) +public class MyConfig { + @Bean //给容器中添加组件。以方法名作为组件的 id。返回类型就是组件类型。返回的值,就是组件在容器中的实例 + public User user(){ + User user = new User("zhangsan", 18); + return user; } - return null; } ``` -![](https://gitee.com/seazean/images/raw/master/Frame/SpringMVC-获取Controller处理器.png) - -* `mapping.getHandler(request)`:调用 AbstractHandlerMapping#getHandler - - * `Object handler = getHandlerInternal(request)`:获取映射器,底层调用 RequestMappingInfoHandlerMapping 类的方法,又调用 AbstractHandlerMethodMapping#getHandlerInternal - * `String lookupPath = initLookupPath(request)`:地址栏的 uri,这里的 lookupPath 为 /user - * `this.mappingRegistry.acquireReadLock()`:加读锁防止并发 - * `handlerMethod = lookupHandlerMethod(lookupPath, request)`:获取当前 HandlerMapping 中的映射规则 - AbstractHandlerMethodMapping.lookupHandlerMethod(): +*** - * `directPathMatches = this.mappingRegistry.getMappingsByDirectPath(lookupPath)`:获取当前的映射器与当前**请求的 URI 有关的所有映射规则** - ![](https://gitee.com/seazean/images/raw/master/Frame/SpringMVC-HandlerMapping的映射规则.png) - * `addMatchingMappings(directPathMatches, matches, request)`:**匹配某个映射规则** +#### Condition - * `for (T mapping : mappings)`:遍历所有的映射规则 - * `match = getMatchingMapping(mapping, request)`:去匹配每一个映射规则,匹配失败返回 null - * `matches.add(new Match())`:匹配成功后封装成匹配器添加到匹配集合中 +##### 条件注解 - * `matches.sort(comparator)`:匹配集合排序 +Condition 是 Spring4.0 后引入的条件化配置接口,通过实现 Condition 接口可以完成有条件的加载相应的 Bean - * `Match bestMatch = matches.get(0)`:匹配完成只剩一个,直接获取返回对应的处理方法 +注解:@Conditional - * `if (matches.size() > 1)`:当有多个映射规则符合请求时,报错 - - * `return bestMatch.getHandlerMethod()`:返回匹配器中的处理方法 +作用:条件装配,满足 Conditional 指定的条件则进行组件注入,加上方法或者类上,作用范围不同 - * `executionChain = getHandlerExecutionChain(handler, request)`:**为当前请求和映射器的构建一个拦截器链** - - * `for (HandlerInterceptor interceptor : this.adaptedInterceptors)`:遍历所有的拦截器 - * `chain.addInterceptor(interceptor)`:把所有的拦截器添加到 HandlerExecutionChain 中,形成拦截器链 - - * `return executionChain`:**返回拦截器链,HandlerMapping 是链的成员属性** +使用:@Conditional 配合 Condition 的实现类(ClassCondition)进行使用 +ConditionContext 类API: +| 方法 | 说明 | +| ------------------------------------------------- | ----------------------------- | +| ConfigurableListableBeanFactory getBeanFactory() | 获取到 IOC 使用的 beanfactory | +| ClassLoader getClassLoader() | 获取类加载器 | +| Environment getEnvironment() | 获取当前环境信息 | +| BeanDefinitionRegistry getRegistry() | 获取到 bean 定义的注册类 | -**** +* ClassCondition: + ```java + public class ClassCondition implements Condition { + /** + * context 上下文对象。用于获取环境,IOC容器,ClassLoader对象 + * metadata 注解元对象。 可以用于获取注解定义的属性值 + */ + @Override + public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { + + //1.需求: 导入Jedis坐标后创建Bean + //思路:判断redis.clients.jedis.Jedis.class文件是否存在 + boolean flag = true; + try { + Class cls = Class.forName("redis.clients.jedis.Jedis"); + } catch (ClassNotFoundException e) { + flag = false; + } + return flag; + } + } + ``` +* UserConfig: -#### 适配器 + ```java + @Configuration + public class UserConfig { + @Bean + @Conditional(ClassCondition.class) + public User user(){ + return new User(); + } + } + ``` -doDispatch() 中调用 `HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler())` +* 启动类: -```java -protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException { - if (this.handlerAdapters != null) { - // 遍历所有的 HandlerAdapter - for (HandlerAdapter adapter : this.handlerAdapters) { - // 判断当前适配器是否支持当前 handle - // 这里返回的是True, - if (adapter.supports(handler)) { - // 返回的是 【RequestMappingHandlerAdapter】 - return adapter; - } - } - } - throw new ServletException(); -} -``` + ```java + @SpringBootApplication + public class SpringbootConditionApplication { + public static void main(String[] args) { + //启动SpringBoot应用,返回Spring的IOC容器 + ConfigurableApplicationContext context = SpringApplication.run(SpringbootConditionApplication.class, args); + + Object user = context.getBean("user"); + System.out.println(user); + } + } + ``` @@ -11081,458 +14018,467 @@ protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletExcepti -#### 方法执行 +##### 自定义注解 -##### 执行流程 +将类的判断定义为动态的,判断哪个字节码文件存在可以动态指定 -实例代码: +* 自定义条件注解类 -```java -@GetMapping("/params") -public String param(Map map, Model model, HttpServletRequest request) { - map.put("k1", "v1"); // 都可以向请求域中添加数据 - model.addAttribute("k2", "v2"); // 它们两个都在数据封装在 【BindingAwareModelMap】,继承自 LinkedHashMap - request.setAttribute("m", "HelloWorld"); - return "forward:/success"; -} -``` + ```java + @Target({ElementType.TYPE, ElementType.METHOD}) + @Retention(RetentionPolicy.RUNTIME) + @Documented + @Conditional(ClassCondition.class) + public @interface ConditionOnClass { + String[] value(); + } + ``` -![](https://gitee.com/seazean/images/raw/master/Frame/SpringMVC-Model和Map的数据解析.png) +* ClassCondition -doDispatch() 中调用 `mv = ha.handle(processedRequest, response, mappedHandler.getHandler())` **使用适配器执行方法** + ```java + public class ClassCondition implements Condition { + @Override + public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata metadata) { + + //需求:通过注解属性值value指定坐标后创建bean + Map map = metadata.getAnnotationAttributes + (ConditionOnClass.class.getName()); + //map = {value={属性值}} + //获取所有的 + String[] value = (String[]) map.get("value"); + + boolean flag = true; + try { + for (String className : value) { + Class cls = Class.forName(className); + } + } catch (Exception e) { + flag = false; + } + return flag; + } + } + ``` -`AbstractHandlerMethodAdapter#handle` → `RequestMappingHandlerAdapter#handleInternal` → `invokeHandlerMethod`: +* UserConfig -```java -protected ModelAndView invokeHandlerMethod(HttpServletRequest request, - HttpServletResponse response, - HandlerMethod handlerMethod) throws Exception { - // 封装成 SpringMVC 的接口,用于通用 Web 请求拦截器,使能够访问通用请求元数据,而不是用于实际处理请求 - ServletWebRequest webRequest = new ServletWebRequest(request, response); - try { - // WebDataBinder 用于【从 Web 请求参数到 JavaBean 对象的数据绑定】,获取创建该实例的工厂 - WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod); - // 创建 Model 实例,用于向模型添加属性 - ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory); - // 方法执行器 - ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod); - - // 参数解析器,有很多 - if (this.argumentResolvers != null) { - invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers); - } - // 返回值处理器,也有很多 - if (this.returnValueHandlers != null) { - invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers); - } - // 设置数据绑定器 - invocableMethod.setDataBinderFactory(binderFactory); - // 设置参数检查器 - invocableMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer); - - // 新建一个 ModelAndViewContainer 并进行初始化和一些属性的填充 - ModelAndViewContainer mavContainer = new ModelAndViewContainer(); - - // 设置一些属性 - - // 【执行目标方法】 - invocableMethod.invokeAndHandle(webRequest, mavContainer); - // 异步请求 - if (asyncManager.isConcurrentHandlingStarted()) { - return null; - } - // 【获取 ModelAndView 对象,封装了 ModelAndViewContainer】 - return getModelAndView(mavContainer, modelFactory, webRequest); - } - finally { - webRequest.requestCompleted(); - } -} -``` + ```java + @Configuration + public class UserConfig { + @Bean + @ConditionOnClass("com.alibaba.fastjson.JSON")//JSON加载了才注册 User 到容器 + public User user(){ + return new User(); + } + } + ``` -ServletInvocableHandlerMethod#invokeAndHandle:执行目标方法 +* 测试 User 对象的创建 -* `returnValue = invokeForRequest(webRequest, mavContainer, providedArgs)`:**执行自己写的 controller 方法,返回的就是自定义方法中 return 的值** - `Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs)`:**参数处理的逻辑**,遍历所有的参数解析器解析参数或者将 URI 中的参数进行绑定,绑定完成后开始执行目标方法 - * `parameters = getMethodParameters()`:获取此处理程序方法的方法参数的详细信息 +*** - * `Object[] args = new Object[parameters.length]`:存放所有的参数 - * `for (int i = 0; i < parameters.length; i++)`:遍历所有的参数 - * `args[i] = findProvidedArgument(parameter, providedArgs)`:获取调用方法时提供的参数,一般是空 +##### 常用注解 - * `if (!this.resolvers.supportsParameter(parameter))`:**获取可以解析当前参数的参数解析器** +SpringBoot 提供的常用条件注解: - `return getArgumentResolver(parameter) != null`:获取参数的解析是否为空 +@ConditionalOnProperty:判断**配置文件**中是否有对应属性和值才初始化 Bean - * `for (HandlerMethodArgumentResolver resolver : this.argumentResolvers)`:遍历容器内所有的解析器 +```java +@Configuration +public class UserConfig { + @Bean + @ConditionalOnProperty(name = "it", havingValue = "seazean") + public User user() { + return new User(); + } +} +``` - `if (resolver.supportsParameter(parameter))`:是否支持当前参数 +```properties +it=seazean +``` - * `PathVariableMethodArgumentResolver#supportsParameter`:**解析标注 @PathVariable 注解的参数** - * `ModelMethodProcessor#supportsParameter`:解析 Map 和 Model 类型的参数,Model 和 Map 的作用一样 - * `ExpressionValueMethodArgumentResolver#supportsParameter`:解析标注 @Value 注解的参数 - * `RequestParamMapMethodArgumentResolver#supportsParameter`:**解析标注 @RequestParam 注解** - * `RequestPartMethodArgumentResolver#supportsParameter`:解析文件上传的信息 - * `ModelAttributeMethodProcessor#supportsParameter`:解析标注 @ModelAttribute 注解或者不是简单类型 - * 子类 ServletModelAttributeMethodProcessor 是解析自定义类型 JavaBean 的解析器 - * 简单类型有 Void、Enum、Number、CharSequence、Date、URI、URL、Locale、Class +@ConditionalOnClass:判断环境中是否有对应类文件才初始化 Bean - * `args[i] = this.resolvers.resolveArgument()`:**开始解析参数,每个参数使用的解析器不同** +@ConditionalOnMissingClass:判断环境中是否有对应类文件才初始化 Bean - `resolver = getArgumentResolver(parameter)`:获取参数解析器 +@ConditionalOnMissingBean:判断环境中没有对应Bean才初始化 Bean - `return resolver.resolveArgument()`:开始解析 - * `PathVariableMapMethodArgumentResolver#resolveArgument`:@PathVariable,包装 URI 中的参数为 Map - * `MapMethodProcessor#resolveArgument`:调用 `mavContainer.getModel()` 返回默认 BindingAwareModelMap 对象 - * `ModelAttributeMethodProcessor#resolveArgument`:**自定义的 JavaBean 的绑定封装**,下一小节详解 - `return doInvoke(args)`:**真正的执行 Controller 方法** +***** - * `Method method = getBridgedMethod()`:从 HandlerMethod 获取要反射执行的方法 - * `ReflectionUtils.makeAccessible(method)`:破解权限 - * `method.invoke(getBean(), args)`:执行方法,getBean 获取的是标记 @Controller 的 Bean 类,其中包含执行方法 -* **进行返回值的处理,响应部分详解**,处理完成进入下面的逻辑 +#### ImportRes +使用 bean.xml 文件生成配置 bean,如果需要继续复用 bean.xml,@ImportResource 导入配置文件即可 -RequestMappingHandlerAdapter#getModelAndView:获取 ModelAndView 对象 +```java +@ImportResource("classpath:beans.xml") +public class MyConfig { + //... +} +``` -* `modelFactory.updateModel(webRequest, mavContainer)`:Model 数据升级到会话域(**请求域中的数据在重定向时丢失**) - * `updateBindingResult(request, defaultModel)`:把绑定的数据添加到 BindingAwareModelMap 中 - -* `if (mavContainer.isRequestHandled())`:判断请求是否已经处理完成了 +```xml + + + + + -* `ModelMap model = mavContainer.getModel()`:获取**包含 Controller 方法参数**的 BindingAwareModelMap(本节开头) + + + + +``` -* `mav = new ModelAndView()`:**把 ModelAndViewContainer 和 ModelMap 中的数据封装到 ModelAndView** -* `if (!mavContainer.isViewReference())`:是否是通过名称指定视图引用 -* `if (model instanceof RedirectAttributes)`:判断 model 是否是重定向数据,如果是进行重定向逻辑 +**** -* `return mav`:**任何方法执行都会返回 ModelAndView 对象** +#### Properties -*** +@ConfigurationProperties:读取到 properties 文件中的内容,并且封装到 JavaBean 中 +配置文件: +```properties +mycar.brand=BYD +mycar.price=100000 +``` -##### 参数解析 +JavaBean 类: -解析自定义的 JavaBean 为例,调用 ModelAttributeMethodProcessor#resolveArgument 处理参数的方法,通过合适的类型转换器把 URL 中的参数转换以后,利用反射获取 set 方法,注入到 JavaBean +```java +@Component //导入到容器内 +@ConfigurationProperties(prefix = "mycar")//代表配置文件的前缀 +public class Car { + private String brand; + private Integer price; +} +``` -* Person.java: - ```java - @Data - @Component //加入到容器中 - public class Person { - private String userName; - private Integer age; - private Date birth; - } - ``` -* Controller: +*** - ```java - @RestController //返回的数据不是页面 - public class ParameterController { - // 数据绑定:页面提交的请求数据(GET、POST)都可以和对象属性进行绑定 - @GetMapping("/saveuser") - public Person saveuser(Person person){ - return person; - } - } - ``` -* 访问 URL:http://localhost:8080/saveuser?userName=zhangsan&age=20 -进入源码:ModelAttributeMethodProcessor#resolveArgument +### 装配原理 -* `name = ModelFactory.getNameForParameter(parameter)`:获取名字,此例就是 person +#### 启动流程 -* `ann = parameter.getParameterAnnotation(ModelAttribute.class)`:是否有 ModelAttribute 注解 +应用启动: -* `if (mavContainer.containsAttribute(name))`:ModelAndViewContainer 中是否包含 person 对象 +```java +@SpringBootApplication +public class BootApplication { + public static void main(String[] args) { + // 启动代码 + SpringApplication.run(BootApplication.class, args); + } +} +``` -* `attribute = createAttribute()`:**创建一个实例,空的 Person 对象** +SpringApplication 构造方法: -* `binder = binderFactory.createBinder(webRequest, attribute, name)`:Web 数据绑定器,可以利用 Converters 将请求数据转成指定的数据类型,绑定到 JavaBean 中 +* `this.resourceLoader = resourceLoader`:资源加载器,初始为 null +* `this.webApplicationType = WebApplicationType.deduceFromClasspath()`:判断当前应用的类型,是响应式还是 Web 类 +* `this.bootstrapRegistryInitializers = getBootstrapRegistryInitializersFromSpringFactories()`:**获取引导器** + * 去 **`META-INF/spring.factories`** 文件中找 org.springframework.boot.Bootstrapper + * 寻找的顺序:classpath → spring-beans → boot-devtools → springboot → boot-autoconfigure +* `setInitializers(getSpringFactoriesInstances(ApplicationContextInitializer.class))`:**获取初始化器** + * 去 `META-INF/spring.factories` 文件中找 org.springframework.context.ApplicationContextInitializer +* `setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class))`:**获取监听器** + * 去 `META-INF/spring.factories` 文件中找 org.springframework.context.ApplicationListener -* `bindRequestParameters(binder, webRequest)`:**利用反射向目标对象填充数据** +* `this.mainApplicationClass = deduceMainApplicationClass()`:获取出 main 程序类 - `servletBinder = (ServletRequestDataBinder) binder`:类型强转 +SpringApplication#run(String... args):创建 IOC 容器并实现了自动装配 - `servletBinder.bind(servletRequest)`:绑定数据 +* `StopWatch stopWatch = new StopWatch()`:停止监听器,**监控整个应用的启停** +* `stopWatch.start()`:记录应用的启动时间 - * `mpvs = new MutablePropertyValues(request.getParameterMap())`:获取请求 URI 参数中的 k-v 键值对 +* `bootstrapContext = createBootstrapContext()`:**创建引导上下文环境** + + * `bootstrapContext = new DefaultBootstrapContext()`:创建默认的引导类环境 + * `this.bootstrapRegistryInitializers.forEach()`:遍历所有的引导器调用 initialize 方法完成初始化设置 +* `configureHeadlessProperty()`:让当前应用进入 headless 模式 - * `addBindValues(mpvs, request)`:子类可以用来为请求添加额外绑定值 +* `listeners = getRunListeners(args)`:**获取所有 RunListener(运行监听器)** + + * 去 `META-INF/spring.factories` 文件中找 org.springframework.boot.SpringApplicationRunListener +* `listeners.starting(bootstrapContext, this.mainApplicationClass)`:遍历所有的运行监听器调用 starting 方法 - * `doBind(mpvs)`:真正的绑定的方法,调用 `applyPropertyValues` 应用参数值,然后调用 `setPropertyValues` 方法 +* `applicationArguments = new DefaultApplicationArguments(args)`:获取所有的命令行参数 - `AbstractPropertyAccessor#setPropertyValues()`: +* `environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments)`:**准备环境** - * `List propertyValues`:获取到所有的参数的值,就是 URI 上的所有的参数值 + * `environment = getOrCreateEnvironment()`:返回或创建基础环境信息对象 + * `switch (this.webApplicationType)`:**根据当前应用的类型创建环境** + * `case SERVLET`:Web 应用环境对应 ApplicationServletEnvironment + * `case REACTIVE`:响应式编程对应 ApplicationReactiveWebEnvironment + * `default`:默认为 Spring 环境 ApplicationEnvironment + * `configureEnvironment(environment, applicationArguments.getSourceArgs())`:读取所有配置源的属性值配置环境 + * `ConfigurationPropertySources.attach(environment)`:属性值绑定环境信息 + * `sources.addFirst(ATTACHED_PROPERTY_SOURCE_NAME,..)`:把 configurationProperties 放入环境的属性信息头部 - * `for (PropertyValue pv : propertyValues)`:遍历所有的参数值 + * `listeners.environmentPrepared(bootstrapContext, environment)`:运行监听器调用 environmentPrepared(),EventPublishingRunListener 发布事件通知所有的监听器当前环境准备完成 - * `setPropertyValue(pv)`:**填充到空的 Person 实例中** + * `DefaultPropertiesPropertySource.moveToEnd(environment)`:移动 defaultProperties 属性源到环境中的最后一个源 - * `nestedPa = getPropertyAccessorForPropertyPath(propertyName)`:获取属性访问器 + * `bindToSpringApplication(environment)`:与容器绑定当前环境 - * `tokens = getPropertyNameTokens()`:获取元数据的信息 + * `ConfigurationPropertySources.attach(environment)`:重新将属性值绑定环境信息 + * `sources.remove(ATTACHED_PROPERTY_SOURCE_NAME)`:从环境信息中移除 configurationProperties + + * `sources.addFirst(ATTACHED_PROPERTY_SOURCE_NAME,..)`:把 configurationProperties 重新放入环境信息 + +* `configureIgnoreBeanInfo(environment)`:**配置忽略的 bean** - * `nestedPa.setPropertyValue(tokens, pv)`:填充数据 +* `printedBanner = printBanner(environment)`:打印 SpringBoot 标志 - * `processLocalProperty(tokens, pv)`:处理属性 +* `context = createApplicationContext()`:**创建 IOC 容器** - * `if (!Boolean.FALSE.equals(pv.conversionNecessary))`:数据是否需要转换了 + `switch (this.webApplicationType)`:根据当前应用的类型创建 IOC 容器 - * `if (pv.isConverted())`:数据已经转换过了,转换了直接赋值,没转换进行转换 + * `case SERVLET`:Web 应用环境对应 AnnotationConfigServletWebServerApplicationContext + * `case REACTIVE`:响应式编程对应 AnnotationConfigReactiveWebServerApplicationContext + * `default`:默认为 Spring 环境 AnnotationConfigApplicationContext - * `oldValue = ph.getValue()`:获取未转换的数据 +* `context.setApplicationStartup(this.applicationStartup)`:设置一个启动器 - * `valueToApply = convertForProperty()`:进行数据转换 +* `prepareContext()`:配置 IOC 容器的基本信息 - `TypeConverterDelegate#convertIfNecessary`:进入该方法的逻辑 + * `postProcessApplicationContext(context)`:后置处理流程 - * `if (conversionService.canConvert(sourceTypeDesc, typeDescriptor))`:判断能不能转换 + * `applyInitializers(context)`:获取所有的**初始化器调用 initialize() 方法**进行初始化 + * `listeners.contextPrepared(context)`:所有的运行监听器调用 environmentPrepared() 方法,EventPublishingRunListener 发布事件通知 IOC 容器准备完成 + * `listeners.contextLoaded(context)`:所有的运行监听器调用 contextLoaded() 方法,通知 IOC 加载完成 - `GenericConverter converter = getConverter(sourceType, targetType)`:**获取类型转换器** +* `refreshContext(context)`:**刷新 IOC 容器** - * `converter = this.converters.find(sourceType, targetType)`:寻找合适的转换器 + * Spring 的容器启动流程 + * `invokeBeanFactoryPostProcessors(beanFactory)`:**实现了自动装配** + * `onRefresh()`:**创建 WebServer** 使用该接口 - * `sourceCandidates = getClassHierarchy(sourceType.getType())`:原数据类型 +* `afterRefresh(context, applicationArguments)`:留给用户自定义容器刷新完成后的处理逻辑 - * `targetCandidates = getClassHierarchy(targetType.getType())`:目标数据类型 +* `stopWatch.stop()`:记录应用启动完成的时间 - ```java - for (Class sourceCandidate : sourceCandidates) { - //双重循环遍历,寻找合适的转换器 - for (Class targetCandidate : targetCandidates) { - ``` +* `callRunners(context, applicationArguments)`:调用所有 runners - * `GenericConverter converter = getRegisteredConverter(..)`:匹配类型转换器 +* `listeners.started(context)`:所有的运行监听器调用 started() 方法 - * `return converter`:返回转换器 +* `listeners.running(context)`:所有的运行监听器调用 running() 方法 - * `conversionService.convert(newValue, sourceTypeDesc, typeDescriptor)`:开始转换 + * 获取容器中的 ApplicationRunner、CommandLineRunner + * `AnnotationAwareOrderComparator.sort(runners)`:合并所有 runner 并且按照 @Order 进行排序 - * `converter = getConverter(sourceType, targetType)`:**获取可用的转换器** - * `result = ConversionUtils.invokeConverter()`:执行转换方法 - * `converter.convert()`:**调用转换器的转换方法**(GenericConverter#convert) - * `return handleResult(sourceType, targetType, result)`:返回结果 + * `callRunner()`:遍历所有的 runner,调用 run 方法 - * `ph.setValue(valueToApply)`:**设置 JavaBean 属性**(BeanWrapperImpl.BeanPropertyHandler) +* `handleRunFailure(context, ex, listeners)`:**处理异常**,出现异常进入该逻辑 - * `Method writeMethod`:获取 set 方法 - * `Class cls = getClass0()`:获取 Class 对象 - * `writeMethodName = Introspector.SET_PREFIX + getBaseName()`:**set 前缀 + 属性名** - * `writeMethod = Introspector.findMethod(cls, writeMethodName, 1, args)`:获取只包含一个参数的 set 方法 - * `setWriteMethod(writeMethod)`:加入缓存 - * `ReflectionUtils.makeAccessible(writeMethod)`:设置访问权限 - * `writeMethod.invoke(getWrappedInstance(), value)`:执行方法 + * `handleExitCode(context, exception)`:处理错误代码 + * `listeners.failed(context, exception)`:运行监听器调用 failed() 方法 + * `reportFailure(getExceptionReporters(context), exception)`:通知异常 + + + +**** -* `bindingResult = binder.getBindingResult()`:获取绑定的结果 -* `mavContainer.addAllAttributes(bindingResultModel)`:**把所有填充的参数放入 ModelAndViewContainer** -* `return attribute`:返回填充后的 Person 对象 +#### 注解分析 +SpringBoot 定义了一套接口规范,这套规范规定 SpringBoot 在启动时会扫描外部引用 jar 包中的 `META-INF/spring.factories` 文件,将文件中配置的类型信息加载到 Spring 容器,并执行类中定义的各种操作,对于外部的 jar 包,直接引入一个 starter 即可 +@SpringBootApplication 注解是 `@SpringBootConfiguration`、`@EnableAutoConfiguration`、`@ComponentScan` 注解的集合 +* @SpringBootApplication 注解 + ```java + @Inherited + @SpringBootConfiguration //代表 @SpringBootApplication 拥有了该注解的功能 + @EnableAutoConfiguration //同理 + @ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class), + @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) }) + // 扫描被 @Component (@Service,@Controller)注解的 bean,容器中将排除TypeExcludeFilter 和 AutoConfigurationExcludeFilter + public @interface SpringBootApplication { } + ``` -**** +* @SpringBootConfiguration 注解: + ```java + @Configuration // 代表是配置类 + @Indexed + public @interface SpringBootConfiguration { + @AliasFor(annotation = Configuration.class) + boolean proxyBeanMethods() default true; + } + ``` + @AliasFor 注解:表示别名,可以注解到自定义注解的两个属性上表示这两个互为别名,两个属性其实是同一个含义相互替代 -### 响应处理 +* @ComponentScan 注解:默认扫描当前类所在包及其子级包下的所有文件 -#### 响应数据 +* **@EnableAutoConfiguration 注解:启用 SpringBoot 的自动配置机制** -以 Person 为例: + ````java + @AutoConfigurationPackage + @Import(AutoConfigurationImportSelector.class) + public @interface EnableAutoConfiguration { + String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration"; + Class[] exclude() default {}; + String[] excludeName() default {}; + } + ```` -```java -@ResponseBody // 利用返回值处理器里面的消息转换器进行处理,而不是视图 -@GetMapping(value = "/person") -public Person getPerson(){ - Person person = new Person(); - person.setAge(28); - person.setBirth(new Date()); - person.setUserName("zhangsan"); - return person; -} -``` + * @AutoConfigurationPackage:**将添加该注解的类所在的 package 作为自动配置 package 进行管理**,把启动类所在的包设置一次,为了给各种自动配置的第三方库扫描用,比如带 @Mapper 注解的类,Spring 自身是不能识别的,但自动配置的 Mybatis 需要扫描用到,而 ComponentScan 只是用来扫描注解类,并没有提供接口给三方使用 -直接进入方法执行完后的逻辑 ServletInvocableHandlerMethod#invokeAndHandle: + ```java + @Import(AutoConfigurationPackages.Registrar.class) // 利用 Registrar 给容器中导入组件 + public @interface AutoConfigurationPackage { + String[] basePackages() default {}; //自动配置包,指定了配置类的包 + Class[] basePackageClasses() default {}; + } + ``` -```java -public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer, - Object... providedArgs) throws Exception { - // 【执行目标方法】,return person 对象 - Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs); - // 设置状态码 - setResponseStatus(webRequest); + `register(registry, new PackageImports(metadata).getPackageNames().toArray(new String[0]))`:注册 BD - // 判断方法是否有返回值 - if (returnValue == null) { - if (isRequestNotModified(webRequest) || getResponseStatus() != null || mavContainer.isRequestHandled()) { - disableContentCachingIfNecessary(webRequest); - mavContainer.setRequestHandled(true); - return; + * `new PackageImports(metadata).getPackageNames()`:获取添加当前注解的类的所在包 + * `registry.registerBeanDefinition(BEAN, new BasePackagesBeanDefinition(packageNames))`:存放到容器中 + * `new BasePackagesBeanDefinition(packageNames)`:把当前主类所在的包名封装到该对象中 + + * @Import(AutoConfigurationImportSelector.class):**自动装配的核心类** + + 容器刷新时执行:**invokeBeanFactoryPostProcessors()** → invokeBeanDefinitionRegistryPostProcessors() → postProcessBeanDefinitionRegistry() → processConfigBeanDefinitions() → parse() → process() → processGroupImports() → getImports() → process() → **AutoConfigurationImportSelector#getAutoConfigurationEntry()** + + ```java + protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) { + if (!isEnabled(annotationMetadata)) { + return EMPTY_ENTRY; } - } // 返回值是字符串 - else if (StringUtils.hasText(getResponseStatusReason())) { - // 设置请求处理完成 - mavContainer.setRequestHandled(true); - return; - // 设置请求没有处理完成,还需要进行返回值的逻辑 - mavContainer.setRequestHandled(false); - Assert.state(this.returnValueHandlers != null, "No return value handlers"); - try { - // 【返回值的处理】 - this.returnValueHandlers.handleReturnValue( - returnValue, getReturnValueType(returnValue), mavContainer, webRequest); + // 获取注解属性,@SpringBootApplication 注解的 exclude 属性和 excludeName 属性 + AnnotationAttributes attributes = getAttributes(annotationMetadata); + // 获取所有需要自动装配的候选项 + List configurations = getCandidateConfigurations(annotationMetadata, attributes); + // 去除重复的选项 + configurations = removeDuplicates(configurations); + // 获取注解配置的排除的自动装配类 + Set exclusions = getExclusions(annotationMetadata, attributes); + checkExcludedClasses(configurations, exclusions); + // 移除所有的配置的不需要自动装配的类 + configurations.removeAll(exclusions); + // 过滤,条件装配 + configurations = getConfigurationClassFilter().filter(configurations); + // 获取 AutoConfigurationImportListener 类的监听器调用 onAutoConfigurationImportEvent 方法 + fireAutoConfigurationImportEvents(configurations, exclusions); + // 包装成 AutoConfigurationEntry 返回 + return new AutoConfigurationEntry(configurations, exclusions); } - catch (Exception ex) {} -} -``` + ``` -* **没有加 @ResponseBody 注解的返回数据按照视图(页面)处理的逻辑**,ViewNameMethodReturnValueHandler(视图详解) -* 此例是加了注解的,返回的数据不是视图,HandlerMethodReturnValueHandlerComposite#handleReturnValue: + AutoConfigurationImportSelector#getCandidateConfigurations:**获取自动配置的候选项** -```java -public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType, - ModelAndViewContainer mavContainer, NativeWebRequest webRequest) { - // 获取合适的返回值处理器 - HandlerMethodReturnValueHandler handler = selectHandler(returnValue, returnType); - if (handler == null) { - throw new IllegalArgumentException(); - } - // 使用处理器处理返回值(详解源码中的这两个函数) - handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest); -} -``` + * `List configurations = SpringFactoriesLoader.loadFactoryNames()`:加载自动配置类 -HandlerMethodReturnValueHandlerComposite#selectHandler:获取合适的返回值处理器 + 参数一:`getSpringFactoriesLoaderFactoryClass()`:获取 @EnableAutoConfiguration 注解类 -* `boolean isAsyncValue = isAsyncReturnValue(value, returnType)`:是否是异步请求 + 参数二:`getBeanClassLoader()`:获取类加载器 -* `for (HandlerMethodReturnValueHandler handler : this.returnValueHandlers)`:遍历所有的返回值处理器 - * `RequestResponseBodyMethodProcessor#supportsReturnType`:**处理标注 @ResponseBody 注解的返回值** - * `ModelAndViewMethodReturnValueHandler#supportsReturnType`:处理返回值类型是 ModelAndView 的处理器 - * `ModelAndViewResolverMethodReturnValueHandler#supportsReturnType`:直接返回 true,处理所有数据 + * `factoryTypeName = factoryType.getName()`:@EnableAutoConfiguration 注解的全类名 + * `return loadSpringFactories(classLoaderToUse).getOrDefault()`:加载资源 + * `urls = classLoader.getResources(FACTORIES_RESOURCE_LOCATION)`:获取资源类 + * `FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories"`:**加载的资源的位置** + * `return configurations`:返回所有自动装配类的候选项 + * 从 spring-boot-autoconfigure-2.5.3.jar/META-INF/spring.factories 文件中寻找 EnableAutoConfiguration 字段,获取自动装配类,**进行条件装配,按需装配** -RequestResponseBodyMethodProcessor#handleReturnValue:处理返回值,要进行内容协商 + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/SpringBoot-自动装配配置文件.png) -* `mavContainer.setRequestHandled(true)`:设置请求处理完成 -* `inputMessage = createInputMessage(webRequest)`:获取输入的数据 -* `outputMessage = createOutputMessage(webRequest)`:获取输出的数据 -* `writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage)`:使用消息转换器进行写出 - * `if (value instanceof CharSequence)`:判断返回的数据是不是字符类型 +*** - * `body = value`:把 value 赋值给 body,此时 body 中就是自定义方法执行完后的 Person 对象 - * `if (isResourceType(value, returnType))`:当前数据是不是流数据 - * `MediaType selectedMediaType`:**内容协商后选择使用的类型,浏览器和服务器都支持的媒体(数据)类型** +#### 装配流程 - * `MediaType contentType = outputMessage.getHeaders().getContentType()`:获取响应头的数据 +Spring Boot 通过 `@EnableAutoConfiguration` 开启自动装配,通过 SpringFactoriesLoader 加载 `META-INF/spring.factories` 中的自动配置类实现自动装配,自动配置类其实就是通过 `@Conditional` 注解按需加载的配置类,想要其生效必须引入 `spring-boot-starter-xxx` 包实现起步依赖 - * `if (contentType != null && contentType.isConcrete())`:判断当前响应头中是否已经有确定的媒体类型 +* SpringBoot 先加载所有的自动配置类 xxxxxAutoConfiguration +* 每个自动配置类进行**条件装配**,默认都会绑定配置文件指定的值(xxxProperties 和配置文件进行了绑定) +* SpringBoot 默认会在底层配好所有的组件,如果用户自己配置了**以用户的优先** +* **定制化配置:** + - 用户可以使用 @Bean 新建自己的组件来替换底层的组件 + - 用户可以去看这个组件是获取的配置文件前缀值,在配置文件中修改 - `selectedMediaType = contentType`:前置处理已经使用了媒体类型,直接继续使用该类型 +以 DispatcherServletAutoConfiguration 为例: - * `acceptableTypes = getAcceptableMediaTypes(request)`:**获取浏览器支持的媒体类型,请求头字段** +```java +@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE) +// 类中的 Bean 默认不是单例 +@Configuration(proxyBeanMethods = false) +@ConditionalOnWebApplication(type = Type.SERVLET) +// 条件装配,环境中有 DispatcherServlet 类才进行自动装配 +@ConditionalOnClass(DispatcherServlet.class) +@AutoConfigureAfter(ServletWebServerFactoryAutoConfiguration.class) +public class DispatcherServletAutoConfiguration { + // 注册的 DispatcherServlet 的 BeanName + public static final String DEFAULT_DISPATCHER_SERVLET_BEAN_NAME = "dispatcherServlet"; - * `this.contentNegotiationManager.resolveMediaTypes()`:调用该方法 - * `for(ContentNegotiationStrategy strategy:this.strategies)`:**默认策略是提取请求头的字段的内容**,策略类为HeaderContentNegotiationStrategy,可以配置添加其他类型的策略 - * `List mediaTypes = strategy.resolveMediaTypes(request)`:解析 Accept 字段存储为 List - * `headerValueArray = request.getHeaderValues(HttpHeaders.ACCEPT)`:获取请求头中 Accept 字段 - * `List mediaTypes = MediaType.parseMediaTypes(headerValues)`:解析成 List 集合 - * `MediaType.sortBySpecificityAndQuality(mediaTypes)`:按照相对品质因数 q 降序排序 - - - ![](https://gitee.com/seazean/images/raw/master/Frame/SpringMVC-浏览器支持接收的数据类型.png) - -* `producibleTypes = getProducibleMediaTypes(request, valueType, targetType)`:**服务器能生成的媒体类型** - - * `request.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE)`:从请求域获取默认的媒体类型 - * ` for (HttpMessageConverter converter : this.messageConverters)`:遍历所有的消息转换器 - * `converter.canWrite(valueClass, null)`:是否支持当前的类型 - * ` result.addAll(converter.getSupportedMediaTypes())`:把当前 MessageConverter 支持的所有类型放入 result - -* `List mediaTypesToUse = new ArrayList<>()`:存储最佳匹配的集合 - -* **内容协商:** - - ```java - for (MediaType requestedType : acceptableTypes) { // 遍历所有浏览器能接受的媒体类型 - for (MediaType producibleType : producibleTypes) { // 遍历所有服务器能产出的 - if (requestedType.isCompatibleWith(producibleType)) { // 判断类型是否匹配,最佳匹配 - // 数据协商匹配成功,一般有多种 - mediaTypesToUse.add(getMostSpecificMediaType(requestedType, producibleType)); - } - } - } - ``` - -* `MediaType.sortBySpecificityAndQuality(mediaTypesToUse)`:按照相对品质因数 q 排序,降序排序,越大的越好 - -* `for (MediaType mediaType : mediaTypesToUse)`:**遍历所有的最佳匹配**,选择一种赋值给选择的类型 - -* `selectedMediaType = selectedMediaType.removeQualityValue()`:媒体类型去除相对品质因数 - -* `for (HttpMessageConverter converter : this.messageConverters)`:遍历所有的 HTTP 数据转换器 - -* `GenericHttpMessageConverter genericConverter`:**MappingJackson2HttpMessageConverter 可以将对象写为 JSON** - -* `((GenericHttpMessageConverter) converter).canWrite()`:判断转换器是否可以写出给定的类型 - - `AbstractJackson2HttpMessageConverter#canWrit` - - * `if (!canWrite(mediaType))`:是否可以写出指定类型 - * `MediaType.ALL.equalsTypeAndSubtype(mediaType)`:是不是 `*/*` 类型 - * `getSupportedMediaTypes()`:支持 `application/json` 和 `application/*+json` 两种类型 - * `return true`:返回 true - * `objectMapper = selectObjectMapper(clazz, mediaType)`:选择可以使用的 objectMapper - * `causeRef = new AtomicReference<>()`:获取并发安全的引用 - * `if (objectMapper.canSerialize(clazz, causeRef))`:objectMapper 可以序列化当前类 - * `return true`:返回 true - - * ` body = getAdvice().beforeBodyWrite()`:**获取要响应的所有数据,就是 Person 对象** - -* `addContentDispositionHeader(inputMessage, outputMessage)`:检查路径 - -* `genericConverter.write(body, targetType, selectedMediaType, outputMessage)`:调用消息转换器的 write 方法 - - `AbstractGenericHttpMessageConverter#write`:该类的方法 - - * `addDefaultHeaders(headers, t, contentType)`:**设置响应头中的数据类型** - - ![](https://gitee.com/seazean/images/raw/master/Frame/SpringMVC-服务器设置数据类型.png) - - * `writeInternal(t, type, outputMessage)`:**数据写出为 JSON 格式** - - * `Object value = object`:value 引用 Person 对象 - * `ObjectWriter objectWriter = objectMapper.writer()`:获取 ObjectWriter 对象 - * `objectWriter.writeValue(generator, value)`:使用 ObjectWriter 写出数据为 JSON + @Configuration(proxyBeanMethods = false) + @Conditional(DefaultDispatcherServletCondition.class) + @ConditionalOnClass(ServletRegistration.class) + // 绑定配置文件的属性,从配置文件中获取配置项 + @EnableConfigurationProperties(WebMvcProperties.class) + protected static class DispatcherServletConfiguration { + + // 给容器注册一个 DispatcherServlet,起名字为 dispatcherServlet + @Bean(name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME) + public DispatcherServlet dispatcherServlet(WebMvcProperties webMvcProperties) { + // 新建一个 DispatcherServlet 设置相关属性 + DispatcherServlet dispatcherServlet = new DispatcherServlet(); + // spring.mvc 中的配置项获取注入,没有就填充默认值 + dispatcherServlet.setDispatchOptionsRequest(webMvcProperties.isDispatchOptionsRequest()); + // ...... + // 返回该对象注册到容器内 + return dispatcherServlet; + } + + @Bean + // 容器中有这个类型组件才进行装配 + @ConditionalOnBean(MultipartResolver.class) + // 容器中没有这个名字 multipartResolver 的组件 + @ConditionalOnMissingBean(name = DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME) + // 方法名就是 BeanName + public MultipartResolver multipartResolver(MultipartResolver resolver) { + // 给 @Bean 标注的方法传入了对象参数,这个参数就会从容器中找,因为用户自定义了该类型,以用户配置的优先 + // 但是名字不符合规范,所以获取到该 Bean 并返回到容器一个规范的名称:multipartResolver + return resolver; + } + } +} +``` + +```java +// 将配置文件中的 spring.mvc 前缀的属性与该类绑定 +@ConfigurationProperties(prefix = "spring.mvc") +public class WebMvcProperties { } +``` @@ -11542,65 +14488,106 @@ RequestResponseBodyMethodProcessor#handleReturnValue:处理返回值,要进 -#### 协商策略 +### 事件监听 -开启基于请求参数的内容协商模式:(SpringBoot 方式) +SpringBoot 在项目启动时,会对几个监听器进行回调,可以实现监听器接口,在项目启动时完成一些操作 -```yaml -spring.mvc.contentnegotiation:favor-parameter: true # 开启请求参数内容协商模式 -``` +ApplicationContextInitializer、SpringApplicationRunListener、CommandLineRunner、ApplicationRunner -发请求: http://localhost:8080/person?format=json,解析 format +* MyApplicationRunner -策略类为 ParameterContentNegotiationStrategy,运行流程如下: + **自定义监听器的启动时机**:MyApplicationRunner 和 MyCommandLineRunner 都是当项目启动后执行,使用 @Component 放入容器即可使用 -* `acceptableTypes = getAcceptableMediaTypes(request)`:获取浏览器支持的媒体类型 + ```java + //当项目启动后执行run方法 + @Component + public class MyApplicationRunner implements ApplicationRunner { + @Override + public void run(ApplicationArguments args) throws Exception { + System.out.println("ApplicationRunner...run"); + System.out.println(Arrays.asList(args.getSourceArgs()));//properties配置信息 + } + } + ``` - `mediaTypes = strategy.resolveMediaTypes(request)`:解析请求 URL 参数中的数据 +* MyCommandLineRunner - * `return resolveMediaTypeKey(webRequest, getMediaTypeKey(webRequest))`: + ```java + @Component + public class MyCommandLineRunner implements CommandLineRunner { + @Override + public void run(String... args) throws Exception { + System.out.println("CommandLineRunner...run"); + System.out.println(Arrays.asList(args)); + } + } + ``` - `getMediaTypeKey(webRequest)`: +* MyApplicationContextInitializer 的启用要**在 resource 文件夹下添加 META-INF/spring.factories** - * `request.getParameter(getParameterName())`:获取 URL 中指定的需求的数据类型 - * `getParameterName()`:获取参数的属性名 format - * `getParameter()`:**获取 URL 中 format 对应的数据** + ```properties + org.springframework.context.ApplicationContextInitializer=\ + com.example.springbootlistener.listener.MyApplicationContextInitializer + ``` - `resolveMediaTypeKey()`:解析媒体类型,封装成集合 + ```java + @Component + public class MyApplicationContextInitializer implements ApplicationContextInitializer { + @Override + public void initialize(ConfigurableApplicationContext applicationContext) { + System.out.println("ApplicationContextInitializer....initialize"); + } + } + ``` -自定义内容协商策略: +* MySpringApplicationRunListener 的使用要添加**构造器** -```java -public class WebConfig implements WebMvcConfigurer { - @Bean - public WebMvcConfigurer webMvcConfigurer() { - return new WebMvcConfigurer() { - @Override //自定义内容协商策略 - public void configureContentNegotiation(ContentNegotiationConfigurer configurer) { - Map mediaTypes = new HashMap<>(); - mediaTypes.put("json", MediaType.APPLICATION_JSON); - mediaTypes.put("xml",MediaType.APPLICATION_XML); - mediaTypes.put("person",MediaType.parseMediaType("application/x-person")); - // 指定支持解析哪些参数对应的哪些媒体类型 - ParameterContentNegotiationStrategy parameterStrategy = new ParameterContentNegotiationStrategy(mediaTypes); + ```java + public class MySpringApplicationRunListener implements SpringApplicationRunListener { + //构造器 + public MySpringApplicationRunListener(SpringApplication sa, String[] args) { + } + + @Override + public void starting() { + System.out.println("starting...项目启动中");//输出SPRING之前 + } + + @Override + public void environmentPrepared(ConfigurableEnvironment environment) { + System.out.println("environmentPrepared...环境对象开始准备"); + } + + @Override + public void contextPrepared(ConfigurableApplicationContext context) { + System.out.println("contextPrepared...上下文对象开始准备"); + } + + @Override + public void contextLoaded(ConfigurableApplicationContext context) { + System.out.println("contextLoaded...上下文对象开始加载"); + } + + @Override + public void started(ConfigurableApplicationContext context) { + System.out.println("started...上下文对象加载完成"); + } + + @Override + public void running(ConfigurableApplicationContext context) { + System.out.println("running...项目启动完成,开始运行"); + } + + @Override + public void failed(ConfigurableApplicationContext context, Throwable exception) { + System.out.println("failed...项目启动失败"); + } + } + ``` - // 请求头解析 - HeaderContentNegotiationStrategy headStrategy = new HeaderContentNegotiationStrategy(); + - // 添加到容器中,即可以解析请求头 又可以解析请求参数 - configurer.strategies(Arrays.asList(parameterStrategy,headStrategy)); - } - - @Override // 自定义消息转换器 - public void extendMessageConverters(List> converters) { - converters.add(new GuiguMessageConverter()); - } - } - } -} -``` -也可以自定义 HttpMessageConverter,实现 HttpMessageConverter 接口重写方法即可 @@ -11608,70 +14595,39 @@ public class WebConfig implements WebMvcConfigurer { -### 视图解析 -#### 返回解析 -请求处理: +## 配置文件 -```java -@GetMapping("/params") -public String param(){ - return "forward:/success"; - //return "redirect:/success"; -} -``` +### 配置方式 -进入执行方法逻辑 ServletInvocableHandlerMethod#invokeAndHandle,进入 `this.returnValueHandlers.handleReturnValue`: +#### 文件类型 -```java -public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType, - ModelAndViewContainer mavContainer, NativeWebRequest webRequest) { - // 获取合适的返回值处理器:调用 if (handler.supportsReturnType(returnType))判断是否支持 - HandlerMethodReturnValueHandler handler = selectHandler(returnValue, returnType); - if (handler == null) { - throw new IllegalArgumentException(); - } - // 使用处理器处理返回值 - handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest); -} -``` +SpringBoot 是基于约定的,很多配置都有默认值,如果想使用自己的配置替换默认配置,可以使用 application.properties 或者 application.yml(application.yaml)进行配置 -* ViewNameMethodReturnValueHandler#supportsReturnType: +* 默认配置文件名称:application +* 在同一级目录下优先级为:properties > yml > yaml - ```java - public boolean supportsReturnType(MethodParameter returnType) { - Class paramType = returnType.getParameterType(); - // 返回值是否是 void 或者是 CharSequence 字符序列,这里是字符序列 - return (void.class == paramType || CharSequence.class.isAssignableFrom(paramType)); - } +例如配置内置 Tomcat 的端口 + +* properties: + + ```properties + server.port=8080 ``` -* ViewNameMethodReturnValueHandler#handleReturnValue: +* yml: - ```java - public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType, - ModelAndViewContainer mavContainer, - NativeWebRequest webRequest) throws Exception { - // 返回值是字符串,是 return "forward:/success" - if (returnValue instanceof CharSequence) { - String viewName = returnValue.toString(); - // 【把视图名称设置进入 ModelAndViewContainer 中】 - mavContainer.setViewName(viewName); - // 判断是否是重定向数据 `viewName.startsWith("redirect:")` - if (isRedirectViewName(viewName)) { - // 如果是重定向,设置是重定向指令 - mavContainer.setRedirectModelScenario(true); - } - } - else if (returnValue != null) { - // should not happen - throw new UnsupportedOperationException(); - } - } + ```yaml + server: port: 8080 + ``` + +* yaml: + + ```yaml + server: port: 8080 ``` - @@ -11679,399 +14635,382 @@ public void handleReturnValue(@Nullable Object returnValue, MethodParameter retu -#### 结果派发 +#### 加载顺序 -doDispatch()中的 processDispatchResult:处理派发结果 +所有位置的配置文件都会被加载,互补配置,**高优先级配置内容会覆盖低优先级配置内容** -```java -private void processDispatchResult(HttpServletRequest request, HttpServletResponse response, - @Nullable HandlerExecutionChain mappedHandler, - @Nullable ModelAndView mv, - @Nullable Exception exception) throws Exception { - boolean errorView = false; - if (exception != null) { - } - // mv 是 ModelAndValue - if (mv != null && !mv.wasCleared()) { - // 渲染视图 - render(mv, request, response); - if (errorView) { - WebUtils.clearErrorRequestAttributes(request); - } - } - else {} -} -``` +扫描配置文件的位置按优先级**从高到底**: -DispatcherServlet#render: +- `file:./config/`:**当前项目**下的 /config 目录下 -* `Locale locale = this.localeResolver.resolveLocale(request)`:国际化相关 +- `file:./`:当前项目的根目录,Project工程目录 -* `String viewName = mv.getViewName()`:视图名字,是请求转发 forward:/success(响应数据解析并存入 ModelAndView) +- `classpath:/config/`:classpath 的 /config 目录 -* `view = resolveViewName(viewName, mv.getModelInternal(), locale, request)`:解析视图 +- `classpath:/`:classpath 的根目录,就是 resoureces 目录 - * `for (ViewResolver viewResolver : this.viewResolvers)`:遍历所有的视图解析器 +项目外部配置文件加载顺序:外部配置文件的使用是为了对内部文件的配合 - `view = viewResolver.resolveViewName(viewName, locale)`:根据视图名字解析视图,调用内容协商视图处理器 ContentNegotiatingViewResolver 的方法 +* 命令行:在 package 打包后的 target 目录下,使用该命令 - * `attrs = RequestContextHolder.getRequestAttributes()`:获取请求的相关属性信息 + ```sh + java -jar myproject.jar --server.port=9000 + ``` - * `requestedMediaTypes = getMediaTypes(((ServletRequestAttributes) attrs).getRequest())`:获取最佳匹配的媒体类型,函数内进行了匹配的逻辑 +* 指定配置文件位置 - * `candidateViews = getCandidateViews(viewName, locale, requestedMediaTypes)`:获取候选的视图对象 + ```sh + java -jar myproject.jar --spring.config.location=e://application.properties + ``` - * `for (ViewResolver viewResolver : this.viewResolvers)`:遍历所有的视图解析器 +* 按优先级从高到底选择配置文件的加载命令 - * `View view = viewResolver.resolveViewName(viewName, locale)`:**解析视图** + ```sh + java -jar myproject.jar + ``` - `AbstractCachingViewResolver#resolveViewName`: + - * `returnview = createView(viewName, locale)`:UrlBasedViewResolver#createView - **请求转发**:实例为 InternalResourceView - * `if (viewName.startsWith(FORWARD_URL_PREFIX))`:视图名字是否是 **`forward:`** 的前缀 - * `forwardUrl = viewName.substring(FORWARD_URL_PREFIX.length())`:名字截取前缀 - * `view = new InternalResourceView(forwardUrl)`:新建 InternalResourceView 对象并返回 +*** - * `return applyLifecycleMethods(FORWARD_URL_PREFIX, view)`:Spring 中的初始化操作 - **重定向**:实例为 RedirectView - * `if (viewName.startsWith(REDIRECT_URL_PREFIX))`:视图名字是否是 **`redirect:`** 的前缀 - * `redirectUrl = viewName.substring(REDIRECT_URL_PREFIX.length())`:名字截取前缀 - * `RedirectView view = new RedirectView()`:新建 RedirectView 对象并返回 +### yaml语法 - * `bestView = getBestView(candidateViews, requestedMediaTypes, attrs)`:选出最佳匹配的视图对象 +基本语法: -* `view.render(mv.getModelInternal(), request, response)`:**页面渲染** +- 大小写敏感 - * `mergedModel = createMergedOutputModel(model, request, response)`:把请求域中的数据封装到 model +- **数据值前边必须有空格,作为分隔符** - * `prepareResponse(request, response)`:响应前的准备工作,设置一些响应头 +- 使用缩进表示层级关系 - * `renderMergedOutputModel(mergedModel, getRequestToExpose(request), response)`:渲染输出的数据 +- 缩进时不允许使用Tab键,只允许使用空格(各个系统 Tab对应空格数目可能不同,导致层次混乱) - `getRequestToExpose(request)`:获取 Servlet 原生的方式 +- 缩进的空格数目不重要,只要相同层级的元素左侧对齐即可 - **请求转发 InternalResourceView 的逻辑:请求域中的数据不丢失** +- ''#" 表示注释,从这个字符一直到行尾,都会被解析器忽略 - * `exposeModelAsRequestAttributes(model, request)`:暴露 model 作为请求域的属性 - * `model.forEach()`:遍历 Model 中的数据 - * `request.setAttribute(name, value)`:**设置到请求域中** - * `exposeHelpers(request)`:自定义接口 - * `dispatcherPath = prepareForRendering(request, response)`:确定调度分派的路径,此例是 /success - * `rd = getRequestDispatcher(request, dispatcherPath)`:**获取 Servlet 原生的 RequestDispatcher 实现转发** - * `rd.forward(request, response)`:实现请求转发 + ```yaml + server: + port: 8080 + address: 127.0.0.1 + ``` - **重定向 RedirectView 的逻辑:请求域中的数据会丢失** +数据格式: - * `targetUrl = createTargetUrl(model, request)`:获取目标 URL - * `enc = request.getCharacterEncoding()`:设置编码 UTF-8 - * `appendQueryProperties(targetUrl, model, enc)`:添加一些属性,比如 `url + ?name=123&&age=324` - * `sendRedirect(request, response, targetUrl, this.http10Compatible)`:重定向 -* `response.sendRedirect(encodedURL)`:**使用 Servlet 原生方法实现重定向** +* 纯量:单个的、不可再分的值 + ```yaml + msg1: 'hello \n world' # 单引忽略转义字符 + msg2: "hello \n world" # 双引识别转义字符 + ``` +* 对象:键值对集合,Map、Hash + ```yaml + person: + name: zhangsan + age: 20 + # 行内写法 + person: {name: zhangsan} + ``` + 注意:不建议使用 JSON,应该使用 yaml 语法 +* 数组:一组按次序排列的值,List、Array + ```yaml + address: + - beijing + - shanghai + # 行内写法 + address: [beijing,shanghai] + ``` -**** + ```yaml + allPerson #List + - {name:lisi, age:18} + - {name:wangwu, age:20} + # 行内写法 + allPerson: [{name:lisi, age:18}, {name:wangwu, age:20}] + ``` +* 参数引用: + ```yaml + name: lisi + person: + name: ${name} # 引用上边定义的name值 + ``` -## 异步调用 +*** -### 请求参数 -名称:@RequestBody -类型:形参注解 +### 获取配置 -位置:处理器类中的方法形参前方 +三种获取配置文件的方式: -作用:将异步提交数据**转换**成标准请求参数格式,并赋值给形参 -范例: +* 注解 @Value -```java -@Controller //控制层 -public class AjaxController { - @RequestMapping("/ajaxController") - public String ajaxController(@RequestBody String message){ - System.out.println(message); - return "page.jsp"; - } -} -``` + ```java + @RestController + public class HelloController { + @Value("${name}") + private String name; + + @Value("${person.name}") + private String name2; + + @Value("${address[0]}") + private String address1; + + @Value("${msg1}") + private String msg1; + + @Value("${msg2}") + private String msg2; + + @RequestMapping("/hello") + public String hello(){ + System.out.println("所有的数据"); + return " hello Spring Boot !"; + } + } + ``` -* 注解添加到 POJO 参数前方时,封装的异步提交数据按照 POJO 的属性格式进行关系映射 - * POJO 中的属性如果请求数据中没有,属性值为 null - * POJO 中没有的属性如果请求数据中有,不进行映射 -* 注解添加到集合参数前方时,封装的异步提交数据按照集合的存储结构进行关系映射 +* Evironment 对象 -```java -@RequestMapping("/ajaxPojoToController") -//如果处理参数是POJO,且页面发送的请求数据格式与POJO中的属性对应,@RequestBody注解可以自动映射对应请求数据到POJO中 -public String ajaxPojoToController(@RequestBody User user){ - System.out.println("controller pojo :"+user); - return "page.jsp"; -} + ```java + @Autowired + private Environment env; + + @RequestMapping("/hello") + public String hello() { + System.out.println(env.getProperty("person.name")); + System.out.println(env.getProperty("address[0]")); + return " hello Spring Boot !"; + } + ``` -@RequestMapping("/ajaxListToController") -//如果处理参数是List集合且封装了POJO,且页面发送的数据是JSON格式,数据将自动映射到集合参数 -public String ajaxListToController(@RequestBody List userList){ - System.out.println("controller list :"+userList); - return "page.jsp"; -} -``` +* 注解 @ConfigurationProperties 配合 @Component 使用 -ajax.jsp + **注意**:参数 prefix 一定要指定 -```html -<%@page pageEncoding="UTF-8" language="java" contentType="text/html;UTF-8" %> + ```java + @Component //不扫描该组件到容器内,无法完成自动装配 + @ConfigurationProperties(prefix = "person") + public class Person { + private String name; + private int age; + private String[] address; + } + ``` -访问springmvc后台controller
-传递Json格式POJO
-传递Json格式List
- - - -``` + ```java + @Autowired + private Person person; + + @RequestMapping("/hello") + public String hello() { + System.out.println(person); + //Person{name='zhangsan', age=20, address=[beijing, shanghai]} + return " hello Spring Boot !"; + } + ``` + + + +*** -web.xml配置:请求响应章节请求中的web.xml配置 -```xml -CharacterEncodingFilter + DispatcherServlet -``` -spring-mvc.xml: +### 配置提示 + +自定义的类和配置文件绑定一般没有提示,添加如下依赖可以使用提示: ```xml - - - + + org.springframework.boot + spring-boot-configuration-processor + true + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.springframework.boot + spring-boot-configuration-processor + + + + + + ``` -**** +*** -### 响应数据 -注解:@ResponseBody -作用:将 java 对象转为 json 格式的数据 +### Profile -方法返回值为 POJO 时,自动封装数据成 Json 对象数据: +@Profile:指定组件在哪个环境的情况下才能被注册到容器中,不指定,任何环境下都能注册这个组件 -```java -@RequestMapping("/ajaxReturnJson") -@ResponseBody -public User ajaxReturnJson(){ - System.out.println("controller return json pojo..."); - User user = new User("Jockme",40); - return user; -} -``` + * 加了环境标识的 bean,只有这个环境被激活的时候才能注册到容器中,默认是 default 环境 + * 写在配置类上,只有是指定的环境的时候,整个配置类里面的所有配置才能开始生效 + * 没有标注环境标识的 bean 在,任何环境下都是加载的 -方法返回值为 List 时,自动封装数据成 json 对象数组数据: +Profile 的配置: -```java -@RequestMapping("/ajaxReturnJsonList") -@ResponseBody -//基于jackon技术,使用@ResponseBody注解可以将返回的保存POJO对象的集合转成json数组格式数据 -public List ajaxReturnJsonList(){ - System.out.println("controller return json list..."); - User user1 = new User("Tom",3); - User user2 = new User("Jerry",5); +* **profile 是用来完成不同环境下,配置动态切换功能** - ArrayList al = new ArrayList(); - al.add(user1); - al.add(user2); - return al; -} -``` +* **profile 配置方式**:多 profile 文件方式,提供多个配置文件,每个代表一种环境 -AJAX 文件: + * application-dev.properties/yml 开发环境 + * application-test.properties/yml 测试环境 + * sapplication-pro.properties/yml 生产环境 -```js -//为id="testAjaxReturnString"的组件绑定点击事件 -$("#testAjaxReturnString").click(function(){ - //发送异步调用 - $.ajax({ - type:"POST", - url:"ajaxReturnString", - //回调函数 - success:function(data){ - //打印返回结果 - alert(data); - } - }); -}); +* yml 多文档方式:在 yml 中使用 --- 分隔不同配置 -//为id="testAjaxReturnJson"的组件绑定点击事件 -$("#testAjaxReturnJson").click(function(){ - $.ajax({ - type:"POST", - url:"ajaxReturnJson", - success:function(data){ - alert(data['name']+" , "+data['age']); - } - }); -}); + ```yacas + --- + server: + port: 8081 + spring: + profiles:dev + --- + server: + port: 8082 + spring: + profiles:test + --- + server: + port: 8083 + spring: + profiles:pro + --- + ``` -//为id="testAjaxReturnJsonList"的组件绑定点击事件 -$("#testAjaxReturnJsonList").click(function(){ - $.ajax({ - type:"POST", - url:"ajaxReturnJsonList", - success:function(data){ - alert(data); - alert(data[0]["name"]); - alert(data[1]["age"]); - } - }); -}); -``` +* **profile 激活方式** + * 配置文件:在配置文件中配置:spring.profiles.active=dev + ```properties + spring.profiles.active=dev + ``` -**** + * 虚拟机参数:在VM options 指定:`-Dspring.profiles.active=dev` + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/SpringBoot-profile激活方式虚拟机参数.png) + * 命令行参数:`java –jar xxx.jar --spring.profiles.active=dev` -### 跨域访问 + 在 Program arguments 里输入,也可以先 package -跨域访问:当通过域名 A 下的操作访问域名 B 下的资源时,称为跨域访问。跨域访问时,会出现无法访问的现象。 -环境搭建: -* 为当前主机添加备用域名 - * 修改 windows 安装目录中的 host 文件 - * 格式: ip 域名 -* 动态刷新 DNS - * 命令: ipconfig /displaydns - * 命令: ipconfig /flushdns -跨域访问支持: -* 名称:@CrossOrigin -* 类型:方法注解 、 类注解 -* 位置:处理器类中的方法上方 或 类上方 -* 作用:设置当前处理器方法 / 处理器类中所有方法支持跨域访问 -* 范例: -```java -@RequestMapping("/cross") -@ResponseBody -//使用@CrossOrigin开启跨域访问 -//标注在处理器方法上方表示该方法支持跨域访问 -//标注在处理器类上方表示该处理器类中的所有处理器方法均支持跨域访问 -@CrossOrigin -public User cross(HttpServletRequest request){ - System.out.println("controller cross..." + request.getRequestURL()); - User user = new User("Jockme",36); - return user; -} -``` -* jsp 文件 +*** -```html -跨域访问
- - -``` +## Web开发 -*** +### 功能支持 + +SpringBoot 自动配置了很多约定,大多场景都无需自定义配置 +* 内容协商视图解析器 ContentNegotiatingViewResolver 和 BeanName 视图解析器 BeanNameViewResolver +* 支持静态资源(包括 webjars)和静态 index.html 页支持 +* 自动注册相关类:Converter、GenericConverter、Formatter +* 内容协商处理器:HttpMessageConverters +* 国际化:MessageCodesResolver +开发规范: +* 使用 `@Configuration` + `WebMvcConfigurer` 自定义规则,不使用 `@EnableWebMvc` 注解 +* 声明 `WebMvcRegistrations` 的实现类改变默认底层组件 +* 使用 `@EnableWebMvc` + `@Configuration` + `DelegatingWebMvcConfiguration` 全面接管 SpringMVC -## 拦截器 -### 基本介绍 +**** -拦截器(Interceptor)是一种动态拦截方法调用的机制 -作用: -1. 在指定的方法调用前后执行预先设定后的的代码 -2. 阻止原始方法的执行 +### 静态资源 -核心原理:AOP 思想 +#### 访问规则 -拦截器链:多个拦截器按照一定的顺序,对原始被调用功能进行增强 +默认的静态资源路径是 classpath 下的,优先级由高到低为:/META-INF/resources、/resources、 /static、/public 的包内,`/` 表示当前项目的根路径 -拦截器和过滤器对比: +静态映射 `/**` ,表示请求 `/ + 静态资源名` 就直接去默认的资源路径寻找请求的资源 -1. 归属不同: Filter 属于 Servlet 技术, Interceptor 属于 SpringMVC 技术 +处理原理:静请求去寻找 Controller 处理,不能处理的请求就会交给静态资源处理器,静态资源也找不到就响应 404 页面 + +* 修改默认资源路径: + + ```yaml + spring: + web: + resources: + static-locations:: [classpath:/haha/] + ``` + +* 修改静态资源访问前缀,默认是 `/**`: + + ```yaml + spring: + mvc: + static-path-pattern: /resources/** + ``` + + 访问 URL:http://localhost:8080/resources/ + 静态资源名,将所有资源**重定位**到 `/resources/` + +* webjar 访问资源: + + ```xml + + org.webjars + jquery + 3.5.1 + + ``` + + 访问地址:http://localhost:8080/webjars/jquery/3.5.1/jquery.js,后面地址要按照依赖里面的包路径 + + + +**** + + + +#### 欢迎页面 -2. 拦截内容不同: Filter 对所有访问进行增强, Interceptor 仅针对 SpringMVC 的访问进行增强 +静态资源路径下 index.html 默认作为欢迎页面,访问 http://localhost:8080 出现该页面,使用 welcome page 功能不能修改前缀 - +网页标签上的小图标可以自定义规则,把资源重命名为 favicon.ico 放在静态资源目录下即可 @@ -12079,130 +15018,251 @@ public User cross(HttpServletRequest request){ -### 处理方法 - -#### 前置处理 +#### 源码分析 -原始方法之前运行: +SpringMVC 功能的自动配置类 WebMvcAutoConfiguration: ```java -public boolean preHandle(HttpServletRequest request, - HttpServletResponse response, - Object handler) throws Exception { - System.out.println("preHandle"); - return true; +public class WebMvcAutoConfiguration { + //当前项目的根路径 + private static final String SERVLET_LOCATION = "/"; } ``` -* 参数: - * request:请求对象 - * response:响应对象 - * handler:被调用的处理器对象,本质上是一个方法对象,对反射中的Method对象进行了再包装 - * handler:public String controller.InterceptorController.handleRun - * handler.getClass():org.springframework.web.method.HandlerMethod -* 返回值: - * 返回值为 false,被拦截的处理器将不执行 +* 内部类 WebMvcAutoConfigurationAdapter: + ```java + @Import(EnableWebMvcConfiguration.class) + // 绑定 spring.mvc、spring.web、spring.resources 相关的配置属性 + @EnableConfigurationProperties({ WebMvcProperties.class,ResourceProperties.class, WebProperties.class }) + @Order(0) + public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer, ServletContextAware { + //有参构造器所有参数的值都会从容器中确定 + public WebMvcAutoConfigurationAdapter(/*参数*/) { + this.resourceProperties = resourceProperties.hasBeenCustomized() ? resourceProperties + : webProperties.getResources(); + this.mvcProperties = mvcProperties; + this.beanFactory = beanFactory; + this.messageConvertersProvider = messageConvertersProvider; + this.resourceHandlerRegistrationCustomizer = resourceHandlerRegistrationCustomizerProvider.getIfAvailable(); + this.dispatcherServletPath = dispatcherServletPath; + this.servletRegistrations = servletRegistrations; + this.mvcProperties.checkConfiguration(); + } + } + ``` + * ResourceProperties resourceProperties:获取和 spring.resources 绑定的所有的值的对象 + * WebMvcProperties mvcProperties:获取和 spring.mvc 绑定的所有的值的对象 + * ListableBeanFactory beanFactory:Spring 的 beanFactory + * HttpMessageConverters:找到所有的 HttpMessageConverters + * ResourceHandlerRegistrationCustomizer:找到 资源处理器的自定义器。 + * DispatcherServletPath:项目路径 + * ServletRegistrationBean:给应用注册 Servlet、Filter -*** +* WebMvcAutoConfiguration.WebMvcAutoConfigurationAdapter.addResourceHandler():两种静态资源映射规则 + ```java + public void addResourceHandlers(ResourceHandlerRegistry registry) { + //配置文件设置 spring.resources.add-mappings: false,禁用所有静态资源 + if (!this.resourceProperties.isAddMappings()) { + logger.debug("Default resource handling disabled");//被禁用 + return; + } + //注册webjars静态资源的映射规则 映射 路径 + addResourceHandler(registry, "/webjars/**", "classpath:/META-INF/resources/webjars/"); + //注册静态资源路径的映射规则 默认映射 staticPathPattern = "/**" + addResourceHandler(registry, this.mvcProperties.getStaticPathPattern(), (registration) -> { + //staticLocations = CLASSPATH_RESOURCE_LOCATIONS + registration.addResourceLocations(this.resourceProperties.getStaticLocations()); + if (this.servletContext != null) { + ServletContextResource resource = new ServletContextResource(this.servletContext, SERVLET_LOCATION); + registration.addResourceLocations(resource); + } + }); + } + ``` + ```java + @ConfigurationProperties("spring.web") + public class WebProperties { + public static class Resources { + //默认资源路径,优先级从高到低 + static final String[] CLASSPATH_RESOURCE_LOCATIONS = { "classpath:/META-INF/resources/", + "classpath:/resources/", + "classpath:/static/", "classpath:/public/" } + private String[] staticLocations = CLASSPATH_RESOURCE_LOCATIONS; + //可以进行规则重写 + public void setStaticLocations(String[] staticLocations) { + this.staticLocations = appendSlashIfNecessary(staticLocations); + this.customized = true; + } + } + } + ``` -#### 后置处理 +* WebMvcAutoConfiguration.EnableWebMvcConfiguration.welcomePageHandlerMapping():欢迎页 -原始方法运行后运行,如果原始方法被拦截,则不执行: + ```java + //spring.web 属性 + @EnableConfigurationProperties(WebProperties.class) + public static class EnableWebMvcConfiguration { + @Bean + public WelcomePageHandlerMapping welcomePageHandlerMapping(/*参数*/) { + WelcomePageHandlerMapping welcomePageHandlerMapping = new WelcomePageHandlerMapping( + new TemplateAvailabilityProviders(applicationContext), + applicationContext, getWelcomePage(), + //staticPathPattern = "/**" + this.mvcProperties.getStaticPathPattern()); + return welcomePageHandlerMapping; + } + } + WelcomePageHandlerMapping(/*参数*/) { + //所以限制 staticPathPattern 必须为 /** 才能启用该功能 + if (welcomePage != null && "/**".equals(staticPathPattern)) { + logger.info("Adding welcome page: " + welcomePage); + //重定向 + setRootViewName("forward:index.html"); + } + else if (welcomeTemplateExists(templateAvailabilityProviders, applicationContext)) { + logger.info("Adding welcome page template: index"); + setRootViewName("index"); + } + } + ``` -```java -public void postHandle(HttpServletRequest request, - HttpServletResponse response, - Object handler, - ModelAndView modelAndView) throws Exception { - System.out.println("postHandle"); -} -``` + WelcomePageHandlerMapping,访问 / 能访问到 index.html -参数: -* modelAndView:如果处理器执行完成具有返回结果,可以读取到对应数据与页面信息,并进行调整 +*** -*** +### Rest映射 +开启 Rest 功能 -#### 异常处理 +```yaml +spring: + mvc: + hiddenmethod: + filter: + enabled: true #开启页面表单的Rest功能 +``` -拦截器最后执行的方法,无论原始方法是否执行: +源码分析,注入了 HiddenHttpMethodFilte 解析 Rest 风格的访问: ```java -public void afterCompletion(HttpServletRequest request, - HttpServletResponse response, - Object handler, - Exception ex) throws Exception { - System.out.println("afterCompletion"); +public class WebMvcAutoConfiguration { + @Bean + @ConditionalOnMissingBean(HiddenHttpMethodFilter.class) + @ConditionalOnProperty(prefix = "spring.mvc.hiddenmethod.filter", name = "enabled") + public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() { + return new OrderedHiddenHttpMethodFilter(); + } } ``` -参数: +详细源码解析:SpringMVC → 基本操作 → Restful → 识别原理 -* ex:如果处理器执行过程中出现异常对象,可以针对异常情况进行单独处理 +Web 部分源码详解:SpringMVC → 运行原理 -*** +**** -### 拦截配置 +### 内嵌容器 -拦截路径: +SpringBoot 嵌入式 Servlet 容器,默认支持的 WebServe:Tomcat、Jetty、Undertow -* `/**`:表示拦截所有映射 -* `/* `:表示拦截所有/开头的映射 -* `/user/*`:表示拦截所有 /user/ 开头的映射 -* `/user/add*`:表示拦截所有 /user/ 开头,且具体映射名称以 add 开头的映射 -* `/user/*All`:表示拦截所有 /user/ 开头,且具体映射名称以 All 结尾的映射 +配置方式: ```xml - - - - - - - - - - - + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-tomcat + + + + + org.springframework.boot + spring-boot-starter-jetty + ``` +Web 应用启动,SpringBoot 导入 Web 场景包 tomcat,创建一个 Web 版的 IOC 容器: +* `SpringApplication.run(BootApplication.class, args)`:应用启动 -*** +* `ConfigurableApplicationContext.run()`: + * `context = createApplicationContext()`:**创建容器** + * `applicationContextFactory = ApplicationContextFactory.DEFAULT` -### 拦截器链 + ```java + ApplicationContextFactory DEFAULT = (webApplicationType) -> { + try { + switch (webApplicationType) { + case SERVLET: + // Servlet 容器,继承自 ServletWebServerApplicationContext + return new AnnotationConfigServletWebServerApplicationContext(); + case REACTIVE: + // 响应式编程 + return new AnnotationConfigReactiveWebServerApplicationContext(); + default: + // 普通 Spring 容器 + return new AnnotationConfigApplicationContext(); + } + } catch (Exception ex) { + throw new IllegalStateException(); + } + } + ``` -**责任链模式**:责任链模式是一种行为模式 + * `applicationContextFactory.create(this.webApplicationType)`:根据应用类型创建容器 -特点:沿着一条预先设定的任务链顺序执行,每个节点具有独立的工作任务 -优势: + * `refreshContext(context)`:容器启动刷新 -* 独立性:只关注当前节点的任务,对其他任务直接放行到下一节点 -* 隔离性:具备链式传递特征,无需知晓整体链路结构,只需等待请求到达后进行处理即可 -* 灵活性:可以任意修改链路结构动态新增或删减整体链路责任 -* 解耦:将动态任务与原始任务解耦 +内嵌容器工作流程: + +- Spring 容器启动逻辑中,在实例化非懒加载的单例 Bean 之前有一个方法 **onRefresh()**,留给子类去扩展,Web 容器就是重写这个方法创建 WebServer + + ```java + protected void onRefresh() { + //省略.... + createWebServer(); + } + private void createWebServer() { + ServletWebServerFactory factory = getWebServerFactory(); + this.webServer = factory.getWebServer(getSelfInitializer()); + createWebServer.end(); + } + ``` + + 获取 WebServer 工厂 ServletWebServerFactory,并且获取的数量不等于 1 会报错,Spring 底层有三种: + + `TomcatServletWebServerFactory`、`JettyServletWebServerFactory`、`UndertowServletWebServerFactory` + +- **自动配置类 ServletWebServerFactoryAutoConfiguration** 导入了 ServletWebServerFactoryConfiguration(配置类),根据条件装配判断系统中到底导入了哪个 Web 服务器的包,创建出服务器并启动 -缺点: +- 默认是 web-starter 导入 tomcat 包,容器中就有 TomcatServletWebServerFactory,创建出 Tomcat 服务器并启动, -* 链路过长时,处理效率低下 -* 可能存在节点上的循环引用现象,造成死循环,导致系统崩溃 + ```java + public TomcatWebServer(Tomcat tomcat, boolean autoStart, Shutdown shutdown) { + // 初始化 + initialize(); + } + ``` - + 初始化方法 initialize 中有启动方法:`this.tomcat.start()` @@ -12210,93 +15270,48 @@ public void afterCompletion(HttpServletRequest request, -### 源码解析 +### 自定义 -DispatcherServlet#doDispatch 方法中: +#### 定制规则 ```java -protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { - try { - // 获取映射器以及映射器的所有拦截器(运行原理部分详解了源码) - mappedHandler = getHandler(processedRequest); - // 前置处理,返回 false 代表条件成立 - if (!mappedHandler.applyPreHandle(processedRequest, response)) { - //请求从这里直接结束 - return; +@Configuration +public class MyWebMvcConfigurer implements WebMvcConfigurer { + @Bean + public WebMvcConfigurer webMvcConfigurer() { + return new WebMvcConfigurer() { + //进行一些方法重写,来实现自定义的规则 + //比如添加一些解析器和拦截器,就是对原始容器功能的增加 } - //所有拦截器都返回 true,执行目标方法 - mv = ha.handle(processedRequest, response, mappedHandler.getHandler()) - // 倒序执行所有拦截器的后置处理方法 - mappedHandler.applyPostHandle(processedRequest, response, mv); - } catch (Exception ex) { - //异常处理机制 - triggerAfterCompletion(processedRequest, response, mappedHandler, ex); } + //也可以不加 @Bean,直接从这里重写方法进行功能增加 } ``` -HandlerExecutionChain#applyPreHandle:前置处理 - -```java -boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception { - //遍历所有的拦截器 - for (int i = 0; i < this.interceptorList.size(); i++) { - HandlerInterceptor interceptor = this.interceptorList.get(i); - //执行前置处理,如果拦截器返回 false,则条件成立,不在执行其他的拦截器,直接返回 false,请求直接结束 - if (!interceptor.preHandle(request, response, this.handler)) { - triggerAfterCompletion(request, response, null); - return false; - } - this.interceptorIndex = i; - } - return true; -} -``` -HandlerExecutionChain#applyPostHandle:后置处理 -```java -void applyPostHandle(HttpServletRequest request, HttpServletResponse response, @Nullable ModelAndView mv) - throws Exception { - //倒序遍历 - for (int i = this.interceptorList.size() - 1; i >= 0; i--) { - HandlerInterceptor interceptor = this.interceptorList.get(i); - interceptor.postHandle(request, response, this.handler, mv); - } -} -``` +*** -DispatcherServlet#triggerAfterCompletion 底层调用 HandlerExecutionChain#triggerAfterCompletion: -* 前面的步骤有任何异常都会直接倒序触发 afterCompletion -* 页面成功渲染有异常,也会倒序触发 afterCompletion +#### 定制容器 -```java -void triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response, @Nullable Exception ex) { - //倒序遍历 - for (int i = this.interceptorIndex; i >= 0; i--) { - HandlerInterceptor interceptor = this.interceptorList.get(i); - try { - //执行异常处理的方法 - interceptor.afterCompletion(request, response, this.handler, ex); - } - catch (Throwable ex2) { - logger.error("HandlerInterceptor.afterCompletion threw exception", ex2); - } - } -} -``` +@EnableWebMvc:全面接管 SpringMVC,所有规则全部自己重新配置 +- @EnableWebMvc + WebMvcConfigurer + @Bean 全面接管SpringMVC +- @Import(DelegatingWebMvcConfiguration.**class**),该类继承 WebMvcConfigurationSupport,自动配置了一些非常底层的组件,只能保证 SpringMVC 最基本的使用 -拦截器的执行流程: +原理:自动配置类 **WebMvcAutoConfiguration** 里面的配置要能生效,WebMvcConfigurationSupport 类不能被加载,所以 @EnableWebMvc 导致配置类失效,从而接管了 SpringMVC - +```java +@ConditionalOnMissingBean(WebMvcConfigurationSupport.class) +public class WebMvcAutoConfiguration {} +``` +注意:一般不适用此注解 -参考文章:https://www.yuque.com/atguigu/springboot/vgzmgh#wtPLU @@ -12304,713 +15319,629 @@ void triggerAfterCompletion(HttpServletRequest request, HttpServletResponse resp -### 自定义 - -* Contoller层 - - ```java - @Controller - public class InterceptorController { - @RequestMapping("/handleRun") - public String handleRun() { - System.out.println("业务处理器运行------------main"); - return "page.jsp"; - } - } - ``` - -* 自定义拦截器需要实现 HandleInterceptor 接口 - ```java - //自定义拦截器需要实现HandleInterceptor接口 - public class MyInterceptor implements HandlerInterceptor { - //处理器运行之前执行 - @Override - public boolean preHandle(HttpServletRequest request, - HttpServletResponse response, - Object handler) throws Exception { - System.out.println("前置运行----a1"); - //返回值为false将拦截原始处理器的运行 - //如果配置多拦截器,返回值为false将终止当前拦截器后面配置的拦截器的运行 - return true; - } - - //处理器运行之后执行 - @Override - public void postHandle(HttpServletRequest request, - HttpServletResponse response, - Object handler, - ModelAndView modelAndView) throws Exception { - System.out.println("后置运行----b1"); - } - - //所有拦截器的后置执行全部结束后,执行该操作 - @Override - public void afterCompletion(HttpServletRequest request, - HttpServletResponse response, - Object handler, - Exception ex) throws Exception { - System.out.println("完成运行----c1"); - } - } - ``` - 说明:三个方法的运行顺序为 preHandle → postHandle → afterCompletion,如果 preHandle 返回值为 false,三个方法仅运行preHandle +## 数据访问 -* web.xml: +### JDBC - ```xml - CharacterEncodingFilter + DispatcherServlet - ``` +#### 基本使用 -* 配置拦截器:spring-mvc.xml +导入 starter: - ```xml - - - - - - - - - ``` +```xml + + + org.springframework.boot + spring-boot-starter-data-jdbc + + + + mysql + mysql-connector-java + + +``` - 注意:配置顺序为**先配置执行位置,后配置执行类** +单独导入 MySQL 驱动是因为不确定用户使用的什么数据库 +配置文件: +```yaml +spring: + datasource: + url: jdbc:mysql://192.168.0.107:3306/db1?useSSL=false # 不加 useSSL 会警告 + username: root + password: 123456 + driver-class-name: com.mysql.jdbc.Driver +``` +测试文件: +```java +@Slf4j +@SpringBootTest +class Boot05WebAdminApplicationTests { -*** + @Autowired + JdbcTemplate jdbcTemplate; + @Test + void contextLoads() { + Long res = jdbcTemplate.queryForObject("select count(*) from account_tbl", Long.class); + log.info("记录总数:{}", res); + } +} +``` -## 异常处理 -### 处理器 +**** -异常处理器: **HandlerExceptionResolver** 接口 -类继承该接口的以后,当开发出现异常后会执行指定的功能 -```java -@Component -public class ExceptionResolver implements HandlerExceptionResolver { - @Override - public ModelAndView resolveException(HttpServletRequest request, - HttpServletResponse response, - Object handler, - Exception ex) { - System.out.println("异常处理器正在执行中"); - ModelAndView modelAndView = new ModelAndView(); - //定义异常现象出现后,反馈给用户查看的信息 - modelAndView.addObject("msg","出错啦! "); - //定义异常现象出现后,反馈给用户查看的页面 - modelAndView.setViewName("error.jsp"); - return modelAndView; - } -} -``` +#### 自动配置 -根据异常的种类不同,进行分门别类的管理,返回不同的信息: +DataSourceAutoConfiguration:数据源的自动配置 ```java -public class ExceptionResolver implements HandlerExceptionResolver { - @Override - public ModelAndView resolveException(HttpServletRequest request, - HttpServletResponse response, - Object handler, - Exception ex) { - System.out.println("my exception is running ...." + ex); - ModelAndView modelAndView = new ModelAndView(); - if( ex instanceof NullPointerException){ - modelAndView.addObject("msg","空指针异常"); - }else if ( ex instanceof ArithmeticException){ - modelAndView.addObject("msg","算数运算异常"); - }else{ - modelAndView.addObject("msg","未知的异常"); - } - modelAndView.setViewName("error.jsp"); - return modelAndView; - } +@Configuration(proxyBeanMethods = false) +@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class }) +@EnableConfigurationProperties(DataSourceProperties.class) +public class DataSourceAutoConfiguration { + + @Conditional(PooledDataSourceCondition.class) + @ConditionalOnMissingBean({ DataSource.class, XADataSource.class }) + @Import({ DataSourceConfiguration.Hikari.class, DataSourceConfiguration.Tomcat.class, + DataSourceConfiguration.Dbcp2.class, DataSourceConfiguration.OracleUcp.class}) + protected static class PooledDataSourceConfiguration {} } +// 配置项 +@ConfigurationProperties(prefix = "spring.datasource") +public class DataSourceProperties implements BeanClassLoaderAware, InitializingBean {} ``` -模拟错误: +- 底层默认配置好的连接池是:**HikariDataSource** +- 数据库连接池的配置,是容器中没有 DataSource 才自动配置的 +- 修改数据源相关的配置:spring.datasource -```java -@Controller -public class UserController { - @RequestMapping("/save") - @ResponseBody - public String save(@RequestBody String name) { - //模拟业务层发起调用产生了异常 -// int i = 1/0; -// String str = null; -// str.length(); +相关配置: - return "error.jsp"; - } -``` +- DataSourceTransactionManagerAutoConfiguration: 事务管理器的自动配置 +- JdbcTemplateAutoConfiguration: JdbcTemplate 的自动配置 + - 可以修改这个配置项 @ConfigurationProperties(prefix = **"spring.jdbc"**) 来修改JdbcTemplate + - `@AutoConfigureAfter(DataSourceAutoConfiguration.class)`:在 DataSource 装配后装配 +- JndiDataSourceAutoConfiguration: jndi 的自动配置 +- XADataSourceAutoConfiguration: 分布式事务相关 -*** +**** -### 注解开发 -使用注解实现异常分类管理,开发异常处理器 -@ControllerAdvice 注解: +### Druid -* 类型:类注解 +导入坐标: -* 位置:异常处理器类上方 +```xml + + com.alibaba + druid-spring-boot-starter + 1.1.17 + +``` -* 作用:设置当前类为异常处理器类 +```java +@Configuration +@ConditionalOnClass(DruidDataSource.class) +@AutoConfigureBefore(DataSourceAutoConfiguration.class) +@EnableConfigurationProperties({DruidStatProperties.class, DataSourceProperties.class}) +@Import({DruidSpringAopConfiguration.class, + DruidStatViewServletConfiguration.class, + DruidWebStatFilterConfiguration.class, + DruidFilterConfiguration.class}) +public class DruidDataSourceAutoConfigure {} +``` -* 格式: +自动配置: - ```java - @Component - //声明该类是一个Controller的通知类,声明后该类就会被加载成异常处理器 - @ControllerAdvice - public class ExceptionAdvice { - } - ``` +- 扩展配置项 **spring.datasource.druid** +- DruidSpringAopConfiguration: 监控 SpringBean,配置项为 `spring.datasource.druid.aop-patterns` -@ExceptionHandler 注解: +- DruidStatViewServletConfiguration:监控页的配置项为 `spring.datasource.druid.stat-view-servlet`,默认开启 +- DruidWebStatFilterConfiguration:Web 监控配置项为 `spring.datasource.druid.web-stat-filter`,默认开启 -* 类型:方法注解 +- DruidFilterConfiguration:所有 Druid 自己 filter 的配置 -* 位置:异常处理器类中针对指定异常进行处理的方法上方 +配置示例: -* 作用:设置指定异常的处理方式 +```yaml +spring: + datasource: + url: jdbc:mysql://localhost:3306/db_account + username: root + password: 123456 + driver-class-name: com.mysql.jdbc.Driver -* 说明:处理器方法可以设定多个 + druid: + aop-patterns: com.atguigu.admin.* #监控SpringBean + filters: stat,wall # 底层开启功能,stat(sql监控),wall(防火墙) -* 格式: + stat-view-servlet: # 配置监控页功能 + enabled: true + login-username: admin #项目启动访问:http://localhost:8080/druid ,账号和密码是 admin + login-password: admin + resetEnable: false - ```java - @Component - @ControllerAdvice - public class ExceptionAdvice { - //类中定义的方法携带@ExceptionHandler注解的会被作为异常处理器,后面添加实际处理的异常类型 - @ExceptionHandler(NullPointerException.class) - @ResponseBody - public String doNullException(Exception ex){ - return "空指针异常"; - } - - @ExceptionHandler(Exception.class) - @ResponseBody - public String doException(Exception ex){ - return "all Exception"; - } - } - ``` + web-stat-filter: # 监控web + enabled: true + urlPattern: /* + exclusions: '*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*' -@ResponseStatus 注解: + filter: + stat: # 对上面filters里面的stat的详细配置 + slow-sql-millis: 1000 + logSlowSql: true + enabled: true + wall: + enabled: true + config: + drop-table-allow: false +``` -* 类型:类注解、方法注解 -* 位置:异常处理器类、方法上方 -* 参数: +配置示例:https://github.com/alibaba/druid/tree/master/druid-spring-boot-starter - value:出现错误指定返回状态码 +配置项列表:https://github.com/alibaba/druid/wiki/DruidDataSource%E9%85%8D%E7%BD%AE%E5%B1%9E%E6%80%A7%E5%88%97%E8%A1%A8 - reason:出现错误返回的错误信息 +**** -*** +### MyBatis +#### 基本使用 +导入坐标: -### 解决方案 +```xml + + org.mybatis.spring.boot + mybatis-spring-boot-starter + 2.1.4 + +``` -* web.xml +* 编写 MyBatis 相关配置:application.yml + + ```yaml + # 配置mybatis规则 + mybatis: + # config-location: classpath:mybatis/mybatis-config.xml 建议不写 + mapper-locations: classpath:mybatis/mapper/*.xml + configuration: + map-underscore-to-camel-case: true + + #可以不写全局配置文件,所有全局配置文件的配置都放在 configuration 配置项中即可 + ``` + +* 定义表和实体类 ```java - DispatcherServlet + CharacterEncodingFilter + public class User { + private int id; + private String username; + private String password; + } ``` -* ajax.jsp +* 编写 dao 和 mapper 文件/纯注解开发 - ```jsp - <%@page pageEncoding="UTF-8" language="java" contentType="text/html;UTF-8" %> - - 点击
- - - + dao:**@Mapper 注解必须加,使用自动装配的 package,否则在启动类指定 @MapperScan() 扫描路径(不建议)** + + ```java + @Mapper //必须加Mapper + @Repository + public interface UserXmlMapper { + public List findAll(); + } ``` -* spring-mvc.xml + mapper.xml ```xml - - - + + + + + ``` -* java / controller / UserController +* 纯注解开发 ```java - @Controller - public class UserController { - @RequestMapping("/save") - @ResponseBody - public List save(@RequestBody User user) { - System.out.println("user controller save is running ..."); - //对用户的非法操作进行判定,并包装成异常对象进行处理,便于统一管理 - if(user.getName().trim().length() < 8){ - throw new BusinessException("对不起,用户名长度不满足要求,请重新输入!"); - } - if(user.getAge() < 0){ - throw new BusinessException("对不起,年龄必须是0到100之间的数字!"); - } - if(user.getAge() > 100){ - throw new SystemException("服务器连接失败,请尽快检查处理!"); - } - - User u1 = new User("Tom",3); - User u2 = new User("Jerry",5); - ArrayList al = new ArrayList(); - al.add(u1);al.add(u2); - return al; - } + @Mapper + @Repository + public interface UserMapper { + @Select("select * from t_user") + public List findAll(); } ``` -* 自定义异常 - ```java - //自定义异常继承RuntimeException,覆盖父类所有的构造方法 - public class BusinessException extends RuntimeException {覆盖父类所有的构造方法} - ``` - ```java - public class SystemException extends RuntimeException {} - ``` +**** + + -* 通过自定义异常将所有的异常现象进行分类管理,以统一的格式对外呈现异常消息 +#### 自动配置 - ```java - @Component - @ControllerAdvice - public class ProjectExceptionAdvice { - @ExceptionHandler(BusinessException.class) - public String doBusinessException(Exception ex, Model m){ - //使用参数Model将要保存的数据传递到页面上,功能等同于ModelAndView - //业务异常出现的消息要发送给用户查看 - m.addAttribute("msg",ex.getMessage()); - return "error.jsp"; - } - - @ExceptionHandler(SystemException.class) - public String doSystemException(Exception ex, Model m){ - //系统异常出现的消息不要发送给用户查看,发送统一的信息给用户看 - m.addAttribute("msg","服务器出现问题,请联系管理员!"); - return "error.jsp"; - } - - @ExceptionHandler(Exception.class) - public String doException(Exception ex, Model m){ - m.addAttribute("msg",ex.getMessage()); - //将ex对象保存起来 - return "error.jsp"; - } - - } - ``` +MybatisAutoConfiguration: - +```java +@EnableConfigurationProperties(MybatisProperties.class) //MyBatis配置项绑定类。 +@AutoConfigureAfter({ DataSourceAutoConfiguration.class, MybatisLanguageDriverAutoConfiguration.class }) +public class MybatisAutoConfiguration { + @Bean + @ConditionalOnMissingBean + public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception { + SqlSessionFactoryBean factory = new SqlSessionFactoryBean(); + return factory.getObject(); + } + + @org.springframework.context.annotation.Configuration + @Import(AutoConfiguredMapperScannerRegistrar.class) + @ConditionalOnMissingBean({ MapperFactoryBean.class, MapperScannerConfigurer.class }) + public static class MapperScannerRegistrarNotFoundConfiguration implements InitializingBean {} +} +@ConfigurationProperties(prefix = "mybatis") +public class MybatisProperties {} +``` +* 配置文件:`mybatis` +* 自动配置了 SqlSessionFactory +* 导入 `AutoConfiguredMapperScannerRegistra` 实现 @Mapper 的扫描 -*** +**** -## 文件传输 +#### MyBatis-Plus -### 上传下载 +```xml + + com.baomidou + mybatis-plus-boot-starter + 3.4.1 + +``` -上传文件过程: +自动配置类:MybatisPlusAutoConfiguration -![](https://gitee.com/seazean/images/raw/master/Frame/SpringMVC-上传文件过程分析.png) +只需要 Mapper 继承 **BaseMapper** 就可以拥有 CRUD 功能 -MultipartResolver接口: +*** -* MultipartResolver 接口定义了文件上传过程中的相关操作,并对通用性操作进行了封装 -* MultipartResolver 接口底层实现类 CommonsMultipartResovler -* CommonsMultipartResovler 并未自主实现文件上传下载对应的功能,而是调用了 apache 文件上传下载组件 -文件上传下载实现: -* 导入坐标 +### Redis - ```xml - - commons-fileupload - commons-fileupload - 1.4 - - ``` +#### 基本使用 -* 页面表单 fileupload.jsp +```xml + + org.springframework.boot + spring-boot-starter-data-redis + +``` - ```html - -
- - - ``` - -* web.xml +* 配置redis相关属性 - ```xml - DispatcherServlet + CharacterEncodingFilter + ```yaml + spring: + redis: + host: 127.0.0.1 # redis的主机ip + port: 6379 ``` -* 控制器 +* 注入 RedisTemplate 模板 ```java - @PostMapping("/upload") - public String upload(@RequestParam("email") String email, - @RequestParam("username") String username, - @RequestPart("headerImg") MultipartFile headerImg) throws IOException { + @RunWith(SpringRunner.class) + @SpringBootTest + public class SpringbootRedisApplicationTests { + @Autowired + private RedisTemplate redisTemplate; - if(!headerImg.isEmpty()){ - //保存到文件服务器,OSS服务器 - String originalFilename = headerImg.getOriginalFilename(); - headerImg.transferTo(new File("H:\\cache\\" + originalFilename)); + @Test + public void testSet() { + //存入数据 + redisTemplate.boundValueOps("name").set("zhangsan"); + } + @Test + public void testGet() { + //获取数据 + Object name = redisTemplate.boundValueOps("name").get(); + System.out.println(name); } - return "main"; } ``` +**** -*** +#### 自动配置 +RedisAutoConfiguration 自动配置类 -### 名称问题 +```java +@Configuration(proxyBeanMethods = false) +@ConditionalOnClass(RedisOperations.class) +@EnableConfigurationProperties(RedisProperties.class) +@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class }) +public class RedisAutoConfiguration { + @Bean + @ConditionalOnMissingBean(name = "redisTemplate") + @ConditionalOnSingleCandidate(RedisConnectionFactory.class) + public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) { + RedisTemplate template = new RedisTemplate<>(); + template.setConnectionFactory(redisConnectionFactory); + return template; + } -MultipartFile 参数中封装了上传的文件的相关信息。 + @Bean + @ConditionalOnMissingBean + @ConditionalOnSingleCandidate(RedisConnectionFactory.class) + public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) { + StringRedisTemplate template = new StringRedisTemplate(); + template.setConnectionFactory(redisConnectionFactory); + return template; + } -1. 文件命名问题, 获取上传文件名,并解析文件名与扩展名 +} +``` - ```java - file.getOriginalFilename(); - ``` +- 配置项:`spring.redis` +- 自动导入了连接工厂配置类:LettuceConnectionConfiguration、JedisConnectionConfiguration -2. 文件名过长问题 +- 自动注入了模板类:RedisTemplate 、StringRedisTemplate,k v 都是 String 类型 -3. 文件保存路径 +- 使用 @Autowired 注入模板类就可以操作 redis - ```java - ServletContext context = request.getServletContext(); - String realPath = context.getRealPath("/uploads"); - File file = new File(realPath + "/"); - if(!file.exists()) file.mkdirs(); - ``` -4. 重名问题 - ```java - String uuid = UUID.randomUUID.toString().replace("-", "").toUpperCase(); - ``` -```java -@Controller -public class FileUploadController { - @RequestMapping(value = "/fileupload") - //参数中定义MultipartFile参数,用于接收页面提交的type=file类型的表单,表单名称与参数名相同 - public String fileupload(MultipartFile file,MultipartFile file1,MultipartFile file2, HttpServletRequest request) throws IOException { - System.out.println("file upload is running ..."+file); -// MultipartFile参数中封装了上传的文件的相关信息 -// System.out.println(file.getSize()); -// System.out.println(file.getBytes().length); -// System.out.println(file.getContentType()); -// System.out.println(file.getName()); -// System.out.println(file.getOriginalFilename()); -// System.out.println(file.isEmpty()); - //首先判断是否是空文件,也就是存储空间占用为0的文件 - if(!file.isEmpty()){ - //如果大小在范围要求内正常处理,否则抛出自定义异常告知用户(未实现) - //获取原始上传的文件名,可以作为当前文件的真实名称保存到数据库中备用 - String fileName = file.getOriginalFilename(); - //设置保存的路径 - String realPath = request.getServletContext().getRealPath("/images"); - //保存文件的方法,通常文件名使用随机生成策略产生,避免文件名冲突问题 - file.transferTo(new File(realPath,file.getOriginalFilename())); - } - //测试一次性上传多个文件 - if(!file1.isEmpty()){ - String fileName = file1.getOriginalFilename(); - //可以根据需要,对不同种类的文件做不同的存储路径的区分,修改对应的保存位置即可 - String realPath = request.getServletContext().getRealPath("/images"); - file1.transferTo(new File(realPath,file1.getOriginalFilename())); - } - if(!file2.isEmpty()){ - String fileName = file2.getOriginalFilename(); - String realPath = request.getServletContext().getRealPath("/images"); - file2.transferTo(new File(realPath,file2.getOriginalFilename())); - } - return "page.jsp"; - } -} -``` + +**** -**** +## 单元测试 -### 源码解析 +### Junit5 -**StandardServletMultipartResolver** 文件上传解析器 +Spring Boot 2.2.0 版本开始引入 JUnit 5 作为单元测试默认库,由三个不同的子模块组成: -DispatcherServlet#doDispatch: +* JUnit Platform:在 JVM 上启动测试框架的基础,不仅支持 Junit 自制的测试引擎,其他测试引擎也可以接入 + +* JUnit Jupiter:提供了 JUnit5 的新的编程模型,是 JUnit5 新特性的核心,内部包含了一个测试引擎,用于在 Junit Platform 上运行 + +* JUnit Vintage:JUnit Vintage 提供了兼容 JUnit4.x、Junit3.x 的测试引擎 + + 注意:SpringBoot 2.4 以上版本移除了默认对 Vintage 的依赖,如果需要兼容 Junit4 需要自行引入 ```java -protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { - // 判断当前请求是不是文件上传请求 - processedRequest = checkMultipart(request); - // 文件上传请求会对 request 进行包装,导致两者不相等,此处赋值为 true,代表已经被解析 - multipartRequestParsed = (processedRequest != request); +@SpringBootTest +class Boot05WebAdminApplicationTests { + @Test + void contextLoads() { } } ``` -DispatcherServlet#checkMultipart: -* `if (this.multipartResolver != null && this.multipartResolver.isMultipart(request))`:判断是否是文件请求 - * `StandardServletMultipartResolver#isMultipart`:根据开头是否符合 multipart/form-data 或者 multipart/ -* `return this.multipartResolver.resolveMultipart(request)`:把请求封装成 StandardMultipartHttpServletRequest 对象 -开始执行 ha.handle() 目标方法进行数据的解析 -* RequestPartMethodArgumentResolver#supportsParameter:支持解析文件上传数据 - ```java - public boolean supportsParameter(MethodParameter parameter) { - // 参数上有 @RequestPart 注解 - if (parameter.hasParameterAnnotation(RequestPart.class)) { - return true; - } - } - ``` +*** + + + +### 常用注解 + +JUnit5 的注解如下: + +- @Test:表示方法是测试方法,但是与 JUnit4 的 @Test 不同,它的职责非常单一不能声明任何属性,拓展的测试将会由 Jupiter 提供额外测试,包是 `org.junit.jupiter.api.Test` +- @ParameterizedTest:表示方法是参数化测试 -* RequestPartMethodArgumentResolver#resolveArgument:解析参数数据,封装成 MultipartFile 对象 +- @RepeatedTest:表示方法可重复执行 +- @DisplayName:为测试类或者测试方法设置展示名称 - * `RequestPart requestPart = parameter.getParameterAnnotation(RequestPart.class)`:获取注解的相关信息 - * `String name = getPartName(parameter, requestPart)`:获取上传文件的名字 - * `Object mpArg = MultipartResolutionDelegate.resolveMultipartArgument()`:解析参数 - * `List files = multipartRequest.getFiles(name)`:获取文件的所有数据 +- @BeforeEach:表示在每个单元测试之前执行 +- @AfterEach:表示在每个单元测试之后执行 -* `return doInvoke(args)`:解析完成执行自定义的方法,完成上传功能 +- @BeforeAll:表示在所有单元测试之前执行 +- @AfterAll:表示在所有单元测试之后执行 +- @Tag:表示单元测试类别,类似于 JUnit4 中的 @Categories +- @Disabled:表示测试类或测试方法不执行,类似于 JUnit4 中的 @Ignore +- @Timeout:表示测试方法运行如果超过了指定时间将会返回错误 +- @ExtendWith:为测试类或测试方法提供扩展类引用 -*** +**** -## 实用技术 +### 断言机制 -### 校验框架 +#### 简单断言 -#### 校验概述 +断言(assertions)是测试方法中的核心,用来对测试需要满足的条件进行验证,断言方法都是 org.junit.jupiter.api.Assertions 的静态方法 -表单校验保障了数据有效性、安全性 +用来对单个值进行简单的验证: -校验分类:客户端校验和服务端校验 +| 方法 | 说明 | +| --------------- | ------------------------------------ | +| assertEquals | 判断两个对象或两个原始类型是否相等 | +| assertNotEquals | 判断两个对象或两个原始类型是否不相等 | +| assertSame | 判断两个对象引用是否指向同一个对象 | +| assertNotSame | 判断两个对象引用是否指向不同的对象 | +| assertTrue | 判断给定的布尔值是否为 true | +| assertFalse | 判断给定的布尔值是否为 false | +| assertNull | 判断给定的对象引用是否为 null | +| assertNotNull | 判断给定的对象引用是否不为 null | -* 格式校验 - * 客户端:使用 js 技术,利用正则表达式校验 - * 服务端:使用校验框架 -* 逻辑校验 - * 客户端:使用ajax发送要校验的数据,在服务端完成逻辑校验,返回校验结果 - * 服务端:接收到完整的请求后,在执行业务操作前,完成逻辑校验 +```java +@Test +@DisplayName("simple assertion") +public void simple() { + assertEquals(3, 1 + 2, "simple math"); + assertNull(null); + assertNotNull(new Object()); +} +``` -表单校验框架: -* JSR(Java Specification Requests):Java 规范提案 -* 303:提供bean属性相关校验规则 +**** -* JCP(Java Community Process):Java社区 -* Hibernate框架中包含一套独立的校验框架hibernate-validator -* 导入坐标: +#### 数组断言 - ```xml - - - javax.validation - validation-api - 2.0.1.Final - - - - org.hibernate - hibernate-validator - 6.1.0.Final - - ``` +通过 assertArrayEquals 方法来判断两个对象或原始类型的数组是否相等 -**注意:** +```java +@Test +@DisplayName("array assertion") +public void array() { + assertArrayEquals(new int[]{1, 2}, new int[] {1, 2}); +} +``` -* tomcat7:搭配 hibernate-validator 版本 5.*.*.Final -* tomcat8.5:搭配 hibernate-validator 版本 6.*.*.Final - *** -#### 基本使用 +#### 组合断言 -##### 开启校验 +assertAll 方法接受多个 org.junit.jupiter.api.Executable 函数式接口的实例作为验证的断言,可以通过 lambda 表达式提供这些断言 -名称:@Valid、@Validated +```java +@Test +@DisplayName("assert all") +public void all() { + assertAll("Math", + () -> assertEquals(2, 1 + 1), + () -> assertTrue(1 > 0) + ); +} +``` -类型:形参注解 -位置:处理器类中的实体类类型的方法形参前方 -作用:设定对当前实体类类型参数进行校验 +*** -范例: + + +#### 异常断言 + +Assertions.assertThrows(),配合函数式编程就可以进行使用 ```java -@RequestMapping(value = "/addemployee") -public String addEmployee(@Valid Employee employee) { - System.out.println(employee); +@Test +@DisplayName("异常测试") +public void exceptionTest() { + ArithmeticException exception = Assertions.assertThrows( + //扔出断言异常 + ArithmeticException.class, () -> System.out.println(1 / 0) + ); } ``` -##### 设置校验规则 - -名称:@NotNull +**** -类型:属性注解等 -位置:实体类属性上方 -作用:设定当前属性校验规则 +#### 超时断言 -范例:每个校验规则所携带的参数不同,根据校验规则进行相应的调整,具体的校验规则查看对应的校验框架进行获取 +Assertions.assertTimeout() 为测试方法设置了超时时间 ```java -public class Employee{ - @NotNull(message = "姓名不能为空") - private String name;//员工姓名 -} +@Test +@DisplayName("超时测试") +public void timeoutTest() { + //如果测试方法时间超过1s将会异常 + Assertions.assertTimeout(Duration.ofMillis(1000), () -> Thread.sleep(500)); +} ``` -##### 获取错误信息 +**** + + + +#### 快速失败 + +通过 fail 方法直接使得测试失败 ```java -@RequestMapping(value = "/addemployee") -//Errors对象用于封装校验结果,如果不满足校验规则,对应的校验结果封装到该对象中,包含校验的属性名和校验不通过返回的消息 -public String addEmployee(@Valid Employee employee, Errors errors, Model model){ - System.out.println(employee); - //判定Errors对象中是否存在未通过校验的字段 - if(errors.hasErrors()){ - for(FieldError error : errors.getFieldErrors()){ - //将校验结果添加到Model对象中,用于页面显示,返回json数据即可 - model.addAttribute(error.getField(),error.getDefaultMessage()); - } - //当出现未通过校验的字段时,跳转页面到原始页面,进行数据回显 - return "addemployee.jsp"; - } - return "success.jsp"; -} +@Test +@DisplayName("fail") +public void shouldFail() { + fail("This should fail"); +} ``` -通过形参Errors获取校验结果数据,通过Model接口将数据封装后传递到页面显示,页面获取后台封装的校验结果信息 -```html -
- 员工姓名:${name}
- 员工年龄:${age}
- -
-``` -**** +*** -#### 多规则校验 +### 前置条件 -* 同一个属性可以添加多个校验器 +JUnit 5 中的前置条件(assumptions)类似于断言,不同之处在于**不满足的断言会使得测试方法失败**,而不满足的**前置条件只会使得测试方法的执行终止**,前置条件可以看成是测试方法执行的前提,当该前提不满足时,就没有继续执行的必要 - ```java - public class Employee{ - @NotBlank(message = "姓名不能为空") - private String name;//员工姓名 - - @NotNull(message = "请输入年龄") - @Max(value = 60,message = "年龄最大值60") - @Min(value = 18,message = "年龄最小值18") - private Integer age;//员工年龄 - } - ``` +```java +@DisplayName("测试前置条件") +@Test +void testassumptions(){ + Assumptions.assumeTrue(false,"结果不是true"); + System.out.println("111111"); -* 三种判定空校验器的区别 - ![](https://gitee.com/seazean/images/raw/master/Frame/SpringMVC-三种判定空检验器的区别.png) +} +``` @@ -13018,733 +15949,521 @@ public String addEmployee(@Valid Employee employee, Errors errors, Model model){ -#### 嵌套校验 +### 嵌套测试 -名称:@Valid +JUnit 5 可以通过 Java 中的内部类和 @Nested 注解实现嵌套测试,从而可以更好的把相关的测试方法组织在一起,在内部类中可以使用 @BeforeEach 和 @AfterEach 注解,而且嵌套的层次没有限制 -类型:属性注解 +```java +@DisplayName("A stack") +class TestingAStackDemo { -位置:实体类中的引用类型属性上方 + Stack stack; -作用:设定当前应用类型属性中的属性开启校验 + @Test + @DisplayName("is instantiated with new Stack()") + void isInstantiatedWithNew() { + assertNull(stack) + } -范例: + @Nested + @DisplayName("when new") + class WhenNew { -```java -public class Employee { - //实体类中的引用类型通过标注@Valid注解,设定开启当前引用类型字段中的属性参与校验 - @Valid - private Address address; + @BeforeEach + void createNewStack() { + stack = new Stack<>(); + } + + @Test + @DisplayName("is empty") + void isEmpty() { + assertTrue(stack.isEmpty()); + } + + @Test + @DisplayName("throws EmptyStackException when popped") + void throwsExceptionWhenPopped() { + assertThrows(EmptyStackException.class, stack::pop); + } + } } ``` -注意:开启嵌套校验后,被校验对象内部需要添加对应的校验规则 - -```java -//嵌套校验的实体中,对每个属性正常添加校验规则即可 -public class Address implements Serializable { - @NotBlank(message = "请输入省份名称") - private String provinceName;//省份名称 - @NotBlank(message = "请输入邮政编码") - @Size(max = 6,min = 6,message = "邮政编码由6位组成") - private String zipCode;//邮政编码 -} -``` -*** +**** -#### 分组校验 +### 参数测试 -分组校验的介绍 +参数化测试是 JUnit5 很重要的一个新特性,它使得用不同的参数多次运行测试成为了可能 -* 同一个模块,根据执行的业务不同,需要校验的属性会有不同 - * 新增用户 - * 修改用户 -* 对不同种类的属性进行分组,在校验时可以指定参与校验的字段所属的组类别 - * 定义组(通用) - * 为属性设置所属组,可以设置多个 - * 开启组校验 +利用**@ValueSource**等注解,指定入参,我们将可以使用不同的参数进行多次单元测试,而不需要每新增一个参数就新增一个单元测试,省去了很多冗余代码。 -domain: +* @ValueSource:为参数化测试指定入参来源,支持八大基础类以及 String 类型、Class 类型 -```java -//用于设定分组校验中的组名,当前接口仅提供字节码,用于识别 -public interface GroupOne { -} -``` +* @NullSource:表示为参数化测试提供一个 null 的入参 -```java -public class Employee{ - @NotBlank(message = "姓名不能为空",groups = {GroupA.class}) - private String name;//员工姓名 +* @EnumSource:表示为参数化测试提供一个枚举入参 - @NotNull(message = "请输入年龄",groups = {GroupA.class}) - @Max(value = 60,message = "年龄最大值60")//不加Group的校验不生效 - @Min(value = 18,message = "年龄最小值18") - private Integer age;//员工年龄 +* @CsvFileSource:表示读取指定 CSV 文件内容作为参数化测试入参 - @Valid - private Address address; - //...... -} -``` +* @MethodSource:表示读取指定方法的返回值作为参数化测试入参(注意方法返回需要是一个流) -controller: -```java -@Controller -public class EmployeeController { - @RequestMapping(value = "/addemployee") - public String addEmployee(@Validated({GroupA.class}) Employee employee, Errors errors, Model m){ - if(errors.hasErrors()){ - List fieldErrors = errors.getFieldErrors(); - System.out.println(fieldErrors.size()); - for(FieldError error : fieldErrors){ - m.addAttribute(error.getField(),error.getDefaultMessage()); - } - return "addemployee.jsp"; - } - return "success.jsp"; - } -} -``` -jsp: -```html -
<%--页面使用${}获取后台传递的校验信息--%> - 员工姓名:${name}
- 员工年龄:${age}
- <%--注意,引用类型的校验未通过信息不是通过对象进行封装的,直接使用对象名.属性名的格式作为整体属性字符串进行保存的,和使用者的属性传递方式有关,不具有通用性,仅适用于本案例--%> - 省:${requestScope['address.provinceName']}
- -/form> -``` +*** -**** -### Lombok +## 指标监控 -Lombok 用标签方式代替构造器、getter/setter、toString() 等方法 +### Actuator -引入依赖: +每一个微服务在云上部署以后,都需要对其进行监控、追踪、审计、控制等,SpringBoot 抽取了 Actuator 场景,使得每个微服务快速引用即可获得生产级别的应用监控、审计等功能 ```xml - org.projectlombok - lombok + org.springframework.boot + spring-boot-starter-actuator ``` -下载插件:IDEA 中 File → Settings → Plugins,搜索安装 Lombok 插件 - -常用注解: - -```java -@NoArgsConstructor // 无参构造 -@AllArgsConstructor // 全参构造 -@Data // set + get -@ToString // toString -@EqualsAndHashCode // hashConde + equals -``` - -简化日志: +暴露所有监控信息为 HTTP: -```java -@Slf4j -@RestController -public class HelloController { - @RequestMapping("/hello") - public String handle01(@RequestParam("name") String name){ - log.info("请求进来了...."); - return "Hello, Spring!" + "你好:" + name; - } -} +```yaml +management: + endpoints: + enabled-by-default: true #暴露所有端点信息 + web: + exposure: + include: '*' #以web方式暴露 ``` +访问 http://localhost:8080/actuator/[beans/health/metrics/] +可视化界面:https://github.com/codecentric/spring-boot-admin +**** +### Endpoint -*** - +默认所有的 Endpoint 除过 shutdown 都是开启的 +```yaml +management: + endpoints: + enabled-by-default: false #禁用所有的 + endpoint: #手动开启一部分 + beans: + enabled: true + health: + enabled: true +``` +端点: +| ID | 描述 | +| ------------------ | ------------------------------------------------------------ | +| `auditevents` | 暴露当前应用程序的审核事件信息。需要一个 `AuditEventRepository` 组件 | +| `beans` | 显示应用程序中所有 Spring Bean 的完整列表 | +| `caches` | 暴露可用的缓存 | +| `conditions` | 显示自动配置的所有条件信息,包括匹配或不匹配的原因 | +| `configprops` | 显示所有 `@ConfigurationProperties` | +| `env` | 暴露 Spring 的属性 `ConfigurableEnvironment` | +| `flyway` | 显示已应用的所有 Flyway 数据库迁移。 需要一个或多个 Flyway 组件。 | +| `health` | 显示应用程序运行状况信息 | +| `httptrace` | 显示 HTTP 跟踪信息,默认情况下 100 个 HTTP 请求-响应需要一个 `HttpTraceRepository` 组件 | +| `info` | 显示应用程序信息 | +| `integrationgraph` | 显示 Spring integrationgraph,需要依赖 `spring-integration-core` | +| `loggers` | 显示和修改应用程序中日志的配置 | +| `liquibase` | 显示已应用的所有 Liquibase 数据库迁移,需要一个或多个 Liquibase 组件 | +| `metrics` | 显示当前应用程序的指标信息。 | +| `mappings` | 显示所有 `@RequestMapping` 路径列表 | +| `scheduledtasks` | 显示应用程序中的计划任务 | +| `sessions` | 允许从 Spring Session 支持的会话存储中检索和删除用户会话,需要使用 Spring Session 的基于 Servlet 的 Web 应用程序 | +| `shutdown` | 使应用程序正常关闭,默认禁用 | +| `startup` | 显示由 `ApplicationStartup` 收集的启动步骤数据。需要使用 `SpringApplication` 进行配置 `BufferingApplicationStartup` | +| `threaddump` | 执行线程转储 | -# SSM +应用程序是 Web 应用程序(Spring MVC,Spring WebFlux 或 Jersey),则可以使用以下附加端点: -## XML +| ID | 描述 | +| ------------ | ------------------------------------------------------------ | +| `heapdump` | 返回 `hprof` 堆转储文件。 | +| `jolokia` | 通过 HTTP 暴露 JMX bean(需要引入 Jolokia,不适用于 WebFlux),需要引入依赖 `jolokia-core` | +| `logfile` | 返回日志文件的内容(如果已设置 `logging.file.name` 或 `logging.file.path` 属性),支持使用 HTTP Range标头来检索部分日志文件的内容。 | +| `prometheus` | 以 Prometheus 服务器可以抓取的格式公开指标,需要依赖 `micrometer-registry-prometheus` | -### 结构搭建 +常用 Endpoint: -整合 SSM 三种框架进行项目开发 +- Health:监控状况 +- Metrics:运行时指标 -* 创建项目,组织项目结构,创建包 +- Loggers:日志记录 -* 创建表与实体类 -* 创建三层架构对应的模块、接口与实体类,建立关联关系 -* 数据层接口(代理自动创建实现类) - * 业务层接口 + 业务层实现类 - * 表现层类 - ![](https://gitee.com/seazean/images/raw/master/Frame/SSM-目录结构.png) +*** -*** +## 项目部署 +SpringBoot 项目开发完毕后,支持两种方式部署到服务器: +* jar 包 (官方推荐,默认) +* war 包 -### 数据准备 +**更改 pom 文件中的打包方式为 war** -* 导入坐标 pom.xml +* 修改启动类 - ```xml - - - - org.springframework - spring-context - 5.1.9.RELEASE - - - - - org.mybatis - mybatis - 3.5.3 - - - - - mysql - mysql-connector-java - 5.1.47 - - - - - org.springframework - spring-jdbc - 5.1.9.RELEASE - - - - - org.mybatis - mybatis-spring - 2.0.3 - - - - - com.alibaba - druid - 1.1.16 - - - - - com.github.pagehelper - pagehelper - 5.1.2 - - - - - - org.springframework - spring-webmvc - 5.1.9.RELEASE - - - - - com.fasterxml.jackson.core - jackson-databind - 2.9.0 - - - com.fasterxml.jackson.core - jackson-core - 2.9.0 - - - com.fasterxml.jackson.core - jackson-annotations - 2.9.0 - - - - - javax.servlet - javax.servlet-api - 3.1.0 - provided - - - - - - - junit - junit - 4.12 - - - - org.springframework - spring-test - 5.1.9.RELEASE - - + ```java + @SpringBootApplication + public class SpringbootDeployApplication extends SpringBootServletInitializer { + public static void main(String[] args) { + SpringApplication.run(SpringbootDeployApplication.class, args); + } + @Override + protected SpringApplicationBuilder configure(SpringApplicationBuilder b) { + return b.sources(SpringbootDeployApplication.class); + } + } + ``` + +* 指定打包的名称 + + ```xml + war - - - - - org.apache.tomcat.maven - tomcat7-maven-plugin - 2.1 - - 80 - / - - - + springboot + + + org.springframework.boot + spring-boot-maven-plugin + + ``` -* resources.jdbc.properties - ```properties - jdbc.driver=com.mysql.jdbc.Driver - jdbc.url=jdbc:mysql://192.168.0.137:3306/ssm_db?useSSL=false - jdbc.username=root - jdbc.password=123456 - ``` -* domain - ```java - public class User implements Serializable { - private Integer uuid; - private String userName; - private String password; - private String realName; - private Integer gender; - private Date birthday; - } - ``` -* Dao层 - ```java - public interface UserDao { - //添加用户 - public boolean save(User user); - - //修改用户 - public boolean update(User user); - - //删除用户 - public boolean delete(Integer uuid); - - //查询单个用户信息 - public User get(Integer uuid); - - //查询全部用户信息 - public List getAll(); - - /** - * 根据用户名密码查询个人信息 - * @param userName 用户名 - * @param password 密码信息 - * @return - */ - //数据层操作不要和业务层操作的名称混淆,通常数据层仅反映与数据库间的信息交换,不体现业务逻辑 - public User getByUserNameAndPassword(@Param("userName") String userName, - @Param("password") String password); - } - ``` -* service.UserService - ```java - public interface UserService { - //添加用户 - public boolean save(User user); - - //修改用户 - public boolean update(User user); - - //删除用户 - public boolean delete(Integer uuid); - - //查询单个用户信息 - public User get(Integer uuid); - - //查询全部用户信息 - public List getAll(); - - //根据用户名密码进行登录 - public User login(String userName,String password); - } - ``` +*** + + + + + +# Cloud + +## 基本介绍 + +SpringCloud 是分布式微服务的一站式解决方案,是多种微服务落地技术的集合体,俗称微服务全家桶 + +![Cloud-组件概览](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Cloud-组件概览.png) + + - service.impl.UserServiceImpl +参考文档:https://www.yuque.com/mrlinxi/pxvr4g/wcwd39 + + + + + +*** + + + + + +## 服务注册 + +### Eureka + +#### 基本介绍 + +Spring Cloud 封装了 Netflix 公司开发的 Eureka 模块来实现服务治理。Eureka 采用了 CS(Client-Server) 的设计架构,Eureka Server 是服务注册中心,系统中的其他微服务使用 Eureka 的客户端连接到 Eureka Server 并维持心跳连接 + +![Cloud-Eureka和Dubbo对比](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Cloud-Eureka和Dubbo对比.png) + +* Eureka Server 提供服务注册服务:各个微服务节点通过配置启动后,会在 EurekaServer 中进行注册,EurekaServer 中的服务注册表中将会存储所有可用服务节点的信息,并且具有可视化界面 + +* Eureka Client 通过注册中心进行访问:用于简化 Eureka Server的交互,客户端也具备一个内置的、使用轮询 (round-robin) 负载算法的负载均衡器。在应用启动后将会向 Eureka Server 发送心跳(默认周期为30秒),如果 Eureka Server 在多个心跳周期内没有接收到某个节点的心跳,将会从服务注册表中把这个服务节点移除(默认 90 秒) + + + + + +**** + + + +#### 服务端 + +服务器端主启动类增加 @EnableEurekaServer 注解,指定该模块作为 Eureka 注册中心的服务器 + +构建流程如下: + +* 主启动类 ```java - @Service //设置为bean - public class UserServiceImpl implements UserService { - @Autowired - private UserDao userDao; - - @Override - public boolean save(User user) { - return userDao.save(user); - } - - @Override - public boolean update(User user) { - return userDao.update(user); - } - - @Override - public boolean delete(Integer uuid) { - return userDao.delete(uuid); - } - - @Override - public User get(Integer uuid) { - return userDao.get(uuid); - } - - @Override - public PageInfo getAll(int page, int size) {//用分页插件 - PageHelper.startPage(page, size); - List all = userDao.getAll(); - return new PageInfo(all); - } - - @Override - public User login(String userName, String password) { - return userDao.getByUserNameAndPassword(userName, password); + @SpringBootApplication + @EnableEurekaServer // 表示当前是Eureka的服务注册中心 + public class EurekaMain7001 { + public static void main(String[] args) { + SpringApplication.run(EurekaMain7001.class, args); } } ``` -* controller +* 修改 pom 文件 - ```java - public class UserController { - - } + ```xml + 1.x: server跟client合在一起 + + org.springframework.cloud + spring-cloud-starter-eureka + + 2.x: server跟client分开 + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-server + + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-client + ``` + +* 修改 application.yml 文件 + + ```yaml + server: + port: 7001 - + eureka: + instance: + hostname: localhost # eureka服务端的实例名称 + client: + # false表示不向注册中心注册自己。 + register-with-eureka: false + # false表示自己端就是注册中心,职责就是维护服务实例,并不需要去检索服务 + fetch-registry: false + service-url: + # 设置与 Eureka Server 交互的地址查询服务和注册服务都需要依赖这个地址。 + defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/ + ``` + +* 游览器访问 http://localhost:7001 + + *** -### Mybatis +#### 客户端 -* Spring环境配置:spring-mvc.xml +##### 生产者 - ```xml - - - - - - - - ``` - -* MyBatis映射:resources.dao.UserDao.xml +服务器端主启动类需要增加 @EnableEurekaClient 注解,表示这是一个 Eureka 客户端,要注册进 EurekaServer 中 - ```xml - - - - - - - INSERT INTO user (userName,password,realName,gender,birthday) VALUES (#{userName},#{password},#{realName},#{gender},#{birthday}) - - - - - DELETE FROM user WHERE uuid = #{uuid} - - - - - UPDATE user SET userName=#{userName},password=#{password},realName=#{realName},gender=#{gender},birthday=#{birthday} WHERE uuid=#{uuid} - - - - - - - - - - - +* 主启动类:PaymentMain8001 + + ```java + @SpringBootApplication + @EnableEurekaClient + public class PaymentMain8001 { + public static void main(String[] args) { + SpringApplication.run(PaymentMain8001.class, args); + } + } ``` -* Mybatis 核心配置:resouces.applicationContext.xml +* 修改 pom 文件:添加一个 Eureka-Client 依赖 ```xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - mysql - true - - - - - - - - - - - - - - - - - + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-client + ``` - -* 业务层接口开启事务 - ```java - @Transactional(readOnly = true) - public interface UserService { - //添加用户 - @Transactional(readOnly = false) - public boolean save(User user); - @Transactional(readOnly = false) - public boolean update(User user); - @Transactional(readOnly = false) - public boolean delete(Integer uuid); - - //查询单个用户 - public User get(Integer uuid); - - //查询全部用户 - public PageInfo getAll(int page, int size); - - //根据用户名密码进行登录 - public User login(String userName, String password); - } +* 写 yml 文件 + + ```yaml + server: + port: 8001 + + eureka: + client: + # 表示将自己注册进EurekaServer默认为true + register-with-eureka: true + # 表示可以从Eureka抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡 + fetch-registry: true + service-url: + defaultZone: http://localhost:7001/eureka + instance: + instance-id: payment8001 # 只暴露服务名,不带有主机名 + prefer-ip-address: true # 访问信息有 IP 信息提示(鼠标停留在服务名称上时) ``` - +* 游览器访问 http://localhost:7001 + + *** -### Junit +##### 消费者 -* 单元测试整合 junit +* 主启动类:PaymentMain8001 ```java - @RunWith(SpringJUnit4ClassRunner.class) - @ContextConfiguration(locations = "classpath:applicationContext.xml") - public class UserServiceTest { - @Autowired - private UserService userService; - - @Test - public void testDelete(){ - User user = new User(); userService.delete(3); + @SpringBootApplication + @EnableEurekaClient + @EnableDiscoveryClient + public class PaymentMain8001 { + public static void main(String[] args) { + SpringApplication.run(PaymentMain8001.class, args); } } ``` -* test.resouces +* pom 文件同生产者 - ```java - applicationContext.xml + jdbc.properties +* 写 yml 文件 + + ```yaml + server: + port: 80 + + # 微服务名称 + spring: + application: + name: cloud-order-service + eureka: + client: + register-with-eureka: true + fetch-registry: true + service-url: + defaultZone: http://localhost:7001/eureka ``` +* 浏览器访问 http://localhost:7001 + + ![Cloud-Eureka可视化界面](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Cloud-Eureka可视化界面.png) + *** -### MVC +#### 集群构建 -* web.xml配置 +##### 服务端 - ```xml - - - - - - contextConfigLocation - classpath*:applicationContext.xml - - - - - org.springframework.web.context.ContextLoaderListener - - - - CharacterEncodingFilter - org.springframework.web.filter.CharacterEncodingFilter - - encoding - UTF-8 - - - - CharacterEncodingFilter - /* - - - - DispatcherServlet - org.springframework.web.servlet.DispatcherServlet - - - contextConfigLocation - classpath*:spring-mvc.xml - - - - DispatcherServlet - / - - - ``` +Server 端高可用集群原理:实现负载均衡和故障容错,互相注册,相互守望 -* spring-mvc.xml +![Cloud-Eureka集群原理](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Cloud-Eureka集群原理.png) - ```xml - - - ``` +多台 Eureka 服务器,每一台 Eureka 服务器需要有自己的主机名,同时各服务器需要相互注册 -* Controller层 +* Eureka1: - ```java - @RestController //@RestController = @Controller + @ResponseBody - @RequestMapping("/user") - public class UserController { - - @PostMapping - public boolean save(User user){ - System.out.println("save ..." + user); - return true; - } - - @PutMapping - public boolean update(User user){ - System.out.println("update ..." + user); - return true; - } - - @DeleteMapping("/{uuid}") - public boolean delete(@PathVariable Integer uuid){ - System.out.println("delete ..." + uuid); - return true; - } - - @GetMapping("/{uuid}") - public User get(@PathVariable Integer uuid){ - System.out.println("get ..." + uuid); - return null; - } + ```yaml + server: + port: 7001 + + eureka: + instance: + hostname: eureka7001.com + client: + register-with-eureka: false + fetch-registry: false + service-url: + # 设置与Eureka Server交互的地址查询服务和注册服务都需要依赖这个地址。 + # 单机就是自己 + # defaultZone: http://eureka7001.com:7001/eureka/ + # 集群指向其他eureka + #defaultZone: http://eureka7002.com:7002/eureka/ + # 写成这样可以直接通过可视化页面跳转到7002 + defaultZone: http://eureka7002.com:7002/ + ``` + +* Eureka2: + + ```yaml + server: + port: 7002 - @GetMapping("/{page}/{size}") - public List getAll(@PathVariable Integer page,@PathVariable Integer size){ - System.out.println("getAll ..." + page+","+size); - return null; + eureka: + instance: + hostname: eureka7002.com + client: + register-with-eureka: false + fetch-registry: false + service-url: + #写成这样可以直接通过可视化页面跳转到7001 + defaultZone: http://eureka7001.com:7001/ + ``` + +* 主启动类: + + ```java + @SpringBootApplication + @EnableEurekaServer + public class EurekaMain7002 { + public static void main(String[] args) { + SpringApplication.run(EurekaMain7002.class, args); } + } + ``` + +* 访问 http://eureka7001.com:7001 和 http://eureka7002.com:7002: + + ![Cloud-EurekaServer集群构建成功](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Cloud-EurekaServer集群构建成功.png) + +* RPC 调用:controller.OrderController + + ```java + @RestController + @Slf4j + public class OrderController { + public static final String PAYMENT_URL = "http://localhost:8001"; - @PostMapping("/login") - public User login(String userName,String password){ - System.out.println("login ..." + userName + " ," +password); - return null; + @Autowired + private RestTemplate restTemplate; + + // CommonResult 是一个公共的返回类型 + @GetMapping("/consumer/payment/get/{id}") + public CommonResult getPayment(@PathVariable("id") long id) { + // 返回对象为响应体中数据转化成的对象,基本上可以理解为JSON + return restTemplate.getForObject(PAYMENT_URL + "/payment/get/" + id, CommonResult.class); } } ``` @@ -13753,145 +16472,127 @@ public class HelloController { -**** - +*** -### 表现层 -#### 数据封装 +##### 生产者 -* 前端接收表现层返回的数据种类 - * 返回数据格式设计:状态、数据、消息 - * 返回数据状态设计,根据业务不同设计:404、500、200 +构建 PaymentMain8001 的服务集群 -* 数据格式代码: +* 主启动类 ```java - public class Result { - // 操作结果编码 - private Integer code; - // 操作数据结果 - private Object data; - // 消息 - private String message; - public Result(Integer code) { - this.code = code; - } - public Result(Integer code, Object data) { - this.code = code; - this.data = data; + @SpringBootApplication + @EnableEurekaClient + @EnableDiscoveryClient + public class PaymentMain8002 { + public static void main(String[] args) { + SpringApplication.run(PaymentMain8002.class, args); } } ``` -* 状态代码格式:状态码常量可以根据自己的业务需求设定 +* 写 yml 文件:端口修改,并且 spring.application.name 均为 cloud-payment-service - ```java - public class Code { - //操作结果编码 - public static final Integer SAVE_OK = 20011; - public static final Integer UPDATE_OK = 20021; - public static final Integer SAVE_ERR = 20010; - public static final Integer UPDATE_ERR = 20020; - - //系统错误编码 - //操作权限编码 - //校验结果编码 - } + ```yaml + server: + port: 8002 + spring: + application: + name: cloud-payment-service + + eureka: + client: + # 表示将自己注册进EurekaServer默认为true + register-with-eureka: true + # 表示可以从Eureka抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡 + fetch-registry: true + service-url: + defaultZone: http://localhost:7001/eureka + ``` + + + +*** + + + +##### 负载均衡 + +消费者端的 Controller + +```java +// public static final String PAYMENT_URL = "http://localhost:8001"; +public static final String PAYMENT_URL = "http://localhost:8002"; +``` + +由于已经建立了生产者集群,所以可以进行负载均衡的操作: + +* Controller:只修改 PAYMENT_URL 会报错,因为 CLOUD-PAYMENT-SERVICE 对应多个微服务,需要规则来判断调用哪个端口 + + ```java + public static final String PAYMENT_URL = "http://CLOUD-PAYMENT-SERVICE"; ``` -* Controller调用 +* 使用 @LoadBlanced 注解赋予 RestTemplate 负载均衡的能力,增加 config.ApplicationContextConfig 文件: ```java - @RestController - public class UserController { - @Autowired - private UserService userService; - @PostMapping - public Result save(User user){ - boolean flag = userService.save(user); - return new Result(flag ? Code.SAVE_OK:Code.SAVE_ERROR); - } - @GetMapping("/{uuid}") - public Result get(@PathVariable Integer uuid){ - User user = userService.get(uuid); - //三目运算符 - return new Result(null != user ?Code.GET_OK: Code.GET_ERROR,user); + @Configuration + public class ApplicationContextConfig { + @Bean + @LoadBalanced + public RestTemplate getRestTemplate() { + return new RestTemplate(); } } ``` -*** - - +**** -#### 自定义异常 -设定自定义异常,封装程序执行过程中出现的问题,便于表现层进行统一的异常拦截并进行处理 -* BusinessException -* SystemException +#### 服务发现 -自定义异常消息返回时需要与业务正常执行的消息按照统一的格式进行处理 +服务发现:对于注册进 Eureka 里面的微服务,可以通过服务发现来获得该服务的信息 -* 定义 BusinessException +* 主启动类增加注解 @EnableDiscoveryClient: ```java - public class BusinessException extends RuntimeException { - //自定义异常中封装对应的错误编码,用于异常处理时获取对应的操作编码 - private Integer code; - - public Integer getCode() { - return code; - } - - public void setCode(Integer code) { - this.code = code; - } - - public BusinessException(Integer code) { - this.code = code; - } - - public BusinessException(String message, Integer code) { - super(message); - this.code = code; - } - - public BusinessException(String message, Throwable cause,Integer code) { - super(message, cause); - this.code = code; - } - - public BusinessException(Throwable cause,Integer code) { - super(cause); - this.code = code; - } - - public BusinessException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace,Integer code) { - super(message, cause, enableSuppression, writableStackTrace); - this.code = code; + @SpringBootApplication + @EnableEurekaClient + @EnableDiscoveryClient + public class PaymentMain8001 { + public static void main(String[] args) { + SpringApplication.run(PaymentMain8001.class, args); } } ``` -* Controller调用 +* 修改生产者的 Controller ```java @RestController - public class UserController { + @Slf4j + public class PaymentController { @Autowired - private UserService userService; - @GetMapping("/{uuid}") - public Result get(@PathVariable Integer uuid){ - User user = userService.get(uuid); - //模拟出现异常,使用条件控制,便于测试结果 - if (uuid == 10 ) throw new BusinessException("查询出错啦,请重试!",Code.GET_ERROR); - return new Result(null != user ?Code.GET_OK: Code.GET_ERROR,user); + private DiscoveryClient discoveryClient; + + @GetMapping(value = "/payment/discovery") + public Object discovery() { + List services = discoveryClient.getServices(); + for (String service : services) { + log.info("**** element:" + service); + } + + List instances = discoveryClient.getInstances("PAYMENT-SERVICE"); + for (ServiceInstance instance : instances) { + log.info(instance.getServiceId() + "\t" + instance.getHost() + "\t" + instance.getPort()); + } + return this.discoveryClient; } } ``` @@ -13902,84 +16603,78 @@ public class HelloController { -#### 兼容异常 +#### 自我保护 -java.controller.interceptor +保护模式用于客户端和 EurekaServer 之间存在网络分区场景下的保护,一旦进入保护模式 EurekaServer 将会尝试保护其服务注册表中的信息,不在删除服务注册表中的数据,属于 CAP 里面的 AP 思想(可用性和分区容错性) -```java -@Component -@ControllerAdvice -public class ProjectExceptionAdivce { - @ExceptionHandler(BusinessException.class) - @ResponseBody - //对出现异常的情况进行拦截,并将其处理成统一的页面数据结果格式 - public Result doBusinessException(BusinessException e){ - return new Result(e.getCode(),e.getMessage()); - } -} -``` +![Cloud-Eureka自我保护机制](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Cloud-Eureka自我保护机制.png) +如果一定时间内丢失大量该微服务的实例,这时 Eureka 就会开启自我保护机制,不会剔除该服务。 因为这个现象可能是因为网络暂时不通,出现了 Eureka 的假死、拥堵、卡顿,客户端恢复后还能正常发送心跳 +禁止自我保护: -**** +* Server: + ```yaml + eureka: + server: + # 关闭自我保护机制,不可用的服务直接删除 + enable-self-preservation: false + eviction-interval-timer-in-ms: 2000 + ``` +* Client: -## 注解 + ```yaml + eureka: + instance: + # Eureka客户端向服务端发送心跳的时间间隔默认30秒 + lease-renewal-interval-in-seconds: 1 + # Eureka服务端在收到最后一次心跳后,90s没有收到心跳,剔除服务 + lease-expiration-duration-in-seconds: 2 + ``` -### 结构搭建 -项目整体目录结构 -![](https://gitee.com/seazean/images/raw/master/Frame/SSM-annotation.png) +**** -*** +### Consul -### UserDao.xml +#### 基本介绍 -注解:@Param +Consul 是开源的分布式服务发现和配置管理系统,采用 Go 语言开发,官网:https://developer.hashicorp.com/consul -作用:当 SQL 语句需要多个(大于1)参数时,用来指定参数的对应规则 +* 提供了微服务系统中心的服务治理,配置中心,控制总线等功能 +* 基于 Raft 协议,支持健康检查,同时支持 HTTP 和 DNS 协议支持跨数据中心的 WAN 集群 +* 提供图形界面 -* 注解替代 UserDao 映射配置文件:dao.UserDao +下载 Consul 后,运行指令:`consul -version` + +```bash +D:\Program Files\Java>consul -version +Consul v1.15.1 +Revision 7c04b6a0 +Build Date 2023-03-07T20:35:33Z +Protocol 2 spoken by default, understands 2 to 3 (.....) +``` + +启动命令: + +```bash +consul agent -dev +``` + +访问浏览器:http://localhost:8500/ - ```java - public interface UserDao { - //添加用户 - @Insert("insert into user(userName,password,realName,gender,birthday)values(#{userName},#{password},#{realName},#{gender},#{birthday})") - public boolean save(User user); - - //修改用户 - @Update("update user set userName=#{userName},password=#{password},realName=#{realName},gender=#{gender},birthday=#{birthday} where uuid=#{uuid}") - public boolean update(User user); - - // 删除用户 - @Delete("delete from user where uuid = #{uuid}") - public boolean delete(Integer uuid); - - //查询单个用户信息 - @Select("select * from user where uuid = #{uuid}") - public User get(Integer uuid); - - //查询全部用户信息 - @Select("select * from user") - public List getAll(); - - - //根据用户名密码查询个人信息 - @Select("select * from user where userName=#{userName} and password=#{password}") - //注意:数据层操作不要和业务层操作的名称混淆,通常数据层仅反映与数据库间的信息交换,不体现业务逻辑 - public User getByUserNameAndPassword(@Param("userName") String userName, @Param("password") String password); - } - ``` - + +中文文档:https://www.springcloud.cc/spring-cloud-consul.html @@ -13987,371 +16682,365 @@ public class ProjectExceptionAdivce { -### applicationContext.xml +#### 基本使用 -![](https://gitee.com/seazean/images/raw/master/Frame/SSM-IoC注解整合MyBatis图解.png) +无需 Server 端代码的编写 -* JdbcConfig +生产者: - ```java - public class JdbcConfig { - //使用注入的形式,读取properties文件中的属性值,等同于 - @Value("${jdbc.driver}") - private String driver; - @Value("${jdbc.url}") - private String url; - @Value("${jdbc.username}") - private String userName; - @Value("${jdbc.password}") - private String password; +* 引入 pom 依赖: + + ```xml + + org.springframework.cloud + spring-cloud-starter-consul-discovery + + ``` + +* application.yml: + + ```yaml + ###consul 服务端口号 + server: + port: 8006 - //定义dataSource的bean,等同于 - @Bean("dataSource") - public DataSource getDataSource(){ - //创建对象 - DruidDataSource ds = new DruidDataSource(); - //,等同于set属性注入 - ds.setDriverClassName(driver); - ds.setUrl(url); - ds.setUsername(userName); - ds.setPassword(password); - return ds; - } - } + spring: + application: + name: consul-provider-payment + ####consul注册中心地址 + cloud: + consul: + host: localhost + port: 8500 + discovery: + service-name: ${spring.application.name} ``` -* MybatisConfig +* 主启动类: ```java - public class MyBatisConfig { - //定义MyBatis的核心连接工厂bean,等同于 - @Bean - //自动装配的形式加载dataSource,为set注入提供数据,dataSource来源于JdbcConfig中 - public SqlSessionFactoryBean getSqlSessionFactoryBean(@Autowired DataSource dataSource,@Autowired Interceptor interceptor){ - SqlSessionFactoryBean ssfb = new SqlSessionFactoryBean(); - //等同于 - ssfb.setTypeAliasesPackage("domain"); - //等同于 - ssfb.setDataSource(dataSource); - //可以把Interceptor写在这里 - ssfb.setPlugins(interceptor); - return ssfb; - } - - //定义MyBatis的映射扫描,等同于 - @Bean - public MapperScannerConfigurer getMapperScannerConfigurer(){ - MapperScannerConfigurer msc = new MapperScannerConfigurer(); - //等同于 - msc.setBasePackage("dao"); - return msc; - } - - @Bean("pageInterceptor") - public Interceptor getPageInterceptor(){ - Interceptor interceptor = new PageInterceptor(); - Properties properties = new Properties(); - properties.setProperty("helperDialect","mysql"); - properties.setProperty("reasonable","true"); - //等同于 - interceptor.setProperties(properties); - return interceptor; + @SpringBootApplication + @EnableDiscoveryClient + public class PaymentMain8006 { + public static void main(String[] args) { + SpringApplication.run(PaymentMain8006.class, args); } } ``` -* SpringConfig.xml +消费者: + +* application.yml: + + ```yaml + ###consul服务端口号 + server: + port: 80 + + spring: + application: + name: cloud-consumer-order + ####consul注册中心地址 + cloud: + consul: + host: localhost + port: 8500 + discovery: + #hostname: 127.0.0.1 + service-name: ${spring.application.name} + ``` + +* 主启动类:同生产者 + +* 配置类: ```java @Configuration - @ComponentScan(value = {"config","dao","service","system"},excludeFilters = - @ComponentScan.Filter(type= FilterType.ANNOTATION,classes = {Controller.class})) - //等同于 - @PropertySource("classpath:jdbc.properties") - //等同于,bean的名称默认取transactionManager - @EnableTransactionManagement - @Import({MyBatisConfig.class,JdbcConfig.class}) - public class SpringConfig { - //等同于 - @Bean("transactionManager") - //等同于 - public DataSourceTransactionManager getTxManager(@Autowired DataSource dataSource){ - DataSourceTransactionManager tm = new DataSourceTransactionManager(); - //等同于 - tm.setDataSource(dataSource); - return tm; + public class ApplicationContextConfig { + @Bean + @LoadBalanced + public RestTemplate getRestTemplate() { + return new RestTemplate(); } } ``` + +* 业务类 Controller: + + ```java + @RestController + @Slf4j + public class OrderConsulController { + public static final String INVOKE_URL = "http://cloud-provider-pament"; + @Resource + private RestTemplate restTemplate; + @GetMapping("/consumer/payment/consul") + public String paymentInfo() { + return restTemplate.getForObject(INVOKE_URL, String.class); + } + } + ``` -*** -### spring-mvc.xml +**** -* 注解替代 spring-mvc.xml:SpringMvcConfig - ```java - @Configuration - //等同于 - @ComponentScan("controller") - //等同于,还不完全相同 - @EnableWebMvc - public class SpringMvcConfig { - //注解配置通用放行静态资源的格式 - @Override - public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) { - configurer.enable(); - } - } - ``` -* EnableWebMvc - 1. 支持 ConversionService 的配置,可以方便配置自定义类型转换器 - 2. 支持 @NumberFormat 注解格式化数字类型 - 3. 支持 @DateTimeFormat 注解格式化日期数据,日期包括 Date、Calendar - 4. 支持 @Valid 的参数校验(需要导入 JSR-303 规范) - 5. 配合第三方 jar 包和 SpringMVC 提供的注解读写 XML 和 JSON 格式数据 +## 服务调用 +### Ribbon -*** +#### 基本介绍 +SpringCloud Ribbon 是基于 Netflix Ribbon 实现的一套负载均衡工具,提供客户端的软件负载均衡算法和服务调用,Ribbon 客户端组件提供一系列完善的配置项如连接超时,重试等 +官网: https://github.com/Netflix/ribbon/wiki/Getting-Started (已进入维护模式,未来替换为 Load Banlancer) -### web.xml +负载均衡 Load Balance (LB) 就是将用户的请求平摊的分配到多个服务上,从而达到系统的 HA(高可用) -* 注解替代 web.xml:ServletContainersInitConfig +**常见的负载均衡算法:** - ```java - public class ServletContainersInitConfig extends AbstractDispatcherServletInitializer { - - //创建Servlet容器,加载SpringMVC配置类中的信息,并加载成WEB专用的ApplicationContext对象,该对象放入了ServletContext范围,后期在整个WEB容器中可以随时获取调用 - @Override - protected WebApplicationContext createServletApplicationContext() { - AnnotationConfigWebApplicationContext ctx = new AnnotationConfigWebApplicationContext(); - ctx.register(SpringMvcConfig.class); - return ctx; - } - - //注解配置映射地址方式,服务于SpringMVC的核心控制器DispatcherServlet - @Override - protected String[] getServletMappings() { - return new String[]{"/"}; - } - //启动服务器时,通过监听器加载spring运行环境 - @Override - protected WebApplicationContext createRootApplicationContext() { - AnnotationConfigWebApplicationContext ctx = new AnnotationConfigWebApplicationContext(); - ctx.register(SpringConfig.class); - return ctx; - } - - //乱码处理作为过滤器,在servlet容器启动时进行配置,相关内容参看Servlet零配置相关课程 - @Override - public void onStartup(ServletContext servletContext) throws ServletException { - //触发父类的onStartup - super.onStartup(servletContext); - //1.创建字符集过滤器对象 - CharacterEncodingFilter cef = new CharacterEncodingFilter(); - //2.设置使用的字符集 - cef.setEncoding("UTF-8"); - //3.添加到容器(它不是ioc容器,而是ServletContainer) - FilterRegistration.Dynamic registration = servletContext.addFilter( - "characterEncodingFilter", cef); - //4.添加映射 - registration.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST, DispatcherType.FORWARD, DispatcherType.INCLUDE), false, "/*"); - } - } - ``` +- 轮询:为请求选择健康池中的第一个后端服务器,然后按顺序往后依次选择 -* WebApplicationContext,生成 Spring 核心容器(主容器/父容器/根容器) +- 最小连接:优先选择连接数最少,即压力最小的后端服务器,在会话较长的情况下可以采取这种方式 - * 父容器:Spring 环境加载后形成的容器,包含 Spring 环境下的所有的 bean - * 子容器:当前 mvc 环境加载后形成的容器,不包含 Spring 环境下的 bean - * 子容器可以访问父容器中的资源,父容器不可以访问子容器的资源 +- 散列:根据请求源的 IP 的散列(hash)来选择要转发的服务器,可以一定程度上保证特定用户能连接到相同的服务器,如果应用需要处理状态而要求用户能连接到和之前相同的服务器,可以采取这种方式 +Ribbon 本地负载均衡客户端与 Nginx 服务端负载均衡区别: +- Nginx 是服务器负载均衡,客户端所有请求都会交给 Nginx,然后由 Nginx 实现转发请求,即负载均衡是由服务端实现的 +- Ribbon 本地负载均衡,在调用微服务接口时会在注册中心上获取注册信息服务列表,然后缓存到 JVM 本地,从而在本地实现 RPC 远程服务调用技术 +集中式 LB 和进程内 LB 的对比: +* 集中式 LB:在服务的消费方和提供方之间使用独立的 LB 设施(如 Nginx),由该设施把访问请求通过某种策略转发至服务的提供方 +* 进程内 LB:将 LB 逻辑集成到消费方,消费方从服务注册中心获知有哪些服务可用,然后从中选择出一个服务器,Ribbon 属于该类 -**** +*** +#### 工作流程 +Ribbon 是一个软负载均衡的客户端组件 -# Boot +![Cloud-Ribbon架构原理](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Cloud-Ribbon架构原理.png) -## 基本介绍 +- 第一步先选择 EurekaServer,优先选择在同一个区域内负载较少的 Server +- 第二步根据用户指定的策略,再从 Server 取到的服务注册列表中选择一个地址 -### Boot介绍 -SpringBoot 提供了一种快速使用 Spring 的方式,基于约定优于配置的思想,可以让开发人员不必在配置与逻辑业务之间进行思维的切换,全身心的投入到逻辑业务的代码编写中,从而大大提高了开发的效率 -SpringBoot 功能: +*** -* 自动配置: - Spring Boot 的自动配置是一个运行时(更准确地说,是应用程序启动时)的过程,考虑了众多因素选择使用哪个配置,该过程是SpringBoot 自动完成的 -* 起步依赖,起步依赖本质上是一个 Maven 项目对象模型(Project Object Model,POM),定义了对其他库的传递依赖,这些东西加在一起即支持某项功能。简单的说,起步依赖就是将具备某种功能的坐标打包到一起,并提供一些默认的功能 +#### 核心组件 -* 辅助功能,提供了一些大型项目中常见的非功能性特性,如内嵌 web 服务器、安全、指标,健康检测、外部配置等 +Ribbon 核心组件 IRule 接口,主要实现类: +- RoundRobinRule:轮询 +- RandomRule:随机 +- RetryRule:先按照 RoundRobinRule 的策略获取服务,如果获取服务失败则在指定时间内会进行重试 +- WeightedResponseTimeRule:对 RoundRobinRule 的扩展,响应速度越快的实例选择权重越大,越容易被选择 +- BestAvailableRule:会先过滤掉由于多次访问故障而处于断路器跳闸状态的服务,然后选择一个并发量最小的服务 +- AvailabilityFilteringRule:先过滤掉故障实例,再选择并发较小的实例 +- ZoneAvoidanceRule:默认规则,复合判断 Server 所在区域的性能和 Server 的可用性选择服务器 +![Cloud-IRule类图](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Cloud-IRule类图.png) -参考视频:https://www.bilibili.com/video/BV19K4y1L7MT +注意:官方文档明确给出了警告,自定义负载均衡配置类不能放在 @ComponentScan 所扫描的当前包下以及子包下 +更换负载均衡算法方式: +* 自定义负载均衡配置类 MySelfRule: + ```java + @Configuration + public class MySelfRule { + @Bean + public IRule myRule() { + return new RandomRule();//定义为随机负载均衡算法 + } + } + ``` -*** +* 主启动类添加 @RibbonCilent 注解 + ```java + @SpringBootApplication + @EnableEurekaClient + // 指明访问的服务CLOUD-PAYMENT-SERVICE,以及指定负载均衡策略 + @RibbonClient(name = "CLOUD-PAYMENT-SERVICE", configuration= MySelfRule.class) + public class OrderMain80 { + public static void main(String[] args) { + SpringApplication.run(OrderMain80.class, args); + } + } + ``` + -### 构建工程 -普通构建: -1. 创建 Maven 项目 +**** -2. 导入 SpringBoot 起步依赖 - ```xml - - - org.springframework.boot - spring-boot-starter-parent - 2.1.8.RELEASE - - - - - - org.springframework.boot - spring-boot-starter-web - - - ``` -3. 定义 Controller - ```java - @RestController - public class HelloController { - @RequestMapping("/hello") - public String hello(){ - return " hello Spring Boot !"; - } - } - ``` -4. 编写引导类 +### OpenFeign + +#### 基本介绍 - ```java - // 引导类,SpringBoot项目的入口 - @SpringBootApplication - public class HelloApplication { - public static void main(String[] args) { - SpringApplication.run(HelloApplication.class, args); - } - } - ``` +Feign 是一个声明式 WebService 客户端,能让编写 Web 客户端更加简单,只要创建一个接口并添加注解 @Feign 即可,可以与 Eureka 和 Ribbon 组合使用支持负载均衡,所以一般**用在消费者端** -快速构建: +OpenFeign 在 Feign 的基础上支持了 SpringMVC 注解,并且 @FeignClient 注解可以解析 @RequestMapping 注解下的接口,并通过动态代理的方式产生实现类,在实现类中做负载均衡和服务调用 -![](https://gitee.com/seazean/images/raw/master/Frame/SpringBoot-IDEA构建工程.png) +优点:利用 RestTemplate 对 HTTP 请求的封装处理,形成了一套模版化的调用方法。但是对服务依赖的调用可能不止一处,往往一个接口会被多处调用,所以一个微服务接口上面标注一个 @Feign 注解,就可以完成包装依赖服务的调用 +**** -*** +#### 基本使用 +@FeignClient("provider name") 注解使用规则: +* 声明的方法签名必须和 provider 微服务中的 controller 中的方法签名一致 +* 如果需要传递参数,那么 `@RequestParam` 、`@RequestBody` 、`@PathVariable` 也需要加上 +改造消费者服务 -## 自动装配 +* 引入 pom 依赖:OpenFeign 整合了 Ribbon,具有负载均衡的功能 -### 依赖管理 + ```xml + + org.springframework.cloud + spring-cloud-starter-openfeign + + ``` -在 spring-boot-starter-parent 中定义了各种技术的版本信息,组合了一套最优搭配的技术版本。在各种 starter 中,定义了完成该功能需要的坐标合集,其中大部分版本信息来自于父工程。工程继承 parent,引入 starter 后,通过依赖传递,就可以简单方便获得需要的 jar 包,并且不会存在版本冲突,自动版本仲裁机制 +* application.yml:不将其注册到 Eureka 作为微服务 + ```yaml + server: + port: 80 + + eureka: + client: + # 表示不将其注入Eureka作为微服务,不作为Eureak客户端了,而是作为Feign客户端 + register-with-eureka: false + service-url: + # 集群版 + defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka + ``` +* 主启动类:开启 Feign -*** + ```java + @SpringBootApplication + @EnableFeignClients //不作为Eureak客户端了,而是作为Feign客户端 + public class OrderOpenFeignMain80 { + public static void main(String[] args) { + SpringApplication.run(OrderOpenFeignMain80.class, args); + } + + } + ``` +* 新建 Service 接口:PaymentFeignService 接口和 @FeignClient 注解,完成 Feign 的包装调用 + ```java + @Component + @FeignClient(value = "CLOUD-PAYMENT-SERVICE") // 作为一个Feign功能绑定的的接口 + public interface PaymentFeignService { + @GetMapping(value = "/payment/get/{id}") + public CommonResult getPaymentById(@PathVariable("id") long id); + + @GetMapping("/payment/feign/timeout") + public String paymentFeignTimeout(); + } + ``` -### 底层注解 +* Controller: -#### SpringBoot + ```java + @RestController + @Slf4j + public class OrderFeignController { + @Autowired + private PaymentFeignService paymentFeignService; + + @GetMapping("/consumer/payment/get/{id}") + public CommonResult getPayment(@PathVariable("id") long id) { + // 返回对象为响应体中数据转化成的对象,基本上可以理解为JSON + return paymentFeignService.getPaymentById(id); + } + + @GetMapping("/consumer/payment/feign/timeout") + public String paymentFeignTimeout() { + // openfeign-ribbon,客户端一般默认等待1s + return paymentFeignService.paymentFeignTimeout(); + } + } + + ``` -@SpringBootApplication:启动注解,实现 SpringBoot 的自动部署 -* 参数 scanBasePackages:可以指定扫描范围 -* 默认扫描当前引导类所在包及其子包 -假如所在包为 com.example.springbootenable,扫描配置包 com.example.config 的信息,三种解决办法: +*** -1. 使用 @ComponentScan 扫描 com.example.config 包 -2. 使用 @Import 注解,加载类,这些类都会被 Spring 创建并放入 ioc 容器,默认组件的名字就是**全类名** -3. 对 @Import 注解进行封装 +#### 超时问题 -```java -//1.@ComponentScan("com.example.config") -//2.@Import(UserConfig.class) -@EnableUser -@SpringBootApplication -public class SpringbootEnableApplication { +Feign 默认是支持 Ribbon,Feign 客户端的负载均衡和超时控制都由 Ribbon 控制 - public static void main(String[] args) { - ConfigurableApplicationContext context = SpringApplication.run(SpringbootEnableApplication.class, args); - //获取Bean - Object user = context.getBean("user"); - System.out.println(user); +设置 Feign 客户端的超时等待时间: - } -} +```yaml +ribbon: + #指的是建立连接后从服务器读取到可用资源所用的时间 + ReadTimeout: 5000 + #指的是建立连接所用的时间,适用于网络状况正常的情况下,两端连接所用的时间 + ConnectTimeout: 5000 ``` -UserConfig: +演示超时现象:OpenFeign 默认等待时间为 1 秒钟,超过后会报错 -```java -@Configuration -public class UserConfig { - @Bean - public User user() { - return new User(); - } -} -``` +* 服务提供方 Controller: -EnableUser 注解类: + ```java + @GetMapping("/payment/feign/timeout") + public String paymentFeignTimeout() { + try { + TimeUnit.SECONDS.sleep(3); + } catch (InterruptedException e) { + e.printStackTrace(); + } + return serverPort; + } + ``` -```java -@Target(ElementType.TYPE) -@Retention(RetentionPolicy.RUNTIME) -@Documented -@Import(UserConfig.class)//@Import注解实现Bean的动态加载 -public @interface EnableUser { -} -``` +* 消费者 PaymentFeignService 和 OrderFeignController 参考上一小节代码 +* 测试报错: + ![Cloud-OpenFeign超时错误](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Cloud-OpenFeign超时错误.png)!](C:\Users\Seazean\Desktop\123\Cloud-OpenFeign超时错误.png) @@ -14359,263 +17048,300 @@ public @interface EnableUser { -#### Configuration +#### 日志级别 -@Configuration:设置当前类为 SpringBoot 的配置类 +Feign 提供了日志打印功能,可以通过配置来调整日志级别,从而了解 Feign 中 HTTP 请求的细节 -* proxyBeanMethods = true:Full 全模式,每个 @Bean 方法被调用多少次返回的组件都是单实例的,默认值,类组件之间**有依赖关系**,方法会被调用得到之前单实例组件 -* proxyBeanMethods = false:Lite 轻量级模式,每个 @Bean 方法被调用多少次返回的组件都是新创建的,类组件之间**无依赖关系**用 Lite 模式加速容器启动过程 +| NONE | 默认的,不显示任何日志 | +| ------- | --------------------------------------------------------- | +| BASIC | 仅记录请求方法、URL、响应状态码及执行时间 | +| HEADERS | 除了 BASIC 中定义的信息之外,还有请求和响应的头信息 | +| FULL | 除了 HEADERS 中定义的信息外,还有请求和响应的正文及元数据 | -```java -@Configuration(proxyBeanMethods = true) -public class MyConfig { - @Bean //给容器中添加组件。以方法名作为组件的 id。返回类型就是组件类型。返回的值,就是组件在容器中的实例 - public User user(){ - User user = new User("zhangsan", 18); - return user; - } -} -``` +配置在消费者端 +* 新建 config.FeignConfig 文件:配置日志 Bean + ```java + @Configuration + public class FeignConfig { + @Bean + Logger.Level feignLoggerLevel() { + return Logger.Level.FULL; + } + } + ``` -*** +* application.yml: + ```yaml + logging: + level: + # feign 日志以什么级别监控哪个接口 + com.atguigu.springcloud.service.PaymentFeignService: debug + ``` +* Debug 后查看后台日志 -*** +**** -#### Condition -##### 条件注解 -Condition 是 Spring4.0 后引入的条件化配置接口,通过实现 Condition 接口可以完成有条件的加载相应的 Bean -注解:@Conditional -作用:条件装配,满足 Conditional 指定的条件则进行组件注入,加上方法或者类上,作用范围不同 +## 服务熔断 -使用:@Conditional 配合 Condition 的实现类(ClassCondition)进行使用 +### Hystrix -ConditionContext 类API: +#### 基本介绍 -| 方法 | 说明 | -| --------------------------------------------------- | ----------------------------- | -| ConfigurableListableBeanFactory getBeanFactory() | 获取到 ioc 使用的 beanfactory | -| ClassLoader getClassLoader() | 获取类加载器 | -| Environment getEnvironment() | 获取当前环境信息 | -| BeanDefinitionRegistry getRegistry() | 获取到bean定义的注册类 | +Hystrix 是一个用于处理分布式系统的延迟和容错的开源库,在分布式系统里,许多依赖会出现调用失败,比如超时、异常等,Hystrix 能够保证在一个依赖出问题的情况下,不会导致整体服务失败,避免级联故障,以提高分布式系统的弹性 -* ClassCondition +断路器本身是一种开关装置,当某个服务单元发生故障之后,通过断路器的故障监控(类似熔断保险丝),向调用方返回一个符合预期的、可处理的备选响应(FallBack),而不是长时间的等待或者抛出调用方无法处理的异常,这样就保证了服务调用方的线程不会被长时间地占用,避免了故障在分布式系统中的蔓延,乃至雪崩 - ```java - public class ClassCondition implements Condition { - /** - * context 上下文对象。用于获取环境,IOC容器,ClassLoader对象 - * metadata 注解元对象。 可以用于获取注解定义的属性值 - */ - @Override - public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { - - //1.需求: 导入Jedis坐标后创建Bean - //思路:判断redis.clients.jedis.Jedis.class文件是否存在 - boolean flag = true; - try { - Class cls = Class.forName("redis.clients.jedis.Jedis"); - } catch (ClassNotFoundException e) { - flag = false; - } - return flag; - } - } - ``` +* 服务降级 Fallback:系统不可用时需要一个兜底的解决方案或备选响应,向调用方返回一个可处理的响应 +* 服务熔断 Break:达到最大服务访问后,直接拒绝访问 +* 服务限流 Flowlimit:高并发操作时严禁所有请求一次性过来拥挤,一秒钟 N 个,有序排队进行 -* UserConfig - ```java - @Configuration - public class UserConfig { - @Bean - @Conditional(ClassCondition.class) - public User user(){ - return new User(); - } - } - ``` -* 启动类: +官方文档:https://github.com/Netflix/Hystrix/wiki/How-To-Use - ```java - @SpringBootApplication - public class SpringbootConditionApplication { - public static void main(String[] args) { - //启动SpringBoot应用,返回Spring的IOC容器 - ConfigurableApplicationContext context = SpringApplication.run(SpringbootConditionApplication.class, args); - - Object user = context.getBean("user"); - System.out.println(user); - } - } - ``` -*** +**** -##### 自定义注解 -将类的判断定义为动态的,判断哪个字节码文件存在可以动态指定 +#### 服务降级 -* 自定义条件注解类 +##### 案例构建 - ```java - @Target({ElementType.TYPE, ElementType.METHOD}) - @Retention(RetentionPolicy.RUNTIME) - @Documented - @Conditional(ClassCondition.class) - public @interface ConditionOnClass { - String[] value(); - } +生产者模块: + +* 引入 pom 依赖: + + ```xml + + org.springframework.cloud + spring-cloud-starter-netflix-hystrix + ``` -* ClassCondition +* 主启动类:开启 Feign ```java - public class ClassCondition implements Condition { - @Override - public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata metadata) { - - //需求:通过注解属性值value指定坐标后创建bean - Map map = metadata.getAnnotationAttributes - (ConditionOnClass.class.getName()); - //map = {value={属性值}} - //获取所有的 - String[] value = (String[]) map.get("value"); - - boolean flag = true; - try { - for (String className : value) { - Class cls = Class.forName(className); - } - } catch (Exception e) { - flag = false; - } - return flag; + @SpringBootApplication + @EnableEurekaClient + @EnableCircuitBreaker // 降级使用 + public class PaymentHystrixMain8001 { + public static void main(String[] args) { + SpringApplication.run(PaymentHystrixMain8001.class, args); } } ``` -* UserConfig +* Controller: ```java - @Configuration - public class UserConfig { - @Bean - @ConditionOnClass("com.alibaba.fastjson.JSON")//JSON加载了才注册 User 到容器 - public User user(){ - return new User(); + @RestController + @Slf4j + public class PaymentController { + @Resource + private PaymentService paymentService; + @Value("${server.port}") + private String serverPort; + + // 正常访问 + @GetMapping("/payment/hystrix/ok/{id}") + private String paymentInfo_Ok(@PathVariable("id") Integer id) { + return paymentService.paymentInfo_Ok(id); + } + // 超时 + @GetMapping("/payment/hystrix/timeout/{id}") + private String paymentInfo_Timeout(@PathVariable("id") Integer id) { + // service 层有 Thread.sleep() 操作,保证超时 + return paymentService.paymentInfo_Timeout(id); } } ``` -* 测试 User 对象的创建 - - - -*** - +* Service: + ```java + @Service + public class PaymentService { + public String paymentInfo_Ok(Integer id) { + return "线程池: " + Thread.currentThread().getName() + "paymentInfo_OK, id: " + id"; + } + + public String paymentInfo_Timeout(Integer id) { + int timeNumber = 3; + try { + TimeUnit.SECONDS.sleep(timeNumber); + } catch (InterruptedException e) { + e.printStackTrace(); + } + return "线程池: " + Thread.currentThread().getName() + " payment_Timeout, id: " + id; + } + } + ``` -##### 常用注解 +* jmeter 压测两个接口,发现接口 paymentInfo_Ok 也变的卡顿 -SpringBoot 提供的常用条件注解: +消费者模块: -@ConditionalOnProperty:判断**配置文件**中是否有对应属性和值才初始化 Bean +* Service 接口: -```java -@Configuration -public class UserConfig { - @Bean - @ConditionalOnProperty(name = "it", havingValue = "seazean") - public User user() { - return new User(); - } -} -``` + ```java + @Component + @FeignClient(value = "CLOUD-PROVIDER-HYSTRIX-PAYMENT") + public interface PaymentHystrixService { + @GetMapping("/payment/hystrix/ok/{id}") + public String paymentInfo_Ok(@PathVariable("id") Integer id); + + @GetMapping("/payment/hystrix/timeout/{id}") + public String paymentInfo_Timeout(@PathVariable("id") Integer id); + } + ``` -```properties -it=seazean -``` +* Controller: -@ConditionalOnClass:判断环境中是否有对应类文件才初始化 Bean + ```java + @RestController + @Slf4j + public class OrderHystirxController { + @Resource + PaymentHystrixService paymentHystrixService; + + @GetMapping("/consumer/payment/hystrix/ok/{id}") + public String paymentInfo_Ok(@PathVariable("id") Integer id) { + return paymentHystrixService.paymentInfo_Ok(id); + } + + @GetMapping("/consumer/payment/hystrix/timeout/{id}") + public String paymentInfo_Timeout(@PathVariable("id") Integer id) { + return paymentHystrixService.paymentInfo_Timeout(id); + } + } + ``` -@ConditionalOnMissingClass:判断环境中是否有对应类文件才初始化 Bean +* 测试:使用的是 Feign 作为客户端,默认 1s 没有得到响应就会报超时错误,进行并发压测 -@ConditionalOnMissingBean:判断环境中没有对应Bean才初始化 Bean +* 解决: + * 超时导致服务器变慢(转圈):超时不再等待 + * 出错(宕机或程序运行出错):出错要有兜底 -***** +**** -#### ImportRes -使用 bean.xml 文件生成配置 bean,如果需要继续复用 bean.xml,@ImportResource 导入配置文件即可 +##### 降级操作 + +生产者端和消费者端都可以进行服务降级,使用 @HystrixCommand 注解指定降级后的方法 + +生产者端:主启动类添加新注解 @EnableCircuitBreaker,业务类(Service)方法进行如下修改, ```java -@ImportResource("classpath:beans.xml") -public class MyConfig { - //... +// 模拟拥堵的情况 +@HystrixCommand(fallbackMethod = "paymentInfo_TimeoutHandler", commandProperties = { + //规定这个线程的超时时间是3s,3s后就由fallbackMethod指定的方法“兜底”(服务降级) + @HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds", value = "3000") +}) +public String paymentInfo_Timeout(Integer id) { + // 超时或者出错 +} + +public String paymentInfo_TimeoutHandler(Integer id) { + return "线程池:" + Thread.currentThread().getName() + " paymentInfo_TimeoutHandler, id: " + id"; } ``` -```xml - - - - - +服务降级的方法和业务处理的方法混杂在了一块,耦合度很高,并且每个方法配置一个服务降级方法 - - - - -``` +- 在业务类Controller上加 @DefaultProperties(defaultFallback = "method_name") 注解 +- 在需要服务降级的方法上标注 @HystrixCommand 注解,如果 @HystrixCommand 里没有指明 fallbackMethod,就默认使用 @DefaultProperties 中指明的降级服务 +```java +@RestController +@Slf4j +@DefaultProperties(defaultFallback = "payment_Global_FallbackMethod") +public class OrderHystrixController { + @Resource + PaymentHystrixService paymentHystrixService; + + @GetMapping("/consumer/payment/hystrix/ok/{id}") + public String paymentInfo_Ok(@PathVariable("id") Integer id) { + return paymentHystrixService.paymentInfo_OK(id); + } + @HystrixCommand + public String paymentInfo_Timeout(@PathVariable("id") Integer id) { + return paymentHystrixService.paymentInfo_Timeout(id); + } -**** + public String paymentTimeOutFallbackMethod(@PathVariable("id") Integer id) { + return "fallback"; + } + // 下面是全局fallback方法 + public String payment_Global_FallbackMethod() { + return "Global fallback"; + } +} +``` +客户端调用服务端,遇到服务端宕机或关闭等极端情况,为 Feign 客户端定义的接口添加一个服务降级实现类即可实现解耦 -#### Properties +* application.yml:配置文件中开启了 Hystrix -@ConfigurationProperties:读取到 properties 文件中的内容,并且封装到 JavaBean 中 + ```yaml + # 用于服务降级 在注解 @FeignClient中添加fallbackFactory属性值 + feign: + hystrix: + enabled: true #在Feign中开启Hystrix + ``` -配置文件: +* Service:统一为接口里面的方法进行异常处理,服务异常找 PaymentFallbackService,来统一进行服务降级的处理 -```properties -mycar.brand=BYD -mycar.price=100000 -``` + ```java + @Component + @FeignClient(value = "PROVIDER-HYSTRIX-PAYMENT", fallback = PaymentFallbackService.class) + public interface PaymentHystrixService { + + @GetMapping("/payment/hystrix/ok/{id}") + public String paymentInfo_OK(@PathVariable("id") Integer id); + + @GetMapping("/payment/hystrix/timeout/{id}") + public String paymentInfo_Timeout(@PathVariable("id") Integer id); + } + ``` -JavaBean 类: +* PaymentFallbackService: -```java -@Component //导入到容器内 -@ConfigurationProperties(prefix = "mycar")//代表配置文件的前缀 -public class Car { - private String brand; - private Integer price; -} -``` + ```java + @Component + public class PaymentFallbackService implements PaymentHystrixService { + @Override + public String paymentInfo_OK(Integer id) { + return "------PaymentFallbackService-paymentInfo_Ok, fallback"; + } + + @Override + public String paymentInfo_Timeout(Integer id) { + return "------PaymentFallbackService-paymentInfo_Timeout, fallback"; + } + } + ``` @@ -14623,125 +17349,103 @@ public class Car { -### 源码解析 +#### 服务熔断 -#### 启动流程 +##### 熔断类型 -应用启动: +熔断机制是应对雪崩效应的一种微服务链路保护机制,当扇出链路的某个微服务出错不可用或者响应时间太长时,会进行服务的降级,进而熔断该节点微服务的调用,快速返回错误的响应信息 -```java -@SpringBootApplication -public class BootApplication { - public static void main(String[] args) { - // 启动代码 - SpringApplication.run(BootApplication.class, args); - } -} -``` +Hystrix 会监控微服务间调用的状况,当失败的调用到一定阈值,缺省时 5 秒内 20 次调用失败,就会启动熔断机制;当检测到该节点微服务调用响应正常后(检测方式是尝试性放开请求),自动恢复调用链路 -SpringApplication 构造方法: +- 熔断打开:请求不再进行调用当前服务,再有请求调用时将不会调用主逻辑,而是直接调用降级 fallback。实现了自动的发现错误并将降级逻辑切换为主逻辑,减少响应延迟效果。内部设置时钟一般为 MTTR(Mean time to repair,平均故障处理时间),当打开时长达到所设时钟则进入半熔断状态 +- 熔断关闭:熔断关闭不会对服务进行熔断,服务正常调用 +- 熔断半开:部分请求根据规则调用当前服务,如果请求成功且符合规则则认为当前服务恢复正常,关闭熔断,反之继续熔断 -* `this.resourceLoader = resourceLoader`:资源加载器,初始为 null -* `this.webApplicationType = WebApplicationType.deduceFromClasspath()`:判断当前应用的类型,是响应式还是 Web 类 -* `this.bootstrapRegistryInitializers = getBootstrapRegistryInitializersFromSpringFactories()`:**获取引导器** - * 去 **`META-INF/spring.factories`** 文件中找 org.springframework.boot.Bootstrapper - * 寻找的顺序:classpath → spring-beans → boot-devtools → springboot → boot-autoconfigure -* `setInitializers(getSpringFactoriesInstances(ApplicationContextInitializer.class))`:**获取初始化器** - * 去 `META-INF/spring.factories` 文件中找 org.springframework.context.ApplicationContextInitializer -* `setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class))`:**获取监听器** - * 去 `META-INF/spring.factories` 文件中找 org.springframework.context.ApplicationListener +![Cloud-Hystrix熔断机制](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Cloud-Hystrix熔断机制.png) -* `this.mainApplicationClass = deduceMainApplicationClass()`:获取出 main 程序类 -SpringApplication#run(String... args):创建 IOC 容器并实现了自动装配 -* `StopWatch stopWatch = new StopWatch()`:停止监听器,**监控整个应用的启停** -* `stopWatch.start()`:记录应用的启动时间 +**** -* `bootstrapContext = createBootstrapContext()`:**创建引导上下文环境** - - * `bootstrapContext = new DefaultBootstrapContext()`:创建默认的引导类环境 - * `this.bootstrapRegistryInitializers.forEach()`:遍历所有的引导器调用 initialize 方法完成初始化设置 -* `configureHeadlessProperty()`:让当前应用进入 headless 模式 -* `listeners = getRunListeners(args)`:**获取所有 RunListener(运行监听器)** - - * 去 `META-INF/spring.factories` 文件中找 org.springframework.boot.SpringApplicationRunListener -* `listeners.starting(bootstrapContext, this.mainApplicationClass)`:遍历所有的运行监听器调用 starting 方法 -* `applicationArguments = new DefaultApplicationArguments(args)`:获取所有的命令行参数 +##### 熔断操作 -* `environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments)`:**准备环境** +涉及到断路器的四个重要参数:**快照时间窗、请求总数阀值、窗口睡眠时间、错误百分比阀值** - * `environment = getOrCreateEnvironment()`:返回或创建基础环境信息对象 - * `switch (this.webApplicationType)`:根据当前应用的类型创建环境 - * `case SERVLET`:Web 应用环境对应 ApplicationServletEnvironment - * `case REACTIVE`:响应式编程对应 ApplicationReactiveWebEnvironment - * `default`:默认为 Spring 环境 ApplicationEnvironment - * `configureEnvironment(environment, applicationArguments.getSourceArgs())`:读取所有配置源的属性值配置环境 - * `ConfigurationPropertySources.attach(environment)`:属性值绑定环境信息 - * `sources.addFirst(ATTACHED_PROPERTY_SOURCE_NAME,..)`:把 configurationProperties 放入环境的属性信息头部 +- circuitBreaker.enabled:是否开启断路器 +- metrics.rollingStats.timeInMilliseconds:快照时间窗口,断路器确定是否打开需要统计一些请求和错误数据,而统计的时间范围就是快照时间窗,默认为最近的 10 秒 +- circuitBreaker.requestVolumeThreshold:请求总数阀值,该属性设置在快照时间窗内(默认 10s)使断路器跳闸的最小请求数量(默认是 20),如果 10s 内请求数小于设定值,就算请求全部失败也不会触发断路器 +- circuitBreaker.sleepWindowInMilliseconds:窗口睡眠时间,短路多久以后开始尝试是否恢复进入半开状态,默认 5s +- circuitBreaker.errorThresholdPercentage:错误百分比阀值,失败率达到多少后将断路器打开 - * `listeners.environmentPrepared(bootstrapContext, environment)`:运行监听器调用 environmentPrepared(),EventPublishingRunListener 发布事件通知所有的监听器当前环境准备完成 +```java + //总的意思就是在n(10)毫秒内的时间窗口期内,m次请求中有p% (60%)的请求失败了,那么断路器启动 +@HystrixCommand(fallbackMethod = "paymentCircuitBreaker_fallback", commandProperties = { + @HystrixProperty(name = "circuitBreaker.enabled", value = "true"), + @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "10"), + @HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "10000"), + @HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "60") +}) +public String paymentCircuitBreaker(@PathVariable("id") Integer id) { + if (id < 0) { + throw new RuntimeException("******id 不能负数"); + } + String serialNumber = IdUtil.simpleUUID(); // 等价于UUID.randomUUID().toString() - * `DefaultPropertiesPropertySource.moveToEnd(environment)`:移动 defaultProperties 属性源到环境中的最后一个源 + return Thread.currentThread().getName() + "\t" + "调用成功,流水号: " + serialNumber; +} +``` - * `bindToSpringApplication(environment)`:与容器绑定当前环境 +* 开启:满足一定的阈值(默认 10 秒内超过 20 个请求次数)、失败率达到阈值(默认 10 秒内超过 50% 的请求失败) +* 关闭:一段时间之后(默认是 5 秒),断路器是半开状态,会让其中一个请求进行转发,如果成功断路器会关闭,反之继续开启 - * `ConfigurationPropertySources.attach(environment)`:重新将属性值绑定环境信息 - * `sources.remove(ATTACHED_PROPERTY_SOURCE_NAME)`:从环境信息中移除 configurationProperties - * `sources.addFirst(ATTACHED_PROPERTY_SOURCE_NAME,..)`:把 configurationProperties 重新放入环境信息 -* `configureIgnoreBeanInfo(environment)`:**配置忽略的 bean** -* `printedBanner = printBanner(environment)`:打印 SpringBoot 标志 +*** -* `context = createApplicationContext()`:**创建 IOC 容器** - `switch (this.webApplicationType)`:根据当前应用的类型创建 IOC 容器 - * `case SERVLET`:Web 应用环境对应 AnnotationConfigServletWebServerApplicationContext - * `case REACTIVE`:响应式编程对应 AnnotationConfigReactiveWebServerApplicationContext - * `default`:默认为 Spring 环境 AnnotationConfigApplicationContext +#### 工作流程 -* `context.setApplicationStartup(this.applicationStartup)`:设置一个启动器 +具体工作流程: -* `prepareContext()`:配置 IOC 容器的基本信息 +1. 创建 HystrixCommand(用在依赖的服务返回单个操作结果的时候) 或 HystrixObserableCommand(用在依赖的服务返回多个操作结果的时候) 对象 - * `postProcessApplicationContext(context)`:后置处理流程 +2. 命令执行,其中 HystrixComand 实现了下面前两种执行方式,而 HystrixObservableCommand 实现了后两种执行方式 - * `applyInitializers(context)`:获取所有的**初始化器调用 initialize() 方法**进行初始化 - * `listeners.contextPrepared(context)`:所有的运行监听器调用 environmentPrepared() 方法,EventPublishingRunListener 发布事件通知 IOC 容器准备完成 - * `listeners.contextLoaded(context)`:所有的运行监听器调用 contextLoaded() 方法,通知 IOC 加载完成 + * execute():同步执行,从依赖的服务返回一个单一的结果对象, 或是在发生错误的时候抛出异常 -* `refreshContext(context)`:**刷新 IOC 容器** + * queue():异步执行, 直接返回 一个 Future 对象, 其中包含了服务执行结束时要返回的单一结果对象 - * Spring 的容器启动流程 - * `invokeBeanFactoryPostProcessors(beanFactory)`:**实现了自动装配** - * `onRefresh()`:**创建 WebServer** 使用该接口 + * observe():返回 Observable 对象,代表了操作的多个结果,它是一个 Hot Obserable(不论事件源是否有订阅者,都会在创建后对事件进行发布,所以对于 Hot Observable 的每个订阅者都有可能是从事件源的中途开始的,并可能只是看到了整个操作的局部过程) -* `afterRefresh(context, applicationArguments)`:留给用户自定义容器刷新完成后的处理逻辑 + * toObservable():同样会返回 Observable 对象,也代表了操作的多个结果,但它返回的是一个 Cold Observable(没有订阅者的时候并不会发布事件,而是进行等待,直到有订阅者之后才发布事件,所以对于 Cold Observable 的订阅者,它可以保证从一开始看到整个操作的全部过程) -* `stopWatch.stop()`:记录应用启动完成的时间 +3. 若当前命令的请求缓存功能是被启用的,并且该命令缓存命中,那么缓存的结果会立即以 Observable 对象的形式返回 +4. 检查断路器是否为打开状态,如果断路器是打开的,那么 Hystrix 不会执行命令,而是转接到 fallback 处理逻辑(第 8 步);如果断路器是关闭的,检查是否有可用资源来执行命令(第 5 步) +5. 线程池/请求队列/信号量是否占满,如果命令依赖服务的专有线程池和请求队列,或者信号量(不使用线程池时)已经被占满, 那么 Hystrix 也不会执行命令, 而是转接到 fallback 处理逻辑(第 8 步) +6. Hystrix 会根据我们编写的方法来决定采取什么样的方式去请求依赖服务 + * HystrixCommand.run():返回一个单一的结果,或者抛出异常 + * HystrixObservableCommand.construct():返回一个Observable 对象来发射多个结果,或通过 onError 发送错误通知 +7. Hystrix会将"成功"、"失败"、"拒绝"、"超时"等信息报告给断路器,而断路器会维护一组计数器来统计这些数据。断路器会使用这些统计数据来决定是否要将断路器打开,来对某个依赖服务的请求进行"熔断/短路" +8. 当命令执行失败的时候,Hystrix 会进入 fallback 尝试回退处理,通常也称该操作为"服务降级",而能够引起服务降级情况: + * 第 4 步:当前命令处于"熔断/短路"状态,断路器是打开的时候 + * 第 5 步:当前命令的线程池、请求队列或 者信号量被占满的时候 + * 第 6 步:HystrixObservableCommand.construct() 或 HystrixCommand.run() 抛出异常的时候 +9. 当 Hystrix 命令执行成功之后, 它会将处理结果直接返回或是以 Observable 的形式返回 -* `callRunners(context, applicationArguments)`:调用所有 runners +注意:如果、没有为命令实现降级逻辑或者在降级处理逻辑中抛出了异常, Hystrix 依然会返回一个 Observable 对象, 但是它不会发射任何结果数据,而是通过 onError 方法通知命令立即中断请求,并通过 onError() 方法将引起命令失败的异常发送给调用者 -* `listeners.started(context)`:所有的运行监听器调用 started() 方法 +![Cloud-Hystrix工作流程](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Cloud-Hystrix工作流程.png) -* `listeners.running(context)`:所有的运行监听器调用 running() 方法 - * 获取容器中的 ApplicationRunner、CommandLineRunner - * `AnnotationAwareOrderComparator.sort(runners)`:合并所有 runner 并且按照 @Order 进行排序 - * `callRunner()`:遍历所有的 runner,调用 run 方法 +官方文档:https://github.com/Netflix/Hystrix/wiki/How-it-Works -* `handleRunFailure(context, ex, listeners)`:**处理异常**,出现异常进入该逻辑 - * `handleExitCode(context, exception)`:处理错误代码 - * `listeners.failed(context, exception)`:运行监听器调用 failed() 方法 - * `reportFailure(getExceptionReporters(context), exception)`:通知异常 @@ -14749,296 +17453,243 @@ SpringApplication#run(String... args):创建 IOC 容器并实现了自动装 -#### 注解分析 +#### 服务监控 -SpringBoot 定义了一套接口规范,这套规范规定 SpringBoot 在启动时会扫描外部引用 jar 包中的 `META-INF/spring.factories` 文件,将文件中配置的类型信息加载到 Spring 容器,并执行类中定义的各种操作,对于外部的 jar 包,直接引入一个 starter 即可 +Hystrix 提供了准实时的调用监控(Hystrix Dashboard),Hystrix 会持续的记录所有通过 Hystrix 发起的请求的执行信息,并以统计报表和图形的形式展示给用户,包括每秒执行多少请求多少成功,多少失败等,Netflix 通过 `hystrix-metrics-event-stream` 项目实现了对以上指标的监控,Spring Cloud 提供了 Hystrix Dashboard 的整合,对监控内容转化成可视化页面 -@SpringBootApplication 注解是 `@SpringBootConfiguration`、`@EnableAutoConfiguration`、`@ComponentScan` 注解的集合 +* 引入 pom 依赖: -* @SpringBootApplication 注解 + ```xml + + org.springframework.cloud + spring-cloud-starter-netflix-hystrix-dashboard + + ``` - ```java - @Inherited - @SpringBootConfiguration //代表 @SpringBootApplication 拥有了该注解的功能 - @EnableAutoConfiguration //同理 - @ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class), - @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) }) - // 扫描被 @Component (@Service,@Controller)注解的 bean,容器中将排除TypeExcludeFilter 和 AutoConfigurationExcludeFilter - public @interface SpringBootApplication { } +* application.yml:只需要端口即可 + + ```yaml + server: + port: 9001 ``` -* @SpringBootConfiguration 注解: +* 主启动类: ```java - @Configuration // 代表是配置类 - @Indexed - public @interface SpringBootConfiguration { - @AliasFor(annotation = Configuration.class) - boolean proxyBeanMethods() default true; + @SpringBootApplication + @EnableHystrixDashboard // 开启Hystrix仪表盘 + public class HystrixDashboardMain9001 { + public static void main(String[] args) { + SpringApplication.run(HystrixDashboardMain9001.class, args); + } } ``` - @AliasFor 注解:表示别名,可以注解到自定义注解的两个属性上表示这两个互为别名,两个属性其实是同一个含义相互替代 +* 所有微服务(生产者)提供类 8001/8002/8003 都需要监控依赖配置 -* @ComponentScan 注解:默认扫描当前类所在包及其子级包下的所有文件 + ```xml + + org.springframework.boot + spring-boot-starter-actuator + + ``` -* **@EnableAutoConfiguration 注解:启用 SpringBoot 的自动配置机制** +* 启动测试:http://localhost:9001/hystrix - ````java - @AutoConfigurationPackage - @Import(AutoConfigurationImportSelector.class) - public @interface EnableAutoConfiguration { - String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration"; - Class[] exclude() default {}; - String[] excludeName() default {}; + Cloud-Hystrix可视化界面 + +* 新版本 Hystrix 需要在需要监控的微服务端的主启动类中指定监控路径,不然会报错 + + ```java + @SpringBootApplication + @EnableEurekaClient // 本服务启动后会自动注册进eureka服务中 + @EnableCircuitBreaker // 对hystrixR熔断机制的支持 + public class PaymentHystrixMain8001 { + public static void main(String[] args) { + SpringApplication.run(PaymentHystrixMain8001.class, args); + } + + /** ======================================需要添加的代码================== + *此配置是为了服务监控而配置,与服务容错本身无关,springcloud升级后的坑 + *ServletRegistrationBean因为springboot的默认路径不是"/hystrix.stream", + *只要在自己的项目里配置上下面的servlet就可以了 + */ + @Bean + public ServletRegistrationBean getServlet() { + HystrixMetricsStreamServlet streamServlet = new HystrixMetricsStreamServlet(); + ServletRegistrationBean registrationBean = new ServletRegistrationBean(streamServlet); + registrationBean.setLoadOnStartup(1); + registrationBean.addUrlMappings("/hystrix.stream"); + registrationBean.setName("HystrixMetricsStreamServlet"); + return registrationBean; + } } - ```` + ``` - * @AutoConfigurationPackage:**将添加该注解的类所在的 package 作为自动配置 package 进行管理**,把启动类所在的包设置一次,为了给各种自动配置的第三方库扫描用,比如带 @Mapper 注解的类,Spring 自身是不能识别的,但自动配置的 Mybatis 需要扫描用到,而 ComponentScan 只是用来扫描注解类,并没有提供接口给三方使用 +* 指标说明: - ```java - @Import(AutoConfigurationPackages.Registrar.class) // 利用 Registrar 给容器中导入组件 - public @interface AutoConfigurationPackage { - String[] basePackages() default {}; //自动配置包,指定了配置类的包 - Class[] basePackageClasses() default {}; - } - ``` + ![Cloud-Hystrix界面图示说明](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Cloud-Hystrix界面图示说明.png) - `register(registry, new PackageImports(metadata).getPackageNames().toArray(new String[0]))`:注册 BD - * `new PackageImports(metadata).getPackageNames()`:获取添加当前注解的类的所在包 - * `registry.registerBeanDefinition(BEAN, new BasePackagesBeanDefinition(packageNames))`:存放到容器中 - * `new BasePackagesBeanDefinition(packageNames)`:把当前主类所在的包名封装到该对象中 - * @Import(AutoConfigurationImportSelector.class):**自动装配的核心类** - 容器刷新时执行:**invokeBeanFactoryPostProcessors()** → invokeBeanDefinitionRegistryPostProcessors() → postProcessBeanDefinitionRegistry() → processConfigBeanDefinitions() → parse() → process() → processGroupImports() → getImports() → process() → **AutoConfigurationImportSelector#getAutoConfigurationEntry()** - ```java - protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) { - if (!isEnabled(annotationMetadata)) { - return EMPTY_ENTRY; - } - // 获取注解属性,@SpringBootApplication 注解的 exclude 属性和 excludeName 属性 - AnnotationAttributes attributes = getAttributes(annotationMetadata); - // 获取所有需要自动装配的候选项 - List configurations = getCandidateConfigurations(annotationMetadata, attributes); - // 去除重复的选项 - configurations = removeDuplicates(configurations); - // 获取注解配置的排除的自动装配类 - Set exclusions = getExclusions(annotationMetadata, attributes); - checkExcludedClasses(configurations, exclusions); - // 移除所有的配置的不需要自动装配的类 - configurations.removeAll(exclusions); - // 过滤,条件装配 - configurations = getConfigurationClassFilter().filter(configurations); - // 获取 AutoConfigurationImportListener 类的监听器调用 onAutoConfigurationImportEvent 方法 - fireAutoConfigurationImportEvents(configurations, exclusions); - // 包装成 AutoConfigurationEntry 返回 - return new AutoConfigurationEntry(configurations, exclusions); - } - ``` +**** - AutoConfigurationImportSelector#getCandidateConfigurations:获取自动配置的候选项 - * `List configurations = SpringFactoriesLoader.loadFactoryNames()`:加载自动配置类 - 参数一:`getSpringFactoriesLoaderFactoryClass()`:获取 @EnableAutoConfiguration 注解类 - 参数二:`getBeanClassLoader()`:获取类加载器 - * `factoryTypeName = factoryType.getName()`:@EnableAutoConfiguration 注解的全类名 - * `return loadSpringFactories(classLoaderToUse).getOrDefault()`:加载资源 - * `urls = classLoader.getResources(FACTORIES_RESOURCE_LOCATION)`:获取资源类 - * `FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories"`:**加载的资源的位置** +## 服务网关 - * `return configurations`:返回所有自动装配类的候选项 +### Zuul - * 从 spring-boot-autoconfigure-2.5.3.jar/META-INF/spring.factories 文件中寻找 EnableAutoConfiguration 字段,获取自动装配类,**进行条件装配,按需装配** +SpringCloud 中所集成的 Zuul 版本,采用的是 Tomcat 容器,基于 Servlet 之上的一个阻塞式处理模型,不支持任何长连接,用 Java 实现,而 JVM 本身会有第一次加载较慢的情况,使得 Zuul 的性能相对较差 - ![](https://gitee.com/seazean/images/raw/master/Frame/SpringBoot-自动装配配置文件.png) +官网: https://github.com/Netflix/zuul/wiki -*** +**** -#### 装配流程 -Spring Boot 通过 `@EnableAutoConfiguration` 开启自动装配,通过 SpringFactoriesLoader 加载 `META-INF/spring.factories` 中的自动配置类实现自动装配,自动配置类其实就是通过 `@Conditional` 注解按需加载的配置类,想要其生效必须引入 `spring-boot-starter-xxx` 包实现起步依赖 -* SpringBoot 先加载所有的自动配置类 xxxxxAutoConfiguration -* 每个自动配置类进行**条件装配**,默认都会绑定配置文件指定的值(xxxProperties 和配置文件进行了绑定) -* SpringBoot 默认会在底层配好所有的组件,如果用户自己配置了**以用户的优先** -* **定制化配置:** - - 用户可以使用 @Bean 新建自己的组件来替换底层的组件 - - 用户可以去看这个组件是获取的配置文件前缀值,在配置文件中修改 +### Gateway -以 DispatcherServletAutoConfiguration 为例: +#### 基本介绍 -```java -@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE) -// 类中的 Bean 默认不是单例 -@Configuration(proxyBeanMethods = false) -@ConditionalOnWebApplication(type = Type.SERVLET) -// 条件装配,环境中有 DispatcherServlet 类才进行自动装配 -@ConditionalOnClass(DispatcherServlet.class) -@AutoConfigureAfter(ServletWebServerFactoryAutoConfiguration.class) -public class DispatcherServletAutoConfiguration { - // 注册的 DispatcherServlet 的 BeanName - public static final String DEFAULT_DISPATCHER_SERVLET_BEAN_NAME = "dispatcherServlet"; +SpringCloud Gateway 是 Spring Cloud 的一个全新项目,基于 Spring 5.0+Spring Boot 2.0 和 Project Reactor 等技术开发的网关,旨在为微服务架构提供一种简单有效的统一的 API 路由管理方式。 - @Configuration(proxyBeanMethods = false) - @Conditional(DefaultDispatcherServletCondition.class) - @ConditionalOnClass(ServletRegistration.class) - // 绑定配置文件的属性,从配置文件中获取配置项 - @EnableConfigurationProperties(WebMvcProperties.class) - protected static class DispatcherServletConfiguration { - - // 给容器注册一个 DispatcherServlet,起名字为 dispatcherServlet - @Bean(name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME) - public DispatcherServlet dispatcherServlet(WebMvcProperties webMvcProperties) { - // 新建一个 DispatcherServlet 设置相关属性 - DispatcherServlet dispatcherServlet = new DispatcherServlet(); - // spring.mvc 中的配置项获取注入,没有就填充默认值 - dispatcherServlet.setDispatchOptionsRequest(webMvcProperties.isDispatchOptionsRequest()); - // ...... - // 返回该对象注册到容器内 - return dispatcherServlet; - } +* 基于 WebFlux 框架实现,而 WebFlux 框架底层则使用了高性能的 Reactor 模式通信框架 Netty(异步非阻塞响应式的框架) +* 基于 Filter 链的方式提供了网关基本的功能,例如:安全、监控/指标、限流等 - @Bean - // 容器中有这个类型组件才进行装配 - @ConditionalOnBean(MultipartResolver.class) - // 容器中没有这个名字 multipartResolver 的组件 - @ConditionalOnMissingBean(name = DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME) - // 方法名就是 BeanName - public MultipartResolver multipartResolver(MultipartResolver resolver) { - // 给 @Bean 标注的方法传入了对象参数,这个参数就会从容器中找,因为用户自定义了该类型,以用户配置的优先 - // 但是名字不符合规范,所以获取到该 Bean 并返回到容器一个规范的名称:multipartResolver - return resolver; - } - } -} -``` +Gateway 的三个核心组件: -```java -// 将配置文件中的 spring.mvc 前缀的属性与该类绑定 -@ConfigurationProperties(prefix = "spring.mvc") -public class WebMvcProperties { } -``` +* Route:路由是构建网关的基本模块,由 ID、目标 URI、一系列的断言和过滤器组成,如果断言为 true 则匹配该路由 +* Predicate:断言,可以匹配 HTTP 请求中的所有内容(例如请求头或请求参数),如果请求参数与断言相匹配则进行路由 +* Filter:指 Spring 框架中的 GatewayFilter实例,使用过滤器可以在请求被路由前或之后(拦截)对请求进行修改 +![Cloud-Gateway工作流程](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Cloud-Gateway工作流程.png) +核心逻辑:路由转发 + 执行过滤器链 +- 客户端向 Spring Cloud Gateway 发出请求,然后在 Gateway Handler Mapping 中找到与请求相匹配的路由,将其发送到 Gateway Web Handler +- Handler 通过指定的过滤器链来将请求发送到际的服务执行业务逻辑,然后返回 +- 过滤器之间用虚线分开是因为过滤器可能会在发送代理请求之前(pre)或之后(post)执行业务逻辑 +- Filter 在 pre 类型的过滤器可以做参数校验、权限校验、流量监控、日志输出、协议转换等,在 post 类型的过滤器中可以做响应内容、响应头的修改、日志的输出、流量监控等 -*** -### 事件监听 +*** -SpringBoot 在项目启动时,会对几个监听器进行回调,可以实现监听器接口,在项目启动时完成一些操作 -ApplicationContextInitializer、SpringApplicationRunListener、CommandLineRunner、ApplicationRunner -* MyApplicationRunner +#### 网关使用 - **自定义监听器的启动时机**:MyApplicationRunner 和 MyCommandLineRunner 都是当项目启动后执行,使用 @Component 放入容器即可使用 +##### 配置方式 - ```java - //当项目启动后执行run方法 - @Component - public class MyApplicationRunner implements ApplicationRunner { - @Override - public void run(ApplicationArguments args) throws Exception { - System.out.println("ApplicationRunner...run"); - System.out.println(Arrays.asList(args.getSourceArgs()));//properties配置信息 - } - } - ``` +Gateway 网关路由有两种配置方式,分别为通过 yml 配置和注入 Bean -* MyCommandLineRunner +* 引入 pom 依赖:Gateway 不需要 spring-boot-starter-web 依赖,否在会报错,原因是底层使用的是 WebFlux 与 Web 冲突 - ```java - @Component - public class MyCommandLineRunner implements CommandLineRunner { - @Override - public void run(String... args) throws Exception { - System.out.println("CommandLineRunner...run"); - System.out.println(Arrays.asList(args)); - } - } + ```xml + + org.springframework.cloud + spring-cloud-starter-gateway + ``` -* MyApplicationContextInitializer 的启用要**在 resource 文件夹下添加 META-INF/spring.factories** +* application.yml: - ```properties - org.springframework.context.ApplicationContextInitializer=\ - com.example.springbootlistener.listener.MyApplicationContextInitializer + ```yaml + server: + port: 9527 + + spring: + application: + name: cloud-gateway + + eureka: + instance: + hostname: cloud-gateway-service + client: #服务提供者provider注册进eureka服务列表内 + service-url: + register-with-eureka: true + fetch-registry: true + defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka #集群版 ``` +* 主启动类(网关不需要业务类): + ```java - @Component - public class MyApplicationContextInitializer implements ApplicationContextInitializer { - @Override - public void initialize(ConfigurableApplicationContext applicationContext) { - System.out.println("ApplicationContextInitializer....initialize"); + @SpringBootApplication + @EnableEurekaClient + public class GateWayMain9527 { + public static void main(String[] args) { + SpringApplication.run(GateWayMain9527.class, args); } } ``` -* MySpringApplicationRunListener 的使用要添加**构造器** +* 以前访问 provider-payment8001 中的 Controller 方法,通过 localhost:8001/payment/get/id 和 localhost:8001/payment/lb,项目不想暴露 8001 端口号,希望在 8001 外面套一层 9527 端口: - ```java - public class MySpringApplicationRunListener implements SpringApplicationRunListener { - //构造器 - public MySpringApplicationRunListener(SpringApplication sa, String[] args) { - } - - @Override - public void starting() { - System.out.println("starting...项目启动中");//输出SPRING之前 - } - - @Override - public void environmentPrepared(ConfigurableEnvironment environment) { - System.out.println("environmentPrepared...环境对象开始准备"); - } - - @Override - public void contextPrepared(ConfigurableApplicationContext context) { - System.out.println("contextPrepared...上下文对象开始准备"); - } - - @Override - public void contextLoaded(ConfigurableApplicationContext context) { - System.out.println("contextLoaded...上下文对象开始加载"); - } - - @Override - public void started(ConfigurableApplicationContext context) { - System.out.println("started...上下文对象加载完成"); - } + ```yaml + server: + port: 9527 - @Override - public void running(ConfigurableApplicationContext context) { - System.out.println("running...项目启动完成,开始运行"); - } + spring: + application: + name: cloud-gateway + ## =====================新增==================== + cloud: + gateway: + routes: + - id: payment_routh # payment_route #路由的ID,没有固定规则但要求【唯一】,建议配合服务名 + uri: http://localhost:8001 #匹配后提供服务的路由地址 + predicates: + - Path=/payment/get/** # 断言,路径相匹配的进行路由 - @Override - public void failed(ConfigurableApplicationContext context, Throwable exception) { - System.out.println("failed...项目启动失败"); - } - } + - id: payment_routh2 # payment_route#路由的ID,没有固定规则但要求【唯一】,建议配合服务名 + uri: http://localhost:8001 #匹配后提供服务的路由地址 + predicates: + - Path=/payment/lb/** # 断言,路径相匹配的进行路由 ``` - + * uri + predicate 拼接就是具体的接口请求路径,通过 localhost:9527 映射的地址 + * predicate 断言 http://localhost:8001下面有一个 /payment/get/** 的地址,如果找到了该地址就返回 true,可以用 9527 端口访问,进行端口的适配 + * `**` 表示通配符,因为这是一个不确定的参数 + + + +**** + + +##### 注入Bean +通过 9527 网关访问到百度的网址 https://www.baidu.com/,在 config 包下创建一个配置类,路由规则是访问 /baidu 跳转到百度 + +```java +@Configuration +public class GatewayConfig { + // 配置了一个 id 为 path_route_cloud 的路由规则 + @Bean + public RouteLocator customRouteLocator(RouteLocatorBuilder routeLocatorBuilder){ + // 构建一个路由器,这个routes相当于yml配置文件中的routes + RouteLocatorBuilder.Builder routes = routeLocatorBuilder.routes(); + // 路由器的id是:path_route_cloud,规则是访问/baidu ,将会转发到 https://www.baidu.com/ + routes.route("path_route_cloud", + r -> r.path("/baidu").uri(" https://www.baidu.com")).build(); + return routes.build(); + } +} +``` @@ -15046,156 +17697,275 @@ ApplicationContextInitializer、SpringApplicationRunListener、CommandLineRunner +##### 动态路由 +Gateway 会根据注册中心注册的服务列表,以注册中心上微服务名为路径创建动态路由进行转发,从而实现动态路由和负载均衡,避免出现一个路由规则仅对应一个接口方法,当请求地址很多时需要很大的配置文件 -## 配置文件 +application.yml 开启动态路由功能 -### 配置方式 +```yaml +spring: + application: + name: cloud-gateway + cloud: + gateway: + discovery: + locator: + enabled: true # 开启从注册中心动态创建路由的功能,利用微服务名进行路由 + routes: + - id: payment_routh1 # 路由的ID,没有固定规则但要求唯一,建议配合服务名 + uri: lb://cloud-payment-service # 匹配后提供服务的路由地址 + predicates: + - Path=/payment/get/** # 断言,路径相匹配的进行路由 + + - id: payment_routh2 #路由的ID,没有固定规则但要求唯一,建议配合服务名 + uri: lb://cloud-payment-service #匹配后提供服务的路由地址 + predicates: + - Path=/payment/lb/** # 断言,路径相匹配的进行路由 + - After=2021-09-28T19:14:51.514+08:00[Asia/Shanghai] +``` -#### 文件类型 +lb:// 开头代表从注册中心中获取服务,后面是需要转发到的服务名称 -SpringBoot 是基于约定的,很多配置都有默认值,如果想使用自己的配置替换默认配置,可以使用 application.properties 或者application.yml(application.yaml)进行配置 -* 默认配置文件名称:application -* 在同一级目录下优先级为:properties > yml > yaml -例如配置内置 Tomcat 的端口 -* properties: - ```properties - server.port=8080 +***** + + + +#### 断言类型 + +After Route Predicate:匹配该断言时间之后的 URI 请求 + +* 获取时间: + + ```java + public class TimeTest { + public static void main(String[] args) { + ZonedDateTime zbj = ZonedDateTime.now(); // 默认时区 + System.out.println(zbj); //2023-01-10T16:31:44.106+08:00[Asia/Shanghai] + } + } ``` -* yml: +* 配置 yml:动态路由小结有配置 + +* 测试:正常访问成功,将时间修改到 2023-01-10T16:31:44.106+08:00[Asia/Shanghai] 之后访问失败 + + ![Cloud-Gateway时间断言](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Cloud-Gateway时间断言.png) + +常见断言类型: + +* Before Route Predicate:匹配该断言时间之前的 URI 请求 + +* Between Route Predicate:匹配该断言时间之间的 URI 请求 ```yaml - server: port: 8080 + - Between=2022-02-02T17:45:06.206+08:00[Asia/Shanghai],2022-03-25T18:59:06.206+08:00[Asia/Shanghai] ``` -* yaml: +* Cookie Route Predicate:Cookie 断言,两个参数分别是 Cookie name 和正则表达式,路由规则会通过获取对应的 Cookie name 值和正则表达式去匹配,如果匹配上就会执行路由 ```yaml - server: port: 8080 + - Cookie=username, seazean # 只有发送的请求有 cookie,而且有username=seazean这个数据才能访问,反之404 ``` +* Header Route Predicate:请求头断言 + ```yaml + - Header=X-Request-Id, \d+ # 请求头要有 X-Request-Id 属性,并且值为整数的正则表达式 + ``` +* Host Route Predicate:指定主机可以访问,可以指定多个用 `,` 分隔开 -*** + ```yaml + - Host=**.seazean.com + ``` +* Method Route Predicate:请求类型断言 + ```yaml + - Method=GET # 只有 Get 请求才能访问 + ``` -#### 加载顺序 +* Path Route Predicate:路径匹配断言 -所有位置的配置文件都会被加载,互补配置,**高优先级配置内容会覆盖低优先级配置内容** +* Query Route Predicate:请求参数断言 -扫描配置文件的位置按优先级**从高到底**: + ```yaml + - Query=username, \d+ # 要有参数名 username 并且值还要是整数才能路由 + ``` -- `file:./config/`:**当前项目**下的 /config 目录下 -- `file:./`:当前项目的根目录,Project工程目录 -- `classpath:/config/`:classpath 的 /config 目录 -- `classpath:/`:classpath 的根目录,就是 resoureces 目录 -项目外部配置文件加载顺序:外部配置文件的使用是为了对内部文件的配合 +**** + + + +#### Filter使用 + +Filter 链是同时满足一系列的过滤链,路由过滤器可用于修改进入的 HTTP 请求和返回的 HTTP 响应,路由过滤器只能指定路由进行使用,Spring Cloud Gateway 内置了多种路由过滤器,都由 GatewayFilter 的工厂类来产生 + +配置文件:https://cloud.spring.io/spring-cloud-static/spring-cloud-gateway/2.2.1.RELEASE/reference/html/#gatewayfilter-factories + +自定义全局过滤器:实现两个主要接口 GlobalFilter, Ordered + +```java +@Component +@Slf4j +public class MyLogGateWayFilter implements GlobalFilter, Ordered { + + @Override + public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { + log.info("*********************come in MyLogGateWayFilter: "+ new Date()); + // 取出请求参数的uname对应的值 + String uname = exchange.getRequest().getQueryParams().getFirst("uname"); + // 如果 uname 为空,就直接过滤掉,不走路由 + if(uname == null){ + log.info("************* 用户名为 NULL 非法用户 o(╥﹏╥)o"); + + // 判断该请求不通过时:给一个回应,返回 + exchange.getResponse().setStatusCode(HttpStatus.NOT_ACCEPTABLE); + return exchange.getResponse().setComplete(); + } + + // 反之,调用下一个过滤器,也就是放行:在该环节判断通过的 exchange 放行,交给下一个 filter 判断 + return chain.filter(exchange); + } + + // 设置这个过滤器在Filter链中的加载顺序,数字越小,优先级越高 + @Override + public int getOrder() { + return 0; + } +} + +``` + + + + + +*** + + + + -* 命令行:在 package 打包后的 target 目录下,使用该命令 +## 服务配置 - ```sh - java -jar myproject.jar --server.port=9000 - ``` +### config -* 指定配置文件位置 +#### 基本介绍 - ```sh - java -jar myproject.jar --spring.config.location=e://application.properties - ``` +SpringCloud Config 为微服务架构中的微服务提供集中化的外部配置支持(Git/GitHub),为各个不同微服务应用的所有环境提供了一个中心化的外部配置(Config Server) -* 按优先级从高到底选择配置文件的加载命令 +![Cloud-Config工作原理](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Cloud-Config工作原理.png) - ```sh - java -jar myproject.jar - ``` +SpringCloud Config 分为服务端和客户端两部分 - +* 服务端也称为分布式配置中心,是一个独立的微服务应用,连接配置服务器并为客户端提供获取配置信息,加密/解密等信息访问接口 +* 客户端通过指定的配置中心来管理应用资源,以及与业务相关的配置内容,并在启动时从配置中心获取和加载配置信息,配置服务器默认采用 Git 来存储配置信息,这样既有助于对环境配置进行版本管理,也可以通过 Git 客户端来方便的管理和访问配置内容 +优点: +* 集中管理配置文件 +* 不同环境不同配置,动态化的配置更新,分环境部署比如 dev/test/prod/beta/release +* 运行期间动态调整配置,服务向配置中心统一拉取配置的信息,**服务不需要重启即可感知到配置的变化并应用新的配置** +* 将配置信息以 Rest 接口的形式暴露 -*** +官网: https://cloud.spring.io/spring-cloud-static/spring-cloud-config/2.2.1.RELEASE/reference/html/ -### yaml语法 -基本语法: -- 大小写敏感 -- **数据值前边必须有空格,作为分隔符** -- 使用缩进表示层级关系 -- 缩进时不允许使用Tab键,只允许使用空格(各个系统 Tab对应空格数目可能不同,导致层次混乱) +**** -- 缩进的空格数目不重要,只要相同层级的元素左侧对齐即可 -- ''#" 表示注释,从这个字符一直到行尾,都会被解析器忽略 - ```yaml - server: - port: 8080 - address: 127.0.0.1 - ``` +#### 服务端 -数据格式: +构建 Config Server 模块 -* 纯量:单个的、不可再分的值 +* 引入 pom 依赖: - ```yaml - msg1: 'hello \n world' # 单引忽略转义字符 - msg2: "hello \n world" # 双引识别转义字符 + ```xml + + + org.springframework.cloud + spring-cloud-config-server + ``` -* 对象:键值对集合,Map、Hash +* application.yml: ```yaml - person: - name: zhangsan - age: 20 - # 行内写法 - person: {name: zhangsan} + server: + port: 3344 + + spring: + application: + name: cloud-config-center #注册进Eureka服务器的微服务名 + cloud: + config: + server: + git: + # GitHub上面的git仓库名字 这里可以写https地址跟ssh地址,https地址需要配置username和 password + uri: git@github.com:seazean/springcloud-config.git + default-label: main + search-paths: + - springcloud-config # 搜索目录 + # username: + # password: + label: main # 读取分支,以前是master + + #服务注册到eureka地址 + eureka: + client: + service-url: + defaultZone: http://localhost:7001/eureka,http://localhost:7002/eureka #集群版 + ``` + + search-paths 表示远程仓库下有一个叫做 springcloud-config 的,label 则表示读取 main分支里面的内容 + +* 主启动类: + + ```java + @SpringBootApplication + @EnableEurekaClient + @EnableConfigServer //开启SpringCloud的 + public class ConfigCenterMain3344 { + public static void main(String[] args) { + SpringApplication.run(ConfigCenterMain3344.class, args); + } + } ``` - 注意:不建议使用 JSON,应该使用 yaml 语法 +配置读取规则: -* 数组:一组按次序排列的值,List、Array +```yaml +/{application}/{profile}[/{label}] +/{application}-{profile}.yml +/{label}/{application}-{profile}.yml +/{application}-{profile}.properties +/{label}/{application}-{profile}.properties +``` - ```yaml - address: - - beijing - - shanghai - # 行内写法 - address: [beijing,shanghai] - ``` +* label:分支 +* name:服务名 +* profile:环境(dev/test/prod) - ```yaml - allPerson #List - - {name:lisi, age:18} - - {name:wangwu, age:20} - # 行内写法 - allPerson: [{name:lisi, age:18}, {name:wangwu, age:20}] - ``` +比如:http://localhost:3344/master/config-dev.yaml -* 参数引用: - ```yaml - name: lisi - person: - name: ${name} # 引用上边定义的name值 - ``` @@ -15203,421 +17973,411 @@ SpringBoot 是基于约定的,很多配置都有默认值,如果想使用自 -### 获取配置 - -三种获取配置文件的方式: +#### 客户端 -* 注解 @Value +##### 基本配置 - ```java - @RestController - public class HelloController { - @Value("${name}") - private String name; - - @Value("${person.name}") - private String name2; - - @Value("${address[0]}") - private String address1; - - @Value("${msg1}") - private String msg1; - - @Value("${msg2}") - private String msg2; - - @RequestMapping("/hello") - public String hello(){ - System.out.println("所有的数据"); - return " hello Spring Boot !"; - } - } - ``` +配置客户端 Config Client,客户端从配置中心(Config Server)获取配置信息 -* Evironment 对象 +* 引入 pom 依赖: - ```java - @Autowired - private Environment env; - - @RequestMapping("/hello") - public String hello() { - System.out.println(env.getProperty("person.name")); - System.out.println(env.getProperty("address[0]")); - return " hello Spring Boot !"; - } + ```xml + + + org.springframework.cloud + spring-cloud-starter-config + ``` -* 注解 @ConfigurationProperties 配合 @Component 使用 +* bootstrap.yml:系统级配置,优先级更高,application.yml 是用户级的资源配置项 - **注意**:参数 prefix 一定要指定 + Spring Cloud 会创建一个 Bootstrap Context 作为 Spring 应用的 Application Context 的父上下文,初始化的时候 Bootstrap Context 负责从外部源加载配置属性并解析配置,这两个上下文共享一个从外部获取的 Environment,为了配置文件的加载顺序和分级管理,这里使用 bootstrap.yml + + ```yaml + server: + port: 3355 # 构建多个微服务,3366 3377 等 + + spring: + application: + name: config-client + cloud: + #Config客户端配置 + config: + label: main #分支名称 以前是master + name: config #配置文件名称 + profile: dev #读取后缀名称 + # main分支上config-dev.yml的配置文件被读取 http://config-3344.com:3344/master/config-dev.yml + uri: http://localhost:3344 # 配置中心地址k + + #服务注册到eureka地址 + eureka: + client: + service-url: + defaultZone: http://localhost:7001/eureka,http://localhost:7002/eureka + ``` + +* 主启动类: ```java - @Component //不扫描该组件到容器内,无法完成自动装配 - @ConfigurationProperties(prefix = "person") - public class Person { - private String name; - private int age; - private String[] address; + @SpringBootApplication + @EnableEurekaClient + public class ConfigClientMain3355 { + public static void main(String[] args) { + SpringApplication.run(ConfigClientMain3355.class, args); + } } ``` +* 业务类:将配置信息以 REST 窗口的形式暴露 + ```java - @Autowired - private Person person; + @RestController + public class ConfigClientController { + @Value("${config.info}") + private String configInfo; - @RequestMapping("/hello") - public String hello() { - System.out.println(person); - //Person{name='zhangsan', age=20, address=[beijing, shanghai]} - return " hello Spring Boot !"; + @GetMapping("/configInfo") + public String getConfigInfo() { + return configInfo; + } } ``` - + *** -### 配置提示 +##### 动态刷新 -自定义的类和配置文件绑定一般没有提示,添加如下依赖可以使用提示: +分布式配置的动态刷新问题,修改 GitHub 上的配置文件,Config Server 配置中心立刻响应,但是 Config Client 客户端没有任何响应,需要重启客户端 -```xml - - org.springframework.boot - spring-boot-configuration-processor - true - +* 引入 pom 依赖: - - - - - org.springframework.boot - spring-boot-maven-plugin - - - - org.springframework.boot - spring-boot-configuration-processor - - - - - - -``` + ```xml + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-actuator + + ``` +* 修改 yml,暴露监控端口:SpringBoot 的 actuator 启动端点监控 Web 端默认加载默认只有两个 info,health 可见的页面节点 + ```yaml + management: + endpoints: + web: + exposure: + include: "*" # 表示包含所有节点页面 + exclude: env,beans # 表示排除env、beans + ``` +* 业务类:加 @RefreshScope 注解 + ```java + @RestController + @RefreshScope + public class ConfigClientController { + // 从配置文件中取前缀为server.port的值 + @Value("${config.info}") + private String configInfo; + // config-{profile}.yml + @GetMapping("/configInfo") + public String getConfigInfo() { + return configInfo; + } + } + ``` -*** +此时客户端还是没有刷新,需要发送 POST 请求刷新 3355:`curl -X POST "http://localhost:3355/actuator/refresh` +引出问题: +* 在微服务多的情况下,每个微服务都需要执行一个 POST 请求,手动刷新成本太大 +* 可否广播,一次通知,处处生效,大范围的实现自动刷新 -### Profile +解决方法:Bus 总线 -@Profile:指定组件在哪个环境的情况下才能被注册到容器中,不指定,任何环境下都能注册这个组件 - * 加了环境标识的 bean,只有这个环境被激活的时候才能注册到容器中,默认是 default 环境 - * 写在配置类上,只有是指定的环境的时候,整个配置类里面的所有配置才能开始生效 - * 没有标注环境标识的 bean 在,任何环境下都是加载的 -Profile 的配置: -* **profile 是用来完成不同环境下,配置动态切换功能** -* **profile 配置方式**:多 profile 文件方式,提供多个配置文件,每个代表一种环境 +*** - * application-dev.properties/yml 开发环境 - * application-test.properties/yml 测试环境 - * sapplication-pro.properties/yml 生产环境 -* yml 多文档方式:在 yml 中使用 --- 分隔不同配置 - ```yacas - --- - server: - port: 8081 - spring: - profiles:dev - --- - server: - port: 8082 - spring: - profiles:test - --- - server: - port: 8083 - spring: - profiles:pro - --- - ``` -* **profile 激活方式** - * 配置文件:在配置文件中配置:spring.profiles.active=dev +## 服务消息 - ```properties - spring.profiles.active=dev - ``` +### Bus - * 虚拟机参数:在VM options 指定:`-Dspring.profiles.active=dev` +#### 基本介绍 - ![](https://gitee.com/seazean/images/raw/master/Frame/SpringBoot-profile激活方式虚拟机参数.png) +Spring Cloud Bus 能管理和传播分布式系统间的消息,就像分布式执行器,可用于广播状态更改、事件推送、微服务间的通信通道等 - * 命令行参数:`java –jar xxx.jar --spring.profiles.active=dev` +消息总线:在微服务架构的系统中,通常会使用轻量级的消息代理来构建一个共用的消息主题,并让系统中所有微服务实例都连接上来。由于该主题中产生的消息会被所有实例监听和消费,所以称为消息总线 - 在 Program arguments 里输入,也可以先 package +基本原理:ConfigClient 实例都监听 MQ 中同一个 Topic(默认 springCloudBus),当一个服务刷新数据时,会把信息放入 Topic 中,这样其它监听同一 Topic 的服务就能得到通知,然后去更新自身的配置 +**** +#### 全局广播 -*** +利用消息总线接触一个服务端 ConfigServer 的 `/bus/refresh` 断点,从而刷新所有客户端的配置 +Cloud-Bus全局广播架构 +改造 ConfigClient: +* 引入 MQ 的依赖: + ```xml + + + org.springframework.cloud + spring-cloud-starter-bus-amqp + + ``` -## Web开发 +* yml 文件添加 MQ 信息: -### 功能支持 + ```yaml + server: + port: 3344 + + spring: + application: + name: config-client #注册进Eureka服务器的微服务名 + cloud: + # rabbitmq相关配置 + rabbitmq: + host: localhost + port: 5672 + username: guest + password: guest + + # rabbitmq相关配置,暴露bus刷新配置的端点 + management: + endpoints: # 暴露bus刷新配置的端点 + web: + exposure: + include: 'bus-refresh' + ``` -SpringBoot 自动配置了很多约定,大多场景都无需自定义配置 +* 只需要调用一次 `curl -X POST "http://localhost:3344/actuator/bus-refresh`,可以实现全局广播 -* 内容协商视图解析器 ContentNegotiatingViewResolver 和 BeanName 视图解析器 BeanNameViewResolver -* 支持静态资源(包括 webjars)和静态 index.html 页支持 -* 自动注册相关类:Converter、GenericConverter、Formatter -* 内容协商处理器:HttpMessageConverters -* 国际化:MessageCodesResolver -开发规范: -* 使用 `@Configuration` + `WebMvcConfigurer` 自定义规则,不使用 `@EnableWebMvc` 注解 -* 声明 `WebMvcRegistrations` 的实现类改变默认底层组件 -* 使用 `@EnableWebMvc` + `@Configuration` + `DelegatingWebMvcConfiguration` 全面接管 SpringMVC +*** -**** +#### 定点通知 +动态刷新情况下,只通知指定的微服务,比如只通知 3355 服务,不通知 3366,指定具体某一个实例生效,而不是全部 +公式:`http://localhost:port/actuator/bus-refresh/{destination}` -### 静态资源 +/bus/refresh 请求不再发送到具体的服务实例上,而是发给 Config Server 并通过 destination 参数类指定需要更新配置的服务或实例 -#### 访问规则 +![Cloud-Bus工作流程](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Cloud-Bus工作流程.png) -默认的静态资源路径是 classpath 下的,优先级由高到低为:/META-INF/resources、/resources、 /static、/public 的包内,`/` 表示当前项目的根路径 -静态映射 `/**` ,表示请求 `/ + 静态资源名` 就直接去默认的资源路径寻找请求的资源 -处理原理:静请求去寻找 Controller 处理,不能处理的请求就会交给静态资源处理器,静态资源也找不到就响应 404 页面 -* 修改默认资源路径: - ```yaml - spring: - web: - resources: - static-locations:: [classpath:/haha/] - ``` +**** -* 修改静态资源访问前缀,默认是 `/**`: - ```yaml - spring: - mvc: - static-path-pattern: /resources/** - ``` - 访问 URL:http://localhost:8080/resources/ + 静态资源名,将所有资源**重定位**到 `/resources/` -* webjar 访问资源: - ```xml - - org.webjars - jquery - 3.5.1 - - ``` +### Stream - 访问地址:http://localhost:8080/webjars/jquery/3.5.1/jquery.js,后面地址要按照依赖里面的包路径 +#### 基本介绍 +Spring Cloud Stream 是一个构建消息驱动微服务的框架,通过定义绑定器 Binder 作为中间层,实现了应用程序与消息中间件细节之间的隔离,屏蔽底层消息中间件的差异,降低切换成本,统一消息的编程模型 +Stream 中的消息通信方式遵循了发布订阅模式,Binder 可以生成 Binding 用来绑定消息容器的生产者和消费者,Binding 有两种类型 Input 和 Output,Input 对应于消费者(消费者从 Stream 接收消息),Output 对应于生产者(生产者从 Stream 发布消息) -**** +![Cloud-Stream工作流程](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Cloud-Stream工作流程.png) +- Binder:连接中间件 +- Channel:通道,是队列 Queue 的一种抽象,在消息通讯系统中实现存储和转发的媒介,通过 Channel 对队列进行配置 +- Source、Sink:生产者和消费者 -#### 欢迎页面 -静态资源路径下 index.html 默认作为欢迎页面,访问 http://localhost:8080 出现该页面,使用 welcome page 功能不能修改前缀 +中文手册:https://m.wang1314.com/doc/webapp/topic/20971999.html -网页标签上的小图标可以自定义规则,把资源重命名为 favicon.ico 放在静态资源目录下即可 +**** -*** +#### 基本使用 -#### 源码分析 +Binder 是应用与消息中间件之间的封装,目前实现了 Kafka 和 RabbitMQ 的 Binder,可以动态的改变消息类型(Kafka 的 Topic 和 RabbitMQ 的 Exchange),可以通过配置文件实现,常用注解如下: -SpringMVC 功能的自动配置类 WebMvcAutoConfiguration: +* @Input:标识输入通道,接收的消息通过该通道进入应用程序 +* @Output:标识输出通道,发布的消息通过该通道离开应用程序 +* @StreamListener:监听队列,用于消费者队列的消息接收 +* @EnableBinding:信道 Channel 和 Exchange 绑定 -```java -public class WebMvcAutoConfiguration { - //当前项目的根路径 - private static final String SERVLET_LOCATION = "/"; -} -``` +生产者发消息模块: -* 内部类 WebMvcAutoConfigurationAdapter: +* 引入 pom 依赖:RabbitMQ - ```java - @Import(EnableWebMvcConfiguration.class) - // 绑定 spring.mvc、spring.web、spring.resources 相关的配置属性 - @EnableConfigurationProperties({ WebMvcProperties.class,ResourceProperties.class, WebProperties.class }) - @Order(0) - public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer, ServletContextAware { - //有参构造器所有参数的值都会从容器中确定 - public WebMvcAutoConfigurationAdapter(/*参数*/) { - this.resourceProperties = resourceProperties.hasBeenCustomized() ? resourceProperties - : webProperties.getResources(); - this.mvcProperties = mvcProperties; - this.beanFactory = beanFactory; - this.messageConvertersProvider = messageConvertersProvider; - this.resourceHandlerRegistrationCustomizer = resourceHandlerRegistrationCustomizerProvider.getIfAvailable(); - this.dispatcherServletPath = dispatcherServletPath; - this.servletRegistrations = servletRegistrations; - this.mvcProperties.checkConfiguration(); - } - } + ```xml + + org.springframework.cloud + spring-cloud-starter-stream-rabbit + ``` - * ResourceProperties resourceProperties:获取和 spring.resources 绑定的所有的值的对象 - * WebMvcProperties mvcProperties:获取和 spring.mvc 绑定的所有的值的对象 - * ListableBeanFactory beanFactory:Spring 的 beanFactory - * HttpMessageConverters:找到所有的 HttpMessageConverters - * ResourceHandlerRegistrationCustomizer:找到 资源处理器的自定义器。 - * DispatcherServletPath:项目路径 - * ServletRegistrationBean:给应用注册 Servlet、Filter +* application.yml: -* WebMvcAutoConfiguration.WebMvcAutoConfigurationAdapter.addResourceHandler():两种静态资源映射规则 + ```yaml + server: + port: 8801 + + spring: + application: + name: cloud-stream-provider + cloud: + stream: + binders: # 在此处配置要绑定的rabbitmq的服务信息; + defaultRabbit: # 表示定义的名称,用于于binding整合 + type: rabbit # 消息组件类型 + environment: # 设置rabbitmq的相关的环境配置 + spring: + rabbitmq: + host: localhost + port: 5672 + username: guest + password: guest + bindings: # 服务的整合处理 + output: # 这个名字是一个通道的名称 + destination: studyExchange # 表示要使用的Exchange名称定义 + content-type: application/json # 设置消息类型,本次为json,文本则设置“text/plain” + binder: defaultRabbit # 设置要绑定的消息服务的具体设置 + + eureka: + client: # 客户端进行Eureka注册的配置 + service-url: + defaultZone: http://localhost:7001/eureka,http://localhost:7002/eureka + instance: + lease-renewal-interval-in-seconds: 2 # 设置心跳的时间间隔(默认是30秒) + lease-expiration-duration-in-seconds: 5 # 如果现在超过了5秒的间隔(默认是90秒) + instance-id: send-8801.com # 在信息列表时显示主机名称 + prefer-ip-address: true # 访问的路径变为IP地址 + ``` + +* 主启动类: ```java - public void addResourceHandlers(ResourceHandlerRegistry registry) { - //配置文件设置 spring.resources.add-mappings: false,禁用所有静态资源 - if (!this.resourceProperties.isAddMappings()) { - logger.debug("Default resource handling disabled");//被禁用 - return; + @SpringBootApplication + @EnableEurekaClient + public class StreamMQMain8801 { + public static void main(String[] args) { + SpringApplication.run(StreamMQMain8801.class, args); } - //注册webjars静态资源的映射规则 映射 路径 - addResourceHandler(registry, "/webjars/**", "classpath:/META-INF/resources/webjars/"); - //注册静态资源路径的映射规则 默认映射 staticPathPattern = "/**" - addResourceHandler(registry, this.mvcProperties.getStaticPathPattern(), (registration) -> { - //staticLocations = CLASSPATH_RESOURCE_LOCATIONS - registration.addResourceLocations(this.resourceProperties.getStaticLocations()); - if (this.servletContext != null) { - ServletContextResource resource = new ServletContextResource(this.servletContext, SERVLET_LOCATION); - registration.addResourceLocations(resource); - } - }); } ``` +* 业务类:MessageChannel 的实例名必须是 output,否则无法启动 + ```java - @ConfigurationProperties("spring.web") - public class WebProperties { - public static class Resources { - //默认资源路径,优先级从高到低 - static final String[] CLASSPATH_RESOURCE_LOCATIONS = { "classpath:/META-INF/resources/", - "classpath:/resources/", - "classpath:/static/", "classpath:/public/" } - private String[] staticLocations = CLASSPATH_RESOURCE_LOCATIONS; - //可以进行规则重写 - public void setStaticLocations(String[] staticLocations) { - this.staticLocations = appendSlashIfNecessary(staticLocations); - this.customized = true; - } + // 可以理解为定义消息的发送管道Source对应output(生产者),Sink对应input(消费者) + @EnableBinding(Source.class) + // @Service:这里不需要,不是传统的controller调用service。这个service是和rabbitMQ打交道的 + // IMessageProvider 只有一个 send 方法的接口 + public class MessageProviderImpl implements IMessageProvider { + @Resource + private MessageChannel output; // 消息的发送管道 + + @Override + public String send() { + String serial = UUID.randomUUID().toString(); + + //创建消息,通过output这个管道向消息中间件发消息 + this.output.send(MessageBuilder.withPayload(serial).build()); + System.out.println("***serial: " + serial); + return serial; } } ``` -* WebMvcAutoConfiguration.EnableWebMvcConfiguration.welcomePageHandlerMapping():欢迎页 +* Controller: ```java - //spring.web 属性 - @EnableConfigurationProperties(WebProperties.class) - public static class EnableWebMvcConfiguration { - @Bean - public WelcomePageHandlerMapping welcomePageHandlerMapping(/*参数*/) { - WelcomePageHandlerMapping welcomePageHandlerMapping = new WelcomePageHandlerMapping( - new TemplateAvailabilityProviders(applicationContext), - applicationContext, getWelcomePage(), - //staticPathPattern = "/**" - this.mvcProperties.getStaticPathPattern()); - return welcomePageHandlerMapping; - } - } - WelcomePageHandlerMapping(/*参数*/) { - //所以限制 staticPathPattern 必须为 /** 才能启用该功能 - if (welcomePage != null && "/**".equals(staticPathPattern)) { - logger.info("Adding welcome page: " + welcomePage); - //重定向 - setRootViewName("forward:index.html"); - } - else if (welcomeTemplateExists(templateAvailabilityProviders, applicationContext)) { - logger.info("Adding welcome page template: index"); - setRootViewName("index"); + @RestController + public class SendMessageController { + @Resource + private IMessageProvider messageProvider; + + @GetMapping(value = "/sendMessage") + public String sendMessage() { + return messageProvider.send(); } } ``` - WelcomePageHandlerMapping,访问 / 能访问到 index.html - - - -*** - - - -### Rest映射 - -开启 Rest 功能 - -```yaml -spring: - mvc: - hiddenmethod: - filter: - enabled: true #开启页面表单的Rest功能 -``` +消费者模块:8802 和 8803 两个消费者 -源码分析,注入了 HiddenHttpMethodFilte 解析 Rest 风格的访问: +* application.yml:只标注出与生产者不同的地方 -```java -public class WebMvcAutoConfiguration { - @Bean - @ConditionalOnMissingBean(HiddenHttpMethodFilter.class) - @ConditionalOnProperty(prefix = "spring.mvc.hiddenmethod.filter", name = "enabled") - public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() { - return new OrderedHiddenHttpMethodFilter(); - } -} -``` + ```yaml + server: + port: 8802 + + spring: + application: + name: cloud-stream-consumer + cloud: + stream: + # ... + bindings: # 服务的整合处理 + input: # 这个名字是一个通道的名称 + # ... + binder: { defaultRabbit } # 设置要绑定的消息服务的具体设置 + + eureka: + # ... + instance: + # ... + instance-id: receive-8802.com # 在信息列表时显示主机名称 + ``` -详细源码解析:SpringMVC → 基本操作 → Restful → 识别原理 +* Controller: -Web 部分源码详解:SpringMVC → 运行原理 + ```java + @Component + @EnableBinding(Sink.class) // 理解为定义一个消息消费者的接收管道 + public class ReceiveMessageListener { + @Value("${server.port}") + private String serverPort; + + @StreamListener(Sink.INPUT) //输入源:作为一个消息监听者 + public void input(Message message) { + // 获取到消息 + String messageStr = message.getPayload(); + System.out.println("消费者1号,------->接收到的消息:" + messageStr + "\t port: " + serverPort); + } + } + ``` @@ -15625,95 +18385,50 @@ Web 部分源码详解:SpringMVC → 运行原理 -### 内嵌容器 +#### 高级特性 -SpringBoot 嵌入式 Servlet 容器,默认支持的 webServe:Tomcat、Jetty、Undertow +重复消费问题:生产者 8801 发送一条消息后,8802 和 8803 会同时收到 8801 的消息 -配置方式: +解决方法:微服务应用放置于同一个 group 中,能够保证消息只会被其中一个应用消费一次。不同的组是可以全面消费的(重复消费),同一个组内的多个消费者会发生竞争关系,只有其中一个可以消费 -```xml - - org.springframework.boot - spring-boot-starter-web - - - org.springframework.boot - spring-boot-starter-tomcat - - - - - org.springframework.boot - spring-boot-starter-jetty - +```yaml +bindings: + input: + destination: studyExchange + content-type: application/json + binder: { defaultRabbit } + group: seazean # 设置分组 ``` -Web 应用启动,SpringBoot 导入 Web 场景包 tomcat,创建一个 Web 版的 IOC 容器: +消息持久化问题: -* `SpringApplication.run(BootApplication.class, args)`:应用启动 +* 停止 8802/8803 并去除掉 8802 的分组 group: seazean,8801 先发送 4 条消息到 MQ +* 先启动 8802,无分组属性配置,后台没有打出来消息,消息丢失 +* 再启动 8803,有分组属性配置,后台打印出来了 MQ 上的消息 -* `ConfigurableApplicationContext.run()`: - * `context = createApplicationContext()`:**创建容器** - * `applicationContextFactory = ApplicationContextFactory.DEFAULT` - ```java - ApplicationContextFactory DEFAULT = (webApplicationType) -> { - try { - switch (webApplicationType) { - case SERVLET: - // Servlet 容器,继承自 ServletWebServerApplicationContext - return new AnnotationConfigServletWebServerApplicationContext(); - case REACTIVE: - // 响应式编程 - return new AnnotationConfigReactiveWebServerApplicationContext(); - default: - // 普通 Spring 容器 - return new AnnotationConfigApplicationContext(); - } - } catch (Exception ex) { - throw new IllegalStateException(); - } - } - ``` - * `applicationContextFactory.create(this.webApplicationType)`:根据应用类型创建容器 +***** - * `refreshContext(context)`:容器启动刷新 -内嵌容器工作流程: -- Spring 容器启动逻辑中,在实例化非懒加载的单例 Bean 之前有一个方法 **onRefresh()**,留给子类去扩展,Web 容器就是重写这个方法创建 WebServer - ```java - protected void onRefresh() { - //省略.... - createWebServer(); - } - private void createWebServer() { - ServletWebServerFactory factory = getWebServerFactory(); - this.webServer = factory.getWebServer(getSelfInitializer()); - createWebServer.end(); - } - ``` - 获取 WebServer 工厂 ServletWebServerFactory,并且获取的数量不等于 1 会报错,Spring 底层有三种: +### Sleuth - `TomcatServletWebServerFactory`、`JettyServletWebServerFactory`、`UndertowServletWebServerFactory` +#### 基本介绍 -- **自动配置类 ServletWebServerFactoryAutoConfiguration** 导入了 ServletWebServerFactoryConfiguration(配置类),根据条件装配判断系统中到底导入了哪个 Web 服务器的包,创建出服务器并启动 +Spring Cloud Sleuth 提供了一套完整的分布式请求链路跟踪的解决方案,并且兼容支持了 zipkin + +在微服务框架中,一个客户端发起的请求在后端系统中会经过多次不同的服务节点调用来协同产生最后的请求结果,形成一条复杂的分布式服务调用链路,链路中的任何一环出现高延时或错误都会引起整个请求最后的失败,所以需要链路追踪 -- 默认是 web-starter 导入 tomcat 包,容器中就有 TomcatServletWebServerFactory,创建出 Tomcat 服务器并启动, - ```java - public TomcatWebServer(Tomcat tomcat, boolean autoStart, Shutdown shutdown) { - // 初始化 - initialize(); - } - ``` - 初始化方法 initialize 中有启动方法:`this.tomcat.start()` +Sleuth 官网:https://github.com/spring-cloud/spring-cloud-sleuth + +zipkin 下载地址:https://repo1.maven.org/maven2/io/zipkin/java/zipkin-server/ @@ -15721,462 +18436,472 @@ Web 应用启动,SpringBoot 导入 Web 场景包 tomcat,创建一个 Web 版 -### 自定义 +#### 链路监控 -#### 定制规则 +Sleuth 负责跟踪整理,zipkin 负责可视化展示 -```java -@Configuration -public class MyWebMvcConfigurer implements WebMvcConfigurer { - @Bean - public WebMvcConfigurer webMvcConfigurer() { - return new WebMvcConfigurer() { - //进行一些方法重写,来实现自定义的规则 - //比如添加一些解析器和拦截器,就是对原始容器功能的增加 - } - } - //也可以不加 @Bean,直接从这里重写方法进行功能增加 -} +```bash +java -jar zipkin-server-2.12.9-exec.jar # 启动 zipkin ``` +访问 http://localhost:9411/zipkin/ 展示交互界面 +一条请求链路通过 Trace ID 唯一标识,Span 标识发起的请求信息 -*** +* Trace:类似于树结构的 Span 集合,表示一条调用链路,存在唯一 ID 标识 +* Span:表示调用链路来源,通俗的理解 Span 就是一次请求信息,各个 Span 通过 ParentID 关联起来 +服务生产者模块: -#### 定制容器 +* 引入 pom 依赖: -@EnableWebMvc:全面接管 SpringMVC,所有规则全部自己重新配置 + ```xml + + + org.springframework.cloud + spring-cloud-starter-zipkin + + ``` -- @EnableWebMvc + WebMvcConfigurer + @Bean 全面接管SpringMVC +* application.yml: -- @Import(DelegatingWebMvcConfiguration.**class**),该类继承 WebMvcConfigurationSupport,自动配置了一些非常底层的组件,只能保证 SpringMVC 最基本的使用 + ```yaml + server: + port: 8001 + + spring: + application: + name: cloud-payment-service + zipkin: + base-url: http://localhost:9411 + sleuth: + sampler: + #采样率值介于 0 到 1 之间,1 则表示全部采集 + probability: 1 + ``` -原理:自动配置类 **WebMvcAutoConfiguration** 里面的配置要能生效,WebMvcConfigurationSupport 类不能被加载,所以 @EnableWebMvc 导致配置类失效,从而接管了 SpringMVC +* 业务类: -```java -@ConditionalOnMissingBean(WebMvcConfigurationSupport.class) -public class WebMvcAutoConfiguration {} -``` + ```java + @GetMapping("/payment/zipkin") + public String paymentZipkin() { + return "hi ,i'am paymentzipkin server fall back,welcome to seazean"; + } + ``` -注意:一般不适用此注解 +服务消费者模块: +* application.yml: + ```yaml + server: + port: 80 + + # 微服务名称 + spring: + application: + name: cloud-order-service + zipkin: + base-url: http://localhost:9411 + sleuth: + sampler: + probability: 1 + ``` +* 业务类: + ```java + @GetMapping("/comsumer/payment/zipkin") + public String paymentZipKin() { + String result = restTemplate.getForObject("http://localhost:8001" + "/payment/zipkin/", String.class); + return result; + } + ``` -*** -## 数据访问 +**** -### JDBC -#### 基本使用 -导入 starter: -```xml - - - org.springframework.boot - spring-boot-starter-data-jdbc - - - - mysql - mysql-connector-java - - -``` -单独导入 MySQL 驱动是因为不确定用户使用的什么数据库 +## Alibaba -配置文件: +Spring Cloud Alibaba 致力于提供微服务开发的一站式解决方案,此项目包含开发分布式应用微服务的必需组件,方便开发者通过 Spring Cloud 编程模型轻松使用这些组件来开发分布式应用服务 -```yaml -spring: - datasource: - url: jdbc:mysql://192.168.0.107:3306/db1?useSSL=false # 不加 useSSL 会警告 - username: root - password: 123456 - driver-class-name: com.mysql.jdbc.Driver -``` +- 服务限流降级:默认支持 WebServlet、WebFlux、OpenFeign、RestTemplate、Spring Cloud Gateway、Zuul、Dubbo 和 RocketMQ 限流降级功能的接入,可以在运行时通过控制台实时修改限流降级规则,还支持查看限流降级 Metrics 监控。 +- 服务注册与发现:适配 Spring Cloud 服务注册与发现标准,默认集成了 Ribbon 的支持 +- 分布式配置管理:支持分布式系统中的外部化配置,配置更改时自动刷新 +- 消息驱动能力:基于 Spring Cloud Stream 为微服务应用构建消息驱动能力 +- 分布式事务:使用 @GlobalTransactional 注解, 高效并且对业务零侵入地解决分布式事务问题 +- 阿里云对象存储:阿里云提供的海量、安全、低成本、高可靠的云存储服务 +- 分布式任务调度:提供秒级、精准、高可靠、高可用的定时(基于 Cron 表达式)任务调度服务。同时提供分布式的任务执行模型,如网格任务。网格任务支持海量子任务均匀分配到所有 Worker(schedulerx-client)上执行 -测试文件: +官方文档:https://github.com/alibaba/spring-cloud-alibaba/blob/master/README-zh.md -```java -@Slf4j -@SpringBootTest -class Boot05WebAdminApplicationTests { +官方手册:https://spring-cloud-alibaba-group.github.io/github-pages/greenwich/spring-cloud-alibaba.html - @Autowired - JdbcTemplate jdbcTemplate; - @Test - void contextLoads() { - Long res = jdbcTemplate.queryForObject("select count(*) from account_tbl", Long.class); - log.info("记录总数:{}", res); - } -} -``` +### Nacos +#### 基本介绍 -**** +Nacos 全称 Dynamic Naming and Configuration Service,一个更易于构建云原生应用的动态服务发现、配置管理和服务的管理平台,Nacos = Eureka + Config + Bus +下载地址:https://github.com/alibaba/nacos/releases +启动命令:命令运行成功后直接访问 http://localhost:8848/nacos,默认账号密码都是 nacos -#### 自动配置 +```bash +startup.cmd -m standalone # standalone 代表着单机模式运行,非集群模式 +``` -DataSourceAutoConfiguration:数据源的自动配置 +关闭命令: -```java -@Configuration(proxyBeanMethods = false) -@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class }) -@EnableConfigurationProperties(DataSourceProperties.class) -public class DataSourceAutoConfiguration { - - @Conditional(PooledDataSourceCondition.class) - @ConditionalOnMissingBean({ DataSource.class, XADataSource.class }) - @Import({ DataSourceConfiguration.Hikari.class, DataSourceConfiguration.Tomcat.class, - DataSourceConfiguration.Dbcp2.class, DataSourceConfiguration.OracleUcp.class}) - protected static class PooledDataSourceConfiguration {} -} -// 配置项 -@ConfigurationProperties(prefix = "spring.datasource") -public class DataSourceProperties implements BeanClassLoaderAware, InitializingBean {} +```bash +shutdown.cmd ``` -- 底层默认配置好的连接池是:**HikariDataSource** -- 数据库连接池的配置,是容器中没有 DataSource 才自动配置的 -- 修改数据源相关的配置:spring.datasource +注册中心对比:C 一致性,A 可用性,P 分区容错性 -相关配置: +| 注册中心 | CAP 模型 | 控制台管理 | +| :-------: | :------: | :--------: | +| Eureka | AP | 支持 | +| Zookeeper | CP | 不支持 | +| Consul | CP | 支持 | +| Nacos | AP | 支持 | -- DataSourceTransactionManagerAutoConfiguration: 事务管理器的自动配置 -- JdbcTemplateAutoConfiguration: JdbcTemplate 的自动配置 - - 可以修改这个配置项 @ConfigurationProperties(prefix = **"spring.jdbc"**) 来修改JdbcTemplate - - `@AutoConfigureAfter(DataSourceAutoConfiguration.class)`:在 DataSource 装配后装配 -- JndiDataSourceAutoConfiguration: jndi 的自动配置 -- XADataSourceAutoConfiguration: 分布式事务相关 +切换模式:`curl -X PUT '$NACOS_SERVER:8848/nacos/v1/ns/operator/switches?entry=serverMode&value=CP` +官网:https://nacos.io + **** -### Druid +#### 注册中心 -导入坐标: +Nacos 作为服务注册中心 -```xml - - com.alibaba - druid-spring-boot-starter - 1.1.17 - -``` +服务提供者: -```java -@Configuration -@ConditionalOnClass(DruidDataSource.class) -@AutoConfigureBefore(DataSourceAutoConfiguration.class) -@EnableConfigurationProperties({DruidStatProperties.class, DataSourceProperties.class}) -@Import({DruidSpringAopConfiguration.class, - DruidStatViewServletConfiguration.class, - DruidWebStatFilterConfiguration.class, - DruidFilterConfiguration.class}) -public class DruidDataSourceAutoConfigure {} -``` +* 引入 pom 依赖: -自动配置: + ```xml + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + + ``` -- 扩展配置项 **spring.datasource.druid** -- DruidSpringAopConfiguration: 监控 SpringBean,配置项为 `spring.datasource.druid.aop-patterns` +* application.yml: -- DruidStatViewServletConfiguration:监控页的配置项为 `spring.datasource.druid.stat-view-servlet`,默认开启 -- DruidWebStatFilterConfiguration:Web 监控配置项为 `spring.datasource.druid.web-stat-filter`,默认开启 + ```yaml + server: + port: 9001 + + spring: + application: + name: nacos-payment-provider + cloud: + nacos: + discovery: + server-addr: localhost:8848 #配置Nacos地址,注册到Nacos + # 做监控需要把这个全部暴露出来 + management: + endpoints: + web: + exposure: + include: '*' + ``` + +* 主启动类:注解是 EnableDiscoveryClient -- DruidFilterConfiguration:所有 Druid 自己 filter 的配置 + ```java + @EnableDiscoveryClient + @SpringBootApplication + public class PaymentMain9001 { + public static void main(String[] args) { + SpringApplication.run(PaymentMain9001.class, args); + } + } + ``` -配置示例: +* Controller: -```yaml -spring: - datasource: - url: jdbc:mysql://localhost:3306/db_account - username: root - password: 123456 - driver-class-name: com.mysql.jdbc.Driver + ```java + @RestController + public class PaymentController { + @Value("${server.port}") + private String serverPort; + + @GetMapping(value = "/payment/nacos/{id}") + public String getPayment(@PathVariable("id") Integer id) { + return "nacos registry, serverPort: " + serverPort + "\t id" + id; + } + } + ``` - druid: - aop-patterns: com.atguigu.admin.* #监控SpringBean - filters: stat,wall # 底层开启功能,stat(sql监控),wall(防火墙) +* 管理后台服务: - stat-view-servlet: # 配置监控页功能 - enabled: true - login-username: admin #项目启动访问:http://localhost:8080/druid ,账号和密码是 admin - login-password: admin - resetEnable: false + ![Cloud-Nacos服务列表](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Cloud-Nacos服务列表.png) - web-stat-filter: # 监控web - enabled: true - urlPattern: /* - exclusions: '*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*' +* 新建一个模块端口是 9002,其他与 9001 服务一样,nacos-payment-provider 的实例数就变为 2 +服务消费者: - filter: - stat: # 对上面filters里面的stat的详细配置 - slow-sql-millis: 1000 - logSlowSql: true - enabled: true - wall: - enabled: true - config: - drop-table-allow: false -``` +* application.yml: + ```yaml + server: + port: 83 + + spring: + application: + name: nacos-order-consumer + cloud: + nacos: + discovery: + server-addr: localhost:8848 + + # 消费者将要去访问的微服务名称(注册成功进nacos的微服务提供者) + service-url: + nacos-user-service: http://nacos-payment-provider + ``` +* 主启动类: -配置示例:https://github.com/alibaba/druid/tree/master/druid-spring-boot-starter + ```java + @SpringBootApplication + @EnableDiscoveryClient + public class OrderNacosMain83 { + public static void main(String[] args) { + SpringApplication.run(OrderNacosMain83.class, args); + } + } + ``` -配置项列表:https://github.com/alibaba/druid/wiki/DruidDataSource%E9%85%8D%E7%BD%AE%E5%B1%9E%E6%80%A7%E5%88%97%E8%A1%A8 +* 业务类: + ```java + @Configuration + public class ApplicationContextBean { + @Bean + @LoadBalanced // 生产者集群状态下,必须添加,防止找不到实例 + public RestTemplate getRestTemplate() { + return new RestTemplate(); + } + } + ``` + ```java + @RestController + @Slf4j + public class OrderNacosController { + @Resource + private RestTemplate restTemplate; + // 从配置文件中读取 URL + @Value("${service-url.nacos-user-service}") + private String serverURL; + + @GetMapping("/consumer/payment/nacos/{id}") + public String paymentInfo(@PathVariable("id") Long id) { + String result = restTemplate.getForObject(serverURL + "/payment/nacos/" + id, String.class); + return result; + } + } + ``` -**** + +*** -### MyBatis -#### 基本使用 +#### 配置中心 -导入坐标: +##### 基础配置 -```xml - - org.mybatis.spring.boot - mybatis-spring-boot-starter - 2.1.4 - -``` +把配置文件写进 Nacos,然后再用 Nacos 做 config 这样的功能,直接从 Nacos 上抓取服务的配置信息 -* 编写 MyBatis 相关配置:application.yml +在 Nacos 中,dataId 的完整格式如下 `${prefix}-${spring.profiles.active}.${file-extension}` - ```yaml - # 配置mybatis规则 - mybatis: - # config-location: classpath:mybatis/mybatis-config.xml 建议不写 - mapper-locations: classpath:mybatis/mapper/*.xml - configuration: - map-underscore-to-camel-case: true - - #可以不写全局配置文件,所有全局配置文件的配置都放在 configuration 配置项中即可 - ``` +* `prefix`:默认为 `spring.application.name` 的值,也可以通过配置项 `spring.cloud.nacos.config.prefix` 来配置 +* `spring.profiles.active`:当前环境对应的 profile,当该值为空时,dataId 的拼接格式变成 `${prefix}.${file-extension}` +* `file-exetension`:配置内容的数据格式,可以通过配置项 `spring.cloud.nacos.config.file-extension` 来配置,目前只支持 properties 和 yaml 类型(不是 yml) -* 定义表和实体类 +构建流程: - ```java - public class User { - private int id; - private String username; - private String password; - } +* 引入 pom 依赖: + + ```xml + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-config + ``` -* 编写 dao 和 mapper 文件/纯注解开发 +* 配置两个 yml 文件:配置文件的加载是存在优先级顺序的,bootstrap 优先级高于 application - dao:**@Mapper 注解必须加,使用自动装配的 package,否则在启动类指定 @MapperScan() 扫描路径(不建议)** + bootstrap.yml:全局配置 - ```java - @Mapper //必须加Mapper - @Repository - public interface UserXmlMapper { - public List findAll(); - } + ```yaml + # nacos配置 + server: + port: 3377 + + spring: + application: + name: nacos-config-client + cloud: + nacos: + discovery: + server-addr: localhost:8848 #Nacos服务注册中心地址 + config: + server-addr: localhost:8848 #Nacos作为配置中心地址 + file-extension: yaml #指定yaml格式的配置 + + # ${spring.application.name}-${spring.profile.active}.${spring.cloud.nacos.config.file-extension} ``` - mapper.xml + application.yml:服务独立配置,表示服务要去配置中心找名为 nacos-config-client-dev.yaml 的文件 - ```xml - - - - - + ```yaml + spring: + profiles: + active: dev # 表示开发环境 ``` -* 纯注解开发 +* 主启动类: ```java - @Mapper - @Repository - public interface UserMapper { - @Select("select * from t_user") - public List findAll(); + @SpringBootApplication + @EnableDiscoveryClient + public class NacosConfigClientMain3377 { + public static void main(String[] args) { + SpringApplication.run(NacosConfigClientMain3377.class, args); + } } ``` +* 业务类:@RefreshScope 注解使当前类下的配置支持 Nacos 的动态刷新功能 + ```java + @RestController + @RefreshScope + public class ConfigClientController { + @Value("${config.info}") + private String configInfo; + + @GetMapping("/config/info") + public String getConfigInfo() { + return configInfo; + } + } + ``` -**** - - +* 新增配置,然后访问 http://localhost:3377/config/info -#### 自动配置 + ![Cloud-Nacos新增配置](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Cloud-Nacos新增配置.png) -MybatisAutoConfiguration: -```java -@EnableConfigurationProperties(MybatisProperties.class) //MyBatis配置项绑定类。 -@AutoConfigureAfter({ DataSourceAutoConfiguration.class, MybatisLanguageDriverAutoConfiguration.class }) -public class MybatisAutoConfiguration { - @Bean - @ConditionalOnMissingBean - public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception { - SqlSessionFactoryBean factory = new SqlSessionFactoryBean(); - return factory.getObject(); - } - - @org.springframework.context.annotation.Configuration - @Import(AutoConfiguredMapperScannerRegistrar.class) - @ConditionalOnMissingBean({ MapperFactoryBean.class, MapperScannerConfigurer.class }) - public static class MapperScannerRegistrarNotFoundConfiguration implements InitializingBean {} -} -@ConfigurationProperties(prefix = "mybatis") -public class MybatisProperties {} -``` +*** -* 配置文件:`mybatis` -* 自动配置了 SqlSessionFactory -* 导入 `AutoConfiguredMapperScannerRegistra` 实现 @Mapper 的扫描 +##### 分类配置 -**** +分布式开发中的多环境多项目管理问题,Namespace 用于区分部署环境,Group 和 DataID 逻辑上区分两个目标对象 +![Cloud-Nacos配置说明](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Cloud-Nacos配置说明.png) +Namespace 默认 public,主要用来实现隔离,图示三个开发环境 -#### MyBatis-Plus +Group 默认是 DEFAULT_GROUP,Group 可以把不同的微服务划分到同一个分组里面去 -```xml - - com.baomidou - mybatis-plus-boot-starter - 3.4.1 - -``` -自动配置类:MybatisPlusAutoConfiguration -只需要 Mapper 继承 **BaseMapper** 就可以拥有 CRUD 功能 +*** -*** +##### 加载配置 +DataID 方案:指定 `spring.profile.active` 和配置文件的 DataID 来使不同环境下读取不同的配置 +Group 方案:通过 Group 实现环境分区,在 config 下增加一条 Group 的配置即可 -### Redis +Namespace 方案: -#### 基本使用 +```yaml +server: + port: 3377 -```xml - - org.springframework.boot - spring-boot-starter-data-redis - +spring: + application: + name: nacos-config-client + cloud: + nacos: + discovery: + server-addr: localhost:8848 #Nacos服务注册中心地址 + config: + server-addr: localhost:8848 #Nacos作为配置中心地址 + file-extension: yaml #指定yaml格式的配置 + group: DEV_GROUP + namespace: 95d44530-a4a6-4ead-98c6-23d192cee298 ``` -* 配置redis相关属性 +![Cloud-Nacos命名空间](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Cloud-Nacos命名空间.png) - ```yaml - spring: - redis: - host: 127.0.0.1 # redis的主机ip - port: 6379 - ``` -* 注入 RedisTemplate 模板 - ```java - @RunWith(SpringRunner.class) - @SpringBootTest - public class SpringbootRedisApplicationTests { - @Autowired - private RedisTemplate redisTemplate; - - @Test - public void testSet() { - //存入数据 - redisTemplate.boundValueOps("name").set("zhangsan"); - } - @Test - public void testGet() { - //获取数据 - Object name = redisTemplate.boundValueOps("name").get(); - System.out.println(name); - } - } - ``` +**** -**** +#### 集群架构 +集群部署参考官方文档,Nacos 支持的三种部署模式: +- 单机模式:用于测试和单机使用 +- 集群模式:用于生产环境,确保高可用 +- 多集群模式:用于多数据中心场景 -#### 自动配置 +集群部署文档:https://nacos.io/zh-cn/docs/v2/guide/admin/cluster-mode-quick-start.html -RedisAutoConfiguration 自动配置类 +默认 Nacos 使用嵌入式数据库 derby 实现数据的存储,重启 Nacos 后配置文件不会消失,但是多个 Nacos 节点数据存储存在一致性问题,每个 Nacos 都有独立的嵌入式数据库,所以 Nacos 采用了集中式存储的方式来支持集群化部署,目前只支持 MySQL 的存储 -```java -@Configuration(proxyBeanMethods = false) -@ConditionalOnClass(RedisOperations.class) -@EnableConfigurationProperties(RedisProperties.class) -@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class }) -public class RedisAutoConfiguration { - @Bean - @ConditionalOnMissingBean(name = "redisTemplate") - @ConditionalOnSingleCandidate(RedisConnectionFactory.class) - public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) { - RedisTemplate template = new RedisTemplate<>(); - template.setConnectionFactory(redisConnectionFactory); - return template; - } +Windows 下 Nacos 切换 MySQL 存储: - @Bean - @ConditionalOnMissingBean - @ConditionalOnSingleCandidate(RedisConnectionFactory.class) - public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) { - StringRedisTemplate template = new StringRedisTemplate(); - template.setConnectionFactory(redisConnectionFactory); - return template; - } +* 在 Nacos 安装目录的 conf 目录下找到一个名为 `nacos-mysql.sql` 的脚本并执行 -} -``` +* 在 conf 目录下找到 `application.properties`,增加如下数据 -- 配置项:`spring.redis` -- 自动导入了连接工厂配置类:LettuceConnectionConfiguration、JedisConnectionConfiguration + ```properties + spring.datasource.platform=mysql + + db.num=1 + db.url.0=jdbc:mysql://127.0.0.1:3306/nacos_config?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true + db.user=username + db.password=password + ``` -- 自动注入了模板类:RedisTemplate 、StringRedisTemplate,k v 都是 String 类型 +* 重新启动 Nacos,可以看到是个全新的空记录界面 -- 使用 @Autowired 注入模板类就可以操作 redis +Linux 参考:https://www.yuque.com/mrlinxi/pxvr4g/rnahsn#dPvMy @@ -16188,261 +18913,320 @@ public class RedisAutoConfiguration { -## 单元测试 +### Sentinel -### Junit5 +#### 基本介绍 -Spring Boot 2.2.0 版本开始引入 JUnit 5 作为单元测试默认库,由三个不同的子模块组成: +Sentinel 是面向分布式、多语言异构化服务架构的流量治理组件 -* JUnit Platform:在 JVM 上启动测试框架的基础,不仅支持 Junit 自制的测试引擎,其他测试引擎也可以接入 +Sentinel 分为两个部分: -* JUnit Jupiter:提供了 JUnit5 的新的编程模型,是 JUnit5 新特性的核心,内部包含了一个测试引擎,用于在 Junit Platform 上运行 +- 核心库(Java 客户端)不依赖任何框架/库,能够运行于 Java 8 及以上的版本的运行时环境 +- 控制台(Dashboard)主要负责管理推送规则、监控、管理机器信息等 -* JUnit Vintage:JUnit Vintage 提供了兼容 JUnit4.x、Junit3.x 的测试引擎 +下载到本地,运行命令:`java -jar sentinel-dashboard-1.8.2.jar` (要求 Java8,且 8080 端口不能被占用),访问 http://localhost:8080/,账号密码均为 sentinel - 注意:SpringBoot 2.4 以上版本移除了默认对 Vintage 的依赖,如果需要兼容 Junit4 需要自行引入 -```java -@SpringBootTest -class Boot05WebAdminApplicationTests { - @Test - void contextLoads() { - } -} -``` +官网:https://sentinelguard.io +下载地址:https://github.com/alibaba/Sentinel/releases -*** +**** -### 常用注解 -JUnit5 的注解如下: +#### 基本使用 -- @Test:表示方法是测试方法,但是与 JUnit4 的 @Test 不同,它的职责非常单一不能声明任何属性,拓展的测试将会由 Jupiter 提供额外测试,包是 `org.junit.jupiter.api.Test` -- @ParameterizedTest:表示方法是参数化测试 +构建演示工程: -- @RepeatedTest:表示方法可重复执行 -- @DisplayName:为测试类或者测试方法设置展示名称 +* 引入 pom 依赖: -- @BeforeEach:表示在每个单元测试之前执行 -- @AfterEach:表示在每个单元测试之后执行 + ```xml + + com.alibaba.cloud + spring-cloud-starter-alibaba-sentinel + + ``` -- @BeforeAll:表示在所有单元测试之前执行 -- @AfterAll:表示在所有单元测试之后执行 +* application.yml:sentinel.transport.port 端口配置会在应用对应的机器上启动一个 HTTP Server,该 Server 与 Sentinel 控制台做交互。比如 Sentinel 控制台添加了 1 个限流规则,会把规则数据 Push 给 Server 接收,Server 再将规则注册到 Sentinel 中 -- @Tag:表示单元测试类别,类似于 JUnit4 中的 @Categories -- @Disabled:表示测试类或测试方法不执行,类似于 JUnit4 中的 @Ignore + ```yaml + server: + port: 8401 + + spring: + application: + name: cloudalibaba-sentinel-service + cloud: + nacos: + discovery: + server-addr: localhost:8848 # Nacos 服务注册中心地址【需要启动Nacos8848】 + sentinel: + transport: + # 配置Sentinel dashboard地址 + dashboard: localhost:8080 + # 默认8719端口,假如被占用会自动从8719开始依次+1扫描,直至找到未被占用的端口 + port: 8719 + + management: + endpoints: + web: + exposure: + include: '*' + ``` + +* 主启动类: -- @Timeout:表示测试方法运行如果超过了指定时间将会返回错误 -- @ExtendWith:为测试类或测试方法提供扩展类引用 + ```java + @EnableDiscoveryClient + @SpringBootApplication + public class SentinelMainApp8401 { + public static void main(String[] args) { + SpringApplication.run(SentinelMainApp8401.class, args); + } + } + ``` +* 流量控制 Controller: + ```java + @RestController + @Slf4j + public class FlowLimitController { + @GetMapping("/testA") + public String testA() { + return "------testA"; + } + + @GetMapping("/testB") + public String testB() { + return "------testB"; + } + } + ``` -**** +* Sentinel 采用懒加载机制,需要先访问 http://localhost:8401/testA,控制台才能看到 -### 断言机制 +*** -#### 简单断言 -断言(assertions)是测试方法中的核心,用来对测试需要满足的条件进行验证,断言方法都是 org.junit.jupiter.api.Assertions 的静态方法 -用来对单个值进行简单的验证: +#### 流控规则 -| 方法 | 说明 | -| --------------- | ------------------------------------ | -| assertEquals | 判断两个对象或两个原始类型是否相等 | -| assertNotEquals | 判断两个对象或两个原始类型是否不相等 | -| assertSame | 判断两个对象引用是否指向同一个对象 | -| assertNotSame | 判断两个对象引用是否指向不同的对象 | -| assertTrue | 判断给定的布尔值是否为 true | -| assertFalse | 判断给定的布尔值是否为 false | -| assertNull | 判断给定的对象引用是否为 null | -| assertNotNull | 判断给定的对象引用是否不为 null | +流量控制规则 FlowRule:同一个资源可以同时有多个限流规则 + +* 资源名 resource:限流规则的作用对象,Demo 中为 testA +* 针对资源 limitApp:针对调用者进行限流,默认为 default 代表不区分调用来源 +* 阈值类型 grade:QPS 或线程数模式 +* 单机阈值 count:限流阈值 +* 流控模式 strategy:调用关系限流策略 + * 直接:资源本身达到限流条件直接限流 + * 关联:当关联的资源达到阈值时,限流自身 + * 链路:只记录指定链路上的流量,从入口资源进来的流量 +* 流控效果 controlBehavior: + * 快速失败:直接失败,抛出异常 + * Warm Up:冷启动,根据 codeFactory(冷加载因子,默认 3)的值,从 count/codeFactory 开始缓慢增加,给系统预热时间 + * 排队等待:匀速排队,让请求以匀速的方式通过,阈值类型必须设置为 QPS,否则无效 + +Cloud-Sentinel增加流控规则 + +通过调用 `SystemRuleManager.loadRules()` 方法来用硬编码的方式定义流量控制规则: ```java -@Test -@DisplayName("simple assertion") -public void simple() { - assertEquals(3, 1 + 2, "simple math"); - assertNull(null); - assertNotNull(new Object()); +private void initSystemProtectionRule() { + List rules = new ArrayList<>(); + SystemRule rule = new SystemRule(); + rule.setHighestSystemLoad(10); + rules.add(rule); + SystemRuleManager.loadRules(rules); } ``` -**** - +详细内容参考文档:https://sentinelguard.io/zh-cn/docs/flow-control.html -#### 数组断言 -通过 assertArrayEquals 方法来判断两个对象或原始类型的数组是否相等 +**** -```java -@Test -@DisplayName("array assertion") -public void array() { - assertArrayEquals(new int[]{1, 2}, new int[] {1, 2}); -} -``` +#### 降级熔断 -*** +Sentinel 熔断降级会在调用链路中某个资源出现不稳定状态时,对这个资源的调用进行限制,让请求快速失败,避免影响到其它的资源而导致级联错误。当资源被降级后,在接下来的降级时间窗口之内,对该资源的调用都自动熔断(默认行为是抛出 DegradeException) +Sentinel 提供以下几种熔断策略: +* 资源名 resource:限流规则的作用对象,Demo 中为 testA +* 熔断策略 grade: + * 慢调用比例(SLOW_REQUEST_RATIO):以慢调用比例作为阈值,需要设置允许的慢调用 RT(即最大的响应时间),请求的响应时间大于该值则统计为慢调用。当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并且慢调用的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求响应时间小于设置的慢调用 RT 则结束熔断,若大于设置的慢调用 RT 则会再次被熔断 + * 异常比例(ERROR_RATIO):当单位统计时长内请求数目大于设置的最小请求数目,并且异常的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态,若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。异常比率的阈值范围是 `[0.0, 1.0]`,代表 0% - 100% + * 异常数 (ERROR_COUNT):当单位统计时长内的异常数目超过阈值之后会自动进行熔断。经过熔断时长后熔断器会进入探测恢复状态,若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断 +* 单机阈值 count:慢调用比例模式下为慢调用临界 RT;异常比例/异常数模式下为对应的阈值 +* 熔断时长 timeWindow:单位为 s +* 最小请求数 minRequestAmount:熔断触发的最小请求数,请求数小于该值时即使异常比率超出阈值也不会熔断,默认 5 +* 统计时长 statIntervalMs:单位统计时长 +* 慢调用比例阈值 slowRatioThreshold:仅慢调用比例模式有效 -#### 组合断言 +Cloud-Sentinel增加熔断规则 -assertAll 方法接受多个 org.junit.jupiter.api.Executable 函数式接口的实例作为验证的断言,可以通过 lambda 表达式提供这些断言 +注意异常降级仅针对业务异常,对 Sentinel 限流降级本身的异常 BlockException 不生效,为了统计异常比例或异常数,需要通过 `Tracer.trace(ex)` 记录业务异常或者通过`@SentinelResource` 注解会自动统计业务异常 ```java -@Test -@DisplayName("assert all") -public void all() { - assertAll("Math", - () -> assertEquals(2, 1 + 1), - () -> assertTrue(1 > 0) - ); +Entry entry = null; +try { + entry = SphU.entry(resource); + + // Write your biz code here. + // <> +} catch (Throwable t) { + if (!BlockException.isBlockException(t)) { + Tracer.trace(t); + } +} finally { + if (entry != null) { + entry.exit(); + } } ``` -*** +详细内容参考文档:https://sentinelguard.io/zh-cn/docs/circuit-breaking.html -#### 异常断言 +**** -Assertions.assertThrows(),配合函数式编程就可以进行使用 -```java -@Test -@DisplayName("异常测试") -public void exceptionTest() { - ArithmeticException exception = Assertions.assertThrows( - //扔出断言异常 - ArithmeticException.class, () -> System.out.println(1 / 0) - ); -} -``` +#### 热点限流 +热点参数限流会统计传入参数中的热点参数,并根据配置的限流阈值与模式,对包含热点参数的资源调用进行限流,Sentinel 利用 LRU 策略统计最近最常访问的热点参数,结合令牌桶算法来进行参数级别的流控 -**** +Cloud-Sentinel热点参数限流 +引入 @SentinelResource 注解:https://sentinelguard.io/zh-cn/docs/annotation-support.html +- value:Sentinel 资源名,默认为请求路径,这里 value 的值可以任意写,但是约定与 Restful 地址一致 -#### 超时断言 +- blockHandler:表示触发了 Sentinel 中配置的流控规则,就会调用兜底方法 `del_testHotKey` -Assertions.assertTimeout() 为测试方法设置了超时时间 +- blockHandlerClass:如果设置了该值,就会去该类中寻找 blockHandler 方法 + +- fallback:用于在抛出异常的时候提供 fallback 处理逻辑 + + 说明:fallback 对应服务降级(服务出错了需要有个兜底方法),blockHandler 对应服务熔断(服务不可用的兜底方法) ```java -@Test -@DisplayName("超时测试") -public void timeoutTest() { - //如果测试方法时间超过1s将会异常 - Assertions.assertTimeout(Duration.ofMillis(1000), () -> Thread.sleep(500)); +@GetMapping("/testHotKey") +@SentinelResource(value = "testHotKey", blockHandler = "del_testHotKey") +public String testHotkey(@RequestParam(value = "p1", required = false) String p1, + @RequestParam(value = "p1", required = false) String p2) { + return "------testHotkey"; +} + +// 自定义的兜底方法,必须是 BlockException +public String del_testHotKey(String p1, String p2, BlockException e) { + return "不用默认的兜底提示 Blocked by Sentinel(flow limiting),自定义提示:del_testHotKeyo."; } ``` +图示设置 p1 参数限流,规则是 1s 访问 1 次,当 p1=5 时 QPS > 100,只访问 p2 不会出现限流 `http://localhost:8401/testHotKey?p2=b` +Cloud-Sentinel增加热点规则 -**** +* 参数索引 paramIdx:热点参数的索引,图中索引 0 对应方法中的 p1 参数 +* 参数例外项 paramFlowItemList:针对指定的参数值单独设置限流阈值,不受 count 阈值的限制,**仅支持基本类型和字符串类型** +说明:@SentinelResource 只管控制台配置规则问题,出现运行时异常依然会报错 -#### 快速失败 -通过 fail 方法直接使得测试失败 +详细内容参考文档:https://sentinelguard.io/zh-cn/docs/parameter-flow-control.html -```java -@Test -@DisplayName("fail") -public void shouldFail() { - fail("This should fail"); -} -``` +*** -*** +#### 系统规则 +Sentinel 系统自适应保护从整体维度对**应用入口流量**进行控制,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性 +系统规则支持以下的阈值类型: -### 前置条件 +- Load(仅对 Linux/Unix-like 机器生效):当系统 load1 超过阈值,且系统当前的并发线程数超过系统容量时才会触发系统保护,系统容量由系统的 `maxQps * minRt` 计算得出,设定参考值一般是 `CPU cores * 2.5` +- RT:当单台机器上所有入口流量的平均 RT 达到阈值即触发系统保护,单位是毫秒 +- 线程数:当单台机器上所有入口流量的并发线程数达到阈值即触发系统保护 +- 入口 QPS:当单台机器上所有入口流量的 QPS 达到阈值即触发系统保护 +- CPU usage:当系统 CPU 使用率超过阈值即触发系统保护(取值范围 0.0-1.0) -JUnit 5 中的前置条件(assumptions)类似于断言,不同之处在于**不满足的断言会使得测试方法失败**,而不满足的**前置条件只会使得测试方法的执行终止**,前置条件可以看成是测试方法执行的前提,当该前提不满足时,就没有继续执行的必要 +Cloud-Sentinel增加系统规则 -```java -@DisplayName("测试前置条件") -@Test -void testassumptions(){ - Assumptions.assumeTrue(false,"结果不是true"); - System.out.println("111111"); -} -``` +详细内容参考文档:https://sentinelguard.io/zh-cn/docs/system-adaptive-protection.html -*** +**** -### 嵌套测试 -JUnit 5 可以通过 Java 中的内部类和 @Nested 注解实现嵌套测试,从而可以更好的把相关的测试方法组织在一起,在内部类中可以使用 @BeforeEach 和 @AfterEach 注解,而且嵌套的层次没有限制 +#### 服务调用 -```java -@DisplayName("A stack") -class TestingAStackDemo { +消费者需要进行服务调用 - Stack stack; +* 引入 pom 依赖: - @Test - @DisplayName("is instantiated with new Stack()") - void isInstantiatedWithNew() { - assertNull(stack) - } + ```xml + + org.springframework.cloud + spring-cloud-starter-openfeign + + ``` - @Nested - @DisplayName("when new") - class WhenNew { +* application.yml:激活 Sentinel 对 Feign 的支持 - @BeforeEach - void createNewStack() { - stack = new Stack<>(); - } + ```yaml + feign: + sentinel: + enabled: true + ``` - @Test - @DisplayName("is empty") - void isEmpty() { - assertTrue(stack.isEmpty()); - } +* 主启动类:加上 @EnableFeignClient 注解开启 OpenFeign - @Test - @DisplayName("throws EmptyStackException when popped") - void throwsExceptionWhenPopped() { - assertThrows(EmptyStackException.class, stack::pop); - } - } -} -``` +* 业务类: + ```java + // 指明调用失败的兜底方法在PaymentFallbackService,使用 fallback 方式是无法获取异常信息的, + // 如果想要获取异常信息,可以使用 fallbackFactory 参数 + @FeignClient(value = "nacos-payment-provider", fallback = PaymentFallbackService.class) + public interface PaymentFeignService { + // 去生产则服务中找相应的接口,方法签名一定要和生产者中 controller 的一致 + @GetMapping(value = "/paymentSQL/{id}") + public CommonResult paymentSQL(@PathVariable("id") Long id); + } + + ``` + ```java + @Component //不要忘记注解,降级方法 + public class PaymentFallbackService implements PaymentFeignService { + @Override + public CommonResult paymentSQL(Long id) { + return new CommonResult<>(444,"服务降级返回,没有该流水信息-------PaymentFallbackSe + ``` @@ -16450,173 +19234,272 @@ class TestingAStackDemo { -### 参数测试 +#### 持久化 -参数化测试是 JUnit5 很重要的一个新特性,它使得用不同的参数多次运行测试成为了可能 +配置持久化: -利用**@ValueSource**等注解,指定入参,我们将可以使用不同的参数进行多次单元测试,而不需要每新增一个参数就新增一个单元测试,省去了很多冗余代码。 +* 引入 pom 依赖: -* @ValueSource:为参数化测试指定入参来源,支持八大基础类以及 String 类型、Class 类型 + ```xml + + + com.alibaba.csp + sentinel-datasource-nacos + + ``` -* @NullSource:表示为参数化测试提供一个 null 的入参 +* 添加 Nacos 数据源配置: -* @EnumSource:表示为参数化测试提供一个枚举入参 + ```yaml + server: + port: 8401 + + spring: + application: + name: cloudalibaba-sentinel-service + cloud: + nacos: + discovery: + server-addr: localhost:8848 #Nacos服务注册中心地址 + sentinel: + transport: + dashboard: localhost:8080 + port: 8719 + # 关闭默认收敛所有URL的入口context,不然链路限流不生效 + web-context-unify: false + # filter: + # enabled: false # 关闭自动收敛 + + #持久化配置 + datasource: + ds1: + nacos: + server-addr: localhost:8848 + dataId: cloudalibaba-sentinel-service + groupId: DEFAULT_GROUP + data-type: json + rule-type: flow + ``` -* @CsvFileSource:表示读取指定 CSV 文件内容作为参数化测试入参 -* @MethodSource:表示读取指定方法的返回值作为参数化测试入参(注意方法返回需要是一个流) +**** -*** +### Seata +#### 分布事物 -## 指标监控 +一个分布式事务过程,可以用分布式处理过程的一 ID + 三组件模型来描述: -### Actuator +* XID (Transaction ID):全局唯一的事务 ID,在这个事务ID下的所有事务会被统一控制 -每一个微服务在云上部署以后,都需要对其进行监控、追踪、审计、控制等,SpringBoot 抽取了 Actuator 场景,使得每个微服务快速引用即可获得生产级别的应用监控、审计等功能 +* TC (Transaction Coordinator):事务协调者,维护全局和分支事务的状态,驱动全局事务提交或回滚 -```xml - - org.springframework.boot - spring-boot-starter-actuator - -``` +* TM (Transaction Manager):事务管理器,定义全局事务的范围,开始全局事务、提交或回滚全局事务 -暴露所有监控信息为 HTTP: +* RM (Resource Manager):资源管理器,管理分支事务处理的资源,与 TC 交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚 -```yaml -management: - endpoints: - enabled-by-default: true #暴露所有端点信息 - web: - exposure: - include: '*' #以web方式暴露 -``` +典型的分布式事务流程: -访问 http://localhost:8080/actuator/[beans/health/metrics/] +1. TM 向 TC 申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的 XID +2. XID 在微服务调用链路的上下文中传播(也就是在多个 TM,RM 中传播) +3. RM 向 TC 注册分支事务,将其纳入 XID 对应全局事务的管辖 +4. TM 向 TC 发起针对 XID 的全局提交或回滚决议 +5. TC 调度 XID 下管辖的全部分支事务完成提交或回滚请求 -可视化界面:https://github.com/codecentric/spring-boot-admin +Cloud-分布式事务流程 -**** +*** -### Endpoint +#### 基本配置 -默认所有的 Endpoint 除过 shutdown 都是开启的 +Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案 -```yaml -management: - endpoints: - enabled-by-default: false #禁用所有的 - endpoint: #手动开启一部分 - beans: - enabled: true - health: - enabled: true -``` +下载 seata-server 文件修改 conf 目录下的配置文件 -端点: +* file.conf:自定义事务组名称、事务日志存储模式为 db、数据库连接信息 -| ID | 描述 | -| ------------------ | ------------------------------------------------------------ | -| `auditevents` | 暴露当前应用程序的审核事件信息。需要一个 `AuditEventRepository` 组件 | -| `beans` | 显示应用程序中所有 Spring Bean 的完整列表 | -| `caches` | 暴露可用的缓存 | -| `conditions` | 显示自动配置的所有条件信息,包括匹配或不匹配的原因 | -| `configprops` | 显示所有 `@ConfigurationProperties` | -| `env` | 暴露 Spring 的属性 `ConfigurableEnvironment` | -| `flyway` | 显示已应用的所有 Flyway 数据库迁移。 需要一个或多个 Flyway 组件。 | -| `health` | 显示应用程序运行状况信息 | -| `httptrace` | 显示 HTTP 跟踪信息,默认情况下 100 个 HTTP 请求-响应需要一个 `HttpTraceRepository` 组件 | -| `info` | 显示应用程序信息 | -| `integrationgraph` | 显示 Spring integrationgraph,需要依赖 `spring-integration-core` | -| `loggers` | 显示和修改应用程序中日志的配置 | -| `liquibase` | 显示已应用的所有 Liquibase 数据库迁移,需要一个或多个 Liquibase 组件 | -| `metrics` | 显示当前应用程序的指标信息。 | -| `mappings` | 显示所有 `@RequestMapping` 路径列表 | -| `scheduledtasks` | 显示应用程序中的计划任务 | -| `sessions` | 允许从 Spring Session 支持的会话存储中检索和删除用户会话,需要使用 Spring Session 的基于 Servlet 的 Web 应用程序 | -| `shutdown` | 使应用程序正常关闭,默认禁用 | -| `startup` | 显示由 `ApplicationStartup` 收集的启动步骤数据。需要使用 `SpringApplication` 进行配置 `BufferingApplicationStartup` | -| `threaddump` | 执行线程转储 | + **事务分组**:seata 的资源逻辑,可以按微服务的需要,在应用程序(客户端)对自行定义事务分组,每组取一个名字 -应用程序是 Web 应用程序(Spring MVC,Spring WebFlux 或 Jersey),则可以使用以下附加端点: + Cloud-Seata配置文件 -| ID | 描述 | -| ------------ | ------------------------------------------------------------ | -| `heapdump` | 返回 `hprof` 堆转储文件。 | -| `jolokia` | 通过 HTTP 暴露 JMX bean(需要引入 Jolokia,不适用于 WebFlux),需要引入依赖 `jolokia-core` | -| `logfile` | 返回日志文件的内容(如果已设置 `logging.file.name` 或 `logging.file.path` 属性),支持使用 HTTP Range标头来检索部分日志文件的内容。 | -| `prometheus` | 以 Prometheus 服务器可以抓取的格式公开指标,需要依赖 `micrometer-registry-prometheus` | +* 数据库新建库 seata,建表 db_store.sql 在 https://github.com/seata/seata/tree/2.x/script/server/db 目录里面 -常用 Endpoint: +* registry.conf:指明注册中心为 Nacos,及修改 Nacos 连接信息 -- Health:监控状况 -- Metrics:运行时指标 + Cloud-Seata注册中心配置 -- Loggers:日志记录 +启动 Nacos 和 Seata,如果 DB 报错,需要把将 lib 文件夹下 mysql-connector-java-5.1.30.jar 删除,替换为自己 MySQL 连接器版本 + +Cloud-Seata配置成功 +官网:https://seata.io + +下载地址:https://github.com/seata/seata/releases + +基本介绍:https://seata.io/zh-cn/docs/overview/what-is-seata.html + *** +#### 基本使用 +两个注解: -## 项目部署 +* Spring 提供的本地事务:@Transactional -SpringBoot 项目开发完毕后,支持两种方式部署到服务器: +* Seata 提供的全局事务:**@GlobalTransactional** -* jar 包 (官方推荐,默认) -* war 包 +搭建简单 Demo: -**更改 pom 文件中的打包方式为 war** +* 创建 UNDO_LOG 表:SEATA AT 模式需要 `UNDO_LOG` 表,如果一个模块的事务提交了,Seata 会把提交了哪些数据记录到 undo_log 表中,如果这时 TC 通知全局事务回滚,那么 RM 就从 undo_log 表中获取之前修改了哪些资源,并根据这个表回滚 -* 修改启动类 + ```sql + -- 注意此处0.3.0+ 增加唯一索引 ux_undo_log + CREATE TABLE `undo_log` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `branch_id` bigint(20) NOT NULL, + `xid` varchar(100) NOT NULL, + `context` varchar(128) NOT NULL, + `rollback_info` longblob NOT NULL, + `log_status` int(11) NOT NULL, + `log_created` datetime NOT NULL, + `log_modified` datetime NOT NULL, + `ext` varchar(100) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`) + ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8; + ``` + +* 引入 pom 依赖: + + ```xml + + com.alibaba.cloud + spring-cloud-starter-alibaba-seata + ${spring-cloud-alibaba.version} + + ``` + +* application.yml: + + ```yaml + spring: + application: + name: seata-order-service + cloud: + alibaba: + seata: + # 自定义事务组名称需要与seata-server中file.conf中配置的事务组ID对应 + # vgroup_mapping.my_test_tx_group = "my_group" + tx-service-group: my_group + nacos: + discovery: + server-addr: localhost:8848 + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://localhost:3306/seata_order?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=UTC + username: root + password: 123456 + ``` + +* 构建三个服务: ```java - @SpringBootApplication - public class SpringbootDeployApplication extends SpringBootServletInitializer { - public static void main(String[] args) { - SpringApplication.run(SpringbootDeployApplication.class, args); - } + // 仓储服务 + public interface StorageService { + // 扣除存储数量 + void deduct(String commodityCode, int count); + } - @Override - protected SpringApplicationBuilder configure(SpringApplicationBuilder b) { - return b.sources(SpringbootDeployApplication.class); - } + // 订单服务 + public interface OrderService { + // 创建订单 + Order create(String userId, String commodityCode, int orderCount); + } + + // 帐户服务 + public interface AccountService { + // 从用户账户中借出 + void debit(String userId, int money); } ``` -* 指定打包的名称 +* 业务逻辑:增加 @GlobalTransactional 注解 - ```xml - war - - springboot - - - org.springframework.boot - spring-boot-maven-plugin - - - + ```java + public class OrderServiceImpl implements OrderService { + @Resource + private OrderDAO orderDAO; + @Resource + private AccountService accountService; + + @Transactional(rollbackFor = Exception.class) + public Order create(String userId, String commodityCode, int orderCount) { + int orderMoney = calculate(commodityCode, orderCount); + // 账户扣钱 + accountService.debit(userId, orderMoney); + + Order order = new Order(); + order.userId = userId; + order.commodityCode = commodityCode; + order.count = orderCount; + order.money = orderMoney; + + return orderDAO.insert(order); + } + } ``` - + ```java + public class BusinessServiceImpl implements BusinessService { + @Resource + private StorageService storageService; + @Resource + private OrderService orderService; + + // 采购,涉及多服务的分布式事务问题 + @GlobalTransactional + @Transactional(rollbackFor = Exception.class) + public void purchase(String userId, String commodityCode, int orderCount) { + storageService.deduct(commodityCode, orderCount); + orderService.create(userId, commodityCode, orderCount); + } + } + ``` + + + + + +详细示例参考:https://github.com/seata/seata-samples/tree/master/springcloud-nacos-seata + + + + + + + + + + + + diff --git a/Tool.md b/Tool.md index d81b3ce..2ed20c7 100644 --- a/Tool.md +++ b/Tool.md @@ -31,7 +31,7 @@ Git 是分布式版本控制系统(Distributed Version Control System,简称 4.提交到本地仓库。本地仓库中保存修改的各个历史版本 -5.修改完成后,需要和团队成员共享代码时,将代码push到远程仓库 +5.修改完成后,需要和团队成员共享代码时,将代码 push 到远程仓库 @@ -66,7 +66,7 @@ GitLab(地址: https://about.gitlab.com/ )是一个用于仓库管理系 设置用户信息: * git config --global user.name “Seazean” -* git config --global user.email “zhyzhyang@sina.com” //用户名和邮箱可以随意填写,不会校对 +* git config --global user.email "imseazean@gmail.com" //用户名和邮箱可以随意填写,不会校对 查看配置信息: @@ -108,8 +108,8 @@ GitLab(地址: https://about.gitlab.com/ )是一个用于仓库管理系 * -t 指定密钥类型,默认是 rsa ,可以省略 * -C 设置注释文字,比如邮箱 * -f 指定密钥文件存储文件名 - * 查看命令: cat ~/.ssh/id_rsa.pub - * 公钥测试命令: ssh -T git@github.com + * 查看命令:cat ~/.ssh/id_rsa.pub + * 公钥测试命令:ssh -T git@github.com @@ -124,7 +124,7 @@ GitLab(地址: https://about.gitlab.com/ )是一个用于仓库管理系 ### 工作过程 -![](https://gitee.com/seazean/images/raw/master/Tool/Git基本工作流程.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/Git基本工作流程.png) 版本库:.git 隐藏文件夹就是版本库,版本库中存储了很多配置信息、日志信息和文件版本信息等 @@ -132,7 +132,7 @@ GitLab(地址: https://about.gitlab.com/ )是一个用于仓库管理系 暂存区:.git 文件夹中有很多文件,其中有一个 index 文件就是暂存区,也可以叫做 stage,暂存区是一个临时保存修改文件的地方 -![](https://gitee.com/seazean/images/raw/master/Tool/文件流程图.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/文件流程图.png) @@ -224,7 +224,7 @@ pull = fetch + merge fetch 是从远程仓库更新到本地仓库,pull是从远程仓库直接更新到工作空间中 -![](https://gitee.com/seazean/images/raw/master/Tool/图解远程仓库工作流程.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/图解远程仓库工作流程.png) @@ -288,7 +288,7 @@ git push :上传本地指定分支到远程仓库 ## 版本管理 -![](https://gitee.com/seazean/images/raw/master/Tool/版本切换.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/版本切换.png) 命令:git reset --hard 版本唯一索引值 @@ -338,7 +338,7 @@ git merge branch-name:合并指定分支到当前分支 有时候合并操作不会如此顺利。 如果你在两个不同的分支中,对同一个文件的同一个部分进行了不同的修改,Git 就没办法合并它们,同时会提示文件冲突。此时需要我们打开冲突的文件并修复冲突内容,最后执行 git add 命令来标识冲突已解决 -​ ![](https://gitee.com/seazean/images/raw/master/Tool/合并分支冲突.png) +​ ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/合并分支冲突.png) @@ -431,14 +431,14 @@ File → Settings 打开设置窗口,找到 Version Control 下的 git 选项 ### 版本管理 * 版本对比 - ![](https://gitee.com/seazean/images/raw/master/Tool/版本对比.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/版本对比.png) * 版本切换方式一:控制台 Version Control → Log → 右键 Reset Current Branch → Reset,这种切换会抛弃原来的提交记录 - ![](https://gitee.com/seazean/images/raw/master/Tool/版本切换方式一.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/版本切换方式一.png) * 版本切换方式二:控制台 Version Control → Log → Revert Commit → Merge → 处理代码 → commit,这种切换会当成一个新的提交记录,之前的提交记录也都保留 - ![](https://gitee.com/seazean/images/raw/master/Tool/版本切换方式二.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/版本切换方式二.png) -​ ![](https://gitee.com/seazean/images/raw/master/Tool/版本切换方式二(1).png) +​ ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/版本切换方式二(1).png) @@ -465,7 +465,7 @@ File → Settings 打开设置窗口,找到 Version Control 下的 git 选项 1. VCS → Git → Push → 点击 master Define remote 2. 将远程仓库的 url 路径复制过来 → Push - ![](https://gitee.com/seazean/images/raw/master/Tool/本地仓库推送到远程仓库.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/本地仓库推送到远程仓库.png) @@ -477,7 +477,7 @@ File → Settings 打开设置窗口,找到 Version Control 下的 git 选项 File → Close Project → Checkout from Version Control → Git → 指定远程仓库的路径 → 指定本地存放的路径 → clone -![](https://gitee.com/seazean/images/raw/master/Tool/远程仓库克隆到本地仓库.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/远程仓库克隆到本地仓库.png) @@ -499,11 +499,11 @@ File → Close Project → Checkout from Version Control → Git → 指定远 操作系统作为接口的示意图: - + 移动设备操作系统: -![](https://gitee.com/seazean/images/raw/master/Tool/移动设备操作系统.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/移动设备操作系统.png) @@ -518,7 +518,7 @@ File → Close Project → Checkout from Version Control → Git → 指定远 ### 系统介绍 从内到位依次是硬件 → 内核层 → Shell 层 → 应用层 → 用户 -![Linux](https://gitee.com/seazean/images/raw/master/Tool/Linux系统.png) +![Linux](https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/Linux系统.png) 内核层:核心和基础,附着在硬件平台上,控制和管理系统内的各种资源,有效的组织进程的运行,扩展硬件的功能,提高资源利用效率,为用户提供安全可靠的应用环境。 @@ -534,42 +534,7 @@ Shell 层:与用户直接交互的界面。用户可以在提示符下输入 Linux 文件系统目录结构和熟知的 windows 系统有较大区别,没有各种盘符的概念。根目录只有一个/,采用层级式的树状目录结构。 -![Linux文件系统](https://gitee.com/seazean/images/raw/master/Tool/Linux文件系统.png) - -/:根目录,所有的目录、文件、设备都在/之下,/就是 Linux 文件系统的组织者,也是最上级的领导者。 - -/bin:bin 就是二进制(binary)英文缩写。在一般的系统当中,都可以在这个目录下找到 linux 常用的命令。系统所需要的那些命令位于此目录。 - -/boot:Linux 的内核及引导系统程序所需要的文件目录。 - -/dev:dev 是设备(device)的英文缩写。这个目录对所有的用户都十分重要。因为在这个目录中包含了所有 linux 系统中使用的外部设备。但是这里并不是放的外部设备的驱动程序。这一点和常用的 windows,dos 操作系统不一样。它实际上是一个访问这些外部设备的端口。可以非常方便地去访问这些外部设备,和访问一个文件,一个目录没有任何区别。 - -/**home**:如果建立一个用户,用户名是"xx",那么在/home 目录下就有一个对应的 -/home/xx 路径,用来存放用户的主目录。家目录 - -/lib:lib 是库(library)英文缩写。这个目录是用来存放系统动态连接共享库的。几乎所有的应用程序都会用到这个目录下的共享库。因此,千万不要轻易对这个目录进行什么操作,一旦发生问题,系统就不能工作了。 - -/**proc**:存储的是当前内核运行状态的一系列特殊文件,用户可以通过这些文件查看有关系统硬件及当前正在运行进程的信息,甚至可以通过更改其中某些文件来改变内核的运行状态。此外还有**/srv /sys** 三个目录,内核相关目录,不要动。 - -/**root**:Linux 超级权限用户 root 的家目录。 - -/sbin:这个目录是用来存放系统管理员的系统管理程序。大多是涉及系统管理的命令的存放,是超级权限用户 root 的可执行命令存放地,普通用户无权限执行这个目录下的命令,sbin 中包含的都是 root 权限才能执行的。 - -/var/log - -/**usr**:这是 linux 系统中占用硬盘空间最大的目录。**用户的很多应用程序和文件都存放在这个目录下**。 Unix software resource usr。类似 windows 系统的 program files - -/usr/local:这里主要存放那些手动安装的软件,即不是通过或 apt-get 安装的软件。它和/usr 目录具有相类似的目录结构。 - -/usr/share :系统共用的东西存放地,比如 /usr/share/fonts 是字体目录,/usr/share/doc和/usr/share/man 帮助文件。 - -/etc:管理所有的配置文件的目录,比如安装 mysql 的配置文件my.conf - -/mnt:可供系统管理员使用,手动挂载一些临时设备媒体设备的目录。 - -/media:是自动挂载的目录。当把 U 盘插入到系统中,会自动挂载到该目录下。比如插入一个 U 盘,会自动到/media 目录中挂载。 - -/opt:额外安装软件存放的目录。比如 mysql 的安装包就可以放在该目录。 +![Linux文件系统](https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/Linux文件系统.png) @@ -585,12 +550,12 @@ Linux 文件系统目录结构和熟知的 windows 系统有较大区别,没 #### NAT -首先设置虚拟机中 NAT 模式的选项,打开 VMware,点击“编辑”下的“虚拟网络编辑器”,设置 NAT 参数 - ![](https://gitee.com/seazean/images/raw/master/Tool/配置NAT.jpg) +首先设置虚拟机中 NAT 模式的选项,打开 VMware,点击编辑下的虚拟网络编辑器,设置 NAT 参数 + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/配置NAT.jpg) **注意**:VMware Network Adapter VMnet8 保证是启用状态 -​ ![](https://gitee.com/seazean/images/raw/master/Tool/本地主机网络连接.jpg) +​ ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/本地主机网络连接.jpg) @@ -637,7 +602,7 @@ Linux 文件系统目录结构和熟知的 windows 系统有较大区别,没 * 宿主机 ping 虚拟机,虚拟机 ping 宿主机 * 在虚拟机中访问网络,需要增加一块 NAT 网卡 * 【虚拟机】--【设置】--【添加】 - * + * @@ -649,10 +614,11 @@ Linux 文件系统目录结构和熟知的 windows 系统有较大区别,没 ### 远程登陆 -**服务器维护工作** 都是在 远程 通过 SSH 客户端 来完成的, 并没有图形界面, 所有的维护工作都需要通过命令来完成,Linux 服务器需要安装 SSH 相关服务。 -首先执行 sudo apt-get install openssh-server 指令。接下来用 xshell 连接。 +**服务器维护工作** 都是在 远程 通过 SSH 客户端 来完成的, 并没有图形界面, 所有的维护工作都需要通过命令来完成,Linux 服务器需要安装 SSH 相关服务 -![](https://gitee.com/seazean/images/raw/master/Tool/远程连接Linux.png) +首先执行 sudo apt-get install openssh-server 指令,接下来用 xshell 连接 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/远程连接Linux.png) 先用普通用户登录,然后转成 root @@ -668,12 +634,12 @@ Linux 文件系统目录结构和熟知的 windows 系统有较大区别,没 ## 用户管理 -Linux 系统是一个多用户、多任务的操作系统。多用户是指在 linux 操作系统中可以创建多个用户,而这些多用户又可以同时执行各自不同的任务,而互不影响。和 windows 系统有很大区别。 +Linux 系统是一个多用户、多任务的操作系统。多用户是指在 Linux 操作系统中可以创建多个用户,而这些多用户又可以同时执行各自不同的任务,而互不影响 在 Linux 系统中,会存在着以下几个概念: * 用户名:用户的名称 -* 用户所属的组:当前用户所属的组。 +* 用户所属的组:当前用户所属的组 * 用户的家目录:当前账号登录成功之后的目录,就叫做该用户的家目录 @@ -684,9 +650,9 @@ Linux 系统是一个多用户、多任务的操作系统。多用户是指在 l logname:用于显示目前用户的名称 -* --help  在线帮助。 +* --help:在线帮助 -* --vesion  显示版本信息。 +* --vesion:显示版本信息 @@ -696,7 +662,7 @@ su UserName:切换用户 su -c comman root:切换用户为 root 并在执行 comman 指令后退出返回原使用者 -su:切换到root用户 +su:切换到 root 用户 @@ -706,16 +672,13 @@ su:切换到root用户 参数说明: -* 选项 - * -c comment 指定一段注释性描述。 - * -d 指定用户主目录,如果此目录不存在,则同时使用-m选项,可以创建主目录。 - * -m 创建用户的主目录。 - * -g 用户组 指定用户所属的用户组,-g 组名 用户名。 - * -G 用户组,用户组 指定用户所属的附加组。 - * -s Shell文件 指定用户的登录Shell。 - * -u 用户号 指定用户的用户号,如果同时有-o选项,则可以重复使用其他用户的标识号。 -* 用户名 - 指定新账号的用户名(后续我们可以使用这个用户名进行系统登录)。 +* -c comment 指定一段注释性描述 +* -d 指定用户主目录,如果此目录不存在,则同时使用 -m 选项,可以创建主目录 +* -m 创建用户的主目录 +* -g 用户组,指定用户所属的用户组 +* -G 用户组,用户组 指定用户所属的附加组 +* -s Shell 文件 指定用户的登录 Shell +* -u 用户号,指定用户的用户号,如果同时有 -o 选项,则可以重复使用其他用户的标识号。 如何知道添加用户成功呢? 通过指令 cat /etc/passwd 查看 @@ -724,7 +687,7 @@ seazean:x: 1000:1000:Seazean:/home/seazean:/bin/bash 用户名 密码 用户ID 组ID 注释 家目录 shell程序 ``` -useradd -m Username新建用户成功之后,会建立家目录,但是此时有问题没有指定 shell 的版本,不是我们熟知的 bash,功能上有很多限制。**sudo useradd -m -s /bin/bash Username** +useradd -m Username 新建用户成功之后,会建立 home 目录,但是此时有问题没有指定 shell 的版本,不是我们熟知的 bash,功能上有很多限制,进行 **sudo useradd -m -s /bin/bash Username** @@ -732,15 +695,15 @@ useradd -m Username新建用户成功之后,会建立家目录,但是此时 #### 用户密码 -系统安装好默认的 root 用户是没有密码的,需要给 root 设置一个密码**sudo passwd root**. +系统安装好默认的 root 用户是没有密码的,需要给 root 设置一个密码 **sudo passwd root**. -* 普通用户:**sudo passwd UserName**。 +* 普通用户:**sudo passwd UserName** * 管理员用户:passwd [options] UserName - * -l 锁定密码,即禁用账号。 - * -u 密码解锁。 - * -d 使账号无密码。 - * -f 强迫用户下次登录时修改密码。 + * -l:锁定密码,即禁用账号 + * -u:密码解锁 + * -d:使账号无密码 + * -f:强迫用户下次登录时修改密码 @@ -748,7 +711,7 @@ useradd -m Username新建用户成功之后,会建立家目录,但是此时 usermod 命令通过修改系统帐户文件来修改用户账户信息 -修改用户账号就是根据实际情况更改用户的有关属性,如用户号、主目录、用户组、登录Shell等。 +修改用户账号就是根据实际情况更改用户的有关属性,如用户号、主目录、用户组、登录 Shell 等 * 普通用户:sudo usermod [options] Username @@ -760,12 +723,12 @@ usermod 命令通过修改系统帐户文件来修改用户账户信息 #### 用户删除 -删除用户账号就是要将/etc/passwd等系统文件中的该用户记录删除,必要时还删除用户的主目录。 +删除用户账号就是要将 /etc/passwd 等系统文件中的该用户记录删除,必要时还删除用户的主目录 * 普通用户:sudo userdel [options] Username * 管理员用户:userdel [options] Username - * -f:强制删除用户,即使用户当前已登录; + * -f:强制删除用户,即使用户当前已登录 * -r:删除用户的同时,删除与用户相关的所有文件 @@ -776,41 +739,37 @@ usermod 命令通过修改系统帐户文件来修改用户账户信息 ### 用户组管理 -开发组,测试组,等 - #### 组管理 添加组:**groupadd 组名** -​ 创建用户的时加入组:useradd -m -g 组名 用户名 +创建用户的时加入组:useradd -m -g 组名 用户名 ​ #### 添加用户组 -新增一个用户组(组名可见名知意,符合规范即可),然后将用户添加到组中 - -**使用者权限:管理员用户** +新增一个用户组(组名可见名知意,符合规范即可),然后将用户添加到组中,需要使用管理员权限 命令:groupadd [options] Groupname -* -g GID指定新用户组的组标识号(GID) -* -o 一般与-g选项同时使用,表示新用户组的GID可以与系统已有用户组的GID相同 +* -g GID 指定新用户组的组标识号(GID) +* -o 一般与 -g 选项同时使用,表示新用户组的 GID 可以与系统已有用户组的 GID 相同 -新增用户组Seazean:groupadd Seazean +新增用户组 Seazean:groupadd Seazean #### 修改用户组 -**使用者权限:管理员用户** +需要使用管理员权限 命令:groupmod [options] Groupname - -g GID 为用户组指定新的组标识号。 -- -o 与-g选项同时使用,用户组的新GID可以与系统已有用户组的GID相同。 +- -o 与 -g 选项同时使用,用户组的新 GID 可以与系统已有用户组的 GID 相同 - -n 新用户组 将用户组的名字改为新名字 -修改Seazean组名为zhy:groupmod -n zhy Seazean +修改 Seazean 组名为 zhy:groupmod -n zhy Seazean @@ -870,8 +829,7 @@ gpasswd 是 Linux 工作组文件 /etc/group 和 /etc/gshadow 管理工具,用 可以看到命令的帮助文档 -**man** [指令名称] 查看帮助文档 -比如 man ls,退出方式 q +**man** [指令名称]:查看帮助文档,比如 man ls,退出方式 q @@ -885,15 +843,17 @@ date 可以用来显示或设定系统的日期与时间 命令:date [options] -* -d<字符串>:显示字符串所指的日期与时间。字符串前后必须加上双引号; -* -s<字符串>:根据字符串来设置日期与时间。字符串前后必须加上双引号 +* -d<字符串>:显示字符串所指的日期与时间,字符串前后必须加上双引号; +* -s<字符串>:根据字符串来设置日期与时间,字符串前后必须加上双引号 -* -u:显示 GMT; +* -u:显示 GMT * --version:显示版本信息 -查看时间:**date** → 2020年 11月 30日 星期一 17:10:54 CST -查看指定格式时间:**date "+%Y-%m-%d %H:%M:%S"** → 2020-11-30 17:11:44 -设置日期指令:**date -s “2019-12-23 19:21:00”** +查看时间:date → 2020年 11月 30日 星期一 17:10:54 CST + +查看指定格式时间:date "+%Y-%m-%d %H:%M:%S" → 2020-11-30 17:11:44 + +设置日期指令:date -s “2019-12-23 19:21:00” @@ -903,17 +863,17 @@ date 可以用来显示或设定系统的日期与时间 ### id -id 会显示用户以及所属群组的实际与有效 ID。若两个 ID 相同,则仅显示实际 ID;若仅指定用户名称,则显示目前用户的 ID。 +id 会显示用户以及所属群组的实际与有效 ID,若两个 ID 相同则仅显示实际 ID;若仅指定用户名称,则显示目前用户的 ID 命令:id [-gGnru] [--help] [--version] [用户名称] //参数的顺序 -- -g 或--group  显示用户所属群组的 ID -- -G 或--groups  显示用户所属附加群组的 ID -- -n 或--name  显示用户,所属群组或附加群组的名称。 -- -r 或--real  显示实际 ID -- -u 或--user  显示用户 ID +- -g 或--group:显示用户所属群组的 ID +- -G 或--groups:显示用户所属附加群组的 ID +- -n 或--name:显示用户,所属群组或附加群组的名称。 +- -r 或--real:显示实际 ID +- -u 或--user:显示用户 ID -> id 命令参数虽然很多,但是常用的是不带参数的id命令,主要看他的uid和组信息 +> id 命令参数虽然很多,但是常用的是不带参数的 id 命令,主要看 uid 和组信息 @@ -923,20 +883,14 @@ id 会显示用户以及所属群组的实际与有效 ID。若两个 ID 相同 ### sudo -sudo:控制用户对系统命令的使用权限,root 允许的操作,通过 sudo 可以提高普通用户的操作权限 +sudo:控制用户对系统命令的使用权限,通过 sudo 可以提高普通用户的操作权限 - -V 显示版本编号 - -h 会显示版本编号及指令的使用方式说明 - -l 显示出自己(执行 sudo 的使用者)的权限 -- -v 因为 sudo 在第一次执行时或是在 N 分钟内没有执行(N 预设为五)会问密码,这个参数是重新做一次确认,如果超过 N 分钟,也会问密码 -- -k 将会强迫使用者在下一次执行 sudo 时询问密码(不论有没有超过 N 分钟) -- -b 将要执行的指令放在背景执行 -- -p prompt 可以更改问密码的提示语,其中 %u 会代换为使用者的帐号名称, %h 会显示主机名称 -- -u username/#uid 不加此参数,代表要以 root 的身份执行指令,而加了此参数,可以以 username 的身份执行指令(#uid 为该 username 的使用者号码) -- -s 执行环境变数中的 SHELL 所指定的 shell ,或是 /etc/passwd 里所指定的 shell -- -H 将环境变数中的 HOME 指定为要变更身份的使用者 HOME 目录(如不加 -u 参数就是系统管理者 root ) - -command 要以系统管理者身份(或以 -u 更改为其他人)执行的指令 - **sudo -u root command -l**:指定 root 用户执行指令 command + + **sudo -u root command -l**:指定 root 用户执行指令 command @@ -955,24 +909,25 @@ top:用于实时显示 process 的动态 * -d 秒数:表示进程界面更新时间(每几秒刷新一次) * -H 表示线程模式 -`top -Hp 进程 id`:分析该进程内各线程的cpu使用情况 +`top -Hp 进程 id`:分析该进程内各线程的 CPU 使用情况 -![](https://gitee.com/seazean/images/raw/master/Tool/top命令.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/top命令.png) **各进程(任务)的状态监控属性解释说明:** - PID — 进程 id - TID — 线程 id - USER — 进程所有者 - PR — 进程优先级 - NI — nice 值,负值表示高优先级,正值表示低优先级 - VIRT — 进程使用的虚拟内存总量,单位 kb,VIRT=SWAP+RES - RES — 进程使用的、未被换出的物理内存大小,单位 kb,RES=CODE+DATA - SHR — 共享内存大小,单位 kb - S — 进程状态,D=不可中断的睡眠状态 R=运行 S=睡眠 T=跟踪/停止 Z=僵尸进程 - %CPU — 上次更新到现在的 CPU 时间占用百分比 - %MEM — 进程使用的物理内存百分比 - TIME+ — 进程使用的 CPU 时间总计,单位 1/100 秒 - COMMAND — 进程名称(命令名/命令行) + +* PID — 进程 id +* TID — 线程 id +* USER — 进程所有者 +* PR — 进程优先级 +* NI — nice 值,负值表示高优先级,正值表示低优先级 +* VIRT — 进程使用的虚拟内存总量,单位 kb,VIRT=SWAP+RES +* RES — 进程使用的、未被换出的物理内存大小,单位 kb,RES=CODE+DATA +* SHR — 共享内存大小,单位 kb +* S — 进程状态,D=不可中断的睡眠状态 R=运行 S=睡眠 T=跟踪/停止 Z=僵尸进程 +* %CPU — 上次更新到现在的 CPU 时间占用百分比 +* %MEM — 进程使用的物理内存百分比 +* TIME+ — 进程使用的 CPU 时间总计,单位 1/100 秒 +* COMMAND — 进程名称(命令名/命令行) @@ -994,10 +949,10 @@ Linux 系统中查看进程使用情况的命令是 ps 指令 * -T:开启线程查看 * -p:指定线程号 - 一般常用格式为 ps -ef 或者 ps aux 两种。显示的信息大体一致,略有区别 -两者区别: - 如果想查看进程的 CPU 占用率和内存占用率,可以使用 aux - 如果想查看进程的父进程 ID 和完整的 COMMAND 命令,可以使用 ef + 一般常用格式为 ps -ef 或者 ps aux 两种。显示的信息大体一致,略有区别: + +* 如果想查看进程的 CPU 占用率和内存占用率,可以使用 aux +* 如果想查看进程的父进程 ID 和完整的 COMMAND 命令,可以使用 ef `ps -T -p `:显示某个进程的线程 @@ -1017,17 +972,17 @@ Linux 系统中查看进程使用情况的命令是 ps 指令 ### kill -Linux kill命令用于删除执行中的程序或工作(可强制中断) +Linux kill 命令用于删除执行中的程序或工作,并不是让进程直接停止,而是给进程发一个信号,可以进入终止逻辑 命令:kill [-s <信息名称或编号>] [程序] 或 kill [-l <信息编号>] -- -l <信息编号>  若不加<信息编号>选项,则-l参数会列出全部的信息名称。 -- -s <信息名称或编号>  指定要送出的信息。 -- -KILL 强制杀死进程 -- **-9 彻底杀死进程(常用)** -- [程序] 程序的 PID、PGID、工作编号。 +- -l <信息编号>:若不加<信息编号>选项,则-l参数会列出全部的信息名称 +- -s <信息名称或编号>:指定要送出的信息 +- -KILL:强制杀死进程 +- **-9:彻底杀死进程(常用)** +- [程序] 程序的 PID、PGID、工作编号 -`kill 15642 `. `kill -KILL 15642`. **`kill -9 15642`** +`kill 15642 `. `kill -KILL 15642`. `kill -9 15642` 杀死指定用户所有进程: @@ -1043,28 +998,28 @@ Linux kill命令用于删除执行中的程序或工作(可强制中断) ### shutdown -shutdown命令可以用来进行关闭系统,并且在关机以前传送讯息给所有使用者正在执行的程序,shutdown 也可以用来重开机 +shutdown 命令可以用来进行关闭系统,并且在关机以前传送讯息给所有使用者正在执行的程序,shutdown 也可以用来重开机 普通用户:sudo shutdown [-t seconds] [-rkhncfF] time [message] 管理员用户:shutdown [-t seconds] [-rkhncfF] time [message] -- -t seconds : 设定在几秒钟之后进行关机程序。 -- -k : 并不会真的关机,只是将警告讯息传送给所有使用者。 -- -r : 关机后重新开机。 -- -h : 关机后停机。 -- -n : 不采用正常程序来关机,用强迫的方式杀掉所有执行中的程序后自行关机。 -- -c : 取消目前已经进行中的关机动作。 -- -f : 关机时,不做 fcsk 动作(检查 Linux 档系统)。 -- -F : 关机时,强迫进行 fsck 动作。 -- time : 设定关机的时间。 -- message : 传送给所有使用者的警告讯息。 +- -t seconds:设定在几秒钟之后进行关机程序 +- -k:并不会真的关机,只是将警告讯息传送给所有使用者 +- -r:关机后重新开机 +- -h:关机后停机 +- -n:不采用正常程序来关机,用强迫的方式杀掉所有执行中的程序后自行关机 +- -c:取消目前已经进行中的关机动作 +- -f:关机时,不做 fcsk 动作(检查 Linux 档系统) +- -F:关机时,强迫进行 fsck 动作 +- time:设定关机的时间 +- message:传送给所有使用者的警告讯息 立即关机:`shutdown -h now` 或者 `shudown now` -指定1分钟后关机并显示警告信息:`shutdown +1 "System will shutdown after 1 minutes" ` +指定 1 分钟后关机并显示警告信息:`shutdown +1 "System will shutdown after 1 minutes" ` -指定1分钟后重启并发出警告信息:`shutdown –r +1 "1分钟后关机重启"` +指定 1 分钟后重启并发出警告信息:`shutdown –r +1 "1分钟后关机重启"` @@ -1074,15 +1029,15 @@ shutdown命令可以用来进行关闭系统,并且在关机以前传送讯息 ### reboot -reboot命令用于用来重新启动计算机 +reboot 命令用于用来重新启动计算机 命令:reboot [-n] [-w] [-d] [-f] [-i] -- -n : 在重开机前不做将记忆体资料写回硬盘的动作 -- -w : 并不会真的重开机,只是把记录写到 /var/log/wtmp 档案里 -- -d : 不把记录写到 /var/log/wtmp 档案里(-n 这个参数包含了 -d) -- -f : 强迫重开机,不呼叫 shutdown 这个指令 -- -i : 在重开机之前先把所有网络相关的装置先停止 +- -n:在重开机前不做将记忆体资料写回硬盘的动作 +- -w:并不会真的重开机,只是把记录写到 /var/log/wtmp 档案里 +- -d:不把记录写到 /var/log/wtmp 档案里(-n 这个参数包含了 -d) +- -f:强迫重开机,不呼叫 shutdown 这个指令 +- -i:在重开机之前先把所有网络相关的装置先停止 @@ -1092,17 +1047,17 @@ reboot命令用于用来重新启动计算机 ### who -who命令用于显示系统中有哪些使用者正在上面,显示的资料包含了使用者 ID、使用的终端机、上线时间、呆滞时间、CPU 使用量、动作等等 +who 命令用于显示系统中有哪些使用者正在上面,显示的资料包含了使用者 ID、使用的终端机、上线时间、CPU 使用量、动作等等 命令:who - [husfV] [user] -- -H 或 --heading:显示各栏位的标题信息列;(常用 `who -H`) -- -i 或 -u 或 --idle:显示闲置时间,若该用户在前一分钟之内有进行任何动作,将标示成"."号,如果该用户已超过24小时没有任何动作,则标示出"old"字符串; -- -m:此参数的效果和指定"am i"字符串相同; -- -q 或--count:只显示登入系统的帐号名称和总人数; -- -s:此参数将忽略不予处理,仅负责解决who指令其他版本的兼容性问题; -- -w 或-T或--mesg或--message或--writable:显示用户的信息状态栏; -- --help:在线帮助; +- -H 或 --heading:显示各栏位的标题信息列(常用 `who -H`) +- -i 或 -u 或 --idle:显示闲置时间,若该用户在前一分钟之内有进行任何动作,将标示成 `.` 号,如果该用户已超过 24 小时没有任何动作,则标示出 `old` 字符串 +- -m:此参数的效果和指定 `am i` 字符串相同 +- -q 或--count:只显示登入系统的帐号名称和总人数 +- -s:此参数将忽略不予处理,仅负责解决who指令其他版本的兼容性问题 +- -w 或-T或--mesg或--message或--writable:显示用户的信息状态栏 +- --help:在线帮助 - --version:显示版本信息 @@ -1117,30 +1072,30 @@ who命令用于显示系统中有哪些使用者正在上面,显示的资料 * --version 查看版本号 -* start:立刻启动后面接的 unit。 +* start:立刻启动后面接的 unit -* stop:立刻关闭后面接的 unit。 +* stop:立刻关闭后面接的 unit -* restart:立刻关闭后启动后面接的 unit,亦即执行 stop 再 start 的意思。 +* restart:立刻关闭后启动后面接的 unit,亦即执行 stop 再 start 的意思 -* reload:不关闭 unit 的情况下,重新载入配置文件,让设置生效。 -* status:目前后面接的这个 unit 的状态,会列出有没有正在执行、开机时是否启动等信息。 +* reload:不关闭 unit 的情况下,重新载入配置文件,让设置生效 +* status:目前后面接的这个 unit 的状态,会列出有没有正在执行、开机时是否启动等信息 -* enable:设置下次开机时,后面接的 unit 会被启动。 +* enable:设置下次开机时,后面接的 unit 会被启动 -* disable:设置下次开机时,后面接的 unit 不会被启动。 +* disable:设置下次开机时,后面接的 unit 不会被启动 -* is-active:目前有没有正在运行中。 +* is-active:目前有没有正在运行中 -* is-enable:开机时有没有默认要启用这个 unit。 +* is-enable:开机时有没有默认要启用这个 unit -* kill :不要被 kill 这个名字吓着了,它其实是向运行 unit 的进程发送信号。 +* kill :不要被 kill 这个名字吓着了,它其实是向运行 unit 的进程发送信号 -* show:列出 unit 的配置。 +* show:列出 unit 的配置 -* mask:注销 unit,注销后你就无法启动这个 unit 了。 +* mask:注销 unit,注销后你就无法启动这个 unit 了 -* unmask:取消对 unit 的注销。 +* unmask:取消对 unit 的注销 @@ -1152,7 +1107,7 @@ who命令用于显示系统中有哪些使用者正在上面,显示的资料 timedatectl用于控制系统时间和日期。可以查询和更改系统时钟于设定,同时可以设定和修改时区信息。在实际开发过程中,系统时间的显示会和实际出现不同步;我们为了校正服务器时间、时区会使用timedatectl命令 -timedatectl :显示系统的时间信息 +timedatectl:显示系统的时间信息 timedatectl status:显示系统的当前时间和日期 @@ -1166,7 +1121,7 @@ timedatectl set-ntp true/false:启用/禁用时间同步 timedatectl set-time "2020-12-20 20:45:00":时间同步关闭后可以设定时间 -NTP即Network Time Protocol(网络时间协议),是一个互联网协议,用于同步计算机之间的系统时钟。timedatectl实用程序可以自动同步你的Linux系统时钟到使用NTP的远程服务器。 +NTP 即 Network Time Protocol(网络时间协议),是一个互联网协议,用于同步计算机之间的系统时钟,timedatectl 实用程序可以自动同步你的Linux系统时钟到使用NTP的远程服务器 @@ -1176,9 +1131,9 @@ NTP即Network Time Protocol(网络时间协议),是一个互联网协议 ### clear -clear命令用于清除屏幕 +clear 命令用于清除屏幕 -通过执行clear命令,就可以把缓冲区的命令全部清理干净了 +通过执行 clear 命令,就可以把缓冲区的命令全部清理干净 @@ -1188,21 +1143,23 @@ clear命令用于清除屏幕 ### exit -exit命令用于退出目前的shell。执行exit可使shell以指定的状态值退出。若不设置状态值参数,则shell以预设值退出。状态值0代表执行成功,其他值代表执行失败。exit也可用在script,离开正在执行的script,回到shell。 +exit 命令用于退出目前的 shell + +执行 exit 可使 shell 以指定的状态值退出。若不设置状态值参数,则 shell 以预设值退出。状态值 0 代表执行成功,其他值代表执行失败;exit 也可用在 script,离开正在执行的 script,回到 shell 命令:exit [状态值] -* 0表示成功(Zero - Success) +* 0 表示成功(Zero - Success) -* 非0表示失败(Non-Zero - Failure) +* 非 0 表示失败(Non-Zero - Failure) -* 2表示用法不当(Incorrect Usage) +* 2 表示用法不当(Incorrect Usage) -* 127表示命令没有找到(Command Not Found) +* 127 表示命令没有找到(Command Not Found) -* 126表示不是可执行的(Not an executable) +* 126 表示不是可执行的(Not an executable) -* 大于等于128 信号产生 +* 大于等于 128 信号产生 @@ -1218,18 +1175,6 @@ exit命令用于退出目前的shell。执行exit可使shell以指定的状态 ### 常用命令 -- ls: 列出目录 -- cd: 切换目录 -- pwd: 显示目前的目录 -- mkdir:创建一个新的目录 -- rmdir:删除一个空的目录 -- cp: 复制文件或目录 -- rm: 移除文件或目录 -- mv: 移动文件与目录或修改文件与目录的名称 -- 在敲出文件/ 目录 / 命令的前几个字母之后, 按下 `tab`键会自动补全,如果还存在其他文件 / 目录 / 命令, 再按一下`tab`键,系统会提示可能存在的命令 - - - #### ls ls命令相当于我们在Windows系统中打开磁盘、或者打开文件夹看到的目录以及文件的明细。 @@ -1239,13 +1184,13 @@ ls命令相当于我们在Windows系统中打开磁盘、或者打开文件夹 - -a :全部的文件,连同隐藏档( 开头为 . 的文件) 一起列出来(常用) - -d :仅列出目录本身,而不是列出目录内的文件数据(常用) - -l :显示不隐藏的文件与文件夹的详细信息;(常用) -- ls -al = ll 命令:显示所有文件与文件夹的详细信息 +- **ls -al = ll 命令**:显示所有文件与文件夹的详细信息 #### pwd -pwd 是 **Print Working Directory** 的缩写,也就是显示目前所在当前目录的命令。 +pwd 是 Print Working Directory 的缩写,也就是显示目前所在当前目录的命令 命令:pwd 选项 @@ -1256,17 +1201,17 @@ pwd 是 **Print Working Directory** 的缩写,也就是显示目前所在当 #### cd -cd是Change Directory的缩写,这是用来变换工作目录的命令 +cd 是 Change Directory 的缩写,这是用来变换工作目录的命令 命令:cd [相对路径或绝对路径] * cd ~ :表示回到根目录 * cd .. :返回上级目录 -- **相对路径** 在输入路径时, 最前面不是以 `/` 开始的 , 表示相对 **当前目录** 所在的目录位置 +- **相对路径** 在输入路径时, 最前面不是以 `/` 开始的 , 表示相对**当前目录**所在的目录位置 - 例如: /usr/share/doc -- **绝对路径** 在输入路径时, 最前面是以 `/` 开始的, 表示 从 **根目录** 开始的具体目录位置! - - 由 /usr/share/doc 到 /usr/share/man 时,可以写成: cd ../man。 +- **绝对路径** 在输入路径时, 最前面是以 `/` 开始的, 表示从**根目录**开始的具体目录位置 + - 由 /usr/share/doc 到 /usr/share/man 时,可以写成: cd ../man - 优点:定位准确, 不会因为 工作目录变化 而变化 @@ -1279,8 +1224,7 @@ mkdir命令用于建立名称为 dirName 之子目录 * -p 确保目录名称存在,不存在的就建一个,用来创建多级目录。 -在 aaa目录下,创建一个 bbb的子目录。 若 aaa目录原本不存在,则建立一个:`mkdir -p aaa/bbb` -注:本例若不加 -p,且原本 aaa目录不存在,则产生错误。 +`mkdir -p aaa/bbb`:在 aaa 目录下,创建一个 bbb 的子目录。 若 aaa 目录原本不存在,则建立一个 @@ -1290,29 +1234,27 @@ rmdir命令删除空的目录 命令:rmdir [-p] dirName -* -p 是当子目录被删除后使它也成为空目录的话,则顺便一并删除。 +* -p 是当子目录被删除后使它也成为空目录的话,则顺便一并删除 -在 aaa目录中,删除名为 bbb的子目录。若 bbb删除后,aaa目录成为空目录,则 aaa同时也会被删除: -`rmdir -p aaa/bbb` +`rmdir -p aaa/bbb`:在 aaa 目录中,删除名为 bbb 的子目录。若 bbb 删除后,aaa 目录成为空目录,则 aaa 同时也会被删除 #### cp -cp命令主要用于复制文件或目录。 +cp 命令主要用于复制文件或目录 命令:cp [options] source... directory -- -a:此选项通常在复制目录时使用,它保留链接、文件属性,并复制目录下的所有内容。其作用等于dpR参数组合。 -- -d:复制时保留链接。这里所说的链接相当于Windows系统中的快捷方式。 -- -f:覆盖已经存在的目标文件而不给出提示。 -- -i:与-f选项相反,在覆盖目标文件之前给出提示,要求用户确认是否覆盖,回答"y"时目标文件将被覆盖。 -- -p:除复制文件的内容外,还把修改时间和访问权限也复制到新文件中。 -- -r/R:若给出的源文件是一个目录文件,此时将复制该目录下所有的**子目录**和文件。 -- -l:不复制文件,只是生成链接文件。 +- -a:此选项通常在复制目录时使用,它保留链接、文件属性,并复制目录下的所有内容。其作用等于dpR参数组合 +- -d:复制时保留链接。这里所说的链接相当于Windows系统中的快捷方式 +- -f:覆盖已经存在的目标文件而不给出提示 +- -i:与 -f 选项相反,在覆盖目标文件之前给出提示,要求用户确认是否覆盖,回答 y 时目标文件将被覆盖 +- -p:除复制文件的内容外,还把修改时间和访问权限也复制到新文件中 +- -r/R:若给出的源文件是一个目录文件,此时将复制该目录下所有的**子目录**和文件 +- -l:不复制文件,只是生成链接文件 -cp –r aaa/* ccc :复制aaa下的所有文件到ccc -用户使用该指令复制目录时,必须使用参数"-r"或者"-R"。如果不加参数"-r"或者"-R",只复制文件,而略过目录 +`cp –r aaa/* ccc`:复制 aaa 下的所有文件到 ccc,不加参数 -r 或者 -R,只复制文件,而略过目录 @@ -1326,7 +1268,7 @@ rm命令用于删除一个文件或者目录。 - -f 即使原档案属性设为唯读,亦直接删除,无需逐一确认 - -r 将目录及以下之档案亦逐一删除,递归删除 -注:文件一旦通过rm命令删除,则无法恢复,所以必须格外小心地使用该命令 +注:文件一旦通过 rm 命令删除,则无法恢复,所以必须格外小心地使用该命令 @@ -1339,9 +1281,9 @@ mv [options] source dest mv [options] source... directory ``` -- -i: 若指定目录已有同名文件,则先询问是否覆盖旧文件; +- -i:若指定目录已有同名文件,则先询问是否覆盖旧文件 -- -f: 在 mv 操作要覆盖某已有的目标文件时不给任何指示; +- -f:在 mv 操作要覆盖某已有的目标文件时不给任何指示 | 命令格式 | 运行结果 | | ------------------ | ------------------------------------------------------------ | @@ -1360,50 +1302,57 @@ mv [options] source... directory #### 基本属性 -Linux系统是一种典型的多用户系统,不同的用户处于不同的地位,拥有不同的权限。为了保护系统的安全性,Linux系统对不同的用户访问同一文件(包括目录文件)的权限做了不同的规定。 +Linux 系统是一种典型的多用户系统,不同的用户处于不同的地位,拥有不同的权限。为了保护系统的安全性,Linux系统对不同的用户访问同一文件(包括目录文件)的权限做了不同的规定 -![](https://gitee.com/seazean/images/raw/master/Tool/用户目录下的文件.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/用户目录下的文件.png) 在Linux中第一个字符代表这个文件是目录、文件或链接文件等等。 -- 当为[ **d** ]则是目录 -- 当为[ **-** ]则是文件; -- 若是[ **l** ]则表示为链接文档(link file); -- 若是[ **b** ]则表示为装置文件里面的可供储存的接口设备(可随机存取装置); -- 若是[ **c** ]则表示为装置文件里面的串行端口设备,例如键盘、鼠标(一次性读取装置)。 +- 当为 d 则是目录 +- 当为 - 则是文件 +- 若是 l 则表示为链接文档 link file +- 若是 b 则表示为装置文件里面的可供储存的接口设备(可随机存取装置) +- 若是 c 则表示为装置文件里面的串行端口设备,例如键盘、鼠标(一次性读取装置) 接下来的字符,以三个为一组,均为[rwx] 的三个参数组合。其中,[ r ]代表可读(read)、[ w ]代表可写(write)、[ x ]代表可执行(execute)。 要注意的是,这三个权限的位置不会改变,如果没有权限,就会出现[ - ]。 - + + +从左至右用 0-9 这些数字来表示: + +* 第 0 位确定文件类型 +* 第 1-3 位确定属主拥有该文件的权限 +* 第 4-6 位确定属组拥有该文件的权限 +* 第 7-9 位确定其他用户拥有该文件的权限 -从左至右用0-9这些数字来表示: - 第0位确定文件类型,第1-3位确定属主(该文件的所有者)拥有该文件的权限。 - 第4-6位确定属组(所有者的同组用户)拥有该文件的权限, - 第7-9位确定其他用户拥有该文件的权限。 -其中,第1、4、7位表示读权限,如果用"r"字符表示,则有读权限,如果用"-"字符表示,则没有读权限;第2、5、8位表示写权限,如果用"w"字符表示,则有写权限,如果用"-"字符表示没有写权限;第3、6、9位表示可执行权限,如果用"x"字符表示,则有执行权限,如果用"-"字符表示,则没有执行权限。 + +*** #### 文件信息 -> 对于一个文件来说,它都有一个特定的所有者,也就是对该文件具有所有权的用户。也就是所谓的属主,它属于哪个用户的意思。除了属主,还有属组,也就是说,这个文件是属于哪个组的(用户所属的组)。 -> 文件的【属主】有一套【读写执行权限rwx】 -> 文件的【属组】有一套【读写执行权限rwx】 +对于一个文件,都有一个特定的所有者,也就是对该文件具有所有权的用户(属主);还有这个文件是属于哪个组的(属组) -![](https://gitee.com/seazean/images/raw/master/Tool/列出目录文件.png) +* 文件的【属主】有一套【读写执行权限rwx】 +* 文件的【属组】有一套【读写执行权限rwx】 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/列出目录文件.png) `ls -l` 可以查看文件夹下文件的详细信息, 从左到右 依次是: -- **权限(A区域)**, 第一个字符如果是 `d` 表示目录 -- **硬链接数(B区域)**, 通俗的讲就是有多少种方式, 可以访问当前目录和文件 -- **属主(C区域)**, 文件是所有者、或是叫做属主 -- **属组(D区域)**, 文件属于哪个组 -- **大小(E区域)**:文件大小 -- **时间(F区域)**:最后一次访问时间 -- **名称(G区域)**:文件的名称 +- 权限(A 区域): 第一个字符如果是 `d` 表示目录 +- 硬链接数(B 区域):通俗的讲就是有多少种方式, 可以访问当前目录和文件 +- 属主(C 区域):文件是所有者、或是叫做属主 +- 属组(D 区域): 文件属于哪个组 +- 大小(E 区域):文件大小 +- 时间(F 区域):最后一次访问时间 +- 名称(G 区域):文件的名称 + +*** @@ -1411,19 +1360,19 @@ Linux系统是一种典型的多用户系统,不同的用户处于不同的地 ##### 权限概述 -Linux文件属性有两种设置方法,一种是数字,一种是符号 +Linux 文件属性有两种设置方法,一种是数字,一种是符号 -Linux的文件调用权限分为三级 : 文件属主、属组、其他,利用 chmod 可以控制文件如何被他人所调用。 +Linux 的文件调用权限分为三级 : 文件属主、属组、其他,利用 chmod 可以控制文件如何被他人所调用。 ```shell chmod [-cfvR] [--help] [--version] mode file... mode : 权限设定字串,格式: [ugoa...][[+-=][rwxX]...][,...] ``` -* u 表示档案的拥有者,g 表示与该档案拥有者属于同一个group者,o表示其他的人,a 表示这三者皆是。 +* u 表示档案的拥有者,g 表示与该档案拥有者属于同一个 group 者,o 表示其他的人,a 表示这三者皆是 -* +表示增加权限、- 表示取消权限、= 表示唯一设定权限。 -* r 表示可读取,w 表示可写入,x 表示可执行,X 表示只有该档案是个子目录或者该档案已经被设定过为可执行。 +* +表示增加权限、- 表示取消权限、= 表示唯一设定权限 +* r 表示可读取,w 表示可写入,x 表示可执行,X 表示只有该档案是个子目录或者该档案已经被设定过为可执行 @@ -1431,56 +1380,64 @@ mode : 权限设定字串,格式: [ugoa...][[+-=][rwxX]...][,...] 命令:chmod [-R] xyz 文件或目录 -- xyz : 就是刚刚提到的数字类型的权限属性,为 rwx 属性数值的相加。 -- -R : 进行递归(recursive)的持续变更,亦即连同次目录下的所有文件都会变更 +- xyz : 就是刚刚提到的数字类型的权限属性,为 rwx 属性数值的相加 +- -R : 进行递归(recursive)的持续变更,亦即连同次目录下的所有文件都会变更 -文件的权限字符为:[-rwxrwxrwx], 这九个权限是三三一组的,我们使用数字来代表各个权限。 +文件的权限字符为:[-rwxrwxrwx], 这九个权限是三三一组的,我们使用数字来代表各个权限 - + -各权限的数字对照表:[r]:4; [w]:2; [x]:1; [-]:0 +各权限的数字对照表:[r]:4、[w]:2、[x]:1、[-]:0 -每种身份(owner/group/others)的三个权限(r/w/x)分数是需要累加的,例如权限为: [-rwxrwx---] 分数是: +每种身份(owner/group/others)的三个权限(r/w/x)分数是需要累加的,例如权限为:[-rwxrwx---] 分数是 - owner = rwx = 4+2+1 = 7 - group = rwx = 4+2+1 = 7 - others= --- = 0+0+0 = 0 -表示为:`chmod -R 770 文件名` +表示为:`chmod -R 770 文件名` ##### 符号权限 -![](https://gitee.com/seazean/images/raw/master/Tool/权限符号表.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/权限符号表.png) - user 属主权限 - group 属组权限 - others 其他权限 - all 全部的身份 -我们就可以使用 **u, g, o,a** 来代表身份的权限!读写的权限可以写成 **r, w, x**。 +我们就可以使用 **u g o a** 来代表身份的权限,读写的权限可以写成 **r w x** + +`chmod u=rwx,g=rx,o=r a.txt`:将as.txt的权限设置为 **-rwxr-xr--** -`chmod u=rwx,g=rx,o=r a.txt`:将as.txt的权限设置为**-rwxr-xr--** +` chmod a-r a.txt`:将文件的所有权限去除 **r** -` chmod a-r a.txt`:将文件的所有权限去除**r** +*** #### 更改属组 -chgrp命令用于变更文件或目录的所属群组。 +chgrp 命令用于变更文件或目录的所属群组 -文件或目录权限的的拥有者由所属群组来管理。可以使用chgrp指令去变更文件与目录的所属群组。 +文件或目录权限的的拥有者由所属群组来管理,可以使用 chgrp 指令去变更文件与目录的所属群组 ```shell chgrp [-cfhRv][--help][--version][所属群组][文件或目录...] chgrp [-cfhRv][--help][--reference=<参考文件或目录>][--version][文件或目录...] ``` -chgrp -v root aaa:将文件aaa的属组更改成root(其他也可以) +chgrp -v root aaa:将文件 aaa 的属组更改成 root(其他也可以) + + + + + +*** @@ -1509,34 +1466,34 @@ chown seazean:seazean aaa:将文件aaa的属主和属组更改为seazean #### touch -touch命令用于创建文件、修改文件或者目录的时间属性,包括存取时间和更改时间。若文件不存在,系统会建立一个新的文件。ls -l 可以显示档案的时间记录 +touch 命令用于创建文件、修改文件或者目录的时间属性,包括存取时间和更改时间。若文件不存在,系统会建立一个新的文件 ```shell touch [-acfm][-d<日期时间>][-r<参考文件或目录>] [-t<日期时间>][--help][--version][文件或目录…] ``` -- -a 改变档案的读取时间记录。 -- -m 改变档案的修改时间记录。 -- -c 假如目的档案不存在,不会建立新的档案。与 --no-create 的效果一样。 -- -f 不使用,是为了与其他 unix 系统的相容性而保留。 -- -r 使用参考档的时间记录,与 --file 的效果一样。 -- -d 设定时间与日期,可以使用各种不同的格式。 -- -t 设定档案的时间记录,格式与 date 指令相同。 -- --no-create 不会建立新档案。 -- --help 列出指令格式。 -- --version 列出版本讯息。 +- -a 改变档案的读取时间记录 +- -m 改变档案的修改时间记录 +- -c 假如目的档案不存在,不会建立新的档案。与 --no-create 的效果一样 +- -f 不使用,是为了与其他 unix 系统的相容性而保留 +- -r 使用参考档的时间记录,与 --file 的效果一样 +- -d 设定时间与日期,可以使用各种不同的格式 +- -t 设定档案的时间记录,格式与 date 指令相同 +- --no-create 不会建立新档案 +- --help 列出指令格式 +- --version 列出版本讯息 -`touch t.txt`:创建t.txt文件 +`touch t.txt`:创建 t.txt 文件 `touch t{1..10}.txt`:创建10 个名为 t1.txt 到 t10.txt 的空文件 -`touch t.txt`:更改t.txt的访问时间为现在 +`touch t.txt`:更改 t.txt 的访问时间为现在 #### stat -stat命令用于显示inode内容。stat以文字的格式来显示inode的内容。 +stat 命令用于显示 inode 内容 命令:stat [文件或目录] @@ -1544,7 +1501,7 @@ stat命令用于显示inode内容。stat以文字的格式来显示inode的内 #### cat -cat 是一个文本文件查看和连接工具。用于小文件 +cat 是一个文本文件查看和连接工具,**用于小文件** 命令:cat [-AbeEnstTuv] [--help] [--version] Filename @@ -1555,7 +1512,7 @@ cat 是一个文本文件查看和连接工具。用于小文件 #### less -less用于查看文件,但是less 在查看之前不会加载整个文件。用于大文件 +less 用于查看文件,但是 less 在查看之前不会加载整个文件,**用于大文件** 命令:less [options] Filename @@ -1565,13 +1522,13 @@ less用于查看文件,但是less 在查看之前不会加载整个文件。 #### tail -tail 命令可用于查看文件的内容,有一个常用的参数 **-f** 常用于查阅正在改变的日志文件。 +tail 命令可用于查看文件的内容,有一个常用的参数 **-f** 常用于查阅正在改变的日志文件 命令:tail [options] Filename * -f 循环读取,动态显示文档的最后内容 -* -n(行数) 显示文件的尾部 n 行内容 -* -c(数目)> 显示的字节数 +* -n 显示文件的尾部 n 行内容 +* -c 显示字节数 * -nf 查看最后几行日志信息 `tail -f filename`:动态显示最尾部的内容 @@ -1588,45 +1545,40 @@ head 命令可用于查看文件的开头部分的内容,有一个常用的参 - -q 隐藏文件名 - -v 显示文件名 -- -c<数目> 显示的字节数。 -- -n<行数> 显示的行数。 +- -c 显示的字节数 +- -n 显示的行数 `head -n Filename`:查看文件的前一部分 -`head -n 20 Filename`:查看文件的前20行 +`head -n 20 Filename`:查看文件的前 20 行 #### grep -grep 指令用于查找内容包含指定的范本样式的文件,若不指定任何文件名称,或是所给予的文件名为 -,则 grep 指令会从标准输入设备读取数据。 +grep 指令用于查找内容包含指定的范本样式的文件,若不指定任何文件名称,或是所给予的文件名为 -,则 grep 指令会从标准输入设备读取数据 ```shell grep [-abcEFGhHilLnqrsvVwxy][-A<显示列数>][-B<显示列数>][-C<显示列数>][-d<进行动作>][-e<范本样式>][-f<范本文件>][--help][范本样式][文件或目录...] ``` -* -c:只输出匹配行的计数 -* -i:不区分大小写 -* -h:查询多文件时不显示文件名 -* -l:查询多文件时只输出包含匹配字符的文件名 -* -n:显示匹配行及行号 -* -s:不显示不存在或无匹配文本的错误信息 -* -v:显示不包含匹配文本的所有行 -* --color=auto :可以将找到的关键词部分加上颜色的显示 - -**管道符 |**:表示将前一个命令处理的结果传递给后面的命令处理。 - -`grep aaaa Filename `:显示存在关键字 aaaa 的行 - -`grep -n aaaa Filename`:显示存在关键字 aaaa 的行,且显示行号 +* -c 只输出匹配行的计数 +* -i 不区分大小写 +* -h 查询多文件时不显示文件名 +* -l 查询多文件时只输出包含匹配字符的文件名 +* -n 显示匹配行及行号 +* -s 不显示不存在或无匹配文本的错误信息 +* -v 显示不包含匹配文本的所有行 +* --color=auto 可以将找到的关键词部分加上颜色的显示 -`grep -i aaaa Filename`:忽略大小写,显示存在关键字 aaaa 的行 +**管道符 |**:表示将前一个命令处理的结果传递给后面的命令处理 -`grep -v aaaa Filename`:显示存在关键字aaaa的所有行 - -`ps -ef | grep sshd`:查找包含 sshd 进程的进程信息 - -` ps -ef | grep -c sshd`:查找 sshd 相关的进程个数 +* `grep aaaa Filename `:显示存在关键字 aaaa 的行 +* `grep -n aaaa Filename`:显示存在关键字 aaaa 的行,且显示行号 +* `grep -i aaaa Filename`:忽略大小写,显示存在关键字 aaaa 的行 +* `grep -v aaaa Filename`:显示存在关键字 aaaa 的所有行 +* `ps -ef | grep sshd`:查找包含 sshd 进程的进程信息 +* ` ps -ef | grep -c sshd`:查找 sshd 相关的进程个数 @@ -1634,23 +1586,23 @@ grep [-abcEFGhHilLnqrsvVwxy][-A<显示列数>][-B<显示列数>][-C<显示列数 #### echo -将字符串输出到控制台 , 通常和 **重定向** 联合使用 +将字符串输出到控制台 , 通常和重定向联合使用 -命令:echo string # 如果字符串有空格, 为了避免歧义 请增加 双引号 或者 单引号 +命令:echo string,如果字符串有空格, 为了避免歧义 请增加 双引号 或者 单引号 -- 通过 `命令 > 文件` 将**命令的成功结果** **覆盖** 指定文件内容 -- 通过 `命令 >> 文件` 将**命令的成功结果** **追加** 指定文件的后面 -- 通过 `命令 &>> 文件` 将 **命令的失败结果** **追加** 指定文件的后面 +- 通过 `命令 > 文件` 将命令的成功结果覆盖指定文件内容 +- 通过 `命令 >> 文件` 将命令的成功结果追加指定文件的后面 +- 通过 `命令 &>> 文件` 将 命令的失败结果追加指定文件的后面 `echo "程序员" >> a.txt`:将程序员追加到 a.txt 后面 -`cat 不存在的目录 &>> error.log`:将错误信息追加到 error.log 文件 +`cat 不存在的目录 &>> error.log`:将错误信息追加到 error.log 文件 #### awk -AWK 是一种处理文本文件的语言,是一个强大的文本分析工具。 +AWK 是一种处理文本文件的语言,是一个强大的文本分析工具 ```shell awk [options] 'script' var=value file(s) @@ -1658,24 +1610,30 @@ awk [options] -f scriptfile var=value file(s) ``` * -F fs:指定输入文件折分隔符,fs 是一个字符串或者是一个正则表达式 + * -v:var=value 赋值一个用户定义变量 + * -f:从脚本文件中读取 awk 命令 -* $n(数字):获取**第几段**内容 + +* $n:获取**第几段**内容 + * $0:获取**当前行** 内容 + * NF:表示当前行共有多少个字段 + * $NF:代表最后一个字段 * $(NF-1):代表倒数第二个字段 * NR:代表处理的是第几行 -* ```shell + ```sh 命令:awk 'BEGIN{初始化操作}{每行都执行} END{结束时操作}' 文件名BEGIN{ 这里面放的是执行前的语句 }{这里面放的是处理每一行时要执行的语句} END {这里面放的是处理完所有的行后要执行的语句 } ``` - + ```a.txt //准备数据 @@ -1697,9 +1655,9 @@ zhouba 98 44 46 zhaoliu 78 44 36 ``` -* `cat a.txt | awk -F ' ' '{print $1,$2,$3}'`:按照空格分割, 打印 一二三列 内容 +* `cat a.txt | awk -F ' ' '{print $1,$2,$3}'`:按照空格分割,打印 一二三列内容 -* `awk -F ' ' '{OFS="\t"}{print $1,$2,$3}'`:按照制表符tab 进行分割, 打印一二三列 +* `awk -F ' ' '{OFS="\t"}{print $1,$2,$3}'`:按照制表符 tab 进行分割,打印一二三列 \b:退格 \f:换页 \n:换行 \r:回车 \t:制表符 ``` @@ -1711,7 +1669,7 @@ zhouba 98 44 46 zhouba 98 44 ``` -* `awk -F ',' '{print toupper($1)}' a.txt`:根据逗号分割, 打印内容,第一段大写 +* `awk -F ',' '{print toupper($1)}' a.txt`:根据逗号分割,打印内容,第一段大写 | 函数名 | 含义 | 作用 | | --------- | ------ | -------------- | @@ -1728,7 +1686,7 @@ zhouba 98 44 46 #### find -find 命令用来在指定目录下查找文件。任何位于参数之前的字符串都将被视为查找的目录名。如果使用该命令不设置任何参数,将在当前目录下查找子目录与文件,并且将查找到的子目录和文件全部进行显示 +find 命令用来在指定目录下查找文件,如果使用该命令不设置任何参数,将在当前目录下查找子目录与文件,并且将查找到的子目录和文件全部进行显示 命令:find <指定目录> <指定条件> <指定内容> @@ -1740,9 +1698,7 @@ find 命令用来在指定目录下查找文件。任何位于参数之前的字 #### read -read 命令用于从标准输入读取数值。 - -read 内部命令被用来从标准输入读取单行数据。这个命令可以用来读取键盘输入,当使用重定向的时候,可以读取文件中的一行数据。 +read 命令用于从标准输入读取数值 ```shell read [-ers] [-a aname] [-d delim] [-i text] [-n nchars] [-N nchars] [-p prompt] [-t timeout] [-u fd] [name ...] @@ -1752,7 +1708,7 @@ read [-ers] [-a aname] [-d delim] [-i text] [-n nchars] [-N nchars] [-p prompt] #### sort -Linux sort命令用于将文本文件内容加以排序 +Linux sort 命令用于将文本文件内容加以排序 ```sh sort [-bcdfimMnr][文件] @@ -1773,20 +1729,20 @@ sort -r a.txt | uniq | head -n 4 #### uniq -uniq用于重复数据处理,使用前先sort排序 +uniq 用于重复数据处理,使用前先 sort 排序 ```sh uniq [OPTION]... [INPUT [OUTPUT]] ``` * -c 在数据行前出现的次数 -* -d 只打印重复的行,重复的行只显示一次 -* -D 只打印重复的行,重复的行出现多少次就显示多少次 +* -d 只打印重复的行,重复的行只显示一次 +* -D 只打印重复的行,重复的行出现多少次就显示多少次 * -f 忽略行首的几个字段 * -i 忽略大小写 * -s 忽略行首的几个字母 * -u 只打印唯一的行 -* -w 比较不超过n个字母 +* -w 比较不超过 n 个字母 @@ -1806,12 +1762,12 @@ tar 的主要功能是打包、压缩和解压文件,tar 本身不具有压缩 命令:tar [必要参数] [选择参数] [文件] -* -c 产生.tar 文件 +* -c 产生 .tar 文件 * -v 显示详细信息 * -z 打包同时压缩 * -f 指定压缩后的文件名 -* -x 解压.tar 文件 -* -t 列出tar文件中包含的文件的信息 +* -x 解压 .tar 文件 +* -t 列出 tar 文件中包含的文件的信息 * -r 附加新的文件到tar文件中 `tar -cvf txt.tar txtfile.txt `:将 txtfile.txt 文件打包(仅打包,不压缩) @@ -1820,10 +1776,7 @@ tar 的主要功能是打包、压缩和解压文件,tar 本身不具有压缩 `tar -ztvf txt.tar.gz`:查看 tar 中有哪些文件 -`tar -zxvf Filename -C 目标路径`:**解压** - -> 参数 f 之后的文件档名是自己取的,习惯上都用 .tar 来作为辨识。 -> 如果加 z 参数,则以 .tar.gz 或 .tgz 来代表 gzip 压缩过的 tar包 +`tar -zxvf Filename -C 目标路径`:解压 @@ -1853,9 +1806,9 @@ gunzip 001.gz :解压001.gz文件 #### zip -zip命令用于压缩文件。 +zip 命令用于压缩文件。 -zip是个使用广泛的压缩程序,文件经它压缩后会另外产生具有".zip"扩展名的压缩文件。 +zip 是个使用广泛的压缩程序,文件经它压缩后会另外产生具有 `.zip` 扩展名的压缩文件 命令:zip [必要参数] [选择参数] [文件] @@ -1868,7 +1821,7 @@ zip是个使用广泛的压缩程序,文件经它压缩后会另外产生具 #### unzip -unzip 命令用于解压缩zip文件,unzip 为.zip压缩文件的解压缩程序 +unzip 命令用于解压缩 zip 文件,unzip 为 `.zip` 压缩文件的解压缩程序 命令:unzip [必要参数] [选择参数] [文件] @@ -1884,9 +1837,9 @@ unzip 命令用于解压缩zip文件,unzip 为.zip压缩文件的解压缩程 #### bzip2 -bzip2命令是.bz2文件的压缩程序。 +bzip2 命令是 `.bz2` 文件的压缩程序。 -bzip2采用新的压缩演算法,压缩效果比传统的LZ77/LZ78压缩演算法来得好。若没有加上任何参数,bzip2压缩完文件后会产生.bz2的压缩文件,并删除原始的文件。 +bzip2 采用新的压缩演算法,压缩效果比传统的 LZ77/LZ78 压缩演算法好,若不加任何参数,bzip2 压缩完文件后会产生 .bz2 的压缩文件,并删除原始的文件 ```sh bzip2 [-cdfhkLstvVz][--repetitive-best][--repetitive-fast][- 压缩等级][要压缩的文件] @@ -1898,7 +1851,7 @@ bzip2 [-cdfhkLstvVz][--repetitive-best][--repetitive-fast][- 压缩等级][要 #### bunzip2 -bunzip2命令是.bz2文件的解压缩程序。 +bunzip2 命令是 `.bz2` 文件的解压缩程序。 命令:bunzip2 [-fkLsvV] [.bz2压缩文件] @@ -1916,19 +1869,17 @@ bunzip2命令是.bz2文件的解压缩程序。 #### Vim -**vim**:是从 vi (系统内置命令)发展出来的一个文本编辑器。代码补全、编译及错误跳转等方便编程的功能特别丰富,在程序员中被广泛使用。 - -简单的来说, vi 是老式的字处理器,不过功能已经很齐全了,但是还是有可以进步的地方。 +vim:是从 vi 发展出来的一个文本编辑器 -**命令模式**:在Linux终端中输入“vim 文件名”就进入了命令模式,但不能输入文字。 -**编辑模式:**在命令模式下按i就会进入编辑模式,此时就可以写入程式,按Esc可回到命令模式。 -**末行模式:**在命令模式下按:进入末行模式,左下角会有一个冒号出现,此时可以敲入命令并执行 +* 命令模式:在 Linux 终端中输入`vim 文件名` 就进入了命令模式,但不能输入文字 +* 编辑模式:在命令模式下按 `i` 就会进入编辑模式,此时可以写入程式,按 Esc 可回到命令模式 +* 末行模式:在命令模式下按 `:` 进入末行模式,左下角会有一个冒号,可以敲入命令并执行 #### 打开文件 -Ubuntu 默认没有安装vim,需要先安装 vim,安装命令:**sudo apt-get install vim** +Ubuntu 默认没有安装 vim,需要先安装 vim,安装命令:**sudo apt-get install vim** Vim 有三种模式:命令模式(Command mode)、插入模式(Insert mode)、末行模式(Last Line mode) @@ -1961,7 +1912,7 @@ Vim 有三种模式:命令模式(Command mode)、插入模式(Insert mod | a | 在光标所在位置之后插入文本 | | A | 在光标所在行的行尾插入文本 | -按下ESC键,离开插入模式,进入命令模式 +按下 ESC 键,离开插入模式,进入命令模式 因为我们是一个空文件,所以使用【I】或者【i】都可以 @@ -1975,7 +1926,7 @@ Vim 有三种模式:命令模式(Command mode)、插入模式(Insert mod #### 命令模式 -Vim 打开一个文件(文件可以存在,也可以不存在),默认就是进入命令模式。在该模式下, 输入的字符会被当做指令,而不会被当做要输入的文字。在该模式下,可以使用指令进行跳至文章开头、文章结尾、删除某行、复制、粘贴等内容。 +Vim 打开一个文件(文件可以存在,也可以不存在),默认进入命令模式。在该模式下, 输入的字符会被当做指令,而不会被当做要输入的文字 ##### 移动光标 @@ -2001,11 +1952,11 @@ Vim 打开一个文件(文件可以存在,也可以不存在),默认就 ##### 选中文本 -在 vi/vim 中要选择文本, 需要显示 visual 命令切换到 **可视模式** +在 vi/vim 中要选择文本,需要显示 visual 命令切换到**可视模式** -vi/vim 中提供了 **三种** 可视模式, 可以方便程序员的选择 **选中文本的方式** +vi/vim 中提供了三种可视模式,方便程序员的选择**选中文本的方式** -按 ESC 可以放弃选中, 返回到 **命令模式** +按 ESC 可以放弃选中, 返回到**命令模式** | 命令 | 模式 | 功能 | | -------- | ---------- | ---------------------------------- | @@ -2017,7 +1968,7 @@ vi/vim 中提供了 **三种** 可视模式, 可以方便程序员的选择 ** ##### 撤销删除 -在学习编辑命令之前,先要知道怎样撤销之前一次 错误的 编辑操作 +在学习编辑命令之前,先要知道怎样撤销之前一次错误的编辑操作 | 命令 | 英文 | 功能 | | -------- | ----- | ------------------------ | @@ -2028,22 +1979,22 @@ vi/vim 中提供了 **三种** 可视模式, 可以方便程序员的选择 ** 删除的内容此时并没有真正的被删除,在剪切板中,按下 p 键,可以将删除的内容粘贴回来 -| 快捷键 | 功能描述 | -| :---------: | :----------------------: | -| x | 删除光标所在位置的字符 | -| d(移动命令) | 删除移动命令对应的内容 | -| dd | 删除光标所在行的内容 | -| D | 删除光标位置到行尾的内容 | -| :n1,n2 | 删除从a1到a2行的文本内容 | +| 快捷键 | 功能描述 | +| :----: | :--------------------------: | +| x | 删除光标所在位置的字符 | +| d | 删除移动命令对应的内容 | +| dd | 删除光标所在行的内容 | +| D | 删除光标位置到行尾的内容 | +| :n1,n2 | 删除从 a1 到 a2 行的文本内容 | **删除命令可以和移动命令连用, 以下是常见的组合命令(扩展):** -| 命令 | 作用 | -| ---- | --------------------------------- | -| dw | 删除从光标位置到单词末尾 | -| d} | 删除从光标位置到段落末尾 | -| dG | 删除光标所行到文件末尾的所有内容 | -| ndd | 删除当前行(包括此行)到后n行内容 | +| 命令 | 作用 | +| ---- | ----------------------------------- | +| dw | 删除从光标位置到单词末尾 | +| d} | 删除从光标位置到段落末尾 | +| dG | 删除光标所行到文件末尾的所有内容 | +| ndd | 删除当前行(包括此行)到后 n 行内容 | @@ -2055,20 +2006,20 @@ vi/vim 中提供了 **三种** 可视模式, 可以方便程序员的选择 ** vim 中提供有一个 被复制文本的缓冲区 -- 复制 命令会将选中的文字保存在缓冲区 -- 删除 命令删除的文字会被保存在缓冲区 -- 在需要的位置, 使用 粘贴 命令可以将缓冲对的文字插入到光标所在的位置 -- vim中的文本缓冲区只有一个,如果后续做过 复制、剪切操作, 之前缓冲区中的内容会被替换. +- 复制命令会将选中的文字保存在缓冲区 +- 删除命令删除的文字会被保存在缓冲区 +- 在需要的位置,使用粘贴命令可以将缓冲对的文字插入到光标所在的位置 +- vim 中的文本缓冲区只有一个,如果后续做过复制、剪切操作,之前缓冲区中的内容会被替换 -| 快捷键 | 功能描述 | -| :-----: | :--------------------------: | -| y | 复制已选中的文本到剪切板 | -| yy | 将光标所在行复制到剪切板 | -| nyy | 复制从光标所在行到向下n行 | -| p | 将剪切板中的内容粘贴到光标后 | -| P(大写) | 将剪切板中的内容粘贴到光标前 | +| 快捷键 | 功能描述 | +| :----: | :--------------------------: | +| y | 复制已选中的文本到剪切板 | +| yy | 将光标所在行复制到剪切板 | +| nyy | 复制从光标所在行到向下n行 | +| p | 将剪切板中的内容粘贴到光标后 | +| P | 将剪切板中的内容粘贴到光标前 | -注意:vi中的 **文本缓冲区**和系统的**剪切板**不是同一个,在其他软件中使用 Ctrl + C 复制的内容,不能在vim 中通过 `p` 命令粘贴,可以在 **编辑模式** 下使用 **鼠标右键粘贴**。 +注意:**vim 中的文本缓冲区和系统的剪切板不是同一个**,在其他软件中使用 Ctrl + C 复制的内容,不能在 vim 中通过 `p` 命令粘贴,可以在编辑模式下使用鼠标右键粘贴 @@ -2082,10 +2033,10 @@ vim 中提供有一个 被复制文本的缓冲区 | 快捷键 | 功能描述 | | :----: | :--------------------------------------: | -| /abc | 从光标所在位置向后查找字符串abc | -| /^abc | 查找以abc为行首的行 | -| /abc$ | 查找以abc为行尾的行 | -| ?abc | 从光标所在位置向前查找字符串abc | +| /abc | 从光标所在位置向后查找字符串 abc | +| /^abc | 查找以 abc 为行首的行 | +| /abc$ | 查找以 abc 为行尾的行 | +| ?abc | 从光标所在位置向前查找字符串 abc | | * | 向后查找当前光标所在单词 | | # | 向前查找当前光标所在单词 | | n | 查找下一个,向同一方向重复上次的查找指令 | @@ -2099,8 +2050,8 @@ vim 中提供有一个 被复制文本的缓冲区 | R | 替换当前行光标后的字符 | 替换模式 | - 光标选中要替换的字符 -- `R` 命令可以进入 **替换模式**, 替换完成后, 按下ESC, 按下 ESC可以回到 **命令模式** -- **替换命令** 的作用就是不用进入 **编辑模式**, 对文件进行 **轻量级的修改** +- `R` 命令可以进入替换模式,替换完成后,按下 ESC 可以回到命令模式 +- 替换命令的作用就是不用进入编辑模式,对文件进行轻量级的修改 @@ -2110,32 +2061,32 @@ vim 中提供有一个 被复制文本的缓冲区 #### 末行模式 -在命令模式下,按下:键进入末行模式 +在命令模式下,按下 `:` 键进入末行模式 -| 命令 | 功能描述 | -| :---------: | :-------------------------------------------------: | -| :wq | 保存并退出Vim编辑器 | -| :wq! | 保存并强制退出Vim编辑器 | -| :q | 不保存且退出Vim编辑器 | -| :q! | 不保存且强制退出Vim编辑器 | -| :w | 保存但是不退出Vim编辑器 | -| :w! | 强制保存但是不退出Vim编辑器 | -| :w filename | 另存到filename文件 | -| x! | 保存文本,退出保存但是不退出Vim编辑器,更通用的命令 | -| ZZ | 直接退出保存但是不退出Vim编辑器 | -| :n | 光标移动至第n行行首 | +| 命令 | 功能描述 | +| :---------: | :---------------------------------------------------: | +| :wq | 保存并退出 Vim 编辑器 | +| :wq! | 保存并强制退出 Vim 编辑器 | +| :q | 不保存且退出 Vim 编辑器 | +| :q! | 不保存且强制退出 Vim 编辑器 | +| :w | 保存但是不退出 Vim 编辑器 | +| :w! | 强制保存但是不退出 Vim 编辑器 | +| :w filename | 另存到 filename 文件 | +| x! | 保存文本,退出保存但是不退出 Vim 编辑器,更通用的命令 | +| ZZ | 直接退出保存但是不退出 Vim 编辑器 | +| :n | 光标移动至第 n 行行首 | #### 异常处理 -* 如果 vim异常退出, 在磁盘上可能会保存有 交换文件 +* 如果 vim 异常退出, 在磁盘上可能会保存有 交换文件 -* 下次再使用 vim 编辑文件时, 会看到以下屏幕信息, +* 下次再使用 vim 编辑文件时,会看到以下屏幕信息: - ![](https://gitee.com/seazean/images/raw/master/Tool/vim异常.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/vim异常.png) -* ls -a 一下,会看到隐藏的.swp文件 删除了此文件即可。 +* ls -a 一下,会看到隐藏的 .swp 文件,删除了此文件即可 @@ -2152,7 +2103,7 @@ ln [-sf] source_filename dist_filename * -s:默认是实体链接,加 -s 为符号链接 * -f:如果目标文件存在时,先删除目标文件 - + **实体链接**: @@ -2210,7 +2161,7 @@ pstree -A #查看所有进程树 -### 进程ID +### 进程 ID 进程号: @@ -2218,13 +2169,16 @@ pstree -A #查看所有进程树 * 进程号为 1 是 init 进程,是一个守护进程,在自举过程结束时由内核调用,init 进程绝不会终止,是一个普通的用户进程,但是它以超级用户特权运行 -父进程 ID 为 0 的进程通常是内核进程,它们作为系统自举过程的一部分而启动,init 进程是个例外,它的父进程是 0,但它是用户进程 +父进程 ID 为 0 的进程通常是内核进程,作为系统**自举过程**的一部分而启动,init 进程是个例外,它的父进程是 0,但它是用户进程 -主存 = RAM + BIOS 部分的 ROM +* 主存 = RAM + BIOS 部分的 ROM +* DISK:存放 OS 和 Bootloader +* BIOS:基于 I/O 处理系统 +* Bootloader:加载 OS,将 OS 放入内存 -自举程序存储在内存中 ROM(BIOS 芯片),用来加载操作系统。CPU 的程序计数器指向 ROM 中自举程序第一条指令,当计算机**通电**,CPU 开始读取并执行自举程序,将操作系统(不是全部,只是启动计算机的那部分程序)装入 RAM 中,这个过程是自举过程。装入完成后 CPU 的程序计数器就被设置为 RAM 中操作系统的第一条指令所对应的位置,接下来 CPU 将开始执行操作系统的指令 +自举程序存储在内存中 ROM,**用来加载操作系统**,初始化 CPU、寄存器、内存等。CPU 的程序计数器指自举程序第一条指令,当计算机**通电**,CPU 开始读取并执行自举程序,将操作系统(不是全部,只是启动计算机的那部分程序)装入 RAM 中,这个过程是自举过程。装入完成后程序计数器设置为 RAM 中操作系统的**第一条指令**,接下来 CPU 将开始执行(启动)操作系统的指令 -存储在 ROM 中保留很小的自举装入程序,完整功能的自举程序保存在磁盘的启动块上,启动块位于磁盘的固定位,拥有启动分区的磁盘称为启动磁盘或系统磁盘(C盘) +存储在 ROM 中保留很小的自举装入程序,完整功能的自举程序保存在磁盘的启动块上,启动块位于磁盘的固定位,拥有启动分区的磁盘称为启动磁盘或系统磁盘(C 盘) @@ -2245,9 +2199,7 @@ pstree -A #查看所有进程树 孤儿进程: * 一个父进程退出,而它的一个或多个子进程还在运行,那么这些子进程将成为孤儿进程 - -* 孤儿进程将被 init 进程所收养,并由 init 进程对它们完成状态收集工作 -* 孤儿进程会被 init 进程收养,所以孤儿进程不会对系统造成危害 +* 孤儿进程将被 init 进程所收养,并由 init 进程对它们完成状态收集工作,所以孤儿进程不会对系统造成危害 僵尸进程: @@ -2259,7 +2211,7 @@ pstree -A #查看所有进程树 补充: * 守护进程(daemon)是一类在后台运行的特殊进程,用于执行特定的系统任务。 -* 守护进程是脱离于终端并且在后台运行的进程,脱离终端是为了避免在执行的过程中的信息在终端上显示,并且进程也不会被任何终端所产生的终端信息所打断 +* 守护进程是**脱离于终端**并且在后台运行的进程,脱离终端是为了避免在执行的过程中的信息在终端上显示,并且进程也不会被任何终端所产生的终端信息所打断 * 很多守护进程在系统引导的时候启动,并且一直运行直到系统关闭;另一些只在需要的时候才启动,完成任务后就自动结束 @@ -2277,7 +2229,7 @@ pstree -A #查看所有进程树 - 得到 SIGCHLD 信号 - waitpid() 或者 wait() 调用会返回 -子进程发送的 SIGCHLD 信号包含了子进程的信息,比如进程 ID、进程状态、进程使用 CPU 的时间等;在子进程退出时,它的进程描述符不会立即释放,这是为了让父进程得到子进程信息,父进程通过 wait() 和 waitpid() 来获得一个已经退出的子进程的信息 +子进程发送的 SIGCHLD 信号包含了子进程的信息,比如进程 ID、进程状态、进程使用 CPU 的时间等;在子进程退出时进程描述符不会立即释放,父进程通过 wait() 和 waitpid() 来获得一个已经退出的子进程的信息,释放子进程的 PCB @@ -2293,7 +2245,7 @@ pid_t wait(int *status) 参数:status 用来保存被收集的子进程退出时的状态,如果不关心子进程**如何**销毁,可以设置这个参数为 NULL -父进程调用 wait() 会一直阻塞,直到收到一个子进程退出的 SIGCHLD 信号,wait() 函数就会销毁子进程并返回 +父进程调用 wait() 会阻塞等待,直到收到一个子进程退出的 SIGCHLD 信号,wait() 函数就会销毁子进程并返回 * 成功,返回被收集的子进程的进程 ID * 失败,返回 -1,同时 errno 被置为 ECHILD(如果调用进程没有子进程,调用就会失败) @@ -2352,13 +2304,12 @@ ifconfig [网络设备][down up -allmulti -arp -promisc][add<地址>][del<地址 ``` * `ifconfig`:显示激活的网卡信息 ens - - - **ens33(有的是eth0)**表示第一块网卡。 - 表示 ens33 网卡的 IP地址是 192.168.0.137,广播地址,broadcast 192.168.0.255,掩码地址netmask:255.255.255.0 ,inet6 对应的是 ipv6 - - **lo** 是表示主机的回坏地址,这个一般是用来测试一个网络程序,但又不想让局域网或外网的用户能够查看,只能在此台主机上运行和查看所用的网络接口 + + ens33(或 eth0)表示第一块网卡,IP地址是 192.168.0.137,广播地址 broadcast 192.168.0.255,掩码地址netmask 255.255.255.0 ,inet6 对应的是 ipv6 + + lo 是表示主机的**回坏地址**,用来测试一个网络程序,但又不想让局域网或外网的用户能够查看,只能在此台主机上运行和查看所用的网络接口 + * ifconfig ens33 down:关闭网卡 * ifconfig ens33 up:启用网卡 @@ -2370,9 +2321,9 @@ ifconfig [网络设备][down up -allmulti -arp -promisc][add<地址>][del<地址 ### ping -ping 命令用于检测主机。 +ping 命令用于检测主机 -执行 ping 指令会使用 ICMP 传输协议,发出要求回应的信息,若远端主机的网络功能没有问题,就会回应该信息,因而得知该主机运作正常。 +执行 ping 指令会使用 ICMP 传输协议,发出要求回应的信息,若远端主机的网络功能没有问题,就会回应该信息 ```shell ping [-dfnqrRv][-c<完成次数>][-i<间隔秒数>][-I<网络界面>][-l<前置载入>][-p<范本样式>][-s<数据包大小>][-t<存活数值>][主机名称或IP地址] @@ -2381,7 +2332,7 @@ ping [-dfnqrRv][-c<完成次数>][-i<间隔秒数>][-I<网络界面>][-l<前置 * -c<完成次数>:设置完成要求回应的次数; * `ping -c 2 www.baidu.com` - ![](https://gitee.com/seazean/images/raw/master/Tool/ping百度.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/ping百度.png) icmp_seq:ping 序列,从1开始 @@ -2431,15 +2382,13 @@ netstat [-acCeFghilMnNoprstuvVwx][-A<网络类型>][--ip] ### 挂载概念 -在安装 Linux 系统时设立的各个分区,如根分区、/boot 分区等都是自动挂载的,也就是说不需要我们人为操作,开机就会自动挂载。但是光盘、U 盘等存储设备如果需要使用,就必须人为的进行挂载。 - -在 Windows 下插入 U 盘也是需要挂载(分配盘符)的,只不过 Windows 下分配盘符是自动的。其实挂载可以理解为 Windows 当中的分配盘符(重要),只不过 Windows 当中是以英文字母 ABCD 等作为盘符,而 Linux 是拿系统目录作为盘符,当然 Linux 当中也不叫盘符,而是称为挂载点,而把为分区或者光盘等存储设备分配一个挂载点的过程称为挂载 - -Linux 中的根目录以外的文件要想被访问,需要将其“关联”到根目录下的某个目录来实现,这种关联操作就是挂载,这个目录就是挂载点,解除次关联关系的过程称之为卸载 +在安装 Linux 系统时设立的各个分区,如根分区、/boot 分区等都是自动挂载的,也就是说不需要人为操作,开机就会自动挂载。但是光盘、U 盘等存储设备如果需要使用,就必须人为的进行挂载 +在 Windows 下插入 U 盘也是需要挂载(分配盘符)的,只不过 Windows 下分配盘符是自动的。其实挂载可以理解为 Windows 当中的分配盘符,只不过 Windows 当中是以英文字母 ABCD 等作为盘符,而 Linux 是拿系统目录作为盘符,当然 Linux 当中也不叫盘符,而是称为挂载点,而把为分区或者光盘等存储设备分配一个挂载点的过程称为挂载 +Linux 中的根目录以外的文件要想被访问,需要将其关联到根目录下的某个目录来实现,这种关联操作就是挂载,这个目录就是挂载点,解除次关联关系的过程称之为卸载 -**注意:“挂载点”的目录需要以下几个要求:** +挂载点的目录需要以下几个要求: * 目录要先存在,可以用 mkdir 命令新建目录 * 挂载点目录不可被其他进程使用到 @@ -2458,26 +2407,26 @@ lsblk 命令的英文是 list block,即用于列出所有可用块设备的信 命令:lsblk [参数] * `lsblk`:以树状列出所有块设备 - ![](https://gitee.com/seazean/images/raw/master/Tool/可用块设备.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/可用块设备.png) - NAME:这是块设备名。 + NAME:这是块设备名 - MAJ:MIN : 本栏显示主要和次要设备号。 + MAJ:MIN : 本栏显示主要和次要设备号 - RM:本栏显示设备是否可移动设备。注意,在上面设备sr0的RM值等于1,这说明他们是可移动设备。 + RM:本栏显示设备是否可移动设备,在上面设备 sr0 的 RM 值等于 1,这说明他们是可移动设备 - SIZE:本栏列出设备的容量大小信息。 + SIZE:本栏列出设备的容量大小信息 - RO:该项表明设备是否为只读。在本案例中,所有设备的RO值为0,表明他们不是只读的。 + RO:该项表明设备是否为只读,在本案例中,所有设备的 RO 值为 0,表明他们不是只读的 - TYPE:本栏显示块设备是否是磁盘或磁盘上的一个分区。在本例中,sda和sdb是磁盘,而sr0是只读存储(rom)。 + TYPE:本栏显示块设备是否是磁盘或磁盘上的一个分区。在本例中,sda 和 sdb 是磁盘,而 sr0 是只读存储(rom)。 MOUNTPOINT:本栏指出设备挂载的挂载点。 * `lsblk -f`:不会列出所有空设备 - ![](https://gitee.com/seazean/images/raw/master/Tool/不包含空设备.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/不包含空设备.png) NAME表示设备名称 @@ -2501,10 +2450,10 @@ lsblk 命令的英文是 list block,即用于列出所有可用块设备的信 命令:df [options]... [FILE]... -* -h, 使用人类可读的格式(预设值是不加这个选项的...) -* --total 计算所有的数据之和 +* -h 使用人类可读的格式(预设值是不加这个选项的...) +* --total 计算所有的数据之和 -![](https://gitee.com/seazean/images/raw/master/Tool/磁盘管理.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/磁盘管理.png) 第一列指定文件系统的名称;第二列指定一个特定的文件系统,1K 是 1024 字节为单位的总容量;已用和可用列分别指定的容量;最后一个已用列指定使用的容量的百分比;最后一栏指定的文件系统的挂载点 @@ -2529,14 +2478,15 @@ mount [-fnrsvw] [-t vfstype] [-o options] device dir - -t:指定档案系统的型态,通常不必指定。mount 会自动选择正确的型态。 -通过挂载的方式查看Linux CD/DVD光驱,查看 ubuntu-20.04.1-desktop-amd64.iso的文件 +通过挂载的方式查看 Linux CD/DVD 光驱,查看 ubuntu-20.04.1-desktop-amd64.iso 的文件 * 进入【虚拟机】--【设置】,设置 CD/DVD 的内容,ubuntu-20.04.1-desktop-amd64.iso * 创建挂载点(注意:一般用户无法挂载 cdrom,只有 root 用户才可以操作) + `mkdir -p /mnt/cdrom `:切换到 root 下创建一个挂载点(其实就是创建一个目录) * 开始挂载 `mount -t auto /dev/cdrom /mnt/cdrom`:通过挂载点的方式查看上面的【ISO文件内容】 - ![挂载成功](https://gitee.com/seazean/images/raw/master/Tool/挂载成功.png) + ![挂载成功](https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/挂载成功.png) * 查看挂载内容:`ls -l -a ./mnt/cdrom/` * 卸载 cdrom:`umount /mnt/cdrom/` @@ -2555,15 +2505,13 @@ mount [-fnrsvw] [-t vfstype] [-o options] device dir ### 概述 -防火墙技术是通过有机结合各类用于安全管理与筛选的软件和硬件设备,帮助计算机网络于其内、外网之间构建一道相对隔绝的保护屏障,以保护用户资料与信息安全性的一种技术。 - -在默认情况下,Linux系统的防火墙状态是打开的,已经启动。 +防火墙技术是通过有机结合各类用于安全管理与筛选的软件和硬件设备,帮助计算机网络于其内、外网之间构建一道相对隔绝的保护屏障,以保护用户资料与信息安全性的一种技术。在默认情况下,Linux 系统的防火墙状态是打开的 ### 状态 -**启动语法:service 服务 status** +启动语法:service name status * 查看防火墙状态:`service iptables status` @@ -2582,7 +2530,7 @@ mount [-fnrsvw] [-t vfstype] [-o options] device dir * 添加放行端口:`-A INPUT -m state --state NEW -m tcp -p tcp --dport 端口号 -j ACCEPT` * 重新加载防火墙规则:`service iptables reload` -备注:默认情况下22端口号是放行的 +备注:默认情况下 22 端口号是放行的 @@ -2596,43 +2544,40 @@ mount [-fnrsvw] [-t vfstype] [-o options] device dir ## Shell -> shell 脚本类似于我们在 Windows 中编写的批处理文件,它的扩展名是.bat,比如我们启动 Tomcat(后面的课程我们会详细讲解)的时候经常启动的 startup.bat,就是 Windows 下的批处理文件。 -> 而在 Linux 中,shell脚本编写的文件是以 .sh 结尾的。比如 Tomcat 下我们经常使用 startup.sh 来启动我们的 Tomcat,这个 startup.sh 文件就是 shell 编写的。 - ### 入门 #### 概念 -Shell 脚本(shell script),是一种为 shell 编写的脚本程序。 +Shell 脚本(shell script),是一种为 shell 编写的脚本程序,又称 Shell 命令稿、程序化脚本,是一种计算机程序使用的文本文件,内容由一连串的 shell 命令组成,经由 Unix Shell 直译其内容后运作 + +Shell 被当成是一种脚本语言来设计,其运作方式与解释型语言相当,由 Unix shell 扮演命令行解释器的角色,在读取 shell 脚本之后,依序运行其中的 shell 命令,之后输出结果 + -[Shell](https://www.leiue.com/tags/shell) [脚本](https://www.leiue.com/tags/脚本)([Shell Script](https://www.leiue.com/tags/shell-script))又称 Shell 命令稿、程序化脚本,是一种计算机程序使用的文本文件,内容由一连串的 shell 命令组成,经由 Unix Shell 直译其内容后运作。Shell 被当成是一种脚本语言来设计,其运作方式与解释型语言相当,由 Unix shell 扮演命令行解释器的角色,在读取 shell 脚本之后,依序运行其中的 shell 命令,之后输出结果。利用 shell 脚本可以进行系统管理,文件操作等。 #### 环境 Shell 编程跟 JavaScript、php 编程一样,只要有一个能编写代码的文本编辑器和一个能解释执行的脚本解释器就可以了。 `cat /etc/shells`:查看解释器 -![](https://gitee.com/seazean/images/raw/master/Tool/shell环境.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/shell环境.png) Linux 的 Shell 种类众多,常见的有: - Bourne Shell(/usr/bin/sh或/bin/sh) -- Bourne Again Shell(/bin/bash) +- Bourne Again Shell(/bin/bash):Bash 是大多数Linux 系统默认的 Shell - C Shell(/usr/bin/csh) - K Shell(/usr/bin/ksh) - Shell for Root(/sbin/sh) - 等等…… -我们当前课程使用的是 Bash,也就是 Bourne Again Shell,由于易用和免费,Bash 在日常工作中被广泛使用。同时,Bash 也是大多数Linux 系统默认的 Shell - #### 第一个shell -* 新建s.sh文件:touch s.sh +* 新建 s.sh 文件:touch s.sh -* 编辑s.sh文件:vim s.sh +* 编辑 s.sh 文件:vim s.sh ```shell #!/bin/bash --- 指定脚本解释器 @@ -2649,7 +2594,7 @@ Linux 的 Shell 种类众多,常见的有: ! ``` -* 查看 s.sh文件:ls -l s.sh文件权限是【-rw-rw-r--】 +* 查看 s.sh文件:ls -l s.sh文件权限是【-rw-rw-r--】 * chmod a+x s.sh s.sh文件权限是【-rwxrwxr-x】 @@ -2659,9 +2604,9 @@ Linux 的 Shell 种类众多,常见的有: **注意:** -**#!** 是一个约定的标记,它告诉系统这个脚本需要什么解释器来执行,即使用哪一种 Shell。 +**#!** 是一个约定的标记,告诉系统这个脚本需要什么解释器来执行,即使用哪一种 Shell -echo 命令用于向窗口输出文本。 +echo 命令用于向窗口输出文本 @@ -3343,7 +3288,7 @@ Docker 架构: * **容器(Container)**:镜像(Image)和容器(Container)的关系,就像是面向对象程序设计中的类和对象一样,镜像是静态的定义,容器是镜像运行时的实体。容器可以被创建、启动、停止、删除、暂停等 * **仓库(Repository)**:仓库可看成一个代码控制中心,用来保存镜像 -![](https://gitee.com/seazean/images/raw/master/Tool/Docker-docker架构.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/Docker-docker架构.png) 安装步骤: @@ -3518,7 +3463,7 @@ sudo systemctl restart docker > Docker 容器和外部机器可以直接交换文件吗? > 容器之间想要进行数据交互? - + **数据卷**:数据卷是宿主机中的一个目录或文件,当容器目录和数据卷目录绑定后,对方的修改会立即同步 @@ -3554,7 +3499,7 @@ sudo systemctl restart docker * 多个容器挂载同一个数据卷 * 数据卷容器 - + * 创建启动c3数据卷容器,使用 –v 参数设置数据卷 @@ -3589,7 +3534,7 @@ sudo systemctl restart docker * 当容器中的网络服务需要被外部机器访问时,可以将容器中提供服务的端口映射到宿主机的端口上。外部机器访问宿主机的该端口,从而间接访问容器的服务。这种操作称为:**端口映射** - ![](https://gitee.com/seazean/images/raw/master/Tool/Docker-MySQL部署.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/Docker-MySQL部署.png) MySQL部署步骤:搜索mysql镜像,拉取mysql镜像,创建容器,操作容器中的mysql @@ -3849,7 +3794,7 @@ Docker镜像原理: ### 镜像制作 -![](https://gitee.com/seazean/images/raw/master/Tool/Docker-Docker镜像原理.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/Docker-Docker镜像原理.png) **** @@ -3954,7 +3899,7 @@ Docker Compose是一个编排多容器分布式部署的工具,提供命令集 3. 运行 docker-compose up 启动应用 -![](https://gitee.com/seazean/images/raw/master/Tool/Docker-Compose原理.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/Docker-Compose原理.png) @@ -4098,7 +4043,7 @@ Docker官方的Docker hub(https://hub.docker.com)是一个用于管理公共 * 容器虚拟化的是操作系统,虚拟机虚拟化的是硬件。 * 传统虚拟机可以运行不同的操作系统,容器只能运行同一类型操作系统 - ![](https://gitee.com/seazean/images/raw/master/Tool/Docker-容器和虚拟机对比.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/Docker-容器和虚拟机对比.png) | 特性 | 容器 | 虚拟机 | | ---------- | ------------------ | ---------- | diff --git a/Web.md b/Web.md index 3bf591f..1aeab98 100644 --- a/Web.md +++ b/Web.md @@ -4,7 +4,7 @@ ### 概述 -HTML(超文本标记语言—HyperText Markup Language)是构成 Web 世界的一砖一瓦。它是一种用来告知浏览器如何组织页面的标记语言。 +HTML(超文本标记语言—HyperText Markup Language)是构成 Web 世界的基础,是一种用来告知浏览器如何组织页面的标记语言 * 超文本 Hypertext,是指连接单个或者多个网站间的网页的链接。通过链接,就能访问互联网中的内容 @@ -18,6 +18,8 @@ HTML(超文本标记语言—HyperText Markup Language)是构成 Web 世界 +参考视频:https://www.bilibili.com/video/BV1Qf4y1T7Hx + *** @@ -64,7 +66,7 @@ HTML 标签可以拥有属性 ### 结构 -![HTML结构](https://gitee.com/seazean/images/raw/master/Web/HTML结构.png) +![HTML结构](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/HTML结构.png) 文档结构介绍: @@ -286,7 +288,7 @@ HTML 标签可以拥有属性 **效果如下**: -![](https://gitee.com/seazean/images/raw/master/Web/HTML文本标签效果图.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/HTML文本标签效果图.png) @@ -364,7 +366,7 @@ target属性取值: 效果图: - + *** @@ -499,7 +501,7 @@ button标签:表示按钮 使用方式:以name属性值作为键,value属性值作为值,构成键值对提交到服务器,多个键值对浏览器使用`&`进行分隔。 -![](https://gitee.com/seazean/images/raw/master/Web/HTML标签input属性-name-value.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/HTML标签input属性-name-value.png) @@ -587,7 +589,7 @@ button标签:表示按钮 ``` -![](https://gitee.com/seazean/images/raw/master/Web/HTML标签input属性-type.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/HTML标签input属性-type.png) @@ -645,7 +647,7 @@ button标签:表示按钮 ``` -![](https://gitee.com/seazean/images/raw/master/Web/HTML标签select和文本域属性.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/HTML标签select和文本域属性.png) *** @@ -690,7 +692,7 @@ button标签:表示按钮 * tr:table row,表示表中单元的行 * td:table data,表示表中一个单元格 * th:table header,表格单元格的表头,通常字体样式加粗居中 -* ![](https://gitee.com/seazean/images/raw/master/Web/HTML表格标签.png) +* ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/HTML表格标签.png) @@ -778,7 +780,7 @@ button标签:表示按钮 效果图: -![](https://gitee.com/seazean/images/raw/master/Web/HTML表格标签跨行跨列效果图.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/HTML表格标签跨行跨列效果图.png) @@ -861,7 +863,7 @@ background属性用来设置背景相关的样式。 ``` - ![](https://gitee.com/seazean/images/raw/master/Web/HTML背景图.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/HTML背景图.png) @@ -881,7 +883,7 @@ background属性用来设置背景相关的样式。 } ``` - ![](https://gitee.com/seazean/images/raw/master/Web/HTML背景设计.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/HTML背景设计.png) *** @@ -905,7 +907,7 @@ background属性用来设置背景相关的样式。
right
``` - ![](https://gitee.com/seazean/images/raw/master/Web/HTML-div简单布局.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/HTML-div简单布局.png) @@ -1013,7 +1015,7 @@ background属性用来设置背景相关的样式。 ``` - ![](https://gitee.com/seazean/images/raw/master/Web/HTML-div基本布局.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/HTML-div基本布局.png) @@ -1032,7 +1034,7 @@ background属性用来设置背景相关的样式。 | **article** | 文章元素 | 表示独立内容区域 | 标签定义的内容本身必须是有意义且必须独立于文档的其他部分 | | **footer** | 页脚元素 | 表示页面的底部 | 块元素,文档中可以定义多个 | -![](https://gitee.com/seazean/images/raw/master/Web/语义化标签结构图.jpg) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/语义化标签结构图.jpg) @@ -1109,7 +1111,7 @@ background属性用来设置背景相关的样式。 ``` -![](https://gitee.com/seazean/images/raw/master/Web/HTML标签video.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/HTML标签video.png) @@ -1194,7 +1196,7 @@ CSS是一门基于规则的语言—你能定义用于你的网页中**特定元 } ``` -![](https://gitee.com/seazean/images/raw/master/Web/CSS的组成.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/CSS的组成.png) @@ -1665,7 +1667,7 @@ h1 { ``` - + @@ -1697,7 +1699,7 @@ h1 { ``` -![](https://gitee.com/seazean/images/raw/master/Web/CSS边框轮廓效果图.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/CSS边框轮廓效果图.png) *** @@ -1709,7 +1711,7 @@ h1 { 盒子模型是通过设置**元素框**与**元素内容**和**外部元素**的边距,而进行布局的方式。 -![](https://gitee.com/seazean/images/raw/master/Web/CSS盒子模型.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/CSS盒子模型.png) - element : 元素。 - padding : 内边距,也有资料将其翻译为填充。 @@ -1805,7 +1807,7 @@ h1 { ``` - ![](https://gitee.com/seazean/images/raw/master/Web/CSS盒子模式-效果图2.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/CSS盒子模式-效果图2.png) @@ -1881,7 +1883,7 @@ span{ 微信 ``` -![](https://gitee.com/seazean/images/raw/master/Web/CSS-文本样式效果图.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/CSS-文本样式效果图.png) @@ -2061,8 +2063,6 @@ a{ ``` -![](https://gitee.com/seazean/images/raw/master/Web/CSS案例登陆页面.png) - @@ -2095,36 +2095,38 @@ HTTP 作用:用于定义 WEB 浏览器与 WEB 服务器之间交换数据的 URL 和 URI * URL:统一资源定位符 - 格式:http://127.0.0.1:8080/request/servletDemo01 - 详解:http:协议;127.0.0.1:域名;8080:端口;request/servletDemo01:请求资源路径 + * 格式:http://127.0.0.1:8080/request/servletDemo01 + * 详解:http:协议;127.0.0.1:域名;8080:端口;request/servletDemo01:请求资源路径 * URI:统一资源标志符 - 格式:/request/servletDemo01 - -* 区别:`URL-HOST=URI`,URI是抽象的定义,URL用地址定位,URI 用名称定位。**只要能唯一标识资源的是 URI,在 URI 的基础上给出其资源的访问方式的是 URL** + * 格式:/request/servletDemo01 -短连接和长连接: +* 区别:`URL - HOST = URI`,URI 是抽象的定义,URL 用地址定位,URI 用名称定位。**只要能唯一标识资源的是 URI,在 URI 的基础上给出其资源的访问方式的是 URL** -* 短连接:客户端和服务器每进行一次 HTTP 操作,就建立一次连接,任务结束就中断连接。 +**从浏览器地址栏输入 URL 到请求返回发生了什么?** - 使用短连接的情况下,当浏览器访问的某个 HTML 或其他类型的 Web 页中包含有其他的 Web 资源(图像文件、CSS文件等),每遇到这样一个 Web 资源,浏览器就会经过三次握手重新建立一个 HTTP 会话 +* 进行 URL 解析,进行编码 -* 长连接:使用长连接的 HTTP 协议,会在响应头加入这行代码 `Connection:keep-alive` +* DNS 解析,顺序是先查 hosts 文件是否有记录,有的话就会把相对应映射的 IP 返回,然后去本地 DNS 缓存中寻找,然后依次向本地域名服务器、根域名服务器、顶级域名服务器、权限域名服务器发起查询请求,最终返回 IP 地址给本地域名服务器 - 使用长连接的情况下,当一个网页打开完成后,客户端和服务器之间用于传输 HTTP 数据的 TCP 连接不会关闭,客户端再次访问这个服务器时,会继续使用这一条已经建立的连接。Keep-Alive 有一个保持时间,不会永久保持连接,设置以后可以实现长连接,前提是需要客户端和服务端都支持长连接 + 本地域名服务器将得到的 IP 地址返回给操作系统,同时将 IP 地址缓存起来;操作系统将 IP 地址返回给浏览器,同时自己也将 IP 地址缓存起来 -* HTTP 协议的长连接和短连接,实质上是 TCP 协议的长连接和短连接 +* 查找到 IP 之后,进行 TCP 协议的三次握手建立连接 -**从浏览器地址栏输入 URL 到请求返回发生了什么?** +* 发出 HTTP 请求,取文件指令 -* 进行 URL 解析,进行编码 -* DNS 解析,顺序是先查 hosts 文件是否有记录,有的话就会把相对应映射的 IP 返回,然后去本地 DNS 缓存中寻找,然后去找计算机上配置的 DNS 服务器上有或者有缓存,最后去找全球的根 DNS 服务器,直到查到为止 -* 查找到 IP 之后,进行 TCP 协议的三次握手,发出 HTTP 请求 * 服务器处理请求,返回响应 + +* 释放 TCP 连接 + * 浏览器解析渲染页面 +推荐阅读:https://xiaolincoding.com/network/ + + + *** @@ -2135,22 +2137,42 @@ URL 和 URI * HTTP/0.9 仅支持 GET 请求,不支持请求头 * HTTP/1.0 默认短连接(一次请求建议一次 TCP 连接,请求完就断开),支持 GET、POST、 HEAD 请求 -* HTTP/1.1 默认长连接(一次 TCP 连接可以多次请求);支持 PUT、DELETE、PATCH 等六种请求;增加 host 头,支持虚拟主机;支持**断点续传**功能 -* HTTP/2.0 多路复用,降低开销(一次 TCP 连接可以处理多个请求);服务器主动推送(相关资源一个请求全部推送);解析基于二进制,解析错误少,更高效(HTTP/1.X 解析基于文本);报头压缩,降低开销。 +* HTTP/1.1 默认长连接(一次 TCP 连接可以多次请求);支持 PUT、DELETE、PATCH 等六种请求;增加 HOST 头,支持虚拟主机;支持**断点续传**功能 +* HTTP/2.0 多路复用,降低开销(一次 TCP 连接可以处理多个请求);服务器主动推送(相关资源一个请求全部推送);解析基于二进制,解析错误少,更高效(HTTP/1.X 解析基于文本);报头压缩,降低开销 +* HTTP/3.0 QUIC (Quick UDP Internet Connections),快速 UDP 互联网连接,基于 UDP 协议 HTTP 1.0 和 HTTP 1.1 的主要区别: * 长短连接: - **在HTTP/1.0中,默认使用的是短连接**,每次请求都要重新建立一次连接。HTTP 基于 TCP/IP 协议的,每一次建立或者断开连接都需要三次握手四次挥手,开销会比较大 + **在HTTP/1.0中,默认使用的是短连接**,每次请求都要重新建立一次连接,比如获取 HTML 和 CSS 文件,需要两次请求。HTTP 基于 TCP/IP 协议的,每一次建立或者断开连接都需要三次握手四次挥手,开销会比较大 + + **HTTP 1.1起,默认使用长连接** ,默认开启 `Connection: keep-alive`,Keep-Alive 有一个保持时间,不会永久保持连接。持续连接有非流水线方式和流水线方式 ,流水线方式是客户端在收到 HTTP 的响应报文之前就能接着发送新的请求报文,非流水线方式是客户端在收到前一个响应后才能发送下一个请求 - **HTTP 1.1起,默认使用长连接** ,默认开启 `Connection: keep-alive`,HTTP/1.1 的持续连接有非流水线方式和流水线方式 ,流水线方式是客户端在收到 HTTP 的响应报文之前就能接着发送新的请求报文,非流水线方式是客户端在收到前一个响应后才能发送下一个请求 + HTTP 协议的长连接和短连接,实质上是 TCP 协议的长连接和短连接 * 错误状态响应码:在 HTTP1.1 中新增了 24 个错误状态响应码,如 409(Conflict)表示请求的资源与资源的当前状态发生冲突,410(Gone)表示服务器上的某个资源被永久性的删除 -* 缓存处理:在 HTTP1.0 中主要使用 header 里的 If-Modified-Since,Expires 来做为缓存判断的标准,HTTP1.1 则引入了更多的缓存控制策略例如 Entity tag,If-Unmodified-Since,If-Match,If-None-Match等更多可供选择的缓存头来控制缓存策略 +* 缓存处理:在 HTTP1.0 中主要使用 header 里的 If-Modified-Since,Expires 来做为缓存判断的标准,HTTP1.1 则引入了更多的缓存控制策略,例如 Entity tag,If-Unmodified-Since,If-Match,If-None-Match等 + +* 带宽优化及网络连接的使用:HTTP1.0 存在一些浪费带宽的现象,例如客户端只需要某个对象的一部分,而服务器却将整个对象送过来了,并且不支持**断点续传**功能,HTTP1.1 则在请求头引入了 range 头域,允许只**请求资源的某个部分**,即返回码是 206(Partial Content),这样就方便了开发者自由的选择以便于充分利用带宽和连接 + +* HOST 头处理:在 HTTP1.0 中认为每台服务器都绑定一个唯一的 IP 地址,因此请求消息中的 URL 并没有传递主机名。HTTP1.1 时代虚拟主机技术发展迅速,在一台物理服务器上可以存在多个虚拟主机,并且共享一个 IP 地址,故 HTTP1.1 增加了 HOST 信息 -* 带宽优化及网络连接的使用:HTTP1.0 中,存在一些浪费带宽的现象,例如客户端只需要某个对象的一部分,而服务器却将整个对象送过来了,并且不支持断点续传功能,HTTP1.1 则在请求头引入了 range 头域,允许只请求资源的某个部分,即返回码是 206(Partial Content),这样就方便了开发者自由的选择以便于充分利用带宽和连接 +HTTP 1.1 和 HTTP 2.0 的主要区别: + +* 新的二进制格式:HTTP1.1 基于文本格式传输数据,HTTP2.0 采用二进制格式传输数据,解析更高效 +* **多路复用**:在一个连接里,允许同时发送多个请求或响应,并且这些请求或响应能够并行的传输而不被阻塞,避免 HTTP1.1 出现的队头堵塞问题 +* 头部压缩,HTTP1.1 的 header 带有大量信息,而且每次都要重复发送;HTTP2.0 把 header 从数据中分离,并封装成头帧和数据帧,使用特定算法压缩头帧。并且 HTTP2.0 在客户端和服务器端记录了之前发送的键值对,对于相同的数据不会重复发送。比如请求 A 发送了所有的头信息字段,请求 B 则只需要发送差异数据,这样可以减少冗余数据,降低开销 +* **服务端推送**:HTTP2.0 允许服务器向客户端推送资源,无需客户端发送请求到服务器获取 + + + +**** + + + +## 安全请求 HTTP 和 HTTPS 的区别: @@ -2160,14 +2182,16 @@ HTTP 和 HTTPS 的区别: **对称加密和非对称加密** -* 对称加密:加密和解密使用同一个秘钥,把密钥转发给需要发送数据的客户机,中途会被拦截(类似于把带锁的箱子和钥匙给别人,对方打开箱子放入数据,上锁后发送),典型的对称加密算法有 DES、AES 等 +* 对称加密:加密和解密使用同一个秘钥,把密钥转发给需要发送数据的客户机,中途会被拦截(类似于把带锁的箱子和钥匙给别人,对方打开箱子放入数据,上锁后发送),私钥用来解密数据,典型的对称加密算法有 DES、AES 等 * 优点:运算速度快 * 缺点:无法安全的将密钥传输给通信方 -* 非对称加密:加密和解密使用不同的秘钥,一把作为公开的公钥,另一把作为私钥,公钥公开给任何人(类似于把锁和箱子给别人,对方打开箱子放入数据,上锁后发送),典型的非对称加密算法有RSA、DSA等 - * 优点:可以更安全地将公开密钥传输给通信发送方 - * 缺点:运算速度慢 - +* 非对称加密:加密和解密使用不同的秘钥,一把作为公开的公钥,另一把作为私钥,**公钥公开给任何人**(类似于把锁和箱子给别人,对方打开箱子放入数据,上锁后发送),典型的非对称加密算法有 RSA、DSA 等 + + * 公钥加密,私钥解密:为了**保证内容传输的安全**,因为被公钥加密的内容,其他人是无法解密的,只有持有私钥的人,才能解密出实际的内容 + * 私钥加密,公钥解密:为了**保证消息不会被冒充**,因为私钥是不可泄露的,如果公钥能正常解密出私钥加密的内容,就能证明这个消息是来源于持有私钥身份的人发送的 + * 可以更安全地将公开密钥传输给通信发送方,但是运算速度慢 + * **使用对称加密和非对称加密的方式传送数据** * 使用非对称密钥加密方式,传输对称密钥加密方式所需要的 Secret Key,从而保证安全性 @@ -2175,27 +2199,31 @@ HTTP 和 HTTPS 的区别: 思想:锁上加锁 -HTTPS 工作流程:服务器端的公钥和私钥,用来进行非对称加密,客户端生成的随机密钥,用来进行对称加密 +名词解释: -![](https://gitee.com/seazean/images/raw/master/Web/HTTP-HTTPS加密过程.png) +* 哈希算法:通过哈希函数计算出内容的哈希值,传输到对端后会重新计算内容的哈希,进行哈希比对来校验内容的完整性 -1. 客户端向服务器发起 HTTPS 请求,连接到服务器的 443 端口 +* 数字签名:附加在报文上的特殊加密校验码,可以防止报文被篡改。一般是通过私钥对内容的哈希值进行加密,公钥正常解密并对比哈希值后,可以确保该内容就是对端发出的,防止出现中间人替换的问题 -2. 服务器端有一个密钥对,即公钥和私钥,用来进行非对称加密,服务器端保存着私钥不能泄露,公钥可以发给任何客户端 +* 数字证书:由权威机构给某网站颁发的一种认可凭证 -3. 服务器将公钥发送给客户端 +HTTPS 工作流程:服务器端的公钥和私钥,用来进行非对称加密,客户端生成的随机密钥,用来进行对称加密 -4. 客户端收到服务器端的数字证书之后,会对证书进行检查,验证其合法性,如果发现发现证书有问题,那么 HTTPS 传输就无法继续。如果公钥合格,那么客户端会生成一个随机值,这个随机值就是用于进行对称加密的密钥,将该密钥称之为 client key,即客户端密钥。然后用服务器的公钥对客户端密钥进行非对称加密,这样客户端密钥就变成密文了,HTTPS 中的第一次 HTTP 请求结束 +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/HTTP-HTTPS加密过程.png) +1. 客户端向服务器发起 HTTPS 请求,连接到服务器的 443 端口,请求携带了浏览器支持的加密算法和哈希算法,协商加密算法 +2. 服务器端会向数字证书认证机构注册公开密钥,认证机构**用 CA 私钥**对公开密钥做数字签名后绑定在数字证书(又叫公钥证书,内容有公钥,网站地址,证书颁发机构,失效日期等) +3. 服务器将数字证书发送给客户端,私钥由服务器持有 +4. 客户端收到服务器端的数字证书后**通过 CA 公钥**(事先置入浏览器或操作系统)对证书进行检查,验证其合法性。如果公钥合格,那么客户端会生成一个随机值,这个随机值就是用于进行对称加密的密钥,将该密钥称之为 client key(客户端密钥、会话密钥)。用服务器的公钥对客户端密钥进行非对称加密,这样客户端密钥就变成密文,HTTPS 中的第一次 HTTP 请求结束 5. 客户端会发起 HTTPS 中的第二个 HTTP 请求,将加密之后的客户端密钥发送给服务器 - 6. 服务器接收到客户端发来的密文之后,会用自己的私钥对其进行非对称解密,解密之后的明文就是客户端密钥,然后用客户端密钥对数据进行对称加密,这样数据就变成了密文 - 7. 服务器将加密后的密文发送给客户端 - 8. 客户端收到服务器发送来的密文,用客户端密钥对其进行对称解密,得到服务器发送的数据,这样 HTTPS 中的第二个 HTTP 请求结束,整个 HTTPS 传输完成 + +参考文章:https://www.cnblogs.com/linianhui/p/security-https-workflow.html + 参考文章:https://www.jianshu.com/p/14cd2c9d2cd2 @@ -2210,15 +2238,15 @@ HTTPS 工作流程:服务器端的公钥和私钥,用来进行非对称加 请求头: 从第二行开始,到第一个空行结束 -请求体: 从第一个空行后开始,到正文的结束(GET没有) +请求体: 从第一个空行后开始,到正文的结束(GET 没有) * 请求方式 * POST - ![](https://gitee.com/seazean/images/raw/master/Web/HTTP请求部分.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/HTTP请求部分.png) - * Get + * GET ```html 【请求行】 @@ -2235,24 +2263,32 @@ HTTPS 工作流程:服务器端的公钥和私钥,用来进行非对称加 Cookie: Idea-b77ddca6=4bc282fe-febf-4fd1-b6c9-72e9e0f381e8 ``` - * 面试题:**Get 和POST比较** + * **GET 和 POST 比较** 作用:GET 用于获取资源,而 POST 用于传输实体主体 - 参数:GET 和 POST 的请求都能使用额外的参数,但是 GET 的参数是以查询字符串出现在 URL 中,而 POST 的参数存储在实体主体中。不能因为 POST 参数存储在实体主体中就认为它的安全性更高,因为照样可以通过一些抓包工具(Fiddler)查看 + 参数:GET 和 POST 的请求都能使用额外的参数,但是 GET 的参数是以查询字符串出现在 URL 中,而 POST 的参数存储在实体主体中(GET 也有请求体,POST 也可以通过 URL 传输参数)。不能因为 POST 参数存储在实体主体中就认为它的安全性更高,因为照样可以通过一些抓包工具(Fiddler)查看 - 安全:安全的 HTTP 方法不会改变服务器状态,也就是说它只是可读的。GET方法是安全的,而POST不是,因为 POST 的目的是传送实体主体内容 + 安全:安全的 HTTP 方法不会改变服务器状态,也就是说它只是可读的。GET 方法是安全的,而 POST 不是,因为 POST 的目的是传送实体主体内容 * 安全的方法除了 GET 之外还有:HEAD、OPTIONS * 不安全的方法除了 POST 之外还有 PUT、DELETE - 幂等性:同样的请求**被执行一次与连续执行多次的效果是一样的**,服务器的状态也是一样的。所有的安全方法也都是幂等的。在正确实现条件下,GET,HEAD,PUT 和 DELETE 等方法都是幂等的,POST 方法不是 + 幂等性:同样的请求**被执行一次与连续执行多次的效果是一样的**,服务器的状态也是一样的,所有的安全方法也都是幂等的。在正确实现条件下,GET,HEAD,PUT 和 DELETE 等方法都是幂等的,POST 方法不是 可缓存:如果要对响应进行缓存,需要满足以下条件 * 请求报文的 HTTP 方法本身是可缓存的,包括 GET 和 HEAD,但是 PUT 和 DELETE 不可缓存,POST 在多数情况下不可缓存 * 响应报文的状态码是可缓存的,包括:200、203、204、206、300、301、404、405、410、414 and 501 * 响应报文的 Cache-Control 首部字段没有指定不进行缓存 + + * PUT 和 POST 的区别 + + PUT 请求:如果两个请求相同,后一个请求会把第一个请求覆盖掉(幂等),所以 PUT 用来修改资源 + + POST 请求:后一个请求不会把第一个请求覆盖掉(非幂等),所以 POST 用来创建资源 + + PATCH 方法 是新引入的,是对 PUT 方法的补充,用来对已知资源进行**局部更新** @@ -2272,32 +2308,33 @@ HTTPS 工作流程:服务器端的公钥和私钥,用来进行非对称加 * 请求头详解 - 从第2行到空行处,都叫请求头,以键值对的形式存在,但存在一个key对应多个值的请求头 - + + 从第 2 行到空行处,都叫请求头,以键值对的形式存在,但存在一个 key 对应多个值的请求头 + | 内容 | 说明 | | ----------------- | ------------------------------------------------------------ | - | Accept | 告知服务器,客户浏览器支持的MIME类型 | + | Accept | 告知服务器,客户浏览器支持的 MIME 类型 | | User-Agent | 浏览器相关信息 | | Accept-Charset | 告诉服务器,客户浏览器支持哪种字符集 | - | Accept-Encoding | 告知服务器,客户浏览器支持的压缩编码格式。常用gzip压缩 | - | Accept-Language | 告知服务器,客户浏览器支持的语言。zh_CN或en_US等 | - | Host | 初始URL中的主机和端口 | + | Accept-Encoding | 告知服务器,客户浏览器支持的压缩编码格式,常用 gzip 压缩 | + | Accept-Language | 告知服务器,客户浏览器支持的语言,zh_CN 或 en_US 等 | + | Host | 初始 URL 中的主机和端口 | | Referer | 告知服务器,当前请求的来源。只有当前请求有来源,才有这个消息头。
作用:1 投放广告 2 防盗链 | - | Content-Type | 告知服务器,请求正文的MIME类型,文件传输的类型,
application/x-www-form-urlencoded | + | Content-Type | 告知服务器,请求正文的 MIME 类型,文件传输的类型,
application/x-www-form-urlencoded | | Content-Length | 告知服务器,请求正文的长度。 | - | Connection | 表示是否需要持久连接。一般是“Keep -Alive”(HTTP 1.1默认进行持久连接 ) | + | Connection | 表示是否需要持久连接,一般是 `Keep -Alive`(HTTP 1.1 默认进行持久连接 ) | | If-Modified-Since | 告知服务器,客户浏览器缓存文件的最后修改时间 | - | Cookie | 会话管理相关(非常的重要) | + | Cookie | 会话管理相关(非常的重要) | * 请求体详解 - * 只有post请求方式,才有请求的正文,get方式的正文是在地址栏中的。 + * 只有 POST 请求方式,才有请求的正文,GET 方式的正文是在地址栏中的 - * 表单的输入域有name属性的才会被提交。不分get和post的请求方式。 + * 表单的输入域有 name 属性的才会被提交,不分 GET 和 POST 的请求方式 - * 表单的enctype属性取值决定了请求正文的体现形式。 + * 表单的 enctype 属性取值决定了请求正文的体现形式 | enctype取值 | 请求正文体现形式 | 示例 | | --------------------------------- | -------------------------------------------------- | ------------------------------------------------------------ | @@ -2314,18 +2351,20 @@ HTTPS 工作流程:服务器端的公钥和私钥,用来进行非对称加 响应部分图: -![](https://gitee.com/seazean/images/raw/master/Web/HTTP响应部分.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/HTTP响应部分.png) * 响应行 HTTP/1.1:使用协议的版本 + 200:响应状态码 + OK:状态码描述 * 响应状态码: - ![](https://gitee.com/seazean/images/raw/master/Web/HTTP状态响应码.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/HTTP状态响应码.png) | 状态码 | 说明 | | ------- | -------------------------------------------------- | @@ -2333,29 +2372,29 @@ HTTPS 工作流程:服务器端的公钥和私钥,用来进行非对称加 | 302/307 | 请求重定向(客户端行为,两次请求,地址栏发生改变) | | 304 | 请求资源未改变,使用缓存 | | 400 | 客户端错误,请求错误,最常见的就是请求参数有问题 | - | 403 | 客户端错误,但 forbidden权 限不够,拒绝处理 | + | 403 | 客户端错误,但 forbidden 权限不够,拒绝处理 | | 404 | 客户端错误,请求资源未找到 | | 500 | 服务器错误,服务器运行内部错误 | - 面试题: + 转移: - * 301 redirect: 301 代表永久性转移 (Permanently Moved) - * 302 redirect: 302 代表暂时性转移 (Temporarily Moved ) + * 301 redirect:301 代表永久性转移 (Permanently Moved) + * 302 redirect:302 代表暂时性转移 (Temporarily Moved ) -* 响应头:以 key:vaue 存在,可能多个 value 情况。 +* 响应头:以 key:vaue 存在,可能多个 value 情况 | 消息头 | 说明 | | ----------------------- | ------------------------------------------------------------ | - | Location | 请求重定向的地址,常与302,307配合使用。 | - | Server | 服务器相关信息。 | - | Content-Type | 告知客户浏览器,响应正文的MIME类型。 | - | Content-Length | 告知客户浏览器,响应正文的长度。 | - | Content-Encoding | 告知客户浏览器,响应正文使用的压缩编码格式。常用的gzip压缩。 | - | Content-Language | 告知客户浏览器,响应正文的语言。zh_CN或en_US等。 | - | Content-Disposition | 告知客户浏览器,以下载的方式打开响应正文。 | - | Refresh | 客户端的刷新频率。单位是秒 | - | Last-Modified | 服务器资源的最后修改时间。 | - | Set-Cookie | 服务器端发送的Cookie,会话管理相关 | + | Location | 请求重定向的地址,常与 302,307 配合使用。 | + | Server | 服务器相关信息 | + | Content-Type | 告知客户浏览器,响应正文的MIME类型 | + | Content-Length | 告知客户浏览器,响应正文的长度 | + | Content-Encoding | 告知客户浏览器,响应正文使用的压缩编码格式,常用的 gzip 压缩 | + | Content-Language | 告知客户浏览器,响应正文的语言,zh_CN 或 en_US 等 | + | Content-Disposition | 告知客户浏览器,以下载的方式打开响应正文 | + | Refresh | 客户端的刷新频率,单位是秒 | + | Last-Modified | 服务器资源的最后修改时间 | + | Set-Cookie | 服务器端发送的 Cookie,会话管理相关 | | Expires:-1 | 服务器资源到客户浏览器后的缓存时间 | | Catch-Control: no-catch | 不要缓存,//针对http协议1.1版本 | | Pragma:no-catch | 不要缓存,//针对http协议1.0版本 | @@ -2392,13 +2431,13 @@ HTTPS 工作流程:服务器端的公钥和私钥,用来进行非对称加 ### JavaEE规范 -`JavaEE`规范是`J2EE`规范的新名称,早期被称为`J2EE`规范,其全称是`Java 2 Platform Enterprise Edition`,它是由SUN公司领导、各厂家共同制定并得到广泛认可的工业标准(`JCP`组织成员)。之所以改名为`JavaEE`,目的还是让大家清楚`J2EE`只是`Java`企业应用。在2004年底中国软件技术大会`Ioc`微容器(也就是`Jdon`框架的实现原理)演讲中指出:我们需要一个跨`J2SE/WEB/EJB`的微容器,保护我们的业务核心组件,以延续它的生命力,而不是依赖`J2SE/J2EE`版本。此次`J2EE`改名为`Java EE`,实际也反映出业界这种共同心声。 +`JavaEE` 规范是 `J2EE` 规范的新名称,早期被称为 `J2EE` 规范,其全称是 `Java 2 Platform Enterprise Edition`,它是由 SUN 公司领导、各厂家共同制定并得到广泛认可的工业标准(`JCP`组织成员)。之所以改名为`JavaEE`,目的还是让大家清楚 `J2EE` 只是 `Java` 企业应用。在 2004 年底中国软件技术大会 `Ioc` 微容器(也就是 `Jdon` 框架的实现原理)演讲中指出:我们需要一个跨 `J2SE/WEB/EJB` 的微容器,保护我们的业务核心组件,以延续它的生命力,而不是依赖 `J2SE/J2EE` 版本。此次 `J2EE` 改名为 `Java EE`,实际也反映出业界这种共同心声 -`JavaEE`规范是很多Java开发技术的总称。这些技术规范都是沿用自`J2EE`的。一共包括了13个技术规范。例如:`jsp/servlet`,`jndi`,`jaxp`,`jdbc`,`jni`,`jaxb`,`jmf`,`jta`,`jpa`,`EJB`等。 +`JavaEE` 规范是很多 Java 开发技术的总称。这些技术规范都是沿用自 `J2EE` 的。一共包括了 13 个技术规范,例如:`jsp/servlet`,`jndi`,`jaxp`,`jdbc`,`jni`,`jaxb`,`jmf`,`jta`,`jpa`,`EJB`等。 -其中,`JCP`组织的全称是Java Community Process。它是一个开放的国际组织,主要由Java开发者以及被授权者组成,职能是发展和更新。成立于1998年。官网是:[JCP](https://jcp.org/en/home/index) +其中,`JCP` 组织的全称是 Java Community Process,是一个开放的国际组织,主要由 Java 开发者以及被授权者组成,职能是发展和更新。成立于 1998 年。官网是:[JCP](https://jcp.org/en/home/index) -`JavaEE`的版本是延续了`J2EE`的版本,但是没有继续采用其命名规则。`J2EE`的版本从1.0开始到1.4结束,而`JavaEE`版本是从`JavaEE 5`版本开始,目前最新的的版本是`JavaEE 8`。 +`JavaEE` 的版本是延续了 `J2EE` 的版本,但是没有继续采用其命名规则。`J2EE` 的版本从 1.0 开始到 1.4 结束,而 `JavaEE` 版本是从 `JavaEE 5` 版本开始,目前最新的的版本是 `JavaEE 8` 详情请参考:[JavaEE8 规范概览](https://www.oracle.com/technetwork/cn/java/javaee/overview/index.html) @@ -2408,15 +2447,15 @@ HTTPS 工作流程:服务器端的公钥和私钥,用来进行非对称加 -### Web概述 +### Web 概述 -Web,在计算机领域指网络。像我们接触的`WWW`,它是由3个单词组成的,即:`World Wide Web `,中文含义是万维网。而我们前面学的HTML的参考文档《W3School全套教程》中的`W3C`就是万维网联盟。他们的出现都是为了让我们在网络的世界中获取资源,这些资源的存放之处,我们称之为网站。我们通过输入网站的地址(网址),就可以访问网站中提供的资源。在网上我们能访问到的内容全是资源(不区分局域网还是广域网)。只不过,不同类型的资源展示的效果不一样。 +Web,在计算机领域指网络。像我们接触的 `WWW`,它是由 3 个单词组成的,即:`World Wide Web `,中文含义是万维网。而我们前面学的 HTML 的参考文档《W3School 全套教程》中的 `W3C` 就是万维网联盟,他们的出现都是为了让我们在网络的世界中获取资源,这些资源的存放之处,我们称之为网站。我们通过输入网站的地址(网址),就可以访问网站中提供的资源。在网上我们能访问到的内容全是资源(不区分局域网还是广域网),只不过不同类型的资源展示的效果不一样 -资源分为静态资源和动态资源。 +资源分为静态资源和动态资源 -* 静态资源指的是,网站中提供给人们展示的资源是一成不变的,也就是说不同人或者在不同时间,看到的内容都是一样的。例如:我们看到的新闻,网站的使用手册,网站功能说明文档等等。而作为开发者,我们编写的`html`,`css`,`js`,图片,多媒体等等都可以称为静态资源。 +* 静态资源指的是,网站中提供给人们展示的资源是一成不变的,也就是说不同人或者在不同时间,看到的内容都是一样的。例如:我们看到的新闻,网站的使用手册,网站功能说明文档等等。而作为开发者,我们编写的 `html`、`css`、`js` 图片,多媒体等等都可以称为静态资源 -* 动态资源它指的是,网站中提供给人们展示的资源是由程序产生的,在不同的时间或者用不同的人员由于身份的不同,所看到的内容是不一样的。例如:我们在CSDN上下载资料,只有登录成功后,且积分足够时才能下载。否则就不能下载,这就是访客身份和会员身份的区别。作为开发人员,我们编写的`JSP`,`servlet`,`php`,`ASP`等都是动态资源。 +* 动态资源它指的是,网站中提供给人们展示的资源是由程序产生的,在不同的时间或者用不同的人员由于身份的不同,所看到的内容是不一样的。例如:我们在CSDN上下载资料,只有登录成功后,且积分足够时才能下载。否则就不能下载,这就是访客身份和会员身份的区别。作为开发人员,我们编写的 `JSP`,`servlet`,`php`,`ASP` 等都是动态资源。 关于广域网和局域网的划分 @@ -2438,11 +2477,11 @@ Web,在计算机领域指网络。像我们接触的`WWW`,它是由3个单 部署方式划分:一体化架构,垂直拆分架构,分布式架构,流动计算架构,微服务架构。 * C/S结构:客户端—服务器的方式。其中C代表Client,S代表服务器。C/S结构的系统设计图如下: - + * B/S结构是浏览器—服务器的方式。B代表Browser,S代表服务器。B/S结构的系统设计图如下: - + @@ -2479,10 +2518,10 @@ Web,在计算机领域指网络。像我们接触的`WWW`,它是由3个单 | 服务器名称 | 说明 | | ----------- | ----------------------------------------------------- | -| weblogic | 实现了javaEE规范,重量级服务器,又称为javaEE容器 | -| websphereAS | 实现了javaEE规范,重量级服务器。 | -| JBOSSAS | 实现了JavaEE规范,重量级服务器。免费的。 | -| Tomcat | 实现了jsp/servlet规范,是一个轻量级服务器,开源免费。 | +| weblogic | 实现了 JavaEE 规范,重量级服务器,又称为 JavaEE 容器 | +| websphereAS | 实现了 JavaEE 规范,重量级服务器。 | +| JBOSSAS | 实现了 JavaEE 规范,重量级服务器,免费 | +| Tomcat | 实现了 jsp/servlet 规范,是一个轻量级服务器,开源免费 | @@ -2498,7 +2537,7 @@ Web,在计算机领域指网络。像我们接触的`WWW`,它是由3个单 目录结构详解: -![](https://gitee.com/seazean/images/raw/master/Web/Tomcat目录结构详解.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/Tomcat目录结构详解.png) @@ -2556,7 +2595,7 @@ Tomcat服务器的停止文件也在二进制文件目录bin中:shutdown.bat * 进程很重要:修改自己的端口号。修改的是 Tomcat 目录下`\conf\server.xml`中的配置。 - ![](https://gitee.com/seazean/images/raw/master/Web/Tomcat-server.xml端口配置.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/Tomcat-server.xml端口配置.png) @@ -2568,7 +2607,7 @@ Tomcat服务器的停止文件也在二进制文件目录bin中:shutdown.bat Run -> Edit Configurations -> Templates -> Tomcat Server -> Local -![](https://gitee.com/seazean/images/raw/master/Web/Tomcat-IDEA配置Tomcat.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/Tomcat-IDEA配置Tomcat.png) @@ -2615,10 +2654,10 @@ Run -> Edit Configurations -> Templates -> Tomcat Server -> Local #### IDEA部署 * 新建工程 - + * 发布工程 - ![](https://gitee.com/seazean/images/raw/master/Web/Tomcat-IDEA发布工程.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/Tomcat-IDEA发布工程.png) * Run @@ -2646,6 +2685,160 @@ Run -> Edit Configurations -> Templates -> Tomcat Server -> Local +**** + + + +### 执行原理 + +#### 整体架构 + +Tomcat 核心组件架构图如下所示: + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/Tomcat-核心组件架构图.png) + +组件介绍: + +- GlobalNamingResources:实现 JNDI,指定一些资源的配置信息 +- Server:Tomcat 是一个 Servlet 容器,一个 Tomcat 对应一个 Server,一个 Server 可以包含多个 Service +- Service:核心服务是 Catalina,用来对请求进行处理,一个 Service 包含多个 Connector 和一个 Container +- Connector:连接器,负责处理客户端请求,解析不同协议及 I/O 方式 +- Executor:线程池 +- Container:容易包含 Engine,Host,Context,Wrapper 等组件 +- Engine:服务交给引擎处理请求,Container 容器中顶层的容器对象,一个 Engine 可以包含多个 Host 主机 +- Host:Engine 容器的子容器,一个 Host 对应一个网络域名,一个 Host 包含多个 Context +- Context:Host 容器的子容器,表示一个 Web 应用 +- Wrapper:Tomcat 中的最小容器单元,表示 Web 应用中的 Servlet + +核心类库: + +* Coyote:Tomcat 连接器的名称,封装了底层的网络通信,为 Catalina 容器提供了统一的接口,使容器与具体的协议以及 I/O 解耦 +* EndPoint:Coyote 通信端点,即通信监听的接口,是 Socket 接收和发送处理器,是对传输层的抽象,用来实现 TCP/IP 协议 +* Processor : Coyote 协议处理接口,用来实现 HTTP 协议,Processor 接收来自 EndPoint 的 Socket,读取字节流解析成 Tomcat 的 Request 和 Response 对象,并通过 Adapter 将其提交到容器处理,Processor 是对应用层协议的抽象 +* CoyoteAdapter:适配器,连接器调用 CoyoteAdapter 的 sevice 方法,传入的是 TomcatRequest 对象,CoyoteAdapter 负责将TomcatRequest 转成 ServletRequest,再调用容器的 service 方法 + + + +参考文章:https://www.jianshu.com/p/7c9401b85704 + +参考文章:https://www.yuque.com/yinhuidong/yu877c/ktq82e + + + +*** + + + +#### 启动过程 + +Tomcat 的启动入口是 Bootstrap#main 函数,首先通过调用 `bootstrap.init()` 初始化相关组件: + +* `initClassLoaders()`:初始化三个类加载器,commonLoader 的父类加载器是启动类加载器 +* `Thread.currentThread().setContextClassLoader(catalinaLoader)`:自定义类加载器加载 Catalina 类,**打破双亲委派** +* `Object startupInstance = startupClass.getConstructor().newInstance()`:反射创建 Catalina 对象 +* `method.invoke(startupInstance, paramValues)`:反射调用方法,设置父类加载器是 sharedLoader +* `catalinaDaemon = startupInstance`:引用 Catalina 对象 + +`daemon.load(args)` 方法反射调用 Catalina 对象的 load 方法,对**服务器的组件进行初始化**,并绑定了 ServerSocket 的端口: + +* `parseServerXml(true)`:解析 XML 配置文件 + +* `getServer().init()`:服务器执行初始化,采用责任链的执行方式 + + * `LifecycleBase.init()`:生命周期接口的初始化方法,开始链式调用 + + * `StandardServer.initInternal()`:Server 的初始化,遍历所有的 Service 进行初始化 + + * `StandardService.initInternal()`:Service 的初始化,对 Engine、Executor、listener、Connector 进行初始化 + + * `StandardEngine.initInternal()`:Engine 的初始化 + + * `getRealm()`:创建一个 Realm 对象 + * `ContainerBase.initInternal()`:容器的初始化,设置处理容器内组件的启动和停止事件的线程池 + + * `Connector.initInternal()`:Connector 的初始化 + + ```java + public Connector() { + this("HTTP/1.1"); //默认无参构造方法,会创建出 Http11NioProtocol 的协议处理器 + } + ``` + + * `adapter = new CoyoteAdapter(this)`:实例化 CoyoteAdapter 对象 + + * `protocolHandler.setAdapter(adapter)`:设置到 ProtocolHandler 协议处理器中 + + * `ProtocolHandler.init()`:协议处理器的初始化,底层调用 `AbstractProtocol#init` 方法 + + `endpoint.init()`:端口的初始化,底层调用 `AbstractEndpoint#init` 方法 + + `NioEndpoint.bind()`:绑定方法 + + * `initServerSocket()`:**初始化 ServerSocket**,以 NIO 的方式监听端口 + * `serverSock = ServerSocketChannel.open()`:**NIO 的方式打开通道** + * `serverSock.bind(addr, getAcceptCount())`:通道绑定连接端口 + * `serverSock.configureBlocking(true)`:切换为阻塞模式(没懂,为什么阻塞) + * `initialiseSsl()`:初始化 SSL 连接 + * `selectorPool.open(getName())`:打开选择器,类似 NIO 的多路复用器 + +初始化完所有的组件,调用 `daemon.start()` 进行**组件的启动**,底层反射调用 Catalina 对象的 start 方法: + +* `getServer().start()`:启动组件,也是责任链的模式 + + * `LifecycleBase.start()`:生命周期接口的初始化方法,开始链式调用 + + * `StandardServer.startInternal()`:Server 服务的启动 + + * `globalNamingResources.start()`:启动 JNDI 服务 + * `for (Service service : services)`:遍历所有的 Service 进行启动 + + * `StandardService.startInternal()`:Service 的启动,对所有 Executor、listener、Connector 进行启 + + * `StandardEngine.startInternal()`:启动引擎,部署项目 + + * `ContainerBase.startInternal()`:容器的启动 + * 启动集群、Realm 组件,并且创建子容器,提交给线程池 + * `((Lifecycle) pipeline).start()`:遍历所有的管道进行启动 + * `Valve current = first`:获取第一个阀门 + * `((Lifecycle) current).start()`:启动阀门,底层 `ValveBase#startInternal` 中设置启动的状态 + * `current = current.getNext()`:获取下一个阀门 + + * `Connector.startInternal()`:Connector 的初始化 + + * `protocolHandler.start()`:协议处理器的启动 + + `endpoint.start()`:端点启动 + + `NioEndpoint.startInternal()`:启动 NIO 的端点 + + * `createExecutor()`:创建 Worker 线程组,10 个线程,用来进行任务处理 + * `initializeConnectionLatch()`:用来进行连接限流,**最大 8*1024 条连接** + * `poller = new Poller()`:**创建 Poller 对象**,开启了一个多路复用器 Selector + * `Thread pollerThread = new Thread(poller, getName() + "-ClientPoller")`:创建并启动 Poller 线程,Poller 实现了 Runnable 接口,是一个任务对象,**线程 start 后进入 Poller#run 方法** + * `pollerThread.setDaemon(true)`:设置为守护线程 + * `startAcceptorThread()`:启动接收者线程 + * `acceptor = new Acceptor<>(this)`:**创建 Acceptor 对象** + * `Thread t = new Thread(acceptor, threadName)`:创建并启动 Acceptor 接受者线程 + + + +*** + + + +#### 处理过程 + +1) Acceptor 监听客户端套接字,每 50ms 调用一次 **`serverSocket.accept`**,获取 Socket 后把封装成 NioSocketWrapper(是 SocketWrapperBase 的子类),并设置为非阻塞模式,把 NioSocketWrapper 封装成 PollerEvent 放入同步队列中 +2) Poller 循环判断同步队列中是否有就绪的事件,如果有则通过 `selector.selectedKeys()` 获取就绪事件,获取 SocketChannel 中携带的 attachment(NioSocketWrapper),在 processKey 方法中根据事件类型进行 processSocket,将 Wrapper 对象封装成 SocketProcessor 对象,该对象是一个任务对象,提交到 Worker 线程池进行执行 +3) `SocketProcessorBase.run()` 加锁调用 `SocketProcessor#doRun`,保证线程安全,从协议处理器 ProtocolHandler 中获取 AbstractProtocol,然后**创建 Http11Processor 对象处理请求** +4) `Http11Processor#service` 中调用 `CoyoteAdapter#service` ,把生成的 Tomcat 下的 Request 和 Response 对象通过方法 postParseRequest 匹配到对应的 Servlet 的请求响应,将请求传递到对应的 Engine 容器中调用 Pipeline,管道中包含若干个 Valve,执行完所有的 Valve 最后执行 StandardEngineValve,继续调用 Host 容器的 Pipeline,执行 Host 的 Valve,再传递给 Context 的 Pipeline,最后传递到 Wrapper 容器 +5) `StandardWrapperValve#invoke` 中创建了 Servlet 对象并执行初始化,并为当前请求准备一个 FilterChain 过滤器链执行 doFilter 方法,`ApplicationFilterChain#doFilter` 是一个**责任链的驱动方法**,通过调用 internalDoFilter 来获取过滤器链的下一个过滤器执行 doFilter,执行完所有的过滤器后执行 `servlet.service` 的方法 +6) 最后调用 HttpServlet#service(),根据请求的方法来调用 doGet、doPost 等,执行到自定义的业务方法 + + + + + *** @@ -2659,10 +2852,8 @@ Socket 是使用 TCP/IP 或者 UDP 协议在服务器与客户端之间进行传 - **Servlet 是使用 HTTP 协议在服务器与客户端之间通信的技术,是 Socket 的一种应用** - **HTTP 协议:是在 TCP/IP 协议之上进一步封装的一层协议,关注数据传输的格式是否规范,底层的数据传输还是运用了 Socket 和 TCP/IP** -Tomcat 和 Servlet 的关系: - -Servlet 的运行环境叫做 Web 容器或 Servlet 服务器,**Tomcat 是 Web 应用服务器,是一个 Servlet/JSP 容器**。Tomcat 作为 Servlet 容器,负责处理客户请求,把请求传送给 Servlet,并将 Servlet 的响应传送回给客户。而 Servlet 是一种运行在支持Java语言的服务器上的组件,Servlet 最常见的用途是扩展 Java Web 服务器功能,提供非常安全的、可移植的、易于使用的 CGI 替代品 -![](https://gitee.com/seazean/images/raw/master/Web/Tomcat与Servlet的关系.png) +Tomcat 和 Servlet 的关系:Servlet 的运行环境叫做 Web 容器或 Servlet 服务器,**Tomcat 是 Web 应用服务器,是一个 Servlet/JSP 容器**。Tomcat 作为 Servlet 容器,负责处理客户请求,把请求传送给 Servlet,并将 Servlet 的响应传送回给客户。而 Servlet 是一种运行在支持 Java 语言的服务器上的组件,Servlet 用来扩展 Java Web 服务器功能,提供非常安全的、可移植的、易于使用的 CGI 替代品 +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/Tomcat与Servlet的关系.png) @@ -2684,7 +2875,7 @@ Servlet是SUN公司提供的一套规范,名称就叫Servlet规范,它也是 4. 支持配置相关功能 - ![](https://gitee.com/seazean/images/raw/master/Web/Servlet类关系总视图.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/Servlet类关系总视图.png) @@ -2696,7 +2887,7 @@ Servlet是SUN公司提供的一套规范,名称就叫Servlet规范,它也是 创建 Web 工程 → 编写普通类继承 Servlet 相关类 → 重写方法 -![](https://gitee.com/seazean/images/raw/master/Web/Servlet入门案例执行.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/Servlet入门案例执行.png) @@ -2704,7 +2895,7 @@ Servlet执行过程分析: 通过浏览器发送请求,请求首先到达Tomcat服务器,由服务器解析请求URL,然后在部署的应用列表中找到应用。然后找到web.xml配置文件,在web.xml中找到FirstServlet的配置(/),找到后执行service方法,最后由FirstServlet响应客户浏览器。整个过程如下图所示: -![](https://gitee.com/seazean/images/raw/master/Web/Servlet执行过程图.jpg) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/Servlet执行过程图.jpg) @@ -2738,7 +2929,7 @@ Servlet执行过程分析: Servlet 3.0 中的异步处理指的是允许Servlet重新发起一条新线程去调用 耗时业务方法,这样就可以避免等待 -![](https://gitee.com/seazean/images/raw/master/Web/Servlet3.0的异步处理.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/Servlet3.0的异步处理.png) @@ -3079,7 +3270,7 @@ ServletConfig 是 Servlet 的配置参数对象。在 Servlet 规范中,允许 * 效果: - ![](https://gitee.com/seazean/images/raw/master/Web/ServletConfig演示.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/ServletConfig演示.png) @@ -3107,7 +3298,7 @@ Servlet 规范中,共有4个域对象,ServletContext 是其中一个,web 数据共享: - + 获取ServletContext: @@ -3284,7 +3475,7 @@ Servlet3.0 版本!不需要配置 web.xml Web服务器收到客户端的http请求,会针对每一次请求,分别创建一个用于代表请求的request对象、和代表响应的response对象。 -![](https://gitee.com/seazean/images/raw/master/Web/Servlet请求响应图.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/Servlet请求响应图.png) @@ -3304,7 +3495,7 @@ Request 作用: * 请求转发 * 作为域对象存数据 -![](https://gitee.com/seazean/images/raw/master/Web/Request请求对象的类视图.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/Request请求对象的类视图.png) @@ -3598,9 +3789,9 @@ HttpServletRequest 类方法: RequestDispatcher 类方法: -* `void forward(ServletRequest request, ServletResponse response)` : 实现转发,将请求从 servlet 转发到服务器上的另一个资源(servlet,JSP文件或HTML文件) +* `void forward(ServletRequest request, ServletResponse response)` : 实现转发,将请求从 Servlet 转发到服务器上的另一个资源(Servlet,JSP 文件或 HTML 文件) -过程:浏览器访问http://localhost:8080/request/servletDemo09,/servletDemo10也会执行 +过程:浏览器访问 http://localhost:8080/request/servletDemo09,/servletDemo10也会执行 ```java @WebServlet("/servletDemo09") @@ -3649,21 +3840,23 @@ public class ServletDemo10 extends HttpServlet { #### 请求包含 -请求包含:合并其他的Servlet中的功能一起响应给客户端。特点: +请求包含:合并其他的 Servlet 中的功能一起响应给客户端。特点: * 浏览器地址栏不变 * 域对象中的数据不丢失 -* 被包含的Servlet响应头会丢失 +* 被包含的 Servlet 响应头会丢失 -请求转发的注意事项:负责转发的Servlet,转发前后的响应正文丢失,由转发目的地来响应浏览器。 +请求转发的注意事项:负责转发的 Servlet,转发前后的响应正文丢失,由转发目的地来响应浏览器 -请求包含的注意事项:被包含者的响应消息头丢失,因为它被包含者包含起来了。 +请求包含的注意事项:被包含者的响应消息头丢失,因为它被包含者包含起来了 -HttpServletRequest类方法: - `RequestDispatcher getRequestDispatcher(String path) ` : 获取任务调度对象 +HttpServletRequest 类方法: -RequestDispatcher类方法: - `void include(ServletRequest request, ServletResponse response) ` : 实现包含。包括响应中资源的内容(servlet,JSP页面,HTML文件)。 +* `RequestDispatcher getRequestDispatcher(String path) ` : 获取任务调度对象 + +RequestDispatcher 类方法: + +* `void include(ServletRequest request, ServletResponse response) ` : 实现包含。包括响应中资源的内容(servlet,JSP页面,HTML文件)。 ```java @WebServlet("/servletDemo11") @@ -3706,9 +3899,8 @@ public class ServletDemo12 extends HttpServlet { 请求体 -* POST - `void setCharacterEncoding(String env)` : 设置请求体的编码 - +* POST:`void setCharacterEncoding(String env)`:设置请求体的编码 + ```java @WebServlet("/servletDemo08") public class ServletDemo08 extends HttpServlet { @@ -3728,9 +3920,8 @@ public class ServletDemo12 extends HttpServlet { } ``` - -* GET - Tomcat8.5版本及以后,Tomcat服务器已经帮我们解决 + +* GET:Tomcat8.5 版本及以后,Tomcat 服务器已经帮我们解决 @@ -3746,17 +3937,18 @@ public class ServletDemo12 extends HttpServlet { 响应,服务器把请求的处理结果告知客户端 -响应对象:在JavaEE工程中,用于发送响应的对象 - 协议无关的对象标准是:ServletResponse接口 - 协议相关的对象标准是:HttpServletResponse接口 +响应对象:在 JavaEE 工程中,用于发送响应的对象 + +* 协议无关的对象标准是:ServletResponse 接口 +* 协议相关的对象标准是:HttpServletResponse 接口 -Response的作用: +Response 的作用: + 操作响应的三部分(行, 头, 体) * 请求重定向 -![](https://gitee.com/seazean/images/raw/master/Web/Response响应类视图.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/Response响应类视图.png) *** @@ -3954,7 +4146,7 @@ public class ServletDemo04 extends HttpServlet { ``` -![](https://gitee.com/seazean/images/raw/master/Web/Response设置缓存时间.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/Response设置缓存时间.png) @@ -4053,14 +4245,14 @@ public class ServletDemo06 extends HttpServlet { ##### 实现重定向 -请求重定向:客户端的一次请求到达后,需要借助其他Servlet来实现功能。特点: +请求重定向:客户端的一次请求到达后,需要借助其他 Servlet 来实现功能。特点: 1. 重定向两次请求 2. 重定向的地址栏路径改变 -3. **重定向的路径写绝对路径**(带域名/ip地址,如果是同一个项目,可以省略域名/ip地址) +3. **重定向的路径写绝对路径**(带域名 /ip 地址,如果是同一个项目,可以省略域名 /ip 地址) 4. 重定向的路径可以是项目内部的,也可以是项目以外的(百度) -5. 重定向不能重定向到WEB-INF下的资源 -6. 把数据存到request域里面, 重定向不可用 +5. 重定向不能重定向到 WEB-INF 下的资源 +6. 把数据存到 request 域里面,重定向不可用 实现方式: @@ -4129,7 +4321,7 @@ public class ServletDemo08 extends HttpServlet { 3. 请求转发可以和请求域对象共享数据,数据不会丢失 4. 请求转发浏览器地址栏不变 -![](https://gitee.com/seazean/images/raw/master/Web/重定向和请求转发对比图.jpg) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/重定向和请求转发对比图.jpg) @@ -4198,7 +4390,7 @@ public class ServletDemo08 extends HttpServlet { **常用的会话管理技术**: * Cookie:客户端会话管理技术,用户浏览的信息以键值对(key=value)的形式保存在浏览器上。如果没有关闭浏览器,再次访问服务器,会把 cookie 带到服务端,服务端就可以做相应的处理 -* Session:服务端会话管理技术。当客户端第一次请求 session 对象时候,服务器为每一个浏览器开辟一块内存空间,并将通过特殊算法算出一个 session 的 ID,用来标识该 session 对象。由于内存空间是每一个浏览器独享的,所有用户在访问的时候,可以把信息保存在 session 对象中。同时服务器把 sessionId 写到 cookie 中,再次访问的时候,浏览器会把 cookie(sessionId) 带过来,找到对应的 session 对象。 +* Session:服务端会话管理技术。当客户端第一次请求 session 对象时,服务器为每一个浏览器开辟一块内存空间,并将通过特殊算法算出一个 session 的 ID,用来标识该 session 对象。由于内存空间是每一个浏览器独享的,所有用户在访问的时候,可以把信息保存在 session 对象中,同时服务器会把 sessionId 写到 cookie 中,再次访问的时候,浏览器会把 cookie(sessionId) 带过来,找到对应的 session 对象即可 tomcat 生成的 sessionID 叫做 jsessionID @@ -4206,9 +4398,7 @@ public class ServletDemo08 extends HttpServlet { * Cookie 存储在客户端中,而 Session 存储在服务器上,相对来说 Session 安全性更高。如果要在 Cookie 中存储一些敏感信息,不要直接写入 Cookie,应该将 Cookie 信息加密然后使用到的时候再去服务器端解密 -* Cookie 一般用来保存用户信息 - - 在 Cookie 中保存已经登录过得用户信息,下次访问网站的时候就不需要重新登录,因为用户登录的时候可以存放一个 Token 在 Cookie 中,下次登录的时候只需要根据 Token 值来查找用户即可(为了安全考虑,重新登录一般要将 Token 重写),所以登录一次网站后访问网站其他页面不需要重新登录 +* Cookie 一般用来保存用户信息,在 Cookie 中保存已经登录过得用户信息,下次访问网站的时候就不需要重新登录,因为用户登录的时候可以存放一个 Token 在 Cookie 中,下次登录的时候只需要根据 Token 值来查找用户即可(为了安全考虑,重新登录一般要将 Token 重写),所以登录一次网站后访问网站其他页面不需要重新登录 * Session 通过服务端记录用户的状态,服务端给特定的用户创建特定的 Session 之后就可以标识这个用户并且跟踪这个用户 @@ -4230,7 +4420,7 @@ Cookie:客户端会话管理技术,把要共享的数据保存到了客户 作用:保存客户浏览器访问网站的相关内容(需要客户端不禁用 Cookie),从而在每次访问同一个内容时,先从本地缓存获取,使资源共享,提高效率。 -![](https://gitee.com/seazean/images/raw/master/Web/Cookie类讲解.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/Cookie类讲解.png) @@ -4284,8 +4474,8 @@ Cookie:客户端会话管理技术,把要共享的数据保存到了客户 设置 Cookie 存活时间 API:`void setMaxAge(int expiry)` -* -1:默认。代表 Cookie 数据存到浏览器关闭(保存在浏览器文件中) -* 0:代表删除 Cookie,如果要删除 Cookie 要确保**路径一致**。 +* -1:默认,代表 Cookie 数据存到浏览器关闭(保存在浏览器文件中) +* 0:代表删除 Cookie,如果要删除 Cookie 要确保**路径一致** * 正整数:以秒为单位保存数据有有效时间(把缓存数据保存到磁盘中) ```java @@ -4380,7 +4570,7 @@ XSS 全称 Cross SiteScript,跨站脚本攻击,是Web程序中常见的漏 ### 基本介绍 -Session:服务器端会话管理技术,本质也是采用客户端会话管理技术,不过在客户端保存的是一个特殊标识,共享的数据保存到了服务器的内存对象中。每次请求时,会将特殊标识带到服务器端,根据标识来找到对应的内存空间,从而实现数据共享。简单说它就是一个服务端会话对象,用于存储用户的会话数据。 +Session:服务器端会话管理技术,本质也是采用客户端会话管理技术,不过在客户端保存的是一个特殊标识,共享的数据保存到了服务器的内存对象中。每次请求时,会将特殊标识带到服务器端,根据标识来找到对应的内存空间,从而实现数据共享。简单说它就是一个服务端会话对象,用于存储用户的会话数据 Session 域(会话域)对象是 Servlet 规范中四大域对象之一,并且它也是用于实现数据共享的 @@ -4407,7 +4597,7 @@ HttpServletRequest类获取Session: | HttpSession getSession() | 获取HttpSession对象 | | HttpSession getSession(boolean creat) | 获取HttpSession对象,未获取到是否自动创建 | - + @@ -4434,7 +4624,7 @@ HttpServletRequest类获取Session: #### 实现会话 -通过第一个Servlet设置共享的数据用户名,并在第二个Servlet获取到。 +通过第一个Servlet设置共享的数据用户名,并在第二个Servlet获取到 项目执行完以后,去浏览器抓包,Request Headers 中的 Cookie JSESSIONID的值是一样的 @@ -4488,7 +4678,7 @@ public class ServletDemo02 extends HttpServlet{ #### 生命周期 -Session 的创建:一个常见的错误是以为 Session 在有客户端访问时就被创建,事实是直到某 server 端程序(如Servlet)调用 `HttpServletRequest.getSession(true)` 这样的语句时才会被创建 +Session 的创建:一个常见的错误是以为 Session 在有客户端访问时就被创建,事实是直到某 server 端程序(如 Servlet)调用 `HttpServletRequest.getSession(true)` 这样的语句时才会被创建 Session 在以下情况会被删除: @@ -4630,7 +4820,7 @@ JSP部署在服务器上,可以处理客户端发送的请求,并根据请 客户端提交请求——Tomcat服务器解析请求地址——找到JSP页面——Tomcat将JSP页面翻译成Servlet的java文件——将翻译好的.java文件编译成.class文件——返回到客户浏览器上 - ![](https://gitee.com/seazean/images/raw/master/Web/JSP执行过程.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/JSP执行过程.png) * 溯源,打开JSP翻译后的Java文件 @@ -4638,7 +4828,7 @@ JSP部署在服务器上,可以处理客户端发送的请求,并根据请 在文件中找到了输出页面的代码,本质都是用out.write()输出的JSP语句 - ![](https://gitee.com/seazean/images/raw/master/Web/Jsp的本质说明.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/Jsp的本质说明.png) @@ -4834,7 +5024,7 @@ jsp中的隐式对象也并不是未声明,它是在翻译成.java文件时声 * PageContext方法如下,页面域操作的方法定义在了PageContext的父类JspContext中 - ![](https://gitee.com/seazean/images/raw/master/Web/PageContext方法详解.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/PageContext方法详解.png) @@ -4865,7 +5055,7 @@ M : model, 通常用于封装数据,封装的是数据模型 V : view,通常用于展示数据。动态展示用jsp页面,静态数据展示用html C : controller,通常用于处理请求和响应,一般指的是Servlet -![](https://gitee.com/seazean/images/raw/master/Web/MVC模型.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/MVC模型.png) @@ -5034,7 +5224,7 @@ str: EL表达式中运算符: -* 关系运算符:![](https://gitee.com/seazean/images/raw/master/Web/EL表达式关系运算符.png) +* 关系运算符:![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/EL表达式关系运算符.png) * 逻辑运算符: @@ -5079,7 +5269,7 @@ EL表达式中运算符: ``` -![](https://gitee.com/seazean/images/raw/master/Web/EL表达式运算符效果图.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/EL表达式运算符效果图.png) @@ -5337,11 +5527,11 @@ JSTL:Java Server Pages Standarded Tag Library,JSP中标准标签库。 ### 过滤器 -Filter:过滤器,是JavaWeb三大组件之一,另外两个是Servlet和Listener。 +Filter:过滤器,是 JavaWeb 三大组件之一,另外两个是 Servlet 和 Listener -工作流程:在程序访问服务器资源时,当一个请求到来,服务器首先判断是否有过滤器与去请求资源相关联,如果有,过滤器可以将请求拦截下来,完成一些特定的功能,再由过滤器决定是否交给请求资源。如果没有就直接请求资源,响应同理。 +工作流程:在程序访问服务器资源时,当一个请求到来,服务器首先判断是否有过滤器与去请求资源相关联,如果有过滤器可以将请求拦截下来,完成一些特定的功能,再由过滤器决定是否交给请求资源,如果没有就直接请求资源,响应同理 -作用:过滤器一般用于完成通用的操作,例如:登录验证、统一编码处理、敏感字符过滤等。 +作用:过滤器一般用于完成通用的操作,例如:登录验证、统一编码处理、敏感字符过滤等 @@ -5353,7 +5543,7 @@ Filter:过滤器,是JavaWeb三大组件之一,另外两个是Servlet和Lis #### Filter -**Filter是一个接口,如果想实现过滤器的功能,必须实现该接口** +Filter是一个接口,如果想实现过滤器的功能,必须实现该接口 * 核心方法 @@ -5365,27 +5555,31 @@ Filter:过滤器,是JavaWeb三大组件之一,另外两个是Servlet和Lis * 配置方式 - * 注解方式 + 注解方式 - ```java - @WebFilter("/*") - ()内填拦截路径,/*代表全部路径 - ``` + ```java + @WebFilter("/*") + ()内填拦截路径,/*代表全部路径 + ``` - * 配置文件 + 配置文件 + + ```xml + + filterDemo01 + filter.FilterDemo01 + + + filterDemo01 + /* + + ``` + + + +*** - ```xml - - filterDemo01 - filter.FilterDemo01 - - - filterDemo01 - /* - - ``` - #### FilterChain @@ -5404,16 +5598,14 @@ Filter:过滤器,是JavaWeb三大组件之一,另外两个是Servlet和Lis FilterConfig 是一个接口,代表过滤器的配置对象,可以加载一些初始化参数 -* 核心方法: +| 方法 | 作用 | +| ------------------------------------------- | -------------------------------------------- | +| String getFilterName() | 获取过滤器对象名称 | +| String getInitParameter(String name) | 获取指定名称的初始化参数的值,不存在返回null | +| Enumeration getInitParameterNames() | 获取所有参数的名称 | +| ServletContext getServletContext() | 获取应用上下文对象 | - | 方法 | 作用 | - | ------------------------------------------- | -------------------------------------------- | - | String getFilterName() | 获取过滤器对象名称 | - | String getInitParameter(String name) | 获取指定名称的初始化参数的值,不存在返回null | - | Enumeration getInitParameterNames() | 获取所有参数的名称 | - | ServletContext getServletContext() | 获取应用上下文对象 | - @@ -5429,7 +5621,7 @@ FilterConfig 是一个接口,代表过滤器的配置对象,可以加载一 过滤器放行之后执行完目标资源,仍会回到过滤器中 -* Filter代码: +* Filter 代码: ```java @WebFilter("/*") @@ -5446,7 +5638,7 @@ FilterConfig 是一个接口,代表过滤器的配置对象,可以加载一 } ``` -* Servlet代码: +* Servlet 代码: ```java @WebServlet("/servletDemo01") @@ -5584,7 +5776,7 @@ FilterConfig 是一个接口,代表过滤器的配置对象,可以加载一 ``` -* Servlet代码:`System.out.println("servletDemo03执行了...");` +* Servlet 代码:`System.out.println("servletDemo03执行了...");` * 控制台输出: @@ -6530,7 +6722,7 @@ DOM(Document Object Model):文档对象模型。 将 HTML 文档的各个组成部分,封装为对象。借助这些对象,可以对 HTML 文档进行增删改查的动态操作。 -![](https://gitee.com/seazean/images/raw/master/Web/DOM介绍.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/DOM介绍.png) @@ -6698,11 +6890,11 @@ Attribute属性的操作: * 常用的事件: - ![](https://gitee.com/seazean/images/raw/master/Web/JS常用的事件.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/JS常用的事件.png) * 更多的事件: - ![](https://gitee.com/seazean/images/raw/master/Web/JS更多的事件.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/JS更多的事件.png) @@ -6763,7 +6955,7 @@ Attribute属性的操作: 在姓名、年龄、性别三个文本框中填写信息后,添加到“学生信息表”列表(表格),点击删除后,删除该行数据,并且不需刷新 -![](https://gitee.com/seazean/images/raw/master/Web/事件案例效果.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/事件案例效果.png) @@ -7182,7 +7374,7 @@ JSON(JavaScript Object Notation):是一种轻量级的数据交换格式。 * 创建格式: **name是字符串类型** - ![](https://gitee.com/seazean/images/raw/master/Web/JSON创建格式.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/JSON创建格式.png) * json常用方法 @@ -7284,8 +7476,8 @@ Jackson:开源免费的 JSON 转换工具,SpringMVC 转换默认使用 Jacks public void test03() throws Exception{ //map转json HashMap map = new HashMap<>(); - map.put("黑马一班",new User("张三",23)); - map.put("黑马二班",new User("李四",24)); + map.put("sea一班",new User("张三",23)); + map.put("sea二班",new User("李四",24)); String json = mapper.writeValueAsString(map); System.out.println("json字符串:" + json); @@ -7294,8 +7486,8 @@ Jackson:开源免费的 JSON 转换工具,SpringMVC 转换默认使用 Jacks new TypeReference>(){}); System.out.println("java对象:" + map2); } - //json字符串 = {"黑马一班":{"name":"张三","age":23},"黑马二班":{....} - //map对象 = {黑马一班=User{name='张三', age=23}, 黑马二班=User{name='李四', age=24}} + //json字符串 = {"sea一班":{"name":"张三","age":23},"sea二班":{....} + //map对象 = {sea一班=User{name='张三', age=23}, sea二班=User{name='李四', age=24}} ``` * List @@ -7350,9 +7542,9 @@ RegExp: #### 验证用户 -使用onsubmit表单提交事件 +使用 onsubmit 表单提交事件 -![](https://gitee.com/seazean/images/raw/master/Web/表单校验.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/表单校验.png) ```html @@ -7404,7 +7596,7 @@ BOM(Browser Object Model):浏览器对象模型。 将浏览器的各个组成部分封装成不同的对象,方便我们进行操作。 -![](https://gitee.com/seazean/images/raw/master/Web/BOM介绍.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/BOM介绍.png) @@ -8105,7 +8297,7 @@ $("#btn5").click(function(){ - 一般的网页如果需要更新内容,必需重新加载个页面。而 AJAX 通过浏览器与服务器进行少量数据交换,就可以使网页实现异步更新。也就是在不重新加载整个页 面的情况下,对网页的部分内容进行**局部更新**。 - ![](https://gitee.com/seazean/images/raw/master/Web/AJAX网页局部更新.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/AJAX网页局部更新.png) @@ -8285,7 +8477,7 @@ $("#btn5").click(function(){ ## 分页知识 -![分页知识](https://gitee.com/seazean/images/raw/master/Web/分页知识.png) +![分页知识](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/分页知识.png) @@ -8358,7 +8550,7 @@ Vue只关注视图层,并且非常容易学习,还可以很方便的与其 el:"#div", data:{ name:"张三", - classRoom:"黑马程序员" + classRoom:"sea程序员" }, methods:{ study(){ @@ -8387,7 +8579,7 @@ Vue只关注视图层,并且非常容易学习,还可以很方便的与其 使用方法:通常编写在标签的属性上,值可以使用 JS 的表达式 -![](https://gitee.com/seazean/images/raw/master/Web/Vue指令介绍.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/Vue指令介绍.png) @@ -8548,7 +8740,7 @@ v-on:为 HTML 标签绑定事件,有简写方式
{{name}}
- +
@@ -8556,7 +8748,7 @@ v-on:为 HTML 标签绑定事件,有简写方式 new Vue({ el:"#div", data:{ - name:"黑马程序员" + name:"sea程序员" }, methods:{ change(){ @@ -8586,7 +8778,7 @@ v-on:为 HTML 标签绑定事件,有简写方式 将Model和View关联起来的就是ViewModel,它是桥梁。 ViewModel负责把Model的数据同步到View显示出来,还负责把View修改的数据同步回Model。 - ![](https://gitee.com/seazean/images/raw/master/Web/MVVM模型.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/MVVM模型.png) ```html @@ -8758,11 +8950,11 @@ Element:网站快速成型工具,是饿了么公司前端开发团队提供 * 生命周期 - ![](https://gitee.com/seazean/images/raw/master/Web/Vue生命周期.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/Vue生命周期.png) * 生命周期八个阶段 - ![](https://gitee.com/seazean/images/raw/master/Web/Vue生命周期的八个阶段.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/Vue生命周期的八个阶段.png) @@ -8872,9 +9064,9 @@ Element:网站快速成型工具,是饿了么公司前端开发团队提供 ## 安装软件 -Nginx(engine x) 是一个高性能的HTTP和[反向代理](https://baike.baidu.com/item/反向代理/7793488)web服务器,同时也提供了IMAP/POP3/SMTP服务。 +Nginx 是一个高性能的 HTTP 和[反向代理 ](https://baike.baidu.com/item/反向代理/7793488)Web 服务器,同时也提供了 IMAP/POP3/SMTP 服务 -Nginx两个最核心的功能:高性能的静态web服务器,反向代理 +Nginx 两个最核心的功能:高性能的静态 Web 服务器,反向代理 * 安装指令:sudo apt-get install nginx @@ -8882,6 +9074,7 @@ Nginx两个最核心的功能:高性能的静态web服务器,反向代理 * 系统指令:systemctl / service start/restart/stop/status nginx 配置文件安装目录:/etc/nginx + 日志文件:/var/log/nginx @@ -8892,20 +9085,20 @@ Nginx两个最核心的功能:高性能的静态web服务器,反向代理 ## 配置文件 -nginx.conf 文件时nginx的主配置文件 +nginx.conf 文件时 Nginx 的主配置文件 - + -* main部分 - +* main 部分 + -* events部分 - +* events 部分 + -* server部分 - +* server 部分 + - root设置的路径会拼接上location的路径,然后去最终路径寻找对应的文件 + root 设置的路径会拼接上 location 的路径,然后去最终路径寻找对应的文件 @@ -8915,15 +9108,18 @@ nginx.conf 文件时nginx的主配置文件 ## 发布项目 -1. 创建一个toutiao目录 - cd /home - mkdir toutiao - -2. 将项目上传到toutiao目录 +1. 创建一个 toutiao 目录 + + ```sh + cd /home + mkdir toutiao + ``` + +2. 将项目上传到 toutiao 目录 3. 解压项目 unzip web.zip -4. 编辑Nginx配置文件nginx.conf +4. 编辑 Nginx 配置文件 nginx.conf ```shell server { @@ -8936,9 +9132,9 @@ nginx.conf 文件时nginx的主配置文件 } ``` -5. 重启nginx服务:systemctl restart nginx +5. 重启 Nginx 服务:systemctl restart nginx -6. 浏览器打开网址 http://127.0.0.1:80 +6. 浏览器打开网址:http://127.0.0.1:80 @@ -8948,32 +9144,32 @@ nginx.conf 文件时nginx的主配置文件 ## 反向代理 -> 无法访问Google,可以配置一个代理服务器,发送请求到代理服务器,代理服务器经过转发,再将请求转发给Google,返回结果之后,再次转发给用户。这个叫做正向代理,正向代理对于用户来说,是有感知的 +> 无法访问 Google,可以配置一个代理服务器,发送请求到代理服务器,代理服务器经过转发,再将请求转发给 Google,返回结果之后,再次转发给用户,这个叫做正向代理,正向代理对于用户来说,是有感知的 -**正向代理(forward proxy)**:是一个位于客户端和目标服务器之间的服务器(代理服务器),为了从目标服务器取得内容,客户端向代理服务器发送一个请求并指定目标,然后代理服务器向目标服务器转交请求并将获得的内容返回给客户端,**正向代理,其实是"代理服务器"代理了"客户端",去和"目标服务器"进行交互** +**正向代理(forward proxy)**:是一个位于客户端和目标服务器之间的代理服务器,为了从目标服务器取得内容,客户端向代理服务器发送一个请求并指定目标,然后代理服务器向目标服务器转交请求并将获得的内容返回给客户端,**正向代理,其实是"代理服务器"代理了当前"客户端",去和"目标服务器"进行交互** 作用: -* 突破访问限制:通过代理服务器,可以突破自身IP访问限制,访问国外网站,教育网等 +* 突破访问限制:通过代理服务器,可以突破自身 IP 访问限制,访问国外网站,教育网等 * 提高访问速度:代理服务器都设置一个较大的硬盘缓冲区,会将部分请求的响应保存到缓冲区中,当其他用户再访问相同的信息时, 则直接由缓冲区中取出信息,传给用户,以提高访问速度 -* 隐藏客户端真实IP:隐藏自己的IP,免受攻击 +* 隐藏客户端真实 IP:隐藏自己的 IP,免受攻击 - + -**反向代理(reverse proxy)**:是指以代理服务器来接受internet上的连接请求,然后将请求转发给内部网络上的服务器,并将从服务器上得到的结果返回给internet上请求连接的客户端,此时代理服务器对外就表现为一个反向代理服务器,**反向代理,其实是"代理服务器"代理了"目标服务器",去和"客户端"进行交互** +**反向代理(reverse proxy)**:是指以代理服务器来接受 Internet 上的连接请求,然后将请求转发给内部网络上的服务器,并将从服务器上得到的结果返回给 Internet 上请求连接的客户端,此时代理服务器对外就表现为一个反向代理服务器,**反向代理,其实是"代理服务器"代理了"目标服务器",去和当前"客户端"进行交互** 作用: -* 隐藏服务器真实IP:使用反向代理,可以对客户端隐藏服务器的IP地址 +* 隐藏服务器真实 IP:使用反向代理,可以对客户端隐藏服务器的 IP 地址 * 负载均衡:根据所有真实服务器的负载情况,将客户端请求分发到不同的真实服务器上 * 提高访问速度:反向代理服务器可以对于静态内容及短时间内有大量访问请求的动态内容提供缓存服务 -* 提供安全保障:反向代理服务器可以作为应用层防火墙,为网站提供对基于Web的攻击行为(例如DoS/DDoS)的防护,更容易排查恶意软件等 +* 提供安全保障:反向代理服务器可以作为应用层防火墙,为网站提供对基于 Web 的攻击行为(例如 DoS/DDoS)的防护,更容易排查恶意软件等 - + 区别: