diff --git a/DB.md b/DB.md index e1e1baa..7fd57da 100644 --- a/DB.md +++ b/DB.md @@ -241,13 +241,13 @@ SHOW PROCESSLIST:查看当前 MySQL 在进行的线程,可以实时地查看 | State | 显示使用当前连接的 sql 语句的状态,以查询为例,需要经过 copying to tmp table、sorting result、sending data等状态才可以完成 | | Info | 显示执行的 sql 语句,是判断问题语句的一个重要依据 | -**Sending data 状态**表示 MySQL 线程开始访问数据行并把结果返回给客户端,而不仅仅只是返回给客户端,是处于执行器过程中的任意阶段。由于在 Sending data 状态下,MySQL 线程需要做大量磁盘读取操作,所以是整个查询中耗时最长的状态。 +**Sending data 状态**表示 MySQL 线程开始访问数据行并把结果返回给客户端,而不仅仅只是返回给客户端,是处于执行器过程中的任意阶段。由于在 Sending data 状态下,MySQL 线程需要做大量磁盘读取操作,所以是整个查询中耗时最长的状态 -*** +*** @@ -264,7 +264,7 @@ SHOW PROCESSLIST:查看当前 MySQL 在进行的线程,可以实时地查看 1. 客户端发送一条查询给服务器 2. 服务器先会检查查询缓存,如果命中了缓存,则立即返回存储在缓存中的结果(一般是 K-V 键值对),否则进入下一阶段 3. 分析器进行 SQL 分析,再由优化器生成对应的执行计划 -4. MySQL 根据优化器生成的执行计划,调用存储引擎的 API 来执行查询 +4. 执行器根据优化器生成的执行计划,调用存储引擎的 API 来执行查询 5. 将结果返回给客户端 大多数情况下不建议使用查询缓存,因为查询缓存往往弊大于利 @@ -280,7 +280,7 @@ SHOW PROCESSLIST:查看当前 MySQL 在进行的线程,可以实时地查看 ##### 缓存配置 -1. 查看当前的 MySQL 数据库是否支持查询缓存: +1. 查看当前 MySQL 数据库是否支持查询缓存: ```mysql SHOW VARIABLES LIKE 'have_query_cache'; -- YES @@ -399,7 +399,7 @@ 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 语法。如果语句不对,就会收到 `You have an error in your SQL syntax` 的错误提醒 预处理器:进一步检查解析树的合法性,比如数据表和数据列是否存在、别名是否有歧义等 @@ -434,7 +434,7 @@ MySQL 中保存着两种统计数据: * innodb_table_stats 存储了表的统计数据,每一条记录对应着一个表的统计数据 * innodb_index_stats 存储了索引的统计数据,每一条记录对应着一个索引的一个统计项的数据 -MySQL 在真正执行语句之前,并不能精确地知道满足条件的记录有多少条,只能根据统计信息来估算记录,统计信息就是索引的区分度,一个索引上不同的值的个数(比如性别只能是男女,就是 2 ),称之为基数(cardinality),**基数越大说明区分度越好** +MySQL 在真正执行语句之前,并不能精确地知道满足条件的记录有多少条,只能根据统计信息来估算记录,统计信息就是索引的区分度,一个索引上不同的值的个数(比如性别只能是男女,就是 2 ),称之为基数(cardinality),**基数越大说明区分度越好** 通过**采样统计**来获取基数,InnoDB 默认会选择 N 个数据页,统计这些页面上的不同值得到一个平均值,然后乘以这个索引的页面数,就得到了这个索引的基数 @@ -1650,7 +1650,7 @@ SELECT * FROM emp WHERE name REGEXP '[uvw]';-- 匹配包含 uvw 的name值 #### 约束介绍 -约束:对表中的数据进行限定,保证数据的正确性、有效性、完整性! +约束:对表中的数据进行限定,保证数据的正确性、有效性、完整性 约束的分类: @@ -1724,7 +1724,7 @@ SELECT * FROM emp WHERE name REGEXP '[uvw]';-- 匹配包含 uvw 的name值 #### 主键自增 -主键自增约束可以为空,并自动增长。删除某条数据不影响自增的下一个数值,依然按照前一个值自增。 +主键自增约束可以为空,并自动增长。删除某条数据不影响自增的下一个数值,依然按照前一个值自增 * 建表时添加主键自增约束 @@ -2122,19 +2122,11 @@ STRAIGHT_JOIN与 JOIN 类似,只不过左表始终在右表之前读取,只 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); + INSERT INTO employee VALUES (1001,'孙悟空',1005,9000.00),..,(1009,'宋江',NULL,16000.00); ``` - + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/自关联查询数据准备.png) - + * 数据查询 ```mysql @@ -2334,7 +2326,7 @@ BNL 即 Block Nested-Loop Join 算法,由于要访问多次被驱动表,会 子查询物化会产生建立临时表的成本,但是将子查询转化为连接查询可以充分发挥优化器的作用,所以引入:半连接 -* t1 和 t2 表进行半连接,对于 t1 表中的某条记录,只需要关心在 s2 表中是否存在,而不需要关心有多少条记录与之匹配,最终结果集只保留 t1 的记录 +* t1 和 t2 表进行半连接,对于 t1 表中的某条记录,只需要关心在 t2 表中是否存在,而不需要关心有多少条记录与之匹配,最终结果集只保留 t1 的记录 * 半连接只是执行子查询的一种方式,MySQL 并没有提供面向用户的半连接语法 @@ -3585,7 +3577,7 @@ MySQL 支持的存储引擎: MyISAM 存储引擎: * 特点:不支持事务和外键,读取速度快,节约资源 -* 应用场景:查询和插入操作为主,只有很少更新和删除操作,并对事务的完整性、并发性要求不高 +* 应用场景:**适用于读多写少的场景**,对事务的完整性要求不高,比如一些数仓、离线数据、支付宝的年度总结之类的场景,业务进行只读操作,查询起来会更快 * 存储方式: * 每个 MyISAM 在磁盘上存储成 3 个文件,其文件名都和表名相同,拓展名不同 * 表的定义保存在 .frm 文件,表数据保存在 .MYD (MYData) 文件中,索引保存在 .MYI (MYIndex) 文件中 @@ -3601,7 +3593,7 @@ InnoDB 存储引擎:(MySQL5.5 版本后默认的存储引擎) MEMORY 存储引擎: - 特点:每个 MEMORY 表实际对应一个磁盘文件 ,该文件中只存储表的结构,表数据保存在内存中,且默认**使用 HASH 索引**,所以数据默认就是无序的,但是在需要快速定位记录可以提供更快的访问,**服务一旦关闭,表中的数据就会丢失**,存储不安全 -- 应用场景:通常用于更新不太频繁的小表,用以快速得到访问结果,类似缓存 +- 应用场景:**缓存型存储引擎**,通常用于更新不太频繁的小表,用以快速得到访问结果 - 存储方式:表结构保存在 .frm 中 MERGE 存储引擎: @@ -3650,14 +3642,10 @@ MERGE 存储引擎: | 批量插入速度 | 高 | 低 | 高 | | **外键** | **不支持** | **支持** | **不支持** | -MyISAM 和 InnoDB 的区别? +只读场景 MyISAM 比 InnoDB 更快: -* 事务:InnoDB 支持事务,MyISAM 不支持事务 -* 外键:InnoDB 支持外键,MyISAM 不支持外键 -* 索引:InnoDB 是聚集(聚簇)索引,MyISAM 是非聚集(非聚簇)索引 - -* 锁粒度:InnoDB 最小的锁粒度是行锁,MyISAM 最小的锁粒度是表锁 -* 存储结构:参考本节上半部分 +* 底层存储结构有差别,MyISAM 是非聚簇索引,叶子节点保存的是数据的具体地址,不用回表查询 +* InnoDB 每次查询需要维护 MVCC 版本状态,保证并发状态下的读写冲突问题 @@ -3720,7 +3708,7 @@ MyISAM 和 InnoDB 的区别? #### 基本介绍 -MySQL 官方对索引的定义为:索引(index)是帮助 MySQL 高效获取数据的一种数据结构,**本质是排好序的快速查找数据结构。**在表数据之外,数据库系统还维护着满足特定查找算法的数据结构,这些数据结构以某种方式指向数据, 这样就可以在这些数据结构上实现高级查找算法,这种数据结构就是索引。 +MySQL 官方对索引的定义为:索引(index)是帮助 MySQL 高效获取数据的一种数据结构,**本质是排好序的快速查找数据结构。**在表数据之外,数据库系统还维护着满足特定查找算法的数据结构,这些数据结构以某种方式指向数据, 这样就可以在这些数据结构上实现高级查找算法,这种数据结构就是索引 **索引是在存储引擎层实现的**,所以并没有统一的索引标准,即不同存储引擎的索引的工作方式并不一样 @@ -3772,7 +3760,7 @@ MySQL 官方对索引的定义为:索引(index)是帮助 MySQL 高效获 | R-tree | 不支持 | 支持 | 不支持 | | Full-text | 5.6 版本之后支持 | 支持 | 不支持 | -联合索引图示:根据身高年龄建立的组合索引(height,age) +联合索引图示:根据身高年龄建立的组合索引(height、age) ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-组合索引图.png) @@ -3999,6 +3987,8 @@ InnoDB 存储引擎中有页(Page)的概念,页是 MySQL 磁盘管理的 * InnoDB 引擎将若干个地址连接磁盘块,以此来达到页的大小 16KB * 在查询数据时如果一个页中的每条数据都能有助于定位数据记录的位置,这将会减少磁盘 I/O 次数,提高查询效率 +超过 16KB 的一条记录,主键索引页只会存储部分数据和指向**溢出页**的指针,剩余数据都会分散存储在溢出页中 + 数据页物理结构,从上到下: * File Header:上一页和下一页的指针、该页的类型(索引页、数据页、日志页等)、**校验和**、LSN(最近一次修改当前页面时的系统 lsn 值,事务持久性部分详解)等信息 @@ -4147,10 +4137,12 @@ B+ 树为了保持索引的有序性,在插入新值的时候需要做相应 一般选用数据小的字段做索引,字段长度越小,普通索引的叶子节点就越小,普通索引占用的空间也就越小 -自增主键的插入数据模式,可以让主键索引尽量地保持递增顺序插入,不涉及到挪动其他记录,**避免了页分裂** +自增主键的插入数据模式,可以让主键索引尽量地保持递增顺序插入,不涉及到挪动其他记录,**避免了页分裂**,页分裂的目的就是保证后一个数据页中的所有行主键值比前一个数据页中主键值大 +参考文章:https://developer.aliyun.com/article/919861 + *** @@ -4256,7 +4248,7 @@ B+ 树为了保持索引的有序性,在插入新值的时候需要做相应 * 需要存储引擎将索引中的数据与条件进行判断(所以**条件列必须都在同一个索引中**),所以优化是基于存储引擎的,只有特定引擎可以使用,适用于 InnoDB 和 MyISAM * 存储引擎没有调用跨存储引擎的能力,跨存储引擎的功能有存储过程、触发器、视图,所以调用这些功能的不可以进行索引下推优化 -* 对于 InnoDB 引擎只适用于二级索引,InnoDB 的聚簇索引会将整行数据读到缓冲区,不再需要去回表查询了,索引下推的目的减少回表的 IO 次数也就失去了意义 +* 对于 InnoDB 引擎只适用于二级索引,InnoDB 的聚簇索引会将整行数据读到缓冲区,不再需要去回表查询了 工作过程:用户表 user,(name, age) 是联合索引 @@ -4434,8 +4426,8 @@ INSERT INTO t VALUES('2017-4-1',1),('2018-4-1',1);-- 这两行记录分别落在 分区表的特点: -* 一个是第一次访问的时候需要访问所有分区 -* 在 Server 层认为这是同一张表,因此所有分区共用同一个 MDL 锁 +* 第一次访问的时候需要访问所有分区 +* 在 Server 层认为这是同一张表,因此**所有分区共用同一个 MDL 锁** * 在引擎层认为这是不同的表,因此 MDL 锁之后的执行过程,会根据分区表规则,只访问需要的分区 @@ -4448,14 +4440,14 @@ INSERT INTO t VALUES('2017-4-1',1),('2018-4-1',1);-- 这两行记录分别落在 分区表的优点: -* 对业务透明,相对于用户分表来说,使用分区表的业务代码更简洁。 +* 对业务透明,相对于用户分表来说,使用分区表的业务代码更简洁 * 分区表可以很方便的清理历史数据。按照时间分区的分区表,就可以直接通过 `alter table t drop partition` 这个语法直接删除分区文件,从而删掉过期的历史数据,与使用 drop 语句删除数据相比,优势是速度快、对系统影响小 使用分区表,不建议创建太多的分区,注意事项: * 分区并不是越细越好,单表或者单分区的数据一千万行,只要没有特别大的索引,对于现在的硬件能力来说都已经是小表 -* 分区不要提前预留太多,在使用之前预先创建即可。比如是按月分区,每年年底时再把下一年度的 12 个新分区创建上即可,并且对于没有数据的历史分区,要及时的 drop 掉。 +* 分区不要提前预留太多,在使用之前预先创建即可。比如是按月分区,每年年底时再把下一年度的 12 个新分区创建上即可,并且对于没有数据的历史分区,要及时的 drop 掉 @@ -4587,8 +4579,6 @@ select v from ht where k >= M order by t_modified desc limit 100; #### 执行频率 -随着生产数据量的急剧增长,很多 SQL 语句逐渐显露出性能问题,对生产的影响也越来越大,此时有问题的 SQL 语句就成为整个系统性能的瓶颈,因此必须要进行优化 - MySQL 客户端连接成功后,查询服务器状态信息: ```mysql @@ -4720,12 +4710,10 @@ EXPLAIN SELECT * FROM table_1 WHERE id = 1; MySQL **执行计划的局限**: * 只是计划,不是执行 SQL 语句,可以随着底层优化器输入的更改而更改 -* EXPLAIN 不会告诉显示关于触发器、存储过程的信息对查询的影响情况 -* EXPLAIN 不考虑各种 Cache +* EXPLAIN 不会告诉显示关于触发器、存储过程的信息对查询的影响情况, 不考虑各种 Cache * EXPLAIN 不能显示 MySQL 在执行查询时的动态,因为执行计划在执行**查询之前生成** -* EXPALIN 部分统计信息是估算的,并非精确值 * EXPALIN 只能解释 SELECT 操作,其他操作要重写为 SELECT 后查看执行计划 -* EXPLAIN PLAN 显示的是在解释语句时数据库将如何运行 SQL 语句,由于执行环境和 EXPLAIN PLAN 环境的不同,此计划可能与 SQL 语句**实际的执行计划不同** +* EXPLAIN PLAN 显示的是在解释语句时数据库将如何运行 SQL 语句,由于执行环境和 EXPLAIN PLAN 环境的不同,此计划可能与 SQL 语句**实际的执行计划不同**,部分统计信息是估算的,并非精确值 SHOW WARINGS:在使用 EXPALIN 命令后执行该语句,可以查询与执行计划相关的拓展信息,展示出 Level、Code、Message 三个字段,当 Code 为 1003 时,Message 字段展示的信息类似于将查询语句重写后的信息,但是不是等价,不能执行复制过来运行 @@ -4865,7 +4853,7 @@ key_len: * Using where:搜索的数据需要在 Server 层判断,无法使用索引下推 * Using join buffer:连接查询被驱动表无法利用索引,需要连接缓冲区来存储中间结果 * Using filesort:无法利用索引完成排序(优化方向),需要对数据使用外部排序算法,将取得的数据在内存或磁盘中进行排序 -* Using temporary:表示 MySQL 需要使用临时表来存储结果集,常见于排序、去重、分组等场景 +* Using temporary:表示 MySQL 需要使用临时表来存储结果集,常见于**排序、去重(UNION)、分组**等场景 * Select tables optimized away:说明仅通过使用索引,优化器可能仅从聚合函数结果中返回一行 * No tables used:Query 语句中使用 from dual 或不含任何 from 子句 @@ -5049,7 +5037,7 @@ CREATE INDEX idx_seller_name_sta_addr ON tb_seller(name, status, address); # 联 * **字符串不加单引号**,造成索引失效:隐式类型转换,当字符串和数字比较时会**把字符串转化为数字** - 在查询时,没有对字符串加单引号,查询优化器会调用 CAST 函数将 status 转换为 int 进行比较,造成索引失效 + 没有对字符串加单引号,查询优化器会调用 CAST 函数将 status 转换为 int 进行比较,造成索引失效 ```mysql EXPLAIN SELECT * FROM tb_seller WHERE name='小米科技' AND status = 1; @@ -5142,7 +5130,7 @@ CREATE INDEX idx_seller_name_sta_addr ON tb_seller(name, status, address); # 联 EXPLAIN SELECT * FROM tb_seller WHERE sellerId NOT IN ('alibaba','huawei'); ``` -* [MySQL 实战 45 讲](https://time.geekbang.org/column/article/74687)该章节最后提出了一种场景,获取到数据以后 Server 层还会做判断 +* [MySQL 实战 45 讲](https://time.geekbang.org/column/article/74687)该章节最后提出了一种慢查询场景,获取到数据以后 Server 层还会做判断 @@ -5210,7 +5198,7 @@ SHOW GLOBAL STATUS LIKE 'Handler_read%'; ##### 自增机制 -自增主键可以让主键索引尽量地保持递增顺序插入,避免了页分裂,因此索引更紧凑 +自增主键可以让主键索引尽量地保持在数据页中递增顺序插入,不自增需要寻找其他页插入,导致随机 IO 和页分裂的情况 表的结构定义存放在后缀名为.frm 的文件中,但是并不会保存自增值,不同的引擎对于自增值的保存策略不同: @@ -5252,7 +5240,7 @@ MySQL 不同的自增 id 在达到上限后的表现不同: ```c++ do { - new_id = thread_id_counter++; + new_id = thread_id_counter++; } while (!thread_ids.insert_unique(new_id).second); ``` @@ -5396,7 +5384,7 @@ CREATE TABLE `emp` ( 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); +CREATE INDEX idx_emp_age_salary ON emp(age, salary); ``` * 第一种是通过对返回数据进行排序,所有不通过索引直接返回结果的排序都叫 FileSort 排序,会在内存中重新排序 @@ -5433,7 +5421,7 @@ CREATE INDEX idx_emp_age_salary ON emp(age,salary); 内存临时表,MySQL 有两种 Filesort 排序算法: -* rowid 排序:首先根据条件(回表)取出排序字段和信息,然后在**排序区 sort buffer(Server 层)**中排序,如果 sort buffer 不够,则在临时表 temporary table 中存储排序结果。完成排序后再根据行指针**回表读取记录**,该操作可能会导致大量随机 I/O 操作 +* rowid 排序:首先根据条件取出排序字段和信息,然后在**排序区 sort buffer(Server 层)**中排序,如果 sort buffer 不够,则在临时表 temporary table 中存储排序结果。完成排序后再根据行指针**回表读取记录**,该操作可能会导致大量随机 I/O 操作 说明:对于临时内存表,回表过程只是简单地根据数据行的位置,直接访问内存得到数据,不会导致多访问磁盘,优先选择该方式 @@ -5486,7 +5474,7 @@ GROUP BY 也会进行排序操作,与 ORDER BY 相比,GROUP BY 主要只是 * 创建索引:索引本身有序,不需要临时表,也不需要再额外排序 ```mysql - CREATE INDEX idx_emp_age_salary ON emp(age,salary); + 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) @@ -5562,7 +5550,7 @@ MySQL 4.1 版本之后,开始支持 SQL 的子查询 ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL嵌套查询2.png) - 连接查询之所以效率更高 ,是因为不需要在内存中创建临时表来完成逻辑上需要两个步骤的查询工作 + 连接查询之所以效率更高 ,是因为**不需要在内存中创建临时表**来完成逻辑上需要两个步骤的查询工作 @@ -5597,7 +5585,7 @@ MySQL 4.1 版本之后,开始支持 SQL 的子查询 * 优化方式二:方案适用于主键自增的表,可以把 LIMIT 查询转换成某个位置的查询 ```mysql - EXPLAIN SELECT * FROM tb_user_1 WHERE id > 200000 LIMIT 10; -- 写法 1 + 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 ``` @@ -5761,7 +5749,7 @@ Flush 链表是一个用来**存储脏页**的链表,对于已经修改过的 * 从 Flush 链表中刷新一部分页面到磁盘: * **后台线程定时**从 Flush 链表刷脏,根据系统的繁忙程度来决定刷新速率,这种方式称为 BUF_FLUSH_LIST - * 线程刷脏的比较慢,导致用户线程加载一个新的数据页时发现没有空闲缓冲页,此时会尝试从 LRU 链表尾部寻找未修改的缓冲页直接释放,如果没有就将 LRU 链表尾部的一个脏页**同步刷新**到磁盘,速度较慢,这种方式称为 BUF_FLUSH_SINGLE_PAGE + * 线程刷脏的比较慢,导致用户线程加载一个新的数据页时发现没有空闲缓冲页,此时会尝试从 LRU 链表尾部寻找缓冲页直接释放,如果该页面是已经修改过的脏页就**同步刷新**到磁盘,速度较慢,这种方式称为 BUF_FLUSH_SINGLE_PAGE * 从 LRU 链表的冷数据中刷新一部分页面到磁盘,即:BUF_FLUSH_LRU * 后台线程会定时从 LRU 链表的尾部开始扫描一些页面,扫描的页面数量可以通过系统变量 `innodb_lru_scan_depth` 指定,如果在 LRU 链表中发现脏页,则把它们刷新到磁盘,这种方式称为 BUF_FLUSH_LRU * 控制块里会存储该缓冲页是否被修改的信息,所以可以很容易的获取到某个缓冲页是否是脏页 @@ -5778,9 +5766,9 @@ Flush 链表是一个用来**存储脏页**的链表,对于已经修改过的 ##### LRU 链表 -当 Buffer Pool 中没有空闲缓冲页时就需要淘汰掉最近最少使用的部分缓冲页,为了实现这个功能,MySQL 创建了一个 LRU 链表,当访问某个页时: +Buffer Pool 需要保证缓存的命中率,所以 MySQL 创建了一个 LRU 链表,当访问某个页时: -* 如果该页不在 Buffer Pool 中,把该页从磁盘加载进来后会将该缓冲页对应的控制块作为节点放入 **LRU 链表的头部** +* 如果该页不在 Buffer Pool 中,把该页从磁盘加载进来后会将该缓冲页对应的控制块作为节点放入 **LRU 链表的头部**,保证热点数据在链表头 * 如果该页在 Buffer Pool 中,则直接把该页对应的控制块移动到 LRU 链表的头部,所以 LRU 链表尾部就是最近最少使用的缓冲页 MySQL 基于局部性原理提供了预读功能: @@ -5788,7 +5776,7 @@ MySQL 基于局部性原理提供了预读功能: * 线性预读:系统变量 `innodb_read_ahead_threshold`,如果顺序访问某个区(extent:16 KB 的页,连续 64 个形成一个区,一个区默认 1MB 大小)的页面数超过了该系统变量值,就会触发一次**异步读取**下一个区中全部的页面到 Buffer Pool 中 * 随机预读:如果某个区 13 个连续的页面都被加载到 Buffer Pool,无论这些页面是否是顺序读取,都会触发一次**异步读取**本区所有的其他页面到 Buffer Pool 中 -预读会造成加载太多用不到的数据页,造成那些使用**频率很高的数据页被挤到 LRU 链表尾部**,所以 InnoDB 将 LRU 链表分成两段: +预读会造成加载太多用不到的数据页,造成那些使用频率很高的数据页被挤到 LRU 链表尾部,所以 InnoDB 将 LRU 链表分成两段,**冷热数据隔离**: * 一部分存储使用频率很高的数据页,这部分链表也叫热数据,young 区,靠近链表头部的区域 * 一部分存储使用频率不高的冷数据,old 区,靠近链表尾部,默认占 37%,可以通过系统变量 `innodb_old_blocks_pct` 指定 @@ -5855,7 +5843,7 @@ MySQL 5.7.5 之前 `innodb_buffer_pool_size` 只支持在系统启动时修改 #### Change -InnoDB 管理的 Buffer Pool 中有一块内存叫 Change Buffer 用来对**增删改操作**提供缓存,参数 `innodb_change_buffer_max_size ` 来动态设置,设置为 50 时表示 Change Buffer 的大小最多只能占用 Buffer Pool 的 50% +InnoDB 管理的 Buffer Pool 中有一块内存叫 Change Buffer 用来对**增删改操作**提供缓存,可以通过参数来动态设置,设置为 50 时表示 Change Buffer 的大小最多占用 Buffer Pool 的 50% * 唯一索引的更新不能使用 Change Buffer,需要将数据页读入内存,判断没有冲突在写入 * 普通索引可以使用 Change Buffer,**直接写入 Buffer 就结束**,不用校验唯一性 @@ -5912,7 +5900,7 @@ SHOW PROCESSLIST 获取线程信息后,处于 Sending to client 状态代表 read_rnd_buffer 是 MySQL 的随机读缓冲区,当按任意顺序读取记录行时将分配一个随机读取缓冲区,进行排序查询时,MySQL 会首先扫描一遍该缓冲,以避免磁盘搜索,提高查询速度,大小是由 read_rnd_buffer_size 参数控制的 -**Multi-Range Read 优化**,将随机 IO 转化为顺序 IO 以降低查询过程中 IO 开销,因为大多数的数据都是按照主键递增顺序插入得到,所以按照主键的递增顺序查询的话,对磁盘的读比较接近顺序读,能够提升读性能 +Multi-Range Read 优化,**将随机 IO 转化为顺序 IO** 以降低查询过程中 IO 开销,因为大多数的数据都是按照主键递增顺序插入得到,所以按照主键的递增顺序查询的话,对磁盘的读比较接近顺序读,能够提升读性能 二级索引为 a,聚簇索引为 id,优化回表流程: @@ -6324,7 +6312,7 @@ InnoDB 存储引擎支持事务,所以加锁分析是基于该存储引擎 * Read Committed 级别,增删改操作会加写锁(行锁),读操作不加锁 - 在 Server 层过滤条件时发现不满足的记录会调用 unlock_row 方法释放该记录的行锁,保证最后只有满足条件的记录加锁,但是扫表过程中每条记录的**加锁操作不能省略**。所以对数据量很大的表做批量修改时,如果无法使用相应的索引(全表扫描),在Server 过滤数据时就会特别慢,出现虽然没有修改某些行的数据,但是还是被锁住了的现象(锁表),这种情况同样适用于 RR + 在 Server 层过滤条件时发现不满足的记录会调用 unlock_row 方法释放该记录的行锁,保证最后只有满足条件的记录加锁,但是扫表过程中每条记录的**加锁操作不能省略**。所以对数据量很大的表做批量修改时,如果无法使用相应的索引(全表扫描),在 Server 过滤数据时就会特别慢,出现虽然没有修改某些行的数据,但是还是被锁住了的现象(锁表),这种情况同样适用于 RR * Repeatable Read 级别,增删改操作会加写锁,读操作不加锁。因为读写锁不兼容,**加了读锁后其他事务就无法修改数据**,影响了并发性能,为了保证隔离性和并发性,MySQL 通过 MVCC 解决了读写冲突。RR 级别下的锁有很多种,锁机制章节详解 @@ -6354,7 +6342,7 @@ InnoDB 存储引擎提供了两种事务日志:redo log(重做日志)和 u * redo log 用于保证事务持久性 * undo log 用于保证事务原子性和隔离性 -undo log 属于逻辑日志,根据每行操作进行记录,记录了 SQL 执行相关的信息,用来回滚行记录到某个版本 +undo log 属于**逻辑日志**,根据每行操作进行记录,记录了 SQL 执行相关的信息,用来回滚行记录到某个版本 当事务对数据库进行修改时,InnoDB 会先记录对应的 undo log,如果事务执行失败或调用了 rollback 导致事务回滚,InnoDB 会根据 undo log 的内容**做与之前相反的操作**: @@ -6438,9 +6426,9 @@ roll_pointer 是一个指针,**指向记录对应的 undo log 日志**,一 * 将旧纪录进行 delete mark,在更新语句提交后由 purge 线程移入垃圾链表 * 根据更新的各列的值创建一条新纪录,插入到聚簇索引中 -在对一条记录修改前会**将记录的隐藏列 trx_id 和 roll_pointer 的旧值记录到 undo log 对应的属性中**,这样当前记录的 roll_pointer 指向当前 undo log 记录,当前 undo log 记录的 roll_pointer 指向旧的 undo log 记录,**形成一个版本链** +在对一条记录修改前会**将记录的隐藏列 trx_id 和 roll_pointer 的旧值记录到当前 undo log 对应的属性中**,这样当前记录的 roll_pointer 指向当前 undo log 记录,当前 undo log 记录的 roll_pointer 指向旧的 undo log 记录,**形成一个版本链** -UPDATE、DELETE 操作产生的 undo 日志可能会用于其他事务的 MVCC 操作,所以不能立即删除 +UPDATE、DELETE 操作产生的 undo 日志会用于其他事务的 MVCC 操作,所以不能立即删除,INSERT 可以删除的原因是 MVCC 是对现有数据的快照 @@ -6561,7 +6549,7 @@ undo log 是逻辑日志,记录的是每个事务对数据执行的操作, undo log 的作用: * 保证事务进行 rollback 时的原子性和一致性,当事务进行回滚的时候可以用 undo log 的数据进行恢复 -* 用于 MVCC 快照读,通过读取 undo log 的历史版本数据可以实现不同事务版本号都拥有自己独立的快照数据版本 +* 用于 MVCC 快照读,通过读取 undo log 的历史版本数据可以实现不同事务版本号都拥有自己独立的快照数据 undo log 主要分为两种: @@ -6607,7 +6595,7 @@ Read View 几个属性: 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 < 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 中 @@ -6723,7 +6711,7 @@ Buffer Pool 的使用提高了读写数据的效率,但是如果 MySQL 宕机 * redo log **记录数据页的物理修改**,而不是某一行或某几行的修改,用来恢复提交后的数据页,只能**恢复到最后一次提交**的位置 * redo log 采用的是 WAL(Write-ahead logging,**预写式日志**),所有修改要先写入日志,再更新到磁盘,保证了数据不会因 MySQL 宕机而丢失,从而满足了持久性要求 -* 简单的 redo log 是纯粹的物理日志,负责的 redo log 会存在物理日志和逻辑日志 +* 简单的 redo log 是纯粹的物理日志,复杂的 redo log 会存在物理日志和逻辑日志 工作过程:MySQL 发生了宕机,InnoDB 会判断一个数据页在崩溃恢复时丢失了更新,就会将它读到内存,然后根据 redo log 内容更新内存,更新完成后,内存页变成脏页,然后进行刷脏 @@ -6749,15 +6737,15 @@ Buffer Pool 的使用提高了读写数据的效率,但是如果 MySQL 宕机 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,该位置之后都是空闲区域(**碰撞指针**) +* log buffer 中有一个指针 buf_free,来标识该位置之前都是填满的 block,该位置之后都是空闲区域 MySQL 规定对底层页面的一次原子访问称为一个 Mini-Transaction(MTR),比如在 B+ 树上插入一条数据就算一个 MTR * 一个事务包含若干个 MTR,一个 MTR 对应一组若干条 redo log,一组 redo log 是不可分割的,在进行数据恢复时也把一组 redo log 当作一个不可分割的整体处理 -* 所以不是每生成一条 redo 日志就将其插入到 log buffer 中,而是一个 MTR 结束后**将一组 redo 日志写入 log buffer** +* 不是每生成一条 redo 日志就将其插入到 log buffer 中,而是一个 MTR 结束后**将一组 redo 日志写入** -InnoDB 的 redo log 是**固定大小**的,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` @@ -6774,10 +6762,10 @@ redo 日志文件也是由若干个 512 字节的 block 组成,日志文件的 ##### 日志刷盘 -redo log 需要在事务提交时将日志写入磁盘,但是比将内存中的 Buffer Pool 修改的数据写入磁盘的速度快,原因: +redo log 需要在事务提交时将日志写入磁盘,但是比 Buffer Pool 修改的数据写入磁盘的速度快,原因: * 刷脏是随机 IO,因为每次修改的数据位置随机;redo log 和 binlog 都是**顺序写**,磁盘的顺序 IO 比随机 IO 速度要快 -* 刷脏是以数据页(Page)为单位的,一个页上的一个小修改都要整页写入;redo log 中只包含真正需要写入的部分,减少无效 IO +* 刷脏是以数据页(Page)为单位的,一个页上的一个小修改都要整页写入;redo log 中只包含真正需要写入的部分,好几页的数据修改可能只记录在一个 redo log 页中,减少无效 IO * **组提交机制**,可以大幅度降低磁盘的 IO 消耗 InnoDB 引擎会在适当的时候,把内存中 redo log buffer 持久化(fsync)到磁盘,具体的**刷盘策略**: @@ -6788,7 +6776,6 @@ InnoDB 引擎会在适当的时候,把内存中 redo log buffer 持久化(fs * 2:在事务提交时将缓冲区的 redo 日志异步写入到磁盘,不能保证提交时肯定会写入,只是有这个动作。日志已经在操作系统的缓存,如果操作系统没有宕机而 MySQL 宕机,也是可以恢复数据的 * 写入 redo log buffer 的日志超过了总容量的一半,就会将日志刷入到磁盘文件,这会影响执行效率,所以开发中应**避免大事务** * 服务器关闭时 -* checkpoint 时(下小节详解) * 并行的事务提交(组提交)时,会将将其他事务的 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 对应的修改已经持久化到磁盘 @@ -6807,7 +6794,7 @@ lsn (log sequence number) 代表已经写入的 redo 日志量、flushed_to_disk MTR 的执行过程中修改过的页对应的控制块会加到 Buffer Pool 的 flush 链表中,链表中脏页是按照第一次修改的时间进行排序的(头插),控制块中有两个指针用来记录脏页被修改的时间: -* oldest_modification:第一次修改 Buffer Pool 中某个缓冲页时,将修改该页的 MTR **开始时**对应的 lsn 值写入这个属性,所以链表页是以该值进行排序的 +* oldest_modification:第一次修改 Buffer Pool 中某个缓冲页时,将修改该页的 MTR **开始时**对应的 lsn 值写入这个属性 * newest_modification:每次修改页面,都将 MTR 结束时全局的 lsn 值写入这个属性,所以该值是该页面最后一次修改后的 lsn 值 全局变量 checkpoint_lsn 表示**当前系统可以被覆盖的 redo 日志总量**,当 redo 日志对应的脏页已经被刷新到磁盘后,该文件空间就可以被覆盖重用,此时执行一次 checkpoint 来更新 checkpoint_lsn 的值存入管理信息(刷脏和执行一次 checkpoint 并不是同一个线程),该值的增量就代表磁盘文件中当前位置向后可以被覆盖的文件的量,所以该值是一直增大的 @@ -6840,10 +6827,8 @@ SHOW ENGINE INNODB STATUS\G 恢复的过程:按照 redo log 依次执行恢复数据,优化方式 -* 使用哈希表:根据 redo log 的 space ID 和 page number 属性计算出哈希值,将对同一页面的修改放入同一个槽里,可以一次性完成对某页的恢复,**避免了随机 IO** -* 跳过已经刷新到磁盘中的页面:数据页的 File Header 中的 FILE_PAGE_LSN 属性(类似 newest_modification)表示最近一次修改页面时的 lsn 值,如果在 checkpoint 后,数据页被刷新到磁盘中,那么该页 lsn 属性肯定大于 checkpoint_lsn - -总结:先写 redo buffer,在写 change buffer,先刷 redo log,再刷脏,在删除完成刷脏 redo log +* 使用哈希表:根据 redo log 的 space id 和 page number 属性计算出哈希值,将对同一页面的修改放入同一个槽里,可以一次性完成对某页的恢复,**避免了随机 IO** +* 跳过已经刷新到磁盘中的页面:数据页的 File Header 中的 FILE_PAGE_LSN 属性(类似 newest_modification)表示最近一次修改页面时的 lsn 值,数据页被刷新到磁盘中,那么该页 lsn 属性肯定大于 checkpoint_lsn @@ -6866,7 +6851,7 @@ MySQL 中还存在 binlog(二进制日志)也可以记录写操作并用于 * 内容不同:redo log 是物理日志,内容基于磁盘的 Page;binlog 的内容是二进制的,根据 binlog_format 参数的不同,可能基于SQL 语句、基于数据本身或者二者的混合(日志部分详解) * 写入时机不同:binlog 在事务提交时一次写入;redo log 的写入时机相对多元 -binlog 为什么不支持奔溃恢复? +binlog 为什么不支持崩溃恢复? * binlog 记录的是语句,并不记录数据页级的数据(哪个页改了哪些地方),所以没有能力恢复数据页 * binlog 是追加写,保存全量的日志,没有标志确定从哪个点开始的数据是已经刷盘了,而 redo log 只要在 checkpoint_lsn 后面的就是没有刷盘的 @@ -6881,14 +6866,14 @@ binlog 为什么不支持奔溃恢复? 更新一条记录的过程:写之前一定先读 -* 在 B+ 树中定位到该记录(这个过程也被称作加锁读),如果该记录所在的页面不在 Buffer Pool 里,先将其加载进内存 +* 在 B+ 树中定位到该记录,如果该记录所在的页面不在 Buffer Pool 里,先将其加载进内存 * 首先更新该记录对应的聚簇索引,更新聚簇索引记录时: * 更新记录前向 undo 页面写 undo 日志,由于这是更改页面,所以需要记录一下相应的 redo 日志 - 注意:修改 undo页面也是在**修改页面**,事务凡是修改页面就需要先记录相应的 redo 日志 + 注意:修改 undo 页面也是在**修改页面**,事务只要修改页面就需要先记录相应的 redo 日志 - * 然后**先记录对应的的 redo 日志**(等待 MTR 提交后写入 redo log buffer),**最后进行真正的更新记录** + * 然后**记录对应的 redo 日志**(等待 MTR 提交后写入 redo log buffer),**最后进行真正的更新记录** * 更新其他的二级索引记录,不会再记录 undo log,只记录 redo log 到 buffer 中 @@ -6929,7 +6914,7 @@ update T set c=c+1 where ID=2; * Prepare 阶段:存储引擎将该事务的 **redo 日志刷盘**,并且将本事务的状态设置为 PREPARE,代表执行完成随时可以提交事务 * Commit 阶段:先将事务执行过程中产生的 binlog 刷新到硬盘,再执行存储引擎的提交工作,引擎把 redo log 改成提交状态 -redo log 和 binlog 都可以用于表示事务的提交状态,而**两阶段提交就是让这两个状态保持逻辑上的一致**,也有利于主从复制,更好的保持主从数据的一致性 +存储引擎层的 redo log 和 server 层的 binlog 可以认为是一个分布式事务, 都可以用于表示事务的提交状态,而**两阶段提交就是让这两个状态保持逻辑上的一致**,也有利于主从复制,更好的保持主从数据的一致性 @@ -6941,12 +6926,12 @@ redo log 和 binlog 都可以用于表示事务的提交状态,而**两阶段 系统崩溃前没有提交的事务的 redo log 可能已经刷盘(定时线程或者 checkpoint),怎么处理崩溃恢复? -工作流程:通过 undo log 在服务器重启时将未提交的事务回滚掉。首先定位到 128 个回滚段遍历 slot,获取 undo 链表首节点页面的 undo segement header 中的 TRX_UNDO_STATE 属性,表示当前链表的事务属性,事务状态是活跃的就全部回滚,如果是 PREPARE 状态,就需要根据 binlog 的状态进行判断: +工作流程:获取 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 中判断对应的事务是否存在并完整,如果完整可以恢复数据,提交事务 + * 如果 redo log 里面的事务只有 prepare,就根据 XID 去 binlog 中判断对应的事务是否存在并完整,如果完整可以恢复数据 判断一个事务的 binlog 是否完整的方法: @@ -7285,7 +7270,7 @@ InnoDB 与 MyISAM 的最大不同有两点:一是支持事务;二是采用 行级锁,也称为记录锁(Record Lock),InnoDB 实现了以下两种类型的行锁: - 共享锁 (S):又称为读锁,简称 S 锁,多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改 -- 排他锁 (X):又称为写锁,简称 X 锁,不能与其他锁并存,如一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁,包括共享锁和排他锁,只有获取排他锁的事务是可以对数据读取和修改 +- 排他锁 (X):又称为写锁,简称 X 锁,不能与其他锁并存,获取排他锁的事务是可以对数据读取和修改 RR 隔离界别下,对于 UPDATE、DELETE 和 INSERT 语句,InnoDB 会**自动给涉及数据集加排他锁**(行锁),在 commit 时自动释放;对于普通 SELECT 语句,不会加任何锁(只是针对 InnoDB 层来说的,因为在 Server 层会**加 MDL 读锁**),通过 MVCC 防止并发冲突 @@ -7416,7 +7401,7 @@ InnoDB 会对间隙(GAP)进行加锁,就是间隙锁 (RR 隔离级别下 InnoDB 加锁的基本单位是 next-key lock,该锁是行锁和 gap lock 的组合(X or S 锁),但是加锁过程是分为间隙锁和行锁两段执行 -* 可以**保护当前记录和前面的间隙**,遵循左开右闭原则,单纯的是间隙锁左开右开 +* 可以**保护当前记录和前面的间隙**,遵循左开右闭原则,单纯的间隙锁是左开右开 * 假设有 10、11、13,那么可能的间隙锁包括:(负无穷,10]、(10,11]、(11,13]、(13,正无穷) 几种索引的加锁情况: @@ -7426,7 +7411,7 @@ InnoDB 加锁的基本单位是 next-key lock,该锁是行锁和 gap lock 的 * 范围查询无论是否是唯一索引,都需要访问到不满足条件的第一个值为止 * 对于联合索引且是唯一索引,如果 where 条件只包括联合索引的一部分,那么会加间隙锁 -间隙锁优点:RR 级别下间隙锁可以解决事务的一部分的**幻读问题**,通过对间隙加锁,可以防止读取过程中数据条目发生变化。一部分的意思是不会对全部间隙加锁,只能加锁一部分的间隙。 +间隙锁优点:RR 级别下间隙锁可以**解决事务的一部分的幻读问题**,通过对间隙加锁,可以防止读取过程中数据条目发生变化。一部分的意思是不会对全部间隙加锁,只能加锁一部分的间隙 间隙锁危害: @@ -7505,7 +7490,7 @@ InnoDB 为了支持多粒度的加锁,允许行锁和表锁同时存在,支 * 0:全部采用 AUTO_INC 锁 * 1:全部采用轻量级锁 -* 2:混合使用,在插入记录的数量确定是采用轻量级锁,不确定时采用 AUTO_INC 锁 +* 2:混合使用,在插入记录的数量确定时采用轻量级锁,不确定时采用 AUTO_INC 锁 @@ -7762,7 +7747,7 @@ MySQL 的主从之间维持了一个**长连接**。主库内部有一个线程 主从复制主要依赖的是 binlog,MySQL 默认是异步复制,需要三个线程: -- binlog thread:在主库事务提交时,负责把数据变更记录在二进制日志文件 binlog 中,并通知 slave 有数据更新 +- binlog thread:在主库事务提交时,把数据变更记录在日志文件 binlog 中,并通知 slave 有数据更新 - I/O thread:负责从主服务器上**拉取二进制日志**,并将 binlog 日志内容依次写到 relay log 中转日志的最末端,并将新的 binlog 文件名和 offset 记录到 master-info 文件中,以便下一次读取日志时从指定 binlog 日志文件及位置开始读取新的 binlog 日志内容 - SQL thread:监测本地 relay log 中新增了日志内容,读取中继日志并重做其中的 SQL 语句,从库在 relay-log.info 中记录当前应用中继日志的文件名和位点以便下一次执行 @@ -7846,7 +7831,7 @@ coordinator 就是原来的 SQL Thread,并行复制中它不再直接更新数 * 线程分配完成并不是立即执行,为了防止造成更新覆盖,更新同一 DB 的两个事务必须被分发到同一个工作线程 * 同一个事务不能被拆开,必须放到同一个工作线程 -MySQL 5.6 版本的策略:每个线程对应一个 hash 表,用于保存当前这个线程的执行队列里的事务所涉及的表,hash 表的 key 是数据库 名,value 是一个数字,表示队列中有多少个事务修改这个库,适用于主库上有多个 DB 的情况 +MySQL 5.6 版本的策略:每个线程对应一个 hash 表,用于保存当前这个线程的执行队列里的事务所涉及的表,hash 表的 key 是数据库名,value 是一个数字,表示队列中有多少个事务修改这个库,适用于主库上有多个 DB 的情况 每个事务在分发的时候,跟线程的**冲突**(事务操作的是同一个库)关系包括以下三种情况: @@ -7856,7 +7841,7 @@ MySQL 5.6 版本的策略:每个线程对应一个 hash 表,用于保存当 优缺点: -* 构造 hash 值的时候很快,只需要库名,而且一个实例上 DB 数也不会很多,不会出现需要构造很多个项的情况 +* 构造 hash 值的时候很快,只需要库名,而且一个实例上 DB 数也不会很多,不会出现需要构造很多项的情况 * 不要求 binlog 的格式,statement 格式的 binlog 也可以很容易拿到库名(日志章节详解了 binlog) * 主库上的表都放在同一个 DB 里面,这个策略就没有效果了;或者不同 DB 的热点不同,比如一个是业务逻辑库,一个是系统配置库,那也起不到并行的效果,需要**把相同热度的表均匀分到这些不同的 DB 中**,才可以使用这个策略 @@ -7987,7 +7972,7 @@ SELECT master_pos_wait(file, pos[, timeout]); * 选定一个从库执行判断位点语句,如果返回值是 >=0 的正整数,说明从库已经同步完事务,可以在这个从库执行查询语句 * 如果出现其他情况,需要到主库执行查询语句 -注意:如果所有的从库都延迟超过 timeout 秒,查询压力就都跑到主库上,所以需要进行权衡 +注意:如果所有的从库都延迟超过 timeout 秒,查询压力就都跑到主库上,所以需要进行权衡 @@ -8289,7 +8274,7 @@ 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 集合**,用来对应当前实例执行过的所有事务 +启动 MySQL 实例时,加上参数 `gtid_mode=on` 和 `enforce_gtid_consistency=on` 就可以启动 GTID 模式,每个事务都会和一个 GTID 一一对应,每个 MySQL 实例都维护了一个 GTID 集合,用来存储当前实例**执行过的所有事务** GTID 有两种生成方式,使用哪种方式取决于 session 变量 gtid_next: @@ -8357,7 +8342,7 @@ GTID 有两种生成方式,使用哪种方式取决于 session 变量 gtid_nex ### 日志分类 -在任何一种数据库中,都会有各种各样的日志,记录着数据库工作的过程,可以帮助数据库管理员追踪数据库曾经发生过的各种事件。 +在任何一种数据库中,都会有各种各样的日志,记录着数据库工作的过程,可以帮助数据库管理员追踪数据库曾经发生过的各种事件 MySQL日志主要包括六种: @@ -8558,7 +8543,7 @@ mysqlbinlog log-file; #### 数据恢复 -误删库或者表时,需要根据 binlog 进行数据恢复, +误删库或者表时,需要根据 binlog 进行数据恢复 一般情况下数据库有定时的全量备份,假如每天 0 点定时备份,12 点误删了库,恢复流程: @@ -8611,7 +8596,7 @@ SELECT * FROM tb_book WHERE id < 8 ### 慢日志 -慢查询日志记录所有执行时间超过 long_query_time 并且扫描记录数不小于 min_examined_row_limit 的所有的 SQL 语句的日志。long_query_time 默认为 10 秒,最小为 0, 精度到微秒 +慢查询日志记录所有执行时间超过 long_query_time 并且扫描记录数不小于 min_examined_row_limit 的所有的 SQL 语句的日志long_query_time 默认为 10 秒,最小为 0, 精度到微秒 慢查询日志默认是关闭的,可以通过两个参数来控制慢查询日志,配置文件 `/etc/mysql/my.cnf`: @@ -8747,9 +8732,9 @@ long_query_time=10 ### 概述 -NoSQL(Not-Only SQL):泛指非关系型的数据库,作为关系型数据库的补充。 +NoSQL(Not-Only SQL):泛指非关系型的数据库,作为关系型数据库的补充 -MySQL 支持 ACID 特性,保证可靠性和持久性,读取性能不高,因此需要缓存的来减缓数据库的访问压力。 +MySQL 支持 ACID 特性,保证可靠性和持久性,读取性能不高,因此需要缓存的来减缓数据库的访问压力 作用:应对基于海量用户和海量数据前提下的数据处理问题 @@ -8792,17 +8777,6 @@ Redis (REmote DIctionary Server) :用 C 语言开发的一个开源的高性 * 有序集合类型:zset/sorted_set(TreeSet) * 支持持久化,可以进行数据灾难恢复 -应用: - -* 为热点数据加速查询(主要场景),如热点商品、热点新闻、热点资讯、推广类等高访问量信息等 - -* 即时信息查询,如排行榜、网站访问统计、公交到站信息、在线人数(聊天室、网站)、设备信号等 - -* 时效性信息控制,如验证码控制、投票控制等 - -* 分布式数据共享,如分布式集群架构中的 session 分离 -* 消息队列 - *** @@ -8811,56 +8785,6 @@ Redis (REmote DIctionary Server) :用 C 语言开发的一个开源的高性 ### 安装启动 -#### CentOS - -1. 下载 Redis - - 下载安装包: - - ```sh - wget http://download.redis.io/releases/redis-5.0.0.tar.gz - ``` - - 解压安装包: - - ```sh - tar –xvf redis-5.0.0.tar.gz - ``` - - 编译(在解压的目录中执行): - - ```sh - make - ``` - - 安装(在解压的目录中执行): - - ```sh - make install - ``` - - - -2. 安装 Redis - - redis-server,服务器启动命令 客户端启动命令 - - redis-cli,redis核心配置文件 - - redis.conf,RDB文件检查工具(快照持久化文件) - - redis-check-dump,AOF文件修复工具 - - redis-check-aof - - - -*** - - - -#### Ubuntu - 安装: * Redis 5.0 被包含在默认的 Ubuntu 20.04 软件源中 @@ -8925,7 +8849,7 @@ Redis (REmote DIctionary Server) :用 C 语言开发的一个开源的高性 mkdir data ``` -2. 创建配置文件副本放入 conf 目录,Ubuntu系统配置文件 redis.conf 在目录 `/etc/redis` 中 +2. 创建配置文件副本放入 conf 目录,Ubuntu 系统配置文件 redis.conf 在目录 `/etc/redis` 中 ```sh cat redis.conf | grep -v "#" | grep -v "^$" -> /conf/redis-6379.conf @@ -9147,7 +9071,7 @@ redis[1]> #### key space -Redis 是一个键值对(key-value pair)数据库服务器,每个数据库都由一个 redisDb 结构表示,redisDb.dict 字典中保存了数据库的所有键值对,将这个字典称为键空间(key space) +Redis 是一个键值对(key-value pair)数据库服务器,每个数据库都由一个 redisDb 结构表示,redisDb.dict **字典中保存了数据库的所有键值对**,将这个字典称为键空间(key space) ```c typedef struct redisDB { @@ -9324,7 +9248,7 @@ Redis 通过过期字典可以检查一个给定键是否过期: * AOF 重写,会对数据库中的键进行检查,忽略已经过期的键 * 复制:当服务器运行在复制模式下时,从服务器的过期键删除动作由主服务器控制 * 主服务器在删除一个过期键之后,会显式地向所有从服务器发送一个 DEL 命令,告知从服务器删除这个过期键 - * 从服务器在执行客户端发送的读命令时,即使碰到过期键也不会将过期键删除,会当作未过期键处理,只有在接到主服务器发来的 DEL 命令之后,才会删除过期键 + * 从服务器在执行客户端发送的读命令时,即使碰到过期键也不会将过期键删除,会当作未过期键处理,只有在接到主服务器发来的 DEL 命令之后,才会删除过期键(数据不一致) @@ -9343,7 +9267,7 @@ Redis 通过过期字典可以检查一个给定键是否过期: 针对过期数据有三种删除策略: - 定时删除 -- 惰性删除 +- 惰性删除(被动删除) - 定期删除 Redis 采用惰性删除和定期删除策略的结合使用 @@ -9398,15 +9322,13 @@ Redis 采用惰性删除和定期删除策略的结合使用 * 如果删除操作执行得太频繁,或者执行时间太长,就会退化成定时删除策略,将 CPU 时间过多地消耗在删除过期键上 * 如果删除操作执行得太少,或者执行时间太短,定期删除策略又会和惰性删除策略一样,出现浪费内存的情况 -所以采用定期删除策略的话,服务器必须根据情况合理地设置删除操作的执行时长和执行频率 - 定期删除是**周期性轮询 Redis 库中的时效性**数据,从过期字典中随机抽取一部分键检查,利用过期数据占比的方式控制删除频度 - Redis 启动服务器初始化时,读取配置 server.hz 的值,默认为 10,执行指令 info server 可以查看,每秒钟执行 server.hz 次 `serverCron() → activeExpireCycle()` - activeExpireCycle() 对某个数据库中的每个 expires 进行检测,工作模式: - * 轮询每个数据库,从数据库中取出一定数量的随机键进行检查,并删除其中的过期键 + * 轮询每个数据库,从数据库中取出一定数量的随机键进行检查,并删除其中的过期键,如果过期 key 的比例超过了 25%,则继续重复此过程,直到过期 key 的比例下降到 25% 以下,或者这次任务的执行耗时超过了 25 毫秒 * 全局变量 current_db 用于记录 activeExpireCycle() 的检查进度(哪一个数据库),下一次调用时接着该进度处理 * 随着函数的不断执行,服务器中的所有数据库都会被检查一遍,这时将 current_db 重置为 0,然后再次开始新一轮的检查 @@ -9562,7 +9484,7 @@ SORT key ALPHA #对key中字母排序,按照字典序 对于 `SORT key [ASC/DESC]` 函数: * 在执行升序排序时,排序算法使用的对比函数产生升序对比结果 -* 在执行降序排序时,排序算法所使用的对比函数产生降序对比结果 +* 在执行降序排序时,排序算法使用的对比函数产生降序对比结果 @@ -9794,7 +9716,7 @@ Redis 基于 Reactor 模式开发了网络事件处理器,这个处理器被 -尽管多个文件事件可能会并发出现,但是 I/O 多路复用程序将所有产生事件的套接字处理请求放入一个**单线程的执行队列**中,通过队列有序、同步的向文件事件分派器传送套接字,上一个套接字产生的事件处理完后,才会继续向分派器传送下一个 +I/O 多路复用程序将所有产生事件的套接字处理请求放入一个**单线程的执行队列**中,通过队列有序、同步的向文件事件分派器传送套接字,上一个套接字产生的事件处理完后,才会继续向分派器传送下一个 @@ -9813,7 +9735,7 @@ Redis 单线程也能高效的原因: ##### 多路复用 -Redis 的 I/O 多路复用程序的所有功能都是通过包装常见的 select 、epoll、 evport 和 kqueue 这些函数库来实现的,Redis在 I/O 多路复用程序的实现源码中用 #include 宏定义了相应的规则,编译时自动选择系统中**性能最高的多路复用函数**来作为底层实现 +Redis 的 I/O 多路复用程序的所有功能都是通过包装常见的 select 、epoll、 evport 和 kqueue 这些函数库来实现的,Redis 在 I/O 多路复用程序的实现源码中用 #include 宏定义了相应的规则,编译时自动选择系统中**性能最高的多路复用函数**来作为底层实现 I/O 多路复用程序监听多个套接字的 AE_READABLE 事件和 AE_WRITABLE 事件,这两类事件和套接字操作之间的对应关系如下: @@ -9840,9 +9762,9 @@ Redis 为文件事件编写了多个处理器,这些事件处理器分别用 Redis 客户端与服务器进行连接并发送命令的整个过程: * Redis 服务器正在运作监听套接字的 AE_READABLE 事件,关联连接应答处理器 -* 当 Redis 客户端向服务器发起连接,监听套接字将产生 AE_READABLE 事件,触发连接应答处理器执行,对客户端的连接请求进行应答,创建客户端套接字以及客户端状态,并将客户端套接字的 AE_READABLE 事件与命令请求处理器进行关联 +* 当 Redis 客户端向服务器发起连接,监听套接字将产生 AE_READABLE 事件,触发连接应答处理器执行,对客户端的连接请求进行应答,创建客户端套接字以及客户端状态,并将客户端套接字的 **AE_READABLE 事件与命令请求处理器**进行关联 * 客户端向服务器发送命令请求,客户端套接字产生 AE_READABLE 事件,引发命令请求处理器执行,读取客户端的命令内容传给相关程序去执行 -* 执行命令会产生相应的命令回复,为了将这些命令回复传送回客户端,服务器会将客户端套接字的 AE_WRITABLE 事件与命令回复处理器进行关联 +* 执行命令会产生相应的命令回复,为了将这些命令回复传送回客户端,服务器会将客户端套接字的 **AE_WRITABLE 事件与命令回复处理器**进行关联 * 当客户端尝试读取命令回复时,客户端套接字产生 AE_WRITABLE 事件,触发命令回复处理器执行,在命令回复全部写入套接字后,服务器就会解除客户端套接字的 AE_WRITABLE 事件与命令回复处理器之间的关联 @@ -9879,8 +9801,6 @@ Redis 的时间事件分为以下两类: 无序链表并不影响时间事件处理器的性能,因为正常模式下的 Redis 服务器**只使用 serverCron 一个时间事件**,在 benchmark 模式下服务器也只使用两个时间事件,所以无序链表不会影响服务器的性能,几乎可以按照一个指针处理 -服务器 → serverCron 详解该时间事件 - *** @@ -9892,18 +9812,6 @@ Redis 的时间事件分为以下两类: 服务器中同时存在文件事件和时间事件两种事件类型,调度伪代码: ```python -# Redis 服务器的主函数的伪代码 -def main(): - # 初始化服务器 - init_server() - - # 循环处理事件,直到服务器关闭 - while server_is_not_shutdown(): - aeProcessEvents() - - # 服务器关闭 - clean_server() - # 事件调度伪代码 def aeProcessEvents(): # 获取到达时间离当前时间最接近的时间事件 @@ -10047,7 +9955,7 @@ typedef struct redisClient { 客户端状态包括两类属性 * 一类是比较通用的属性,这些属性很少与特定功能相关,无论客户端执行的是什么工作,都要用到这些属性 -* 另一类是和特定功能相关的属性,比如操作数据库时用到的 db 属性和 dict id属性,执行事务时用到的 mstate 属性,以及执行 WATCH 命令时用到的 watched_keys 属性等,代码中没有列出 +* 另一类是和特定功能相关的属性,比如操作数据库时用到的 db 属性和 dict id 属性,执行事务时用到的 mstate 属性,以及执行 WATCH 命令时用到的 watched_keys 属性等,代码中没有列出 @@ -10095,24 +10003,12 @@ typedef struct redisClient { 一部分标志记录目前**客户端所处的状态**: -* REDIS_MONITOR 表示客户端正在执行 MONITOR 命令 * REDIS_UNIX_SOCKET 表示服务器使用 UNIX 套接字来连接客户端 * REDIS_BLOCKED 表示客户端正在被 BRPOP、BLPOP 等命令阻塞 * REDIS_UNBLOCKED 表示客户端已经从 REDIS_BLOCKED 所表示的阻塞状态脱离,在 REDIS_BLOCKED 标志打开的情况下使用 * REDIS_MULTI 标志表示客户端正在执行事务 * REDIS_DIRTY_CAS 表示事务使用 WATCH 命令监视的数据库键已经被修改 -* REDIS_DIRTY_EXEC 表示事务在命令入队时出现了错误。以上两个标志都表示事务的安全性已经被破坏,只要两个标记中的任意一个被打开,EXEC 命令必然会执行失败,这两个标志只能在客户端打开了 REDIS_MULTI 标志的情况下使用 -* REDIS_CLOSE_ASAP 表示客户端的输出缓冲区大小超出了服务器允许的范围,关闭这个客户端并且直接丢弃缓冲区的内容 -* REDIS_CLOSE_AFTER_REPLY 表示有用户对这个客户端执行了 `CLIENT KILL` 命令,或者客户端发送给服务器的命令请求中包含了错误的协议内容,服务器会将客户端积存在输出缓冲区中的所有内容发送给客户端,然后关闭客户端 -* REDIS_ASKING 表示客户端向集群节点(运行在集群模式下的服务器)发送了 `ASKING` 命令 -* REDIS_FORCE_AOF 表示强制服务器将当前执行的命令写入到 AOF 文件里,执行 `PUBSUB` 命令会使客户端打开该标志 -* REDIS_FORCE_REPL 表示强制主服务器将当前执行的命令复制给所有从服务器,执行 `SCRIPT LOAD` 命令会使客户端打开 REDIS_FORCE_AOF 标志和 REDIS_FORCE_REPL 标志 -* REDIS_MASTER_FORCE_REPLY 表示将要进行主从复制,在主从服务器进行命令传播期间,从服务器需要向主服务器发送 `REPLICATION ACK` 命令,在发送这个命令之前从服务器必须打开主服务器对应的客户端的该标志,否则发送操作会被拒绝执行 - -Redis 只会将对数据库进行了修改的命令写入到 AOF 文件,并复制到各个从服务器,但是 PUBSUB 和 SCRIPT LOAD 命令例外: - -* PUBSUB 命令虽然没有修改数据库,但 PUBSUB 命令向频道的所有订阅者发送消息这一行为带有副作用,接收到消息的所有客户端的状态都会因为这个命令而改变,所以服务器需要使用 REDIS_FORCE_AOF 标志强制将这个命令写入 AOF 文件。这样在将来载入 AOF 文件时,服务器就可以再次执行相同的 PUBSUB 命令,并产生相同的副作用 -* SCRIPT LOAD 命令虽然没有修改数据库,但它修改了服务器状态,所以也是一个带有副作用的命令,需要使用 REDIS_FORCE_AOF +* ..... @@ -10218,7 +10114,7 @@ obuf_soft_limit_reached_time 属性记录了**输出缓冲区第一次到达软 服务器使用不同的方式来创建和关闭不同类型的客户端 -如果客户端是通过网络连接与服务器进行连接的普通客户端,那么在客户端使用 connect 函数连接到服务器时,服务器就会调用连接事件处理器为客户端创建相应的客户端状态,并将这个新的客户端状态添加到服务器状态结构 clients 链表的末尾 +如果客户端是通过网络连接与服务器进行连接的普通客户端,那么在客户端使用 connect 函数连接到服务器时,服务器就会调用连接应答处理器为客户端创建相应的客户端状态,并将这个新的客户端状态添加到服务器状态结构 clients 链表的末尾 ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-服务器clients链表.png) @@ -10400,7 +10296,7 @@ struct redisCommand { ##### 基本介绍 -Redis 服务器以周期性事件的方式来运行 serverCron 函数,服务器初始化时读取配置 server.hz 的值,默认为 10,代表每秒钟执行 10 次,即每隔 100 毫秒执行一次,执行指令 info server 可以查看 +Redis 服务器以周期性事件的方式来运行 serverCron 函数,服务器初始化时读取配置 server.hz 的值,默认为 10,代表每秒钟执行 10 次,即**每隔 100 毫秒执行一次**,执行指令 info server 可以查看 serverCron 函数负责定期对自身的资源和状态进行检查和调整,从而确保服务器可以长期、稳定地运行 @@ -10569,7 +10465,7 @@ clientsCron 函数对一定数量的客户端进行以下两个检查: * 如果客户端与服务器之间的连接巳经超时(很长一段时间客户端和服务器都没有互动),那么程序释放这个客户端 * 如果客户端在上一次执行命令请求之后,输入缓冲区的大小超过了一定的长度,那么程序会释放客户端当前的输入缓冲区,并重新创建一个默认大小的输入缓冲区,从而防止客户端的输入缓冲区耗费了过多的内存 -databasesCron 函数会对服务器中的一部分数据库进行检查,删除其中的过期键,并在有需要时 对字典进行收缩操作 +databasesCron 函数会对服务器中的一部分数据库进行检查,删除其中的过期键,并在有需要时对字典进行收缩操作 @@ -10579,7 +10475,7 @@ databasesCron 函数会对服务器中的一部分数据库进行检查,删除 ##### 持久状态 -服务器状态中记录执行 BGSAVE 命令和 BGREWRITEAOF 命令的子进程的 ID, +服务器状态中记录执行 BGSAVE 命令和 BGREWRITEAOF 命令的子进程的 ID ```c struct redisServer { @@ -10632,7 +10528,7 @@ serverCron 函数会检查 BGSAVE 或者 BGREWRITEAOF 命令是否正在执行 -##### cronloops +##### 执行次数 服务器状态的 cronloops 属性记录了 serverCron 函数执行的次数 @@ -10684,7 +10580,7 @@ struct redisServer { initServer 还进行了非常重要的设置操作: * 为服务器设置进程信号处理器 -* 创建共享对象,包含 OK、ERR、整数 1 到 10000 的字符串对象等 +* 创建共享对象,包含 OK、ERR、**整数 1 到 10000 的字符串对象**等 * **打开服务器的监听端口** * **为 serverCron 函数创建时间事件**, 等待服务器正式运行时执行 serverCron 函数 * 如果 AOF 持久化功能已经打开,那么打开现有的 AOF 文件,如果 AOF 文件不存在,那么创建并打开一个新的 AOF 文件 ,为 AOF 写入做好准备 @@ -10850,7 +10746,7 @@ struct sdshdr { }; ``` -SDS 遵循 C 字符串**以空字符结尾**的惯例, 保存空字符的 1 字节不计算在 len 属性,SDS 会自动为空字符分配额外的 1 字节空间和添加空字符到字符串末尾,所以空字符对于 SDS 的使用者来说是完全透明的 +SDS 遵循 C 字符串**以空字符结尾**的惯例,保存空字符的 1 字节不计算在 len 属性,SDS 会自动为空字符分配额外的 1 字节空间和添加空字符到字符串末尾,所以空字符对于 SDS 的使用者来说是完全透明的 ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-SDS底层结构.png) @@ -10880,7 +10776,7 @@ SDS 遵循 C 字符串**以空字符结尾**的惯例, 保存空字符的 1 二进制安全: * C 字符串中的字符必须符合某种编码(比如 ASCII)方式,除了字符串末尾以外其他位置不能包含空字符,否则会被误认为是字符串的结尾,所以只能保存文本数据 -* SDS 的 API 都是二进制安全的,使用字节数组 buf 保存一系列的二进制数据,使用 len 属性来判断数据的结尾,所以可以保存图片、视频、压缩文件等二进制数据 +* SDS 的 API 都是二进制安全的,使用字节数组 buf 保存一系列的二进制数据,**使用 len 属性来判断数据的结尾**,所以可以保存图片、视频、压缩文件等二进制数据 兼容 C 字符串的函数:SDS 会在为 buf 数组分配空间时多分配一个字节来保存空字符,所以可以重用一部分 C 字符串函数库的函数 @@ -10898,7 +10794,7 @@ SDS 通过未使用空间解除了字符串长度和底层数组长度之间的 内存重分配涉及复杂的算法,需要执行**系统调用**,是一个比较耗时的操作,SDS 的两种优化策略: -* 空间预分配:当 SDS 的 API 进行修改并且需要进行空间扩展时,程序不仅会为 SDS 分配修改所必需的空间, 还会为 SDS 分配额外的未使用空间 +* 空间预分配:当 SDS 需要进行空间扩展时,程序不仅会为 SDS 分配修改所必需的空间, 还会为 SDS 分配额外的未使用空间 * 对 SDS 修改之后,SDS 的长度(len 属性)小于 1MB,程序分配和 len 属性同样大小的未使用空间,此时 len 和 free 相等 @@ -10910,7 +10806,7 @@ SDS 通过未使用空间解除了字符串长度和底层数组长度之间的 在扩展 SDS 空间前,API 会先检查 free 空间是否足够,如果足够就无需执行内存重分配,所以通过预分配策略,SDS 将连续增长 N 次字符串所需内存的重分配次数从**必定 N 次降低为最多 N 次** -* 惰性空间释放:当 SDS 的 API 需要缩短字符串时,程序并不立即使用内存重分配来回收缩短后多出来的字节,而是使用 free 属性将这些字节的数量记录起来,并等待将来使用 +* 惰性空间释放:当 SDS 缩短字符串时,程序并不立即使用内存重分配来回收缩短后多出来的字节,而是使用 free 属性将这些字节的数量记录起来,并等待将来复用 SDS 提供了相应的 API 来真正释放 SDS 的未使用空间,所以不用担心空间惰性释放策略造成的内存浪费问题 @@ -11109,7 +11005,7 @@ load_factor = ht[0].used / ht[0].size 原因:执行该命令的过程中,Redis 需要创建当前服务器进程的子进程,而大多数操作系统都采用写时复制(copy-on­-write)技术来优化子进程的使用效率,通过提高执行扩展操作的负载因子,尽可能地避免在子进程存在期间进行哈希表扩展操作,可以避免不必要的内存写入操作,最大限度地节约内存 -哈希表执行收缩的条件:负载因子小于 0.1(自动执行,servreCron 中检测),缩小为字典中数据个数的 50% 左右 +哈希表执行收缩的条件:负载因子小于 0.1(自动执行,servreCron 中检测) @@ -11125,7 +11021,7 @@ load_factor = ht[0].used / ht[0].size * 如果执行的是扩展操作,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 做准备 +* 当 ht[0] 包含的所有键值对都迁移到了 ht[1] 之后(ht[0] 变为空表),释放 ht[0],将 ht[1] 设置为 ht[0],并在 ht[1] 创建一个新的空白哈希表,为下一次 rehash 做准备 如果哈希表里保存的键值对数量很少,rehash 就可以在瞬间完成,但是如果哈希表里数据很多,那么要一次性将这些键值对全部 rehash 到 ht[1] 需要大量计算,可能会导致服务器在一段时间内停止服务 @@ -11133,8 +11029,8 @@ Redis 对 rehash 做了优化,使 rehash 的动作并不是一次性、集中 * 为 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 进行期间,每次对字典执行增删改查操作时,程序除了执行指定的操作以外,还会顺带将 ht[0] 哈希表在 rehashidx 索引上的所有键值对 rehash 到 ht[1],rehash 完成之后**将 rehashidx 属性的值增一** +* 随着字典操作的不断执行,最终在某个时间点 ht[0] 的所有键值对都被 rehash 至 ht[1],将 rehashidx 属性的值设为 -1 渐进式 rehash 采用**分而治之**的方式,将 rehash 键值对所需的计算工作均摊到对字典的每个添加、删除、查找和更新操作上,从而避免了集中式 rehash 带来的庞大计算量 @@ -11207,23 +11103,23 @@ typedef struct zskiplistNode { 层:level 数组包含多个元素,每个元素包含指向其他节点的指针。根据幕次定律(power law,越大的数出现的概率越小)**随机**生成一个介于 1 和 32 之间的值(Redis5 之后最大为 64)作为 level 数组的大小,这个大小就是层的高度,节点的第一层是 level[0] = L1 -前进指针:forward 用于从表头到表尾方向正序(升序)遍历节点,遇到 NULL 停止遍历 +前进指针:forward 用于从表头到表尾方向**正序(升序)遍历节点**,遇到 NULL 停止遍历 -跨度:span 用于记录两个节点之间的距离,用来**计算排位(rank)**: +跨度:span 用于记录两个节点之间的距离,用来计算排位(rank): * 两个节点之间的跨度越大相距的就越远,指向 NULL 的所有前进指针的跨度都为 0 -* 在查找某个节点的过程中,将沿途访问过的所有层的跨度累计起来,结果就是目标节点在跳跃表中的排位,按照上图所示: +* 在查找某个节点的过程中,**将沿途访问过的所有层的跨度累计起来,结果就是目标节点在跳跃表中的排位**,按照上图所示: 查找分值为 3.0 的节点,沿途经历的层:查找的过程只经过了一个层,并且层的跨度为 3,所以目标节点在跳跃表中的排位为 3 查找分值为 2.0 的节点,沿途经历的层:经过了两个跨度为 1 的节点,因此可以计算出目标节点在跳跃表中的排位为 2 -后退指针:backward 用于从表尾到表头方向逆序(降序)遍历节点 +后退指针:backward 用于从表尾到表头方向**逆序(降序)遍历节点** -分值:score 属性一个 double 类型的浮点数,跳跃表中的所有节点都**按分值从小到大来排序** +分值:score 属性一个 double 类型的浮点数,跳跃表中的所有节点都按分值从小到大来排序 -成员对象:obj 属性是一个指针,指向一个 SDS 字符串对象。同一个跳跃表中,各个节点保存的成员对象必须是唯一的,但是多个节点保存的分值可以是相同的,分值相同的节点将按照成员对象在字典序中的大小来进行排序(从小到大) +成员对象:obj 属性是一个指针,指向一个 SDS 字符串对象。同一个跳跃表中,各个节点保存的**成员对象必须是唯一的**,但是多个节点保存的分值可以是相同的,分值相同的节点将按照成员对象在字典序中的大小来进行排序(从小到大) @@ -11268,7 +11164,7 @@ encoding 取值为三种:INTSET_ENC_INT16、INTSET_ENC_INT32、INTSET_ENC_INT6 -#### 升级降级 +#### 类型升级 整数集合添加的新元素的类型比集合现有所有元素的类型都要长时,需要先进行升级(upgrade),升级流程: @@ -11295,7 +11191,7 @@ encoding 取值为三种:INTSET_ENC_INT16、INTSET_ENC_INT32、INTSET_ENC_INT6 * 节约内存:要让数组可以同时保存 int16、int32、int64 三种类型的值,可以直接使用 int64_t 类型的数组作为整数集合的底层实现,但是会造成内存浪费,整数集合可以确保升级操作只会在有需要的时候进行,尽量节省内存 -整数集合不支持降级操作,一旦对数组进行了升级,编码就会一直保持升级后的状态 +整数集合**不支持降级操作**,一旦对数组进行了升级,编码就会一直保持升级后的状态 @@ -11313,7 +11209,7 @@ encoding 取值为三种:INTSET_ENC_INT16、INTSET_ENC_INT32、INTSET_ENC_INT6 ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-压缩列表底层结构.png) -* zlbytes:uint32_t 类型 4 字节,记录整个压缩列表占用的内存字节数,在对压缩列表进行内存重分或者计算 zlend 的位置时使用 +* zlbytes:uint32_t 类型 4 字节,记录整个压缩列表占用的内存字节数,在对压缩列表进行内存重分配或者计算 zlend 的位置时使用 * zltail:uint32_t 类型 4 字节,记录压缩列表表尾节点距离起始地址有多少字节,通过这个偏移量程序无须遍历整个压缩列表就可以确定表尾节点的地址 * zllen:uint16_t 类型 2 字节,记录了压缩列表包含的节点数量,当该属性的值小于 UINT16_MAX (65535) 时,该值就是压缩列表中节点的数量;当这个值等于 UINT16_MAX 时节点的真实数量需要遍历整个压缩列表才能计算得出 * entryX:列表节点,压缩列表中的各个节点,**节点的长度由节点保存的内容决定** @@ -11335,14 +11231,14 @@ encoding 取值为三种:INTSET_ENC_INT16、INTSET_ENC_INT32、INTSET_ENC_INT6 ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-压缩列表节点.png) -previous_entry_length:以字节为单位记录了压缩列表中前一个节点的长度,程序可以通过指针运算,根据当前节点的起始地址来计算出前一个节点的起始地址,完成从表尾向表头遍历操作 +previous_entry_length:以字节为单位记录了压缩列表中前一个节点的长度,程序可以通过指针运算,根据当前节点的起始地址来计算出前一个节点的起始地址,完成**从表尾向表头遍历**操作 * 如果前一节点的长度小于 254 字节,该属性的长度为 1 字节,前一节点的长度就保存在这一个字节里 * 如果前一节点的长度大于等于 254 字节,该属性的长度为 5 字节,其中第一字节会被设置为 0xFE(十进制 254),之后的四个字节则用于保存前一节点的长度 encoding:记录了节点的 content 属性所保存的数据类型和长度 -* 长度为 1 字节、2 字节或者 5 字节,值的最高位为 00、01 或者 10 的是字节数组编码,数组的长度由编码除去最高两位之后的其他位记录,下划线 `_` 表示留空,而 `b`、`x` 等变量则代表实际的二进制数据 +* **长度为 1 字节、2 字节或者 5 字节**,值的最高位为 00、01 或者 10 的是字节数组编码,数组的长度由编码除去最高两位之后的其他位记录,下划线 `_` 表示留空,而 `b`、`x` 等变量则代表实际的二进制数据 ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-压缩列表字节数组编码.png) @@ -11431,7 +11327,7 @@ typedef struct redisObiect { Redis 并没有直接使用数据结构来实现键值对数据库,而是基于这些数据结构创建了一个对象系统,包含字符串对象、列表对象、哈希对象、集合对象和有序集合对象这五种类型的对象,而每种对象又通过不同的编码映射到不同的底层数据结构 -Redis 自身是一个 Map,其中所有的数据都是采用 key : value 的形式存储,**键对象都是字符串对象**,而值对象有五种基本类型和三种高级类型对象 +Redis 是一个 Map 类型,其中所有的数据都是采用 key : value 的形式存储,**键对象都是字符串对象**,而值对象有五种基本类型和三种高级类型对象 ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-对象编码.png) @@ -11652,7 +11548,7 @@ Redis 所有操作都是**原子性**的,采用**单线程**机制,命令是 -#### 对象 +#### 实现 字符串对象的编码可以是 int、raw、embstr 三种 @@ -12189,8 +12085,8 @@ set 类型:与 hash 存储结构哈希表完全相同,只是仅存储键不 使用字典加跳跃表的优势: -* 字典为有序集合创建了一个从成员到分值的映射,用 O(1) 复杂度查找给定成员的分值 -* 排序操作使用跳跃表完成,节省每次重新排序带来的时间成本和空间成本 +* 字典为有序集合创建了一个**从成员到分值的映射**,用 O(1) 复杂度查找给定成员的分值 +* **排序操作使用跳跃表完成**,节省每次重新排序带来的时间成本和空间成本 使用 ziplist 格式存储需要满足以下两个条件: @@ -12199,6 +12095,11 @@ set 类型:与 hash 存储结构哈希表完全相同,只是仅存储键不 当元素比较多时,此时 ziplist 的读写效率会下降,时间复杂度是 O(n),跳表的时间复杂度是 O(logn) +为什么用跳表而不用平衡树? + +* 在做范围查找的时候,跳表操作简单(前进指针或后退指针),平衡树需要回旋查找 +* 跳表比平衡树实现简单,平衡树的插入和删除操作可能引发子树的旋转调整,而跳表的插入和删除只需要修改相邻节点的指针 + *** @@ -12453,7 +12354,7 @@ AOF:将数据的操作过程进行保存,日志形式,存储操作过程 #### 文件创建 -RDB 持久化功能所生成的 RDB 文件 是一个经过压缩的紧凑二进制文件,通过该文件可以还原生成 RDB 文件时的数据库状态,有两个 Redis 命令可以生成 RDB 文件,一个是 SAVE,另一个是 BGSAVE +RDB 持久化功能所生成的 RDB 文件是一个经过压缩的紧凑二进制文件,通过该文件可以还原生成 RDB 文件时的数据库状态,有两个 Redis 命令可以生成 RDB 文件,一个是 SAVE,另一个是 BGSAVE @@ -12486,7 +12387,7 @@ BGSAVE:bg 是 background,代表后台执行,命令的完成需要两个进 -流程:客户端发出 BGSAVE 指令,Redis 服务器使用 fork 函数创建一个子进程,然后响应后台已经开始执行的信息给客户端。子进程会去执行持久化的操作,持久化过程是先将数据写入到一个临时文件中,持久化操作结束再用这个临时文件**替换**上次持久化的文件 +流程:客户端发出 BGSAVE 指令,Redis 服务器使用 fork 函数创建一个子进程,然后响应后台已经开始执行的信息给客户端。子进程会异步执行持久化的操作,持久化过程是先将数据写入到一个临时文件中,持久化操作结束再用这个临时文件**替换**上次持久化的文件 ```python # 创建子进程 @@ -12521,7 +12422,7 @@ rdbchecksum yes|no * SAVE 命令会被服务器拒绝,服务器禁止 SAVE 和 BGSAVE 命令同时执行是为了避免父进程(服务器进程)和子进程同时执行两个 rdbSave 调用,产生竞争条件 * BGSAVE 命令也会被服务器拒绝,也会产生竞争条件 * BGREWRITEAOF 和 BGSAVE 两个命令不能同时执行 - * 如果 BGSAVE 命令正在执行,那么 BGREWRITEAOF 命令会被延迟到 BGSAVE 命令执行完毕之后执行 + * 如果 BGSAVE 命令正在执行,那么 BGREWRITEAOF 命令会被**延迟**到 BGSAVE 命令执行完毕之后执行 * 如果 BGREWRITEAOF 命令正在执行,那么 BGSAVE 命令会被服务器拒绝 @@ -12697,7 +12598,10 @@ AOF 写数据过程: +Redis 只会将对数据库进行了修改的命令写入到 AOF 文件,并复制到各个从服务器,但是 PUBSUB 和 SCRIPT LOAD 命令例外: +* PUBSUB 命令虽然没有修改数据库,但 PUBSUB 命令向频道的所有订阅者发送消息这一行为带有副作用,接收到消息的所有客户端的状态都会因为这个命令而改变,所以服务器需要使用 REDIS_FORCE_AOF 标志强制将这个命令写入 AOF 文件。这样在将来载入 AOF 文件时,服务器就可以再次执行相同的 PUBSUB 命令,并产生相同的副作用 +* SCRIPT LOAD 命令虽然没有修改数据库,但它修改了服务器状态,所以也是一个带有副作用的命令,需要使用 REDIS_FORCE_AOF @@ -12738,7 +12642,7 @@ struct redisServer { ##### 文件写入 -服务器在处理文件事件时可能会执行写命令,追加一些内容到 aof_buf 缓冲区里,所以服务器每次结束一个事件循环之前,就会执行 flushAppendOnlyFile 函数,判断是否需要**将 aof_buf 缓冲区中的内容写入和保存到 AOF 文件**里 +服务器在处理文件事件时会执行**写命令,追加一些内容到 aof_buf 缓冲区**里,所以服务器每次结束一个事件循环之前,就会执行 flushAppendOnlyFile 函数,判断是否需要**将 aof_buf 缓冲区中的内容写入和保存到 AOF 文件**里 flushAppendOnlyFile 函数的行为由服务器配置的 appendfsync 选项的值来决定 @@ -12751,12 +12655,12 @@ appendfsync always|everysec|no #AOF写数据策略:默认为everysec 特点:安全性最高,数据零误差,但是性能较低,不建议使用 -- everysec:先将 aof_buf 缓冲区中的内容写入到 AOF 文件,判断上次同步 AOF 文件的时间距离现在超过一秒钟,再次对 AOF 文件进行同步,这个同步操作是由一个(子)线程专门负责执行的 +- everysec:先将 aof_buf 缓冲区中的内容写入到操作系统缓存,判断上次同步 AOF 文件的时间距离现在超过一秒钟,再次进行同步 fsync,这个同步操作是由一个(子)线程专门负责执行的 特点:在系统突然宕机的情况下丢失 1 秒内的数据,准确性较高,性能较高,建议使用,也是默认配置 -- no:将 aof_buf 缓冲区中的内容写入到 AOF 文件,但并不对 AOF 文件进行同步,何时同步由操作系统来决定 +- no:将 aof_buf 缓冲区中的内容写入到操作系统缓存,但并不进行同步,何时同步由操作系统来决定 特点:**整体不可控**,服务器宕机会丢失上次同步 AOF 后的所有写指令 @@ -12819,8 +12723,6 @@ Redis 读取 AOF 文件并还原数据库状态的步骤: ##### 重写策略 -随着命令不断写入 AOF,文件会越来越大,很可能对 Redis 服务器甚至整个宿主计算机造成影响,为了解决这个问题 Redis 引入了 AOF 重写机制压缩文件体积 - AOF 重写:读取服务器当前的数据库状态,**生成新 AOF 文件来替换旧 AOF 文件**,不会对现有的 AOF 文件进行任何读取、分析或者写入操作,而是直接原子替换。新 AOF 文件不会包含任何浪费空间的冗余命令,所以体积通常会比旧 AOF 文件小得多 AOF 重写规则: @@ -12856,7 +12758,7 @@ bgrewriteaof * 子进程进行 AOF 重写期间,服务器进程(父进程)可以继续处理命令请求 -* 子进程带有服务器进程的数据副本,使用子进程而不是线程,可以在避免使用锁的情况下, 保证数据的安全性 +* 子进程带有服务器进程的数据副本,使用子进程而不是线程,可以在避免使用锁的情况下, 保证数据安全性 ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-AOF手动重写原理.png) @@ -12865,7 +12767,7 @@ bgrewriteaof 工作流程: * Redis 服务器执行完一个写命令,会同时将该命令追加到 AOF 缓冲区和 AOF 重写缓冲区(从创建子进程后才开始写入) -* 当子进程完成 AOF 重写工作之后,会向父进程发送一个信号,父进程在接到该信号之后, 会调用一个信号处理函数,该函数执行时会**对服务器进程(父进程)造成阻塞**(影响很小),主要工作: +* 当子进程完成 AOF 重写工作之后,会向父进程发送一个信号,父进程在接到该信号之后, 会调用一个信号处理函数,该函数执行时会**对服务器进程(父进程)造成阻塞**(影响很小,类似 JVM STW),主要工作: * 将 AOF 重写缓冲区中的所有内容写入到新 AOF 文件中, 这时新 AOF 文件所保存的状态将和服务器当前的数据库状态一致 * 对新的 AOF 文件进行改名,**原子地(atomic)覆盖**现有的 AOF 文件,完成新旧两个 AOF 文件的替换 @@ -12922,7 +12824,7 @@ RDB 的特点 AOF 特点: -* AOF 的优点:数据持久化有较好的实时性,通过 AOF 重写可以降低文件的体积 +* AOF 的优点:数据持久化有**较好的实时性**,通过 AOF 重写可以降低文件的体积 * AOF 的缺点:文件较大时恢复较慢 AOF 和 RDB 同时开启,系统默认取 AOF 的数据(数据不会存在丢失) @@ -13051,7 +12953,7 @@ int main(void) -在 p3224 和 p3225 执行完第二个循环后,main 函数退出,进程死亡。所以 p3226,p3227 就没有父进程了,成为孤儿进程,所以 p3226 和 p3227 的父进程就被置为 ID 为 1的 init 进程(笔记 Tool → Linux → 进程管理详解) +在 p3224 和 p3225 执行完第二个循环后,main 函数退出,进程死亡。所以 p3226,p3227 就没有父进程了,成为孤儿进程,所以 p3226 和 p3227 的父进程就被置为 ID 为 1 的 init 进程(笔记 Tool → Linux → 进程管理详解) 参考文章:https://blog.csdn.net/love_gaohz/article/details/41727415 @@ -13256,7 +13158,7 @@ Redis 不支持事务回滚机制(rollback),即使事务队列中的某个 -* 执行错误:命令执行出现错误,例如对字符串进行 incr 操作,食物中正确的命令会被执行,运行错误的命令不会被执行 +* 执行错误:命令执行出现错误,例如对字符串进行 incr 操作,事务中正确的命令会被执行,运行错误的命令不会被执行 @@ -13369,7 +13271,7 @@ Redis 服务器创建并修改 Lua 环境的整个过程: * 创建 redis.pcall 函数的错误报告辅助函数 `_redis_err_handler `,这个函数可以打印出错代码的来源和发生错误的行数 -* 对 Lua环境中的全局环境进行保护,确保传入服务器的脚本不会因忘记使用 local 关键字,而将额外的全局变量添加到 Lua 环境 +* 对 Lua 环境中的全局环境进行保护,确保传入服务器的脚本不会因忘记使用 local 关键字,而将额外的全局变量添加到 Lua 环境 * 将完成修改的 Lua 环境保存到服务器状态的 lua 属性中,等待执行服务器传来的 Lua 脚本 @@ -13468,7 +13370,7 @@ EVAL 命令第二步是将客户端传入的脚本保存到服务器的 lua_scri EVAL 命令第三步是执行脚本函数 -* 将 EVAL 命令中传入的**键名(key name)参数和脚本参数**分别保存到 KEYS 数组和 ARGV 数组,将这两个数组作为**全局变量**传入到 Lua 环境里 +* 将 EVAL 命令中传入的**键名参数和脚本参数**分别保存到 KEYS 数组和 ARGV 数组,将这两个数组作为**全局变量**传入到 Lua 环境 * 为 Lua 环境装载超时处理钩子(hook),这个钩子可以在脚本出现超时运行情况时,让客户端通过 `SCRIPT KILL` 命令停止脚本,或者通过 SHUTDOWN 命令直接关闭服务器 因为 Redis 是单线程的执行命令,当 Lua 脚本阻塞时需要兜底策略,可以中断执行 @@ -13536,8 +13438,6 @@ Redis 复制 EVAL、SCRIPT FLUSH、SCRIPT LOAD 三个命令的方法和复制普 - - *** @@ -13595,7 +13495,7 @@ Redis 分布式锁的基本使用,悲观锁 `NX`:只在键不存在时,才对键进行设置操作,`SET key value NX` 效果等同于 `SETNX key value` - `XX` :只在键已经存在时,才对键进行设置操作 + `XX`:只在键已经存在时,才对键进行设置操作 `EX`:设置键 key 的过期时间,单位时秒 @@ -13616,7 +13516,7 @@ Redis 分布式锁的基本使用,悲观锁 PEXPIRE lock-key milliseconds ``` - 通过 EXPIRE 设置过期时间缺乏原子性,如果在 SETNX 和 EXPIRE 之间出现异常,锁也无法释放 + 通过 EXPIRE 设置过期时间缺乏原子性,如果在 SETNX 和 EXPIRE 之间出现异常,锁也无法释放 * 在 SET 时指定过期时间,保证原子性 @@ -13643,7 +13543,7 @@ SETNX 获取锁时,设置一个指定的唯一值(UUID),释放前获取 SET lock_key unique_value NX PX 10000 ``` - Lua 脚本(unlock.script)实现的释放锁操作的伪代码:key 类型参数会放入 KEYS 数组,其它参数会放入 ARGV 数组,在脚本中通过 KEYS 和 ARGV 传递参数,**保证判断标识和释放锁这两个操作的原子性** +Lua 脚本(unlock.script)实现的释放锁操作的伪代码:key 类型参数会放入 KEYS 数组,其它参数会放入 ARGV 数组,在脚本中通过 KEYS 和 ARGV 传递参数,**保证判断标识和释放锁这两个操作的原子性** ```sh EVAL "return redis.call('set', KEYS[1], ARGV[1])" 1 lock_key unique_value # 1 代表需要一个参数 @@ -13715,9 +13615,10 @@ end 超时释放:锁超时释放可以避免死锁,但如果是业务执行耗时较长,需要进行锁续时,防止业务未执行完提前释放锁 -看门狗 watchDog 机制: +看门狗 Watch Dog 机制: * 获取锁成功后,提交周期任务,每隔一段时间(Redisson 中默认为过期时间 / 3),重置一次超时时间 +* 如果服务宕机,Watch Dog 机制线程就停止,就不会再延长 key 的过期时间 * 释放锁后,终止周期任务 @@ -13732,7 +13633,7 @@ end 主从一致性:集群模式下,主从同步存在延迟,当加锁后主服务器宕机时,从服务器还没同步主服务器中的锁数据,此时从服务器升级为主服务器,其他线程又可以获取到锁 -将服务器升级为多主多从,: +将服务器升级为多主多从: * 获取锁需要从所有主服务器 SET 成功才算获取成功 * 某个 master 宕机,slave 还没有同步锁数据就升级为 master,其他线程尝试加锁会加锁失败,因为其他 master 上已经存在该锁 @@ -13764,7 +13665,7 @@ end 主从复制的特点: -* **薪火相传**:一个 slave 可以是下一个 slave 的 master,slave 同样可以接收其他 slave 的连接和同步请求,那么该 slave 作为了链条中下一个的 master, 可以有效减轻 master 的写压力,去中心化降低风险 +* **薪火相传**:一个 slave 可以是下一个 slave 的 master,slave 同样可以接收其他 slave 的连接和同步请求,那么该 slave 作为了链条中下一个的 master,可以有效减轻 master 的写压力,去中心化降低风险 注意:主机挂了,从机还是从机,无法写数据了 @@ -14106,7 +14007,7 @@ PSYNC 命令的调用方法有两种 #### 心跳机制 -心跳机制:进入命令传播阶段,从服务器默认会以每秒一次的频率,向主服务器发送命令:`REPLCONF ACK `,re_offset 是从服务器当前的复制偏移量 +心跳机制:进入命令传播阶段,**从服务器**默认会以每秒一次的频率,**向主服务器发送命令**:`REPLCONF ACK `,replication_offset 是从服务器当前的复制偏移量 心跳的作用: @@ -14145,16 +14046,19 @@ slavel: ip=127.0.0.1,port=22222,state=online,offset=456,lag=3 # 3秒之前发送 #### 配置选项 -Redis 的 min-slaves-to-write 和 min-slaves-max-lag 两个选项可以防止主服务器在**不安全的情况下**执行写命令 +Redis 的 min-slaves-to-write 和 min-slaves-max-lag 两个选项可以防止主服务器在**不安全的情况下**拒绝执行写命令 比如向主服务器设置: +* min-slaves-to-write:主库最少有 N 个健康的从库存活才能执行写命令,没有足够的从库直接拒绝写入 +* min-slaves-max-lag:从库和主库进行数据复制时的 ACK 消息延迟的最大时间 + ```sh min-slaves-to-write 5 min-slaves-max-lag 10 ``` -那么在从服务器的数最少于 5 个,或者 5 个从服务器的延迟(lag)值都大于或等于10 秒时,主服务器将拒绝执行写命令 +那么在从服务器的数少于 5 个,或者 5 个从服务器的延迟(lag)值都大于或等于10 秒时,主服务器将拒绝执行写命令 @@ -14209,7 +14113,7 @@ master 的 CPU 占用过高或 slave 频繁断开连接 * 出现的原因: * slave 每 1 秒发送 REPLCONF ACK 命令到 master - * 当 slave 接到了慢查询时(keys * ,hgetall等),会大量占用 CPU 性能 + * 当 slave 接到了慢查询时(keys * ,hgetall 等),会大量占用 CPU 性能 * master 每 1 秒调用复制定时函数 replicationCron(),比对 slave 发现长时间没有进行响应 最终导致 master 各种资源(输出缓冲区、带宽、连接等)被严重占用 @@ -14373,7 +14277,7 @@ Sentinel 本质上只是一个运行在特殊模式下的 Redis 服务器,当 #### 代码替换 -将一部分普通 Redis服务器使用的代码替换成 Sentinel 专用代码 +将一部分普通 Redis 服务器使用的代码替换成 Sentinel 专用代码 Redis 服务器端口: @@ -14417,7 +14321,7 @@ struct sentinelState { // 当前纪元,用于实现故障转移 uint64_t current_epoch; - // 保存了所有被这个sentinel监视的主服务器 + // 【保存了所有被这个sentinel监视的主服务器】 dict *masters; // 是否进入了 TILT 模式 @@ -14534,10 +14438,10 @@ typedef struct sentinelAddr { ##### 主服务器 -Sentinel 默认会以每十秒一次的频率,通过命令连接向被监视的主服务器发送 INFO 命令,来获取主服务器的当前信息 +Sentinel 默认会以每十秒一次的频率,通过命令连接向被监视的主服务器发送 INFO 命令,来获取主服务器的信息 * 一部分是主服务器本身的信息,包括 runid 域记录的服务器运行 ID,以及 role 域记录的服务器角色 -* 另一部分是服务器属下所有从服务器的信息,每个从服务器都由一个 slave 字符串开头的行记录,根据这些 IP 地址和端口号,Sentinel 无须用户提供从服务器的地址信息,就可以自动发现从服务器 +* 另一部分是服务器属下所有从服务器的信息,每个从服务器都由一个 slave 字符串开头的行记录,根据这些 IP 地址和端口号,Sentinel 无须用户提供从服务器的地址信息,就可以**自动发现从服务器** ```sh # Server @@ -14566,7 +14470,7 @@ slave1: ip=l27.0.0.1, port=22222, state=online, offset=22, lag=0 ##### 从服务器 -当 Sentinel 发现主服务器有新的从服务器出现时,会为这个新的从服务器创建相应的实例结构, 还会创建到从服务器的命令连接和订阅连接,所以 Sentinel 对所有的从服务器之间都可以进行命令操作 +当 Sentinel 发现主服务器有新的从服务器出现时,会为这个新的从服务器创建相应的实例结构,还会**创建到从服务器的命令连接和订阅连接**,所以 Sentinel 对所有的从服务器之间都可以进行命令操作 Sentinel 默认会以每十秒一次的频率,向从服务器发送 INFO 命令: @@ -14633,26 +14537,13 @@ SUBSCRIBE _sentinel_:hello * 如果信息中记录的 Sentinel 运行 ID 与自己的相同,不做进一步处理 * 如果不同,将根据信息中的各个参数,对相应主服务器的实例结构进行更新 -对于监视同一个服务器的多个 Sentinel 来说,**一个 Sentinel 发送的信息会被其他 Sentinel 接收到**,这些信息会被用于更新其他 Sentinel 对发送信息 Sentinel 的认知,也会被用于更新其他 Sentinel 对被监视的服务器的认知 - -哨兵实例之间可以相互发现,要归功于 Redis 提供发布订阅机制 - - - -*** - - - -##### 更新字典 - Sentinel 为主服务器创建的实例结构的 sentinels 字典保存所有同样监视这个**主服务器的 Sentinel 信息**(包括 Sentinel 自己),字典的键是 Sentinel 的名字,格式为 `ip:port`,值是键所对应 Sentinel 的实例结构 -当 Sentinel 接收到其他 Sentinel 发来的信息时(发送信息的为源 Sentinel,接收信息的为目标 Sentinel),目标 Sentinel 会分析提取参数,在自己的 Sentinel 状态 sentinelState.masters 中查找相应的主服务器实例结构,检查主服务器实例结构的 sentinels 字典中,源 Sentinel 的实例结构是否存在 +监视同一个服务器的 Sentinel 订阅的频道相同,Sentinel 发送的信息会被其他 Sentinel 接收到(发送信息的为源 Sentinel,接收信息的为目标 Sentinel),目标 Sentinel 在自己的 sentinelState.masters 中查找源 Sentinel 服务器的实例结构进行添加或更新 -* 如果源 Sentinel 的实例结构存在,那么对源 Sentinel 的实例结构进行更新 -* 如果源 Sentinel 的实例结构不存在,说明源 Sentinel 是刚开始监视主服务器,目标 Sentinel 会为源 Sentinel 创建一个新的实例结构,并将这个结构添加到 sentinels 字典里面 +因为 Sentinel 可以接收到的频道信息来感知其他 Sentinel 的存在,并通过发送频道信息来让其他 Sentinel 知道自己的存在,所以用户在使用 Sentinel 时并不需要提供各个 Sentinel 的地址信息,**监视同一个主服务器的多个 Sentinel 可以相互发现对方** -因为 Sentinel 可以接收到的频道信息来获知其他 Sentinel 的存在,并通过发送频道信息来让其他 Sentinel 知道自己的存在,所以用户在使用 Sentinel 时并不需要提供各个 Sentinel 的地址信息,**监视同一个主服务器的多个 Sentinel 可以自动发现对方** +哨兵实例之间可以相互发现,要归功于 Redis 提供发布订阅机制 @@ -14723,7 +14614,7 @@ SENTINEL is-master-down-by-addr 源 Sentinel 将统计其他 Sentinel 同意主服务器已下线的数量,当这一数量达到配置指定的判断客观下线所需的数量(quorum)时,Sentinel 会将主服务器对应实例结构 flags 属性的 SRI_O_DOWN 标识打开,代表客观下线,并对主服务器执行故障转移操作 -注意:不同 Sentinel 判断客观下线的条件可能不同,因为载入的配置文件中的属性(quorum)可能不同 +注意:**不同 Sentinel 判断客观下线的条件可能不同**,因为载入的配置文件中的属性 quorum 可能不同 @@ -14733,7 +14624,7 @@ SENTINEL is-master-down-by-addr ### 领头选举 -主服务器被判断为客观下线时,监视这个主服务器的各个 Sentinel 会进行协商,选举出一个领头 Sentinel 对下线服务器执行故障转移 +主服务器被判断为客观下线时,**监视该主服务器的各个 Sentinel 会进行协商**,选举出一个领头 Sentinel 对下线服务器执行故障转移 Redis 选举领头 Sentinel 的规则: @@ -14742,7 +14633,7 @@ Redis 选举领头 Sentinel 的规则: * 在一个配置纪元里,所有 Sentinel 都只有一次将某个 Sentinel 设置为局部领头 Sentinel 的机会,并且局部领头一旦设置,在这个配置纪元里就不能再更改 * Sentinel 设置局部领头 Sentinel 的规则是先到先得,最先向目标 Sentinel 发送设置要求的源 Sentinel 将成为目标 Sentinel 的局部领头 Sentinel,之后接收到的所有设置要求都会被目标 Sentinel 拒绝 -* 领头 Sentinel 的产生需要半数以上 Sentinel 的支持,并且每个 Sentinel 只有一票,所以一个配置纪元只会出现一个领头 Sentinel,比如 10 个 Sentinel 的系统中,至少需要 `10/2 + 1 = 6` 票 +* 领头 Sentinel 的产生**需要半数以上 Sentinel 的支持**,并且每个 Sentinel 只有一票,所以一个配置纪元只会出现一个领头 Sentinel,比如 10 个 Sentinel 的系统中,至少需要 `10/2 + 1 = 6` 票 选举过程: @@ -14750,7 +14641,7 @@ Redis 选举领头 Sentinel 的规则: * 目标 Sentinel 接受命令处理完成后,将返回一条命令回复,回复中的 leader_runid 和 leader_epoch 参数分别记录了目标 Sentinel 的局部领头 Sentinel 的运行 ID 和配置纪元 * 源 Sentinel 接收目标 Sentinel 命令回复之后,会判断 leader_epoch 是否和自己的相同,相同就继续判断 leader_runid 是否和自己的运行 ID 一致,成立表示目标 Sentinel 将源 Sentinel 设置成了局部领头 Sentinel,即获得一票 * 如果某个 Sentinel 被半数以上的 Sentinel 设置成了局部领头 Sentinel,那么这个 Sentinel 成为领头 Sentinel -* 如果在给定时限内,没有一个 Sentinel 被选举为领头 Sentinel,那么各个 Sentinel 将在一段时间后再次选举,直到选出领头 +* 如果在给定时限内,没有一个 Sentinel 被选举为领头 Sentinel,那么各个 Sentinel 将在一段时间后**再次选举**,直到选出领头 * 每次进行领头 Sentinel 选举之后,不论选举是否成功,所有 Sentinel 的配置纪元(configuration epoch)都要自增一次 Sentinel 集群至少 3 个节点的原因: @@ -14758,7 +14649,10 @@ Sentinel 集群至少 3 个节点的原因: * 如果 Sentinel 集群只有 2 个 Sentinel 节点,则领头选举需要 `2/2 + 1 = 2` 票,如果一个节点挂了,那就永远选不出领头 * Sentinel 集群允许 1 个 Sentinel 节点故障则需要 3 个节点的集群,允许 2 个节点故障则需要 5 个节点集群 +**如何获取哨兵节点的半数数量**? +* 客观下线是通过配置文件获取的数量,达到 quorum 就客观下线 +* 哨兵数量是通过主节点是实例结构中,保存着监视该主节点的所有哨兵信息,从而获取得到 @@ -14857,7 +14751,8 @@ typedef struct clusterState { // 集群当前的状态,是在线还是下线 int state; - // 集群中至少处理着一个槽的节点的数量,为0表示集群目前没有任何节点在处理槽 + // 集群中至少处理着一个槽的(主)节点的数量,为0表示集群目前没有任何节点在处理槽 + // 【选举时投票数量超过半数,从这里获取的】 int size; // 集群节点名单(包括 myself 节点),字典的键为节点的名字,字典的值为节点对应的clusterNode结构 @@ -14953,7 +14848,7 @@ CLUSTER MEET #### 基本操作 -Redis 集群通过分片的方式来保存数据库中的键值对,集群的整个数据库被分为16384 个槽(slot),数据库中的每个键都属于 16384 个槽中的一个,集群中的每个节点可以处理 0 个或最多 16384 个槽(**每个主节点存储的数据并不一样**) +Redis 集群通过分片的方式来保存数据库中的键值对,集群的整个数据库被分为 16384 个槽(slot),数据库中的每个键都属于 16384 个槽中的一个,集群中的每个节点可以处理 0 个或最多 16384 个槽(**每个主节点存储的数据并不一样**) * 当数据库中的 16384 个槽都有节点在处理时,集群处于上线状态(ok) * 如果数据库中有任何一个槽得到处理,那么集群处于下线状态(fail) @@ -15038,7 +14933,7 @@ typedef struct clusterState { #### 集群数据 -集群节点保存键值对以及键值对过期时间的方式,与单机 Redis 服务器保存键值对以及键值对过期时间的方式完全相同,但是集群节点只能使用 0 号数据库,单机服务器可以任意使用 +集群节点保存键值对以及键值对过期时间的方式,与单机 Redis 服务器保存键值对以及键值对过期时间的方式完全相同,但是**集群节点只能使用 0 号数据库**,单机服务器可以任意使用 除了将键值对保存在数据库里面之外,节点还会用 clusterState 结构中的 slots_to_keys 跳跃表来**保存槽和键之间的关系** @@ -15092,10 +14987,7 @@ def CLUSTER_KEYSLOT(key): reply_client(slot); ``` -判断槽是否由当前节点负责处理: - -* 如果 clusterState.slots[i] 等于 clusterState.myself,那么说明槽 i 由当前节点负责,节点可以执行客户端发送的命令 -* 如果 clusterState.slots[i] 不等于 clusterState.myself,那么说明槽 i 并非由当前节点负责,节点会根据 clusterState.slots[i] 指向的clusterNode 结构所记录的节点 IP 和端口号,向客户端返回 MOVED 错误 +判断槽是否由当前节点负责处理:如果 clusterState.slots[i] 不等于 clusterState.myself,那么说明槽 i 并非由当前节点负责,节点会根据 clusterState.slots[i] 指向的 clusterNode 结构所记录的节点 IP 和端口号,向客户端返回 MOVED 错误 @@ -15160,8 +15052,8 @@ Redis 集群的重新分片操作可以将任意数量已经指派给某个节 Redis 的集群管理软件 redis-trib 负责执行重新分片操作,redis-trib 通过向源节点和目标节点发送命令来进行重新分片操作 -* redis-trib 向目标节点发送 `CLUSTER SETSLOT IMPORTING ` 命令,让目标节点准备好从源节点导入属于槽 slot 的键值对 -* redis-trib 向源节点发送 `CLUSTER SETSLOT MIGRATING ` 命令,让源节点准备好将属于槽 slot 的键值对迁移至目标节点 +* 向目标节点发送 `CLUSTER SETSLOT IMPORTING ` 命令,准备好从源节点导入属于槽 slot 的键值对 +* 向源节点发送 `CLUSTER SETSLOT MIGRATING ` 命令,让源节点准备好将属于槽 slot 的键值对迁移 * redis-trib 向源节点发送 `CLUSTER GETKEYSINSLOT ` 命令,获得最多 count 个属于槽 slot 的键值对的键名 * 对于每个 key,redis-trib 都向源节点发送一个 `MIGRATE 0 -旁路缓存的缺点: +客户端还可以通过 PSUBSCRIBE 命令订阅一个或多个模式,每当有其他客户端向某个频道发送消息时,消息不仅会被发送给这个频道的所有订阅者,还会被**发送给所有与这个频道相匹配的模式的订阅者**,比如 `PSUBSCRIBE channel*` 订阅模式,与 channel1 匹配 -* 首次请求数据一定不在 cache 的问题,一般采用缓存预热的方法,将热点数据可以提前放入 cache 中 -* 写操作比较频繁的话导致 cache 中的数据会被频繁被删除,影响缓存命中率 +注意:发布的消息没有持久化,所以订阅的客户端只能收到订阅后发布的消息 -删除缓存而不是更新缓存的原因:每次更新数据库都更新缓存,造成无效写操作较多 +*** -**** +#### 频道操作 -#### 读写穿透 +Redis 将所有频道的订阅关系都保存在服务器状态的 pubsub_channels 字典里,键是某个被订阅的频道,值是一个记录所有订阅这个频道的客户端链表 -读写穿透模式 Read/Write Through Pattern:服务端把 cache 视为主要数据存储,从中读取数据并将数据写入其中,cache 负责将此数据同步写入 DB,从而减轻了应用程序的职责 +```c +struct redisServer { + // 保存所有频道的订阅关系, + dict *pubsub_channels; +} +``` -* 写操作:先查 cache,cache 中不存在,直接更新 DB;cache 中存在则先更新 cache,然后 cache 服务更新 DB(同步更新 cache 和 DB) +客户端执行 SUBSCRIBE 命令订阅某个或某些频道,服务器会将客户端与频道进行关联: -* 读操作:从 cache 中读取数据,读取到就直接返回 ;读取不到先从 DB 加载,写入到 cache 后返回响应 +* 频道已经存在,直接将客户端添加到链表末尾 +* 频道还未有任何订阅者,在字典中为频道创建一个键值对,再将客户端添加到链表 - Read-Through Pattern 实际只是在 Cache-Aside Pattern 之上进行了封装。在 Cache-Aside Pattern 下,发生读请求的时候,如果 cache 中不存在对应的数据,是由客户端负责把数据写入 cache,而 Read Through Pattern 则是 cache 服务自己来写入缓存的,对客户端是透明的 +UNSUBSCRIBE 命令用来退订某个频道,服务器将从 pubsub_channels 中解除客户端与被退订频道之间的关联 -Read-Through Pattern 也存在首次不命中的问题,采用缓存预热解决 -*** +**** -#### 异步缓存 -异步缓存写入 Write Behind Pattern 由 cache 服务来负责 cache 和 DB 的读写,对比读写穿透不同的是 Write Behind Caching 是只更新缓存,不直接更新 DB,改为**异步批量**的方式来更新 DB,可以减小写的成本 +#### 模式操作 -缺点:这种模式对数据一致性没有高要求,可能出现 cache 还没异步更新 DB,服务就挂掉了 +Redis 服务器将所有模式的订阅关系都保存在服务器状态的 pubsub_patterns 属性里 -应用: +```c +struct redisServer { + // 保存所有模式订阅关系,链表中每个节点是一个 pubsubPattern + list *pubsub_patterns; +} -* DB 的写性能非常高,适合一些数据经常变化又对数据一致性要求不高的场景,比如浏览量、点赞量 +typedef struct pubsubPattern { + // 订阅的客户端 + redisClient *client; + // 被订阅的模式,比如 channel* + robj *pattern; +} +``` -* MySQL 的 InnoDB Buffer Pool 机制用到了这种策略 +客户端执行 PSUBSCRIBE 命令订阅某个模式,服务器会新建一个 pubsubPattern 结构并赋值,放入 pubsub_patterns 链表结尾 +模式的退订命令 PUNSUBSCRIBE 是订阅命令的反操作,服务器在 pubsub_patterns 链表中查找并删除对应的结构 -**** -### 缓存一致 +*** -使用缓存代表不需要强一致性,只需要最终一致性 -缓存不一致的方法: -* 数据库和缓存数据强一致场景: - * 同步双写:更新 DB 时同样更新 cache,保证在一个事务中,通过加锁来保证更新 cache 时不存在线程安全问题 - - * 延迟双删:先淘汰缓存再写数据库,休眠 1 秒再次淘汰缓存,可以将 1 秒内造成的缓存脏数据再次删除 - - * 异步通知: - - * 基于 MQ 的异步通知:对数据的修改后,代码需要发送一条消息到 MQ 中,缓存服务监听 MQ 消息 - * Canal 订阅 MySQL binlog 的变更上报给 Kafka,系统监听 Kafka 消息触发缓存失效,或者直接将变更发送到处理服务,**没有任何代码侵入** - - 低耦合,可以同时通知多个缓存服务,但是时效性一般,可能存在中间不一致状态 - -* 低一致性场景: +#### 发送消息 - * 更新 DB 的时候同样更新 cache,但是给缓存加一个比较短的过期时间,这样就可以保证即使数据不一致影响也比较小 - * 使用 Redis 自带的内存淘汰机制 +Redis 客户端执行 `PUBLISH ` 命令将消息 message发送给频道 channel,服务器会执行: +* 在 pubsub_channels 字典里找到频道 channel 的订阅者名单,将消息 message 发送给所有订阅者 +* 遍历整个 pubsub_patterns 链表,查找与 channel 频道相**匹配的模式**,并将消息发送给所有订阅了这些模式的客户端 + +```c +// 如果频道和模式相匹配 +if match(channel, pubsubPattern.pattern) { + // 将消息发送给订阅该模式的客户端 + send_message(pubsubPattern.client, message); +} +``` @@ -15843,41 +15744,39 @@ Read-Through Pattern 也存在首次不命中的问题,采用缓存预热解 -### 企业方案 +#### 查看信息 -#### 缓存预热 +PUBSUB 命令用来查看频道或者模式的相关信息 -场景:宕机,服务器启动后迅速宕机 +`PUBSUB CHANNELS [pattern]` 返回服务器当前被订阅的频道,其中 pattern 参数是可选的 -问题排查: +* 如果不给定 pattern 参数,那么命令返回服务器当前被订阅的所有频道 +* 如果给定 pattern 参数,那么命令返回服务器当前被订阅的频道中与 pattern 模式相匹配的频道 -1. 请求数量较高,大量的请求过来之后都需要去从缓存中获取数据,但是缓存中又没有,此时从数据库中查找数据然后将数据再存入缓存,造成了短期内对 redis 的高强度操作从而导致问题 +`PUBSUB NUMSUB [channel-1 channel-2 ... channel-n]` 命令接受任意多个频道作为输入参数,并返回这些频道的订阅者数量 -2. 主从之间数据吞吐量较大,数据同步操作频度较高 +`PUBSUB NUMPAT` 命令用于返回服务器当前被订阅模式的数量 -解决方案: -- 前置准备工作: - 1. 日常例行统计数据访问记录,统计访问频度较高的热点数据 - 2. 利用 LRU 数据删除策略,构建数据留存队列例如:storm 与 kafka 配合 -- 准备工作: +**** - 1. 将统计结果中的数据分类,根据级别,redis 优先加载级别较高的热点数据 - 2. 利用分布式多服务器同时进行数据读取,提速数据加载过程 - 3. 热点数据主从同时预热 +### ACL 指令 -- 实施: +Redis ACL 是 Access Control List(访问控制列表)的缩写,该功能允许根据可以执行的命令和可以访问的键来限制某些连接 - 4. 使用脚本程序固定触发数据预热过程 +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-ACL指令.png) + +* acl cat:查看添加权限指令类别 +* acl whoami:查看当前用户 + +* acl setuser username on >password ~cached:* +get:设置有用户名、密码、ACL 权限(只能 get) - 5. 如果条件允许,使用了 CDN(内容分发网络),效果会更好 -总的来说:缓存预热就是系统启动前,提前将相关的缓存数据直接加载到缓存系统。避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题,用户直接查询事先被预热的缓存数据! @@ -15885,23 +15784,39 @@ Read-Through Pattern 也存在首次不命中的问题,采用缓存预热解 -#### 缓存雪崩 +### 监视器 -场景:数据库服务器崩溃,一连串的问题会随之而来 +MONITOR 命令,可以将客户端变为一个监视器,实时地接收并打印出服务器当前处理的命令请求的相关信息 -问题排查:在一个较短的时间内,**缓存中较多的 key 集中过期**,此周期内请求访问过期的数据 Redis 未命中,Redis 向数据库获取数据,数据库同时收到大量的请求无法及时处理。 +```c +// 实现原理 +def MONITOR(): + // 打开客户端的监视器标志 + client.flags |= REDIS_MONITOR + + // 将客户端添加到服务器状态的 redisServer.monitors链表的末尾 + server.monitors.append(client) + // 向客户端返回 ok + send_reply("OK") +``` -解决方案: +服务器每次处理命令请求都会调用 replicationFeedMonitors 函数,函数将被处理的命令请求的相关信息**发送给各个监视器** -1. 加锁,慎用 -2. 设置热点数据永远不过期,如果缓存数据库是分布式部署,将热点数据均匀分布在不同搞得缓存数据库中 -3. 缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生 -4. 构建**多级缓存**架构,Nginx 缓存 + Redis 缓存 + ehcache 缓存 -5. 灾难预警机制,监控 Redis 服务器性能指标,CPU 使用率、内存容量、平均响应时间、线程数 -6. 限流、降级:短时间范围内牺牲一些客户体验,限制一部分请求访问,降低应用服务器压力,待业务低速运转后再逐步放开访问 + + +```sh +redis> MONITOR +OK +1378822099.421623 [0 127.0.0.1:56604] "PING" +1378822105.089572 [0 127.0.0.1:56604] "SET" "msg" "hello world" +1378822109.036925 [0 127.0.0.1:56604] "SET" "number" "123" +1378822140.649496 (0 127.0.0.1:56604] "SADD" "fruits" "Apple" "Banana" "Cherry" +1378822154.117160 [0 127.0.0.1:56604] "EXPIRE" "msg" "10086" +1378822257.329412 [0 127.0.0.1:56604] "KEYS" "*" +1378822258.690131 [0 127.0.0.1:56604] "DBSIZE" +``` -总的来说:缓存雪崩就是瞬间过期数据量太大,导致对数据库服务器造成压力。如能够有效避免过期时间集中,可以有效解决雪崩现象的出现(约 40%),配合其他策略一起使用,并监控服务器的运行数据,根据运行记录做快速调整。 @@ -15909,35 +15824,45 @@ Read-Through Pattern 也存在首次不命中的问题,采用缓存预热解 -#### 缓存击穿 +### 批处理 -缓存击穿也叫热点 Key 问题 +Redis 的管道 Pipeline 机制可以一次处理多条指令 -场景:系统平稳运行过程中,数据库连接量瞬间激增,Redis 服务器无大量 key 过期,Redis 内存平稳无波动,Redis 服务器 CPU 正常,但是数据库崩溃 +* Pipeline 中的多条命令非原子性,因为在向管道内添加命令时,其他客户端的发送的命令仍然在执行 +* 原生批命令(MSET 等)是服务端实现,而 Pipeline 需要服务端与客户端共同完成 -问题排查: +使用 Pipeline 封装的命令数量不能太多,数据量过大会增加客户端的等待时间,造成网络阻塞,Jedis 中的 Pipeline 使用方式: -1. **Redis 中某个 key 过期,该 key 访问量巨大** - -2. 多个数据请求从服务器直接压到 Redis 后,均未命中 - -3. Redis 在短时间内发起了大量对数据库中同一数据的访问 - -简而言之两点:单个 key 高热数据,key 过期 +```java +// 创建管道 +Pipeline pipeline = jedis.pipelined(); +for (int i = 1; i <= 100000; i++) { + // 放入命令到管道 + pipeline.set("key_" + i, "value_" + i); + if (i % 1000 == 0) { + // 每放入1000条命令,批量执行 + pipeline.sync(); + } +} +``` -解决方案: +集群下模式下,批处理命令的多个 key 必须落在一个插槽中,否则就会导致执行失败,N 条批处理命令的优化方式: -1. 预先设定:以电商为例,每个商家根据店铺等级,指定若干款主打商品,在购物节期间,加大此类信息 key 的过期时长 注意:购物节不仅仅指当天,以及后续若干天,访问峰值呈现逐渐降低的趋势 +* 串行命令:for 循环遍历,依次执行每个命令 +* 串行 slot:在客户端计算每个 key 的 slot,将 slot 一致的分为一组,每组都利用 Pipeline 批处理,串行执行各组命令 +* 并行 slot:在客户端计算每个 key 的 slot,将 slot 一致的分为一组,每组都利用 Pipeline 批处理,**并行执行各组命令** +* hash_tag:将所有 key 设置相同的 hash_tag,则所有 key 的 slot 一定相同 -2. 现场调整:监控访问量,对自然流量激增的数据**延长过期时间或设置为永久性 key** +| | 耗时 | 优点 | 缺点 | +| --------- | ------------------------------------------------- | -------------------- | -------------------- | +| 串行命令 | N 次网络耗时 + N 次命令耗时 | 实现简单 | 耗时久 | +| 串行 slot | m 次网络耗时 + N 次命令耗时,m = key 的 slot 个数 | 耗时较短 | 实现稍复杂 | +| 并行 slot | 1 次网络耗时 + N 次命令耗时 | 耗时非常短 | 实现复杂 | +| hash_tag | 1 次网络耗时 + N 次命令耗时 | 耗时非常短、实现简单 | 容易出现**数据倾斜** | -3. 后台刷新数据:启动定时任务,高峰期来临之前,刷新数据有效期,确保不丢失 -4. **二级缓存**:设置不同的失效时间,保障不会被同时淘汰就行 -5. 加锁:分布式锁,防止被击穿,但是要注意也是性能瓶颈,慎重 -总的来说:缓存击穿就是单个高热数据过期的瞬间,数据访问量较大,未命中 Redis 后,发起了大量对同一数据的数据库访问,导致对数据库服务器造成压力。应对策略应该在业务数据分析与预防方面进行,配合运行监控测试与即时调整策略,毕竟单个 key 的过期监控难度较高,配合雪崩处理策略即可 @@ -15945,250 +15870,150 @@ Read-Through Pattern 也存在首次不命中的问题,采用缓存预热解 -#### 缓存穿透 -场景:系统平稳运行过程中,应用服务器流量随时间增量较大,Redis 服务器命中率随时间逐步降低,Redis 内存平稳,内存无压力,Redis 服务器 CPU 占用激增,数据库服务器压力激增,数据库崩溃 -问题排查: +## 解决方案 -1. Redis 中大面积出现未命中 +### 缓存方案 -2. 出现非正常 URL 访问 +#### 缓存模式 -问题分析: +##### 旁路缓存 -- 访问了不存在的数据,跳过了 Redis 缓存,数据库页查询不到对应数据 -- Redis 获取到 null 数据未进行持久化,直接返回 -- 出现黑客攻击服务器 - -解决方案: - -1. 缓存 null:对查询结果为 null 的数据进行缓存,设定短时限,例如 30-60 秒,最高 5 分钟 - -2. 白名单策略:提前预热各种分类**数据 id 对应的 bitmaps**,id 作为 bitmaps 的 offset,相当于设置了数据白名单。当加载正常数据时放行,加载异常数据时直接拦截(效率偏低),也可以使用布隆过滤器(有关布隆过滤器的命中问题对当前状况可以忽略) - -3. 实时监控:实时监控 Redis 命中率(业务正常范围时,通常会有一个波动值)与 null 数据的占比 - - * 非活动时段波动:通常检测 3-5 倍,超过 5 倍纳入重点排查对象 - * 活动时段波动:通常检测10-50 倍,超过 50 倍纳入重点排查对象 - - 根据倍数不同,启动不同的排查流程。然后使用黑名单进行防控 - -4. key 加密:临时启动防灾业务 key,对 key 进行业务层传输加密服务,设定校验程序,过来的 key 校验;例如每天随机分配 60 个加密串,挑选 2 到 3 个,混淆到页面数据 id 中,发现访问 key 不满足规则,驳回数据访问 - -总的来说:缓存击穿是指访问了不存在的数据,跳过了合法数据的 Redis 数据缓存阶段,**每次访问数据库**,导致对数据库服务器造成压力。通常此类数据的出现量是一个较低的值,当出现此类情况以毒攻毒,并及时报警。无论是黑名单还是白名单,都是对整体系统的压力,警报解除后尽快移除 - - - -参考视频:https://www.bilibili.com/video/BV15y4y1r7X3 - - - - - -*** - - - -### 性能指标 - -Redis 中的监控指标如下: - -* 性能指标:Performance - - 响应请求的平均时间: - - ```sh - latency - ``` +缓存本质:弥补 CPU 的高算力和 IO 的慢读写之间巨大的鸿沟 - 平均每秒处理请求总数: +旁路缓存模式 Cache Aside Pattern 是平时使用比较多的一个缓存读写模式,比较适合读请求比较多的场景 - ```sh - instantaneous_ops_per_sec - ``` +Cache Aside Pattern 中服务端需要同时维系 DB 和 cache,并且是以 DB 的结果为准 - 缓存查询命中率(通过查询总次数与查询得到非nil数据总次数计算而来): +* 写操作:先更新 DB,然后直接删除 cache +* 读操作:从 cache 中读取数据,读取到就直接返回;读取不到就从 DB 中读取数据返回,并放到 cache - ```sh - hit_rate(calculated) - ``` +时序导致的不一致问题: -* 内存指标:Memory +* 在写数据的过程中,不能先删除 cache 再更新 DB,因为会造成缓存的不一致。比如请求 1 先写数据 A,请求 2 随后读数据 A,当请求 1 删除 cache 后,请求 2 直接读取了 DB,此时请求 1 还没写入 DB(延迟双删) - 当前内存使用量: +* 在写数据的过程中,先更新 DB 再删除 cache 也会出现问题,但是概率很小,因为缓存的写入速度非常快 - ```sh - used_memory - ``` +旁路缓存的缺点: - 内存碎片率(关系到是否进行碎片整理): +* 首次请求数据一定不在 cache 的问题,一般采用缓存预热的方法,将热点数据可以提前放入 cache 中 +* 写操作比较频繁的话导致 cache 中的数据会被频繁被删除,影响缓存命中率 - ```sh - mem_fragmentation_ratio - ``` +**删除缓存而不是更新缓存的原因**:每次更新数据库都更新缓存,造成无效写操作较多(懒惰加载,需要的时候再放入缓存) - 为避免内存溢出删除的key的总数量: - ```sh - evicted_keys - ``` - 基于阻塞操作(BLPOP等)影响的客户端数量: - ```sh - blocked_clients - ``` -* 基本活动指标:Basic_activity +**** - 当前客户端连接总数: - ```sh - connected_clients - ``` - 当前连接 slave 总数: +##### 读写穿透 - ```sh - connected_slaves - ``` +读写穿透模式 Read/Write Through Pattern:服务端把 cache 视为主要数据存储,从中读取数据并将数据写入其中,cache 负责将此数据同步写入 DB,从而减轻了应用程序的职责 - 最后一次主从信息交换距现在的秒: +* 写操作:先查 cache,cache 中不存在,直接更新 DB;cache 中存在则先更新 cache,然后 cache 服务更新 DB(同步更新 cache 和 DB) - ```sh - master_last_io_seconds_ago - ``` +* 读操作:从 cache 中读取数据,读取到就直接返回 ;读取不到先从 DB 加载,写入到 cache 后返回响应 - key 的总数: + Read-Through Pattern 实际只是在 Cache-Aside Pattern 之上进行了封装。在 Cache-Aside Pattern 下,发生读请求的时候,如果 cache 中不存在对应的数据,是由客户端负责把数据写入 cache,而 Read Through Pattern 则是 cache 服务自己来写入缓存的,对客户端是透明的 - ```sh - keyspace - ``` +Read-Through Pattern 也存在首次不命中的问题,采用缓存预热解决 -* 持久性指标:Persistence - 当前服务器其最后一次 RDB 持久化的时间: - ```sh - rdb_last_save_time - ``` +*** - 当前服务器最后一次 RDB 持久化后数据变化总量: - ```sh - rdb_changes_since_last_save - ``` -* 错误指标:Error +##### 异步缓存 - 被拒绝连接的客户端总数(基于达到最大连接值的因素): +异步缓存写入 Write Behind Pattern 由 cache 服务来负责 cache 和 DB 的读写,对比读写穿透不同的是 Write Behind Caching 是只更新缓存,不直接更新 DB,改为**异步批量**的方式来更新 DB,可以减小写的成本 - ```sh - rejected_connections - ``` +缺点:这种模式对数据一致性没有高要求,可能出现 cache 还没异步更新 DB,服务就挂掉了 - key未命中的总次数: +应用: - ```sh - keyspace_misses - ``` +* DB 的写性能非常高,适合一些数据经常变化又对数据一致性要求不高的场景,比如浏览量、点赞量 - 主从断开的秒数: +* MySQL 的 InnoDB Buffer Pool 机制用到了这种策略 - ```sh - master_link_down_since_seconds - ``` -要对 Redis 的相关指标进行监控,我们可以采用一些用具: -- CloudInsight Redis -- Prometheus -- Redis-stat -- Redis-faina -- RedisLive -- zabbix +**** -命令工具: -* benchmark - 测试当前服务器的并发性能: +#### 缓存一致 - ```sh - redis-benchmark [-h ] [-p ] [-c ] [-n [-k ] - ``` +使用缓存代表不需要强一致性,只需要最终一致性 - 范例:100 个连接,5000 次请求对应的性能 +缓存不一致的方法: - ```sh - redis-benchmark -c 100 -n 5000 - ``` +* 数据库和缓存数据强一致场景: - ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-redis-benchmark指令.png) + * 同步双写:更新 DB 时同样更新 cache,保证在一个事务中,通过加锁来保证更新 cache 时不存在线程安全问题 -* redis-cli + * 延迟双删:先淘汰缓存再写数据库,休眠 1 秒再次淘汰缓存,可以将 1 秒内造成的缓存脏数据再次删除 - monitor:启动服务器调试信息 + * 异步通知: - ```sh - monitor - ``` + * 基于 MQ 的异步通知:对数据的修改后,代码需要发送一条消息到 MQ 中,缓存服务监听 MQ 消息 + * Canal 订阅 MySQL binlog 的变更上报给 Kafka,系统监听 Kafka 消息触发缓存失效,或者直接将变更发送到处理服务,**没有任何代码侵入** - slowlog:慢日志 + 低耦合,可以同时通知多个缓存服务,但是时效性一般,可能存在中间不一致状态 - ```sh - slowlog [operator] #获取慢查询日志 - ``` +* 低一致性场景: - * get :获取慢查询日志信息 - * len :获取慢查询日志条目数 - * reset :重置慢查询日志 + * 更新 DB 的时候同样更新 cache,但是给缓存加一个比较短的过期时间,这样就可以保证即使数据不一致影响也比较小 + * 使用 Redis 自带的内存淘汰机制 - 相关配置: - ```sh - slowlog-log-slower-than 1000 #设置慢查询的时间下线,单位:微妙 - slowlog-max-len 100 #设置慢查询命令对应的日志显示长度,单位:命令数 - ``` +*** -**** +#### 缓存问题 +##### 缓存预热 +场景:宕机,服务器启动后迅速宕机 -## 其他操作 +问题排查: -### 发布订阅 +1. 请求数量较高,大量的请求过来之后都需要去从缓存中获取数据,但是缓存中又没有,此时从数据库中查找数据然后将数据再存入缓存,造成了短期内对 redis 的高强度操作从而导致问题 -#### 基本指令 +2. 主从之间数据吞吐量较大,数据同步操作频度较高 -Redis 发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息 +解决方案: -Redis 客户端可以订阅任意数量的频道,每当有客户端向被订阅的频道发送消息(message)时,频道的**所有订阅者都会收到消息** +- 前置准备工作: -![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-发布订阅.png) + 1. 日常例行统计数据访问记录,统计访问频度较高的热点数据 -操作过程: + 2. 利用 LRU 数据删除策略,构建数据留存队列例如:storm 与 kafka 配合 -* 打开一个客户端订阅 channel1:`SUBSCRIBE channel1` +- 准备工作: -* 打开另一个客户端,给 channel1 发布消息 hello:`PUBLISH channel1 hello` + 1. 将统计结果中的数据分类,根据级别,redis 优先加载级别较高的热点数据 -* 第一个客户端可以看到发送的消息 + 2. 利用分布式多服务器同时进行数据读取,提速数据加载过程 - + 3. 热点数据主从同时预热 -客户端还可以通过 PSUBSCRIBE 命令订阅一个或多个模式,每当有其他客户端向某个频道发送消息时,消息不仅会被发送给这个频道的所有订阅者,还会被**发送给所有与这个频道相匹配的模式的订阅者**,比如 `PSUBSCRIBE channel*` 订阅模式,与 channel1 匹配 +- 实施: -注意:发布的消息没有持久化,所以订阅的客户端只能收到订阅后发布的消息 + 4. 使用脚本程序固定触发数据预热过程 + 5. 如果条件允许,使用了 CDN(内容分发网络),效果会更好 +总的来说:缓存预热就是系统启动前,提前将相关的缓存数据直接加载到缓存系统。避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题,用户直接查询事先被预热的缓存数据 @@ -16196,78 +16021,53 @@ Redis 客户端可以订阅任意数量的频道,每当有客户端向被订 -#### 频道操作 - -Redis 将所有频道的订阅关系都保存在服务器状态的 pubsub_channels 字典里,键是某个被订阅的频道,值是一个记录所有订阅这个频道的客户端链表 - -```c -struct redisServer { - // 保存所有频道的订阅关系, - dict *pubsub_channels; -} -``` - -客户端执行 SUBSCRIBE 命令订阅某个或某些频道,服务器会将客户端与频道进行关联: - -* 频道已经存在,直接将客户端添加到链表末尾 -* 频道还未有任何订阅者,在字典中为频道创建一个键值对,再将客户端添加到链表 - -UNSUBSCRIBE 命令用来退订某个频道,服务器将从 pubsub_channels 中解除客户端与被退订频道之间的关联 - - - +##### 缓存雪崩 +场景:数据库服务器崩溃,一连串的问题会随之而来 -**** +问题排查:在一个较短的时间内,**缓存中较多的 key 集中过期**,此周期内请求访问过期的数据 Redis 未命中,Redis 向数据库获取数据,数据库同时收到大量的请求无法及时处理。 +解决方案: +1. 加锁,慎用 +2. 设置热点数据永远不过期,如果缓存数据库是分布式部署,将热点数据均匀分布在不同搞得缓存数据库中 +3. 缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生 +4. 构建**多级缓存**架构,Nginx 缓存 + Redis 缓存 + ehcache 缓存 +5. 灾难预警机制,监控 Redis 服务器性能指标,CPU 使用率、内存容量、平均响应时间、线程数 +6. **限流、降级**:短时间范围内牺牲一些客户体验,限制一部分请求访问,降低应用服务器压力,待业务低速运转后再逐步放开访问 -#### 模式操作 -Redis 服务器将所有模式的订阅关系都保存在服务器状态的 pubsub_patterns 属性里 +总的来说:缓存雪崩就是瞬间过期数据量太大,导致对数据库服务器造成压力。如能够有效避免过期时间集中,可以有效解决雪崩现象的出现(约 40%),配合其他策略一起使用,并监控服务器的运行数据,根据运行记录做快速调整。 -```c -struct redisServer { - // 保存所有模式订阅关系,链表中每个节点是一个 pubsubPattern - list *pubsub_patterns; -} -typedef struct pubsubPattern { - // 订阅的客户端 - redisClient *client; - // 被订阅的模式,比如 channel* - robj *pattern; -} -``` -客户端执行 PSUBSCRIBE 命令订阅某个模式,服务器会新建一个 pubsubPattern 结构并赋值,放入 pubsub_patterns 链表结尾 +*** -模式的退订命令 PUNSUBSCRIBE 是订阅命令的反操作,服务器在 pubsub_patterns 链表中查找并删除对应的结构 +##### 缓存击穿 +缓存击穿也叫热点 Key 问题 +1. **Redis 中某个 key 过期,该 key 访问量巨大** -*** +2. 多个数据请求从服务器直接压到 Redis 后,均未命中 +3. Redis 在短时间内发起了大量对数据库中同一数据的访问 +解决方案: -#### 发送消息 +1. 预先设定:以电商为例,每个商家根据店铺等级,指定若干款主打商品,在购物节期间,加大此类信息 key 的过期时长 注意:购物节不仅仅指当天,以及后续若干天,访问峰值呈现逐渐降低的趋势 -Redis 客户端执行 `PUBLISH ` 命令将消息 message发送给频道 channel,服务器会执行: +2. 现场调整:监控访问量,对自然流量激增的数据**延长过期时间或设置为永久性 key** -* 在 pubsub_channels 字典里找到频道 channel 的订阅者名单,将消息 message 发送给所有订阅者 -* 遍历整个 pubsub_patterns 链表,查找与 channel 频道相**匹配的模式**,并将消息发送给所有订阅了这些模式的客户端 +3. 后台刷新数据:启动定时任务,高峰期来临之前,刷新数据有效期,确保不丢失 -```c -// 如果频道和模式相匹配 -if match(channel, pubsubPattern.pattern) { - // 将消息发送给订阅该模式的客户端 - send_message(pubsubPattern.client, message); -} -``` +4. **二级缓存**:设置不同的失效时间,保障不会被同时淘汰就行 +5. 加锁:分布式锁,防止被击穿,但是要注意也是性能瓶颈,慎重 +总的来说:缓存击穿就是单个高热数据过期的瞬间,数据访问量较大,未命中 Redis 后,发起了大量对同一数据的数据库访问,导致对数据库服务器造成压力。应对策略应该在业务数据分析与预防方面进行,配合运行监控测试与即时调整策略,毕竟单个 key 的过期监控难度较高,配合雪崩处理策略即可 @@ -16275,37 +16075,42 @@ if match(channel, pubsubPattern.pattern) { -#### 查看信息 +##### 缓存穿透 -PUBSUB 命令用来查看频道或者模式的相关信息 +场景:系统平稳运行过程中,应用服务器流量随时间增量较大,Redis 服务器命中率随时间逐步降低,Redis 内存平稳,内存无压力,Redis 服务器 CPU 占用激增,数据库服务器压力激增,数据库崩溃 -`PUBSUB CHANNELS [pattern]` 返回服务器当前被订阅的频道,其中 pattern 参数是可选的 +问题排查: -* 如果不给定 pattern 参数,那么命令返回服务器当前被订阅的所有频道 -* 如果给定 pattern 参数,那么命令返回服务器当前被订阅的频道中与 pattern 模式相匹配的频道 +1. Redis 中大面积出现未命中 -`PUBSUB NUMSUB [channel-1 channel-2 ... channel-n]` 命令接受任意多个频道作为输入参数,并返回这些频道的订阅者数量 +2. 出现非正常 URL 访问 -`PUBSUB NUMPAT` 命令用于返回服务器当前被订阅模式的数量 +问题分析: +- 访问了不存在的数据,跳过了 Redis 缓存,数据库页查询不到对应数据 +- Redis 获取到 null 数据未进行持久化,直接返回 +- 出现黑客攻击服务器 +解决方案: +1. 缓存 null:对查询结果为 null 的数据进行缓存,设定短时限,例如 30-60 秒,最高 5 分钟 +2. 白名单策略:提前预热各种分类**数据 id 对应的 bitmaps**,id 作为 bitmaps 的 offset,相当于设置了数据白名单。当加载正常数据时放行,加载异常数据时直接拦截(效率偏低),也可以使用布隆过滤器(有关布隆过滤器的命中问题对当前状况可以忽略) -**** +3. 实时监控:实时监控 Redis 命中率(业务正常范围时,通常会有一个波动值)与 null 数据的占比 + * 非活动时段波动:通常检测 3-5 倍,超过 5 倍纳入重点排查对象 + * 活动时段波动:通常检测10-50 倍,超过 50 倍纳入重点排查对象 + 根据倍数不同,启动不同的排查流程。然后使用黑名单进行防控 -### ACL 指令 +4. key 加密:临时启动防灾业务 key,对 key 进行业务层传输加密服务,设定校验程序,过来的 key 校验;例如每天随机分配 60 个加密串,挑选 2 到 3 个,混淆到页面数据 id 中,发现访问 key 不满足规则,驳回数据访问 -Redis ACL 是 Access Control List(访问控制列表)的缩写,该功能允许根据可以执行的命令和可以访问的键来限制某些连接 +总的来说:缓存击穿是指访问了不存在的数据,跳过了合法数据的 Redis 数据缓存阶段,**每次访问数据库**,导致对数据库服务器造成压力。通常此类数据的出现量是一个较低的值,当出现此类情况以毒攻毒,并及时报警。无论是黑名单还是白名单,都是对整体系统的压力,警报解除后尽快移除 -![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-ACL指令.png) -* acl cat:查看添加权限指令类别 -* acl whoami:查看当前用户 -* acl setuser username on >password ~cached:* +get:设置有用户名、密码、ACL 权限(只能 get) +参考视频:https://www.bilibili.com/video/BV15y4y1r7X3 @@ -16315,88 +16120,57 @@ Redis ACL 是 Access Control List(访问控制列表)的缩写,该功能 -### 监视器 - -MONITOR 命令,可以将客户端变为一个监视器,实时地接收并打印出服务器当前处理的命令请求的相关信息 - -```c -// 实现原理 -def MONITOR(): - // 打开客户端的监视器标志 - client.flags |= REDIS_MONITOR - - // 将客户端添加到服务器状态的 redisServer.monitors链表的末尾 - server.monitors.append(client) - // 向客户端返回 ok - send_reply("OK") -``` - -服务器每次处理命令请求都会调用 replicationFeedMonitors 函数,函数将被处理的命令请求的相关信息**发送给各个监视器** - -```sh -redis> MONITOR -OK -1378822099.421623 [0 127.0.0.1:56604] "PING" -1378822105.089572 [0 127.0.0.1:56604] "SET" "msg" "hello world" -1378822109.036925 [0 127.0.0.1:56604] "SET" "number" "123" -1378822140.649496 (0 127.0.0.1:56604] "SADD" "fruits" "Apple" "Banana" "Cherry" -1378822154.117160 [0 127.0.0.1:56604] "EXPIRE" "msg" "10086" -1378822257.329412 [0 127.0.0.1:56604] "KEYS" "*" -1378822258.690131 [0 127.0.0.1:56604] "DBSIZE" -``` +### Key 设计 +大 Key:通常以 Key 的大小和 Key 中成员的数量来综合判定,引发的问题: +- 客户端执行命令的时长变慢 +- Redis 内存达到 maxmemory 定义的上限引发操作阻塞或重要的 Key 被逐出,甚至引发内存溢出(OOM) +- 集群架构下,某个数据分片的内存使用率远超其他数据分片,使**数据分片的内存资源不均衡** +- 对大 Key 执行读请求,会使 Redis 实例的带宽使用率被占满,导致自身服务变慢,同时易波及相关的服务 +- 对大 Key 执行删除操作,会造成主库较长时间的阻塞,进而可能引发同步中断或主从切换 +热 Key:通常以其接收到的 Key 被请求频率来判定,引发的问题: +- 占用大量的 CPU 资源,影响其他请求并导致整体性能降低 +- 分布式集群架构下,产生**访问倾斜**,即某个数据分片被大量访问,而其他数据分片处于空闲状态,可能引起该数据分片的连接数被耗尽,新的连接建立请求被拒绝等问题 +- 在抢购或秒杀场景下,可能因商品对应库存 Key 的请求量过大,超出 Redis 处理能力造成超卖 +- 热 Key 的请求压力数量超出 Redis 的承受能力易造成缓存击穿,即大量请求将被直接指向后端的存储层,导致存储访问量激增甚至宕机,从而影响其他业务 -*** +热 Key 分类两种,治理方式如下: +* 一种是单一数据,比如秒杀场景,假设总量 10000 可以拆为多个 Key 进行访问,每次对请求进行路由到不同的 Key 访问,保证最终一致性,但是会出现访问不同 Key 产生的剩余量是不同的,这时可以通过前端进行 Mock 假数据 +* 一种是多数据集合,比如进行 ID 过滤,这时可以添加本地 LRU 缓存,减少对热 Key 的访问 -### 批处理 -Redis 的管道 Pipeline 机制可以一次处理多条指令 -* Pipeline 中的多条命令非原子性,因为在向管道内添加命令时,其他客户端的发送的命令仍然在执行 -* 原生批命令(mset 等)是服务端实现,而 pipeline 需要服务端与客户端共同完成 -使用 Pipeline 封装的命令数量不能太多,数据量过大会增加客户端的等待时间,造成网络阻塞,Jedis 中的 Pipeline 使用方式: +参考文档:https://help.aliyun.com/document_detail/353223.html -```java -// 创建管道 -Pipeline pipeline = jedis.pipelined(); -for (int i = 1; i <= 100000; i++) { - // 放入命令到管道 - pipeline.set("key_" + i, "value_" + i); - if (i % 1000 == 0) { - // 每放入1000条命令,批量执行 - pipeline.sync(); - } -} -``` -集群下模式下,批处理命令的多个 key 必须落在一个插槽中,否则就会导致执行失败,N 条批处理命令的优化方式: -* 串行命令:for 循环遍历,依次执行每个命令 -* 串行 slot:在客户端计算每个 key 的 slot,将 slot 一致的分为一组,每组都利用 Pipeline 批处理,串行执行各组命令 -* 并行 slot:在客户端计算每个 key 的 slot,将 slot 一致的分为一组,每组都利用 Pipeline 批处理,**并行执行各组命令** -* hash_tag:将所有 key 设置相同的 hash_tag,则所有 key 的 slot 一定相同 +*** -| | 耗时 | 优点 | 缺点 | -| --------- | ------------------------------------------------- | -------------------- | -------------------- | -| 串行命令 | N 次网络耗时 + N 次命令耗时 | 实现简单 | 耗时久 | -| 串行 slot | m 次网络耗时 + N 次命令耗时,m = key 的 slot 个数 | 耗时较短 | 实现稍复杂 | -| 并行 slot | 1 次网络耗时 + N 次命令耗时 | 耗时非常短 | 实现复杂 | -| hash_tag | 1 次网络耗时 + N 次命令耗时 | 耗时非常短、实现简单 | 容易出现**数据倾斜** | +### 慢查询 + +确认服务和 Redis 之间的链路是否正常,排除网络原因后进行 Redis 的排查: + +* 使用复杂度过高的命令 +* 操作大 key,分配内存和释放内存会比较耗时 +* key 集中过期,导致定时任务需要更长的时间去清理 +* 实例内存达到上限,每次写入新的数据之前,Redis 必须先从实例中踢出一部分数据 +参考文章:https://www.cnblogs.com/traditional/p/15633919.html(非常好) + @@ -16583,227 +16357,6 @@ public class JDBCDemo01 { -*** - - - -### 工具类 - -* 配置文件(在 src 下创建 config.properties) - - ```properties - driverClass=com.mysql.jdbc.Driver - url=jdbc:mysql://192.168.2.184:3306/db14 - username=root - password=123456 - ``` - -* 工具类 - - ```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); - } - } - ``` - - - - -**** - - - -### 数据封装 - -从数据库读取数据并封装成 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'); - ``` - -- 操作数据库 - - ```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; - } - } - ``` - - - *** @@ -16882,292 +16435,15 @@ PreparedStatement:预编译 sql 语句的执行者对象,继承 `PreparedSta -**** - - - -#### 自定义池 - -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(); - - System.out.println("使用之前连接池数量:" + dataSource.getSize());//10 - - //获取数据库连接对象 - Connection con = dataSource.getConnection(); - System.out.println(con.getClass());// JDBC4Connection - - //查询学生表全部信息 - String sql = "SELECT * FROM student"; - PreparedStatement pst = con.prepareStatement(sql); - ResultSet rs = pst.executeQuery(); - - 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(); - - System.out.println("使用之后连接池数量:" + dataSource.getSize());//9 - } -} -``` - -结论:释放资源并没有把连接归还给连接池 - - - -*** - - - -#### 归还连接 - -归还数据库连接的方式:继承方式、装饰者设计者模式、适配器设计模式、动态代理方式 - -##### 继承方式 - -继承(无法解决) - -- 通过打印连接对象,发现 DriverManager 获取的连接实现类是 JDBC4Connection -- 自定义一个类,继承 JDBC4Connection 这个类,重写 close() 方法 -- 查看 JDBC 工具类获取连接的方法发现:虽然自定义了一个子类,完成了归还连接的操作。但是 DriverManager 获取的还是 JDBC4Connection 这个对象,并不是我们的子类对象 - -代码实现 - -* 自定义继承连接类 - - ```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); - } - } - ``` - -* 自定义连接池类 - - ```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 实现类相同的功能 - -在实现类对每个获取的 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(); - } - .......... - } - ``` - -* 自定义连接池类 - ```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("连接数量已用尽"); - } - } - ``` - - *** -##### 适配器 - -使用适配器设计模式改进,提供一个适配器类,实现 Connection 接口,将所有功能进行实现(除了 close 方法),自定义连接类只需要继承这个适配器类,重写需要改进的 close() 方法即可。 - -特点:自定义连接类中很简洁。剩余所有的方法抽取到了适配器类中,但是适配器这个类还是我们自己编写。 -* 适配器类 - ```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(); - } - } - ``` - -* 自定义连接类 - - ```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); - } - } - ``` - -* 自定义连接池类 - - ```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("连接数量已用尽"); - } - } - ``` - - - -*** - - - -##### 动态代理 +#### 归还连接 使用动态代理的方式来改进 @@ -17352,101 +16628,6 @@ Druid 连接池: -*** - - - -#### 工具类 - -数据库连接池的工具类: - -```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; - } - - //6.释放资源 - 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) { - if(con != null) { - try { - con.close(); - } catch (SQLException e) { - e.printStackTrace(); - } - } - - if(stat != null) { - try { - stat.close(); - } catch (SQLException e) { - e.printStackTrace(); - } - } - } -} - -``` - diff --git a/Frame.md b/Frame.md index 7baad52..f00ae48 100644 --- a/Frame.md +++ b/Frame.md @@ -739,9 +739,13 @@ Maven 的插件用来执行生命周期中的相关事件 ### 继承 -作用:通过继承可以实现在子工程中沿用父工程中的配置 +Maven 中的继承与 Java 中的继承相似,可以实现在子工程中沿用父工程中的配置 -- Maven 中的继承与 Java 中的继承相似,在子工程中配置继承关系 +dependencyManagement 里只是声明依赖,并不实现引入,所以子工程需要显式声明需要用的依赖 + +- 如果子工程中未声明依赖,则不会从父项目继承下来 +- 在子工程中声明该依赖项,并且不指定具体版本,才会从父项目中继承该项,version 和 scope 都继承取自父工程 pom 文件 +- 如果子工程中指定了版本号,那么使用子工程中指定的 jar 版本 制作方式: @@ -1544,9 +1548,9 @@ Reactor 对象通过 select 监控客户端请求事件,收到事件后通过 采用多个 Reactor ,执行流程: -* Reactor 主线程 MainReactor 通过 select 监控建立连接事件,收到事件后通过 Acceptor 接收,处理建立连接事件,处理完成后 MainReactor 会将连接分配给 Reactor 子线程的 SubReactor(有多个)处理 +* Reactor 主线程 MainReactor 通过 select **监控建立连接事件**,收到事件后通过 Acceptor 接收,处理建立连接事件,处理完成后 MainReactor 会将连接分配给 Reactor 子线程的 SubReactor(有多个)处理 -* SubReactor 将连接加入连接队列进行监听,并创建一个 Handler 用于处理该连接的事件,当有新的事件发生时,SubReactor 会调用连接对应的 Handler 进行响应 +* SubReactor 将连接加入连接队列进行监听其他事件,并创建一个 Handler 用于处理该连接的事件,当有新的事件发生时,SubReactor 会调用连接对应的 Handler 进行响应 * Handler 通过 read 读取数据后,会分发给 Worker 线程池进行业务处理 @@ -1623,7 +1627,7 @@ Netty 主要基于主从 Reactors 多线程模型做了一定的改进,Netty - 处理 I/O 事件,即 read,write 事件,在对应 NioSocketChannel 处理 - 处理任务队列的任务,即 runAllTasks -6. 每个 Worker NioEventLoop 处理业务时,会使用 pipeline(管道),pipeline 中包含了 channel,即通过 pipeline 可以获取到对应通道,管道中维护了很多的处理器 Handler +6. 每个 Worker NioEventLoop 处理业务时,会使用 Pipeline(管道),Pipeline 中包含了 Channel,即通过 Pipeline 可以获取到对应通道,管道中维护了很多的处理器 Handler @@ -1641,9 +1645,9 @@ Netty 主要基于主从 Reactors 多线程模型做了一定的改进,Netty 开发简单的服务器端和客户端,基本介绍: -* channel 理解为数据的通道,把 msg 理解为流动的数据,最开始输入是 ByteBuf,但经过 pipeline 的加工,会变成其它类型对象,最后输出又变成 ByteBuf -* handler 理解为数据的处理工序,pipeline 负责发布事件传播给每个 handler,handler 对自己感兴趣的事件进行处理(重写了相应事件处理方法),分 Inbound 和 Outbound 两类 -* eventLoop 理解为处理数据的执行者,既可以执行 IO 操作,也可以进行任务处理。每个执行者有任务队列,队列里可以堆放多个 channel 的待处理任务,任务分为普通任务、定时任务。按照 pipeline 顺序,依次按照 handler 的规划(代码)处理数据 +* Channel 理解为数据的通道,把 msg 理解为流动的数据,最开始输入是 ByteBuf,但经过 Pipeline 的加工,会变成其它类型对象,最后输出又变成 ByteBuf +* Handler 理解为数据的处理工序,Pipeline 负责发布事件传播给每个 Handler,Handler 对自己感兴趣的事件进行处理(重写了相应事件处理方法),分 Inbound 和 Outbound 两类 +* EventLoop 理解为处理数据的执行者,既可以执行 IO 操作,也可以进行任务处理。每个执行者有任务队列,队列里可以堆放多个 Channel 的待处理任务,任务分为普通任务、定时任务。按照 Pipeline 顺序,依次按照 Handler 的规划(代码)处理数据 代码实现: @@ -1706,7 +1710,7 @@ Netty 主要基于主从 Reactors 多线程模型做了一定的改进,Netty new Bootstrap() // 2. 添加 EventLoop .group(new NioEventLoopGroup()) - //.option() //给 SocketChannel 配置参数 + //.option(),给 SocketChannel 配置参数 // 3. 选择客户端 channel 实现 .channel(NioSocketChannel.class) // 4. 添加处理器 @@ -1793,7 +1797,6 @@ public class EventLoopServer { public void channelRead(ChannelHandlerContext ctx, Object msg) { ByteBuf buf = (ByteBuf) msg; log.debug(buf.toString(Charset.defaultCharset())); - } }); } @@ -1885,8 +1888,13 @@ public class ChannelClient { @Override // nio 线程连接建立好以后,回调该方法 public void operationComplete(ChannelFuture future) throws Exception { - Channel channel = future.channel(); - channel.writeAndFlush("hello, world"); + if (future.isSuccess()) { + Channel channel = future.channel(); + channel.writeAndFlush("hello, world"); + } else { + // 建立失败,需要关闭 + future.channel().close(); + } } }); } @@ -1908,15 +1916,7 @@ public class CloseFutureClient { public static void main(String[] args) throws InterruptedException { NioEventLoopGroup group = new NioEventLoopGroup(); ChannelFuture channelFuture = new Bootstrap() - .group(group) - .channel(NioSocketChannel.class) - .handler(new ChannelInitializer() { - @Override - protected void initChannel(NioSocketChannel ch) throws Exception { - ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG)); - ch.pipeline().addLast(new StringEncoder()); - } - }) + // .... .connect(new InetSocketAddress("127.0.0.1", 8080)); Channel channel = channelFuture.sync().channel(); // 发送数据 @@ -2065,7 +2065,7 @@ public static void main(String[] args) { protected void initChannel(NioSocketChannel ch) throws Exception { // 1. 通过 channel 拿到 pipeline ChannelPipeline pipeline = ch.pipeline(); - // 2. 添加处理器 head -> h1 -> h2 -> h3 -> h4 -> h5 -> h6 -> tail + // 2. 添加处理器 head -> h1 -> h2 -> h3 -> h4 -> tail pipeline.addLast("h1", new ChannelInboundHandlerAdapter() { @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { @@ -2219,7 +2219,7 @@ ByteBuf 由四部分组成,最开始读写指针(**双指针**)都在 0 * 写入几位写指针后移几位,指向可以写入的位置 * 网络传输,默认习惯是 Big Endian -扩容:写入数据时,容量不够了(初始容量是 10),这时会引发扩容 +扩容:写入数据时,容量不够了(初始容量是 10),这时会引发**扩容** * 如果写入后数据大小未超过 512,则选择下一个 16 的整数倍,例如写入后大小为 12 ,则扩容后 capacity 是 16 * 如果写入后数据大小超过 512,则选择下一个 2^n,例如写入后大小为 513,则扩容后 capacity 是 2^10 = 1024(2^9=512 不够) @@ -2264,16 +2264,14 @@ try { Pipeline 的存在,需要将 ByteBuf 传递给下一个 ChannelHandler,如果在 finally 中 release 了,就失去了传递性,处理规则: -* 创建 ByteBuf 放入 pipeline +* 创建 ByteBuf 放入 Pipeline * 入站 ByteBuf 处理原则 - * 对原始 ByteBuf 不做处理,调用 ctx.fireChannelRead(msg) 向后传递,这时无须 release + * 对原始 ByteBuf 不做处理,调用 ctx.fireChannelRead(msg) 向后传递,这时无须 release,反之不传递需要 * 将原始 ByteBuf 转换为其它类型的 Java 对象,这时 ByteBuf 就没用了,此时必须 release - * 如果不调用 ctx.fireChannelRead(msg) 向后传递,那么也必须 release - * 如果出现异常,ByteBuf 没有成功传递到下一个 ChannelHandler,必须 release * 假设消息一直向后传,那么 TailContext 会负责释放未处理消息(原始的 ByteBuf) @@ -2295,7 +2293,7 @@ Pipeline 的存在,需要将 ByteBuf 传递给下一个 ChannelHandler,如 return false; } ``` - + * 出站 ByteBuf 处理原则 * 出站消息最终都会转为 ByteBuf 输出,一直向前传,由 HeadContext flush 后 release @@ -2702,7 +2700,7 @@ public class HttpDemo { // 只针对某一种类型的请求处理,此处针对 HttpRequest ch.pipeline().addLast(new SimpleChannelInboundHandler() { @Override - protected void channelRead0(ChannelHandlerContext ctx, HttpRequest msg) throws Exception { + protected void channelRead0(ChannelHandlerContext ctx, HttpRequest msg) { // 获取请求 log.debug(msg.uri()); @@ -3060,7 +3058,7 @@ Codec(编解码器)的组成部分有两个:Decoder(解码器)和 Enco -Protobuf 是 Google 发布的开源项目,全称 Google Protocol Buffers ,是一种轻便高效的结构化数据存储格式,可以用于结构化数据串行化,或者说序列化。很适合做数据存储或 RPC(远程过程调用 remote procedure call)数据交换格式。目前很多公司从 HTTP + Json 转向 TCP + Protobuf ,效率会更高 +Protobuf 是 Google 发布的开源项目,全称 Google Protocol Buffers ,是一种轻便高效的结构化数据存储格式,可以用于结构化数据串行化,或者说序列化。很适合做数据存储或 RPC(远程过程调用 remote procedure call)数据交换格式。目前很多公司从 HTTP + Json 转向 TCP + Protobuf ,效率会更高 Protobuf 是以 message 的方式来管理数据,支持跨平台、跨语言(客户端和服务器端可以是不同的语言编写的),高性能、高可靠性 @@ -3079,7 +3077,7 @@ Protobuf 是以 message 的方式来管理数据,支持跨平台、跨语言 ```protobuf syntax = "proto3"; // 版本 option java_outer_classname = "StudentPOJO"; // 生成的外部类名,同时也是文件名 - // protobuf 使用 message 管理数据 + message Student { // 在 StudentPOJO 外部类种生成一个内部类 Student,是真正发送的 POJO 对象 int32 id = 1; // Student 类中有一个属性:名字为 id 类型为 int32(protobuf类型) ,1表示属性序号,不是值 string name = 2; @@ -3234,7 +3232,7 @@ HTTP 协议是无状态的,浏览器和服务器间的请求响应一次,下 } }); - //启动服务器 + // 启动服务器 ChannelFuture channelFuture = serverBootstrap.bind(8080).sync(); channelFuture.channel().closeFuture().sync(); @@ -3441,6 +3439,8 @@ RCVBUF_ALLOCATOR:属于 SocketChannal 参数 ### 消息队列 +#### 应用场景 + 消息队列是一种先进先出的数据结构,常见的应用场景: * 应用解耦:系统的耦合性越高,容错性就越低 @@ -3449,7 +3449,7 @@ RCVBUF_ALLOCATOR:属于 SocketChannal 参数 ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/RocketMQ-解耦.png) -* 流量削峰:应用系统如果遇到系统请求流量的瞬间猛增,有可能会将系统压垮,使用消息队列可以将大量请求缓存起来,分散到很长一段时间处理,这样可以提高系统的稳定性和用户体验。 +* 流量削峰:应用系统如果遇到系统请求流量的瞬间猛增,有可能会将系统压垮,使用消息队列可以将大量请求缓存起来,分散到很长一段时间处理,这样可以提高系统的稳定性和用户体验 ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/RocketMQ-流量削峰.png) @@ -3465,6 +3465,36 @@ RCVBUF_ALLOCATOR:属于 SocketChannal 参数 +*** + + + +#### 技术选型 + +RocketMQ 对比 Kafka 的优点 + +* 支持 Pull和 Push 两种消息模式 + +- 支持延时消息、死信队列、消息重试、消息回溯、消息跟踪、事务消息等高级特性 +- 对消息可靠性做了改进,**保证消息不丢失并且至少消费一次**,与 Kafka 一样是先写 PageCache 再落盘,并且数据有多副本 +- RocketMQ 存储模型是所有的 Topic 都写到同一个 Commitlog 里,是一个 append only 操作,在海量 Topic 下也能将磁盘的性能发挥到极致,并且保持稳定的写入时延。Kafka 的吞吐非常高(零拷贝、操作系统页缓存、磁盘顺序写),但是在多 Topic 下时延不够稳定(顺序写入特性会被破坏从而引入大量的随机 I/O),不适合实时在线业务场景 +- 经过阿里巴巴多年双 11 验证过、可以支持亿级并发的开源消息队列 + +Kafka 比 RocketMQ 吞吐量高: + +* Kafka 将 Producer 端将多个小消息合并,采用异步批量发送的机制,当发送一条消息时,消息并没有发送到 Broker 而是缓存起来,直接向业务返回成功,当缓存的消息达到一定数量时再批量发送 + +* 减少了网络 I/O,提高了消息发送的性能,但是如果消息发送者宕机,会导致消息丢失,降低了可靠性 +* RocketMQ 缓存过多消息会导致频繁 GC,并且为了保证可靠性没有采用这种方式 + +Topic 的 partition 数量过多时,Kafka 的性能不如 RocketMQ: + +* 两者都使用文件存储,但是 Kafka 是一个分区一个文件,Topic 过多时分区的总量也会增加,过多的文件导致对消息刷盘时出现文件竞争磁盘,造成性能的下降。**一个分区只能被一个消费组中的一个消费线程进行消费**,因此可以同时消费的消费端也比较少 + +* RocketMQ 所有队列都存储在一个文件中,每个队列存储的消息量也比较小,因此多 Topic 的对 RocketMQ 的性能的影响较小 + + + **** @@ -3976,9 +4006,9 @@ Broker 可以配置 messageDelayLevel,该属性是 Broker 的属性,不属 - 1<=level<=maxLevel:消息延迟特定时间,例如 level==1,延迟 1s - level > maxLevel:则 level== maxLevel,例如 level==20,延迟 2h -定时消息会暂存在名为 SCHEDULE_TOPIC_XXXX 的 Topic 中,并根据 delayTimeLevel 存入特定的 queue,队列的标识 `queueId = delayTimeLevel – 1`,即**一个 queue 只存相同延迟的消息**,保证具有相同发送延迟的消息能够顺序消费。Broker 会调度地消费 SCHEDULE_TOPIC_XXXX,将消息写入真实的 Topic +定时消息会暂存在名为 SCHEDULE_TOPIC_XXXX 的 Topic 中,并根据 delayTimeLevel 存入特定的 queue,队列的标识 `queueId = delayTimeLevel – 1`,即**一个 queue 只存相同延迟的消息**,保证具有相同发送延迟的消息能够顺序消费。Broker 会为每个延迟级别提交一个定时任务,调度地消费 SCHEDULE_TOPIC_XXXX,将消息写入真实的 Topic -注意:定时消息在第一次写入和调度写入真实 Topic 时都会计数,因此发送数量、tps 都会变高。 +注意:定时消息在第一次写入和调度写入真实 Topic 时都会计数,因此发送数量、tps 都会变高 @@ -4561,7 +4591,7 @@ RocketMQ 消息的存储是由 ConsumeQueue 和 CommitLog 配合完成 的,Com * ConsumerQueue:消息消费队列,存储消息在 CommitLog 的索引。RocketMQ 消息消费时要遍历 CommitLog 文件,并根据主题 Topic 检索消息,这是非常低效的。引入 ConsumeQueue 作为消费消息的索引,**保存了指定 Topic 下的队列消息在 CommitLog 中的起始物理偏移量 offset**,消息大小 size 和消息 Tag 的 HashCode 值,每个 ConsumeQueue 文件大小约 5.72M * IndexFile:为了消息查询提供了一种通过 Key 或时间区间来查询消息的方法,通过 IndexFile 来查找消息的方法**不影响发送与消费消息的主流程**。IndexFile 的底层存储为在文件系统中实现的 HashMap 结构,故 RocketMQ 的索引文件其底层实现为 **hash 索引** -RocketMQ 采用的是混合型的存储结构,即为 Broker 单个实例下所有的队列共用一个日志数据文件(CommitLog)来存储。混合型存储结构(多个 Topic 的消息实体内容都存储于一个 CommitLog 中)针对 Producer 和 Consumer 分别采用了**数据和索引部分相分离**的存储结构,Producer 发送消息至 Broker 端,然后 Broker 端使用同步或者异步的方式对消息刷盘持久化,保存至 CommitLog 中。只要消息被持久化至磁盘文件 CommitLog 中,Producer 发送的消息就不会丢失,Consumer 也就肯定有机会去消费这条消息 +RocketMQ 采用的是混合型的存储结构,即为 Broker 单个实例下所有的队列共用一个日志数据文件(CommitLog)来存储,多个 Topic 的消息实体内容都存储于一个 CommitLog 中。混合型存储结构针对 Producer 和 Consumer 分别采用了**数据和索引部分相分离**的存储结构,Producer 发送消息至 Broker 端,然后 Broker 端使用同步或者异步的方式对消息刷盘持久化,保存至 CommitLog 中。只要消息被持久化至磁盘文件 CommitLog 中,Producer 发送的消息就不会丢失,Consumer 也就肯定有机会去消费这条消息 服务端支持长轮询模式,当消费者无法拉取到消息后,可以等下一次消息拉取,Broker 允许等待 30s 的时间,只要这段时间内有新消息到达,将直接返回给消费端。RocketMQ 的具体做法是,使用 Broker 端的后台服务线程 ReputMessageService 不停地分发请求并异步构建 ConsumeQueue(逻辑消费队列)和 IndexFile(索引文件)数据 @@ -4599,7 +4629,7 @@ MappedByteBuffer 内存映射的方式**限制**一次只能映射 1.5~2G 的文 页缓存(PageCache)是 OS 对文件的缓存,每一页的大小通常是 4K,用于加速对文件的读写。因为 OS 将一部分的内存用作 PageCache,所以程序对文件进行顺序读写的速度几乎接近于内存的读写速度 -* 对于数据的写入,OS 会先写入至 Cache 内,随后通过异步的方式由 pdflush 内核线程将 Cache 内的数据刷盘至物理磁盘上 +* 对于数据的写入,OS 会先写入至 Cache 内,随后**通过异步的方式由 pdflush 内核线程将 Cache 内的数据刷盘至物理磁盘上** * 对于数据的读取,如果一次读取文件时出现未命中 PageCache 的情况,OS 从物理磁盘上访问读取文件的同时,会顺序对其他相邻块的数据文件进行**预读取**(局部性原理,最大 128K) 在 RocketMQ 中,ConsumeQueue 逻辑消费队列存储的数据较少,并且是顺序读取,在 PageCache 机制的预读取作用下,Consume Queue 文件的读性能几乎接近读内存,即使在有消息堆积情况下也不会影响性能。但是 CommitLog 消息存储的日志数据文件读取内容时会产生较多的随机访问读取,严重影响性能。选择合适的系统 IO 调度算法和固态硬盘,比如设置调度算法为 Deadline,随机读的性能也会有所提升 @@ -4633,8 +4663,6 @@ RocketMQ 采用文件系统的方式,无论同步还是异步刷盘,都使 - - **** @@ -4704,7 +4732,9 @@ BrokerServer 的高可用通过 Master 和 Slave 的配合: * Slave 只负责读,当 Master 不可用,对应的 Slave 仍能保证消息被正常消费 * 配置多组 Master-Slave 组,其他的 Master-Slave 组也会保证消息的正常发送和消费 -* 目前不支持把 Slave 自动转成 Master,需要手动停止 Slave 角色的 Broker,更改配置文件,用新的配置文件启动 Broker +* **目前不支持把 Slave 自动转成 Master**,需要手动停止 Slave 角色的 Broker,更改配置文件,用新的配置文件启动 Broker + + 所以需要配置多个 Master 保证可用性,否则一个 Master 挂了导致整体系统的写操作不可用 生产端的高可用:在创建 Topic 的时候,把 Topic 的**多个 Message Queue 创建在多个 Broker 组**上(相同 Broker 名称,不同 brokerId 的机器),当一个 Broker 组的 Master 不可用后,其他组的 Master 仍然可用,Producer 仍然可以发送消息 @@ -4722,7 +4752,7 @@ BrokerServer 的高可用通过 Master 和 Slave 的配合: 如果一个 Broker 组有 Master 和 Slave,消息需要从 Master 复制到 Slave 上,有同步和异步两种复制方式: -* 同步复制方式:Master 和 Slave 均写成功后才反馈给客户端写成功状态。在同步复制方式下,如果 Master 出故障, Slave 上有全部的备份数据,容易恢复,但是同步复制会增大数据写入延迟,降低系统吞吐量 +* 同步复制方式:Master 和 Slave 均写成功后才反馈给客户端写成功状态(写 Page Cache)。在同步复制方式下,如果 Master 出故障, Slave 上有全部的备份数据,容易恢复,但是同步复制会增大数据写入延迟,降低系统吞吐量 * 异步复制方式:只要 Master 写成功,即可反馈给客户端写成功状态,系统拥有较低的延迟和较高的吞吐量,但是如果 Master 出了故障,有些数据因为没有被写入 Slave,有可能会丢失 @@ -4743,6 +4773,8 @@ RocketMQ 支持消息的高可靠,影响消息可靠性的几种情况: 后两种属于单点故障,且无法恢复,一旦发生,在此单点上的消息全部丢失。RocketMQ 在这两种情况下,通过主从异步复制,可保证 99% 的消息不丢,但是仍然会有极少量的消息可能丢失。通过**同步双写技术**可以完全避免单点,但是会影响性能,适合对消息可靠性要求极高的场合,RocketMQ 从 3.0 版本开始支持同步双写 +一般而言,我们会建议采取同步双写 + 异步刷盘的方式,在消息的可靠性和性能间有一个较好的平衡 + **** @@ -4792,7 +4824,7 @@ LatencyFaultTolerance 机制是实现消息发送高可用的核心关键所在 ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/RocketMQ-平均队列轮流分配.png) -集群模式下,**queue 都是只允许分配只一个实例**,如果多个实例同时消费一个 queue 的消息,由于拉取哪些消息是 Consumer 主动控制的,会导致同一个消息在不同的实例下被消费多次 +集群模式下,**queue 都是只允许分配一个实例**,如果多个实例同时消费一个 queue 的消息,由于拉取哪些消息是 Consumer 主动控制的,会导致同一个消息在不同的实例下被消费多次 通过增加 Consumer 实例去分摊 queue 的消费,可以起到水平扩展的消费能力的作用。而当有实例下线时,会重新触发负载均衡,这时候原来分配到的 queue 将分配到其他实例上继续消费。但是如果 Consumer 实例的数量比 Message Queue 的总数量还多的话,多出来的 Consumer 实例将无法分到 queue,也就无法消费到消息,也就无法起到分摊负载的作用了,所以需要**控制让 queue 的总数量大于等于 Consumer 的数量** @@ -4808,7 +4840,7 @@ LatencyFaultTolerance 机制是实现消息发送高可用的核心关键所在 Consumer 端实现负载均衡的核心类 **RebalanceImpl** -在 Consumer 实例的启动流程中的会启动 MQClientInstance 实例,完成负载均衡服务线程 RebalanceService 的启动(每隔 20s 执行一次),RebalanceService 线程的 run() 方法最终调用的是 RebalanceImpl 类的 rebalanceByTopic() 方法,该方法是实现 Consumer 端负载均衡的核心。rebalanceByTopic() 方法会根据消费者通信类型为广播模式还是集群模式做不同的逻辑处理。这里主要看下集群模式下的处理流程: +在 Consumer 实例的启动流程中的会启动 MQClientInstance 实例,完成负载均衡服务线程 RebalanceService 的启动(**每隔 20s 执行一次**负载均衡),RebalanceService 线程的 run() 方法最终调用的是 RebalanceImpl 类的 rebalanceByTopic() 方法,该方法是实现 Consumer 端负载均衡的核心。rebalanceByTopic() 方法会根据广播模式还是集群模式做不同的逻辑处理。主要看集群模式: * 从 rebalanceImpl 实例的本地缓存变量 topicSubscribeInfoTable 中,获取该 Topic 主题下的消息消费队列集合 mqSet @@ -4828,7 +4860,7 @@ Consumer 端实现负载均衡的核心类 **RebalanceImpl** 对比下 RebalancePushImpl 和 RebalancePullImpl 两个实现类的 dispatchPullRequest() 方法,RebalancePullImpl 类里面的该方法为空 -消息消费队列在同一消费组不同消费者之间的负载均衡,其核心设计理念是在**一个消息消费队列在同一时间只允许被同一消费组内的一个消费者消费,一个消息消费者能同时消费多个消息队列** +消息消费队列在**同一消费组不同消费者之间的负载均衡**,其核心设计理念是在**一个消息消费队列在同一时间只允许被同一消费组内的一个消费者消费,一个消息消费者能同时消费多个消息队列** @@ -4889,7 +4921,7 @@ IndexFile 文件的存储在 `$HOME\store\index${fileName}`,文件名 fileName #### 消息重投 -生产者在发送消息时,同步消息和异步消息失败会重投,oneway 没有任何保证。消息重投保证消息尽可能发送成功、不丢失,但当出现消息量大、网络抖动时,可能会造成消息重复;生产者主动重发、Consumer 负载变化也会导致重复消息。 +生产者在发送消息时,同步消息和异步消息失败会重投,oneway 没有任何保证。消息重投保证消息尽可能发送成功、不丢失,但当出现消息量大、网络抖动时,可能会造成消息重复;生产者主动重发、Consumer 负载变化也会导致重复消息 如下方法可以设置消息重投策略: @@ -4921,7 +4953,7 @@ RocketMQ 会为每个消费组都设置一个 Topic 名称为 `%RETRY%+consumerG * 无序消息(普通、定时、延时、事务消息)的重试,可以通过设置返回状态达到消息重试的结果。无序消息的重试只针对集群消费方式生效,广播方式不提供失败重试特性,即消费失败后,失败消息不再重试,继续消费新的消息 -**无序消息情况下**,因为异常恢复需要一些时间,会为重试队列设置多个重试级别,每个重试级别都有对应的重新投递延时,重试次数越多投递延时就越大。RocketMQ 对于重试消息的处理是先保存至 Topic 名称为 `SCHEDULE_TOPIC_XXXX` 的延迟队列中,后台定时任务按照对应的时间进行 Delay 后重新保存至 `%RETRY%+consumerGroup` 的重试队列中 +**无序消息情况下**,因为异常恢复需要一些时间,会为重试队列设置多个重试级别,每个重试级别都有对应的重新投递延时,重试次数越多投递延时就越大。RocketMQ 对于重试消息的处理是先保存至 Topic 名称为 `SCHEDULE_TOPIC_XXXX` 的延迟队列中,后台定时任务**按照对应的时间进行 Delay 后**重新保存至 `%RETRY%+consumerGroup` 的重试队列中 消息队列 RocketMQ 默认允许每条消息最多重试 16 次,每次重试的间隔时间如下表示: @@ -5051,6 +5083,31 @@ public class MessageListenerImpl implements MessageListener { +*** + + + +### 高可靠性 + +RocketMQ 消息丢失可能发生在以下三个阶段: + +- 生产阶段:消息在 Producer 发送端创建出来,经过网络传输发送到 Broker 存储端 + - 生产者得到一个成功的响应,就可以认为消息的存储和消息的消费都是可靠的 + - 消息重投机制 +- 存储阶段:消息在 Broker 端存储,如果是主备或者多副本,消息会在这个阶段被复制到其他的节点或者副本上 + - 单点:刷盘机制(同步或异步) + - 主从:消息同步机制(异步复制或同步双写,主从复制章节详解) + - 过期删除:操作 CommitLog、ConsumeQueue 文件是基于文件内存映射机制,并且在启动的时候会将所有的文件加载,为了避免内存与磁盘的浪费,让磁盘能够循环利用,防止磁盘不足导致消息无法写入等引入了文件过期删除机制。最终使得磁盘水位保持在一定水平,最终保证新写入消息的可靠存储 +- 消费阶段:Consumer 消费端从 Broker存储端拉取消息,经过网络传输发送到 Consumer 消费端上 + - 消息重试机制来最大限度的保证消息的消费 + - 消费失败的进行消息回退,重试次数过多的消息放入死信队列 + + + +推荐文章:https://cdn.modb.pro/db/394751 + + + **** @@ -7380,7 +7437,7 @@ AllocateMappedFileService **创建 MappedFile 服务** ReputMessageService 消息分发服务,用于构**建 ConsumerQueue 和 IndexFile 文件** -* run():**循环执行 doReput 方法**,所以发送的消息存储进 CL 就可以产生对应的 CQ,每执行一次线程休眠 1 毫秒 +* run():**循环执行 doReput 方法**,**所以发送的消息存储进 CL 就可以产生对应的 CQ**,每执行一次线程休眠 1 毫秒 ```java public void run() @@ -7572,7 +7629,7 @@ public GetMessageResult getMessage(final String group, final String topic, final `if ((bufferTotal + sizePy) > ...)`:热数据一次 pull 请求最大允许获取 256kb 的消息 - `if (messageTotal > ...)`:冷数据一次 pull 请求最大允许获取32 条消息 + `if (messageTotal > ...)`:冷数据一次 pull 请求最大允许获取 32 条消息 * `if (messageFilter != null)`:按照消息 tagCode 进行过滤 @@ -10215,16 +10272,17 @@ ConsumeRequest 是 ConsumeMessageOrderlyService 的内部类,是一个 Runnabl 生产流程: -* 首先获取当前消息主题的发布信息,获取不到去 Namesrv 获取(默认有 TBW102),并将获取的到的路由数据转化为发布数据,**创建 MQ 队列**,客户端实例同样更新订阅数据,创建 MQ 队列,放入负载均衡服务 topicSubscribeInfoTable 中 +* 首先获取当前消息主题的发布信息,获取不到去 Namesrv 获取(默认有 TBW102),并将获取的到的路由数据转化为发布数据,**创建 MQ 队列在多个 Broker 组**(一组代表一主多从的 Broker 架构),客户端实例同样更新订阅数据,创建 MQ 队列,放入负载均衡服务 topicSubscribeInfoTable 中 * 然后从发布数据中选择一个 MQ 队列发送消息 -* Broker 端通过 SendMessageProcessor 对发送的消息进行持久化处理,存储到 CommitLog。将重试次数过多的消息加入死信队列,将延迟消息的主题和队列修改为调度主题和调度队列 ID +* Broker 端通过 SendMessageProcessor 对发送的消息进行持久化处理,存储到 CommitLog。将重试次数过多的消息加入**死信队列**,将延迟消息的主题和队列修改为调度主题和调度队列 ID * Broker 启动 ScheduleMessageService 服务会为每个延迟级别创建一个延迟任务,让延迟消息得到有效的处理,将到达交付时间的消息修改为原始主题的原始 ID 存入 CommitLog,消费者就可以进行消费了 消费流程: +* 消息消费队列 ConsumerQueue 存储消息在 CommitLog 的索引,消费者通过该队列来读取消息实体内容,一个 MQ 就对应一个 CQ * 首先通过负载均衡服务,将分配到当前消费者实例的 MQ 创建 PullRequest,并放入 PullMessageService 的本地阻塞队列内 * PullMessageService 循环从阻塞队列获取请求对象,发起拉消息请求,并创建 PullCallback 回调对象,将正常拉取的消息**提交到消费任务线程池**,并设置请求的下一次拉取位点,重新放入阻塞队列,形成闭环 -* 消费任务服务对消费失败的消息进行回退,回退失败的消息会再次提交消费任务重新消费 +* 消费任务服务对消费失败的消息进行回退,通过内部生产者实例发送回退消息,回退失败的消息会再次提交消费任务重新消费 * Broker 端对拉取消息的请求进行处理(processRequestCommand),查询成功将消息放入响应体,通过 Netty 写回客户端,当 `pullRequest.offset == queue.maxOffset` 说明该队列已经没有需要获取的消息,将请求放入长轮询集合等待有新消息 * PullRequestHoldService 负责长轮询,每 5 秒遍历一次长轮询集合,将满足条件的 PullRequest 再次提交到线程池内处理 @@ -10256,8 +10314,9 @@ Zookeeper 是基于观察者模式设计的分布式服务管理框架,负责 * 集群中只要有半数以上节点存活就能正常服务,所以 Zookeeper 适合部署奇数台服务器 * **全局数据一致**,每个 Server 保存一份相同的数据副本,Client 无论连接到哪个 Server,数据都是一致 * 更新的请求顺序执行,来自同一个 Client 的请求按其发送顺序依次执行 -* 数据更新原子性,一次数据更新要么成功,要么失败 +* **数据更新原子性**,一次数据更新要么成功,要么失败 * 实时性,在一定的时间范围内,Client 能读到最新数据 +* 心跳检测,会定时向各个服务提供者发送一个请求(实际上建立的是一个 Socket 长连接) ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Zookeeper-框架结构.png) @@ -10295,7 +10354,7 @@ Zookeeper 提供的主要功能包括:统一命名服务、统一配置管理 * 集群环境中,需要实时掌握每个集群节点的状态,可以将这些信息放入 ZNode,通过监控通知的机制实现 -* 实现客户端实时观察服务器上下线的变化 +* 实现客户端实时观察服务器上下线的变化,通过心跳检测实现 @@ -10339,7 +10398,7 @@ Zookeeper 提供的主要功能包括:统一命名服务、统一配置管理 Zookeeper 中的配置文件 zoo.cfg 中参数含义解读: -* tickTime = 2000:通信心跳时间,Zookeeper 服务器与客户端心跳时间,单位毫秒 +* tickTime = 2000:通信心跳时间,**Zookeeper 服务器与客户端心跳**时间,单位毫秒 * initLimit = 10:Leader 与 Follower 初始通信时限,初始连接时能容忍的最多心跳次数 * syncLimit = 5:Leader 与 Follower 同步通信时限,LF 通信时间超过 `syncLimit * tickTime`,Leader 认为 Follwer 下线 * dataDir:保存 Zookeeper 中的数据目录,默认是 tmp目录,容易被 Linux 系统定期删除,所以建议修改 @@ -10520,11 +10579,11 @@ Zookeepe 集群三个角色: 相关属性: * SID:服务器 ID,用来唯一标识一台集群中的机器,和 myid 一致 -* ZXID:事务ID,用来标识一次服务器状态的变更,在某一时刻集群中每台机器的 ZXID 值不一定完全一致,这和 ZooKeeper 服务器对于客户端更新请求的处理逻辑有关 +* ZXID:事务 ID,用来标识一次服务器状态的变更,在某一时刻集群中每台机器的 ZXID 值不一定完全一致,这和 ZooKeeper 服务器对于客户端更新请求的处理逻辑有关 * Epoch:每个 Leader 任期的代号,同一轮选举投票过程中的该值是相同的,投完一次票就增加 -选举机制:半数机制,超过半数的投票旧通过 +选举机制:半数机制,超过半数的投票就通过 * 第一次启动选举规则:投票过半数时,服务器 ID 大的胜出 @@ -10717,7 +10776,7 @@ CAP 理论指的是在一个分布式系统中,Consistency(一致性)、Av CAP 三个基本需求,因为 P 是必须的,因此分布式系统选择就在 CP 或者 AP 中: * 一致性:指数据在多个副本之间是否能够保持数据一致的特性,当一个系统在数据一致的状态下执行更新操作后,也能保证系统的数据仍然处于一致的状态 -* 可用性:指系统提供的服务必须一直处于可用的状态,对于用户的每一个操作请求总是能够在有限的时间内返回结果 +* 可用性:指系统提供的服务必须一直处于可用的状态,即使集群中一部分节点故障,对于用户的每一个操作请求总是能够在有限的时间内返回结果 * 分区容错性:分布式系统在遇到任何网络分区故障时,仍然能够保证对外提供服务,不会宕机,除非是整个网络环境都发生了故障 @@ -10749,7 +10808,7 @@ get –w /path # 监听【节点数据】的变化 * 在主线程中创建 Zookeeper 客户端,这时就会创建**两个线程**,一个负责网络连接通信(connet),一个负责监听(listener) * 通过 connect 线程将注册的监听事件发送给 Zookeeper -* 在 Zookeeper 的注册监听器列表中将注册的监听事件添加到列表中 +* 在 Zookeeper 的注册监听器列表中将注册的**监听事件添加到列表**中 * Zookeeper 监听到有数据或路径变化,将消息发送给 listener 线程 * listener 线程内部调用 process() 方法 @@ -10858,7 +10917,7 @@ public class DistributeServer { } private void register(String hostname) throws KeeperException, InterruptedException { - // OPEN_ACL_UNSAFE: ACL开放 + // OPEN_ACL_UNSAFE: ACL 开放 // EPHEMERAL_SEQUENTIAL: 临时顺序节点 String create = zk.create("/servers/" + hostname, hostname.getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL); @@ -11228,7 +11287,7 @@ FastLeaderElection 中有 WorkerReceiver 线程 -### 状态同步 +#### 状态同步 选举结束后,每个节点都需要根据角色更新自己的状态,Leader 更新状态为 Leader,其他节点更新状态为 Follower,整体流程: diff --git a/Java.md b/Java.md index 26fbec9..23a9e8b 100644 --- a/Java.md +++ b/Java.md @@ -6,25 +6,22 @@ #### 变量类型 -| | 成员变量 | 局部变量 | 静态变量 | -| :------: | :------------: | :------------------------: | :-------------------------: | -| 定义位置 | 在类中,方法外 | 方法中或者方法的形参 | 在类中,方法外 | -| 初始化值 | 有默认初始化值 | 无,先定义,赋值后才能使用 | 有默认初始化值 | -| 调用方法 | 对象调用 | | 对象调用,类名调用 | -| 存储位置 | 堆中 | 栈中 | 方法区(JDK8 以后移到堆中) | -| 生命周期 | 与对象共存亡 | 与方法共存亡 | 与类共存亡 | -| 别名 | 实例变量 | | 类变量,静态成员变量 | +| | 成员变量 | 局部变量 | 静态变量 | +| :------: | :------------: | :------------------: | :-------------------------: | +| 定义位置 | 在类中,方法外 | 方法中或者方法的形参 | 在类中,方法外 | +| 初始化值 | 有默认初始化值 | 无,赋值后才能使用 | 有默认初始化值 | +| 调用方法 | 对象调用 | | 对象调用,类名调用 | +| 存储位置 | 堆中 | 栈中 | 方法区(JDK8 以后移到堆中) | +| 生命周期 | 与对象共存亡 | 与方法共存亡 | 与类共存亡 | +| 别名 | 实例变量 | | 类变量,静态成员变量 | -**静态变量只有一个,成员变量是类中的变量,局部变量是方法中的变量** +静态变量只有一个,成员变量是类中的变量,局部变量是方法中的变量 -初学时笔记内容参考视频:https://www.bilibili.com/video/BV1TE41177mP,随着学习的深入又增加了很多知识 +初学时笔记内容参考视频:https://www.bilibili.com/video/BV1TE41177mP,随着学习的深入又增加很多知识 -给初学者的一些个人建议: -* 初学者对编程的认知比较浅显,一些专有词汇和概念难以理解,所以建议观看视频进行入门,大部分公开课视频讲的比较基础 -* 在有了一定的编程基础后,需要看一些经典书籍和技术博客,来扩容自己的知识广度和深度,可以长期保持记录笔记的好习惯 @@ -40,9 +37,8 @@ Java 语言提供了八种基本类型。六种数字类型(四个整数型, **byte:** -- byte 数据类型是 8 位、有符号的,以**二进制补码**表示的整数,**8 位一个字节**,首位是符号位 -- 最小值是 -128(-2^7) -- 最大值是 127(2^7-1) +- byte 数据类型是 8 位、有符号的,以二进制补码表示的整数,**8 位一个字节**,首位是符号位 +- 最小值是 -128(-2^7)、最大值是 127(2^7-1) - 默认值是 `0` - byte 类型用在大型数组中节约空间,主要代替整数,byte 变量占用的空间只有 int 类型的四分之一 - 例子:`byte a = 100,byte b = -50` @@ -50,8 +46,7 @@ Java 语言提供了八种基本类型。六种数字类型(四个整数型, **short:** - short 数据类型是 16 位、有符号的以二进制补码表示的整数 -- 最小值是 -32768(-2^15) -- 最大值是 32767(2^15 - 1) +- 最小值是 -32768(-2^15)、最大值是 32767(2^15 - 1) - short 数据类型也可以像 byte 那样节省空间,一个 short 变量是 int 型变量所占空间的二分之一 - 默认值是 `0` - 例子:`short s = 1000,short r = -20000` @@ -59,8 +54,7 @@ Java 语言提供了八种基本类型。六种数字类型(四个整数型, **int:** - int 数据类型是 32 位 4 字节、有符号的以二进制补码表示的整数 -- 最小值是 -2,147,483,648(-2^31) -- 最大值是 2,147,483,647(2^31 - 1) +- 最小值是 -2,147,483,648(-2^31)、最大值是 2,147,483,647(2^31 - 1) - 一般地整型变量默认为 int 类型 - 默认值是 `0` - 例子:`int a = 100000, int b = -200000` @@ -68,12 +62,10 @@ Java 语言提供了八种基本类型。六种数字类型(四个整数型, **long:** - long 数据类型是 64 位 8 字节、有符号的以二进制补码表示的整数 -- 最小值是 -9,223,372,036,854,775,808(-2^63) -- 最大值是 9,223,372,036,854,775,807(2^63 -1) +- 最小值是 -9,223,372,036,854,775,808(-2^63)、最大值是 9,223,372,036,854,775,807(2^63 -1) - 这种类型主要使用在需要比较大整数的系统上 - 默认值是 ` 0L` -- 例子: `long a = 100000L,Long b = -200000L` - L 理论上不分大小写,但是若写成 I 容易与数字 1 混淆,不容易分辩,所以最好大写 +- 例子: `long a = 100000L,Long b = -200000L`,L 理论上不分大小写,但是若写成 I 容易与数字 1 混淆,不容易分辩 **float:** @@ -104,7 +96,7 @@ Java 语言提供了八种基本类型。六种数字类型(四个整数型, - char 类型是一个单一的 16 位**两个字节**的 Unicode 字符 - 最小值是 `\u0000`(即为 0) - 最大值是 `\uffff`(即为 65535) -- char 数据类型可以存储任何字符 +- char 数据类型可以**存储任何字符** - 例子:`char c = 'A'`,`char c = '张'` @@ -117,7 +109,7 @@ Java 语言提供了八种基本类型。六种数字类型(四个整数型, * float 与 double: - Java 不能隐式执行**向下转型**,因为这会使得精度降低(参考多态),但是可以向上转型 + Java 不能隐式执行**向下转型**,因为这会使得精度降低,但是可以向上转型 ```java //1.1字面量属于double类型,不能直接将1.1直接赋值给 float 变量,因为这是向下转型 @@ -146,7 +138,7 @@ Java 语言提供了八种基本类型。六种数字类型(四个整数型, 字面量 1 是 int 类型,比 short 类型精度要高,因此不能隐式地将 int 类型向下转型为 short 类型 - 使用 += 或者 ++ 运算符会执行隐式类型转换: + 使用 += 或者 ++ 运算符会执行类型转换: ```java short s1 = 1; @@ -177,12 +169,12 @@ Java 语言提供了八种基本类型。六种数字类型(四个整数型, 基本数据类型 包装类(引用数据类型) byte Byte short Short -int Integer(特殊) +int Integer long Long float Float double Double -char Character(特殊) +char Character boolean Boolean ``` Java 为包装类做了一些特殊功能,具体来看特殊功能主要有: @@ -209,17 +201,15 @@ Java 为包装类做了一些特殊功能,具体来看特殊功能主要有: String itStr1 = Integer.toString(it); System.out.println(itStr1+1);//1001 // c.直接把基本数据类型+空字符串就得到了字符串。 - String itStr2 = it+""; + String itStr2 = it + ""; System.out.println(itStr2+1);// 1001 - // 2.把字符串类型的数值转换成对应的基本数据类型的值。(真的很有用) + // 2.把字符串类型的数值转换成对应的基本数据类型的值 String numStr = "23"; - //int numInt = Integer.parseInt(numStr); int numInt = Integer.valueOf(numStr); System.out.println(numInt+1);//24 String doubleStr = "99.9"; - //double doubleDb = Double.parseDouble(doubleStr); double doubleDb = Double.valueOf(doubleStr); System.out.println(doubleDb+0.1);//100.0 } @@ -329,7 +319,7 @@ new Integer(123) 与 Integer.valueOf(123) 的区别在于: System.out.println(z == k); // true ``` -valueOf() 方法的实现比较简单,就是先判断值是否在缓存池中,如果在的话就直接返回缓存池的内容。编译器会在自动装箱过程调用 valueOf() 方法,因此多个值相同且值在缓存池范围内的 Integer 实例使用自动装箱来创建,那么就会引用相同的对象。 +valueOf() 方法的实现比较简单,就是先判断值是否在缓存池中,如果在的话就直接返回缓存池的内容。编译器会在自动装箱过程调用 valueOf() 方法,因此多个值相同且值在缓存池范围内的 Integer 实例使用自动装箱来创建,那么就会引用相同的对象 **基本类型对应的缓存池如下:** @@ -340,7 +330,7 @@ valueOf() 方法的实现比较简单,就是先判断值是否在缓存池中 - Integer values between -128 and 127 - Character in the range \u0000 to \u007F (0 and 127) -在 jdk 1.8 所有的数值类缓冲池中,**Integer 的缓存池 IntegerCache 很特殊,这个缓冲池的下界是 -128,上界默认是 127**,但是上界是可调的,在启动 JVM 时通过 `AutoBoxCacheMax=` 来指定这个缓冲池的大小,该选项在 JVM 初始化的时候会设定一个名为 java.lang.IntegerCache.high 系统属性,然后 IntegerCache 初始化的时候就会读取该系统属性来决定上界 +在 jdk 1.8 所有的数值类缓冲池中,**Integer 的缓存池 IntegerCache 很特殊,这个缓冲池的下界是 -128,上界默认是 127**,但是上界是可调的,在启动 JVM 时通过 `AutoBoxCacheMax=` 来指定这个缓冲池的大小,该选项在 JVM 初始化的时候会设定一个名为 java.lang.Integer.IntegerCache 系统属性,然后 IntegerCache 初始化的时候就会读取该系统属性来决定上界 ```java Integer x = 100; // 自动装箱,底层调用 Integer.valueOf(1) @@ -417,7 +407,7 @@ public static void main(String[] args) { #### 元素访问 -* **索引**:每一个存储到数组的元素,都会自动的拥有一个编号,从 **0** 开始。这个自动编号称为数组索引(index),可以通过数组的索引访问到数组中的元素。 +* **索引**:每一个存储到数组的元素,都会自动的拥有一个编号,从 **0** 开始。这个自动编号称为数组索引(index),可以通过数组的索引访问到数组中的元素 * **访问格式**:数组名[索引],`arr[0]` * **赋值:**`arr[0] = 10` @@ -430,7 +420,7 @@ public static void main(String[] args) { #### 内存分配 -内存是计算机中的重要器件,临时存储区域,作用是运行程序。我们编写的程序是存放在硬盘中,在硬盘中的程序是不会运行的,必须放进内存中才能运行,运行完毕后会清空内存。 Java 虚拟机要运行程序,必须要对内存进行空间的分配和管理。 +内存是计算机中的重要器件,临时存储区域,作用是运行程序。编写的程序是存放在硬盘中,在硬盘中的程序是不会运行的,必须放进内存中才能运行,运行完毕后会清空内存,Java 虚拟机要运行程序,必须要对内存进行空间的分配和管理 | 区域名称 | 作用 | | ---------- | ---------------------------------------------------------- | @@ -440,7 +430,7 @@ public static void main(String[] args) { | 堆内存 | 存储对象或者数组,new 来创建的,都存储在堆内存 | | 方法栈 | 方法运行时使用的内存,比如 main 方法运行,进入方法栈中执行 | -**内存分配图**:Java 内存分配 +内存分配图:**Java 数组分配在堆内存** * 一个数组内存图 @@ -475,9 +465,9 @@ public static void main(String[] args) { } ``` - arr = null,表示变量 arr 将不再保存数组的内存地址,也就不允许再操作数组,因此运行的时候会抛出空指针异常。在开发中,空指针异常是不能出现的,一旦出现了,就必须要修改编写的代码。 + arr = null,表示变量 arr 将不再保存数组的内存地址,也就不允许再操作数组,因此运行的时候会抛出空指针异常。在开发中,空指针异常是不能出现的,一旦出现了,就必须要修改编写的代码 - 解决方案:给数组一个真正的堆内存空间引用即可! + 解决方案:给数组一个真正的堆内存空间引用即可 @@ -491,14 +481,12 @@ public static void main(String[] args) { 初始化: -* 动态初始化: - - 数据类型[][] 变量名 = new 数据类型[m] [n] : `int[][] arr = new int[3][3]` +* 动态初始化:数据类型[][] 变量名 = new 数据类型[m] [n],`int[][] arr = new int[3][3]` * m 表示这个二维数组,可以存放多少个一维数组,行 * n 表示每一个一维数组,可以存放多少个元素,列 * 静态初始化 - * 数据类型[][] 变量名 = new 数据类型[][]{ {元素1, 元素2...} , {元素1, 元素2...} + * 数据类型[][] 变量名 = new 数据类型 [][]{{元素1, 元素2...} , {元素1, 元素2...} * 数据类型[][] 变量名 = {{元素1, 元素2...}, {元素1, 元素2...}...} * `int[][] arr = {{11,22,33}, {44,55,66}}` @@ -537,20 +525,20 @@ public class Test1 { ### 运算 * i++ 与 ++i 的区别? - i++ 表示先将 i 放在表达式中运算,然后再加 1 - ++i 表示先将 i 加 1,然后再放在表达式中运算 + + i++ 表示先将 i 放在表达式中运算,然后再加 1,++i 表示先将 i 加 1,然后再放在表达式中运算 * || 和 |,&& 和& 的区别,逻辑运算符 - **& 和| 称为布尔运算符,位运算符。&& 和 || 称为条件布尔运算符,也叫短路运算符**。 + **& 和| 称为布尔运算符,位运算符;&& 和 || 称为条件布尔运算符,也叫短路运算符** 如果 && 运算符的第一个操作数是 false,就不需要考虑第二个操作数的值了,因为无论第二个操作数的值是什么,其结果都是 false;同样,如果第一个操作数是 true,|| 运算符就返回 true,无需考虑第二个操作数的值;但 & 和 | 却不是这样,它们总是要计算两个操作数。为了提高性能,**尽可能使用 && 和 || 运算符** -* ^ 异或:两位相异为 1,相同为 0,又叫不进位加法。同或:两位相同为 1,相异为 0 +* 异或 ^:两位相异为 1,相同为 0,又叫不进位加法 -* switch +* 同或:两位相同为 1,相异为 0 - 从 Java 7 开始,可以在 switch 条件判断语句中使用 String 对象 +* switch:从 Java 7 开始,可以在 switch 条件判断语句中使用 String 对象 ```java String s = "a"; @@ -570,11 +558,9 @@ public class Test1 { * break:跳出一层循环 -* 移位运算 +* 移位运算:计算机里一般用**补码表示数字**,正数、负数的表示区别就是最高位是 0 还是 1 - 计算机里一般用**补码表示数字**,正数、负数的表示区别就是最高位是 0 还是 1 - - * 正数的原码反码补码相同 + * 正数的原码反码补码相同,最高位为 0 ```java 100: 00000000 00000000 00000000 01100100 @@ -583,7 +569,7 @@ public class Test1 { * 负数: 原码:最高位为 1,其余位置和正数相同 反码:保证符号位不变,其余位置取反 - 补码:保证符号位不变,其余位置取反加 1,即反码 +1 + 补码:保证符号位不变,其余位置取反后加 1,即反码 +1 ```java -100 原码: 10000000 00000000 00000000 01100100 //32位 @@ -636,7 +622,7 @@ public class Test1 { 格式:数据类型... 参数名称 -作用:传输参数非常灵活,方便,可以不传输参数、传输一个参数、或者传输一个数组。 +作用:传输参数非常灵活,可以不传输参数、传输一个参数、或者传输一个数组 可变参数的注意事项: @@ -837,7 +823,7 @@ public class MethodDemo { Java 的参数是以**值传递**的形式传入方法中 -值传递和引用传递的区别在于传递后会不会影响实参的值:值传递会创建副本,引用传递不会创建副本 +值传递和引用传递的区别在于传递后会不会影响实参的值:**值传递会创建副本**,引用传递不会创建副本 * 基本数据类型:形式参数的改变,不影响实际参数 @@ -996,11 +982,11 @@ Debug 是供程序员使用的程序调试工具,它可以用于查看程序 ### 概述 -**Java 是一种面向对象的高级编程语言。** +Java 是一种面向对象的高级编程语言 -**三大特征:封装,继承,多态** +面向对象三大特征:**封装,继承,多态** -面向对象最重要的两个概念:类和对象 +两个概念:类和对象 * 类:相同事物共同特征的描述,类只是学术上的一个概念并非真实存在的,只能描述一类事物 * 对象:是真实存在的实例, 实例 == 对象,**对象是类的实例化** @@ -1198,11 +1184,11 @@ Java 是通过成员变量是否有 static 修饰来区分是类的还是属于 内存问题: -* **栈内存存放 main 方法和地址** +* 栈内存存放 main 方法和地址 -* **堆内存存放对象和变量** +* 堆内存存放对象和变量 -* **方法区存放 class 和静态变量(jdk8 以后移入堆)** +* 方法区存放 class 和静态变量(jdk8 以后移入堆) 访问问题: @@ -1210,7 +1196,7 @@ Java 是通过成员变量是否有 static 修饰来区分是类的还是属于 * 实例方法是否可以直接访问静态成员变量?可以,静态成员变量可以被共享访问 * 实例方法是否可以直接访问实例方法? 可以,实例方法和实例方法都属于对象 * 实例方法是否可以直接访问静态方法?可以,静态方法可以被共享访问 -* 静态方法是否可以直接访问实例变量? 不可以,实例变量必须用对象访问!! +* 静态方法是否可以直接访问实例变量? 不可以,实例变量**必须用对象访问**!! * 静态方法是否可以直接访问静态变量? 可以,静态成员变量可以被共享访问。 * 静态方法是否可以直接访问实例方法? 不可以,实例方法必须用对象访问!! * 静态方法是否可以直接访问静态方法?可以,静态方法可以被共享访问!! @@ -1256,7 +1242,7 @@ Java 是通过成员变量是否有 static 修饰来区分是类的还是属于 * 子类不能继承父类的构造器,子类有自己的构造器 * 子类是不能可以继承父类的私有成员的,可以反射暴力去访问继承自父类的私有成员 -* 子类是不能继承父类的静态成员的,子类只是可以访问父类的静态成员,父类静态成员只有一份可以被子类共享访问,**共享并非继承** +* 子类是不能继承父类的静态成员,父类静态成员只有一份可以被子类共享访问,**共享并非继承** ```java public class ExtendsDemo { @@ -1267,8 +1253,10 @@ public class ExtendsDemo { System.out.println(Cat.schoolName); } } + class Cat extends Animal{ } + class Animal{ public static String schoolName ="seazean"; public static void test(){} @@ -1295,6 +1283,7 @@ public class ExtendsDemo { w.showName(); } } + class Wolf extends Animal{ private String name = "子类狼"; public void showName(){ @@ -1327,7 +1316,7 @@ class Animal{ 方法重写的校验注解:@Override -* 方法加了这个注解,那就必须是成功重写父类的方法,否则报错 +* 方法加了这个注解,那就必须是成功重写父类的方法,否则报错 * @Override 优势:可读性好,安全,优雅 **子类可以扩展父类的功能,但不能改变父类原有的功能**,重写有以下三个限制: @@ -1432,11 +1421,11 @@ class Animal{ * this 代表了当前对象的引用(继承中指代子类对象):this.子类成员变量、this.子类成员方法、**this(...)** 可以根据参数匹配访问本类其他构造器 * super 代表了父类对象的引用(继承中指代了父类对象空间):super.父类成员变量、super.父类的成员方法、super(...) 可以根据参数匹配访问父类的构造器 -**注意:** +注意: -* this(...) 借用本类其他构造器,super(...) 调用父类的构造器。 -* this(...) 或 super(...) 必须放在构造器的第一行,否则报错! -* this(...) 和 super(...) 不能同时出现在构造器中,因为构造函数必须出现在第一行上,只能选择一个。 +* this(...) 借用本类其他构造器,super(...) 调用父类的构造器 +* this(...) 或 super(...) 必须放在构造器的第一行,否则报错 +* this(...) 和 super(...) **不能同时出现**在构造器中,因为构造函数必须出现在第一行上,只能选择一个 ```java public class ThisDemo { @@ -1503,7 +1492,7 @@ final 和 abstract 的关系是**互斥关系**,不能同时修饰类或者同 final 修饰静态成员变量,变量变成了常量 -**常量:有 public static final 修饰,名称字母全部大写,多个单词用下划线连接。** +常量:有 public static final 修饰,名称字母全部大写,多个单词用下划线连接 final 修饰静态成员变量可以在哪些地方赋值: @@ -1520,7 +1509,6 @@ public class FinalDemo { static{ //SCHOOL_NAME = "java";//报错 SCHOOL_NAME1 = "张三1"; - //SCHOOL_NAME1 = "张三2"; // 报错,第二次赋值! } } ``` @@ -1575,13 +1563,13 @@ public class FinalDemo { #### 基本介绍 -> 父类知道子类要完成某个功能,但是每个子类实现情况不一样。 +> 父类知道子类要完成某个功能,但是每个子类实现情况不一样 抽象方法:没有方法体,只有方法签名,必须用 abstract 修饰的方法就是抽象方法 抽象类:拥有抽象方法的类必须定义成抽象类,必须用 abstract 修饰,**抽象类是为了被继承** -一个类继承抽象类,**必须重写抽象类的全部抽象方法**,否则这个类必须定义成抽象类,因为拥有抽象方法的类必须定义成抽象类 +一个类继承抽象类,**必须重写抽象类的全部抽象方法**,否则这个类必须定义成抽象类 ```java public class AbstractDemo { @@ -1811,32 +1799,32 @@ public class InterfaceDemo { InterfaceJDK8.inAddr(); } } -class Man implements InterfaceJDK8{ +class Man implements InterfaceJDK8 { @Override public void work() { System.out.println("工作中。。。"); } } -interface InterfaceJDK8{ +interface InterfaceJDK8 { //抽象方法!! void work(); // a.默认方法(就是之前写的普通实例方法) // 必须用接口的实现类的对象来调用。 - default void run(){ + default void run() { go(); System.out.println("开始跑步🏃‍"); } // b.静态方法 // 注意:接口的静态方法必须用接口的类名本身来调用 - static void inAddr(){ + static void inAddr() { System.out.println("我们在武汉"); } // c.私有方法(就是私有的实例方法): JDK 1.9才开始有的。 // 只能在本接口中被其他的默认方法或者私有方法访问。 - private void go(){ + private void go() { System.out.println("开始。。"); } } @@ -1874,7 +1862,7 @@ interface InterfaceJDK8{ #### 基本介绍 -多态的概念:同一个实体同时具有多种形式同一个类型的对象,执行同一个行为,在不同的状态下会表现出不同的行为特征。 +多态的概念:同一个实体同时具有多种形式同一个类型的对象,执行同一个行为,在不同的状态下会表现出不同的行为特征 多态的格式: @@ -1897,11 +1885,11 @@ interface InterfaceJDK8{ * 存在方法重写 多态的优势: -* 在多态形式下,右边对象可以实现组件化切换,业务功能也随之改变,便于扩展和维护。可以实现类与类之间的**解耦** +* 在多态形式下,右边对象可以实现组件化切换,便于扩展和维护,也可以实现类与类之间的**解耦** * 父类类型作为方法形式参数,传递子类对象给方法,可以传入一切子类对象进行方法的调用,更能体现出多态的**扩展性**与便利性 多态的劣势: -* 多态形式下,不能直接调用子类特有的功能,因为编译看左边,父类中没有子类独有的功能,所以代码在编译阶段就直接报错了! +* 多态形式下,不能直接调用子类特有的功能,因为编译看左边,父类中没有子类独有的功能,所以代码在编译阶段就直接报错了 ```java public class PolymorphicDemo { @@ -2061,7 +2049,7 @@ static class Outter{ #### 实例内部类 -定义:无 static 修饰的内部类,属于外部类的每个对象,跟着外部类对象一起加载。 +定义:无 static 修饰的内部类,属于外部类的每个对象,跟着外部类对象一起加载 实例内部类的成分特点:实例内部类中不能定义静态成员,其他都可以定义 @@ -2192,9 +2180,8 @@ static { ``` * 静态代码块特点: - * 必须有 static 修饰 + * 必须有 static 修饰,只能访问静态资源 * 会与类一起优先加载,且自动触发执行一次 - * 只能访问静态资源 * 静态代码块作用: * 可以在执行类的方法等操作之前先在静态代码块中进行静态资源的初始化 * **先执行静态代码块,在执行 main 函数里的操作** @@ -2337,7 +2324,7 @@ Object 的 clone() 是 protected 方法,一个类不显式去重写 clone(), * 浅拷贝 (shallowCopy):**对基本数据类型进行值传递,对引用数据类型只是复制了引用**,被复制对象属性的所有的引用仍然指向原来的对象,简而言之就是增加了一个指针指向原来对象的内存地址 - **Java 中的复制方法基本都是浅克隆**:Object.clone()、System.arraycopy()、Arrays.copyOf() + **Java 中的复制方法基本都是浅拷贝**:Object.clone()、System.arraycopy()、Arrays.copyOf() * 深拷贝 (deepCopy):对基本数据类型进行值传递,对引用数据类型是一个整个独立的对象拷贝,会拷贝所有的属性并指向的动态分配的内存,简而言之就是把所有属性复制到一个新的内存,增加一个指针指向新内存。所以使用深拷贝的情况下,释放内存的时候不会出现使用浅拷贝时释放同一块内存的错误 @@ -2390,11 +2377,11 @@ Objects 类与 Object 是继承关系 Objects 的方法: -* `public static boolean equals(Object a, Object b)`:比较两个对象是否相同。 - 底层进行非空判断,从而可以避免空指针异常,更安全,推荐使用! - - ```java +* `public static boolean equals(Object a, Object b)`:比较两个对象是否相同 + + ```java public static boolean equals(Object a, Object b) { + // 进行非空判断,从而可以避免空指针异常 return a == b || a != null && a.equals(b); } ``` @@ -2637,7 +2624,7 @@ public static void main(String[] args) { System.out.println(str1 == str1.intern());//true,字符串池中不存在,把堆中的引用复制一份放入串池 String str2 = new StringBuilder("ja").append("va").toString(); - System.out.println(str2 == str2.intern());//false + System.out.println(str2 == str2.intern());//false,字符串池中存在,直接返回已经存在的引用 } ``` @@ -2878,7 +2865,7 @@ public class MyArraysDemo { 1. 导入包:`import java.util.Random` 2. 创建对象:`Random r = new Random()` 3. 随机整数:`int num = r.nextInt(10)` - * 解释:10 代表的是一个范围,如果括号写 10,产生的随机数就是 0 - 9,括号写 20 的随机数则是 0 - 19 + * 解释:10 代表的是一个范围,如果括号写 10,产生的随机数就是 0 - 9,括号写 20 的随机数则是 0 - 19 * 获取 0 - 10:`int num = r.nextInt(10 + 1)` 4. 随机小数:`public double nextDouble()` 从范围 `0.0d` 至 `1.0d` (左闭右开),伪随机地生成并返回 @@ -2900,11 +2887,11 @@ System 代表当前系统 * `public static long currentTimeMillis()`:获取当前系统此刻时间毫秒值 * `static void arraycopy(Object var0, int var1, Object var2, int var3, int var4)`:数组拷贝 - 参数一:原数组 - 参数二:从原数组的哪个位置开始赋值 - 参数三:目标数组 - 参数四:从目标数组的哪个位置开始赋值 - 参数五:赋值几个 + * 参数一:原数组 + * 参数二:从原数组的哪个位置开始赋值 + * 参数三:目标数组 + * 参数四:从目标数组的哪个位置开始赋值 + * 参数五:赋值几个 ```java public class SystemDemo { @@ -3758,15 +3745,12 @@ public class RegexDemo { 数据存储的常用结构有:栈、队列、数组、链表和红黑树 * 队列(queue):先进先出,后进后出。(FIFO first in first out) - 场景:各种排队、叫号系统,有很多集合可以实现队列 - + * 栈(stack):后进先出,先进后出 (LIFO) - 压栈 == 入栈、弹栈 == 出栈 - 场景:手枪的弹夹 - + * 数组:数组是内存中的连续存储区域,分成若干等分的小区域(每个区域大小是一样的)元素存在索引 - 特点:**查询元素快**(根据索引快速计算出元素的地址,然后立即去定位) - **增删元素慢**(创建新数组,迁移元素) + + 特点:**查询元素快**(根据索引快速计算出元素的地址,然后立即去定位),**增删元素慢**(创建新数组,迁移元素) * 链表:元素不是内存中的连续区域存储,元素是游离存储的,每个元素会记录下个元素的地址 特点:**查询元素慢,增删元素快**(针对于首尾元素,速度极快,一般是双链表) @@ -3774,11 +3758,12 @@ public class RegexDemo { * 树: * 二叉树:binary tree 永远只有一个根节点,是每个结点不超过2个节点的树(tree) + 特点:二叉排序树:小的左边,大的右边,但是可能树很高,性能变差,为了做排序和搜索会进行左旋和右旋实现平衡查找二叉树,让树的高度差不大于1 -* 红黑树(基于红黑规则实现自平衡的排序二叉树):树保证到了很矮小,但是又排好序,性能最高的树 + * 红黑树(基于红黑规则实现自平衡的排序二叉树):树保证到了很矮小,但是又排好序,性能最高的 - 特点:**红黑树的增删查改性能都好** + 特点:**红黑树的增删查改性能都好** 各数据结构时间复杂度对比: @@ -3970,8 +3955,8 @@ ArrayList 添加的元素,是有序,可重复,有索引的 * `public boolean add(E e)`:将指定的元素追加到此集合的末尾 * `public void add(int index, E element)`:将指定的元素,添加到该集合中的指定位置上 * `public E get(int index)`:返回集合中指定位置的元素 -* `public E remove(int index)`:移除列表中指定位置的元素, 返回的是被移除的元素 -* `public E set(int index, E element)`:用指定元素替换集合中指定位置的元素,返回更新前的元素值 +* `public E remove(int index)`:移除列表中指定位置的元素,返回的是被移除的元素 +* `public E set(int index, E element)`:用指定元素替换集合中指定位置的元素,返回更新前的元素值 * `int indexOf(Object o)`:返回列表中指定元素第一次出现的索引,如果不包含此元素,则返回 -1 ```java @@ -4008,7 +3993,7 @@ public class ArrayList extends AbstractList 核心方法: -* 构造函数:以无参数构造方法创建 ArrayList 时,实际上初始化赋值的是一个空数组。当真正对数组进行添加元素操作时,才真正分配容量,即向数组中添加第一个元素时,**数组容量扩为 10** +* 构造函数:以无参数构造方法创建 ArrayList 时,实际上初始化赋值的是一个空数组。当真正对数组进行添加元素操作时,才真正分配容量(惰性初始化),即向数组中添加第一个元素时,**数组容量扩为 10** * 添加元素: @@ -4059,8 +4044,8 @@ public class ArrayList extends AbstractList public void add(int index, E element) { rangeCheckForAdd(index); ensureCapacityInternal(size + 1); // Increments modCount!! - System.arraycopy(elementData, index, elementData, index + 1, - size - index); + // 将指定索引后的数据后移 + System.arraycopy(elementData, index, elementData, index + 1, size - index); elementData[index] = element; size++; } @@ -4091,7 +4076,7 @@ public class ArrayList extends AbstractList * OutOfMemoryError:Requested array size exceeds VM limit(请求的数组大小超出 VM 限制) * OutOfMemoryError: Java heap space(堆区内存不足,可以通过设置 JVM 参数 -Xmx 来调节) -* 删除元素:需要调用 System.arraycopy() 将 index+1 后面的元素都复制到 index 位置上,在旧数组上操作,该操作的时间复杂度为 O(N),可以看到 ArrayList 删除元素的代价是非常高的, +* 删除元素:需要调用 System.arraycopy() 将 index+1 后面的元素都复制到 index 位置上,在旧数组上操作,该操作的时间复杂度为 O(N),可以看到 ArrayList 删除元素的代价是非常高的 ```java public E remove(int index) { @@ -4212,10 +4197,6 @@ LinkedList 除了拥有 List 集合的全部功能还多了很多操作首尾元 * `public E poll()`:检索并删除此列表的头(第一个元素) * `public void addFirst(E e)`:将指定元素插入此列表的开头 * `public void addLast(E e)`:将指定元素添加到此列表的结尾 -* `public E getFirst()`:返回此列表的第一个元素 -* `public E getLast()`:返回此列表的最后一个元素 -* `public E removeFirst()`:移除并返回此列表的第一个元素 -* `public E removeLast()`:移除并返回此列表的最后一个元素 * `public E pop()`:从此列表所表示的堆栈处弹出一个元素 * `public void push(E e)`:将元素推入此列表所表示的堆栈 * `public int indexOf(Object o)`:返回此列表中指定元素的第一次出现的索引,如果不包含返回 -1 @@ -4401,7 +4382,7 @@ Set 集合添加的元素是无序,不重复的。 每个元素的 hashcode() 的值进行响应的算法运算,计算出的值相同的存入一个数组块中,以链表的形式存储,如果链表长度超过8就采取红黑树存储,所以输出的元素是无序的。 -* 如何设置只要对象内容一样,就希望集合认为它们重复了:**重写 hashCode 和 equals 方法** +* 如何设置只要对象内容一样,就希望集合认为重复:**重写 hashCode 和 equals 方法** @@ -4585,7 +4566,7 @@ Map集合的体系: LinkedHashMap(实现类) ``` -Map集合的特点: +Map 集合的特点: 1. Map 集合的特点都是由键决定的 2. Map 集合的键是无序,不重复的,无索引的(Set) @@ -4597,13 +4578,6 @@ HashMap:元素按照键是无序,不重复,无索引,值不做要求 LinkedHashMap:元素按照键是有序,不重复,无索引,值不做要求 -```java -//经典代码 -Map maps = new HashMap<>(); -maps.put("手机",1); -System.out.println(maps); -``` - *** @@ -4714,12 +4688,12 @@ JDK7 对比 JDK8: * 哈希表(Hash table,也叫散列表),根据关键码值而直接访问的数据结构。通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度,这个映射函数叫做散列函数,存放记录的数组叫做散列表 -* JDK1.8 之前 HashMap 由 数组+链表 组成 +* JDK1.8 之前 HashMap 由数组+链表组成 * 数组是 HashMap 的主体 * 链表则是为了解决哈希冲突而存在的(**拉链法解决冲突**),拉链法就是头插法,两个对象调用的 hashCode 方法计算的哈希码值(键的哈希)一致导致计算的数组索引值相同 -* JDK1.8 以后 HashMap 由 **数组+链表 +红黑树**数据结构组成 +* JDK1.8 以后 HashMap 由**数组+链表 +红黑树**数据结构组成 * 解决哈希冲突时有了较大的变化 * 当链表长度**超过(大于)阈值**(或者红黑树的边界值,默认为 8)并且当前数组的**长度大于等于 64 时**,此索引位置上的所有数据改为红黑树存储 @@ -4767,7 +4741,7 @@ HashMap 继承关系如下图所示: ```java // 默认的初始容量是16 -- 1<<4相当于1*2的4次方---1*16 - static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; + static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; ``` HashMap 构造方法指定集合的初始化容量大小: @@ -4776,9 +4750,9 @@ HashMap 继承关系如下图所示: HashMap(int initialCapacity)// 构造一个带指定初始容量和默认加载因子 (0.75) 的空 HashMap ``` - * 为什么必须是 2 的 n 次幂? + * 为什么必须是 2 的 n 次幂?用位运算替代取余计算,减少 rehash 的代价(移动的节点少) - HashMap 中添加元素时,需要根据 key 的 hash 值,确定在数组中的具体位置。为了存取高效,要尽量较少碰撞,把数据分配均匀,每个链表长度大致相同,实现该方法的算法就是取模,hash%length,计算机中直接求余效率不如位移运算,所以源码中使用 hash&(length-1),实际上**hash % length == hash & (length-1)的前提是 length 是 2 的n次幂** + HashMap 中添加元素时,需要根据 key 的 hash 值确定在数组中的具体位置。为了减少碰撞,把数据分配均匀,每个链表长度大致相同,实现该方法就是取模 `hash%length`,计算机中直接求余效率不如位移运算, **`hash % length == hash & (length-1)` 的前提是 length 是 2 的 n 次幂** 散列平均分布:2 的 n 次方是 1 后面 n 个 0,2 的 n 次方 -1 是 n 个 1,可以**保证散列的均匀性**,减少碰撞 @@ -4804,11 +4778,6 @@ HashMap 继承关系如下图所示: static final int MAXIMUM_CAPACITY = 1 << 30;// 0100 0000 0000 0000 0000 0000 0000 0000 = 2 ^ 30 ``` - 最大容量为什么是 2 的 30 次方原因: - - * int 类型是 32 位整型,占 4 个字节 - * Java 的原始类型里没有无符号类型,所以首位是符号位正数为 0,负数为 1, - 5. 当链表的值超过 8 则会转红黑树(JDK1.8 新增) ```java @@ -4852,7 +4821,7 @@ HashMap 继承关系如下图所示: static final int MIN_TREEIFY_CAPACITY = 64; ``` - 原因:数组比较小的情况下变为红黑树结构,反而会降低效率,红黑树需要进行左旋,右旋,变色这些操作来保持平衡。同时数组长度小于 64 时,搜索时间相对快些,所以为了提高性能和减少搜索时间,底层在阈值大于 8 并且数组长度大于等于 64 时,链表才转换为红黑树,效率也变的更高效 + 原因:数组比较小的情况下变为红黑树结构,反而会降低效率,红黑树需要进行左旋,右旋,变色这些操作来保持平衡 8. table 用来初始化(必须是二的 n 次幂) @@ -4861,8 +4830,6 @@ HashMap 继承关系如下图所示: transient Node[] table; ``` - jdk8 之前数组类型是 Entry类型,之后是 Node 类型。只是换了个名字,都实现了一样的接口 Map.Entry,负责存储键值对数据的 - 9. HashMap 中**存放元素的个数** ```java @@ -4884,7 +4851,7 @@ HashMap 继承关系如下图所示: int threshold; ``` -12. **哈希表的加载因子(重点)** +12. **哈希表的加载因子** ```java final float loadFactor; @@ -4892,9 +4859,9 @@ HashMap 继承关系如下图所示: * 加载因子的概述 - loadFactor 加载因子,是用来衡量 HashMap 满的程度,表示 **HashMap 的疏密程度**,影响 hash 操作到同一个数组位置的概率,计算 HashMap 的实时加载因子的方法为:size/capacity,而不是占用桶的数量去除以 capacity,capacity 是桶的数量,也就是 table 的长度 length。 + loadFactor 加载因子,是用来衡量 HashMap 满的程度,表示 HashMap 的疏密程度,影响 hash 操作到同一个数组位置的概率,计算 HashMap 的实时加载因子的方法为 **size/capacity**,而不是占用桶的数量去除以 capacity,capacity 是桶的数量,也就是 table 的长度 length - 当 HashMap 里面容纳的元素已经达到 HashMap 数组长度的 75% 时,表示 HashMap 拥挤,需要扩容,而扩容这个过程涉及到 rehash、复制数据等操作,非常消耗性能,所以开发中尽量减少扩容的次数,可以通过创建 HashMap 集合对象时指定初始容量来尽量避免。 + 当 HashMap 容纳的元素已经达到数组长度的 75% 时,表示 HashMap 拥挤需要扩容,而扩容这个过程涉及到 rehash、复制数据等操作,非常消耗性能,所以开发中尽量减少扩容的次数,通过创建 HashMap 集合对象时指定初始容量来避免 ```java HashMap(int initialCapacity, float loadFactor)//构造指定初始容量和加载因子的空HashMap @@ -4904,7 +4871,7 @@ HashMap 继承关系如下图所示: loadFactor 太大导致查找元素效率低,存放的数据拥挤,太小导致数组的利用率低,存放的数据会很分散。loadFactor 的默认值为 **0.75f 是官方给出的一个比较好的临界值** - * threshold 计算公式:capacity(数组长度默认16) * loadFactor(默认 0.75)。当 size>=threshold 的时候,那么就要考虑对数组的 resize(扩容),这就是衡量数组是否需要扩增的一个标准, 扩容后的 HashMap 容量是之前容量的**两倍** + * threshold 计算公式:capacity(数组长度默认16) * loadFactor(默认 0.75)。当 size >= threshold 的时候,那么就要考虑对数组的 resize(扩容),这就是衡量数组是否需要扩增的一个标准, 扩容后的 HashMap 容量是之前容量的**两倍** @@ -5013,8 +4980,8 @@ HashMap 继承关系如下图所示: ```java static final int hash(Object key) { int h; - // 1)如果key等于null:可以看到当key等于null的时候也是有哈希值的,返回的是0. - // 2)如果key不等于null:首先计算出key的hashCode赋值给h,然后与h无符号右移16位后的二进制进行按位异或得到最后的hash值 + // 1)如果key等于null:可以看到当key等于null的时候也是有哈希值的,返回的是0 + // 2)如果key不等于null:首先计算出key的hashCode赋值给h,然后与h无符号右移16位后的二进制进行按位异或 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } ``` @@ -5096,8 +5063,6 @@ HashMap 继承关系如下图所示: 2. 如果是树形化遍历桶中的元素,创建相同个数的树形节点,复制内容,建立起联系,类似单向链表转换为双向链表 3. 让桶中的第一个元素即数组中的元素指向新建的红黑树的节点,以后这个桶里的元素就是红黑树而不是链表数据结构了 - - * tableSizeFor():创建 HashMap 指定容量时,HashMap 通过位移运算和或运算得到比指定初始化容量大的最小的 2 的 n 次幂 ```java @@ -5678,15 +5643,11 @@ TreeMap 集合指定大小规则有 2 种方式: #### WeakMap -WeakHashMap 是基于弱引用的 - -内部的 Entry 继承 WeakReference,被弱引用关联的对象在**下一次垃圾回收时会被回收**,并且构造方法传入引用队列,用来在清理对象完成以后清理引用 +WeakHashMap 是基于弱引用的,内部的 Entry 继承 WeakReference,被弱引用关联的对象在**下一次垃圾回收时会被回收**,并且构造方法传入引用队列,用来在清理对象完成以后清理引用 ```java private static class Entry extends WeakReference implements Map.Entry { - Entry(Object key, V value, - ReferenceQueue queue, - int hash, Entry next) { + Entry(Object key, V value, ReferenceQueue queue, int hash, Entry next) { super(key, queue); this.value = value; this.hash = hash; @@ -5758,7 +5719,7 @@ Tomcat 中的 ConcurrentCache 使用了 WeakHashMap 来实现缓存功能,Conc 注意: * JDK 1.7 开始之后,泛型后面的申明可以省略不写 -* **泛型和集合都只能支持引用数据类型,不支持基本数据类型。** +* **泛型和集合都只能支持引用数据类型,不支持基本数据类型** ```java ArrayList lists = new ArrayList<>(); @@ -5930,7 +5891,7 @@ class Dog{} ### 基本介绍 -异常:程序在编译或者执行的过程中可能出现的问题,Java 为常见的代码异常都设计一个类来代表。 +异常:程序在编译或者执行的过程中可能出现的问题,Java 为常见的代码异常都设计一个类来代表 错误:Error ,程序员无法处理的错误,只能重启系统,比如内存奔溃,JVM 本身的奔溃 @@ -5943,7 +5904,6 @@ Java 中异常继承的根类是:Throwable Error Exception(异常,需要研究和处理) / \ 编译时异常 RuntimeException(运行时异常) - ``` Exception 异常的分类: @@ -6016,7 +5976,7 @@ public static void main(String[] args) throws ParseException { 在出现编译时异常的地方层层把异常抛出去给调用者,调用者最终抛出给 JVM 虚拟机,JVM 虚拟机输出异常信息,直接终止掉程序,这种方式与默认方式是一样的 -**Exception是异常最高类型可以抛出一切异常** +**Exception 是异常最高类型可以抛出一切异常** ```java public static void main(String[] args) throws Exception { @@ -6038,7 +5998,7 @@ public static void main(String[] args) throws Exception { 可以处理异常,并且出现异常后代码也不会死亡 -* 自己捕获异常和处理异常的格式:**捕获处理** +* 捕获异常和处理异常的格式:**捕获处理** ```java try{ @@ -6129,35 +6089,6 @@ public class ExceptionDemo{ 5. 算术异常(数学操作异常):ArithmeticException 6. 数字转换异常:NumberFormatException -```java -public class ExceptionDemo { - public static void main(String[] args) { - System.out.println("程序开始。。。。。。"); - // 1.数组索引越界异常: ArrayIndexOutOfBoundsException。 - int[] arrs = {10 ,20 ,30}; - System.out.println(arrs[3]); //出现了数组索引越界异常。代码在此处直接执行死亡! - - // 2.空指针异常 : NullPointerException。 - String name = null ; - System.out.println(name); // 直接输出没有问题 - System.out.println(name.length());//出现了空指针异常。代码直接执行死亡! - - /** 3.类型转换异常:ClassCastException。 */ - Object o = "齐天大圣"; - Integer s = (Integer) o; // 此处出现了类型转换异常。代码在此处直接执行死亡! - - /** 5.数学操作异常:ArithmeticException。 */ - int c = 10 / 0 ; // 此处出现了数学操作异常。代码在此处直接执行死亡! - - /** 6.数字转换异常: NumberFormatException。 */ - String num = "23aa"; - Integer it = Integer.valueOf(num); //出现了数字转换异常。代码在此处执行死亡! - - System.out.println("程序结束。。。。。。"); - } -} -``` - **** @@ -6168,7 +6099,7 @@ public class ExceptionDemo { 运行时异常在编译阶段是不会报错,在运行阶段才会出错,运行时出错了程序还是会停止,运行时异常也建议要处理,运行时异常是自动往外抛出的,不需要手工抛出 -**运行时异常的处理规范**:直接在最外层捕获处理即可,底层会自动抛出!! +**运行时异常的处理规范**:直接在最外层捕获处理即可,底层会自动抛出 ```java public class ExceptionDemo{ @@ -6196,7 +6127,7 @@ public class ExceptionDemo{ ### Finally -用在捕获处理的异常格式中的,放在最后面。 +用在捕获处理的异常格式中的,放在最后面 ```java try{ @@ -6267,9 +6198,9 @@ public class FinallyDemo { * 自定义编译时异常:定义一个异常类继承 Exception,重写构造器,在出现异常的地方用 throw new 自定义对象抛出 * 自定义运行时异常:定义一个异常类继承 RuntimeException,重写构造器,在出现异常的地方用 throw new 自定义对象抛出 -**throws:用在方法上,用于抛出方法中的异常** +throws:用在方法上,用于抛出方法中的异常 -**throw: 用在出现异常的地方,创建异常对象且立即从此处抛出** +throw: 用在出现异常的地方,创建异常对象且立即从此处抛出 ```java //需求:认为年龄小于0岁,大于200岁就是一个异常。 @@ -6318,7 +6249,7 @@ public class AgeIllegalRuntimeException extends RuntimeException{ 1. 运行时异常被抛出可以不处理,可以自动抛出;**编译时异常必须处理**;按照规范都应该处理 2. **重写方法申明抛出的异常,子类方法抛出的异常类型必须是父类抛出异常类型或为其子类型** 3. 方法默认都可以自动抛出运行时异常, throws RuntimeException 可以省略不写 -4. 当多异常处理时,捕获处理,前面的异常类不能是后面异常类的父类。 +4. 当多异常处理时,捕获处理,前面的异常类不能是后面异常类的父类 5. 在 try/catch 后可以追加 finally 代码块,其中的代码一定会被执行,通常用于资源回收操作 异常的作用: @@ -6366,11 +6297,11 @@ Lambda 表达式是 JDK1.8 开始之后的新技术,是一种代码的新语 作用:为了简化匿名内部类的代码写法 -Lambda 表达式的格式: +Lambda 表达式的格式: ```java (匿名内部类被重写方法的形参列表) -> { - //被重写方法的方法体代码。 + //被重写方法的方法体代码 } ``` @@ -6392,13 +6323,13 @@ Lambda 表达式的省略写法(进一步在 Lambda 表达式的基础上继 * 如果 Lambda 表达式的方法体代码只有一行代码,可以省略大括号不写,同时要省略分号;如果这行代码是 return 语句,必须省略 return 不写 * 参数类型可以省略不写 -* 如果只有一个参数,参数类型可以省略,同时()也可以省略 +* 如果只有一个参数,参数类型可以省略,同时 `()` 也可以省略 ```java List names = new ArrayList<>(); -names.add("胡"); -names.add("甘"); -names.add("洪"); +names.add("a"); +names.add("b"); +names.add("c"); names.forEach(new Consumer() { @Override @@ -6634,8 +6565,6 @@ public class ConstructorDemo { Stream 流其实就是一根传送带,元素在上面可以被 Stream 流操作 -作用: - * 可以解决已有集合类库或者数组 API 的弊端 * Stream 流简化集合和数组的操作 * 链式编程 @@ -6706,7 +6635,7 @@ public class StreamDemo { public static void main(String[] args) { List list = new ArrayList<>(); list.add("张无忌"); list.add("周芷若"); list.add("赵敏"); - list.add("张强"); list.add("张三丰"); list.add("张三丰"); + list.add("张三"); list.add("张三丰"); list.add("张"); //取以张开头并且名字是三位数的 list.stream().filter(s -> s.startsWith("张") .filter(s -> s.length == 3).forEach(System.out::println); @@ -6719,7 +6648,7 @@ public class StreamDemo { list.stream().filter(s -> s.length == 3).skip(2).forEach(...); // 需求:把名称都加上“张三的:+xxx” - list.stream().map(s -> "张三的"+s).forEach(System.out::println); + list.stream().map(s -> "张三的" + s).forEach(System.out::println); // 需求:把名称都加工厂学生对象放上去!! // list.stream().map(name -> new Student(name)); list.stream.map(Student::new).forEach(System.out::println); @@ -6767,7 +6696,7 @@ list.stream().filter(s -> s.startsWith("张")) 收集 Stream:把 Stream 流的数据转回到集合中去 -* Stream流:工具 +* Stream 流:工具 * 集合:目的 Stream 收集方法:`R collect(Collector collector)` 把结果收集到集合中 @@ -6817,10 +6746,10 @@ File 类构造器: * `public File(String pathname)`:根据路径获取文件对象 * `public File(String parent , String child)`:根据父路径和文件名称获取文件对象 -File 类创建文件对象的格式: +File 类创建文件对象的格式: * `File f = new File("绝对路径/相对路径");` - * 绝对路径:从磁盘的的盘符一路走到目的位置的路径。 + * 绝对路径:从磁盘的的盘符一路走到目的位置的路径 * 绝对路径依赖具体的环境,一旦脱离环境,代码可能出错 * 一般是定位某个操作系统中的某个文件对象 * **相对路径**:不带盘符的(重点) @@ -6972,9 +6901,9 @@ public class FileDemo { #### 遍历目录 -- `public String[] list()`:获取当前目录下所有的"一级文件名称"到一个字符串数组中去返回。 -- `public File[] listFiles()`:获取当前目录下所有的"一级文件对象"到一个**文件对象数组**中去返回(**重点**) -- `public long lastModified`:返回此抽象路径名表示的文件上次修改的时间。 +- `public String[] list()`:获取当前目录下所有的一级文件名称到一个字符串数组中去返回 +- `public File[] listFiles()`:获取当前目录下所有的一级文件对象到一个**文件对象数组**中去返回(**重点**) +- `public long lastModified`:返回此抽象路径名表示的文件上次修改的时间 ```java public class FileDemo { @@ -7061,7 +6990,7 @@ public static void searchFiles(File dir , String fileName){ 字符集:为字符编制的一套编号规则 -计算机的底层是不能直接存储字符的,只能存储二进制,010101 +计算机的底层是不能直接存储字符的,只能存储二进制 010101 ASCII 编码:8 个开关一组就可以编码字符,1 个字节 2^8 = 256, 一个字节存储一个字符完全够用,英文和数字在底层存储都是采用 1 个字节存储的 @@ -7080,7 +7009,7 @@ B 66 美国人:收集全球所有的字符,统一编号,这套编码叫 Unicode 编码(万国码),一个英文等于两个字节,一个中文(含繁体)等于两个字节,中文标点占两个字节,英文标点占两个字节 -* UTF-8 是变种形式,也必须兼容ASCII编码表 +* UTF-8 是变种形式,也必须兼容 ASCII 编码表 * UTF-8 一个中文一般占 3 个字节,中文标点占 3 个,英文字母和数字 1 个字节 编码前与编码后的编码集必须一致才不会乱码 @@ -7145,7 +7074,7 @@ FileInputStream 文件字节输入流:以内存为基准,把磁盘文件中 方法: -* `public int read()`:每次读取一个字节返回,读取完毕会返回-1 +* `public int read()`:每次读取一个字节返回,读取完毕会返回 -1 * `public int read(byte[] buffer)`:从字节输入流中读取字节到字节数组中去,返回读取的字节数量,没有字节可读返回 -1,**byte 中新读取的数据默认是覆盖原数据**,构造 String 需要设定长度 * `public String(byte[] bytes,int offset,int length)`:构造新的 String * `public long transferTo(OutputStream out) `:从输入流中读取所有字节,并按读取的顺序,将字节写入给定的输出流 @@ -7603,7 +7532,7 @@ public class InputStreamReaderDemo{ // 1.提取GBK文件的原始字节流 InputStream is = new FileInputStream("D:\\seazean\\Netty.txt"); // 2.把原始字节输入流通过转换流,转换成 字符输入转换流InputStreamReader - InputStreamReader isr = new InputStreamReader(is,"GBK"); + InputStreamReader isr = new InputStreamReader(is, "GBK"); // 3.包装成缓冲流 BufferedReader br = new BufferedReader(isr); //循环读取 @@ -7649,7 +7578,7 @@ osw.close(); ##### 基本介绍 -对象序列化:把 Java 对象转换成字节序列的过程,将对象写入到 IO 流中。对象 => 文件中 +对象序列化:把 Java 对象转换成字节序列的过程,将对象写入到 IO 流中,对象 => 文件中 对象反序列化:把字节序列恢复为 Java 对象的过程,从 IO 流中恢复对象,文件中 => 对象 @@ -7671,7 +7600,7 @@ transient 关键字修饰的成员变量,将不参与序列化 序列化方法:`public final void writeObject(Object obj)` -注意:对象如果想参与序列化,对象必须实现序列化接口 **implements Serializable** ,否则序列化失败! +注意:对象如果想参与序列化,对象必须实现序列化接口 **implements Serializable** ,否则序列化失败 ```java public class SerializeDemo01 { @@ -7861,7 +7790,7 @@ Properties 方法: | String getProperty(String key) | 使用此属性列表中指定的键搜索属性 | | Set stringPropertyNames() | 所有键的名称的集合 | | synchronized void load(Reader r) | 从输入字符流读取属性列表(键和元素对) | -| synchronized void load(InputStream inStream) | 加载属性文件的数据到属性集对象中去 | +| synchronized void load(InputStream in) | 加载属性文件的数据到属性集对象中去 | | void store(Writer w, String comments) | 将此属性列表(键和元素对)写入 Properties 表 | | void store(OutputStream os, String comments) | 保存数据到属性文件中去 | @@ -7936,7 +7865,7 @@ public static void main(String[] args) throws Exception { ### Commons -commons-io 是 apache 提供的一组有关 IO 操作的类库,可以挺提高 IO 功能开发的效率。 +commons-io 是 apache 提供的一组有关 IO 操作的类库,可以提高 IO 功能开发的效率 commons-io 工具包提供了很多有关 IO 操作的类: @@ -8438,7 +8367,7 @@ public class ReflectDemo { // b.从ArrayList的Class对象中定位add方法 Method add = c.getDeclaredMethod("add", Object.class); // c.触发scores集合对象中的add执行(运行阶段,泛型不能约束了) - add.invoke(scores,"波仔"); + add.invoke(scores, "字符串"); System.out.println(scores); } } @@ -8667,9 +8596,9 @@ public class AnnotationDemo{ } } -@Book(value = "《Java基础到精通》", price = 99.5, authors = {"波仔","波妞"}) +@Book(value = "《Java基础到精通》", price = 99.5, authors = {"张三","李四"}) class BookStore{ - @Book(value = "《Mybatis持久层框架》", price = 199.5, authors = {"dlei","播客"}) + @Book(value = "《Mybatis持久层框架》", price = 199.5, authors = {"王五","小六"}) public void run(){ } } @@ -8684,47 +8613,6 @@ class BookStore{ -*** - - - -### 注解模拟 - -注解模拟写一个 Junit 框架的基本使用 - -1. 定义一个自定义注解 MyTest,只能注解方法,存活范围一直都在。 -2. 定义若干个方法,只要有 @MyTest 注解的方法就能被触发执行,没有这个注解的方法不能执行!! - -```java -public class TestDemo{ - @MyTest - public void test01(){System.out.println("===test01===");} - public void test02(){System.out.println("===test02===");} - @MyTest - public void test03(){System.out.println("===test03===");} - @MyTest - public void test04(){System.out.println("===test04===");} - - public static void main(String[] args) throws Exception { - TestDemo t = new TestDemo(); - Class c = TestDemo.class; - Method[] methods = c.getDeclaredMethods(); - for (Method method : methods) { - if(method.isAnnotationPresent(MyTest.class)){ - method.invoke(t); - } - } - } -} - -@Target(ElementType.METHOD) // 只能注解方法! -@Retention(RetentionPolicy.RUNTIME) // 一直都活着 -@interface MyTest{ -} -``` - - - **** @@ -8879,8 +8767,6 @@ XML 文件中常见的组成元素有:文档声明、元素、属性、注释、 #### DTD -##### DTD 定义 - DTD 是文档类型定义(Document Type Definition)。DTD 可以定义在 XML 文档中出现的元素、这些元素出现的次序、它们如何相互嵌套以及 XML 文档结构的其它详细信息。 DTD 规则: @@ -8964,135 +8850,24 @@ DTD 规则: * 代码 ```dtd - - id ID #REQUIRED - 编号 CDATA #IMPLIED - 出版社 (清华|北大|传智播客) "传智播客" - type CDATA #FIXED "IT" + + id ID #REQUIRED + 编号 CDATA #IMPLIED + 出版社 (清华|北大) "清华" + type CDATA #FIXED "IT" > ``` -**** - - - -##### DTD 引入 - -* 引入本地 dtd - - ```dtd - - ``` - -* 在 xml 文件内部引入 - - ```dtd - - ``` - -* 引入网络 dtd - - ```dtd - - ``` - -```dtd - - - - - -``` - -```xml - - - - - 张三 - 23 - - - -``` - -```xml-dtd - - - - - - - ]> - - - - 张三 - 23 - - -``` - -```dtd - - - - - - 张三 - 23 - - -``` - - - -*** - - - -##### DTD 实现 - -persondtd.dtd 文件 - -```dtd - - - - - -``` - -```xml-dtd - - - - - - 张三 - 23 - - - - 张三 - 23 - - -``` - - - *** #### Schema -##### XSD 定义 +XSD 定义: 1. Schema 语言也可作为 XSD(XML Schema Definition) 2. Schema 约束文件本身也是一个 XML 文件,符合 XML 的语法,这个文件的后缀名 .xsd @@ -9100,13 +8875,7 @@ persondtd.dtd 文件 4. dtd 里面元素类型的取值比较单一常见的是 PCDATA 类型,但是在 Schema 里面可以支持很多个数据类型 5. **Schema 文件约束 XML 文件的同时也被别的文件约束着** - - -*** - - - -##### XSD 规则 +XSD 规则: 1. 创建一个文件,这个文件的后缀名为 .xsd 2. 定义文档声明 @@ -9153,88 +8922,6 @@ person.xsd -**** - - - -##### XSD 引入 - -1. 在根标签上定义属性 xmlns="http://www.w3.org/2001/XMLSchema-instance" -2. **通过 xmlns 引入约束文件的名称空间** -3. 给某一个 xmlns 属性添加一个标识,用于区分不同的名称空间,格式为 `xmlns:标识="名称空间url"` ,标识可以是任意的,但是一般取值都是 xsi -4. 通过 xsi:schemaLocation 指定名称空间所对应的约束文件路径,格式为 `xsi:schemaLocation = "名称空间url 文件路径` - -```scheme - - - xmlns="http://www.seazean.cn/javase" - xsi:schemaLocation="http://www.seazean.cn/javase person.xsd" -> - - - 张三 - 23 - - - -``` - - - -**** - - - -##### XSD 属性 - -```scheme - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 张三 - 23 - - - -``` - *** @@ -9298,7 +8985,7 @@ public class Dom4JDemo { JavaWeb开发教程 - 张孝祥 + 张三 100.00元 @@ -9432,8 +9119,6 @@ public class Dom4JDemo { System.out.println(bookEle.elementTextTrim("name")); // 去前后空格 System.out.println(bookEle.elementText("author")); System.out.println(bookEle.elementTextTrim("author")); // 去前后空格 - System.out.println(bookEle.elementText("sale")); - System.out.println(bookEle.elementTextTrim("sale")); // 去前后空格 // 6.先获取到子元素对象,再获取该文本值 Element bookNameEle = bookEle.element("name"); @@ -9549,10 +9234,6 @@ public class XPathDemo { #### 基本介绍 -创建型模式的主要关注点是怎样创建对象,将对象的创建与使用分离,降低系统的耦合度,使用者不需要关注对象的创建细节 - -创建型模式分为:单例模式、工厂方法模式、抽象工程模式、原型模式、建造者模式 - 单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一,提供了一种创建对象的最佳方式 单例设计模式分类两种: @@ -9603,7 +9284,7 @@ public class XPathDemo { * 构造方法设置为私有,防止其他类无限创建对象,但是不能防止反射破坏 - * 静态变量初始化在类加载时完成,由 JVM 保证线程安全,能保证单例对象创建时的安全 + * 静态变量初始化在类加载时完成,**由 JVM 保证线程安全**,能保证单例对象创建时的安全 * 提供静态方法而不是直接将 INSTANCE 设置为 public,体现了更好的封装性、提供泛型支持、可以改进成懒汉单例设计 @@ -10388,7 +10069,7 @@ JVM:全称 Java Virtual Machine,即 Java 虚拟机,一种规范,本身 * Java 虚拟机基于**二进制字节码**执行,由一套字节码指令集、一组寄存器、一个栈、一个垃圾回收堆、一个方法区等组成 * JVM 屏蔽了与操作系统平台相关的信息,从而能够让 Java 程序只需要生成能够在 JVM 上运行的字节码文件,通过该机制实现的**跨平台性** -Java 代码执行流程:Java 程序 --(编译)--> 字节码文件 --(解释执行)--> 操作系统(Win,Linux) +Java 代码执行流程:`Java 程序 --(编译)--> 字节码文件 --(解释执行)--> 操作系统(Win,Linux)` JVM 结构: @@ -10396,7 +10077,7 @@ JVM 结构: JVM、JRE、JDK 对比: -* JDK(Java SE Development Kit):Java 标准开发包,它提供了编译、运行 Java 程序所需的各种工具和资源 +* JDK(Java SE Development Kit):Java 标准开发包,提供了编译、运行 Java 程序所需的各种工具和资源 * JRE( Java Runtime Environment):Java 运行环境,用于解释执行 Java 的字节码文件 @@ -10642,23 +10323,26 @@ Return Address:存放调用该方法的 PC 寄存器的值 本地方法栈是为虚拟机执行本地方法时提供服务的 -JNI:Java Native Interface,通过使用 Java 本地接口书写程序,可以确保代码在不同的平台上方便移植 +JNI:Java Native Interface,通过使用 Java 本地接口程序,可以确保代码在不同的平台上方便移植 * 不需要进行 GC,与虚拟机栈类似,也是线程私有的,有 StackOverFlowError 和 OutOfMemoryError 异常 - * 虚拟机栈执行的是 Java 方法,在 HotSpot JVM 中,直接将本地方法栈和虚拟机栈合二为一 - * 本地方法一般是由其他语言编写,并且被编译为基于本机硬件和操作系统的程序 - * 当某个线程调用一个本地方法时,就进入了不再受虚拟机限制的世界,和虚拟机拥有同样的权限 * 本地方法可以通过本地方法接口来**访问虚拟机内部的运行时数据区** * 直接从本地内存的堆中分配任意数量的内存 * 可以直接使用本地处理器中的寄存器 - - - + +原理:将本地的 C 函数(如 foo)编译到一个共享库(foo.so)中,当正在运行的 Java 程序调用 foo 时,Java 解释器利用 dlopen 接口动态链接和加载 foo.so 后再调用该函数 + +* dlopen 函数:Linux 系统加载和链接共享库 +* dlclose 函数:卸载共享库 + + + + 图片来源:https://github.com/CyC2018/CS-Notes/blob/master/notes/Java%20%E8%99%9A%E6%8B%9F%E6%9C%BA.md @@ -10730,7 +10414,7 @@ Heap 堆:是 JVM 内存中最大的一块,由所有线程共享,由垃圾 在 Java7 中堆内会存在**年轻代、老年代和方法区(永久代)**: -* Young 区被划分为三部分,Eden 区和两个大小严格相同的 Survivor 区。Survivor 区某一时刻只有其中一个是被使用的,另外一个留做垃圾回收时复制对象。在 Eden 区变满的时候, GC 就会将存活的对象移到空闲的 Survivor 区间中,根据 JVM 的策略,在经过几次垃圾回收后,仍然存活于 Survivor 的对象将被移动到 Tenured 区间 +* Young 区被划分为三部分,Eden 区和两个大小严格相同的 Survivor 区。Survivor 区某一时刻只有其中一个是被使用的,另外一个留做垃圾回收时复制对象。在 Eden 区变满的时候,GC 就会将存活的对象移到空闲的 Survivor 区间中,根据 JVM 的策略,在经过几次垃圾回收后,仍然存活于 Survivor 的对象将被移动到 Tenured 区间 * Tenured 区主要保存生命周期长的对象,一般是一些老的对象,当一些对象在 Young 复制转移一定的次数以后,对象就会被转移到 Tenured 区 * Perm 代主要保存 Class、ClassLoader、静态变量、常量、编译后的代码,在 Java7 中堆内方法区会受到 GC 的管理 @@ -10738,9 +10422,9 @@ Heap 堆:是 JVM 内存中最大的一块,由所有线程共享,由垃圾 ```java public static void main(String[] args) { - //返回Java虚拟机中的堆内存总量 + // 返回Java虚拟机中的堆内存总量 long initialMemory = Runtime.getRuntime().totalMemory() / 1024 / 1024; - //返回Java虚拟机使用的最大堆内存量 + // 返回Java虚拟机使用的最大堆内存量 long maxMemory = Runtime.getRuntime().maxMemory() / 1024 / 1024; System.out.println("-Xms : " + initialMemory + "M");//-Xms : 245M @@ -10910,7 +10594,7 @@ public class Demo1_8 extends ClassLoader { // 可以用来加载类的二进制 String b = "iloveu"; //iloveu便是字面量 ``` -* 符号引用:在编译过程中并不知道每个类的地址,因为可能这个类还没有加载,如果在一个类中引用了另一个类,无法知道它的内存地址,只能用他的类名作为符号引用,在类加载完后用这个符号引用去获取内存地址 +* 符号引用:在编译过程中并不知道每个类的地址,因为可能这个类还没有加载,如果在一个类中引用了另一个类,无法知道它的内存地址,只能用它的类名作为符号引用,在类加载完后用这个符号引用去获取内存地址 @@ -11051,7 +10735,7 @@ JVM 是将 TLAB 作为内存分配的首选,但不是所有的对象实例都 ##### 分代介绍 -Java8 时,堆被分为了两份:新生代和老年代(1:2),在 Java7 时,还存在一个永久代 +Java8 时,堆被分为了两份:新生代和老年代(1:2),在 Java7 时,还存在一个永久代 - 新生代使用:复制算法 - 老年代使用:标记 - 清除 或者 标记 - 整理 算法 @@ -11374,7 +11058,7 @@ objD.fieldG = G; // 写 * **写屏障 (Store Barrier) + SATB**:当原来成员变量的引用发生变化之前,记录下原来的引用对象 - 保留 GC 开始时的对象图,即原始快照 SATB,当 GC Roots 确定后,对象图就已经确定,那后续的标记也应该是按照这个时刻的对象图走,如果期间对白色对象有了新的引用会记录下来,并且将白色对象变灰(说明可达了),重新扫描该对象的引用关系 + 保留 GC 开始时的对象图,即原始快照 SATB,当 GC Roots 确定后,对象图就已经确定,那后续的标记也应该是按照这个时刻的对象图走,如果期间对白色对象有了新的引用会记录下来,并且将白色对象变灰(说明可达了,并且原始快照中本来就应该是灰色对象),最后重新扫描该对象的引用关系 SATB (Snapshot At The Beginning) 破坏了条件一,从而保证了不会漏标 @@ -11540,7 +11224,7 @@ Java 语言提供了对象终止(finalization)机制来允许开发人员提 - 主要不足是**只使用了内存的一半** - 对于 G1 这种分拆成为大量 region 的 GC,复制而不是移动,意味着 GC 需要维护 region 之间对象引用关系,不管是内存占用或者时间开销都不小 -现在的商业虚拟机都采用这种收集算法**回收新生代**,但是并不是划分为大小相等的两块,而是一块较大的 Eden 空间和两块较小的 Survivor 空间 +现在的商业虚拟机都采用这种收集算法**回收新生代**,因为新生代 GC 频繁并且对象的存活率不高,但是并不是划分为大小相等的两块,而是一块较大的 Eden 空间和两块较小的 Survivor 空间 @@ -11550,12 +11234,6 @@ Java 语言提供了对象终止(finalization)机制来允许开发人员提 #### 标记清除 -当成功区分出内存中存活对象和死亡对象后,GC 接下来的任务就是执行垃圾回收,释放掉无用对象所占用的内存空间,以便有足够的可用内存空间为新对象分配内存。目前在 JVM 中比较常见的三种垃圾收集算法: - -- 标记清除算法(Mark-Sweep) -- 复制算法(copying) -- 标记压缩算法(Mark-Compact) - 标记清除算法,是将垃圾回收分为两个阶段,分别是**标记和清除** - **标记**:Collector 从引用根节点开始遍历,标记所有被引用的对象,一般是在对象的 Header 中记录为可达对象,**标记的是引用的对象,不是垃圾** @@ -11594,8 +11272,7 @@ Java 语言提供了对象终止(finalization)机制来允许开发人员提 | 空间开销 | 少(但会堆积碎片) | 少(不堆积碎片) | 通常需要活对象的 2 倍大小(不堆积碎片) | | 移动对象 | 否 | 是 | 是 | -- 效率上来说,复制算法是当之无愧的老大,但是却浪费了太多内存 -- 为了尽量兼顾三个指标,标记一整理算法相对来说更平滑一些 + @@ -11654,7 +11331,7 @@ GC 性能指标: #### Serial -Serial:串行垃圾收集器,作用于新生代,是指使用单线程进行垃圾回收,采用**复制算法**,新生代基本都是复制算法,因为分区了 +Serial:串行垃圾收集器,作用于新生代,是指使用单线程进行垃圾回收,采用**复制算法**,新生代基本都是复制算法 **STW(Stop-The-World)**:垃圾回收时,只有一个线程在工作,并且 Java 应用中的所有线程都要暂停,等待垃圾回收的完成 @@ -11679,6 +11356,33 @@ Serial:串行垃圾收集器,作用于新生代,是指使用单线程进 +#### ParNew + +Par 是 Parallel 并行的缩写,New 是只能处理的是新生代 + +并行垃圾收集器在串行垃圾收集器的基础之上做了改进,**采用复制算法**,将单线程改为了多线程进行垃圾回收,可以缩短垃圾回收的时间 + +对于其他的行为(收集算法、stop the world、对象分配规则、回收策略等)同 Serial 收集器一样,应用在年轻代,除 Serial 外,只有**ParNew GC 能与 CMS 收集器配合工作** + +相关参数: + +* `-XX:+UseParNewGC`:表示年轻代使用并行收集器,不影响老年代 + +* `-XX:ParallelGCThreads`:默认开启和 CPU 数量相同的线程数 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-ParNew收集器.png) + +ParNew 是很多 JVM 运行在 Server 模式下新生代的默认垃圾收集器 + +- 对于新生代,回收次数频繁,使用并行方式高效 +- 对于老年代,回收次数少,使用串行方式节省资源(CPU 并行需要切换线程,串行可以省去切换线程的资源) + + + +*** + + + #### Parallel Parallel Scavenge 收集器是应用于新生代的并行垃圾回收器,**采用复制算法**、并行回收和 Stop the World 机制 @@ -11707,8 +11411,8 @@ Parallel Old 收集器:是一个应用于老年代的并行垃圾回收器,* * `-XX:+UseParallelGC`:手动指定年轻代使用 Paralle 并行收集器执行内存回收任务 * `-XX:+UseParalleloldcc`:手动指定老年代使用并行回收收集器执行内存回收任务 * 上面两个参数,默认开启一个,另一个也会被开启(互相激活),默认 JDK8 是开启的 -* `-XX:+UseAdaptivesizepplicy`:设置 Parallel scavenge 收集器具有**自适应调节策略**,在这种模式下,年轻代的大小、Eden 和 Survivor 的比例、晋升老年代的对象年龄等参数会被自动调整,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量 -* `-XX:ParallelGcrhreads`:设置年轻代并行收集器的线程数,一般最好与 CPU 数量相等,以避免过多的线程数影响垃圾收集性能 +* `-XX:+UseAdaptivesizepplicy`:设置 Parallel Scavenge 收集器具有**自适应调节策略**,在这种模式下,年轻代的大小、Eden 和 Survivor 的比例、晋升老年代的对象年龄等参数会被自动调整,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量 +* `-XX:ParallelGcrhreads`:设置年轻代并行收集器的线程数,一般与 CPU 数量相等,以避免过多的线程数影响垃圾收集性能 * 在默认情况下,当 CPU 数量小于 8 个,ParallelGcThreads 的值等于 CPU 数量 * 当 CPU 数量大于 8 个,ParallelGCThreads 的值等于 3+[5*CPU Count]/8] * `-XX:MaxGCPauseMillis`:设置垃圾收集器最大停顿时间(即 STW 的时间),单位是毫秒 @@ -11720,31 +11424,6 @@ Parallel Old 收集器:是一个应用于老年代的并行垃圾回收器,* -*** - - - -#### ParNew - -Par 是 Parallel 并行的缩写,New 是只能处理的是新生代 - -并行垃圾收集器在串行垃圾收集器的基础之上做了改进,**采用复制算法**,将单线程改为了多线程进行垃圾回收,可以缩短垃圾回收的时间 - -对于其他的行为(收集算法、stop the world、对象分配规则、回收策略等)同 Serial 收集器一样,应用在年轻代,除 Serial 外,只有**ParNew GC 能与 CMS 收集器配合工作** - -相关参数: - -* `-XX:+UseParNewGC`:表示年轻代使用并行收集器,不影响老年代 - -* `-XX:ParallelGCThreads`:默认开启和 CPU 数量相同的线程数 - -![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-ParNew收集器.png) - -ParNew 是很多 JVM 运行在 Server 模式下新生代的默认垃圾收集器 - -- 对于新生代,回收次数频繁,使用并行方式高效 -- 对于老年代,回收次数少,使用串行方式节省资源(CPU 并行需要切换线程,串行可以省去切换线程的资源) - **** @@ -11761,7 +11440,7 @@ CMS 收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿 - 初始标记:使用 STW 出现短暂停顿,仅标记一下 GC Roots 能直接关联到的对象,速度很快 - 并发标记:进行 GC Roots 开始遍历整个对象图,在整个回收过程中耗时最长,不需要 STW,可以与用户线程并发运行 -- 重新标记:修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,比初始标记时间长但远比并发标记时间短,需要 STW(不停顿就会一直变化,采用写屏障 + 增量更新来避免漏标情况) +- 重新标记:修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象,比初始标记时间长但远比并发标记时间短,需要 STW(不停顿就会一直变化,采用写屏障 + 增量更新来避免漏标情况) - 并发清除:清除标记为可以回收对象,**不需要移动存活对象**,所以这个阶段可以与用户线程同时并发的 Mark Sweep 会造成内存碎片,不把算法换成 Mark Compact 的原因:Mark Compact 算法会整理内存,导致用户线程使用的**对象的地址改变**,影响用户线程继续执行 @@ -11797,7 +11476,7 @@ Mark Sweep 会造成内存碎片,不把算法换成 Mark Compact 的原因:M * `-XX:ParallelCMSThreads`:设置 CMS 的线程数量 - * CMS 默认启动的线程数是(ParallelGCThreads+3)/4,ParallelGCThreads 是年轻代并行收集器的线程数 + * CMS 默认启动的线程数是 (ParallelGCThreads+3)/4,ParallelGCThreads 是年轻代并行收集器的线程数 * 收集线程占用的 CPU 资源多于25%,对用户程序影响可能较大;当 CPU 资源比较紧张时,受到 CMS 收集器线程的影响,应用程序的性能在垃圾回收阶段可能会非常糟糕 @@ -11833,7 +11512,7 @@ G1 对比其他处理器的优点: - 空间整合: - CMS:标记-清除算法、内存碎片、若干次 GC 后进行一次碎片整理 - - G1:整体来看是基于标记 - 整理算法实现的收集器,从局部(Region 之间)上来看是基于复制算法实现的,两种算法都可以避免内存碎片 + - G1:整体来看是**基于标记 - 整理算法实现**的收集器,从局部(Region 之间)上来看是基于复制算法实现的,两种算法都可以避免内存碎片 - **可预测的停顿时间模型(软实时 soft real-time)**:可以指定在 M 毫秒的时间片段内,消耗在 GC 上的时间不得超过 N 毫秒 @@ -12005,7 +11684,7 @@ ZGC 目标: ZGC 的工作过程可以分为 4 个阶段: * 并发标记(Concurrent Mark): 遍历对象图做可达性分析的阶段,也要经过初始标记和最终标记,需要短暂停顿 -* 并发预备重分配( Concurrent Prepare for Relocate):根据特定的查询条件统计得出本次收集过程要清理哪些 Region,将这些 Region 组成重分配集(Relocation Set) +* 并发预备重分配(Concurrent Prepare for Relocate):根据特定的查询条件统计得出本次收集过程要清理哪些 Region,将这些 Region 组成重分配集(Relocation Set) * 并发重分配(Concurrent Relocate): 重分配是 ZGC 执行过程中的核心阶段,这个过程要把重分配集中的存活对象复制到新的 Region 上,并为重分配集中的**每个 Region 维护一个转发表**(Forward Table),记录从旧地址到新地址的转向关系 * 并发重映射(Concurrent Remap):修正整个堆中指向重分配集中旧对象的所有引用,ZGC 的并发映射并不是一个必须要立即完成的任务,ZGC 很巧妙地把并发重映射阶段要做的工作,合并到下一次垃圾收集循环中的并发标记阶段里去完成,因为都是要遍历所有对象,这样合并节省了一次遍历的开销 @@ -12277,7 +11956,7 @@ public Object pop() { * `int[] arr = new int[10]` ```ruby - # 由于需要8位对齐,所以最终大小为56byte`。 + # 由于需要8位对齐,所以最终大小为56byte 4(Mark Word) + 4(Klass Word) + 4(length) + 4*10(10个int大小) + 4(Padding) = 56sbyte ``` @@ -12591,8 +12270,8 @@ Java 对象创建时机: 加载过程完成以下三件事: - 通过类的完全限定名称获取定义该类的二进制字节流(二进制字节码) -- 将该字节流表示的**静态存储结构转换为方法区的运行时存储结构**(Java 类模型) -- 在内存中生成一个代表该类的 Class 对象,作为该类在方法区中的各种数据的访问入口 +- 将该字节流表示的静态存储结构转换为方法区的运行时存储结构(Java 类模型) +- **将字节码文件加载至方法区后,在堆中生成一个代表该类的 Class 对象,作为该类在方法区中的各种数据的访问入口** 其中二进制字节流可以从以下方式中获取: @@ -12601,11 +12280,9 @@ Java 对象创建时机: - 由其他文件生成,例如由 JSP 文件生成对应的 Class 类 - 运行时计算生成,例如动态代理技术,在 java.lang.reflect.Proxy 使用 ProxyGenerator.generateProxyClass 生成字节码 -将字节码文件加载至方法区后,会**在堆中**创建一个 java.lang.Class 对象,用来引用位于方法区内的数据结构,该 Class 对象是在加载类的过程中创建的,每个类都对应有一个 Class 类型的对象 - 方法区内部采用 C++ 的 instanceKlass 描述 Java 类的数据结构: -* `_java_mirror` 即 java 的类镜像,例如对 String 来说就是 String.class,作用是把 class 暴露给 Java 使用 +* `_java_mirror` 即 Java 的类镜像,例如对 String 来说就是 String.class,作用是把 class 暴露给 Java 使用 * `_super` 即父类、`_fields` 即成员变量、`_methods` 即方法、`_constants` 即常量池、`_class_loader` 即类加载器、`_vtable` **虚方法表**、`_itable` 接口方法表 加载过程: @@ -12665,10 +12342,7 @@ Java 对象创建时机: ##### 准备 -准备阶段为**静态变量分配内存并设置初始值**,使用的是方法区的内存: - -* 类变量也叫静态变量,就是是被 static 修饰的变量 -* 实例变量也叫对象变量,即没加 static 的变量 +准备阶段为**静态变量(类变量)分配内存并设置初始值**,使用的是方法区的内存: 说明:实例变量不会在这阶段分配内存,它会在对象实例化时随着对象一起被分配在堆中,类加载发生在所有实例化操作之前,并且类加载只进行一次,实例化可以进行多次 @@ -12692,7 +12366,7 @@ Java 对象创建时机: public static final int value = 123; ``` -* Java 并不支持 boolean 类型,对于 boolean 类型,内部实现是 int,由于 int 的默认值是0,故 boolean 的默认值就是 false +* Java 并不支持 boolean 类型,对于 boolean 类型,内部实现是 int,由于 int 的默认值是 0,故 boolean 的默认值就是 false @@ -12704,7 +12378,7 @@ Java 对象创建时机: 将常量池中类、接口、字段、方法的**符号引用替换为直接引用**(内存地址)的过程: -* 符号引用:一组符号来描述目标,可以是任何字面量,属于编译原理方面的概念,如:包括类和接口的全限名、字段的名称和描述符、方法的名称和**方法描述符** +* 符号引用:一组符号来描述目标,可以是任何字面量,属于编译原理方面的概念,如:包括类和接口的全限名、字段的名称和描述符、方法的名称和**方法描述符**(因为类还没有加载完,很多方法是找不到的) * 直接引用:直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄,如果有了直接引用,那说明引用的目标必定已经存在于内存之中 例如:在 `com.demo.Solution` 类中引用了 `com.test.Quest`,把 `com.test.Quest` 作为符号引用存进类常量池,在类加载完后,**用这个符号引用去方法区找这个类的内存地址** @@ -12835,6 +12509,8 @@ init 指的是实例构造器,主要作用是在类实例化过程中执行, 类实例化过程:**父类的类构造器() -> 子类的类构造器() -> 父类的成员变量和实例代码块 -> 父类的构造函数 -> 子类的成员变量和实例代码块 -> 子类的构造函数** +new 关键字会创建对象并复制 dup 一个对象引用,一个调用 方法,另一个用来赋值给接收者 + *** @@ -12843,7 +12519,7 @@ init 指的是实例构造器,主要作用是在类实例化过程中执行, #### 卸载阶段 -时机:执行了 System.exit() 方法,程序正常执行结束,程序在执行过程中遇到了异常或错误而异常终止,由于操作系统出现错误而导致Java虚拟机进程终止 +时机:执行了 System.exit() 方法,程序正常执行结束,程序在执行过程中遇到了异常或错误而异常终止,由于操作系统出现错误而导致Java 虚拟机进程终止 卸载类即该类的 **Class 对象被 GC**,卸载类需要满足3个要求: @@ -12993,7 +12669,7 @@ ClassLoader 类常用方法: - **全盘加载:**当一个类加载器负责加载某个 Class 时,该 Class 所依赖和引用的其他 Class 也将由该类加载器负责载入,除非显示指定使用另外一个类加载器来载入 -- **双亲委派:**先让父类加载器加载该 Class,在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。简单来说就是,某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父加载器,**依次递归**,如果父加载器可以完成类加载任务,就成功返回;只有当父加载器无法完成此加载任务时,才自己去加载 +- **双亲委派:**某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父加载器,**依次递归**,如果父加载器可以完成类加载任务,就成功返回;只有当父加载器无法完成此加载任务时,才自己去加载 - **缓存机制:**会保证所有加载过的 Class 都会被缓存,当程序中需要使用某个 Class 时,类加载器先从缓存区中搜寻该 Class,只有当缓存区中不存在该 Class 对象时,系统才会读取该类对应的二进制数据,并将其转换成 Class 对象存入缓冲区(方法区)中 - 这就是修改了 Class 后,必须重新启动 JVM,程序所做的修改才会生效的原因 @@ -13030,7 +12706,7 @@ ClassLoader 类常用方法: } ``` - 此时执行 main 函数,会出现异常,在类 java.lang.String 中找不到 main 方法,防止恶意篡改核心 API 库。出现该信息是因为双亲委派的机制,java.lang.String 的在启动类加载器(Bootstrap)得到加载,启动类加载器优先级更高,在核心 jre 库中有其相同名字的类文件,但该类中并没有 main 方法 + 此时执行 main 函数会出现异常,在类 java.lang.String 中找不到 main 方法。因为双亲委派的机制,java.lang.String 的在启动类加载器(Bootstrap)得到加载,启动类加载器优先级更高,在核心 jre 库中有其相同名字的类文件,但该类中并没有 main 方法 双亲委派机制的缺点:检查类是否加载的委托过程是单向的,这个方式虽然从结构上看比较清晰,使各个 ClassLoader 的职责非常明确,但**顶层的 ClassLoader 无法访问底层的 ClassLoader 所加载的类**(可见性) @@ -13105,9 +12781,9 @@ protected Class loadClass(String name, boolean resolve) * 如果不想破坏双亲委派模型,只需要重写 findClass 方法 * 如果想要去破坏双亲委派模型,需要去**重写 loadClass **方法 -* 引入线程**上下文类加载器** +* 引入**线程上下文类加载器** - Java 提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现。常见的 SPI 有 JDBC、JCE、JNDI 等。这些 SPI 接口由 Java 核心库来提供,而 SPI 的实现代码则是作为 Java 应用所依赖的 jar 包被包含进类路径 classpath 里,SPI 接口中的代码需要加载具体的实现类: + Java 提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现。常见的有 JDBC、JCE、JNDI 等。这些 SPI 接口由 Java 核心库来提供,而 SPI 的实现代码则是作为 Java 应用所依赖的 jar 包被包含进类路径 classpath 里,SPI 接口中的代码需要加载具体的实现类: * SPI 的接口是 Java 核心库的一部分,是由引导类加载器来加载的 * SPI 的实现类是由系统类加载器加载,引导类加载器是无法找到 SPI 的实现类,因为双亲委派模型中 BootstrapClassloader 无法委派 AppClassLoader 来加载类 @@ -13299,7 +12975,7 @@ Java 语言:跨平台的语言(write once ,run anywhere) * 后端运行期编译器:HotSpot VM 的 C1、C2 编译器,也就是 JIT 编译器,Graal 编译器 - * JIT编译器:执行引擎部分详解 + * JIT 编译器:执行引擎部分详解 * Graal 编译器:JDK10 HotSpot 加入的一个全新的即时编译器,编译效果短短几年时间就追平了 C2 * 静态提前编译器:AOT (Ahead Of Time Compiler)编译器,直接把源代码编译成本地机器代码 @@ -13785,7 +13461,7 @@ Java 字节码属于 JVM 基本执行指令。由一个字节长度的代表某 * f 代表 float * d 代表 double -大部分的指令都没有支持 byte、char、short、boolean 类型,编译器会在编译期或运行期将 byte 和 short 类型的数据带符号扩展(Sign-Extend-)为相应的 int 类型数据,将 boolean 和 char 类型数据零位扩展(Zero-Extend-)为相应的 int 类型数据 +大部分的指令都没有支持 byte、char、short、boolean 类型,编译器会在编译期或运行期将 byte 和 short 类型的数据带符号扩展(Sign-Extend-)为相应的 int 类型数据,将 boolean 和 char 类型数据零位扩展(Zero-Extend)为相应的 int 类型数据 在做值相关操作时: @@ -13808,7 +13484,7 @@ Java 字节码属于 JVM 基本执行指令。由一个字节长度的代表某 * 指令 xload_n 表示将第 n 个局部变量压入操作数栈,aload_n 表示将一个对象引用压栈 * 指令 xload n 通过指定参数的形式,把局部变量压入操作数栈,局部变量数量超过 4 个时使用这个命令 -常量入栈指令:将常数压入操作数栈,根据数据类型和入栈内容的不同,又分为 const、push、ldc指令 +常量入栈指令:将常数压入操作数栈,根据数据类型和入栈内容的不同,又分为 const、push、ldc 指令 * push:包括 bipush 和 sipush,区别在于接收数据类型的不同,bipush 接收 8 位整数作为参数,sipush 接收 16 位整数 * ldc:如果以上指令不能满足需求,可以使用 ldc 指令,接收一个 8 位的参数,该参数指向常量池中的 int、 float 或者 String 的索引,将指定的内容压入堆栈。ldc_w 接收两个 8 位参数,能支持的索引范围更大,如果要压入的元素是 long 或 double 类型的,则使用 ldc2_w 指令 @@ -13831,7 +13507,7 @@ Java 字节码属于 JVM 基本执行指令。由一个字节长度的代表某 算术指令用于对两个操作数栈上的值进行某种特定运算,并把计算结果重新压入操作数栈 -没有直接支持 byte、 short、 char 和 boolean类型的算术指令,对于这些数据的运算,都使用 int 类型的指令来处理,数组类型也是转换成 int 数组 +没有直接支持 byte、 short、 char 和 boolean 类型的算术指令,对于这些数据的运算,都使用 int 类型的指令来处理,数组类型也是转换成 int 数组 * 加法指令:iadd、ladd、fadd、dadd * 减法指令:isub、lsub、fsub、dsub @@ -13860,7 +13536,7 @@ double j = i / 0.0; System.out.println(j);//无穷大,NaN: not a number ``` -**分析 i++**:从字节码角度分析:a++ 和 ++a 的区别是先执行 iload 还是 先执行 iinc +**分析 i++**:从字节码角度分析:a++ 和 ++a 的区别是先执行 iload 还是先执行 iinc ```java 4 iload_1 //存入操作数栈 @@ -14520,14 +14196,9 @@ public static int invoke(Object... args) { 在 JVM 中,将符号引用转换为直接引用有两种机制: - 静态链接:当一个字节码文件被装载进 JVM 内部时,如果被调用的目标方法在编译期可知,且运行期保持不变,将调用方法的符号引用转换为直接引用的过程称之为静态链接(类加载的解析阶段) -- 动态链接:如果被调用的方法在编译期无法被确定下来,只能够在程序运行期将调用方法的符号引用转换为直接引用,由于这种引用转换过程具备动态性,因此被称为动态链接(初始化后的解析阶段) - -对应方法的绑定(分配)机制:静态绑定和动态绑定。绑定是一个字段、方法或者类从符号引用被替换为直接引用的过程,仅发生一次: +- 动态链接:被调用的方法在编译期无法被确定下来,只能在程序运行期将调用方法的符号引用转换为直接引用,由于这种引用转换过程具备动态性,因此被称为动态链接(初始化后的解析阶段) -- 静态绑定:被调用的目标方法在编译期可知,且运行期保持不变,将这个方法与所属的类型进行绑定 -- 动态绑定:被调用的目标方法在编译期无法确定,只能在程序运行期根据实际的类型绑定相关的方法 - -* Java 编译器已经区分了重载的方法(静态绑定和动态绑定),因此可以认为虚拟机中不存在重载 +* 对应方法的绑定(分配)机制:静态绑定和动态绑定,编译器已经区分了重载的方法(静态绑定和动态绑定),因此可以认为虚拟机中不存在重载 非虚方法: @@ -14561,7 +14232,7 @@ public static int invoke(Object... args) { 普通调用指令: - invokestatic:调用静态方法 -- invokespecial:调用私有实例方法、构造器,和父类的实例方法或构造器,以及所实现接口的默认方法 +- invokespecial:调用私有方法、构造器,和父类的实例方法或构造器,以及所实现接口的默认方法 - invokevirtual:调用所有虚方法(虚方法分派) - invokeinterface:调用接口方法 @@ -14593,9 +14264,6 @@ public static int invoke(Object... args) { 在编译过程中,虚拟机并不知道目标方法的具体内存地址,Java 编译器会暂时用符号引用来表示该目标方法,这一符号引用包括目标方法所在的类或接口的名字,以及目标方法的方法名和方法描述符 -* 对于静态绑定的方法调用而言,实际引用是一个指向方法的指针 -* 对于需要动态绑定的方法调用而言,实际引用则是一个方法表的索引 - 符号引用存储在方法区常量池中,根据目标方法是否为接口方法,分为接口符号引用和非接口符号引用: ```java @@ -14697,8 +14365,6 @@ Java 虚拟机中关于方法重写的判定基于方法描述符,如果子类 2. 如果在类型 C 中找到与描述符和名称都相符的方法,则进行访问**权限校验**(私有的),如果通过则返回这个方法的直接引用,查找过程结束;如果不通过,则返回 java.lang.IllegalAccessError 异常 - IllegalAccessError:表示程序试图访问或修改一个属性或调用一个方法,这个属性或方法没有权限访问,一般会引起编译器异常。如果这个错误发生在运行时,就说明一个类发生了不兼容的改变 - 3. 找不到,就会按照继承关系从下往上依次对 C 的各个父类进行第二步的搜索和验证过程 4. 如果始终没有找到合适的方法,则抛出 java.lang.AbstractMethodError 异常 @@ -14722,15 +14388,15 @@ Java 虚拟机中关于方法重写的判定基于方法描述符,如果子类 虚方法表的执行过程: -* 对于静态绑定的方法调用而言,实际引用将指向具体的目标方法 -* 对于动态绑定的方法调用而言,实际引用则是方法表的索引值,也就是方法的间接地址。Java 虚拟机获取调用者的实际类型,并在该实际类型的虚方法表中,根据索引值获得目标方法内存偏移量(指针) +* 对于静态绑定的方法调用而言,实际引用是一个指向方法的指针 +* 对于动态绑定的方法调用而言,实际引用是方法表的索引值,也就是方法的间接地址。Java 虚拟机获取调用者的实际类型,并在该实际类型的虚方法表中,根据索引值获得目标方法内存偏移量(指针) 为了优化对象调用方法的速度,方法区的类型信息会增加一个指针,该指针指向一个记录该类方法的方法表。每个类中都有一个虚方法表,本质上是一个数组,每个数组元素指向一个当前类及其祖先类中非私有的实例方法 方法表满足以下的特质: * 其一,子类方法表中包含父类方法表中的**所有方法**,并且在方法表中的索引值与父类方法表种的索引值相同 -* 其二,**非重写的方法指向父类的方法表项,与父类共享一个方法表项,重写的方法指向本身自己的实现**。所以这就是为什么多态情况下可以访问父类的方法。 +* 其二,**非重写的方法指向父类的方法表项,与父类共享一个方法表项,重写的方法指向本身自己的实现**,这就是为什么多态情况下可以访问父类的方法。 @@ -14866,7 +14532,7 @@ JDK5 以后编译阶段自动转换成上述片段 #### 泛型擦除 -泛型也是在 JDK 5 开始加入的特性,但 Java 在编译泛型代码后会执行**泛型擦除**的动作,即泛型信息。在编译为字节码之后就丢失了,实际的类型都**当做了 Object 类型**来处理: +泛型也是在 JDK 5 开始加入的特性,但 Java 在编译泛型代码后会执行**泛型擦除**的动作,即泛型信息在编译为字节码之后就丢失了,实际的类型都**当做了 Object 类型**来处理: ```java List list = new ArrayList<>(); @@ -14926,7 +14592,7 @@ public static void main(String[] args) { #### foreach -**数组的循环:** +数组的循环: ```java int[] array = {1, 2, 3, 4, 5}; // 数组赋初值的简化写法也是语法糖 @@ -14944,7 +14610,7 @@ for(int i = 0; i < array.length; ++i) { } ``` -**集合的循环:** +集合的循环: ```java List list = Arrays.asList(1,2,3,4,5); @@ -15139,7 +14805,7 @@ try(资源变量 = 创建资源对象){ } ``` -其中资源对象需要实现 **AutoCloseable**接口,例如 InputStream、OutputStream、Connection、Statement、ResultSet 等接口都实现了 AutoCloseable ,使用 try-withresources可以不用写 finally 语句块,编译器会帮助生成关闭资源代码: +其中资源对象需要实现 **AutoCloseable** 接口,例如 InputStream、OutputStream、Connection、Statement、ResultSet 等接口都实现了 AutoCloseable ,使用 try-withresources可以不用写 finally 语句块,编译器会帮助生成关闭资源代码: ```java try(InputStream is = new FileInputStream("d:\\1.txt")) { @@ -17832,7 +17498,7 @@ public class MGraph { 布隆过滤器查询一个数据,是否在二进制的集合中,查询过程如下: -- 通过 K 个哈希函数计算该数据,对应计算出的 K 个hash值 +- 通过 K 个哈希函数计算该数据,对应计算出的 K 个 hash 值 - 通过 hash 值找到对应的二进制的数组下标 - 判断方法:如果存在一处位置的二进制数据是 0,那么该数据一定不存在。如果都是 1,则认为数据存在集合中(会误判) diff --git a/Prog.md b/Prog.md index 9bf9c71..2aee6d0 100644 --- a/Prog.md +++ b/Prog.md @@ -8,7 +8,7 @@ 进程的特征:并发性、异步性、动态性、独立性、结构性 -**线程**:线程是属于进程的,是一个基本的 CPU 执行单元,是程序执行流的最小单元。线程是进程中的一个实体,是系统**独立调度的基本单位**,线程本身不拥有系统资源,只拥有一点在运行中必不可少的资源,但它可与同属一个进程的其他线程共享进程所拥有的全部资源 +**线程**:线程是属于进程的,是一个基本的 CPU 执行单元,是程序执行流的最小单元。线程是进程中的一个实体,是系统**独立调度的基本单位**,线程本身不拥有系统资源,只拥有一点在运行中必不可少的资源,与同属一个进程的其他线程共享进程所拥有的全部资源 关系:一个进程可以包含多个线程,这就是多线程,比如看视频是进程,图画、声音、广告等就是多个线程 @@ -389,7 +389,7 @@ public class Test { 打断的线程会发生上下文切换,操作系统会保存线程信息,抢占到 CPU 后会从中断的地方接着运行(打断不是停止) -* sleep、wait、join 方法都会让线程进入阻塞状态,打断进程**会清空打断状态**(false) +* sleep、wait、join 方法都会让线程进入阻塞状态,打断线程**会清空打断状态**(false) ```java public static void main(String[] args) throws InterruptedException { @@ -514,7 +514,7 @@ class TwoPhaseTermination { System.out.println("执行监控记录"); // 在此被打断不会异常 } catch (InterruptedException e) { // 在睡眠期间被打断,进入异常处理的逻辑 e.printStackTrace(); - // 重新设置打断标记 + // 重新设置打断标记,打断 sleep 会清除打断状态 thread.interrupt(); } } @@ -651,7 +651,7 @@ Java 提供了线程优先级的机制,优先级会提示(hint)调度器 #### 未来优化 -内核级线程调度的成本较大,所以引入了更轻量级的协程。用户线程的调度由用户自己实现,被设计为协同式调度,所以叫协程 +内核级线程调度的成本较大,所以引入了更轻量级的协程。用户线程的调度由用户自己实现(多对一的线程模型,多**个用户线程映射到一个内核级线程**),被设计为协同式调度,所以叫协程 * 有栈协程:协程会完整的做调用栈的保护、恢复工作,所以叫有栈协程 * 无栈协程:本质上是一种有限状态机,状态保存在闭包里,比有栈协程更轻量,但是功能有限 @@ -711,13 +711,13 @@ Java 提供了线程优先级的机制,优先级会提示(hint)调度器 ### 查看线程 -windows: +Windows: * 任务管理器可以查看进程和线程数,也可以用来杀死进程 * tasklist 查看进程 * taskkill 杀死进程 -linux: +Linux: * ps -ef 查看所有进程 * ps -fT -p 查看某个进程(PID)的所有线程 @@ -1391,43 +1391,6 @@ public class Dead { } ``` -面向对象写法: - -```java -public class DeadLock { - static String lockA = "lockA"; - static String lockB = "lockB"; - public static void main(String[] args) { - new Thread(new HoldLockThread(lockA, lockB)).start(); - new Thread(new HoldLockThread(lockB, lockA)).start(); - } -} -class HoldLockThread implements Runnable { - private String lockA; - private String lockB; - - public HoldLockThread(String lockA, String lockB) { - this.lockA = lockA; - this.lockB = lockB; - } - - @Override - public void run() { - synchronized (lockA) { - System.out.println(Thread.currentThread().getName() + " 持有" + lockA + ",尝试获得" + lockB); - try { - Thread.sleep(2000); - } catch (InterruptedException e) { - e.printStackTrace(); - } - synchronized (lockB) { - System.out.println(Thread.currentThread().getName() + " 持有" + lockB + ",尝试获得" + lockA); - } - } - } -} -``` - *** @@ -1542,7 +1505,7 @@ Object 类 API: ```java public final void notify():唤醒正在等待对象监视器的单个线程。 public final void notifyAll():唤醒正在等待对象监视器的所有线程。 -public final void wait():导致当前线程等待,直到另一个线程调用该对象的notify()方法或 notifyAll()方法。 +public final void wait():导致当前线程等待,直到另一个线程调用该对象的 notify() 方法或 notifyAll()方法。 public final native void wait(long timeout):有时限的等待, 到n毫秒后结束等待,或是被唤醒 ``` @@ -1736,7 +1699,7 @@ LockSupport 出现就是为了增强 wait & notify 的功能: ```java Hashtable table = new Hashtable(); // 线程1,线程2 - if( table.get("key") == null) { + if(table.get("key") == null) { table.put("key", value); } ``` @@ -2440,19 +2403,19 @@ MESI(Modified Exclusive Shared Or Invalid)是一种广泛使用的**支持 * M:被修改(Modified) - 该缓存行只被缓存在该 CPU 的缓存中,并且是被修改过的,与主存中的数据不一致 (dirty),该缓存行中的内存需要在未来的某个时间点(其它 CPU 读取主存中相应数据之前)写回 (write back) 主存 + 该缓存行只被缓存在该 CPU 的缓存中,并且是被修改过的,与主存中的数据不一致 (dirty),该缓存行中的内存需要写回 (write back) 主存。该状态的数据再次被修改不会发送广播,因为其他核心的数据已经在第一次修改时失效一次 - 当被写回主存之后,该缓存行的状态会变成独享 (exclusive) 状态。 + 当被写回主存之后,该缓存行的状态会变成独享 (exclusive) 状态 * E:独享的(Exclusive) - 该缓存行只被缓存在该 CPU 的缓存中,是未被修改过的 (clear),与主存中数据一致,该状态可以在任何时刻有其它 CPU 读取该内存时变成共享状态 (shared) + 该缓存行只被缓存在该 CPU 的缓存中,是未被修改过的 (clear),与主存中数据一致,修改数据不需要通知其他 CPU 核心,该状态可以在任何时刻有其它 CPU 读取该内存时变成共享状态 (shared) 当 CPU 修改该缓存行中内容时,该状态可以变成 Modified 状态 * S:共享的(Shared) - 该状态意味着该缓存行可能被多个 CPU 缓存,并且各个缓存中的数据与主存数据一致,当有一个 CPU 修改该缓存行中,其它 CPU 中该缓存行变成无效状态 (Invalid) + 该状态意味着该缓存行可能被多个 CPU 缓存,并且各个缓存中的数据与主存数据一致,当 CPU 修改该缓存行中,会向其它 CPU 核心广播一个请求,使该缓存行变成无效状态 (Invalid),然后再更新当前 Cache 里的数据 * I:无效的(Invalid) @@ -2485,7 +2448,7 @@ MESI(Modified Exclusive Shared Or Invalid)是一种广泛使用的**支持 * 总线嗅探:每个处理器通过嗅探在总线上传播的数据来检查自己缓存值是否过期了,当处理器发现自己的缓存对应的内存地址的数据被修改,就**将当前处理器的缓存行设置为无效状态**,当处理器对这个数据进行操作时,会重新从内存中把数据读取到处理器缓存中 -* 总线风暴:由于 volatile 的 MESI 缓存一致性协议,需要不断的从主内存嗅探和 CAS 循环,无效的交互会导致总线带宽达到峰值;因此不要大量使用 volatile 关键字,使用 volatile、syschonized 都需要根据实际场景 +* 总线风暴:当某个 CPU 核心更新了 Cache 中的数据,要把该事件广播通知到其他核心(**写传播**),CPU 需要每时每刻监听总线上的一切活动,但是不管别的核心的 Cache 是否缓存相同的数据,都需要发出一个广播事件,不断的从主内存嗅探和 CAS 循环,无效的交互会导致总线带宽达到峰值;因此不要大量使用 volatile 关键字,使用 volatile、syschonized 都需要根据实际场景 @@ -2576,9 +2539,7 @@ volatile 修饰的变量,可以禁用指令重排 ##### 缓存一致 -使用 volatile 修饰的共享变量,总线会开启 **CPU 总线嗅探机制**来解决 JMM 缓存一致性问题,也就是共享变量在多线程中可见性的问题,实现 MESI 缓存一致性协议 - -底层是通过汇编 lock 前缀指令,共享变量加了 lock 前缀指令,在线程修改完共享变量后,会先执行**缓存锁定**的操作然后写回主存,其他的 CPU 上运行的线程根据 CPU 总线嗅探机制会修改其共享变量为失效状态,读取时会重新从主内存中读取最新的数据 +使用 volatile 修饰的共享变量,底层通过汇编 lock 前缀指令进行缓存锁定,在线程修改完共享变量后写回主存,其他的 CPU 核心上运行的线程通过 CPU 总线嗅探机制会修改其共享变量为失效状态,读取时会重新从主内存中读取最新的数据 lock 前缀指令就相当于内存屏障,Memory Barrier(Memory Fence) @@ -2636,7 +2597,7 @@ lock 前缀指令就相当于内存屏障,Memory Barrier(Memory Fence) 不能解决指令交错: -* 写屏障仅仅是保证之后的读能够读到最新的结果,但不能保证其他的读跑到写屏障之前 +* 写屏障仅仅是保证之后的读能够读到最新的结果,但不能保证其他线程的读跑到写屏障之前 * 有序性的保证也只是保证了本线程内相关代码不被重排序 @@ -2958,7 +2919,7 @@ CAS 特点: CAS 缺点: -- 循环时间长,开销大,因为执行的是循环操作,如果比较不成功一直在循环,最差的情况某个线程一直取到的值和预期值都不一样,就会无限循环导致饥饿,**使用 CAS 线程数不要超过 CPU 的核心数** +- 执行的是循环操作,如果比较不成功一直在循环,最差的情况某个线程一直取到的值和预期值都不一样,就会无限循环导致饥饿,**使用 CAS 线程数不要超过 CPU 的核心数**,采用分段 CAS 和自动迁移机制 - 只能保证一个共享变量的原子操作 - 对于一个共享变量执行操作时,可以通过循环 CAS 的方式来保证原子操作 - 对于多个共享变量操作时,循环 CAS 就无法保证操作的原子性,这个时候**只能用锁来保证原子性** @@ -2976,8 +2937,8 @@ CAS 缺点: CAS 与 synchronized 总结: -* synchronized 是从悲观的角度出发:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞(**共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程**),因此 synchronized 也称之为悲观锁,ReentrantLock 也是一种悲观锁,**性能较差** -* CAS 是从乐观的角度出发:总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据。**如果别人修改过,则获取现在最新的值,如果别人没修改过,直接修改共享数据的值**,CAS 这种机制也称之为乐观锁,**综合性能较好** +* synchronized 是从悲观的角度出发:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程),因此 synchronized 也称之为悲观锁,ReentrantLock 也是一种悲观锁,性能较差 +* CAS 是从乐观的角度出发:总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据。**如果别人修改过,则获取现在最新的值,如果别人没修改过,直接修改共享数据的值**,CAS 这种机制也称之为乐观锁,综合性能较好 @@ -3225,7 +3186,7 @@ LongAdder 和 LongAccumulator 区别: 相同点: -* LongAddr 与 LongAccumulator 类都是使用非阻塞算法 CAS 实现的, +* LongAddr 与 LongAccumulator 类都是使用非阻塞算法 CAS 实现的 * LongAddr 类是 LongAccumulator 类的一个特例,只是 LongAccumulator 提供了更强大的功能,可以自定义累加规则,当accumulatorFunction 为 null 时就等价于 LongAddr 不同点: @@ -3507,7 +3468,7 @@ ABA 问题:当进行获取主内存值时,该内存值在写入主内存时 * `public AtomicStampedReference(V initialRef, int initialStamp)`:初始值和初始版本号 * 常用API: - * ` public boolean compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp)`:期望引用和期望版本号都一致才进行 CAS 修改数据 + * ` public boolean compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp)`:**期望引用和期望版本号都一致**才进行 CAS 修改数据 * `public void set(V newReference, int newStamp)`:设置值和版本号 * `public V getReference()`:返回引用的值 * `public int getStamp()`:返回当前版本号 @@ -3632,7 +3593,10 @@ public class TestFinal { final 变量的赋值通过 putfield 指令来完成,在这条指令之后也会加入写屏障,保证在其它线程读到它的值时不会出现为 0 的情况 -其他线程访问 final 修饰的变量**会复制一份放入栈中**,效率更高 +其他线程访问 final 修饰的变量 + +* **复制一份放入栈中**直接访问,效率高 +* 大于 short 最大值会将其复制到类的常量池,访问时从常量池获取 @@ -4461,9 +4425,9 @@ ThreadLocal 内部解决方法:在 ThreadLocalMap 中的 set/getEntry 方法 ##### 基本使用 -父子线程:**创建子线程的线程是父线程**,比如实例中的 main 线程就是父线程 +父子线程:创建子线程的线程是父线程,比如实例中的 main 线程就是父线程 -ThreadLocal 中存储的是线程的局部变量,如果想实现线程间局部变量传递可以使用 InheritableThreadLocal 类 +ThreadLocal 中存储的是线程的局部变量,如果想**实现线程间局部变量传递**可以使用 InheritableThreadLocal 类 ```java public static void main(String[] args) { @@ -4607,8 +4571,8 @@ java.util.concurrent.BlockingQueue 接口有以下阻塞队列的实现:**FIFO 与普通队列(LinkedList、ArrayList等)的不同点在于阻塞队列中阻塞添加和阻塞删除方法,以及线程安全: -* 阻塞添加 take():当阻塞队列元素已满时,添加队列元素的线程会被阻塞,直到队列元素不满时才重新唤醒线程执行 -* 阻塞删除 put():在队列元素为空时,删除队列元素的线程将被阻塞,直到队列不为空再执行删除操作(一般会返回被删除的元素) +* 阻塞添加 put():当阻塞队列元素已满时,添加队列元素的线程会被阻塞,直到队列元素不满时才重新唤醒线程执行 +* 阻塞删除 take():在队列元素为空时,删除队列元素的线程将被阻塞,直到队列不为空再执行删除操作(一般会返回被删除的元素) @@ -4645,7 +4609,7 @@ java.util.concurrent.BlockingQueue 接口有以下阻塞队列的实现:**FIFO ##### 入队出队 -LinkedBlockingQueue源码: +LinkedBlockingQueue 源码: ```java public class LinkedBlockingQueue extends AbstractQueue @@ -5251,7 +5215,7 @@ TransferStack 类成员方法: ##### 公平实现 -TransferQueue 是公平的同步队列,采用 FIFO 的队列实现 +TransferQueue 是公平的同步队列,采用 FIFO 的队列实现,请求节点与队尾模式不同,需要与队头发生匹配 TransferQueue 类成员变量: @@ -5602,7 +5566,7 @@ Executors 提供了四种线程池的创建:newCachedThreadPool、newFixedThre 原因:父类不能直接调用子类中的方法,需要反射或者创建对象的方式,可以调用子类静态方法 -* Executors.newFixedThreadPool(1) 初始时为1,可以修改。对外暴露的是 ThreadPoolExecutor 对象,可以强转后调用 setCorePoolSize 等方法进行修改 +* Executors.newFixedThreadPool(1) 初始时为 1,可以修改。对外暴露的是 ThreadPoolExecutor 对象,可以强转后调用 setCorePoolSize 等方法进行修改 ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-newSingleThreadExecutor.png) @@ -5640,7 +5604,7 @@ Executors 提供了四种线程池的创建:newCachedThreadPool、newFixedThre 核心线程数常用公式: -- **CPU 密集型任务(N+1):** 这种任务消耗的是 CPU 资源,可以将核心线程数设置为 N (CPU 核心数) + 1,比 CPU 核心数多出来的一个线程是为了防止线程发生缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 某个核心就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间 +- **CPU 密集型任务 (N+1):** 这种任务消耗的是 CPU 资源,可以将核心线程数设置为 N (CPU 核心数) + 1,比 CPU 核心数多出来的一个线程是为了防止线程发生缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 某个核心就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间 CPU 密集型简单理解就是利用 CPU 计算能力的任务比如在内存中对大量数据进行分析 @@ -5687,7 +5651,7 @@ ExecutorService 类 API: | 方法 | 说明 | | ----------------------------------------------------- | ------------------------------------------------------------ | -| void shutdown() | 线程池状态变为 SHUTDOWN,等待任务执行完后关闭线程池,不会接收新任务,但已提交任务会执行完,而且也可以添加线程 | +| void shutdown() | 线程池状态变为 SHUTDOWN,等待任务执行完后关闭线程池,不会接收新任务,但已提交任务会执行完,而且也可以添加线程(不绑定任务) | | List shutdownNow() | 线程池状态变为 STOP,用 interrupt 中断正在执行的任务,直接关闭线程池,不会接收新任务,会将队列中的任务返回 | | boolean isShutdown() | 不在 RUNNING 状态的线程池,此执行者已被关闭,方法返回 true | | boolean isTerminated() | 线程池状态是否是 TERMINATED,如果所有任务在关闭后完成,返回 true | @@ -5845,7 +5809,7 @@ ThreadPoolExecutor 使用 int 的**高 3 位来表示线程池状态,低 29 成员变量 -* 线程池中存放 Worker 的容器:线程池没有初始化,直接往池中加线程即可 +* **线程池中存放 Worker 的容器**:线程池没有初始化,直接往池中加线程即可 ```java private final HashSet workers = new HashSet(); @@ -6050,7 +6014,7 @@ ThreadPoolExecutor 使用 int 的**高 3 位来表示线程池状态,低 29 * addWorker():**添加线程到线程池**,返回 true 表示创建 Worker 成功,且线程启动。首先判断线程池是否允许添加线程,允许就让线程数量 + 1,然后去创建 Worker 加入线程池 - 注意:SHUTDOWN 状态也能添加线程,但是要求新加的 Woker 没有 firstTask,而且当前 queue 不为空,所以创建一个线程来帮助执行队列中的任务 + 注意:SHUTDOWN 状态也能添加线程,但是要求新加的 Woker 没有 firstTask,而且当前 queue 不为空,所以创建一个线程来帮助线程池执行队列中的任务 ```java // core == true 表示采用核心线程数量限制,false 表示采用 maximumPoolSize @@ -6388,7 +6352,7 @@ ThreadPoolExecutor 使用 int 的**高 3 位来表示线程池状态,低 29 } ``` -* interruptIdleWorkers():shutdown 方法会**中断所有空闲线程**,根据是否可以获取 AQS 独占锁判断是否处于工作状态。线程之所以空闲是因为阻塞队列没有任务,不会中断正在运行的线程,所以 shutdown 方法会让所有的任务执行完毕。 +* interruptIdleWorkers():shutdown 方法会**中断所有空闲线程**,根据是否可以获取 AQS 独占锁判断是否处于工作状态。线程之所以空闲是因为阻塞队列没有任务,不会中断正在运行的线程,所以 shutdown 方法会让所有的任务执行完毕 ```java // onlyOne == true 说明只中断一个线程 ,false 则中断所有线程 @@ -6620,7 +6584,7 @@ FutureTask 类的成员属性: private volatile Thread runner; // 当前任务被线程执行期间,保存当前执行任务的线程对象引用 ``` -* 线程阻塞队列的头节点: +* **线程阻塞队列的头节点**: ```java // 会有很多线程去 get 当前任务的结果,这里使用了一种数据结构头插头取(类似栈)的一个队列来保存所有的 get 线程 @@ -7892,7 +7856,7 @@ AQS 核心思想: * 如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并将共享资源设置锁定状态 -* 请求的共享资源被占用,AQS 用 CLH 队列实现线程阻塞等待以及被唤醒时锁分配的机制,将暂时获取不到锁的线程加入到队列中 +* 请求的共享资源被占用,AQS 用队列实现线程阻塞等待以及被唤醒时锁分配的机制,将暂时获取不到锁的线程加入到队列中 CLH 是一种基于单向链表的**高性能、公平的自旋锁**,AQS 是将每条请求共享资源的线程封装成一个 CLH 锁队列的一个结点(Node)来实现锁的分配 @@ -8288,7 +8252,7 @@ public void lock() { } ``` -* 接下来进入 addWaiter 逻辑,构造 Node 队列,前置条件是当前线程获取锁失败,说明有线程占用了锁 +* 接下来进入 addWaiter 逻辑,构造 Node 队列(不是阻塞队列),前置条件是当前线程获取锁失败,说明有线程占用了锁 * 图中黄色三角表示该 Node 的 waitStatus 状态,其中 0 为默认**正常状态** * Node 的创建是懒惰的,其中第一个 Node 称为 **Dummy(哑元)或哨兵**,用来占位,并不关联线程 @@ -8299,7 +8263,7 @@ public void lock() { // 将当前线程关联到一个 Node 对象上, 模式为独占模式 Node node = new Node(Thread.currentThread(), mode); Node pred = tail; - // 快速入队,如果 tail 不为 null,说明存在阻塞队列 + // 快速入队,如果 tail 不为 null,说明存在队列 if (pred != null) { // 将当前节点的前驱节点指向 尾节点 node.prev = pred; @@ -8309,7 +8273,7 @@ public void lock() { return node; } } - // 初始时没有队列为空,或者 CAS 失败进入这里 + // 初始时队列为空,或者 CAS 失败进入这里 enq(node); return node; } @@ -8342,7 +8306,7 @@ public void lock() { -* 线程节点加入阻塞队列成功,进入 AbstractQueuedSynchronizer#acquireQueued 逻辑阻塞线程 +* 线程节点加入队列成功,进入 AbstractQueuedSynchronizer#acquireQueued 逻辑阻塞线程 * acquireQueued 会在一个自旋中不断尝试获得锁,失败后进入 park 阻塞 @@ -9056,7 +9020,7 @@ public static void main(String[] args) throws InterruptedException { ###### await -总体流程是将 await 线程包装成 node 节点放入 ConditionObject 的条件队列,如果被唤醒就将 node 转移到 AQS 的执行阻塞队列,等待获取锁 +总体流程是将 await 线程包装成 node 节点放入 ConditionObject 的条件队列,如果被唤醒就将 node 转移到 AQS 的执行阻塞队列,等待获取锁,**每个 Condition 对象都包含一个等待队列** * 开始 Thread-0 持有锁,调用 await,线程进入 ConditionObject 等待,直到被唤醒或打断,调用 await 方法的线程都是持锁状态的,所以说逻辑里**不存在并发** @@ -10422,7 +10386,7 @@ public static void main(String[] args) { try { // 根据是否需要超时等待选择阻塞方法 if (!timed) - // 当前线程释放掉 lock,进入到 trip 条件队列的尾部挂起自己,等待被唤醒 + // 当前线程释放掉 lock,【进入到 trip 条件队列的尾部挂起自己】,等待被唤醒 trip.await(); else if (nanos > 0L) nanos = trip.awaitNanos(nanos); @@ -10511,7 +10475,7 @@ Semaphore(信号量)用来限制能同时访问共享资源的线程上限 构造方法: * `public Semaphore(int permits)`:permits 表示许可线程的数量(state) -* `public Semaphore(int permits, boolean fair)`:fair 表示公平性,如果这个设为 true 的话,下次执行的线程会是等待最久的线程 +* `public Semaphore(int permits, boolean fair)`:fair 表示公平性,如果设为 true,下次执行的线程会是等待最久的线程 常用API: @@ -10795,7 +10759,7 @@ public class ExchangerDemo { // 创建交换对象(信使) Exchanger exchanger = new Exchanger<>(); new ThreadA(exchanger).start(); - new ThreadA(exchanger).start(); + new ThreadB(exchanger).start(); } } class ThreadA extends Thread{ @@ -11278,19 +11242,7 @@ B站视频解析:https://www.bilibili.com/video/BV1n541177Ea break; // 与 addCount 逻辑相同 else if (tab == table) { - int rs = resizeStamp(n); - if (sc < 0) { - Node[] nt; - if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 || - sc == rs + MAX_RESIZERS || (nt = nextTable) == null || - transferIndex <= 0) - break; - if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) - transfer(tab, nt); - } - else if (U.compareAndSwapInt(this, SIZECTL, sc, - (rs << RESIZE_STAMP_SHIFT) + 2)) - transfer(tab, null); + } } } @@ -11976,7 +11928,7 @@ ConcurrentHashMap 使用 get() 方法获取指定 key 的数据 } ``` -* replaceNode():替代指定的元素,会协助扩容,**增删改(写)都会协助扩容,只有查询(读)操作不会**,因为读操作不涉及加锁 +* replaceNode():替代指定的元素,会协助扩容,**增删改(写)都会协助扩容,查询(读)操作不会**,因为读操作不涉及加锁 ```java final V replaceNode(Object key, V value, Object cv) { @@ -12166,7 +12118,7 @@ public CopyOnWriteArraySet() { 适合读多写少的应用场景 -* 迭代器:CopyOnWriteArrayList 在返回迭代器时,**创建一个该内部数组当前的快照(引用)**,即使其他线程替换了原始数组,迭代器遍历的快照依然引用的是创建快照时的数组,所以这种实现方式也存在一定的数据延迟性,对其他线程并行添加的数据不可见 +* 迭代器:CopyOnWriteArrayList 在返回迭代器时,**创建一个内部数组当前的快照(引用)**,即使其他线程替换了原始数组,迭代器遍历的快照依然引用的是创建快照时的数组,所以这种实现方式也存在一定的数据延迟性,对其他线程并行添加的数据不可见 ```java public Iterator iterator() { @@ -13223,7 +13175,7 @@ final void updateHead(Node h, Node p) { * C/S 结构 :全称为 Client/Server 结构,是指客户端和服务器结构,常见程序有 QQ、IDEA 等软件 * B/S 结构 :全称为 Browser/Server 结构,是指浏览器和服务器结构 -两种架构各有优势,但是无论哪种架构,都离不开网络的支持。、 +两种架构各有优势,但是无论哪种架构,都离不开网络的支持 网络通信的三要素: @@ -13231,7 +13183,7 @@ final void updateHead(Node h, Node p) { 2. IP 地址:互联网协议地址(Internet Protocol Address),用来给一个网络中的计算机设备做唯一的编号 - * IPv4:4个字节,32 位组成,192.168.1.1 + * IPv4:4 个字节,32 位组成,192.168.1.1 * IPv6:可以实现为所有设备分配 IP,128 位 * ipconfig:查看本机的 IP @@ -13258,14 +13210,6 @@ final void updateHead(Node h, Node p) { 网络通信协议:对计算机必须遵守的规则,只有遵守这些规则,计算机之间才能进行通信 -> 应用层:应用程序(QQ、微信、浏览器),可能用到的协议(HTTP、FTP、SMTP) -> -> 传输层:TCP/IP 协议 - UDP 协议 -> -> 网络层 :IP 协议,封装自己的 IP 和对方的 IP 和端口 -> -> 数据链路层 : 进入到硬件(网) - 通信**是进程与进程之间的通信**,不是主机与主机之间的通信 TCP/IP协议:传输控制协议 (Transmission Control Protocol) @@ -13301,7 +13245,7 @@ Java 中的通信模型: 同步阻塞式性能极差:大量线程,大量阻塞 -2. 伪异步通信:引入线程池,不需要一个客户端一个线程,实现线程复用来处理很多个客户端,线程可控。 +2. 伪异步通信:引入线程池,不需要一个客户端一个线程,实现线程复用来处理很多个客户端,线程可控 高并发下性能还是很差:线程数量少,数据依然是阻塞的,数据没有来线程还是要等待 @@ -13422,7 +13366,7 @@ IO 复用让单个进程具有处理多个 I/O 事件的能力,又被称为 Ev #### 异步 IO -应用进程执行 aio_read 系统调用会立即返回,给内核传递描述符、缓冲区指针、缓冲区大小等。应用进程可以继续执行不会被阻塞,内核会在所有操作完成之后向应用进程发送信号。 +应用进程执行 aio_read 系统调用会立即返回,给内核传递描述符、缓冲区指针、缓冲区大小等。应用进程可以继续执行不会被阻塞,内核会在所有操作完成之后向应用进程发送信号 异步 I/O 与信号驱动 I/O 的区别在于,异步 I/O 的信号是通知应用进程 I/O 完成,而信号驱动 I/O 的信号是通知应用进程可以开始 I/O @@ -13440,7 +13384,7 @@ IO 复用让单个进程具有处理多个 I/O 事件的能力,又被称为 Ev ##### 函数 -Socket 不是文件,只是一个标识符,但是 Unix 操作系统把所有东西都**看作**是文件,所以 socket 说成 file descriptor,也就是 fd +Socket 不是文件,只是一个标识符,但是 Unix 操作系统把所有东西都**看作**是文件,所以 Socket 说成 file descriptor,也就是 fd select 允许应用程序监视一组文件描述符,等待一个或者多个描述符成为就绪状态,从而完成 I/O 操作。 @@ -13448,7 +13392,7 @@ select 允许应用程序监视一组文件描述符,等待一个或者多个 int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); ``` -- fd_set 使用 **bitmap 数组**实现,数组大小用 FD_SETSIZE 定义,只能监听少于 FD_SETSIZE 数量的描述符,32 位机默认是 1024 个,64 位机默认是 2048,可以对进行修改,然后重新编译内核 +- fd_set 使用 **bitmap 数组**实现,数组大小用 FD_SETSIZE 定义,**单进程**只能监听少于 FD_SETSIZE 数量的描述符,32 位机默认是 1024 个,64 位机默认是 2048,可以对进行修改,然后重新编译内核 - fd_set 有三种类型的描述符:readset、writeset、exceptset,对应读、写、异常条件的描述符集合 @@ -13702,7 +13646,7 @@ epoll 的特点: * epoll 的时间复杂度 O(1),epoll 理解为 event poll,不同于忙轮询和无差别轮询,调用 epoll_wait **只是轮询就绪链表**。当监听列表有设备就绪时调用回调函数,把就绪 fd 放入就绪链表中,并唤醒在 epoll_wait 中阻塞的进程,所以 epoll 实际上是**事件驱动**(每个事件关联上fd)的,降低了 system call 的时间复杂度 * epoll 内核中根据每个 fd 上的 callback 函数来实现,只有活跃的 socket 才会主动调用 callback,所以使用 epoll 没有前面两者的线性下降的性能问题,效率提高 -* epoll 注册新的事件都是注册到到内核中 epoll 句柄中,不需要每次调用 epoll_wait 时重复拷贝,对比前面两种,epoll 只需要将描述符从进程缓冲区向内核缓冲区**拷贝一次**,epoll 也可以利用 **mmap() 文件映射内存**加速与内核空间的消息传递(只是可以用) +* epoll 注册新的事件是注册到到内核中 epoll 句柄中,不需要每次调用 epoll_wait 时重复拷贝,对比前面两种只需要将描述符从进程缓冲区向内核缓冲区**拷贝一次**,也可以利用 **mmap() 文件映射内存**加速与内核空间的消息传递(只是可以用,并没有用) * 前面两者要把 current 往设备等待队列中挂一次,epoll 也只把 current 往等待队列上挂一次,但是这里的等待队列并不是设备等待队列,只是一个 epoll 内部定义的等待队列,这样可以节省开销 * epoll 对多线程编程更有友好,一个线程调用了 epoll_wait() 另一个线程关闭了同一个描述符,也不会产生像 select 和 poll 的不确定情况 @@ -13753,7 +13697,7 @@ epoll 的特点: * 进程描述符和用户的进程是一一对应的 * SYS_API 系统调用:如 read、write,系统调用就是 0X80 中断 -* 进程描述符 pd:进程从用户态切换到内核态时,需要保存用户态时的上下文信息在 PCB 中 +* 进程描述符 pd:进程从用户态切换到内核态时,需要**保存用户态时的上下文信息在 PCB 中** * 线程上下文:用户程序基地址,程序计数器、cpu cache、寄存器等,方便程序切回用户态时恢复现场 * 内核堆栈:**系统调用函数也是要创建变量的,**这些变量在内核堆栈上分配 @@ -13830,7 +13774,7 @@ DMA 方式是一种完全由硬件进行信息传送的控制方式,通常系 传统的 I/O 操作进行了 4 次用户空间与内核空间的上下文切换,以及 4 次数据拷贝: -* JVM 发出 read 系统调用,OS 上下文切换到内核模式(切换 1)并将数据从网卡或硬盘等设备通过 DMA 读取到内核空间缓冲区(拷贝 1) +* JVM 发出 read 系统调用,OS 上下文切换到内核模式(切换 1)并将数据从网卡或硬盘等设备通过 DMA 读取到内核空间缓冲区(拷贝 1),内核缓冲区实际上是**磁盘高速缓存(PageCache)** * OS 内核将数据复制到用户空间缓冲区(拷贝 2),然后 read 系统调用返回,又会导致一次内核空间到用户空间的上下文切换(切换 2) * JVM 处理代码逻辑并发送 write() 系统调用,OS 上下文切换到内核模式(切换3)并从用户空间缓冲区复制数据到内核空间缓冲区(拷贝3) * 将内核空间缓冲区中的数据写到 hardware(拷贝4),write 系统调用返回,导致内核空间到用户空间的再次上下文切换(切换4) @@ -13858,7 +13802,7 @@ mmap(Memory Mapped Files)内存映射加 write 实现零拷贝,**零拷贝 进行了 4 次用户空间与内核空间的上下文切换,以及 3 次数据拷贝(2 次 DMA,一次 CPU 复制): * 发出 mmap 系统调用,DMA 拷贝到内核缓冲区,映射到共享缓冲区;mmap 系统调用返回,无需拷贝 -* 发出 write 系统调用,将数据从内核缓冲区拷贝到内核 Socket 缓冲区;write系统调用返回,DMA 将内核空间 Socket 缓冲区中的数据传递到协议引擎 +* 发出 write 系统调用,将数据从内核缓冲区拷贝到内核 Socket 缓冲区;write 系统调用返回,DMA 将内核空间 Socket 缓冲区中的数据传递到协议引擎 ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/IO-mmap工作流程.png) @@ -13880,6 +13824,8 @@ sendfile 实现零拷贝,打开文件的文件描述符 fd 和 socket 的 fd 原理:数据根本不经过用户态,直接从内核缓冲区进入到 Socket Buffer,由于和用户态完全无关,就减少了两次上下文切换 +说明:零拷贝技术是不允许进程对文件内容作进一步的加工的,比如压缩数据再发送 + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/IO-sendfile工作流程.png) sendfile2.4 之后,sendfile 实现了更简单的方式,文件到达内核缓冲区后,不必再将数据全部复制到 socket buffer 缓冲区,而是只**将记录数据位置和长度相关等描述符信息**保存到 socket buffer,DMA 根据 Socket 缓冲区中描述符提供的位置和偏移量信息直接将内核空间缓冲区中的数据拷贝到协议引擎上(2 次复制 2 次切换) @@ -13905,7 +13851,7 @@ Java NIO 对 sendfile 的支持是 `FileChannel.transferTo()/transferFrom()`, 成员方法: * `static InetAddress getLocalHost()`:获得本地主机 IP 地址对象 -* `static InetAddress getByName(String host)`:根据 IP 地址字符串或主机名获得对应的IP地址对象 +* `static InetAddress getByName(String host)`:根据 IP 地址字符串或主机名获得对应的 IP 地址对象 * `String getHostName()`:获取主机名 * `String getHostAddress()`:获得 IP 地址字符串 @@ -14072,7 +14018,6 @@ TCP/IP 协议的特点: * 面向连接的协议,提供可靠交互,速度慢 * 点对点的全双工通信 -* 只能由客户端主动发送数据给服务器端,服务器端接收到数据之后,可以给客户端响应数据 * 通过**三次握手**建立连接,连接成功形成数据传输通道;通过**四次挥手**断开连接 * 基于字节流进行数据传输,传输数据大小没有限制 @@ -14562,7 +14507,7 @@ NIO 和 BIO 的比较: ### 实现原理 -NIO 三大核心部分:Channel( 通道) ,Buffer( 缓冲区), Selector( 选择器) +NIO 三大核心部分:Channel (通道)、Buffer (缓冲区)、Selector (选择器) * Buffer 缓冲区 @@ -14570,7 +14515,7 @@ NIO 三大核心部分:Channel( 通道) ,Buffer( 缓冲区), Selector( 选 * Channel 通道 - Java NIO 的通道类似流,不同的是既可以从通道中读取数据,又可以写数据到通道,流的读写通常是单向的,通道可以非阻塞读取和写入通道,支持读取或写入缓冲区,也支持异步地读写。 + Java NIO 的通道类似流,不同的是既可以从通道中读取数据,又可以写数据到通道,流的读写通常是单向的,通道可以非阻塞读取和写入通道,支持读取或写入缓冲区,也支持异步地读写 * Selector 选择器 @@ -14794,19 +14739,20 @@ Byte Buffer 有两种类型,一种是基于直接内存(也就是非堆内 Direct Memory 优点: -* Java 的 NIO 库允许 Java 程序使用直接内存,用于数据缓冲区,使用 native 函数直接分配堆外内存 +* Java 的 NIO 库允许 Java 程序使用直接内存,使用 native 函数直接分配堆外内存 * **读写性能高**,读写频繁的场合可能会考虑使用直接内存 * 大大提高 IO 性能,避免了在 Java 堆和 native 堆来回复制数据 直接内存缺点: +* 不能使用内核缓冲区 Page Cache 的缓存优势,无法缓存最近被访问的数据和使用预读功能 * 分配回收成本较高,不受 JVM 内存回收管理 * 可能导致 OutOfMemoryError 异常:OutOfMemoryError: Direct buffer memory * 回收依赖 System.gc() 的调用,但这个调用 JVM 不保证执行、也不保证何时执行,行为是不可控的。程序一般需要自行管理,成对去调用 malloc、free 应用场景: -- 有很大的数据需要存储,数据的生命周期很长 +- 传输很大的数据文件,数据的生命周期很长,导致 Page Cache 没有起到缓存的作用,一般采用直接 IO 的方式 - 适合频繁的 IO 操作,比如网络并发场景 数据流的角度: @@ -14955,8 +14901,8 @@ FileChannel 中的成员属性: * MapMode.mode:内存映像文件访问的方式,共三种: * `MapMode.READ_ONLY`:只读,修改得到的缓冲区将导致抛出异常 - * `MapMode.READ_WRITE`:读/写,对缓冲区的更改最终将写入文件,但此次修改对映射到同一文件的其他程序不一定是可见的 - * `MapMode.PRIVATE`:私用,可读可写,但是修改的内容不会写入文件,只是 buffer 自身的改变,称之为写时复制 + * `MapMode.READ_WRITE`:读/写,对缓冲区的更改最终将写入文件,但此次修改对映射到同一文件的其他程序不一定是可见 + * `MapMode.PRIVATE`:私用,可读可写,但是修改的内容不会写入文件,只是 buffer 自身的改变 * `public final FileLock lock()`:获取此文件通道的排他锁 @@ -15026,7 +14972,7 @@ public class MappedByteBufferTest { * 通道可以实现异步读写数据 * 通道可以从缓冲读数据,也可以写数据到缓冲 -2. BIO 中的 Stream 是单向的,NIO中的 Channel 是双向的,可以读操作,也可以写操作 +2. BIO 中的 Stream 是单向的,NIO 中的 Channel 是双向的,可以读操作,也可以写操作 3. Channel 在 NIO 中是一个接口:`public interface Channel extends Closeable{}` @@ -15041,7 +14987,7 @@ Channel 实现类: * SocketChannel:通过 TCP 读写网络中的数据 -* ServerSocketChannel:可以监听新进来的 TCP 连接,对每一个新进来的连接都会创建一个 SocketChannel +* ServerSocketChannel:可以**监听**新进来的 TCP 连接,对每一个新进来的连接都会创建一个 SocketChannel 提示:ServerSocketChanne 类似 ServerSocket、SocketChannel 类似 Socket @@ -15266,11 +15212,11 @@ public class ChannelTest { #### 基本介绍 -选择器(Selector) 是 SelectableChannle 对象的**多路复用器**,Selector 可以同时监控多个通道的状况,利用 Selector 可使一个单独的线程管理多个 Channel,**Selector 是非阻塞 IO 的核心**。 +选择器(Selector) 是 SelectableChannle 对象的**多路复用器**,Selector 可以同时监控多个通道的状况,利用 Selector 可使一个单独的线程管理多个 Channel,**Selector 是非阻塞 IO 的核心** ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/NIO-Selector.png) -* Selector 能够检测多个注册的通道上是否有事件发生(多个 Channel 以事件的方式可以注册到同一个 Selector),如果有事件发生,便获取事件然后针对每个事件进行相应的处理,就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求 +* Selector 能够检测多个注册的通道上是否有事件发生(多个 Channel 以事件的方式可以注册到同一个 Selector),如果有事件发生,就获取事件然后针对每个事件进行相应的处理,就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求 * 只有在连接/通道真正有读写事件发生时,才会进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程 * 避免了多线程之间的上下文切换导致的开销 @@ -15487,13 +15433,17 @@ public class Client { + + *** + + ## AIO -Java AIO(NIO.2) : AsynchronousI/O,异步非阻塞,采用了 Proactor 模式。服务器实现模式为一个有效请求一个线程,客户端的 I/O 请求都是由 OS 先完成了再通知服务器应用去启动线程进行处理。 +Java AIO(NIO.2) : AsynchronousI/O,异步非阻塞,采用了 Proactor 模式。服务器实现模式为一个有效请求一个线程,客户端的 I/O 请求都是由 OS 先完成了再通知服务器应用去启动线程进行处理 ```java AIO异步非阻塞,基于NIO的,可以称之为NIO2.0 diff --git a/README.md b/README.md index be257cf..a013bf5 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,15 @@ +## 组内直招 + +阿里巴巴 AliExpress 营销团队:https://aidc-jobs.alibaba.com/off-campus/position-detail?lang=zh&positionId=1040520 + +联系邮箱:xizan.zhy@alibaba-inc.com + + + + + +## 仓库介绍 + **Java** 学习笔记,记录作者从编程入门到深入学习的所有知识,每次学有所获都会更新笔记,排版布局**美观整洁**,希望对各位读者朋友有所帮助。 个人邮箱:imseazean@gmail.com @@ -8,7 +20,7 @@ * Frame:Maven、Netty、RocketMQ、Zookeeper * Java:JavaSE、JVM、Algorithm * Prog:Concurrent、Network Programming -* SSM:MyBatis、Spring、SpringMVC、SpringBoot +* SSM:MyBatis、Spring、SpringMVC、SpringBoot、SpringCloud * Tool:Git、Linux、Docker * Web:HTML、CSS、HTTP、Servlet、JavaScript @@ -17,4 +29,3 @@ * 推荐使用 Typora 阅读笔记,打开目录栏效果更佳。 * 所有的知识不保证权威性,如果各位朋友发现错误,欢迎与我讨论。 * 笔记的编写基于 Windows 平台,可能会因为平台的不同而造成空格、制表符的显示效果不同。 - diff --git a/SSM.md b/SSM.md index fbe9aa0..60035dc 100644 --- a/SSM.md +++ b/SSM.md @@ -2878,39 +2878,6 @@ PageInfo相关API: ## 概述 -### 框架 - -框架源自于建筑学,隶属土木工程,后发展到软件工程领域 - -软件工程框架:经过验证的,具有一定功能的,半成品软件 - -- 经过验证 - -- 具有一定功能 - -- 半成品 - -框架作用: - -* 提高开发效率 -* 增强可重用性 - -* 提供编写规范 -* 节约维护成本 -* 解耦底层实现原理 - - - -参考视频:https://space.bilibili.com/37974444 - - - -**** - - - -### Spring - Spring 是分层的 JavaSE/EE 应用 full-stack 轻量级开源框架 ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Spring-框架介绍.png) @@ -2928,6 +2895,10 @@ Spring 优点: ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Spring-体系结构.png) +参考视频:https://space.bilibili.com/37974444 + + + *** @@ -4730,9 +4701,7 @@ UserService userService = (UserService)bf.getBean("userService"); ##### FactoryBean -繁琐的 bean 初始化过程处理: - -* FactoryBean:对单一的 bean 的初始化过程进行封装,达到简化配置的目的 +FactoryBean:对单一的 bean 的初始化过程进行封装,达到简化配置的目的 FactoryBean与 BeanFactory 区别: @@ -10154,6 +10123,20 @@ Controller 加载控制:SpringMVC 的处理器对应的 bean 必须按照规 ### 注解驱动 +WebApplicationContext,生成 Spring 核心容器(主容器/父容器/根容器) + +* 父容器:Spring 环境加载后形成的容器,包含 Spring 环境下的所有的 bean +* 子容器:当前 mvc 环境加载后形成的容器,不包含 Spring 环境下的 bean +* 子容器可以访问父容器中的资源,父容器不可以访问子容器的资源 + +EnableWebMvc 注解作用: + +* 支持 ConversionService 的配置,可以方便配置自定义类型转换器 +* 支持 @NumberFormat 注解格式化数字类型 +* 支持 @DateTimeFormat 注解格式化日期数据,日期包括 Date、Calendar +* 支持 @Valid 的参数校验(需要导入 JSR-303 规范) +* 配合第三方 jar 包和 SpringMVC 提供的注解读写 XML 和 JSON 格式数据 + 纯注解开发: * 使用注解形式转化 SpringMVC 核心配置文件为配置类 java / config / SpringMVCConfiguration.java @@ -10164,12 +10147,9 @@ Controller 加载控制:SpringMVC 的处理器对应的 bean 必须按照规 type=FilterType.ANNOTATION, classes = {Controller.class} ) ) + //等同于,还不完全相同 + @EnableWebMvc public class SpringMVCConfiguration implements WebMvcConfigurer{ - //注解配置放行指定资源格式 - // @Override - // public void addResourceHandlers(ResourceHandlerRegistry registry) { - // registry.addResourceHandler("/img/**").addResourceLocations("/img/"); - // } //注解配置通用放行资源的格式 建议使用 @Override public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) { @@ -10177,14 +10157,14 @@ Controller 加载控制:SpringMVC 的处理器对应的 bean 必须按照规 } } ``` - + * 基于 servlet3.0 规范,自定义 Servlet 容器初始化配置类,加载 SpringMVC 核心配置类 ```java - //创建Servlet容器时,使用注解方式加载SPRINGMVC配置类中的信息, - //并加载成WEB专用的ApplicationContext对象该对象放入了ServletContext范围, - //在整个WEB容器中可以随时获取调用 public class ServletContainersInitConfig extends AbstractDispatcherServletInitializer { + //创建Servlet容器时,使用注解方式加载SPRINGMVC配置类中的信息, + //并加载成WEB专用的ApplicationContext对象该对象放入了ServletContext范围, + //在整个WEB容器中可以随时获取调用 @Override protected WebApplicationContext createServletApplicationContext() { A.C.W.A ctx = new AnnotationConfigWebApplicationContext(); @@ -10220,6 +10200,8 @@ Controller 加载控制:SpringMVC 的处理器对应的 bean 必须按照规 + + *** @@ -10568,38 +10550,10 @@ public String requestParam1(String name ,int age){ SpringMVC 对接收的数据进行自动类型转换,该工作通过 Converter 接口实现: -* **标量转换器** - StringToBooleanConverter String → Boolean - ObjectToStringConverter Object → String - StringToNumberConverterFactory String → Number( Integer、 Long 等) - NumberToNumberConverterFactory Number子类型之间(Integer、 Long、 Double 等) - StringToCharacterConverter String → java.lang.Character - NumberToCharacterConverter Number子类型(Integer、 Long、 Double 等)→ java.lang.Character - CharacterToNumberFactory java.lang.Character → Number子类型(Integer、Long、Double 等) - StringToEnumConverterFactory String → enum类型 - EnumToStringConverter enum类型 → String - StringToLocaleConverter String → java.util.Local - PropertiesToStringConverter java.util.Properties → String - StringToPropertiesConverter String → java.util.Properties - -* **集合、数组相关转换器** - ArrayToCollectionConverter 数组 → 集合( List、 Set) - CollectionToArrayConverter 集合( List、 Set) →数组 - ArrayToArrayConverter 数组间 - CollectionToCollectionConverter 集合间( List、 Set) - MapToMapConverter Map间 - ArrayToStringConverter 数组→String类型 - StringToArrayConverter String →数组, trim后使用“,”split - ArrayToObjectConverter 数组 → Object - ObjectToArrayConverter Object → 单元素数组 - CollectionToStringConverter 集合( List、 Set) →String - StringToCollectionConverter String → 集合( List、 Set), trim后使用“,”split - CollectionToObjectConverter 集合 → Object - ObjectToCollectionConverter Object → 单元素集合 -* **默认转换器** - ObjectToObjectConverter Object间 - IdToEntityConverter Id → Entity - FallbackObjectToStringConverter Object → String +* 标量转换器 + +* 集合、数组相关转换器 +* 默认转换器 @@ -13784,574 +13738,2732 @@ public class HelloController { +**** -*** +# Boot +## 基本介绍 -# SSM +### Boot介绍 -## XML +SpringBoot 提供了一种快速使用 Spring 的方式,基于约定优于配置的思想,可以让开发人员不必在配置与逻辑业务之间进行思维的切换,全身心的投入到逻辑业务的代码编写中,从而大大提高了开发的效率 -### 结构搭建 +SpringBoot 功能: -整合 SSM 三种框架进行项目开发 +* 自动配置,自动配置是一个运行时(更准确地说,是应用程序启动时)的过程,考虑了众多因素选择使用哪个配置,该过程是SpringBoot 自动完成的 -* 创建项目,组织项目结构,创建包 +* 起步依赖,起步依赖本质上是一个 Maven 项目对象模型(Project Object Model,POM),定义了对其他库的传递依赖,这些东西加在一起即支持某项功能。简单的说,起步依赖就是将具备某种功能的坐标打包到一起,并提供一些默认的功能 -* 创建表与实体类 +* 辅助功能,提供了一些大型项目中常见的非功能性特性,如内嵌 web 服务器、安全、指标,健康检测、外部配置等 -* 创建三层架构对应的模块、接口与实体类,建立关联关系 -* 数据层接口(代理自动创建实现类) - * 业务层接口 + 业务层实现类 - * 表现层类 +参考视频:https://www.bilibili.com/video/BV19K4y1L7MT - ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/SSM-目录结构.png) +*** -*** +### 构建工程 +普通构建: -### 数据准备 +1. 创建 Maven 项目 -* 导入坐标 pom.xml +2. 导入 SpringBoot 起步依赖 - ```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 - - - - - - - - - org.apache.tomcat.maven - tomcat7-maven-plugin - 2.1 - - 80 - / - - - - - ``` + ```xml + + + org.springframework.boot + spring-boot-starter-parent + 2.1.8.RELEASE + + + + + + org.springframework.boot + spring-boot-starter-web + + + ``` -* resources.jdbc.properties +3. 定义 Controller - ```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 - ``` + ```java + @RestController + public class HelloController { + @RequestMapping("/hello") + public String hello(){ + return " hello Spring Boot !"; + } + } + ``` -* domain +4. 编写引导类 - ```java - public class User implements Serializable { - private Integer uuid; - private String userName; - private String password; - private String realName; - private Integer gender; - private Date birthday; - } - ``` + ```java + // 引导类,SpringBoot项目的入口 + @SpringBootApplication + public class HelloApplication { + public static void main(String[] args) { + SpringApplication.run(HelloApplication.class, args); + } + } + ``` -* 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); - } - ``` +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/SpringBoot-IDEA构建工程.png) -* 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); - } - ``` - service.impl.UserServiceImpl - ```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); - } - } - ``` -* controller - ```java - public class UserController { - - } - ``` - - *** -### Mybatis -* Spring环境配置:spring-mvc.xml - ```xml - - - - - - - - ``` - -* MyBatis映射:resources.dao.UserDao.xml +## 自动装配 - ```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} - - - - - - - - - - - - ``` +### 依赖管理 + +在 spring-boot-starter-parent 中定义了各种技术的版本信息,组合了一套最优搭配的技术版本。在各种 starter 中,定义了完成该功能需要的坐标合集,其中大部分版本信息来自于父工程。工程继承 parent,引入 starter 后,通过依赖传递,就可以简单方便获得需要的 jar 包,并且不会存在版本冲突,自动版本仲裁机制 + + + +*** + + + +### 底层注解 + +#### SpringBoot + +@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 { + + 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 注解类: + +```java +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Import(UserConfig.class)//@Import注解实现Bean的动态加载 +public @interface EnableUser { +} +``` + + + + + +*** + + + +#### Configuration + +@Configuration:设置当前类为 SpringBoot 的配置类 + +* proxyBeanMethods = true:Full 全模式,每个 @Bean 方法被调用多少次返回的组件都是单实例的,默认值,类组件之间**有依赖关系**,方法会被调用得到之前单实例组件 +* proxyBeanMethods = false:Lite 轻量级模式,每个 @Bean 方法被调用多少次返回的组件都是新创建的,类组件之间**无依赖关系**用 Lite 模式加速容器启动过程 + +```java +@Configuration(proxyBeanMethods = true) +public class MyConfig { + @Bean //给容器中添加组件。以方法名作为组件的 id。返回类型就是组件类型。返回的值,就是组件在容器中的实例 + public User user(){ + User user = new User("zhangsan", 18); + return user; + } +} +``` + + + + + +*** + + + +#### Condition + +##### 条件注解 + +Condition 是 Spring4.0 后引入的条件化配置接口,通过实现 Condition 接口可以完成有条件的加载相应的 Bean + +注解:@Conditional + +作用:条件装配,满足 Conditional 指定的条件则进行组件注入,加上方法或者类上,作用范围不同 + +使用:@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(); + } + } + ``` + +* 启动类: + + ```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(); + } + ``` + +* ClassCondition + + ```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; + } + } + ``` + +* UserConfig + + ```java + @Configuration + public class UserConfig { + @Bean + @ConditionOnClass("com.alibaba.fastjson.JSON")//JSON加载了才注册 User 到容器 + public User user(){ + return new User(); + } + } + ``` + +* 测试 User 对象的创建 + + + +*** + + + +##### 常用注解 + +SpringBoot 提供的常用条件注解: + +@ConditionalOnProperty:判断**配置文件**中是否有对应属性和值才初始化 Bean + +```java +@Configuration +public class UserConfig { + @Bean + @ConditionalOnProperty(name = "it", havingValue = "seazean") + public User user() { + return new User(); + } +} +``` + +```properties +it=seazean +``` + +@ConditionalOnClass:判断环境中是否有对应类文件才初始化 Bean + +@ConditionalOnMissingClass:判断环境中是否有对应类文件才初始化 Bean + +@ConditionalOnMissingBean:判断环境中没有对应Bean才初始化 Bean + + + +***** + + + +#### ImportRes + +使用 bean.xml 文件生成配置 bean,如果需要继续复用 bean.xml,@ImportResource 导入配置文件即可 + +```java +@ImportResource("classpath:beans.xml") +public class MyConfig { + //... +} +``` + +```xml + + + + + + + + + + +``` + + + +**** + + + +#### Properties + +@ConfigurationProperties:读取到 properties 文件中的内容,并且封装到 JavaBean 中 + +配置文件: + +```properties +mycar.brand=BYD +mycar.price=100000 +``` + +JavaBean 类: + +```java +@Component //导入到容器内 +@ConfigurationProperties(prefix = "mycar")//代表配置文件的前缀 +public class Car { + private String brand; + private Integer price; +} +``` + + + +*** + + + +### 装配原理 + +#### 启动流程 + +应用启动: + +```java +@SpringBootApplication +public class BootApplication { + public static void main(String[] args) { + // 启动代码 + SpringApplication.run(BootApplication.class, args); + } +} +``` + +SpringApplication 构造方法: + +* `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 + +* `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 放入环境的属性信息头部 + + * `listeners.environmentPrepared(bootstrapContext, environment)`:运行监听器调用 environmentPrepared(),EventPublishingRunListener 发布事件通知所有的监听器当前环境准备完成 + + * `DefaultPropertiesPropertySource.moveToEnd(environment)`:移动 defaultProperties 属性源到环境中的最后一个源 + + * `bindToSpringApplication(environment)`:与容器绑定当前环境 + + * `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 容器的基本信息 + + * `postProcessApplicationContext(context)`:后置处理流程 + + * `applyInitializers(context)`:获取所有的**初始化器调用 initialize() 方法**进行初始化 + * `listeners.contextPrepared(context)`:所有的运行监听器调用 environmentPrepared() 方法,EventPublishingRunListener 发布事件通知 IOC 容器准备完成 + * `listeners.contextLoaded(context)`:所有的运行监听器调用 contextLoaded() 方法,通知 IOC 加载完成 + +* `refreshContext(context)`:**刷新 IOC 容器** + + * Spring 的容器启动流程 + * `invokeBeanFactoryPostProcessors(beanFactory)`:**实现了自动装配** + * `onRefresh()`:**创建 WebServer** 使用该接口 + +* `afterRefresh(context, applicationArguments)`:留给用户自定义容器刷新完成后的处理逻辑 + +* `stopWatch.stop()`:记录应用启动完成的时间 + +* `callRunners(context, applicationArguments)`:调用所有 runners + +* `listeners.started(context)`:所有的运行监听器调用 started() 方法 + +* `listeners.running(context)`:所有的运行监听器调用 running() 方法 + + * 获取容器中的 ApplicationRunner、CommandLineRunner + * `AnnotationAwareOrderComparator.sort(runners)`:合并所有 runner 并且按照 @Order 进行排序 + + * `callRunner()`:遍历所有的 runner,调用 run 方法 + +* `handleRunFailure(context, ex, listeners)`:**处理异常**,出现异常进入该逻辑 + + * `handleExitCode(context, exception)`:处理错误代码 + * `listeners.failed(context, exception)`:运行监听器调用 failed() 方法 + * `reportFailure(getExceptionReporters(context), exception)`:通知异常 + + + +**** + + + +#### 注解分析 + +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 的自动配置机制** + + ````java + @AutoConfigurationPackage + @Import(AutoConfigurationImportSelector.class) + public @interface EnableAutoConfiguration { + String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration"; + Class[] exclude() default {}; + String[] excludeName() default {}; + } + ```` + + * @AutoConfigurationPackage:**将添加该注解的类所在的 package 作为自动配置 package 进行管理**,把启动类所在的包设置一次,为了给各种自动配置的第三方库扫描用,比如带 @Mapper 注解的类,Spring 自身是不能识别的,但自动配置的 Mybatis 需要扫描用到,而 ComponentScan 只是用来扫描注解类,并没有提供接口给三方使用 + + ```java + @Import(AutoConfigurationPackages.Registrar.class) // 利用 Registrar 给容器中导入组件 + public @interface AutoConfigurationPackage { + String[] basePackages() default {}; //自动配置包,指定了配置类的包 + Class[] basePackageClasses() default {}; + } + ``` + + `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`:返回所有自动装配类的候选项 + + * 从 spring-boot-autoconfigure-2.5.3.jar/META-INF/spring.factories 文件中寻找 EnableAutoConfiguration 字段,获取自动装配类,**进行条件装配,按需装配** + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/SpringBoot-自动装配配置文件.png) + + + + + +*** + + + +#### 装配流程 + +Spring Boot 通过 `@EnableAutoConfiguration` 开启自动装配,通过 SpringFactoriesLoader 加载 `META-INF/spring.factories` 中的自动配置类实现自动装配,自动配置类其实就是通过 `@Conditional` 注解按需加载的配置类,想要其生效必须引入 `spring-boot-starter-xxx` 包实现起步依赖 + +* SpringBoot 先加载所有的自动配置类 xxxxxAutoConfiguration +* 每个自动配置类进行**条件装配**,默认都会绑定配置文件指定的值(xxxProperties 和配置文件进行了绑定) +* SpringBoot 默认会在底层配好所有的组件,如果用户自己配置了**以用户的优先** +* **定制化配置:** + - 用户可以使用 @Bean 新建自己的组件来替换底层的组件 + - 用户可以去看这个组件是获取的配置文件前缀值,在配置文件中修改 + +以 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"; + + @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 { } +``` + + + + + +*** + + + +### 事件监听 + +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配置信息 + } + } + ``` + +* MyCommandLineRunner + + ```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)); + } + } + ``` + +* MyApplicationContextInitializer 的启用要**在 resource 文件夹下添加 META-INF/spring.factories** + + ```properties + org.springframework.context.ApplicationContextInitializer=\ + com.example.springbootlistener.listener.MyApplicationContextInitializer + ``` + + ```java + @Component + public class MyApplicationContextInitializer implements ApplicationContextInitializer { + @Override + public void initialize(ConfigurableApplicationContext applicationContext) { + System.out.println("ApplicationContextInitializer....initialize"); + } + } + ``` + +* MySpringApplicationRunListener 的使用要添加**构造器** + + ```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...项目启动失败"); + } + } + ``` + + + + + + + +*** + + + + + +## 配置文件 + +### 配置方式 + +#### 文件类型 + +SpringBoot 是基于约定的,很多配置都有默认值,如果想使用自己的配置替换默认配置,可以使用 application.properties 或者 application.yml(application.yaml)进行配置 + +* 默认配置文件名称:application +* 在同一级目录下优先级为:properties > yml > yaml + +例如配置内置 Tomcat 的端口 + +* properties: + + ```properties + server.port=8080 + ``` + +* yml: + + ```yaml + server: port: 8080 + ``` + +* yaml: + + ```yaml + server: port: 8080 + ``` + + + + +*** + + + +#### 加载顺序 + +所有位置的配置文件都会被加载,互补配置,**高优先级配置内容会覆盖低优先级配置内容** + +扫描配置文件的位置按优先级**从高到底**: + +- `file:./config/`:**当前项目**下的 /config 目录下 + +- `file:./`:当前项目的根目录,Project工程目录 + +- `classpath:/config/`:classpath 的 /config 目录 + +- `classpath:/`:classpath 的根目录,就是 resoureces 目录 + +项目外部配置文件加载顺序:外部配置文件的使用是为了对内部文件的配合 + +* 命令行:在 package 打包后的 target 目录下,使用该命令 + + ```sh + java -jar myproject.jar --server.port=9000 + ``` + +* 指定配置文件位置 + + ```sh + java -jar myproject.jar --spring.config.location=e://application.properties + ``` + +* 按优先级从高到底选择配置文件的加载命令 + + ```sh + java -jar myproject.jar + ``` + + + + + +*** + + + +### yaml语法 + +基本语法: + +- 大小写敏感 + +- **数据值前边必须有空格,作为分隔符** + +- 使用缩进表示层级关系 + +- 缩进时不允许使用Tab键,只允许使用空格(各个系统 Tab对应空格数目可能不同,导致层次混乱) + +- 缩进的空格数目不重要,只要相同层级的元素左侧对齐即可 + +- ''#" 表示注释,从这个字符一直到行尾,都会被解析器忽略 + + ```yaml + server: + port: 8080 + address: 127.0.0.1 + ``` + +数据格式: + +* 纯量:单个的、不可再分的值 + + ```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值 + ``` + + + +*** + + + +### 获取配置 + +三种获取配置文件的方式: + +* 注解 @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 !"; + } + } + ``` + +* Evironment 对象 + + ```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 !"; + } + ``` + +* 注解 @ConfigurationProperties 配合 @Component 使用 + + **注意**:参数 prefix 一定要指定 + + ```java + @Component //不扫描该组件到容器内,无法完成自动装配 + @ConfigurationProperties(prefix = "person") + public class Person { + private String name; + private int age; + private String[] address; + } + ``` + + ```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 !"; + } + ``` + + + +*** + + + +### 配置提示 + +自定义的类和配置文件绑定一般没有提示,添加如下依赖可以使用提示: + +```xml + + org.springframework.boot + spring-boot-configuration-processor + true + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.springframework.boot + spring-boot-configuration-processor + + + + + + +``` + + + + + +*** + + + +### Profile + +@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 + ``` + + * 虚拟机参数:在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 + + + + + + + +*** + + + + + +## Web开发 + +### 功能支持 + +SpringBoot 自动配置了很多约定,大多场景都无需自定义配置 + +* 内容协商视图解析器 ContentNegotiatingViewResolver 和 BeanName 视图解析器 BeanNameViewResolver +* 支持静态资源(包括 webjars)和静态 index.html 页支持 +* 自动注册相关类:Converter、GenericConverter、Formatter +* 内容协商处理器:HttpMessageConverters +* 国际化:MessageCodesResolver + +开发规范: + +* 使用 `@Configuration` + `WebMvcConfigurer` 自定义规则,不使用 `@EnableWebMvc` 注解 +* 声明 `WebMvcRegistrations` 的实现类改变默认底层组件 +* 使用 `@EnableWebMvc` + `@Configuration` + `DelegatingWebMvcConfiguration` 全面接管 SpringMVC + + + +**** + + + +### 静态资源 + +#### 访问规则 + +默认的静态资源路径是 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 + + ``` + + 访问地址:http://localhost:8080/webjars/jquery/3.5.1/jquery.js,后面地址要按照依赖里面的包路径 + + + +**** + + + +#### 欢迎页面 + +静态资源路径下 index.html 默认作为欢迎页面,访问 http://localhost:8080 出现该页面,使用 welcome page 功能不能修改前缀 + +网页标签上的小图标可以自定义规则,把资源重命名为 favicon.ico 放在静态资源目录下即可 + + + +*** + + + +#### 源码分析 + +SpringMVC 功能的自动配置类 WebMvcAutoConfiguration: + +```java +public class WebMvcAutoConfiguration { + //当前项目的根路径 + private static final String SERVLET_LOCATION = "/"; +} +``` + +* 内部类 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"); + } + } + ``` + + WelcomePageHandlerMapping,访问 / 能访问到 index.html + + + +*** + + + +### Rest映射 + +开启 Rest 功能 + +```yaml +spring: + mvc: + hiddenmethod: + filter: + enabled: true #开启页面表单的Rest功能 +``` + +源码分析,注入了 HiddenHttpMethodFilte 解析 Rest 风格的访问: + +```java +public class WebMvcAutoConfiguration { + @Bean + @ConditionalOnMissingBean(HiddenHttpMethodFilter.class) + @ConditionalOnProperty(prefix = "spring.mvc.hiddenmethod.filter", name = "enabled") + public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() { + return new OrderedHiddenHttpMethodFilter(); + } +} +``` + +详细源码解析:SpringMVC → 基本操作 → Restful → 识别原理 + +Web 部分源码详解:SpringMVC → 运行原理 + + + +**** + + + +### 内嵌容器 + +SpringBoot 嵌入式 Servlet 容器,默认支持的 WebServe:Tomcat、Jetty、Undertow + +配置方式: + +```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()` + + + +*** + + + +### 自定义 + +#### 定制规则 + +```java +@Configuration +public class MyWebMvcConfigurer implements WebMvcConfigurer { + @Bean + public WebMvcConfigurer webMvcConfigurer() { + return new WebMvcConfigurer() { + //进行一些方法重写,来实现自定义的规则 + //比如添加一些解析器和拦截器,就是对原始容器功能的增加 + } + } + //也可以不加 @Bean,直接从这里重写方法进行功能增加 +} +``` + + + +*** + + + +#### 定制容器 + +@EnableWebMvc:全面接管 SpringMVC,所有规则全部自己重新配置 + +- @EnableWebMvc + WebMvcConfigurer + @Bean 全面接管SpringMVC + +- @Import(DelegatingWebMvcConfiguration.**class**),该类继承 WebMvcConfigurationSupport,自动配置了一些非常底层的组件,只能保证 SpringMVC 最基本的使用 + +原理:自动配置类 **WebMvcAutoConfiguration** 里面的配置要能生效,WebMvcConfigurationSupport 类不能被加载,所以 @EnableWebMvc 导致配置类失效,从而接管了 SpringMVC + +```java +@ConditionalOnMissingBean(WebMvcConfigurationSupport.class) +public class WebMvcAutoConfiguration {} +``` + +注意:一般不适用此注解 + + + + + +*** + + + + + +## 数据访问 + +### JDBC + +#### 基本使用 + +导入 starter: + +```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); + } +} +``` + + + + + +**** + + + +#### 自动配置 + +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 {} +``` + +- 底层默认配置好的连接池是:**HikariDataSource** +- 数据库连接池的配置,是容器中没有 DataSource 才自动配置的 +- 修改数据源相关的配置:spring.datasource + +相关配置: + +- DataSourceTransactionManagerAutoConfiguration: 事务管理器的自动配置 +- JdbcTemplateAutoConfiguration: JdbcTemplate 的自动配置 + - 可以修改这个配置项 @ConfigurationProperties(prefix = **"spring.jdbc"**) 来修改JdbcTemplate + - `@AutoConfigureAfter(DataSourceAutoConfiguration.class)`:在 DataSource 装配后装配 +- JndiDataSourceAutoConfiguration: jndi 的自动配置 +- XADataSourceAutoConfiguration: 分布式事务相关 + + + + + +**** + + + +### 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 {} +``` + +自动配置: + +- 扩展配置项 **spring.datasource.druid** +- DruidSpringAopConfiguration: 监控 SpringBean,配置项为 `spring.datasource.druid.aop-patterns` + +- 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 + + web-stat-filter: # 监控web + enabled: true + urlPattern: /* + exclusions: '*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*' + + + 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 + +配置项列表: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 + + + +**** + + + +### MyBatis + +#### 基本使用 + +导入坐标: + +```xml + + org.mybatis.spring.boot + mybatis-spring-boot-starter + 2.1.4 + +``` + +* 编写 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 + public class User { + private int id; + private String username; + private String password; + } + ``` + +* 编写 dao 和 mapper 文件/纯注解开发 + + dao:**@Mapper 注解必须加,使用自动装配的 package,否则在启动类指定 @MapperScan() 扫描路径(不建议)** + + ```java + @Mapper //必须加Mapper + @Repository + public interface UserXmlMapper { + public List findAll(); + } + ``` + + mapper.xml + + ```xml + + + + + + ``` + +* 纯注解开发 + + ```java + @Mapper + @Repository + public interface UserMapper { + @Select("select * from t_user") + public List findAll(); + } + ``` + + + +**** + + + +#### 自动配置 + +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 + +只需要 Mapper 继承 **BaseMapper** 就可以拥有 CRUD 功能 + + + +*** + + + +### Redis + +#### 基本使用 + +```xml + + org.springframework.boot + spring-boot-starter-data-redis + +``` + +* 配置redis相关属性 + + ```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); + } + } + ``` + + + +**** + + + +#### 自动配置 + +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; + } + + @Bean + @ConditionalOnMissingBean + @ConditionalOnSingleCandidate(RedisConnectionFactory.class) + public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) { + StringRedisTemplate template = new StringRedisTemplate(); + template.setConnectionFactory(redisConnectionFactory); + return template; + } + +} +``` + +- 配置项:`spring.redis` +- 自动导入了连接工厂配置类:LettuceConnectionConfiguration、JedisConnectionConfiguration + +- 自动注入了模板类:RedisTemplate 、StringRedisTemplate,k v 都是 String 类型 + +- 使用 @Autowired 注入模板类就可以操作 redis + + + + + +**** + + + + + +## 单元测试 + +### Junit5 + +Spring Boot 2.2.0 版本开始引入 JUnit 5 作为单元测试默认库,由三个不同的子模块组成: + +* JUnit Platform:在 JVM 上启动测试框架的基础,不仅支持 Junit 自制的测试引擎,其他测试引擎也可以接入 + +* JUnit Jupiter:提供了 JUnit5 的新的编程模型,是 JUnit5 新特性的核心,内部包含了一个测试引擎,用于在 Junit Platform 上运行 + +* JUnit Vintage:JUnit Vintage 提供了兼容 JUnit4.x、Junit3.x 的测试引擎 + + 注意:SpringBoot 2.4 以上版本移除了默认对 Vintage 的依赖,如果需要兼容 Junit4 需要自行引入 + +```java +@SpringBootTest +class Boot05WebAdminApplicationTests { + @Test + void contextLoads() { } +} +``` + + + + + +*** + + + +### 常用注解 + +JUnit5 的注解如下: + +- @Test:表示方法是测试方法,但是与 JUnit4 的 @Test 不同,它的职责非常单一不能声明任何属性,拓展的测试将会由 Jupiter 提供额外测试,包是 `org.junit.jupiter.api.Test` +- @ParameterizedTest:表示方法是参数化测试 + +- @RepeatedTest:表示方法可重复执行 +- @DisplayName:为测试类或者测试方法设置展示名称 + +- @BeforeEach:表示在每个单元测试之前执行 +- @AfterEach:表示在每个单元测试之后执行 + +- @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 | + +```java +@Test +@DisplayName("simple assertion") +public void simple() { + assertEquals(3, 1 + 2, "simple math"); + assertNull(null); + assertNotNull(new Object()); +} +``` + + + +**** + + + +#### 数组断言 + +通过 assertArrayEquals 方法来判断两个对象或原始类型的数组是否相等 + +```java +@Test +@DisplayName("array assertion") +public void array() { + assertArrayEquals(new int[]{1, 2}, new int[] {1, 2}); +} +``` + + + +*** + + + +#### 组合断言 + +assertAll 方法接受多个 org.junit.jupiter.api.Executable 函数式接口的实例作为验证的断言,可以通过 lambda 表达式提供这些断言 + +```java +@Test +@DisplayName("assert all") +public void all() { + assertAll("Math", + () -> assertEquals(2, 1 + 1), + () -> assertTrue(1 > 0) + ); +} +``` + + + +*** + + + +#### 异常断言 + +Assertions.assertThrows(),配合函数式编程就可以进行使用 + +```java +@Test +@DisplayName("异常测试") +public void exceptionTest() { + ArithmeticException exception = Assertions.assertThrows( + //扔出断言异常 + ArithmeticException.class, () -> System.out.println(1 / 0) + ); +} +``` + + + +**** + + + +#### 超时断言 + +Assertions.assertTimeout() 为测试方法设置了超时时间 + +```java +@Test +@DisplayName("超时测试") +public void timeoutTest() { + //如果测试方法时间超过1s将会异常 + Assertions.assertTimeout(Duration.ofMillis(1000), () -> Thread.sleep(500)); +} +``` + + + +**** + + + +#### 快速失败 + +通过 fail 方法直接使得测试失败 + +```java +@Test +@DisplayName("fail") +public void shouldFail() { + fail("This should fail"); +} +``` + + + + + +*** + + + +### 前置条件 + +JUnit 5 中的前置条件(assumptions)类似于断言,不同之处在于**不满足的断言会使得测试方法失败**,而不满足的**前置条件只会使得测试方法的执行终止**,前置条件可以看成是测试方法执行的前提,当该前提不满足时,就没有继续执行的必要 + +```java +@DisplayName("测试前置条件") +@Test +void testassumptions(){ + Assumptions.assumeTrue(false,"结果不是true"); + System.out.println("111111"); + +} +``` + + + +*** + + + +### 嵌套测试 + +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 { + + @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); + } + } +} +``` + + + + + +**** + + + +### 参数测试 + +参数化测试是 JUnit5 很重要的一个新特性,它使得用不同的参数多次运行测试成为了可能 + +利用**@ValueSource**等注解,指定入参,我们将可以使用不同的参数进行多次单元测试,而不需要每新增一个参数就新增一个单元测试,省去了很多冗余代码。 + +* @ValueSource:为参数化测试指定入参来源,支持八大基础类以及 String 类型、Class 类型 + +* @NullSource:表示为参数化测试提供一个 null 的入参 + +* @EnumSource:表示为参数化测试提供一个枚举入参 + +* @CsvFileSource:表示读取指定 CSV 文件内容作为参数化测试入参 + +* @MethodSource:表示读取指定方法的返回值作为参数化测试入参(注意方法返回需要是一个流) + + + + + +*** + + + + + +## 指标监控 + +### Actuator + +每一个微服务在云上部署以后,都需要对其进行监控、追踪、审计、控制等,SpringBoot 抽取了 Actuator 场景,使得每个微服务快速引用即可获得生产级别的应用监控、审计等功能 + +```xml + + org.springframework.boot + spring-boot-starter-actuator + +``` + +暴露所有监控信息为 HTTP: + +```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` | 执行线程转储 | + +应用程序是 Web 应用程序(Spring MVC,Spring WebFlux 或 Jersey),则可以使用以下附加端点: + +| 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: + +- Health:监控状况 +- Metrics:运行时指标 + +- Loggers:日志记录 + + + + + +*** -* Mybatis 核心配置:resouces.applicationContext.xml - ```xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - mysql - true - - - - - - - - - - - - - - - - - - ``` - -* 业务层接口开启事务 + + + +## 项目部署 + +SpringBoot 项目开发完毕后,支持两种方式部署到服务器: + +* jar 包 (官方推荐,默认) +* war 包 + +**更改 pom 文件中的打包方式为 war** + +* 修改启动类 ```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); + @SpringBootApplication + public class SpringbootDeployApplication extends SpringBootServletInitializer { + public static void main(String[] args) { + SpringApplication.run(SpringbootDeployApplication.class, args); + } - //根据用户名密码进行登录 - public User login(String userName, String password); + @Override + protected SpringApplicationBuilder configure(SpringApplicationBuilder b) { + return b.sources(SpringbootDeployApplication.class); + } } ``` - +* 指定打包的名称 + + ```xml + war + + springboot + + + org.springframework.boot + spring-boot-maven-plugin + + + + ``` + + + + + + + + +*** + + + + + +# Cloud + +## 基本介绍 + +SpringCloud 是分布式微服务的一站式解决方案,是多种微服务落地技术的集合体,俗称微服务全家桶 + +![Cloud-组件概览](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Cloud-组件概览.png) + + + +参考文档:https://www.yuque.com/mrlinxi/pxvr4g/wcwd39 + + + + *** -### Junit -* 单元测试整合 junit + +## 服务注册 + +### 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 - @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 + @EnableEurekaServer // 表示当前是Eureka的服务注册中心 + public class EurekaMain7001 { + public static void main(String[] args) { + SpringApplication.run(EurekaMain7001.class, args); } } ``` -* test.resouces +* 修改 pom 文件 - ```java - applicationContext.xml + jdbc.properties + ```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 + *** -### 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 - / - - +服务器端主启动类需要增加 @EnableEurekaClient 注解,表示这是一个 Eureka 客户端,要注册进 EurekaServer 中 + +* 主启动类:PaymentMain8001 + + ```java + @SpringBootApplication + @EnableEurekaClient + public class PaymentMain8001 { + public static void main(String[] args) { + SpringApplication.run(PaymentMain8001.class, args); + } + } ``` -* spring-mvc.xml +* 修改 pom 文件:添加一个 Eureka-Client 依赖 ```xml - - + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-client + + ``` + +* 写 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 信息提示(鼠标停留在服务名称上时) ``` -* Controller层 +* 游览器访问 http://localhost:7001 + + + +*** + + + +##### 消费者 + +* 主启动类:PaymentMain8001 ```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; + @SpringBootApplication + @EnableEurekaClient + @EnableDiscoveryClient + public class PaymentMain8001 { + public static void main(String[] args) { + SpringApplication.run(PaymentMain8001.class, args); } + } + ``` + +* pom 文件同生产者 + +* 写 yml 文件 + + ```yaml + server: + port: 80 - @GetMapping("/{uuid}") - public User get(@PathVariable Integer uuid){ - System.out.println("get ..." + uuid); - return null; - } + # 微服务名称 + 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) + + + +*** + + + +#### 集群构建 + +##### 服务端 + +Server 端高可用集群原理:实现负载均衡和故障容错,互相注册,相互守望 + +![Cloud-Eureka集群原理](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Cloud-Eureka集群原理.png) + +多台 Eureka 服务器,每一台 Eureka 服务器需要有自己的主机名,同时各服务器需要相互注册 + +* Eureka1: + + ```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); } } ``` @@ -14360,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调用 + + +*** + + + +##### 负载均衡 + +消费者端的 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 - @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); - } - } + public static final String PAYMENT_URL = "http://CLOUD-PAYMENT-SERVICE"; ``` +* 使用 @LoadBlanced 注解赋予 RestTemplate 负载均衡的能力,增加 config.ApplicationContextConfig 文件: + ```java + @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; } } ``` @@ -14509,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://seazean.oss-cn-beijing.aliyuncs.com/img/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` - ```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); - } - ``` +```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/ - + + +中文文档:https://www.springcloud.cc/spring-cloud-consul.html @@ -14594,306 +16682,323 @@ public class ProjectExceptionAdivce { -### applicationContext.xml +#### 基本使用 -![](https://seazean.oss-cn-beijing.aliyuncs.com/img/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 + +#### 基本介绍 + +Feign 是一个声明式 WebService 客户端,能让编写 Web 客户端更加简单,只要创建一个接口并添加注解 @Feign 即可,可以与 Eureka 和 Ribbon 组合使用支持负载均衡,所以一般**用在消费者端** + +OpenFeign 在 Feign 的基础上支持了 SpringMVC 注解,并且 @FeignClient 注解可以解析 @RequestMapping 注解下的接口,并通过动态代理的方式产生实现类,在实现类中做负载均衡和服务调用 + +优点:利用 RestTemplate 对 HTTP 请求的封装处理,形成了一套模版化的调用方法。但是对服务依赖的调用可能不止一处,往往一个接口会被多处调用,所以一个微服务接口上面标注一个 @Feign 注解,就可以完成包装依赖服务的调用 + + + - ```java - // 引导类,SpringBoot项目的入口 - @SpringBootApplication - public class HelloApplication { - public static void main(String[] args) { - SpringApplication.run(HelloApplication.class, args); - } - } - ``` -快速构建: +**** -![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/SpringBoot-IDEA构建工程.png) +#### 基本使用 +@FeignClient("provider name") 注解使用规则: +* 声明的方法签名必须和 provider 微服务中的 controller 中的方法签名一致 +* 如果需要传递参数,那么 `@RequestParam` 、`@RequestBody` 、`@PathVariable` 也需要加上 +改造消费者服务 +* 引入 pom 依赖:OpenFeign 整合了 Ribbon,具有负载均衡的功能 -*** + ```xml + + org.springframework.cloud + spring-cloud-starter-openfeign + + ``` + +* 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: -在 spring-boot-starter-parent 中定义了各种技术的版本信息,组合了一套最优搭配的技术版本。在各种 starter 中,定义了完成该功能需要的坐标合集,其中大部分版本信息来自于父工程。工程继承 parent,引入 starter 后,通过依赖传递,就可以简单方便获得需要的 jar 包,并且不会存在版本冲突,自动版本仲裁机制 + ```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(); + } + } + + ``` @@ -14901,324 +17006,342 @@ SpringBoot 功能: -### 底层注解 +#### 超时问题 -#### SpringBoot +Feign 默认是支持 Ribbon,Feign 客户端的负载均衡和超时控制都由 Ribbon 控制 -@SpringBootApplication:启动注解,实现 SpringBoot 的自动部署 +设置 Feign 客户端的超时等待时间: -* 参数 scanBasePackages:可以指定扫描范围 -* 默认扫描当前引导类所在包及其子包 +```yaml +ribbon: + #指的是建立连接后从服务器读取到可用资源所用的时间 + ReadTimeout: 5000 + #指的是建立连接所用的时间,适用于网络状况正常的情况下,两端连接所用的时间 + ConnectTimeout: 5000 +``` -假如所在包为 com.example.springbootenable,扫描配置包 com.example.config 的信息,三种解决办法: +演示超时现象:OpenFeign 默认等待时间为 1 秒钟,超过后会报错 -1. 使用 @ComponentScan 扫描 com.example.config 包 +* 服务提供方 Controller: -2. 使用 @Import 注解,加载类,这些类都会被 Spring 创建并放入 ioc 容器,默认组件的名字就是**全类名** + ```java + @GetMapping("/payment/feign/timeout") + public String paymentFeignTimeout() { + try { + TimeUnit.SECONDS.sleep(3); + } catch (InterruptedException e) { + e.printStackTrace(); + } + return serverPort; + } + ``` -3. 对 @Import 注解进行封装 +* 消费者 PaymentFeignService 和 OrderFeignController 参考上一小节代码 -```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); + ![Cloud-OpenFeign超时错误](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Cloud-OpenFeign超时错误.png)!](C:\Users\Seazean\Desktop\123\Cloud-OpenFeign超时错误.png) - } -} -``` -UserConfig: -```java -@Configuration -public class UserConfig { - @Bean - public User user() { - return new User(); - } -} -``` +*** -EnableUser 注解类: -```java -@Target(ElementType.TYPE) -@Retention(RetentionPolicy.RUNTIME) -@Documented -@Import(UserConfig.class)//@Import注解实现Bean的动态加载 -public @interface EnableUser { -} -``` +#### 日志级别 +Feign 提供了日志打印功能,可以通过配置来调整日志级别,从而了解 Feign 中 HTTP 请求的细节 +| NONE | 默认的,不显示任何日志 | +| ------- | --------------------------------------------------------- | +| BASIC | 仅记录请求方法、URL、响应状态码及执行时间 | +| HEADERS | 除了 BASIC 中定义的信息之外,还有请求和响应的头信息 | +| FULL | 除了 HEADERS 中定义的信息外,还有请求和响应的正文及元数据 | +配置在消费者端 -*** +* 新建 config.FeignConfig 文件:配置日志 Bean + ```java + @Configuration + public class FeignConfig { + @Bean + Logger.Level feignLoggerLevel() { + return Logger.Level.FULL; + } + } + ``` +* application.yml: -#### Configuration + ```yaml + logging: + level: + # feign 日志以什么级别监控哪个接口 + com.atguigu.springcloud.service.PaymentFeignService: debug + ``` -@Configuration:设置当前类为 SpringBoot 的配置类 +* Debug 后查看后台日志 -* proxyBeanMethods = true:Full 全模式,每个 @Bean 方法被调用多少次返回的组件都是单实例的,默认值,类组件之间**有依赖关系**,方法会被调用得到之前单实例组件 -* proxyBeanMethods = false:Lite 轻量级模式,每个 @Bean 方法被调用多少次返回的组件都是新创建的,类组件之间**无依赖关系**用 Lite 模式加速容器启动过程 -```java -@Configuration(proxyBeanMethods = true) -public class MyConfig { - @Bean //给容器中添加组件。以方法名作为组件的 id。返回类型就是组件类型。返回的值,就是组件在容器中的实例 - public User user(){ - User user = new User("zhangsan", 18); - return user; - } -} -``` +**** -*** -#### Condition +## 服务熔断 -##### 条件注解 +### Hystrix -Condition 是 Spring4.0 后引入的条件化配置接口,通过实现 Condition 接口可以完成有条件的加载相应的 Bean +#### 基本介绍 -注解:@Conditional +Hystrix 是一个用于处理分布式系统的延迟和容错的开源库,在分布式系统里,许多依赖会出现调用失败,比如超时、异常等,Hystrix 能够保证在一个依赖出问题的情况下,不会导致整体服务失败,避免级联故障,以提高分布式系统的弹性 -作用:条件装配,满足 Conditional 指定的条件则进行组件注入,加上方法或者类上,作用范围不同 +断路器本身是一种开关装置,当某个服务单元发生故障之后,通过断路器的故障监控(类似熔断保险丝),向调用方返回一个符合预期的、可处理的备选响应(FallBack),而不是长时间的等待或者抛出调用方无法处理的异常,这样就保证了服务调用方的线程不会被长时间地占用,避免了故障在分布式系统中的蔓延,乃至雪崩 -使用:@Conditional 配合 Condition 的实现类(ClassCondition)进行使用 +* 服务降级 Fallback:系统不可用时需要一个兜底的解决方案或备选响应,向调用方返回一个可处理的响应 +* 服务熔断 Break:达到最大服务访问后,直接拒绝访问 +* 服务限流 Flowlimit:高并发操作时严禁所有请求一次性过来拥挤,一秒钟 N 个,有序排队进行 -ConditionContext 类API: -| 方法 | 说明 | -| --------------------------------------------------- | ----------------------------- | -| ConfigurableListableBeanFactory getBeanFactory() | 获取到 IOC 使用的 beanfactory | -| ClassLoader getClassLoader() | 获取类加载器 | -| Environment getEnvironment() | 获取当前环境信息 | -| BeanDefinitionRegistry getRegistry() | 获取到 bean 定义的注册类 | -* ClassCondition +官方文档:https://github.com/Netflix/Hystrix/wiki/How-To-Use - ```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(); - } - } - ``` -* 启动类: - ```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); - } - } - ``` +**** -*** +#### 服务降级 +##### 案例构建 +生产者模块: -##### 自定义注解 +* 引入 pom 依赖: -将类的判断定义为动态的,判断哪个字节码文件存在可以动态指定 + ```xml + + org.springframework.cloud + spring-cloud-starter-netflix-hystrix + + ``` -* 自定义条件注解类 +* 主启动类:开启 Feign ```java - @Target({ElementType.TYPE, ElementType.METHOD}) - @Retention(RetentionPolicy.RUNTIME) - @Documented - @Conditional(ClassCondition.class) - public @interface ConditionOnClass { - String[] value(); + @SpringBootApplication + @EnableEurekaClient + @EnableCircuitBreaker // 降级使用 + public class PaymentHystrixMain8001 { + public static void main(String[] args) { + SpringApplication.run(PaymentHystrixMain8001.class, args); + } } ``` -* ClassCondition +* Controller: ```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; + @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); } } ``` -* UserConfig +* Service: ```java - @Configuration - public class UserConfig { - @Bean - @ConditionOnClass("com.alibaba.fastjson.JSON")//JSON加载了才注册 User 到容器 - public User user(){ - return new User(); + @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; } } ``` -* 测试 User 对象的创建 - - +* jmeter 压测两个接口,发现接口 paymentInfo_Ok 也变的卡顿 -*** - - - -##### 常用注解 +消费者模块: -SpringBoot 提供的常用条件注解: +* Service 接口: -@ConditionalOnProperty:判断**配置文件**中是否有对应属性和值才初始化 Bean + ```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); + } + ``` -```java -@Configuration -public class UserConfig { - @Bean - @ConditionalOnProperty(name = "it", havingValue = "seazean") - public User user() { - return new User(); - } -} -``` +* Controller: -```properties -it=seazean -``` + ```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); + } + } + ``` -@ConditionalOnClass:判断环境中是否有对应类文件才初始化 Bean +* 测试:使用的是 Feign 作为客户端,默认 1s 没有得到响应就会报超时错误,进行并发压测 -@ConditionalOnMissingClass:判断环境中是否有对应类文件才初始化 Bean +* 解决: -@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"; + } + } + ``` @@ -15226,124 +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 容器的基本信息 - * `postProcessApplicationContext(context)`:后置处理流程 +#### 工作流程 - * `applyInitializers(context)`:获取所有的**初始化器调用 initialize() 方法**进行初始化 - * `listeners.contextPrepared(context)`:所有的运行监听器调用 environmentPrepared() 方法,EventPublishingRunListener 发布事件通知 IOC 容器准备完成 - * `listeners.contextLoaded(context)`:所有的运行监听器调用 contextLoaded() 方法,通知 IOC 加载完成 +具体工作流程: -* `refreshContext(context)`:**刷新 IOC 容器** +1. 创建 HystrixCommand(用在依赖的服务返回单个操作结果的时候) 或 HystrixObserableCommand(用在依赖的服务返回多个操作结果的时候) 对象 - * Spring 的容器启动流程 - * `invokeBeanFactoryPostProcessors(beanFactory)`:**实现了自动装配** - * `onRefresh()`:**创建 WebServer** 使用该接口 +2. 命令执行,其中 HystrixComand 实现了下面前两种执行方式,而 HystrixObservableCommand 实现了后两种执行方式 -* `afterRefresh(context, applicationArguments)`:留给用户自定义容器刷新完成后的处理逻辑 + * execute():同步执行,从依赖的服务返回一个单一的结果对象, 或是在发生错误的时候抛出异常 -* `stopWatch.stop()`:记录应用启动完成的时间 + * queue():异步执行, 直接返回 一个 Future 对象, 其中包含了服务执行结束时要返回的单一结果对象 -* `callRunners(context, applicationArguments)`:调用所有 runners + * observe():返回 Observable 对象,代表了操作的多个结果,它是一个 Hot Obserable(不论事件源是否有订阅者,都会在创建后对事件进行发布,所以对于 Hot Observable 的每个订阅者都有可能是从事件源的中途开始的,并可能只是看到了整个操作的局部过程) -* `listeners.started(context)`:所有的运行监听器调用 started() 方法 + * toObservable():同样会返回 Observable 对象,也代表了操作的多个结果,但它返回的是一个 Cold Observable(没有订阅者的时候并不会发布事件,而是进行等待,直到有订阅者之后才发布事件,所以对于 Cold Observable 的订阅者,它可以保证从一开始看到整个操作的全部过程) -* `listeners.running(context)`:所有的运行监听器调用 running() 方法 +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 的形式返回 - * 获取容器中的 ApplicationRunner、CommandLineRunner - * `AnnotationAwareOrderComparator.sort(runners)`:合并所有 runner 并且按照 @Order 进行排序 +注意:如果、没有为命令实现降级逻辑或者在降级处理逻辑中抛出了异常, Hystrix 依然会返回一个 Observable 对象, 但是它不会发射任何结果数据,而是通过 onError 方法通知命令立即中断请求,并通过 onError() 方法将引起命令失败的异常发送给调用者 - * `callRunner()`:遍历所有的 runner,调用 run 方法 +![Cloud-Hystrix工作流程](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Cloud-Hystrix工作流程.png) + + + +官方文档: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)`:通知异常 @@ -15351,453 +17453,519 @@ 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://seazean.oss-cn-beijing.aliyuncs.com/img/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(); + } +} +``` + *** +##### 动态路由 +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 - ``` -* 指定配置文件位置 +## 服务配置 - ```sh - java -jar myproject.jar --spring.config.location=e://application.properties - ``` +### config -* 按优先级从高到底选择配置文件的加载命令 +#### 基本介绍 - ```sh - java -jar myproject.jar - ``` +SpringCloud Config 为微服务架构中的微服务提供集中化的外部配置支持(Git/GitHub),为各个不同微服务应用的所有环境提供了一个中心化的外部配置(Config Server) - +![Cloud-Config工作原理](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Cloud-Config工作原理.png) +SpringCloud Config 分为服务端和客户端两部分 +* 服务端也称为分布式配置中心,是一个独立的微服务应用,连接配置服务器并为客户端提供获取配置信息,加密/解密等信息访问接口 +* 客户端通过指定的配置中心来管理应用资源,以及与业务相关的配置内容,并在启动时从配置中心获取和加载配置信息,配置服务器默认采用 Git 来存储配置信息,这样既有助于对环境配置进行版本管理,也可以通过 Git 客户端来方便的管理和访问配置内容 -*** +优点: +* 集中管理配置文件 +* 不同环境不同配置,动态化的配置更新,分环境部署比如 dev/test/prod/beta/release +* 运行期间动态调整配置,服务向配置中心统一拉取配置的信息,**服务不需要重启即可感知到配置的变化并应用新的配置** +* 将配置信息以 Rest 接口的形式暴露 -### yaml语法 -基本语法: +官网: https://cloud.spring.io/spring-cloud-static/spring-cloud-config/2.2.1.RELEASE/reference/html/ -- 大小写敏感 -- **数据值前边必须有空格,作为分隔符** -- 使用缩进表示层级关系 -- 缩进时不允许使用Tab键,只允许使用空格(各个系统 Tab对应空格数目可能不同,导致层次混乱) -- 缩进的空格数目不重要,只要相同层级的元素左侧对齐即可 -- ''#" 表示注释,从这个字符一直到行尾,都会被解析器忽略 +**** - ```yaml - server: - port: 8080 - address: 127.0.0.1 - ``` -数据格式: -* 纯量:单个的、不可再分的值 +#### 服务端 - ```yaml - msg1: 'hello \n world' # 单引忽略转义字符 - msg2: "hello \n world" # 双引识别转义字符 +构建 Config Server 模块 + +* 引入 pom 依赖: + + ```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值 - ``` @@ -15805,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://seazean.oss-cn-beijing.aliyuncs.com/img/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); + } + } + ``` @@ -16227,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/ @@ -16323,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 @@ -16790,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 + ``` @@ -17052,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 004807e..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 @@ -550,7 +550,7 @@ Linux 文件系统目录结构和熟知的 windows 系统有较大区别,没 #### NAT -首先设置虚拟机中 NAT 模式的选项,打开 VMware,点击“编辑”下的“虚拟网络编辑器”,设置 NAT 参数 +首先设置虚拟机中 NAT 模式的选项,打开 VMware,点击编辑下的虚拟网络编辑器,设置 NAT 参数 ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/配置NAT.jpg) **注意**:VMware Network Adapter VMnet8 保证是启用状态 @@ -614,8 +614,9 @@ Linux 文件系统目录结构和熟知的 windows 系统有较大区别,没 ### 远程登陆 -**服务器维护工作** 都是在 远程 通过 SSH 客户端 来完成的, 并没有图形界面, 所有的维护工作都需要通过命令来完成,Linux 服务器需要安装 SSH 相关服务。 -首先执行 sudo apt-get install openssh-server 指令。接下来用 xshell 连接。 +**服务器维护工作** 都是在 远程 通过 SSH 客户端 来完成的, 并没有图形界面, 所有的维护工作都需要通过命令来完成,Linux 服务器需要安装 SSH 相关服务 + +首先执行 sudo apt-get install openssh-server 指令,接下来用 xshell 连接 ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/远程连接Linux.png) @@ -633,12 +634,12 @@ Linux 文件系统目录结构和熟知的 windows 系统有较大区别,没 ## 用户管理 -Linux 系统是一个多用户、多任务的操作系统。多用户是指在 linux 操作系统中可以创建多个用户,而这些多用户又可以同时执行各自不同的任务,而互不影响。和 windows 系统有很大区别。 +Linux 系统是一个多用户、多任务的操作系统。多用户是指在 Linux 操作系统中可以创建多个用户,而这些多用户又可以同时执行各自不同的任务,而互不影响 在 Linux 系统中,会存在着以下几个概念: * 用户名:用户的名称 -* 用户所属的组:当前用户所属的组。 +* 用户所属的组:当前用户所属的组 * 用户的家目录:当前账号登录成功之后的目录,就叫做该用户的家目录 @@ -649,9 +650,9 @@ Linux 系统是一个多用户、多任务的操作系统。多用户是指在 l logname:用于显示目前用户的名称 -* --help  在线帮助。 +* --help:在线帮助 -* --vesion  显示版本信息。 +* --vesion:显示版本信息 @@ -661,7 +662,7 @@ su UserName:切换用户 su -c comman root:切换用户为 root 并在执行 comman 指令后退出返回原使用者 -su:切换到root用户 +su:切换到 root 用户 @@ -671,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 查看 @@ -689,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** @@ -697,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:强迫用户下次登录时修改密码 @@ -713,7 +711,7 @@ useradd -m Username新建用户成功之后,会建立家目录,但是此时 usermod 命令通过修改系统帐户文件来修改用户账户信息 -修改用户账号就是根据实际情况更改用户的有关属性,如用户号、主目录、用户组、登录Shell等。 +修改用户账号就是根据实际情况更改用户的有关属性,如用户号、主目录、用户组、登录 Shell 等 * 普通用户:sudo usermod [options] Username @@ -725,12 +723,12 @@ usermod 命令通过修改系统帐户文件来修改用户账户信息 #### 用户删除 -删除用户账号就是要将/etc/passwd等系统文件中的该用户记录删除,必要时还删除用户的主目录。 +删除用户账号就是要将 /etc/passwd 等系统文件中的该用户记录删除,必要时还删除用户的主目录 * 普通用户:sudo userdel [options] Username * 管理员用户:userdel [options] Username - * -f:强制删除用户,即使用户当前已登录; + * -f:强制删除用户,即使用户当前已登录 * -r:删除用户的同时,删除与用户相关的所有文件 @@ -741,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 @@ -835,8 +829,7 @@ gpasswd 是 Linux 工作组文件 /etc/group 和 /etc/gshadow 管理工具,用 可以看到命令的帮助文档 -**man** [指令名称] 查看帮助文档 -比如 man ls,退出方式 q +**man** [指令名称]:查看帮助文档,比如 man ls,退出方式 q @@ -850,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” @@ -868,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 和组信息 @@ -888,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 @@ -920,24 +909,25 @@ top:用于实时显示 process 的动态 * -d 秒数:表示进程界面更新时间(每几秒刷新一次) * -H 表示线程模式 -`top -Hp 进程 id`:分析该进程内各线程的cpu使用情况 +`top -Hp 进程 id`:分析该进程内各线程的 CPU 使用情况 ![](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 — 进程名称(命令名/命令行) @@ -959,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 `:显示某个进程的线程 @@ -986,10 +976,10 @@ Linux kill 命令用于删除执行中的程序或工作,并不是让进程直 命令:kill [-s <信息名称或编号>] [程序] 或 kill [-l <信息编号>] -- -l <信息编号>  若不加<信息编号>选项,则-l参数会列出全部的信息名称 -- -s <信息名称或编号>  指定要送出的信息 -- -KILL 强制杀死进程 -- **-9 彻底杀死进程(常用)** +- -l <信息编号>:若不加<信息编号>选项,则-l参数会列出全部的信息名称 +- -s <信息名称或编号>:指定要送出的信息 +- -KILL:强制杀死进程 +- **-9:彻底杀死进程(常用)** - [程序] 程序的 PID、PGID、工作编号 `kill 15642 `. `kill -KILL 15642`. `kill -9 15642` @@ -1008,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分钟后关机重启"` @@ -1039,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:在重开机之前先把所有网络相关的装置先停止 @@ -1057,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:显示版本信息 @@ -1082,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 的注销 @@ -1117,7 +1107,7 @@ who命令用于显示系统中有哪些使用者正在上面,显示的资料 timedatectl用于控制系统时间和日期。可以查询和更改系统时钟于设定,同时可以设定和修改时区信息。在实际开发过程中,系统时间的显示会和实际出现不同步;我们为了校正服务器时间、时区会使用timedatectl命令 -timedatectl :显示系统的时间信息 +timedatectl:显示系统的时间信息 timedatectl status:显示系统的当前时间和日期 @@ -1131,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的远程服务器 @@ -1141,9 +1131,9 @@ NTP即Network Time Protocol(网络时间协议),是一个互联网协议 ### clear -clear命令用于清除屏幕 +clear 命令用于清除屏幕 -通过执行clear命令,就可以把缓冲区的命令全部清理干净了 +通过执行 clear 命令,就可以把缓冲区的命令全部清理干净 @@ -1153,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 信号产生 @@ -1183,18 +1175,6 @@ exit命令用于退出目前的shell。执行exit可使shell以指定的状态 ### 常用命令 -- ls: 列出目录 -- cd: 切换目录 -- pwd: 显示目前的目录 -- mkdir:创建一个新的目录 -- rmdir:删除一个空的目录 -- cp: 复制文件或目录 -- rm: 移除文件或目录 -- mv: 移动文件与目录或修改文件与目录的名称 -- 在敲出文件/ 目录 / 命令的前几个字母之后, 按下 `tab`键会自动补全,如果还存在其他文件 / 目录 / 命令, 再按一下`tab`键,系统会提示可能存在的命令 - - - #### ls ls命令相当于我们在Windows系统中打开磁盘、或者打开文件夹看到的目录以及文件的明细。 @@ -1204,13 +1184,13 @@ ls命令相当于我们在Windows系统中打开磁盘、或者打开文件夹 - -a :全部的文件,连同隐藏档( 开头为 . 的文件) 一起列出来(常用) - -d :仅列出目录本身,而不是列出目录内的文件数据(常用) - -l :显示不隐藏的文件与文件夹的详细信息;(常用) -- ls -al = ll 命令:显示所有文件与文件夹的详细信息 +- **ls -al = ll 命令**:显示所有文件与文件夹的详细信息 #### pwd -pwd 是 **Print Working Directory** 的缩写,也就是显示目前所在当前目录的命令。 +pwd 是 Print Working Directory 的缩写,也就是显示目前所在当前目录的命令 命令:pwd 选项 @@ -1221,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 - 优点:定位准确, 不会因为 工作目录变化 而变化 @@ -1244,8 +1224,7 @@ mkdir命令用于建立名称为 dirName 之子目录 * -p 确保目录名称存在,不存在的就建一个,用来创建多级目录。 -在 aaa目录下,创建一个 bbb的子目录。 若 aaa目录原本不存在,则建立一个:`mkdir -p aaa/bbb` -注:本例若不加 -p,且原本 aaa目录不存在,则产生错误。 +`mkdir -p aaa/bbb`:在 aaa 目录下,创建一个 bbb 的子目录。 若 aaa 目录原本不存在,则建立一个 @@ -1255,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,只复制文件,而略过目录 @@ -1291,7 +1268,7 @@ rm命令用于删除一个文件或者目录。 - -f 即使原档案属性设为唯读,亦直接删除,无需逐一确认 - -r 将目录及以下之档案亦逐一删除,递归删除 -注:文件一旦通过rm命令删除,则无法恢复,所以必须格外小心地使用该命令 +注:文件一旦通过 rm 命令删除,则无法恢复,所以必须格外小心地使用该命令 @@ -1304,9 +1281,9 @@ mv [options] source dest mv [options] source... directory ``` -- -i: 若指定目录已有同名文件,则先询问是否覆盖旧文件; +- -i:若指定目录已有同名文件,则先询问是否覆盖旧文件 -- -f: 在 mv 操作要覆盖某已有的目标文件时不给任何指示; +- -f:在 mv 操作要覆盖某已有的目标文件时不给任何指示 | 命令格式 | 运行结果 | | ------------------ | ------------------------------------------------------------ | @@ -1325,70 +1302,77 @@ mv [options] source... directory #### 基本属性 -Linux系统是一种典型的多用户系统,不同的用户处于不同的地位,拥有不同的权限。为了保护系统的安全性,Linux系统对不同的用户访问同一文件(包括目录文件)的权限做了不同的规定。 +Linux 系统是一种典型的多用户系统,不同的用户处于不同的地位,拥有不同的权限。为了保护系统的安全性,Linux系统对不同的用户访问同一文件(包括目录文件)的权限做了不同的规定 ![](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】 +对于一个文件,都有一个特定的所有者,也就是对该文件具有所有权的用户(属主);还有这个文件是属于哪个组的(属组) + +* 文件的【属主】有一套【读写执行权限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 区域):文件的名称 +*** + #### 更改权限 ##### 权限概述 -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 表示只有该档案是个子目录或者该档案已经被设定过为可执行 @@ -1396,22 +1380,22 @@ 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 文件名` @@ -1424,28 +1408,36 @@ mode : 权限设定字串,格式: [ugoa...][[+-=][rwxX]...][,...] - 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(其他也可以) + + + + + +*** @@ -1474,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 [文件或目录] @@ -1509,7 +1501,7 @@ stat命令用于显示inode内容。stat以文字的格式来显示inode的内 #### cat -cat 是一个文本文件查看和连接工具。用于小文件 +cat 是一个文本文件查看和连接工具,**用于小文件** 命令:cat [-AbeEnstTuv] [--help] [--version] Filename @@ -1520,7 +1512,7 @@ cat 是一个文本文件查看和连接工具。用于小文件 #### less -less用于查看文件,但是less 在查看之前不会加载整个文件。用于大文件 +less 用于查看文件,但是 less 在查看之前不会加载整个文件,**用于大文件** 命令:less [options] Filename @@ -1530,13 +1522,13 @@ less用于查看文件,但是less 在查看之前不会加载整个文件。 #### tail -tail 命令可用于查看文件的内容,有一个常用的参数 **-f** 常用于查阅正在改变的日志文件。 +tail 命令可用于查看文件的内容,有一个常用的参数 **-f** 常用于查阅正在改变的日志文件 命令:tail [options] Filename * -f 循环读取,动态显示文档的最后内容 -* -n(行数) 显示文件的尾部 n 行内容 -* -c(数目)> 显示的字节数 +* -n 显示文件的尾部 n 行内容 +* -c 显示字节数 * -nf 查看最后几行日志信息 `tail -f filename`:动态显示最尾部的内容 @@ -1553,38 +1545,38 @@ 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 :可以将找到的关键词部分加上颜色的显示 +* -c 只输出匹配行的计数 +* -i 不区分大小写 +* -h 查询多文件时不显示文件名 +* -l 查询多文件时只输出包含匹配字符的文件名 +* -n 显示匹配行及行号 +* -s 不显示不存在或无匹配文本的错误信息 +* -v 显示不包含匹配文本的所有行 +* --color=auto 可以将找到的关键词部分加上颜色的显示 **管道符 |**:表示将前一个命令处理的结果传递给后面的命令处理 * `grep aaaa Filename `:显示存在关键字 aaaa 的行 * `grep -n aaaa Filename`:显示存在关键字 aaaa 的行,且显示行号 * `grep -i aaaa Filename`:忽略大小写,显示存在关键字 aaaa 的行 -* `grep -v aaaa Filename`:显示存在关键字aaaa的所有行 +* `grep -v aaaa Filename`:显示存在关键字 aaaa 的所有行 * `ps -ef | grep sshd`:查找包含 sshd 进程的进程信息 * ` ps -ef | grep -c sshd`:查找 sshd 相关的进程个数 @@ -1594,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) @@ -1618,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 //准备数据 @@ -1657,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:制表符 ``` @@ -1671,7 +1669,7 @@ zhouba 98 44 46 zhouba 98 44 ``` -* `awk -F ',' '{print toupper($1)}' a.txt`:根据逗号分割, 打印内容,第一段大写 +* `awk -F ',' '{print toupper($1)}' a.txt`:根据逗号分割,打印内容,第一段大写 | 函数名 | 含义 | 作用 | | --------- | ------ | -------------- | @@ -1688,7 +1686,7 @@ zhouba 98 44 46 #### find -find 命令用来在指定目录下查找文件。任何位于参数之前的字符串都将被视为查找的目录名。如果使用该命令不设置任何参数,将在当前目录下查找子目录与文件,并且将查找到的子目录和文件全部进行显示 +find 命令用来在指定目录下查找文件,如果使用该命令不设置任何参数,将在当前目录下查找子目录与文件,并且将查找到的子目录和文件全部进行显示 命令:find <指定目录> <指定条件> <指定内容> @@ -1700,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 ...] @@ -1712,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][文件] @@ -1733,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 个字母 @@ -1766,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 文件打包(仅打包,不压缩) @@ -1780,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 目标路径`:解压 @@ -1813,9 +1806,9 @@ gunzip 001.gz :解压001.gz文件 #### zip -zip命令用于压缩文件。 +zip 命令用于压缩文件。 -zip是个使用广泛的压缩程序,文件经它压缩后会另外产生具有".zip"扩展名的压缩文件。 +zip 是个使用广泛的压缩程序,文件经它压缩后会另外产生具有 `.zip` 扩展名的压缩文件 命令:zip [必要参数] [选择参数] [文件] @@ -1828,7 +1821,7 @@ zip是个使用广泛的压缩程序,文件经它压缩后会另外产生具 #### unzip -unzip 命令用于解压缩zip文件,unzip 为.zip压缩文件的解压缩程序 +unzip 命令用于解压缩 zip 文件,unzip 为 `.zip` 压缩文件的解压缩程序 命令:unzip [必要参数] [选择参数] [文件] @@ -1844,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][- 压缩等级][要压缩的文件] @@ -1858,7 +1851,7 @@ bzip2 [-cdfhkLstvVz][--repetitive-best][--repetitive-fast][- 压缩等级][要 #### bunzip2 -bunzip2命令是.bz2文件的解压缩程序。 +bunzip2 命令是 `.bz2` 文件的解压缩程序。 命令:bunzip2 [-fkLsvV] [.bz2压缩文件] @@ -1876,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) @@ -1921,7 +1912,7 @@ Vim 有三种模式:命令模式(Command mode)、插入模式(Insert mod | a | 在光标所在位置之后插入文本 | | A | 在光标所在行的行尾插入文本 | -按下ESC键,离开插入模式,进入命令模式 +按下 ESC 键,离开插入模式,进入命令模式 因为我们是一个空文件,所以使用【I】或者【i】都可以 @@ -1935,7 +1926,7 @@ Vim 有三种模式:命令模式(Command mode)、插入模式(Insert mod #### 命令模式 -Vim 打开一个文件(文件可以存在,也可以不存在),默认就是进入命令模式。在该模式下, 输入的字符会被当做指令,而不会被当做要输入的文字。在该模式下,可以使用指令进行跳至文章开头、文章结尾、删除某行、复制、粘贴等内容。 +Vim 打开一个文件(文件可以存在,也可以不存在),默认进入命令模式。在该模式下, 输入的字符会被当做指令,而不会被当做要输入的文字 ##### 移动光标 @@ -1961,11 +1952,11 @@ Vim 打开一个文件(文件可以存在,也可以不存在),默认就 ##### 选中文本 -在 vi/vim 中要选择文本, 需要显示 visual 命令切换到 **可视模式** +在 vi/vim 中要选择文本,需要显示 visual 命令切换到**可视模式** -vi/vim 中提供了 **三种** 可视模式, 可以方便程序员的选择 **选中文本的方式** +vi/vim 中提供了三种可视模式,方便程序员的选择**选中文本的方式** -按 ESC 可以放弃选中, 返回到 **命令模式** +按 ESC 可以放弃选中, 返回到**命令模式** | 命令 | 模式 | 功能 | | -------- | ---------- | ---------------------------------- | @@ -1977,7 +1968,7 @@ vi/vim 中提供了 **三种** 可视模式, 可以方便程序员的选择 ** ##### 撤销删除 -在学习编辑命令之前,先要知道怎样撤销之前一次 错误的 编辑操作 +在学习编辑命令之前,先要知道怎样撤销之前一次错误的编辑操作 | 命令 | 英文 | 功能 | | -------- | ----- | ------------------------ | @@ -1988,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 行内容 | @@ -2015,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` 命令粘贴,可以在编辑模式下使用鼠标右键粘贴 @@ -2042,10 +2033,10 @@ vim 中提供有一个 被复制文本的缓冲区 | 快捷键 | 功能描述 | | :----: | :--------------------------------------: | -| /abc | 从光标所在位置向后查找字符串abc | -| /^abc | 查找以abc为行首的行 | -| /abc$ | 查找以abc为行尾的行 | -| ?abc | 从光标所在位置向前查找字符串abc | +| /abc | 从光标所在位置向后查找字符串 abc | +| /^abc | 查找以 abc 为行首的行 | +| /abc$ | 查找以 abc 为行尾的行 | +| ?abc | 从光标所在位置向前查找字符串 abc | | * | 向后查找当前光标所在单词 | | # | 向前查找当前光标所在单词 | | n | 查找下一个,向同一方向重复上次的查找指令 | @@ -2059,8 +2050,8 @@ vim 中提供有一个 被复制文本的缓冲区 | R | 替换当前行光标后的字符 | 替换模式 | - 光标选中要替换的字符 -- `R` 命令可以进入 **替换模式**, 替换完成后, 按下ESC, 按下 ESC可以回到 **命令模式** -- **替换命令** 的作用就是不用进入 **编辑模式**, 对文件进行 **轻量级的修改** +- `R` 命令可以进入替换模式,替换完成后,按下 ESC 可以回到命令模式 +- 替换命令的作用就是不用进入编辑模式,对文件进行轻量级的修改 @@ -2070,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://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/vim异常.png) -* ls -a 一下,会看到隐藏的.swp文件 删除了此文件即可。 +* ls -a 一下,会看到隐藏的 .swp 文件,删除了此文件即可 @@ -2185,9 +2176,9 @@ pstree -A #查看所有进程树 * 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 盘) @@ -2254,7 +2245,7 @@ pid_t wait(int *status) 参数:status 用来保存被收集的子进程退出时的状态,如果不关心子进程**如何**销毁,可以设置这个参数为 NULL -父进程调用 wait() 会一直阻塞,直到收到一个子进程退出的 SIGCHLD 信号,wait() 函数就会销毁子进程并返回 +父进程调用 wait() 会阻塞等待,直到收到一个子进程退出的 SIGCHLD 信号,wait() 函数就会销毁子进程并返回 * 成功,返回被收集的子进程的进程 ID * 失败,返回 -1,同时 errno 被置为 ECHILD(如果调用进程没有子进程,调用就会失败) @@ -2315,11 +2306,10 @@ 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:启用网卡 @@ -2331,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地址] @@ -2392,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 命令新建目录 * 挂载点目录不可被其他进程使用到 @@ -2421,17 +2409,17 @@ lsblk 命令的英文是 list block,即用于列出所有可用块设备的信 * `lsblk`:以树状列出所有块设备 ![](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:本栏指出设备挂载的挂载点。 @@ -2462,8 +2450,8 @@ lsblk 命令的英文是 list block,即用于列出所有可用块设备的信 命令:df [options]... [FILE]... -* -h, 使用人类可读的格式(预设值是不加这个选项的...) -* --total 计算所有的数据之和 +* -h 使用人类可读的格式(预设值是不加这个选项的...) +* --total 计算所有的数据之和 ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/磁盘管理.png) @@ -2490,10 +2478,11 @@ 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文件内容】 @@ -2516,15 +2505,13 @@ mount [-fnrsvw] [-t vfstype] [-o options] device dir ### 概述 -防火墙技术是通过有机结合各类用于安全管理与筛选的软件和硬件设备,帮助计算机网络于其内、外网之间构建一道相对隔绝的保护屏障,以保护用户资料与信息安全性的一种技术。 - -在默认情况下,Linux系统的防火墙状态是打开的,已经启动。 +防火墙技术是通过有机结合各类用于安全管理与筛选的软件和硬件设备,帮助计算机网络于其内、外网之间构建一道相对隔绝的保护屏障,以保护用户资料与信息安全性的一种技术。在默认情况下,Linux 系统的防火墙状态是打开的 ### 状态 -**启动语法:service 服务 status** +启动语法:service name status * 查看防火墙状态:`service iptables status` @@ -2543,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 端口号是放行的 @@ -2557,16 +2544,15 @@ 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 脚本可以进行系统管理,文件操作等。 #### 环境 @@ -2578,22 +2564,20 @@ Shell 编程跟 JavaScript、php 编程一样,只要有一个能编写代码 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 --- 指定脚本解释器 @@ -2610,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】 @@ -2620,9 +2604,9 @@ Linux 的 Shell 种类众多,常见的有: **注意:** -**#!** 是一个约定的标记,它告诉系统这个脚本需要什么解释器来执行,即使用哪一种 Shell。 +**#!** 是一个约定的标记,告诉系统这个脚本需要什么解释器来执行,即使用哪一种 Shell -echo 命令用于向窗口输出文本。 +echo 命令用于向窗口输出文本 diff --git a/Web.md b/Web.md index a1ff3d5..1aeab98 100644 --- a/Web.md +++ b/Web.md @@ -2063,8 +2063,6 @@ a{ ``` -![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/CSS案例登陆页面.png) - @@ -2105,18 +2103,6 @@ URL 和 URI * 区别:`URL - HOST = URI`,URI 是抽象的定义,URL 用地址定位,URI 用名称定位。**只要能唯一标识资源的是 URI,在 URI 的基础上给出其资源的访问方式的是 URL** -短连接和长连接: - -* 短连接:客户端和服务器每进行一次 HTTP 操作,就建立一次连接,任务结束就中断连接 - - 使用短连接的情况下,当浏览器访问的某个 HTML 或其他类型的 Web 页中包含有其他的 Web 资源(图像文件、CSS 文件等),每遇到这样一个 Web 资源,浏览器就会经过三次握手重新建立一个 HTTP 会话 - -* 长连接:使用长连接的 HTTP 协议,会在响应头加入这行代码 `Connection:keep-alive` - - 使用长连接的情况下,当一个网页打开完成后,客户端和服务器之间用于传输 HTTP 数据的 TCP 连接不会关闭,客户端再次访问这个服务器时,会继续使用这一条已经建立的连接。Keep-Alive 有一个保持时间,不会永久保持连接,设置以后可以实现长连接,前提是需要客户端和服务端都支持长连接 - -* HTTP 协议的长连接和短连接,实质上是 TCP 协议的长连接和短连接 - **从浏览器地址栏输入 URL 到请求返回发生了什么?** * 进行 URL 解析,进行编码 @@ -2137,6 +2123,10 @@ URL 和 URI +推荐阅读:https://xiaolincoding.com/network/ + + + *** @@ -2155,25 +2145,35 @@ 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`,HTTP/1.1 的持续连接有非流水线方式和流水线方式 ,流水线方式是客户端在收到 HTTP 的响应报文之前就能接着发送新的请求报文,非流水线方式是客户端在收到前一个响应后才能发送下一个请求 + **HTTP 1.1起,默认使用长连接** ,默认开启 `Connection: keep-alive`,Keep-Alive 有一个保持时间,不会永久保持连接。持续连接有非流水线方式和流水线方式 ,流水线方式是客户端在收到 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),这样就方便了开发者自由的选择以便于充分利用带宽和连接 +* 带宽优化及网络连接的使用:HTTP1.0 存在一些浪费带宽的现象,例如客户端只需要某个对象的一部分,而服务器却将整个对象送过来了,并且不支持**断点续传**功能,HTTP1.1 则在请求头引入了 range 头域,允许只**请求资源的某个部分**,即返回码是 206(Partial Content),这样就方便了开发者自由的选择以便于充分利用带宽和连接 * HOST 头处理:在 HTTP1.0 中认为每台服务器都绑定一个唯一的 IP 地址,因此请求消息中的 URL 并没有传递主机名。HTTP1.1 时代虚拟主机技术发展迅速,在一台物理服务器上可以存在多个虚拟主机,并且共享一个 IP 地址,故 HTTP1.1 增加了 HOST 信息 HTTP 1.1 和 HTTP 2.0 的主要区别: * 新的二进制格式:HTTP1.1 基于文本格式传输数据,HTTP2.0 采用二进制格式传输数据,解析更高效 -* **多路复用**:在一个连接里,允许同时发送多个请求或响应,**并且这些请求或响应能够并行的传输而不被阻塞**,避免 HTTP1.1 出现的队头堵塞问题 -* 头部压缩,HTTP1.1 的 header 带有大量信息,而且每次都要重复发送;HTTP2.0 把 header 从数据中分离,并封装成头帧和数据帧,**使用特定算法压缩头帧**。并且 HTTP2.0 在客户端和服务器端记录了之前发送的键值对,对于相同的数据不会重复发送。比如请求 A 发送了所有的头信息字段,请求 B 则只需要发送差异数据,这样可以减少冗余数据,降低开销 +* **多路复用**:在一个连接里,允许同时发送多个请求或响应,并且这些请求或响应能够并行的传输而不被阻塞,避免 HTTP1.1 出现的队头堵塞问题 +* 头部压缩,HTTP1.1 的 header 带有大量信息,而且每次都要重复发送;HTTP2.0 把 header 从数据中分离,并封装成头帧和数据帧,使用特定算法压缩头帧。并且 HTTP2.0 在客户端和服务器端记录了之前发送的键值对,对于相同的数据不会重复发送。比如请求 A 发送了所有的头信息字段,请求 B 则只需要发送差异数据,这样可以减少冗余数据,降低开销 * **服务端推送**:HTTP2.0 允许服务器向客户端推送资源,无需客户端发送请求到服务器获取 + + +**** + + + +## 安全请求 + HTTP 和 HTTPS 的区别: * 端口 :HTTP 默认使用端口 80,HTTPS 默认使用端口 443 @@ -2187,17 +2187,23 @@ HTTP 和 HTTPS 的区别: * 缺点:无法安全的将密钥传输给通信方 * 非对称加密:加密和解密使用不同的秘钥,一把作为公开的公钥,另一把作为私钥,**公钥公开给任何人**(类似于把锁和箱子给别人,对方打开箱子放入数据,上锁后发送),典型的非对称加密算法有 RSA、DSA 等 - * 优点:可以更安全地将公开密钥传输给通信发送方 - * 缺点:运算速度慢 - + + * 公钥加密,私钥解密:为了**保证内容传输的安全**,因为被公钥加密的内容,其他人是无法解密的,只有持有私钥的人,才能解密出实际的内容 + * 私钥加密,公钥解密:为了**保证消息不会被冒充**,因为私钥是不可泄露的,如果公钥能正常解密出私钥加密的内容,就能证明这个消息是来源于持有私钥身份的人发送的 + * 可以更安全地将公开密钥传输给通信发送方,但是运算速度慢 + * **使用对称加密和非对称加密的方式传送数据** * 使用非对称密钥加密方式,传输对称密钥加密方式所需要的 Secret Key,从而保证安全性 * 获取到 Secret Key 后,再使用对称密钥加密方式进行通信,从而保证效率 思想:锁上加锁 - -* 数字签名:附加在报文上的特殊加密校验码,可以防止报文被篡改,一般是通过哈希算法 + +名词解释: + +* 哈希算法:通过哈希函数计算出内容的哈希值,传输到对端后会重新计算内容的哈希,进行哈希比对来校验内容的完整性 + +* 数字签名:附加在报文上的特殊加密校验码,可以防止报文被篡改。一般是通过私钥对内容的哈希值进行加密,公钥正常解密并对比哈希值后,可以确保该内容就是对端发出的,防止出现中间人替换的问题 * 数字证书:由权威机构给某网站颁发的一种认可凭证 @@ -2206,9 +2212,9 @@ HTTPS 工作流程:服务器端的公钥和私钥,用来进行非对称加 ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/HTTP-HTTPS加密过程.png) 1. 客户端向服务器发起 HTTPS 请求,连接到服务器的 443 端口,请求携带了浏览器支持的加密算法和哈希算法,协商加密算法 -2. 服务器端会向数字证书认证机构提出公开密钥的申请,认证机构对公开密钥做数字签名后进行分配,会将公钥绑定在数字证书(又叫公钥证书,内容有公钥,网站地址,证书颁发机构,失效日期等) +2. 服务器端会向数字证书认证机构注册公开密钥,认证机构**用 CA 私钥**对公开密钥做数字签名后绑定在数字证书(又叫公钥证书,内容有公钥,网站地址,证书颁发机构,失效日期等) 3. 服务器将数字证书发送给客户端,私钥由服务器持有 -4. 客户端收到服务器端的数字证书后对证书进行检查,验证其合法性,如果发现发现证书有问题,那么 HTTPS 传输就无法继续。如果公钥合格,那么客户端会生成一个随机值,**这个随机值就是用于进行对称加密的密钥**,将该密钥称之为 client key(客户端密钥、会话密钥)。用服务器的公钥对客户端密钥进行非对称加密,这样客户端密钥就变成密文,HTTPS 中的第一次 HTTP 请求结束 +4. 客户端收到服务器端的数字证书后**通过 CA 公钥**(事先置入浏览器或操作系统)对证书进行检查,验证其合法性。如果公钥合格,那么客户端会生成一个随机值,这个随机值就是用于进行对称加密的密钥,将该密钥称之为 client key(客户端密钥、会话密钥)。用服务器的公钥对客户端密钥进行非对称加密,这样客户端密钥就变成密文,HTTPS 中的第一次 HTTP 请求结束 5. 客户端会发起 HTTPS 中的第二个 HTTP 请求,将加密之后的客户端密钥发送给服务器 6. 服务器接收到客户端发来的密文之后,会用自己的私钥对其进行非对称解密,解密之后的明文就是客户端密钥,然后用客户端密钥对数据进行对称加密,这样数据就变成了密文 7. 服务器将加密后的密文发送给客户端 @@ -2370,12 +2376,12 @@ HTTPS 工作流程:服务器端的公钥和私钥,用来进行非对称加 | 404 | 客户端错误,请求资源未找到 | | 500 | 服务器错误,服务器运行内部错误 | - 面试题: + 转移: * 301 redirect:301 代表永久性转移 (Permanently Moved) * 302 redirect:302 代表暂时性转移 (Temporarily Moved ) -* 响应头:以 key:vaue 存在,可能多个 value 情况。 +* 响应头:以 key:vaue 存在,可能多个 value 情况 | 消息头 | 说明 | | ----------------------- | ------------------------------------------------------------ | @@ -4468,8 +4474,8 @@ Cookie:客户端会话管理技术,把要共享的数据保存到了客户 设置 Cookie 存活时间 API:`void setMaxAge(int expiry)` -* -1:默认。代表 Cookie 数据存到浏览器关闭(保存在浏览器文件中) -* 0:代表删除 Cookie,如果要删除 Cookie 要确保**路径一致**。 +* -1:默认,代表 Cookie 数据存到浏览器关闭(保存在浏览器文件中) +* 0:代表删除 Cookie,如果要删除 Cookie 要确保**路径一致** * 正整数:以秒为单位保存数据有有效时间(把缓存数据保存到磁盘中) ```java @@ -4564,7 +4570,7 @@ XSS 全称 Cross SiteScript,跨站脚本攻击,是Web程序中常见的漏 ### 基本介绍 -Session:服务器端会话管理技术,本质也是采用客户端会话管理技术,不过在客户端保存的是一个特殊标识,共享的数据保存到了服务器的内存对象中。每次请求时,会将特殊标识带到服务器端,根据标识来找到对应的内存空间,从而实现数据共享。简单说它就是一个服务端会话对象,用于存储用户的会话数据。 +Session:服务器端会话管理技术,本质也是采用客户端会话管理技术,不过在客户端保存的是一个特殊标识,共享的数据保存到了服务器的内存对象中。每次请求时,会将特殊标识带到服务器端,根据标识来找到对应的内存空间,从而实现数据共享。简单说它就是一个服务端会话对象,用于存储用户的会话数据 Session 域(会话域)对象是 Servlet 规范中四大域对象之一,并且它也是用于实现数据共享的 @@ -4618,7 +4624,7 @@ HttpServletRequest类获取Session: #### 实现会话 -通过第一个Servlet设置共享的数据用户名,并在第二个Servlet获取到。 +通过第一个Servlet设置共享的数据用户名,并在第二个Servlet获取到 项目执行完以后,去浏览器抓包,Request Headers 中的 Cookie JSESSIONID的值是一样的 @@ -4672,7 +4678,7 @@ public class ServletDemo02 extends HttpServlet{ #### 生命周期 -Session 的创建:一个常见的错误是以为 Session 在有客户端访问时就被创建,事实是直到某 server 端程序(如Servlet)调用 `HttpServletRequest.getSession(true)` 这样的语句时才会被创建 +Session 的创建:一个常见的错误是以为 Session 在有客户端访问时就被创建,事实是直到某 server 端程序(如 Servlet)调用 `HttpServletRequest.getSession(true)` 这样的语句时才会被创建 Session 在以下情况会被删除: