diff --git a/DB.md b/DB.md index a909a5e..7fd57da 100644 --- a/DB.md +++ b/DB.md @@ -264,7 +264,7 @@ SHOW PROCESSLIST:查看当前 MySQL 在进行的线程,可以实时地查看 1. 客户端发送一条查询给服务器 2. 服务器先会检查查询缓存,如果命中了缓存,则立即返回存储在缓存中的结果(一般是 K-V 键值对),否则进入下一阶段 3. 分析器进行 SQL 分析,再由优化器生成对应的执行计划 -4. MySQL 根据优化器生成的执行计划,调用存储引擎的 API 来执行查询 +4. 执行器根据优化器生成的执行计划,调用存储引擎的 API 来执行查询 5. 将结果返回给客户端 大多数情况下不建议使用查询缓存,因为查询缓存往往弊大于利 @@ -434,7 +434,7 @@ MySQL 中保存着两种统计数据: * innodb_table_stats 存储了表的统计数据,每一条记录对应着一个表的统计数据 * innodb_index_stats 存储了索引的统计数据,每一条记录对应着一个索引的一个统计项的数据 -MySQL 在真正执行语句之前,并不能精确地知道满足条件的记录有多少条,只能根据统计信息来估算记录,统计信息就是索引的区分度,一个索引上不同的值的个数(比如性别只能是男女,就是 2 ),称之为基数(cardinality),**基数越大说明区分度越好** +MySQL 在真正执行语句之前,并不能精确地知道满足条件的记录有多少条,只能根据统计信息来估算记录,统计信息就是索引的区分度,一个索引上不同的值的个数(比如性别只能是男女,就是 2 ),称之为基数(cardinality),**基数越大说明区分度越好** 通过**采样统计**来获取基数,InnoDB 默认会选择 N 个数据页,统计这些页面上的不同值得到一个平均值,然后乘以这个索引的页面数,就得到了这个索引的基数 @@ -3577,7 +3577,7 @@ MySQL 支持的存储引擎: MyISAM 存储引擎: * 特点:不支持事务和外键,读取速度快,节约资源 -* 应用场景:查询和插入操作为主,只有很少更新和删除操作,并对事务的完整性、并发性要求不高 +* 应用场景:**适用于读多写少的场景**,对事务的完整性要求不高,比如一些数仓、离线数据、支付宝的年度总结之类的场景,业务进行只读操作,查询起来会更快 * 存储方式: * 每个 MyISAM 在磁盘上存储成 3 个文件,其文件名都和表名相同,拓展名不同 * 表的定义保存在 .frm 文件,表数据保存在 .MYD (MYData) 文件中,索引保存在 .MYI (MYIndex) 文件中 @@ -3593,7 +3593,7 @@ InnoDB 存储引擎:(MySQL5.5 版本后默认的存储引擎) MEMORY 存储引擎: - 特点:每个 MEMORY 表实际对应一个磁盘文件 ,该文件中只存储表的结构,表数据保存在内存中,且默认**使用 HASH 索引**,所以数据默认就是无序的,但是在需要快速定位记录可以提供更快的访问,**服务一旦关闭,表中的数据就会丢失**,存储不安全 -- 应用场景:通常用于更新不太频繁的小表,用以快速得到访问结果,类似缓存 +- 应用场景:**缓存型存储引擎**,通常用于更新不太频繁的小表,用以快速得到访问结果 - 存储方式:表结构保存在 .frm 中 MERGE 存储引擎: @@ -3642,14 +3642,10 @@ MERGE 存储引擎: | 批量插入速度 | 高 | 低 | 高 | | **外键** | **不支持** | **支持** | **不支持** | -MyISAM 和 InnoDB 的区别? +只读场景 MyISAM 比 InnoDB 更快: -* 事务:InnoDB 支持事务,MyISAM 不支持事务 -* 外键:InnoDB 支持外键,MyISAM 不支持外键 -* 索引:InnoDB 是聚集(聚簇)索引,MyISAM 是非聚集(非聚簇)索引 - -* 锁粒度:InnoDB 最小的锁粒度是行锁,MyISAM 最小的锁粒度是表锁 -* 存储结构:参考本节上半部分 +* 底层存储结构有差别,MyISAM 是非聚簇索引,叶子节点保存的是数据的具体地址,不用回表查询 +* InnoDB 每次查询需要维护 MVCC 版本状态,保证并发状态下的读写冲突问题 @@ -5041,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; @@ -5134,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 层还会做判断 @@ -5202,7 +5198,7 @@ SHOW GLOBAL STATUS LIKE 'Handler_read%'; ##### 自增机制 -自增主键可以让主键索引尽量地保持递增顺序插入,避免了页分裂,因此索引更紧凑 +自增主键可以让主键索引尽量地保持在数据页中递增顺序插入,不自增需要寻找其他页插入,导致随机 IO 和页分裂的情况 表的结构定义存放在后缀名为.frm 的文件中,但是并不会保存自增值,不同的引擎对于自增值的保存策略不同: @@ -5770,9 +5766,9 @@ Flush 链表是一个用来**存储脏页**的链表,对于已经修改过的 ##### LRU 链表 -当 Buffer Pool 中没有空闲缓冲页时就需要淘汰掉最近最少使用的部分缓冲页,为了实现这个功能,MySQL 创建了一个 LRU 链表,当访问某个页时: +Buffer Pool 需要保证缓存的命中率,所以 MySQL 创建了一个 LRU 链表,当访问某个页时: -* 如果该页不在 Buffer Pool 中,把该页从磁盘加载进来后会将该缓冲页对应的控制块作为节点放入 **LRU 链表的头部** +* 如果该页不在 Buffer Pool 中,把该页从磁盘加载进来后会将该缓冲页对应的控制块作为节点放入 **LRU 链表的头部**,保证热点数据在链表头 * 如果该页在 Buffer Pool 中,则直接把该页对应的控制块移动到 LRU 链表的头部,所以 LRU 链表尾部就是最近最少使用的缓冲页 MySQL 基于局部性原理提供了预读功能: @@ -5780,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` 指定 @@ -5847,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 就结束**,不用校验唯一性 @@ -5904,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,优化回表流程: @@ -6346,7 +6342,7 @@ InnoDB 存储引擎提供了两种事务日志:redo log(重做日志)和 u * redo log 用于保证事务持久性 * undo log 用于保证事务原子性和隔离性 -undo log 属于逻辑日志,根据每行操作进行记录,记录了 SQL 执行相关的信息,用来回滚行记录到某个版本 +undo log 属于**逻辑日志**,根据每行操作进行记录,记录了 SQL 执行相关的信息,用来回滚行记录到某个版本 当事务对数据库进行修改时,InnoDB 会先记录对应的 undo log,如果事务执行失败或调用了 rollback 导致事务回滚,InnoDB 会根据 undo log 的内容**做与之前相反的操作**: @@ -6430,7 +6426,7 @@ 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 操作,所以不能立即删除,INSERT 可以删除的原因是 MVCC 是对现有数据的快照 @@ -6553,7 +6549,7 @@ undo log 是逻辑日志,记录的是每个事务对数据执行的操作, undo log 的作用: * 保证事务进行 rollback 时的原子性和一致性,当事务进行回滚的时候可以用 undo log 的数据进行恢复 -* 用于 MVCC 快照读,通过读取 undo log 的历史版本数据可以实现不同事务版本号都拥有自己独立的快照数据版本 +* 用于 MVCC 快照读,通过读取 undo log 的历史版本数据可以实现不同事务版本号都拥有自己独立的快照数据 undo log 主要分为两种: @@ -6741,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` @@ -6766,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)到磁盘,具体的**刷盘策略**: @@ -6780,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 对应的修改已经持久化到磁盘 @@ -6876,9 +6871,9 @@ binlog 为什么不支持崩溃恢复? * 首先更新该记录对应的聚簇索引,更新聚簇索引记录时: * 更新记录前向 undo 页面写 undo 日志,由于这是更改页面,所以需要记录一下相应的 redo 日志 - 注意:修改 undo页面也是在**修改页面**,事务凡是修改页面就需要先记录相应的 redo 日志 + 注意:修改 undo 页面也是在**修改页面**,事务只要修改页面就需要先记录相应的 redo 日志 - * 然后**先记录对应的的 redo 日志**(等待 MTR 提交后写入 redo log buffer),**最后进行真正的更新记录** + * 然后**记录对应的 redo 日志**(等待 MTR 提交后写入 redo log buffer),**最后进行真正的更新记录** * 更新其他的二级索引记录,不会再记录 undo log,只记录 redo log 到 buffer 中 @@ -6919,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 可以认为是一个分布式事务, 都可以用于表示事务的提交状态,而**两阶段提交就是让这两个状态保持逻辑上的一致**,也有利于主从复制,更好的保持主从数据的一致性 @@ -6931,7 +6926,7 @@ redo log 和 binlog 都可以用于表示事务的提交状态,而**两阶段 系统崩溃前没有提交的事务的 redo log 可能已经刷盘(定时线程或者 checkpoint),怎么处理崩溃恢复? -工作流程:获取 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: @@ -7406,7 +7401,7 @@ InnoDB 会对间隙(GAP)进行加锁,就是间隙锁 (RR 隔离级别下 InnoDB 加锁的基本单位是 next-key lock,该锁是行锁和 gap lock 的组合(X or S 锁),但是加锁过程是分为间隙锁和行锁两段执行 -* 可以**保护当前记录和前面的间隙**,遵循左开右闭原则,单纯的是间隙锁左开右开 +* 可以**保护当前记录和前面的间隙**,遵循左开右闭原则,单纯的间隙锁是左开右开 * 假设有 10、11、13,那么可能的间隙锁包括:(负无穷,10]、(10,11]、(11,13]、(13,正无穷) 几种索引的加锁情况: @@ -7416,7 +7411,7 @@ InnoDB 加锁的基本单位是 next-key lock,该锁是行锁和 gap lock 的 * 范围查询无论是否是唯一索引,都需要访问到不满足条件的第一个值为止 * 对于联合索引且是唯一索引,如果 where 条件只包括联合索引的一部分,那么会加间隙锁 -间隙锁优点:RR 级别下间隙锁可以解决事务的一部分的**幻读问题**,通过对间隙加锁,可以防止读取过程中数据条目发生变化。一部分的意思是不会对全部间隙加锁,只能加锁一部分的间隙 +间隙锁优点:RR 级别下间隙锁可以**解决事务的一部分的幻读问题**,通过对间隙加锁,可以防止读取过程中数据条目发生变化。一部分的意思是不会对全部间隙加锁,只能加锁一部分的间隙 间隙锁危害: @@ -7752,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 中记录当前应用中继日志的文件名和位点以便下一次执行 @@ -7836,7 +7831,7 @@ coordinator 就是原来的 SQL Thread,并行复制中它不再直接更新数 * 线程分配完成并不是立即执行,为了防止造成更新覆盖,更新同一 DB 的两个事务必须被分发到同一个工作线程 * 同一个事务不能被拆开,必须放到同一个工作线程 -MySQL 5.6 版本的策略:每个线程对应一个 hash 表,用于保存当前这个线程的执行队列里的事务所涉及的表,hash 表的 key 是数据库 名,value 是一个数字,表示队列中有多少个事务修改这个库,适用于主库上有多个 DB 的情况 +MySQL 5.6 版本的策略:每个线程对应一个 hash 表,用于保存当前这个线程的执行队列里的事务所涉及的表,hash 表的 key 是数据库名,value 是一个数字,表示队列中有多少个事务修改这个库,适用于主库上有多个 DB 的情况 每个事务在分发的时候,跟线程的**冲突**(事务操作的是同一个库)关系包括以下三种情况: @@ -7846,7 +7841,7 @@ MySQL 5.6 版本的策略:每个线程对应一个 hash 表,用于保存当 优缺点: -* 构造 hash 值的时候很快,只需要库名,而且一个实例上 DB 数也不会很多,不会出现需要构造很多个项的情况 +* 构造 hash 值的时候很快,只需要库名,而且一个实例上 DB 数也不会很多,不会出现需要构造很多项的情况 * 不要求 binlog 的格式,statement 格式的 binlog 也可以很容易拿到库名(日志章节详解了 binlog) * 主库上的表都放在同一个 DB 里面,这个策略就没有效果了;或者不同 DB 的热点不同,比如一个是业务逻辑库,一个是系统配置库,那也起不到并行的效果,需要**把相同热度的表均匀分到这些不同的 DB 中**,才可以使用这个策略 @@ -7977,7 +7972,7 @@ SELECT master_pos_wait(file, pos[, timeout]); * 选定一个从库执行判断位点语句,如果返回值是 >=0 的正整数,说明从库已经同步完事务,可以在这个从库执行查询语句 * 如果出现其他情况,需要到主库执行查询语句 -注意:如果所有的从库都延迟超过 timeout 秒,查询压力就都跑到主库上,所以需要进行权衡 +注意:如果所有的从库都延迟超过 timeout 秒,查询压力就都跑到主库上,所以需要进行权衡 @@ -9272,7 +9267,7 @@ Redis 通过过期字典可以检查一个给定键是否过期: 针对过期数据有三种删除策略: - 定时删除 -- 惰性删除 +- 惰性删除(被动删除) - 定期删除 Redis 采用惰性删除和定期删除策略的结合使用 @@ -9333,7 +9328,7 @@ Redis 采用惰性删除和定期删除策略的结合使用 - activeExpireCycle() 对某个数据库中的每个 expires 进行检测,工作模式: - * 轮询每个数据库,从数据库中取出一定数量的随机键进行检查,并删除其中的过期键 + * 轮询每个数据库,从数据库中取出一定数量的随机键进行检查,并删除其中的过期键,如果过期 key 的比例超过了 25%,则继续重复此过程,直到过期 key 的比例下降到 25% 以下,或者这次任务的执行耗时超过了 25 毫秒 * 全局变量 current_db 用于记录 activeExpireCycle() 的检查进度(哪一个数据库),下一次调用时接着该进度处理 * 随着函数的不断执行,服务器中的所有数据库都会被检查一遍,这时将 current_db 重置为 0,然后再次开始新一轮的检查 @@ -9721,7 +9716,7 @@ Redis 基于 Reactor 模式开发了网络事件处理器,这个处理器被 -尽管多个文件事件可能会并发出现,但是 I/O 多路复用程序将所有产生事件的套接字处理请求放入一个**单线程的执行队列**中,通过队列有序、同步的向文件事件分派器传送套接字,上一个套接字产生的事件处理完后,才会继续向分派器传送下一个 +I/O 多路复用程序将所有产生事件的套接字处理请求放入一个**单线程的执行队列**中,通过队列有序、同步的向文件事件分派器传送套接字,上一个套接字产生的事件处理完后,才会继续向分派器传送下一个 @@ -9806,8 +9801,6 @@ Redis 的时间事件分为以下两类: 无序链表并不影响时间事件处理器的性能,因为正常模式下的 Redis 服务器**只使用 serverCron 一个时间事件**,在 benchmark 模式下服务器也只使用两个时间事件,所以无序链表不会影响服务器的性能,几乎可以按照一个指针处理 -服务器 → serverCron 详解该时间事件 - *** @@ -10801,7 +10794,7 @@ SDS 通过未使用空间解除了字符串长度和底层数组长度之间的 内存重分配涉及复杂的算法,需要执行**系统调用**,是一个比较耗时的操作,SDS 的两种优化策略: -* 空间预分配:当 SDS 的 API 进行修改并且需要进行空间扩展时,程序不仅会为 SDS 分配修改所必需的空间, 还会为 SDS 分配额外的未使用空间 +* 空间预分配:当 SDS 需要进行空间扩展时,程序不仅会为 SDS 分配修改所必需的空间, 还会为 SDS 分配额外的未使用空间 * 对 SDS 修改之后,SDS 的长度(len 属性)小于 1MB,程序分配和 len 属性同样大小的未使用空间,此时 len 和 free 相等 @@ -10813,7 +10806,7 @@ SDS 通过未使用空间解除了字符串长度和底层数组长度之间的 在扩展 SDS 空间前,API 会先检查 free 空间是否足够,如果足够就无需执行内存重分配,所以通过预分配策略,SDS 将连续增长 N 次字符串所需内存的重分配次数从**必定 N 次降低为最多 N 次** -* 惰性空间释放:当 SDS 的 API 需要缩短字符串时,程序并不立即使用内存重分配来回收缩短后多出来的字节,而是使用 free 属性将这些字节的数量记录起来,并等待将来使用 +* 惰性空间释放:当 SDS 缩短字符串时,程序并不立即使用内存重分配来回收缩短后多出来的字节,而是使用 free 属性将这些字节的数量记录起来,并等待将来复用 SDS 提供了相应的 API 来真正释放 SDS 的未使用空间,所以不用担心空间惰性释放策略造成的内存浪费问题 @@ -11012,7 +11005,7 @@ load_factor = ht[0].used / ht[0].size 原因:执行该命令的过程中,Redis 需要创建当前服务器进程的子进程,而大多数操作系统都采用写时复制(copy-on­-write)技术来优化子进程的使用效率,通过提高执行扩展操作的负载因子,尽可能地避免在子进程存在期间进行哈希表扩展操作,可以避免不必要的内存写入操作,最大限度地节约内存 -哈希表执行收缩的条件:负载因子小于 0.1(自动执行,servreCron 中检测),缩小为字典中数据个数的 50% 左右 +哈希表执行收缩的条件:负载因子小于 0.1(自动执行,servreCron 中检测) @@ -11037,7 +11030,7 @@ 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 操作已完成 +* 随着字典操作的不断执行,最终在某个时间点 ht[0] 的所有键值对都被 rehash 至 ht[1],将 rehashidx 属性的值设为 -1 渐进式 rehash 采用**分而治之**的方式,将 rehash 键值对所需的计算工作均摊到对字典的每个添加、删除、查找和更新操作上,从而避免了集中式 rehash 带来的庞大计算量 @@ -11112,11 +11105,11 @@ typedef struct zskiplistNode { 前进指针:forward 用于从表头到表尾方向**正序(升序)遍历节点**,遇到 NULL 停止遍历 -跨度:span 用于记录两个节点之间的距离,用来**计算排位(rank)**: +跨度:span 用于记录两个节点之间的距离,用来计算排位(rank): * 两个节点之间的跨度越大相距的就越远,指向 NULL 的所有前进指针的跨度都为 0 -* 在查找某个节点的过程中,将沿途访问过的所有层的跨度累计起来,结果就是目标节点在跳跃表中的排位,按照上图所示: +* 在查找某个节点的过程中,**将沿途访问过的所有层的跨度累计起来,结果就是目标节点在跳跃表中的排位**,按照上图所示: 查找分值为 3.0 的节点,沿途经历的层:查找的过程只经过了一个层,并且层的跨度为 3,所以目标节点在跳跃表中的排位为 3 @@ -11124,9 +11117,9 @@ typedef struct zskiplistNode { 后退指针:backward 用于从表尾到表头方向**逆序(降序)遍历节点** -分值:score 属性一个 double 类型的浮点数,跳跃表中的所有节点都**按分值从小到大来排序** +分值:score 属性一个 double 类型的浮点数,跳跃表中的所有节点都按分值从小到大来排序 -成员对象:obj 属性是一个指针,指向一个 SDS 字符串对象。同一个跳跃表中,各个节点保存的成员对象必须是唯一的,但是多个节点保存的分值可以是相同的,分值相同的节点将按照成员对象在字典序中的大小来进行排序(从小到大) +成员对象:obj 属性是一个指针,指向一个 SDS 字符串对象。同一个跳跃表中,各个节点保存的**成员对象必须是唯一的**,但是多个节点保存的分值可以是相同的,分值相同的节点将按照成员对象在字典序中的大小来进行排序(从小到大) @@ -11171,7 +11164,7 @@ encoding 取值为三种:INTSET_ENC_INT16、INTSET_ENC_INT32、INTSET_ENC_INT6 -#### 升级降级 +#### 类型升级 整数集合添加的新元素的类型比集合现有所有元素的类型都要长时,需要先进行升级(upgrade),升级流程: @@ -11245,7 +11238,7 @@ previous_entry_length:以字节为单位记录了压缩列表中前一个节 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) @@ -11334,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) @@ -11555,7 +11548,7 @@ Redis 所有操作都是**原子性**的,采用**单线程**机制,命令是 -#### 对象 +#### 实现 字符串对象的编码可以是 int、raw、embstr 三种 @@ -12092,8 +12085,8 @@ set 类型:与 hash 存储结构哈希表完全相同,只是仅存储键不 使用字典加跳跃表的优势: -* 字典为有序集合创建了一个从成员到分值的映射,用 O(1) 复杂度查找给定成员的分值 -* 排序操作使用跳跃表完成,节省每次重新排序带来的时间成本和空间成本 +* 字典为有序集合创建了一个**从成员到分值的映射**,用 O(1) 复杂度查找给定成员的分值 +* **排序操作使用跳跃表完成**,节省每次重新排序带来的时间成本和空间成本 使用 ziplist 格式存储需要满足以下两个条件: @@ -12102,6 +12095,11 @@ set 类型:与 hash 存储结构哈希表完全相同,只是仅存储键不 当元素比较多时,此时 ziplist 的读写效率会下降,时间复杂度是 O(n),跳表的时间复杂度是 O(logn) +为什么用跳表而不用平衡树? + +* 在做范围查找的时候,跳表操作简单(前进指针或后退指针),平衡树需要回旋查找 +* 跳表比平衡树实现简单,平衡树的插入和删除操作可能引发子树的旋转调整,而跳表的插入和删除只需要修改相邻节点的指针 + *** @@ -12356,7 +12354,7 @@ AOF:将数据的操作过程进行保存,日志形式,存储操作过程 #### 文件创建 -RDB 持久化功能所生成的 RDB 文件 是一个经过压缩的紧凑二进制文件,通过该文件可以还原生成 RDB 文件时的数据库状态,有两个 Redis 命令可以生成 RDB 文件,一个是 SAVE,另一个是 BGSAVE +RDB 持久化功能所生成的 RDB 文件是一个经过压缩的紧凑二进制文件,通过该文件可以还原生成 RDB 文件时的数据库状态,有两个 Redis 命令可以生成 RDB 文件,一个是 SAVE,另一个是 BGSAVE @@ -12657,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 后的所有写指令 @@ -12760,7 +12758,7 @@ bgrewriteaof * 子进程进行 AOF 重写期间,服务器进程(父进程)可以继续处理命令请求 -* 子进程带有服务器进程的数据副本,使用子进程而不是线程,可以在避免使用锁的情况下, 保证数据的安全性 +* 子进程带有服务器进程的数据副本,使用子进程而不是线程,可以在避免使用锁的情况下, 保证数据安全性 ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-AOF手动重写原理.png) @@ -12826,7 +12824,7 @@ RDB 的特点 AOF 特点: -* AOF 的优点:数据持久化有较好的实时性,通过 AOF 重写可以降低文件的体积 +* AOF 的优点:数据持久化有**较好的实时性**,通过 AOF 重写可以降低文件的体积 * AOF 的缺点:文件较大时恢复较慢 AOF 和 RDB 同时开启,系统默认取 AOF 的数据(数据不会存在丢失) @@ -12955,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 @@ -13440,8 +13438,6 @@ Redis 复制 EVAL、SCRIPT FLUSH、SCRIPT LOAD 三个命令的方法和复制普 - - *** @@ -13499,7 +13495,7 @@ Redis 分布式锁的基本使用,悲观锁 `NX`:只在键不存在时,才对键进行设置操作,`SET key value NX` 效果等同于 `SETNX key value` - `XX` :只在键已经存在时,才对键进行设置操作 + `XX`:只在键已经存在时,才对键进行设置操作 `EX`:设置键 key 的过期时间,单位时秒 @@ -13637,7 +13633,7 @@ end 主从一致性:集群模式下,主从同步存在延迟,当加锁后主服务器宕机时,从服务器还没同步主服务器中的锁数据,此时从服务器升级为主服务器,其他线程又可以获取到锁 -将服务器升级为多主多从,: +将服务器升级为多主多从: * 获取锁需要从所有主服务器 SET 成功才算获取成功 * 某个 master 宕机,slave 还没有同步锁数据就升级为 master,其他线程尝试加锁会加锁失败,因为其他 master 上已经存在该锁 @@ -14011,7 +14007,7 @@ PSYNC 命令的调用方法有两种 #### 心跳机制 -心跳机制:进入命令传播阶段,从服务器默认会以每秒一次的频率,向主服务器发送命令:`REPLCONF ACK `,re_offset 是从服务器当前的复制偏移量 +心跳机制:进入命令传播阶段,**从服务器**默认会以每秒一次的频率,**向主服务器发送命令**:`REPLCONF ACK `,replication_offset 是从服务器当前的复制偏移量 心跳的作用: @@ -14050,10 +14046,13 @@ 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 @@ -14114,7 +14113,7 @@ master 的 CPU 占用过高或 slave 频繁断开连接 * 出现的原因: * slave 每 1 秒发送 REPLCONF ACK 命令到 master - * 当 slave 接到了慢查询时(keys * ,hgetall等),会大量占用 CPU 性能 + * 当 slave 接到了慢查询时(keys * ,hgetall 等),会大量占用 CPU 性能 * master 每 1 秒调用复制定时函数 replicationCron(),比对 slave 发现长时间没有进行响应 最终导致 master 各种资源(输出缓冲区、带宽、连接等)被严重占用 @@ -14278,7 +14277,7 @@ Sentinel 本质上只是一个运行在特殊模式下的 Redis 服务器,当 #### 代码替换 -将一部分普通 Redis服务器使用的代码替换成 Sentinel 专用代码 +将一部分普通 Redis 服务器使用的代码替换成 Sentinel 专用代码 Redis 服务器端口: @@ -14322,7 +14321,7 @@ struct sentinelState { // 当前纪元,用于实现故障转移 uint64_t current_epoch; - // 保存了所有被这个sentinel监视的主服务器 + // 【保存了所有被这个sentinel监视的主服务器】 dict *masters; // 是否进入了 TILT 模式 @@ -14439,10 +14438,10 @@ typedef struct sentinelAddr { ##### 主服务器 -Sentinel 默认会以每十秒一次的频率,通过命令连接向被监视的主服务器发送 INFO 命令,来获取主服务器的当前信息 +Sentinel 默认会以每十秒一次的频率,通过命令连接向被监视的主服务器发送 INFO 命令,来获取主服务器的信息 * 一部分是主服务器本身的信息,包括 runid 域记录的服务器运行 ID,以及 role 域记录的服务器角色 -* 另一部分是服务器属下所有从服务器的信息,每个从服务器都由一个 slave 字符串开头的行记录,根据这些 IP 地址和端口号,Sentinel 无须用户提供从服务器的地址信息,就可以自动发现从服务器 +* 另一部分是服务器属下所有从服务器的信息,每个从服务器都由一个 slave 字符串开头的行记录,根据这些 IP 地址和端口号,Sentinel 无须用户提供从服务器的地址信息,就可以**自动发现从服务器** ```sh # Server @@ -14471,7 +14470,7 @@ slave1: ip=l27.0.0.1, port=22222, state=online, offset=22, lag=0 ##### 从服务器 -当 Sentinel 发现主服务器有新的从服务器出现时,会为这个新的从服务器创建相应的实例结构,还会创建到从服务器的命令连接和订阅连接,所以 Sentinel 对所有的从服务器之间都可以进行命令操作 +当 Sentinel 发现主服务器有新的从服务器出现时,会为这个新的从服务器创建相应的实例结构,还会**创建到从服务器的命令连接和订阅连接**,所以 Sentinel 对所有的从服务器之间都可以进行命令操作 Sentinel 默认会以每十秒一次的频率,向从服务器发送 INFO 命令: @@ -14538,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 提供发布订阅机制 @@ -14628,7 +14614,7 @@ SENTINEL is-master-down-by-addr 源 Sentinel 将统计其他 Sentinel 同意主服务器已下线的数量,当这一数量达到配置指定的判断客观下线所需的数量(quorum)时,Sentinel 会将主服务器对应实例结构 flags 属性的 SRI_O_DOWN 标识打开,代表客观下线,并对主服务器执行故障转移操作 -注意:不同 Sentinel 判断客观下线的条件可能不同,因为载入的配置文件中的属性(quorum)可能不同 +注意:**不同 Sentinel 判断客观下线的条件可能不同**,因为载入的配置文件中的属性 quorum 可能不同 @@ -14638,7 +14624,7 @@ SENTINEL is-master-down-by-addr ### 领头选举 -主服务器被判断为客观下线时,监视这个主服务器的各个 Sentinel 会进行协商,选举出一个领头 Sentinel 对下线服务器执行故障转移 +主服务器被判断为客观下线时,**监视该主服务器的各个 Sentinel 会进行协商**,选举出一个领头 Sentinel 对下线服务器执行故障转移 Redis 选举领头 Sentinel 的规则: @@ -14647,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` 票 选举过程: @@ -14655,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 个节点的原因: @@ -14663,7 +14649,10 @@ Sentinel 集群至少 3 个节点的原因: * 如果 Sentinel 集群只有 2 个 Sentinel 节点,则领头选举需要 `2/2 + 1 = 2` 票,如果一个节点挂了,那就永远选不出领头 * Sentinel 集群允许 1 个 Sentinel 节点故障则需要 3 个节点的集群,允许 2 个节点故障则需要 5 个节点集群 +**如何获取哨兵节点的半数数量**? +* 客观下线是通过配置文件获取的数量,达到 quorum 就客观下线 +* 哨兵数量是通过主节点是实例结构中,保存着监视该主节点的所有哨兵信息,从而获取得到 @@ -14762,7 +14751,8 @@ typedef struct clusterState { // 集群当前的状态,是在线还是下线 int state; - // 集群中至少处理着一个槽的节点的数量,为0表示集群目前没有任何节点在处理槽 + // 集群中至少处理着一个槽的(主)节点的数量,为0表示集群目前没有任何节点在处理槽 + // 【选举时投票数量超过半数,从这里获取的】 int size; // 集群节点名单(包括 myself 节点),字典的键为节点的名字,字典的值为节点对应的clusterNode结构 @@ -14858,7 +14848,7 @@ CLUSTER MEET #### 基本操作 -Redis 集群通过分片的方式来保存数据库中的键值对,集群的整个数据库被分为16384 个槽(slot),数据库中的每个键都属于 16384 个槽中的一个,集群中的每个节点可以处理 0 个或最多 16384 个槽(**每个主节点存储的数据并不一样**) +Redis 集群通过分片的方式来保存数据库中的键值对,集群的整个数据库被分为 16384 个槽(slot),数据库中的每个键都属于 16384 个槽中的一个,集群中的每个节点可以处理 0 个或最多 16384 个槽(**每个主节点存储的数据并不一样**) * 当数据库中的 16384 个槽都有节点在处理时,集群处于上线状态(ok) * 如果数据库中有任何一个槽得到处理,那么集群处于下线状态(fail) @@ -14943,7 +14933,7 @@ typedef struct clusterState { #### 集群数据 -集群节点保存键值对以及键值对过期时间的方式,与单机 Redis 服务器保存键值对以及键值对过期时间的方式完全相同,但是集群节点只能使用 0 号数据库,单机服务器可以任意使用 +集群节点保存键值对以及键值对过期时间的方式,与单机 Redis 服务器保存键值对以及键值对过期时间的方式完全相同,但是**集群节点只能使用 0 号数据库**,单机服务器可以任意使用 除了将键值对保存在数据库里面之外,节点还会用 clusterState 结构中的 slots_to_keys 跳跃表来**保存槽和键之间的关系** @@ -15203,7 +15193,7 @@ struct clusterNode { #### 故障检测 -集群中的每个节点都会定期地向集群中的其他节点发送 PING 消息,来检测对方是否在线,如果接收 PING 的节点没有在规定的时间内返回 PONG 消息,那么发送消息节点就会将接收节点标记为**疑似下线**(probable fail, PFAIL) +集群中的每个节点都会定期地向集群中的其他节点发送 PING 消息,来检测对方是否在线,如果接收 PING 的节点没有在规定的时间内返回 PONG 消息,那么发送消息节点就会将接收节点标记为**疑似下线**(probable fail) 集群中的节点会互相发送消息,来**交换集群中各个节点的状态信息**,当一个主节点 A 通过消息得知主节点 B 认为主节点 C 进入了疑似下线状态时,主节点 A 会在 clusterState.nodes 字典中找到主节点 C 所对应的节点,并将主节点 B 的下线报告(failure report)添加到 clusterNode.fail_reports 链表里面 @@ -15267,7 +15257,7 @@ struct clusterNodeFailReport { -选举新主节点的方法和选举领头 Sentinel 的方法非常相似,两者都是基于 Raft 算法的领头选举(eader election)方法实现的 +选举新主节点的方法和选举领头 Sentinel 的方法非常相似,两者都是基于 Raft 算法的领头选举(leader election)方法实现的 @@ -15470,7 +15460,7 @@ typedef struct clusterMsgDataPublish { ### 脑裂问题 -脑裂指在主从集群中,同时有两个相同的主节点能接收写请求,导致客户端不知道应该往哪个主节点写入数据,导致不同客户端往不同的主节点上写入数据 +脑裂指在主从集群中,同时有两个相同的主节点能接收写请求,导致客户端不知道应该往哪个主节点写入数据,最后 不同客户端往不同的主节点上写入数据 * 原主节点并没有真的发生故障,由于某些原因无法处理请求(CPU 利用率很高、自身阻塞),无法按时响应心跳请求,被哨兵/集群主节点错误的判断为下线 * 在被判断下线之后,原主库又重新开始处理请求了,哨兵/集群主节点还没有完成主从切换,客户端仍然可以和原主库通信,客户端发送的写操作就会在原主库上写入数据,造成脑裂问题 @@ -15481,6 +15471,7 @@ typedef struct clusterMsgDataPublish { * 假设从库有 K 个,可以将 min-slaves-to-write 设置为 K/2+1(如果 K 等于 1,就设为 1) * 将 min-slaves-max-lag 设置为十几秒(例如 10~20s) +* 在假故障期间无法响应哨兵发出的心跳测试,无法和从库进行 ACK 确认,并且没有足够的从库,**拒绝客户端的写入** @@ -15838,7 +15829,7 @@ OK Redis 的管道 Pipeline 机制可以一次处理多条指令 * Pipeline 中的多条命令非原子性,因为在向管道内添加命令时,其他客户端的发送的命令仍然在执行 -* 原生批命令(mset 等)是服务端实现,而 pipeline 需要服务端与客户端共同完成 +* 原生批命令(MSET 等)是服务端实现,而 Pipeline 需要服务端与客户端共同完成 使用 Pipeline 封装的命令数量不能太多,数据量过大会增加客户端的等待时间,造成网络阻塞,Jedis 中的 Pipeline 使用方式: @@ -16148,183 +16139,37 @@ Read-Through Pattern 也存在首次不命中的问题,采用缓存预热解 - 在抢购或秒杀场景下,可能因商品对应库存 Key 的请求量过大,超出 Redis 处理能力造成超卖 - 热 Key 的请求压力数量超出 Redis 的承受能力易造成缓存击穿,即大量请求将被直接指向后端的存储层,导致存储访问量激增甚至宕机,从而影响其他业务 +热 Key 分类两种,治理方式如下: +* 一种是单一数据,比如秒杀场景,假设总量 10000 可以拆为多个 Key 进行访问,每次对请求进行路由到不同的 Key 访问,保证最终一致性,但是会出现访问不同 Key 产生的剩余量是不同的,这时可以通过前端进行 Mock 假数据 +* 一种是多数据集合,比如进行 ID 过滤,这时可以添加本地 LRU 缓存,减少对热 Key 的访问 -参考文档:https://help.aliyun.com/document_detail/353223.html - - - -*** - - - - - -### 性能指标 - -Redis 中的监控指标如下: - -* 性能指标:Performance - - 响应请求的平均时间: - - ```sh - latency - ``` - 平均每秒处理请求总数: - ```sh - instantaneous_ops_per_sec - ``` - - 缓存查询命中率(通过查询总次数与查询得到非nil数据总次数计算而来): - - ```sh - hit_rate(calculated) - ``` - -* 内存指标:Memory - - 当前内存使用量: - - ```sh - used_memory - ``` - 内存碎片率(关系到是否进行碎片整理): - ```sh - mem_fragmentation_ratio - ``` - - 为避免内存溢出删除的key的总数量: - - ```sh - evicted_keys - ``` - - 基于阻塞操作(BLPOP等)影响的客户端数量: - - ```sh - blocked_clients - ``` - -* 基本活动指标:Basic_activity - - 当前客户端连接总数: - - ```sh - connected_clients - ``` - - 当前连接 slave 总数: - - ```sh - connected_slaves - ``` - - 最后一次主从信息交换距现在的秒: - - ```sh - master_last_io_seconds_ago - ``` - - key 的总数: - - ```sh - keyspace - ``` - -* 持久性指标:Persistence - - 当前服务器其最后一次 RDB 持久化的时间: - - ```sh - rdb_last_save_time - ``` - - 当前服务器最后一次 RDB 持久化后数据变化总量: - - ```sh - rdb_changes_since_last_save - ``` - -* 错误指标:Error - - 被拒绝连接的客户端总数(基于达到最大连接值的因素): - - ```sh - rejected_connections - ``` - - key未命中的总次数: - - ```sh - keyspace_misses - ``` - - 主从断开的秒数: - - ```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 ] - ``` +参考文档:https://help.aliyun.com/document_detail/353223.html - 范例:100 个连接,5000 次请求对应的性能 - ```sh - redis-benchmark -c 100 -n 5000 - ``` - ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-redis-benchmark指令.png) +*** -* redis-cli - monitor:启动服务器调试信息 - ```sh - monitor - ``` +### 慢查询 - slowlog:慢日志 +确认服务和 Redis 之间的链路是否正常,排除网络原因后进行 Redis 的排查: - ```sh - slowlog [operator] #获取慢查询日志 - ``` - - * get :获取慢查询日志信息 - * len :获取慢查询日志条目数 - * reset :重置慢查询日志 +* 使用复杂度过高的命令 +* 操作大 key,分配内存和释放内存会比较耗时 +* key 集中过期,导致定时任务需要更长的时间去清理 +* 实例内存达到上限,每次写入新的数据之前,Redis 必须先从实例中踢出一部分数据 - 相关配置: - - ```sh - slowlog-log-slower-than 1000 #设置慢查询的时间下线,单位:微妙 - slowlog-max-len 100 #设置慢查询命令对应的日志显示长度,单位:命令数 - ``` +参考文章:https://www.cnblogs.com/traditional/p/15633919.html(非常好) @@ -16512,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; - } - } - ``` - - - *** @@ -16811,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("连接数量已用尽"); - } - } - ``` - - - -*** - - - -##### 动态代理 +#### 归还连接 使用动态代理的方式来改进 @@ -17281,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 d75e634..f00ae48 100644 --- a/Frame.md +++ b/Frame.md @@ -739,9 +739,13 @@ Maven 的插件用来执行生命周期中的相关事件 ### 继承 -作用:通过继承可以实现在子工程中沿用父工程中的配置 +Maven 中的继承与 Java 中的继承相似,可以实现在子工程中沿用父工程中的配置 -- Maven 中的继承与 Java 中的继承相似,在子工程中配置继承关系 +dependencyManagement 里只是声明依赖,并不实现引入,所以子工程需要显式声明需要用的依赖 + +- 如果子工程中未声明依赖,则不会从父项目继承下来 +- 在子工程中声明该依赖项,并且不指定具体版本,才会从父项目中继承该项,version 和 scope 都继承取自父工程 pom 文件 +- 如果子工程中指定了版本号,那么使用子工程中指定的 jar 版本 制作方式: @@ -3435,6 +3439,8 @@ RCVBUF_ALLOCATOR:属于 SocketChannal 参数 ### 消息队列 +#### 应用场景 + 消息队列是一种先进先出的数据结构,常见的应用场景: * 应用解耦:系统的耦合性越高,容错性就越低 @@ -3459,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 的性能的影响较小 + + + **** @@ -4555,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(索引文件)数据 @@ -4593,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,随机读的性能也会有所提升 @@ -4627,8 +4663,6 @@ RocketMQ 采用文件系统的方式,无论同步还是异步刷盘,都使 - - **** @@ -4698,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 仍然可以发送消息 @@ -4716,7 +4752,7 @@ BrokerServer 的高可用通过 Master 和 Slave 的配合: 如果一个 Broker 组有 Master 和 Slave,消息需要从 Master 复制到 Slave 上,有同步和异步两种复制方式: -* 同步复制方式:Master 和 Slave 均写成功后才反馈给客户端写成功状态。在同步复制方式下,如果 Master 出故障, Slave 上有全部的备份数据,容易恢复,但是同步复制会增大数据写入延迟,降低系统吞吐量 +* 同步复制方式:Master 和 Slave 均写成功后才反馈给客户端写成功状态(写 Page Cache)。在同步复制方式下,如果 Master 出故障, Slave 上有全部的备份数据,容易恢复,但是同步复制会增大数据写入延迟,降低系统吞吐量 * 异步复制方式:只要 Master 写成功,即可反馈给客户端写成功状态,系统拥有较低的延迟和较高的吞吐量,但是如果 Master 出了故障,有些数据因为没有被写入 Slave,有可能会丢失 @@ -4737,6 +4773,8 @@ RocketMQ 支持消息的高可靠,影响消息可靠性的几种情况: 后两种属于单点故障,且无法恢复,一旦发生,在此单点上的消息全部丢失。RocketMQ 在这两种情况下,通过主从异步复制,可保证 99% 的消息不丢,但是仍然会有极少量的消息可能丢失。通过**同步双写技术**可以完全避免单点,但是会影响性能,适合对消息可靠性要求极高的场合,RocketMQ 从 3.0 版本开始支持同步双写 +一般而言,我们会建议采取同步双写 + 异步刷盘的方式,在消息的可靠性和性能间有一个较好的平衡 + **** @@ -4822,7 +4860,7 @@ Consumer 端实现负载均衡的核心类 **RebalanceImpl** 对比下 RebalancePushImpl 和 RebalancePullImpl 两个实现类的 dispatchPullRequest() 方法,RebalancePullImpl 类里面的该方法为空 -消息消费队列在同一消费组不同消费者之间的负载均衡,其核心设计理念是在**一个消息消费队列在同一时间只允许被同一消费组内的一个消费者消费,一个消息消费者能同时消费多个消息队列** +消息消费队列在**同一消费组不同消费者之间的负载均衡**,其核心设计理念是在**一个消息消费队列在同一时间只允许被同一消费组内的一个消费者消费,一个消息消费者能同时消费多个消息队列** @@ -4883,7 +4921,7 @@ IndexFile 文件的存储在 `$HOME\store\index${fileName}`,文件名 fileName #### 消息重投 -生产者在发送消息时,同步消息和异步消息失败会重投,oneway 没有任何保证。消息重投保证消息尽可能发送成功、不丢失,但当出现消息量大、网络抖动时,可能会造成消息重复;生产者主动重发、Consumer 负载变化也会导致重复消息。 +生产者在发送消息时,同步消息和异步消息失败会重投,oneway 没有任何保证。消息重投保证消息尽可能发送成功、不丢失,但当出现消息量大、网络抖动时,可能会造成消息重复;生产者主动重发、Consumer 负载变化也会导致重复消息 如下方法可以设置消息重投策略: @@ -5045,6 +5083,31 @@ public class MessageListenerImpl implements MessageListener { +*** + + + +### 高可靠性 + +RocketMQ 消息丢失可能发生在以下三个阶段: + +- 生产阶段:消息在 Producer 发送端创建出来,经过网络传输发送到 Broker 存储端 + - 生产者得到一个成功的响应,就可以认为消息的存储和消息的消费都是可靠的 + - 消息重投机制 +- 存储阶段:消息在 Broker 端存储,如果是主备或者多副本,消息会在这个阶段被复制到其他的节点或者副本上 + - 单点:刷盘机制(同步或异步) + - 主从:消息同步机制(异步复制或同步双写,主从复制章节详解) + - 过期删除:操作 CommitLog、ConsumeQueue 文件是基于文件内存映射机制,并且在启动的时候会将所有的文件加载,为了避免内存与磁盘的浪费,让磁盘能够循环利用,防止磁盘不足导致消息无法写入等引入了文件过期删除机制。最终使得磁盘水位保持在一定水平,最终保证新写入消息的可靠存储 +- 消费阶段:Consumer 消费端从 Broker存储端拉取消息,经过网络传输发送到 Consumer 消费端上 + - 消息重试机制来最大限度的保证消息的消费 + - 消费失败的进行消息回退,重试次数过多的消息放入死信队列 + + + +推荐文章:https://cdn.modb.pro/db/394751 + + + **** @@ -7374,7 +7437,7 @@ AllocateMappedFileService **创建 MappedFile 服务** ReputMessageService 消息分发服务,用于构**建 ConsumerQueue 和 IndexFile 文件** -* run():**循环执行 doReput 方法**,所以发送的消息存储进 CL 就可以产生对应的 CQ,每执行一次线程休眠 1 毫秒 +* run():**循环执行 doReput 方法**,**所以发送的消息存储进 CL 就可以产生对应的 CQ**,每执行一次线程休眠 1 毫秒 ```java public void run() @@ -7566,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 进行过滤 @@ -10209,16 +10272,17 @@ ConsumeRequest 是 ConsumeMessageOrderlyService 的内部类,是一个 Runnabl 生产流程: -* 首先获取当前消息主题的发布信息,获取不到去 Namesrv 获取(默认有 TBW102),并将获取的到的路由数据转化为发布数据,**创建 MQ 队列**,客户端实例同样更新订阅数据,创建 MQ 队列,放入负载均衡服务 topicSubscribeInfoTable 中 +* 首先获取当前消息主题的发布信息,获取不到去 Namesrv 获取(默认有 TBW102),并将获取的到的路由数据转化为发布数据,**创建 MQ 队列在多个 Broker 组**(一组代表一主多从的 Broker 架构),客户端实例同样更新订阅数据,创建 MQ 队列,放入负载均衡服务 topicSubscribeInfoTable 中 * 然后从发布数据中选择一个 MQ 队列发送消息 * 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 再次提交到线程池内处理 @@ -10712,7 +10776,7 @@ CAP 理论指的是在一个分布式系统中,Consistency(一致性)、Av CAP 三个基本需求,因为 P 是必须的,因此分布式系统选择就在 CP 或者 AP 中: * 一致性:指数据在多个副本之间是否能够保持数据一致的特性,当一个系统在数据一致的状态下执行更新操作后,也能保证系统的数据仍然处于一致的状态 -* 可用性:指系统提供的服务必须一直处于可用的状态,对于用户的每一个操作请求总是能够在有限的时间内返回结果 +* 可用性:指系统提供的服务必须一直处于可用的状态,即使集群中一部分节点故障,对于用户的每一个操作请求总是能够在有限的时间内返回结果 * 分区容错性:分布式系统在遇到任何网络分区故障时,仍然能够保证对外提供服务,不会宕机,除非是整个网络环境都发生了故障 diff --git a/Java.md b/Java.md index da0a45a..23a9e8b 100644 --- a/Java.md +++ b/Java.md @@ -330,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) @@ -430,7 +430,7 @@ public static void main(String[] args) { | 堆内存 | 存储对象或者数组,new 来创建的,都存储在堆内存 | | 方法栈 | 方法运行时使用的内存,比如 main 方法运行,进入方法栈中执行 | -**内存分配图**:Java 内存分配 +内存分配图:**Java 数组分配在堆内存** * 一个数组内存图 @@ -2865,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` (左闭右开),伪随机地生成并返回 @@ -5891,7 +5891,7 @@ class Dog{} ### 基本介绍 -异常:程序在编译或者执行的过程中可能出现的问题,Java 为常见的代码异常都设计一个类来代表。 +异常:程序在编译或者执行的过程中可能出现的问题,Java 为常见的代码异常都设计一个类来代表 错误:Error ,程序员无法处理的错误,只能重启系统,比如内存奔溃,JVM 本身的奔溃 @@ -6089,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("程序结束。。。。。。"); - } -} -``` - **** @@ -8625,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(){ } } @@ -8642,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{ -} -``` - - - **** @@ -8837,8 +8767,6 @@ XML 文件中常见的组成元素有:文档声明、元素、属性、注释、 #### DTD -##### DTD 定义 - DTD 是文档类型定义(Document Type Definition)。DTD 可以定义在 XML 文档中出现的元素、这些元素出现的次序、它们如何相互嵌套以及 XML 文档结构的其它详细信息。 DTD 规则: @@ -8922,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 @@ -9058,13 +8875,7 @@ persondtd.dtd 文件 4. dtd 里面元素类型的取值比较单一常见的是 PCDATA 类型,但是在 Schema 里面可以支持很多个数据类型 5. **Schema 文件约束 XML 文件的同时也被别的文件约束着** - - -*** - - - -##### XSD 规则 +XSD 规则: 1. 创建一个文件,这个文件的后缀名为 .xsd 2. 定义文档声明 @@ -9111,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 - - - -``` - *** @@ -9256,7 +8985,7 @@ public class Dom4JDemo { JavaWeb开发教程 - 张孝祥 + 张三 100.00元 @@ -9390,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"); @@ -10596,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 @@ -11328,7 +11058,7 @@ objD.fieldG = G; // 写 * **写屏障 (Store Barrier) + SATB**:当原来成员变量的引用发生变化之前,记录下原来的引用对象 - 保留 GC 开始时的对象图,即原始快照 SATB,当 GC Roots 确定后,对象图就已经确定,那后续的标记也应该是按照这个时刻的对象图走,如果期间对白色对象有了新的引用会记录下来,并且将白色对象变灰(说明可达了),重新扫描该对象的引用关系 + 保留 GC 开始时的对象图,即原始快照 SATB,当 GC Roots 确定后,对象图就已经确定,那后续的标记也应该是按照这个时刻的对象图走,如果期间对白色对象有了新的引用会记录下来,并且将白色对象变灰(说明可达了,并且原始快照中本来就应该是灰色对象),最后重新扫描该对象的引用关系 SATB (Snapshot At The Beginning) 破坏了条件一,从而保证了不会漏标 @@ -11494,7 +11224,7 @@ Java 语言提供了对象终止(finalization)机制来允许开发人员提 - 主要不足是**只使用了内存的一半** - 对于 G1 这种分拆成为大量 region 的 GC,复制而不是移动,意味着 GC 需要维护 region 之间对象引用关系,不管是内存占用或者时间开销都不小 -现在的商业虚拟机都采用这种收集算法**回收新生代**,但是并不是划分为大小相等的两块,而是一块较大的 Eden 空间和两块较小的 Survivor 空间 +现在的商业虚拟机都采用这种收集算法**回收新生代**,因为新生代 GC 频繁并且对象的存活率不高,但是并不是划分为大小相等的两块,而是一块较大的 Eden 空间和两块较小的 Survivor 空间 @@ -11601,7 +11331,7 @@ GC 性能指标: #### Serial -Serial:串行垃圾收集器,作用于新生代,是指使用单线程进行垃圾回收,采用**复制算法**,新生代基本都是复制算法,因为分区了 +Serial:串行垃圾收集器,作用于新生代,是指使用单线程进行垃圾回收,采用**复制算法**,新生代基本都是复制算法 **STW(Stop-The-World)**:垃圾回收时,只有一个线程在工作,并且 Java 应用中的所有线程都要暂停,等待垃圾回收的完成 @@ -11681,7 +11411,7 @@ Parallel Old 收集器:是一个应用于老年代的并行垃圾回收器,* * `-XX:+UseParallelGC`:手动指定年轻代使用 Paralle 并行收集器执行内存回收任务 * `-XX:+UseParalleloldcc`:手动指定老年代使用并行回收收集器执行内存回收任务 * 上面两个参数,默认开启一个,另一个也会被开启(互相激活),默认 JDK8 是开启的 -* `-XX:+UseAdaptivesizepplicy`:设置 Parallel scavenge 收集器具有**自适应调节策略**,在这种模式下,年轻代的大小、Eden 和 Survivor 的比例、晋升老年代的对象年龄等参数会被自动调整,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量 +* `-XX:+UseAdaptivesizepplicy`:设置 Parallel Scavenge 收集器具有**自适应调节策略**,在这种模式下,年轻代的大小、Eden 和 Survivor 的比例、晋升老年代的对象年龄等参数会被自动调整,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量 * `-XX:ParallelGcrhreads`:设置年轻代并行收集器的线程数,一般与 CPU 数量相等,以避免过多的线程数影响垃圾收集性能 * 在默认情况下,当 CPU 数量小于 8 个,ParallelGcThreads 的值等于 CPU 数量 * 当 CPU 数量大于 8 个,ParallelGCThreads 的值等于 3+[5*CPU Count]/8] @@ -11710,7 +11440,7 @@ CMS 收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿 - 初始标记:使用 STW 出现短暂停顿,仅标记一下 GC Roots 能直接关联到的对象,速度很快 - 并发标记:进行 GC Roots 开始遍历整个对象图,在整个回收过程中耗时最长,不需要 STW,可以与用户线程并发运行 -- 重新标记:修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,比初始标记时间长但远比并发标记时间短,需要 STW(不停顿就会一直变化,采用写屏障 + 增量更新来避免漏标情况) +- 重新标记:修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象,比初始标记时间长但远比并发标记时间短,需要 STW(不停顿就会一直变化,采用写屏障 + 增量更新来避免漏标情况) - 并发清除:清除标记为可以回收对象,**不需要移动存活对象**,所以这个阶段可以与用户线程同时并发的 Mark Sweep 会造成内存碎片,不把算法换成 Mark Compact 的原因:Mark Compact 算法会整理内存,导致用户线程使用的**对象的地址改变**,影响用户线程继续执行 @@ -11782,7 +11512,7 @@ G1 对比其他处理器的优点: - 空间整合: - CMS:标记-清除算法、内存碎片、若干次 GC 后进行一次碎片整理 - - G1:整体来看是基于标记 - 整理算法实现的收集器,从局部(Region 之间)上来看是基于复制算法实现的,两种算法都可以避免内存碎片 + - G1:整体来看是**基于标记 - 整理算法实现**的收集器,从局部(Region 之间)上来看是基于复制算法实现的,两种算法都可以避免内存碎片 - **可预测的停顿时间模型(软实时 soft real-time)**:可以指定在 M 毫秒的时间片段内,消耗在 GC 上的时间不得超过 N 毫秒 @@ -12541,7 +12271,7 @@ Java 对象创建时机: - 通过类的完全限定名称获取定义该类的二进制字节流(二进制字节码) - 将该字节流表示的静态存储结构转换为方法区的运行时存储结构(Java 类模型) -- **在内存中生成一个代表该类的 Class 对象,作为该类在方法区中的各种数据的访问入口** +- **将字节码文件加载至方法区后,在堆中生成一个代表该类的 Class 对象,作为该类在方法区中的各种数据的访问入口** 其中二进制字节流可以从以下方式中获取: @@ -12550,8 +12280,6 @@ 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 使用 @@ -12638,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 @@ -12791,7 +12519,7 @@ new 关键字会创建对象并复制 dup 一个对象引用,一个调用 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 来加载类 @@ -14468,14 +14196,9 @@ public static int invoke(Object... args) { 在 JVM 中,将符号引用转换为直接引用有两种机制: - 静态链接:当一个字节码文件被装载进 JVM 内部时,如果被调用的目标方法在编译期可知,且运行期保持不变,将调用方法的符号引用转换为直接引用的过程称之为静态链接(类加载的解析阶段) -- 动态链接:如果被调用的方法在编译期无法被确定下来,只能够在程序运行期将调用方法的符号引用转换为直接引用,由于这种引用转换过程具备动态性,因此被称为动态链接(初始化后的解析阶段) - -对应方法的绑定(分配)机制:静态绑定和动态绑定。绑定是一个字段、方法或者类从符号引用被替换为直接引用的过程,仅发生一次: - -- 静态绑定:被调用的目标方法在编译期可知,且运行期保持不变,将这个方法与所属的类型进行绑定 -- 动态绑定:被调用的目标方法在编译期无法确定,只能在程序运行期根据实际的类型绑定相关的方法 +- 动态链接:被调用的方法在编译期无法被确定下来,只能在程序运行期将调用方法的符号引用转换为直接引用,由于这种引用转换过程具备动态性,因此被称为动态链接(初始化后的解析阶段) -* Java 编译器已经区分了重载的方法(静态绑定和动态绑定),因此可以认为虚拟机中不存在重载 +* 对应方法的绑定(分配)机制:静态绑定和动态绑定,编译器已经区分了重载的方法(静态绑定和动态绑定),因此可以认为虚拟机中不存在重载 非虚方法: @@ -14509,7 +14232,7 @@ public static int invoke(Object... args) { 普通调用指令: - invokestatic:调用静态方法 -- invokespecial:调用私有实例方法、构造器,和父类的实例方法或构造器,以及所实现接口的默认方法 +- invokespecial:调用私有方法、构造器,和父类的实例方法或构造器,以及所实现接口的默认方法 - invokevirtual:调用所有虚方法(虚方法分派) - invokeinterface:调用接口方法 @@ -14541,9 +14264,6 @@ public static int invoke(Object... args) { 在编译过程中,虚拟机并不知道目标方法的具体内存地址,Java 编译器会暂时用符号引用来表示该目标方法,这一符号引用包括目标方法所在的类或接口的名字,以及目标方法的方法名和方法描述符 -* 对于静态绑定的方法调用而言,实际引用是一个指向方法的指针 -* 对于需要动态绑定的方法调用而言,实际引用则是一个方法表的索引 - 符号引用存储在方法区常量池中,根据目标方法是否为接口方法,分为接口符号引用和非接口符号引用: ```java @@ -14645,8 +14365,6 @@ Java 虚拟机中关于方法重写的判定基于方法描述符,如果子类 2. 如果在类型 C 中找到与描述符和名称都相符的方法,则进行访问**权限校验**(私有的),如果通过则返回这个方法的直接引用,查找过程结束;如果不通过,则返回 java.lang.IllegalAccessError 异常 - IllegalAccessError:表示程序试图访问或修改一个属性或调用一个方法,这个属性或方法没有权限访问,一般会引起编译器异常。如果这个错误发生在运行时,就说明一个类发生了不兼容的改变 - 3. 找不到,就会按照继承关系从下往上依次对 C 的各个父类进行第二步的搜索和验证过程 4. 如果始终没有找到合适的方法,则抛出 java.lang.AbstractMethodError 异常 @@ -14670,15 +14388,15 @@ Java 虚拟机中关于方法重写的判定基于方法描述符,如果子类 虚方法表的执行过程: -* 对于静态绑定的方法调用而言,实际引用将指向具体的目标方法 -* 对于动态绑定的方法调用而言,实际引用则是方法表的索引值,也就是方法的间接地址。Java 虚拟机获取调用者的实际类型,并在该实际类型的虚方法表中,根据索引值获得目标方法内存偏移量(指针) +* 对于静态绑定的方法调用而言,实际引用是一个指向方法的指针 +* 对于动态绑定的方法调用而言,实际引用是方法表的索引值,也就是方法的间接地址。Java 虚拟机获取调用者的实际类型,并在该实际类型的虚方法表中,根据索引值获得目标方法内存偏移量(指针) 为了优化对象调用方法的速度,方法区的类型信息会增加一个指针,该指针指向一个记录该类方法的方法表。每个类中都有一个虚方法表,本质上是一个数组,每个数组元素指向一个当前类及其祖先类中非私有的实例方法 方法表满足以下的特质: * 其一,子类方法表中包含父类方法表中的**所有方法**,并且在方法表中的索引值与父类方法表种的索引值相同 -* 其二,**非重写的方法指向父类的方法表项,与父类共享一个方法表项,重写的方法指向本身自己的实现**。所以这就是为什么多态情况下可以访问父类的方法。 +* 其二,**非重写的方法指向父类的方法表项,与父类共享一个方法表项,重写的方法指向本身自己的实现**,这就是为什么多态情况下可以访问父类的方法。 diff --git a/Prog.md b/Prog.md index 5b3a51c..2aee6d0 100644 --- a/Prog.md +++ b/Prog.md @@ -651,7 +651,7 @@ Java 提供了线程优先级的机制,优先级会提示(hint)调度器 #### 未来优化 -内核级线程调度的成本较大,所以引入了更轻量级的协程。用户线程的调度由用户自己实现(一对多的线程模型),被设计为协同式调度,所以叫协程 +内核级线程调度的成本较大,所以引入了更轻量级的协程。用户线程的调度由用户自己实现(多对一的线程模型,多**个用户线程映射到一个内核级线程**),被设计为协同式调度,所以叫协程 * 有栈协程:协程会完整的做调用栈的保护、恢复工作,所以叫有栈协程 * 无栈协程:本质上是一种有限状态机,状态保存在闭包里,比有栈协程更轻量,但是功能有限 @@ -1505,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毫秒后结束等待,或是被唤醒 ``` @@ -2403,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) @@ -2448,7 +2448,7 @@ MESI(Modified Exclusive Shared Or Invalid)是一种广泛使用的**支持 * 总线嗅探:每个处理器通过嗅探在总线上传播的数据来检查自己缓存值是否过期了,当处理器发现自己的缓存对应的内存地址的数据被修改,就**将当前处理器的缓存行设置为无效状态**,当处理器对这个数据进行操作时,会重新从内存中把数据读取到处理器缓存中 -* 总线风暴:由于 volatile 的 MESI 缓存一致性协议,需要不断的从主内存嗅探和 CAS 循环,无效的交互会导致总线带宽达到峰值;因此不要大量使用 volatile 关键字,使用 volatile、syschonized 都需要根据实际场景 +* 总线风暴:当某个 CPU 核心更新了 Cache 中的数据,要把该事件广播通知到其他核心(**写传播**),CPU 需要每时每刻监听总线上的一切活动,但是不管别的核心的 Cache 是否缓存相同的数据,都需要发出一个广播事件,不断的从主内存嗅探和 CAS 循环,无效的交互会导致总线带宽达到峰值;因此不要大量使用 volatile 关键字,使用 volatile、syschonized 都需要根据实际场景 @@ -2539,9 +2539,7 @@ volatile 修饰的变量,可以禁用指令重排 ##### 缓存一致 -使用 volatile 修饰的共享变量,总线会开启 **CPU 总线嗅探机制**来解决 JMM 缓存一致性问题,也就是共享变量在多线程中可见性的问题,实现 MESI 缓存一致性协议 - -底层是通过汇编 lock 前缀指令,共享变量加了 lock 前缀指令就会进行缓存锁定,在线程修改完共享变量后写回主存,其他的 CPU 核心上运行的线程根据总线嗅探机制会修改其共享变量为失效状态,读取时会重新从主内存中读取最新的数据 +使用 volatile 修饰的共享变量,底层通过汇编 lock 前缀指令进行缓存锁定,在线程修改完共享变量后写回主存,其他的 CPU 核心上运行的线程通过 CPU 总线嗅探机制会修改其共享变量为失效状态,读取时会重新从主内存中读取最新的数据 lock 前缀指令就相当于内存屏障,Memory Barrier(Memory Fence) @@ -2921,7 +2919,7 @@ CAS 特点: CAS 缺点: -- 循环时间长,开销大,因为执行的是循环操作,如果比较不成功一直在循环,最差的情况某个线程一直取到的值和预期值都不一样,就会无限循环导致饥饿,**使用 CAS 线程数不要超过 CPU 的核心数** +- 执行的是循环操作,如果比较不成功一直在循环,最差的情况某个线程一直取到的值和预期值都不一样,就会无限循环导致饥饿,**使用 CAS 线程数不要超过 CPU 的核心数**,采用分段 CAS 和自动迁移机制 - 只能保证一个共享变量的原子操作 - 对于一个共享变量执行操作时,可以通过循环 CAS 的方式来保证原子操作 - 对于多个共享变量操作时,循环 CAS 就无法保证操作的原子性,这个时候**只能用锁来保证原子性** @@ -3470,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()`:返回当前版本号 @@ -3595,7 +3593,10 @@ public class TestFinal { final 变量的赋值通过 putfield 指令来完成,在这条指令之后也会加入写屏障,保证在其它线程读到它的值时不会出现为 0 的情况 -其他线程访问 final 修饰的变量**会复制一份放入栈中**,效率更高 +其他线程访问 final 修饰的变量 + +* **复制一份放入栈中**直接访问,效率高 +* 大于 short 最大值会将其复制到类的常量池,访问时从常量池获取 @@ -4424,9 +4425,9 @@ ThreadLocal 内部解决方法:在 ThreadLocalMap 中的 set/getEntry 方法 ##### 基本使用 -父子线程:**创建子线程的线程是父线程**,比如实例中的 main 线程就是父线程 +父子线程:创建子线程的线程是父线程,比如实例中的 main 线程就是父线程 -ThreadLocal 中存储的是线程的局部变量,如果想实现线程间局部变量传递可以使用 InheritableThreadLocal 类 +ThreadLocal 中存储的是线程的局部变量,如果想**实现线程间局部变量传递**可以使用 InheritableThreadLocal 类 ```java public static void main(String[] args) { @@ -4608,7 +4609,7 @@ java.util.concurrent.BlockingQueue 接口有以下阻塞队列的实现:**FIFO ##### 入队出队 -LinkedBlockingQueue源码: +LinkedBlockingQueue 源码: ```java public class LinkedBlockingQueue extends AbstractQueue @@ -5603,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 计算能力的任务比如在内存中对大量数据进行分析 @@ -5650,7 +5651,7 @@ ExecutorService 类 API: | 方法 | 说明 | | ----------------------------------------------------- | ------------------------------------------------------------ | -| void shutdown() | 线程池状态变为 SHUTDOWN,等待任务执行完后关闭线程池,不会接收新任务,但已提交任务会执行完,而且也可以添加线程(不绑定ren'wu) | +| void shutdown() | 线程池状态变为 SHUTDOWN,等待任务执行完后关闭线程池,不会接收新任务,但已提交任务会执行完,而且也可以添加线程(不绑定任务) | | List shutdownNow() | 线程池状态变为 STOP,用 interrupt 中断正在执行的任务,直接关闭线程池,不会接收新任务,会将队列中的任务返回 | | boolean isShutdown() | 不在 RUNNING 状态的线程池,此执行者已被关闭,方法返回 true | | boolean isTerminated() | 线程池状态是否是 TERMINATED,如果所有任务在关闭后完成,返回 true | @@ -5808,7 +5809,7 @@ ThreadPoolExecutor 使用 int 的**高 3 位来表示线程池状态,低 29 成员变量 -* 线程池中存放 Worker 的容器:线程池没有初始化,直接往池中加线程即可 +* **线程池中存放 Worker 的容器**:线程池没有初始化,直接往池中加线程即可 ```java private final HashSet workers = new HashSet(); @@ -6583,7 +6584,7 @@ FutureTask 类的成员属性: private volatile Thread runner; // 当前任务被线程执行期间,保存当前执行任务的线程对象引用 ``` -* 线程阻塞队列的头节点: +* **线程阻塞队列的头节点**: ```java // 会有很多线程去 get 当前任务的结果,这里使用了一种数据结构头插头取(类似栈)的一个队列来保存所有的 get 线程 @@ -7855,7 +7856,7 @@ AQS 核心思想: * 如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并将共享资源设置锁定状态 -* 请求的共享资源被占用,AQS 用 CLH 队列实现线程阻塞等待以及被唤醒时锁分配的机制,将暂时获取不到锁的线程加入到队列中 +* 请求的共享资源被占用,AQS 用队列实现线程阻塞等待以及被唤醒时锁分配的机制,将暂时获取不到锁的线程加入到队列中 CLH 是一种基于单向链表的**高性能、公平的自旋锁**,AQS 是将每条请求共享资源的线程封装成一个 CLH 锁队列的一个结点(Node)来实现锁的分配 @@ -8251,7 +8252,7 @@ public void lock() { } ``` -* 接下来进入 addWaiter 逻辑,构造 Node 队列,前置条件是当前线程获取锁失败,说明有线程占用了锁 +* 接下来进入 addWaiter 逻辑,构造 Node 队列(不是阻塞队列),前置条件是当前线程获取锁失败,说明有线程占用了锁 * 图中黄色三角表示该 Node 的 waitStatus 状态,其中 0 为默认**正常状态** * Node 的创建是懒惰的,其中第一个 Node 称为 **Dummy(哑元)或哨兵**,用来占位,并不关联线程 @@ -8262,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; @@ -8305,7 +8306,7 @@ public void lock() { -* 线程节点加入阻塞队列成功,进入 AbstractQueuedSynchronizer#acquireQueued 逻辑阻塞线程 +* 线程节点加入队列成功,进入 AbstractQueuedSynchronizer#acquireQueued 逻辑阻塞线程 * acquireQueued 会在一个自旋中不断尝试获得锁,失败后进入 park 阻塞 @@ -10758,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{ @@ -13391,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,对应读、写、异常条件的描述符集合 @@ -13645,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 的不确定情况 @@ -13773,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) @@ -13823,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 次切换) @@ -14736,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 操作,比如网络并发场景 数据流的角度: 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 6939b15..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 + + + *** @@ -13890,7 +13861,7 @@ SpringBoot 功能: 1. 使用 @ComponentScan 扫描 com.example.config 包 -2. 使用 @Import 注解,加载类,这些类都会被 Spring 创建并放入 ioc 容器,默认组件的名字就是**全类名** +2. 使用 @Import 注解加载类,这些类都会被 Spring 创建并放入 ioc 容器,默认组件的名字就是**全类名** 3. 对 @Import 注解进行封装 @@ -13982,14 +13953,14 @@ Condition 是 Spring4.0 后引入的条件化配置接口,通过实现 Conditi ConditionContext 类API: -| 方法 | 说明 | -| --------------------------------------------------- | ----------------------------- | -| ConfigurableListableBeanFactory getBeanFactory() | 获取到 IOC 使用的 beanfactory | -| ClassLoader getClassLoader() | 获取类加载器 | -| Environment getEnvironment() | 获取当前环境信息 | -| BeanDefinitionRegistry getRegistry() | 获取到 bean 定义的注册类 | +| 方法 | 说明 | +| ------------------------------------------------- | ----------------------------- | +| ConfigurableListableBeanFactory getBeanFactory() | 获取到 IOC 使用的 beanfactory | +| ClassLoader getClassLoader() | 获取类加载器 | +| Environment getEnvironment() | 获取当前环境信息 | +| BeanDefinitionRegistry getRegistry() | 获取到 bean 定义的注册类 | -* ClassCondition +* ClassCondition: ```java public class ClassCondition implements Condition { @@ -14013,7 +13984,7 @@ ConditionContext 类API: } ``` -* UserConfig +* UserConfig: ```java @Configuration @@ -14202,7 +14173,7 @@ public class Car { -### 源码解析 +### 装配原理 #### 启动流程 @@ -14632,7 +14603,7 @@ ApplicationContextInitializer、SpringApplicationRunListener、CommandLineRunner #### 文件类型 -SpringBoot 是基于约定的,很多配置都有默认值,如果想使用自己的配置替换默认配置,可以使用 application.properties 或者application.yml(application.yaml)进行配置 +SpringBoot 是基于约定的,很多配置都有默认值,如果想使用自己的配置替换默认配置,可以使用 application.properties 或者 application.yml(application.yaml)进行配置 * 默认配置文件名称:application * 在同一级目录下优先级为:properties > yml > yaml @@ -16192,7 +16163,3343 @@ SpringBoot 项目开发完毕后,支持两种方式部署到服务器: ``` + + + + + + + +*** + + + + + +# Cloud + +## 基本介绍 + +SpringCloud 是分布式微服务的一站式解决方案,是多种微服务落地技术的集合体,俗称微服务全家桶 + +![Cloud-组件概览](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Cloud-组件概览.png) + + + +参考文档:https://www.yuque.com/mrlinxi/pxvr4g/wcwd39 + + + + + +*** + + + + + +## 服务注册 + +### Eureka + +#### 基本介绍 + +Spring Cloud 封装了 Netflix 公司开发的 Eureka 模块来实现服务治理。Eureka 采用了 CS(Client-Server) 的设计架构,Eureka Server 是服务注册中心,系统中的其他微服务使用 Eureka 的客户端连接到 Eureka Server 并维持心跳连接 + +![Cloud-Eureka和Dubbo对比](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Cloud-Eureka和Dubbo对比.png) + +* Eureka Server 提供服务注册服务:各个微服务节点通过配置启动后,会在 EurekaServer 中进行注册,EurekaServer 中的服务注册表中将会存储所有可用服务节点的信息,并且具有可视化界面 + +* Eureka Client 通过注册中心进行访问:用于简化 Eureka Server的交互,客户端也具备一个内置的、使用轮询 (round-robin) 负载算法的负载均衡器。在应用启动后将会向 Eureka Server 发送心跳(默认周期为30秒),如果 Eureka Server 在多个心跳周期内没有接收到某个节点的心跳,将会从服务注册表中把这个服务节点移除(默认 90 秒) + + + + + +**** + + + +#### 服务端 + +服务器端主启动类增加 @EnableEurekaServer 注解,指定该模块作为 Eureka 注册中心的服务器 + +构建流程如下: + +* 主启动类 + + ```java + @SpringBootApplication + @EnableEurekaServer // 表示当前是Eureka的服务注册中心 + public class EurekaMain7001 { + public static void main(String[] args) { + SpringApplication.run(EurekaMain7001.class, args); + } + } + ``` + +* 修改 pom 文件 + + ```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 + + + +*** + + + +#### 客户端 + +##### 生产者 + +服务器端主启动类需要增加 @EnableEurekaClient 注解,表示这是一个 Eureka 客户端,要注册进 EurekaServer 中 + +* 主启动类:PaymentMain8001 + + ```java + @SpringBootApplication + @EnableEurekaClient + public class PaymentMain8001 { + public static void main(String[] args) { + SpringApplication.run(PaymentMain8001.class, args); + } + } + ``` + +* 修改 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 信息提示(鼠标停留在服务名称上时) + ``` + +* 游览器访问 http://localhost:7001 + + + +*** + + + +##### 消费者 + +* 主启动类:PaymentMain8001 + + ```java + @SpringBootApplication + @EnableEurekaClient + @EnableDiscoveryClient + public class PaymentMain8001 { + public static void main(String[] args) { + SpringApplication.run(PaymentMain8001.class, args); + } + } + ``` + +* pom 文件同生产者 + +* 写 yml 文件 + + ```yaml + server: + port: 80 + + # 微服务名称 + spring: + application: + name: cloud-order-service + eureka: + client: + register-with-eureka: true + fetch-registry: true + service-url: + defaultZone: http://localhost:7001/eureka + ``` + +* 浏览器访问 http://localhost:7001 + + ![Cloud-Eureka可视化界面](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Cloud-Eureka可视化界面.png) + + + +*** + + + +#### 集群构建 + +##### 服务端 + +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 + + 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"; + + @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); + } + } + ``` + + + + + +*** + + + +##### 生产者 + +构建 PaymentMain8001 的服务集群 + +* 主启动类 + + ```java + @SpringBootApplication + @EnableEurekaClient + @EnableDiscoveryClient + public class PaymentMain8002 { + public static void main(String[] args) { + SpringApplication.run(PaymentMain8002.class, args); + } + } + ``` + +* 写 yml 文件:端口修改,并且 spring.application.name 均为 cloud-payment-service + + ```yaml + server: + port: 8002 + spring: + application: + name: cloud-payment-service + + eureka: + client: + # 表示将自己注册进EurekaServer默认为true + register-with-eureka: true + # 表示可以从Eureka抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡 + fetch-registry: true + service-url: + defaultZone: http://localhost:7001/eureka + ``` + + + +*** + + + +##### 负载均衡 + +消费者端的 Controller + +```java +// public static final String PAYMENT_URL = "http://localhost:8001"; +public static final String PAYMENT_URL = "http://localhost:8002"; +``` + +由于已经建立了生产者集群,所以可以进行负载均衡的操作: + +* Controller:只修改 PAYMENT_URL 会报错,因为 CLOUD-PAYMENT-SERVICE 对应多个微服务,需要规则来判断调用哪个端口 + + ```java + public static final String PAYMENT_URL = "http://CLOUD-PAYMENT-SERVICE"; + ``` + +* 使用 @LoadBlanced 注解赋予 RestTemplate 负载均衡的能力,增加 config.ApplicationContextConfig 文件: + + ```java + @Configuration + public class ApplicationContextConfig { + @Bean + @LoadBalanced + public RestTemplate getRestTemplate() { + return new RestTemplate(); + } + } + ``` + + + +**** + + + +#### 服务发现 + +服务发现:对于注册进 Eureka 里面的微服务,可以通过服务发现来获得该服务的信息 + +* 主启动类增加注解 @EnableDiscoveryClient: + + ```java + @SpringBootApplication + @EnableEurekaClient + @EnableDiscoveryClient + public class PaymentMain8001 { + public static void main(String[] args) { + SpringApplication.run(PaymentMain8001.class, args); + } + } + ``` + +* 修改生产者的 Controller + + ```java + @RestController + @Slf4j + public class PaymentController { + @Autowired + 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; + } + } + ``` + + + +*** + + + +#### 自我保护 + +保护模式用于客户端和 EurekaServer 之间存在网络分区场景下的保护,一旦进入保护模式 EurekaServer 将会尝试保护其服务注册表中的信息,不在删除服务注册表中的数据,属于 CAP 里面的 AP 思想(可用性和分区容错性) + +![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 + ``` + + + + + +**** + + + + + +### Consul + +#### 基本介绍 + +Consul 是开源的分布式服务发现和配置管理系统,采用 Go 语言开发,官网:https://developer.hashicorp.com/consul + +* 提供了微服务系统中心的服务治理,配置中心,控制总线等功能 +* 基于 Raft 协议,支持健康检查,同时支持 HTTP 和 DNS 协议支持跨数据中心的 WAN 集群 +* 提供图形界面 + +下载 Consul 后,运行指令:`consul -version` + +```bash +D:\Program Files\Java>consul -version +Consul v1.15.1 +Revision 7c04b6a0 +Build Date 2023-03-07T20:35:33Z +Protocol 2 spoken by default, understands 2 to 3 (.....) +``` + +启动命令: + +```bash +consul agent -dev +``` + +访问浏览器:http://localhost:8500/ + + + +中文文档:https://www.springcloud.cc/spring-cloud-consul.html + + + +*** + + + +#### 基本使用 + +无需 Server 端代码的编写 + +生产者: + +* 引入 pom 依赖: + + ```xml + + org.springframework.cloud + spring-cloud-starter-consul-discovery + + ``` + +* application.yml: + + ```yaml + ###consul 服务端口号 + server: + port: 8006 + + spring: + application: + name: consul-provider-payment + ####consul注册中心地址 + cloud: + consul: + host: localhost + port: 8500 + discovery: + service-name: ${spring.application.name} + ``` + +* 主启动类: + + ```java + @SpringBootApplication + @EnableDiscoveryClient + public class PaymentMain8006 { + public static void main(String[] args) { + SpringApplication.run(PaymentMain8006.class, args); + } + } + ``` + +消费者: + +* 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 + 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); + } + } + ``` + + + + + +**** + + + + + +## 服务调用 + +### Ribbon + +#### 基本介绍 + +SpringCloud Ribbon 是基于 Netflix Ribbon 实现的一套负载均衡工具,提供客户端的软件负载均衡算法和服务调用,Ribbon 客户端组件提供一系列完善的配置项如连接超时,重试等 + +官网: https://github.com/Netflix/ribbon/wiki/Getting-Started (已进入维护模式,未来替换为 Load Banlancer) + +负载均衡 Load Balance (LB) 就是将用户的请求平摊的分配到多个服务上,从而达到系统的 HA(高可用) + +**常见的负载均衡算法:** + +- 轮询:为请求选择健康池中的第一个后端服务器,然后按顺序往后依次选择 + +- 最小连接:优先选择连接数最少,即压力最小的后端服务器,在会话较长的情况下可以采取这种方式 + +- 散列:根据请求源的 IP 的散列(hash)来选择要转发的服务器,可以一定程度上保证特定用户能连接到相同的服务器,如果应用需要处理状态而要求用户能连接到和之前相同的服务器,可以采取这种方式 + +Ribbon 本地负载均衡客户端与 Nginx 服务端负载均衡区别: + +- Nginx 是服务器负载均衡,客户端所有请求都会交给 Nginx,然后由 Nginx 实现转发请求,即负载均衡是由服务端实现的 +- Ribbon 本地负载均衡,在调用微服务接口时会在注册中心上获取注册信息服务列表,然后缓存到 JVM 本地,从而在本地实现 RPC 远程服务调用技术 + +集中式 LB 和进程内 LB 的对比: + +* 集中式 LB:在服务的消费方和提供方之间使用独立的 LB 设施(如 Nginx),由该设施把访问请求通过某种策略转发至服务的提供方 +* 进程内 LB:将 LB 逻辑集成到消费方,消费方从服务注册中心获知有哪些服务可用,然后从中选择出一个服务器,Ribbon 属于该类 + + + +*** + + + +#### 工作流程 + +Ribbon 是一个软负载均衡的客户端组件 + +![Cloud-Ribbon架构原理](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Cloud-Ribbon架构原理.png) + +- 第一步先选择 EurekaServer,优先选择在同一个区域内负载较少的 Server +- 第二步根据用户指定的策略,再从 Server 取到的服务注册列表中选择一个地址 + + + +*** + + + +#### 核心组件 + +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) + +注意:官方文档明确给出了警告,自定义负载均衡配置类不能放在 @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); + } + } + ``` + + + + + +**** + + + + + +### OpenFeign + +#### 基本介绍 + +Feign 是一个声明式 WebService 客户端,能让编写 Web 客户端更加简单,只要创建一个接口并添加注解 @Feign 即可,可以与 Eureka 和 Ribbon 组合使用支持负载均衡,所以一般**用在消费者端** + +OpenFeign 在 Feign 的基础上支持了 SpringMVC 注解,并且 @FeignClient 注解可以解析 @RequestMapping 注解下的接口,并通过动态代理的方式产生实现类,在实现类中做负载均衡和服务调用 + +优点:利用 RestTemplate 对 HTTP 请求的封装处理,形成了一套模版化的调用方法。但是对服务依赖的调用可能不止一处,往往一个接口会被多处调用,所以一个微服务接口上面标注一个 @Feign 注解,就可以完成包装依赖服务的调用 + + + + + +**** + + + +#### 基本使用 + +@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: + + ```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(); + } + } + + ``` + + + +*** + + + +#### 超时问题 + +Feign 默认是支持 Ribbon,Feign 客户端的负载均衡和超时控制都由 Ribbon 控制 + +设置 Feign 客户端的超时等待时间: + +```yaml +ribbon: + #指的是建立连接后从服务器读取到可用资源所用的时间 + ReadTimeout: 5000 + #指的是建立连接所用的时间,适用于网络状况正常的情况下,两端连接所用的时间 + ConnectTimeout: 5000 +``` + +演示超时现象:OpenFeign 默认等待时间为 1 秒钟,超过后会报错 + +* 服务提供方 Controller: + + ```java + @GetMapping("/payment/feign/timeout") + public String paymentFeignTimeout() { + try { + TimeUnit.SECONDS.sleep(3); + } catch (InterruptedException e) { + e.printStackTrace(); + } + return serverPort; + } + ``` + +* 消费者 PaymentFeignService 和 OrderFeignController 参考上一小节代码 + +* 测试报错: + + ![Cloud-OpenFeign超时错误](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Cloud-OpenFeign超时错误.png)!](C:\Users\Seazean\Desktop\123\Cloud-OpenFeign超时错误.png) + + + +*** + + + +#### 日志级别 + +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: + + ```yaml + logging: + level: + # feign 日志以什么级别监控哪个接口 + com.atguigu.springcloud.service.PaymentFeignService: debug + ``` + +* Debug 后查看后台日志 + + + + + +**** + + + + + +## 服务熔断 + +### Hystrix + +#### 基本介绍 + +Hystrix 是一个用于处理分布式系统的延迟和容错的开源库,在分布式系统里,许多依赖会出现调用失败,比如超时、异常等,Hystrix 能够保证在一个依赖出问题的情况下,不会导致整体服务失败,避免级联故障,以提高分布式系统的弹性 + +断路器本身是一种开关装置,当某个服务单元发生故障之后,通过断路器的故障监控(类似熔断保险丝),向调用方返回一个符合预期的、可处理的备选响应(FallBack),而不是长时间的等待或者抛出调用方无法处理的异常,这样就保证了服务调用方的线程不会被长时间地占用,避免了故障在分布式系统中的蔓延,乃至雪崩 + +* 服务降级 Fallback:系统不可用时需要一个兜底的解决方案或备选响应,向调用方返回一个可处理的响应 +* 服务熔断 Break:达到最大服务访问后,直接拒绝访问 +* 服务限流 Flowlimit:高并发操作时严禁所有请求一次性过来拥挤,一秒钟 N 个,有序排队进行 + + + +官方文档:https://github.com/Netflix/Hystrix/wiki/How-To-Use + + + + + +**** + + + +#### 服务降级 + +##### 案例构建 + +生产者模块: + +* 引入 pom 依赖: + + ```xml + + org.springframework.cloud + spring-cloud-starter-netflix-hystrix + + ``` + +* 主启动类:开启 Feign + + ```java + @SpringBootApplication + @EnableEurekaClient + @EnableCircuitBreaker // 降级使用 + public class PaymentHystrixMain8001 { + public static void main(String[] args) { + SpringApplication.run(PaymentHystrixMain8001.class, args); + } + } + ``` + +* Controller: + + ```java + @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); + } + } + ``` + +* Service: + + ```java + @Service + public class PaymentService { + public String paymentInfo_Ok(Integer id) { + return "线程池: " + Thread.currentThread().getName() + "paymentInfo_OK, id: " + id"; + } + + public String paymentInfo_Timeout(Integer id) { + int timeNumber = 3; + try { + TimeUnit.SECONDS.sleep(timeNumber); + } catch (InterruptedException e) { + e.printStackTrace(); + } + return "线程池: " + Thread.currentThread().getName() + " payment_Timeout, id: " + id; + } + } + ``` + +* jmeter 压测两个接口,发现接口 paymentInfo_Ok 也变的卡顿 + +消费者模块: + +* Service 接口: + + ```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); + } + ``` + +* Controller: + + ```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); + } + } + ``` + +* 测试:使用的是 Feign 作为客户端,默认 1s 没有得到响应就会报超时错误,进行并发压测 + +* 解决: + + * 超时导致服务器变慢(转圈):超时不再等待 + * 出错(宕机或程序运行出错):出错要有兜底 + + + +**** + + + +##### 降级操作 + +生产者端和消费者端都可以进行服务降级,使用 @HystrixCommand 注解指定降级后的方法 + +生产者端:主启动类添加新注解 @EnableCircuitBreaker,业务类(Service)方法进行如下修改, + +```java +// 模拟拥堵的情况 +@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"; +} +``` + +服务降级的方法和业务处理的方法混杂在了一块,耦合度很高,并且每个方法配置一个服务降级方法 + +- 在业务类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 客户端定义的接口添加一个服务降级实现类即可实现解耦 + +* application.yml:配置文件中开启了 Hystrix + + ```yaml + # 用于服务降级 在注解 @FeignClient中添加fallbackFactory属性值 + feign: + hystrix: + enabled: true #在Feign中开启Hystrix + ``` + +* Service:统一为接口里面的方法进行异常处理,服务异常找 PaymentFallbackService,来统一进行服务降级的处理 + + ```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); + } + ``` + +* PaymentFallbackService: + + ```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"; + } + } + ``` + + + +*** + + + +#### 服务熔断 + +##### 熔断类型 + +熔断机制是应对雪崩效应的一种微服务链路保护机制,当扇出链路的某个微服务出错不可用或者响应时间太长时,会进行服务的降级,进而熔断该节点微服务的调用,快速返回错误的响应信息 + +Hystrix 会监控微服务间调用的状况,当失败的调用到一定阈值,缺省时 5 秒内 20 次调用失败,就会启动熔断机制;当检测到该节点微服务调用响应正常后(检测方式是尝试性放开请求),自动恢复调用链路 + +- 熔断打开:请求不再进行调用当前服务,再有请求调用时将不会调用主逻辑,而是直接调用降级 fallback。实现了自动的发现错误并将降级逻辑切换为主逻辑,减少响应延迟效果。内部设置时钟一般为 MTTR(Mean time to repair,平均故障处理时间),当打开时长达到所设时钟则进入半熔断状态 +- 熔断关闭:熔断关闭不会对服务进行熔断,服务正常调用 +- 熔断半开:部分请求根据规则调用当前服务,如果请求成功且符合规则则认为当前服务恢复正常,关闭熔断,反之继续熔断 + +![Cloud-Hystrix熔断机制](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Cloud-Hystrix熔断机制.png) + + + +**** + + + +##### 熔断操作 + +涉及到断路器的四个重要参数:**快照时间窗、请求总数阀值、窗口睡眠时间、错误百分比阀值** + +- circuitBreaker.enabled:是否开启断路器 +- metrics.rollingStats.timeInMilliseconds:快照时间窗口,断路器确定是否打开需要统计一些请求和错误数据,而统计的时间范围就是快照时间窗,默认为最近的 10 秒 +- circuitBreaker.requestVolumeThreshold:请求总数阀值,该属性设置在快照时间窗内(默认 10s)使断路器跳闸的最小请求数量(默认是 20),如果 10s 内请求数小于设定值,就算请求全部失败也不会触发断路器 +- circuitBreaker.sleepWindowInMilliseconds:窗口睡眠时间,短路多久以后开始尝试是否恢复进入半开状态,默认 5s +- circuitBreaker.errorThresholdPercentage:错误百分比阀值,失败率达到多少后将断路器打开 + +```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() + + return Thread.currentThread().getName() + "\t" + "调用成功,流水号: " + serialNumber; +} +``` + +* 开启:满足一定的阈值(默认 10 秒内超过 20 个请求次数)、失败率达到阈值(默认 10 秒内超过 50% 的请求失败) +* 关闭:一段时间之后(默认是 5 秒),断路器是半开状态,会让其中一个请求进行转发,如果成功断路器会关闭,反之继续开启 + + + + + +*** + + + +#### 工作流程 + +具体工作流程: + +1. 创建 HystrixCommand(用在依赖的服务返回单个操作结果的时候) 或 HystrixObserableCommand(用在依赖的服务返回多个操作结果的时候) 对象 + +2. 命令执行,其中 HystrixComand 实现了下面前两种执行方式,而 HystrixObservableCommand 实现了后两种执行方式 + + * execute():同步执行,从依赖的服务返回一个单一的结果对象, 或是在发生错误的时候抛出异常 + + * queue():异步执行, 直接返回 一个 Future 对象, 其中包含了服务执行结束时要返回的单一结果对象 + + * observe():返回 Observable 对象,代表了操作的多个结果,它是一个 Hot Obserable(不论事件源是否有订阅者,都会在创建后对事件进行发布,所以对于 Hot Observable 的每个订阅者都有可能是从事件源的中途开始的,并可能只是看到了整个操作的局部过程) + + * toObservable():同样会返回 Observable 对象,也代表了操作的多个结果,但它返回的是一个 Cold Observable(没有订阅者的时候并不会发布事件,而是进行等待,直到有订阅者之后才发布事件,所以对于 Cold Observable 的订阅者,它可以保证从一开始看到整个操作的全部过程) + +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 的形式返回 + +注意:如果、没有为命令实现降级逻辑或者在降级处理逻辑中抛出了异常, Hystrix 依然会返回一个 Observable 对象, 但是它不会发射任何结果数据,而是通过 onError 方法通知命令立即中断请求,并通过 onError() 方法将引起命令失败的异常发送给调用者 + +![Cloud-Hystrix工作流程](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Cloud-Hystrix工作流程.png) + + + +官方文档:https://github.com/Netflix/Hystrix/wiki/How-it-Works + + + + + +**** + + + +#### 服务监控 + +Hystrix 提供了准实时的调用监控(Hystrix Dashboard),Hystrix 会持续的记录所有通过 Hystrix 发起的请求的执行信息,并以统计报表和图形的形式展示给用户,包括每秒执行多少请求多少成功,多少失败等,Netflix 通过 `hystrix-metrics-event-stream` 项目实现了对以上指标的监控,Spring Cloud 提供了 Hystrix Dashboard 的整合,对监控内容转化成可视化页面 + +* 引入 pom 依赖: + + ```xml + + org.springframework.cloud + spring-cloud-starter-netflix-hystrix-dashboard + + ``` + +* application.yml:只需要端口即可 + + ```yaml + server: + port: 9001 + ``` + +* 主启动类: + + ```java + @SpringBootApplication + @EnableHystrixDashboard // 开启Hystrix仪表盘 + public class HystrixDashboardMain9001 { + public static void main(String[] args) { + SpringApplication.run(HystrixDashboardMain9001.class, args); + } + } + ``` + +* 所有微服务(生产者)提供类 8001/8002/8003 都需要监控依赖配置 + + ```xml + + org.springframework.boot + spring-boot-starter-actuator + + ``` + +* 启动测试:http://localhost:9001/hystrix + + 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; + } + } + ``` + +* 指标说明: + + ![Cloud-Hystrix界面图示说明](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Cloud-Hystrix界面图示说明.png) + + + + + +**** + + + + + +## 服务网关 + +### Zuul + +SpringCloud 中所集成的 Zuul 版本,采用的是 Tomcat 容器,基于 Servlet 之上的一个阻塞式处理模型,不支持任何长连接,用 Java 实现,而 JVM 本身会有第一次加载较慢的情况,使得 Zuul 的性能相对较差 + +官网: https://github.com/Netflix/zuul/wiki + + + + + +**** + + + + + +### Gateway + +#### 基本介绍 + +SpringCloud Gateway 是 Spring Cloud 的一个全新项目,基于 Spring 5.0+Spring Boot 2.0 和 Project Reactor 等技术开发的网关,旨在为微服务架构提供一种简单有效的统一的 API 路由管理方式。 + +* 基于 WebFlux 框架实现,而 WebFlux 框架底层则使用了高性能的 Reactor 模式通信框架 Netty(异步非阻塞响应式的框架) +* 基于 Filter 链的方式提供了网关基本的功能,例如:安全、监控/指标、限流等 + +Gateway 的三个核心组件: + +* 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 类型的过滤器中可以做响应内容、响应头的修改、日志的输出、流量监控等 + + + + + +*** + + + +#### 网关使用 + +##### 配置方式 + +Gateway 网关路由有两种配置方式,分别为通过 yml 配置和注入 Bean + +* 引入 pom 依赖:Gateway 不需要 spring-boot-starter-web 依赖,否在会报错,原因是底层使用的是 WebFlux 与 Web 冲突 + + ```xml + + org.springframework.cloud + spring-cloud-starter-gateway + + ``` + +* application.yml: + + ```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 + @SpringBootApplication + @EnableEurekaClient + public class GateWayMain9527 { + public static void main(String[] args) { + SpringApplication.run(GateWayMain9527.class, args); + } + } + ``` + +* 以前访问 provider-payment8001 中的 Controller 方法,通过 localhost:8001/payment/get/id 和 localhost:8001/payment/lb,项目不想暴露 8001 端口号,希望在 8001 外面套一层 9527 端口: + + ```yaml + server: + port: 9527 + + spring: + application: + name: cloud-gateway + ## =====================新增==================== + cloud: + gateway: + routes: + - id: payment_routh # payment_route #路由的ID,没有固定规则但要求【唯一】,建议配合服务名 + uri: http://localhost:8001 #匹配后提供服务的路由地址 + predicates: + - Path=/payment/get/** # 断言,路径相匹配的进行路由 + + - 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:// 开头代表从注册中心中获取服务,后面是需要转发到的服务名称 + + + + + +***** + + + +#### 断言类型 + +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:动态路由小结有配置 + +* 测试:正常访问成功,将时间修改到 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 + - Between=2022-02-02T17:45:06.206+08:00[Asia/Shanghai],2022-03-25T18:59:06.206+08:00[Asia/Shanghai] + ``` + +* Cookie Route Predicate:Cookie 断言,两个参数分别是 Cookie name 和正则表达式,路由规则会通过获取对应的 Cookie name 值和正则表达式去匹配,如果匹配上就会执行路由 + + ```yaml + - 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 并且值还要是整数才能路由 + ``` + + + + + +**** + + + +#### 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; + } +} + +``` + + + + + +*** + + + + + +## 服务配置 + +### config + +#### 基本介绍 + +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 接口的形式暴露 + + + +官网: https://cloud.spring.io/spring-cloud-static/spring-cloud-config/2.2.1.RELEASE/reference/html/ + + + + + + +**** + + + +#### 服务端 + +构建 Config Server 模块 + +* 引入 pom 依赖: + + ```xml + + + org.springframework.cloud + spring-cloud-config-server + + ``` + +* application.yml: + + ```yaml + 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); + } + } + ``` + +配置读取规则: + +```yaml +/{application}/{profile}[/{label}] +/{application}-{profile}.yml +/{label}/{application}-{profile}.yml +/{application}-{profile}.properties +/{label}/{application}-{profile}.properties +``` + +* label:分支 +* name:服务名 +* profile:环境(dev/test/prod) + +比如:http://localhost:3344/master/config-dev.yaml + + + + + +*** + + + +#### 客户端 + +##### 基本配置 + +配置客户端 Config Client,客户端从配置中心(Config Server)获取配置信息 + +* 引入 pom 依赖: + + ```xml + + + org.springframework.cloud + spring-cloud-starter-config + + ``` + +* bootstrap.yml:系统级配置,优先级更高,application.yml 是用户级的资源配置项 + + 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 + @SpringBootApplication + @EnableEurekaClient + public class ConfigClientMain3355 { + public static void main(String[] args) { + SpringApplication.run(ConfigClientMain3355.class, args); + } + } + ``` + +* 业务类:将配置信息以 REST 窗口的形式暴露 + + ```java + @RestController + public class ConfigClientController { + @Value("${config.info}") + private String configInfo; + + @GetMapping("/configInfo") + public String getConfigInfo() { + return configInfo; + } + } + ``` + + + +*** + + + +##### 动态刷新 + +分布式配置的动态刷新问题,修改 GitHub 上的配置文件,Config Server 配置中心立刻响应,但是 Config Client 客户端没有任何响应,需要重启客户端 + +* 引入 pom 依赖: + + ```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 请求,手动刷新成本太大 +* 可否广播,一次通知,处处生效,大范围的实现自动刷新 + +解决方法:Bus 总线 + + + + + +*** + + + + + +## 服务消息 + +### Bus + +#### 基本介绍 + +Spring Cloud Bus 能管理和传播分布式系统间的消息,就像分布式执行器,可用于广播状态更改、事件推送、微服务间的通信通道等 + +消息总线:在微服务架构的系统中,通常会使用轻量级的消息代理来构建一个共用的消息主题,并让系统中所有微服务实例都连接上来。由于该主题中产生的消息会被所有实例监听和消费,所以称为消息总线 + +基本原理:ConfigClient 实例都监听 MQ 中同一个 Topic(默认 springCloudBus),当一个服务刷新数据时,会把信息放入 Topic 中,这样其它监听同一 Topic 的服务就能得到通知,然后去更新自身的配置 + + + +**** + + + +#### 全局广播 + +利用消息总线接触一个服务端 ConfigServer 的 `/bus/refresh` 断点,从而刷新所有客户端的配置 + +Cloud-Bus全局广播架构 + +改造 ConfigClient: + +* 引入 MQ 的依赖: + + ```xml + + + org.springframework.cloud + spring-cloud-starter-bus-amqp + + ``` + +* 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' + ``` + +* 只需要调用一次 `curl -X POST "http://localhost:3344/actuator/bus-refresh`,可以实现全局广播 + + + +*** + + + +#### 定点通知 + +动态刷新情况下,只通知指定的微服务,比如只通知 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) + + + + + +**** + + + + + +### Stream + +#### 基本介绍 + +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:生产者和消费者 + + + +中文手册:https://m.wang1314.com/doc/webapp/topic/20971999.html + + + +**** + + + +#### 基本使用 + +Binder 是应用与消息中间件之间的封装,目前实现了 Kafka 和 RabbitMQ 的 Binder,可以动态的改变消息类型(Kafka 的 Topic 和 RabbitMQ 的 Exchange),可以通过配置文件实现,常用注解如下: + +* @Input:标识输入通道,接收的消息通过该通道进入应用程序 +* @Output:标识输出通道,发布的消息通过该通道离开应用程序 +* @StreamListener:监听队列,用于消费者队列的消息接收 +* @EnableBinding:信道 Channel 和 Exchange 绑定 + +生产者发消息模块: + +* 引入 pom 依赖:RabbitMQ + + ```xml + + org.springframework.cloud + spring-cloud-starter-stream-rabbit + + ``` + +* application.yml: + + ```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 + @SpringBootApplication + @EnableEurekaClient + public class StreamMQMain8801 { + public static void main(String[] args) { + SpringApplication.run(StreamMQMain8801.class, args); + } + } + ``` + +* 业务类:MessageChannel 的实例名必须是 output,否则无法启动 + + ```java + // 可以理解为定义消息的发送管道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; + } + } + ``` + +* Controller: + + ```java + @RestController + public class SendMessageController { + @Resource + private IMessageProvider messageProvider; + + @GetMapping(value = "/sendMessage") + public String sendMessage() { + return messageProvider.send(); + } + } + ``` + +消费者模块:8802 和 8803 两个消费者 + +* application.yml:只标注出与生产者不同的地方 + + ```yaml + server: + port: 8802 + + spring: + application: + name: cloud-stream-consumer + cloud: + stream: + # ... + bindings: # 服务的整合处理 + input: # 这个名字是一个通道的名称 + # ... + binder: { defaultRabbit } # 设置要绑定的消息服务的具体设置 + + eureka: + # ... + instance: + # ... + instance-id: receive-8802.com # 在信息列表时显示主机名称 + ``` + +* Controller: + + ```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); + } + } + ``` + + + +**** + + + +#### 高级特性 + +重复消费问题:生产者 8801 发送一条消息后,8802 和 8803 会同时收到 8801 的消息 + +解决方法:微服务应用放置于同一个 group 中,能够保证消息只会被其中一个应用消费一次。不同的组是可以全面消费的(重复消费),同一个组内的多个消费者会发生竞争关系,只有其中一个可以消费 + +```yaml +bindings: + input: + destination: studyExchange + content-type: application/json + binder: { defaultRabbit } + group: seazean # 设置分组 +``` + +消息持久化问题: + +* 停止 8802/8803 并去除掉 8802 的分组 group: seazean,8801 先发送 4 条消息到 MQ +* 先启动 8802,无分组属性配置,后台没有打出来消息,消息丢失 +* 再启动 8803,有分组属性配置,后台打印出来了 MQ 上的消息 + + + + + +***** + + + + + +### Sleuth + +#### 基本介绍 + +Spring Cloud Sleuth 提供了一套完整的分布式请求链路跟踪的解决方案,并且兼容支持了 zipkin + +在微服务框架中,一个客户端发起的请求在后端系统中会经过多次不同的服务节点调用来协同产生最后的请求结果,形成一条复杂的分布式服务调用链路,链路中的任何一环出现高延时或错误都会引起整个请求最后的失败,所以需要链路追踪 + + + +Sleuth 官网:https://github.com/spring-cloud/spring-cloud-sleuth + +zipkin 下载地址:https://repo1.maven.org/maven2/io/zipkin/java/zipkin-server/ + + + +*** + + + +#### 链路监控 + +Sleuth 负责跟踪整理,zipkin 负责可视化展示 + +```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 依赖: + + ```xml + + + org.springframework.cloud + spring-cloud-starter-zipkin + + ``` + +* application.yml: + + ```yaml + server: + port: 8001 + + spring: + application: + name: cloud-payment-service + zipkin: + base-url: http://localhost:9411 + sleuth: + sampler: + #采样率值介于 0 到 1 之间,1 则表示全部采集 + probability: 1 + ``` + +* 业务类: + + ```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; + } + ``` + + + + + + +**** + + + + + +## Alibaba + +Spring Cloud Alibaba 致力于提供微服务开发的一站式解决方案,此项目包含开发分布式应用微服务的必需组件,方便开发者通过 Spring Cloud 编程模型轻松使用这些组件来开发分布式应用服务 + +- 服务限流降级:默认支持 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 + +官方手册:https://spring-cloud-alibaba-group.github.io/github-pages/greenwich/spring-cloud-alibaba.html + + + + + +### 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 代表着单机模式运行,非集群模式 +``` + +关闭命令: + +```bash +shutdown.cmd +``` + +注册中心对比:C 一致性,A 可用性,P 分区容错性 + +| 注册中心 | CAP 模型 | 控制台管理 | +| :-------: | :------: | :--------: | +| Eureka | AP | 支持 | +| Zookeeper | CP | 不支持 | +| Consul | CP | 支持 | +| Nacos | AP | 支持 | + +切换模式:`curl -X PUT '$NACOS_SERVER:8848/nacos/v1/ns/operator/switches?entry=serverMode&value=CP` + + + +官网:https://nacos.io + + + +**** + + + +#### 注册中心 + +Nacos 作为服务注册中心 + +服务提供者: + +* 引入 pom 依赖: + + ```xml + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + + ``` + +* application.yml: + + ```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 + + ```java + @EnableDiscoveryClient + @SpringBootApplication + public class PaymentMain9001 { + public static void main(String[] args) { + SpringApplication.run(PaymentMain9001.class, args); + } + } + ``` + +* Controller: + + ```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; + } + } + ``` + +* 管理后台服务: + + ![Cloud-Nacos服务列表](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Cloud-Nacos服务列表.png) + +* 新建一个模块端口是 9002,其他与 9001 服务一样,nacos-payment-provider 的实例数就变为 2 + +服务消费者: + +* 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 + ``` + +* 主启动类: + + ```java + @SpringBootApplication + @EnableDiscoveryClient + public class OrderNacosMain83 { + public static void main(String[] args) { + SpringApplication.run(OrderNacosMain83.class, args); + } + } + ``` + +* 业务类: + + ```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; + } + } + ``` + + + +*** + + + +#### 配置中心 + +##### 基础配置 + +把配置文件写进 Nacos,然后再用 Nacos 做 config 这样的功能,直接从 Nacos 上抓取服务的配置信息 + +在 Nacos 中,dataId 的完整格式如下 `${prefix}-${spring.profiles.active}.${file-extension}` + +* `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) + +构建流程: + +* 引入 pom 依赖: + + ```xml + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-config + + ``` + +* 配置两个 yml 文件:配置文件的加载是存在优先级顺序的,bootstrap 优先级高于 application + + bootstrap.yml:全局配置 + + ```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} + ``` + + application.yml:服务独立配置,表示服务要去配置中心找名为 nacos-config-client-dev.yaml 的文件 + + ```yaml + spring: + profiles: + active: dev # 表示开发环境 + ``` + +* 主启动类: + + ```java + @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) + + + +*** + + + +##### 分类配置 + +分布式开发中的多环境多项目管理问题,Namespace 用于区分部署环境,Group 和 DataID 逻辑上区分两个目标对象 + +![Cloud-Nacos配置说明](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Cloud-Nacos配置说明.png) + +Namespace 默认 public,主要用来实现隔离,图示三个开发环境 + +Group 默认是 DEFAULT_GROUP,Group 可以把不同的微服务划分到同一个分组里面去 + + + +*** + + + +##### 加载配置 + +DataID 方案:指定 `spring.profile.active` 和配置文件的 DataID 来使不同环境下读取不同的配置 + +Group 方案:通过 Group 实现环境分区,在 config 下增加一条 Group 的配置即可 + +Namespace 方案: + +```yaml +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格式的配置 + group: DEV_GROUP + namespace: 95d44530-a4a6-4ead-98c6-23d192cee298 +``` + +![Cloud-Nacos命名空间](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Cloud-Nacos命名空间.png) + + + +**** + + + +#### 集群架构 + +集群部署参考官方文档,Nacos 支持的三种部署模式: + +- 单机模式:用于测试和单机使用 +- 集群模式:用于生产环境,确保高可用 +- 多集群模式:用于多数据中心场景 + +集群部署文档:https://nacos.io/zh-cn/docs/v2/guide/admin/cluster-mode-quick-start.html + +默认 Nacos 使用嵌入式数据库 derby 实现数据的存储,重启 Nacos 后配置文件不会消失,但是多个 Nacos 节点数据存储存在一致性问题,每个 Nacos 都有独立的嵌入式数据库,所以 Nacos 采用了集中式存储的方式来支持集群化部署,目前只支持 MySQL 的存储 + +Windows 下 Nacos 切换 MySQL 存储: + +* 在 Nacos 安装目录的 conf 目录下找到一个名为 `nacos-mysql.sql` 的脚本并执行 + +* 在 conf 目录下找到 `application.properties`,增加如下数据 + + ```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 + ``` + +* 重新启动 Nacos,可以看到是个全新的空记录界面 + +Linux 参考:https://www.yuque.com/mrlinxi/pxvr4g/rnahsn#dPvMy + + + + + +**** + + + + + +### Sentinel + +#### 基本介绍 + +Sentinel 是面向分布式、多语言异构化服务架构的流量治理组件 + +Sentinel 分为两个部分: + +- 核心库(Java 客户端)不依赖任何框架/库,能够运行于 Java 8 及以上的版本的运行时环境 +- 控制台(Dashboard)主要负责管理推送规则、监控、管理机器信息等 + +下载到本地,运行命令:`java -jar sentinel-dashboard-1.8.2.jar` (要求 Java8,且 8080 端口不能被占用),访问 http://localhost:8080/,账号密码均为 sentinel + + + +官网:https://sentinelguard.io + +下载地址:https://github.com/alibaba/Sentinel/releases + + + + + +**** + + + +#### 基本使用 + +构建演示工程: + +* 引入 pom 依赖: + + ```xml + + com.alibaba.cloud + spring-cloud-starter-alibaba-sentinel + + ``` + +* application.yml:sentinel.transport.port 端口配置会在应用对应的机器上启动一个 HTTP Server,该 Server 与 Sentinel 控制台做交互。比如 Sentinel 控制台添加了 1 个限流规则,会把规则数据 Push 给 Server 接收,Server 再将规则注册到 Sentinel 中 + + ```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: '*' + ``` + +* 主启动类: + + ```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,控制台才能看到 + + + +*** + + + +#### 流控规则 + +流量控制规则 FlowRule:同一个资源可以同时有多个限流规则 + +* 资源名 resource:限流规则的作用对象,Demo 中为 testA +* 针对资源 limitApp:针对调用者进行限流,默认为 default 代表不区分调用来源 +* 阈值类型 grade:QPS 或线程数模式 +* 单机阈值 count:限流阈值 +* 流控模式 strategy:调用关系限流策略 + * 直接:资源本身达到限流条件直接限流 + * 关联:当关联的资源达到阈值时,限流自身 + * 链路:只记录指定链路上的流量,从入口资源进来的流量 +* 流控效果 controlBehavior: + * 快速失败:直接失败,抛出异常 + * Warm Up:冷启动,根据 codeFactory(冷加载因子,默认 3)的值,从 count/codeFactory 开始缓慢增加,给系统预热时间 + * 排队等待:匀速排队,让请求以匀速的方式通过,阈值类型必须设置为 QPS,否则无效 + +Cloud-Sentinel增加流控规则 + +通过调用 `SystemRuleManager.loadRules()` 方法来用硬编码的方式定义流量控制规则: + +```java +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 + + + +**** + + + +#### 降级熔断 + +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增加熔断规则 + +注意异常降级仅针对业务异常,对 Sentinel 限流降级本身的异常 BlockException 不生效,为了统计异常比例或异常数,需要通过 `Tracer.trace(ex)` 记录业务异常或者通过`@SentinelResource` 注解会自动统计业务异常 + +```java +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 + + + +**** + + + +#### 热点限流 + +热点参数限流会统计传入参数中的热点参数,并根据配置的限流阈值与模式,对包含热点参数的资源调用进行限流,Sentinel 利用 LRU 策略统计最近最常访问的热点参数,结合令牌桶算法来进行参数级别的流控 + +Cloud-Sentinel热点参数限流 + +引入 @SentinelResource 注解:https://sentinelguard.io/zh-cn/docs/annotation-support.html + +- value:Sentinel 资源名,默认为请求路径,这里 value 的值可以任意写,但是约定与 Restful 地址一致 + +- blockHandler:表示触发了 Sentinel 中配置的流控规则,就会调用兜底方法 `del_testHotKey` + +- blockHandlerClass:如果设置了该值,就会去该类中寻找 blockHandler 方法 + +- fallback:用于在抛出异常的时候提供 fallback 处理逻辑 + + 说明:fallback 对应服务降级(服务出错了需要有个兜底方法),blockHandler 对应服务熔断(服务不可用的兜底方法) + +```java +@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 只管控制台配置规则问题,出现运行时异常依然会报错 + + + +详细内容参考文档:https://sentinelguard.io/zh-cn/docs/parameter-flow-control.html + + + +*** + + + +#### 系统规则 + +Sentinel 系统自适应保护从整体维度对**应用入口流量**进行控制,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性 + +系统规则支持以下的阈值类型: + +- Load(仅对 Linux/Unix-like 机器生效):当系统 load1 超过阈值,且系统当前的并发线程数超过系统容量时才会触发系统保护,系统容量由系统的 `maxQps * minRt` 计算得出,设定参考值一般是 `CPU cores * 2.5` +- RT:当单台机器上所有入口流量的平均 RT 达到阈值即触发系统保护,单位是毫秒 +- 线程数:当单台机器上所有入口流量的并发线程数达到阈值即触发系统保护 +- 入口 QPS:当单台机器上所有入口流量的 QPS 达到阈值即触发系统保护 +- CPU usage:当系统 CPU 使用率超过阈值即触发系统保护(取值范围 0.0-1.0) + +Cloud-Sentinel增加系统规则 + + + +详细内容参考文档:https://sentinelguard.io/zh-cn/docs/system-adaptive-protection.html + + + +**** + + + +#### 服务调用 + +消费者需要进行服务调用 + +* 引入 pom 依赖: + + ```xml + + org.springframework.cloud + spring-cloud-starter-openfeign + + ``` + +* application.yml:激活 Sentinel 对 Feign 的支持 + + ```yaml + feign: + sentinel: + enabled: true + ``` + +* 主启动类:加上 @EnableFeignClient 注解开启 OpenFeign + +* 业务类: + + ```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 + ``` + + + +**** + + + +#### 持久化 + +配置持久化: + +* 引入 pom 依赖: + + ```xml + + + com.alibaba.csp + sentinel-datasource-nacos + + ``` + +* 添加 Nacos 数据源配置: + + ```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 + ``` + + + + + +**** + + + + + +### Seata + +#### 分布事物 + +一个分布式事务过程,可以用分布式处理过程的一 ID + 三组件模型来描述: + +* XID (Transaction ID):全局唯一的事务 ID,在这个事务ID下的所有事务会被统一控制 + +* TC (Transaction Coordinator):事务协调者,维护全局和分支事务的状态,驱动全局事务提交或回滚 + +* TM (Transaction Manager):事务管理器,定义全局事务的范围,开始全局事务、提交或回滚全局事务 + +* RM (Resource Manager):资源管理器,管理分支事务处理的资源,与 TC 交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚 + +典型的分布式事务流程: + +1. TM 向 TC 申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的 XID +2. XID 在微服务调用链路的上下文中传播(也就是在多个 TM,RM 中传播) +3. RM 向 TC 注册分支事务,将其纳入 XID 对应全局事务的管辖 +4. TM 向 TC 发起针对 XID 的全局提交或回滚决议 +5. TC 调度 XID 下管辖的全部分支事务完成提交或回滚请求 + +Cloud-分布式事务流程 + + + +*** + + + +#### 基本配置 + +Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案 + +下载 seata-server 文件修改 conf 目录下的配置文件 + +* file.conf:自定义事务组名称、事务日志存储模式为 db、数据库连接信息 + + **事务分组**:seata 的资源逻辑,可以按微服务的需要,在应用程序(客户端)对自行定义事务分组,每组取一个名字 + + Cloud-Seata配置文件 + +* 数据库新建库 seata,建表 db_store.sql 在 https://github.com/seata/seata/tree/2.x/script/server/db 目录里面 + +* registry.conf:指明注册中心为 Nacos,及修改 Nacos 连接信息 + + Cloud-Seata注册中心配置 + +启动 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 + +* Seata 提供的全局事务:**@GlobalTransactional** + +搭建简单 Demo: + +* 创建 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 + // 仓储服务 + public interface StorageService { + // 扣除存储数量 + void deduct(String commodityCode, int count); + } + + // 订单服务 + public interface OrderService { + // 创建订单 + Order create(String userId, String commodityCode, int orderCount); + } + + // 帐户服务 + public interface AccountService { + // 从用户账户中借出 + void debit(String userId, int money); + } + ``` + +* 业务逻辑:增加 @GlobalTransactional 注解 + + ```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 53f4fe5..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 — 进程名称(命令名/命令行) @@ -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 行内容 | @@ -2018,17 +2009,17 @@ vim 中提供有一个 被复制文本的缓冲区 - 复制命令会将选中的文字保存在缓冲区 - 删除命令删除的文字会被保存在缓冲区 - 在需要的位置,使用粘贴命令可以将缓冲对的文字插入到光标所在的位置 -- vim 中的文本缓冲区只有一个,如果后续做过 复制、剪切操作,之前缓冲区中的内容会被替换. +- vim 中的文本缓冲区只有一个,如果后续做过复制、剪切操作,之前缓冲区中的内容会被替换 -| 快捷键 | 功能描述 | -| :-----: | :--------------------------: | -| y | 复制已选中的文本到剪切板 | -| yy | 将光标所在行复制到剪切板 | -| nyy | 复制从光标所在行到向下n行 | -| p | 将剪切板中的内容粘贴到光标后 | -| P(大写) | 将剪切板中的内容粘贴到光标前 | +| 快捷键 | 功能描述 | +| :----: | :--------------------------: | +| y | 复制已选中的文本到剪切板 | +| yy | 将光标所在行复制到剪切板 | +| nyy | 复制从光标所在行到向下n行 | +| p | 将剪切板中的内容粘贴到光标后 | +| P | 将剪切板中的内容粘贴到光标前 | -注意:vim 中的文本缓冲区和系统的剪切板不是同一个,在其他软件中使用 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,7 +2050,7 @@ vim 中提供有一个 被复制文本的缓冲区 | R | 替换当前行光标后的字符 | 替换模式 | - 光标选中要替换的字符 -- `R` 命令可以进入替换模式**,替换完成后,按下 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 编辑文件时,会看到以下屏幕信息: ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/vim异常.png) -* ls -a 一下,会看到隐藏的 .swp 文件 删除了此文件即可 +* ls -a 一下,会看到隐藏的 .swp 文件,删除了此文件即可 @@ -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 5dfad06..1aeab98 100644 --- a/Web.md +++ b/Web.md @@ -2063,8 +2063,6 @@ a{ ``` -![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/CSS案例登陆页面.png) -