From 8aa1a3cc5e387a70c4f87f14a2437e8ae6f2171f Mon Sep 17 00:00:00 2001 From: Seazean Date: Mon, 2 May 2022 00:44:00 +0800 Subject: [PATCH 01/35] Update Java Note --- DB.md | 2319 +++++++++++++++++++++++++++++++++++++++++------------- Frame.md | 2 +- Prog.md | 8 +- 3 files changed, 1771 insertions(+), 558 deletions(-) diff --git a/DB.md b/DB.md index 4a24762..c688e0a 100644 --- a/DB.md +++ b/DB.md @@ -334,7 +334,7 @@ mysqlshow -uroot -p1234 test book --count -## 体系结构 +## 体系架构 ### 整体架构 @@ -760,6 +760,8 @@ KILL CONNECTION id + + **** @@ -1630,6 +1632,8 @@ SELECT * FROM emp WHERE name REGEXP '[uvw]';-- 匹配包含 uvw 的name值 + + *** @@ -6083,8 +6087,6 @@ MySQL Server 是多线程结构,包括后台线程和客户服务线程。多 - - *** @@ -6999,8 +7001,6 @@ InnoDB 刷脏页的控制策略: - - **** @@ -8741,7 +8741,7 @@ long_query_time=10 ## NoSQL -### 基本介绍 +### 概述 NoSQL(Not-Only SQL):泛指非关系型的数据库,作为关系型数据库的补充。 @@ -8985,7 +8985,7 @@ Redis (REmote DIctionary Server) :用 C 语言开发的一个开源的高性 #### 客户端 -* 服务器允许客户端连接最大数量,默认0,表示无限制,当客户端连接到达上限后,Redis 会拒绝新的连接: +* 服务器允许客户端连接最大数量,默认 0,表示无限制,当客户端连接到达上限后,Redis 会拒绝新的连接: ```sh maxclients count @@ -9037,174 +9037,179 @@ dbfilename "dump-6379.rdb" - - *** +#### 基本指令 +帮助信息: -## 体系结构 - -### 线程模型 - -#### 单线程 +* 获取命令帮助文档 -Redis 基于 Reactor 模式开发了网络事件处理器,这个处理器被称为文件事件处理器(file event handler),这个文件事件处理器是单线程的,所以 Redis 叫做单线程的模型 + ```sh + help [command] + #help set + ``` -文件事件处理器以单线程方式运行,但是使用 I/O 多路复用程序来监听多个套接字,既实现了高性能的网络通信模型,又很好地与 Redis 服务器中其他同样以单线程方式运行的模块进行对接,保持了 Redis 单线程设计的简单性 +* 获取组中所有命令信息名称 -工作原理: + ```sh + help [@group-name] + #help @string + ``` -* 文件事件处理器使用 I/O 多路复用(multiplexing)程序来同时监听多个套接字,并根据套接字目前执行的任务来为套接字关联不同的事件处理器 +退出服务 -* 当被监听的套接字准备好执行连接应答 (accept)、读取 (read)、写入 (write)、关闭 (close) 等操作时,与操作相对应的文件事件就会产生,这时文件事件处理器会将处理请求放入**单线程的执行队列**中,等待调用套接字关联好的事件处理器来处理事件 +* 退出客户端: - 操作都会被放入一个执行队列顺序执行,所以不存在并发安全问题 + ```sh + quit + exit + ``` -**Redis 单线程也能高效的原因**: +* 退出客户端服务器快捷键: -* 纯内存操作 -* 核心是基于非阻塞的 IO 多路复用机制,单线程可以高效处理多个请求 -* 底层使用 C 语言实现,C 语言实现的程序距离操作系统更近,执行速度相对会更快 -* 单线程同时也**避免了多线程的上下文频繁切换问题**,预防了多线程可能产生的竞争问题 + ```sh + Ctrl+C + ``` -**** -#### 多线程 -Redis6.0 引入多线程主要是为了提高网络 IO 读写性能,因为这是 Redis 的一个性能瓶颈(Redis 的瓶颈主要受限于内存和网络),多线程只是用来**处理网络数据的读写和协议解析**, 执行命令仍然是单线程顺序执行,因此不需要担心线程安全问题。 -Redis6.0 的多线程默认是禁用的,只使用主线程。如需开启需要修改 redis 配置文件 `redis.conf` : +*** -```sh -io-threads-do-reads yesCopy to clipboardErrorCopied -``` -开启多线程后,还需要设置线程数,否则是不生效的,同样需要修改 redis 配置文件 : -```sh -io-threads 4 #官网建议4核的机器建议设置为2或3个线程,8核的建议设置为6个线程 -``` - +## 数据库 +### 服务器 -参考文章:https://mp.weixin.qq.com/s/dqmiR0ECf4lB6Y2OyK-dyA +Redis 服务器将所有数据库保存在**服务器状态 redisServer 结构**的 db 数组中,数组的每一项都是 redisDb 结构,代表一个数据库,每个数据库之间相互独立,**共用 **Redis 内存,不区分大小。在初始化服务器时,根据 dbnum 属性决定创建数据库的数量,该属性由服务器配置的 database 选项决定,默认 16 +```c +struct redisServer { + // 保存服务器所有的数据库 + redisDB *db; + + // 服务器数据库的数量 + int dbnum; +}; +``` + +**在服务器内部**,客户端状态 redisClient 结构的 db 属性记录了目标数据库,是一个指向 redisDb 结构的指针 +```c +struct redisClient { + // 记录客户端正在使用的数据库,指向 redisServer.db 数组中的某一个 db + redisDB *db; +}; +``` +每个 Redis 客户端都有目标数据库,执行数据库读写命令时目标数据库就会成为这些命令的操作对象,默认情况下 Redis 客户端的目标数据库为 0 号数据库,客户端可以执行 SELECT 命令切换目标数据库,原理是通过修改 redisClient.db 指针指向服务器中不同数据库 +命令操作: -*** +```sh +select index #切换数据库,index从0-15取值 +move key db #数据移动到指定数据库,db是数据库编号 +ping #测试数据库是否连接正常,返回PONG +echo message #控制台输出信息 +``` +Redis 没有可以返回客户端目标数据库的命令,但是 redis-cli 客户端旁边会提示当前所使用的目标数据库 +```sh +redis> SELECT 1 +OK +redis[1]> +``` -## 基本指令 -### 操作指令 -读写数据: +*** -* 设置 key,value 数据: - ```sh - set key value - #set name seazean - ``` -* 根据 key 查询对应的 value,如果**不存在,返回空(nil)**: +### 键空间 - ```sh - get key - #get name - ``` +#### key space -帮助信息: +Redis 是一个键值对(key-value pair)数据库服务器,每个数据库都由一个 redisDb 结构表示,redisDb.dict 字典中保存了数据库的所有键值对,将这个字典称为键空间(key space) -* 获取命令帮助文档 +```c +typedef struct redisDB { + // 数据库键空间,保存所有键值对 + dict *dict +} redisDB; +``` - ```sh - help [command] - #help set - ``` +键空间和用户所见的数据库是直接对应的: -* 获取组中所有命令信息名称 +* 键空间的键就是数据库的键,每个键都是一个字符串对象 +* 键空间的值就是数据库的值,每个值可以是任意一种 Redis 对象 - ```sh - help [@group-name] - #help @string - ``` +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-数据库键空间.png) -退出服务 +当使用 Redis 命令对数据库进行读写时,服务器不仅会对键空间执行指定的读写操作,还会**进行一些维护操作**: -* 退出客户端: +* 在读取一个键后(读操作和写操作都要对键进行读取),服务器会根据键是否存在来更新服务器的键空间命中 hit 次数或键空间不命中 miss 次数,这两个值可以在 `INFO stats` 命令的 keyspace_hits 属性和 keyspace_misses 属性中查看 +* 更新键的 LRU(最后使用)时间,该值可以用于计算键的闲置时间,使用 `OBJECT idletime key` 查看键 key 的闲置时间 +* 如果在读取一个键时发现该键已经过期,服务器会**先删除过期键**,再执行其他操作 +* 如果客户端使用 WATCH 命令监视了某个键,那么服务器在对被监视的键进行修改之后,会将这个键标记为脏(dirty),从而让事务注意到这个键已经被修改过 +* 服务器每次修改一个键之后,都会对 dirty 键计数器的值增1,该计数器会触发服务器的持久化以及复制操作 +* 如果服务器开启了数据库通知功能,那么在对键进行修改之后,服务器将按配置发送相应的数据库通知 - ```sh - quit - exit - ``` -* 退出客户端服务器快捷键: - ```sh - Ctrl+C - ``` +*** - -*** +#### 读写指令 +常见键操作指令: -### key 指令 +* 增加指令 -key 是一个字符串,通过 key 获取 redis 中保存的数据 + ```sh + set key value #添加一个字符串类型的键值对 -* 基本操作 +* 删除指令 ```sh del key #删除指定key unlink key #非阻塞删除key,真正的删除会在后续异步操作 - exists key #获取key是否存在 - type key #获取key的类型 - sort key [ASC/DESC] #对key中数据排序,默认对数字排序,并不更改集合中的数据位置,只是查询 - sort key alpha #对key中字母排序 - rename key newkey #改名 - renamenx key newkey #改名 ``` -* 时效性控制 +* 更新指令 ```sh - expire key seconds #为指定key设置有效期,单位为秒 - pexpire key milliseconds #为指定key设置有效期,单位为毫秒 - expireat key timestamp #为指定key设置有效期,单位为时间戳 - pexpireat key mil-timestamp #为指定key设置有效期,单位为毫秒时间戳 - - ttl key #获取key的有效时间,每次获取会自动变化(减小),类似于倒计时, - #-1代表永久性,-2代表不存在/失效 - pttl key #获取key的有效时间,单位是毫秒,每次获取会自动变化(减小) - persist key #切换key从时效性转换为永久性 + rename key newkey #改名 + renamenx key newkey #改名 ``` -* 查询模式 + 值得更新需要参看具体得 Redis 对象得操作方式,比如字符串对象执行 `SET key value` 就可以完成修改 + +* 查询指令 ```sh + exists key #获取key是否存在 + randomkey #随机返回一个键 keys pattern #查询key ``` - 查询模式规则:*匹配任意数量的任意符号;?配合一个任意符号;[]匹配一个指定符号 + 查询模式规则:* 匹配任意数量的任意符号、? 配合一个任意符号、[] 匹配一个指定符号 ```sh keys * #查询所有key @@ -9215,111 +9220,147 @@ key 是一个字符串,通过 key 获取 redis 中保存的数据 keys u[st]er:1 #查询所有以u开头,以er:1结尾,中间包含一个字母,s或t ``` - +* 其他指令 + ```sh + type key #获取key的类型 + sort key [ASC/DESC] #对key中数据排序,默认对数字排序,并不更改集合中的数据位置,只是查询 + sort key alpha #对key中字母排序 + dbsize #获取当前数据库的数据总量,即key的个数 + flushdb #清除当前数据库的所有数据(慎用) + flushall #清除所有数据(慎用) + ``` -*** + 在执行 FLUSHDB 这样的危险命令之前,最好先执行一个 SELECT 命令,保证当前所操作的数据库是目标数据库 -### DB 指令 -Redis 在使用过程中,随着操作数据量的增加,会出现大量的数据以及对应的 key,数据不区分种类、类别混在一起,容易引起重复或者冲突,所以 Redis 为每个服务提供 16 个数据库,编码 0-15,每个数据库之间相互独立,**共用 **Redis 内存,不区分大小 -* 基本操作 +*** - ```sh - select index #切换数据库,index从0-15取值 - ping #测试数据库是否连接正常,返回PONG - echo message #控制台输出信息 - ``` -* 扩展操作 - ```sh - move key db #数据移动到指定数据库,db是数据库编号 - dbsize #获取当前数据库的数据总量,即key的个数 - flushdb #清除当前数据库的所有数据 - flushall #清除所有数据 - ``` +#### 时效设置 +客户端可以以秒或毫秒的精度为数据库中的某个键设置生存时间(TimeTo Live, TTL),在经过指定时间之后,服务器就会自动删除生存时间为 0 的键;也可以以 UNIX 时间戳的方式设置过期时间(expire time),当键的过期时间到达,服务器会自动删除这个键 +```sh +expire key seconds #为指定key设置生存时间,单位为秒 +pexpire key milliseconds #为指定key设置生存时间,单位为毫秒 +expireat key timestamp #为指定key设置过期时间,单位为时间戳 +pexpireat key mil-timestamp #为指定key设置过期时间,单位为毫秒时间戳 +``` +* 实际上 EXPIRE、EXPIRE、EXPIREAT 三个命令**底层都是转换为 PEXPIREAT 命令**来实现的 +* SETEX 命令可以在设置一个字符串键的同时为键设置过期时间,但是该命令是一个类型限定命令 -**** +redisDb 结构的 expires 字典保存了数据库中所有键的过期时间,字典称为过期字典: +* 键是一个指针,指向键空间中的某个键对象(复用键空间的对象,不会产生内存浪费) +* 值是一个 long long 类型的整数,保存了键的过期时间,是一个毫秒精度的 UNIX 时间戳 +```c +typedef struct redisDB { + // 过期字典,保存所有键的过期时间 + dict *expires +} redisDB; +``` -### 通信指令 +客户端执行 PEXPIREAT 命令,服务器会在数据库的过期字典中关联给定的数据库键和过期时间: + +```python +def PEXPIREAT(key, expire_time_in_ms): + # 如果给定的键不存在于键空间,那么不能设置过期时间 + if key not in redisDb.dict: + return 0 + + # 在过期字典中关联键和过期时间 + redisDB.expires[key] = expire_time_in_ms + + # 过期时间设置成功 + return 1 +``` -Redis 发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息。 -Redis 客户端可以订阅任意数量的频道 -![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-发布订阅.png) +**** -操作命令: -1. 打开一个客户端订阅 channel1:`SUBSCRIBE channel1` -2. 打开另一个客户端,给 channel1发布消息 hello:`publish channel1 hello` -3. 第一个客户端可以看到发送的消息 - +#### 时效状态 -注意:发布的消息没有持久化,所以订阅的客户端只能收到订阅后发布的消息 +TTL 和 PTTL 命令通过计算键的过期时间和当前时间之间的差,返回这个键的剩余生存时间 +* 返回正数代表该数据在内存中还能存活的时间 +* 返回 -1 代表永久性,返回 -2 代表键不存在 +```sh +ttl key #获取key的剩余时间,每次获取会自动变化(减小),类似于倒计时 +pttl key #获取key的剩余时间,单位是毫秒,每次获取会自动变化(减小) +``` -**** +PERSIST 是 PEXPIREAT 命令的反操作,在过期字典中查找给定的键,并解除键和值(过期时间)在过期字典中的关联 +```sh +persist key #切换key从时效性转换为永久性 +``` +Redis 通过过期字典可以检查一个给定键是否过期: -### ACL 指令 +* 检查给定键是否存在于过期字典:如果存在,那么取得键的过期时间 +* 检查当前 UNIX 时间戳是否大于键的过期时间:如果是那么键已经过期,否则键未过期 -Redis ACL 是 Access Control List(访问控制列表)的缩写,该功能允许根据可以执行的命令和可以访问的键来限制某些连接 +补充:AOF、RDB 和复制功能对过期键的处理 -![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-ACL指令.png) +* RDB : + * 生成 RDB 文件,程序会对数据库中的键进行检查,已过期的键不会被保存到新创建的 RDB 文件中 + * 载入 RDB 文件,如果服务器以主服务器模式运行,那么在载入时会对键进行检查,过期键会被忽略;如果服务器以从服务器模式运行,会载入所有键,包括过期键,但是主从服务器进行数据同步时就会删除这些键 +* AOF: + * 写入 AOF 文件,如果数据库中的某个键已经过期,但还没有被删除,那么 AOF 文件不会因为这个过期键而产生任何影响;当该过期键被删除,程序会向 AOF 文件追加一条 DEL 命令,显式的删除该键 + * AOF 重写,会对数据库中的键进行检查,忽略已经过期的键 +* 复制:当服务器运行在复制模式下时,从服务器的过期键删除动作由主服务器控制 + * 主服务器在删除一个过期键之后,会显式地向所有从服务器发送一个 DEL 命令,告知从服务器删除这个过期键 + * 从服务器在执行客户端发送的读命令时,即使碰到过期键也不会将过期键删除,会当作未过期键处理,只有在接到主服务器发来的 DEL 命令之后,才会删除过期键 -* acl cat:查看添加权限指令类别 -* acl whoami:查看当前用户 -* acl setuser username on>password ~cached:* +get:设置有用户名、密码、ACL 权限(只能 get) +**** -*** +### 过期删除 +#### 删除策略 +删除策略就是**针对已过期数据的处理策略**,已过期的数据不一定被立即删除,在不同的场景下使用不同的删除方式会有不同效果,在内存占用与 CPU 占用之间寻找一种平衡,顾此失彼都会造成整体 Redis 性能的下降,甚至引发服务器宕机或内存泄露 +针对过期数据有三种删除策略: -## 数据结构 +- 定时删除 +- 惰性删除 +- 定期删除 -### 字符串 +Redis 采用惰性删除和定期删除策略的结合使用 -#### SDS -Redis 构建了简单动态字符串(SDS)的数据类型,作为 Redis 的默认字符串表示,包含字符串的键值对在底层都是由 SDS 实现 -```c -struct sdshdr { - // 记录buf数组中已使用字节的数量,等于 SDS 所保存字符串的长度 - int len; - - // 记录buf数组中未使用字节的数量 - int free; - - // 【字节】数组,用于保存字符串(不是字符数组) - char buf[]; -}; -``` +*** -SDS 遵循 C 字符串**以空字符结尾**的惯例, 保存空字符的 1 字节不计算在 len 属性,SDS 会自动为空字符分配额外的 1 字节空间和添加空字符到字符串末尾,所以空字符对于 SDS 的使用者来说是完全透明的 -![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-SDS底层结构.png) + +#### 定时删除 + +在设置键的过期时间的同时,创建一个定时器(timer),让定时器在键的过期时间到达时,立即执行对键的删除操作 + +- 优点:节约内存,到时就删除,快速释放掉不必要的内存占用 +- 缺点:对 CPU 不友好,无论 CPU 此时负载多高均占用 CPU,会影响 Redis 服务器响应时间和指令吞吐量 +- 总结:用处理器性能换取存储空间(拿时间换空间) + +创建一个定时器需要用到 Redis 服务器中的时间事件,而时间事件的实现方式是无序链表,查找一个事件的时间复杂度为 O(N),并不能高效地处理大量时间事件,所以采用这种方式并不现实 @@ -9327,63 +9368,70 @@ SDS 遵循 C 字符串**以空字符结尾**的惯例, 保存空字符的 1 -#### 对比 +#### 惰性删除 -SDS 与 C 字符串的主要区别: +数据到达过期时间不做处理,等下次访问到该数据时执行 **expireIfNeeded()** 判断: -常数复杂度获取字符串长度: +* 如果输入键已经过期,那么 expireIfNeeded 函数将输入键从数据库中删除,接着访问就会返回空 +* 如果输入键未过期,那么 expireIfNeeded 函数不做动作 -* C 字符串不记录自身的长度,获取时需要遍历整个字符串,遇到空字符串为止,时间复杂度为 O(N) -* SDS 获取字符串长度的时间复杂度为 O(1),设置和更新 SDS 长度由函数底层自动完成 +所有的 Redis 读写命令在执行前都会调用 expireIfNeeded 函数进行检查,该函数就像一个过滤器,在命令真正执行之前过滤掉过期键 -杜绝缓冲区溢出: +惰性删除的特点: -* C 字符串调用 strcat 函数拼接字符串时,如果字符串内存不够容纳目标字符串,就会造成缓冲区溢出(Buffer Overflow) +* 优点:节约 CPU 性能,删除的目标仅限于当前处理的键,不会在删除其他无关的过期键上花费任何 CPU 时间 +* 缺点:内存压力很大,出现长期占用内存的数据,如果过期键永远不被访问,这种情况相当于内存泄漏 +* 总结:用存储空间换取处理器性能(拿空间换时间) - s1 和 s2 是内存中相邻的字符串,执行 `strcat(s1, " Cluster")`(有空格): - ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-内存溢出问题.png) -* SDS 空间分配策略:当对 SDS 进行修改时,首先检查 SDS 的空间是否满足修改所需的要求, 如果不满足会自动将 SDS 的空间扩展至执行修改所需的大小,然后执行实际的修改操作, 避免了缓冲区溢出的问题 +*** -二进制安全: -* C 字符串中的字符必须符合某种编码(比如 ASCII)方式,除了字符串末尾以外其他位置不能包含空字符,否则会被误认为是字符串的结尾,所以只能保存文本数据 -* SDS 的 API 都是二进制安全的,使用字节数组 buf 保存一系列的二进制数据,使用 len 属性来判断数据的结尾,所以可以保存图片、视频、压缩文件等二进制数据 -兼容 C 字符串的函数:SDS 会在为 buf 数组分配空间时多分配一个字节来保存空字符,所以可以重用一部分 C 字符串函数库的函数 +#### 定期删除 +定期删除策略是每隔一段时间执行一次删除过期键操作,并通过限制删除操作执行的时长和频率来减少删除操作对 CPU 时间的影响 +* 如果删除操作执行得太频繁,或者执行时间太长,就会退化成定时删除策略,将 CPU 时间过多地消耗在删除过期键上 +* 如果删除操作执行得太少,或者执行时间太短,定期删除策略又会和惰性删除策略一样,出现浪费内存的情况 -*** +所以采用定期删除策略的话,服务器必须根据情况合理地设置删除操作的执行时长和执行频率 +定期删除是**周期性轮询 Redis 库中的时效性**数据,从过期字典中随机抽取一部分键检查,利用过期数据占比的方式控制删除频度 +- Redis 启动服务器初始化时,读取配置 server.hz 的值,默认为 10,执行指令 info server 可以查看,每秒钟执行 server.hz 次 `serverCron() → activeExpireCycle()` -#### 内存 +- activeExpireCycle() 对某个数据库中的每个 expires 进行检测,工作模式: -C 字符串每次增长或者缩短都会进行一次内存重分配,拼接操作通过重分配扩展底层数组空间,截断操作通过重分配释放不使用的内存空间,防止出现内存泄露 + * 轮询每个数据库,从数据库中取出一定数量的随机键进行检查,并删除其中的过期键 -SDS 通过未使用空间解除了字符串长度和底层数组长度之间的关联,在 SDS 中 buf 数组的长度不一定就是字符数量加一, 数组里面可以包含未使用的字节,字节的数量由 free 属性记录 + * 全局变量 current_db 用于记录 activeExpireCycle() 的检查进度(哪一个数据库),下一次调用时接着该进度处理 + * 随着函数的不断执行,服务器中的所有数据库都会被检查一遍,这时将 current_db 重置为 0,然后再次开始新一轮的检查 -内存重分配涉及复杂的算法,需要执行系统调用,是一个比较耗时的操作,SDS 的两种优化策略: +定期删除特点: -* 空间预分配:当 SDS 的 API 进行修改并且需要进行空间扩展时,程序不仅会为 SDS 分配修改所必需的空间, 还会为 SDS 分配额外的未使用空间 +- CPU 性能占用设置有峰值,检测频度可自定义设置 +- 内存压力不是很大,长期占用内存的**冷数据会被持续清理** +- 周期性抽查存储空间(随机抽查,重点抽查) - * 对 SDS 修改之后,SDS 的长度(len 属性)小于 1MB,程序分配和 len 属性同样大小的未使用空间,此时 len 和 free 相等 - s 为 Redis,执行 `sdscat(s, " Cluster")` 后,len 变为 13 字节,所以也分配了 13 字节的 free 空间,总长度变为 27 字节(额外的一字节保存空字符,13 + 13 + 1 = 27) - ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-SDS内存预分配.png) +*** - * 对 SDS 修改之后,SDS 的长度大于等于 1MB,程序会分配 1MB 的未使用空间 - 在扩展 SDS 空间前,API 会先检查 free 空间是否足够,如果足够就无需执行内存重分配,所以通过预分配策略,SDS 将连续增长 N 次字符串所需内存的重分配次数从**必定 N 次降低为最多 N 次** -* 惰性空间释放:当 SDS 的 API 需要缩短字符串时,程序并不立即使用内存重分配来回收缩短后多出来的字节,而是使用 free 属性将这些字节的数量记录起来,并等待将来使用 +### 数据淘汰 - SDS 提供了相应的 API 来真正释放 SDS 的未使用空间,所以不用担心空间惰性释放策略造成的内存浪费问题 +#### 逐出算法 +数据淘汰策略:当新数据进入 Redis 时,在执行每一个命令前,会调用 **freeMemoryIfNeeded()** 检测内存是否充足。如果内存不满足新加入数据的最低存储要求,Redis 要临时删除一些数据为当前指令清理存储空间,清理数据的策略称为**逐出算法** + +逐出数据的过程不是 100% 能够清理出足够的可使用的内存空间,如果不成功则反复执行,当对所有数据尝试完毕,如不能达到内存清理的要求,**出现 Redis 内存打满异常**: +```sh +(error) OOM command not allowed when used memory>'maxmemory' +``` @@ -9391,35 +9439,1211 @@ SDS 通过未使用空间解除了字符串长度和底层数组长度之间的 -### 链表 +#### 策略配置 -链表提供了高效的节点重排能力,C 语言并没有内置这种数据结构,所以 Redis 构建了链表数据类型 +Redis 如果不设置最大内存大小或者设置最大内存大小为 0,在 64 位操作系统下不限制内存大小,在 32 位操作系统默认为 3GB 内存,一般推荐设置 Redis 内存为最大物理内存的四分之三 -链表节点: +内存配置方式: -```c -typedef struct listNode { - // 前置节点 - struct listNode *prev; - - // 后置节点 - struct listNode *next; - - // 节点的值 - void *value -} listNode; -``` +* 通过修改文件配置(永久生效):修改配置文件 maxmemory 字段,单位为字节 -多个 listNode 通过 prev 和 next 指针组成**双端链表**: +* 通过命令修改(重启失效): -![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-链表节点底层结构.png) + * `config set maxmemory 104857600`:设置 Redis 最大占用内存为 100MB + * `config get maxmemory`:获取 Redis 最大占用内存 -list 链表结构:提供了表头指针 head 、表尾指针 tail 以及链表长度计数器 len + * `info` :可以查看 Redis 内存使用情况,`used_memory_human` 字段表示实际已经占用的内存,`maxmemory` 表示最大占用内存 -```c -typedef struct list { - // 表头节点 - listNode *head; +影响数据淘汰的相关配置如下,配置 conf 文件: + +* 每次选取待删除数据的个数,采用随机获取数据的方式作为待检测删除数据,防止全库扫描,导致严重的性能消耗,降低读写性能 + + ```sh + maxmemory-samples count + ``` + +* 达到最大内存后的,对被挑选出来的数据进行删除的策略 + + ```sh + maxmemory-policy policy + ``` + + 数据删除的策略 policy:3 类 8 种 + + 第一类:检测易失数据(可能会过期的数据集 server.db[i].expires): + + ```sh + volatile-lru # 对设置了过期时间的 key 选择最近最久未使用使用的数据淘汰 + volatile-lfu # 对设置了过期时间的 key 选择最近使用次数最少的数据淘汰 + volatile-ttl # 对设置了过期时间的 key 选择将要过期的数据淘汰 + volatile-random # 对设置了过期时间的 key 选择任意数据淘汰 + ``` + + 第二类:检测全库数据(所有数据集 server.db[i].dict ): + + ```sh + allkeys-lru # 对所有 key 选择最近最少使用的数据淘汰 + allkeLyRs-lfu # 对所有 key 选择最近使用次数最少的数据淘汰 + allkeys-random # 对所有 key 选择任意数据淘汰,相当于随机 + ``` + + 第三类:放弃数据驱逐 + + ```sh + no-enviction #禁止驱逐数据(redis4.0中默认策略),会引发OOM(Out Of Memory) + ``` + +数据淘汰策略配置依据:使用 INFO 命令输出监控信息,查询缓存 hit 和 miss 的次数,根据需求调优 Redis 配置 + + + + + +*** + + + +### 通知机制 + +数据库通知是可以让客户端通过订阅给定的频道或者模式,来获知数据库中键的变化,以及数据库中命令的执行情况 + +* 关注某个键执行了什么命令的通知称为键空间通知(key-space notification) +* 关注某个命令被什么键执行的通知称为键事件通知(key-event notification) + +图示订阅 0 号数据库 message 键: + + + +服务器配置的 notify-keyspace-events 选项决定了服务器所发送通知的类型 + +* AKE 代表服务器发送所有类型的键空间通知和键事件通知 +* AK 代表服务器发送所有类型的键空间通知 +* AE 代表服务器发送所有类型的键事件通知 +* K$ 代表服务器只发送和字符串键有关的键空间通知 +* EL 代表服务器只发送和列表键有关的键事件通知 +* ..... + +发送数据库通知的功能是由 notifyKeyspaceEvent 函数实现的: + +* 如果给定的通知类型 type 不是服务器允许发送的通知类型,那么函数会直接返回 +* 如果给定的通知是服务器允许发送的通知 + * 检测服务器是否允许发送键空间通知,如果允许程序就会构建并发送事件通知 + * 检测服务器是否允许发送键事件通知,如果允许程序就会构建并发送事件通知 + + + + + +*** + + + + + +## 体系架构 + +### 事件驱动 + +#### 基本介绍 + +Redis 服务器是一个事件驱动程序,服务器需要处理两类事件 + +* 文件事件 (file event):服务器通过套接字与客户端(或其他 Redis 服务器)进行连接,而文件事件就是服务器对套接字操作的抽象。服务器与客户端的通信会产生相应的文件事件,服务器通过监听并处理这些事件完成一系列网络通信操作 +* 时间事件 (time event):Redis 服务器中的一些操作(比如 serverCron 函数)需要在指定时间执行,而时间事件就是服务器对这类定时操作的抽象 + + + + + +*** + + + +#### 文件事件 + +##### 基本组成 + +Redis 基于 Reactor 模式开发了网络事件处理器,这个处理器被称为文件事件处理器 (file event handler) + +* 使用 I/O 多路复用 (multiplexing) 程序来同时监听多个套接字,并根据套接字执行的任务来为套接字关联不同的事件处理器 + +* 当被监听的套接字准备好执行连接应答 (accept)、 读取 (read)、 写入 (write)、 关闭 (close) 等操作时,与操作相对应的文件事件就会产生,这时文件事件分派器会调用套接字关联好的事件处理器来处理事件 + +文件事件处理器**以单线程方式运行**,但通过使用 I/O 多路复用程序来监听多个套接字, 既实现了高性能的网络通信模型,又可以很好地与 Redis 服务器中其他同样以单线程方式运行的模块进行对接,保持了 Redis 内部单线程设计的简单性 + +文件事件处理器的组成结构: + + + +尽管多个文件事件可能会并发出现,但是 I/O 多路复用程序将所有产生事件的套接字处理请求放入一个**单线程的执行队列**中,通过队列有序、同步的向文件事件分派器传送套接字,上一个套接字产生的事件处理完后,才会继续向分派器传统下一个 + + + +Redis 单线程也能高效的原因: + +* 纯内存操作 +* 核心是基于非阻塞的 IO 多路复用机制,单线程可以高效处理多个请求 +* 底层使用 C 语言实现,C 语言实现的程序距离操作系统更近,执行速度相对会更快 +* 单线程同时也**避免了多线程的上下文频繁切换问题**,预防了多线程可能产生的竞争问题 + + + +**** + + + +##### 多路复用 + +Redis 的 I/O 多路复用程序的所有功能都是通过包装常见的 select 、epoll、 evport 和 kqueue 这些函数库来实现的,Redis在 I/O 多路复用程序的实现源码中用 #include 宏定义了相应的规则,编译时自动选择系统中**性能最高的多路复用函数**来作为底层实现 + +I/O 多路复用程序监听多个套接字的 AE_READABLE 事件和 AE_WRITABLE 事件,这两类事件和套接字操作之间的对应关系如下: + +* 当套接字变得**可读**时(客户端对套接字执行 write 操作或者 close 操作),或者有新的**可应答**(acceptable)套接字出现时(客户端对服务器的监听套接字执行 connect 连接操作),套接字产生 AE_READABLE 事件 +* 当套接字变得可写时(客户端对套接字执行 read 操作,对于服务器来说就是可以写了),套接字产生 AE_WRITABLE 事件 + +I/O 多路复用程序允许服务器同时监听套接字的 AE_READABLE 和 AE_WRITABLE 事件, 如果一个套接字同时产生了这两种事件,那么文件事件分派器会优先处理 AE_READABLE 事件, 等 AE_READABLE 事件处理完之后才处理 AE_WRITABLE 事件 + + + +*** + + + +##### 处理器 + +Redis 为文件事件编写了多个处理器,这些事件处理器分别用于实现不同的网络通信需求: + +* 连接应答处理器,用于对连接服务器的各个客户端进行应答,Redis 服务器初始化时将该处理器与 AE_READABLE 事件关联 +* 命令请求处理器,用于接收客户端传来的命令请求,执行套接字的读入操作,与 AE_READABLE 事件关联 +* 命令回复处理器,用于向客户端返回命令的执行结果,执行套接字的写入操作,与 AE_WRITABLE 事件关联 +* 复制处理器,当主服务器和从服务器进行复制操作时,主从服务器都需要关联该处理器 + +Redis 客户端与服务器进行连接并发送命令的整个过程: + +* Redis 服务器正在运作监听套接字的 AE_READABLE 事件,关联连接应答处理器 +* 当 Redis 客户端向服务器发起连接,监听套接字将产生 AE_READABLE 事件,触发连接应答处理器执行,对客户端的连接请求进行应答,创建客户端套接字以及客户端状态,并将客户端套接字的 AE_READABLE 事件与命令请求处理器进行关联 +* 客户端向服务器发送命令请求,客户端套接字产生 AE_READABLE 事件,引发命令请求处理器执行,读取客户端的命令内容传给相关程序去执行 +* 执行命令会产生相应的命令回复,为了将这些命令回复传送回客户端,服务器会将客户端套接字的 AE_WRITABLE 事件与命令回复处理器进行关联 +* 当客户端尝试读取命令回复时,客户端套接字产生 AE_WRITABLE 事件,触发命令回复处理器执行,在命令回复全部写入套接字后,服务器就会解除客户端套接字的 AE_WRITABLE 事件与命令回复处理器之间的关联 + + + + + +*** + + + +#### 时间事件 + +Redis 的时间事件分为以下两类: + +* 定时事件:在指定的时间之后执行一次(Redis 中暂时未使用) +* 周期事件:每隔指定时间就执行一次 + +一个时间事件主要由以下三个属性组成: + +* id:服务器为时间事件创建的全局唯一 ID(标识号),从小到大顺序递增,新事件的 ID 比旧事件的 ID 号要大 +* when:毫秒精度的 UNIX 时间戳,记录了时间事件的到达(arrive)时间 +* timeProc:时间事件处理器,当时间事件到达时,服务器就会调用相应的处理器来处理事件 + +时间事件是定时事件还是周期性事件取决于时间事件处理器的返回值: + +* 定时事件:事件处理器返回 AE_NOMORE,该事件在到达一次后就会被删除 +* 周期事件:事件处理器返回非 AE_NOMORE 的整数值,服务器根据该值对事件的 when 属性更新,让该事件在一段时间后再次交付 + +服务器将所有时间事件都放在一个**无序链表**中,新的时间事件插入到链表的表头: + + + +无序链表指是链表不按 when 属性的大小排序,每当时间事件执行器运行时就必须遍历整个链表,查找所有已到达的时间事件,并调用相应的事件处理器处理 + +无序链表并不影响时间事件处理器的性能,因为正常模式下的 Redis 服务器**只使用 serverCron 一个时间事件**,在 benchmark 模式下服务器也只使用两个时间事件,所以无序链表不会影响服务器的性能,几乎可以按照一个指针处理 + +服务器 → serverCron 详解该时间事件 + + + +*** + + + +#### 事件调度 + +服务器中同时存在文件事件和时间事件两种事件类型,调度伪代码: + +```python +# Redis 服务器的主函数的伪代码 +def main(): + # 初始化服务器 + init_server() + + # 循环处理事件,直到服务器关闭 + while server_is_not_shutdown(): + aeProcessEvents() + + # 服务器关闭 + clean_server() + +# 事件调度伪代码 +def aeProcessEvents(): + # 获取到达时间离当前时间最接近的时间事件 + time_event = aeSearchNearestTime() + + # 计算最接近的时间事件距离到达还有多少亳秒 + remaind_ms = time_event.when - unix_ts_now() + # 如果事件已到达,那么 remaind_ms 的值可能为负数,设置为 0 + if remaind_ms < 0: + remaind_ms = 0 + + # 根据 remaind_ms 的值,创建 timeval 结构 + timeval = create_timeval_with_ms(remaind_ms) + # 【阻塞并等待文件事件】产生,最大阻塞时间由传入的timeval结构决定,remaind_ms的值为0时调用后马上返回,不阻塞 + aeApiPoll(timeval) + + # 处理所有已产生的文件事件 + processFileEvents() + # 处理所有已到达的时间事件 + processTimeEvents() +``` + +事件的调度和执行规则: + +* aeApiPoll 函数的最大阻塞时间由到达时间最接近当前时间的时间事件决定,可以避免服务器对时间事件进行频繁的轮询(忙等待),也可以确保 aeApiPoll 函数不会阻塞过长时间 +* 文件事件是随机出现的,如果等待并处理完一次文件事件后仍未有任何时间事件到达,那么服务器将再次等待并处理文件事件。随着文件事件的不断执行,会逐渐向时间事件所设置的到达时间逼近,并最终来到到达时间,这时就可以开始处理时间事件 +* 对文件事件和时间事件的处理都是**同步、有序、原子地执行**,服务器不会中途中断事件处理,也不会对事件进行抢占,所以两种处理器都要尽可地减少程序的阻塞时间,并在有需要时**主动让出执行权**,从而降低事件饥饿的可能性 + * 命令回复处理器在写入字节数超过了某个预设常量,就会主动用 break 跳出写入循环,将余下的数据留到下次再写 + * 时间事件也会将非常耗时的持久化操作放到子线程或者子进程执行 + +* 时间事件在文件事件之后执行,并且事件之间不会出现抢占,所以时间事件的实际处理时间通常会比设定的到达时间稍晚 + + + + + +**** + + + +#### 多线程 + +Redis6.0 引入多线程主要是为了提高网络 IO 读写性能,因为这是 Redis 的一个性能瓶颈(Redis 的瓶颈主要受限于内存和网络),多线程只是用来**处理网络数据的读写和协议解析**, 执行命令仍然是单线程顺序执行,因此不需要担心线程安全问题。 + +Redis6.0 的多线程默认是禁用的,只使用主线程。如需开启需要修改 redis 配置文件 `redis.conf` : + +```sh +io-threads-do-reads yesCopy to clipboardErrorCopied +``` + +开启多线程后,还需要设置线程数,否则是不生效的,同样需要修改 redis 配置文件 : + +```sh +io-threads 4 #官网建议4核的机器建议设置为2或3个线程,8核的建议设置为6个线程 +``` + + + + + +参考文章:https://mp.weixin.qq.com/s/dqmiR0ECf4lB6Y2OyK-dyA + + + + + +**** + + + +### 客户端 + +#### 基本介绍 + +Redis 服务器是典型的一对多程序,一个服务器可以与多个客户端建立网络连接,服务器对每个连接的客户端建立了相应的 redisClient 结构(客户端状态,**在服务器端的存储结构**),保存了客户端当前的状态信息,以及执行相关功能时需要用到的数据结构 + +Redis 服务器状态结构的 clients 属性是一个链表,这个链表保存了所有与服务器连接的客户端的状态结构: + +```c +struct redisServer { + // 一个链表,保存了所有客户端状态 + list *clients; + + //... +}; +``` + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-服务器clients链表.png) + + + + + +*** + + + +#### 数据结构 + +##### redisClient + +客户端的数据结构: + +```c +typedef struct redisClient { + //... + + // 套接字 + int fd; + // 名字 + robj *name; + // 标志 + int flags; + + // 输入缓冲区 + sds querybuf; + // 输出缓冲区 buf 数组 + char buf[REDIS_REPLY_CHUNK_BYTES]; + // 记录了 buf 数组目前已使用的字节数量 + int bufpos; + // 可变大小的输出缓冲区,链表 + 字符串对象 + list *reply; + + // 命令数组 + rboj **argv; + // 命令数组的长度 + int argc; + // 命令的信息 + struct redisCommand *cmd; + + // 是否通过身份验证 + int authenticated; + + // 创建客户端的时间 + time_t ctime; + // 客户端与服务器最后一次进行交互的时间 + time_t lastinteraction; + // 输出缓冲区第一次到达软性限制 (soft limit) 的时间 + time_t obuf_soft_limit_reached_time; +} +``` + +客户端状态包括两类属性 + +* 一类是比较通用的属性,这些属性很少与特定功能相关,无论客户端执行的是什么工作,都要用到这些属性 +* 另一类是和特定功能相关的属性,比如操作数据库时用到的 db 属性和 dict id属性,执行事务时用到的 mstate 属性,以及执行 WATCH 命令时用到的 watched_keys 属性等,代码中没有列出 + + + + + +*** + + + +##### 套接字 + +客户端状态的 fd 属性记录了客户端正在使用的套接字描述符,根据客户端类型的不同,fd 属性的值可以是 -1 或者大于 -1 的整数: + +* 伪客户端 (fake client) 的 fd 属性的值为 -1,命令请求来源于 AOF 文件或者 Lua 脚本,而不是网络,所以不需要套接字连接 +* 普通客户端的 fd 属性的值为大于 -1 的整数,因为合法的套接字描述符不能是 -1 + +执行 `CLIENT list` 命令可以列出目前所有连接到服务器的普通客户端,不包括伪客户端 + + + +*** + + + +##### 名字 + +在默认情况下,一个连接到服务器的客户端是没有名字的,使用 `CLIENT setname` 命令可以为客户端设置一个名字 + + + +*** + + + +##### 标志 + +客户端的标志属性 flags 记录了客户端的角色以及客户端目前所处的状态,每个标志使用一个常量表示 + +* flags 的值可以是单个标志:`flags = ` +* flags 的值可以是多个标志的二进制:`flags = | | ... ` + +一部分标志记录**客户端的角色**: + +* REDIS_MASTER 表示客户端是一个从服务器,REDIS_SLAVE 表示客户端是一个从服务器,在主从复制时使用 +* REDIS_PRE_PSYNC 表示客户端是一个版本低于 Redis2.8 的从服务器,主服务器不能使用 PSYNC 命令与该从服务器进行同步,这个标志只能在 REDIS_ SLAVE 标志处于打开状态时使用 +* REDIS_LUA_CLIENT 表示客户端是专门用于处理 Lua 脚本里面包含的 Redis 命令的伪客户端 + +一部分标志记录目前**客户端所处的状态**: + +* REDIS_MONITOR 表示客户端正在执行 MONITOR 命令 +* REDIS_UNIX_SOCKET 表示服务器使用 UNIX 套接字来连接客户端 +* REDIS_BLOCKED 表示客户端正在被 BRPOP、BLPOP 等命令阻塞 +* REDIS_UNBLOCKED 表示客户端已经从 REDIS_BLOCKED 所表示的阻塞状态脱离,该标志只能在 REDIS_BLOCKED 标志已经打开的情况下使用 +* REDIS_MULTI 标志表示客户端正在执行事务 +* REDIS_DIRTY_CAS 表示事务使用 WATCH 命令监视的数据库键已经被修改 +* REDIS_DIRTY_EXEC 表示事务在命令入队时出现了错误。以上两个标志都表示事务的安全性已经被破坏,只要两个标记中的任意一个被打开,EXEC 命令必然会执行失败,这两个标志只能在客户端打开了 REDIS_MULTI 标志的情况下使用 +* REDIS_CLOSE_ASAP 表示客户端的输出缓冲区大小超出了服务器允许的范围,服务器会在下 一次执行 serverCron 函数时关闭这个客户端,防止服务器的稳定性受到这个客户端影响,积存在输出缓冲区中的所有内容会被**直接释放**,不会返回给客户端 +* REDIS_CLOSE_AFTER_REPLY 表示有用户对这个客户端执行了 `CLIENT KILL` 命令,或者客户端发送给服务器的命令请求中包含了错误的协议内容,服务器会将客户端积存在输出缓冲区中的所有内容发送给客户端,然后关闭客户端 +* REDIS_ASKING 表示客户端向集群节点(运行在集群模式下的服务器)发送了 `ASKING` 命令 +* REDIS_FORCE_AOF 表示强制服务器将当前执行的命令写入到 AOF 文件里,执行 `PUBSUB` 命令会使客户端打开该标志 +* REDIS_FORCE_REPL 表示强制主服务器将当前执行的命令复制给所有从服务器,执行 `SCRIPT LOAD` 命令会使客户端打开 REDIS_FORCE_AOF 标志和 REDIS_FORCE_REPL 标志 +* REDIS_MASTER_FORCE_REPLY 表示将要进行主从复制,在主从服务器进行命令传播期间,从服务器需要向主服务器发送 `REPLICATION ACK` 命令,在发送这个命令之前从服务器必须打开主服务器对应的客户端的该标志,否则发送操作会被拒绝执行 + +Redis 只会将对数据库进行了修改的命令写入到 AOF 文件,并复制到各个从服务器,但是 PUBSUB 和 SCRIPT LOAD 命令例外: + +* PUBSUB 命令虽然没有修改数据库,但 PUBSUB 命令向频道的所有订阅者发送消息这一行为带有副作用,接收到消息的所有客户端的状态都会因为这个命令而改变,所以服务器需要使用 REDIS_FORCE_AOF 标志强制将这个命令写入 AOF 文件。这样在将来载入 AOF 文件时,服务器就可以再次执行相同的 PUBSUB 命令,并产生相同的副作用 +* SCRIPT LOAD 命令虽然没有修改数据库,但它修改了服务器状态,所以也是一个带有副作用的命令,需要使用 REDIS_FORCE_AOF + + + + + +**** + + + +##### 缓冲区 + +客户端状态的输入缓冲区用于保存客户端发送的命令请求,输入缓冲区的大小会根据输入内容动态地缩小或者扩大,但最大大小不能超过 1GB,否则服务器将关闭这个客户端,比如执行 `SET key value `,那么缓冲区 querybuf 的内容: + +```sh +*3\r\n3ドル\r\nSET\r\n3ドル\r\nkey\r\n5ドル\r\nvalue\r\n # +``` + +输出缓冲区是服务器用于保存执行客户端命令所得的命令回复,每个客户端都有两个输出缓冲区可用: + +* 一个是固定大小的缓冲区,保存长度比较小的回复,比如 OK、简短的字符串值、整数值、错误回复等 +* 一个是可变大小的缓冲区,保存那些长度比较大的回复, 比如一个非常长的字符串值或者一个包含了很多元素的集合等 + +buf 是一个大小为 REDIS_REPLY_CHUNK_BYTES (常量默认 16*1024 = 16KB) 字节的字节数组,bufpos 属性记录了 buf 数组目前已使用的字节数量,当 buf 数组的空间已经用完或者回复数据太大无法放进 buf 数组里,服务器就会开始使用可变大小的缓冲区 + +通过使用 reply 链表连接多个字符串对象,可以为客户端保存一个非常长的命令回复,而不必受到固定大小缓冲区 16KB 大小的限制 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-可变输出缓冲区.png) + + + + + +*** + + + +##### 命令 + +服务器对 querybuf 中的命令请求的内容进行分析,得出的命令参数以及参数的数量分别保存到客户端状态的 argv 和 argc 属性 + +* argv 属性是一个数组,数组中的每项都是字符串对象,其中 argv[0] 是要执行的命令,而之后的其他项则是命令的参数 +* argc 属性负责记录 argv 数组的长度 + + + +服务器将根据项 argv[0] 的值,在命令表中查找命令所对应的命令的 redisCommand,将客户端状态的 cmd 指向该结构 + +命令表是一个字典结构,键是 SDS 结构保存命令的名字;值是命令所对应的 redisCommand 结构,保存了命令的实现函数、命令标志、 命令应该给定的参数个数、命令的总执行次数和总消耗时长等统计信息 + + + + + +**** + + + +##### 验证 + +客户端状态的 authenticated 属性用于记录客户端是否通过了身份验证 + +* authenticated 值为 0,表示客户端未通过身份验证 +* authenticated 值为 1,表示客户端已通过身份验证 + +当客户端 authenticated = 0 时,除了 AUTH 命令之外, 客户端发送的所有其他命令都会被服务器拒绝执行 + +```sh +redis> PING +(error) NOAUTH Authentication required. +redis> AUTH 123321 +OK +redis> PING +PONG +``` + + + +*** + + + +##### 时间 + +ctime 属性记录了创建客户端的时间,这个时间可以用来计算客户端与服务器已经连接了多少秒,`CLIENT list` 命令的 age 域记录了这个秒数 + +lastinteraction 属性记录了客户端与服务器最后一次进行互动 (interaction) 的时间,互动可以是客户端向服务器发送命令请求,也可以是服务器向客户端发送命令回复。该属性可以用来计算客户端的空转 (idle) 时长, 就是距离客户端与服务器最后一次进行互动已经过去了多少秒,`CLIENT list` 命令的 idle 域记录了这个秒数 + +obuf_soft_limit_reached_time 属性记录了**输出缓冲区第一次到达软性限制** (soft limit) 的时间 + + + + + +*** + + + + + +#### 生命周期 + +##### 创建 + +服务器使用不同的方式来创建和关闭不同类型的客户端 + +如果客户端是通过网络连接与服务器进行连接的普通客户端,那么在客户端使用 connect 函数连接到服务器时,服务器就会调用连接事件处理器为客户端创建相应的客户端状态,并将这个新的客户端状态添加到服务器状态结构 clients 链表的末尾 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-服务器clients链表.png) + +服务器会在初始化时创建负责执行 Lua 脚本中包含的 Redis 命令的伪客户端,并将伪客户端关联在服务器状态的 lua_client 属性 + +```c +struct redisServer { + // 保存伪客户端 + redisClient *lua_client; + + //... +}; +``` + +lua_client 伪客户端在服务器运行的整个生命周期会一直存在,只有服务器被关闭时,这个客户端才会被关闭 + +载入 AOF 文件时, 服务器会创建用于执行 AOF 文件包含的 Redis 命令的伪客户端,并在载入完成之后,关闭这个伪客户端 + + + +**** + + + +##### 关闭 + +一个普通客户端可以因为多种原因而被关闭: + +* 客户端进程退出或者被杀死,那么客户端与服务器之间的网络连接将被关闭,从而造成客户端被关闭 +* 客户端向服务器发送了带有不符合协议格式的命令请求,那么这个客户端会**被服务器关闭** +* 客户端是 `CLIENT KILL` 命令的目标 +* 如果用户为服务器设置了 timeout 配置选项,那么当客户端的空转时间超过该值时将被关闭,特殊情况不会被关闭: + * 客户端是主服务器(REDIS_MASTER )或者从服务器(打开了 REDIS_SLAVE 标志) + * 正在被 BLPOP 等命令阻塞(REDIS_BLOCKED) + * 正在执行 SUBSCRIBE、PSUBSCRIBE 等订阅命令 +* 客户端发送的命令请求的大小超过了输入缓冲区的限制大小(默认为 1GB) +* 发送给客户端的命令回复的大小超过了输出缓冲区的限制大小 + +理论上来说,可变缓冲区可以保存任意长的命令回复,但是为了回复过大占用过多的服务器资源,服务器会时刻检查客户端的输出缓冲区的大小,并在缓冲区的大小超出范围时,执行相应的限制操作: + +* 硬性限制 (hard limit):输出缓冲区的大小超过了硬性限制所设置的大小,那么服务器立即关闭客户端 +* 软性限制 (soft limit):输出缓冲区的大小超过了软性限制所设置的大小,小于硬性限制的大小,服务器的操作: + * 用属性 obuf_soft_limit_reached_time 记录下客户端到达软性限制的起始时间,继续监视客户端 + * 如果输出缓冲区的大小一直超出软性限制,并且持续时间超过服务器设定的时长,那么服务器将关闭客户端 + * 如果在指定时间内不再超出软性限制,那么客户端就不会被关闭,并且 o_s_l_r_t 属性清零 + +使用 client-output-buffer-limit 选项可以为普通客户端、从服务器客户端、执行发布与订阅功能的客户端分别设置不同的软性限制和硬性限制,格式: + +```sh +client-output-buffer-limit + +client-output-buffer-limit normal 0 0 0 +client-output-buffer-limit slave 256mb 64mb 60 +client-output-buffer-limit pubsub 32mb 8mb 60 +``` + +* 第一行:将普通客户端的硬性限制和软性限制都设置为 0,表示不限制客户端的输出缓冲区大小 +* 第二行:将从服务器客户端的硬性限制设置为 256MB,软性限制设置为 64MB,软性限制的时长为 60 秒 +* 第三行:将执行发布与订阅功能的客户端的硬性限制设置为 32MB,软性限制设置为 8MB,软性限制的时长为 60 秒 + + + + + +**** + + + +### 服务器 + +#### 执行流程 + +Redis 服务器与多个客户端建立网络连接,处理客户端发送的命令请求,在数据库中保存客户端执行命令所产生的数据,并通过资源管理来维持服务器自身的运转,所以一个命令请求从发送到获得回复的过程中,客户端和服务器需要完成一系列操作 + + + +##### 命令请求 + +Redis 服务器的命令请求来自 Redis 客户端,当用户在客户端中键入一个命令请求时,客户端会将这个命令请求转换成协议格式,通过连接到服务器的套接字,将协议格式的命令请求发送给服务器 + +```sh +SET KEY VALUE -> # 命令 +*3\r\nS3\r\nSET\r\n3ドル\r\nKEY\r\n5ドル\r\nVALUE\r\n # 协议格式 +``` + +当客户端与服务器之间的连接套接字因为客户端的写入而变得可读,服务器调用**命令请求处理器**来执行以下操作: + +* 读取套接字中协议格式的命令请求,并将其保存到客户端状态的输入缓冲区里面 +* 对输入缓冲区中的命令请求进行分析,提取出命令请求中包含的命令参数以及命令参数的个数,然后分别将参数和参数个数保存到客户端状态的 argv 属性和 argc 属性里 +* 调用命令执行器,执行客户端指定的命令 + +最后客户端接收到协议格式的命令回复之后,会将这些回复转换成用户可读的格式打印给用户观看,至此整体流程结束 + + + +**** + + + +##### 命令执行 + +命令执行器开始对命令操作: + +* 查找命令:首先根据客户端状态的 argv[0] 参数,在命令表 (command table) 中查找参数所指定的命令,并将找到的命令保存到客户端状态的 cmd 属性里面,是一个 redisCommand 结构 + + 命令查找算法与字母的大小写无关,所以命令名字的大小写不影响命令表的查找结果 + +* 执行预备操作: + + * 检查客户端状态的 cmd 指针是否指向 NULL,根据 redisCommand 检查请求参数的数量是否正确 + * 检查客户端是否通过身份验证 + * 如果服务器打开了 maxmemory 功能,执行命令之前要先检查服务器的内存占用,在有需要时进行内存回收(**逐出算法**) + * 如果服务器上一次执行 BGSAVE 命令出错,并且服务器打开了 stop-writes-on-bgsave-error 功能,那么如果本次执行的是写命令,服务会拒绝执行,并返回错误 + * 如果客户端当前正在用 SUBSCRIBE 或 PSUBSCRIBE 命令订阅频道,那么服务器会拒绝除了 SUBSCRIBE、SUBSCRIBE、 UNSUBSCRIBE、PUNSUBSCRIBE 之外的其他命令 + * 如果服务器正在进行载入数据,只有 sflags 带有 1 标识(比如 INFO、SHUTDOWN、PUBLISH等)的命令才会被执行 + * 如果服务器执行 Lua 脚本而超时并进入阻塞状态,那么只会执行客户端发来的 SHUTDOWN nosave 和 SCRIPT KILL 命令 + * 如果客户端正在执行事务,那么服务器只会执行客户端发来的 EXEC、DISCARD、MULTI、WATCH 四个命令,其他命令都会被**放进事务队列**中 + * 如果服务器打开了监视器功能,那么会将要执行的命令和参数等信息发送给监视器 + +* 调用命令的实现函数:被调用的函数会执行指定的操作并产生相应的命令回复,回复会被保存在客户端状态的输出缓冲区里面(buf 和 reply 属性),然后实现函数还会**为客户端的套接字关联命令回复处理器**,这个处理器负责将命令回复返回给客户端 + +* 执行后续工作: + + * 如果服务器开启了慢查询日志功能,那么慢查询日志模块会检查是否需要为刚刚执行完的命令请求添加一条新的慢查询日志 + * 根据执行命令所耗费的时长,更新命令的 redisCommand 结构的 milliseconds 属性,并将命令 calls 计数器的值增一 + * 如果服务器开启了 AOF 持久化功能,那么 AOF 持久化模块会将刚刚执行的命令请求写入到 AOF 缓冲区里面 + * 如果有其他从服务器正在复制当前这个服务器,那么服务器会将刚刚执行的命令传播给所有从服务器 + +* 将命令回复发送给客户端:客户端套接字变为可写状态时,服务器就会执行命令回复处理器,将客户端输出缓冲区中的命令回复发送给客户端,发送完毕之后回复处理器会清空客户端状态的输出缓冲区,为处理下一个命令请求做好准备 + + + +**** + + + +##### Command + +每个 redisCommand 结构记录了一个Redis 命令的实现信息,主要属性 + +```c +struct redisCommand { + // 命令的名字,比如"set" + char *name; + + // 函数指针,指向命令的实现函数,比如setCommand + // redisCommandProc 类型的定义为 typedef void redisCommandProc(redisClient *c) + redisCommandProc *proc; + + // 命令参数的个数,用于检查命令请求的格式是否正确。如果这个值为负数-N, 那么表示参数的数量大于等于N。 + // 注意命令的名字本身也是一个参数,比如 SET msg "hello",命令的参数是"SET"、"msg"、"hello" 三个 + int arity; + + // 字符串形式的标识值,这个值记录了命令的属性,, + // 比如这个命令是写命令还是读命令,这个命令是否允许在载入数据时使用,是否允许在Lua脚本中使用等等 + char *sflags; + + // 对sflags标识进行分析得出的二进制标识,由程序自动生成。服务器对命令标识进行检查时使用的都是 flags 属性 + // 而不是sflags属性,因为对二进制标识的检查可以方便地通过& ^ ~ 等操作来完成 + int flags; + + // 服务器总共执行了多少次这个命令 + long long calls; + + // 服务器执行这个命令所耗费的总时长 + long long milliseconds; +}; +``` + + + + + +**** + + + +#### serverCron + +##### 基本介绍 + +Redis 服务器以周期性事件的方式来运行 serverCron 函数,服务器初始化时读取配置 server.hz 的值,默认为 10,代表每秒钟执行 10 次,即每隔 100 毫秒执行一次,执行指令 info server 可以查看 + +serverCron 函数负责定期对自身的资源和状态进行检查和调整,从而确保服务器可以长期、稳定地运行 + +* 更新服务器的各类统计信息,比如时间、内存占用、 数据库占用情况等 +* 清理数据库中的过期键值对 +* 关闭和清理连接失效的客户端 +* 进行 AOF 或 RDB 持久化操作 +* 如果服务器是主服务器,那么对从服务器进行定期同步 +* 如果处于集群模式,对集群进行定期同步和连接测试 + + + +**** + + + +##### 时间缓存 + +Redis 服务器中有很多功能需要获取系统的当前时间,而每次获取系统的当前时间都需要执行一次系统调用,为了减少系统调用的执行次数,服务器状态中的 unixtime 属性和 mstime 属性被用作当前时间的缓存 + +```c +struct redisServer { + // 保存了秒级精度的系统当前UNIX时间戳 + time_t unixtime; + // 保存了毫秒级精度的系统当前UNIX时间戳 + long long mstime; + +}; +``` + +serverCron 函数默认以每 100 毫秒一次的频率更新两个属性,所以属性记录的时间的精确度并不高 + +* 服务器只会在打印日志、更新服务器的 LRU 时钟、决定是否执行持久化任务、计算服务器上线时间(uptime)这类对时间精确度要求不高的功能上 +* 对于为键设置过期时间、添加慢查询日志这种需要高精确度时间的功能来说,服务器还是会再次执行系统调用,从而获得最准确的系统当前时间 + + + +*** + + + +##### LRU 时钟 + +服务器状态中的 lruclock 属性保存了服务器的 LRU 时钟 + +```c +struct redisServer { + // 默认每10秒更新一次的时钟缓存,用于计算键的空转(idle)时长。 + unsigned lruclock:22; +}; +``` + +每个 Redis 对象都会有一个 lru 属性, 这个 lru 属性保存了对象最后一次被命令访问的时间 + +```c +typedef struct redisObiect { + unsigned lru:22; +} robj; +``` + +当服务器要计算一个数据库键的空转时间(即数据库键对应的值对象的空转时间),程序会用服务器的 lruclock 属性记录的时间减去对象的 lru 属性记录的时间 + +serverCron 函数默认以每 100 毫秒一次的频率更新这个属性,所以得出的空转时间也是模糊的 + + + +*** + + + +##### 命令次数 + +serverCron 中的 trackOperationsPerSecond 函数以每 100 毫秒一次的频率执行,函数功能是以**抽样计算**的方式,估算并记录服务器在最近一秒钟处理的命令请求数量,这个值可以通过 INFO status 命令的 instantaneous_ops_per_sec 域查看: + +```sh +redis> INFO stats +# Stats +instantaneous_ops_per_sec:6 +``` + +根据上一次抽样时间 ops_sec_last_sample_time 和当前系统时间,以及上一次已执行的命令数 ops_sec_last_sample_ops 和服务器当前已经执行的命令数,计算出两次函数调用期间,服务器平均每毫秒处理了多少个命令请求,该值乘以 1000 得到每秒内的执行命令的估计值,放入 ops_sec_samples 环形数组里 + +```c +struct redisServer { + // 上一次进行抽样的时间 + long long ops_sec_last_sample_time; + // 上一次抽样时,服务器已执行命令的数量 + long long ops_sec_last_sample_ops; + // REDIS_OPS_SEC_SAMPLES 大小(默认值为16)的环形数组,数组的每一项记录一次的抽样结果 + long long ops_sec_samples[REDIS_OPS_SEC_SAMPLES]; + // ops_sec_samples数组的索引值,每次抽样后将值自增一,值为16时重置为0,让数组成为一个环形数组 + int ops_sec_idx; +}; +``` + + + + + +*** + + + +##### 内存峰值 + +服务器状态里的 stat_peak_memory 属性记录了服务器内存峰值大小,循环函数每次执行时都会查看服务器当前使用的内存数量,并与 stat_peak_memory 保存的数值进行比较,设置为较大的值 + +```c +struct redisServer { + // 已使用内存峰值 + size_t stat_peak_memory; +}; +``` + +INFO memory 命令的 used_memory_peak 和 used_memory_peak_human 两个域分别以两种格式记录了服务器的内存峰值: + +```sh +redis> INFO memory +# Memory +... +used_memory_peak:501824 +used_memory_peak_human:490.06K +``` + + + +*** + + + +##### SIGTERM + +服务器启动时,Redis 会为服务器进程的 SIGTERM 信号关联处理器 sigtermHandler 函数,该信号处理器负责在服务器接到 SIGTERM 信号时,打开服务器状态的 shutdown_asap 标识 + +```c +struct redisServer { + // 关闭服务器的标识:值为1时关闭服务器,值为0时不做操作 + int shutdown_asap; +}; +``` + +每次 serverCron 函数运行时,程序都会对服务器状态的 shutdown_asap 属性进行检查,并根据属性的值决定是否关闭服务器 + +服务器在接到 SIGTERM 信号之后,关闭服务器并打印相关日志的过程: + +```sh +[6794 | signal handler] (1384435690) Received SIGTERM, scheduling shutdown ... +[6794] 14 Nov 21:28:10.108 # User requested shutdown ... +[6794] 14 Nov 21:28:10.108 * Saving the final RDB snapshot before exiting. +[6794) 14 Nov 21:28:10.161 * DB saved on disk +[6794) 14 Nov 21:28:10.161 # Redisis now ready to exit, bye bye ... +``` + + + +*** + + + +##### 管理资源 + +serverCron 函数每次执行都会调用 clientsCron 和 databasesCron 函数,进行管理客户端资源和数据库资源 + +clientsCron 函数对一定数量的客户端进行以下两个检查: + +* 如果客户端与服务器之间的连接巳经超时(很长一段时间客户端和服务器都没有互动),那么程序释放这个客户端 +* 如果客户端在上一次执行命令请求之后,输入缓冲区的大小超过了一定的长度,那么程序会释放客户端当前的输入缓冲区,并重新创建一个默认大小的输入缓冲区,从而防止客户端的输入缓冲区耗费了过多的内存 + +databasesCron 函数会对服务器中的一部分数据库进行检查,删除其中的过期键,并在有需要时 对字典进行收缩操作 + + + +*** + + + +##### 持久状态 + +服务器状态中记录执行 BGSAVE 命令和 BGREWRITEAOF 命令的子进程的 ID, + +```c +struct redisServer { + // 记录执行BGSAVE命令的子进程的ID,如果服务器没有在执行BGSAVE,那么这个属性的值为-1 + pid_t rdb_child_pid; + // 记录执行BGREWRITEAOF命令的子进程的ID,如果服务器没有在执行那么这个属性的值为-1 + pid_t aof_child_pid +}; +``` + +serverCron 函数执行时,会检查两个属性的值,只要其中一个属性的值不为 -1,程序就会执行一次 wait3 函数,检查子进程是否有信号发来服务器进程: + +* 如果有信号到达,那么表示新的 RDB 文件已经生成或者 AOF 重写完毕,服务器需要进行相应命令的后续操作,比如用新的 RDB 文件替换现有的 RDB 文件,用重写后的 AOF 文件替换现有的 AOF 文件 +* 如果没有信号到达,那么表示持久化操作未完成,程序不做动作 + +如果两个属性的值都为 -1,表示服务器没有进行持久化操作 + +* 查看是否有 BGREWRITEAOF 被延迟,然后执行 AOF 后台重写 + +* 查看服务器的自动保存条件是否已经被满足,并且服务器没有在进行持久化,就开始一次新的 BGSAVE 操作 + + 因为条件 1 可能会引发一次 AOF,所以在这个检查中会再次确认服务器是否已经在执行持久化操作 + +* 检查服务器设置的 AOF 重写条件是否满足,条件满足并且服务器没有进行持久化,就进行一次 AOF 重写 + +如果服务器开启了 AOF 持久化功能,并且 AOF 缓冲区里还有待写入的数据, 那么 serverCron 函数会调用相应的程序,将 AOF 缓冲区中的内容写入到 AOF 文件里 + + + +*** + + + +##### 延迟执行 + +在服务器执行 BGSAVE 命令的期间,如果客户端发送 BGREWRITEAOF 命令,那么服务器会将 BGREWRITEAOF 命令的执行时间延迟到 BGSAVE 命令执行完毕之后,用服务器状态的 aof_rewrite_scheduled 属性标识延迟与否 + +```c +struct redisServer { + // 如果值为1,那么表示有 BGREWRITEAOF命令被延迟了 + int aof_rewrite_scheduled; +}; +``` + +serverCron 函数会检查 BGSAVE 或者 BGREWRITEAOF 命令是否正在执行,如果这两个命令都没在执行,并且 aof_rewrite_scheduled 属性的值为 1,那么服务器就会执行之前被推延的 BGREWRITEAOF 命令 + + + +**** + + + +##### cronloops + +服务器状态的 cronloops 属性记录了 serverCron 函数执行的次数 + +```c +struct redisServer { + // serverCron 函数每执行一次,这个属性的值就增 1 + int cronloops; +}; +``` + + + +**** + + + +##### 缓冲限制 + +服务器会关闭那些输入或者输出缓冲区大小超出限制的客户端 + + + + + +**** + + + +#### 初始化 + +##### 初始结构 + +一个 Redis 服务器从启动到能够接受客户端的命令请求,需要经过一系列的初始化和设置过程 + +第一步:创建一个 redisServer 类型的实例变量 server 作为服务器的状态,并为结构中的各个属性设置默认值,由 initServerConfig 函数进行初始化一般属性: + +* 设置服务器的运行 ID、默认运行频率、默认配置文件路径、默认端口号、默认 RDB 持久化条件和 AOF 持久化条件 +* 初始化服务器的 LRU 时钟,创建命令表 + +第二步:载入配置选项,用户可以通过给定配置参数或者指定配置文件,对 server 变量相关属性的默认值进行修改 + +第三步:初始化服务器数据结构(除了命令表之外),因为服务器**必须先载入用户指定的配置选项才能正确地对数据结构进行初始化**,所以载入配置完成后才进性数据结构的初始化,服务器将调用 initServer 函数: + +* server.clients 链表,记录了的客户端的状态结构;server.db 数组,包含了服务器的所有数据库 +* 用于保存频道订阅信息的 server.pubsub_channels 字典, 以及保存模式订阅信息的 server.pubsub_patterns 链表 +* 用于执行 Lua 脚本的 Lua 环境 server.lua +* 保存慢查询日志的 server.slowlog 属性 + +initServer 还进行了非常重要的设置操作: + +* 为服务器设置进程信号处理器 +* 创建共享对象,包含 OK、ERR、整数 1 到 10000 的字符串对象等 +* **打开服务器的监听端口** +* **为 serverCron 函数创建时间事件**, 等待服务器正式运行时执行 serverCron 函数 +* 如果 AOF 持久化功能已经打开,那么打开现有的 AOF 文件,如果 AOF 文件不存在,那么创建并打开一个新的 AOF 文件 ,为 AOF 写入做好准备 +* **初始化服务器的后台 I/O 模块**(BIO), 为将来的 I/O 操作做好准备 + +当 initServer 函数执行完毕之后, 服务器将用 ASCII 字符在日志中打印出 Redis 的图标, 以及 Redis 的版本号信息 + + + +*** + + + +##### 还原状态 + +在完成了对服务器状态的初始化之后,服务器需要载入RDB文件或者AOF 文件, 并根据文件记录的内容来还原服务器的数据库状态: + +* 如果服务器启用了 AOF 持久化功能,那么服务器使用 AOF 文件来还原数据库状态 +* 如果服务器没有启用 AOF 持久化功能,那么服务器使用 RDB 文件来还原数据库状态 + +当服务器完成数据库状态还原工作之后,服务器将在日志中打印出载入文件并还原数据库状态所耗费的时长 + +```sh +[7171] 22 Nov 22:43:49.084 * DB loaded from disk: 0.071 seconds +``` + + + +*** + + + +##### 驱动循环 + +在初始化的最后一步,服务器将打印出以下日志,并开始**执行服务器的事件循环**(loop) + +```c +[7171] 22 Nov 22:43:49.084 * The server is now ready to accept connections on pert 6379 +``` + +服务器现在开始可以接受客户端的连接请求,并处理客户端发来的命令请求了 + + + + + +*** + + + + + +## 数据结构 + +### 字符串 + +#### SDS + +Redis 构建了简单动态字符串(SDS)的数据类型,作为 Redis 的默认字符串表示,包含字符串的键值对在底层都是由 SDS 实现 + +```c +struct sdshdr { + // 记录buf数组中已使用字节的数量,等于 SDS 所保存字符串的长度 + int len; + + // 记录buf数组中未使用字节的数量 + int free; + + // 【字节】数组,用于保存字符串(不是字符数组) + char buf[]; +}; +``` + +SDS 遵循 C 字符串**以空字符结尾**的惯例, 保存空字符的 1 字节不计算在 len 属性,SDS 会自动为空字符分配额外的 1 字节空间和添加空字符到字符串末尾,所以空字符对于 SDS 的使用者来说是完全透明的 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-SDS底层结构.png) + + + +*** + + + +#### 对比 + +常数复杂度获取字符串长度: + +* C 字符串不记录自身的长度,获取时需要遍历整个字符串,遇到空字符串为止,时间复杂度为 O(N) +* SDS 获取字符串长度的时间复杂度为 O(1),设置和更新 SDS 长度由函数底层自动完成 + +杜绝缓冲区溢出: + +* C 字符串调用 strcat 函数拼接字符串时,如果字符串内存不够容纳目标字符串,就会造成缓冲区溢出(Buffer Overflow) + + s1 和 s2 是内存中相邻的字符串,执行 `strcat(s1, " Cluster")`(有空格): + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-内存溢出问题.png) + +* SDS 空间分配策略:当对 SDS 进行修改时,首先检查 SDS 的空间是否满足修改所需的要求, 如果不满足会自动将 SDS 的空间扩展至执行修改所需的大小,然后执行实际的修改操作, 避免了缓冲区溢出的问题 + +二进制安全: + +* C 字符串中的字符必须符合某种编码(比如 ASCII)方式,除了字符串末尾以外其他位置不能包含空字符,否则会被误认为是字符串的结尾,所以只能保存文本数据 +* SDS 的 API 都是二进制安全的,使用字节数组 buf 保存一系列的二进制数据,使用 len 属性来判断数据的结尾,所以可以保存图片、视频、压缩文件等二进制数据 + +兼容 C 字符串的函数:SDS 会在为 buf 数组分配空间时多分配一个字节来保存空字符,所以可以重用一部分 C 字符串函数库的函数 + + + +*** + + + +#### 内存 + +C 字符串**每次**增长或者缩短都会进行一次内存重分配,拼接操作通过重分配扩展底层数组空间,截断操作通过重分配释放不使用的内存空间,防止出现内存泄露 + +SDS 通过未使用空间解除了字符串长度和底层数组长度之间的关联,在 SDS 中 buf 数组的长度不一定就是字符数量加一, 数组里面可以包含未使用的字节,字节的数量由 free 属性记录 + +内存重分配涉及复杂的算法,需要执行系统调用,是一个比较耗时的操作,SDS 的两种优化策略: + +* 空间预分配:当 SDS 的 API 进行修改并且需要进行空间扩展时,程序不仅会为 SDS 分配修改所必需的空间, 还会为 SDS 分配额外的未使用空间 + + * 对 SDS 修改之后,SDS 的长度(len 属性)小于 1MB,程序分配和 len 属性同样大小的未使用空间,此时 len 和 free 相等 + + s 为 Redis,执行 `sdscat(s, " Cluster")` 后,len 变为 13 字节,所以也分配了 13 字节的 free 空间,总长度变为 27 字节(额外的一字节保存空字符,13 + 13 + 1 = 27) + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-SDS内存预分配.png) + + * 对 SDS 修改之后,SDS 的长度大于等于 1MB,程序会分配 1MB 的未使用空间 + + 在扩展 SDS 空间前,API 会先检查 free 空间是否足够,如果足够就无需执行内存重分配,所以通过预分配策略,SDS 将连续增长 N 次字符串所需内存的重分配次数从**必定 N 次降低为最多 N 次** + +* 惰性空间释放:当 SDS 的 API 需要缩短字符串时,程序并不立即使用内存重分配来回收缩短后多出来的字节,而是使用 free 属性将这些字节的数量记录起来,并等待将来使用 + + SDS 提供了相应的 API 来真正释放 SDS 的未使用空间,所以不用担心空间惰性释放策略造成的内存浪费问题 + + + + + +**** + + + +### 链表 + +链表提供了高效的节点重排能力,C 语言并没有内置这种数据结构,所以 Redis 构建了链表数据类型 + +链表节点: + +```c +typedef struct listNode { + // 前置节点 + struct listNode *prev; + + // 后置节点 + struct listNode *next; + + // 节点的值 + void *value +} listNode; +``` + +多个 listNode 通过 prev 和 next 指针组成**双端链表**: + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-链表节点底层结构.png) + +list 链表结构:提供了表头指针 head 、表尾指针 tail 以及链表长度计数器 len + +```c +typedef struct list { + // 表头节点 + listNode *head; // 表尾节点 listNode *tail; @@ -9578,7 +10802,7 @@ load_factor = ht[0].used / ht[0].size 原因:执行该命令的过程中,Redis 需要创建当前服务器进程的子进程,而大多数操作系统都采用写时复制(copy-on­-write)技术来优化子进程的使用效率,通过提高执行扩展操作的负载因子,尽可能地避免在子进程存在期间进行哈希表扩展操作,可以避免不必要的内存写入操作,最大限度地节约内存 -哈希表执行收缩的条件:负载因子小于 0.1(自动执行),缩小为字典中数据个数的 50% 左右 +哈希表执行收缩的条件:负载因子小于 0.1(自动执行,servreCron 中检测),缩小为字典中数据个数的 50% 左右 @@ -9922,7 +11146,7 @@ Redis 中用于操作键的命令分为两种类型: Redis 为了确保只有指定类型的键可以执行某些特定的命令,在执行类型特定的命令之前,先通过值对象 redisObject 结构 type 属性检查操作类型是否正确,然后再决定是否执行指定的命令 -对于多态命令,列表对象有 ziplist 和 linkedlist 两种实现方式,通过 redisObject 结构 encoding 属性确定具体的编码类型,底层调用对应的 API 实现具体的操作(基于编码的多态) +对于多态命令,比如列表对象有 ziplist 和 linkedlist 两种实现方式,通过 redisObject 结构 encoding 属性确定具体的编码类型,底层调用对应的 API 实现具体的操作(基于编码的多态) @@ -9967,7 +11191,7 @@ Redis 在初始化服务器时创建一万个(配置文件可以修改)字 比如创建一个值为 100 的键 A,并使用 OBJECT REFCOUNT 命令查看键 A 的值对象的引用计数,会发现值对象的引用计数为 2,引用这个值对象的两个程序分别是持有这个值对象的服务器程序,以及共享这个值对象的键 A -共享对象在数据结构中嵌套了字符串对象的对象(linkedlist 编码的列表对象、hashtable 编码的哈希对象、zset 编码的有序集合对象)也可以使用 +共享对象在嵌套了字符串对象的对象(linkedlist 编码的列表、hashtable 编码的哈希、zset 编码的有序集合)中也能使用 Redis 不共享包含字符串对象的原因:验证共享对象和目标对象是否相同的复杂度越高,消耗的 CPU 时间也会越多 @@ -10056,7 +11280,7 @@ Redis 所有操作都是**原子性**的,采用**单线程**机制,命令是 * 查询操作 ```sh - get key #获取数据 + get key #获取数据,如果不存在,返回空(nil) mget key1 key2... #获取多个数据 strlen key #获取数据字符个数(字符串长度) ``` @@ -10819,7 +12043,7 @@ GeoHash 是一种地址编码方法,把二维的空间经纬度数据编码成 geohash key member [member ...] #计算经纬度 ``` -redis 应用于地理位置计算 +Redis 应用于地理位置计算 @@ -10831,7 +12055,7 @@ redis 应用于地理位置计算 -## 持久化 +## 持久机制 ### 概述 @@ -10842,9 +12066,9 @@ redis 应用于地理位置计算 计算机中的数据全部都是二进制,保存一组数据有两种方式 -第一种:将当前数据状态进行保存,快照形式,存储数据结果,存储格式简单 +RDB:将当前数据状态进行保存,快照形式,存储数据结果,存储格式简单 -第二种:将数据的操作过程进行保存,日志形式,存储操作过程,存储格式复杂 +AOF:将数据的操作过程进行保存,日志形式,存储操作过程,存储格式复杂 @@ -10854,9 +12078,17 @@ redis 应用于地理位置计算 ### RDB -#### save +#### 文件创建 + +RDB 持久化功能所生成的 RDB文件 是一个经过压缩的紧凑二进制文件,通过该文件可以还原生成 RDB 文件时的数据库状态,有两个 Redis 命令可以生成 RDB 文件,一个是 SAVE,另一个是 BGSAVE + + -save 指令:手动执行一次保存操作 +##### SAVE + +SAVE 指令:手动执行一次保存操作,该指令的执行会阻塞当前 Redis 服务器,客户端发送的所有命令请求都会被拒绝,直到当前 RDB 过程完成为止,有可能会造成长时间阻塞,线上环境不建议使用 + +工作原理:Redis 是个**单线程的工作模式**,会创建一个任务队列,所有的命令都会进到这个队列排队执行。当某个指令在执行的时候,队列后面的指令都要等待,所以这种执行方式会非常耗时 配置 redis.conf: @@ -10864,23 +12096,40 @@ save 指令:手动执行一次保存操作 dir path #设置存储.rdb文件的路径,通常设置成存储空间较大的目录中,目录名称data dbfilename "x.rdb" #设置本地数据库文件名,默认值为dump.rdb,通常设置为dump-端口号.rdb rdbcompression yes|no #设置存储至本地数据库时是否压缩数据,默认yes,设置为no节省CPU运行时间 -rdbchecksum yes|no #设置读写文件过程是否进行RDB格式校验,默认yes,设置为no,节约读写10%时间 - #消耗,但存在数据损坏的风险 +rdbchecksum yes|no #设置读写文件过程是否进行RDB格式校验,默认yes ``` -工作原理:redis 是个**单线程的工作模式**,会创建一个任务队列,所有的命令都会进到这个队列排队执行。当某个指令在执行的时候,队列后面的指令都要等待,所以这种执行方式会非常耗时 -save 指令的执行会阻塞当前 Redis 服务器,直到当前 RDB 过程完成为止,有可能会造成长时间阻塞,线上环境不建议使用 +*** -*** +##### BGSAVE +BGSAVE:bg 是 background,代表后台执行,命令的完成需要两个进程,**进程之间不相互影响**,所以持久化期间 Redis 正常工作 -#### bgsave +工作原理: -指令:bgsave(bg 是 background,后台执行的意思) + + +流程:客户端发出 BGSAVE 指令,Redis 服务器使用 fork 函数创建一个子进程,然后响应后台已经开始执行的信息给客户端。子进程会去执行持久化的操作,持久化过程是先将数据写入到一个临时文件中,持久化操作结束再用这个临时文件**替换**上次持久化的文件 + +```python +# 创建子进程 +pid = fork() +if pid == 0: + # 子进程负责创建 RDB 文件 + rdbSave() + # 完成之后向父进程发送信号 + signal_parent() +elif pid> 0: + # 父进程继续处理命令请求,并通过轮询等待子进程的信号 + handle_request_and_wait_signal() +else: + # 处理出错恃况 + handle_fork_error() +``` 配置 redis.conf @@ -10892,15 +12141,43 @@ rdbcompression yes|no rdbchecksum yes|no ``` -bgsave 指令工作原理: +注意:BGSAVE 命令是针对 SAVE 阻塞问题做的优化,Redis 内部所有涉及到 RDB 操作都采用 BGSAVE 的方式,SAVE 命令放弃使用 + +在 BGSAVE 命令执行期间,服务器处理 SAVE、BGSAVE、BGREWRITEAOF 三个命令的方式会和平时有所不同 + +* SAVE 命令会被服务器拒绝,服务器禁止 SAVE 和 BGSAVE 命令同时执行是为了避免父进程(服务器进程)和子进程同时执行两个 rdbSave 调用,产生竞争条件 +* BGSAVE 命令也会被服务器拒绝,也会产生竞争条件 +* BGREWRITEAOF 和 BGSAVE 两个命令不能同时执行 + * 如果 BGSAVE 命令正在执行,那么 BGREWRITEAOF 命令会被延迟到 BGSAVE 命令执行完毕之后执行 + * 如果 BGREWRITEAOF 命令正在执行,那么 BGSAVE 命令会被服务器拒绝 + + + +*** + + + +##### 特殊指令 + +RDB 特殊启动形式的指令(客户端输入) + +* 服务器运行过程中重启 + + ```sh + debug reload + ``` + +* 关闭服务器时指定保存数据 + + ```sh + shutdown save + ``` -![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-bgsave工作原理.png) + 默认情况下执行 shutdown 命令时,自动执行 bgsave(如果没有开启 AOF 持久化功能) -流程:当执行 bgsave 的时候,客户端发出 bgsave 指令给到 redis 服务器,服务器返回后台已经开始执行的信息给客户端,同时使用 fork 函数**创建一个子进程**,让子进程去执行 save 相关的操作。持久化过程是先将数据写入到一个临时文件中,持久化操作结束再用这个临时文件**替换**上次持久化的文件,在这个过程中主进程是不进行任何 IO 操作的,这确保了极高的性能 +* 全量复制:主从复制部分详解 -bgsave 分成两个过程:第一个是服务端收到指令直接告诉客户端开始执行;另外一个过程是 fork 的子进程在完成后台的保存操作,操作完以后返回消息。**两个进程不相互影响**,所以在持久化期间 Redis 可以正常工作 -注意:bgsave 命令是针对 save 阻塞问题做的优化,Redis 内部所有涉及到 RDB 操作都采用 bgsave 的方式,save 命令可以放弃使用 @@ -10908,9 +12185,34 @@ bgsave 分成两个过程:第一个是服务端收到指令直接告诉客户 -#### 自动 +#### 文件载入 -配置文件自动 RDB,无需显式调用相关指令,save 配置启动后底层执行的是 bgsave 操作 +RDB 文件的载入工作是在服务器启动时自动执行,期间 Redis 会一直处于阻塞状态,直到载入完成 + +Redis 并没有专门用于载入 RDB 文件的命令,只要服务器在启动时检测到 RDB 文件存在,就会自动载入 RDB 文件 + +```sh +[7379] 30 Aug 21:07:01.289 * DB loaded from disk: 0.018 seconds # 服务器在成功载入 RDB 文件之后打印 +``` + +AOF 文件的更新频率通常比 RDB 文件的更新频率高: + +* 如果服务器开启了 AOF 持久化功能,那么会优先使用 AOF 文件来还原数据库状态 +* 只有在 AOF 持久化功能处于关闭状态时,服务器才会使用 RDB 文件来还原数据库状态 + + + + + +**** + + + +#### 自动保存 + +##### 配置文件 + +Redis 支持通过配置服务器的 save 选项,让服务器每隔一段时间自动执行一次 BGSAVE 命令 配置 redis.conf: @@ -10918,72 +12220,91 @@ bgsave 分成两个过程:第一个是服务端收到指令直接告诉客户 save second changes #设置自动持久化条件,满足限定时间范围内key的变化数量就进行持久化(bgsave) ``` -参数: - * second:监控时间范围 * changes:监控 key 的变化量 -说明: save 配置中对于 second 与 changes 设置通常具有互补对应关系,尽量不要设置成包含性关系 - -示例: +默认三个条件: ```sh -save 300 10 #300s内10个key发生变化就进行持久化 +save 900 1 # 900s内1个key发生变化就进行持久化 +save 300 10 +save 60 10000 ``` -判定 key 变化的原理: +判定 key 变化的依据: -* 对数据产生了影响 +* 对数据产生了影响,不包括查询 * 不进行数据比对,比如 name 键存在,重新 set name seazean 也算一次变化 save 配置要根据实际业务情况进行设置,频度过高或过低都会出现性能问题,结果可能是灾难性的 -RDB 三种启动方式对比: -| 方式 | save指令 | bgsave指令 | -| -------------- | -------- | ---------- | -| 读写 | 同步 | 异步 | -| 阻塞客户端指令 | 是 | 否 | -| 额外内存消耗 | 否 | 是 | -| 启动新进程 | 否 | 是 | +*** -*** +##### 自动原理 +服务器状态相关的属性: -#### 总结 +```c +struct redisServer { + // 记录了保存条件的数组 + struct saveparam *saveparams; + + // 修改计数器 + long long dirty; + + // 上一次执行保存的时间 + time_t lastsave; +}; +``` -* RDB 特殊启动形式的指令(客户端输入) +* Redis 服务器启动时,可以通过指定配置文件或者传入启动参数的方式设置 save 选项, 如果没有自定义就设置为三个默认值(上节提及),设置服务器状态 redisServe.saveparams 属性,该数组每一项为一个 saveparam 结构,代表 save 的选项设置 - * 服务器运行过程中重启 + ```c + struct saveparam { + // 秒数 + time_t seconds + // 修改数 + int changes; + }; + ``` + +* dirty 计数器记录距离上一次成功执行 SAVE 或者 BGSAVE 命令之后,服务器中的所有数据库进行了多少次修改(包括写入、删除、更新等操作),当服务器成功执行一个修改指令,该命令修改了多少次数据库, dirty 的值就增加多少 - ```sh - debug reload - ``` +* lastsave 属性是一个 UNIX 时间戳,记录了服务器上一次成功执行 SAVE 或者 BGSAVE 命令的时间 - * 关闭服务器时指定保存数据 +Redis 的服务器周期性操作函数 serverCron 默认每隔 100 毫秒就会执行一次,该函数用于对正在运行的服务器进行维护 - ```sh - shutdown save - ``` +serverCron 函数的其中一项工作是检查 save 选项所设置的保存条件是否满足,会遍历 saveparams 数组中的**所有保存条件**,只要有任意一个条件被满足服务器就会执行 BGSAVE 命令 - 默认情况下执行 shutdown 命令时,自动执行 bgsave(如果没有开启 AOF 持久化功能) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-BGSAVE执行原理.png) - * 全量复制:主从复制部分详解 -* RDB 优点: - - RDB 是一个紧凑压缩的二进制文件,存储效率较高,但存储数据量较大时,存储效率较低 - - RDB 内部存储的是 Redis 在某个时间点的数据快照,非常**适合用于数据备份,全量复制**等场景 - - RDB 恢复数据的速度要比 AOF 快很多,因为是快照,直接恢复 -* RDB 缺点: - - bgsave 指令每次运行要执行 fork 操作创建子进程,会牺牲一些性能 - - RDB 方式无论是执行指令还是利用配置,无法做到实时持久化,具有丢失数据的可能性,最后一次持久化后的数据可能丢失 - - Redis 的众多版本中未进行 RDB 文件格式的版本统一,可能出现各版本之间数据格式无法兼容 -* 应用:服务器中每 X 小时执行 bgsave 备份,并将 RDB 文件拷贝到远程机器中,用于灾难恢复 + +*** + + + +#### 文件结构 + +RDB 的存储结构:图示全大写单词标示常量,用全小写单词标示变量和数据 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-RDB文件结构.png) + +* REDIS:长度为 5 字节,保存着 `REDIS` 五个字符,是 RDB 文件的开头,在载入文件时可以快速检查所载入的文件是否 RDB 文件 +* db_version:长度为 4 字节,是一个用字符串表示的整数,记录 RDB 的版本号 +* database:包含着零个或任意多个数据库,以及各个数据库中的键值对数据 +* EOF:长度为 1 字节的常量,标志着 RDB 文件正文内容的结束,当读入遇到这个值时,代表所有数据库的键值对都已经载入完毕 +* check_sum:长度为 8 字节的无符号整数,保存着一个校验和,该值是通过 REDIS、db_version、databases、EOF 四个部分的内容进行计算得出。服务器在载入 RDB 文件时,会将载入数据所计算出的校验和与 check_sum 所记录的校验和进行对比,来检查 RDB 文件是否有出错或者损坏 + +Redis 本身带有 RDB 文件检查工具 redis-check-dump + + @@ -10993,14 +12314,17 @@ RDB 三种启动方式对比: ### AOF -#### 概述 +#### 基本概述 -AOF(append only file)持久化:以独立日志的方式记录每次写命令(不记录读),**增量保存**,只许追加文件但不可以改写文件,重启时再重新执行 AOF 文件中命令达到恢复数据的目的,**与 RDB 相比可以简单理解为由记录数据改为记录数据的变化** +AOF(append only file)持久化:以独立日志的方式记录每次写命令(不记录读)来记录数据库状态,**增量保存**只许追加文件但不可以改写文件,**与 RDB 相比可以理解为由记录数据改为记录数据的变化** -AOF 主要作用是解决了数据持久化的实时性,目前已经是 Redis 持久化的主流方式 +AOF 主要作用是解决了**数据持久化的实时性**,目前已经是 Redis 持久化的主流方式 AOF 写数据过程: -![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-AOF工作原理.png) + + + + @@ -11008,11 +12332,15 @@ AOF 写数据过程: -#### 策略 +#### 持久实现 + +AOF 持久化功能的实现可以分为命令追加(append)、文件写入、文件同步(sync)三个步骤 + -客户端的请求**写命令**会被 append 追加到 AOF 缓冲区内 -启动 AOF 基本配置: +##### 命令追加 + +启动 AOF 的基本配置: ```sh appendonly yes|no #开启AOF持久化功能,默认no,即不开启状态 @@ -11020,64 +12348,124 @@ appendfilename filename #AOF持久化文件名,默认appendonly.aof,建议 dir #AOF持久化文件保存路径,与RDB持久化文件路径保持一致即可 ``` +当 AOF 持久化功能处于打开状态时,服务器在执行完一个写命令之后,会以协议格式将被执行的写命令**追加**到服务器状态的 aof_buf 缓冲区的末尾 + +```c +struct redisServer { + // AOF 缓冲区 + sds aof_buf; +}; +``` + + + +*** + + + +##### 文件写入 + +服务器在处理文件事件时可能会执行写命令,追加一些内容到 aof_buf 缓冲区里,所以服务器每次结束一个事件循环之前,就会执行 flushAppendOnlyFile 函数,判断是否需要**将 aof_buf 缓冲区中的内容写入和保存到 AOF 文件**里 + +flushAppendOnlyFile 函数的行为由服务器配置的 appendfsync 选项的值来决定 + ```sh appendfsync always|everysec|no #AOF写数据策略:默认为everysec ``` -AOF 持久化数据的三种策略(appendfsync): +- always:每次写入操作都将 aof_buf 缓冲区中的所有内容**写入并同步**到 AOF 文件 + + 特点:安全性最高,数据零误差,但是性能较低,不建议使用 + + +- everysec:先将 aof_buf 缓冲区中的内容写入到 AOF 文件,判断上次同步 AOF 文件的时间距离现在超过一秒钟,再次对 AOF 文件进行同步,这个同步操作是由一个(子)线程专门负责执行的 + + 特点:在系统突然宕机的情况下丢失 1 秒内的数据,准确性较高,性能较高,建议使用,也是默认配置 + + +- no:将 aof_buf 缓冲区中的内容写入到 AOF 文件,但并不对 AOF 文件进行同步,何时同步由操作系统来决定 + + 特点:**整体不可控**,服务器宕机会丢失上次同步 AOF 后的所有写指令 + + + +**** + + + +##### 文件同步 + +在现代操作系统中,当用户调用 write 函数将数据写入文件时,操作系统通常会将写入数据暂时保存在一个内存缓冲区空间,等到缓冲区**写满或者到达特定时间周期**,才真正地将缓冲区中的数据写入到磁盘里面(刷脏) + +* 优点:提高文件的写入效率 +* 缺点:为写入数据带来了安全问题,如果计算机发生停机,那么保存在内存缓冲区里面的写入数据将会丢失 + +系统提供了 fsync 和 fdatasync 两个同步函数做**强制硬盘同步**,可以让操作系统立即将缓冲区中的数据写入到硬盘里面,函数会阻塞到写入硬盘完成后返回,保证了数据持久化 + +异常恢复:AOF 文件损坏,通过 redis-check-aof--fix appendonly.aof 进行恢复,重启 Redis,然后重新加载 + + + + + +*** + -- always(每次):每次写入操作均同步到 AOF 文件中,**数据零误差,性能较低**,不建议使用。 +#### 文件载入 -- everysec(每秒):每秒将缓冲区中的指令同步到 AOF 文件中,在系统突然宕机的情况下丢失 1 秒内的数据 数据**准确性较高,性能较高**,建议使用,也是默认配置 +AOF 文件里包含了重建数据库状态所需的所有写命令,所以服务器只要读入并重新执行一遍 AOF 文件里的命令,就还原服务器关闭之前的数据库状态,服务器在启动时,还原数据库状态打印的日志: +```sh +[8321] 05 Sep 11:58:50.449 * DB loaded from append only file: 0.000 seconds +``` -- no(系统控制):由操作系统控制每次同步到 AOF 文件的周期,整体过程**不可控** +AOF 文件里面除了用于指定数据库的 SELECT 命令是服务器自动添加的,其他都是通过客户端发送的命令 -**AOF 缓冲区同步文件策略**,系统调用 write 和 fsync: +```sh +* 2\r\n6ドル\r\nSELECT\r\n1ドル\r\n0\r\n # 服务器自动添加 +* 3\r\n3ドル\r\nSET\r\n3ドル\r\nmsg\r\n5ドル\r\nhello\r\n +* 5\r\n4ドル\r\nSADD\r\n6ドル\r\nfruits\r\n5ドル\r\napple\r\n6ドル\r\nbanana\r\n6ドル\r\ncherry\r\n +``` -* write 操作会触发延迟写(delayed write)机制,Linux 在内核提供页缓冲区用来提高硬盘 IO 性能,write 操作在写入系统缓冲区后直接返回 -* 同步硬盘操作(刷脏)依赖于系统调度机制,比如缓冲区页空间写满或达到特定时间周期。同步文件之前,如果此时系统故障宕机,缓冲区内数据将丢失 -* fsync 针对单个文件操作(比如 AOF 文件)做强制硬盘同步,fsync 将阻塞到写入硬盘完成后返回,保证了数据持久化 +Redis 读取 AOF 文件并还原数据库状态的步骤: -异常恢复:AOF 文件损坏,通过 redis-check-aof--fix appendonly.aof 进行恢复,重启 Redis,然后重新加载 +* 创建一个**不带网络连接的伪客户端**(fake client)执行命令,因为 Redis 的命令只能在客户端上下文中执行, 而载入 AOF 文件时所使用的命令来源于本地 AOF 文件而不是网络连接 +* 从 AOF 文件分析并读取一条写命令 +* 使用伪客户端执行被读出的写命令,然后重复上述步骤 -*** +**** -#### 重写 -##### 介绍 -随着命令不断写入 AOF,文件会越来越大,为了解决这个问题 Redis 引入了 AOF 重写机制压缩文件体积 +#### 重写实现 -AOF 重写:将 Redis 进程内的数据转化为写命令同步到新 AOF 文件的过程,简单说就是将对同一个数据的若干个条命令执行结果转化成最终结果数据对应的指令进行记录 +##### 重写策略 -AOF 重写作用: +随着命令不断写入 AOF,文件会越来越大,很可能对 Redis 服务器甚至整个宿主计算机造成影响,为了解决这个问题 Redis 引入了 AOF 重写机制压缩文件体积 -- 降低磁盘占用量,提高磁盘利用率 -- 提高持久化效率,降低持久化写时间,提高 IO 性能 -- 降低数据恢复的用时,提高数据恢复效率 +AOF 重写:读取服务器当前的数据库状态,**生成新 AOF 文件来替换旧 AOF 文件**,不会对现有的 AOF 文件进行任何读取、分析或者写入操作,而是直接原子替换。新 AOF 文件不会包含任何浪费空间的冗余命令,所以体积通常会比旧 AOF 文件小得多 AOF 重写规则: - 进程内具有时效性的数据,并且数据已超时将不再写入文件 -- 非写入类的无效指令将被忽略,只保留最终数据的写入命令 +- 对同一数据的多条写命令合并为一条命令,因为会读取当前的状态,所以直接将当前状态转换为一条命令即可。为防止数据量过大造成客户端缓冲区溢出,对 list、set、hash、zset 等集合类型,**单条指令**最多写入 64 个元素 - 如 del key1、 hdel key2、srem key3、set key4 111、set key4 222 等,select 指令虽然不更改数据,但是更改了数据的存储位置,此类命令同样需要记录 + 如 lpushlist1 a、lpush list1 b、lpush list1 c 可以转化为:lpush list1 a b c -- 对同一数据的多条写命令合并为一条命令 - - 如 lpushlist1 a、lpush list1 b、lpush list1 c 可以转化为:lpush list1 a b c。 - - 为防止数据量过大造成客户端缓冲区溢出,对 list、set、hash、zset 等类型,每条指令最多写入 64 个元素 +- 非写入类的无效指令将被忽略,只保留最终数据的写入命令,但是 select 指令虽然不更改数据,但是更改了数据的存储位置,此类命令同样需要记录 +AOF 重写作用: +- 降低磁盘占用量,提高磁盘利用率 +- 提高持久化效率,降低持久化写时间,提高 IO 性能 +- 降低数据恢复的用时,提高数据恢复效率 @@ -11085,38 +12473,30 @@ AOF 重写规则: -##### 方式 +##### 重写原理 -* 手动重写 +AOF 重写程序 aof_rewrite 函数可以创建一个新 AOF 文件, 但是该函数会进行大量的写入操作,调用这个函数的线程将被长时间阻塞,所以 Redis 将 AOF 重写程序放到 fork 的子进程里执行,不会阻塞父进程,重写命令: - ```sh - bgrewriteaof - ``` +```sh +bgrewriteaof +``` - 原理分析: +* 子进程进行 AOF 重写期间,服务器进程(父进程)可以继续处理命令请求 + +* 子进程带有服务器进程的数据副本,使用子进程而不是线程,可以在避免使用锁的情况下, 保证数据的安全性 ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-AOF手动重写原理.png) -* 自动重写 +子进程在进行 AOF 重写期间,服务器进程还需要继续处理命令请求,而新命令可能会对现有的数据库状态进行修改,从而使得服务器当前的数据库状态和重写后的 AOF 文件所保存的数据库状态不一致,所以 Redis 设置了 AOF 重写缓冲区 - 触发时机:Redis 会记录上次重写时的 AOF 大小,默认配置是当 AOF 文件大小是上次重写后大小的一倍且文件大于 64M 时触发 - - ```sh - auto-aof-rewrite-min-size size #设置重写的基准值,最小文件 64MB,达到这个值开始重写 - auto-aof-rewrite-percentage percent #触发AOF文件执行重写的增长率,当前AOF文件大小超过上一次重写的AOF文件大小的百分之多少才会重写,比如文件达到 100% 时开始重写就是两倍时触发 - ``` +工作流程: + +* Redis 服务器执行完一个写命令,会同时将该命令追加到 AOF 缓冲区和 AOF 重写缓冲区(从创建子进程后才开始写入) +* 当子进程完成 AOF 重写工作之后,会向父进程发送一个信号,父进程在接到该信号之后, 会调用一个信号处理函数,该函数执行时会**对服务器进程(父进程)造成阻塞**(影响很小),主要工作: + * 将 AOF 重写缓冲区中的所有内容写入到新 AOF 文件中, 这时新 AOF 文件所保存的状态将和服务器当前的数据库状态一致 + * 对新的 AOF 文件进行改名,**原子地(atomic)覆盖**现有的 AOF 文件,完成新旧两个 AOF 文件的替换 - 自动重写触发比对参数( 运行指令 `info Persistence` 获取具体信息 ): - - ```sh - aof_current_size #AOF文件当前尺寸大小(单位:字节) - aof_base_size #AOF文件上次启动和重写时的尺寸大小(单位:字节) - ``` - 自动重写触发条件公式: - - - aof_current_size> auto-aof-rewrite-min-size - - (aof_current_size - aof_base_size) / aof_base_size>= auto-aof-rewrite-percentage @@ -11124,19 +12504,28 @@ AOF 重写规则: +##### 自动重写 +触发时机:Redis 会记录上次重写时的 AOF 大小,默认配置是当 AOF 文件大小是上次重写后大小的一倍且文件大于 64M 时触发 -#### 流程 +```sh +auto-aof-rewrite-min-size size #设置重写的基准值,最小文件 64MB,达到这个值开始重写 +auto-aof-rewrite-percentage percent #触发AOF文件执行重写的增长率,当前AOF文件大小超过上一次重写的AOF文件大小的百分之多少才会重写,比如文件达到 100% 时开始重写就是两倍时触发 +``` -持久化流程: +自动重写触发比对参数( 运行指令 `info Persistence` 获取具体信息 ): + +```sh +aof_current_size #AOF文件当前尺寸大小(单位:字节) +aof_base_size #AOF文件上次启动和重写时的尺寸大小(单位:字节) +``` -![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-AOF重写流程1.png) +自动重写触发条件公式: -重写流程: +- aof_current_size> auto-aof-rewrite-min-size +- (aof_current_size - aof_base_size) / aof_base_size>= auto-aof-rewrite-percentage -![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-AOF重写流程2.png) -使用**新的 AOF 文件覆盖旧的 AOF 文件**,完成 AOF 重写 @@ -11146,39 +12535,40 @@ AOF 重写规则: ### 对比 -AOF 和 RDB 同时开启,系统默认取 AOF 的数据(数据不会存在丢失) +RDB 的特点 - RDB 与 AOF 对比: +* RDB 优点: + - RDB 是一个紧凑压缩的二进制文件,存储效率较高,但存储数据量较大时,存储效率较低 + - RDB 内部存储的是 Redis 在某个时间点的数据快照,非常**适合用于数据备份,全量复制、灾难恢复** + - RDB 恢复数据的速度要比 AOF 快很多,因为是快照,直接恢复 +* RDB 缺点: -| 持久化方式 | RDB | AOF | -| ------------ | ------------------ | ------------------ | -| 占用存储空间 | 小(数据级:压缩) | 大(指令级:重写) | -| **存储速度** | 慢 | 快 | -| **恢复速度** | 快 | 慢 | -| 数据安全性 | 会丢失数据 | 依据策略决定 | -| 资源消耗 | 高/重量级 | 低/轻量级 | -| 启动优先级 | 低 | 高 | + - BGSAVE 指令每次运行要执行 fork 操作创建子进程,会牺牲一些性能 + - RDB 方式无论是执行指令还是利用配置,无法做到实时持久化,具有丢失数据的可能性,最后一次持久化后的数据可能丢失 + - Redis 的众多版本中未进行 RDB 文件格式的版本统一,可能出现各版本之间数据格式无法兼容 -应用场景: +AOF 特点: -- 对数据**非常敏感**,建议使用默认的 AOF 持久化方案 +* AOF 的优点:数据持久化有较好的实时性,通过 AOF 重写可以降低文件的体积 +* AOF 的缺点:文件较大时恢复较慢 - AOF 持久化策略使用 everysecond,每秒钟 fsync 一次,该策略 Redis 仍可以保持很好的处理性能,当出现问题时,最多丢失 1 秒内的数据 +AOF 和 RDB 同时开启,系统默认取 AOF 的数据(数据不会存在丢失) - 注意:AOF 文件存储体积较大,恢复速度较慢,因为要执行每条指令 +应用场景: -- 数据呈现**阶段有效性**,建议使用 RDB 持久化方案 +- 对数据**非常敏感**,建议使用默认的 AOF 持久化方案,AOF 持久化策略使用 everysecond,每秒钟 fsync 一次,该策略 Redis 仍可以保持很好的处理性能 - 数据可以良好的做到阶段内无丢失,且恢复速度较快,阶段内数据恢复通常采用 RDB 方案 + 注意:AOF 文件存储体积较大,恢复速度较慢,因为要执行每条指令 + +- 数据呈现**阶段有效性**,建议使用 RDB 持久化方案,可以做到阶段内无丢失,且恢复速度较快 注意:利用 RDB 实现紧凑的数据持久化,存储数据量较大时,存储效率较低 综合对比: - RDB 与 AOF 的选择实际上是在做一种权衡,每种都有利有弊 -- 如不能承受数分钟以内的数据丢失,对业务数据非常敏感,选用 AOF -- 如能承受数分钟以内的数据丢失,且追求大数据集的恢复速度,选用 RDB - 灾难恢复选用 RDB +- 如不能承受数分钟以内的数据丢失,对业务数据非常敏感,选用 AOF;如能承受数分钟以内的数据丢失,且追求大数据集的恢复速度,选用 RDB - 双保险策略,同时开启 RDB 和 AOF,重启后 Redis 优先使用 AOF 来恢复数据,降低丢失数据的量 - 不建议单独用 AOF,因为可能会出现 Bug,如果只是做纯内存缓存,可以都不用 @@ -11498,219 +12888,6 @@ unique_value 是客户端的**唯一标识**,可以用一个随机生成的字 -*** - - - -## 删除策略 - -### 过期数据 - -Redis 是一种内存级数据库,所有数据均存放在内存中,内存中的数据可以通过 TTL 指令获取其状态 - -TTL 返回的值有三种情况:正数,-1,-2 - -- 正数:代表该数据在内存中还能存活的时间 -- -1:永久有效的数据 -- 2 :已经过期的数据或被删除的数据或未定义的数据 - -删除策略:**删除策略就是针对已过期数据的处理策略**,已过期的数据不一定被立即删除,在不同的场景下使用不同的删除方式会有不同效果,这就是删除策略的问题 - -过期数据是一块独立的存储空间,Hash 结构,field 是内存地址,value 是过期时间,保存了所有 key 的过期描述,在最终进行过期处理的时候,对该空间的数据进行检测, 当时间到期之后通过 field 找到内存该地址处的数据,然后进行相关操作 - - - - - -**** - - - -### 数据删除 - -#### 删除策略 - -在内存占用与 CPU 占用之间寻找一种平衡,顾此失彼都会造成整体 Redis 性能的下降,甚至引发服务器宕机或内存泄露 - -针对过期数据有三种删除策略: - -- 定时删除 -- 惰性删除 -- 定期删除 - - - -*** - - - -#### 定时删除 - -创建一个定时器,当 key 设置有过期时间,且过期时间到达时,由定时器任务立即执行对键的删除操作 - -- 优点:节约内存,到时就删除,快速释放掉不必要的内存占用 -- 缺点:无论 CPU 此时负载量多高,均占用 CPU,会影响 Redis 服务器响应时间和指令吞吐量 -- 总结:用处理器性能换取存储空间(拿时间换空间) - - - -*** - - - -#### 惰性删除 - -数据到达过期时间,不做处理,等下次访问该数据时,需要判断: - -* 如果未过期,返回数据 -* 如果已过期,删除,返回不存在 - -在任何 get 操作之前都要执行 **expireIfNeeded()**,相当于绑定在一起 - -特点: - -* 优点:节约 CPU 性能,发现必须删除的时候才删除 -* 缺点:内存压力很大,出现长期占用内存的数据 -* 总结:用存储空间换取处理器性能(拿空间换时间) - -Redis 采用惰性删除和定期删除策略的结合使用 - - - -*** - - - -#### 定期删除 - -定时删除和惰性删除这两种方案都是走的极端,定期删除就是折中方案 - -定期删除是**周期性轮询 Redis 库中的时效性**数据,采用随机抽取的策略,利用过期数据占比的方式控制删除频度 - -定期删除方案: - -- Redis 启动服务器初始化时,读取配置 server.hz 的值,默认为 10,执行指令 info server 可以查看 - -- 每秒钟执行 server.hz 次 serverCron() → databasesCron() → activeExpireCycle() - -- databasesCron() 操作是**轮询每个数据库** - -- activeExpireCycle() 对某个数据库中的每个 expires 进行检测,每次执行耗时:250ms/server.hz - - 对某个 expires[*] 检测时,随机挑选 W 个 key 检测 - - - 如果 key 超时,删除 key - - 如果一轮中删除的 key 的数量> W*25%,循环该过程 - - 如果一轮中删除的 key 的数量 ≤ W*25%,检查下一个 expires[],0-15 循环 - - W 取值 = ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP 属性值,自定义值 - -* 参数 current_db 用于记录 activeExpireCycle() 进入哪个expires[*] 执行 -* 如果 activeExpireCycle() 执行时间到期,下次从 current_db 继续向下执行 - - - -定期删除特点: - -- CPU 性能占用设置有峰值,检测频度可自定义设置 -- 内存压力不是很大,长期占用内存的**冷数据会被持续清理** -- 周期性抽查存储空间(随机抽查,重点抽查) - - - -*** - - - -#### 策略对比 - -| | 优点 | 缺点 | 特点 | -| -------- | ---------------- | ------------------------------- | ------------------ | -| 定时删除 | 节约内存,无占用 | 不分时段占用 CPU 资源,频度高 | 拿时间换空间 | -| 惰性删除 | 内存占用严重 | 延时执行,CPU 利用率高 | 拿空间换时间 | -| 定期删除 | 内存定期随机清理 | 每秒花费固定的 CPU 资源维护内存 | 随机抽查,重点抽查 | - - - -*** - - - -### 数据淘汰 - -#### 逐出算法 - -数据淘汰策略:当新数据进入 Redis 时,在执行每一个命令前,会调用 **freeMemoryIfNeeded()** 检测内存是否充足。如果内存不满足新加入数据的最低存储要求,Redis 要临时删除一些数据为当前指令清理存储空间,清理数据的策略称为**逐出算法** - -逐出数据的过程不是 100% 能够清理出足够的可使用的内存空间,如果不成功则反复执行,当对所有数据尝试完毕,如不能达到内存清理的要求,**出现 Redis 内存打满异常**: - -```sh -(error) OOM command not allowed when used memory>'maxmemory' -``` - - - -**** - - - -#### 策略配置 - -Redis 如果不设置最大内存大小或者设置最大内存大小为 0,在 64 位操作系统下不限制内存大小,在 32 位操作系统默认为 3GB 内存,一般推荐设置 Redis 内存为最大物理内存的四分之三 - -内存配置方式: - -* 通过修改文件配置(永久生效):修改配置文件 maxmemory 字段,单位为字节 - -* 通过命令修改(重启失效): - - * `config set maxmemory 104857600`:设置 Redis 最大占用内存为 100MB - * `config get maxmemory`:获取 Redis 最大占用内存 - - * `info` :可以查看 Redis 内存使用情况,`used_memory_human` 字段表示实际已经占用的内存,`maxmemory` 表示最大占用内存 - -影响数据淘汰的相关配置如下,配置 conf 文件: - -* 每次选取待删除数据的个数,采用随机获取数据的方式作为待检测删除数据,防止全库扫描,导致严重的性能消耗,降低读写性能 - - ```sh - maxmemory-samples count - ``` - -* 达到最大内存后的,对被挑选出来的数据进行删除的策略 - - ```sh - maxmemory-policy policy - ``` - - 数据删除的策略 policy:3 类 8 种 - - 第一类:检测易失数据(可能会过期的数据集 server.db[i].expires): - - ```sh - volatile-lru # 对设置了过期时间的 key 选择最近最久未使用使用的数据淘汰 - volatile-lfu # 对设置了过期时间的 key 选择最近使用次数最少的数据淘汰 - volatile-ttl # 对设置了过期时间的 key 选择将要过期的数据淘汰 - volatile-random # 对设置了过期时间的 key 选择任意数据淘汰 - ``` - - 第二类:检测全库数据(所有数据集 server.db[i].dict ): - - ```sh - allkeys-lru # 对所有 key 选择最近最少使用的数据淘汰 - allkeLyRs-lfu # 对所有 key 选择最近使用次数最少的数据淘汰 - allkeys-random # 对所有 key 选择任意数据淘汰,相当于随机 - ``` - - 第三类:放弃数据驱逐 - - ```sh - no-enviction #禁止驱逐数据(redis4.0中默认策略),会引发OOM(Out Of Memory) - ``` - -数据淘汰策略配置依据:使用 INFO 命令输出监控信息,查询缓存 hit 和 miss 的次数,根据需求调优 Redis 配置 - - - *** @@ -12916,7 +14093,7 @@ Redis 中的监控指标如下: master_link_down_since_seconds ``` -要对redis的相关指标进行监控,我们可以采用一些用具: +要对 Redis 的相关指标进行监控,我们可以采用一些用具: - CloudInsight Redis - Prometheus @@ -12972,11 +14149,46 @@ Redis 中的监控指标如下: +**** + + + +## 其他指令 + +### 通信指令 + +Redis 发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息。 + +Redis 客户端可以订阅任意数量的频道 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-发布订阅.png) + +操作命令: + +1. 打开一个客户端订阅 channel1:`SUBSCRIBE channel1` +2. 打开另一个客户端,给 channel1发布消息 hello:`publish channel1 hello` +3. 第一个客户端可以看到发送的消息 + + + +注意:发布的消息没有持久化,所以订阅的客户端只能收到订阅后发布的消息 + + + +**** + + +### ACL 指令 +Redis ACL 是 Access Control List(访问控制列表)的缩写,该功能允许根据可以执行的命令和可以访问的键来限制某些连接 +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-ACL指令.png) +* acl cat:查看添加权限指令类别 +* acl whoami:查看当前用户 +* acl setuser username on>password ~cached:* +get:设置有用户名、密码、ACL 权限(只能 get) @@ -14162,3 +15374,4 @@ public JedisPool(GenericObjectPoolConfig poolConfig, String host, int port) { + diff --git a/Frame.md b/Frame.md index 598c6c7..9b54eaf 100644 --- a/Frame.md +++ b/Frame.md @@ -5297,7 +5297,7 @@ RocketMQ 基于 NettyRemotingServer 的 Reactor 多线程模型: * 一个 Reactor 主线程(eventLoopGroupBoss)负责监听 TCP 网络连接请求,建立好连接创建 SocketChannel(RocketMQ 会自动根据 OS 的类型选择 NIO 和 Epoll,也可以通过参数配置),并注册到 Selector 上,然后监听真正的网络数据 * 拿到网络数据交给 Worker 线程池(eventLoopGroupSelector,默认设置为 3),在真正执行业务逻辑之前需要进行 SSL 验证、编解码、空闲检查、网络连接管理,这些工作交给 defaultEventExecutorGroup(默认设置为 8)去做 -* 处理业务操作放在业务线程池中执行,根据 RomotingCommand 的**业务请求码 code*** 去 processorTable 这个本地缓存变量中找到对应的 processor,封装成 task 任务提交给对应的 processor 处理线程池来执行(sendMessageExecutor,以发送消息为例) +* 处理业务操作放在业务线程池中执行,根据 RomotingCommand 的**业务请求码 code** 去 processorTable 这个本地缓存变量中找到对应的 processor,封装成 task 任务提交给对应的 processor 处理线程池来执行(sendMessageExecutor,以发送消息为例) * 从入口到业务逻辑的几个步骤中线程池一直再增加,这跟每一步逻辑复杂性相关,越复杂,需要的并发通道越宽 | 线程数 | 线程名 | 线程具体说明 | diff --git a/Prog.md b/Prog.md index 3550754..9eea252 100644 --- a/Prog.md +++ b/Prog.md @@ -5887,7 +5887,7 @@ ThreadPoolExecutor 使用 int 的**高 3 位来表示线程池状态,低 29 private long completedTaskCount; // 记录线程池所完成任务总数,当某个 worker 退出时将完成的任务累加到该属性 ``` -* 控制核心线程数量内的线程是否可以被回收: +* 控制**核心线程数量内的线程是否可以被回收**: ```java // false(默认)代表不可以,为 true 时核心线程空闲超过 keepAliveTime 也会被回收 @@ -13454,7 +13454,7 @@ int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct t - n 是监测的 socket 的最大数量 -- timeout 为超时参数,调用 select 会一直阻塞直到有描述符的事件到达或者等待的时间超过 timeout +- timeout 为超时参数,调用 select 会一直**阻塞**直到有描述符的事件到达或者等待的时间超过 timeout ```c struct timeval{ @@ -14109,9 +14109,9 @@ Socket 类: * `Socket(InetAddress address,int port)`:创建流套接字并将其连接到指定 IP 指定端口号 - * `Socket(String host, int port)`:根据ip地址字符串和端口号创建客户端 Socket 对象 + * `Socket(String host, int port)`:根据 IP 地址字符串和端口号创建客户端 Socket 对象 - 注意事项:执行该方法,就会立即连接指定的服务器,连接成功,则表示三次握手通过,反之抛出异常 + 注意事项:**执行该方法,就会立即连接指定的服务器,连接成功,则表示三次握手通过**,反之抛出异常 * 常用 API: * `OutputStream getOutputStream()`:获得字节输出流对象 From 44c38ae036b62f0377d11db6dfbefe1aa74bc72d Mon Sep 17 00:00:00 2001 From: Seazean Date: Fri, 6 May 2022 19:42:27 +0800 Subject: [PATCH 02/35] Update Java Note --- DB.md | 1811 +++++++++++++++++++++++++++++++++++++++++++----------- Frame.md | 2 +- 2 files changed, 1464 insertions(+), 349 deletions(-) diff --git a/DB.md b/DB.md index c688e0a..c1be7a3 100644 --- a/DB.md +++ b/DB.md @@ -33,7 +33,7 @@ 参考视频:https://www.bilibili.com/video/BV1zJ411M7TB -参考文章:https://time.geekbang.org/column/intro/139 +参考专栏:https://time.geekbang.org/column/intro/139 参考书籍:https://book.douban.com/subject/35231266/ @@ -8764,8 +8764,6 @@ MySQL 支持 ACID 特性,保证可靠性和持久性,读取性能不高, 参考视频:https://www.bilibili.com/video/BV1CJ411m7Gc -参考视频:https://www.bilibili.com/video/BV1Rv41177Af - *** @@ -9575,7 +9573,7 @@ Redis 基于 Reactor 模式开发了网络事件处理器,这个处理器被 -尽管多个文件事件可能会并发出现,但是 I/O 多路复用程序将所有产生事件的套接字处理请求放入一个**单线程的执行队列**中,通过队列有序、同步的向文件事件分派器传送套接字,上一个套接字产生的事件处理完后,才会继续向分派器传统下一个 +尽管多个文件事件可能会并发出现,但是 I/O 多路复用程序将所有产生事件的套接字处理请求放入一个**单线程的执行队列**中,通过队列有序、同步的向文件事件分派器传送套接字,上一个套接字产生的事件处理完后,才会继续向分派器传送下一个 @@ -9834,8 +9832,6 @@ typedef struct redisClient { - - *** @@ -9883,11 +9879,11 @@ typedef struct redisClient { * REDIS_MONITOR 表示客户端正在执行 MONITOR 命令 * REDIS_UNIX_SOCKET 表示服务器使用 UNIX 套接字来连接客户端 * REDIS_BLOCKED 表示客户端正在被 BRPOP、BLPOP 等命令阻塞 -* REDIS_UNBLOCKED 表示客户端已经从 REDIS_BLOCKED 所表示的阻塞状态脱离,该标志只能在 REDIS_BLOCKED 标志已经打开的情况下使用 +* REDIS_UNBLOCKED 表示客户端已经从 REDIS_BLOCKED 所表示的阻塞状态脱离,在 REDIS_BLOCKED 标志打开的情况下使用 * REDIS_MULTI 标志表示客户端正在执行事务 * REDIS_DIRTY_CAS 表示事务使用 WATCH 命令监视的数据库键已经被修改 * REDIS_DIRTY_EXEC 表示事务在命令入队时出现了错误。以上两个标志都表示事务的安全性已经被破坏,只要两个标记中的任意一个被打开,EXEC 命令必然会执行失败,这两个标志只能在客户端打开了 REDIS_MULTI 标志的情况下使用 -* REDIS_CLOSE_ASAP 表示客户端的输出缓冲区大小超出了服务器允许的范围,服务器会在下 一次执行 serverCron 函数时关闭这个客户端,防止服务器的稳定性受到这个客户端影响,积存在输出缓冲区中的所有内容会被**直接释放**,不会返回给客户端 +* REDIS_CLOSE_ASAP 表示客户端的输出缓冲区大小超出了服务器允许的范围,关闭这个客户端并且直接丢弃缓冲区的内容 * REDIS_CLOSE_AFTER_REPLY 表示有用户对这个客户端执行了 `CLIENT KILL` 命令,或者客户端发送给服务器的命令请求中包含了错误的协议内容,服务器会将客户端积存在输出缓冲区中的所有内容发送给客户端,然后关闭客户端 * REDIS_ASKING 表示客户端向集群节点(运行在集群模式下的服务器)发送了 `ASKING` 命令 * REDIS_FORCE_AOF 表示强制服务器将当前执行的命令写入到 AOF 文件里,执行 `PUBSUB` 命令会使客户端打开该标志 @@ -10044,7 +10040,7 @@ lua_client 伪客户端在服务器运行的整个生命周期会一直存在, 理论上来说,可变缓冲区可以保存任意长的命令回复,但是为了回复过大占用过多的服务器资源,服务器会时刻检查客户端的输出缓冲区的大小,并在缓冲区的大小超出范围时,执行相应的限制操作: -* 硬性限制 (hard limit):输出缓冲区的大小超过了硬性限制所设置的大小,那么服务器立即关闭客户端 +* 硬性限制 (hard limit):输出缓冲区的大小超过了硬性限制所设置的大小,那么服务器会关闭客户端(serverCron 函数中执行),积存在输出缓冲区中的所有内容会被**直接释放**,不会返回给客户端 * 软性限制 (soft limit):输出缓冲区的大小超过了软性限制所设置的大小,小于硬性限制的大小,服务器的操作: * 用属性 obuf_soft_limit_reached_time 记录下客户端到达软性限制的起始时间,继续监视客户端 * 如果输出缓冲区的大小一直超出软性限制,并且持续时间超过服务器设定的时长,那么服务器将关闭客户端 @@ -10436,7 +10432,7 @@ struct redisServer { ##### 缓冲限制 -服务器会关闭那些输入或者输出缓冲区大小超出限制的客户端 +服务器会关闭那些输入或者输出**缓冲区大小超出限制**的客户端 @@ -10829,7 +10825,7 @@ Redis 对 rehash 做了优化,使 rehash 的动作并不是一次性、集中 * 在 rehash 进行期间,每次对字典执行添加、删除、查找或者更新操作时,程序除了执行指定的操作以外,还会顺带将 ht[0] 哈希表在 rehashidx 索引上的所有键值对 rehash 到 ht[1],rehash 完成之后**将 rehashidx 属性的值增一** * 随着字典操作的不断执行,最终在某个时间点上 ht[0] 的所有键值对都被 rehash 至 ht[1],这时程序将 rehashidx 属性的值设为 -1,表示 rehash 操作已完成 -渐进式 rehash 采用分而治之的方式,将 rehash 键值对所需的计算工作均摊到对字典的每个添加、删除、查找和更新操作上,从而避免了集中式 rehash 带来的庞大计算量 +渐进式 rehash 采用**分而治之**的方式,将 rehash 键值对所需的计算工作均摊到对字典的每个添加、删除、查找和更新操作上,从而避免了集中式 rehash 带来的庞大计算量 渐进式 rehash 期间的哈希表操作: @@ -10914,7 +10910,7 @@ typedef struct zskiplistNode { 后退指针:backward 用于从表尾到表头方向逆序(降序)遍历节点 -分值:score 属性一个 double 类型的浮点数,跳跃表中的所有节点都按分值从小到大来排序 +分值:score 属性一个 double 类型的浮点数,跳跃表中的所有节点都**按分值从小到大来排序** 成员对象:obj 属性是一个指针,指向一个 SDS 字符串对象。同一个跳跃表中,各个节点保存的成员对象必须是唯一的,但是多个节点保存的分值可以是相同的,分值相同的节点将按照成员对象在字典序中的大小来进行排序(从小到大) @@ -12722,6 +12718,8 @@ vfork(虚拟内存 fork virtual memory fork):调用 vfork() 父进程被 + + ## 事务机制 ### 基本操作 @@ -12894,35 +12892,22 @@ unique_value 是客户端的**唯一标识**,可以用一个随机生成的字 -## 主从复制 - -### 基本介绍 - -**三高**架构: -- 高并发:应用提供某一业务要能支持很多客户端同时访问的能力,称为并发 -- 高性能:性能带给我们最直观的感受就是速度快,时间短 +## 主从复制 -- 高可用: - - 可用性:应用服务在全年宕机的时间加在一起就是全年应用服务不可用的时间 - - 业界可用性目标 5 个 9,即 99.999%,即服务器年宕机时长低于 315 秒,约 5.25 分钟 +### 基本操作 -主从复制: +#### 主从介绍 -* 概念:将 master 中的数据即时、有效的复制到 slave 中 -* 特征:一个 master 可以拥有多个 slave,一个 slave 只对应一个 master -* 职责:master 和 slave 各自的职责不一样 +主从复制:一个服务器去复制另一个服务器,被复制的服务器为主服务器 master,复制的服务器为从服务器 slave - master: - * **写数据**,执行写操作时,将出现变化的数据自动同步到 slave - * 读数据(可忽略) +* master 用来**写数据**,执行写操作时,将出现变化的数据自动同步到 slave,很少会进行读取操作 +* slave 用来读数据,禁止在 slave 服务器上进行读操作 - slave - * **读数据** - * 写数据(禁止) +进行复制中的主从服务器双方的数据库将保存相同的数据,将这种现象称作**数据库状态一致** -主从复制的机制: +主从复制的特点: * **薪火相传**:一个 slave 可以是下一个 slave 的 master,slave 同样可以接收其他 slave 的连接和同步请求,那么该 slave 作为了链条中下一个的 master, 可以有效减轻 master 的写压力,去中心化降低风险 @@ -12930,8 +12915,6 @@ unique_value 是客户端的**唯一标识**,可以用一个随机生成的字 * **反客为主**:当一个 master 宕机后,后面的 slave 可以立刻升为 master,其后面的 slave 不做任何修改 - 将从机变为主机的命令:`slaveof no one` - 主从复制的作用: - **读写分离**:master 写、slave 读,提高服务器的读写负载能力 @@ -12940,31 +12923,16 @@ unique_value 是客户端的**唯一标识**,可以用一个随机生成的字 - 数据冗余:实现数据热备份,是持久化之外的一种数据冗余方式 - 高可用基石:基于主从复制,构建哨兵模式与集群,实现 Redis 的高可用方案 -主从复制的应用场景: - -* 机器故障:硬盘故障、系统崩溃,造成数据丢失,对业务形成灾难性打击,基本上会放弃使用redis - -* 容量瓶颈:内存不足,放弃使用 Redis - -* 解决方案:为了避免单点 Redis 服务器故障,准备多台服务器,互相连通。将数据复制多个副本保存在不同的服务器上连接在一起,并保证数据是同步的。即使有其中一台服务器宕机,其他服务器依然可以继续提供服务,实现 Redis 高可用,同时实现数据冗余备份 - - - - - -*** - +**三高**架构: -### 工作流程 - -主从复制过程大体可以分为3个阶段 +- 高并发:应用提供某一业务要能支持很多客户端同时访问的能力,称为并发 -* 建立连接阶段(即准备阶段) -* 数据同步阶段 -* 命令传播阶段 +- 高性能:性能最直观的感受就是速度快,时间短 -![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-主从复制工作流程.png) +- 高可用: + - 可用性:应用服务在全年宕机的时间加在一起就是全年应用服务不可用的时间 + - 业界可用性目标 5 个 9,即 99.999%,即服务器年宕机时长低于 315 秒,约 5.25 分钟 @@ -12972,152 +12940,102 @@ unique_value 是客户端的**唯一标识**,可以用一个随机生成的字 -### 建立连接 - -#### 建立流程 - -建立连接阶段:建立 slave 到 master 的连接,使 master 能够识别 slave,并保存 slave 端口号 - -流程如下: - -1. 设置 master 的地址和端口,保存 master 信息 -2. 建立 socket 连接 -3. 发送 ping 命令(定时器任务) -4. 身份验证(可能没有) -5. 发送 slave 端口信息 -6. 主从连接成功 - -连接成功的状态: - -* slave:保存 master 的地址与端口 - -* master:保存 slave 的端口 - -* 主从之间创建了连接的 socket - - - - - -*** - +#### 操作指令 +系统状态指令: -#### 相关指令 +```sh +INFO replication +``` -* master 和 slave 互联 +master 和 slave 互连: - 方式一:客户端发送命令 +* 方式一:客户端发送命令,设置 slaveof 选项,产生主从结构 ```sh slaveof masterip masterport ``` - 方式二:服务器带参启动 +* 方式二:服务器带参启动 ```sh redis-server --slaveof masterip masterport ``` - 方式三:服务器配置(主流方式) +* 方式三:服务器配置(主流方式) ```sh slaveof masterip masterport ``` - * slave 系统信息:info 指令 - - ```sh - master_link_down_since_seconds - masterhost & masterport - ``` - - * master 系统信息: - - ```sh - uslave_listening_port(多个) - ``` - - * 系统信息: - - ```sh - info replication - ``` - -* 主从断开连接:断开 slave 与 master 的连接,slave 断开连接后,不会删除已有数据,只是不再接受 master 发送的数据 +主从断开连接: - slave客户端执行命令: +* slave 断开连接后,不会删除已有数据,只是不再接受 master 发送的数据,可以作**为从服务器升级为主服务器的指令** ```sh slaveof no one ``` - -* 授权访问:master 有服务端和客户端,slave 也有服务端和客户端,不仅服务端之间可以发命令,客户端也可以 - master 客户端发送命令设置密码: +授权访问:master 有服务端和客户端,slave 也有服务端和客户端,不仅服务端之间可以发命令,客户端也可以 + +* master 客户端发送命令设置密码: ```sh requirepass password ``` - + master 配置文件设置密码: ```sh config set requirepass password config get requirepass ``` - - slave 客户端发送命令设置密码: + +* slave 客户端发送命令设置密码: ```sh auth password ``` - + slave 配置文件设置密码: ```sh masterauth password ``` - + slave 启动服务器设置密码: ```sh redis-server –a password ``` - - -*** -### 数据同步 -#### 同步流程 +*** -数据同步需求: -- 在 slave 初次连接 master 后,复制 master 中的所有数据到 slave -- 将 slave 的数据库状态更新成 master 当前的数据库状态 -同步过程如下: +### 复制流程 -1. 请求同步数据 -2. 创建 RDB 同步数据 -3. 恢复 RDB 同步数据(从服务器会**清空原有数据**) -4. 请求部分同步数据 -5. 恢复部分同步数据 -6. 数据同步工作完成 +#### 旧版复制 -同步完成的状态: +Redis 的复制功能分为同步(sync)和命令传播(command propagate)两个操作 -* slave:具有 master 端全部数据,包含 RDB 过程接收的数据 +同步操作用于将从服务器的数据库状态更新至主服务器当前所处的数据库状态,该过程又叫全量复制: -* master:保存 slave 当前数据同步的位置 +* 从服务器向主服务器发送 SYNC 命令来进行同步 +* 收到 SYNC 的主服务器执行 BGSAVE 命令,在后台生成一个 RDB 文件,并使用一个**缓冲区**记录从现在开始执行的所有**写命令** +* 当 BGSAVE 命令执行完毕时,主服务器会将 RDB 文件发送给从服务器 +* 从服务接收并载入 RDB 文件(从服务器会**清空原有数据**) +* 缓冲区记录了 RDB 文件所在状态后的所有写命令,主服务器将在缓冲区的所有命令发送给从服务器,从服务器执行这些写命令 +* 至此从服务器的数据库状态和主服务器一致 -* 主从之间完成了数据克隆 +命令传播用于在主服务器的数据库状态被修改,导致主从数据库状态出现不一致时, 让主从服务器的数据库重新回到一致状态 - +* 主服务器会将自己执行的写命令,也即是造成主从服务器不一致的那条写命令,发送给从服务器 +* 从服务器接受命令并执行,主从服务器将再次回到一致状态 @@ -13125,39 +13043,37 @@ unique_value 是客户端的**唯一标识**,可以用一个随机生成的字 -#### 同步优化 +#### 功能缺陷 -* 数据同步阶段 master 说明 +SYNC 本身就是一个非常消耗资源的操作,每次执行 SYNC 命令,都需要执行以下动作: - 1. master 数据量巨大,数据同步阶段应避开流量高峰期,避免造成 master 阻塞,影响业务正常执行 +* 生成 RDB 文件,耗费主服务器大量 CPU 、内存和磁盘 I/O 资源 +* RDB 文件发送给从服务器,耗费主从服务器大量的网络资源(带宽和流量),并对主服务器响应命令请求的时间产生影响 +* 从服务器载入 RDB 文件,期间会因为阻塞而没办法处理命令请求 - 2. 复制缓冲区大小设定不合理,会导致**数据溢出**。比如进行全量复制周期太长,进行部分复制时发现数据已经存在丢失的情况,必须进行第二次全量复制,致使 slave 陷入死循环状态 +SYNC 命令下的从服务器对主服务器的复制分为两种情况: - ```sh - repl-backlog-size ?mb - ``` +* 初次复制:从服务器没有复制过任何主服务器,或者从服务器当前要复制的主服务器和上一次复制的主服务器不同 +* 断线后重复制:处于命令传播阶段的主从服务器因为网络原因而中断了复制,自动重连后并继续复制主服务器 - 建议设置如下: +旧版复制在断线后重复制时,也会创建 RDB 文件进行**全量复制**,但是从服务器只需要断线时间内的这部分数据,所以旧版复制的实现方式非常浪费资源 - * 测算从 master 到 slave 的重连平均时长 second - * 获取 master 平均每秒产生写命令数据总量 write_size_per_second - * 最优复制缓冲区空间 = 2 * second * write_size_per_second - 3. master 单机内存占用主机内存的比例不应过大,建议使用 50%-70% 的内存,留下 30%-50% 的内存用于执行 bgsave 命令和创建复制缓冲区 -* 数据同步阶段 slave 说明 +**** - 1. 为避免 slave 进行全量复制、部分复制时服务器响应阻塞或数据不同步,建议关闭此期间的对外服务 - ```sh - slave-serve-stale-data yes|no - ``` - 2. 数据同步阶段,master 发给 slave 信息可以理解 master是 slave 的一个客户端,主动向 slave 发送命令 +#### 新版复制 + +Redis 从 2.8 版本开始,使用 PSYNC 命令代替 SYNC 命令来执行复制时的**同步操作**(命令传播阶段相同),解决了旧版复制在处理断线重复制情况的低效问题 + +PSYNC 命令具有完整重同步(full resynchronization)和**部分重同步**(partial resynchronization)两种模式: + +* 完整重同步:处理初次复制情况,执行步骤和 SYNC命令基本一样 +* 部分重同步:处理断线后重复制情况,主服务器可以将主从连接断开期间执行的写命令发送给从服务器,从服务器只要接收并执行这些写命令,就可以将数据库更新至主服务器当前所处的状态,该过程又叫**部分复制** - 3. 多个 slave 同时对 master 请求数据同步,master 发送的 RDB 文件增多,会对带宽造成巨大冲击,如果 master 带宽不足,因此数据同步需要根据业务需求,适量错峰 - 4. slave 过多时,建议调整拓扑结构,由一主多从结构变为树状结构,中间的节点既是 master,也是 slave。注意使用树状结构时,由于层级深度,导致深度越高的 slave 与最顶层 master 间数据同步延迟较大,数据一致性变差,应谨慎选择 @@ -13165,53 +13081,62 @@ unique_value 是客户端的**唯一标识**,可以用一个随机生成的字 -### 命令传播 +### 部分同步 + +部分重同步功能由以下三个部分构成: -#### 传播原理 +* 主服务器的复制偏移量(replication offset)和从服务器的复制偏移量 +* 主服务器的复制积压缓冲区(replication backlog) +* 服务器的运行 ID (run ID) -命令传播:当 master 数据库状态被修改后,导致主从服务器数据库状态不一致,此时需要让主从数据同步到一致的状态,同步的动作称为命令传播 -命令传播的过程:master 将接收到的数据变更命令发送给 slave,slave 接收命令后执行命令 -命令传播阶段出现了断网现象: +#### 偏移量 -* 网络闪断闪连:忽略 -* 短时间网络中断:部分复制 -* 长时间网络中断:全量复制 +主服务器和从服务器会分别维护一个复制偏移量: -部分复制的三个核心要素:服务器的运行 id(run id)、主服务器的复制积压缓冲区、主从服务器的复制偏移量 +* 主服务器每次向从服务器传播 N 个字节的数据时,就将自己的复制偏移量的值加上 N -* 服务器运行ID(runid):服务器运行 ID 是每一台服务器每次运行的身份识别码,一台服务器多次运行可以生成多个运行 ID,由 40 位字符组成,是一个随机的十六进制字符 +* 从服务器每次收到主服务器传播来的 N 个字节的数据时,就将自己的复制偏移量的值加上 N - 作用:服务器间进行传输识别身份,如果想两次操作均对同一台服务器进行,**每次必须操作携带对应的运行 ID**,用于对方识别 +通过对比主从服务器的复制偏移量,可以判断主从服务器是否处于一致状态 - 实现:运行 ID 在每台服务器启动时自动生成,master 在首次连接 slave 时,将运行 ID 发送给 slave,slave 保存此 ID,通过 info Server 命令,可以查看节点的 runid +* 主从服务器的偏移量是相同的,说明主从服务器处于一致状态 +* 主从服务器的偏移量是不同的,说明主从服务器处于不一致状态 -* 复制缓冲区:复制积压缓冲区,是一个先进先出(FIFO)的队列,用于存储服务器执行过的命令 - 作用:用于保存 master 收到的所有指令(仅影响数据变更的指令,例如 set,select) - 实现方式:每次传播命令,master 都会将传播的命令记录下来,并存储在复制缓冲区,复制缓冲区默认数据存储空间大小是 1M,当入队元素的数量大于队列长度时,最先入队的元素被弹出,新元素会被放入队列 +*** -* 复制偏移量:一个数字,描述复制缓冲区中的指令字节位置 - - master 复制偏移量:记录发送给所有 slave 的指令字节对应的位置(多个) - - slave 复制偏移量:记录 slave 接收 master 发送过来的指令字节对应的位置(一个) - - 作用:同步信息,比对 master 与 slave 的差异,当 slave 断线后,恢复数据使用 - - 数据来源: - - - master 端:发送一次记录一次 - - slave 端:接收一次记录一次 -**工作原理**: +#### 缓冲区 + +复制积压缓冲区是由主服务器维护的一个固定长度(fixed-size)先进先出(FIFO)队列,默认大小为 1MB + +* 出队规则跟普通的先进先出队列一样 +* 入队规则是当入队元素的数量大于队列长度时,最先入队的元素会被弹出,然后新元素才会被放入队列 + +当主服务器进行**命令传播时,不仅会将写命令发送给所有从服务器,还会将写命令入队到复制积压缓冲区**,缓冲区会保存着一部分最近传播的写命令,并且缓冲区会为队列中的每个字节记录相应的复制偏移量 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-复制积压缓冲区.png) -- 通过 offset 区分不同的 slave 当前数据传播的差异 -- master 记录已发送的信息对应的 offset -- slave 记录已接收的信息对应的 offset +从服务器会通过 PSYNC 命令将自己的复制偏移量 offset 发送给主服务器,主服务器会根据这个复制偏移量来决定对从服务器执行何种同步操作: - +* offset 之后的数据(即 offset+1)仍然存在于复制积压缓冲区里,那么主服务器将对从服务器执行部分重同步操作 +* offset 之后的数据已经不在复制积压缓冲区,说明部分数据已经丢失,那么主服务器将对从服务器执行完整重同步操作 + +复制缓冲区大小设定不合理,会导致**数据溢出**。比如主服务器需要执行大量写命令,又或者主从服务器断线后重连接所需的时间较长,导致缓冲区中的数据已经丢失,则必须进行完整重同步 + +```sh +repl-backlog-size ?mb +``` + +建议设置如下,这样可以保证绝大部分断线情况都能用部分重同步来处理: + +* 从服务器断线后重新连接上主服务器所需的平均时间 second +* 获取 master 平均每秒产生写命令数据总量 write_size_per_second +* 最优复制缓冲区空间 = 2 * second * write_size_per_second @@ -13219,9 +13144,98 @@ unique_value 是客户端的**唯一标识**,可以用一个随机生成的字 -#### 复制流程 +#### 运行ID + +服务器运行 ID(run ID):是每一台服务器每次运行的身份识别码,在服务器启动时自动生成,由 40 位随机的十六进制字符组成,一台服务器多次运行可以生成多个运行 ID + +作用:服务器间进行传输识别身份,如果想两次操作均对同一台服务器进行,**每次必须操作携带对应的运行 ID**,用于对方识别 + +从服务器对主服务器进行初次复制时,主服务器将自己的运行 ID 传送给从服务器,然后从服务器会将该运行 ID 保存。当从服务器断线并重新连上一个主服务器时,会向当前连接的主服务器发送之前保存的运行 ID: + +* 如果运行 ID 和当前连接的主服务器的运行 ID 相同,说明从服务器断线之前复制的就是当前连接的这个主服务器,执行部分重同步 +* 如果不同,需要执行完整重同步操作 + + + + + +*** + + + +#### PSYNC + +PSYNC 命令的调用方法有两种 + +* 如果从服务器之前没有复制过任何主服务器,或者执行了 `SLAVEOF no one`,开始一次新的复制时将向主服务器发送 `PSYNC ? -1` 命令,主动请求主服务器进行完整重同步 +* 如果从服务器已经复制过某个主服务器,那么从服务器在开始一次新的复制时将向主服务器发送 `PSYNC ` 命令,runid 是上一次复制的主服务器的运行 ID,offset 是复制的偏移量 + +接收到 PSYNC 命令的主服务器会向从服务器返回以下三种回复的其中一种: + +* 执行完整重同步操作:返回 `+FULLRESYNC `,runid 是主服务器的运行 ID,offset 是主服务器的复制偏移量 +* 执行部分重同步操作:返回 `+CONTINUE`,从服务器收到该回复说明只需要等待主服务器发送缺失的部分数据即可 +* 主服务器的版本低于 Redis2.8:返回 `-ERR`,版本过低识别不了 PSYNC,从服务器将向主服务器发送 SYNC 命令 + + + + + +*** + + + +### 复制实现 + +#### 实现流程 + +通过向从服务器发送 SLAVEOF 命令,可以让从服务器去复制一个主服务器 + +* 设置主服务器的地址和端口:将 SLAVEOF 命令指定的 ip 和 port 保存到服务器状态 redisServer + + ```c + struct redisServer { + // 主服务器的地址 + char *masterhost; + //主服务器的端口 + int masterport; + }; + ``` + + SLAVEOF 命令是一个异步命令,在完成属性的设置后服务器直接返回 OK,而实际的复制工作将在 OK返回之后才真正开始执行 + +* 建立套接字连接: + + * 从服务器 connect 主服务器建立套接字连接,成功后从服务器将为这个套接字关联一个用于复制工作的文件事件处理器,负责执行后续的复制工作,如接收 RDB 文件、接收主服务器传播来的写命令等 + * 主服务器在接受 accept 从务器的套接字连接后,将为该套接字创建相应的客户端状态,将从服务器看作一个客户端,从服务器将同时具有 server 和 client(可以发命令)两个身份 + +* 发送 PING 命令:从服务器向主服务器发送一个 PING 命令,检查主从之间的通信是否正常、主服务器处理命令的能力是否正常 + + * 返回错误,表示主服务器无法处理从服务器的命令请求(忙碌),从服务器断开并重新创建连向主服务器的套接字 + * 返回命令回复,但从服务器不能在规定的时间内读取出命令回复的内容,表示主从之间的网络状态不佳,需要断开重连 + * 读取到 PONG,表示一切状态正常,可以执行复制 + +* 身份验证:如果从服务器设置了 masterauth 选项就进行身份验证,将向主服务器发送一条 AUTH 命令,命令参数为从服务器 masterauth 选项的值,如果主从设置的密码不相同,那么主将返回一个 invalid password 错误 + +* 发送端口信息:身份验证后 + + * 从服务器执行命令 `REPLCONF listening-port `, 向主服务器发送从服务器的监听端口号 + * 主服务器在接收到这个命令后,会将端口号记录在对应的客户端状态 redisClient.slave_listening_port 属性中: + +* 同步:从服务器将向主服务器发送 PSYNC 命令,在同步操作执行之后,**主从服务器双方都是对方的客户端**,可以相互发送命令 + + * 完整重同步:主服务器需要成为从服务器的客户端,才能将保存在缓冲区里面的写命令发送给从服务器执行 + + * 部分重同步:主服务器需要成为从服务器的客户端,才能向从服务器发送保存在复制积压缓冲区里面的写命令 + +* 命令传播:主服务器将写命令发送给从服务器,保持数据库的状态一致 + + + +*** + + -全量复制/部分复制 +#### 复制图示 ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-主从复制流程更新.png) @@ -13233,38 +13247,73 @@ unique_value 是客户端的**唯一标识**,可以用一个随机生成的字 +### 心跳检测 + #### 心跳机制 -心跳机制:进入命令传播阶段,master 与 slave 间需要信息交换,使用心跳机制维护,实现双方连接保持在线 +心跳机制:进入命令传播阶段,从服务器默认会以每秒一次的频率,向主服务器发送命令:`REPLCONF ACK `,re_offset 是从服务器当前的复制偏移量 -master 心跳任务: +心跳的作用: -- 内部指令:PING -- 周期:由 `repl-ping-slave-period` 决定,默认10秒 -- 作用:判断 slave 是否在线 -- 查询:INFO replication 获取 slave 最后一次连接时间间隔,lag 项维持在 0 或 1 视为正常 +* 检测主从服务器的网络连接状态 +* 辅助实现 min-slaves 选项 +* 检测命令丢失 -slave 心跳任务 -- 内部指令:REPLCONF ACK {offset} -- 周期:1秒 -- 作用:汇报 slave 自己的复制偏移量,获取最新的数据变更指令;判断 master 是否在线 -心跳阶段注意事项: +*** -* 当 slave 多数掉线,或延迟过高时,master 为保障数据稳定性,将拒绝所有信息同步 - slave 数量少于 2 个,或者所有 slave 的延迟都大于等于 8 秒时,强制关闭 master 写功能,停止数据同步 - ```sh - min-slaves-to-write 2 - min-slaves-max-lag 8 - ``` +#### 网络状态 + +如果主服务器超过一秒钟没有收到从服务器发来的 REPLCONF ACK 命令,主服务就认为主从服务器之间的连接出现问题 + +向主服务器发送 `INFO replication` 命令,lag 一栏表示从服务器最后一次向主服务器发送 ACK 命令距离现在多少秒: + +```sh +127.0.0.1:6379> INFO replication +# Replication +role:master +connected_slaves:2 +slave0: ip=127.0.0.1,port=11111,state=online,offset=123,lag=O # 刚刚发送过 REPLCONF ACK +slavel: ip=127.0.0.1,port=22222,state=online,offset=456,lag=3 # 3秒之前发送过REPLCONF ACK +``` + +在一般情况下,lag 的值应该在 0 或者 1 秒之间跳动,如果超过 1 秒说明主从服务器之间的连接出现了故障 + + + +*** + -* slave 数量由 slave 发送 REPLCONF ACK 命令做确认 + +#### 配置选项 + +Redis 的 min-slaves-to-write 和 min-slaves-max-lag 两个选项可以防止主服务器在**不安全的情况下**执行写命令 + +比如向主服务器设置: + +```sh +min-slaves-to-write 5 +min-slaves-max-lag 10 +``` + +那么在从服务器的数最少于 5 个,或者 5 个从服务器的延迟(lag)值都大于或等于10 秒时,主服务器将拒绝执行写命令 + + + +*** + + + +#### 命令丢失 + +检测命令丢失:由于网络或者其他原因,主服务器传播给从服务器的写命令丢失,那么当从服务器向主服务器发送 REPLCONF ACK 命令时,主服务器会检查从服务器的复制偏移量是否小于自己的,然后在复制积压缓冲区里找到从服务器缺少的数据,并将这些数据重新发送给从服务器 + +说明:REPLCONF ACK 命令和复制积压缓冲区都是 Redis 2.8 版本新增的,在 Redis 2.8 版本以前,即使命令在传播过程中丢失,主从服务器都不会注意到,也不会向从服务器补发丢失的数据,所以为了保证主从复制的数据一致性,最好使用 2.8 或以上版本的 Redis -- slave 延迟由 slave 发送 REPLCONF ACK 命令做确认 @@ -13276,24 +13325,21 @@ slave 心跳任务 #### 重启恢复 -系统不断运行,master 的数据量会越来越大,一旦 master 重启,runid 将发生变化,会导致全部 slave 的全量复制操作 +系统不断运行,master 的数据量会越来越大,一旦 **master 重启**,runid 将发生变化,会导致全部 slave 的全量复制操作 解决方法:本机保存上次 runid,重启后恢复该值,使所有 slave 认为还是之前的 master 优化方案: -* master 内部创建 master_replid 变量,使用 runid 相同的策略生成,长度41位,并发送给所有 slave +* master 内部创建 master_replid 变量,使用 runid 相同的策略生成,并发送给所有 slave -* 在master关闭时执行命令 `shutdown save`,进行 RDB 持久化,将 runid 与 offset 保存到 RDB 文件中 +* 在 master 关闭时执行命令 `shutdown save`,进行 RDB 持久化,将 runid 与 offset 保存到 RDB 文件中 `redis-check-rdb dump.rdb` 命令可以查看该信息,保存为 repl-id 和 repl-offset -* master 重启后加载 RDB 文件,恢复数据 +* master 重启后加载 RDB 文件,恢复数据,将 RDB 文件中保存的 repl-id 与 repl-offset 加载到内存中,master_repl_id = repl-id,master_repl_offset = repl-offset - 重启后,将 RDB 文件中保存的 repl-id 与 repl-offset 加载到内存中 - - * master_repl_id = repl-id,master_repl_offset = repl-offset - * 通过 info 命令可以查看该信息 +* 通过 info 命令可以查看该信息 @@ -13306,9 +13352,10 @@ slave 心跳任务 master 的 CPU 占用过高或 slave 频繁断开连接 * 出现的原因: + * slave 每 1 秒发送 REPLCONF ACK 命令到 master * 当 slave 接到了慢查询时(keys * ,hgetall等),会大量占用 CPU 性能 - * master 每1秒调用复制定时函数 replicationCron(),比对 slave 发现长时间没有进行响应 + * master 每 1 秒调用复制定时函数 replicationCron(),比对 slave 发现长时间没有进行响应 最终导致 master 各种资源(输出缓冲区、带宽、连接等)被严重占用 @@ -13321,6 +13368,7 @@ master 的 CPU 占用过高或 slave 频繁断开连接 slave 与 master 连接断开 * 出现的原因: + * master 发送 ping 指令频度较低 * master 设定超时时间较短 * ping 指令在网络中存在丢包 @@ -13355,6 +13403,8 @@ slave 与 master 连接断开 开启后仅响应 info、slaveof 等少数命令(慎用,除非对数据一致性要求很高) +* 多个 slave 同时对 master 请求数据同步,master 发送的 RDB 文件增多,会对带宽造成巨大冲击,造成 master 带宽不足,因此数据同步需要根据业务需求,适量错峰 + @@ -13363,26 +13413,29 @@ slave 与 master 连接断开 + + ## 哨兵模式 ### 哨兵概述 -如果 Redis 的 master 宕机了,需要从 slave 中重新选出一个 master,要实现这些功能就需要 Redis 的哨兵 +Sentinel(哨兵)是 Redis 的高可用性(high availability)解决方案,由一个或多个 Sentinel 实例 instance 组成的 Sentinel 系统可以监视任意多个主服务器,以及这些主服务器的所有从服务器,并在被监视的主服务器下线时进行故障转移 -哨兵(sentinel)是一个分布式系统,用于对主从结构中的每台服务器进行**监控**,当出现故障时通过**投票机制选择**新的 master 并将所有 slave 连接到新的 master + - +* 双环图案表示主服务器 +* 单环图案表示三个从服务器 哨兵的作用: - 监控:监控 master 和 slave,不断的检查 master 和 slave 是否正常运行,master 存活检测、master 与 slave 运行情况检测 -- 通知:当被监控的服务器出现问题时,向其他(哨兵间,客户端)发送通知 +- 通知:当被监控的服务器出现问题时,向其他哨兵发送通知 - 自动故障转移:断开 master 与 slave 连接,选取一个 slave 作为 master,将其他 slave 连接新的 master,并告知客户端新的服务器地址 -注意:哨兵也是一台 Redis 服务器,只是不提供数据相关服务,通常哨兵的数量配置为单数(投票) + @@ -13392,103 +13445,107 @@ slave 与 master 连接断开 ### 启用哨兵 -配置哨兵: +#### 配置方式 + +配置三个哨兵 sentinel.conf:一般多个哨兵配置相同、端口不同,特殊需求可以配置不同的属性 -* 配置一拖二的主从结构 +```sh +port 26401 +dir "/redis/data" +sentinel monitor mymaster 127.0.0.1 6401 2 +sentinel down-after-milliseconds mymaster 5000 +sentinel failover-timeout mymaster 20000 +sentinel parallel-sync mymaster 1 +sentinel deny-scripts-reconfig yes +``` -* 配置三个哨兵(配置相同,端口不同),sentinel.conf +配置说明: + +* 设置哨兵监听的主服务器信息,判断主观下线所需要的票数 ```sh - port 26401 - dir "/redis/data" - sentinel monitor mymaster 127.0.0.1 6401 2 - sentinel down-after-milliseconds mymaster 5000 - sentinel failover-timeout mymaster 20000 - sentinel parallel-sync mymaster 1 - sentinel deny-scripts-reconfig yes + sentinel monitor ``` - 配置说明: +* 指定哨兵在监控 Redis 服务时,设置判定服务器宕机的时长,该设置控制是否进行主从切换 - * 设置哨兵监听的主服务器信息, sentinel_number 表示参与投票的哨兵数量 - - ```sh - sentinel monitor master_name master_host master_port sentinel_number - ``` - - * 指定哨兵在监控 Redis 服务时,设置判定服务器宕机的时长,该设置控制是否进行主从切换 + ```sh + sentinel down-after-milliseconds + ``` - ```sh - sentinel down-after-milliseconds master_name million_seconds - ``` +* 出现故障后,故障切换的最大超时时间,超过该值,认定切换失败,默认 3 分钟 - * 出现故障后,故障切换的最大超时时间,超过该值,认定切换失败,默认 3 分钟 + ```sh + sentinel failover-timeout + ``` - ```sh - sentinel failover-timeout master_name million_seconds - ``` +* 故障转移时,同时进行主从同步的 slave 数量,数值越大,要求网络资源越高 - * 指定同时进行主从的 slave 数量,数值越大,要求网络资源越高 + ```sh + sentinel parallel-syncs + ``` - ```sh - sentinel parallel-syncs master_name sync_slave_number - ``` +启动哨兵:服务端命令(Linux 命令) -启动哨兵: +```sh +redis-sentinel filename +``` -* 服务端命令(Linux 命令): - ```sh - redis-sentinel filename - ``` +*** -*** +#### 初始化 +Sentinel 本质上只是一个运行在特殊模式下的 Redis 服务器,当一个 Sentinel 启动时,首先初始化 Redis 服务器,但是初始化过程和普通 Redis 服务器的初始化过程并不完全相同,哨兵**不提供数据相关服务**,所以不会载入 RDB、AOF 文件 -### 工作原理 +整体流程: -#### 监控阶段 +* 初始化服务器 +* 将普通 Redis 服务器使用的代码替换成 Sentinel 专用代码 -哨兵在进行主从切换过程中经历三个阶段 +* 初始化 Sentinel 状态 +* 根据给定的配置文件,初始化 Sentinel 的监视主服务器列表 +* 创建连向主服务器的网络连接 -- 监控 -- 通知 -- 故障转移 -监控阶段作用:同步各个节点的状态信息 -* 获取各个 sentinel 的状态(是否在线) +*** -- 获取 master 的状态 - ```markdown - master属性 - prunid - prole:master - 各个slave的详细信息 - ``` +#### 代码替换 -- 获取所有 slave 的状态(根据 master 中的 slave 信息) +将一部分普通 Redis服务器使用的代码替换成 Sentinel 专用代码 - ```markdown - slave属性 - prunid - prole:slave - pmaster_host、master_port - poffset - ``` +Redis 服务器端口: -内部的工作原理: +```c +# define REDIS_SERVERPORT 6379 // 普通服务器端口 +# define REDIS_SENTINEL_PORT 26379 // 哨兵端口 +``` -sentinel 1 首先连接 master,建立 cmd 通道,根据主节点访问从节点,连接完成 +服务器的命令表: -sentinel 2 首先连接 master,然后通过 master 中的 sentinels 发现其他哨兵,然后寻找哨兵建立连接,哨兵之间同步数据 +```c +// 普通 Redis 服务器 +struct redisCommand redisCommandTable[] = { + {"get", getCommand, 2, "r", 0, NULL, 1, 1, 1, 0, 0}, + {"set", setCommand, -3, "wm", 0, noPreloadGetKeys, 1, 1, 1, 0, 0}, + //.... +} +// 哨兵 +struct redisCommand sentinelcmds[] = { + {"ping", pingCommand, 1, "", 0, NULL, 0, 0, 0, 0, 0}, + {"sentinel", sentinelCommand, -2,"",0,NULL,0,0,0,0,0}, + {"subscribe",...}, {"unsubscribe",...O}, {"psubscribe",...}, {"punsubscribe",...}, + {"info",...} +}; +``` - +上述表是哨兵模式下客户端可以执行的命令,所以对于 GET、SET 等命令,服务器根本就没有载入 @@ -13496,11 +13553,33 @@ sentinel 2 首先连接 master,然后通过 master 中的 sentinels 发现其 -#### 通知阶段 +#### 哨兵状态 -sentinel 在通知阶段不断的去获取 master/slave 的信息,然后在各个 sentinel 之间进行共享,流程如下: +服务器会初始化一个 sentinelState 结构,又叫 Sentinel 状态,结构保存了服务器中所有和 Sentinel 功能有关的状态(服务器的一般状态仍然由 redisServer 结构保存) -![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-哨兵模式通知工作流程.png) +```c +struct sentinelState { + // 当前纪元,用于实现故障转移 + uint64_t current_epoch; + + // 保存了所有被这个sentinel监视的主服务器 + dict *masters; + + // 是否进入了 TILT 模式 + int tilt; + // 进入 TILT 模式的时间 + mstime_t tilt_start_time; + + // 最后一次执行时间处理的事件 + mstime_t previous_time; + + // 目前正在执行的脚本数量 + int running_scripts; + // 一个FIFO队列,包含了所有需要执行的用户脚本 + list *scripts_queue; + +} sentinel; +``` @@ -13508,91 +13587,1127 @@ sentinel 在通知阶段不断的去获取 master/slave 的信息,然后在各 -#### 故障转移 - -当 master 宕机后,sentinel 会判断出 master 是否真的宕机,具体的操作流程: - -* 检测 master +#### 监控列表 - sentinel1 检测到 master 下线后会做 flag:SRI_S_DOWN 标志,此时 master 的状态是主观下线,并通知其他哨兵,其他哨兵也会尝试与 master 连接,如果大于 (n/2) + 1 个sentinel 检测到 master 下线,就达成共识更改 flag,此时 master 的状态是客观下线 +Sentinel 状态的初始化将 masters 字典的初始化,根据被载入的 Sentinel 配置文件 conf 来进行属性赋值 - ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-哨兵模式故障转移工作流程1.png) +Sentinel 状态中的 masters 字典记录了所有被 Sentinel 监视的**主服务器的相关信息**,字典的键是被监视主服务器的名字,值是主服务器对应的实例结构 -* 当 sentinel 认定 master 下线之后,此时需要决定更换 master,选举某个 sentinel 处理事故 +实例结构是一个 sentinelRedisinstance 数据类型,代表被 Sentinel 监视的实例,这个实例可以是主、从服务器,或者其他 Sentinel - 在选举的时候每一个 sentinel 都有一票,于是每个 sentinel 都会发出一个指令,在内网广播要做主持人;比如 sentinel1 和 sentinel4 发出这个选举指令了,那么 sentinel2 既能接到 sentinel1 的也能接到 sentinel4 的,sentinel2 会把一票投给其中一方,投给指令最先到达的 sentinel。选举最终得票多的,就成为了处理事故的哨兵,需要注意在这个过程中有可能会存在失败的现象,就是一轮选举完没有选取,那就会接着进行第二轮第三轮直到完成选举。 +```c +typedef struct sentinelRedisinstance { + // 标识值,记录了实例的类型,以及该实例的当前状态 + int flags; + + // 实例的名字,主服务器的名字由用户在配置文件中设置, + // 从服务器和哨兵的名字由 Sentinel 自动设置,格式为 ip:port,例如 127.0.0.1:6379 + char *name; + + // 实例运行的 ID + char *runid; + + // 配置纪元,用于实现故障转移 + uint64_t config_epoch; + + // 实例地址 + sentinelAddr *addr; + + // 如果当前实例时主服务器,该字段保存从服务器信息,键是名字格式为 ip:port,值是实例结构 + dict *slaves; + + // 所有监视当前服务器的 Sentinel 实例,键是名字格式为 ip:port,值是实例结构 + dict *sentinels; + + // sentinel down-after-milliseconds 的值,表示实例无响应多少毫秒后会被判断为主观下线(subjectively down) + mstime_t down_after_period; + + // sentinel monitor 选项中的quorum参数,判断这个实例为客观下线(objectively down)所需的支持投票数量 + int quorum; + + // sentinel parallel-syncs 的值,在执行故障转移操作时,可以同时对新的主服务器进行同步的从服务器数量 + int parallel-syncs; + + // sentinel failover-timeout的值,刷新故障迁移状态的最大时限 + mstime_t failover_timeout; +} +``` - ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-哨兵模式故障转移工作流程2.png) +addr 属性是一个指向 sentinelAddr 的指针: -选择新的 master,在服务器列表中挑选备选 master 的原则: +```c +typedef struct sentinelAddr { + char *ip; + int port; +} +``` -- 不在线的 OUT -- 响应慢的 OUT -- 与原 master 断开时间久的 OUT -- 优先原则:先根据优先级 → offset → runid +*** -选出新的 master之后,发送指令(sentinel )给其他的 slave -* 向新的 master 发送 slaveof no one -* 向其他 slave 发送 slaveof 新 master IP 端口 +#### 网络连接 +初始化 Sentinel 的最后一步是创建连向被监视主服务器的网络连接,Sentinel 将成为主服务器的客户端,可以向主服务器发送命令,并从命令回复中获取相关的信息 +每个被 Sentinel 监视的主服务器,Sentinel 会创建两个连向主服务器的**异步网络连接**: +* 命令连接:用于向主服务器发送命令,并接收命令回复 +* 订阅连接:用于订阅主服务器的 `_sentinel_:hello` 频道 -**** +建立两个连接的原因: +* 在 Redis 目前的发布与订阅功能中,被发送的信息都不会保存在 Redis 服务器里, 如果在信息发送时接收信息的客户端离线或断线,那么这个客户端就会丢失这条信息,为了不丢失 hello 频道的任何信息,Sentinel 必须用一个订阅连接来接收该频道的信息 +* Sentinel 还必须向主服务器发送命令,以此来与主服务器进行通信,所以 Sentinel 还必须向主服务器创建命令连接 -## 集群模式 +说明:断线的意思就是网络连接断开 -### 集群概述 +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-哨兵系统建立连接.png) -集群就是使用网络将若干台计算机联通起来,并提供统一的管理方式,使其对外呈现单机的服务效果 -![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-集群图示.png) -**集群作用:** +*** -- 分散单台服务器的访问压力,实现负载均衡 -- 分散单台服务器的存储压力,实现可扩展性 -- 降低单台服务器宕机带来的业务灾难 +### 信息交互 -*** +#### 获取信息 +##### 主服务器 +Sentinel 默认会以每十秒一次的频率,通过命令连接向被监视的主服务器发送 INFO 命令,来获取主服务器的当前信息 -### 结构设计 +* 一部分是主服务器本身的信息,包括 runid 域记录的服务器运行 ID,以及 role 域记录的服务器角色 +* 另一部分是服务器属下所有从服务器的信息,每个从服务器都由一个 slave 字符串开头的行记录,根据这些 IP 地址和端口号,Sentinel 无须用户提供从服务器的地址信息,就可以自动发现从服务器 -**数据存储设计:** +```sh +# Server +run_id:76llc59dc3a29aa6fa0609f84lbb6al019008a9c +... +# Replication +role:master +... +slave0: ip=l27.0.0.1, port=11111, state=online, offset=22, lag=0 +slave1: ip=l27.0.0.1, port=22222, state=online, offset=22, lag=0 +... +``` -1. 通过算法设计,计算出 key 应该保存的位置(类似哈希寻址) +根据 run_id 和 role 记录的信息 Sentinel 将对主服务器的实例结构进行更新,比如主服务器重启之后,运行 ID 就会和实例结构之前保存的运行 ID 不同,哨兵检测到这一情况之后就会对实例结构的运行 ID 进行更新 - ```markdown - key -> CRC16(key) -> 值 -> %16384 -> 存储位置 - ``` +对于主服务器返回的从服务器信息,用实例结构的 slaves 字典记录了从服务器的信息: -2. 将所有的存储空间计划切割成 16384 份,每台主机保存一部分 +* 如果从服务器对应的实例结构已经存在,那么 Sentinel 对从服务器的实例结构进行更新 +* 如果不存在,为这个从服务器新创建一个实例结构加入字典,字典键为 `ip:port` - 注意:每份代表的是一个存储空间,不是一个 key 的保存空间,可以存储多个 key -3. 将 key 按照计算出的结果放到对应的存储空间 - +*** -查找数据: -- 各个数据库相互通信,保存各个库中槽的编号数据 -- 一次命中,直接返回 -- 一次未命中,告知具体位置,最多两次命中 -设置数据:系统默认存储到某一个 +##### 从服务器 + +当 Sentinel 发现主服务器有新的从服务器出现时,会为这个新的从服务器创建相应的实例结构, 还会创建到从服务器的命令连接和订阅连接,所以 Sentinel 对所有的从服务器之间都可以进行命令操作 + +Sentinel 默认会以每十秒一次的频率,向从服务器发送 INFO 命令: + +```sh +# Server +run_id:76llc59dc3a29aa6fa0609f84lbb6al019008a9c #从服务器的运行 id +... +# Replication +role:slave # 从服务器角色 +... +master_host:127.0.0.1 # 主服务器的 ip +master_port:6379 # 主服务器的 port +master_link_status:up # 主从服务器的连接状态 +slave_repl_offset:11111 # 从服务器的复制偏移蜇 +slave_priority:100 # 从服务器的优先级 +... +``` + +* **优先级属性**在故障转移时会用到 + +根据这些信息,Sentinel 会对从服务器的实例结构进行更新 + + + + + +*** + + + +#### 发送信息 + +Sentinel 在默认情况下,会以每两秒一次的频率,通过命令连接向所有被监视的主服务器和从服务器发送以下格式的命令: + +```sh +PUBLISH _sentinel_:hello ", , , , , , , +``` + +这条命令向服务器的 `_sentinel_:hello` 频道发送了一条信息,信息的内容由多个参数组成: + +* 以 s_ 开头的参数记录的是 Sentinel 本身的信息 +* 以 m_ 开头的参数记录的则是主服务器的信息 + +说明:**通过命令连接发送的频道信息** + + + +*** + + + +#### 接受信息 + +##### 订阅频道 + +Sentinel 与一个主或从服务器建立起订阅连接之后,就会通过订阅连接向服务器发送订阅命令,频道的订阅会一直持续到 Sentinel 与服务器的连接断开为止 + +```sh +SUBSCRIBE _sentinel_:hello +``` + +订阅成功后,Sentinel 就可以通过订阅连接从服务器的 `_sentinel_:hello` 频道接收信息,对消息分析: + +* 如果信息中记录的 Sentinel 运行 ID 与自己的相同,不做进一步处理 +* 如果不同,将根据信息中的各个参数,对相应主服务器的实例结构进行更新 + +对于监视同一个服务器的多个 Sentinel 来说,**一个 Sentinel 发送的信息会被其他 Sentinel 接收到**,这些信息会被用于更新其他 Sentinel 对发送信息 Sentinel 的认知,也会被用于更新其他 Sentinel 对被监视的服务器的认知 + + + +*** + + + +##### 更新字典 + +Sentinel 为主服务器创建的实例结构的 sentinels 字典保存所有同样监视这个**主服务器的 Sentinel 信息**(包括 Sentinel 自己),字典的键是 Sentinel 的名字,格式为 `ip:port`,值是键所对应 Sentinel 的实例结构 + +当一个 Sentinel 接收到其他 Sentinel 发来的信息时(发送信息为源 Sentinel,接收信息的为目标 Sentinel),目标 Sentinel 会分析提取出 Sentinel 相关的参数和主服务器相关的参数。根据主服务器参数,目标 Sentinel 会在自己的 Sentinel 状态 sentinelState.masters 中查找相应的主服务器实例结构,检查主服务器实例结构的 sentinels 字典中, 源 Sentinel 的实例结构是否存在 + +* 如果源 Sentinel 的实例结构存在,那么对源 Sentinel 的实例结构进行更新 +* 如果源 Sentinel 的实例结构不存在,说明源 Sentinel 是刚开始监视主服务器,目标 Sentinel 会为源 Sentinel 创建一个新的实例结构,并将这个结构添加到 sentinels 字典里面 + +因为 Sentinel 可以接收到的频道信息来获知其他 Sentinel 的存在,并通过发送频道信息来让其他 Sentinel 知道自己的存在,所以用户在使用 Sentinel 时并不需要提供各个 Sentinel 的地址信息,**监视同一个主服务器的多个 Sentinel 可以自动发现对方** + + + +*** + + + +##### 命令连接 + +Sentinel 通过频道信息发现新的 Sentinel,除了创建实例结构,还会创建一个连向新 Sentinel 的命令连接,而新 Sentinel 也同样会创建连向这个 Sentinel 的命令连接,最终监视同一主服务器的多个 Sentinel 将形成相互连接的网络 + +作用:**通过命令连接相连的各个 Sentinel** 可以向其他 Sentinel 发送命令请求来进行信息交换 + +Sentinel 之间不会创建订阅连接: + +* Sentinel 需要通过接收主服务器或者从服务器发来的频道信息来发现未知的新 Sentinel,所以才创建订阅连接 +* 相互已知的 Sentinel 只要使用命令连接来进行通信就足够了 + + + + + +*** + + + +### 下线检测 + +#### 主观下线 + +Sentinel 在默认情况下会以每秒一次的频率向所有与它创建了命令连接的实例(包括主从服务器、其他 Sentinel)发送 PING 命令,通过实例返回的 PING 命令回复来判断实例是否在线 + +* 有效回复:实例返回 +PONG、-LOADING、-MASTERDOWN 三种回复的其中一种 +* 无效回复:实例返回除上述三种以外的任何数据 + +Sentinel 配置文件中 down-after-milliseconds 选项指定了判断实例进入主观下线所需的时长,如果主服务器在该时间内一直向 Sentinel 返回无效回复,Sentinel 就会在该服务器对应实例结构的 flags 属性打开 SRI_S_DOWN 标识,表示该主服务器进入主观下线状态 + +配置的 down-after-milliseconds 值不仅适用于主服务器,还会被用于当前 Sentinel 判断主服务器属下的所有从服务器,以及所有同样监视这个主服务器的其他 Sentinel 的主观下线状态 + +注意:对于监视同一个主服务器的多个 Sentinel 来说,设置的 down-after-milliseconds 选项的值可能不同,所以当一个 Sentinel 将主服务器判断为主观下线时,其他 Sentinel 可能仍然会认为主服务器处于在线状态 + + + +*** + + + +#### 客观下线 + +当 Sentinel 将一个主服务器判断为主观下线之后,会向同样监视这一主服务器的其他 Sentinel 进行询问 + +Sentinel 使用命令询问其他 Sentinel 是否同意主服务器已下线: + +```sh +SENTINEL is-master-down-by-addr +``` + +* ip:被 Sentinel 判断为主观下线的主服务器的 IP 地址 +* port:被 Sentinel 判断为主观下线的主服务器的端口号 +* current_epoch:Sentinel 当前的配置纪元,用于选举领头 Sentinel +* runid:取值为 * 符号代表命令仅仅用于检测主服务器的客观下线状态;取值为 Sentinel 的运行 ID 则用于选举领头 Sentinel + +目标 Sentinel 接收到源 Sentinel 的命令时,会根据参数的 lP 和端口号,检查主服务器是否已下线,然后返回一条包含三个参数的 Multi Bulk 回复: + +* down_state:返回目标 Sentinel 对服务器的检查结果,1 代表主服务器已下线,0 代表未下线 +* leader_runid:取值为 * 符号代表命令仅用于检测服务器的下线状态;而局部领头 Sentinel 的运行 ID 则用于选举领头 Sentinel +* leader_epoch:目标 Sentinel 的局部领头 Sentinel 的配置纪元 + +源 Sentinel 将统计其他 Sentinel 同意主服务器已下线的数量,当这一数量达到配置指定的判断客观下线所需的数量(quorum)时,Sentinel 会将主服务器对应实例结构 flags 属性的 SRI_O_DOWN 标识打开,代表客观下线,并对主服务器执行故障转移操作 + +注意:不同 Sentinel 判断客观下线的条件可能不同,因为载入的配置文件中的属性(quorum)可能不同 + + + +*** + + + +### 领头选举 + +主服务器被判断为客观下线时,监视这个主服务器的各个 Sentinel 会进行协商,选举出一个领头 Sentinel 对下线服务器执行故障转移 + +Redis 选举领头 Sentinel 的规则: + +* 所有在线的 Sentinel 都有被选为领头 Sentinel 的资格 +* 每个发现主服务器进入客观下线的 Sentinel 都会要求其他 Sentinel 将自己设置为局部领头 Sentinel + +* 在一个配置纪元里,所有 Sentinel 都只有一次将某个 Sentinel 设置为局部领头 Sentinel 的机会,并且局部领头一旦设置,在这个配置纪元里就不能再更改 +* Sentinel 设置局部领头 Sentinel 的规则是先到先得,最先向目标 Sentinel 发送设置要求的源 Sentinel 将成为目标 Sentinel 的局部领头 Sentinel,之后接收到的所有设置要求都会被目标 Sentinel 拒绝 +* 领头 Sentinel 的产生需要半数以上 Sentinel 的支持,并且每个 Sentinel 只有一票,所以一个配置纪元只会出现一个领头 Sentinel,比如 10 个 Sentinel 的系统中,至少需要 `10/2 + 1 = 6` 票 + +选举过程: + +* 一个 Sentinel 向目标 Sentinel 发送 `SENTINEL is-master-down-by-addr` 命令,命令中的 runid 参数不是*符号而是源 Sentinel 的运行 ID,表示源 Sentinel 要求目标 Sentinel 将自己设置为它的局部领头 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 的配置纪元(configuration epoch)都要自增一次 + +Sentinel 集群至少 3 个节点的原因: + +* 如果 Sentinel 集群只有 2 个 Sentinel 节点,则领头选举需要 `2/2 + 1 = 2` 票,如果一个节点挂了,那就永远选不出领头 +* Sentinel 集群允许 1 个 Sentinel 节点故障则需要 3 个节点的集群,允许 2 个节点故障则需要 5 个节点集群 + + + + + +*** + + + +### 故障转移 + +#### 执行流程 + +领头 Sentinel 将对已下线的主服务器执行故障转移操作,该操作包含以下三个步骤 + +* 从下线主服务器属下的所有从服务器里面,挑选出一个从服务器,执行 `SLAVEOF no one`,将从服务器升级为主服务器 + + 在发送 SLAVEOF no one 命令后,领头 Sentinel 会以**每秒一次的频率**(一般是 10s/次)向被升级的从服务器发送 INFO 命令,观察命令回复中的角色信息,当被升级服务器的 role 从 slave 变为 master 时,说明从服务器已经顺利升级为主服务器 + +* 将已下线的主服务器的所有从服务器改为复制新的主服务器,通过向从服务器发送 SLAVEOF 命令实现 + +* 将已经下线的主服务器设置为新的主服务器的从服务器,设置是保存在服务器对应的实例结构中,当旧的主服务器重新上线时,Sentinel 就会向它发送 SLAVEOF 命令,成为新的主服务器的从服务器 + +示例:sever1 是主,sever2、sever3、sever4 是从服务器,sever1 故障后选中 sever2 升级 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-哨兵执行故障转移.png) + + + + + +*** + + + +#### 选择算法 + +领头 Sentinel 会将已下线主服务器的所有从服务器保存到一个列表里,然后按照以下规则对列表进行过滤,最后挑选出一个**状态良好、数据完整**的从服务器 + +* 删除列表中所有处于下线或者断线状态的从服务器,保证列表中的从服务器都是正常在线的 + +* 删除列表中所有最近五秒内没有回复过领头 Sentinel 的 INFO 命令的从服务器,保证列表中的从服务器最近成功进行过通信 + +* 删除所有与已下线主服务器连接断开超过 `down-after-milliseconds * 10` 毫秒的从服务器,保证列表中剩余的从服务器都没有过早地与主服务器断开连接,保存的数据都是比较新的 + + down-after-milliseconds 时间用来判断是否主观下线,其余的时间完全可以完成客观下线和领头选举 + +* 根据从服务器的优先级,对列表中剩余的从服务器进行排序,并选出其中**优先级最高**的从服务器 + +* 如果有多个具有相同最高优先级的从服务器,领头 Sentinel 将对这些相同优先级的服务器按照复制偏移量进行排序,选出其中偏移量最大的从服务器,也就是保存着最新数据的从服务器 + +* 如果还没选出来,就按照运行 ID 对这些从服务器进行排序,并选出其中运行 ID 最小的从服务器 + + + + + +**** + + + + + +## 集群模式 + +### 集群节点 + +#### 节点概述 + +Redis 集群是 Redis 提供的分布式数据库方案,集群通过分片(sharding)来进行数据共享, 并提供复制和故障转移功能,一个 Redis 集群通常由多个节点(node)组成,将各个独立的节点连接起来,构成一个包含多节点的集群 + +一个节点就是一个**运行在集群模式下的 Redis 服务器**,Redis 在启动时会根据配置文件中的 `cluster-enabled` 配置选项是否为 yes 来决定是否开启服务器的集群模式 + +节点会继续使用所有在单机模式中使用的服务器组件,使用 redisServer 结构来保存服务器的状态,使用 redisClient 结构来保存客户端的状态,也有集群特有的数据结构 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-集群模式.png) + + + + + +*** + + + +#### 数据结构 + +每个节点都保存着一个集群状态 clusterState 结构,这个结构记录了在当前节点的视角下,集群目前所处的状态 + +```c +typedef struct clusterState { + // 指向当前节点的指针 + clusterNode *myself; + + // 集群当前的配置纪元,用于实现故障转移 + uint64_t currentEpoch; + + // 集群当前的状态,是在线还是下线 + int state; + + // 集群中至少处理着一个槽的节点的数量,为0表示集群目前没有任何节点在处理槽 + int size; + + // 集群节点名单(包括 myself 节点),字典的键为节点的名字,字典的值为节点对应的clusterNode结构 + dict *nodes; +} +``` + +每个节点都会使用 clusterNode 结构记录当前状态,并为集群中的所有其他节点(包括主节点和从节点)都创建一个相应的 clusterNode 结构,以此来记录其他节点的状态 + +```c +struct clusterNode { + // 创建节点的时间 + mstime_t ctime; + + // 节点的名字,由 40 个十六进制字符组成 + char name[REDIS_CLUSTER_NAMELEN]; + + // 节点标识,使用各种不同的标识值记录节点的角色(比如主节点或者从节点)以及节点目前所处的状态(比如在线或者下线) + int flags; + + // 节点当前的配置纪元,用于实现故障转移 + uint64_t configEpoch; + + // 节点的IP地址 + char ip[REDIS_IP_STR_LEN]; + + // 节点的端口号 + int port; + + // 保存连接节点所需的有关信息 + clusterLink *link; +} +``` + +clusterNode 结构的 link 属性是一个 clusterLink 结构,该结构保存了连接节点所需的有关信息 + +```c +typedef struct clusterLink { + // 连接的创建时间 + mstime_t ctime; + + // TCP套接字描述符 + int fd; + + // 输出缓冲区,保存着等待发送给其他节点的消息(message)。 + sds sndbuf; + + // 输入缓冲区,保存着从其他节点接收到的消息。 + sds rcvbuf; + + // 与这个连接相关联的节点,如果没有的话就为NULL + struct clusterNode *node; +} +``` + +* redisClient 结构中的套接宇和缓冲区是用于连接客户端的 +* clusterLink结构中的套接宇和缓冲区则是用于连接节点的 + + + +**** + + + +#### MEET + +CLUSTER MEET 命令用来将 ip 和 port 所指定的节点添加到接受命令的节点所在的集群中 + +```sh +CLUSTER MEET +``` + +假设向节点 A 发送 CLUSTER MEET 命令,让节点 A 将另一个节点 B 添加到节点 A 当前所在的集群里,收到命令的节点 A 将与根据 ip 和 port 向节点 B 进行握手(handshake): + +* 节点 A 会为节点 B 创建一个 clusterNode 结构,并将该结构添加到自己的 clusterState.nodes 字典里,然后节点 A 向节点 B **发送 MEET 消息**(message) +* 节点 B 收到 MEET 消息后,节点 B 会为节点 A 创建一个 clusterNode 结构,并将该结构添加到自己的 clusterState.nodes 字典里,之后节点 B 将向节点 A **返回一条 PONG 消息** +* 节点 A 收到 PONG 消息后,代表节点 A 可以知道节点 B 已经成功地接收到了自已发送的 MEET 消息,此时节点 A 将向节点 B **返回一条 PING 消息** +* 节点 B 收到 PING 消息后, 代表节点 B 可以知道节点 A 已经成功地接收到了自己返回的 PONG 消息,握手完成 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-集群节点握手.png) + +节点 A 会将节点 B 的信息通过 Gossip 协议传播给集群中的其他节点,让其他节点也与节点 B 进行握手,最终经过一段时间之后,节点 B 会被集群中的所有节点认识 + + + + + +*** + + + +### 槽指派 + +#### 基本操作 + +Redis 集群通过分片的方式来保存数据库中的键值对,集群的整个数据库被分为16384 个槽(slot),数据库中的每个键都属于 16384 个槽中的一个,集群中的每个节点可以处理 0 个或最多 16384 个槽(**每个主节点存储的数据并不一样**) + +* 当数据库中的 16384 个槽都有节点在处理时,集群处于上线状态(ok) +* 如果数据库中有任何一个槽得到处理,那么集群处于下线状态(fail) + +通过向节点发送 CLUSTER ADDSLOTS 命令,可以将一个或多个槽指派(assign)给节点负责 + +```sh +CLUSTER ADDSLOTS [slot ... ] +``` + +```sh +127.0.0.1:7000> CLUSTER ADDSLOTS 0 1 2 3 4 ... 5000 # 将槽0至槽5000指派给节点7000负责 +OK +``` + +命令执行细节: + +* 如果命令参数中有一个槽已经被指派给了某个节点,那么会向客户端返回错误,并终止命令执行 +* 将 slots 数组中的索引 i 上的二进制位设置为 1,就代表指派成功 + + + + + +*** + + + +#### 节点指派 + +clusterNode 结构的 slots 属性和 numslot 属性记录了节点负责处理哪些槽: + +```c +struct clusterNode { + // 处理信息,一字节等于 8 位 + unsigned char slots[l6384/8]; + // 记录节点负责处理的槽的数量,就是 slots 数组中值为 1 的二进制位数量 + int numslots; +} +``` + +slots 是一个二进制位数组(bit array),长度为 `16384/8 = 2048` 个字节,包含 16384 个二进制位,Redis 以 0 为起始索引,16383 为终止索引,对 slots 数组的 16384 个二进制位进行编号,并根据索引 i 上的二进制位的值来判断节点是否负责处理槽 i: + +* 在索引 i 上的二进制位的值为 1,那么表示节点负责处理槽 i +* 在索引 i 上的二进制位的值为 0,那么表示节点不负责处理槽 i + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-集群槽指派信息.png) + +取出和设置 slots 数组中的任意一个二进制位的值的**复杂度仅为 O(1)**,所以对于一个给定节点的 slots 数组来说,检查节点是否负责处理某个槽或者将某个槽指派给节点负责,这两个动作的复杂度都是 O(1) + +**传播节点的槽指派信息**:一个节点除了会将自己负责处理的槽记录在 clusterNode 中,还会将自己的 slots 数组通过消息发送给集群中的其他节点,每个接收到 slots 数组的节点都会将数组保存到相应节点的 clusterNode 结构里面,因此集群中的**每个节点**都会知道数据库中的 16384 个槽分别被指派给了集群中的哪些节点 + + + + + +*** + + + +#### 集群指派 + +集群状态 clusterState 结构中的 slots 数组记录了集群中所有 16384 个槽的指派信息,数组每一项都是一个指向 clusterNode 的指针 + +```c +typedef struct clusterState { + // ... + clusterNode *slots[16384]; +} +``` + +* 如果 slots[i] 指针指向 NULL,那么表示槽 i 尚未指派给任何节点 +* 如果 slots[i] 指针指向一个 clusterNode 结构,那么表示槽 i 已经指派给该节点所代表的节点 + +通过该节点,程序检查槽 i 是否已经被指派或者取得负责处理槽 i 的节点,只需要访问 clusterState. slots[i] 即可,时间复杂度仅为 O(1) + + + + + +*** + + + +### 集群命令 + +#### 执行命令 + +集群处于上线状态,客户端就可以向集群中的节点发送命令(16384 个槽全部指派就进入上线状态) + +当客户端向节点发送与数据库键有关的命令时,接收命令的节点会计算出命令该键属于哪个槽,并检查这个槽是否指派给了自己 + +* 如果键所在的槽正好就指派给了当前节点,那么节点直接执行这个命令 +* 反之,节点会向客户端返回一个 MOVED 错误,指引客户端转向(redirect)至正确的节点,再次发送该命令 + +计算键归属哪个槽的**寻址算法**: + +```c +def slot_number(key): // CRC16(key) 语句计算键 key 的 CRC-16 校验和 + return CRC16(key) & 16383; // 取模,十进制对16384的取余 +``` + +使用 `CLUSTER KEYSLOT ` 命令可以查看一个给定键属于哪个槽,底层实现: + +```c +def CLUSTER_KEYSLOT(key): + // 计算槽号 + slot = slot_number(key); + // 将槽号返回给客户端 + reply_client(slot); +``` + +判断槽是否由当前节点负责处理: + +* 如果 clusterState.slots[i] 等于 clusterState.myself,那么说明槽 i 由当前节点负责,节点可以执行客户端发送的命令 +* 如果 clusterState.slots[i] 不等于 clusterState.myself,那么说明槽 i 并非由当前节点负责,节点会根据 clusterState.slots[i] 指向的clusterNode 结构所记录的节点 IP 和端口号,向客户端返回 MOVED 错误 + + + +*** + + + +#### MOVED + +MOVED 错误的格式为: + +```sh +MOVED : SET msg "happy" +-> Redirected to slot [6257] located at 127.0.0.1:6380 +OK + +127.0.0.1:6379> +``` + +使用单机(stand alone)模式的 redis-cli 会打印错误,因为单机模式客户端不清楚 MOVED 错误的作用,不会进行自动转向: + +```sh +$ redis-cli -c -p 6379 #集群模式 +127.0.0.1:6379> SET msg "happy" +(error) MOVED 6257 127.0.0.1:6380 + +127.0.0.1:6379> +``` + + + + + +*** + + + +### 集群数据 + +集群节点保存键值对以及键值对过期时间的方式,与单机 Redis 服务器保存键值对以及键值对过期时间的方式完全相同,但是集群节点只能使用 0 号数据库,单机服务器可以任意使用 + +除了将键值对保存在数据库里面之外,节点还会用 clusterState 结构中的 slots_to_keys 跳跃表来保存**槽和键**之间的关系 + +```c +typedef struct clusterState { + // ... + zskiplist *slots_to_keys; +} +``` + +slots_to_keys 跳跃表每个节点的分值(score)都是一个槽号,而每个节点的成员(member)都是一个数据库键(按槽号升序) + +* 当节点往数据库中添加一个新的键值对时,节点就会将这个键以及键的槽号关联到 slots_to_keys 跳跃表 +* 当节点删除数据库中的某个键值对时,节点就会在 slots_to_keys 跳跃表解除被删除键与槽号的关联 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-槽和键跳跃表.png) + +通过在 slots_to_keys 跳跃表中记录各个数据库键所属的槽,可以很方便地对属于某个或某些槽的所有数据库键进行批量操作,比如 `CLUSTER GETKEYSINSLOT ` 命令返回最多 count 个属于槽 slot 的数据库键,就是通过该跳表实现 + + + + + +*** + + + +### 重新分片 + +#### 实现原理 + +Redis 集群的重新分片操作可以将任意数量已经指派给某个节点(源节点)的槽改为指派给另一个节点(目标节点),并且相关槽的键值对也会从源节点被移动到目标节点,该操作是可以在线(online)进行,在重新分片的过程中源节点和目标节点都可以处理命令请求 + +Redis 的集群管理软件 redis-trib 负责执行重新分片操作,redis-trib 通过向源节点和目标节点发送命令来进行重新分片操作 + +* redis-trib 向目标节点发送 `CLUSTER SETSLOT IMPORTING ` 命令,让目标节点准备好从源节点导入属于槽 slot 的键值对 +* redis-trib 向源节点发送 `CLUSTER SETSLOT MIGRATING ` 命令,让源节点准备好将属于槽 slot 的键值对迁移至目标节点 +* redis-trib 向源节点发送 `CLUSTER GETKEYSINSLOT ` 命令,获得最多 count 个属于槽 slot 的键值对的键名 +* 对于每个 key,redis-trib 都向源节点发送一个 `MIGRATE 0 NODE ` 命令,将槽 slot 指派给目标节点,这一指派信息会通过消息传播至整个集群,最终集群中的所有节点都直到槽 slot 已经指派给了目标节点 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-集群重新分片.png) + +如果重新分片涉及多个槽,那么 redis-trib 将对每个给定的槽分别执行上面给出的步骤 + + + + + +*** + + + +#### 命令原理 + +clusterState 结构的 importing_slots_from 数组记录了当前节点正在从其他节点导入的槽,migrating_slots_to 数组记录了当前节点正在迁移至其他节点的槽: + +```c +typedef struct clusterState { + // 如果 importing_slots_from[i] 的值不为 NULL,而是指向一个 clusterNode 结构, + // 那么表示当前节点正在从 clusterNode 所代表的节点导入槽 i + clusterNode *importing_slots_from[16384]; + + // 表示当前节点正在将槽 i 迁移至 clusterNode 所代表的节点 + clusterNode *migrating_slots_to[16384]; +} +``` + +`CLUSTER SETSLOT IMPORTING ` 命令:将目标节点 `clusterState.importing_slots_from[slot]` 的值设置为 source_id 所代表节点的 clusterNode 结构 + +`CLUSTER SETSLOT MIGRATING ` 命令:将源节点 `clusterState.migrating_slots_to[slot]` 的值设置为target_id 所代表节点的 clusterNode 结构 + + + +*** + + + +#### ASK 错误 + +重新分片期间,源节点向目标节点迁移一个槽的过程中,可能出现被迁移槽的一部分键值对保存在源节点,另一部分保存在目标节点 + +客户端向源节点发送命令请求,并且命令要处理的数据库键属于被迁移的槽: + +* 源节点会先在数据库里面查找指定键,如果找到的话,就直接执行客户端发送的命令 + +* 未找到会检查 clusterState.migrating_slots_to[slot],看键 key 所属的槽 slot 是否正在进行迁移 + +* 槽 slot 正在迁移则源节点将向客户端返回一个 ASK 错误,指引客户端转向正在导入槽的目标节点 + + ```sh + ASK + ``` + +* 接到 ASK 错误的客户端,会根据错误提供的 IP 地址和端口号转向目标节点,首先向目标节点发送一个 ASKING 命令,再重新发送原本想要执行的命令 + +和 MOVED 错误情况类似,集群模式的 redis-cli 在接到 ASK 错误时不会打印错误进行自动转向;单机模式的 redis-cli 会打印错误 + +对比 MOVED 错误: + +* MOVED 错误代表槽的负责权已经从一个节点转移到了另一个节点,转向是一种持久性的转向 + +* ASK 错误只是两个节点在迁移槽的过程中使用的一种临时措施,ASK 的转向不会对客户端今后发送关于槽 slot 的命令请求产生任何影响,客户端仍然会将槽 slot 的命令请求发送至目前负责处理槽 slot 的节点,除非 ASK 错误再次出现 + + + +*** + + + +#### ASKING + +客户端不发送 ASKING 命令,而是直接发送执行的命令,那么客户端发送的命令将被节点拒绝执行,并返回 MOVED 错误 + +ASKING 命令作用是打开发送该命令的客户端的 REDIS_ASKING 标识,该命令的伪代码实现: + +```c +def ASKING (): + // 打开标识 + client.flags |= REDIS_ASKING + // 向客户端返回OK回复 + reply("OK") +``` + +当前节点正在导入槽 slot,并且发送命令的客户端带有 REDIS_ASKING 标识,那么节点将破例执行这个关于槽 slot 的命令一次 + +客户端的 REDIS_ASKING 标识是一次性标识,当节点执行了一个带有 REDIS_ASKING 标识的客户端发送的命令之后,该客户端的 REDIS_ASKING 标识就会被移除 + + + + + +*** + + + +### 高可用 + +#### 节点复制 + +Redis 集群中的节点分为主节点(master)和从节点(slave),其中主节点用于处理槽,而从节点则用于复制主节点,并在被复制的主节点下线时,代替下线主节点继续处理命令请求 + +```sh +CLUSTER REPLICATE +``` + +向一个节点发送命令可以让接收命令的节点成为 node_id 所指定节点的从节点,并开始对主节点进行复制 + +* 接受命令的节点首先会在的 clusterState.nodes 字典中找到 node_id 所对应节点的 clusterNode 结构,并将自己的节点中的 clusterState.myself.slaveof 指针指向这个结构,记录这个节点正在复制的主节点 + +* 节点会修改 clusterState.myself.flags 中的属性,关闭 REDIS_NODE_MASTER 标识,打开 REDIS_NODE_SLAVE 标识 +* 节点会调用复制代码,对主节点进行复制(节点的复制功能和单机 Redis 服务器的使用了相同的代码) + +一个节点成为从节点,并开始复制某个主节点这一信息会通过消息发送给集群中的其他节点,最终集群中的所有节点都会知道某个从节点正在复制某个主节点 + +主节点的 clusterNode 结构的 slaves 属性和 numslaves 属性中记录正在复制这个主节点的从节点名单: + +```c +struct clusterNode { + // 正在复制这个主节点的从节点数量 + int numslaves; + + // 数组项指向一个正在复制这个主节点的从节点的clusterNode结构 + struct clusterNode **slaves; +} +``` + + + +*** + + + +#### 故障检测 + +集群中的每个节点都会定期地向集群中的其他节点发送 PING 消息,来检测对方是否在线,如果接收 PING 的节点没有在规定的时间内返回 PONG 消息,那么发送消息节点就会将接收节点标记为**疑似下线**(probable fail, PFAIL) + +集群中的节点会互相发送消息,来**交换集群中各个节点的状态信息**,当一个主节点 A 通过消息得知主节点 B 认为主节点 C 进入了疑似下线状态时,主节点 A 会在 clusterState.nodes 字典中找到主节点 C 所对应的节点,并将主节点 B 的下线报告(failure report)添加到 clusterNode.fail_reports 链表里面 + +```c +struct clusterNode { + // 一个链表,记录了所有其他节点对该节点的下线报告 + list *fail_reports; +} +// 每个下线报告由一个 clusterNodeFailReport 结构表示 +struct clusterNodeFailReport { + // 报告目标节点巳经下线的节点 + struct clusterNode *node; + + // 最后一次从node节点收到下线报告的时间 + // 程序使用这个时间戳来检查下线报告是否过期,与当前时间相差太久的下线报告会被删除 + mstime_t time; +}; +``` + +集群里**半数以上**负责处理槽的主节点都将某个主节点 X 报告为疑似下线,那么 X 将被标记为**已下线**(FAIL),将 X 标记为已下线的节点会向集群广播一条关于主节点 X 的 FAIL 消息,所有收到消息的节点都会将 X 标记为已下线 + + + +**** + + + +#### 故障转移 + +当一个从节点发现所属的主节点进入了已下线状态,从节点将开始对下线主节点进行故障转移,执行步骤: + +* 下属的从节点通过选举产生一个节点 +* 被选中的从节点会执行 `SLAVEOF no one` 命令,成为新的主节点 +* 新的主节点会撤销所有对已下线主节点的槽指派,并将这些槽全部指派给自己 +* 新的主节点向集群广播一条 PONG 消息,让集群中的其他节点知道当前节点变成了主节点,并且接管了下线节点负责处理的槽 +* 新的主节点开始接收有关的命令请求,故障转移完成 + + + +*** + + + +#### 选举算法 + +集群选举新的主节点的规则: + +* 集群的配置纪元是一个自增的计数器,初始值为 0 +* 当集群里某个节点开始一次故障转移,集群的配置纪元就是增加一 +* 每个配置纪元里,集群中每个主节点都有一次投票的机会,而第一个向主节点要求投票的从节点将获得该主节点的投票 +* 具有投票权的主节点是必须具有正在处理的槽 +* 集群里有 N 个具有投票权的主节点,那么当一个从节点收集到大于等于 `N/2+1` 张支持票时,从节点就会当选 +* 每个配置纪元里,具有投票权的主节点只能投一次票,所以获得一半以上票的节点只会有一个 + +选举流程: + +* 当某个从节点发现正在复制的主节点进入已下线状态时,会向集群广播一条 `CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST` 消息,要求所有收到这条消息、并且具有投票权的主节点向这个从节点投票 +* 如果主节点尚未投票给其他从节点,将向要求投票的从节点返回一条 `CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK` 消息,表示这个主节点支持从节点成为新的主节点 +* 如果从节点获取到了半数以上的选票,则会当选新的主节点 +* 如果一个配置纪元里没有从节点能收集到足够多的支待票,那么集群进入一个新的配置纪元,并再次进行选举,直到选出新的主节点 + + + +选举新主节点的方法和选举领头 Sentinel 的方法非常相似,两者都是基于 Raft 算法的领头选举(eader election)方法实现的 + + + + + +*** + + + +### 消息机制 + +#### 消息结构 + +集群中的各个节点通过发送和接收消息(message)来进行通信,将发送消息的节点称为发送者(sender),接收消息的节点称为接收者(receiver) + +节点发送的消息主要有: + +* MEET 消息:当发送者接到客户端发送的 CLUSTER MEET 命令时,会向接收者发送 MEET 消息,请求接收者加入到发送者当前所处的集群里 + +* PING 消息:集群里的每个节点默认每隔一秒钟就会从已知节点列表中随机选出五个,然后对这五个节点中最长时间没有发送过 PING 消息的节点发送 PING,以此来**随机检测**被选中的节点是否在线 + + 如果节点 A 最后一次收到节点 B 发送的 PONG 消息的时间,距离当前已经超过了节点 A 的 cluster-node­-timeout 设置时长的一半,那么 A 也会向 B 发送 PING 消息,防止 A 因为长时间没有随机选中 B 发送 PING,而导致对节点 B 的信息更新滞后 + +* PONG 消息:当接收者收到 MEET 消息或者 PING 消息时,为了让发送者确认已经成功接收消息,会向发送者返回一条 PONG;节点也可以通过向集群广播 PONG 消息来让集群中的其他节点立即刷新关于这个节点的认识(从升级为主) + +* FAIL 消息:当一个主节点 A 判断另一个主节点 B 已经进入 FAIL 状态时,节点 A 会向集群广播一条 B 节点的 FAIL 信息 + +* PUBLISH 消息:当节点接收到一个 PUBLISH 命令时,节点会执行这个命令并向集群广播一条 PUBLISH 消息,接收到 PUBLISH 消息的节点都会执行相同的 PUBLISH 命令 + + + + + +*** + + + +#### 消息头 + +节点发送的所有消息都由一个消息头包裹,消息头除了包含消息正文之外,还记录了消息发送者自身的一些信息 + +消息头: + +```c +typedef struct clusterMsg { + // 消息的长度(包括这个消息头的长度和消息正文的长度) + uint32_t totlen; + // 消息的类型 + uint16_t type; + // 消息正文包含的节点信息数量,只在发送MEET、PING、PONG这三种Gossip协议消息时使用 + uint16_t count; + + // 发送者所处的配置纪元 + uint64_t currentEpoch; + // 如果发送者是一个主节点,那么这里记录的是发送者的配置纪元 + // 如果发送者是一个从节点,那么这里记录的是发送者正在复制的主节点的配置纪元 + uint64_t configEpoch; + + // 发送者的名字(ID) + char sender[REDIS CLUSTER NAMELEN]; + // 发送者目前的槽指派信息 + unsigned char myslots[REDIS_CLUSTER_SLOTS/8]; + + // 如果发送者是一个从节点,那么这里记录的是发送者正在复制的主节点的名字 + // 如果发送者是一个主节点,那么这里记录的是 REDIS_NODE_NULL_NAME,一个 40 宇节长值全为 0 的字节数组 + char slaveof[REDIS_CLUSTER_NAMELEN]; + + // 发送者的端口号 + uint16_t port; + // 发送者的标识值 + uint16_t flags; + //发送者所处集群的状态 + unsigned char state; + // 消息的正文(或者说, 内容) + union clusterMsgData data; +} +``` + +clusterMsg 结构的 currentEpoch、sender、myslots 等属性记录了发送者的节点信息,接收者会根据这些信息在 clusterState.nodes 字典里找到发送者对应的 clusterNode 结构,并对结构进行更新,比如**传播节点的槽指派信息** + +消息正文: + +```c +union clusterMsgData { + // MEET、PING、PONG 消息的正文 + struct { + // 每条 MEET、PING、PONG 消息都包含两个 clusterMsgDataGossip 结构 + clusterMsgDataGossip gossip[1]; + } ping; + + // FAIL 消息的正文 + struct { + clusterMsgDataFail about; + } fail; + + // PUBLISH 消息的正文 + struct { + clusterMsgDataPublish msg; + } publish; + + // 其他消息正文... +} +``` + + + +*** + + + + + +#### Gossip + +Redis 集群中的各个节点通过 Gossip 协议来交换各自关于不同节点的状态信息,其中 Gossip 协议由 MEET、PING、PONG 消息实现,三种消息使用相同的消息正文,所以节点通过消息头的 type 属性来判断消息的具体类型 + +发送者发送这三种消息时,会从已知节点列表中**随机选出两个节点**(主从都可以),将两个被选中节点信息保存到两个 Gossip 结构 + +```c +typedef struct clusterMsgDataGossip { + // 节点的名字 + char nodename[REDIS CLUSTER NAMELEN]; + + // 最后一次向该节点发送PING消息的时间戳 + uint32_t ping_sent; + // 最后一次从该节点接收到PONG消息的时间戳 + uint32_t pong_received; + + // 节点的IP地址 + char ip[16]; + // 节点的端口号 + uint16_t port; + // 节点的标识值 + uint16_t flags; +} +``` + +当接收者收到消息时,会访问消息正文中的两个数据结构,来进行相关操作 + +* 如果被选中节点不存在于接收者的已知节点列表,接收者将根据结构中记录的 IP 地址和端口号,与节点进行握手 +* 如果存在,根据 Gossip 结构记录的信息对节点所对应的 clusterNode 结构进行更新 + + + +*** + + + +#### FAIL + +在集群的节点数量比较大的情况下,使用 Gossip 协议来传播节点的已下线信息会带来一定延迟,因为 Gossip 协议消息通常需要一段时间才能传播至整个集群,所以通过发送 FAIL消息可以让集群里的所有节点立即知道某个主节点已下线,从而尽快进行其他操作 + +FAIL 消息的正文由 clusterMsgDataFail 结构表示,该结构只有一个属性,记录了已下线节点的名字 + +```c +typedef struct clusterMsgDataFail { + char nodename[REDIS_CLUSTER_NAMELEN)]; +}; +``` + +因为传播下线信息不需要其他属性,所以节省了传播的资源 + + + +*** + + + +#### PUBLISH + +当客户端向集群中的某个节点发送命令,接收到 PUBLISH 命令的节点不仅会向 channel 频道发送消息 message,还会向集群广播一条 PUBLISH 消息,所有接收到这条 PUBLISH 消息的节点都会向 channel 频道发送 message 消息,最终集群中所有节点都发了 + +```sh +PUBLISH +``` + +PUBLISH 消息的正文由 clusterMsgDataPublish 结构表示: + +```c +typedef struct clusterMsgDataPublish { + // channel参数的长度 + uint32_t channel_len; + // message参数的长度 + uint32_t message_len; + + // 定义为8字节只是为了对齐其他消息结构,实际的长度由保存的内容决定 + // bulk_data 的 0 至 channel_len-1 字节保存的是channel参数 + // bulk_data的 channel_len 字节至 channel_len + message_len-1 字节保存的则是message参数 + unsigned char bulk_data[8]; +} +``` + +让集群的所有节点执行相同的 PUBLISH 命令,最简单的方法就是向所有节点广播相同的 PUBLISH 命令,这也是 Redis 复制 PUBLISH 命令时所使用的,但是这种做法并不符合 Redis 集群的各**个节点通过发送和接收消息来进行通信**的规则 + - @@ -13640,25 +14755,25 @@ sentinel 在通知阶段不断的去获取 master/slave 的信息,然后在各 - 是否启用 cluster,加入 cluster 节点 - ```properties + ```sh cluster-enabled yes|no ``` - cluster 配置文件名,该文件属于自动生成,仅用于快速查找文件并查询文件内容 - ```properties + ```sh cluster-config-file filename ``` - 节点服务响应超时时间,用于判定该节点是否下线或切换为从节点 - ```properties + ```sh cluster-node-timeout milliseconds ``` - master 连接的 slave 最小数量 - ```properties + ```sh cluster-migration-barrier min_slave_number ``` @@ -13668,31 +14783,31 @@ sentinel 在通知阶段不断的去获取 master/slave 的信息,然后在各 - 查看集群节点信息 - ```properties + ```sh cluster nodes ``` - 更改 slave 指向新的 master - ```properties + ```sh cluster replicate master-id ``` - 发现一个新节点,新增 master - ```properties + ```sh cluster meet ip:port ``` - 忽略一个没有 solt 的节点 - ```properties + ```sh cluster forget server_id ``` - 手动故障转移 - ```properties + ```sh cluster failover ``` @@ -13700,7 +14815,7 @@ sentinel 在通知阶段不断的去获取 master/slave 的信息,然后在各 * 创建集群 - ```properties + ```sh redis-cli –-cluster create masterhost1:masterport1 masterhost2:masterport2 masterhost3:masterport3 [masterhostn:masterportn ...] slavehost1:slaveport1 slavehost2:slaveport2 slavehost3:slaveport3 -–cluster-replicas n ``` @@ -13709,25 +14824,25 @@ sentinel 在通知阶段不断的去获取 master/slave 的信息,然后在各 * 添加 master 到当前集群中,连接时可以指定任意现有节点地址与端口 - ```properties + ```sh redis-cli --cluster add-node new-master-host:new-master-port now-host:now-port ``` * 添加 slave - ```properties + ```sh redis-cli --cluster add-node new-slave-host:new-slave-port master-host:master-port --cluster-slave --cluster-master-id masterid ``` * 删除节点,如果删除的节点是 master,必须保障其中没有槽 slot - ```properties + ```sh redis-cli --cluster del-node del-slave-host:del-slave-port del-slave-id ``` * 重新分槽,分槽是从具有槽的 master 中划分一部分给其他 master,过程中不创建新的槽 - ```properties + ```sh redis-cli --cluster reshard new-master-host:new-master:port --cluster-from src- master-id1, src-master-id2, src-master-idn --cluster-to target-master-id -- cluster-slots slots ``` @@ -13736,7 +14851,7 @@ sentinel 在通知阶段不断的去获取 master/slave 的信息,然后在各 * 重新分配槽,从具有槽的 master 中分配指定数量的槽到另一个 master 中,常用于清空指定 master 中的槽 - ```properties + ```sh redis-cli --cluster reshard src-master-host:src-master-port --cluster-from src- master-id --cluster-to target-master-id --cluster-slots slots --cluster-yes ``` diff --git a/Frame.md b/Frame.md index 9b54eaf..35b4916 100644 --- a/Frame.md +++ b/Frame.md @@ -4486,7 +4486,7 @@ NameServer 主要包括两个功能: NameServer 特点: * NameServer 通常是集群的方式部署,**各实例间相互不进行信息通讯** -* Broker 向每一台 NameServer 注册自己的路由信息,所以每个 NameServer 实例上面**都保存一份完整的路由信息** +* Broker 向每一台 NameServer(集群)注册自己的路由信息,所以每个 NameServer 实例上面**都保存一份完整的路由信息** * 当某个 NameServer 因某种原因下线了,Broker 仍可以向其它 NameServer 同步其路由信息 BrokerServer 主要负责消息的存储、投递和查询以及服务高可用保证,在 RocketMQ 系统中接收从生产者发送来的消息并存储、同时为消费者的拉取请求作准备,也存储消息相关的元数据,包括消费者组、消费进度偏移和主题和队列消息等 From cfc3e63881c809b52e836693dca6b5e4e2891828 Mon Sep 17 00:00:00 2001 From: Seazean Date: Mon, 9 May 2022 12:14:44 +0800 Subject: [PATCH 03/35] Update Java Note --- DB.md | 1066 +++++++++++++++++++++++++++++++++++++++++++++++++++---- Prog.md | 6 +- 2 files changed, 991 insertions(+), 81 deletions(-) diff --git a/DB.md b/DB.md index c1be7a3..b7d2a4a 100644 --- a/DB.md +++ b/DB.md @@ -9223,13 +9223,11 @@ typedef struct redisDB { ```sh type key #获取key的类型 - sort key [ASC/DESC] #对key中数据排序,默认对数字排序,并不更改集合中的数据位置,只是查询 - sort key alpha #对key中字母排序 dbsize #获取当前数据库的数据总量,即key的个数 flushdb #清除当前数据库的所有数据(慎用) flushall #清除所有数据(慎用) ``` - + 在执行 FLUSHDB 这样的危险命令之前,最好先执行一个 SELECT 命令,保证当前所操作的数据库是目标数据库 @@ -9415,6 +9413,8 @@ Redis 采用惰性删除和定期删除策略的结合使用 + + *** @@ -9497,6 +9497,221 @@ Redis 如果不设置最大内存大小或者设置最大内存大小为 0,在 +*** + + + +### 排序机制 + +#### 基本介绍 + +Redis 的 SORT 命令可以对列表键、集合键或者有序集合键的值进行排序,并不更改集合中的数据位置,只是查询 + +```sh +SORT key [ASC/DESC] #对key中数据排序,默认对数字排序,并不更改集合中的数据位置,只是查询 +SORT key ALPHA #对key中字母排序,按照字典序 +``` + + + + + +*** + + + +#### SORT + +`SORT ` 命令可以对一个包含数字值的键 key 进行排序 + +假设 `RPUSH numbers 3 1 2`,执行 `SORT numbers` 的详细步骤: + +* 创建一个和 key 列表长度相同的数组,数组每项都是 redisSortObject 结构 + + ```c + typedef struct redisSortObject { + // 被排序键的值 + robj *obj; + + // 权重 + union { + // 排序数字值时使用 + double score; + // 排序带有 BY 选项的字符串 + robj *cmpobj; + } u; + } + ``` + +* 遍历数组,将各个数组项的 obj 指针分别指向 numbers 列表的各个项 + +* 遍历数组,将 obj 指针所指向的列表项转换成一个 double 类型的浮点数,并将浮点数保存在对应数组项的 u.score 属性里 + +* 根据数组项 u.score 属性的值,对数组进行数字值排序,排序后的数组项按 u.score 属性的值**从小到大排列** + +* 遍历数组,将各个数组项的 obj 指针所指向的值作为排序结果返回给客户端,程序首先访问数组的索引 0,依次向后访问 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-sort排序.png) + +对于 `SORT key [ASC/DESC]` 函数: + +* 在执行升序排序时,排序算法使用的对比函数产生升序对比结果 +* 在执行降序排序时,排序算法所使用的对比函数产生降序对比结果 + + + +**** + + + +#### BY + +SORT 命令默认使用被排序键中包含的元素作为排序的权重,元素本身决定了元素在排序之后所处的位置,通过使用 BY 选项,SORT 命令可以指定某些字符串键,或者某个哈希键所包含的某些域(field)来作为元素的权重,对一个键进行排序 + +```sh +SORT BY # 数值 +SORT BY ALPHA # 字符 +``` + +```sh +redis> SADD fruits "apple" "banana" "cherry" +(integer) 3 +redis> SORT fruits ALPHA +1) "apple" +2) "banana" +3) "cherry" +``` + +```sh +redis> MSET apple-price 8 banana-price 5.5 cherry-price 7 +OK +# 使用水果的价钱进行排序 +redis> SORT fruits BY *-price +1) "banana" +2) "cherry" +3) "apple" +``` + +实现原理:排序时的 u.score 属性就会被设置为对应的权重 + + + + + +*** + + + +#### LIMIT + +SORT 命令默认会将排序后的所有元素都返回给客户端,通过 LIMIT 选项可以让 SORT 命令只返回其中一部分已排序的元素 + +```sh +LIMIT +``` + +* offset 参数表示要跳过的已排序元素数量 +* count 参数表示跳过给定数量的元素后,要返回的已排序元素数量 + +```sh +# 对应 a b c d e f g +redis> SORT alphabet ALPHA LIMIT 2 3 +1) "c" +2) "d" +3) "e" +``` + +实现原理:在排序后的 redisSortObject 结构数组中,将指针移动到数组的索引 2 上,依次访问 array[2]、array[3]、array[4] 这 3 个数组项,并将数组项的 obj 指针所指向的元素返回给客户端 + + + + + +*** + + + +#### GET + +SORT 命令默认在对键进行排序后,返回被排序键本身所包含的元素,通过使用 GET 选项, 可以在对键进行排序后,根据被排序的元素以及 GET 选项所指定的模式,查找并返回某些键的值 + +```sh +SORT GET +``` + +```sh +redis> SADD students "tom" "jack" "sea" +#设置全名 +redis> SET tom-name "Tom Li" +OK +redis> SET jack-name "Jack Wang" +OK +redis> SET sea-name "Sea Zhang" +OK +``` + +```sh +redis> SORT students ALPHA GET *-name +1) "Jack Wang" +2) "Sea Zhang" +3) "Tom Li" +``` + +实现原理:对 students 进行排序后,对于 jack 元素和 *-name 模式,查找程序返回键 jack-name,然后获取 jack-name 键对应的值 + + + + + +*** + + + +#### STORE + +SORT 命令默认只向客户端返回排序结果,而不保存排序结果,通过使用 STORE 选项可以将排序结果保存在指定的键里面 + +```sh +SORT STORE +``` + +```sh +redis> SADD students "tom" "jack" "sea" +(integer) 3 +redis> SORT students ALPHA STORE sorted_students +(integer) 3 +``` + +实现原理:排序后,检查 sorted_students 键是否存在,如果存在就删除该键,设置 sorted_students 为空白的列表键,遍历排序数组将元素依次放入 + + + + + +*** + + + +#### 执行顺序 + +调用 SORT 命令,除了 GET 选项之外,改变其他选项的摆放顺序并不会影响命令执行选项的顺序 + +```sh +SORT ALPHA [ASC/DESC] BY LIMIT GET STORE +``` + +执行顺序: + +* 排序:命令会使用 ALPHA 、ASC 或 DESC、BY 这几个选项,对输入键进行排序,并得到一个排序结果集 +* 限制排序结果集的长度:使用 LIMIT 选项,对排序结果集的长度进行限制 +* 获取外部键:根据排序结果集中的元素以及 GET 选项指定的模式,查找并获取指定键的值,并用这些值来作为新的排序结果集 +* 保存排序结果集:使用 STORE 选项,将排序结果集保存到指定的键上面去 +* 向客户端返回排序结果集:最后一步命令遍历排序结果集,并依次向客户端返回排序结果集中的元素 + + + + + *** @@ -9525,8 +9740,8 @@ Redis 如果不设置最大内存大小或者设置最大内存大小为 0,在 * 如果给定的通知类型 type 不是服务器允许发送的通知类型,那么函数会直接返回 * 如果给定的通知是服务器允许发送的通知 - * 检测服务器是否允许发送键空间通知,如果允许程序就会构建并发送事件通知 - * 检测服务器是否允许发送键事件通知,如果允许程序就会构建并发送事件通知 + * 检测服务器是否允许发送键空间通知,允许就会构建并发送事件通知 + * 检测服务器是否允许发送键事件通知,允许就会构建并发送事件通知 @@ -9708,11 +9923,9 @@ def aeProcessEvents(): 事件的调度和执行规则: * aeApiPoll 函数的最大阻塞时间由到达时间最接近当前时间的时间事件决定,可以避免服务器对时间事件进行频繁的轮询(忙等待),也可以确保 aeApiPoll 函数不会阻塞过长时间 -* 文件事件是随机出现的,如果等待并处理完一次文件事件后仍未有任何时间事件到达,那么服务器将再次等待并处理文件事件。随着文件事件的不断执行,会逐渐向时间事件所设置的到达时间逼近,并最终来到到达时间,这时就可以开始处理时间事件 * 对文件事件和时间事件的处理都是**同步、有序、原子地执行**,服务器不会中途中断事件处理,也不会对事件进行抢占,所以两种处理器都要尽可地减少程序的阻塞时间,并在有需要时**主动让出执行权**,从而降低事件饥饿的可能性 * 命令回复处理器在写入字节数超过了某个预设常量,就会主动用 break 跳出写入循环,将余下的数据留到下次再写 * 时间事件也会将非常耗时的持久化操作放到子线程或者子进程执行 - * 时间事件在文件事件之后执行,并且事件之间不会出现抢占,所以时间事件的实际处理时间通常会比设定的到达时间稍晚 @@ -10103,7 +10316,7 @@ SET KEY VALUE -> # 命令 命令执行器开始对命令操作: -* 查找命令:首先根据客户端状态的 argv[0] 参数,在命令表 (command table) 中查找参数所指定的命令,并将找到的命令保存到客户端状态的 cmd 属性里面,是一个 redisCommand 结构 +* 查找命令:首先根据客户端状态的 argv[0] 参数,在**命令表 (command table)** 中查找参数所指定的命令,并将找到的命令保存到客户端状态的 cmd 属性里面,是一个 redisCommand 结构 命令查找算法与字母的大小写无关,所以命令名字的大小写不影响命令表的查找结果 @@ -10125,10 +10338,10 @@ SET KEY VALUE -> # 命令 * 如果服务器开启了慢查询日志功能,那么慢查询日志模块会检查是否需要为刚刚执行完的命令请求添加一条新的慢查询日志 * 根据执行命令所耗费的时长,更新命令的 redisCommand 结构的 milliseconds 属性,并将命令 calls 计数器的值增一 - * 如果服务器开启了 AOF 持久化功能,那么 AOF 持久化模块会将刚刚执行的命令请求写入到 AOF 缓冲区里面 - * 如果有其他从服务器正在复制当前这个服务器,那么服务器会将刚刚执行的命令传播给所有从服务器 + * 如果服务器开启了 AOF 持久化功能,那么 AOF 持久化模块会将执行的命令请求写入到 AOF 缓冲区里面 + * 如果有其他从服务器正在复制当前这个服务器,那么服务器会将执行的命令传播给所有从服务器 -* 将命令回复发送给客户端:客户端套接字变为可写状态时,服务器就会执行命令回复处理器,将客户端输出缓冲区中的命令回复发送给客户端,发送完毕之后回复处理器会清空客户端状态的输出缓冲区,为处理下一个命令请求做好准备 +* 将命令回复发送给客户端:客户端**套接字变为可写状态**时,服务器就会执行命令回复处理器,将客户端输出缓冲区中的命令回复发送给客户端,发送完毕之后回复处理器会清空客户端状态的输出缓冲区,为处理下一个命令请求做好准备 @@ -10512,6 +10725,98 @@ initServer 还进行了非常重要的设置操作: +***** + + + +### 慢日志 + +#### 基本介绍 + +Redis 的慢查询日志功能用于记录执行时间超过给定时长的命令请求,通过产生的日志来监视和优化查询速度 + +服务器配置有两个和慢查询日志相关的选项: + +* slowlog-log-slower-than 选项指定执行时间超过多少微秒的命令请求会被记录到日志上 +* slowlog-max-len 选项指定服务器最多保存多少条慢查询日志 + +服务器使用先进先出 FIFO 的方式保存多条慢查询日志,当服务器存储的慢查询日志数量等于 slowlog-max-len 选项的值时,在添加一条新的慢查询日志之前,会先将最旧的一条慢查询日志删除 + +配置选项可以通过 CONFIG SET option value 命令进行设置 + +常用命令: + +```sh +SLOWLOG GET 命令查看服务器保存的慢日志 +SLOWLOG LEN 命令查看日志数量 +SLOWLOG RESET 命令清除所有慢查询日志 +``` + + + +*** + + + +#### 日志保存 + +服务器状态中包含了慢查询日志功能有关的属性: + +```c +struct redisServer { + // 下一条慢查询日志的ID + long long slowlog_entry_id; + + // 保存了所有慢查询日志的链表 + list *slowlog; + + // 服务器配置选项的值 + long long slowlog-log-slower-than; + // 服务器配置选项的值 + unsigned long slowlog_max_len; +} +``` + +slowlog_entry_id 属性的初始值为 0,每当创建一条新的慢查询日志时,这个属性就会用作新日志的 id 值,之后该属性增一 + +slowlog 链表保存了服务器中的所有慢查询日志,链表中的每个节点是一个 slowlogEntry 结构, 代表一条慢查询日志: + +```c +typedef struct slowlogEntry { + // 唯一标识符 + long long id; + // 命令执行时的时间,格式为UNIX时间戳 + time_t time; + // 执行命令消耗的时间,以微秒为单位 + long long duration; + // 命令与命令参数 + robj **argv; + // 命令与命令参数的数量 + int argc; +} +``` + + + + + +*** + + + +#### 添加日志 + +在每次执行命令的前后,程序都会记录微秒格式的当前 UNIX 时间戳,两个时间之差就是执行命令所耗费的时长,函数会检查命令的执行时长是否超过 slowlog-log-slower-than 选项所设置: + +* 如果是的话,就为命令创建一个新的日志,并将新日志添加到 slowlog 链表的表头 +* 检查慢查询日志的长度是否超过 slowlog-max-len 选项所设置的长度,如果是将多出来的日志从 slowlog 链表中删除掉 + +* 将 redisServer. slowlog_entry_id 的值增 1 + + + + + *** @@ -10585,7 +10890,7 @@ C 字符串**每次**增长或者缩短都会进行一次内存重分配,拼 SDS 通过未使用空间解除了字符串长度和底层数组长度之间的关联,在 SDS 中 buf 数组的长度不一定就是字符数量加一, 数组里面可以包含未使用的字节,字节的数量由 free 属性记录 -内存重分配涉及复杂的算法,需要执行系统调用,是一个比较耗时的操作,SDS 的两种优化策略: +内存重分配涉及复杂的算法,需要执行**系统调用**,是一个比较耗时的操作,SDS 的两种优化策略: * 空间预分配:当 SDS 的 API 进行修改并且需要进行空间扩展时,程序不仅会为 SDS 分配修改所必需的空间, 还会为 SDS 分配额外的未使用空间 @@ -11910,39 +12215,107 @@ set 类型:与 hash 存储结构哈希表完全相同,只是仅存储键不 ### Bitmaps -#### 操作 +### Bitmaps + +#### 基本操作 + +Bitmaps 是二进制位数组(bit array),底层使用 SDS 字符串表示,因为 SDS 是二进制安全的 -Bitmaps 本身不是一种数据类型, 实际上就是字符串(key-value) , 但是它可以对字符串的位进行操作 +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-位数组结构.png) + +buf 数组的每个字节用一行表示,buf[1] 是 `'0円'`,保存位数组的顺序和书写位数组的顺序是完全相反的,图示的位数组 0100 1101 数据结构的详解查看 Java → Algorithm → 位图 -指令操作: -* 获取指定 key 对应**偏移量**上的 bit 值 - ```sh - getbit key offset - ``` -* 设置指定 key 对应偏移量上的 bit 值,value 只能是 1 或 0 - ```sh - setbit key offset value - ``` +*** -* 对指定 key 按位进行交、并、非、异或操作,并将结果保存到 destKey 中 - ```sh - bitop option destKey key1 [key2...] - ``` - option:and 交、or 并、not 非、xor 异或 +#### 命令实现 -* 统计指定 key 中1的数量 +##### GETBIT - ```sh - bitcount key [start end] - ``` +GETBIT 命令获取位数组 bitarray 在 offset 偏移量上的二进制位的值 + +```sh +GETBIT +``` + +执行过程: + +* 计算 `byte = offset/8`(向下取整), byte 值记录数据保存在位数组中的索引 +* 计算 `bit = (offset mod 8) + 1`,bit 值记录数据在位数组中的第几个二进制位 +* 根据 byte 和 bit 值,在位数组 bitarray 中定位 offset 偏移量指定的二进制位,并返回这个位的值 + +GETBIT 命令执行的所有操作都可以在常数时间内完成,所以时间复杂度为 O(1) + + + +*** + + + +##### SETBIT + +SETBIT 将位数组 bitarray 在 offset 偏移量上的二进制位的值设置为 value,并向客户端返回二进制位的旧值 + +```sh +SETBIT +``` + +执行过程: + +* 计算 `len = offset/8 + 1`,len 值记录了保存该数据至少需要多少个字节 +* 检查 bitarray 键保存的位数组的长度是否小于 len,成立就会将 SDS 扩展为 len 字节(注意空间预分配机制),所有新扩展空间的二进制位的值置为 0 +* 计算 `byte = offset/8`(向下取整), byte 值记录数据保存在位数组中的索引 +* 计算 `bit = (offset mod 8) + 1`,bit 值记录数据在位数组中的第几个二进制位 +* 根据 byte 和 bit 值,在位数组 bitarray 中定位 offset 偏移量指定的二进制位,首先将指定位现存的值保存在 oldvalue 变量,然后将新值 value 设置为这个二进制位的值 +* 向客户端返回 oldvalue 变量的值 + + + +*** + + + +##### BITCOUNT + +BITCOUNT 命令用于统计给定位数组中,值为 1 的二进制位的数量 + +```sh +BITCOUNT [start end] +``` + +二进制位统计算法: + +* 遍历法:遍历位数组中的每个二进制位 +* 查表算法:读取每个字节(8 位)的数据,查表获取数值对应的二进制中有几个 1 +* variable-precision SWAR算法:计算汉明距离 +* Redis 实现: + * 如果二进制位的数量大于等于 128 位, 那么使用 variable-precision SWAR 算法来计算二进制位的汉明重量 + * 如果二进制位的数量小于 128 位,那么使用查表算法来计算二进制位的汉明重量 + + + +**** + + + +##### BITOP + +BITOP 命令对指定 key 按位进行交、并、非、异或操作,并将结果保存到指定的键中 + +```sh +BITOP OPTION destKey key1 [key2...] +``` + +OPTION 有 AND(与)、OR(或)、 XOR(异或)和 NOT(非)四个选项 + +AND、OR、XOR 三个命令可以接受多个位数组作为输入,需要遍历输入的每个位数组的每个字节来进行计算,所以命令的复杂度为 O(n^2);与此相反,NOT 命令只接受一个位数组输入,所以时间复杂度为 O(n) @@ -12076,7 +12449,7 @@ AOF:将数据的操作过程进行保存,日志形式,存储操作过程 #### 文件创建 -RDB 持久化功能所生成的 RDB文件 是一个经过压缩的紧凑二进制文件,通过该文件可以还原生成 RDB 文件时的数据库状态,有两个 Redis 命令可以生成 RDB 文件,一个是 SAVE,另一个是 BGSAVE +RDB 持久化功能所生成的 RDB 文件 是一个经过压缩的紧凑二进制文件,通过该文件可以还原生成 RDB 文件时的数据库状态,有两个 Redis 命令可以生成 RDB 文件,一个是 SAVE,另一个是 BGSAVE @@ -12722,37 +13095,68 @@ vfork(虚拟内存 fork virtual memory fork):调用 vfork() 父进程被 ## 事务机制 -### 基本操作 +### 事务特征 -Redis 事务的主要作用就是串联多个命令防止别的命令插队 +Redis 事务就是将多个命令请求打包,然后**一次性、按顺序**地执行多个命令的机制,并且在事务执行期间,服务器不会中断事务去执行其他的命令请求,会将事务中的所有命令都执行完毕,然后才去处理其他客户端的命令请求,Redis 事务的特性: -* 开启事务 +* Redis 事务**没有隔离级别**的概念,队列中的命令在事务没有提交之前都不会实际被执行 +* Redis 单条命令式保存原子性的,但是事务**不保证原子性**,事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚 - ```sh - multi #设定事务的开启位置,此指令执行后,后续的所有指令均加入到事务中 + + + + +*** + + + +### 工作流程 + +事务的执行流程分为三个阶段: + +* 事务开始:MULTI 命令的执行标志着事务的开始,通过在客户端状态的 flags 属性中打开 REDIS_MULTI 标识,将执行该命令的客户端从非事务状态切换至事务状态 + + ```sh + MULTI # 设定事务的开启位置,此指令执行后,后续的所有指令均加入到事务中 + ``` + +* 命令入队:事务队列以先进先出(FIFO)的方式保存入队的命令,每个 Redis 客户端都有事务状态,包含着事务队列: + + ```c + typedef struct redisClient { + // 事务状态 + multiState mstate; /* MULTI/EXEC state */ + } + + typedef struct multiState { + // 事务队列,FIFO顺序 + multiCmd *commands; + + // 已入队命令计数 + int count; + } ``` -* 执行事务 + * 如果命令为 EXEC、DISCARD、WATCH、MULTI 四个命中的一个,那么服务器立即执行这个命令 + * 其他命令服务器不执行,而是将命令放入一个事务队列里面,然后向客户端返回 QUEUED 回复 + +* 事务执行:EXEC 提交事务给服务器执行,服务器会遍历这个客户端的事务队列,执行队列中的命令并将执行结果返回 ```sh - exec #设定事务的结束位置,同时执行事务,与multi成对出现,成对使用 + EXEC # Commit 提交,执行事务,与multi成对出现,成对使用 ``` - 加入事务的命令暂时进入到任务队列中,并没有立即执行,只有执行 exec 命令才开始执行 +事务取消的方法: -* 取消事务 +* 取消事务: ```sh - discard #终止当前事务的定义,发生在multi之后,exec之前 + DISCARD # 终止当前事务的定义,发生在multi之后,exec之前 ``` 一般用于事务执行过程中输入了错误的指令,直接取消这次事务,类似于回滚 -Redis 事务的三大特性: -* Redis 事务是一个单独的隔离操作,将一系列预定义命令包装成一个整体(一个队列),当执行时按照添加顺序依次执行,中间不会被打断或者干扰 -* Redis 事务**没有隔离级别**的概念,队列中的命令在事务没有提交之前都不会实际被执行 -* Redis 单条命令式保存原子性的,但是事务**不保证原子性**,事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚 @@ -12760,32 +13164,76 @@ Redis 事务的三大特性: -### 工作流程 +### WATCH + +#### 监视机制 + +WATCH 命令是一个乐观锁(optimistic locking),可以在 EXEC 命令执行之前,监视任意数量的数据库键,并在 EXEC 命令执行时,检查被监视的键是否至少有一个已经被修改过了,如果是服务器将拒绝执行事务,并向客户端返回代表事务执行失败的空回复 + +* 添加监控锁 + + ```sh + WATCH key1 [key2......] #可以监控一个或者多个key + ``` + +* 取消对所有 key 的监视 -事务机制整体工作流程: + ```sh + UNWATCH + ``` -![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-事务的工作流程.png) -几种常见错误: -* 定义事务的过程中,命令格式输入错误,出现语法错误造成,**整体事务中所有命令均不会执行**,包括那些语法正确的命令 +*** + + + +#### 实现原理 + +每个 Redis 数据库都保存着一个 watched_keys 字典,键是某个被 WATCH 监视的数据库键,值则是一个链表,记录了所有监视相应数据库键的客户端: + +```c +typedef struct redisDb { + // 正在被 WATCH 命令监视的键 + dict *watched_keys; +} +``` + +所有对数据库进行修改的命令,在执行后都会调用 `multi.c/touchWatchKey` 函数对 watched_keys 字典进行检查,是否有客户端正在监视刚被命令修改过的数据库键,如果有的话函数会将监视被修改键的客户端的 REDIS_DIRTY_CAS 标识打开,表示该客户端的事务安全性已经被破坏 - +服务器接收到个客户端 EXEC 命令时,会根据这个客户端是否打开了 REDIS_DIRTY_CAS 标识,如果打开了说明客户端提交事务不安全,服务器会拒绝执行 -* 定义事务的过程中,命令执行出现错误,例如对字符串进行 incr 操作,能够正确运行的命令会执行,运行错误的命令不会被执行 - -* 已经执行完毕的命令对应的数据不会自动回滚,需要程序员在代码中实现回滚,应该尽可能避免: - 事务操作之前记录数据的状态 + +**** + + + +### ACID + +#### 原子性 + +在 Redis 中,事务总是具有原子性(Atomicity)、一致性(Consistency)和隔离性(Isolation),并且当 Redis 运行在某种特定的持久化模式下,事务也具有持久性(Durability) + +Redis 的事务队列中的命令要么就全部都执行,要么一个都不执行,因此 Redis 的事务是具有原子性的 + +Redis 不支持事务回滚机制(rollback),即使事务队列中的某个命令在执行期间出现了错误,整个事务也会继续执行下去,直到将事务队列中的所有命令都执行完毕为止 + +回滚需要程序员在代码中实现,应该尽可能避免: + +* 事务操作之前记录数据的状态 * 单数据:string + * 多数据:hash、list、set、zset - 设置指令恢复所有的被修改的项 + +* 设置指令恢复所有的被修改的项 * 单数据:直接 set(注意周边属性,例如时效) + * 多数据:修改对应值或整体克隆复制 @@ -12794,23 +13242,277 @@ Redis 事务的三大特性: -### 监控锁 +#### 一致性 + +事务具有一致性指的是,数据库在执行事务之前是一致的,那么在事务执行之后,无论事务是否执行成功,数据库也应该仍然是一致的 -对 key 添加监视锁,是一种乐观锁,在执行 exec 前如果其他客户端的操作导致 key 发生了变化,执行结果为 nil +一致是数据符合数据库的定义和要求,没有包含非法或者无效的错误数据,Redis 通过错误检测和简单的设计来保证事务的一致性: -* 添加监控锁 +* 入队错误:命令格式输入错误,出现语法错误造成,**整体事务中所有命令均不会执行**,包括那些语法正确的命令 - ```sh - watch key1 [key2......] #可以监控一个或者多个key + + +* 执行错误:命令执行出现错误,例如对字符串进行 incr 操作,食物中正确的命令会被执行,运行错误的命令不会被执行 + + + +* 服务器停机: + + * 如果服务器运行在无持久化的内存模式下,那么重启之后的数据库将是空白的,因此数据库是一致的 + * 如果服务器运行在持久化模式下,重启之后将数据库还原到一致的状态 + + + + +*** + + + +#### 隔离性 + +Redis 使用单线程的方式来执行事务,并且服务器保在执行事务期间不会对事务进行中断, 因此 Redis 的事务总是以串行的方式运行的,事务也总是具有隔离性的 + + + +*** + + + +#### 持久性 + +Redis 并没有为事务提供任何额外的持久化功能,事务的持久性由 Redis 所使用的持久化模式决定 + +配置选项 `no-appendfsync-on-rewrite` 可以配合 appendfsync 选项在 AOF 持久化模式使用: + +* 选项打开时在执行 BGSAVE 或者 BGREWRITEAOF 期间,服务器会暂时停止对 AOF 文件进行同步,从而尽可能地减少 I/O 阻塞 +* 选项打开时运行在 always 模式的 AOF 持久化,事务也不具有持久性,所以该选项默认关闭 + +在一个事务的最后加上 SAVE 命令总可以保证事务的耐久性 + + + + + +*** + + + +## Lua 脚本 + +### 环境创建 + +#### 基本介绍 + +Redis 从 2.6 版本引入对 Lua 脚本的支持,通过在服务器中嵌入 Lua 环境,客户端可以使用 Lua 脚本直接在服务器端**原子地执行**多个 Redis 命令 + +```sh +EVAL > [key ...] [arg ...] +EVALSHA [key ...] [arg ...] +``` + +EVAL 命令可以直接对输入的脚本计算: + +```sh +redis> EVAL "return 1 + 1" 0 +(integer) 2 +``` + +EVALSHA 命令根据脚本的 SHA1 校验和来对脚本计算: + +```sh +redis> EVALSHA "2f3lba2bb6d6a0f42ccl59d2e2dad55440778de3" 0 +(integer) 2 +``` + + + + + +*** + + + +#### 创建过程 + +Redis 服务器创建并修改 Lua 环境的整个过程: + +* 创建一个基础的 Lua 环境,调用 Lua 的 API 函数 lua_open + +* 载入多个函数库到 Lua 环境里面,让 Lua 脚本可以使用这些函数库来进行数据操作,包括基础核心函数 + +* 创建全局变量 redis 表格,表格包含以下函数: + + * 执行 Redis 命令的 redis.call 和 redis.pcall 函数 + * 记录 Redis 日志的 redis.log 函数,以及相应的日志级别 (level) 常量 redis.LOG_DEBUG 等 + * 计算 SHAl 校验和的 redis.shalhex 函数 + * 返回错误信息的 redis.error_reply 函数和 redis.status_reply 函数 + +* 使用 Redis 自制的随机函数来替换 Lua 原有的带有副作用的随机函数,从而避免在脚本中引入副作用 + + Redis 要求所有传入服务器的 Lua 脚本,以及 Lua 环境中的所有函数,都必须是无副作用(side effect)的纯函数(pure function),所以对有副作用的随机函数 `math.random` 和 `math.randornseed` 进行替换 + +* 创建排序辅助函数 ` _redis_compare_helper`,使用辅助函数来对一部分 Redis 命令的结果进行排序,从而消除命令的不确定性 + + 比如集合元素的排列是无序的, 所以即使两个集合的元素完全相同,输出结果也不一定相同,Redis 将 SMEMBERS 这类在相同数据集上产生不同输出的命令称为带有不确定性的命令 + +* 创建 redis.pcall 函数的错误报告辅助函数 `_redis_err_handler `,这个函数可以打印出错代码的来源和发生错误的行数 + +* 对 Lua环境中的全局环境进行保护,确保传入服务器的脚本不会因忘记使用 local 关键字,而将额外的全局变量添加到 Lua 环境 + +* 将完成修改的 Lua 环境保存到服务器状态的 lua 属性中,等待执行服务器传来的 Lua 脚本 + + ```c + struct redisServer { + Lua *lua; + }; ``` -* 取消对所有 key 的监视 +Redis 使用串行化的方式来执行 Redis 命令,所以在任何时间里最多都只会有一个脚本能够被放进 Lua 环境里面运行,因此整个 Redis 服务器只需要创建一个 Lua 环境即可 + + + + + +**** + + + +### 协作组件 + +#### 伪客户端 + +Redis 服务器为 Lua 环境创建了一个伪客户端负责处理 Lua 脚本中包含的所有 Redis 命令,工作流程: + +* Lua 环境将 redis.call 或者 redis.pcall 函数想要执行的命令传给伪客户端 +* 伪客户端将命令传给命令执行器 +* 命令执行器执行命令并将命令的执行结果返回给伪客户端 +* 伪客户端接收命令执行器返回的命令结果,并将结果返回给 Lua 环境 +* Lua 将命令结果返回给 redis.call 函数或者 redis.pcall 函数 +* redis.call 函数或者 redis.pcall 函数会将命令结果作为返回值返回给脚本的调用者 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-Lua伪客户端执行.png) + + + + + +*** + + + +#### 脚本字典 + +Redis 服务器为 Lua 环境创建 lua_scripts 字典,键为某个 Lua 脚本的 SHA1 校验和(checksum),值则是校验和对应的 Lua 脚本 + +```c +struct redisServer { + dict *lua_scripts; +}; +``` + +服务器会将所有被 EVAL 命令执行过的 Lua 脚本,以及所有被 SCRIPT LOAD 命令载入过的 Lua 脚本都保存到 lua_scripts 字典 + +```sh +redis> SCRIPT LOAD "return 'hi'" +"2f3lba2bb6d6a0f42ccl59d2e2dad55440778de3" # 字典的键,SHA1 校验和 +``` + + + + + +*** + + + +### 命令实现 + +#### 脚本函数 + +EVAL 命令的执行的第一步是为传入的脚本定义一个相对应的 Lua 函数,Lua 函数的名字由 f_ 前缀加上脚本的 SHA1 校验和(四十个字符长)组成,而函数的体(body)则是脚本本身 + +```sh +EVAL "return 'hello world'" 0 +# 命令将会定义以下的函数 +function f_533203lc6b470dc5a0dd9b4bf2030dea6d65de91() { + return 'hello world' +} +``` + +使用函数来保存客户端传入的脚本有以下优点: + +* 通过函数的局部性来让 Lua 环境保持清洁,减少了垃圾回收的工作最, 并且避免了使用全局变量 +* 如果某个脚本在 Lua 环境中被定义过至少一次,那么只需要 SHA1 校验和,服务器就可以在不知道脚本本身的情况下,直接通过调用 Lua 函数来执行脚本 + + + + + +#### 保存脚本 + +EVAL 命令第二步是将客户端传入的脚本保存到服务器的 lua_scripts 字典里,在 lua_scripts字典中新添加一个键值对 + + + + + +*** + + + +#### 执行函数 + +EVAL 命令第三步是执行脚本函数 + +* 将 EVAL 命令中传入的键名(key name)参数和脚本参数分别保存到 KEYS 数组和 ARGV 数组,将这两个数组作为全局变量传入到 Lua 环境里 +* 为 Lua 环境装载超时处理钩子(hook),这个钩子可以在脚本出现超时运行情况时,让客户端通过 `SCRIPT KILL` 命令停止脚本,或者通过 SHUTDOWN 命令直接关闭服务器 +* 执行脚本函数 +* 移除之前装载的超时钩子 +* 将执行脚本函数的结果保存到客户端状态的输出缓冲区里,等待服务器将结果返回给客户端 + + + + + +*** + + + +#### EVALSHA + +EVALSHA 命令的实现原理就是根据脚本的 SHA1 校验和来调用**脚本对应的函数**,如果函数在 Lua 环境中不存在,找不到 f_ 开头的函数,就会返回 `SCRIPT NOT FOUND` + + + + + +*** + + + +### 管理命令 + +Redis 中与 Lua 脚本有关的管理命令有四个: + +* SCRIPT FLUSH:用于清除服务器中所有和 Lua 脚本有关的信息,会释放并重建 lua_scripts 字典,关闭现有的 Lua 环境并重新创建一个新的 Lua 环境 + +* SCRIPT EXISTS:根据输入的 SHA1 校验和(允许一次传入多个校验和),检查校验和对应的脚本是否存在于服务器中,通过检查 lua_scripts 字典实现 + +* SCRIPT LOAD:在 Lua 环境中为脚本创建相对应的函数,然后将脚本保存到 lua_scripts字典里 ```sh - unwatch + redis> SCRIPT LOAD "return 'hi'" + "2f3lba2bb6d6a0f42ccl59d2e2dad55440778de3" ``` -应用:基于状态控制的批量任务执行,防止其他线程对变量的修改 +* SCRIPT KILL:停止脚本 + +如果服务器配置了 lua-time-li­mit 选项,那么在每次执行 Lua 脚本之前,都会设置一个超时处理的钩子。钩子会在脚本运行期间会定期检查运行时间是否超过配置时间,如果超时钩子将定期在脚本运行的间隙中,查看是否有 SCRIPT KILL 或者 SHUTDOWN 到达: + +* 如果超时运行的脚本没有执行过写入操作,客户端可以通过 SCRIPT KILL 来停止这个脚本 +* 如果执行过写入操作,客户端只能用 SHUTDOWN nosave 命令来停止服务器,防止不合法的数据被写入数据库中 + + @@ -12818,9 +13520,62 @@ Redis 事务的三大特性: -### 分布式锁 +### 脚本复制 -#### 基本操作 +#### 命令复制 + +当服务器运行在复制模式时,具有写性质的脚本命令也会被复制到从服务器,包括 EVAL、EVALSHA、SCRIPT FLUSH,以及 SCRIPT LOAD 命令 + +Redis 复制 EVAL、SCRIPT FLUSH、SCRIPT LOAD 三个命令的方法和复制普通 Redis 命令的方法一样,当主服务器执行完以上三个命令的其中一个时,会直接将被执行的命令传播(propagate)给所有从服务器,在从服务器中产生相同的效果 + + + + + +*** + + + +#### EVALSHA + +EVALSHA 命令的复制操作相对复杂,因为多个从服务器之间载入 Lua 脚本的清况各有不同,一个在主服务器被成功执行的 EVALSHA 命令,在从服务器执行时可能会出现脚本未找到(not found)错误 + +Redis 要求主服务器在传播 EVALSHA 命令时,必须确保 EVALSHA 命令要执行的脚本已经被所有从服务器载入过,如果不能确保主服务器会**将 EVALSHA 命令转换成一个等价的 EVAL 命令**,然后通过传播 EVAL 命令来代替 EVALSHA 命令 + +主服务器使用服务器状态的 repl_scriptcache_dict 字典记录已经将哪些脚本传播给了**所有从服务器**,当一个校验和出现在字典时,说明校验和对应的 Lua 脚本已经传播给了所有从服务器,主服务器可以直接传播 EVALSHA 命令 + +```c +struct redisServer { + // 键是一个个 Lua 脚本的 SHA1 校验和,值则全部都是 NULL + dict *repl_scriptcache_dict; +} +``` + +注意:每当主服务器添加一个新的从服务器时,都会清空 repl_scriptcache_dict 字典,因为字典里面记录的脚本已经不再被所有从服务器载入过,所以服务器以清空字典的方式,强制重新向所有从服务器传播脚本 + +通过使用 EVALSHA 命令指定的 SHA1 校验和,以及 lua_scripts 字典保存的 Lua 脚本,可以将一个 EVALSHA 命令转化为 EVAL 命令 + +```sh +EVALSHA "533203lc6b470dc5a0dd9b4bf2030dea6d65de91" 0 +# -> 转换 +EVAL "return'hello world'" 0 +``` + +脚本内容 `"return'hello world'"` 来源于 lua_scripts 字典 533203lc6b470dc5a0dd9b4bf2030dea6d65de91 键的值 + + + + + +*** + + + + + +## 分布式锁 + +### 基本操作 由于分布式系统多线程并发分布在不同机器上,这将使单机部署情况下的并发控制锁策略失效,需要分布式锁 @@ -12873,7 +13628,7 @@ Redis 分布式锁的基本使用,悲观锁 -#### 防误删 +### 防误删 setnx 获取锁时,设置一个指定的唯一值(uuid),释放前获取这个值,判断是否自己的锁,防止出现线程之间误删了其他线程的锁 @@ -13780,6 +14535,8 @@ SUBSCRIBE _sentinel_:hello 对于监视同一个服务器的多个 Sentinel 来说,**一个 Sentinel 发送的信息会被其他 Sentinel 接收到**,这些信息会被用于更新其他 Sentinel 对发送信息 Sentinel 的认知,也会被用于更新其他 Sentinel 对被监视的服务器的认知 +哨兵实例之间可以相互发现,要归功于 Redis 提供发布订阅机制 + *** @@ -13790,7 +14547,7 @@ SUBSCRIBE _sentinel_:hello Sentinel 为主服务器创建的实例结构的 sentinels 字典保存所有同样监视这个**主服务器的 Sentinel 信息**(包括 Sentinel 自己),字典的键是 Sentinel 的名字,格式为 `ip:port`,值是键所对应 Sentinel 的实例结构 -当一个 Sentinel 接收到其他 Sentinel 发来的信息时(发送信息为源 Sentinel,接收信息的为目标 Sentinel),目标 Sentinel 会分析提取出 Sentinel 相关的参数和主服务器相关的参数。根据主服务器参数,目标 Sentinel 会在自己的 Sentinel 状态 sentinelState.masters 中查找相应的主服务器实例结构,检查主服务器实例结构的 sentinels 字典中, 源 Sentinel 的实例结构是否存在 +当 Sentinel 接收到其他 Sentinel 发来的信息时(发送信息的为源 Sentinel,接收信息的为目标 Sentinel),目标 Sentinel 会分析提取参数,在自己的 Sentinel 状态 sentinelState.masters 中查找相应的主服务器实例结构,检查主服务器实例结构的 sentinels 字典中,源 Sentinel 的实例结构是否存在 * 如果源 Sentinel 的实例结构存在,那么对源 Sentinel 的实例结构进行更新 * 如果源 Sentinel 的实例结构不存在,说明源 Sentinel 是刚开始监视主服务器,目标 Sentinel 会为源 Sentinel 创建一个新的实例结构,并将这个结构添加到 sentinels 字典里面 @@ -14855,7 +15612,8 @@ typedef struct clusterMsgDataPublish { redis-cli --cluster reshard src-master-host:src-master-port --cluster-from src- master-id --cluster-to target-master-id --cluster-slots slots --cluster-yes ``` - + + @@ -14863,6 +15621,8 @@ typedef struct clusterMsgDataPublish { + + ## 缓存方案 ### 缓存模式 @@ -15268,28 +16028,138 @@ Redis 中的监控指标如下: + + ## 其他指令 -### 通信指令 +### 发布订阅 -Redis 发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息。 +#### 基本指令 + +Redis 发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息 -Redis 客户端可以订阅任意数量的频道 +Redis 客户端可以订阅任意数量的频道,每当有客户端向被订阅的频道发送消息(message)时,频道的**所有订阅者都会收到消息** ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-发布订阅.png) -操作命令: +操作过程: + +* 打开一个客户端订阅 channel1:`SUBSCRIBE channel1` -1. 打开一个客户端订阅 channel1:`SUBSCRIBE channel1` -2. 打开另一个客户端,给 channel1发布消息 hello:`publish channel1 hello` -3. 第一个客户端可以看到发送的消息 +* 打开另一个客户端,给 channel1 发布消息 hello:`PUBLISH channel1 hello` + +* 第一个客户端可以看到发送的消息 +客户端还可以通过 PSUBSCRIBE 命令订阅一个或多个模式,每当有其他客户端向某个频道发送消息时,消息不仅会被发送给这个频道的所有订阅者,还会被**发送给所有与这个频道相匹配的模式的订阅者**,比如 `PSUBSCRIBE channel*` 订阅模式,与 channel1 匹配 + 注意:发布的消息没有持久化,所以订阅的客户端只能收到订阅后发布的消息 + + +*** + + + +#### 频道操作 + +Redis 将所有频道的订阅关系都保存在服务器状态的 pubsub_channels 字典里,键是某个被订阅的频道,值是一个记录所有订阅这个频道的客户端链表 + +```c +struct redisServer { + // 保存所有频道的订阅关系, + dict *pubsub_channels; +} +``` + +客户端执行 SUBSCRIBE 命令订阅某个或某些频道,服务器会将客户端与频道进行关联: + +* 频道已经存在,直接将客户端添加到链表末尾 +* 频道还未有任何订阅者,在字典中为频道创建一个键值对,再将客户端添加到链表 + +UNSUBSCRIBE 命令用来退订某个频道,服务器将从 pubsub_channels 中解除客户端与被退订频道之间的关联 + + + + + +**** + + + +#### 模式操作 + +Redis 服务器将所有模式的订阅关系都保存在服务器状态的 pubsub_patterns 属性里 + +```c +struct redisServer { + // 保存所有模式订阅关系,链表中每个节点是一个 pubsubPattern + list *pubsub_patterns; +} + +typedef struct pubsubPattern { + // 订阅的客户端 + redisClient *client; + // 被订阅的模式,比如 channel* + robj *pattern; +} +``` + +客户端执行 PSUBSCRIBE 命令订阅某个模式,服务器会新建一个 pubsubPattern 结构并赋值,放入 pubsub_patterns 链表结尾 + +模式的退订命令 PUNSUBSCRIBE 是订阅命令的反操作,服务器在 pubsub_patterns 链表中查找并删除对应的结构 + + + + + +*** + + + +#### 发送消息 + +Redis 客户端执行 `PUBLISH ` 命令将消息 message发送给频道 channel,服务器会执行: + +* 在 pubsub_channels 字典里找到频道 channel 的订阅者名单,将消息 message 发送给所有订阅者 +* 遍历整个 pubsub_patterns 链表,查找与 channel 频道相**匹配的模式**,并将消息发送给所有订阅了这些模式的客户端 + +```c +// 如果频道和模式相匹配 +if match(channel, pubsubPattern.pattern) { + // 将消息发送给订阅该模式的客户端 + send_message(pubsubPattern.client, message); +} +``` + + + + + +*** + + + +#### 查看信息 + +PUBSUB 命令用来查看频道或者模式的相关信息 + +`PUBSUB CHANNELS [pattern]` 返回服务器当前被订阅的频道,其中 pattern 参数是可选的 + +* 如果不给定 pattern 参数,那么命令返回服务器当前被订阅的所有频道 +* 如果给定 pattern 参数,那么命令返回服务器当前被订阅的频道中与 pattern 模式相匹配的频道 + +`PUBSUB NUMSUB [channel-1 channel-2 ... channel-n]` 命令接受任意多个频道作为输入参数,并返回这些频道的订阅者数量 + +`PUBSUB NUMPAT` 命令用于返回服务器当前被订阅模式的数量 + + + + + **** @@ -15309,6 +16179,46 @@ Redis ACL 是 Access Control List(访问控制列表)的缩写,该功能 +*** + + + +### 监视器 + +MONITOR 命令,可以将客户端变为一个监视器,实时地接收并打印出服务器当前处理的命令请求的相关信息 + +```c +// 实现原理 +def MONITOR(): + // 打开客户端的监视器标志 + client.flags |= REDIS_MONITOR + + // 将客户端添加到服务器状态的 redisServer.monitors链表的末尾 + server.monitors.append(client) + // 向客户端返回 ok + send_reply("OK") +``` + +服务器每次处理命令请求都会调用 replicationFeedMonitors 函数,函数将被处理的命令请求的相关信息**发送给各个监视器** + + + +```sh +redis> MONITOR +OK +1378822099.421623 [0 127.0.0.1:56604] "PING" +1378822105.089572 [0 127.0.0.1:56604] "SET" "msg" "hello world" +1378822109.036925 [0 127.0.0.1:56604] "SET" "number" "123" +1378822140.649496 (0 127.0.0.1:56604] "SADD" "fruits" "Apple" "Banana" "Cherry" +1378822154.117160 [0 127.0.0.1:56604] "EXPIRE" "msg" "10086" +1378822257.329412 [0 127.0.0.1:56604] "KEYS" "*" +1378822258.690131 [0 127.0.0.1:56604] "DBSIZE" +``` + + + + + diff --git a/Prog.md b/Prog.md index 9eea252..50792cc 100644 --- a/Prog.md +++ b/Prog.md @@ -95,7 +95,7 @@ Thread 构造器: * `public Thread(String name)` ```java -public class ThreadDemo{ +public class ThreadDemo { public static void main(String[] args) { Thread t = new MyThread(); t.start(); @@ -105,10 +105,10 @@ public class ThreadDemo{ // main线程输出放在上面 就变成有先后顺序了,因为是 main 线程驱动的子线程运行 } } -class MyThread extends Thread{ +class MyThread extends Thread { @Override public void run() { - for(int i = 0 ; i < 100 ; i++ ){ + for(int i = 0 ; i < 100 ; i++ ) { System.out.println("子线程输出:"+i) } } From 20f10d25006e81eb970bb0b79dfd7ae139572d04 Mon Sep 17 00:00:00 2001 From: Seazean Date: 2022年5月11日 23:28:18 +0800 Subject: [PATCH 04/35] Update Java Note --- DB.md | 288 ++++++++++++++++++++++++++++++++++++++++++++----------- Frame.md | 12 ++- 2 files changed, 243 insertions(+), 57 deletions(-) diff --git a/DB.md b/DB.md index b7d2a4a..3ad14ec 100644 --- a/DB.md +++ b/DB.md @@ -9207,8 +9207,10 @@ typedef struct redisDB { keys pattern #查询key ``` - 查询模式规则:* 匹配任意数量的任意符号、? 配合一个任意符号、[] 匹配一个指定符号 + KEYS 命令需要**遍历存储的键值对**,操作延时高,一般不被建议用于生产环境中 + 查询模式规则:* 匹配任意数量的任意符号、? 配合一个任意符号、[] 匹配一个指定符号 + ```sh keys * #查询所有key keys aa* #查询所有以aa开头 @@ -10747,9 +10749,9 @@ Redis 的慢查询日志功能用于记录执行时间超过给定时长的命 常用命令: ```sh -SLOWLOG GET 命令查看服务器保存的慢日志 -SLOWLOG LEN 命令查看日志数量 -SLOWLOG RESET 命令清除所有慢查询日志 +SLOWLOG GET [n] # 查看 n 条服务器保存的慢日志 +SLOWLOG LEN # 查看日志数量 +SLOWLOG RESET # 清除所有慢查询日志 ``` @@ -12213,8 +12215,6 @@ set 类型:与 hash 存储结构哈希表完全相同,只是仅存储键不 -### Bitmaps - ### Bitmaps #### 基本操作 @@ -12323,9 +12323,9 @@ AND、OR、XOR 三个命令可以接受多个位数组作为输入,需要遍 -#### 应用 +#### 应用场景 -- 解决 Redis 缓存穿透,判断给定数据是否存在, 防止缓存穿透 +- **解决 Redis 缓存穿透**,判断给定数据是否存在, 防止缓存穿透 @@ -13215,9 +13215,9 @@ typedef struct redisDb { #### 原子性 -在 Redis 中,事务总是具有原子性(Atomicity)、一致性(Consistency)和隔离性(Isolation),并且当 Redis 运行在某种特定的持久化模式下,事务也具有持久性(Durability) +事务具有原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability) -Redis 的事务队列中的命令要么就全部都执行,要么一个都不执行,因此 Redis 的事务是具有原子性的 +原子性指事务队列中的命令要么就全部都执行,要么一个都不执行,但是在命令执行出错时,不会保证原子性(下一节详解) Redis 不支持事务回滚机制(rollback),即使事务队列中的某个命令在执行期间出现了错误,整个事务也会继续执行下去,直到将事务队列中的所有命令都执行完毕为止 @@ -13270,7 +13270,10 @@ Redis 不支持事务回滚机制(rollback),即使事务队列中的某个 #### 隔离性 -Redis 使用单线程的方式来执行事务,并且服务器保在执行事务期间不会对事务进行中断, 因此 Redis 的事务总是以串行的方式运行的,事务也总是具有隔离性的 +Redis 是一个单线程的执行原理,所以对于隔离性,分以下两种情况: + +* 并发操作在 EXEC 命令前执行,隔离性的保证要使用 WATCH 机制来实现,否则隔离性无法保证 +* 并发操作在 EXEC 命令后执行,隔离性可以保证 @@ -13303,7 +13306,7 @@ Redis 并没有为事务提供任何额外的持久化功能,事务的持久 #### 基本介绍 -Redis 从 2.6 版本引入对 Lua 脚本的支持,通过在服务器中嵌入 Lua 环境,客户端可以使用 Lua 脚本直接在服务器端**原子地执行**多个 Redis 命令 +Redis 对 Lua 脚本支持,通过在服务器中嵌入 Lua 环境,客户端可以使用 Lua 脚本直接在服务器端**原子地执行**多个命令 ```sh EVAL > [key ...] [arg ...] @@ -13313,7 +13316,7 @@ EVALSHA [key ...] [arg ...] EVAL 命令可以直接对输入的脚本计算: ```sh -redis> EVAL "return 1 + 1" 0 +redis> EVAL "return 1 + 1" 0 # 0代表需要的参数 (integer) 2 ``` @@ -13324,7 +13327,12 @@ redis> EVALSHA "2f3lba2bb6d6a0f42ccl59d2e2dad55440778de3" 0 (integer) 2 ``` +应用场景:Redis 只保证单条命令的原子性,所以为了实现原子操作,将多条的对 Redis 的操作整合到一个脚本里,但是避免把不需要做并发控制的操作写入脚本中 +Lua 语法特点: + +* 声明变量的时候无需指定数据类型,而是用 local 来声明变量为局部变量 +* 数组下标是从 1 开始 @@ -13444,15 +13452,7 @@ function f_533203lc6b470dc5a0dd9b4bf2030dea6d65de91() { * 通过函数的局部性来让 Lua 环境保持清洁,减少了垃圾回收的工作最, 并且避免了使用全局变量 * 如果某个脚本在 Lua 环境中被定义过至少一次,那么只需要 SHA1 校验和,服务器就可以在不知道脚本本身的情况下,直接通过调用 Lua 函数来执行脚本 - - - - -#### 保存脚本 - -EVAL 命令第二步是将客户端传入的脚本保存到服务器的 lua_scripts 字典里,在 lua_scripts字典中新添加一个键值对 - - +EVAL 命令第二步是将客户端传入的脚本保存到服务器的 lua_scripts 字典里,在字典中新添加一个键值对 @@ -13464,8 +13464,10 @@ EVAL 命令第二步是将客户端传入的脚本保存到服务器的 lua_scri EVAL 命令第三步是执行脚本函数 -* 将 EVAL 命令中传入的键名(key name)参数和脚本参数分别保存到 KEYS 数组和 ARGV 数组,将这两个数组作为全局变量传入到 Lua 环境里 +* 将 EVAL 命令中传入的**键名(key name)参数和脚本参数**分别保存到 KEYS 数组和 ARGV 数组,将这两个数组作为**全局变量**传入到 Lua 环境里 * 为 Lua 环境装载超时处理钩子(hook),这个钩子可以在脚本出现超时运行情况时,让客户端通过 `SCRIPT KILL` 命令停止脚本,或者通过 SHUTDOWN 命令直接关闭服务器 + + 因为 Redis 是单线程的执行命令,当 Lua 脚本阻塞时需要兜底策略,可以中断执行 * 执行脚本函数 * 移除之前装载的超时钩子 * 将执行脚本函数的结果保存到客户端状态的输出缓冲区里,等待服务器将结果返回给客户端 @@ -13577,50 +13579,47 @@ EVAL "return'hello world'" 0 ### 基本操作 -由于分布式系统多线程并发分布在不同机器上,这将使单机部署情况下的并发控制锁策略失效,需要分布式锁 +在分布式场景下,锁变量需要由一个共享存储系统来维护,多个客户端才可以通过访问共享存储系统来访问锁变量,加锁和释放锁的操作就变成了读取、判断和设置共享存储系统中的锁变量值多步操作 Redis 分布式锁的基本使用,悲观锁 -* 使用 setnx 设置一个公共锁 +* 使用 SETNX 设置一个公共锁 ```sh - setnx lock-key value # value任意数,返回为1设置成功,返回为0设置失败 + SETNX lock-key value # value任意数,返回为1设置成功,返回为0设置失败 ``` - * 对于返回设置成功的,拥有控制权,进行下一步的具体业务操作 - * 对于返回设置失败的,不具有控制权,排队或等待 - `NX`:只在键不存在时,才对键进行设置操作,`SET key value NX` 效果等同于 `SETNX key value` - + `XX` :只在键已经存在时,才对键进行设置操作 - + `EX`:设置键 key 的过期时间,单位时秒 - + `PX`:设置键 key 的过期时间,单位时毫秒 - + 说明:由于 `SET` 命令加上选项已经可以完全取代 SETNX、SETEX、PSETEX 的功能,Redis 不推荐使用这几个命令 - -* 操作完毕通过 del 操作释放锁 + +* 操作完毕通过 DEL 操作释放锁 ```sh - del lock-key + DEL lock-key ``` -* 使用 expire 为锁 key 添加存活(持有)时间,过期自动删除(放弃)锁 +* 使用 EXPIRE 为锁 key 添加存活(持有)时间,过期自动删除(放弃)锁,防止线程出现异常,无法释放锁 ```sh - expire lock-key second - pexpire lock-key milliseconds + EXPIRE lock-key second + PEXPIRE lock-key milliseconds ``` - 通过 expire 设置过期时间缺乏原子性,如果在 setnx 和 expire 之间出现异常,锁也无法释放 + 通过 EXPIRE 设置过期时间缺乏原子性,如果在 SETNX 和 EXPIRE 之间出现异常,锁也无法释放 -* 在 set 时指定过期时间 +* 在 SET 时指定过期时间,保证原子性 ```sh - SET key value [EX seconds | PX milliseconds] NX + SET key value NX [EX seconds | PX milliseconds] + -应用:解决抢购时出现超卖现象 @@ -13630,14 +13629,111 @@ Redis 分布式锁的基本使用,悲观锁 ### 防误删 -setnx 获取锁时,设置一个指定的唯一值(uuid),释放前获取这个值,判断是否自己的锁,防止出现线程之间误删了其他线程的锁 +场景描述:线程 A 正在执行,但是业务阻塞,在锁的过期时间内未执行完成,过期删除后线程 B 重新获取到锁,此时线程 A 执行完成,删除锁,导致线程 B 的锁被线程 A 误删 + +SETNX 获取锁时,设置一个指定的唯一值(UUID),释放前获取这个值,判断是否自己的锁,防止出现线程之间误删了其他线程的锁 ```java -// 加锁, unique_value作为客户端唯一性的标识 +// 加锁, unique_value作为客户端唯一性的标识, +// PX 10000 则表示 lock_key 会在 10s 后过期,以免客户端在这期间发生异常而无法释放锁 SET lock_key unique_value NX PX 10000 ``` -unique_value 是客户端的**唯一标识**,可以用一个随机生成的字符串来表示,PX 10000 则表示 lock_key 会在 10s 后过期,以免客户端在这期间发生异常而无法释放锁 + Lua 脚本(unlock.script)实现的释放锁操作的伪代码:key 类型参数会放入 KEYS 数组,其它参数会放入 ARGV 数组,在脚本中通过 KEYS 和 ARGV 传递参数,**保证判断标识和释放锁这两个操作的原子性** + +```sh +EVAL "return redis.call('set', KEYS[1], ARGV[1])" 1 lock_key unique_value # 1 代表需要一个参数 +``` + +```c +// 释放锁,KEYS[1] 就是锁的 key,ARGV[1] 就是标识值,避免误释放 +// 获取标识值,判断是否与当前线程标示一致 +if redis.call("get", KEYS[1]) == ARGV[1] then + return redis.call("del", KEYS[1]) +else + return 0 +end +``` + + + + + +*** + + + +### 优化锁 + +#### 不可重入 + +不可重入:同一个线程无法多次获取同一把锁 + +使用 hash 键,filed 是加锁的线程标识, value 是**锁重入次数** + +```sql +| key | value | +| | filed | value | +|-------------------------------| +| lock_key | thread1 | 1 | +``` + +锁重入: + +* 加锁时判断锁的 filed 属性是否是当前线程,如果是将 value 加 1 +* 解锁时判断锁的 filed 属性是否是当前线程,首先将 value 减一,如果 value 为 0 直接释放锁 + +使用 Lua 脚本保证多条命令的原子性 + + + +*** + + + +#### 不可重试 + +不可重试:获取锁只尝试一次就返回 false,没有重试机制 + +* 利用 Lua 脚本尝试获取锁,获取失败获取锁的剩余超时时间 ttl,或者通过参数传入线程抢锁允许等待的时间 +* 利用订阅功能订阅锁释放的信息,然后线程挂起等待 ttl 时间 +* 利用 Lua 脚本在释放锁时,发布一条锁释放的消息 + + + + + +*** + + + +#### 超时释放 + +超时释放:锁超时释放可以避免死锁,但如果是业务执行耗时较长,需要进行锁续时,防止业务未执行完提前释放锁 + +看门狗 watchDog 机制: + +* 获取锁成功后,提交周期任务,每隔一段时间(Redisson 中默认为过期时间 / 3),重置一次超时时间 +* 释放锁后,终止周期任务 + + + + + +*** + + + +#### 主从一致 + +主从一致性:集群模式下,主从同步存在延迟,当加锁后主服务器宕机时,从服务器还没同步主服务器中的锁数据,此时从服务器升级为主服务器,其他线程又可以获取到锁 + +将服务器升级为多主多从,: + +* 获取锁需要从所有主服务器 SET 成功才算获取成功 +* 某个 master 宕机,slave 还没有同步锁数据就升级为 master,其他线程尝试加锁会加锁失败,因为其他 master 上已经存在该锁 + + @@ -13776,7 +13872,7 @@ master 和 slave 互连: #### 旧版复制 -Redis 的复制功能分为同步(sync)和命令传播(command propagate)两个操作 +Redis 的复制功能分为同步(sync)和命令传播(command propagate)两个操作,主从库间的复制是**异步进行的** 同步操作用于将从服务器的数据库状态更新至主服务器当前所处的数据库状态,该过程又叫全量复制: @@ -13956,7 +14052,7 @@ PSYNC 命令的调用方法有两种 }; ``` - SLAVEOF 命令是一个异步命令,在完成属性的设置后服务器直接返回 OK,而实际的复制工作将在 OK返回之后才真正开始执行 + SLAVEOF 命令是一个**异步命令**,在完成属性的设置后服务器直接返回 OK,而实际的复制工作将在 OK 返回之后才真正开始执行 * 建立套接字连接: @@ -15466,6 +15562,26 @@ typedef struct clusterMsgDataPublish { +*** + + + +### 脑裂问题 + +脑裂指在主从集群中,同时有两个相同的主节点能接收写请求,导致客户端不知道应该往哪个主节点写入数据,导致不同客户端往不同的主节点上写入数据 + +* 原主节点并没有真的发生故障,由于某些原因无法处理请求(CPU 利用率很高、自身阻塞),无法按时响应心跳请求,被哨兵/集群主节点错误的判断为下线 +* 在被判断下线之后,原主库又重新开始处理请求了,哨兵/集群主节点还没有完成主从切换,客户端仍然可以和原主库通信,客户端发送的写操作就会在原主库上写入数据,造成脑裂问题 + +数据丢失问题:从库一旦升级为新主库,哨兵就会让原主库执行 slave of 命令,和新主库重新进行全量同步,原主库需要清空本地的数据,加载新主库发送的 RDB 文件,所以原主库在主从切换期间保存的新写数据就丢失了 + +预防脑裂:在主从集群部署时,合理地配置参数 min-slaves-to-write 和 min-slaves-max-lag + +* 假设从库有 K 个,可以将 min-slaves-to-write 设置为 K/2+1(如果 K 等于 1,就设为 1) +* 将 min-slaves-max-lag 设置为十几秒(例如 10〜20s) + + + *** @@ -15649,6 +15765,10 @@ Cache Aside Pattern 中服务端需要同时维系 DB 和 cache,并且是以 D * 首次请求数据一定不在 cache 的问题,一般采用缓存预热的方法,将热点数据可以提前放入 cache 中 * 写操作比较频繁的话导致 cache 中的数据会被频繁被删除,影响缓存命中率 +删除缓存而不是更新缓存的原因:每次更新数据库都更新缓存,造成无效写操作较多 + + + **** @@ -15698,14 +15818,24 @@ Read-Through Pattern 也存在首次不命中的问题,采用缓存预热解 缓存不一致的方法: * 数据库和缓存数据强一致场景: - * 更新 DB 时同样更新 cache,加一个锁来保证更新 cache 时不存在线程安全问题,这样可以增加命中率 + * 同步双写:更新 DB 时同样更新 cache,保证在一个事务中,通过加锁来保证更新 cache 时不存在线程安全问题 + * 延迟双删:先淘汰缓存再写数据库,休眠 1 秒再次淘汰缓存,可以将 1 秒内造成的缓存脏数据再次删除 - * CDC 同步:通过 canal 订阅 MySQL binlog 的变更上报给 Kafka,系统监听 Kafka 消息触发缓存失效 -* 可以短暂允许数据库和缓存数据不一致场景:更新 DB 的时候同样更新 cache,但是给缓存加一个比较短的过期时间,这样就可以保证即使数据不一致影响也比较小 + + * 异步通知: + + * 基于 MQ 的异步通知:对数据的修改后,代码需要发送一条消息到 MQ 中,缓存服务监听 MQ 消息 + * Canal 订阅 MySQL binlog 的变更上报给 Kafka,系统监听 Kafka 消息触发缓存失效,或者直接将变更发送到处理服务,**没有任何代码侵入** + + 低耦合,可以同时通知多个缓存服务,但是时效性一般,可能存在中间不一致状态 + +* 低一致性场景: + + * 更新 DB 的时候同样更新 cache,但是给缓存加一个比较短的过期时间,这样就可以保证即使数据不一致影响也比较小 + * 使用 Redis 自带的内存淘汰机制 -参考文章:http://cccboke.com/archives/2020-09-30-21-29-56 @@ -15781,6 +15911,8 @@ Read-Through Pattern 也存在首次不命中的问题,采用缓存预热解 #### 缓存击穿 +缓存击穿也叫热点 Key 问题 + 场景:系统平稳运行过程中,数据库连接量瞬间激增,Redis 服务器无大量 key 过期,Redis 内存平稳无波动,Redis 服务器 CPU 正常,但是数据库崩溃 问题排查: @@ -15844,7 +15976,7 @@ Read-Through Pattern 也存在首次不命中的问题,采用缓存预热解 4. key 加密:临时启动防灾业务 key,对 key 进行业务层传输加密服务,设定校验程序,过来的 key 校验;例如每天随机分配 60 个加密串,挑选 2 到 3 个,混淆到页面数据 id 中,发现访问 key 不满足规则,驳回数据访问 -总的来说:缓存击穿是指访问了不存在的数据,跳过了合法数据的 Redis 数据缓存阶段,每次访问数据库,导致对数据库服务器造成压力。通常此类数据的出现量是一个较低的值,当出现此类情况以毒攻毒,并及时报警。无论是黑名单还是白名单,都是对整体系统的压力,警报解除后尽快移除 +总的来说:缓存击穿是指访问了不存在的数据,跳过了合法数据的 Redis 数据缓存阶段,**每次访问数据库**,导致对数据库服务器造成压力。通常此类数据的出现量是一个较低的值,当出现此类情况以毒攻毒,并及时报警。无论是黑名单还是白名单,都是对整体系统的压力,警报解除后尽快移除 @@ -16030,7 +16162,7 @@ Redis 中的监控指标如下: -## 其他指令 +## 其他操作 ### 发布订阅 @@ -16219,6 +16351,52 @@ OK +*** + + + +### 批处理 + +Redis 的管道 Pipeline 机制可以一次处理多条指令 + +* Pipeline 中的多条命令非原子性,因为在向管道内添加命令时,其他客户端的发送的命令仍然在执行 +* 原生批命令(mset 等)是服务端实现,而 pipeline 需要服务端与客户端共同完成 + +使用 Pipeline 封装的命令数量不能太多,数据量过大会增加客户端的等待时间,造成网络阻塞,Jedis 中的 Pipeline 使用方式: + +```java +// 创建管道 +Pipeline pipeline = jedis.pipelined(); +for (int i = 1; i <= 100000; i++) { + // 放入命令到管道 + pipeline.set("key_" + i, "value_" + i); + if (i % 1000 == 0) { + // 每放入1000条命令,批量执行 + pipeline.sync(); + } +} +``` + +集群下模式下,批处理命令的多个 key 必须落在一个插槽中,否则就会导致执行失败,N 条批处理命令的优化方式: + +* 串行命令:for 循环遍历,依次执行每个命令 +* 串行 slot:在客户端计算每个 key 的 slot,将 slot 一致的分为一组,每组都利用 Pipeline 批处理,串行执行各组命令 +* 并行 slot:在客户端计算每个 key 的 slot,将 slot 一致的分为一组,每组都利用 Pipeline 批处理,**并行执行各组命令** +* hash_tag:将所有 key 设置相同的 hash_tag,则所有 key 的 slot 一定相同 + +| | 耗时 | 优点 | 缺点 | +| --------- | ------------------------------------------------- | -------------------- | -------------------- | +| 串行命令 | N 次网络耗时 + N 次命令耗时 | 实现简单 | 耗时久 | +| 串行 slot | m 次网络耗时 + N 次命令耗时,m = key 的 slot 个数 | 耗时较短 | 实现稍复杂 | +| 并行 slot | 1 次网络耗时 + N 次命令耗时 | 耗时非常短 | 实现复杂 | +| hash_tag | 1 次网络耗时 + N 次命令耗时 | 耗时非常短、实现简单 | 容易出现**数据倾斜** | + + + + + + + @@ -17302,7 +17480,9 @@ Jedis 用于 Java 语言连接 Redis 服务,并提供对应的操作 API * 客户端连接 Redis:API 文档 http://xetorthio.github.io/jedis/ 连接 redis:`Jedis jedis = new Jedis("192.168.0.185", 6379)` + 操作 redis:`jedis.set("name", "seazean"); jedis.get("name")` + 关闭 redis:`jedis.close()` 代码实现: diff --git a/Frame.md b/Frame.md index 35b4916..67c8500 100644 --- a/Frame.md +++ b/Frame.md @@ -4710,11 +4710,17 @@ RocketMQ 网络部署特点: #### 高可用性 -在创建 Topic 的时候,把 Topic 的多个 Message Queue 创建在多个 Broker 组上(相同 Broker 名称,不同 brokerId 的机器组成一个 Broker 组),当一个 Broker 组的 Master 不可用后,其他组的 Master 仍然可用,Producer 仍然可以发送消息 +NameServer 节点是无状态的,且各个节点直接的数据是一致的,部分 NameServer 不可用也可以保证 MQ 服务正常运行 -在 Consumer 的配置文件中,并不需要设置是从 Master 读还是从 Slave 读,当 Master 不可用或者繁忙的时候,Consumer 会被自动切换到从 Slave 读。有了自动切换的机制,当一个 Master 机器出现故障后,Consumer 仍然可以从 Slave 读取消息,不影响 Consumer 程序,达到了消费端的高可用性 +BrokerServer 的高可用通过 Master 和 Slave 的配合: -RocketMQ 目前还不支持把 Slave 自动转成 Master,需要手动停止 Slave 角色的 Broker,更改配置文件,用新的配置文件启动 Broker +* Slave 只负责读,当 Master 不可用,对应的 Slave 仍能保证消息被正常消费 +* 配置多组 Master-Slave 组,其他的 Master-Slave 组也会保证消息的正常发送和消费 +* 目前不支持把 Slave 自动转成 Master,需要手动停止 Slave 角色的 Broker,更改配置文件,用新的配置文件启动 Broker + +生产端的高可用:在创建 Topic 的时候,把 Topic 的**多个 Message Queue 创建在多个 Broker 组**上(相同 Broker 名称,不同 brokerId 的机器),当一个 Broker 组的 Master 不可用后,其他组的 Master 仍然可用,Producer 仍然可以发送消息 + +消费端的高可用:在 Consumer 的配置文件中,并不需要设置是从 Master Broker 读还是从 Slave 读,当 Master 不可用或者繁忙的时候,Consumer 会被自动切换到从 Slave 读。有了自动切换的机制,当一个 Master 机器出现故障后,Consumer 仍然可以从 Slave 读取消息,不影响 Consumer 程序,达到了消费端的高可用性 ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/RocketMQ-高可用.png) From ce03bf0cd7d5ff60015b878b01eba99887606afa Mon Sep 17 00:00:00 2001 From: Seazean Date: 2022年5月15日 16:14:33 +0800 Subject: [PATCH 05/35] Update README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 211ff15..be257cf 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ 内容说明: * DB:MySQL、Redis -* Frame:Maven、Netty、RocketMQ +* Frame:Maven、Netty、RocketMQ、Zookeeper * Java:JavaSE、JVM、Algorithm * Prog:Concurrent、Network Programming * SSM:MyBatis、Spring、SpringMVC、SpringBoot From 99bf86738bd5c0d88f9707410b0d9ba565c96a6f Mon Sep 17 00:00:00 2001 From: Seazean Date: 2022年5月15日 23:25:52 +0800 Subject: [PATCH 06/35] Update Java Note --- DB.md | 468 +++++++++++------------ Frame.md | 1096 +++++++++++++++++++++++++++++++++++++++++++++++++++++- Prog.md | 4 +- Tool.md | 35 -- 4 files changed, 1317 insertions(+), 286 deletions(-) diff --git a/DB.md b/DB.md index 3ad14ec..e1e1baa 100644 --- a/DB.md +++ b/DB.md @@ -127,205 +127,6 @@ MySQL 配置: -*** - - - -### 常用工具 - -#### mysql - -mysql 不是指 mysql 服务,而是指 mysql 的客户端工具 - -```sh -mysql [options] [database] -``` - -* -u --user=name:指定用户名 -* -p --password[=name]:指定密码 -* -h --host=name:指定服务器IP或域名 -* -P --port=#:指定连接端口 -* -e --execute=name:执行SQL语句并退出,在控制台执行SQL语句,而不用连接到数据库执行 - -示例: - -```sh -mysql -h 127.0.0.1 -P 3306 -u root -p -mysql -uroot -p2143 db01 -e "select * from tb_book"; -``` - - - -*** - - - -#### admin - -mysqladmin 是一个执行管理操作的客户端程序,用来检查服务器的配置和当前状态、创建并删除数据库等 - -通过 `mysqladmin --help` 指令查看帮助文档 - -```sh -mysqladmin -uroot -p2143 create 'test01'; -``` - - - -*** - - - -#### binlog - -服务器生成的日志文件以二进制格式保存,如果需要检查这些文本,就要使用 mysqlbinlog 日志管理工具 - -```sh -mysqlbinlog [options] log-files1 log-files2 ... -``` - -* -d --database=name:指定数据库名称,只列出指定的数据库相关操作 - -* -o --offset=#:忽略掉日志中的前 n 行命令。 - -* -r --result-file=name:将输出的文本格式日志输出到指定文件。 - -* -s --short-form:显示简单格式,省略掉一些信息。 - -* --start-datatime=date1 --stop-datetime=date2:指定日期间隔内的所有日志 - -* --start-position=pos1 --stop-position=pos2:指定位置间隔内的所有日志 - - - -*** - - - -#### dump - -##### 命令介绍 - -mysqldump 客户端工具用来备份数据库或在不同数据库之间进行数据迁移,备份内容包含创建表,及插入表的 SQL 语句 - -```sh -mysqldump [options] db_name [tables] -mysqldump [options] --database/-B db1 [db2 db3...] -mysqldump [options] --all-databases/-A -``` - -连接选项: - -* -u --user=name:指定用户名 -* -p --password[=name]:指定密码 -* -h --host=name:指定服务器 IP 或域名 -* -P --port=#:指定连接端口 - -输出内容选项: - -* --add-drop-database:在每个数据库创建语句前加上 Drop database 语句 -* --add-drop-table:在每个表创建语句前加上 Drop table 语句 , 默认开启,不开启 (--skip-add-drop-table) -* -n --no-create-db:不包含数据库的创建语句 -* -t --no-create-info:不包含数据表的创建语句 -* -d --no-data:不包含数据 -* -T, --tab=name:自动生成两个文件:一个 .sql 文件,创建表结构的语句;一个 .txt 文件,数据文件,相当于 select into outfile - -示例: - -```sh -mysqldump -uroot -p2143 db01 tb_book --add-drop-database --add-drop-table> a -mysqldump -uroot -p2143 -T /tmp test city -``` - - - -*** - - - -##### 数据备份 - -命令行方式: - -* 备份命令:mysqldump -u root -p 数据库名称> 文件保存路径 -* 恢复 - 1. 登录MySQL数据库:`mysql -u root p` - 2. 删除已经备份的数据库 - 3. 重新创建与备份数据库名称相同的数据库 - 4. 使用该数据库 - 5. 导入文件执行:`source 备份文件全路径` - -更多方式参考:https://time.geekbang.org/column/article/81925 - -图形化界面: - -* 备份 - - ![图形化界面备份](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/图形化界面备份.png) - -* 恢复 - - ![图形化界面恢复](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/图形化界面恢复.png) - - - - - -*** - - - -#### import - -mysqlimport 是客户端数据导入工具,用来导入mysqldump 加 -T 参数后导出的文本文件 - -```sh -mysqlimport [options] db_name textfile1 [textfile2...] -``` - -示例: - -```sh -mysqlimport -uroot -p2143 test /tmp/city.txt -``` - -导入 sql 文件,可以使用 MySQL 中的 source 指令 : - -```mysql -source 文件全路径 -``` - - - -*** - - - -#### show - -mysqlshow 客户端对象查找工具,用来很快地查找存在哪些数据库、数据库中的表、表中的列或者索引 - -```sh -mysqlshow [options] [db_name [table_name [col_name]]] -``` - -* --count:显示数据库及表的统计信息(数据库,表 均可以不指定) - -* -i:显示指定数据库或者指定表的状态信息 - -示例: - -```sh -#查询每个数据库的表的数量及表中记录的数量 -mysqlshow -uroot -p1234 --count -#查询test库中每个表中的字段书,及行数 -mysqlshow -uroot -p1234 test --count -#查询test库中book表的详细情况 -mysqlshow -uroot -p1234 test book --count -``` - - - *** @@ -762,6 +563,209 @@ KILL CONNECTION id +*** + + + +### 常用工具 + +#### mysql + +mysql 不是指 mysql 服务,而是指 mysql 的客户端工具 + +```sh +mysql [options] [database] +``` + +* -u --user=name:指定用户名 +* -p --password[=name]:指定密码 +* -h --host=name:指定服务器IP或域名 +* -P --port=#:指定连接端口 +* -e --execute=name:执行SQL语句并退出,在控制台执行SQL语句,而不用连接到数据库执行 + +示例: + +```sh +mysql -h 127.0.0.1 -P 3306 -u root -p +mysql -uroot -p2143 db01 -e "select * from tb_book"; +``` + + + +*** + + + +#### admin + +mysqladmin 是一个执行管理操作的客户端程序,用来检查服务器的配置和当前状态、创建并删除数据库等 + +通过 `mysqladmin --help` 指令查看帮助文档 + +```sh +mysqladmin -uroot -p2143 create 'test01'; +``` + + + +*** + + + +#### binlog + +服务器生成的日志文件以二进制格式保存,如果需要检查这些文本,就要使用 mysqlbinlog 日志管理工具 + +```sh +mysqlbinlog [options] log-files1 log-files2 ... +``` + +* -d --database=name:指定数据库名称,只列出指定的数据库相关操作 + +* -o --offset=#:忽略掉日志中的前 n 行命令。 + +* -r --result-file=name:将输出的文本格式日志输出到指定文件。 + +* -s --short-form:显示简单格式,省略掉一些信息。 + +* --start-datatime=date1 --stop-datetime=date2:指定日期间隔内的所有日志 + +* --start-position=pos1 --stop-position=pos2:指定位置间隔内的所有日志 + + + +*** + + + +#### dump + +##### 命令介绍 + +mysqldump 客户端工具用来备份数据库或在不同数据库之间进行数据迁移,备份内容包含创建表,及插入表的 SQL 语句 + +```sh +mysqldump [options] db_name [tables] +mysqldump [options] --database/-B db1 [db2 db3...] +mysqldump [options] --all-databases/-A +``` + +连接选项: + +* -u --user=name:指定用户名 +* -p --password[=name]:指定密码 +* -h --host=name:指定服务器 IP 或域名 +* -P --port=#:指定连接端口 + +输出内容选项: + +* --add-drop-database:在每个数据库创建语句前加上 Drop database 语句 +* --add-drop-table:在每个表创建语句前加上 Drop table 语句 , 默认开启,不开启 (--skip-add-drop-table) +* -n --no-create-db:不包含数据库的创建语句 +* -t --no-create-info:不包含数据表的创建语句 +* -d --no-data:不包含数据 +* -T, --tab=name:自动生成两个文件:一个 .sql 文件,创建表结构的语句;一个 .txt 文件,数据文件,相当于 select into outfile + +示例: + +```sh +mysqldump -uroot -p2143 db01 tb_book --add-drop-database --add-drop-table> a +mysqldump -uroot -p2143 -T /tmp test city +``` + + + +*** + + + +##### 数据备份 + +命令行方式: + +* 备份命令:mysqldump -u root -p 数据库名称> 文件保存路径 +* 恢复 + 1. 登录MySQL数据库:`mysql -u root p` + 2. 删除已经备份的数据库 + 3. 重新创建与备份数据库名称相同的数据库 + 4. 使用该数据库 + 5. 导入文件执行:`source 备份文件全路径` + +更多方式参考:https://time.geekbang.org/column/article/81925 + +图形化界面: + +* 备份 + + ![图形化界面备份](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/图形化界面备份.png) + +* 恢复 + + ![图形化界面恢复](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/图形化界面恢复.png) + + + + + +*** + + + +#### import + +mysqlimport 是客户端数据导入工具,用来导入mysqldump 加 -T 参数后导出的文本文件 + +```sh +mysqlimport [options] db_name textfile1 [textfile2...] +``` + +示例: + +```sh +mysqlimport -uroot -p2143 test /tmp/city.txt +``` + +导入 sql 文件,可以使用 MySQL 中的 source 指令 : + +```mysql +source 文件全路径 +``` + + + +*** + + + +#### show + +mysqlshow 客户端对象查找工具,用来很快地查找存在哪些数据库、数据库中的表、表中的列或者索引 + +```sh +mysqlshow [options] [db_name [table_name [col_name]]] +``` + +* --count:显示数据库及表的统计信息(数据库,表 均可以不指定) + +* -i:显示指定数据库或者指定表的状态信息 + +示例: + +```sh +#查询每个数据库的表的数量及表中记录的数量 +mysqlshow -uroot -p1234 --count +#查询test库中每个表中的字段书,及行数 +mysqlshow -uroot -p1234 test --count +#查询test库中book表的详细情况 +mysqlshow -uroot -p1234 test book --count +``` + + + + + + + **** @@ -8866,7 +8870,7 @@ Redis (REmote DIctionary Server) :用 C 语言开发的一个开源的高性 sudo apt install redis-server ``` -* 检查Redis状态 +* 检查 Redis 状态 ```sh sudo systemctl status redis-server @@ -9017,7 +9021,7 @@ Redis (REmote DIctionary Server) :用 C 语言开发的一个开源的高性 logfile filename ``` -注意:日志级别开发期设置为 verbose 即可,生产环境中配置为 notice,简化日志输出量,降低写日志IO的频度 +注意:日志级别开发期设置为 verbose 即可,生产环境中配置为 notice,简化日志输出量,降低写日志 IO 的频度 @@ -14127,7 +14131,7 @@ PSYNC 命令的调用方法有两种 # Replication role:master connected_slaves:2 -slave0: ip=127.0.0.1,port=11111,state=online,offset=123,lag=O # 刚刚发送过 REPLCONF ACK +slave0: ip=127.0.0.1,port=11111,state=online,offset=123,lag=0 # 刚刚发送过 REPLCONF ACK slavel: ip=127.0.0.1,port=22222,state=online,offset=456,lag=3 # 3秒之前发送过REPLCONF ACK ``` @@ -14162,7 +14166,7 @@ min-slaves-max-lag 10 检测命令丢失:由于网络或者其他原因,主服务器传播给从服务器的写命令丢失,那么当从服务器向主服务器发送 REPLCONF ACK 命令时,主服务器会检查从服务器的复制偏移量是否小于自己的,然后在复制积压缓冲区里找到从服务器缺少的数据,并将这些数据重新发送给从服务器 -说明:REPLCONF ACK 命令和复制积压缓冲区都是 Redis 2.8 版本新增的,在 Redis 2.8 版本以前,即使命令在传播过程中丢失,主从服务器都不会注意到,也不会向从服务器补发丢失的数据,所以为了保证主从复制的数据一致性,最好使用 2.8 或以上版本的 Redis +说明:REPLCONF ACK 命令和复制积压缓冲区都是 Redis 2.8 版本新增的,在 Redis 2.8 版本以前,即使命令在传播过程中丢失,主从服务器都不会注意到,也不会向从服务器补发丢失的数据,所以为了保证**主从复制的数据一致性**,最好使用 2.8 或以上版本的 Redis @@ -14910,7 +14914,7 @@ typedef struct clusterLink { ``` * redisClient 结构中的套接宇和缓冲区是用于连接客户端的 -* clusterLink结构中的套接宇和缓冲区则是用于连接节点的 +* clusterLink 结构中的套接宇和缓冲区则是用于连接节点的 @@ -15028,6 +15032,32 @@ typedef struct clusterState { +*** + + + +#### 集群数据 + +集群节点保存键值对以及键值对过期时间的方式,与单机 Redis 服务器保存键值对以及键值对过期时间的方式完全相同,但是集群节点只能使用 0 号数据库,单机服务器可以任意使用 + +除了将键值对保存在数据库里面之外,节点还会用 clusterState 结构中的 slots_to_keys 跳跃表来**保存槽和键之间的关系** + +```c +typedef struct clusterState { + // ... + zskiplist *slots_to_keys; +} +``` + +slots_to_keys 跳跃表每个节点的分值(score)都是一个槽号,而每个节点的成员(member)都是一个数据库键(按槽号升序) + +* 当节点往数据库中添加一个新的键值对时,节点就会将这个键以及键的槽号关联到 slots_to_keys 跳跃表 +* 当节点删除数据库中的某个键值对时,节点就会在 slots_to_keys 跳跃表解除被删除键与槽号的关联 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-槽和键跳跃表.png) + +通过在 slots_to_keys 跳跃表中记录各个数据库键所属的槽,可以很方便地对属于某个或某些槽的所有数据库键进行批量操作,比如 `CLUSTER GETKEYSINSLOT ` 命令返回最多 count 个属于槽 slot 的数据库键,就是通过该跳表实现 + *** @@ -15118,36 +15148,6 @@ $ redis-cli -c -p 6379 #集群模式 -*** - - - -### 集群数据 - -集群节点保存键值对以及键值对过期时间的方式,与单机 Redis 服务器保存键值对以及键值对过期时间的方式完全相同,但是集群节点只能使用 0 号数据库,单机服务器可以任意使用 - -除了将键值对保存在数据库里面之外,节点还会用 clusterState 结构中的 slots_to_keys 跳跃表来保存**槽和键**之间的关系 - -```c -typedef struct clusterState { - // ... - zskiplist *slots_to_keys; -} -``` - -slots_to_keys 跳跃表每个节点的分值(score)都是一个槽号,而每个节点的成员(member)都是一个数据库键(按槽号升序) - -* 当节点往数据库中添加一个新的键值对时,节点就会将这个键以及键的槽号关联到 slots_to_keys 跳跃表 -* 当节点删除数据库中的某个键值对时,节点就会在 slots_to_keys 跳跃表解除被删除键与槽号的关联 - -![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-槽和键跳跃表.png) - -通过在 slots_to_keys 跳跃表中记录各个数据库键所属的槽,可以很方便地对属于某个或某些槽的所有数据库键进行批量操作,比如 `CLUSTER GETKEYSINSLOT ` 命令返回最多 count 个属于槽 slot 的数据库键,就是通过该跳表实现 - - - - - *** diff --git a/Frame.md b/Frame.md index 67c8500..7baad52 100644 --- a/Frame.md +++ b/Frame.md @@ -270,16 +270,6 @@ Path 下配置:`%MAVEN_HOME%\bin` -### 插件构建 - -![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Maven-插件构建.png) - - - -*** - - - ### IDEA搭建 #### 不用原型 @@ -502,9 +492,7 @@ Maven 的构建生命周期描述的是一次构建过程经历了多少个事 * clean:移除上一次构建产生的所有文件 * post-clean:执行一些在 clean 之后立刻完成的工作 -* default:核心工作,例如编译,测试,打包,部署等 - - 对于 default 生命周期,每个事件在执行之前都会**将之前的所有事件依次执行一遍** +* default:核心工作,例如编译,测试,打包,部署等,每个事件在执行之前都会**将之前的所有事件依次执行一遍** ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Maven-default生命周期.png) @@ -1309,7 +1297,7 @@ Log4j 是 Apache 的一个开源项目。使用 Log4j,通过一个配置文件 + 格式:log4j.rootLogger=日志级别,appenderName1,appenderName2,... + 日志级别:常见的五个级别:**DEBUG < INFO < WARN < ERROR < FATAL**(可以自定义) - Log4j规则:只输出级别不低于设定级别的日志信息 + Log4j 规则:只输出级别不低于设定级别的日志信息 + appenderName1:指定日志信息要输出地址。可以同时指定多个输出目的地,用逗号隔开: @@ -1353,7 +1341,7 @@ Log4j 是 Apache 的一个开源项目。使用 Log4j,通过一个配置文件 * log4j 的配置文件,名字为 log4j.properties, 放在 src 根目录下 ```properties - log4j.rootLogger=debug,my,fileAppender + log4j.rootLogger=I ### direct log messages to my ### log4j.appender.my=org.apache.log4j.ConsoleAppender @@ -10246,6 +10234,1084 @@ ConsumeRequest 是 ConsumeMessageOrderlyService 的内部类,是一个 Runnabl +*** + + + + + + + +# Zookeeper + +## 基本介绍 + +### 框架特征 + +Zookeeper 是 Apache Hadoop 项目子项目,为分布式框架提供协调服务,是一个树形目录服务 + +Zookeeper 是基于观察者模式设计的分布式服务管理框架,负责存储和管理共享数据,接受观察者的注册监控,一旦这些数据的状态发生变化,Zookeeper 会通知观察者 + +* Zookeeper 是一个领导者(Leader),多个跟随者(Follower)组成的集群 +* 集群中只要有半数以上节点存活就能正常服务,所以 Zookeeper 适合部署奇数台服务器 +* **全局数据一致**,每个 Server 保存一份相同的数据副本,Client 无论连接到哪个 Server,数据都是一致 +* 更新的请求顺序执行,来自同一个 Client 的请求按其发送顺序依次执行 +* 数据更新原子性,一次数据更新要么成功,要么失败 +* 实时性,在一定的时间范围内,Client 能读到最新数据 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Zookeeper-框架结构.png) + + + +参考视频:https://www.bilibili.com/video/BV1to4y1C7gw + + + + + +*** + + + +### 应用场景 + +Zookeeper 提供的主要功能包括:统一命名服务、统一配置管理、统一集群管理、服务器节点动态上下线、软负载均衡、分布式锁等 + +* 在分布式环境中,经常对应用/服务进行统一命名,便于识别,例如域名相对于 IP 地址更容易被接收 + + ```sh + /service/www.baidu.com # 节点路径 + 192.168.1.1 192.168.1.2 # 节点值 + ``` + + 如果在节点中记录每台服务器的访问数,让访问数最少的服务器去处理最新的客户端请求,可以实现负载均衡 + + ```sh + 192.168.1.1 10 # 次数 + 192.168.1.1 15 + ``` + +* 配置文件同步可以通过 Zookeeper 实现,将配置信息写入某个 ZNode,其他客户端监视该节点,当节点数据被修改,通知各个客户端服务器 + +* 集群环境中,需要实时掌握每个集群节点的状态,可以将这些信息放入 ZNode,通过监控通知的机制实现 + +* 实现客户端实时观察服务器上下线的变化 + + + + + +*** + + + + + +## 基本操作 + +### 安装搭建 + +安装步骤: + +* 安装 JDK + +* 拷贝 apache-zookeeper-3.5.7-bin.tar.gz 安装包到 Linux 系统下,并解压到指定目录 + +* conf 目录下的配置文件重命名: + + ``` + mv zoo_sample.cfg zoo.cfg + ``` + +* 修改配置文件: + + ```sh + vim zoo.cfg + # 修改内容 + dataDir=/home/seazean/SoftWare/zookeeper-3.5.7/zkData + ``` + +* 在对应目录创建 zkData 文件夹: + + ```sh + mkdir zkData + ``` + +Zookeeper 中的配置文件 zoo.cfg 中参数含义解读: + +* tickTime = 2000:通信心跳时间,Zookeeper 服务器与客户端心跳时间,单位毫秒 +* initLimit = 10:Leader 与 Follower 初始通信时限,初始连接时能容忍的最多心跳次数 +* syncLimit = 5:Leader 与 Follower 同步通信时限,LF 通信时间超过 `syncLimit * tickTime`,Leader 认为 Follwer 下线 +* dataDir:保存 Zookeeper 中的数据目录,默认是 tmp目录,容易被 Linux 系统定期删除,所以建议修改 +* clientPort = 2181:客户端连接端口,通常不做修改 + + + +*** + + + +### 操作命令 + +#### 服务端 + +Linux 命令: + +* 启动 ZooKeeper 服务:`./zkServer.sh start` + +* 查看 ZooKeeper 服务:`./zkServer.sh status` + +* 停止 ZooKeeper 服务:`./zkServer.sh stop` + +* 重启 ZooKeeper 服务:`./zkServer.sh restart ` + +* 查看进程是否启动:`jps` + + + + + +*** + + + +#### 客户端 + +Linux 命令: + +* 连接 ZooKeeper 服务端: + + ```sh + ./zkCli.sh # 直接启动 + ./zkCli.sh –server ip:port # 指定 host 启动 + ``` + +客户端命令: + +* 基础操作: + + ```sh + quit # 停止连接 + help # 查看命令帮助 + ``` + +* 创建命令:**`/` 代表根目录** + + ```sh + create /path value # 创建节点,value 可选 + create -e /path value # 创建临时节点 + create -s /path value # 创建顺序节点 + create -es /path value # 创建临时顺序节点,比如node10000012 删除12后也会继续从13开始,只会增加 + ``` + +* 查询命令: + + ```sh + ls /path # 显示指定目录下子节点 + ls –s /path # 查询节点详细信息 + ls –w /path # 监听子节点数量的变化 + stat /path # 查看节点状态 + get –s /path # 查询节点详细信息 + get –w /path # 监听节点数据的变化 + ``` + + ```sh + # 属性,分为当前节点的属性和子节点属性 + czxid: 节点被创建的事务ID, 是ZooKeeper中所有修改总的次序,每次修改都有唯一的 zxid,谁小谁先发生 + ctime: 被创建的时间戳 + mzxid: 最后一次被更新的事务ID + mtime: 最后修改的时间戳 + pzxid: 子节点列表最后一次被更新的事务ID + cversion: 子节点的变化号,修改次数 + dataversion: 节点的数据变化号,数据的变化次数 + aclversion: 节点的访问控制列表变化号 + ephemeralOwner: 用于临时节点,代表节点拥有者的 session id,如果为持久节点则为0 + dataLength: 节点存储的数据的长度 + numChildren: 当前节点的子节点数量 + ``` + +* 删除命令: + + ```sh + delete /path # 删除节点 + deleteall /path # 递归删除节点 + ``` + + + +*** + + + +### 数据结构 + +ZooKeeper 是一个树形目录服务,类似 Unix 的文件系统,每一个节点都被称为 ZNode,每个 ZNode 默认存储 1MB 的数据,节点上会保存数据和节点信息,每个 ZNode 都可以通过其路径唯一标识 + +节点可以分为四大类: + +* PERSISTENT:持久化节点 +* EPHEMERAL:临时节点,客户端和服务器端**断开连接**后,创建的节点删除 +* PERSISTENT_SEQUENTIAL:持久化顺序节点,创建 znode 时设置顺序标识,节点名称后会附加一个值,**顺序号是一个单调递增的计数器**,由父节点维护 +* EPHEMERAL_SEQUENTIAL:临时顺序节点 + +注意:在分布式系统中,顺序号可以被用于为所有的事件进行全局排序,这样客户端可以通过顺序号推断事件的顺序 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Zookeeper-节点树形结构.png) + + + +*** + + + +### 代码实现 + +添加 Maven 依赖: + +```xml + + org.apache.zookeeper + zookeeper + 3.5.7 + +``` + +实现代码: + +```java +public static void main(String[] args) { + // 参数一:连接地址 + // 参数二:会话超时时间 + // 参数三:监听器 + ZooKeeper zkClient = new ZooKeeper("192.168.3.128:2181", 20000, new Watcher() { + @Override + public void process(WatchedEvent event) { + System.out.println("监听处理函数"); + } + }); +} +``` + + + + + + + +*** + + + + + +## 集群介绍 + +### 相关概念 + +Zookeepe 集群三个角色: + +* Leader 领导者:处理客户端**事务请求**,负责集群内部各服务器的调度 + +* Follower 跟随者:处理客户端非事务请求,转发事务请求给 Leader 服务器,参与 Leader 选举投票 + +* Observer 观察者:观察集群的最新状态的变化,并将这些状态进行同步;处理非事务性请求,事务性请求会转发给 Leader 服务器进行处理;不会参与任何形式的投票。只提供非事务性的服务,通常用于在不影响集群事务处理能力的前提下,提升集群的非事务处理能力(提高集群读的能力,但是也降低了集群选主的复杂程度) + + +相关属性: + +* SID:服务器 ID,用来唯一标识一台集群中的机器,和 myid 一致 +* ZXID:事务ID,用来标识一次服务器状态的变更,在某一时刻集群中每台机器的 ZXID 值不一定完全一致,这和 ZooKeeper 服务器对于客户端更新请求的处理逻辑有关 + +* Epoch:每个 Leader 任期的代号,同一轮选举投票过程中的该值是相同的,投完一次票就增加 + +选举机制:半数机制,超过半数的投票旧通过 + +* 第一次启动选举规则:投票过半数时,服务器 ID 大的胜出 + +* 第二次启动选举规则: + * EPOCH 大的直接胜出 + * EPOCH 相同,事务 ID 大的胜出(事务 ID 越大,数据越新) + * 事务 ID 相同,服务器 ID 大的胜出 + + + + + +*** + + + +### 初次选举 + +选举过程: + +* 服务器 1 启动,发起一次选举,服务器 1 投自己一票,票数不超过半数,选举无法完成,服务器 1 状态保持为 LOOKING +* 服务器 2 启动,再发起一次选举,服务器 1 和 2 分别投自己一票并**交换选票信息**,此时服务器 1 会发现服务器 2 的 SID 比自己投票推举的(服务器 1)大,更改选票为推举服务器 2。投票结果为服务器 1 票数 0 票,服务器 2 票数 2 票,票数不超过半数,选举无法完成,服务器 1、2 状态保持 LOOKING +* 服务器 3 启动,发起一次选举,此时服务器 1 和 2 都会更改选票为服务器 3,投票结果为服务器 3 票数 3 票,此时服务器 3 的票数已经超过半数,服务器 3 当选 Leader,服务器 1、2 更改状态为 FOLLOWING,服务器 3 更改状态为 LEADING +* 服务器 4 启动,发起一次选举,此时服务器 1、2、3 已经不是 LOOKING 状态,不会更改选票信息,交换选票信息结果后服务器 3 为 3 票,服务器 4 为 1 票,此时服务器 4 更改选票信息为服务器 3,并更改状态为 FOLLOWING +* 服务器 5 启动,同 4 一样 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Zookeeper-初次选举机制.png) + + + +*** + + + +### 再次选举 + +ZooKeeper 集群中的一台服务器出现以下情况之一时,就会开始进入 Leader 选举: + +* 服务器初始化启动 +* 服务器运行期间无法和 Leader 保持连接 + +当一台服务器进入 Leader 选举流程时,当前集群可能会处于以下两种状态: + +* 集群中本来就已经存在一个 Leader,服务器试图去选举 Leader 时会被告知当前服务器的 Leader 信息,对于该服务器来说,只需要和 Leader 服务器建立连接,并进行状态同步即可 + +* 集群中确实不存在 Leader,假设服务器 3 和 5 出现故障,开始进行 Leader 选举,SID 为 1、2、4 的机器投票情况 + + ```sh + (EPOCH,ZXID,SID): (1, 8, 1), (1, 8, 2), (1, 7, 4) + ``` + + 根据选举规则,服务器 2 胜出 + + + +*** + + + +### 数据写入 + +写操作就是事务请求,写入请求直接发送给 Leader 节点:Leader 会先将数据写入自身,同时通知其他 Follower 写入,**当集群中有半数以上节点写入完成**,Leader 节点就会响应客户端数据写入完成 + + + +写入请求直接发送给 Follower 节点:Follower 没有写入权限,会将写请求转发给 Leader,Leader 将数据写入自身,通知其他 Follower 写入,当集群中有半数以上节点写入完成,Leader 会通知 Follower 写入完成,**由 Follower 响应客户端数据写入完成** + + + + + + + +**** + + + + + +## 底层协议 + +### Paxos + +Paxos 算法:基于消息传递且具有高度容错特性的一致性算法 + +优点:快速正确的在一个分布式系统中对某个数据值达成一致,并且保证不论发生任何异常,都不会破坏整个系统的一致性 + +缺陷:在网络复杂的情况下,可能很久无法收敛,甚至陷入活锁的情况 + + + +*** + + + +### ZAB + +#### 算法介绍 + +ZAB 协议借鉴了 Paxos 算法,是为 Zookeeper 设计的支持崩溃恢复的原子广播协议,基于该协议 Zookeeper 设计为只有一台客户端(Leader)负责处理外部的写事务请求,然后 Leader 将数据同步到其他 Follower 节点 + +Zab 协议包括两种基本的模式:消息广播、崩溃恢复 + + + +*** + + + +#### 消息广播 + +ZAB 协议针对事务请求的处理过程类似于一个**两阶段提交**过程:广播事务阶段、广播提交操作 + +* 客户端发起写操作请求,Leader 服务器将请求转化为事务 Proposal 提案,同时为 Proposal 分配一个全局的 ID,即 ZXID +* Leader 服务器为每个 Follower 分配一个单独的队列,将广播的 Proposal **依次放到队列**中去,根据 FIFO 策略进行消息发送 +* Follower 接收到 Proposal 后,将其以事务日志的方式写入本地磁盘中,写入成功后向 Leader 反馈一个 ACK 响应消息 +* Leader 接收到超过半数以上 Follower 的 ACK 响应消息后,即认为消息发送成功,可以发送 Commit 消息 +* Leader 向所有 Follower 广播 commit 消息,同时自身也会完成事务提交,Follower 接收到 Commit 后,将上一条事务提交 + + + +两阶段提交模型可能因为 Leader 宕机带来数据不一致: + +* Leader 发起一个事务 Proposal 后就宕机,Follower 都没有 Proposal +* Leader 收到半数 ACK 宕机,没来得及向 Follower 发送 Commit + + + +*** + + + +#### 崩溃恢复 + +Leader 服务器出现崩溃或者由于网络原因导致 Leader 服务器失去了与**过半 Follower的联系**,那么就会进入崩溃恢复模式,崩溃恢复主要包括两部分:Leader 选举和数据恢复 + +Zab 协议崩溃恢复要求满足以下两个要求: + +* 已经被 Leader 提交的提案 Proposal,必须最终被所有的 Follower 服务器正确提交 +* 丢弃已经被 Leader 提出的,但是没有被提交的 Proposal + +Zab 协议需要保证选举出来的 Leader 需要满足以下条件: + +* 新选举的 Leader 不能包含未提交的 Proposal,即新 Leader 必须都是已经提交了 Proposal 的 Follower 节点 +* 新选举的 Leader 节点含有**最大的 ZXID**,可以避免 Leader 服务器检查 Proposal 的提交和丢弃工作 + + + +数据恢复阶段: + +* 完成 Leader 选举后,在正式开始工作之前(接收事务请求提出新的 Proposal),Leader 服务器会首先确认事务日志中的所有 Proposal 是否已经被集群中过半的服务器 Commit +* Leader 服务器需要确保所有的 Follower 服务器能够接收到每一条事务的 Proposal,并且能将所有已经提交的事务 Proposal 应用到内存数据中,所以只有当 Follower 将所有尚未同步的事务 Proposal 都**从 Leader 服务器上同步**,并且应用到内存数据后,Leader 才会把该 Follower 加入到真正可用的 Follower 列表中 + + + +**** + + + +#### 异常处理 + +Zab 的事务编号 zxid 设计: + +* zxid 是一个 64 位的数字,低 32 位是一个简单的单增计数器,针对客户端每一个事务请求,Leader 在产生新的 Proposal 事务时,都会对该计数器加 1,而高 32 位则代表了 Leader 周期的 epoch 编号 +* epoch 为当前集群所处的代或者周期,每次 Leader 变更后都会在 epoch 的基础上加 1,Follower 只服从 epoch 最高的 Leader 命令,所以旧的 Leader 崩溃恢复之后,其他 Follower 就不会继续追随 +* 每次选举产生一个新的 Leader,就会从新 Leader 服务器上取出本地事务日志中最大编号 Proposal 的 zxid,从 zxid 中解析得到对应的 epoch 编号,然后再对其加 1 后作为新的 epoch 值,并将低 32 位数字归零,由 0 开始重新生成 zxid + +Zab 协议通过 epoch 编号来区分 Leader 变化周期,能够有效避免不同的 Leader 错误的使用了相同的 zxid 编号提出了不一样的 Proposal 的异常情况 + +Zab 数据同步过程:**数据同步阶段要以 Leader 服务器为准** + +* 一个包含了上个 Leader 周期中尚未提交过的事务 Proposal 的服务器启动时,这台机器加入集群中会以 Follower 角色连上 Leader +* Leader 会根据自己服务器上最后提交的 Proposal 和 Follower 服务器的 Proposal 进行比对,让 Follower 进行一个**回退或者前进操作**,到一个已经被集群中过半机器 Commit 的最新 Proposal(源码解析部分详解) + + + + + +*** + + + +### CAP + +CAP 理论指的是在一个分布式系统中,Consistency(一致性)、Availability(可用性)、Partition Tolerance(分区容错性)不能同时成立,ZooKeeper 保证的是 CP + +* ZooKeeper 不能保证每次服务请求的可用性,在极端环境下可能会丢弃一些请求,消费者程序需要重新请求才能获得结果 +* 进行 Leader 选举时**集群都是不可用** + +CAP 三个基本需求,因为 P 是必须的,因此分布式系统选择就在 CP 或者 AP 中: + +* 一致性:指数据在多个副本之间是否能够保持数据一致的特性,当一个系统在数据一致的状态下执行更新操作后,也能保证系统的数据仍然处于一致的状态 +* 可用性:指系统提供的服务必须一直处于可用的状态,对于用户的每一个操作请求总是能够在有限的时间内返回结果 +* 分区容错性:分布式系统在遇到任何网络分区故障时,仍然能够保证对外提供服务,不会宕机,除非是整个网络环境都发生了故障 + + + + + + + +*** + + + + + +## 监听机制 + +### 实现原理 + +ZooKeeper 中引入了 Watcher 机制来实现了发布/订阅功能,客户端注册监听目录节点,在特定事件触发时,ZooKeeper 会通知所有关注该事件的客户端,保证 ZooKeeper 保存的任何的数据的任何改变都能快速的响应到监听应用程序 + +监听命令:**只能生效一次**,接收一次通知,再次监听需要重新注册 + +```sh +ls –w /path # 监听【子节点数量】的变化 +get –w /path # 监听【节点数据】的变化 +``` + +工作流程: + +* 在主线程中创建 Zookeeper 客户端,这时就会创建**两个线程**,一个负责网络连接通信(connet),一个负责监听(listener) +* 通过 connect 线程将注册的监听事件发送给 Zookeeper +* 在 Zookeeper 的注册监听器列表中将注册的监听事件添加到列表中 +* Zookeeper 监听到有数据或路径变化,将消息发送给 listener 线程 +* listener 线程内部调用 process() 方法 + +Curator 框架引入了 Cache 来实现对 ZooKeeper 服务端事件的监听,三种 Watcher: + +* NodeCache:只是监听某一个特定的节点 +* PathChildrenCache:监控一个 ZNode 的子节点 +* TreeCache:可以监控整个树上的所有节点,类似于 PathChildrenCache 和 NodeCache 的组合 + + + + + +*** + + + +### 监听案例 + +#### 整体架构 + +客户端实时监听服务器动态上下线 + + + + + +*** + + + +#### 代码实现 + +客户端:先启动客户端进行监听 + +```java +public class DistributeClient { + private String connectString = "192.168.3.128:2181"; + private int sessionTimeout = 20000; + private ZooKeeper zk; + + public static void main(String[] args) throws Exception { + DistributeClient client = new DistributeClient(); + + // 1 获取zk连接 + client.getConnect(); + + // 2 监听/servers下面子节点的增加和删除 + client.getServerList(); + + // 3 业务逻辑 + client.business(); + } + + private void business() throws InterruptedException { + Thread.sleep(Long.MAX_VALUE); + } + + private void getServerList() throws KeeperException, InterruptedException { + ArrayList servers = new ArrayList(); + // 获取所有子节点,true 代表触发监听操作 + List children = zk.getChildren("/servers", true); + + for (String child : children) { + // 获取子节点的数据 + byte[] data = zk.getData("/servers/" + child, false, null); + servers.add(new String(data)); + } + System.out.println(servers); + } + + private void getConnect() throws IOException { + zk = new ZooKeeper(connectString, sessionTimeout, new Watcher() { + @Override + public void process(WatchedEvent event) { + getServerList(); + } + }); + } +} +``` + +服务端:启动时需要 Program arguments + +```java +public class DistributeServer { + private String connectString = "192.168.3.128:2181"; + private int sessionTimeout = 20000; + private ZooKeeper zk; + + public static void main(String[] args) throws Exception { + DistributeServer server = new DistributeServer(); + + // 1 获取 zookeeper 连接 + server.getConnect(); + + // 2 注册服务器到 zk 集群,注意参数 + server.register(args[0]); + + // 3 启动业务逻辑 + server.business(); + } + + private void business() throws InterruptedException { + Thread.sleep(Long.MAX_VALUE); + } + + private void register(String hostname) throws KeeperException, InterruptedException { + // OPEN_ACL_UNSAFE: ACL开放 + // EPHEMERAL_SEQUENTIAL: 临时顺序节点 + String create = zk.create("/servers/" + hostname, hostname.getBytes(), + ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL); + System.out.println(hostname + " is online"); + } + + private void getConnect() throws IOException { + zk = new ZooKeeper(connectString, sessionTimeout, new Watcher() { + @Override + public void process(WatchedEvent event) { + } + }); + } +} +``` + + + + + +*** + + + + + +## 分布式锁 + +### 实现原理 + +分布式锁可以实现在分布式系统中多个进程有序的访问该临界资源,多个进程之间不会相互干扰 + +核心思想:当客户端要获取锁,则创建节点,使用完锁,则删除该节点 + +1. 客户端获取锁时,在 /locks 节点下创建**临时顺序**节点 + * 使用临时节点是为了防止当服务器或客户端宕机以后节点无法删除(持久节点),导致锁无法释放 + * 使用顺序节点是为了系统自动编号排序,找最小的节点,防止客户端饥饿现象,保证公平 +2. 获取 /locks 目录的所有子节点,判断自己的**子节点序号是否最小**,成立则客户端获取到锁,使用完锁后将该节点删除 + +3. 反之客户端需要找到比自己小的节点,**对其注册事件监听器,监听删除事件** +4. 客户端的 Watcher 收到删除事件通知,就会重新判断当前节点是否是子节点中序号最小,如果是则获取到了锁, 如果不是则重复以上步骤继续获取到比自己小的一个节点并注册监听 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Zookeeper-分布式锁原理.png) + + + +*** + + + +### Curator + +Curator 实现分布式锁 API,在 Curator 中有五种锁方案: + +- InterProcessSemaphoreMutex:分布式排它锁(非可重入锁) + +- InterProcessMutex:分布式可重入排它锁 + +- InterProcessReadWriteLock:分布式读写锁 + +- InterProcessMultiLock:将多个锁作为单个实体管理的容器 + +- InterProcessSemaphoreV2:共享信号量 + +```java +public class CuratorLock { + + public static CuratorFramework getCuratorFramework() { + // 重试策略对象 + ExponentialBackoffRetry policy = new ExponentialBackoffRetry(3000, 3); + // 构建客户端 + CuratorFramework client = CuratorFrameworkFactory.builder() + .connectString("192.168.3.128:2181") + .connectionTimeoutMs(2000) // 连接超时时间 + .sessionTimeoutMs(20000) // 会话超时时间 单位ms + .retryPolicy(policy) // 重试策略 + .build(); + + // 启动客户端 + client.start(); + System.out.println("zookeeper 启动成功"); + return client; + } + + public static void main(String[] args) { + // 创建分布式锁1 + InterProcessMutex lock1 = new InterProcessMutex(getCuratorFramework(), "/locks"); + + // 创建分布式锁2 + InterProcessMutex lock2 = new InterProcessMutex(getCuratorFramework(), "/locks"); + + new Thread(new Runnable() { + @Override + public void run() { + lock1.acquire(); + System.out.println("线程1 获取到锁"); + + Thread.sleep(5 * 1000); + + lock1.release(); + System.out.println("线程1 释放锁"); + } + }).start(); + + new Thread(new Runnable() { + @Override + public void run() { + lock2.acquire(); + System.out.println("线程2 获取到锁"); + + Thread.sleep(5 * 1000); + + lock2.release(); + System.out.println("线程2 释放锁"); + + } + }).start(); + } +} +``` + +```xml + + org.apache.curator + curator-framework + 4.3.0 + + + org.apache.curator + curator-recipes + 4.3.0 + + + org.apache.curator + curator-client + 4.3.0 +``` + + + + + +*** + + + + + +## 源码解析 + +### 服务端 + +服务端程序的入口 QuorumPeerMain + +```java +public static void main(String[] args) { + QuorumPeerMain main = new QuorumPeerMain(); + main.initializeAndRun(args); +} +``` + +initializeAndRun 的工作: + +* 解析启动参数 + +* 提交周期任务,定时删除过期的快照 + +* 初始化通信模型,默认是 NIO 通信 + + ```java + // QuorumPeerMain#runFromConfig + public void runFromConfig(QuorumPeerConfig config) { + // 通信信组件初始化,默认是 NIO 通信 + ServerCnxnFactory cnxnFactory = ServerCnxnFactory.createFactory(); + // 初始化NIO 服务端socket,绑定2181 端口,可以接收客户端请求 + cnxnFactory.configure(config.getClientPortAddress(), config.getMaxClientCnxns(), false); + // 启动 zk + quorumPeer.start(); + } + ``` + +* 启动 zookeeper + + ```java + // QuorumPeer#start + public synchronized void start() { + if (!getView().containsKey(myid)) { + throw new RuntimeException("My id " + myid + " not in the peer list"); + } + // 冷启动数据恢复,将快照中数据恢复到 DataTree + loadDataBase(); + // 启动通信工厂实例对象 + startServerCnxnFactory(); + try { + adminServer.start(); + } catch (AdminServerException e) { + LOG.warn("Problem starting AdminServer", e); + System.out.println(e); + } + // 准备选举环境 + startLeaderElection(); + // 执行选举 + super.start(); + } + ``` + + + + + +*** + + + +### 选举机制 + +#### 环境准备 + +QuorumPeer#startLeaderElection 初始化选举环境: + +```java +synchronized public void startLeaderElection() { + try { + // Looking 状态,需要选举 + if (getPeerState() == ServerState.LOOKING) { + // 选票组件: myid (serverid), zxid, epoch + // 开始选票时,serverid 是自己,【先投自己】 + currentVote = new Vote(myid, getLastLoggedZxid(), getCurrentEpoch()); + } + } + if (electionType == 0) { + try { + udpSocket = new DatagramSocket(getQuorumAddress().getPort()); + // 响应投票结果线程 + responder = new ResponderThread(); + responder.start(); + } catch (SocketException e) { + throw new RuntimeException(e); + } + } + // 创建选举算法实例 + this.electionAlg = createElectionAlgorithm(electionType); +} +``` + +```java +// zk总的发送和接收队列准备好 +protected Election createElectionAlgorithm(int electionAlgorithm){ + // 负责选举过程中的所有网络通信,创建各种队列和集合 + QuorumCnxManager qcm = createCnxnManager(); + QuorumCnxManager.Listener listener = qcm.listener; + if(listener != null){ + // 启动监听线程, 调用 client = ss.accept()阻塞,等待处理请求 + listener.start(); + // 准备好发送和接收队列准备 + FastLeaderElection fle = new FastLeaderElection(this, qcm); + // 启动选举线程,【WorkerSender 和 WorkerReceiver】 + fle.start(); + le = fle; + } +} +``` + + + +*** + + + +#### 选举源码 + +当 Zookeeper 启动后,首先都是 Looking 状态,通过选举让其中一台服务器成为 Leader + +执行 `super.start()` 相当于执行 `QuorumPeer#run()` 方法 + +```java +public void run() { + case LOOKING: + // 进行选举,选举结束返回最终成为 Leader 胜选的那张选票 + setCurrentVote(makeLEStrategy().lookForLeader()); +} +``` + +FastLeaderElection 类: + +* lookForLeader:选举 + + ```java + public Vote lookForLeader() { + // 正常启动中其他服务器都会向我发送一个投票,保存每个服务器的最新合法有效的投票 + HashMap recvset = new HashMap(); + // 存储合法选举之外的投票结果 + HashMap outofelection = new HashMap(); + // 一次选举的最大等待时间,默认值是0.2s + int notTimeout = finalizeWait; + // 每发起一轮选举,logicalclock++,在没有合法的epoch 数据之前,都使用逻辑时钟代替 + synchronized(this){ + // 更新逻辑时钟,每进行一次选举,都需要更新逻辑时钟 + logicalclock.incrementAndGet(); + // 更新选票(serverid, zxid, epoch) + updateProposal(getInitId(), getInitLastLoggedZxid(), getPeerEpoch()); + } + // 广播选票,把自己的选票发给其他服务器 + sendNotifications(); + // 一轮一轮的选举直到选举成功 + while ((self.getPeerState() == ServerState.LOOKING) && (!stop)){ } + } + ``` + +* sendNotifications:广播选票 + + ```java + private void sendNotifications() { + // 遍历投票参与者,给每台服务器发送选票 + for (long sid : self.getCurrentAndNextConfigVoters()) { + // 创建发送选票 + ToSend notmsg = new ToSend(...); + // 把发送选票放入发送队列 + sendqueue.offer(notmsg); + } + } + ``` + +FastLeaderElection 中有 WorkerSender 线程: + +* `ToSend m = sendqueue.poll(3000, TimeUnit.MILLISECONDS)`:**阻塞获取要发送的选票** + +* `process(m)`:处理要发送的选票 + + `manager.toSend(m.sid, requestBuffer)`:发送选票 + + * `if (this.mySid == sid)`:如果**消息的接收者 sid 是自己**,直接进入自己的 RecvQueue(自己投自己) + + * `else`:如果接收者是其他服务器,创建对应的发送队列或者复用已经存在的发送队列,把消息放入该队列 + + * `connectOne(sid)`:建立连接 + + * `sock.connect(electionAddr, cnxTO)`:建立与 sid 服务器的连接 + + * `initiateConnection(sock, sid)`:初始化连接 + + `startConnection(sock, sid)`:创建并启动发送器线程和接收器线程 + + * `dout = new DataOutputStream(buf)`:**获取 Socket 输出流**,向服务器发送数据 + * `din = new DataInputStream(new BIS(sock.getInputStream())))`:通过输入流读取对方发送过来的选票 + * `if (sid> self.getId())`:接收者 sid 比我的大,没有资格给对方发送连接请求的,直接关闭自己的客户端 + * `SendWorker sw`:初始化发送器,并启动发送器线程,线程 run 方法 + * `while (running && !shutdown && sock != null)`:连接没有断开就一直运行 + * `ByteBuffer b = pollSendQueue()`:从发送队列 SendQueue 中获取发送消息 + * `lastMessageSent.put(sid, b)`:更新对于 sid 这台服务器的最近一条消息 + * `send(b)`:**执行发送** + * `RecvWorker rw`:初始化接收器,并启动接收器线程 + * `din.readFully(msgArray, 0, length)`:输入流接收消息 + * `addToRecvQueue(new Message(messagg, sid))`:将消息放入接收消息 recvQueue 队列 + +FastLeaderElection 中有 WorkerReceiver 线程 + +* `response = manager.pollRecvQueue()`:从 RecvQueue 中**阻塞获取出选举投票消息**(其他服务器发送过来的) + + + + + + + +*** + + + +### 状态同步 + +选举结束后,每个节点都需要根据角色更新自己的状态,Leader 更新状态为 Leader,其他节点更新状态为 Follower,整体流程: + +* Follower 需要让 Leader 知道自己的状态 (sid, epoch, zxid) +* Leader 接收到信息,**根据信息构建新的 epoch**,要返回对应的信息给 Follower,Follower 更新自己的 epoch +* Leader 需要根据 Follower 的状态,确定何种方式的数据同步 DIFF、TRUNC、SNAP,就是要**以 Leader 服务器数据为准** + * DIFF:Leader 提交的 zxid 比 Follower 的 zxid 大,发送 Proposal 给 Follower 提交执行 + * TRUNC:Follower 的 zxid 比leader 的 zxid 大,Follower 要进行回滚 + * SNAP:Follower 没有任何数据,直接全量同步 +* 执行数据同步,当 Leader 接收到超过半数 Follower 的 Ack 之后,进入正常工作状态,集群启动完成 + + + +核心函数解析: + +* Leader 更新状态入口:`Leader.lead()` + * `zk.loadData()`:恢复数据到内存 + * `cnxAcceptor = new LearnerCnxAcceptor()`:启动通信组件 + * `s = ss.accept()`:等待其他 Follower 节点向 Leader 节点发送同步状态 + * `LearnerHandler fh `:接收到 Follower 的请求,就创建 LearnerHandler 对象 + * `fh.start()`:启动线程,通过 switch-case 语法判断接收的命令,执行相应的操作 +* Follower 更新状态入口:`Follower.followerLeader()` + * `QuorumServer leaderServer = findLeader()`:查找 Leader + * `connectToLeader(addr, hostname) `:与 Leader 建立连接 + * `long newEpochZxid = registerWithLeader(Leader.FOLLOWERINFO)`:向 Leader 注册 + + + + + +*** + + + +#### 主从工作 + +Leader:主服务的工作流程 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Zookeeper-Leader启动.png) + +Follower:从服务的工作流程,核心函数为 `Follower#followLeader()` + +* `readPacket(qp)`:读取信息 + +* `processPacket(qp)`:处理信息 + + ```java + protected void processPacket(QuorumPacket qp) throws Exception{ + switch (qp.getType()) { + case Leader.PING: + break; + case Leader.PROPOSAL: + break; + case Leader.COMMIT: + break; + case Leader.COMMITANDACTIVATE: + break; + case Leader.UPTODATE: + break; + case Leader.REVALIDATE: + break; + case Leader.SYNC: + break; + default: + break; + } + } + ``` + + + +*** + + + +### 客户端 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Zookeeper-客户端初始化.png) + + + + + diff --git a/Prog.md b/Prog.md index 50792cc..9bf9c71 100644 --- a/Prog.md +++ b/Prog.md @@ -6925,8 +6925,8 @@ private static void method1() { }; // 使用 timer 添加两个任务,希望它们都在 1s 后执行 // 但由于 timer 内只有一个线程来顺序执行队列中的任务,因此任务1的延时,影响了任务2的执行 - timer.schedule(task1,1000);//17:45:56 c.ThreadPool [Timer-0] - task 1 - timer.schedule(task2,1000);//17:45:58 c.ThreadPool [Timer-0] - task 2 + timer.schedule(task1, 1000);//17:45:56 c.ThreadPool [Timer-0] - task 1 + timer.schedule(task2, 1000);//17:45:58 c.ThreadPool [Timer-0] - task 2 } ``` diff --git a/Tool.md b/Tool.md index cc67520..004807e 100644 --- a/Tool.md +++ b/Tool.md @@ -536,41 +536,6 @@ Linux 文件系统目录结构和熟知的 windows 系统有较大区别,没 ![Linux文件系统](https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/Linux文件系统.png) -/:根目录,所有的目录、文件、设备都在/之下,/就是 Linux 文件系统的组织者,也是最上级的领导者。 - -/bin:bin 就是二进制(binary)英文缩写。在一般的系统当中,都可以在这个目录下找到 linux 常用的命令。系统所需要的那些命令位于此目录。 - -/boot:Linux 的内核及引导系统程序所需要的文件目录。 - -/dev:dev 是设备(device)的英文缩写。这个目录对所有的用户都十分重要。因为在这个目录中包含了所有 linux 系统中使用的外部设备。但是这里并不是放的外部设备的驱动程序。这一点和常用的 windows,dos 操作系统不一样。它实际上是一个访问这些外部设备的端口。可以非常方便地去访问这些外部设备,和访问一个文件,一个目录没有任何区别。 - -/**home**:如果建立一个用户,用户名是"xx",那么在/home 目录下就有一个对应的 -/home/xx 路径,用来存放用户的主目录。家目录 - -/lib:lib 是库(library)英文缩写。这个目录是用来存放系统动态连接共享库的。几乎所有的应用程序都会用到这个目录下的共享库。因此,千万不要轻易对这个目录进行什么操作,一旦发生问题,系统就不能工作了。 - -/**proc**:存储的是当前内核运行状态的一系列特殊文件,用户可以通过这些文件查看有关系统硬件及当前正在运行进程的信息,甚至可以通过更改其中某些文件来改变内核的运行状态。此外还有**/srv /sys** 三个目录,内核相关目录,不要动。 - -/**root**:Linux 超级权限用户 root 的家目录。 - -/sbin:这个目录是用来存放系统管理员的系统管理程序。大多是涉及系统管理的命令的存放,是超级权限用户 root 的可执行命令存放地,普通用户无权限执行这个目录下的命令,sbin 中包含的都是 root 权限才能执行的。 - -/var/log - -/**usr**:这是 linux 系统中占用硬盘空间最大的目录。**用户的很多应用程序和文件都存放在这个目录下**。 Unix software resource usr。类似 windows 系统的 program files - -/usr/local:这里主要存放那些手动安装的软件,即不是通过或 apt-get 安装的软件。它和/usr 目录具有相类似的目录结构。 - -/usr/share :系统共用的东西存放地,比如 /usr/share/fonts 是字体目录,/usr/share/doc和/usr/share/man 帮助文件。 - -/etc:管理所有的配置文件的目录,比如安装 mysql 的配置文件my.conf - -/mnt:可供系统管理员使用,手动挂载一些临时设备媒体设备的目录。 - -/media:是自动挂载的目录。当把 U 盘插入到系统中,会自动挂载到该目录下。比如插入一个 U 盘,会自动到/media 目录中挂载。 - -/opt:额外安装软件存放的目录。比如 mysql 的安装包就可以放在该目录。 - From 8a3dbab159ad46d27eec2e0716eb8d6044c85086 Mon Sep 17 00:00:00 2001 From: Seazean Date: 2022年5月17日 22:42:48 +0800 Subject: [PATCH 07/35] Update Java Note --- DB.md | 2 +- SSM.md | 1050 ++------------------------------------------------------ 2 files changed, 29 insertions(+), 1023 deletions(-) diff --git a/DB.md b/DB.md index e1e1baa..5bb77c1 100644 --- a/DB.md +++ b/DB.md @@ -13643,7 +13643,7 @@ SETNX 获取锁时,设置一个指定的唯一值(UUID),释放前获取 SET lock_key unique_value NX PX 10000 ``` - Lua 脚本(unlock.script)实现的释放锁操作的伪代码:key 类型参数会放入 KEYS 数组,其它参数会放入 ARGV 数组,在脚本中通过 KEYS 和 ARGV 传递参数,**保证判断标识和释放锁这两个操作的原子性** +Lua 脚本(unlock.script)实现的释放锁操作的伪代码:key 类型参数会放入 KEYS 数组,其它参数会放入 ARGV 数组,在脚本中通过 KEYS 和 ARGV 传递参数,**保证判断标识和释放锁这两个操作的原子性** ```sh EVAL "return redis.call('set', KEYS[1], ARGV[1])" 1 lock_key unique_value # 1 代表需要一个参数 diff --git a/SSM.md b/SSM.md index fbe9aa0..2f16964 100644 --- a/SSM.md +++ b/SSM.md @@ -10154,6 +10154,20 @@ Controller 加载控制:SpringMVC 的处理器对应的 bean 必须按照规 ### 注解驱动 +WebApplicationContext,生成 Spring 核心容器(主容器/父容器/根容器) + +* 父容器:Spring 环境加载后形成的容器,包含 Spring 环境下的所有的 bean +* 子容器:当前 mvc 环境加载后形成的容器,不包含 Spring 环境下的 bean +* 子容器可以访问父容器中的资源,父容器不可以访问子容器的资源 + +EnableWebMvc 注解作用: + +* 支持 ConversionService 的配置,可以方便配置自定义类型转换器 +* 支持 @NumberFormat 注解格式化数字类型 +* 支持 @DateTimeFormat 注解格式化日期数据,日期包括 Date、Calendar +* 支持 @Valid 的参数校验(需要导入 JSR-303 规范) +* 配合第三方 jar 包和 SpringMVC 提供的注解读写 XML 和 JSON 格式数据 + 纯注解开发: * 使用注解形式转化 SpringMVC 核心配置文件为配置类 java / config / SpringMVCConfiguration.java @@ -10164,12 +10178,9 @@ Controller 加载控制:SpringMVC 的处理器对应的 bean 必须按照规 type=FilterType.ANNOTATION, classes = {Controller.class} ) ) + //等同于,还不完全相同 + @EnableWebMvc public class SpringMVCConfiguration implements WebMvcConfigurer{ - //注解配置放行指定资源格式 - // @Override - // public void addResourceHandlers(ResourceHandlerRegistry registry) { - // registry.addResourceHandler("/img/**").addResourceLocations("/img/"); - // } //注解配置通用放行资源的格式 建议使用 @Override public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) { @@ -10177,14 +10188,14 @@ Controller 加载控制:SpringMVC 的处理器对应的 bean 必须按照规 } } ``` - + * 基于 servlet3.0 规范,自定义 Servlet 容器初始化配置类,加载 SpringMVC 核心配置类 ```java - //创建Servlet容器时,使用注解方式加载SPRINGMVC配置类中的信息, - //并加载成WEB专用的ApplicationContext对象该对象放入了ServletContext范围, - //在整个WEB容器中可以随时获取调用 public class ServletContainersInitConfig extends AbstractDispatcherServletInitializer { + //创建Servlet容器时,使用注解方式加载SPRINGMVC配置类中的信息, + //并加载成WEB专用的ApplicationContext对象该对象放入了ServletContext范围, + //在整个WEB容器中可以随时获取调用 @Override protected WebApplicationContext createServletApplicationContext() { A.C.W.A ctx = new AnnotationConfigWebApplicationContext(); @@ -10220,6 +10231,8 @@ Controller 加载控制:SpringMVC 的处理器对应的 bean 必须按照规 + + *** @@ -13784,1030 +13797,23 @@ public class HelloController { +**** -*** - - - - - -# SSM - -## XML - -### 结构搭建 - -整合 SSM 三种框架进行项目开发 - -* 创建项目,组织项目结构,创建包 - -* 创建表与实体类 - -* 创建三层架构对应的模块、接口与实体类,建立关联关系 - -* 数据层接口(代理自动创建实现类) - - * 业务层接口 + 业务层实现类 - * 表现层类 - - ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/SSM-目录结构.png) - - - - - -*** - - - -### 数据准备 - -* 导入坐标 pom.xml - - ```xml - - - - org.springframework - spring-context - 5.1.9.RELEASE - - - - - org.mybatis - mybatis - 3.5.3 - - - - - mysql - mysql-connector-java - 5.1.47 - - - - - org.springframework - spring-jdbc - 5.1.9.RELEASE - - - - - org.mybatis - mybatis-spring - 2.0.3 - - - - - com.alibaba - druid - 1.1.16 - - - - - com.github.pagehelper - pagehelper - 5.1.2 - - - - - - org.springframework - spring-webmvc - 5.1.9.RELEASE - - - - - com.fasterxml.jackson.core - jackson-databind - 2.9.0 - - - com.fasterxml.jackson.core - jackson-core - 2.9.0 - - - com.fasterxml.jackson.core - jackson-annotations - 2.9.0 - - - - - javax.servlet - javax.servlet-api - 3.1.0 - provided - - - - - - - junit - junit - 4.12 - - - - org.springframework - spring-test - 5.1.9.RELEASE - - - - - - - - - org.apache.tomcat.maven - tomcat7-maven-plugin - 2.1 - - 80 - / - - - - - ``` - -* resources.jdbc.properties - - ```properties - jdbc.driver=com.mysql.jdbc.Driver - jdbc.url=jdbc:mysql://192.168.0.137:3306/ssm_db?useSSL=false - jdbc.username=root - jdbc.password=123456 - ``` - -* domain - - ```java - public class User implements Serializable { - private Integer uuid; - private String userName; - private String password; - private String realName; - private Integer gender; - private Date birthday; - } - ``` - -* Dao 层 - - ```java - public interface UserDao { - //添加用户 - public boolean save(User user); - - //修改用户 - public boolean update(User user); - - //删除用户 - public boolean delete(Integer uuid); - - //查询单个用户信息 - public User get(Integer uuid); - - //查询全部用户信息 - public List getAll(); - - /** - * 根据用户名密码查询个人信息 - * @param userName 用户名 - * @param password 密码信息 - * @return - */ - //数据层操作不要和业务层操作的名称混淆,通常数据层仅反映与数据库间的信息交换,不体现业务逻辑 - public User getByUserNameAndPassword(@Param("userName") String userName, - @Param("password") String password); - } - ``` - -* service.UserService - - ```java - public interface UserService { - //添加用户 - public boolean save(User user); - - //修改用户 - public boolean update(User user); - - //删除用户 - public boolean delete(Integer uuid); - - //查询单个用户信息 - public User get(Integer uuid); - - //查询全部用户信息 - public List getAll(); - - //根据用户名密码进行登录 - public User login(String userName,String password); - } - ``` - - service.impl.UserServiceImpl - - ```java - @Service //设置为bean - public class UserServiceImpl implements UserService { - @Autowired - private UserDao userDao; - - @Override - public boolean save(User user) { - return userDao.save(user); - } - - @Override - public boolean update(User user) { - return userDao.update(user); - } - - @Override - public boolean delete(Integer uuid) { - return userDao.delete(uuid); - } - - @Override - public User get(Integer uuid) { - return userDao.get(uuid); - } - - @Override - public PageInfo getAll(int page, int size) {//用分页插件 - PageHelper.startPage(page, size); - List all = userDao.getAll(); - return new PageInfo(all); - } - - @Override - public User login(String userName, String password) { - return userDao.getByUserNameAndPassword(userName, password); - } - } - ``` - -* controller - - ```java - public class UserController { - - } - ``` - - - -*** - - - -### Mybatis - -* Spring环境配置:spring-mvc.xml - - ```xml - - - - - - - - ``` - -* MyBatis映射:resources.dao.UserDao.xml - - ```xml - - - -
    - - - INSERT INTO user (userName,password,realName,gender,birthday) VALUES (#{userName},#{password},#{realName},#{gender},#{birthday}) - - - - - DELETE FROM user WHERE uuid = #{uuid} - - - - - UPDATE user SET userName=#{userName},password=#{password},realName=#{realName},gender=#{gender},birthday=#{birthday} WHERE uuid=#{uuid} - - - - - - - - - - - - ``` - -* Mybatis 核心配置:resouces.applicationContext.xml - - ```xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - mysql - true - - - - - - - - - - - - - - - - - - ``` - -* 业务层接口开启事务 - - ```java - @Transactional(readOnly = true) - public interface UserService { - //添加用户 - @Transactional(readOnly = false) - public boolean save(User user); - @Transactional(readOnly = false) - public boolean update(User user); - @Transactional(readOnly = false) - public boolean delete(Integer uuid); - - //查询单个用户 - public User get(Integer uuid); - - //查询全部用户 - public PageInfo getAll(int page, int size); - - //根据用户名密码进行登录 - public User login(String userName, String password); - } - ``` - -*** +# Boot +## 基本介绍 -### Junit +### Boot介绍 -* 单元测试整合 junit - - ```java - @RunWith(SpringJUnit4ClassRunner.class) - @ContextConfiguration(locations = "classpath:applicationContext.xml") - public class UserServiceTest { - @Autowired - private UserService userService; - - @Test - public void testDelete(){ - User user = new User(); userService.delete(3); - } - } - ``` - -* test.resouces - - ```java - applicationContext.xml + jdbc.properties - ``` - - - -*** - - - -### MVC - -* web.xml配置 - - ```xml - - - - - - contextConfigLocation - classpath*:applicationContext.xml - - - - - org.springframework.web.context.ContextLoaderListener - - - - CharacterEncodingFilter - org.springframework.web.filter.CharacterEncodingFilter - - encoding - UTF-8 - - - - CharacterEncodingFilter - /* - - - - DispatcherServlet - org.springframework.web.servlet.DispatcherServlet - - - contextConfigLocation - classpath*:spring-mvc.xml - - - - DispatcherServlet - / - - - ``` - -* spring-mvc.xml - - ```xml - - - ``` - -* Controller层 - - ```java - @RestController //@RestController = @Controller + @ResponseBody - @RequestMapping("/user") - public class UserController { - - @PostMapping - public boolean save(User user){ - System.out.println("save ..." + user); - return true; - } - - @PutMapping - public boolean update(User user){ - System.out.println("update ..." + user); - return true; - } - - @DeleteMapping("/{uuid}") - public boolean delete(@PathVariable Integer uuid){ - System.out.println("delete ..." + uuid); - return true; - } - - @GetMapping("/{uuid}") - public User get(@PathVariable Integer uuid){ - System.out.println("get ..." + uuid); - return null; - } - - @GetMapping("/{page}/{size}") - public List getAll(@PathVariable Integer page,@PathVariable Integer size){ - System.out.println("getAll ..." + page+","+size); - return null; - } - - @PostMapping("/login") - public User login(String userName,String password){ - System.out.println("login ..." + userName + " ," +password); - return null; - } - } - ``` - - - - - -**** - - - -### 表现层 - -#### 数据封装 - -* 前端接收表现层返回的数据种类 - * 返回数据格式设计:状态、数据、消息 - * 返回数据状态设计,根据业务不同设计:404、500、200 - -* 数据格式代码: - - ```java - public class Result { - // 操作结果编码 - private Integer code; - // 操作数据结果 - private Object data; - // 消息 - private String message; - public Result(Integer code) { - this.code = code; - } - public Result(Integer code, Object data) { - this.code = code; - this.data = data; - } - } - ``` - -* 状态代码格式:状态码常量可以根据自己的业务需求设定 - - ```java - public class Code { - //操作结果编码 - public static final Integer SAVE_OK = 20011; - public static final Integer UPDATE_OK = 20021; - public static final Integer SAVE_ERR = 20010; - public static final Integer UPDATE_ERR = 20020; - - //系统错误编码 - //操作权限编码 - //校验结果编码 - } - - ``` - -* Controller调用 - - ```java - @RestController - public class UserController { - @Autowired - private UserService userService; - @PostMapping - public Result save(User user){ - boolean flag = userService.save(user); - return new Result(flag ? Code.SAVE_OK:Code.SAVE_ERROR); - } - @GetMapping("/{uuid}") - public Result get(@PathVariable Integer uuid){ - User user = userService.get(uuid); - //三目运算符 - return new Result(null != user ?Code.GET_OK: Code.GET_ERROR,user); - } - } - ``` - - - -*** - - - -#### 自定义异常 - -设定自定义异常,封装程序执行过程中出现的问题,便于表现层进行统一的异常拦截并进行处理 - -* BusinessException -* SystemException - -自定义异常消息返回时需要与业务正常执行的消息按照统一的格式进行处理 - -* 定义 BusinessException - - ```java - public class BusinessException extends RuntimeException { - //自定义异常中封装对应的错误编码,用于异常处理时获取对应的操作编码 - private Integer code; - - public Integer getCode() { - return code; - } - - public void setCode(Integer code) { - this.code = code; - } - - public BusinessException(Integer code) { - this.code = code; - } - - public BusinessException(String message, Integer code) { - super(message); - this.code = code; - } - - public BusinessException(String message, Throwable cause,Integer code) { - super(message, cause); - this.code = code; - } - - public BusinessException(Throwable cause,Integer code) { - super(cause); - this.code = code; - } - - public BusinessException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace,Integer code) { - super(message, cause, enableSuppression, writableStackTrace); - this.code = code; - } - } - ``` - -* Controller调用 - - ```java - @RestController - public class UserController { - @Autowired - private UserService userService; - @GetMapping("/{uuid}") - public Result get(@PathVariable Integer uuid){ - User user = userService.get(uuid); - //模拟出现异常,使用条件控制,便于测试结果 - if (uuid == 10 ) throw new BusinessException("查询出错啦,请重试!",Code.GET_ERROR); - return new Result(null != user ?Code.GET_OK: Code.GET_ERROR,user); - } - } - ``` - - - -*** - - - -#### 兼容异常 - -java.controller.interceptor - -```java -@Component -@ControllerAdvice -public class ProjectExceptionAdivce { - @ExceptionHandler(BusinessException.class) - @ResponseBody - //对出现异常的情况进行拦截,并将其处理成统一的页面数据结果格式 - public Result doBusinessException(BusinessException e){ - return new Result(e.getCode(),e.getMessage()); - } -} -``` - - - -**** - - - -## 注解 - -### 结构搭建 - -项目整体目录结构 - -![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/SSM-annotation.png) - - - - - -*** - - - -### UserDao.xml - -注解:@Param - -作用:当 SQL 语句需要多个(大于1)参数时,用来指定参数的对应规则 - -* 注解替代 UserDao 映射配置文件:dao.UserDao - - ```java - public interface UserDao { - //添加用户 - @Insert("insert into user(userName,password,realName,gender,birthday)values(#{userName},#{password},#{realName},#{gender},#{birthday})") - public boolean save(User user); - - //修改用户 - @Update("update user set userName=#{userName},password=#{password},realName=#{realName},gender=#{gender},birthday=#{birthday} where uuid=#{uuid}") - public boolean update(User user); - - // 删除用户 - @Delete("delete from user where uuid = #{uuid}") - public boolean delete(Integer uuid); - - //查询单个用户信息 - @Select("select * from user where uuid = #{uuid}") - public User get(Integer uuid); - - //查询全部用户信息 - @Select("select * from user") - public List getAll(); - - - //根据用户名密码查询个人信息 - @Select("select * from user where userName=#{userName} and password=#{password}") - //注意:数据层操作不要和业务层操作的名称混淆,通常数据层仅反映与数据库间的信息交换,不体现业务逻辑 - public User getByUserNameAndPassword(@Param("userName") String userName, @Param("password") String password); - } - ``` - - - - - -*** - - - -### applicationContext.xml - -![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/SSM-IoC注解整合MyBatis图解.png) - -* JdbcConfig - - ```java - public class JdbcConfig { - //使用注入的形式,读取properties文件中的属性值,等同于 - @Value("${jdbc.driver}") - private String driver; - @Value("${jdbc.url}") - private String url; - @Value("${jdbc.username}") - private String userName; - @Value("${jdbc.password}") - private String password; - - //定义dataSource的bean,等同于 - @Bean("dataSource") - public DataSource getDataSource(){ - //创建对象 - DruidDataSource ds = new DruidDataSource(); - //,等同于set属性注入 - ds.setDriverClassName(driver); - ds.setUrl(url); - ds.setUsername(userName); - ds.setPassword(password); - return ds; - } - } - ``` - -* MybatisConfig - - ```java - public class MyBatisConfig { - //定义MyBatis的核心连接工厂bean,等同于 - @Bean - //自动装配的形式加载dataSource,为set注入提供数据,dataSource来源于JdbcConfig中 - public SqlSessionFactoryBean getSqlSessionFactoryBean(@Autowired DataSource dataSource,@Autowired Interceptor interceptor){ - SqlSessionFactoryBean ssfb = new SqlSessionFactoryBean(); - //等同于 - ssfb.setTypeAliasesPackage("domain"); - //等同于 - ssfb.setDataSource(dataSource); - //可以把Interceptor写在这里 - ssfb.setPlugins(interceptor); - return ssfb; - } - - //定义MyBatis的映射扫描,等同于 - @Bean - public MapperScannerConfigurer getMapperScannerConfigurer(){ - MapperScannerConfigurer msc = new MapperScannerConfigurer(); - //等同于 - msc.setBasePackage("dao"); - return msc; - } - - @Bean("pageInterceptor") - public Interceptor getPageInterceptor(){ - Interceptor interceptor = new PageInterceptor(); - Properties properties = new Properties(); - properties.setProperty("helperDialect","mysql"); - properties.setProperty("reasonable","true"); - //等同于 - interceptor.setProperties(properties); - return interceptor; - } - } - ``` - -* SpringConfig.xml - - ```java - @Configuration - @ComponentScan(value = {"config","dao","service","system"},excludeFilters = - @ComponentScan.Filter(type= FilterType.ANNOTATION,classes = {Controller.class})) - //等同于 - @PropertySource("classpath:jdbc.properties") - //等同于,bean的名称默认取transactionManager - @EnableTransactionManagement - @Import({MyBatisConfig.class,JdbcConfig.class}) - public class SpringConfig { - //等同于 - @Bean("transactionManager") - //等同于 - public DataSourceTransactionManager getTxManager(@Autowired DataSource dataSource){ - DataSourceTransactionManager tm = new DataSourceTransactionManager(); - //等同于 - tm.setDataSource(dataSource); - return tm; - } - } - ``` - - - - -*** - - - -### spring-mvc.xml - -* 注解替代 spring-mvc.xml:SpringMvcConfig - - ```java - @Configuration - //等同于 - @ComponentScan("controller") - //等同于,还不完全相同 - @EnableWebMvc - public class SpringMvcConfig { - //注解配置通用放行静态资源的格式 - @Override - public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) { - configurer.enable(); - } - } - ``` - -* EnableWebMvc - - 1. 支持 ConversionService 的配置,可以方便配置自定义类型转换器 - 2. 支持 @NumberFormat 注解格式化数字类型 - 3. 支持 @DateTimeFormat 注解格式化日期数据,日期包括 Date、Calendar - 4. 支持 @Valid 的参数校验(需要导入 JSR-303 规范) - 5. 配合第三方 jar 包和 SpringMVC 提供的注解读写 XML 和 JSON 格式数据 - - - -*** - - - -### web.xml - -* 注解替代 web.xml:ServletContainersInitConfig - - ```java - public class ServletContainersInitConfig extends AbstractDispatcherServletInitializer { - - //创建Servlet容器,加载SpringMVC配置类中的信息,并加载成WEB专用的ApplicationContext对象,该对象放入了ServletContext范围,后期在整个WEB容器中可以随时获取调用 - @Override - protected WebApplicationContext createServletApplicationContext() { - AnnotationConfigWebApplicationContext ctx = new AnnotationConfigWebApplicationContext(); - ctx.register(SpringMvcConfig.class); - return ctx; - } - - //注解配置映射地址方式,服务于SpringMVC的核心控制器DispatcherServlet - @Override - protected String[] getServletMappings() { - return new String[]{"/"}; - } - //启动服务器时,通过监听器加载spring运行环境 - @Override - protected WebApplicationContext createRootApplicationContext() { - AnnotationConfigWebApplicationContext ctx = new AnnotationConfigWebApplicationContext(); - ctx.register(SpringConfig.class); - return ctx; - } - - //乱码处理作为过滤器,在servlet容器启动时进行配置,相关内容参看Servlet零配置相关课程 - @Override - public void onStartup(ServletContext servletContext) throws ServletException { - //触发父类的onStartup - super.onStartup(servletContext); - //1.创建字符集过滤器对象 - CharacterEncodingFilter cef = new CharacterEncodingFilter(); - //2.设置使用的字符集 - cef.setEncoding("UTF-8"); - //3.添加到容器(它不是ioc容器,而是ServletContainer) - FilterRegistration.Dynamic registration = servletContext.addFilter( - "characterEncodingFilter", cef); - //4.添加映射 - registration.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST, DispatcherType.FORWARD, DispatcherType.INCLUDE), false, "/*"); - } - } - ``` - -* WebApplicationContext,生成 Spring 核心容器(主容器/父容器/根容器) - - * 父容器:Spring 环境加载后形成的容器,包含 Spring 环境下的所有的 bean - * 子容器:当前 mvc 环境加载后形成的容器,不包含 Spring 环境下的 bean - * 子容器可以访问父容器中的资源,父容器不可以访问子容器的资源 - - - - - - - -**** - - - - - -# Boot - -## 基本介绍 - -### Boot介绍 - -SpringBoot 提供了一种快速使用 Spring 的方式,基于约定优于配置的思想,可以让开发人员不必在配置与逻辑业务之间进行思维的切换,全身心的投入到逻辑业务的代码编写中,从而大大提高了开发的效率 +SpringBoot 提供了一种快速使用 Spring 的方式,基于约定优于配置的思想,可以让开发人员不必在配置与逻辑业务之间进行思维的切换,全身心的投入到逻辑业务的代码编写中,从而大大提高了开发的效率 SpringBoot 功能: -* 自动配置: - - Spring Boot 的自动配置是一个运行时(更准确地说,是应用程序启动时)的过程,考虑了众多因素选择使用哪个配置,该过程是SpringBoot 自动完成的 +* 自动配置,自动配置是一个运行时(更准确地说,是应用程序启动时)的过程,考虑了众多因素选择使用哪个配置,该过程是SpringBoot 自动完成的 * 起步依赖,起步依赖本质上是一个 Maven 项目对象模型(Project Object Model,POM),定义了对其他库的传递依赖,这些东西加在一起即支持某项功能。简单的说,起步依赖就是将具备某种功能的坐标打包到一起,并提供一些默认的功能 From a641d4cdd49f1f03ed7f7382b05f2e5b1b927f2d Mon Sep 17 00:00:00 2001 From: Seazean Date: 2022年5月22日 23:47:33 +0800 Subject: [PATCH 08/35] Update Java Note --- DB.md | 780 +++++++++++++++++++++++++++---------------------------- Frame.md | 59 ++--- SSM.md | 10 +- 3 files changed, 407 insertions(+), 442 deletions(-) diff --git a/DB.md b/DB.md index 5bb77c1..2eed8fa 100644 --- a/DB.md +++ b/DB.md @@ -8811,56 +8811,6 @@ Redis (REmote DIctionary Server) :用 C 语言开发的一个开源的高性 ### 安装启动 -#### CentOS - -1. 下载 Redis - - 下载安装包: - - ```sh - wget http://download.redis.io/releases/redis-5.0.0.tar.gz - ``` - - 解压安装包: - - ```sh - tar –xvf redis-5.0.0.tar.gz - ``` - - 编译(在解压的目录中执行): - - ```sh - make - ``` - - 安装(在解压的目录中执行): - - ```sh - make install - ``` - - - -2. 安装 Redis - - redis-server,服务器启动命令 客户端启动命令 - - redis-cli,redis核心配置文件 - - redis.conf,RDB文件检查工具(快照持久化文件) - - redis-check-dump,AOF文件修复工具 - - redis-check-aof - - - -*** - - - -#### Ubuntu - 安装: * Redis 5.0 被包含在默认的 Ubuntu 20.04 软件源中 @@ -8925,7 +8875,7 @@ Redis (REmote DIctionary Server) :用 C 语言开发的一个开源的高性 mkdir data ``` -2. 创建配置文件副本放入 conf 目录,Ubuntu系统配置文件 redis.conf 在目录 `/etc/redis` 中 +2. 创建配置文件副本放入 conf 目录,Ubuntu 系统配置文件 redis.conf 在目录 `/etc/redis` 中 ```sh cat redis.conf | grep -v "#" | grep -v "^$" -> /conf/redis-6379.conf @@ -10095,24 +10045,12 @@ typedef struct redisClient { 一部分标志记录目前**客户端所处的状态**: -* REDIS_MONITOR 表示客户端正在执行 MONITOR 命令 * REDIS_UNIX_SOCKET 表示服务器使用 UNIX 套接字来连接客户端 * REDIS_BLOCKED 表示客户端正在被 BRPOP、BLPOP 等命令阻塞 * REDIS_UNBLOCKED 表示客户端已经从 REDIS_BLOCKED 所表示的阻塞状态脱离,在 REDIS_BLOCKED 标志打开的情况下使用 * REDIS_MULTI 标志表示客户端正在执行事务 * REDIS_DIRTY_CAS 表示事务使用 WATCH 命令监视的数据库键已经被修改 -* REDIS_DIRTY_EXEC 表示事务在命令入队时出现了错误。以上两个标志都表示事务的安全性已经被破坏,只要两个标记中的任意一个被打开,EXEC 命令必然会执行失败,这两个标志只能在客户端打开了 REDIS_MULTI 标志的情况下使用 -* REDIS_CLOSE_ASAP 表示客户端的输出缓冲区大小超出了服务器允许的范围,关闭这个客户端并且直接丢弃缓冲区的内容 -* REDIS_CLOSE_AFTER_REPLY 表示有用户对这个客户端执行了 `CLIENT KILL` 命令,或者客户端发送给服务器的命令请求中包含了错误的协议内容,服务器会将客户端积存在输出缓冲区中的所有内容发送给客户端,然后关闭客户端 -* REDIS_ASKING 表示客户端向集群节点(运行在集群模式下的服务器)发送了 `ASKING` 命令 -* REDIS_FORCE_AOF 表示强制服务器将当前执行的命令写入到 AOF 文件里,执行 `PUBSUB` 命令会使客户端打开该标志 -* REDIS_FORCE_REPL 表示强制主服务器将当前执行的命令复制给所有从服务器,执行 `SCRIPT LOAD` 命令会使客户端打开 REDIS_FORCE_AOF 标志和 REDIS_FORCE_REPL 标志 -* REDIS_MASTER_FORCE_REPLY 表示将要进行主从复制,在主从服务器进行命令传播期间,从服务器需要向主服务器发送 `REPLICATION ACK` 命令,在发送这个命令之前从服务器必须打开主服务器对应的客户端的该标志,否则发送操作会被拒绝执行 - -Redis 只会将对数据库进行了修改的命令写入到 AOF 文件,并复制到各个从服务器,但是 PUBSUB 和 SCRIPT LOAD 命令例外: - -* PUBSUB 命令虽然没有修改数据库,但 PUBSUB 命令向频道的所有订阅者发送消息这一行为带有副作用,接收到消息的所有客户端的状态都会因为这个命令而改变,所以服务器需要使用 REDIS_FORCE_AOF 标志强制将这个命令写入 AOF 文件。这样在将来载入 AOF 文件时,服务器就可以再次执行相同的 PUBSUB 命令,并产生相同的副作用 -* SCRIPT LOAD 命令虽然没有修改数据库,但它修改了服务器状态,所以也是一个带有副作用的命令,需要使用 REDIS_FORCE_AOF +* ..... @@ -12697,7 +12635,10 @@ AOF 写数据过程: +Redis 只会将对数据库进行了修改的命令写入到 AOF 文件,并复制到各个从服务器,但是 PUBSUB 和 SCRIPT LOAD 命令例外: +* PUBSUB 命令虽然没有修改数据库,但 PUBSUB 命令向频道的所有订阅者发送消息这一行为带有副作用,接收到消息的所有客户端的状态都会因为这个命令而改变,所以服务器需要使用 REDIS_FORCE_AOF 标志强制将这个命令写入 AOF 文件。这样在将来载入 AOF 文件时,服务器就可以再次执行相同的 PUBSUB 命令,并产生相同的副作用 +* SCRIPT LOAD 命令虽然没有修改数据库,但它修改了服务器状态,所以也是一个带有副作用的命令,需要使用 REDIS_FORCE_AOF @@ -15732,108 +15673,116 @@ typedef struct clusterMsgDataPublish { +**** -*** +## 其他操作 -## 缓存方案 +### 发布订阅 -### 缓存模式 +#### 基本指令 -#### 旁路缓存 +Redis 发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息 -缓存本质:弥补 CPU 的高算力和 IO 的慢读写之间巨大的鸿沟 +Redis 客户端可以订阅任意数量的频道,每当有客户端向被订阅的频道发送消息(message)时,频道的**所有订阅者都会收到消息** -旁路缓存模式 Cache Aside Pattern 是平时使用比较多的一个缓存读写模式,比较适合读请求比较多的场景 +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-发布订阅.png) -Cache Aside Pattern 中服务端需要同时维系 DB 和 cache,并且是以 DB 的结果为准 +操作过程: -* 写操作:先更新 DB,然后直接删除 cache -* 读操作:从 cache 中读取数据,读取到就直接返回;读取不到就从 DB 中读取数据返回,并放到 cache +* 打开一个客户端订阅 channel1:`SUBSCRIBE channel1` -时序导致的不一致问题: +* 打开另一个客户端,给 channel1 发布消息 hello:`PUBLISH channel1 hello` -* 在写数据的过程中,不能先删除 cache 再更新 DB,因为会造成缓存的不一致。比如请求 1 先写数据 A,请求 2 随后读数据 A,当请求 1 删除 cache 后,请求 2 直接读取了 DB,此时请求 1 还没写入 DB(延迟双删) +* 第一个客户端可以看到发送的消息 -* 在写数据的过程中,先更新 DB 再删除 cache 也会出现问题,但是概率很小,因为缓存的写入速度非常快 + -旁路缓存的缺点: +客户端还可以通过 PSUBSCRIBE 命令订阅一个或多个模式,每当有其他客户端向某个频道发送消息时,消息不仅会被发送给这个频道的所有订阅者,还会被**发送给所有与这个频道相匹配的模式的订阅者**,比如 `PSUBSCRIBE channel*` 订阅模式,与 channel1 匹配 -* 首次请求数据一定不在 cache 的问题,一般采用缓存预热的方法,将热点数据可以提前放入 cache 中 -* 写操作比较频繁的话导致 cache 中的数据会被频繁被删除,影响缓存命中率 +注意:发布的消息没有持久化,所以订阅的客户端只能收到订阅后发布的消息 -删除缓存而不是更新缓存的原因:每次更新数据库都更新缓存,造成无效写操作较多 +*** -**** +#### 频道操作 -#### 读写穿透 +Redis 将所有频道的订阅关系都保存在服务器状态的 pubsub_channels 字典里,键是某个被订阅的频道,值是一个记录所有订阅这个频道的客户端链表 -读写穿透模式 Read/Write Through Pattern:服务端把 cache 视为主要数据存储,从中读取数据并将数据写入其中,cache 负责将此数据同步写入 DB,从而减轻了应用程序的职责 +```c +struct redisServer { + // 保存所有频道的订阅关系, + dict *pubsub_channels; +} +``` -* 写操作:先查 cache,cache 中不存在,直接更新 DB;cache 中存在则先更新 cache,然后 cache 服务更新 DB(同步更新 cache 和 DB) +客户端执行 SUBSCRIBE 命令订阅某个或某些频道,服务器会将客户端与频道进行关联: -* 读操作:从 cache 中读取数据,读取到就直接返回 ;读取不到先从 DB 加载,写入到 cache 后返回响应 +* 频道已经存在,直接将客户端添加到链表末尾 +* 频道还未有任何订阅者,在字典中为频道创建一个键值对,再将客户端添加到链表 - Read-Through Pattern 实际只是在 Cache-Aside Pattern 之上进行了封装。在 Cache-Aside Pattern 下,发生读请求的时候,如果 cache 中不存在对应的数据,是由客户端负责把数据写入 cache,而 Read Through Pattern 则是 cache 服务自己来写入缓存的,对客户端是透明的 +UNSUBSCRIBE 命令用来退订某个频道,服务器将从 pubsub_channels 中解除客户端与被退订频道之间的关联 -Read-Through Pattern 也存在首次不命中的问题,采用缓存预热解决 -*** +**** -#### 异步缓存 -异步缓存写入 Write Behind Pattern 由 cache 服务来负责 cache 和 DB 的读写,对比读写穿透不同的是 Write Behind Caching 是只更新缓存,不直接更新 DB,改为**异步批量**的方式来更新 DB,可以减小写的成本 +#### 模式操作 -缺点:这种模式对数据一致性没有高要求,可能出现 cache 还没异步更新 DB,服务就挂掉了 +Redis 服务器将所有模式的订阅关系都保存在服务器状态的 pubsub_patterns 属性里 -应用: +```c +struct redisServer { + // 保存所有模式订阅关系,链表中每个节点是一个 pubsubPattern + list *pubsub_patterns; +} -* DB 的写性能非常高,适合一些数据经常变化又对数据一致性要求不高的场景,比如浏览量、点赞量 +typedef struct pubsubPattern { + // 订阅的客户端 + redisClient *client; + // 被订阅的模式,比如 channel* + robj *pattern; +} +``` -* MySQL 的 InnoDB Buffer Pool 机制用到了这种策略 +客户端执行 PSUBSCRIBE 命令订阅某个模式,服务器会新建一个 pubsubPattern 结构并赋值,放入 pubsub_patterns 链表结尾 +模式的退订命令 PUNSUBSCRIBE 是订阅命令的反操作,服务器在 pubsub_patterns 链表中查找并删除对应的结构 -**** -### 缓存一致 +*** -使用缓存代表不需要强一致性,只需要最终一致性 -缓存不一致的方法: -* 数据库和缓存数据强一致场景: - * 同步双写:更新 DB 时同样更新 cache,保证在一个事务中,通过加锁来保证更新 cache 时不存在线程安全问题 - - * 延迟双删:先淘汰缓存再写数据库,休眠 1 秒再次淘汰缓存,可以将 1 秒内造成的缓存脏数据再次删除 - - * 异步通知: - - * 基于 MQ 的异步通知:对数据的修改后,代码需要发送一条消息到 MQ 中,缓存服务监听 MQ 消息 - * Canal 订阅 MySQL binlog 的变更上报给 Kafka,系统监听 Kafka 消息触发缓存失效,或者直接将变更发送到处理服务,**没有任何代码侵入** - - 低耦合,可以同时通知多个缓存服务,但是时效性一般,可能存在中间不一致状态 - -* 低一致性场景: +#### 发送消息 - * 更新 DB 的时候同样更新 cache,但是给缓存加一个比较短的过期时间,这样就可以保证即使数据不一致影响也比较小 - * 使用 Redis 自带的内存淘汰机制 +Redis 客户端执行 `PUBLISH ` 命令将消息 message发送给频道 channel,服务器会执行: +* 在 pubsub_channels 字典里找到频道 channel 的订阅者名单,将消息 message 发送给所有订阅者 +* 遍历整个 pubsub_patterns 链表,查找与 channel 频道相**匹配的模式**,并将消息发送给所有订阅了这些模式的客户端 + +```c +// 如果频道和模式相匹配 +if match(channel, pubsubPattern.pattern) { + // 将消息发送给订阅该模式的客户端 + send_message(pubsubPattern.client, message); +} +``` @@ -15843,101 +15792,125 @@ Read-Through Pattern 也存在首次不命中的问题,采用缓存预热解 -### 企业方案 +#### 查看信息 -#### 缓存预热 +PUBSUB 命令用来查看频道或者模式的相关信息 -场景:宕机,服务器启动后迅速宕机 +`PUBSUB CHANNELS [pattern]` 返回服务器当前被订阅的频道,其中 pattern 参数是可选的 -问题排查: +* 如果不给定 pattern 参数,那么命令返回服务器当前被订阅的所有频道 +* 如果给定 pattern 参数,那么命令返回服务器当前被订阅的频道中与 pattern 模式相匹配的频道 -1. 请求数量较高,大量的请求过来之后都需要去从缓存中获取数据,但是缓存中又没有,此时从数据库中查找数据然后将数据再存入缓存,造成了短期内对 redis 的高强度操作从而导致问题 +`PUBSUB NUMSUB [channel-1 channel-2 ... channel-n]` 命令接受任意多个频道作为输入参数,并返回这些频道的订阅者数量 -2. 主从之间数据吞吐量较大,数据同步操作频度较高 +`PUBSUB NUMPAT` 命令用于返回服务器当前被订阅模式的数量 -解决方案: -- 前置准备工作: - 1. 日常例行统计数据访问记录,统计访问频度较高的热点数据 - 2. 利用 LRU 数据删除策略,构建数据留存队列例如:storm 与 kafka 配合 -- 准备工作: +**** - 1. 将统计结果中的数据分类,根据级别,redis 优先加载级别较高的热点数据 - 2. 利用分布式多服务器同时进行数据读取,提速数据加载过程 - 3. 热点数据主从同时预热 +### ACL 指令 -- 实施: +Redis ACL 是 Access Control List(访问控制列表)的缩写,该功能允许根据可以执行的命令和可以访问的键来限制某些连接 - 4. 使用脚本程序固定触发数据预热过程 +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-ACL指令.png) - 5. 如果条件允许,使用了 CDN(内容分发网络),效果会更好 +* acl cat:查看添加权限指令类别 +* acl whoami:查看当前用户 -总的来说:缓存预热就是系统启动前,提前将相关的缓存数据直接加载到缓存系统。避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题,用户直接查询事先被预热的缓存数据! +* acl setuser username on>password ~cached:* +get:设置有用户名、密码、ACL 权限(只能 get) -*** +*** -#### 缓存雪崩 -场景:数据库服务器崩溃,一连串的问题会随之而来 -问题排查:在一个较短的时间内,**缓存中较多的 key 集中过期**,此周期内请求访问过期的数据 Redis 未命中,Redis 向数据库获取数据,数据库同时收到大量的请求无法及时处理。 +### 监视器 -解决方案: +MONITOR 命令,可以将客户端变为一个监视器,实时地接收并打印出服务器当前处理的命令请求的相关信息 -1. 加锁,慎用 -2. 设置热点数据永远不过期,如果缓存数据库是分布式部署,将热点数据均匀分布在不同搞得缓存数据库中 -3. 缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生 -4. 构建**多级缓存**架构,Nginx 缓存 + Redis 缓存 + ehcache 缓存 -5. 灾难预警机制,监控 Redis 服务器性能指标,CPU 使用率、内存容量、平均响应时间、线程数 -6. 限流、降级:短时间范围内牺牲一些客户体验,限制一部分请求访问,降低应用服务器压力,待业务低速运转后再逐步放开访问 +```c +// 实现原理 +def MONITOR(): + // 打开客户端的监视器标志 + client.flags |= REDIS_MONITOR + + // 将客户端添加到服务器状态的 redisServer.monitors链表的末尾 + server.monitors.append(client) + // 向客户端返回 ok + send_reply("OK") +``` +服务器每次处理命令请求都会调用 replicationFeedMonitors 函数,函数将被处理的命令请求的相关信息**发送给各个监视器** -总的来说:缓存雪崩就是瞬间过期数据量太大,导致对数据库服务器造成压力。如能够有效避免过期时间集中,可以有效解决雪崩现象的出现(约 40%),配合其他策略一起使用,并监控服务器的运行数据,根据运行记录做快速调整。 + +```sh +redis> MONITOR +OK +1378822099.421623 [0 127.0.0.1:56604] "PING" +1378822105.089572 [0 127.0.0.1:56604] "SET" "msg" "hello world" +1378822109.036925 [0 127.0.0.1:56604] "SET" "number" "123" +1378822140.649496 (0 127.0.0.1:56604] "SADD" "fruits" "Apple" "Banana" "Cherry" +1378822154.117160 [0 127.0.0.1:56604] "EXPIRE" "msg" "10086" +1378822257.329412 [0 127.0.0.1:56604] "KEYS" "*" +1378822258.690131 [0 127.0.0.1:56604] "DBSIZE" +``` -*** -#### 缓存击穿 +*** -缓存击穿也叫热点 Key 问题 -场景:系统平稳运行过程中,数据库连接量瞬间激增,Redis 服务器无大量 key 过期,Redis 内存平稳无波动,Redis 服务器 CPU 正常,但是数据库崩溃 -问题排查: +### 批处理 -1. **Redis 中某个 key 过期,该 key 访问量巨大** +Redis 的管道 Pipeline 机制可以一次处理多条指令 -2. 多个数据请求从服务器直接压到 Redis 后,均未命中 +* Pipeline 中的多条命令非原子性,因为在向管道内添加命令时,其他客户端的发送的命令仍然在执行 +* 原生批命令(mset 等)是服务端实现,而 pipeline 需要服务端与客户端共同完成 -3. Redis 在短时间内发起了大量对数据库中同一数据的访问 +使用 Pipeline 封装的命令数量不能太多,数据量过大会增加客户端的等待时间,造成网络阻塞,Jedis 中的 Pipeline 使用方式: -简而言之两点:单个 key 高热数据,key 过期 +```java +// 创建管道 +Pipeline pipeline = jedis.pipelined(); +for (int i = 1; i <= 100000; i++) { + // 放入命令到管道 + pipeline.set("key_" + i, "value_" + i); + if (i % 1000 == 0) { + // 每放入1000条命令,批量执行 + pipeline.sync(); + } +} +``` -解决方案: +集群下模式下,批处理命令的多个 key 必须落在一个插槽中,否则就会导致执行失败,N 条批处理命令的优化方式: -1. 预先设定:以电商为例,每个商家根据店铺等级,指定若干款主打商品,在购物节期间,加大此类信息 key 的过期时长 注意:购物节不仅仅指当天,以及后续若干天,访问峰值呈现逐渐降低的趋势 +* 串行命令:for 循环遍历,依次执行每个命令 +* 串行 slot:在客户端计算每个 key 的 slot,将 slot 一致的分为一组,每组都利用 Pipeline 批处理,串行执行各组命令 +* 并行 slot:在客户端计算每个 key 的 slot,将 slot 一致的分为一组,每组都利用 Pipeline 批处理,**并行执行各组命令** +* hash_tag:将所有 key 设置相同的 hash_tag,则所有 key 的 slot 一定相同 -2. 现场调整:监控访问量,对自然流量激增的数据**延长过期时间或设置为永久性 key** +| | 耗时 | 优点 | 缺点 | +| --------- | ------------------------------------------------- | -------------------- | -------------------- | +| 串行命令 | N 次网络耗时 + N 次命令耗时 | 实现简单 | 耗时久 | +| 串行 slot | m 次网络耗时 + N 次命令耗时,m = key 的 slot 个数 | 耗时较短 | 实现稍复杂 | +| 并行 slot | 1 次网络耗时 + N 次命令耗时 | 耗时非常短 | 实现复杂 | +| hash_tag | 1 次网络耗时 + N 次命令耗时 | 耗时非常短、实现简单 | 容易出现**数据倾斜** | -3. 后台刷新数据:启动定时任务,高峰期来临之前,刷新数据有效期,确保不丢失 -4. **二级缓存**:设置不同的失效时间,保障不会被同时淘汰就行 -5. 加锁:分布式锁,防止被击穿,但是要注意也是性能瓶颈,慎重 -总的来说:缓存击穿就是单个高热数据过期的瞬间,数据访问量较大,未命中 Redis 后,发起了大量对同一数据的数据库访问,导致对数据库服务器造成压力。应对策略应该在业务数据分析与预防方面进行,配合运行监控测试与即时调整策略,毕竟单个 key 的过期监控难度较高,配合雪崩处理策略即可 @@ -15945,11 +15918,222 @@ Read-Through Pattern 也存在首次不命中的问题,采用缓存预热解 -#### 缓存穿透 -场景:系统平稳运行过程中,应用服务器流量随时间增量较大,Redis 服务器命中率随时间逐步降低,Redis 内存平稳,内存无压力,Redis 服务器 CPU 占用激增,数据库服务器压力激增,数据库崩溃 -问题排查: +## 解决方案 + +### 缓存方案 + +#### 缓存模式 + +##### 旁路缓存 + +缓存本质:弥补 CPU 的高算力和 IO 的慢读写之间巨大的鸿沟 + +旁路缓存模式 Cache Aside Pattern 是平时使用比较多的一个缓存读写模式,比较适合读请求比较多的场景 + +Cache Aside Pattern 中服务端需要同时维系 DB 和 cache,并且是以 DB 的结果为准 + +* 写操作:先更新 DB,然后直接删除 cache +* 读操作:从 cache 中读取数据,读取到就直接返回;读取不到就从 DB 中读取数据返回,并放到 cache + +时序导致的不一致问题: + +* 在写数据的过程中,不能先删除 cache 再更新 DB,因为会造成缓存的不一致。比如请求 1 先写数据 A,请求 2 随后读数据 A,当请求 1 删除 cache 后,请求 2 直接读取了 DB,此时请求 1 还没写入 DB(延迟双删) + +* 在写数据的过程中,先更新 DB 再删除 cache 也会出现问题,但是概率很小,因为缓存的写入速度非常快 + +旁路缓存的缺点: + +* 首次请求数据一定不在 cache 的问题,一般采用缓存预热的方法,将热点数据可以提前放入 cache 中 +* 写操作比较频繁的话导致 cache 中的数据会被频繁被删除,影响缓存命中率 + +删除缓存而不是更新缓存的原因:每次更新数据库都更新缓存,造成无效写操作较多 + + + + + +**** + + + +##### 读写穿透 + +读写穿透模式 Read/Write Through Pattern:服务端把 cache 视为主要数据存储,从中读取数据并将数据写入其中,cache 负责将此数据同步写入 DB,从而减轻了应用程序的职责 + +* 写操作:先查 cache,cache 中不存在,直接更新 DB;cache 中存在则先更新 cache,然后 cache 服务更新 DB(同步更新 cache 和 DB) + +* 读操作:从 cache 中读取数据,读取到就直接返回 ;读取不到先从 DB 加载,写入到 cache 后返回响应 + + Read-Through Pattern 实际只是在 Cache-Aside Pattern 之上进行了封装。在 Cache-Aside Pattern 下,发生读请求的时候,如果 cache 中不存在对应的数据,是由客户端负责把数据写入 cache,而 Read Through Pattern 则是 cache 服务自己来写入缓存的,对客户端是透明的 + +Read-Through Pattern 也存在首次不命中的问题,采用缓存预热解决 + + + +*** + + + +##### 异步缓存 + +异步缓存写入 Write Behind Pattern 由 cache 服务来负责 cache 和 DB 的读写,对比读写穿透不同的是 Write Behind Caching 是只更新缓存,不直接更新 DB,改为**异步批量**的方式来更新 DB,可以减小写的成本 + +缺点:这种模式对数据一致性没有高要求,可能出现 cache 还没异步更新 DB,服务就挂掉了 + +应用: + +* DB 的写性能非常高,适合一些数据经常变化又对数据一致性要求不高的场景,比如浏览量、点赞量 + +* MySQL 的 InnoDB Buffer Pool 机制用到了这种策略 + + + +**** + + + +#### 缓存一致 + +使用缓存代表不需要强一致性,只需要最终一致性 + +缓存不一致的方法: + +* 数据库和缓存数据强一致场景: + + * 同步双写:更新 DB 时同样更新 cache,保证在一个事务中,通过加锁来保证更新 cache 时不存在线程安全问题 + + * 延迟双删:先淘汰缓存再写数据库,休眠 1 秒再次淘汰缓存,可以将 1 秒内造成的缓存脏数据再次删除 + + * 异步通知: + + * 基于 MQ 的异步通知:对数据的修改后,代码需要发送一条消息到 MQ 中,缓存服务监听 MQ 消息 + * Canal 订阅 MySQL binlog 的变更上报给 Kafka,系统监听 Kafka 消息触发缓存失效,或者直接将变更发送到处理服务,**没有任何代码侵入** + + 低耦合,可以同时通知多个缓存服务,但是时效性一般,可能存在中间不一致状态 + +* 低一致性场景: + + * 更新 DB 的时候同样更新 cache,但是给缓存加一个比较短的过期时间,这样就可以保证即使数据不一致影响也比较小 + * 使用 Redis 自带的内存淘汰机制 + + + + + + +*** + + + +#### 缓存问题 + +##### 缓存预热 + +场景:宕机,服务器启动后迅速宕机 + +问题排查: + +1. 请求数量较高,大量的请求过来之后都需要去从缓存中获取数据,但是缓存中又没有,此时从数据库中查找数据然后将数据再存入缓存,造成了短期内对 redis 的高强度操作从而导致问题 + +2. 主从之间数据吞吐量较大,数据同步操作频度较高 + +解决方案: + +- 前置准备工作: + + 1. 日常例行统计数据访问记录,统计访问频度较高的热点数据 + + 2. 利用 LRU 数据删除策略,构建数据留存队列例如:storm 与 kafka 配合 + +- 准备工作: + + 1. 将统计结果中的数据分类,根据级别,redis 优先加载级别较高的热点数据 + + 2. 利用分布式多服务器同时进行数据读取,提速数据加载过程 + + 3. 热点数据主从同时预热 + +- 实施: + + 4. 使用脚本程序固定触发数据预热过程 + + 5. 如果条件允许,使用了 CDN(内容分发网络),效果会更好 + +总的来说:缓存预热就是系统启动前,提前将相关的缓存数据直接加载到缓存系统。避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题,用户直接查询事先被预热的缓存数据! + + + +*** + + + +##### 缓存雪崩 + +场景:数据库服务器崩溃,一连串的问题会随之而来 + +问题排查:在一个较短的时间内,**缓存中较多的 key 集中过期**,此周期内请求访问过期的数据 Redis 未命中,Redis 向数据库获取数据,数据库同时收到大量的请求无法及时处理。 + +解决方案: + +1. 加锁,慎用 +2. 设置热点数据永远不过期,如果缓存数据库是分布式部署,将热点数据均匀分布在不同搞得缓存数据库中 +3. 缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生 +4. 构建**多级缓存**架构,Nginx 缓存 + Redis 缓存 + ehcache 缓存 +5. 灾难预警机制,监控 Redis 服务器性能指标,CPU 使用率、内存容量、平均响应时间、线程数 +6. 限流、降级:短时间范围内牺牲一些客户体验,限制一部分请求访问,降低应用服务器压力,待业务低速运转后再逐步放开访问 + + +总的来说:缓存雪崩就是瞬间过期数据量太大,导致对数据库服务器造成压力。如能够有效避免过期时间集中,可以有效解决雪崩现象的出现(约 40%),配合其他策略一起使用,并监控服务器的运行数据,根据运行记录做快速调整。 + + + +*** + + + +##### 缓存击穿 + +缓存击穿也叫热点 Key 问题 + +场景:系统平稳运行过程中,数据库连接量瞬间激增,Redis 服务器无大量 key 过期,Redis 内存平稳无波动,Redis 服务器 CPU 正常,但是数据库崩溃 + +问题排查: + +1. **Redis 中某个 key 过期,该 key 访问量巨大** + +2. 多个数据请求从服务器直接压到 Redis 后,均未命中 + +3. Redis 在短时间内发起了大量对数据库中同一数据的访问 + +简而言之两点:单个 key 高热数据,key 过期 + +解决方案: + +1. 预先设定:以电商为例,每个商家根据店铺等级,指定若干款主打商品,在购物节期间,加大此类信息 key 的过期时长 注意:购物节不仅仅指当天,以及后续若干天,访问峰值呈现逐渐降低的趋势 + +2. 现场调整:监控访问量,对自然流量激增的数据**延长过期时间或设置为永久性 key** + +3. 后台刷新数据:启动定时任务,高峰期来临之前,刷新数据有效期,确保不丢失 + +4. **二级缓存**:设置不同的失效时间,保障不会被同时淘汰就行 + +5. 加锁:分布式锁,防止被击穿,但是要注意也是性能瓶颈,慎重 + +总的来说:缓存击穿就是单个高热数据过期的瞬间,数据访问量较大,未命中 Redis 后,发起了大量对同一数据的数据库访问,导致对数据库服务器造成压力。应对策略应该在业务数据分析与预防方面进行,配合运行监控测试与即时调整策略,毕竟单个 key 的过期监控难度较高,配合雪崩处理策略即可 + + + +*** + + + +##### 缓存穿透 + +场景:系统平稳运行过程中,应用服务器流量随时间增量较大,Redis 服务器命中率随时间逐步降低,Redis 内存平稳,内存无压力,Redis 服务器 CPU 占用激增,数据库服务器压力激增,数据库崩溃 + +问题排查: 1. Redis 中大面积出现未命中 @@ -15990,6 +16174,37 @@ Read-Through Pattern 也存在首次不命中的问题,采用缓存预热解 + + +### Key 设计 + +大 Key:通常以 Key 的大小和 Key 中成员的数量来综合判定,引发的问题: + +- 客户端执行命令的时长变慢 +- Redis 内存达到 maxmemory 定义的上限引发操作阻塞或重要的 Key 被逐出,甚至引发内存溢出(OOM) +- 集群架构下,某个数据分片的内存使用率远超其他数据分片,使**数据分片的内存资源不均衡** +- 对大 Key 执行读请求,会使 Redis 实例的带宽使用率被占满,导致自身服务变慢,同时易波及相关的服务 +- 对大 Key 执行删除操作,会造成主库较长时间的阻塞,进而可能引发同步中断或主从切换 + +热 Key:通常以其接收到的 Key 被请求频率来判定,引发的问题: + +- 占用大量的 CPU 资源,影响其他请求并导致整体性能降低 +- 分布式集群架构下,产生**访问倾斜**,即某个数据分片被大量访问,而其他数据分片处于空闲状态,可能引起该数据分片的连接数被耗尽,新的连接建立请求被拒绝等问题 +- 在抢购或秒杀场景下,可能因商品对应库存 Key 的请求量过大,超出 Redis 处理能力造成超卖 +- 热 Key 的请求压力数量超出 Redis 的承受能力易造成缓存击穿,即大量请求将被直接指向后端的存储层,导致存储访问量激增甚至宕机,从而影响其他业务 + + + +参考文档:https://help.aliyun.com/document_detail/353223.html + + + +*** + + + + + ### 性能指标 Redis 中的监控指标如下: @@ -16156,247 +16371,6 @@ Redis 中的监控指标如下: -**** - - - - - -## 其他操作 - -### 发布订阅 - -#### 基本指令 - -Redis 发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息 - -Redis 客户端可以订阅任意数量的频道,每当有客户端向被订阅的频道发送消息(message)时,频道的**所有订阅者都会收到消息** - -![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-发布订阅.png) - -操作过程: - -* 打开一个客户端订阅 channel1:`SUBSCRIBE channel1` - -* 打开另一个客户端,给 channel1 发布消息 hello:`PUBLISH channel1 hello` - -* 第一个客户端可以看到发送的消息 - - - -客户端还可以通过 PSUBSCRIBE 命令订阅一个或多个模式,每当有其他客户端向某个频道发送消息时,消息不仅会被发送给这个频道的所有订阅者,还会被**发送给所有与这个频道相匹配的模式的订阅者**,比如 `PSUBSCRIBE channel*` 订阅模式,与 channel1 匹配 - -注意:发布的消息没有持久化,所以订阅的客户端只能收到订阅后发布的消息 - - - - - -*** - - - -#### 频道操作 - -Redis 将所有频道的订阅关系都保存在服务器状态的 pubsub_channels 字典里,键是某个被订阅的频道,值是一个记录所有订阅这个频道的客户端链表 - -```c -struct redisServer { - // 保存所有频道的订阅关系, - dict *pubsub_channels; -} -``` - -客户端执行 SUBSCRIBE 命令订阅某个或某些频道,服务器会将客户端与频道进行关联: - -* 频道已经存在,直接将客户端添加到链表末尾 -* 频道还未有任何订阅者,在字典中为频道创建一个键值对,再将客户端添加到链表 - -UNSUBSCRIBE 命令用来退订某个频道,服务器将从 pubsub_channels 中解除客户端与被退订频道之间的关联 - - - - - -**** - - - -#### 模式操作 - -Redis 服务器将所有模式的订阅关系都保存在服务器状态的 pubsub_patterns 属性里 - -```c -struct redisServer { - // 保存所有模式订阅关系,链表中每个节点是一个 pubsubPattern - list *pubsub_patterns; -} - -typedef struct pubsubPattern { - // 订阅的客户端 - redisClient *client; - // 被订阅的模式,比如 channel* - robj *pattern; -} -``` - -客户端执行 PSUBSCRIBE 命令订阅某个模式,服务器会新建一个 pubsubPattern 结构并赋值,放入 pubsub_patterns 链表结尾 - -模式的退订命令 PUNSUBSCRIBE 是订阅命令的反操作,服务器在 pubsub_patterns 链表中查找并删除对应的结构 - - - - - -*** - - - -#### 发送消息 - -Redis 客户端执行 `PUBLISH ` 命令将消息 message发送给频道 channel,服务器会执行: - -* 在 pubsub_channels 字典里找到频道 channel 的订阅者名单,将消息 message 发送给所有订阅者 -* 遍历整个 pubsub_patterns 链表,查找与 channel 频道相**匹配的模式**,并将消息发送给所有订阅了这些模式的客户端 - -```c -// 如果频道和模式相匹配 -if match(channel, pubsubPattern.pattern) { - // 将消息发送给订阅该模式的客户端 - send_message(pubsubPattern.client, message); -} -``` - - - - - -*** - - - -#### 查看信息 - -PUBSUB 命令用来查看频道或者模式的相关信息 - -`PUBSUB CHANNELS [pattern]` 返回服务器当前被订阅的频道,其中 pattern 参数是可选的 - -* 如果不给定 pattern 参数,那么命令返回服务器当前被订阅的所有频道 -* 如果给定 pattern 参数,那么命令返回服务器当前被订阅的频道中与 pattern 模式相匹配的频道 - -`PUBSUB NUMSUB [channel-1 channel-2 ... channel-n]` 命令接受任意多个频道作为输入参数,并返回这些频道的订阅者数量 - -`PUBSUB NUMPAT` 命令用于返回服务器当前被订阅模式的数量 - - - - - -**** - - - -### ACL 指令 - -Redis ACL 是 Access Control List(访问控制列表)的缩写,该功能允许根据可以执行的命令和可以访问的键来限制某些连接 - -![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-ACL指令.png) - -* acl cat:查看添加权限指令类别 -* acl whoami:查看当前用户 - -* acl setuser username on>password ~cached:* +get:设置有用户名、密码、ACL 权限(只能 get) - - - - - -*** - - - -### 监视器 - -MONITOR 命令,可以将客户端变为一个监视器,实时地接收并打印出服务器当前处理的命令请求的相关信息 - -```c -// 实现原理 -def MONITOR(): - // 打开客户端的监视器标志 - client.flags |= REDIS_MONITOR - - // 将客户端添加到服务器状态的 redisServer.monitors链表的末尾 - server.monitors.append(client) - // 向客户端返回 ok - send_reply("OK") -``` - -服务器每次处理命令请求都会调用 replicationFeedMonitors 函数,函数将被处理的命令请求的相关信息**发送给各个监视器** - - - -```sh -redis> MONITOR -OK -1378822099.421623 [0 127.0.0.1:56604] "PING" -1378822105.089572 [0 127.0.0.1:56604] "SET" "msg" "hello world" -1378822109.036925 [0 127.0.0.1:56604] "SET" "number" "123" -1378822140.649496 (0 127.0.0.1:56604] "SADD" "fruits" "Apple" "Banana" "Cherry" -1378822154.117160 [0 127.0.0.1:56604] "EXPIRE" "msg" "10086" -1378822257.329412 [0 127.0.0.1:56604] "KEYS" "*" -1378822258.690131 [0 127.0.0.1:56604] "DBSIZE" -``` - - - - - -*** - - - -### 批处理 - -Redis 的管道 Pipeline 机制可以一次处理多条指令 - -* Pipeline 中的多条命令非原子性,因为在向管道内添加命令时,其他客户端的发送的命令仍然在执行 -* 原生批命令(mset 等)是服务端实现,而 pipeline 需要服务端与客户端共同完成 - -使用 Pipeline 封装的命令数量不能太多,数据量过大会增加客户端的等待时间,造成网络阻塞,Jedis 中的 Pipeline 使用方式: - -```java -// 创建管道 -Pipeline pipeline = jedis.pipelined(); -for (int i = 1; i <= 100000; i++) { - // 放入命令到管道 - pipeline.set("key_" + i, "value_" + i); - if (i % 1000 == 0) { - // 每放入1000条命令,批量执行 - pipeline.sync(); - } -} -``` - -集群下模式下,批处理命令的多个 key 必须落在一个插槽中,否则就会导致执行失败,N 条批处理命令的优化方式: - -* 串行命令:for 循环遍历,依次执行每个命令 -* 串行 slot:在客户端计算每个 key 的 slot,将 slot 一致的分为一组,每组都利用 Pipeline 批处理,串行执行各组命令 -* 并行 slot:在客户端计算每个 key 的 slot,将 slot 一致的分为一组,每组都利用 Pipeline 批处理,**并行执行各组命令** -* hash_tag:将所有 key 设置相同的 hash_tag,则所有 key 的 slot 一定相同 - -| | 耗时 | 优点 | 缺点 | -| --------- | ------------------------------------------------- | -------------------- | -------------------- | -| 串行命令 | N 次网络耗时 + N 次命令耗时 | 实现简单 | 耗时久 | -| 串行 slot | m 次网络耗时 + N 次命令耗时,m = key 的 slot 个数 | 耗时较短 | 实现稍复杂 | -| 并行 slot | 1 次网络耗时 + N 次命令耗时 | 耗时非常短 | 实现复杂 | -| hash_tag | 1 次网络耗时 + N 次命令耗时 | 耗时非常短、实现简单 | 容易出现**数据倾斜** | - - - - - - - diff --git a/Frame.md b/Frame.md index 7baad52..4f33c36 100644 --- a/Frame.md +++ b/Frame.md @@ -1623,7 +1623,7 @@ Netty 主要基于主从 Reactors 多线程模型做了一定的改进,Netty - 处理 I/O 事件,即 read,write 事件,在对应 NioSocketChannel 处理 - 处理任务队列的任务,即 runAllTasks -6. 每个 Worker NioEventLoop 处理业务时,会使用 pipeline(管道),pipeline 中包含了 channel,即通过 pipeline 可以获取到对应通道,管道中维护了很多的处理器 Handler +6. 每个 Worker NioEventLoop 处理业务时,会使用 Pipeline(管道),Pipeline 中包含了 Channel,即通过 Pipeline 可以获取到对应通道,管道中维护了很多的处理器 Handler @@ -1641,9 +1641,9 @@ Netty 主要基于主从 Reactors 多线程模型做了一定的改进,Netty 开发简单的服务器端和客户端,基本介绍: -* channel 理解为数据的通道,把 msg 理解为流动的数据,最开始输入是 ByteBuf,但经过 pipeline 的加工,会变成其它类型对象,最后输出又变成 ByteBuf -* handler 理解为数据的处理工序,pipeline 负责发布事件传播给每个 handler,handler 对自己感兴趣的事件进行处理(重写了相应事件处理方法),分 Inbound 和 Outbound 两类 -* eventLoop 理解为处理数据的执行者,既可以执行 IO 操作,也可以进行任务处理。每个执行者有任务队列,队列里可以堆放多个 channel 的待处理任务,任务分为普通任务、定时任务。按照 pipeline 顺序,依次按照 handler 的规划(代码)处理数据 +* Channel 理解为数据的通道,把 msg 理解为流动的数据,最开始输入是 ByteBuf,但经过 Pipeline 的加工,会变成其它类型对象,最后输出又变成 ByteBuf +* Handler 理解为数据的处理工序,Pipeline 负责发布事件传播给每个 Handler,Handler 对自己感兴趣的事件进行处理(重写了相应事件处理方法),分 Inbound 和 Outbound 两类 +* EventLoop 理解为处理数据的执行者,既可以执行 IO 操作,也可以进行任务处理。每个执行者有任务队列,队列里可以堆放多个 Channel 的待处理任务,任务分为普通任务、定时任务。按照 Pipeline 顺序,依次按照 Handler 的规划(代码)处理数据 代码实现: @@ -1706,7 +1706,7 @@ Netty 主要基于主从 Reactors 多线程模型做了一定的改进,Netty new Bootstrap() // 2. 添加 EventLoop .group(new NioEventLoopGroup()) - //.option() //给 SocketChannel 配置参数 + //.option(),给 SocketChannel 配置参数 // 3. 选择客户端 channel 实现 .channel(NioSocketChannel.class) // 4. 添加处理器 @@ -1793,7 +1793,6 @@ public class EventLoopServer { public void channelRead(ChannelHandlerContext ctx, Object msg) { ByteBuf buf = (ByteBuf) msg; log.debug(buf.toString(Charset.defaultCharset())); - } }); } @@ -1885,8 +1884,13 @@ public class ChannelClient { @Override // nio 线程连接建立好以后,回调该方法 public void operationComplete(ChannelFuture future) throws Exception { - Channel channel = future.channel(); - channel.writeAndFlush("hello, world"); + if (future.isSuccess()) { + Channel channel = future.channel(); + channel.writeAndFlush("hello, world"); + } else { + // 建立失败,需要关闭 + future.channel().close(); + } } }); } @@ -1908,15 +1912,7 @@ public class CloseFutureClient { public static void main(String[] args) throws InterruptedException { NioEventLoopGroup group = new NioEventLoopGroup(); ChannelFuture channelFuture = new Bootstrap() - .group(group) - .channel(NioSocketChannel.class) - .handler(new ChannelInitializer() { - @Override - protected void initChannel(NioSocketChannel ch) throws Exception { - ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG)); - ch.pipeline().addLast(new StringEncoder()); - } - }) + // .... .connect(new InetSocketAddress("127.0.0.1", 8080)); Channel channel = channelFuture.sync().channel(); // 发送数据 @@ -2065,7 +2061,7 @@ public static void main(String[] args) { protected void initChannel(NioSocketChannel ch) throws Exception { // 1. 通过 channel 拿到 pipeline ChannelPipeline pipeline = ch.pipeline(); - // 2. 添加处理器 head -> h1 -> h2 -> h3 -> h4 -> h5 -> h6 -> tail + // 2. 添加处理器 head -> h1 -> h2 -> h3 -> h4 -> tail pipeline.addLast("h1", new ChannelInboundHandlerAdapter() { @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { @@ -2219,7 +2215,7 @@ ByteBuf 由四部分组成,最开始读写指针(**双指针**)都在 0 * 写入几位写指针后移几位,指向可以写入的位置 * 网络传输,默认习惯是 Big Endian -扩容:写入数据时,容量不够了(初始容量是 10),这时会引发扩容 +扩容:写入数据时,容量不够了(初始容量是 10),这时会引发**扩容** * 如果写入后数据大小未超过 512,则选择下一个 16 的整数倍,例如写入后大小为 12 ,则扩容后 capacity 是 16 * 如果写入后数据大小超过 512,则选择下一个 2^n,例如写入后大小为 513,则扩容后 capacity 是 2^10 = 1024(2^9=512 不够) @@ -2264,16 +2260,14 @@ try { Pipeline 的存在,需要将 ByteBuf 传递给下一个 ChannelHandler,如果在 finally 中 release 了,就失去了传递性,处理规则: -* 创建 ByteBuf 放入 pipeline +* 创建 ByteBuf 放入 Pipeline * 入站 ByteBuf 处理原则 - * 对原始 ByteBuf 不做处理,调用 ctx.fireChannelRead(msg) 向后传递,这时无须 release + * 对原始 ByteBuf 不做处理,调用 ctx.fireChannelRead(msg) 向后传递,这时无须 release,反之不传递需要 * 将原始 ByteBuf 转换为其它类型的 Java 对象,这时 ByteBuf 就没用了,此时必须 release - * 如果不调用 ctx.fireChannelRead(msg) 向后传递,那么也必须 release - * 如果出现异常,ByteBuf 没有成功传递到下一个 ChannelHandler,必须 release * 假设消息一直向后传,那么 TailContext 会负责释放未处理消息(原始的 ByteBuf) @@ -2295,7 +2289,7 @@ Pipeline 的存在,需要将 ByteBuf 传递给下一个 ChannelHandler,如 return false; } ``` - + * 出站 ByteBuf 处理原则 * 出站消息最终都会转为 ByteBuf 输出,一直向前传,由 HeadContext flush 后 release @@ -2702,7 +2696,7 @@ public class HttpDemo { // 只针对某一种类型的请求处理,此处针对 HttpRequest ch.pipeline().addLast(new SimpleChannelInboundHandler() { @Override - protected void channelRead0(ChannelHandlerContext ctx, HttpRequest msg) throws Exception { + protected void channelRead0(ChannelHandlerContext ctx, HttpRequest msg) { // 获取请求 log.debug(msg.uri()); @@ -3060,7 +3054,7 @@ Codec(编解码器)的组成部分有两个:Decoder(解码器)和 Enco -Protobuf 是 Google 发布的开源项目,全称 Google Protocol Buffers ,是一种轻便高效的结构化数据存储格式,可以用于结构化数据串行化,或者说序列化。很适合做数据存储或 RPC(远程过程调用 remote procedure call)数据交换格式。目前很多公司从 HTTP + Json 转向 TCP + Protobuf ,效率会更高 +Protobuf 是 Google 发布的开源项目,全称 Google Protocol Buffers ,是一种轻便高效的结构化数据存储格式,可以用于结构化数据串行化,或者说序列化。很适合做数据存储或 RPC(远程过程调用 remote procedure call)数据交换格式。目前很多公司从 HTTP + Json 转向 TCP + Protobuf ,效率会更高 Protobuf 是以 message 的方式来管理数据,支持跨平台、跨语言(客户端和服务器端可以是不同的语言编写的),高性能、高可靠性 @@ -3234,7 +3228,7 @@ HTTP 协议是无状态的,浏览器和服务器间的请求响应一次,下 } }); - //启动服务器 + // 启动服务器 ChannelFuture channelFuture = serverBootstrap.bind(8080).sync(); channelFuture.channel().closeFuture().sync(); @@ -10256,8 +10250,9 @@ Zookeeper 是基于观察者模式设计的分布式服务管理框架,负责 * 集群中只要有半数以上节点存活就能正常服务,所以 Zookeeper 适合部署奇数台服务器 * **全局数据一致**,每个 Server 保存一份相同的数据副本,Client 无论连接到哪个 Server,数据都是一致 * 更新的请求顺序执行,来自同一个 Client 的请求按其发送顺序依次执行 -* 数据更新原子性,一次数据更新要么成功,要么失败 +* **数据更新原子性**,一次数据更新要么成功,要么失败 * 实时性,在一定的时间范围内,Client 能读到最新数据 +* 心跳检测,会定时向各个服务提供者发送一个请求(实际上建立的是一个 Socket 长连接) ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Zookeeper-框架结构.png) @@ -10295,7 +10290,7 @@ Zookeeper 提供的主要功能包括:统一命名服务、统一配置管理 * 集群环境中,需要实时掌握每个集群节点的状态,可以将这些信息放入 ZNode,通过监控通知的机制实现 -* 实现客户端实时观察服务器上下线的变化 +* 实现客户端实时观察服务器上下线的变化,通过心跳检测实现 @@ -10339,7 +10334,7 @@ Zookeeper 提供的主要功能包括:统一命名服务、统一配置管理 Zookeeper 中的配置文件 zoo.cfg 中参数含义解读: -* tickTime = 2000:通信心跳时间,Zookeeper 服务器与客户端心跳时间,单位毫秒 +* tickTime = 2000:通信心跳时间,**Zookeeper 服务器与客户端心跳**时间,单位毫秒 * initLimit = 10:Leader 与 Follower 初始通信时限,初始连接时能容忍的最多心跳次数 * syncLimit = 5:Leader 与 Follower 同步通信时限,LF 通信时间超过 `syncLimit * tickTime`,Leader 认为 Follwer 下线 * dataDir:保存 Zookeeper 中的数据目录,默认是 tmp目录,容易被 Linux 系统定期删除,所以建议修改 @@ -10520,7 +10515,7 @@ Zookeepe 集群三个角色: 相关属性: * SID:服务器 ID,用来唯一标识一台集群中的机器,和 myid 一致 -* ZXID:事务ID,用来标识一次服务器状态的变更,在某一时刻集群中每台机器的 ZXID 值不一定完全一致,这和 ZooKeeper 服务器对于客户端更新请求的处理逻辑有关 +* ZXID:事务 ID,用来标识一次服务器状态的变更,在某一时刻集群中每台机器的 ZXID 值不一定完全一致,这和 ZooKeeper 服务器对于客户端更新请求的处理逻辑有关 * Epoch:每个 Leader 任期的代号,同一轮选举投票过程中的该值是相同的,投完一次票就增加 @@ -10858,7 +10853,7 @@ public class DistributeServer { } private void register(String hostname) throws KeeperException, InterruptedException { - // OPEN_ACL_UNSAFE: ACL开放 + // OPEN_ACL_UNSAFE: ACL 开放 // EPHEMERAL_SEQUENTIAL: 临时顺序节点 String create = zk.create("/servers/" + hostname, hostname.getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL); diff --git a/SSM.md b/SSM.md index 2f16964..9e40cb7 100644 --- a/SSM.md +++ b/SSM.md @@ -4730,9 +4730,7 @@ UserService userService = (UserService)bf.getBean("userService"); ##### FactoryBean -繁琐的 bean 初始化过程处理: - -* FactoryBean:对单一的 bean 的初始化过程进行封装,达到简化配置的目的 +FactoryBean:对单一的 bean 的初始化过程进行封装,达到简化配置的目的 FactoryBean与 BeanFactory 区别: @@ -15235,7 +15233,7 @@ Web 部分源码详解:SpringMVC → 运行原理 ### 内嵌容器 -SpringBoot 嵌入式 Servlet 容器,默认支持的 webServe:Tomcat、Jetty、Undertow +SpringBoot 嵌入式 Servlet 容器,默认支持的 WebServe:Tomcat、Jetty、Undertow 配置方式: @@ -15814,9 +15812,7 @@ Spring Boot 2.2.0 版本开始引入 JUnit 5 作为单元测试默认库,由 @SpringBootTest class Boot05WebAdminApplicationTests { @Test - void contextLoads() { - - } + void contextLoads() { } } ``` From 8345cccd84be856286acd2424606ded2383fb001 Mon Sep 17 00:00:00 2001 From: Seazean Date: 2022年5月24日 22:46:43 +0800 Subject: [PATCH 09/35] Update Java Note --- Frame.md | 2 +- SSM.md | 36 ++++-------------------------------- 2 files changed, 5 insertions(+), 33 deletions(-) diff --git a/Frame.md b/Frame.md index 4f33c36..65b948a 100644 --- a/Frame.md +++ b/Frame.md @@ -10744,7 +10744,7 @@ get –w /path # 监听【节点数据】的变化 * 在主线程中创建 Zookeeper 客户端,这时就会创建**两个线程**,一个负责网络连接通信(connet),一个负责监听(listener) * 通过 connect 线程将注册的监听事件发送给 Zookeeper -* 在 Zookeeper 的注册监听器列表中将注册的监听事件添加到列表中 +* 在 Zookeeper 的注册监听器列表中将注册的**监听事件添加到列表**中 * Zookeeper 监听到有数据或路径变化,将消息发送给 listener 线程 * listener 线程内部调用 process() 方法 diff --git a/SSM.md b/SSM.md index 9e40cb7..6939b15 100644 --- a/SSM.md +++ b/SSM.md @@ -10579,38 +10579,10 @@ public String requestParam1(String name ,int age){ SpringMVC 对接收的数据进行自动类型转换,该工作通过 Converter 接口实现: -* **标量转换器** - StringToBooleanConverter String → Boolean - ObjectToStringConverter Object → String - StringToNumberConverterFactory String → Number( Integer、 Long 等) - NumberToNumberConverterFactory Number子类型之间(Integer、 Long、 Double 等) - StringToCharacterConverter String → java.lang.Character - NumberToCharacterConverter Number子类型(Integer、 Long、 Double 等)→ java.lang.Character - CharacterToNumberFactory java.lang.Character → Number子类型(Integer、Long、Double 等) - StringToEnumConverterFactory String → enum类型 - EnumToStringConverter enum类型 → String - StringToLocaleConverter String → java.util.Local - PropertiesToStringConverter java.util.Properties → String - StringToPropertiesConverter String → java.util.Properties - -* **集合、数组相关转换器** - ArrayToCollectionConverter 数组 → 集合( List、 Set) - CollectionToArrayConverter 集合( List、 Set) →数组 - ArrayToArrayConverter 数组间 - CollectionToCollectionConverter 集合间( List、 Set) - MapToMapConverter Map间 - ArrayToStringConverter 数组→String类型 - StringToArrayConverter String →数组, trim后使用","split - ArrayToObjectConverter 数组 → Object - ObjectToArrayConverter Object → 单元素数组 - CollectionToStringConverter 集合( List、 Set) →String - StringToCollectionConverter String → 集合( List、 Set), trim后使用","split - CollectionToObjectConverter 集合 → Object - ObjectToCollectionConverter Object → 单元素集合 -* **默认转换器** - ObjectToObjectConverter Object间 - IdToEntityConverter Id → Entity - FallbackObjectToStringConverter Object → String +* 标量转换器 + +* 集合、数组相关转换器 +* 默认转换器 From dbeec2b2d0daf17f9f1904aa5aa090d9e03d0748 Mon Sep 17 00:00:00 2001 From: Seazean Date: 2022年6月12日 23:13:35 +0800 Subject: [PATCH 10/35] Update Java Note --- DB.md | 17 +-- Java.md | 330 ++++++++++++++++++++++++-------------------------------- 2 files changed, 151 insertions(+), 196 deletions(-) diff --git a/DB.md b/DB.md index 2eed8fa..734e730 100644 --- a/DB.md +++ b/DB.md @@ -3999,6 +3999,8 @@ InnoDB 存储引擎中有页(Page)的概念,页是 MySQL 磁盘管理的 * InnoDB 引擎将若干个地址连接磁盘块,以此来达到页的大小 16KB * 在查询数据时如果一个页中的每条数据都能有助于定位数据记录的位置,这将会减少磁盘 I/O 次数,提高查询效率 +超过 16KB 的一条记录,主键索引页只会存储部分数据和指向**溢出页**的指针,剩余数据都会分散存储在溢出页中 + 数据页物理结构,从上到下: * File Header:上一页和下一页的指针、该页的类型(索引页、数据页、日志页等)、**校验和**、LSN(最近一次修改当前页面时的系统 lsn 值,事务持久性部分详解)等信息 @@ -4147,9 +4149,11 @@ B+ 树为了保持索引的有序性,在插入新值的时候需要做相应 一般选用数据小的字段做索引,字段长度越小,普通索引的叶子节点就越小,普通索引占用的空间也就越小 -自增主键的插入数据模式,可以让主键索引尽量地保持递增顺序插入,不涉及到挪动其他记录,**避免了页分裂** +自增主键的插入数据模式,可以让主键索引尽量地保持递增顺序插入,不涉及到挪动其他记录,**避免了页分裂**,页分裂的目的就是保证后一个数据页中的所有行主键值比前一个数据页中主键值大 + +参考文章:https://developer.aliyun.com/article/919861 @@ -5597,7 +5601,7 @@ MySQL 4.1 版本之后,开始支持 SQL 的子查询 * 优化方式二:方案适用于主键自增的表,可以把 LIMIT 查询转换成某个位置的查询 ```mysql - EXPLAIN SELECT * FROM tb_user_1 WHERE id> 200000 LIMIT 10; -- 写法 1 + EXPLAIN SELECT * FROM tb_user_1 WHERE id> 200000 LIMIT 10; -- 写法 1 EXPLAIN SELECT * FROM tb_user_1 WHERE id BETWEEN 200000 and 200010; -- 写法 2 ``` @@ -10507,7 +10511,7 @@ clientsCron 函数对一定数量的客户端进行以下两个检查: * 如果客户端与服务器之间的连接巳经超时(很长一段时间客户端和服务器都没有互动),那么程序释放这个客户端 * 如果客户端在上一次执行命令请求之后,输入缓冲区的大小超过了一定的长度,那么程序会释放客户端当前的输入缓冲区,并重新创建一个默认大小的输入缓冲区,从而防止客户端的输入缓冲区耗费了过多的内存 -databasesCron 函数会对服务器中的一部分数据库进行检查,删除其中的过期键,并在有需要时 对字典进行收缩操作 +databasesCron 函数会对服务器中的一部分数据库进行检查,删除其中的过期键,并在有需要时对字典进行收缩操作 @@ -10570,7 +10574,7 @@ serverCron 函数会检查 BGSAVE 或者 BGREWRITEAOF 命令是否正在执行 -##### cronloops +##### 执行次数 服务器状态的 cronloops 属性记录了 serverCron 函数执行的次数 @@ -15033,10 +15037,7 @@ def CLUSTER_KEYSLOT(key): reply_client(slot); ``` -判断槽是否由当前节点负责处理: - -* 如果 clusterState.slots[i] 等于 clusterState.myself,那么说明槽 i 由当前节点负责,节点可以执行客户端发送的命令 -* 如果 clusterState.slots[i] 不等于 clusterState.myself,那么说明槽 i 并非由当前节点负责,节点会根据 clusterState.slots[i] 指向的clusterNode 结构所记录的节点 IP 和端口号,向客户端返回 MOVED 错误 +判断槽是否由当前节点负责处理:如果 clusterState.slots[i] 不等于 clusterState.myself,那么说明槽 i 并非由当前节点负责,节点会根据 clusterState.slots[i] 指向的 clusterNode 结构所记录的节点 IP 和端口号,向客户端返回 MOVED 错误 diff --git a/Java.md b/Java.md index 26fbec9..a8c8702 100644 --- a/Java.md +++ b/Java.md @@ -6,25 +6,22 @@ #### 变量类型 -| | 成员变量 | 局部变量 | 静态变量 | -| :------: | :------------: | :------------------------: | :-------------------------: | -| 定义位置 | 在类中,方法外 | 方法中或者方法的形参 | 在类中,方法外 | -| 初始化值 | 有默认初始化值 | 无,先定义,赋值后才能使用 | 有默认初始化值 | -| 调用方法 | 对象调用 | | 对象调用,类名调用 | -| 存储位置 | 堆中 | 栈中 | 方法区(JDK8 以后移到堆中) | -| 生命周期 | 与对象共存亡 | 与方法共存亡 | 与类共存亡 | -| 别名 | 实例变量 | | 类变量,静态成员变量 | +| | 成员变量 | 局部变量 | 静态变量 | +| :------: | :------------: | :------------------: | :-------------------------: | +| 定义位置 | 在类中,方法外 | 方法中或者方法的形参 | 在类中,方法外 | +| 初始化值 | 有默认初始化值 | 无,赋值后才能使用 | 有默认初始化值 | +| 调用方法 | 对象调用 | | 对象调用,类名调用 | +| 存储位置 | 堆中 | 栈中 | 方法区(JDK8 以后移到堆中) | +| 生命周期 | 与对象共存亡 | 与方法共存亡 | 与类共存亡 | +| 别名 | 实例变量 | | 类变量,静态成员变量 | -**静态变量只有一个,成员变量是类中的变量,局部变量是方法中的变量** +静态变量只有一个,成员变量是类中的变量,局部变量是方法中的变量 -初学时笔记内容参考视频:https://www.bilibili.com/video/BV1TE41177mP,随着学习的深入又增加了很多知识 +初学时笔记内容参考视频:https://www.bilibili.com/video/BV1TE41177mP,随着学习的深入又增加很多知识 -给初学者的一些个人建议: -* 初学者对编程的认知比较浅显,一些专有词汇和概念难以理解,所以建议观看视频进行入门,大部分公开课视频讲的比较基础 -* 在有了一定的编程基础后,需要看一些经典书籍和技术博客,来扩容自己的知识广度和深度,可以长期保持记录笔记的好习惯 @@ -40,9 +37,8 @@ Java 语言提供了八种基本类型。六种数字类型(四个整数型, **byte:** -- byte 数据类型是 8 位、有符号的,以**二进制补码**表示的整数,**8 位一个字节**,首位是符号位 -- 最小值是 -128(-2^7) -- 最大值是 127(2^7-1) +- byte 数据类型是 8 位、有符号的,以二进制补码表示的整数,**8 位一个字节**,首位是符号位 +- 最小值是 -128(-2^7)、最大值是 127(2^7-1) - 默认值是 `0` - byte 类型用在大型数组中节约空间,主要代替整数,byte 变量占用的空间只有 int 类型的四分之一 - 例子:`byte a = 100,byte b = -50` @@ -50,8 +46,7 @@ Java 语言提供了八种基本类型。六种数字类型(四个整数型, **short:** - short 数据类型是 16 位、有符号的以二进制补码表示的整数 -- 最小值是 -32768(-2^15) -- 最大值是 32767(2^15 - 1) +- 最小值是 -32768(-2^15)、最大值是 32767(2^15 - 1) - short 数据类型也可以像 byte 那样节省空间,一个 short 变量是 int 型变量所占空间的二分之一 - 默认值是 `0` - 例子:`short s = 1000,short r = -20000` @@ -59,8 +54,7 @@ Java 语言提供了八种基本类型。六种数字类型(四个整数型, **int:** - int 数据类型是 32 位 4 字节、有符号的以二进制补码表示的整数 -- 最小值是 -2,147,483,648(-2^31) -- 最大值是 2,147,483,647(2^31 - 1) +- 最小值是 -2,147,483,648(-2^31)、最大值是 2,147,483,647(2^31 - 1) - 一般地整型变量默认为 int 类型 - 默认值是 `0` - 例子:`int a = 100000, int b = -200000` @@ -68,12 +62,10 @@ Java 语言提供了八种基本类型。六种数字类型(四个整数型, **long:** - long 数据类型是 64 位 8 字节、有符号的以二进制补码表示的整数 -- 最小值是 -9,223,372,036,854,775,808(-2^63) -- 最大值是 9,223,372,036,854,775,807(2^63 -1) +- 最小值是 -9,223,372,036,854,775,808(-2^63)、最大值是 9,223,372,036,854,775,807(2^63 -1) - 这种类型主要使用在需要比较大整数的系统上 - 默认值是 ` 0L` -- 例子: `long a = 100000L,Long b = -200000L` - L 理论上不分大小写,但是若写成 I 容易与数字 1 混淆,不容易分辩,所以最好大写 +- 例子: `long a = 100000L,Long b = -200000L`,L 理论上不分大小写,但是若写成 I 容易与数字 1 混淆,不容易分辩 **float:** @@ -104,7 +96,7 @@ Java 语言提供了八种基本类型。六种数字类型(四个整数型, - char 类型是一个单一的 16 位**两个字节**的 Unicode 字符 - 最小值是 `\u0000`(即为 0) - 最大值是 `\uffff`(即为 65535) -- char 数据类型可以存储任何字符 +- char 数据类型可以**存储任何字符** - 例子:`char c = 'A'`,`char c = '张'` @@ -117,7 +109,7 @@ Java 语言提供了八种基本类型。六种数字类型(四个整数型, * float 与 double: - Java 不能隐式执行**向下转型**,因为这会使得精度降低(参考多态),但是可以向上转型 + Java 不能隐式执行**向下转型**,因为这会使得精度降低,但是可以向上转型 ```java //1.1字面量属于double类型,不能直接将1.1直接赋值给 float 变量,因为这是向下转型 @@ -146,7 +138,7 @@ Java 语言提供了八种基本类型。六种数字类型(四个整数型, 字面量 1 是 int 类型,比 short 类型精度要高,因此不能隐式地将 int 类型向下转型为 short 类型 - 使用 += 或者 ++ 运算符会执行隐式类型转换: + 使用 += 或者 ++ 运算符会执行类型转换: ```java short s1 = 1; @@ -177,12 +169,12 @@ Java 语言提供了八种基本类型。六种数字类型(四个整数型, 基本数据类型 包装类(引用数据类型) byte Byte short Short -int Integer(特殊) +int Integer long Long float Float double Double -char Character(特殊) +char Character boolean Boolean ``` Java 为包装类做了一些特殊功能,具体来看特殊功能主要有: @@ -209,17 +201,15 @@ Java 为包装类做了一些特殊功能,具体来看特殊功能主要有: String itStr1 = Integer.toString(it); System.out.println(itStr1+1);//1001 // c.直接把基本数据类型+空字符串就得到了字符串。 - String itStr2 = it+""; + String itStr2 = it + ""; System.out.println(itStr2+1);// 1001 - // 2.把字符串类型的数值转换成对应的基本数据类型的值。(真的很有用) + // 2.把字符串类型的数值转换成对应的基本数据类型的值 String numStr = "23"; - //int numInt = Integer.parseInt(numStr); int numInt = Integer.valueOf(numStr); System.out.println(numInt+1);//24 String doubleStr = "99.9"; - //double doubleDb = Double.parseDouble(doubleStr); double doubleDb = Double.valueOf(doubleStr); System.out.println(doubleDb+0.1);//100.0 } @@ -329,7 +319,7 @@ new Integer(123) 与 Integer.valueOf(123) 的区别在于: System.out.println(z == k); // true ``` -valueOf() 方法的实现比较简单,就是先判断值是否在缓存池中,如果在的话就直接返回缓存池的内容。编译器会在自动装箱过程调用 valueOf() 方法,因此多个值相同且值在缓存池范围内的 Integer 实例使用自动装箱来创建,那么就会引用相同的对象。 +valueOf() 方法的实现比较简单,就是先判断值是否在缓存池中,如果在的话就直接返回缓存池的内容。编译器会在自动装箱过程调用 valueOf() 方法,因此多个值相同且值在缓存池范围内的 Integer 实例使用自动装箱来创建,那么就会引用相同的对象 **基本类型对应的缓存池如下:** @@ -417,7 +407,7 @@ public static void main(String[] args) { #### 元素访问 -* **索引**:每一个存储到数组的元素,都会自动的拥有一个编号,从 **0** 开始。这个自动编号称为数组索引(index),可以通过数组的索引访问到数组中的元素。 +* **索引**:每一个存储到数组的元素,都会自动的拥有一个编号,从 **0** 开始。这个自动编号称为数组索引(index),可以通过数组的索引访问到数组中的元素 * **访问格式**:数组名[索引],`arr[0]` * **赋值:**`arr[0] = 10` @@ -430,7 +420,7 @@ public static void main(String[] args) { #### 内存分配 -内存是计算机中的重要器件,临时存储区域,作用是运行程序。我们编写的程序是存放在硬盘中,在硬盘中的程序是不会运行的,必须放进内存中才能运行,运行完毕后会清空内存。 Java 虚拟机要运行程序,必须要对内存进行空间的分配和管理。 +内存是计算机中的重要器件,临时存储区域,作用是运行程序。编写的程序是存放在硬盘中,在硬盘中的程序是不会运行的,必须放进内存中才能运行,运行完毕后会清空内存,Java 虚拟机要运行程序,必须要对内存进行空间的分配和管理 | 区域名称 | 作用 | | ---------- | ---------------------------------------------------------- | @@ -475,9 +465,9 @@ public static void main(String[] args) { } ``` - arr = null,表示变量 arr 将不再保存数组的内存地址,也就不允许再操作数组,因此运行的时候会抛出空指针异常。在开发中,空指针异常是不能出现的,一旦出现了,就必须要修改编写的代码。 + arr = null,表示变量 arr 将不再保存数组的内存地址,也就不允许再操作数组,因此运行的时候会抛出空指针异常。在开发中,空指针异常是不能出现的,一旦出现了,就必须要修改编写的代码 - 解决方案:给数组一个真正的堆内存空间引用即可! + 解决方案:给数组一个真正的堆内存空间引用即可 @@ -491,14 +481,12 @@ public static void main(String[] args) { 初始化: -* 动态初始化: - - 数据类型[][] 变量名 = new 数据类型[m] [n] : `int[][] arr = new int[3][3]` +* 动态初始化:数据类型[][] 变量名 = new 数据类型[m] [n],`int[][] arr = new int[3][3]` * m 表示这个二维数组,可以存放多少个一维数组,行 * n 表示每一个一维数组,可以存放多少个元素,列 * 静态初始化 - * 数据类型[][] 变量名 = new 数据类型[][]{ {元素1, 元素2...} , {元素1, 元素2...} + * 数据类型[][] 变量名 = new 数据类型 [][]{{元素1, 元素2...} , {元素1, 元素2...} * 数据类型[][] 变量名 = {{元素1, 元素2...}, {元素1, 元素2...}...} * `int[][] arr = {{11,22,33}, {44,55,66}}` @@ -537,20 +525,20 @@ public class Test1 { ### 运算 * i++ 与 ++i 的区别? - i++ 表示先将 i 放在表达式中运算,然后再加 1 - ++i 表示先将 i 加 1,然后再放在表达式中运算 + + i++ 表示先将 i 放在表达式中运算,然后再加 1,++i 表示先将 i 加 1,然后再放在表达式中运算 * || 和 |,&& 和& 的区别,逻辑运算符 - **& 和| 称为布尔运算符,位运算符。&& 和 || 称为条件布尔运算符,也叫短路运算符**。 + **& 和| 称为布尔运算符,位运算符;&& 和 || 称为条件布尔运算符,也叫短路运算符** 如果 && 运算符的第一个操作数是 false,就不需要考虑第二个操作数的值了,因为无论第二个操作数的值是什么,其结果都是 false;同样,如果第一个操作数是 true,|| 运算符就返回 true,无需考虑第二个操作数的值;但 & 和 | 却不是这样,它们总是要计算两个操作数。为了提高性能,**尽可能使用 && 和 || 运算符** -* ^ 异或:两位相异为 1,相同为 0,又叫不进位加法。同或:两位相同为 1,相异为 0 +* 异或 ^:两位相异为 1,相同为 0,又叫不进位加法 -* switch +* 同或:两位相同为 1,相异为 0 - 从 Java 7 开始,可以在 switch 条件判断语句中使用 String 对象 +* switch:从 Java 7 开始,可以在 switch 条件判断语句中使用 String 对象 ```java String s = "a"; @@ -570,11 +558,9 @@ public class Test1 { * break:跳出一层循环 -* 移位运算 +* 移位运算:计算机里一般用**补码表示数字**,正数、负数的表示区别就是最高位是 0 还是 1 - 计算机里一般用**补码表示数字**,正数、负数的表示区别就是最高位是 0 还是 1 - - * 正数的原码反码补码相同 + * 正数的原码反码补码相同,最高位为 0 ```java 100: 00000000 00000000 00000000 01100100 @@ -583,7 +569,7 @@ public class Test1 { * 负数: 原码:最高位为 1,其余位置和正数相同 反码:保证符号位不变,其余位置取反 - 补码:保证符号位不变,其余位置取反加 1,即反码 +1 + 补码:保证符号位不变,其余位置取反后加 1,即反码 +1 ```java -100 原码: 10000000 00000000 00000000 01100100 //32位 @@ -636,7 +622,7 @@ public class Test1 { 格式:数据类型... 参数名称 -作用:传输参数非常灵活,方便,可以不传输参数、传输一个参数、或者传输一个数组。 +作用:传输参数非常灵活,可以不传输参数、传输一个参数、或者传输一个数组 可变参数的注意事项: @@ -837,7 +823,7 @@ public class MethodDemo { Java 的参数是以**值传递**的形式传入方法中 -值传递和引用传递的区别在于传递后会不会影响实参的值:值传递会创建副本,引用传递不会创建副本 +值传递和引用传递的区别在于传递后会不会影响实参的值:**值传递会创建副本**,引用传递不会创建副本 * 基本数据类型:形式参数的改变,不影响实际参数 @@ -996,11 +982,11 @@ Debug 是供程序员使用的程序调试工具,它可以用于查看程序 ### 概述 -**Java 是一种面向对象的高级编程语言。** +Java 是一种面向对象的高级编程语言 -**三大特征:封装,继承,多态** +面向对象三大特征:**封装,继承,多态** -面向对象最重要的两个概念:类和对象 +两个概念:类和对象 * 类:相同事物共同特征的描述,类只是学术上的一个概念并非真实存在的,只能描述一类事物 * 对象:是真实存在的实例, 实例 == 对象,**对象是类的实例化** @@ -1198,11 +1184,11 @@ Java 是通过成员变量是否有 static 修饰来区分是类的还是属于 内存问题: -* **栈内存存放 main 方法和地址** +* 栈内存存放 main 方法和地址 -* **堆内存存放对象和变量** +* 堆内存存放对象和变量 -* **方法区存放 class 和静态变量(jdk8 以后移入堆)** +* 方法区存放 class 和静态变量(jdk8 以后移入堆) 访问问题: @@ -1210,7 +1196,7 @@ Java 是通过成员变量是否有 static 修饰来区分是类的还是属于 * 实例方法是否可以直接访问静态成员变量?可以,静态成员变量可以被共享访问 * 实例方法是否可以直接访问实例方法? 可以,实例方法和实例方法都属于对象 * 实例方法是否可以直接访问静态方法?可以,静态方法可以被共享访问 -* 静态方法是否可以直接访问实例变量? 不可以,实例变量必须用对象访问!! +* 静态方法是否可以直接访问实例变量? 不可以,实例变量**必须用对象访问**!! * 静态方法是否可以直接访问静态变量? 可以,静态成员变量可以被共享访问。 * 静态方法是否可以直接访问实例方法? 不可以,实例方法必须用对象访问!! * 静态方法是否可以直接访问静态方法?可以,静态方法可以被共享访问!! @@ -1256,7 +1242,7 @@ Java 是通过成员变量是否有 static 修饰来区分是类的还是属于 * 子类不能继承父类的构造器,子类有自己的构造器 * 子类是不能可以继承父类的私有成员的,可以反射暴力去访问继承自父类的私有成员 -* 子类是不能继承父类的静态成员的,子类只是可以访问父类的静态成员,父类静态成员只有一份可以被子类共享访问,**共享并非继承** +* 子类是不能继承父类的静态成员,父类静态成员只有一份可以被子类共享访问,**共享并非继承** ```java public class ExtendsDemo { @@ -1267,8 +1253,10 @@ public class ExtendsDemo { System.out.println(Cat.schoolName); } } + class Cat extends Animal{ } + class Animal{ public static String schoolName ="seazean"; public static void test(){} @@ -1295,6 +1283,7 @@ public class ExtendsDemo { w.showName(); } } + class Wolf extends Animal{ private String name = "子类狼"; public void showName(){ @@ -1327,7 +1316,7 @@ class Animal{ 方法重写的校验注解:@Override -* 方法加了这个注解,那就必须是成功重写父类的方法,否则报错 +* 方法加了这个注解,那就必须是成功重写父类的方法,否则报错 * @Override 优势:可读性好,安全,优雅 **子类可以扩展父类的功能,但不能改变父类原有的功能**,重写有以下三个限制: @@ -1432,11 +1421,11 @@ class Animal{ * this 代表了当前对象的引用(继承中指代子类对象):this.子类成员变量、this.子类成员方法、**this(...)** 可以根据参数匹配访问本类其他构造器 * super 代表了父类对象的引用(继承中指代了父类对象空间):super.父类成员变量、super.父类的成员方法、super(...) 可以根据参数匹配访问父类的构造器 -**注意:** +注意: -* this(...) 借用本类其他构造器,super(...) 调用父类的构造器。 -* this(...) 或 super(...) 必须放在构造器的第一行,否则报错! -* this(...) 和 super(...) 不能同时出现在构造器中,因为构造函数必须出现在第一行上,只能选择一个。 +* this(...) 借用本类其他构造器,super(...) 调用父类的构造器 +* this(...) 或 super(...) 必须放在构造器的第一行,否则报错 +* this(...) 和 super(...) **不能同时出现**在构造器中,因为构造函数必须出现在第一行上,只能选择一个 ```java public class ThisDemo { @@ -1503,7 +1492,7 @@ final 和 abstract 的关系是**互斥关系**,不能同时修饰类或者同 final 修饰静态成员变量,变量变成了常量 -**常量:有 public static final 修饰,名称字母全部大写,多个单词用下划线连接。** +常量:有 public static final 修饰,名称字母全部大写,多个单词用下划线连接 final 修饰静态成员变量可以在哪些地方赋值: @@ -1520,7 +1509,6 @@ public class FinalDemo { static{ //SCHOOL_NAME = "java";//报错 SCHOOL_NAME1 = "张三1"; - //SCHOOL_NAME1 = "张三2"; // 报错,第二次赋值! } } ``` @@ -1575,13 +1563,13 @@ public class FinalDemo { #### 基本介绍 -> 父类知道子类要完成某个功能,但是每个子类实现情况不一样。 +> 父类知道子类要完成某个功能,但是每个子类实现情况不一样 抽象方法:没有方法体,只有方法签名,必须用 abstract 修饰的方法就是抽象方法 抽象类:拥有抽象方法的类必须定义成抽象类,必须用 abstract 修饰,**抽象类是为了被继承** -一个类继承抽象类,**必须重写抽象类的全部抽象方法**,否则这个类必须定义成抽象类,因为拥有抽象方法的类必须定义成抽象类 +一个类继承抽象类,**必须重写抽象类的全部抽象方法**,否则这个类必须定义成抽象类 ```java public class AbstractDemo { @@ -1811,32 +1799,32 @@ public class InterfaceDemo { InterfaceJDK8.inAddr(); } } -class Man implements InterfaceJDK8{ +class Man implements InterfaceJDK8 { @Override public void work() { System.out.println("工作中。。。"); } } -interface InterfaceJDK8{ +interface InterfaceJDK8 { //抽象方法!! void work(); // a.默认方法(就是之前写的普通实例方法) // 必须用接口的实现类的对象来调用。 - default void run(){ + default void run() { go(); System.out.println("开始跑步🏃‍"); } // b.静态方法 // 注意:接口的静态方法必须用接口的类名本身来调用 - static void inAddr(){ + static void inAddr() { System.out.println("我们在武汉"); } // c.私有方法(就是私有的实例方法): JDK 1.9才开始有的。 // 只能在本接口中被其他的默认方法或者私有方法访问。 - private void go(){ + private void go() { System.out.println("开始。。"); } } @@ -1874,7 +1862,7 @@ interface InterfaceJDK8{ #### 基本介绍 -多态的概念:同一个实体同时具有多种形式同一个类型的对象,执行同一个行为,在不同的状态下会表现出不同的行为特征。 +多态的概念:同一个实体同时具有多种形式同一个类型的对象,执行同一个行为,在不同的状态下会表现出不同的行为特征 多态的格式: @@ -1897,11 +1885,11 @@ interface InterfaceJDK8{ * 存在方法重写 多态的优势: -* 在多态形式下,右边对象可以实现组件化切换,业务功能也随之改变,便于扩展和维护。可以实现类与类之间的**解耦** +* 在多态形式下,右边对象可以实现组件化切换,便于扩展和维护,也可以实现类与类之间的**解耦** * 父类类型作为方法形式参数,传递子类对象给方法,可以传入一切子类对象进行方法的调用,更能体现出多态的**扩展性**与便利性 多态的劣势: -* 多态形式下,不能直接调用子类特有的功能,因为编译看左边,父类中没有子类独有的功能,所以代码在编译阶段就直接报错了! +* 多态形式下,不能直接调用子类特有的功能,因为编译看左边,父类中没有子类独有的功能,所以代码在编译阶段就直接报错了 ```java public class PolymorphicDemo { @@ -2061,7 +2049,7 @@ static class Outter{ #### 实例内部类 -定义:无 static 修饰的内部类,属于外部类的每个对象,跟着外部类对象一起加载。 +定义:无 static 修饰的内部类,属于外部类的每个对象,跟着外部类对象一起加载 实例内部类的成分特点:实例内部类中不能定义静态成员,其他都可以定义 @@ -2192,9 +2180,8 @@ static { ``` * 静态代码块特点: - * 必须有 static 修饰 + * 必须有 static 修饰,只能访问静态资源 * 会与类一起优先加载,且自动触发执行一次 - * 只能访问静态资源 * 静态代码块作用: * 可以在执行类的方法等操作之前先在静态代码块中进行静态资源的初始化 * **先执行静态代码块,在执行 main 函数里的操作** @@ -2337,7 +2324,7 @@ Object 的 clone() 是 protected 方法,一个类不显式去重写 clone(), * 浅拷贝 (shallowCopy):**对基本数据类型进行值传递,对引用数据类型只是复制了引用**,被复制对象属性的所有的引用仍然指向原来的对象,简而言之就是增加了一个指针指向原来对象的内存地址 - **Java 中的复制方法基本都是浅克隆**:Object.clone()、System.arraycopy()、Arrays.copyOf() + **Java 中的复制方法基本都是浅拷贝**:Object.clone()、System.arraycopy()、Arrays.copyOf() * 深拷贝 (deepCopy):对基本数据类型进行值传递,对引用数据类型是一个整个独立的对象拷贝,会拷贝所有的属性并指向的动态分配的内存,简而言之就是把所有属性复制到一个新的内存,增加一个指针指向新内存。所以使用深拷贝的情况下,释放内存的时候不会出现使用浅拷贝时释放同一块内存的错误 @@ -2390,11 +2377,11 @@ Objects 类与 Object 是继承关系 Objects 的方法: -* `public static boolean equals(Object a, Object b)`:比较两个对象是否相同。 - 底层进行非空判断,从而可以避免空指针异常,更安全,推荐使用! - - ```java +* `public static boolean equals(Object a, Object b)`:比较两个对象是否相同 + + ```java public static boolean equals(Object a, Object b) { + // 进行非空判断,从而可以避免空指针异常 return a == b || a != null && a.equals(b); } ``` @@ -2637,7 +2624,7 @@ public static void main(String[] args) { System.out.println(str1 == str1.intern());//true,字符串池中不存在,把堆中的引用复制一份放入串池 String str2 = new StringBuilder("ja").append("va").toString(); - System.out.println(str2 == str2.intern());//false + System.out.println(str2 == str2.intern());//false,字符串池中存在,直接返回已经存在的引用 } ``` @@ -2900,11 +2887,11 @@ System 代表当前系统 * `public static long currentTimeMillis()`:获取当前系统此刻时间毫秒值 * `static void arraycopy(Object var0, int var1, Object var2, int var3, int var4)`:数组拷贝 - 参数一:原数组 - 参数二:从原数组的哪个位置开始赋值 - 参数三:目标数组 - 参数四:从目标数组的哪个位置开始赋值 - 参数五:赋值几个 + * 参数一:原数组 + * 参数二:从原数组的哪个位置开始赋值 + * 参数三:目标数组 + * 参数四:从目标数组的哪个位置开始赋值 + * 参数五:赋值几个 ```java public class SystemDemo { @@ -3758,15 +3745,12 @@ public class RegexDemo { 数据存储的常用结构有:栈、队列、数组、链表和红黑树 * 队列(queue):先进先出,后进后出。(FIFO first in first out) - 场景:各种排队、叫号系统,有很多集合可以实现队列 - + * 栈(stack):后进先出,先进后出 (LIFO) - 压栈 == 入栈、弹栈 == 出栈 - 场景:手枪的弹夹 - + * 数组:数组是内存中的连续存储区域,分成若干等分的小区域(每个区域大小是一样的)元素存在索引 - 特点:**查询元素快**(根据索引快速计算出元素的地址,然后立即去定位) - **增删元素慢**(创建新数组,迁移元素) + + 特点:**查询元素快**(根据索引快速计算出元素的地址,然后立即去定位),**增删元素慢**(创建新数组,迁移元素) * 链表:元素不是内存中的连续区域存储,元素是游离存储的,每个元素会记录下个元素的地址 特点:**查询元素慢,增删元素快**(针对于首尾元素,速度极快,一般是双链表) @@ -3774,11 +3758,12 @@ public class RegexDemo { * 树: * 二叉树:binary tree 永远只有一个根节点,是每个结点不超过2个节点的树(tree) + 特点:二叉排序树:小的左边,大的右边,但是可能树很高,性能变差,为了做排序和搜索会进行左旋和右旋实现平衡查找二叉树,让树的高度差不大于1 -* 红黑树(基于红黑规则实现自平衡的排序二叉树):树保证到了很矮小,但是又排好序,性能最高的树 + * 红黑树(基于红黑规则实现自平衡的排序二叉树):树保证到了很矮小,但是又排好序,性能最高的 - 特点:**红黑树的增删查改性能都好** + 特点:**红黑树的增删查改性能都好** 各数据结构时间复杂度对比: @@ -3970,8 +3955,8 @@ ArrayList 添加的元素,是有序,可重复,有索引的 * `public boolean add(E e)`:将指定的元素追加到此集合的末尾 * `public void add(int index, E element)`:将指定的元素,添加到该集合中的指定位置上 * `public E get(int index)`:返回集合中指定位置的元素 -* `public E remove(int index)`:移除列表中指定位置的元素, 返回的是被移除的元素 -* `public E set(int index, E element)`:用指定元素替换集合中指定位置的元素,返回更新前的元素值 +* `public E remove(int index)`:移除列表中指定位置的元素,返回的是被移除的元素 +* `public E set(int index, E element)`:用指定元素替换集合中指定位置的元素,返回更新前的元素值 * `int indexOf(Object o)`:返回列表中指定元素第一次出现的索引,如果不包含此元素,则返回 -1 ```java @@ -4008,7 +3993,7 @@ public class ArrayList extends AbstractList 核心方法: -* 构造函数:以无参数构造方法创建 ArrayList 时,实际上初始化赋值的是一个空数组。当真正对数组进行添加元素操作时,才真正分配容量,即向数组中添加第一个元素时,**数组容量扩为 10** +* 构造函数:以无参数构造方法创建 ArrayList 时,实际上初始化赋值的是一个空数组。当真正对数组进行添加元素操作时,才真正分配容量(惰性初始化),即向数组中添加第一个元素时,**数组容量扩为 10** * 添加元素: @@ -4059,8 +4044,8 @@ public class ArrayList extends AbstractList public void add(int index, E element) { rangeCheckForAdd(index); ensureCapacityInternal(size + 1); // Increments modCount!! - System.arraycopy(elementData, index, elementData, index + 1, - size - index); + // 将指定索引后的数据后移 + System.arraycopy(elementData, index, elementData, index + 1, size - index); elementData[index] = element; size++; } @@ -4091,7 +4076,7 @@ public class ArrayList extends AbstractList * OutOfMemoryError:Requested array size exceeds VM limit(请求的数组大小超出 VM 限制) * OutOfMemoryError: Java heap space(堆区内存不足,可以通过设置 JVM 参数 -Xmx 来调节) -* 删除元素:需要调用 System.arraycopy() 将 index+1 后面的元素都复制到 index 位置上,在旧数组上操作,该操作的时间复杂度为 O(N),可以看到 ArrayList 删除元素的代价是非常高的, +* 删除元素:需要调用 System.arraycopy() 将 index+1 后面的元素都复制到 index 位置上,在旧数组上操作,该操作的时间复杂度为 O(N),可以看到 ArrayList 删除元素的代价是非常高的 ```java public E remove(int index) { @@ -4212,10 +4197,6 @@ LinkedList 除了拥有 List 集合的全部功能还多了很多操作首尾元 * `public E poll()`:检索并删除此列表的头(第一个元素) * `public void addFirst(E e)`:将指定元素插入此列表的开头 * `public void addLast(E e)`:将指定元素添加到此列表的结尾 -* `public E getFirst()`:返回此列表的第一个元素 -* `public E getLast()`:返回此列表的最后一个元素 -* `public E removeFirst()`:移除并返回此列表的第一个元素 -* `public E removeLast()`:移除并返回此列表的最后一个元素 * `public E pop()`:从此列表所表示的堆栈处弹出一个元素 * `public void push(E e)`:将元素推入此列表所表示的堆栈 * `public int indexOf(Object o)`:返回此列表中指定元素的第一次出现的索引,如果不包含返回 -1 @@ -4401,7 +4382,7 @@ Set 集合添加的元素是无序,不重复的。 每个元素的 hashcode() 的值进行响应的算法运算,计算出的值相同的存入一个数组块中,以链表的形式存储,如果链表长度超过8就采取红黑树存储,所以输出的元素是无序的。 -* 如何设置只要对象内容一样,就希望集合认为它们重复了:**重写 hashCode 和 equals 方法** +* 如何设置只要对象内容一样,就希望集合认为重复:**重写 hashCode 和 equals 方法** @@ -4585,7 +4566,7 @@ Map集合的体系: LinkedHashMap(实现类) ``` -Map集合的特点: +Map 集合的特点: 1. Map 集合的特点都是由键决定的 2. Map 集合的键是无序,不重复的,无索引的(Set) @@ -4597,13 +4578,6 @@ HashMap:元素按照键是无序,不重复,无索引,值不做要求 LinkedHashMap:元素按照键是有序,不重复,无索引,值不做要求 -```java -//经典代码 -Map maps = new HashMap(); -maps.put("手机",1); -System.out.println(maps); -``` - *** @@ -4714,12 +4688,12 @@ JDK7 对比 JDK8: * 哈希表(Hash table,也叫散列表),根据关键码值而直接访问的数据结构。通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度,这个映射函数叫做散列函数,存放记录的数组叫做散列表 -* JDK1.8 之前 HashMap 由 数组+链表 组成 +* JDK1.8 之前 HashMap 由数组+链表组成 * 数组是 HashMap 的主体 * 链表则是为了解决哈希冲突而存在的(**拉链法解决冲突**),拉链法就是头插法,两个对象调用的 hashCode 方法计算的哈希码值(键的哈希)一致导致计算的数组索引值相同 -* JDK1.8 以后 HashMap 由 **数组+链表 +红黑树**数据结构组成 +* JDK1.8 以后 HashMap 由**数组+链表 +红黑树**数据结构组成 * 解决哈希冲突时有了较大的变化 * 当链表长度**超过(大于)阈值**(或者红黑树的边界值,默认为 8)并且当前数组的**长度大于等于 64 时**,此索引位置上的所有数据改为红黑树存储 @@ -4767,7 +4741,7 @@ HashMap 继承关系如下图所示: ```java // 默认的初始容量是16 -- 1<<4相当于1*2的4次方---1*16 - static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; + static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; ``` HashMap 构造方法指定集合的初始化容量大小: @@ -4776,9 +4750,9 @@ HashMap 继承关系如下图所示: HashMap(int initialCapacity)// 构造一个带指定初始容量和默认加载因子 (0.75) 的空 HashMap ``` - * 为什么必须是 2 的 n 次幂? + * 为什么必须是 2 的 n 次幂?用位运算替代取余计算 - HashMap 中添加元素时,需要根据 key 的 hash 值,确定在数组中的具体位置。为了存取高效,要尽量较少碰撞,把数据分配均匀,每个链表长度大致相同,实现该方法的算法就是取模,hash%length,计算机中直接求余效率不如位移运算,所以源码中使用 hash&(length-1),实际上**hash % length == hash & (length-1)的前提是 length 是 2 的n次幂** + HashMap 中添加元素时,需要根据 key 的 hash 值确定在数组中的具体位置。为了减少碰撞,把数据分配均匀,每个链表长度大致相同,实现该方法就是取模 `hash%length`,计算机中直接求余效率不如位移运算, **`hash % length == hash & (length-1)` 的前提是 length 是 2 的 n 次幂** 散列平均分布:2 的 n 次方是 1 后面 n 个 0,2 的 n 次方 -1 是 n 个 1,可以**保证散列的均匀性**,减少碰撞 @@ -4804,11 +4778,6 @@ HashMap 继承关系如下图所示: static final int MAXIMUM_CAPACITY = 1 << 30;// 0100 0000 0000 0000 0000 0000 0000 0000 = 2 ^ 30 ``` - 最大容量为什么是 2 的 30 次方原因: - - * int 类型是 32 位整型,占 4 个字节 - * Java 的原始类型里没有无符号类型,所以首位是符号位正数为 0,负数为 1, - 5. 当链表的值超过 8 则会转红黑树(JDK1.8 新增) ```java @@ -4852,7 +4821,7 @@ HashMap 继承关系如下图所示: static final int MIN_TREEIFY_CAPACITY = 64; ``` - 原因:数组比较小的情况下变为红黑树结构,反而会降低效率,红黑树需要进行左旋,右旋,变色这些操作来保持平衡。同时数组长度小于 64 时,搜索时间相对快些,所以为了提高性能和减少搜索时间,底层在阈值大于 8 并且数组长度大于等于 64 时,链表才转换为红黑树,效率也变的更高效 + 原因:数组比较小的情况下变为红黑树结构,反而会降低效率,红黑树需要进行左旋,右旋,变色这些操作来保持平衡 8. table 用来初始化(必须是二的 n 次幂) @@ -4861,8 +4830,6 @@ HashMap 继承关系如下图所示: transient Node[] table; ``` - jdk8 之前数组类型是 Entry类型,之后是 Node 类型。只是换了个名字,都实现了一样的接口 Map.Entry,负责存储键值对数据的 - 9. HashMap 中**存放元素的个数** ```java @@ -4884,7 +4851,7 @@ HashMap 继承关系如下图所示: int threshold; ``` -12. **哈希表的加载因子(重点)** +12. **哈希表的加载因子** ```java final float loadFactor; @@ -4892,9 +4859,9 @@ HashMap 继承关系如下图所示: * 加载因子的概述 - loadFactor 加载因子,是用来衡量 HashMap 满的程度,表示 **HashMap 的疏密程度**,影响 hash 操作到同一个数组位置的概率,计算 HashMap 的实时加载因子的方法为:size/capacity,而不是占用桶的数量去除以 capacity,capacity 是桶的数量,也就是 table 的长度 length。 + loadFactor 加载因子,是用来衡量 HashMap 满的程度,表示 HashMap 的疏密程度,影响 hash 操作到同一个数组位置的概率,计算 HashMap 的实时加载因子的方法为 **size/capacity**,而不是占用桶的数量去除以 capacity,capacity 是桶的数量,也就是 table 的长度 length - 当 HashMap 里面容纳的元素已经达到 HashMap 数组长度的 75% 时,表示 HashMap 拥挤,需要扩容,而扩容这个过程涉及到 rehash、复制数据等操作,非常消耗性能,所以开发中尽量减少扩容的次数,可以通过创建 HashMap 集合对象时指定初始容量来尽量避免。 + 当 HashMap 容纳的元素已经达到数组长度的 75% 时,表示 HashMap 拥挤需要扩容,而扩容这个过程涉及到 rehash、复制数据等操作,非常消耗性能,所以开发中尽量减少扩容的次数,通过创建 HashMap 集合对象时指定初始容量来避免 ```java HashMap(int initialCapacity, float loadFactor)//构造指定初始容量和加载因子的空HashMap @@ -4904,7 +4871,7 @@ HashMap 继承关系如下图所示: loadFactor 太大导致查找元素效率低,存放的数据拥挤,太小导致数组的利用率低,存放的数据会很分散。loadFactor 的默认值为 **0.75f 是官方给出的一个比较好的临界值** - * threshold 计算公式:capacity(数组长度默认16) * loadFactor(默认 0.75)。当 size>=threshold 的时候,那么就要考虑对数组的 resize(扩容),这就是衡量数组是否需要扩增的一个标准, 扩容后的 HashMap 容量是之前容量的**两倍** + * threshold 计算公式:capacity(数组长度默认16) * loadFactor(默认 0.75)。当 size>= threshold 的时候,那么就要考虑对数组的 resize(扩容),这就是衡量数组是否需要扩增的一个标准, 扩容后的 HashMap 容量是之前容量的**两倍** @@ -5013,8 +4980,8 @@ HashMap 继承关系如下图所示: ```java static final int hash(Object key) { int h; - // 1)如果key等于null:可以看到当key等于null的时候也是有哈希值的,返回的是0. - // 2)如果key不等于null:首先计算出key的hashCode赋值给h,然后与h无符号右移16位后的二进制进行按位异或得到最后的hash值 + // 1)如果key等于null:可以看到当key等于null的时候也是有哈希值的,返回的是0 + // 2)如果key不等于null:首先计算出key的hashCode赋值给h,然后与h无符号右移16位后的二进制进行按位异或 return (key == null) ? 0 : (h = key.hashCode()) ^ (h>>> 16); } ``` @@ -5096,8 +5063,6 @@ HashMap 继承关系如下图所示: 2. 如果是树形化遍历桶中的元素,创建相同个数的树形节点,复制内容,建立起联系,类似单向链表转换为双向链表 3. 让桶中的第一个元素即数组中的元素指向新建的红黑树的节点,以后这个桶里的元素就是红黑树而不是链表数据结构了 - - * tableSizeFor():创建 HashMap 指定容量时,HashMap 通过位移运算和或运算得到比指定初始化容量大的最小的 2 的 n 次幂 ```java @@ -5678,15 +5643,11 @@ TreeMap 集合指定大小规则有 2 种方式: #### WeakMap -WeakHashMap 是基于弱引用的 - -内部的 Entry 继承 WeakReference,被弱引用关联的对象在**下一次垃圾回收时会被回收**,并且构造方法传入引用队列,用来在清理对象完成以后清理引用 +WeakHashMap 是基于弱引用的,内部的 Entry 继承 WeakReference,被弱引用关联的对象在**下一次垃圾回收时会被回收**,并且构造方法传入引用队列,用来在清理对象完成以后清理引用 ```java private static class Entry extends WeakReference implements Map.Entry { - Entry(Object key, V value, - ReferenceQueue queue, - int hash, Entry next) { + Entry(Object key, V value, ReferenceQueue queue, int hash, Entry next) { super(key, queue); this.value = value; this.hash = hash; @@ -5758,7 +5719,7 @@ Tomcat 中的 ConcurrentCache 使用了 WeakHashMap 来实现缓存功能,Conc 注意: * JDK 1.7 开始之后,泛型后面的申明可以省略不写 -* **泛型和集合都只能支持引用数据类型,不支持基本数据类型。** +* **泛型和集合都只能支持引用数据类型,不支持基本数据类型** ```java ArrayList lists = new ArrayList(); @@ -5943,7 +5904,6 @@ Java 中异常继承的根类是:Throwable Error Exception(异常,需要研究和处理) / \ 编译时异常 RuntimeException(运行时异常) - ``` Exception 异常的分类: @@ -6016,7 +5976,7 @@ public static void main(String[] args) throws ParseException { 在出现编译时异常的地方层层把异常抛出去给调用者,调用者最终抛出给 JVM 虚拟机,JVM 虚拟机输出异常信息,直接终止掉程序,这种方式与默认方式是一样的 -**Exception是异常最高类型可以抛出一切异常** +**Exception 是异常最高类型可以抛出一切异常** ```java public static void main(String[] args) throws Exception { @@ -6038,7 +5998,7 @@ public static void main(String[] args) throws Exception { 可以处理异常,并且出现异常后代码也不会死亡 -* 自己捕获异常和处理异常的格式:**捕获处理** +* 捕获异常和处理异常的格式:**捕获处理** ```java try{ @@ -6168,7 +6128,7 @@ public class ExceptionDemo { 运行时异常在编译阶段是不会报错,在运行阶段才会出错,运行时出错了程序还是会停止,运行时异常也建议要处理,运行时异常是自动往外抛出的,不需要手工抛出 -**运行时异常的处理规范**:直接在最外层捕获处理即可,底层会自动抛出!! +**运行时异常的处理规范**:直接在最外层捕获处理即可,底层会自动抛出 ```java public class ExceptionDemo{ @@ -6196,7 +6156,7 @@ public class ExceptionDemo{ ### Finally -用在捕获处理的异常格式中的,放在最后面。 +用在捕获处理的异常格式中的,放在最后面 ```java try{ @@ -6267,9 +6227,9 @@ public class FinallyDemo { * 自定义编译时异常:定义一个异常类继承 Exception,重写构造器,在出现异常的地方用 throw new 自定义对象抛出 * 自定义运行时异常:定义一个异常类继承 RuntimeException,重写构造器,在出现异常的地方用 throw new 自定义对象抛出 -**throws:用在方法上,用于抛出方法中的异常** +throws:用在方法上,用于抛出方法中的异常 -**throw: 用在出现异常的地方,创建异常对象且立即从此处抛出** +throw: 用在出现异常的地方,创建异常对象且立即从此处抛出 ```java //需求:认为年龄小于0岁,大于200岁就是一个异常。 @@ -6318,7 +6278,7 @@ public class AgeIllegalRuntimeException extends RuntimeException{ 1. 运行时异常被抛出可以不处理,可以自动抛出;**编译时异常必须处理**;按照规范都应该处理 2. **重写方法申明抛出的异常,子类方法抛出的异常类型必须是父类抛出异常类型或为其子类型** 3. 方法默认都可以自动抛出运行时异常, throws RuntimeException 可以省略不写 -4. 当多异常处理时,捕获处理,前面的异常类不能是后面异常类的父类。 +4. 当多异常处理时,捕获处理,前面的异常类不能是后面异常类的父类 5. 在 try/catch 后可以追加 finally 代码块,其中的代码一定会被执行,通常用于资源回收操作 异常的作用: @@ -6366,11 +6326,11 @@ Lambda 表达式是 JDK1.8 开始之后的新技术,是一种代码的新语 作用:为了简化匿名内部类的代码写法 -Lambda 表达式的格式: +Lambda 表达式的格式: ```java (匿名内部类被重写方法的形参列表) -> { - //被重写方法的方法体代码。 + //被重写方法的方法体代码 } ``` @@ -6392,13 +6352,13 @@ Lambda 表达式的省略写法(进一步在 Lambda 表达式的基础上继 * 如果 Lambda 表达式的方法体代码只有一行代码,可以省略大括号不写,同时要省略分号;如果这行代码是 return 语句,必须省略 return 不写 * 参数类型可以省略不写 -* 如果只有一个参数,参数类型可以省略,同时()也可以省略 +* 如果只有一个参数,参数类型可以省略,同时 `()` 也可以省略 ```java List names = new ArrayList(); -names.add("胡"); -names.add("甘"); -names.add("洪"); +names.add("a"); +names.add("b"); +names.add("c"); names.forEach(new Consumer() { @Override @@ -6634,8 +6594,6 @@ public class ConstructorDemo { Stream 流其实就是一根传送带,元素在上面可以被 Stream 流操作 -作用: - * 可以解决已有集合类库或者数组 API 的弊端 * Stream 流简化集合和数组的操作 * 链式编程 @@ -6706,7 +6664,7 @@ public class StreamDemo { public static void main(String[] args) { List list = new ArrayList(); list.add("张无忌"); list.add("周芷若"); list.add("赵敏"); - list.add("张强"); list.add("张三丰"); list.add("张三丰"); + list.add("张三"); list.add("张三丰"); list.add("张"); //取以张开头并且名字是三位数的 list.stream().filter(s -> s.startsWith("张") .filter(s -> s.length == 3).forEach(System.out::println); @@ -6719,7 +6677,7 @@ public class StreamDemo { list.stream().filter(s -> s.length == 3).skip(2).forEach(...); // 需求:把名称都加上"张三的:+xxx" - list.stream().map(s -> "张三的"+s).forEach(System.out::println); + list.stream().map(s -> "张三的" + s).forEach(System.out::println); // 需求:把名称都加工厂学生对象放上去!! // list.stream().map(name -> new Student(name)); list.stream.map(Student::new).forEach(System.out::println); @@ -6767,7 +6725,7 @@ list.stream().filter(s -> s.startsWith("张")) 收集 Stream:把 Stream 流的数据转回到集合中去 -* Stream流:工具 +* Stream 流:工具 * 集合:目的 Stream 收集方法:`R collect(Collector collector)` 把结果收集到集合中 @@ -6817,10 +6775,10 @@ File 类构造器: * `public File(String pathname)`:根据路径获取文件对象 * `public File(String parent , String child)`:根据父路径和文件名称获取文件对象 -File 类创建文件对象的格式: +File 类创建文件对象的格式: * `File f = new File("绝对路径/相对路径");` - * 绝对路径:从磁盘的的盘符一路走到目的位置的路径。 + * 绝对路径:从磁盘的的盘符一路走到目的位置的路径 * 绝对路径依赖具体的环境,一旦脱离环境,代码可能出错 * 一般是定位某个操作系统中的某个文件对象 * **相对路径**:不带盘符的(重点) @@ -6972,9 +6930,9 @@ public class FileDemo { #### 遍历目录 -- `public String[] list()`:获取当前目录下所有的"一级文件名称"到一个字符串数组中去返回。 -- `public File[] listFiles()`:获取当前目录下所有的"一级文件对象"到一个**文件对象数组**中去返回(**重点**) -- `public long lastModified`:返回此抽象路径名表示的文件上次修改的时间。 +- `public String[] list()`:获取当前目录下所有的一级文件名称到一个字符串数组中去返回 +- `public File[] listFiles()`:获取当前目录下所有的一级文件对象到一个**文件对象数组**中去返回(**重点**) +- `public long lastModified`:返回此抽象路径名表示的文件上次修改的时间 ```java public class FileDemo { @@ -7061,7 +7019,7 @@ public static void searchFiles(File dir , String fileName){ 字符集:为字符编制的一套编号规则 -计算机的底层是不能直接存储字符的,只能存储二进制,010101 +计算机的底层是不能直接存储字符的,只能存储二进制 010101 ASCII 编码:8 个开关一组就可以编码字符,1 个字节 2^8 = 256, 一个字节存储一个字符完全够用,英文和数字在底层存储都是采用 1 个字节存储的 @@ -7080,7 +7038,7 @@ B 66 美国人:收集全球所有的字符,统一编号,这套编码叫 Unicode 编码(万国码),一个英文等于两个字节,一个中文(含繁体)等于两个字节,中文标点占两个字节,英文标点占两个字节 -* UTF-8 是变种形式,也必须兼容ASCII编码表 +* UTF-8 是变种形式,也必须兼容 ASCII 编码表 * UTF-8 一个中文一般占 3 个字节,中文标点占 3 个,英文字母和数字 1 个字节 编码前与编码后的编码集必须一致才不会乱码 @@ -7145,7 +7103,7 @@ FileInputStream 文件字节输入流:以内存为基准,把磁盘文件中 方法: -* `public int read()`:每次读取一个字节返回,读取完毕会返回-1 +* `public int read()`:每次读取一个字节返回,读取完毕会返回 -1 * `public int read(byte[] buffer)`:从字节输入流中读取字节到字节数组中去,返回读取的字节数量,没有字节可读返回 -1,**byte 中新读取的数据默认是覆盖原数据**,构造 String 需要设定长度 * `public String(byte[] bytes,int offset,int length)`:构造新的 String * `public long transferTo(OutputStream out) `:从输入流中读取所有字节,并按读取的顺序,将字节写入给定的输出流 @@ -7603,7 +7561,7 @@ public class InputStreamReaderDemo{ // 1.提取GBK文件的原始字节流 InputStream is = new FileInputStream("D:\\seazean\\Netty.txt"); // 2.把原始字节输入流通过转换流,转换成 字符输入转换流InputStreamReader - InputStreamReader isr = new InputStreamReader(is,"GBK"); + InputStreamReader isr = new InputStreamReader(is, "GBK"); // 3.包装成缓冲流 BufferedReader br = new BufferedReader(isr); //循环读取 @@ -7649,7 +7607,7 @@ osw.close(); ##### 基本介绍 -对象序列化:把 Java 对象转换成字节序列的过程,将对象写入到 IO 流中。对象 => 文件中 +对象序列化:把 Java 对象转换成字节序列的过程,将对象写入到 IO 流中,对象 => 文件中 对象反序列化:把字节序列恢复为 Java 对象的过程,从 IO 流中恢复对象,文件中 => 对象 @@ -7671,7 +7629,7 @@ transient 关键字修饰的成员变量,将不参与序列化 序列化方法:`public final void writeObject(Object obj)` -注意:对象如果想参与序列化,对象必须实现序列化接口 **implements Serializable** ,否则序列化失败! +注意:对象如果想参与序列化,对象必须实现序列化接口 **implements Serializable** ,否则序列化失败 ```java public class SerializeDemo01 { @@ -7861,7 +7819,7 @@ Properties 方法: | String getProperty(String key) | 使用此属性列表中指定的键搜索属性 | | Set stringPropertyNames() | 所有键的名称的集合 | | synchronized void load(Reader r) | 从输入字符流读取属性列表(键和元素对) | -| synchronized void load(InputStream inStream) | 加载属性文件的数据到属性集对象中去 | +| synchronized void load(InputStream in) | 加载属性文件的数据到属性集对象中去 | | void store(Writer w, String comments) | 将此属性列表(键和元素对)写入 Properties 表 | | void store(OutputStream os, String comments) | 保存数据到属性文件中去 | @@ -7936,7 +7894,7 @@ public static void main(String[] args) throws Exception { ### Commons -commons-io 是 apache 提供的一组有关 IO 操作的类库,可以挺提高 IO 功能开发的效率。 +commons-io 是 apache 提供的一组有关 IO 操作的类库,可以提高 IO 功能开发的效率 commons-io 工具包提供了很多有关 IO 操作的类: @@ -8438,7 +8396,7 @@ public class ReflectDemo { // b.从ArrayList的Class对象中定位add方法 Method add = c.getDeclaredMethod("add", Object.class); // c.触发scores集合对象中的add执行(运行阶段,泛型不能约束了) - add.invoke(scores,"波仔"); + add.invoke(scores, "字符串"); System.out.println(scores); } } @@ -9549,10 +9507,6 @@ public class XPathDemo { #### 基本介绍 -创建型模式的主要关注点是怎样创建对象,将对象的创建与使用分离,降低系统的耦合度,使用者不需要关注对象的创建细节 - -创建型模式分为:单例模式、工厂方法模式、抽象工程模式、原型模式、建造者模式 - 单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一,提供了一种创建对象的最佳方式 单例设计模式分类两种: @@ -9603,7 +9557,7 @@ public class XPathDemo { * 构造方法设置为私有,防止其他类无限创建对象,但是不能防止反射破坏 - * 静态变量初始化在类加载时完成,由 JVM 保证线程安全,能保证单例对象创建时的安全 + * 静态变量初始化在类加载时完成,**由 JVM 保证线程安全**,能保证单例对象创建时的安全 * 提供静态方法而不是直接将 INSTANCE 设置为 public,体现了更好的封装性、提供泛型支持、可以改进成懒汉单例设计 From 390f948a9b77df1af52bbbd7c4c5ef3a66263a99 Mon Sep 17 00:00:00 2001 From: Seazean Date: 2022年6月26日 23:54:59 +0800 Subject: [PATCH 11/35] Update Java Note --- DB.md | 3 +- Java.md | 118 +++++++++++++++++++++----------------------- Prog.md | 148 ++++++++++++++++++-------------------------------------- Tool.md | 4 +- Web.md | 14 +++--- 5 files changed, 114 insertions(+), 173 deletions(-) diff --git a/DB.md b/DB.md index 734e730..b26ee6b 100644 --- a/DB.md +++ b/DB.md @@ -13660,9 +13660,10 @@ end 超时释放:锁超时释放可以避免死锁,但如果是业务执行耗时较长,需要进行锁续时,防止业务未执行完提前释放锁 -看门狗 watchDog 机制: +看门狗 Watch Dog 机制: * 获取锁成功后,提交周期任务,每隔一段时间(Redisson 中默认为过期时间 / 3),重置一次超时时间 +* 如果服务宕机,Watch Dog 机制线程就停止,就不会再延长 key 的过期时间 * 释放锁后,终止周期任务 diff --git a/Java.md b/Java.md index a8c8702..90a56bd 100644 --- a/Java.md +++ b/Java.md @@ -10342,7 +10342,7 @@ JVM:全称 Java Virtual Machine,即 Java 虚拟机,一种规范,本身 * Java 虚拟机基于**二进制字节码**执行,由一套字节码指令集、一组寄存器、一个栈、一个垃圾回收堆、一个方法区等组成 * JVM 屏蔽了与操作系统平台相关的信息,从而能够让 Java 程序只需要生成能够在 JVM 上运行的字节码文件,通过该机制实现的**跨平台性** -Java 代码执行流程:Java 程序 --(编译)--> 字节码文件 --(解释执行)--> 操作系统(Win,Linux) +Java 代码执行流程:`Java 程序 --(编译)--> 字节码文件 --(解释执行)--> 操作系统(Win,Linux)` JVM 结构: @@ -10350,7 +10350,7 @@ JVM 结构: JVM、JRE、JDK 对比: -* JDK(Java SE Development Kit):Java 标准开发包,它提供了编译、运行 Java 程序所需的各种工具和资源 +* JDK(Java SE Development Kit):Java 标准开发包,提供了编译、运行 Java 程序所需的各种工具和资源 * JRE( Java Runtime Environment):Java 运行环境,用于解释执行 Java 的字节码文件 @@ -10684,7 +10684,7 @@ Heap 堆:是 JVM 内存中最大的一块,由所有线程共享,由垃圾 在 Java7 中堆内会存在**年轻代、老年代和方法区(永久代)**: -* Young 区被划分为三部分,Eden 区和两个大小严格相同的 Survivor 区。Survivor 区某一时刻只有其中一个是被使用的,另外一个留做垃圾回收时复制对象。在 Eden 区变满的时候, GC 就会将存活的对象移到空闲的 Survivor 区间中,根据 JVM 的策略,在经过几次垃圾回收后,仍然存活于 Survivor 的对象将被移动到 Tenured 区间 +* Young 区被划分为三部分,Eden 区和两个大小严格相同的 Survivor 区。Survivor 区某一时刻只有其中一个是被使用的,另外一个留做垃圾回收时复制对象。在 Eden 区变满的时候,GC 就会将存活的对象移到空闲的 Survivor 区间中,根据 JVM 的策略,在经过几次垃圾回收后,仍然存活于 Survivor 的对象将被移动到 Tenured 区间 * Tenured 区主要保存生命周期长的对象,一般是一些老的对象,当一些对象在 Young 复制转移一定的次数以后,对象就会被转移到 Tenured 区 * Perm 代主要保存 Class、ClassLoader、静态变量、常量、编译后的代码,在 Java7 中堆内方法区会受到 GC 的管理 @@ -10692,9 +10692,9 @@ Heap 堆:是 JVM 内存中最大的一块,由所有线程共享,由垃圾 ```java public static void main(String[] args) { - //返回Java虚拟机中的堆内存总量 + // 返回Java虚拟机中的堆内存总量 long initialMemory = Runtime.getRuntime().totalMemory() / 1024 / 1024; - //返回Java虚拟机使用的最大堆内存量 + // 返回Java虚拟机使用的最大堆内存量 long maxMemory = Runtime.getRuntime().maxMemory() / 1024 / 1024; System.out.println("-Xms : " + initialMemory + "M");//-Xms : 245M @@ -10864,7 +10864,7 @@ public class Demo1_8 extends ClassLoader { // 可以用来加载类的二进制 String b = "iloveu"; //iloveu便是字面量 ``` -* 符号引用:在编译过程中并不知道每个类的地址,因为可能这个类还没有加载,如果在一个类中引用了另一个类,无法知道它的内存地址,只能用他的类名作为符号引用,在类加载完后用这个符号引用去获取内存地址 +* 符号引用:在编译过程中并不知道每个类的地址,因为可能这个类还没有加载,如果在一个类中引用了另一个类,无法知道它的内存地址,只能用它的类名作为符号引用,在类加载完后用这个符号引用去获取内存地址 @@ -11005,7 +11005,7 @@ JVM 是将 TLAB 作为内存分配的首选,但不是所有的对象实例都 ##### 分代介绍 -Java8 时,堆被分为了两份:新生代和老年代(1:2),在 Java7 时,还存在一个永久代 +Java8 时,堆被分为了两份:新生代和老年代(1:2),在 Java7 时,还存在一个永久代 - 新生代使用:复制算法 - 老年代使用:标记 - 清除 或者 标记 - 整理 算法 @@ -11504,12 +11504,6 @@ Java 语言提供了对象终止(finalization)机制来允许开发人员提 #### 标记清除 -当成功区分出内存中存活对象和死亡对象后,GC 接下来的任务就是执行垃圾回收,释放掉无用对象所占用的内存空间,以便有足够的可用内存空间为新对象分配内存。目前在 JVM 中比较常见的三种垃圾收集算法: - -- 标记清除算法(Mark-Sweep) -- 复制算法(copying) -- 标记压缩算法(Mark-Compact) - 标记清除算法,是将垃圾回收分为两个阶段,分别是**标记和清除** - **标记**:Collector 从引用根节点开始遍历,标记所有被引用的对象,一般是在对象的 Header 中记录为可达对象,**标记的是引用的对象,不是垃圾** @@ -11548,8 +11542,7 @@ Java 语言提供了对象终止(finalization)机制来允许开发人员提 | 空间开销 | 少(但会堆积碎片) | 少(不堆积碎片) | 通常需要活对象的 2 倍大小(不堆积碎片) | | 移动对象 | 否 | 是 | 是 | -- 效率上来说,复制算法是当之无愧的老大,但是却浪费了太多内存 -- 为了尽量兼顾三个指标,标记一整理算法相对来说更平滑一些 + @@ -11633,6 +11626,33 @@ Serial:串行垃圾收集器,作用于新生代,是指使用单线程进 +#### ParNew + +Par 是 Parallel 并行的缩写,New 是只能处理的是新生代 + +并行垃圾收集器在串行垃圾收集器的基础之上做了改进,**采用复制算法**,将单线程改为了多线程进行垃圾回收,可以缩短垃圾回收的时间 + +对于其他的行为(收集算法、stop the world、对象分配规则、回收策略等)同 Serial 收集器一样,应用在年轻代,除 Serial 外,只有**ParNew GC 能与 CMS 收集器配合工作** + +相关参数: + +* `-XX:+UseParNewGC`:表示年轻代使用并行收集器,不影响老年代 + +* `-XX:ParallelGCThreads`:默认开启和 CPU 数量相同的线程数 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-ParNew收集器.png) + +ParNew 是很多 JVM 运行在 Server 模式下新生代的默认垃圾收集器 + +- 对于新生代,回收次数频繁,使用并行方式高效 +- 对于老年代,回收次数少,使用串行方式节省资源(CPU 并行需要切换线程,串行可以省去切换线程的资源) + + + +*** + + + #### Parallel Parallel Scavenge 收集器是应用于新生代的并行垃圾回收器,**采用复制算法**、并行回收和 Stop the World 机制 @@ -11662,7 +11682,7 @@ Parallel Old 收集器:是一个应用于老年代的并行垃圾回收器,* * `-XX:+UseParalleloldcc`:手动指定老年代使用并行回收收集器执行内存回收任务 * 上面两个参数,默认开启一个,另一个也会被开启(互相激活),默认 JDK8 是开启的 * `-XX:+UseAdaptivesizepplicy`:设置 Parallel scavenge 收集器具有**自适应调节策略**,在这种模式下,年轻代的大小、Eden 和 Survivor 的比例、晋升老年代的对象年龄等参数会被自动调整,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量 -* `-XX:ParallelGcrhreads`:设置年轻代并行收集器的线程数,一般最好与 CPU 数量相等,以避免过多的线程数影响垃圾收集性能 +* `-XX:ParallelGcrhreads`:设置年轻代并行收集器的线程数,一般与 CPU 数量相等,以避免过多的线程数影响垃圾收集性能 * 在默认情况下,当 CPU 数量小于 8 个,ParallelGcThreads 的值等于 CPU 数量 * 当 CPU 数量大于 8 个,ParallelGCThreads 的值等于 3+[5*CPU Count]/8] * `-XX:MaxGCPauseMillis`:设置垃圾收集器最大停顿时间(即 STW 的时间),单位是毫秒 @@ -11674,31 +11694,6 @@ Parallel Old 收集器:是一个应用于老年代的并行垃圾回收器,* -*** - - - -#### ParNew - -Par 是 Parallel 并行的缩写,New 是只能处理的是新生代 - -并行垃圾收集器在串行垃圾收集器的基础之上做了改进,**采用复制算法**,将单线程改为了多线程进行垃圾回收,可以缩短垃圾回收的时间 - -对于其他的行为(收集算法、stop the world、对象分配规则、回收策略等)同 Serial 收集器一样,应用在年轻代,除 Serial 外,只有**ParNew GC 能与 CMS 收集器配合工作** - -相关参数: - -* `-XX:+UseParNewGC`:表示年轻代使用并行收集器,不影响老年代 - -* `-XX:ParallelGCThreads`:默认开启和 CPU 数量相同的线程数 - -![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-ParNew收集器.png) - -ParNew 是很多 JVM 运行在 Server 模式下新生代的默认垃圾收集器 - -- 对于新生代,回收次数频繁,使用并行方式高效 -- 对于老年代,回收次数少,使用串行方式节省资源(CPU 并行需要切换线程,串行可以省去切换线程的资源) - **** @@ -11751,7 +11746,7 @@ Mark Sweep 会造成内存碎片,不把算法换成 Mark Compact 的原因:M * `-XX:ParallelCMSThreads`:设置 CMS 的线程数量 - * CMS 默认启动的线程数是(ParallelGCThreads+3)/4,ParallelGCThreads 是年轻代并行收集器的线程数 + * CMS 默认启动的线程数是 (ParallelGCThreads+3)/4,ParallelGCThreads 是年轻代并行收集器的线程数 * 收集线程占用的 CPU 资源多于25%,对用户程序影响可能较大;当 CPU 资源比较紧张时,受到 CMS 收集器线程的影响,应用程序的性能在垃圾回收阶段可能会非常糟糕 @@ -11959,7 +11954,7 @@ ZGC 目标: ZGC 的工作过程可以分为 4 个阶段: * 并发标记(Concurrent Mark): 遍历对象图做可达性分析的阶段,也要经过初始标记和最终标记,需要短暂停顿 -* 并发预备重分配( Concurrent Prepare for Relocate):根据特定的查询条件统计得出本次收集过程要清理哪些 Region,将这些 Region 组成重分配集(Relocation Set) +* 并发预备重分配(Concurrent Prepare for Relocate):根据特定的查询条件统计得出本次收集过程要清理哪些 Region,将这些 Region 组成重分配集(Relocation Set) * 并发重分配(Concurrent Relocate): 重分配是 ZGC 执行过程中的核心阶段,这个过程要把重分配集中的存活对象复制到新的 Region 上,并为重分配集中的**每个 Region 维护一个转发表**(Forward Table),记录从旧地址到新地址的转向关系 * 并发重映射(Concurrent Remap):修正整个堆中指向重分配集中旧对象的所有引用,ZGC 的并发映射并不是一个必须要立即完成的任务,ZGC 很巧妙地把并发重映射阶段要做的工作,合并到下一次垃圾收集循环中的并发标记阶段里去完成,因为都是要遍历所有对象,这样合并节省了一次遍历的开销 @@ -12231,7 +12226,7 @@ public Object pop() { * `int[] arr = new int[10]` ```ruby - # 由于需要8位对齐,所以最终大小为56byte`。 + # 由于需要8位对齐,所以最终大小为56byte 4(Mark Word) + 4(Klass Word) + 4(length) + 4*10(10个int大小) + 4(Padding) = 56sbyte ``` @@ -12545,8 +12540,8 @@ Java 对象创建时机: 加载过程完成以下三件事: - 通过类的完全限定名称获取定义该类的二进制字节流(二进制字节码) -- 将该字节流表示的**静态存储结构转换为方法区的运行时存储结构**(Java 类模型) -- 在内存中生成一个代表该类的 Class 对象,作为该类在方法区中的各种数据的访问入口 +- 将该字节流表示的静态存储结构转换为方法区的运行时存储结构(Java 类模型) +- **在内存中生成一个代表该类的 Class 对象,作为该类在方法区中的各种数据的访问入口** 其中二进制字节流可以从以下方式中获取: @@ -12559,7 +12554,7 @@ Java 对象创建时机: 方法区内部采用 C++ 的 instanceKlass 描述 Java 类的数据结构: -* `_java_mirror` 即 java 的类镜像,例如对 String 来说就是 String.class,作用是把 class 暴露给 Java 使用 +* `_java_mirror` 即 Java 的类镜像,例如对 String 来说就是 String.class,作用是把 class 暴露给 Java 使用 * `_super` 即父类、`_fields` 即成员变量、`_methods` 即方法、`_constants` 即常量池、`_class_loader` 即类加载器、`_vtable` **虚方法表**、`_itable` 接口方法表 加载过程: @@ -12619,10 +12614,7 @@ Java 对象创建时机: ##### 准备 -准备阶段为**静态变量分配内存并设置初始值**,使用的是方法区的内存: - -* 类变量也叫静态变量,就是是被 static 修饰的变量 -* 实例变量也叫对象变量,即没加 static 的变量 +准备阶段为**静态变量(类变量)分配内存并设置初始值**,使用的是方法区的内存: 说明:实例变量不会在这阶段分配内存,它会在对象实例化时随着对象一起被分配在堆中,类加载发生在所有实例化操作之前,并且类加载只进行一次,实例化可以进行多次 @@ -12658,7 +12650,7 @@ Java 对象创建时机: 将常量池中类、接口、字段、方法的**符号引用替换为直接引用**(内存地址)的过程: -* 符号引用:一组符号来描述目标,可以是任何字面量,属于编译原理方面的概念,如:包括类和接口的全限名、字段的名称和描述符、方法的名称和**方法描述符** +* 符号引用:一组符号来描述目标,可以是任何字面量,属于编译原理方面的概念,如:包括类和接口的全限名、字段的名称和描述符、方法的名称和**方法描述符**(因为类还没有加载完,很多方法是找不到的) * 直接引用:直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄,如果有了直接引用,那说明引用的目标必定已经存在于内存之中 例如:在 `com.demo.Solution` 类中引用了 `com.test.Quest`,把 `com.test.Quest` 作为符号引用存进类常量池,在类加载完后,**用这个符号引用去方法区找这个类的内存地址** @@ -12789,6 +12781,8 @@ init 指的是实例构造器,主要作用是在类实例化过程中执行, 类实例化过程:**父类的类构造器() -> 子类的类构造器() -> 父类的成员变量和实例代码块 -> 父类的构造函数 -> 子类的成员变量和实例代码块 -> 子类的构造函数** +new 关键字会创建对象并复制 dup 一个对象引用,一个调用 方法,另一个用来赋值给接收者 + *** @@ -13253,7 +13247,7 @@ Java 语言:跨平台的语言(write once ,run anywhere) * 后端运行期编译器:HotSpot VM 的 C1、C2 编译器,也就是 JIT 编译器,Graal 编译器 - * JIT编译器:执行引擎部分详解 + * JIT 编译器:执行引擎部分详解 * Graal 编译器:JDK10 HotSpot 加入的一个全新的即时编译器,编译效果短短几年时间就追平了 C2 * 静态提前编译器:AOT (Ahead Of Time Compiler)编译器,直接把源代码编译成本地机器代码 @@ -13739,7 +13733,7 @@ Java 字节码属于 JVM 基本执行指令。由一个字节长度的代表某 * f 代表 float * d 代表 double -大部分的指令都没有支持 byte、char、short、boolean 类型,编译器会在编译期或运行期将 byte 和 short 类型的数据带符号扩展(Sign-Extend-)为相应的 int 类型数据,将 boolean 和 char 类型数据零位扩展(Zero-Extend-)为相应的 int 类型数据 +大部分的指令都没有支持 byte、char、short、boolean 类型,编译器会在编译期或运行期将 byte 和 short 类型的数据带符号扩展(Sign-Extend-)为相应的 int 类型数据,将 boolean 和 char 类型数据零位扩展(Zero-Extend)为相应的 int 类型数据 在做值相关操作时: @@ -13762,7 +13756,7 @@ Java 字节码属于 JVM 基本执行指令。由一个字节长度的代表某 * 指令 xload_n 表示将第 n 个局部变量压入操作数栈,aload_n 表示将一个对象引用压栈 * 指令 xload n 通过指定参数的形式,把局部变量压入操作数栈,局部变量数量超过 4 个时使用这个命令 -常量入栈指令:将常数压入操作数栈,根据数据类型和入栈内容的不同,又分为 const、push、ldc指令 +常量入栈指令:将常数压入操作数栈,根据数据类型和入栈内容的不同,又分为 const、push、ldc 指令 * push:包括 bipush 和 sipush,区别在于接收数据类型的不同,bipush 接收 8 位整数作为参数,sipush 接收 16 位整数 * ldc:如果以上指令不能满足需求,可以使用 ldc 指令,接收一个 8 位的参数,该参数指向常量池中的 int、 float 或者 String 的索引,将指定的内容压入堆栈。ldc_w 接收两个 8 位参数,能支持的索引范围更大,如果要压入的元素是 long 或 double 类型的,则使用 ldc2_w 指令 @@ -13785,7 +13779,7 @@ Java 字节码属于 JVM 基本执行指令。由一个字节长度的代表某 算术指令用于对两个操作数栈上的值进行某种特定运算,并把计算结果重新压入操作数栈 -没有直接支持 byte、 short、 char 和 boolean类型的算术指令,对于这些数据的运算,都使用 int 类型的指令来处理,数组类型也是转换成 int 数组 +没有直接支持 byte、 short、 char 和 boolean 类型的算术指令,对于这些数据的运算,都使用 int 类型的指令来处理,数组类型也是转换成 int 数组 * 加法指令:iadd、ladd、fadd、dadd * 减法指令:isub、lsub、fsub、dsub @@ -13814,7 +13808,7 @@ double j = i / 0.0; System.out.println(j);//无穷大,NaN: not a number ``` -**分析 i++**:从字节码角度分析:a++ 和 ++a 的区别是先执行 iload 还是 先执行 iinc +**分析 i++**:从字节码角度分析:a++ 和 ++a 的区别是先执行 iload 还是先执行 iinc ```java 4 iload_1 //存入操作数栈 @@ -14820,7 +14814,7 @@ JDK5 以后编译阶段自动转换成上述片段 #### 泛型擦除 -泛型也是在 JDK 5 开始加入的特性,但 Java 在编译泛型代码后会执行**泛型擦除**的动作,即泛型信息。在编译为字节码之后就丢失了,实际的类型都**当做了 Object 类型**来处理: +泛型也是在 JDK 5 开始加入的特性,但 Java 在编译泛型代码后会执行**泛型擦除**的动作,即泛型信息在编译为字节码之后就丢失了,实际的类型都**当做了 Object 类型**来处理: ```java List list = new ArrayList(); @@ -14880,7 +14874,7 @@ public static void main(String[] args) { #### foreach -**数组的循环:** +数组的循环: ```java int[] array = {1, 2, 3, 4, 5}; // 数组赋初值的简化写法也是语法糖 @@ -14898,7 +14892,7 @@ for(int i = 0; i < array.length; ++i) { } ``` -**集合的循环:** +集合的循环: ```java List list = Arrays.asList(1,2,3,4,5); @@ -15093,7 +15087,7 @@ try(资源变量 = 创建资源对象){ } ``` -其中资源对象需要实现 **AutoCloseable**接口,例如 InputStream、OutputStream、Connection、Statement、ResultSet 等接口都实现了 AutoCloseable ,使用 try-withresources可以不用写 finally 语句块,编译器会帮助生成关闭资源代码: +其中资源对象需要实现 **AutoCloseable** 接口,例如 InputStream、OutputStream、Connection、Statement、ResultSet 等接口都实现了 AutoCloseable ,使用 try-withresources可以不用写 finally 语句块,编译器会帮助生成关闭资源代码: ```java try(InputStream is = new FileInputStream("d:\1円.txt")) { @@ -17786,7 +17780,7 @@ public class MGraph { 布隆过滤器查询一个数据,是否在二进制的集合中,查询过程如下: -- 通过 K 个哈希函数计算该数据,对应计算出的 K 个hash值 +- 通过 K 个哈希函数计算该数据,对应计算出的 K 个 hash 值 - 通过 hash 值找到对应的二进制的数组下标 - 判断方法:如果存在一处位置的二进制数据是 0,那么该数据一定不存在。如果都是 1,则认为数据存在集合中(会误判) diff --git a/Prog.md b/Prog.md index 9bf9c71..5b3a51c 100644 --- a/Prog.md +++ b/Prog.md @@ -8,7 +8,7 @@ 进程的特征:并发性、异步性、动态性、独立性、结构性 -**线程**:线程是属于进程的,是一个基本的 CPU 执行单元,是程序执行流的最小单元。线程是进程中的一个实体,是系统**独立调度的基本单位**,线程本身不拥有系统资源,只拥有一点在运行中必不可少的资源,但它可与同属一个进程的其他线程共享进程所拥有的全部资源 +**线程**:线程是属于进程的,是一个基本的 CPU 执行单元,是程序执行流的最小单元。线程是进程中的一个实体,是系统**独立调度的基本单位**,线程本身不拥有系统资源,只拥有一点在运行中必不可少的资源,与同属一个进程的其他线程共享进程所拥有的全部资源 关系:一个进程可以包含多个线程,这就是多线程,比如看视频是进程,图画、声音、广告等就是多个线程 @@ -389,7 +389,7 @@ public class Test { 打断的线程会发生上下文切换,操作系统会保存线程信息,抢占到 CPU 后会从中断的地方接着运行(打断不是停止) -* sleep、wait、join 方法都会让线程进入阻塞状态,打断进程**会清空打断状态**(false) +* sleep、wait、join 方法都会让线程进入阻塞状态,打断线程**会清空打断状态**(false) ```java public static void main(String[] args) throws InterruptedException { @@ -514,7 +514,7 @@ class TwoPhaseTermination { System.out.println("执行监控记录"); // 在此被打断不会异常 } catch (InterruptedException e) { // 在睡眠期间被打断,进入异常处理的逻辑 e.printStackTrace(); - // 重新设置打断标记 + // 重新设置打断标记,打断 sleep 会清除打断状态 thread.interrupt(); } } @@ -651,7 +651,7 @@ Java 提供了线程优先级的机制,优先级会提示(hint)调度器 #### 未来优化 -内核级线程调度的成本较大,所以引入了更轻量级的协程。用户线程的调度由用户自己实现,被设计为协同式调度,所以叫协程 +内核级线程调度的成本较大,所以引入了更轻量级的协程。用户线程的调度由用户自己实现(一对多的线程模型),被设计为协同式调度,所以叫协程 * 有栈协程:协程会完整的做调用栈的保护、恢复工作,所以叫有栈协程 * 无栈协程:本质上是一种有限状态机,状态保存在闭包里,比有栈协程更轻量,但是功能有限 @@ -711,13 +711,13 @@ Java 提供了线程优先级的机制,优先级会提示(hint)调度器 ### 查看线程 -windows: +Windows: * 任务管理器可以查看进程和线程数,也可以用来杀死进程 * tasklist 查看进程 * taskkill 杀死进程 -linux: +Linux: * ps -ef 查看所有进程 * ps -fT -p 查看某个进程(PID)的所有线程 @@ -1391,43 +1391,6 @@ public class Dead { } ``` -面向对象写法: - -```java -public class DeadLock { - static String lockA = "lockA"; - static String lockB = "lockB"; - public static void main(String[] args) { - new Thread(new HoldLockThread(lockA, lockB)).start(); - new Thread(new HoldLockThread(lockB, lockA)).start(); - } -} -class HoldLockThread implements Runnable { - private String lockA; - private String lockB; - - public HoldLockThread(String lockA, String lockB) { - this.lockA = lockA; - this.lockB = lockB; - } - - @Override - public void run() { - synchronized (lockA) { - System.out.println(Thread.currentThread().getName() + " 持有" + lockA + ",尝试获得" + lockB); - try { - Thread.sleep(2000); - } catch (InterruptedException e) { - e.printStackTrace(); - } - synchronized (lockB) { - System.out.println(Thread.currentThread().getName() + " 持有" + lockB + ",尝试获得" + lockA); - } - } - } -} -``` - *** @@ -1736,7 +1699,7 @@ LockSupport 出现就是为了增强 wait & notify 的功能: ```java Hashtable table = new Hashtable(); // 线程1,线程2 - if( table.get("key") == null) { + if(table.get("key") == null) { table.put("key", value); } ``` @@ -2578,7 +2541,7 @@ volatile 修饰的变量,可以禁用指令重排 使用 volatile 修饰的共享变量,总线会开启 **CPU 总线嗅探机制**来解决 JMM 缓存一致性问题,也就是共享变量在多线程中可见性的问题,实现 MESI 缓存一致性协议 -底层是通过汇编 lock 前缀指令,共享变量加了 lock 前缀指令,在线程修改完共享变量后,会先执行**缓存锁定**的操作然后写回主存,其他的 CPU 上运行的线程根据 CPU 总线嗅探机制会修改其共享变量为失效状态,读取时会重新从主内存中读取最新的数据 +底层是通过汇编 lock 前缀指令,共享变量加了 lock 前缀指令就会进行缓存锁定,在线程修改完共享变量后写回主存,其他的 CPU 核心上运行的线程根据总线嗅探机制会修改其共享变量为失效状态,读取时会重新从主内存中读取最新的数据 lock 前缀指令就相当于内存屏障,Memory Barrier(Memory Fence) @@ -2636,7 +2599,7 @@ lock 前缀指令就相当于内存屏障,Memory Barrier(Memory Fence) 不能解决指令交错: -* 写屏障仅仅是保证之后的读能够读到最新的结果,但不能保证其他的读跑到写屏障之前 +* 写屏障仅仅是保证之后的读能够读到最新的结果,但不能保证其他线程的读跑到写屏障之前 * 有序性的保证也只是保证了本线程内相关代码不被重排序 @@ -2976,8 +2939,8 @@ CAS 缺点: CAS 与 synchronized 总结: -* synchronized 是从悲观的角度出发:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞(**共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程**),因此 synchronized 也称之为悲观锁,ReentrantLock 也是一种悲观锁,**性能较差** -* CAS 是从乐观的角度出发:总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据。**如果别人修改过,则获取现在最新的值,如果别人没修改过,直接修改共享数据的值**,CAS 这种机制也称之为乐观锁,**综合性能较好** +* synchronized 是从悲观的角度出发:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程),因此 synchronized 也称之为悲观锁,ReentrantLock 也是一种悲观锁,性能较差 +* CAS 是从乐观的角度出发:总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据。**如果别人修改过,则获取现在最新的值,如果别人没修改过,直接修改共享数据的值**,CAS 这种机制也称之为乐观锁,综合性能较好 @@ -3225,7 +3188,7 @@ LongAdder 和 LongAccumulator 区别: 相同点: -* LongAddr 与 LongAccumulator 类都是使用非阻塞算法 CAS 实现的, +* LongAddr 与 LongAccumulator 类都是使用非阻塞算法 CAS 实现的 * LongAddr 类是 LongAccumulator 类的一个特例,只是 LongAccumulator 提供了更强大的功能,可以自定义累加规则,当accumulatorFunction 为 null 时就等价于 LongAddr 不同点: @@ -4607,8 +4570,8 @@ java.util.concurrent.BlockingQueue 接口有以下阻塞队列的实现:**FIFO 与普通队列(LinkedList、ArrayList等)的不同点在于阻塞队列中阻塞添加和阻塞删除方法,以及线程安全: -* 阻塞添加 take():当阻塞队列元素已满时,添加队列元素的线程会被阻塞,直到队列元素不满时才重新唤醒线程执行 -* 阻塞删除 put():在队列元素为空时,删除队列元素的线程将被阻塞,直到队列不为空再执行删除操作(一般会返回被删除的元素) +* 阻塞添加 put():当阻塞队列元素已满时,添加队列元素的线程会被阻塞,直到队列元素不满时才重新唤醒线程执行 +* 阻塞删除 take():在队列元素为空时,删除队列元素的线程将被阻塞,直到队列不为空再执行删除操作(一般会返回被删除的元素) @@ -5251,7 +5214,7 @@ TransferStack 类成员方法: ##### 公平实现 -TransferQueue 是公平的同步队列,采用 FIFO 的队列实现 +TransferQueue 是公平的同步队列,采用 FIFO 的队列实现,请求节点与队尾模式不同,需要与队头发生匹配 TransferQueue 类成员变量: @@ -5602,7 +5565,7 @@ Executors 提供了四种线程池的创建:newCachedThreadPool、newFixedThre 原因:父类不能直接调用子类中的方法,需要反射或者创建对象的方式,可以调用子类静态方法 -* Executors.newFixedThreadPool(1) 初始时为1,可以修改。对外暴露的是 ThreadPoolExecutor 对象,可以强转后调用 setCorePoolSize 等方法进行修改 +* Executors.newFixedThreadPool(1) 初始时为 1,可以修改。对外暴露的是 ThreadPoolExecutor 对象,可以强转后调用 setCorePoolSize 等方法进行修改 ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-newSingleThreadExecutor.png) @@ -5687,7 +5650,7 @@ ExecutorService 类 API: | 方法 | 说明 | | ----------------------------------------------------- | ------------------------------------------------------------ | -| void shutdown() | 线程池状态变为 SHUTDOWN,等待任务执行完后关闭线程池,不会接收新任务,但已提交任务会执行完,而且也可以添加线程 | +| void shutdown() | 线程池状态变为 SHUTDOWN,等待任务执行完后关闭线程池,不会接收新任务,但已提交任务会执行完,而且也可以添加线程(不绑定ren'wu) | | List shutdownNow() | 线程池状态变为 STOP,用 interrupt 中断正在执行的任务,直接关闭线程池,不会接收新任务,会将队列中的任务返回 | | boolean isShutdown() | 不在 RUNNING 状态的线程池,此执行者已被关闭,方法返回 true | | boolean isTerminated() | 线程池状态是否是 TERMINATED,如果所有任务在关闭后完成,返回 true | @@ -6050,7 +6013,7 @@ ThreadPoolExecutor 使用 int 的**高 3 位来表示线程池状态,低 29 * addWorker():**添加线程到线程池**,返回 true 表示创建 Worker 成功,且线程启动。首先判断线程池是否允许添加线程,允许就让线程数量 + 1,然后去创建 Worker 加入线程池 - 注意:SHUTDOWN 状态也能添加线程,但是要求新加的 Woker 没有 firstTask,而且当前 queue 不为空,所以创建一个线程来帮助执行队列中的任务 + 注意:SHUTDOWN 状态也能添加线程,但是要求新加的 Woker 没有 firstTask,而且当前 queue 不为空,所以创建一个线程来帮助线程池执行队列中的任务 ```java // core == true 表示采用核心线程数量限制,false 表示采用 maximumPoolSize @@ -6388,7 +6351,7 @@ ThreadPoolExecutor 使用 int 的**高 3 位来表示线程池状态,低 29 } ``` -* interruptIdleWorkers():shutdown 方法会**中断所有空闲线程**,根据是否可以获取 AQS 独占锁判断是否处于工作状态。线程之所以空闲是因为阻塞队列没有任务,不会中断正在运行的线程,所以 shutdown 方法会让所有的任务执行完毕。 +* interruptIdleWorkers():shutdown 方法会**中断所有空闲线程**,根据是否可以获取 AQS 独占锁判断是否处于工作状态。线程之所以空闲是因为阻塞队列没有任务,不会中断正在运行的线程,所以 shutdown 方法会让所有的任务执行完毕 ```java // onlyOne == true 说明只中断一个线程 ,false 则中断所有线程 @@ -8309,7 +8272,7 @@ public void lock() { return node; } } - // 初始时没有队列为空,或者 CAS 失败进入这里 + // 初始时队列为空,或者 CAS 失败进入这里 enq(node); return node; } @@ -9056,7 +9019,7 @@ public static void main(String[] args) throws InterruptedException { ###### await -总体流程是将 await 线程包装成 node 节点放入 ConditionObject 的条件队列,如果被唤醒就将 node 转移到 AQS 的执行阻塞队列,等待获取锁 +总体流程是将 await 线程包装成 node 节点放入 ConditionObject 的条件队列,如果被唤醒就将 node 转移到 AQS 的执行阻塞队列,等待获取锁,**每个 Condition 对象都包含一个等待队列** * 开始 Thread-0 持有锁,调用 await,线程进入 ConditionObject 等待,直到被唤醒或打断,调用 await 方法的线程都是持锁状态的,所以说逻辑里**不存在并发** @@ -10422,7 +10385,7 @@ public static void main(String[] args) { try { // 根据是否需要超时等待选择阻塞方法 if (!timed) - // 当前线程释放掉 lock,进入到 trip 条件队列的尾部挂起自己,等待被唤醒 + // 当前线程释放掉 lock,【进入到 trip 条件队列的尾部挂起自己】,等待被唤醒 trip.await(); else if (nanos> 0L) nanos = trip.awaitNanos(nanos); @@ -10511,7 +10474,7 @@ Semaphore(信号量)用来限制能同时访问共享资源的线程上限 构造方法: * `public Semaphore(int permits)`:permits 表示许可线程的数量(state) -* `public Semaphore(int permits, boolean fair)`:fair 表示公平性,如果这个设为 true 的话,下次执行的线程会是等待最久的线程 +* `public Semaphore(int permits, boolean fair)`:fair 表示公平性,如果设为 true,下次执行的线程会是等待最久的线程 常用API: @@ -11278,19 +11241,7 @@ B站视频解析:https://www.bilibili.com/video/BV1n541177Ea break; // 与 addCount 逻辑相同 else if (tab == table) { - int rs = resizeStamp(n); - if (sc < 0) { - Node[] nt; - if ((sc>>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 || - sc == rs + MAX_RESIZERS || (nt = nextTable) == null || - transferIndex <= 0) - break; - if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) - transfer(tab, nt); - } - else if (U.compareAndSwapInt(this, SIZECTL, sc, - (rs << RESIZE_STAMP_SHIFT) + 2)) - transfer(tab, null); + } } } @@ -11976,7 +11927,7 @@ ConcurrentHashMap 使用 get() 方法获取指定 key 的数据 } ``` -* replaceNode():替代指定的元素,会协助扩容,**增删改(写)都会协助扩容,只有查询(读)操作不会**,因为读操作不涉及加锁 +* replaceNode():替代指定的元素,会协助扩容,**增删改(写)都会协助扩容,查询(读)操作不会**,因为读操作不涉及加锁 ```java final V replaceNode(Object key, V value, Object cv) { @@ -12166,7 +12117,7 @@ public CopyOnWriteArraySet() { 适合读多写少的应用场景 -* 迭代器:CopyOnWriteArrayList 在返回迭代器时,**创建一个该内部数组当前的快照(引用)**,即使其他线程替换了原始数组,迭代器遍历的快照依然引用的是创建快照时的数组,所以这种实现方式也存在一定的数据延迟性,对其他线程并行添加的数据不可见 +* 迭代器:CopyOnWriteArrayList 在返回迭代器时,**创建一个内部数组当前的快照(引用)**,即使其他线程替换了原始数组,迭代器遍历的快照依然引用的是创建快照时的数组,所以这种实现方式也存在一定的数据延迟性,对其他线程并行添加的数据不可见 ```java public Iterator iterator() { @@ -13223,7 +13174,7 @@ final void updateHead(Node h, Node p) { * C/S 结构 :全称为 Client/Server 结构,是指客户端和服务器结构,常见程序有 QQ、IDEA 等软件 * B/S 结构 :全称为 Browser/Server 结构,是指浏览器和服务器结构 -两种架构各有优势,但是无论哪种架构,都离不开网络的支持。、 +两种架构各有优势,但是无论哪种架构,都离不开网络的支持 网络通信的三要素: @@ -13231,7 +13182,7 @@ final void updateHead(Node h, Node p) { 2. IP 地址:互联网协议地址(Internet Protocol Address),用来给一个网络中的计算机设备做唯一的编号 - * IPv4:4个字节,32 位组成,192.168.1.1 + * IPv4:4 个字节,32 位组成,192.168.1.1 * IPv6:可以实现为所有设备分配 IP,128 位 * ipconfig:查看本机的 IP @@ -13258,14 +13209,6 @@ final void updateHead(Node h, Node p) { 网络通信协议:对计算机必须遵守的规则,只有遵守这些规则,计算机之间才能进行通信 -> 应用层:应用程序(QQ、微信、浏览器),可能用到的协议(HTTP、FTP、SMTP) -> -> 传输层:TCP/IP 协议 - UDP 协议 -> -> 网络层 :IP 协议,封装自己的 IP 和对方的 IP 和端口 -> -> 数据链路层 : 进入到硬件(网) - 通信**是进程与进程之间的通信**,不是主机与主机之间的通信 TCP/IP协议:传输控制协议 (Transmission Control Protocol) @@ -13301,7 +13244,7 @@ Java 中的通信模型: 同步阻塞式性能极差:大量线程,大量阻塞 -2. 伪异步通信:引入线程池,不需要一个客户端一个线程,实现线程复用来处理很多个客户端,线程可控。 +2. 伪异步通信:引入线程池,不需要一个客户端一个线程,实现线程复用来处理很多个客户端,线程可控 高并发下性能还是很差:线程数量少,数据依然是阻塞的,数据没有来线程还是要等待 @@ -13422,7 +13365,7 @@ IO 复用让单个进程具有处理多个 I/O 事件的能力,又被称为 Ev #### 异步 IO -应用进程执行 aio_read 系统调用会立即返回,给内核传递描述符、缓冲区指针、缓冲区大小等。应用进程可以继续执行不会被阻塞,内核会在所有操作完成之后向应用进程发送信号。 +应用进程执行 aio_read 系统调用会立即返回,给内核传递描述符、缓冲区指针、缓冲区大小等。应用进程可以继续执行不会被阻塞,内核会在所有操作完成之后向应用进程发送信号 异步 I/O 与信号驱动 I/O 的区别在于,异步 I/O 的信号是通知应用进程 I/O 完成,而信号驱动 I/O 的信号是通知应用进程可以开始 I/O @@ -13440,7 +13383,7 @@ IO 复用让单个进程具有处理多个 I/O 事件的能力,又被称为 Ev ##### 函数 -Socket 不是文件,只是一个标识符,但是 Unix 操作系统把所有东西都**看作**是文件,所以 socket 说成 file descriptor,也就是 fd +Socket 不是文件,只是一个标识符,但是 Unix 操作系统把所有东西都**看作**是文件,所以 Socket 说成 file descriptor,也就是 fd select 允许应用程序监视一组文件描述符,等待一个或者多个描述符成为就绪状态,从而完成 I/O 操作。 @@ -13753,7 +13696,7 @@ epoll 的特点: * 进程描述符和用户的进程是一一对应的 * SYS_API 系统调用:如 read、write,系统调用就是 0X80 中断 -* 进程描述符 pd:进程从用户态切换到内核态时,需要保存用户态时的上下文信息在 PCB 中 +* 进程描述符 pd:进程从用户态切换到内核态时,需要**保存用户态时的上下文信息在 PCB 中** * 线程上下文:用户程序基地址,程序计数器、cpu cache、寄存器等,方便程序切回用户态时恢复现场 * 内核堆栈:**系统调用函数也是要创建变量的,**这些变量在内核堆栈上分配 @@ -13858,7 +13801,7 @@ mmap(Memory Mapped Files)内存映射加 write 实现零拷贝,**零拷贝 进行了 4 次用户空间与内核空间的上下文切换,以及 3 次数据拷贝(2 次 DMA,一次 CPU 复制): * 发出 mmap 系统调用,DMA 拷贝到内核缓冲区,映射到共享缓冲区;mmap 系统调用返回,无需拷贝 -* 发出 write 系统调用,将数据从内核缓冲区拷贝到内核 Socket 缓冲区;write系统调用返回,DMA 将内核空间 Socket 缓冲区中的数据传递到协议引擎 +* 发出 write 系统调用,将数据从内核缓冲区拷贝到内核 Socket 缓冲区;write 系统调用返回,DMA 将内核空间 Socket 缓冲区中的数据传递到协议引擎 ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/IO-mmap工作流程.png) @@ -13905,7 +13848,7 @@ Java NIO 对 sendfile 的支持是 `FileChannel.transferTo()/transferFrom()`, 成员方法: * `static InetAddress getLocalHost()`:获得本地主机 IP 地址对象 -* `static InetAddress getByName(String host)`:根据 IP 地址字符串或主机名获得对应的IP地址对象 +* `static InetAddress getByName(String host)`:根据 IP 地址字符串或主机名获得对应的 IP 地址对象 * `String getHostName()`:获取主机名 * `String getHostAddress()`:获得 IP 地址字符串 @@ -14072,7 +14015,6 @@ TCP/IP 协议的特点: * 面向连接的协议,提供可靠交互,速度慢 * 点对点的全双工通信 -* 只能由客户端主动发送数据给服务器端,服务器端接收到数据之后,可以给客户端响应数据 * 通过**三次握手**建立连接,连接成功形成数据传输通道;通过**四次挥手**断开连接 * 基于字节流进行数据传输,传输数据大小没有限制 @@ -14562,7 +14504,7 @@ NIO 和 BIO 的比较: ### 实现原理 -NIO 三大核心部分:Channel( 通道) ,Buffer( 缓冲区), Selector( 选择器) +NIO 三大核心部分:Channel (通道)、Buffer (缓冲区)、Selector (选择器) * Buffer 缓冲区 @@ -14570,7 +14512,7 @@ NIO 三大核心部分:Channel( 通道) ,Buffer( 缓冲区), Selector( 选 * Channel 通道 - Java NIO 的通道类似流,不同的是既可以从通道中读取数据,又可以写数据到通道,流的读写通常是单向的,通道可以非阻塞读取和写入通道,支持读取或写入缓冲区,也支持异步地读写。 + Java NIO 的通道类似流,不同的是既可以从通道中读取数据,又可以写数据到通道,流的读写通常是单向的,通道可以非阻塞读取和写入通道,支持读取或写入缓冲区,也支持异步地读写 * Selector 选择器 @@ -14955,8 +14897,8 @@ FileChannel 中的成员属性: * MapMode.mode:内存映像文件访问的方式,共三种: * `MapMode.READ_ONLY`:只读,修改得到的缓冲区将导致抛出异常 - * `MapMode.READ_WRITE`:读/写,对缓冲区的更改最终将写入文件,但此次修改对映射到同一文件的其他程序不一定是可见的 - * `MapMode.PRIVATE`:私用,可读可写,但是修改的内容不会写入文件,只是 buffer 自身的改变,称之为写时复制 + * `MapMode.READ_WRITE`:读/写,对缓冲区的更改最终将写入文件,但此次修改对映射到同一文件的其他程序不一定是可见 + * `MapMode.PRIVATE`:私用,可读可写,但是修改的内容不会写入文件,只是 buffer 自身的改变 * `public final FileLock lock()`:获取此文件通道的排他锁 @@ -15026,7 +14968,7 @@ public class MappedByteBufferTest { * 通道可以实现异步读写数据 * 通道可以从缓冲读数据,也可以写数据到缓冲 -2. BIO 中的 Stream 是单向的,NIO中的 Channel 是双向的,可以读操作,也可以写操作 +2. BIO 中的 Stream 是单向的,NIO 中的 Channel 是双向的,可以读操作,也可以写操作 3. Channel 在 NIO 中是一个接口:`public interface Channel extends Closeable{}` @@ -15041,7 +14983,7 @@ Channel 实现类: * SocketChannel:通过 TCP 读写网络中的数据 -* ServerSocketChannel:可以监听新进来的 TCP 连接,对每一个新进来的连接都会创建一个 SocketChannel +* ServerSocketChannel:可以**监听**新进来的 TCP 连接,对每一个新进来的连接都会创建一个 SocketChannel 提示:ServerSocketChanne 类似 ServerSocket、SocketChannel 类似 Socket @@ -15266,11 +15208,11 @@ public class ChannelTest { #### 基本介绍 -选择器(Selector) 是 SelectableChannle 对象的**多路复用器**,Selector 可以同时监控多个通道的状况,利用 Selector 可使一个单独的线程管理多个 Channel,**Selector 是非阻塞 IO 的核心**。 +选择器(Selector) 是 SelectableChannle 对象的**多路复用器**,Selector 可以同时监控多个通道的状况,利用 Selector 可使一个单独的线程管理多个 Channel,**Selector 是非阻塞 IO 的核心** ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/NIO-Selector.png) -* Selector 能够检测多个注册的通道上是否有事件发生(多个 Channel 以事件的方式可以注册到同一个 Selector),如果有事件发生,便获取事件然后针对每个事件进行相应的处理,就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求 +* Selector 能够检测多个注册的通道上是否有事件发生(多个 Channel 以事件的方式可以注册到同一个 Selector),如果有事件发生,就获取事件然后针对每个事件进行相应的处理,就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求 * 只有在连接/通道真正有读写事件发生时,才会进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程 * 避免了多线程之间的上下文切换导致的开销 @@ -15487,13 +15429,17 @@ public class Client { + + *** + + ## AIO -Java AIO(NIO.2) : AsynchronousI/O,异步非阻塞,采用了 Proactor 模式。服务器实现模式为一个有效请求一个线程,客户端的 I/O 请求都是由 OS 先完成了再通知服务器应用去启动线程进行处理。 +Java AIO(NIO.2) : AsynchronousI/O,异步非阻塞,采用了 Proactor 模式。服务器实现模式为一个有效请求一个线程,客户端的 I/O 请求都是由 OS 先完成了再通知服务器应用去启动线程进行处理 ```java AIO异步非阻塞,基于NIO的,可以称之为NIO2.0 diff --git a/Tool.md b/Tool.md index 004807e..5f786da 100644 --- a/Tool.md +++ b/Tool.md @@ -2185,9 +2185,9 @@ pstree -A #查看所有进程树 * BIOS:基于 I/O 处理系统 * Bootloader:加载 OS,将 OS 放入内存 -自举程序存储在内存中 ROM(BIOS 芯片),**用来加载操作系统**。CPU 的程序计数器指向 ROM 中自举程序第一条指令,当计算机**通电**,CPU 开始读取并执行自举程序,将操作系统(不是全部,只是启动计算机的那部分程序)装入 RAM 中,这个过程是自举过程。装入完成后 CPU 的程序计数器就被设置为 RAM 中操作系统的**第一条指令**所对应的位置,接下来 CPU 将开始执行操作系统的指令 +自举程序存储在内存中 ROM,**用来加载操作系统**,初始化 CPU、寄存器、内存等。CPU 的程序计数器指自举程序第一条指令,当计算机**通电**,CPU 开始读取并执行自举程序,将操作系统(不是全部,只是启动计算机的那部分程序)装入 RAM 中,这个过程是自举过程。装入完成后程序计数器设置为 RAM 中操作系统的**第一条指令**,接下来 CPU 将开始执行(启动)操作系统的指令 -存储在 ROM 中保留很小的自举装入程序,完整功能的自举程序保存在磁盘的启动块上,启动块位于磁盘的固定位,拥有启动分区的磁盘称为启动磁盘或系统磁盘(C盘) +存储在 ROM 中保留很小的自举装入程序,完整功能的自举程序保存在磁盘的启动块上,启动块位于磁盘的固定位,拥有启动分区的磁盘称为启动磁盘或系统磁盘(C 盘) diff --git a/Web.md b/Web.md index a1ff3d5..b8e1725 100644 --- a/Web.md +++ b/Web.md @@ -2370,12 +2370,12 @@ HTTPS 工作流程:服务器端的公钥和私钥,用来进行非对称加 | 404 | 客户端错误,请求资源未找到 | | 500 | 服务器错误,服务器运行内部错误 | - 面试题: + 转移: * 301 redirect:301 代表永久性转移 (Permanently Moved) * 302 redirect:302 代表暂时性转移 (Temporarily Moved ) -* 响应头:以 key:vaue 存在,可能多个 value 情况。 +* 响应头:以 key:vaue 存在,可能多个 value 情况 | 消息头 | 说明 | | ----------------------- | ------------------------------------------------------------ | @@ -4468,8 +4468,8 @@ Cookie:客户端会话管理技术,把要共享的数据保存到了客户 设置 Cookie 存活时间 API:`void setMaxAge(int expiry)` -* -1:默认。代表 Cookie 数据存到浏览器关闭(保存在浏览器文件中) -* 0:代表删除 Cookie,如果要删除 Cookie 要确保**路径一致**。 +* -1:默认,代表 Cookie 数据存到浏览器关闭(保存在浏览器文件中) +* 0:代表删除 Cookie,如果要删除 Cookie 要确保**路径一致** * 正整数:以秒为单位保存数据有有效时间(把缓存数据保存到磁盘中) ```java @@ -4564,7 +4564,7 @@ XSS 全称 Cross SiteScript,跨站脚本攻击,是Web程序中常见的漏 ### 基本介绍 -Session:服务器端会话管理技术,本质也是采用客户端会话管理技术,不过在客户端保存的是一个特殊标识,共享的数据保存到了服务器的内存对象中。每次请求时,会将特殊标识带到服务器端,根据标识来找到对应的内存空间,从而实现数据共享。简单说它就是一个服务端会话对象,用于存储用户的会话数据。 +Session:服务器端会话管理技术,本质也是采用客户端会话管理技术,不过在客户端保存的是一个特殊标识,共享的数据保存到了服务器的内存对象中。每次请求时,会将特殊标识带到服务器端,根据标识来找到对应的内存空间,从而实现数据共享。简单说它就是一个服务端会话对象,用于存储用户的会话数据 Session 域(会话域)对象是 Servlet 规范中四大域对象之一,并且它也是用于实现数据共享的 @@ -4618,7 +4618,7 @@ HttpServletRequest类获取Session: #### 实现会话 -通过第一个Servlet设置共享的数据用户名,并在第二个Servlet获取到。 +通过第一个Servlet设置共享的数据用户名,并在第二个Servlet获取到 项目执行完以后,去浏览器抓包,Request Headers 中的 Cookie JSESSIONID的值是一样的 @@ -4672,7 +4672,7 @@ public class ServletDemo02 extends HttpServlet{ #### 生命周期 -Session 的创建:一个常见的错误是以为 Session 在有客户端访问时就被创建,事实是直到某 server 端程序(如Servlet)调用 `HttpServletRequest.getSession(true)` 这样的语句时才会被创建 +Session 的创建:一个常见的错误是以为 Session 在有客户端访问时就被创建,事实是直到某 server 端程序(如 Servlet)调用 `HttpServletRequest.getSession(true)` 这样的语句时才会被创建 Session 在以下情况会被删除: From a8af5d18a53e125b0215d8e6481fcbe506006f3b Mon Sep 17 00:00:00 2001 From: Seazean Date: Mon, 4 Jul 2022 00:48:09 +0800 Subject: [PATCH 12/35] Update Java Note --- DB.md | 203 +++++++++++++++++++++---------------------------------- Frame.md | 20 +++--- Java.md | 2 +- 3 files changed, 89 insertions(+), 136 deletions(-) diff --git a/DB.md b/DB.md index b26ee6b..22e3274 100644 --- a/DB.md +++ b/DB.md @@ -241,13 +241,13 @@ SHOW PROCESSLIST:查看当前 MySQL 在进行的线程,可以实时地查看 | State | 显示使用当前连接的 sql 语句的状态,以查询为例,需要经过 copying to tmp table、sorting result、sending data等状态才可以完成 | | Info | 显示执行的 sql 语句,是判断问题语句的一个重要依据 | -**Sending data 状态**表示 MySQL 线程开始访问数据行并把结果返回给客户端,而不仅仅只是返回给客户端,是处于执行器过程中的任意阶段。由于在 Sending data 状态下,MySQL 线程需要做大量磁盘读取操作,所以是整个查询中耗时最长的状态。 +**Sending data 状态**表示 MySQL 线程开始访问数据行并把结果返回给客户端,而不仅仅只是返回给客户端,是处于执行器过程中的任意阶段。由于在 Sending data 状态下,MySQL 线程需要做大量磁盘读取操作,所以是整个查询中耗时最长的状态 -*** +*** @@ -280,7 +280,7 @@ SHOW PROCESSLIST:查看当前 MySQL 在进行的线程,可以实时地查看 ##### 缓存配置 -1. 查看当前的 MySQL 数据库是否支持查询缓存: +1. 查看当前 MySQL 数据库是否支持查询缓存: ```mysql SHOW VARIABLES LIKE 'have_query_cache'; -- YES @@ -399,7 +399,7 @@ SELECT * FROM t WHERE id = 1; 解析器:处理语法和解析查询,生成一课对应的解析树 * 先做**词法分析**,输入的是由多个字符串和空格组成的一条 SQL 语句,MySQL 需要识别出里面的字符串分别是什么代表什么。从输入的 select 这个关键字识别出来这是一个查询语句;把字符串 t 识别成 表名 t,把字符串 id 识别成列 id -* 然后做**语法分析**,根据词法分析的结果,语法分析器会根据语法规则,判断你输入的这个 SQL 语句是否满足 MySQL 语法。如果你的语句不对,就会收到 `You have an error in your SQL syntax` 的错误提醒 +* 然后做**语法分析**,根据词法分析的结果,语法分析器会根据语法规则,判断你输入的这个 SQL 语句是否满足 MySQL 语法。如果语句不对,就会收到 `You have an error in your SQL syntax` 的错误提醒 预处理器:进一步检查解析树的合法性,比如数据表和数据列是否存在、别名是否有歧义等 @@ -1650,7 +1650,7 @@ SELECT * FROM emp WHERE name REGEXP '[uvw]';-- 匹配包含 uvw 的name值 #### 约束介绍 -约束:对表中的数据进行限定,保证数据的正确性、有效性、完整性! +约束:对表中的数据进行限定,保证数据的正确性、有效性、完整性 约束的分类: @@ -1724,7 +1724,7 @@ SELECT * FROM emp WHERE name REGEXP '[uvw]';-- 匹配包含 uvw 的name值 #### 主键自增 -主键自增约束可以为空,并自动增长。删除某条数据不影响自增的下一个数值,依然按照前一个值自增。 +主键自增约束可以为空,并自动增长。删除某条数据不影响自增的下一个数值,依然按照前一个值自增 * 建表时添加主键自增约束 @@ -2122,19 +2122,11 @@ STRAIGHT_JOIN与 JOIN 类似,只不过左表始终在右表之前读取,只 salary DOUBLE -- 员工工资 ); -- 添加数据 - INSERT INTO employee VALUES (1001,'孙悟空',1005,9000.00), - (1002,'猪八戒',1005,8000.00), - (1003,'沙和尚',1005,8500.00), - (1004,'小白龙',1005,7900.00), - (1005,'唐僧',NULL,15000.00), - (1006,'武松',1009,7600.00), - (1007,'李逵',1009,7400.00), - (1008,'林冲',1009,8100.00), - (1009,'宋江',NULL,16000.00); + INSERT INTO employee VALUES (1001,'孙悟空',1005,9000.00),..,(1009,'宋江',NULL,16000.00); ``` - + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/自关联查询数据准备.png) - + * 数据查询 ```mysql @@ -2334,7 +2326,7 @@ BNL 即 Block Nested-Loop Join 算法,由于要访问多次被驱动表,会 子查询物化会产生建立临时表的成本,但是将子查询转化为连接查询可以充分发挥优化器的作用,所以引入:半连接 -* t1 和 t2 表进行半连接,对于 t1 表中的某条记录,只需要关心在 s2 表中是否存在,而不需要关心有多少条记录与之匹配,最终结果集只保留 t1 的记录 +* t1 和 t2 表进行半连接,对于 t1 表中的某条记录,只需要关心在 t2 表中是否存在,而不需要关心有多少条记录与之匹配,最终结果集只保留 t1 的记录 * 半连接只是执行子查询的一种方式,MySQL 并没有提供面向用户的半连接语法 @@ -3720,7 +3712,7 @@ MyISAM 和 InnoDB 的区别? #### 基本介绍 -MySQL 官方对索引的定义为:索引(index)是帮助 MySQL 高效获取数据的一种数据结构,**本质是排好序的快速查找数据结构。**在表数据之外,数据库系统还维护着满足特定查找算法的数据结构,这些数据结构以某种方式指向数据, 这样就可以在这些数据结构上实现高级查找算法,这种数据结构就是索引。 +MySQL 官方对索引的定义为:索引(index)是帮助 MySQL 高效获取数据的一种数据结构,**本质是排好序的快速查找数据结构。**在表数据之外,数据库系统还维护着满足特定查找算法的数据结构,这些数据结构以某种方式指向数据, 这样就可以在这些数据结构上实现高级查找算法,这种数据结构就是索引 **索引是在存储引擎层实现的**,所以并没有统一的索引标准,即不同存储引擎的索引的工作方式并不一样 @@ -3772,7 +3764,7 @@ MySQL 官方对索引的定义为:索引(index)是帮助 MySQL 高效获 | R-tree | 不支持 | 支持 | 不支持 | | Full-text | 5.6 版本之后支持 | 支持 | 不支持 | -联合索引图示:根据身高年龄建立的组合索引(height,age) +联合索引图示:根据身高年龄建立的组合索引(height、age) ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-组合索引图.png) @@ -4260,7 +4252,7 @@ B+ 树为了保持索引的有序性,在插入新值的时候需要做相应 * 需要存储引擎将索引中的数据与条件进行判断(所以**条件列必须都在同一个索引中**),所以优化是基于存储引擎的,只有特定引擎可以使用,适用于 InnoDB 和 MyISAM * 存储引擎没有调用跨存储引擎的能力,跨存储引擎的功能有存储过程、触发器、视图,所以调用这些功能的不可以进行索引下推优化 -* 对于 InnoDB 引擎只适用于二级索引,InnoDB 的聚簇索引会将整行数据读到缓冲区,不再需要去回表查询了,索引下推的目的减少回表的 IO 次数也就失去了意义 +* 对于 InnoDB 引擎只适用于二级索引,InnoDB 的聚簇索引会将整行数据读到缓冲区,不再需要去回表查询了 工作过程:用户表 user,(name, age) 是联合索引 @@ -4438,8 +4430,8 @@ INSERT INTO t VALUES('2017-4-1',1),('2018-4-1',1);-- 这两行记录分别落在 分区表的特点: -* 一个是第一次访问的时候需要访问所有分区 -* 在 Server 层认为这是同一张表,因此所有分区共用同一个 MDL 锁 +* 第一次访问的时候需要访问所有分区 +* 在 Server 层认为这是同一张表,因此**所有分区共用同一个 MDL 锁** * 在引擎层认为这是不同的表,因此 MDL 锁之后的执行过程,会根据分区表规则,只访问需要的分区 @@ -4452,14 +4444,14 @@ INSERT INTO t VALUES('2017-4-1',1),('2018-4-1',1);-- 这两行记录分别落在 分区表的优点: -* 对业务透明,相对于用户分表来说,使用分区表的业务代码更简洁。 +* 对业务透明,相对于用户分表来说,使用分区表的业务代码更简洁 * 分区表可以很方便的清理历史数据。按照时间分区的分区表,就可以直接通过 `alter table t drop partition` 这个语法直接删除分区文件,从而删掉过期的历史数据,与使用 drop 语句删除数据相比,优势是速度快、对系统影响小 使用分区表,不建议创建太多的分区,注意事项: * 分区并不是越细越好,单表或者单分区的数据一千万行,只要没有特别大的索引,对于现在的硬件能力来说都已经是小表 -* 分区不要提前预留太多,在使用之前预先创建即可。比如是按月分区,每年年底时再把下一年度的 12 个新分区创建上即可,并且对于没有数据的历史分区,要及时的 drop 掉。 +* 分区不要提前预留太多,在使用之前预先创建即可。比如是按月分区,每年年底时再把下一年度的 12 个新分区创建上即可,并且对于没有数据的历史分区,要及时的 drop 掉 @@ -4591,8 +4583,6 @@ select v from ht where k>= M order by t_modified desc limit 100; #### 执行频率 -随着生产数据量的急剧增长,很多 SQL 语句逐渐显露出性能问题,对生产的影响也越来越大,此时有问题的 SQL 语句就成为整个系统性能的瓶颈,因此必须要进行优化 - MySQL 客户端连接成功后,查询服务器状态信息: ```mysql @@ -4724,12 +4714,10 @@ EXPLAIN SELECT * FROM table_1 WHERE id = 1; MySQL **执行计划的局限**: * 只是计划,不是执行 SQL 语句,可以随着底层优化器输入的更改而更改 -* EXPLAIN 不会告诉显示关于触发器、存储过程的信息对查询的影响情况 -* EXPLAIN 不考虑各种 Cache +* EXPLAIN 不会告诉显示关于触发器、存储过程的信息对查询的影响情况, 不考虑各种 Cache * EXPLAIN 不能显示 MySQL 在执行查询时的动态,因为执行计划在执行**查询之前生成** -* EXPALIN 部分统计信息是估算的,并非精确值 * EXPALIN 只能解释 SELECT 操作,其他操作要重写为 SELECT 后查看执行计划 -* EXPLAIN PLAN 显示的是在解释语句时数据库将如何运行 SQL 语句,由于执行环境和 EXPLAIN PLAN 环境的不同,此计划可能与 SQL 语句**实际的执行计划不同** +* EXPLAIN PLAN 显示的是在解释语句时数据库将如何运行 SQL 语句,由于执行环境和 EXPLAIN PLAN 环境的不同,此计划可能与 SQL 语句**实际的执行计划不同**,部分统计信息是估算的,并非精确值 SHOW WARINGS:在使用 EXPALIN 命令后执行该语句,可以查询与执行计划相关的拓展信息,展示出 Level、Code、Message 三个字段,当 Code 为 1003 时,Message 字段展示的信息类似于将查询语句重写后的信息,但是不是等价,不能执行复制过来运行 @@ -4869,7 +4857,7 @@ key_len: * Using where:搜索的数据需要在 Server 层判断,无法使用索引下推 * Using join buffer:连接查询被驱动表无法利用索引,需要连接缓冲区来存储中间结果 * Using filesort:无法利用索引完成排序(优化方向),需要对数据使用外部排序算法,将取得的数据在内存或磁盘中进行排序 -* Using temporary:表示 MySQL 需要使用临时表来存储结果集,常见于排序、去重、分组等场景 +* Using temporary:表示 MySQL 需要使用临时表来存储结果集,常见于**排序、去重(UNION)、分组**等场景 * Select tables optimized away:说明仅通过使用索引,优化器可能仅从聚合函数结果中返回一行 * No tables used:Query 语句中使用 from dual 或不含任何 from 子句 @@ -5256,7 +5244,7 @@ MySQL 不同的自增 id 在达到上限后的表现不同: ```c++ do { - new_id = thread_id_counter++; + new_id = thread_id_counter++; } while (!thread_ids.insert_unique(new_id).second); ``` @@ -5400,7 +5388,7 @@ CREATE TABLE `emp` ( PRIMARY KEY (`id`) ) ENGINE=INNODB DEFAULT CHARSET=utf8mb4; INSERT INTO `emp` (`id`, `name`, `age`, `salary`) VALUES('1','Tom','25','2300');-- ... -CREATE INDEX idx_emp_age_salary ON emp(age,salary); +CREATE INDEX idx_emp_age_salary ON emp(age, salary); ``` * 第一种是通过对返回数据进行排序,所有不通过索引直接返回结果的排序都叫 FileSort 排序,会在内存中重新排序 @@ -5437,7 +5425,7 @@ CREATE INDEX idx_emp_age_salary ON emp(age,salary); 内存临时表,MySQL 有两种 Filesort 排序算法: -* rowid 排序:首先根据条件(回表)取出排序字段和信息,然后在**排序区 sort buffer(Server 层)**中排序,如果 sort buffer 不够,则在临时表 temporary table 中存储排序结果。完成排序后再根据行指针**回表读取记录**,该操作可能会导致大量随机 I/O 操作 +* rowid 排序:首先根据条件取出排序字段和信息,然后在**排序区 sort buffer(Server 层)**中排序,如果 sort buffer 不够,则在临时表 temporary table 中存储排序结果。完成排序后再根据行指针**回表读取记录**,该操作可能会导致大量随机 I/O 操作 说明:对于临时内存表,回表过程只是简单地根据数据行的位置,直接访问内存得到数据,不会导致多访问磁盘,优先选择该方式 @@ -5490,7 +5478,7 @@ GROUP BY 也会进行排序操作,与 ORDER BY 相比,GROUP BY 主要只是 * 创建索引:索引本身有序,不需要临时表,也不需要再额外排序 ```mysql - CREATE INDEX idx_emp_age_salary ON emp(age,salary); + CREATE INDEX idx_emp_age_salary ON emp(age, salary); ``` ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL GROUP BY排序3.png) @@ -5566,7 +5554,7 @@ MySQL 4.1 版本之后,开始支持 SQL 的子查询 ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL嵌套查询2.png) - 连接查询之所以效率更高 ,是因为不需要在内存中创建临时表来完成逻辑上需要两个步骤的查询工作 + 连接查询之所以效率更高 ,是因为**不需要在内存中创建临时表**来完成逻辑上需要两个步骤的查询工作 @@ -5765,7 +5753,7 @@ Flush 链表是一个用来**存储脏页**的链表,对于已经修改过的 * 从 Flush 链表中刷新一部分页面到磁盘: * **后台线程定时**从 Flush 链表刷脏,根据系统的繁忙程度来决定刷新速率,这种方式称为 BUF_FLUSH_LIST - * 线程刷脏的比较慢,导致用户线程加载一个新的数据页时发现没有空闲缓冲页,此时会尝试从 LRU 链表尾部寻找未修改的缓冲页直接释放,如果没有就将 LRU 链表尾部的一个脏页**同步刷新**到磁盘,速度较慢,这种方式称为 BUF_FLUSH_SINGLE_PAGE + * 线程刷脏的比较慢,导致用户线程加载一个新的数据页时发现没有空闲缓冲页,此时会尝试从 LRU 链表尾部寻找缓冲页直接释放,如果该页面是已经修改过的脏页就**同步刷新**到磁盘,速度较慢,这种方式称为 BUF_FLUSH_SINGLE_PAGE * 从 LRU 链表的冷数据中刷新一部分页面到磁盘,即:BUF_FLUSH_LRU * 后台线程会定时从 LRU 链表的尾部开始扫描一些页面,扫描的页面数量可以通过系统变量 `innodb_lru_scan_depth` 指定,如果在 LRU 链表中发现脏页,则把它们刷新到磁盘,这种方式称为 BUF_FLUSH_LRU * 控制块里会存储该缓冲页是否被修改的信息,所以可以很容易的获取到某个缓冲页是否是脏页 @@ -6328,7 +6316,7 @@ InnoDB 存储引擎支持事务,所以加锁分析是基于该存储引擎 * Read Committed 级别,增删改操作会加写锁(行锁),读操作不加锁 - 在 Server 层过滤条件时发现不满足的记录会调用 unlock_row 方法释放该记录的行锁,保证最后只有满足条件的记录加锁,但是扫表过程中每条记录的**加锁操作不能省略**。所以对数据量很大的表做批量修改时,如果无法使用相应的索引(全表扫描),在Server 过滤数据时就会特别慢,出现虽然没有修改某些行的数据,但是还是被锁住了的现象(锁表),这种情况同样适用于 RR + 在 Server 层过滤条件时发现不满足的记录会调用 unlock_row 方法释放该记录的行锁,保证最后只有满足条件的记录加锁,但是扫表过程中每条记录的**加锁操作不能省略**。所以对数据量很大的表做批量修改时,如果无法使用相应的索引(全表扫描),在 Server 过滤数据时就会特别慢,出现虽然没有修改某些行的数据,但是还是被锁住了的现象(锁表),这种情况同样适用于 RR * Repeatable Read 级别,增删改操作会加写锁,读操作不加锁。因为读写锁不兼容,**加了读锁后其他事务就无法修改数据**,影响了并发性能,为了保证隔离性和并发性,MySQL 通过 MVCC 解决了读写冲突。RR 级别下的锁有很多种,锁机制章节详解 @@ -6444,7 +6432,7 @@ roll_pointer 是一个指针,**指向记录对应的 undo log 日志**,一 在对一条记录修改前会**将记录的隐藏列 trx_id 和 roll_pointer 的旧值记录到 undo log 对应的属性中**,这样当前记录的 roll_pointer 指向当前 undo log 记录,当前 undo log 记录的 roll_pointer 指向旧的 undo log 记录,**形成一个版本链** -UPDATE、DELETE 操作产生的 undo 日志可能会用于其他事务的 MVCC 操作,所以不能立即删除 +UPDATE、DELETE 操作产生的 undo 日志会用于其他事务的 MVCC 操作,所以不能立即删除,INSERT 可以删除的原因是 MVCC 是对现有数据的快照 @@ -6611,7 +6599,7 @@ Read View 几个属性: creator 创建一个 Read View,进行可见性算法分析:(解决了读未提交) * db_trx_id == creator_trx_id:表示这个数据就是当前事务自己生成的,自己生成的数据自己肯定能看见,所以此数据对 creator 是可见的 -* db_trx_id < min_trx_id:该版本对应的事务 ID 小于 Read view 中的最小活跃事务 ID,则这个事务在当前事务之前就已经被提交了,对 creator 可见(因为比已提交的最大事务 ID 小的并不一定已经提交,所以应该先判断是否在活跃事务列表) +* db_trx_id < min_trx_id:该版本对应的事务 ID 小于 Read view 中的最小活跃事务 ID,则这个事务在当前事务之前就已经被提交了,对 creator 可见(因为比已提交的最大事务 ID 小的并不一定已经提交,所以应该判断是否在活跃事务列表) * db_trx_id>= max_trx_id:该版本对应的事务 ID 大于 Read view 中当前系统的最大事务 ID,则说明该数据是在当前 Read view 创建之后才产生的,对 creator 不可见 * min_trx_id<= db_trx_id < max_trx_id:判断 db_trx_id 是否在活跃事务列表 m_ids 中 @@ -6727,7 +6715,7 @@ Buffer Pool 的使用提高了读写数据的效率,但是如果 MySQL 宕机 * redo log **记录数据页的物理修改**,而不是某一行或某几行的修改,用来恢复提交后的数据页,只能**恢复到最后一次提交**的位置 * redo log 采用的是 WAL(Write-ahead logging,**预写式日志**),所有修改要先写入日志,再更新到磁盘,保证了数据不会因 MySQL 宕机而丢失,从而满足了持久性要求 -* 简单的 redo log 是纯粹的物理日志,负责的 redo log 会存在物理日志和逻辑日志 +* 简单的 redo log 是纯粹的物理日志,复杂的 redo log 会存在物理日志和逻辑日志 工作过程:MySQL 发生了宕机,InnoDB 会判断一个数据页在崩溃恢复时丢失了更新,就会将它读到内存,然后根据 redo log 内容更新内存,更新完成后,内存页变成脏页,然后进行刷脏 @@ -6811,7 +6799,7 @@ lsn (log sequence number) 代表已经写入的 redo 日志量、flushed_to_disk MTR 的执行过程中修改过的页对应的控制块会加到 Buffer Pool 的 flush 链表中,链表中脏页是按照第一次修改的时间进行排序的(头插),控制块中有两个指针用来记录脏页被修改的时间: -* oldest_modification:第一次修改 Buffer Pool 中某个缓冲页时,将修改该页的 MTR **开始时**对应的 lsn 值写入这个属性,所以链表页是以该值进行排序的 +* oldest_modification:第一次修改 Buffer Pool 中某个缓冲页时,将修改该页的 MTR **开始时**对应的 lsn 值写入这个属性 * newest_modification:每次修改页面,都将 MTR 结束时全局的 lsn 值写入这个属性,所以该值是该页面最后一次修改后的 lsn 值 全局变量 checkpoint_lsn 表示**当前系统可以被覆盖的 redo 日志总量**,当 redo 日志对应的脏页已经被刷新到磁盘后,该文件空间就可以被覆盖重用,此时执行一次 checkpoint 来更新 checkpoint_lsn 的值存入管理信息(刷脏和执行一次 checkpoint 并不是同一个线程),该值的增量就代表磁盘文件中当前位置向后可以被覆盖的文件的量,所以该值是一直增大的 @@ -6844,10 +6832,8 @@ SHOW ENGINE INNODB STATUS\G 恢复的过程:按照 redo log 依次执行恢复数据,优化方式 -* 使用哈希表:根据 redo log 的 space ID 和 page number 属性计算出哈希值,将对同一页面的修改放入同一个槽里,可以一次性完成对某页的恢复,**避免了随机 IO** -* 跳过已经刷新到磁盘中的页面:数据页的 File Header 中的 FILE_PAGE_LSN 属性(类似 newest_modification)表示最近一次修改页面时的 lsn 值,如果在 checkpoint 后,数据页被刷新到磁盘中,那么该页 lsn 属性肯定大于 checkpoint_lsn - -总结:先写 redo buffer,在写 change buffer,先刷 redo log,再刷脏,在删除完成刷脏 redo log +* 使用哈希表:根据 redo log 的 space id 和 page number 属性计算出哈希值,将对同一页面的修改放入同一个槽里,可以一次性完成对某页的恢复,**避免了随机 IO** +* 跳过已经刷新到磁盘中的页面:数据页的 File Header 中的 FILE_PAGE_LSN 属性(类似 newest_modification)表示最近一次修改页面时的 lsn 值,数据页被刷新到磁盘中,那么该页 lsn 属性肯定大于 checkpoint_lsn @@ -6885,7 +6871,7 @@ binlog 为什么不支持奔溃恢复? 更新一条记录的过程:写之前一定先读 -* 在 B+ 树中定位到该记录(这个过程也被称作加锁读),如果该记录所在的页面不在 Buffer Pool 里,先将其加载进内存 +* 在 B+ 树中定位到该记录,如果该记录所在的页面不在 Buffer Pool 里,先将其加载进内存 * 首先更新该记录对应的聚簇索引,更新聚簇索引记录时: * 更新记录前向 undo 页面写 undo 日志,由于这是更改页面,所以需要记录一下相应的 redo 日志 @@ -6945,12 +6931,12 @@ redo log 和 binlog 都可以用于表示事务的提交状态,而**两阶段 系统崩溃前没有提交的事务的 redo log 可能已经刷盘(定时线程或者 checkpoint),怎么处理崩溃恢复? -工作流程:通过 undo log 在服务器重启时将未提交的事务回滚掉。首先定位到 128 个回滚段遍历 slot,获取 undo 链表首节点页面的 undo segement header 中的 TRX_UNDO_STATE 属性,表示当前链表的事务属性,事务状态是活跃的就全部回滚,如果是 PREPARE 状态,就需要根据 binlog 的状态进行判断: +工作流程:获取 undo 链表首节点页面的 undo segement header 中的 TRX_UNDO_STATE 属性,表示当前链表的事务属性,事务状态是活跃(未提交)的就全部回滚,如果是 PREPARE 状态,就需要根据 binlog 的状态进行判断: * 如果在时刻 A 发生了崩溃(crash),由于此时 binlog 还没完成,所以需要进行回滚 * 如果在时刻 B 发生了崩溃,redo log 和 binlog 有一个共**同的数据字段叫 XID**,崩溃恢复的时候,会按顺序扫描 redo log: * 如果 redo log 里面的事务是完整的,也就是已经有了 commit 标识,说明 binlog 也已经记录完整,直接从 redo log 恢复数据 - * 如果 redo log 里面的事务只有 prepare,就根据 XID 去 binlog 中判断对应的事务是否存在并完整,如果完整可以恢复数据,提交事务 + * 如果 redo log 里面的事务只有 prepare,就根据 XID 去 binlog 中判断对应的事务是否存在并完整,如果完整可以恢复数据 判断一个事务的 binlog 是否完整的方法: @@ -7289,7 +7275,7 @@ InnoDB 与 MyISAM 的最大不同有两点:一是支持事务;二是采用 行级锁,也称为记录锁(Record Lock),InnoDB 实现了以下两种类型的行锁: - 共享锁 (S):又称为读锁,简称 S 锁,多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改 -- 排他锁 (X):又称为写锁,简称 X 锁,不能与其他锁并存,如一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁,包括共享锁和排他锁,只有获取排他锁的事务是可以对数据读取和修改 +- 排他锁 (X):又称为写锁,简称 X 锁,不能与其他锁并存,获取排他锁的事务是可以对数据读取和修改 RR 隔离界别下,对于 UPDATE、DELETE 和 INSERT 语句,InnoDB 会**自动给涉及数据集加排他锁**(行锁),在 commit 时自动释放;对于普通 SELECT 语句,不会加任何锁(只是针对 InnoDB 层来说的,因为在 Server 层会**加 MDL 读锁**),通过 MVCC 防止并发冲突 @@ -7430,7 +7416,7 @@ InnoDB 加锁的基本单位是 next-key lock,该锁是行锁和 gap lock 的 * 范围查询无论是否是唯一索引,都需要访问到不满足条件的第一个值为止 * 对于联合索引且是唯一索引,如果 where 条件只包括联合索引的一部分,那么会加间隙锁 -间隙锁优点:RR 级别下间隙锁可以解决事务的一部分的**幻读问题**,通过对间隙加锁,可以防止读取过程中数据条目发生变化。一部分的意思是不会对全部间隙加锁,只能加锁一部分的间隙。 +间隙锁优点:RR 级别下间隙锁可以解决事务的一部分的**幻读问题**,通过对间隙加锁,可以防止读取过程中数据条目发生变化。一部分的意思是不会对全部间隙加锁,只能加锁一部分的间隙 间隙锁危害: @@ -7509,7 +7495,7 @@ InnoDB 为了支持多粒度的加锁,允许行锁和表锁同时存在,支 * 0:全部采用 AUTO_INC 锁 * 1:全部采用轻量级锁 -* 2:混合使用,在插入记录的数量确定是采用轻量级锁,不确定时采用 AUTO_INC 锁 +* 2:混合使用,在插入记录的数量确定时采用轻量级锁,不确定时采用 AUTO_INC 锁 @@ -8293,7 +8279,7 @@ GTID=source_id:transaction_id * source_id:是一个实例第一次启动时自动生成的,是一个全局唯一的值 * transaction_id:初始值是 1,每次提交事务的时候分配给这个事务,并加 1,是连续的(区分事务 ID,事务 ID 是在执行时生成) -启动 MySQL 实例时,加上参数 `gtid_mode=on` 和 `enforce_gtid_consistency=on` 就可以启动 GTID 模式,每个事务都会和一个 GTID 一一对应,每个 MySQL 实例都维护了一个 **GTID 集合**,用来对应当前实例执行过的所有事务 +启动 MySQL 实例时,加上参数 `gtid_mode=on` 和 `enforce_gtid_consistency=on` 就可以启动 GTID 模式,每个事务都会和一个 GTID 一一对应,每个 MySQL 实例都维护了一个 GTID 集合,用来存储当前实例**执行过的所有事务** GTID 有两种生成方式,使用哪种方式取决于 session 变量 gtid_next: @@ -8361,7 +8347,7 @@ GTID 有两种生成方式,使用哪种方式取决于 session 变量 gtid_nex ### 日志分类 -在任何一种数据库中,都会有各种各样的日志,记录着数据库工作的过程,可以帮助数据库管理员追踪数据库曾经发生过的各种事件。 +在任何一种数据库中,都会有各种各样的日志,记录着数据库工作的过程,可以帮助数据库管理员追踪数据库曾经发生过的各种事件 MySQL日志主要包括六种: @@ -8562,7 +8548,7 @@ mysqlbinlog log-file; #### 数据恢复 -误删库或者表时,需要根据 binlog 进行数据恢复, +误删库或者表时,需要根据 binlog 进行数据恢复 一般情况下数据库有定时的全量备份,假如每天 0 点定时备份,12 点误删了库,恢复流程: @@ -8615,7 +8601,7 @@ SELECT * FROM tb_book WHERE id < 8 ### 慢日志 -慢查询日志记录所有执行时间超过 long_query_time 并且扫描记录数不小于 min_examined_row_limit 的所有的 SQL 语句的日志。long_query_time 默认为 10 秒,最小为 0, 精度到微秒 +慢查询日志记录所有执行时间超过 long_query_time 并且扫描记录数不小于 min_examined_row_limit 的所有的 SQL 语句的日志long_query_time 默认为 10 秒,最小为 0, 精度到微秒 慢查询日志默认是关闭的,可以通过两个参数来控制慢查询日志,配置文件 `/etc/mysql/my.cnf`: @@ -8751,9 +8737,9 @@ long_query_time=10 ### 概述 -NoSQL(Not-Only SQL):泛指非关系型的数据库,作为关系型数据库的补充。 +NoSQL(Not-Only SQL):泛指非关系型的数据库,作为关系型数据库的补充 -MySQL 支持 ACID 特性,保证可靠性和持久性,读取性能不高,因此需要缓存的来减缓数据库的访问压力。 +MySQL 支持 ACID 特性,保证可靠性和持久性,读取性能不高,因此需要缓存的来减缓数据库的访问压力 作用:应对基于海量用户和海量数据前提下的数据处理问题 @@ -8796,17 +8782,6 @@ Redis (REmote DIctionary Server) :用 C 语言开发的一个开源的高性 * 有序集合类型:zset/sorted_set(TreeSet) * 支持持久化,可以进行数据灾难恢复 -应用: - -* 为热点数据加速查询(主要场景),如热点商品、热点新闻、热点资讯、推广类等高访问量信息等 - -* 即时信息查询,如排行榜、网站访问统计、公交到站信息、在线人数(聊天室、网站)、设备信号等 - -* 时效性信息控制,如验证码控制、投票控制等 - -* 分布式数据共享,如分布式集群架构中的 session 分离 -* 消息队列 - *** @@ -9101,7 +9076,7 @@ redis[1]> #### key space -Redis 是一个键值对(key-value pair)数据库服务器,每个数据库都由一个 redisDb 结构表示,redisDb.dict 字典中保存了数据库的所有键值对,将这个字典称为键空间(key space) +Redis 是一个键值对(key-value pair)数据库服务器,每个数据库都由一个 redisDb 结构表示,redisDb.dict **字典中保存了数据库的所有键值对**,将这个字典称为键空间(key space) ```c typedef struct redisDB { @@ -9278,7 +9253,7 @@ Redis 通过过期字典可以检查一个给定键是否过期: * AOF 重写,会对数据库中的键进行检查,忽略已经过期的键 * 复制:当服务器运行在复制模式下时,从服务器的过期键删除动作由主服务器控制 * 主服务器在删除一个过期键之后,会显式地向所有从服务器发送一个 DEL 命令,告知从服务器删除这个过期键 - * 从服务器在执行客户端发送的读命令时,即使碰到过期键也不会将过期键删除,会当作未过期键处理,只有在接到主服务器发来的 DEL 命令之后,才会删除过期键 + * 从服务器在执行客户端发送的读命令时,即使碰到过期键也不会将过期键删除,会当作未过期键处理,只有在接到主服务器发来的 DEL 命令之后,才会删除过期键(数据不一致) @@ -9352,8 +9327,6 @@ Redis 采用惰性删除和定期删除策略的结合使用 * 如果删除操作执行得太频繁,或者执行时间太长,就会退化成定时删除策略,将 CPU 时间过多地消耗在删除过期键上 * 如果删除操作执行得太少,或者执行时间太短,定期删除策略又会和惰性删除策略一样,出现浪费内存的情况 -所以采用定期删除策略的话,服务器必须根据情况合理地设置删除操作的执行时长和执行频率 - 定期删除是**周期性轮询 Redis 库中的时效性**数据,从过期字典中随机抽取一部分键检查,利用过期数据占比的方式控制删除频度 - Redis 启动服务器初始化时,读取配置 server.hz 的值,默认为 10,执行指令 info server 可以查看,每秒钟执行 server.hz 次 `serverCron() → activeExpireCycle()` @@ -9516,7 +9489,7 @@ SORT key ALPHA #对key中字母排序,按照字典序 对于 `SORT key [ASC/DESC]` 函数: * 在执行升序排序时,排序算法使用的对比函数产生升序对比结果 -* 在执行降序排序时,排序算法所使用的对比函数产生降序对比结果 +* 在执行降序排序时,排序算法使用的对比函数产生降序对比结果 @@ -9767,7 +9740,7 @@ Redis 单线程也能高效的原因: ##### 多路复用 -Redis 的 I/O 多路复用程序的所有功能都是通过包装常见的 select 、epoll、 evport 和 kqueue 这些函数库来实现的,Redis在 I/O 多路复用程序的实现源码中用 #include 宏定义了相应的规则,编译时自动选择系统中**性能最高的多路复用函数**来作为底层实现 +Redis 的 I/O 多路复用程序的所有功能都是通过包装常见的 select 、epoll、 evport 和 kqueue 这些函数库来实现的,Redis 在 I/O 多路复用程序的实现源码中用 #include 宏定义了相应的规则,编译时自动选择系统中**性能最高的多路复用函数**来作为底层实现 I/O 多路复用程序监听多个套接字的 AE_READABLE 事件和 AE_WRITABLE 事件,这两类事件和套接字操作之间的对应关系如下: @@ -9846,18 +9819,6 @@ Redis 的时间事件分为以下两类: 服务器中同时存在文件事件和时间事件两种事件类型,调度伪代码: ```python -# Redis 服务器的主函数的伪代码 -def main(): - # 初始化服务器 - init_server() - - # 循环处理事件,直到服务器关闭 - while server_is_not_shutdown(): - aeProcessEvents() - - # 服务器关闭 - clean_server() - # 事件调度伪代码 def aeProcessEvents(): # 获取到达时间离当前时间最接近的时间事件 @@ -10001,7 +9962,7 @@ typedef struct redisClient { 客户端状态包括两类属性 * 一类是比较通用的属性,这些属性很少与特定功能相关,无论客户端执行的是什么工作,都要用到这些属性 -* 另一类是和特定功能相关的属性,比如操作数据库时用到的 db 属性和 dict id属性,执行事务时用到的 mstate 属性,以及执行 WATCH 命令时用到的 watched_keys 属性等,代码中没有列出 +* 另一类是和特定功能相关的属性,比如操作数据库时用到的 db 属性和 dict id 属性,执行事务时用到的 mstate 属性,以及执行 WATCH 命令时用到的 watched_keys 属性等,代码中没有列出 @@ -10160,7 +10121,7 @@ obuf_soft_limit_reached_time 属性记录了**输出缓冲区第一次到达软 服务器使用不同的方式来创建和关闭不同类型的客户端 -如果客户端是通过网络连接与服务器进行连接的普通客户端,那么在客户端使用 connect 函数连接到服务器时,服务器就会调用连接事件处理器为客户端创建相应的客户端状态,并将这个新的客户端状态添加到服务器状态结构 clients 链表的末尾 +如果客户端是通过网络连接与服务器进行连接的普通客户端,那么在客户端使用 connect 函数连接到服务器时,服务器就会调用连接应答处理器为客户端创建相应的客户端状态,并将这个新的客户端状态添加到服务器状态结构 clients 链表的末尾 ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-服务器clients链表.png) @@ -10521,7 +10482,7 @@ databasesCron 函数会对服务器中的一部分数据库进行检查,删除 ##### 持久状态 -服务器状态中记录执行 BGSAVE 命令和 BGREWRITEAOF 命令的子进程的 ID, +服务器状态中记录执行 BGSAVE 命令和 BGREWRITEAOF 命令的子进程的 ID ```c struct redisServer { @@ -10626,7 +10587,7 @@ struct redisServer { initServer 还进行了非常重要的设置操作: * 为服务器设置进程信号处理器 -* 创建共享对象,包含 OK、ERR、整数 1 到 10000 的字符串对象等 +* 创建共享对象,包含 OK、ERR、**整数 1 到 10000 的字符串对象**等 * **打开服务器的监听端口** * **为 serverCron 函数创建时间事件**, 等待服务器正式运行时执行 serverCron 函数 * 如果 AOF 持久化功能已经打开,那么打开现有的 AOF 文件,如果 AOF 文件不存在,那么创建并打开一个新的 AOF 文件 ,为 AOF 写入做好准备 @@ -10792,7 +10753,7 @@ struct sdshdr { }; ``` -SDS 遵循 C 字符串**以空字符结尾**的惯例, 保存空字符的 1 字节不计算在 len 属性,SDS 会自动为空字符分配额外的 1 字节空间和添加空字符到字符串末尾,所以空字符对于 SDS 的使用者来说是完全透明的 +SDS 遵循 C 字符串**以空字符结尾**的惯例,保存空字符的 1 字节不计算在 len 属性,SDS 会自动为空字符分配额外的 1 字节空间和添加空字符到字符串末尾,所以空字符对于 SDS 的使用者来说是完全透明的 ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-SDS底层结构.png) @@ -10822,7 +10783,7 @@ SDS 遵循 C 字符串**以空字符结尾**的惯例, 保存空字符的 1 二进制安全: * C 字符串中的字符必须符合某种编码(比如 ASCII)方式,除了字符串末尾以外其他位置不能包含空字符,否则会被误认为是字符串的结尾,所以只能保存文本数据 -* SDS 的 API 都是二进制安全的,使用字节数组 buf 保存一系列的二进制数据,使用 len 属性来判断数据的结尾,所以可以保存图片、视频、压缩文件等二进制数据 +* SDS 的 API 都是二进制安全的,使用字节数组 buf 保存一系列的二进制数据,**使用 len 属性来判断数据的结尾**,所以可以保存图片、视频、压缩文件等二进制数据 兼容 C 字符串的函数:SDS 会在为 buf 数组分配空间时多分配一个字节来保存空字符,所以可以重用一部分 C 字符串函数库的函数 @@ -11067,7 +11028,7 @@ load_factor = ht[0].used / ht[0].size * 如果执行的是扩展操作,ht[1] 的大小为第一个大于等于 $ht[0].used * 2$ 的 2ドル^n$ * 如果执行的是收缩操作,ht[1] 的大小为第一个大于等于 $ht[0].used$ 的 2ドル^n$ * 将保存在 ht[0] 中所有的键值对重新计算哈希值和索引值,迁移到 ht[1] 上 -* 当 ht[0] 包含的所有键值对都迁移到了 ht[1] 之后(ht[0]变为空表), 释放 ht[0],将 ht[1] 设置为 ht[0],并在 ht[1] 创建一个新的空白哈希表,为下一次 rehash 做准备 +* 当 ht[0] 包含的所有键值对都迁移到了 ht[1] 之后(ht[0] 变为空表),释放 ht[0],将 ht[1] 设置为 ht[0],并在 ht[1] 创建一个新的空白哈希表,为下一次 rehash 做准备 如果哈希表里保存的键值对数量很少,rehash 就可以在瞬间完成,但是如果哈希表里数据很多,那么要一次性将这些键值对全部 rehash 到 ht[1] 需要大量计算,可能会导致服务器在一段时间内停止服务 @@ -11075,7 +11036,7 @@ Redis 对 rehash 做了优化,使 rehash 的动作并不是一次性、集中 * 为 ht[1] 分配空间,此时字典同时持有 ht[0] 和 ht[1] 两个哈希表 * 在字典中维护了一个索引计数器变量 rehashidx,并将变量的值设为 0,表示 rehash 正式开始 -* 在 rehash 进行期间,每次对字典执行添加、删除、查找或者更新操作时,程序除了执行指定的操作以外,还会顺带将 ht[0] 哈希表在 rehashidx 索引上的所有键值对 rehash 到 ht[1],rehash 完成之后**将 rehashidx 属性的值增一** +* 在 rehash 进行期间,每次对字典执行增删改查操作时,程序除了执行指定的操作以外,还会顺带将 ht[0] 哈希表在 rehashidx 索引上的所有键值对 rehash 到 ht[1],rehash 完成之后**将 rehashidx 属性的值增一** * 随着字典操作的不断执行,最终在某个时间点上 ht[0] 的所有键值对都被 rehash 至 ht[1],这时程序将 rehashidx 属性的值设为 -1,表示 rehash 操作已完成 渐进式 rehash 采用**分而治之**的方式,将 rehash 键值对所需的计算工作均摊到对字典的每个添加、删除、查找和更新操作上,从而避免了集中式 rehash 带来的庞大计算量 @@ -11149,7 +11110,7 @@ typedef struct zskiplistNode { 层:level 数组包含多个元素,每个元素包含指向其他节点的指针。根据幕次定律(power law,越大的数出现的概率越小)**随机**生成一个介于 1 和 32 之间的值(Redis5 之后最大为 64)作为 level 数组的大小,这个大小就是层的高度,节点的第一层是 level[0] = L1 -前进指针:forward 用于从表头到表尾方向正序(升序)遍历节点,遇到 NULL 停止遍历 +前进指针:forward 用于从表头到表尾方向**正序(升序)遍历节点**,遇到 NULL 停止遍历 跨度:span 用于记录两个节点之间的距离,用来**计算排位(rank)**: @@ -11161,7 +11122,7 @@ typedef struct zskiplistNode { 查找分值为 2.0 的节点,沿途经历的层:经过了两个跨度为 1 的节点,因此可以计算出目标节点在跳跃表中的排位为 2 -后退指针:backward 用于从表尾到表头方向逆序(降序)遍历节点 +后退指针:backward 用于从表尾到表头方向**逆序(降序)遍历节点** 分值:score 属性一个 double 类型的浮点数,跳跃表中的所有节点都**按分值从小到大来排序** @@ -11237,7 +11198,7 @@ encoding 取值为三种:INTSET_ENC_INT16、INTSET_ENC_INT32、INTSET_ENC_INT6 * 节约内存:要让数组可以同时保存 int16、int32、int64 三种类型的值,可以直接使用 int64_t 类型的数组作为整数集合的底层实现,但是会造成内存浪费,整数集合可以确保升级操作只会在有需要的时候进行,尽量节省内存 -整数集合不支持降级操作,一旦对数组进行了升级,编码就会一直保持升级后的状态 +整数集合**不支持降级操作**,一旦对数组进行了升级,编码就会一直保持升级后的状态 @@ -11255,7 +11216,7 @@ encoding 取值为三种:INTSET_ENC_INT16、INTSET_ENC_INT32、INTSET_ENC_INT6 ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-压缩列表底层结构.png) -* zlbytes:uint32_t 类型 4 字节,记录整个压缩列表占用的内存字节数,在对压缩列表进行内存重分或者计算 zlend 的位置时使用 +* zlbytes:uint32_t 类型 4 字节,记录整个压缩列表占用的内存字节数,在对压缩列表进行内存重分配或者计算 zlend 的位置时使用 * zltail:uint32_t 类型 4 字节,记录压缩列表表尾节点距离起始地址有多少字节,通过这个偏移量程序无须遍历整个压缩列表就可以确定表尾节点的地址 * zllen:uint16_t 类型 2 字节,记录了压缩列表包含的节点数量,当该属性的值小于 UINT16_MAX (65535) 时,该值就是压缩列表中节点的数量;当这个值等于 UINT16_MAX 时节点的真实数量需要遍历整个压缩列表才能计算得出 * entryX:列表节点,压缩列表中的各个节点,**节点的长度由节点保存的内容决定** @@ -11277,7 +11238,7 @@ encoding 取值为三种:INTSET_ENC_INT16、INTSET_ENC_INT32、INTSET_ENC_INT6 ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-压缩列表节点.png) -previous_entry_length:以字节为单位记录了压缩列表中前一个节点的长度,程序可以通过指针运算,根据当前节点的起始地址来计算出前一个节点的起始地址,完成从表尾向表头遍历操作 +previous_entry_length:以字节为单位记录了压缩列表中前一个节点的长度,程序可以通过指针运算,根据当前节点的起始地址来计算出前一个节点的起始地址,完成**从表尾向表头遍历**操作 * 如果前一节点的长度小于 254 字节,该属性的长度为 1 字节,前一节点的长度就保存在这一个字节里 * 如果前一节点的长度大于等于 254 字节,该属性的长度为 5 字节,其中第一字节会被设置为 0xFE(十进制 254),之后的四个字节则用于保存前一节点的长度 @@ -12428,7 +12389,7 @@ BGSAVE:bg 是 background,代表后台执行,命令的完成需要两个进 -流程:客户端发出 BGSAVE 指令,Redis 服务器使用 fork 函数创建一个子进程,然后响应后台已经开始执行的信息给客户端。子进程会去执行持久化的操作,持久化过程是先将数据写入到一个临时文件中,持久化操作结束再用这个临时文件**替换**上次持久化的文件 +流程:客户端发出 BGSAVE 指令,Redis 服务器使用 fork 函数创建一个子进程,然后响应后台已经开始执行的信息给客户端。子进程会异步执行持久化的操作,持久化过程是先将数据写入到一个临时文件中,持久化操作结束再用这个临时文件**替换**上次持久化的文件 ```python # 创建子进程 @@ -12463,7 +12424,7 @@ rdbchecksum yes|no * SAVE 命令会被服务器拒绝,服务器禁止 SAVE 和 BGSAVE 命令同时执行是为了避免父进程(服务器进程)和子进程同时执行两个 rdbSave 调用,产生竞争条件 * BGSAVE 命令也会被服务器拒绝,也会产生竞争条件 * BGREWRITEAOF 和 BGSAVE 两个命令不能同时执行 - * 如果 BGSAVE 命令正在执行,那么 BGREWRITEAOF 命令会被延迟到 BGSAVE 命令执行完毕之后执行 + * 如果 BGSAVE 命令正在执行,那么 BGREWRITEAOF 命令会被**延迟**到 BGSAVE 命令执行完毕之后执行 * 如果 BGREWRITEAOF 命令正在执行,那么 BGSAVE 命令会被服务器拒绝 @@ -12764,8 +12725,6 @@ Redis 读取 AOF 文件并还原数据库状态的步骤: ##### 重写策略 -随着命令不断写入 AOF,文件会越来越大,很可能对 Redis 服务器甚至整个宿主计算机造成影响,为了解决这个问题 Redis 引入了 AOF 重写机制压缩文件体积 - AOF 重写:读取服务器当前的数据库状态,**生成新 AOF 文件来替换旧 AOF 文件**,不会对现有的 AOF 文件进行任何读取、分析或者写入操作,而是直接原子替换。新 AOF 文件不会包含任何浪费空间的冗余命令,所以体积通常会比旧 AOF 文件小得多 AOF 重写规则: @@ -12810,7 +12769,7 @@ bgrewriteaof 工作流程: * Redis 服务器执行完一个写命令,会同时将该命令追加到 AOF 缓冲区和 AOF 重写缓冲区(从创建子进程后才开始写入) -* 当子进程完成 AOF 重写工作之后,会向父进程发送一个信号,父进程在接到该信号之后, 会调用一个信号处理函数,该函数执行时会**对服务器进程(父进程)造成阻塞**(影响很小),主要工作: +* 当子进程完成 AOF 重写工作之后,会向父进程发送一个信号,父进程在接到该信号之后, 会调用一个信号处理函数,该函数执行时会**对服务器进程(父进程)造成阻塞**(影响很小,类似 JVM STW),主要工作: * 将 AOF 重写缓冲区中的所有内容写入到新 AOF 文件中, 这时新 AOF 文件所保存的状态将和服务器当前的数据库状态一致 * 对新的 AOF 文件进行改名,**原子地(atomic)覆盖**现有的 AOF 文件,完成新旧两个 AOF 文件的替换 @@ -13201,7 +13160,7 @@ Redis 不支持事务回滚机制(rollback),即使事务队列中的某个 -* 执行错误:命令执行出现错误,例如对字符串进行 incr 操作,食物中正确的命令会被执行,运行错误的命令不会被执行 +* 执行错误:命令执行出现错误,例如对字符串进行 incr 操作,事务中正确的命令会被执行,运行错误的命令不会被执行 @@ -13314,7 +13273,7 @@ Redis 服务器创建并修改 Lua 环境的整个过程: * 创建 redis.pcall 函数的错误报告辅助函数 `_redis_err_handler `,这个函数可以打印出错代码的来源和发生错误的行数 -* 对 Lua环境中的全局环境进行保护,确保传入服务器的脚本不会因忘记使用 local 关键字,而将额外的全局变量添加到 Lua 环境 +* 对 Lua 环境中的全局环境进行保护,确保传入服务器的脚本不会因忘记使用 local 关键字,而将额外的全局变量添加到 Lua 环境 * 将完成修改的 Lua 环境保存到服务器状态的 lua 属性中,等待执行服务器传来的 Lua 脚本 @@ -13413,7 +13372,7 @@ EVAL 命令第二步是将客户端传入的脚本保存到服务器的 lua_scri EVAL 命令第三步是执行脚本函数 -* 将 EVAL 命令中传入的**键名(key name)参数和脚本参数**分别保存到 KEYS 数组和 ARGV 数组,将这两个数组作为**全局变量**传入到 Lua 环境里 +* 将 EVAL 命令中传入的**键名参数和脚本参数**分别保存到 KEYS 数组和 ARGV 数组,将这两个数组作为**全局变量**传入到 Lua 环境 * 为 Lua 环境装载超时处理钩子(hook),这个钩子可以在脚本出现超时运行情况时,让客户端通过 `SCRIPT KILL` 命令停止脚本,或者通过 SHUTDOWN 命令直接关闭服务器 因为 Redis 是单线程的执行命令,当 Lua 脚本阻塞时需要兜底策略,可以中断执行 @@ -13561,7 +13520,7 @@ Redis 分布式锁的基本使用,悲观锁 PEXPIRE lock-key milliseconds ``` - 通过 EXPIRE 设置过期时间缺乏原子性,如果在 SETNX 和 EXPIRE 之间出现异常,锁也无法释放 + 通过 EXPIRE 设置过期时间缺乏原子性,如果在 SETNX 和 EXPIRE 之间出现异常,锁也无法释放 * 在 SET 时指定过期时间,保证原子性 @@ -13710,7 +13669,7 @@ end 主从复制的特点: -* **薪火相传**:一个 slave 可以是下一个 slave 的 master,slave 同样可以接收其他 slave 的连接和同步请求,那么该 slave 作为了链条中下一个的 master, 可以有效减轻 master 的写压力,去中心化降低风险 +* **薪火相传**:一个 slave 可以是下一个 slave 的 master,slave 同样可以接收其他 slave 的连接和同步请求,那么该 slave 作为了链条中下一个的 master,可以有效减轻 master 的写压力,去中心化降低风险 注意:主机挂了,从机还是从机,无法写数据了 @@ -14100,7 +14059,7 @@ min-slaves-to-write 5 min-slaves-max-lag 10 ``` -那么在从服务器的数最少于 5 个,或者 5 个从服务器的延迟(lag)值都大于或等于10 秒时,主服务器将拒绝执行写命令 +那么在从服务器的数少于 5 个,或者 5 个从服务器的延迟(lag)值都大于或等于10 秒时,主服务器将拒绝执行写命令 @@ -14512,7 +14471,7 @@ slave1: ip=l27.0.0.1, port=22222, state=online, offset=22, lag=0 ##### 从服务器 -当 Sentinel 发现主服务器有新的从服务器出现时,会为这个新的从服务器创建相应的实例结构, 还会创建到从服务器的命令连接和订阅连接,所以 Sentinel 对所有的从服务器之间都可以进行命令操作 +当 Sentinel 发现主服务器有新的从服务器出现时,会为这个新的从服务器创建相应的实例结构,还会创建到从服务器的命令连接和订阅连接,所以 Sentinel 对所有的从服务器之间都可以进行命令操作 Sentinel 默认会以每十秒一次的频率,向从服务器发送 INFO 命令: @@ -15103,8 +15062,8 @@ Redis 集群的重新分片操作可以将任意数量已经指派给某个节 Redis 的集群管理软件 redis-trib 负责执行重新分片操作,redis-trib 通过向源节点和目标节点发送命令来进行重新分片操作 -* redis-trib 向目标节点发送 `CLUSTER SETSLOT IMPORTING ` 命令,让目标节点准备好从源节点导入属于槽 slot 的键值对 -* redis-trib 向源节点发送 `CLUSTER SETSLOT MIGRATING ` 命令,让源节点准备好将属于槽 slot 的键值对迁移至目标节点 +* 向目标节点发送 `CLUSTER SETSLOT IMPORTING ` 命令,准备好从源节点导入属于槽 slot 的键值对 +* 向源节点发送 `CLUSTER SETSLOT MIGRATING ` 命令,让源节点准备好将属于槽 slot 的键值对迁移 * redis-trib 向源节点发送 `CLUSTER GETKEYSINSLOT ` 命令,获得最多 count 个属于槽 slot 的键值对的键名 * 对于每个 key,redis-trib 都向源节点发送一个 `MIGRATE 0 maxLevel:则 level== maxLevel,例如 level==20,延迟 2h -定时消息会暂存在名为 SCHEDULE_TOPIC_XXXX 的 Topic 中,并根据 delayTimeLevel 存入特定的 queue,队列的标识 `queueId = delayTimeLevel – 1`,即**一个 queue 只存相同延迟的消息**,保证具有相同发送延迟的消息能够顺序消费。Broker 会调度地消费 SCHEDULE_TOPIC_XXXX,将消息写入真实的 Topic +定时消息会暂存在名为 SCHEDULE_TOPIC_XXXX 的 Topic 中,并根据 delayTimeLevel 存入特定的 queue,队列的标识 `queueId = delayTimeLevel – 1`,即**一个 queue 只存相同延迟的消息**,保证具有相同发送延迟的消息能够顺序消费。Broker 会为每个延迟级别提交一个定时任务,调度地消费 SCHEDULE_TOPIC_XXXX,将消息写入真实的 Topic -注意:定时消息在第一次写入和调度写入真实 Topic 时都会计数,因此发送数量、tps 都会变高。 +注意:定时消息在第一次写入和调度写入真实 Topic 时都会计数,因此发送数量、tps 都会变高 @@ -4786,7 +4786,7 @@ LatencyFaultTolerance 机制是实现消息发送高可用的核心关键所在 ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/RocketMQ-平均队列轮流分配.png) -集群模式下,**queue 都是只允许分配只一个实例**,如果多个实例同时消费一个 queue 的消息,由于拉取哪些消息是 Consumer 主动控制的,会导致同一个消息在不同的实例下被消费多次 +集群模式下,**queue 都是只允许分配一个实例**,如果多个实例同时消费一个 queue 的消息,由于拉取哪些消息是 Consumer 主动控制的,会导致同一个消息在不同的实例下被消费多次 通过增加 Consumer 实例去分摊 queue 的消费,可以起到水平扩展的消费能力的作用。而当有实例下线时,会重新触发负载均衡,这时候原来分配到的 queue 将分配到其他实例上继续消费。但是如果 Consumer 实例的数量比 Message Queue 的总数量还多的话,多出来的 Consumer 实例将无法分到 queue,也就无法消费到消息,也就无法起到分摊负载的作用了,所以需要**控制让 queue 的总数量大于等于 Consumer 的数量** @@ -4802,7 +4802,7 @@ LatencyFaultTolerance 机制是实现消息发送高可用的核心关键所在 Consumer 端实现负载均衡的核心类 **RebalanceImpl** -在 Consumer 实例的启动流程中的会启动 MQClientInstance 实例,完成负载均衡服务线程 RebalanceService 的启动(每隔 20s 执行一次),RebalanceService 线程的 run() 方法最终调用的是 RebalanceImpl 类的 rebalanceByTopic() 方法,该方法是实现 Consumer 端负载均衡的核心。rebalanceByTopic() 方法会根据消费者通信类型为广播模式还是集群模式做不同的逻辑处理。这里主要看下集群模式下的处理流程: +在 Consumer 实例的启动流程中的会启动 MQClientInstance 实例,完成负载均衡服务线程 RebalanceService 的启动(**每隔 20s 执行一次**负载均衡),RebalanceService 线程的 run() 方法最终调用的是 RebalanceImpl 类的 rebalanceByTopic() 方法,该方法是实现 Consumer 端负载均衡的核心。rebalanceByTopic() 方法会根据广播模式还是集群模式做不同的逻辑处理。主要看集群模式: * 从 rebalanceImpl 实例的本地缓存变量 topicSubscribeInfoTable 中,获取该 Topic 主题下的消息消费队列集合 mqSet @@ -4915,7 +4915,7 @@ RocketMQ 会为每个消费组都设置一个 Topic 名称为 `%RETRY%+consumerG * 无序消息(普通、定时、延时、事务消息)的重试,可以通过设置返回状态达到消息重试的结果。无序消息的重试只针对集群消费方式生效,广播方式不提供失败重试特性,即消费失败后,失败消息不再重试,继续消费新的消息 -**无序消息情况下**,因为异常恢复需要一些时间,会为重试队列设置多个重试级别,每个重试级别都有对应的重新投递延时,重试次数越多投递延时就越大。RocketMQ 对于重试消息的处理是先保存至 Topic 名称为 `SCHEDULE_TOPIC_XXXX` 的延迟队列中,后台定时任务按照对应的时间进行 Delay 后重新保存至 `%RETRY%+consumerGroup` 的重试队列中 +**无序消息情况下**,因为异常恢复需要一些时间,会为重试队列设置多个重试级别,每个重试级别都有对应的重新投递延时,重试次数越多投递延时就越大。RocketMQ 对于重试消息的处理是先保存至 Topic 名称为 `SCHEDULE_TOPIC_XXXX` 的延迟队列中,后台定时任务**按照对应的时间进行 Delay 后**重新保存至 `%RETRY%+consumerGroup` 的重试队列中 消息队列 RocketMQ 默认允许每条消息最多重试 16 次,每次重试的间隔时间如下表示: @@ -10211,7 +10211,7 @@ ConsumeRequest 是 ConsumeMessageOrderlyService 的内部类,是一个 Runnabl * 首先获取当前消息主题的发布信息,获取不到去 Namesrv 获取(默认有 TBW102),并将获取的到的路由数据转化为发布数据,**创建 MQ 队列**,客户端实例同样更新订阅数据,创建 MQ 队列,放入负载均衡服务 topicSubscribeInfoTable 中 * 然后从发布数据中选择一个 MQ 队列发送消息 -* Broker 端通过 SendMessageProcessor 对发送的消息进行持久化处理,存储到 CommitLog。将重试次数过多的消息加入死信队列,将延迟消息的主题和队列修改为调度主题和调度队列 ID +* Broker 端通过 SendMessageProcessor 对发送的消息进行持久化处理,存储到 CommitLog。将重试次数过多的消息加入**死信队列**,将延迟消息的主题和队列修改为调度主题和调度队列 ID * Broker 启动 ScheduleMessageService 服务会为每个延迟级别创建一个延迟任务,让延迟消息得到有效的处理,将到达交付时间的消息修改为原始主题的原始 ID 存入 CommitLog,消费者就可以进行消费了 消费流程: @@ -10519,7 +10519,7 @@ Zookeepe 集群三个角色: * Epoch:每个 Leader 任期的代号,同一轮选举投票过程中的该值是相同的,投完一次票就增加 -选举机制:半数机制,超过半数的投票旧通过 +选举机制:半数机制,超过半数的投票就通过 * 第一次启动选举规则:投票过半数时,服务器 ID 大的胜出 @@ -11223,7 +11223,7 @@ FastLeaderElection 中有 WorkerReceiver 线程 -### 状态同步 +#### 状态同步 选举结束后,每个节点都需要根据角色更新自己的状态,Leader 更新状态为 Leader,其他节点更新状态为 Follower,整体流程: diff --git a/Java.md b/Java.md index 90a56bd..da0a45a 100644 --- a/Java.md +++ b/Java.md @@ -4750,7 +4750,7 @@ HashMap 继承关系如下图所示: HashMap(int initialCapacity)// 构造一个带指定初始容量和默认加载因子 (0.75) 的空 HashMap ``` - * 为什么必须是 2 的 n 次幂?用位运算替代取余计算 + * 为什么必须是 2 的 n 次幂?用位运算替代取余计算,减少 rehash 的代价(移动的节点少) HashMap 中添加元素时,需要根据 key 的 hash 值确定在数组中的具体位置。为了减少碰撞,把数据分配均匀,每个链表长度大致相同,实现该方法就是取模 `hash%length`,计算机中直接求余效率不如位移运算, **`hash % length == hash & (length-1)` 的前提是 length 是 2 的 n 次幂** From 5e2c97e3e9914f4d432c33100195cf269cac2e0e Mon Sep 17 00:00:00 2001 From: Seazean Date: 2022年7月19日 23:39:55 +0800 Subject: [PATCH 13/35] Update Java Note --- DB.md | 12 ++++++------ Frame.md | 4 ++-- Tool.md | 28 +++++++++++++-------------- Web.md | 58 ++++++++++++++++++++++++++++++++------------------------ 4 files changed, 55 insertions(+), 47 deletions(-) diff --git a/DB.md b/DB.md index 22e3274..a909a5e 100644 --- a/DB.md +++ b/DB.md @@ -6856,7 +6856,7 @@ MySQL 中还存在 binlog(二进制日志)也可以记录写操作并用于 * 内容不同:redo log 是物理日志,内容基于磁盘的 Page;binlog 的内容是二进制的,根据 binlog_format 参数的不同,可能基于SQL 语句、基于数据本身或者二者的混合(日志部分详解) * 写入时机不同:binlog 在事务提交时一次写入;redo log 的写入时机相对多元 -binlog 为什么不支持奔溃恢复? +binlog 为什么不支持崩溃恢复? * binlog 记录的是语句,并不记录数据页级的数据(哪个页改了哪些地方),所以没有能力恢复数据页 * binlog 是追加写,保存全量的日志,没有标志确定从哪个点开始的数据是已经刷盘了,而 redo log 只要在 checkpoint_lsn 后面的就是没有刷盘的 @@ -9767,9 +9767,9 @@ Redis 为文件事件编写了多个处理器,这些事件处理器分别用 Redis 客户端与服务器进行连接并发送命令的整个过程: * Redis 服务器正在运作监听套接字的 AE_READABLE 事件,关联连接应答处理器 -* 当 Redis 客户端向服务器发起连接,监听套接字将产生 AE_READABLE 事件,触发连接应答处理器执行,对客户端的连接请求进行应答,创建客户端套接字以及客户端状态,并将客户端套接字的 AE_READABLE 事件与命令请求处理器进行关联 +* 当 Redis 客户端向服务器发起连接,监听套接字将产生 AE_READABLE 事件,触发连接应答处理器执行,对客户端的连接请求进行应答,创建客户端套接字以及客户端状态,并将客户端套接字的 **AE_READABLE 事件与命令请求处理器**进行关联 * 客户端向服务器发送命令请求,客户端套接字产生 AE_READABLE 事件,引发命令请求处理器执行,读取客户端的命令内容传给相关程序去执行 -* 执行命令会产生相应的命令回复,为了将这些命令回复传送回客户端,服务器会将客户端套接字的 AE_WRITABLE 事件与命令回复处理器进行关联 +* 执行命令会产生相应的命令回复,为了将这些命令回复传送回客户端,服务器会将客户端套接字的 **AE_WRITABLE 事件与命令回复处理器**进行关联 * 当客户端尝试读取命令回复时,客户端套接字产生 AE_WRITABLE 事件,触发命令回复处理器执行,在命令回复全部写入套接字后,服务器就会解除客户端套接字的 AE_WRITABLE 事件与命令回复处理器之间的关联 @@ -10303,7 +10303,7 @@ struct redisCommand { ##### 基本介绍 -Redis 服务器以周期性事件的方式来运行 serverCron 函数,服务器初始化时读取配置 server.hz 的值,默认为 10,代表每秒钟执行 10 次,即每隔 100 毫秒执行一次,执行指令 info server 可以查看 +Redis 服务器以周期性事件的方式来运行 serverCron 函数,服务器初始化时读取配置 server.hz 的值,默认为 10,代表每秒钟执行 10 次,即**每隔 100 毫秒执行一次**,执行指令 info server 可以查看 serverCron 函数负责定期对自身的资源和状态进行检查和调整,从而确保服务器可以长期、稳定地运行 @@ -12644,7 +12644,7 @@ struct redisServer { ##### 文件写入 -服务器在处理文件事件时可能会执行写命令,追加一些内容到 aof_buf 缓冲区里,所以服务器每次结束一个事件循环之前,就会执行 flushAppendOnlyFile 函数,判断是否需要**将 aof_buf 缓冲区中的内容写入和保存到 AOF 文件**里 +服务器在处理文件事件时会执行**写命令,追加一些内容到 aof_buf 缓冲区**里,所以服务器每次结束一个事件循环之前,就会执行 flushAppendOnlyFile 函数,判断是否需要**将 aof_buf 缓冲区中的内容写入和保存到 AOF 文件**里 flushAppendOnlyFile 函数的行为由服务器配置的 appendfsync 选项的值来决定 @@ -15909,7 +15909,7 @@ Cache Aside Pattern 中服务端需要同时维系 DB 和 cache,并且是以 D * 首次请求数据一定不在 cache 的问题,一般采用缓存预热的方法,将热点数据可以提前放入 cache 中 * 写操作比较频繁的话导致 cache 中的数据会被频繁被删除,影响缓存命中率 -删除缓存而不是更新缓存的原因:每次更新数据库都更新缓存,造成无效写操作较多(懒惰加载,需要的时候再放入缓存) +**删除缓存而不是更新缓存的原因**:每次更新数据库都更新缓存,造成无效写操作较多(懒惰加载,需要的时候再放入缓存) diff --git a/Frame.md b/Frame.md index 5adc62b..d75e634 100644 --- a/Frame.md +++ b/Frame.md @@ -1544,9 +1544,9 @@ Reactor 对象通过 select 监控客户端请求事件,收到事件后通过 采用多个 Reactor ,执行流程: -* Reactor 主线程 MainReactor 通过 select 监控建立连接事件,收到事件后通过 Acceptor 接收,处理建立连接事件,处理完成后 MainReactor 会将连接分配给 Reactor 子线程的 SubReactor(有多个)处理 +* Reactor 主线程 MainReactor 通过 select **监控建立连接事件**,收到事件后通过 Acceptor 接收,处理建立连接事件,处理完成后 MainReactor 会将连接分配给 Reactor 子线程的 SubReactor(有多个)处理 -* SubReactor 将连接加入连接队列进行监听,并创建一个 Handler 用于处理该连接的事件,当有新的事件发生时,SubReactor 会调用连接对应的 Handler 进行响应 +* SubReactor 将连接加入连接队列进行监听其他事件,并创建一个 Handler 用于处理该连接的事件,当有新的事件发生时,SubReactor 会调用连接对应的 Handler 进行响应 * Handler 通过 read 读取数据后,会分发给 Worker 线程池进行业务处理 diff --git a/Tool.md b/Tool.md index 5f786da..53f4fe5 100644 --- a/Tool.md +++ b/Tool.md @@ -959,10 +959,10 @@ Linux 系统中查看进程使用情况的命令是 ps 指令 * -T:开启线程查看 * -p:指定线程号 - 一般常用格式为 ps -ef 或者 ps aux 两种。显示的信息大体一致,略有区别 -两者区别: - 如果想查看进程的 CPU 占用率和内存占用率,可以使用 aux - 如果想查看进程的父进程 ID 和完整的 COMMAND 命令,可以使用 ef + 一般常用格式为 ps -ef 或者 ps aux 两种。显示的信息大体一致,略有区别: + +* 如果想查看进程的 CPU 占用率和内存占用率,可以使用 aux +* 如果想查看进程的父进程 ID 和完整的 COMMAND 命令,可以使用 ef `ps -T -p `:显示某个进程的线程 @@ -1520,7 +1520,7 @@ cat 是一个文本文件查看和连接工具。用于小文件 #### less -less用于查看文件,但是less 在查看之前不会加载整个文件。用于大文件 +less 用于查看文件,但是 less 在查看之前不会加载整个文件,用于大文件 命令:less [options] Filename @@ -2015,10 +2015,10 @@ vi/vim 中提供了 **三种** 可视模式, 可以方便程序员的选择 ** vim 中提供有一个 被复制文本的缓冲区 -- 复制 命令会将选中的文字保存在缓冲区 -- 删除 命令删除的文字会被保存在缓冲区 -- 在需要的位置, 使用 粘贴 命令可以将缓冲对的文字插入到光标所在的位置 -- vim中的文本缓冲区只有一个,如果后续做过 复制、剪切操作, 之前缓冲区中的内容会被替换. +- 复制命令会将选中的文字保存在缓冲区 +- 删除命令删除的文字会被保存在缓冲区 +- 在需要的位置,使用粘贴命令可以将缓冲对的文字插入到光标所在的位置 +- vim 中的文本缓冲区只有一个,如果后续做过 复制、剪切操作,之前缓冲区中的内容会被替换. | 快捷键 | 功能描述 | | :-----: | :--------------------------: | @@ -2028,7 +2028,7 @@ vim 中提供有一个 被复制文本的缓冲区 | p | 将剪切板中的内容粘贴到光标后 | | P(大写) | 将剪切板中的内容粘贴到光标前 | -注意:vi中的 **文本缓冲区**和系统的**剪切板**不是同一个,在其他软件中使用 Ctrl + C 复制的内容,不能在vim 中通过 `p` 命令粘贴,可以在 **编辑模式** 下使用 **鼠标右键粘贴**。 +注意:vim 中的文本缓冲区和系统的剪切板不是同一个,在其他软件中使用 Ctrl + C 复制的内容,不能在 vim 中通过 `p` 命令粘贴,可以在编辑模式下使用鼠标右键粘贴 @@ -2059,8 +2059,8 @@ vim 中提供有一个 被复制文本的缓冲区 | R | 替换当前行光标后的字符 | 替换模式 | - 光标选中要替换的字符 -- `R` 命令可以进入 **替换模式**, 替换完成后, 按下ESC, 按下 ESC可以回到 **命令模式** -- **替换命令** 的作用就是不用进入 **编辑模式**, 对文件进行 **轻量级的修改** +- `R` 命令可以进入替换模式**,替换完成后,按下 ESC 可以回到 **命令模式** +- 替换命令的作用就是不用进入编辑模式,对文件进行轻量级的修改 @@ -2091,11 +2091,11 @@ vim 中提供有一个 被复制文本的缓冲区 * 如果 vim异常退出, 在磁盘上可能会保存有 交换文件 -* 下次再使用 vim 编辑文件时, 会看到以下屏幕信息, +* 下次再使用 vim 编辑文件时,会看到以下屏幕信息: ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/vim异常.png) -* ls -a 一下,会看到隐藏的.swp文件 删除了此文件即可。 +* ls -a 一下,会看到隐藏的 .swp 文件 删除了此文件即可 diff --git a/Web.md b/Web.md index b8e1725..5dfad06 100644 --- a/Web.md +++ b/Web.md @@ -2105,18 +2105,6 @@ URL 和 URI * 区别:`URL - HOST = URI`,URI 是抽象的定义,URL 用地址定位,URI 用名称定位。**只要能唯一标识资源的是 URI,在 URI 的基础上给出其资源的访问方式的是 URL** -短连接和长连接: - -* 短连接:客户端和服务器每进行一次 HTTP 操作,就建立一次连接,任务结束就中断连接 - - 使用短连接的情况下,当浏览器访问的某个 HTML 或其他类型的 Web 页中包含有其他的 Web 资源(图像文件、CSS 文件等),每遇到这样一个 Web 资源,浏览器就会经过三次握手重新建立一个 HTTP 会话 - -* 长连接:使用长连接的 HTTP 协议,会在响应头加入这行代码 `Connection:keep-alive` - - 使用长连接的情况下,当一个网页打开完成后,客户端和服务器之间用于传输 HTTP 数据的 TCP 连接不会关闭,客户端再次访问这个服务器时,会继续使用这一条已经建立的连接。Keep-Alive 有一个保持时间,不会永久保持连接,设置以后可以实现长连接,前提是需要客户端和服务端都支持长连接 - -* HTTP 协议的长连接和短连接,实质上是 TCP 协议的长连接和短连接 - **从浏览器地址栏输入 URL 到请求返回发生了什么?** * 进行 URL 解析,进行编码 @@ -2137,6 +2125,10 @@ URL 和 URI +推荐阅读:https://xiaolincoding.com/network/ + + + *** @@ -2155,25 +2147,35 @@ HTTP 1.0 和 HTTP 1.1 的主要区别: * 长短连接: - **在HTTP/1.0中,默认使用的是短连接**,每次请求都要重新建立一次连接。HTTP 基于 TCP/IP 协议的,每一次建立或者断开连接都需要三次握手四次挥手,开销会比较大 + **在HTTP/1.0中,默认使用的是短连接**,每次请求都要重新建立一次连接,比如获取 HTML 和 CSS 文件,需要两次请求。HTTP 基于 TCP/IP 协议的,每一次建立或者断开连接都需要三次握手四次挥手,开销会比较大 - **HTTP 1.1起,默认使用长连接** ,默认开启 `Connection: keep-alive`,HTTP/1.1 的持续连接有非流水线方式和流水线方式 ,流水线方式是客户端在收到 HTTP 的响应报文之前就能接着发送新的请求报文,非流水线方式是客户端在收到前一个响应后才能发送下一个请求 + **HTTP 1.1起,默认使用长连接** ,默认开启 `Connection: keep-alive`,Keep-Alive 有一个保持时间,不会永久保持连接。持续连接有非流水线方式和流水线方式 ,流水线方式是客户端在收到 HTTP 的响应报文之前就能接着发送新的请求报文,非流水线方式是客户端在收到前一个响应后才能发送下一个请求 + + HTTP 协议的长连接和短连接,实质上是 TCP 协议的长连接和短连接 * 错误状态响应码:在 HTTP1.1 中新增了 24 个错误状态响应码,如 409(Conflict)表示请求的资源与资源的当前状态发生冲突,410(Gone)表示服务器上的某个资源被永久性的删除 -* 缓存处理:在 HTTP1.0 中主要使用 header 里的 If-Modified-Since,Expires 来做为缓存判断的标准,HTTP1.1 则引入了更多的**缓存控制策略**例如 Entity tag,If-Unmodified-Since,If-Match,If-None-Match等 +* 缓存处理:在 HTTP1.0 中主要使用 header 里的 If-Modified-Since,Expires 来做为缓存判断的标准,HTTP1.1 则引入了更多的缓存控制策略,例如 Entity tag,If-Unmodified-Since,If-Match,If-None-Match等 -* 带宽优化及网络连接的使用:HTTP1.0 存在一些浪费带宽的现象,例如客户端只需要某个对象的一部分,而服务器却将整个对象送过来了,并且不支持断点续传功能,HTTP1.1 则在请求头引入了 range 头域,允许只**请求资源的某个部分**,即返回码是 206(Partial Content),这样就方便了开发者自由的选择以便于充分利用带宽和连接 +* 带宽优化及网络连接的使用:HTTP1.0 存在一些浪费带宽的现象,例如客户端只需要某个对象的一部分,而服务器却将整个对象送过来了,并且不支持**断点续传**功能,HTTP1.1 则在请求头引入了 range 头域,允许只**请求资源的某个部分**,即返回码是 206(Partial Content),这样就方便了开发者自由的选择以便于充分利用带宽和连接 * HOST 头处理:在 HTTP1.0 中认为每台服务器都绑定一个唯一的 IP 地址,因此请求消息中的 URL 并没有传递主机名。HTTP1.1 时代虚拟主机技术发展迅速,在一台物理服务器上可以存在多个虚拟主机,并且共享一个 IP 地址,故 HTTP1.1 增加了 HOST 信息 HTTP 1.1 和 HTTP 2.0 的主要区别: * 新的二进制格式:HTTP1.1 基于文本格式传输数据,HTTP2.0 采用二进制格式传输数据,解析更高效 -* **多路复用**:在一个连接里,允许同时发送多个请求或响应,**并且这些请求或响应能够并行的传输而不被阻塞**,避免 HTTP1.1 出现的队头堵塞问题 -* 头部压缩,HTTP1.1 的 header 带有大量信息,而且每次都要重复发送;HTTP2.0 把 header 从数据中分离,并封装成头帧和数据帧,**使用特定算法压缩头帧**。并且 HTTP2.0 在客户端和服务器端记录了之前发送的键值对,对于相同的数据不会重复发送。比如请求 A 发送了所有的头信息字段,请求 B 则只需要发送差异数据,这样可以减少冗余数据,降低开销 +* **多路复用**:在一个连接里,允许同时发送多个请求或响应,并且这些请求或响应能够并行的传输而不被阻塞,避免 HTTP1.1 出现的队头堵塞问题 +* 头部压缩,HTTP1.1 的 header 带有大量信息,而且每次都要重复发送;HTTP2.0 把 header 从数据中分离,并封装成头帧和数据帧,使用特定算法压缩头帧。并且 HTTP2.0 在客户端和服务器端记录了之前发送的键值对,对于相同的数据不会重复发送。比如请求 A 发送了所有的头信息字段,请求 B 则只需要发送差异数据,这样可以减少冗余数据,降低开销 * **服务端推送**:HTTP2.0 允许服务器向客户端推送资源,无需客户端发送请求到服务器获取 + + +**** + + + +## 安全请求 + HTTP 和 HTTPS 的区别: * 端口 :HTTP 默认使用端口 80,HTTPS 默认使用端口 443 @@ -2187,17 +2189,23 @@ HTTP 和 HTTPS 的区别: * 缺点:无法安全的将密钥传输给通信方 * 非对称加密:加密和解密使用不同的秘钥,一把作为公开的公钥,另一把作为私钥,**公钥公开给任何人**(类似于把锁和箱子给别人,对方打开箱子放入数据,上锁后发送),典型的非对称加密算法有 RSA、DSA 等 - * 优点:可以更安全地将公开密钥传输给通信发送方 - * 缺点:运算速度慢 - + + * 公钥加密,私钥解密:为了**保证内容传输的安全**,因为被公钥加密的内容,其他人是无法解密的,只有持有私钥的人,才能解密出实际的内容 + * 私钥加密,公钥解密:为了**保证消息不会被冒充**,因为私钥是不可泄露的,如果公钥能正常解密出私钥加密的内容,就能证明这个消息是来源于持有私钥身份的人发送的 + * 可以更安全地将公开密钥传输给通信发送方,但是运算速度慢 + * **使用对称加密和非对称加密的方式传送数据** * 使用非对称密钥加密方式,传输对称密钥加密方式所需要的 Secret Key,从而保证安全性 * 获取到 Secret Key 后,再使用对称密钥加密方式进行通信,从而保证效率 思想:锁上加锁 - -* 数字签名:附加在报文上的特殊加密校验码,可以防止报文被篡改,一般是通过哈希算法 + +名词解释: + +* 哈希算法:通过哈希函数计算出内容的哈希值,传输到对端后会重新计算内容的哈希,进行哈希比对来校验内容的完整性 + +* 数字签名:附加在报文上的特殊加密校验码,可以防止报文被篡改。一般是通过私钥对内容的哈希值进行加密,公钥正常解密并对比哈希值后,可以确保该内容就是对端发出的,防止出现中间人替换的问题 * 数字证书:由权威机构给某网站颁发的一种认可凭证 @@ -2206,9 +2214,9 @@ HTTPS 工作流程:服务器端的公钥和私钥,用来进行非对称加 ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/HTTP-HTTPS加密过程.png) 1. 客户端向服务器发起 HTTPS 请求,连接到服务器的 443 端口,请求携带了浏览器支持的加密算法和哈希算法,协商加密算法 -2. 服务器端会向数字证书认证机构提出公开密钥的申请,认证机构对公开密钥做数字签名后进行分配,会将公钥绑定在数字证书(又叫公钥证书,内容有公钥,网站地址,证书颁发机构,失效日期等) +2. 服务器端会向数字证书认证机构注册公开密钥,认证机构**用 CA 私钥**对公开密钥做数字签名后绑定在数字证书(又叫公钥证书,内容有公钥,网站地址,证书颁发机构,失效日期等) 3. 服务器将数字证书发送给客户端,私钥由服务器持有 -4. 客户端收到服务器端的数字证书后对证书进行检查,验证其合法性,如果发现发现证书有问题,那么 HTTPS 传输就无法继续。如果公钥合格,那么客户端会生成一个随机值,**这个随机值就是用于进行对称加密的密钥**,将该密钥称之为 client key(客户端密钥、会话密钥)。用服务器的公钥对客户端密钥进行非对称加密,这样客户端密钥就变成密文,HTTPS 中的第一次 HTTP 请求结束 +4. 客户端收到服务器端的数字证书后**通过 CA 公钥**(事先置入浏览器或操作系统)对证书进行检查,验证其合法性。如果公钥合格,那么客户端会生成一个随机值,这个随机值就是用于进行对称加密的密钥,将该密钥称之为 client key(客户端密钥、会话密钥)。用服务器的公钥对客户端密钥进行非对称加密,这样客户端密钥就变成密文,HTTPS 中的第一次 HTTP 请求结束 5. 客户端会发起 HTTPS 中的第二个 HTTP 请求,将加密之后的客户端密钥发送给服务器 6. 服务器接收到客户端发来的密文之后,会用自己的私钥对其进行非对称解密,解密之后的明文就是客户端密钥,然后用客户端密钥对数据进行对称加密,这样数据就变成了密文 7. 服务器将加密后的密文发送给客户端 From f5e9e89ab8e176b43d4bcaf70be4fa1c2ed1d006 Mon Sep 17 00:00:00 2001 From: Seazean Date: 2022年7月24日 23:34:04 +0800 Subject: [PATCH 14/35] Update Java Note --- Java.md | 6 +- Prog.md | 25 +- Tool.md | 708 +++++++++++++++++++++++++++----------------------------- 3 files changed, 363 insertions(+), 376 deletions(-) diff --git a/Java.md b/Java.md index da0a45a..bd943b5 100644 --- a/Java.md +++ b/Java.md @@ -11328,7 +11328,7 @@ objD.fieldG = G; // 写 * **写屏障 (Store Barrier) + SATB**:当原来成员变量的引用发生变化之前,记录下原来的引用对象 - 保留 GC 开始时的对象图,即原始快照 SATB,当 GC Roots 确定后,对象图就已经确定,那后续的标记也应该是按照这个时刻的对象图走,如果期间对白色对象有了新的引用会记录下来,并且将白色对象变灰(说明可达了),重新扫描该对象的引用关系 + 保留 GC 开始时的对象图,即原始快照 SATB,当 GC Roots 确定后,对象图就已经确定,那后续的标记也应该是按照这个时刻的对象图走,如果期间对白色对象有了新的引用会记录下来,并且将白色对象变灰(说明可达了,并且原始快照中本来就应该是灰色对象),最后重新扫描该对象的引用关系 SATB (Snapshot At The Beginning) 破坏了条件一,从而保证了不会漏标 @@ -11601,7 +11601,7 @@ GC 性能指标: #### Serial -Serial:串行垃圾收集器,作用于新生代,是指使用单线程进行垃圾回收,采用**复制算法**,新生代基本都是复制算法,因为分区了 +Serial:串行垃圾收集器,作用于新生代,是指使用单线程进行垃圾回收,采用**复制算法**,新生代基本都是复制算法 **STW(Stop-The-World)**:垃圾回收时,只有一个线程在工作,并且 Java 应用中的所有线程都要暂停,等待垃圾回收的完成 @@ -11681,7 +11681,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] diff --git a/Prog.md b/Prog.md index 5b3a51c..0dab7f7 100644 --- a/Prog.md +++ b/Prog.md @@ -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 都需要根据实际场景 @@ -7855,7 +7855,7 @@ AQS 核心思想: * 如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并将共享资源设置锁定状态 -* 请求的共享资源被占用,AQS 用 CLH 队列实现线程阻塞等待以及被唤醒时锁分配的机制,将暂时获取不到锁的线程加入到队列中 +* 请求的共享资源被占用,AQS 用队列实现线程阻塞等待以及被唤醒时锁分配的机制,将暂时获取不到锁的线程加入到队列中 CLH 是一种基于单向链表的**高性能、公平的自旋锁**,AQS 是将每条请求共享资源的线程封装成一个 CLH 锁队列的一个结点(Node)来实现锁的分配 @@ -13391,7 +13391,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 +13645,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 +13773,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 +13823,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 +14738,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/Tool.md b/Tool.md index 53f4fe5..f118b29 100644 --- a/Tool.md +++ b/Tool.md @@ -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 表示只有该档案是个子目录或者该档案已经被设定过为可执行 @@ -1403,9 +1387,9 @@ mode : 权限设定字串,格式: [ugoa...][[+-=][rwxX]...][,...] -各权限的数字对照表:[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 @@ -1432,20 +1416,28 @@ mode : 权限设定字串,格式: [ugoa...][[+-=][rwxX]...][,...] +*** + #### 更改属组 -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 文件,删除了此文件即可 @@ -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 命令用于向窗口输出文本 From ccf1f996420d35210fe6c6099c9bd99db6862c92 Mon Sep 17 00:00:00 2001 From: Seazean Date: 2022年7月27日 21:14:51 +0800 Subject: [PATCH 15/35] Update Java Note --- Tool.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Tool.md b/Tool.md index f118b29..efe91aa 100644 --- a/Tool.md +++ b/Tool.md @@ -1380,10 +1380,10 @@ mode : 权限设定字串,格式: [ugoa...][[+-=][rwxX]...][,...] 命令:chmod [-R] xyz 文件或目录 -- xyz : 就是刚刚提到的数字类型的权限属性,为 rwx 属性数值的相加。 -- -R : 进行递归(recursive)的持续变更,亦即连同次目录下的所有文件都会变更 +- xyz : 就是刚刚提到的数字类型的权限属性,为 rwx 属性数值的相加 +- -R : 进行递归(recursive)的持续变更,亦即连同次目录下的所有文件都会变更 -文件的权限字符为:[-rwxrwxrwx], 这九个权限是三三一组的,我们使用数字来代表各个权限。 +文件的权限字符为:[-rwxrwxrwx], 这九个权限是三三一组的,我们使用数字来代表各个权限 @@ -1395,7 +1395,7 @@ mode : 权限设定字串,格式: [ugoa...][[+-=][rwxX]...][,...] - group = rwx = 4+2+1 = 7 - others= --- = 0+0+0 = 0 -表示为:`chmod -R 770 文件名` +表示为:`chmod -R 770 文件名` @@ -1408,11 +1408,11 @@ 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** From 1b75f5c5cd1f3123787c5b8ea5a3afb2f5aae63e Mon Sep 17 00:00:00 2001 From: Seazean Date: Sat, 6 Aug 2022 20:00:36 +0800 Subject: [PATCH 16/35] Update Java Note --- DB.md | 10 +++++----- Java.md | 2 +- Prog.md | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/DB.md b/DB.md index a909a5e..592feab 100644 --- a/DB.md +++ b/DB.md @@ -12657,7 +12657,7 @@ appendfsync always|everysec|no #AOF写数据策略:默认为everysec 特点:安全性最高,数据零误差,但是性能较低,不建议使用 -- everysec:先将 aof_buf 缓冲区中的内容写入到 AOF 文件,判断上次同步 AOF 文件的时间距离现在超过一秒钟,再次对 AOF 文件进行同步,这个同步操作是由一个(子)线程专门负责执行的 +- everysec:先将 aof_buf 缓冲区中的内容写入到 AOF 文件,判断上次同步 AOF 文件的时间距离现在超过一秒钟,再次对 AOF 文件进行同步 fsync,这个同步操作是由一个(子)线程专门负责执行的 特点:在系统突然宕机的情况下丢失 1 秒内的数据,准确性较高,性能较高,建议使用,也是默认配置 @@ -12826,7 +12826,7 @@ RDB 的特点 AOF 特点: -* AOF 的优点:数据持久化有较好的实时性,通过 AOF 重写可以降低文件的体积 +* AOF 的优点:数据持久化有**较好的实时性**,通过 AOF 重写可以降低文件的体积 * AOF 的缺点:文件较大时恢复较慢 AOF 和 RDB 同时开启,系统默认取 AOF 的数据(数据不会存在丢失) @@ -12955,7 +12955,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 @@ -14557,7 +14557,7 @@ Sentinel 为主服务器创建的实例结构的 sentinels 字典保存所有同 * 如果源 Sentinel 的实例结构存在,那么对源 Sentinel 的实例结构进行更新 * 如果源 Sentinel 的实例结构不存在,说明源 Sentinel 是刚开始监视主服务器,目标 Sentinel 会为源 Sentinel 创建一个新的实例结构,并将这个结构添加到 sentinels 字典里面 -因为 Sentinel 可以接收到的频道信息来获知其他 Sentinel 的存在,并通过发送频道信息来让其他 Sentinel 知道自己的存在,所以用户在使用 Sentinel 时并不需要提供各个 Sentinel 的地址信息,**监视同一个主服务器的多个 Sentinel 可以自动发现对方** +因为 Sentinel 可以接收到的频道信息来获知其他 Sentinel 的存在,并通过发送频道信息来让其他 Sentinel 知道自己的存在,所以用户在使用 Sentinel 时并不需要提供各个 Sentinel 的地址信息,**监视同一个主服务器的多个 Sentinel 可以xiang发现对方** @@ -14628,7 +14628,7 @@ SENTINEL is-master-down-by-addr 源 Sentinel 将统计其他 Sentinel 同意主服务器已下线的数量,当这一数量达到配置指定的判断客观下线所需的数量(quorum)时,Sentinel 会将主服务器对应实例结构 flags 属性的 SRI_O_DOWN 标识打开,代表客观下线,并对主服务器执行故障转移操作 -注意:不同 Sentinel 判断客观下线的条件可能不同,因为载入的配置文件中的属性(quorum)可能不同 +注意:**不同 Sentinel 判断客观下线的条件可能不同**,因为载入的配置文件中的属性(quorum)可能不同 diff --git a/Java.md b/Java.md index bd943b5..ad43393 100644 --- a/Java.md +++ b/Java.md @@ -5891,7 +5891,7 @@ class Dog{} ### 基本介绍 -异常:程序在编译或者执行的过程中可能出现的问题,Java 为常见的代码异常都设计一个类来代表。 +异常:程序在编译或者执行的过程中可能出现的问题,Java 为常见的代码异常都设计一个类来代表 错误:Error ,程序员无法处理的错误,只能重启系统,比如内存奔溃,JVM 本身的奔溃 diff --git a/Prog.md b/Prog.md index 0dab7f7..6646f7c 100644 --- a/Prog.md +++ b/Prog.md @@ -651,7 +651,7 @@ Java 提供了线程优先级的机制,优先级会提示(hint)调度器 #### 未来优化 -内核级线程调度的成本较大,所以引入了更轻量级的协程。用户线程的调度由用户自己实现(一对多的线程模型),被设计为协同式调度,所以叫协程 +内核级线程调度的成本较大,所以引入了更轻量级的协程。用户线程的调度由用户自己实现(多对一的线程模型,多**个用户线程映射到一个内核级线程**),被设计为协同式调度,所以叫协程 * 有栈协程:协程会完整的做调用栈的保护、恢复工作,所以叫有栈协程 * 无栈协程:本质上是一种有限状态机,状态保存在闭包里,比有栈协程更轻量,但是功能有限 @@ -4608,7 +4608,7 @@ java.util.concurrent.BlockingQueue 接口有以下阻塞队列的实现:**FIFO ##### 入队出队 -LinkedBlockingQueue源码: +LinkedBlockingQueue 源码: ```java public class LinkedBlockingQueue extends AbstractQueue @@ -5603,7 +5603,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 计算能力的任务比如在内存中对大量数据进行分析 From 56fa3fa46bbf0ae425b357afec1920d400cc9fcf Mon Sep 17 00:00:00 2001 From: Seazean Date: 2022年8月11日 21:54:56 +0800 Subject: [PATCH 17/35] ByteDance Campus Recruiting --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index be257cf..6083f8e 100644 --- a/README.md +++ b/README.md @@ -18,3 +18,6 @@ * 所有的知识不保证权威性,如果各位朋友发现错误,欢迎与我讨论。 * 笔记的编写基于 Windows 平台,可能会因为平台的不同而造成空格、制表符的显示效果不同。 +字节跳动校园招聘:https://jobs.bytedance.com/campus/invite?referral_code=1VQUWCD + +![](https://seazean.oss-cn-beijing.aliyuncs.com/bytedance.jpg) From 03871e29d2c5ea55e1bd7a58d49df3b77ada7e2c Mon Sep 17 00:00:00 2001 From: Seazean Date: 2022年8月12日 22:49:57 +0800 Subject: [PATCH 18/35] Update Java Note --- DB.md | 795 ++----------------------------------------------------- Frame.md | 2 +- 2 files changed, 19 insertions(+), 778 deletions(-) diff --git a/DB.md b/DB.md index 592feab..4c6f1a2 100644 --- a/DB.md +++ b/DB.md @@ -9272,7 +9272,7 @@ Redis 通过过期字典可以检查一个给定键是否过期: 针对过期数据有三种删除策略: - 定时删除 -- 惰性删除 +- 惰性删除(被动删除) - 定期删除 Redis 采用惰性删除和定期删除策略的结合使用 @@ -9333,7 +9333,7 @@ Redis 采用惰性删除和定期删除策略的结合使用 - activeExpireCycle() 对某个数据库中的每个 expires 进行检测,工作模式: - * 轮询每个数据库,从数据库中取出一定数量的随机键进行检查,并删除其中的过期键 + * 轮询每个数据库,从数据库中取出一定数量的随机键进行检查,并删除其中的过期键,如果过期 key 的比例超过了 25%,则继续重复此过程,直到过期 key 的比例下降到 25% 以下,或者这次任务的执行耗时超过了 25 毫秒 * 全局变量 current_db 用于记录 activeExpireCycle() 的检查进度(哪一个数据库),下一次调用时接着该进度处理 * 随着函数的不断执行,服务器中的所有数据库都会被检查一遍,这时将 current_db 重置为 0,然后再次开始新一轮的检查 @@ -9806,8 +9806,6 @@ Redis 的时间事件分为以下两类: 无序链表并不影响时间事件处理器的性能,因为正常模式下的 Redis 服务器**只使用 serverCron 一个时间事件**,在 benchmark 模式下服务器也只使用两个时间事件,所以无序链表不会影响服务器的性能,几乎可以按照一个指针处理 -服务器 → serverCron 详解该时间事件 - *** @@ -11334,7 +11332,7 @@ typedef struct redisObiect { Redis 并没有直接使用数据结构来实现键值对数据库,而是基于这些数据结构创建了一个对象系统,包含字符串对象、列表对象、哈希对象、集合对象和有序集合对象这五种类型的对象,而每种对象又通过不同的编码映射到不同的底层数据结构 -Redis 自身是一个 Map,其中所有的数据都是采用 key : value 的形式存储,**键对象都是字符串对象**,而值对象有五种基本类型和三种高级类型对象 +Redis 是一个 Map 类型,其中所有的数据都是采用 key : value 的形式存储,**键对象都是字符串对象**,而值对象有五种基本类型和三种高级类型对象 ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-对象编码.png) @@ -13440,8 +13438,6 @@ Redis 复制 EVAL、SCRIPT FLUSH、SCRIPT LOAD 三个命令的方法和复制普 - - *** @@ -14054,6 +14050,9 @@ 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 @@ -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 可以xiang发现对方** +哨兵实例之间可以相互发现,要归功于 Redis 提供发布订阅机制 @@ -15481,6 +15467,7 @@ typedef struct clusterMsgDataPublish { * 假设从库有 K 个,可以将 min-slaves-to-write 设置为 K/2+1(如果 K 等于 1,就设为 1) * 将 min-slaves-max-lag 设置为十几秒(例如 10〜20s) +* 在假故障期间无法响应哨兵发出的心跳测试,无法和从库进行 ACK 确认,并且没有足够的从库,**拒绝客户端的写入** @@ -16158,173 +16145,20 @@ Read-Through Pattern 也存在首次不命中的问题,采用缓存预热解 +### 慢查询 +确认服务和 Redis 之间的链路是否正常,排除网络原因后进行 Redis 的排查: -### 性能指标 - -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 - ``` +* 使用复杂度过高的命令 +* 操作大 key,分配内存和释放内存会比较耗时 +* key 集中过期,导致定时任务需要更长的时间去清理 +* 实例内存达到上限,每次写入新的数据之前,Redis 必须先从实例中踢出一部分数据 - 最后一次主从信息交换距现在的秒: - - ```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 ] - ``` - - 范例: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:慢日志 - - ```sh - slowlog [operator] #获取慢查询日志 - ``` - - * get :获取慢查询日志信息 - * len :获取慢查询日志条目数 - * reset :重置慢查询日志 - - 相关配置: - - ```sh - slowlog-log-slower-than 1000 #设置慢查询的时间下线,单位:微妙 - slowlog-max-len 100 #设置慢查询命令对应的日志显示长度,单位:命令数 - ``` +参考文章:https://www.cnblogs.com/traditional/p/15633919.html(非常好) @@ -16512,227 +16346,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 +16424,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 +16617,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..57f6cbd 100644 --- a/Frame.md +++ b/Frame.md @@ -10218,7 +10218,7 @@ ConsumeRequest 是 ConsumeMessageOrderlyService 的内部类,是一个 Runnabl * 首先通过负载均衡服务,将分配到当前消费者实例的 MQ 创建 PullRequest,并放入 PullMessageService 的本地阻塞队列内 * PullMessageService 循环从阻塞队列获取请求对象,发起拉消息请求,并创建 PullCallback 回调对象,将正常拉取的消息**提交到消费任务线程池**,并设置请求的下一次拉取位点,重新放入阻塞队列,形成闭环 -* 消费任务服务对消费失败的消息进行回退,回退失败的消息会再次提交消费任务重新消费 +* 消费任务服务对消费失败的消息进行回退,通过内部生产者实例发送回退消息,回退失败的消息会再次提交消费任务重新消费 * Broker 端对拉取消息的请求进行处理(processRequestCommand),查询成功将消息放入响应体,通过 Netty 写回客户端,当 `pullRequest.offset == queue.maxOffset` 说明该队列已经没有需要获取的消息,将请求放入长轮询集合等待有新消息 * PullRequestHoldService 负责长轮询,每 5 秒遍历一次长轮询集合,将满足条件的 PullRequest 再次提交到线程池内处理 From 3ae2803eebc5943b70f625974078da4ad27360ce Mon Sep 17 00:00:00 2001 From: Seazean Date: 2022年8月21日 22:11:49 +0800 Subject: [PATCH 19/35] Update Java Note --- DB.md | 22 +++++++++++----------- Prog.md | 4 ++-- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/DB.md b/DB.md index 4c6f1a2..9292e64 100644 --- a/DB.md +++ b/DB.md @@ -10799,7 +10799,7 @@ SDS 通过未使用空间解除了字符串长度和底层数组长度之间的 内存重分配涉及复杂的算法,需要执行**系统调用**,是一个比较耗时的操作,SDS 的两种优化策略: -* 空间预分配:当 SDS 的 API 进行修改并且需要进行空间扩展时,程序不仅会为 SDS 分配修改所必需的空间, 还会为 SDS 分配额外的未使用空间 +* 空间预分配:当 SDS需要进行空间扩展时,程序不仅会为 SDS 分配修改所必需的空间, 还会为 SDS 分配额外的未使用空间 * 对 SDS 修改之后,SDS 的长度(len 属性)小于 1MB,程序分配和 len 属性同样大小的未使用空间,此时 len 和 free 相等 @@ -10811,7 +10811,7 @@ SDS 通过未使用空间解除了字符串长度和底层数组长度之间的 在扩展 SDS 空间前,API 会先检查 free 空间是否足够,如果足够就无需执行内存重分配,所以通过预分配策略,SDS 将连续增长 N 次字符串所需内存的重分配次数从**必定 N 次降低为最多 N 次** -* 惰性空间释放:当 SDS 的 API 需要缩短字符串时,程序并不立即使用内存重分配来回收缩短后多出来的字节,而是使用 free 属性将这些字节的数量记录起来,并等待将来使用 +* 惰性空间释放:当 SDS 缩短字符串时,程序并不立即使用内存重分配来回收缩短后多出来的字节,而是使用 free 属性将这些字节的数量记录起来,并等待将来复用 SDS 提供了相应的 API 来真正释放 SDS 的未使用空间,所以不用担心空间惰性释放策略造成的内存浪费问题 @@ -11035,7 +11035,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 带来的庞大计算量 @@ -11110,11 +11110,11 @@ typedef struct zskiplistNode { 前进指针:forward 用于从表头到表尾方向**正序(升序)遍历节点**,遇到 NULL 停止遍历 -跨度:span 用于记录两个节点之间的距离,用来**计算排位(rank)**: +跨度:span 用于记录两个节点之间的距离,用来计算排位(rank): * 两个节点之间的跨度越大相距的就越远,指向 NULL 的所有前进指针的跨度都为 0 -* 在查找某个节点的过程中,将沿途访问过的所有层的跨度累计起来,结果就是目标节点在跳跃表中的排位,按照上图所示: +* 在查找某个节点的过程中,**将沿途访问过的所有层的跨度累计起来,结果就是目标节点在跳跃表中的排位**,按照上图所示: 查找分值为 3.0 的节点,沿途经历的层:查找的过程只经过了一个层,并且层的跨度为 3,所以目标节点在跳跃表中的排位为 3 @@ -11122,9 +11122,9 @@ typedef struct zskiplistNode { 后退指针:backward 用于从表尾到表头方向**逆序(降序)遍历节点** -分值:score 属性一个 double 类型的浮点数,跳跃表中的所有节点都**按分值从小到大来排序** +分值:score 属性一个 double 类型的浮点数,跳跃表中的所有节点都按分值从小到大来排序 -成员对象:obj 属性是一个指针,指向一个 SDS 字符串对象。同一个跳跃表中,各个节点保存的成员对象必须是唯一的,但是多个节点保存的分值可以是相同的,分值相同的节点将按照成员对象在字典序中的大小来进行排序(从小到大) +成员对象:obj 属性是一个指针,指向一个 SDS 字符串对象。同一个跳跃表中,各个节点保存的**成员对象必须是唯一的**,但是多个节点保存的分值可以是相同的,分值相同的节点将按照成员对象在字典序中的大小来进行排序(从小到大) @@ -11169,7 +11169,7 @@ encoding 取值为三种:INTSET_ENC_INT16、INTSET_ENC_INT32、INTSET_ENC_INT6 -#### 升级降级 +#### 类型升级 整数集合添加的新元素的类型比集合现有所有元素的类型都要长时,需要先进行升级(upgrade),升级流程: @@ -11243,7 +11243,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) @@ -12090,8 +12090,8 @@ set 类型:与 hash 存储结构哈希表完全相同,只是仅存储键不 使用字典加跳跃表的优势: -* 字典为有序集合创建了一个从成员到分值的映射,用 O(1) 复杂度查找给定成员的分值 -* 排序操作使用跳跃表完成,节省每次重新排序带来的时间成本和空间成本 +* 字典为有序集合创建了一个**从成员到分值的映射**,用 O(1) 复杂度查找给定成员的分值 +* **排序操作使用跳跃表完成**,节省每次重新排序带来的时间成本和空间成本 使用 ziplist 格式存储需要满足以下两个条件: diff --git a/Prog.md b/Prog.md index 6646f7c..c0a54d0 100644 --- a/Prog.md +++ b/Prog.md @@ -5808,7 +5808,7 @@ ThreadPoolExecutor 使用 int 的**高 3 位来表示线程池状态,低 29 成员变量 -* 线程池中存放 Worker 的容器:线程池没有初始化,直接往池中加线程即可 +* **线程池中存放 Worker 的容器**:线程池没有初始化,直接往池中加线程即可 ```java private final HashSet workers = new HashSet(); @@ -6583,7 +6583,7 @@ FutureTask 类的成员属性: private volatile Thread runner; // 当前任务被线程执行期间,保存当前执行任务的线程对象引用 ``` -* 线程阻塞队列的头节点: +* **线程阻塞队列的头节点**: ```java // 会有很多线程去 get 当前任务的结果,这里使用了一种数据结构头插头取(类似栈)的一个队列来保存所有的 get 线程 From 1939eee7f19f0bf803e2132bd5caa00865640330 Mon Sep 17 00:00:00 2001 From: Seazean Date: Mon, 5 Sep 2022 23:55:52 +0800 Subject: [PATCH 20/35] Update Java Note --- Frame.md | 77 +++++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 68 insertions(+), 9 deletions(-) diff --git a/Frame.md b/Frame.md index 57f6cbd..9c7fdaa 100644 --- a/Frame.md +++ b/Frame.md @@ -3435,6 +3435,8 @@ RCVBUF_ALLOCATOR:属于 SocketChannal 参数 ### 消息队列 +#### 应用场景 + 消息队列是一种先进先出的数据结构,常见的应用场景: * 应用解耦:系统的耦合性越高,容错性就越低 @@ -3459,6 +3461,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 +4587,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 +4625,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 +4659,6 @@ RocketMQ 采用文件系统的方式,无论同步还是异步刷盘,都使 - - **** @@ -4698,7 +4728,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 +4748,7 @@ BrokerServer 的高可用通过 Master 和 Slave 的配合: 如果一个 Broker 组有 Master 和 Slave,消息需要从 Master 复制到 Slave 上,有同步和异步两种复制方式: -* 同步复制方式:Master 和 Slave 均写成功后才反馈给客户端写成功状态。在同步复制方式下,如果 Master 出故障, Slave 上有全部的备份数据,容易恢复,但是同步复制会增大数据写入延迟,降低系统吞吐量 +* 同步复制方式:Master 和 Slave 均写成功后才反馈给客户端写成功状态(写 Page Cache)。在同步复制方式下,如果 Master 出故障, Slave 上有全部的备份数据,容易恢复,但是同步复制会增大数据写入延迟,降低系统吞吐量 * 异步复制方式:只要 Master 写成功,即可反馈给客户端写成功状态,系统拥有较低的延迟和较高的吞吐量,但是如果 Master 出了故障,有些数据因为没有被写入 Slave,有可能会丢失 @@ -4737,6 +4769,8 @@ RocketMQ 支持消息的高可靠,影响消息可靠性的几种情况: 后两种属于单点故障,且无法恢复,一旦发生,在此单点上的消息全部丢失。RocketMQ 在这两种情况下,通过主从异步复制,可保证 99% 的消息不丢,但是仍然会有极少量的消息可能丢失。通过**同步双写技术**可以完全避免单点,但是会影响性能,适合对消息可靠性要求极高的场合,RocketMQ 从 3.0 版本开始支持同步双写 +一般而言,我们会建议采取同步双写 + 异步刷盘的方式,在消息的可靠性和性能间有一个较好的平衡 + **** @@ -4883,7 +4917,7 @@ IndexFile 文件的存储在 `$HOME\store\index${fileName}`,文件名 fileName #### 消息重投 -生产者在发送消息时,同步消息和异步消息失败会重投,oneway 没有任何保证。消息重投保证消息尽可能发送成功、不丢失,但当出现消息量大、网络抖动时,可能会造成消息重复;生产者主动重发、Consumer 负载变化也会导致重复消息。 +生产者在发送消息时,同步消息和异步消息失败会重投,oneway 没有任何保证。消息重投保证消息尽可能发送成功、不丢失,但当出现消息量大、网络抖动时,可能会造成消息重复;生产者主动重发、Consumer 负载变化也会导致重复消息 如下方法可以设置消息重投策略: @@ -5045,6 +5079,31 @@ public class MessageListenerImpl implements MessageListener { +*** + + + +### 高可靠性 + +RocketMQ 消息丢失可能发生在以下三个阶段: + +- 生产阶段:消息在 Producer 发送端创建出来,经过网络传输发送到 Broker 存储端 + - 生产者得到一个成功的响应,就可以认为消息的存储和消息的消费都是可靠的 + - 消息重投机制 +- 存储阶段:消息在 Broker 端存储,如果是主备或者多副本,消息会在这个阶段被复制到其他的节点或者副本上 + - 单点:刷盘机制(同步或异步) + - 主从:消息同步机制(异步复制或同步双写,主从复制章节详解) + - 过期删除:操作 CommitLog、ConsumeQueue 文件是基于文件内存映射机制,并且在启动的时候会将所有的文件加载,为了避免内存与磁盘的浪费,让磁盘能够循环利用,防止磁盘不足导致消息无法写入等引入了文件过期删除机制。最终使得磁盘水位保持在一定水平,最终保证新写入消息的可靠存储 +- 消费阶段:Consumer 消费端从 Broker存储端拉取消息,经过网络传输发送到 Consumer 消费端上 + - 消息重试机制来最大限度的保证消息的消费 + - 消费失败的进行消息回退,重试次数过多的消息放入死信队列 + + + +推荐文章:https://cdn.modb.pro/db/394751 + + + **** @@ -7374,7 +7433,7 @@ AllocateMappedFileService **创建 MappedFile 服务** ReputMessageService 消息分发服务,用于构**建 ConsumerQueue 和 IndexFile 文件** -* run():**循环执行 doReput 方法**,所以发送的消息存储进 CL 就可以产生对应的 CQ,每执行一次线程休眠 1 毫秒 +* run():**循环执行 doReput 方法**,**所以发送的消息存储进 CL 就可以产生对应的 CQ**,每执行一次线程休眠 1 毫秒 ```java public void run() @@ -7566,7 +7625,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 进行过滤 From 74fa38961d4d2cf415c90098710c0bec871f9dc7 Mon Sep 17 00:00:00 2001 From: Seazean Date: 2022年9月18日 19:20:16 +0800 Subject: [PATCH 21/35] Update Java Note --- Java.md | 17 ++++++++++------- Tool.md | 10 +++++----- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/Java.md b/Java.md index ad43393..be64732 100644 --- a/Java.md +++ b/Java.md @@ -10596,23 +10596,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 diff --git a/Tool.md b/Tool.md index efe91aa..d0b47cc 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 @@ -2245,7 +2245,7 @@ pid_t wait(int *status) 参数:status 用来保存被收集的子进程退出时的状态,如果不关心子进程**如何**销毁,可以设置这个参数为 NULL -父进程调用 wait() 会一直阻塞,直到收到一个子进程退出的 SIGCHLD 信号,wait() 函数就会销毁子进程并返回 +父进程调用 wait() 会阻塞等待,直到收到一个子进程退出的 SIGCHLD 信号,wait() 函数就会销毁子进程并返回 * 成功,返回被收集的子进程的进程 ID * 失败,返回 -1,同时 errno 被置为 ECHILD(如果调用进程没有子进程,调用就会失败) From 47f25d993cc196be20d05caa70bb3f995f5d0fa2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CSeazean=E2=80=9D?= Date: 2022年9月18日 19:44:36 +0800 Subject: [PATCH 22/35] Update Java Note --- Tool.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tool.md b/Tool.md index d0b47cc..2ed20c7 100644 --- a/Tool.md +++ b/Tool.md @@ -66,7 +66,7 @@ GitLab(地址: https://about.gitlab.com/ )是一个用于仓库管理系 设置用户信息: * git config --global user.name "Seazean" -* git config --global user.email "imseazean@gmail.com" //用户名和邮箱可以随意填写,不会校对 +* git config --global user.email "imseazean@gmail.com" //用户名和邮箱可以随意填写,不会校对 查看配置信息: From 2bb1d62afa95c48ad700913f750780a2ca968ff5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CSeazean=E2=80=9D?= Date: 2022年9月23日 23:35:57 +0800 Subject: [PATCH 23/35] Update Java Note --- DB.md | 2 +- Prog.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/DB.md b/DB.md index 9292e64..5284330 100644 --- a/DB.md +++ b/DB.md @@ -7416,7 +7416,7 @@ InnoDB 加锁的基本单位是 next-key lock,该锁是行锁和 gap lock 的 * 范围查询无论是否是唯一索引,都需要访问到不满足条件的第一个值为止 * 对于联合索引且是唯一索引,如果 where 条件只包括联合索引的一部分,那么会加间隙锁 -间隙锁优点:RR 级别下间隙锁可以解决事务的一部分的**幻读问题**,通过对间隙加锁,可以防止读取过程中数据条目发生变化。一部分的意思是不会对全部间隙加锁,只能加锁一部分的间隙 +间隙锁优点:RR 级别下间隙锁可以**解决事务的一部分的幻读问题**,通过对间隙加锁,可以防止读取过程中数据条目发生变化。一部分的意思是不会对全部间隙加锁,只能加锁一部分的间隙 间隙锁危害: diff --git a/Prog.md b/Prog.md index c0a54d0..eac3448 100644 --- a/Prog.md +++ b/Prog.md @@ -2921,7 +2921,7 @@ CAS 特点: CAS 缺点: -- 循环时间长,开销大,因为执行的是循环操作,如果比较不成功一直在循环,最差的情况某个线程一直取到的值和预期值都不一样,就会无限循环导致饥饿,**使用 CAS 线程数不要超过 CPU 的核心数** +- 执行的是循环操作,如果比较不成功一直在循环,最差的情况某个线程一直取到的值和预期值都不一样,就会无限循环导致饥饿,**使用 CAS 线程数不要超过 CPU 的核心数**,采用分段 CAS 和自动迁移机制 - 只能保证一个共享变量的原子操作 - 对于一个共享变量执行操作时,可以通过循环 CAS 的方式来保证原子操作 - 对于多个共享变量操作时,循环 CAS 就无法保证操作的原子性,这个时候**只能用锁来保证原子性** @@ -3470,7 +3470,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()`:返回当前版本号 From 8bd077ad95531d518aeff7d2b9d4675bf10316e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CSeazean=E2=80=9D?= Date: 2022年9月26日 00:40:32 +0800 Subject: [PATCH 24/35] Update Java Note --- DB.md | 10 +++++----- Prog.md | 2 +- SSM.md | 20 ++++++++++---------- Web.md | 2 -- 4 files changed, 16 insertions(+), 18 deletions(-) diff --git a/DB.md b/DB.md index 5284330..0e246ea 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 个数据页,统计这些页面上的不同值得到一个平均值,然后乘以这个索引的页面数,就得到了这个索引的基数 @@ -5041,7 +5041,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 +5134,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 层还会做判断 @@ -7406,7 +7406,7 @@ InnoDB 会对间隙(GAP)进行加锁,就是间隙锁 (RR 隔离级别下 InnoDB 加锁的基本单位是 next-key lock,该锁是行锁和 gap lock 的组合(X or S 锁),但是加锁过程是分为间隙锁和行锁两段执行 -* 可以**保护当前记录和前面的间隙**,遵循左开右闭原则,单纯的是间隙锁左开右开 +* 可以**保护当前记录和前面的间隙**,遵循左开右闭原则,单纯的间隙锁是左开右开 * 假设有 10、11、13,那么可能的间隙锁包括:(负无穷,10]、(10,11]、(11,13]、(13,正无穷) 几种索引的加锁情况: diff --git a/Prog.md b/Prog.md index eac3448..9f62829 100644 --- a/Prog.md +++ b/Prog.md @@ -10758,7 +10758,7 @@ public class ExchangerDemo { // 创建交换对象(信使) Exchanger exchanger = new Exchanger(); new ThreadA(exchanger).start(); - new ThreadA(exchanger).start(); + new ThreadB(exchanger).start(); } } class ThreadA extends Thread{ diff --git a/SSM.md b/SSM.md index 6939b15..74a08e4 100644 --- a/SSM.md +++ b/SSM.md @@ -13890,7 +13890,7 @@ SpringBoot 功能: 1. 使用 @ComponentScan 扫描 com.example.config 包 -2. 使用 @Import 注解,加载类,这些类都会被 Spring 创建并放入 ioc 容器,默认组件的名字就是**全类名** +2. 使用 @Import 注解加载类,这些类都会被 Spring 创建并放入 ioc 容器,默认组件的名字就是**全类名** 3. 对 @Import 注解进行封装 @@ -13982,14 +13982,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 +14013,7 @@ ConditionContext 类API: } ``` -* UserConfig +* UserConfig: ```java @Configuration @@ -14202,7 +14202,7 @@ public class Car { -### 源码解析 +### 装配原理 #### 启动流程 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) - From 950d29ae2f5d04ee70a44faf677838ca143b4ad6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CSeazean=E2=80=9D?= Date: 2022年9月28日 17:13:12 +0800 Subject: [PATCH 25/35] Update Java Note --- DB.md | 110 +++++++++++++++++++++++++++++----------------------------- 1 file changed, 55 insertions(+), 55 deletions(-) diff --git a/DB.md b/DB.md index 0e246ea..206d7ee 100644 --- a/DB.md +++ b/DB.md @@ -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 版本状态,保证并发状态下的读写冲突问题 @@ -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)到磁盘,具体的**刷盘策略**: @@ -6876,9 +6872,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 +6915,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 +6927,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: @@ -7752,7 +7748,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 +7832,7 @@ coordinator 就是原来的 SQL Thread,并行复制中它不再直接更新数 * 线程分配完成并不是立即执行,为了防止造成更新覆盖,更新同一 DB 的两个事务必须被分发到同一个工作线程 * 同一个事务不能被拆开,必须放到同一个工作线程 -MySQL 5.6 版本的策略:每个线程对应一个 hash 表,用于保存当前这个线程的执行队列里的事务所涉及的表,hash 表的 key 是数据库 名,value 是一个数字,表示队列中有多少个事务修改这个库,适用于主库上有多个 DB 的情况 +MySQL 5.6 版本的策略:每个线程对应一个 hash 表,用于保存当前这个线程的执行队列里的事务所涉及的表,hash 表的 key 是数据库名,value 是一个数字,表示队列中有多少个事务修改这个库,适用于主库上有多个 DB 的情况 每个事务在分发的时候,跟线程的**冲突**(事务操作的是同一个库)关系包括以下三种情况: @@ -7846,7 +7842,7 @@ MySQL 5.6 版本的策略:每个线程对应一个 hash 表,用于保存当 优缺点: -* 构造 hash 值的时候很快,只需要库名,而且一个实例上 DB 数也不会很多,不会出现需要构造很多个项的情况 +* 构造 hash 值的时候很快,只需要库名,而且一个实例上 DB 数也不会很多,不会出现需要构造很多项的情况 * 不要求 binlog 的格式,statement 格式的 binlog 也可以很容易拿到库名(日志章节详解了 binlog) * 主库上的表都放在同一个 DB 里面,这个策略就没有效果了;或者不同 DB 的热点不同,比如一个是业务逻辑库,一个是系统配置库,那也起不到并行的效果,需要**把相同热度的表均匀分到这些不同的 DB 中**,才可以使用这个策略 @@ -7977,7 +7973,7 @@ SELECT master_pos_wait(file, pos[, timeout]); * 选定一个从库执行判断位点语句,如果返回值是>=0 的正整数,说明从库已经同步完事务,可以在这个从库执行查询语句 * 如果出现其他情况,需要到主库执行查询语句 -注意:如果所有的从库都延迟超过 timeout 秒,查询压力就都跑到主库上,所以需要进行权衡 +注意:如果所有的从库都延迟超过 timeout 秒,查询压力就都跑到主库上,所以需要进行权衡 @@ -9721,7 +9717,7 @@ Redis 基于 Reactor 模式开发了网络事件处理器,这个处理器被 -尽管多个文件事件可能会并发出现,但是 I/O 多路复用程序将所有产生事件的套接字处理请求放入一个**单线程的执行队列**中,通过队列有序、同步的向文件事件分派器传送套接字,上一个套接字产生的事件处理完后,才会继续向分派器传送下一个 +I/O 多路复用程序将所有产生事件的套接字处理请求放入一个**单线程的执行队列**中,通过队列有序、同步的向文件事件分派器传送套接字,上一个套接字产生的事件处理完后,才会继续向分派器传送下一个 @@ -11010,7 +11006,7 @@ load_factor = ht[0].used / ht[0].size 原因:执行该命令的过程中,Redis 需要创建当前服务器进程的子进程,而大多数操作系统都采用写时复制(copy-on­-write)技术来优化子进程的使用效率,通过提高执行扩展操作的负载因子,尽可能地避免在子进程存在期间进行哈希表扩展操作,可以避免不必要的内存写入操作,最大限度地节约内存 -哈希表执行收缩的条件:负载因子小于 0.1(自动执行,servreCron 中检测),缩小为字典中数据个数的 50% 左右 +哈希表执行收缩的条件:负载因子小于 0.1(自动执行,servreCron 中检测) @@ -11553,7 +11549,7 @@ Redis 所有操作都是**原子性**的,采用**单线程**机制,命令是 -#### 对象 +#### 实现 字符串对象的编码可以是 int、raw、embstr 三种 @@ -12354,7 +12350,7 @@ AOF:将数据的操作过程进行保存,日志形式,存储操作过程 #### 文件创建 -RDB 持久化功能所生成的 RDB 文件 是一个经过压缩的紧凑二进制文件,通过该文件可以还原生成 RDB 文件时的数据库状态,有两个 Redis 命令可以生成 RDB 文件,一个是 SAVE,另一个是 BGSAVE +RDB 持久化功能所生成的 RDB 文件是一个经过压缩的紧凑二进制文件,通过该文件可以还原生成 RDB 文件时的数据库状态,有两个 Redis 命令可以生成 RDB 文件,一个是 SAVE,另一个是 BGSAVE @@ -12758,7 +12754,7 @@ bgrewriteaof * 子进程进行 AOF 重写期间,服务器进程(父进程)可以继续处理命令请求 -* 子进程带有服务器进程的数据副本,使用子进程而不是线程,可以在避免使用锁的情况下, 保证数据的安全性 +* 子进程带有服务器进程的数据副本,使用子进程而不是线程,可以在避免使用锁的情况下, 保证数据安全性 ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-AOF手动重写原理.png) @@ -13495,7 +13491,7 @@ Redis 分布式锁的基本使用,悲观锁 `NX`:只在键不存在时,才对键进行设置操作,`SET key value NX` 效果等同于 `SETNX key value` - `XX` :只在键已经存在时,才对键进行设置操作 + `XX`:只在键已经存在时,才对键进行设置操作 `EX`:设置键 key 的过期时间,单位时秒 @@ -13633,7 +13629,7 @@ end 主从一致性:集群模式下,主从同步存在延迟,当加锁后主服务器宕机时,从服务器还没同步主服务器中的锁数据,此时从服务器升级为主服务器,其他线程又可以获取到锁 -将服务器升级为多主多从,: +将服务器升级为多主多从: * 获取锁需要从所有主服务器 SET 成功才算获取成功 * 某个 master 宕机,slave 还没有同步锁数据就升级为 master,其他线程尝试加锁会加锁失败,因为其他 master 上已经存在该锁 @@ -14007,7 +14003,7 @@ PSYNC 命令的调用方法有两种 #### 心跳机制 -心跳机制:进入命令传播阶段,从服务器默认会以每秒一次的频率,向主服务器发送命令:`REPLCONF ACK `,re_offset 是从服务器当前的复制偏移量 +心跳机制:进入命令传播阶段,**从服务器**默认会以每秒一次的频率,**向主服务器发送命令**:`REPLCONF ACK `,replication_offset 是从服务器当前的复制偏移量 心跳的作用: @@ -14046,7 +14042,7 @@ 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 两个选项可以防止主服务器在**不安全的情况下**拒绝执行写命令 比如向主服务器设置: @@ -14113,7 +14109,7 @@ master 的 CPU 占用过高或 slave 频繁断开连接 * 出现的原因: * slave 每 1 秒发送 REPLCONF ACK 命令到 master - * 当 slave 接到了慢查询时(keys * ,hgetall等),会大量占用 CPU 性能 + * 当 slave 接到了慢查询时(keys * ,hgetall 等),会大量占用 CPU 性能 * master 每 1 秒调用复制定时函数 replicationCron(),比对 slave 发现长时间没有进行响应 最终导致 master 各种资源(输出缓冲区、带宽、连接等)被严重占用 @@ -14277,7 +14273,7 @@ Sentinel 本质上只是一个运行在特殊模式下的 Redis 服务器,当 #### 代码替换 -将一部分普通 Redis服务器使用的代码替换成 Sentinel 专用代码 +将一部分普通 Redis 服务器使用的代码替换成 Sentinel 专用代码 Redis 服务器端口: @@ -14321,7 +14317,7 @@ struct sentinelState { // 当前纪元,用于实现故障转移 uint64_t current_epoch; - // 保存了所有被这个sentinel监视的主服务器 + // 【保存了所有被这个sentinel监视的主服务器】 dict *masters; // 是否进入了 TILT 模式 @@ -14438,10 +14434,10 @@ typedef struct sentinelAddr { ##### 主服务器 -Sentinel 默认会以每十秒一次的频率,通过命令连接向被监视的主服务器发送 INFO 命令,来获取主服务器的当前信息 +Sentinel 默认会以每十秒一次的频率,通过命令连接向被监视的主服务器发送 INFO 命令,来获取主服务器的信息 * 一部分是主服务器本身的信息,包括 runid 域记录的服务器运行 ID,以及 role 域记录的服务器角色 -* 另一部分是服务器属下所有从服务器的信息,每个从服务器都由一个 slave 字符串开头的行记录,根据这些 IP 地址和端口号,Sentinel 无须用户提供从服务器的地址信息,就可以自动发现从服务器 +* 另一部分是服务器属下所有从服务器的信息,每个从服务器都由一个 slave 字符串开头的行记录,根据这些 IP 地址和端口号,Sentinel 无须用户提供从服务器的地址信息,就可以**自动发现从服务器** ```sh # Server @@ -14470,7 +14466,7 @@ slave1: ip=l27.0.0.1, port=22222, state=online, offset=22, lag=0 ##### 从服务器 -当 Sentinel 发现主服务器有新的从服务器出现时,会为这个新的从服务器创建相应的实例结构,还会创建到从服务器的命令连接和订阅连接,所以 Sentinel 对所有的从服务器之间都可以进行命令操作 +当 Sentinel 发现主服务器有新的从服务器出现时,会为这个新的从服务器创建相应的实例结构,还会**创建到从服务器的命令连接和订阅连接**,所以 Sentinel 对所有的从服务器之间都可以进行命令操作 Sentinel 默认会以每十秒一次的频率,向从服务器发送 INFO 命令: @@ -14614,7 +14610,7 @@ SENTINEL is-master-down-by-addr 源 Sentinel 将统计其他 Sentinel 同意主服务器已下线的数量,当这一数量达到配置指定的判断客观下线所需的数量(quorum)时,Sentinel 会将主服务器对应实例结构 flags 属性的 SRI_O_DOWN 标识打开,代表客观下线,并对主服务器执行故障转移操作 -注意:**不同 Sentinel 判断客观下线的条件可能不同**,因为载入的配置文件中的属性(quorum)可能不同 +注意:**不同 Sentinel 判断客观下线的条件可能不同**,因为载入的配置文件中的属性 quorum 可能不同 @@ -14633,7 +14629,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` 票 选举过程: @@ -14641,7 +14637,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 个节点的原因: @@ -14649,7 +14645,10 @@ Sentinel 集群至少 3 个节点的原因: * 如果 Sentinel 集群只有 2 个 Sentinel 节点,则领头选举需要 `2/2 + 1 = 2` 票,如果一个节点挂了,那就永远选不出领头 * Sentinel 集群允许 1 个 Sentinel 节点故障则需要 3 个节点的集群,允许 2 个节点故障则需要 5 个节点集群 +**如何获取哨兵节点的半数数量**? +* 客观下线是通过配置文件获取的数量,达到 quorum 就客观下线 +* 哨兵数量是通过主节点是实例结构中,保存着监视该主节点的所有哨兵信息,从而获取得到 @@ -14748,7 +14747,8 @@ typedef struct clusterState { // 集群当前的状态,是在线还是下线 int state; - // 集群中至少处理着一个槽的节点的数量,为0表示集群目前没有任何节点在处理槽 + // 集群中至少处理着一个槽的(主)节点的数量,为0表示集群目前没有任何节点在处理槽 + // 【选举时投票数量超过半数,可以从这里获取的】 int size; // 集群节点名单(包括 myself 节点),字典的键为节点的名字,字典的值为节点对应的clusterNode结构 @@ -14844,7 +14844,7 @@ CLUSTER MEET #### 基本操作 -Redis 集群通过分片的方式来保存数据库中的键值对,集群的整个数据库被分为16384 个槽(slot),数据库中的每个键都属于 16384 个槽中的一个,集群中的每个节点可以处理 0 个或最多 16384 个槽(**每个主节点存储的数据并不一样**) +Redis 集群通过分片的方式来保存数据库中的键值对,集群的整个数据库被分为 16384 个槽(slot),数据库中的每个键都属于 16384 个槽中的一个,集群中的每个节点可以处理 0 个或最多 16384 个槽(**每个主节点存储的数据并不一样**) * 当数据库中的 16384 个槽都有节点在处理时,集群处于上线状态(ok) * 如果数据库中有任何一个槽得到处理,那么集群处于下线状态(fail) @@ -14929,7 +14929,7 @@ typedef struct clusterState { #### 集群数据 -集群节点保存键值对以及键值对过期时间的方式,与单机 Redis 服务器保存键值对以及键值对过期时间的方式完全相同,但是集群节点只能使用 0 号数据库,单机服务器可以任意使用 +集群节点保存键值对以及键值对过期时间的方式,与单机 Redis 服务器保存键值对以及键值对过期时间的方式完全相同,但是**集群节点只能使用 0 号数据库**,单机服务器可以任意使用 除了将键值对保存在数据库里面之外,节点还会用 clusterState 结构中的 slots_to_keys 跳跃表来**保存槽和键之间的关系** @@ -15189,7 +15189,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 链表里面 @@ -15456,7 +15456,7 @@ typedef struct clusterMsgDataPublish { ### 脑裂问题 -脑裂指在主从集群中,同时有两个相同的主节点能接收写请求,导致客户端不知道应该往哪个主节点写入数据,导致不同客户端往不同的主节点上写入数据 +脑裂指在主从集群中,同时有两个相同的主节点能接收写请求,导致客户端不知道应该往哪个主节点写入数据,最后 不同客户端往不同的主节点上写入数据 * 原主节点并没有真的发生故障,由于某些原因无法处理请求(CPU 利用率很高、自身阻塞),无法按时响应心跳请求,被哨兵/集群主节点错误的判断为下线 * 在被判断下线之后,原主库又重新开始处理请求了,哨兵/集群主节点还没有完成主从切换,客户端仍然可以和原主库通信,客户端发送的写操作就会在原主库上写入数据,造成脑裂问题 @@ -15825,7 +15825,7 @@ OK Redis 的管道 Pipeline 机制可以一次处理多条指令 * Pipeline 中的多条命令非原子性,因为在向管道内添加命令时,其他客户端的发送的命令仍然在执行 -* 原生批命令(mset 等)是服务端实现,而 pipeline 需要服务端与客户端共同完成 +* 原生批命令(MSET 等)是服务端实现,而 Pipeline 需要服务端与客户端共同完成 使用 Pipeline 封装的命令数量不能太多,数据量过大会增加客户端的等待时间,造成网络阻塞,Jedis 中的 Pipeline 使用方式: From c6015094c3b9dac260f5a264ab7f83b16b4179bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CSeazean=E2=80=9D?= Date: 2022年10月21日 13:20:36 +0800 Subject: [PATCH 26/35] Update Java Note --- Frame.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Frame.md b/Frame.md index 9c7fdaa..2f9f73b 100644 --- a/Frame.md +++ b/Frame.md @@ -3485,7 +3485,7 @@ Kafka 比 RocketMQ 吞吐量高: Topic 的 partition 数量过多时,Kafka 的性能不如 RocketMQ: -* 两者都使用文件存储,但是 Kafka 是一个分区一个文件,Topic 过多时分区的总量也会增加,过多的文件导致对消息刷盘时出现文件竞争磁盘,造成性能的下降。一个分区只能被一个消费组中的一个消费线程进行消费,因此可以同时消费的消费端也比较少 +* 两者都使用文件存储,但是 Kafka 是一个分区一个文件,Topic 过多时分区的总量也会增加,过多的文件导致对消息刷盘时出现文件竞争磁盘,造成性能的下降。**一个分区只能被一个消费组中的一个消费线程进行消费**,因此可以同时消费的消费端也比较少 * RocketMQ 所有队列都存储在一个文件中,每个队列存储的消息量也比较小,因此多 Topic 的对 RocketMQ 的性能的影响较小 @@ -4856,7 +4856,7 @@ Consumer 端实现负载均衡的核心类 **RebalanceImpl** 对比下 RebalancePushImpl 和 RebalancePullImpl 两个实现类的 dispatchPullRequest() 方法,RebalancePullImpl 类里面的该方法为空 -消息消费队列在同一消费组不同消费者之间的负载均衡,其核心设计理念是在**一个消息消费队列在同一时间只允许被同一消费组内的一个消费者消费,一个消息消费者能同时消费多个消息队列** +消息消费队列在**同一消费组不同消费者之间的负载均衡**,其核心设计理念是在**一个消息消费队列在同一时间只允许被同一消费组内的一个消费者消费,一个消息消费者能同时消费多个消息队列** From 2a238b78a48c9cd00045ec64cb7cbb5b706020cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CSeazean=E2=80=9D?= Date: 2022年11月17日 09:36:02 +0800 Subject: [PATCH 27/35] Update Java Note --- DB.md | 23 +++++++++++++++++------ Java.md | 44 ++++++++++++++++---------------------------- 2 files changed, 33 insertions(+), 34 deletions(-) diff --git a/DB.md b/DB.md index 206d7ee..9a506de 100644 --- a/DB.md +++ b/DB.md @@ -6776,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 对应的修改已经持久化到磁盘 @@ -10795,7 +10794,7 @@ SDS 通过未使用空间解除了字符串长度和底层数组长度之间的 内存重分配涉及复杂的算法,需要执行**系统调用**,是一个比较耗时的操作,SDS 的两种优化策略: -* 空间预分配:当 SDS需要进行空间扩展时,程序不仅会为 SDS 分配修改所必需的空间, 还会为 SDS 分配额外的未使用空间 +* 空间预分配:当 SDS 需要进行空间扩展时,程序不仅会为 SDS 分配修改所必需的空间, 还会为 SDS 分配额外的未使用空间 * 对 SDS 修改之后,SDS 的长度(len 属性)小于 1MB,程序分配和 len 属性同样大小的未使用空间,此时 len 和 free 相等 @@ -12096,6 +12095,11 @@ set 类型:与 hash 存储结构哈希表完全相同,只是仅存储键不 当元素比较多时,此时 ziplist 的读写效率会下降,时间复杂度是 O(n),跳表的时间复杂度是 O(logn) +为什么用跳表而不用平衡树? + +* 在做范围查找的时候,跳表操作简单(前进指针或后退指针),平衡树需要回旋查找 +* 跳表比平衡树实现简单,平衡树的插入和删除操作可能引发子树的旋转调整,而跳表的插入和删除只需要修改相邻节点的指针 + *** @@ -12651,12 +12655,12 @@ appendfsync always|everysec|no #AOF写数据策略:默认为everysec 特点:安全性最高,数据零误差,但是性能较低,不建议使用 -- everysec:先将 aof_buf 缓冲区中的内容写入到 AOF 文件,判断上次同步 AOF 文件的时间距离现在超过一秒钟,再次对 AOF 文件进行同步 fsync,这个同步操作是由一个(子)线程专门负责执行的 +- everysec:先将 aof_buf 缓冲区中的内容写入到操作系统缓存,判断上次同步 AOF 文件的时间距离现在超过一秒钟,再次进行同步 fsync,这个同步操作是由一个(子)线程专门负责执行的 特点:在系统突然宕机的情况下丢失 1 秒内的数据,准确性较高,性能较高,建议使用,也是默认配置 -- no:将 aof_buf 缓冲区中的内容写入到 AOF 文件,但并不对 AOF 文件进行同步,何时同步由操作系统来决定 +- no:将 aof_buf 缓冲区中的内容写入到操作系统缓存,但并不进行同步,何时同步由操作系统来决定 特点:**整体不可控**,服务器宕机会丢失上次同步 AOF 后的所有写指令 @@ -14620,7 +14624,7 @@ SENTINEL is-master-down-by-addr ### 领头选举 -主服务器被判断为客观下线时,监视这个主服务器的各个 Sentinel 会进行协商,选举出一个领头 Sentinel 对下线服务器执行故障转移 +主服务器被判断为客观下线时,**监视该主服务器的各个 Sentinel 会进行协商**,选举出一个领头 Sentinel 对下线服务器执行故障转移 Redis 选举领头 Sentinel 的规则: @@ -14748,7 +14752,7 @@ typedef struct clusterState { int state; // 集群中至少处理着一个槽的(主)节点的数量,为0表示集群目前没有任何节点在处理槽 - // 【选举时投票数量超过半数,可以从这里获取的】 + // 【选举时投票数量超过半数,从这里获取的】 int size; // 集群节点名单(包括 myself 节点),字典的键为节点的名字,字典的值为节点对应的clusterNode结构 @@ -16135,6 +16139,13 @@ Read-Through Pattern 也存在首次不命中的问题,采用缓存预热解 - 在抢购或秒杀场景下,可能因商品对应库存 Key 的请求量过大,超出 Redis 处理能力造成超卖 - 热 Key 的请求压力数量超出 Redis 的承受能力易造成缓存击穿,即大量请求将被直接指向后端的存储层,导致存储访问量激增甚至宕机,从而影响其他业务 +热 Key 分类两种,治理方式如下: + +* 一种是单一数据,比如秒杀场景,假设总量 10000 可以拆为多个 Key 进行访问,每次对请求进行路由到不同的 Key 访问,保证最终一致性,但是会出现访问不同 Key 产生的剩余量是不同的,这时可以通过前端进行 Mock 假数据 +* 一种是多数据集合,比如进行 ID 过滤,这时可以添加本地 LRU 缓存,减少对热 Key 的访问 + + + 参考文档:https://help.aliyun.com/document_detail/353223.html diff --git a/Java.md b/Java.md index be64732..431d0a7 100644 --- a/Java.md +++ b/Java.md @@ -11497,7 +11497,7 @@ Java 语言提供了对象终止(finalization)机制来允许开发人员提 - 主要不足是**只使用了内存的一半** - 对于 G1 这种分拆成为大量 region 的 GC,复制而不是移动,意味着 GC 需要维护 region 之间对象引用关系,不管是内存占用或者时间开销都不小 -现在的商业虚拟机都采用这种收集算法**回收新生代**,但是并不是划分为大小相等的两块,而是一块较大的 Eden 空间和两块较小的 Survivor 空间 +现在的商业虚拟机都采用这种收集算法**回收新生代**,因为新生代 GC 频繁并且对象的存活率不高,但是并不是划分为大小相等的两块,而是一块较大的 Eden 空间和两块较小的 Survivor 空间 @@ -11713,7 +11713,7 @@ CMS 收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿 - 初始标记:使用 STW 出现短暂停顿,仅标记一下 GC Roots 能直接关联到的对象,速度很快 - 并发标记:进行 GC Roots 开始遍历整个对象图,在整个回收过程中耗时最长,不需要 STW,可以与用户线程并发运行 -- 重新标记:修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,比初始标记时间长但远比并发标记时间短,需要 STW(不停顿就会一直变化,采用写屏障 + 增量更新来避免漏标情况) +- 重新标记:修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象,比初始标记时间长但远比并发标记时间短,需要 STW(不停顿就会一直变化,采用写屏障 + 增量更新来避免漏标情况) - 并发清除:清除标记为可以回收对象,**不需要移动存活对象**,所以这个阶段可以与用户线程同时并发的 Mark Sweep 会造成内存碎片,不把算法换成 Mark Compact 的原因:Mark Compact 算法会整理内存,导致用户线程使用的**对象的地址改变**,影响用户线程继续执行 @@ -11785,7 +11785,7 @@ G1 对比其他处理器的优点: - 空间整合: - CMS:标记-清除算法、内存碎片、若干次 GC 后进行一次碎片整理 - - G1:整体来看是基于标记 - 整理算法实现的收集器,从局部(Region 之间)上来看是基于复制算法实现的,两种算法都可以避免内存碎片 + - G1:整体来看是**基于标记 - 整理算法实现**的收集器,从局部(Region 之间)上来看是基于复制算法实现的,两种算法都可以避免内存碎片 - **可预测的停顿时间模型(软实时 soft real-time)**:可以指定在 M 毫秒的时间片段内,消耗在 GC 上的时间不得超过 N 毫秒 @@ -12544,7 +12544,7 @@ Java 对象创建时机: - 通过类的完全限定名称获取定义该类的二进制字节流(二进制字节码) - 将该字节流表示的静态存储结构转换为方法区的运行时存储结构(Java 类模型) -- **在内存中生成一个代表该类的 Class 对象,作为该类在方法区中的各种数据的访问入口** +- **将字节码文件加载至方法区后,在堆中生成一个代表该类的 Class 对象,作为该类在方法区中的各种数据的访问入口** 其中二进制字节流可以从以下方式中获取: @@ -12553,8 +12553,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 使用 @@ -12641,7 +12639,7 @@ Java 对象创建时机: public static final int value = 123; ``` -* Java 并不支持 boolean 类型,对于 boolean 类型,内部实现是 int,由于 int 的默认值是0,故 boolean 的默认值就是 false +* Java 并不支持 boolean 类型,对于 boolean 类型,内部实现是 int,由于 int 的默认值是 0,故 boolean 的默认值就是 false @@ -12794,7 +12792,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 来加载类 @@ -14471,14 +14469,9 @@ public static int invoke(Object... args) { 在 JVM 中,将符号引用转换为直接引用有两种机制: - 静态链接:当一个字节码文件被装载进 JVM 内部时,如果被调用的目标方法在编译期可知,且运行期保持不变,将调用方法的符号引用转换为直接引用的过程称之为静态链接(类加载的解析阶段) -- 动态链接:如果被调用的方法在编译期无法被确定下来,只能够在程序运行期将调用方法的符号引用转换为直接引用,由于这种引用转换过程具备动态性,因此被称为动态链接(初始化后的解析阶段) - -对应方法的绑定(分配)机制:静态绑定和动态绑定。绑定是一个字段、方法或者类从符号引用被替换为直接引用的过程,仅发生一次: +- 动态链接:被调用的方法在编译期无法被确定下来,只能在程序运行期将调用方法的符号引用转换为直接引用,由于这种引用转换过程具备动态性,因此被称为动态链接(初始化后的解析阶段) -- 静态绑定:被调用的目标方法在编译期可知,且运行期保持不变,将这个方法与所属的类型进行绑定 -- 动态绑定:被调用的目标方法在编译期无法确定,只能在程序运行期根据实际的类型绑定相关的方法 - -* Java 编译器已经区分了重载的方法(静态绑定和动态绑定),因此可以认为虚拟机中不存在重载 +* 对应方法的绑定(分配)机制:静态绑定和动态绑定,编译器已经区分了重载的方法(静态绑定和动态绑定),因此可以认为虚拟机中不存在重载 非虚方法: @@ -14512,7 +14505,7 @@ public static int invoke(Object... args) { 普通调用指令: - invokestatic:调用静态方法 -- invokespecial:调用私有实例方法、构造器,和父类的实例方法或构造器,以及所实现接口的默认方法 +- invokespecial:调用私有方法、构造器,和父类的实例方法或构造器,以及所实现接口的默认方法 - invokevirtual:调用所有虚方法(虚方法分派) - invokeinterface:调用接口方法 @@ -14544,9 +14537,6 @@ public static int invoke(Object... args) { 在编译过程中,虚拟机并不知道目标方法的具体内存地址,Java 编译器会暂时用符号引用来表示该目标方法,这一符号引用包括目标方法所在的类或接口的名字,以及目标方法的方法名和方法描述符 -* 对于静态绑定的方法调用而言,实际引用是一个指向方法的指针 -* 对于需要动态绑定的方法调用而言,实际引用则是一个方法表的索引 - 符号引用存储在方法区常量池中,根据目标方法是否为接口方法,分为接口符号引用和非接口符号引用: ```java @@ -14648,8 +14638,6 @@ Java 虚拟机中关于方法重写的判定基于方法描述符,如果子类 2. 如果在类型 C 中找到与描述符和名称都相符的方法,则进行访问**权限校验**(私有的),如果通过则返回这个方法的直接引用,查找过程结束;如果不通过,则返回 java.lang.IllegalAccessError 异常 - IllegalAccessError:表示程序试图访问或修改一个属性或调用一个方法,这个属性或方法没有权限访问,一般会引起编译器异常。如果这个错误发生在运行时,就说明一个类发生了不兼容的改变 - 3. 找不到,就会按照继承关系从下往上依次对 C 的各个父类进行第二步的搜索和验证过程 4. 如果始终没有找到合适的方法,则抛出 java.lang.AbstractMethodError 异常 @@ -14673,15 +14661,15 @@ Java 虚拟机中关于方法重写的判定基于方法描述符,如果子类 虚方法表的执行过程: -* 对于静态绑定的方法调用而言,实际引用将指向具体的目标方法 -* 对于动态绑定的方法调用而言,实际引用则是方法表的索引值,也就是方法的间接地址。Java 虚拟机获取调用者的实际类型,并在该实际类型的虚方法表中,根据索引值获得目标方法内存偏移量(指针) +* 对于静态绑定的方法调用而言,实际引用是一个指向方法的指针 +* 对于动态绑定的方法调用而言,实际引用是方法表的索引值,也就是方法的间接地址。Java 虚拟机获取调用者的实际类型,并在该实际类型的虚方法表中,根据索引值获得目标方法内存偏移量(指针) 为了优化对象调用方法的速度,方法区的类型信息会增加一个指针,该指针指向一个记录该类方法的方法表。每个类中都有一个虚方法表,本质上是一个数组,每个数组元素指向一个当前类及其祖先类中非私有的实例方法 方法表满足以下的特质: * 其一,子类方法表中包含父类方法表中的**所有方法**,并且在方法表中的索引值与父类方法表种的索引值相同 -* 其二,**非重写的方法指向父类的方法表项,与父类共享一个方法表项,重写的方法指向本身自己的实现**。所以这就是为什么多态情况下可以访问父类的方法。 +* 其二,**非重写的方法指向父类的方法表项,与父类共享一个方法表项,重写的方法指向本身自己的实现**,这就是为什么多态情况下可以访问父类的方法。 From 2783b5288cc72f3ff37a33aea0ba3ea4f093d488 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CSeazean=E2=80=9D?= Date: 2022年11月20日 16:54:28 +0800 Subject: [PATCH 28/35] Update Java Note --- Prog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Prog.md b/Prog.md index 9f62829..cd8fd35 100644 --- a/Prog.md +++ b/Prog.md @@ -5650,7 +5650,7 @@ ExecutorService 类 API: | 方法 | 说明 | | ----------------------------------------------------- | ------------------------------------------------------------ | -| void shutdown() | 线程池状态变为 SHUTDOWN,等待任务执行完后关闭线程池,不会接收新任务,但已提交任务会执行完,而且也可以添加线程(不绑定ren'wu) | +| void shutdown() | 线程池状态变为 SHUTDOWN,等待任务执行完后关闭线程池,不会接收新任务,但已提交任务会执行完,而且也可以添加线程(不绑定任务) | | List shutdownNow() | 线程池状态变为 STOP,用 interrupt 中断正在执行的任务,直接关闭线程池,不会接收新任务,会将队列中的任务返回 | | boolean isShutdown() | 不在 RUNNING 状态的线程池,此执行者已被关闭,方法返回 true | | boolean isTerminated() | 线程池状态是否是 TERMINATED,如果所有任务在关闭后完成,返回 true | From 32a34ec06463f38d2a0cd1e05f0fe9e5f0835663 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CSeazean=E2=80=9D?= Date: 2022年11月25日 17:23:41 +0800 Subject: [PATCH 29/35] Update README --- README.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/README.md b/README.md index 6083f8e..19fad3f 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,3 @@ * 推荐使用 Typora 阅读笔记,打开目录栏效果更佳。 * 所有的知识不保证权威性,如果各位朋友发现错误,欢迎与我讨论。 * 笔记的编写基于 Windows 平台,可能会因为平台的不同而造成空格、制表符的显示效果不同。 - -字节跳动校园招聘:https://jobs.bytedance.com/campus/invite?referral_code=1VQUWCD - -![](https://seazean.oss-cn-beijing.aliyuncs.com/bytedance.jpg) From 45d4cf9a7ea3c9b79c1aea26dd432646d021d51b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CSeazean=E2=80=9D?= Date: Mon, 9 Jan 2023 11:36:59 +0800 Subject: [PATCH 30/35] Update Java Note --- DB.md | 2 +- Frame.md | 11 ++- Java.md | 293 ++----------------------------------------------------- Prog.md | 14 ++- 4 files changed, 25 insertions(+), 295 deletions(-) diff --git a/DB.md b/DB.md index 9a506de..7fd57da 100644 --- a/DB.md +++ b/DB.md @@ -15257,7 +15257,7 @@ struct clusterNodeFailReport { -选举新主节点的方法和选举领头 Sentinel 的方法非常相似,两者都是基于 Raft 算法的领头选举(eader election)方法实现的 +选举新主节点的方法和选举领头 Sentinel 的方法非常相似,两者都是基于 Raft 算法的领头选举(leader election)方法实现的 diff --git a/Frame.md b/Frame.md index 2f9f73b..d655130 100644 --- a/Frame.md +++ b/Frame.md @@ -739,9 +739,13 @@ Maven 的插件用来执行生命周期中的相关事件 ### 继承 -作用:通过继承可以实现在子工程中沿用父工程中的配置 +Maven 中的继承与 Java 中的继承相似,可以实现在子工程中沿用父工程中的配置 -- Maven 中的继承与 Java 中的继承相似,在子工程中配置继承关系 +dependencyManagement 里只是声明依赖,并不实现引入,所以子工程需要显式声明需要用的依赖 + +- 如果子工程中未声明依赖,则不会从父项目继承下来 +- 在子工程中声明该依赖项,并且不指定具体版本,才会从父项目中继承该项,version 和 scope 都继承取自父工程 pom 文件 +- 如果子工程中指定了版本号,那么使用子工程中指定的 jar 版本 制作方式: @@ -10268,13 +10272,14 @@ 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 回调对象,将正常拉取的消息**提交到消费任务线程池**,并设置请求的下一次拉取位点,重新放入阻塞队列,形成闭环 * 消费任务服务对消费失败的消息进行回退,通过内部生产者实例发送回退消息,回退失败的消息会再次提交消费任务重新消费 diff --git a/Java.md b/Java.md index 431d0a7..6592504 100644 --- a/Java.md +++ b/Java.md @@ -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 - ``` -**** - - - -##### 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 - - - - - 张三 - 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"); diff --git a/Prog.md b/Prog.md index cd8fd35..acd32d1 100644 --- a/Prog.md +++ b/Prog.md @@ -2539,9 +2539,7 @@ volatile 修饰的变量,可以禁用指令重排 ##### 缓存一致 -使用 volatile 修饰的共享变量,总线会开启 **CPU 总线嗅探机制**来解决 JMM 缓存一致性问题,也就是共享变量在多线程中可见性的问题,实现 MESI 缓存一致性协议 - -底层是通过汇编 lock 前缀指令,共享变量加了 lock 前缀指令就会进行缓存锁定,在线程修改完共享变量后写回主存,其他的 CPU 核心上运行的线程根据总线嗅探机制会修改其共享变量为失效状态,读取时会重新从主内存中读取最新的数据 +使用 volatile 修饰的共享变量,底层通过汇编 lock 前缀指令进行缓存锁定,在线程修改完共享变量后写回主存,其他的 CPU 核心上运行的线程通过 CPU 总线嗅探机制会修改其共享变量为失效状态,读取时会重新从主内存中读取最新的数据 lock 前缀指令就相当于内存屏障,Memory Barrier(Memory Fence) @@ -4424,9 +4422,9 @@ ThreadLocal 内部解决方法:在 ThreadLocalMap 中的 set/getEntry 方法 ##### 基本使用 -父子线程:**创建子线程的线程是父线程**,比如实例中的 main 线程就是父线程 +父子线程:创建子线程的线程是父线程,比如实例中的 main 线程就是父线程 -ThreadLocal 中存储的是线程的局部变量,如果想实现线程间局部变量传递可以使用 InheritableThreadLocal 类 +ThreadLocal 中存储的是线程的局部变量,如果想**实现线程间局部变量传递**可以使用 InheritableThreadLocal 类 ```java public static void main(String[] args) { @@ -8251,7 +8249,7 @@ public void lock() { } ``` -* 接下来进入 addWaiter 逻辑,构造 Node 队列,前置条件是当前线程获取锁失败,说明有线程占用了锁 +* 接下来进入 addWaiter 逻辑,构造 Node 队列(不是阻塞队列),前置条件是当前线程获取锁失败,说明有线程占用了锁 * 图中黄色三角表示该 Node 的 waitStatus 状态,其中 0 为默认**正常状态** * Node 的创建是懒惰的,其中第一个 Node 称为 **Dummy(哑元)或哨兵**,用来占位,并不关联线程 @@ -8262,7 +8260,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 +8303,7 @@ public void lock() { -* 线程节点加入阻塞队列成功,进入 AbstractQueuedSynchronizer#acquireQueued 逻辑阻塞线程 +* 线程节点加入队列成功,进入 AbstractQueuedSynchronizer#acquireQueued 逻辑阻塞线程 * acquireQueued 会在一个自旋中不断尝试获得锁,失败后进入 park 阻塞 From 8bd68a038a169e0b95e09f63a6561e22c2855c2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CSeazean=E2=80=9D?= Date: Fri, 3 Mar 2023 13:48:10 +0800 Subject: [PATCH 31/35] Update Java Note --- Java.md | 2 +- Prog.md | 5 ++++- SSM.md | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Java.md b/Java.md index 6592504..be5e74c 100644 --- a/Java.md +++ b/Java.md @@ -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` (左闭右开),伪随机地生成并返回 diff --git a/Prog.md b/Prog.md index acd32d1..21a474b 100644 --- a/Prog.md +++ b/Prog.md @@ -3593,7 +3593,10 @@ public class TestFinal { final 变量的赋值通过 putfield 指令来完成,在这条指令之后也会加入写屏障,保证在其它线程读到它的值时不会出现为 0 的情况 -其他线程访问 final 修饰的变量**会复制一份放入栈中**,效率更高 +其他线程访问 final 修饰的变量 + +* **复制一份放入栈中**直接访问,效率高 +* 大于 short 最大值会将其复制到类的常量池,访问时从常量池获取 diff --git a/SSM.md b/SSM.md index 74a08e4..b91d349 100644 --- a/SSM.md +++ b/SSM.md @@ -14632,7 +14632,7 @@ ApplicationContextInitializer、SpringApplicationRunListener、CommandLineRunner #### 文件类型 -SpringBoot 是基于约定的,很多配置都有默认值,如果想使用自己的配置替换默认配置,可以使用 application.properties 或者application.yml(application.yaml)进行配置 +SpringBoot 是基于约定的,很多配置都有默认值,如果想使用自己的配置替换默认配置,可以使用 application.properties 或者 application.yml(application.yaml)进行配置 * 默认配置文件名称:application * 在同一级目录下优先级为:properties> yml> yaml From 528bcf2c8f0bb7d963010168eb39f687653fba88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CSeazean=E2=80=9D?= Date: 2023年3月23日 23:02:55 +0800 Subject: [PATCH 32/35] Update README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 19fad3f..31eeb49 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,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 From 92420ac014840c2ba8023a79b16901aee4cbe1cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CSeazean=E2=80=9D?= Date: 2023年3月23日 23:03:15 +0800 Subject: [PATCH 33/35] Update Java Note --- Frame.md | 2 +- Java.md | 2 +- Prog.md | 2 +- SSM.md | 3373 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 3343 insertions(+), 36 deletions(-) diff --git a/Frame.md b/Frame.md index d655130..f00ae48 100644 --- a/Frame.md +++ b/Frame.md @@ -10776,7 +10776,7 @@ CAP 理论指的是在一个分布式系统中,Consistency(一致性)、Av CAP 三个基本需求,因为 P 是必须的,因此分布式系统选择就在 CP 或者 AP 中: * 一致性:指数据在多个副本之间是否能够保持数据一致的特性,当一个系统在数据一致的状态下执行更新操作后,也能保证系统的数据仍然处于一致的状态 -* 可用性:指系统提供的服务必须一直处于可用的状态,对于用户的每一个操作请求总是能够在有限的时间内返回结果 +* 可用性:指系统提供的服务必须一直处于可用的状态,即使集群中一部分节点故障,对于用户的每一个操作请求总是能够在有限的时间内返回结果 * 分区容错性:分布式系统在遇到任何网络分区故障时,仍然能够保证对外提供服务,不会宕机,除非是整个网络环境都发生了故障 diff --git a/Java.md b/Java.md index be5e74c..df12275 100644 --- a/Java.md +++ b/Java.md @@ -430,7 +430,7 @@ public static void main(String[] args) { | 堆内存 | 存储对象或者数组,new 来创建的,都存储在堆内存 | | 方法栈 | 方法运行时使用的内存,比如 main 方法运行,进入方法栈中执行 | -**内存分配图**:Java 内存分配 +内存分配图:**Java 数组分配在堆内存** * 一个数组内存图 diff --git a/Prog.md b/Prog.md index 21a474b..2aee6d0 100644 --- a/Prog.md +++ b/Prog.md @@ -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毫秒后结束等待,或是被唤醒 ``` diff --git a/SSM.md b/SSM.md index b91d349..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 + + + *** @@ -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月07日T20: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\Desktop123円\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月28日T19: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月10日T16: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月02日T17:45:06.206+08:00[Asia/Shanghai],2022年03月25日T18: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 + + + + + + + + + + + + From 323c4c39f5ae9398088ecabbd6e6e72e80f20ce8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CSeazean=E2=80=9D?= Date: Fri, 7 Apr 2023 11:39:44 +0800 Subject: [PATCH 34/35] Update Java Note --- Java.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Java.md b/Java.md index df12275..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) From 5721d65d367c2ecdba3156f84ab73a69ee435019 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CSeazean=E2=80=9D?= Date: 2023年9月26日 13:15:03 +0800 Subject: [PATCH 35/35] add recruitment info --- README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/README.md b/README.md index 31eeb49..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

    AltStyle によって変換されたページ (->オリジナル) /