mirror of
https://github.com/Snailclimb/JavaGuide
synced 2025-08-01 16:28:03 +08:00
Compare commits
8 Commits
82aa276bd2
...
d02465ae34
Author | SHA1 | Date | |
---|---|---|---|
|
d02465ae34 | ||
|
f61c9e3ea4 | ||
|
aabe1ea380 | ||
|
00f612ee12 | ||
|
d5eb5f4ce2 | ||
|
790f10dc1e | ||
|
9b8d1e3d35 | ||
|
a349232896 |
@ -13,12 +13,21 @@ tag:
|
||||
|
||||
建立一个 TCP 连接需要“三次握手”,缺一不可:
|
||||
|
||||
- **一次握手**:客户端发送带有 SYN(SEQ=x) 标志的数据包 -> 服务端,然后客户端进入 **SYN_SEND** 状态,等待服务器的确认;
|
||||
- **二次握手**:服务端发送带有 SYN+ACK(SEQ=y,ACK=x+1) 标志的数据包 –> 客户端,然后服务端进入 **SYN_RECV** 状态
|
||||
- **三次握手**:客户端发送带有 ACK(ACK=y+1) 标志的数据包 –> 服务端,然后客户端和服务器端都进入**ESTABLISHED** 状态,完成 TCP 三次握手。
|
||||
- **一次握手**:客户端发送带有 SYN(SEQ=x) 标志的数据包 -> 服务端,然后客户端进入 **SYN_SEND** 状态,等待服务端的确认;
|
||||
- **二次握手**:服务端发送带有 SYN+ACK(SEQ=y,ACK=x+1) 标志的数据包 –> 客户端,然后服务端进入 **SYN_RECV** 状态;
|
||||
- **三次握手**:客户端发送带有 ACK(ACK=y+1) 标志的数据包 –> 服务端,然后客户端和服务端都进入**ESTABLISHED** 状态,完成 TCP 三次握手。
|
||||
|
||||
当建立了 3 次握手之后,客户端和服务端就可以传输数据啦!
|
||||
|
||||
### 什么是半连接队列和全连接队列?
|
||||
|
||||
在 TCP 三次握手过程中,Linux 内核会维护两个队列来管理连接请求:
|
||||
|
||||
1. **半连接队列**(也称 SYN Queue):当服务端收到客户端的 SYN 请求时,此时双方还没有完全建立连接,它会把半连接状态的连接放在半连接队列。
|
||||
2. **全连接队列**(也称 Accept Queue):当服务端收到客户端对 ACK 响应时,意味着三次握手成功完成,服务端会将该连接从半连接队列移动到全连接队列。如果未收到客户端的 ACK 响应,会进行重传,重传的等待时间通常是指数增长的。如果重传次数超过系统规定的最大重传次数,系统将从半连接队列中删除该连接信息。
|
||||
|
||||
这两个队列的存在是为了处理并发连接请求,确保服务端能够有效地管理新的连接请求。另外,新的连接请求被拒绝或忽略除了和每个队列的大小限制有关系之外,还和很多其他因素有关系,这里就不详细介绍了,整体逻辑比较复杂。
|
||||
|
||||
### 为什么要三次握手?
|
||||
|
||||
三次握手的目的是建立可靠的通信信道,说到通讯,简单来说就是数据的发送与接收,而三次握手最主要的目的就是双方确认自己与对方的发送与接收是正常的。
|
||||
@ -35,7 +44,13 @@ tag:
|
||||
|
||||
服务端传回发送端所发送的 ACK 是为了告诉客户端:“我接收到的信息确实就是你所发送的信号了”,这表明从客户端到服务端的通信是正常的。回传 SYN 则是为了建立并确认从服务端到客户端的通信。
|
||||
|
||||
> SYN 同步序列编号(Synchronize Sequence Numbers) 是 TCP/IP 建立连接时使用的握手信号。在客户机和服务器之间建立正常的 TCP 网络连接时,客户机首先发出一个 SYN 消息,服务器使用 SYN-ACK 应答表示接收到了这个消息,最后客户机再以 ACK(Acknowledgement)消息响应。这样在客户机和服务器之间才能建立起可靠的 TCP 连接,数据才可以在客户机和服务器之间传递。
|
||||
> SYN 同步序列编号(Synchronize Sequence Numbers) 是 TCP/IP 建立连接时使用的握手信号。在客户机和服务端之间建立正常的 TCP 网络连接时,客户机首先发出一个 SYN 消息,服务端使用 SYN-ACK 应答表示接收到了这个消息,最后客户机再以 ACK(Acknowledgement)消息响应。这样在客户机和服务端之间才能建立起可靠的 TCP 连接,数据才可以在客户机和服务端之间传递。
|
||||
|
||||
### 三次握手过程中可以携带数据吗?
|
||||
|
||||
在 TCP 三次握手过程中,第三次握手是可以携带数据的(客户端发送完 ACK 确认包之后就进入 ESTABLISHED 状态了),这一点在 RFC 793 文档中有提到。也就是说,一旦完成了前两次握手,TCP 协议允许数据在第三次握手时开始传输。
|
||||
|
||||
如果第三次握手的 ACK 确认包丢失,但是客户端已经开始发送携带数据的包,那么服务端在收到这个携带数据的包时,如果该包中包含了 ACK 标记,服务端会将其视为有效的第三次握手确认。这样,连接就被认为是建立的,服务端会处理该数据包,并继续正常的数据传输流程。
|
||||
|
||||
## 断开连接-TCP 四次挥手
|
||||
|
||||
@ -43,8 +58,8 @@ tag:
|
||||
|
||||
断开一个 TCP 连接则需要“四次挥手”,缺一不可:
|
||||
|
||||
1. **第一次挥手**:客户端发送一个 FIN(SEQ=x) 标志的数据包->服务端,用来关闭客户端到服务器的数据传送。然后客户端进入 **FIN-WAIT-1** 状态。
|
||||
2. **第二次挥手**:服务器收到这个 FIN(SEQ=X) 标志的数据包,它发送一个 ACK (ACK=x+1)标志的数据包->客户端 。然后服务端进入 **CLOSE-WAIT** 状态,客户端进入 **FIN-WAIT-2** 状态。
|
||||
1. **第一次挥手**:客户端发送一个 FIN(SEQ=x) 标志的数据包->服务端,用来关闭客户端到服务端的数据传送。然后客户端进入 **FIN-WAIT-1** 状态。
|
||||
2. **第二次挥手**:服务端收到这个 FIN(SEQ=X) 标志的数据包,它发送一个 ACK (ACK=x+1)标志的数据包->客户端 。然后服务端进入 **CLOSE-WAIT** 状态,客户端进入 **FIN-WAIT-2** 状态。
|
||||
3. **第三次挥手**:服务端发送一个 FIN (SEQ=y)标志的数据包->客户端,请求关闭连接,然后服务端进入 **LAST-ACK** 状态。
|
||||
4. **第四次挥手**:客户端发送 ACK (ACK=y+1)标志的数据包->服务端,然后客户端进入**TIME-WAIT**状态,服务端在收到 ACK (ACK=y+1)标志的数据包后进入 CLOSE 状态。此时如果客户端等待 **2MSL** 后依然没有收到回复,就证明服务端已正常关闭,随后客户端也可以关闭连接了。
|
||||
|
||||
@ -61,17 +76,17 @@ TCP 是全双工通信,可以双向传输数据。任何一方都可以在数
|
||||
3. **第三次挥手**:于是 B 可能又巴拉巴拉说了一通,最后 B 说“我说完了”
|
||||
4. **第四次挥手**:A 回答“知道了”,这样通话才算结束。
|
||||
|
||||
### 为什么不能把服务器发送的 ACK 和 FIN 合并起来,变成三次挥手?
|
||||
### 为什么不能把服务端发送的 ACK 和 FIN 合并起来,变成三次挥手?
|
||||
|
||||
因为服务器收到客户端断开连接的请求时,可能还有一些数据没有发完,这时先回复 ACK,表示接收到了断开连接的请求。等到数据发完之后再发 FIN,断开服务器到客户端的数据传送。
|
||||
因为服务端收到客户端断开连接的请求时,可能还有一些数据没有发完,这时先回复 ACK,表示接收到了断开连接的请求。等到数据发完之后再发 FIN,断开服务端到客户端的数据传送。
|
||||
|
||||
### 如果第二次挥手时服务器的 ACK 没有送达客户端,会怎样?
|
||||
### 如果第二次挥手时服务端的 ACK 没有送达客户端,会怎样?
|
||||
|
||||
客户端没有收到 ACK 确认,会重新发送 FIN 请求。
|
||||
|
||||
### 为什么第四次挥手客户端需要等待 2\*MSL(报文段最长寿命)时间后才进入 CLOSED 状态?
|
||||
|
||||
第四次挥手时,客户端发送给服务器的 ACK 有可能丢失,如果服务端因为某些原因而没有收到 ACK 的话,服务端就会重发 FIN,如果客户端在 2\*MSL 的时间内收到了 FIN,就会重新发送 ACK 并再次等待 2MSL,防止 Server 没有收到 ACK 而不断重发 FIN。
|
||||
第四次挥手时,客户端发送给服务端的 ACK 有可能丢失,如果服务端因为某些原因而没有收到 ACK 的话,服务端就会重发 FIN,如果客户端在 2\*MSL 的时间内收到了 FIN,就会重新发送 ACK 并再次等待 2MSL,防止 Server 没有收到 ACK 而不断重发 FIN。
|
||||
|
||||
> **MSL(Maximum Segment Lifetime)** : 一个片段在网络中最大的存活时间,2MSL 就是一个发送和一个回复所需的最大时间。如果直到 2MSL,Client 都没有再次收到 FIN,那么 Client 推断 ACK 已经被成功接收,则结束 TCP 连接。
|
||||
|
||||
@ -83,4 +98,6 @@ TCP 是全双工通信,可以双向传输数据。任何一方都可以在数
|
||||
|
||||
- TCP and UDP Tutorial:<https://www.9tut.com/tcp-and-udp-tutorial>
|
||||
|
||||
- 从一次线上问题说起,详解 TCP 半连接队列、全连接队列:<https://mp.weixin.qq.com/s/YpSlU1yaowTs-pF6R43hMw>
|
||||
|
||||
<!-- @include: @article-footer.snippet.md -->
|
||||
|
@ -9,27 +9,27 @@ tag:
|
||||
|
||||
## 前言
|
||||
|
||||
`MySQL` 日志 主要包括错误日志、查询日志、慢查询日志、事务日志、二进制日志几大类。其中,比较重要的还要属二进制日志 `binlog`(归档日志)和事务日志 `redo log`(重做日志)和 `undo log`(回滚日志)。
|
||||
MySQL 日志 主要包括错误日志、查询日志、慢查询日志、事务日志、二进制日志几大类。其中,比较重要的还要属二进制日志 binlog(归档日志)和事务日志 redo log(重做日志)和 undo log(回滚日志)。
|
||||
|
||||

|
||||
|
||||
今天就来聊聊 `redo log`(重做日志)、`binlog`(归档日志)、两阶段提交、`undo log` (回滚日志)。
|
||||
今天就来聊聊 redo log(重做日志)、binlog(归档日志)、两阶段提交、undo log(回滚日志)。
|
||||
|
||||
## redo log
|
||||
|
||||
`redo log`(重做日志)是`InnoDB`存储引擎独有的,它让`MySQL`拥有了崩溃恢复能力。
|
||||
redo log(重做日志)是 InnoDB 存储引擎独有的,它让 MySQL 拥有了崩溃恢复能力。
|
||||
|
||||
比如 `MySQL` 实例挂了或宕机了,重启时,`InnoDB`存储引擎会使用`redo log`恢复数据,保证数据的持久性与完整性。
|
||||
比如 MySQL 实例挂了或宕机了,重启时,InnoDB 存储引擎会使用 redo log 恢复数据,保证数据的持久性与完整性。
|
||||
|
||||

|
||||
|
||||
`MySQL` 中数据是以页为单位,你查询一条记录,会从硬盘把一页的数据加载出来,加载出来的数据叫数据页,会放入到 `Buffer Pool` 中。
|
||||
MySQL 中数据是以页为单位,你查询一条记录,会从硬盘把一页的数据加载出来,加载出来的数据叫数据页,会放入到 `Buffer Pool` 中。
|
||||
|
||||
后续的查询都是先从 `Buffer Pool` 中找,没有命中再去硬盘加载,减少硬盘 `IO` 开销,提升性能。
|
||||
后续的查询都是先从 `Buffer Pool` 中找,没有命中再去硬盘加载,减少硬盘 IO 开销,提升性能。
|
||||
|
||||
更新表数据的时候,也是如此,发现 `Buffer Pool` 里存在要更新的数据,就直接在 `Buffer Pool` 里更新。
|
||||
|
||||
然后会把“在某个数据页上做了什么修改”记录到重做日志缓存(`redo log buffer`)里,接着刷盘到 `redo log` 文件里。
|
||||
然后会把“在某个数据页上做了什么修改”记录到重做日志缓存(`redo log buffer`)里,接着刷盘到 redo log 文件里。
|
||||
|
||||

|
||||
|
||||
@ -64,15 +64,15 @@ InnoDB 将 redo log 刷到磁盘上有几种情况:
|
||||
|
||||
刷盘策略`innodb_flush_log_at_trx_commit` 的默认值为 1,设置为 1 的时候才不会丢失任何数据。为了保证事务的持久性,我们必须将其设置为 1。
|
||||
|
||||
另外,`InnoDB` 存储引擎有一个后台线程,每隔`1` 秒,就会把 `redo log buffer` 中的内容写到文件系统缓存(`page cache`),然后调用 `fsync` 刷盘。
|
||||
另外,InnoDB 存储引擎有一个后台线程,每隔`1` 秒,就会把 `redo log buffer` 中的内容写到文件系统缓存(`page cache`),然后调用 `fsync` 刷盘。
|
||||
|
||||

|
||||
|
||||
也就是说,一个没有提交事务的 `redo log` 记录,也可能会刷盘。
|
||||
也就是说,一个没有提交事务的 redo log 记录,也可能会刷盘。
|
||||
|
||||
**为什么呢?**
|
||||
|
||||
因为在事务执行过程 `redo log` 记录是会写入`redo log buffer` 中,这些 `redo log` 记录会被后台线程刷盘。
|
||||
因为在事务执行过程 redo log 记录是会写入`redo log buffer` 中,这些 redo log 记录会被后台线程刷盘。
|
||||
|
||||

|
||||
|
||||
@ -84,15 +84,15 @@ InnoDB 将 redo log 刷到磁盘上有几种情况:
|
||||
|
||||

|
||||
|
||||
为`0`时,如果`MySQL`挂了或宕机可能会有`1`秒数据的丢失。
|
||||
为`0`时,如果 MySQL 挂了或宕机可能会有`1`秒数据的丢失。
|
||||
|
||||
#### innodb_flush_log_at_trx_commit=1
|
||||
|
||||

|
||||
|
||||
为`1`时, 只要事务提交成功,`redo log`记录就一定在硬盘里,不会有任何数据丢失。
|
||||
为`1`时, 只要事务提交成功,redo log 记录就一定在硬盘里,不会有任何数据丢失。
|
||||
|
||||
如果事务执行期间`MySQL`挂了或宕机,这部分日志丢了,但是事务并没有提交,所以日志丢了也不会有损失。
|
||||
如果事务执行期间 MySQL 挂了或宕机,这部分日志丢了,但是事务并没有提交,所以日志丢了也不会有损失。
|
||||
|
||||
#### innodb_flush_log_at_trx_commit=2
|
||||
|
||||
@ -100,13 +100,13 @@ InnoDB 将 redo log 刷到磁盘上有几种情况:
|
||||
|
||||
为`2`时, 只要事务提交成功,`redo log buffer`中的内容只写入文件系统缓存(`page cache`)。
|
||||
|
||||
如果仅仅只是`MySQL`挂了不会有任何数据丢失,但是宕机可能会有`1`秒数据的丢失。
|
||||
如果仅仅只是 MySQL 挂了不会有任何数据丢失,但是宕机可能会有`1`秒数据的丢失。
|
||||
|
||||
### 日志文件组
|
||||
|
||||
硬盘上存储的 `redo log` 日志文件不只一个,而是以一个**日志文件组**的形式出现的,每个的`redo`日志文件大小都是一样的。
|
||||
硬盘上存储的 redo log 日志文件不只一个,而是以一个**日志文件组**的形式出现的,每个的`redo`日志文件大小都是一样的。
|
||||
|
||||
比如可以配置为一组`4`个文件,每个文件的大小是 `1GB`,整个 `redo log` 日志文件组可以记录`4G`的内容。
|
||||
比如可以配置为一组`4`个文件,每个文件的大小是 `1GB`,整个 redo log 日志文件组可以记录`4G`的内容。
|
||||
|
||||
它采用的是环形数组形式,从头开始写,写到末尾又回到头循环写,如下图所示。
|
||||
|
||||
@ -117,15 +117,15 @@ InnoDB 将 redo log 刷到磁盘上有几种情况:
|
||||
- **write pos** 是当前记录的位置,一边写一边后移
|
||||
- **checkpoint** 是当前要擦除的位置,也是往后推移
|
||||
|
||||
每次刷盘 `redo log` 记录到**日志文件组**中,`write pos` 位置就会后移更新。
|
||||
每次刷盘 redo log 记录到**日志文件组**中,`write pos` 位置就会后移更新。
|
||||
|
||||
每次 `MySQL` 加载**日志文件组**恢复数据时,会清空加载过的 `redo log` 记录,并把 `checkpoint` 后移更新。
|
||||
每次 MySQL 加载**日志文件组**恢复数据时,会清空加载过的 redo log 记录,并把 `checkpoint` 后移更新。
|
||||
|
||||
`write pos` 和 `checkpoint` 之间的还空着的部分可以用来写入新的 `redo log` 记录。
|
||||
`write pos` 和 `checkpoint` 之间的还空着的部分可以用来写入新的 redo log 记录。
|
||||
|
||||

|
||||
|
||||
如果 `write pos` 追上 `checkpoint` ,表示**日志文件组**满了,这时候不能再写入新的 `redo log` 记录,`MySQL` 得停下来,清空一些记录,把 `checkpoint` 推进一下。
|
||||
如果 `write pos` 追上 `checkpoint` ,表示**日志文件组**满了,这时候不能再写入新的 redo log 记录,MySQL 得停下来,清空一些记录,把 `checkpoint` 推进一下。
|
||||
|
||||

|
||||
|
||||
@ -172,9 +172,9 @@ MySQL830 mysql:8.0.32
|
||||
|
||||
### redo log 小结
|
||||
|
||||
相信大家都知道 `redo log` 的作用和它的刷盘时机、存储形式。
|
||||
相信大家都知道 redo log 的作用和它的刷盘时机、存储形式。
|
||||
|
||||
现在我们来思考一个问题:**只要每次把修改后的数据页直接刷盘不就好了,还有 `redo log` 什么事?**
|
||||
现在我们来思考一个问题:**只要每次把修改后的数据页直接刷盘不就好了,还有 redo log 什么事?**
|
||||
|
||||
它们不都是刷盘么?差别在哪里?
|
||||
|
||||
@ -190,32 +190,32 @@ MySQL830 mysql:8.0.32
|
||||
|
||||
而且数据页刷盘是随机写,因为一个数据页对应的位置可能在硬盘文件的随机位置,所以性能是很差。
|
||||
|
||||
如果是写 `redo log`,一行记录可能就占几十 `Byte`,只包含表空间号、数据页号、磁盘文件偏移
|
||||
如果是写 redo log,一行记录可能就占几十 `Byte`,只包含表空间号、数据页号、磁盘文件偏移
|
||||
量、更新值,再加上是顺序写,所以刷盘速度很快。
|
||||
|
||||
所以用 `redo log` 形式记录修改内容,性能会远远超过刷数据页的方式,这也让数据库的并发能力更强。
|
||||
所以用 redo log 形式记录修改内容,性能会远远超过刷数据页的方式,这也让数据库的并发能力更强。
|
||||
|
||||
> 其实内存的数据页在一定时机也会刷盘,我们把这称为页合并,讲 `Buffer Pool`的时候会对这块细说
|
||||
|
||||
## binlog
|
||||
|
||||
`redo log` 它是物理日志,记录内容是“在某个数据页上做了什么修改”,属于 `InnoDB` 存储引擎。
|
||||
redo log 它是物理日志,记录内容是“在某个数据页上做了什么修改”,属于 InnoDB 存储引擎。
|
||||
|
||||
而 `binlog` 是逻辑日志,记录内容是语句的原始逻辑,类似于“给 ID=2 这一行的 c 字段加 1”,属于`MySQL Server` 层。
|
||||
而 binlog 是逻辑日志,记录内容是语句的原始逻辑,类似于“给 ID=2 这一行的 c 字段加 1”,属于`MySQL Server` 层。
|
||||
|
||||
不管用什么存储引擎,只要发生了表数据更新,都会产生 `binlog` 日志。
|
||||
不管用什么存储引擎,只要发生了表数据更新,都会产生 binlog 日志。
|
||||
|
||||
那 `binlog` 到底是用来干嘛的?
|
||||
那 binlog 到底是用来干嘛的?
|
||||
|
||||
可以说`MySQL`数据库的**数据备份、主备、主主、主从**都离不开`binlog`,需要依靠`binlog`来同步数据,保证数据一致性。
|
||||
可以说 MySQL 数据库的**数据备份、主备、主主、主从**都离不开 binlog,需要依靠 binlog 来同步数据,保证数据一致性。
|
||||
|
||||

|
||||
|
||||
`binlog`会记录所有涉及更新数据的逻辑操作,并且是顺序写。
|
||||
binlog 会记录所有涉及更新数据的逻辑操作,并且是顺序写。
|
||||
|
||||
### 记录格式
|
||||
|
||||
`binlog` 日志有三种格式,可以通过`binlog_format`参数指定。
|
||||
binlog 日志有三种格式,可以通过`binlog_format`参数指定。
|
||||
|
||||
- **statement**
|
||||
- **row**
|
||||
@ -237,21 +237,21 @@ MySQL830 mysql:8.0.32
|
||||
|
||||
这样就能保证同步数据的一致性,通常情况下都是指定为`row`,这样可以为数据库的恢复与同步带来更好的可靠性。
|
||||
|
||||
但是这种格式,需要更大的容量来记录,比较占用空间,恢复与同步时会更消耗`IO`资源,影响执行速度。
|
||||
但是这种格式,需要更大的容量来记录,比较占用空间,恢复与同步时会更消耗 IO 资源,影响执行速度。
|
||||
|
||||
所以就有了一种折中的方案,指定为`mixed`,记录的内容是前两者的混合。
|
||||
|
||||
`MySQL`会判断这条`SQL`语句是否可能引起数据不一致,如果是,就用`row`格式,否则就用`statement`格式。
|
||||
MySQL 会判断这条`SQL`语句是否可能引起数据不一致,如果是,就用`row`格式,否则就用`statement`格式。
|
||||
|
||||
### 写入机制
|
||||
|
||||
`binlog`的写入时机也非常简单,事务执行过程中,先把日志写到`binlog cache`,事务提交的时候,再把`binlog cache`写到`binlog`文件中。
|
||||
binlog 的写入时机也非常简单,事务执行过程中,先把日志写到`binlog cache`,事务提交的时候,再把`binlog cache`写到 binlog 文件中。
|
||||
|
||||
因为一个事务的`binlog`不能被拆开,无论这个事务多大,也要确保一次性写入,所以系统会给每个线程分配一个块内存作为`binlog cache`。
|
||||
因为一个事务的 binlog 不能被拆开,无论这个事务多大,也要确保一次性写入,所以系统会给每个线程分配一个块内存作为`binlog cache`。
|
||||
|
||||
我们可以通过`binlog_cache_size`参数控制单个线程 binlog cache 大小,如果存储内容超过了这个参数,就要暂存到磁盘(`Swap`)。
|
||||
|
||||
`binlog`日志刷盘流程如下
|
||||
binlog 日志刷盘流程如下
|
||||
|
||||

|
||||
|
||||
@ -272,57 +272,63 @@ MySQL830 mysql:8.0.32
|
||||
|
||||

|
||||
|
||||
在出现`IO`瓶颈的场景里,将`sync_binlog`设置成一个比较大的值,可以提升性能。
|
||||
在出现 IO 瓶颈的场景里,将`sync_binlog`设置成一个比较大的值,可以提升性能。
|
||||
|
||||
同样的,如果机器宕机,会丢失最近`N`个事务的`binlog`日志。
|
||||
同样的,如果机器宕机,会丢失最近`N`个事务的 binlog 日志。
|
||||
|
||||
## 两阶段提交
|
||||
|
||||
`redo log`(重做日志)让`InnoDB`存储引擎拥有了崩溃恢复能力。
|
||||
redo log(重做日志)让 InnoDB 存储引擎拥有了崩溃恢复能力。
|
||||
|
||||
`binlog`(归档日志)保证了`MySQL`集群架构的数据一致性。
|
||||
binlog(归档日志)保证了 MySQL 集群架构的数据一致性。
|
||||
|
||||
虽然它们都属于持久化的保证,但是侧重点不同。
|
||||
|
||||
在执行更新语句过程,会记录`redo log`与`binlog`两块日志,以基本的事务为单位,`redo log`在事务执行过程中可以不断写入,而`binlog`只有在提交事务时才写入,所以`redo log`与`binlog`的写入时机不一样。
|
||||
在执行更新语句过程,会记录 redo log 与 binlog 两块日志,以基本的事务为单位,redo log 在事务执行过程中可以不断写入,而 binlog 只有在提交事务时才写入,所以 redo log 与 binlog 的写入时机不一样。
|
||||
|
||||

|
||||
|
||||
回到正题,`redo log`与`binlog`两份日志之间的逻辑不一致,会出现什么问题?
|
||||
回到正题,redo log 与 binlog 两份日志之间的逻辑不一致,会出现什么问题?
|
||||
|
||||
我们以`update`语句为例,假设`id=2`的记录,字段`c`值是`0`,把字段`c`值更新成`1`,`SQL`语句为`update T set c=1 where id=2`。
|
||||
|
||||
假设执行过程中写完`redo log`日志后,`binlog`日志写期间发生了异常,会出现什么情况呢?
|
||||
假设执行过程中写完 redo log 日志后,binlog 日志写期间发生了异常,会出现什么情况呢?
|
||||
|
||||

|
||||
|
||||
由于`binlog`没写完就异常,这时候`binlog`里面没有对应的修改记录。因此,之后用`binlog`日志恢复数据时,就会少这一次更新,恢复出来的这一行`c`值是`0`,而原库因为`redo log`日志恢复,这一行`c`值是`1`,最终数据不一致。
|
||||
由于 binlog 没写完就异常,这时候 binlog 里面没有对应的修改记录。因此,之后用 binlog 日志恢复数据时,就会少这一次更新,恢复出来的这一行`c`值是`0`,而原库因为 redo log 日志恢复,这一行`c`值是`1`,最终数据不一致。
|
||||
|
||||

|
||||
|
||||
为了解决两份日志之间的逻辑一致问题,`InnoDB`存储引擎使用**两阶段提交**方案。
|
||||
为了解决两份日志之间的逻辑一致问题,InnoDB 存储引擎使用**两阶段提交**方案。
|
||||
|
||||
原理很简单,将`redo log`的写入拆成了两个步骤`prepare`和`commit`,这就是**两阶段提交**。
|
||||
原理很简单,将 redo log 的写入拆成了两个步骤`prepare`和`commit`,这就是**两阶段提交**。
|
||||
|
||||

|
||||
|
||||
使用**两阶段提交**后,写入`binlog`时发生异常也不会有影响,因为`MySQL`根据`redo log`日志恢复数据时,发现`redo log`还处于`prepare`阶段,并且没有对应`binlog`日志,就会回滚该事务。
|
||||
使用**两阶段提交**后,写入 binlog 时发生异常也不会有影响,因为 MySQL 根据 redo log 日志恢复数据时,发现 redo log 还处于`prepare`阶段,并且没有对应 binlog 日志,就会回滚该事务。
|
||||
|
||||

|
||||
|
||||
再看一个场景,`redo log`设置`commit`阶段发生异常,那会不会回滚事务呢?
|
||||
再看一个场景,redo log 设置`commit`阶段发生异常,那会不会回滚事务呢?
|
||||
|
||||

|
||||
|
||||
并不会回滚事务,它会执行上图框住的逻辑,虽然`redo log`是处于`prepare`阶段,但是能通过事务`id`找到对应的`binlog`日志,所以`MySQL`认为是完整的,就会提交事务恢复数据。
|
||||
并不会回滚事务,它会执行上图框住的逻辑,虽然 redo log 是处于`prepare`阶段,但是能通过事务`id`找到对应的 binlog 日志,所以 MySQL 认为是完整的,就会提交事务恢复数据。
|
||||
|
||||
## undo log
|
||||
|
||||
> 这部分内容为 JavaGuide 的补充:
|
||||
|
||||
我们知道如果想要保证事务的原子性,就需要在异常发生时,对已经执行的操作进行**回滚**,在 MySQL 中,恢复机制是通过 **回滚日志(undo log)** 实现的,所有事务进行的修改都会先记录到这个回滚日志中,然后再执行相关的操作。如果执行过程中遇到异常的话,我们直接利用 **回滚日志** 中的信息将数据回滚到修改之前的样子即可!并且,回滚日志会先于数据持久化到磁盘上。这样就保证了即使遇到数据库突然宕机等情况,当用户再次启动数据库的时候,数据库还能够通过查询回滚日志来回滚将之前未完成的事务。
|
||||
每一个事务对数据的修改都会被记录到 undo log ,当执行事务过程中出现错误或者需要执行回滚操作的话,MySQL 可以利用 undo log 将数据恢复到事务开始之前的状态。
|
||||
|
||||
另外,`MVCC` 的实现依赖于:**隐藏字段、Read View、undo log**。在内部实现中,`InnoDB` 通过数据行的 `DB_TRX_ID` 和 `Read View` 来判断数据的可见性,如不可见,则通过数据行的 `DB_ROLL_PTR` 找到 `undo log` 中的历史版本。每个事务读到的数据版本可能是不一样的,在同一个事务中,用户只能看到该事务创建 `Read View` 之前已经提交的修改和该事务本身做的修改
|
||||
undo log 属于逻辑日志,记录的是 SQL 语句,比如说事务执行一条 DELETE 语句,那 undo log 就会记录一条相对应的 INSERT 语句。同时,undo log 的信息也会被记录到 redo log 中,因为 undo log 也要实现持久性保护。并且,undo-log 本身是会被删除清理的,例如 INSERT 操作,在事务提交之后就可以清除掉了;UPDATE/DELETE 操作在事务提交不会立即删除,会加入 history list,由后台线程 purge 进行清理。
|
||||
|
||||
undo log 是采用 segment(段)的方式来记录的,每个 undo 操作在记录的时候占用一个 **undo log segment**(undo 日志段),undo log segment 包含在 **rollback segment**(回滚段)中。事务开始时,需要为其分配一个 rollback segment。每个 rollback segment 有 1024 个 undo log segment,这有助于管理多个并发事务的回滚需求。
|
||||
|
||||
通常情况下, **rollback segment header**(通常在回滚段的第一个页)负责管理 rollback segment。rollback segment header 是 rollback segment 的一部分,通常在回滚段的第一个页。**history list** 是 rollback segment header 的一部分,它的主要作用是记录所有已经提交但还没有被清理(purge)的事务的 undo log。这个列表使得 purge 线程能够找到并清理那些不再需要的 undo log 记录。
|
||||
|
||||
另外,`MVCC` 的实现依赖于:**隐藏字段、Read View、undo log**。在内部实现中,InnoDB 通过数据行的 `DB_TRX_ID` 和 `Read View` 来判断数据的可见性,如不可见,则通过数据行的 `DB_ROLL_PTR` 找到 undo log 中的历史版本。每个事务读到的数据版本可能是不一样的,在同一个事务中,用户只能看到该事务创建 `Read View` 之前已经提交的修改和该事务本身做的修改
|
||||
|
||||
## 总结
|
||||
|
||||
@ -330,7 +336,7 @@ MySQL830 mysql:8.0.32
|
||||
|
||||
MySQL InnoDB 引擎使用 **redo log(重做日志)** 保证事务的**持久性**,使用 **undo log(回滚日志)** 来保证事务的**原子性**。
|
||||
|
||||
`MySQL`数据库的**数据备份、主备、主主、主从**都离不开`binlog`,需要依靠`binlog`来同步数据,保证数据一致性。
|
||||
MySQL 数据库的**数据备份、主备、主主、主从**都离不开 binlog,需要依靠 binlog 来同步数据,保证数据一致性。
|
||||
|
||||
## 参考
|
||||
|
||||
|
@ -263,13 +263,13 @@ MySQL 5.5 版本之后,InnoDB 是 MySQL 的默认存储引擎。
|
||||
|
||||
言归正传!咱们下面还是来简单对比一下两者:
|
||||
|
||||
**1.是否支持行级锁**
|
||||
**1、是否支持行级锁**
|
||||
|
||||
MyISAM 只有表级锁(table-level locking),而 InnoDB 支持行级锁(row-level locking)和表级锁,默认为行级锁。
|
||||
|
||||
也就说,MyISAM 一锁就是锁住了整张表,这在并发写的情况下是多么滴憨憨啊!这也是为什么 InnoDB 在并发写的时候,性能更牛皮了!
|
||||
|
||||
**2.是否支持事务**
|
||||
**2、是否支持事务**
|
||||
|
||||
MyISAM 不提供事务支持。
|
||||
|
||||
@ -277,7 +277,7 @@ InnoDB 提供事务支持,实现了 SQL 标准定义了四个隔离级别,
|
||||
|
||||
关于 MySQL 事务的详细介绍,可以看看我写的这篇文章:[MySQL 事务隔离级别详解](./transaction-isolation-level.md)。
|
||||
|
||||
**3.是否支持外键**
|
||||
**3、是否支持外键**
|
||||
|
||||
MyISAM 不支持,而 InnoDB 支持。
|
||||
|
||||
@ -291,19 +291,19 @@ MyISAM 不支持,而 InnoDB 支持。
|
||||
|
||||
总结:一般我们也是不建议在数据库层面使用外键的,应用层面可以解决。不过,这样会对数据的一致性造成威胁。具体要不要使用外键还是要根据你的项目来决定。
|
||||
|
||||
**4.是否支持数据库异常崩溃后的安全恢复**
|
||||
**4、是否支持数据库异常崩溃后的安全恢复**
|
||||
|
||||
MyISAM 不支持,而 InnoDB 支持。
|
||||
|
||||
使用 InnoDB 的数据库在异常崩溃后,数据库重新启动的时候会保证数据库恢复到崩溃前的状态。这个恢复的过程依赖于 `redo log` 。
|
||||
|
||||
**5.是否支持 MVCC**
|
||||
**5、是否支持 MVCC**
|
||||
|
||||
MyISAM 不支持,而 InnoDB 支持。
|
||||
|
||||
讲真,这个对比有点废话,毕竟 MyISAM 连行级锁都不支持。MVCC 可以看作是行级锁的一个升级,可以有效减少加锁操作,提高性能。
|
||||
|
||||
**6.索引实现不一样。**
|
||||
**6、索引实现不一样。**
|
||||
|
||||
虽然 MyISAM 引擎和 InnoDB 引擎都是使用 B+Tree 作为索引结构,但是两者的实现方式不太一样。
|
||||
|
||||
@ -311,12 +311,16 @@ InnoDB 引擎中,其数据文件本身就是索引文件。相比 MyISAM,索
|
||||
|
||||
详细区别,推荐你看看我写的这篇文章:[MySQL 索引详解](./mysql-index.md)。
|
||||
|
||||
**7.性能有差别。**
|
||||
**7、性能有差别。**
|
||||
|
||||
InnoDB 的性能比 MyISAM 更强大,不管是在读写混合模式下还是只读模式下,随着 CPU 核数的增加,InnoDB 的读写能力呈线性增长。MyISAM 因为读写不能并发,它的处理能力跟核数没关系。
|
||||
|
||||

|
||||
|
||||
**8、数据缓存策略和机制实现不同。**
|
||||
|
||||
InnoDB 使用缓冲池(Buffer Pool)缓存数据页和索引页,MyISAM 使用键缓存(Key Cache)仅缓存索引页而不缓存数据页。
|
||||
|
||||
**总结**:
|
||||
|
||||
- InnoDB 支持行级别的锁粒度,MyISAM 不支持,只支持表级别的锁粒度。
|
||||
|
@ -18,7 +18,7 @@ head:
|
||||
|
||||
### Java 集合概览
|
||||
|
||||
Java 集合,也叫作容器,主要是由两大接口派生而来:一个是 `Collection`接口,主要用于存放单一元素;另一个是 `Map` 接口,主要用于存放键值对。对于`Collection` 接口,下面又有三个主要的子接口:`List`、`Set` 、 `Queue`、`Map`。
|
||||
Java 集合,也叫作容器,主要是由两大接口派生而来:一个是 `Collection`接口,主要用于存放单一元素;另一个是 `Map` 接口,主要用于存放键值对。对于`Collection` 接口,下面又有三个主要的子接口:`List`、`Set` 、 `Queue`。
|
||||
|
||||
Java 集合框架如下图所示:
|
||||
|
||||
|
@ -14,9 +14,11 @@ head:
|
||||
|
||||
<!-- @include: @small-advertisement.snippet.md -->
|
||||
|
||||
## 什么是线程和进程?
|
||||
## 线程
|
||||
|
||||
### 何为进程?
|
||||
### 什么是线程和进程?
|
||||
|
||||
#### 何为进程?
|
||||
|
||||
进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。
|
||||
|
||||
@ -26,7 +28,7 @@ head:
|
||||
|
||||

|
||||
|
||||
### 何为线程?
|
||||
#### 何为线程?
|
||||
|
||||
线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享进程的**堆**和**方法区**资源,但每个线程有自己的**程序计数器**、**虚拟机栈**和**本地方法栈**,所以系统在产生一个线程,或是在各个线程之间做切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。
|
||||
|
||||
@ -59,7 +61,7 @@ public class MultiThread {
|
||||
|
||||
从上面的输出内容可以看出:**一个 Java 程序的运行是 main 线程和多个其他线程同时运行**。
|
||||
|
||||
## Java 线程和操作系统的线程有啥区别?
|
||||
### Java 线程和操作系统的线程有啥区别?
|
||||
|
||||
JDK 1.2 之前,Java 线程是基于绿色线程(Green Threads)实现的,这是一种用户级线程(用户线程),也就是说 JVM 自己模拟了多线程的运行,而不依赖于操作系统。由于绿色线程和原生线程比起来在使用时有一些限制(比如绿色线程不能直接使用操作系统提供的功能如异步 I/O、只能在一个内核线程上运行无法利用多核),在 JDK 1.2 及以后,Java 线程改为基于原生线程(Native Threads)实现,也就是说 JVM 直接使用操作系统原生的内核级线程(内核线程)来实现 Java 线程,由操作系统内核进行线程的调度和管理。
|
||||
|
||||
@ -84,11 +86,7 @@ JDK 1.2 之前,Java 线程是基于绿色线程(Green Threads)实现的,
|
||||
|
||||
虚拟线程在 JDK 21 顺利转正,关于虚拟线程、平台线程(也就是我们上面提到的 Java 线程)和内核线程三者的关系可以阅读我写的这篇文章:[Java 20 新特性概览](../new-features/java20.md)。
|
||||
|
||||
## 请简要描述线程与进程的关系,区别及优缺点?
|
||||
|
||||
从 JVM 角度说进程和线程之间的关系。
|
||||
|
||||
### 图解进程和线程的关系
|
||||
### 请简要描述线程与进程的关系,区别及优缺点?
|
||||
|
||||
下图是 Java 内存区域,通过下图我们从 JVM 的角度来说一下线程和进程之间的关系。
|
||||
|
||||
@ -102,7 +100,7 @@ JDK 1.2 之前,Java 线程是基于绿色线程(Green Threads)实现的,
|
||||
|
||||
下面来思考这样一个问题:为什么**程序计数器**、**虚拟机栈**和**本地方法栈**是线程私有的呢?为什么堆和方法区是线程共享的呢?
|
||||
|
||||
### 程序计数器为什么是私有的?
|
||||
#### 程序计数器为什么是私有的?
|
||||
|
||||
程序计数器主要有下面两个作用:
|
||||
|
||||
@ -113,61 +111,18 @@ JDK 1.2 之前,Java 线程是基于绿色线程(Green Threads)实现的,
|
||||
|
||||
所以,程序计数器私有主要是为了**线程切换后能恢复到正确的执行位置**。
|
||||
|
||||
### 虚拟机栈和本地方法栈为什么是私有的?
|
||||
#### 虚拟机栈和本地方法栈为什么是私有的?
|
||||
|
||||
- **虚拟机栈:** 每个 Java 方法在执行之前会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。
|
||||
- **本地方法栈:** 和虚拟机栈所发挥的作用非常相似,区别是:**虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。** 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。
|
||||
|
||||
所以,为了**保证线程中的局部变量不被别的线程访问到**,虚拟机栈和本地方法栈是线程私有的。
|
||||
|
||||
### 一句话简单了解堆和方法区
|
||||
#### 一句话简单了解堆和方法区
|
||||
|
||||
堆和方法区是所有线程共享的资源,其中堆是进程中最大的一块内存,主要用于存放新创建的对象 (几乎所有对象都在这里分配内存),方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
|
||||
|
||||
## 并发与并行的区别
|
||||
|
||||
- **并发**:两个及两个以上的作业在同一 **时间段** 内执行。
|
||||
- **并行**:两个及两个以上的作业在同一 **时刻** 执行。
|
||||
|
||||
最关键的点是:是否是 **同时** 执行。
|
||||
|
||||
## 同步和异步的区别
|
||||
|
||||
- **同步**:发出一个调用之后,在没有得到结果之前, 该调用就不可以返回,一直等待。
|
||||
- **异步**:调用在发出之后,不用等待返回结果,该调用直接返回。
|
||||
|
||||
## 为什么要使用多线程?
|
||||
|
||||
先从总体上来说:
|
||||
|
||||
- **从计算机底层来说:** 线程可以比作是轻量级的进程,是程序执行的最小单位,线程间的切换和调度的成本远远小于进程。另外,多核 CPU 时代意味着多个线程可以同时运行,这减少了线程上下文切换的开销。
|
||||
- **从当代互联网发展趋势来说:** 现在的系统动不动就要求百万级甚至千万级的并发量,而多线程并发编程正是开发高并发系统的基础,利用好多线程机制可以大大提高系统整体的并发能力以及性能。
|
||||
|
||||
再深入到计算机底层来探讨:
|
||||
|
||||
- **单核时代**:在单核时代多线程主要是为了提高单进程利用 CPU 和 IO 系统的效率。 假设只运行了一个 Java 进程的情况,当我们请求 IO 的时候,如果 Java 进程中只有一个线程,此线程被 IO 阻塞则整个进程被阻塞。CPU 和 IO 设备只有一个在运行,那么可以简单地说系统整体效率只有 50%。当使用多线程的时候,一个线程被 IO 阻塞,其他线程还可以继续使用 CPU。从而提高了 Java 进程利用系统资源的整体效率。
|
||||
- **多核时代**: 多核时代多线程主要是为了提高进程利用多核 CPU 的能力。举个例子:假如我们要计算一个复杂的任务,我们只用一个线程的话,不论系统有几个 CPU 核心,都只会有一个 CPU 核心被利用到。而创建多个线程,这些线程可以被映射到底层多个 CPU 上执行,在任务中的多个线程没有资源竞争的情况下,任务执行的效率会有显著性的提高,约等于(单核时执行时间/CPU 核心数)。
|
||||
|
||||
## 使用多线程可能带来什么问题?
|
||||
|
||||
并发编程的目的就是为了能提高程序的执行效率进而提高程序的运行速度,但是并发编程并不总是能提高程序运行速度的,而且并发编程可能会遇到很多问题,比如:内存泄漏、死锁、线程不安全等等。
|
||||
|
||||
## 如何理解线程安全和不安全?
|
||||
|
||||
线程安全和不安全是在多线程环境下对于同一份数据的访问是否能够保证其正确性和一致性的描述。
|
||||
|
||||
- 线程安全指的是在多线程环境下,对于同一份数据,不管有多少个线程同时访问,都能保证这份数据的正确性和一致性。
|
||||
- 线程不安全则表示在多线程环境下,对于同一份数据,多个线程同时访问时可能会导致数据混乱、错误或者丢失。
|
||||
|
||||
## 单核 CPU 上运行多个线程效率一定会高吗?
|
||||
|
||||
单核 CPU 同时运行多个线程的效率是否会高,取决于线程的类型和任务的性质。一般来说,有两种类型的线程:CPU 密集型和 IO 密集型。CPU 密集型的线程主要进行计算和逻辑处理,需要占用大量的 CPU 资源。IO 密集型的线程主要进行输入输出操作,如读写文件、网络通信等,需要等待 IO 设备的响应,而不占用太多的 CPU 资源。
|
||||
|
||||
在单核 CPU 上,同一时刻只能有一个线程在运行,其他线程需要等待 CPU 的时间片分配。如果线程是 CPU 密集型的,那么多个线程同时运行会导致频繁的线程切换,增加了系统的开销,降低了效率。如果线程是 IO 密集型的,那么多个线程同时运行可以利用 CPU 在等待 IO 时的空闲时间,提高了效率。
|
||||
|
||||
因此,对于单核 CPU 来说,如果任务是 CPU 密集型的,那么开很多线程会影响效率;如果任务是 IO 密集型的,那么开很多线程会提高效率。当然,这里的“很多”也要适度,不能超过系统能够承受的上限。
|
||||
|
||||
## 如何创建线程?
|
||||
### 如何创建线程?
|
||||
|
||||
一般来说,创建线程有很多种方式,例如继承`Thread`类、实现`Runnable`接口、实现`Callable`接口、使用线程池、使用`CompletableFuture`类等等。
|
||||
|
||||
@ -177,7 +132,7 @@ JDK 1.2 之前,Java 线程是基于绿色线程(Green Threads)实现的,
|
||||
|
||||
关于这个问题的详细分析可以查看这篇文章:[大家都说 Java 有三种创建线程的方式!并发编程中的惊天骗局!](https://mp.weixin.qq.com/s/NspUsyhEmKnJ-4OprRFp9g)。
|
||||
|
||||
## 说说线程的生命周期和状态?
|
||||
### 说说线程的生命周期和状态?
|
||||
|
||||
Java 线程在运行的生命周期中的指定时刻只可能处于下面 6 种不同状态的其中一个状态:
|
||||
|
||||
@ -209,7 +164,7 @@ Java 线程状态变迁图(图源:[挑错 |《Java 并发编程的艺术》中
|
||||
|
||||
相关阅读:[线程的几种状态你真的了解么?](https://mp.weixin.qq.com/s/R5MrTsWvk9McFSQ7bS0W2w) 。
|
||||
|
||||
## 什么是线程上下文切换?
|
||||
### 什么是线程上下文切换?
|
||||
|
||||
线程在执行过程中会有自己的运行条件和状态(也称上下文),比如上文所说到过的程序计数器,栈信息等。当出现如下情况的时候,线程会从占用 CPU 状态中退出。
|
||||
|
||||
@ -222,9 +177,81 @@ Java 线程状态变迁图(图源:[挑错 |《Java 并发编程的艺术》中
|
||||
|
||||
上下文切换是现代操作系统的基本功能,因其每次需要保存信息恢复信息,这将会占用 CPU,内存等系统资源进行处理,也就意味着效率会有一定损耗,如果频繁切换就会造成整体效率低下。
|
||||
|
||||
## 什么是线程死锁?如何避免死锁?
|
||||
### Thread#sleep() 方法和 Object#wait() 方法对比
|
||||
|
||||
### 认识线程死锁
|
||||
**共同点**:两者都可以暂停线程的执行。
|
||||
|
||||
**区别**:
|
||||
|
||||
- **`sleep()` 方法没有释放锁,而 `wait()` 方法释放了锁** 。
|
||||
- `wait()` 通常被用于线程间交互/通信,`sleep()`通常被用于暂停执行。
|
||||
- `wait()` 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 `notify()`或者 `notifyAll()` 方法。`sleep()`方法执行完成后,线程会自动苏醒,或者也可以使用 `wait(long timeout)` 超时后线程会自动苏醒。
|
||||
- `sleep()` 是 `Thread` 类的静态本地方法,`wait()` 则是 `Object` 类的本地方法。为什么这样设计呢?下一个问题就会聊到。
|
||||
|
||||
### 为什么 wait() 方法不定义在 Thread 中?
|
||||
|
||||
`wait()` 是让获得对象锁的线程实现等待,会自动释放当前线程占有的对象锁。每个对象(`Object`)都拥有对象锁,既然要释放当前线程占有的对象锁并让其进入 WAITING 状态,自然是要操作对应的对象(`Object`)而非当前的线程(`Thread`)。
|
||||
|
||||
类似的问题:**为什么 `sleep()` 方法定义在 `Thread` 中?**
|
||||
|
||||
因为 `sleep()` 是让当前线程暂停执行,不涉及到对象类,也不需要获得对象锁。
|
||||
|
||||
### 可以直接调用 Thread 类的 run 方法吗?
|
||||
|
||||
这是另一个非常经典的 Java 多线程面试问题,而且在面试中会经常被问到。很简单,但是很多人都会答不上来!
|
||||
|
||||
new 一个 `Thread`,线程进入了新建状态。调用 `start()`方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 `start()` 会执行线程的相应准备工作,然后自动执行 `run()` 方法的内容,这是真正的多线程工作。 但是,直接执行 `run()` 方法,会把 `run()` 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。
|
||||
|
||||
**总结:调用 `start()` 方法方可启动线程并使线程进入就绪状态,直接执行 `run()` 方法的话不会以多线程的方式执行。**
|
||||
|
||||
## 多线程
|
||||
|
||||
### 并发与并行的区别
|
||||
|
||||
- **并发**:两个及两个以上的作业在同一 **时间段** 内执行。
|
||||
- **并行**:两个及两个以上的作业在同一 **时刻** 执行。
|
||||
|
||||
最关键的点是:是否是 **同时** 执行。
|
||||
|
||||
### 同步和异步的区别
|
||||
|
||||
- **同步**:发出一个调用之后,在没有得到结果之前, 该调用就不可以返回,一直等待。
|
||||
- **异步**:调用在发出之后,不用等待返回结果,该调用直接返回。
|
||||
|
||||
### 为什么要使用多线程?
|
||||
|
||||
先从总体上来说:
|
||||
|
||||
- **从计算机底层来说:** 线程可以比作是轻量级的进程,是程序执行的最小单位,线程间的切换和调度的成本远远小于进程。另外,多核 CPU 时代意味着多个线程可以同时运行,这减少了线程上下文切换的开销。
|
||||
- **从当代互联网发展趋势来说:** 现在的系统动不动就要求百万级甚至千万级的并发量,而多线程并发编程正是开发高并发系统的基础,利用好多线程机制可以大大提高系统整体的并发能力以及性能。
|
||||
|
||||
再深入到计算机底层来探讨:
|
||||
|
||||
- **单核时代**:在单核时代多线程主要是为了提高单进程利用 CPU 和 IO 系统的效率。 假设只运行了一个 Java 进程的情况,当我们请求 IO 的时候,如果 Java 进程中只有一个线程,此线程被 IO 阻塞则整个进程被阻塞。CPU 和 IO 设备只有一个在运行,那么可以简单地说系统整体效率只有 50%。当使用多线程的时候,一个线程被 IO 阻塞,其他线程还可以继续使用 CPU。从而提高了 Java 进程利用系统资源的整体效率。
|
||||
- **多核时代**: 多核时代多线程主要是为了提高进程利用多核 CPU 的能力。举个例子:假如我们要计算一个复杂的任务,我们只用一个线程的话,不论系统有几个 CPU 核心,都只会有一个 CPU 核心被利用到。而创建多个线程,这些线程可以被映射到底层多个 CPU 上执行,在任务中的多个线程没有资源竞争的情况下,任务执行的效率会有显著性的提高,约等于(单核时执行时间/CPU 核心数)。
|
||||
|
||||
### 使用多线程可能带来什么问题?
|
||||
|
||||
并发编程的目的就是为了能提高程序的执行效率进而提高程序的运行速度,但是并发编程并不总是能提高程序运行速度的,而且并发编程可能会遇到很多问题,比如:内存泄漏、死锁、线程不安全等等。
|
||||
|
||||
### 如何理解线程安全和不安全?
|
||||
|
||||
线程安全和不安全是在多线程环境下对于同一份数据的访问是否能够保证其正确性和一致性的描述。
|
||||
|
||||
- 线程安全指的是在多线程环境下,对于同一份数据,不管有多少个线程同时访问,都能保证这份数据的正确性和一致性。
|
||||
- 线程不安全则表示在多线程环境下,对于同一份数据,多个线程同时访问时可能会导致数据混乱、错误或者丢失。
|
||||
|
||||
### 单核 CPU 上运行多个线程效率一定会高吗?
|
||||
|
||||
单核 CPU 同时运行多个线程的效率是否会高,取决于线程的类型和任务的性质。一般来说,有两种类型的线程:CPU 密集型和 IO 密集型。CPU 密集型的线程主要进行计算和逻辑处理,需要占用大量的 CPU 资源。IO 密集型的线程主要进行输入输出操作,如读写文件、网络通信等,需要等待 IO 设备的响应,而不占用太多的 CPU 资源。
|
||||
|
||||
在单核 CPU 上,同一时刻只能有一个线程在运行,其他线程需要等待 CPU 的时间片分配。如果线程是 CPU 密集型的,那么多个线程同时运行会导致频繁的线程切换,增加了系统的开销,降低了效率。如果线程是 IO 密集型的,那么多个线程同时运行可以利用 CPU 在等待 IO 时的空闲时间,提高了效率。
|
||||
|
||||
因此,对于单核 CPU 来说,如果任务是 CPU 密集型的,那么开很多线程会影响效率;如果任务是 IO 密集型的,那么开很多线程会提高效率。当然,这里的“很多”也要适度,不能超过系统能够承受的上限。
|
||||
|
||||
## 死锁
|
||||
|
||||
### 什么是线程死锁?
|
||||
|
||||
线程死锁描述的是这样一种情况:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。
|
||||
|
||||
@ -291,6 +318,29 @@ Thread[线程 2,5,main]waiting get resource1
|
||||
3. 不剥夺条件:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
|
||||
4. 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。
|
||||
|
||||
### 如何检测死锁?
|
||||
|
||||
- 使用`jmap`、`jstack`等命令查看 JVM 线程栈和堆内存的情况。如果有死锁,`jstack` 的输出中通常会有 `Found one Java-level deadlock:`的字样,后面会跟着死锁相关的线程信息。另外,实际项目中还可以搭配使用`top`、`df`、`free`等命令查看操作系统的基本情况,出现死锁可能会导致CPU、内存等资源消耗过高。
|
||||
- 采用 VisualVM、JConsole 等工具进行排查。
|
||||
|
||||
这里以 JConsole 工具为例进行演示。
|
||||
|
||||
首先,我们要找到 JDK 的 bin 目录,找到 jconsole 并双击打开。
|
||||
|
||||

|
||||
|
||||
对于 MAC 用户来说,可以通过 `/usr/libexec/java_home -V`查看 JDK 安装目录,找到后通过 `open . + 文件夹地址`打开即可。例如,我本地的某个 JDK 的路径是:
|
||||
|
||||
```bash
|
||||
open . /Users/guide/Library/Java/JavaVirtualMachines/corretto-1.8.0_252/Contents/Home
|
||||
```
|
||||
|
||||
打开 jconsole 后,连接对应的程序,然后进入线程界面选择检测死锁即可!
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
### 如何预防和避免线程死锁?
|
||||
|
||||
**如何预防死锁?** 破坏死锁的产生的必要条件即可:
|
||||
@ -341,31 +391,4 @@ Process finished with exit code 0
|
||||
|
||||
线程 1 首先获得到 resource1 的监视器锁,这时候线程 2 就获取不到了。然后线程 1 再去获取 resource2 的监视器锁,可以获取到。然后线程 1 释放了对 resource1、resource2 的监视器锁的占用,线程 2 获取到就可以执行了。这样就破坏了破坏循环等待条件,因此避免了死锁。
|
||||
|
||||
## sleep() 方法和 wait() 方法对比
|
||||
|
||||
**共同点**:两者都可以暂停线程的执行。
|
||||
|
||||
**区别**:
|
||||
|
||||
- **`sleep()` 方法没有释放锁,而 `wait()` 方法释放了锁** 。
|
||||
- `wait()` 通常被用于线程间交互/通信,`sleep()`通常被用于暂停执行。
|
||||
- `wait()` 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 `notify()`或者 `notifyAll()` 方法。`sleep()`方法执行完成后,线程会自动苏醒,或者也可以使用 `wait(long timeout)` 超时后线程会自动苏醒。
|
||||
- `sleep()` 是 `Thread` 类的静态本地方法,`wait()` 则是 `Object` 类的本地方法。为什么这样设计呢?下一个问题就会聊到。
|
||||
|
||||
## 为什么 wait() 方法不定义在 Thread 中?
|
||||
|
||||
`wait()` 是让获得对象锁的线程实现等待,会自动释放当前线程占有的对象锁。每个对象(`Object`)都拥有对象锁,既然要释放当前线程占有的对象锁并让其进入 WAITING 状态,自然是要操作对应的对象(`Object`)而非当前的线程(`Thread`)。
|
||||
|
||||
类似的问题:**为什么 `sleep()` 方法定义在 `Thread` 中?**
|
||||
|
||||
因为 `sleep()` 是让当前线程暂停执行,不涉及到对象类,也不需要获得对象锁。
|
||||
|
||||
## 可以直接调用 Thread 类的 run 方法吗?
|
||||
|
||||
这是另一个非常经典的 Java 多线程面试问题,而且在面试中会经常被问到。很简单,但是很多人都会答不上来!
|
||||
|
||||
new 一个 `Thread`,线程进入了新建状态。调用 `start()`方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 `start()` 会执行线程的相应准备工作,然后自动执行 `run()` 方法的内容,这是真正的多线程工作。 但是,直接执行 `run()` 方法,会把 `run()` 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。
|
||||
|
||||
**总结:调用 `start()` 方法方可启动线程并使线程进入就绪状态,直接执行 `run()` 方法的话不会以多线程的方式执行。**
|
||||
|
||||
<!-- @include: @article-footer.snippet.md -->
|
||||
|
@ -311,22 +311,22 @@ public ScheduledThreadPoolExecutor(int corePoolSize) {
|
||||
- `keepAliveTime`:线程池中的线程数量大于 `corePoolSize` 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 `keepAliveTime`才会被回收销毁。
|
||||
- `unit` : `keepAliveTime` 参数的时间单位。
|
||||
- `threadFactory` :executor 创建新线程的时候会用到。
|
||||
- `handler` :饱和策略(后面会单独详细介绍一下)。
|
||||
- `handler` :拒绝策略(后面会单独详细介绍一下)。
|
||||
|
||||
下面这张图可以加深你对线程池中各个参数的相互关系的理解(图片来源:《Java 性能调优实战》):
|
||||
|
||||

|
||||
|
||||
### 线程池的饱和策略有哪些?
|
||||
### 线程池的拒绝策略有哪些?
|
||||
|
||||
如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任务时,`ThreadPoolExecutor` 定义一些策略:
|
||||
|
||||
- `ThreadPoolExecutor.AbortPolicy`:抛出 `RejectedExecutionException`来拒绝新任务的处理。
|
||||
- `ThreadPoolExecutor.CallerRunsPolicy`:调用执行自己的线程运行任务,也就是直接在调用`execute`方法的线程中运行(`run`)被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。如果您的应用程序可以承受此延迟并且你要求任何一个任务请求都要被执行的话,你可以选择这个策略。
|
||||
- `ThreadPoolExecutor.CallerRunsPolicy`:调用执行自己的线程运行任务,也就是直接在调用`execute`方法的线程中运行(`run`)被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。如果你的应用程序可以承受此延迟并且你要求任何一个任务请求都要被执行的话,你可以选择这个策略。
|
||||
- `ThreadPoolExecutor.DiscardPolicy`:不处理新任务,直接丢弃掉。
|
||||
- `ThreadPoolExecutor.DiscardOldestPolicy`:此策略将丢弃最早的未处理的任务请求。
|
||||
|
||||
举个例子:Spring 通过 `ThreadPoolTaskExecutor` 或者我们直接通过 `ThreadPoolExecutor` 的构造函数创建线程池的时候,当我们不指定 `RejectedExecutionHandler` 饱和策略来配置线程池的时候,默认使用的是 `AbortPolicy`。在这种饱和策略下,如果队列满了,`ThreadPoolExecutor` 将抛出 `RejectedExecutionException` 异常来拒绝新来的任务 ,这代表你将丢失对这个任务的处理。如果不想丢弃任务的话,可以使用`CallerRunsPolicy`。`CallerRunsPolicy` 和其他的几个策略不同,它既不会抛弃任务,也不会抛出异常,而是将任务回退给调用者,使用调用者的线程来执行任务
|
||||
举个例子:Spring 通过 `ThreadPoolTaskExecutor` 或者我们直接通过 `ThreadPoolExecutor` 的构造函数创建线程池的时候,当我们不指定 `RejectedExecutionHandler` 拒绝策略来配置线程池的时候,默认使用的是 `AbortPolicy`。在这种拒绝策略下,如果队列满了,`ThreadPoolExecutor` 将抛出 `RejectedExecutionException` 异常来拒绝新来的任务 ,这代表你将丢失对这个任务的处理。如果不想丢弃任务的话,可以使用`CallerRunsPolicy`。`CallerRunsPolicy` 和其他的几个策略不同,它既不会抛弃任务,也不会抛出异常,而是将任务回退给调用者,使用调用者的线程来执行任务。
|
||||
|
||||
```java
|
||||
public static class CallerRunsPolicy implements RejectedExecutionHandler {
|
||||
@ -359,7 +359,7 @@ public static class CallerRunsPolicy implements RejectedExecutionHandler {
|
||||
1. 如果当前运行的线程数小于核心线程数,那么就会新建一个线程来执行任务。
|
||||
2. 如果当前运行的线程数等于或大于核心线程数,但是小于最大线程数,那么就把该任务放入到任务队列里等待执行。
|
||||
3. 如果向任务队列投放任务失败(任务队列已经满了),但是当前运行的线程数是小于最大线程数的,就新建一个线程来执行任务。
|
||||
4. 如果当前运行的线程数已经等同于最大线程数了,新建线程将会使当前运行的线程超出最大线程数,那么当前任务会被拒绝,饱和策略会调用`RejectedExecutionHandler.rejectedExecution()`方法。
|
||||
4. 如果当前运行的线程数已经等同于最大线程数了,新建线程将会使当前运行的线程超出最大线程数,那么当前任务会被拒绝,拒绝策略会调用`RejectedExecutionHandler.rejectedExecution()`方法。
|
||||
|
||||
### 如何给线程池命名?
|
||||
|
||||
|
@ -125,13 +125,13 @@ public class ScheduledThreadPoolExecutor
|
||||
- `keepAliveTime`:线程池中的线程数量大于 `corePoolSize` 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 `keepAliveTime`才会被回收销毁。
|
||||
- `unit` : `keepAliveTime` 参数的时间单位。
|
||||
- `threadFactory` :executor 创建新线程的时候会用到。
|
||||
- `handler` :饱和策略(后面会单独详细介绍一下)。
|
||||
- `handler` :拒绝策略(后面会单独详细介绍一下)。
|
||||
|
||||
下面这张图可以加深你对线程池中各个参数的相互关系的理解(图片来源:《Java 性能调优实战》):
|
||||
|
||||

|
||||
|
||||
**`ThreadPoolExecutor` 饱和策略定义:**
|
||||
**`ThreadPoolExecutor` 拒绝策略定义:**
|
||||
|
||||
如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任务时,`ThreadPoolExecutor` 定义一些策略:
|
||||
|
||||
@ -142,7 +142,7 @@ public class ScheduledThreadPoolExecutor
|
||||
|
||||
举个例子:
|
||||
|
||||
举个例子:Spring 通过 `ThreadPoolTaskExecutor` 或者我们直接通过 `ThreadPoolExecutor` 的构造函数创建线程池的时候,当我们不指定 `RejectedExecutionHandler` 饱和策略来配置线程池的时候,默认使用的是 `AbortPolicy`。在这种饱和策略下,如果队列满了,`ThreadPoolExecutor` 将抛出 `RejectedExecutionException` 异常来拒绝新来的任务 ,这代表你将丢失对这个任务的处理。如果不想丢弃任务的话,可以使用`CallerRunsPolicy`。`CallerRunsPolicy` 和其他的几个策略不同,它既不会抛弃任务,也不会抛出异常,而是将任务回退给调用者,使用调用者的线程来执行任务
|
||||
举个例子:Spring 通过 `ThreadPoolTaskExecutor` 或者我们直接通过 `ThreadPoolExecutor` 的构造函数创建线程池的时候,当我们不指定 `RejectedExecutionHandler` 拒绝策略来配置线程池的时候,默认使用的是 `AbortPolicy`。在这种拒绝策略下,如果队列满了,`ThreadPoolExecutor` 将抛出 `RejectedExecutionException` 异常来拒绝新来的任务 ,这代表你将丢失对这个任务的处理。如果不想丢弃任务的话,可以使用`CallerRunsPolicy`。`CallerRunsPolicy` 和其他的几个策略不同,它既不会抛弃任务,也不会抛出异常,而是将任务回退给调用者,使用调用者的线程来执行任务
|
||||
|
||||
```java
|
||||
public static class CallerRunsPolicy implements RejectedExecutionHandler {
|
||||
@ -325,7 +325,7 @@ public class ThreadPoolExecutorDemo {
|
||||
- `keepAliveTime` : 等待时间为 1L。
|
||||
- `unit`: 等待时间的单位为 TimeUnit.SECONDS。
|
||||
- `workQueue`:任务队列为 `ArrayBlockingQueue`,并且容量为 100;
|
||||
- `handler`:饱和策略为 `CallerRunsPolicy`。
|
||||
- `handler`:拒绝策略为 `CallerRunsPolicy`。
|
||||
|
||||
**输出结构**:
|
||||
|
||||
@ -413,7 +413,7 @@ Finished all threads // 任务全部执行完了才会跳出来,因为executo
|
||||
1. 如果当前运行的线程数小于核心线程数,那么就会新建一个线程来执行任务。
|
||||
2. 如果当前运行的线程数等于或大于核心线程数,但是小于最大线程数,那么就把该任务放入到任务队列里等待执行。
|
||||
3. 如果向任务队列投放任务失败(任务队列已经满了),但是当前运行的线程数是小于最大线程数的,就新建一个线程来执行任务。
|
||||
4. 如果当前运行的线程数已经等同于最大线程数了,新建线程将会使当前运行的线程超出最大线程数,那么当前任务会被拒绝,饱和策略会调用`RejectedExecutionHandler.rejectedExecution()`方法。
|
||||
4. 如果当前运行的线程数已经等同于最大线程数了,新建线程将会使当前运行的线程超出最大线程数,那么当前任务会被拒绝,拒绝策略会调用`RejectedExecutionHandler.rejectedExecution()`方法。
|
||||
|
||||

|
||||
|
||||
|
@ -8,9 +8,11 @@ icon: guide
|
||||
|
||||
你可以从下面几个方向来做贡献:
|
||||
|
||||
- 笔记内容大多是手敲,所以难免会有笔误,你可以帮我找错别字。
|
||||
- 很多知识点我可能没有涉及到,所以你可以对其他知识点进行补充。
|
||||
- 现有的知识点难免存在不完善或者错误,所以你可以对已有知识点进行修改/补充。
|
||||
- 修改错别字,毕竟内容基本都是手敲,难免会有笔误。
|
||||
- 对原有内容进行修改完善,例如对某个面试问题的答案进行完善、对某篇文章的内容进行完善。
|
||||
- 新增内容,例如新增面试常问的问题、添加重要知识点的详解。
|
||||
|
||||
目前的贡献奖励也比较丰富和完善,对于多次贡献的用户,有耳机、键盘等实物奖励以及现金奖励!
|
||||
|
||||
一定一定一定要注意 **排版规范**:
|
||||
|
||||
@ -25,3 +27,5 @@ icon: guide
|
||||
- [《如何向开源社区提问题》](https://github.com/seajs/seajs/issues/545)
|
||||
- [《如何有效地报告 Bug》](http://www.chiark.greenend.org.uk/~sgtatham/bugs-cn.html)
|
||||
- [《如何向开源项目提交无法解答的问题》](https://zhuanlan.zhihu.com/p/25795393)。
|
||||
|
||||
另外,你可以参考学习别人的文章,但一定一定一定不能复制粘贴别人的内容,努力比别人写的更容易理解,用自己的话讲出来,适当简化表达,突出重点!
|
||||
|
@ -27,7 +27,6 @@ icon: project
|
||||
|
||||
- [paicoding](https://github.com/itwanger/paicoding):一款好用又强大的开源社区,基于 Spring Boot 系列主流技术栈,附详细的教程。
|
||||
- [forest](https://github.com/rymcu/forest):下一代的知识社区系统,可以自定义专题和作品集。后端基于 SpringBoot + Shrio + MyBatis + JWT + Redis,前端基于 Vue + NuxtJS + Element-UI。
|
||||
- [vhr](https://github.com/lenve/vhr "vhr"):微人事是一个前后端分离的人力资源管理系统,项目采用 SpringBoot+Vue 开发。
|
||||
- [community](https://github.com/codedrinker/community):开源论坛、问答系统,现有功能提问、回复、通知、最新、最热、消除零回复功能。功能持续更新中…… 技术栈 Spring、Spring Boot、MyBatis、MySQL/H2、Bootstrap。
|
||||
- [VBlog](https://github.com/lenve/VBlog):V 部落,Vue+SpringBoot 实现的多用户博客管理平台!
|
||||
- [My-Blog](https://github.com/ZHENFENG13/My-Blog): SpringBoot + Mybatis + Thymeleaf 等技术实现的 Java 博客系统,页面美观、功能齐全、部署简单及完善的代码,一定会给使用者无与伦比的体验。
|
||||
@ -49,6 +48,7 @@ icon: project
|
||||
- [HOJ](https://gitee.com/himitzh0730/hoj):分布式架构的在线测评平台 OJ ,功能非常全面,支持刷题、训练、比赛、评测等功能。
|
||||
- [VOJ](https://github.com/simplefanC/voj):基于微服务架构的高性能在线评测系统。拥有本地判题服务,同时支持其它知名 OJ (HDU、POJ...) 的远程判题。采用现阶段流行技术实现,采用 Docker 容器化部署。
|
||||
- [OnlineJudge](https://github.com/SDUOJ/OnlineJudge):基于微服务架构的在线评测系统,支持多种国际赛制支持(ICPC/OI/IOI),采用 Docker 容器化部署。
|
||||
- [sg-exam](https://gitee.com/wells2333/sg-exam):方便易用、高颜值的教学管理平台,提供多租户、权限管理、考试、练习、在线学习等功能。
|
||||
- [uexam](https://gitee.com/mindskip/uexam):功能全面的在线考试系统,开发部署简单快捷、界面设计友好、代码结构清晰。相关阅读:[好一个 Spring Boot 开源在线考试系统!解决了我的燃眉之急](http://link.zhihu.com/?target=https%3A//mp.weixin.qq.com/s%3F__biz%3DMzg2OTA0Njk0OA%3D%3D%26mid%3D2247491585%26idx%3D1%26sn%3D8d3c6768c22e72d6bfcbeee9624886a7%26chksm%3Dcea1afcaf9d626dc918760289c37025ad526f6255786bc198d2402203df64c873ad7934f58df%26scene%3D178%26cur_album_id%3D1345382825083895808%23rd) 。
|
||||
- [PassJava-Platform](https://github.com/Jackson0714/PassJava-Platform):基于微服务架构的面试刷题小程序!相关阅读:[一个基于 Spring Cloud 的面试刷题系统。面试、毕设、项目经验一网打尽](http://link.zhihu.com/?target=https%3A//mp.weixin.qq.com/s%3F__biz%3DMzg2OTA0Njk0OA%3D%3D%26mid%3D2247497045%26idx%3D1%26sn%3D577175bfd6c040a0df5a494fce6f9758%26chksm%3Dcea1ba9ef9d633883a2e213c0fb9a88bdc87051347d4b3fad2c2befb65d8b16e1ea81d8146dd%26scene%3D178%26cur_album_id%3D1345382825083895808%23rd)。
|
||||
|
||||
|
@ -632,6 +632,163 @@ public class GlobalExceptionHandler {
|
||||
- **适配器模式** : Spring AOP 的增强或通知(Advice)使用到了适配器模式、spring MVC 中也是用到了适配器模式适配`Controller`。
|
||||
- ……
|
||||
|
||||
## Spring 的循环依赖
|
||||
|
||||
### Spring 循环依赖了解吗,怎么解决?
|
||||
|
||||
循环依赖是指 Bean 对象循环引用,是两个或多个 Bean 之间相互持有对方的引用,例如 CircularDependencyA → CircularDependencyB → CircularDependencyA。
|
||||
|
||||
```java
|
||||
@Component
|
||||
public class CircularDependencyA {
|
||||
@Autowired
|
||||
private CircularDependencyB circB;
|
||||
}
|
||||
|
||||
@Component
|
||||
public class CircularDependencyB {
|
||||
@Autowired
|
||||
private CircularDependencyA circA;
|
||||
}
|
||||
```
|
||||
|
||||
单个对象的自我依赖也会出现循环依赖,但这种概率极低,属于是代码编写错误。
|
||||
|
||||
```java
|
||||
@Component
|
||||
public class CircularDependencyA {
|
||||
@Autowired
|
||||
private CircularDependencyA circA;
|
||||
}
|
||||
```
|
||||
|
||||
Spring 框架通过使用三级缓存来解决这个问题,确保即使在循环依赖的情况下也能正确创建 Bean。
|
||||
|
||||
Spring 中的三级缓存其实就是三个 Map,如下:
|
||||
|
||||
```java
|
||||
// 一级缓存
|
||||
/** Cache of singleton objects: bean name to bean instance. */
|
||||
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
|
||||
|
||||
// 二级缓存
|
||||
/** Cache of early singleton objects: bean name to bean instance. */
|
||||
private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);
|
||||
|
||||
// 三级缓存
|
||||
/** Cache of singleton factories: bean name to ObjectFactory. */
|
||||
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
|
||||
```
|
||||
|
||||
简单来说,Spring 的三级缓存包括:
|
||||
|
||||
1. **一级缓存(singletonObjects)**:存放最终形态的 Bean(已经实例化、属性填充、初始化),单例池,为“Spring 的单例属性”⽽⽣。一般情况我们获取 Bean 都是从这里获取的,但是并不是所有的 Bean 都在单例池里面,例如原型 Bean 就不在里面。
|
||||
2. **二级缓存(earlySingletonObjects)**:存放过渡 Bean(半成品,尚未属性填充),也就是三级缓存中`ObjectFactory`产生的对象,与三级缓存配合使用的,可以防止 AOP 的情况下,每次调用`ObjectFactory#getObject()`都是会产生新的代理对象的。
|
||||
3. **三级缓存(singletonFactories)**:存放`ObjectFactory`,`ObjectFactory`的`getObject()`方法(最终调用的是`getEarlyBeanReference()`方法)可以生成原始 Bean 对象或者代理对象(如果 Bean 被 AOP 切面代理)。三级缓存只会对单例 Bean 生效。
|
||||
|
||||
接下来说一下 Spring 创建 Bean 的流程:
|
||||
|
||||
1. 先去 **一级缓存 `singletonObjects`** 中获取,存在就返回;
|
||||
2. 如果不存在或者对象正在创建中,于是去 **二级缓存 `earlySingletonObjects`** 中获取;
|
||||
3. 如果还没有获取到,就去 **三级缓存 `singletonFactories`** 中获取,通过执行 `ObjectFacotry` 的 `getObject()` 就可以获取该对象,获取成功之后,从三级缓存移除,并将该对象加入到二级缓存中。
|
||||
|
||||
在三级缓存中存储的是 `ObjectFacoty` :
|
||||
|
||||
```java
|
||||
public interface ObjectFactory<T> {
|
||||
T getObject() throws BeansException;
|
||||
}
|
||||
```
|
||||
|
||||
Spring 在创建 Bean 的时候,如果允许循环依赖的话,Spring 就会将刚刚实例化完成,但是属性还没有初始化完的 Bean 对象给提前暴露出去,这里通过 `addSingletonFactory` 方法,向三级缓存中添加一个 `ObjectFactory` 对象:
|
||||
|
||||
```java
|
||||
// AbstractAutowireCapableBeanFactory # doCreateBean #
|
||||
public abstract class AbstractAutowireCapableBeanFactory ... {
|
||||
protected Object doCreateBean(...) {
|
||||
//...
|
||||
|
||||
// 支撑循环依赖:将 ()->getEarlyBeanReference 作为一个 ObjectFactory 对象的 getObject() 方法加入到三级缓存中
|
||||
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
那么上边在说 Spring 创建 Bean 的流程时说了,如果一级缓存、二级缓存都取不到对象时,会去三级缓存中通过 `ObjectFactory` 的 `getObject` 方法获取对象。
|
||||
|
||||
```java
|
||||
class A {
|
||||
// 使用了 B
|
||||
private B b;
|
||||
}
|
||||
class B {
|
||||
// 使用了 A
|
||||
private A a;
|
||||
}
|
||||
```
|
||||
|
||||
以上面的循环依赖代码为例,整个解决循环依赖的流程如下:
|
||||
|
||||
- 当 Spring 创建 A 之后,发现 A 依赖了 B ,又去创建 B,B 依赖了 A ,又去创建 A;
|
||||
- 在 B 创建 A 的时候,那么此时 A 就发生了循环依赖,由于 A 此时还没有初始化完成,因此在 **一二级缓存** 中肯定没有 A;
|
||||
- 那么此时就去三级缓存中调用 `getObject()` 方法去获取 A 的 **前期暴露的对象** ,也就是调用上边加入的 `getEarlyBeanReference()` 方法,生成一个 A 的 **前期暴露对象**;
|
||||
- 然后就将这个 `ObjectFactory` 从三级缓存中移除,并且将前期暴露对象放入到二级缓存中,那么 B 就将这个前期暴露对象注入到依赖,来支持循环依赖。
|
||||
|
||||
**只用两级缓存够吗?** 在没有 AOP 的情况下,确实可以只使用一级和三级缓存来解决循环依赖问题。但是,当涉及到 AOP 时,二级缓存就显得非常重要了,因为它确保了即使在 Bean 的创建过程中有多次对早期引用的请求,也始终只返回同一个代理对象,从而避免了同一个 Bean 有多个代理对象的问题。
|
||||
|
||||
**最后总结一下 Spring 如何解决三级缓存**:
|
||||
|
||||
在三级缓存这一块,主要记一下 Spring 是如何支持循环依赖的即可,也就是如果发生循环依赖的话,就去 **三级缓存 `singletonFactories`** 中拿到三级缓存中存储的 `ObjectFactory` 并调用它的 `getObject()` 方法来获取这个循环依赖对象的前期暴露对象(虽然还没初始化完成,但是可以拿到该对象在堆中的存储地址了),并且将这个前期暴露对象放到二级缓存中,这样在循环依赖时,就不会重复初始化了!
|
||||
|
||||
不过,这种机制也有一些缺点,比如增加了内存开销(需要维护三级缓存,也就是三个 Map),降低了性能(需要进行多次检查和转换)。并且,还有少部分情况是不支持循环依赖的,比如非单例的 bean 和`@Async`注解的 bean 无法支持循环依赖。
|
||||
|
||||
### @Lazy 能解决循环依赖吗?
|
||||
|
||||
`@Lazy` 用来标识类是否需要懒加载/延迟加载,可以作用在类上、方法上、构造器上、方法参数上、成员变量中。
|
||||
|
||||
Spring Boot 2.2 新增了全局懒加载属性,开启后全局 bean 被设置为懒加载,需要时再去创建。
|
||||
|
||||
配置文件配置全局懒加载:
|
||||
|
||||
```properties
|
||||
#默认false
|
||||
spring.main.lazy-initialization=true
|
||||
```
|
||||
|
||||
编码的方式设置全局懒加载:
|
||||
|
||||
```java
|
||||
SpringApplication springApplication=new SpringApplication(Start.class);
|
||||
springApplication.setLazyInitialization(false);
|
||||
springApplication.run(args);
|
||||
```
|
||||
|
||||
如非必要,尽量不要用全局懒加载。全局懒加载会让 Bean 第一次使用的时候加载会变慢,并且它会延迟应用程序问题的发现(当 Bean 被初始化时,问题才会出现)。
|
||||
|
||||
如果一个 Bean 没有被标记为懒加载,那么它会在 Spring IoC 容器启动的过程中被创建和初始化。如果一个 Bean 被标记为懒加载,那么它不会在 Spring IoC 容器启动时立即实例化,而是在第一次被请求时才创建。这可以帮助减少应用启动时的初始化时间,也可以用来解决循环依赖问题。
|
||||
|
||||
循环依赖问题是如何通过`@Lazy` 解决的呢?这里举一个例子,比如说有两个 Bean,A 和 B,他们之间发生了循环依赖,那么 A 的构造器上添加 `@Lazy` 注解之后(延迟 Bean B 的实例化),加载的流程如下:
|
||||
|
||||
- 首先 Spring 会去创建 A 的 Bean,创建时需要注入 B 的属性;
|
||||
- 由于在 A 上标注了 `@Lazy` 注解,因此 Spring 会去创建一个 B 的代理对象,将这个代理对象注入到 A 中的 B 属性;
|
||||
- 之后开始执行 B 的实例化、初始化,在注入 B 中的 A 属性时,此时 A 已经创建完毕了,就可以将 A 给注入进去。
|
||||
|
||||
通过 `@Lazy` 就解决了循环依赖的注入, 关键点就在于对 A 中的属性 B 进行注入时,注入的是 B 的代理对象,因此不会循环依赖。
|
||||
|
||||
之前说的发生循环依赖是因为在对 A 中的属性 B 进行注入时,注入的是 B 对象,此时又会去初始化 B 对象,发现 B 又依赖了 A,因此才导致的循环依赖。
|
||||
|
||||
一般是不建议使用循环依赖的,但是如果项目比较复杂,可以使用 `@Lazy` 解决一部分循环依赖的问题。
|
||||
|
||||
### SpringBoot 允许循环依赖发生么?
|
||||
|
||||
SpringBoot 2.6.x 以前是默认允许循环依赖的,也就是说你的代码出现了循环依赖问题,一般情况下也不会报错。SpringBoot 2.6.x 以后官方不再推荐编写存在循环依赖的代码,建议开发者自己写代码的时候去减少不必要的互相依赖。这其实也是我们最应该去做的,循环依赖本身就是一种设计缺陷,我们不应该过度依赖 Spring 而忽视了编码的规范和质量,说不定未来某个 SpringBoot 版本就彻底禁止循环依赖的代码了。
|
||||
|
||||
SpringBoot 2.6.x 以后,如果你不想重构循环依赖的代码的话,也可以采用下面这些方法:
|
||||
|
||||
- 在全局配置文件中设置允许循环依赖存在:`spring.main.allow-circular-references=true`。最简单粗暴的方式,不太推荐。
|
||||
- 在导致循环依赖的 Bean 上添加 `@Lazy` 注解,这是一种比较推荐的方式。`@Lazy` 用来标识类是否需要懒加载/延迟加载,可以作用在类上、方法上、构造器上、方法参数上、成员变量中。
|
||||
- ……
|
||||
|
||||
## Spring 事务
|
||||
|
||||
关于 Spring 事务的详细介绍,可以看我写的 [Spring 事务详解](https://javaguide.cn/system-design/framework/spring/spring-transaction.html) 这篇文章。
|
||||
|
@ -32,17 +32,19 @@ tag:
|
||||
- 不能从哈希值还原出原始数据。
|
||||
- 原始数据的任何改变都会导致哈希值的巨大变化。
|
||||
|
||||
哈希算法分为两类:
|
||||
哈希算法可以简单分为两类:
|
||||
|
||||
- **加密哈希算法**:安全性较高的哈希算法,它可以提供一定的数据完整性保护和数据防篡改能力,能够抵御一定的攻击手段,安全性相对较高,适用于对安全性要求较高的场景。例如,SHA-256、SHA-512、SM3、Bcrypt 等等。
|
||||
- **非加密哈希算法**:安全性相对较低的哈希算法,易受到暴力破解、冲突攻击等攻击手段的影响,但性能较高,适用于对安全性没有要求的业务场景。例如,CRC32、MurMurHash3 等等。
|
||||
1. **加密哈希算法**:安全性较高的哈希算法,它可以提供一定的数据完整性保护和数据防篡改能力,能够抵御一定的攻击手段,安全性相对较高,但性能较差,适用于对安全性要求较高的场景。例如 SHA2、SHA3、SM3、RIPEMD-160、BLAKE2、SipHash 等等。
|
||||
2. **非加密哈希算法**:安全性相对较低的哈希算法,易受到暴力破解、冲突攻击等攻击手段的影响,但性能较高,适用于对安全性没有要求的业务场景。例如 CRC32、MurMurHash3、SipHash 等等。
|
||||
|
||||
除了这两种之外,还有一些特殊的哈希算法,例如安全性更高的**慢哈希算法**。
|
||||
|
||||
常见的哈希算法有:
|
||||
|
||||
- MD(Message Digest,消息摘要算法):MD2、MD4、MD5 等,已经不被推荐使用。
|
||||
- SHA(Secure Hash Algorithm,安全哈希算法):SHA-1 系列安全性低,SHA2,SHA3 系列安全性较高。
|
||||
- 国密算法:例如 SM2、SM3、SM4,其中 SM2 为非对称加密算法,SM4 为对称加密算法,SM3 为哈希算法(安全性及效率和 SHA-256 相当,但更适合国内的应用环境)。
|
||||
- Bcrypt(密码哈希算法):基于 Blowfish 加密算法的密码哈希算法,专门为密码加密而设计,安全性高。
|
||||
- Bcrypt(密码哈希算法):基于 Blowfish 加密算法的密码哈希算法,专门为密码加密而设计,安全性高,属于慢哈希算法。
|
||||
- MAC(Message Authentication Code,消息认证码算法):HMAC 是一种基于哈希的 MAC,可以与任何安全的哈希算法结合使用,例如 SHA-256。
|
||||
- CRC:(Cyclic Redundancy Check,循环冗余校验):CRC32 是一种 CRC 算法,它的特点是生成 32 位的校验值,通常用于数据完整性校验、文件校验等场景。
|
||||
- SipHash:加密哈希算法,它的设计目的是在速度和安全性之间达到一个平衡,用于防御[哈希泛洪 DoS 攻击](https://aumasson.jp/siphash/siphashdos_29c3_slides.pdf)。Rust 默认使用 SipHash 作为哈希算法,从 Redis4.0 开始,哈希算法被替换为 SipHash。
|
||||
|
Loading…
x
Reference in New Issue
Block a user