Compare commits
489 Commits
7e5eec2677
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
db552403d2 | ||
|
|
9f51f13801 | ||
|
|
640560d264 | ||
|
|
89232ffb86 | ||
|
|
8e1f2b72b4 | ||
|
|
f07db479b0 | ||
|
|
c6e216db9a | ||
|
|
598227ab92 | ||
|
|
5546c1c0dc | ||
|
|
02d33f437a | ||
|
|
cd1deff03e | ||
|
|
1e29bf0ca7 | ||
|
|
cca7672cbf | ||
|
|
4cdefcd2bb | ||
|
|
bca627e036 | ||
|
|
ed656f4e53 | ||
|
|
ad29dddc5f | ||
|
|
274f7b6c62 | ||
|
|
7b360b4d06 | ||
|
|
d4890cb337 | ||
|
|
ccb112e375 | ||
|
|
c5bfadb2b1 | ||
|
|
04e8485dca | ||
|
|
5e9a6fa7cd | ||
|
|
5b79be33d1 | ||
|
|
ae1497593c | ||
|
|
987620a2d6 | ||
|
|
dc90ad42c2 | ||
|
|
008fe45df1 | ||
|
|
964cd99e7a | ||
|
|
6e02856586 | ||
|
|
6d4e7c4375 | ||
|
|
f159c6cc0e | ||
|
|
e47dc4711e | ||
|
|
97c4a830a9 | ||
|
|
e9a8e69327 | ||
|
|
67818df180 | ||
|
|
c478a97a99 | ||
|
|
492eb5d64a | ||
|
|
65cfe0e072 | ||
|
|
a38527cc17 | ||
|
|
3baa0caff2 | ||
|
|
10aca33a06 | ||
|
|
4a0ee84e7e | ||
|
|
4e3a3388d6 | ||
|
|
3ed69c87a0 | ||
|
|
1a01da4c34 | ||
|
|
840ae043d6 | ||
|
|
df43af8900 | ||
|
|
81c1ab11e0 | ||
|
|
8eb167073e | ||
|
|
35671a787c | ||
|
|
a95b64b7b2 | ||
|
|
58fa0617ce | ||
|
|
fe9e3ebbe7 | ||
|
|
33c29d2d88 | ||
|
|
235d84afff | ||
|
|
11ad9a1420 | ||
|
|
110b653c2d | ||
|
|
79d9d6d66f | ||
|
|
fd97d2a362 | ||
|
|
172fd4c0ae | ||
|
|
a1bded7e5e | ||
|
|
a7206d120e | ||
|
|
1c9b73cd5d | ||
|
|
8e4fca612a | ||
|
|
a9057466a8 | ||
|
|
20578b535e | ||
|
|
39f792e911 | ||
|
|
42dbfbac36 | ||
|
|
57507cc3af | ||
|
|
491908e87a | ||
|
|
388e18af2a | ||
|
|
4ddad7d316 | ||
|
|
7f060618a9 | ||
|
|
80b25ede3d | ||
|
|
c5c9380ecd | ||
|
|
a6fc0c68e9 | ||
|
|
530efc036d | ||
|
|
7252d739a0 | ||
|
|
5d8eeee8ad | ||
|
|
64436d8cd4 | ||
|
|
702b26f361 | ||
|
|
b0d8de4fd3 | ||
| f6cacead3d | |||
|
|
ced1d2db95 | ||
|
|
26ac8916bf | ||
|
|
2b2be0b6d9 | ||
|
|
97990229c9 | ||
|
|
8edc81b4c2 | ||
|
|
35b41067e1 | ||
|
|
44f2a8c540 | ||
|
|
b649c1ec0c | ||
|
|
a10ca0ad31 | ||
|
|
d668c20082 | ||
|
|
6b65bd4811 | ||
|
|
2989b25ad6 | ||
|
|
c20bea9ea7 | ||
|
|
ecd049c842 | ||
|
|
ab6898d7eb | ||
|
|
3f20f381a8 | ||
|
|
e2fe983467 | ||
|
|
0e2445ccbf | ||
|
|
68c21c80d1 | ||
|
|
7d7d8746e6 | ||
|
|
d4bf768fe3 | ||
|
|
dee8180b4d | ||
|
|
dcafbb38d8 | ||
|
|
b90bb79459 | ||
|
|
49fb828650 | ||
|
|
1bdc9db292 | ||
|
|
e67c9a4cae | ||
|
|
bcf0b15fd0 | ||
|
|
ed2324ccb2 | ||
|
|
3ab6dfcb48 | ||
|
|
965f75a3ee | ||
|
|
3383d032a7 | ||
|
|
caf011a1fc | ||
|
|
d3e1f2b5c3 | ||
|
|
434e83d389 | ||
|
|
5fe5320e73 | ||
|
|
ed116d4bb7 | ||
|
|
088e0ead13 | ||
|
|
449caf7a6c | ||
|
|
4cbfb1bda2 | ||
|
|
e861ac1ece | ||
|
|
2c9278a410 | ||
|
|
63c89cf851 | ||
|
|
2e854a4d9f | ||
|
|
ad4e53c93d | ||
|
|
1da87e132c | ||
|
|
92dc46a702 | ||
|
|
fa0a80f1f9 | ||
|
|
94a8f8eadc | ||
|
|
e3e6bc99ac | ||
|
|
35d84c57b1 | ||
|
|
3a808113ba | ||
|
|
301102efe6 | ||
|
|
26d4122be9 | ||
|
|
8e90e58987 | ||
|
|
8fad9108f1 | ||
|
|
a5476b5d09 | ||
|
|
362ccaec26 | ||
|
|
ffe73a4a2a | ||
|
|
48a3e7614b | ||
|
|
65a711ad96 | ||
|
|
3ddc976a66 | ||
|
|
89f8bd9db4 | ||
|
|
db56eddf84 | ||
|
|
5e565ecfc6 | ||
|
|
4b03f8d44e | ||
|
|
1eea88ccb8 | ||
|
|
a22cc0d592 | ||
|
|
e2de3c1a16 | ||
|
|
7a53199fb8 | ||
|
|
521d591047 | ||
|
|
6a6dfd5bed | ||
|
|
7e2c4317c3 | ||
|
|
da4093838f | ||
|
|
f437f84ba4 | ||
|
|
e71cde7bb8 | ||
|
|
f57adf0481 | ||
|
|
c6be75ec7a | ||
|
|
2764227a0e | ||
|
|
0822e5052f | ||
|
|
1d858eb6c1 | ||
|
|
b69bd6cdd8 | ||
|
|
3ae2a0dc15 | ||
|
|
3928ac7dc6 | ||
|
|
e5b3cfead4 | ||
|
|
d640bd00e8 | ||
|
|
b5f09da87d | ||
|
|
78eb257c4f | ||
|
|
16e06d6be2 | ||
|
|
f8c55e9709 | ||
|
|
95793600ba | ||
|
|
28af3a569b | ||
|
|
ffcfb75f9e | ||
|
|
45de3952b7 | ||
|
|
cf6f069e79 | ||
|
|
e55fa5a32b | ||
|
|
25b226d1a6 | ||
|
|
a74f948934 | ||
|
|
a60eeabdd6 | ||
|
|
de65d0a208 | ||
|
|
e0829d4282 | ||
|
|
36dee5bcc4 | ||
|
|
fda63dcc4a | ||
|
|
fcf534828e | ||
|
|
44763e6487 | ||
|
|
2a4931ac51 | ||
|
|
275372be35 | ||
|
|
79c4982792 | ||
|
|
be52af362d | ||
|
|
4e9fed19e8 | ||
|
|
012f4b44cb | ||
|
|
d04f72efc0 | ||
|
|
64d8c576ab | ||
|
|
d44fd7921f | ||
|
|
d2e278dcce | ||
|
|
142b4deb8a | ||
|
|
8b22a93919 | ||
|
|
3ce8f7f062 | ||
|
|
4fb1f7e7f7 | ||
|
|
bb028ceafc | ||
|
|
89a8616cfc | ||
|
|
25cd3fc411 | ||
|
|
c3bf955058 | ||
|
|
aa006e2dab | ||
|
|
4f13a6c959 | ||
|
|
838ec5ccc8 | ||
|
|
d2d901b37c | ||
|
|
6aca9f0a96 | ||
|
|
6faa9ac410 | ||
|
|
c56c06555e | ||
|
|
3c65a5f827 | ||
|
|
a569b77b6c | ||
|
|
50e7d02355 | ||
|
|
ac43337b3f | ||
|
|
e9000354db | ||
|
|
ed5f2297cf | ||
|
|
f657d2e2ed | ||
|
|
130714fe74 | ||
|
|
6b5e042d3d | ||
|
|
6ae820123d | ||
| 92658eb0b1 | |||
| 049240dc16 | |||
| db8f9eb5c9 | |||
| 92dc60f7a3 | |||
| 31be54e703 | |||
| 60609e2442 | |||
|
|
36b9bc26b0 | ||
|
|
04bb5d26c7 | ||
|
|
762decee30 | ||
|
|
f247130c0a | ||
|
|
bf8b6b42d3 | ||
|
|
93441d04fd | ||
|
|
d417c36d9a | ||
|
|
69df0c7a09 | ||
|
|
07ac8271a7 | ||
|
|
630ed107ea | ||
|
|
0b58982907 | ||
|
|
b8c173c529 | ||
|
|
1137d1c420 | ||
|
|
4fe88021e4 | ||
|
|
148c94e386 | ||
|
|
26c14bbc9d | ||
|
|
8a0d036a14 | ||
|
|
b96041b9e1 | ||
|
|
b0e7e87e08 | ||
|
|
dc13365c07 | ||
|
|
246983ea3a | ||
|
|
06a9726bca | ||
| 93395e1aae | |||
|
|
14a371447f | ||
|
|
a20f47a43f | ||
|
|
807314bd10 | ||
|
|
5b53e6dc6f | ||
|
|
3577791743 | ||
|
|
f3dcfd77f6 | ||
|
|
bd6bb93bac | ||
|
|
25678c96cd | ||
|
|
500af61cc4 | ||
|
|
bd1c088091 | ||
|
|
a204b15618 | ||
|
|
b59337a36b | ||
|
|
443cb1085b | ||
|
|
f4ac1d4427 | ||
|
|
5bf56c33db | ||
|
|
ba967259bd | ||
|
|
353e984695 | ||
|
|
fef5679e49 | ||
|
|
2976a65082 | ||
|
|
1e3a90a430 | ||
|
|
864e4a3f43 | ||
|
|
720230cd82 | ||
|
|
d689e04464 | ||
|
|
4ce97c08b5 | ||
|
|
5362e59e11 | ||
|
|
e00dbdc108 | ||
|
|
785b694479 | ||
|
|
ce92d62bdf | ||
|
|
07c9a23f22 | ||
|
|
6f56194fb5 | ||
|
|
698530d66a | ||
|
|
5d462efce6 | ||
|
|
34562345a9 | ||
|
|
d2f82e210c | ||
|
|
183625869f | ||
|
|
c629b0ba50 | ||
|
|
22bf36ef47 | ||
|
|
21b761bd34 | ||
|
|
d844663f86 | ||
|
|
c06915ddfe | ||
|
|
73eab827a9 | ||
|
|
3517559617 | ||
|
|
c8d37b3e2f | ||
|
|
9fb7fccdf3 | ||
|
|
cfc2c162df | ||
|
|
db8d2ddeb5 | ||
|
|
1669041d56 | ||
|
|
b2ae12d2db | ||
|
|
800763897e | ||
|
|
bad8149951 | ||
|
|
316e3c56cd | ||
|
|
293252d913 | ||
|
|
6edbdbd79e | ||
|
|
8563c9aca0 | ||
|
|
c0f21f27b0 | ||
|
|
3c3ab1f41e | ||
|
|
19ce2bfe4f | ||
|
|
093eb9e9b2 | ||
|
|
0f5f9327b9 | ||
|
|
69f07de1f6 | ||
|
|
080746df22 | ||
|
|
fa251a7910 | ||
|
|
bb5cfc9c7b | ||
|
|
a3d445549a | ||
|
|
572dd332b8 | ||
|
|
0b72fb58f1 | ||
|
|
828d865ca8 | ||
|
|
2e90007639 | ||
|
|
3f77333e4e | ||
|
|
19325fe22b | ||
|
|
baac0d4fee | ||
|
|
11850cbb51 | ||
|
|
8f82f1fc8e | ||
|
|
ba440e4a32 | ||
|
|
1772fde535 | ||
|
|
e37ec41b6e | ||
|
|
7c539ad040 | ||
|
|
279c73e29f | ||
|
|
34991d580a | ||
|
|
df5101160b | ||
|
|
82a6acfe5e | ||
|
|
ace697e380 | ||
|
|
eff9847a5d | ||
|
|
553931005d | ||
|
|
35d39bcea0 | ||
|
|
6c0404485c | ||
|
|
e593bfaa81 | ||
|
|
254062c161 | ||
|
|
b2805148b6 | ||
|
|
8ef6aaa8c5 | ||
|
|
fc8cdbf2d9 | ||
|
|
08b7bc1a78 | ||
|
|
3117618172 | ||
|
|
64b66a0b38 | ||
|
|
3d89f7a47e | ||
|
|
24ce571d45 | ||
|
|
0d653ba29f | ||
|
|
c88a6e4a2a | ||
|
|
ca7facff06 | ||
|
|
a8c6f344de | ||
|
|
6bcbd361e7 | ||
|
|
e9f9244199 | ||
|
|
69b0aad449 | ||
|
|
3cb2d5cdcc | ||
|
|
7499ea6ee3 | ||
|
|
3381d8c2e8 | ||
|
|
6d9a4eb48f | ||
|
|
4d28c2f1a6 | ||
|
|
c65d93d529 | ||
|
|
40ffdb90b8 | ||
|
|
308e53cce7 | ||
|
|
48b5f46d52 | ||
|
|
f507171cfc | ||
|
|
969fcc285b | ||
|
|
47f5222bbf | ||
| ebf8665806 | |||
| f631fe6ec4 | |||
|
|
bace3202f5 | ||
|
|
01f013fbe8 | ||
| d2930c17dd | |||
| e4d198c890 | |||
| 828b7848cc | |||
| 7e1b790441 | |||
| 6f7f99f582 | |||
|
|
b856a9af7b | ||
|
|
4e686152a4 | ||
|
|
670539a025 | ||
|
|
5f2fc404c2 | ||
| a871d01214 | |||
| 4197045981 | |||
| 48d52cf098 | |||
| fb01d0a2c4 | |||
| 267f93d544 | |||
| baea8f6802 | |||
| 0bb208569f | |||
| 7765c48ffb | |||
| e7b24a85b7 | |||
| a0ed38fd29 | |||
| 4ce0974356 | |||
| bb73633973 | |||
| 44f37fc0ff | |||
| 5e84650984 | |||
| 8d6e169b8c | |||
| d01dd144f4 | |||
| a83e5897c5 | |||
| 33543a3445 | |||
| 68b9b34345 | |||
| 2034b8f8c7 | |||
| 2769db29e7 | |||
| e7ed863e79 | |||
| c78febc0d7 | |||
| e5b607eb9d | |||
| 6e7fd59764 | |||
| 0b6112106a | |||
| 4a5c580031 | |||
| 5cd38c6fd3 | |||
| 63725457cb | |||
| edc0638cb7 | |||
| 88d8d4abd3 | |||
| af390d79b2 | |||
| 6004b5fddb | |||
| dab013e3bb | |||
| 25af13f91f | |||
| fa247a16c1 | |||
| 52ad7e30aa | |||
| 0cd5c1ef28 | |||
| e608c50af1 | |||
| f7f3ce18ba | |||
| 44e6f4122f | |||
| 5400fe07ee | |||
| 63a9ed1bca | |||
| 8254dd60a5 | |||
| d541cd7dbc | |||
| 949bad547e | |||
| 9f8ee7e160 | |||
| 46d3efb46d | |||
| f9e5d27f6b | |||
| 64d6fcdee5 | |||
| ed84bf7053 | |||
| d4e8a39fee | |||
| 207fbfd0ff | |||
| ab88027378 | |||
| 1190043b71 | |||
| 9b63b9df01 | |||
| d911e59895 | |||
| 36a2550982 | |||
| 40918e6277 | |||
| 3a57ae584a | |||
| 3c2996feeb | |||
| 33b098bea7 | |||
| 0544303a88 | |||
| 0fb3dc2432 | |||
| b894b5bbb6 | |||
| 1362552f08 | |||
| dd0c822faf | |||
| 326b98edc9 | |||
| 89c4ce2077 | |||
| 3f2b6189ba | |||
| 826d2c9615 | |||
|
|
a51a1402d2 | ||
|
|
c734d1973c | ||
| 63fab22414 | |||
| be40280c8c | |||
| 1d6198e9d8 | |||
| b1ff440a5f | |||
|
|
53cce93ee0 | ||
|
|
4bbb9e44cf | ||
| 342715e493 | |||
|
|
13f3a43ddc | ||
| a6b6032921 | |||
| 9ffa6b3eab | |||
|
|
ded0e315b8 | ||
|
|
5cb990c270 | ||
| 2a31c8f42d | |||
|
|
4016aa61e2 | ||
| 3dec6613e8 | |||
|
|
0f6c62e459 | ||
| 632324d94b | |||
|
|
bd3884c617 | ||
| cb3ef91737 | |||
|
|
fa8dd3f67f | ||
|
|
56e693cfa0 | ||
|
|
bc069d1211 | ||
|
|
c32dd7ead5 | ||
|
|
e38a9a9333 | ||
|
|
b52561bf68 | ||
|
|
febfc63b9e | ||
|
|
f38edad36d | ||
|
|
38b3752860 | ||
|
|
eff5492b2e | ||
|
|
2cbdca091c | ||
|
|
999ef6c2c4 | ||
|
|
e00410e379 | ||
|
|
28e6e75599 | ||
|
|
9aca3a3e1e |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1 +1 @@
|
||||
.idea/
|
||||
.idea/
|
||||
|
||||
22
Git/git rebase.md
Normal file
22
Git/git rebase.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# Git Rebase
|
||||
## Rebasing
|
||||
git中,通常有两种方式将一个分支上的变动整合到另一个分支:merge和rebase。
|
||||
- merge:将两个不同branch相对其最近公共祖先节点的修改进行merge操作,产生一个新的snapshot和commit
|
||||
- rebase:将c3和c4相对最近公共祖先节点c2的修改进行rebase操作,将c4相对c2的修改应用到c3上。
|
||||
|
||||
### rebase细节
|
||||
找到最近公共祖先节点,将当前节点相对公共祖先节点引入的变动保存到临时文件中,并且将当前branch的指针移动到想要rebase到的目标分支上,最后应用保存到临时文件中的变动。
|
||||
### rebase和merge比较
|
||||
rebase操作和merge操作最终产物是相同的,通过两种方式整合的分支最后都完全相同。但是相比较于merge,rebase操作产生的git log更加干净,其提交历史更像是一条直线,即使存在并行修改,在提交历史中也更像是串行提交的。
|
||||
### rebase --onto
|
||||
对于rebase操作,可以将branch 1相对于branch 1和branch 2公共祖先节点的变动rebase onto到branch 3上去。
|
||||
```shell
|
||||
# 将client相对于client和server公共先祖节点的修改应用到master分支上
|
||||
git rebase --onto master server client
|
||||
```
|
||||
可以通过git rebase \<base-branch\> \<topic-branch\>命令来执行rebase操作,这样在执行rebase操作时无需先切换到topic-branch分支,该命令会切换到rebase分支并在base branch上执行replay操作。
|
||||
## rebase操作的危险
|
||||
不要对存在于本地库之外(远程仓库也存在)的提交进行rebase操作,如果对一个提交执行了rebase操作,并且其他开发者基于该提交进行了开发工作,这样会造成问题。
|
||||
在git fetch远程分支之后,可以通过git rebase来代替git merge,从而保持提交历史的干净。
|
||||
可以通过git pull --rebase来简化每次fetch后手动rebase的过程。
|
||||
也可以设置git config --global pull.rebase true来默认在git pull时使用rebase。
|
||||
1784
Golang/Golang Document.md
Normal file
1784
Golang/Golang Document.md
Normal file
File diff suppressed because it is too large
Load Diff
239
Golang/ent.md
Normal file
239
Golang/ent.md
Normal file
@@ -0,0 +1,239 @@
|
||||
# ent
|
||||
## Introduction
|
||||
ent是一个简单、功能强大的entity framework。下面,将会是一个ent的使用示例。
|
||||
|
||||
### init project
|
||||
```bash
|
||||
go mod init entdemo
|
||||
```
|
||||
### 创建schema
|
||||
在entdemo目录下运行
|
||||
```bash
|
||||
go run -mod=mod entgo.io/ent/cmd/ent new User
|
||||
```
|
||||
上述命令会为`User`创建schema,创建位置位于`entdemo/ent/schema`目录下,`entdemo/ent/schema/user.go`文件内容如下:
|
||||
```go
|
||||
package schema
|
||||
|
||||
import "entgo.io/ent"
|
||||
|
||||
// User holds the schema definition for the User entity.
|
||||
type User struct {
|
||||
ent.Schema
|
||||
}
|
||||
|
||||
// Fields of the User.
|
||||
func (User) Fields() []ent.Field {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Edges of the User.
|
||||
func (User) Edges() []ent.Edge {
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
### 向User实体中添加字段
|
||||
可以向User中添加两个字段,修改`Field`内容为如下:
|
||||
```go
|
||||
// Fields of the User.
|
||||
func (User) Fields() []ent.Field {
|
||||
return []ent.Field{
|
||||
field.Int("age").
|
||||
Positive(),
|
||||
field.String("name").
|
||||
Default("unknown"),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### go generate
|
||||
之后,可以调用`go generate`:
|
||||
```bash
|
||||
go generate ./ent
|
||||
```
|
||||
产生内容如下:
|
||||
```
|
||||
ent
|
||||
├── client.go
|
||||
├── config.go
|
||||
├── context.go
|
||||
├── ent.go
|
||||
├── generate.go
|
||||
├── mutation.go
|
||||
... truncated
|
||||
├── schema
|
||||
│ └── user.go
|
||||
├── tx.go
|
||||
├── user
|
||||
│ ├── user.go
|
||||
│ └── where.go
|
||||
├── user.go
|
||||
├── user_create.go
|
||||
├── user_delete.go
|
||||
├── user_query.go
|
||||
└── user_update.go
|
||||
```
|
||||
|
||||
### 创建表并插入实体
|
||||
#### 创建表
|
||||
在执行完上述步骤后,可以通过如下代码来创建table
|
||||
```golang
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
|
||||
"entdemo/ent"
|
||||
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
)
|
||||
|
||||
func main() {
|
||||
client, err := ent.Open("mysql", "<user>:<pass>@tcp(<host>:<port>)/<database>?parseTime=True")
|
||||
if err != nil {
|
||||
log.Fatalf("failed opening connection to mysql: %v", err)
|
||||
}
|
||||
defer client.Close()
|
||||
// Run the auto migration tool.
|
||||
if err := client.Schema.Create(context.Background()); err != nil {
|
||||
log.Fatalf("failed creating schema resources: %v", err)
|
||||
}
|
||||
}
|
||||
```
|
||||
> 在执行上述代码前,需要先通过`create database xxx`先创建数据库。
|
||||
|
||||
#### 插入实体
|
||||
通过如下代码,可以插入实体:
|
||||
```golang
|
||||
entdemo/start.go
|
||||
func CreateUser(ctx context.Context, client *ent.Client) (*ent.User, error) {
|
||||
u, err := client.User.
|
||||
Create().
|
||||
SetAge(30).
|
||||
SetName("a8m").
|
||||
Save(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed creating user: %w", err)
|
||||
}
|
||||
log.Println("user was created: ", u)
|
||||
return u, nil
|
||||
}
|
||||
```
|
||||
|
||||
### 查询实体
|
||||
查询实体可以通过如下代码实现:
|
||||
```golang
|
||||
func QueryUser(ctx context.Context, client *ent.Client) (ul []*ent.User, err error) {
|
||||
ul, err = client.User.
|
||||
Query().
|
||||
Where(
|
||||
user.Age(30),
|
||||
user.Name("a8m"),
|
||||
).All(ctx)
|
||||
if err != nil {
|
||||
log.Errorf("failed querying user: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
```
|
||||
|
||||
### 添加关联关系
|
||||
另外创建两个实体`Car`和`Group`
|
||||
```bash
|
||||
go run -mod=mod entgo.io/ent/cmd/ent new Car Group
|
||||
```
|
||||
修改Car实体如下
|
||||
```golang
|
||||
package schema
|
||||
|
||||
import (
|
||||
"entgo.io/ent"
|
||||
"entgo.io/ent/schema/field"
|
||||
)
|
||||
|
||||
// Car holds the schema definition for the Car entity.
|
||||
type Car struct {
|
||||
ent.Schema
|
||||
}
|
||||
|
||||
// Fields of the Car.
|
||||
func (Car) Fields() []ent.Field {
|
||||
return []ent.Field{
|
||||
field.String("model"),
|
||||
field.Time("registered_at"),
|
||||
}
|
||||
}
|
||||
|
||||
// Edges of the Car.
|
||||
func (Car) Edges() []ent.Edge {
|
||||
return nil
|
||||
}
|
||||
```
|
||||
修改Group实体如下:
|
||||
```golang
|
||||
package schema
|
||||
|
||||
import (
|
||||
"entgo.io/ent"
|
||||
"entgo.io/ent/schema/field"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
// Group holds the schema definition for the Group entity.
|
||||
type Group struct {
|
||||
ent.Schema
|
||||
}
|
||||
|
||||
// Fields of the Group.
|
||||
func (Group) Fields() []ent.Field {
|
||||
return []ent.Field{
|
||||
field.String("name").
|
||||
// Regexp validation for group name.
|
||||
Match(regexp.MustCompile("[a-zA-Z_]+$")),
|
||||
}
|
||||
}
|
||||
|
||||
// Edges of the Group.
|
||||
func (Group) Edges() []ent.Edge {
|
||||
return nil
|
||||
}
|
||||
```
|
||||
并修改User实体如下,为其新增`Edges`方法
|
||||
```golang
|
||||
package schema
|
||||
|
||||
import (
|
||||
"entgo.io/ent"
|
||||
"entgo.io/ent/schema/edge"
|
||||
"entgo.io/ent/schema/field"
|
||||
)
|
||||
|
||||
// User holds the schema definition for the User entity.
|
||||
type User struct {
|
||||
ent.Schema
|
||||
}
|
||||
|
||||
// Fields of the User.
|
||||
func (User) Fields() []ent.Field {
|
||||
return []ent.Field{
|
||||
field.Int("age").Positive(),
|
||||
field.String("name").Default("unknown"),
|
||||
}
|
||||
}
|
||||
|
||||
// Edges of the User.
|
||||
func (User) Edges() []ent.Edge {
|
||||
return []ent.Edge{
|
||||
edge.To("cars", Car.Type),
|
||||
}
|
||||
}
|
||||
```
|
||||
可知,Car和User实体的关系如下图所示:
|
||||
- 一个user可以拥有多个car
|
||||
|
||||
<img loading="lazy" src="https://entgo.io/images/assets/re_user_cars.png" alt="er-user-cars" class="img_ev3q" id="er-user-cars">
|
||||
|
||||
运行`go generate ./ent`后如下:
|
||||
```bash
|
||||
577
Golang/go-sqlx.md
Normal file
577
Golang/go-sqlx.md
Normal file
@@ -0,0 +1,577 @@
|
||||
- [SQLX](#sqlx)
|
||||
- [Getting Start with SQLite](#getting-start-with-sqlite)
|
||||
- [Handle Types](#handle-types)
|
||||
- [Connecting to database](#connecting-to-database)
|
||||
- [Open Db and connect in same time](#open-db-and-connect-in-same-time)
|
||||
- [Quering](#quering)
|
||||
- [Exec](#exec)
|
||||
- [bindvars](#bindvars)
|
||||
- [Rebind](#rebind)
|
||||
- [bindvars is only used for parameterization](#bindvars-is-only-used-for-parameterization)
|
||||
- [Query](#query)
|
||||
- [Query errors](#query-errors)
|
||||
- [Connection Closed Scenes](#connection-closed-scenes)
|
||||
- [Queryx](#queryx)
|
||||
- [QueryRow](#queryrow)
|
||||
- [QueryRowx](#queryrowx)
|
||||
- [Get \& Select](#get--select)
|
||||
- [scannable](#scannable)
|
||||
- [Exec和Query在归还连接池上的差异](#exec和query在归还连接池上的差异)
|
||||
- [Transactions](#transactions)
|
||||
- [PreparedStatement](#preparedstatement)
|
||||
- [PreparedStatement事务操作](#preparedstatement事务操作)
|
||||
- [QueryHelper](#queryhelper)
|
||||
- [`In` Queries](#in-queries)
|
||||
- [`Named Queries`](#named-queries)
|
||||
- [Advanced Scanning](#advanced-scanning)
|
||||
- [struct embeded](#struct-embeded)
|
||||
- [Scan Destination Safety](#scan-destination-safety)
|
||||
- [Control Name Mapping](#control-name-mapping)
|
||||
- [sqlx.DB.MapperFunc](#sqlxdbmapperfunc)
|
||||
- [Slice Scan \& Map Scan](#slice-scan--map-scan)
|
||||
- [Scanner/Valuer](#scannervaluer)
|
||||
- [Connection Pool](#connection-pool)
|
||||
|
||||
|
||||
# SQLX
|
||||
## Getting Start with SQLite
|
||||
为了安装sqlx和database driver,可以通过如下命令:
|
||||
```bash
|
||||
$ go get github.com/jmoiron/sqlx
|
||||
$ go get github.com/mattn/go-sqlite3
|
||||
```
|
||||
|
||||
## Handle Types
|
||||
`sqlx`和`database/sql`等价,主要有四种类型:
|
||||
- `sqlx.DB`: 和`sql.DB`等价,用于表示database
|
||||
- `sqlx.Tx`: 和`sql.Tx`等价,用于表示事务
|
||||
- `sqlx.Stmt`: 和`sql.Stmt`等价,用于表示Prepared Statement
|
||||
- `sqlx.NamedStmt`: 用于表示`Prepared Statement with support for named parameters`
|
||||
|
||||
Handle Types中都嵌入了其`database/sql`中等价的对象,在调用`sqlx.DB.query`时,其实际调用的代码和`sql.DB.Query`。
|
||||
|
||||
> 例如,`sqlx.DB`实现中包含了`sql.DB`的对象引用,在调用`sqlx.DB`的方法时,实际会调用内部嵌套的`sql.DB`中的方法。
|
||||
|
||||
sqlx中除了上述类型外,还引入了两个cursor类型:
|
||||
- `sqlx.Rows`: 等价于`sql.Rows`,为`Queryx`返回的cursor
|
||||
- `sqlx.Row`: 等价于`sql.Row`:,由`QueryRowx`返回的结果
|
||||
|
||||
在上述两个cursor类型中,`sqlx.Rows`嵌入了`sql.Rows`。`但是,由于底层实现不可访问,sqlx.Row对sql.Row的部分内容进行了重新实现,标准接口保持一致`。
|
||||
|
||||
### Connecting to database
|
||||
一个`DB`实例代表的只是一个数据库的抽象,其并不代表实际连接。`故而,对DB实例进行创建并不会返回error或是抛出panic`。
|
||||
|
||||
DB实例在内部维护了一个连接池,并且,在`初次需要获取连接时`去尝试建立连接。
|
||||
|
||||
创建`sqlx.DB`有两种方式,
|
||||
- 通过`sqlx.DB.Open`方法进行创建
|
||||
- 通过`sqlx.DB.NewDb`方法进行创建,该方法接收一个已经存在的`sql.DB`实例
|
||||
|
||||
```golang
|
||||
var db *sqlx.DB
|
||||
|
||||
// exactly the same as the built-in
|
||||
db = sqlx.Open("sqlite3", ":memory:")
|
||||
|
||||
// from a pre-existing sql.DB; note the required driverName
|
||||
db = sqlx.NewDb(sql.Open("sqlite3", ":memory:"), "sqlite3")
|
||||
|
||||
// force a connection and test that it worked
|
||||
err = db.Ping()
|
||||
```
|
||||
|
||||
#### Open Db and connect in same time
|
||||
在某些场景下,可能希望在创建数据库实例时就连接数据库,可以使用如下方法,它们会在创建数据库的同时调用`Ping`
|
||||
- `sqlx.Connect`:如果在遇到错误时,会返回error
|
||||
- `sqlx.MustConnect`:在遇到错误时,会发生panic
|
||||
|
||||
```go
|
||||
var err error
|
||||
// open and connect at the same time:
|
||||
db, err = sqlx.Connect("sqlite3", ":memory:")
|
||||
|
||||
// open and connect at the same time, panicing on error
|
||||
db = sqlx.MustConnect("sqlite3", ":memory:")
|
||||
```
|
||||
## Quering
|
||||
handle Types中实现了和`database/sql`中同样的database方法:
|
||||
- `Exec(...) (sql.Result, error)`: 和`database/sql`中一致
|
||||
- `Query(...) (*sql.Rows, error)`:和`database/sql`中一致
|
||||
- `QueryRow(...) *sqlRow`:和`database/sql`中一致
|
||||
|
||||
如下是内置方法的拓展:
|
||||
- `MustExec() sql.Result `: Exec,并且在错误时发生panic
|
||||
- `Queryx(...) (*sqlx.Rows, error)`: Query,但是会返回`sqlx.Rows`
|
||||
- `QueryRowx(...) *sqlx.Row`: QueryRow,但是会返回`sqlx.Row`
|
||||
|
||||
如下是新语意的方法:
|
||||
- `Get(dest interface{}, ...) error`
|
||||
- `Select(dest interface{}, ...) error`
|
||||
|
||||
### Exec
|
||||
`Exec`和`MustExec`方法会从connection pool中获取连接并且执行提供的sql方法。
|
||||
|
||||
> 并且,在Exec向调用方返回`sql.Result`对象之前,连接将会被归还给连接池。
|
||||
>
|
||||
> `此时,server已经向client发送了query text的执行结果,在根据返回结果构建sql.Result对象之前,会将来凝结返回给连接池。`
|
||||
|
||||
```go
|
||||
schema := `CREATE TABLE place (
|
||||
country text,
|
||||
city text NULL,
|
||||
telcode integer);`
|
||||
|
||||
// execute a query on the server
|
||||
result, err := db.Exec(schema)
|
||||
|
||||
// or, you can use MustExec, which panics on error
|
||||
cityState := `INSERT INTO place (country, telcode) VALUES (?, ?)`
|
||||
countryCity := `INSERT INTO place (country, city, telcode) VALUES (?, ?, ?)`
|
||||
db.MustExec(cityState, "Hong Kong", 852)
|
||||
db.MustExec(cityState, "Singapore", 65)
|
||||
db.MustExec(countryCity, "South Africa", "Johannesburg", 27)
|
||||
```
|
||||
|
||||
`db.Exec`返回的结果中包含两部分信息:
|
||||
- `LastInsertedId`: 在mysql中,该字段在使用auto_increment key时,会返回最后插入的id
|
||||
- `RowsAffected`
|
||||
|
||||
#### bindvars
|
||||
上述示例中,`?`占位符在内部调用了`bindvars`,使用占位符能够避免sql注入。
|
||||
|
||||
`database/sql`并不会对query text做任何验证,其将query text和encoded params原封不动的发送给server。
|
||||
|
||||
除非底层driver做了实现,否则query语句会在server端运行sql语句之前执行prepare操作,bindvars每个database可能语法都不相同:
|
||||
- mysql会使用`?`来作为bindvars的占位符
|
||||
- postgresql会使用`$1, $2`来作为bindvars的占位符
|
||||
- sqlite既接收`?`又接收`$1`
|
||||
- oracle接收`:name`语法
|
||||
|
||||
##### Rebind
|
||||
可以通过`sqlx.DB.Rebind(string) string`方法,将使用`?`的query sql转化为当前数据库类型的query sql。
|
||||
|
||||
##### bindvars is only used for parameterization
|
||||
`bindvars机制`只能够被用于参数化,并不允许通过bindvars改变sql语句的结构。例如,如下语句都是不被允许的:
|
||||
```go
|
||||
// doesn't work
|
||||
db.Query("SELECT * FROM ?", "mytable")
|
||||
|
||||
// also doesn't work
|
||||
db.Query("SELECT ?, ? FROM people", "name", "location")
|
||||
```
|
||||
### Query
|
||||
`database/sql`主要通过`Query`方法来执行查询语句并获取row results。`Query`方法会返回一个`sql.Rows`对象和一个error,
|
||||
```go
|
||||
// fetch all places from the db
|
||||
rows, err := db.Query("SELECT country, city, telcode FROM place")
|
||||
|
||||
// iterate over each row
|
||||
for rows.Next() {
|
||||
var country string
|
||||
// note that city can be NULL, so we use the NullString type
|
||||
var city sql.NullString
|
||||
var telcode int
|
||||
err = rows.Scan(&country, &city, &telcode)
|
||||
}
|
||||
// check the error from rows
|
||||
err = rows.Err()
|
||||
```
|
||||
在使用Rows时,应当将其看作是database cursor,而非是结果反序列化之后构成的列表。尽管驱动对结果集的缓存行为各不相同,但是通过`Next`方法对`Rows`中的结果进行迭代仍然可以在result set较大的场景下节省内存的使用,因为`Next`同时只会对一行结果进行扫描。
|
||||
|
||||
`Scan`方法会使用反射将column返回的结果类型映射为go类型(例如string, []byte等)。
|
||||
|
||||
> 在使用`Rows`时,如果并不迭代完整个结果集,请确保调用`rows.Close()`方法将连接返回到连接池中。
|
||||
|
||||
#### Query errors
|
||||
其中,`Query`方返回的error可能是`在服务端进行prepare操作或execute操作时发生的任何异常`,该异常的可能场景如下:
|
||||
- 从连接池中获取了bad connection
|
||||
- 因sql语法、类型不匹配、不正确的field name或table name导致的错误
|
||||
|
||||
在大多数场景下,`Rows.Scan`会复制其从driver获取的数据,因为`Rows.Scan`并无法感知driver对缓冲区进行reuse的方式。类型`sql.RawBytes`可以被用于获取` zero-copy slice of bytes from the actual data returned by the driver`。在下一次调用`Next`方法时,该值将会无效,因为该bytes的内存空间将会被driver重写。
|
||||
|
||||
#### Connection Closed Scenes
|
||||
在调用完`Query`方法后,connection将会等到如下两种场景才会关闭:
|
||||
- Rows中所有的行都通过`Next`方法调用被迭代
|
||||
- rows中所有行未被完全迭代,但是`rows.Close()`方法被调用
|
||||
|
||||
#### Queryx
|
||||
sqlx拓展的`Queryx`方法,其行为和`Query`方法一致,但是实际返回的是`sqlx.Rows`类型,`sqlx.Rows`类型对scan行为进行了拓展:
|
||||
```go
|
||||
type Place struct {
|
||||
Country string
|
||||
City sql.NullString
|
||||
TelephoneCode int `db:"telcode"`
|
||||
}
|
||||
|
||||
rows, err := db.Queryx("SELECT * FROM place")
|
||||
for rows.Next() {
|
||||
var p Place
|
||||
err = rows.StructScan(&p)
|
||||
}
|
||||
```
|
||||
`sqlx.Rows`的主要拓展是支持`StructScan()`,其会自动将结果扫描到struct fields中。
|
||||
> 注意,在使用struct scan时,struct field必须被exported(首字母大写)。
|
||||
|
||||
可以使用`db` struct tag指定field映射到哪个column。默认情况下,会对field name使用`strings.Lower`并和column name相匹配。
|
||||
|
||||
#### QueryRow
|
||||
QueryRow会从server端拉取一行数据。其从connection pool中获取一个连接,并且通过Query执行该查询,并返回一个`Row` object,`Row object`内部包含了`Rows`对象
|
||||
```go
|
||||
row := db.QueryRow("SELECT * FROM place WHERE telcode=?", 852)
|
||||
var telcode int
|
||||
err = row.Scan(&telcode)
|
||||
```
|
||||
|
||||
和Query不同的是,QueryRow并不会返回error,故而,可以对返回结果链式嵌套其他方法调用,例如`Scan`。
|
||||
|
||||
如果在执行`QueryRow`查询时报错,那么该error将会被`Scan`方法返回,如果并没有查询到rows,那么Scan方法将会返回`sql.ErrNoRows`。
|
||||
|
||||
如果Scan操作失败了(例如类型不匹配),error也会被Scan方法返回。
|
||||
|
||||
Row对象内部的`Rows`结构在Scan后就会关闭,`即代表QueryRow使用的连接持续打开,直到Result被扫描后才会关闭`。
|
||||
|
||||
#### QueryRowx
|
||||
`QueryRowx`拓展将会返回一个sqlx.Row`,其实现了和`sqlx.Rows`相同的拓展,示例如下
|
||||
```go
|
||||
var p Place
|
||||
err := db.QueryRowx("SELECT city, telcode FROM place LIMIT 1").StructScan(&p)
|
||||
```
|
||||
#### Get & Select
|
||||
Get/Select和`QueryRow`/`Query`类似,但是其能够节省代码编写,并能提供灵活的扫描语义。
|
||||
|
||||
##### scannable
|
||||
scannable定义如下:
|
||||
- 如果value并不是struct,那么该value是scannable的
|
||||
- 如果value实现了sql.Scanner,那么该value是scannable的
|
||||
- 如果struct没有exported field,那么其是scannble的
|
||||
|
||||
Get和Select对于scannable类型使用了`rows.Scan`方法,对non-scannable类型使用`rows.StructScan`方法,使用示例如下:
|
||||
```go
|
||||
p := Place{}
|
||||
pp := []Place{}
|
||||
|
||||
// this will pull the first place directly into p
|
||||
err = db.Get(&p, "SELECT * FROM place LIMIT 1")
|
||||
|
||||
// this will pull places with telcode > 50 into the slice pp
|
||||
err = db.Select(&pp, "SELECT * FROM place WHERE telcode > ?", 50)
|
||||
|
||||
// they work with regular types as well
|
||||
var id int
|
||||
err = db.Get(&id, "SELECT count(*) FROM place")
|
||||
|
||||
// fetch at most 10 place names
|
||||
var names []string
|
||||
err = db.Select(&names, "SELECT name FROM place LIMIT 10")
|
||||
```
|
||||
|
||||
`Get`和`Select`都会对Rows进行关闭,并且在执行遇到错误时会返回error。
|
||||
|
||||
> 但是,需要注意的是,Select会将整个result set都导入到内存中。如果结果集较大,最好使用传统的`Queryx`/`StructScan`迭代方式。
|
||||
|
||||
### Exec和Query在归还连接池上的差异
|
||||
Exec操作和Query操作在归还连接到连接池的时机有所不同:
|
||||
- `Exec`: `Exec`方法在`server返回执行结果给client之后`,`client根据返回结果构建并返回sql.Result之前`,将会将连接返回给连接池
|
||||
- `Query`: `Query`方法和`Exec`方法不同,其返回信息中包含结果集,必须等待结果集`迭代完成`或`手动调用rows.Close`方法之后,才会归还连接给连接池
|
||||
- `QueryRow`:在返回的Row对象被Scan后,将会归还连接给数据库
|
||||
|
||||
## Transactions
|
||||
为了使用事务,必须通过`DB.Begin()`方法创建事务,如下代码将`不会起作用`:
|
||||
```go
|
||||
// this will not work if connection pool > 1
|
||||
db.MustExec("BEGIN;")
|
||||
db.MustExec(...)
|
||||
db.MustExec("COMMIT;")
|
||||
```
|
||||
> 在通过`Exec`执行语句时,每次都是从数据库获取连接,并在执行完后将连接返还到连接池中。
|
||||
>
|
||||
> `连接池并不保证第二次Exec执行时获取的连接和第一次Exec时获取的来连接相同`。可能`db.MustExec("BEGIN;")`在获取连接->执行->将连接返还连接池后,第二次调用`db.MustExec(...)`时,从数据库获取的连接并不是`Must("BEGIN;")`执行时所在的连接。
|
||||
|
||||
可以按照如下示例来使用事务:
|
||||
```go
|
||||
tx, err := db.Begin()
|
||||
err = tx.Exec(...)
|
||||
err = tx.Commit()
|
||||
```
|
||||
类似的,sqlx同样提供了`Beginx()`方法和`MustBegin`方法,其会返回`sqlx.Tx`而不是`sql.Tx`,示例如下:
|
||||
```go
|
||||
tx := db.MustBegin()
|
||||
tx.MustExec(...)
|
||||
err = tx.Commit()
|
||||
```
|
||||
由于事务是连接状态,Tx对象必须绑定并控制连接池中的一个连接。Tx对象将会在生命周期内维持该connection,仅当commit或rollback被调用时才将连接释放。
|
||||
|
||||
在对Tx对象进行使用时,应当确保调用`commit`或`rollback`中的一个方法,否则连接将会被持有,直至发生垃圾回收。
|
||||
|
||||
在事务的声明周期中,只会关联一个连接,且`在执行其他查询前,row和rows对应的cursor必须被scan或关闭`。
|
||||
|
||||
## PreparedStatement
|
||||
可以通过`sqlx.DB.Prepare()`方法来对想要重用的statements进行prepare操作:
|
||||
```go
|
||||
stmt, err := db.Prepare(`SELECT * FROM place WHERE telcode=?`)
|
||||
row = stmt.QueryRow(65)
|
||||
|
||||
tx, err := db.Begin()
|
||||
txStmt, err := tx.Prepare(`SELECT * FROM place WHERE telcode=?`)
|
||||
row = txStmt.QueryRow(852)
|
||||
```
|
||||
|
||||
prepare操作实际会在数据库运行,故而其需要connection和connection state。
|
||||
|
||||
### PreparedStatement事务操作
|
||||
在使用sqlx进行事务操作时,首先需要通过`db.Begin()`开启事务,且`后续所有想要加入事务的操作(dml/query)都需要通过tx.Exec/tx.Query来执行`。
|
||||
|
||||
如果在开启事务并获取`tx`对象后,后续操作仍然通过`db.Exec/db.Query`来执行,`那么后续操作会从连接池中获取一个新的连接来执行,并不会自动加入已经开启的事务`。
|
||||
|
||||
```go
|
||||
tx, _ = db.Begin()
|
||||
// 错误操作,此处dml会从连接池获取新的连接来执行
|
||||
db.Exec(xxxx)
|
||||
// 正确操作,此处dml会在tx绑定的连接中执行
|
||||
tx.Exec(xxxx)
|
||||
```
|
||||
|
||||
对于PreparedStatement操作,如果想要将其和事务相关联,有如下两种使用方式:
|
||||
- `如果statement此时尚未创建`,可以通过`tx.Prepare`来创建该连接
|
||||
- `如果Statement此时已经创建`,那么可以通过`tx.Stmt(dbStmt)`来获取新的Stmt,新Stmt会和事务所属连接绑定,通过新Stmt执行dml可以加入当前事务
|
||||
|
||||
## QueryHelper
|
||||
`database/sql`并不会对传入的实际query text做任何处理,故而在编写部分语句时可能会较为困难。
|
||||
|
||||
### `In` Queries
|
||||
由于`database/sql`并不会对query text做为何处理,而是直接将其传给driver,故而在处理`in`查询语句时将变得相当困难:
|
||||
```sql
|
||||
SELECT * FROM users WHERE level IN (?);
|
||||
```
|
||||
此处,可以通过`sqlx.In`方法来首先进行处理,示例如下:
|
||||
```golang
|
||||
var levels = []int{4, 6, 7}
|
||||
query, args, err := sqlx.In("SELECT * FROM users WHERE level IN (?);", levels)
|
||||
|
||||
// sqlx.In returns queries with the `?` bindvar, we can rebind it for our backend
|
||||
query = db.Rebind(query)
|
||||
rows, err := db.Query(query, args...)
|
||||
```
|
||||
`sqlx.In`会将query text中所有和`args中slice类型argument相关联的bindvar`拓展到`slice`的长度,并且将args中的参数重新添加到新的argList中,示例如下所示:
|
||||
```go
|
||||
var query string
|
||||
var args []interface{}
|
||||
query, args, err = sqlx.In("select * from location where cities in (?) and code = ? and id in (?)", []string{"BEIJING", "NEW_YORK"},"asahi", []uint64{1, 3})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
query = db.Rebind(query)
|
||||
log.Printf("query transformed: %s\n", query)
|
||||
log.Printf("args transformed: %v, %v, %v, %v, %v\n", args[0], args[1], args[2], args[3], args[4])
|
||||
```
|
||||
其对应结果为
|
||||
```go
|
||||
2025/06/21 16:30:38 query transformed: select * from location where cities in (?, ?) and code = ? and id in (?, ?)
|
||||
2025/06/21 16:30:38 args transformed: BEIJING, NEW_YORK, asahi, 1, 3
|
||||
```
|
||||
其中,包含`[]string{"BEIJING", "NEW_YORK"},"asahi", []uint64{1, 3})`三个元素的args被重新转换为了包含`BEIJING, NEW_YORK, asahi, 1, 3`五个元素的argList。
|
||||
|
||||
### `Named Queries`
|
||||
可以使用`named bindvar`语法来将struct field/map keys和variable绑定在一起,struct field命名遵循`StructScan`。
|
||||
|
||||
关联方法如下:
|
||||
- `NamedQuery(...) (*sqlx.Rows, error)`:和Queryx类似,但是支持named bindvars
|
||||
- `NamedExec(...) (sql.Result, error)`:和Exec类似,但是支持named bindvars
|
||||
|
||||
除此之外,还有额外的类型:
|
||||
- `NamedStmt`: 和`sqlx.Stmt`类似,支持`prepared with named bindvars`
|
||||
|
||||
```go
|
||||
// named query with a struct
|
||||
p := Place{Country: "South Africa"}
|
||||
rows, err := db.NamedQuery(`SELECT * FROM place WHERE country=:country`, p)
|
||||
|
||||
// named query with a map
|
||||
m := map[string]interface{}{"city": "Johannesburg"}
|
||||
result, err := db.NamedExec(`SELECT * FROM place WHERE city=:city`, m)
|
||||
```
|
||||
`Named query execution`和`Named preparation`针对struct和map都起作用,如果希望在所有的query verbs都支持named queries,可以使用named statement
|
||||
```go
|
||||
p := Place{TelephoneCode: 50}
|
||||
pp := []Place{}
|
||||
|
||||
// select all telcodes > 50
|
||||
nstmt, err := db.PrepareNamed(`SELECT * FROM place WHERE telcode > :telcode`)
|
||||
err = nstmt.Select(&pp, p)
|
||||
```
|
||||
`Named query`会将`:param`语法转化为底层数据库支持的`bindvar`语法,并且在执行时执行mapping,故而其对sqlx支持的所有数据库都适用。
|
||||
|
||||
可以使用`sqlx.Named`方式来将`:param`语法转化为`?`形式,并后续和`sqlx.In`相结合,示例如下:
|
||||
```go
|
||||
var query string
|
||||
params := map[string]any{
|
||||
"code": "ASAHI",
|
||||
"cities": []string{"BEIJING", "NEWYORK"},
|
||||
"id": []uint64{1, 3},
|
||||
}
|
||||
var args []any
|
||||
query, args, err = sqlx.Named("select * from location where cities in (:cities) and code = :code and id in (:id)",
|
||||
params)
|
||||
if err != nil {
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
log.Printf("query after named transformation: %s\n", query)
|
||||
log.Printf("args after named transformation: %v\n", args)
|
||||
```
|
||||
上述的输出为
|
||||
```go
|
||||
2025/06/21 16:58:39 query after named transformation: select * from location where cities in (?) and code = ? and id in (?)
|
||||
2025/06/21 16:58:39 args after named transformation: [[BEIJING NEWYORK] ASAHI [1 3]]
|
||||
```
|
||||
其中,`sqlx.Named`会将`struct/map`参数转化为`argList`,并且将`named query`转化为`query with bindvar syntax`的形式,后续,`query with bindvar syntax`还可以结合`sqlx.In`来使用
|
||||
```go
|
||||
arg := map[string]interface{}{
|
||||
"published": true,
|
||||
"authors": []{8, 19, 32, 44},
|
||||
}
|
||||
query, args, err := sqlx.Named("SELECT * FROM articles WHERE published=:published AND author_id IN (:authors)", arg)
|
||||
query, args, err := sqlx.In(query, args...)
|
||||
query = db.Rebind(query)
|
||||
db.Query(query, args...)
|
||||
```
|
||||
|
||||
## Advanced Scanning
|
||||
Struct Scan支持embeded struct,并且对`embeded attribute和method access`使用和golang相同的优先顺序:
|
||||
- 即struct scan在执行过程中,如果struct A嵌套了struct B,并且A和B中都拥有名为`ID`的字段,那么将query结果扫描到A中时,query result中的`id`将会被优先映射到A中的`ID`字段中,而`B.ID`字段则会被忽略
|
||||
|
||||
```go
|
||||
type struct B {
|
||||
ID string
|
||||
xxx
|
||||
}
|
||||
|
||||
type struct A {
|
||||
B
|
||||
ID string
|
||||
xxx
|
||||
}
|
||||
|
||||
a := A{}
|
||||
db.Select(&a, "select id from table_a where xxx")
|
||||
// 会被映射到A中名为ID的struct field中,B.ID在struct scan时会被忽略
|
||||
```
|
||||
### struct embeded
|
||||
在日常使用中,通常会使用`struct embeded`通常用作`将多张tables共享的公共field抽取到一个embeded struct中`,示例如下:
|
||||
```go
|
||||
type AutoIncr struct {
|
||||
ID uint64
|
||||
Created time.Time
|
||||
}
|
||||
|
||||
type Place struct {
|
||||
Address string
|
||||
AutoIncr
|
||||
}
|
||||
|
||||
type Person struct {
|
||||
Name string
|
||||
AutoIncr
|
||||
}
|
||||
```
|
||||
上述示例中,person和place都支持在struct scan时接收id和created字段。并且,`其是递归的`。
|
||||
```go
|
||||
type Employee struct {
|
||||
BossID uint64
|
||||
EmployeeID uint64
|
||||
Person
|
||||
}
|
||||
```
|
||||
上述示例中,`Employee`中包含了来自`Person`中的`Name`和`AutoIncr ID/Created`字段。
|
||||
|
||||
在sqlx构建field name和field address的映射时,在将数据扫描到struct之前,无法知晓name是否会在struct tree中遭遇两次。
|
||||
|
||||
> field name和field address的映射关系,其表示如下:
|
||||
> - field name: 该struct field对应的name
|
||||
> - field address: 该struct field对应的address
|
||||
>
|
||||
> 例如:
|
||||
> ```go
|
||||
> type A struct {
|
||||
> ID int `db:"id"`
|
||||
> }
|
||||
> 其中,field name为`id`,而field address则为`&a.ID`所代表的地址
|
||||
>
|
||||
> field name到field address的映射关系将会在后续structScan时使用
|
||||
|
||||
并不像go中`ambiguous selectors`会导致异常,`struct Scan将会选择遇到的第一个拥有相同name的field`。以最外层的struct作为tree root,embeded struct作为child root,其遵循`shallowest, top-most`的匹配原则,即`离root最近,且在同一struct定义中,定义靠上的struct field更优先`。
|
||||
|
||||
例如,在如下定义中:
|
||||
```go
|
||||
type PersonPlace struct {
|
||||
Person
|
||||
Place
|
||||
}
|
||||
```
|
||||
|
||||
### Scan Destination Safety
|
||||
默认情况下,如果column并没有匹配到对应的field,将会返回一个error。
|
||||
|
||||
但是,如果想要在column未匹配到field的情况下,不返回error,那么可以使用`db.Unsafe`方法,示例如下:
|
||||
```go
|
||||
var p Person
|
||||
// err here is not nil because there are no field destinations for columns in `place`
|
||||
err = db.Get(&p, "SELECT * FROM person, place LIMIT 1;")
|
||||
|
||||
// this will NOT return an error, even though place columns have no destination
|
||||
udb := db.Unsafe()
|
||||
err = udb.Get(&p, "SELECT * FROM person, place LIMIT 1;")
|
||||
```
|
||||
|
||||
### Control Name Mapping
|
||||
被用作target field的struct field首字母必须大写,从而使该field可以被sqlx访问。
|
||||
|
||||
因为struct field的首字母为大写,故而sqlx使用了`NameMapper`,对field name执行了`strings.ToLower`方法,并将toLower之后的fieldname和rows result相匹配。
|
||||
|
||||
sqlx除了上述默认的匹配方法外,还支持自定义的匹配。
|
||||
|
||||
#### sqlx.DB.MapperFunc
|
||||
最简单的自定义匹配的方式是使用`sqlx.DB.MapperFunc`,其接收一个`func(string) string`参数。使用示例如下:
|
||||
```go
|
||||
// if our db schema uses ALLCAPS columns, we can use normal fields
|
||||
db.MapperFunc(strings.ToUpper)
|
||||
|
||||
// suppose a library uses lowercase columns, we can create a copy
|
||||
copy := sqlx.NewDb(db.DB, db.DriverName())
|
||||
copy.MapperFunc(strings.ToLower)
|
||||
```
|
||||
### Slice Scan & Map Scan
|
||||
除了structScan之外,sqlx还支持slice scan和map scan的形式,示例如下:
|
||||
```go
|
||||
rows, err := db.Queryx("SELECT * FROM place")
|
||||
for rows.Next() {
|
||||
// cols is an []interface{} of all of the column results
|
||||
cols, err := rows.SliceScan()
|
||||
}
|
||||
|
||||
rows, err := db.Queryx("SELECT * FROM place")
|
||||
for rows.Next() {
|
||||
results := make(map[string]interface{})
|
||||
err = rows.MapScan(results)
|
||||
}
|
||||
```
|
||||
### Scanner/Valuer
|
||||
通过sql.Scanner/driver.Valuer接口,可以实现自定义类型的读取/写入。
|
||||
|
||||
## Connection Pool
|
||||
db对象将会管理一个连接池,有两种方式可以控制连接池的大小:
|
||||
- `DB.SetMaxIdleConns(n int)`
|
||||
- `DB.SetMaxOpenConns(n int)`
|
||||
|
||||
默认情况下,连接池会无限增长,在任何时刻如果连接池中没有空闲的连接都会创建新连接。可以通过`DB.SetMaxOpenConns(n int)`来控制连接池中的最大连接数目。
|
||||
|
||||
如果连接池中的连接没有在使用,那么会被标注为idle状态,并且在不再被需要时关闭。为了避免频繁的关闭和创建连接,可以使用`DB.SetMaxIdleConns`来设置最大的空闲连接数量,从而适配业务场景的查询负载。
|
||||
|
||||
为了避免长期持有连接,需要确保如下条目:
|
||||
- 确保针对每个Row Object执行了Scan操作
|
||||
- 确保对于Rows object,要么调用了Close方法,要么调用Next`进行了完全迭代`
|
||||
- 确保每个事务都调用了commit或rollback
|
||||
|
||||
如果上述描述中的某一项没有被保证,那么连接可能被长期持有直到发生垃圾回收,为了弥补被持有的连接,需要创建更多连接。
|
||||
|
||||
> `Rows.Close`方法可以被安全的多次调用,故而当rows object不再被需要时,应当调用该方法,无需考虑其是否已经被调用过。
|
||||
|
||||
313
Golang/gorm.md
Normal file
313
Golang/gorm.md
Normal file
@@ -0,0 +1,313 @@
|
||||
- [GORM](#gorm)
|
||||
- [QuickStart](#quickstart)
|
||||
- [Declaring Models](#declaring-models)
|
||||
- [约定](#约定)
|
||||
- [gorm.Model](#gormmodel)
|
||||
- [Advanced](#advanced)
|
||||
- [Field Level Permission](#field-level-permission)
|
||||
- [CreatedAt / UpdatedAt](#createdat--updatedat)
|
||||
- [struct embedded](#struct-embedded)
|
||||
- [field tags](#field-tags)
|
||||
|
||||
|
||||
# GORM
|
||||
## QuickStart
|
||||
GORM为golang的orm框架,其使用示例如下:
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/driver/sqlite"
|
||||
)
|
||||
|
||||
type Product struct {
|
||||
gorm.Model
|
||||
Code string
|
||||
Price uint
|
||||
}
|
||||
|
||||
func main() {
|
||||
db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
|
||||
if err != nil {
|
||||
panic("failed to connect database")
|
||||
}
|
||||
|
||||
// Migrate the schema
|
||||
db.AutoMigrate(&Product{})
|
||||
|
||||
// Create
|
||||
db.Create(&Product{Code: "D42", Price: 100})
|
||||
|
||||
// Read
|
||||
var product Product
|
||||
db.First(&product, 1) // find product with integer primary key
|
||||
db.First(&product, "code = ?", "D42") // find product with code D42
|
||||
|
||||
// Update - update product's price to 200
|
||||
db.Model(&product).Update("Price", 200)
|
||||
// Update - update multiple fields
|
||||
db.Model(&product).Updates(Product{Price: 200, Code: "F42"}) // non-zero fields
|
||||
db.Model(&product).Updates(map[string]interface{}{"Price": 200, "Code": "F42"})
|
||||
|
||||
// Delete - delete product
|
||||
db.Delete(&product, 1)
|
||||
}
|
||||
```
|
||||
## Declaring Models
|
||||
gorm中,Model通过struct来进行定义,struct中的fields可以为如下类型:
|
||||
- 基础golang类型
|
||||
- 指针
|
||||
- type alias
|
||||
- 自定义类型
|
||||
- 自定义类型需要实现`database/sql`中的Scanner和Valuer接口
|
||||
|
||||
如下为一个Model定义的示例:
|
||||
```go
|
||||
type User struct {
|
||||
ID uint // Standard field for the primary key
|
||||
Name string // A regular string field
|
||||
Email *string // A pointer to a string, allowing for null values
|
||||
Age uint8 // An unsigned 8-bit integer
|
||||
Birthday *time.Time // A pointer to time.Time, can be null
|
||||
MemberNumber sql.NullString // Uses sql.NullString to handle nullable strings
|
||||
ActivatedAt sql.NullTime // Uses sql.NullTime for nullable time fields
|
||||
CreatedAt time.Time // Automatically managed by GORM for creation time
|
||||
UpdatedAt time.Time // Automatically managed by GORM for update time
|
||||
ignored string // fields that aren't exported are ignored
|
||||
}
|
||||
```
|
||||
在上述Model中
|
||||
- 基础类型的字段可以直接访问,例如`uint, uint8, string`等
|
||||
- 指针类型,例如`*time.Time, *string`等,代表该字段值可为空
|
||||
- `sql/database`中的`sql.NullString`和`sql.NullTime`类型,也代表该字段可为空
|
||||
- `CreatedAt`字段和`UpdatedAt`字段是由Gorm管理的字段,当Record被创建或更新时,该字段的值会自动被注入为当前时间
|
||||
- `Non-exported fields`(首字母不是大写)并不会被映射
|
||||
|
||||
### 约定
|
||||
- `Primary Key`: 对于每个Model,gorm中将name为`ID`的field看作默认primary key
|
||||
- `Table names`: 默认情况下,gorm将struct name转换为`snake_case`,并将`snake_case`的`复数化`作为表名。例如,`User`struct对应的表名为`users`,`GormUserName`对应的表名为`gorm_user_names`。
|
||||
- `Column Names`: gorm会自动将field name转化为`snake_case`,用于表中字段名的映射
|
||||
- `Timestamp Field`: gorm默认使用`CreatedAt`和`UpdatedAt`字段来跟踪数据的创建和更新日期。
|
||||
|
||||
上述是gorm管理实体时的默认约定,但是除了默认约定外,gorm也支持对实体进行自定义配置。
|
||||
|
||||
### gorm.Model
|
||||
gorm提供了一个预先定义的实体`gorm.Model`,其中包含了常用字段:
|
||||
```go
|
||||
// gorm.Model definition
|
||||
type Model struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
DeletedAt gorm.DeletedAt `gorm:"index"`
|
||||
}
|
||||
```
|
||||
在自定义实体时,可以将Model实体作为一个field包含在自定义实体中,那么Model中定义的实体也会自动包含在自定义实体中。
|
||||
|
||||
上述预定义字段含义如下:
|
||||
- `ID`:每行数据的唯一标识符
|
||||
- `CreatedAt`:行创建时间
|
||||
- `UpdatedAt`:行最后更新时间
|
||||
- `DeletedAt`:用于逻辑删除
|
||||
|
||||
### Advanced
|
||||
#### Field Level Permission
|
||||
对于Exported Fields(以大写开头的Field),在执行CRUD时GORM默认用友所有权限。但是,gorm允许通过`tag`来修改field level permission,故而可以将field指定为`read-only, write-only, create-only, update-only, ignored`。
|
||||
|
||||
> 当字段被标记为`ignored`时,若使用gorm migrator创建表时,`ignored fields不会被创建`。
|
||||
|
||||
|
||||
```go
|
||||
type User struct {
|
||||
Name string `gorm:"<-:create"` // allow read and create
|
||||
Name string `gorm:"<-:update"` // allow read and update
|
||||
Name string `gorm:"<-"` // allow read and write (create and update)
|
||||
Name string `gorm:"<-:false"` // allow read, disable write permission
|
||||
Name string `gorm:"->"` // readonly (disable write permission unless it configured)
|
||||
Name string `gorm:"->;<-:create"` // allow read and create
|
||||
Name string `gorm:"->:false;<-:create"` // createonly (disabled read from db)
|
||||
Name string `gorm:"-"` // ignore this field when write and read with struct
|
||||
Name string `gorm:"-:all"` // ignore this field when write, read and migrate with struct
|
||||
Name string `gorm:"-:migration"` // ignore this field when migrate with struct
|
||||
}
|
||||
```
|
||||
#### CreatedAt / UpdatedAt
|
||||
gorm默认使用CreatedAt/UpdatedAt来跟踪record创建和更新时间,如果这些字段被定义,将会在创建/更新操作时设置这些字段。
|
||||
|
||||
如果想要为Create/Update字段指定不同的名称,可以为自定义字段指定`autoCreateTime`和`autoUpdateTime` tag。
|
||||
|
||||
如果想要为时间戳指定不同精度,可以按照如下格式进行定义:
|
||||
```go
|
||||
type User struct {
|
||||
CreatedAt time.Time // Set to current time if it is zero on creating
|
||||
UpdatedAt int // Set to current unix seconds on updating or if it is zero on creating
|
||||
Updated int64 `gorm:"autoUpdateTime:nano"` // Use unix nano seconds as updating time
|
||||
Updated int64 `gorm:"autoUpdateTime:milli"`// Use unix milli seconds as updating time
|
||||
Created int64 `gorm:"autoCreateTime"` // Use unix seconds as creating time
|
||||
}
|
||||
```
|
||||
#### struct embedded
|
||||
在gorm中,如果想要嵌套类,可以使用如下方式:
|
||||
- 将匿名struct作为field:
|
||||
```golang
|
||||
type Author struct {
|
||||
Name string
|
||||
Email string
|
||||
}
|
||||
|
||||
type Blog struct {
|
||||
Author
|
||||
ID int
|
||||
Upvotes int32
|
||||
}
|
||||
// equals
|
||||
type Blog struct {
|
||||
ID int64
|
||||
Name string
|
||||
Email string
|
||||
Upvotes int32
|
||||
}
|
||||
```
|
||||
- 将非匿名struct用`gorm:"embedded"`进行标记:
|
||||
```golang
|
||||
type Author struct {
|
||||
Name string
|
||||
Email string
|
||||
}
|
||||
|
||||
type Blog struct {
|
||||
ID int
|
||||
Author Author `gorm:"embedded"`
|
||||
Upvotes int32
|
||||
}
|
||||
// equals
|
||||
type Blog struct {
|
||||
ID int64
|
||||
Name string
|
||||
Email string
|
||||
Upvotes int32
|
||||
}
|
||||
```
|
||||
- 除了指定`gorm:"embeded"`tag外,还可以为嵌套struct指定prefix:
|
||||
```golang
|
||||
type Blog struct {
|
||||
ID int
|
||||
Author Author `gorm:"embedded;embeddedPrefix:author_"`
|
||||
Upvotes int32
|
||||
}
|
||||
// equals
|
||||
type Blog struct {
|
||||
ID int64
|
||||
AuthorName string
|
||||
AuthorEmail string
|
||||
Upvotes int32
|
||||
}
|
||||
```
|
||||
#### field tags
|
||||
当定义model时,tag是可选的,gorm支持tag如下所示。tag是大小写不敏感的,更推荐使用驼峰的写法。
|
||||
|
||||
如果需要指定多个tags,可以将多个tags根据`;`进行分隔,gorm参数值的转义符为`\`字符。
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Tag Name</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><tr>
|
||||
<td>column</td>
|
||||
<td>column db name</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>type</td>
|
||||
<td>column data type, prefer to use compatible general type, e.g: bool, int, uint, float, string, time, bytes, which works for all databases, and can be used with other tags together, like <code>not null</code>, <code>size</code>, <code>autoIncrement</code>… specified database data type like <code>varbinary(8)</code> also supported, when using specified database data type, it needs to be a full database data type, for example: <code>MEDIUMINT UNSIGNED NOT NULL AUTO_INCREMENT</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>serializer</td>
|
||||
<td>specifies serializer for how to serialize and deserialize data into db, e.g: <code>serializer:json/gob/unixtime</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>size</td>
|
||||
<td>specifies column data size/length, e.g: <code>size:256</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>primaryKey</td>
|
||||
<td>specifies column as primary key</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>unique</td>
|
||||
<td>specifies column as unique</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>default</td>
|
||||
<td>specifies column default value</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>precision</td>
|
||||
<td>specifies column precision</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>scale</td>
|
||||
<td>specifies column scale</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>not null</td>
|
||||
<td>specifies column as NOT NULL</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>autoIncrement</td>
|
||||
<td>specifies column auto incrementable</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>autoIncrementIncrement</td>
|
||||
<td>auto increment step, controls the interval between successive column values</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>embedded</td>
|
||||
<td>embed the field</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>embeddedPrefix</td>
|
||||
<td>column name prefix for embedded fields</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>autoCreateTime</td>
|
||||
<td>track current time when creating, for <code>int</code> fields, it will track unix seconds, use value <code>nano</code>/<code>milli</code> to track unix nano/milli seconds, e.g: <code>autoCreateTime:nano</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>autoUpdateTime</td>
|
||||
<td>track current time when creating/updating, for <code>int</code> fields, it will track unix seconds, use value <code>nano</code>/<code>milli</code> to track unix nano/milli seconds, e.g: <code>autoUpdateTime:milli</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>index</td>
|
||||
<td>create index with options, use same name for multiple fields creates composite indexes, refer <a href="indexes.html">Indexes</a> for details</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>uniqueIndex</td>
|
||||
<td>same as <code>index</code>, but create uniqued index</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>check</td>
|
||||
<td>creates check constraint, eg: <code>check:age > 13</code>, refer <a href="constraints.html">Constraints</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><-</td>
|
||||
<td>set field’s write permission, <code><-:create</code> create-only field, <code><-:update</code> update-only field, <code><-:false</code> no write permission, <code><-</code> create and update permission</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>-></td>
|
||||
<td>set field’s read permission, <code>->:false</code> no read permission</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>-</td>
|
||||
<td>ignore this field, <code>-</code> no read/write permission, <code>-:migration</code> no migrate permission, <code>-:all</code> no read/write/migrate permission</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>comment</td>
|
||||
<td>add comment for field when migration</td>
|
||||
</tr>
|
||||
</tbody></table>
|
||||
|
||||
|
||||
204
css/css.md
204
css/css.md
@@ -1,102 +1,102 @@
|
||||
# CSS
|
||||
- ## css选择器
|
||||
- 根据标签名选择
|
||||
```css
|
||||
<!--
|
||||
多个标签类型之间可以用逗号隔开
|
||||
-->
|
||||
h1,p,li {
|
||||
color:red;
|
||||
}
|
||||
```
|
||||
- 根据class进行选择
|
||||
- 通过class属性来进行选择
|
||||
```css
|
||||
.disk-block {
|
||||
border:1px black dashed;
|
||||
}
|
||||
```
|
||||
- 通过标签名和class同时来进行选择
|
||||
```css
|
||||
li.pink-css,div.pink-css {
|
||||
color:pink;
|
||||
}
|
||||
```
|
||||
- 后代选择器
|
||||
- 后代选择其会根据标签的位置来进行选择,多个标签之间通过空格隔开
|
||||
```css
|
||||
li em {
|
||||
font-style: italic;
|
||||
}
|
||||
li span.pink-style {
|
||||
color:pink;
|
||||
}
|
||||
```
|
||||
- 相邻兄弟选择器
|
||||
- 相邻兄弟选择器会选择其相邻的下一个兄弟节点
|
||||
```css
|
||||
li + li {
|
||||
color:red;
|
||||
}
|
||||
```
|
||||
- 子选择器
|
||||
- 相比与后代选择器,子选择器只会选择其节点的直接后代,间接的后代不会被选中
|
||||
```css
|
||||
li > em {
|
||||
color:pink;
|
||||
}
|
||||
```
|
||||
- ## css使用方法
|
||||
- 引用外部css文件
|
||||
```html
|
||||
<head>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
```
|
||||
- 内部样式
|
||||
```html
|
||||
<head>
|
||||
<style>
|
||||
p {
|
||||
color: red;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
```
|
||||
- 内联样式
|
||||
```html
|
||||
<p style="font-size:1.5em;color:red;">Hello</p>
|
||||
```
|
||||
- ## 多条样式规则应用于同一个元素
|
||||
- 多个样式规则的选择器范围相同
|
||||
- 在选择其范围相同的情况下,后出现的选择器样式会覆盖先前出现的选择器样式
|
||||
```css
|
||||
p {
|
||||
color:red;
|
||||
}
|
||||
p {
|
||||
color:blue;
|
||||
}
|
||||
/* 后出现的p样式会覆盖之前出现的p样式,最终颜色为blue */
|
||||
```
|
||||
- 多个样式规则的选择器范围不同
|
||||
- 如果多个样式的选择器范围不同,那么范围更小更特殊的选择器样式胜出
|
||||
```css
|
||||
.red-p {
|
||||
color:red;
|
||||
}
|
||||
p {
|
||||
color:blue
|
||||
}
|
||||
/* 此时类选择器比元素选择器更特殊,故而最终颜色为red */
|
||||
```
|
||||
- 在同一个选择器中重复指定样式
|
||||
- 在同一选择器中重复指定样式,位于后面的样式会覆盖位于前面的样式
|
||||
```css
|
||||
p {
|
||||
color:blue;
|
||||
color:red;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
# CSS
|
||||
- ## css选择器
|
||||
- 根据标签名选择
|
||||
```css
|
||||
<!--
|
||||
多个标签类型之间可以用逗号隔开
|
||||
-->
|
||||
h1,p,li {
|
||||
color:red;
|
||||
}
|
||||
```
|
||||
- 根据class进行选择
|
||||
- 通过class属性来进行选择
|
||||
```css
|
||||
.disk-block {
|
||||
border:1px black dashed;
|
||||
}
|
||||
```
|
||||
- 通过标签名和class同时来进行选择
|
||||
```css
|
||||
li.pink-css,div.pink-css {
|
||||
color:pink;
|
||||
}
|
||||
```
|
||||
- 后代选择器
|
||||
- 后代选择其会根据标签的位置来进行选择,多个标签之间通过空格隔开
|
||||
```css
|
||||
li em {
|
||||
font-style: italic;
|
||||
}
|
||||
li span.pink-style {
|
||||
color:pink;
|
||||
}
|
||||
```
|
||||
- 相邻兄弟选择器
|
||||
- 相邻兄弟选择器会选择其相邻的下一个兄弟节点
|
||||
```css
|
||||
li + li {
|
||||
color:red;
|
||||
}
|
||||
```
|
||||
- 子选择器
|
||||
- 相比与后代选择器,子选择器只会选择其节点的直接后代,间接的后代不会被选中
|
||||
```css
|
||||
li > em {
|
||||
color:pink;
|
||||
}
|
||||
```
|
||||
- ## css使用方法
|
||||
- 引用外部css文件
|
||||
```html
|
||||
<head>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
```
|
||||
- 内部样式
|
||||
```html
|
||||
<head>
|
||||
<style>
|
||||
p {
|
||||
color: red;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
```
|
||||
- 内联样式
|
||||
```html
|
||||
<p style="font-size:1.5em;color:red;">Hello</p>
|
||||
```
|
||||
- ## 多条样式规则应用于同一个元素
|
||||
- 多个样式规则的选择器范围相同
|
||||
- 在选择其范围相同的情况下,后出现的选择器样式会覆盖先前出现的选择器样式
|
||||
```css
|
||||
p {
|
||||
color:red;
|
||||
}
|
||||
p {
|
||||
color:blue;
|
||||
}
|
||||
/* 后出现的p样式会覆盖之前出现的p样式,最终颜色为blue */
|
||||
```
|
||||
- 多个样式规则的选择器范围不同
|
||||
- 如果多个样式的选择器范围不同,那么范围更小更特殊的选择器样式胜出
|
||||
```css
|
||||
.red-p {
|
||||
color:red;
|
||||
}
|
||||
p {
|
||||
color:blue
|
||||
}
|
||||
/* 此时类选择器比元素选择器更特殊,故而最终颜色为red */
|
||||
```
|
||||
- 在同一个选择器中重复指定样式
|
||||
- 在同一选择器中重复指定样式,位于后面的样式会覆盖位于前面的样式
|
||||
```css
|
||||
p {
|
||||
color:blue;
|
||||
color:red;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
530
docker/docker file.md
Normal file
530
docker/docker file.md
Normal file
@@ -0,0 +1,530 @@
|
||||
- [Dockerfile](#dockerfile)
|
||||
- [format](#format)
|
||||
- [parser directive](#parser-directive)
|
||||
- [syntax](#syntax)
|
||||
- [escape](#escape)
|
||||
- [ENV](#env)
|
||||
- [.dockerignore file](#dockerignore-file)
|
||||
- [FROM](#from)
|
||||
- [ARG和FROM的交互](#arg和from的交互)
|
||||
- [RUN](#run)
|
||||
- [RUN --mount](#run---mount)
|
||||
- [Mount Types](#mount-types)
|
||||
- [RUN --mount=type=bind](#run---mounttypebind)
|
||||
- [RUN --network](#run---network)
|
||||
- [CMD](#cmd)
|
||||
- [LABEL](#label)
|
||||
- [EXPOSE](#expose)
|
||||
- [ENV](#env-1)
|
||||
- [ADD](#add)
|
||||
- [ADD --chown](#add---chown)
|
||||
- [ADD \<git ref\> \<dir\>](#add-git-ref-dir)
|
||||
- [COPY](#copy)
|
||||
- [COPY --from](#copy---from)
|
||||
- [ENTRYPOINT](#entrypoint)
|
||||
- [ENTRYPOINT和CMD指令的交互](#entrypoint和cmd指令的交互)
|
||||
- [VOLUME](#volume)
|
||||
- [VOLUME指令要点](#volume指令要点)
|
||||
- [USER](#user)
|
||||
- [WORKDIR](#workdir)
|
||||
- [ARG](#arg)
|
||||
- [default value](#default-value)
|
||||
- [Scope](#scope)
|
||||
- [ENV和ARG使用](#env和arg使用)
|
||||
- [预定义的ARG](#预定义的arg)
|
||||
- [ONBUILD](#onbuild)
|
||||
|
||||
|
||||
|
||||
# Dockerfile
|
||||
Docker可以通过读取dockerfile中的指令自动构建image。
|
||||
## format
|
||||
如下是dockerfile的格式:
|
||||
```dockerfile
|
||||
# Comment
|
||||
INSTRUCTION arguments
|
||||
```
|
||||
指令是不区分大小写的,但是习惯将其大写以区分INSTRUCTION和arguments。
|
||||
Dockerfile中的指令将会被按顺序执行,dockerfile必须以FROM指令开头。FROM指令制定了构建的Parent image。
|
||||
## parser directive
|
||||
parser指令是可选的,并影响dockerfile后续的处理方式。parser指令并不会向build过程中添加layer,也不会展示为一个build过程。parser指令的格式如下所示:
|
||||
```dockerfile
|
||||
# directive=value
|
||||
```
|
||||
一旦任何comment、空行或builder instruction被处理,docker将不再接受parser directive,即使后面有格式符合parser instruction的行也会被当作注释处理。故而parser instruction必须位于dockerfile的最顶端。
|
||||
parser directive是大小写不敏感的,但是推荐将其小写。约定也要求在任何parser directive后添加一个空白行。
|
||||
**如下格式是无效的:**
|
||||
```dockerfile
|
||||
# direc \
|
||||
tive=value
|
||||
```
|
||||
**如下格式中parser指令连续出现了两次,也是无效的:**
|
||||
```dockerfile
|
||||
# directive=value1
|
||||
# directive=value2
|
||||
|
||||
FROM ImageName
|
||||
```
|
||||
**由于之前已经有一条指令,如下parser指令将会被当作注释处理:**
|
||||
```dockerfile
|
||||
FROM ImageName
|
||||
# directive=value
|
||||
```
|
||||
**由于之前存在一条注释,故而parser指令也会被当作注释处理:**
|
||||
```dockerfile
|
||||
# About my dockerfile
|
||||
# directive=value
|
||||
FROM ImageName
|
||||
```
|
||||
**不被识别的parser指令也会被当作注释处理,由于上一条parser指令不被识别被当作注释,故而第二条能被识别的parser指令也会被当作注释处理:**
|
||||
```dockerfile
|
||||
# unknowndirective=value
|
||||
# knowndirective=value
|
||||
```
|
||||
**非换行空白符允许在parser指令中出现,故而下列格式的parser instruction都是允许的:**
|
||||
```dockerfile
|
||||
#directive=value
|
||||
# directive =value
|
||||
# directive= value
|
||||
# directive = value
|
||||
# dIrEcTiVe=value
|
||||
```
|
||||
dockerfile支持如下两条parser directive:
|
||||
- syntax
|
||||
- escape
|
||||
### syntax
|
||||
该特性仅当使用了BuildKit backend时可用,当使用classic builder backend时会被忽略。
|
||||
### escape
|
||||
```dockerfile
|
||||
# escape=\ (backslash)
|
||||
```
|
||||
or
|
||||
```dockerfile
|
||||
# escape=` (backtick)
|
||||
```
|
||||
escape指令用于设置dockerfile中的转义符。如果没有显式设置的情况下,默认情况下转义符为\\
|
||||
转义符在dockerfile中既用于对行内的字符进行转义,也用于对换行符进行转义,\后跟一个换行符可以将一行指令拆分为多行进行编写。
|
||||
## ENV
|
||||
环境变量(dockerfile中以ENV开始的变量)可以在其他指令中作为变量被使用,由dockerfile进行解析。
|
||||
环境变量在dockerfile中通过$variable_name或${variable_name}的形式进行使用。
|
||||
${variable_name}支持一些标准的bash使用:
|
||||
- ${variable:-word}:如果variable被设置,那么返回值为variable设置的值;如果variable没有被设置,那么返回值为word
|
||||
- \${variable:+word}:如果variable被设置,那么返回word,否则返回空字符串
|
||||
可以通过\符号对$进行转义
|
||||
```dockerfile
|
||||
FROM busybox
|
||||
ENV FOO=/bar
|
||||
WORKDIR ${FOO} # WORKDIR /bar
|
||||
ADD . $FOO # ADD . /bar
|
||||
COPY \$FOO /quux # COPY $FOO /quux
|
||||
```
|
||||
在dockerfile中,如下命令都支持使用环境变量:
|
||||
- ADD
|
||||
- COPY
|
||||
- ENV
|
||||
- EXPOSE
|
||||
- FROM
|
||||
- LABEL
|
||||
- STOPSIGNAL
|
||||
- USER
|
||||
- VOLUME
|
||||
- WORKDIR
|
||||
- ONBUILD (when combined with one of the supported instructions above)
|
||||
在ENV中使用环境变量时,环境变量的值使用的是上一条ENV指令结束时的值:
|
||||
```dockerfile
|
||||
ENV abc=hello
|
||||
ENV abc=bye def=$abc
|
||||
ENV ghi=$abc
|
||||
```
|
||||
其中def的值为hello,而ghi的值为bye。
|
||||
## .dockerignore file
|
||||
当docker cli将context发送给docker daemon之前,其会先在context根目录查找文件名为.dockerignore的文件。如果该.dockerignore文件存在,docker cli将会删除context中符合pattern的文件。者可以避免务必要的发送大文件或敏感文件。
|
||||
docker cli将.dockerignore解释为用换行符分隔的一系列模式,**context的根目录既被当作工作目录,也被当作根目录**。
|
||||
> 在.dockerignore中,#开头的行将被看作注释。
|
||||
|
||||
.dockerignore的语法如下:
|
||||
| rule | behave |
|
||||
| :-: | :-: |
|
||||
| # comment | 注释,忽略 |
|
||||
| \*/temp\* | 排除了根目录的直接子目录中任何以temp开头的文件和目录,例如/somedir/temporary.txt或/somedir/temp |
|
||||
| \*/\*/temp\* | 排除深度为2的子目录中任何以temp开头的文件或者目录,例如/somedir/subdir/temporary.txt is excluded |
|
||||
| temp? | 排除根目录的子目录中名称为temp+任意字符的文件和目录 |
|
||||
| \*\*/\*.go | **用于匹配任意层(包含0)的路径,此pattern会排除任何以.go结尾的文件 |
|
||||
|
||||
当pattern以!开头时,代表不排除满足pattern的文件
|
||||
```.dockerignore
|
||||
*.md
|
||||
!README.md
|
||||
```
|
||||
上述代表排除所有.md文件,但是不排除README.md
|
||||
> 在.dockerignore文件中匹配的最后一条pattern将会决定该文件是否包含在context中
|
||||
```.dockerignore
|
||||
*.md
|
||||
!README*.md
|
||||
README-secret.md
|
||||
```
|
||||
上诉代表所有的.md文件都会被移除,但是满足README*.md格式的文件将会被保留,README-secret.md文件会被移除。
|
||||
> 如果想要在.dockerignore中指定哪些文件要被包含,而不是指定哪些文件被排除,可以使用如下形式
|
||||
```.dockerignore
|
||||
*
|
||||
!include-pattern...
|
||||
```
|
||||
## FROM
|
||||
FROM命令格式如下:
|
||||
```dockerfile
|
||||
FROM [--platform=<platform>] <image> [AS <name>]
|
||||
```
|
||||
or
|
||||
```dockerfile
|
||||
FROM [--platform=<platform>] <image>[:<tag>] [AS <name>]
|
||||
```
|
||||
or
|
||||
```dockerfile
|
||||
FROM [--platform=<platform>] <image>[@<digest>] [AS <name>]
|
||||
```
|
||||
FROM命令设置了一个初始build状态,并且为其他命令设置了一个初始的镜像。一个有效的dockerfile文件必须以FROM命令开始。
|
||||
- 在同一个dockerfile中,FROM命令可以出现多次,用于创建多个镜像或将其中一个build stage作为另一个的依赖。只需要在每条新的FROM语句之前记录上一个由commit输出的image id,每条新FROM语句都会清除之前语句创建的所有状态。
|
||||
- 可以为一个新的build stage指定一个名称,通过在FROM语句之后添加AS NAME,该名称可以在来连续的FROM语句或COPY --from=\<name\>中被使用。
|
||||
- tag或digest值也是可选的,如果忽略,那么会使用带lattest标签的值。
|
||||
如果FROM引用了一个多平台的镜像,那么--platform可以被使用,例如linux/amd64, linux/arm64, or windows/amd64.默认情况下,会使用build请求目标平台的值(--platform=$BUILDPLATFORM)。
|
||||
### ARG和FROM的交互
|
||||
FROM指令支持使用ARG定义的变量,例如:
|
||||
```dockerfile
|
||||
ARG CODE_VERSION=latest
|
||||
FROM base:${CODE_VERSION}
|
||||
CMD /code/run-app
|
||||
|
||||
FROM extras:${CODE_VERSION}
|
||||
CMD /code/run-extras
|
||||
```
|
||||
位于FROM指令之前的的ARG指令是不被包含在build stage中的,故而无法被FROM之后的指令使用。如果想要使用在FROM之前声明的ARG变量的值,可以在build stage之内使用一个没有值的ARG指令,如下所示:
|
||||
```dockerfile
|
||||
ARG VERSION=latest
|
||||
FROM busybox:$VERSION
|
||||
# 再次声明VERSION,以此使用FROM之前的VERSION值
|
||||
ARG VERSION
|
||||
RUN echo $VERSION > image_version
|
||||
```
|
||||
## RUN
|
||||
RUN命令具有两种格式:
|
||||
- RUN \<command\>:shell格式,该命令默认是跑在shell中的,默认情况下是/bin/sh -c
|
||||
- RUN ["executable", "param1", "param2"]
|
||||
RUN指令将会在新的layer中执行任何command并且将结果提交,提交结果镜像将会作为dockerfile中下一步操作的镜像。
|
||||
在shell格式中,可以使用转义符来将RUN命令拓展到多行:
|
||||
```dockerfile
|
||||
RUN /bin/bash -c 'source $HOME/.bashrc && \
|
||||
echo $HOME'
|
||||
# 其和如下形式命令等效
|
||||
RUN /bin/bash -c 'source $HOME/.bashrc && echo $HOME'
|
||||
```
|
||||
如果想要使用其他的shell,可以使用如下形式:
|
||||
```dockerfile
|
||||
RUN ["/bin/bash", "-c", "echo hello"]
|
||||
```
|
||||
RUN指令的缓存在下次build操作执行时并不会失效,在下次build操作时会被重用。RUN指令的缓存可以通过指定--no-cache选项被失效,使用示例如下:docker build --no-cache
|
||||
## RUN --mount
|
||||
RUN --amount允许创建一个文件系统挂载,build操作可以访问该文件系统。
|
||||
syntax:
|
||||
```dockerfile
|
||||
RUN --mount=[type=<TYPE>][,option=<value>[,option=<value>]...]
|
||||
```
|
||||
### Mount Types
|
||||
挂载类型如下:
|
||||
- bind(default):绑定装载上下文目录(只读)
|
||||
- cache:挂载一个临时目录到缓存目录,用于编译和包管理
|
||||
- secret:允许build container访问secure文件(例如私钥)而无需将私钥拷贝到image中
|
||||
- ssh:允许build container通过ssh agent访问ssh key
|
||||
### RUN --mount=type=bind
|
||||
该mount type允许将文件或者目录绑定到build container中,默认情况下一个bind-mount是只读的。
|
||||
| OPTION | DESCRIPTION |
|
||||
| :-: | :-: |
|
||||
| `target` | 挂载到的目录 |
|
||||
| `source` | `from`中的source path |
|
||||
| `from` | `source`根路径的build stage或image name,默认情况下为build context |
|
||||
| `rw`,`readwrite` | 允许在mount后的文件系统执行写入操作,写入信息将会被丢弃 |
|
||||
> RUN --mount=type=bind允许将context或镜像中的目录绑定到build container中,并且只有在该条RUN指令运行时,才可以访问挂载的目录。
|
||||
## RUN --network
|
||||
控制该命令运行在哪种网络环境下,支持如下网络环境:
|
||||
`syntax`:RUN --network=type
|
||||
- default:运行在默认网络环境下
|
||||
- none:运行在无网络访问的环境下
|
||||
- host:运行在宿主机的网络环境下
|
||||
## CMD
|
||||
CMD命令具有三种形式:
|
||||
- CMD ["executable","param1","param2"] (exec form)
|
||||
- CMD ["param1","param2"] (作为ENTRYPOINT的默认参数)
|
||||
- CMD command param1 param2 (shell form)
|
||||
|
||||
在dockerfile中,只有一条CMD指令能够生效,如果dockerfile中存在多条CMD指令,那么只有最后一条CMD指令能够生效。
|
||||
CMD指令的主要作用是为执行中的容器提供默认值。该默认值可以包含可执行文件,也可以省略可执行文件,将CMD指令的参数作为ENTRYPOINT指令的默认参数。
|
||||
> 如果CMD指令用于向ENTRYPOINT指令提供默认参数,那么ENTRYPOINT指令和CMD指令都要按照JSON数组的格式进行声明
|
||||
|
||||
当使用exec form或shell form时,CMD指令制定了镜像运行时默认执行的command。
|
||||
如果使用CMD的shell form,那么command将会在`/bin/sh -c`中执行
|
||||
```dockerfile
|
||||
FROM ubuntu
|
||||
CMD echo "This is a test." | wc -
|
||||
```
|
||||
如果想不在shell中运行CMD,那么必须将command作为JSON ARRAY传递,并且指定可执行文件的full path。
|
||||
```dockerfile
|
||||
FROM ubuntu
|
||||
CMD ["/usr/bin/wc","--help"]
|
||||
```
|
||||
如果用户在`docker run`命令中指定了参数,那么参数将会覆盖CMD命令提供的默认值。
|
||||
## LABEL
|
||||
LABEL命令的格式如下所示:
|
||||
```dockerfile
|
||||
LABEL <key>=<value> <key>=<value> <key>=<value> ...
|
||||
```
|
||||
LABEL命令向image中添加元数据,一个LABEL是一个key-value对,如果想要在LABEL value中包含空格,需要加入双引号或是转义符:
|
||||
```dockerfile
|
||||
LABEL "com.example.vendor"="ACME Incorporated"
|
||||
LABEL com.example.label-with-value="foo"
|
||||
LABEL version="1.0"
|
||||
LABEL description="This text illustrates \
|
||||
that label-values can span multiple lines."
|
||||
```
|
||||
可以在同一行LABEL命令中指定多个key-value pair。
|
||||
**在父镜像中定义的LABEL能被继承,如果父镜像和子镜像中都定义了相同的LABEL,那么最近的LABEL定义将会覆盖父镜像的同名LABEL。
|
||||
如果要查看一个镜像的LABEL,可以通过`docker image inspect`命令来进行查看,可以通过`--format`指定显示的格式:
|
||||
```shell
|
||||
# docker image inspect --format='' myimage
|
||||
```
|
||||
```json
|
||||
{
|
||||
"com.example.vendor": "ACME Incorporated",
|
||||
"com.example.label-with-value": "foo",
|
||||
"version": "1.0",
|
||||
"description": "This text illustrates that label-values can span multiple lines.",
|
||||
"multi.label1": "value1",
|
||||
"multi.label2": "value2",
|
||||
"other": "value3"
|
||||
}
|
||||
```
|
||||
## EXPOSE
|
||||
```dockerfile
|
||||
EXPOSE <port> [<port>/<protocol>...]
|
||||
```
|
||||
EXPOSE指令会告知Docker该container在监听指定的网络端口,可以指定该端口是在监听TCP或是UDP,默认情况下如果没有指定protocol,默认值为TCP。
|
||||
EXPOSE指令实际并不开放端口,其作用只是在build image的人和运行container的人之间提供文档,该文档会告知哪些端口需要被开放。
|
||||
> 想要真正的开放端口,需要在运行容器时通过docker run命令指定-p选项来开放和映射端口。
|
||||
默认情况下,EXPOSE在未指定协议的情况下使用TCP。可以显式指定开放端口的协议为UDP。
|
||||
```dockerfile
|
||||
EXPOSE 80/udp
|
||||
```
|
||||
可以通过多行EXPOSE指令同时暴露TCP和UDP端口:
|
||||
```dockerfile
|
||||
EXPOSE 80/tcp
|
||||
EXPOSE 80/udp
|
||||
```
|
||||
不管EXPOSE命令如何设置,都可以在`docker run`时通过-p选项来覆盖设置:
|
||||
```shell
|
||||
$ docker run -p 80:80/tcp -p 80:80/udp ...
|
||||
```
|
||||
## ENV
|
||||
ENV指令将环境变量key设置为value,该值可以为后续的指令提供inline替换。
|
||||
> 如果双引号没有被转义,那么value中的双引号将会被移除。
|
||||
|
||||
ENV指令允许一行内设置多条key-value pair。
|
||||
```dockerfile
|
||||
ENV MY_NAME="John Doe" MY_DOG=Rex\ The\ Dog \
|
||||
MY_CAT=fluffy
|
||||
```
|
||||
通过ENV命令设置的环境变量将会被保留,当container通过resulting image产生时。可以通过docker inspect来查看值,并且通过`docker run --env key=value`的形式来更改。
|
||||
> ENV命令设置的key-value对会保留在最终的镜像中,并且在镜像产生的容器中可见。
|
||||
|
||||
如果环境变量只是在build的过程中需要,并且最终image中不希望存在该环境变量,可以考虑只为单行命令设置value:
|
||||
```dockerfile
|
||||
RUN DEBIAN_FRONTEND=noninteractive apt-get update && apt-get install -y ...
|
||||
```
|
||||
或者可以考虑ARG,ARG不会持久化到最终的image中:
|
||||
```dockerfile
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
RUN apt-get update && apt-get install -y ...
|
||||
```
|
||||
## ADD
|
||||
ADD指令拥有两种形式:
|
||||
```dockerfile
|
||||
ADD [--chown=<user>:<group>] [--checksum=<checksum>] <src>... <dest>
|
||||
ADD [--chown=<user>:<group>] ["<src>",... "<dest>"]
|
||||
```
|
||||
如果路径中包含空格,需要使用第二种形式。
|
||||
ADD指令会从`src`拷贝新文件、目录或remote file URL并将其添加到镜像的文件系统中,path为`dest`。
|
||||
可以指定复数个`src`资源,但如果`src`为文件或者目录,那么`src`的path将会被解释为相对于build context的路径。
|
||||
每个`src`可以含有通配符,示例如下:
|
||||
```dockerfile
|
||||
ADD hom* /mydir/
|
||||
```
|
||||
`?`符号可以匹配任意单个字符,示例如下:
|
||||
```dockerfile
|
||||
ADD hom?.txt /mydir/
|
||||
```
|
||||
`dest`是一个绝对路径,或是相对于`WORKDIR`的相对路径,`src`将会被复制到目标容器中。
|
||||
如果想要ADD包含特殊字符的文件或目录(例如`[`或`]`),需要对这些路径进行转义。
|
||||
### ADD --chown
|
||||
所有新文件和目录创建时,UID和GID都是0,如果想要为新建文件或目录指定其他的值,可以使用`--chown`选项来指定`username:groupname`或`uid:gid.
|
||||
在仅仅指定了username或UID而没有指定groupname或GID的情况下,将会把GID设置为和UID相同的值。
|
||||
ADD指令的使用示例如下:
|
||||
```dockerfile
|
||||
ADD --chown=55:mygroup files* /somedir/
|
||||
ADD --chown=bin files* /somedir/
|
||||
ADD --chown=1 files* /somedir/
|
||||
ADD --chown=10:11 files* /somedir/
|
||||
```
|
||||
- 如果`src`是一个本地archive并且以一种可识别的格式压缩,那么其会被解压为一个目录,从remote获取的资源不会被解压。
|
||||
- 如果制定了多个`src`,则`dest`必须是目录,并且`dest`必须以`/`结尾。
|
||||
- 如果`dest`没有指定`/`作为结尾,那么`dest`被视作一个常规文件。
|
||||
- 如果`dest`不存在,那么其会自动创建路径中所有的缺失目录。
|
||||
|
||||
### ADD \<git ref\> \<dir\>
|
||||
该形式允许添加一个git仓库到镜像中,而不需要镜像中存在git命令。
|
||||
```dockerfile
|
||||
ADD [--keep-git-dir=<boolean>] <git ref> <dir>
|
||||
```
|
||||
`--keep-git-dir`选项代表是否保存git仓库中的.git目录,该选项的默认值是`false`.
|
||||
## COPY
|
||||
COPY指令可以按如下两种形式编写:
|
||||
```dockerfile
|
||||
COPY [--chown=<user>:<group>] <src>... <dest>
|
||||
COPY [--chown=<user>:<group>] ["<src>",... "<dest>"]
|
||||
```
|
||||
如果路径中含有空格,使用第二种形式。
|
||||
COPY命令从`src`中复制文件和目录并且将其添加到容器文件系统中,添加路径为`dest`.
|
||||
COPY的使用示例如下所示:
|
||||
```dockerfile
|
||||
COPY hom* /mydir/
|
||||
```
|
||||
类似`ADD --chown`,`COPY`命令也支持`COPY --chown`的用法.
|
||||
### COPY --from
|
||||
COPY支持`--from=<name>`选项,可以将`source`设置为先前的build stage而不是build context。**如果找不到具有相同名称的build stage,则会采用具有相同名称的image**。
|
||||
## ENTRYPOINT
|
||||
`ENTRYPOINT`命令具有两种格式:
|
||||
- ***exec***格式:
|
||||
```dockerfile
|
||||
ENTRYPOINT ["executable", "param1", "param2"]
|
||||
```
|
||||
- ***shell***格式:
|
||||
```dockerfile
|
||||
ENTRYPOINT command param1 param2
|
||||
```
|
||||
ENTRYPOINT允许像一个可执行程序一样配置容器。
|
||||
例如,如下示例使用默认内容启动了一个nginx实例,监听80端口:
|
||||
```dockerfile
|
||||
docker run -i -t --rm -p 80:80 nginx
|
||||
```
|
||||
`docker run <image>`命令之后的命令行参数将添加到***exec***形式ENTRYPOINT命令的所有元素之后,**并且命令行参数会覆盖CMD指令中指定的元素。**其允许参数被传递给entry point。
|
||||
例如,`docker run <image> -d`将会把`-d`传递给entry point。
|
||||
> 可以通过`docker run --entrypoint`命令来覆盖ENTRYPOINT指令。
|
||||
|
||||
`shell`格式的ENTRYPOINT将阻止任何`CMD`或`run command line`参数被使用,但是`shell`形式的ENTRYPOINT会作为`/bin/sh -c`的一个子命令启动。
|
||||
> 如果ENTRYPOINT作为`/bin/sh -c`的一个子命令启动,意味着目标可执行文件并不是容器的`PID 1`,也不会接收Unix信号-可以执行文件不会从docker stop命令中接收到SIGTERM信号。
|
||||
|
||||
只有dockerfile中的最后一个ENTRYPOINT命令会起作用。
|
||||
如果想要在使用ENTRYPOINT `shell form`时保证`PID 1`进程是目标可执行文件,可以在ENTRYPOINT指令前加上`exec`,示例如下所示:
|
||||
```dockerfile
|
||||
FROM ubuntu
|
||||
ENTRYPOINT exec top -b
|
||||
```
|
||||
如果`shell form`不带`exec`,该命令将作为`/bin/sh -c`的子命令执行,`PID 1`进程为`sh`而不是`top`,调用`docker stop`时容器SIGTERM信号将会被发送给`sh`,然后`top`进程会在超时之后接收到一个SIGKILL信号,并无法干净的退出。
|
||||
### ENTRYPOINT和CMD指令的交互
|
||||
`ENTRYPOINT`指令和`CMD`指令都用于指定容器启动时的命令,它们遵循如下规则:
|
||||
- dockerfile至少应该指定一条ENTRYPOINT或CMD指令
|
||||
- 当想要将容器像可执行文件一样运行(可以在`docker run`命令后添加传递给`ENTRYPOINT`指令的参数时),应该使用`ENTRYPOINT`指令
|
||||
- 当为`docker run`命令指定了额外参数时,`CMD`指令中为`ENTRYPOINT`指令指定的默认参数将会被命令行参数覆盖
|
||||
## VOLUME
|
||||
```dockerfile
|
||||
VOLUME ["/data"]
|
||||
```
|
||||
`VOLUMN`指令创建了一个具有指定名称的挂载点,并且将其标记为持有外部挂载volume。VOLUMNE指令的值可以通过json array的形式指定(`VOLUME ["/var/log/"]`),也可以通过纯字符串的形式来指定(`VOLUME /var/log`或`VOLUME /var/log /var/db`)。
|
||||
VOLUMN指令会使用基础image指定目录下的数据来初始化新创建的volume,示例如下:
|
||||
```dockerfile
|
||||
FROM ubuntu
|
||||
RUN mkdir /myvol
|
||||
RUN
|
||||
Learn more about the "RUN " Dockerfile command.
|
||||
echo "hello world" > /myvol/greeting
|
||||
VOLUME /myvol
|
||||
```
|
||||
在使用`docker run`指令执行上述dockerfile产生镜像时,会在/myvol创建一个挂载点,并且将greeting文件复制到新创建的volume中。
|
||||
### VOLUME指令要点
|
||||
- 如果在VOLUME指令创建volume之后,任何build step修改了volume路径下的数据,那些修改都会被丢弃
|
||||
## USER
|
||||
```dockerfile
|
||||
USER <user>[:<group>]
|
||||
# or
|
||||
USER <UID>[:<GID>]
|
||||
```
|
||||
USER指令用于指定当前stage剩余steps的默认user和group。指定用户用于RUN指令和ENTRYPOINT,CMD命令的执行。
|
||||
## WORKDIR
|
||||
```dockerfile
|
||||
WORKDIR /path/to/workdir
|
||||
```
|
||||
WORKDIR用于设置dockerfile中位于该命令之后步骤的工作目录。如果指定的工作目录不存在,那么会创建该工作目录。
|
||||
在dockerfile中,WORKDIR指令可以使用多次,如果提供了相对路径,相对路径会基于当前工作目录解析:
|
||||
```dockerfile
|
||||
WORKDIR /a
|
||||
WORKDIR b
|
||||
WORKDIR c
|
||||
RUN pwd
|
||||
```
|
||||
最终,pwd输出的路径是/a/b/c。
|
||||
WORKDIR指令可以解析环境变量,只可以解析显式在dockerfile中设置的环境变量:
|
||||
```dockerfile
|
||||
ENV DIRPATH=/path
|
||||
WORKDIR $DIRPATH/$DIRNAME
|
||||
RUN pwd
|
||||
```
|
||||
输出值为`/path/$DIRNAME`
|
||||
> 如果WORKDIR尚未被指定,那么WORKDIR的默认值是`/`。如果父镜像中设置了WORKDIR的值,那么dockerfile将使用父镜像的WORKDIR
|
||||
|
||||
为了避免在非预期的目录中执行操作,最好显式在dockerfile中设置WORKDIR.
|
||||
## ARG
|
||||
```dockerfile
|
||||
ARG <name>[=<default value>]
|
||||
```
|
||||
ARG指令可以定义一系列变量,定义的变量在build时可以通过`docker build --build-arg <var-name>=<var-value>`来设置值。
|
||||
如果在`--build-arg`中传递了dockfile中不存在的参数,会抛出警告,表示该变量没有被dockerfile消费。
|
||||
```dockerfile
|
||||
FROM busybox
|
||||
ARG user1
|
||||
ARG buildno
|
||||
# ...
|
||||
```
|
||||
### default value
|
||||
可以为ARG指定一个默认值,示例如下所示:
|
||||
```dockerfile
|
||||
FROM busybox
|
||||
ARG user1=someuser
|
||||
ARG buildno=1
|
||||
# ...
|
||||
```
|
||||
如果在build时没有传递值给ARG变量,那么会使用默认值。
|
||||
### Scope
|
||||
一个ARG指令的作用域范围从定义该ARG行开始算起,一直到当前build stage结束。
|
||||
### ENV和ARG使用
|
||||
当ENV和ARG指令同时被定义时,ENV定义的变量总是会覆盖ARG指令定义的变量。
|
||||
### 预定义的ARG
|
||||
dockerfile含有一组预定义的ARG变量,可以直接使用这些预定义的ARG变量而无需在dockerfile中添加ARG指令:
|
||||
- HTTP_PROXY
|
||||
- http_proxy
|
||||
- HTTPS_PROXY
|
||||
- https_proxy
|
||||
- FTP_PROXY
|
||||
- ftp_proxy
|
||||
- NO_PROXY
|
||||
- no_proxy
|
||||
- ALL_PROXY
|
||||
- all_proxy
|
||||
```shell
|
||||
docker build --build-arg HTTPS_PROXY=https://my-proxy.example.com .
|
||||
```
|
||||
## ONBUILD
|
||||
```dockerfile
|
||||
ONBUILD <INSTRUCTION>
|
||||
```
|
||||
ONBUILD指令添加了一个触发器,当该镜像作为其他镜像的基础镜像时,ONBUILD指定的指令将会被执行。ONBUILD触发器的指令将会在下游build操作的context中被执行,就像ONBUILD包含的指令被直接插入到下游FROM指令之后。
|
||||
使用示例如下:
|
||||
```dockerfile
|
||||
ONBUILD ADD . /app/src
|
||||
ONBUILD RUN /usr/local/bin/python-build --dir /app/src
|
||||
```
|
||||
235
docker/docker guide.md
Normal file
235
docker/docker guide.md
Normal file
@@ -0,0 +1,235 @@
|
||||
- [docker guide](#docker-guide)
|
||||
- [Push image to docker hub](#push-image-to-docker-hub)
|
||||
- [volume mount](#volume-mount)
|
||||
- [volume detail](#volume-detail)
|
||||
- [bind mount](#bind-mount)
|
||||
- [bind mount和volume mount区别](#bind-mount和volume-mount区别)
|
||||
- [docker run -v](#docker-run--v)
|
||||
- [multi container app](#multi-container-app)
|
||||
- [container networking](#container-networking)
|
||||
- [create network](#create-network)
|
||||
- [nicolaka/netshoot](#nicolakanetshoot)
|
||||
|
||||
|
||||
# docker guide
|
||||
## Push image to docker hub
|
||||
1. 通过命令`docker login -u ${username}`登录docker hub
|
||||
2. 使用`docker tag`命令给镜像`getting-started`一个新的名称。需要用docker hub id替换`YOUR-USER-NAME`。
|
||||
```shell
|
||||
$ docker tag getting-started YOUR-USER-NAME/getting-started
|
||||
```
|
||||
3. 如果`git push`操作未指定tagname,那么默认会使用tag `latest`
|
||||
```shell
|
||||
$ docker push YOUR-USER-NAME/getting-started
|
||||
```
|
||||
## volume mount
|
||||
在一个容器中对文件进行的操作(修改、删除、新增)在容器被删除后都会丢失,即使两个容器从同一镜像启动,一个容器的修改对另一个容器仍然不可见。
|
||||
`volume`提供了将容器中文件系统指定路径连接到宿主机的功能。在容器中被挂载的目录下,对文件的修改也会同步到宿主机的路径下。如果在不同的容器启动时挂载相同的目录,那么目录下的文件将会在容器之间进行共享。
|
||||
volume可以看作是一个不透明的数据桶,volume完全由docker管理,docker会管理volume在磁盘中存储的位置。用户只需要指定volume的name即可。
|
||||
1. 创建docker volume:
|
||||
```shell
|
||||
docker volume create todo-db
|
||||
```
|
||||
2. 通过`--mount`选项将volume挂载到容器中。使用示例如下所示:
|
||||
```shell
|
||||
<!--
|
||||
src指定volume的name
|
||||
target指定挂载路径
|
||||
-->
|
||||
# docker run -dp 3000:3000 --mount type=volume,src=todo-db,target=/etc/todos getting-started
|
||||
```
|
||||
### volume detail
|
||||
如果想要知道volume的详细信息,例如在宿主机中的存储位置,可以调用`docker volume inspect`命令:
|
||||
```shell
|
||||
$ docker volume inspect todo-db
|
||||
[
|
||||
{
|
||||
"CreatedAt": "2019-09-26T02:18:36Z",
|
||||
"Driver": "local",
|
||||
"Labels": {},
|
||||
"Mountpoint": "/var/lib/docker/volumes/todo-db/_data",
|
||||
"Name": "todo-db",
|
||||
"Options": {},
|
||||
"Scope": "local"
|
||||
}
|
||||
]
|
||||
```
|
||||
## bind mount
|
||||
相对于volume mount,bind mount能够让同一目录在宿主机和容器之间进行共享,宿主机对于路径下文件的修改能对容器可见。
|
||||
1. 执行如下命令创建bind mount,将宿主机路径挂载到容器中:
|
||||
```shell
|
||||
<!--
|
||||
当mount type为bind时,
|
||||
src为宿主机路径
|
||||
target为容器的目标路径
|
||||
-->
|
||||
docker run -it --mount type=bind,src="$(pwd)",target=/src ubuntu bash
|
||||
```
|
||||
### bind mount和volume mount区别
|
||||
对于bind mount,不管宿主机中是否由内容,宿主机路径对应的文件内容都会覆盖容器目录中的内容。
|
||||
对于volume mount,如果volume中没有内容,会将容器目录下的内容复制到宿主机的volume中,如果volume中有内容,volume中内容会覆盖容器目录中的内容。
|
||||
### docker run -v
|
||||
`docker run -v`命令后由三部分组成,形式为`src:dest:mode`,当src为宿主机的一个路径时,使用的是bind mount,如果src为一个name时,使用的是volume mount。第三块是可选的,为读写权限,默认情况下mode为rw。
|
||||
## multi container app
|
||||
通常情况下,每个容器之应该做一件事情。故而,例如一个应用需要mysql,应该将mysql置于另一个容器中进行部署。
|
||||
### container networking
|
||||
容器在默认情况下是隔离运行的,不知道位于同一机器上的其他容器和进程。故而,容器之间通过网络进行通信。
|
||||
如果多个容器需要通过network进行通信,那么需要将多个容器放置到同一个network中。
|
||||
### create network
|
||||
可以通过如下命令创建一个网络:
|
||||
```shell
|
||||
$ docker network create todo-app
|
||||
```
|
||||
如下命令会启动一个mysql容器并且将其添加到todo-app的网络中:
|
||||
```shell
|
||||
$ docker run -d \
|
||||
--network todo-app --network-alias mysql \
|
||||
-v todo-mysql-data:/var/lib/mysql \
|
||||
-e MYSQL_ROOT_PASSWORD=secret \
|
||||
-e MYSQL_DATABASE=todos \
|
||||
mysql:8.0
|
||||
```
|
||||
`--network todo-app`选项会将mysql容器添加到`todo-app`网络中,而`--network-alias mysql`则会创建一条dns `mysql`,该条dns指向mysql容器的ip地址。
|
||||
> 在上述指令中,并没有调用`docker volume create`命令来创建volume,docker知道我们要使用命名好的volume并会为我们自动创建。
|
||||
### nicolaka/netshoot
|
||||
nicolaka/netshoot容器中集成了很多网络工具,在解决网络问题时会很有用。
|
||||
1. 将netshoot容器添加到指定网络中:
|
||||
```shell
|
||||
$ docker run -it --network todo-app nicolaka/netshoot
|
||||
```
|
||||
2. 在netshoot容器中使用dig命令,dig是很有用的DNS工具:
|
||||
```shell
|
||||
$ dig mysql
|
||||
```
|
||||
dig命令输出如下:
|
||||
```shell
|
||||
; <<>> DiG 9.18.8 <<>> mysql
|
||||
;; global options: +cmd
|
||||
;; Got answer:
|
||||
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 32162
|
||||
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0
|
||||
|
||||
;; QUESTION SECTION:
|
||||
;mysql. IN A
|
||||
|
||||
;; ANSWER SECTION:
|
||||
mysql. 600 IN A 172.23.0.2
|
||||
|
||||
;; Query time: 0 msec
|
||||
;; SERVER: 127.0.0.11#53(127.0.0.11)
|
||||
;; WHEN: Tue Oct 01 23:47:24 UTC 2019
|
||||
;; MSG SIZE rcvd: 44
|
||||
```
|
||||
其中ANSWER SECTION输出了network-alias mysql对应的ip地址。
|
||||
> mysql并不是一个有效的域名,但是docker能将该network-alias解析为对应容器的ip地址
|
||||
## docker compose
|
||||
docker compose是用于定义和共享多容器应用的工具。通过compose,可以通过一个yaml文件来定义应用服务,并且通过单行命令启动和停止多容器应用服务。
|
||||
### 创建compose file
|
||||
1. 定义container的service条目和对应image,我们可以为service指定任何名称。该指定的名称会自动成为network-alias
|
||||
```yml
|
||||
services:
|
||||
app:
|
||||
image: node:18-alpine
|
||||
```
|
||||
2. 在为service定义name和image之后,可以为service指定command
|
||||
```yml
|
||||
services:
|
||||
app:
|
||||
image: node:18-alpine
|
||||
command: sh -c "yarn install && yarn run dev"
|
||||
```
|
||||
3. 为service指定ports:
|
||||
```yml
|
||||
services:
|
||||
app:
|
||||
image: node:18-alpine
|
||||
command: sh -c "yarn install && yarn run dev"
|
||||
ports:
|
||||
- 3000:3000
|
||||
```
|
||||
4. 为service设置workdir或挂载卷时,通过如下方式进行:
|
||||
```yml
|
||||
services:
|
||||
app:
|
||||
image: node:18-alpine
|
||||
command: sh -c "yarn install && yarn run dev"
|
||||
ports:
|
||||
- 3000:3000
|
||||
working_dir: /app
|
||||
volumes:
|
||||
- ./:/app
|
||||
```
|
||||
5. 如果需要为service指定环境变量,如果如下方式进行配置:
|
||||
```yml
|
||||
services:
|
||||
app:
|
||||
image: node:18-alpine
|
||||
command: sh -c "yarn install && yarn run dev"
|
||||
ports:
|
||||
- 3000:3000
|
||||
working_dir: /app
|
||||
volumes:
|
||||
- ./:/app
|
||||
environment:
|
||||
MYSQL_HOST: mysql
|
||||
MYSQL_USER: root
|
||||
MYSQL_PASSWORD: secret
|
||||
MYSQL_DB: todos
|
||||
```
|
||||
6. 在docker run时,通过-v选项指定的volume name不存在,那么volume会自动被创建,但是在compose file中,通过volumes指定的volume name并不会被自动创建。想要创建volume,必须yml文件的顶层`volumes:`中指定volume 。
|
||||
通常情况下,只用指定`volume-name:`即可,会使用默认的选项。
|
||||
示例如下所示:
|
||||
```yml
|
||||
services:
|
||||
app:
|
||||
# The app service definition
|
||||
mysql:
|
||||
image: mysql:8.0
|
||||
volumes:
|
||||
- todo-mysql-data:/var/lib/mysql
|
||||
|
||||
volumes:
|
||||
todo-mysql-data:
|
||||
```
|
||||
7. 最终,整合两个docker container的应用yml文件配置如下所示:
|
||||
```yml
|
||||
services:
|
||||
app:
|
||||
image: node:18-alpine
|
||||
command: sh -c "yarn install && yarn run dev"
|
||||
ports:
|
||||
- 3000:3000
|
||||
working_dir: /app
|
||||
volumes:
|
||||
- ./:/app
|
||||
environment:
|
||||
MYSQL_HOST: mysql
|
||||
MYSQL_USER: root
|
||||
MYSQL_PASSWORD: secret
|
||||
MYSQL_DB: todos
|
||||
|
||||
mysql:
|
||||
image: mysql:8.0
|
||||
volumes:
|
||||
- todo-mysql-data:/var/lib/mysql
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: secret
|
||||
MYSQL_DATABASE: todos
|
||||
|
||||
volumes:
|
||||
todo-mysql-data:
|
||||
```
|
||||
### run application stack
|
||||
可以通过`docker compose up`命令来启动application stack。可以通过添加-d选项在后台运行:
|
||||
```shell
|
||||
docker compose up -d
|
||||
```
|
||||
**默认情况下,docker compose会自动为compose文件中的application stack创建一个network。**
|
||||
如果想要停止application stack,可以调用`docker compose down`命令。
|
||||
> 默认情况下,docker compose down并不会移除创建的volume,如果想要移除volume,可以指定选项--volumes
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
2518
elastic search/01_elastic_search_introduce.md
Normal file
2518
elastic search/01_elastic_search_introduce.md
Normal file
File diff suppressed because it is too large
Load Diff
328
elastic search/02_elastic_search_index.md
Normal file
328
elastic search/02_elastic_search_index.md
Normal file
@@ -0,0 +1,328 @@
|
||||
- [index modules](#index-modules)
|
||||
- [index module introduce](#index-module-introduce)
|
||||
- [索引设置](#索引设置)
|
||||
- [static index settings](#static-index-settings)
|
||||
- [`index.number_of_shards`](#indexnumber_of_shards)
|
||||
- [Analysis](#analysis)
|
||||
- [index shard allocation](#index-shard-allocation)
|
||||
- [index blocks](#index-blocks)
|
||||
- [index block settings](#index-block-settings)
|
||||
- [`index.blocks.read_only`](#indexblocksread_only)
|
||||
- [`index.blocks.read_only_allow_delete`](#indexblocksread_only_allow_delete)
|
||||
- [`index.blocks.read`](#indexblocksread)
|
||||
- [`index.blocks.write`](#indexblockswrite)
|
||||
- [`index.blocks.metadata`](#indexblocksmetadata)
|
||||
- [增加index block示例](#增加index-block示例)
|
||||
- [path param](#path-param)
|
||||
- [query param](#query-param)
|
||||
- [示例](#示例)
|
||||
- [Similarity module](#similarity-module)
|
||||
- [配置similarity](#配置similarity)
|
||||
- [slow query](#slow-query)
|
||||
- [Search Slow log](#search-slow-log)
|
||||
- [identify search log origin](#identify-search-log-origin)
|
||||
- [index slow log](#index-slow-log)
|
||||
- [Store](#store)
|
||||
- [File System Storage types](#file-system-storage-types)
|
||||
- [preloading data into the file system cache](#preloading-data-into-the-file-system-cache)
|
||||
|
||||
|
||||
# index modules
|
||||
## index module introduce
|
||||
index module用于对索引进行创建,并且控制和索引相关的各个方面。
|
||||
|
||||
### 索引设置
|
||||
索引级别的设置可以针对每个索引来进行设置,设置分类如下:
|
||||
- static:static设置只能在如下时机应用于索引
|
||||
- 索引创建时
|
||||
- 索引处于closed状态
|
||||
- 使用`update-index-settings` api,并且带有`reopen=true`的查询参数,带有该参数时,会关闭受影响索引,更新后将受影响索引重新开启
|
||||
- dynamic:dynamic索引可以在索引处于活跃状态时,通过`udpate-index-settings` api进行修改
|
||||
|
||||
> #### closed index
|
||||
> 当一个索引处于`closed`状态时,所有针对该索引的read/write操作都会被阻塞,所有可以用于`opened`状态索引的操作,其执行对于`closed`状态的索引来说都不允许。
|
||||
>
|
||||
> 对于`closed`状态的索引,既无法向其中新增文档,也无法在索引中检索文档。
|
||||
>
|
||||
> 处于closed状态的索引能够通过`open index api`来重新开启。
|
||||
|
||||
### static index settings
|
||||
#### `index.number_of_shards`
|
||||
索引拥有的primary shards数量,默认为1,该配置项只能在索引创建时被设置。`即使索引处于closed状态,该配置项也无法修改。`
|
||||
|
||||
> `index.number_of_shards`最大为1024
|
||||
|
||||
## Analysis
|
||||
index analysis module充当了可配置的analyzer注册中心,analyzer可用于将`string`类型字段转换为独立的terms,这将用于:
|
||||
- 将文档string field的字段转化为terms,并且将terms添加到倒排索引中,令文档可以被搜索
|
||||
- analyzer被用于高级查询,例如`match`查询,将用户输入的查询字符串分割为search terms用于查询
|
||||
|
||||
## index shard allocation
|
||||
index shard allocation提供了可针对单个索引的设置,用于控制node中shard的分配:
|
||||
- shard allocating filtering:控制shard被分配给哪个node
|
||||
- delayed allocation:`节点离开导致未分配shard`的延迟分配
|
||||
- total shards per node:相同索引在同一node中,shards数量的上限
|
||||
- data tier allocation:控制对data tier分配的索引
|
||||
|
||||
## index blocks
|
||||
index blocks限制了针对特定索引的操作类型。操作阻塞的类型可以分为:
|
||||
- 读操作阻塞
|
||||
- 写操作阻塞
|
||||
- 元数据操作阻塞
|
||||
|
||||
针对索引操作的阻塞可以通过`dynamic index setting`来进行新增和移除。并且,阻塞也可以通过特定的api来进行添加和移除。
|
||||
|
||||
针对wrtie blocks设置的修改,一旦修改成功,那么所有的在途写操作都已经完成。
|
||||
|
||||
### index block settings
|
||||
#### `index.blocks.read_only`
|
||||
如果该配置项设置为true,该索引和索引的元数据都是只读的,当设置为`false`时,允许写操作和元数据变更。
|
||||
|
||||
#### `index.blocks.read_only_allow_delete`
|
||||
类似于`index.blocks.write`,但是可以对index进行删除。不要针对`index.blocks.read_only_allow_delete`进行手动设置或移除。`disk-based shard allocator`会根据磁盘剩余空间自动添加或移除该配置项。
|
||||
|
||||
从索引中删除文档释放资源(而不是删除索引本身)会暂时增加索引的大小,故而在node的磁盘空间不足时可能无法实现。当`index.blocks.read_only_allow_delete`被设置为true时,`并不允许删除索引中的文档`。但是,删除索引本身的操作只需要极少量的额外磁盘空间,并且几乎可以立即删除索引所占用的空间,故而删除索引本身的操作仍然被允许。
|
||||
|
||||
> elastic search在磁盘占用高于`flood stage watermark`时,会自动为索引增加` read-only-allow-delete`阻塞;当磁盘占用率跌倒`high watermark`之下时,则是会自动释放该阻塞
|
||||
|
||||
#### `index.blocks.read`
|
||||
如果设置为true,会阻塞针对index的读操作
|
||||
|
||||
#### `index.blocks.write`
|
||||
如果设置为true,会阻塞针对索引的写操作,和`index.blocks.read_only`不同,本设置项并不影响metadata。
|
||||
|
||||
例如,为索引设置write block后,仍然可以对metadata进行变更,但是设置了`index.blocks.read_only`后,无法对元数据进行变更
|
||||
|
||||
#### `index.blocks.metadata`
|
||||
如果设置为true,会禁止对元数据的读取和变更
|
||||
|
||||
### 增加index block示例
|
||||
```bash
|
||||
# PUT /<index>/_block/<block>
|
||||
PUT /my-index-000001/_block/write
|
||||
```
|
||||
#### path param
|
||||
- `<index>`: 由`,`分隔的列表或通配符表达式,代表该请求的索引名称
|
||||
- 默认情况下,`<index>`部分需要指定索引的精确名称。如果想要使用`_all, *`等通配表达式,需要将`action.destructive_requires_name`属性设置为`false`。
|
||||
- `<block>`: 向索引应用的阻塞类型
|
||||
- <block>部分可选的值为`metadata, read, read_only, write`
|
||||
|
||||
#### query param
|
||||
- `allow_no_indices`:
|
||||
- 如果该参数设置为false,那么当索引项中任一`wildcard expression, idnex alias或_all`值没有匹配的索引或只能匹配到closed状态的索引,那么该请求会返回异常。
|
||||
- 例如`foo*,bar*`,`foo*`表达式匹配到索引,但是`bar*`没有相匹配的索引,那么会抛出异常。
|
||||
- 该参数认值为`true`
|
||||
- `expand_wildcards`:
|
||||
- wildcard pattern能够匹配到的索引类型。如果请求能够匹配到data stream,那么该参数能够决定wildcard pattern能够匹配到hidden data stream
|
||||
- 该参数的值支持`,`分隔,有效的值如下:
|
||||
- `all`:匹配任何data stream或index,包括hidden的
|
||||
- `open`:匹配`open, non-hidden`状态的索引和`non-hidden`状态的data stream
|
||||
- `closed`:匹配`closed, non-hidden`状态的索引和`non-hidden`状态的data stream
|
||||
- `hidden`:匹配`hidden`状态的索引和`hidden`状态的data stream。`hidden`必须和`open, closed`中任一组合使用,也能和两者一起使用`open, closed, hidden`
|
||||
- `none`:不接受wildcard pattern
|
||||
- 该参数默认值为`open`
|
||||
- `ignore_unavailable`: 如果参数设置为false,若未匹配到索引或匹配到closed状态的索引,返回异常
|
||||
- 该参数默认值为`false`
|
||||
- `master_timeout`:等待master node的最大时间,默认为`30s`,如果超过该限制master node仍然不可访问,那么该请求会返回异常
|
||||
- `timeout`:在更新完metadata后,等待cluster中所有节点返回的时间限制,默认为`30s`。如果超时后仍未能接受到返回,那么针对cluster metadata的修改仍然会被应用,但是在返回中会指定并非接受到了所有的ack
|
||||
|
||||
#### 示例
|
||||
添加write block的示例如下所示:
|
||||
```
|
||||
PUT /my-index-000001/_block/write
|
||||
```
|
||||
返回结果如下:
|
||||
```
|
||||
{
|
||||
"acknowledged" : true,
|
||||
"shards_acknowledged" : true,
|
||||
"indices" : [ {
|
||||
"name" : "my-index-000001",
|
||||
"blocked" : true
|
||||
} ]
|
||||
}
|
||||
```
|
||||
## Similarity module
|
||||
similarity moudle(scoring/ranking model)定义了如何对匹配到的document进行打分。similaity是针对单个字段的,这意味着可以通过mapping为每个字段都定义不同的mapping。
|
||||
|
||||
similarity仅适用于text类型和keyword类型的字段。
|
||||
|
||||
### 配置similarity
|
||||
大多similarity都可以通过如下方式进行配置:
|
||||
```
|
||||
PUT /index
|
||||
{
|
||||
"settings": {
|
||||
"index": {
|
||||
"similarity": {
|
||||
"my_similarity": {
|
||||
"type": "DFR",
|
||||
"basic_model": "g",
|
||||
"after_effect": "l",
|
||||
"normalization": "h2",
|
||||
"normalization.h2.c": "3.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
上述示例中,配置了DFR similarity,故而,在mapping中,即可通过`my_similarity`来进行引用,示例如下所示:
|
||||
```
|
||||
PUT /index/_mapping
|
||||
{
|
||||
"properties" : {
|
||||
"title" : { "type" : "text", "similarity" : "my_similarity" }
|
||||
}
|
||||
}
|
||||
```
|
||||
## slow query
|
||||
### Search Slow log
|
||||
shard level slow search log允许将slow query记录到特定的日志文件中。
|
||||
|
||||
对于threshold,可以对query阶段和fetch阶段分别进行配置,示例如下所示:
|
||||
```
|
||||
index.search.slowlog.threshold.query.warn: 10s
|
||||
index.search.slowlog.threshold.query.info: 5s
|
||||
index.search.slowlog.threshold.query.debug: 2s
|
||||
index.search.slowlog.threshold.query.trace: 500ms
|
||||
|
||||
index.search.slowlog.threshold.fetch.warn: 1s
|
||||
index.search.slowlog.threshold.fetch.info: 800ms
|
||||
index.search.slowlog.threshold.fetch.debug: 500ms
|
||||
index.search.slowlog.threshold.fetch.trace: 200ms
|
||||
```
|
||||
上述所有的配置都是`dynamic`的,并且可以针对每个index单独进行设置,示例如下所示:
|
||||
```
|
||||
PUT /my-index-000001/_settings
|
||||
{
|
||||
"index.search.slowlog.threshold.query.warn": "10s",
|
||||
"index.search.slowlog.threshold.query.info": "5s",
|
||||
"index.search.slowlog.threshold.query.debug": "2s",
|
||||
"index.search.slowlog.threshold.query.trace": "500ms",
|
||||
"index.search.slowlog.threshold.fetch.warn": "1s",
|
||||
"index.search.slowlog.threshold.fetch.info": "800ms",
|
||||
"index.search.slowlog.threshold.fetch.debug": "500ms",
|
||||
"index.search.slowlog.threshold.fetch.trace": "200ms"
|
||||
}
|
||||
```
|
||||
|
||||
默认情况下,threshold为`-1`,代表threshold被停用。
|
||||
|
||||
该日志针对的是shard的范围。
|
||||
|
||||
search slow log file在`log4j2.properties`文件中进行配置。
|
||||
|
||||
### identify search log origin
|
||||
通过将`index.search.slowlog.include.user`配置项设置为true,可以在slow log中输出`触发该slow query的用户信息`,示例如下:
|
||||
```
|
||||
PUT /my-index-000001/_settings
|
||||
{
|
||||
"index.search.slowlog.include.user": true
|
||||
}
|
||||
```
|
||||
上述设置将导致用户信息将会被包含在slow log中:
|
||||
```json
|
||||
{
|
||||
"@timestamp": "2024-02-21T12:42:37.255Z",
|
||||
"log.level": "WARN",
|
||||
"auth.type": "REALM",
|
||||
"elasticsearch.slowlog.id": "tomcat-123",
|
||||
"elasticsearch.slowlog.message": "[index6][0]",
|
||||
"elasticsearch.slowlog.search_type": "QUERY_THEN_FETCH",
|
||||
"elasticsearch.slowlog.source": "{\"query\":{\"match_all\":{\"boost\":1.0}}}",
|
||||
"elasticsearch.slowlog.stats": "[]",
|
||||
"elasticsearch.slowlog.took": "747.3micros",
|
||||
"elasticsearch.slowlog.took_millis": 0,
|
||||
"elasticsearch.slowlog.total_hits": "1 hits",
|
||||
"elasticsearch.slowlog.total_shards": 1,
|
||||
"user.name": "elastic",
|
||||
"user.realm": "reserved",
|
||||
"ecs.version": "1.2.0",
|
||||
"service.name": "ES_ECS",
|
||||
"event.dataset": "elasticsearch.index_search_slowlog",
|
||||
"process.thread.name": "elasticsearch[runTask-0][search][T#5]",
|
||||
"log.logger": "index.search.slowlog.query",
|
||||
"elasticsearch.cluster.uuid": "Ui23kfF1SHKJwu_hI1iPPQ",
|
||||
"elasticsearch.node.id": "JK-jn-XpQ3OsDUsq5ZtfGg",
|
||||
"elasticsearch.node.name": "node-0",
|
||||
"elasticsearch.cluster.name": "distribution_run"
|
||||
}
|
||||
```
|
||||
### index slow log
|
||||
index slow log和search slow log类似,其log file名称以`_index_indexing_slowlog.json`结尾。index slow log的配置如下所示:
|
||||
```
|
||||
index.indexing.slowlog.threshold.index.warn: 10s
|
||||
index.indexing.slowlog.threshold.index.info: 5s
|
||||
index.indexing.slowlog.threshold.index.debug: 2s
|
||||
index.indexing.slowlog.threshold.index.trace: 500ms
|
||||
index.indexing.slowlog.source: 1000
|
||||
```
|
||||
index slow log的配置也是dynamic的,可以通过如下示例来进行配置:
|
||||
```
|
||||
PUT /my-index-000001/_settings
|
||||
{
|
||||
"index.indexing.slowlog.threshold.index.warn": "10s",
|
||||
"index.indexing.slowlog.threshold.index.info": "5s",
|
||||
"index.indexing.slowlog.threshold.index.debug": "2s",
|
||||
"index.indexing.slowlog.threshold.index.trace": "500ms",
|
||||
"index.indexing.slowlog.source": "1000"
|
||||
}
|
||||
```
|
||||
如果想要在日志中包含触发该slow index请求的用户,可以通过如下方式进行请求:
|
||||
```
|
||||
PUT /my-index-000001/_settings
|
||||
{
|
||||
"index.indexing.slowlog.include.user": true
|
||||
}
|
||||
```
|
||||
默认情况下,elasticsearch会打印slow log中头1000个字符。可以通过`index.indexing.slowlog.source`来修改该配置。
|
||||
- 如果将`indexing.slowlog.source`设置为false或0,将会跳过对`source`的输出
|
||||
- 如果将`indexing.slowlog.source`设置为true,将会输出所有`source`的内容
|
||||
|
||||
## Store
|
||||
Store module控制如何对磁盘上的index data进行存储和访问。
|
||||
### File System Storage types
|
||||
对于存储类型,存在许多不同的实现。默认情况下,elasticsearch会基于操作系统选择最佳实现。
|
||||
|
||||
可以对所有index都显式的设置存储类型,需要修改`config/elasticsearch.yml`:
|
||||
```
|
||||
index.store.type: hybridfs
|
||||
```
|
||||
`index.store.type`为static设置,在索引创建时,可以针对每个索引进行单独设置:
|
||||
```
|
||||
PUT /my-index-000001
|
||||
{
|
||||
"settings": {
|
||||
"index.store.type": "hybridfs"
|
||||
}
|
||||
}
|
||||
```
|
||||
如下列举了受支持的storage types:
|
||||
- `fs`:默认file system实现,该设置会基于操作系统选择最佳的文件系统类型,目前在所有支持`hybridfs`的系统中都会选择hybirdfs,但是后续可能会改变。
|
||||
- `simplefs`:`7.15`中已经被废弃
|
||||
- `niofs`:`NIO FS`使用nio将shard index存储在文件系统中。其允许多个线程同时对一个文件进行读取。该文件系统不推荐在windows下使用。
|
||||
- `mmapfs`:`MMAPFS`将shard index存储在文件系统中,并且会将文件映射到内存中。内存映射将会使用一部分虚拟内存空间地址,使用大小和文件大小相同,请确保有足够多的虚拟内存空间被分配。
|
||||
- `hybirdfs`:`hybirdfs`是`niofs`和`mmapfs`的混合体,对于每种读取访问类型都会选择最佳的文件系统。
|
||||
|
||||
### preloading data into the file system cache
|
||||
默认情况下,elasticsearch完全依赖操作系统文件系统的io操作缓存。可以通过设置`index.store.preload`来告知操作系统在打开索引时,将`hot index file`文件中的内容加载到内存中。
|
||||
|
||||
`index.store.preload`接受一个由`,`分隔的拓展名列表,所有后续名包含在列表中的文件都会被预加载到内存中。这将在操作系统重启、系统内存缓存丢失时极大改善性能。但是,这将会降低索引打开的速度,只有当`index.store.preload`中指定的内容加载到内存中时,索引才能够被访问。
|
||||
|
||||
该设置只会尽力而为,可能并不会起作用,取决于store type和操作系统。
|
||||
|
||||
`index.store.preload`是一个static设置,可以在`config/elasticsearch.yml`中设置:
|
||||
```
|
||||
index.store.preload: ["nvd", "dvd"]
|
||||
```
|
||||
在索引创建时,该配置项同样也可以设置:
|
||||
```
|
||||
PUT /my-index-000001
|
||||
{
|
||||
"settings": {
|
||||
"index.store.preload": ["nvd", "dvd"]
|
||||
}
|
||||
}
|
||||
```
|
||||
该属性的默认值为`[]`,代表不会预加载任何内容。对于主动被搜索的
|
||||
173
http/http3.md
Normal file
173
http/http3.md
Normal file
@@ -0,0 +1,173 @@
|
||||
- [HTTP3 \& QUIC Protocols](#http3--quic-protocols)
|
||||
- [What is HTTP3](#what-is-http3)
|
||||
- [TCP/IP模型中HTTP3 vs QUIC](#tcpip模型中http3-vs-quic)
|
||||
- [QUIC Protocol](#quic-protocol)
|
||||
- [What is QUIC Used For](#what-is-quic-used-for)
|
||||
- [HTTP/1.1 vs HTTP/2 vs HTTP/3: Main differences](#http11-vs-http2-vs-http3-main-differences)
|
||||
- [Best Features of HTTP/3 and QUIC](#best-features-of-http3-and-quic)
|
||||
- [QUIC handshake](#quic-handshake)
|
||||
- [0-RTT on Prior Connections](#0-rtt-on-prior-connections)
|
||||
- [Head-of-Line Blocking Removal](#head-of-line-blocking-removal)
|
||||
- [HOL blocking术语](#hol-blocking术语)
|
||||
- [How does QUIC remove head-of-line blocking](#how-does-quic-remove-head-of-line-blocking)
|
||||
- [Flexible Bandwidth Management](#flexible-bandwidth-management)
|
||||
- [Pre-Stream Flow Control](#pre-stream-flow-control)
|
||||
- [拥塞控制算法](#拥塞控制算法)
|
||||
|
||||
|
||||
# HTTP3 & QUIC Protocols
|
||||
http3旨在通过QUIC(下一代传输层协议)来令网站更快、更安全。
|
||||
|
||||
在协议高层,http3提供了和http2相同的功能,例如header compression和stream优先级控制。然而,在协议底层,QUIC传输层协议彻底修改了web传输数据的方式。
|
||||
|
||||
## What is HTTP3
|
||||
HTTP是一个应用层的网络传输协议,定义了client和server之间的request-reponse机制,允许client/server发送和接收HTML文档和其他文本、meidia files。
|
||||
|
||||
http3最初被称为`HTTP-over-QUIC`,其主要目标是令http语法及现存的http/2功能能够和QUIC传输协议兼容。
|
||||
|
||||
故而,`HTTP/3`的所有新特性都来源于QUIC层,包括内置加密、新型加密握手、对先前的连接进行zero round-trip恢复,消除头部阻塞问题以及原生多路复用。
|
||||
|
||||
## TCP/IP模型中HTTP3 vs QUIC
|
||||
通过internet传输信息是复杂的操作,涉及到软件和硬件层面。由于不同的设备、工具、软件都拥有不同的特性,故而单一协议无法描述完整的通信流程。
|
||||
|
||||
故而,网络通信是已于通信的协议栈实现的,协议栈中每一层职责都不同。为了使用网络系统来通信,host必须实现构成互联网协议套件的一系列分层协议集。通常,主机至少为每层实现一个协议。
|
||||
|
||||
而HTTP则是应用层协议,令web server和web browser之间可以相互通信。http消息(request/reponse)在互联网中则是通过传输层协议来进行传递:
|
||||
- 在http/2和http/1.1中,通过TCP协议来进行传递
|
||||
- 在`http/3`中,则是通过QUIC协议来进行传递
|
||||
|
||||
> `QUIC`为http/3新基于的传输层协议,之前http都基于tcp协议进行传输
|
||||
|
||||
## QUIC Protocol
|
||||
`QUIC`协议是一个通用的传输层协议,其可以和任意兼容的应用层协议来一起使用,HTTP/3是QUIC的最新用例。
|
||||
|
||||
`QUIC`协议基于`UDP`协议构建,其负责server和client之间应用数据的物理传输。UDP协议是一个简单、轻量的协议,其传输速度高但是缺失可靠性、安全性等特性。QUIC实现了这些高层的传输特性,故而可以用于优化http数据通过网络的传输。
|
||||
|
||||
在HTTP/3中,HTTP的连接从`TCP-based`迁移到了`UDP-based`,底层的网络通信结构都发生了变化。
|
||||
|
||||
### What is QUIC Used For
|
||||
QUIC其创建是同于代替`TCP`协议的,QUIC作为传输层协议,相比于TCP更加灵活,性能问题更少。QUIC协议继承了安全传输的特性,并且拥有更快的adoption rate。
|
||||
|
||||
QUIC协议底层基于UDP协议的原因是`大多数设备只支持TCP和UDP的端口号`。
|
||||
|
||||
除此之外,`QUIC`还利用了UDP的如下特性:
|
||||
- UDP的connectionless特性可以使其将多路复用下移到传输层,并且基于UDP的QUIC实现并不会和TCP协议一样存在头部阻塞的问题
|
||||
- UDP的间接性能够令QUIC重新实现TCP的可靠性和带宽管理功能
|
||||
|
||||
基于QUIC协议的传输和TCP相比是完全不同的方案:
|
||||
- 在底层,其是无连接的,其底层基于UDP协议
|
||||
- 在高层,其是`connection-oriented`的,其在高层重新实现了TCP协议中连接建立、loss detection等特性,从而确保了数据的可靠传输
|
||||
|
||||
综上,QUIC协议结合了UDP和TCP两种协议的优点。
|
||||
|
||||
除了上述优点外,QUIC还`在传输层实现了高级别的安全性`。QUIC集成了`TLS 1.3`协议中的大部分特性,令其和自身的传输机制相兼容。在`HTTP/3` stack中,encryption并非是可选的,而是内置特性。
|
||||
|
||||
TCP, UDP, QUIC协议的相互比较如下:
|
||||
| | TCP | UDP | QUIC |
|
||||
| :-: | :-: | :-: | :-: |
|
||||
Layer in the TCP/IP model | transport | transport | transport |
|
||||
| place in the TCP/IP model | on top of ipv4/ipv6 | on top of ipv4/ipv6 | on top of UDP |
|
||||
| connection type | connection-oriented | connectionless | connection-oriented |
|
||||
| order of delivery | in-order delivery | out-of-order delivery | out-of-order delivery between streams, in order delivery within stremas |
|
||||
| guarantee of delivery | guaranteed | no guarantee of delivery | guaranteed |
|
||||
| security | unencrypted | unencrypted | encrypted |
|
||||
| data identification | knows nothing about the data it transports | knows nothing about the data it transports | use stream IDs to identify the independent streams it transports |
|
||||
|
||||
## HTTP/1.1 vs HTTP/2 vs HTTP/3: Main differences
|
||||
H3除了在底层协议栈传输层中引入QUIC和UDP协议外,还存在其他改动,具体如图所示:
|
||||
<img loading="lazy" alt="Comparison of the HTTP/1.1 vs HTTP/2 vs HTTP/3 protocol stacks" src="https://www.debugbear.com/assets/images/http11-2-3-comparison-88d3a12d6cc3f5422c3700a7f2f8c76b.jpg" width="1526" height="926" style="border-radius:4px" data-abc="true" class="img_CujE">
|
||||
|
||||
HTTP/3-QUIC-UDP stack和TCP-based版本的HTTP最重要的区别如下:
|
||||
- QUIC集成了TLS 1.3协议中绝大部分特性,encryption从应用层移动到了传输层
|
||||
- HTTP/3在不同的streams间并不会对连接进行多路复用,多路复用的特性是由QUIC在传输层执行的
|
||||
- 传输层的多路复用移解决了HTTP/2中TCP中头部阻塞的问题(HTTP/1.1中并不存在头部阻塞问题,因为其会开启多个TCP连接,并且会提供pipelining选项,后来该方案被发现拥有严重实现缺陷,被替换为了HTTP/2中应用层的多路复用)
|
||||
|
||||
## Best Features of HTTP/3 and QUIC
|
||||
HTTP/3和QUIC的新特性能够令server connections速度更快、传输更安全、可靠性更高。
|
||||
|
||||
### QUIC handshake
|
||||
在HTTP2中,client和server在执行handshake的过程中,至少需要2次round-trips:
|
||||
- tcp handshake需要一次round-trip
|
||||
- tls handleshake至少需要一次round-trip
|
||||
|
||||
和QUIC则将上述两次handshakes整合成了一个,HTTP3仅需一次round-trip就可以在client和server之间建立一个secure connection。`QUIC可以带来更快的连接建立和更低的延迟。`
|
||||
|
||||
QUIC集成了`TLS1.3`中的绝大多数特性,`TLS1.3`是目前最新版本的`Transport Layer Security`协议,其代表:
|
||||
- HTTP/3中,消息的加密是强制的,并不像HTTP/1.1和HTTP/2中一样是可选的。在使用HTTP/3时,所有的消息都默认通过encrypted connection来进行发送。
|
||||
- TLS 1.3引入了一个`improved cryptographic handshake`,在client和server间仅需要一次round-trip;而TLS 1.2中则需要两次round-trips用于认证
|
||||
- 而在QUIC中,则将该`improved cryptographic handshake`和其本身用于连接创建的handshake进行了整合,并替代了TCP的handshake
|
||||
- 在HTTP/3中,消息都是在传输层加密的,故而加密的信息比HTTP/1.1和HTTP/2中都更多
|
||||
- 在HTTP/1.1和HTTP/2协议栈中,TLS都运行在应用层,故而HTTP data是加密的,但是,TCP header则是明文发送的,TCP header的明文可能会带来一些安全问题
|
||||
- 在HTTP/3 stack中,TLS运行在传输层,故而不仅http message被加密,大多数QUIC packet header也是被加密的
|
||||
|
||||
简单来说,HTTP/3使用的传输机制相比于TCP-based HTTP版本来说要更加安全。(传输层协议本身的header也被加密)
|
||||
|
||||
### 0-RTT on Prior Connections
|
||||
对于先前存在的connections,QUIC利用了TLS 1.3的`0-RTT`特性。
|
||||
|
||||
`0-RTT`代表zero round-trip time resumption,是TLS 1.3中引入的一个新特性。
|
||||
|
||||
TLS session resumption通过复用先前建立的安全参数,减少建立secure connection所花费的时间。当client和server频繁建立连接并断开时,这将带来性能改善。
|
||||
|
||||
通过0-RTT resumption,client可以在连接的第一个round-trip中发送http请求,复用先前建立的cryptographic keys。
|
||||
|
||||
下面展示了H2和H3 stack在建立连接时的区别:
|
||||
|
||||
<img loading="lazy" alt="Connection setup in the HTTP/2 vs HTTP/3 stacks" src="https://www.debugbear.com/assets/images/http2-vs-http3-roundtrips-1b14a7c6b6f7badbd345f67895dbf142.jpg" width="2063" height="1406" style="border-radius:4px" data-abc="true" class="img_CujE">
|
||||
|
||||
- 当使用HTTP2和TLS 1.2时,client发送第一个http request需要4个round-trip
|
||||
- 在使用HTTP2和TLS 1.3时,client发送第一个http equest需要2、3个round-trip(根据是否使用0-rtt有所不同)
|
||||
- 在使用HTTP3和QUIC时,其默认包含TLS 1.3,其可以在1、2个round-trip内发送第一个http请求(根据是否复用先前连接的加密信息)
|
||||
|
||||
### Head-of-Line Blocking Removal
|
||||
HTTP/3协议栈和HTTP/2协议栈的结构不同,其解决了HTTP/2中最大的性能问题:`head-of-line`阻塞。
|
||||
- 该问题主要发生在HTTP/2中packet丢失的场景下,直到丢失的包被重传前,整个的数据传输过程都会停止,所有packets都必须在网络上等待,这将会导致页面的加载时间延长
|
||||
|
||||
在HTTP/3中,行首阻塞通过原生的多路复用解决了。这是QUIC最重要的特性之一。
|
||||
|
||||
#### HOL blocking术语
|
||||
如下是HOL问题涉及到的概念:
|
||||
- byte stream:是通过网络发送的字节序列。bytes作为不同大小的packets被传输。byte stream本质上是单个资源(file)的物理表现形式,通过网络来发送
|
||||
- 复用:通过复用,可以在一个connection上传输多个byte streams,这将代表浏览器可以在同一个连接上同时加载多个文件
|
||||
- 在HTTP/1.1中,并不支持复用,其会未每个byte stream新开一个tcp连接。HTTP/2中引入了应用层的复用,其只会建立一个TCP连接,并通过其传输所有byte streams。故而,仅有HTTP/2会存在HOL问题
|
||||
- HOL blocking:这是由tcp byte stream抽象造成的性能问题。TCP并不知晓其所传输的数据,并将其所传输的所有数据都看作一个byte stream。故而,如果在网络传输过程中,任意位置的packet发生的丢失,所有在复用连接中的其他packets都会停止传输,并等待丢失的packets被重传
|
||||
- 这代表,复用的连接中,所有byte streams都会被TCP协议看作是一个byte stream,故而stream A中的packet丢失也会造成stream B的传输被阻塞,直至丢失packet被重传
|
||||
- 并且,TCP使用了in-order传输,如果发生packet丢失,那么将阻塞整个的传输过程。在高丢包率的环境下,这将极大程度上影响传输速度。即使在HTTP/2中已经引入了性能优化特性,在2%丢包率的场景下,也会比HTTP/1.1的传输速度更慢
|
||||
- native multiplexing:在HTTP/3协议栈中,复用被移动到了传输层,实现了原生复用。QUIC通过stream ID来表示每个byte stream,并不像TCP一样将所有byte streams都看作是一个。
|
||||
|
||||
#### How does QUIC remove head-of-line blocking
|
||||
QUIC基于UDP实现,其使用了out-of-order delivery,故而每个byte stream都通过网络独立的进行传输。然而,为了可靠性,QUIC确保了在同一byte stream内packets的in-order delivery,故而相同请求中关联的数据到达的顺序是一致的。
|
||||
|
||||
QUIC标识了所有byte stream,并且streams是独立进行传输的,如果发生packet丢失,其他byte streams并不会停止并等待重传。
|
||||
|
||||
下图中展示了QUIC原生复用和HTTP2应用层复用的区别:
|
||||
<img loading="lazy" alt="HTTP/2 vs QUIC multiplexing diagrams" src="https://www.debugbear.com/assets/images/http2-vs-quic-multiplexing-36d594c1c356cae410fe60cfc2945d91.jpg" width="900" height="532" style="border-radius:4px" data-abc="true" class="img_CujE">
|
||||
|
||||
如上图所示,HTTP/2和HTTP/3在传输多个资源时,都只创建了一个连接。但是,QUIC中不同byte streams是独立传输的,拥有不同的传输路径,并且不同byte streams之间不会彼此阻塞。
|
||||
|
||||
即使QUIC解决了HTTP/2中引入的HOL问题,乱序传输也会存在弊端:byte streams并不会按照其被发送的顺序到达。例如,在使用乱序传输时,最不重要的资源可能会最先到达。
|
||||
|
||||
#### Flexible Bandwidth Management
|
||||
带宽管理用于在packets和streams之间按照最优的方式对网络带宽进行分配。这是至关重要的功能,发送方和接收方的机器以及二者之间的网络节点处理packets的速度都有所不同,并且速度也会动态变化。
|
||||
|
||||
带宽管理有助于避免网络中的数据溢出和拥塞,这些问题可能会导致server响应速度变慢,同时也可能会带来安全问题。
|
||||
|
||||
UDP中并没有内置带宽控制,QUIC则是在HTTP3协议栈中负责该功能,其对TCP带宽管理中的两大部分进行了重新实现:
|
||||
- 流控制: 其在接收方限制了数据发送的速率,用于避免发送方造成接收方过载
|
||||
- 拥塞控制:其限制了发送方和接收方之间的路径中每一个节点的发送速率,用于避免网络拥塞
|
||||
|
||||
##### Pre-Stream Flow Control
|
||||
为了支持独立的stream,QUIC采用了per-stream based flow control。其在两个级别控制了stream data的带宽消耗:
|
||||
- 对于每个独立的流,都设置了一个可分配给其的最大数据数量
|
||||
- 在整个连接范的围内,设置了active streams最大的累积数量
|
||||
|
||||
通过per-stream flow control,QUIC限制了同时可以发送的数据数量,用于避免接收方过载,并且在多个streams间大致公平的分配网络容量。
|
||||
|
||||
##### 拥塞控制算法
|
||||
QUIC允许实现选择不同的拥塞控制算法,使用最广泛的算法如下:
|
||||
- NewReno:TCP使用的拥塞控制算法
|
||||
- CUBIC: 和NewReno类似,但是使用了cubic function而不是linear function
|
||||
- BBR
|
||||
|
||||
在网络状况较差的场景下,不同的拥塞控制算法性能可能存在较大差异。
|
||||
|
||||
114
java se/CompletableFuture.md
Normal file
114
java se/CompletableFuture.md
Normal file
@@ -0,0 +1,114 @@
|
||||
# CompletableFuture
|
||||
对于Future对象,需要调用其get方法来获取值,get方法会阻塞当前线程直到该值可获取。
|
||||
而CompletableFuture实现了Future接口,并且其提供了其他机制来获取result。通过CompletableFuture可以注册一个callback,该回调会在result可获取时调用。
|
||||
```java
|
||||
CompletableFuture<String> f = . . .;
|
||||
f.thenAccept(s -> Process the result string s);
|
||||
```
|
||||
**通过这种方法,就无需等待result处于可获取状态之后再对其进行处理。**
|
||||
通常情况下,很少有方法返回类型为CompletableFuture,此时,需要自己指定返回类型。CompletableFuture使用方法如下:
|
||||
```java
|
||||
public CompletableFuture<String> readPage(URL url)
|
||||
{
|
||||
return CompletableFuture.supplyAsync(() ->
|
||||
{
|
||||
try
|
||||
{
|
||||
return new String(url.openStream().readAllBytes(), "UTF-8");
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
}, executor);
|
||||
}
|
||||
```
|
||||
> Compatable.supplyAsync方法接受一个Supplier而不是一个Callable,虽然它们都没有参数且返回一个T,但是Callable允许抛出Exception,但Supplier不许抛出Checked Exception。
|
||||
> 并且,**再没有显式为supplyAsync方法指定Executor参数时,其会默认采用ForkJoinPool.commonPool()**
|
||||
|
||||
Complatable可以以两种方式执行结束:(1)是否有返回值(2)是否有未捕获异常。要处理以上两种情况,需要使用whenComplete方法。提供给whenComplete函数的方法将会和执行result(如果没有返回值则为null)与抛出异常exception(如果没有则为null)一起被调用。
|
||||
```java
|
||||
f.whenComplete((s, t) -> {
|
||||
if (t == null)
|
||||
{
|
||||
Process the result s;
|
||||
}
|
||||
else
|
||||
{
|
||||
Process the Throwable t;
|
||||
}
|
||||
});
|
||||
```
|
||||
CompletableFuture可以手动设置completable value。虽然CompletableFuture对象的完成状态在supplyAsync方法的task执行结束之后会被隐式设置为已完成,但是通过手动设置完成状态可以提供更大的灵活性,例如可以同时以两种方式进行计算,任一方式先计算结束则将CompletableFuture对象的完成状态设置为已完成:
|
||||
```java
|
||||
var f = new CompletableFuture<Integer>();
|
||||
executor.execute(() ->
|
||||
{
|
||||
int n = workHard(arg);
|
||||
f.complete(n);
|
||||
});
|
||||
executor.execute(() ->
|
||||
{
|
||||
int n = workSmart(arg);
|
||||
f.complete(n);
|
||||
});
|
||||
```
|
||||
如果想要以抛出异常的方式将CompletableFuture对象的完成状态设置为已完成,可以调用completeExcetpionally方法:
|
||||
```java
|
||||
Throwable t = . . .;
|
||||
f.completeExceptionally(t);
|
||||
```
|
||||
> 在不同的线程中调用同一个对象的complete或completeExceptionally方法是线程安全的,如果该completableFuture对象的完成状态为已完成,那么再次调用complete或completeExceptionally方法无效。
|
||||
|
||||
> ## Caution
|
||||
> 并不像Future对象,CompletableFuture对象在调用其cancel方法时,并不会对其task执行的线程进行中断操作,而是**仅仅将completableFuture对象的完成状态设置为抛出CancellationException的已完成状态。**
|
||||
> 因为对于一个CompletableFuture对象,可能并没有一个唯一的线程对应其task的执行(future对象对应task可能未在任何线程中执行,也可能在多个线程中执行)
|
||||
|
||||
## CompletableFuture组合
|
||||
异步方法的非阻塞调用是通过callback来实现的,调用者注册一个action,该action在task被完成时调用。如果注册的action仍然是异步的,那么该action的下一个action位于另一个完全不同的callback中。
|
||||
```java
|
||||
f1.whenComplete((s,t)->{
|
||||
CompletableFuture<> f2 = ...
|
||||
f2.whencomplete((s,t)->{
|
||||
//...
|
||||
})
|
||||
})
|
||||
```
|
||||
这样可能会造成callback hell的情况,并且异常处理也特别不方便,由此CompletableFuture提供了一种机制来组合多个CompletableFuture:
|
||||
```java
|
||||
CompletableFuture<String> contents = readPage(url);
|
||||
CompletableFuture<List<URL>> imageURLs =
|
||||
contents.thenApply(this::getLinks);
|
||||
```
|
||||
在completableFuture对象上调用thenApply方法,thenApply方法也不会阻塞,其会返回另一个completableFuture对象,并且,当第一个Future对象contents返回之后,返回结果会填充到getLinks方法的参数中。
|
||||
### CompletableFuture常用方法
|
||||
CompletableFuture组合可以有如下常用方法:
|
||||
- thenApply(T -> U):对CompletableFuture对象的返回结果执行操作,并且产生一个返回值
|
||||
- thenAccept(T -> void):类似thenApply,操作上一个future的返回值,但是返回类型为void
|
||||
- handle(T,Throwable)->U:处理上一个future返回的result并且产生一个返回值
|
||||
- thenCompose(T->CompletableFuture\<U\>):将上一个Future返回值作为参数传递,并且返回CompletableFuture\<U\>
|
||||
- whenComplete(T,Throwable)->void:类似于handle,但是不产生返回值
|
||||
- exceptionally(Throwable->T):处理异常并返回一个结果
|
||||
- completeOnTimeout(T,long,TimeUnit):当超时时,将传入的参数T作为返回结果
|
||||
- orTimeOut(long,TimeUnit):当超时时,产生一个TimeoutExcetpion作为结果
|
||||
- thenRun(Runnable):执行该Runnable并且返回一个CompletableFuture<void>
|
||||
如上的每一个方法都有另一个Async版本,如thenApplyAsync:
|
||||
```java
|
||||
CompletableFuture<U> future.thenApply(f);
|
||||
CompletableFuture<U> future.thenApplyAsync(f);
|
||||
```
|
||||
thenApply中,会对future的返回结果执行f操作;而在thenApplyAsync中,对f操作的执行会在另一个线程中。
|
||||
thenCompose通常用来连接两个返回类型都为CompletableFuture的方法。
|
||||
|
||||
### CompletableFuture Combine
|
||||
- thenCombine(CompletableFuture\<U\>,(T,U)->V):执行两个future,并且通过f对两个future的返回值进行combine
|
||||
- thenAcceptBoth(CompletableFuture\<U\>,(T,U)->Void):执行两个future,并通过f处理两个future的返回值,但是返回类型为void
|
||||
- runAfterBoth(CompletableFuture\<?\>,Runnable):当两个future都执行完之后,执行Runnable
|
||||
- applyToEither(CompletableFuture\<T\>,(T)-> U):当任一future执行完成,通过该result产生返回值
|
||||
- acceptEither(CompletableFuture\<T\>,T->Void):类似applyToEither,但是返回值为void
|
||||
- runAfterEither(CompletableFuture\<?\>,Runnable):在任一future执行完成之后,执行Runnable
|
||||
- static allOf(CompletableFuture\<?\>...):在参数中所有future执行完成之后,返回的future处于完成状态
|
||||
- static anyOf(CompletableFuture\<?\>...):在参数中任一future执行完成之后,返回的future处于完成状态
|
||||
|
||||
### completedFuture
|
||||
CompletableFuture.completedFuture会返回一个已经执行完成的CompletableFuture对象,并且该future对象返回值为T
|
||||
74
java se/classloader.md
Normal file
74
java se/classloader.md
Normal file
@@ -0,0 +1,74 @@
|
||||
# Classloader
|
||||
Java ClassLoader是构成JRE(Java Runtime Environment)的一部分,ClassLoader负责动态的将java classes加载到JVM中。java classes并不是一次性全部加载到内存中的,而是在应用需要时动态加载到内存中。
|
||||
|
||||
> JRE调用classloader,然后classloader负责将classes动态加载到内存中。
|
||||
|
||||
在java`按需将需要的类动态加载到内存中`的特性中,classloader扮演了重要的作用,令Java应用更加的弹性和高效。
|
||||
|
||||
## Types of ClassLoaders in Java
|
||||
java ClassLoaders可以被归类为不同的类型,每种类型负责从指定的路径加载classes:
|
||||
### Bootstrap ClassLoader(原始类加载器)
|
||||
- Bootstrap Classloader是机器码,负责初始化JVM的操作
|
||||
- 直到java 8中,其从`rt.jar`中加载核心java文件;但是从java 9开始,其负责从java runtime image中加载核心java文件
|
||||
- Bootstrap ClassLoader独立的操作,其并没有parent ClassLoader
|
||||
|
||||
### Platform ClassLoader(Extension ClassLoader)
|
||||
- 在java 9之前,存在Extension ClassLoader,但是在java 9及之后,其被称作Platform ClassLoader
|
||||
- 该classloader会从JDK module系统中加载平台特定的拓展
|
||||
- platform class loader会从java runtime image中加载文件,同时也会从`java.platform`属性所指定的module或`--module-path`所指定的module处加载文件
|
||||
|
||||
### System ClassLoader(Application ClassLoader)
|
||||
- System ClassLoader也被称为`Application ClassLoader`,其从应用的classpath加载classes
|
||||
- 其是Platform ClassLoader的一个child
|
||||
- 将会从`CLASSPATH`环境变量、`-classpath`或`-cp`选项所指定的目录处加载
|
||||
|
||||
## Principles of Functionality of a Java ClassLoader
|
||||
java ClasLoader基于如下准则进行操作:
|
||||
### Delegation Model
|
||||
- ClassLoaders遵循委托继承算法
|
||||
- 当JVM遇到一个class时,其会检查其是否已经被加载,如果该class还未被加载,那么JVM会将class的加载委托给`a chain of ClassLoaders`
|
||||
- 该委托架构从Application ClassLoader开始,向上移动到Platform ClassLoader,最终移动到Bootstrap ClassLoader
|
||||
- 在层次结构中的每一个ClassLoader,都会在其负责的locations处查找class,并且在必要时将class的加载过程委托给父级
|
||||
|
||||
### Visibility Principle
|
||||
- 由Parent ClassLoaders负责加载的classes对于child classloader来说也是可见的;反之,由child classloader负责加载的classes对于parent classloader来说则不可见
|
||||
- 上述的可见性定义确保了封装性,防止由不同的classloaders负责加载的classes出现冲突
|
||||
|
||||
### Uniqueness Property
|
||||
- ClassLoader会确保class只会被加载一次,从而保证class在JVM中的唯一性
|
||||
- 只有当parent ClassLoader无法查询到某个类时,当前classloader才会尝试去加载该类
|
||||
|
||||
## Methods of java.lang.ClassLoader
|
||||
为了加载类,ClassLoader中提供了如下方法:
|
||||
- `loadClass(String name, boolean resolve)`: 其会加载JVM所引用的类,并在必要时进行解析
|
||||
- `defineClass()`: 该方法会将一个字节数组转化为Class实例对象。如果字节数组中的内容不是有效的class,那么将会抛出ClassFormatError异常
|
||||
- `findClass(String name)`:该方法会对class进行查找,但是并不会对其进行加载
|
||||
- `findLoadedClass(String name)`:该方法用于验证是否该类已经被加载
|
||||
- `Class.forName(String name, boolean initialize, ClassLoader loader)`: 该方法会加载并初始化class,允许指定ClassLoader,如果该classLoader没有指定,则默认会使用Bootstrap ClassLoader
|
||||
|
||||
上述方法的使用示例如下:
|
||||
```java
|
||||
// Code executed before class loading
|
||||
Class<?> clazz = ClassLoader.getSystemClassLoader().loadClass("com.example.MyClass");
|
||||
```
|
||||
<img alt="ClassLoader in Java" height="inherit" src="https://media.geeksforgeeks.org/wp-content/uploads/jvmclassloader.jpg" width="inherit">
|
||||
|
||||
### Delegation Model
|
||||
JVM和ClassLoader使用了委托层次结构算法来加载class,classLoader运行原则如下:
|
||||
- classLoader永远会遵循委托层次结构原则
|
||||
- 当jvm遇到class时,其首先会检查该class是否已经被加载
|
||||
- 如果该类未被加载,那么jvm将会请求classloader sub-system去加载该类,而classloader sub-system则会将该加载工作委托给Application ClassLoader
|
||||
- Application Classloader会将该类的加载任务委托给Extension ClassLoader,而Extension ClassLoader则是会再次将该加载任务委托给Bootstrap Classloader
|
||||
- Bootstrap Classloader会在bootstrap classpath(JDK/JRE/LIB/EXT)路径下查找该类,如果该class存在,那么其将会被加载,否则加载请求将会回到Extension Classloader
|
||||
- Extension Classloader也会从Extension classpath下查找该类,如果查找到该类,则加载该类,否则请求将会被委托给Application ClassLoader
|
||||
- Application ClassLoader将会在Application classpath路径下查找该类,如果类存在会加载该类,否则其会抛出ClassNotFoundException
|
||||
|
||||
### Visibility Principle
|
||||
可见性原则保证了`由parent classloader加载的类对于child classloader来说是可见的,而child classloader加载的类对parent classloader来说是不可见的`。
|
||||
|
||||
假设类`GEEK.class`被Extension ClassLoader加载,那么该类仅被Extension ClassLoader和Application Classloader可见,对于Bootstrap ClassLoader来说`GEEK.class`是不可见的。如果该类尝试通过Bootstrap ClassLoader再次加载,其会报`ClassNotFoundException`
|
||||
|
||||
### Uniqueness Property
|
||||
唯一性保证了class是唯一的,其保证了由parent classloader加载的类不会再被child classloader加载。只有当parent classloader无法查找到该类时,当前classloader才会尝试对其进行加载
|
||||
|
||||
|
||||
29
java se/juc.md
Normal file
29
java se/juc.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# juc
|
||||
## ThreadLocal
|
||||
通过ThreadLocal api,可以存储对象,并且存储的对象只有指定线程才能够访问。
|
||||
|
||||
### 示例
|
||||
```java
|
||||
// 声明一个ThreadLocal对象,其中存储的Integer值和特定线程绑定
|
||||
ThreadLocal<Integer> threadLocalVlaue = new ThreadLocal<>();
|
||||
// 再特定线程中,调用get/set方法,可以对与该线程绑定
|
||||
// 的integer值进行获取或设置
|
||||
threadLocalValue.set(1);
|
||||
Integer result = threadLocalValue.get();
|
||||
```
|
||||
ThreadLocal类似于一个map,其中key为线程,value为与线程绑定的值,再特定线程中调用
|
||||
|
||||
### api
|
||||
#### withInitial
|
||||
可以通过`ThreadLocal.withInitial`方法来构造一个带初始值的threadLocal对象,该静态方法接收一个supplier对象。
|
||||
```java
|
||||
ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 1);
|
||||
```
|
||||
如果一个ThreadLocal对象指定了withInitial方法,那么当该ThreadLocal对象get为空时,会调用withInitial并用该方法的返回值来初始化该threadLocal对象。
|
||||
|
||||
#### remove
|
||||
如果要移除ThreadLocal中的对象,可以调用`remove`方法
|
||||
```java
|
||||
threadLocal.remove();
|
||||
```
|
||||
|
||||
379
java se/nio.md
Normal file
379
java se/nio.md
Normal file
@@ -0,0 +1,379 @@
|
||||
# NIO
|
||||
## 简介
|
||||
nio库在jdk 1.4时被引入,和传统i/o不同的是,nio性能高且基于block。在nio中,定义了类来持有数据,并以block为单位对数据进行处理。
|
||||
|
||||
### nio和传统io区别
|
||||
nio和传统io最大的区别是数据打包和传输的方式。在传统io中,会以stream的形式来处理数据;而在nio中,数据则是以block的形式被处理。
|
||||
|
||||
面向stream的io系统在同一时刻只能处理1字节的数据,通常较慢。
|
||||
|
||||
面向block的io系统则是以block为单位处理数据,处理速度较传统io快。
|
||||
|
||||
## Channel/Buffer
|
||||
channel和buffer是nio中的核心对象,几乎在每个io操作中被使用。
|
||||
|
||||
channel类似于传统io中的stream,所有数据的写入或读取都需要通过channel。而buffer本质上则是数据的容器,`所有被发送到channel中的数据都要先被放置到buffer中`,`所有从channel中读取的数据都要先被读取到buffer中`。
|
||||
|
||||
### Buffer
|
||||
Buffer是一个类,用于存储数据,buffer中的数据要么将要被写入到Channel中,要么刚从Channel中读取出来。
|
||||
|
||||
> Buffer是nio和传统io的重要区别,在传统io中,数据直接从stream中被读取出来,也被直接写入到stream中。
|
||||
>
|
||||
> 在nio中,数据的读取和写入都需要经过Buffer
|
||||
|
||||
buffer的本质是一个字节数组,buffer提供了对数据的结构化访问,并且,buffer还追踪了系统的读/写操作。
|
||||
|
||||
#### Buffer类型
|
||||
最常用的Buffer类型是ByteBuffer,其支持对底层的字节数据进行set/get操作,初次之外,Buffer还有其他类型。
|
||||
|
||||
- ByteBuffer
|
||||
- CharBuffer
|
||||
- ShortBuffer
|
||||
- IntBuffer
|
||||
- LongBuffer
|
||||
- FloatBuffer
|
||||
- DoubleBuffer
|
||||
|
||||
上述的每个Buffer类都实现了Buffer接口,除了ByteBuffer之外,其他每个Buffer类型都拥有相同的操作。由于ByteBuffer被绝大多数标准io操作锁使用,故而ByteBuffer除了拥有上述操作外,还拥有一些额外的操作。
|
||||
|
||||
#### Buffer设计
|
||||
Buffer是存储指定primitive type数据的容器(Long,Byte,Int,Float,Char,Double,Short),buffer是线性并且有序的。
|
||||
|
||||
buffer的主要属性包括capacity、limit和position:
|
||||
- capacity:buffer的capacity是其可以包含的元素数量,capacity不可以为负数并且不可以被改变
|
||||
- limit:buffer的limit代表buffer中第一个不应该被读/写的元素下表,`(例如buffer的capacity为5,但是其中只包含3个元素,那么limit则为3)`,limit不能为负数,并且limit不可以比capacity大
|
||||
- position:position代表下一个被读/写元素的下标,position部为负数也不应该比limit大
|
||||
|
||||
#### 传输数据
|
||||
buffer的每个子类都定义了get/put类似的操作,相应的read或write操作会从position转移一个或多个元素的数据,如果请求转移的数据比limit大,那么相应的get操作会抛出`BufferUnderflowException`,set操作则是会抛出`BufferOverflowException`;在上述两种情况下,没有数据会被传输。
|
||||
|
||||
#### mark & reset
|
||||
buffer的mark操作标识对buffer执行reset操作后position会被重置到的位置。mark并非所有情况下都被定义,当mark被定义时,其应该不为负数并且不应该比position大。如果position或limit被调整为比mark更小的值之后,mark值将会被丢弃。如果在mark没有被定义时调用reset操作,那么会抛出`InvalidMarkException`异常。
|
||||
|
||||
> #### 属性之间的大小关系
|
||||
>
|
||||
> `0<=mark<=position<=limit<=capacity`
|
||||
|
||||
#### clear/flip/rewind
|
||||
除了对buffer定义mark/reset操作之外,buffer还定义了如下操作:
|
||||
- `clear()`:使buffer可以执行新的sequence channel-read操作或relative put操作
|
||||
- clear操作会将limit设置为capacity的值并且将position设置为0
|
||||
- `flip()`:使buffer可以执行sequence channel-wirte操作和relative get操作:
|
||||
- flip操作会将limit设置为position,并且将position设置为0
|
||||
- `rewind()`:使buffer中的数据可以重新被读取:
|
||||
- rewind操作会将position设置为0,limit保持不变
|
||||
|
||||
> 在向buffer写入数据之前,应该调用`clear()`方法,将limit设置为capacity并将position设置为0,从而可以向[0, capacity-1]的范围内写入数据
|
||||
>
|
||||
> 当写入完成之后,想要从buffer中读取数据时,需要先调用`flip()`方法,将limit设置为position并且将position设置为0,从而可以读取[0, position-1]范围内的数据
|
||||
>
|
||||
> 如果想要对buffer中的数据进行重新读取,可以调用`rewind()`方法,其将position设置为0,从而可以对[0, limit-1]范围内的数据重新进行读取。
|
||||
|
||||
#### Buffer get/put方法
|
||||
##### get
|
||||
在ByteBuffer中,拥有如下`get`方法:
|
||||
```java
|
||||
byte get();
|
||||
ByteBuffer get( byte dst[] );
|
||||
ByteBuffer get( byte dst[], int offset, int length );
|
||||
byte get( int index );
|
||||
```
|
||||
其中,前3个get方法都是relative方法,其基于当前position和limit来获取数据。第4个get方法则是absolute方法,其不基于position和limit,而是基于index绝对偏移量来获取数据。
|
||||
|
||||
> absolute get方法调用不基于position和limit,也不会对position和limit造成影响。
|
||||
|
||||
##### put
|
||||
ByteBuffer拥有如下`put`方法:
|
||||
```java
|
||||
ByteBuffer put( byte b );
|
||||
ByteBuffer put( byte src[] );
|
||||
ByteBuffer put( byte src[], int offset, int length );
|
||||
ByteBuffer put( ByteBuffer src );
|
||||
ByteBuffer put( int index, byte b );
|
||||
```
|
||||
|
||||
#### Buffer allocation and warpping
|
||||
在创建buffer之前,必须对buffer进行分配,
|
||||
```java
|
||||
ByteBuffer buffer = ByteBuffer.allocate(1024);
|
||||
```
|
||||
`Buffer.allocate`会创建一个指定大小的底层数组,并将数组封装到一个buffer对象中。
|
||||
|
||||
除了调用`allocate`之外,还可以将一个已经存在的数组分配给buffer:
|
||||
```java
|
||||
byte array[] = new byte[1024];
|
||||
ByteBuffer buffer = ByteBuffer.wrap(array);
|
||||
```
|
||||
在调用`wrap`方法之后,会创建一个buffer对象,buffer对象对array进行的包装。此后,既可以通过array直接访问数组,也可以通过buffer访问数组。
|
||||
|
||||
#### Buffer slice
|
||||
buffer slice会基于buffer对象创建一个sub buffer,新建的sub buffer会和原来的buffer共享部分的底层数组数据。
|
||||
|
||||
```java
|
||||
ByteBuffer buffer = ByteBuffer.allocate(10);
|
||||
|
||||
for (int i = 0; i < buffer.capacity(); ++i) {
|
||||
buffer.put((byte) i);
|
||||
}
|
||||
|
||||
buffer.position(3);
|
||||
buffer.limit(7);
|
||||
|
||||
ByteBuffer slice = buffer.slice();
|
||||
```
|
||||
通过上述方法,可以创建一个包含原始buffer[3,6]范围内元素的sub buffer。但是,sub buffer和buffer共享[3,6]范围内的数据,实例如下所示:
|
||||
```java
|
||||
// 将[3,6]范围内的元素都 * 11
|
||||
for (int i = 0; i < slice.capacity(); ++i) {
|
||||
byte b = slice.get(i);
|
||||
b *= 11;
|
||||
slice.put(i, b);
|
||||
}
|
||||
|
||||
// 重置original buffer的position和limit并读取数据
|
||||
buffer.position(0);
|
||||
buffer.limit(buffer.capacity());
|
||||
while (buffer.remaining() > 0) {
|
||||
System.out.println(buffer.get());
|
||||
}
|
||||
```
|
||||
修改sub buffer中的元素内容后,访问original buffer中的内容,输出结果如下所示:
|
||||
```shell
|
||||
$ java SliceBuffer
|
||||
0
|
||||
1
|
||||
2
|
||||
33
|
||||
44
|
||||
55
|
||||
66
|
||||
7
|
||||
8
|
||||
9
|
||||
```
|
||||
易得知在sub buffer修改内容后,内容修改对buffer也可见。
|
||||
|
||||
#### ReadOnly Buffer
|
||||
对于readonly buffer,可以从其中读取值,但是无法向其写入值。对于任何常规buffer,可以对其调用`buffer.asReadOnlyBuffer()`来获取一个readonly buffer。
|
||||
|
||||
#### Direct & Indirect Buffer
|
||||
direct buffer的内存通过特定方式来分配,从而增加io速度。
|
||||
|
||||
对于direct buffer,jvm在将尽量避免在调用本地io操作前(后),将buffer中内容写入/读取到中间buffer。
|
||||
|
||||
|
||||
|
||||
|
||||
### Channel
|
||||
Channel为一个对象,可以从channel中读取数据或向channel写入数据。将传统io和nio相比较,channel类似于stream。
|
||||
|
||||
Channel和Stream不同的是,channel为双向的,而Stream则仅支持单项(InputStream或OutputStream)。一个开启的channel可以被用于读取、写入或同时读取或写入。
|
||||
|
||||
## Channel读写
|
||||
向channel读取或写入数据十分简单:
|
||||
- 读:仅需创建一个buffer对象,并且访问channel将数据读取到buffer对象中
|
||||
- 写:创建一个buffer对象,并将数据填充到buffer对象,之后访问channel并将bufffer中的数据写入到channel中
|
||||
|
||||
### 读取文件
|
||||
通过nio读取文件分为如下步骤:
|
||||
1. 从FileInputStream中获取Channel
|
||||
2. 创建buffer对象
|
||||
3. 将channel中的数据读取到buffer
|
||||
|
||||
示例如下所示:
|
||||
```java
|
||||
FileInputStream fin = new FileInputStream( "readandshow.txt" );
|
||||
// 1. 获取channel
|
||||
FileChannel fc = fin.getChannel();
|
||||
// 2. 创建buffer
|
||||
ByteBuffer buffer = ByteBuffer.allocate(1024);
|
||||
// 3. 将数据从channel中读取到buffer中
|
||||
fc.read( buffer );
|
||||
```
|
||||
|
||||
> 如上述示例所示,在从channel读取数据时,无需指定向buffer中读取多少字节的数据,每个buffer内部中都有一个复杂的统计系统,会跟踪已经读取了多少字节的数据、buffer中还有多少空间用于读取更多数据。
|
||||
|
||||
### 写入文件
|
||||
通过nio写入文件步骤和读取类似,示例如下所示:
|
||||
```java
|
||||
FileOutputStream fout = new FileOutputStream( "writesomebytes.txt" );
|
||||
// 1. 获取channel
|
||||
FileChannel fc = fout.getChannel();
|
||||
// 2. 创建buffer对象
|
||||
ByteBuffer buffer = ByteBuffer.allocate( 1024 );
|
||||
for (int i = 0; i < message.length; ++i) {
|
||||
buffer.put(message[i]);
|
||||
}
|
||||
|
||||
buffer.flip();
|
||||
|
||||
// 3. 将buffer中的数据写入到channel中
|
||||
fc.write(buffer);
|
||||
```
|
||||
|
||||
在向channel写入数据时,同样无需指定需要向channel写入多少字节的数据,buffer内部的统计系统同样会追踪buffer中含有多少字节的数据、还有多少数据待写入。
|
||||
|
||||
### 同时读取和写入文件
|
||||
同时读取和写入文件的示例如下所示:
|
||||
```java
|
||||
public class CopyFile {
|
||||
static public void main(String args[]) throws Exception {
|
||||
if (args.length < 2) {
|
||||
System.err.println("Usage: java CopyFile infile outfile");
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
String infile = args[0];
|
||||
String outfile = args[1];
|
||||
|
||||
try (FileInputStream fin = new FileInputStream(infile);
|
||||
FileOutputStream fout = new FileOutputStream(outfile)) {
|
||||
|
||||
FileChannel fcin = fin.getChannel();
|
||||
FileChannel fcout = fout.getChannel();
|
||||
|
||||
ByteBuffer buffer = ByteBuffer.allocate(1024);
|
||||
|
||||
while (true) {
|
||||
buffer.clear();
|
||||
int r = fcin.read(buffer);
|
||||
|
||||
if (r == -1) {
|
||||
break;
|
||||
}
|
||||
|
||||
buffer.flip();
|
||||
fcout.write(buffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
在上述示例中,通过`channel.read(buffer)`的返回值来判断是否已经读取到文件末尾,如果返回值为-1,那么代表文件读取已经完成。
|
||||
|
||||
## File Lock
|
||||
可以针对整个文件或文件的部分进行加锁。文件锁分为独占锁和共享锁,如果获取了独占锁,那么其他进程将无法获取同一文件加锁;如果获取了共享锁,那么其他进程也可以获取同一文件的共享锁,但是无法获取到独占锁。
|
||||
|
||||
### 对文件进行上锁
|
||||
在获取锁时,需要在FileChannel上调用lock方法:
|
||||
```java
|
||||
RandomAccessFile raf = new RandomAccessFile("usefilelocks.txt", "rw");
|
||||
FileChannel fc = raf.getChannel();
|
||||
FileLock lock = fc.lock(start, end, false);
|
||||
```
|
||||
释放锁方法如下所示:
|
||||
```java
|
||||
lock.release();
|
||||
```
|
||||
## Network和异步IO
|
||||
通过异步io,可以在没有blocking的情况下读取和写入数据,在通常情况下,调用read方法会一直阻塞到数据可以被读取,而write方法会一直阻塞到数据可以被写入。
|
||||
|
||||
异步IO通过注册io事件来实现,当例如新的可读数据、新的网络连接到来时,系统会根据注册的io时间监听来发送消息提醒。
|
||||
|
||||
异步io的好处是,可以在同一线程内处理大量的io操作。在传统的io操作中,如果要处理大量io操作,通常需要轮询、创建大量线程来针对io操作进行处理。
|
||||
- 轮询:对io请求进行排队,处理完一个io请求后再处理别的请求
|
||||
- 创建大量线程:针对每个io请求,为其创建一个线程进行处理
|
||||
|
||||
通过nio,可以通过单个线程来监听多个channel的io事件,无需轮询也无需额外的事件。
|
||||
|
||||
### Selector
|
||||
nio的核心对象为selector,将channel及其感兴趣的io事件类型注册到selector后,如果io事件触发,那么`selector.select`方法将会返回对应的SelectionKey。
|
||||
|
||||
Selector创建如下:
|
||||
|
||||
```java
|
||||
Selector selector = Selector.open();
|
||||
```
|
||||
|
||||
### 开启ServerSocketChannel
|
||||
为了接收外部连接,需要一个ServerSocketChannel,创建ServerSocketChannel并配置对应ServerSocket的示例如下所示:
|
||||
```java
|
||||
ServerSocketChannel ssc = ServerSocketChannel.open();
|
||||
ssc.configureBlocking(false);
|
||||
ServerSocket ss = ssc.socket();
|
||||
|
||||
InetSocketAddress address = new InetSocketAddress(ports[i]);
|
||||
ss.bind(address);
|
||||
```
|
||||
|
||||
### Selection keys
|
||||
在创建完SelectableChannel之后,需要将channel注册到selector,可以调用`channel.register`来执行注册操作:
|
||||
```java
|
||||
SelectionKey key = ssc.register(selector, SelectionKey.OP_ACCEPT);
|
||||
```
|
||||
|
||||
上述方法为ServerSocketChannel注册了OP_ACCEPT的事件监听,OP_ACCEPT事件当新连接到来时将会被触发,OP_ACCEPT是适用于ServerSocketChannel的唯一io事件类型。
|
||||
|
||||
register方法的返回值SelectionKey代表channel和selector的注册关系,当io事件触发时,selector也会返回事件关联的SelectionKey。
|
||||
|
||||
除此之外,SelectionKey也可以被用于取消channel对selector的注册。
|
||||
|
||||
### inner loop
|
||||
在向selector注册完channel之后,会进入到selector的循环
|
||||
```java
|
||||
while (true) {
|
||||
selector.select();
|
||||
|
||||
Set<SelectionKey> selectedKeys = selector.selectedKeys();
|
||||
Iterator<SelectionKey> it = selectedKeys.iterator();
|
||||
|
||||
while (it.hasNext()) {
|
||||
SelectionKey key = it.next();
|
||||
// ... deal with I/O event ...
|
||||
}
|
||||
}
|
||||
```
|
||||
在调用`selector.select`方法时,改方法会阻塞,直到有至少一个注册的io事件被触发。`selector.select`会返回触发事件的个数。
|
||||
|
||||
`select.selectedKeys()`方法则是会返回被触发事件关联的SelectionKey集合。
|
||||
|
||||
对于每个SelectionKey,需要通过`key.readyOps()`来判断事件的类型,并以此对其进行处理。
|
||||
|
||||
### 为新连接注册
|
||||
当新连接到来后,需要将新连接注册到selector中
|
||||
|
||||
```java
|
||||
ServerSocketChannel ssc = (ServerSocketChannel)key.channel();
|
||||
SocketChannel sc = ssc.accept();
|
||||
|
||||
sc.configureBlocking(false);
|
||||
// 监听新连接的READ事件类型
|
||||
sc.register(selector, SelectionKey.OP_READ);
|
||||
```
|
||||
|
||||
### remove SelectionKey
|
||||
在完成对io事件的处理之后,必须要将SelectionKey从selectedKeys之中移除,否则selectionKey将会被一直作为活跃的io事件
|
||||
```java
|
||||
it.remove();
|
||||
```
|
||||
|
||||
### 新连接有可读数据
|
||||
当注册的新连接传来可读数据之后,可读数据处理方式如下:
|
||||
```java
|
||||
else if ((key.readyOps() & SelectionKey.OP_READ)
|
||||
== SelectionKey.OP_READ) {
|
||||
|
||||
// Read the data
|
||||
SocketChannel sc = (SocketChannel) key.channel();
|
||||
```
|
||||
## CharsetEncoder/CharsetDecoder
|
||||
通过`CharsetEncoder/CharsetDecoder`,可以进行`CharBuffer`和`ByteBuffer`之间的转化。
|
||||
|
||||
```java
|
||||
// 获取字符集
|
||||
Charset latin1 = Charset.forName("ISO-8859-1");
|
||||
|
||||
// 通过字符集创建encoder/decoder
|
||||
CharsetDecoder decoder = latin1.newDecoder();
|
||||
CharsetEncoder encoder = latin1.newEncoder();
|
||||
|
||||
CharBuffer cb = decoder.decode(inputData);
|
||||
|
||||
ByteBuffer outputData = encoder.encode(cb);
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
231
java se/反射.md
Normal file
231
java se/反射.md
Normal file
@@ -0,0 +1,231 @@
|
||||
- [反射](#反射)
|
||||
- [Class对象](#class对象)
|
||||
- [获取Class对象方法](#获取class对象方法)
|
||||
- [调用实例的getClass方法](#调用实例的getclass方法)
|
||||
- [调用Class类的静态方法forName方法](#调用class类的静态方法forname方法)
|
||||
- [通过T.class获取](#通过tclass获取)
|
||||
- [Class对象的比较](#class对象的比较)
|
||||
- [通过Class对象创建新实例](#通过class对象创建新实例)
|
||||
- [使用反射分析类](#使用反射分析类)
|
||||
- [Field.getType](#fieldgettype)
|
||||
- [获取类的域、方法和构造器](#获取类的域方法和构造器)
|
||||
- [Class.getFields](#classgetfields)
|
||||
- [Class.getDeclaredFields](#classgetdeclaredfields)
|
||||
- [Class.getMethods](#classgetmethods)
|
||||
- [Class.getDeclaredMethods](#classgetdeclaredmethods)
|
||||
- [Class.getConstructors](#classgetconstructors)
|
||||
- [Class.getDeclaredConstructors](#classgetdeclaredconstructors)
|
||||
- [Method、Field、Constructor中的方法](#methodfieldconstructor中的方法)
|
||||
- [getDeclaringClass](#getdeclaringclass)
|
||||
- [getExceptionTypes(Method和Constructor中包含)](#getexceptiontypesmethod和constructor中包含)
|
||||
- [getModifiers](#getmodifiers)
|
||||
- [getName](#getname)
|
||||
- [getParameterTypes(Method和Constructor中包含)](#getparametertypesmethod和constructor中包含)
|
||||
- [getReturnType(只在Method方法中包含)](#getreturntype只在method方法中包含)
|
||||
- [Modifier中的方法](#modifier中的方法)
|
||||
- [toString](#tostring)
|
||||
- [isAbstract](#isabstract)
|
||||
- [isFinal](#isfinal)
|
||||
- [isInterface](#isinterface)
|
||||
- [isNative](#isnative)
|
||||
- [isPrivate](#isprivate)
|
||||
- [isProtected](#isprotected)
|
||||
- [isPublic](#ispublic)
|
||||
- [isStatic](#isstatic)
|
||||
- [isSynchronized](#issynchronized)
|
||||
- [isVolatile](#isvolatile)
|
||||
- [使用反射分析类的内容](#使用反射分析类的内容)
|
||||
- [AccessibleObject](#accessibleobject)
|
||||
- [setAccessible](#setaccessible)
|
||||
- [isAccessible](#isaccessible)
|
||||
- [使用反射调用方法](#使用反射调用方法)
|
||||
- [Method.getMethod](#methodgetmethod)
|
||||
|
||||
|
||||
# 反射
|
||||
## Class对象
|
||||
Class对象用于访问类信息,想要获取class对象,可以通过如下方法
|
||||
### 获取Class对象方法
|
||||
#### 调用实例的getClass方法
|
||||
```java
|
||||
Random generator = new Random():
|
||||
Class cl = generator . getClass () ;
|
||||
String name = cl.getName() ; // name is set to "java . util . Random"
|
||||
```
|
||||
#### 调用Class类的静态方法forName方法
|
||||
```java
|
||||
String dassName = " java . util . Random" ;
|
||||
Class cl = Class.forName ( dassName ) ;
|
||||
```
|
||||
|
||||
#### 通过T.class获取
|
||||
如果T是一个java类型,那么T.class代表T类型对应的Class对象
|
||||
```java
|
||||
Class dl = Random.class;
|
||||
Gass cl 2 = int.class ;
|
||||
Class cl 3 = Double[].class ;
|
||||
```
|
||||
Class对象表示的是一个类型,而类型不一定是类,也包含基本类型,如int,long等。
|
||||
|
||||
### Class对象的比较
|
||||
JVM为每个类型只维护一个Class对象,故而不同的Class之间可以通过`==`符号进行比较
|
||||
```java
|
||||
if(e.getClass()==Employee.class)
|
||||
```
|
||||
### 通过Class对象创建新实例
|
||||
下述示例创建了一个与e具有相同类型的实例,newInstance方法默认会调用默认构造函数,如果该类型没有默认构造函数,会抛出一个checked exception
|
||||
```java
|
||||
e.getClass().newInstance()
|
||||
```
|
||||
如果将forName和newInstance方法结合起来使用,可以根据存储在字符串中的类型动态创建一个对象:
|
||||
```java
|
||||
String driverClassName = "com.mysql.cj.jdbc.Driver";
|
||||
Class.forName(driverClassName).newInstance();
|
||||
```
|
||||
|
||||
## 使用反射分析类
|
||||
java.lang.reflect类中含有三个类:
|
||||
- Field:用于描述类的域
|
||||
- Method:用于描述类的方法
|
||||
- Constructor:用于描述类的构造器
|
||||
|
||||
### Field.getType
|
||||
Field类有一个getType方法,返回值是域所属类型的Class对象。
|
||||
### 获取类的域、方法和构造器
|
||||
#### Class.getFields
|
||||
返回值为类的public域,包含从父类继承的public域
|
||||
#### Class.getDeclaredFields
|
||||
返回类中声明的所有域,包含为private和protected的域,但是不包含从父类继承的域
|
||||
#### Class.getMethods
|
||||
返回值为类提供的public方法,包含从父类继承的public方法
|
||||
#### Class.getDeclaredMethods
|
||||
返回类中声明的所有方法,包含private方法和protected方法,但是不包含从父类继承的方法
|
||||
#### Class.getConstructors
|
||||
只能获取到public构造器
|
||||
#### Class.getDeclaredConstructors
|
||||
能获取到所有的构造器,包含private和protected构造器
|
||||
### Method、Field、Constructor中的方法
|
||||
#### getDeclaringClass
|
||||
返回定义该方法、域、构造器类型的Class对象
|
||||
```java
|
||||
Class getDeclaringClass()
|
||||
```
|
||||
#### getExceptionTypes(Method和Constructor中包含)
|
||||
返回方法抛出的异常类型的数组
|
||||
```java
|
||||
Class[] getExceptionTypes()
|
||||
```
|
||||
#### getModifiers
|
||||
返回一个描述Field、Method、Constrcutor的修饰符的整数类型(不同的位描述不同的修饰符状态,如public、static、是否抽象类、是否接口等)
|
||||
```java
|
||||
int getModifiers()
|
||||
```
|
||||
#### getName
|
||||
返回描述Field、Constructor、Method的字符串
|
||||
```java
|
||||
String getName()
|
||||
```
|
||||
#### getParameterTypes(Method和Constructor中包含)
|
||||
返回一个Class数组,用于描述参数类型
|
||||
```java
|
||||
Class[] getParameterTypes()
|
||||
```
|
||||
#### getReturnType(只在Method方法中包含)
|
||||
返回Method的返回类型
|
||||
```java
|
||||
Class getReturnType()
|
||||
```
|
||||
### Modifier中的方法
|
||||
#### toString
|
||||
该静态方法将modifiers整数中的修饰符内容转化为字符串
|
||||
```java
|
||||
static String toString(int modifiers)
|
||||
```
|
||||
#### isAbstract
|
||||
```java
|
||||
static boolean isAbstract(int modifiers)
|
||||
```
|
||||
#### isFinal
|
||||
```java
|
||||
static boolean isFinal(int modifiers)
|
||||
```
|
||||
#### isInterface
|
||||
```java
|
||||
static boolean isInterface(int modifiers)
|
||||
```
|
||||
#### isNative
|
||||
```java
|
||||
static boolean isNative(int modifiers)
|
||||
```
|
||||
#### isPrivate
|
||||
```java
|
||||
static boolean isPrivate(int modifiers)
|
||||
```
|
||||
#### isProtected
|
||||
```java
|
||||
static boolean isProtected(int modifiers)
|
||||
```
|
||||
#### isPublic
|
||||
```java
|
||||
static boolean isPublic(int modifiers)
|
||||
```
|
||||
#### isStatic
|
||||
```java
|
||||
static boolean isStatic(int modifiers)
|
||||
```
|
||||
#### isSynchronized
|
||||
```java
|
||||
static boolean isSynchronized(int modifiers)
|
||||
```
|
||||
#### isVolatile
|
||||
```java
|
||||
static boolean isVolatile(int modifiers)
|
||||
```
|
||||
## 使用反射分析类的内容
|
||||
如果在运行时有一个Field对象和一个类实例obj,想要查看obj实例中Field对应域的值,可以使用如下方式:
|
||||
```java
|
||||
f.get(obj);
|
||||
```
|
||||
但是,如果Field的modifiers显示该Field是private的,那么`f.get(obj)`方法将会抛出一个IllegalAccessException。
|
||||
此时,可以通过`f.setAccessible(true)`来访问private域中的内容。
|
||||
```java
|
||||
f.setAccessible(true);
|
||||
Object v = f.get(obj);
|
||||
```
|
||||
设置Field的值则是可以通过`f.set(obj,value)`来实现。
|
||||
```java
|
||||
f.setAccessible(true);
|
||||
f.setValue(obj,"shiro");
|
||||
```
|
||||
### AccessibleObject
|
||||
Field,Method,Constructor类型都拥有公共的父类AccessibleObject
|
||||
#### setAccessible
|
||||
```java
|
||||
void setAccessible(boolean flag);
|
||||
|
||||
// 批量设置可访问状态
|
||||
static void setAccessible(AccessibleObject[] array,boolean flag);
|
||||
```
|
||||
#### isAccessible
|
||||
```java
|
||||
boolean isAccessible();
|
||||
```
|
||||
## 使用反射调用方法
|
||||
Method类中提供了一个invoke方法来调用Method所表示的方法,`Method.invoke`方法签名如下:
|
||||
```java
|
||||
Object invoke(Object obj,Object... args)
|
||||
```
|
||||
obj表示调用方法的对象,args表示参数数组。
|
||||
> 如果Method位静态方法,那么obj应该传入null
|
||||
|
||||
如果Method代表的方法返回的是一个基本类型,那么invoke方法会返回器包装类型,如`int->Integer`.
|
||||
### Method.getMethod
|
||||
如果想要根据方法名称和参数类型来获取Class对象中的Method,可以调用如下方法:
|
||||
```java
|
||||
Method getMethod(String name,Class... parameterTypes)
|
||||
```
|
||||
示例如下:
|
||||
```java
|
||||
Method ml = Employee. class.getMethod ( "getName") ;
|
||||
Method m2 = Employee.class.getMethod ( "raiseSalary" , double.class) ;
|
||||
```
|
||||
109
java se/垃圾回收和引用.md
Normal file
109
java se/垃圾回收和引用.md
Normal file
@@ -0,0 +1,109 @@
|
||||
- [垃圾回收和引用](#垃圾回收和引用)
|
||||
- [finalize](#finalize)
|
||||
- [引用](#引用)
|
||||
- [Reference](#reference)
|
||||
- [get](#get)
|
||||
- [clear](#clear)
|
||||
- [enqueue](#enqueue)
|
||||
- [isEnqueue](#isenqueue)
|
||||
- [引用的可达性](#引用的可达性)
|
||||
- [强可达(strongly reachable)](#强可达strongly-reachable)
|
||||
- [软可达(softly reachable)](#软可达softly-reachable)
|
||||
- [弱可达(weakly reachable)](#弱可达weakly-reachable)
|
||||
- [终结器可达(finalizer reachable)](#终结器可达finalizer-reachable)
|
||||
- [幽灵可达(phantom reachable)](#幽灵可达phantom-reachable)
|
||||
- [引用类型](#引用类型)
|
||||
- [软引用(SoftReference)](#软引用softreference)
|
||||
- [弱引用(WeakReference)](#弱引用weakreference)
|
||||
- [幽灵可达](#幽灵可达)
|
||||
- [Weak Hash Map](#weak-hash-map)
|
||||
- [引用队列](#引用队列)
|
||||
- [ReferenceQueue.poll](#referencequeuepoll)
|
||||
- [ReferenceQueue.remove](#referencequeueremove)
|
||||
- [弱引用和软引用的使用](#弱引用和软引用的使用)
|
||||
|
||||
# 垃圾回收和引用
|
||||
## finalize
|
||||
finalize方法将会在对象的空间被回收之前被调用。如果一个对象被垃圾回收器判为不可达而需要被回收时,垃圾回收器将调用该对象的finalize方法,通过finalize方法可以清除对象的一些非内存资源。
|
||||
在每个对象中,finalize方法最多被调用一次。
|
||||
> finalize方法可以抛出任何异常,但是抛出的异常将被垃圾回收器忽略。
|
||||
|
||||
在finalize方法调用时,该对象引用的其他对象可能也是垃圾对象,并且已经被回收。
|
||||
## 引用
|
||||
### Reference
|
||||
Reference是一个抽象类,是所有特定引用类的父类。
|
||||
#### get
|
||||
```java
|
||||
public Object get()
|
||||
```
|
||||
将会返回引用对象指向的被引用对象
|
||||
#### clear
|
||||
```java
|
||||
public void clear()
|
||||
```
|
||||
清空引用对象从而使其不指向任何对象
|
||||
#### enqueue
|
||||
```java
|
||||
public boolean enqueue()
|
||||
```
|
||||
如果存在引用对象,将引用对象加入到注册的引用队列中,如果加入队列成功,返回true;如果没有成功加入到队列,或者该引用对象已经在队列中,那么返回false
|
||||
#### isEnqueue
|
||||
```java
|
||||
public boolean isEnqueue()
|
||||
```
|
||||
如果该引用对象已经被加入到引用队列中,那么返回true,否则返回false
|
||||
### 引用的可达性
|
||||
#### 强可达(strongly reachable)
|
||||
至少通过一条强引用链可达
|
||||
#### 软可达(softly reachable)
|
||||
不是强可达,但是至少可以通过一条包含软引用的引用链可达
|
||||
#### 弱可达(weakly reachable)
|
||||
不是弱可达,但是至少通过一条包含弱引用的引用链可达
|
||||
#### 终结器可达(finalizer reachable)
|
||||
不是弱可达,但是该对象的finalize方法尚未执行
|
||||
#### 幽灵可达(phantom reachable)
|
||||
如果finalize方法已经执行,但是可通过至少一条包含幽灵引用的引用链可达
|
||||
### 引用类型
|
||||
#### 软引用(SoftReference)
|
||||
对于软可达的对象,垃圾回收程序会随意进行处置,如果可用内存很低,回收器会清空SoftReference对象中的引用,之后该被引用对象则能被回收
|
||||
> 在抛出OOM之前,所有的SoftReference引用将会被清空
|
||||
|
||||
#### 弱引用(WeakReference)
|
||||
弱可达对象将会被垃圾回收器回收,如果垃圾回收器认为对象是弱可达的,所有指向其的WeakReference对象都会被清空
|
||||
|
||||
#### 幽灵可达
|
||||
幽灵可达并不是真正的可达,虚引用并不会影响对象的生命周期,如果一个对象和虚引用关联,则该对象跟没有与该虚引用关联一样,在任何时候都有可能被垃圾回收。虚引用主要用于跟踪对象垃圾回收的活动。
|
||||
|
||||
### Weak Hash Map
|
||||
WeakHashMap会使用WeakReference来存储key。
|
||||
如果垃圾回收器发现一个对象弱可达时,会将弱引用放入到引用队列中。WeakHashMap会定期检查引用队列中新到达的弱引用,并且新到的弱引用代表该key不再被使用,WeakHashMap会移除关联的entry。
|
||||
|
||||
|
||||
### 引用队列
|
||||
如果对象的可达性状态发生了改变,那么执行该对象的引用类型将会被放置到引用队列中。引用队列通常被垃圾回收器使用。
|
||||
也可以在自己的代码中对引用队列进行使用,通过引用队列,可以监听对象的可达性改变。
|
||||
> 例如当对象不再被强引用,变为弱可达时,引用将会被添加到引用队列中,再通过代码监听引用队列的变化,即可监听到对象可达性的变化
|
||||
|
||||
> ReferenceQueue是线程安全的。
|
||||
|
||||
#### ReferenceQueue.poll
|
||||
```java
|
||||
pulbic Reference poll()
|
||||
```
|
||||
该方法会删除并且返回队列中的下一个引用对象,若队列为空,则返回值为null
|
||||
|
||||
#### ReferenceQueue.remove
|
||||
```java
|
||||
public Reference remove() throws InterruptedException
|
||||
```
|
||||
该方法同样会删除并返回队列中下一个引用对象,但是该方法在队列为空时会无限阻塞下去
|
||||
```java
|
||||
public Reference remove(long timeout) throws InterruptedException
|
||||
```
|
||||
|
||||
引用对象在构造时,会和特定的引用队列相关联,当被引用对象的可达性状态发生变化时,被添加到引用队列中。在被添加到队列之前,引用对象都已经被清空。
|
||||
|
||||
### 弱引用和软引用的使用
|
||||
弱引用和软引用都提供两种类型的构造函数,
|
||||
- 只接受被引用对象,并不将引用注册到引用队列
|
||||
- 既接受被引用对象,还将引用注册到指定的引用队列,然后可以通过检查引用队列中的情况来监听被引用对象可达状态变更
|
||||
423
jvm/深入理解java虚拟机.md
Normal file
423
jvm/深入理解java虚拟机.md
Normal file
@@ -0,0 +1,423 @@
|
||||
- [深入理解java虚拟机](#深入理解java虚拟机)
|
||||
- [Java内存区域和内存溢出异常](#java内存区域和内存溢出异常)
|
||||
- [运行时数据区](#运行时数据区)
|
||||
- [程序计数器](#程序计数器)
|
||||
- [Java虚拟机栈(Java Virutal Machine Stack)](#java虚拟机栈java-virutal-machine-stack)
|
||||
- [局部变量](#局部变量)
|
||||
- [本地方法栈](#本地方法栈)
|
||||
- [Java堆](#java堆)
|
||||
- [方法区](#方法区)
|
||||
- [运行时常量池](#运行时常量池)
|
||||
- [直接内存](#直接内存)
|
||||
- [Hotspot虚拟机对象探秘](#hotspot虚拟机对象探秘)
|
||||
- [对象创建](#对象创建)
|
||||
- [类加载](#类加载)
|
||||
- [内存分配方式](#内存分配方式)
|
||||
- [TLAB](#tlab)
|
||||
- [初始化](#初始化)
|
||||
- [对象布局](#对象布局)
|
||||
- [对象头](#对象头)
|
||||
- [实例数据](#实例数据)
|
||||
- [对齐填充](#对齐填充)
|
||||
- [对象的访问定位](#对象的访问定位)
|
||||
- [垃圾收集器和内存分配策略](#垃圾收集器和内存分配策略)
|
||||
- [对象是否应当被回收](#对象是否应当被回收)
|
||||
- [引用计数算法](#引用计数算法)
|
||||
- [可达性分析算法](#可达性分析算法)
|
||||
- [引用类型](#引用类型)
|
||||
- [垃圾回收细节](#垃圾回收细节)
|
||||
- [回收方法区](#回收方法区)
|
||||
- [判断类是否可回收](#判断类是否可回收)
|
||||
- [垃圾收集算法](#垃圾收集算法)
|
||||
- [标记-清除算法](#标记-清除算法)
|
||||
- [复制算法](#复制算法)
|
||||
- [基于复制算法的虚拟机新生代内存划分](#基于复制算法的虚拟机新生代内存划分)
|
||||
- [标记-整理算法](#标记-整理算法)
|
||||
- [分代收集算法](#分代收集算法)
|
||||
- [Hotspot算法实现](#hotspot算法实现)
|
||||
- [枚举根节点](#枚举根节点)
|
||||
- [OopMap](#oopmap)
|
||||
- [安全点](#安全点)
|
||||
- [安全区域](#安全区域)
|
||||
- [垃圾收集器](#垃圾收集器)
|
||||
- [Serial收集器](#serial收集器)
|
||||
- [ParNew](#parnew)
|
||||
- [Parallel Scavenge](#parallel-scavenge)
|
||||
- [MaxGCPauseMillis](#maxgcpausemillis)
|
||||
- [GCTimeRatio](#gctimeratio)
|
||||
- [Serial Old](#serial-old)
|
||||
- [Parallel Old](#parallel-old)
|
||||
- [CMS收集器](#cms收集器)
|
||||
- [CMS缺陷](#cms缺陷)
|
||||
|
||||
|
||||
# 深入理解java虚拟机
|
||||
## Java内存区域和内存溢出异常
|
||||
### 运行时数据区
|
||||
java程序在虚拟机运行时会将其所管理的内存区域划分为若干个不同的数据区。java运行时数据区的结构如下:
|
||||
|
||||
<img src="https://i-blog.csdnimg.cn/blog_migrate/858b0aecc9d90cdb0dea25ea077c11c4.jpeg#pic_center" alt="在这里插入图片描述">
|
||||
|
||||
|
||||
#### 程序计数器
|
||||
程序计数器是一块较小的内存空间,其可以被看做是当前线程所执行字节码的行号指示器。`在虚拟机的概念模型中,字节码解释器在工作时会修改程序计数器的值,从而选取下一条需要执行的字节码指令`。分支、循环、跳转、异常处理、线程恢复等基础功能都依赖计数器来完成。
|
||||
|
||||
在java虚拟机中,多线程是通过`线程轮流切换,并分配处理器的执行时间片`来实现的。故而,在任一给定时刻,一个处理器只会执行一个线程中的指令(即给定时刻一个处理器只和一个线程进行绑定)。为了线程切换后,仍然能恢复到线程上次执行的位置,`每个线程都会有一个独立的程序计数器`。各个线程之间的程序计数器互不干扰,独立存储,该类内存为`线程私有`的内存。
|
||||
|
||||
> 由于JVM支持通过`本地方法`来调用c++/c等其他语言的native方法,故而:
|
||||
> - 如果线程正在执行的是java方法,计数器记录的是正在执行的虚拟字节码指令地址
|
||||
> - 如果线程正在执行的是native方法,计数器的值为空
|
||||
|
||||
#### Java虚拟机栈(Java Virutal Machine Stack)
|
||||
和程序计数器一样,java虚拟机栈也是线程私有的,其声明周期和线程相同。
|
||||
|
||||
虚拟机栈描述了java方法执行时的内存模型:
|
||||
- 每个方法在执行时都会创建一个栈帧,用于存储局部变量、操作数栈、动态链接、方法出口等信息
|
||||
- 每个方法从调用直至执行完成的过程,对应一个栈帧在虚拟机栈中入栈、出栈的过程
|
||||
|
||||
##### 局部变量
|
||||
局部变量中存放了编译器可知的各种数据类型,如下:
|
||||
- 基本数据类型(primitives)
|
||||
- 对象引用
|
||||
- returnAddress类型(指向一条字节码指令的地址)
|
||||
|
||||
> 其中,64位长度的long和double数据类型会占用2个局部变量空间,其余数据类型只占用一个。
|
||||
>
|
||||
> 局部变量表所需的空间大小在编译时就已经确定,在运行时进入一个方法时,该方法需要的局部变量表大小就已经确定,并且`方法执行过程中不会改变局部变量表的大小`
|
||||
|
||||
在java虚拟机规范中,针对该区域定义了两种异常状况:
|
||||
- `StackOverflowError`:线程所请求的栈深度大于虚拟机所允许的栈深度,将抛出StackOverflowError
|
||||
- `OutOfMemoryError`:如果虚拟机栈可以动态拓展,且拓展时无法申请到足够的内存,将会抛出`OutOfMemoryError`
|
||||
|
||||
#### 本地方法栈
|
||||
本地方法栈和虚拟机栈的作用类似,区别如下:
|
||||
- 虚拟机栈为虚拟机执行Java方法服务
|
||||
- 本地方法栈为虚拟机用到的Native方法服务
|
||||
|
||||
和虚拟机栈相同,本地方法栈也会抛出OutOfMemoryError和StackOverflowError
|
||||
|
||||
#### Java堆
|
||||
Java堆是java虚拟机所管理内存中最大的部分,`java堆为所有线程所共享,在虚拟机创建时启动`。java堆内存用于存放对象实例,几乎所有的对象实例堆在该处分配内存。
|
||||
|
||||
Java堆是垃圾收集器管理的主要区域,在很多时候也被称为`GC堆`。从垃圾收集的角度看,现代收集器基本都采用分代收集的算法,故而java堆内存还可以进一步细分为`新生代和老年代`。
|
||||
|
||||
虽然Java堆内存为线程所共享的区域,但从内存分配的角度看,Java堆中可能划分出多个线程私有的分配缓冲区(Thead Local Allocation Buffer,TLAB)。
|
||||
|
||||
但是,不论Java堆内存如何划分,其存放的内容都为对象实例。Java堆内存可以处于物理上不连续的内存空间中,只需在逻辑上连续即可。通常,java堆内存都是可拓展的(通过-Xms和-Xmx控制),如果在为实例分配内存时,没有足够的内存空间进行分配,并且堆也无法再进行拓展时,将会抛出`OutOfMemoryError`。
|
||||
|
||||
#### 方法区
|
||||
方法区和java堆一样,是各个线程共享的区域,其用于存储已被虚拟机加载的类信息、常量、静态变量、JIT编译后的代码等数据。
|
||||
|
||||
jvm规范对方法区的限制比较宽松,方法区的内存并不需要在物理上连续,并可选择固定大小或可拓展。除此之外,方法区还可以选择不实现垃圾回收。
|
||||
|
||||
通常,发生在方法区的垃圾回收比较罕见,对方法区的垃圾回收主要是针对常量池的回收和对类型的卸载。类型卸载触发的条件比较苛刻。
|
||||
|
||||
当方法区无法满足内存分配的需求时,将抛出OutOfMemoryError。
|
||||
|
||||
##### 运行时常量池
|
||||
运行时常量池是方法区的一部分。在class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放在编译期生成的各种字面量和符号引用。
|
||||
|
||||
`该部分内存将会在类加载后,进入方法区的运行时常量池进行存放`。
|
||||
|
||||
> 在java程序运行时,也能将新的常量放入池中,例如`String.intern()`方法。
|
||||
|
||||
#### 直接内存
|
||||
直接内存不是虚拟机运行时数据区的一部分,也不是java虚拟机规范中定义的内存区域,但该部分内存也会被频繁使用,并且可能导致OutOfMemoryError。
|
||||
|
||||
在JDK 1.4中,引入了NIO,其可以使用native函数库直接分配堆外内存,然后通过一个存储在Java堆内的DirectByteBuffer对象操作堆外内存。这样能够在部分场景中显著提高性能,避免在java堆和native堆中来回复制数据。
|
||||
|
||||
并且,直接内存并不会受java堆大小的限制,但仍然会受物理机物理内存大小的限制。
|
||||
|
||||
|
||||
### Hotspot虚拟机对象探秘
|
||||
#### 对象创建
|
||||
##### 类加载
|
||||
虚拟机在遇到new指令时,首先会去方法区常量池中进行匹配,检查是否有对应的符号引用,并检查该符号引用对应的类是否已经被加载、解析、初始化。如果尚未被加载,则先执行该类的加载过程。
|
||||
|
||||
在加载该类后,对象所需的内存大小便可完全确定,会为该对象的创建分配内存空间。
|
||||
|
||||
##### 内存分配方式
|
||||
在java堆内存中的内存分配存在如下几种方式:
|
||||
- 指针碰撞: 如果Java堆内存中内存是绝对规整的,所有用过的指针都放在一边,空闲内存放在另一边,那么可以通过一个中间的指针来划分使用的空间和空闲空间
|
||||
- 当分配内存空间时,仅需将指针向空闲空间的那个方向移动即可
|
||||
- FreeList:如果Java中的堆内存并不规整,已使用的内存空间和空闲内存空间相互交错,此时并无法使用指针碰撞的方式来分配内存。虚拟机会维护一个列表,列表中维护空闲的内存块。
|
||||
- 在内存分配时,会从列表中找到一块足够大的空间划分给对象,并更新列表上的空闲内存记录
|
||||
|
||||
选择采用哪种内存分配方式取决于java堆内存是否规整,而java堆是否规整则取决于采用的垃圾收集器是否带有`压缩整理`功能。
|
||||
|
||||
> 故而,在使用Serial、ParNew等带Compact过程的收集器时,系统采用的分配算法是指针碰撞;而使用CMS和Mark-Sweep算法的收集器时,通常采用FreeList
|
||||
|
||||
##### TLAB
|
||||
对象创建是非常频繁的行为,多线程场景下容易出现并发安全的问题。即使使用指针碰撞的方式来分配内存空间,也会出现争用问题。解决内存分配下的并发问题存在两种方案:
|
||||
- 对分配内存空间的操作进行同步,可以采用CAS等方案保证分配内存动作的原子性
|
||||
- 将内存分配的动作按照线程的不同划分在不同区域中,即每个线程先在java堆内存中预先分配一小块内存,称为TLAB,每个线程都在自身的TLAB中分配内存空间
|
||||
- 只有当线程的TLAB使用完并需要分配新的TLAB时,才需要同步锁定操作,这样能减少同步锁定这一耗时操作的次数
|
||||
|
||||
虚拟机是否采用TLAB,可以通过`-XX:+/-UseTLAB`参数来设定。
|
||||
|
||||
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为0,若使用TLAB,则该操作可提前到分配TLAB时。该操作可以保证对象的实例字段在不赋值初始值时就能使用,程序能够访问这些字段数据类型所对应的零值。
|
||||
|
||||
##### 初始化
|
||||
在为对象分配完空间后,会执行`<init>`方法进行初始化。
|
||||
|
||||
#### 对象布局
|
||||
在Hotspot虚拟机中,对象在内存中的布局可以分为3块区域:
|
||||
- 对象头(Header)
|
||||
- 实例数据(Instance Data)
|
||||
- 对齐填充(Padding)
|
||||
|
||||
##### 对象头
|
||||
对象头中包含两部分信息:
|
||||
- 对象的运行时数据,如hash code、gc分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等
|
||||
- 该部分在32bit和64bit的虚拟机中,长度分别为32bit和64bit,被官方称为`MarkWord`
|
||||
- 类型指针:该部分信息指向对象对应`类元数据`的指针,虚拟机会通过该指针来确定对象是哪个类的实例。
|
||||
- 数组长度:如果对象是一个java数组,那么对象头中还需要记录数组的长度
|
||||
|
||||
##### 实例数据
|
||||
实例数据为对象真正存储的有效信息,即程序代码中定义的各种类型字段内容,无论是从父类继承的还是子类中定义的。
|
||||
|
||||
实例数据中的存储顺序会收到虚拟机分配策略参数和字段在java源码中定义顺序的影响。Hotspot虚拟机默认的分配策略为longs/doubles, ints, shorts/chars, bytes/booleans、oops(Ordinary Object Pointers),相同宽度的字段会被分配到一起。
|
||||
|
||||
##### 对齐填充
|
||||
对齐填充并不是自然存在的,仅起占位符的作用。hotsopt虚拟机自动内存管理系统要求对象起始地址必须是8字节的整数倍,即对象大小必须为8字节的整数倍。
|
||||
|
||||
对象头刚好是8字节整数倍,故而当对象的实例数据没有对对齐,需要通过对象填充来对齐。
|
||||
|
||||
#### 对象的访问定位
|
||||
java程序需要通过虚拟机栈上的reference来操作java堆中具体的对象。目前,访问堆上对象有两种主流的方式:
|
||||
- 句柄访问:java堆中会划分一块区域来作为句柄池,references存储的地址即为对象句柄的地址,句柄中则包含了对象实例数据地址和对象类型数据地址
|
||||
- 使用句柄访问时,reference中存储的是句柄地址,即使对象被移动(在垃圾回收时非常普遍),也只需要修改句柄中的实例数据指针,不需要修改虚拟机栈中的reference
|
||||
|
||||
<img width="825" height="477" src="https://img2024.cnblogs.com/blog/2123988/202406/2123988-20240609154659695-36774084.png" style="display: block; margin-left: auto; margin-right: auto">
|
||||
|
||||
- 直接指针访问:采用直接指针访问时,堆内存中无需维护句柄池,而java的堆对象中则包含类型数据的地址。reference中直接会存储java堆对象的地址,而可以通过堆对象中存储的类型数据地址再次访问类型数据
|
||||
- 使用直接指针访问时,访问速度更改,在访问实例数据时能够节省一次内存查找的开销。
|
||||
|
||||
<a data-fancybox="gallery" href="https://img2024.cnblogs.com/blog/2123988/202406/2123988-20240609154731546-1183511045.png"><img width="808" height="500" src="https://img2024.cnblogs.com/blog/2123988/202406/2123988-20240609154731546-1183511045.png" style="display: block; margin-left: auto; margin-right: auto"></a>
|
||||
|
||||
在Hotspot实现中,采用的是直接指针访问方案。
|
||||
|
||||
## 垃圾收集器和内存分配策略
|
||||
在上一章节中提到的java运行时数据区域中,java虚拟机栈、本地方法栈、程序计数器三个区域是线程私有的,生命周期和线程一致,无需考虑垃圾回收问题,在方法结束/线程终止时内存会随之回收。
|
||||
|
||||
但是,`方法区`和`java堆内存`这两个区域则不同,内存的分配和回收都是动态的,垃圾收集器主要关注这部分内存。
|
||||
|
||||
### 对象是否应当被回收
|
||||
堆中几乎存放着所有的java对象,垃圾收集器在对对内存进行回收前,需要确定哪些对象可以被回收,哪些对象不能被回收。
|
||||
|
||||
#### 引用计数算法
|
||||
该算法会为对象添加一个引用计数器,每当有一个地方引用该对象时,计数器加一;当引用失效时,计数器则减一;技术器为0代表该对象不再被引用。
|
||||
|
||||
但是,java虚拟机并没有采用引用计数算法来管理内存,主要原因是其难以解决对象之间循环引用的问题。
|
||||
|
||||
#### 可达性分析算法
|
||||
在主流的商用程序语言(JAVA/C#等)的主流实现中,都通过可达性分析来判定对象是否可以被回收的。该算法核心是`根据一系列被称为GC Roots的对象来作为起点,从这些节点开始向下搜索,搜索时所走过的路径被称为引用链,当一个对象GC Roots没有任何的引用链相连接时,即从GC Roots到该对象不可达时,证明该对象是不可用的,可以被回收`。
|
||||
|
||||
在Java语言中,GC Roots包含如下内容:
|
||||
- 虚拟机栈中所引用的对象
|
||||
- 方法区中静态属性应用的对象
|
||||
- 方法区中常量所引用的对象
|
||||
- 本地方法栈中JNI所引用的对象
|
||||
|
||||
### 引用类型
|
||||
在JDK 1.2之后,Java对引用的概念进行了扩充,引入了如下的引用类型概念:
|
||||
- 强引用(Strong Reference)
|
||||
- 软引用(Soft Reference)
|
||||
- 弱引用(Weak Reference)
|
||||
- 虚引用(Phantom Reference)
|
||||
|
||||
上述四种引用类型,引用强度逐级递减:
|
||||
- `强引用`:对于强引用类型,只要强引用还存在,垃圾收集器永远不会回收掉被引用对象
|
||||
- `软引用`:软引用用于描述一些有用但非必须的对象。对于软引用关联着的对象,在系统即将要发生内存溢出的异常之前,会将这些引用列进回收范围之内进行二次回收。如果此次回收之后还没有足够的内存,才会抛出内存溢出异常。
|
||||
- 即在抛出OOM之前,SoftReference会被清空,如果清空后内存仍然不够,就会实际抛出OOM
|
||||
- `弱引用`:弱引用用于描述非必须对象,但其强度比软引用更弱一些。被弱引用关联的对象只能生存到下一次垃圾收集发生之前。
|
||||
- 在垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象
|
||||
- `虚引用`:虚引用是最弱的引用关系,一个对象是有虚引用存在,完全不会对其生存时间造成影响,也无法通过虚引用获得一个对象实例。
|
||||
- 为对象设置虚引用关联的唯一目的是能在该对象被收集器回收时收到一个系统通知
|
||||
|
||||
### 垃圾回收细节
|
||||
即使在可达性分析算法中被判定为不可达,对象也并非一定会被回收。要真正宣告对象的死亡,必须经历两次标记过程:
|
||||
- 如果在可达性分析后发现GC Roots到该对象不可达,其将会被第一次标记并进行一次筛选,筛选条件是对该对象是否有必要执行finalize方法:
|
||||
- 如果当前对象没有覆盖finalize方法,或是finalize方法已经被虚拟机调用过,则虚拟机将其视为没有必要执行
|
||||
- 如果当前对象被判定为需要执行finalize方法,那么该对象将会被放置到F-QUEUE的队列中,并且稍后由一个`由虚拟机自动建立的、优先级较低的Finalizer线程`去执行。该执行只是触发该方法,并不承诺会等待其执行结束。
|
||||
- finalize方法是对象逃脱死亡命运的最后一次机会,稍后GC将会对F-QUEUE中的对象进行第二次小规模的标记,如果对象需要在finalize中避免自己被回收,只需将自身和引用链上任一对象建立关联即可(例如将自身赋值给引用链上任一对象的成员变量或类变量),那么在第二次标记时其将被移除即使回收的集合;如果此时对象仍未逃脱,其会被真正回收
|
||||
|
||||
即对象被GC第一次标记为不可达后,只能通过finalize方法来避免自己被回收,并且`finalize方法只会被调用一次`。需要覆盖finalize逻辑,在finalize方法中重新令自身变得可达,此时才能避免被垃圾回收。
|
||||
|
||||
> 如果一个对象已经调用过一次finalize,并且成功变为可达。那么其再次变为不可达后,将不会有再次调用finalize的机会,这次对象将会被回收。
|
||||
|
||||
通常,不推荐使用finalize方法。
|
||||
|
||||
### 回收方法区
|
||||
Java虚拟机规范并不强制要求对方法区进行垃圾回收,并且在方法区进行垃圾回收的性价比较低:
|
||||
- 在堆中,尤其在新生代中,常规应用进行一次垃圾收集一般可以回收70%~95%的空间,但方法区垃圾回收的效率远低于此
|
||||
|
||||
方法区的垃圾回收主要回收两部分内容:
|
||||
- 废弃常量
|
||||
- 无用类
|
||||
|
||||
例如,在回收字面量时,如果一个字符串“abc"已进入常量池,但是当前系统没有任何一个String对象引用常量池中“abc”常量,也没有其他地方引用该字面量,如果此时发生垃圾回收,该常量将会被系统清理出常量池。常量池中其他方法、接口、字段、类的符号引用也与此类似。
|
||||
|
||||
##### 判断类是否可回收
|
||||
判断类是否可回收的条件比较苛刻,必须同时满足如下条件:
|
||||
- 该类所有实例已经被回收,java堆中不存在任何该类的实例
|
||||
- 加载该类的classloader已经被回收
|
||||
- 该类对应的java.lang.Class对象没有在其他任何地方被引用,无法在其他任何地方通过反射访问该类的方法
|
||||
|
||||
在大量使用反射、动态代理、Cglib、动态生成jsp、动态生成osgi等频繁自定义ClassLoader的场景都需要虚拟机具备类卸载功能,已保证永久代不会溢出。
|
||||
|
||||
### 垃圾收集算法
|
||||
#### 标记-清除算法
|
||||
最基础的垃圾收集算法是`标记-清除`算法(Mark-Sweep),算法分为`标记`和`清除`两个阶段:
|
||||
- 首先,算法会标记出需要回收的对象
|
||||
- 对象在被认定为需要回收前,需要经历两次标记,具体细节在前文有描述
|
||||
- 在标记完成后,会对所有被标记对象进行统一回收
|
||||
|
||||
标记清除算法存在如下不足之处:
|
||||
- `效率问题`:标记和清除这两个过程的效率都不高
|
||||
- `空间问题`: 标记和清除之后会产生大量不连续的内存碎片,大量碎片可能导致后续需要分配大对象时无法找到足够的连续内存而不得不触发另一次垃圾收集动作
|
||||
|
||||
#### 复制算法
|
||||
为了解决效率问题,提出了一种`复制`算法。该算法将可用内存划分为大小相等的两块,每次只使用其中的一块,当本块内存用尽后,其会将存活对象复制到另一块上,并将已使用的内存空间一次性清理掉。这样,每次回收都是针对半区的,并且在分配内存时也不用考虑内存碎片的问题。
|
||||
|
||||
复制算法存在如下弊端:
|
||||
- 复制算法将可用内存划分为相同的两部分,每次只使用一半,该代价比较高昂
|
||||
|
||||
##### 基于复制算法的虚拟机新生代内存划分
|
||||
当前,商用虚拟机都采用该种方法来回收新生代。在新生代中,98%的对象生命周期都很短暂,完全不需要根据1:1来划分内存空间。`对于新生代内存空间,将其划分为了一块较大的Eden空间和两块较小的Survivor空间,每次只会使用Eden和其中一块Survivor。`
|
||||
|
||||
当发生垃圾回收时,会将Eden区和Survivor区中还存活的对象一次性复制到另一块Survivor。Hotspot虚拟机默认Eden区和两块Survivor区的大小比例为`8:1:1`,即每次新生代内存区域中可用内存为`90%`。当然,`并不能确保每次发生垃圾回收时,另一块Survivor区能够容纳Eden+Survivor中所有的存活对象`,故而,需要老年代来作为担保。
|
||||
|
||||
#### 标记-整理算法
|
||||
在采用复制算法时,如果存活对象较多,那么将会需要大量复制操作,这样会带来较高的性能开销,并且,如果不想带来50%的内存空间浪费,就需要额外的内存空间来进行担保,故而,在老年代采用复制算法来进行垃圾回收是不适当的。
|
||||
|
||||
基于老年代的特点,提出了`标记-整理`算法(Mark-Compact):
|
||||
- 标记过程仍然和`标记-清除`一样
|
||||
- 标记过程完成后,并不是直接对可回收对象进行清理,而是将存活对象都向一端移动,然后直接清理掉边界以外的内存
|
||||
|
||||
`标记-整理`算法相对于`标记-清除`算法,其能够避免产生内存碎片。
|
||||
|
||||
#### 分代收集算法
|
||||
目前,商业虚拟机的垃圾收集都采用`分代收集`的算法,这种算法将对象根据存活周期的不同,将内存划分为好几块。
|
||||
|
||||
一般,会将java堆内存划分为新生代和老年代,针对新生代对象的特点和老年代对象的特点采用不同且适当的垃圾回收算法:
|
||||
- 在新生代中,对象生命周期短暂,每次垃圾收集都有大量对象需要被回收,只有少部分对象才能存活,应采用复制算法
|
||||
- 在新生代,存活对象较少,复制的目标区的内存占比也可以划分的更少,并且较少存活对象带来的复制成本也较低
|
||||
- 在老年代,对象存活率更高,并且没有额外的内存空间来对齐进行担保,故而不应使用复制算法,而是应当采用`标记-清除`算法或`标记-整理`算法来进行回收
|
||||
|
||||
### Hotspot算法实现
|
||||
#### 枚举根节点
|
||||
在可达性分析中,需要通过GC Roots来查找引用链。可以作为GC Roots的节点有:
|
||||
- 全局性的引用
|
||||
- 常量
|
||||
- 类静态属性
|
||||
- 执行上下文
|
||||
- 栈帧中的本地变量表
|
||||
|
||||
目前,很多应用仅方法区就包含数百兆,如果需要逐个检查这方面引用,需要消耗较多时间。
|
||||
|
||||
可达性分析对执行时间的敏感还体现在GC停顿上,在执行可达性分析时,需要停顿所有的Java执行线程(STW),避免在进行可达性分析时对象之间的引用关系还在不断的变化。
|
||||
|
||||
在Hotspot实现中,采用OopMap的数据结构来实现该目的,在类加载完成时,Hotspot会计算出对象内什么位置是什么类型的数据,在JIT编译过程中也会在特定位置记录栈和寄存器中哪些位置是引用。
|
||||
|
||||
##### OopMap
|
||||
OopMap用于存储java stack中object referrence的位置。其主用于在java stack中查找GC roots,并且在堆中对象发生移动时,更新references。
|
||||
|
||||
OopMaps的种类示例如下:
|
||||
- `OopMaps for interpreted methods`: 这类OopMap是惰性计算的,仅当GC发生时才通过分析字节码流生成
|
||||
- `OopMaps for JIT-compiled methods`:这类OopMap在JIT编译时产生,并且和编译后的代码一起保存
|
||||
|
||||
#### 安全点
|
||||
在OopMap的帮助下,Hotspot可以快速完成GC Roots的枚举。但是,可能会导致OopMap内容变化的指令非常多,如果为每条指令都生成OopMap,将耗费大量额外空间。
|
||||
|
||||
故而,Hotspot引入了安全点的概念,只有到达安全点时,才能暂停并进行GC。在选定Safepoint时,选定既不能太少以让GC等待时间太长,也不能过于频繁。在选定安全点时,一般会选中`令程序长时间执行`的的指令,例如`方法调用、循环跳转、异常跳转`等。
|
||||
|
||||
对于Safepoint,需要考虑如何在GC发生时让所有线程都跑到最近的安全点上再停顿下来。这里有两种方案:
|
||||
- `抢先式中断`:在GC发生时,首先把所有线程全部中断,如果有线程中断的地方不在安全点上,就恢复线程,让其跑到安全点上
|
||||
- `主动式中断`:需要中断线程时,不直接对线程进行操作,只是会设置一个全局标志,线程在执行时主动轮询该标志,发现中断标志为true时自己中断挂起。
|
||||
- 轮询的时机和安全点是重合的
|
||||
|
||||
VM Thread会控制JVM的STW过程,其会设置全局safepoint标志,所有线程都会轮询该标志,并中断执行。当所有线程都中断后,才会实际发起GC操作。
|
||||
|
||||
#### 安全区域
|
||||
safepoint解决了线程执行时如何进入GC的问题,但是如果此时线程处于Sleep或Blocked状态,此时线程无法响应JVM的中断请求,jvm也不可能等待线程重新被分配cpu时间。
|
||||
|
||||
在上述描述的场景下,需要使用安全区域(Safe Region)。安全区域是指在一段代码片段中,引用关系不会发生变化。在这个区域中的任意位置开始GC都是安全的。可以把Safe Region看作是Safe Point的拓展版本。
|
||||
|
||||
当线程执行到SafeRegion时,首先标识自身已经进入Safe Region。此时,当这段时间里JVM要发起GC时,对于状态已经被标识为Safe Region的线程,不用进行处理。当线程要离开安全区域时,需要检查系统是否已经完成了根节点枚举(或整个GC过程),如果完成了,线程继续执行,否则其必须等待,直到收到可以安全离开SafeRegion的信号。
|
||||
|
||||
Safe Region可以用于解决线程在GC时陷入sleep或blocked状态的问题。
|
||||
|
||||
### 垃圾收集器
|
||||
Java中常见垃圾回收器搭配如下
|
||||
<img alt="" class="has" height="433" src="https://i-blog.csdnimg.cn/blog_migrate/e13d5c0dce925d52ee6109d9345a1256.png" width="630">
|
||||
|
||||
#### Serial收集器
|
||||
Serial是最基本、历史最悠久的收集器,负责新生代对象的回收。该收集器是单线程的,并且`在serial收集器进行回收时,必须暂停其他所有的工作线程`,直到serial收集器回收结束。
|
||||
|
||||
Serial搭配Serial Old的收集方式如下所示:
|
||||
<img src="https://pica.zhimg.com/v2-64275c9eccce584fb1154a2b681129fa_1440w.jpg" data-caption="" data-size="normal" data-rawwidth="1295" data-rawheight="645" data-original-token="v2-8519c4f1d766880d0804bd58b618f11c" class="origin_image zh-lightbox-thumb" width="1295" data-original="https://pica.zhimg.com/v2-64275c9eccce584fb1154a2b681129fa_r.jpg">
|
||||
|
||||
#### ParNew
|
||||
ParNew即是Serial的多线程版本,除了使用多线程进行垃圾收集外,其他行为都和Serial完全一样。
|
||||
|
||||
ParNew运行示意图如下所示:
|
||||
<img src="https://pica.zhimg.com/v2-8bed62efa3ec77b248d87219db794a44_1440w.jpg" data-caption="" data-size="normal" data-rawwidth="838" data-rawheight="643" data-original-token="v2-d0e912b9f73670a16b0a1d9bf36d0d55" class="origin_image zh-lightbox-thumb" width="838" data-original="https://pica.zhimg.com/v2-8bed62efa3ec77b248d87219db794a44_r.jpg">
|
||||
|
||||
#### Parallel Scavenge
|
||||
Parallel Scavenge是一个新生代的垃圾收集器,也采用了多线程并行的垃圾收集方式。
|
||||
|
||||
Parallel Scavenge收集器的特点是其关注点和其他收集器不同,CMS等收集器会尽可能缩短收集时STW的时间,而Parallel Scavenge的目标则是达到一个可控制的吞吐量。
|
||||
|
||||
> 此处,吞吐量指的是 `运行用户代码时间/(运行用户代码时间 + 垃圾收集时间)`,例如,虚拟机总共运行100分钟,垃圾收集花掉1分钟,那么吞吐量就为99%
|
||||
|
||||
- 对于吞吐量较高的应用,则可以更高效的利用cpu时间,更快完成运算任务,主要适合在后台运行且不需要太多交互的任务
|
||||
- 而对于和用户交互的应用,STW时间越短则相应时间越短,用户交互体验越好
|
||||
|
||||
##### MaxGCPauseMillis
|
||||
`-XX:MaxGCPauseMillis`用于控制最大的GC停顿时间,该值是一个大于0的毫秒数,垃圾回收器尽量确保内存回收花费的时间不超过该设定值。
|
||||
|
||||
##### GCTimeRatio
|
||||
该值应该是一个大于0且小于100的整数,如果该值为n,代表允许的GC时间占总时间的最大比例为`1/(1+n)`。
|
||||
|
||||
#### Serial Old
|
||||
Serial Old是Serial的老年代版本,同样是一个单线程的收集器,采用`标记-整理`算法对老年代对象进行垃圾收集。
|
||||
|
||||
#### Parallel Old
|
||||
Parallel Old是Parallel Scavenge的老年代版本,使用多线程和`标记-整理`算法。
|
||||
|
||||
Parallel Old主要是和Parallel Scavenge搭配使用,否则Parallel Scavenge只能搭配Serial Old使用。
|
||||
|
||||
> 在Parallel Scavenge/Parallel Old搭配使用时,发生GC时用户线程也都处于暂停状态。
|
||||
|
||||
|
||||
#### CMS收集器
|
||||
CMS(Concurrent Mark Sweep)是一种`以获取最短回收停顿时间`的收集器。在重视服务响应时间的应用中,适合使用CMS收集器进行老年代的垃圾回收。
|
||||
|
||||
从名字上可以看出,CMS收集器基于`标记-清除`算法实现,其运作过程相对于前面集中收集器来说更加复杂,其分为如下步骤:
|
||||
- 初始标记(STW):
|
||||
- 初始标记仅仅会标记GC Roots能够直接关联到的对象,速度很快
|
||||
- 并发标记:
|
||||
- 并发标记阶段会执行GC Roots Tracing
|
||||
- 重新标记(STW)
|
||||
- 重新标记期间会修正并发标记期间因用户线程继续运作而导致标记产生变动的那一部分对象的标记记录
|
||||
- 该阶段的耗时比初始标记长,但是远比并发标记短
|
||||
- 并发清除
|
||||
|
||||
在CMS的整个收集过程中,初始标记和重新标记阶段是存在STW的,但是并发标记和并发清除时收集器线程可以和用户线程同时运行。
|
||||
|
||||
<img src="https://cdn.tobebetterjavaer.com/stutymore/gc-collector-20231228211056.png" alt="" tabindex="0" loading="lazy" photo-swipe="" style="cursor: zoom-in;">
|
||||
|
||||
##### CMS缺陷
|
||||
CMS虽然在垃圾收集时能够做到停顿时间较短,但是其仍然存在如下缺陷:
|
||||
- CMS对CPU资源比较敏感。`CMS默认启动的回收线程数是(CPU数量 + 3)/4个,当cpu物理核心数较少时,垃圾收集线程将会抢占大量cpu资源`
|
||||
- CMS无法处理浮动垃圾,由于CMS在并发清理阶段并不会引入STW,故而此时用户进程可以运行并伴随新的垃圾产生。该部分垃圾cms无法进行处理,只能等待下一次GC来清掉。
|
||||
- 由于在并发清除阶段同时还有用户线程运行,必须为用户线程预留内存,`故而CMS无法在老年代空间几乎被填满后才进行垃圾收集`,`需预留一部分空间以供并发收集时用户线程使用`
|
||||
- 在JDK 1.6中,CMS收集器的启动阈值提升到了92%,要是预留的内存没有办法满足用户线程的运行需要,`会出现Concurrent Mode Failure`,此时虚拟机将执行后备方案`临时启用Serial Old收集器重新进行老年代的垃圾收集`
|
||||
- 故而`-XX:CMSInitiatingOccupancyFraction`参数设置过大时,容易产生大量`Concurrent Mode Failure`,性能反而降低
|
||||
- CMS是一款基于标记-清除算法的老年代垃圾收集器,故而在收集结束时会有大量的内存碎片产生
|
||||
- 为了解决内存碎片问题,CMS提供了`-XX:+UseCMSCompactAtFullCollection`开关参数(默认是开启的),用于在CMS进行full gc时开启内存碎片的合并过程
|
||||
- 但是,每次full gc都进行内存碎片合并会导致等待时间过长,故而引入了另一个参数`-XX:CMSFullGCsBeforeCompaction`,该参数用于限制执行多少次不压缩的full gc后,再执行一次带压缩的(默认为0,代表每次进入fullgc后都进行碎片整理)
|
||||
|
||||
|
||||
|
||||
182
jwt/jwt.md
182
jwt/jwt.md
@@ -1,25 +1,159 @@
|
||||
- [JWT](#jwt)
|
||||
- [JWT应用场景](#jwt应用场景)
|
||||
- [JSON Web Token结构](#json-web-token结构)
|
||||
- [Header](#header)
|
||||
- [Payload](#payload)
|
||||
- [registered claims](#registered-claims)
|
||||
- [Public claims](#public-claims)
|
||||
- [private claims](#private-claims)
|
||||
- [Signature](#signature)
|
||||
- [putting together](#putting-together)
|
||||
- [JSON Web Tokens工作方式](#json-web-tokens工作方式)
|
||||
- [JWT的validation和verification](#jwt的validation和verification)
|
||||
- [Validation](#validation)
|
||||
- [Verification](#verification)
|
||||
|
||||
|
||||
# JWT
|
||||
* ## jwt简介
|
||||
> jwt是一个开源标准。jwt定义了一个紧凑且自包含的方式安全的在多方之间通过json字符串传递信息。由于传递的信息会被签名,故而信息是可验证和信任的。
|
||||
jwt只能对payload中的信息进行验证,并不会对信息进行加密。故而jwt中不应该包含敏感信息。jwt只是对header和payload的信息进行签名,保证该信息的来源方式可信的。
|
||||
* ## jwt用途
|
||||
* jwt通常用来进行认证操作
|
||||
* jwt还可以用作信息交换,由于传递的信息会被签名,故而通过jwt来传输信息是安全的
|
||||
* ## jwt结构
|
||||
* ### Header
|
||||
* 对于header部分的信息,会进行base64url编码
|
||||
* ### Payload
|
||||
* 对于payload部分的数据,也会进行base64url形式的编码
|
||||
*
|
||||
* ### Signature
|
||||
* 通常,在header中记录了加密算法,对base64url编码后的header和payload用指定算法和服务端私钥技能型签名后,会生成签名部分
|
||||
* 将签名部分和header、payload通过.分隔并且连接,然后就可以生成jwt
|
||||
* ## jwt认证流程
|
||||
* 首先,客户端第一次向服务端发送登录请求,服务端根据客户端发送的用户名、密码等登录信息在数据库中查找信息并验证,如果登录成功,会在服务端生成一个jwt
|
||||
* 服务端会将生成的jwt返回给客户端,jwt中可以记录一些不敏感信息
|
||||
* 而后每次客户端向服务端发送请求时,都会携带jwt,服务端在接收到jwt后会对jwt中的信息进行验证
|
||||
* ## jwt相对于session存储用户信息的优势
|
||||
* jwt的数据存储在客户端中,故而其并不会像session一样占用服务端内存,能够有效减轻服务端的内存压力
|
||||
* 将一些非敏感信息存储在jwt中,可以避免服务端频繁的在数据库中查询信息
|
||||
* jwt是以json格式存储信息的,故而jwt可以跨语言
|
||||
* jwt可以解决跨域问题,不像cookie只能在同一父域名的不同子域名下共享
|
||||
JSON Web Token是一个开源标准,定义了一种`紧凑且自自包含`的方式,用于在各方之间安全的传输信息,这些信息以JSON对象的形式呈现。传输的信息是被数字签名的,故而信息可以被验证并信任。
|
||||
|
||||
JWT可以通过密钥(使用hmac算法)或public/private key pair(使用RSA/ECDSA算法)来进行签名。
|
||||
|
||||
即使JWT可以通过加密在传递过程中提供安全性,但是本文会集中在signed tokens:
|
||||
- `signed tokens`: signed tokens可以验证token中包含声明的完整性
|
||||
- `encrypted tokens`: 加密则是将token中的claims内容向第三方隐藏
|
||||
|
||||
## JWT应用场景
|
||||
如下是JWT的常用场景:
|
||||
- Authorization:这是JWT的最通用场景。一旦用户登录,后续的请求都会包含JWT,允许用户访问该JWT被允许的路由、服务、资源。
|
||||
- `单点登录`特性普遍使用JWT,其带来开销较小,并且能够轻松的跨domains使用
|
||||
- Information Exchange:JWT能够安全的在各方传递信息。由于JWT可以被签名,例如在使用public/private key时,可以确保发送方身份的可靠性。
|
||||
- 并且,由于签名是通过header和payload进行计算的,还可以用于验证token内容是否被修改
|
||||
|
||||
## JSON Web Token结构
|
||||
JWT的结构较为紧凑,由三个部分组成,三个部分之间通过`.`进行分隔:
|
||||
- Header
|
||||
- Payload
|
||||
- Sinature
|
||||
|
||||
因此,JWT通常看起来如下:
|
||||
```
|
||||
xxxxx.yyyyy.zzzzz
|
||||
```
|
||||
|
||||
### Header
|
||||
header通常由两部分组成,
|
||||
- token的类型:JWT
|
||||
- 使用的签名算法: HMAC/SHA256/RSA
|
||||
|
||||
示例如下所示:
|
||||
```json
|
||||
{
|
||||
"alg": "HS256",
|
||||
"typ": "JWT"
|
||||
}
|
||||
```
|
||||
|
||||
`对于JWT的第一部分而言,json形式的内容会被以Base64Url的形式进行编码`。
|
||||
|
||||
### Payload
|
||||
token的第二部分是payload,包含了token中的claims:
|
||||
- `claim`:即对某个实体及其附加数据的描述(实体通常是用户)
|
||||
|
||||
claims通常存在三种类型:
|
||||
- registered
|
||||
- public
|
||||
- private
|
||||
|
||||
#### registered claims
|
||||
存在一系列预定义的claims,并非强制,但是推荐进行使用。
|
||||
- `iss`: issuer,签发者
|
||||
- `exp`: expiration time,过期时间
|
||||
- `sub`:subject,主体,例如用户唯一标识
|
||||
- `aud`:audience,接收方/受众,指JWT的目标接收方,限制token的使用范围
|
||||
|
||||
#### Public claims
|
||||
pubic claims可以被jwt的使用者任意定义。但是,为了避免冲突,public claims应该在`IANA JSON Web Token Registry`中被定义,或在命名中包含namespace从而避免冲突。
|
||||
|
||||
#### private claims
|
||||
private claims也可以被自由定义,用于在各方之间共享信息。
|
||||
|
||||
private claims和public claims的区别如下:
|
||||
- private claims可以被任意定义,只需要传递的双方协商一致即可
|
||||
- public claims的命名需要遵守相应规范,应在`IANA JSON Web Token Registry`中或以URI的形式定义
|
||||
|
||||
payload的示例如下所示:
|
||||
```json
|
||||
{
|
||||
"sub": "1234567890",
|
||||
"name": "John Doe",
|
||||
"admin": true
|
||||
}
|
||||
```
|
||||
|
||||
作为JWT的第二部分,上述内容也会通过Base64Url的形式进行编码。
|
||||
|
||||
### Signature
|
||||
为了创建jwt的第三部分,必须接收如下内容:
|
||||
- encoded header
|
||||
- encoded payload
|
||||
- secret
|
||||
- header中指定的算法
|
||||
|
||||
可以根据上述内容生成签名。
|
||||
|
||||
例如,如果使用`HMAC SHA256`算法进行签名,签名应当以如下方式进行创建:
|
||||
```
|
||||
HMACSHA256(
|
||||
base64UrlEncode(header) + "." +
|
||||
base64UrlEncode(payload),
|
||||
secret)
|
||||
```
|
||||
|
||||
> HMACSHA256是一种基于hash的消息认证码算法,需要提供secret和待散列的消息
|
||||
|
||||
signature会被用于验证token内容是否被修改,并且,在使用private key进行签名时,可以验证jwt的签发方身份。
|
||||
|
||||
### putting together
|
||||
最终的jwt会将上述三个部分通过`.`分隔符拼接在一起。
|
||||
|
||||
## JSON Web Tokens工作方式
|
||||
在认证过程中,当用户通过凭据成功登录后,系统将会返回一个jwt。返回的token本身就是凭据,需谨慎对待避免出现安全问题。
|
||||
|
||||
通常,不应该在jwt中存储敏感的会话数据。
|
||||
|
||||
不论何时,当用户想要访问受保护的route和resource时,user agent都应该发送JWT。通常,JWT被包含在`Authorization` header中,并使用`Bearer` schema。header的内容如下所示:
|
||||
```
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
在http header中发送jwt时,必须避免jwt大小过大。部分server不接受超过8KB的headers。如果需要在jwt中集成较多信息,需要使用替代方案,例如`Auth0 Fine-Grained Authorization`。
|
||||
|
||||
下图展示了JWT获取和通过JWT访问API、资源的流程
|
||||
|
||||
<img alt="How does a JSON Web Token work" loading="lazy" width="480" height="480" decoding="async" data-nimg="1" class="markdown-image_image__vmfuh" sizes="100vw" src="https://www.jwt.io//_next/image?url=https%3A%2F%2Fcdn.auth0.com%2Fwebsite%2Fjwt%2Fintroduction%2Fclient-credentials-grant.png&w=3840&q=75" style="color:transparent;width:100%;height:auto">
|
||||
|
||||
1. 应用或client会向authorization server请求授权
|
||||
2. 当授权被授予后,authorization server将会向应用返回access token
|
||||
3. 应用将会使用token去访问受保护资源
|
||||
|
||||
需要注意,在使用signed tokens时,token中的所有内容都会暴露给用户和其他方,即使他们无法修改这些信息。即不应该在token中包含机密信息。
|
||||
|
||||
## JWT的validation和verification
|
||||
jwt的validation和verification针对的是JWT的不同方面:
|
||||
- validation:jwt的格式是否正确
|
||||
- verification:确保jwt真实且未修改
|
||||
|
||||
### Validation
|
||||
JWT validation会检查jwt的结构、格式、内容:
|
||||
- 结构:确保jwt是被`.`符号分隔的三个部分(header, payload, signature)
|
||||
- format:校验每个部分都是使用Base64URL进行编码的的,并且payload包含预期的claims
|
||||
- content:校验payload中的claims是否正确,例如过期时间(exp)、issued_at(iat)、not before(nbf)等,确保token没有过期,并且没有在生效前使用
|
||||
|
||||
### Verification
|
||||
verification保证的是jwt的真实性和完整性
|
||||
- Signature Verification:其为验证的主要环节,会通过jwt的header和payload部分来校验jwt的signature部分。该部分会通过header中指定的算法(例如RSA/ECDSA/HMAC)来完成。如果签名不符合预期,token可能被修改或来源非可信源。
|
||||
- Issuer Verification:校验`iss` claim指定的内容是否和预期相符
|
||||
- Audience Check:确保`aud` claim和预期相符
|
||||
|
||||
|
||||
|
||||
161
linux/btrfs.md
Normal file
161
linux/btrfs.md
Normal file
@@ -0,0 +1,161 @@
|
||||
# btrfs
|
||||
btrfs是一个现代的COW(copy on write)文件系统,该文件系统旨在实现高级功能的同时还注重容错、修复和易于管理。
|
||||
|
||||
## 文件系统特性
|
||||
### 在单个设备上的文件系统
|
||||
想要在分区`/dev/partition`上创建文件系统,可以通过如下命令:
|
||||
```bash
|
||||
# mkfs.btrfs -L mylabel /dev/partition
|
||||
```
|
||||
btrfs元数据的默认节点大小为16KB,而数据默认的扇区大小等于page size并自动检测。如果想要自定义元数据的nodesize,可以通过`-n`属性来指定(必须是扇区大小的整数倍):
|
||||
```bash
|
||||
# mkfs.btrfs -L mylabel -n 32k /dev/partition
|
||||
```
|
||||
|
||||
### 在多个设备上创建文件系统
|
||||
可以在多个磁盘设备上创建btrfs文件系统,并且在创建文件系统后,可以向文件系统中添加更多磁盘。
|
||||
|
||||
默认情况下,metadata将会被镜像到两个设备上,而data则是会被分散到所有的设备上。其等价于`mkfs.btrfs -m raid1 -d raid0`。
|
||||
|
||||
> 默认情况下,metadata将会使用raid1,存在两个copy,而data则是使用raid0,只存在一个copy(single)。
|
||||
|
||||
如果只存在一个磁盘设备,那么metadata将会在一个设备上被重复存储,`mkfs.btrfs -m dup -d single`。但如果存在多个磁盘设备,那么不应该对metadata使用dup,而是指定raid1,`mkfs.btrfs -m raid1 -d single`.
|
||||
|
||||
#### 新增和删除设备
|
||||
btrfs支持在创建文件系统后,实时的新增和移除设备,并且支持在raid level之间进行切换。
|
||||
|
||||
btrfs支持raid0, raid1, raid5, raid10, raid6。当btrfs针对block进行读取时,会校验checksum。如果checksum校验失败,会尝试从备份copy处进行读取,如果备份copy读取成功,则会修复前面损坏的block。
|
||||
|
||||
raid1 profile允许2,3,4个copies,分别叫做RAID1, RAIDC3, RAIDC4。
|
||||
|
||||
> 对于RAID5和RAID6,raid5需要的最小设备数量为3个,而raid6需要的最小设备数量为4个。
|
||||
|
||||
## 文件系统创建
|
||||
`mkfs.btrfs`可以用于创建btrfs文件系统,通过-d来指定data的raid模式,通过-m来指定metadata的raid模式。
|
||||
|
||||
指定raid时,有效的值如下:
|
||||
- raid0
|
||||
- raid1
|
||||
- raid10
|
||||
- raid5
|
||||
- raid6
|
||||
- single
|
||||
- dup
|
||||
|
||||
raid 10 至少需要4块设备:
|
||||
```bash
|
||||
# Create a filesystem across four drives (metadata mirrored, linear data allocation)
|
||||
mkfs.btrfs -d single /dev/sdb /dev/sdc /dev/sdd /dev/sde
|
||||
|
||||
# Stripe the data without mirroring, metadata are mirrored
|
||||
mkfs.btrfs -d raid0 /dev/sdb /dev/sdc
|
||||
|
||||
# Use raid10 for both data and metadata
|
||||
mkfs.btrfs -m raid10 -d raid10 /dev/sdb /dev/sdc /dev/sdd /dev/sde
|
||||
|
||||
# Don't duplicate metadata on a single drive (default on single SSDs)
|
||||
mkfs.btrfs -m single /dev/sdb
|
||||
```
|
||||
|
||||
如果想要使用不同大小的device,那么striped raid级别(raid0,raid10,raid5,raid6)可能不会使用所有的磁盘空间,而non-striped的等效替代则是会更有效的使用空间(single替换raid0, raid1替换raid10)。
|
||||
```bash
|
||||
# Use full capacity of multiple drives with different sizes (metadata mirrored, data not mirrored and not striped)
|
||||
mkfs.btrfs -d single /dev/sdb /dev/sdc
|
||||
```
|
||||
|
||||
一旦创建文件系统后,可以对文件系统中的任何设备执行挂载操作:
|
||||
```shell
|
||||
mkfs.btrfs /dev/sdb /dev/sdc /dev/sde
|
||||
mount /dev/sde /mnt
|
||||
```
|
||||
|
||||
当重启或重新导入了btrfs模块后,需要通过`btrfs device scan`来扫描文件系统中所有的设备。
|
||||
|
||||
### device scan
|
||||
`btrfs device scan`用于扫描/dev下所有的块设备,并用于探测btrfs volume。在导入btrfs模块后,如果文件系统中存在多个磁盘设备,需要执行`btrfs device scan`命令。
|
||||
|
||||
```shell
|
||||
# Scan all devices
|
||||
btrfs device scan
|
||||
|
||||
# Scan a single device
|
||||
btrfs device scan /dev/sdb
|
||||
```
|
||||
|
||||
### 添加新设备
|
||||
`btrfs filesystem show`命令将会展示系统中所有的btrfs文件系统和文件系统所包含的磁盘设备。
|
||||
|
||||
`btrfs device add`用于添加新的磁盘设备到已经挂载的文件系统中。
|
||||
|
||||
`btrfs balance`可以在已经存在的设备间重新分配负载(restrip)
|
||||
|
||||
对于一个已经挂载在`/mnt`的文件系统,调用如下命令可以将`/dev/sdc`设备添加到该文件系统中:
|
||||
```shell
|
||||
btrfs device add /dev/sdc /mnt
|
||||
```
|
||||
在已经将磁盘设备添加到文件系统后,所有的metadata和data仍然存储在原来的磁盘中,从而可以通过`restrip`来将数据重新负载到各个设备上:
|
||||
```shell
|
||||
btrfs balance start /mnt
|
||||
```
|
||||
`btrfs balance`命令会读取原来所有的metadata和data,并且将其重写到所有可获取的设备中。
|
||||
|
||||
### 非raid文件系统转化为raid文件系统
|
||||
可以通过向文件系统中新增设备,并运行balance filter来将非raid文件系统转化为raid文件系统,其会改变chunk allocate profile。
|
||||
|
||||
例如,可以通过如下方式将一个已经存在的single device转化为RAID1:
|
||||
```bash
|
||||
mount /dev/sdb1 /mnt
|
||||
btrfs device add /dev/sdc1 /mnt
|
||||
btrfs balance start -dconvert=raid1 -mconvert=raid1 /mnt
|
||||
```
|
||||
|
||||
### 移除设备
|
||||
`btrfs device delete`用于实时丛文件系统中移除设备,其会将待移除设备上所有的内容重新分配到文件系统中的其他设备上。
|
||||
|
||||
移除设备示例如下:
|
||||
```bash
|
||||
mkfs.btrfs /dev/sdb /dev/sdc /dev/sdd /dev/sde
|
||||
mount /dev/sdb /mnt
|
||||
# Put some data on the filesystem here
|
||||
btrfs device delete /dev/sdc /mnt
|
||||
```
|
||||
|
||||
|
||||
`btrfs device delete missing`会删除`出现在文件系统元数据描述中但是当前没有被挂载的设备`。
|
||||
|
||||
|
||||
### 替换失败的设备
|
||||
当文件系统中的设备正在发生故障或是已经发生故障时,应该使用`btrfs replace`而不是添加新设备后移除故障设备。
|
||||
|
||||
示例如下所示:
|
||||
```bash
|
||||
user@host:~$ sudo btrfs filesystem show
|
||||
Label: none uuid: 67b4821f-16e0-436d-b521-e4ab2c7d3ab7
|
||||
Total devices 6 FS bytes used 5.47TiB
|
||||
devid 1 size 1.81TiB used 1.71TiB path /dev/sda3
|
||||
devid 3 size 1.81TiB used 1.71TiB path /dev/sdb3
|
||||
devid 4 size 1.82TiB used 1.72TiB path /dev/sdc1
|
||||
devid 6 size 1.82TiB used 1.72TiB path /dev/sdd1
|
||||
devid 8 size 2.73TiB used 2.62TiB path /dev/sde1
|
||||
*** Some devices missing
|
||||
```
|
||||
|
||||
挂载文件系统:
|
||||
```bash
|
||||
sudo mount -o degraded /dev/sda3 /mnt
|
||||
```
|
||||
将缺失的设备替换为新设备:
|
||||
```bash
|
||||
sudo btrfs replace start 7 /dev/sdf1 /mnt
|
||||
```
|
||||
替换操作将会在后台进行,可以通过`btrfs replace status`来查看任务执行状态。
|
||||
```bash
|
||||
user@host:~$ sudo btrfs replace status /mnt
|
||||
Started on 27.Mar 22:34:20, finished on 28.Mar 06:36:15, 0 write errs, 0 uncorr. read errs
|
||||
```
|
||||
- 当用一个更小的设备来替换已经存在的设备时,需要先调用`btrfs fi resize`
|
||||
- 而如果使用一个更大的设备来替换已经存在的设备,可以在替换之后调用`btrfs fi resize`
|
||||
|
||||
|
||||
|
||||
|
||||
78
linux/rsync.md
Normal file
78
linux/rsync.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# Rsync
|
||||
Rsync是一个灵活的网络同步工具。其可以用于在多台unix机器间增量同步文件,也可以用于系统备份。
|
||||
## Syntax
|
||||
### 在本地进行文件同步
|
||||
当想要在本地目录之间进行文件同步时,可以调用如下形式的命令:
|
||||
```shell
|
||||
rsync -r dir1/ dir2
|
||||
```
|
||||
|
||||
`-r`选项代表递归的对dir目录下的文件进行备份,当需要对目录进行同步时,该选项是必须的。
|
||||
|
||||
除了指定`-r`选项外,还可以指定`--archive | -a`选项,该选项是一个组合选项,archive mode代表`-rlptgoD`。指定`-a`选项后,该选项会递归的同步文件,并且保存符号链接、device文件、文件mtime、group信息、owner信息、权限信息。该选项比`-r`选项使用的更频繁,并且是推荐使用的选项。使用示例如下:
|
||||
```shell
|
||||
# 第一个目录参数中指定了`/`符号,如果没有指定该符号,那么dir目录本身
|
||||
# 也会被放到dir2中,同步后dir2中文件结构则是dir2/dir1/[files]
|
||||
rsync -a dir1/ dir2
|
||||
```
|
||||
|
||||
另外,在使用rsync时,可以指定`-n`或是`--dry-run`选项,指定该选项后,执行命令时并不会实际执行修改操作,该选项只是执行一个trail run,通常和`-v`选项一起使用,查看实际执行时会造成那些修改。使用示例如下:
|
||||
```shell
|
||||
rsync -anv dir1/ dir2
|
||||
```
|
||||
### 同步到远程服务器
|
||||
如果要通过rsync向远程服务器同步数据,只需要拥有远程服务器的ssh访问权限即可,同时需要本地和远程都安装有rsync。
|
||||
|
||||
当向远程服务器同步文件时,其语法如下:
|
||||
```shell
|
||||
rsync -a ~/dir1 username@remote_host:destination_directory
|
||||
```
|
||||
上述命令为push模式,其主动将本地文件同步到远程目录中。
|
||||
|
||||
还可以使用pull模式从远程服务器中同步文件到本地,其命令如下:
|
||||
```shell
|
||||
rsync -a username@remote_host:/home/username/dir1 place_to_sync_on_local_machine
|
||||
```
|
||||
|
||||
### 使用Rsync的其他选项
|
||||
#### `-z`
|
||||
如果在通过rsync传输文件时,可以通过指定`-z`选项来在传输文件时添加对文件的压缩来减少传输带宽:
|
||||
```shell
|
||||
rsync -az source destination
|
||||
```
|
||||
#### `--partial`
|
||||
默认情况下,当传输被中断时,rsync会删除只传输了部分的文件。当指定了`--partial`选项时,rsync会对部分传输的文件进行保留,下次传输时就只用传输缺失的部分,能够减少下次传输时间。
|
||||
|
||||
#### `-P`
|
||||
`-P`选项组合了`--progress`和`--partial`两个选项。第一个选项提供了一个传输文件的进度条,而第二个选项则是允许对中断的传输进行恢复操作。
|
||||
```shell
|
||||
rsync -azP source destination
|
||||
```
|
||||
#### `--delete`
|
||||
默认情况下,rsync在同步文件时,如果source目录中移除了某文件,同步文件后target目录中该文件并不会被同步移除。通过指定`--delete`选项,可以同步移除target目录中的文件。
|
||||
|
||||
#### `--exclude`
|
||||
如果在同步文件时,想要抛出source目录内某些目录或是文件,可以在`--exclude`中指定排除的pattern。在想要排除多个路径时,既可以指定多个`--exclude`选项,也可以在一个`--exclude`选项中指定多个模式,多个pattern之间通过`,`分割,首尾符号为`{`和`}`:
|
||||
```shell
|
||||
rsync -aPznv --delete --exclude={"fiels0","fiels1"} dir1/ dir2
|
||||
```
|
||||
#### `--include`
|
||||
可以使用`--include`来覆盖`--exclude`的排除操作,示例如下:
|
||||
```shell
|
||||
rsync -a --exclude=pattern_to_exclude --include=pattern_to_include source destination
|
||||
```
|
||||
#### `--backup`
|
||||
rsync的`--backup`选项可以用于存储文件的备份。该选项和`--backup-dir`选项联合使用,用于指定备份文件的存储路径。
|
||||
|
||||
--backup选项会将文件备份在target根路径下`backup-dir`路径指定的位置。并且,只有本次rsync操作增量同步的文件才会存储在`backup-dir`中。
|
||||
|
||||
```shell
|
||||
rsync -a --delete --backup --backup-dir=/path/to/backups /path/to/source destination
|
||||
```
|
||||
|
||||
#### `--link-dest`
|
||||
rsync的`--link-dest`选项可用于对内容进行增量同步
|
||||
|
||||
|
||||
|
||||
|
||||
518
mq/kafka/kafka-尚硅谷.md
Normal file
518
mq/kafka/kafka-尚硅谷.md
Normal file
@@ -0,0 +1,518 @@
|
||||
- [Kafka](#kafka)
|
||||
- [简介](#简介)
|
||||
- [消息队列应用场景](#消息队列应用场景)
|
||||
- [缓存/削峰](#缓存削峰)
|
||||
- [解耦](#解耦)
|
||||
- [异步通信](#异步通信)
|
||||
- [消息队列模式](#消息队列模式)
|
||||
- [点对点](#点对点)
|
||||
- [发布订阅模式](#发布订阅模式)
|
||||
- [kafka架构](#kafka架构)
|
||||
- [分区存储](#分区存储)
|
||||
- [group消费](#group消费)
|
||||
- [zookeeper](#zookeeper)
|
||||
- [生产者](#生产者)
|
||||
- [分区](#分区)
|
||||
- [批量发送](#批量发送)
|
||||
- [ack级别](#ack级别)
|
||||
- [异步发送](#异步发送)
|
||||
- [同步发送](#同步发送)
|
||||
- [生产者发送消息的分区策略](#生产者发送消息的分区策略)
|
||||
- [kafka消息传递语义](#kafka消息传递语义)
|
||||
- [数据乱序](#数据乱序)
|
||||
- [broker](#broker)
|
||||
- [broker选举](#broker选举)
|
||||
- [partition reassign](#partition-reassign)
|
||||
- [broker宕机对分区造成的影响](#broker宕机对分区造成的影响)
|
||||
- [broker AR \& OSR](#broker-ar--osr)
|
||||
- [leader选举机制](#leader选举机制)
|
||||
- [follower故障恢复细节](#follower故障恢复细节)
|
||||
- [leader宕机的细节](#leader宕机的细节)
|
||||
- [kafka分区存储机制](#kafka分区存储机制)
|
||||
- [kafka文件清除策略](#kafka文件清除策略)
|
||||
- [消费者](#消费者)
|
||||
- [kafka消费者工作流程](#kafka消费者工作流程)
|
||||
- [消费者组](#消费者组)
|
||||
- [消费者实例拉取数据流程](#消费者实例拉取数据流程)
|
||||
- [消费者api](#消费者api)
|
||||
- [分区分配和再平衡](#分区分配和再平衡)
|
||||
- [offset](#offset)
|
||||
- [rebalance](#rebalance)
|
||||
- [kafka消费起始offset](#kafka消费起始offset)
|
||||
- [kafka exactly-once语义](#kafka-exactly-once语义)
|
||||
- [apache kafka idempotence](#apache-kafka-idempotence)
|
||||
- [事务-在多个分区之间进行原子写入](#事务-在多个分区之间进行原子写入)
|
||||
- [transactional message \& non-transactional message](#transactional-message--non-transactional-message)
|
||||
|
||||
|
||||
# Kafka
|
||||
## 简介
|
||||
### 消息队列应用场景
|
||||
缓存/削峰、解耦、异步通信
|
||||
### 缓存/削峰
|
||||
当生产端生产数据的速率大于消费端消费数据的速率时,消息队列可用于对消费不完的数据进行缓存
|
||||
### 解耦
|
||||
当数据生产方来源存在多个(例如数据库、网络接口等),且消费方也存在多种(Hadoop大数据平台、spring boot服务实例等)时,可以将消息队列中存储的消息作为数据从生产端到消费端的中间格式。
|
||||
|
||||
生产端负责将产生的数据转化成特定格式的消息发送到消息队列,而消费端负责消费消息队列中的消息。不同种类的生产端和消费端只用针对消息的队列中的消息各自进行适配即可。
|
||||
|
||||
### 异步通信
|
||||
通过消息队列,可以实现多个服务实例之间的异步调用,服务调用方在将调用信息封装到消息并发送消息到mq后,即可返回,并不需要同步等待。被调用方可以异步的从mq中获取消息并进行消费。
|
||||
|
||||
### 消息队列模式
|
||||
#### 点对点
|
||||
一个消息队列可以对应多个生产者和多个消费者,**但消息只能被一个消费者进行消费**,消费者将数据拉取并消费后,向mq发送确认,mq中将被消费的消息删除
|
||||
#### 发布订阅模式
|
||||
**生产者产生的数据可以被多个消费者消费**,生产者将消息发送到topic,订阅该topic的消费者会拉取消息进行消费,消费完成后并不会删除该消息,该消息仍可被其他订阅该topic的消费者进行消费。(消息并不会永久保存在消息队列中,通常会设置超期时间,消息保存超过该时间后自动删除)
|
||||
|
||||
### kafka架构
|
||||
#### 分区存储
|
||||
kafka将一个topic分为了多个分区,用于分布式存储数据。而每个分区都跨多台服务器实例进行存储,从而增加容错性。对特定分区来说,其存在于多台服务器实例,其中一台为主机,其他服务器为从机,**主机会处理对该分区数据所有的读写请求**(由于读写操作全都在主机上发生,而每台服务器都担当了某些分区的主机和其他分区的从机,故而读写请求在不同的服务器之间进行了负载均衡),而从机只会被动的复制主机修改。
|
||||
#### group消费
|
||||
每个消费者实例都属于一个consume group,一个consume group则是可以保存多个消费者实例。对于发送到kafka mq topic的每一条消息,都会被广播给订阅了该topic的每个consume group。而consume group接收到消息后,只会将消息发送给group中的一个消费者实例来进行处理,故而在consume group中,实现了消息在不同消费实例之间的负载均衡。
|
||||
> 在kafka mq中,topic的实际订阅者是consume group,kafka mq会将topic中的消息广播给所有订阅了该topic的consume group,而每条消息都只会被consume group中的一个消费者实例进行消费
|
||||
>
|
||||
> 在一个consume group中,每个消费者实例都负责某一topic中相应的分区,每个特定分区只有一个消费者实例负责。
|
||||
|
||||
|
||||
> #### 消费者订阅
|
||||
> 消费者的订阅操作是在消费者实例中调用的,在同一消费者组中,**不同消费者实例可以订阅不同的topic集合**。topic的分区只会在订阅了该topic的实例之间进行分配。
|
||||
>
|
||||
> 例如存在消费者实例c1,c2,c3, 其中c1订阅了t1,c2订阅了t1和t2,c3订阅了t1, t2, t3, 那么t1的分区将会在c1, c2, c3之间进行发呢配,t2的分区只会在c2和c3之间进行分配,t3的分区只会被分配给c3
|
||||
|
||||
|
||||
#### zookeeper
|
||||
zookeeper作为注册中心,用于记录存在多个kafka实例时,当前已上线且状态正常的kafka实例,以及各个分区leader实例的信息
|
||||
|
||||
#### 生产者
|
||||
##### 分区
|
||||
生产者客户端将会决定将消息发送到某个分区,可以通过负载均衡随机决定将消息发送到哪个分区,也可以提供一个key并通过算法决定将消息发送到哪个分区。
|
||||
|
||||
决定将消息发送到哪个分区后,生产者会直接将消息发送到该分区对应的leader broker中。
|
||||
|
||||
##### 批量发送
|
||||
生产者并不会每次产生消息后都立即将消息发送到broker,而是会累积消息,**直到消息数据积累到特定大小(默认为16K)后**,才会将消息发送给broker。
|
||||
> **消息发送条件**
|
||||
>
|
||||
> - batch.size: 当消息累计到特定大小(默认为16K)后,发送给broker。如果消息的大小大于`batch.size`,那么并不会对消息做累积操作。**发送到broker的请求将会包含多个batch,每个batch对应一个分区,不同分区的消息通过不同的batch进行发送**
|
||||
> - linger.ms: 生产者将会把发送给broker的两个请求之间的消息都累积到一个batch里,该batch将会在下次发送请求给broker时发送。但是,对特定分区累积大小如果没有达到`batch.size`的限制,哪个通过linger.ms来控制该消息延迟发送的最长时间。`linger.ms`单位为ms,**其默认值为0,代表即使消息累积没有达到`batch.size`,也会立马发送给broker**。若`linger.ms`设置为1000,则代表没有累积到`batch.size`大小的消息也会在延迟1s后发送给broker
|
||||
|
||||
##### ack级别
|
||||
生产者将消息发送到broker后,broker对生产者有三种应答级别:
|
||||
- 0:生产者发送数据后无需等待broker返回ack应答
|
||||
- 1:生产者发送消息到broker后,leader broker将消息持久化后向生产者返回ack
|
||||
- -1:生产者发送过来的数据,leader broker和isr队列中所有的broker都持久化完数据后,返回ack
|
||||
|
||||
##### 异步发送
|
||||
kafka生产者异步发送是指生产者调用kafka客户端接口将发送的消息传递给kafka的客户端,此时消息并未发送到broker,而异步调用接口即返回。调用异步接口可以通过指定回调来获取消息传递到的topic、分区等信息。
|
||||
|
||||
##### 同步发送
|
||||
除了异步调用接口外,还可以调用同步调用的接口来发送消息。在调用异步接口发送消息时,仅仅将消息放到缓冲区后调用就返回,可能放到缓冲区的消息并没有发送到broker。可以通过调用同步发送消息的接口来发送消息,其会阻塞并等待broker将消息持久化后返回的异常或者ack应答。
|
||||
|
||||
想要同步发送消息,只需要对异步接口返回的future对象调用`.get`即可,其会等待future对象完成。
|
||||
|
||||
##### 生产者发送消息的分区策略
|
||||
在生产者发送消息时,需要决定将消息发送到哪个分区,决定策略如下所示:
|
||||
|
||||
1. 如果发送消息时指定了要发送到哪个分区,那么发送到哪个分区
|
||||
2. 如果发送消息时没有指定分区,但是指定了消息的key,那么对key进行散列,根据key散列后的值决定发送到哪个分区
|
||||
3. 如果发送消息时没有指定发送到哪个分区,也没有指定key,那么根据黏性策略发送消息
|
||||
> 黏性分区策略
|
||||
>
|
||||
> 黏性分区策略会随机选择一个分区进行发送,并尽可能的一直使用该分区,直到该分区的batch size已满,此时kafka会随机再选择另一个分区进行发送
|
||||
|
||||
除了上述分区策略外,kafka支持自定义分区策略。
|
||||
|
||||
##### kafka消息传递语义
|
||||
1. 至少一次:acks=-1情况下,消息能被确保传递不丢失,但是可能会存在消息重复传递的问题
|
||||
2. 至多一次:acks=0的情况下,kafka能保证消息最多只传递一次的问题,但是无法保证消息会发送到broker,可能存在消息丢失问题
|
||||
3. 精确一次:数据既不会重复也不会丢失
|
||||
|
||||
为了保证精确一次,kafka引入了两个特性:`幂等性`和`事务`
|
||||
|
||||
> 幂等性
|
||||
>
|
||||
> 生产者在初始化时会被分配一个producer id,并且生产者在发送消息时,针对每条消息都存在序列号。生产者每次发送消息时,都会附带pid(生产者id)和消息的序列号。序列号是针对topic分区的,生产者向分区发送消息时,针对每个分区都维护了一套序列号。当消息被发送给broker后,其会比较当前分区来源PID和消息PID相同,并且已经被ack的最大序列号,如果当前消息的序列号小于已经ack的序列号,说明该消息已经发送过,属于重复消息,被丢弃。
|
||||
>
|
||||
> **由于每个新的生产者实例都会被分配一个新的producer id,那么只能在单个生产者会话中保证幂等。**
|
||||
>
|
||||
> 生产者可以配置`enable.idempotence`属性用于控制幂等是否开启,默认情况下该属性值为true,默认开启幂等性,生产者会保证对每条消息只有一个副本被写入
|
||||
|
||||
> 事务
|
||||
> #### 概念
|
||||
> kafka事务能够保证应用能够原子的向多个topic分区中写入数据,所有写入操作要么同时成功,要么同时失败。
|
||||
>
|
||||
> 另外,消费过程可以看作是对offset topic的写操作,那么kafka事务允许将消息的消费和消息的生成包含在一个原子的单元中。
|
||||
>
|
||||
> #### 保证
|
||||
> 为了实现kafka事务,需要应用提供一个全局唯一的transactionId,该transactionId会稳定贯穿所有该应用的会话。当提供了transactionId后,kafka会做出如下保证:
|
||||
>
|
||||
> - 只有一个活跃的生产者具有指定transactionId。为了实现该保证,当具有相同transactionId的新生产者实例上线后,会隔离旧的实例
|
||||
> - 在应用会话之间将会进行事务的恢复。如果应用实例宕机,那么那么下一个实例恢复工作前将会有一个干净的状态,任何尚未完成的事务都会处于完成状态(提交或异常退出)
|
||||
>
|
||||
> #### 事务协调器
|
||||
> 每个生产者实例都被分配了一个事务协调器,分配producer id、管理事务等逻辑都由事务协调器负责
|
||||
> #### Transaction Log
|
||||
> Transaction Log是一个内部的kafka topic,类似于consumer offset topic,transaction log是一条持久化和主从复制的记录,记录了每个事务的状态。transaction log是对事务协调器的状态存储,最新版本log快照封装了当前每个活跃事务的状态。
|
||||
> #### 控制消息
|
||||
> 控制消息是写入用户topic中的特殊消息,由客户端进行处理,但是不暴露给用户。例如,其被用于broker告知消费者其先前拉取的消息是否已经被原子的提交。
|
||||
> #### transactionId
|
||||
> transactionlId用于唯一标识producer,并且transactionId是持久化的(producer id在生产者实例重启后会重新获取)。如果不同生产者实例具有相同的transactionId,那么会恢复或终止上一生产者实例所初始化的任何事务
|
||||
>
|
||||
> #### 数据流
|
||||
> ##### 发现事务协调器
|
||||
> 由于事务协调器是分配PID和管理事务的中心,首先生产者会发送请求给任一broker来询问其所属的事务协调器
|
||||
> ##### 获取PID
|
||||
> 查询到其所属事务协调器后,会获取producer id。如果transactionId已经被指定,那么在获取PID的请求中会包含transactionId,并且transactionId和PID的关联将会被记录到transaction log中,以便于之后如果上线具有相同transactionId的新实例,能够根据transactionId该映射返回旧实例的PID。
|
||||
>
|
||||
> 在上线新实例,返回具有相同transactionId的旧实例后,会执行如下操作:
|
||||
>
|
||||
> 1. 隔离旧实例,旧实例所属的事务无法再继续推进
|
||||
> 2. 针对旧生产者实例所属的尚未完成的事务进行恢复操作
|
||||
>
|
||||
> 如果请求PID时transactionId尚未被指定,那么PID将会被分配,但是生产者实例只能在单个会话中使用幂等语义和事务语义
|
||||
>
|
||||
> #### 开启事务
|
||||
> 可以通过beginTransaction()接口调用来开启事务,生产者会将本地状态改为事务已开启,但是事务协调器直到发送了第一条记录时才会将事务状态标记为已开启
|
||||
> #### 消费-处理-生产
|
||||
> 开启事务之后,生产者可以对消息进行消费和处理,并且产生新的消息,
|
||||
> ##### AddPartitionsToTxnRequest
|
||||
> 当某个topic分区第一次作为事务的一部分被执行写操作时,生产者将会发送AddPartitionsToTxnRequest请求到事务协调器。将该topic分区添加到事务的操作将会被事务协调器记录。可以通过该记录信息来对事务中的分区进行提交或回滚操作。如果第一个分区被添加到事务中,事务协调器将会开启事务计时器。
|
||||
> ##### ProduceRequest
|
||||
> 生产者会向topic分区发送多个ProduceRequest来写入消息,请求中包含PID、epoch、序列号。
|
||||
>
|
||||
> #### 提交或回滚事务
|
||||
> 一旦消息被写入,用户必须调用 commitTransaction或abortTransaction方法来对事务进行提交或者回滚。
|
||||
>
|
||||
|
||||
##### 数据乱序
|
||||
kafka如果想要保证消息在分区内是有序的,那么需要开启幂等性(默认开启)并且`max.inflight.requests.per.connection`需要设置小于或等于5(默认情况下该值为5),在不开启幂等性的情况下如果想要保证分区内数据有序需要设置`max.inflight.requests.per.connection`的值为1.
|
||||
|
||||
> `max.inflight.requests.per.connect`
|
||||
>
|
||||
> 该值用于设置客户端向broker发送数据时,在阻塞前每个连接能发送的未收到ack的请求数目。如果该值被设置为大于1并且幂等性未开启,那么在消息发送重试时可能会导致消息的乱序,此时需要开启幂等性或者关闭消息重试机制才能重新保证幂等性。
|
||||
>
|
||||
> 并且,启用幂等性需要该值小于或等于5,如果该值大于5,且幂等性开启
|
||||
> - 如果幂等性未显式指定开启,那么在检测到冲突设置后,幂等性会关闭
|
||||
> - 如果显式指定幂等性开启并且该值大于5,那么会抛出ConfigException异常
|
||||
|
||||
#### broker
|
||||
##### broker选举
|
||||
每个broker节点中都存在controller模块,只有ISR中的节点才能够参与选举(acks=all时,也只需要ISR队列中所有节点都同步消息,才将消息视为已提交)。controller会通过watch来监听zookeeper中节点信息的变化,如果某个分区的leader broker宕机,那么controller模块在监听到变化后会重新开始选举,在ISR中重新选出一个节点作为leader。
|
||||
|
||||
##### partition reassign
|
||||
如果kafka集群在运行时新增了新的节点,**此时节点中旧的topic分区并不会自动同步到新增节点中,如果要将旧topic分区存储到新增broker节点,可以调用重新分区的命令。**
|
||||
|
||||
可以通过`kafka-reassign-partitions.sh --generate`命令对分区在所有节点(包含新节点)之间进行重新分配,此命令调用后会生成一个topic分区在节点间重新分配的计划。
|
||||
|
||||
在确认了生成的重新分配计划后,可以调用`kafka-reassign-partitions.sh --execute`命令来执行新生成的重新分配计划。
|
||||
|
||||
如果想要在kafka集群中退役一台broker server,不能直接关闭该broker节点,也需要跟新增broker节点类似,重新生成一个分区分配计划,分配计划中移除待下线节点,然后执行该分配计划。执行分配计划后,该节点上便不再存储分区,此后该节点并可以安全的关闭。
|
||||
|
||||
##### broker宕机对分区造成的影响
|
||||
broker宕机并不会导致分区的重新分配,例如一个分区的replica-factor为2,存在broker-1,broker-2,broker-3,分区存储在(1,3)服务器上,leader为1号服务器。如果broker-1宕机,那么分区的leader会自动切换为broker-3,但是分区存在的副本数量从2个变成了一个,ISR中也只有(3),此时并不会在未存储该分区的broker-2节点上新增一个副本。
|
||||
|
||||
如果宕机的broker-1重新再上线,那么broker-1会重新存储之前负责存储的分区,但是此时分区对应的leader节点仍然是broker-2,此时broker-2作为分区的leader会导致读写操作全都转移到broker-2节点,负载均衡会造成偏移。
|
||||
|
||||
> `auto.leader.rebalance.enable`
|
||||
>
|
||||
> 该属性默认设置为true,一个后台线程会定期(时间间隔为`leader.imbalance.check.interval.seconds`,默认为300s)检测分区leader是否为默认的perferred leader。如果分区leader不为preferred leader的数量超过一定的比率(` leader.imbalance.per.broker.percentage`,默认为10%),会触发将分区leader改为默认preferred leader的操作。
|
||||
|
||||
##### broker AR & OSR
|
||||
kafka中副本的默认数量为1个,通常生产环境将其配置为2个或2个以上,以便在leader副本宕机后follower副本能够继续服务,避免数据丢失。
|
||||
|
||||
- AR: AR代表集群中分区对应的所有副本集合,all-replicas
|
||||
- ISR: 代表集群中处于同步状态的副本集合,in-sync-replicas 如果ISR中的节点长期未从leader中同步数据,会被剔除出ISR,最长未同步的时间由`replica.lag.time.max.ms`控制,默认为30s。如果leader对应的broker宕机,那么新leader将会从ISR中选举产生
|
||||
- OSR:代表集群中不处于同步状态的副本集合,为AR - ISR
|
||||
|
||||
##### leader选举机制
|
||||
leader选举根据AR中节点的排序来决定,能够成为leader的节点需要再ISR中存活,然后ISR中存活节点在AR中排序最靠前的将会被选举为leader。如果leader发生宕机,那么由ISR中存活且AR中排序最靠前的的节点成为新leader。
|
||||
|
||||
> 选举机制中leader决定是按照AR中broker节点的顺序进行决定的,每一个分区都有一个默认的preferred leader。如果某些节点宕机后再恢复,ISR中节点的顺序将会发生变化,但是AR中的节点顺序并不变,并且preferred leader也不会发生变化。
|
||||
|
||||
##### follower故障恢复细节
|
||||
follower故障恢复细节中,涉及到如下两个概念:
|
||||
- LEO(Log End Offset):每个副本结束offset的下一个位置,类似于java中的List.size()
|
||||
- HW(High WaterMark):当前ISR队列中所有副本最小的HOW(水位线,类似于木桶效应中最短的那一根木板)
|
||||
|
||||
如果follower发生故障,那么会按顺序发生如下:
|
||||
1. 故障follower会被剔除出ISR
|
||||
2. follower故障期间ISR中剩余的follower节点和leader节点会继续接受数据
|
||||
3. 故障follower如果恢复,会读取宕机前记录的旧集群HW,并且将大于等于该旧HW的log记录全部都删除,并且从leader重新同步记录
|
||||
4. 当故障恢复的follower从旧HW位置同步消息到当前集群中的新HW位置时,此时故障恢复的log区数据已经同步到新HW的水位线水平,此时故障恢复的follower节点可以重新加入到ISR中
|
||||
|
||||
##### leader宕机的细节
|
||||
如果leader broker发生宕机,那么新选举成为leader的follower将会成为leader,并且所有节点会将大于等于HW的数据丢弃(由于宕机的是leader,LEO最大,故而leader宕机不会对HW造成影响),并且重新从新选举的leader中同步数据。
|
||||
|
||||
由上可知,如果leader宕机前,其他follower尚未同步完leader中全部的消息,那么leader宕机后可能会发生消息的丢失。如果需要确保消息不丢失,需要设置生产者的acks为all,确保消息再提交前同步到ISR中所有的节点中。
|
||||
|
||||
##### kafka分区存储机制
|
||||
kafka中topic是一个逻辑概念,topic由分区组成,每个分区则是可以看作一个log文件,log中存放生产者产生的数据。生产者产生的消息会被追加到log文件的末端,由于是线性追加,不涉及到平衡树等数据结构,故而**kafka追加消息时,不管当前分区数据存储量大小,追加数据的开销都是相同的,追加操作不会随着数据量变大而变慢**。
|
||||
|
||||
为了防止分区对应的log文件过大而导致的数据定位效率低下,kafka采用了分片和索引的机制,将每个分区分为了多个segment。单个segment默认存储的数据量的大小为1G,且单个segment由如下文件组成:
|
||||
- .log文件:日志文件,用于存储消息(log文件的命名以当前segment中第一条消息再分区中的offset来命名)
|
||||
- .index:偏移量索引文件,用于存储偏移量和position的映射关系,用于快速定位消息
|
||||
- .timeindex:时间戳索引文件,该文件用于存储消息对应的时间戳信息,kafka中的消息默认保存7天后丢弃,通过时间戳信息来决定消息是否应该丢弃
|
||||
|
||||
> index索引
|
||||
>
|
||||
> kafka中的索引为稀疏索引,默认每往log中写入4KB的数据,.index文件中会记录一条偏移量索引信息。
|
||||
>
|
||||
> 可以通过`log.index.interval.bytes`来配置索引记录的密度,每写入多少数据才记录一条索引
|
||||
|
||||
##### kafka文件清除策略
|
||||
kafka中默认消息保存的时间为7天,可以通过修改如下配置来对默认保存时间进行修改:
|
||||
- log.retention.hours:消息默认保存小时:默认为168(24 * 7)
|
||||
- log.retention.minutes:消息默认保存时间,按分钟计
|
||||
- log.retention.ms:消息设置默认保存时间,按ms计
|
||||
|
||||
上述设置中,优先级为`ms`>`minutes`>`hours`,如果优先级较大的被设置,那么取优先级高的设置,在高优先级条目没有被设置时,才取低优先级设置。
|
||||
|
||||
> 如果想要设置消息不超时,可以将`log.retention.ms`设置为-1
|
||||
|
||||
`log.retention.check.interval.ms`参数可以检查消息是否超时的周期,默认情况下该值设置为5min。
|
||||
|
||||
kafka中的日志清除策略由如下两种:
|
||||
- 删除
|
||||
- 压缩
|
||||
|
||||
> 删除
|
||||
>
|
||||
> 当`log.cleanup.policy`默认值为delete,当该值被设置为delete时,存储超期日志的文件将会被删除。
|
||||
>
|
||||
> - 基于时间:默认打开。如果segment中所有记录中时间戳最大的记录(最新插入的记录)超过最长时间设置,那么会将该segment删除。(以segment中最新消息时间戳作为segment文件时间戳)
|
||||
> - 基于大小:基于大小的删除默认是关闭的。`log.retention.bytes`默认值为-1,表示对log文件最大的限制,如果单个log文件的大小超过该大小限制,那么会删除log文件对应的最早的segment
|
||||
|
||||
> 压缩
|
||||
>
|
||||
> 当`log.cleanup.policy`值设置为compact时,会对超时的segment进行压缩(指segment最新一条插入的消息超时),对于相同key的消息,只会保留最后插入的一条消息,演示如下。
|
||||
>
|
||||
> | k2 | k1 | k3 | k1 | k2 |
|
||||
> |:-:|:-:|:-:|:-:|:-:|
|
||||
> | 1 | 2 | 3 | 4 | 5 |
|
||||
>
|
||||
> 会被压缩为
|
||||
>
|
||||
> | k3 | k1 | k2 |
|
||||
> |:-:|:-:|:-:|
|
||||
> | 3 | 4 | 5 |
|
||||
>
|
||||
> 压缩后offset可能并不连续,此时若想要消费的offset不存在,那么会拿到比预期offset大的offset的消息
|
||||
|
||||
#### 消费者
|
||||
通过消息队列的消费方式分为两种:
|
||||
- pull: 拉取
|
||||
- push: 推送
|
||||
|
||||
kafka采用的是拉取模式来进行消费,因为拉取模式可以很好的兼容不同消费者实例的消费速率。各个消费者可以根据自己消费消息的速度来选择拉取消息的速度,如此能避免消息在多个消费者实例间的不合适分配,造成有的消费者实例空闲,而有的消费者实例消息堆积的情况。
|
||||
|
||||
##### kafka消费者工作流程
|
||||
在kafka架构中,一个分区只能够被同一消费者组中的一个消费者实例进行消费,但是消费者组中的一个消费者实例能够对多个分区进行消费。
|
||||
|
||||
并且,任一分区中的消息都会被广播到所有订阅该topic的消费者组中,但是一条消息只能够被消费者组中的一个消费者实例进行消费。
|
||||
|
||||
> #### offset
|
||||
> kafka中一个一个分区能够被多个实例进行消费(被多个位于不同消费者组中的消费者进行消费),kafka通过`offset`来记录每个消费者消费到分区的偏移量。
|
||||
>
|
||||
> offset被存储在kafka的一个主题中(_consumer_offsets),用于持久化offset数据。
|
||||
|
||||
##### 消费者组
|
||||
消费者组是由若干个消费者实例组成的集合,位于同一消费者组中的消费者实例,其groupid都相同。
|
||||
|
||||
在同一消费者组中,一条消息只能由一个消费者实例进行消费。消费者组中的消费者实例负责消费同一topic中的不同分区,各实例负责的分区没有重叠部分。
|
||||
|
||||
> 当消费者组中的消费者实例大于分区数时,消费者组中将存在闲置的消费者实例,同一分区无法由同一消费者组中的多个消费者实例共同消费。
|
||||
>
|
||||
|
||||
> ##### 消费者组初始化流程
|
||||
> 在消费者组中,通过coordinator来协助消费者组实现初始化和分区分配操作,kafka集群中的每个broker实例都有一个对应的coordinator。
|
||||
>
|
||||
> 消费者组在选择通过哪个coordinator实例来协助进行初始化时,会将groupid根据_consumer_offset主题的分区数量进行取模(默认_consumer_offset主题默认有50个分区,故而默认为groupid%50),根据取模后的结果查找_consumer_offsets的第N号分区位于哪台broker上,该broker的coordinator即会协助消费者组进行初始化。(例如`groupid%50=4`,即会选取_consumer_offsets第四号分区所在的broker来作为协助初始化的broker,该broker上的coordinator会协助消费者组进行初始化。
|
||||
>
|
||||
> 在消费者组初始化时,组内每个消费者实例都会向选中的coordinator发送消息,请求加入消费者组,而coordinator则会选中其中一个消费者实例作为该消费者组的leader。
|
||||
>
|
||||
> 选中leader分区之后,coordinator会把broker集群中待消费的topic信息全部发送给leader消费者,leader消费者则是会指定分区分配计划,将topic分区在组内的多个消费者实例之间进行分配。
|
||||
>
|
||||
> 在leader指定完分区分配计划之后,会将分配计划发送给coordinator,coordinator收到分配方案后则会将分配方案分发给消费者组内的各个消费者实例。
|
||||
|
||||
> ##### 消费者组的心跳机制
|
||||
> 消费者组中的每个消费者都会和coordinator维持长连接,消费者会向coordinator发送心跳包(心跳包默认时间为3s)。
|
||||
>
|
||||
> 一旦超过指定时间(`session.timeout.ms`,默认为45s)消费者没有向coordinator发送心跳包,那么该消费者将会被从消费者组中移除,并且会触发分区的再平衡,该宕机消费者实例负责的分区将会被分配给同组中别的消费者实例。
|
||||
>
|
||||
> 如果消费者实例处理消息的时间过长(`max.poll.interval.ms`,默认为5min),自从上一次拉取分区消息时起超过5min还未再次拉取数据,也会触发再平衡。
|
||||
|
||||
##### 消费者实例拉取数据流程
|
||||
消费者实例在拉取消息时,首先会向broker发送一个fetch请求,从broker处批量拉取数据。其中,fetch过程能够通过如下参数自定义:
|
||||
- `fetch.min.bytes`:单个fetch请求中,broker server返回的最小消息大小。如果消费者向broker发送fetch请求时,可获取的消息没有达到该参数限制的大小,那么broker会等待消息累积,直到累积消息达到`fetch.min.bytes`指定的大小,再将消息批量返回给消费者。`默认情况下,该参数的值为1`,代表fetch请求时消息会被立刻返回。增大该值会批量累积消息提升效率,但是也会增加fetch请求的延迟。
|
||||
- `fetch.max.bytes`:单个fetch请求中,broker server返回消息最大数据量大小。fetch操作请求数据时,数据将会被broker批量返回。如果如果分区中待返回第一条record的大小大于`fetch.max.bytes`指定的大小,那么该消息大小也会被返回,即使消息大小大于限制。`默认情况下,该参数的默认值为50m`。
|
||||
- `fetch.max.wait.ms`:在单个请求中,如果broker没有累积到`fetch.max.bytes`指定规模的消息数量,当broker阻塞时间超过`fetch.max.wait.ms`指定的超时时间后,也会将消息返回给消费者。`默认情况下,该参数默认值为500ms`。
|
||||
|
||||
在消费者通过fetch请求拉取到数据后,会将拉取的records缓存起来,然后在调用poll方法时返回缓存的record数据。
|
||||
- `max.poll.records`:该参数用于限制poll()调用返回的最大消息条数。`该参数默认值为500`,调用poll方法时,最多返回500条缓存数据。`max.poll.records`参数用于指定poll行为,并不会对fetch行为造成影响。
|
||||
|
||||
##### 消费者api
|
||||
1. `public void subscribe(Collection<String> topics, ConsumerRebalanceListener listener)`:
|
||||
|
||||
消费者调用该方法时会订阅给定的主题,并且会动态分配给该消费者分区。**主题的订阅并不是增量的,本次调用subscribe方法将会覆盖之前该消费者被分配的分区**。
|
||||
|
||||
如果给定topics为空,那么其效果相当于调用`unsubscribe()`。
|
||||
|
||||
> #### rebalance
|
||||
> 作为group management的一部分,组中的消费者实例会跟踪属于同一消费者组中的其他实例,如果发生了如下事件则是会触发rebalance(再平衡):
|
||||
> - 如果消费者组订阅的任一topic其分区数发生变化
|
||||
> - 一个订阅的topic被创建或是删除
|
||||
> - 消费者组中的一个实例发生宕机
|
||||
> - 一个新的消费者实例加入到消费者组
|
||||
> - group中成员单位订阅的topic发生变化(新增订阅或取消订阅)
|
||||
>
|
||||
> 当上述任一事件发生时,都会触发rebalance操作,此时,提供给subscribe接口的listener将会被调用,代表该消费者实例对应的分区分配被取消了。**并且,在接收到新的分区分配方案时,该listener接口会再次被调用**。
|
||||
>
|
||||
> rebalance只会在调用poll(Duration)接口时被触发,故而callback也只会在调用poll的时间内被触发
|
||||
|
||||
|
||||
2. `public void assign(Collection<TopicPartition> partitions)`:
|
||||
|
||||
手动将分区集合分配给消费者。该接口调用同样**不是增量的**。该接口调用会覆盖之前消费者被分配的分区。
|
||||
|
||||
如果给定partitions为空,那么其效果相当于调用`unsubscribe()`。
|
||||
|
||||
通过该方法进行手动主题分区分配并不会使用消费者组的管理功能,故而,**若当消费者组成员发生变化,或broker集群或主题元数据发生变化时,消费者组rebalance操作也不会被触发。**
|
||||
|
||||
如果开启了自动提交,那么在新的assignment代替旧assignment之前,async commit会被触发,async commit基于旧的assignment。
|
||||
|
||||
3. `public ConsumerRecords<K, V> poll(final Duration timeout)`:
|
||||
|
||||
|
||||
从被分配的分区中拉取数据,如果在调用poll方法之前并没有被分配任何主题或分区,那么会抛异常。
|
||||
|
||||
每次拉取数据时,消费者都会使用`last consumed offset`作为拉取数据的开始偏移量,并按顺序再拉取数据。`last consumed offset`可以通过` seek(TopicPartition, long)`方法手动设置,`last consumed offset`也会被自动设置为订阅分区列表中的最后一次提交偏移量。
|
||||
|
||||
在存在可获取的记录时,该方法会立刻返回,否则其会等待timeout指定的时间,在超过该时间限制后,会返回一个空的record集合。
|
||||
|
||||
> poll方法调用在没有可用的消息集合时,会发送fetch请求从broker拉取数据
|
||||
|
||||
##### 分区分配和再平衡
|
||||
一个消费者组中含有多个消费者实例,而一个topic会包含多个分区,需要将topic中的分区在消费者组中的多个消费者实例之间进行分配。
|
||||
|
||||
kafka中包含如下分区策略:
|
||||
- range
|
||||
- RoundRobin
|
||||
- Sticky
|
||||
- CooperativeSticky
|
||||
|
||||
kafka中的分区策略通过`partition.assignment.strategy`参数来进行配置,kafka可以通过使用多个分区分配策略,多个策略会进行叠加。默认情况下,kafka采用`Range + CooperativeSticky`的策略来进行分区分配。
|
||||
|
||||
> #### Range策略及再平衡
|
||||
> range策略会针对每个topic进行分配。如果一个topic中含有n个分区,消费者组中含有m个消费者实例,那么每个消费者实例将会分配到n/m(整除)个分区,多出来的n%m个分区将会被分配给实例名称靠前的n%m个实例。
|
||||
>
|
||||
> range策略将会有如下弊端,如果每个topic都无法对消费者实例整除,那么剩余的分区都会被分配给排序靠前的消费者实例,这样会造成分区在消费者实例之间分配的不平衡,排序靠前的消费者实例会负担更多的分区
|
||||
>
|
||||
> 在使用range策略时,如果某台消费者实例宕机,那么在宕机超过限定时间(45s内没有向broker coordinator发送心跳包),会将topic分区重新在消费组中的剩余实例之间进行再分配(完全重新生成分配方案,排序靠前的消费者实例在分区数量无法整除实例个数时,负担更多的分区消费)
|
||||
|
||||
> #### RoundRobin
|
||||
> RoundRobin策略针对所有消费者组订阅的所有topic中的分区,会将所有分区根据hashcode进行排序,并且按轮询的顺序分配给消费者实例。例如,第一个分区分配给第一个消费者实例,第二个分区分配给第二个实例...第m+1个分区再次分配给第一个实例(实例总数为m)。RoundRobin将所有分区在所有消费者实例之间进行了平衡,而不像range一样只是针对单个topic中的分区。
|
||||
>
|
||||
> 在使用RoundRobin策略时,如果消费者中某一实例宕机,超过超时时间后,分区同样会在剩余的消费者实例之间进行重新分配。
|
||||
|
||||
> #### Sticky
|
||||
> Sticky策略同样是针对所有topic的分区,类似于RoundRobin。但是,不同于RoundRobin的轮询,Sticky在分配策略时会遵循如下原则:
|
||||
> - Sticky策略会尽可能均匀的在不同消费者实例之间分配分区
|
||||
> - 如果因为某些原因(例如实例宕机)导致需要进行分区的重新分配,Sticky会尽可能的保证存活实例上已经产生的分区分配不被改变,并再次基础上令所有分区在实例间的分配尽量均衡
|
||||
>
|
||||
> 在使用Sticky策略时,即使某台实例宕机,再平衡后存活实例被分配的分区仍然不会变,只是会将宕机实例负责的分区在存活实例之间尽可能均衡的分配
|
||||
|
||||
##### offset
|
||||
在kafka集群中,会保存各个分区的消费情况,将分区针对每个消费者组的偏移量存储在__consumer_offsets主题中。默认情况下,__consumer_offsets采用key/value的形式来存储数据,key为`groupid+topic+分区号`,value则是当前offset的值。
|
||||
|
||||
每个一段时间,kafka就会对该topic进行压缩。
|
||||
|
||||
> #### kafka offset自动提交
|
||||
> kafka默认开启了自动提交功能,在使用kafka时可以专注消费的业务逻辑
|
||||
>
|
||||
> 自动提交相关参数如下:
|
||||
> - `enable.auto.commit`:自动提交是否开启,该参数默认值为true
|
||||
> - `auto.commit.interval.ms`:自动提交默认的间隔时间为5s
|
||||
>
|
||||
> 在开启自动提交时,每次消费者调用poll接口时,都会检查是否距离上次提交的时间间隔已超过5s,若超过则执行自动提交逻辑。
|
||||
>
|
||||
> 在自动提交场景下,可能会造成消息的重复消费,如果自动提交的间隔为10s,在上次提交完成后,过了6s,消费完100条消息,但是此时消费者宕机了,导致消费的100条消息没有提交offset;此时该宕机消费者负责的分区将会被分配给消费者组中的其他消费者,其他消费者消费时仍然会从最后一次提交的offset开始消费,导致100条消息会被重复消费。
|
||||
|
||||
> #### kafka手动提交
|
||||
> kafka可以选择设置手动提交,只用把`enable.auto.commit`关闭。kafka手动提交分为如下两种场景:
|
||||
> - commitSync:同步提交。当关闭自动提交时,可以调用`commitSync`接口来进行手动提交。手动提交的offset为最后一次调用poll方法返回的offset。该方法通常在消费完所有poll返回的消息后再调用,否则若commit后消息还没有消费完,消费者宕机,则会导致消息丢失,有些消费不会被消费。<br>
|
||||
> 在同步提交的场景下,调用commitSync后,线程会一直阻塞,直到接收到kafka server返回的ack,ack代表broker对commit offsets的确认
|
||||
> - commitAsync:由于同步提交会造成阻塞,一直等待broker返回ack,故而会影响消息消费的吞吐量。可以通过异步提交来解决吞吐量问题,但是,`异步提交可能会造成更多消息被重复消费`。
|
||||
|
||||
> #### kafka手动提交、自动提交的优劣
|
||||
> - kafka自动提交可能会造成消息的丢失,自动提交默认间隔为5s,如果在上次poll后的5s内消息并未被消费完成,那么在kafka自动提交后,即使尚未被消费的消息后续未被消费,那么kafka也会将其视为已消费,从而造成消息丢失
|
||||
>
|
||||
> - kafka commitSync可手动同步提交offset,但是在调用commitSync接口后,会等待broker返回确认信息,在此之前消费者会一直阻塞。这样会影响消费者的吞吐量,故而,为了提高吞吐量,可以尽量减少commitSync的提交次数
|
||||
>
|
||||
> - kafka commitAsync,相比于同步提交,commitAsync在调用后并不会阻塞,而是直接返回,此后可以继续调用poll来继续从broker拉取后续消息。但是,相比同步提交,commitAsync可能在发生rebalance时造成重复消费的情况。
|
||||
>
|
||||
> 在使用异步提交时,如果在发生rebalance之前(rebalance只能发生在poll过程中),commitAsync提交失败,由于commitAsync不会失败重试,故而在分区重新分配后,新分配到该分区的消费者实例将会重新消费之前未提交成功的消息,因此产生了消息的重复消费)。
|
||||
>
|
||||
> 而同步提交时,commitSync在提交失败后会无限次重试,直到提交成功,故而在发生rebalance时(rebalance只能发生在poll的过程中),在发生rebalance之前,可以保证之前commitSync操作已经成功。
|
||||
>
|
||||
> #### kafka commitSync
|
||||
> kafka commitSync方法调用可以指定一个超时时间,在超过该超时事件后,会抛出TimoutException。如果调用该api时没有指定超时时间,会默认使用`default.api.timeout.ms`来作为超时时间,`default.api.timeout.ms`的默认值为1min。
|
||||
>
|
||||
> **对于kafka commitSync方法,在超时前,都会一直对提交进行重试,直至提交成功或是发生不可恢复(unrecoverable)的异常。**
|
||||
|
||||
> #### kafka的手动提交重试机制
|
||||
> 针对kafka的手动提交,当使用`commitSync`进行同步提交时,如果提交失败,同步提交会无限次的进行重试,直到提交成功或是发生了不可恢复的异常。
|
||||
>
|
||||
> 但是,在使用`commitAsync`方法进行提交时,kafka消费者在提交失败之后则不会进行重试。在处理kafka commitAsync重试问题时,还需要考虑commit order。当消费者进行异步提交时,如果发现当前batch提交失败,此时可能位于当前batch之后的batch已经处理完成并进行提交(commitAsync并不会等待当前batch提交成功之后再拉取下一批,而是直接拉取下一批继续处理,故而下一批batch可能提交早于当前batch)。故而,如果对当前异常的batch进行重试提交,可能会之后批次的commit offset被覆盖,从而造成消息的重复消费。
|
||||
>
|
||||
> ##### commitAsync接口提交失败后并不会抛出异常,也不会重试
|
||||
|
||||
##### rebalance
|
||||
rebalance通常有两个阶段,`revocation`和`assignment`,即撤销当前消费者被分配的分区和重新分配给消费者新的分区。revocation方法会在rebalance之前被调用,且revocation是rebalance之前消费者最后一次提交offset的机会,可以重写revocation方法,在rebalance发生之前对offset进行同步提交。
|
||||
|
||||
而assignment则是发生在rebalance之后,可以重写assignment方法来初始化各分区的offset。
|
||||
|
||||
通常情况下,commitAsync相较commitSync是更不安全的,在宕机之前提交失败将会造成消息的重复消费。可以通过在回调中使用commitSync来减轻消息的重复消费风险。
|
||||
|
||||
##### kafka消费起始offset
|
||||
通过`auto.offset.reset`属性,可以配置当kafka broker中没有存储分区与特定消费者组offset关系时,消费者消费的行为,其可配置值如下:
|
||||
- earliest:从分区最开始的位置进行消费
|
||||
- latest:默认值为latest,当offset不存在时,从最新的offset开始消费
|
||||
- none:当消费者组针对该分区没有找到offset记录时,抛出异常
|
||||
|
||||
默认情况下,`auto.offset.reset`值为latest,故而当消费者组新订阅一个topic时,并不会从头开始消费分区中的历史消息,而是从分区最新offset开始,消费后续分区接收到的消息。
|
||||
|
||||
> ##### 自定义分区起始消费offset
|
||||
> 除了配置上述属性指定消费起始位置外,还可以通过`KafkaConsumer#seek`接口来指定起始消费分区的offset位置。
|
||||
|
||||
> #### 消费指定时间开始的消息
|
||||
> 如果在消费消息时,想要消费从指定时刻之后的消息,可以通过`kafkaConsumer#offsetsForTimes`接口,能根据传入的分区和时间戳来得到该分区下指定时刻消息的起始offset,返回offset为时间戳大于或等于指定时间戳的第一条消息对应offset
|
||||
|
||||
## kafka exactly-once语义
|
||||
在一个基于发布/订阅的消息系统中,组成该消息系统的节点可能会发生故障。在kafka中,broker可能会宕机,在生产者向topic发送消息时也可能发生网络故障。基于生产者处理这些失败场景的方式,可能会有如下三种语义:
|
||||
- At-least-once:如果生产者在向broker发送消息时,接收到broker返回的ack(调用同步接口发送消息),并且生产者配置`acks=all`(会等待消息写入到所有isr队列中),这代表该消息已经被写入到消息系统中。但如果生产者向broker发送消息后,消息已经被写入到broker集群中,但是broker集群尚未向生产者返回ack,此时broker leader宕机,那么生产者将不会收到ack。此时生产者会重试发送消息,这将会导致消息在broker集群中存在多条,同一消息也会被消费者消费多次。
|
||||
- At-most-once semantics:如果生产者在向broker发送消息时,若出现等待ack超时或发生异常情况后,不进行重试发送,那么消息将最多被发送给broker一次,此时不会出现消息被重复发送的情况。
|
||||
- exactly-once:即使生产者多次发送相同的消息到broker集群,也只会有一条消息被传递给消费者。exactly-once语义要求生产者、broker、消费者的协同才能实现。
|
||||
|
||||
### apache kafka idempotence
|
||||
kafka生产者支持幂等性操作,即使相同的消息被多次发送给broker,broker也只会向分区中写入一条消息。除此之外,开启生产者的`enable.idempotence=true`属性,还代表生产者发送到同一分区的消息是顺序的。
|
||||
|
||||
生产者实现幂等性的原理和tcp类似,每次被批量发送给broker集群的消息都含有一个序列号,broker将会使用序列号进行去重操作。但是,和tcp不同的是,该序列号会持久化到replicate log中,故而即使broker leader宕机,重新成为leader的broker也能知道是否接受到的消息是重复发送。
|
||||
|
||||
并且,kafka生产者开启幂等性带来的开销很小,仅仅为批量提交的消息附加的序列号。
|
||||
|
||||
### 事务-在多个分区之间进行原子写入
|
||||
通过事务api,kafka支持在多个分区之间原子的进行写入。事务特性支持生产者在向broker集群多个分区批量发送消息时,要么发送的所有消息都能被消费者可见,要么发送的消息对消费者都不可见。
|
||||
|
||||
事务特性允许在事务内对消息进行消费、处理、提交消息处理生成的结果的同时,在同一事务中对消息消费的offset进行提交。通过事务特性,在消息需要消费->处理->提交生成结果的场景下,能够支持exactly-once。
|
||||
|
||||
#### transactional message & non-transactional message
|
||||
分区中存储的消费,可以分为事务消息和非事务消息。事务消息,又可以分为进行中事务的消息,事务提交成功的消息,事务提交失败的消息。
|
||||
|
||||
消费者在消费消息时,可以设置`isolation.level`:
|
||||
- read_committed:在设置该隔离级别后,消费者只能消费非事务消息或事务提交成功后的消息。`poll`接口在该隔离级别时,会返回LSO之前所有的消息(LSO值第一个活跃事务对应消息的offset - 1)。任何offset位于LSO之后的消息都将会被broker保留,直到活跃的事务被提交。故而,在活跃事务提交前,消费者无法读取到LSO之后的消息。
|
||||
- read_uncommitted:设置为该隔离级别之后,消费者可以读取到所有消息,即使该消息关联的事务尚未被提交,**或消息关联事务已经被废弃(aborted)**。
|
||||
|
||||
默认情况下,`isolation.level`的值为read_uncommitted。
|
||||
|
||||
|
||||
446
mq/kafka/kafka.md
Normal file
446
mq/kafka/kafka.md
Normal file
@@ -0,0 +1,446 @@
|
||||
- [Apache Kafka](#apache-kafka)
|
||||
- [Introduction](#introduction)
|
||||
- [Topic and Logs](#topic-and-logs)
|
||||
- [Distribution](#distribution)
|
||||
- [Producers](#producers)
|
||||
- [Consumers](#consumers)
|
||||
- [multi-tenancy(多租户)](#multi-tenancy多租户)
|
||||
- [Guarantees](#guarantees)
|
||||
- [Kafka Storage System](#kafka-storage-system)
|
||||
- [Quick Start](#quick-start)
|
||||
- [Start the server](#start-the-server)
|
||||
- [Create a topic](#create-a-topic)
|
||||
- [Send Some Message](#send-some-message)
|
||||
- [Start a consumer](#start-a-consumer)
|
||||
- [Setting multi-broker cluster](#setting-multi-broker-cluster)
|
||||
- [APIS](#apis)
|
||||
- [Producer API](#producer-api)
|
||||
- [Consumer API](#consumer-api)
|
||||
- [Stream API](#stream-api)
|
||||
- [Admin API](#admin-api)
|
||||
- [Configuration](#configuration)
|
||||
- [Broker Configs](#broker-configs)
|
||||
- [Broker Configs 动态更新](#broker-configs-动态更新)
|
||||
- [动态更新Password Configs](#动态更新password-configs)
|
||||
- [在启动broker之前更新zookeeper password config](#在启动broker之前更新zookeeper-password-config)
|
||||
- [Design](#design)
|
||||
- [Motivation](#motivation)
|
||||
- [Persistence](#persistence)
|
||||
- [filesystem store structure](#filesystem-store-structure)
|
||||
- [Constant Time Suffices](#constant-time-suffices)
|
||||
- [效率](#效率)
|
||||
- [small I/O](#small-io)
|
||||
- [byte copying](#byte-copying)
|
||||
- [端对端的批量压缩](#端对端的批量压缩)
|
||||
- [生产者](#生产者)
|
||||
- [负载均衡](#负载均衡)
|
||||
- [异步发送](#异步发送)
|
||||
- [消费者](#消费者)
|
||||
- [push vs pull](#push-vs-pull)
|
||||
- [consumer position](#consumer-position)
|
||||
- [writing to an external system](#writing-to-an-external-system)
|
||||
- [静态成员](#静态成员)
|
||||
- [消息传递语义](#消息传递语义)
|
||||
- [复制](#复制)
|
||||
- [分区备份](#分区备份)
|
||||
- [broker节点的活跃状态定义](#broker节点的活跃状态定义)
|
||||
- [ISR](#isr)
|
||||
- [消息提交的定义](#消息提交的定义)
|
||||
- [acks](#acks)
|
||||
- [最小写入副本数](#最小写入副本数)
|
||||
|
||||
|
||||
# Apache Kafka
|
||||
## Introduction
|
||||
Apache Kafka是一个分布式的stream平台,具有如下三个核心功能:
|
||||
- 发布和订阅record stream,类似于消息队列或企业消息系统
|
||||
- 通过一种容错的持久化方式对record stream进行存储
|
||||
- 当record stream产生时对其进行处理
|
||||
|
||||
Kafka通常应用于如下两类应用:
|
||||
- 构建实时的流数据管道,应用或系统之间传递的数据是可靠传输的
|
||||
- 构建实时stream应用,应用会传输流数据并且对流数据进行响应
|
||||
|
||||
Kafka核心观念:
|
||||
- Kafka可以作为集群在一台或多台服务器上运行,服务器可以跨多个数据中心
|
||||
- kafka在`topic`的分类中存储stream record
|
||||
- 每条record都含有一个key、一个value和一个时间戳
|
||||
|
||||
Kafka具有四类核心API:
|
||||
- Producer API:允许应用发布records stream到一个或多个kafka topic中
|
||||
- Consumer API:允许应用订阅一个或多个topic,并且处理被订阅topic的record流
|
||||
- Stream API:Stream API允许应用像stream processor一样,从一个或多个topic消费input stream并向一个或多个output topic产生output stream,能够有效的将输入流转化为输出流
|
||||
- Connector API:可以用于构建和运行一个可重用的生产者或消费者,生产者或消费者会将Kafka topic现存的应用或data system。**例如,一个关系型数据库的connector可以捕获对数据库表中的所有变动
|
||||
|
||||
在client和server之间的交流通过一个简单、高性能、语言无关的tcp协议进行。该tcp协议拥有版本,并且新的版本会兼容旧的版本。
|
||||
### Topic and Logs
|
||||
Topic是被发布record的类别或是源名称。在Kafka中,一个Topic可以有0个、1个或多个消费者订阅。
|
||||
对于每个topic,kafka集群都会维护一个分区日志,每个分区日志都是一个有序的、不可变的record序列,record序列被不断添加到分区的尾部。分区中的每条record都分配了一个序列号`offset`,序列号将唯一标识分区中的每条record。
|
||||
Kafka集群将会把所有已发布的record进行持久化,无论其是否已经被消费,record的保留期是可配置的。**例如,如果将保留策略设置为2天,在消息被发布的两天之内,record都是可消费的,但是两天之后record将会被丢弃并且释放空间。**
|
||||
Kafka即使数据大小不同,性能也是恒定的,存储大量数据并不会影响Kafka的性能,故而长时间存储数据并不会带来性能的问题。
|
||||
实际上,每个消费者保存的元数据是Kafka log的偏移量,偏移量由消费者控制。正常情况下,消费者会在读取记录后线性推进offset,但是offset完全由消费者控制,故而消费者可以按其想要的任何顺序对消息进行消费。**例如,消费者可以将offset设置为旧的位置并且对已经被消费的record进行重复消费。**
|
||||
|
||||
日志中分区的目的如下:
|
||||
- 允许日志扩展到适合单个server的大小。每个独立的分区必须适合分区位于的服务器,但是一个topic可以含有多个分区,故而topic可以处理任意数量的数据
|
||||
- 日志分区某种意义上充当了并行的单元
|
||||
### Distribution
|
||||
log分区分布在Kafka集群的服务器上,每个服务器处理对共享分区的数据和请求。每个分区都跨可配置数量的服务器进行复制,从而增加容错性。
|
||||
每个分区都有一台server作为“主机”,并由0或多台其他server作为“从机”。“主机”会处理所有对该分区的读写请求,而“从机”只是会被动的复制主机。如果“主机”宕机,那么“从机”会自动成为新的“主机”。每台服务器都充当了部分分区的“主机”和其他分区的“从机”,从而负载在集群中间是均衡的。
|
||||
> 传统的主从复制,例如redis主从复制,读操作分布在主机和从机之间,而写操作只针对主机,针对多读少写的场景能在集群之间分担读压力
|
||||
> 而对于kafka集群,通过分区来实现负载均衡,每个分区都分布在多台server上,而读写操作全部发生在主机上,从机只作为主机宕机后的备份。而一个Topic可分为多个分区,故而通过分区实现了负载在集群服务器之间的均衡。
|
||||
### Producers
|
||||
生产者发送record数据到topic。生产者可以选择将record发送到topic中的哪个分区。可以通过轮询的方式来将record分配到不同的分区。
|
||||
### Consumers
|
||||
消费者通过***consumer group name***的属性来标记自己。对于每条发布到topic中的记录,都会被发布到订阅了该topic的所有consumer group中,对于每个consumer group,record都会被发送给consumer group中的一个消费者实例。消费者实例可以在不同的进程上或不同的机器上。
|
||||
如果所有的消费者实例都位于一个consumer gorup中,那么record会在所有的消费者实例之间进行负载均衡。
|
||||
如果所有的消费者实例位于不同的consumer group中,那么record会被广播给所有的consumer group。
|
||||
> 通常的,topic含有较少数量的consumer group,每个consumer group都是一个“逻辑订阅者”。每个consumer group都有许多的消费者实例组成,从而实现容错性和可拓展性。
|
||||
> 上述consumer group整体作为一个订阅者,**在此语义中订阅者并非是单个进程,而是一个由消费者实例组成的集群**
|
||||
|
||||
在Kafka中,“消费”操作的实现方式是通过将分区根据消费者实例进行划分,故而每个实例在任何时间点对于其分配到的分区都是唯一的消费者。
|
||||
> 一个消费者实例可能会被分配到多个分区,但是任何一个分区,其对应的消费者实例只能有一个
|
||||
维护consumer group中成员的过程是由Kafka协议动态处理的,如果新的消费者实例加入到consumer group中,它们会接管从其他group成员中获取到的分区;如果现有消费者实例宕机,那么宕机实例的分区将会被分配给其他的消费者实例。
|
||||
### multi-tenancy(多租户)
|
||||
可以将部署Kafka作为多租户的解决方案。可以通过配置哪些topic可以产生和消费数据来启用多租户。这些操作也支持配额。
|
||||
### Guarantees
|
||||
在高层,Kafka可以提供如下保证:
|
||||
- 一个生产者实例发送给特定topic分区的消息会按消息发送的顺序添加到topic分区中
|
||||
- 一个消费者实例将会按照消息存储在分区log中的顺序消费消息
|
||||
- 对于一个复制因子为N的主题,其最多可以容忍N-1台服务器宕机而不会丢失提交给日志的任何记录
|
||||
### Kafka Storage System
|
||||
写入到Kafka的数据将会被持久化到磁盘中,并被复制到从机以解决容错。Kafka允许生产者等待ack返回,在数据被完全的被复制并且持久化之前,生产者向Kafka的写入操作都不会被认为完成。
|
||||
Kafka使用的磁盘结构具有很好的拓展性,不管有50K或是50T的数据写入到服务器,Kafka的性能都相同。
|
||||
由于严格的存储策略和允许客户端控制其读写位置,可以将Kafka看作一种特殊目的的分布式文件系统,该文件系统具有高性能、低延时,并且容错性高,主从复制。
|
||||
## Quick Start
|
||||
### Start the server
|
||||
Kafka使用了Zookeeper,在启动Kafka之前必须启动一个zookeeper实例。
|
||||
启动zookeeper命令:
|
||||
```shell
|
||||
> bin/zookeeper-server-start.sh config/zookeeper.properties
|
||||
[2013-04-22 15:01:37,495] INFO Reading configuration from: config/zookeeper.properties (org.apache.zookeeper.server.quorum.QuorumPeerConfig)
|
||||
...
|
||||
```
|
||||
启动zookeeper之后可以启动kafka实例:
|
||||
```shell
|
||||
> bin/kafka-server-start.sh config/server.properties
|
||||
[2013-04-22 15:01:47,028] INFO Verifying properties (kafka.utils.VerifiableProperties)
|
||||
[2013-04-22 15:01:47,051] INFO Property socket.send.buffer.bytes is overridden to 1048576 (kafka.utils.VerifiableProperties)
|
||||
...
|
||||
```
|
||||
### Create a topic
|
||||
如下命令会创建一个名为”test“的topic,该topic含有一个分区和一个复制:
|
||||
```shell
|
||||
> bin/kafka-topics.sh --create --bootstrap-server localhost:9092 --replication-factor 1 --partitions 1 --topic test
|
||||
```
|
||||
可以通过如下命令列出现存的topic:
|
||||
```shell
|
||||
> bin/kafka-topics.sh --list --bootstrap-server localhost:9092
|
||||
test
|
||||
```
|
||||
除了手动创建topic,还可以将broker配置为如果record发布到一个不存在topic,该不存在topic会自动被创建。
|
||||
### Send Some Message
|
||||
可以通过命令行将文件或是标准输入中的内容发送到Kafka集群,默认情况下,每一行都会作为一个独立的消息被发送:
|
||||
```shell
|
||||
> bin/kafka-console-producer.sh --broker-list localhost:9092 --topic test
|
||||
This is a message
|
||||
This is another message
|
||||
```
|
||||
### Start a consumer
|
||||
Kafka同样有一个命令行消费者,可以将消息内容输出到标准输出:
|
||||
```shell
|
||||
> bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic test --from-beginning
|
||||
This is a message
|
||||
This is another message
|
||||
```
|
||||
### Setting multi-broker cluster
|
||||
为每个broker创建一个config文件:
|
||||
```shell
|
||||
> cp config/server.properties config/server-1.properties
|
||||
> cp config/server.properties config/server-2.properties
|
||||
```
|
||||
为每个broker单独设置配置文件
|
||||
```vim
|
||||
config/server-1.properties:
|
||||
broker.id=1
|
||||
listeners=PLAINTEXT://:9093
|
||||
log.dirs=/tmp/kafka-logs-1
|
||||
|
||||
config/server-2.properties:
|
||||
broker.id=2
|
||||
listeners=PLAINTEXT://:9094
|
||||
log.dirs=/tmp/kafka-logs-2
|
||||
```
|
||||
`broker.id`属性对集群中的每个节点都是唯一且永久的名称。
|
||||
通过如下命令可以再开启两个kafka节点:
|
||||
```shell
|
||||
> bin/kafka-server-start.sh config/server-1.properties &
|
||||
...
|
||||
> bin/kafka-server-start.sh config/server-2.properties &
|
||||
...
|
||||
```
|
||||
通过如下方式可以创建一个复制因子为3的新topic:
|
||||
```shell
|
||||
> bin/kafka-topics.sh --create --bootstrap-server localhost:9092 --replication-factor 3 --partitions 1 --topic my-replicated-topic
|
||||
```
|
||||
通过如下命令行,可以获知集群和各个节点的信息:
|
||||
```shell
|
||||
> bin/kafka-topics.sh --describe --bootstrap-server localhost:9092 --topic my-replicated-topic
|
||||
Topic:my-replicated-topic PartitionCount:1 ReplicationFactor:3 Configs:
|
||||
Topic: my-replicated-topic Partition: 0 Leader: 1 Replicas: 1,2,0 Isr: 1,2,0
|
||||
```
|
||||
对于每个分区,leader都是随机选择的,并且,在leader宕机之后会有一台从机自动的成为leader
|
||||
|
||||
## APIS
|
||||
Kafka包含5个核心api:
|
||||
1. Producer API:允许应用向Kafka集群的Topic发送stream数据
|
||||
2. Consumer API:允许应用丛Kafka集群的Topic中读取数据
|
||||
3. Streams API:允许在input topic和output topic之间传递stream数据
|
||||
4. Connect API:允许实现连接器,连接器会不断从源系统或应用拉拉取数据到Kafka或从Kafka推送数据到系统或者应用
|
||||
5. Admin API:允许管理和查看topic,broker和其他Kafka对象
|
||||
|
||||
### Producer API
|
||||
要使用Producer API,需要添加如下maven依赖:
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>org.apache.kafka</groupId>
|
||||
<artifactId>kafka-clients</artifactId>
|
||||
<version>2.4.1</version>
|
||||
</dependency>
|
||||
```
|
||||
### Consumer API
|
||||
要使用Consumer API,需要添加如下maven依赖:
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>org.apache.kafka</groupId>
|
||||
<artifactId>kafka-clients</artifactId>
|
||||
<version>2.4.1</version>
|
||||
</dependency>
|
||||
```
|
||||
### Stream API
|
||||
要使用Stream API,需要添加如下maven依赖:
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>org.apache.kafka</groupId>
|
||||
<artifactId>kafka-streams</artifactId>
|
||||
<version>2.4.1</version>
|
||||
</dependency>
|
||||
```
|
||||
### Admin API
|
||||
要使用Admin API,需要添加如下maven依赖:
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>org.apache.kafka</groupId>
|
||||
<artifactId>kafka-clients</artifactId>
|
||||
<version>2.4.1</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
## Configuration
|
||||
### Broker Configs
|
||||
基础的配置如下:
|
||||
- broker.id
|
||||
- log.dirs
|
||||
- zookeeper
|
||||
> zookeeper.connect
|
||||
> 通过`hostname:port`以字符串的形式来指定zookeeper连接,hostname和port为zookeeper server对应的域名和端口。如果要指定zookeeper集群,可以通过`hostname1:port1,hostname2:port2,hostname3:port3`的形式来指定多个hosts。
|
||||
|
||||
#### Broker Configs 动态更新
|
||||
从Kafka版本1.1往后,一些broker config可以被更新而不需要重启broker。可以在`Broker Config`条目的`Dynamic Update Mode`栏查看是否该config栏是否允许动态更新:
|
||||
- read-only:如果要更新该条目,需要重新启动broker
|
||||
- per-broker:可以对每个broker进行动态更新
|
||||
- cluster-wide:可以在集群的范围内进行动态更新
|
||||
|
||||
如下命令会修改broker 0的配置:
|
||||
```shell
|
||||
> bin/kafka-configs.sh --bootstrap-server localhost:9092 --entity-type brokers --entity-name 0 --alter --add-config log.cleaner.threads=2
|
||||
```
|
||||
如下命令会返回broker 0的动态配置:
|
||||
```shell
|
||||
> bin/kafka-configs.sh --bootstrap-server localhost:9092 --entity-type brokers --entity-name 0 --describe
|
||||
```
|
||||
如果要删除一个覆盖的配置并且将配置值回滚为静态配置或者默认值,可以使用如下命令:
|
||||
```shell
|
||||
> bin/kafka-configs.sh --bootstrap-server localhost:9092 --entity-type brokers --entity-name 0 --alter --delete-config log.cleaner.threads
|
||||
```
|
||||
一些配置可以设置为集群默认的配置,在整个集群中都维护为一致的值。集群中所有的broker都会处理cluster default update,如下命令会更新集群中所有broker的log cleaner threads:
|
||||
```shell
|
||||
> bin/kafka-configs.sh --bootstrap-server localhost:9092 --entity-type brokers --entity-default --alter --add-config log.cleaner.threads=2
|
||||
```
|
||||
通过如下命令可以输出集群范围内的默认配置:
|
||||
```shell
|
||||
> bin/kafka-configs.sh --bootstrap-server localhost:9092 --entity-type brokers --entity-default --describe
|
||||
```
|
||||
所有可以在集群层面配置的条目都可以针对单个broker配置。如果一个条目在多个条目都有配置,那么会按照如下优先级:
|
||||
- 存储在zookeeper中的针对单个broker的配置
|
||||
- 存储在zookeeper中的针对集群的配置
|
||||
- server.properties中静态配置的条目
|
||||
- Kafka的默认值
|
||||
|
||||
#### 动态更新Password Configs
|
||||
动态更新的password config值在存储到zookeeper之前会被加密。如果想要启用password config的动态更新,broker config中的`password.encoder.secret`条目必须要在server.properties中被配置。
|
||||
如果broker重启,那么用于password加密的old secret必须在静态broker config中的`password.encoder.old.secret`条目中被配置,而新的secret将会在`password.encoder.secret`条目配置。当容器启动时,当前zookeeper中存储的所有动态password config都会通过new secret重新加密。
|
||||
|
||||
#### 在启动broker之前更新zookeeper password config
|
||||
kafka-config.sh支持动态broker config在启动broker之前通过zookeeper被更新。如果在alter命令行中包含了任何password config,那么broker config条目`password.encoder.secret`必须被指定。可以指定额外的加密参数,password encoder config将不会被存储在zookeeper中。
|
||||
示例如下所示:
|
||||
```shell
|
||||
> bin/kafka-configs.sh --zookeeper localhost:2181 --entity-type brokers --entity-name 0 --alter --add-config
|
||||
'listener.name.internal.ssl.key.password=key-password,password.encoder.secret=secret,password.encoder.iterations=8192'
|
||||
```
|
||||
如上命令中,listener.name.internal.ssl.key.password将会使用指定的指定的encoder config被加密存储在zookeeper中,encoder.secret和encoder.iterations则不会被存储在zookeeper中。
|
||||
|
||||
## Design
|
||||
### Motivation
|
||||
Kafka可以具有如下用例:
|
||||
- Kafka用于高吞吐量来应对大量事件流的场景
|
||||
- Kafka能够处理大量的数据积压,从而支持离线系统的周期性事件加载
|
||||
- Kafka也支持处理低延迟交付
|
||||
- Kafka支持分区和分布式存储
|
||||
### Persistence
|
||||
#### filesystem store structure
|
||||
Kafka非常依赖于文件系统来存储和缓存消息。通常观点中,磁盘效率被认为很慢。但是,磁盘的效率是依赖于其存储结构的,一个拥有良好结构设计的磁盘存储能够提供和网络一样快速的性能。
|
||||
对于磁盘性能,磁盘读写的吞吐量和磁盘查找所带来的延迟会存在极大差异,磁盘查找所带来的延迟会极大影响磁盘读写的性能。在磁盘阵列的测试中,线性写入的性能接近600MB/sec,但是随机写入的性能只在100k/sec左右,差距近乎6000倍。线性写入是所有模式中最可预测的,并且由操作系统进行了优化。一个现代的操作系统提供了read-ahead和write-behind技术,该技术可以以大块的形式预取数据,并且将较小的逻辑写入组合成大的物理写入来优化性能。
|
||||
为了弥补随机读写和线性读写的性能差异,现代操作系统在将内存作为磁盘缓存方面变得十分激进。现代操作系统会将当前所有的空闲内存转化为磁盘缓存,并且在回收内存时只会带来很小的性能开销。所有的磁盘读写操作都会经过内存,除非使用direct IO。
|
||||
更重要的是,Kafka是基于JVM构建的,JVM具有如下特性:
|
||||
- java对象带来的额外内存开销十分大,通常是存储数据的两倍或更多
|
||||
- 随着堆内存中数据的增加,java GC变得越来越复杂和缓慢
|
||||
|
||||
由于JVM结构存在以上缺陷,故而使用filesystem并依赖页面缓存的方案优于维护一个内存缓存或其他结构:
|
||||
> 通过将所有可获取的空闲内存作为缓存,至少将可获取缓存大小增加了一倍;而通过紧凑的字节结构而不是对象结构,又几乎将可获取的缓存大小增加了一倍
|
||||
|
||||
在32GB内存的机器中,缓存大小至多可至28-20GB,并且没有GC开销。甚至,在服务重启时,之前的cache仍然会有效。而对于进程内的内存缓存,在服务重启之后,缓存的rebuild会带来巨大的开销(10GB缓存可能带来10分钟的开销);如果重启之后不执行rebuild操作,那么重启之后的初始性能将会受到很大的影响。
|
||||
通过使用操作系统提供的page cache,关于维护缓存和文件系统一致性的逻辑全都包含在操作系统中,无需在代码中再次实现,故而这也大大简化了code的实现。
|
||||
#### Constant Time Suffices
|
||||
在消息系统中,使用的持久化数据结构通常是对每个消费者队列关联一个BTree或其他通用的随机访问数据结构,用于维护消息的元数据。BTree通常会带来较高的开销。即使BTree操作的时间复杂度是O(logN),虽然O(logN)通常被认为是等于恒定事件,但是对于磁盘操作来说并不是这样。磁盘查找一次需耗费大概10ms,并且每个磁盘同一时刻只能够进行一个查找,故而并行性也会收到限制。故而,即使是少量的磁盘查找也会带来巨大的开销。当存储系统将非常快的缓存操作和非常慢的物理磁盘操作混合在一起时,即使数据增加固定量的缓存,树结构的性能开销通常也是超线性的:例如,将data增加两倍时,性能开销将增加不止两倍。
|
||||
持久队列可以建立在简单的文件读取和向文件末尾添加数据上,logging解决方案通常就是这样的。该结构具有如下优势:所有操作都是O(1)的,并且读操作不会阻塞写操作,读操作之间也不会相互阻塞。这也具有明显的性能优势,读写性能完全和数据量大小分离。
|
||||
具有几乎无限的磁盘空间,而不会带来任何性能损失,意味着可以提供一些其他消息系统没有的特性。例如,在Kafka中,即使消息已经被消费者消费,也不会像其他消息系统中一样马上将消息删除,而是可以选择将消息保存一段时间之后再删除(例如一周)。
|
||||
### 效率
|
||||
在消息系统中,低下的效率通常是由糟糕的磁盘访问模式造成的,通常这种效率低下有两种形式:
|
||||
- 太多的small I/O操作
|
||||
- 过多的字节复制
|
||||
#### small I/O
|
||||
small I/O操作通常发生在客户端和服务端之间或服务端自己的持久化操作之中。为了避免这些,Kafka的协议是基于“消息集合”的抽象构建的,“消息集合”会将消息组合在一起。“消息集合”允许网络针对消息集合进行请求,并且分担网络往返的开销,而不是一次仅仅发送一条消息。服务端会一次性将消息块添加到log中,消费者也会一次性获取大的线性块。
|
||||
该优化会将速度提高几个数量级,批量处理会导致更大的网络包,更大的连续磁盘操作,连续的内存块等,所有这些都将使Kafka将随机消息的突发流转化为流向消费者线性写入。
|
||||
#### byte copying
|
||||
另一个造成效率低下的问题是过多的字节复制。为了避免过多的字节复制,Kafka采用了一个标准的二进制消息格式,该消息格式由生产者,broker,消费者进行共享,故而消息在生产者、broker、消费者之间进行传递时无需进行任何修改。
|
||||
由broker维护的消息日志其本身只是目录下的一些文件,每个文件中都填充了一些列的消息集合,消息写入磁盘的格式和生产者、消费者使用的消息格式完全相同。
|
||||
现代unix操作系统提供了一个高度优化的code path用于从页面缓存中传输数据到socket,在linux中其通过sendfile system call完成。
|
||||
将数据从文件中传输到socket的公共路径:
|
||||
- 操作系统将数据从磁盘中读入到内核空间的页面缓存中
|
||||
- 应用将数据从内核空间读取到用户空间的缓冲区中
|
||||
- 应用将数据写回到内核空间的套接字缓冲区中
|
||||
- 操作系统将socket buffer中的数据复制到NIC缓冲区中,并发送给网络
|
||||
|
||||
上述通用路径显然是低效的,其中有四次复制和两次系统调用,通过sendfile,允许操作系统直接从页面缓存向网络直接发送数据,重复的复制操作可以被避免。故而,在优化后的路径中,只需要将数据复制到NIC buffer中。
|
||||
在一个Topic存在多个消费者的用例中,使用上述的优化,数据只需要被复制到页面缓存中一次,而不是被存储到内存中并且每次读取时都复制到用户空间中。
|
||||
### 端对端的批量压缩
|
||||
在一些用例场景下,瓶颈并不是cpu或disk而是网络带宽。用户可以通过压缩单条消息来节省带宽,但是这样可能会导致压缩率很低,因为重复总是在相同类型消息之间产生(例如多条JSON格式的消息具有相同的field name)。如果要提升压缩效率,需要将多条消息一起压缩,而不是压缩单条消息。
|
||||
Kafka支持批量的高效压缩,消息可以被批量压缩并且并且被发送给服务端。该批量消息可以以压缩的格式写入到磁盘中,并且只会被消费者解压缩。
|
||||
Kafka支持GZIP,LZ4,ZStandard压缩协议。
|
||||
### 生产者
|
||||
#### 负载均衡
|
||||
生产者会直接将数据发送给作为该分区leader的broker,并不会经过任何的中间路由层。为了帮助生产者实现这些,所有的Kafka节点都能响应对元数据的请求,元数据中包含哪些server还存活以及topic中分区对应的leader都在哪。返回的元数据可以正确的告知生产者应该将请求发送到哪个server。
|
||||
客户端会控制将消息发送到哪一个分区。这可以随机完成,实现一种随机负载均衡,或者通过语义分区函数完成。Kafka提供了一个接口用于语义分区,可以允许用户来提供一个key进行分区,并使用该key散列到一个分区(也可以覆盖分区函数)。
|
||||
#### 异步发送
|
||||
批量处理是提高性能的重要因素之一,为了支持批量处理,Kafka生产者会在内存中累计数据并在单个请求中发送更大的批数据。批处理可以被配置为累计不超过固定数量的消息或不超过特定界限的延迟(例如64K,10ms)。
|
||||
### 消费者
|
||||
Kafka消费者会向其消费分区的leader broker发送fetch请求。消费者在每次请求中指定log offset并从该位置接收一块log。consumer对于位置具有绝对的控制权,故而在需要时可以执行rewind操作对数据进行重新消费。
|
||||
#### push vs pull
|
||||
在Kafka的实现中,和大多数消息系统一样,data从生产者push到broker,消费者从broker中pull data。
|
||||
#### consumer position
|
||||
大多数消息系统中,会在broker端通过metadata来追踪哪些消息已经被消费,当消息被实际传送给consumer或被consumer所ack时,会记录消息此时的状态。使用该方式会存在如下问题:
|
||||
- 如果在消息被传递给consumer后,立马将该消息标记为已消费:
|
||||
- 若consumer接收到消息后,如果处理该消息失败,那么该消息会丢失
|
||||
- 如果在消息被consumer成功处理,且consumer向broker发送ack后,broker才将消息标记为已消费:
|
||||
- 如果consumer成功处理该消息,但是向broker发送ack失败,那么该消息会被重复传递并消费,这会导致消息的重复消费
|
||||
- broker需要为消息维护多个状态(已发送未消费/已消费/未发送),这将会带来性能问题
|
||||
|
||||
相比于通过ack来保证消息被消费者正确处理,kafka通过其他方式来对其进行处理。kafka topic由一系列分区组成,在一个时间点,每个分区都只会由一个consumer group中的一个消费者实例消费。每个消费者实例只需要维护一个integer,作为ack确认的等价物,维护position的开销相当小。
|
||||
|
||||
并且,通过offset方案,还支持通过rewind对旧消息进行重复消费,在一些场景下该特性十分有用。
|
||||
|
||||
|
||||
##### writing to an external system
|
||||
当消息处理的逻辑中存在`将output向外部系统写入时`,需要协调consumer position和`向外部系统写入output`两个操作。通常,在实现该协调过程时,会引入两阶段提交,保证`consumer position的存储`和`consumer output`的存储是一致、原子的,要么都发生,要么都不发生。
|
||||
|
||||
但是,通常情况下,会`在相同的位置存储offset和output`,这样可以无需引入两阶段提交,尤其在外部系统并不支持两阶段提交协议的情况下。
|
||||
|
||||
> 例如,在消息消费时,如果要将消息消费结果写入到mysql数据库中时,也可以同时在数据库中记录`(topic, partition, consumer group, last consumed offset)`的信息,此时,可以通过数据库事务来保证offset的更新和消息结果的写入同时成功/失败。
|
||||
>
|
||||
> 并且,在consumer实例启动、rebalance分配到分区时,都会从数据库中读取offset,这样能够保证消息的exactly-once消费。
|
||||
|
||||
|
||||
|
||||
#### 静态成员
|
||||
静态成员用于提升基于group rebalance协议应用的性能。rebalance协议依赖于组协调器,组协调器用于为组成员分配实体id。组协调器产生的实体id是短暂的,并且当成员重启并重新加入到组之后,实体id会发生变化。对于基于consumer的app,上述“动态成员”可能会造成在应用周期性重新启动时大量任务会被新分配给其他消费者实例。
|
||||
由于上述缺点,Kafka group管理协议允许组成员提供持久的实体id,基于这些id,组成员身份保持不变,故而rebalance不会被触发。
|
||||
如果想要使用静态成员:
|
||||
- 将`ConsumerConfig#GROUP_INSTANCE_ID_CONFIG`设置为一个该组中唯一的值,不与同组其他消费者实例重复
|
||||
#### 消息传递语义
|
||||
Kafka在生产者和消费者之间提供了如下语义保证:
|
||||
- at most once:消息可能丢失,但是消息永远不会被重复传递
|
||||
- at least once:消息永远不会丢失,但是消息可能被重复传递
|
||||
- Exaclty once:每条被传输一次并且只能被传输一次
|
||||
|
||||
可以将上述问题拆分为两个问题:发送消息的持久性保证和消费消息的保证。
|
||||
Kafka的语义是直截了当的,当消息被发布时,有一个消息被提交到log。只要消息被提交,且有一个包含消息所写入分区的broker存活,那么消息就不会被丢失。
|
||||
> 如果一个生产者尝试发布一条消息,并且遭遇到了网络错误,那么生产者无法确定网络错误发生在消息提交之前或之后。
|
||||
|
||||
在0.11.0.0之前,如果一个生产者没有接收到消息已经成功被提交的相应,那么生产者会重新发送该消息。这种实现提供了at-least-once的语义,如果前一次消息已经被写入到log中,那么重新发送的重复消息仍会被重复写入到log中。
|
||||
自从0.11.0.0开始,kafka生产者还支持幂等传输选项,幂等传输会保证消息的重复发送并不会导致log中存在重复条目。为了实现幂等传输,broker会分配给每个生产者一个id,并且通过和每条消息一起发送的序列号来判断去除重复消息。**同时,从0.11.0.0开始,生产者支持以类似事务的方式向多个topic分区发送多条消息:要么所有的消息被成功写入,要么所有的消息都未被写入。**
|
||||
但是,并非是所有的用例都需要如此严格的保证。对于延迟敏感的场景,生产者可以指定其期望持久性级别。如果生产者指定其需要等待消息被提交,那么可能会需要10ms;但是生产者也可以指定异步发送消息或只需要等待到leader broker收到该信息(followers不用收到)。
|
||||
在消费者的视角看来,所有的复制分区中,log和offset都完全相同,消费者控制log的位置。如果消费者永远不会宕机,那么可以消费者可以将position存储在内存中;但是如果消费者失败,若想让该topic分区被一个新的消费者实例接管,新的消费者实例需要选定一个position开始处理接管分区。
|
||||
在消费者处理消息并更新position时,有如下选择:
|
||||
- `at-most-once`:读取消息,然后将position保存,最后处理消息。这样,如果消费者在保存完position之后崩溃,但此时消息尚未处理完成,那么其他消费者实例接管分区时会从被保存的position开始,即使之前的消息没有被正确处理。
|
||||
- `at-least-once`:读取消息,处理消息,然后将position保存。这样,如果消费者在处理完消息但是尚未保存position时崩溃,那么接管该分区的消费者实例可能重复消费先前的数据。这就是`at-least-once`语义。在很多场景下,更新操作是幂等的,故而消息
|
||||
- `exactly-once`:当写入到外部系统时,限制是需要协调消费者位置和实际存储内容。通常情况下,实现该功能需要在消费者position的存储系统和消费者输出的存储系统之间引入两阶段提交。但是,可以通过将position和consumer output存储在一个位置来简化该过程。因为任何想要写入的系统可能并不支持两阶段提交。
|
||||
> Kafka在Kafka Streams中支持`exactly-once`传输,并且在处理topics之间数据时通过使用transactional producer/consumer来提供`exactly-once`功能。
|
||||
> 其他情况下,kafka默认保证at-least-once传输,但是也允许用户实现at-most-once传输,通过关闭生产者的重试功能并且在处理数据之前提交position变动。
|
||||
|
||||
### 复制
|
||||
#### 分区备份
|
||||
kafka会将topic分区复制到多台mq server上,mq server的数量可配置。(可以针对单个topic来设置replication factor的数量)。在其他server上保存副本允许当某台server发生故障宕机后,仍然可以从其他server上存储的副本中获取信息。
|
||||
|
||||
复制针对的是topic分区。在kafka中,每个分区都有一个leader和零或多个follower。leader加上follower的数量构成了replication factor。所有的写操作都会针对leader分区,而读操作则是可以针对leader或follower分区。通常情况下,分区比broker要多,leader分区分布在broker中。follower分区上的日志和leader分区上的日志都相同,offset和日志内容都相同(在特定时间点,leader尾部可能有一些尚未被复制到follower的消息)。
|
||||
|
||||
follower从leader分区消息消息,就像一个普通的消费者一样,并且将从leader处消费的消息追加到自己分区的尾部。
|
||||
|
||||
#### broker节点的活跃状态定义
|
||||
就像大多数的分布式系统一样,自动的故障容灾需要精确定义一个节点的“活跃”状态。在kafka中,存在一个节点单独来负责管理集群中broker的注册,该节点被称之为controller。broker的活跃状态需要满足如下要求:
|
||||
- broker需要和controller维持一个active session来接收常规的元数据更新
|
||||
- 作为follower的broker需要从leader处同步leader的写操作变动,从而保持分区数据和leader处相同
|
||||
|
||||
对于使用zookeeper的集群,在broker节点初始化与zookeeper的连接会话时,会在zookeeper中创建一个节点,如果broker没能在`zookeeper.session.timeout.ms`时间内向zookeeper发送心跳包,那么broker就会丢失和zookeeper的会话,zookeeper中对应的节点也会被删除。controller会根据zookeeper watch注意到节点的删除,并且将该节点对应的broker标记为离线。
|
||||
|
||||
#### ISR
|
||||
满足上述“活跃”要求的节点会被成为"in sync"状态。而leader会跟踪那些处于“in sync”状态的副本,处于“in sync”状态的副本集合被称之为ISR(in sync replias,ISR中包含leader和follower).
|
||||
|
||||
如果ISR中的节点不再满足”活跃“对应的两条要求,那么该节点将会从ISR中被移除。例如,如果一个follower宕机,那么controller将会注意到zookeeper中节点的删除,并且将该broker中ISR中移除。
|
||||
|
||||
另外,**如果一个节点仍然处于活跃状态,但是离同步leader的数据有很长的延迟,那么leader将会将该节点从ISR中移除**。延迟的最长时间通过`replica.lag.time.max.ms`来配置。如果在`replica.lag.time.max.ms`时间内副本没能通过leader到日志结尾的数据,那么副本节点将会从ISR中被移除。
|
||||
|
||||
> `replica.lag.time.max.ms`的默认值为30s
|
||||
|
||||
#### 消息提交的定义
|
||||
在某分区对应ISR中所有的副本都将消息追加到它们的log后,则可以认为该消息被提交。故而,消费者并无需担心分区的leader宕机后消息会丢失的问题,因为消息在提交前已经持久化到其分区副本中。
|
||||
|
||||
作为生产者,可以在发送消息时决定是否等待消息被提交,这需要在等待提交所带来的延迟和不等待提交所带来的消息丢失风险中进行权衡。是否等待提交取决于生产者的ack设置。
|
||||
|
||||
#### acks
|
||||
可以针对生产者设置acks模式。acks模式可设置为如下值:
|
||||
- 0:此时生产者发送消息时无需等待broker的ack,消息会被直接添加到缓冲区中,并且消息被认为已发送成功。此时并无法保证消息被发送给broker,并且重试设置也不会起作用
|
||||
- 1:这种情况下会等待来自于leader的ack,保证消息被写入到leader分区中。但是,如果leader broker返回ack后立马宕机,其他副本broker并没有同步leader分区数据,那么消息将会被丢失
|
||||
- -1(all):这种情况下,leader broker会等待ISR队列中所有的副本broker都对该消息返回ack后才对生产者返回ack。此时只要当前集群中还存在一个in-sync副本,那么消息就不会丢失
|
||||
|
||||
> `acks`的默认值是-1(all)
|
||||
|
||||
> acks=-1的情况下会产生重复数据的问题,如果发送消息后,消息已经全部存储到所有的broker,但是再尚未ack的情况下leader宕机,那么生产者会重新发送消息,此时消息会被重复存储在消息队列中
|
||||
|
||||
#### 最小写入副本数
|
||||
当acks设置为-1(all)时,topic可以设置一个最小写入的副本数,通过配置`min.insync.replicas`,可以对最小写入副本数进行配置。即使消息已经同步到所有ISR副本后,如果同步数目小于该值(同步数目包含leader),消息也无法被视为提交
|
||||
|
||||
> `min.insync.replicas`的值默认为1. 该值只有当生产者ack模式设置为-1(all)时才起作用
|
||||
|
||||
> kafka保证一条消息只要被提交,只要有一个in-sync-replica处于活跃状态,那么消息就不会被丢失
|
||||
|
||||
|
||||
2169
mybatis/mybatis.md
2169
mybatis/mybatis.md
File diff suppressed because it is too large
Load Diff
22
mysql/mysql底层/Undo logging and History System.md
Normal file
22
mysql/mysql底层/Undo logging and History System.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# Undo logging and History System
|
||||
innodb存储引擎实现了多版本并发控制(MVCC),故而不同用户在访问数据时,会读取到数据不同版本的快照(取决于事务的隔离级别)。
|
||||
|
||||
通过MVCC,其为用户提供了一致的视图。undo logging和history system则是实现mvcc的底层机制。
|
||||
|
||||
## 当innodb修改时,会保存备份
|
||||
在innodb针对mvcc的实现中,如果某一条记录发生了修改,那么当前记录的`修改前版本`将会作为`undo record`被记录到undo log中。
|
||||
|
||||
通过undo log,用户能够将修改后的数据还原到修改前的版本。
|
||||
|
||||
对于innodb中的记录,每条记录都会包含其`most recent undo record`的引用,引用被称为`rollback pointer`或`ROLL_PTR`。并且,每条undo record还包含指向其前一条undo record的引用(init record insert除外)。
|
||||
|
||||
综上所述,每条记录都包含一个记录了`all versions of record`的undo record chain。故而,可以通过undo record chain构建记录的所有先前版本数据。
|
||||
|
||||
## Global history and purge operations
|
||||
每条记录都可以通过undo record chain还原到先前的版本,除此之外,在整个数据库层面还有一个全局的history view,该history view被称之为`history list`。当每个事务被提交时,事务关联的`history`将会被链接到global history list中,history顺序按照事务提交顺序。history list主要用用于数据的清理。
|
||||
|
||||
|
||||
innodb会运行一个连续的`清理`process,其主要负责如下内容:
|
||||
- 对`delete-marked records`进行`实际删除`
|
||||
- 释放undo log page,并且将其从history list中移除,使得其可以被reuse
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# binlog 日志
|
||||
* ## binlog(二进制日志)
|
||||
* 定义:binlog日志文件也称之为变更日志文件,记录了数据库记录的所有DDL、DML等数据库更新事件的语句,但是不包括任何没有更新数据的语句。
|
||||
* 用途:
|
||||
* 数据恢复:如果mysql数据库进程意外停止,可以通过binlog中的记录来恢复数据库中的数据
|
||||
# binlog 日志
|
||||
* ## binlog(二进制日志)
|
||||
* 定义:binlog日志文件也称之为变更日志文件,记录了数据库记录的所有DDL、DML等数据库更新事件的语句,但是不包括任何没有更新数据的语句。
|
||||
* 用途:
|
||||
* 数据恢复:如果mysql数据库进程意外停止,可以通过binlog中的记录来恢复数据库中的数据
|
||||
* 数据复制:可以通过binlog来实现mysql数据库的主从一致
|
||||
@@ -1,49 +1,49 @@
|
||||
# 多版本并发控制
|
||||
* ## 多版本并发控制解决的问题:
|
||||
* 在多个事务并发访问资源时,可能会发生并发安全问题。为了解决多个事务同时对资进行读写的问题,可以采用如下解决方案:
|
||||
* 多事务同时对资源进行读取:并发安全
|
||||
* 多事务同时对资源进行写操作:非并发安全,可以对写操作进行加锁来解决并发安全问题
|
||||
* 多事务同时对资源进行读和写操作:非并发安全
|
||||
1. 对读操作和写操作都加锁:性能较差
|
||||
2. 通过MVCC机制来解决并发读写的问题,对写操作加锁,并用MVCC机制来使读操作读到先前的值
|
||||
* ## 多版本并发控的概念:
|
||||
* MVCC通过数据库行的多个版本管理来实现数据库的并发控制。当一个写事务正在更新数据行的值时,通过MVCC机制,可以在另一个事物对数据行进行读操作时读取到被写事务更新之前的值。
|
||||
* 通过MVCC机制,可以更好的解决事务在读-写操作同时发生时的性能问题。MVCC可以避免在写事务时另一个读事务必须等待当前写事务释放排他锁,而是可以通过MVCC读取资源被写事务修改之前的值。
|
||||
|
||||
* ## mysql中读操作的种类:
|
||||
* 当前读:读取数据库中当前的值,为了解决并发安全问题,需要对读操作进行加锁操作,可以尝试加排他锁或者共享锁,当当前事务对资源进行写操作时,读事务会阻塞直到写事务释放锁:
|
||||
```mysql
|
||||
# 为select语句加上共享锁
|
||||
select * from user where... lock in share mode
|
||||
|
||||
# 为select语句加上排他锁
|
||||
select * from user where... for update
|
||||
```
|
||||
* 快照读:mysql中默认select语句的读取方式,当当前事务对资源进行读操作时,如果另一个事务正在对资源进行写操作,那么读操作并不会阻塞而是会读取资源被写事务修改之前的值
|
||||
|
||||
* ## MVCC原理
|
||||
* 隐藏字段:对于使用innodb存储引擎的表,其聚簇索引包含两个隐藏字段:
|
||||
1. trx_id:每个事务对每条记录进行更改时,该trx_id字段都会记载最后一次修改该行记录的事务id
|
||||
2. roll_pointer:指向undo log中关于修改该条数据的记录
|
||||
* ReadView:ReadView是MVCC机制中事务对数据进行快照读时产生的一个读视图。
|
||||
* ReadView原理:
|
||||
* 当事务开启时,会产生当前数据库系统的一个快照,innodb为每个事务构造了一个数组,用来维护当前系统中活跃事务的ID(活跃事务为当前开启了但是尚未提交的事务id)
|
||||
* ReadView中保存了如下信息:
|
||||
* creator_trx_id:创建当前ReadView的事务id
|
||||
* trx_ids:记录创建失误时当前mysql系统中活跃的事务id集合(已经开始但是还没有被提交的事务id)
|
||||
* up_limit_id:trx_ids中最小的事务id
|
||||
* low_limit_id:表示生成ReadView中当前系统应该分配给下一个事务的id
|
||||
* MVCC仅针对读已提交和可重复读的情况,在读已提交的情况下,事务中每执行一次select操作,ReadView都会重复生成;而在可重复读的隔离级别下,事务仅仅会在第一次select操作时生成ReadView
|
||||
* MVCC细节:
|
||||
* 当select语句想要对一条记录中的数据进行读取时,首先会查看记录的trx_id是是否对当前事务的读操作是可见的,判断事务是否可见的标准如下所示:
|
||||
* 如果记录的trx_id大于low_limit_id,那么说明在创建ReadView时对记录进行修改的事务还没有被创建,当然修改对当前读事务来说是不可见的
|
||||
* 如果trx_id小于up_limit_id,那么说明对该记录进行修改的事务id小于创建ReadView时最小的活跃事务id,在创建ReadView时修改记录的事物已经被提交,修改对当前事务来说可见
|
||||
* 如果trx_id位于up_limit_id和low_limit_id之间,那么:
|
||||
* trx_id如果与trx_ids中保存的某个活跃事务id相同,那么说明在创建ReadView时修改事务的id尚未被提交,修改对当前失误不可见
|
||||
* 如果trx_id与trx_ids中每个活跃事物id都不相同,那么修改事务在创建ReadView事物之时已经被提交,修改对当前事务可见
|
||||
* 根据上述规则,如果想要读取的记录trx_id对当前事务来说可见,那么获取该事务id所对应的数据值;如果trx_id对当前ReadView来说不可见,那么沿着roll_pointer沿着undo log向前寻找,知道找到对当前ReadView可见的事务id;如果undo log中所有的事务id对当前ReadView来说都不可见,那么对当前事物来说数据表中该条记录并不可见
|
||||
|
||||
* ## MVCC与幻读问题
|
||||
* MVCC仅在读已提交和可重复读的情况下起作用,而关于幻读问题,MVCC仅在可重复读的隔离级别下解决。
|
||||
* 在可重复读的隔离级别下,ReadView仅仅在第一次select语句时生成,故而在同一事务中多次读取之间,其他事务插入了新数据,那么该新数据对应的trx_id在readView创建时应该处于活跃或者未创建的状态,故而对ReadView所对应事物来说,即使其他事务插入了新数据,那么新插入的数据也不可见。
|
||||
# 多版本并发控制
|
||||
* ## 多版本并发控制解决的问题:
|
||||
* 在多个事务并发访问资源时,可能会发生并发安全问题。为了解决多个事务同时对资进行读写的问题,可以采用如下解决方案:
|
||||
* 多事务同时对资源进行读取:并发安全
|
||||
* 多事务同时对资源进行写操作:非并发安全,可以对写操作进行加锁来解决并发安全问题
|
||||
* 多事务同时对资源进行读和写操作:非并发安全
|
||||
1. 对读操作和写操作都加锁:性能较差
|
||||
2. 通过MVCC机制来解决并发读写的问题,对写操作加锁,并用MVCC机制来使读操作读到先前的值
|
||||
* ## 多版本并发控的概念:
|
||||
* MVCC通过数据库行的多个版本管理来实现数据库的并发控制。当一个写事务正在更新数据行的值时,通过MVCC机制,可以在另一个事物对数据行进行读操作时读取到被写事务更新之前的值。
|
||||
* 通过MVCC机制,可以更好的解决事务在读-写操作同时发生时的性能问题。MVCC可以避免在写事务时另一个读事务必须等待当前写事务释放排他锁,而是可以通过MVCC读取资源被写事务修改之前的值。
|
||||
|
||||
* ## mysql中读操作的种类:
|
||||
* 当前读:读取数据库中当前的值,为了解决并发安全问题,需要对读操作进行加锁操作,可以尝试加排他锁或者共享锁,当当前事务对资源进行写操作时,读事务会阻塞直到写事务释放锁:
|
||||
```mysql
|
||||
# 为select语句加上共享锁
|
||||
select * from user where... lock in share mode
|
||||
|
||||
# 为select语句加上排他锁
|
||||
select * from user where... for update
|
||||
```
|
||||
* 快照读:mysql中默认select语句的读取方式,当当前事务对资源进行读操作时,如果另一个事务正在对资源进行写操作,那么读操作并不会阻塞而是会读取资源被写事务修改之前的值
|
||||
|
||||
* ## MVCC原理
|
||||
* 隐藏字段:对于使用innodb存储引擎的表,其聚簇索引包含两个隐藏字段:
|
||||
1. trx_id:每个事务对每条记录进行更改时,该trx_id字段都会记载最后一次修改该行记录的事务id
|
||||
2. roll_pointer:指向undo log中关于修改该条数据的记录
|
||||
* ReadView:ReadView是MVCC机制中事务对数据进行快照读时产生的一个读视图。
|
||||
* ReadView原理:
|
||||
* 当事务开启时,会产生当前数据库系统的一个快照,innodb为每个事务构造了一个数组,用来维护当前系统中活跃事务的ID(活跃事务为当前开启了但是尚未提交的事务id)
|
||||
* ReadView中保存了如下信息:
|
||||
* creator_trx_id:创建当前ReadView的事务id
|
||||
* trx_ids:记录创建失误时当前mysql系统中活跃的事务id集合(已经开始但是还没有被提交的事务id)
|
||||
* up_limit_id:trx_ids中最小的事务id
|
||||
* low_limit_id:表示生成ReadView中当前系统应该分配给下一个事务的id
|
||||
* MVCC仅针对读已提交和可重复读的情况,在读已提交的情况下,事务中每执行一次select操作,ReadView都会重复生成;而在可重复读的隔离级别下,事务仅仅会在第一次select操作时生成ReadView
|
||||
* MVCC细节:
|
||||
* 当select语句想要对一条记录中的数据进行读取时,首先会查看记录的trx_id是是否对当前事务的读操作是可见的,判断事务是否可见的标准如下所示:
|
||||
* 如果记录的trx_id大于low_limit_id,那么说明在创建ReadView时对记录进行修改的事务还没有被创建,当然修改对当前读事务来说是不可见的
|
||||
* 如果trx_id小于up_limit_id,那么说明对该记录进行修改的事务id小于创建ReadView时最小的活跃事务id,在创建ReadView时修改记录的事物已经被提交,修改对当前事务来说可见
|
||||
* 如果trx_id位于up_limit_id和low_limit_id之间,那么:
|
||||
* trx_id如果与trx_ids中保存的某个活跃事务id相同,那么说明在创建ReadView时修改事务的id尚未被提交,修改对当前失误不可见
|
||||
* 如果trx_id与trx_ids中每个活跃事物id都不相同,那么修改事务在创建ReadView事物之时已经被提交,修改对当前事务可见
|
||||
* 根据上述规则,如果想要读取的记录trx_id对当前事务来说可见,那么获取该事务id所对应的数据值;如果trx_id对当前ReadView来说不可见,那么沿着roll_pointer沿着undo log向前寻找,知道找到对当前ReadView可见的事务id;如果undo log中所有的事务id对当前ReadView来说都不可见,那么对当前事物来说数据表中该条记录并不可见
|
||||
|
||||
* ## MVCC与幻读问题
|
||||
* MVCC仅在读已提交和可重复读的情况下起作用,而关于幻读问题,MVCC仅在可重复读的隔离级别下解决。
|
||||
* 在可重复读的隔离级别下,ReadView仅仅在第一次select语句时生成,故而在同一事务中多次读取之间,其他事务插入了新数据,那么该新数据对应的trx_id在readView创建时应该处于活跃或者未创建的状态,故而对ReadView所对应事物来说,即使其他事务插入了新数据,那么新插入的数据也不可见。
|
||||
* 而在读已提交的隔离级别下,ReadView在每次读操作的情况下都会被创建,故而在两次读操作之间,如果新的数据被插入,那么新插入的数据对后一次读操作创建的ReadView来说是已经提交的数据,是可见的。故而MVCC在读已提交的隔离级别下并不能够解决幻读的问题。
|
||||
@@ -1,64 +1,64 @@
|
||||
# mysql锁
|
||||
* ## msyql中锁的分类
|
||||
* 从数据库操作的类型划分,mysql中的锁可以分为读锁/共享锁和写锁/排他锁
|
||||
* 读锁(S锁,Share)
|
||||
* 写锁(X锁,Exclusive)
|
||||
* 写锁和读锁示例
|
||||
```mysql
|
||||
# 为语句加上写锁
|
||||
select * from table_name for udpate
|
||||
|
||||
# 为查询语句加上读锁
|
||||
select * from table_name lock in share mode
|
||||
# or
|
||||
select * from table_name for share(mysql 8.0新增语法)
|
||||
```
|
||||
* 在获取读锁或者写锁之后,只有当事务结束之后,获取到的读锁或者写锁才会被释放。在innodb中,for update或者for share语句只会为查询匹配的行数据上锁,并且,当事务结束或者回滚之后,会释放为数据行所加的排他锁或者共享锁
|
||||
* 在对mysql中数据进行写操作(UPDATE、INSERT、DELETE)时,会通过如下方式加锁:
|
||||
* DELETE操作:对mysql表中数据执行DELETE操作,需要为要删除数据添加排他锁(X锁)
|
||||
* UPDATE操作:对mysql表中数据执行UPDATE操作分如下情况:
|
||||
* update操作不改变记录的键值,且更新的列在更新前后占用的空间未发生变化:
|
||||
* 此时会在B+数中定位到该条记录,并且为该条记录添加X锁,在修改完成之后释放X锁
|
||||
* update操作没有修改键值,但是更新前后该条记录的某一列占用空间发生了变化:
|
||||
* 此时,会先对原先的记录加上X锁,并进行DELETE操作,并且在DELETE操作后,通过INSERT操作并添加隐式锁来插入修改后的数据
|
||||
* update操作修改了键值:
|
||||
* 同样,也会通过先DELETE再INSERT的操作来更新数据
|
||||
* INSERT操作:通过添加隐式锁的操作来保证mysql中多事务的并发安全
|
||||
* 从mysql数据库的粒度来对锁进行划分
|
||||
* 表锁:
|
||||
* 表级锁会锁住mysql中的整张表,其粒度较大,受其影响并发性能较低
|
||||
* 表级锁是mysql中最基本的加锁策略,并不依赖于存储引擎,不管是innodb还是myisam都支持表级锁
|
||||
* 表级锁的使用场景:
|
||||
* 通过的select或者update、delete、insert操作innodb并不会加表级锁,但是对于alter table、drop table这类ddl语句对表的结构进行修改时,需要对其表的元数据进行加锁(MDL),对元数据加锁的过程中,想要对该表中数据进行select、delete、update、insert的操作会被阻塞
|
||||
* 对表锁进行加锁的语句:
|
||||
```mysql
|
||||
# 对表锁进行加锁
|
||||
# 在对表进行上锁操作后,无法再去读取其他未上锁的表
|
||||
lock tables table_name read/write
|
||||
|
||||
# 对上锁的表进行释放操作
|
||||
unlock tables;
|
||||
```
|
||||
* 对于myisam存储引擎,读操作之前会为涉及到的表加上读锁,并且在读操作执行完成之后释放上锁的表;而对于写操作,myisam存储引擎会在写操作执行之前为涉及到的表加上写锁。反之,innodb存储引擎在对数据进行查找和修改时不会在表级别加锁,而是会在更细的粒度上为行数据进行加锁,以此来提高程序的并发性能。
|
||||
* 元数据锁(MDL):对于表结构的修改(DDL操作),会对其元数据锁进行加锁,而对于该表的增删改操作,则是会默认加上元数据的读锁。故而在对表结构进行修改时,想要对表中数据进行増删改操作需要获取其元数据的读锁,此时对表数据的増删改操作会被阻塞。
|
||||
* 意向锁:意向锁通常用来简化行级锁和表级锁是否兼容的判断操作。如果为一个表中某条数据加上行级锁,那么innodb存储引擎会自动的为该行记录所在的页和表加上页级和表级的意向锁,那么当接下来有事务想要对该表进行表级加锁操作时,就无需查看该表中所有数据来判断是否存在表中数据的锁和表级锁冲突。只需判断该表的意向锁和想要加上的表级锁是否冲突即可。
|
||||
* 行锁
|
||||
* 相对与表锁,行锁的粒度更小,故而其并发度更高,并发性能更好。但是行锁可能会造成死锁问题,加锁的开销也更大。
|
||||
* 是否支持行锁取决与存储引擎,比如myisam不支持行锁但是innodb却支持行锁
|
||||
* 行锁种类:
|
||||
* 记录锁:记录锁针对于单条记录,在一个事物中,如果存在update、delete等修改操作,其默认会获得想要修改的那条记录的记录锁,并且在执行修改操作之后才会被释放。若一个事务中对某条记录调用update操作,那么直到当前事务提交或者回滚前,若其他事务想要修改同一行记录,会进入阻塞状态,知道持有记录写锁的退出才能继续执行。
|
||||
* 间隙锁:间隙锁对记录的范围进行加锁。间隙锁是不冲突的,多个事务可以同时持有某一个范围的间隙锁。用间隙锁或者mvcc机制都能够解决死锁问题。但是间隙锁可能会导致死锁问题,如果多个事务各自持有自己范围的间隙锁,并同时向对方持有间隙锁的范围插入数据,此时两个事务都会等待对方释放间隙锁,在这种情况下会发生死锁问题。
|
||||
* 临键锁(next-key lock):在锁住某条记录的同时也锁定某个范围内的数据,使之无法被其他事务插入数据
|
||||
* 页锁:在页面的粒度上进行上锁
|
||||
* 在粒度上,不同层级的所的数量是有上限的,例如innodb,其优先会选用行锁来进行加锁。当行锁数量过多,占用空间超过表空间上限时,会进行锁升级的行为。行锁会升级为页面锁,此时锁的粒度会增加,锁花费的开销也会变小,但是粒度变大后并发性能也会变低。
|
||||
* 通过对待锁的方式进行划分
|
||||
* 乐观锁:乐观锁假设每次对数据进行修改时,其他事务不会访问数据,故而不会对数据正真的加锁,只是会在修改时检查数据是否被其他事务修改过。
|
||||
* 乐观锁的实现方式:
|
||||
* cas
|
||||
* 版本号机制
|
||||
* 悲观锁:
|
||||
* 悲观锁假设一个事务在对数据进行修改时,其他事务也会对数据进行修改,故而在每次修改数据时都会对要修改的数据进行加锁,悲观锁是通过mysql内部提供的锁机制来实现的
|
||||
* 通过加锁方式进行划分:
|
||||
* 隐式锁:隐式锁通常用于插入操作。在某一个事务在对所记录进行插入操作时,如果其他事务想要访问该条记录,会查看最后修改该条记录的事务id是否是活跃的事务,如果是活跃事务,那么其会帮助插入事务创建一个X锁并且让自己进入阻塞状态。
|
||||
* 隐式锁是一种延迟加锁的机制,只有当有其他事务想要访问未提交事务插入的记录时,隐式锁才会被创建。该机制能够有效减少创建锁的数量。
|
||||
# mysql锁
|
||||
* ## msyql中锁的分类
|
||||
* 从数据库操作的类型划分,mysql中的锁可以分为读锁/共享锁和写锁/排他锁
|
||||
* 读锁(S锁,Share)
|
||||
* 写锁(X锁,Exclusive)
|
||||
* 写锁和读锁示例
|
||||
```mysql
|
||||
# 为语句加上写锁
|
||||
select * from table_name for udpate
|
||||
|
||||
# 为查询语句加上读锁
|
||||
select * from table_name lock in share mode
|
||||
# or
|
||||
select * from table_name for share(mysql 8.0新增语法)
|
||||
```
|
||||
* 在获取读锁或者写锁之后,只有当事务结束之后,获取到的读锁或者写锁才会被释放。在innodb中,for update或者for share语句只会为查询匹配的行数据上锁,并且,当事务结束或者回滚之后,会释放为数据行所加的排他锁或者共享锁
|
||||
* 在对mysql中数据进行写操作(UPDATE、INSERT、DELETE)时,会通过如下方式加锁:
|
||||
* DELETE操作:对mysql表中数据执行DELETE操作,需要为要删除数据添加排他锁(X锁)
|
||||
* UPDATE操作:对mysql表中数据执行UPDATE操作分如下情况:
|
||||
* update操作不改变记录的键值,且更新的列在更新前后占用的空间未发生变化:
|
||||
* 此时会在B+数中定位到该条记录,并且为该条记录添加X锁,在修改完成之后释放X锁
|
||||
* update操作没有修改键值,但是更新前后该条记录的某一列占用空间发生了变化:
|
||||
* 此时,会先对原先的记录加上X锁,并进行DELETE操作,并且在DELETE操作后,通过INSERT操作并添加隐式锁来插入修改后的数据
|
||||
* update操作修改了键值:
|
||||
* 同样,也会通过先DELETE再INSERT的操作来更新数据
|
||||
* INSERT操作:通过添加隐式锁的操作来保证mysql中多事务的并发安全
|
||||
* 从mysql数据库的粒度来对锁进行划分
|
||||
* 表锁:
|
||||
* 表级锁会锁住mysql中的整张表,其粒度较大,受其影响并发性能较低
|
||||
* 表级锁是mysql中最基本的加锁策略,并不依赖于存储引擎,不管是innodb还是myisam都支持表级锁
|
||||
* 表级锁的使用场景:
|
||||
* 通过的select或者update、delete、insert操作innodb并不会加表级锁,但是对于alter table、drop table这类ddl语句对表的结构进行修改时,需要对其表的元数据进行加锁(MDL),对元数据加锁的过程中,想要对该表中数据进行select、delete、update、insert的操作会被阻塞
|
||||
* 对表锁进行加锁的语句:
|
||||
```mysql
|
||||
# 对表锁进行加锁
|
||||
# 在对表进行上锁操作后,无法再去读取其他未上锁的表
|
||||
lock tables table_name read/write
|
||||
|
||||
# 对上锁的表进行释放操作
|
||||
unlock tables;
|
||||
```
|
||||
* 对于myisam存储引擎,读操作之前会为涉及到的表加上读锁,并且在读操作执行完成之后释放上锁的表;而对于写操作,myisam存储引擎会在写操作执行之前为涉及到的表加上写锁。反之,innodb存储引擎在对数据进行查找和修改时不会在表级别加锁,而是会在更细的粒度上为行数据进行加锁,以此来提高程序的并发性能。
|
||||
* 元数据锁(MDL):对于表结构的修改(DDL操作),会对其元数据锁进行加锁,而对于该表的增删改操作,则是会默认加上元数据的读锁。故而在对表结构进行修改时,想要对表中数据进行増删改操作需要获取其元数据的读锁,此时对表数据的増删改操作会被阻塞。
|
||||
* 意向锁:意向锁通常用来简化行级锁和表级锁是否兼容的判断操作。如果为一个表中某条数据加上行级锁,那么innodb存储引擎会自动的为该行记录所在的页和表加上页级和表级的意向锁,那么当接下来有事务想要对该表进行表级加锁操作时,就无需查看该表中所有数据来判断是否存在表中数据的锁和表级锁冲突。只需判断该表的意向锁和想要加上的表级锁是否冲突即可。
|
||||
* 行锁
|
||||
* 相对与表锁,行锁的粒度更小,故而其并发度更高,并发性能更好。但是行锁可能会造成死锁问题,加锁的开销也更大。
|
||||
* 是否支持行锁取决与存储引擎,比如myisam不支持行锁但是innodb却支持行锁
|
||||
* 行锁种类:
|
||||
* 记录锁:记录锁针对于单条记录,在一个事物中,如果存在update、delete等修改操作,其默认会获得想要修改的那条记录的记录锁,并且在执行修改操作之后才会被释放。若一个事务中对某条记录调用update操作,那么直到当前事务提交或者回滚前,若其他事务想要修改同一行记录,会进入阻塞状态,知道持有记录写锁的退出才能继续执行。
|
||||
* 间隙锁:间隙锁对记录的范围进行加锁。间隙锁是不冲突的,多个事务可以同时持有某一个范围的间隙锁。用间隙锁或者mvcc机制都能够解决死锁问题。但是间隙锁可能会导致死锁问题,如果多个事务各自持有自己范围的间隙锁,并同时向对方持有间隙锁的范围插入数据,此时两个事务都会等待对方释放间隙锁,在这种情况下会发生死锁问题。
|
||||
* 临键锁(next-key lock):在锁住某条记录的同时也锁定某个范围内的数据,使之无法被其他事务插入数据
|
||||
* 页锁:在页面的粒度上进行上锁
|
||||
* 在粒度上,不同层级的所的数量是有上限的,例如innodb,其优先会选用行锁来进行加锁。当行锁数量过多,占用空间超过表空间上限时,会进行锁升级的行为。行锁会升级为页面锁,此时锁的粒度会增加,锁花费的开销也会变小,但是粒度变大后并发性能也会变低。
|
||||
* 通过对待锁的方式进行划分
|
||||
* 乐观锁:乐观锁假设每次对数据进行修改时,其他事务不会访问数据,故而不会对数据正真的加锁,只是会在修改时检查数据是否被其他事务修改过。
|
||||
* 乐观锁的实现方式:
|
||||
* cas
|
||||
* 版本号机制
|
||||
* 悲观锁:
|
||||
* 悲观锁假设一个事务在对数据进行修改时,其他事务也会对数据进行修改,故而在每次修改数据时都会对要修改的数据进行加锁,悲观锁是通过mysql内部提供的锁机制来实现的
|
||||
* 通过加锁方式进行划分:
|
||||
* 隐式锁:隐式锁通常用于插入操作。在某一个事务在对所记录进行插入操作时,如果其他事务想要访问该条记录,会查看最后修改该条记录的事务id是否是活跃的事务,如果是活跃事务,那么其会帮助插入事务创建一个X锁并且让自己进入阻塞状态。
|
||||
* 隐式锁是一种延迟加锁的机制,只有当有其他事务想要访问未提交事务插入的记录时,隐式锁才会被创建。该机制能够有效减少创建锁的数量。
|
||||
* 显示锁:可以通过命令查看到的锁,通常会用for update、lock in share mode 或者 update、delete操作会生成显示锁
|
||||
@@ -1,5 +1,5 @@
|
||||
# redo log & undo log
|
||||
* ## undo log
|
||||
* undo log通常用于事务的回滚操作,用来保证事务的原子性。
|
||||
* 每当发生数据修改操作时(update、insert、delete),关于当前修改操作的相反操作会被记录到undo log中,通常用于在回滚时将数据回复到修改之前的值。
|
||||
# redo log & undo log
|
||||
* ## undo log
|
||||
* undo log通常用于事务的回滚操作,用来保证事务的原子性。
|
||||
* 每当发生数据修改操作时(update、insert、delete),关于当前修改操作的相反操作会被记录到undo log中,通常用于在回滚时将数据回复到修改之前的值。
|
||||
* undo log默认被记录到mysql的表空间中,因而对undo log进行追加时,对表中页数据进行修改时也会产生redo log,对undo log的追加会通过fsync操作持久化到redo log中。这样即使在一个事务尚未被提交或是回滚之前,mysql服务器崩溃,下次重启时,也可以通过redo log恢复对数据的修改和undo log的内容,回滚事务时也能将数据恢复到崩溃前尚未被事务修改的状态。
|
||||
@@ -1,24 +1,24 @@
|
||||
# mysql索引
|
||||
* ## mysql索引的分类
|
||||
* 从功能逻辑上对索引进行分类:
|
||||
* 普通索引:只是用于提升查询效率,没有任何的附加约束
|
||||
* 唯一性索引:通过unique关键字可以设定唯一性索引,其会限制该索引值必须是唯一的,但是允许为null
|
||||
* 主键索引:特殊的唯一性索引,在唯一性索引的基础上,主键索引还增加了不为空的约束
|
||||
* 单列索引:作用在一个字段上的索引
|
||||
* 联合索引:作用于多个字段上的索引
|
||||
* ## 索引的创建、删除操作
|
||||
```mysql
|
||||
# 索引的创建方式
|
||||
alter table table_name add [unique/index] idx_name (col_name...)
|
||||
# or
|
||||
create [unique/index] on table_name(col_name...)
|
||||
|
||||
# 索引的删除方式
|
||||
alter table table_name drop index idx_name
|
||||
# or
|
||||
drop index idx_name on table_name
|
||||
```
|
||||
* ## 索引的可见性
|
||||
```mysql
|
||||
# 通过修改索引的可见性,可以比较创建
|
||||
# mysql索引
|
||||
* ## mysql索引的分类
|
||||
* 从功能逻辑上对索引进行分类:
|
||||
* 普通索引:只是用于提升查询效率,没有任何的附加约束
|
||||
* 唯一性索引:通过unique关键字可以设定唯一性索引,其会限制该索引值必须是唯一的,但是允许为null
|
||||
* 主键索引:特殊的唯一性索引,在唯一性索引的基础上,主键索引还增加了不为空的约束
|
||||
* 单列索引:作用在一个字段上的索引
|
||||
* 联合索引:作用于多个字段上的索引
|
||||
* ## 索引的创建、删除操作
|
||||
```mysql
|
||||
# 索引的创建方式
|
||||
alter table table_name add [unique/index] idx_name (col_name...)
|
||||
# or
|
||||
create [unique/index] on table_name(col_name...)
|
||||
|
||||
# 索引的删除方式
|
||||
alter table table_name drop index idx_name
|
||||
# or
|
||||
drop index idx_name on table_name
|
||||
```
|
||||
* ## 索引的可见性
|
||||
```mysql
|
||||
# 通过修改索引的可见性,可以比较创建
|
||||
```
|
||||
File diff suppressed because it is too large
Load Diff
128
mysql/mysql文档/bin log.md
Normal file
128
mysql/mysql文档/bin log.md
Normal file
@@ -0,0 +1,128 @@
|
||||
- [bin log](#bin-log)
|
||||
- [Introduce](#introduce)
|
||||
- [binary log构成](#binary-log构成)
|
||||
- [binary log用途](#binary-log用途)
|
||||
- [binary log variable](#binary-log-variable)
|
||||
- [binlog\_encryption](#binlog_encryption)
|
||||
- [log\_bin](#log_bin)
|
||||
- [binlog\_checksum](#binlog_checksum)
|
||||
- [binary log format](#binary-log-format)
|
||||
- [mysqlbinlog](#mysqlbinlog)
|
||||
- [sync\_binlog](#sync_binlog)
|
||||
- [Binary Logging Format](#binary-logging-format)
|
||||
- [STATEMENT](#statement)
|
||||
- [ROW](#row)
|
||||
- [MIXED](#mixed)
|
||||
- [mysql表更新操作的binary log记录格式](#mysql表更新操作的binary-log记录格式)
|
||||
|
||||
|
||||
# bin log
|
||||
## Introduce
|
||||
### binary log构成
|
||||
binary log由一系列描述数据库变动的事件构成,例如table creation操作或对table data进行的修改操作。
|
||||
|
||||
除上述对数据库造成修改的事件外,binary log中还包含`可能会潜在对数据库造成修改的statement`所对应的事件(例如一个没有匹配任何行的delete语句)。
|
||||
|
||||
binary log中还会包含每条statement更新数据所花费的时间。
|
||||
|
||||
### binary log用途
|
||||
binary log用途主要用于如下方面:
|
||||
- `for replication`:source replication server中的binary log提供了要发送给replicas的数据变动。source replication server会将binary log中包含的消息发送给replcias,replicas则是会重新执行这些变动。
|
||||
- `for data recovery`:某些data recovery操作需要使用binary log。当backup被恢复后,需要对binary log中位于backup之后的时间进行重新执行操作
|
||||
|
||||
binary log不会记录`SELECT`或`SHOW`这些并不会对数据库数据进行修改的动作。如果想要对所有statement做记录,需要使用general query log。
|
||||
|
||||
在运行mysql server时开启binary log通常会令性能有所降低,但是能带来主从复制和数据恢复方面的好处。
|
||||
|
||||
binary log能兼容程序的意外中止,只有已完成的event或transaction才能记录到binary log或从binary log中read back。
|
||||
|
||||
写入binary log中的statement如果包含密码,密码将会被server重写,binary log中不会出现密码的明文。
|
||||
|
||||
### binary log variable
|
||||
#### binlog_encryption
|
||||
binary log和relay log可以被加密,用于保护这些日志文件的数据安全。可以通过`binlog_encryption`变量来设置binary log是否被加密。
|
||||
|
||||
#### log_bin
|
||||
通过`log_bin`可以设置binary log是否被启用,`log_bin`环境变量默认为`ON`,是启用的。
|
||||
|
||||
如果要在启动mysqld时禁用binary log,可以在`my.cnf`中指定`skip-log-bin`或`disable-log-bin`。
|
||||
|
||||
可以通过`--log-bin[=base_name]`来指定binary log file的base name。如果没有指定`--log-bin`选项,那么base name默认为`binlog`(binary log文件名为`binlog.xxxxx`。
|
||||
|
||||
mysqld会在base name之后追加数字将其作为binlog的文件名称,每次创建新的binlog文件时,数字都会递增。mysqld会在如下场景下创建新的binlog文件:
|
||||
- mysql server执行`start`或`restart`操作
|
||||
- server对log进行`flush`操作
|
||||
- 当前log大小达到`max_binlog_size`的大小
|
||||
|
||||
如果在事务中写入大量数据,那么binlog的大小可能会大于`max_binlog_size`,因为同一个事务中的event只会被记录到一个binlog中,不会写入到多个binlog中。
|
||||
|
||||
> #### binary log index
|
||||
> 为了追踪当前mysql使用了哪些binary log文件,mysqld创建了一个`binary log index`文件,其中包含了`binary log`文件的文件名。可以通过`--log-bin-index[=filename]`来指定`log index file`的名称。
|
||||
|
||||
#### binlog_checksum
|
||||
默认情况下,server在输出event到日志时,会同时输出event的长度,并且用event长度来校验是否输出正确。除此之外,可以通过设置`binlog_checksum`来让server在输出event的同时输出event的checksum。在从binlog中read back时,source默认会使用event length,但是可以通过设置`source_verify_checksum`为启用来使用checksum。作为接收方的replica会校验从source接收到的event。
|
||||
|
||||
### binary log format
|
||||
binary log中event目前支持三种格式:
|
||||
- row-based-logging
|
||||
- statement-based-logging
|
||||
- mixed-based-logging
|
||||
|
||||
### mysqlbinlog
|
||||
当想要展示binary log文件中的内容时,可以使用mysqlbinlog工具,如果想要重新执行binlog中对应的statement,该工具将十分有用。
|
||||
|
||||
```bash
|
||||
mysqlbinlog /var/lib/mysql/binlog.000503 | less
|
||||
```
|
||||
通过上述命令可以查看binlog的内容。
|
||||
|
||||
可以通过如下命令来根据binlog内容更新数据库信息:
|
||||
```bash
|
||||
mysqlbinlog log_file | mysql -h server_name
|
||||
```
|
||||
|
||||
binary logging操作会在statement或transaction执行完成后立马被执行,但是logging操作在`锁释放`和`commit提交`之前。这将会保证日志将会按照commit的顺序来打印。
|
||||
|
||||
对非事务(引擎不支持事务)的表进行操作,在执行后会立即将log打印到binary log file中。
|
||||
|
||||
对于事务的表(例如innodb)表,在一个尚未提交的事务中,所有更新操作(update/insert/delete)将会被缓存,直到执行commit操作。在执行commit前,会将缓存中的整个事务都写入到binary log中。
|
||||
|
||||
### sync_binlog
|
||||
默认情况下,在每次写操作后,binary log都会被同步到磁盘中(sync_binlog为1)。如果`sync_binlog`没有被启用,并且系统发生宕机,那么位于binary log末尾的statements可能会丢失。为了保证不发生binlog文件丢失的情况,`sync_binlog`会设置每经过`N`次commit group之后,将binary log同步到disk中。
|
||||
|
||||
在早期的mysql版本中,即使将`sync_binlog`设置为1,也有可能发生table content和binlog content不一致的情况。在使用innodb tables并且mysql server处理了commit statement,那么其会将所有prepared transactions按顺序写入到binary log,并将binary log同步到disk中,并且提交innodb事务。
|
||||
|
||||
但是,在`同步binary log到disk`和`提交innodb事务`之间,server有可能会宕机,在server重启之后transaction会被回滚,但是事务对应event已经被写入到了binary log中,此时会发生binary log和table content不一致的情况。
|
||||
|
||||
上述情况在之前的mysql发行版中已经被解决,只需启用XA两阶段事务即可。在mysql 8.4中,innodb针对两阶段提交的XA事务支持一直是被启用的。
|
||||
|
||||
Innodb在XA事务中对两阶段提交的支持确保了binary log和innodb data files的一致性。在innodb隐式支持了XA两阶段事务之后,如果sync_binlog被设置,那么当server崩溃后重启时,会对未提交事务做回滚,同时会扫描binlog中最后的部分来收集`xid`,从而计算出binary log file中的`最后有效位置`。mysql server会告知innodb完成所有写入到binary log中的prepared transactions,并且将binary log file截取到最后有效位置。这将会保证table content和binary log的一致性。
|
||||
|
||||
## Binary Logging Format
|
||||
在binary log中,会采用如下集中格式来记录消息:
|
||||
### STATEMENT
|
||||
mysql中的复制功能最初是基于sql statement的传播,sql statement将会从source实例发送到replica实例。这种行为被称作`statement-based logging`。
|
||||
|
||||
在启动server时,可以通过指定`--binlog-format=STATEMENT`来使用`statement-based logging`。
|
||||
|
||||
### ROW
|
||||
默认情况下,使用的是`row-based logging`。source实例会在`行数据如何被修改`的维度,向binary log中写入事件。
|
||||
|
||||
在启动server时,可以通过指定`--binlog-format=ROW`来使用`row-based logging`。
|
||||
|
||||
### MIXED
|
||||
在使用mixed-based logging时,`statement-based logging`会被默认使用,但是遇到下列描述的场景时,会自动切换到`row-based-logging`。
|
||||
|
||||
在启动server时,可以通过指定`--binlog-format=MIXED`来使用`mixed-based logging`。
|
||||
|
||||
随着使用的存储引擎不同,logging format也会收到影响和限制。
|
||||
|
||||
### mysql表更新操作的binary log记录格式
|
||||
对数据库中表数据的更改可以通过直接或间接的方式完成:
|
||||
- 直接: `INSERT, UPDATE, DELETE, REPLACE, DO, LOAD DATA, SELECT, and TRUNCATE TABLE`
|
||||
- 间接:`GRANT, REVOKE, SET PASSWORD, RENAME USER`
|
||||
|
||||
对于直接修改mysql表内容的语句,`log format`将会基于`binlog_format`配置。
|
||||
|
||||
对于间接修改表内容的语句,将会忽略`binlog_format`配置,直接以`STATEMENT`的形式写入到binary log中。
|
||||
|
||||
35
mysql/mysql文档/general query log & slow query log.md
Normal file
35
mysql/mysql文档/general query log & slow query log.md
Normal file
@@ -0,0 +1,35 @@
|
||||
- [General Query Log \& Slow Query Log](#general-query-log--slow-query-log)
|
||||
- [Information Written](#information-written)
|
||||
- [General Query Log](#general-query-log)
|
||||
- [Slow Query Log](#slow-query-log)
|
||||
- [设置general query log和slow query log](#设置general-query-log和slow-query-log)
|
||||
- [log\_output](#log_output)
|
||||
- [general\_log](#general_log)
|
||||
- [slow\_query\_log](#slow_query_log)
|
||||
|
||||
|
||||
# General Query Log & Slow Query Log
|
||||
## Information Written
|
||||
### General Query Log
|
||||
general query log 会向log中写入`建立的客户端连接`和`从client接收到的statement`。
|
||||
|
||||
### Slow Query Log
|
||||
slow query log 会向log中写入`花费时间大于 long_query_time 秒的query语句
|
||||
|
||||
## 设置general query log和slow query log
|
||||
当general query log 和 slow query log被启用时,可以将日志写入log文件或`mysql`schema的`general_log`和`slow_log`表中,也可以以上两者都写入。
|
||||
|
||||
### log_output
|
||||
可以通过log_output来指定log ouput的目标。设置log_output并不代表general query log 和 slow query log被启用,如果要启用general query log 和 slow query log,必须显式启用。
|
||||
|
||||
- 如果`log_output`并没有在启动时显式指定,那么其默认值为`FILE`
|
||||
- 在显式指定`log_output`时,其可以指定多个由`,`分隔的值,可选值为`TABLE`, `FILE`, `NONE`。当指定的值中包含`NONE`时,`NONE`优先级最高。
|
||||
|
||||
> 如果要指定写入到`FILE`和`TABLE`,可以按如下方式指定`FILE,TABLE`
|
||||
|
||||
### general_log
|
||||
`general_log`变量控制了是否将general query log输出到`log_output`指定的目的地。
|
||||
|
||||
### slow_query_log
|
||||
`slow_query_log`控制了是否将slow query log输出到`log_output`指定的目的地。
|
||||
|
||||
688
mysql/mysql文档/innodb体系结构.md
Normal file
688
mysql/mysql文档/innodb体系结构.md
Normal file
@@ -0,0 +1,688 @@
|
||||
- [innodb体系结构](#innodb体系结构)
|
||||
- [innodb体系结构](#innodb体系结构-1)
|
||||
- [mysql存储结构](#mysql存储结构)
|
||||
- [内存结构](#内存结构)
|
||||
- [Buffer Pool](#buffer-pool)
|
||||
- [change Buffer](#change-buffer)
|
||||
- [Adaptive hash index](#adaptive-hash-index)
|
||||
- [log Buffer](#log-buffer)
|
||||
- [磁盘存储结构](#磁盘存储结构)
|
||||
- [system tablespace](#system-tablespace)
|
||||
- [后台线程](#后台线程)
|
||||
- [Master Thread](#master-thread)
|
||||
- [IO Thread](#io-thread)
|
||||
- [Purge Thread](#purge-thread)
|
||||
- [Page Cleaner Thread](#page-cleaner-thread)
|
||||
- [内存](#内存)
|
||||
- [缓冲池](#缓冲池)
|
||||
- [缓冲池参数配置](#缓冲池参数配置)
|
||||
- [innodb\_buffer\_pool\_size](#innodb_buffer_pool_size)
|
||||
- [LRU List, Free List, Flush List](#lru-list-free-list-flush-list)
|
||||
- [midpoint](#midpoint)
|
||||
- [page made young](#page-made-young)
|
||||
- [youngs/s \& non-youngs/s](#youngss--non-youngss)
|
||||
- [buffer pool hit rate](#buffer-pool-hit-rate)
|
||||
- [FreeList](#freelist)
|
||||
- [Flush List](#flush-list)
|
||||
- [redo log buffer](#redo-log-buffer)
|
||||
- [checkpoint](#checkpoint)
|
||||
- [缩短数据库恢复时间](#缩短数据库恢复时间)
|
||||
- [缓冲池不够用](#缓冲池不够用)
|
||||
- [redo log不可用](#redo-log不可用)
|
||||
- [LSN](#lsn)
|
||||
- [Fuzzy Checkpoint](#fuzzy-checkpoint)
|
||||
- [checkpoint种类](#checkpoint种类)
|
||||
- [Master Thread Checkpoint](#master-thread-checkpoint)
|
||||
- [FLUSH\_LRU\_LIST\_CHECKPOINT](#flush_lru_list_checkpoint)
|
||||
- [Async/Sync Flush Checkpoint](#asyncsync-flush-checkpoint)
|
||||
- [Dirty Page too Much](#dirty-page-too-much)
|
||||
- [Buffer pool刷新](#buffer-pool刷新)
|
||||
- [innodb\_max\_dirty\_pages\_pct\_lwm](#innodb_max_dirty_pages_pct_lwm)
|
||||
- [innodb\_lru\_scan\_depth](#innodb_lru_scan_depth)
|
||||
- [自适应刷新](#自适应刷新)
|
||||
- [innodb\_adaptive\_flushing\_lwm](#innodb_adaptive_flushing_lwm)
|
||||
- [innodb\_adaptive\_flushing](#innodb_adaptive_flushing)
|
||||
- [Log Buffer](#log-buffer-1)
|
||||
- [innodb\_flush\_log\_at\_trx\_commit](#innodb_flush_log_at_trx_commit)
|
||||
- [innodb\_flush\_log\_at\_timeout](#innodb_flush_log_at_timeout)
|
||||
- [Master Thread](#master-thread-1)
|
||||
- [每秒钟执行一次的操作](#每秒钟执行一次的操作)
|
||||
- [每10秒执行的操作](#每10秒执行的操作)
|
||||
- [change buffer](#change-buffer-1)
|
||||
- [聚簇索引和辅助索引插入顺序](#聚簇索引和辅助索引插入顺序)
|
||||
- [change buffer配置](#change-buffer配置)
|
||||
- [优势](#优势)
|
||||
- [弊端](#弊端)
|
||||
- [`innodb_change_buffering`](#innodb_change_buffering)
|
||||
- [`innodb_change_buffer_max_size`](#innodb_change_buffer_max_size)
|
||||
- [double write](#double-write)
|
||||
- [double write结构](#double-write结构)
|
||||
- [innodb\_doublewrite](#innodb_doublewrite)
|
||||
- [DETECT\_AND\_RECOVERY](#detect_and_recovery)
|
||||
- [DETECT\_ONLY](#detect_only)
|
||||
- [innodb\_doublewrite\_dir](#innodb_doublewrite_dir)
|
||||
- [innodb\_doublewrite\_pages](#innodb_doublewrite_pages)
|
||||
- [innodb\_doublewrite\_files](#innodb_doublewrite_files)
|
||||
- [flush list dobulewrite file](#flush-list-dobulewrite-file)
|
||||
- [lru doublewrite file](#lru-doublewrite-file)
|
||||
- [adaptive hash index(自适应哈希)](#adaptive-hash-index自适应哈希)
|
||||
- [自适应哈希](#自适应哈希)
|
||||
- [自适应哈希构建](#自适应哈希构建)
|
||||
- [访问模式](#访问模式)
|
||||
- [AHI构建要求](#ahi构建要求)
|
||||
- [non-hash searches](#non-hash-searches)
|
||||
- [innodb\_adaptive\_hash\_index](#innodb_adaptive_hash_index)
|
||||
- [异步io](#异步io)
|
||||
- [Sync IO](#sync-io)
|
||||
- [AIO](#aio)
|
||||
- [io merge](#io-merge)
|
||||
- [刷新邻接页](#刷新邻接页)
|
||||
- [启动、关闭和恢复](#启动关闭和恢复)
|
||||
- [innodb\_fast\_shutdown](#innodb_fast_shutdown)
|
||||
- [innodb\_force\_recovery](#innodb_force_recovery)
|
||||
|
||||
|
||||
# innodb体系结构
|
||||
## innodb体系结构
|
||||
innodb体系结构由如下部分构成
|
||||
- innodb存储引擎内存池
|
||||
- 后台线程
|
||||
- 磁盘上的文件数据
|
||||
|
||||
innodb内存池由多个内存块构成,内存快负责如下功能:
|
||||
- 维护进程、线程访问的内部数据结构
|
||||
- 对磁盘上的文件数据进行缓存(加快读取速度),同时缓存对磁盘数据的修改
|
||||
- redo log缓冲
|
||||
|
||||
在innodb存储引擎中,后台线程主要负责刷新内存池中的数据,保证内存缓存为最新状态。此时后台线程还负责将内存池中的修改刷新到磁盘中。
|
||||
|
||||
## mysql存储结构
|
||||
mysql存储结构如图所示
|
||||

|
||||
|
||||
mysql存储结构主要分为两部分:
|
||||
- 内存结构
|
||||
- 磁盘存储结构
|
||||
|
||||
### 内存结构
|
||||
mysql内存结构主要是为了管理和优化数据存储和数据检索,mysql内存结构包含:
|
||||
- 缓冲区(Buffer Pool)
|
||||
- Change Buffer
|
||||
- 自适应哈希索引(Adaptive hash index)
|
||||
- Log Buffer
|
||||
|
||||
#### Buffer Pool
|
||||
buffer pool主要用于缓存经常访问的数据
|
||||
|
||||
#### change Buffer
|
||||
change buffer主要用于缓存针对辅助索引页面的修改。当被修改的辅助索引页面不存在于内存中时,将修改缓存在change buffer中,当后续辅助索引页面被加载到内存中时,会将对该页面的修改进行合并。
|
||||
|
||||
#### Adaptive hash index
|
||||
自适应哈希索引是一个内存结构,用于优化部分读取操作的性能。其提供了一个快速内存检索机制,用于加速对索引页的频繁访问。
|
||||
|
||||
#### log Buffer
|
||||
log buffer是一个内存区域,用于存储将要被写入到transactiion log中的数据。
|
||||
|
||||
### 磁盘存储结构
|
||||
innodb存储引擎将数据持久化存储到磁盘中。磁盘存储的结构如下所示:
|
||||
- system tablespace
|
||||
- file-per-table tablespaces
|
||||
- general tablespaces
|
||||
- undo tablespaces
|
||||
- temporary tablespaces
|
||||
- double write buffer
|
||||
- redo log
|
||||
- undo log
|
||||
|
||||
#### system tablespace
|
||||
system tablespace作为change buffer的存储区域。
|
||||
|
||||
innodb用一个或多个文件来存储system tablespace,默认情况下,mysql会创建名为`ibdata1`的文件。
|
||||
|
||||
`innodb_data_file_path`决定了system tablespace files的大小和数量。
|
||||
|
||||
## 后台线程
|
||||
innodb采用多线程模型,存在多个后台线程,每种后台线程负责不同的后台任务。
|
||||
|
||||
### Master Thread
|
||||
Master Thread主要负责将内存池中的缓存数据异步刷新到磁盘中,包括脏页的刷新、合并`插入缓冲`(insert buffer),undo页的回收等。
|
||||
|
||||
### IO Thread
|
||||
innodb中使用AIO来处理IO请求,IO Thread主要则用来处理异步IO的回调,其中`innodb_read_io_threads`和`innodb_write_io_threads`默认均为4个。
|
||||
|
||||
```sql
|
||||
show variables like 'innodb_%_io_threads'
|
||||
```
|
||||
|Variable_name|Value|
|
||||
|-------------|-----|
|
||||
|innodb_read_io_threads|4|
|
||||
|innodb_write_io_threads|4|
|
||||
|
||||
|
||||
### Purge Thread
|
||||
当事务被提交之后,其对应的undo log不再会被需要,需要Purge Thread来回收已经使用的undo页。
|
||||
|
||||
可以设置多个purge threads,默认情况下,mysql 8的purge threads为4个:
|
||||
```sql
|
||||
show variables like 'innodb_purge_threads'
|
||||
```
|
||||
|Variable_name|Value|
|
||||
|-------------|-----|
|
||||
|innodb_purge_threads|4|
|
||||
|
||||
|
||||
### Page Cleaner Thread
|
||||
page cleaner thread将脏页刷新的任务放到单线程中来完成,从而减轻原Master Thread的工作以及减少对用户查询线程的阻塞。
|
||||
|
||||
## 内存
|
||||
### 缓冲池
|
||||
innodb存储引擎是基于磁盘存储的,并将记录基于页的方式进行管理。为了提升数据库系统的读写性能,通常采用缓冲池来提升数据库的整体性能。
|
||||
|
||||
> ### 缓冲池原理
|
||||
> #### 读缓冲
|
||||
> 在数据库读取磁盘上的页面时,会将丛磁盘上读取到的页存放到缓冲池中,后续再读取相同的页数据时,先丛缓冲池中查找。如果缓冲池中存在该页,直接从缓冲池中读取。
|
||||
> #### 写缓冲
|
||||
> 在数据库针对磁盘上的页数据进行修改时,首先会尝试修改缓冲池中的页数据,并且,缓冲池中的页数据会定期刷新到磁盘中。
|
||||
>
|
||||
> 缓冲池刷新页到磁盘中的操作,由checkpoint机制进行触发,并不会在每次更新缓冲池中的页数据后立马触发。
|
||||
|
||||
### 缓冲池参数配置
|
||||
对于innodb,其缓冲池大小通过`innodb_buffer_pool_size`来配置。默认情况下,`innodb_buffer_pool_size`大小为128M.
|
||||
|
||||
```sql
|
||||
show variables like 'innodb_buffer_pool_size'
|
||||
```
|
||||
|Variable_name|Value|
|
||||
|-------------|-----|
|
||||
|innodb_buffer_pool_size|134217728|
|
||||
|
||||
缓冲池中缓存的数据页类型如下:
|
||||
- 索引页
|
||||
- 数据页
|
||||
- 插入缓冲(insert buffer)
|
||||
- 自适应哈希索引
|
||||
- innodb存储的锁信息
|
||||
- 数据字典信息
|
||||
|
||||
在mysql 8中,innodb可以支持多个缓冲池实例,每个页根据hash值不同被散列到不同缓冲池实例中,这样可以提高应用的并发能力。
|
||||
|
||||
缓冲池实例数量可以通过`innodb_buffer_pool_instances`变量来进行设置,该变量默认值为1.
|
||||
|
||||
#### innodb_buffer_pool_size
|
||||
当修改`innodb_buffer_pool_size`时,操作将会在chunk上执行。chunk size通过`innodb_buffer_pool_chunk_size`来配置。
|
||||
|
||||
令`M = innodb_buffer_pool_chunk size * innodb_buffer_pool_instances`
|
||||
|
||||
`innodb_buffer_pool_size`必须等于`M`或是`M`的整数倍。如果`innodb_buffer_pool_size`不等于M且不是M的整数倍,那么`innodb_buffer_pool_size`将会被自动调整到等于M或是M的整数倍。
|
||||
|
||||
> #### 设置innodb_buffer_pool_size示例
|
||||
> `innodb_buffer_pool_chunk_size`其默认大小为128M,如果将`innodb_buffer_pool_instances`调整为16,那么`M`值为`128M * 16 = 2G`.
|
||||
>
|
||||
> ##### 将innodb_buffer_pool_size设置为8G
|
||||
> 由于`8G = 2G * 4`,那么8G是2G的整数倍,此时该innodb_buffer_pool_size有效
|
||||
>
|
||||
> ##### 将innodb_buffer_pool_size设置为9G
|
||||
> 由于9G不是2G的整数倍,那么innodb_buffer_pool_size将会被自动调整到10G,10G是2G的整数倍
|
||||
|
||||
### LRU List, Free List, Flush List
|
||||
缓冲池是一块由页构成的内存区域。
|
||||
|
||||
innodb中缓冲池通过LRU算法来进行管理,LRU中最频繁使用的页放在最前端,而较少使用的页放在最尾端。当缓冲池中内存已满,不能存放新读取到的页时,会释放LRU尾端较少使用的页。
|
||||
|
||||
|
||||
innodb中,`页大小默认为16KB`.
|
||||
|
||||
#### midpoint
|
||||
在LRU中,新读取的页,`并不放在LRU的首部,而是放在midpoint的位置,该算法被称为midpoint insertion strategy`。
|
||||
|
||||
默认情况下,midpoint位于`5/8`的位置,离首部5/8, 离尾部3/8。在innodb中,将`首部 -> midpoint`部分的页称之为new列表,`midpoint -> 尾部`部分称之为old列表。
|
||||
|
||||
midpoint位置可以通过`innodb_old_blocks_pct`来进行控制,默认情况下该值为`37`。
|
||||
|
||||
```sql
|
||||
show variables like 'innodb_old_blocks_pct'
|
||||
```
|
||||
结果为
|
||||
|
||||
|Variable_name|Value|
|
||||
|-------------|-----|
|
||||
|innodb_old_blocks_pct|37|
|
||||
|
||||
引入midpoint的原因是防止在进行数据扫描等操作时,热点数据被淘汰。
|
||||
|
||||
此外,innodb还引入了`innodb_old_blocks_time`变量来管理LRU列表,代表LRU列表在被读取到midpoint位置后,需要经过多久时间才能被加入到`LRU的new部分`。
|
||||
|
||||
默认情况下,`innodb_old_blocks_time`该值为1000(单位为ms),如果增加该值,会使新页面更快的从缓冲区中淘汰。
|
||||
|
||||
#### page made young
|
||||
在innodb LRU中,使用了midpoint insertion的方法来对LRU列表进行管理。当需要将一个新的页添加到缓冲池时,最近最少被使用的页将会从缓冲池中淘汰,并且新的页将会被插入到midpoint位置。
|
||||
|
||||
midpoint将LRU列表分为了两部分:
|
||||
- young部分:该部分为head到midpoint的部分,默认占列表长度的5/8,用于存放访问频繁的页
|
||||
- old部分:用于存放访问频率较少的页
|
||||
|
||||
默认情况下,LRU通过如下算法管理缓冲池:
|
||||
- 3/8部分属于old sublist部分
|
||||
- 当innodb读取页到缓冲池中时,新读取的页将会被插入到midpoint位置。页可能因为如下原因被读取:
|
||||
- 由用户发起的操作,例如sql查询
|
||||
- 由innodb执行的read ahead操作
|
||||
- 当访问old部分的页时,会`made young`,即将页从old部分移动到young部分的头部。
|
||||
- `如果old部分的页是因为用户发起的查询被读取到缓冲池,那么该页马上会被访问,并且该页会被made young`
|
||||
- `如果该页是被因为innodb的read ahead被读取到缓冲池中,那么该页可能不会马上被访问,甚至知道该页被淘汰也不会被访问`
|
||||
- 随着数据库的运行,LRU中的young部分和old部分页面都会向LRU列表尾部移动,这被称为`老化`。当其他页面触发`made young`操作时:
|
||||
- 如果发生`made young`操作,那么young部分和old部分的节点都会向后移动,发生老化
|
||||
- 如果由新页面被插入到midpoint,那么只有old部分的节点会发生老化
|
||||
|
||||
> #### table scan导致的问题
|
||||
> 默认情况下,因用户发起的查询操作而被读取的页面,挥别马上移动到LRU young部分。例如mysqldump操作或不带where条件的select,都会发起table scan,而table scan会读取大量数据到缓冲区中,此时,会对缓冲区中旧的页进行淘汰。(通常,table scan读取到缓冲区的新数据,后续可能并不会被用到)。
|
||||
>
|
||||
> table scan会快速的将原本处于young部分的页面向后推动到old部分,通常,这些页都是被频繁使用的。
|
||||
|
||||
如上所述,table scan会将大量页读取到缓冲池中,但是这些页只会在短期内被很快的访问过几次(访问会导致该页被made young,移动到young部分),只会就不会再用到,这样会导致大量原本被访问的页被淘汰。
|
||||
|
||||
为了解决该问题,innodb引入了`innodb_old_blocks_time`,在第一次访问了位于old区域的页后的`innodb_old_blocks_time`时间范围内,再次访问该页并不会导致该页被移动到LRU的young部分。`innodb_old_blocks_time`的默认值为1000,增加该值将会使old部分的页触发`made young`的条件变得更苛刻,old部分老化和淘汰的速度也会更快。
|
||||
|
||||
#### youngs/s & non-youngs/s
|
||||
- youngs/s: `youngs/s`该指标代表每秒平均的`因访问old页从而导致made young的访问次数`,如果相同的页发生多次访问,那么所有的访问将都会被计入
|
||||
- non-youngs/s: 该指标代表`访问old页且没有导致made young的访问次数`,如果相同页发生多次访问,那么所有的访问都会被计入
|
||||
|
||||
如果在没有大量扫描发生的情况下,youngs/s的指标值仍然很小,那么可以考虑适当降低`innodb_old_blocks_time`的值,让更多的页更快进入young部分。同样,可以适当增加old部分的百分比,从而可以令old页更慢移动到LRU尾端,更有可能被made young。
|
||||
|
||||
如果在发生大量扫描的情况下,non-youngs/s的指标值仍然不高,那么可以考虑增加`innodb_old_blocks_time`的值,延长old页不触发made young的时间窗口
|
||||
|
||||
#### buffer pool hit rate
|
||||
buffer pool hit rate代表缓冲池中页的命中率,如果命中率高则代表该缓冲池运行良好。如果命中率较低,则需要考虑是否应增加`innodb_old_blocks_time`的值,避免table scan导致缓冲池被污染。
|
||||
|
||||
#### FreeList
|
||||
当数据库实例刚启动时,LRU里列表中并没有任何页,此时页都存放在Free List中。当要从缓冲池中获取页时,首先查看Free List中是否有空闲的页,`如果有则从FreeList中获取,并将该页添加到LRU的midpoint位置`;`若Free List中没有空闲的页,那么将根据LRU算法淘汰LRU尾部的页,将淘汰页的内存空间分配给新的页。`
|
||||
|
||||
#### Flush List
|
||||
LRU中被修改的页称其为`脏页`,在缓冲池中的数据被修改之后,并不会马上就刷新到磁盘中,而是会通过checkpoint将脏页刷回到磁盘中。
|
||||
|
||||
FlushList即是脏页的列表,需要注意的是,脏页既存在于LRU中,又存在于FlushList中,LRU和FlushList都管理的是指向该内存页的指针,LRU管理缓冲,而FlushList管理脏页回刷,二者互不影响。
|
||||
|
||||
### redo log buffer
|
||||
innodb存储引擎的内存区域中,除了有缓冲池之外,还存在redo log buffer。innodb首先会将redo log放入到这个缓冲区,然后会按照一定频率将其刷新到redo log文件。
|
||||
|
||||
redo log缓冲区大小并不需要很大,通常每隔1s会将redo log buffer中的内容刷新到文件中。`innodb_log_buffer_size`负责控制该缓冲区域大小,该参数默认值为`8M`。(mysql 8中实际测试为`16M`)
|
||||
|
||||
redo log buffer在如下场景下会被刷新到文件中:
|
||||
- master thread每秒将缓冲刷新到磁盘文件中
|
||||
- 每个事务提交时都会将redo log buffer刷新到磁盘文件中
|
||||
- 当redo log buffer剩余空间小于一般时,redo log buffer刷新到磁盘文件中
|
||||
|
||||
## checkpoint
|
||||
在innodb中,对数据页的修改都是在缓冲池中完成的,在对内存页进行修改后,内存页和磁盘上的页内容不一致,此时被称为`脏页`。
|
||||
|
||||
在存在脏页时,如果在脏页被刷新到磁盘时,数据库发生宕机,那么将会发生数据修改的丢失。为了解决该问题,innodb采用了`write ahead log`策略,即当事务提交时,先写redo log,再修改页。当发生数据丢失时,可以通过redo log来完成数据的修复。
|
||||
|
||||
checkpoint解决了如下问题:
|
||||
- 缩短数据库恢复时间
|
||||
- 缓冲池不够用时,将脏页刷新到磁盘
|
||||
- redo log不可用时,刷新脏页
|
||||
|
||||
### 缩短数据库恢复时间
|
||||
通过checkpoint,数据库并不需要在宕机重启之后对所有日志执行redo操作,checkpoint之前的页都已经刷新到磁盘。故而,数据库只需要对checkpoint之后的数据执行redo操作即可。
|
||||
|
||||
### 缓冲池不够用
|
||||
当缓冲池不够用时,LRU会进行页面淘汰,此时,被淘汰的页如果是脏页,需要强制执行checkpoint,将脏页刷新到磁盘中。
|
||||
|
||||
### redo log不可用
|
||||
redo log类似循环队列,checkpoint之前的位置都已经被刷新到磁盘中,可以被覆盖使用。如果当redo log文件中所有的内容都未被刷新到磁盘中,那么此时会强制触发checkpoint。
|
||||
|
||||
### LSN
|
||||
LSN(log sequence number)为日志序列号, 是一个全局单调递增的64位整数,`类似于innodb内部的逻辑时钟),是全局唯一的`。
|
||||
|
||||
LSN代表写入redo log的字节数总量,随着redo log的不断产生而单调递增。当发生数据的修改时,会生成redo log,此时LSN也会增加。
|
||||
|
||||
### Fuzzy Checkpoint
|
||||
innodb中实现了fuzzy checkpoint机制,会基于小批量(small batches)来将buffer pool中的页刷新到磁盘中。`并不需要在一次batch中将buffer pool中的页都刷新到磁盘中,否则checkpoint过程会中断用户的sql语句处理`。
|
||||
|
||||
在`crash recovery`的过程中,innodb会查找已经写入到log file中的checkpoint,位于checkpoint之前的内容已经被全部写入到数据库的磁盘文件中,innodb会扫描checkpoint之后的内容,并且将log file中的修改都应用到数据库中。
|
||||
|
||||
> 如果数据库事务发生宕机,且缓冲池中存在部分变更尚未被写入到磁盘,那么数据库实例重启之后,会查看redo log日志中位于`checkpoint`之后的内容,并且将`checkpoint`之后的变更写入到磁盘中。
|
||||
|
||||
### checkpoint种类
|
||||
innodb内部使用Fuzzy Checkpoint进行页的刷新,只会将一部分的脏页刷新到磁盘。
|
||||
|
||||
innodb中存在如下集中类型的fuzzy checkpoint:
|
||||
- Master Thread Checkpoint
|
||||
- FLUSH_LRU_LIST_CHECKPOINT
|
||||
- Async/Sync Checkpoint
|
||||
- Dirty Page Too Much Checkpoint
|
||||
|
||||
#### Master Thread Checkpoint
|
||||
在Master Thread,大约以固定的间隔将一定比例的脏页刷新到磁盘中。Master Thread刷新脏页的操作是异步的,用户线程并不会因之阻塞。
|
||||
|
||||
#### FLUSH_LRU_LIST_CHECKPOINT
|
||||
在innodb中,需要保证LRU中有一定数量的空闲页,如果空闲页少于该数量,那么innodb会将位于LRU尾端的页面淘汰。如果被淘汰页中存在脏页,那么对这些脏页需要执行checkpoint操作。
|
||||
|
||||
`flush_lru_list_checkpoint`操作在单独的page cleaner线程中被执行,可以通过`innodb_lru_scan_depth`来控制lru中可用页的数量,该值默认为`1024`。
|
||||
|
||||
#### Async/Sync Flush Checkpoint
|
||||
定义如下变量:
|
||||
```
|
||||
checkpoint_age = redo_lsn - checkpoint_lsn
|
||||
async_water_mark = 0.75 * total_redo_log_file_size
|
||||
sync_water_mark = 0.9 * total_redo_log_file_size
|
||||
```
|
||||
|
||||
假设定义了2个redo log文件,并且每个文件大小为1G,那么`total_redo_log_file_size`的大小为2G.
|
||||
|
||||
那么,`async_water_mark`为`1.5G`,`sync_water_mark`为`1.8G`.
|
||||
|
||||
- 当`checkpoint_age < async_water_mark`时,不需要触发任何刷新操作
|
||||
- 当`async_water_mark < checkpoint_age < sync_water_mark`时,触发`async flush`,从flush列表中刷新`足够`的脏页回磁盘
|
||||
> 刷新足够脏页回磁盘,是指刷新后满足`checkpoint_age < sync_water_mark`
|
||||
- `checkpoint_age > sync_water_mark`,会触发`sync_water_mark`,刷新足够的脏页回磁盘
|
||||
|
||||
`sync/async flush checkpoint`操作同样放入到了page cleaner线程中,不会阻塞用户操作
|
||||
|
||||
> redo异步刷新的水位线为`0.75`,同步刷新的水位线为`0.9`
|
||||
>
|
||||
> 当`redo_lsn - checkpoint`的大小超过异步或同步水位线时,会把足够的脏页刷新到磁盘中,刷新后满足`redo_lsn - checkpoint < async_water_mark`
|
||||
|
||||
#### Dirty Page too Much
|
||||
如果buffer中存在的脏页数量过多,那么会触发innodb强制进行脏页刷新,将脏页刷新到磁盘。
|
||||
|
||||
## Buffer pool刷新
|
||||
innodb会在后台将脏页刷新到磁盘中。在innodb中,buffer pool刷新由page cleaner thread来执行。
|
||||
|
||||
> ### page cleaner threads
|
||||
> page cleaner threads的数量由`innodb_page_cleaners`变量来控制,该变量存在默认值,默认值为`innodb_buffer_pool_instances`的值。
|
||||
|
||||
### innodb_max_dirty_pages_pct_lwm
|
||||
当buffer pool中的脏页比例达到`innodb_max_dirty_pages_pct`的百分比时,将会触发buffer pool刷新操作。
|
||||
|
||||
`innodb_max_dirty_pages_pct_lwm`的默认值为`10%`,如果将该变量的值设置为0,那么将会禁用该刷新行为。
|
||||
|
||||
当配置`innodb_max_dirty_pages_pct_lwm`变量时,应该确保该变量的值小于`innodb_max_dirty_pages_pct`的值。
|
||||
|
||||
### innodb_lru_scan_depth
|
||||
`innodb_lru_scan_depth`该变量制定了每个缓冲池实例中,page cleaner在扫描lru列表时待刷新脏页的深度。该后台操作由page cleaner thread每秒执行一次。
|
||||
|
||||
> 若增加`innodb_lru_scan_depth`的值,在用户线程IO的基础上,会额外增加IO的负载。只有在工作负载之外存在空闲IO容量时,才考虑增加该变量的值。
|
||||
>
|
||||
> 如果工作负载已经令IO容量饱和,那么可以考虑减少`innodb_lru_scan_depth`的大小。
|
||||
>
|
||||
> `该变量默认值大小为1024`。
|
||||
|
||||
## 自适应刷新
|
||||
innodb采用了自适应的缓冲区刷新算法,根据redo log的生成速率和当前刷新频率动态调整刷新速率。通过该算法,可以令刷新活动和当前的工作负载保持平衡。
|
||||
|
||||
通过自适应刷新,可以避免因`buffer flush产生的IO活动挤占普通读写活动的IO容量`造成的吞吐量下降。
|
||||
|
||||
自适应刷新根据buffer pool中的脏页数量以及redo log日志的生成速率来帮助避免`buffer flush`造成的io活动突然增加,避免出现工作负载的突然变化。
|
||||
|
||||
### innodb_adaptive_flushing_lwm
|
||||
` innodb_adaptive_flushing_lwm`变量针对redo log容量定义了一个`low water mark`,当超过该阈值时,自适应花心将会被启用,`即使innodb_adaptive_flushing变量被禁用`。
|
||||
|
||||
### innodb_adaptive_flushing
|
||||
通过`innodb_adaptive_flushing`变量,可以控制自使用刷新是否被启用,默认情况下其被启用。
|
||||
|
||||
## Log Buffer
|
||||
log buffer为内存中的区域,用于保存将要写入到磁盘log文件中的数据。log buffer的大小通过`innodb_log_buffer_size`来进行配置,默认其值为`64MB`。
|
||||
|
||||
log buffer中的内容将会被定期刷新到磁盘中,`当log buffer足够大时,事务在提交之前无需将redo log内容刷到磁盘中`。
|
||||
|
||||
### innodb_flush_log_at_trx_commit
|
||||
`innodb_flush_log_at_trx_commit`用于控制如何将log buffer中的内容刷新到磁盘中。
|
||||
|
||||
默认情况下,该变量的值为`1`,代表在事务提交时会将log buffer中的redo log内容刷新到磁盘为文件中。
|
||||
|
||||
### innodb_flush_log_at_timeout
|
||||
`innodb_flush_log_at_timeout`控制刷新频率。
|
||||
|
||||
## Master Thread
|
||||
master thread中的操作主要分为两部分:
|
||||
- 每秒钟执行一次的操作
|
||||
- 每10s执行一次的操作
|
||||
|
||||
### 每秒钟执行一次的操作
|
||||
master thread中每秒一次的操作包括如下内容:
|
||||
- 将log buffer中的内容刷新到磁盘,即使对应事务尚未提交(总是)
|
||||
- 合并插入缓冲(可能)
|
||||
|
||||
> 对于redo log,即使事务当前尚未提交,innodb仍然会每秒将log buffer中的内容刷新到缓冲区中
|
||||
|
||||
> 合并插入缓冲并不是每秒都会发生,innodb会根据前1s内发生的IO次数是否小于5来判断是否合并插入缓冲。如果小于5次,那么当前IO负载较小,可以执行合并缓冲操作。
|
||||
|
||||
### 每10秒执行的操作
|
||||
- 合并至多5个插入缓冲(总是)
|
||||
- 将日志缓冲刷新到磁盘(总是)
|
||||
- 删除无用的undo log页(总是)
|
||||
|
||||
## change buffer
|
||||
change buffer为一个特殊的数据结构,当想要修改的辅助索引page不在缓冲区中时,会将针对辅助索引page的修改缓冲到change buffer中。
|
||||
|
||||
对于辅助索引页面的修改可能是由`insert, update, delete`操作造成的,当后续待修改的页面被读入到缓冲区中时,会对change buffer中缓冲的修改操作进行合并。
|
||||
|
||||
### 聚簇索引和辅助索引插入顺序
|
||||
和聚簇索引不同,辅助索引通常不是唯一的。`并且,当使用`auto_increment`的聚簇索引时,新数据的插入通常都是顺序的(插入的多个数据唯一同一page中)。`
|
||||
|
||||
但是,当插入新数据时,新数据对应辅助索引记录的插入则相对是无序的,多条辅助索引记录的插入可能都分布在不同的page。同样的,针对辅助索引记录的修改和删除,其影响的辅助索引记录页可能分布在不同的索引页中。
|
||||
|
||||
通过change buffer,将针对辅助索引页面的修改缓冲在buffer中,并且当需要修改的辅助索引页面读取到缓冲区中时,再对change buffer中的修改操作进行合并,这样能很大程度上避免多次随机读取辅助索引页到缓冲区中带来的开销。
|
||||
|
||||
> 定期执行的purge操作会将被更新的索引页写入到磁盘中。相比于马上将被修改的索引页写入到磁盘中,purge操作更高效。
|
||||
|
||||
当存在很多被修改的行数据以及辅助索引数据时,change buffer merging操作可能花费数个小时来执行。在change buffer merging期间,磁盘IO将会增加,可能会导致磁盘相关查询的性能下降。
|
||||
|
||||
在内存中,change bufer占用的缓冲区的一部分,而在磁盘中,change buffer是system tablespace的一部分。当数据库实例未启动时,针对辅助索引的修改存储在磁盘的change buffer文件中。
|
||||
|
||||
|
||||
|
||||
> 当辅助索引中包含`descending index column`或主键中包含`descending index column`时,change buffer不适用。
|
||||
|
||||
降序索引示例如下:
|
||||
```sql
|
||||
CREATE TABLE t (
|
||||
c1 INT, c2 INT,
|
||||
INDEX idx1 (c1, c2 DESC),
|
||||
INDEX idx2 (c1 DESC, c2),
|
||||
INDEX idx3 (c1 DESC, c2 DESC)
|
||||
);
|
||||
```
|
||||
|
||||
### change buffer配置
|
||||
当对于table执行insert、update、delete一些列操作时,这些操作集合中辅助索引列的值通常是无序的(分散在多个辅助索引页中),如果大量更新多个索引页。`当待更新的辅助索引页不位于buffer pool中时,针对辅助索引页的修改将会被缓存。当待更新的索引页面被加载到buffer pool中时,被缓存的修改操作将会被合并,被更新的页面后续会刷新到磁盘中`。
|
||||
|
||||
> innodb在数据库实例空闲或slow shutdown时会进行change buffer的合并。
|
||||
|
||||
#### 优势
|
||||
change buffer可以带来更少的磁盘读取和磁盘写入,故而在工作负载收到io限制时,change buffer能发挥重大作用。例如,在存在大量插入操作时,change buffer能够显著提升性能。
|
||||
|
||||
#### 弊端
|
||||
由于change buffer会占用buffer pool的内存空间,故而当change buffer较大时,会减少数据缓存的可用空间。`但是,如果work set的大小和buffer pool几乎相当时,或是数据库表中几乎没有辅助索引时,此时则可以禁用change buffer`。
|
||||
|
||||
### `innodb_change_buffering`
|
||||
`innodb_change_buffering`变量控制chnage buffer的行为,通过`innodb_change_buffering`,可以对如下操作进行启用和禁用:
|
||||
- insert operations
|
||||
- delete operations:`当index record被标记为删除`
|
||||
- purge operations: `当index被物理删除`
|
||||
|
||||
> update操作是insert操作和delete操作的组合
|
||||
|
||||
`innodb_change_buffering`取值如下:
|
||||
|
||||
| value | numeric value | description |
|
||||
| :-: | :-: | :-: |
|
||||
| none | 0 | `默认`,不针对任何操作进行缓存 |
|
||||
| inserts | 1 | 针对insert操作进行缓存 |
|
||||
| deletes | 2 | 针对delete marking操作进行缓存 |
|
||||
| changes | 3 | 针对inserts和delete-marking操作进行缓存 |
|
||||
| purges | 4 | 针对后台发生的物理删除操作进行缓存 |
|
||||
| all | 5 | 针对inserts, deletes, purges操作进行缓存 |
|
||||
|
||||
### `innodb_change_buffer_max_size`
|
||||
`innodb_change_buffer_max_size`允许按照百分比的方式来配置change buffer占用buffer pool的最大大小,默认为`25`,最大可为`50`。
|
||||
|
||||
当mysql server存在大量insert、update、delete操作时,change buffer merging操作的速率无法跟上change buffer entries的新增速率,此时可以考虑增加`innodb_change_buffer_max_size`。
|
||||
|
||||
当change buffer占用了过多buffer pool中内存,导致buffer pool中页面老化速度超出预期时,此时可以尝试将`innodb_change_buffer_max_size`适当调小。
|
||||
|
||||
## double write
|
||||
double write buffer为存储区域,当innodb将buffer pool中的页刷新到innodb data file中的适当位置之前,会页写入到double write buffer中。
|
||||
|
||||
如果在写入页面的过程中,发生操作系统、存储子系统、mysql进程的异常,通过double write buffer,在crash recovery的过程中innodb能够从double write buffer中找到page的备份。
|
||||
|
||||
即使数据写了两次,double write buffer也不会造成两倍的IO负载或两倍的io操作数量。数据按照large chunk的方式写入到double write buffer中,并且会调用`fsync`方法。
|
||||
|
||||
### double write结构
|
||||
double write由两部分组成:
|
||||
- 内存: double write buffer,位于内存中
|
||||
- 磁盘:磁盘中的表空间
|
||||
|
||||
当innodb刷新buffer pool中的页面时,操作如下:
|
||||
1. 将页面写入到double write buffer中(其含有128个页),并且在写入到double write buffer后调用`fsync`,确保将double write buffer中的数据刷新到磁盘中
|
||||
2. 在完成`1`操作后,会实际将页数据写入到data file磁盘文件中,再次调用`fsync`确保数据被持久化到磁盘中
|
||||
|
||||
在后续innodb执行recovery操作时,会检查double write buffer中的内容和data file中page的内容:
|
||||
- 如果double write buffer中内容不一致,那么仅会将double write buffer中的内容丢弃
|
||||
- 如果datafile中页内容不一致,那么将会从double write buffer中恢复数据
|
||||
|
||||
### innodb_doublewrite
|
||||
`innodb_doublewrite`控制是否启用double write buffer。默认情况下,double write开启。如果想要关闭double write,可以将`innodb_doublewrite`设置为`OFF`.
|
||||
|
||||
#### DETECT_AND_RECOVERY
|
||||
`DETECT_AND_RECOVERY`和`ON`设置相同,在该设置下,double write被完全启用。innodb将会将page内容写入到double write buffer中,并且在recovery过程中会通过double write buffer中的数据来修复不一致page。
|
||||
|
||||
#### DETECT_ONLY
|
||||
当`innodb_doublewrite`被设置为`DETECT_ONLY`时,只有元数据会被写入到double write buffer,数据库的page content并不会被写入到double write buffer中,并且recovery过程也不会用double write buffer中的内容来恢复数据。
|
||||
|
||||
`DETECT_ONLY`设置只是为了检测`incomplete page write`。
|
||||
|
||||
> 如果dobule write buffer位于支持`atomic write`的fusion-io设备上,那么double write buffer将会自动被关闭,datafile的写入将会使用`fusion-io atomic write`。
|
||||
|
||||
### innodb_doublewrite_dir
|
||||
`innodb_doublewrite_dir`变量定义了innodb创建doublewrite files的位置。如果该变量没有被指定,那么innodb将会把文件创建在`innodb_data_home_dir`。
|
||||
|
||||
### innodb_doublewrite_pages
|
||||
`innodb_doublewrite_pages`控制每个线程其doublewrite page的最大数量。
|
||||
|
||||
### innodb_doublewrite_files
|
||||
`innodb_doublewrite_files`定义了doublewrite file的文件个数,默认情况下值为2,会为每个buffer pool instance创建两个doublewrite文件:
|
||||
- flush list doublewrite file
|
||||
- lru list doublewrite file
|
||||
|
||||
#### flush list dobulewrite file
|
||||
flush list doublewrite file是用于buffer pool flush list中page的刷新的。flush list doublewrite file的默认大小为`innodb page size * double write pages`。
|
||||
|
||||
#### lru doublewrite file
|
||||
lru doublewrite file是用于从buffer pool lru list中刷新的,其默认大小为`innodb page size * (doublewrite pages + (512 / buffer pool instances))`。
|
||||
|
||||
其中512为slot的总数量。
|
||||
|
||||
doublewirte file命名格式如下所示:
|
||||
```
|
||||
#ib_page_size_file_number.dblwr
|
||||
```
|
||||
|
||||
## adaptive hash index(自适应哈希)
|
||||
自适应哈希允许innodb在不牺牲传统食物特性以及可靠性的基础上,在工作负载适当,buffer pool内存充足的场景下,表现的更像in-memory database。
|
||||
|
||||
### 自适应哈希
|
||||
哈希的查找性能较B+树来说较高,B+树的查找次数取决于B+树的高度,生产环境中B+树的高度通常为3~4层,故而需要3~4次查询,而哈希通常只需要一次查找就能定位到数据。
|
||||
|
||||
#### 自适应哈希构建
|
||||
innodb监控索引查询,并且当观测到建立哈希索引能够加速查询时,会自动创建哈希索引,故而成为自适应哈希索引(AHI)。
|
||||
|
||||
AHI是通过buffer pool中的`B+树页`来构建的,故而构建数度很快。`AHI并不需要根据整张表构建哈希索引`,innodb会根据访问频率和模式来自动为某些热点页建立哈希索引。
|
||||
|
||||
#### 访问模式
|
||||
AHI在监控索引访问时,要求页的连续访问模式必须是一样的。例如,对于`(a,b)`这样的联合索引页,其访问模式存在如下可能场景:
|
||||
- where a = xxx
|
||||
- where a = xxx and b = xxx
|
||||
|
||||
上面两种场景都会走`(a,b)`联合索引,但是在AHI看来其访问模式并不相同。
|
||||
|
||||
#### AHI构建要求
|
||||
在满足对索引页的连续访问模式一样后,AHI的构建还有如下要求:
|
||||
- 以相同模式访问了100次
|
||||
- 待构建的索引页通过该模式访问了N次,`N = 页中的记录数 / 16`
|
||||
|
||||
即对于制定索引,既要求访问该索引的条件相同,也要求索引页被多次访问(索引页被访问次数达到页中记录数的1/16)
|
||||
|
||||
在启用AHI后,读取和写入速度可以提升2~3倍,辅助索引连接性能可以提升5倍。
|
||||
|
||||
#### non-hash searches
|
||||
hash索引只能被用于等值查询,对于其他类型查找,例如范围查找并不适用。
|
||||
|
||||
### innodb_adaptive_hash_index
|
||||
是否启用自适应哈希索引可以通过`innodb_adaptive_hash_index`变量来控制,当值为`ON`时,代表自适应哈希索引被启用。
|
||||
|
||||
hash index是通过index key的前缀来进行构建的,prefix可以是任何长度,并且只有b树中的一些值能够出现在hash index中。`hash index是为经常被访问的索引页面按需进行构建的`。
|
||||
|
||||
如果一张表几乎全部都加载到了内存中,那么通过hash index能够加速对该表的查询。此时,hash index对应的value类似于指针,能够直接访问表中的任何元素。
|
||||
|
||||
> innodb中存在监控索引搜索的机制,如果innodb发现可以通过构建hash index来受益,那么其会字段能够构建hash索引。
|
||||
|
||||
在某些工作负载下,hash index带来的查找加速远远超过了`监控索引搜索`以及`维护hash index结构`带来的开销。
|
||||
|
||||
但是,在某些高工作负载的场景下,hash index可能并不会带来收益,此时,可以关闭hash index从而减少额外开销。
|
||||
|
||||
adaptive hash index是被分区的,每个hash index被绑定到一个分区,并且每个分区被一个独立的latch保护。
|
||||
|
||||
分区数量可以通过`innodb_adaptive_hash_index_parts`来控制,默认为8,最大可以设置为512.
|
||||
|
||||
可以通过`show engine innodb status`中输出的` SEMAPHORES`部分来监控adaptive hash index。如果存在许多线程正在等待`btr0sea.c`中创建的rw latches,可以考虑增加adaptive hash index partitions或者禁用adaptive hash index。
|
||||
|
||||
## 异步io
|
||||
为了提升数据库性能,innodb采用异步io操作方式。
|
||||
|
||||
### Sync IO
|
||||
传统的Sync IO要求每进行一次io操作,都要求当前io操作执行完成后才能执行下一次io操作。
|
||||
|
||||
对于同步阻塞io,当用户发送了一条索引扫描的查询指令时,那么该sql可能需要扫描多个索引页面,每次索引页面的扫描需要一次IO操作。sync io需要每次索引扫描完成后再执行下一次索引扫描。
|
||||
|
||||
### AIO
|
||||
对于异步io,用户可以在发送一个io指令后立刻再发送另一个io指令,并在所有io指令发送后等待所有io操作完成。发送另一个io指令时,并不需要等待前面io操作的完成,这样可以降低在io阻塞上等待的时间。
|
||||
|
||||
#### io merge
|
||||
aio除了可以无阻塞的执行多条io操作外,还可以进行io merge操作,即将多个io合并为一个io。通过将多个io请求合并为一个io请求,可以提升iops(每秒钟执行的io数量)。
|
||||
|
||||
例如,用户需要访问的页(space, page)为`(8,6), (8,7), (8,8)`,每个页的大小为16kb,那么当使用同步io时,需要执行3次io操作。
|
||||
|
||||
当时,当使用aio时,aio会判断`(8,6), (8, 7), (8,8)`这是那个页是连续的,那么aio底层只会发送一个io请求,从(8,6)开始,读取48KB大小的数据。
|
||||
|
||||
通过linux命令`iostat`,能够观察`rrqm/s`和`wrqm/s`(read/write requests merged per second)。
|
||||
|
||||
## 刷新邻接页
|
||||
innodb中提供了刷新邻接页的特性。当刷新脏页时,innodb会检测页所在区(extent)中的所有页,如果也是脏页,则会一起执行刷新操作。
|
||||
|
||||
通过刷新邻接页的操作,可能将不怎么脏的脏页也进行了刷新,而该页和快又被变脏;而且,在使用固态硬盘时,固态硬盘拥有较高的iops,可能并不太需要该特性。
|
||||
|
||||
可以通过`innodb_flush_neighbors`变量来控制是否刷新邻接页。
|
||||
|
||||
## 启动、关闭和恢复
|
||||
### innodb_fast_shutdown
|
||||
在innodb进行关闭时,`innodb_fast_shutdown`参数影响innodb引擎的行为,该参数可选值为`0,1,2`,默认值为1。
|
||||
|
||||
参数各值代表的行为如下:
|
||||
- 0: mysql数据库关闭时,innodb需要完成所有full purge和merge insert buffer,并且要将所有的脏页刷新到磁盘中。这需要很长时间来完成,`如果在对innodb进行升级时,需要将这个参数调整为0,并关闭数据库`
|
||||
- 1: 默认值,代表不需要完成full purge和merge insert buffer操作,但是要将脏页刷新会磁盘
|
||||
- 2: 代表不完成full purge和merge insert操作,也不将脏页刷新回磁盘,而是将日志都写到日志文件中,这样不会有任何事物的丢失,但是下次mysql启动时,需要执行恢复操作(recovery)
|
||||
|
||||
### innodb_force_recovery
|
||||
参数innodb_force_recovery控制innodb引擎的恢复情况。该参数默认为0,代表需要恢复时进行所有恢复操作;并且当不能有效恢复时,例如数据页发生冲突,会令mysql宕机,并且将错误写入到错误日志中。
|
||||
|
||||
innodb_force_recovery的取值如下:
|
||||
- 0:默认
|
||||
- 1(SRV_FORCE_IGNORE_CORRUPT):忽略检查到的冲突页
|
||||
- 2(SRV_FORCE_NO_BACKGROUND):阻止master thread线程的运行,如master thead需要进行full purge,这会导致crash
|
||||
- 3(SRV_FORCE_NO_TRX_UNDO):不进行事务的回滚
|
||||
- 4(SRV_FORCE_NO_IBUF_MERGE):不进行插入缓冲的合并操作
|
||||
- 5(SRV_FORCE_NO_UNDO_LOG_SCAN):不查看undo log,innodb会将未提交事务看作已提交
|
||||
- 6(SRV_FORCE_NO_LOG_REDO):innodb不执行前滚操作
|
||||
|
||||
> 如果在数据库发生宕机时,存在未提交事务,那么将下次数据库启动时,会先根据未提交事务的undo log来对数据进行回滚。
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
150
mysql/mysql文档/mysql_mvcc_impl.md
Normal file
150
mysql/mysql文档/mysql_mvcc_impl.md
Normal file
@@ -0,0 +1,150 @@
|
||||
- [mysql mvcc](#mysql-mvcc)
|
||||
- [Internal Impl](#internal-impl)
|
||||
- [undo log](#undo-log)
|
||||
- [insert undo log](#insert-undo-log)
|
||||
- [update undo log](#update-undo-log)
|
||||
- [commit regularly](#commit-regularly)
|
||||
- [purge](#purge)
|
||||
- [insert and delete rows in smallish batches](#insert-and-delete-rows-in-smallish-batches)
|
||||
- [purge lag](#purge-lag)
|
||||
- [innodb\_purge\_threads](#innodb_purge_threads)
|
||||
- [innodb\_max\_purge\_lag](#innodb_max_purge_lag)
|
||||
- [Multi-Versioning and Secondary Indexes](#multi-versioning-and-secondary-indexes)
|
||||
- [mvcc mechanism](#mvcc-mechanism)
|
||||
- [read view](#read-view)
|
||||
- [read view for read committed isolation level](#read-view-for-read-committed-isolation-level)
|
||||
- [read view for repeatable read isolation level](#read-view-for-repeatable-read-isolation-level)
|
||||
- [read view impl](#read-view-impl)
|
||||
- [innodb clustered index hidden columns](#innodb-clustered-index-hidden-columns)
|
||||
- [implementation principle](#implementation-principle)
|
||||
|
||||
|
||||
# mysql mvcc
|
||||
innodb是一个`多版本`的存储引擎,对于被修改的records,其保存了records旧版本的信息,从而支持事务的`并发`和`回滚`等特性。
|
||||
|
||||
innodb使用了rollback segment中存储的信息来进行`undo操作`,从而`支持事务在需要时进行回滚`;`并且支持通过undo信息构建record的早期版本,从而实现一致性读`。
|
||||
|
||||
## Internal Impl
|
||||
在innodb的内部实现中,其为数据库中存储的每行数据都额外添加了3个字段:
|
||||
- `DB_TRX_ID`:长度为6字节,用于标识`最后对改行数据进行insert/update操作的事务`
|
||||
- 其中,delete操作也被看做是`update`,因为innodb在对数据执行删除操作时,并不会立马对数据进行物理删除,而是会先标记该行数据的delete mark,数据实际被物理删除发生在purge阶段
|
||||
- `DB_ROLL_PTR`:长度为7字节,被称为`roll pointer`。该指针指向undo record,通过undo record中的内容可以构建该行数据被更新前的版本
|
||||
- `DB_ROW_ID`:长度为6字节,该字段包含一个row id,该row id在数据被插入时会单调的增长。
|
||||
- 如果表使用的是innodb自动生成的聚簇索引(未显式指定主键),那么自动生成的聚簇索引将会包含该row id的值
|
||||
- 如果表显式指定了聚簇索引,那么`DB_ROW_ID`并不会出现在任何索引中
|
||||
|
||||
## undo log
|
||||
在rollback segment中,undo log可以分为两种类型:
|
||||
- insert undo log
|
||||
- update undo log
|
||||
|
||||
### insert undo log
|
||||
insert undo log只会在事务回滚时被用到,在事务提交之后,insert undo log的内容可以被立马删除。
|
||||
|
||||
### update undo log
|
||||
update undo log除了用于事务回滚外,还用于一致性读(MVCC)。对于update undo log,其只在满足如下条件时可以被删除
|
||||
- 当`需要通过该update undo log来构建数据先前版本`的事务都不存在时,update undo log才可以被删除
|
||||
|
||||
> innodb会为每个事务分配一个快照,事务在进行一致性读时,实际读取的是快照中的旧版本数据。
|
||||
>
|
||||
> 快照需要通过update undo log来还原行数据的旧版本。如果没有事务再需要通过该update undo log来还原旧数据,代表该update undo log可以被删除。
|
||||
|
||||
### commit regularly
|
||||
在编写和数据库交互的代码时,推荐周期性的提交事务,`包括只会进行一致性读的读事务`。如果事务A的运行时间较长,且一直未提交,那么在事务A运行时,其他事务对数据库的更新操作,其生成的update undo log在事务A运行期间都无法被丢弃(因为有可能A会读取被修改数据之前的版本,此时需要通过undo update log来进行旧版本数据的还原)。
|
||||
|
||||
故而,如果存在一直不提交的事务,那么可能会造成update undo log无法丢弃,那么rollback segment占用的空间大小会不断增加,填满其所位于的undo tablespace。
|
||||
|
||||
### purge
|
||||
在innodb的mvcc方案中,在通过delete sql删除行数据后,行数据并不会立马就从该数据库中被移除(不会立马被物理删除)。
|
||||
|
||||
innodb直到`删除update undo log record时`才`删除undo log record关联的行数据/index records`。该删除操作被称为`purge`。
|
||||
|
||||
purge的操作很快,并且purge的顺序通常和执行delete sql的时间顺序相同。
|
||||
|
||||
### insert and delete rows in smallish batches
|
||||
如果针对表同时执行insert和delete的小批量操作,且insert和delete的速率相同,那么purge现成的回收速率可能会小于数据的插入速率。这样会导致`dead rows`(标记为逻辑删除但未实际purge的数据行)堆积,表占用空间越来越大,并造成disk相关操作变慢。
|
||||
|
||||
在上述场景下,应当限制新操作,并且通过调整innodb_max_purge_lag向purge thread分配更多资源。
|
||||
|
||||
#### purge lag
|
||||
##### innodb_purge_threads
|
||||
purge操作在后台由一个或多个purge threads执行。purge threads的数量由`innodb_purge_threads`来进行控制,默认值的取值逻辑如下:
|
||||
- 如果可获取的逻辑核数小于等于16,则默认值为1
|
||||
- 如果logic processors大于16,那么默认值为4
|
||||
|
||||
当dml操作集中在一张表上,那么该表的purge操作由一个现成来执行,这样可能会造成purge操作变慢,增加purge lag。
|
||||
|
||||
##### innodb_max_purge_lag
|
||||
如果purge lag超过`innodb_max_purge_lag`时,purge工作会自动在多个purge threads之间进行重新分配。
|
||||
|
||||
当purge threads设置过大时,可能会造成与user threads的争用,故而相当适当的管理purge threads大小。
|
||||
|
||||
`innodb_max_purge_lag`的默认值为0,代表默认不存在max purge lag。
|
||||
|
||||
|
||||
## Multi-Versioning and Secondary Indexes
|
||||
在mvcc中,对待聚簇索引和辅助索引的方式不同。在聚簇索引中,record中的列是`update in-place`,并且其hidden system columns指向undo log records,通过undo log records可以还原数据的先前版本。
|
||||
|
||||
但是,辅助索引不包含hidden system columns,并且对于辅助索引的更新也不是update in-place。
|
||||
|
||||
当辅助索引的column被更新时,旧的辅助索引记录将会被标记为delete marked,并且插入新的辅助索引记录。`delete marked index records`最终会被purge。
|
||||
|
||||
当辅助索引中的index record被`delete marked`或`辅助索引页被newer transaction(更新的事务)更新时,innodb将会在聚簇索引中查询记录`。在聚簇索引中,该record的DB_TRX_ID将会被检查,查看并且会通过undo log构建出当前线程可见的数据版本。
|
||||
|
||||
> 行数据的版本记录存储在聚簇索引中。如果innodb在查找记录索引时发现该辅助index record被delete marked,此时并不确定delete marked的操作是否对查询事务可见,需要在聚簇索引中查找记录并构建历史版本。
|
||||
>
|
||||
> 当辅助索引页被newer transaction更新时,也无法确定该辅助索引页中的内容是否对当前事务可见,同样需要查询聚簇索引来构建先前的历史版本。
|
||||
|
||||
> 如果当前事务初始化后,record才被其他事务修改,那么根据事务的一致性读原则,record的修改不应对当前事务可见,需要通过record对应的undo log内容还原到数据的旧版本。
|
||||
|
||||
> 如果`当前辅助索引中的数据被delete marked`或`当前辅助索引页被newer transaction所更新`,那么`covering index`技术将不会被使用。故而,并不会直接从辅助索引中返回值,innodb而是会再次从clustered index中查询记录。
|
||||
|
||||
## mvcc mechanism
|
||||
mvcc机制主要用于处理多事务之间对数据的并发访问,并且通过`版本快照`来实现事务之间的数据隔离。
|
||||
|
||||
事务A可见的数据版本为`事务A开启时的数据版本`,即使在事务A执行的过程中,事务B对数据进行了修改,事务A后续读取时仍然读取的是修改之前的版本。
|
||||
|
||||
综上所属,根据mvcc机制可以实现可重复读的隔离级别。
|
||||
|
||||
### read view
|
||||
在mvcc中,存在`read view`这一概念,其原理类似于如下描述:
|
||||
- 在事务开启时,为所有数据创建一个快照,后续该事务在执行过程中都会从快照中读取数据,从而可以消除其他事务修改数据所造成的影响
|
||||
|
||||
#### read view for read committed isolation level
|
||||
对于`read committed`隔离级别的事务,其等价于`generate a read view before each select statement is executed`
|
||||
|
||||
#### read view for repeatable read isolation level
|
||||
对于`repeatable read`隔离级别的事务,其等价于`generate a read view before transaction executes the first select statement`,并且在后续整个事务的执行过程中都使用该read view。
|
||||
|
||||
#### read view impl
|
||||
read view实现并非是简单的为所有数据创建一个备份,其原理如下。
|
||||
|
||||
read view在被创建时,会记录如下信息:
|
||||
- `m_ids`:当read view被创建时,read view会记录当前数据库中所有活跃事务id的集合
|
||||
- `min_trx_id`: 该field代表当read view被创建时,数据库中所有活跃事务id中的最小值,相当于`m_ids`集合中的最小值
|
||||
- `max_trx_id`: 该值代表当`read view被创建时`,`数据库将要授予下一个新创建事务的事务id`,即`当前全局最大的事务id + 1`
|
||||
- `creator_trx_id`:代表创建该read view的事务id
|
||||
|
||||
#### innodb clustered index hidden columns
|
||||
除了read view外,innodb mvcc中还包含另外的部分:`hidden_columns`。
|
||||
|
||||
innodb的clustered index中会包含如下hidden columns:
|
||||
- `trx_id`: 代表最后对该clustered index record进行修改的事务id
|
||||
- `roll_pointer`: 每当clustered index record被修改时,指向`记录数据旧版本undo log`的指针会被写入到该column中。故而,该`roll_pointer`包含了一个单向链表,`包含clustered index record所有的旧版本`
|
||||
|
||||
#### implementation principle
|
||||
通过上述记录的read view信息和clustered index hidden columns信息,可以决定数据的历史版本对事务是否可见:(`每个历史版本中都包含trx_id信息,用于代表该版本的最后修改事务id`)
|
||||
- `trx_id = creator_trx_id`: 如果`数据最后被修改的事务id`和`创建read view的事务id`相同,那么该行数据版本对当前事务可见
|
||||
- `trx_id < min_trx_id`:如果`行数据的最后被修改事务id`小于`read view中最小的活跃事务id`,代表read view创建时,trx_id对应的事务已经被提交,此时该行数据版本对当前事务可见
|
||||
- `trx_id >= max_trx_id`:如果`行数据最后被修改的事务id`大于或等于`read view被创建时的全局最大事务id + 1`,read view创建时`该数据版本对应的修改事务id还不存在`,即read view创建时该修改还未发生,故而该行数据版本对当前事务不可见
|
||||
- `min_trx_id <= trx_id < max_trx_id`:
|
||||
- `trx_id位于m_ids中`:如果trx位于m_ids中,代表read view在创建时,该数据版本对应的事务还未提交,即该数据版本对事务不可见
|
||||
- `trx_id未位于m_ids中`: 代表在read view创建时,该数据版本对应的事务已经提交,故而该行数据版本可见
|
||||
|
||||
> read view通过记录创建时的`活跃事务id列表`和`全局最大事务id + 1`,可以明确的区分`数据版本的最后修改事务id造成的修改`是否对readview可见,其判断逻辑如下:
|
||||
> - 对于id大于等于`全局最大事务id + 1`的事务,在read view创建时都没有被创建,故而未来事务造成的修改对read view不可见
|
||||
> - 对于id小于`最小活跃事务id`的事务,代表在read view创建时已提交,read view创建之前提交的事务对read view是可见的
|
||||
> - 对于id位于`[min_trx_id, max_trx_id)`之间的事务,其是否可见取决于其是否位于`活跃事务id列表`中来进行判断
|
||||
> - 如果位于`活跃事务id列表`中,代表read view创建时该历史版本对应修改还未提交,故而对read view不可见
|
||||
> - 如果不位于`活跃事务id列表`中,代表read view创建时该历史版本对应修改已经提交,已提交事务的修改对read view可见
|
||||
|
||||
821
mysql/mysql文档/mysql_事务.md
Normal file
821
mysql/mysql文档/mysql_事务.md
Normal file
@@ -0,0 +1,821 @@
|
||||
- [事务](#事务)
|
||||
- [事务分类](#事务分类)
|
||||
- [扁平事务](#扁平事务)
|
||||
- [带有保存点的扁平事务](#带有保存点的扁平事务)
|
||||
- [链事务](#链事务)
|
||||
- [嵌套事务](#嵌套事务)
|
||||
- [savepoint和嵌套事务区别](#savepoint和嵌套事务区别)
|
||||
- [分布式事务](#分布式事务)
|
||||
- [事务实现](#事务实现)
|
||||
- [redo](#redo)
|
||||
- [redo log persists before transaction committed](#redo-log-persists-before-transaction-committed)
|
||||
- [redo log和undo log比较](#redo-log和undo-log比较)
|
||||
- [redo log和binlog的比较](#redo-log和binlog的比较)
|
||||
- [innodb\_flush\_log\_at\_trx\_commit](#innodb_flush_log_at_trx_commit)
|
||||
- [log block](#log-block)
|
||||
- [block \& atomic](#block--atomic)
|
||||
- [log block header](#log-block-header)
|
||||
- [`LOG_BLOCK_HDR_NO`](#log_block_hdr_no)
|
||||
- [`LOG_BLOCK_HDR_DATA_LEN`](#log_block_hdr_data_len)
|
||||
- [`LOG_BLOCK_FIRST_REC_GROUP`](#log_block_first_rec_group)
|
||||
- [`LOG_BLOCK_CHECKPOINT_NO`](#log_block_checkpoint_no)
|
||||
- [log block tailer](#log-block-tailer)
|
||||
- [log group](#log-group)
|
||||
- [redo log buffer刷新到磁盘中的时机](#redo-log-buffer刷新到磁盘中的时机)
|
||||
- [WAL](#wal)
|
||||
- [redo log format](#redo-log-format)
|
||||
- [redo log头部格式](#redo-log头部格式)
|
||||
- [LSN](#lsn)
|
||||
- [页LSN](#页lsn)
|
||||
- [查看LSN](#查看lsn)
|
||||
- [recovery](#recovery)
|
||||
- [undo](#undo)
|
||||
- [undo log和redo log差异](#undo-log和redo-log差异)
|
||||
- [非锁定读](#非锁定读)
|
||||
- [undo log的产生会伴随redo log的产生](#undo-log的产生会伴随redo-log的产生)
|
||||
- [存储管理](#存储管理)
|
||||
- [innodb\_undo\_directory](#innodb_undo_directory)
|
||||
- [innodb\_rollback\_segments](#innodb_rollback_segments)
|
||||
- [innodb\_undo\_tablespaces](#innodb_undo_tablespaces)
|
||||
- [purge](#purge)
|
||||
- [undo页的重用设计](#undo页的重用设计)
|
||||
- [核心概念](#核心概念)
|
||||
- [rollback segment](#rollback-segment)
|
||||
- [undo slots](#undo-slots)
|
||||
- [undo segment](#undo-segment)
|
||||
- [undo log格式](#undo-log格式)
|
||||
- [insert undo log](#insert-undo-log)
|
||||
- [update undo log](#update-undo-log)
|
||||
- [undo log组织形式](#undo-log组织形式)
|
||||
- [undo log的逻辑组织方式](#undo-log的逻辑组织方式)
|
||||
- [record versions](#record-versions)
|
||||
- [物理组织方式](#物理组织方式)
|
||||
- [undo segment](#undo-segment-1)
|
||||
- [undo page header](#undo-page-header)
|
||||
- [undo segment header](#undo-segment-header)
|
||||
- [undo log storing](#undo-log-storing)
|
||||
- [文件组织方式](#文件组织方式)
|
||||
- [rollback segment](#rollback-segment-1)
|
||||
- [内存组织方式](#内存组织方式)
|
||||
- [undo::Tablespace](#undotablespace)
|
||||
- [trx\_rseg\_t](#trx_rseg_t)
|
||||
- [trx\_undo\_t](#trx_undo_t)
|
||||
- [undo writing](#undo-writing)
|
||||
- [undo record写入](#undo-record写入)
|
||||
- [undo for rollback](#undo-for-rollback)
|
||||
- [undo for mvcc](#undo-for-mvcc)
|
||||
- [历史版本](#历史版本)
|
||||
- [purge](#purge-1)
|
||||
- [history](#history)
|
||||
- [history的purge流程](#history的purge流程)
|
||||
- [innodb\_purge\_batch\_size](#innodb_purge_batch_size)
|
||||
- [innodb\_max\_purge\_lag](#innodb_max_purge_lag)
|
||||
- [innodb\_max\_purge\_lag\_delay](#innodb_max_purge_lag_delay)
|
||||
- [group commit](#group-commit)
|
||||
- [binlog](#binlog)
|
||||
- [replication \&\& point-in-time recovery](#replication--point-in-time-recovery)
|
||||
- [binlog format](#binlog-format)
|
||||
- [group commit](#group-commit-1)
|
||||
- [group commit机制](#group-commit机制)
|
||||
- [`binlog_order_commits`](#binlog_order_commits)
|
||||
- [`binlog_max_flush_queue_time = microseconds`](#binlog_max_flush_queue_time--microseconds)
|
||||
- [binlog details](#binlog-details)
|
||||
- [sync the storage engine and binary log](#sync-the-storage-engine-and-binary-log)
|
||||
- [prepare\_commit\_mutex](#prepare_commit_mutex)
|
||||
- [binary log group commit](#binary-log-group-commit)
|
||||
- [事务控制语句](#事务控制语句)
|
||||
- [rollback](#rollback)
|
||||
- [rollback to savepoint](#rollback-to-savepoint)
|
||||
- [事务操作统计](#事务操作统计)
|
||||
- [事务隔离级别](#事务隔离级别)
|
||||
- [长事务](#长事务)
|
||||
|
||||
# 事务
|
||||
## 事务分类
|
||||
事务通常可分为如下类型:
|
||||
- 扁平事务(flat transaction)
|
||||
- 带保存点的扁平事务(flat transaction with savepoints)
|
||||
- 链事务(chained transaction)
|
||||
- 嵌套事务(nested transaction)
|
||||
- 分布式事务(distributed transaction)
|
||||
|
||||
### 扁平事务
|
||||
扁平事务为最常用的事务,在扁平事务中,所有操作都处于统一层次,其由`begin work`开始,并由`commit work`或`rollback work`结束。
|
||||
|
||||
扁平事务的操作是原子的,要么都提交,要么都回滚。
|
||||
|
||||
### 带有保存点的扁平事务
|
||||
对于带有保存点的扁平事务,`其支持在事务执行过程中回滚到同一事务中较早的一个状态`。
|
||||
|
||||
在事务执行过程中,可能并不希望所有的操作都回滚,放弃所有操作的代价可能太大。通过`保存点`,可以记住事务当前的状态,在后续发生错误后,事务能够回到保存点当时的状态。
|
||||
|
||||
相较于扁平事务只能够全部回滚,带保存点的扁平事务能够回滚到保存点时的状态。
|
||||
|
||||
保存点可以通过`save work`来创建,使用示例如下所示
|
||||
|
||||
<img alt="" height="752" src="https://i-blog.csdnimg.cn/blog_migrate/2ef380492373bb8e4a6e529109baee3c.png" width="728">
|
||||
|
||||
### 链事务
|
||||
可视为保存点事务的一个变种。在使用带保存点的事务时,如果系统发生崩溃,那么所有的保存点都会消失。`在后续重启进行恢复时,事务需要从开始处重新执行`,而不是从最近的一个保存点开始执行。
|
||||
|
||||
> 若事务在数据库未提交时发生崩溃,那么在数据库再次重启执行recovery操作时,会对未提交的事务进行回滚,即使之前事务存在保存点,也会全部回滚
|
||||
|
||||
链事务的思想是,当提交事务时,释放不必要的数据对象,将必要的上下文隐式传递给下一个要开始的事务。`提交事务和开始下一个事务操作必须为原子操作`,下一个事务必须徐要能看到上一个事务的结果,示例如下:
|
||||
|
||||
<img class="trans" src="https://images2015.cnblogs.com/blog/754297/201602/754297-20160204112125600-267403241.jpg">
|
||||
|
||||
和带保存点的扁平事务不同的是,带保存点的扁平事务能够回滚到任意正确的保存点,而链事务只能回滚当前事务。
|
||||
|
||||
且链事务和带保存点的扁平事务,对于锁的处理也不同:
|
||||
- `链事务`:对于每个事务,commit后释放持有的锁
|
||||
- `带保存点的扁平事务`:在整个事务提交前,不会释放持有的锁
|
||||
|
||||
### 嵌套事务
|
||||
嵌套事务为一个层次结构框架,由顶层事务控制各个层次的事务。嵌套在顶层事务中的事务被称为`子事务`。
|
||||
|
||||
<img src="https://pic2.zhimg.com/v2-01f00b04df29181143da399008b92055_r.jpg">
|
||||
|
||||
如下为Moss理论嵌套事务的定义:
|
||||
- 嵌套事务是由若干事务组成的一颗树,子树既可以是嵌套事务,又可以是扁平事务
|
||||
- 处在叶子节点的事务是扁平事务
|
||||
- 位于根节点的事务被称为顶层事务,其他事务被称为子事务,事务的`predecessor`被称为父事务,事务的下一层事务被称为子事务
|
||||
- 子事务既可以提交又可以回滚,但是子事务的提交并不会立马生效,除非其父事务已经被提交。`任何子事务都在顶层事务提交后才真正提交`
|
||||
- 树中任何一个事务的回滚会引其所有子事务都一起回滚
|
||||
|
||||
在Moss理论中,实际工作被交由叶子节点来完成,`只有叶子节点的事务才能够访问数据库,发送消息,获取其他类型的资源`。`高层事务仅仅负责逻辑控制,即负责何时调用相关子事务`。
|
||||
|
||||
即使一个系统不支持嵌套事务,也可以通过保存点技术来模拟嵌套事务。
|
||||
|
||||
#### savepoint和嵌套事务区别
|
||||
在使用保存点来模拟嵌套事务时,在锁持有方面和嵌套事务有差别。
|
||||
- 嵌套事务:在使用嵌套事务时,不同子事务在数据库持有的锁不同
|
||||
- 保存点:在通过保存点来模拟嵌套事务时,用户无法选择哪些锁被哪些子事务继承,无论有多少个保存点,所有的锁都可以得到访问
|
||||
|
||||
|
||||
### 分布式事务
|
||||
通常是在分布式环境运行的扁平事务,需要访问网络中的不同节点。
|
||||
|
||||
分布式事务同样需要满足ACID的特性,要么都发生,要么都不发生。
|
||||
|
||||
对于innodb存储引擎,其支持扁平事务、带有保存点的扁平事务、链事务、分布式事务。`innodb并不原生支持嵌套事务,单可以通过带保存点的事务来模拟串行的嵌套事务`。
|
||||
|
||||
## 事务实现
|
||||
对于事务的ACID特性,其实现如下:
|
||||
- `I(隔离性)`:事务隔离性通过锁来实现
|
||||
- `A(原子性), D(持久性)`:事务的原子性和持久性可以通过redo log来实现
|
||||
- `C(一致性)`:事务一致性通过undo log来实现
|
||||
|
||||
### redo
|
||||
redo log(重做日志)用于实现事务的持久性,其由两部分组成:
|
||||
- 内存中的重做日志缓冲(redo log buffer)
|
||||
- 磁盘中的重做日志文件(redo log file)
|
||||
|
||||
#### redo log persists before transaction committed
|
||||
innodb存储引擎支持事务,其通过`force log at commit`机制实现事务的持久性,即当事务提交时,必须先将该事务的所有日志写入到磁盘的日志文件中进行持久化,直到该过程完成后事务才提交完成。
|
||||
|
||||
> 上述描述中`提交时将事务所有日志写入到磁盘的日志文件中`,这句话中`日志`代表`redo log 和 undo log`。
|
||||
> - redo log用于保证事务持久性
|
||||
> - undo log用于帮助事务回滚,也用于mvcc功能
|
||||
|
||||
#### redo log和undo log比较
|
||||
- redo log在mysql server运行时,只会被顺序的写入,server运行时并不会被读取
|
||||
- undo log则不同,在server运行时可能需要对事务进行回滚或执行mvcc操作,此时可能需要对undo log文件进行`随机读写`
|
||||
|
||||
#### redo log和binlog的比较
|
||||
binglog通常用于对数据库进行`point in time`形式的恢复(从某个时间点起恢复数据)以及主从复制;但是,binlog和redo log的差别如下:
|
||||
- binlog针对的是mysql数据库级别,不止用于innodb,还用于其他存储引擎
|
||||
- redo log属于innodb存储引擎级别
|
||||
- binlog实际记录的是对应sql语句,属于逻辑日志
|
||||
- redo log实际记录的格式则是物理格式,具体为针对某个页面的物理修改
|
||||
|
||||
redo log和 bin log日志写入磁盘的时机也有所不同:
|
||||
- bin log`仅在事务提交后才进行一次写入`
|
||||
- redo log`在事务执行过程中也可能发生写入`(redo log buffer满后会写入到磁盘中),故而,redo log file中同一事务写入的redo log内容可能并非是连续的,`多个事务在写入redo log时可能会交叉写入`
|
||||
|
||||
#### innodb_flush_log_at_trx_commit
|
||||
参数`innodb_flush_log_at_trx_commit`用于控制redo log/ undo log刷新到磁盘的策略,该参数默认值为`1`,即事务提交时刷新日志到磁盘。除了默认值之外,还可以为该参数设置如下值:
|
||||
- 0: 事务提交时不刷新日志到磁盘,仅在master thread中刷新日志到磁盘,master thread中刷新操作每秒触发一次
|
||||
- 2:事务提交时刷新日志,但是仅将日志写入到文件系统的缓存中,`并不进行fsync操作`
|
||||
|
||||
将`innodb_flush_log_at_trx_commit`参数设置为`0`或`2`虽然可以在一定幅度上提高性能,但是会丧失数据库的ACID特性。
|
||||
|
||||
#### log block
|
||||
在innodb中,redo都是以512字节的大小为单位进行存储的,即redo log buffer、redo log file都是以block的形式进行保存,block的大小为512字节。
|
||||
|
||||
##### block & atomic
|
||||
若针对相同的页,redo log的大小大于512字节,那么其会被分割为多个block进行存储。且由于redo log block的大小和磁盘扇区相同,故而在将block时,无需使用double write机制,针对特定的block,起写入为原子的,要么写入成功要么写入失败,不会像页(page)一样存在dirty的情况。
|
||||
|
||||
redo log block中包含的内容除了日志本身外,还包含`log block header`和`log block tailer`内容。`log block header + log block content + log block tailer`合计占用512字节,其中,各部分大小如下:
|
||||
- `log block header`: 12字节大小
|
||||
- `log block content`: 492字节大小
|
||||
- `log block tailer`: 8字节大小
|
||||
|
||||
如上所示,每个redo log block可实际存储的内容大小为492字节。
|
||||
|
||||
##### log block header
|
||||
log block header大小为12字节,由如下部分组成:
|
||||
- `LOG_BLOCK_HDR_NO`: 占用4字节
|
||||
- `LOG_BLOCK_HDR_DATA_LEN`: 占用2字节
|
||||
- `LOG_BLOCK_FIRST_REC_GROUP`: 占用2字节
|
||||
- `LOG_BLOCK_CHECKPOINT_NO`: 占用4字节
|
||||
|
||||
###### `LOG_BLOCK_HDR_NO`
|
||||
log buffer由log block所组成,可以将log buffer看作是log block的数组,故而,log block header中`LOG_BLOCK_HDR_NO`起代表当前block在buffer中的位置。
|
||||
|
||||
`LOG_BLOCK_HDR_NO`由于表示的是log buffer中的数组小标,故而可知`LOG_BLOCK_HDR_NO`其是递增的,并且可以循环使用。`LOG_BLOCK_HDR_NO`的大小为4字节,但是其首位用作`flush bit`,故而,其可表示的最大长度为`2^31 bytes = 2GiB`。
|
||||
|
||||
##### `LOG_BLOCK_HDR_DATA_LEN`
|
||||
`LOG_BLOCK_HDR_DATA_LEN`大小为2字节,代表`log block`所占用的大小,当log block被写满时,该值为`0x200`,表示当前log block使用完`block`中所有的可用空间,即log block的大小为512字节。
|
||||
|
||||
##### `LOG_BLOCK_FIRST_REC_GROUP`
|
||||
`LOG_BLOCK_FIRST_REC_GROUP`占用2个字节,表示log block中第一个日志所处的偏移量。
|
||||
|
||||
`LOG_BLOCK-FIRST_REC_GROUP`的取值可能存在如下场景:
|
||||
- 该值大小和`LOG_BLOCK_HDR_DATA_LEN`相同,则代表当前block中`不包含新的日志`(即当前block中存储的存储的全是上一block中record的后续部分)
|
||||
|
||||
下图表示`事务T1的重做日志占用762字节`,`事务T2的重做日志占用100字节`的场景。
|
||||
|
||||
<img alt="" class="has" src="https://i-blog.csdnimg.cn/blog_migrate/41c0ed462afe1e9d56e1e53f11175f9a.jpeg">
|
||||
|
||||
由于每个block中最多只能保存492字节的数据,故而T1事务的762字节需要分布在两个block中,第一个block保存492字节的数据,第二个block中保存剩余270字节的数据。
|
||||
|
||||
- 其中,左侧的block,其`LOG_BLOCK_FIRST_REC_GROUP`值为12,代表第一个record开始的位置紧接在log block header之后
|
||||
- 而右侧的block,其`LOG_BLOCK_FIRST_REC_GROUP`的值为`12 + 270 = 282 bytes`。在存放第一条record之前,不仅有log block header对应的12字节,还有之前T1剩余日志的270字节
|
||||
|
||||
##### `LOG_BLOCK_CHECKPOINT_NO`
|
||||
`LOG_BLOCK_CHECKPOINT_NO`占用4字节大小,代表log block最后被写入时的检查点第四字节的值。
|
||||
|
||||
LSN(log sequence number)为一个`全局唯一且单调递增的64位数字,当发生数据修改时,redo log内容会增加,此时LSN也会增加`。
|
||||
|
||||
CHECKPOINT则是一个LSN值,同样为64位整数,代表位于`CHECKPOINT`之前所有的修改已经被持久化到数据库中,`位于CHECKPOINT之前的redo log内容可以被安全的覆盖`。
|
||||
|
||||
##### log block tailer
|
||||
log block tailer中仅由一个部分组成,即`LOG_BLOCK_TRL_NO`,其值和`LOG_BLOCK_HDR_NO`相同。
|
||||
|
||||
#### log group
|
||||
log group被称为重做日志组,其中包含多个redo log文件,innodb中只有一个log group。
|
||||
|
||||
log group只是一个逻辑上的概念,由多个redo log file组成。log group中每个redo log file大小相同。redo log file中存储的是redo log block,`在innodb引擎运行过程中,会将redo log buffer中的log block刷新到磁盘文件中。`
|
||||
|
||||
##### redo log buffer刷新到磁盘中的时机
|
||||
redo log buffer会在如下时机将log block刷新到磁盘中:
|
||||
- 事务提交时
|
||||
- log buffer中有一半的内存空间已经被使用时
|
||||
- log checkpoint时(checkpoint时会导致脏页被刷新到磁盘上,而WAL要求脏页刷新前刷新redo log buffer)
|
||||
|
||||
##### WAL
|
||||
`write-ahead logging`是一种为database系统提供`原子性`与`持久性`的技术。
|
||||
|
||||
`write ahead log`是`append-only`的辅助磁盘存储结构,用于crash recovery和transaction recovery。
|
||||
|
||||
在使用`WAL`的系统中,在所有的changes被应用到数据库之前,要求changes都被写入到log中。
|
||||
|
||||
所以,在innodb中,脏页被刷新到磁盘之前,脏页对应的`newest_lsn`之前的redo log都必须被刷新到磁盘中。`redo log file中最新的lsn必须大于磁盘页文件中最大的lsn`。
|
||||
|
||||
在redo log buffer中的log block刷新到redo log file中时,其会追加(append)到redo log file的末尾。当redo log group中的一个文件被写满时,其会接着写入下一个redo log file,其行为称为`round-robin`。
|
||||
|
||||
在redo log group中的每个redo log file中,其前2KB(4个log block大小)均不用于存储log block,前2KB内容如下:
|
||||
- 对于log group中的第一个redo log file,前2KB用于存储如下内容,下列每个部分大小均为一个block:
|
||||
- log file header
|
||||
- checkpoint 1
|
||||
- 空
|
||||
- checkpoint2
|
||||
- 对于log group中`非第一个redo log file`,其仅保留开头2KB的空间,但并不保存信息
|
||||
|
||||
#### redo log format
|
||||
innodb中存储管理是基于页的,故而redo log format格式也基于页。
|
||||
|
||||
##### redo log头部格式
|
||||
redo log头部格式通常包含3部分:
|
||||
- `redo_log_type`: 重做日志类型
|
||||
- space: 表空间id
|
||||
- page_no: 页的偏移量
|
||||
|
||||
之后redo log body部分,根据重做日志类型的不同会存储不同内容。
|
||||
|
||||
#### LSN
|
||||
LSN代表的是日志序列号,其大小为8字节,且单调递增。`LSN代表事务写入redo log的总字节数`。
|
||||
|
||||
##### 页LSN
|
||||
在每个页的头部,都存在`FIL_PAGE_LSN`,其记录了该页的lsn。在页中,`LSN`代表该页最后刷新时的lsn大小。
|
||||
|
||||
FIL_PAGE_LSN在`buffer pool page`和`disk page`中均存在,二者记录的值不同:
|
||||
- `buffer pool page` header中`FIL_PAGE_LSN`记录的是`内存页最后被修改的LSN`
|
||||
- `disk page` header中`FIL_PAGE_LSN`记录的是最后被刷新到磁盘的页对应的最大修改LSN
|
||||
|
||||
在执行crash recovery过程中,会从CHECKPOINT开始,一直到redo log file末尾,逐条处理redo log record,对于每条redo log record关联的页,会比较`record_lsn`和`FIL_PAGE_LSN`的大小:
|
||||
- `record_lsn <= FIL_PAGE_LSN`:代表当前redo record对应的修改已经包含在页中,当前redo log record直接跳过即可
|
||||
- `record_lsn > FIL_PAGE_LSN`:代表当前redo record中的修改不存在于页中,需要对页应用record修改,并在修改完后更新页的`FIL_PAGE_LSN`
|
||||
|
||||
##### 查看LSN
|
||||
可以通过`show engine innodb status`来查看LSN情况,核心数值如下:
|
||||
- `log sequence number`:代表当前LSN
|
||||
- `log flushed up to`: 代表已经刷新到磁盘文件中的LSN
|
||||
- `last checkpoint at`: 代表页已经刷新到磁盘的LSN
|
||||
|
||||
#### recovery
|
||||
innodb在启动时,`不管上次数据库运行是否正常关闭,都会尝试执行恢复`。
|
||||
|
||||
`redo log是物理日志,故而恢复速度相较逻辑日志要快得多`,恢复操作仅需从`checkpoint`开始。
|
||||
|
||||
例如,对于如下数据表
|
||||
```sql
|
||||
create table t (
|
||||
a int,
|
||||
b int,
|
||||
primary key(a),
|
||||
key(b)
|
||||
);
|
||||
```
|
||||
若执行sql语句
|
||||
```sql
|
||||
insert into t select 1,2;
|
||||
```
|
||||
在执行时,需要修改如下内容:
|
||||
- 聚簇索引页(包含数据)
|
||||
- 辅助索引页
|
||||
|
||||
故而,其记录重做日志内容大致为
|
||||
```
|
||||
page(2,3), offset 32, value 1,2; # 聚簇索引页
|
||||
page(2,4), offset 64, value 2; # 辅助索引页
|
||||
```
|
||||
由上述示例可知,redo log为物理日志,记录的是对页的物理修改,故而`redo log是幂等的`。
|
||||
|
||||
### undo
|
||||
redo log记录了对页的物理操作,可以用于进行`redo`。而undo和redo不同,undo主要用于对事务的回滚。
|
||||
|
||||
undo的存放位置和redo不同:
|
||||
- redo log存放在redo log file中
|
||||
- undo log存放在数据库内部的segment中,该segment被称为`undo segment`。`undo段位于共享的表空间内`
|
||||
|
||||
#### undo log和redo log差异
|
||||
- redo log为物理日志,记录的是对页的修改;而undo log则是逻辑日志,对每个insert操作,undo log会生成一个相反的delete,对update也会生成另一个逆向的update
|
||||
- redo log是全局的,innodb中所有事务都会`向同一个redo log交叉写入`;而undo log则是针对事务的,每个事务都有其自己的undo log chain
|
||||
|
||||
#### 非锁定读
|
||||
除了用于事务回滚外,undo log还可以用于MVCC。当事务A尝试读取一条记录R时,如果记录R已经被另一个事务B占用,那么事务A可以通过undo log读取行数据之前的版本。
|
||||
|
||||
上述实现被称为`非锁定读`。
|
||||
|
||||
#### undo log的产生会伴随redo log的产生
|
||||
`undo log其本质仍然是数据`。undo log其存放在表空间的undo segment中,仍然可被可做是数据,而`WAL(write-ahead logging)要求变更被应用到数据库之前,需要先写入日志`。
|
||||
|
||||
故而,在生成undo log时,对于undo页的修改也会被记录到redo log中。
|
||||
|
||||
#### 存储管理
|
||||
innodb通过segment来管理undo log,其管理方式如下:
|
||||
- innodb包含rollback segment
|
||||
- 每个rollback segment会记录1024个undo log segment
|
||||
- undo log segment中会进行undo页的申请
|
||||
- 共享表空间偏移量为5的页会记录所有rollback segment header所在的页
|
||||
- 偏移量为5的页类型为FIL_PAGE_TYPE_SYS
|
||||
|
||||
##### innodb_undo_directory
|
||||
该参数用于设置rollback segment文件所在的路径,默认为`./`,代表`datadir`。
|
||||
|
||||
如果`innodb_undo_directory`变量没有被定义,那么undo tablespace将会被创建再`datadir`下。默认情况下,undo tablespaces文件的名称为`undo_001`和`undo_002`。
|
||||
|
||||
##### innodb_rollback_segments
|
||||
每个undo tablespace支持最大128个rollback segments,`innodb_rollback_segments`变量定义了rolback segments的数量。
|
||||
|
||||
每个rollback segments支持的事务数量由`rollback segment中undo slot的数量`和`每个事务需要的undo log数量`来决定。
|
||||
|
||||
> 当innodb页大小为16KB时,rollback segment中undo slot的数量为`innodb page size/ 16`,即1024个。
|
||||
|
||||
##### innodb_undo_tablespaces
|
||||
该变量设置了undo tablespaces的数量。
|
||||
|
||||
##### purge
|
||||
在事务提交之后,并不能立刻删除undo log以及undo log所在的页,`其他事务仍有可能通过undo log来还原数据行的之前版本`。故而,在事务提交时,会将undo log放入到一个链表中,交由purge线程来决定是否最终删除undo log以及undo log所在的页。
|
||||
|
||||
> purge代表`清空不再被需要的旧版本数据行及其对应的undo log记录。
|
||||
|
||||
如果为每一个事务分配一个单独的undo页,那么会非常浪费存储空间。由于事务提交时,所分配的undo页并不能立刻释放,故而,当数据库负载较大时,可能同时存在大量的undo页,会占用相当多的存储空间。
|
||||
|
||||
##### undo页的重用设计
|
||||
在innodb对undo页的设计中,考虑了对undo页的重用。当事务提交时,首先会将undo log放在链表中,然后判断undo页的使用空间是否小于`3/4`。如果是,代表该undo页可以被重用,之后新的undo log会记录在当前undo log的后面。
|
||||
|
||||
> 即undo page是可被重用的,当事务提交时,如果undo log的使用空间小于3/4,那么该undo页是可以被重用的,一个undo页中可能包含多个undo log
|
||||
|
||||
#### 核心概念
|
||||
##### rollback segment
|
||||
undo tablespace由rollback segment构成,每个undo tablespace最多支持128个rollback segment,`innodb_rollback_segments`定义了rollback segments的数量。
|
||||
|
||||
##### undo slots
|
||||
undo slot是rollback segment内的slot,由rollback segment进行管理。
|
||||
|
||||
undo slot主要用于关联undo segment,`当事务启动时,系统会从rollback segment中获取一个空闲的undo slot`,`成功获取undo slot后即代表关联了一个undo segment`。
|
||||
|
||||
每个undo log slot中会存储一个`page_no`,其指向undo log segment的起始页位置。
|
||||
|
||||
##### undo segment
|
||||
undo segment为undo slot指向的空间,undo segment中包含多个undo pages,而undo segment中则包含了undo log。
|
||||
|
||||
#### undo log格式
|
||||
在innodb存储引擎中,undo log分为如下两种类型:
|
||||
- insert undo log:
|
||||
- insert undo log是事务在insert操作中产生的undo log
|
||||
- 在事务提交之前,事务插入的数据对其他事务不可见;而事务提交之后,事务插入的数据对其他读已提交的事务才可见;故而,insert undo log在事务提交之后不再被需要,因为在读已提交隔离级别下,insert undo log是可见的;在可重复读的隔离级别下,insert undo log则是不可见的,没有中间版本,只能可见/不可见
|
||||
- 故而,当事务提交之后,即可删除该事务关联的insert undo log
|
||||
- update undo log
|
||||
|
||||
##### insert undo log
|
||||
在事务提交之后,insert undo log即可被删除,故而无需purge操作。
|
||||
|
||||
insert undo log结构如下:
|
||||
- next:下一个undo log的位置,长度为2字节
|
||||
- type_cmpl: 记录undo的类型,对于insert undo来说,该值为11。该字段长度为1字节
|
||||
- undo_no:记录事务的id(压缩后保存)
|
||||
- table_id: 记录undo log对应的表对象(压缩后保存)
|
||||
- 记录所有主键的列和值(本次插入的数据,可能多条)
|
||||
- start:位于undo log尾部,记录undo log的开始位置,长度为2字节
|
||||
|
||||
> ##### rollback
|
||||
> 在执行rollback操作时,可以根据insert undo log中存储的table id,主键列、主键值来定位需要回滚的行数据,直接删除回滚数据即可。
|
||||
|
||||
<img src="https://ts3.tc.mm.bing.net/th/id/OIP-C.DUFx18elzLQKwUNUss-FLgAAAA?rs=1&pid=ImgDetMain&o=7&rm=3" alt="mysql redo log 事务大_MySQL事务实现及Redo Log和Undo Log详解-CSDN博客" class=" nofocus" tabindex="0" aria-label="mysql redo log 事务大_MySQL事务实现及Redo Log和Undo Log详解-CSDN博客" role="button">
|
||||
|
||||
##### update undo log
|
||||
update undo log针对的是`delete`和`update`操作。在mvcc机制的实现中,需要用到该undo log,故而`update undo log在事务提交后不能立刻删除`。
|
||||
|
||||
update undo log的格式如下图所示。
|
||||
|
||||
<img src="https://img-blog.csdnimg.cn/20190917162925836.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3NhcmFmaW5hNTI3,size_16,color_FFFFFF,t_70" alt="MySQL的redo log、undo log、binlog_mysql redolog undolog binlog-CSDN博客" class=" nofocus" tabindex="0" aria-label="MySQL的redo log、undo log、binlog_mysql redolog undolog binlog-CSDN博客" role="button">
|
||||
|
||||
对于update undo log,其哥字段含义如下:
|
||||
- type_cmpl: 对update undo log,其`type_cmpl`可能的取值如下:
|
||||
- 12:TRX_UNDO_UPD_EXIST_REC,更新未被标记为delete的记录
|
||||
- 13:TRX_UNDO_UPD_DEL_REC,更新`已经被标记为delete的记录`
|
||||
- 14:TRX_UNDO_DEL_MARK_REC,将记录标记为delete
|
||||
- update vector: update vector中记录了update操作导致发生改变的列,每个被修改的列信息都记录在undo log中。
|
||||
|
||||
> 在执行delete操作时,并不会直接对行数据进行物理删除操作,而是`将行数据标记为delete`,待后续purge操作中才会实际对数据进行物理删除。
|
||||
|
||||
undo log中主要存储旧的col值,用于在回滚或mvcc时为undo操作提供信息,还原数据先前的版本。
|
||||
|
||||
在实际生成insert/update undo log时,对于插入、更新、删除等操作,其实际生成undo log的方式如下:
|
||||
- insert操作:
|
||||
- 对于insert操作,其实际生成一条insert undo log,`type_cmpl`的值为`TRX_UNDO_INSERT_REC`
|
||||
- delete操作:
|
||||
- 对于delete操作,其实际生成一条`type_cmpl`值为`TRX_UNDO_DEL_MARK_REC`的undo log
|
||||
- delete操作并不会直接对记录进行物理删除,而是将记录标记为delete,后续进行purge操作时才会实际删除
|
||||
- update记录的非主键值:
|
||||
- 在对记录的非主键值进行update时,会生成一条`type_cmpl`值为`TRX_UNDO_UPD_EXIST_REC`的记录,用于对更新操作进行回滚
|
||||
- update记录的主键值
|
||||
- 当对记录主键列进行修改时,会生成两条undo log:
|
||||
- `TRX_UNDO_DEL_MARK_REC`类型的记录,对应`将原记录标记为删除`的回滚
|
||||
- `TRX_UNDO_INSERT_REC`类型的记录,标记对`新纪录`的插入回滚
|
||||
|
||||
> 对于insert/update undo log record,每条undo record均只针对一条记录。故而,当一条sql语句对多行记录进行删除/更改时,会生成多条insert/update undo record。
|
||||
|
||||
### undo log组织形式
|
||||
上述内容介绍了undo log record的格式,为了对undo log records进行高效率的访问和管理,undo log record应当以适当的形式被组织。
|
||||
|
||||
#### undo log的逻辑组织方式
|
||||
每个事务都会修改一系列的records,并产生相应的undo log records,事务对应的undo log由这些undo log records组成。
|
||||
|
||||
undo log除了由undo log record组成外,其还包含了一个undo log header,结构如下:
|
||||
|
||||
<img src="https://yqintl.alicdn.com/4eeedc0d845c26417e570782cb01824f98d7dcd3.png" alt="3" title="3">
|
||||
|
||||
如上述结构所示,undo log header中包含如下部分:
|
||||
- trx id: 事务id,用于标识产生该undo log的事务
|
||||
- trx no: trx no用于表示事务的提交顺序(header中trx no并非一定有值,在事务提交前,该field未定义,当事务提交后,该field会被填充)
|
||||
- `trx no`用于判断`是否purge应当被执行`
|
||||
- delete mark: 判断是否undo log中存在`TRX_UNDO_DEL_MARK_REC`类型的undo record,从而在purge时避免扫描
|
||||
- log start offset: 该field记录了undo log header结尾的位置,便于后续对undo log header进行拓展
|
||||
- History List Node:undo log通过该结构挂载到History List中
|
||||
|
||||
> 故而,undo log就粒度存在两种结构
|
||||
> - undo record: 记录每行数据的历史版本,行数据通过维护undo record chain来还原历史版本
|
||||
> - undo log: 针对事务级别,每个undo log由undo log header和多个undo records组成,并且undo log会根据事务的提交顺序被挂载到history list中
|
||||
|
||||
##### record versions
|
||||
如果多个事务针对同一条record进行了修改(串行),那么,每个事务对record造成的修改都会生成不同的版本(version),不同的版本之间通过链表进行链接,用于后续的mvcc,示例如下:
|
||||
|
||||
<img src="https://yqintl.alicdn.com/ee4f6e18f7fb39e128e06d26daed3432ff203390.png" alt="4" title="4">
|
||||
|
||||
如上所示,事务`I, J, K`都对`id=1`的记录进行了操作,其操作顺序如下:
|
||||
1. 事务I插入了id为1的记录,并且将`a`的值设置为A
|
||||
2. 事务J将id为1记录的`a`修改为了B
|
||||
3. 事务K将id为1记录的`a`修改为了C
|
||||
|
||||
事务I, J, K都有其事务对应的undo log,并且每个undo log都拥有对应的undo log header和undo log records。
|
||||
|
||||
通过index中的记录及其rollptr引用的链表,可以对记录关联的`I, J, K`三个版本进行还原。
|
||||
|
||||
#### 物理组织方式
|
||||
undo log的结构如上述所示,我们无法控制事务生成undo log的大小(当事务操作大量的数据时,其undo log对应的undo records数量也会变多,undo log大小更大)。但是,`最终生成undo log写入磁盘时,会基于固定的大小进行写入(16KB)`。
|
||||
|
||||
由于事务关联`undo log`的大小是无法控制的,其大小可能需要多个页来进行存储。故而,`对占用空间较大的undo log,undo page会按照需要进行分配`;`而对于大小较小的undo log,会将多个undo log放置在同一个undo page中`。
|
||||
|
||||
> undo log和undo page的对应关系是灵活的,既可能一个undo log占用多个undo pages,也可能多个undo log共用相同的undo page
|
||||
|
||||
关于undo log的物理组织方式,如下图所示:
|
||||
<img src="https://yqintl.alicdn.com/4680cd1e8572e5f39310973603c43445a49ee236.png" alt="5" title="5">
|
||||
|
||||
##### undo segment
|
||||
每当一个事务开启时,都需要持有一个undo segment,对于undo segment中磁盘空间的释放和占用(对16KB页的释放和占用)都由FSP segment进行管理。
|
||||
|
||||
undo segment中会至少持有一个undo page,并且,`每个undo page都会记录undo page header`。
|
||||
|
||||
##### undo page header
|
||||
undo page header中包含如下内容:
|
||||
- undo page type:undo page的类型
|
||||
- last log record offset:最后一条记录的offset
|
||||
- free space offset:page中空闲空间的offset
|
||||
- undo page list node:指向List中的下一个undo page
|
||||
|
||||
undo segment中的第一个undo page除了undo page header外,`还会记录undo segment header`。
|
||||
|
||||
##### undo segment header
|
||||
undo segment header中包含如下内容:
|
||||
- state:该field记录了udno segment的状态(TRX_UNDO_CACHED/TRX_UNDO_PURGE)
|
||||
- undo segment中最后一条undo record的位置
|
||||
- 当前segment被分配的undo page组成的链表
|
||||
|
||||
##### undo log storing
|
||||
undo page中的空间用于存储undo log,对于`大小较小的undo log`innodb会对undo page进行reuse,在undo page中存储多个undo logs避免浪费空间。而对于大小较大的undo log,会使用多个undo page来对该undo log进行存储。(undo page reuse只会发生在segment的第一个page)。
|
||||
|
||||
#### 文件组织方式
|
||||
在同一时刻,一个undo segment只属于一个事务,一个undo segment无法同时被多个事务共享。每当事务开启时,都会获取一个undo segment,故而,在多个事务并行的运行时,需要多个undo segment。
|
||||
|
||||
##### rollback segment
|
||||
在innodb中,每个rollback segment中包含了1024个undo slot,而每个undo tablespace最多包含128个rollback segment。
|
||||
|
||||
rollback segment header中包含了128个slot,每个slot包含4字节,并且都指向`某个undo segment的第一页`。
|
||||
|
||||
undo slot的数量会影响innodb数据库中的事务并行程度,
|
||||
|
||||
#### 内存组织方式
|
||||
innodb中针对undo log的内存数据奇结构如下:
|
||||
<img src="https://yqintl.alicdn.com/70a8768288fddc5a8e5b9c0845e004e374392211.png" alt="7" title="7" data-spm-anchor-id="a2c65.11461447.0.i1.319b654a1qaiie">
|
||||
|
||||
##### undo::Tablespace
|
||||
对于磁盘中的每个undo tablespace,都会在内存中维护一个`undo::Tablespace`结构,`undo::Tablespace`结构中,最重要的部分是`trx_rseg_t`。
|
||||
|
||||
##### trx_rseg_t
|
||||
`trx_rseg_t`关联了rollback segment header。除了基础元数据之外,其还包含了四个`trx_undo_t`类型的链表:
|
||||
- Update List: update list中包含`用于记录update undo record的undo segments`
|
||||
- Update Cached:update cached list中包含可以被重用的update undo segments
|
||||
- Insert List:insert list中包含了正在使用的insert undo segments
|
||||
- Insert Cached:isnert cached list中包含了后续可以被重用的insert undo segments
|
||||
|
||||
##### trx_undo_t
|
||||
trx_undo_t关联的则是上面描述的undo segment。
|
||||
|
||||
#### undo writing
|
||||
当一个写事务开启时,将会通过`trx_assign_rseg_durable`分配Rollback Segment,内存中的trx_t也会指向对应的trx_rseg_t内存结构。
|
||||
|
||||
rollback segment的分配策略很简单,会依次尝试下一个活跃的rollback segment。在此之后,如果事务内的第一条修改命令需要写undo record,将会调用`trx_undo_assign_undo`来获取undo segment。在获取undo segment时,`trx_rseg_t`中包含的cached list中节点将会被优先使用。
|
||||
> 如果已经存在`已分配但是未被使用的undo segment`,将会优先使用这部分undo segment。
|
||||
>
|
||||
> 如果不存在已分配但是未被使用的undo segment,将会调用trx_undo_create创建新的undo segment。
|
||||
|
||||
##### undo record写入
|
||||
当undo page被写满后,会调用`trx_undo_add_page`来向当前undo segment中添加新的undo page。在将undo record写入到undo page时,存在如下约束:
|
||||
- 一条undo record无法跨页面存在于两个page,如果当前page剩下的空间不足以写入undo record,那么会将undo record写入到下一个undo page
|
||||
|
||||
在事务完成之后(提交/回滚),
|
||||
- `如果当前undo segment中只使用了一个undo page,并且undo page的使用率小于75%`,那么该undo segment将会被保留并添加到对应的insert/update cachedlist中
|
||||
- `如果undo segment中undo page大于一个,或者undo page的使用率大于75%`,如么对于undo segment的处理如下:
|
||||
- 对于insert类型的undo segment,其会在事务提交/回滚时直接被回收
|
||||
- 对于update类型的undo segment,其会在purge完成后被回收
|
||||
|
||||
根据场景的不同,undo segment其header的状态将会从`TRX_UNDO_ACTIVE`变为`TRX_UNDO_FREE`/`TRX_UNDO_TO_PURGE`/`TRX_UNDO_CACHED`,其代表了innodb事务的结束。
|
||||
|
||||
#### undo for rollback
|
||||
在回滚时,会按照undo record生成的时序,逆向进行回滚。回滚根据undo record的内容。
|
||||
|
||||
#### undo for mvcc
|
||||
mvcc中`“多版本”设计是为了读事务和写事务之间互相等待`。在传统的读写锁设计中,读写/写写之间都是互斥的,只要有事务持有读锁,则其他事务拿不到写锁;如果有事务持有写锁,则其他事务拿不到读锁和写锁。在多事务同时进行并发操作的场景下,如果采用读写锁进行相互等待,那么将会对性能造成很大的影响。
|
||||
|
||||
在采用多版本的设计后,每个读事务在对record进行读取时,`无需对record添加锁,而是查看读事务可见的历史版本即可`。
|
||||
|
||||
##### 历史版本
|
||||
在多版本设计中,“历史版本”在设计上等效于如下描述:
|
||||
- 在读事务开启时,会针对整个数据库中的生成一个快照,后续该事务的读操作都从该快照中获取数据
|
||||
|
||||
### purge
|
||||
表`t`中,`a`为聚簇索引,`b`为辅助索引,若执行如下sql
|
||||
```sql
|
||||
delete from t where a=1;
|
||||
```
|
||||
|
||||
上图示例中,undo segment中包含了2个page和3个undo record:
|
||||
- 第一个undo page中包含了undo record 1
|
||||
- 第二个undo page中包含了undo record 2和undo record 3
|
||||
|
||||
回滚流程如下所示:
|
||||
- 从undo segment header中获取last undo page header的位置
|
||||
- 从last undo page header中获取最后一条undo record的位置,即undo record 3的位置,并根据undo record 3的内容执行回滚操作
|
||||
- 根据prev record offset的值来获取上一条undo record的位置,如此逆向遍历undo page中所有的undo record,并且执行回滚操作
|
||||
- 在当前page中,如果所有的undo records都执行完回滚操作,会根据undo page header查询前一个undo page,并按照上述流程执行回滚操作
|
||||
|
||||
|
||||
|
||||
上述语句造成的修改如下:
|
||||
- 将主键`a`为1的记录标记为delete,将记录`delete flag`设置为1(聚簇索引中的数据并没有被实际物理删除),`且会生成针对聚簇索引的undo log`。当事务发生回滚时,仅需将`delete flag`重新设置为0即可完成对删除的回滚。
|
||||
- 对于辅助索引中满足`a=1`的记录,同样不会做任何处理,`也不会产生针对辅助索引的undo log`
|
||||
|
||||
> undo log只针对聚簇索引生成,对于辅助索引的变化并不会生成对应的undo log。
|
||||
>
|
||||
> 从上文的undo log内容来看,update undo log和insert undo log都记录了唯一索引(聚簇索引)的值,并且update undo log记录了各修改列的oldValue,二者均不涉及辅助索引内容。
|
||||
|
||||
可知在delete语句执行时,并不会马上就对记录进行物理删除,而是将记录标记为delete,记录实际的删除在purge操作时才被执行。
|
||||
|
||||
对于记录的`delete`操作和`update`操作(update主键列时,会先将原记录标记为删除,后插入一条新的记录),会在purge操作中完成。purge确保了innodb存储引擎对于MVCC机制的支持,记录不能再事务提交时立刻进行处理,仍会有其他事务会访问该记录的旧版本。
|
||||
|
||||
是否可以对记录进行物理删除由purge来决定,若某行记录不被其他任何事务引用,可以执行物理delete操作。
|
||||
|
||||
#### history
|
||||
innodb存储引擎中维护了一个history列表,其`根据事务的提交顺序`对undo log进行链接,`在history list中,先提交的事务其undo log位于history list的尾端`。
|
||||
|
||||
<img src="https://img-blog.csdnimg.cn/8bf680dd74014ed593d9f51a06448667.png" alt="Mysql undo log_mysql undolog-CSDN博客" class=" nofocus" tabindex="0" aria-label="Mysql undo log_mysql undolog-CSDN博客" role="button" data-bm="6">
|
||||
|
||||
##### history的purge流程
|
||||
已知,先提交的事务位于history list的尾端,故而,purge操作会从尾端开始查找需要被清理的记录。以上图为例,purge操作执行流程如下:
|
||||
- 从尾端找到第一个需要被清理的记录,此处为trx1
|
||||
- 清理完trx1之后,其会在trx 1的undo log为位于的undo page中继续查找是否可清空其他记录,此处可以找到`trx3`,`trx5被其他事务引用不能清理
|
||||
- 再次去history list中查找,找到trx2,清理trx2
|
||||
- 找到trx2 undo log所在的undo页,然后清理trx 6和trx 4
|
||||
|
||||
上述流程中,purge会首先从history list中查找undo log,然后会随便清除undo log位于的undo page中其他可以被清理的undo log,`这样能够减少page的随机访问次数,提高性能`。
|
||||
|
||||
##### innodb_purge_batch_size
|
||||
`innodb_purge_batch_size`用于设置每次purge操作需要清理的undo page数量,该值默认为`300`。
|
||||
|
||||
通常,该值设置越大,每次回收的undo page越多,可供重用的undo page也越多,会减少新开undo page的开销。但是,该值设置过大时,会增加purge占用的cpu和io资源,令用户线程的资源减少。
|
||||
|
||||
##### innodb_max_purge_lag
|
||||
当innodb存储引擎压力较大时,并无法高效进行purge操作。此时,history list长度会越来越长。
|
||||
|
||||
参数`innodb_max_purge_lag`用于控制history list的长度,当history list长度大于`innodb_max_purge_lag`的值时(`innodb_max_purge_lag`值需大于0),会对用户的DML操作进行延缓,延缓算法如下:
|
||||
```
|
||||
delay = ((length(history_list) - innodb_max_purge_lag) * 10) - 5
|
||||
```
|
||||
其中,delay的值为ms,且delay针对的是每一行。例如,当一个update操作需要更新5行数据时,每行数据都要被delay,故而故而该update操作的总delay为`5 * delay`。
|
||||
|
||||
delay会在每一次purge操作完成后重新计算。
|
||||
|
||||
##### innodb_max_purge_lag_delay
|
||||
该参数用于控制delay的最大值。当基于公式计算出的delay大于`innodb_max_purge_lag_delay`时,delay的值取`innodb_max_purge_lag_delay`,避免delay值过大导致用户线程无限等待。
|
||||
|
||||
### group commit
|
||||
#### binlog
|
||||
在mysql中,binlog通常用于replication和point-in-time recovery等方面。在binlog中,每个修改数据库内容的事件都会被记录。
|
||||
|
||||
binlog是一系列log files的集合,其中记录了mysql的数据修改和结构变化,binlog确保了所有事务都能够在slave servers之间准确的被复制,维护了在分布式环境下的一致性。如果发生宕机,可以通过binlog将数据库寰宇拿到其已知的`最后良好状态`。
|
||||
|
||||
##### replication && point-in-time recovery
|
||||
`replication`涉及到从mysql master server到salve server复制数据的过程。通过binlog,slave server能够按照事件发生的顺序从master server中复制数据。
|
||||
|
||||
`point-in-time recovery`则是支持将数据库恢复到先前的某一个时间点,通过binlog的replay即可实现。
|
||||
|
||||
##### binlog format
|
||||
mysql支持三种binlog类型
|
||||
- row:其会记录row的变更,提供受影响数据的详细细节
|
||||
- statement:其会记录`造成修改的实际sql语句`。其记录的信息不如row详细,但是会更加紧凑
|
||||
- mixed:其整合了row和statement格式,会基于不同事务的需要动态在`row`和`statement`之间进行切换
|
||||
|
||||
#### group commit
|
||||
group commit通常用于提高事务处理系统的性能。group commit将多个事务绑定在一起,系统能够减少磁盘写入次数并且条性能,特别是在事务负载较高的系统中。
|
||||
|
||||
##### group commit机制
|
||||
group commit允许多个事务合并为一次提交,而不是每个事务单独提交。该方案会减少磁盘的io操作,提高整体吞吐量。在高并发场景下,相比于每个事务单独提交,group commit机制能够显著提高性能。
|
||||
|
||||
##### `binlog_order_commits`
|
||||
`binlog_order_commits`为global variables,并且无需停止server的运行就能对该变量进行设置。
|
||||
|
||||
如果该变量的值为`off(0)`,则代表事务可以被并行提交。在部分场景下,其能够带来性能的提升。
|
||||
|
||||
##### `binlog_max_flush_queue_time = microseconds`
|
||||
|
||||
##### binlog details
|
||||
当server在执行事务时,会收集事务造成的修改,并将其存放到`per-connection`的`transaction cache`中。如果使用了statement-based replication,那么statement将会被写入到transaction cache中;如果使用了row-based replication,那么row changes将会被写入到transaction cache中。一旦事务提交,transaction cache将会作为一个single block被写入到binary log中。
|
||||
|
||||
上述的binlog写入流程能够令每个session都独立的执行(每个connection都有其独立的transaction cache),并且仅需在写入transaction data到binary log时加锁即可。由于事务之间是相互隔离的,故而在提交时对事务进行序列化即可。
|
||||
|
||||
##### sync the storage engine and binary log
|
||||
binlog机制mysql上层的,和存储引擎无关,而redo log机制是和存储引擎绑定的。为了对存储引擎和binlog进行同步,server使用了2PC协议(tow-phase commit protocol)。
|
||||
|
||||
<img src="https://img-blog.csdn.net/20161222103535710?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvc2hhb2NoZW5zaHVv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center" alt="">
|
||||
|
||||
如上图所示,使用2pc提交协议是为了能够保证`要么事务同时位于engine和binlog中,要么事务都不位于engine和binlog中`。故而,不应该出现`事务位于binlog却不位于engine中,或事务出现在engine中却不出现在binlog中`的场景,即使server在prepare后发生崩溃并后续恢复。
|
||||
|
||||
通过两阶段提交,可以实现上述需要求,一旦事务在engine中prepare,其可以被完全提交或完全回滚(即engine和binlog全部提交或全部回滚),即使prepare后server发生崩溃并后续恢复。在recovery阶段,存储引擎能够提供所有prepared但是尚未提交的事务,并且之后:
|
||||
- 如果事务可以在binlog被找到,那么事务将会被提交
|
||||
- 如果事务在binlog中找不到,那么事务将会被回滚
|
||||
|
||||
> 如果`innodb_flush_log_at_trx_commit`被设置为1,那么在2pc的prepare阶段就被持久化到磁盘中,后续再对binlog进行write和fsync。故而,当执行crash recovery时,如果redo log为prepared状态,且对应事务在binlog中存在,那么事务在redo log中一定存在,可以直接对redo log进行commit;但如果事务在binlog中不存在,那么可以对innodb事务进行回滚。
|
||||
|
||||
在大多场景下,如果binlog不存在,那么仅需对所有prepared transaction进行回滚即可。
|
||||
|
||||
> 当使用`on-line backup`方法(例如`Innodb Hot Backup`)时,这些工具会直接拷贝当前数据库的文件和innodb的transaction logs。但是,transaction logs也有可能包含prepared状态的事务。在通过备份恢复时,也会对所有prepared事务及逆行回滚,令数据库处于一致的状态。
|
||||
>
|
||||
> 并且,`on-line backup`经常被用于启动新slave。`last committed transaction`在binary log中的log position被记录到了redo log header中,在recovery时,recovery program将会打印binary log中last committed transaction的位置。
|
||||
|
||||
##### prepare_commit_mutex
|
||||
为了保证`on-line backup`行为的正确性,事务进行提交的顺序必须要和其被写入到binlog中的顺序一致,如果不能保证顺序的一致性,将会出现如下问题:
|
||||
- 如果事务按照`T1, T2, T3`的顺序写入到binlog,但是其提交顺序为`T1, T3, T2`,那么将会导致根据binlog执行恢复时,binlog中间出现空洞:例如,T1和T3已经被提交,但是T2尚未被提交,导致binlog中T1和T3的binlog block都是commit,但是T2却是prepared
|
||||
|
||||
<img src="https://img-blog.csdn.net/20161222103606133?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvc2hhb2NoZW5zaHVv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center" alt="">
|
||||
|
||||
<img src="https://img-blog.csdn.net/20161222103703259?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvc2hhb2NoZW5zaHVv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center" alt="">
|
||||
|
||||
> 上图中,事务提交顺序为`T2, T3`,但是binlog中的顺序为`T1, T2, T3`。从而导致位于binlog中最靠前位置的T1还未提交,为prepared状态;但是T2和T3的binlog处于commit状态,从而binlog发生空洞
|
||||
|
||||
在slave机器上发生recovery时,如果binlog存在空洞,slave将会直接跳过空洞,从而导致slave中事务缺失。
|
||||
|
||||
为了解决该问题,innodb补充了`prepare_commit_mutex`,当对事务进行prepare时,该mutex将会被获取;并且在事务commit时被释放。故而,在多事务场景下,一个事务`prepare-write-flush-commit`流程中不会插入其他操作,binlog写入事务的顺序和innodb存储引擎中事务提交的顺序是一致的。
|
||||
|
||||
> `prepare_commit_mutex`会将执行`prepare-write-commit`操作的事务串行化,由于将binlog写入到文件比较耗时,故而引入prepare_commit_mutext会造成bin log写入较慢。
|
||||
|
||||
##### binary log group commit
|
||||
为了在`保证事务写binlog顺序和innodb存储引擎提交顺序一致`的基础上提高性能,故而不应采用`prepare_commit_mutex`,而是采用了如下设计。
|
||||
|
||||
`binary log group commit`实现将commit过程拆分为了如下图所示的多个阶段:
|
||||
|
||||
<img src="https://img-blog.csdn.net/20161222103803619?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvc2hhb2NoZW5zaHVv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center" alt="">
|
||||
|
||||
上述stages位于binlog commit过程的内部,并且不会造成其他影响。由于commit过程被拆分为了如下stages,故而可以同时存在数个线程同时对事务进行处理,这样会增加吞吐量。
|
||||
|
||||
对于每个stage,都存在一个queue,session在queue中排队等待被处理。如果一个session被注册到了空的queue,那么其将被认为是stage leader,如果session被注册时queue不为空,那么其将作为stage follower。stage leader将会将queue中所有的threads经过stage处理并且将其注册到下一个stage,并且将leader和所有的followers注册到下个stage。
|
||||
|
||||
followers会被移动,并且等待leader通知整个commit都执行完成。
|
||||
|
||||
leader可能会注册到一个非空queue中,即leader可以决定成为一个follower;但是follower永远不会变成leader。
|
||||
|
||||
当leader进入到stage后,会一次性将queue抓取并依次处理queue中排队的session。当queue被抓取后,当leader在处理old queue时,其余的session又可以被注册到stage。
|
||||
|
||||
- `Flush Stage`: 在flush stage,所有注册到queue的session都会将其cache写入到binlog中。`(实际将内容刷新到系统缓冲区中,但并不实际调用fsync,fsync的调用发生在sync stage)`
|
||||
- `sync stage`:在sync stage,会根据`sync_binlog`的设置将binary log同步到磁盘中。如果`sync_binlog`值为1,所有被flushed的session都会被同步到磁盘中
|
||||
- `commit stage`:在commit stage,所有session将会按照其register的顺序在engine中进行提交,该步骤由stage leader完成。由于在commit procedure中,每个stage都保留了顺序,那么binlog中写入事务的顺序和引擎中事务的提交顺序是一致的。
|
||||
|
||||
当commit stage执行完成后,commit stage queue中所有的线程都将被标记为完成,并且会向所有线程都发送signal令其继续执行。
|
||||
|
||||
由于leader注册到下一个stage时可能变为follower,最慢的stage可能会积累最多的工作。通常情况下,sync stage会积累最多的工作。但是,向flush阶段填充尽可能多的事务是至关重要的,flush stage会被单独处理。
|
||||
|
||||
在flush stage,leader将会逐个扫描flush queue中的sessions,扫描过程在满足下列任一条件时终止:
|
||||
- 当queue为空时,leader将会立即进入下一阶段,并且将所有sessions注册到sync stage中
|
||||
- 如果`从第一个任务被unqueue起`已经超过时长限制`binlog_max_flush_queue_time`,那么整个queue都会被抓取,并且sessions的transaction cache都会被flushed(处理流程和未超时场景下一致)。之后leader再回进入到下一阶段。
|
||||
|
||||
> 通过将sessions在stage queue进行排队,在write、sync、commit多个阶段都能进行group处理。这样能够提高多事务场景下的事务提交性能,通过binary log group commit也能大大减少fsync的磁盘写入次数。
|
||||
|
||||
### 事务控制语句
|
||||
在默认设置下,mysql命令行的事务为自动提交的,在执行完sql后马上就会执行commit操作。可以通过`begin`和`start transaction`命令显式的开启一个事务。
|
||||
|
||||
事务的控制语句如下:
|
||||
- `start transaction|commit`:显式开启一个事务
|
||||
- `commit`:提交事务
|
||||
- `rollback`:回滚事务
|
||||
- `savepoint identifier`:savepoint指令允许在事务中创建一个保存点,一个事务中允许有多个保存点
|
||||
- `release savepoint identifer`:删除一个事务的保存点,当保存点不存在时会抛出异常
|
||||
- `rollback to [savepoint] identifier`:该指令用用于将事务回滚到指定的保存点。该指令通常和`savepoint`一起使用
|
||||
- `set transaction`:该指令用于设置事务的隔离级别,innodb存储引擎提供了`read uncommitted`,`read committed`,`repeatable read`,`serializable`四种隔离级别
|
||||
|
||||
在mysql中,`start transaction`和`begin`都能够开启一个事务,但是在存储过程中,mysql会自动将`begin`识别为`begin ... end`,`故而在存储过程中只能通过start transaction来开启事务`。
|
||||
|
||||
#### rollback
|
||||
在mysql中,一条语句的执行,要么完全成功,要么完全回滚,`并且一条语句失败抛出异常时,并不会导致之前已经执行的语句回滚`,所有的修改都会保留,必须由用户来决定是否对其进行提交或回滚。
|
||||
|
||||
#### rollback to savepoint
|
||||
`rollback to savepoint`语句并不会实际的对事务进行提交或回滚,而是将事务恢复到一个保存点的状态,在执行完`rollback to savepoint`后,仍然需要用户对事务进行整体的提交或回滚。
|
||||
|
||||
### 事务操作统计
|
||||
innodb存储引擎是支持事务的,故而使用innodb存储引擎的应用除了要考虑`每秒请求数`(QPS)之外,还需要考虑每秒的事务处理能力(`TPS`)。
|
||||
|
||||
TPS的计算方式如下:`(com_commit + com_rollback)/time`。使用该计算方式的前提为`所有事务都是显式提交/回滚的`,如果存在隐式的提交/回滚,则并不会计算到`com_commit/com_rollback`变量中。
|
||||
|
||||
### 事务隔离级别
|
||||
如果想要设置事务的隔离级别,可以使用如下命令
|
||||
```sql
|
||||
set [global | session] transaction isolation level
|
||||
{
|
||||
read uncommitted
|
||||
| read committed
|
||||
| repeatable read
|
||||
| serializable
|
||||
}
|
||||
```
|
||||
如果想要在事务启动时就设置事务的默认隔离级别,需要修改mysql的配置文件,在`[mysql]`中添加如下内容:
|
||||
```
|
||||
[mysql]
|
||||
transaction-isolation=READ-COMMITTED
|
||||
```
|
||||
如果需要查看当前的事务隔离级别,可以按照如下方式:
|
||||
- session:
|
||||
- `select @@tx_isolation`
|
||||
- global
|
||||
- `select @@global.tx_isolation`
|
||||
|
||||
在serializable隔离级别下,innodb会为每个select语句都自动添加`lock in share mode`,故而对`非锁定读`不再支持。(写操作发生时会占用排他锁,而排他锁和共享锁互斥)。
|
||||
|
||||
|
||||
|
||||
### 长事务
|
||||
长事务是指执行时间较长的事务,对于长事务而言,因硬件或操作系统发生问题导致事务回滚的代价是难以接受的(事务回滚花费的时间甚至会多于正向执行时花费的时间)。对于长事务而言,有时可以通过将其转化为小批量的事务来处理。当事务发生错误时,可以仅回滚一小部分数据,然后从上次已经完成的部分来继续处理。
|
||||
|
||||
在事务执行前,可以先通过`lock in share mode`来获取共享锁,保证事务在处理过程中没有其他的事务可以修改数据。
|
||||
|
||||
144
mysql/mysql文档/mysql_备份.md
Normal file
144
mysql/mysql文档/mysql_备份.md
Normal file
@@ -0,0 +1,144 @@
|
||||
# 备份
|
||||
## 概述
|
||||
通常来讲,备份分为如下类型:
|
||||
- Hot Backup(热备):在数据库运行的同时直接备份,同时也被称为`在线备份`
|
||||
- Cold Backup(冷备):在数据库停止的情况下进行备份,一般只需要复制数据库的物理文件`离线备份`
|
||||
- Warm Backup(温备):Warm Backup类似于Hot Backup,同样也是在数据库运行时进行备份,但是和Hot Backup相比,Warm Backup会对当前数据库的操作存在影响(`例如,在备份时添加全局的共享锁,从而保证备份数据的一致性`)、
|
||||
|
||||
按照备份后的文件内容,也可以对备份做出如下划分:
|
||||
- 逻辑备份:对mysql而言,逻辑备份代表备份后产生的文件,其文件内容是可读的,一般由文本组成,内容是`sql语句`或是`表内的实际数据`
|
||||
- 常用的逻辑备份有`mysqldump`和`select * into outfile`等,这种方式可以观察导出文件的内容
|
||||
- 对于逻辑备份而言,其恢复过程一般耗时较长
|
||||
- 裸文件备份:裸文件备份是指对数据库的物理文件进行复制,既可以在数据库运行时进行复制,也可以在数据库停止运行时复制
|
||||
- 对于裸文件备份,其数据恢复的耗时一般远小于逻辑备份
|
||||
|
||||
若按照数据库备份的内容,即可以对备份做出如下划分:
|
||||
- 完全备份:完全备份是指对数据库中所有数据做一个全量备份
|
||||
- 增量备份:增量备份是指在上次完全备份的基础上,对发生修改的数据进行备份
|
||||
- 日志备份:对于mysql而言,日志备份主要是指对mysql数据库的binlog进行备份,其记录了`对数据库造成修改的事件`,通过对binlog进行replay,能够实现数据库的`point-in-time`恢复
|
||||
- 对于mysql数据库的`replication`,其原理即是异步的将binlog传输到slave数据库,并在slave数据库对binlog的内容进行replay
|
||||
|
||||
### 增量备份原理
|
||||
在mysql中,并没有官方的增量备份方法被提供,大多是通过binlog来完成增量备份的工作。通过binlog来实现的备份较增量备份方案来说效率较低。
|
||||
|
||||
`xtrabackup`工具提供了增量备份方案,其原理是通过记录每页最后的checkpoint lsn,如果大于之前全量备份时的lsn,那么就对该页进行备份,否则无需备份该页。
|
||||
|
||||
### 数据一致性
|
||||
在进行数据备份时,`要求数据在备份时是一致的`。例如在玩家购买道具时,其会先扣款并对道具进行增加,如果数据不一致,那么则可能发生扣款已经发生但是道具尚未被添加这类数据不一致的情况。
|
||||
|
||||
对于innodb而言,由于其支持mvcc,故而对保证数据一致性的备份实现比较简单。在备份时,可以先开启一个事务,然后导出一系列相关的表,然后提交。由于undo log和read view的设计,可以保证事务中读取到的数据是一致的。
|
||||
|
||||
对于mysqldump工具而言,可以通过添加`--single-transaction`选项来获取innodb的一致性备份,原理和上述所示的相同,备份逻辑会在一个长事务中执行。`在使用mysqldump进行备份时,务必添加--single-transaction选项`。
|
||||
|
||||
## Cold Backup
|
||||
对于innodb存储引擎而言,cold backup相对简单,只需要备份mysql数据库的frm文件(mysql8中没有该类型文件)、共享表空间文件、独立表空间文件、redo log文件即可。另外,建议定期备份mysql数据库的配置文件`my.cnf`,有利于恢复操作。
|
||||
|
||||
cold backup存在如下优点:
|
||||
- 备份简单,仅需复制相关文件即可
|
||||
- 备份文件易于在不同操作系统、不同mysql版本上进行复制
|
||||
- 恢复速度快,仅需拷贝文件即可,无需执行sql语句也无需进行索引重建
|
||||
|
||||
cold backup存在如下缺点:
|
||||
- innodb存储引擎的cold backup文件通常要比逻辑文件大很多,因为表空间中存放着很多其他数据,例如`undo log段`和`change buffer`等
|
||||
- cold backup并不总是可以轻易跨平台,操作系统、mysql版本、文件大小写敏感、浮点格式都会造成问题
|
||||
|
||||
> change buffer也是系统表空间的一部分,故而,change buffer也存在磁盘页。在进行cold backup时,同样需要备份change buffer。
|
||||
|
||||
## 逻辑备份
|
||||
### mysqldump
|
||||
mysqldump工具通常用于创建数据库的备份或是在不同数据库之间对数据进行移植,如从mysql低版本升级到mysql高版本,或从mysql数据库迁移到oracle、sql server等数据库。
|
||||
|
||||
mysqldump的语法如下所示:
|
||||
```sql
|
||||
mysqldump [arguments] >file_name
|
||||
```
|
||||
如果想用mysqldump备份所有的数据库,可以使用如下命令:
|
||||
```sql
|
||||
mysqldump --all-databases >dump.sql
|
||||
```
|
||||
如果想要备份指定的数据库,可以使用如下命令
|
||||
```sql
|
||||
mysqldump --databases db1 db2 db3 > dump.sql
|
||||
```
|
||||
通过`--single-transaction`可以保证备份的一致性,示例如下:
|
||||
```sql
|
||||
mysqldump --single-transaction test >test_backup.sql
|
||||
```
|
||||
上述操作针对`test`数据库创建了一个备份。
|
||||
|
||||
备份出的内容为表结构和表数据。
|
||||
|
||||
mysqldump命令用友如下较为重要的参数:
|
||||
|
||||
#### --single-transaction
|
||||
当指定了`--single-transaction`参数后,其在备份开始前会先执行`start transaction`命令,从而保证备份数据的一致性。
|
||||
|
||||
该参数只针对innodb存储引擎有效,且在启用该参数进行备份时,需要确保没有ddl语句在执行,一致性读操作并不能对ddl进行隔离。
|
||||
|
||||
#### --lock-tables(`-l`)
|
||||
在备份时,会依次对表进行锁定,一般用于myisam存储引擎,在备份时只能对数据库执行读操作,同样可以保证备份的一致性。
|
||||
|
||||
对于innodb存储引擎,并不需要使用`-l`参数,使用`--single-transaction`参数即能够保证备份数据的一致性。
|
||||
|
||||
> `--single-transaction`选项和`--lock-tables`选项是互斥的,并不能够同时指定。
|
||||
|
||||
#### --lock-all-tables(`-x`)
|
||||
在使用`-x`时,会针对数据库中所有的表都进行加锁,其能够避免`--lock-tables`不能锁住所有的表的问题。
|
||||
|
||||
#### --add-drop-database
|
||||
在`create database`之前先调用`drop database`,该选项需要和`--all-databases`/`--databases`一起使用。默认情况下,导出的备份中并不会含有`create database`语句,除非手动指定该选项。
|
||||
|
||||
#### --routinues
|
||||
该选项用于备份存储过程和函数
|
||||
|
||||
#### --triggers
|
||||
该选项用于备份触发器
|
||||
|
||||
#### --hex-blob
|
||||
将`binary, varbinary, blog, text`类型备份为16进制的格式。
|
||||
|
||||
### select ... into outfile
|
||||
`select ... into outfile`也是一种逻辑备份方法,用于导出一张表中的数据,示例如下:
|
||||
```sql
|
||||
select * into outfile '/root/a.txt` from a;
|
||||
```
|
||||
其中,导出路径的权限必须是`mysql:mysql`的,否则mysql会报错没有导出权限。
|
||||
|
||||
如果想要导出的文件已经存在,同样也会报错。
|
||||
|
||||
## 逻辑备份恢复
|
||||
### mysqldump
|
||||
mysqldump的逻辑备份恢复比较简单,只需要执行备份出的sql语句即可,示例如下
|
||||
```bash
|
||||
mysql -uroot -p <test_backup.sql
|
||||
```
|
||||
|
||||
除了上述方式外,还可以通过source命令来对逻辑备份进行恢复,示例如下:
|
||||
```mysql
|
||||
mysql> source /home/mysql/test_backup.sql
|
||||
```
|
||||
|
||||
### 二进制日志的备份和恢复
|
||||
通过二进制日志,用户可以实现point-in-time的恢复。
|
||||
|
||||
在备份二进制日志之前,可以通过`flush logs`命令来生成一个新的二进制日志文件,然后对之前的二进制日志文件进行备份。
|
||||
|
||||
可以通过`mysqlbinlog`命令对二进制日志进行恢复,示例如下:
|
||||
```bash
|
||||
mysqlbinlog binlog.0000001 | mysql -uroot -p test
|
||||
```
|
||||
|
||||
如果要恢复多个二进制文件,可以按照如下方式:
|
||||
```bash
|
||||
mysqlbinlog binlog.[0-10]* | mysql -u root -p test
|
||||
```
|
||||
|
||||
也可以通过`mysqlbinlog`命令导入到一个文件,然后再对文件执行source。
|
||||
|
||||
可以通过`--start-position`和`--stop-position`从二进制日志的指定偏移量来进行恢复,这样可以针对某些语句进行跳过,示例如下:
|
||||
```bash
|
||||
mysqlbinlog --start-position=107856 binlog.0000001 | mysql -uroot -p test
|
||||
```
|
||||
`--start-datetime`和`--stop-datetime`则是可以选择从二进制日志的某个时间点来进行恢复。
|
||||
|
||||
|
||||
363
mysql/mysql文档/mysql_文件.md
Normal file
363
mysql/mysql文档/mysql_文件.md
Normal file
@@ -0,0 +1,363 @@
|
||||
- [文件](#文件)
|
||||
- [参数](#参数)
|
||||
- [参数查看](#参数查看)
|
||||
- [参数类型](#参数类型)
|
||||
- [动态参数修改](#动态参数修改)
|
||||
- [静态参数修改](#静态参数修改)
|
||||
- [日志文件](#日志文件)
|
||||
- [错误日志](#错误日志)
|
||||
- [慢查询日志](#慢查询日志)
|
||||
- [log\_queries\_not\_using\_indexes](#log_queries_not_using_indexes)
|
||||
- [查询日志](#查询日志)
|
||||
- [二进制日志](#二进制日志)
|
||||
- [max\_binlog\_size](#max_binlog_size)
|
||||
- [binlog\_cache\_size](#binlog_cache_size)
|
||||
- [binlog\_cache\_use](#binlog_cache_use)
|
||||
- [binlog\_cache\_disk\_use](#binlog_cache_disk_use)
|
||||
- [sync\_binlog](#sync_binlog)
|
||||
- [innodb\_flush\_log\_at\_trx\_commit](#innodb_flush_log_at_trx_commit)
|
||||
- [binlog\_format](#binlog_format)
|
||||
- [使用statement可能会存在的问题](#使用statement可能会存在的问题)
|
||||
- [mysqlbinlog](#mysqlbinlog)
|
||||
- [pid文件](#pid文件)
|
||||
- [表结构定义文件](#表结构定义文件)
|
||||
- [表空间文件](#表空间文件)
|
||||
- [innodb\_data\_file\_path](#innodb_data_file_path)
|
||||
- [innodb\_file\_per\_table](#innodb_file_per_table)
|
||||
- [redo log文件](#redo-log文件)
|
||||
- [循环写入](#循环写入)
|
||||
- [redo log capacity](#redo-log-capacity)
|
||||
- [redo log和binlog的区别](#redo-log和binlog的区别)
|
||||
- [记录内容](#记录内容)
|
||||
- [写入时机](#写入时机)
|
||||
- [redo log写入时机](#redo-log写入时机)
|
||||
|
||||
|
||||
# 文件
|
||||
## 参数
|
||||
### 参数查看
|
||||
mysql参数为键值对,可以通过`show variables`命令查看所有的数据库参数,并可以通过`like`来过滤参数名称。
|
||||
|
||||
除了`show variables`命令之外,还能够在`performance_schema`下的`global_variables`视图来查找数据库参数,示例如下:
|
||||
```sql
|
||||
-- 查看innodb_buffer_pool_size参数
|
||||
show variables like 'innodb_buffer_pool_size'
|
||||
```
|
||||
上述`show variables`命令的执行结果为
|
||||
| Variable\_name | Value |
|
||||
| :--- | :--- |
|
||||
| innodb\_buffer\_pool\_size | 4294967296 |
|
||||
|
||||
```sql
|
||||
select * from performance_schema.global_variables where variable_name like 'innodb_buffer_pool%';
|
||||
```
|
||||
上述sql的执行结果如下:
|
||||
| VARIABLE\_NAME | VARIABLE\_VALUE |
|
||||
| :--- | :--- |
|
||||
| innodb\_buffer\_pool\_chunk\_size | 134217728 |
|
||||
| innodb\_buffer\_pool\_dump\_at\_shutdown | ON |
|
||||
| innodb\_buffer\_pool\_dump\_now | OFF |
|
||||
| innodb\_buffer\_pool\_dump\_pct | 25 |
|
||||
| innodb\_buffer\_pool\_filename | ib\_buffer\_pool |
|
||||
| innodb\_buffer\_pool\_in\_core\_file | ON |
|
||||
| innodb\_buffer\_pool\_instances | 4 |
|
||||
| innodb\_buffer\_pool\_load\_abort | OFF |
|
||||
| innodb\_buffer\_pool\_load\_at\_startup | ON |
|
||||
| innodb\_buffer\_pool\_load\_now | OFF |
|
||||
| innodb\_buffer\_pool\_size | 4294967296 |
|
||||
|
||||
### 参数类型
|
||||
mysql中的参数可以分为`动态`和`静态`两种类型,
|
||||
- 动态:动态参数代表可以在mysql运行过程中进行修改
|
||||
- 静态:代表在整个实例的声明周期内都不得进行修改
|
||||
|
||||
#### 动态参数修改
|
||||
对于动态参数,可以在运行时通过`SET`命令来进行修改,`SET`命令语法如下:
|
||||
```sql
|
||||
set
|
||||
| [global | session] system_var_name=expr
|
||||
| [@@global. | @@session. | @@] system_var_name = expr
|
||||
```
|
||||
在上述语法中,`global`和`session`关键字代表该动态参数的修改是针对`当前会话`还是针对`整个实例的生命周期`。
|
||||
|
||||
- 有些动态参数只能在会话范围内进行修改,例如`autocommit`
|
||||
- 有些参数修改后,实例整个生命周期内都会生效,例如`binglog_cache_size`
|
||||
- 有些参数既可以在会话范围内进行修改,又可以在实例声明周期范围内进行修改,例如`read_buffer_size`
|
||||
|
||||
使用示例如下:
|
||||
|
||||
查询read_buffer_size的global和session值
|
||||
```sql
|
||||
-- 查询read_buffer_size的global和session值
|
||||
select @@session.read_buffer_size,@@global.read_buffer_size;
|
||||
```
|
||||
返回结果为
|
||||
|
||||
| @@session.read\_buffer\_size | @@global.read\_buffer\_size |
|
||||
| :--- | :--- |
|
||||
| 131072 | 131072 |
|
||||
|
||||
设置@@session.read_buffer_size为524288
|
||||
```sql
|
||||
set @@session.read_buffer_size = 1024 * 512;
|
||||
```
|
||||
设置后,再次查询read_buffer_size的global和session值,结果为
|
||||
| @@session.read\_buffer\_size | @@global.read\_buffer\_size |
|
||||
| :--- | :--- |
|
||||
| 524288 | 131072 |
|
||||
|
||||
在调用set命令修改session read_buffer_size参数后,session参数发生变化,但是global参数仍然为旧的值。
|
||||
|
||||
> `set session xxx`命令并不会对global参数的值造成影响,新会话的参数值仍然为修改前的值。
|
||||
|
||||
之后,再对global read_buffer_size值进行修改,执行如下命令
|
||||
```sql
|
||||
set @@global.read_buffer_size = 496 * 1024;
|
||||
```
|
||||
执行该命令后,sesion和global参数值为
|
||||
| @@session.read\_buffer\_size | @@global.read\_buffer\_size |
|
||||
| :--- | :--- |
|
||||
| 524288 | 507904 |
|
||||
|
||||
> `set global xxx`命令只会修改global参数值,对session参数值不会造成影响,新的session其`session参数值, global参数值`和修改后的global参数值保持一致
|
||||
|
||||
> 即使针对参数的global值进行了修改,其影响范围是当前实例的整个生命周期,`但是,其并不会对参数文件中的参数值进行修改,故而下次启动mysql实例时,仍然会从参数文件中取值,新实例的值仍然是修改前的值`。
|
||||
>
|
||||
> 如果想要修改下次启动实例的参数值,需要修改参数文件中该参数的值。(参数文件路径通常为`/etc/my.cnf`)
|
||||
|
||||
#### 静态参数修改
|
||||
在运行时,如果尝试对静态参数进行修改,那么会发生错误,示例如下:
|
||||
```sql
|
||||
> set global datadir='/db/mysql'
|
||||
[2025-01-30 15:05:17] [HY000][1238] Variable 'datadir' is a read only variable
|
||||
```
|
||||
## 日志文件
|
||||
mysql中常见日志文件如下
|
||||
- 错误日志(error log)
|
||||
- 二进制日志(binlog)
|
||||
- 慢查询日志(slow query log)
|
||||
- 查询日志(log)
|
||||
|
||||
### 错误日志
|
||||
错误日志针对mysql的启动、运行、关闭过程进行了记录,用户可以通过`show variables like 'log_error';`来获取错误日志的路径:
|
||||
```sql
|
||||
show variables like 'log_error';
|
||||
```
|
||||
其输出值如下:
|
||||
| Variable\_name | Value |
|
||||
| :--- | :--- |
|
||||
| log\_error | /var/log/mysql/mysqld.log |
|
||||
|
||||
当mysql数据库无法正常启动时,应当首先查看错误日志。
|
||||
|
||||
### 慢查询日志
|
||||
慢查询日志存在一个阈值,通过`long_query_time`参数来进行控制,该参数默认值为`10`,代表慢查询的限制为10s。
|
||||
|
||||
通过`slow_query_log`参数,可以控制是否日志输出慢查询日志,默认为`OFF`,如果需要开启慢查询日志,需要将该值设置为`ON`。
|
||||
|
||||
关于慢查询日志的输出地点,可以通过`log_output`参数来进行控制。该参数默认为`FILE`,支持`FILE, TABLE, NONE`。`log_output`支持制定多个值,多个值之间可以通过`,`分隔,当值中包含`NONE`时,以`NONE`优先。
|
||||
|
||||
#### log_queries_not_using_indexes
|
||||
当`log_queryies_not_using_indexes`开启时,如果运行的sql语句没有使用索引,那么这条sql同样会被输出到慢查询日志。该参数默认关闭。
|
||||
|
||||
`log_throttle_queries_not_using_idnexes`用于记录`每分钟允许记录到慢查询日志并且没有使用索引`的sql语句次数,该参数值默认为0,代表每分钟输出到慢查询日志中的数量没有限制。
|
||||
|
||||
该参数主要用于防止大量没有使用索引的sql添加到慢查询日志中,造成慢查询日志大小快速增加。
|
||||
|
||||
当慢查询日志中的内容越来越多时,可以通过mysql提供的工具`mysqldumpslow`命令,示例如下:
|
||||
```sql
|
||||
mysqldumpslow -s at -n 10 ${slow_query_log_path}
|
||||
```
|
||||
|
||||
### 查询日志
|
||||
查询日志记录了对mysql数据库所有的请求信息,无论请求是否正确执行。
|
||||
|
||||
查询日志通过`general_log`参数来进行控制,默认该参数值为`OFF`.
|
||||
|
||||
### 二进制日志
|
||||
二进制日志(binary log)记录了针对mysql数据库执行的所有更改操作(不包含select以及show这类读操作)。
|
||||
|
||||
对于update操作等,即使没有对数据库进行修改(affected rows为0),也会被写入到binary log中。
|
||||
|
||||
二进制日志的主要用途如下:
|
||||
- 恢复(recovery):某些数据恢复需要二进制日志,例如在数据库全备份文件恢复后,用户可以通过二进制日志进行point-in-time的恢复
|
||||
- 复制(replication):通过将一台主机(master)的binlog同步到另一台主机(slave),并且在另一台主机上执行该binlog,可以令slave与master进行实时同步
|
||||
- 审计(audit):用户可以对binlog中的信息进行审计,判断是否存在对数据库进行的注入攻击
|
||||
|
||||
通过参数`log_bin`可以控制是否启用二进制日志。
|
||||
|
||||
binlog通常存放在`datadir`参数所指定的目录路径下。在该路径下,还存在`binlog.index`文件,该文件为binlog的索引文件,文件内容包含所有binlog的文件名称。
|
||||
|
||||
#### max_binlog_size
|
||||
`max_binlog_size`参数控制单个binlog文件的最大大小,如果单个文件超过该值,会产生新的二进制文件,新binlog的后缀会+1,并且新文件的文件名会被记录到`.index`文件中。
|
||||
|
||||
`max_binlog_size`的默认值大小为`1G`。
|
||||
|
||||
#### binlog_cache_size
|
||||
当使用innodb存储引擎时,所有未提交事务的binlog会被记录到缓存中,等到事务提交后,会将缓存中的binlog写入到文件中。缓存大小通过`binlog_cache_size`决定,该值默认为`32768`,即`32KB`。
|
||||
|
||||
`binlog_cache_size`是基于会话的,`在每个线程开启一个事务时,mysql会自动分配一个大小为binlog_cache_size大小的缓存,因而该值不能设置过大`。
|
||||
|
||||
当一个事务的记录大于设定的`binlog_cache_size`时,mysql会将缓冲中的日志写入到一个临时文件中,故而,该值无法设置过小。
|
||||
|
||||
通过`show global status like 'binlog_cache%`命令可以查看`binlog_cache_use`和`binlog_cache_disk_use`的状态,可以通过上述两个状态判断binlog cache大小是否合适。
|
||||
|
||||
##### binlog_cache_use
|
||||
`binlog_cache_use`记录了使用缓冲写binlog的次数
|
||||
|
||||
##### binlog_cache_disk_use
|
||||
`binlog_cache_disk_use`记录了使用临时文件写二进制日志的次数
|
||||
|
||||
#### sync_binlog
|
||||
`sync_binlog`参数控制mysql server同步binlog到磁盘的频率,该值默认为`1`
|
||||
|
||||
- 0: 如果参数值为0,代表mysql server禁用binary log同步到磁盘。mysql会依赖操作系统将binary log刷新到磁盘中,该设置性能最佳,但是遇到操作系统崩溃时,可能会出现mysql事务提交但是还没有同步到binary log的场景
|
||||
- 1: 如果参数值设置为1,代表在事务提交之前将binary log同步到磁盘中,该设置最安全,但是会增加disk write次数,对性能会带来负面影响。在操作系统崩溃的场景下,binlog中缺失的事务还只处于prepared状态,从而确保binlog中没有事务丢失
|
||||
- N:当参数值被设置为非`0,1`的值时,每当n个binlog commit groups被收集到后,同步binlog到磁盘。在这种情况下,可能会发生事务提交但是还没有被刷新到binlog中,`当n值越大时,性能会越好,但是也会增加数据丢失的风险`
|
||||
|
||||
为了在使用innodb事务和replciation时获得最好的一致性和持久性,请使用如下设置:
|
||||
```cnf
|
||||
sync_binlog=1
|
||||
innodb_flush_log_at_trx_commit=1
|
||||
```
|
||||
|
||||
#### innodb_flush_log_at_trx_commit
|
||||
innodb_flush_log_at_trx_commit用于控制redo log的刷新。
|
||||
|
||||
该参数用于平衡`commit操作ACID的合规性`以及`更高性能`。通过修改该参数值,可以实现更佳的性能,但是在崩溃时可能会丢失事务:
|
||||
- 1: 1为该参数默认值,代表完全的ACID合规性,日志在每次事务提交后被写入并刷新到磁盘中
|
||||
- 0: 日志每秒被写入和刷新到磁盘中,如果事务没有被刷新,那么日志将会在崩溃中被丢失
|
||||
- 2: 每当事务提交后,日志将会被写入,并且每秒钟都会被刷新到磁盘中。如果事务没有被刷新,崩溃同样会造成日志的丢失
|
||||
|
||||
如果当前数据库为slave角色,那么其不会把`从master同步的binlog`写入到自己的binlog中,如果要实现`master=>slave=>slave`的同步架构,必须设置`log_slave_updates`参数。
|
||||
|
||||
#### binlog_format
|
||||
binlog_format用于控制二进制文件的格式,可能有如下取值:
|
||||
- statement: 二进制文件记录的是日志的逻辑sql语句
|
||||
- row:记录表的行更改情况,默认值为`row`
|
||||
- mixed: 如果参数被配置为mixed,mysql默认会采用`statement`格式进行记录,但是在特定场景能够下会使用`row`格式:
|
||||
- 使用了uuid, user, current_user,found_rows, row_count等不确定函数
|
||||
- 使用了insert delay语句
|
||||
- 使用了用户自定义函数
|
||||
- 使用了临时表
|
||||
|
||||
##### 使用statement可能会存在的问题
|
||||
在使用statement格式时,可能会存在如下问题
|
||||
- master运行rand,uuid等不确定函数时,或使用触发器操作时,会导致主从服务器上的数据不一致
|
||||
- innodb的默认事务隔离级别为`repetable_read`,如果使用`read_commited`级别时,statement格式可能会导致丢失更新的情况,从而令master和slave的数据不一致
|
||||
|
||||
binlog为动态参数,可以在数据库运行时进行修改,并且可以针对session和global进行修改。
|
||||
|
||||
#### mysqlbinlog
|
||||
在查看二进制日志时,可以使用`mysqlbinlog`命令,示例如下
|
||||
```bash
|
||||
mysqlbinlog --start-position=203 ${binlog_path}
|
||||
```
|
||||
|
||||
## pid文件
|
||||
mysql实例启动时,会将进程id写入到一个文件中,该文件被称为pid文件。
|
||||
|
||||
pid文件路径通过`pid_file`参数来进行控制,fedora中默认路径为`/run/mysqld/mysqld.pid`。
|
||||
|
||||
## 表结构定义文件
|
||||
mysql中数据的存储是根据表进行的,每个表都有与之对应的文件。无论表采用何种存储引擎,都会存在一个以`frm`为后缀的文件,该文件中保存了该表的表结构定义。
|
||||
|
||||
> mysql 8中,schema对应目录下不再包含frm文件。
|
||||
|
||||
## 表空间文件
|
||||
innodb采用将存储的数据按照表空间(tablespace)进行存放的设计。在默认配置下,将会有一个初始大小为10MB,名称为ibdata1的文件,该文件为默认的表空间文件。
|
||||
|
||||
### innodb_data_file_path
|
||||
可以通过`innodb_data_file_path`参数对默认表空间文件进行设置,示例如下:
|
||||
```sql
|
||||
innodb_data_file_path=datafile_spec1[;datafile_spec2]...
|
||||
```
|
||||
用户可以通过多个文件组成一个表空间,示例如下:
|
||||
```sql
|
||||
innodb_data_file_path=/db/ibdata1:2000M;/dr2/db/ibdata2:2000M;autoextend
|
||||
```
|
||||
在上述配置中,表空间由`/db/ibdata1`和`/dr2/db/ibdata2`两个文件组成,如果两个文件位于不同的磁盘上,那么磁盘的负载将会被平均,数据库的整体性能将会被提高。
|
||||
|
||||
同时,在上述示例中,为两个文件都指定了后续属性,含义如下:
|
||||
- ibdata1:文件大小为2000M
|
||||
- ibdata2:文件大小为2000M,并且当文件大小被用完后,文件会自动增长
|
||||
|
||||
当`innodb_data_file_path`被设置后,所有基于innodb存储引擎的表,其数据都会记录到该共享表空间中。
|
||||
|
||||
### innodb_file_per_table
|
||||
如果`innodb_file_per_table`被启用后(默认启用),则每个基于innodb存储引擎的表都可以有一个独立的表空间,独立表空间的命名规则为`表名+.ibd`。
|
||||
|
||||
通过innodb_file_per_table,用户不需要将所有的数据都放置在默认的表空间中。
|
||||
|
||||
> `innodb_file_per_table`所产生的独立表空间文件,其仅存储该表的数据、索引和插入缓冲BITMAP信息,其余信息仍然存放在默认的表空间中。
|
||||
|
||||
## redo log文件
|
||||
redo log是一个基于磁盘的数据结构,用于在crash recovery过程中纠正由`未完成事务写入的错误数据`。
|
||||
|
||||
> 在一般操作中,redo log对那些`会造成表数据发生改变的请求`进行encode操作,请求通常由sql statement或地级别api发起。
|
||||
|
||||
redo log通常代表磁盘上的redo log file。写入重做日志文件的数据通常基于受影响的记录进行编码。在数据被写入到redo log file中时,LSN值也会不断增加。
|
||||
|
||||
### 循环写入
|
||||
innodb会按顺序写入redo log文件,例如redo log file group中存在两个文件,innodb会先写文件1,文件1写满后会切换文件2,在文件2写满后,重新切换到文件1。
|
||||
|
||||
### redo log capacity
|
||||
从mysql 8.0.30开始,`innodb_redo_log_capacity`参数用于控制redo log file占用磁盘空间的大小。该参数可以在实例启动时进行设置,也可以通过`set global`来进行设置。
|
||||
|
||||
`innodb_redo_log_capacity`默认值为`104857600`,即`100M`。
|
||||
|
||||
redo log文件默认位于`datadir`路径下的`#innodb_redo`目录下。innodb会尝试维护32个redo log file,每个redo log file文件大小都相同,为`1/32 * innodb_redo_log_capacity`。
|
||||
|
||||
redo log file将会使用`#ib_redoN`的命名方式,`N`是redo log file number。
|
||||
|
||||
innodb redo log file分为如下两种:
|
||||
- ordinary:正在被使用的redo log file
|
||||
- spare:等待被使用的redo log file
|
||||
|
||||
> 相比于ordinary redo log file,spare redo log file的名称中还包含了`_tmp`后缀
|
||||
|
||||
每个oridnary redo log file都关联了一个制定的LSN范围,可以通过查询`performance_schema.innodb_redo_log_files`表里获取LSN范围。
|
||||
|
||||
示例如下:
|
||||
```sql
|
||||
select file_name, start_lsn, end_lsn from performance_schema.innodb_redo_log_files;
|
||||
```
|
||||
查询结果示例如下:
|
||||
| file\_name | start\_lsn | end\_lsn |
|
||||
| :--- | :--- | :--- |
|
||||
| ./#innodb\_redo/#ib\_redo6 | 19656704 | 22931456 |
|
||||
|
||||
当执行checkpoint时,innodb会将checkpoint LSN存储在文件的header中,在recovery过程中,所有的redo log文件都将被检查,并且基于最大的LSN来执行恢复操作。
|
||||
|
||||
常用的redo log状态如下
|
||||
```bash
|
||||
# resize operation status
|
||||
Innodb_redo_log_resize_status
|
||||
# 当前redo log capacity
|
||||
Innodb_redo_log_capacity_resized
|
||||
Innodb_redo_log_checkpoint_lsn
|
||||
Innodb_redo_log_current_lsn
|
||||
Innodb_redo_log_flushed_to_disk_lsn
|
||||
Innodb_redo_log_logical_size
|
||||
Innodb_redo_log_physical_size
|
||||
Innodb_redo_log_read_only
|
||||
Innodb_redo_log_uuid
|
||||
```
|
||||
> 重做日志大小设置时,如果设置大小过大,那么在执行恢复操作时,可能需要花费很长时间;如果重做日志文件大小设置过小,可能会导致事务的日志需要多次切换重做日志文件。
|
||||
>
|
||||
> 此外,重做日志太小会频繁发生async checkpoint,导致性能抖动。重做日志存在一个capacity,代表了最后的checkpoint不能够超过这个阈值,如果超过必须将缓冲区中的部分脏页刷新到磁盘中,此时可能会造成用户线程的阻塞。
|
||||
|
||||
### redo log和binlog的区别
|
||||
#### 记录内容
|
||||
binlog记录的是一个事务的具体操作内容,该日志为逻辑日志。
|
||||
|
||||
而innodb redo log记录的是关于某个页的修改,为物理日志。
|
||||
|
||||
#### 写入时机
|
||||
binlog仅当事务提交前才进行提交,即只会写磁盘一次。
|
||||
|
||||
redo log则是在事务运行过程中,不断有重做日志被写入到redo log file中。
|
||||
|
||||
### redo log写入时机
|
||||
- master thread会每秒将redo log从buffer中刷新到redo log ile中,不露内事务是否已经提交
|
||||
- innodb_flush_log_at_trx_commit控制redo log的刷新时机,默认情况下,在事务提交前会将数据从redo log buffer刷新到redo log file中
|
||||
1004
mysql/mysql文档/mysql_索引.md
Normal file
1004
mysql/mysql文档/mysql_索引.md
Normal file
File diff suppressed because it is too large
Load Diff
713
mysql/mysql文档/mysql_表.md
Normal file
713
mysql/mysql文档/mysql_表.md
Normal file
@@ -0,0 +1,713 @@
|
||||
- [表](#表)
|
||||
- [索引组织表](#索引组织表)
|
||||
- [innodb逻辑存储结构](#innodb逻辑存储结构)
|
||||
- [表空间](#表空间)
|
||||
- [innodb\_file\_per\_table](#innodb_file_per_table)
|
||||
- [段(segment)](#段segment)
|
||||
- [区(Extent)](#区extent)
|
||||
- [页(Page)](#页page)
|
||||
- [行](#行)
|
||||
- [innodb行记录格式](#innodb行记录格式)
|
||||
- [Compact](#compact)
|
||||
- [变长字段长度列表](#变长字段长度列表)
|
||||
- [NULL标志位](#null标志位)
|
||||
- [记录头信息](#记录头信息)
|
||||
- [行溢出数据](#行溢出数据)
|
||||
- [dynamic](#dynamic)
|
||||
- [char存储结构](#char存储结构)
|
||||
- [innodb数据页结构](#innodb数据页结构)
|
||||
- [File Header](#file-header)
|
||||
- [Infimum和Supremum record](#infimum和supremum-record)
|
||||
- [user record 和 free space](#user-record-和-free-space)
|
||||
- [page directory](#page-directory)
|
||||
- [B+树索引](#b树索引)
|
||||
- [File Trailer](#file-trailer)
|
||||
- [完整性校验](#完整性校验)
|
||||
- [分区表](#分区表)
|
||||
- [partition keys \& primary keys \& unique keys](#partition-keys--primary-keys--unique-keys)
|
||||
- [表中不存在唯一索引](#表中不存在唯一索引)
|
||||
- [后续向分区表添加唯一索引](#后续向分区表添加唯一索引)
|
||||
- [对非分区表进行分区](#对非分区表进行分区)
|
||||
- [分区类型](#分区类型)
|
||||
- [RANGE](#range)
|
||||
- [information\_schema.partitions](#information_schemapartitions)
|
||||
- [看select语句查询了哪些分区](#看select语句查询了哪些分区)
|
||||
- [插入超过分区范围的数据](#插入超过分区范围的数据)
|
||||
- [向分区表中添加分区](#向分区表中添加分区)
|
||||
- [向分区表中删除分区](#向分区表中删除分区)
|
||||
- [LIST](#list)
|
||||
- [HASH](#hash)
|
||||
- [新增HASH分区](#新增hash分区)
|
||||
- [减少HASH分区](#减少hash分区)
|
||||
- [LINEAR HASH](#linear-hash)
|
||||
- [KEY \& LINEAR KEY](#key--linear-key)
|
||||
- [COLUMNS](#columns)
|
||||
- [range columns](#range-columns)
|
||||
- [list columns](#list-columns)
|
||||
- [子分区](#子分区)
|
||||
- [分区中的NULL值](#分区中的null值)
|
||||
- [分区和性能](#分区和性能)
|
||||
- [不分区](#不分区)
|
||||
- [按id进行分区](#按id进行分区)
|
||||
- [在表和分区之间交换数据](#在表和分区之间交换数据)
|
||||
|
||||
|
||||
# 表
|
||||
## 索引组织表
|
||||
innodb存储引擎中,表都是根据主键顺序组织存放的,这种存储方式被称为索引组织表(index organized table)。在innodb存储引擎表中,每张表都有主键(primary key),如果在创建表时没有显式指定主键,那么innodb会按照如下方式创建主键:
|
||||
- 首先判断表中是否存在非空的唯一索引(unique not null)字段,如果有,则其为主键
|
||||
- 如果不存在非空唯一索引,那么innodb会自动创建一个6字节大小的指针作为主键
|
||||
|
||||
如果有多个非空唯一索引,innodb存储引擎将会选择第一个定义的非空唯一索引作为主键。
|
||||
|
||||
## innodb逻辑存储结构
|
||||
在innodb的存储逻辑结构中,所有的数据都被逻辑存放在表空间(table space)中。表空间则由`段(segement),区(extent),页(page)`组成。
|
||||
|
||||
组成如图所示:
|
||||
|
||||
<img src="https://pic2.zhimg.com/v2-7a3fe8eb03c68e1378f24847e464e139_1440w.jpg" data-caption="" data-size="normal" data-rawwidth="421" data-rawheight="275" data-original-token="v2-d7e41f080ece2820138fd0331f965a79" class="origin_image zh-lightbox-thumb" width="421" data-original="https://pic2.zhimg.com/v2-7a3fe8eb03c68e1378f24847e464e139_r.jpg">
|
||||
|
||||
### 表空间
|
||||
表空间为innodb存储引擎逻辑结构的最高层,所有数据都存放于表空间中。innodb存在一个默认的共享表空间`ibdata1`,在开启`innodb_file_per_table`参数后,每张表内的数据可以单独存放到一个表空间。
|
||||
|
||||
#### innodb_file_per_table
|
||||
`innodb_file_per_table`参数启用会导致每张表的`数据、索引、插入缓冲bitmap页`存放到单独的文件中;但是其他数据,例如`回滚(undo)信息,插入缓冲页,系统事务信息,double write buffer`等还是存放在默认的共享表空间中。
|
||||
|
||||
### 段(segment)
|
||||
如上图所示,表空间是由段(segment)所组成的,常见的段分为`数据段,索引段,回滚段`等。
|
||||
|
||||
在innodb存储引擎中,数据即索引,索引即数据。`数据段即为B+树的叶子节点(Leaf node segment)`,`索引段即为B+树的非叶子节点(Non-leaf node segment)`。
|
||||
|
||||
### 区(Extent)
|
||||
区是由连续页组成的空间,在任何情况下每个区的大小都为1MB。为了保证区中页的连续性,innodb存储引擎会一次性从磁盘申请4~5个区。在默认情况下,innodb存储引擎中页的大小为`16KB`,一个区中包含64个页。
|
||||
|
||||
在新建表时,新建表的大小为96KB,小于一个Extent的大小1MB,因为每个段Segmenet开始时,都会有至多32个页大小的碎片页,等使用完这些页后才会申请64个连续页作为Extent。
|
||||
|
||||
都与一些小表或是undo这样的段,可以在开始时申请较少的空间,节省磁盘容量开销。
|
||||
|
||||
### 页(Page)
|
||||
innodb存储引擎中页的大小默认为`16KB`,默认的页大小可以通过`innodb_page_size`参数进行修改。通过该参数,可以将innodb的默认页大小设置为4K,8K。
|
||||
|
||||
页是innodb磁盘管理的最小单位,在innodb中,常见的页有:
|
||||
- 数据页(B-tree node)
|
||||
- undo页(undo log page)
|
||||
- 系统页(system page)
|
||||
- 事务数据页(transaction system page)
|
||||
- 插入缓冲位图页(insert buffer bitmap)
|
||||
- 插入缓冲空闲列表页(insert buffer free list)
|
||||
- 未压缩的二进制大对象页(uncompressed blob page)
|
||||
- 压缩的二进制大对象页(compressed blob page)
|
||||
|
||||
### 行
|
||||
innodb存储引擎是面向行的,数据按行进行存放。每个页中至多可以存放`16KB/2 - 200`行的记录,即7992行记录。
|
||||
|
||||
## innodb行记录格式
|
||||
innodb存储引擎以行的形式进行存储,可以通过`show table status like '{table_name}'`的语句来查询表的行格式,示例如下:
|
||||
```sql
|
||||
show table status like 'demo_t1'
|
||||
```
|
||||
|
||||
| Name | Engine | Version | Row\_format | Rows | Avg\_row\_length | Data\_length | Max\_data\_length | Index\_length | Data\_free | Auto\_increment | Create\_time | Update\_time | Check\_time | Collation | Checksum | Create\_options | Comment |
|
||||
| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- |
|
||||
| demo\_t1 | InnoDB | 10 | Dynamic | 0 | 0 | 16384 | 0 | 32768 | 0 | 1 | 2025-01-30 15:18:57 | null | null | utf8mb4\_0900\_ai\_ci | null | | |
|
||||
|
||||
上述示例中表的row_format为dynamic。
|
||||
|
||||
### Compact
|
||||
在使用Compact行记录格式时,一个页中存放数据越多,其性能越高。
|
||||
|
||||
Compact格式下行记录的存储格式如下:
|
||||
<table>
|
||||
<tr>
|
||||
<td>变长字段长度列表</td>
|
||||
<td>NULL标志位</td>
|
||||
<td>记录头信息</td>
|
||||
<td>列1数据</td>
|
||||
<td>列2数据</td>
|
||||
<td>......</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
#### 变长字段长度列表
|
||||
Compact行记录格式其首部是一个`非Null变长字段长度`列表,其按照列的顺序逆序放置,变长列的长度为:
|
||||
- 如果列的长度小于255字节,用1字节表示
|
||||
- 如果列的长度大于255字节,用2个字节表示
|
||||
|
||||
变长字段的长度不能小于2字节,因为varchar类型最长长度限制为65535。
|
||||
|
||||
#### NULL标志位
|
||||
NULL标志位为bitmap,代表每一列是否为空。如果行中存在n个字段可为空,那么NULL标志位部分的长度为ceiling(n/8)。
|
||||
|
||||
#### 记录头信息
|
||||
record header,固定占用5字节,其中,record header的各bit含义如下所示:
|
||||
| 名称 | 大小(bit) | |
|
||||
| :-: | :-: | :-: |
|
||||
| () | 1 | 未知 |
|
||||
| () | 1 | 未知 |
|
||||
| deleted_flag | 1 | 该行是否已经被删除 |
|
||||
| min_rec_flag | 1 | 该行是否为预订被定义的最小记录行 |
|
||||
| n_owned | 4 | 该记录拥有的记录数 |
|
||||
| heap_no | 13 | 索引堆中该条记录的排序记录 |
|
||||
| record_type | 3 | 记录类型,000代表普通,001代表B+树节点指针, 010代表infimum,011代表supremum,1xx保留 |
|
||||
| next_record | 16 | 页中下一条记录相对位置 |
|
||||
|
||||
除了上述3个部分之外,其他部分就是各个列的实际值。
|
||||
|
||||
> 在Compact格式中,NULL除了占有NULL标志位外,不占用任何实际空间。
|
||||
|
||||
> 每行数据中,除了有用户自定义的列外,还存在两个隐藏列,即`事务id列`和`回滚指针列`,长度分别为6字节和7字节。
|
||||
>
|
||||
> 如果innodb表没有自定义主键,每行还会增加一个rowid列。
|
||||
|
||||
### 行溢出数据
|
||||
innodb存储引擎可能将一条记录中某些数据存储在真正的存储数据页面之外。一般来说,blob、lob这类大对象的存储位于数据页面之外。
|
||||
|
||||
当行数据的大小特别大,导致一个页无法存放2条行数据时,innodb会自动将占用空间大的`blob`字段或`varchar`字段值放到额外的uncompressed blob page中。
|
||||
|
||||
### dynamic
|
||||
dynamic格式几乎和compact格式相同,但是对于每个blob字段,其存储只消耗20字节用于存储指针。
|
||||
|
||||
而对于Compact格式,其会在blob格式中存储768字节的前缀字节。
|
||||
|
||||
### char存储结构
|
||||
`char(n)`字段中n代表`字符长度`而非字节长度,故而在不同字符集下,char类型字段的内部存储可能不是定长的。
|
||||
|
||||
例如,在utf8字符集下,`ab`和`我们`两个字符串,其字符数都是2个,但是`ab`其占用2字节,而`我们`占用4字节,即使同样是`char(2)`类型的字符串,其占用字节数量仍然有可能不同。
|
||||
|
||||
## innodb数据页结构
|
||||
innodb中页是磁盘管理的最小结构,页类型为B-tree Node的页存放的即是表中行的实际数据。
|
||||
|
||||
innodb数据页由如下7个部分组成:
|
||||
- File Header(文件头)
|
||||
- Page Header(页头)
|
||||
- Infimum和Supremum Records
|
||||
- user records(用户记录,即行记录)
|
||||
- free space(空闲空间)
|
||||
- page directory(页目录)
|
||||
- file trailer(文件结尾信息)
|
||||
|
||||
file header,page header,file trailer的大小是固定的,分别为38,56,8字节,这些空间用于标记页的一些信息,例如checksum,数据页所在B+树的层数等。
|
||||
|
||||
<img src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6d066e690d22484ebe33bbb4977c3cfb~tplv-k3u1fbpfcp-zoom-in-crop-mark:1512:0:0:0.awebp" alt="image.png" loading="lazy" class="medium-zoom-image">
|
||||
|
||||
### File Header
|
||||
File Header用于记录页的一些头信息,由8个部分组成,共占38字节:
|
||||
| 名称 | 大小 | 说明 |
|
||||
| :-: | :-: | :-: |
|
||||
| FIL_PAGE_SPACE_OR_CHKSUM | 4 | 代表页的checksum值 |
|
||||
| FIL_PAGE_OFFSET | 4 | 表空间中页的偏移位置 |
|
||||
| FIL_PAGE_PREV | 4 | 当前页的上一个页,B+树决定叶子节点为双向列表 |
|
||||
| FIL_PAGE_NEXT | 4 | 当前页的下一个页 |
|
||||
| FIL_PAGE_LSN | 8 | 代表该页最后被修改的日志序列位置LSN |
|
||||
| FIL_PAGE_TYPE | 2 | 存储引擎页类型 |
|
||||
| FIL_PAGE_FILE_FLUSH_LSN | 8 | 该值仅在系统表空间的一个页中定义,代表文件至少被更新到了该LSN值,对于独立的表空间,该值为0 |
|
||||
| FIL_PAGE_ARCH_LOG_NO_OR_SPACE_ID | 4 | 代表页属于哪个表空间 |
|
||||
|
||||
### Infimum和Supremum record
|
||||
在innodb存储引擎中,每个数据页都有两行虚拟的行记录,用于限定记录边界。Infimum是比该页中所有主键值都要小的值,Supremum是比任何可能值都要大的值。`这两个值在页创建时被建立,并且在任何情况下都不会被删除。`
|
||||
|
||||
### user record 和 free space
|
||||
user reocrd代表实际存储行记录中的内容,free space则是代表空闲空间,同样是链表数据结构,在一条记录被删除后,该空间会被加入到空闲列表中。
|
||||
|
||||
### page directory
|
||||
page directory(页目录)中存放了记录的相对位置,这些记录指针被称为目录槽(directory slots)。`innodb中槽是一个稀疏目录,一个槽中可能包含多个记录`,记录Infimum的n_owned总为1,记录Supremum的n_owned取值范围为`[1,8]`,其他用户记录的n_owned为`[4,8]`。当记录被插入或删除时,需要对槽进行分裂或平衡的维护操作。
|
||||
|
||||
在slots中,记录按照索引键值顺序存放,可以通过二叉查询迅速找到记录的指针。
|
||||
|
||||
由于在innodb存储引擎中,page directory是稀疏目录,二叉查找结果只是一个粗略的结果,innodb存储引擎必须通过record header中的next_record来继续查找相关记录。
|
||||
|
||||
#### B+树索引
|
||||
<img src="//cloud.mo4tech.com/images/ldWYtlmLrJXYtJXZ0F2dtA3YmBnYmFTdzsWL2xGc05XYhR2MwIjZycTMyczN4gDOmBTY0Y2Y5QmYygjY1YTNk9CcjZGciZWM1NzatkWLuNWLz9Gdv02bj5yZtlWZ0lnYu4WaqVWdq1iNw9yL6MHc0RHa/cefbe79c0791cd0781f7c6aa3a923e21.image" data-cdn="">
|
||||
如上图所示,innodb中的数据是按照索引来进行组织的。但是,通过B+树索引,其无法直接查询到指定的记录,其只能查询到记录所位于的页。
|
||||
|
||||
例如,在上图示例中,其在查询`ID=32`的时,只能查询到该记录位于page 17中。
|
||||
|
||||
> 在B+树的叶子节点,每个页的File Header中都存有指向前一个页和后一个页的指针,`故而每个页之间是通过双向列表结构来维护的`;但是对于页中的记录,`记录与记录之间维维护的时单向列表的关系`。
|
||||
>
|
||||
> 对于Compact或Dynamic行格式的页,其每条记录的record header中都包含一个next_record字段指向下一条记录。故而,位于同一个页中的记录可以单向访问。
|
||||
|
||||
但是,在一个页内查找某条记录时,沿着单向链表进行查找其效率很低。故而,page中的数据时机被分为了多个组,被分为的组构成了一个subdirectory,故而,通过子目录能够缩小查询范围,提高查询性能。
|
||||
|
||||
page directory是一个能够存储多个slots的部分,每个slot中存储了group中最后一条记录的相对位置。假设slot中最大一条记录为`r`,那么group中记录的条数被存储在`r`记录record header的`n_owned`字段中。
|
||||
|
||||
group中record的数量约束如下:
|
||||
- infimum group中records数量限制为`[1,8]`
|
||||
- supremum group中records数量限制为`[1,8]`
|
||||
- 其他group中records限制为`[4,8]`
|
||||
|
||||
page directory生成过程如下所示:
|
||||
- 最开始时,页中只有infimum和supremum两条记录,它们分别属于两个group。page directory中有两个slots指向这两条记录,两个slot的n_owned都为1
|
||||
- 后续,当新的记录被插入页时,系统会查找page directory中主键值大于待插入记录的第一个slot。slot对应最大记录的`n_owned`字段会增加1,代表group中新插入了记录,直到group中的记录数量到达8
|
||||
- 当新纪录被插入到的group中记录数大于8时,group中的记录被分为2个group,一个group包含4条记录,另一个group包含5条记录。该过程将会向page directory中新增一个新的slot
|
||||
- 当记录被删除时,slot最最大一条记录的`n_owned`将会减少1,当n_owned字段小于4时,会触发再平衡操作,平衡后的page directory满足上述要求
|
||||
|
||||
page directory中的slots数量如page header中的`PAGE_N_DIR_SLOTS`所示。
|
||||
|
||||
一旦innodb中页包含page directory后,其会通过二分查找快速的定位slot,并且从group中最小记录开始,通过`next_record`指针来遍历页中的记录列表。这样能够快速的定位记录位置。
|
||||
|
||||
### File Trailer
|
||||
|
||||
#### 完整性校验
|
||||
在innodb中,页的大小为16KB,可能和磁盘的扇区大小不同。通常磁盘的扇区大小为512字节,故而在写入一个页到磁盘中时,需要分32个扇区进行写入。
|
||||
|
||||
在写入一个页时,可能会发生宕机场景,这时,一个页只写入了一部分,可能会发生脏写。此时,可以通过double write buffer机制对脏写的页进行恢复。
|
||||
|
||||
在一个页被写入到磁盘中时,首先会被写入的是File Header的`FIL_PAGE_SPACE_OR_CHKSUM`,该部分是page的checksum。
|
||||
|
||||
innodb设置了File Trailer部分来校验page是否被完全写入到磁盘中,File Trailer中只包含一个`FIL_PAGE_END_LSN`部分,占用8字节,前4字节代表该页的checksum值,后4字节和File Header中的`FIL_PAGE_LSN`相同,`代表最后一次修改该页关联的LSN位置`。将上述两个字段和File Header中的`FIL_PAGE_SPACE_OR_CHKSUM`和`FIL_PAGE_LSN`值进行比较,查看是否一致,从而保证页的完整性。
|
||||
|
||||
默认情况下,在innodb每次从磁盘读取page时,都会检查页面的完整性。
|
||||
|
||||
## 分区表
|
||||
innodb存储引擎支持分区功能,分区过程将一个表或索引分为多个更小、更可管理的部分。
|
||||
|
||||
对于访问数据库的应用而言,逻辑上的一个表或索引,其物理上可能由多个物理分区组成,每个分区都是独立的对象,既可以独自进行处理,又可以作为一个更大对象的一部分进行处理。
|
||||
|
||||
> mysql仅支持水平分区(将同一张表中的不同行记录分配到不同的物理文件中),并不支持垂直分区(将同一张表中的不同列分配到不同的物理文件中)。
|
||||
|
||||
目前mysql支持如下几种类型的分区:
|
||||
- `RANGE`: 行数据基于一个给定连续区间的列值被放入分区
|
||||
- `LIST`: 和`RANGE`类似,但`LIST`分区面对的是更加离散的值
|
||||
- `HASH`: 根据用户自定义的表达式返回值来进行分区,返回值不能为负数
|
||||
- `KEY`:根据mysql提供的哈希函数来进行分区
|
||||
|
||||
`不论创建何种类型的分区,如果表中存在唯一索引或主键,分区列必须是唯一索引的一个组成部分。`
|
||||
|
||||
### partition keys & primary keys & unique keys
|
||||
对于分区表而言,`所有在partition expression中使用到的col,其必须被包含在分区表所有的唯一索引中`。
|
||||
|
||||
> `所有唯一索引`中包含主键索引。
|
||||
|
||||
正确建立分区表的声明示例如下:
|
||||
```sql
|
||||
CREATE TABLE t1 (
|
||||
col1 INT NOT NULL,
|
||||
col2 DATE NOT NULL,
|
||||
col3 INT NOT NULL,
|
||||
col4 INT NOT NULL,
|
||||
UNIQUE KEY (col1, col2, col3)
|
||||
)
|
||||
PARTITION BY HASH(col3)
|
||||
PARTITIONS 4;
|
||||
|
||||
CREATE TABLE t2 (
|
||||
col1 INT NOT NULL,
|
||||
col2 DATE NOT NULL,
|
||||
col3 INT NOT NULL,
|
||||
col4 INT NOT NULL,
|
||||
UNIQUE KEY (col1, col3)
|
||||
)
|
||||
PARTITION BY HASH(col1 + col3)
|
||||
PARTITIONS 4;
|
||||
|
||||
|
||||
CREATE TABLE t7 (
|
||||
col1 INT NOT NULL,
|
||||
col2 DATE NOT NULL,
|
||||
col3 INT NOT NULL,
|
||||
col4 INT NOT NULL,
|
||||
PRIMARY KEY(col1, col2)
|
||||
)
|
||||
PARTITION BY HASH(col1 + YEAR(col2))
|
||||
PARTITIONS 4;
|
||||
|
||||
CREATE TABLE t8 (
|
||||
col1 INT NOT NULL,
|
||||
col2 DATE NOT NULL,
|
||||
col3 INT NOT NULL,
|
||||
col4 INT NOT NULL,
|
||||
PRIMARY KEY(col1, col2, col4),
|
||||
UNIQUE KEY(col2, col1)
|
||||
)
|
||||
PARTITION BY HASH(col1 + YEAR(col2))
|
||||
PARTITIONS 4;
|
||||
|
||||
|
||||
create table t2_partition (
|
||||
col1 int ,
|
||||
col2 date,
|
||||
col3 int null,
|
||||
col4 int null,
|
||||
unique index `idx_t2_partition_cols` (col1, col2,col3),
|
||||
primary key (col2, col1)
|
||||
)
|
||||
partition by hash(col1)
|
||||
partitions 4;
|
||||
```
|
||||
#### 表中不存在唯一索引
|
||||
如果表中没有唯一索引(也未定义primary key),那么上述要求并不会生效,可以在partition expression中使用任意cols。
|
||||
|
||||
#### 后续向分区表添加唯一索引
|
||||
如果想要向分区表中添加唯一索引,那么新增的唯一索引中必须包含partition expression中所有的列。
|
||||
|
||||
#### 对非分区表进行分区
|
||||
可以参照如下示例对非分区表进行分区:
|
||||
```sql
|
||||
CREATE TABLE np_pk (
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
name VARCHAR(50),
|
||||
added DATE,
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
```
|
||||
可以按`id`进行分区
|
||||
```sql
|
||||
ALTER TABLE np_pk
|
||||
PARTITION BY HASH(id)
|
||||
PARTITIONS 4;
|
||||
```
|
||||
### 分区类型
|
||||
#### RANGE
|
||||
RANGE为较常见的分区类型,如下示例为创建RANGE类型分区表的示例:
|
||||
```sql
|
||||
create table t1 (
|
||||
id bigint auto_increment not null,
|
||||
tran_date datetime(6) not null default now(6) on update now(6),
|
||||
primary key (id, tran_date)
|
||||
) engine=innodb
|
||||
partition by range (year(tran_date)) (
|
||||
partition part_y_let_2024 values less than (2025),
|
||||
partition part_y_eq_2025 values less than (2026)
|
||||
);
|
||||
```
|
||||
上述创建了表`t1`,t1主键为`(id, tran_date)`,且按照`tran_date`字段的年部分进行分区,分区类型为RANGE。
|
||||
|
||||
表t1的分区为两部分,分区1为`tran_date位于2024或之前年份`的分区`part_y_let_2024`,分区2是`tran_date位于2025年`的分区`part_y_eq_2025`。
|
||||
|
||||
##### information_schema.partitions
|
||||
如果要查看表的分区情况,可以查询`information_schema.partitions`表,示例如下
|
||||
```sql
|
||||
select * from information_schema.partitions where table_schema = 'learn_innodb' and table_name = 't1';
|
||||
```
|
||||
查询结果如下所示:
|
||||
| TABLE\_CATALOG | TABLE\_SCHEMA | TABLE\_NAME | PARTITION\_NAME | SUBPARTITION\_NAME | PARTITION\_ORDINAL\_POSITION | SUBPARTITION\_ORDINAL\_POSITION | PARTITION\_METHOD | SUBPARTITION\_METHOD | PARTITION\_EXPRESSION | SUBPARTITION\_EXPRESSION | PARTITION\_DESCRIPTION | TABLE\_ROWS | AVG\_ROW\_LENGTH | DATA\_LENGTH | MAX\_DATA\_LENGTH | INDEX\_LENGTH | DATA\_FREE | CREATE\_TIME | UPDATE\_TIME | CHECK\_TIME | CHECKSUM | PARTITION\_COMMENT | NODEGROUP | TABLESPACE\_NAME |
|
||||
| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- |
|
||||
| def | learn\_innodb | t1 | part\_y\_eq\_2025 | null | 2 | null | RANGE | null | year\(\`tran\_date\`\) | null | 2026 | 0 | 0 | 16384 | 0 | 0 | 0 | 2025-02-08 01:05:53 | null | null | null | | default | null |
|
||||
| def | learn\_innodb | t1 | part\_y\_let\_2024 | null | 1 | null | RANGE | null | year\(\`tran\_date\`\) | null | 2025 | 0 | 0 | 16384 | 0 | 0 | 0 | 2025-02-08 01:05:53 | null | null | null | | default | null |
|
||||
|
||||
为分区表预置如下数据
|
||||
```sql
|
||||
insert into t1(tran_date) values ('2023-01-01'), ('2024-12-31'), ('2025-01-01');
|
||||
```
|
||||
可以看到分区表中数据分布如下:
|
||||
```sql
|
||||
select partition_name, table_rows from information_schema.partitions where table_schema = 'learn_innodb' and table_name = 't1';
|
||||
```
|
||||
| PARTITION\_NAME | TABLE\_ROWS |
|
||||
| :--- | :--- |
|
||||
| part\_y\_eq\_2025 | 1 |
|
||||
| part\_y\_let\_2024 | 2 |
|
||||
|
||||
易知`('2023-01-01'), ('2024-12-31')`数据位于`part_y_let_2024`分区,故而该分区存在两条数据;而`('2025-01-01')`数据位于`part_y_let_2024`分区,该分区存在一条数据。
|
||||
|
||||
##### 看select语句查询了哪些分区
|
||||
数据分布可以通过如下方式验证:
|
||||
```sql
|
||||
explain select * from t1 where tran_date >= '2023-01-01' and tran_date <= '2024-12-31';
|
||||
```
|
||||
| id | select\_type | table | partitions | type | possible\_keys | key | key\_len | ref | rows | filtered | Extra |
|
||||
| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- |
|
||||
| 1 | SIMPLE | t1 | part\_y\_let\_2024 | index | PRIMARY | PRIMARY | 16 | null | 2 | 50 | Using where; Using index |
|
||||
|
||||
```sql
|
||||
explain select * from t1 where tran_date >= '2025-01-01'
|
||||
```
|
||||
| id | select\_type | table | partitions | type | possible\_keys | key | key\_len | ref | rows | filtered | Extra |
|
||||
| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- |
|
||||
| 1 | SIMPLE | t1 | part\_y\_eq\_2025 | index | PRIMARY | PRIMARY | 16 | null | 1 | 100 | Using where; Using index |
|
||||
|
||||
##### 插入超过分区范围的数据
|
||||
如果尝试向分区表中插入超过所有分区范围的数据,会执行失败,示例如下:
|
||||
```sql
|
||||
insert into t1(tran_date) values ('2026-01-01');
|
||||
```
|
||||
由于分区表`t1`中现有分区最大只支持2025年的分区,故而当尝试插入2026年分区时,会抛出如下错误:
|
||||
```bash
|
||||
[2025-02-08 01:32:51] [HY000][1526] Table has no partition for value 2026
|
||||
```
|
||||
##### 向分区表中添加分区
|
||||
mysql支持向分区表中添加分区,语法如下
|
||||
```sql
|
||||
alter table t1 add partition (
|
||||
partition part_year_egt_2026 values less than maxvalue
|
||||
);
|
||||
```
|
||||
添加分区后,mysql支持`tran_date大于等于2026`的分区,执行如下语句
|
||||
```sql
|
||||
insert into t1(tran_date) values ('2026-01-01'), ('2027-12-31'), ('2028-01-01');
|
||||
```
|
||||
之后,再次分区分区中的数据分区,结果如下
|
||||
| PARTITION\_NAME | TABLE\_ROWS |
|
||||
| :--- | :--- |
|
||||
| part\_y\_eq\_2025 | 1 |
|
||||
| part\_y\_let\_2024 | 2 |
|
||||
| part\_year\_egt\_2026 | 3 |
|
||||
|
||||
> `information.partitions`表中,`table_rows`字段可能存在不准确的情况,如果想要获取准确的值,需要执行`analyze table {schema}.{table_name}`语句
|
||||
|
||||
##### 向分区表中删除分区
|
||||
当想要删除分区表中现存分区时,可以通过执行如下语句
|
||||
```sql
|
||||
alter table t1 drop partition part_year_egt_2026;
|
||||
```
|
||||
在删除分区后,分区中的数据也会被删除,执行查询语句后
|
||||
```sql
|
||||
select * from t1;
|
||||
|
||||
```
|
||||
可知`part_year_egt_2026`被删除后分区中数据全部消失
|
||||
| id | tran\_date |
|
||||
| :--- | :--- |
|
||||
| 1 | 2023-01-01 00:00:00.000000 |
|
||||
| 2 | 2024-12-31 00:00:00.000000 |
|
||||
| 3 | 2025-01-01 00:00:00.000000 |
|
||||
|
||||
#### LIST
|
||||
LIST分区类型和RANGE分区类型非常相似,但是LIST分区列的值是离散的,RANGE分区列的值是连续的。
|
||||
|
||||
创建LIST类型分区表的示例如下:
|
||||
```sql
|
||||
create table p_list_t1 (
|
||||
id bigint not null auto_increment,
|
||||
area smallint not null,
|
||||
primary key (id, area)
|
||||
)
|
||||
partition by list (area) (
|
||||
partition p_ch_beijing values in (1),
|
||||
partition p_ch_hubei values in (2),
|
||||
partition p_ch_jilin values in (3)
|
||||
);
|
||||
```
|
||||
之后可向`p_list_t1`表中预置数据,执行语句如下:
|
||||
```sql
|
||||
insert into p_list_t1(area) values (1),(1), (2), (3), (3);
|
||||
```
|
||||
此时,数据分布如下:
|
||||
| PARTITION\_METHOD | PARTITION\_NAME | TABLE\_ROWS |
|
||||
| :--- | :--- | :--- |
|
||||
| LIST | p\_ch\_beijing | 2 |
|
||||
| LIST | p\_ch\_hubei | 1 |
|
||||
| LIST | p\_ch\_jilin | 2 |
|
||||
|
||||
如果想要向分区表中插入不位于现存分区中的值,那么插入语句同样会执行失败,示例如下
|
||||
```sql
|
||||
insert into p_list_t1(area) values (4);
|
||||
```
|
||||
由于目前没有area为4的分区,故而插入语句执行失败,报错如下
|
||||
```bash
|
||||
[2025-02-09 20:30:03] [HY000][1526] Table has no partition for value 4
|
||||
```
|
||||
|
||||
#### HASH
|
||||
HASH分区方式是将数据均匀分布到预先定义的各个分区中,期望各个分区中散列的数据数量大致相同。
|
||||
|
||||
在通过hash来进行分区时,建表时需要指定一个分区表达式,该表达式返回整数。
|
||||
|
||||
创建hash分区的示例如下:
|
||||
```sql
|
||||
create table p_hash_t1 (
|
||||
id bigint not null auto_increment,
|
||||
content varchar(256),
|
||||
primary key (id)
|
||||
)
|
||||
partition by hash (id)
|
||||
partitions 4;
|
||||
```
|
||||
上述ddl创建了一个按照`id`列进行hash分区的分区表,分区表包含4个分区。
|
||||
|
||||
预置数据sql如下:
|
||||
```sql
|
||||
insert into p_hash_t1(content) values ('asahi'), ('maki'), ('katahara');
|
||||
```
|
||||
预置数据后数据分布如下
|
||||
| PARTITION\_METHOD | PARTITION\_NAME | TABLE\_ROWS |
|
||||
| :--- | :--- | :--- |
|
||||
| HASH | p0 | 0 |
|
||||
| HASH | p1 | 1 |
|
||||
| HASH | p2 | 1 |
|
||||
| HASH | p3 | 1 |
|
||||
|
||||
预置的三条数据id分别为`1, 2, 3`,被hash放置到分区`p1, p2, p3`中。
|
||||
|
||||
> 当使用hash分区方式时,例如存在`4`个分区,对于待插入数据其分区表达式的值为`2`,那么待插入数据将会被插入到`2%4 = 2`,第`3`个分区中(0,1,2分区,排第三),也就是`p2`。
|
||||
>
|
||||
> 可以通过`explain select * from p_hash_t1 where id = 2;`语句来进行验证,得到如下结果。
|
||||
>
|
||||
> | id | select\_type | table | partitions | type | possible\_keys | key | key\_len | ref | rows | filtered | Extra |
|
||||
> | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- |
|
||||
> | 1 | SIMPLE | p\_hash\_t1 | p2 | const | PRIMARY | PRIMARY | 8 | const | 1 | 100 | null |
|
||||
|
||||
|
||||
##### 新增HASH分区
|
||||
对于hash分区方式,可以通过如下方式来新增分区:
|
||||
```sql
|
||||
alter table p_hash_t1 add partition partitions 3;
|
||||
```
|
||||
其会新增3个分区,执行后分区数量为7。
|
||||
|
||||
此时,原本分区中的数据会被重新散列到新分区中
|
||||
|
||||
##### 减少HASH分区
|
||||
对于hash分区,在尝试减少分区数量的同时,通常不希望删除任何数据,故而无法使用`drop`。
|
||||
|
||||
故而,可以使用`coalesce`,其只会减少分区数量,不会删除数据。
|
||||
|
||||
示例如下:
|
||||
```sql
|
||||
alter table p_hash_t1 coalesce partition 4;
|
||||
```
|
||||
上述语句会将分区数量减少4个,故而减少后分区数量为`3`。
|
||||
|
||||
|
||||
#### LINEAR HASH
|
||||
创建linear hash分区表的方式和hash类似,可以通过如下方式
|
||||
```sql
|
||||
create table p_linear_hash_t1 (
|
||||
id bigint not null auto_increment,
|
||||
content varchar(256),
|
||||
primary key(id)
|
||||
)
|
||||
partition by linear hash (id)
|
||||
partitions 5;
|
||||
```
|
||||
相较于hash分区,在使用linear hash分区时,添加、删除、合并、拆分分区将会变得更加快捷,但是相比于hash分区,其数据分区可能会不太均匀。
|
||||
|
||||
#### KEY & LINEAR KEY
|
||||
KEY分区方式和HASH分区方式类似,但是HASH分区采用用户自定义的函数进行分区,而KEY分区方式则是采用mysql提供的函数进行分区。
|
||||
|
||||
创建key分区表的示例如下:
|
||||
```sql
|
||||
create table p_key_t1 (
|
||||
id bigint not null auto_increment,
|
||||
content varchar(256),
|
||||
primary key(id)
|
||||
)
|
||||
partition by key(id)
|
||||
partitions 5;
|
||||
```
|
||||
> key分区方式同样可以使用linear进行修饰,效果和hash分区方式类似。
|
||||
|
||||
> 对于key分区,可以指定除了integer之外的col类型,而`range, list, hash`等分区类型只支持integer类型的col。
|
||||
|
||||
|
||||
#### COLUMNS
|
||||
COLUMNS分区方式为`RANGE`和`LIST`分区的变体。
|
||||
|
||||
COLUMNS分区方式支持在分区时使用多个列,在插入新数据或查询数据决定分区时,所有的列都会被考虑。
|
||||
|
||||
COLUMNS分区方式分为如下两种
|
||||
- RANGE COLUMNS
|
||||
- LIST COLUMNS
|
||||
|
||||
上述两种方式都支持非整数类型的列,COLUMNS支持的数据类型如下:
|
||||
- 所有integers类型(DECIMAL, FLOAT则不受支持)
|
||||
- DATE和DATETIME类型
|
||||
- 字符串类型:CHAR, VARCHAR, BINARY, VARBINARY(TEXT, BLOB类型不受支持)
|
||||
|
||||
##### range columns
|
||||
创建range columns分区表的示例如下:
|
||||
```sql
|
||||
create table p_range_columns_t1 (
|
||||
id bigint not null auto_increment,
|
||||
name varchar(256),
|
||||
born_ts datetime(6),
|
||||
primary key (id, born_ts)
|
||||
)
|
||||
partition by range columns (born_ts) (
|
||||
partition p0 values less than ('1900-01-01'),
|
||||
partition p1 values less than ('2000-01-01'),
|
||||
partition p2 values less than ('2999-01-01')
|
||||
);
|
||||
```
|
||||
|
||||
##### list columns
|
||||
创建list columns分区表的示例则如下:
|
||||
```sql
|
||||
create table p_list_columns_t1 (
|
||||
id bigint not null auto_increment,
|
||||
province varchar(256),
|
||||
city varchar(256),
|
||||
primary key (id, province, city)
|
||||
)
|
||||
partition by list columns (province, city) (
|
||||
partition p_beijing values in (('china', 'beijing')),
|
||||
partition p_wuhan values in (('hubei', 'wuhan'))
|
||||
);
|
||||
```
|
||||
|
||||
> 通过range columns和list columns,可以很好的替代range和list分区,而无需将列值都转换为整型。
|
||||
|
||||
### 子分区
|
||||
`子分区`代表在分区的基础上再次进行分区,mysql允许`在range和list分区的基础`上再次进行`hash或key`的子分区,示例如下:
|
||||
```sql
|
||||
create table p_list_columns_t1 (
|
||||
id bigint not null auto_increment,
|
||||
province varchar(256),
|
||||
city varchar(256),
|
||||
primary key (id, province, city)
|
||||
)
|
||||
partition by list columns (province, city)
|
||||
subpartition by key(id)
|
||||
subpartitions 3
|
||||
(
|
||||
partition p_beijing values in (('china', 'beijing')),
|
||||
partition p_wuhan values in (('hubei', 'wuhan'))
|
||||
);
|
||||
```
|
||||
上述示例先按照`list columns`分区方式进行了分区,然后在为每个分区都按`key`分了三个子分区,查看分区和子分区详情可以通过如下语句:
|
||||
```sql
|
||||
select partition_name, partition_method, subpartition_name, subpartition_method from information_schema.partitions where table_name = 'p_list_columns_t1';
|
||||
```
|
||||
| PARTITION\_NAME | PARTITION\_METHOD | SUBPARTITION\_NAME | SUBPARTITION\_METHOD |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| p\_wuhan | LIST COLUMNS | p\_wuhansp0 | KEY |
|
||||
| p\_wuhan | LIST COLUMNS | p\_wuhansp1 | KEY |
|
||||
| p\_wuhan | LIST COLUMNS | p\_wuhansp2 | KEY |
|
||||
| p\_beijing | LIST COLUMNS | p\_beijingsp0 | KEY |
|
||||
| p\_beijing | LIST COLUMNS | p\_beijingsp1 | KEY |
|
||||
| p\_beijing | LIST COLUMNS | p\_beijingsp2 | KEY |
|
||||
|
||||
### 分区中的NULL值
|
||||
mysql允许对null值做分区,但`mysql数据库中的分区总是视null值小于任何非null的值`,该逻辑`和order by处理null值的逻辑一致`。
|
||||
|
||||
故而,在使用不同的分区类型时,对于null值的处理逻辑如下:
|
||||
- 对于range类型分区,当插入null值时,其会被放在最左侧的分区中
|
||||
- 对于list类型的分区,如果要插入null值,必须在分区的`values in (...)`表达式中指定null值,否则插入语句将会报错
|
||||
- hash和key的分区类型,将会将null值当作`0`来散列
|
||||
|
||||
### 分区和性能
|
||||
对于OLTP类型的应用,使用分区表时应该相当小心,因为分区表可能会带来严重的性能问题。
|
||||
|
||||
例如,对于包含1000w条数据的表`t`,如果包含主键索引`id`和非主键索引`code`,分区和不分区,其性能分析如下
|
||||
|
||||
#### 不分区
|
||||
如果不对表t进行分区,那么根据`id`(唯一主键索引)或`code`(非unique索引)进行查询,例如`select * from t where id = xxx`或`select * from t where code = xxx`时,可能只会进行2~3次磁盘io(1000w数据构成的B+树其层高为2~3)。
|
||||
|
||||
#### 按id进行分区
|
||||
如果对表`t`按照`id`进行`hash`分区,分为10个分区,那么:
|
||||
- 对于按`id`进行查找的语句`select * from t where id = xxx`
|
||||
- 将t按id分为10个区后,如果分区均匀,那么每个分区数据大概为100w,对分区的查询开销大概为2次磁盘io
|
||||
- 根据`id`的查询只会在一个分区内进行查找,故而磁盘io会从3次减少为2次,可以提升查询效率
|
||||
- 对于按`code`进行查找的语句`select * from t where code = xxx`
|
||||
- 对于按code进行查找的语句,需要扫描所有的10个分区,每个分区大概需要2次磁盘io,故而总共的磁盘io大约为20次
|
||||
|
||||
> 在使用分区表时,应尽量小心,不正确的使用分区将可能会带来大量的io,造成性能瓶颈
|
||||
|
||||
### 在表和分区之间交换数据
|
||||
mysql支持在表分区和非分区表之间交换数据:
|
||||
- 在非分区表为空的场景下,相当于将分区中的数据移动到非分区表中
|
||||
- 在表分区为空的场景下,相当于将非分区表中的数据移动到表分区中
|
||||
|
||||
在表和分区之间交换数据,可以通过`alter table ... exchange partition`语句,且必须满足如下条件:
|
||||
- 要交换的非分区表和分区表必须要拥有相同的结构,但是,非分区表中不能够含有分区
|
||||
- 非分区表中的数据都要位于表分区的范围内
|
||||
- 被交换的表中不能含有外键,或是其他表含有被交换表的外键引用
|
||||
- 用户需要拥有`alter, insert, create, drop`权限
|
||||
- 使用`alter table ... exchange parition`语句时,不会触发交换表和被交换表上的触发器
|
||||
- `auto_increment`将会被重置
|
||||
|
||||
`exchange partition ... with table`的示例如下:
|
||||
```sql
|
||||
alter table p_range_columns_t exchange partition p_2024 with table np_t
|
||||
```
|
||||
29
mysql/mysql文档/undo log.md
Normal file
29
mysql/mysql文档/undo log.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# innodb存储引擎
|
||||
## innodb多版本控制
|
||||
innodb是一个多版本存储引擎,innodb针对被修改的数据行(changed rows)保存了旧版本信息,故而其支持并发、回滚等事务特性。旧版本信息存储在undo tablespaces中,按rollback segment的结构进行存储。在事务回滚时,innodb会使用rollback segment中存储的信息来执行undo操作。
|
||||
|
||||
同样的,innodb也会使用rollback segment中的信息来构建更早的行数据的版本,从而保证读操作的一致性。
|
||||
|
||||
innodb会在内部为存储在数据库中的每行数据新增3个字段:
|
||||
- `DB_TRX`: 该字段为6字节,代表能唯一标识`最后更新或插入(insert/update)该行数据的事务`的标识id。并且,在内部`delete操作也被视为update操作`,在删除数据时,行数据中特定位置的bit会被置位,将该行数据标识为删除
|
||||
- `DB_ROLL_PTR`: 该字段为7字节,被称为roll pointer。roll pointer指向一条写入到rollback sengment中的undo log记录,如果当前行被更新,那么undo log中包含可以重新构建行数据中更新前版本的信息。
|
||||
- `DB_ROLL_ID`:该字段为6字节,该字段随着新数据被插入而单调增加。如果innodb自动产生了聚簇索引,那么索引中将会包含row id的值;否则`DB_ROW_ID`将不会出现在任何索引中
|
||||
|
||||
rollback segment中的undo logs被区分为了insert和update类型。
|
||||
- insert undo log只在事务回滚时才需要,并且在事务提交之后就被丢弃
|
||||
- update undo log除了在事务回滚时需要用到外,在一致性读操作中也需要用到。innodb为一致性读分配了一个事务的快照,只有当快照中的事务都不存在后,update undo log才能被丢弃。在保证一致性读时,可能需要undo log中的信息来构建行数据修改前的早期版本。
|
||||
|
||||
`建议定期提交事务,即使是只进行一致性读的事务`。否则,在事务提交前,innodb无法丢弃update undo log,rollback segment占用空间大小会快速增加,填满其所在的undo tablespace。
|
||||
|
||||
rollback segment中的undo log record,其占用物理空间大小通常小于关联的插入或更新行,可以通过该信息来计算rollback segment所需要的空间。
|
||||
|
||||
在innodb的多版本控制方案中,在通过sql语句删除行数据时,行数据并不会马上从数据库中被移除。只有该删除操作关联的update undo log record被丢弃时,行数据和其关联索引记录才会被物理移除。该移除操作称之为purge,移除操作的发生顺序和sql语句的执行时间顺序相同。
|
||||
|
||||
### 多版本和辅助索引
|
||||
innodb mvcc将聚簇索引和非聚簇索引分开对待。聚簇索引中的记录会就地更新,并且其隐藏的系统列会指向undo log record。不像聚簇索引记录,非聚簇索引记录并不会包含隐藏的系统列,也不会就地更新。
|
||||
|
||||
当非聚簇索引列被更新时,旧的聚簇索引记录将会被标记为删除,新记录将会被插入,并且标记为删除的索引记录将会被purge。当非聚簇索引记录被标记为删除或非聚簇索引page被新事务更新,innodb会在聚簇索引中查找数据记录。在聚簇索引中,如果记录在读取事务初始化之后被更新,那么记录的`DB_TRX`将会被检查,并且会从undo log中获取正确的版本号。
|
||||
|
||||
|
||||
|
||||
|
||||
375
mysql/mysql文档/锁.md
Normal file
375
mysql/mysql文档/锁.md
Normal file
@@ -0,0 +1,375 @@
|
||||
- [锁](#锁)
|
||||
- [lock 和 latch](#lock-和-latch)
|
||||
- [latch](#latch)
|
||||
- [lock](#lock)
|
||||
- [innodb中的锁](#innodb中的锁)
|
||||
- [意向锁](#意向锁)
|
||||
- [Record Lock](#record-lock)
|
||||
- [一致性读](#一致性读)
|
||||
- [一致性非锁定读](#一致性非锁定读)
|
||||
- [一致性锁定读](#一致性锁定读)
|
||||
- [Gap Lock](#gap-lock)
|
||||
- [行锁算法](#行锁算法)
|
||||
- [Next-Key Lock](#next-key-lock)
|
||||
- [insert-intention lock](#insert-intention-lock)
|
||||
- [加锁示例](#加锁示例)
|
||||
- [performance\_schema.data\_locks](#performance_schemadata_locks)
|
||||
- [select ... 加锁情况](#select--加锁情况)
|
||||
- [select ... for update 加锁情况](#select--for-update-加锁情况)
|
||||
- [select语句未命中索引](#select语句未命中索引)
|
||||
- [select语句命中非unique索引](#select语句命中非unique索引)
|
||||
- [select语句命中主键索引](#select语句命中主键索引)
|
||||
- [update ... 加锁情况](#update--加锁情况)
|
||||
- [update语句未命中索引的加锁情况](#update语句未命中索引的加锁情况)
|
||||
- [update语句命中非unique索引](#update语句命中非unique索引)
|
||||
- [update语句命中主键索引](#update语句命中主键索引)
|
||||
- [innodb死锁](#innodb死锁)
|
||||
- [超时](#超时)
|
||||
- [wait-for graph](#wait-for-graph)
|
||||
- [innodb锁升级](#innodb锁升级)
|
||||
|
||||
# 锁
|
||||
## lock 和 latch
|
||||
在mysql数据库中,lock和latch都可以被称之为锁,但是两者锁包含意义不同。
|
||||
### latch
|
||||
latch一般为轻量级的锁,要求锁定时间通常非常短,否则会影响性能。在innodb中,latch可以分为mutex和rwlock。
|
||||
|
||||
`latch通常被用于防止代码中临界资源的并发访问`,保证并发操作的安全性。latch操作通常没有死锁检测机制。
|
||||
|
||||
### lock
|
||||
lock和latch则稍有区别,lock一般针对的是事务,用于锁定数据库中的表、页、行等数据。并且,`lock一般在数据库事务进行commit或rollback后才释放`。
|
||||
|
||||
在innodb中,lock拥有死锁检测机制。
|
||||
|
||||
latch和lock的比较如下:
|
||||
| | lock | latch |
|
||||
| :-: | :-: | :-: |
|
||||
| 对象 | 事务 | 线程 |
|
||||
| 保护 | 数据库内容 | 代码中的临界资源 |
|
||||
| 持续时间 | 整个事务过程 | 临界资源 |
|
||||
| 模式 | 行锁、表锁、意向锁 | 读写锁、互斥量 |
|
||||
| 死锁 | 通过waits-for graph、timeout等机制检测死锁 | 无死锁检测机制 |
|
||||
|
||||
在innodb中,可以通过`show engine innodb status`以及`information_schema`下的`innodb_trx`来查看锁的情况。
|
||||
|
||||
## innodb中的锁
|
||||
innodb中针对行级锁有如下两种实现:
|
||||
- S lock(共享锁):允许事务针对一行数据进行读取
|
||||
- X lock(排他锁):允许事务针对一行数据进行更新和删除
|
||||
|
||||
S lock和S lock之间可以互相兼容,但是S lock和X lock、 X lock和 X lock之间都是不兼容的。当一个事务t持有某行数据的X lock时,其他事务必须等待事务t提交或回滚之后才能获取该行数据的S lock或X lock。
|
||||
|
||||
除了行锁之外,innodb还支持多粒度的锁定,允许行级和表级的锁同时存在。为了支持该操作,innodb引入了一种额外的加锁方式`意向锁`。
|
||||
|
||||
### 意向锁
|
||||
意向锁支持多粒度加锁,允许行锁和表锁共存。例如,`lock tables ... write`会在指定表上加X锁。为了在多个粒度上进行加锁,innodb使用了意向锁,意向锁是表级锁,代表事务稍后需要为表中的数据加上哪种锁。
|
||||
|
||||
意向锁分为两种类型:
|
||||
- IS: 事务旨在为table中独立的行添加共享锁
|
||||
- IX: 事务旨在为table中的行添加排他锁
|
||||
|
||||
例如,`select ... for share`设置IS锁,而`select ... for update`则设置了IX锁。
|
||||
|
||||
意向锁协议如下:
|
||||
- 在事务获取table中某行记录的s锁之前,其必须获取table的IS或更高级别锁
|
||||
- 在事务获取table中某行记录的x锁之前,其必须获取table的IX锁
|
||||
|
||||
table level锁的兼容性如下所示:
|
||||
| | x | ix | s | is |
|
||||
| :-: | :-: | :-: | :-: | :-: |
|
||||
| x | 不兼容 | 不兼容 | 不兼容 | 不兼容 |
|
||||
| ix | 不兼容 | 兼容 | 不兼容 | 兼容 |
|
||||
| s | 不兼容 | 不兼容 | 兼容 | 兼容 |
|
||||
| is | 不兼容 | 兼容 | 兼容 | 兼容 |
|
||||
|
||||
如果要加的锁和已经存在的锁兼容,那么它将被授予请求加锁的事务;要加的锁和已经存在的锁冲突时,则不会授予。请求加锁的事务将会阻塞,一直等待到存在冲突的锁被释放。
|
||||
|
||||
如果事务中的锁请求因为与现有锁冲突而无法被授予,并且造成死锁,那么会抛出异常。
|
||||
|
||||
> 意向锁和意向锁之间是兼容的,故而意向锁只会和全表扫描相冲突。
|
||||
|
||||
引入意向锁的意图是展示有人锁定了表中的某一行,或是将要锁定表中的某一行。
|
||||
|
||||
事务和意向锁相关的信息可以通过`show engine innodb status`来展示,其输出如下:
|
||||
```
|
||||
TABLE LOCK table `test`.`t` trx id 10080 lock mode IX
|
||||
```
|
||||
|
||||
### Record Lock
|
||||
记录锁是针对一条index record的锁。例如,`SELECT c1 FROM t WHERE c1 = 10 FOR UPDATE`语句会阻塞任何其他事务针对`c1 = 10`记录的插入、更新或删除。
|
||||
|
||||
records总是会针对index record加锁,即使当前table没有定义索引。对于table未定义索引的情况,innodb会创建一个隐藏的聚簇索引,并使用该聚簇索引来进行record lock。
|
||||
|
||||
事务相关record lock的信息,可以通过`SHOW ENGINE INNODB STATUS`来展示,其输出格式如下:
|
||||
```
|
||||
RECORD LOCKS space id 58 page no 3 n bits 72 index `PRIMARY` of table `test`.`t`
|
||||
trx id 10078 lock_mode X locks rec but not gap
|
||||
Record lock, heap no 2 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
|
||||
0: len 4; hex 8000000a; asc ;;
|
||||
1: len 6; hex 00000000274f; asc 'O;;
|
||||
2: len 7; hex b60000019d0110; asc ;;
|
||||
```
|
||||
|
||||
### 一致性读
|
||||
#### 一致性非锁定读
|
||||
`一致性非锁定读`是指innodb通过mvcc(多版本并发控制)来读取行数据。如果当前待读取的行正在执行update或delete操作,那么此时`读取行的操作并不会被阻塞,而是会去读取当前被修改行的快照历史版本`。这种行为被成为`非锁定读`。
|
||||
|
||||
在非锁定读的场景下,事务读取行数据时并不需要等待其他事务持有的X锁释放,而是之前读取行数据的历史快照版本。`历史快照版本通过`undo log`来生成`。
|
||||
|
||||
非锁定读极大的提高了数据库的并发性,一个事务对行数据的写操作并不会阻塞其他的事务对该行进行读取。innodb在默认的隔离级别下,默认通过非锁定读来读取数据。
|
||||
|
||||
`在不同事务隔离级别下,读取数据的方式可能会不同,并非所有隔离级别都采用非锁定读的读取方式`。
|
||||
|
||||
> #### 多版本并发控制(mvcc)
|
||||
> 在非锁定读场景下,一行数据通常不会只有一个历史版本,其数据快照并不只有一个,这被称为多版本并发控制。
|
||||
>
|
||||
> innodb对read committed和repeatable read隔离级别使用非锁定读,但是,两种隔离级别`快照定义并不相同`。
|
||||
>
|
||||
> #### read committed
|
||||
>
|
||||
> 在read committed隔离级别下,非锁定读总是会读取行数据`最新的快照`。
|
||||
>
|
||||
> #### repeatable read
|
||||
> 在repeatable read隔离级别下,非锁定读则是会读取`事务开始时的行数据版本`。
|
||||
|
||||
|
||||
|
||||
|
||||
#### 一致性锁定读
|
||||
在默认隔离级别下,read committed和repeatable read都是用非锁定读,但是,可以通过语法显式的支持锁定读。`用户可以通过加锁来保证读取数据的一致性`。
|
||||
|
||||
在通过select语句对数据加锁时,支持加两种类型的锁:
|
||||
- select ... for update
|
||||
- select ... lock in share mode
|
||||
|
||||
`select ... for update`实际是对读取的行记录加上X锁,其他事务均不能对该数据添加任何的锁。
|
||||
|
||||
`select ... lock in share mode`则是对读取的记录加上S锁,其他事务可以向被锁定记录加S锁,但是不能加X锁。
|
||||
|
||||
`对于非锁定读,即使数据被添加了X锁,也可以进行读取。`只有通过`for update`或`lock in share mode`在读取时添加X锁或S锁时,读取操作才会被阻塞。
|
||||
|
||||
并且,`for update`或`lock in share mode`添加的行锁,在事务commit或rollback时会被释放。
|
||||
|
||||
|
||||
### Gap Lock
|
||||
gap lock是针对index record之间的间隙来进行加锁的(或是针对第一条index record之前或最后一条index record之后的间隙进行加锁)。例如,`SELECT c1 FROM t WHERE c1 BETWEEN 10 and 20 FOR UPDATE; `语句会阻止其他事务在c1值的`10~20`范围之间插入数据,不管这条数据存不存在,在`10~20`的间隙之前所有的值都被锁定了。
|
||||
|
||||
间隙范围可能跨单个索引值,多个索引值甚至为空。
|
||||
|
||||
间隙锁在部分的事务隔离级别中有使用,在其他隔离级别中则不会被使用。
|
||||
|
||||
在通过unique index区查询唯一行时,并不需要使用间隙锁(并不包含unique复合索引的情况)。例如,在id为唯一索引的情况下,如下所示的语句只会使用index record lock:
|
||||
```sql
|
||||
SELECT * FROM child WHERE id = 100;
|
||||
```
|
||||
|
||||
但是,如果id并不是索引,或者id的缩影并不unique,那么上述语句会对之前的间隙进行加锁。
|
||||
|
||||
值得注意的是,不同事务针对同一间隙,可以持有相互冲突的锁。例如,A事务针对间隙持有S锁,而B事务针对间隙持有X锁,当index record从索引中被清除时,不同事务所持有的间隙锁将会被合并。
|
||||
|
||||
在innodb中,间隙锁只是为了防止其他事务在该间隙中插入锁,间隙锁是可以共存的。如果事务A针对间隙加锁,这并不会阻止事务B获取同一间隙的锁。`间隙的S锁和X锁都遵循该规则,且S锁和X锁之间并不会相互冲突,间隙的S锁和X锁功能相同`。
|
||||
|
||||
间隙锁可以被显示的禁用,如果将事务的隔离级别改为`read committed`,在查找和索引扫描时间隙锁将会被禁用,间隙锁只会被用于外键检查和重复key检查。
|
||||
|
||||
### 行锁算法
|
||||
innodb中有三种行锁的算法,如下:
|
||||
- Record Lock:单个行记录上的锁
|
||||
- Gap Lock:间隙锁,锁定一个范围,但是不包含记录本身
|
||||
- Next-Key Lock:Gap Lock + Record Lock,锁定一个范围,并且锁定记录本身
|
||||
|
||||
Record Lock是针对索引加的锁,`如果在建表时没有为表设置索引,那么innodb会采用隐式的主键来进行锁定。`
|
||||
|
||||
#### Next-Key Lock
|
||||
Next-Key Lock结合了record lock和gap lock,如果一个索引中有10,11,13,20这四条记录,那么next-key lock的可锁定区间如下:
|
||||
- (-∞, 10]
|
||||
- (10, 11]
|
||||
- (11, 13]
|
||||
- (13, 20]
|
||||
- (20, +∞)
|
||||
|
||||
Next-Key Lock是为了解决幻读问题而引入的,如果事务T1已经锁定了`(10, 11]`和`(11, 13]`区间,那么在T1插入值为12的记录时,锁定的范围会变为:
|
||||
`(10, 11] (11, 12] (12, 13]`。
|
||||
|
||||
但是,`如果查询的索引为unique索引,那么innodb则是会针对next-key lock进行优化,将其降级为record lock,紧锁住索引本身,而不对范围进行加锁。`
|
||||
|
||||
|
||||
#### insert-intention lock
|
||||
插入意向锁是在插入行数据前,由`插入操作`设置的间隙锁。多个事务在针对同一间隙进行插入操作时,`如果他们并不在同一位置进行插入,那么各个事务之间并不需要彼此等待`。
|
||||
|
||||
例如,多个事务需要在`(4, 7)`的间隙之间插入值,但是A事务插入5,B事务插入6,那么在获取待插入行的X锁之前,都需要通过insert intention lock来锁住`(4, 7)`的间隙。插入意向锁之间并不会相互阻塞。
|
||||
|
||||
|
||||
### 加锁示例
|
||||
首先,创建数据库并预制数据
|
||||
```sql
|
||||
-- ddl
|
||||
CREATE TABLE `t_learn_lock` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||
`content` varchar(32) NOT NULL DEFAULT '',
|
||||
`lv` int NOT NULL DEFAULT '-1',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_lv` (`lv`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
|
||||
|
||||
-- dml
|
||||
insert into t_lear_lock(id, content, lv) values
|
||||
(1, 'a', 3),
|
||||
(2, 'd', 7),
|
||||
(3, 'f', 9),
|
||||
(4, 'o', 13);
|
||||
```
|
||||
|
||||
会话的事务默认隔离级别为repeatable read
|
||||
|
||||
#### performance_schema.data_locks
|
||||
`performace_schema.data_locks`中存储了加锁情况,其表结构如下:
|
||||
- engine:该锁代表的存储引擎
|
||||
- engine_transaction_id: 存储引擎中标识事务的id
|
||||
- thread_id: `创建该锁的会话`的线程id
|
||||
- object_schema: 包含该表的schema
|
||||
- object_name:表名称
|
||||
- index_name: 被加锁的索引名称
|
||||
- lock_type: 加锁的类型,对于innodb,其值为record或table,代表行锁或表锁
|
||||
- lock_mode: 请求锁的方式,对于innodb,其格式为`S[,GAP], X[,GAP], IS[,GAP], IX[,GAP], AUTO_INC, and UNKNOWN`。
|
||||
- lock_data:锁相关的数据
|
||||
- lock_status: 请求锁的状态,在innodb中,可以为`GRANTED或WAITING`
|
||||
|
||||
> #### lock mode
|
||||
> 在lock mode字段中,除了auto_inc和unknown之外的值都代表gap lock
|
||||
>
|
||||
> 其取值如下:
|
||||
> - X: next_key_locking,`代表x和x之前的间隙`
|
||||
> - X,GAP:间隙锁,`代表x之前的间隙,不包含x记录本身`
|
||||
> - X, REC_NOT_GAP:`代表X记录本身,不包含之前的间隙`
|
||||
> - X,GAP,INSERT_INTENTION:插入意向锁,彼此之间不互斥
|
||||
|
||||
#### select ... 加锁情况
|
||||
在开启事务后,执行`select * from t_lear_lock`语句,默认`并不会为index record或表加上任何锁`,在可重复读的隔离级别下,默认会采用无锁的一致性读方式,来读取数据的历史版本快照,期间并不需要进行任何加锁操作。
|
||||
|
||||
#### select ... for update 加锁情况
|
||||
在开启事务后,如果执行`select ... for update`语句,那么分为如下几种场景:
|
||||
|
||||
##### select语句未命中索引
|
||||
若执行`select * from t_learn_lock where content < 'h' and content > 'e' for update for update;`语句,由于content字段上并没有添加索引,故而该查询语句并不会命中索引,possible_keys为空。
|
||||
|
||||
在未命中索引的情况下,`select ... for update`语句`会针对主键索引来进行加锁`,其加锁情况如下:
|
||||
|
||||
|INDEX_NAME|LOCK_TYPE|LOCK_MODE|LOCK_STATUS|LOCK_DATA|
|
||||
|----------|---------|---------|-----------|---------|
|
||||
||TABLE|IX|GRANTED||
|
||||
|PRIMARY|RECORD|X|GRANTED|supremum pseudo-record|
|
||||
|PRIMARY|RECORD|X|GRANTED|1|
|
||||
|PRIMARY|RECORD|X|GRANTED|2|
|
||||
|PRIMARY|RECORD|X|GRANTED|3|
|
||||
|PRIMARY|RECORD|X|GRANTED|4|
|
||||
|
||||
其中,`select * from t_learn_lock where content < 'h' and content > 'e' for update for update;`会查询出id为`3`的记录,但是由于未命中索引,`该语句会针对主键中所有的index record进行加锁(包括 supremum prseudo-record)`。
|
||||
|
||||
并且,在可重复读(repeatable read)的隔离级别下,会针对所有的记录添加`next-key`锁,锁住index record和位于该记录之前的间隙。
|
||||
|
||||
而在读已提交(read committed)的隔离级别下,则只会针对index record来进行加锁,并不会添加间隙锁。
|
||||
|
||||
> ##### supremum preseudo-record
|
||||
> 该index record并不是一条真实存在的索引记录,其代表了`比索引中所有记录的值都大的一条虚拟记录`,类似有序链表中的尾节点。
|
||||
|
||||
##### select语句命中非unique索引
|
||||
当执行的select语句,其where条件命中非unique索引,`那么,其会针对命中索引以及主键索引都进行加锁`。
|
||||
|
||||
例如,`select * from t_learn_lock where lv between 4 and 8 for update;`,该语句会针对`idx_lv`索引记录和主键索引记录进行加锁,加锁状况如下:
|
||||
|
||||
|INDEX_NAME|LOCK_TYPE|LOCK_MODE|LOCK_STATUS|LOCK_DATA|
|
||||
|----------|---------|---------|-----------|---------|
|
||||
||TABLE|IX|GRANTED||
|
||||
|idx_lv|RECORD|X|GRANTED|7, 2|
|
||||
|idx_lv|RECORD|X|GRANTED|9, 3|
|
||||
|PRIMARY|RECORD|X,REC_NOT_GAP|GRANTED|2|
|
||||
|
||||
上述查询语句会命中`(2, 'd', 7)`这条记录,但是,其针对索引加锁的操作如下:
|
||||
- 对于`idx_lv`索引记录,不仅对语句查询出的`(7,2)`这条索引记录加了`next-key`锁,还针对了下一条索引记录`(9,3)`加了`next-key`锁
|
||||
- 对于`primary`主键索引记录,则只针对查询出的id为2的记录进行了加锁`(2)`
|
||||
|
||||
对于`idx_lv`中`(9,3)`这条记录进行加锁,主要是为了防止幻读,当对`(9, 3)`这条记录加锁后,如果后续其他事务想要向lv值为`(7,9)`的范围内进行加锁时,会被间隙锁阻塞,故而保证多次读取结果都一致。
|
||||
|
||||
##### select语句命中主键索引
|
||||
当unique命中主键索引时,则是会针对主键的index record添加记录锁(非间隙锁),示例如下`select * from t_learn_lock where id = 3 for update`,其加锁状况如下:
|
||||
|
||||
|ENGINE_TRANSACTION_ID|LOCK_TYPE|LOCK_MODE|LOCK_STATUS|LOCK_DATA|
|
||||
|---------------------|---------|---------|-----------|---------|
|
||||
|4381865|TABLE|IX|GRANTED||
|
||||
|4381865|RECORD|X,REC_NOT_GAP|GRANTED|3|
|
||||
|
||||
|
||||
#### update ... 加锁情况
|
||||
##### update语句未命中索引的加锁情况
|
||||
类似于`select ... for update`未命中索引的情况,update操作在未命中索引的情况下,`也会针对主键索引的记录进行加锁,并且会获取主键索引中所有记录的锁`。
|
||||
|
||||
其加锁情况和`select ... for update`一样,例如`update t_learn_lock set content = 'fuck' where content = 'm';`,该语句并不会实际修改任何行记录,但是还是会锁住主键中所有的index record。
|
||||
|
||||
|INDEX_NAME|LOCK_TYPE|LOCK_MODE|LOCK_STATUS|LOCK_DATA|
|
||||
|----------|---------|---------|-----------|---------|
|
||||
||TABLE|IX|GRANTED||
|
||||
|PRIMARY|RECORD|X|GRANTED|supremum pseudo-record|
|
||||
|PRIMARY|RECORD|X|GRANTED|1|
|
||||
|PRIMARY|RECORD|X|GRANTED|2|
|
||||
|PRIMARY|RECORD|X|GRANTED|3|
|
||||
|PRIMARY|RECORD|X|GRANTED|4|
|
||||
|
||||
##### update语句命中非unique索引
|
||||
类似于`select ... for update`命中非unique索引一样,update语句在命中非unique索引之后,`也会同时针对命中索引和主键索引进行加锁`。
|
||||
|
||||
例如,在`update t_learn_lock set content = 'fuck' where lv between 4 and 8;`该更新语句命中`idx_lv`索引之后,既针对`idx_lv`索引进行了加锁,又针对`primary`主键索引进行了加锁。
|
||||
|
||||
同样和select相似的是,该update语句只会修改`(2, 'd', 7)`记录,但是update语句仍然获取了`(3, 'f', 9)`记录的锁。
|
||||
|
||||
update语句获取锁的情况如下:
|
||||
|
||||
|INDEX_NAME|LOCK_TYPE|LOCK_MODE|LOCK_STATUS|LOCK_DATA|
|
||||
|----------|---------|---------|-----------|---------|
|
||||
||TABLE|IX|GRANTED||
|
||||
|idx_lv|RECORD|X|GRANTED|7, 2|
|
||||
|idx_lv|RECORD|X|GRANTED|9, 3|
|
||||
|PRIMARY|RECORD|X,REC_NOT_GAP|GRANTED|3|
|
||||
|PRIMARY|RECORD|X,REC_NOT_GAP|GRANTED|2|
|
||||
|
||||
类似`select ... for update`,update语句针对非unique索引进行了`next-key`加锁。
|
||||
|
||||
##### update语句命中主键索引
|
||||
如果`update ...`语句命中主键索引,那么类似select语句,也同样只会针对索引记录添加记录所(非间隙)
|
||||
|
||||
`update t_learn_lock set content = 'fuck' where id= 3;`语句加锁情况如下:
|
||||
|
||||
|INDEX_NAME|LOCK_TYPE|LOCK_MODE|LOCK_STATUS|LOCK_DATA|
|
||||
|----------|---------|---------|-----------|---------|
|
||||
||TABLE|IX|GRANTED||
|
||||
|PRIMARY|RECORD|X,REC_NOT_GAP|GRANTED|3|
|
||||
|
||||
### innodb死锁
|
||||
当innodb中多个事务持有各自的资源并同时请求对方所持有的资源时,就会发生死锁。当发生死锁时,事务获取锁的请求会被阻塞,阻塞超时后,会根据诗剧苦设置决定是否回滚(`innodb_rollback_on_timeout`)。
|
||||
|
||||
#### 超时
|
||||
在innodb中,可以通过如下变量来控制innodb的超时时间和超时后行为
|
||||
- innodb_lock_wait_timeout: 超时等待时间,默认为`50`,即超时时间限制为50s
|
||||
- innodb_rollback_on_timeout: 在等待超时后,对事务是否进行回滚,默认为`OFF`,代表不回滚
|
||||
|
||||
#### wait-for graph
|
||||
相较于被动等待持有锁的事务超时,wait-for graph是一种更加主动的死锁检测方式。
|
||||
|
||||
在wait-for graph中,`节点`代表`事务`,而节点指向另一个节点的有向边则代表事务事务之间的等待关系。
|
||||
|
||||
wait-for graph中,事务`t1`指向事务`t2`的有向边代表如下两种可能的场景:
|
||||
- 事务t1所等待的资源正在被事务t2占用
|
||||
- 事务t1和事务t2都在等待相同的资源,但是在等待队列中,t2的排序在t1的前面,只有t2获取等待的资源并释放后,t1才能获取
|
||||
|
||||
故而,`在wait-for grah中存在回路时,则代表当前存在死锁。`
|
||||
|
||||
### innodb锁升级
|
||||
在其他数据库中,可能会将多个细粒度锁合并为一个粗粒度锁,从而减少锁占用的资源,例如将1000个行锁合并为一个页锁。
|
||||
|
||||
但是,`innodb`中并不存在锁升级问题。innodb中,锁是按页进行管理的,通过位图(bitmap)来对锁进行管理,页中不论有多少条记录进行了加锁,锁占用的资源都是相同的。
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
80
mysql/mysql集群/CAP Theorem in DBMS.md
Normal file
80
mysql/mysql集群/CAP Theorem in DBMS.md
Normal file
@@ -0,0 +1,80 @@
|
||||
- [CAP Theorem in DBMS](#cap-theorem-in-dbms)
|
||||
- [What is the CAP Theorem](#what-is-the-cap-theorem)
|
||||
- [Consistency](#consistency)
|
||||
- [Availability](#availability)
|
||||
- [Partition Tolerance](#partition-tolerance)
|
||||
- [The Trade-Offs in the CAP Theorem](#the-trade-offs-in-the-cap-theorem)
|
||||
- [CA(Consistency and Availability)](#caconsistency-and-availability)
|
||||
- [AP(Availability and Partition Tolerance)](#apavailability-and-partition-tolerance)
|
||||
- [CP(Consistency and Partition Tolerance)](#cpconsistency-and-partition-tolerance)
|
||||
|
||||
# CAP Theorem in DBMS
|
||||
在网络共享数据系统设计中固有的权衡令构建一个可靠且高效的系统十分困难。CAP理论是理解分布式系统中这些权衡的核心基础。CAP理论强调了系统设计者在处理distributed data replication时的局限性。CAP理论指出,在分布式系统中,只能同时满足`一致性、可用性、分区容错`这三个特性中的两种。
|
||||
|
||||
> CAP理论中,对分布式系统提出了如下三个特性:
|
||||
> - consistency(一致性)
|
||||
> - availability(可用性)
|
||||
> - partition tolerance(分区容错)
|
||||
|
||||
由于该底层限制,开发者必须根据其应用的需要谨慎的平衡这些属性。设计者必须决定优先考虑哪些特性,从而获取最佳的性能和系统可靠性。
|
||||
|
||||
## What is the CAP Theorem
|
||||
CAP理论是分布式系统的基础概念,其指出分布式系统中所有的三个特性无法被同时满足。
|
||||
|
||||
### Consistency
|
||||
`Consistency`代表network中所有的节点都包含相同的replicated data拷贝,而这些数据对不同事务可见。其保证分布式居群中所有节点返回相同、最新的数据。其代表所有client对数据的视角都相同。存在许多不同类型的一致性模型,而CAP中的一致性代指的是顺序一致性,是一种非常强的一致性形式。
|
||||
|
||||
`ACID`和`CAP`中都包含一致性,但是这两种一致性所有不同:
|
||||
- 在`ACID`中,其代表事务不能破坏数据库的完整性约束
|
||||
- 在`CAP`中,其代表分布式系统中相同数据项在不同副本中的一致性
|
||||
|
||||
### Availability
|
||||
Availability代表每个对数据项的read/write request要么能被成功处理,要么能收到一个操作无法被完成的响应。每个non-failing节点都会为所有读写请求在合理的时间范围内生成响应。
|
||||
|
||||
其中,“每个节点”代表,即使发生network partition,节点只要不处于failing状态,无论位于network partition的哪一侧,都应该能在合理的时间范围内返回响应。
|
||||
|
||||
### Partition Tolerance
|
||||
partition tolerance代表在连接节点的网络发生错误、造成两个或多个分区时,系统仍然能够继续进行操作。通常,在出现network partition时,每个partition中的节点只能彼此互相沟通,跨分区的节点通信被阻断。
|
||||
|
||||
这意味着,即使发生network partition,系统仍然能持续运行并保证其一致性。network partition是不可避免地,在网络分区恢复正常后,拥有partition tolerance的分布式系统能够优雅的从分区状态中恢复。
|
||||
|
||||
CAP理论指出分布式数据库最多只能兼顾如下三个特性中的两种:consistency, availability, partition tolerance。
|
||||
|
||||
<img src="https://media.geeksforgeeks.org/wp-content/uploads/20240808172250/venndiagram.png" alt="CAP - venndiagram" width="390" height="321" srcset="https://media.geeksforgeeks.org/wp-content/uploads/20240808172250/venndiagram.png 390w,https://media.geeksforgeeks.org/wp-content/uploads/20240808172250/venndiagram-100.png 100w,https://media.geeksforgeeks.org/wp-content/uploads/20240808172250/venndiagram-200.png 200w,https://media.geeksforgeeks.org/wp-content/uploads/20240808172250/venndiagram-300.png 300w" loading="lazy">
|
||||
|
||||
## The Trade-Offs in the CAP Theorem
|
||||
CAP理论表明分布式系统中只能同时满足三种特性中的两种。
|
||||
### CA(Consistency and Availability)
|
||||
> 对于CA类型的系统,其总是可以接收来源于用户的查询和修改数据请求,并且分布式网络中所有database nodes都会返回相同的响应
|
||||
|
||||
然而,该种类似的分布式系统在现实世界中不可能存在,因为在network failure发生时,仅有如下两种选项:
|
||||
- 发送network failure发生前复制的old data
|
||||
- 不允许用户访问old data
|
||||
|
||||
如果我们选择第一个选项,那么系统将满足Availibility;如果选择第二个选项,系统则是Consistent。
|
||||
|
||||
`在分布式系统中,consistency和availability的组合是不可能的`。为了实现`CA`,系统必须是单体架构,当用户更新系统状态时,所有其他用户都能访问到该更新,这将代表系统满足一致性;而在单体架构中,所有用户都连接到一个节点上,这代表其是可用的。`CA`系统通常不被青睐,因为实现分布式计算必须牺牲consistency或availability中的一种,并将剩余的和partition tolerance组合,即`CP/AP`系统。
|
||||
|
||||
> CAP中的A(Availability)只是要求非failing状态下的节点能够在合理的时间范围内返回响应,故而单体架构可以满足`Availability`。
|
||||
>
|
||||
> 即使单体架构可能因单点故障导致系统不可用,不满足`Reliable`(可靠性),并不影响其满足`Availability`(可用性)。
|
||||
|
||||
<img src="https://media.geeksforgeeks.org/wp-content/uploads/20240813184051/cap.png" alt="CA (Consistency and Availability)" width="631" height="579" srcset="https://media.geeksforgeeks.org/wp-content/uploads/20240813184051/cap.png 631w,https://media.geeksforgeeks.org/wp-content/uploads/20240813184051/cap-100.png 100w,https://media.geeksforgeeks.org/wp-content/uploads/20240813184051/cap-200.png 200w,https://media.geeksforgeeks.org/wp-content/uploads/20240813184051/cap-300.png 300w" loading="lazy">
|
||||
|
||||
### AP(Availability and Partition Tolerance)
|
||||
> 这种类型的系统本质上是分布式的,确保即使在network partition场景下,用户发送的针对database nodes中数据的查看和修改请求不会被丢失
|
||||
|
||||
该系统优先考虑了Availability而非Consistency,并且可能会返回过期的数据。一些技术failure可能会导致partition,故而过期数据则是代表partition产生前被同步的数据。
|
||||
|
||||
AP系统通常在构建社交媒体网站如Facebook和在线内容网站如YouTube时使用,其并不要求一致性。对于使用AP系统的场景,相比于不一致,不可用会造成更大的问题。
|
||||
|
||||
AP系统是分布式的,可以分布于多个节点,即使在network partition发生的前提下也能够可靠运行。
|
||||
|
||||
### CP(Consistency and Partition Tolerance)
|
||||
> 该类系统本质上是分布式的,确保由用户发起的针对database nodes中数据进行查看或修改的请求,在存在network partition的场景下,会直接被丢弃,而不是返回不一致的数据
|
||||
|
||||
`CP`系统优先考虑了Consistency而非Availability,如果发生network partition,其不允许用户从replica读取`在network partition发生前同步的数据`。对于部分应用程序来说,相比于可用性,其更强调数据的一致性,例如股票交易系统、订票系统、银行系统等)
|
||||
|
||||
例如,在订票系统中,还剩余一个可订购座位。在该CP系统中,将会创建数据库的副本,并且将副本发送给系统中其他的节点。此时,如果发生网络问题,那么连接到partitioned node的用户将会从replica获取数据。此时,其他连接到unpartitioned部分的用户则可以对剩余的作为进行预定。这样,在连接到partitioned node的用户视角中,仍然存在一个seat,其将导致数据不一致。
|
||||
|
||||
在上述场景下,CP系统通常会令其系统对`连接到partitioned node的用户`不可用。
|
||||
345
mysql/mysql集群/Mysql Group Replication.md
Normal file
345
mysql/mysql集群/Mysql Group Replication.md
Normal file
@@ -0,0 +1,345 @@
|
||||
- [Mysql Group Replication](#mysql-group-replication)
|
||||
- [Group Replication Background](#group-replication-background)
|
||||
- [Replication Technologies](#replication-technologies)
|
||||
- [group replication](#group-replication)
|
||||
- [Group replication Use Cases](#group-replication-use-cases)
|
||||
- [Exmaple Use Cases](#exmaple-use-cases)
|
||||
- [Multi-primary and Single-Primary Modes](#multi-primary-and-single-primary-modes)
|
||||
- [Single-Primary Mode](#single-primary-mode)
|
||||
- [group\_replication\_enforce\_update\_everywhere\_checks](#group_replication_enforce_update_everywhere_checks)
|
||||
- [group\_replication\_consistency](#group_replication_consistency)
|
||||
- [Primary Election Algorithm](#primary-election-algorithm)
|
||||
- [Finding the Primary](#finding-the-primary)
|
||||
- [Multi-Primary Mode](#multi-primary-mode)
|
||||
- [Transaction checks](#transaction-checks)
|
||||
- [Data Definition Statements](#data-definition-statements)
|
||||
- [Version Compatibility](#version-compatibility)
|
||||
- [Group Membership](#group-membership)
|
||||
- [group\_replication\_member\_expel\_timeout](#group_replication_member_expel_timeout)
|
||||
- [Failure detection](#failure-detection)
|
||||
- [Fault-tolerance](#fault-tolerance)
|
||||
- [Observability](#observability)
|
||||
- [group replication plugin architecture](#group-replication-plugin-architecture)
|
||||
- [APIs for Capture, Apply, Lifecycle](#apis-for-capture-apply-lifecycle)
|
||||
- [components](#components)
|
||||
- [protocol](#protocol)
|
||||
- [GCS API/XCom](#gcs-apixcom)
|
||||
- [Getting Started](#getting-started)
|
||||
- [Deploying Group Replication in Single-Primary Mode](#deploying-group-replication-in-single-primary-mode)
|
||||
- [Deploying Instances for Group Replication](#deploying-instances-for-group-replication)
|
||||
|
||||
|
||||
# Mysql Group Replication
|
||||
`Mysql Group Replication`可以用于创建弹性、高可用、有容错的replication拓扑。
|
||||
|
||||
groups可以在single-primary mode下进行执行,其支持自动的primary选举,在同一时刻只有一个server接收更新操作。
|
||||
|
||||
另外,groups也支持multi-primary mode部署,此时所有servers都能够接收写操作,即使多个写请求是并行发送的。
|
||||
|
||||
MGR存在一个内置的group membership service,其在任一时刻都能为所有servers提供一致且可用的组视图。server可以加入和离开group,group view也会随之改变。在某些时刻,server可能非预期的离开group,在这种情况下failure detection机制将会自动感知并通知group去更新group view。
|
||||
|
||||
group replication是作为mysql server的插件被提供的。可以通过Innodb Cluster来部署`mysql server instances group`。
|
||||
|
||||
> 为了部署多个mysql实例,可以使用Innodb Cluster,其能够通过Mysql Shell轻松的管理mysql server实例组。Innodb Cluster和Mysql Router无缝集成,在应用连接到集群式,无需在应用中编写failover流程。
|
||||
|
||||
## Group Replication Background
|
||||
创建有容错性系统的最常用方式是`采用冗余组件的方案`,即使组件被移除,系统仍可按预期继续运行。这样将会增加系统的复杂性,特别的,`replicated databases`必须维护和管理多台server;并且,servers以集群的方式对外提供服务,还需要处理其他若干的经典分布式系统问题:如network partition或split brain问题。
|
||||
|
||||
mysql group replication提供了distributed state machine replication,并保证了servers间的强协同;server在作为同一group的一部分,会自动的彼此之间进行协调。
|
||||
|
||||
对于待提交的事务,majority of group必须就`事务在global transactions中的顺序`达成一致。决定事务commit或abort是由每个server单独处理的,但是所有server中,该事务提交/回滚状态都完全一致。如果存在network partition,导致members不能就transaction order达成一致,那么系统将不能继续处理,直至该问题被解决。故而,MGR中存在内置、自动的split-brain保护机制。
|
||||
|
||||
上述所有内容都由`GCS`(Group Communication System)协议提供,其提供了如下内容
|
||||
- 故障检测机制
|
||||
- group membership service
|
||||
- safe and completely ordered message delivery
|
||||
|
||||
上述所有机制都保证了数据在group of servers之间能够被一致的复制。GCS技术的核心基于`Paxos`算法实现,其用作group communication engine。
|
||||
|
||||
### Replication Technologies
|
||||
传统的mysql replication提供了一种简单的`source to replica`的复制方法:
|
||||
- source为primary实例
|
||||
- replica为secondaries实例
|
||||
|
||||
source将会应用事务,并对事务进行提交。之后,事务会异步的被发送到replicas,接收到事务的replica会对事务进行重新执行(statement-based replicaion)或被应用(row-based replication)。在默认情况下,所有的server都拥有数据的完整副本。
|
||||
|
||||
默认情况下,mysql的异步replication示例图如下:
|
||||
|
||||
<img src="https://dev.mysql.com/doc/refman/8.4/en/images/async-replication-diagram.png" style="width: 100%; max-width: 725px;">
|
||||
|
||||
除此之外,还存在半同步的replication,其在协议中添加了同步步骤。示例如下所示,primary会等待replicas接收到事务并向primary返回ack,在接收到ack后才会执行commit操作。
|
||||
<img src="https://dev.mysql.com/doc/refman/8.4/en/images/semisync-replication-diagram.png" style="width: 100%; max-width: 725px;">
|
||||
|
||||
上面两张图为传统的asynchronous MYSQL replication protocol。
|
||||
|
||||
### group replication
|
||||
group replication是一种用于实现fault-tolerant系统的技术。replication group是一系列servers的集合,其中每个server都含有数据的完整副本,servers之间通过message进行交互。
|
||||
|
||||
group中的communication layer提供了一系列保证,例如atomic message和total order message delivery。
|
||||
|
||||
Mysql Group Replication基于这些特性、抽象,实现了`multi-source update everywhere replication protocol`。一个replication group由多个server组成,每个server在每时每刻都独立的执行事务。
|
||||
- read/write事务都只在其被group同意之后才提交;即对于read/write事务,都要由整个group决定其是否能被提交,故而提交操作并不是由源服务器单方面决定的。(read/write transaction代表事务中包含写操作的事务)。
|
||||
- read-only事务则是不需要group内其他server的协助,立刻就能提交
|
||||
|
||||
当`read/write`事务准备好在源server上提交时,都会原子性的广播`write values`和`write set`:
|
||||
- write values:代表被修改后的整行数据
|
||||
- write set:被修改数据的唯一标识符
|
||||
|
||||
由于transaction是被原子广播的,故而,要么`group中所有servers都接收到该事务`,要么`group中所有servers都未收到该事务`。并且,`对于所有servers,其接收到的所有事务都相同,并且所有事务按照相同的顺序被接收`,并且,会为所有事务建立一个`global total order`。
|
||||
|
||||
但是,事务在不同的servers上并行执行时,可能会发生冲突。在被称为`certification`的过程中,该冲突通过查看和比较两个不同并行事务的write sets来检测。在certification的过程中,冲突检测在行级别执行:如果两个并行的事务在不同的servers上执行,并且修改了相同的行,那么这将会存在冲突。
|
||||
|
||||
该冲突将会按照如下方案被解决:对于在global total order中排序靠前的事务,其将会在所有server上被提交;而排序靠后的事务则是会终止,在源服务器上被提交,并且被group中的其他servers丢弃。
|
||||
|
||||
例如,t1和t2在不同的servers上并发执行,两者都修改了同一行,t2的排序在t1之前,那么在冲突中t2将会获胜而t1会回滚。上述规则则是分布式的`first commit wins rule`。
|
||||
|
||||
> 如果两个事务发生冲突的可能性高于不冲突的可能性,那么最好让其在相同的server上执行,并且通过server的local lock机制来对事务进行同步;而不是在不同的server上执行,并因certification造成回滚。
|
||||
|
||||
对于已经被认证的事务进行`applying`和`externalizing`,group replicaion允许server`在不破坏一致性和有效性的前提下`偏离`agreed order of the transactions`。`group replication`是一个最终一致性的系统,代表incoming traffic降低或停止时,所有的group members都将包含相同的data content。当traffic较高时,事务可以以稍微不同的顺序进行externalizing,并且,事务在进行externalizing时,在一些servers进行externalizing的时机也可能早于其他servers。(`externalizing`指的即是实际提交事务并使事务的修改对client可见;`applying`指的是server应用来自其他server的事务修改)。
|
||||
|
||||
例如,在mulit-primary模式下,一个本地事务可能在certification之后立马就被externalized,即使一个来自remote server的transaction在global order中顺序更靠前并且尚未被applied。当certification过程中,多个transactions间不涉及冲突时,该操作是被允许的。
|
||||
|
||||
在single-primary模式下,在primary server上,可能小概率会发生`并发、非冲突的本地事务按照和global order agreed by group replication的顺序进行提交和externalized`。
|
||||
|
||||
在不接受来自客户端写入操作的secondaries节点上,事务总是按照agreed order被提交和externalized。
|
||||
|
||||
具体mysql group replication protocol的示意图如下所示:
|
||||
<img src="https://dev.mysql.com/doc/refman/8.4/en/images/gr-replication-diagram.png" style="width: 100%; max-width: 725px;">
|
||||
|
||||
### Group replication Use Cases
|
||||
Group Replication通过将系统状态复制到一组servers,能够创建一个有冗余性的容错系统。即使部分服务器随后fail,只要不是所有或majority发生故障,那么系统仍然可访问。
|
||||
|
||||
取决于group中故障server的数量不同,group可能出现性能下降或可拓展性降低的情况,但是整个系统仍然是可用的。server的故障是隔离和独立的,故障由group membership service来进行追踪,其基于分布式的failure detector,该detector能够在任何server自愿退出或异常退出group时发送信号。
|
||||
|
||||
在该系统中,无需进行server failover,且`multi-source update everywhere`的特性确保在单台server故障的场景下,update操作的执行仍然不会受阻。总而言之,mysql group replication保证了数据库服务是持续可用的。
|
||||
|
||||
需要认识到的是,即使数据库服务整体是可用的,当server非预期的退出时,连接到该故障server的clients仍然需要被重定向或故障转移到其他server。该处理流程不应由group replication来处理,而是应该由connector/load balancer/router/其他中间件来处理。
|
||||
|
||||
总之,mysql group replication提供了一个高可用、高弹性、可靠的mysql服务。
|
||||
|
||||
#### Exmaple Use Cases
|
||||
如下示例为group replication的典型用例场景:
|
||||
- `Elastic Replication`: 适用于高灵活度复制结构的环境,servers的数量需要可以动态的增加或缩减,并尽可能的减少副作用
|
||||
- `Highly Available Shards`: sharding是一种实现`write scale-out`的流行方案,可以使用mysql group replication来实现高可用shards,其中每个shard都被映射到一个replication group
|
||||
- `async source-replica replication的替代方案`: 在某些场景下,使用单个source server可能会造成单点争用,向整个group写入在特定场景下则更具有可拓展性(MGR支持多主模式部署,能够在group范围内缓解写压力)
|
||||
|
||||
### Multi-primary and Single-Primary Modes
|
||||
Group Replication可以在single-primary或multi-primary模式上工作。该group mode为一个group范围内的配置设置,通过system variable `group_replication_single_primary_mode`来指定,在所有group members中该variable必须都相同。
|
||||
- `ON`: 该值代表single-primary mode,也为变量的默认值
|
||||
- `OFF`: 该值代表multi-primary mode
|
||||
|
||||
> 在group中,所有的members其`group_replication_single_primary_mode`变量的值都必须相同
|
||||
|
||||
在group replication运行时,并无法手动修改`group_replication_single_primary_mode`的值,但是,在group replication运行时,可以使用`group_replication_switch_to_single_primary_mode()`和`group_replication_switch_to_multi_primary_mode()`方法来将group从一个模式切换到另一个模式。这些方法能够修改group的mode,并且确保数据的一致性和安全性。
|
||||
|
||||
不管处于哪种mode下,group replciation都不会处理client的failover,其必须由中间件framework例如`Mysql Router`/`proxy`/`connector`/`应用本身`来处理。
|
||||
|
||||
#### Single-Primary Mode
|
||||
在single-primary mode(group_replication_single_primary_mode=ON)时,group中只存在一个`primary server`(被设置为read/write mode)。group中所有其他的members都被设置为read-only mode(super_read_only=ON)。该primary server通常会引导整个集群的启动,所有其他加入到group中的servers都会识别到primary server并且自动被设置为read-only mode.
|
||||
|
||||
在single-primary mode时,group replication会强制只有一个server能够向group中写入,相比于multi-primary mode,single-primary mode的数据一致性检查可以不那么严格,DDL statements也无需额外谨慎的处理。
|
||||
|
||||
##### group_replication_enforce_update_everywhere_checks
|
||||
`group_replication_enforce_update_everywhere_checks`选项能够启用/禁用`对group的严格一致性检查`。当在single-primary mode下部署时,或将group的mode修改为single-primary mode时,该system variable的值必须被设置为`OFF`。
|
||||
|
||||
被指定为primary server的member可以按如下方式进行变更:
|
||||
- 如果该primary正常/非预期退出group,那么新primary的选举将会自动被触发
|
||||
- 可以通过`group_replication_set_as_primary()`方法将指定的member任命为primary
|
||||
- 如果使用`group_replication_switch_to_single_primary_mode()`方法将group从multi-primary mode切换为single-primary mode,那么将会自动选举出一个新的primary,或者可以通过方法指定primary
|
||||
|
||||
当new primary server被选举出后(自动或手动),岂会被自动设置为read/write状态;并且其他的group members将会变为`secondaries`,状态变为`read-only`,具体流程如下图所示:
|
||||
|
||||
<img src="https://dev.mysql.com/doc/refman/8.4/en/images/single-primary-election.png" style="width: 100%; max-width: 899px;">
|
||||
|
||||
当新primary被选中时,可能会存在部分变更的积压,这些变更已经在old primary中被应用,但是还没有被应用到new primary中。在这种情况下,直到new primary追上old primary之前,read/write transaction可能造成冲突并被回滚,而read-only transaction则可能会读取过时的数据。
|
||||
|
||||
通过group replication flow control mechanism,将会将fast member和slow member的差异最小化,在该机制被激活并经合理调优的情况下,可以有效降低该上述情况的发生概率。
|
||||
|
||||
##### group_replication_consistency
|
||||
可以通过该system variable来设置group的事务一致性级别,从而解决上述问题。
|
||||
- `BEFORE_ON_PRIMARY_FAILOVER`: 该值为默认值
|
||||
- 将该变量设置为`BEFORE_ON_PRIMARY_FAILOVER`或更高级别时,新选举出的primary将会暂时持有新事务,直到primary完成积压changes的applying
|
||||
|
||||
##### Primary Election Algorithm
|
||||
自动的primary member选举过程涉及每个member:每个member都会查看group的new view,对潜在的new primary members进行排序,并且选择最合适的member。在每个member的决策过程中,都会独立的在本地进行决策,决策遵循mysql server的primary election algorithm。
|
||||
> 由于所有的members都必须就决策达成一致,如果其他group member运行的mysql server版本较低时,members会调整其primary election algorithm,令其行为和group内最低版本的mysql server保持一致。
|
||||
|
||||
在members选举primary时,会考虑如下因素,考虑顺序如下:
|
||||
- 首先考虑的因素为`哪个member运行最低的mysql server version`,所有group members都会首先按照mysql server version进行排序
|
||||
- 如果存在多个members都运行最低的mysql server version,那么第二个考虑的因素为members中每个member的member weight,member weight由`group_replication_member_weight`进行指定
|
||||
- `group_replication_member_weight` system variable通过一个范围为`0-100`的数字进行指定,所有members的weight默认为50,该值设置越小,排序越靠后,值设置越大,排序越靠前
|
||||
- 通过weighting function可以令硬件更好的member优先级更高,或在primary计划维护时转移至特定的节点
|
||||
- 如果存在多个members运行相同的mysql server version,并且多个members都拥有最高的member weight,那么考虑的第三个因素则是每个member UUID的字典序,UUID通过`server_uuid` system variable来指定。拥有最小server UUID的member将会被选中为primary
|
||||
|
||||
##### Finding the Primary
|
||||
在部署在single-primary mode下时,如果要找出哪个server为当前的primary,可以通过`performance_schema.replication_group_members`表中的`MEMBER_ROLE`来进行判断,示例如下:
|
||||
```sql
|
||||
mysql> SELECT MEMBER_HOST, MEMBER_ROLE FROM performance_schema.replication_group_members;
|
||||
+-------------------------+-------------+
|
||||
| MEMBER_HOST | MEMBER_ROLE |
|
||||
+-------------------------+-------------+
|
||||
| remote1.example.com | PRIMARY |
|
||||
| remote2.example.com | SECONDARY |
|
||||
| remote3.example.com | SECONDARY |
|
||||
+-------------------------+-------------+
|
||||
```
|
||||
|
||||
#### Multi-Primary Mode
|
||||
在multi-primary mode(group_replication_single_primary_mode=OFF)下,没有member拥有特殊的role。任何member在加入group时都被设置为read/write模式,即使在并发情况下也能执行write transaction。
|
||||
|
||||
如果一个member停止接收write transactions,例如发生非预期的server exit,连接到member的clients将会被重定向或failover到其他的任一处于read/write模式的member。group replciation本身并不会处理client-side failover,可以使用中间件来处理,例如mysql router。
|
||||
|
||||
<img src="https://dev.mysql.com/doc/refman/8.4/en/images/multi-primary.png" style="width: 100%; max-width: 899px;">
|
||||
|
||||
group replication是一个最终一致性的系统,其代表当incoming traffic降低或停止时,所有group members都会包含相同的数据内容。当traffic流动时,某些members对transactions进行externalized的时机可能位于另一些members之前。尤其是某些member的写吞吐可能低于其他member,导致连接到该member的client可能读取到过时数据。
|
||||
|
||||
在multi-primary mode下,较慢的member也可能会积累过多的待认证事务和待applying的事务,导致冲突和认证失败的风险更高。为了限制该问题,可以采用group replication的flow control mechansim机制来最小化fast member和slow member的差异。
|
||||
|
||||
如果想要为group中的每个事务提供一致性保证,可以使用`group_replication_consistency`系统变量实现。可以根据group中工作负载和数据读写的优先级来选择合适的设置,同时应考虑提升一致性所引入的同步操作对性能带来的影响。可以针对独立的session来设置该系统变量,从而保护对并发性敏感的事务。
|
||||
|
||||
##### Transaction checks
|
||||
当group在multi-primary mode下部署时,事务将会被校验,确保其和该模式兼容。如下strict consistency checks将会在multi-primary部署模式下被校验:
|
||||
- 如果事务在SERIALIZABLE的隔离级别下被执行,那么在`synchronizing itself with the group`时(指通过原子广播向group发送write set和write values时),提交操作将会失败
|
||||
- 如果事务执行时目标table拥有级联约束,那么在`synchronizing itself with the group`时,其提交将会失败
|
||||
- 如果在MGR中允许级联,在不同的members中,级联操作影响的行可能有不同,这样会影响members中数据的一致性
|
||||
|
||||
上述校验通过`group_replication_enforce_update_everywhere_checks`的system variable来进行控制。在multi-primary mode的场景下,system variable应该被设置为`ON`,但是该校验可以被关闭,通过将system variable设置为`OFF`。当在single-primary模式下部署时,system variable必须被设置为`OFF`
|
||||
|
||||
##### Data Definition Statements
|
||||
在Group Replication在multi-primary模式下的拓扑中,需要额外关注DDL的执行场景。
|
||||
|
||||
mysql 8.4支持atomic ddl,ddl语句的执行要么提交、要么删除,类似一个原子的事务。对于DDL语句,无论其是否是原子的,都会隐式的终止当前session中活跃的事务,如同在执行ddl statements之前执行了`COMMIT`。这将代表,DDL statements无法在另一事务中执行,也无法在`start transaction ... commit`中执行,更无法将多个ddl statements整合在一个事务中执行。
|
||||
|
||||
Group Replication基于的是乐观的replication范式,statements首先会被乐观的执行,并且在后续有需要时回滚。每个server在执行statements之前,不会先确保在group内达成共识。故而,在multi-primary mode下,对DDL进行复制需要更加注意。
|
||||
|
||||
`如果在执行DDL对schema进行变更时,也对同一张表的数据执行了DML变更,那么在DDL变更被完成并复制到所有members之前,需要确保DML操作在同一个server上被处理。`
|
||||
|
||||
如果没有确保上述行为,那么可能会导致数据的不一致性,从而导致操作被中断或被部分完成。
|
||||
|
||||
但是,当group通过single-primary模式被部署时,该问题则不会发生,所有修改操作都会被发送到同一server,即primary server。
|
||||
|
||||
##### Version Compatibility
|
||||
为了实现最佳兼容性和性能,group中的所有members都应当运行在相同的mysql server版本下,从而保证相同版本的group replication。在multi-primary mode下,该要求将更加重要,因为所有加入到group的members都处于read/write模式。如果group中的成员运行了多个mysql版本,则存在部分成员和其他成员不兼容的风险,不同版本成员之间支持的方法可能有所不同。
|
||||
|
||||
为了防范上述问题,当新member加入到group时(包含之前的member被升级或重启的场景),该member将会对group中的其他members进行兼容性检查。
|
||||
|
||||
在multi-primary mode下,兼容性检查的结果尤为重要。如果新加入member运行的mysql server版本比`当前group中member最小的版本号`更高,那么其将以read-only的模式加入到group中。
|
||||
|
||||
如果group运行在multi-primary mode下,使用了不同的mysql server versions,group replication将自动管理memebers的read/write和read-only状态。如果member离开group,那么离开后`当前运行最低版本的members`将会自动被设置为read/write模式。
|
||||
|
||||
如果将运行在single-primary mode下的group修改为在multi-primary下运行,那么可以使用`group_replication_switch_to_multi_primary_mode()`方法,group replication将会自动将members设置为正确的模式。并且,对于运行的版本比lowest-version高的members,其状态将会被自动设置为read-only,而运行lowest-version则将会被设置为read/write状态。
|
||||
|
||||
### Group Membership
|
||||
在mysql group replication中,多个servers组成了replication group。group拥有UUID形式的名称。group是动态的,server可以在任何时刻加入/离开group。在server加入或离开group时,group能够对自身进行调整。
|
||||
|
||||
如果server加入到group,其会自动从现存的server处拉取最新的状态,令自身保持最新状态。如果server离开group(例如因维护而下线),那么剩余的servers将会监测到server已经离开,并且自动对group进行reconfigure。
|
||||
|
||||
group replication中拥有一个group membership service,其定义了哪些servers处于online状态并加入到group。online servers的列表被称为`view`。group中每个server都拥有一致的view,能够获知给定时刻哪些servers作为group中的members是活跃的。
|
||||
|
||||
group members不仅需要对事务的提交达成一致,也需要对当前的view达成一致。如果现有members认同一个新的server应该成为group的一部分,那么group将被reconfigure,并将该server纳入group,这也将触发view的变更。如果server离开group,不管是主动还是非预期退出,group将会动态调整配置并且触发view的变更。
|
||||
|
||||
如果member自愿离开group,那么其首先会触发动态的group reconfiguration,在此期间所有的members都应该就`new view without the leaving server`达成一致。
|
||||
|
||||
但是,如果member非预期的退出group(例如因网络连接中断而停止运行),则非预期退出的member无法发起reconfiguration。在该场景下,group replication的failure detection mechanism将会在member离开的短暂时间期间之后检测到该退出行为,并且提出`a reconfiguration of the group without the failed member`。
|
||||
|
||||
当member主动离开group时,reconfiguration需要majority of servers达成一致,但是如果该group无法达成一致(例如因为网络分区导致在线servers数量没有达到majority),那么系统将无法动态的更改configuration;为了防止split-brain场景,系统会处于阻塞状态。对于该种情况,需要管理员的干预。
|
||||
|
||||
对于member而言,下线一小段时间,并且在failure detection mechanism探知到该failure前尝试重新加入group是可能的,此时group也不会reconfigre并移除该member。在该场景下,重新加入group的member将会忘记之前的状态,`但是,若存在其他members发送给其崩溃前状态的消息`,这将可能会造成数据不一致的问题。
|
||||
|
||||
为了检测这种情况,MGR会检测相同server的新实例(server address和port号相同)想要加入到group,而server的旧实例也被作为member的场景。那么,在这种情况下,新实例的加入操作将会被阻塞,直到旧实例可以通过reconfiguration被移除。
|
||||
|
||||
##### group_replication_member_expel_timeout
|
||||
通过`group_replication_member_expel_timeout`的system variable,引入了一个waiting period,允许members该时间内重新连接到group,从而避免被从group中驱逐。如果member在该时间范围内重新连接到了group,那么该member可以重新在group中处于活跃状态。但是,当member超过expel timeout并且从group中被驱逐时,或通过`STOP GROUP_REPLICATION`语句停止group replication时,或server failure时,其必须作为新的实例加入到group中。
|
||||
|
||||
### Failure detection
|
||||
group replication failure detection mechanism是一个分布式的service,用于检测group中server没有和其他servers沟通的情况,从而判定该server有停止服务的嫌疑。如果在group的共识中该嫌疑为true,那么group将会做出协调一致的决定,将该member逐出group。
|
||||
|
||||
将不参与communicating的member驱逐出group是必要的,因为group在对transaction或view change需要majority of members达成一致。如果member不参与这些决策,group必须对member进行移除,从而增加group中包含majority of correctly working members的可能性,从而能够继续处理transactions。
|
||||
|
||||
在replication group中,每两个member间都有一个point-to-point的communication channel,从而形成了一个full connected graph。这些连接通过group communication engine(XCom, a Paxos variant)来进行管,连接使用TCP/IP sockets。
|
||||
|
||||
如果member经过5s都没有从另一个member处接收到消息,那么其会假设另一个member已经处于failed状态,并且在其自己的`performance_schema.replication_group_members`中将failed member的状态标记为`UNREACHABLE`。通常来说,两个member都会假定对方failed,因为二者之间没有进行通信。但是,有小概率存在如下场景:member A假定member B处于failed状态,但是member B仍然认为member A正常;这可能是由routing和firewall导致的问题。当member被group的其他members怀疑时,其将假设所有的其他members都已经失败。
|
||||
|
||||
如果怀疑持续超过10s,那么发出怀疑的member将会尝试将`被怀疑member可能已经失败`的观点传播给group中的其他members。如果member实际上和group中的其他members隔离,其会尝试传播其view,但是该传播不会产生任何后果,因为其无法让其view与其他members达成共识。
|
||||
|
||||
`怀疑`只会在满足如下条件时生效:
|
||||
- memeber是notifier
|
||||
- 怀疑持续的时间足够长,可以被传播到group中的其他members
|
||||
- group中其他的members就该怀疑达成一致
|
||||
|
||||
在该种场景下,被怀疑member将会根据协调决策被标记为`从group中移除`的状态,并且在等待`group_replication_member_expel_timeout`后,由移除机制检测到并执行移除操作。
|
||||
|
||||
### Fault-tolerance
|
||||
mysql group replication基于Paxos分布式算法来实现,为servers间提供了分布式协调。故而,其需要`majority of servers`处于活跃状态,从而参与决策。这直接影响了系统可以容忍的`failures`个数。
|
||||
|
||||
> `当server的数量为2*f+1时,其能够容忍f个failures`
|
||||
|
||||
故而,能够影响1个failure的group必须含有3个servers。当一个server失败时,剩下的2个servers仍然能够形成majority(2~3),并允许系统继续执行决策。但是,当故障的server达到两台时,group将会陷入阻塞状态,因无法达成majority的决策条件。
|
||||
|
||||
下表展示了不同group规模下可容忍的failures数量和形成majority的数量
|
||||
| group size | majority | instant failures tolerated |
|
||||
| :-: | :-: | :-: |
|
||||
| 1 | 1 | 0 |
|
||||
| 2 | 2 | 0 |
|
||||
| 3 | 2 | 1 |
|
||||
| 4 | 3 | 1 |
|
||||
| 5 | 3 | 2 |
|
||||
| 6 | 4 | 2 |
|
||||
| 7 | 4 | 3 |
|
||||
|
||||
若group size为n,各指标计算公式如下:
|
||||
- `majority`: `floor(n/2)+1`
|
||||
- `tolerated failures`: `n - floor(n/2) - 1`
|
||||
|
||||
### Observability
|
||||
在group replication plugin中,存在大量的自动功能,但是,有时需要对其进行观测。故而,整个系统的状态(包括view、冲突数据、service状态)可以通过`performance_schema`的表来查询。`replication protocol的分布式特性`以及`servers就元数据和事务达成一致并同步的事实`,使得检查group状态变得更加容易。
|
||||
|
||||
例如,可以通过连接到single server并通过select语句查询performance_schema中对应的表来获取local information和global information。
|
||||
|
||||
### group replication plugin architecture
|
||||
mysql group replication是mysql plugin,其基于现存的mysql replication结构构建,利用了binary log、row-based logging、global transaction identifiers等特性。其和当前mysql的框架做了集成,例如`performance_schema`、plugin和service结构。下图展示了mysql group replication的架构:
|
||||
|
||||
<img src="https://dev.mysql.com/doc/refman/8.4/en/images/gr-plugin-blocks.png" alt="The text following the figure describes the content of the diagram." style="width: 100%; max-width: 391px;">
|
||||
|
||||
#### APIs for Capture, Apply, Lifecycle
|
||||
mysql group replication plugin包含了一系列用于capture、apply、lifecycle的API,其可以用于控制plugin和mysql server的交互。存在令消息在server和plugin之间双向流动的接口,这些接口将mysql server core和group replication plugin进行了隔离,并且接口大部分都是位于事务执行pipeline的hooks。
|
||||
|
||||
在server到plugin的方向,存在如下事件通知:
|
||||
- server starting
|
||||
- server recovering
|
||||
- server being ready to accept connections
|
||||
- server being about to commit a transaction
|
||||
|
||||
在plugin到server的方向,plugin会指示server去执行一些操作,例如`对正在执行中的事务进行提交/回滚`,`将事务放入relay log中排队`
|
||||
|
||||
#### components
|
||||
再group replication plugin的再下一层,则是一系列components,对被路由给其的notification进行响应。
|
||||
- capture component负责对执行事务的上下文进行跟踪
|
||||
- applier component负责再database执行远程事务
|
||||
- recovery component用于管理分布式recovery,并负责将加入到group的server更新到最新
|
||||
|
||||
#### protocol
|
||||
再下一层,则是replciation protocol module,包含了replication protocol的特定逻辑。其处理冲突检测,接收事务并将事务传播给group。
|
||||
|
||||
#### GCS API/XCom
|
||||
最后两层则是GCS API和基于Paxos实现的group communication engine(XCom)。
|
||||
|
||||
GCS API是高层API,对构建replicated state machine所需的属性进行了抽象,其将消息曾的实现和插件的上层进行了解耦。
|
||||
|
||||
group communication engine则是负责处理replication group中members的通信。
|
||||
|
||||
## Getting Started
|
||||
mysql group replication为mysql server提供了插件。在group中,每个server都需要配置并安装插件。
|
||||
|
||||
### Deploying Group Replication in Single-Primary Mode
|
||||
在group中,每个mysql server都可以运行在一台独立的物理host machine上,这也是部署group replication推荐的方式。本次介绍了如何创建一个拥有3个server实例的replication group,每个实例都运行在一个不同的host machine上。
|
||||
|
||||
<img src="https://dev.mysql.com/doc/refman/8.4/en/images/gr-3-server-group.png" style="width: 100%; max-width: 238px;">
|
||||
|
||||
上述为replication group的结构示意图。
|
||||
|
||||
#### Deploying Instances for Group Replication
|
||||
在本示例中,group中会包含3个实例,也是创建group所需的最少示例数。为group增加更多的实例能够提升group的fault tolerance。
|
||||
|
||||
当group中包含3个实例时,group可以容忍一个实例的失败。通过向group中添加更多实例,group可容忍的失败实例数量也会增加。group中可以使用的最大实例数量为9.
|
||||
|
||||
355
mysql/mysql集群/Replication.md
Normal file
355
mysql/mysql集群/Replication.md
Normal file
@@ -0,0 +1,355 @@
|
||||
- [Replication](#replication)
|
||||
- [Configuration Replication](#configuration-replication)
|
||||
- [binary log file position based replication configuration overview](#binary-log-file-position-based-replication-configuration-overview)
|
||||
- [Replication with Global Transaction Identifiers](#replication-with-global-transaction-identifiers)
|
||||
- [GTID Format and Storage](#gtid-format-and-storage)
|
||||
- [GTID Sets](#gtid-sets)
|
||||
- [msyql.gtid\_executed](#msyqlgtid_executed)
|
||||
- [mysql.gtid\_executed table compression](#mysqlgtid_executed-table-compression)
|
||||
- [GTID生命周期](#gtid生命周期)
|
||||
- [What changes are assign a GTID](#what-changes-are-assign-a-gtid)
|
||||
- [Replication Implementation](#replication-implementation)
|
||||
- [Replication Formats](#replication-formats)
|
||||
- [使用statement-based和row-based replication的优缺点对比](#使用statement-based和row-based-replication的优缺点对比)
|
||||
- [Advantages of statement-based replication](#advantages-of-statement-based-replication)
|
||||
- [Disadvantages of statement-based replication](#disadvantages-of-statement-based-replication)
|
||||
- [Advantages of row-based replication](#advantages-of-row-based-replication)
|
||||
- [disadvantages of row-based replication](#disadvantages-of-row-based-replication)
|
||||
- [Replay Log and Replication Metadata Repositories](#replay-log-and-replication-metadata-repositories)
|
||||
- [The Relay Log](#the-relay-log)
|
||||
|
||||
# Replication
|
||||
Replication允许数据从一个database server被复制到一个或多个mysql database servers(replicas)。replication默认是异步的;replicas并无需永久连接即可接收来自source的更新。基于configuration,可以针对所有的databases、选定的databases、甚至database中指定的tables进行replicate。
|
||||
|
||||
mysql中replication包含如下优点:
|
||||
- 可拓展方案:可以将负载分散到多个replicas,从而提升性能。在该环境中,所有的writes和updates都发生在source server。但是,reads则是可以发生在一个或多个replicas上。通过该模型,能够提升writes的性能(因为source专门用于更新),并可以通过增加replicas的数量来提高read speed
|
||||
- data security: 因为replica可以暂停replication过程,故而可以在replica中运行备份服务,而不会破坏对应的source data
|
||||
- analytics:实时数据可以在source中生成,而对信息的分析则可以发生在replica,分析过程不会影响source的性能
|
||||
- long-distance data distribution:可以replication来对remote site创建一个local copy,而无需对source的永久访问
|
||||
|
||||
在mysql 8.4中,支持不同的replication方式。
|
||||
- 传统方式基于从source的binary log中复制事件,并且需要log files和`在source和replica之间进行同步的position`。
|
||||
- 新的replication方式基于global transaction identifiers(GTIDs),是事务的,并且不需要和log files、position进行交互,其大大简化了许多通用的replication任务。使用GTIDs的replication保证了source和replica的一致性,所有在source提交的事务都会在replica被应用。
|
||||
|
||||
mysql中的replication支持不同类型的同步。
|
||||
- 原始的同步是单向、异步的复制,一个server作为source、其他一个或多个servers作为replicas。
|
||||
- NDB Cluster中,支持synchronous replication
|
||||
- 在mysql 8.4中,支持了半同步复制,通过半同步复制,在source中执行的提交将会被阻塞,直到至少一个replica收到transaction并对transaction进行log event,且向source发送ack
|
||||
- mysql 8.4同样支持delayed replication,使得replica故意落后于source至少指定的时间
|
||||
|
||||
## Configuration Replication
|
||||
### binary log file position based replication configuration overview
|
||||
在该section中,会描述mysql servers间基于binary log file position的replication方案。(source会将database的writes和updates操作以events的形式写入到binary log中)。根据database记录的变更,binary log中的信息将会以不同的logging format存储。replicas可以被配置,从source的binary log读取events,并且在replica本地的binary log中执行时间。
|
||||
|
||||
每个replica都会接收binary log的完整副本内容,并由replica来决定binary log中哪些statements应当被执行。除非你显式指定,否则binary log中所有的events都会在replica上被执行。如果需要,你可以对replica进行配置,仅处理应用于特定databases、tables的events。
|
||||
|
||||
每个replica都会记录二进制日志的坐标:其已经从source读取并处理的file name和文件中的position。这代表多个replicas可以连接到source,并执行相同binary log的不同部分。由于该过程受replicas控制,故而独立的replicas可以和source建立连接、取消连接,并且不会影响到source的操作。并且,每个replica记录了binary log的当前position,replica可以断开连接、重新连接并重启之前的过程。
|
||||
|
||||
source和每个replica都配置了unique ID(使用`server_id` system variable),除此之外,每个replica必须配置如下信息:
|
||||
- source's host name
|
||||
- source's log file name
|
||||
- position of source's file
|
||||
|
||||
这些可以在replica的mysql session内通过`CHANGE REPLICATION SOURCE TO`语句来管理,并且这些信息存储在replica的connection metadata repository中。
|
||||
|
||||
### Replication with Global Transaction Identifiers
|
||||
在该章节中,描述了使用global transaction identifiers(GTIDs)的transaction-based replication。当使用GTIDs时,每个在source上提交的事务都可以被标识和追踪,并可以被任一replica应用。
|
||||
|
||||
在使用GTIDs时,启动新replica或failover to a new source时并不需要参照log files或file position。由于GTIDs时完全transaction-based的,其更容易判断是否source和replicas一致,只要在source中提交的事务全部也都在replica上提交,那么source和replica则是保证一致的。
|
||||
|
||||
在使用GTIDs时,可以选择是statement-based的还是row-based的,推荐使用row-based format。
|
||||
|
||||
GTIDs在source和replica都是持久化的。总是可以通过检测binary log来判断是否source中的任一事务是否在replica上被应用。此外,一旦给定GTID的事务在给定server上被提交,那么后续任一事务如果拥有相同的GTID,其都会被server忽略。因此,在source上被提交的事务可以在replica上应用多次,冗余的应用会被忽略,其可以保证数据的一致性。
|
||||
|
||||
#### GTID Format and Storage
|
||||
一个global transation identifier(GTID)是一个唯一标识符,该标识符的创建并和`source上提交的每一个事务`进行关联。GTID不仅在创建其的server上是唯一的,并且在给定replication拓扑的所有servers间是唯一的。
|
||||
|
||||
GTID的分配区分了client trasactions(在source上提交的事务)以及replicated transactions(在replica上reproduced的事务)。当client transaction在source上提交时,如果transaction被写入到binary log中,其会被分配一个新的GTID。client transactions将会被保证单调递增,并且生成的编号之间不会存在间隙。`如果client transaction没有被写入到binary log,那么其在source中并不会被分配GTID`。
|
||||
|
||||
Replicated transaction将会使用`事务被source server分配的GTID`。在replicated transaction开始执行前,GTID就已经存在,并且,即使replicated transaction没有写入到replica的binary log中或是被replica过滤,该GTID也会被持久化。`mysql.gtid_executed` system table将会被用于保存`应用到给定server的所有事务的GTID,但是,已经存储到当前active binary log file的除外`。
|
||||
|
||||
> `active binary log file`中的gtid最终也会被写入到`gtid_executed`表中,但是,对于不同的存储引擎,写入时机可能不同。对于innodb而言,在事务提交时就会写入gtid_executed表,而对于其他存储引擎而言,则会在binary log发生rotation/server shutdown后才会将active binary log中尚未写入的GTIDs写入到`mysql.gtid_executed`表
|
||||
|
||||
GTIDs的auto-skip功能代表一个在source上提交的事务最多只能在replica上应用一次,这有助于保证一致性。一旦拥有给定GTID的事务在给定server上被提交,任何后续执行的事务如果拥有相同的GTID,都会被server忽略。并不会抛出异常,并且事务中的statements都不会被实际执行。
|
||||
|
||||
如果拥有给定GTID的事务在server上已经开始执行,但是尚未提交或回滚,那么任何尝试开启一个`拥有相同GTID事务的操作都会被阻塞`。该server并不会开始执行拥有相同GTID的事务,也不会将控制权交还给client。一旦第一个事务提交或回滚,那么被阻塞的事务就可以开始执行。如果第一次执行被回滚,那么被阻塞事务可以实际执行;如果第一个事务成功提交,那么被阻塞事务并不会被实际执行,而是会被自动跳过。
|
||||
|
||||
GTID的表示形式如下:
|
||||
```
|
||||
GTID = source_id:transaction_id
|
||||
```
|
||||
- `source_id`表示了originating server,通常为source的`server_uuid`
|
||||
- `transaction_id`则是一个由`transaction在source上提交顺序`决定的序列号。例如,第一个被提交的事务,其transaction_id为1;而第十个被提交的事务,其transaction_id则是为10.
|
||||
- 在GTID中,transaction_id部分的值不可能为0
|
||||
|
||||
例如,在UUID为`3E11FA47-71CA-11E1-9E33-C80AA9429562`上的server提交的第23个事务,其GTID为
|
||||
```
|
||||
3E11FA47-71CA-11E1-9E33-C80AA9429562:23
|
||||
```
|
||||
|
||||
在GTID中,序列号的上限值为`2^63 - 1, or 9223372036854775807`(signed 64-bit integer)。如果server运行时超过GTIDs,其将执行`binlog_error_action`指定的行为。当server接近该限制时,会发出一个warning message。
|
||||
|
||||
在mysql 8.4中,也支持tagged GTIDs,一个tagged GTID由三部分组成,通过`:`进行分隔,示例如下所示:
|
||||
```
|
||||
GTID = source_id:tag:transaction_id
|
||||
```
|
||||
|
||||
在该示例中,`source_id`和`transaction_id`的含义和先前相同,`tag`则是一个用户自定义的字符串,用于表示`specific group of transactions`。
|
||||
|
||||
例如,在UUID为`ed102faf-eb00-11eb-8f20-0c5415bfaa1d`的server上第117个提交的tag为`Domain_1`的事务,其GTID为
|
||||
```
|
||||
ed102faf-eb00-11eb-8f20-0c5415bfaa1d:Domain_1:117
|
||||
```
|
||||
|
||||
事务的GTID会展示在`mysqlbinlog`的输出中,用于分辨`performance schema replication status tables`中独立的事务,例如`replication_applier_status_by_worker`表。
|
||||
|
||||
`gtid_next` system variable的值是单个GTID。
|
||||
|
||||
##### GTID Sets
|
||||
一个GTID set由`一个或多个GTIDs`或`GTIDs`范围构成。`gtid_executed`或`gtid_purged` system variables存储的就是GTID sets。`START REPLICA`选项`UNTIL SQL_BEFORE_GTIDS`和`UNTIL SQL_AFTER_GTIDS`可令replica在处理事务时,`最多只处理到GTID set中的第一个GTID`或`在处理完GTID set中的最后一个GTID后停止`。内置的`GTID_SUBSET()`和`GTID_SUBTRACT()`函数需要`GTID sets`作为输入。
|
||||
|
||||
在相同server上的GTIDs范围可以按照如下形式来表示:
|
||||
```
|
||||
3E11FA47-71CA-11E1-9E33-C80AA9429562:1-5
|
||||
```
|
||||
上述示例表示在uuid为`3E11FA47-71CA-11E1-9E33-C80AA9429562`的server上提交的顺序为`1~5`之间的事务。
|
||||
|
||||
相同server上的多个single GTIDs或ranges of GTIDs可以通过如下形式来表示
|
||||
```
|
||||
3E11FA47-71CA-11E1-9E33-C80AA9429562:1-3:11:47-49
|
||||
```
|
||||
|
||||
GTID set可以包含任意`single GTIDs`和`GTIDs range`的组合,也可以包含来自不同server的GTIDs。如下实例中展示了`gtid_executed` system variable中存储的GTID set,代表replica中应用了来自超过一个source的事务:
|
||||
```
|
||||
2174B383-5441-11E8-B90A-C80AA9429562:1-3, 24DA167-0C0C-11E8-8442-00059A3C7B00:1-19
|
||||
```
|
||||
当GTID set从server variables中返回时,UUID按照字母排序返回,而序列号间区间则是合并后按照升序排列。
|
||||
|
||||
当构建GTID set时,用户自定义tag也会被作为UUID的一部分,这代表来自相同server且tag相同的多个GTIDs可以被包含在一个表达式中,如下所示:
|
||||
```
|
||||
3E11FA47-71CA-11E1-9E33-C80AA9429562:Domain_1:1-3:11:47-49
|
||||
```
|
||||
来源相同server,但是tags不同的GTIDs表示方式如下:
|
||||
```
|
||||
3E11FA47-71CA-11E1-9E33-C80AA9429562:Domain_1:1-3:15-21, 3E11FA47-71CA-11E1-9E33-C80AA9429562:Domain_2:8-52
|
||||
```
|
||||
|
||||
GTID set的完整语法如下所示:
|
||||
```
|
||||
gtid_set:
|
||||
uuid_set [, uuid_set] ...
|
||||
| ''
|
||||
|
||||
uuid_set:
|
||||
uuid:[tag:]interval[:interval]...
|
||||
|
||||
uuid:
|
||||
hhhhhhhh-hhhh-hhhh-hhhh-hhhhhhhhhhhh
|
||||
|
||||
h:
|
||||
[0-9|A-F]
|
||||
|
||||
tag:
|
||||
[a-z_][a-z0-9_]{0,31}
|
||||
|
||||
interval:
|
||||
m[-n]
|
||||
|
||||
(m >= 1; n > m)
|
||||
```
|
||||
##### msyql.gtid_executed
|
||||
GTIDs被存储在`mysql.gtid_executed`表中,该表中的行用于表示GTID或GTID set,行包含信息如下
|
||||
- source server的uuid
|
||||
- 用户自定义的tag
|
||||
- starting transaction IDs of the set
|
||||
- ending transaction IDs of the set
|
||||
|
||||
如果行代表single GTID,那么最后两个字段的值相同。
|
||||
|
||||
`mysql.gtid_executed`表在mysql server安装或升级时会被自动创建,其表结构如下:
|
||||
```sql
|
||||
CREATE TABLE gtid_executed (
|
||||
source_uuid CHAR(36) NOT NULL,
|
||||
interval_start BIGINT NOT NULL,
|
||||
interval_end BIGINT NOT NULL,
|
||||
gtid_tag CHAR(32) NOT NULL,
|
||||
PRIMARY KEY (source_uuid, gtid_tag, interval_start)
|
||||
);
|
||||
```
|
||||
|
||||
`mysql.gtid_executed`用于mysql server内部使用,其允许replica在binary logging被禁用的情况下使用GTIDs,其也可以在binary logs丢失时保留GTID的状态。如果执行`RESET BINARY LOGS AND GTIDS`,那么`mysql.gtid_executed`表将会被清空。
|
||||
|
||||
只有当`gtid_mode`被设置为`ON`或`ON_PERMISSIVE`时,GTIDs才会被存储到mysql.gtid_executed中。如果binary logging被禁用,或者`log_replica_updates`被禁用,server在事务提交时将会把属于个各事务的GTID和事务一起存储到buffer中,并且background threads将会周期性将buffer中的内容以entries的形式添加到`mysql.gtid_executed`表中。
|
||||
|
||||
对于innodb存储引擎而言,如果binary logging被启用,server更新`mysql.gtid_executed`表的方式将会和`binary logging或replica update logging被启用`时一样,都会在每个事务提交时存储GTID。对于其他存储引擎,则是在binary log rotation发生时或server shut down时更新`mysql.gtid_executed`表的内容。
|
||||
|
||||
如果mysql.gtid_executed表无法被写访问,并且binary log file因`reaching the maximum file size`之外的任何理由被rotated,那么current binary log file将仍被使用。并且,当client发起rotation时将会返回错误信息,并且server将会输出warning日志。如果`mysql.gtid_executed`无法被写访问,并且binary log单个文件大小达到`max_binlog_size`,那么server将会根据`binlog_error_action`设置来执行操作。如果`IGNORE_ERROR`被设置,那么server将会输出error到日志,并且binary logging将会被停止;如果`ABORT_SERVER`被设置,那么server将会shutdown。
|
||||
|
||||
> 因为写入到active binary log的GTIDs最终也要被写入`mysql.gtid_executed`表,但是该表若当前不可写访问,那么此时将无法触发`binary log rotation`。
|
||||
>
|
||||
> MySQL.gtid_executed中缺失的GTIDs必须包含在active binary log file中,如果log file触发rotation,但是无法向mysql.gtid_executed中写入数据,那么rotation是不被允许的。
|
||||
|
||||
|
||||
##### mysql.gtid_executed table compression
|
||||
随着时间的推移,mysql.gtid_executed表会填满大量行,行数据代表独立的GTID,示例如下:
|
||||
```
|
||||
+--------------------------------------+----------------+--------------+----------+
|
||||
| source_uuid | interval_start | interval_end | gtid_tag |
|
||||
|--------------------------------------+----------------+--------------|----------+
|
||||
| 3E11FA47-71CA-11E1-9E33-C80AA9429562 | 31 | 31 | Domain_1 |
|
||||
| 3E11FA47-71CA-11E1-9E33-C80AA9429562 | 32 | 32 | Domain_1 |
|
||||
| 3E11FA47-71CA-11E1-9E33-C80AA9429562 | 33 | 33 | Domain_1 |
|
||||
| 3E11FA47-71CA-11E1-9E33-C80AA9429562 | 34 | 34 | Domain_1 |
|
||||
| 3E11FA47-71CA-11E1-9E33-C80AA9429562 | 35 | 35 | Domain_1 |
|
||||
| 3E11FA47-71CA-11E1-9E33-C80AA9429562 | 36 | 36 | Domain_2 |
|
||||
| 3E11FA47-71CA-11E1-9E33-C80AA9429562 | 37 | 37 | Domain_2 |
|
||||
| 3E11FA47-71CA-11E1-9E33-C80AA9429562 | 38 | 38 | Domain_2 |
|
||||
| 3E11FA47-71CA-11E1-9E33-C80AA9429562 | 39 | 39 | Domain_2 |
|
||||
| 3E11FA47-71CA-11E1-9E33-C80AA9429562 | 40 | 40 | Domain_1 |
|
||||
| 3E11FA47-71CA-11E1-9E33-C80AA9429562 | 41 | 41 | Domain_1 |
|
||||
| 3E11FA47-71CA-11E1-9E33-C80AA9429562 | 42 | 42 | Domain_1 |
|
||||
| 3E11FA47-71CA-11E1-9E33-C80AA9429562 | 43 | 43 | Domain_1 |
|
||||
| 3E11FA47-71CA-11E1-9E33-C80AA9429562 | 44 | 44 | Domain_2 |
|
||||
| 3E11FA47-71CA-11E1-9E33-C80AA9429562 | 45 | 45 | Domain_2 |
|
||||
| 3E11FA47-71CA-11E1-9E33-C80AA9429562 | 46 | 46 | Domain_2 |
|
||||
| 3E11FA47-71CA-11E1-9E33-C80AA9429562 | 47 | 47 | Domain_1 |
|
||||
| 3E11FA47-71CA-11E1-9E33-C80AA9429562 | 48 | 48 | Domain_1 |
|
||||
```
|
||||
|
||||
为了节省空间,可以对gtid_executed表周期性的进行压缩,将多个`single GTID`替换为单个`GTID set`,压缩后的数据如下;
|
||||
```
|
||||
+--------------------------------------+----------------+--------------+----------+
|
||||
| source_uuid | interval_start | interval_end | gtid_tag |
|
||||
|--------------------------------------+----------------+--------------|----------+
|
||||
| 3E11FA47-71CA-11E1-9E33-C80AA9429562 | 31 | 35 | Domain_1 |
|
||||
| 3E11FA47-71CA-11E1-9E33-C80AA9429562 | 36 | 39 | Domain_2 |
|
||||
| 3E11FA47-71CA-11E1-9E33-C80AA9429562 | 40 | 43 | Domain_1 |
|
||||
| 3E11FA47-71CA-11E1-9E33-C80AA9429562 | 44 | 46 | Domain_2 |
|
||||
| 3E11FA47-71CA-11E1-9E33-C80AA9429562 | 47 | 48 | Domain_1 |
|
||||
...
|
||||
```
|
||||
server可通过名为`thread/sql/compress_gtid_table`的前台线程来执行gtid_executed表的压缩操作,该线程并不会在`show processlist`的输出中被列出,但是可以通过查询`threads`表来查看,示例如下:
|
||||
```sql
|
||||
mysql> SELECT * FROM performance_schema.threads WHERE NAME LIKE '%gtid%'\G
|
||||
*************************** 1. row ***************************
|
||||
THREAD_ID: 26
|
||||
NAME: thread/sql/compress_gtid_table
|
||||
TYPE: FOREGROUND
|
||||
PROCESSLIST_ID: 1
|
||||
PROCESSLIST_USER: NULL
|
||||
PROCESSLIST_HOST: NULL
|
||||
PROCESSLIST_DB: NULL
|
||||
PROCESSLIST_COMMAND: Daemon
|
||||
PROCESSLIST_TIME: 1509
|
||||
PROCESSLIST_STATE: Suspending
|
||||
PROCESSLIST_INFO: NULL
|
||||
PARENT_THREAD_ID: 1
|
||||
ROLE: NULL
|
||||
INSTRUMENTED: YES
|
||||
HISTORY: YES
|
||||
CONNECTION_TYPE: NULL
|
||||
THREAD_OS_ID: 18677
|
||||
```
|
||||
当server启用了binary log时,上述压缩方式并不会被使用,`mysql.gtid_executed`在每次binary log rotation时会被压缩。但是,当binary logging被禁用时,`thread/sql/compress_gtid`线程处于sleep状态,每有一定数量的事务被执行时,线程会wake up并执行压缩操作。在table被压缩之前经过的事务数量,即压缩率,可由system variable `gtid_executed_compression_period`来进行控制。如果将值设置为0,代表该thread永远不会wake up。
|
||||
|
||||
相比于其他存储引擎,innodb事务写入`mysql.gtid_executed`的过程有所不同,在innodb存储引擎中,过程通过线程`innodb/clone_gtid_thread`来执行。此GTID持久化线程将会分组收集GTIDs,并将其刷新到`mysql.gtid_executed`表中,并对table进行压缩。如果server中既包含innodb事务,又包含non-innodb事务,那么由`compress_gtid_table`线程执行的压缩操作将会干扰`clone_gtid_thread`的工作,并对其效率进行显著下降。为此,在此场景下更推荐将`gtid_executed_compression_period`设置为0,故而`compress_gtid_table`线程将永远不被激活。
|
||||
|
||||
`gtid_executed_compression_period`的默认值为0,且所有事务不论其所属存储引擎,都会被`clone_gitid_thread`写入到`mysql.gtid_exec uted`中。
|
||||
|
||||
当server实例启动后,如果`gtid_executed_compression_preiod`被设置为非0值,并且`compress_gtid_table`线程也启动,在多数server配置中,将会对`msyql.gtid_executed`表执行显式压缩。压缩通过线程的启动触发。
|
||||
|
||||
#### GTID生命周期
|
||||
GTID生命周期由如下步骤组成:
|
||||
1. 事务在source上被执行并提交,client transaction将会被分配GTID,GTID由`source UUID`和`smallest nonzero transaction sequence number not yet used on this server`组成。GTID也会被写入到binary log中(在log中,GTID被写入在事务之前)。如果client transaction没有被写入binary log(例如,事务被过滤或事务为read-only),那么该事务不会被分配GTID
|
||||
2. 如果为事务分配了GTID,那么在事务提交时GTID会被原子的持久化。在binary log中,GTID会被写入到事务开始的位置。不管何时,如果binary log发生rotation或server shutdown,server会将所有写入到binary log file中事务的GTIDs写入到`mysql.gtid_executed`表中
|
||||
3. 如果为事务分配了GTID,那么GTID的外部化是`non-atomically`的(在事务提交后非常短的时间)。外部化过程会将GTID添加到`gtid_executed` system variable所代表的GTID set中(`@@GLOBAL.gtid_executed`)。该GTID set中包含`representation of the set of all committed GTID transactions`,并且在replication中会作为代表server state的token使用。
|
||||
1. 当binary logging启用时,`gtid_executed` system variable代表的GTIDs set是已被应用事务的完整记录。但是`mysql.gtid_executed`表则并记录所有GTIDs,可能由部分GTIDs仍存在于active binary log file中尚未被写入到gtid_executed table
|
||||
4. 在binary log data被转移到replica并存储在replica的relay log中后,replica会读取GTID的值,并且将其设置到`gtid_next` system variable中。这告知replica下一个事务将会使用`gtid_next`所代表的GTID。replica在session context中设置`gtid_next`
|
||||
5. replica在处理事务前,会验证没有其他线程已经持有了`gtid_next`中GTID的所有权。通过该方法,replica可以保证该该GTID关联的事务在replica上没有被应用过,并且还能保证没有其他session已经读取该GTID但是尚未提交关联事务。故而,如果并发的尝试应用相同的事务,那么server只会让其中的一个运行。
|
||||
1. replica的`gtid_owned` system variable(`@@GLOBAL.gtid_owned`)代表了当前正在使用的每个GTID和拥有该GTID的线程ID。如果GTID已经被使用过,并不会抛出错误,`auto-skip`功能会忽略该事务
|
||||
6. 如果GTID尚未被使用,那么replica将会对replicated transaction进行应用。因为`gtid_next`被设置为了`由source分配的GTID`,replica将不会尝试为事务分配新的GTID,而是直接使用存储在`gtid_next`中存储的GTID
|
||||
7. 如果replica上启用了binary logging,GTID将会在提交时被原子的持久化,写入到binary log中事务开始的位置。无论何时,如果binary log发生rotation或server shutdown,server会将`先前写入到binary log中事务的GTIDs`写入到`mysql.gtid_executed`表中
|
||||
8. 如果replica禁用binary log,GTID将会原子的被持久化,直接被写入到`mysql.gtid_executed`表中。mysql会向事务中追加一个statement,并且将GTID插入到table中该操作是原子的,在该场景下,`mysql.gtid_executed`表记录了完整的`transactions applied on the replica`。
|
||||
9. 在replicated transaction提交后很短的时间,GTID会被非原子的externalized,GTID会被添加到replica的`gtid_executed` system variable代表的GTIDs中。在source中,`gtid_executed` system variable包含了所有提交的GTID事务。
|
||||
|
||||
在source上被完全过滤的client transactions并不会被分配GTID,因此其不会被添加到`gtid_executed` system variable或被添加到`mysql.gtid_executed`表中。然而,replicated transaction中的GTIDs即使在replica被完全过滤,也会被持久化。
|
||||
- 如果binary logging在replica开启,被过滤的事务将会作为gtid_log_event的形式被写入到binary log,后续跟着一个empty transaction,事务中仅包含BEGIN和COMMIT语句
|
||||
- 如果binary logging在replica被禁用,给过滤事务的GTID将会被写入到`mysql.gtid_executed`表
|
||||
|
||||
将filtered-out transactions的GTIDs保留,可以确保`mysql.gtid_executed`表和`gtid_executed`system variable中的GTIDs可以被压缩。同时其也能确保replica重新连接到source时,filtered-out transactions不会被重新获取
|
||||
|
||||
在多线程的replica上(`replica_parallel_workers > 0`),事务可以被并行的应用,故而replicated transactions可以以不同的顺序来提交(除非`replica_preserve_commit_order = 1`)。在并行提交时,`gtid_executed`system variable中的GTIDs将包含多个GTID ranges,多个范围之间存在空隙。在多线程replicas上,只有最近被应用的事务间才会存在空隙,并且会随着replication的进行而被填充。当replication threads通过`STOP REPLICA`停止时,这些空隙会被填补。当发生shutdown event时(server failure导致),此时replication 停止,空隙仍可能保留。
|
||||
|
||||
##### What changes are assign a GTID
|
||||
GTID生成的典型场景为`server为提交事务生成新的GTID`。然而,GTIDs可以被分配给除事务外的其他修改,且在某些场景下一个事务可以被分配多个GTIDs。
|
||||
|
||||
每个写入binary log的数据库修改(`DDL/DML`)都会被分配一个GTID。其包含了自动提交的变更、使用`BEGIN...COMMIT`的变更和使用`START TRANSACTION`的变更。一个GTID会被分配给数据库的`creation, alteration, deletion`操作,并且`non-table` database object例如`procedure, function, trigger, event, view, user, role, grant`的`creation, alteration, deletion`也会被分配GTID。
|
||||
|
||||
非事务更新和事务更新都会被分配`GTID`,额外的,对于非事务更新,如果在尝试写入binary log cahche时发生disk write failure,导致binary log中出现间隙,那么生成的日志事件将会被分配一个GTID。
|
||||
|
||||
## Replication Implementation
|
||||
在replication的设计中,source server会追踪其binary log中所有对databases的修改。binary log中记录了自server启动时所有修改数据库结构或内容的事件。通常,`SELECT`语句将不会被记录,其并没有对数据库结构或内容造成修改。
|
||||
|
||||
每个连接到source的replica都会请求binary log的副本,replica会从source处拉取数据,而不是source向replica推送数据。replica也会执行其从binary log收到的事件。`发生在source上的修改将会在replica上进行重现`。在重现过程中,会发生表创建、表结构修改、数据的新增/删除/修改等操作。
|
||||
|
||||
由于每个replica都是独立的,每个连接到source的replica,其对`source中binary log`内容的replaying都是独立的。此外,因为每个replica只通过请求source来接收binary log的拷贝,replica能够以其自身的节奏来读取和更新其数据副本,并且其能在不对source和其他replicas造成影响的条件下开启或停止replication过程。
|
||||
|
||||
### Replication Formats
|
||||
在binary log中,events根据其事件类型以不同的格式被记录。replication使用的foramts取决于事件被记录到source server的binary log中时所使用的foramt。binary log format和replciation过程中使用到的format,其关联关系如下:
|
||||
- 当使用statement-based binary logging时,source会将sql statements写入到binary log。从source到replica的replication将会在replica上执行该sql语句。这被称之为statement-based replication,关联了mysql statement-based binary logging format
|
||||
- 当使用row-based loggging时,source将会将`table rows如何变更`的事件写入到binary log中。在replica中,将会重现events对table rows所做的修改。则会被成为row-based replication
|
||||
- `row-based logging`是默认的方法
|
||||
- 可以配置mysql使用`mix-format logging`,但使用`mix-format logging`时,将默认使用statement-based log。但是对于特定的语句,也取决于使用的存储引擎,在某些床惊吓log也会被自动切换到row-based。使用mixed format的replication被称之为mix-based replication或mix-format replication
|
||||
|
||||
mysql server中的logging format通过`binlog_format` system variable来进行控制。该变量可以在session/global级别进行设置
|
||||
- 在将variable设置为session级别时,将仅会对当前session生效,并且仅持续到当前session结束
|
||||
- 将variable设置为global级别时,该设置将会对`clients that connect after the change`生效,`但对于any current client sessions,包括.发出该修改请求的session都不生效`
|
||||
|
||||
#### 使用statement-based和row-based replication的优缺点对比
|
||||
每个binary logging format都有优缺点。对大多数users,mixed replication format能够提供性能和数据完整性的最佳组合。
|
||||
|
||||
##### Advantages of statement-based replication
|
||||
- 在使用statement-based格式时,会向log files中写入更少的数据。特别是在更新或删除大量行时,使用statement-based格式能够导致更少的空间占用。在从备份中获取和恢复时,其速度也更快
|
||||
- log files包含所有造成修改的statements,可以被用于审核database
|
||||
|
||||
##### Disadvantages of statement-based replication
|
||||
- 对于Statement-based replication而言,statements并不是安全的。并非所有对数据造成修改的statements都可以使用statement-based replication。在使用statement-based replication时,任何非确定性的行为都难以复制(例如在语句中包含随机函数等非确定性行为)
|
||||
- 对于复杂语句,statement必须在实际对目标行执行修改前重新计算。而当使用row-based replication时,replica可以直接修改受影响行,而无需重新计算
|
||||
|
||||
##### Advantages of row-based replication
|
||||
- 所有的修改可以被replicated,其是最安全的replication形式
|
||||
|
||||
##### disadvantages of row-based replication
|
||||
- 相比于statement-based replication,row-based replication通常会向log中ieur更多数据,特别是当statement操作大量行数据是
|
||||
- 在replica并无法查看从source接收并执行的statements。可以通过`mysqlbinlog`的`--base64-output=DECODE-ROWS`和`--verbose`选项来查看数据变更
|
||||
|
||||
### Replay Log and Replication Metadata Repositories
|
||||
replica server会创建一系列仓库,其中存储在replication过程中使用的信息;
|
||||
- `relay log`: 该日志被replication I/O线程写入,包含从source server的binary log读取的事务。relay log中记录的事务将会被replication SQL thread应用到replica
|
||||
- `connection metadata repository`: 包含replication receiver thread连接到source server并从binary log获取事务时,需要的信息。connection metadata repository会被写入到`mysql.slave_master_info`中
|
||||
- `applier metadata repository`: 包含replication applier thread从relay log读取并应用事务时所需要的信息。applier metadata repository会被写入到`mysql.slave_relay_log_info`表中
|
||||
|
||||
#### The Relay Log
|
||||
relay log和binary log类似,由一些`numbered files`构成,文件中包含`描述数据库变更的事件`。relay log还包含一个index file,其中记录了所有被使用的relay log files的名称。默认情况下,relay log files位于data directory中
|
||||
|
||||
relay log files拥有和binary log相同的格式,也可以通过mysqlbinlog进行读取。如果使用了binary log transaction compression,那么写入relay log的事务payloads也会按照和binary log相同的方式被压缩。
|
||||
|
||||
对于默认的replication channel,relay log file命名形式如下`host_name-relay-bin.nnnnnn`:
|
||||
- `host_name`为replica server host的名称
|
||||
- `nnnnnn`是序列号,序列号从000001开始
|
||||
|
||||
对于非默认的replication channels,默认的名称为`host_name-relay-bin-channel`:
|
||||
- `channel`为replciation channel的名称
|
||||
|
||||
replica会使用index file来追踪当前在使用的relay log files。默认relay log index file的名称为`host_name-relay-bin.index`。
|
||||
|
||||
当如下条件下,replica server将会创建新的relay log file:
|
||||
- 每次replication I/O thread开启时
|
||||
- 当logs被刷新时(例如,当执行`FLUSH LOGS`命令时)
|
||||
- 当当前relay log file的大小太大时,大小上限可以通过如下方式决定
|
||||
- 如果`max_relay_log_size`的大小大于0,那么其就是relay log file的最大大小
|
||||
- 如果`max_relay_log_size`为0,那么relay log file的最大大小由`max_binlog_size`决定
|
||||
|
||||
replication sql thread在其执行完文件中所有events之后,都不再需要该文件,被执行完的relay log file都会被自动删除。目前没有显式的机制来删除relay logs,relay log的删除由replciation sql thread来处理。但是,`FLUSH LOGS`可以针对relay logs进行rotation。
|
||||
|
||||
|
||||
227
netty/netty-doc.md
Normal file
227
netty/netty-doc.md
Normal file
@@ -0,0 +1,227 @@
|
||||
# netty document
|
||||
## ChannelPipeline
|
||||
I/O Request
|
||||
via Channel or
|
||||
ChannelHandlerContext
|
||||
|
|
||||
+---------------------------------------------------+---------------+
|
||||
| ChannelPipeline | |
|
||||
| \|/ |
|
||||
| +---------------------+ +-----------+----------+ |
|
||||
| | Inbound Handler N | | Outbound Handler 1 | |
|
||||
| +----------+----------+ +-----------+----------+ |
|
||||
| /|\ | |
|
||||
| | \|/ |
|
||||
| +----------+----------+ +-----------+----------+ |
|
||||
| | Inbound Handler N-1 | | Outbound Handler 2 | |
|
||||
| +----------+----------+ +-----------+----------+ |
|
||||
| /|\ . |
|
||||
| . . |
|
||||
| ChannelHandlerContext.fireIN_EVT() ChannelHandlerContext.OUT_EVT()|
|
||||
| [ method call] [method call] |
|
||||
| . . |
|
||||
| . \|/ |
|
||||
| +----------+----------+ +-----------+----------+ |
|
||||
| | Inbound Handler 2 | | Outbound Handler M-1 | |
|
||||
| +----------+----------+ +-----------+----------+ |
|
||||
| /|\ | |
|
||||
| | \|/ |
|
||||
| +----------+----------+ +-----------+----------+ |
|
||||
| | Inbound Handler 1 | | Outbound Handler M | |
|
||||
| +----------+----------+ +-----------+----------+ |
|
||||
| /|\ | |
|
||||
+---------------+-----------------------------------+---------------+
|
||||
| \|/
|
||||
+---------------+-----------------------------------+---------------+
|
||||
| | | |
|
||||
| [ Socket.read() ] [ Socket.write() ] |
|
||||
| |
|
||||
| Netty Internal I/O Threads (Transport Implementation) |
|
||||
+-------------------------------------------------------------------+
|
||||
|
||||
如上图所示,i/o event将会被`ChannelInboundHandler`或`ChannelOutboundHandler`处理,并且通过调用`ChannelHandlerContext`中的event propagation method(触发事件传播的方法)来进行转发。
|
||||
|
||||
常见的event propagation method如下,例如`ChannelHandlerContext.write(Object)`。
|
||||
|
||||
### inbound event
|
||||
inbound event会通过inbound handler自底向上的进行处理,例如上图的左边部分所示。inbound handler通常处理由io thread产生的inbound data,inbound则是通常从remote peer读取。
|
||||
|
||||
> 如果inbound event传播到高于top inbound handler,那么该event将会被丢弃。
|
||||
|
||||
### outbound event
|
||||
outbound event会按照自顶向下的顺序被outbound进行处理,outbound handler通常产生outbound traffic或对outbound traffic进行处理。
|
||||
|
||||
> 如果outbound event传播到低于bottom outbound handler, 其会直接被channel关联的io线程处理。
|
||||
|
||||
> 通常,io thread会执行实际的输出操作,例如`SocketChannel.write(ByteBuffer)`。
|
||||
|
||||
### pipeline处理顺序
|
||||
例如,按照如下顺序项pipeline中添加handler时,
|
||||
```java
|
||||
ChannelPipeline p = ...;
|
||||
p.addLast("1", new InboundHandlerA());
|
||||
p.addLast("2", new InboundHandlerB());
|
||||
p.addLast("3", new OutboundHandlerA());
|
||||
p.addLast("4", new OutboundHandlerB());
|
||||
p.addLast("5", new InboundOutboundHandlerX());
|
||||
```
|
||||
#### inbound
|
||||
对于inbound event,handler的执行顺序为`1,2,3,4,5`
|
||||
|
||||
但由于`3,4`没有实现ChannelInboundHandler,故而Inbound event会跳过`3,4` handler,实际Inbound event的handler顺序为`1,2,5`
|
||||
|
||||
#### outbound
|
||||
对于outbound event,handler的执行顺序为`5,4,3,2,1`
|
||||
|
||||
对于outbound evnet,由于`1,2`并没有实现ChannelOutboundHandler,故而outbound event的handler顺序为`5,4,3`。
|
||||
|
||||
> inbound顺序和pipeline handler的添加顺序相同,outbound顺序和pipeline handler添加顺序相反。
|
||||
|
||||
### 将event转发给下一个handler
|
||||
如上图所示,再handler中必须调用`ChannelHandlerContext`中的event propagation method来将event转发给下一个handler。`event propagation event`包括的方法如下:
|
||||
|
||||
#### Inbound event propagation method
|
||||
- ChannelHandlerContext.fireChannelRegistered()
|
||||
- ChannelHandlerContext.fireChannelActive()
|
||||
- ChannelHandlerContext.fireChannelRead(Object)
|
||||
- ChannelHandlerContext.fireChannelReadComplete()
|
||||
- ChannelHandlerContext.fireExceptionCaught(Throwable)
|
||||
- ChannelHandlerContext.fireUserEventTriggered(Object)
|
||||
- ChannelHandlerContext.fireChannelWritabilityChanged()
|
||||
- ChannelHandlerContext.fireChannelInactive()
|
||||
- ChannelHandlerContext.fireChannelUnregistered()
|
||||
|
||||
#### Outbound event propagation method
|
||||
- ChannelHandlerContext.bind(SocketAddress, ChannelPromise)
|
||||
- ChannelHandlerContext.connect(SocketAddress, SocketAddress, ChannelPromise)
|
||||
- ChannelHandlerContext.write(Object, ChannelPromise)
|
||||
- ChannelHandlerContext.flush()
|
||||
- ChannelHandlerContext.read()
|
||||
- ChannelHandlerContext.disconnect(ChannelPromise)
|
||||
- ChannelHandlerContext.close(ChannelPromise)
|
||||
- ChannelHandlerContext.deregister(ChannelPromise)
|
||||
|
||||
### build pipeline
|
||||
在一个pipeline中,应该包含一个或多个ChannelHandler用于接收IO事件(read)和请求IO操作(write and close)。例如,一个典型的server其channel的pipeline中应该包含如下handler:
|
||||
- protocol decoder:将二进制数据转化为java object
|
||||
- protocol encoder:将java object转化为二进制数据
|
||||
- business logic handler:执行实际业务操作
|
||||
|
||||
构建pipeline示例如下:
|
||||
```java
|
||||
static final EventExecutorGroup group = new DefaultEventExecutorGroup(16);
|
||||
...
|
||||
ChannelPipeline pipeline = ch.pipeline();
|
||||
|
||||
pipeline.addLast("decoder", new MyProtocolDecoder());
|
||||
pipeline.addLast("encoder", new MyProtocolEncoder());
|
||||
|
||||
// Tell the pipeline to run MyBusinessLogicHandler's event handler methods
|
||||
// in a different thread than an I/O thread so that the I/O thread is not blocked by
|
||||
// a time-consuming task.
|
||||
// If your business logic is fully asynchronous or finished very quickly, you don't
|
||||
// need to specify a group.
|
||||
pipeline.addLast(group, "handler", new MyBusinessLogicHandler());
|
||||
```
|
||||
当为BusinessLogicHandler指定DefaultEventExecutorGroup时,虽然会将操作从EventLoop中卸载,但是其针对每个ChannelHandlerContext仍然是串行进行处理的(即先提交的task先执行)。故而,串行处理可能仍然会导致性能瓶颈。如果在用例场景下顺序不太重要时,可以考虑使用`UnorderedThreadPoolEventExecutor`来最大化任务的并行执行。
|
||||
|
||||
> ### DefaultEventExecutorGroup
|
||||
> 当为handler指定DefaultEventExecutorGroup时,其ChannelHandlerContext中executor对应的是
|
||||
> DefaultEventExecutorGroup中的其中一个,故而handler对所有任务的处理都通过同一个executor进行。
|
||||
>
|
||||
> DefaultEventExecutorGroup中,所有executor其类型都为`SingleThreadEventExecutor`,故而handler对所有任务的处理都是有序的。在前面的exectuor处理完成之前,后续任务都被阻塞。
|
||||
|
||||
> ### UnorderedThreadPoolEventExecutor
|
||||
> UnorderedThreadPoolEventExecutor其实现了EventExecutorGroup,但是其`next`方法用于只返回其本身`(this)`.
|
||||
>
|
||||
> 并且,UnorderedThreadPoolEventExecutor继承了ScheduledThreadPoolExecutor,并可以指定线程数,故而,当为handler指定该类型为group时,提交给相同handler的tasks,可能会被不同的线程执行,不保证有序。
|
||||
>
|
||||
> 故而,针对同一handler,其后续任务无需等待前面任务执行完成之后再执行,这样能够提高吞吐量和并发度。
|
||||
|
||||
### ThreadSafety
|
||||
channelhandler可以在任何时刻添加到pipeline或从pipeline中移除,ChannelPipeline是线程安全的。例如,可以在交换敏感信息前添加encryption handler并且在交换完信息后将encryption handler移除。
|
||||
|
||||
## Heartbeat机制
|
||||
在netty中,可以通过使用`IdleStateHandler`来实现心跳机制,可以向pipeline中添加`IdleStateHandler`作为心跳检测processor,并且可以添加一个自定义handler并实现`userEventTriggered`接口作为对超时事件的处理。
|
||||
|
||||
### 服务端heartbeat实现
|
||||
服务端实现示例如下:
|
||||
```java
|
||||
ServerBootstrap b= new ServerBootstrap();
|
||||
b.group(bossGroup,workerGroup).channel(NioServerSocketChannel.class)
|
||||
.option(ChannelOption.SO_BACKLOG,1024)
|
||||
.childHandler(new ChannelInitializer<SocketChannel>() {
|
||||
@Override
|
||||
protected void initChannel(SocketChannel socketChannel) throws Exception {
|
||||
socketChannel.pipeline().addLast(new IdleStateHandler(5, 0, 0, TimeUnit.SECONDS));
|
||||
socketChannel.pipeline().addLast(new StringDecoder());
|
||||
socketChannel.pipeline().addLast(new HeartBeatServerHandler());
|
||||
}
|
||||
});
|
||||
```
|
||||
自定义的心跳超时处理handler,其逻辑则如下所示。当每当timeout event被触发时,都会调用`userEventTriggered`事件,`timeout event`包含`read idle timeout`或`write idle timeout`。
|
||||
```java
|
||||
class HeartBeatServerHandler extends ChannelInboundHandlerAdapter {
|
||||
private int lossConnectCount = 0;
|
||||
|
||||
@Override
|
||||
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
|
||||
System.out.println("No message from the client has been received for 5 seconds!");
|
||||
if (evt instanceof IdleStateEvent){
|
||||
IdleStateEvent event = (IdleStateEvent)evt;
|
||||
if (event.state()== IdleState.READER_IDLE){
|
||||
lossConnectCount++;
|
||||
if (lossConnectCount>2){
|
||||
System.out.println("Close this inactive channel!");
|
||||
ctx.channel().close();
|
||||
}
|
||||
}
|
||||
}else {
|
||||
super.userEventTriggered(ctx,evt);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
|
||||
lossConnectCount = 0;
|
||||
System.out.println("client says: "+msg.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
|
||||
ctx.close();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 客户端heartbeat实现
|
||||
客户端也会将IdleStateHandler作为心跳检测processor提娜佳到pipeline中,并且添加自定义handler作为超时事件处理。
|
||||
|
||||
如下示例将IdleStateHandler的心跳检测设置为了每4s一次,并且自定义handler的`userEventTriggered`方法用于处理write idle的场景,代码示例如下:
|
||||
```java
|
||||
Bootstrap b = new Bootstrap();
|
||||
b.group(group).channel(NioSocketChannel.class)
|
||||
.handler(new ChannelInitializer<SocketChannel>() {
|
||||
@Override
|
||||
protected void initChannel(SocketChannel socketChannel) throws Exception {
|
||||
socketChannel.pipeline().addLast(new IdleStateHandler(0,4,0, TimeUnit.SECONDS));
|
||||
socketChannel.pipeline().addLast(new StringEncoder());
|
||||
socketChannel.pipeline().addLast(new HeartBeatClientHandler());
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
```java
|
||||
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
|
||||
System.out.println("Client cyclic heartbeat monitoring sending: "+new Date());
|
||||
if (evt instanceof IdleStateEvent){
|
||||
IdleStateEvent event = (IdleStateEvent)evt;
|
||||
if (event.state()== IdleState.WRITER_IDLE){
|
||||
if (curTime<beatTime){
|
||||
curTime++;
|
||||
ctx.writeAndFlush("biubiu");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
514
netty/netty-j.md
Normal file
514
netty/netty-j.md
Normal file
@@ -0,0 +1,514 @@
|
||||
# netty
|
||||
## netty解决的问题
|
||||
当前,http协议被广泛使用于client和server之间的通信。但是,部分场景下,http协议并不适用,例如传输大文件、e-mail消息、事实的经济或游戏数据。对于该类场景,http协议并无法很好的满足,此时需要一个高度优化的自定义协议实现,而通过netty可以及进行快速的协议实现。
|
||||
|
||||
netty提供了一个`异步的事件驱动的`网络应用框架,用于快速开发一个拥有高性能、高可拓展性的协议的server和client。
|
||||
|
||||
## netty example
|
||||
### Discard Server Example
|
||||
如下是一个Discard Protocol的样例,其会对所有接收到的数据进行丢弃,并且不返回任何结果。handler方法会处理由netty产生的io事件:
|
||||
```java
|
||||
package io.netty.example.discard;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.ChannelInboundHandlerAdapter;
|
||||
|
||||
/**
|
||||
* Handles a server-side channel.
|
||||
*/
|
||||
public class DiscardServerHandler extends ChannelInboundHandlerAdapter { // (1)
|
||||
|
||||
@Override
|
||||
public void channelRead(ChannelHandlerContext ctx, Object msg) { // (2)
|
||||
// Discard the received data silently.
|
||||
((ByteBuf) msg).release(); // (3)
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { // (4)
|
||||
// Close the connection when an exception is raised.
|
||||
cause.printStackTrace();
|
||||
ctx.close();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
DiscardServerHandler继承了`ChannelInboundHandlerAdapter`,而`ChannelInboundHandlerAdapter`则是实现了`ChannelInboundHandler`。`ChannelInboundHandler`提供了各种不同的io事件处理方法,可以对这些方法进行覆盖。
|
||||
|
||||
在上述示例中,对chnnelRead方法进行了重写,channelRead方法将会在从客户端接收到数据时被调用,被调用时会把从客户端接收到的消息作为参数。在上述示例中,接收到的消息是ByteBuf类型。
|
||||
|
||||
> ByteBuf是一个引用计数对象,必须要通过调用`release`方法来显式释放。并且,`handler method有责任对传入的引用计数对象进行释放操作`。
|
||||
|
||||
通常,channelRead方法的重写按照如下形式:
|
||||
```java
|
||||
@Override
|
||||
public void channelRead(ChannelHandlerContext ctx, Object msg) {
|
||||
try {
|
||||
// Do something with msg
|
||||
} finally {
|
||||
// handler makes sure that msg passed has been released
|
||||
ReferenceCountUtil.release(msg);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
上述示例中除了重写channelRead方法外,还重写了`exceptionCaught`方法。exceptionCaught方法会在 ***netty由于io error抛出异常*** 或是 ***handler method实现在处理事件时抛出异常*** 的场景下被调用。
|
||||
|
||||
在通常情况下,被exceptionCaught方法捕获的异常,其异常信息应该被打印到日志中,且异常关联的channel应该被关闭。通过重写caughtException也可以自定义异常捕获后的实现,例如在关闭channel之前向client发送error code.
|
||||
|
||||
### netty应用程序实现示例
|
||||
如下是netty实现的Discard Server示例:
|
||||
```java
|
||||
package io.netty.example.discard;
|
||||
|
||||
import io.netty.bootstrap.ServerBootstrap;
|
||||
|
||||
import io.netty.channel.ChannelFuture;
|
||||
import io.netty.channel.ChannelInitializer;
|
||||
import io.netty.channel.ChannelOption;
|
||||
import io.netty.channel.EventLoopGroup;
|
||||
import io.netty.channel.nio.NioEventLoopGroup;
|
||||
import io.netty.channel.socket.SocketChannel;
|
||||
import io.netty.channel.socket.nio.NioServerSocketChannel;
|
||||
|
||||
/**
|
||||
* Discards any incoming data.
|
||||
*/
|
||||
public class DiscardServer {
|
||||
|
||||
private int port;
|
||||
|
||||
public DiscardServer(int port) {
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
public void run() throws Exception {
|
||||
EventLoopGroup bossGroup = new NioEventLoopGroup(); // (1)
|
||||
EventLoopGroup workerGroup = new NioEventLoopGroup();
|
||||
try {
|
||||
ServerBootstrap b = new ServerBootstrap(); // (2)
|
||||
b.group(bossGroup, workerGroup)
|
||||
.channel(NioServerSocketChannel.class) // (3)
|
||||
.childHandler(new ChannelInitializer<SocketChannel>() { // (4)
|
||||
@Override
|
||||
public void initChannel(SocketChannel ch) throws Exception {
|
||||
ch.pipeline().addLast(new DiscardServerHandler());
|
||||
}
|
||||
})
|
||||
.option(ChannelOption.SO_BACKLOG, 128) // (5)
|
||||
.childOption(ChannelOption.SO_KEEPALIVE, true); // (6)
|
||||
|
||||
// Bind and start to accept incoming connections.
|
||||
ChannelFuture f = b.bind(port).sync(); // (7)
|
||||
|
||||
// Wait until the server socket is closed.
|
||||
// In this example, this does not happen, but you can do that to gracefully
|
||||
// shut down your server.
|
||||
f.channel().closeFuture().sync();
|
||||
} finally {
|
||||
workerGroup.shutdownGracefully();
|
||||
bossGroup.shutdownGracefully();
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
int port = 8080;
|
||||
if (args.length > 0) {
|
||||
port = Integer.parseInt(args[0]);
|
||||
}
|
||||
|
||||
new DiscardServer(port).run();
|
||||
}
|
||||
}
|
||||
```
|
||||
### 核心组件解析
|
||||
#### NioEventLoopGroup
|
||||
`NioEventLoopGroup`是一个多线程的event loop,用于处理io操作。netty提供了不同的EventLoopGroup实现用于不同种类的传输。
|
||||
|
||||
在上述示例中,使用了两个NioEventLoopGroup:
|
||||
- boss:boss event loop用于接收incoming connections
|
||||
- worker:worker event loop用于处理accepted connection的通信
|
||||
|
||||
每当boss event loop接收到connection之后,其会将新的连接注册到worker event loop。
|
||||
|
||||
event group loop中拥有的线程数,以及线程如何与channel相关联(可能是每个线程对应一个channel、或是一个线程管理多个channel),由EventLoopGroup的实现来决定,并且可以通过构造器来进行配置。
|
||||
|
||||
#### ServerBootstrap
|
||||
ServerBoostrap是一个helper类,用于方便的构建一个netty server。
|
||||
|
||||
#### NioServerSocketChannel
|
||||
在上述使用中,使用了NioServerSocketChannel来实例化一个channel,改channel用于接收incoming connection。
|
||||
|
||||
#### handler注册
|
||||
在上述示例中,为accepted connection注册handler的示例如下所示:
|
||||
```java
|
||||
.childHandler(new ChannelInitializer<SocketChannel>() { // (4)
|
||||
@Override
|
||||
public void initChannel(SocketChannel ch) throws Exception {
|
||||
ch.pipeline().addLast(new DiscardServerHandler());
|
||||
}
|
||||
})
|
||||
```
|
||||
在上述注册示例中,每个newly accepted channel都会调用DiscardServerHandler。
|
||||
|
||||
`ChannelInitializer`是一个特殊的handler,用于帮助用户为新channel配置ahndler。其initChannel为新channel的ChannelPipeline配置添加自定义的handler,从而实现自定义的消息处理逻辑。
|
||||
|
||||
#### 为Channel实现设置参数
|
||||
通过option和childOption方法,可以为channel设置参数。由于netty server基于TCP/IP,故而可以指定socket option例如`tcpNoDelay`和`keepAlive`参数。
|
||||
|
||||
- option:option设置的是NioServerSocketCahnel,该channel用于接收incoming connection
|
||||
- childOption:childOption用于设置被server channel接收的channel,被接收的channel为SocketChannel
|
||||
|
||||
#### 为Channel绑定port
|
||||
在经历完上述配置之后,就可以为server绑定监听端口并启动了。
|
||||
|
||||
## TimeServer Example
|
||||
如下示例展示了一个Time Server,其在连接建立后就会向客户端发送一个32位的integer,并在消息发送完之后关闭连接。该实例并不会接收任何请求。
|
||||
|
||||
```java
|
||||
package io.netty.example.time;
|
||||
|
||||
public class TimeServerHandler extends ChannelInboundHandlerAdapter {
|
||||
|
||||
@Override
|
||||
public void channelActive(final ChannelHandlerContext ctx) { // (1)
|
||||
final ByteBuf time = ctx.alloc().buffer(4); // (2)
|
||||
time.writeInt((int) (System.currentTimeMillis() / 1000L + 2208988800L));
|
||||
|
||||
final ChannelFuture f = ctx.writeAndFlush(time); // (3)
|
||||
f.addListener(new ChannelFutureListener() {
|
||||
@Override
|
||||
public void operationComplete(ChannelFuture future) {
|
||||
assert f == future;
|
||||
ctx.close();
|
||||
}
|
||||
}); // (4)
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
|
||||
cause.printStackTrace();
|
||||
ctx.close();
|
||||
}
|
||||
}
|
||||
```
|
||||
上述示例中,重写了`channelActive`方法,并在回写请求结束后立刻关闭了channel。
|
||||
|
||||
### channelActive
|
||||
channelActive方法会在连接被建立并且可以产生网络通信之后被调用。
|
||||
|
||||
在channelActive实现中,上述示例创建了一个代表当前时间的integer,并且创建了一个容量为4字节的ByteBuf。
|
||||
|
||||
> ByteBuf并没有类似ByteBuffer的flip方法,因为其是双指针的:其中一个pointer属于read操作,另一个属于write操作。
|
||||
|
||||
此外`ChannelHandlerContext.writeAndFlush`方法将会返回一个ChannelFuture类型的返回值。
|
||||
|
||||
而在对`ChannelHandlerContext.close`方法进行调用时,其也不会马上对channel进行关闭,而是会返回一个`ChannelFuture`。
|
||||
|
||||
### ChannelFutureListener
|
||||
可以为ChannelFuture指定一个listener,在上述示例中,为writeAndFlush操作指定了一个操作完成后的监听:
|
||||
```java
|
||||
final ChannelFuture f = ctx.writeAndFlush(time); // (3)
|
||||
f.addListener(new ChannelFutureListener() {
|
||||
@Override
|
||||
public void operationComplete(ChannelFuture future) {
|
||||
assert f == future;
|
||||
ctx.close();
|
||||
}
|
||||
}); // (4)
|
||||
```
|
||||
## TimeClient Example
|
||||
如下示例会实现一个基于上述Time协议的client,和TimeServer不同的是,time client将会使用不同的Bootstrap和Channel实现。
|
||||
|
||||
```java
|
||||
package io.netty.example.time;
|
||||
|
||||
public class TimeClient {
|
||||
public static void main(String[] args) throws Exception {
|
||||
String host = args[0];
|
||||
int port = Integer.parseInt(args[1]);
|
||||
EventLoopGroup workerGroup = new NioEventLoopGroup();
|
||||
|
||||
try {
|
||||
Bootstrap b = new Bootstrap(); // (1)
|
||||
b.group(workerGroup); // (2)
|
||||
b.channel(NioSocketChannel.class); // (3)
|
||||
b.option(ChannelOption.SO_KEEPALIVE, true); // (4)
|
||||
b.handler(new ChannelInitializer<SocketChannel>() {
|
||||
@Override
|
||||
public void initChannel(SocketChannel ch) throws Exception {
|
||||
ch.pipeline().addLast(new TimeClientHandler());
|
||||
}
|
||||
});
|
||||
|
||||
// Start the client.
|
||||
ChannelFuture f = b.connect(host, port).sync(); // (5)
|
||||
|
||||
// Wait until the connection is closed.
|
||||
f.channel().closeFuture().sync();
|
||||
} finally {
|
||||
workerGroup.shutdownGracefully();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
### Bootstrap
|
||||
Bootstrap和ServerBoostrap类似,但是Bootstrap是为了创建non-server channel。
|
||||
|
||||
并且,如果只指定一个EventLoopGroup,那么其将会被同时用作boss和worker,
|
||||
|
||||
### NioSocketChannel
|
||||
相较于time server,time client指定了NioSocketChannel,用于创建client side channel。
|
||||
|
||||
在client side,并不需要指定childOption
|
||||
|
||||
### connect
|
||||
上完成上述配置之后,只需要调用connect即可
|
||||
|
||||
|
||||
在time client实现中,解析time server返回的integer,其逻辑如下所示:
|
||||
```java
|
||||
package io.netty.example.time;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
public class TimeClientHandler extends ChannelInboundHandlerAdapter {
|
||||
@Override
|
||||
public void channelRead(ChannelHandlerContext ctx, Object msg) {
|
||||
ByteBuf m = (ByteBuf) msg; // (1)
|
||||
try {
|
||||
long currentTimeMillis = (m.readUnsignedInt() - 2208988800L) * 1000L;
|
||||
System.out.println(new Date(currentTimeMillis));
|
||||
ctx.close();
|
||||
} finally {
|
||||
m.release();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
|
||||
cause.printStackTrace();
|
||||
ctx.close();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 处理stream-based transport
|
||||
在TCP/IP类似的stream-based transport中,接收到的数据被存储在socket receive buffer中。该buffer中存储的数据是按字节存储的而不是按packet存储的。故而,即使发送方发送了两个独立的packet,操作系统也不会把他们看作是两条消息而是将其看作一系列的字节数据。故而,client端读取到的数据并不一定是server端写入的数据。
|
||||
|
||||
例如,TCP/IP协议栈接收到了三个packet:
|
||||
```
|
||||
ABC DEF GHI
|
||||
```
|
||||
但是由于stream-based protocol的特性,对方有可能按照如下顺序读取到:
|
||||
```
|
||||
AB CDEFG H I
|
||||
```
|
||||
> TCP协议虽然保证了数据的可靠性和有序性,但是其是stream-based协议,传输的基本单位为字节,并不具有packet的概念,无法区分哪些位置的字节属于哪一个packet
|
||||
|
||||
故而,作为数据的接收方,无论是server端还是client端,都需要将接收到的消息转化为一个或多个拥有实际意义的frame。
|
||||
|
||||
接收到的数据应该被分为有意义的frame:
|
||||
```
|
||||
ABC DEF GHI
|
||||
```
|
||||
|
||||
### 解决方案1
|
||||
对于stream-based transport所产生的问题,可以按照如下方案来解决,只有当传输过来的数据达到4字节或以上时,才对接收数据进行处理,在数据量不足以解析一个frame时则不进行处理:
|
||||
```java
|
||||
package io.netty.example.time;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
public class TimeClientHandler extends ChannelInboundHandlerAdapter {
|
||||
private ByteBuf buf;
|
||||
|
||||
@Override
|
||||
public void handlerAdded(ChannelHandlerContext ctx) {
|
||||
buf = ctx.alloc().buffer(4); // (1)
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handlerRemoved(ChannelHandlerContext ctx) {
|
||||
buf.release(); // (1)
|
||||
buf = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelRead(ChannelHandlerContext ctx, Object msg) {
|
||||
ByteBuf m = (ByteBuf) msg;
|
||||
buf.writeBytes(m); // (2)
|
||||
m.release();
|
||||
|
||||
if (buf.readableBytes() >= 4) { // (3)
|
||||
long currentTimeMillis = (buf.readUnsignedInt() - 2208988800L) * 1000L;
|
||||
System.out.println(new Date(currentTimeMillis));
|
||||
ctx.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
|
||||
cause.printStackTrace();
|
||||
ctx.close();
|
||||
}
|
||||
}
|
||||
```
|
||||
#### handlerAdded和handlerRemoved
|
||||
ChannelHandler拥有两个生命周期handler method:`handlerAdded`和`handlerRemoved`。可以执行任意任意的初始化动作。
|
||||
|
||||
在上述实现中,channelRead被调用时,会将数据积累到buf中,如果buf中字节数不足一个frame,则不进行后续处理,等待下一次channelRead调用来积累更多字节数据,直到积累的字节数达到一个frame
|
||||
|
||||
### 解决方案2
|
||||
上述解决方案针对可变长度frame的处理可能不是很理想。
|
||||
|
||||
由于,可以向ChannelPipeline中添加多个ChannelHandler,故而可将单个复杂的ChannelHandler拆分为多个模块化的ChannelHandler来降低程序的复杂性。
|
||||
|
||||
故而,可以将TimeClientHandler拆分为两个handler:
|
||||
- TimeDecoder:处理fragment任务
|
||||
- 解析fragement
|
||||
|
||||
对于数据碎片的处理。netty提供了一个开箱即用的类:
|
||||
```java
|
||||
package io.netty.example.time;
|
||||
|
||||
public class TimeDecoder extends ByteToMessageDecoder { // (1)
|
||||
@Override
|
||||
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) { // (2)
|
||||
if (in.readableBytes() < 4) {
|
||||
return; // (3)
|
||||
}
|
||||
|
||||
out.add(in.readBytes(4)); // (4)
|
||||
}
|
||||
}
|
||||
```
|
||||
#### ByteToMessageDecoder
|
||||
ByteToMessageDecoder是ChannelInBoundHandler的一个实现,可以将数据碎片问题的处理变得更加简单。
|
||||
|
||||
在接收到新消息后,ByteToMessageDecoder会调用decode方法,调用时会包含一个内部维护的积累缓冲区。
|
||||
|
||||
当内部积累缓冲区中的数据尚未达到一个frame时,`ByteToMessageDecoder#decode`方法可以决定什么都不做,当又有新数据到达时,会重新调用decode方法判断内部积累缓冲区中是否有足够的数据。
|
||||
|
||||
如果decode方法向out中追加了一个out,那么其代表decoder成功解析了一条数据,ByteToMessageDecoder会丢弃此次读取的内容(尚未读取的部分继续保留)。
|
||||
|
||||
> 注意,在decode方法的实现中,并不需要decode多条消息,因为decode会被反复调用,直到其在本次调用中未向out写入任何内容
|
||||
|
||||
根据上述介绍,故而可以将ChannelHandler的注册改为如下方式:
|
||||
```java
|
||||
b.handler(new ChannelInitializer<SocketChannel>() {
|
||||
@Override
|
||||
public void initChannel(SocketChannel ch) throws Exception {
|
||||
ch.pipeline().addLast(new TimeDecoder(), new TimeClientHandler());
|
||||
}
|
||||
});
|
||||
```
|
||||
除此之外,netty还提供了许多开箱即用的decoder实现,故而可以简单的实现大多数协议,decoder实现类的路径如下:
|
||||
- io.netty.example.factorial:针对二进制协议
|
||||
- io.netty.example.telnet:针对text line-based协议
|
||||
|
||||
## POJO
|
||||
上述的所有实例,其消息的数据结构都是ByteBuf,在下述示例中,会将消息数据结构改为POJO
|
||||
|
||||
在handler method中使用POJO的优势是明显的,这可以将解析ByteBuf中数据的逻辑从handler method中剥离,这将增加程序的可重用性和可维护性。
|
||||
|
||||
POJO定义如下:
|
||||
```java
|
||||
package io.netty.example.time;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
public class UnixTime {
|
||||
|
||||
private final long value;
|
||||
|
||||
public UnixTime() {
|
||||
this(System.currentTimeMillis() / 1000L + 2208988800L);
|
||||
}
|
||||
|
||||
public UnixTime(long value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public long value() {
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return new Date((value() - 2208988800L) * 1000L).toString();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
之后,就可以更改TimeDecoder的逻辑,来向out中添加一个UnixTime类型的数据而不是ByteBuf
|
||||
```java
|
||||
@Override
|
||||
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
|
||||
if (in.readableBytes() < 4) {
|
||||
return;
|
||||
}
|
||||
|
||||
out.add(new UnixTime(in.readUnsignedInt()));
|
||||
}
|
||||
```
|
||||
TimeClientHandler实现也可以改为如下方式:
|
||||
```java
|
||||
@Override
|
||||
public void channelRead(ChannelHandlerContext ctx, Object msg) {
|
||||
UnixTime m = (UnixTime) msg;
|
||||
System.out.println(m);
|
||||
ctx.close();
|
||||
}
|
||||
```
|
||||
### Encoder
|
||||
除了在对消息进行decode时,可以将二进制数据转化为pojo,在server端向channel中发送数据时,也可以写入POJO,然后再由encoder转化为二进制字节数据。
|
||||
|
||||
发送数据时写入POJO如下:
|
||||
```java
|
||||
@Override
|
||||
public void channelActive(ChannelHandlerContext ctx) {
|
||||
ChannelFuture f = ctx.writeAndFlush(new UnixTime());
|
||||
f.addListener(ChannelFutureListener.CLOSE);
|
||||
}
|
||||
```
|
||||
|
||||
在对消息进行encode时,需要实现ChannelOutboundHandler,其会将POJO转化为ByteBuf
|
||||
|
||||
```java
|
||||
package io.netty.example.time;
|
||||
|
||||
public class TimeEncoder extends ChannelOutboundHandlerAdapter {
|
||||
@Override
|
||||
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) {
|
||||
UnixTime m = (UnixTime) msg;
|
||||
ByteBuf encoded = ctx.alloc().buffer(4);
|
||||
encoded.writeInt((int)m.value());
|
||||
ctx.write(encoded, promise); // (1)
|
||||
}
|
||||
}
|
||||
```
|
||||
在上述实现中,将ChannelPromise原样传递给了ctx.write方法,故而在encoded data被实际写入时,会更新ChannelPromise的状态。
|
||||
|
||||
为了简化上述的流程,可以使用`MessageToByteEncoder`:
|
||||
```java
|
||||
public class TimeEncoder extends MessageToByteEncoder<UnixTime> {
|
||||
@Override
|
||||
protected void encode(ChannelHandlerContext ctx, UnixTime msg, ByteBuf out) {
|
||||
out.writeInt((int)msg.value());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 关闭server
|
||||
关闭netty应用只需要关闭所有创建的EventLoopGroup即可。在关闭EventLoopGroup时,调用`shutdownGracefully`即可,其会返回一个Future,当eventLoopGroup被关闭,并且所有关联的channel也被关闭时,future会得到提醒。
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
598
protobuf/protobuf.md
Normal file
598
protobuf/protobuf.md
Normal file
@@ -0,0 +1,598 @@
|
||||
- [protobuf](#protobuf)
|
||||
- [language guide(proto3)](#language-guideproto3)
|
||||
- [定义message type](#定义message-type)
|
||||
- [Assign Field Numbers](#assign-field-numbers)
|
||||
- [重复使用filed number的后果](#重复使用filed-number的后果)
|
||||
- [指定字段基数](#指定字段基数)
|
||||
- [Singular](#singular)
|
||||
- [repeated](#repeated)
|
||||
- [map](#map)
|
||||
- [Message Type Files Always have Field Presence](#message-type-files-always-have-field-presence)
|
||||
- [well-formed messages](#well-formed-messages)
|
||||
- [在相同`.proto`中定义多个message type](#在相同proto中定义多个message-type)
|
||||
- [删除Fields](#删除fields)
|
||||
- [reversed field number](#reversed-field-number)
|
||||
- [reversed field names](#reversed-field-names)
|
||||
- [what generated from `.proto`](#what-generated-from-proto)
|
||||
- [Scalar Value Types](#scalar-value-types)
|
||||
- [default field values](#default-field-values)
|
||||
- [enumerations](#enumerations)
|
||||
- [enum value alias](#enum-value-alias)
|
||||
- [修改message Type需要遵循的原则](#修改message-type需要遵循的原则)
|
||||
- [Unknown Fields](#unknown-fields)
|
||||
- [unknown fields丢失](#unknown-fields丢失)
|
||||
- [Any](#any)
|
||||
- [OneOf](#oneof)
|
||||
- [OneOf Feature](#oneof-feature)
|
||||
- [向后兼容问题](#向后兼容问题)
|
||||
- [Maps](#maps)
|
||||
- [map feature](#map-feature)
|
||||
- [向后兼容性](#向后兼容性)
|
||||
- [package](#package)
|
||||
- [Service In Rpc System](#service-in-rpc-system)
|
||||
- [gRPC](#grpc)
|
||||
- [json](#json)
|
||||
- [Options](#options)
|
||||
- [消息级别](#消息级别)
|
||||
- [java\_package (file option)](#java_package-file-option)
|
||||
- [java\_outer\_class\_name (file option)](#java_outer_class_name-file-option)
|
||||
- [java\_multiple\_files (file option)](#java_multiple_files-file-option)
|
||||
- [optimize\_for (file option)](#optimize_for-file-option)
|
||||
- [deprecated (field option)](#deprecated-field-option)
|
||||
- [生成代码](#生成代码)
|
||||
- [Encoding](#encoding)
|
||||
- [Simple Message](#simple-message)
|
||||
- [Base 128 varints](#base-128-varints)
|
||||
- [varint编码原理](#varint编码原理)
|
||||
- [消息结构](#消息结构)
|
||||
- [wire type](#wire-type)
|
||||
- [More Integer Types](#more-integer-types)
|
||||
- [Boolean \& Enum](#boolean--enum)
|
||||
- [Length-Delimited Records](#length-delimited-records)
|
||||
- [Sub messages](#sub-messages)
|
||||
- [Optional \& Repeated](#optional--repeated)
|
||||
|
||||
|
||||
# protobuf
|
||||
## language guide(proto3)
|
||||
### 定义message type
|
||||
如下为一个定义search request message format的示例,
|
||||
```proto
|
||||
synatx="proto3"
|
||||
|
||||
message SearchRequest {
|
||||
string query = 1;
|
||||
int32 page_number = 2;
|
||||
int32 results_per_page = 3;
|
||||
}
|
||||
```
|
||||
上述示例含义如下:
|
||||
- `synatx = "proto3"`:
|
||||
- 代表当前protobuf的language版本为`proto3`
|
||||
- 如果没有指定`syntax`,那么protocol buffer compiler默认会假设在使用`proto2`
|
||||
- `Search Request`消息定义了3个fields,每个field都代表`希望包含在message中的一部分数据`
|
||||
|
||||
### Assign Field Numbers
|
||||
可以为message中的每个field定义一个整数,范围为`[1,536,870,911]`,并有如下约束
|
||||
- message中所有field的给定数字必须唯一
|
||||
- field number `[19,000, 19,999]`是为`Protocol Buffer`实现保留的,如果使用这些数字,protocol buffer compiler将会报错
|
||||
|
||||
一旦消息类型被使用后,field number就不能被改变,field number代表message wire format中的field。
|
||||
|
||||
如果对field的field number进行了修改,代表删除旧的field并且新建一个相同类型的field。
|
||||
|
||||
filed number不应该被重用。
|
||||
|
||||
对于`频繁被设置`的fields,应该将其的field number设置为`[1,15]`。在wire format中,field number的值越小占用空间越小。
|
||||
|
||||
> 例如,`[1,15]`在编码时只占用1字节,而`[16, 2047]`则会占用2字节。
|
||||
|
||||
### 重复使用filed number的后果
|
||||
如果重复使用field number,将会造成解码wire-format message的二义性。
|
||||
|
||||
> 对于protobuf wire format,其在编码和解码过程中,`fields的定义`必须一致。
|
||||
|
||||
field number被限制为`29bit`,故而field number的最大值为`536870911`。
|
||||
|
||||
### 指定字段基数
|
||||
在protobuf协议中,field可以为如下的集中类型
|
||||
#### Singular
|
||||
在proto3中,有两种singular field:
|
||||
- `optional`(推荐使用): 一个optional field可能有如下两种状态
|
||||
- 如果optional field值被设置,那么其将会被序列化到`wire`中
|
||||
- 如果optional field值未被设置,那么该field将会返回一个默认值,并且其不会被序列化到wire中
|
||||
- `implict`(不推荐使用):一个隐式字段没有显式基数标签,并且行为如下:
|
||||
- 如果field为一个message type,那么其行为和`optional`相同
|
||||
- 如果field不是message,那么其有两种状态:
|
||||
- 如果field被设置为非默认值(non-zero),其会被序列化到wire中
|
||||
- 如果field被设置为默认值,那么其不会被序列化到wire中
|
||||
|
||||
> 相比于`implict`,更推荐使用`optional`,使用`optional`能更好与proto2相兼容
|
||||
|
||||
> `optional`和`implicit`的区别是,如果scalar field被设置为默认值,在`optional`场景下,其会被序列化到wire中,而`implicit`则不会对其进行序列化
|
||||
|
||||
#### repeated
|
||||
代表该field可以在消息中出现0次或多次,消息出现的顺序也将被维护
|
||||
|
||||
#### map
|
||||
代表field为成对的键值对
|
||||
|
||||
### Message Type Files Always have Field Presence
|
||||
在proto3中,message-type field永远都存在field presence。故而,对于message-type field添加`optional`修饰符并不会改变该field的field presence。
|
||||
|
||||
例如,如下示例中定义的`Message2`和`Message3`对所有的语言都会生成相同的code,并且在binary json、text format格式下如下两种定义的数据展示都不会有任何区别
|
||||
```proto
|
||||
syntax="proto3";
|
||||
|
||||
package foo.bar;
|
||||
|
||||
message Message1 {}
|
||||
|
||||
message Message2 {
|
||||
Message1 foo = 1;
|
||||
}
|
||||
|
||||
message Message3 {
|
||||
optional Message1 bar = 1;
|
||||
}
|
||||
```
|
||||
|
||||
### well-formed messages
|
||||
用`well-formed`来修饰`protobuf message`时,其代表被序列化或反序列化的bytes。在对bytes进行转化时,protoc parser将会校验是否`proto定义文件`是可转化的。
|
||||
|
||||
对于singular field,其可以在`wire-format` bytes中出现多次,parser会接收该输入,但是,在转化过程中,只有field的最后一次出现才有效。
|
||||
|
||||
### 在相同`.proto`中定义多个message type
|
||||
可以在相同`.proto`文件中定义多个message type,示例如下所示:
|
||||
```proto
|
||||
message SearchRequest {
|
||||
string query = 1;
|
||||
int32 page_number = 2;
|
||||
int32 results_per_page = 3;
|
||||
}
|
||||
|
||||
message SearchResponse {
|
||||
...
|
||||
}
|
||||
```
|
||||
> 应尽可能的在每个proto文件中包含较少的message type定义,在同一proto问文件中包含过多message type可能会造成依赖的膨胀。
|
||||
|
||||
### 删除Fields
|
||||
当不再需要某个field,并且所有对该field的references都被从client code中移除时,可以从message type definition中移除该field。`但是,该field对应的field number必须被reserved,防止field number后续被重用`。
|
||||
|
||||
该field对应的field name也应该被reserved,以允许`按json或text-format进行编码`的消息你能偶被正常的转换。
|
||||
|
||||
#### reversed field number
|
||||
在将field注释或删除时,将来使用者可能仍会对field number进行重用。为了避免该问题,可以将被删除field的field number添加到`reversed`列表中,示例如下:
|
||||
```proto
|
||||
message Foo {
|
||||
reversed 2, 15, 9 to 11;
|
||||
}
|
||||
```
|
||||
> 上述示例中,`9 to 11`代表`9,10,11`
|
||||
|
||||
#### reversed field names
|
||||
对被删除field的field name通常是安全的,除非使用TextProc或json的编码格式,在使用这些格式时field name也会参与序列化。为了避免该问题,可以将`deleted field name`添加到`reversed`列表中。
|
||||
|
||||
reversed names只会影响protoc compiler的行为,并不会对rumtime behavior造成影响,但是存在一个例外:
|
||||
- 在parse过程中,TextProto实现会丢弃`reversed`中包含的未知fields,而不会抛出异常
|
||||
- runtime json parse过程不会受到reversed names影响
|
||||
|
||||
使用reversed names示例如下:
|
||||
```proto
|
||||
message Foo {
|
||||
reversed 2, 15, 9 to 11;
|
||||
reversed "foo", "bar";
|
||||
}
|
||||
```
|
||||
> 上述示例中,将field numbers和field names分为了两个`reversed`语句,实际上,可以在同一行`reversed`语句中包含它们
|
||||
|
||||
### what generated from `.proto`
|
||||
当针对`.proto`文件运行`protocol buffer compiler`时,compiler将会生成`所选中编程语言`对应的`和message type进行交互的代码`,生成的交互代码包括如下部分:
|
||||
- getting and setting field values
|
||||
- serialize message to outputstream
|
||||
- parse message from input stream
|
||||
|
||||
对于常用变成语言,其生成文件内容如下:
|
||||
- java:
|
||||
- 对于java,其会为每个message type生成其对应的class
|
||||
- 除了clas外,还会生成对应的Builder类,用于生成class实例
|
||||
- go:
|
||||
- 对于go,其会为每个message type生成`.pb.go`文件
|
||||
|
||||
### Scalar Value Types
|
||||
一个scalar message field可以是如下类型
|
||||
<table><tbody><tr><th>Proto Type</th><th>Notes</th></tr><tr><td>double</td><td></td></tr><tr><td>float</td><td></td></tr><tr><td>int32</td><td>Uses variable-length encoding. Inefficient for encoding negative
|
||||
numbers – if your field is likely to have negative values, use sint32
|
||||
instead.</td></tr><tr><td>int64</td><td>Uses variable-length encoding. Inefficient for encoding negative
|
||||
numbers – if your field is likely to have negative values, use sint64
|
||||
instead.</td></tr><tr><td>uint32</td><td>Uses variable-length encoding.</td></tr><tr><td>uint64</td><td>Uses variable-length encoding.</td></tr><tr><td>sint32</td><td>Uses variable-length encoding. Signed int value. These more
|
||||
efficiently encode negative numbers than regular int32s.</td></tr><tr><td>sint64</td><td>Uses variable-length encoding. Signed int value. These more
|
||||
efficiently encode negative numbers than regular int64s.</td></tr><tr><td>fixed32</td><td>Always four bytes. More efficient than uint32 if values are often
|
||||
greater than 2<sup>28</sup>.</td></tr><tr><td>fixed64</td><td>Always eight bytes. More efficient than uint64 if values are often
|
||||
greater than 2<sup>56</sup>.</td></tr><tr><td>sfixed32</td><td>Always four bytes.</td></tr><tr><td>sfixed64</td><td>Always eight bytes.</td></tr><tr><td>bool</td><td></td></tr><tr><td>string</td><td>A string must always contain UTF-8 encoded or 7-bit ASCII text, and cannot
|
||||
be longer than 2<sup>32</sup>.</td></tr><tr><td>bytes</td><td>May contain any arbitrary sequence of bytes no longer than 2<sup>32</sup>.</td></tr></tbody></table>
|
||||
|
||||
上述scalar type,在各个编程语言中对应的类型如下所示:
|
||||
<table style="width:100%;overflow-x:scroll"><tbody><tr><th>Proto Type</th><th>C++ Type</th><th>Java/Kotlin Type<sup>[1]</sup></th><th>Python Type<sup>[3]</sup></th><th>Go Type</th><th>Ruby Type</th><th>C# Type</th><th>PHP Type</th><th>Dart Type</th><th>Rust Type</th></tr><tr><td>double</td><td>double</td><td>double</td><td>float</td><td>float64</td><td>Float</td><td>double</td><td>float</td><td>double</td><td>f64</td></tr><tr><td>float</td><td>float</td><td>float</td><td>float</td><td>float32</td><td>Float</td><td>float</td><td>float</td><td>double</td><td>f32</td></tr><tr><td>int32</td><td>int32_t</td><td>int</td><td>int</td><td>int32</td><td>Fixnum or Bignum (as required)</td><td>int</td><td>integer</td><td>int</td><td>i32</td></tr><tr><td>int64</td><td>int64_t</td><td>long</td><td>int/long<sup>[4]</sup></td><td>int64</td><td>Bignum</td><td>long</td><td>integer/string<sup>[6]</sup></td><td>Int64</td><td>i64</td></tr><tr><td>uint32</td><td>uint32_t</td><td>int<sup>[2]</sup></td><td>int/long<sup>[4]</sup></td><td>uint32</td><td>Fixnum or Bignum (as required)</td><td>uint</td><td>integer</td><td>int</td><td>u32</td></tr><tr><td>uint64</td><td>uint64_t</td><td>long<sup>[2]</sup></td><td>int/long<sup>[4]</sup></td><td>uint64</td><td>Bignum</td><td>ulong</td><td>integer/string<sup>[6]</sup></td><td>Int64</td><td>u64</td></tr><tr><td>sint32</td><td>int32_t</td><td>int</td><td>int</td><td>int32</td><td>Fixnum or Bignum (as required)</td><td>int</td><td>integer</td><td>int</td><td>i32</td></tr><tr><td>sint64</td><td>int64_t</td><td>long</td><td>int/long<sup>[4]</sup></td><td>int64</td><td>Bignum</td><td>long</td><td>integer/string<sup>[6]</sup></td><td>Int64</td><td>i64</td></tr><tr><td>fixed32</td><td>uint32_t</td><td>int<sup>[2]</sup></td><td>int/long<sup>[4]</sup></td><td>uint32</td><td>Fixnum or Bignum (as required)</td><td>uint</td><td>integer</td><td>int</td><td>u32</td></tr><tr><td>fixed64</td><td>uint64_t</td><td>long<sup>[2]</sup></td><td>int/long<sup>[4]</sup></td><td>uint64</td><td>Bignum</td><td>ulong</td><td>integer/string<sup>[6]</sup></td><td>Int64</td><td>u64</td></tr><tr><td>sfixed32</td><td>int32_t</td><td>int</td><td>int</td><td>int32</td><td>Fixnum or Bignum (as required)</td><td>int</td><td>integer</td><td>int</td><td>i32</td></tr><tr><td>sfixed64</td><td>int64_t</td><td>long</td><td>int/long<sup>[4]</sup></td><td>int64</td><td>Bignum</td><td>long</td><td>integer/string<sup>[6]</sup></td><td>Int64</td><td>i64</td></tr><tr><td>bool</td><td>bool</td><td>boolean</td><td>bool</td><td>bool</td><td>TrueClass/FalseClass</td><td>bool</td><td>boolean</td><td>bool</td><td>bool</td></tr><tr><td>string</td><td>string</td><td>String</td><td>str/unicode<sup>[5]</sup></td><td>string</td><td>String (UTF-8)</td><td>string</td><td>string</td><td>String</td><td>ProtoString</td></tr><tr><td>bytes</td><td>string</td><td>ByteString</td><td>str (Python 2), bytes (Python 3)</td><td>[]byte</td><td>String (ASCII-8BIT)</td><td>ByteString</td><td>string</td><td>List<int></int></td><td>ProtoBytes</td></tr></tbody></table>
|
||||
|
||||
### default field values
|
||||
当message执行反序列化操作时,如果encoded message bytes中并不包含指定的field,那么对反序列化后的对象,访问field时,将会返回一个默认值。
|
||||
|
||||
各种类型的默认值都不一样:
|
||||
- 对于`string`类型,默认值为空字符串
|
||||
- 对于`bytes`,默认值为empty bytes
|
||||
- 对于`bool`类型,默认值为bool
|
||||
- 对于numeric types,默认值为0
|
||||
- 对于message fields,默认值为该field没有被设置
|
||||
- 对于enum,默认值为`first defined enum value`
|
||||
|
||||
对于repeated fields,其默认值为`empty`(empty list)。
|
||||
|
||||
对于map fields,其默认值为emtpy(empty map)。
|
||||
|
||||
> #### implicit-presence
|
||||
> 对于implicit presence scalar fields, 当消息被反序列化后,没有方法区分`该field是被显式设置为default value`还是`该field根本未被设置`。
|
||||
|
||||
### enumerations
|
||||
在proto中定义枚举的示例如下:
|
||||
```proto
|
||||
enum Corpus {
|
||||
CORPUS_UNSPECIFIED = 0;
|
||||
CORPUS_UNIVERSAL = 1;
|
||||
CORPUS_WEB = 2;
|
||||
CORPUS_IMAGES = 3;
|
||||
CORPUS_LOCAL = 4;
|
||||
CORPUS_NEWS = 5;
|
||||
CORPUS_PRODUCTS = 6;
|
||||
CORPUS_VIDEO = 7;
|
||||
}
|
||||
|
||||
message SearchRequest {
|
||||
string query = 1;
|
||||
int32 page_number = 2;
|
||||
int32 results_per_page = 3;
|
||||
Corpus corpus = 4;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
在proto3中,`first value defined in enum`其值必须为0,并且,其名称必须为`{ENUM_TYPE_NAME}_UNSPECIFIED`或`{ENUM_TYPE_NAME}_UNKNOWN`。
|
||||
|
||||
#### enum value alias
|
||||
当开启`allow_alias` option时,允许为相同的枚举值指定不同的枚举项。在反序列化时,所有的alias值都有效,但是只有第一个会被用于反序列化。
|
||||
|
||||
enum alias示例如下:
|
||||
```proto
|
||||
enum EnumAllowingAlias {
|
||||
option allow_alias = true;
|
||||
EAA_UNSPECIFIED = 0;
|
||||
EAA_STARTED = 1;
|
||||
EAA_RUNNING = 1;
|
||||
EAA_FINISHED = 2;
|
||||
}
|
||||
|
||||
enum EnumNotAllowingAlias {
|
||||
ENAA_UNSPECIFIED = 0;
|
||||
ENAA_STARTED = 1;
|
||||
// ENAA_RUNNING = 1; // Uncommenting this line will cause a warning message.
|
||||
ENAA_FINISHED = 2;
|
||||
}
|
||||
```
|
||||
在使用枚举时,可以在一个message type中定义枚举,然后在另一个message type中使用枚举,语法如下` _MessageType_._EnumType_`。
|
||||
|
||||
### 修改message Type需要遵循的原则
|
||||
如果旧的message type不再满足需求,想要添加新的field,且在修改message type后仍想和旧代码保持兼容,则必须要遵守如下原则
|
||||
- 不要修改field number
|
||||
- 在添加新field后,使用旧`meesage type`序列化到的消息格式仍然能被新的`message type`反序列化。同样的,新的`message type`序列化的消息同样能被旧的`message type`定义反序列化
|
||||
- 即是,若`service A`和`service B`通过message type进行通信,`service A`为调用方而`service B`为被调用方,如果`service B`修改了message type定义,向message中添加了field,但是`serivce A`仍然使用的是旧的message定义,那么`service A`使用旧的message type定义序列化的数据,仍然能被`service B`反序列化
|
||||
- 同样的,`service B`使用新的message type定义序列化的新消息数据仍然能被`service A`反序列化,`对于旧的消息定义,其会忽略新添加的字段`
|
||||
- field可以被删除,但是被删除的field,其field number不能被重用。
|
||||
- `int32, uint32, int64, uint64, bool`这些类型的值都是兼容的,代表可以将field的类型从一个修改为另一个,并且不会打破向前或向后兼容。
|
||||
- `sint32和sint64`能够彼此兼容,但是和其他数值类型不相兼容
|
||||
- `string和bytes`能够互相兼容,但要求bytes为有效的utf8字符串
|
||||
- 嵌套的消息能够和bytes相互兼容,但是要求bytes内容为序列化后的message实例
|
||||
- `fixed32和sfixed32`相互兼容,`fixed64`和`sfixed64`相互兼容
|
||||
- 对于`string`, `bytes`,message fields, singular和`repeated`能够相互兼容。
|
||||
- 假如被序列化的数据中包含repeated field,并且client期望该field为singluar,那么在反序列化时
|
||||
- 若field为primitive type,client会取repeated field的最后一个值
|
||||
- 如果field为message type,那么client会对所有的field element进行merge操作
|
||||
- `enum`和`int32, uint32, int64, uint64`能够互相兼容
|
||||
|
||||
### Unknown Fields
|
||||
如果被序列化的数据中包含parser无法识别的fields时,其被称为unknown fields。例如,old parser针对new sender发送的数据进行反序列化时,如果new sender发送的数据中包含new field,那么new
|
||||
field即为unknown field。
|
||||
|
||||
proto3会对unknown fields进行保存,并在序列化和反序列化时包含它们,该行为和proto2一致。
|
||||
|
||||
#### unknown fields丢失
|
||||
一些行为可能会造成unknown fields丢失,示例如下:
|
||||
- 将消息序列化为json
|
||||
- 遍历消息中的field并将其注入给新的message
|
||||
|
||||
为了避免unknown fields的丢失,遵循如下规则:
|
||||
- 使用binary格式,在数据交换时避免使用text-format
|
||||
- 使用message-oriented api来拷贝消息,例如`CopyFrom`或`MergeFrom`,不要使用field-by-field的拷贝方式
|
||||
|
||||
### Any
|
||||
any的使用类似于泛型,允许在使用嵌套类型时无需声明其`.proto`定义,`Any`将会包含如下内容
|
||||
- 任意被序列化为bytes的消息
|
||||
- 一个唯一标识消息类型的url,用于对消息的反序列化
|
||||
|
||||
为了使用Any类型,需要`import google/protobuf/any.proto`,示例如下:
|
||||
```proto
|
||||
import "google/protobuf/any.proto";
|
||||
|
||||
message ErrorStatus {
|
||||
string message = 1;
|
||||
repeated google.protobuf.Any details = 2;
|
||||
}
|
||||
```
|
||||
对于给定消息类型的默认type url为`type.googleapis.com/_packagename_._messagename_.`。
|
||||
|
||||
### OneOf
|
||||
如果消息中包含多个singluar fields,并且在同一时刻最多只能有一个被设置,则可以使用OneOf特性。
|
||||
|
||||
OneOf fields类似于optional fields,但是所有oneof fields都保存在oneof共享内存中,在同一时间最多只能有一个field被设置。`对任一oneof member进行设置都会自动清空其他oneof members`。
|
||||
|
||||
可以通过`cause()或WhichoneOf()`方法来得知哪一个field被设置。
|
||||
|
||||
oneof使用如下所示:
|
||||
```protobuf
|
||||
message SampleMessage {
|
||||
oneof test_oneof {
|
||||
string name = 4;
|
||||
SubMessage sub_message = 9;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
在使用oneof field时,可以向oneof中添加任意类型的field,除了`repeated`和`map`。
|
||||
|
||||
#### OneOf Feature
|
||||
- 当parser对数据进行转换时,如果存在多个oneof member被设置,那么只有最后一个oneof member才会被使用
|
||||
- oneof不能为repeated
|
||||
- 反射api对oneof field也适用
|
||||
|
||||
#### 向后兼容问题
|
||||
在向oneof中添加field时应该要注意,如果oneof的值返回`None/NOT_SET`,其可能代表`oneof没有被设置`或`其有可能被设置,但是设置的member不包含在旧版本的消息类型中`。
|
||||
|
||||
### Maps
|
||||
如果想要创建一个map作为data definition的一部分,可以使用如下语法:
|
||||
```proto
|
||||
map<key_type, value_type> map_field = N;
|
||||
```
|
||||
上述示例中,`key_type`可以是任意整数类型或string类型。`value_type`可以是除了`map`外的任何类型。
|
||||
|
||||
创建示例如下所示;
|
||||
```proto
|
||||
map<string, Project> projects = 3;
|
||||
```
|
||||
#### map feature
|
||||
- map fields 不能被`repeated`修饰
|
||||
- map中值的遍历顺序以及值在format中的顺序是未定义的
|
||||
- 在merge或反序列化过程中,如果存在多个相同的key,那么最后出现的key将会被使用
|
||||
|
||||
#### 向后兼容性
|
||||
map synatx声明等价于如下声明:
|
||||
```proto
|
||||
message MapFieldEntry {
|
||||
key_type key = 1;
|
||||
value_type value = 2;
|
||||
}
|
||||
|
||||
repeated MapFieldEntry map_field = N;
|
||||
```
|
||||
故而,不支持maps的protocol buffer实现也能够接收map数据。
|
||||
|
||||
### package
|
||||
可以在`.proto`文件中指定一个package name,用于避免message type冲突,示例如下:
|
||||
```proto
|
||||
package foo.bar;
|
||||
message Open { ... }
|
||||
```
|
||||
|
||||
```proto
|
||||
message Foo {
|
||||
...
|
||||
foo.bar.Open open = 1;
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
### Service In Rpc System
|
||||
如果想要在rpc系统中使用message type类型,可以在proto文件中定义rpc service interface。protocol buffer compiler将会生成service interface code和stub,示例如下:
|
||||
```proto
|
||||
service SearchService {
|
||||
rpc Search(SearchRequest) returns (SearchResponse);
|
||||
}
|
||||
```
|
||||
#### gRPC
|
||||
与protocol buffer一起使用的最简单的rpc系统为`gRPC`。其是语言和平台中立的,其能够直接通过`.proto`文件产生rpc代码。
|
||||
|
||||
### json
|
||||
`binary wire format`为protobuf的首选格式,但protobuf支持json编码规范。
|
||||
|
||||
### Options
|
||||
`.proto`文件中的声明可以添加options。options并不会改变声明的含义,但是会影响特定上下文下对声明的处理。
|
||||
|
||||
#### 消息级别
|
||||
- 部分option是文件级别的,其应当被写在文件作用域的最上方,而不应该在message type、enum、service之内
|
||||
- 部分option则是message级别的,其应该被写在消息中
|
||||
- 部分option是field级别的,其应该被写在field 定义之内
|
||||
|
||||
常用options如下
|
||||
#### java_package (file option)
|
||||
`java_package` option代表生成classes的package name。如果没有指定该选项,会默认使用`proto`文件的package。
|
||||
|
||||
使用示例如下:
|
||||
```proto
|
||||
option java_package = "com.example.foo";
|
||||
```
|
||||
|
||||
#### java_outer_class_name (file option)
|
||||
生成class文件的outer classname。如果该选项没有指定,默认为`proto`文件的文件名,使用示例如下:
|
||||
```proto
|
||||
option java_outer_classname = "Ponycopter";
|
||||
```
|
||||
|
||||
#### java_multiple_files (file option)
|
||||
如果该选项被指定为false,那么所有`.proto`文件产生的内容都会被嵌套在outer class中。
|
||||
|
||||
如果选项被指定为true,那么会为每个message type单独生成一个java文件。
|
||||
|
||||
该选项默认为false,使用示例如下:
|
||||
```proto
|
||||
option java_multiple_files = true;
|
||||
```
|
||||
|
||||
#### optimize_for (file option)
|
||||
该选项可以被设置为`SPEED, CODE_SIZE, LIFE_RUNTIME`,其将会影响java和c++生成器:
|
||||
- SPEED: SPEED为默认值,protocol buffer compiler将会生成序列化、反序列化、执行其他常用操作的代码。生成的代码是高度优化的
|
||||
- CODE_SIZE: protocol buffer compiler将会生成最小的类代码,类代码中依赖共享、反射等操作来实现序列化、反序列化以及其他常用操作,指定该选项后生成的代码长度要比SPEED小得多,但是操作可能会更慢。
|
||||
- LIFE_RUNTIME
|
||||
|
||||
使用示例如下所示:
|
||||
```proto
|
||||
option optimize_for = CODE_SIZE;
|
||||
```
|
||||
|
||||
#### deprecated (field option)
|
||||
如果该option为true,代表该field已经被废弃并不应该被新代码使用。在java中,其代表生成的代码中field将会被标注为`@Deprecated`。
|
||||
|
||||
使用示例如下所示:
|
||||
```proto
|
||||
int32 old_field = 6 [deprecated = true];
|
||||
```
|
||||
|
||||
### 生成代码
|
||||
在安装完protocol buffer compiler后,可以通过运行`protoc`命令来生成类文件,示例如下:
|
||||
```bash
|
||||
protoc --proto_path=IMPORT_PATH --cpp_out=DST_DIR --java_out=DST_DIR --python_out=DST_DIR --go_out=DST_DIR --ruby_out=DST_DIR --objc_out=DST_DIR --csharp_out=DST_DIR path/to/file.proto
|
||||
```
|
||||
其中,IMPORT_PATH代表当使用import指令时,查找`.proto`文件的路径,如果省略,将会使用当前目录。如果要指定多个目录时,可以传递多个`--proto_path`选项。
|
||||
|
||||
在指定输入时,也可以指定多个proto文件。
|
||||
|
||||
|
||||
## Encoding
|
||||
默认,protobuf会将消息序列化为wire format,wire format定义了如何将消息发送到wire中,并定义了消息占用的空间大小。
|
||||
|
||||
### Simple Message
|
||||
如下是一个简单的消息定义示例:
|
||||
```proto
|
||||
message Test1 {
|
||||
optional int32 a = 1;
|
||||
}
|
||||
```
|
||||
在application code中,可以创建Test1消息实例并将a设置为150,之后将消息序列化到outputstream中。`被序列化之后消息的hex内容为`:
|
||||
```
|
||||
08 96 01
|
||||
```
|
||||
如果使用protoscope tool来转储这些字节,可以得到`1:150`的结果。
|
||||
|
||||
### Base 128 varints
|
||||
可变宽度整数(varints)为wire format的基础。通过varints,可以将unsigned 64bit整数编码为1~10个字节。
|
||||
|
||||
> varint可以对固定长度8字节的64bit unsigned整数进行编码,当64bit整数越小,varint花费的字节数越少。
|
||||
>
|
||||
> varint中每个字节的载荷为7位(取值范围为0~127),故而对64bit整数进行编码时,最多可能需要花费10个字节。`(7 * 9 = 63 < 64)`
|
||||
>
|
||||
> 而对于7位载荷能够代表的数字,例如1,则varint只需要1个字节即能对其进行编码。
|
||||
|
||||
#### varint编码原理
|
||||
对于varint中的每个字节,都含有一个`continuation bit`,代表下一字节是否为varint的一部分,`continuation bit`是字节的`MSB`。
|
||||
|
||||
> 故而,在varint编码产生的结果中,最后一个字节的MSB都是0,而其他字节的MSB都是1,这样可以对varint进行分隔。
|
||||
|
||||
构成varint的每个字节,除了MSB之外的7bit构成了载荷,将所有字节的7bit拼接在一起即是varint代表的整数。
|
||||
|
||||
例如,整数1,其编码为varint后的长度为1字节,内容为`01`,其中`MSB`为0,代表varint只由当前字节构成,7位载荷为`0000001`,代表该varint的值为1。
|
||||
|
||||
对于整数150,其16进制代表为`0x96`,一个字节能表示的最大数为127,故而150需要两个字节来表示,表示的载荷为`1001 0110`,将其分割为7位并由两个字节来表示之后,在大端环境下,编码后字节内容为`1 0000001 0 0010110`,即`0x8116`;在小端环境下,编码后字节内容为`1 0010110 0 0000001`,即`0x9601`。
|
||||
|
||||
> 网络传输默认使用大端字节序,故而网络传输字节内容为`0x9601`
|
||||
|
||||
### 消息结构
|
||||
protocol buffer message由一系列的key/value pairs构成,message的二进制数据,将`field number`作为key,field的类型以及field name只有在decode时参照message type的定义才能得知。
|
||||
|
||||
当消息被编码后,每个key/value pair都会被转化为一条记录,记录中包含`field number, wire type, payload`。其中,`wire type`会告知parser后续payload的大小。
|
||||
|
||||
> 对于old parser,通过wire type可以得知payload的大小,故而old parser可以跳过unknown field。
|
||||
|
||||
#### wire type
|
||||
wire type故而被称为`tag-length-value`,即`TLV`。
|
||||
|
||||
有6中wire type,`varint,i64, len, sgroup, egroup, i32`。
|
||||
|
||||
<table><thead><tr><th>ID</th><th>Name</th><th>Used For</th></tr></thead><tbody><tr><td>0</td><td>VARINT</td><td>int32, int64, uint32, uint64, sint32, sint64, bool, enum</td></tr><tr><td>1</td><td>I64</td><td>fixed64, sfixed64, double</td></tr><tr><td>2</td><td>LEN</td><td>string, bytes, embedded messages, packed repeated fields</td></tr><tr><td>3</td><td>SGROUP</td><td>group start (deprecated)</td></tr><tr><td>4</td><td>EGROUP</td><td>group end (deprecated)</td></tr><tr><td>5</td><td>I32</td><td>fixed32, sfixed32, float</td></tr></tbody></table>
|
||||
|
||||
对于一条记录(field key/value pair), 其`tag`被编码为了一个varint,被编码值的计算公式如下:
|
||||
```
|
||||
(field_number << 3) | wire_type
|
||||
```
|
||||
故而,在对varint类型的tag进行decode操作后,其结果最低三位代表`wire type`,其他部分代表field number。
|
||||
|
||||
故而,可以stream中永远以`varint`数字开头,代表field的`tag`,例如,stream中的第一个字节为`08`
|
||||
```
|
||||
0000 1000
|
||||
```
|
||||
其去掉符号位后,载荷为`0001 000`,后三位代表wire type,值为`0`的wire type代表varint,即field value的类型为varint。前4位代表field number,故而field number的值为`0001`,即`1`。
|
||||
|
||||
故而,`08`tag 代表field number为1,并且field value的类型为varing的field。
|
||||
|
||||
### More Integer Types
|
||||
#### Boolean & Enum
|
||||
bool类型和enum类型的编码方式和`int32`一致,bool值通常会被编码为`00`或`01`。
|
||||
|
||||
#### Length-Delimited Records
|
||||
`length prefixed`为wire format另一个要点。wire type中值为2的类型为`LEN`,其在`tag`后有一个varint类型的动态长度,动态长度之后跟随载荷。
|
||||
|
||||
示例如下:
|
||||
```proto
|
||||
message Test2 {
|
||||
optional string b = 2;
|
||||
}
|
||||
```
|
||||
如果新建一个`Test2`的实例,并且将`b`设置为`testing`,那么其编码后的结果为
|
||||
```
|
||||
12 07 [74 65 73 74 69 6e 67]
|
||||
```
|
||||
解析如下:
|
||||
- 第一个varint值`12`代表tag,其载荷`0010 010`代表field number为`2`,并且wire type为`2`,即`LEN`。
|
||||
- wire type为`LEN`代表tag后跟随一个varint表示field值的长度,而`07`其载荷表示的值为7,代表field value的长度为7,`testing`字符串长度正好为7
|
||||
- 字符串`testing`的utf8编码为`0x74657374696e67`,其正好和后续内容一致
|
||||
|
||||
#### Sub messages
|
||||
对于sub messages类型的记录,其仍使用`LEN` wire type,在`tag`和`length varint`之后,跟随的是sub message编码之后的二进制内容。
|
||||
|
||||
#### Optional & Repeated
|
||||
对于otpional场景,编码时,如果field没有被设置,只需要跳过其即可。
|
||||
|
||||
对于repeated场景,普通(not packed)repeated fields会为field中的每一个元素单独发送一个record,示例如下:
|
||||
|
||||
```proto
|
||||
message Test4 {
|
||||
optional string d = 4;
|
||||
repeated int32 e = 5;
|
||||
}
|
||||
```
|
||||
如果构造一个Test4实例,d为`hello`并且e为`1, 2, 3`,那么,其可以按照如下方式被编码:
|
||||
```
|
||||
4: {"hello"}
|
||||
5: 1
|
||||
5: 2
|
||||
5: 3
|
||||
```
|
||||
|
||||
其中,e的多条record顺序并不需要在一起,可以乱序排放,例如
|
||||
```
|
||||
5: 1
|
||||
5: 2
|
||||
4: {"hello"}
|
||||
5: 3
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
166
python/argparse.md
Normal file
166
python/argparse.md
Normal file
@@ -0,0 +1,166 @@
|
||||
- [argparse](#argparse)
|
||||
- [位置参数](#位置参数)
|
||||
- [可选项参数](#可选项参数)
|
||||
- [store\_true](#store_true)
|
||||
- [短选项](#短选项)
|
||||
- [为可选参数指定type](#为可选参数指定type)
|
||||
- [choice](#choice)
|
||||
- [count](#count)
|
||||
- [add\_mutually\_exclusive\_group](#add_mutually_exclusive_group)
|
||||
- [指定命令行描述](#指定命令行描述)
|
||||
|
||||
|
||||
# argparse
|
||||
通过argparse模块,可以针对命令行参数进行解析。
|
||||
|
||||
## 位置参数
|
||||
```py
|
||||
import argparse
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("echo", help="echo the string you use here")
|
||||
args = parser.parse_args()
|
||||
print(args.echo)
|
||||
```
|
||||
|
||||
## 可选项参数
|
||||
### store_true
|
||||
通过将action指定为`store_true`,当命令行参数中出现该可选参数时,`args.{opt_flag_name}`值为True,否则为False
|
||||
```py
|
||||
import argparse
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--verbose", help="increase output verbosity",
|
||||
action="store_true")
|
||||
args = parser.parse_args()
|
||||
if args.verbose:
|
||||
print("verbosity turned on")
|
||||
```
|
||||
|
||||
### 短选项
|
||||
可以为可选参数指定段选项,令`-v`和`--verbose`达到相同的效果
|
||||
```py
|
||||
import argparse
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("-v", "--verbose", help="increase output verbosity",
|
||||
action="store_true")
|
||||
args = parser.parse_args()
|
||||
if args.verbose:
|
||||
print("verbosity turned on")
|
||||
```
|
||||
|
||||
### 为可选参数指定type
|
||||
为可选参数指定type后,可选参数后必须跟随一个整数值。
|
||||
```py
|
||||
import argparse
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("square", type=int,
|
||||
help="display a square of a given number")
|
||||
parser.add_argument("-v", "--verbosity", type=int,
|
||||
help="increase output verbosity")
|
||||
args = parser.parse_args()
|
||||
answer = args.square**2
|
||||
if args.verbosity == 2:
|
||||
print(f"the square of {args.square} equals {answer}")
|
||||
elif args.verbosity == 1:
|
||||
print(f"{args.square}^2 == {answer}")
|
||||
else:
|
||||
print(answer)
|
||||
```
|
||||
### choice
|
||||
可以通过为可选参数指定`choice`来限定可选参数值范围
|
||||
```py
|
||||
import argparse
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("square", type=int,
|
||||
help="display a square of a given number")
|
||||
parser.add_argument("-v", "--verbosity", type=int, choices=[0, 1, 2],
|
||||
help="increase output verbosity")
|
||||
args = parser.parse_args()
|
||||
answer = args.square**2
|
||||
if args.verbosity == 2:
|
||||
print(f"the square of {args.square} equals {answer}")
|
||||
elif args.verbosity == 1:
|
||||
print(f"{args.square}^2 == {answer}")
|
||||
else:
|
||||
print(answer)
|
||||
```
|
||||
### count
|
||||
可以将可选参数的action指定为`count`,此时`args.{opt_flag_name}`的值将为该可选参数出现的次数,如未指定该可选参数,值为None。
|
||||
|
||||
```py
|
||||
import argparse
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("square", type=int,
|
||||
help="display the square of a given number")
|
||||
parser.add_argument("-v", "--verbosity", action="count",
|
||||
help="increase output verbosity")
|
||||
args = parser.parse_args()
|
||||
answer = args.square**2
|
||||
if args.verbosity == 2:
|
||||
print(f"the square of {args.square} equals {answer}")
|
||||
elif args.verbosity == 1:
|
||||
print(f"{args.square}^2 == {answer}")
|
||||
else:
|
||||
print(answer)
|
||||
```
|
||||
|
||||
如果想要在未指定可选参数时,为该参数指定一个默认值,可以使用`default`:
|
||||
```py
|
||||
import argparse
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("square", type=int,
|
||||
help="display a square of a given number")
|
||||
parser.add_argument("-v", "--verbosity", action="count", default=0,
|
||||
help="increase output verbosity")
|
||||
args = parser.parse_args()
|
||||
answer = args.square**2
|
||||
if args.verbosity >= 2:
|
||||
print(f"the square of {args.square} equals {answer}")
|
||||
elif args.verbosity >= 1:
|
||||
print(f"{args.square}^2 == {answer}")
|
||||
else:
|
||||
print(answer)
|
||||
```
|
||||
|
||||
### add_mutually_exclusive_group
|
||||
通过`add_mutually_exclusive_group`,可以指定彼此之间相互冲突的选项:
|
||||
```py
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
group = parser.add_mutually_exclusive_group()
|
||||
group.add_argument("-v", "--verbose", action="store_true")
|
||||
group.add_argument("-q", "--quiet", action="store_true")
|
||||
parser.add_argument("x", type=int, help="the base")
|
||||
parser.add_argument("y", type=int, help="the exponent")
|
||||
args = parser.parse_args()
|
||||
answer = args.x**args.y
|
||||
|
||||
if args.quiet:
|
||||
print(answer)
|
||||
elif args.verbose:
|
||||
print(f"{args.x} to the power {args.y} equals {answer}")
|
||||
else:
|
||||
print(f"{args.x}^{args.y} == {answer}")
|
||||
```
|
||||
此时,`-v`和`-q`为冲突的可选参数,无法同时指定
|
||||
|
||||
## 指定命令行描述
|
||||
```py
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description="calculate X to the power of Y")
|
||||
group = parser.add_mutually_exclusive_group()
|
||||
group.add_argument("-v", "--verbose", action="store_true")
|
||||
group.add_argument("-q", "--quiet", action="store_true")
|
||||
parser.add_argument("x", type=int, help="the base")
|
||||
parser.add_argument("y", type=int, help="the exponent")
|
||||
args = parser.parse_args()
|
||||
answer = args.x**args.y
|
||||
|
||||
if args.quiet:
|
||||
print(answer)
|
||||
elif args.verbose:
|
||||
print(f"{args.x} to the power {args.y} equals {answer}")
|
||||
else:
|
||||
print(f"{args.x}^{args.y} == {answer}")
|
||||
```
|
||||
541
python/beatifulsoup.md
Normal file
541
python/beatifulsoup.md
Normal file
@@ -0,0 +1,541 @@
|
||||
- [Beautiful Soup](#beautiful-soup)
|
||||
- [安装](#安装)
|
||||
- [构造BeautifulSoup对象](#构造beautifulsoup对象)
|
||||
- [Tag](#tag)
|
||||
- [name](#name)
|
||||
- [attributes](#attributes)
|
||||
- [多值属性](#多值属性)
|
||||
- [NavigableString](#navigablestring)
|
||||
- [文档树遍历](#文档树遍历)
|
||||
- [.contents](#contents)
|
||||
- [.children](#children)
|
||||
- [.descendants](#descendants)
|
||||
- [.string](#string)
|
||||
- [.parent](#parent)
|
||||
- [.parents](#parents)
|
||||
- [.next\_sibling和.previous\_sibling](#next_sibling和previous_sibling)
|
||||
- [.next\_siblings和.previous\_siblings](#next_siblings和previous_siblings)
|
||||
- [搜索文档树](#搜索文档树)
|
||||
- [过滤器类型](#过滤器类型)
|
||||
- [name过滤](#name过滤)
|
||||
- [正则](#正则)
|
||||
- [列表](#列表)
|
||||
- [True](#true)
|
||||
- [自定义方法](#自定义方法)
|
||||
- [属性查找](#属性查找)
|
||||
- [按class进行搜索](#按class进行搜索)
|
||||
- [按string](#按string)
|
||||
- [limit](#limit)
|
||||
- [recursive](#recursive)
|
||||
- [find](#find)
|
||||
- [其他find方法变体](#其他find方法变体)
|
||||
- [css选择器](#css选择器)
|
||||
- [输出](#输出)
|
||||
|
||||
|
||||
# Beautiful Soup
|
||||
## 安装
|
||||
```bash
|
||||
pip install beautifulsoup4
|
||||
```
|
||||
## 构造BeautifulSoup对象
|
||||
```python
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
soup = BeautifulSoup(open("index.html"),'html.parser')
|
||||
|
||||
soup = BeautifulSoup("<html>data</html>",'html.parser')
|
||||
```
|
||||
在调用玩构造方法后,BeautifulSoup会将传入的html文本解析为一个树结构的python对象,python对象存在如下几种类型:
|
||||
- Tag
|
||||
- NavigableString
|
||||
- BeautifulSoup
|
||||
- Comment
|
||||
|
||||
## Tag
|
||||
Tag对象与html文档中的tag相同
|
||||
```py
|
||||
soup = BeautifulSoup('<b class="boldest">Extremely bold</b>','html.parser')
|
||||
tag = soup.b
|
||||
type(tag)
|
||||
# <class 'bs4.element.Tag'>
|
||||
```
|
||||
Tag对象中具有两个重要属性:name和attributes
|
||||
### name
|
||||
每个Tag对象都拥有name属性,可以通过`.name`来获取
|
||||
```py
|
||||
soup = BeautifulSoup('<b class="boldest">Extremely bold</b>', 'html.parser')
|
||||
print(soup.b.name) # b
|
||||
```
|
||||
tag的name属性可以进行修改,如果改变tag的name属性,那么BeautifulSoup对象也会随之修改
|
||||
```py
|
||||
soup = BeautifulSoup('<b class="boldest">Extremely bold</b>', 'html.parser')
|
||||
soup.b.name = 'h1'
|
||||
print(soup)
|
||||
# <h1 class="boldest">Extremely bold</h1>
|
||||
```
|
||||
### attributes
|
||||
一个tag能拥有许多属性,tag属性的获取和字典操作相同:
|
||||
```py
|
||||
soup = BeautifulSoup('<b class="boldest">Extremely bold</b>', 'html.parser')
|
||||
print(soup.b['class']) # ['boldest']
|
||||
print(soup.b.attrs['class']) # ['boldest']
|
||||
```
|
||||
tag的属性同样能在运行时被修改,且tag的属性操作和字典操作相同,运行时能够新增或删除、修改属性,对tag属性的改动会影响BeautifulSoup对象
|
||||
```py
|
||||
soup = BeautifulSoup('<b class="boldest">Extremely bold</b>', 'html.parser')
|
||||
print(soup) # <b class="boldest">Extremely bold</b>
|
||||
soup.b['class'] = 'nvidia'
|
||||
print(soup) # <b class="nvidia">Extremely bold</b>
|
||||
soup.b['id'] = 'amd'
|
||||
print(soup) # <b class="nvidia" id="amd">Extremely bold</b>
|
||||
del soup.b['class']
|
||||
print(soup) # <b id="amd">Extremely bold</b>
|
||||
```
|
||||
### 多值属性
|
||||
html协议中,存在部分属性存在多个值的场景。最常见的多值属性为`rel`,`rev`,`accept-charset`,`headers`,`class`,`accesskey`等。
|
||||
|
||||
在BeautifulSoup中,多值属性的返回类型为list:
|
||||
```py
|
||||
soup = BeautifulSoup('<b class="nvidia amd">Extremely bold</b>', 'html.parser')
|
||||
print(soup.b['class']) # ['nvidia', 'amd']
|
||||
```
|
||||
如果有属性看起来像有多个值,但是html协议中该属性未被定义为多值属性,那么BeautifulSoup会将该属性值作为字符串返回
|
||||
```py
|
||||
soup = BeautifulSoup('<b id="nvidia amd">Extremely bold</b>', 'html.parser')
|
||||
print(soup.b['id']) # nvidia amd
|
||||
```
|
||||
|
||||
## NavigableString
|
||||
NavigableString值通常被嵌套在Tag中,可以通过`tag.string`进行获取
|
||||
```py
|
||||
soup = BeautifulSoup('<b id="nvidia amd">Extremely bold</b>', 'html.parser')
|
||||
print(soup.b.string) # Extremely bold
|
||||
print(type(soup.b.string)) # <class 'bs4.element.NavigableString'>
|
||||
```
|
||||
如果想要将NavigableString转化为unicode字符串,可以调用`unicode`方法
|
||||
```py
|
||||
unicode_string = unicode(tag.string)
|
||||
unicode_string
|
||||
# u'Extremely bold'
|
||||
type(unicode_string)
|
||||
# <type 'unicode'>
|
||||
```
|
||||
如果想要在BeautifulSoup之外使用NavigableString对象,应该将其转为unicode字符串
|
||||
|
||||
## 文档树遍历
|
||||
BeautifulSoup对象中,Tag对象通常有其子节点,可以通过`tag.{child-tag-type}`的形式来获取tag对象第一个`child-tag-type`类型的子节点,示例如下:
|
||||
```py
|
||||
soup = BeautifulSoup('<body><p>i hate nvidia</p><p>nvidia is a piece of shit</p></body>', 'html.parser')
|
||||
print(soup.p) # <p>i hate nvidia</p>
|
||||
```
|
||||
如果想要获取soup对象中所有`child-tag-type`类型的标签,需要调用`find_all`方法。`find_all`将会在整个树结构中寻找指定类型的子节点,不仅包含其直接子节点,还包含间接子节点:
|
||||
```py
|
||||
soup = BeautifulSoup(
|
||||
'<body><p>i hate nvidia</p><p>nvidia is a piece of shit</p><block><p>fuck Jensen Huang</p></block></body>', 'html.parser')
|
||||
print(soup.find_all("p"))
|
||||
# [<p>i hate nvidia</p>, <p>nvidia is a piece of shit</p>, <p>fuck Jensen Huang</p>]
|
||||
```
|
||||
### .contents
|
||||
通过`contents`属性,可以获取直接子节点:
|
||||
```py
|
||||
soup = BeautifulSoup(
|
||||
'<body><p>i hate nvidia</p><p>nvidia is a piece of shit</p><block><p>fuck Jensen Huang</p></block></body>', 'html.parser')
|
||||
print(soup.body.contents)
|
||||
# [<p>i hate nvidia</p>, <p>nvidia is a piece of shit</p>, <block><p>fuck Jensen Huang</p></block>]
|
||||
```
|
||||
### .children
|
||||
`children`属性可以对子节点进行迭代
|
||||
```py
|
||||
soup = BeautifulSoup(
|
||||
'<body><p>i hate nvidia</p><p>nvidia is a piece of shit</p><block><p>fuck Jensen Huang</p></block></body>', 'html.parser')
|
||||
for e in soup.body.children:
|
||||
print(e)
|
||||
# <p>i hate nvidia</p>
|
||||
# <p>nvidia is a piece of shit</p>
|
||||
# <block><p>fuck Jensen Huang</p></block>
|
||||
```
|
||||
### .descendants
|
||||
通过`descendants`属性,可以对所有直接和间接的子节点进行遍历
|
||||
```py
|
||||
soup = BeautifulSoup(
|
||||
'<body><p>i hate nvidia</p><p>nvidia is a piece of shit</p><block><p>fuck Jensen Huang</p></block></body>', 'html.parser')
|
||||
for t in soup.descendants:
|
||||
print(t)
|
||||
```
|
||||
产生的结果为
|
||||
```bash
|
||||
<body><p>i hate nvidia</p><p>nvidia is a piece of shit</p><block><p>fuck Jensen Huang</p></block></body>
|
||||
<p>i hate nvidia</p>
|
||||
i hate nvidia
|
||||
<p>nvidia is a piece of shit</p>
|
||||
nvidia is a piece of shit
|
||||
<block><p>fuck Jensen Huang</p></block>
|
||||
<p>fuck Jensen Huang</p>
|
||||
fuck Jensen Huang
|
||||
```
|
||||
### .string
|
||||
如果tag只有一个NavigableString类型的子节点,那么直接可以通过`string`属性进行访问:
|
||||
```py
|
||||
soup = BeautifulSoup(
|
||||
'<body><p>i hate nvidia</p><p>nvidia is a piece of shit</p><block><p>fuck Jensen Huang</p></block></body>', 'html.parser')
|
||||
print(soup.body.p.string) # i hate nvidia
|
||||
```
|
||||
### .parent
|
||||
通过`parent`属性,可以得到一个节点的直接父节点
|
||||
```py
|
||||
soup = BeautifulSoup(
|
||||
'<body><p>i hate nvidia</p><p>nvidia is a piece of shit</p><block><p>fuck Jensen Huang</p></block></body>', 'html.parser')
|
||||
grand_son_node = soup.body.contents[2].p
|
||||
print(grand_son_node.parent)
|
||||
```
|
||||
输出为
|
||||
```bash
|
||||
<block><p>fuck Jensen Huang</p></block>
|
||||
```
|
||||
### .parents
|
||||
通过`parents`属性,可以得到一个节点的所有父节点:
|
||||
```py
|
||||
soup = BeautifulSoup(
|
||||
'<body><p>i hate nvidia</p><p>nvidia is a piece of shit</p><block><p>fuck Jensen Huang</p></block></body>',
|
||||
'html.parser')
|
||||
grand_son_node = soup.body.contents[2].p
|
||||
i = 0
|
||||
for p in grand_son_node.parents:
|
||||
for j in range(0, i):
|
||||
print("\t", end='')
|
||||
i += 1
|
||||
print(f"type: {type(p)}, content: {p}")
|
||||
```
|
||||
输出为
|
||||
```bash
|
||||
type: <class 'bs4.element.Tag'>, content: <block><p>fuck Jensen Huang</p></block>
|
||||
type: <class 'bs4.element.Tag'>, content: <body><p>i hate nvidia</p><p>nvidia is a piece of shit</p><block><p>fuck Jensen Huang</p></block></body>
|
||||
type: <class 'bs4.BeautifulSoup'>, content: <body><p>i hate nvidia</p><p>nvidia is a piece of shit</p><block><p>fuck Jensen Huang</p></block></body>
|
||||
```
|
||||
|
||||
### .next_sibling和.previous_sibling
|
||||
可以通过`.next_sibling`和`.previous_sibling`来查询兄弟节点
|
||||
```py
|
||||
soup = BeautifulSoup(
|
||||
'<body><p>i hate nvidia</p><p>nvidia is a piece of shit</p><block><p>fuck Jensen Huang</p></block></body>',
|
||||
'html.parser')
|
||||
mid_node = soup.body.contents[1]
|
||||
print(mid_node.previous_sibling) # <p>i hate nvidia</p>
|
||||
print(mid_node.next_sibling) # <block><p>fuck Jensen Huang</p></block>
|
||||
```
|
||||
|
||||
### .next_siblings和.previous_siblings
|
||||
通过`.next_siblings`和`previous_siblings`,可以遍历所有之前和之后的兄弟节点
|
||||
```py
|
||||
soup = BeautifulSoup(
|
||||
'<body><p>i hate nvidia</p><p>nvidia is a piece of shit</p><block><p>fuck Jensen Huang</p></block></body>',
|
||||
'html.parser')
|
||||
mid_node = soup.body.contents[1]
|
||||
print([e for e in mid_node.previous_siblings])
|
||||
print([e for e in mid_node.next_siblings])
|
||||
```
|
||||
输出
|
||||
```bash
|
||||
[<p>i hate nvidia</p>]
|
||||
[<block><p>fuck Jensen Huang</p></block>]
|
||||
```
|
||||
## 搜索文档树
|
||||
### 过滤器类型
|
||||
如果要在文档树中进行查找,过滤器有如下类型:
|
||||
- name
|
||||
- attributes
|
||||
- 字符串
|
||||
|
||||
### name过滤
|
||||
```py
|
||||
soup = BeautifulSoup(
|
||||
'<body><p>i hate nvidia</p><p>nvidia is a piece of shit</p><block><p>fuck Jensen Huang</p></block></body>',
|
||||
'html.parser')
|
||||
print(soup.find_all(name='p'))
|
||||
```
|
||||
输出为
|
||||
```bash
|
||||
[<p>i hate nvidia</p>, <p>nvidia is a piece of shit</p>, <p>fuck Jensen Huang</p>]
|
||||
```
|
||||
#### 正则
|
||||
在根据name查询时,可以适配正则
|
||||
```py
|
||||
soup = BeautifulSoup(
|
||||
'<body><p>i hate nvidia</p><p>nvidia is a piece of shit</p><block><p>fuck Jensen Huang</p></block></body>',
|
||||
'html.parser')
|
||||
print(soup.find_all(name=re.compile("^b")))
|
||||
```
|
||||
上述正则会匹配`body`和`block`标签,查询结果为
|
||||
```bash
|
||||
[<body><p>i hate nvidia</p><p>nvidia is a piece of shit</p><block><p>fuck Jensen Huang</p></block></body>, <block><p>fuck Jensen Huang</p></block>]
|
||||
```
|
||||
#### 列表
|
||||
在根据name查询时,可以传入多个tag类型
|
||||
```py
|
||||
soup = BeautifulSoup(
|
||||
'<body><p>i hate nvidia</p><p>nvidia is a piece of shit</p><block><p>fuck Jensen Huang</p></block></body>',
|
||||
'html.parser')
|
||||
print(soup.find_all(name=['p', 'block']))
|
||||
```
|
||||
其会查询p类型和block类型的tag,输出为
|
||||
```bash
|
||||
[<p>i hate nvidia</p>, <p>nvidia is a piece of shit</p>, <block><p>fuck Jensen Huang</p></block>, <p>fuck Jensen Huang</p>]
|
||||
```
|
||||
|
||||
#### True
|
||||
如果要查询所有的tag,可以向name传入True
|
||||
```py
|
||||
soup = BeautifulSoup(
|
||||
'<body><p>i hate nvidia</p><p>nvidia is a piece of shit</p><block><p>fuck Jensen Huang</p></block></body>',
|
||||
'html.parser')
|
||||
for e in soup.find_all(name=True):
|
||||
print(e)
|
||||
```
|
||||
输出为
|
||||
```bash
|
||||
<body><p>i hate nvidia</p><p>nvidia is a piece of shit</p><block><p>fuck Jensen Huang</p></block></body>
|
||||
<p>i hate nvidia</p>
|
||||
<p>nvidia is a piece of shit</p>
|
||||
<block><p>fuck Jensen Huang</p></block>
|
||||
<p>fuck Jensen Huang</p>
|
||||
```
|
||||
#### 自定义方法
|
||||
除了上述外,还可以自定义过滤方法来对tag对象进行过滤
|
||||
```py
|
||||
def is_match(tag):
|
||||
return tag.name == 'p' and 'id' in tag.attrs and tag.attrs['id'] == '100'
|
||||
|
||||
|
||||
soup = BeautifulSoup(
|
||||
'<body><p id = "200">i hate nvidia</p><p id = "100">nvidia is a piece of shit</p><block><p>fuck Jensen Huang</p></block></body>',
|
||||
'html.parser')
|
||||
for e in soup.find_all(name=is_match):
|
||||
print(e)
|
||||
```
|
||||
|
||||
输出为
|
||||
```bash
|
||||
<p id="100">nvidia is a piece of shit</p>
|
||||
```
|
||||
### 属性查找
|
||||
如果为find_all方法指定了一个命名参数,但是该参数不是find_all方法的内置命名参数,那么会将该参数名称作为属性名称进行查找:
|
||||
```py
|
||||
soup = BeautifulSoup(
|
||||
'<body><p id = "200">i hate nvidia</p><p id = "100">nvidia is a piece of shit</p><block><p>fuck Jensen Huang</p></block></body>',
|
||||
'html.parser')
|
||||
for e in soup.find_all(id="200"):
|
||||
print(e)
|
||||
```
|
||||
上述会查找拥有id属性且id值为200的tag对象,输出为
|
||||
```bash
|
||||
soup = BeautifulSoup(
|
||||
'<body><p id = "200">i hate nvidia</p><p id = "100">nvidia is a piece of shit</p><block><p>fuck Jensen Huang</p></block></body>',
|
||||
'html.parser')
|
||||
for e in soup.find_all(id="200"):
|
||||
print(e)
|
||||
```
|
||||
同样的,根据属性查找也支持正则:
|
||||
```py
|
||||
soup = BeautifulSoup(
|
||||
'<body><p id = "200">i hate nvidia</p><p id = "100">nvidia is a piece of shit</p><block><p>fuck Jensen Huang</p></block></body>',
|
||||
'html.parser')
|
||||
for e in soup.find_all(id=re.compile("^[0-9]{1,}00$")):
|
||||
print(e)
|
||||
```
|
||||
上述会查找拥有id属性并且id值符合正则pattern的tag对象,输出为:
|
||||
```bash
|
||||
<p id="200">i hate nvidia</p>
|
||||
<p id="100">nvidia is a piece of shit</p>
|
||||
```
|
||||
|
||||
根据属性查找,还可以通过向attrs参数传递一个字典:
|
||||
```py
|
||||
soup = BeautifulSoup(
|
||||
'<body><p id = "200">i hate nvidia</p><p id = "100" page="1">nvidia is a piece of shit</p><block id = "100"><p>fuck Jensen Huang</p></block></body>',
|
||||
'html.parser')
|
||||
for e in soup.find_all(attrs={
|
||||
'id': "100",
|
||||
"page": True
|
||||
}):
|
||||
print(e)
|
||||
```
|
||||
此时输出为:
|
||||
```bash
|
||||
<p id="100" page="1">nvidia is a piece of shit</p>
|
||||
```
|
||||
### 按class进行搜索
|
||||
可以通过指定class_来按class进行搜索
|
||||
```py
|
||||
soup = BeautifulSoup(
|
||||
'<body><p id = "200">i hate nvidia</p><p class = "main show">nvidia is a piece of shit</p><block><p>fuck Jensen Huang</p></block></body>',
|
||||
'html.parser')
|
||||
for e in soup.find_all(class_="main"):
|
||||
print(e)
|
||||
```
|
||||
输出为
|
||||
```bash
|
||||
<p class="main show">nvidia is a piece of shit</p>
|
||||
```
|
||||
### 按string
|
||||
指定string后,可以针对html文档中的字符串内容进行搜索,搜索中的元素只会是NavigableString类型
|
||||
```py
|
||||
soup = BeautifulSoup(
|
||||
'<body><p id = "200">i hate nvidia</p><p class = "main show">nvidia is a piece of shit</p><block><p>fuck Jensen Huang</p></block></body>',
|
||||
'html.parser')
|
||||
for e in soup.find_all(string=re.compile("nvidia")):
|
||||
print(f"type: {type(e)}, content: {e}")
|
||||
```
|
||||
输出为:
|
||||
```bash
|
||||
type: <class 'bs4.element.NavigableString'>, content: i hate nvidia
|
||||
type: <class 'bs4.element.NavigableString'>, content: nvidia is a piece of shit
|
||||
```
|
||||
### limit
|
||||
通过limit参数,可以限制搜索的数量,如果文档很大,limit可以降低搜索时间
|
||||
```py
|
||||
soup = BeautifulSoup(
|
||||
'<body><p id = "200">i hate nvidia</p><p class = "main show">nvidia is a piece of shit</p><block><p>fuck Jensen Huang</p></block></body>',
|
||||
'html.parser')
|
||||
for e in soup.find_all(string=re.compile("nvidia"), limit=1):
|
||||
print(f"type: {type(e)}, content: {e}")
|
||||
```
|
||||
此时只会输出一条搜索结果
|
||||
```bash
|
||||
type: <class 'bs4.element.NavigableString'>, content: i hate nvidia
|
||||
```
|
||||
### recursive
|
||||
通过指定recursive参数为`False`,find_all只会搜索当前tag的直接子节点,未指定该参数时默认搜索所有直接子节点和间接子节点:
|
||||
|
||||
### find
|
||||
调用`find_all`方法会返回所有匹配的对象,而如果只想搜索一个对象,可以调用`find`。
|
||||
|
||||
`find`方法等价于`find_all(..., limit=1)`
|
||||
|
||||
> find方法和find_all方法只会搜索当前节点的下级节点,并不包含当前节点本身
|
||||
|
||||
### 其他find方法变体
|
||||
除了find方法和find_all方法外,还包含如下find方法变体:
|
||||
```py
|
||||
find_parents( name , attrs , recursive , string , **kwargs )
|
||||
|
||||
find_parent( name , attrs , recursive , string , **kwargs )
|
||||
|
||||
find_next_siblings( name , attrs , recursive , string , **kwargs )
|
||||
|
||||
find_next_sibling( name , attrs , recursive , string , **kwargs )
|
||||
|
||||
find_previous_siblings( name , attrs , recursive , string , **kwargs )
|
||||
|
||||
find_previous_sibling( name , attrs , recursive , string , **kwargs )
|
||||
|
||||
find_all_next( name , attrs , recursive , string , **kwargs )
|
||||
|
||||
find_next( name , attrs , recursive , string , **kwargs )
|
||||
|
||||
find_all_previous( name , attrs , recursive , string , **kwargs )
|
||||
|
||||
find_previous( name , attrs , recursive , string , **kwargs )
|
||||
```
|
||||
### css选择器
|
||||
通过`select`方法,支持通过css选择器语法查找tag
|
||||
```py
|
||||
soup.select("title")
|
||||
# [<title>The Dormouse's story</title>]
|
||||
|
||||
soup.select("p:nth-of-type(3)")
|
||||
# [<p class="story">...</p>]
|
||||
```
|
||||
逐层查找
|
||||
```py
|
||||
soup.select("body a")
|
||||
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
|
||||
# <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
|
||||
# <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]
|
||||
|
||||
soup.select("html head title")
|
||||
# [<title>The Dormouse's story</title>]
|
||||
```
|
||||
查找直接下级子标签
|
||||
```py
|
||||
soup.select("head > title")
|
||||
# [<title>The Dormouse's story</title>]
|
||||
|
||||
soup.select("p > a")
|
||||
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
|
||||
# <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
|
||||
# <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]
|
||||
|
||||
soup.select("p > a:nth-of-type(2)")
|
||||
# [<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>]
|
||||
|
||||
soup.select("p > #link1")
|
||||
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>]
|
||||
|
||||
soup.select("body > a")
|
||||
# []
|
||||
```
|
||||
css类名查找
|
||||
```py
|
||||
soup.select(".sister")
|
||||
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
|
||||
# <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
|
||||
# <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]
|
||||
|
||||
soup.select("[class~=sister]")
|
||||
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
|
||||
# <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
|
||||
# <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]
|
||||
```
|
||||
查找兄弟节点标签
|
||||
```py
|
||||
soup.select("#link1 ~ .sister")
|
||||
# [<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
|
||||
# <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]
|
||||
|
||||
soup.select("#link1 + .sister")
|
||||
# [<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>]
|
||||
```
|
||||
根据tag的id查找
|
||||
```py
|
||||
soup.select("#link1")
|
||||
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>]
|
||||
|
||||
soup.select("a#link2")
|
||||
# [<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>]
|
||||
```
|
||||
同时用多种css选择器
|
||||
```py
|
||||
soup.select("#link1,#link2")
|
||||
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
|
||||
# <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>]
|
||||
```
|
||||
## 输出
|
||||
可以通过调用`prettify`方法来美化输出:
|
||||
```py
|
||||
markup = '<a href="http://example.com/">I linked to <i>example.com</i></a>'
|
||||
soup = BeautifulSoup(markup)
|
||||
soup.prettify()
|
||||
# '<html>\n <head>\n </head>\n <body>\n <a href="http://example.com/">\n...'
|
||||
|
||||
print(soup.prettify())
|
||||
# <html>
|
||||
# <head>
|
||||
# </head>
|
||||
# <body>
|
||||
# <a href="http://example.com/">
|
||||
# I linked to
|
||||
# <i>
|
||||
# example.com
|
||||
# </i>
|
||||
# </a>
|
||||
# </body>
|
||||
# </html>
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
49
python/py log.md
Normal file
49
python/py log.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# python log
|
||||
## Logging use
|
||||
可以通过`logger = getLogger(__name__)`创建一个logger,之后可以通过logger来访问日志功能。
|
||||
|
||||
针对logger对象,可以调用`debug(), info(), warning(), error(), critical()`方法。
|
||||
|
||||
python中,日志级别及其适用性如下:
|
||||
| 级别 | 实用性 |
|
||||
| :-: | :-: |
|
||||
| DEBUG | 详细信息,通常只在调试时用到 |
|
||||
| INFO | 用于确认应用正在按照预期执行 |
|
||||
| WARNING | 代表存在非预期事件发生,但是应用仍然按期望运行 |
|
||||
| ERROR | 代表应用存在问题,部分功能可能无法正常访问 |
|
||||
| CRITIAL | 严重错误,代表应用不能继续执行 |
|
||||
|
||||
logger默认的隔离级别是`WARNING`, 只有`WARNING`隔离级别或更高的隔离级别,日志才会被输出。该默认隔离级别也可以手动修改。
|
||||
|
||||
## 日志使用简单示例
|
||||
```python
|
||||
import logging
|
||||
logging.warning('Watch out!') # will print a message to the console
|
||||
logging.info('I told you so') # will not print anything
|
||||
```
|
||||
上述示例,其会输出
|
||||
```
|
||||
WARNING:root:Watch out!
|
||||
```
|
||||
> 默认输出的日志隔离级别为WARNING,info信息并不会被输出
|
||||
|
||||
当直接调用`logging`中的方法时,其并没有创建logger对象,方法调用是针对`root logger`对象的。
|
||||
|
||||
## loggging to file
|
||||
将日志输出到文件的示例如下所示:
|
||||
```python
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
logging.basicConfig(filename='example.log', encoding='utf-8', level=logging.DEBUG)
|
||||
logger.debug('This message should go to the log file')
|
||||
logger.info('So should this')
|
||||
logger.warning('And this, too')
|
||||
logger.error('And non-ASCII stuff, too, like Øresund and Malmö')
|
||||
```
|
||||
其输出内容如下所示:
|
||||
```
|
||||
DEBUG:__main__:This message should go to the log file
|
||||
INFO:__main__:So should this
|
||||
WARNING:__main__:And this, too
|
||||
ERROR:__main__:And non-ASCII stuff, too, like Øresund and Malmö
|
||||
```
|
||||
2355
python/py.md
Normal file
2355
python/py.md
Normal file
File diff suppressed because it is too large
Load Diff
575
python/selenium.md
Normal file
575
python/selenium.md
Normal file
@@ -0,0 +1,575 @@
|
||||
- [selenium](#selenium)
|
||||
- [Components](#components)
|
||||
- [Driver](#driver)
|
||||
- [WebDriver](#webdriver)
|
||||
- [Getting Started](#getting-started)
|
||||
- [安装](#安装)
|
||||
- [常用操作](#常用操作)
|
||||
- [开启浏览器会话](#开启浏览器会话)
|
||||
- [通过webdriver执行操作](#通过webdriver执行操作)
|
||||
- [请求浏览器信息](#请求浏览器信息)
|
||||
- [指定等待策略](#指定等待策略)
|
||||
- [获取页面上的元素](#获取页面上的元素)
|
||||
- [针对页面上的元素执行操作](#针对页面上的元素执行操作)
|
||||
- [访问元素上的信息](#访问元素上的信息)
|
||||
- [关闭会话](#关闭会话)
|
||||
- [提交表单样例](#提交表单样例)
|
||||
- [Driver](#driver-1)
|
||||
- [Browser Options](#browser-options)
|
||||
- [browserName](#browsername)
|
||||
- [Driver Service](#driver-service)
|
||||
- [使用默认service实例](#使用默认service实例)
|
||||
- [指定driver port](#指定driver-port)
|
||||
- [Remote Web Driver](#remote-web-driver)
|
||||
- [元素交互](#元素交互)
|
||||
- [文件上传](#文件上传)
|
||||
- [文件定位](#文件定位)
|
||||
- [在整个dom中进行查找](#在整个dom中进行查找)
|
||||
- [基于已定位的元素进行查找](#基于已定位的元素进行查找)
|
||||
- [css选择器](#css选择器)
|
||||
- [查找所有匹配的元素](#查找所有匹配的元素)
|
||||
- [同web元素进行交互](#同web元素进行交互)
|
||||
- [click](#click)
|
||||
- [send\_keys](#send_keys)
|
||||
- [clear](#clear)
|
||||
- [获取元素信息](#获取元素信息)
|
||||
- [is\_displayed](#is_displayed)
|
||||
- [is\_enabled](#is_enabled)
|
||||
- [is\_selected](#is_selected)
|
||||
- [tag\_name](#tag_name)
|
||||
- [元素位置和大小](#元素位置和大小)
|
||||
- [获取css的值](#获取css的值)
|
||||
- [获取文本内容](#获取文本内容)
|
||||
- [获取元素属性](#获取元素属性)
|
||||
- [浏览器交互](#浏览器交互)
|
||||
- [获取当前页签的title](#获取当前页签的title)
|
||||
- [获取当前url](#获取当前url)
|
||||
- [浏览器导航](#浏览器导航)
|
||||
- [跳转到页面](#跳转到页面)
|
||||
- [返回到上一个页签](#返回到上一个页签)
|
||||
- [forward](#forward)
|
||||
- [刷新当前页签](#刷新当前页签)
|
||||
- [alert](#alert)
|
||||
- [confirm](#confirm)
|
||||
- [prompt](#prompt)
|
||||
- [新增cookie](#新增cookie)
|
||||
- [iframe](#iframe)
|
||||
- [winodws/tab 交互](#winodwstab-交互)
|
||||
- [获取当前窗口句柄](#获取当前窗口句柄)
|
||||
- [切换窗口句柄](#切换窗口句柄)
|
||||
- [创建新窗口/tab](#创建新窗口tab)
|
||||
- [关闭tab](#关闭tab)
|
||||
- [关闭会话](#关闭会话-1)
|
||||
- [获取窗口大小](#获取窗口大小)
|
||||
- [设置窗口大小](#设置窗口大小)
|
||||
- [获取窗口位置](#获取窗口位置)
|
||||
- [设置窗口位置](#设置窗口位置)
|
||||
- [最大化窗口](#最大化窗口)
|
||||
- [最小化窗口](#最小化窗口)
|
||||
- [全屏窗口](#全屏窗口)
|
||||
- [截屏](#截屏)
|
||||
- [为元素截屏](#为元素截屏)
|
||||
- [执行js脚本](#执行js脚本)
|
||||
- [打印页面](#打印页面)
|
||||
|
||||
|
||||
# selenium
|
||||
## Components
|
||||
### Driver
|
||||
Driver负责控制实际的浏览器,大多数driver是由浏览器供应商提供的。
|
||||
|
||||
### WebDriver
|
||||
WebDriver通过Driver和浏览器通信,通过driver, webDriver将命令发送给浏览器,并且通过driver收到浏览器的返回信息。
|
||||
|
||||
> Drvier和浏览器运行在同一个操作系统上,WebDriver和`drvier&浏览器`可能并不运行在同一系统上。WebDriver会远程调用driver,driver再调用处于同一系统的浏览器。
|
||||
|
||||
## Getting Started
|
||||
### 安装
|
||||
```bash
|
||||
pip install selenium
|
||||
```
|
||||
### 常用操作
|
||||
#### 开启浏览器会话
|
||||
```py
|
||||
driver = webdriver.Chrome()
|
||||
```
|
||||
#### 通过webdriver执行操作
|
||||
```py
|
||||
driver.get("https://www.selenium.dev/selenium/web/web-form.html")
|
||||
```
|
||||
|
||||
#### 请求浏览器信息
|
||||
```py
|
||||
# driver.title代表浏览器当前页签的标题
|
||||
title = driver.title
|
||||
```
|
||||
|
||||
#### 指定等待策略
|
||||
再获取元素之前,需要确保元素存在于当前页面上,并且与元素进行交互时,必须确保当前元素是可交互的(页签加载需要时间)。
|
||||
|
||||
此时,可以通过自定义等待时间来解决。
|
||||
```py
|
||||
driver.implicitly_wait(0.5)
|
||||
```
|
||||
|
||||
#### 获取页面上的元素
|
||||
```py
|
||||
text_box = driver.find_element(by=By.NAME, value="my-text")
|
||||
submit_button = driver.find_element(by=By.CSS_SELECTOR, value="button")
|
||||
```
|
||||
|
||||
#### 针对页面上的元素执行操作
|
||||
```py
|
||||
text_box.send_keys("Selenium")
|
||||
submit_button.click()
|
||||
```
|
||||
|
||||
#### 访问元素上的信息
|
||||
```py
|
||||
text = message.text
|
||||
```
|
||||
|
||||
#### 关闭会话
|
||||
```py
|
||||
driver.quit()
|
||||
```
|
||||
|
||||
#### 提交表单样例
|
||||
```py
|
||||
from selenium import webdriver
|
||||
from selenium.webdriver.common.by import By
|
||||
|
||||
|
||||
def test_eight_components():
|
||||
driver = webdriver.Chrome()
|
||||
|
||||
driver.get("https://www.selenium.dev/selenium/web/web-form.html")
|
||||
|
||||
title = driver.title
|
||||
assert title == "Web form"
|
||||
|
||||
driver.implicitly_wait(0.5)
|
||||
|
||||
text_box = driver.find_element(by=By.NAME, value="my-text")
|
||||
submit_button = driver.find_element(by=By.CSS_SELECTOR, value="button")
|
||||
|
||||
text_box.send_keys("Selenium")
|
||||
submit_button.click()
|
||||
|
||||
message = driver.find_element(by=By.ID, value="message")
|
||||
value = message.text
|
||||
assert value == "Received!"
|
||||
|
||||
driver.quit()
|
||||
```
|
||||
## Driver
|
||||
开启和关闭session代表着开启和关闭浏览器。
|
||||
|
||||
### Browser Options
|
||||
对于remote driver sessions,需要为其指定options对象,指定的options对象将会决定使用远程的哪个浏览器。
|
||||
|
||||
每个浏览器都会有其独属的额外选项,可以为每个浏览器额外指定。
|
||||
|
||||
#### browserName
|
||||
当使用options对象时,browserName选项会被默认指定:
|
||||
|
||||
### Driver Service
|
||||
Service用于管理local driver的启动和停止,service无法被用于remote webdriver session。
|
||||
|
||||
service允许指定driver的信息,例如driver位置和使用的端口号,也可以通过service指定日志信息。
|
||||
|
||||
#### 使用默认service实例
|
||||
```py
|
||||
service = webdriver.ChromeService()
|
||||
driver = webdriver.Chrome(service=service)
|
||||
```
|
||||
|
||||
#### 指定driver port
|
||||
如果想要driver运行在指定的端口,可以通过service进行指定。
|
||||
```py
|
||||
service = webdriver.ChromeService(port=1234)
|
||||
```
|
||||
|
||||
### Remote Web Driver
|
||||
Selenium允许调用位于远程机器上的浏览器,需要远程机器上安装Selenium Grid。
|
||||
```py
|
||||
options = webdriver.ChromeOptions()
|
||||
driver = webdriver.Remote(command_executor=server, options=options)
|
||||
```
|
||||
|
||||
## 元素交互
|
||||
### 文件上传
|
||||
通过元素的`send_keys`方法,支持支持文件上传。
|
||||
```py
|
||||
file_input = driver.find_element(By.CSS_SELECTOR, "input[type='file']")
|
||||
file_input.send_keys(upload_file)
|
||||
driver.find_element(By.ID, "file-submit").click()
|
||||
```
|
||||
|
||||
### 文件定位
|
||||
#### 在整个dom中进行查找
|
||||
可以通过调用`driver.find_element`来在整个dom中对元素进行查找,其会返回匹配的第一个元素
|
||||
```py
|
||||
vegetable = driver.find_element(By.CLASS_NAME, "tomatoes")
|
||||
```
|
||||
#### 基于已定位的元素进行查找
|
||||
如果想要在已经定位元素的子节点中进行查找,可以调用如下方法:
|
||||
```py
|
||||
fruits = driver.find_element(By.ID, "fruits")
|
||||
fruit = fruits.find_element(By.CLASS_NAME,"tomatoes")
|
||||
```
|
||||
#### css选择器
|
||||
selenium支持通过css选择器的语法来进行查找:
|
||||
```py
|
||||
fruit = driver.find_element(By.CSS_SELECTOR,"#fruits .tomatoes")
|
||||
```
|
||||
#### 查找所有匹配的元素
|
||||
可以通过`find_elements`方法来获取所有匹配的元素,而不是第一个匹配的元素
|
||||
```py
|
||||
plants = driver.find_elements(By.TAG_NAME, "li")
|
||||
```
|
||||
|
||||
### 同web元素进行交互
|
||||
可以针对元素指定如下类型的交互
|
||||
- click(可使用任何元素)
|
||||
- send_keys(只针对文本输入框或内容可编辑的元素)
|
||||
- clear(只针对文本输入框或内容可编辑的元素)
|
||||
- submit(只针对表单元素)
|
||||
- select(只针对下拉框类型元素)
|
||||
|
||||
#### click
|
||||
```py
|
||||
# Navigate to url
|
||||
driver.get("https://www.selenium.dev/selenium/web/inputs.html")
|
||||
|
||||
# Click on the element
|
||||
driver.find_element(By.NAME, "color_input").click()
|
||||
```
|
||||
#### send_keys
|
||||
```py
|
||||
# Navigate to url
|
||||
driver.get("https://www.selenium.dev/selenium/web/inputs.html")
|
||||
|
||||
# Clear field to empty it from any previous data
|
||||
driver.find_element(By.NAME, "email_input").clear()
|
||||
|
||||
# Enter Text
|
||||
driver.find_element(By.NAME, "email_input").send_keys("admin@localhost.dev" )
|
||||
```
|
||||
#### clear
|
||||
```py
|
||||
# Navigate to url
|
||||
driver.get("https://www.selenium.dev/selenium/web/inputs.html")
|
||||
|
||||
# Clear field to empty it from any previous data
|
||||
driver.find_element(By.NAME, "email_input").clear()
|
||||
```
|
||||
### 获取元素信息
|
||||
#### is_displayed
|
||||
```py
|
||||
# Navigate to the url
|
||||
driver.get("https://www.selenium.dev/selenium/web/inputs.html")
|
||||
|
||||
# Get boolean value for is element display
|
||||
is_email_visible = driver.find_element(By.NAME, "email_input").is_displayed()
|
||||
```
|
||||
#### is_enabled
|
||||
```py
|
||||
# Navigate to url
|
||||
driver.get("https://www.selenium.dev/selenium/web/inputs.html")
|
||||
|
||||
# Returns true if element is enabled else returns false
|
||||
value = driver.find_element(By.NAME, 'button_input').is_enabled()
|
||||
```
|
||||
#### is_selected
|
||||
```py
|
||||
# Navigate to url
|
||||
driver.get("https://www.selenium.dev/selenium/web/inputs.html")
|
||||
|
||||
# Returns true if element is checked else returns false
|
||||
value = driver.find_element(By.NAME, "checkbox_input").is_selected()
|
||||
```
|
||||
#### tag_name
|
||||
```py
|
||||
# Navigate to url
|
||||
driver.get("https://www.selenium.dev/selenium/web/inputs.html")
|
||||
|
||||
# Returns TagName of the element
|
||||
attr = driver.find_element(By.NAME, "email_input").tag_name
|
||||
```
|
||||
#### 元素位置和大小
|
||||
```py
|
||||
# Navigate to url
|
||||
driver.get("https://www.selenium.dev/selenium/web/inputs.html")
|
||||
|
||||
# Returns height, width, x and y coordinates referenced element
|
||||
res = driver.find_element(By.NAME, "range_input").rect
|
||||
```
|
||||
#### 获取css的值
|
||||
```py
|
||||
# Navigate to Url
|
||||
driver.get('https://www.selenium.dev/selenium/web/colorPage.html')
|
||||
|
||||
# Retrieves the computed style property 'color' of linktext
|
||||
cssValue = driver.find_element(By.ID, "namedColor").value_of_css_property('background-color')
|
||||
```
|
||||
#### 获取文本内容
|
||||
```py
|
||||
# Navigate to url
|
||||
driver.get("https://www.selenium.dev/selenium/web/linked_image.html")
|
||||
|
||||
# Retrieves the text of the element
|
||||
text = driver.find_element(By.ID, "justanotherlink").text
|
||||
```
|
||||
#### 获取元素属性
|
||||
```py
|
||||
# Navigate to the url
|
||||
driver.get("https://www.selenium.dev/selenium/web/inputs.html")
|
||||
|
||||
# Identify the email text box
|
||||
email_txt = driver.find_element(By.NAME, "email_input")
|
||||
|
||||
# Fetch the value property associated with the textbox
|
||||
value_info = email_txt.get_attribute("value")
|
||||
```
|
||||
## 浏览器交互
|
||||
### 获取当前页签的title
|
||||
```py
|
||||
String title = driver.getTitle();
|
||||
```
|
||||
### 获取当前url
|
||||
```py
|
||||
String url = driver.getCurrentUrl();
|
||||
```
|
||||
### 浏览器导航
|
||||
#### 跳转到页面
|
||||
```py
|
||||
driver.get("https://www.selenium.dev/selenium/web/index.html")
|
||||
```
|
||||
#### 返回到上一个页签
|
||||
```py
|
||||
driver.back()
|
||||
```
|
||||
#### forward
|
||||
```py
|
||||
driver.forward()
|
||||
```
|
||||
#### 刷新当前页签
|
||||
```py
|
||||
driver.refresh()
|
||||
```
|
||||
#### alert
|
||||
```py
|
||||
element = driver.find_element(By.LINK_TEXT, "See an example alert")
|
||||
element.click()
|
||||
|
||||
wait = WebDriverWait(driver, timeout=2)
|
||||
alert = wait.until(lambda d : d.switch_to.alert)
|
||||
text = alert.text
|
||||
# 接受alert
|
||||
alert.accept()
|
||||
```
|
||||
#### confirm
|
||||
```py
|
||||
element = driver.find_element(By.LINK_TEXT, "See a sample confirm")
|
||||
driver.execute_script("arguments[0].click();", element)
|
||||
|
||||
wait = WebDriverWait(driver, timeout=2)
|
||||
alert = wait.until(lambda d : d.switch_to.alert)
|
||||
text = alert.text
|
||||
alert.dismiss()
|
||||
```
|
||||
#### prompt
|
||||
```py
|
||||
element = driver.find_element(By.LINK_TEXT, "See a sample prompt")
|
||||
driver.execute_script("arguments[0].click();", element)
|
||||
|
||||
wait = WebDriverWait(driver, timeout=2)
|
||||
alert = wait.until(lambda d : d.switch_to.alert)
|
||||
alert.send_keys("Selenium")
|
||||
text = alert.text
|
||||
alert.accept()
|
||||
```
|
||||
### 新增cookie
|
||||
```py
|
||||
from selenium import webdriver
|
||||
|
||||
driver = webdriver.Chrome()
|
||||
|
||||
driver.get("http://www.example.com")
|
||||
|
||||
# Adds the cookie into current browser context
|
||||
driver.add_cookie({"name": "key", "value": "value"})
|
||||
```
|
||||
### iframe
|
||||
想要与iframe中的内容进行交互,需要先移动到iframe区域
|
||||
```py
|
||||
# Store iframe web element
|
||||
iframe = driver.find_element(By.CSS_SELECTOR, "#modal > iframe")
|
||||
|
||||
# switch to selected iframe
|
||||
driver.switch_to.frame(iframe)
|
||||
|
||||
# Now click on button
|
||||
driver.find_element(By.TAG_NAME, 'button').click()
|
||||
```
|
||||
### winodws/tab 交互
|
||||
#### 获取当前窗口句柄
|
||||
```py
|
||||
driver.current_window_handle
|
||||
```
|
||||
#### 切换窗口句柄
|
||||
可以通过`driver.switch_to.window(windows_handle)`来进行切换页签
|
||||
```py
|
||||
from selenium import webdriver
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
from selenium.webdriver.support import expected_conditions as EC
|
||||
|
||||
with webdriver.Firefox() as driver:
|
||||
# Open URL
|
||||
driver.get("https://seleniumhq.github.io")
|
||||
|
||||
# Setup wait for later
|
||||
wait = WebDriverWait(driver, 10)
|
||||
|
||||
# Store the ID of the original window
|
||||
original_window = driver.current_window_handle
|
||||
|
||||
# Check we don't have other windows open already
|
||||
assert len(driver.window_handles) == 1
|
||||
|
||||
# Click the link which opens in a new window
|
||||
driver.find_element(By.LINK_TEXT, "new window").click()
|
||||
|
||||
# Wait for the new window or tab
|
||||
wait.until(EC.number_of_windows_to_be(2))
|
||||
|
||||
# Loop through until we find a new window handle
|
||||
for window_handle in driver.window_handles:
|
||||
if window_handle != original_window:
|
||||
driver.switch_to.window(window_handle)
|
||||
break
|
||||
|
||||
# Wait for the new tab to finish loading content
|
||||
wait.until(EC.title_is("SeleniumHQ Browser Automation"))
|
||||
```
|
||||
#### 创建新窗口/tab
|
||||
```py
|
||||
# Opens a new tab and switches to new tab
|
||||
driver.switch_to.new_window('tab')
|
||||
|
||||
# Opens a new window and switches to new window
|
||||
driver.switch_to.new_window('window')
|
||||
```
|
||||
#### 关闭tab
|
||||
调用`driver.close`会关闭当前的页签
|
||||
```py
|
||||
#Close the tab or window
|
||||
driver.close()
|
||||
|
||||
#Switch back to the old tab or window
|
||||
driver.switch_to.window(original_window)
|
||||
```
|
||||
|
||||
#### 关闭会话
|
||||
当不再使用浏览器会话后,应该调用`driver.quit()`方法。
|
||||
|
||||
如果未调用该方法或是调用该方法失败,将会导致driver进程和端口号一直不被释放。
|
||||
|
||||
|
||||
#### 获取窗口大小
|
||||
```py
|
||||
# Access each dimension individually
|
||||
width = driver.get_window_size().get("width")
|
||||
height = driver.get_window_size().get("height")
|
||||
|
||||
# Or store the dimensions and query them later
|
||||
size = driver.get_window_size()
|
||||
width1 = size.get("width")
|
||||
height1 = size.get("height")
|
||||
```
|
||||
#### 设置窗口大小
|
||||
```py
|
||||
driver.set_window_size(1024, 768)
|
||||
```
|
||||
|
||||
#### 获取窗口位置
|
||||
```py
|
||||
# Access each dimension individually
|
||||
x = driver.get_window_position().get('x')
|
||||
y = driver.get_window_position().get('y')
|
||||
|
||||
# Or store the dimensions and query them later
|
||||
position = driver.get_window_position()
|
||||
x1 = position.get('x')
|
||||
y1 = position.get('y')
|
||||
```
|
||||
|
||||
#### 设置窗口位置
|
||||
```py
|
||||
# Move the window to the top left of the primary monitor
|
||||
driver.set_window_position(0, 0)
|
||||
```
|
||||
#### 最大化窗口
|
||||
```py
|
||||
driver.maximize_window()
|
||||
```
|
||||
|
||||
#### 最小化窗口
|
||||
```py
|
||||
driver.minimize_window()
|
||||
```
|
||||
|
||||
#### 全屏窗口
|
||||
```py
|
||||
driver.fullscreen_window()
|
||||
```
|
||||
#### 截屏
|
||||
```py
|
||||
from selenium import webdriver
|
||||
|
||||
driver = webdriver.Chrome()
|
||||
|
||||
driver.get("http://www.example.com")
|
||||
|
||||
# Returns and base64 encoded string into image
|
||||
driver.save_screenshot('./image.png')
|
||||
|
||||
driver.quit()
|
||||
```
|
||||
|
||||
#### 为元素截屏
|
||||
```py
|
||||
from selenium import webdriver
|
||||
from selenium.webdriver.common.by import By
|
||||
|
||||
driver = webdriver.Chrome()
|
||||
|
||||
driver.get("http://www.example.com")
|
||||
|
||||
ele = driver.find_element(By.CSS_SELECTOR, 'h1')
|
||||
|
||||
# Returns and base64 encoded string into image
|
||||
ele.screenshot('./image.png')
|
||||
|
||||
driver.quit()
|
||||
```
|
||||
#### 执行js脚本
|
||||
```py
|
||||
# Stores the header element
|
||||
header = driver.find_element(By.CSS_SELECTOR, "h1")
|
||||
|
||||
# Executing JavaScript to capture innerText of header element
|
||||
driver.execute_script('return arguments[0].innerText', header)
|
||||
```
|
||||
#### 打印页面
|
||||
```py
|
||||
from selenium.webdriver.common.print_page_options import PrintOptions
|
||||
|
||||
print_options = PrintOptions()
|
||||
print_options.page_ranges = ['1-2']
|
||||
|
||||
driver.get("printPage.html")
|
||||
|
||||
base64code = driver.print_page(print_options)
|
||||
```
|
||||
196
react/next.js.md
Normal file
196
react/next.js.md
Normal file
@@ -0,0 +1,196 @@
|
||||
# next.js
|
||||
## Routing
|
||||
### 创建路由
|
||||
next.js使用基于文件系统的路由,文件路径用于定于路由。
|
||||
每一个文件夹都代表到一个匹配到`URL segment`的`route segment`,为了创建嵌套路由,可以在文件夹中嵌套文件夹。
|
||||
- 例如,app文件夹代表根URL,`app/dashboard/settings`则是代表`/dashboard/settings`
|
||||
- 而一个page.js文件则是让该route segment可以公共访问。
|
||||
|
||||
> 如果app/dashboard/路径下存在文件page.js,则是代表该路径/dashboard可以被公共访问,而另一个路径app/analytics下没有page.js文件,则是代表该路径/analytics不能够被公共访问,故而该路径可以被用于存储组件、样式表、图片和其他文件
|
||||
|
||||
### 创建UI
|
||||
在位每一个route segment创建UI时,会使用特殊的文件规范,其中,page会用于创建route UI,而layout则是被用于创建被多个route所共享的UI。
|
||||
|
||||
### pages和layouts
|
||||
#### pages
|
||||
page UI每个都唯一对应一个route。可以通过在page.js文件中导出组件来定义page UI,定义page.js可以让该路径变得公共可访问。
|
||||
创建page的示例如下:
|
||||
```tsx
|
||||
// app/page.tsx
|
||||
export default function Page() {
|
||||
return <h1>Hello, Next.js!</h1>;
|
||||
}
|
||||
|
||||
// `app/dashboard/page.tsx` is the UI for the `/dashboard` URL
|
||||
export default function Page() {
|
||||
return <h1>Hello, Dashboard Page!</h1>;
|
||||
}
|
||||
```
|
||||
|
||||
pages使用须知:
|
||||
- root layout被应用下所有page共享
|
||||
- 任意root segment可以选择定义自己的layout,该layout会被在该segment下的所有page共享
|
||||
- route中的layout默认是嵌套的,每个parent layout都会使用child prop来包含嵌套layout,
|
||||
|
||||
|
||||
#### layouts
|
||||
layout UI在多个page UI之间被共享。在导航时,layout会保留state,保持交互并且不会被重新渲染。layout也可以嵌套。
|
||||
可以通过在layout.js中导出一个默认react组件来定义一个layout,该组件应该接受一个children prop,并且在渲染时被注入一个child layout或child page。
|
||||
创建layout示例如下:
|
||||
```js
|
||||
export default function DashboardLayout({
|
||||
children, // will be a page or nested layout
|
||||
}) {
|
||||
return (
|
||||
<section>
|
||||
{/* Include shared UI here e.g. a header or sidebar */}
|
||||
<nav></nav>
|
||||
|
||||
{children}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
#### Root Layout(必须)
|
||||
root layout在app目录下被定义,并且应用于所有的route。该layout允许修改从server返回的初始html:
|
||||
```js
|
||||
export default function RootLayout({ children }) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
```
|
||||
root layout使用需要满足如下条件
|
||||
- app目录下必须包含一个root layout
|
||||
- root layout必须要定义`<html>`标签和`<body>`标签,next.js并不会自动创建它们
|
||||
- 如果layout.js存在多层嵌套,例如`/layout.js`和`/about/layout.js`,`/about/asahi/layout.js`都存在,那么嵌套结构将是layout.js->about/layout.js->about/asahi/layout.js->about/asahi/page.js,其中about/asahi/page.js渲染的效果将会包含上层所有的layout
|
||||
|
||||
#### template
|
||||
template和layout相似,在template中也可以包含子page和子layout。
|
||||
|
||||
|
||||
layout和template区别如下:
|
||||
- layout在route跳转时,layout本身维护的state会保持不变
|
||||
- 而template在route跳转时,**对于其每一个children,都会含有一个独立的template实例**,template在多个route进行跳转时,state不会持久化
|
||||
|
||||
> 故而,只有在layout中维护的state才会保持不变,而template中维护的state,在route之间进行跳转时,会清空
|
||||
|
||||
可以通过创建一个template.js文件并且在文件中export default一个组件来定义template
|
||||
```js
|
||||
export default function Template({ children }) {
|
||||
return <div>{children}</div>;
|
||||
}
|
||||
```
|
||||
如果一个route segment同时具有template和layout,那么渲染输出如下所示:
|
||||
```js
|
||||
<Layout>
|
||||
{/* Note that the template is given a unique key. */}
|
||||
<Template key={routeParam}>{children}</Template>
|
||||
</Layout>
|
||||
```
|
||||
|
||||
#### 修改head
|
||||
在app目录中,可以自定义html文件的head标签的属性,例如`title`和`meta`。
|
||||
需要在page.js或layout.js中导出一个metadata的对象:
|
||||
```js
|
||||
export const metadata = {
|
||||
title: 'Next.js',
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
return '...';
|
||||
}
|
||||
```
|
||||
|
||||
### linking and navigating
|
||||
#### Link组件
|
||||
`<Link>`是一个html组件,该组件继承了html`<a>`元素,用于提供routes之间的预取和导航。该组件是react组件之间进行跳转的主要方法。
|
||||
如果要使用Link组件,需要从`next/link`中导入该组件,并且在组件的href属性中写入目标路径:
|
||||
```js
|
||||
import Link from 'next/link';
|
||||
|
||||
export default function Page() {
|
||||
return <Link href="/dashboard">Dashboard</Link>;
|
||||
}
|
||||
```
|
||||
#### 路由到动态的路径
|
||||
如果想要路由到动态的路径,可以按照如下方式:
|
||||
```js
|
||||
// 在路径字符串中,可以通过${}来动态获取值
|
||||
import Link from 'next/link';
|
||||
|
||||
export default function PostList({ posts }) {
|
||||
return (
|
||||
<ul>
|
||||
{posts.map((post) => (
|
||||
<li key={post.id}>
|
||||
<Link href={`/blog/${post.slug}`}>{post.title}</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
```
|
||||
#### 跳转到目标的element
|
||||
默认情况下,Link会跳转到目标route页面的顶部,如果想要跳转到目标route的指定位置,可以在href中加入`#${id}`。
|
||||
```js
|
||||
<Link href="/#hashid" scroll={false}>
|
||||
Scroll to specific id.
|
||||
</Link>
|
||||
```
|
||||
#### useRouter
|
||||
useRouter允许client component在代码中触发路由的跳转,如果要使用useRouter,需要导入`next/navigation`中的`useRouter`:
|
||||
```js
|
||||
'use client';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
export default function Page() {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<button type="button" onClick={() => router.push('/dashboard')}>
|
||||
Dashboard
|
||||
</button>
|
||||
);
|
||||
}
|
||||
```
|
||||
### route group
|
||||
在app文件夹中,通过通过文件路径结构来表示url path,如果想要一个文件夹不影响url path,可以将其标识为route group。
|
||||
|
||||
如果要创建route group,可以用小括号将目录名包起来,形式如下`
|
||||
(foldername)`。
|
||||
|
||||
#### 在不影响url path的情况下组织目录结构
|
||||
为了在不影响url path的情况下组织目录结构,可以通过如下方式来使用route group,其中route group文件夹将会在url path中被忽略:
|
||||
```bash
|
||||
app/(market)/about # 该url对应为/about
|
||||
app/(shop)/account # 该url对应为/account
|
||||
```
|
||||
> 即使在使用route group时,(market)和(shop)文件夹下共享同一路径'/',但是可以在(market)目录和(shop)目录下创建不同的layout.js文件
|
||||
|
||||
可以通过在(market)和(shop)下创建不同的layout.js,位于(market)下的组件共享makretLayout,位于(shop)下的组件共享shopLayout。
|
||||
|
||||
同理,可以通过route group创建多个root layout。
|
||||
|
||||
### 动态路由
|
||||
当不知道确切的segment name,并且想要根据动态数据来创建route segment,可以使用动态路由。
|
||||
|
||||
可以通过将文件夹名称通过`[]`包围起来来创建动态路由,例如`app/blog/[slug]/page.js`。
|
||||
动态数据将会被作为params参数传入,动态路由使用如下所示:
|
||||
```js
|
||||
// app/blog/[slug]/page.js
|
||||
// params:{ slug: 'a' } url:/blog/a
|
||||
// params:{ slug: 'b' } url:/blog/b
|
||||
export default function Page({ params }) {
|
||||
return <div>My Post</div>;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
191
react/react.md
Normal file
191
react/react.md
Normal file
@@ -0,0 +1,191 @@
|
||||
# React
|
||||
## 概览
|
||||
React是一个声明式,用于构建用户界面的JavaScript库。React可以将一些简短、独立的代码片段组合成复杂的ui界面,**独立的代码片段在React中被称之为组件**。
|
||||
## React.Component组件
|
||||
```js
|
||||
class ShoppingList extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<div className="shopping-list">
|
||||
<h1>Shopping List for {this.props.name}</h1>
|
||||
<ul>
|
||||
<li>Instagram</li>
|
||||
<li>WhatsApp</li>
|
||||
<li>Oculus</li>
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 用法示例: <ShoppingList name="Mark" />
|
||||
```
|
||||
可以通过组件来告诉React需要在屏幕上显示的内容,当数据发生改变时,**React会高效的更新内容并重新渲染组件**。
|
||||
以上,ShoppingList声明了一个React组件类型。React组件类型会接受一些组件参数,该参数称之为`props`,组件还会通过render方法返回显示在屏幕上的视图层次结构。
|
||||
render方法的返回值将会描述希望在屏幕上看到的内容。render返回值是一个React元素,是对渲染元素的轻量级描述。`jsx`语法支持更简单的书写这些结构,语法\</div\>会被编译为如下内容:
|
||||
```js
|
||||
return React.createElement('div', {className: 'shopping-list'},
|
||||
React.createElement('h1', /* ... h1 children ... */),
|
||||
React.createElement('ul', /* ... ul children ... */)
|
||||
);
|
||||
```
|
||||
在jsx中,可以在html元素中使用js表达式,只需要用大括号将js表达式括起来。
|
||||
```js
|
||||
<h1>Shopping List for {this.props.name}</h1>
|
||||
```
|
||||
上述示例中的ShoppingList只渲染了一些内置的DOM组件,如`div`,`li`等,但是也可以组合和渲染自定义的React组件。例如,可以通过`<ShoppingList/>`来表示整个购物清单组件。
|
||||
> 每个组件都是封装好的,并可以单独运行,如此可以通过组合组件来构建复杂代码
|
||||
|
||||
## 组件响应事件及状态
|
||||
### 事件处理函数
|
||||
如果想要为组件指定事件处理函数,可以通过如下方式:
|
||||
```js
|
||||
class Square extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<button className="square" onClick={function() { alert('click'); }}>
|
||||
{this.props.value}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
### 组件状态
|
||||
可以通过在组件构造函数中初始化`this.state`属性来存储组件状态,this.state应被视作组件的私有属性。
|
||||
示例如下:
|
||||
```js
|
||||
class Square extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
value: null,
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<button className="square" onClick={() => alert('click')}>
|
||||
{this.props.value}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
可以通过如下代码来修改和渲染React组件中的state属性:
|
||||
```js
|
||||
class Square extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
value: null,
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<button
|
||||
className="square"
|
||||
onClick={() => this.setState({value: 'X'})}
|
||||
>
|
||||
{this.state.value}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
> ### setState
|
||||
> 每次在组件内调用setState方法时,React都会自动更新其子组件。
|
||||
|
||||
### 组件状态提升
|
||||
如果需要同时获取多个子组件的数据,或者多个组件之间需要通信,此时,可以将子组件的state数据提升至父组件中进行存储。
|
||||
父子组件之间传递数据和处理函数的示例如下:
|
||||
```js
|
||||
// parent component,
|
||||
// pass `value`,`onclick` props to child component
|
||||
class Board extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
squares: Array(9).fill(null),
|
||||
};
|
||||
}
|
||||
|
||||
handleClick(i) {
|
||||
const squares = this.state.squares.slice();
|
||||
squares[i] = 'X';
|
||||
this.setState({squares: squares});
|
||||
}
|
||||
|
||||
renderSquare(i) {
|
||||
return (
|
||||
<Square
|
||||
value={this.state.squares[i]}
|
||||
onClick={() => this.handleClick(i)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const status = 'Next player: X';
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="status">{status}</div>
|
||||
<div className="board-row">
|
||||
{this.renderSquare(0)}
|
||||
{this.renderSquare(1)}
|
||||
{this.renderSquare(2)}
|
||||
</div>
|
||||
<div className="board-row">
|
||||
{this.renderSquare(3)}
|
||||
{this.renderSquare(4)}
|
||||
{this.renderSquare(5)}
|
||||
</div>
|
||||
<div className="board-row">
|
||||
{this.renderSquare(6)}
|
||||
{this.renderSquare(7)}
|
||||
{this.renderSquare(8)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// child component
|
||||
class Square extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<button
|
||||
className="square"
|
||||
onClick={() => this.props.onClick()}
|
||||
>
|
||||
{this.props.value}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
### React不可变性
|
||||
相比于直接修改原有数据的属性,直接用新对象替换旧的数据对象是更推荐的做法,其有如下优点:
|
||||
- 如果需要查看属性的历史值或实现撤回功能,那么不可变数据将会令实现相当简单,只要存储历史state对象就可以
|
||||
- 如果使用不可变对象,查看数据是否更改会容易很多,只需将旧对象地址和新对象地址进行比较即可,无需遍历对象结构并比较属性值
|
||||
- 不可变对象可以轻易的发现数据对象是否发生改变,从而确定React是否应该重新渲染
|
||||
|
||||
## 函数组件
|
||||
如果组件中并不包含state状态,且组件只包含render一个方法,可以通过函数来实现其组件,而无需继承React.Component类。
|
||||
通过函数组件替换React.Component组件的示例如下:
|
||||
```js
|
||||
function Square(props) {
|
||||
return (
|
||||
<button className="square" onClick={props.onClick}>
|
||||
{props.value}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
```
|
||||
## React list
|
||||
在React中使用列表时,需要为列表项指定一个key,以区分该列表项和其他兄弟列表项。
|
||||
`key`是React中一个特殊的保留属性,当React元素被创建时,React会获取key属性并且将其存储到返回的元素中。元素的`key`属性并无法通过`this.props.key`形式来获取,React会通过`key`自动判断哪些组件需要被更新,但是组件无法访问它的`key`。
|
||||
> 组件的key并不需要在全局保证唯一,只需要在当前同一级元素之中key保持唯一即可
|
||||
|
||||
|
||||
552
react/react文档.md
Normal file
552
react/react文档.md
Normal file
@@ -0,0 +1,552 @@
|
||||
# 快速入门
|
||||
## 创建和嵌套组件
|
||||
React应用由组件组成,每个组件都是UI的一部分,组件拥有自己的外观和逻辑。
|
||||
### 组件是返回标签的js函数
|
||||
```js
|
||||
function MyButton() {
|
||||
return (
|
||||
<button>I'm a button</button>
|
||||
);
|
||||
}
|
||||
```
|
||||
上述已经声明了MyButton组件,可以将该组件嵌套到另一个组件中
|
||||
```js
|
||||
export default function MyApp() {
|
||||
return (
|
||||
<div>
|
||||
<h1>Welcome to my app</h1>
|
||||
<MyButton />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
> react组件必须以大写字母开头,而html组件则是必须以小写字母开头,可以通过组件开头字母的大小写来区分html组件和react组件
|
||||
|
||||
## jsx编写标签
|
||||
上述返回标签的语法被称为jsx,大多数react项目都支持jsx。
|
||||
jsx比html更加严格:
|
||||
1. 所有的标签都要有闭合标签(例如`<br/>`)
|
||||
2. 组件不能返回多个标签,只能返回一个标签,如果存在多个,必须将其包含到一个公共的父级`<div>`或`<>`中
|
||||
```js
|
||||
function AboutPage() {
|
||||
return (
|
||||
<>
|
||||
<h1>About</h1>
|
||||
<p>Hello there.<br />How do you do?</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## 添加样式
|
||||
React中,可以在标签中添加`className`属性来添加样式,其和html中的`class`工作方式相同
|
||||
```html
|
||||
<img className="avatar" />
|
||||
```
|
||||
```css
|
||||
/* In your CSS */
|
||||
.avatar {
|
||||
border-radius: 50%;
|
||||
}
|
||||
```
|
||||
## 显示数据
|
||||
在jsx中,标签位于js中,而可以在标签内通过`{}`来计算js表达式并将其填充到标签中
|
||||
```js
|
||||
return (
|
||||
<h1>
|
||||
{user.name}
|
||||
</h1>
|
||||
);
|
||||
```
|
||||
jsx还可以将表达式的值传递给标签属性,通过`{}`传递表达式的值
|
||||
```js
|
||||
return (
|
||||
<img
|
||||
className="avatar"
|
||||
src={user.imageUrl}
|
||||
/>
|
||||
);
|
||||
```
|
||||
同时,可以通过js表达式来复制css
|
||||
```js
|
||||
const user = {
|
||||
name: 'Hedy Lamarr',
|
||||
imageUrl: 'https://i.imgur.com/yXOvdOSs.jpg',
|
||||
imageSize: 90,
|
||||
};
|
||||
|
||||
export default function Profile() {
|
||||
return (
|
||||
<>
|
||||
<h1>{user.name}</h1>
|
||||
<img
|
||||
className="avatar"
|
||||
src={user.imageUrl}
|
||||
alt={'Photo of ' + user.name}
|
||||
style={{
|
||||
width: user.imageSize,
|
||||
height: user.imageSize
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
## 条件渲染
|
||||
在React中,没有特殊语法编写条件,故而可以在js中通过if条件引入jsx:
|
||||
```js
|
||||
let content;
|
||||
if (isLoggedIn) {
|
||||
content = <AdminPanel />;
|
||||
} else {
|
||||
content = <LoginForm />;
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
{content}
|
||||
</div>
|
||||
);
|
||||
```
|
||||
也可以使用如下方式:
|
||||
```js
|
||||
<div>
|
||||
{isLoggedIn ? (
|
||||
<AdminPanel />
|
||||
) : (
|
||||
<LoginForm />
|
||||
)}
|
||||
</div>
|
||||
```
|
||||
## 渲染列表
|
||||
可以通过如下方式将js数组渲染为列表
|
||||
```js
|
||||
const products = [
|
||||
{ title: 'Cabbage', id: 1 },
|
||||
{ title: 'Garlic', id: 2 },
|
||||
{ title: 'Apple', id: 3 },
|
||||
];
|
||||
const listItems = products.map(product =>
|
||||
<li key={product.id}>
|
||||
{product.title}
|
||||
</li>
|
||||
);
|
||||
|
||||
return (
|
||||
<ul>{listItems}</ul>
|
||||
);
|
||||
```
|
||||
上述示例中,`<li>`有一个key属性,对于列表中的每个属性,都应该传递给其一个字符串或数字的key,用于在兄弟节点之间唯一标识该元素。
|
||||
|
||||
## 响应事件
|
||||
可以在组件中声明事件处理函数来响应事件
|
||||
```js
|
||||
function MyButton() {
|
||||
function handleClick() {
|
||||
alert('You clicked me!');
|
||||
}
|
||||
|
||||
return (
|
||||
<button onClick={handleClick}>
|
||||
Click me
|
||||
</button>
|
||||
);
|
||||
}
|
||||
```
|
||||
## 更新界面
|
||||
如果希望组件维护状态信息,可以通过导入useState来完成
|
||||
```js
|
||||
import { useState } from 'react';
|
||||
|
||||
function MyButton() {
|
||||
const [count, setCount] = useState(0);
|
||||
```
|
||||
其中,count记录当前的状态,而setCount则是用于改变状态的函数,可以为数组中变量取任何名称。
|
||||
```js
|
||||
import { useState } from 'react';
|
||||
|
||||
export default function MyApp() {
|
||||
return (
|
||||
<div>
|
||||
<h1>Counters that update separately</h1>
|
||||
<MyButton />
|
||||
<MyButton />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MyButton() {
|
||||
const [count, setCount] = useState(0);
|
||||
|
||||
function handleClick() {
|
||||
setCount(count + 1);
|
||||
}
|
||||
|
||||
return (
|
||||
<button onClick={handleClick}>
|
||||
Clicked {count} times
|
||||
</button>
|
||||
);
|
||||
}
|
||||
```
|
||||
## 使用hook
|
||||
以`use`开头的函数被称为hook,useState是react提供的一个内置hook。
|
||||
hook比普通函数更为严格,只能在组件顶层或其他hook的顶层调用hook。如果想要在条件或循环中使用hook,请新建一个组件并在组件内部使用。
|
||||
## 组件之间共享数据
|
||||
如果想要将状态在多个组件之间共享,需要将状态提升存储到最近的公共父组件中
|
||||
```js
|
||||
export default function MyApp() {
|
||||
const [count, setCount] = useState(0);
|
||||
|
||||
function handleClick() {
|
||||
setCount(count + 1);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>Counters that update together</h1>
|
||||
<MyButton count={count} onClick={handleClick} />
|
||||
<MyButton count={count} onClick={handleClick} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MyButton({ count, onClick }) {
|
||||
return (
|
||||
<button onClick={onClick}>
|
||||
Clicked {count} times
|
||||
</button>
|
||||
);
|
||||
}
|
||||
```
|
||||
此时,由MyApp传递给MyButton的值称之为prop
|
||||
## jsx展开传递props
|
||||
如果父组件想要将接收到的props全部传递给子组件,无需在子组件上列出props中全部属性,可以使用`...props`来进行展开
|
||||
```js
|
||||
function Profile(props) {
|
||||
return (
|
||||
<div className="card">
|
||||
<Avatar {...props} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
## 将jsx作为子组件传递
|
||||
当采用如下形式时:
|
||||
```js
|
||||
<Card>
|
||||
<Avatar />
|
||||
</Card>
|
||||
```
|
||||
其中,父组件接收到的prop中,children将代表接受到的子组件内容
|
||||
```js
|
||||
import Avatar from './Avatar.js';
|
||||
|
||||
function Card({ children }) {
|
||||
return (
|
||||
<div className="card">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Profile() {
|
||||
return (
|
||||
<Card>
|
||||
<Avatar
|
||||
size={100}
|
||||
person={{
|
||||
name: 'Katsuko Saruhashi',
|
||||
imageId: 'YfeOqp2'
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
```
|
||||
此时,想要嵌套在父组件内部的子组件可以通过props.children来访问。
|
||||
|
||||
## 条件渲染
|
||||
如果在某些条件下,组件不想显示任何内容,可以返回null
|
||||
```js
|
||||
function Item({ name, isPacked }) {
|
||||
if (isPacked) {
|
||||
return null;
|
||||
}
|
||||
return <li className="item">{name}</li>;
|
||||
}
|
||||
```
|
||||
## 组件的渲染和提交
|
||||
### 组件渲染的原因
|
||||
- 组件的初次渲染
|
||||
- 组件(或先祖组件)的状态发生了变化
|
||||
#### 初次渲染
|
||||
当引用启动时,会触发初次渲染
|
||||
#### 状态更新时重新渲染
|
||||
当初次渲染完成后,可以通过set函数来更新state并触发渲染。更新组件状态会将重新渲染加入到队列。
|
||||
|
||||
对于初次渲染,React会调用根部组件的方法来进行渲染,而对于状态更新触发的渲染,react只会调用状态更新对应组件的函数
|
||||
> 对于组件的渲染,其过程是递归的。如果待渲染的组件中包含了子组件,那么react会对子组件进行渲染,对子组件中包含的孙组件也同样。渲染会对组件子树中所有的组件进行渲染。
|
||||
|
||||
#### 渲染差异
|
||||
React只会在渲染之间存在差异时才会更改React节点,如一个节点,如果渲染之间从父组件接受到了不同的props,此时该组件才会发生重新渲染。
|
||||
|
||||
## state快照
|
||||
在渲染函数中,state的快照是不可变的,即使在调用setState函数将更新state后,旧组件中获取的state值仍然没有变化。
|
||||
```js
|
||||
// 即使调用setTitle,此快照中的title值在setTitle之前和之后仍然没有变化
|
||||
// ,而新渲染的组件title初始值则为改变之后的值
|
||||
function Title() {
|
||||
const [title,setTitle]=useState('Morning News');
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>{title}</h1>
|
||||
<button onClick={
|
||||
()=>{
|
||||
alert(title);
|
||||
setTitle(title==='Morning News'?'Evening Newng News':'Morning News');
|
||||
alert(title);
|
||||
}
|
||||
}>Switch</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
## 向setState中传入更新函数
|
||||
react支持向setState中传入更新函数而不是更新后的值,例如
|
||||
```js
|
||||
setNumber(n => n + 1);
|
||||
setNumber(n => n + 1);
|
||||
setNumber(n => n + 1);
|
||||
```
|
||||
此时react会将上述三个函数以此加入到队列中,并在下次渲染时遍历执行队列中的函数。
|
||||
|
||||
## 更新state对象
|
||||
如果向useState中传入object,那么在调用setState函数时,则是应该传入一个新的对象,而不是在原有state对象上进行更新。
|
||||
**应该将state对象看作是不可变的,调用setState时,应该创建一个state对象的副本,并且修改副本对象后用副本对象的值去更新setState方法。**
|
||||
```js
|
||||
// 如果想要修改对象中的某个字段,可以使用如下方法
|
||||
let oldObj = {
|
||||
name : 'kazusa',
|
||||
isMale: false,
|
||||
};
|
||||
let newObj = {
|
||||
// 展开对象中的属性
|
||||
...odlObj,
|
||||
// 新设置某个属性,用于覆盖旧的值
|
||||
name:'mashiro',
|
||||
}
|
||||
```
|
||||
### 修改嵌套对象中的属性
|
||||
```js
|
||||
setPerson({
|
||||
...person, // Copy other fields
|
||||
artwork: { // but replace the artwork
|
||||
...person.artwork, // with the same one
|
||||
city: 'New Delhi' // but in New Delhi!
|
||||
}
|
||||
});
|
||||
```
|
||||
## Immer
|
||||
如果要修改深层嵌套的state值,可以引入Immer库来完成对象的拷贝工作。通过Immer库,可以无需关心对象不可变,而是直接以可变的语法来修改对象属性,而Immer会实际帮忙处理对象不可便的工作。
|
||||
```js
|
||||
// 通过该import代替react中useState
|
||||
import { useImmer } from 'use-immer';
|
||||
|
||||
// 然后可以直接修改对象中的值
|
||||
const [person, updatePerson] = useImmer({
|
||||
name: 'Niki de Saint Phalle',
|
||||
artwork: {
|
||||
title: 'Blue Nana',
|
||||
city: 'Hamburg',
|
||||
image: 'https://i.imgur.com/Sd1AgUOm.jpg',
|
||||
}
|
||||
});
|
||||
|
||||
function handleNameChange(e) {
|
||||
updatePerson(draft => {
|
||||
draft.name = e.target.value;
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## 更新state数组
|
||||
和object一样,在react中数组应该也要是不可变的,在更新state中的值时,也应该再创建一个新的数组,并用新的数组值设置
|
||||
|
||||
## 重新渲染时组件状态的保存
|
||||
只要一个组件还在**UI树中相同的位置**,那么react就会保存其state;如果组件被移除,或另有一个类型的组件被渲染在相同的位置,那么react将会丢弃旧组件的state。
|
||||
如果不同同类型组件被渲染在相同位置,state仍然会被保存。
|
||||
|
||||
### 在UI树结构的同一位置渲染不同组件
|
||||
如果在两次渲染之间,在UI树的同一位置渲染了相同类型的组件,那么默认情况下两次ui组件的state相同的。
|
||||
如果要显式指定两次渲染的组件是不同的,各自有独立的state,可以为两次渲染的组件指定不同的key:
|
||||
```js
|
||||
import { useState } from 'react';
|
||||
|
||||
export default function Scoreboard() {
|
||||
const [isPlayerA, setIsPlayerA] = useState(true);
|
||||
return (
|
||||
<div>
|
||||
{isPlayerA ? (
|
||||
<Counter person="Taylor" />
|
||||
) : (
|
||||
<Counter person="Sarah" />
|
||||
)}
|
||||
<button onClick={() => {
|
||||
setIsPlayerA(!isPlayerA);
|
||||
}}>
|
||||
下一位玩家!
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Counter({ person }) {
|
||||
const [score, setScore] = useState(0);
|
||||
const [hover, setHover] = useState(false);
|
||||
|
||||
let className = 'counter';
|
||||
if (hover) {
|
||||
className += ' hover';
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={className}
|
||||
onPointerEnter={() => setHover(true)}
|
||||
onPointerLeave={() => setHover(false)}
|
||||
>
|
||||
<h1>{person} 的分数:{score}</h1>
|
||||
<button onClick={() => setScore(score + 1)}>
|
||||
加一
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
```
|
||||
> 注意,key并不需要全局唯一,key只用于指定父组件内部的顺序
|
||||
|
||||
### 前后两次组件渲染位于不同UI树位置
|
||||
如果前后两次组件渲染时位于不同的ui树位置,如果要保持前后两次渲染组件state相同,可以为前后两次渲染指定相同的key,那么在前后两次渲染时,即使ui树位置不同,state仍然会延续相同。
|
||||
|
||||
state和树中位置相关,例如
|
||||
```js
|
||||
// 第一次渲染
|
||||
return (
|
||||
<ul>
|
||||
<li key={id1}><Item key="a" /></li>
|
||||
<li key={id2}><Item key="b"/></li>
|
||||
</ul>
|
||||
)
|
||||
// 第二次渲染
|
||||
return (
|
||||
<ul>
|
||||
<li key={id2}><Item key="b"/></li>
|
||||
<li key={id1}><Item key="a"/></li>
|
||||
</ul>
|
||||
)
|
||||
```
|
||||
在上述两次渲染中,`item a`和`item b`父节点`<li>`的顺序虽然发生了改变,但是能够通过`<li>`元素的key属性来区分,故而`item a`和`item b`的state仍然会保持原样。显示效果为列表第一行元素和第二行元素发生了对调。
|
||||
|
||||
## Reducer
|
||||
reducer是一个函数,接受两个参数:
|
||||
- 当前state
|
||||
- action,代表改变状态的参动作,数据结构可以自定义
|
||||
|
||||
reducer方法的返回值是一个新的state,react会将旧的状态设置为新的状态。
|
||||
|
||||
### Reducer使用
|
||||
```js
|
||||
import { useReducer } from 'react';
|
||||
|
||||
// 用于替换useState
|
||||
// useReducer接受两个参数,一个reducer函数,一个state的初始值
|
||||
// 其会返回有状态的值和一个dispatch函数,dispatch函数用于分发action给reducer
|
||||
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
|
||||
|
||||
// 之后可以向dispatch函数提交action
|
||||
function handleAddTask(text) {
|
||||
dispatch({
|
||||
type: 'added',
|
||||
id: nextId++,
|
||||
text: text,
|
||||
});
|
||||
}
|
||||
|
||||
function handleChangeTask(task) {
|
||||
dispatch({
|
||||
type: 'changed',
|
||||
task: task,
|
||||
});
|
||||
}
|
||||
|
||||
// reducer函数会处理action
|
||||
function tasksReducer(tasks, action) {
|
||||
switch (action.type) {
|
||||
case 'added': {
|
||||
return [
|
||||
...tasks,
|
||||
{
|
||||
id: action.id,
|
||||
text: action.text,
|
||||
done: false,
|
||||
},
|
||||
];
|
||||
}
|
||||
case 'changed': {
|
||||
return tasks.map((t) => {
|
||||
if (t.id === action.task.id) {
|
||||
return action.task;
|
||||
} else {
|
||||
return t;
|
||||
}
|
||||
});
|
||||
}
|
||||
case 'deleted': {
|
||||
return tasks.filter((t) => t.id !== action.id);
|
||||
}
|
||||
default: {
|
||||
throw Error('未知 action: ' + action.type);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
## Context
|
||||
如果需要在先祖节点和子孙节点间隔多级时,从先祖节点向子孙节点传递数据,可以使用Context.
|
||||
Context使用如下:
|
||||
### 创建Context
|
||||
```js
|
||||
import { createContext } from 'react';
|
||||
|
||||
export const LevelContext = createContext(1);
|
||||
```
|
||||
### 子孙组件从Context中获取值
|
||||
```js
|
||||
import { useContext } from 'react';
|
||||
import { LevelContext } from './LevelContext.js';
|
||||
export default function Heading({ children }) {
|
||||
const level = useContext(LevelContext);
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### 先祖节点提供Context
|
||||
```js
|
||||
import { LevelContext } from './LevelContext.js';
|
||||
|
||||
export default function Section({ level, children }) {
|
||||
return (
|
||||
<section className="section">
|
||||
<LevelContext.Provider value={level}>
|
||||
{children}
|
||||
</LevelContext.Provider>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
92
regular expression/正则进阶语法.md
Normal file
92
regular expression/正则进阶语法.md
Normal file
@@ -0,0 +1,92 @@
|
||||
# 正则表达式
|
||||
## 语法
|
||||
### No Capturing Group `(?:regex)`
|
||||
如果不想要正则表达式中的group捕获其匹配,可以通过在group的左`(`符号后指定`?:`,其会将group设置为非捕获匹配。
|
||||
```bash
|
||||
# 使用示例如下
|
||||
color=(?:red|green|blue)
|
||||
```
|
||||
|
||||
### Named Caputure Group `(?<name>regex)`
|
||||
在java中,支持named group机制,从而可使用name来获取识捕获的匹配。其可以避免在复杂表达式中通过下表进行匹配时带来的复杂解析问题。
|
||||
|
||||
使用语法为`(?<name>regex)`,使用示例如下所示:
|
||||
```bash
|
||||
[Ww]indows(?<versionTag>95|98|7|8|10|11)
|
||||
```
|
||||
```java
|
||||
public static void testRegex() {
|
||||
Pattern pattern = Pattern.compile("[Ww]indows(?<versionTag>95|98|7|8|10|11)");
|
||||
Matcher matcher = pattern.matcher("I use windows11 Operating System...");
|
||||
if(matcher.find()) {
|
||||
String versionTag = matcher.group("versionTag");
|
||||
System.out.printf("windows version used is %s%n", versionTag);
|
||||
} else {
|
||||
System.out.println("I don't use Windows System");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Atomic Grouping `(?>regex)`
|
||||
在正则表达式中,支持通过`(?>regex)`的语法来指atomic grouping。当指定匹配为atomic grouping后,例如`a(?>bc|b)c`,其会匹配abcc,但是不会匹配abc。
|
||||
|
||||
因为当一个group被指定atomic grouping后,一旦该group成功匹配过一次,那么其会丢弃group中token记录的backtracking position,不会再尝试group中指定的其他匹配。例如在匹配abc时,`(?>bc|b)`表达式和`bc`匹配成功后,atomic grouping即会丢弃回溯位置,后续regex的末尾`c`字符无法匹配,即会导致整个匹配失败,而不会回溯后重新匹配`(?>bc|b)`中的`b`。
|
||||
|
||||
使用示例如下所示:
|
||||
```bash
|
||||
# 其会匹配abcc,但是不会匹配abc
|
||||
a(?>bc|b)c
|
||||
```
|
||||
|
||||
### Negative Lookahead `(?!regex)`
|
||||
通过negative Lookahead语法,可以指定`后面不被...跟随`的正则表达式。例如,`q(?!u)`,其会匹配`aqe`字符串中的`q`。
|
||||
|
||||
注意,`q[^u]`是无法表示和`q(?!u)`一样的效果的,因为`q(?!u)`匹配的是`aqe`字符串中的`q`,而`q[^u]`匹配的则是`aqe`字符串中的`qe`。
|
||||
|
||||
并且,**Negative Lookahead是No Caputure Group**。
|
||||
|
||||
使用示例如下所示:
|
||||
```bash
|
||||
# 其会匹配aqe字符串中的q
|
||||
q(?!u)
|
||||
```
|
||||
|
||||
### Positive Lookahead `(?=regex)`
|
||||
通过positive lookahead的语法,则是可以指定`后面被...跟随`的正则表达式,例如`q(?=u)`。但是,`u并不会成为该regex匹配的一部分。`
|
||||
|
||||
并且,**Negative Lookahead是No Caputure Group**。
|
||||
|
||||
使用示例如下所示:
|
||||
```bash
|
||||
# 匹配后缀有u的q,例如aqua中的q
|
||||
q(?=u)
|
||||
```
|
||||
|
||||
> 如果想要捕获Lookahead中的匹配内容,可以使用`(?=(regex))`的语法。
|
||||
|
||||
### Negative Lookbehind `(?<!regex)`
|
||||
Negative Lookbehind和Negative Lookahead类似,但是是针对前缀做断言。其会告诉regex引擎暂时性后退一步,检测lookbehind中指定的regex是否可以匹配。
|
||||
|
||||
该语法匹配`前面不由...前缀`的正则表达式。
|
||||
|
||||
`(?<!a)b`正则表达式,则是可以匹配`b`和`debt`中的`b`,但是匹配不了`cab`中的`b`。
|
||||
|
||||
使用示例:
|
||||
```bash
|
||||
(?<!a)b
|
||||
```
|
||||
|
||||
### Positive Lookbehind `(?<=regex)
|
||||
该语法匹配`前面由...前缀`的正则表达式。
|
||||
|
||||
`(?<=a)b`可以匹配`cab`中的`b`,但是无法匹配`b`和`debt`中的b。
|
||||
|
||||
使用示例:
|
||||
```bash
|
||||
(?<=a)b
|
||||
```
|
||||
|
||||
### 不包含某表达式的语法
|
||||
如果想要通过正则来表达`不包含...`的语法,可以通过`^((?!regex).)*$`来表示。
|
||||
|
||||
|
||||
346
spring/AMQP/Spring AMQP RabbitMQ.md
Normal file
346
spring/AMQP/Spring AMQP RabbitMQ.md
Normal file
@@ -0,0 +1,346 @@
|
||||
# Spring AMQP
|
||||
Spring AMQP将spring项目中的核心理念应用到了基于AMQP的消息解决方案的开发。项目中提供了一个高级别抽象的“template”来发送和接收消息。
|
||||
## 示例
|
||||
### Spring配置
|
||||
如下示例展示了Spring项目中消息的接收和发送:
|
||||
```java
|
||||
ApplicationContext context =
|
||||
new AnnotationConfigApplicationContext(RabbitConfiguration.class);
|
||||
AmqpTemplate template = context.getBean(AmqpTemplate.class);
|
||||
template.convertAndSend("myqueue", "foo");
|
||||
String foo = (String) template.receiveAndConvert("myqueue");
|
||||
|
||||
........
|
||||
|
||||
@Configuration
|
||||
public class RabbitConfiguration {
|
||||
|
||||
@Bean
|
||||
public CachingConnectionFactory connectionFactory() {
|
||||
return new CachingConnectionFactory("localhost");
|
||||
}
|
||||
|
||||
@Bean
|
||||
public RabbitAdmin amqpAdmin() {
|
||||
return new RabbitAdmin(connectionFactory());
|
||||
}
|
||||
|
||||
@Bean
|
||||
public RabbitTemplate rabbitTemplate() {
|
||||
return new RabbitTemplate(connectionFactory());
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Queue myQueue() {
|
||||
return new Queue("myqueue");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
### SpringBoot自动装配配置
|
||||
SpringBoot会自动配置bean对象的结构,按如下样例所示:
|
||||
```java
|
||||
@SpringBootApplication
|
||||
public class Application {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(Application.class, args);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ApplicationRunner runner(AmqpTemplate template) {
|
||||
return args -> template.convertAndSend("myqueue", "foo");
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Queue myQueue() {
|
||||
return new Queue("myqueue");
|
||||
}
|
||||
|
||||
@RabbitListener(queues = "myqueue")
|
||||
public void listen(String in) {
|
||||
System.out.println(in);
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
## AMQP抽象
|
||||
Spring AMQP由两部分组成:`spring-amqp`和`spring-rabbit`。其中`spring-amqp`为抽象层,不依赖于任何AMQP Broker实现或client library。因此开发者只需要针对抽象层进行开发,开发的代码也能够跨amqp实现进行移植。该抽象层通过`spring-rabbit`来进行实现,目前只有RABBITMQ的实现。
|
||||
AMQP在协议层进行操作,可以通过RabbitMQ的客户端与任何实现了AMQP相同版本协议的broker进行通信。
|
||||
### Message
|
||||
Spring AMQP定义了Message类作为AMQP模型中对消息的抽象,Message中既包含了消息的内容,也包含了消息的属性,如下展示了Message类的定义:
|
||||
```java
|
||||
public class Message {
|
||||
|
||||
private final MessageProperties messageProperties;
|
||||
|
||||
private final byte[] body;
|
||||
|
||||
public Message(byte[] body, MessageProperties messageProperties) {
|
||||
this.body = body;
|
||||
this.messageProperties = messageProperties;
|
||||
}
|
||||
|
||||
public byte[] getBody() {
|
||||
return this.body;
|
||||
}
|
||||
|
||||
public MessageProperties getMessageProperties() {
|
||||
return this.messageProperties;
|
||||
}
|
||||
}
|
||||
```
|
||||
`MessageProperties`类中定义了通用的属性,例如'messageId', 'timestamp', 'contentType'等。除了这些属性外,还可以通过`setHeader(String key, Object value)`方法来自定义一些header。
|
||||
### Exchange
|
||||
Exchange接口代表了AMQP Exchange,消息生产者将消息发送到Exchange。每个Exchange都位于broker的virtual host中,并含有一个唯一的name,并且Exchange还有其他属性。
|
||||
```java
|
||||
public interface Exchange {
|
||||
|
||||
String getName();
|
||||
|
||||
String getExchangeType();
|
||||
|
||||
boolean isDurable();
|
||||
|
||||
boolean isAutoDelete();
|
||||
|
||||
Map<String, Object> getArguments();
|
||||
|
||||
}
|
||||
```
|
||||
#### Exchange参数
|
||||
- isDurable:当isDurable参数被设置为true时,该交换机即使当broker重启仍然会存在,如果isDurable参数被设置为false,当broker重启之后,交换机必须重新declare
|
||||
- autoDelete:当最后一个queue从交换机解绑定时,交换机将会被删除
|
||||
> 默认情况下,Spring Exchange中创建的交换机,isDurable属性为true,autoDelete属性为false
|
||||
|
||||
#### Exchange类型
|
||||
交换机类型通过`ExchangeTypes`中的常量进行表示,常用的类型如下:direct, topic, fanout, and headers。
|
||||
不同类型交换机之间的区别,在于它们如何处理与队列之间的绑定:
|
||||
- direct:direct exchange会让一个queue被绑定到一个固定的routing key
|
||||
- topic:topic exchange支持通过routing pattern来进行绑定,routing pattern中包含`*`和`#`通配符,其中`*` 代表一个任意word,而`#`代表0个或多个任意word
|
||||
> ### routing_key格式
|
||||
> routging_key组成格式为一个由`.`符号分隔的word列表,形式如下:
|
||||
> `stock.usd.nyse`,`nyse.vmw`,`quick.orange.rabbit`,word的数量任意,但是大小需要控制在256字节以内
|
||||
> topic exchange中的通配符如下:`#`和`*`,其中`*`可以匹配一个word,而`#`则是可以匹配0个或多个word
|
||||
> 例如`*.orange.*`,`*.*.rabbit`,`lazy.#`
|
||||
- fanout:fanout exchange会将消息发布到所有绑定到交换机上的队列,而不会考虑是否routing key
|
||||
#### default Exchange
|
||||
AMQP协议要求所有broker实现需要提供一个默认的exchange,该默认交换机没有名称(name为空字符串),且类型为direct。所有创建的queue都会自动绑定到该交换机,并且routing_key和queueName相同。
|
||||
|
||||
### Queue
|
||||
Queue代表消息消费者接收消息的组件。如下展示了Queue类结构:
|
||||
```java
|
||||
public class Queue {
|
||||
|
||||
private final String name;
|
||||
|
||||
private volatile boolean durable;
|
||||
|
||||
private volatile boolean exclusive;
|
||||
|
||||
private volatile boolean autoDelete;
|
||||
|
||||
private volatile Map<String, Object> arguments;
|
||||
|
||||
/**
|
||||
* The queue is durable, non-exclusive and non auto-delete.
|
||||
*
|
||||
* @param name the name of the queue.
|
||||
*/
|
||||
public Queue(String name) {
|
||||
this(name, true, false, false);
|
||||
}
|
||||
|
||||
// Getters and Setters omitted for brevity
|
||||
|
||||
}
|
||||
```
|
||||
Queue构造器接收queue name,取决于实现,adminTemplate会提供方法来产生唯一的named queue。这些队列可以用于reply address,故而这些队列的exclusive和autodelete属性都应该被设置为true。
|
||||
#### Queue的参数
|
||||
- durable:该队列当broker重启时是否仍会存在,如果设置为false,broker重启之后,queue必须被重新decalre
|
||||
- exclusive:只能够被一个连接使用,当该连接断开之后,queue也会被删除
|
||||
- auto-delete:当最后一个consumer不再订阅该queue时,该queue会被自动删除
|
||||
|
||||
> 当声明一个queue时,如果该queue不存在,那么queue会被创建;如果该queue已经存在,并且声明的属性和已存在队列的属性相同,那么不会发生任何事;如果queue已经存在,并且声明属性和已存在队列不同,那么会抛出异常,并且406的异常code(PRECONDITION_FAILED)会被返回
|
||||
|
||||
### Binding
|
||||
生产者向exchange发送消息,而消费者会从queue接收消息,而exchange和queue之间的关系通过binding表示。
|
||||
#### 将queue绑定到DirectExchange
|
||||
```java
|
||||
// routing_key为固定的"foo.bar"
|
||||
new Binding(someQueue, someDirectExchange, "foo.bar");
|
||||
```
|
||||
#### 将queue绑定到TopicExchange
|
||||
```java
|
||||
// routing_key为pattern “foo.*"
|
||||
new Binding(someQueue, someTopicExchange, "foo.*");
|
||||
```
|
||||
#### 将queue绑定到FanoutExchange
|
||||
```java
|
||||
// 绑定到fanout交换机时,无需routing_key
|
||||
new Binding(someQueue, someFanoutExchange);
|
||||
```
|
||||
#### 通过BindingBuilder来创建Binding
|
||||
```java
|
||||
Binding b = BindingBuilder.bind(someQueue).to(someTopicExchange).with("foo.*");
|
||||
```
|
||||
## 连接和资源管理
|
||||
管理到RabbitMQ broker连接的是`ConnectionFactory`接口,该接口的职责是提供`org.springframework.amqp.rabbit.connection.Connection`连接
|
||||
### 选择ConnectionFactory
|
||||
有三种connectionFactory可供选择:
|
||||
- PooledChannelConnectionFactory
|
||||
- ThreadChannelConnectionFactory
|
||||
- CachingConnectionFactory
|
||||
|
||||
在大多数情况下,应该使用PooledChannelConnectionFactory。
|
||||
#### PooledChannelConnectionFactory
|
||||
该factory管理一个连接和两个channel pool,其中一个channel pool是事务channel,另一个channel pool用于非事务的channel。
|
||||
```java
|
||||
@Bean
|
||||
PooledChannelConnectionFactory pcf() throws Exception {
|
||||
ConnectionFactory rabbitConnectionFactory = new ConnectionFactory();
|
||||
rabbitConnectionFactory.setHost("localhost");
|
||||
PooledChannelConnectionFactory pcf = new PooledChannelConnectionFactory(rabbitConnectionFactory);
|
||||
pcf.setPoolConfigurer((pool, tx) -> {
|
||||
if (tx) {
|
||||
// configure the transactional pool
|
||||
}
|
||||
else {
|
||||
// configure the non-transactional pool
|
||||
}
|
||||
});
|
||||
return pcf;
|
||||
}
|
||||
|
||||
```
|
||||
#### ThreadChannelConnectionFactory
|
||||
该factory管理一个连接和两个ThreadLocal,一个用于事务channel,一个用于非事务channel。该factory会保证位于同一线程的操作会使用同一个channel。
|
||||
该特性允许在不借助Scoped操作的前提下进行严格的消息排序。为了避免内存泄漏,如果你你的应用使用了很多生命周期很短的线程,必须手动调用factory的`closeThreadChannel()`方法来释放channel资源。
|
||||
#### CachingConnectionFactory
|
||||
CachingConnectionFactory会建立一个在应用程序范围内共享的connection proxy。共享连接是可能的,因为与AMQP进行消息传递的其实是Channel,而connection实例提供了createChnanel方法。
|
||||
`CachingConnectionFactory`实现支持缓存channel对象,并且对事务channel和非事务channel进行分别缓存。
|
||||
在创建CachingConnectionFactory时,可以通过构造器提供hostname参数,还可以提供usernmae和password参数。
|
||||
如果要设置channel cache size(默认情况下为25),可以调用`setChannelCacheSize()`方法。
|
||||
也可以设置CachingConnectionFactory来缓存connection,每次调用`createConnection()`方法都会创建一个新的连接,关闭一个连接则是会将其返还到connection cache中。在这些connection创建的channel也会被缓存。如果要缓存这些连接,需要将`cacheMode`设置为`CacheMode.CONNECTION`。
|
||||
#### cache size并不是limit
|
||||
cache size并不会限制应用中使用channel的数量,但是限制channel被缓存的数量。当cache size被设置为10时,应用中可以使用任意多数量的channel。如果由超过10个channel被使用,那么当这些channel被返还到cache时,只会有10个channel被缓存,其他的channel都会被物理关闭。
|
||||
默认情况下,cache size被设置为25,在多线程环境下,如果cache size被设置的过小,那么会频繁的对channel进行创建和关闭。可以在RabbitMQ Admin UI界面实时监控channel数量,并且对cache size进行调整。
|
||||
#### 通过CachingConnectionFactory来创建来连接
|
||||
```java
|
||||
CachingConnectionFactory connectionFactory = new CachingConnectionFactory("somehost");
|
||||
connectionFactory.setUsername("guest");
|
||||
connectionFactory.setPassword("guest");
|
||||
|
||||
Connection connection = connectionFactory.createConnection();
|
||||
```
|
||||
### 连接到集群
|
||||
如果要连接到一个集群,需要设置`CachingConnectionFactory`的`addresses`属性:
|
||||
```java
|
||||
@Bean
|
||||
public CachingConnectionFactory ccf() {
|
||||
CachingConnectionFactory ccf = new CachingConnectionFactory();
|
||||
ccf.setAddresses("host1:5672,host2:5672,host3:5672");
|
||||
return ccf;
|
||||
}
|
||||
```
|
||||
默认情况下,当新连接创建时会随机连接到集群中的一个host,如果想要从第一个尝试到最后一个,可以设置`ddressShuffleMode`属性为`AddressShuffleMode.NONE`。
|
||||
### Publisher Confirm and Returns
|
||||
confirm和返回消息可以通过设置`CachingConnectionFactory`来支持,将`CachingConnectionFactory`的`publisherConfirmType`属性设置为`ConfirmType.CORRELATED`并将`publisherReturns`的属性设置为true。
|
||||
当这些选项设置后,被factory创建的Channel实例将会被包装在`PublisherCallbackChannel`中。当这些channel对象被获取后,使用者可以向channel中注册一个`PublisherCallbackChannel.Listener`回调。`PublisherCallbackChannel`实现中含有逻辑来将confirm或return路由到特定的listener中。
|
||||
#### Publisher Confirm
|
||||
由于网络故障或是其他原因,client向socket中写入的数据无法保证被传输到server端并被处理,可能会出现数据丢失的情况。
|
||||
在使用标准AMQP 0-9-1情况下,唯一保证消息不被丢失的方法是使用事务:令channel是事务的,并且对消息进行发布和提交。在这种情况下,不必要的事务会极大的影响吞吐量和效率,会将吞吐量降低250倍左右。为了解决这种情况,引入了确认机制。
|
||||
要启用确认机制,client会发送`confirm.select`,broker则会回复`confirm.select-ok`。一旦`confirm.select`在channel中被使用,则意味着confirm mode已经开启,一个事务channel无法开启确认机制,且一个确认机制开启的channel也无法将其设置为事务的。
|
||||
一旦channel处于确认模式,client和broker都会计算消息数量(从1开始,即从confirm.select开始),broker在确认消息时,会向channel中发送`basic.ack`,而`delivery-tag`则是包含了确认消息的序列号。broker也会在`basic.ack`中设置`multiple`属性,代表该消息和位于该消息之前的消息都被确认。
|
||||
## Amqp Template
|
||||
Spring AMQP提供了高层抽象的template,在该tempalte中定义了接收消息和发送消息的主要操作。
|
||||
### 添加重试功能
|
||||
可以配置RabbitTemplate使用RetryTemplate,如下样例展示了exponential back off policy和默认的SimpleRetryPolicy,该policy会在抛异常之前重试3次。
|
||||
```java
|
||||
@Bean
|
||||
public RabbitTemplate rabbitTemplate() {
|
||||
RabbitTemplate template = new RabbitTemplate(connectionFactory());
|
||||
RetryTemplate retryTemplate = new RetryTemplate();
|
||||
ExponentialBackOffPolicy backOffPolicy = new ExponentialBackOffPolicy();
|
||||
backOffPolicy.setInitialInterval(500);
|
||||
backOffPolicy.setMultiplier(10.0);
|
||||
backOffPolicy.setMaxInterval(10000);
|
||||
retryTemplate.setBackOffPolicy(backOffPolicy);
|
||||
template.setRetryTemplate(retryTemplate);
|
||||
return template;
|
||||
}
|
||||
```
|
||||
### 异步发布
|
||||
发布消息是一个异步的机制,默认情况下,rabbitMQ会将不能路由的消息丢弃。为了成功的发布,需要获取异步的confirm。考虑如下两种失败场景:
|
||||
- 发送到一个exchange,但是没有匹配的queue
|
||||
- 发送到一个不存在的exchange
|
||||
|
||||
第一种场景由publisher returns涵盖。
|
||||
对于第二种场景,消息将会被丢弃并且没有任何返回,channel将会被关闭并且抛出异常。默认情况下,异常将会在日志中打印,但是可以注册一个ChannelListener到CachingConnectionFactory,用于获取如下事件。
|
||||
```java
|
||||
this.connectionFactory.addConnectionListener(new ConnectionListener() {
|
||||
|
||||
@Override
|
||||
public void onCreate(Connection connection) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onShutDown(ShutdownSignalException signal) {
|
||||
...
|
||||
}
|
||||
|
||||
});
|
||||
```
|
||||
可以检查signal的reason属性,从而确定产生的问题。
|
||||
|
||||
### Correlated Publisher Confirms and Returns
|
||||
RabbitTemplate中实现了AmqpTemplate中对于发布确认和Returns的支持。
|
||||
#### Rabbitmq Returns
|
||||
通过Rabbitmq完成RPC调用,消息中需要含有如下属性:
|
||||
- deliveryMode: 将消息标识为持久化的或者暂时的,如果该属性值为2,则消息是持久化的,任何其他值消息都是暂时的
|
||||
- contentType:用于表示消息的mime-type,例如可以是application/json
|
||||
- replyTo:通常用于命名一个callback queue
|
||||
- correlationId:该属性用于关联rpc响应和请求
|
||||
|
||||
##### Correlation Id
|
||||
如果对来自所有客户端的rpc请求,都只创建一个callback queue,该方案效率会很低。因此,应该为每一个客户端都创建一个callback queue。
|
||||
> 因为如果所有客户端使用一条callback queue,那么传递给callback queue的rpc响应会广播给所有订阅的客户端,然后再用客户端丢弃或匹配。这样会极大影响吞吐量。
|
||||
|
||||
在为每一个client创建一个callback queue后,从callback queue中接收到的响应再通过`correlationId`属性同请求关联到一起。如果correlationid关联的请求不存在,那么只需要安全的丢弃该请求即可。
|
||||
|
||||
##### Rabitmq实现RPC流程
|
||||
- 对于rpc请求,客户端发送消息时消息应该携带两个属性:`replyTo`:replyTo是一个匿名的独占队列(exclusive为true,当该队列只能由一个连接使用,并且当连接断开时会自动删除),`correleationId`则是一个唯一的值,用于将请求和响应关联起来
|
||||
- 请求将会被发送到rpc_queue
|
||||
- rpc worker将会从rpc_queue中拉取消息,并对拉取数据进行处理。处理完成之后,会将结果封装在消息中并且发送给客户端,消息会被发送到`replyTo`属性指定的queue
|
||||
- client等待reply_queue中的响应消息,当拉取到响应消息之后,其会检查correlationId属性,并根据其来匹配响应对应的请求
|
||||
|
||||
|
||||
#### Spring AMQP Returns
|
||||
对于返回的消息, template的`mandatory`属性必须要被设置为true,或`mandatory-expression`表达式必须要为true。该特性要求CachingConnectionFactory的`publisherReturns`属性被设置为true。返回将会通过`setReturnsCallback(ReturnsCallback callback)`方法注册`RabbitTemplate.ReturnsCallback`回调来发送到客户端,该回调必须实现如下方法:
|
||||
```java
|
||||
void returnedMessage(ReturnedMessage returned);
|
||||
```
|
||||
> 在通过setReturnsCallback来注册回调时,每个rabbitTemplate只能够注册一个ReturnsCallback
|
||||
|
||||
ReturnedMessage必须含有如下属性:
|
||||
- message:返回消息本身
|
||||
- replyCode:代表return reason的code
|
||||
- replyText:一个文本的return reason,例如`NO_ROUTE`
|
||||
- exchange:消息发送到的交换机
|
||||
- routing key:使用的routing key
|
||||
|
||||
#### 发布确认
|
||||
对于发布确认,该template需要一个CachingConnectionFactory,并且CachingConnectionFactory的`publisherConfirm`属性需要被设置为`ConfirmType.CORRELATED`。确认将会被发送给客户端,可以通过`setConfirmCallback(ConfirmCallback callback)`注册一个`RabbitTemplate.ConfirmCallback`方法。该回调需要实现如下方法:
|
||||
```java
|
||||
void confirm(CorrelationData correlationData, boolean ack, String cause);
|
||||
```
|
||||
其中correlationData是客户端在发送消息时提供的原始消息对象。`ack`参数如果为`true`代表`ack`,为`false`则是代表`nack`。对于`nack`,`cause`参数将会包含`nack`的原因。
|
||||
> 例如,如果将消息发送给一个不存在的交换机,在这种情况下,broker将会关闭channel,关闭的原因则会包含在`cause`中。
|
||||
|
||||
ConfirmCallback只在RabbitTemplate中被支持。
|
||||
|
||||
如果同时启用了发布确认和返回,且消息无法被路由到任何队列中,那么CorrelationData的return属性将会被注入到返回的消息中。其会保证返回消息的属性会在
|
||||
|
||||
### Scoped Operations
|
||||
通常情况下,在使用template时,Channel从cache中获取,使用完毕之后再被返还到cache中以便重用。在多线程环境下,并不会保证线程在下次使用Channel时会使用一样的Channel。
|
||||
96
spring/Apache Shiro/Apache Shiro Authentication.md
Normal file
96
spring/Apache Shiro/Apache Shiro Authentication.md
Normal file
@@ -0,0 +1,96 @@
|
||||
- [Apache Shiro Authentication](#apache-shiro-authentication)
|
||||
- [Apache Shiro Authentication简介](#apache-shiro-authentication简介)
|
||||
- [Apache Authentication概念](#apache-authentication概念)
|
||||
- [subject](#subject)
|
||||
- [principals](#principals)
|
||||
- [credentials](#credentials)
|
||||
- [realms](#realms)
|
||||
- [Shiro Authentication过程](#shiro-authentication过程)
|
||||
- [Shiro框架的Authentication过程](#shiro框架的authentication过程)
|
||||
- [收集用户的principals和credentials](#收集用户的principals和credentials)
|
||||
- [将收集的principals和credentials提交给认证系统](#将收集的principals和credentials提交给认证系统)
|
||||
- [身份认证后对访问进行allow/retry authentication/block](#身份认证后对访问进行allowretry-authenticationblock)
|
||||
- [rememberMe支持](#rememberme支持)
|
||||
- [remembered和authenticated的区别](#remembered和authenticated的区别)
|
||||
- [logging out](#logging-out)
|
||||
|
||||
# Apache Shiro Authentication
|
||||
## Apache Shiro Authentication简介
|
||||
Authentication是一个对用户进行身份认证的过程,在认证过程中用户需要向应用提供用于证明用户的凭据。
|
||||
## Apache Authentication概念
|
||||
### subject
|
||||
在应用的角度,subject即是一个用户
|
||||
### principals
|
||||
主体,用于标识一个用户,可以是username、social security nubmer等
|
||||
### credentials
|
||||
凭据,在用户认证过程中用于认证用户的身份,可以是密码、生物识别数据(如指纹、面容等)
|
||||
### realms
|
||||
专用于security的dao对象,用于和后端的datasource进行沟通。
|
||||
## Shiro Authentication过程
|
||||
### Shiro框架的Authentication过程
|
||||
1. 收集用户的principals和credentials
|
||||
2. 向应用的认证系统提交用户的principals和credentials
|
||||
3. 认证结束之后,根据认证结果允许访问、重试访问请求或者阻塞访问
|
||||
### 收集用户的principals和credentials
|
||||
可以通过UsernamePasswordToken来存储用户提交的username和password,并可以调用UsernamePasswordToken.rememberMe方法来启用Shiro的“remember-me”功能。
|
||||
```java
|
||||
//Example using most common scenario:
|
||||
//String username and password. Acquire in
|
||||
//system-specific manner (HTTP request, GUI, etc)
|
||||
UsernamePasswordToken token = new UsernamePasswordToken( username, password );
|
||||
|
||||
//”Remember Me” built-in, just do this:
|
||||
token.setRememberMe(true);
|
||||
```
|
||||
### 将收集的principals和credentials提交给认证系统
|
||||
在收集完用户的principals和credentials之后,需要将其提交给应用的认证系统。
|
||||
在Shiro中,代表认证系统的是Realm,其从存放安全数据的datasource中获取数据,并且对用户提交的principals和credentials进行校验。
|
||||
在Shiro中,该过程用如下代码表示:
|
||||
```java
|
||||
//With most of Shiro, you'll always want to make sure you're working with the currently
|
||||
//executing user, referred to as the subject
|
||||
Subject currentUser = SecurityUtils.getSubject();
|
||||
|
||||
//Authenticate the subject by passing
|
||||
//the user name and password token
|
||||
//into the login method
|
||||
currentUser.login(token);
|
||||
```
|
||||
> 在Shiro中,subject可以被看做是用户,**在当前执行的线程中永远有一个subject与其相关联。**
|
||||
> **可以通过SecurityUtils.getSubject()方法来获取当前执行线程相关联的subject。**
|
||||
|
||||
> 在获取当前执行线程关联subject之后,需要对当前subject进行身份认证,通过subject.login(token)来对用户提交的principals和credentials进行Authentication
|
||||
|
||||
### 身份认证后对访问进行allow/retry authentication/block
|
||||
在调用subject.login(token)之后,如果身份认证成功,用户将在seesion的生命周期内维持他们的identity。但是如果身份认证失败,可以为抛出的异常指定不同的异常处理逻辑来定义登录失败之后的行为。
|
||||
```java
|
||||
try {
|
||||
currentUser.login(token);
|
||||
} catch ( UnknownAccountException uae ) { ...
|
||||
} catch ( IncorrectCredentialsException ice ) { ...
|
||||
} catch ( LockedAccountException lae ) { ...
|
||||
} catch ( ExcessiveAttemptsException eae ) { ...
|
||||
} ... your own ...
|
||||
} catch ( AuthenticationException ae ) {
|
||||
//unexpected error?
|
||||
}
|
||||
//No problems, show authenticated view…
|
||||
```
|
||||
## rememberMe支持
|
||||
Apache Shiro除了正常的Authentication流程外,还支持rememberMe功能。
|
||||
Shiro中Subject对象拥有两个方法,isRemembered()和isAuthenticated()。
|
||||
> - 一个remembered subject,其identity和principals自上次session成功认证后就被记住
|
||||
> - 一个authenticated subject,其identity只在本次会话中有效
|
||||
|
||||
### remembered和authenticated的区别
|
||||
在Shiro中,一个remembered subject并不代表该subject已经被authenticated。如果一个subject被remembered,仅仅会向系统提示该subject可能是系统的某个用户,但是不会对subject的身份提供保证。但是如果subject被authenticated,该subject的identity在当前会话中已经被认证。
|
||||
> 故而,isRemembered校验可以用来执行一些非敏感的操作,如用户自定义界面视图等。但是,敏感性操作如金额信息和变动操作等,必须通过isAuthenticated校验而不是isRemembered校验,敏感性操作的用户身份必须得到认证。
|
||||
|
||||
## logging out
|
||||
在Shiro中,登出操作可以通过如下代码实现
|
||||
```java
|
||||
currentUser.logout(); //removes all identifying information and invalidates their session too.
|
||||
```
|
||||
当执行登出操作时,Shiro会关闭当前session,并且会移除当前subject的任何identity。如果在web环境中使用rememberMe,logout默认会从浏览器中删除rememberMe cookie。
|
||||
|
||||
|
||||
94
spring/Apache Shiro/Apache Shiro Authorization.md
Normal file
94
spring/Apache Shiro/Apache Shiro Authorization.md
Normal file
@@ -0,0 +1,94 @@
|
||||
- [Apache Shiro Authorization](#apache-shiro-authorization)
|
||||
- [Authorization简介](#authorization简介)
|
||||
- [Authorization的核心元素](#authorization的核心元素)
|
||||
- [Permission](#permission)
|
||||
- [权限粒度级别](#权限粒度级别)
|
||||
- [Roles](#roles)
|
||||
- [Role分类](#role分类)
|
||||
- [User](#user)
|
||||
- [在Apache Shiro中实行Authorization](#在apache-shiro中实行authorization)
|
||||
- [通过java code实现authorization](#通过java-code实现authorization)
|
||||
- [基于String的权限鉴定](#基于string的权限鉴定)
|
||||
- [通过注解实现Authorization](#通过注解实现authorization)
|
||||
|
||||
# Apache Shiro Authorization
|
||||
## Authorization简介
|
||||
Authorization(访问控制),分配访问某资源的特定权限。
|
||||
## Authorization的核心元素
|
||||
### Permission
|
||||
Permission是最原子级别的安全策略,用来控制用户与应用进行交互时可以执行哪些操作。**格式良好的permission描述了资源的类型和与该资源交互时可以执行的操作。**
|
||||
对于与数据相关的资源,权限通常有create、read、update、delete(CRUD)。
|
||||
#### 权限粒度级别
|
||||
在Shiro中,可以在任何粒度对permission进行定义。如下是permission粒度的一些定义:
|
||||
1. Resource级别:该级别是最广泛和最容易构建的粒度级别,在该级别用户可以对资源执行特定的操作。**在Resource级别,该资源类型被指定,但是没有限制用户操作特定的资源实例(即用户可以对该Resource类型的所有实例进行操作)**
|
||||
2. Instance级别:该级别限定了Permission可以操作的Resource Instance,在该级别用户只能够对特定的Resource实例进行操作。
|
||||
3. Attribute级别:该级别比限定了Permission可以操作Resouce类型或Resource实例的某个属性
|
||||
### Roles
|
||||
Roles是一个Permission的集合,用于简化权限和用户管理过程。用户可以被授予特定的角色来获得操作某些资源的权限。
|
||||
#### Role分类
|
||||
1. Role不实际关联具体的Permission,当你具有banker的角色时,其角色隐含你可以对账户进行操作的权限;当你具有waiter的角色时,默认可以对厨房的door进行open/close操作
|
||||
2. Role实际关联具体的Permission,在该情况下Role即为一系列Permission的集合,你可以对银行账号进行create、delete操作,因为操作银行账号是你已分配的admin角色的一个下属权限
|
||||
### User
|
||||
在Shiro中,User即是一个Subject实例。在Shiro中,Subject可以是任何与系统进行交互的主体,可以是浏览器、客户端、crond定时任务等。
|
||||
## 在Apache Shiro中实行Authorization
|
||||
在Apache Shiro中,Authorization可以通过如下方式执行:
|
||||
1. 通过代码实现:即在java程序中通过代码实现访问控制
|
||||
2. jdk注解:可以在你的方法上加上authorization注解
|
||||
3. jsp/gsp taglibs
|
||||
### 通过java code实现authorization
|
||||
可以通过如下代码进行角色鉴定:
|
||||
```java
|
||||
//get the current Subject
|
||||
Subject currentUser = SecurityUtils.getSubject();
|
||||
|
||||
if (currentUser.hasRole("administrator")) {
|
||||
//show a special button
|
||||
} else {
|
||||
//don’t show the button?)
|
||||
}
|
||||
```
|
||||
可以通过如下代码实现对权限的鉴定操作:
|
||||
```java
|
||||
Subject currentUser = SecurityUtils.getSubject();
|
||||
|
||||
Permission printPermission = new PrinterPermission("laserjet3000n","print");
|
||||
|
||||
If (currentUser.isPermitted(printPermission)) {
|
||||
//do one thing (show the print button?)
|
||||
} else {
|
||||
//don’t show the button?
|
||||
}
|
||||
```
|
||||
#### 基于String的权限鉴定
|
||||
如果不想构造Permission对象,可以通过构造一个字符串来代表权限。该字符串可以是任何格式,只要你的Realm能够识别该格式并且与权限进行交互。
|
||||
```java
|
||||
String perm = "printer:print:laserjet4400n";
|
||||
|
||||
if(currentUser.isPermitted(perm)){
|
||||
//show the print button?
|
||||
} else {
|
||||
//don’t show the button?
|
||||
}
|
||||
```
|
||||
### 通过注解实现Authorization
|
||||
可以通过java注解来实现Authorization过程,**在使用注解之前,必须先开启aop**。
|
||||
如果在执行openAccount之前,当前Subject必须拥有account:create权限,那么可以通过如下方式来实现权限鉴定。如果当前用户未被直接授予或通过role间接授予该权限,那么会抛出AuthorizationException异常。
|
||||
```java
|
||||
//Will throw an AuthorizationException if none
|
||||
//of the caller’s roles imply the Account
|
||||
//'create' permission
|
||||
@RequiresPermissions("account:create")
|
||||
public void openAccount( Account acct ) {
|
||||
//create the account
|
||||
}
|
||||
```
|
||||
如果要在执行方法之前进行角色校验,可以通过如下方式加上注解达到预期功能。
|
||||
```java
|
||||
//Throws an AuthorizationException if the caller
|
||||
//doesn’t have the ‘teller’ role:
|
||||
@RequiresRoles( "teller" )
|
||||
public void openAccount( Account acct ) {
|
||||
//do something in here that only a teller
|
||||
//should do
|
||||
}
|
||||
```
|
||||
92
spring/Apache Shiro/Apache Shiro QuickStart.md
Normal file
92
spring/Apache Shiro/Apache Shiro QuickStart.md
Normal file
@@ -0,0 +1,92 @@
|
||||
- [Apache Shiro Quick Start](#apache-shiro-quick-start)
|
||||
- [Apache Shiro常用API](#apache-shiro常用api)
|
||||
- [获取当前用户](#获取当前用户)
|
||||
- [设置用户Session](#设置用户session)
|
||||
- [通过用户名和密码对用户进行身份认证](#通过用户名和密码对用户进行身份认证)
|
||||
- [对身份认证失败的情况进行异常处理](#对身份认证失败的情况进行异常处理)
|
||||
- [对已经登录的用户进行role检验](#对已经登录的用户进行role检验)
|
||||
- [检测某用户是否具有某项特定权限](#检测某用户是否具有某项特定权限)
|
||||
- [在实例级别对用户的权限进行检测](#在实例级别对用户的权限进行检测)
|
||||
- [用户登出](#用户登出)
|
||||
|
||||
# Apache Shiro Quick Start
|
||||
## Apache Shiro常用API
|
||||
### 获取当前用户
|
||||
在任何环境中,都可以通过如下代码来获取当前执行的用户:
|
||||
```java
|
||||
Subject currentUser = SecurityUtils.getSubject();
|
||||
```
|
||||
### 设置用户Session
|
||||
可以通过如下代码获取用户的Shiro Session,并可以向Session中设置属性和值,设置的值在用户会话期间内都可以使用。
|
||||
**Shiro Session在使用时并不要求当前位于HTTP环境下**
|
||||
```java
|
||||
Session session = currentUser.getSession();
|
||||
session.setAttribute( "someKey", "aValue" );
|
||||
```
|
||||
> 如果当前应用部署于Web环境下,那么Shiro Session默认会使用HttpSession,但是如果当前应用部署在非Web环境下时,Shiro Session会使用其Enterprise Session Management。
|
||||
|
||||
### 通过用户名和密码对用户进行身份认证
|
||||
通过如下代码,可以通过UsernamePasswordToken来对未认证的用户进行身份认证。
|
||||
```java
|
||||
if ( !currentUser.isAuthenticated() ) {
|
||||
//collect user principals and credentials in a gui specific manner
|
||||
//such as username/password html form, X509 certificate, OpenID, etc.
|
||||
//We'll use the username/password example here since it is the most common.
|
||||
//(do you know what movie this is from? ;)
|
||||
UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa");
|
||||
//this is all you have to do to support 'remember me' (no config - built in!):
|
||||
token.setRememberMe(true);
|
||||
currentUser.login(token);
|
||||
}
|
||||
```
|
||||
### 对身份认证失败的情况进行异常处理
|
||||
如果在身份认证的过程中失败,可以通过如下代码捕获认证失败抛出的异常,并对异常进行异常处理
|
||||
```java
|
||||
try {
|
||||
currentUser.login( token );
|
||||
//if no exception, that's it, we're done!
|
||||
} catch ( UnknownAccountException uae ) {
|
||||
//username wasn't in the system, show them an error message?
|
||||
} catch ( IncorrectCredentialsException ice ) {
|
||||
//password didn't match, try again?
|
||||
} catch ( LockedAccountException lae ) {
|
||||
//account for that username is locked - can't login. Show them a message?
|
||||
}
|
||||
... more types exceptions to check if you want ...
|
||||
} catch ( AuthenticationException ae ) {
|
||||
//unexpected condition - error?
|
||||
}
|
||||
```
|
||||
### 对已经登录的用户进行role检验
|
||||
如果用户已经登录,如果要检测该用户是否被授予某role权限,可以通过如下代码进行检验
|
||||
```java
|
||||
if ( currentUser.hasRole( "schwartz" ) ) {
|
||||
log.info("May the Schwartz be with you!" );
|
||||
} else {
|
||||
log.info( "Hello, mere mortal." );
|
||||
}
|
||||
```
|
||||
### 检测某用户是否具有某项特定权限
|
||||
如果要对已经登录的用户执行检测,检测其是否被授予某项特定的前线,可以通过如下方式进行检测。
|
||||
```java
|
||||
if ( currentUser.isPermitted( "lightsaber:wield" ) ) {
|
||||
log.info("You may use a lightsaber ring. Use it wisely.");
|
||||
} else {
|
||||
log.info("Sorry, lightsaber rings are for schwartz masters only.");
|
||||
}
|
||||
```
|
||||
### 在实例级别对用户的权限进行检测
|
||||
在Shiro中,可以检测用户是否对某实例具有特定权限,通过如下代码:
|
||||
```java
|
||||
if ( currentUser.isPermitted( "winnebago:drive:eagle5" ) ) {
|
||||
log.info("You are permitted to 'drive' the 'winnebago' with license plate (id) 'eagle5'. " +
|
||||
"Here are the keys - have fun!");
|
||||
} else {
|
||||
log.info("Sorry, you aren't allowed to drive the 'eagle5' winnebago!");
|
||||
}
|
||||
```
|
||||
### 用户登出
|
||||
如果已经登录的用户想要执行登出操作,可以通过如下代码进行登录:
|
||||
```java
|
||||
currentUser.logout();
|
||||
```
|
||||
133
spring/Apache Shiro/Apache Shiro Realm.md
Normal file
133
spring/Apache Shiro/Apache Shiro Realm.md
Normal file
@@ -0,0 +1,133 @@
|
||||
- [Apache Shiro Realm](#apache-shiro-realm)
|
||||
- [Realm简介](#realm简介)
|
||||
- [Realm配置](#realm配置)
|
||||
- [Realm Authentication](#realm-authentication)
|
||||
- [支持Authentication](#支持authentication)
|
||||
- [处理AuthenticationToken](#处理authenticationtoken)
|
||||
- [credentials匹配](#credentials匹配)
|
||||
- [简单比较是否相等](#简单比较是否相等)
|
||||
- [Hash Credentials](#hash-credentials)
|
||||
- [通过sha-256算法来生成账户信息](#通过sha-256算法来生成账户信息)
|
||||
- [指定HashedCredentialsMatcher](#指定hashedcredentialsmatcher)
|
||||
- [SaltedAuthenticationInfo](#saltedauthenticationinfo)
|
||||
- [关闭Realm的Authentication](#关闭realm的authentication)
|
||||
- [Realm Authorization](#realm-authorization)
|
||||
- [基于role的authorization](#基于role的authorization)
|
||||
- [基于permission的authorization](#基于permission的authorization)
|
||||
|
||||
# Apache Shiro Realm
|
||||
## Realm简介
|
||||
Realm是一个组件,用来访问针对特定应用的安全数据,例如user、role或permission。Realm负责将这些安全信息翻译为Shiro能够理解的格式。
|
||||
由于大多数数据源都会同时存储authentication和authorization信息,故而Realm能够同时执行authentication和authorization操作。
|
||||
## Realm配置
|
||||
对于Realm的配置,可以在ini文件中进行如下配置:
|
||||
```ini
|
||||
fooRealm = com.company.foo.Realm
|
||||
barRealm = com.company.another.Realm
|
||||
bazRealm = com.company.baz.Realm
|
||||
|
||||
; 如下指定的顺序会影响Authentication/Authorization过程中的顺序
|
||||
securityManager.realms = $fooRealm, $barRealm, $bazRealm
|
||||
```
|
||||
## Realm Authentication
|
||||
### 支持Authentication
|
||||
在Realm被询问去执行Authentication时,首先会调用该Realm的supports方法,如果supports方法的返回值是true时getAuthenticationInfo方法才会被调用。
|
||||
通常情况下,Realm会对提交的Token类型进行检测,并查看当前Realm是否能对该类型Token进行处理。
|
||||
### 处理AuthenticationToken
|
||||
如果Realm支持该提交的Token类型,那么Authenticator会调用Realm的getAuthenticationInfo方法,该方法代表了通过Realm数据库来进行认证尝试。
|
||||
该方法会按照如下顺序进行执行:
|
||||
1. 查看Token中存储的principals信息
|
||||
2. 根据Token中的principals,在data source中查找对应的账户信息
|
||||
3. 确保提交Token中的credentials和data source中查找出的credentials相匹配
|
||||
4. 如果credentials匹配,那么会将用户账户的信息封装到AuthenticationInfo中并返回
|
||||
5. 如果credentials不匹配,会抛出AuthenticationException
|
||||
### credentials匹配
|
||||
为了确保在credentials匹配的过程中,该过程是可插入(pluggable)和可自定义的(customizable)的,AuthenticationRealm支持CredentialsMatcher的概念,通过CredentialsMatcher来进行credentials的比较。
|
||||
在从data source中查询到账户数据之后,会将其和提交Token中的credentials一起传递给CredentialsMatcher,由CredentialsMatcher来判断credentials是否相等。
|
||||
可以通过如下方式来定义CredentialsMatcher的比较逻辑:
|
||||
```java
|
||||
Realm myRealm = new com.company.shiro.realm.MyRealm();
|
||||
CredentialsMatcher customMatcher = new com.company.shiro.realm.CustomCredentialsMatcher();
|
||||
myRealm.setCredentialsMatcher(customMatcher);
|
||||
```
|
||||
或
|
||||
```ini
|
||||
[main]
|
||||
...
|
||||
customMatcher = com.company.shiro.realm.CustomCredentialsMatcher
|
||||
myRealm = com.company.shiro.realm.MyRealm
|
||||
myRealm.credentialsMatcher = $customMatcher
|
||||
...
|
||||
```
|
||||
### 简单比较是否相等
|
||||
Shiro中所有开箱即用的Realm,其实现都默认使用SimpleCredentialsMatcher,SimpleCredentialsMatcher简单会对存储在data source中的principals和提交Token中的credentials进行比较相等操作。
|
||||
### Hash Credentials
|
||||
将用户提交的principals不做任何转换直接存储在data source中是一种不安全的做法,通常是将其进行单向hash之后再存入数据库。
|
||||
这样可以确保用户的credentials不会以raw text的方式存储再data source中,即使数据库数据被泄露,用户credentials的原始值也不会被任何人知道。
|
||||
为了支持Token中credentials和data source中hash之后credentials的比较,Shiro提供了HashedCredentialsMatcher实现,可以通过配置HashedCredentialsMatcher来取代SimpleCredentialsMatcher。
|
||||
#### 通过sha-256算法来生成账户信息
|
||||
```java
|
||||
import org.apache.shiro.crypto.hash.Sha256Hash;
|
||||
import org.apache.shiro.crypto.RandomNumberGenerator;
|
||||
import org.apache.shiro.crypto.SecureRandomNumberGenerator;
|
||||
...
|
||||
|
||||
//We'll use a Random Number Generator to generate salts. This
|
||||
//is much more secure than using a username as a salt or not
|
||||
//having a salt at all. Shiro makes this easy.
|
||||
//
|
||||
//Note that a normal app would reference an attribute rather
|
||||
//than create a new RNG every time:
|
||||
RandomNumberGenerator rng = new SecureRandomNumberGenerator();
|
||||
Object salt = rng.nextBytes();
|
||||
|
||||
//Now hash the plain-text password with the random salt and multiple
|
||||
//iterations and then Base64-encode the value (requires less space than Hex):
|
||||
String hashedPasswordBase64 = new Sha256Hash(plainTextPassword, salt, 1024).toBase64();
|
||||
|
||||
User user = new User(username, hashedPasswordBase64);
|
||||
//save the salt with the new account. The HashedCredentialsMatcher
|
||||
//will need it later when handling login attempts:
|
||||
user.setPasswordSalt(salt);
|
||||
userDAO.create(user);
|
||||
```
|
||||
#### 指定HashedCredentialsMatcher
|
||||
可以通过如下方式来指定特定HashedCredentialsMatcher实现类。
|
||||
```ini
|
||||
[main]
|
||||
...
|
||||
credentialsMatcher = org.apache.shiro.authc.credential.Sha256CredentialsMatcher
|
||||
# base64 encoding, not hex in this example:
|
||||
credentialsMatcher.storedCredentialsHexEncoded = false
|
||||
credentialsMatcher.hashIterations = 1024
|
||||
# This next property is only needed in Shiro 1.0\. Remove it in 1.1 and later:
|
||||
credentialsMatcher.hashSalted = true
|
||||
|
||||
...
|
||||
myRealm = com.company.....
|
||||
myRealm.credentialsMatcher = $credentialsMatcher
|
||||
...
|
||||
```
|
||||
#### SaltedAuthenticationInfo
|
||||
如果制定了HashedCredentialsMatcher,那么Realm.getAuthenticationInfo必须返回一个SaltedAuthenticationInfo实例而不是普通的Authentication实例。该SaltedAuthenticationInfo确保在创建用户信息时使用的salt可以在CredentialsMatcher中被使用。
|
||||
HashedCredentialsMatcher在对Token中提交的credentials进行hash时,需要使用到salt值来将用户提交的credentials进行和创建用户时相同的散列。
|
||||
### 关闭Realm的Authentication
|
||||
如果对某个Realm,想要对该realm不执行Authentication,可以将其实现类的supports方法只返回false,此时该realm在authentication过程中绝对不会被询问。
|
||||
## Realm Authorization
|
||||
SecurityManager将校验permission和role的工作委托给了Authorizer,默认是ModularRealmAuthorizer。
|
||||
### 基于role的authorization
|
||||
当subject的hasRoles或checkRoles被调用,其具体的执行流程如下:
|
||||
1. subject将校验role的任务委托给SecurityManager
|
||||
2. SecurityManager将任务委托给Authorizer
|
||||
3. Authorizier会调用所有的Authorizing Realm直到该role被分配给subject。如果所有realm都没有授予subject该role,那么访问失败,返回false。
|
||||
4. Authorizing Realm的AuthorizationInfo.getRoles方法会获取所有分配给该subject的role
|
||||
5. 如果待检测的role在getRoles返回的role list中,那么授权成功,subject可以对该资源进行访问
|
||||
### 基于permission的authorization
|
||||
当subject的isPermitted或checkPermission方法被调用时,其执行流程如下:
|
||||
1. subject将检测Permission的任务委托给SecurityManager
|
||||
2. SecurityManager将该任务委托给Authorizer
|
||||
3. Authorizer会以此访问所有的Authorizer Realm直到Permission被授予。如果所有的realm都没有授予该subject权限,那么subject授权失败。
|
||||
4. Realm按照如下顺序来检测Permission是否被授予:
|
||||
1. 其会调用AuthorizationInfo的getObjectPermissions方法和getStringPermissions方法并聚合结果,从而获取直接分配给该subject的所有权限
|
||||
2. 如果RolePermissionRegister被注册,那么会根据subject被授予的role来获取role相关的permission,根据RolePermissionResolver.resolvePermissionsInRole()方法
|
||||
3. 对于上述返回的权限集合,implies方法会被调用,用来检测待检测权限是否隐含在其中
|
||||
@@ -1,73 +1,175 @@
|
||||
# Apache Shiro
|
||||
## Shiro简介
|
||||
Shiro是一个简单易用且功能强大的Java安全框架,用于实现认证、授权、加密、session管理等场景,并且Shiro可以被用于任何应用,包括命令行应用、移动应用、大型web应用或是企业应用。
|
||||
Shiro在如下方面提供Security API:
|
||||
- Authentication:为用户提供身份认证
|
||||
- Authorization:为应用提供用户的访问控制
|
||||
- 加密:避免应用数据处于明文状态
|
||||
- session管理:每个用户的时间敏感状态
|
||||
## Shiro中常用的概念
|
||||
Shiro框架的结构主要分为三个部分:Subject、SecurityManager、Realms
|
||||
### Subject
|
||||
Subject:Subject是一个安全术语,通常意味着当前执行的用户。
|
||||
```java
|
||||
import org.apache.shiro.subject.Subject;
|
||||
import org.apache.shiro.SecurityUtils;
|
||||
// 获取Subject对象
|
||||
Subject currentUser = SecurityUtils.getSubject();
|
||||
```
|
||||
### SecurityManager
|
||||
相对于Subject代表对于当前用户的安全操作,SecurityManager代表对所有用户的安全操作。SecurityManager是Shiro结构的核心,其由多个security component嵌套组成。
|
||||
> 一旦SecurityManager和其嵌套的security component被配置完成,那么用户将不再使用SecurityManager而是调用Subject API。
|
||||
|
||||
对每个应用中,只存在一个SecurityManager,SecurityManager是应用范围内的单例。默认SecurityManager的实现是POJO,可以通过java代码、Spring xml、yaml、properties等方式来进行配置。
|
||||
|
||||
### realms
|
||||
realms是shiro和应用中security data(如账户登录的登录数据或授权管理的权限数据)之间的连接器。当Shiro和security data进行交互时,shiro会从配置好的一个或者多个realm中获取security data。
|
||||
> 在上述情况下,realm类似于一个安全数据的特定DAO,其封装了到数据源连接的详细信息并且为Shiro提供安全数据。当配置Shiro时,必须指定至少一个Realm用于身份认证和权限认证。
|
||||
|
||||
> Shiro提供了开箱即用的Realm来连接很多种security data source,比如LDAP,关系型数据库(JDBC),基于文本配置的data source例如ini或properties文件。可以通过自定义Realm的实现来访问自定义的data source,如果默认的Realm不能满足需求。
|
||||
|
||||
## Authentication
|
||||
认证过程用于认证用户的身份。认证过程的主要流程如下:
|
||||
1. 收集用户的身份标识信息(通常称之为主体,principal)和身份证明(通常称之为凭据,credentials)
|
||||
2. 向系统提交主体和凭据
|
||||
3. 如果提交的凭据和系统中该主体对应的凭据相同,那么该用户会被认为是“通过认证的”。如果提交的凭据不符,那么该用户会被认为是“认证失败的”。
|
||||
```java
|
||||
// 认证流程代码
|
||||
//1. Acquire submitted principals and credentials:
|
||||
AuthenticationToken token =
|
||||
new UsernamePasswordToken(username, password);
|
||||
//2. Get the current Subject:
|
||||
Subject currentUser = SecurityUtils.getSubject();
|
||||
|
||||
//3. Login:
|
||||
currentUser.login(token);
|
||||
```
|
||||
当login方法被调用时,SecurityMananger将会收到AuthenticationToken并且将其分配到一个或多个已经配置好的Realm中,并且让每个Realm执行需要的认证流程。每个Reaml都能对提交的AuthenticaitonToken做出处理。
|
||||
如果认证过程失败,那么会抛出AuthenticationException,可以通过捕获该异常来对失败的认证进行处理。
|
||||
```java
|
||||
//3. Login:
|
||||
try {
|
||||
currentUser.login(token);
|
||||
} catch (IncorrectCredentialsException ice) { …
|
||||
} catch (LockedAccountException lae) { …
|
||||
}
|
||||
…
|
||||
catch (AuthenticationException ae) {…
|
||||
}
|
||||
```
|
||||
**当用户通过身份认证之后,其被认为是“通过身份认证的”,并且被允许使用应用。但是,通过身份认证并不意味着可以在系统中执行任何操作。通过授权,可以决定用户能够在系统中执行哪些操作。**
|
||||
|
||||
## Authorization
|
||||
Authorization的实质是访问控制,通常用于控制用户在系统中能够使用哪些资源。大多数情况下,可以通过role或permission等形式来实现访问控制,用户通过分配给他们的角色或者权限来执行操作。通过检查用户的role或者permission,系统可以决定将哪些功能暴露给用户。
|
||||
```java
|
||||
// 通过如下代码,Subject可以实现对用户的role检测
|
||||
if ( subject.hasRole(“administrator”) ) {
|
||||
//show the ‘Create User’ button
|
||||
} else {
|
||||
//grey-out the button?
|
||||
}
|
||||
```
|
||||
> 通过role来实现权限认证可能会存在问题,
|
||||
|
||||
- [Apache Shiro](#apache-shiro)
|
||||
- [Shiro简介](#shiro简介)
|
||||
- [Shiro中常用的概念](#shiro中常用的概念)
|
||||
- [Subject](#subject)
|
||||
- [SecurityManager](#securitymanager)
|
||||
- [realms](#realms)
|
||||
- [Authentication](#authentication)
|
||||
- [Authorization](#authorization)
|
||||
- [Session Management](#session-management)
|
||||
- [Shiro Session可在任何应用中使用](#shiro-session可在任何应用中使用)
|
||||
- [Shiro加密](#shiro加密)
|
||||
- [shiro hash](#shiro-hash)
|
||||
- [Shiro Ciphers](#shiro-ciphers)
|
||||
- [Shiro框架的Web支持](#shiro框架的web支持)
|
||||
- [Web Session管理](#web-session管理)
|
||||
- [Shiro Native Session](#shiro-native-session)
|
||||
|
||||
# Apache Shiro
|
||||
## Shiro简介
|
||||
Shiro是一个简单易用且功能强大的Java安全框架,用于实现认证、授权、加密、session管理等场景,并且Shiro可以被用于任何应用,包括命令行应用、移动应用、大型web应用或是企业应用。
|
||||
Shiro在如下方面提供Security API:
|
||||
- Authentication:为用户提供身份认证
|
||||
- Authorization:为应用提供用户的访问控制
|
||||
- 加密:避免应用数据处于明文状态
|
||||
- session管理:每个用户的时间敏感状态
|
||||
## Shiro中常用的概念
|
||||
Shiro框架的结构主要分为三个部分:Subject、SecurityManager、Realms
|
||||
### Subject
|
||||
Subject:Subject是一个安全术语,通常意味着当前执行的用户。
|
||||
```java
|
||||
import org.apache.shiro.subject.Subject;
|
||||
import org.apache.shiro.SecurityUtils;
|
||||
// 获取Subject对象
|
||||
Subject currentUser = SecurityUtils.getSubject();
|
||||
```
|
||||
### SecurityManager
|
||||
相对于Subject代表对于当前用户的安全操作,SecurityManager代表对所有用户的安全操作。SecurityManager是Shiro结构的核心,其由多个security component嵌套组成。
|
||||
> 一旦SecurityManager和其嵌套的security component被配置完成,那么用户将不再使用SecurityManager而是调用Subject API。
|
||||
|
||||
对每个应用中,只存在一个SecurityManager,SecurityManager是应用范围内的单例。默认SecurityManager的实现是POJO,可以通过java代码、Spring xml、yaml、properties等方式来进行配置。
|
||||
|
||||
### realms
|
||||
realms是shiro和应用中security data(如账户登录的登录数据或授权管理的权限数据)之间的连接器。当Shiro和security data进行交互时,shiro会从配置好的一个或者多个realm中获取security data。
|
||||
> 在上述情况下,realm类似于一个安全数据的特定DAO,其封装了到数据源连接的详细信息并且为Shiro提供安全数据。当配置Shiro时,必须指定至少一个Realm用于身份认证和权限认证。
|
||||
|
||||
> Shiro提供了开箱即用的Realm来连接很多种security data source,比如LDAP,关系型数据库(JDBC),基于文本配置的data source例如ini或properties文件。可以通过自定义Realm的实现来访问自定义的data source,如果默认的Realm不能满足需求。
|
||||
|
||||
## Authentication
|
||||
认证过程用于认证用户的身份。认证过程的主要流程如下:
|
||||
1. 收集用户的身份标识信息(通常称之为主体,principal)和身份证明(通常称之为凭据,credentials)
|
||||
2. 向系统提交主体和凭据
|
||||
3. 如果提交的凭据和系统中该主体对应的凭据相同,那么该用户会被认为是“通过认证的”。如果提交的凭据不符,那么该用户会被认为是“认证失败的”。
|
||||
```java
|
||||
// 认证流程代码
|
||||
//1. Acquire submitted principals and credentials:
|
||||
AuthenticationToken token =
|
||||
new UsernamePasswordToken(username, password);
|
||||
//2. Get the current Subject:
|
||||
Subject currentUser = SecurityUtils.getSubject();
|
||||
|
||||
//3. Login:
|
||||
currentUser.login(token);
|
||||
```
|
||||
当login方法被调用时,SecurityMananger将会收到AuthenticationToken并且将其分配到一个或多个已经配置好的Realm中,并且让每个Realm执行需要的认证流程。每个Reaml都能对提交的AuthenticaitonToken做出处理。
|
||||
如果认证过程失败,那么会抛出AuthenticationException,可以通过捕获该异常来对失败的认证进行处理。
|
||||
```java
|
||||
//3. Login:
|
||||
try {
|
||||
currentUser.login(token);
|
||||
} catch (IncorrectCredentialsException ice) { …
|
||||
} catch (LockedAccountException lae) { …
|
||||
}
|
||||
…
|
||||
catch (AuthenticationException ae) {…
|
||||
}
|
||||
```
|
||||
**当用户通过身份认证之后,其被认为是“通过身份认证的”,并且被允许使用应用。但是,通过身份认证并不意味着可以在系统中执行任何操作。通过授权,可以决定用户能够在系统中执行哪些操作。**
|
||||
|
||||
## Authorization
|
||||
Authorization的实质是访问控制,通常用于控制用户在系统中能够使用哪些资源。大多数情况下,可以通过role或permission等形式来实现访问控制,用户通过分配给他们的角色或者权限来执行操作。通过检查用户的role或者permission,系统可以决定将哪些功能暴露给用户。
|
||||
```java
|
||||
// 通过如下代码,Subject可以实现对用户的role检测
|
||||
if ( subject.hasRole(“administrator”) ) {
|
||||
//show the ‘Create User’ button
|
||||
} else {
|
||||
//grey-out the button?
|
||||
}
|
||||
|
||||
// 通过如下代码,可以实现权限分配而不是角色检测
|
||||
if ( subject.isPermitted(“user:create”) ) {
|
||||
//show the ‘Create User’ button
|
||||
} else {
|
||||
//grey-out the button?
|
||||
}
|
||||
```
|
||||
权限控制甚至支持非常细粒度的权限,譬如实例级别的权限控制。
|
||||
```java
|
||||
// 如下代码检测用户是否拥有删除jsmith用户的权限
|
||||
if ( subject.isPermitted(“user:delete:jsmith”) ) {
|
||||
//delete the ‘jsmith’ user
|
||||
} else {
|
||||
//don’t delete ‘jsmith’
|
||||
}
|
||||
```
|
||||
和Authentication类似,Authorization也会进入SecurityManager,并且通过一个或者多个Realm来决定是否允许访问。
|
||||
**根据需要,Realm既会响应Authentication过程,也会响应Authority过程**
|
||||
|
||||
## Session Management
|
||||
Apache提供了一致的会话管理,在任何类型和架构的程序中都可以使用,该Session API从小型的守护进程到大型集群web应用都可以使用。
|
||||
并且,Shiro提供的Session API是容器无关的,在任何容器环境下Session API都相同。Shiro结构也支持可插入的session存储,可以将session存储在企业缓存、关系型数据库或者nosql中。
|
||||
Shiro Seesion的另一个好处是Shiro Session可以在不同技术实现的客户端之间进行共享,譬如Swing桌面客户端可以和web客户端一起加入同一个应用会话(用户同时使用swing客户端和web客户端时很有用)。
|
||||
```java
|
||||
/**
|
||||
* 用户获取Session
|
||||
*/
|
||||
// 如果当前用户存在会话,则获取已存在会话,
|
||||
// 如果当前用户不存在会话,则创建新的会话
|
||||
Session session = subject.getSession();
|
||||
// 接受一个boolean值,该boolean值标识如果当前用户不存在会话,是否创建一个新的会话
|
||||
Session session = subject.getSession(boolean create);
|
||||
```
|
||||
### Shiro Session可在任何应用中使用
|
||||
```java
|
||||
Session session = subject.getSession();
|
||||
session.getAttribute(“key”, someValue);
|
||||
Date start = session.getStartTimestamp();
|
||||
Date timestamp = session.getLastAccessTime();
|
||||
session.setTimeout(millis);
|
||||
```
|
||||
## Shiro加密
|
||||
Shiro加密是独立于用户(Subject)的,故而Shiro加密可以独立于Subject使用。
|
||||
Shiro加密主要关注于如下两个模块:
|
||||
- hash加密(又名消息摘要,message digest)
|
||||
- ciphers加密
|
||||
### shiro hash
|
||||
在Shiro hash中,如果想要快速计算文件的MD5值,可以通过如下方式快速计算:
|
||||
```java
|
||||
String hex = new Md5Hash(myFile).toHex();
|
||||
```
|
||||
在shiro hash中,如果想要对密码进行sha-512进行hash操作并且对结果进行base64编码成可打印字符串,可以进行如下操作:
|
||||
```java
|
||||
String encodedPassword =
|
||||
new Sha512Hash(password, salt, count).toBase64();
|
||||
```
|
||||
Shiro框架在很大程度上简化了hash和编码。
|
||||
### Shiro Ciphers
|
||||
Ciphers是一种算法,可以通过指定的key将加密后的数据还原到加密之前(不同于hash操作,hash操作通常是不可逆的)。通常用Ciphers来保证数据传输时的安全,防止数据在传输时被窥探。
|
||||
Shiro通过引入CipherService API来简化加密的流程,**CipherService是一个简单,无状态并且线程安全的API,可以对应用数据进行加密或者解密操作。**
|
||||
```java
|
||||
// Shiro CipherService对数据进行加密操作
|
||||
|
||||
AesCipherService cipherService = new AesCipherService();
|
||||
cipherService.setKeySize(256);
|
||||
//create a test key:
|
||||
byte[] testKey = cipherService.generateNewKey();
|
||||
|
||||
//encrypt a file’s bytes:
|
||||
byte[] encrypted =
|
||||
cipherService.encrypt(fileBytes, testKey);
|
||||
```
|
||||
## Shiro框架的Web支持
|
||||
### Web Session管理
|
||||
对于web应用,shiro默认情况下其session会使用已经存在的servlet容器session。当使用subject.getSession()或者subject.getSession(boolean)获取session实例时,**Shiro将会返回一个基于Servlet容器的HttpSession实例来作为Session实例返回值**。
|
||||
> 对于Shiro来说,其业务层代码通过subject.getSession来获取Shiro Session实例,即使当前运行于Servlet容器,业务层代码在与Shiro Session交互时也不知道与其交互的是HttpSession。
|
||||
> 故而在使用Shiro Session时,其Session是独立与环境的,Web应用和非Web应用都可以通过相同的Shiro Session API与Shiro Session进行交互,而Shiro Session是否是基于Servlet容器的HttpSession,用户是无感知的。
|
||||
|
||||
### Shiro Native Session
|
||||
当启用Shiro Native Session之后,对于Web应用,如果想使用Shiro Session来代替基于Servlet容器的HttpSession,无需修改HttpServletRequest.getSession()和HttpSession API为Shiro Session API。
|
||||
Shiro Session完全实现了Servlet Session的标准,以此支持Shiro Session在Web应用中的使用。在使用了Shiro Native Session后,任何对HttpServletRequest和HttpSession API的调用都会被Shiro拦截,Shiro会用Shiro Native Session API来代理这些请求。
|
||||
> **故而,当想要在Web环境中使用Shiro Session API时,无需变动Web环境先前未使用Shiro Session API时的任何代码。**
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
975
spring/Spring Cloud/CircuitBreaker/CircuitBreaker.md
Normal file
975
spring/Spring Cloud/CircuitBreaker/CircuitBreaker.md
Normal file
@@ -0,0 +1,975 @@
|
||||
- [CircuitBreaker](#circuitbreaker)
|
||||
- [Introduction](#introduction)
|
||||
- [maven](#maven)
|
||||
- [CircuitBreaker](#circuitbreaker-1)
|
||||
- [State Machine](#state-machine)
|
||||
- [Sliding Window](#sliding-window)
|
||||
- [Count-based sliding window](#count-based-sliding-window)
|
||||
- [Time-based sliding window](#time-based-sliding-window)
|
||||
- [Failure rate and slow call rate thresholds](#failure-rate-and-slow-call-rate-thresholds)
|
||||
- [Failure rate \& exception list](#failure-rate--exception-list)
|
||||
- [Slow call rate](#slow-call-rate)
|
||||
- [CircuitBreaker in `OPEN`/`HALF_OPEN` state](#circuitbreaker-in-openhalf_open-state)
|
||||
- [Special States](#special-states)
|
||||
- [thread-safe](#thread-safe)
|
||||
- [CircuitBreakerRegistry](#circuitbreakerregistry)
|
||||
- [Create and configure CircuitBreakerConfig](#create-and-configure-circuitbreakerconfig)
|
||||
- [success/failure/ignore判断流程](#successfailureignore判断流程)
|
||||
- [Decorate and execute a functional interface](#decorate-and-execute-a-functional-interface)
|
||||
- [Consume emitted RegistryEvents](#consume-emitted-registryevents)
|
||||
- [consume emitted CircuitBreakerEvents](#consume-emitted-circuitbreakerevents)
|
||||
- [Bulkhead](#bulkhead)
|
||||
- [create a BulkheadRegistry](#create-a-bulkheadregistry)
|
||||
- [Create and configure a Bulkhead](#create-and-configure-a-bulkhead)
|
||||
- [Create and configure a ThreadPoolBulkhead](#create-and-configure-a-threadpoolbulkhead)
|
||||
- [decorate and execute a functional interface](#decorate-and-execute-a-functional-interface-1)
|
||||
- [consume emitted RegistryEvents](#consume-emitted-registryevents-1)
|
||||
- [consume emitted BulkheadEvents](#consume-emitted-bulkheadevents)
|
||||
- [ThreadPoolBulkhead弊端](#threadpoolbulkhead弊端)
|
||||
- [RateLimiter](#ratelimiter)
|
||||
- [Create RateLimiterRegistry](#create-ratelimiterregistry)
|
||||
- [Create and configure a RateLimiter](#create-and-configure-a-ratelimiter)
|
||||
- [decorate and execute a functional interface](#decorate-and-execute-a-functional-interface-2)
|
||||
- [consume emitted RegistryEvents](#consume-emitted-registryevents-2)
|
||||
- [consume emitted RateLimiterEvents](#consume-emitted-ratelimiterevents)
|
||||
- [Retry](#retry)
|
||||
- [Create a RetryRegistry](#create-a-retryregistry)
|
||||
- [Create and configure Retry](#create-and-configure-retry)
|
||||
- [Decorate and execute a functional interface](#decorate-and-execute-a-functional-interface-3)
|
||||
- [consume emitted RegistryEvents](#consume-emitted-registryevents-3)
|
||||
- [use custom IntervalFunction](#use-custom-intervalfunction)
|
||||
- [TimeLimiter](#timelimiter)
|
||||
- [Create a TimeLimiterRegistry](#create-a-timelimiterregistry)
|
||||
- [Create and configure TimeLimiter](#create-and-configure-timelimiter)
|
||||
- [decorate and execute a functional interface](#decorate-and-execute-a-functional-interface-4)
|
||||
- [Spring Cloud CircuitBreaker](#spring-cloud-circuitbreaker)
|
||||
- [core concepts](#core-concepts)
|
||||
- [Configuration](#configuration)
|
||||
|
||||
|
||||
# CircuitBreaker
|
||||
resilience4j是一个轻量级的fault tolerance library,其针对函数式编程进行设计。resilence4j提供了更高阶的函数(`decorator`)来对`function interface, lambda expression, method reference`等内容进行增强。
|
||||
|
||||
decorators包含如下分类:
|
||||
- CircuitBreaker
|
||||
- Rate Limiter
|
||||
- Retry
|
||||
- Bulkhead
|
||||
|
||||
对于任何`function interface, lambda expression, method reference`,都可以使用多个decorators进行装饰。
|
||||
|
||||
## Introduction
|
||||
在如下示例中,会展示如何通过CircuitBreaker和Retry来对lambda expression进行装饰,令lambda在发生异常时最多重试3次。
|
||||
|
||||
可以针对多次retry之间的interval进行配置,也支持自定义的backoff algorithm。
|
||||
|
||||
```java
|
||||
// Create a CircuitBreaker with default configuration
|
||||
CircuitBreaker circuitBreaker = CircuitBreaker
|
||||
.ofDefaults("backendService");
|
||||
|
||||
// Create a Retry with default configuration
|
||||
// 3 retry attempts and a fixed time interval between retries of 500ms
|
||||
Retry retry = Retry
|
||||
.ofDefaults("backendService");
|
||||
|
||||
// Create a Bulkhead with default configuration
|
||||
Bulkhead bulkhead = Bulkhead
|
||||
.ofDefaults("backendService");
|
||||
|
||||
Supplier<String> supplier = () -> backendService
|
||||
.doSomething(param1, param2)
|
||||
|
||||
// Decorate your call to backendService.doSomething()
|
||||
// with a Bulkhead, CircuitBreaker and Retry
|
||||
// **note: you will need the resilience4j-all dependency for this
|
||||
Supplier<String> decoratedSupplier = Decorators.ofSupplier(supplier)
|
||||
.withCircuitBreaker(circuitBreaker)
|
||||
.withBulkhead(bulkhead)
|
||||
.withRetry(retry)
|
||||
.decorate();
|
||||
|
||||
// When you don't want to decorate your lambda expression,
|
||||
// but just execute it and protect the call by a CircuitBreaker.
|
||||
String result = circuitBreaker
|
||||
.executeSupplier(backendService::doSomething);
|
||||
|
||||
// You can also run the supplier asynchronously in a ThreadPoolBulkhead
|
||||
ThreadPoolBulkhead threadPoolBulkhead = ThreadPoolBulkhead
|
||||
.ofDefaults("backendService");
|
||||
|
||||
// The Scheduler is needed to schedule a timeout
|
||||
// on a non-blocking CompletableFuture
|
||||
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(3);
|
||||
TimeLimiter timeLimiter = TimeLimiter.of(Duration.ofSeconds(1));
|
||||
|
||||
CompletableFuture<String> future = Decorators.ofSupplier(supplier)
|
||||
.withThreadPoolBulkhead(threadPoolBulkhead)
|
||||
.withTimeLimiter(timeLimiter, scheduledExecutorService)
|
||||
.withCircuitBreaker(circuitBreaker)
|
||||
.withFallback(asList(TimeoutException.class,
|
||||
CallNotPermittedException.class,
|
||||
BulkheadFullException.class),
|
||||
throwable -> "Hello from Recovery")
|
||||
.get().toCompletableFuture();
|
||||
```
|
||||
### maven
|
||||
resilence4j需要jdk17及以上,如果使用maven,可以按照如下方式来引入
|
||||
|
||||
引入所有包的方式如下
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>io.github.resilience4j</groupId>
|
||||
<artifactId>resilience4j-all</artifactId>
|
||||
<version>${resilience4jVersion}</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
按需引入方式如下
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>io.github.resilience4j</groupId>
|
||||
<artifactId>resilience4j-circuitbreaker</artifactId>
|
||||
<version>${resilience4jVersion}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.github.resilience4j</groupId>
|
||||
<artifactId>resilience4j-ratelimiter</artifactId>
|
||||
<version>${resilience4jVersion}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.github.resilience4j</groupId>
|
||||
<artifactId>resilience4j-retry</artifactId>
|
||||
<version>${resilience4jVersion}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.github.resilience4j</groupId>
|
||||
<artifactId>resilience4j-bulkhead</artifactId>
|
||||
<version>${resilience4jVersion}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.github.resilience4j</groupId>
|
||||
<artifactId>resilience4j-cache</artifactId>
|
||||
<version>${resilience4jVersion}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.github.resilience4j</groupId>
|
||||
<artifactId>resilience4j-timelimiter</artifactId>
|
||||
<version>${resilience4jVersion}</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
## CircuitBreaker
|
||||
### State Machine
|
||||
CircuitBreaker通过有限状态机实现,其拥有如下状态:
|
||||
- `CLOSED`
|
||||
- `OPEN`
|
||||
- `HALF_OPEN`
|
||||
- `METRICS_ONLY`
|
||||
- `DISABLED`
|
||||
- `FORCED_OPEN`
|
||||
|
||||
其中,前三个状态为正常状态,后三个状态为特殊状态。
|
||||
|
||||
<img alt="" loading="lazy" src="https://files.readme.io/39cdd54-state_machine.jpg" title="state_machine.jpg" align="" caption="" height="auto" width="auto">
|
||||
|
||||
上述circuitbreaker状态转换逻辑如下所示:
|
||||
- 处于`CLOSED`状态时,如果实际接口的失败率超过上限后,会从`CLOSED`状态转换为`OPEN`状态
|
||||
- 处于`OPEN`状态下经过一段时间后,会从`OPEN`状态转换为`HALF_OPEN`状态
|
||||
- 处于`HALF_OPEN`状态下,如果失败率小于上限,会从`HALF_OPEN`状态重新变为`CLOSED`状态
|
||||
- 如果`HALF_OPEN`状态下,失败率仍然超过上限,则会从`HALF_OPEN`状态重新变为`OPEN`状态
|
||||
|
||||
### Sliding Window
|
||||
CircuitBreaker会使用`滑动窗口`来存储和聚合调用结果。在使用CircuitBreaker时,可以选择`count-based`的滑动窗口还是`time-based`滑动窗口。
|
||||
|
||||
- `count-based`:`count-based`滑动窗口会对最近`N`次调用的结果进行聚合
|
||||
- `time-based`:`time-based`滑动窗口将会对最近`N`秒的调用结果进行聚合
|
||||
|
||||
#### Count-based sliding window
|
||||
count-based sliding window是通过循环数组来实现的,循环数组中包含了n个measurements。如果count window的大小为10,那么circular array一直都会有10个measurements。
|
||||
|
||||
count-based的滑动窗口实现会`total aggregation`结果进行更新,更新逻辑如下:
|
||||
- 当一个新的调用返回结果后,其结果将会被记录,并且total aggregation也会被更新,将新调用的结果加到total aggregation中
|
||||
- 发生新调用时,循环数组中最老(oldest)的measurement将会被淘汰,并且measurement也会从total aggregation中被减去,bucket也会被重置(bucket即measurement,bucket被重置即代表oldest measurement会被重置)
|
||||
|
||||
对于聚合结果的检查的开销是`O(1)`的,因为其是`pre-aggregated`的,并且和window size无关。
|
||||
|
||||
#### Time-based sliding window
|
||||
Time-based sliding window其也是通过循环数组实现,数组中含有`N`个partial aggregation(bucket)。
|
||||
|
||||
如果time window大小是10秒,那么circular array一直都会有10的buckets。每个bucket都对应了一个epoch second,bucket会对该epoch second内发生的调用结果进行聚合。(`Partial aggregation`)。
|
||||
|
||||
在循环数组中,head buket中存储了当前epoch second中发生的调用结果,而其他的partial aggregation则存储的是之前second发生的调用结果。在Time-based的滑动窗口实现中,并不会像`Count-based`那样独立的存储调用结果,而是增量的对`partial aggregation`进行更新。
|
||||
|
||||
除了更新`Partial aggregation`外,time-based滑动窗口还会在新请求结果返回时,对`total aggregation`进行更新。当oldest bucket被淘汰时,该bucket的partial aggregation也会从total aggregation中被减去,并且bucket也会被重置。
|
||||
|
||||
检查聚合结果的开销也是`O(1)`的,Time-based滑动窗口也是`pre-aggregated`的。
|
||||
|
||||
partial aggregation中包含了3个integer,用于记录如下信息:
|
||||
- failed calls次数
|
||||
- slow calls次数
|
||||
- 总共的call次数
|
||||
|
||||
除此之外,partial aggregation中还会包含一个long,用于存储所有请求的总耗时
|
||||
|
||||
### Failure rate and slow call rate thresholds
|
||||
#### Failure rate & exception list
|
||||
当failure rate`大于等于`配置的threshold时,CircuitBreaker的状态将会从`CLOSED`变为`OPEN`。
|
||||
|
||||
默认情况下,所有的抛出的异常都会被统计为failure,在使用时也可以指定一个`exception list`,在exception list中的异常才会被统计为failure,而不在exception list中的异常会被视为success。除此之外,还可以对异常进行`ignored`,被忽视的异常`既不会被统计为success,也不会被统计为failure`。
|
||||
|
||||
#### Slow call rate
|
||||
当slow call rate大于或等于配置的threshold时,CircuitBreaker的状态也会从`CLOSED`变为`OPEN`。通过slow call rate,可以降低外部系统的负载。
|
||||
|
||||
只有`当记录的call数量达到最小数量时`,failure rate和slow call rate才能被计算。例如,`minimum number of required calls`为10,只有当被记录的calls次数达到10时,failure rate和slow call rate才能被计算。`如果当前只记录了9个calls,即使9次调用全部都失败,circuitbreaker也不会变为open状态。`
|
||||
|
||||
#### CircuitBreaker in `OPEN`/`HALF_OPEN` state
|
||||
circuitbreaker在`OPEN`状态时,会拒绝所有的调用,并且抛出`CallNotPermittedException`。在等待一段时间后,`CircuitBreaker`将会从`OPEN`状态转为`HALF_OPEN`状态,并允许一个`configurable number`数量的请求进行实际调用,从而检测是否backend已经恢复并且可以再次访问。
|
||||
|
||||
处于`HALF_OPEN`状态的circuitbreaker,假设`permittedNumberOfCalls`的数量为10,此时存在20个调用,那么前10个调用都能正常调用,而后10个调用将会被拒绝,并且抛出`CallNotPermittedException`。
|
||||
|
||||
在`HALF_OPEN`状态下,如果failure rate或是slow call rate大于等于配置的threshold,那么circuitbreaker状态将会转为OPEN。如果failure rate和slow call rate小于threshold,那么circuitbreaker状态将变为CLOSED。
|
||||
|
||||
### Special States
|
||||
CircuitBreaker支持3个特殊状态:
|
||||
- `METRICS_ONLY`:处于该状态时,其行为如下
|
||||
- 所有`circuit breaker events`都会正常生成(除了state transition外),并且metrics会正常记录
|
||||
- 该状态和`CLOSED`状态类似,但是circuitbreaker在threshold达到时,不会切换为OPEN状态
|
||||
- `DISABLED`:
|
||||
- 没有`CircuitBreakerEvent`会被产生,metrics也不会被记录
|
||||
- 会允许所有的访问
|
||||
- `FORCED_OPEN`:
|
||||
- 没有`CircuitBreakerEvent`会被产生,metrics也不会被记录
|
||||
- 会拒绝所有的访问
|
||||
|
||||
退出这些特殊状态的方式如下:
|
||||
- 触发state transition
|
||||
- 对CircuitBreaker执行reset
|
||||
|
||||
### thread-safe
|
||||
`CircuitBreaker线程安全`,但是CircuitBreaker并不会对function call进行串行化,故而`在使用CircuitBreaker时,function call可能会并行执行`。
|
||||
|
||||
对于Closed状态的CircuitBreaker而言,如果20个线程同时对cirbuitbreaker进行访问,那么所有的方法调用都能同时并行执行,即使滑动窗口的大小为`15`小于并行数量。`滑动窗口的大小不会对方法的并行程度造成影响`。
|
||||
|
||||
如果想要对并行程度做出限制,可以使用`Bulkhead`。
|
||||
|
||||
### CircuitBreakerRegistry
|
||||
resilence4j附带了一个`in-memory`的`CircuitBreakerRegistry`,基于`ConcurrentHashMap`实现。可以通过`CircuitBreakerRegistry`来管理(创建和获取)CircuitBreaker实例。可以按照如下示例,根据`默认的CircuitBreakerConfig`来创建`CircuitBreakerRegistry`:
|
||||
```java
|
||||
CircuitBreakerRegistry circuitBreakerRegistry =
|
||||
CircuitBreakerRegistry.ofDefaults();
|
||||
```
|
||||
|
||||
#### Create and configure CircuitBreakerConfig
|
||||
除了使用默认的CircuitBreakerConfig外,还可以提供自定义的`CircuitBreakerConfig`,对象可以通过builder来构建。
|
||||
|
||||
`CircuitBreakerConfig`的可配置属性如下:
|
||||
- `failureRateThreshold`:配置failure rate threshold的默认百分比
|
||||
- `default value`: 50
|
||||
- `description`:当failure rate`大于等于`该threshold值时,CircuitBreaker会切为`OPEN`状态,并开始`short-circuiting calls`
|
||||
- `slowCallRateThreshold`:配置threshold百分比
|
||||
- `default value`: 100
|
||||
- `description`: 当slow calls的百分比等于或超过该threshold时,CircuitBreaker会切换到`OPEN`状态,并且开始short-circuiting calls
|
||||
- `slowCallDurationThreshold`: 配置slow calls的duration threshold
|
||||
- `default value`: 60000 [ms]
|
||||
- `description`: 当call的耗时超过该duration threshold限制时,会被认定为slow call,并且会增加slow call rate
|
||||
- `permittedNumberOfCallsInHalfOpenState`:
|
||||
- `default value`: 10
|
||||
- `description`:配置circuitbreaker切换到half open状态时,permitted calls的数量
|
||||
- `maxWairDurationInfHalfOpenState`:
|
||||
- `default value`: 0 [ms]
|
||||
- `description`:配置`在CircuitBreaker从Half Open状态切换回Open状态前,其可以处于Half Open抓过你太的最长时间`。默认值为`0`,代表其等待时间没有上限,直到所有的permitted calls都执行完成
|
||||
- `slidingWindowType`:配置滑动窗口的类型
|
||||
- `default value`: `COUNT_BASED`
|
||||
- `desceiption`:在CircuitBreaker处于closed状态时,滑动窗口用于记录调用的结果。滑动窗口类型可以是`count-based`和`time-based`
|
||||
- `counted_based`:会记录最后`slidingWindowSize`个请求的结果并对其进行聚合
|
||||
- `time_based`:会记录最后`slidingWindowSize`秒的调用结果,并且会对其进行聚合
|
||||
- `slidingWindowSize`:
|
||||
- `default value`: 100
|
||||
- `description`:用于配置滑动窗口的大小
|
||||
- `minimumNumberOfCalls`:
|
||||
- `default value`:100
|
||||
- `description`: 在可以计算error rate和slow call rate之前,至少需要记录`minimum number`个调用。例如,如果`minimumNumberOfCalls`为10,那么在可以计算failure rate前,必须至少记录10个calls(范围是整个滑动窗口期间范围内)。如果已经计算了9个calls,即使9个calls都调用失败,在记录的请求数达到`minimumNumberOfCalls`之前,也不会切换到open状态
|
||||
- `waitDurationInOpenState`:
|
||||
- `default value`: 60000 [ms]
|
||||
- `description`:该值代表`处于OPEN状态的CircuitBreaker`在切换为`HALF-OPEN`之前,会等待的时间长度
|
||||
- `automaticTransitionFromOpenToHalfOpenEnabled`:
|
||||
- `default value`: false
|
||||
- `description`:
|
||||
- `如果该值设置为true`,会创建一个线程来对所有`CircuitBreakers`对象进行监控,并在waitDurationInOpenState设置的时间达到后将CircuitBreaker从Open切换到HALF_OPEN状态
|
||||
- `如果该值设置为false(默认)`,那么`CircuitBreaker`从`OPEN`状态切换到`HALF_OPEN`状态的过程并不由专门的线程来触发,`而是由请求来触发`。如果该值设置为false(默认),即使`waitDurationInOpenState`设置的时间达到,如果没有后续请求,那么从`OPEN`到`HALF_OPEN`的变化也不会自动被触发
|
||||
- `recordExceptions`:
|
||||
- `default value`: empty
|
||||
- `description`: 该属性用于配置exception list,位于该exception list中的异常类型将会被视为failure,并增加failure rate。
|
||||
- 任何匹配或者继承exception list中的异常将会被视为failure,除非通过`ignoreExceptions`显式的忽略了该异常
|
||||
- 如果通过`recordExcepions`指定了exception list,那么所有其他的异常都会被视为`success`,除非显式被`ignoreExceptions`指定
|
||||
- `ignoreExceptions`:
|
||||
- `default value`: empty
|
||||
- `description`: 用于配置exception list,该list中配置的异常类型将会被忽略,既不会被记录为failure也不会被记录为success
|
||||
- 任何匹配或者继承了该list中异常类型的异常将会被忽略,即使该异常在`recordExceptions`中被指定
|
||||
- `recordFailurePredicate`:
|
||||
- `default value`: throwable -> true
|
||||
- `description`: 一个自定义的predicate,用于评估一个异常是否应该被记录为failure。如果该异常应当被记录为failure,那么该predicate应当返回true;如果该异常应当被记录为failure,那么该predicate应当返回false
|
||||
- `ignoreExceptionPredicate`:
|
||||
- `default value`: throwable -> false
|
||||
- `description`: 一个自定义的predicate,用于评估一个异常是否应当被忽略或是记录为failure/success。
|
||||
- 如果异常应当被忽略,那么该predicate应当返回true
|
||||
- 如果异常不应该被忽略,predicate应当返回为false,该异常应该被视为failure/success
|
||||
|
||||
##### success/failure/ignore判断流程
|
||||
在实际调用发生异常时,决定将异常视为success/failure/ignore的判断流程如下:
|
||||
- 如果实际调用未抛出异常,则记录为调用成功,否则继续
|
||||
- 如果抛出异常,首先会检查异常是否位于`Ignored Exceptions`中,如果位于其中,则忽略该异常,否则继续
|
||||
- 如果ignoreExceptionPredicate不为空,根据该predicate进行判断,如果返回为true,则忽略该异常,否则继续
|
||||
- 校验异常是否位于`recordExceptions`中,如果位于其中,则将其视为failure,否则继续
|
||||
- 如果recordFailurePredicate不为空,根据`recordFailurePredicate`判断是否该异常应当被视为failure,如果返回为true,将其视为failure,否则继续
|
||||
- 如果上述都不满足,那么将其视为success
|
||||
|
||||
创建自定义`CircuitBreakerConfig`的示例如下所示:
|
||||
```java
|
||||
// Create a custom configuration for a CircuitBreaker
|
||||
CircuitBreakerConfig circuitBreakerConfig = CircuitBreakerConfig.custom()
|
||||
.failureRateThreshold(50)
|
||||
.slowCallRateThreshold(50)
|
||||
.waitDurationInOpenState(Duration.ofMillis(1000))
|
||||
.slowCallDurationThreshold(Duration.ofSeconds(2))
|
||||
.permittedNumberOfCallsInHalfOpenState(3)
|
||||
.minimumNumberOfCalls(10)
|
||||
.slidingWindowType(SlidingWindowType.TIME_BASED)
|
||||
.slidingWindowSize(5)
|
||||
.recordException(e -> INTERNAL_SERVER_ERROR
|
||||
.equals(getResponse().getStatus()))
|
||||
.recordExceptions(IOException.class, TimeoutException.class)
|
||||
.ignoreExceptions(BusinessException.class, OtherBusinessException.class)
|
||||
.build();
|
||||
|
||||
// Create a CircuitBreakerRegistry with a custom global configuration
|
||||
CircuitBreakerRegistry circuitBreakerRegistry =
|
||||
CircuitBreakerRegistry.of(circuitBreakerConfig);
|
||||
|
||||
// Get or create a CircuitBreaker from the CircuitBreakerRegistry
|
||||
// with the global default configuration
|
||||
CircuitBreaker circuitBreakerWithDefaultConfig =
|
||||
circuitBreakerRegistry.circuitBreaker("name1");
|
||||
|
||||
// Get or create a CircuitBreaker from the CircuitBreakerRegistry
|
||||
// with a custom configuration
|
||||
CircuitBreaker circuitBreakerWithCustomConfig = circuitBreakerRegistry
|
||||
.circuitBreaker("name2", circuitBreakerConfig);
|
||||
```
|
||||
|
||||
除此之外,还可以在circuitRegistry中添加配置,该配置可以被多个CircuitBreaker实例共享
|
||||
```java
|
||||
CircuitBreakerConfig circuitBreakerConfig = CircuitBreakerConfig.custom()
|
||||
.failureRateThreshold(70)
|
||||
.build();
|
||||
|
||||
circuitBreakerRegistry.addConfiguration("someSharedConfig", config);
|
||||
|
||||
CircuitBreaker circuitBreaker = circuitBreakerRegistry
|
||||
.circuitBreaker("name", "someSharedConfig");
|
||||
```
|
||||
|
||||
并且,可以针对默认配置进行overwrite
|
||||
```java
|
||||
CircuitBreakerConfig defaultConfig = circuitBreakerRegistry
|
||||
.getDefaultConfig();
|
||||
|
||||
CircuitBreakerConfig overwrittenConfig = CircuitBreakerConfig
|
||||
.from(defaultConfig)
|
||||
.waitDurationInOpenState(Duration.ofSeconds(20))
|
||||
.build();
|
||||
```
|
||||
|
||||
如果不想使用CircuitBreakerRegistry来管理CircuitBreaker实例,也可以自己直接创建CircuitBreaker实例:
|
||||
```java
|
||||
// Create a custom configuration for a CircuitBreaker
|
||||
CircuitBreakerConfig circuitBreakerConfig = CircuitBreakerConfig.custom()
|
||||
.recordExceptions(IOException.class, TimeoutException.class)
|
||||
.ignoreExceptions(BusinessException.class, OtherBusinessException.class)
|
||||
.build();
|
||||
|
||||
CircuitBreaker customCircuitBreaker = CircuitBreaker
|
||||
.of("testName", circuitBreakerConfig);
|
||||
```
|
||||
|
||||
如果想要plugin in自己的registry实现,可以提供一个自定义的`RegistryStore`实现,并且通过Builder方法来plug in
|
||||
```java
|
||||
CircuitBreakerRegistry registry = CircuitBreakerRegistry.custom()
|
||||
.withRegistryStore(new YourRegistryStoreImplementation())
|
||||
.withCircuitBreakerConfig(CircuitBreakerConfig.ofDefaults())
|
||||
.build();
|
||||
```
|
||||
|
||||
### Decorate and execute a functional interface
|
||||
CircuitBreaker可以针对`callable, supplier, runnable, consumer, checkedrunnable, checkedsupplier, checkedconsumer, completionStage`来进行decorate。
|
||||
|
||||
可以通过`Try.of`或`Try.run`来调用decorated function,这样允许链式调用,使用示例如下:
|
||||
```java
|
||||
// Given
|
||||
CircuitBreaker circuitBreaker = CircuitBreaker.ofDefaults("testName");
|
||||
|
||||
// When I decorate my function
|
||||
CheckedFunction0<String> decoratedSupplier = CircuitBreaker
|
||||
.decorateCheckedSupplier(circuitBreaker, () -> "This can be any method which returns: 'Hello");
|
||||
|
||||
// and chain an other function with map
|
||||
Try<String> result = Try.of(decoratedSupplier)
|
||||
.map(value -> value + " world'");
|
||||
|
||||
// Then the Try Monad returns a Success<String>, if all functions ran successfully.
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
assertThat(result.get()).isEqualTo("This can be any method which returns: 'Hello world'");
|
||||
```
|
||||
|
||||
### Consume emitted RegistryEvents
|
||||
可以向CircuitBreakerRegistry注册一个event consumer,并且在`CircuitBreaker`被创建、替换、删除时执行对应的action
|
||||
```java
|
||||
CircuitBreakerRegistry circuitBreakerRegistry = CircuitBreakerRegistry.ofDefaults();
|
||||
circuitBreakerRegistry.getEventPublisher()
|
||||
.onEntryAdded(entryAddedEvent -> {
|
||||
CircuitBreaker addedCircuitBreaker = entryAddedEvent.getAddedEntry();
|
||||
LOG.info("CircuitBreaker {} added", addedCircuitBreaker.getName());
|
||||
})
|
||||
.onEntryRemoved(entryRemovedEvent -> {
|
||||
CircuitBreaker removedCircuitBreaker = entryRemovedEvent.getRemovedEntry();
|
||||
LOG.info("CircuitBreaker {} removed", removedCircuitBreaker.getName());
|
||||
});
|
||||
```
|
||||
|
||||
### consume emitted CircuitBreakerEvents
|
||||
CircuitBreakerEvent在如下场景下会被发出:
|
||||
- state transition
|
||||
- circuit breaker reset
|
||||
- successful call
|
||||
- recorded error
|
||||
- ignored error
|
||||
|
||||
所有的event都包含额外的信息,例如事件创建时间、call的处理时长等。如果想要消费该类事件,需要向CircuitBreaker注册事件:
|
||||
```java
|
||||
circuitBreaker.getEventPublisher()
|
||||
.onSuccess(event -> logger.info(...))
|
||||
.onError(event -> logger.info(...))
|
||||
.onIgnoredError(event -> logger.info(...))
|
||||
.onReset(event -> logger.info(...))
|
||||
.onStateTransition(event -> logger.info(...));
|
||||
// Or if you want to register a consumer listening
|
||||
// to all events, you can do:
|
||||
circuitBreaker.getEventPublisher()
|
||||
.onEvent(event -> logger.info(...));
|
||||
```
|
||||
|
||||
可以使用`CircularEventConsumer`来对事件进行存储,事件会被存储在一个固定容量的circular buffer中
|
||||
```java
|
||||
CircularEventConsumer<CircuitBreakerEvent> ringBuffer =
|
||||
new CircularEventConsumer<>(10);
|
||||
circuitBreaker.getEventPublisher().onEvent(ringBuffer);
|
||||
List<CircuitBreakerEvent> bufferedEvents = ringBuffer.getBufferedEvents()
|
||||
```
|
||||
|
||||
## Bulkhead
|
||||
resilence4j提供了两种bulkhead的实现,bukhead可以用于限制并发执行的数量:
|
||||
- `SemaphoreBulkhead`: 该bulkhead实现基于信号量
|
||||
- `FixedThreadPoolBulkhead`: 该实现使用了`a bounded queue and a fixed thread pool`
|
||||
|
||||
### create a BulkheadRegistry
|
||||
和CircuitBreaker module类似,Bulkhead module也提供了in-memory的`BulkheadRegistry和ThreadPoolBulkheadRegistry`,用于管理Bulkhead实例,示例如下:
|
||||
```java
|
||||
BulkheadRegistry bulkheadRegistry = BulkheadRegistry.ofDefaults();
|
||||
|
||||
ThreadPoolBulkheadRegistry threadPoolBulkheadRegistry =
|
||||
ThreadPoolBulkheadRegistry.ofDefaults();
|
||||
```
|
||||
|
||||
### Create and configure a Bulkhead
|
||||
可以自己提供一个全局的`BulkheadConfig`,可通过`BulkheadConfig` builder来创建config对象,该config对象支持如下属性配置:
|
||||
- `maxConcurentCalls`:
|
||||
- `default value`:25
|
||||
- `description`:代表bulkhead允许的最大并行执行数量
|
||||
- `maxWaitDuration`:
|
||||
- `default value`: 0
|
||||
- `description`: 代表一个线程尝试进入饱和的bulkhead时,该线程的最长等待时间
|
||||
|
||||
示例如下所示:
|
||||
```java
|
||||
// Create a custom configuration for a Bulkhead
|
||||
BulkheadConfig config = BulkheadConfig.custom()
|
||||
.maxConcurrentCalls(150)
|
||||
.maxWaitDuration(Duration.ofMillis(500))
|
||||
.build();
|
||||
|
||||
// Create a BulkheadRegistry with a custom global configuration
|
||||
BulkheadRegistry registry = BulkheadRegistry.of(config);
|
||||
|
||||
// Get or create a Bulkhead from the registry -
|
||||
// bulkhead will be backed by the default config
|
||||
Bulkhead bulkheadWithDefaultConfig = registry.bulkhead("name1");
|
||||
|
||||
// Get or create a Bulkhead from the registry,
|
||||
// use a custom configuration when creating the bulkhead
|
||||
Bulkhead bulkheadWithCustomConfig = registry.bulkhead("name2", custom);
|
||||
```
|
||||
|
||||
### Create and configure a ThreadPoolBulkhead
|
||||
可以提供一个自定义的global `ThreadPoolBulkheadConfig`,支持通过builder创建。
|
||||
|
||||
`ThreadPoolBulkheadConfig`支持如下配置属性:
|
||||
- `maxThreadPoolSize`:
|
||||
- `default value`: `Runtime.getRuntime().availableProcessors()`
|
||||
- `description`: 配置线程池的最大线程数
|
||||
- `coreThreadPoolSize`:
|
||||
- `default value`: `Runtime.getRuntime().availableProcessors()-1`
|
||||
- `description`: 配置线程池的核心线程数
|
||||
- `queueCapacity`:
|
||||
- `default value`: 100
|
||||
- `descritpion`: 配置queue的容量
|
||||
- `keepAliveDuration`:
|
||||
- `default value`: 20 [ms]
|
||||
- `description`: 当线程池中线程数量大于coreSize时,该值代表非核心线程在销毁前空闲的最大时长
|
||||
- `writableStackTraceEnabled`:
|
||||
- `default value`: true
|
||||
- `descritpion`: 当bulkhead抛出异常时,是否打印除Stack Trace,当该值设置为false时,仅打印单行
|
||||
|
||||
```java
|
||||
ThreadPoolBulkheadConfig config = ThreadPoolBulkheadConfig.custom()
|
||||
.maxThreadPoolSize(10)
|
||||
.coreThreadPoolSize(2)
|
||||
.queueCapacity(20)
|
||||
.build();
|
||||
|
||||
// Create a BulkheadRegistry with a custom global configuration
|
||||
ThreadPoolBulkheadRegistry registry = ThreadPoolBulkheadRegistry.of(config);
|
||||
|
||||
// Get or create a ThreadPoolBulkhead from the registry -
|
||||
// bulkhead will be backed by the default config
|
||||
ThreadPoolBulkhead bulkheadWithDefaultConfig = registry.bulkhead("name1");
|
||||
|
||||
// Get or create a Bulkhead from the registry,
|
||||
// use a custom configuration when creating the bulkhead
|
||||
ThreadPoolBulkheadConfig custom = ThreadPoolBulkheadConfig.custom()
|
||||
.maxThreadPoolSize(5)
|
||||
.build();
|
||||
|
||||
ThreadPoolBulkhead bulkheadWithCustomConfig = registry.bulkhead("name2", custom);
|
||||
```
|
||||
|
||||
### decorate and execute a functional interface
|
||||
可以使用Bulkhead对`callable, supplier, runnable, consumer, checkedrunnable, checkedsupplier, checkedconsumer, completionStage`来进行decorate,示例如下:
|
||||
```java
|
||||
// Given
|
||||
Bulkhead bulkhead = Bulkhead.of("name", config);
|
||||
|
||||
// When I decorate my function
|
||||
CheckedFunction0<String> decoratedSupplier = Bulkhead
|
||||
.decorateCheckedSupplier(bulkhead, () -> "This can be any method which returns: 'Hello");
|
||||
|
||||
// and chain an other function with map
|
||||
Try<String> result = Try.of(decoratedSupplier)
|
||||
.map(value -> value + " world'");
|
||||
|
||||
// Then the Try Monad returns a Success<String>, if all functions ran successfully.
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
assertThat(result.get()).isEqualTo("This can be any method which returns: 'Hello world'");
|
||||
assertThat(bulkhead.getMetrics().getAvailableConcurrentCalls()).isEqualTo(1);
|
||||
```
|
||||
```java
|
||||
ThreadPoolBulkheadConfig config = ThreadPoolBulkheadConfig.custom()
|
||||
.maxThreadPoolSize(10)
|
||||
.coreThreadPoolSize(2)
|
||||
.queueCapacity(20)
|
||||
.build();
|
||||
|
||||
ThreadPoolBulkhead bulkhead = ThreadPoolBulkhead.of("name", config);
|
||||
|
||||
CompletionStage<String> supplier = ThreadPoolBulkhead
|
||||
.executeSupplier(bulkhead, backendService::doSomething);
|
||||
```
|
||||
|
||||
### consume emitted RegistryEvents
|
||||
可以向`BulkheadRegistry`注册event consumer,用于监听Bulkhead的创建、替换和删除事件
|
||||
```java
|
||||
BulkheadRegistry registry = BulkheadRegistry.ofDefaults();
|
||||
registry.getEventPublisher()
|
||||
.onEntryAdded(entryAddedEvent -> {
|
||||
Bulkhead addedBulkhead = entryAddedEvent.getAddedEntry();
|
||||
LOG.info("Bulkhead {} added", addedBulkhead.getName());
|
||||
})
|
||||
.onEntryRemoved(entryRemovedEvent -> {
|
||||
Bulkhead removedBulkhead = entryRemovedEvent.getRemovedEntry();
|
||||
LOG.info("Bulkhead {} removed", removedBulkhead.getName());
|
||||
});
|
||||
```
|
||||
|
||||
### consume emitted BulkheadEvents
|
||||
BulkHead会发送BulkHeadEvent事件,发送的事件包含如下类型:
|
||||
- permitted execution
|
||||
- rejected execution
|
||||
- finished execution
|
||||
|
||||
如果想要消费上述事件,可以按照如下示例注册event consumer
|
||||
```java
|
||||
bulkhead.getEventPublisher()
|
||||
.onCallPermitted(event -> logger.info(...))
|
||||
.onCallRejected(event -> logger.info(...))
|
||||
.onCallFinished(event -> logger.info(...));
|
||||
```
|
||||
|
||||
### ThreadPoolBulkhead弊端
|
||||
在ThreadPoolBulkhead的实现中,会为每一个bulkhead都创建独立的线程池,故而应当避免在项目中创建大量的bulkhead,避免项目线程数量的膨胀以及线程切换带来的巨大开销。
|
||||
|
||||
## RateLimiter
|
||||
Resilience4j提供了RateLimiter,其将从`epoch`(`某一特定时间点,详细见System.nanoTime方法的注释`)开始的所有nanos划分为了一系列周期,每个周期的时长可以通过`RateLimiterConfig.limitRefreshPeriod`来进行配置。在每个周期的开始,RateLimiter都会将`active permissions number`设置为`RateLimiterConfig.limitForPeriod`。
|
||||
|
||||
RateLimiter的默认实现为`AtomicRateLimiter`,其通过`AtomicReference`来管理自身的状态。`AtomicRateLimiter.State`其本身是不可变的,并且包含如下fields:
|
||||
- `activeCycle`: 上次调用所使用的cycle number
|
||||
- `activePermissions`: 在上次调用之后的available permissions,该field可以为负数
|
||||
- `nanosToWait`:为了最后一次调用所需要等待的nanos数
|
||||
|
||||
除了`AtomicRateLimiter`之外,还存在`SemaphoreBasedRateLimiter`实现,其使用了semaphore和scheduler在每次`RatelImiterConfig#limitRefreshPeriod`之后对permissions进行刷新
|
||||
|
||||
### Create RateLimiterRegistry
|
||||
和CircuitBreaker module类似,RateLimiter module也提供了in-memory RateLimiterRegistry用于管理RateLimiter实例。
|
||||
|
||||
```java
|
||||
RateLimiterRegistry rateLimiterRegistry = RateLimiterRegistry.ofDefaults();
|
||||
```
|
||||
|
||||
### Create and configure a RateLimiter
|
||||
可以提供自定义的`RateLimiterConfig`,可通过builder进行创建。`RateLimiterConfig`支持如下配置属性:
|
||||
- `timeoutDuration`:
|
||||
- `default value`: 5 [s]
|
||||
- `description`: 线程等待permission的默认等待时间
|
||||
- `limitRefreshPeriod`:
|
||||
- `default value`: 500 [ns]
|
||||
- `description`: limit refresh period。在每个period刷新后,rate limiter都会将其permission count重新设置为`limitForPeriod`
|
||||
- `limitForPeriod`:
|
||||
- `default value`: 50
|
||||
- `description`: 一个refresh period内的可获取permissions数量
|
||||
|
||||
通过rateLimiter限制某些接口调用速率不能超过`10 req/ms`的示例如下:
|
||||
```java
|
||||
RateLimiterConfig config = RateLimiterConfig.custom()
|
||||
.limitRefreshPeriod(Duration.ofMillis(1))
|
||||
.limitForPeriod(10)
|
||||
.timeoutDuration(Duration.ofMillis(25))
|
||||
.build();
|
||||
|
||||
// Create registry
|
||||
RateLimiterRegistry rateLimiterRegistry = RateLimiterRegistry.of(config);
|
||||
|
||||
// Use registry
|
||||
RateLimiter rateLimiterWithDefaultConfig = rateLimiterRegistry
|
||||
.rateLimiter("name1");
|
||||
|
||||
RateLimiter rateLimiterWithCustomConfig = rateLimiterRegistry
|
||||
.rateLimiter("name2", config);
|
||||
```
|
||||
|
||||
### decorate and execute a functional interface
|
||||
RateLimiter支持对`callable, supplier, runnable, consumer, checkedRunnalbe, checkedSupplier, checkedConsumer, CompletionStage`进行decorate:
|
||||
```java
|
||||
// Decorate your call to BackendService.doSomething()
|
||||
CheckedRunnable restrictedCall = RateLimiter
|
||||
.decorateCheckedRunnable(rateLimiter, backendService::doSomething);
|
||||
|
||||
Try.run(restrictedCall)
|
||||
.andThenTry(restrictedCall)
|
||||
.onFailure((RequestNotPermitted throwable) -> LOG.info("Wait before call it again :)"));
|
||||
```
|
||||
可以使用`changeTimeoutDuration`和`changeLimitForPeriod`在运行时改变rateLimiter的参数。new timeout并不会影响`正在等待permissions`的线程。并且,new limit不会影响当前period的permissions,new limiter会从下个period开始应用:
|
||||
```java
|
||||
// Decorate your call to BackendService.doSomething()
|
||||
CheckedRunnable restrictedCall = RateLimiter
|
||||
.decorateCheckedRunnable(rateLimiter, backendService::doSomething);
|
||||
|
||||
// during second refresh cycle limiter will get 100 permissions
|
||||
rateLimiter.changeLimitForPeriod(100);
|
||||
```
|
||||
|
||||
### consume emitted RegistryEvents
|
||||
可以针对`RateLimiterRegistry`注册event consumer,对rateLimiter的创建、替换和删除事件进行监听:
|
||||
```java
|
||||
RateLimiterRegistry registry = RateLimiterRegistry.ofDefaults();
|
||||
registry.getEventPublisher()
|
||||
.onEntryAdded(entryAddedEvent -> {
|
||||
RateLimiter addedRateLimiter = entryAddedEvent.getAddedEntry();
|
||||
LOG.info("RateLimiter {} added", addedRateLimiter.getName());
|
||||
})
|
||||
.onEntryRemoved(entryRemovedEvent -> {
|
||||
RateLimiter removedRateLimiter = entryRemovedEvent.getRemovedEntry();
|
||||
LOG.info("RateLimiter {} removed", removedRateLimiter.getName());
|
||||
});
|
||||
```
|
||||
### consume emitted RateLimiterEvents
|
||||
RateLimiter会在如下场景下发送事件:
|
||||
- a successful permission acquire
|
||||
- acquire failure
|
||||
|
||||
所有的事件都包含额外信息,例如事件创建事件和rate limiter name等。如果想要消费事件,可以针对rateLimiter注册event consumer:
|
||||
```java
|
||||
rateLimiter.getEventPublisher()
|
||||
.onSuccess(event -> logger.info(...))
|
||||
.onFailure(event -> logger.info(...));
|
||||
```
|
||||
如果使用project reactor,可以使用如下方式进行注册:
|
||||
```java
|
||||
ReactorAdapter.toFlux(rateLimiter.getEventPublisher())
|
||||
.filter(event -> event.getEventType() == FAILED_ACQUIRE)
|
||||
.subscribe(event -> logger.info(...))
|
||||
```
|
||||
|
||||
## Retry
|
||||
### Create a RetryRegistry
|
||||
和CircuitBreaker module类似,该module也提供了in-memory RetryRegistry,用于对Retry对象进行管理。
|
||||
|
||||
```java
|
||||
RetryRegistry retryRegistry = RetryRegistry.ofDefaults();
|
||||
```
|
||||
|
||||
### Create and configure Retry
|
||||
可以提供一个自定义的global RetryConfig,可以使用builder来创建RetryConfig。
|
||||
|
||||
RetryConfig支持如下配置:
|
||||
- `maxAttempts`:
|
||||
- `default value`: 3
|
||||
- `description`:该参数代表最大尝试次数(包含最初始的调用,最初始的调用为first attemp)
|
||||
- `waitDuration`:
|
||||
- `default value`: 500 [ms]
|
||||
- `description`: 在多次retry attempts之间的固定等待间隔
|
||||
- `intervalFunction`:
|
||||
- `defaultValue`: `numOfAttempts -> waitDuration`
|
||||
- `description`: 该参数对应的function用于在失败后修改等待时间,默认情况下每次失败后等待时间是固定的,都是waitDuration
|
||||
- `intervalBiFunction`:
|
||||
- `default value`: `(int numOfAttempts, Either<Throwable, T> result)->waitDuration`
|
||||
- `description`: 该function用于在failure后修改等待时间间隔,即基于attempt number和result/exception来计算等待时间间隔。当同时使用`intervalFunction`和`intervalBiFunction`时,会抛出异常
|
||||
- `retryOnResultPredicate`:
|
||||
- `default value`: `result -> false`
|
||||
- `description`: 该参数用于配置predicate,用于判断result是否应该被重试。如果result应当被重试,那么返回true,否则返回false
|
||||
- `retryOnExceptionPredicate`:
|
||||
- `default`: `throwable -> true`
|
||||
- `description`: 该参数用于判断是否exception应当被重试。如果exception应当被重试,predicate返回true,否则返回false
|
||||
- `retryExceptions`:
|
||||
- `default value`: empty
|
||||
- `description`: 配置exception list,其将被记录为failure并且应当被重试。
|
||||
- `ignoreExceptions`:
|
||||
- `default value`: empty
|
||||
- `descritpion`: 配置exception list,该列表中的异常会被ignore并且不会重试
|
||||
- `failAfterMaxAttempts`:
|
||||
- `default value`: false
|
||||
- `description`: 该参数用于启用和关闭`MaxRetriesExceededException`的抛出,当Retry达到配置的maxAttempts后,若result没有通过`retryOnResultPredicate`,则会根据`failAfterMaxAttempts`来重试
|
||||
|
||||
> 在抛出异常后,是否重试的逻辑如下:
|
||||
> - 根据predicate判断该异常是否应该重试,predicate逻辑判断流程如下
|
||||
> - 如果异常位于ignoreExceptions中,则不应重试
|
||||
> - 如果异常位于retryExceptions中,则predicate返回为true
|
||||
> - 如果异常不位于retryExceptions中,则根据retryOnExceptionPredicate来判断是否异常应当触发重试
|
||||
> - 如果上述的predicate判断异常应该被重试,那么再递增重试次数,判断当前重试是否超过maxAttempts
|
||||
> - 如果没有超过,则在等待interval后触发重试
|
||||
> - 如果超过maxAttempts规定的上限,则不再重试直接抛出异常
|
||||
|
||||
> 在未抛出异常时,判断是否重试的逻辑如下:
|
||||
> - 首先,根据`retryOnResultPredicate`判断当前返回结果是否应当触发重试,如果不应触发重试,则流程结束
|
||||
> - 如果应当触发重试,则增加当前的重试次数,并和maxAttempts进行比较
|
||||
> - 如果当前重试次数未超过maxAttempts,则在等待interval后触发重试
|
||||
> - 如果重试次数超过maxAttempts规定的值,那么将根据failAfterMaxAttempts来决定是否抛出异常。当failAfterMaxAttempts为true时,抛出异常;当为false时,不跑出异常。默认不会抛出异常。
|
||||
|
||||
创建RetryConfig的默认示例如下:
|
||||
```java
|
||||
RetryConfig config = RetryConfig.custom()
|
||||
.maxAttempts(2)
|
||||
.waitDuration(Duration.ofMillis(1000))
|
||||
.retryOnResult(response -> response.getStatus() == 500)
|
||||
.retryOnException(e -> e instanceof WebServiceException)
|
||||
.retryExceptions(IOException.class, TimeoutException.class)
|
||||
.ignoreExceptions(BusinessException.class, OtherBusinessException.class)
|
||||
.failAfterMaxAttempts(true)
|
||||
.build();
|
||||
|
||||
// Create a RetryRegistry with a custom global configuration
|
||||
RetryRegistry registry = RetryRegistry.of(config);
|
||||
|
||||
// Get or create a Retry from the registry -
|
||||
// Retry will be backed by the default config
|
||||
Retry retryWithDefaultConfig = registry.retry("name1");
|
||||
|
||||
// Get or create a Retry from the registry,
|
||||
// use a custom configuration when creating the retry
|
||||
RetryConfig custom = RetryConfig.custom()
|
||||
.waitDuration(Duration.ofMillis(100))
|
||||
.build();
|
||||
|
||||
Retry retryWithCustomConfig = registry.retry("name2", custom);
|
||||
```
|
||||
### Decorate and execute a functional interface
|
||||
Retry可以针对`callable, supplier, runnable, consumer, checkedrunnable, checkedsupplier, checkedconsumer, completionstage`进行decorate,使用示例如下:
|
||||
```java
|
||||
// Given I have a HelloWorldService which throws an exception
|
||||
HelloWorldService helloWorldService = mock(HelloWorldService.class);
|
||||
given(helloWorldService.sayHelloWorld())
|
||||
.willThrow(new WebServiceException("BAM!"));
|
||||
|
||||
// Create a Retry with default configuration
|
||||
Retry retry = Retry.ofDefaults("id");
|
||||
// Decorate the invocation of the HelloWorldService
|
||||
CheckedFunction0<String> retryableSupplier = Retry
|
||||
.decorateCheckedSupplier(retry, helloWorldService::sayHelloWorld);
|
||||
|
||||
// When I invoke the function
|
||||
Try<String> result = Try.of(retryableSupplier)
|
||||
.recover((throwable) -> "Hello world from recovery function");
|
||||
|
||||
// Then the helloWorldService should be invoked 3 times
|
||||
BDDMockito.then(helloWorldService).should(times(3)).sayHelloWorld();
|
||||
// and the exception should be handled by the recovery function
|
||||
assertThat(result.get()).isEqualTo("Hello world from recovery function");
|
||||
```
|
||||
|
||||
### consume emitted RegistryEvents
|
||||
可以向RetryRegistry注册监听,消费Retry的create, replace, delete事件
|
||||
```java
|
||||
RetryRegistry registry = RetryRegistry.ofDefaults();
|
||||
registry.getEventPublisher()
|
||||
.onEntryAdded(entryAddedEvent -> {
|
||||
Retry addedRetry = entryAddedEvent.getAddedEntry();
|
||||
LOG.info("Retry {} added", addedRetry.getName());
|
||||
})
|
||||
.onEntryRemoved(entryRemovedEvent -> {
|
||||
Retry removedRetry = entryRemovedEvent.getRemovedEntry();
|
||||
LOG.info("Retry {} removed", removedRetry.getName());
|
||||
});
|
||||
```
|
||||
|
||||
### use custom IntervalFunction
|
||||
如果不想使用fixed wait duration,可以自定义`IntervalFunction`,该函数可以在每次attempt时独立计算wait duration。resilience4j支持一些工厂方法用于创建IntervalFunction,示例如下
|
||||
```java
|
||||
IntervalFunction defaultWaitInterval = IntervalFunction
|
||||
.ofDefaults();
|
||||
|
||||
// This interval function is used internally
|
||||
// when you only configure waitDuration
|
||||
IntervalFunction fixedWaitInterval = IntervalFunction
|
||||
.of(Duration.ofSeconds(5));
|
||||
|
||||
IntervalFunction intervalWithExponentialBackoff = IntervalFunction
|
||||
.ofExponentialBackoff();
|
||||
|
||||
IntervalFunction intervalWithCustomExponentialBackoff = IntervalFunction
|
||||
.ofExponentialBackoff(IntervalFunction.DEFAULT_INITIAL_INTERVAL, 2d);
|
||||
|
||||
IntervalFunction randomWaitInterval = IntervalFunction
|
||||
.ofRandomized();
|
||||
|
||||
// Overwrite the default intervalFunction with your custom one
|
||||
RetryConfig retryConfig = RetryConfig.custom()
|
||||
.intervalFunction(intervalWithExponentialBackoff)
|
||||
.build();
|
||||
```
|
||||
> intervalFunction和intervalBiFunction不能同时指定,同时指定时会抛出异常。
|
||||
>
|
||||
> 如果指定了intervalFunction,那么在通过builder创建RetryConfig时,会自动通过intervalFunction给intervalBiFunction也赋值。
|
||||
>
|
||||
> 如果指定了intervalFunction或intervalBiFunction中任一,则使用指定的函数来计算waitDuration,当二者都没有指定时,则waitDuration固定为waitDuration
|
||||
|
||||
## TimeLimiter
|
||||
### Create a TimeLimiterRegistry
|
||||
和`CircuitBreaker` module类似,TimeLimiter module支持提供in-memory TimeLimiterRegistry,可用于管理TimeLimiter实例。
|
||||
```java
|
||||
TimeLimiterRegistry timeLimiterRegistry = TimeLimiterRegistry.ofDefaults();
|
||||
```
|
||||
|
||||
### Create and configure TimeLimiter
|
||||
可以提供一个全局的自定义TimeLimiterConfig,支持通过builder创建。
|
||||
|
||||
TimeLimiterConfig支持配置如下两个参数:
|
||||
- the timeout duration
|
||||
- whether cancel should be called on the running future
|
||||
|
||||
使用示例如下所示:
|
||||
```java
|
||||
TimeLimiterConfig config = TimeLimiterConfig.custom()
|
||||
.cancelRunningFuture(true)
|
||||
.timeoutDuration(Duration.ofMillis(500))
|
||||
.build();
|
||||
|
||||
// Create a TimeLimiterRegistry with a custom global configuration
|
||||
TimeLimiterRegistry timeLimiterRegistry = TimeLimiterRegistry.of(config);
|
||||
|
||||
// Get or create a TimeLimiter from the registry -
|
||||
// TimeLimiter will be backed by the default config
|
||||
TimeLimiter timeLimiterWithDefaultConfig = registry.timeLimiter("name1");
|
||||
|
||||
// Get or create a TimeLimiter from the registry,
|
||||
// use a custom configuration when creating the TimeLimiter
|
||||
TimeLimiterConfig config = TimeLimiterConfig.custom()
|
||||
.cancelRunningFuture(false)
|
||||
.timeoutDuration(Duration.ofMillis(1000))
|
||||
.build();
|
||||
|
||||
TimeLimiter timeLimiterWithCustomConfig = registry.timeLimiter("name2", config);
|
||||
```
|
||||
### decorate and execute a functional interface
|
||||
TimeLimiter可以对`CompletionStage`和`Future`进行decorate,示例如下:
|
||||
```java
|
||||
// Given I have a helloWorldService.sayHelloWorld() method which takes too long
|
||||
HelloWorldService helloWorldService = mock(HelloWorldService.class);
|
||||
|
||||
// Create a TimeLimiter
|
||||
TimeLimiter timeLimiter = TimeLimiter.of(Duration.ofSeconds(1));
|
||||
// The Scheduler is needed to schedule a timeout on a non-blocking CompletableFuture
|
||||
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(3);
|
||||
|
||||
// The non-blocking variant with a CompletableFuture
|
||||
CompletableFuture<String> result = timeLimiter.executeCompletionStage(
|
||||
scheduler, () -> CompletableFuture.supplyAsync(helloWorldService::sayHelloWorld)).toCompletableFuture();
|
||||
|
||||
// The blocking variant which is basically future.get(timeoutDuration, MILLISECONDS)
|
||||
String result = timeLimiter.executeFutureSupplier(
|
||||
() -> CompletableFuture.supplyAsync(() -> helloWorldService::sayHelloWorld));
|
||||
```
|
||||
|
||||
在TimeLimiter的实现中,`timeoutDuration`设置了`等待future执行结束的最长等待时间`,如果该等待时间超时,将会抛出`TimeoutException`。
|
||||
|
||||
当发生等待超时时,将会根据`shouldCancelRunningFuture`参数的配置来决定`是否针对尚未执行完成的future调用cancel`。
|
||||
|
||||
## Spring Cloud CircuitBreaker
|
||||
Spring Cloud Circuit Breaker提供了circuit breaker的抽象,可以让开发者自由选择circuit breaker的实现。目前,spring cloud支持如下circuit breaker的实现:
|
||||
- Resilience4j
|
||||
- Sentinel
|
||||
- spring retry
|
||||
|
||||
### core concepts
|
||||
可以通过`CircuitBreakerFactory` api来创建circuit breaker。在classpath中包含spring cloud circuit breaker starter时,将会自动创建实现了`CircuitBreakerFactory`的bean。如下示例展示了如何使用该api:
|
||||
```java
|
||||
@Service
|
||||
public static class DemoControllerService {
|
||||
private RestTemplate rest;
|
||||
private CircuitBreakerFactory cbFactory;
|
||||
|
||||
public DemoControllerService(RestTemplate rest, CircuitBreakerFactory cbFactory) {
|
||||
this.rest = rest;
|
||||
this.cbFactory = cbFactory;
|
||||
}
|
||||
|
||||
public String slow() {
|
||||
return cbFactory.create("slow").run(() -> rest.getForObject("/slow", String.class), throwable -> "fallback");
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
`CircuitBreakerFactory.create`方法将会创建一个`CircuitBreaker`的实例,run方法将会接收一个Supplier和一个Function:
|
||||
- supplier是实际被封装在circuitbreaker中的方法
|
||||
- function是circuit breaker被触发之后的fallback
|
||||
- function接收一个Throwable,throwable为导致该fallback被触发的异常
|
||||
|
||||
### Configuration
|
||||
可以通过创建`Customizer`类型的bean来配置circuit breaker。
|
||||
|
||||
在`Resilience4JCircuitBreaker`的实现中,每次调用`circuitbreaker#run`时,都会调用`Customizer#customize`方法,该行为可能会存在效率问题。故而,可以使用`Customizer#once`方法来创建Customizer,其会保证customizer中的逻辑最多只会被调用一次。
|
||||
|
||||
示例如下所示:
|
||||
```java
|
||||
Customizer.once(circuitBreaker -> {
|
||||
circuitBreaker.getEventPublisher()
|
||||
.onStateTransition(event -> log.info("{}: {}", event.getCircuitBreakerName(), event.getStateTransition()));
|
||||
}, CircuitBreaker::getName)
|
||||
```
|
||||
112
spring/Spring Cloud/Spring Cloud Circuit Breaker.md
Normal file
112
spring/Spring Cloud/Spring Cloud Circuit Breaker.md
Normal file
@@ -0,0 +1,112 @@
|
||||
- [Spring Cloud Circuit Breaker](#spring-cloud-circuit-breaker)
|
||||
- [配置Resilience4J Circuit Breakers](#配置resilience4j-circuit-breakers)
|
||||
- [关闭自动装配](#关闭自动装配)
|
||||
- [默认配置](#默认配置)
|
||||
- [自定义ExecutorService](#自定义executorservice)
|
||||
- [指定Circuit Breaker 配置](#指定circuit-breaker-配置)
|
||||
- [Circuit Breaker属性配置](#circuit-breaker属性配置)
|
||||
- [全局默认](#全局默认)
|
||||
- [Configs Properties Configuration](#configs-properties-configuration)
|
||||
- [实例属性设置](#实例属性设置)
|
||||
|
||||
|
||||
# Spring Cloud Circuit Breaker
|
||||
Spring Cloud Circuit Breaker项目包含Resilience4J实现和Spring Retry实现。
|
||||
## 配置Resilience4J Circuit Breakers
|
||||
关于resilience4J Circuit Breakers实现,拥有如下两个启动器,一个用于响应式项目,另一个用于非响应式项目:
|
||||
- `org.springframework.cloud:spring-cloud-starter-circuitbreaker-resilience4j`
|
||||
- `org.springframework.cloud:spring-cloud-starter-circuitbreaker-reactor-resilience4j`
|
||||
|
||||
### 关闭自动装配
|
||||
可以通过设置`spring.cloud.circuitbreaker.resilience4j.enabled`为false来关闭Resilience4J的自动装配
|
||||
### 默认配置
|
||||
如果想要为所有的circuit breaker提供默认配置,可以创建一个`Customizer` bean对象,该bean对象接收一个`Resilience4JCircuitBreakerFactory`类型参数。configureDefault方法可以用于提供默认的配置:
|
||||
```java
|
||||
@Bean
|
||||
public Customizer<Resilience4JCircuitBreakerFactory> defaultCustomizer() {
|
||||
return factory -> factory.configureDefault(id -> new Resilience4JConfigBuilder(id)
|
||||
.timeLimiterConfig(TimeLimiterConfig.custom().timeoutDuration(Duration.ofSeconds(4)).build())
|
||||
.circuitBreakerConfig(CircuitBreakerConfig.ofDefaults())
|
||||
.build());
|
||||
}
|
||||
```
|
||||
### 自定义ExecutorService
|
||||
如果想要自定义执行circuit breaker的`ExecutorService`,可以通过Resilience4JCircuitBreakerFactor来指定。
|
||||
例如,想要指定一个context aware ExecutorService,可以按如下所示:
|
||||
```java
|
||||
@Bean
|
||||
public Customizer<ReactiveResilience4JCircuitBreakerFactory> defaultCustomizer() {
|
||||
return factory -> {
|
||||
ContextAwareScheduledThreadPoolExecutor executor = ContextAwareScheduledThreadPoolExecutor.newScheduledThreadPool().corePoolSize(5)
|
||||
.build();
|
||||
factory.configureExecutorService(executor);
|
||||
};
|
||||
}
|
||||
```
|
||||
### 指定Circuit Breaker 配置
|
||||
类似为所有Circuit Breakers指定默认配置,也能以如下方式为特定Circuit Breaker指定配置:
|
||||
```java
|
||||
@Bean
|
||||
public Customizer<Resilience4JCircuitBreakerFactory> slowCustomizer() {
|
||||
return factory -> factory.configure(builder -> builder.circuitBreakerConfig(CircuitBreakerConfig.ofDefaults())
|
||||
.timeLimiterConfig(TimeLimiterConfig.custom().timeoutDuration(Duration.ofSeconds(2)).build()), "slow");
|
||||
}
|
||||
```
|
||||
### Circuit Breaker属性配置
|
||||
可以在配置文件中指定`CircuitBreaker`和`TimeLimiter`配置,配置文件中指定的配置比Customizer中指定的配置优先级更高。
|
||||
优先级从上到下递减:
|
||||
- 方法级别(id)配置 - 针对指定的方法或操作
|
||||
- service级别(group)配置 - 针对指定应用的service或操作
|
||||
- 全局默认配置
|
||||
|
||||
#### 全局默认
|
||||
```yml
|
||||
resilience4j.circuitbreaker:
|
||||
configs:
|
||||
default:
|
||||
registerHealthIndicator: true
|
||||
slidingWindowSize: 50
|
||||
|
||||
resilience4j.timelimiter:
|
||||
configs:
|
||||
default:
|
||||
timeoutDuration: 5s
|
||||
cancelRunningFuture: true
|
||||
```
|
||||
#### Configs Properties Configuration
|
||||
```yml
|
||||
resilience4j.circuitbreaker:
|
||||
configs:
|
||||
groupA:
|
||||
registerHealthIndicator: true
|
||||
slidingWindowSize: 200
|
||||
|
||||
resilience4j.timelimiter:
|
||||
configs:
|
||||
groupC:
|
||||
timeoutDuration: 3s
|
||||
cancelRunningFuture: true
|
||||
```
|
||||
#### 实例属性设置
|
||||
```yml
|
||||
resilience4j.circuitbreaker:
|
||||
instances:
|
||||
backendA:
|
||||
registerHealthIndicator: true
|
||||
slidingWindowSize: 100
|
||||
backendB:
|
||||
registerHealthIndicator: true
|
||||
slidingWindowSize: 10
|
||||
permittedNumberOfCallsInHalfOpenState: 3
|
||||
slidingWindowType: TIME_BASED
|
||||
recordFailurePredicate: io.github.robwin.exception.RecordFailurePredicate
|
||||
|
||||
resilience4j.timelimiter:
|
||||
instances:
|
||||
backendA:
|
||||
timeoutDuration: 2s
|
||||
cancelRunningFuture: true
|
||||
backendB:
|
||||
timeoutDuration: 1s
|
||||
cancelRunningFuture: false
|
||||
```
|
||||
190
spring/Spring Cloud/Spring Cloud Netflix.md
Normal file
190
spring/Spring Cloud/Spring Cloud Netflix.md
Normal file
@@ -0,0 +1,190 @@
|
||||
- [Eureka](#eureka)
|
||||
- [Service Discovery: Eureka Client](#service-discovery-eureka-client)
|
||||
- [将Eureka Client包含到项目中](#将eureka-client包含到项目中)
|
||||
- [向Eureka中注册](#向eureka中注册)
|
||||
- [status page和health indicator](#status-page和health-indicator)
|
||||
- [Eureka的健康检查](#eureka的健康检查)
|
||||
- [Eureka实例和客户端的元数据](#eureka实例和客户端的元数据)
|
||||
- [修改Eureka instance id](#修改eureka-instance-id)
|
||||
- [使用EurekaClient](#使用eurekaclient)
|
||||
- [注册服务速度](#注册服务速度)
|
||||
- [Eureka Server](#eureka-server)
|
||||
- [Eureka Server的启动](#eureka-server的启动)
|
||||
- [高可用](#高可用)
|
||||
- [单机模式](#单机模式)
|
||||
- [Peer Awareness](#peer-awareness)
|
||||
- [IP地址注册](#ip地址注册)
|
||||
|
||||
|
||||
# Eureka
|
||||
## Service Discovery: Eureka Client
|
||||
服务发现是基于微服务体系结构的关键之一。在微服务体系结构中,如果手动配置每个客户端,这将会很困难,并且很脆弱。
|
||||
Eureka是Netflix的服务发现的client和server。通过配置和部署,Eureka server可以实现高可用,每个server节点会向其他server节点复制已注册service的状态。
|
||||
## 将Eureka Client包含到项目中
|
||||
如果想要将Eureka Client包含到项目中,可以在依赖中添加如下starter:
|
||||
```xml
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
|
||||
```
|
||||
## 向Eureka中注册
|
||||
当一个client向eureka server中注册时,其提供了关于其自身的元数据(例如host,port,判断health状态的url等)。eureka从一个service的所有实例接收心跳信息,如果一个实例心跳报接收超过特定的时间,那么该实例将会从注册中心移除。
|
||||
如下显示了一个简单的Eureka Client:
|
||||
```java
|
||||
@SpringBootApplication
|
||||
@RestController
|
||||
public class Application {
|
||||
|
||||
@RequestMapping("/")
|
||||
public String home() {
|
||||
return "Hello world";
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
new SpringApplicationBuilder(Application.class).web(true).run(args);
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
在上述client项目中,如果classpath中存在`spring-cloud-starter-netflix-eureka-client`依赖,那么client应用将会自动在注册到eureka server中。
|
||||
eureka server的信息需要手动配置:
|
||||
```yml
|
||||
eureka:
|
||||
client:
|
||||
serviceUrl:
|
||||
defaultZone: http://localhost:8761/eureka/
|
||||
```
|
||||
在上述的配置中,defaultZone是一个默认的回退值,用于向client提供service url默认值。
|
||||
将`spring-cloud-starter-netflix-eureka-client`依赖包含在classpath中,既会将应用注册为eureka的一个实例,也会将应用变为eureka的一个客户端(可以查询注册中心并且获取其他service的信息)。
|
||||
eureka实例的行为将会由`eureka.instance.*`来进行配置,但是当项目制定了`spring.application.name`时,使用默认值来控制eureka实例行为即可。
|
||||
如果想要禁用Eureka Discovery Client,可以将`eureka.client.enabled`属性配置为false,当`spring.cloud.discovery.enabled`属性被设置为false时,Eureka Discovery Client同样会被禁用。
|
||||
## status page和health indicator
|
||||
Eureka实例的status page和health indicator分别指向/info和/health,是spring boot actuator应用的默认端点。
|
||||
## Eureka的健康检查
|
||||
默认情况下,Eureka使用客户端的心跳来判断该客户端是否启用。除非显式指定,否则客户端并不会传播当前应用的健康检查状态。因此,在成功注册之后,Eureka总是宣布该应用处于`UP`状态。
|
||||
通过显式启用健康检查,上述行为可以被改变,客户端将会将应用的状态传播给eureka server。所有的应用都不会向处于非`UP`状态的节点发送负载。
|
||||
可以按如下方式启用健康检查:
|
||||
```yml
|
||||
eureka:
|
||||
client:
|
||||
healthcheck:
|
||||
enabled: true
|
||||
```
|
||||
## Eureka实例和客户端的元数据
|
||||
标准元数据包含hostname、ip address、port、status page、health indicator等信息。这些元数据信息将会被发布到注册中心,并且被eureka client使用,用于调用远程服务。
|
||||
### 修改Eureka instance id
|
||||
Spring Cloud为Eureka instance提供了一个合理的id默认值,其默认值如下:
|
||||
`${spring.cloud.client.hostname}:${spring.application.name}:${spring.application.instance_id:${server.port}}`
|
||||
例如`myhost:myappname:8080`
|
||||
在Spring Cloud中,可以通过如下方式自定义Eureka实例的id:
|
||||
```yml
|
||||
eureka:
|
||||
instance:
|
||||
instanceId: ${spring.application.name}:${vcap.application.instance_id:${spring.application.instance_id:${random.value}}}
|
||||
```
|
||||
## 使用EurekaClient
|
||||
当应用中包含discovery client时,可以使用其从Eureka Server中发现服务实例:
|
||||
```java
|
||||
@Autowired
|
||||
private EurekaClient discoveryClient;
|
||||
|
||||
public String serviceUrl() {
|
||||
InstanceInfo instance = discoveryClient.getNextServerFromEureka("STORES", false);
|
||||
return instance.getHomePageUrl();
|
||||
}
|
||||
```
|
||||
## 注册服务速度
|
||||
在注册为服务时,需要在一定期间内持续向eureka server发送心跳包,默认情况下该期间持续时间为30s。可以通过定义`eureka.instance.leaseRenewalIntervalInSeconds`值来缩短该期间的时间,将其值设置为小于30.但是,默认情况下,最好将其保留为默认值。
|
||||
## Eureka Server
|
||||
如果要在项目中包含eureka-server,可以在项目中包含如下启动器:
|
||||
```xml
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
|
||||
```
|
||||
### Eureka Server的启动
|
||||
如下显示了要启动一个Eureka Server的最简代码:
|
||||
```java
|
||||
@SpringBootApplication
|
||||
@EnableEurekaServer
|
||||
public class Application {
|
||||
|
||||
public static void main(String[] args) {
|
||||
new SpringApplicationBuilder(Application.class).web(true).run(args);
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
在`/eureka/*`路径下,该eureka server拥有UI和http api endpoint。
|
||||
### 高可用
|
||||
Eureka Server并没有后端存储,但是注册到Eureka Server中的所有service实例都必须向eureka server发送心跳包来保证其注册状态是最新的,上述所有操作都是在eureka server的内存中完成的。
|
||||
client端也会在内存中对eureka server中的注册状态进行缓存,故而无需每次请求其他service时都要从eureka server中获取注册状态。
|
||||
> 默认情况下,每个eureka server都同时是一个client,并且需要至少一个URL来定位其他的peer instance。如果没有提供该URL,服务仍然能启动,但是会一直输出日志表示无法向其他peer instance注册。
|
||||
|
||||
### 单机模式
|
||||
单机模式下,可能需要关闭eureka的客户端行为,例如持续获取其peer instance并失败。可以通过如下配置来关闭客户端的行为:
|
||||
```yml
|
||||
server:
|
||||
port: 8761
|
||||
|
||||
eureka:
|
||||
instance:
|
||||
hostname: localhost
|
||||
client:
|
||||
registerWithEureka: false
|
||||
fetchRegistry: false
|
||||
serviceUrl:
|
||||
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
|
||||
```
|
||||
单机模式下,`serviceUrl`指向与本地实例相同的host地址。
|
||||
### Peer Awareness
|
||||
相比于单机模式,通过运行多个eureka实例并且让实例之间相互注册,Eureka可以配置的更加弹性和高可用。上述是eureka的默认行为,配置时只需要指定peer instance的`serviceUrl`,如下所示:
|
||||
```yml
|
||||
---
|
||||
spring:
|
||||
profiles: peer1
|
||||
eureka:
|
||||
instance:
|
||||
hostname: peer1
|
||||
client:
|
||||
serviceUrl:
|
||||
defaultZone: https://peer2/eureka/
|
||||
|
||||
---
|
||||
spring:
|
||||
profiles: peer2
|
||||
eureka:
|
||||
instance:
|
||||
hostname: peer2
|
||||
client:
|
||||
serviceUrl:
|
||||
defaultZone: https://peer1/eureka/
|
||||
```
|
||||
可以向系统中添加多个peer instance,实例之间两两相互连接,实例之间相互同步注册信息。示例如下:
|
||||
```yml
|
||||
eureka:
|
||||
client:
|
||||
serviceUrl:
|
||||
defaultZone: https://peer1/eureka/,http://peer2/eureka/,http://peer3/eureka/
|
||||
|
||||
---
|
||||
spring:
|
||||
profiles: peer1
|
||||
eureka:
|
||||
instance:
|
||||
hostname: peer1
|
||||
|
||||
---
|
||||
spring:
|
||||
profiles: peer2
|
||||
eureka:
|
||||
instance:
|
||||
hostname: peer2
|
||||
|
||||
---
|
||||
spring:
|
||||
profiles: peer3
|
||||
eureka:
|
||||
instance:
|
||||
hostname: peer3
|
||||
```
|
||||
### IP地址注册
|
||||
在某些场景下,可能更喜欢通过ip地址而不是hostname来进行注册。如果将`eureka.instance.preferIpAddress`设置为true,那么在应用注册到eureka时,会使用ip地址而不是hostname
|
||||
161
spring/Spring Cloud/Spring Cloud OpenFeign.md
Normal file
161
spring/Spring Cloud/Spring Cloud OpenFeign.md
Normal file
@@ -0,0 +1,161 @@
|
||||
- [Spring Cloud OpenFeign](#spring-cloud-openfeign)
|
||||
- [Feign](#feign)
|
||||
- [引入Feign](#引入feign)
|
||||
- [@FeignClient注解的属性解析模式](#feignclient注解的属性解析模式)
|
||||
- [覆盖Feign的默认属性](#覆盖feign的默认属性)
|
||||
- [重试](#重试)
|
||||
- [通过Configuration类或配置文件来修改Feign Client的默认行为](#通过configuration类或配置文件来修改feign-client的默认行为)
|
||||
- [超时处理](#超时处理)
|
||||
- [Feign Caching](#feign-caching)
|
||||
|
||||
|
||||
# Spring Cloud OpenFeign
|
||||
## Feign
|
||||
Feign是一个声明式的web service client,其能够让编写web service client的过程更加简单。在使用Feign时,只需要创建一个接口并且为其添加注解。
|
||||
在使用Feign时,Spring Cloud集成了Eureka、Spring Cloud CircuitBreaker和Spring Cloud LoadBalancer来向使用者提供一个负载均衡的http client。
|
||||
### 引入Feign
|
||||
如果要在项目中引入Feign,可以添加如下启动器依赖:
|
||||
```xml
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-openfeign</artifactId>
|
||||
```
|
||||
在使用Feign时,需要为启动类添加`@EnableFeignClients`注解,示例如下所示:
|
||||
```java
|
||||
@SpringBootApplication
|
||||
@EnableFeignClients
|
||||
public class Application {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(Application.class, args);
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
使用Feign时,定义接口并添加注解的示例如下所示:
|
||||
```java
|
||||
@FeignClient("stores")
|
||||
public interface StoreClient {
|
||||
@RequestMapping(method = RequestMethod.GET, value = "/stores")
|
||||
List<Store> getStores();
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, value = "/stores")
|
||||
Page<Store> getStores(Pageable pageable);
|
||||
|
||||
@RequestMapping(method = RequestMethod.POST, value = "/stores/{storeId}", consumes = "application/json")
|
||||
Store update(@PathVariable("storeId") Long storeId, Store store);
|
||||
|
||||
@RequestMapping(method = RequestMethod.DELETE, value = "/stores/{storeId:\\d+}")
|
||||
void delete(@PathVariable Long storeId);
|
||||
}
|
||||
```
|
||||
上述示例中,@FeignClient中指定的字符串是一个任意的client name,用于创建spring cloud loadbalancer client,对于该注解还可以指定一个`url`属性。在spring context中,该bean对象的name即是接口的全类名,bean name也可以通过@FeignClient注解中的`qualifiers`属性来替换。
|
||||
上述的loadbalancer client会取发现"stores" service对应的物理地址,如果你的应用是一个Eureka client,那么loadbalancer client会从Eureka注册中心中进行解析。
|
||||
#### @FeignClient注解的属性解析模式
|
||||
当创建Feign client bean对象时,会解析传递给@FeignClient注解的属性值。这些值默认是立即解析的。
|
||||
如果如果需要延迟这些属性值的解析时机,可以配置`spring.cloud.openfeign.lazy-attributes-resolution`的值为true。
|
||||
### 覆盖Feign的默认属性
|
||||
Spring Cloud Feign的核心理念是命名客户端(named client)。每个Feign client都是组件集合一部分,组件集合协同工作,并且在需要时连接远程的server。该组件集合有一个名称,该值通过@FeignClient的value属性赋值。对于每个named client,Spring Cloud使用`FeignClientsConfiguration`来创建一个新的集合作为Application Client。
|
||||
组件集合中包含有一个`feign.Decoder`,一个`feign.Encoder`和`feign.Contract`。可以通过@FeignClient注解中的`contextId`属性来覆盖集合name。
|
||||
Spring Cloud允许通过声明额外的configuration来完全掌控feign client,示例如下:
|
||||
```java
|
||||
@FeignClient(name = "stores", configuration = FooConfiguration.class)
|
||||
public interface StoreClient {
|
||||
//..
|
||||
}
|
||||
```
|
||||
在这种情况下,client由`FeignClientsConfiguration`中已经存在的组件和`FooConfiguration`中的组件共同组成,并且后者会覆盖前者。
|
||||
> FooConfiguration类在声明时不要为其添加@Configuration注解,否则其会成为feign.Decoder, feign.Encoder, feign.Contract的默认来源。
|
||||
|
||||
对于`name`和`url`属性,赋值支持占位符:
|
||||
```java
|
||||
@FeignClient(name = "${feign.name}", url = "${feign.url}")
|
||||
public interface StoreClient {
|
||||
//..
|
||||
}
|
||||
```
|
||||
#### 重试
|
||||
一个`Retryer.NEVER_RETRY`的bean对象会被默认创建,其会禁止重试操作。但是,其会自动重试IOException,将IOException看作暂时性地网络异常,并且在ErrorDecoder中抛出RetryableException。
|
||||
在@FeignClient注解`configuration`属性指定的配置类中创建Bean对象可以允许覆盖默认的bean对象,示例如下:
|
||||
```java
|
||||
@Configuration
|
||||
public class FooConfiguration {
|
||||
@Bean
|
||||
public Contract feignContract() {
|
||||
return new feign.Contract.Default();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public BasicAuthRequestInterceptor basicAuthRequestInterceptor() {
|
||||
return new BasicAuthRequestInterceptor("user", "password");
|
||||
}
|
||||
}
|
||||
```
|
||||
#### 通过Configuration类或配置文件来修改Feign Client的默认行为
|
||||
@FeignClient也可以通过配置文件来进行配置:
|
||||
```yml
|
||||
spring:
|
||||
cloud:
|
||||
openfeign:
|
||||
client:
|
||||
config:
|
||||
{feignName}:
|
||||
url: http://remote-service.com
|
||||
connectTimeout: 5000
|
||||
readTimeout: 5000
|
||||
loggerLevel: full
|
||||
errorDecoder: com.example.SimpleErrorDecoder
|
||||
retryer: com.example.SimpleRetryer
|
||||
defaultQueryParameters:
|
||||
query: queryValue
|
||||
defaultRequestHeaders:
|
||||
header: headerValue
|
||||
requestInterceptors:
|
||||
- com.example.FooRequestInterceptor
|
||||
- com.example.BarRequestInterceptor
|
||||
responseInterceptor: com.example.BazResponseInterceptor
|
||||
dismiss404: false
|
||||
encoder: com.example.SimpleEncoder
|
||||
decoder: com.example.SimpleDecoder
|
||||
contract: com.example.SimpleContract
|
||||
capabilities:
|
||||
- com.example.FooCapability
|
||||
- com.example.BarCapability
|
||||
queryMapEncoder: com.example.SimpleQueryMapEncoder
|
||||
micrometer.enabled: false
|
||||
```
|
||||
feignName在上述配置文件中代表@FeignClient注解中的value值。
|
||||
> 若feignName的值为default,可以将配置应用于所有的feign client。
|
||||
|
||||
可以在`@EnableFeignClients`注解中指定defaultConfiguration属性,其可以指定一个自定义的默认配置类,覆盖的行为会作用于所有的Feign Client。
|
||||
可以使用`spring.cloud.openfeign.client.config.feignName.defaultQueryParameters`和`spring.cloud.openfeign.client.config.feignName.defaultRequestHeaders`来指定client每次请求都会发送的请求参数和请求header。
|
||||
|
||||
如果在创建Configuration类的同时也指定了配置文件,那么配置文件的优先级更高,配置文件的值会覆盖Configuration类中的值。
|
||||
如果想要多个feign client拥有相同的name和url,以使它们指向同一个server,但不同feign client拥有不同的配置。可以为它们指定不同的`contextId`。
|
||||
```java
|
||||
@FeignClient(contextId = "fooClient", name = "stores", configuration = FooConfiguration.class)
|
||||
public interface FooClient {
|
||||
//..
|
||||
}
|
||||
@FeignClient(contextId = "barClient", name = "stores", configuration = BarConfiguration.class)
|
||||
public interface BarClient {
|
||||
//..
|
||||
}
|
||||
```
|
||||
### 超时处理
|
||||
可以为named client或所有client指定超时处理。OpenFeign使用两个timeout参数:
|
||||
- connectTimeout:该属性避免server长时间处理造成的调用方阻塞
|
||||
- readTimeout:该属性从连接建立时开始应用,并且当返回响应过慢时被触发(该参数衡量的是从建立连接到返回响应的过程)
|
||||
|
||||
### Feign Caching
|
||||
当使用@EnableCaching之后,一个`CachingCapability`的bean对象将会被配置,Feign client将会识别接口上的`@Cache*`注解。
|
||||
```java
|
||||
public interface DemoClient {
|
||||
|
||||
@GetMapping("/demo/{filterParam}")
|
||||
@Cacheable(cacheNames = "demo-cache", key = "#keyParam")
|
||||
String demoEndpoint(String keyParam, @PathVariable String filterParam);
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
478
spring/Spring Cloud/Spring Cloud gateway.md
Normal file
478
spring/Spring Cloud/Spring Cloud gateway.md
Normal file
@@ -0,0 +1,478 @@
|
||||
- [spring cloud gateway](#spring-cloud-gateway)
|
||||
- [项目引入spring cloud gateway](#项目引入spring-cloud-gateway)
|
||||
- [Spring Cloud gateway核心概念](#spring-cloud-gateway核心概念)
|
||||
- [Spring Cloud Gateway如何工作](#spring-cloud-gateway如何工作)
|
||||
- [配置route predicate factories和gateway filter factories](#配置route-predicate-factories和gateway-filter-factories)
|
||||
- [shortcut配置方式](#shortcut配置方式)
|
||||
- [Route Predicate Factories](#route-predicate-factories)
|
||||
- [After Route Predicate Factory](#after-route-predicate-factory)
|
||||
- [Before Route Predicate Factory](#before-route-predicate-factory)
|
||||
- [Between Route Predicate Factory](#between-route-predicate-factory)
|
||||
- [Cookie Route Predicate Factory](#cookie-route-predicate-factory)
|
||||
- [Header Route Predicate Factory](#header-route-predicate-factory)
|
||||
- [Host Route Predicate Factory](#host-route-predicate-factory)
|
||||
- [Method Route Predicate Factory](#method-route-predicate-factory)
|
||||
- [Path Route Predicate Factory](#path-route-predicate-factory)
|
||||
- [Query Route Predicate Factory](#query-route-predicate-factory)
|
||||
- [RemoteAddr Route Predicate Factory](#remoteaddr-route-predicate-factory)
|
||||
- [修改Remote Addr的解析方式](#修改remote-addr的解析方式)
|
||||
- [Weight Route Predicate Factory](#weight-route-predicate-factory)
|
||||
- [XForwarded Remote Addr Route Predicate](#xforwarded-remote-addr-route-predicate)
|
||||
- [GatewayFilter Factory](#gatewayfilter-factory)
|
||||
- [AddRequestHeader GatewayFilter Factory](#addrequestheader-gatewayfilter-factory)
|
||||
- [AddRequestHeadersIfNotPresent GatewayFilter Factory](#addrequestheadersifnotpresent-gatewayfilter-factory)
|
||||
- [AddRequestParameter GatewayFilter Factory](#addrequestparameter-gatewayfilter-factory)
|
||||
- [AddResponseHeader GatewayFilter Factory](#addresponseheader-gatewayfilter-factory)
|
||||
- [CircuitBreaker GatewayFilter Factory](#circuitbreaker-gatewayfilter-factory)
|
||||
- [CacheRequestBody GatewayFilter Factory](#cacherequestbody-gatewayfilter-factory)
|
||||
- [DedupeResponseHeader GatewayFilter Factory](#deduperesponseheader-gatewayfilter-factory)
|
||||
- [默认filters](#默认filters)
|
||||
- [Global Filters](#global-filters)
|
||||
- [组合Global Filter和Gateway Filter](#组合global-filter和gateway-filter)
|
||||
|
||||
|
||||
# spring cloud gateway
|
||||
## 项目引入spring cloud gateway
|
||||
如果要在项目中引入spring cloud gateway,可以在项目pom文件中添加如下依赖:
|
||||
```xml
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-gateway</artifactId>
|
||||
```
|
||||
> 如果已经在项目中引入了spring-cloud-starter-gateway的依赖,但是不想启用gateway,可以在配置文件中指定`spring.cloud.gateway.enabled=false`
|
||||
|
||||
> Spring Cloud gateway在运行时需要netty作为运行时环境,并且采用Spring Webflux构建,gateway在传统的基于servlet的环境中无法运行。
|
||||
|
||||
## Spring Cloud gateway核心概念
|
||||
- Route:gateway的基本构建单元,Route由一个id、一个目标uri,predicate集合、filter集合组成。如果聚合predicate为true,那么route被匹配到
|
||||
- Predicate:Predicate的输入是` Spring Framework ServerWebExchange`,通过该输入,可以匹配http请求中的任何内容,如header或请求参数等
|
||||
- Filter:其是GatewayFilter的实例,通过特定的factory构造。在filter中,可以在发送到下游请求之前或者之后对request和response进行修改。
|
||||
|
||||
## Spring Cloud Gateway如何工作
|
||||
1. 客户端向spring cloud gateway发送请求,如果Gateway Handler Mapping将请求匹配到route,那么会向gateway web handler发送请求
|
||||
2. gateway web handler通过filter chain对请求进行处理
|
||||
|
||||
## 配置route predicate factories和gateway filter factories
|
||||
有两种方式来配置predicate和filter:shortcuts和全展开参数
|
||||
### shortcut配置方式
|
||||
```yml
|
||||
spring:
|
||||
cloud:
|
||||
gateway:
|
||||
routes:
|
||||
- id: after_route
|
||||
uri: https://example.org
|
||||
predicates:
|
||||
- Cookie=mycookie,mycookievalue
|
||||
```
|
||||
## Route Predicate Factories
|
||||
Spring Cloud Gateway包含了许多内置的route predicate factories,所有的这些predicate都匹配到http请求的不同属性。可以通过逻辑运算符`and`将多个route predicate factory组合起来。
|
||||
### After Route Predicate Factory
|
||||
After Route Predicate Factory接收一个参数`datetime`,该参数为ZonedDateTime类型,该Predicate匹配发生在指定时间之后的请求。
|
||||
```yml
|
||||
spring:
|
||||
cloud:
|
||||
gateway:
|
||||
routes:
|
||||
- id: after_route
|
||||
uri: https://example.org
|
||||
predicates:
|
||||
- After=2017-01-20T17:42:47.789-07:00[America/Denver]
|
||||
```
|
||||
该route匹配2017-01-20T17:42:47.789-07:00[America/Denver]该时间之后的任意请求
|
||||
|
||||
### Before Route Predicate Factory
|
||||
Before route predicate factory接收一个`datetime`参数,参数为ZonedDateTime类型,该predicate匹配发生在某时间之前的请求。
|
||||
```yml
|
||||
spring:
|
||||
cloud:
|
||||
gateway:
|
||||
routes:
|
||||
- id: before_route
|
||||
uri: https://example.org
|
||||
predicates:
|
||||
- Before=2017-01-20T17:42:47.789-07:00[America/Denver]
|
||||
```
|
||||
该route匹配发生在2017-01-20T17:42:47.789-07:00[America/Denver]之前的任意请求
|
||||
### Between Route Predicate Factory
|
||||
Between Route Predicate Factory接收两个参数,都是ZonedDateTime类型,该predicate匹配发生在datetime1和datetime2之间的请求,其中第二个参数指定的时间必须位于第一个参数指定时间之后。如下指定了一个between predicate示例:
|
||||
```yml
|
||||
spring:
|
||||
cloud:
|
||||
gateway:
|
||||
routes:
|
||||
- id: between_route
|
||||
uri: https://example.org
|
||||
predicates:
|
||||
- Between=2017-01-20T17:42:47.789-07:00[America/Denver], 2017-01-21T17:42:47.789-07:00[America/Denver]
|
||||
|
||||
```
|
||||
### Cookie Route Predicate Factory
|
||||
Cookie Route Predicate Factory接收两个参数,`name`和`regexp`。该predicate匹配请求中含有指定名称,并且cookie值满足`regexp`正则表达式。如下展示了一个cookie predicate示例:
|
||||
```xml
|
||||
spring:
|
||||
cloud:
|
||||
gateway:
|
||||
routes:
|
||||
- id: cookie_route
|
||||
uri: https://example.org
|
||||
predicates:
|
||||
- Cookie=chocolate, ch.p
|
||||
```
|
||||
上述cookie_route会匹配请求中含有name为chocolate的cookie,并且cookie值满足`ch.p`正则表达式的请求。
|
||||
### Header Route Predicate Factory
|
||||
Header Route Predicate Factory接收两个参数,`header`和`regexp`(regexp是正则表达式)。该predicate会匹配一个拥有指定名称header的请求,且header对应的值要满足regexp表达式。
|
||||
如下示例显示了header route predicate的用法:
|
||||
```yml
|
||||
spring:
|
||||
cloud:
|
||||
gateway:
|
||||
routes:
|
||||
- id: header_route
|
||||
uri: https://example.org
|
||||
predicates:
|
||||
- Header=X-Request-Id, \d+
|
||||
```
|
||||
### Host Route Predicate Factory
|
||||
Host Route Predicate Factory接收一个参数:一个pattern列表。pattern是ant-style并用`.`作为分隔符。该predicate会匹配`Host`header,示例如下所示:
|
||||
```yml
|
||||
spring:
|
||||
cloud:
|
||||
gateway:
|
||||
routes:
|
||||
- id: host_route
|
||||
uri: https://example.org
|
||||
predicates:
|
||||
- Host=**.somehost.org,**.anotherhost.org
|
||||
```
|
||||
URI template 变量例如`{sub}.myhost.org`也支持。
|
||||
该route会匹配如下请求:headers中包含name为Host的header,并且Host header的值为`www.somehost.org`,`beta.somehost.org`,`www.anotherhost.org`等形式
|
||||
### Method Route Predicate Factory
|
||||
Method Route Predicate Factory接收一个`methods`参数,其指定了匹配的HTTP METHOD. 如下示例指定了一个method route predicate示例:
|
||||
```yml
|
||||
spring:
|
||||
cloud:
|
||||
gateway:
|
||||
routes:
|
||||
- id: method_route
|
||||
uri: https://example.org
|
||||
predicates:
|
||||
- Method=GET,POST
|
||||
```
|
||||
该predicate匹配method为GET或POST的任意请求。
|
||||
### Path Route Predicate Factory
|
||||
Path Route Predicate Factory接收两个参数,一个PathMatcher Pattern的列表,和一个可选的matchTrailingSlash(默认设置为true)。
|
||||
如下实例配置了一条path route predicate:
|
||||
```yml
|
||||
spring:
|
||||
cloud:
|
||||
gateway:
|
||||
routes:
|
||||
- id: path_route
|
||||
uri: https://example.org
|
||||
predicates:
|
||||
- Path=/red/{segment},/blue/{segment}
|
||||
```
|
||||
上述配置的route能够匹配/red/1或/red/1/、/red/blue或/blue/green.
|
||||
如果`matchTrailingSlash`被设置为false,那么/red/1/将不会被匹配到。
|
||||
该predicate将URI template变量(例如segment)解析为name、value键值对并且放到ServerWebExchange.getAttributes()中。这些值可以被GatewayFilter factories使用。
|
||||
可以通过如下的工具类方法来对这些变量进行访问,按如下示例所示:
|
||||
```java
|
||||
Map<String, String> uriVariables = ServerWebExchangeUtils.getUriTemplateVariables(exchange);
|
||||
|
||||
String segment = uriVariables.get("segment");
|
||||
```
|
||||
### Query Route Predicate Factory
|
||||
Query Route Predicate Factory接收两个参数:必填参数param和可选参数regexp,示例如下所示:
|
||||
```yml
|
||||
spring:
|
||||
cloud:
|
||||
gateway:
|
||||
routes:
|
||||
- id: query_route
|
||||
uri: https://example.org
|
||||
predicates:
|
||||
- Query=green
|
||||
```
|
||||
上述路由配置,匹配请求参数中含有green参数的请求
|
||||
```yml
|
||||
spring:
|
||||
cloud:
|
||||
gateway:
|
||||
routes:
|
||||
- id: query_route
|
||||
uri: https://example.org
|
||||
predicates:
|
||||
- Query=red, gree.
|
||||
```
|
||||
该route则是匹配请求参数含有red,并且其value满足`gree.`正则表达式的请求。
|
||||
### RemoteAddr Route Predicate Factory
|
||||
RemoteAddr route predicate factory接收一个`sources`列表(列表最小长度为1),列表元素为ipv4或ipv6字符串,例如`192.168.0.1/1`.
|
||||
如下显示了一个RemoteAddr route predicate示例:
|
||||
```yml
|
||||
spring:
|
||||
cloud:
|
||||
gateway:
|
||||
routes:
|
||||
- id: remoteaddr_route
|
||||
uri: https://example.org
|
||||
predicates:
|
||||
- RemoteAddr=192.168.1.1/24
|
||||
```
|
||||
该route匹配如下请求:如果请求的remote addr满足表达式,例如`192.168.1.10`。
|
||||
#### 修改Remote Addr的解析方式
|
||||
默认情况下,RemoteAddr predicate factory的解析方式是使用请求的remote address。但是,如果Spring Cloud Gateway如果位于proxy layer之后,那么remote address匹配的不是实际的client ip。
|
||||
可以通过自定义`RemoteAddressResolver`来自定义remote address的解析方式。Spring Cloud Gateway附带了一个非默认的remote address reslover,该reslover基于` X-Forwarded-For`header进行解析,该resolver是`XForwardedRemoteAddressResolver`。
|
||||
XForwardedRemoteAddressResolver有两个静态的构造器方法,分别使用了两种不同的安全方式:
|
||||
- `XForwardedRemoteAddressResolver::trustAll`:返回一个RemoteAddressResolver,会一直从`X-Forwarded-For`中取第一个ip address。该方法很容易收到欺骗,因为恶意客户端程序会设置`X-Forwarded-For`字段的初始值,而此时reslover会接收虚假设置的值。
|
||||
- `XForwardedRemoteAddressResolver::maxTrustedIndex`:接收一个index,该index和Spring Cloud Gateway之前运行的受信任设施的数量相关。
|
||||
|
||||
如存在如下header:
|
||||
```yml
|
||||
X-Forwarded-For: 0.0.0.1, 0.0.0.2, 0.0.0.3
|
||||
```
|
||||
如下maxTrustedIndex会产生的remote address将如下所示:
|
||||
| maxTrustedIndex | result |
|
||||
| :-: | :-: |
|
||||
| [Integer.MIN_VALUE,0] | 该范围输入不合法 |
|
||||
| 1 | 0.0.0.3 |
|
||||
| 2 | 0.0.0.2 |
|
||||
| 3 | 0.0.0.1 |
|
||||
| [4, Integer.MAX_VALUE] | 0.0.0.1 |
|
||||
|
||||
如下展示了如何通过java代码来完成相同的配置:
|
||||
```java
|
||||
RemoteAddressResolver resolver = XForwardedRemoteAddressResolver
|
||||
.maxTrustedIndex(1);
|
||||
|
||||
...
|
||||
|
||||
.route("direct-route",
|
||||
r -> r.remoteAddr("10.1.1.1", "10.10.1.1/24")
|
||||
.uri("https://downstream1")
|
||||
.route("proxied-route",
|
||||
r -> r.remoteAddr(resolver, "10.10.1.1", "10.10.1.1/24")
|
||||
.uri("https://downstream2")
|
||||
)
|
||||
```
|
||||
### Weight Route Predicate Factory
|
||||
Weight Route Predicate Factory接收两个参数:`group`和`weight`,weight将对每个group进行计算。如下示例配置了一个Weight Route Predicate:
|
||||
```yml
|
||||
spring:
|
||||
cloud:
|
||||
gateway:
|
||||
routes:
|
||||
- id: weight_high
|
||||
uri: https://weighthigh.org
|
||||
predicates:
|
||||
- Weight=group1, 8
|
||||
- id: weight_low
|
||||
uri: https://weightlow.org
|
||||
predicates:
|
||||
- Weight=group1, 2
|
||||
```
|
||||
该route将会把80%的负载转发到https://weighthigh.org,把20%的负载发送到https://weightlow.org
|
||||
### XForwarded Remote Addr Route Predicate
|
||||
XForwarded Remote Addr Route Predicate接收一个`sources`列表,列表元素为ipv4或ipv6地址,例如`192.168.0.1/16`。
|
||||
该route predicate允许通过`X-Forwarded-For` header来过滤请求。
|
||||
该predicate可以和反向代理服务器(通常用作负载均衡或web应用防火墙)一起使用,只有当请求来源于这些受信ip地址列表时,请求才会被允许访问。
|
||||
```yml
|
||||
spring:
|
||||
cloud:
|
||||
gateway:
|
||||
routes:
|
||||
- id: xforwarded_remoteaddr_route
|
||||
uri: https://example.org
|
||||
predicates:
|
||||
- XForwardedRemoteAddr=192.168.1.1/24
|
||||
```
|
||||
上述route,只有当`X-Forwarded-For` header中包含例如`192.168.1.10`的ip时,请求才会被匹配。
|
||||
|
||||
## GatewayFilter Factory
|
||||
Route filters 允许对进入的请求和外出的响应进行修改,Route Filter作用域为特定的route。Spring Cloud Gateway有许多内置的route filter。
|
||||
### AddRequestHeader GatewayFilter Factory
|
||||
AddRequestHeader GatewayFilter Factory接收一个name和value参数,如下示例配置了一个AddRequestHeader GatewayFilter:
|
||||
```yml
|
||||
spring:
|
||||
cloud:
|
||||
gateway:
|
||||
routes:
|
||||
- id: add_request_header_route
|
||||
uri: https://example.org
|
||||
filters:
|
||||
- AddRequestHeader=X-Request-red, blue
|
||||
```
|
||||
上述配置对所有匹配的请求在下游请求头中添加了`X-Request-red:blue`header。
|
||||
AddRequestHeader可以使用用于匹配path或host的URI变量。URI变量可以在value中使用,并且在运行时自动拓展,如下示例配置了一个使用URI变量的`AddRequestHeader GatewayFilter`:
|
||||
```yml
|
||||
spring:
|
||||
cloud:
|
||||
gateway:
|
||||
routes:
|
||||
- id: add_request_header_route
|
||||
uri: https://example.org
|
||||
predicates:
|
||||
- Path=/red/{segment}
|
||||
filters:
|
||||
- AddRequestHeader=X-Request-Red, Blue-{segment}
|
||||
```
|
||||
### AddRequestHeadersIfNotPresent GatewayFilter Factory
|
||||
AddRequestHeadersIfNotPresent GatewayFilter接收一个name和value对集合,name和value之间通过`:`进行分隔,如下示例配置了一个filter:
|
||||
```yml
|
||||
spring:
|
||||
cloud:
|
||||
gateway:
|
||||
routes:
|
||||
- id: add_request_headers_route
|
||||
uri: https://example.org
|
||||
filters:
|
||||
- AddRequestHeadersIfNotPresent=X-Request-Color-1:blue,X-Request-Color-2:green
|
||||
```
|
||||
上述配置会添加两个headers`X-Request-Color-1:blue`和`X-Request-Color-2:green`到下游请求的headers中。该操作和`AddRequestHeader`类似,但是AddRequestHeadersIfNotPresent只会当header不存在时才进行添加。如果该header存在,那么会保留该header的原有值。
|
||||
> 如果要设置一个multi-valued header,可以使用该headerName多次,例如`AddRequestHeadersIfNotPresent=X-Request-Color-1:blue,X-Request-Color-1:green`
|
||||
|
||||
`AddRequestHeadersIfNotPresent`也支持URI变量,如下配置了一个使用URI变量的Gateway Filter:
|
||||
```yml
|
||||
spring:
|
||||
cloud:
|
||||
gateway:
|
||||
routes:
|
||||
- id: add_request_header_route
|
||||
uri: https://example.org
|
||||
predicates:
|
||||
- Path=/red/{segment}
|
||||
filters:
|
||||
- AddRequestHeadersIfNotPresent=X-Request-Red:Blue-{segment}
|
||||
```
|
||||
### AddRequestParameter GatewayFilter Factory
|
||||
AddRequestParameter GatewayFilter Factory接收一个name和value参数,如下示例配置了一个filter:
|
||||
```xml
|
||||
spring:
|
||||
cloud:
|
||||
gateway:
|
||||
routes:
|
||||
- id: add_request_parameter_route
|
||||
uri: https://example.org
|
||||
filters:
|
||||
- AddRequestParameter=red, blue
|
||||
```
|
||||
上述配置会向下游请求中加入请求参数。
|
||||
AddRequestParameter GatewayFilter可以使用URI变量,使用URI变量的配置如下所示:
|
||||
```yml
|
||||
spring:
|
||||
cloud:
|
||||
gateway:
|
||||
routes:
|
||||
- id: add_request_parameter_route
|
||||
uri: https://example.org
|
||||
predicates:
|
||||
- Host: {segment}.myhost.org
|
||||
filters:
|
||||
- AddRequestParameter=foo, bar-{segment}
|
||||
```
|
||||
### AddResponseHeader GatewayFilter Factory
|
||||
`AddResponseHeader GatewayFilter Factory接收一个name和value参数,如下配置了一个filter:
|
||||
```yml
|
||||
spring:
|
||||
cloud:
|
||||
gateway:
|
||||
routes:
|
||||
- id: add_response_header_route
|
||||
uri: https://example.org
|
||||
filters:
|
||||
- AddResponseHeader=X-Response-Red, Blue
|
||||
```
|
||||
该filter添加了`X-Response-Red:Blue` header到下游响应的headers中。
|
||||
`AddResponseHeader GatewayFilter`可以使用URI变量,如下实例展示了使用URI变量的配置:
|
||||
```yml
|
||||
spring:
|
||||
cloud:
|
||||
gateway:
|
||||
routes:
|
||||
- id: add_response_header_route
|
||||
uri: https://example.org
|
||||
predicates:
|
||||
- Host: {segment}.myhost.org
|
||||
filters:
|
||||
- AddResponseHeader=foo, bar-{segment}
|
||||
```
|
||||
### CircuitBreaker GatewayFilter Factory
|
||||
CircuitBreaker GatewayFilter Factory使用了Spring Cloud CircuitBreaker API来将网关路由包装到断路器中。
|
||||
为了启用Spring Cloud CircuitBreaker filter,需要将`spring-cloud-starter-circuitbreaker-reactor-resilience4j`放到classpath路径下,如下示例配置了filter:
|
||||
```xml
|
||||
spring:
|
||||
cloud:
|
||||
gateway:
|
||||
routes:
|
||||
- id: circuitbreaker_route
|
||||
uri: https://example.org
|
||||
filters:
|
||||
- CircuitBreaker=myCircuitBreaker
|
||||
```
|
||||
### CacheRequestBody GatewayFilter Factory
|
||||
有一些场景需要读取请求体,因为请求只能被读取一次,因此需要缓存请求体。可以使用CacheRequestBody filter来缓存请求体,在请求被发送到下游之前,并且可以从`exchange`属性中获取请求体。
|
||||
```xml
|
||||
spring:
|
||||
cloud:
|
||||
gateway:
|
||||
routes:
|
||||
- id: cache_request_body_route
|
||||
uri: lb://downstream
|
||||
predicates:
|
||||
- Path=/downstream/**
|
||||
filters:
|
||||
- name: CacheRequestBody
|
||||
args:
|
||||
bodyClass: java.lang.String
|
||||
```
|
||||
上述配置获取请求体并且将请求体转为了String类型,后续可以通过`ServerWebExchange.getAttributes()`来获取,key为`ServerWebExchangeUtils.CACHED_REQUEST_BODY_ATTR`.
|
||||
|
||||
### DedupeResponseHeader GatewayFilter Factory
|
||||
DedupeResponseHeader GatewayFilter factory接收一个name参数和一个可选的strategy参数,name是一个用空格分隔的list,list元素为header name。如下配置了一个filter示例:
|
||||
```yml
|
||||
spring:
|
||||
cloud:
|
||||
gateway:
|
||||
routes:
|
||||
- id: dedupe_response_header_route
|
||||
uri: https://example.org
|
||||
filters:
|
||||
- DedupeResponseHeader=Access-Control-Allow-Credentials Access-Control-Allow-Origin
|
||||
```
|
||||
上述配置中移除了Access-Control-Allow-Credentials和Access-Control-Allow-Origin响应header中的重复值,当gateway cors逻辑和下游逻辑都添加了它们时。
|
||||
DedupeResponseHeader还有一个可选的strategy参数,默认为RETAIN_FIRST,还可以是RETAIN_LAST和RETAIN_UNIQUE。
|
||||
|
||||
### 默认filters
|
||||
如果想要添加一个filter并将其应用到所有的routes中,可以使用`spring.cloud.gateway.default-filters`,该属性可以接收一个filter列表。如下定义了一系列默认filter:
|
||||
```yml
|
||||
spring:
|
||||
cloud:
|
||||
gateway:
|
||||
default-filters:
|
||||
- AddResponseHeader=X-Response-Default-Red, Default-Blue
|
||||
- PrefixPath=/httpbin
|
||||
```
|
||||
## Global Filters
|
||||
GlobalFilter接口的签名和Gateway接口的签名一样。GlobalFilter会在满足条件后应用于所有的routes。
|
||||
### 组合Global Filter和Gateway Filter
|
||||
当请求匹配到一个route之后,filtering web handler将所有的GlobalFilter实例和route对应的Gateway Filter实例添加到一个filter chain中。该filter chain通过`org.springframework.core.Ordered`接口进行排序。filter chain中排序最靠前的filter在pre-phase中位于第一个,并且在post-phase中位于最后一个。
|
||||
如下配置了一个filter chain:
|
||||
```java
|
||||
@Bean
|
||||
public GlobalFilter customFilter() {
|
||||
return new CustomGlobalFilter();
|
||||
}
|
||||
|
||||
public class CustomGlobalFilter implements GlobalFilter, Ordered {
|
||||
|
||||
@Override
|
||||
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
|
||||
log.info("custom global filter");
|
||||
return chain.filter(exchange);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOrder() {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
```
|
||||
33
spring/Spring Cloud/Spring Cloud.md
Normal file
33
spring/Spring Cloud/Spring Cloud.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# Spring Cloud简介
|
||||
## eureka
|
||||
eureka是由netflix公司开源的一个服务注册与发现组件。eureka和其他一些同样由netflix公司开源的组建一起被整合为spring cloud netflix模块。
|
||||
### eureka和zookeeper的区别
|
||||
#### CAP原则
|
||||
CAP原则指的是在一个分布式系统中,一致性(Consistency)、可用性(Aailability)和分区容错性(Partition Tolerance),三者中最多只能实现两者,无法实现三者兼顾。
|
||||
- C:一致性表示分布式系统中,不同节点中的数据都要相同
|
||||
- A:可用性代表可以允许某一段时间内分布式系统中不同节点的数据可以不同,只要分布式系统能够保证最终数据的一致性。中途,允许分布式系统中的节点数据存在不一致的情形
|
||||
- P:及Partition Tolerance,通常情况下,节点之间的网络的断开被称之为network partition。故而,partition tolerance则是能够保证即使发生network partition,分布式系统中的节点也能够继续运行
|
||||
|
||||
通常P是必选的,而在满足P的前提下,A和C只能够满足一条,即AP或CP。
|
||||
#### zookeeper遵循的原则
|
||||
zookeeper遵循的是cp原则,如果zookeeper集群中的leader宕机,那么在新leader选举出来之前,zookeeper集群是拒绝向外提供服务的。
|
||||
#### eureka遵循的原则
|
||||
和zookeeper不同,eureka遵循的是ap原则,这这意味着eureka允许多个节点之间存在数据不一致的情况,即使某个节点的宕机,eureka仍然能够向外提供服务。
|
||||
|
||||
### eureka使用
|
||||
可以通过向项目中添加eureka-server的依赖来启动一个eureka-server实例。eureka-server作为注册中心,会将其本身也作为一个服务注册到注册中心中。
|
||||
#### 注册实例id
|
||||
注册实例id由三部分组成,`主机名称:应用名称:端口号`构成了实例的id。每个实例id都唯一。
|
||||
#### eureka-server配置
|
||||
- eviction-interval-timer-in-ms:eureka-server会运行固定的scheduled task来清除过期的client,eviction-interval-timer-in-ms属性用于定义task之间的间隔,默认情况下该属性值为60s
|
||||
- renewal-percent-threshold:基于该属性,eureka来计算每分钟期望从所有客户端接受到的心跳数。根据eureka-server的自我保护机制,如果eureka-server收到的心跳数小于threshold,那么eureka-server会停止进行客户端实例的淘汰,直到接收到的心跳数大于threshold
|
||||
#### eureka-instance配置
|
||||
eureka-server-instance本身也作为一个instance注册到注册中心中,故而可以针对eureka-instance作一些配置。
|
||||
#### eureka集群
|
||||
eureka集群是去中心化的集群,没有主机和从机的概念,eureka节点会向集群中所有其他的节点广播数据的变动。
|
||||
## Ribbon
|
||||
Spring Cloud Ribbon是一个基于Http和Tcp的客户端负载均衡工具,基于Netflix Ribbon实现,Ribbon主要用于提供负载均衡算法和服务调用。Ribbon的客户端组件提供了一套完善的配置项,如超时和重试等。
|
||||
在通过Spring Cloud构建微服务时,Ribbon有两种使用方法,一种是和RedisTemplate结合使用,另一种是和OpenFegin相结合。
|
||||
## OpenFeign
|
||||
OpenFeign是一个远程调用组件,使用接口和注解以http的形式完成调用。
|
||||
Feign中集成了Ribbon,而Ribbon中则集成了eureka。
|
||||
111
spring/Spring Http Client/Spring RestTemplate.md
Normal file
111
spring/Spring Http Client/Spring RestTemplate.md
Normal file
@@ -0,0 +1,111 @@
|
||||
# RestTemplate
|
||||
RestTemplate提供了比http client library更高层级的api,其允许以更加容易的方式来调用rest接口。
|
||||
## 初始化
|
||||
RestTemplate的默认构造函数使用`java.net.HttpURLConnection`来执行http请求。
|
||||
## URIs
|
||||
RestTemplate的许多方法都接收一个URI template和URI tempalte变量要么作为`String`变量参数,要么作为`Map<String,String>`.
|
||||
如下示例使用了String类型参数:
|
||||
```java
|
||||
String result = restTemplate.getForObject(
|
||||
"https://example.com/hotels/{hotel}/bookings/{booking}", String.class, "42", "21");
|
||||
```
|
||||
如下示例则是使用了Map类型的参数:
|
||||
```java
|
||||
Map<String, String> vars = Collections.singletonMap("hotel", "42");
|
||||
|
||||
String result = restTemplate.getForObject(
|
||||
"https://example.com/hotels/{hotel}/rooms/{hotel}", String.class, vars);
|
||||
```
|
||||
URI tempalte是自动编码的,按如下示例所示:
|
||||
```java
|
||||
restTemplate.getForObject("https://example.com/hotel list", String.class);
|
||||
|
||||
// Results in request to "https://example.com/hotel%20list"
|
||||
```
|
||||
## headers
|
||||
可以通过`exchange()`方法来指定请求的headers,如下所示:
|
||||
```java
|
||||
String uriTemplate = "https://example.com/hotels/{hotel}";
|
||||
URI uri = UriComponentsBuilder.fromUriString(uriTemplate).build(42);
|
||||
|
||||
RequestEntity<Void> requestEntity = RequestEntity.get(uri)
|
||||
.header("MyRequestHeader", "MyValue")
|
||||
.build();
|
||||
|
||||
ResponseEntity<String> response = template.exchange(requestEntity, String.class);
|
||||
|
||||
String responseHeader = response.getHeaders().getFirst("MyResponseHeader");
|
||||
String body = response.getBody();
|
||||
```
|
||||
## body
|
||||
向RestTemplate中方法传入的参数或RestTemplate方法返回的对象,其与raw content之间的转换都是通过`HttpMessageConverter`来执行的。
|
||||
在一个POST请求中,传入到RestTemplate方法中的参数会被序列化到请求的body中:
|
||||
```java
|
||||
URI location = template.postForLocation("https://example.com/people", person);
|
||||
```
|
||||
**使用者并不需要显式设置请求体的`Content-Type`header**。在大多数情况下,都可以基于source Object类型找到一个合适的message converter,而被选中的message converter则会设置content type header。如果在必要情况下,可以通过`exchange`方法来显式提供`Content-Type` header,并且在设置content type请求头之后,会影响选中的message converter。
|
||||
如下示例显示了一个get请求,将响应的body反序列化为方法的返回类型:
|
||||
```java
|
||||
Person person = restTemplate.getForObject("https://example.com/people/{id}", Person.class, 42);
|
||||
```
|
||||
类似于`Content-Type`请求头,`Accept`请求头也不需要显式设置。在多数情况下,根据方法返回类型选中的message converter会帮助注入Accept请求头。在必要情况下,可以通过`exchange()`方法来指定要传递的`Accept`请求头。
|
||||
默认情况下,RestTemplate注册了所有内置的message converter。
|
||||
|
||||
## Multipart
|
||||
如果需要发送multipart数据,需要提供一个`MultiValueMap<String,Object>`,该map的value可以是一个part content object,file part resource,含有part content和header的HttpEntity。
|
||||
```java
|
||||
MultiValueMap<String, Object> parts = new LinkedMultiValueMap<>();
|
||||
|
||||
parts.add("fieldPart", "fieldValue");
|
||||
parts.add("filePart", new FileSystemResource("...logo.png"));
|
||||
parts.add("jsonPart", new Person("Jason"));
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_XML);
|
||||
parts.add("xmlPart", new HttpEntity<>(myBean, headers));
|
||||
|
||||
```
|
||||
当MultiValueMap构建好之后,可以将其传递给RestTemplate,如下所示:
|
||||
```java
|
||||
MultiValueMap<String, Object> parts = ...;
|
||||
template.postForObject("https://example.com/upload", parts, Void.class);
|
||||
```
|
||||
如果MultiValueMap至少含有一个不为String的值,那么Content-Type将会被`FormHttpMessageConverter`设置为`multipart/form-data`。
|
||||
如果MultiValueMap中只有String类型的值,那么content-type会被设置为`application/x-www-form-urlencoded`。
|
||||
|
||||
## Http接口
|
||||
Spring允许将Http service定义为java接口,该方法上有注解用于http交换。可以通过该接口产生一个代理对象实现该接口,代理对象可以执行http请求交换。
|
||||
首先,定义一个接口,并且通过`@HttpExchange`注解来标注接口中的方法:
|
||||
```java
|
||||
interface RepositoryService {
|
||||
|
||||
@GetExchange("/repos/{owner}/{repo}")
|
||||
Repository getRepository(@PathVariable String owner, @PathVariable String repo);
|
||||
|
||||
// more HTTP exchange methods...
|
||||
|
||||
}
|
||||
```
|
||||
其次,可以创建一个代理对象来执行http请求,创建方式如下所示:
|
||||
```java
|
||||
WebClient client = WebClient.builder().baseUrl("https://api.github.com/").build();
|
||||
HttpServiceProxyFactory factory = HttpServiceProxyFactory.builder(WebClientAdapter.forClient(client)).build();
|
||||
|
||||
RepositoryService service = factory.createClient(RepositoryService.class);
|
||||
```
|
||||
`@HttpExchange`注解也支持类级别,类级别注解将会应用到接口中所有的方法:
|
||||
```java
|
||||
@HttpExchange(url = "/repos/{owner}/{repo}", accept = "application/vnd.github.v3+json")
|
||||
interface RepositoryService {
|
||||
|
||||
@GetExchange
|
||||
Repository getRepository(@PathVariable String owner, @PathVariable String repo);
|
||||
|
||||
@PatchExchange(contentType = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
|
||||
void updateRepository(@PathVariable String owner, @PathVariable String repo,
|
||||
@RequestParam String name, @RequestParam String description, @RequestParam String homepage);
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
118
spring/Spring Security/spring_security_sgg.md
Normal file
118
spring/Spring Security/spring_security_sgg.md
Normal file
@@ -0,0 +1,118 @@
|
||||
# Spring Security
|
||||
Spring Security提供了authentication(认证)、authorization(授权)、protection against common attacks(防御常见攻击)的功能。
|
||||
|
||||
Spring Security提供了针对如下常见攻击的防御:
|
||||
- CSRF
|
||||
- Http Headers
|
||||
- Http Requests
|
||||
|
||||
## 默认自动装配
|
||||
当web项目引入Spring Security依赖后,如果不做任何特殊处理,Spring Security会默认为项目配置一个登录校验。当用户没有进行登录而访问页面时,会拦截用户请求返回一个登录页,在用户登录成功之后,再会将url重定向到用户先前请求的页面。
|
||||
|
||||
### Spring Security默认为项目做的操作
|
||||
1. 保护web项目的url,Spring Security要求与http接口进行的任何交互都需要进行身份认证
|
||||
2. Spring Security会在web项目启动时生成一个默认的用户,用户名为user,用户密码会通过日志打出
|
||||
3. Spring Security会为项目生成默认的登录和注销页面,并提供了基本登录流程和注销流程
|
||||
4. 对于http请求,如果在没有进行身份认证的情况下,会重定向到登录页面
|
||||
|
||||
## Spring Security底层结构
|
||||
Spring Security底层通过Filter链来实现。
|
||||
|
||||
### DelegatingFilterProxy
|
||||
spring boot提供了Filter的实现类DelegatingFilterProxy,该类将实际的过滤逻辑委托给另一个filter bean对象。
|
||||
|
||||
通过DelegatingFilterProxy,可以将servlet container和spring容器结合起来,将DelegatingFilterProxy对象作为filter注册到servlet container中,然后DelegatingFilterProxy把实际过滤逻辑委托给spring context中的bean对象,如此可以通过向spring容器中注册bean对象来自定义filter chain逻辑。
|
||||
|
||||
### SecurityFilterChain
|
||||
DelegationFilterProxy将filter逻辑委托给了FilterChainProxy。
|
||||
|
||||
而FilterProxyChain则是包含了SecurityFilterChain(并且可以包含多个SecurityFilterChain)。SecurityFilterChain中包含的才是多个filter bean对象。
|
||||
|
||||
如果FilterChainProxy中注册了多个SecurityFilterChain,那么将由FilterChainProxy来决定实际调用哪个SecurityFilterChain。只有匹配的第一个SecurityFilterChain会被实际调用。
|
||||
|
||||
> 如果当spring容器中存在多个SecurityFilterChian bean对象,那么可以通过@Order注解来指定SecurityFilterChain bean对象的顺序,从而调整不同SecurityFilterChain的优先级
|
||||
|
||||
示例如下:
|
||||
```java
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
public class SecurityConfig {
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.csrf(Customizer.withDefaults())
|
||||
.authorizeHttpRequests(authorize -> authorize
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.httpBasic(Customizer.withDefaults())
|
||||
.formLogin(Customizer.withDefaults());
|
||||
return http.build();
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
上述代码注册的SecurityFilterChain将拥有如下顺序:
|
||||
| Filter | Added by |
|
||||
| :-: | :-: |
|
||||
| CsrfFilter | HttpSecurity#csrf |
|
||||
| UsernamePasswordAuthenticationFilter | HttpSecurity#formLogin |
|
||||
| BasicAuthenticationFilter | HttpSecurity#httpBasic |
|
||||
| AuthorizationFilter | HttpSecurity#authorizeHttpRequests |
|
||||
|
||||
> 通常,security filter chain都是先执行authentication再执行authorization
|
||||
|
||||
### 自定义Filter
|
||||
除了使用Spring Security预置的filter外,还可以使用自定义的filter,自定义filter使用示例如下:
|
||||
```java
|
||||
// Filter定义
|
||||
import java.io.IOException;
|
||||
|
||||
import jakarta.servlet.Filter;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.ServletRequest;
|
||||
import jakarta.servlet.ServletResponse;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
|
||||
public class TenantFilter implements Filter {
|
||||
|
||||
@Override
|
||||
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
|
||||
HttpServletRequest request = (HttpServletRequest) servletRequest;
|
||||
HttpServletResponse response = (HttpServletResponse) servletResponse;
|
||||
|
||||
String tenantId = request.getHeader("X-Tenant-Id"); (1)
|
||||
boolean hasAccess = isUserAllowed(tenantId); (2)
|
||||
if (hasAccess) {
|
||||
filterChain.doFilter(request, response); (3)
|
||||
return;
|
||||
}
|
||||
throw new AccessDeniedException("Access denied"); (4)
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
```java
|
||||
// 将自定义filter添加到SecurityFilterChain中
|
||||
@Bean
|
||||
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
// ...
|
||||
.addFilterBefore(new TenantFilter(), AuthorizationFilter.class);
|
||||
return http.build();
|
||||
}
|
||||
```
|
||||
## 授权
|
||||
对于spring security,其授权可以分为基于request的授权和基于方法的授权。
|
||||
|
||||
### 基于request的授权
|
||||
基于request的授权可以根据用户-权限、用户-角色、用户-角色-权限进行实现。
|
||||
|
||||
### 基于方法的授权
|
||||
基于方法的授权可以通过向方法添加权限注解来实现。
|
||||
|
||||
|
||||
12
spring/Spring core/@Validated, @Valid.md
Normal file
12
spring/Spring core/@Validated, @Valid.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# @Validated & @Valid
|
||||
## @Validated
|
||||
@Validated可以用于方法级别,方法参数级别以及类级别
|
||||
- 类级别:当@Validated注解用于类级别时,该类中所有的约束(例如@Max,@Size)都会被校验
|
||||
- 参数级别:类似于@Valid
|
||||
- 方法级别:将@Validated注解用于方法级别,会override group信息,但是不会插入切面
|
||||
|
||||
可以将@Validated作用于spring mvc handler的参数,也可以将其作为方法级别的验证。方法级别的验证允许覆盖validation group,但是不会作为切面。
|
||||
> @Validated实现原理基于spring aop,故而只有标注了@Validated注解的bean对象才会被代理并拦截
|
||||
|
||||
## @Valid
|
||||
相对于@Validated注解,@Valid注解允许应用于返回类型和field上,故而通过@Valid注解可以用于嵌套类的校验
|
||||
@@ -1,15 +1,15 @@
|
||||
# POJO
|
||||
## POJO定义
|
||||
POJO(Plain Old Java Object)是一种直接的类型,POJO并不包含对任何框架的引用。
|
||||
> 对于POJO类型,该类属性和方法的定义并没有特定的约束和限制
|
||||
## Java Bean命名约束
|
||||
由于对POJO本身并没有对POJO类属性和方法的定义强制指定命名约束,因而许多框默认支持Java Bean命名约束。
|
||||
> ### Java Bean命名约束
|
||||
> 在Java Bean命名约束中,为POJO类属性和方法的命名指定了如下规则:
|
||||
> 1. 属性的访问权限都被设置为private,属性通过getter和setter向外暴露
|
||||
> 2. 对于方法的命名,getter和setter遵循getXXX/setXXX的命名规范(对于boolean属性的getter,可以使用isXXX形式
|
||||
> 3. Java Bean命名规范要求Java Bean对象需要提供无参构造方法
|
||||
> 4. 实现Serializable接口,能够将对象以二进制的格式进行存储
|
||||
## 其他命名规范
|
||||
由于Java Bean命名规范中有些规则强制对Java Bean的命名进行限制可能会带来弊端,故而如今许多框架在接受Java Bean命名规范之余,仍然支持其他的POJO命名规范
|
||||
> 如在Spring中,通过@Component注解注册Bean对象时,被@Component注解的类并不一定要实现Serializable接口,也不一定要拥有无参构造方法。
|
||||
# POJO
|
||||
## POJO定义
|
||||
POJO(Plain Old Java Object)是一种直接的类型,POJO并不包含对任何框架的引用。
|
||||
> 对于POJO类型,该类属性和方法的定义并没有特定的约束和限制
|
||||
## Java Bean命名约束
|
||||
由于对POJO本身并没有对POJO类属性和方法的定义强制指定命名约束,因而许多框默认支持Java Bean命名约束。
|
||||
> ### Java Bean命名约束
|
||||
> 在Java Bean命名约束中,为POJO类属性和方法的命名指定了如下规则:
|
||||
> 1. 属性的访问权限都被设置为private,属性通过getter和setter向外暴露
|
||||
> 2. 对于方法的命名,getter和setter遵循getXXX/setXXX的命名规范(对于boolean属性的getter,可以使用isXXX形式
|
||||
> 3. Java Bean命名规范要求Java Bean对象需要提供无参构造方法
|
||||
> 4. 实现Serializable接口,能够将对象以二进制的格式进行存储
|
||||
## 其他命名规范
|
||||
由于Java Bean命名规范中有些规则强制对Java Bean的命名进行限制可能会带来弊端,故而如今许多框架在接受Java Bean命名规范之余,仍然支持其他的POJO命名规范
|
||||
> 如在Spring中,通过@Component注解注册Bean对象时,被@Component注解的类并不一定要实现Serializable接口,也不一定要拥有无参构造方法。
|
||||
|
||||
@@ -1,109 +1,109 @@
|
||||
# SpEL(Spring Expression Language)
|
||||
- ## SpEL的用法
|
||||
- SpEL如何将表达式从字符串转化为计算后的值
|
||||
- 在转化过程中,在parseExpression方法执行时可能会抛出ParseException异常,在执行getValue方法时可能会抛出EvaluationException
|
||||
```java
|
||||
ExpressionParser parser = new SpelExpressionParser();
|
||||
Expression exp = parser.parseExpression("'Hello World'");
|
||||
String message = (String) exp.getValue();
|
||||
```
|
||||
- 在SpEL中获取String的字节数组
|
||||
```java
|
||||
ExpressionParser parser=new SpelExpressionParser();
|
||||
Expression exp=parser.parseExpression("'Hello World'.bytes");
|
||||
byte[] bytes=(byte[])exp.getValue();
|
||||
```
|
||||
- 在调用Expression类型的getValue方法时,可以不用进行强制类型转换,而是在getValue方法中传入一个Class参数,返回值将会被自动转换成Class对应的目标类型,当转换失败时会抛出EvaluationException
|
||||
```java
|
||||
ExpressionParser parser=new SpelExpressionParser();
|
||||
Expression exp=parser.parseExpression("'Hello World'.bytes.length");
|
||||
Integer bytes=exp.getValue(Integer.class);
|
||||
```
|
||||
- SpEL可以针对特定的对象,给出一个表达式并且在getValue方法中传入一个对象,那么表达式中的变量将会针对该对象中的特定属性
|
||||
```java
|
||||
// 如下步骤会比较waifu对象的name属性是否为"touma"字符串
|
||||
ExpressionParser parser=new SpelExpressionParser();
|
||||
Expression exp=parser.parseExpression("name=='touma'");
|
||||
Boolean equals=exp.getValue(waifu,Boolean.class);
|
||||
```
|
||||
- 可以为parser设置一个parserconfiguration,用于处理当列表或集合元素的index操作超过集合长度时的默认行为
|
||||
```java
|
||||
class Demo {
|
||||
public List<String> list;
|
||||
}
|
||||
|
||||
// Turn on:
|
||||
// - auto null reference initialization
|
||||
// - auto collection growing
|
||||
SpelParserConfiguration config = new SpelParserConfiguration(true, true);
|
||||
|
||||
ExpressionParser parser = new SpelExpressionParser(config);
|
||||
|
||||
Expression expression = parser.parseExpression("list[3]");
|
||||
|
||||
Demo demo = new Demo();
|
||||
|
||||
Object o = expression.getValue(demo);
|
||||
|
||||
// demo.list will now be a real collection of 4 entries
|
||||
// Each entry is a new empty String
|
||||
```
|
||||
- ## SpEL在bean对象定义时的使用
|
||||
- 在使用@Value注解时,可以结合SpEL表达式进行使用,@Value注解可以运用在域变量、方法、方法和构造器的参数上。@Value会指定默认值
|
||||
- ## SpEL对List、Map的支持
|
||||
- 可以通过{}来直接表示list
|
||||
```java
|
||||
List numbers = (List) parser.parseExpression("{1,2,3,4}").getValue(context);
|
||||
|
||||
List listOfLists = (List) parser.parseExpression("{{'a','b'},{'x','y'}}").getValue(context);
|
||||
```
|
||||
- 可以通过{key:value}形式来直接表示map,空map用{:}来进行表示
|
||||
```java
|
||||
// evaluates to a Java map containing the two entries
|
||||
Map inventorInfo = (Map) parser.parseExpression("{name:'Nikola',dob:'10-July-1856'}").getValue(context);
|
||||
|
||||
Map mapOfMaps = (Map) parser.parseExpression("{name:{first:'Nikola',last:'Tesla'},dob:{day:10,month:'July',year:1856}}").getValue(context);
|
||||
```
|
||||
- 可以通过new int[]{}的形式为SpEL指定数组
|
||||
```java
|
||||
int[] numbers1 = (int[]) parser.parseExpression("new int[4]").getValue(context);
|
||||
|
||||
// Array with initializer
|
||||
int[] numbers2 = (int[]) parser.parseExpression("new int[]{1,2,3}").getValue(context);
|
||||
|
||||
// Multi dimensional array
|
||||
int[][] numbers3 = (int[][]) parser.parseExpression("new int[4][5]").getValue(context);
|
||||
```
|
||||
- ## SpEL支持的特殊操作符
|
||||
- instanceof
|
||||
```java
|
||||
boolean falseValue = parser.parseExpression(
|
||||
"'xyz' instanceof T(Integer)").getValue(Boolean.class);
|
||||
```
|
||||
- 正则表达式
|
||||
```java
|
||||
boolean trueValue = parser.parseExpression(
|
||||
"'5.00' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean.class);
|
||||
```
|
||||
- 类型操作符,获取类型的Class对象、调用静态方法
|
||||
```java
|
||||
Class dateClass = parser.parseExpression("T(java.util.Date)").getValue(Class.class);
|
||||
|
||||
Class stringClass = parser.parseExpression("T(String)").getValue(Class.class);
|
||||
|
||||
boolean trueValue = parser.parseExpression(
|
||||
"T(java.math.RoundingMode).CEILING < T(java.math.RoundingMode).FLOOR")
|
||||
.getValue(Boolean.class);
|
||||
```
|
||||
- new操作符:
|
||||
- 可以在SpEL表达式中通过new操作符来调用构造器,但是除了位于java.lang包中的类,对其他的类调用构造器时都必须指定类的全类名
|
||||
```java
|
||||
Inventor einstein = p.parseExpression(
|
||||
"new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German')")
|
||||
.getValue(Inventor.class);
|
||||
|
||||
// create new Inventor instance within the add() method of List
|
||||
p.parseExpression(
|
||||
"Members.add(new org.spring.samples.spel.inventor.Inventor(
|
||||
'Albert Einstein', 'German'))").getValue(societyContext);
|
||||
# SpEL(Spring Expression Language)
|
||||
- ## SpEL的用法
|
||||
- SpEL如何将表达式从字符串转化为计算后的值
|
||||
- 在转化过程中,在parseExpression方法执行时可能会抛出ParseException异常,在执行getValue方法时可能会抛出EvaluationException
|
||||
```java
|
||||
ExpressionParser parser = new SpelExpressionParser();
|
||||
Expression exp = parser.parseExpression("'Hello World'");
|
||||
String message = (String) exp.getValue();
|
||||
```
|
||||
- 在SpEL中获取String的字节数组
|
||||
```java
|
||||
ExpressionParser parser=new SpelExpressionParser();
|
||||
Expression exp=parser.parseExpression("'Hello World'.bytes");
|
||||
byte[] bytes=(byte[])exp.getValue();
|
||||
```
|
||||
- 在调用Expression类型的getValue方法时,可以不用进行强制类型转换,而是在getValue方法中传入一个Class参数,返回值将会被自动转换成Class对应的目标类型,当转换失败时会抛出EvaluationException
|
||||
```java
|
||||
ExpressionParser parser=new SpelExpressionParser();
|
||||
Expression exp=parser.parseExpression("'Hello World'.bytes.length");
|
||||
Integer bytes=exp.getValue(Integer.class);
|
||||
```
|
||||
- SpEL可以针对特定的对象,给出一个表达式并且在getValue方法中传入一个对象,那么表达式中的变量将会针对该对象中的特定属性
|
||||
```java
|
||||
// 如下步骤会比较waifu对象的name属性是否为"touma"字符串
|
||||
ExpressionParser parser=new SpelExpressionParser();
|
||||
Expression exp=parser.parseExpression("name=='touma'");
|
||||
Boolean equals=exp.getValue(waifu,Boolean.class);
|
||||
```
|
||||
- 可以为parser设置一个parserconfiguration,用于处理当列表或集合元素的index操作超过集合长度时的默认行为
|
||||
```java
|
||||
class Demo {
|
||||
public List<String> list;
|
||||
}
|
||||
|
||||
// Turn on:
|
||||
// - auto null reference initialization
|
||||
// - auto collection growing
|
||||
SpelParserConfiguration config = new SpelParserConfiguration(true, true);
|
||||
|
||||
ExpressionParser parser = new SpelExpressionParser(config);
|
||||
|
||||
Expression expression = parser.parseExpression("list[3]");
|
||||
|
||||
Demo demo = new Demo();
|
||||
|
||||
Object o = expression.getValue(demo);
|
||||
|
||||
// demo.list will now be a real collection of 4 entries
|
||||
// Each entry is a new empty String
|
||||
```
|
||||
- ## SpEL在bean对象定义时的使用
|
||||
- 在使用@Value注解时,可以结合SpEL表达式进行使用,@Value注解可以运用在域变量、方法、方法和构造器的参数上。@Value会指定默认值
|
||||
- ## SpEL对List、Map的支持
|
||||
- 可以通过{}来直接表示list
|
||||
```java
|
||||
List numbers = (List) parser.parseExpression("{1,2,3,4}").getValue(context);
|
||||
|
||||
List listOfLists = (List) parser.parseExpression("{{'a','b'},{'x','y'}}").getValue(context);
|
||||
```
|
||||
- 可以通过{key:value}形式来直接表示map,空map用{:}来进行表示
|
||||
```java
|
||||
// evaluates to a Java map containing the two entries
|
||||
Map inventorInfo = (Map) parser.parseExpression("{name:'Nikola',dob:'10-July-1856'}").getValue(context);
|
||||
|
||||
Map mapOfMaps = (Map) parser.parseExpression("{name:{first:'Nikola',last:'Tesla'},dob:{day:10,month:'July',year:1856}}").getValue(context);
|
||||
```
|
||||
- 可以通过new int[]{}的形式为SpEL指定数组
|
||||
```java
|
||||
int[] numbers1 = (int[]) parser.parseExpression("new int[4]").getValue(context);
|
||||
|
||||
// Array with initializer
|
||||
int[] numbers2 = (int[]) parser.parseExpression("new int[]{1,2,3}").getValue(context);
|
||||
|
||||
// Multi dimensional array
|
||||
int[][] numbers3 = (int[][]) parser.parseExpression("new int[4][5]").getValue(context);
|
||||
```
|
||||
- ## SpEL支持的特殊操作符
|
||||
- instanceof
|
||||
```java
|
||||
boolean falseValue = parser.parseExpression(
|
||||
"'xyz' instanceof T(Integer)").getValue(Boolean.class);
|
||||
```
|
||||
- 正则表达式
|
||||
```java
|
||||
boolean trueValue = parser.parseExpression(
|
||||
"'5.00' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean.class);
|
||||
```
|
||||
- 类型操作符,获取类型的Class对象、调用静态方法
|
||||
```java
|
||||
Class dateClass = parser.parseExpression("T(java.util.Date)").getValue(Class.class);
|
||||
|
||||
Class stringClass = parser.parseExpression("T(String)").getValue(Class.class);
|
||||
|
||||
boolean trueValue = parser.parseExpression(
|
||||
"T(java.math.RoundingMode).CEILING < T(java.math.RoundingMode).FLOOR")
|
||||
.getValue(Boolean.class);
|
||||
```
|
||||
- new操作符:
|
||||
- 可以在SpEL表达式中通过new操作符来调用构造器,但是除了位于java.lang包中的类,对其他的类调用构造器时都必须指定类的全类名
|
||||
```java
|
||||
Inventor einstein = p.parseExpression(
|
||||
"new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German')")
|
||||
.getValue(Inventor.class);
|
||||
|
||||
// create new Inventor instance within the add() method of List
|
||||
p.parseExpression(
|
||||
"Members.add(new org.spring.samples.spel.inventor.Inventor(
|
||||
'Albert Einstein', 'German'))").getValue(societyContext);
|
||||
```
|
||||
@@ -1,177 +1,177 @@
|
||||
# Spring AOP
|
||||
- ## Spring AOP的核心概念
|
||||
- Aspect:切面,一个模块化的考虑
|
||||
- Joint Point:连接点,程序执行时的一个时间点,通常是方法的执行
|
||||
- Advice:当切面在一个切入点执行多做时,执行的动作被称之为Advice,Advice有不同的类型:before、after、around
|
||||
- Pointcut:切入点,advice通常运行在满足pointcut的join point上,pointcut表达式与join point相关联,Spring中默认使用AspectJ切入点表达式
|
||||
- Introduction:在类中声明新的方法、域变量甚至是接口实现
|
||||
- linking:将应用类型或对象和切面链接起来
|
||||
- ## Spring AOP的类型
|
||||
- before:在连接点之前运行,但是无法阻止后续连接点的执行
|
||||
- after returning:在连接点正常返回之后进行
|
||||
- after throwing:在链接点抛出异常正常退出之后进行
|
||||
- after finally:上两种的结合,不管连接点是正常退出还是抛出异常退出,都会在其之后执行
|
||||
- around:around可以自定义连接点之前和之后的执行内容,其也能够选择时候执行连接点的方法
|
||||
- ## Spring AOP的特点
|
||||
- 区别于AspectJ AOP框架,Spring AOP框架是基于代理来实现的
|
||||
- 对于实现了接口的类,Spring AOP通常是通过JDK动态代理来实现的,对于没有实现接口的类,Spring AOP是通过cglib来实现的
|
||||
- 可以强制Spring AOP使用cglib,在如下场景:
|
||||
- 如果想要advise类中方法,而该方法没有在接口中定义
|
||||
- 如果想要将代理对象传递给一个具有特定类型的方法作为参数
|
||||
- ## Spring AOP的AspectJ注解支持
|
||||
- Spring AOP支持AspectJ注解,Spring AOP可以解释和AspectJ 5相同的注解,通过使用AspectJ提供的包来进行切入点解析和匹配
|
||||
- 但是,即使使用了AspectJ注解,AOP在运行时仍然是纯粹的Spring AOP,项目不需要引入AspectJ的编译器和weaver
|
||||
- Spring AOP对AspectJ注解支持的开启
|
||||
- 通过@EnableAspectJAutoProxy注解,会自动的为满足切入点匹配的连接点bean对象创建移动代理对象
|
||||
```java
|
||||
@Configuration
|
||||
@EnableAspectJAutoProxy
|
||||
class AspectJConfiguration {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
- ## 声明Spring AOP切面
|
||||
- 在容器中,任何bean对象,如其类型具有@AspectJ注解,将会被自动探知到并且用来配置spring aop
|
||||
- 在Spring AOP中,aspect其自身是无法作为其他aspect的目标对象的。被标记为@Aspect的类,不仅标明其为aspect,并且将其从自动代理中排除
|
||||
- 如果为某个bean对象配置了切面,那么在后续创建该bean对象时,实际上是创建该bean对象的代理对象
|
||||
```java
|
||||
@Component // 将该类型声明为bean对象
|
||||
@Aspect // 声明切面
|
||||
public class ProxyAspect {
|
||||
|
||||
}
|
||||
```
|
||||
- ## 声明Spring AOP切入点
|
||||
- 由于Spring AOP仅仅支持方法的连接点,故而可以将切入点看做对bean对象方法的匹配
|
||||
- Join Point expression的种类:
|
||||
- execution:匹配目标方法的执行,可以在括号中接收一个函数签名,包含返回类型、函数名和函数参数类型
|
||||
```java
|
||||
// 被@JointPoint注解标注的方法必须具有void的返回类型
|
||||
@JoinPoint("execution(* Point.*(..))")
|
||||
void methodInjected() {
|
||||
|
||||
}
|
||||
```
|
||||
- within:匹配声明在某一特定类中的方法
|
||||
```java
|
||||
@JoinPoint("within(Point)")
|
||||
```
|
||||
- this:匹配生成的代理对象为该类型的一个实例
|
||||
- target:匹配目标对象为该类型的一个实例
|
||||
- args:匹配特定参数
|
||||
- @args:传递参数的类型具有指定的注解
|
||||
- @target:运行时该对象的类具有指定的注解
|
||||
- @within:运行时执行的方法,其方法定义在具有指定注解的类中(可以是继承父类的方法,父类指定了注解
|
||||
- @annotation:执行的方法具有指定注解
|
||||
- Spring AOP同样支持将JoinPoint匹配为具有特定name的Spring bean对象
|
||||
```java
|
||||
@JoinPoint("bean(nameA) || bean(nameB))")
|
||||
```
|
||||
- ## Spring AOP中的Advice
|
||||
- Advice和Pointcut Expresion相关联,主要可以分为before、after、around等种类
|
||||
- Before:
|
||||
```java
|
||||
@Before("execution(* Point.*(..))")
|
||||
public void doSomething() {
|
||||
|
||||
}
|
||||
```
|
||||
- AfterReturning:
|
||||
```java
|
||||
// AfterReturning支持获取切入点执行后返回的值
|
||||
@AfterReturning(
|
||||
pointcut="execution(* Point.*(..))",
|
||||
returning="retVal")
|
||||
public void doSomething(int retVal) {
|
||||
|
||||
}
|
||||
```
|
||||
- AfterThrowing:
|
||||
```java
|
||||
@AfterThrowing(
|
||||
pointcut="execution(* Point.*())",
|
||||
throwing="ex"
|
||||
)
|
||||
public void doSomething(Throwable ex) {
|
||||
|
||||
}
|
||||
```
|
||||
- After:After不管是切入点正常返回还是抛出异常,都会执行,类似于finally
|
||||
```java
|
||||
@After("execution(* Point.*())")
|
||||
public void doSomething() {
|
||||
|
||||
}
|
||||
```
|
||||
- Around:其方法必须会一个Oject类型的返回值,并且方法的第一个参数类型是ProceedingJoinPoint
|
||||
```java
|
||||
@Around("execution(* Point.*())")
|
||||
public Object doSomething(ProceedingJoinPoint pjp) {
|
||||
return isCacheExisted()?returnFromCache():pjp.proceed();
|
||||
}
|
||||
```
|
||||
- ## Spring AOP中Advice方法对JoinPoint的访问
|
||||
- 任何advice方法,都可以声明声明其第一个参数为JoinPoint类型。@Around标注的adivce方法其第一个参数的类型必须为ProceedingJoinPoint类型,该类型为JoinPoint的子类型
|
||||
- JoinPoint接口具有如下方法:
|
||||
- getArgs:返回方法参数
|
||||
- getThis:返回代理对象
|
||||
- getTarget:返回目标对象
|
||||
- getSignature:返回函数的签名
|
||||
- toString:返回该advice方法的描述信息
|
||||
- ## Advice方法通过参数来获取传递给底层方法的参数
|
||||
- 在pointcut表达式的args中,如果用advice方法中的参数名来代替参数类型,那么该类型的参数值会被传递给该参数
|
||||
```java
|
||||
@Before("execution(* Point.*(..) && args(position,..))")
|
||||
public void adviceMethod(Position position) {
|
||||
|
||||
}
|
||||
```
|
||||
- 或者,可以通过如下方式,先通过一个Pointcut获取参数,在在另一个方法中获取named pointcut已获取的参数
|
||||
```java
|
||||
// 此时,adviceMethodTwo同样能够获取Position参数
|
||||
@Pointcut("execution(* Point.*(..)) && args(position,..)")
|
||||
public void adviceMethodOne(Position position) {
|
||||
|
||||
}
|
||||
|
||||
@Before("adviceMethodOne(position)")
|
||||
public void adviceMethodTwo(Position position) {
|
||||
|
||||
}
|
||||
```
|
||||
- Spring AOP可以通过如下方式来约束泛型的参数
|
||||
```java
|
||||
@Before("execution(* GenericsInterface+.method(*) && args(param))")
|
||||
public void adviceMethod(DesiredType param) {
|
||||
|
||||
}
|
||||
```
|
||||
- ## 通过Spring AOP对参数进行预处理
|
||||
```java
|
||||
@Around("execution(* Point.area(*) && args(width,height))")
|
||||
public double caculateInCM(ProceedingJoinPoint jp,double width,double height) {
|
||||
width*=100;
|
||||
height*=100;
|
||||
return jp.proceed(width,height);
|
||||
}
|
||||
```
|
||||
- ## Spring AOP中多个advice对应到同一个Pointcut
|
||||
- 如果多个advice都具有相同的pointcut,那么多个advice之间的执行顺序是未定义的。可以为Aspect类实现Ordered接口,或者添加@Order标记来定义该advice的执行优先级,那么具有具有较小order值的方法将会优先被执行
|
||||
- ## Spring AOP Introduction
|
||||
- 在Spring AOP中,可以通过Introduction来声明一个对象继承了某接口,并且为被代理的对象提供被继承接口的实现
|
||||
- 可以通过@DeclareParent注解为指定对象添加接口并且指明该接口默认的实现类,完成后可以直接将生成的代理对象复制给接口变量
|
||||
```java
|
||||
@Aspect
|
||||
public class MyAspect {
|
||||
@DeclareParent(value="cc.rikakonatsumi.interfaces.*+",defaultImpl=DefaultImpl.class)
|
||||
private static MyInterface myInterface;
|
||||
|
||||
// 之后,可以直接通过this(ref)在pointcut表达式中获取服务对象,也可以通过getBean方法获取容器中的对象
|
||||
}
|
||||
```
|
||||
- ## @RestControllerAdvice的使用
|
||||
- @RestControllerAdvice是@Componnent注解的一个特例,@RestControllerAdivce注解的组成包含@Component
|
||||
- @RestControllerAdivce组合了@ControllerAdvice和@ResponseBody两个注解
|
||||
- 通常,@RestControllerAdvice用作为spring mvc的所有方法做ExceptionHandler
|
||||
|
||||
|
||||
# Spring AOP
|
||||
- ## Spring AOP的核心概念
|
||||
- Aspect:切面,一个模块化的考虑
|
||||
- Joint Point:连接点,程序执行时的一个时间点,通常是方法的执行
|
||||
- Advice:当切面在一个切入点执行多做时,执行的动作被称之为Advice,Advice有不同的类型:before、after、around
|
||||
- Pointcut:切入点,advice通常运行在满足pointcut的join point上,pointcut表达式与join point相关联,Spring中默认使用AspectJ切入点表达式
|
||||
- Introduction:在类中声明新的方法、域变量甚至是接口实现
|
||||
- linking:将应用类型或对象和切面链接起来
|
||||
- ## Spring AOP的类型
|
||||
- before:在连接点之前运行,但是无法阻止后续连接点的执行
|
||||
- after returning:在连接点正常返回之后进行
|
||||
- after throwing:在链接点抛出异常正常退出之后进行
|
||||
- after finally:上两种的结合,不管连接点是正常退出还是抛出异常退出,都会在其之后执行
|
||||
- around:around可以自定义连接点之前和之后的执行内容,其也能够选择时候执行连接点的方法
|
||||
- ## Spring AOP的特点
|
||||
- 区别于AspectJ AOP框架,Spring AOP框架是基于代理来实现的
|
||||
- 对于实现了接口的类,Spring AOP通常是通过JDK动态代理来实现的,对于没有实现接口的类,Spring AOP是通过cglib来实现的
|
||||
- 可以强制Spring AOP使用cglib,在如下场景:
|
||||
- 如果想要advise类中方法,而该方法没有在接口中定义
|
||||
- 如果想要将代理对象传递给一个具有特定类型的方法作为参数
|
||||
- ## Spring AOP的AspectJ注解支持
|
||||
- Spring AOP支持AspectJ注解,Spring AOP可以解释和AspectJ 5相同的注解,通过使用AspectJ提供的包来进行切入点解析和匹配
|
||||
- 但是,即使使用了AspectJ注解,AOP在运行时仍然是纯粹的Spring AOP,项目不需要引入AspectJ的编译器和weaver
|
||||
- Spring AOP对AspectJ注解支持的开启
|
||||
- 通过@EnableAspectJAutoProxy注解,会自动的为满足切入点匹配的连接点bean对象创建移动代理对象
|
||||
```java
|
||||
@Configuration
|
||||
@EnableAspectJAutoProxy
|
||||
class AspectJConfiguration {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
- ## 声明Spring AOP切面
|
||||
- 在容器中,任何bean对象,如其类型具有@AspectJ注解,将会被自动探知到并且用来配置spring aop
|
||||
- 在Spring AOP中,aspect其自身是无法作为其他aspect的目标对象的。被标记为@Aspect的类,不仅标明其为aspect,并且将其从自动代理中排除
|
||||
- 如果为某个bean对象配置了切面,那么在后续创建该bean对象时,实际上是创建该bean对象的代理对象
|
||||
```java
|
||||
@Component // 将该类型声明为bean对象
|
||||
@Aspect // 声明切面
|
||||
public class ProxyAspect {
|
||||
|
||||
}
|
||||
```
|
||||
- ## 声明Spring AOP切入点
|
||||
- 由于Spring AOP仅仅支持方法的连接点,故而可以将切入点看做对bean对象方法的匹配
|
||||
- Join Point expression的种类:
|
||||
- execution:匹配目标方法的执行,可以在括号中接收一个函数签名,包含返回类型、函数名和函数参数类型
|
||||
```java
|
||||
// 被@JointPoint注解标注的方法必须具有void的返回类型
|
||||
@Pointcut("execution(* Point.*(..))")
|
||||
void methodInjected() {
|
||||
|
||||
}
|
||||
```
|
||||
- within:匹配声明在某一特定类中的方法
|
||||
```java
|
||||
@Pointcut("within(Point)")
|
||||
```
|
||||
- this:匹配生成的代理对象为该类型的一个实例
|
||||
- target:匹配目标对象为该类型的一个实例
|
||||
- args:匹配特定参数
|
||||
- @args:传递参数的类型具有指定的注解
|
||||
- @target:运行时该对象的类具有指定的注解
|
||||
- @within:运行时执行的方法,其方法定义在具有指定注解的类中(可以是继承父类的方法,父类指定了注解
|
||||
- @annotation:执行的方法具有指定注解
|
||||
- Spring AOP同样支持将JoinPoint匹配为具有特定name的Spring bean对象
|
||||
```java
|
||||
@Pointcut("bean(nameA) || bean(nameB))")
|
||||
```
|
||||
- ## Spring AOP中的Advice
|
||||
- Advice和Pointcut Expresion相关联,主要可以分为before、after、around等种类
|
||||
- Before:
|
||||
```java
|
||||
@Before("execution(* Point.*(..))")
|
||||
public void doSomething() {
|
||||
|
||||
}
|
||||
```
|
||||
- AfterReturning:
|
||||
```java
|
||||
// AfterReturning支持获取切入点执行后返回的值
|
||||
@AfterReturning(
|
||||
pointcut="execution(* Point.*(..))",
|
||||
returning="retVal")
|
||||
public void doSomething(int retVal) {
|
||||
|
||||
}
|
||||
```
|
||||
- AfterThrowing:
|
||||
```java
|
||||
@AfterThrowing(
|
||||
pointcut="execution(* Point.*())",
|
||||
throwing="ex"
|
||||
)
|
||||
public void doSomething(Throwable ex) {
|
||||
|
||||
}
|
||||
```
|
||||
- After:After不管是切入点正常返回还是抛出异常,都会执行,类似于finally
|
||||
```java
|
||||
@After("execution(* Point.*())")
|
||||
public void doSomething() {
|
||||
|
||||
}
|
||||
```
|
||||
- Around:其方法必须会一个Oject类型的返回值,并且方法的第一个参数类型是ProceedingJoinPoint
|
||||
```java
|
||||
@Around("execution(* Point.*())")
|
||||
public Object doSomething(ProceedingJoinPoint pjp) {
|
||||
return isCacheExisted()?returnFromCache():pjp.proceed();
|
||||
}
|
||||
```
|
||||
- ## Spring AOP中Advice方法对JoinPoint的访问
|
||||
- 任何advice方法,都可以声明声明其第一个参数为JoinPoint类型。@Around标注的adivce方法其第一个参数的类型必须为ProceedingJoinPoint类型,该类型为JoinPoint的子类型
|
||||
- JoinPoint接口具有如下方法:
|
||||
- getArgs:返回方法参数
|
||||
- getThis:返回代理对象
|
||||
- getTarget:返回目标对象
|
||||
- getSignature:返回函数的签名
|
||||
- toString:返回该advice方法的描述信息
|
||||
- ## Advice方法通过参数来获取传递给底层方法的参数
|
||||
- 在pointcut表达式的args中,如果用advice方法中的参数名来代替参数类型,那么该类型的参数值会被传递给该参数
|
||||
```java
|
||||
@Before("execution(* Point.*(..) && args(position,..))")
|
||||
public void adviceMethod(Position position) {
|
||||
|
||||
}
|
||||
```
|
||||
- 或者,可以通过如下方式,先通过一个Pointcut获取参数,在在另一个方法中获取named pointcut已获取的参数
|
||||
```java
|
||||
// 此时,adviceMethodTwo同样能够获取Position参数
|
||||
@Pointcut("execution(* Point.*(..)) && args(position,..)")
|
||||
public void adviceMethodOne(Position position) {
|
||||
|
||||
}
|
||||
|
||||
@Before("adviceMethodOne(position)")
|
||||
public void adviceMethodTwo(Position position) {
|
||||
|
||||
}
|
||||
```
|
||||
- Spring AOP可以通过如下方式来约束泛型的参数
|
||||
```java
|
||||
@Before("execution(* GenericsInterface+.method(*) && args(param))")
|
||||
public void adviceMethod(DesiredType param) {
|
||||
|
||||
}
|
||||
```
|
||||
- ## 通过Spring AOP对参数进行预处理
|
||||
```java
|
||||
@Around("execution(* Point.area(*) && args(width,height))")
|
||||
public double caculateInCM(ProceedingJoinPoint jp,double width,double height) {
|
||||
width*=100;
|
||||
height*=100;
|
||||
return jp.proceed(width,height);
|
||||
}
|
||||
```
|
||||
- ## Spring AOP中多个advice对应到同一个Pointcut
|
||||
- 如果多个advice都具有相同的pointcut,那么多个advice之间的执行顺序是未定义的。可以为Aspect类实现Ordered接口,或者添加@Order标记来定义该advice的执行优先级,那么具有具有较小order值的方法将会优先被执行
|
||||
- ## Spring AOP Introduction
|
||||
- 在Spring AOP中,可以通过Introduction来声明一个对象继承了某接口,并且为被代理的对象提供被继承接口的实现
|
||||
- 可以通过@DeclareParent注解为指定对象添加接口并且指明该接口默认的实现类,完成后可以直接将生成的代理对象复制给接口变量
|
||||
```java
|
||||
@Aspect
|
||||
public class MyAspect {
|
||||
@DeclareParent(value="cc.rikakonatsumi.interfaces.*+",defaultImpl=DefaultImpl.class)
|
||||
private static MyInterface myInterface;
|
||||
|
||||
// 之后,可以直接通过this(ref)在pointcut表达式中获取服务对象,也可以通过getBean方法获取容器中的对象
|
||||
}
|
||||
```
|
||||
- ## @RestControllerAdvice的使用
|
||||
- @RestControllerAdvice是@Componnent注解的一个特例,@RestControllerAdivce注解的组成包含@Component
|
||||
- @RestControllerAdivce组合了@ControllerAdvice和@ResponseBody两个注解
|
||||
- 通常,@RestControllerAdvice用作为spring mvc的所有方法做ExceptionHandler
|
||||
|
||||
|
||||
|
||||
@@ -1,183 +1,183 @@
|
||||
# Spring Core IOC
|
||||
- ## IOC容器和bean简介
|
||||
- IOC简介:
|
||||
- IOC(控制反转)也被称之为依赖注入(DI),对象通过构造函数参数、工厂方法参数、或者在构造后通过setter来设置属性来定义依赖。在对象被创建时,IOC容器会将依赖注入到bean对象中,
|
||||
- IOC容器:
|
||||
- IOC容器接口:
|
||||
- BeanFactory:BeanFactory是一个接口,提供了高级配置功能来管理任何类型的对象
|
||||
- ApplicationContext:ApplicationContext是BeanFactory的一个子接口,在BeanFactory的基础上,其添加了一些更为特殊的特性。
|
||||
- IOC容器职责
|
||||
- IOC容器负责来初始化、配置、组装bean对象
|
||||
- ## 基于注解的Spring容器配置
|
||||
- @Required
|
||||
- @Required应用于bean对象属性的setter方法,表示该属性在配置时必须被填充,通过依赖注入或是用xml定义bean时显式指定值
|
||||
- 该注解当前已经被弃用
|
||||
- @Autowired
|
||||
- 通过在构造函数上标明@Autowired来对方法参数进行注入
|
||||
- 当在构造函数上标记@Autowired时,如果当前类中只有一个构造函数,那么@Autowired注解可以被省略;如果当前类有多个构造函数,那么应该在某个构造函数上指明@Autowired注解
|
||||
```java
|
||||
@Component
|
||||
class Shiromiya {
|
||||
private JdbcTemplate jdbcTemplate;
|
||||
|
||||
@Autowired
|
||||
public Shiromiya(JdbcTemplate jdbcTemplate) {
|
||||
this.jdbcTemplate=jdbcTemplate;
|
||||
}
|
||||
}
|
||||
```
|
||||
- 同样,可以在setter上应用@Component
|
||||
```java
|
||||
@Component
|
||||
class Shiromiya {
|
||||
private JdbcTemplate jdbcTemplate;
|
||||
|
||||
@Autowired
|
||||
public setJdbcTemplate(JdbcTemplate jdbcTemplate) {
|
||||
this.jdbcTemplate=jdbcTemplate;
|
||||
}
|
||||
}
|
||||
```
|
||||
- 将@Autowired应用于字段
|
||||
```java
|
||||
@Component
|
||||
class Shiromiya {
|
||||
@Autowired
|
||||
private JdbcTemplate jdbcTemplate;
|
||||
}
|
||||
```
|
||||
- 通过@Autowired获取相同类型的所有bean对象
|
||||
```java
|
||||
@Component
|
||||
class Shiromiya {
|
||||
private String[] waifus;
|
||||
/*
|
||||
* 这里,同样也支持Collections类型
|
||||
* 例如 List<String>
|
||||
*/
|
||||
|
||||
@Autowired
|
||||
public Shiromiya(String[] waifus) {
|
||||
this.waifus=waifus;
|
||||
}
|
||||
}
|
||||
|
||||
// 同样,可以通过Map类型来获取所有相同类型bean对象的name和value
|
||||
// key:对应bean对象的name
|
||||
// value:对应该bean对象的值
|
||||
@Component
|
||||
class Shiromiya {
|
||||
private Map<String,String> waifus;
|
||||
|
||||
@Autowired
|
||||
public void setWaifus(Map<String,String> waifus) {
|
||||
this.waifus=waifus;
|
||||
}
|
||||
}
|
||||
```
|
||||
- 在@Autowired标记在构造函数上时,即使required为true,在参数为多bean类型时,即使没有匹配的bean,该属性会赋值为{}(空集合)而不是抛出异常
|
||||
- @Autowired作用与构造函数的规则
|
||||
- 当required属性为其默认值true时,在bean类型中只有一个构造函数可以用@Autowired标注
|
||||
- 如果bean类型中有多个构造函数标注了@Autowired注解,那么那么他们都必须将required属性设置为false,并且所有标注了@Autowired属性的构造函数都会被视为依赖注入的候选构造函数
|
||||
- 如果有多个候选的构造函数,那么在IOC容器中可以满足的匹配bean最多的构造函数将会被选中
|
||||
- 如果没有候选函数被选中,那么其会采用默认构造函数,如无默认构造函数,则抛出异常
|
||||
- 如果bean有多个构造函数,并且所有构造函数都没有标明@Autowired注解,那么会采用默认构造函数,如果默认构造函数不存在,抛出异常
|
||||
- 如果bean类型只有一个构造函数,那么该构造函数会被用来进行依赖注入,即使该构造函数没有标注@Autowired注解
|
||||
- 除了使用@Autowired的required属性,还可以使用@Nullable注解来标注可为空
|
||||
```java
|
||||
@Component
|
||||
public class Shiromiya {
|
||||
@Autowired
|
||||
@Nullable
|
||||
private int num;
|
||||
}
|
||||
```
|
||||
- @Primary
|
||||
- @Autowired注解是通过类型注入,如果相同类型存在多个bean时,可以通过@Primary注解来表明一个primary bean
|
||||
```java
|
||||
@Configuration
|
||||
public class BeanConfiguration {
|
||||
@Bean
|
||||
@Primary
|
||||
public String name_1() {
|
||||
return "kazusa";
|
||||
}
|
||||
|
||||
@Bean
|
||||
public String name_2() {
|
||||
return "ogiso";
|
||||
}
|
||||
}
|
||||
/*
|
||||
* 此时,若通过@Autowired注入String类型,“kazusa”将会是默认值
|
||||
*/
|
||||
```
|
||||
- @Qualifier
|
||||
- 可以通过@Qualifier来指定bean的name导入特定bean,并且可以为bean指定默认的qualifier
|
||||
```java
|
||||
@Component
|
||||
public class Shiromiya {
|
||||
@Autowired
|
||||
@Qualifier("name_2")
|
||||
private String n_2;
|
||||
|
||||
private String n_1;
|
||||
|
||||
@Autowired
|
||||
public Shiromiya(@Qualifier("name_1") String n) {
|
||||
this.n_1=n;
|
||||
}
|
||||
}
|
||||
```
|
||||
- bean对象的qualifier并不需要唯一,可以为不同的bean对象赋值相同的qualifier,并且在注入bean集合的时候根据qualifier过滤
|
||||
```java
|
||||
@Configuration
|
||||
@Qualifier("config")
|
||||
class BeanConfiguration {
|
||||
@Bean
|
||||
@Qualifier("name")
|
||||
public String name_1() {
|
||||
return "kazusa";
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Qualifier("name")
|
||||
public String name_2() {
|
||||
return "ogiso";
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Qualifier("not-name")
|
||||
public String not_name_1() {
|
||||
return "fuck";
|
||||
}
|
||||
}
|
||||
|
||||
@Component
|
||||
public class Person {
|
||||
/* 此nameList属性会注入qualifier为name的所有bean
|
||||
* 在此处为"kazusa"和"ogiso"
|
||||
*/
|
||||
@Autowired
|
||||
@Qualifier("name")
|
||||
Map<String,String> nameList;
|
||||
|
||||
@Autowired
|
||||
@Qualifier("config")
|
||||
BeanConfiguration conf;
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Person{" +
|
||||
"nameList=" + nameList +
|
||||
", conf=" + conf +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
```
|
||||
- 作为一种回退机制,当bean的qualifier未被定义时,bean的name属性将会被作为其qualifier,autowired时会根据@Qualifier注解中指定的值匹配具有相同name的bean对象
|
||||
- 若想根据bean的name进行匹配,无需@Qualifier注解,只需要将注入点的name(filed的变量名,标注为@Autowired函数的形参名)和bean的name进行比较,如果相同则匹配成功,否则匹配失败
|
||||
- @Autowired同样支持自身引用的注入,但是自身引用的注入只能作为一种fallback机制。如果当前IOC容器中存在其他的同类型对象,那么其他对象会被优先注入,对象自己并不会参与候选的对象注入。但是,如果IOC中并不存在其他同类型对象,那么自身对象将会被作为引用注入。
|
||||
- @Resource
|
||||
- @Resource标签类似于@Autowired标签,但是@Resource具有一个name属性用来匹配bean对象的name属性
|
||||
# Spring Core IOC
|
||||
- ## IOC容器和bean简介
|
||||
- IOC简介:
|
||||
- IOC(控制反转)也被称之为依赖注入(DI),对象通过构造函数参数、工厂方法参数、或者在构造后通过setter来设置属性来定义依赖。在对象被创建时,IOC容器会将依赖注入到bean对象中,
|
||||
- IOC容器:
|
||||
- IOC容器接口:
|
||||
- BeanFactory:BeanFactory是一个接口,提供了高级配置功能来管理任何类型的对象
|
||||
- ApplicationContext:ApplicationContext是BeanFactory的一个子接口,在BeanFactory的基础上,其添加了一些更为特殊的特性。
|
||||
- IOC容器职责
|
||||
- IOC容器负责来初始化、配置、组装bean对象
|
||||
- ## 基于注解的Spring容器配置
|
||||
- @Required
|
||||
- @Required应用于bean对象属性的setter方法,表示该属性在配置时必须被填充,通过依赖注入或是用xml定义bean时显式指定值
|
||||
- 该注解当前已经被弃用
|
||||
- @Autowired
|
||||
- 通过在构造函数上标明@Autowired来对方法参数进行注入
|
||||
- 当在构造函数上标记@Autowired时,如果当前类中只有一个构造函数,那么@Autowired注解可以被省略;如果当前类有多个构造函数,那么应该在某个构造函数上指明@Autowired注解
|
||||
```java
|
||||
@Component
|
||||
class Shiromiya {
|
||||
private JdbcTemplate jdbcTemplate;
|
||||
|
||||
@Autowired
|
||||
public Shiromiya(JdbcTemplate jdbcTemplate) {
|
||||
this.jdbcTemplate=jdbcTemplate;
|
||||
}
|
||||
}
|
||||
```
|
||||
- 同样,可以在setter上应用@Component
|
||||
```java
|
||||
@Component
|
||||
class Shiromiya {
|
||||
private JdbcTemplate jdbcTemplate;
|
||||
|
||||
@Autowired
|
||||
public setJdbcTemplate(JdbcTemplate jdbcTemplate) {
|
||||
this.jdbcTemplate=jdbcTemplate;
|
||||
}
|
||||
}
|
||||
```
|
||||
- 将@Autowired应用于字段
|
||||
```java
|
||||
@Component
|
||||
class Shiromiya {
|
||||
@Autowired
|
||||
private JdbcTemplate jdbcTemplate;
|
||||
}
|
||||
```
|
||||
- 通过@Autowired获取相同类型的所有bean对象
|
||||
```java
|
||||
@Component
|
||||
class Shiromiya {
|
||||
private String[] waifus;
|
||||
/*
|
||||
* 这里,同样也支持Collections类型
|
||||
* 例如 List<String>
|
||||
*/
|
||||
|
||||
@Autowired
|
||||
public Shiromiya(String[] waifus) {
|
||||
this.waifus=waifus;
|
||||
}
|
||||
}
|
||||
|
||||
// 同样,可以通过Map类型来获取所有相同类型bean对象的name和value
|
||||
// key:对应bean对象的name
|
||||
// value:对应该bean对象的值
|
||||
@Component
|
||||
class Shiromiya {
|
||||
private Map<String,String> waifus;
|
||||
|
||||
@Autowired
|
||||
public void setWaifus(Map<String,String> waifus) {
|
||||
this.waifus=waifus;
|
||||
}
|
||||
}
|
||||
```
|
||||
- 在@Autowired标记在构造函数上时,即使required为true,在参数为多bean类型时,即使没有匹配的bean,该属性会赋值为{}(空集合)而不是抛出异常
|
||||
- @Autowired作用与构造函数的规则
|
||||
- 当required属性为其默认值true时,在bean类型中只有一个构造函数可以用@Autowired标注
|
||||
- 如果bean类型中有多个构造函数标注了@Autowired注解,那么那么他们都必须将required属性设置为false,并且所有标注了@Autowired属性的构造函数都会被视为依赖注入的候选构造函数
|
||||
- 如果有多个候选的构造函数,那么在IOC容器中可以满足的匹配bean最多的构造函数将会被选中
|
||||
- 如果没有候选函数被选中,那么其会采用默认构造函数,如无默认构造函数,则抛出异常
|
||||
- 如果bean有多个构造函数,并且所有构造函数都没有标明@Autowired注解,那么会采用默认构造函数,如果默认构造函数不存在,抛出异常
|
||||
- 如果bean类型只有一个构造函数,那么该构造函数会被用来进行依赖注入,即使该构造函数没有标注@Autowired注解
|
||||
- 除了使用@Autowired的required属性,还可以使用@Nullable注解来标注可为空
|
||||
```java
|
||||
@Component
|
||||
public class Shiromiya {
|
||||
@Autowired
|
||||
@Nullable
|
||||
private int num;
|
||||
}
|
||||
```
|
||||
- @Primary
|
||||
- @Autowired注解是通过类型注入,如果相同类型存在多个bean时,可以通过@Primary注解来表明一个primary bean
|
||||
```java
|
||||
@Configuration
|
||||
public class BeanConfiguration {
|
||||
@Bean
|
||||
@Primary
|
||||
public String name_1() {
|
||||
return "kazusa";
|
||||
}
|
||||
|
||||
@Bean
|
||||
public String name_2() {
|
||||
return "ogiso";
|
||||
}
|
||||
}
|
||||
/*
|
||||
* 此时,若通过@Autowired注入String类型,“kazusa”将会是默认值
|
||||
*/
|
||||
```
|
||||
- @Qualifier
|
||||
- 可以通过@Qualifier来指定bean的name导入特定bean,并且可以为bean指定默认的qualifier
|
||||
```java
|
||||
@Component
|
||||
public class Shiromiya {
|
||||
@Autowired
|
||||
@Qualifier("name_2")
|
||||
private String n_2;
|
||||
|
||||
private String n_1;
|
||||
|
||||
@Autowired
|
||||
public Shiromiya(@Qualifier("name_1") String n) {
|
||||
this.n_1=n;
|
||||
}
|
||||
}
|
||||
```
|
||||
- bean对象的qualifier并不需要唯一,可以为不同的bean对象赋值相同的qualifier,并且在注入bean集合的时候根据qualifier过滤
|
||||
```java
|
||||
@Configuration
|
||||
@Qualifier("config")
|
||||
class BeanConfiguration {
|
||||
@Bean
|
||||
@Qualifier("name")
|
||||
public String name_1() {
|
||||
return "kazusa";
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Qualifier("name")
|
||||
public String name_2() {
|
||||
return "ogiso";
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Qualifier("not-name")
|
||||
public String not_name_1() {
|
||||
return "fuck";
|
||||
}
|
||||
}
|
||||
|
||||
@Component
|
||||
public class Person {
|
||||
/* 此nameList属性会注入qualifier为name的所有bean
|
||||
* 在此处为"kazusa"和"ogiso"
|
||||
*/
|
||||
@Autowired
|
||||
@Qualifier("name")
|
||||
Map<String,String> nameList;
|
||||
|
||||
@Autowired
|
||||
@Qualifier("config")
|
||||
BeanConfiguration conf;
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Person{" +
|
||||
"nameList=" + nameList +
|
||||
", conf=" + conf +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
```
|
||||
- 作为一种回退机制,当bean的qualifier未被定义时,bean的name属性将会被作为其qualifier,autowired时会根据@Qualifier注解中指定的值匹配具有相同name的bean对象
|
||||
- 若想根据bean的name进行匹配,无需@Qualifier注解,只需要将注入点的name(filed的变量名,标注为@Autowired函数的形参名)和bean的name进行比较,如果相同则匹配成功,否则匹配失败
|
||||
- @Autowired同样支持自身引用的注入,但是自身引用的注入只能作为一种fallback机制。如果当前IOC容器中存在其他的同类型对象,那么其他对象会被优先注入,对象自己并不会参与候选的对象注入。但是,如果IOC中并不存在其他同类型对象,那么自身对象将会被作为引用注入。
|
||||
- @Resource
|
||||
- @Resource标签类似于@Autowired标签,但是@Resource具有一个name属性用来匹配bean对象的name属性
|
||||
- @Resource标签首先会对具有相同name的bean对象,如果没有匹配到具有相同name的bean对象,才会fallback到类型匹配
|
||||
@@ -1,71 +1,71 @@
|
||||
# Spring Data Access
|
||||
- ## Spring事务
|
||||
- 本地事务和全局事务:
|
||||
- 全局事务:全局事务允许使用多个事务资源,应用服务器来对全局事务进行管理
|
||||
- 本地事务:本地事务无法管理多个事务资源
|
||||
- 本地事务和全局事务的优缺点
|
||||
- 全局事务的使用需要和服务器环境相绑定,降低了代码的重用性
|
||||
- 本地事务无法使用多个事务资源,无法通过JTA等框架来对多个事务资源进行管理,无法使用分布式事务
|
||||
- ## 声明式事务
|
||||
- Spring中声明式事务是通过AOP来实现的
|
||||
- 在声明式事务中,可以为方法级的粒度指定事务行为
|
||||
- 声明式事务的回滚规则:
|
||||
- 在Spring声明式事务中,可以为事务指定回滚规则,即指定针对哪些异常,事务会自动执行回滚操作
|
||||
- 在默认情况下,只有抛出unchecked异常(通常为RuntimeException)时,声明式事务才会进行回滚
|
||||
- 声明式事务的实现细节:
|
||||
- 声明式事务通过aop代理实现,并且事务的advice是通过xml元数据配置来驱动的
|
||||
- aop和事务元数据联合产生了一个aop代理对象,并且该代理对象通过使用TransactionInterceptor和TransactionManager来实现事务
|
||||
- @Transactional通常和线程绑定的事务一起工作,线程绑定的事务由PlatformTransactionManager管理。@Transactional会将事务暴露给当前执行线程中所有的dao操作
|
||||
- 声明式事务的回滚:
|
||||
- 在Spring事务中推荐让事务回滚的方式是在事务执行的方法中抛出一个异常
|
||||
- Spring事务在默认情况下只会针对unchecked异常(RuntimeException)进行回滚,对于Error,Spring事务也会执行回滚操作
|
||||
- checked异常并不会导致事务的回滚操作,可以注册rollback rule来指定对特定的异常(包括checked异常)进行回滚操作
|
||||
- rollback rule:
|
||||
- 回滚规则(rollback rule)通常用来指定当一个异常被抛出时,是否为该异常执行事务的回滚操作
|
||||
- 在@Transactional注解中,可以指定rollbackFor/noRollbackFor、rollbackForClassName/noRollbackForClassName来指定为那些异常类执行回滚操作
|
||||
> 当指定rollbackFor属性为checked异常时(如rollbackFor=FileNotFoundException.class),此时指定的异常不会覆盖其默认行为(为RuntimeException和Error异常执行回滚操作)。
|
||||
> 故而指定后其默认会为Error、RuntimeException、FileNotFoundException三类异常执行回滚操作
|
||||
|
||||
```java
|
||||
@Transactional(rollbackFor={MyException.class})
|
||||
public void myOperation() {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
- ## 基于注解的声明式事务
|
||||
- @Transactional既可以作用于类上,也可以作用于方法上。当作用于类上时,该声明类中所有的方法都会被声明是事务的,该类子类中所有的方法也都是事务的
|
||||
- @Transactional是可以继承的,被@Inherited元注解修饰。
|
||||
>@Inherited类是元注解,用来修饰注解类。如果一个注解类被@Inherited注解标识,那么在对class查询该注解类时,如果当前class没有声明该注解,将会在当前class的父类中查找该注解,依次递归。直到在父类中找到该注解或是到达继承结构的顶部(Object类)。@Inherited标注的注解仅能在类继承中有效,如注解被标注在接口上,那么将@Inherited标注的注解将不会被递归查询到。
|
||||
- 并且,class级别的@Transactional并不应用在从父类继承的方法上,即若一个类被@Transactional注解标注,并且该类从父类继承方法,那么该类从父类继承的方法并不会被看作是事务的,除非在该类中重新声明继承的方法。
|
||||
- 通过在Configuration类上注解@EnableTransactionManagement,配合@Transactional可以将一个bean对象声明是事务的
|
||||
- 当基于标准spring配置时,应该仅将@Transactional注解标注于public方法,当将@Transactional注解标注于非public方法时无效
|
||||
- 在Spring中,仅推荐将@Transactional注解应用于类上,不推荐将其应用在接口(接口方法)上。如果将其应用在接口上,那么该事务配置仅仅对基于接口的动态代理有效,对基于class的代理无效。
|
||||
- 当类级别和方法级别都设置了@Transactional注解时,方法级别的设置会优先被使用
|
||||
- ## @Transactional注解的配置
|
||||
- 事务的传播: 默认情况下,@Transactional的propagation属性是PROPAGATION_REQUIRED
|
||||
- 事务的隔离级别: 默认情况下,@Transactional的isolation属性是ISOLATION_DEFAULT,使用数据库默认的隔离级别
|
||||
- readOnly: 默认情况下,@Transactional的readOnly属性是false,默认事务是可读写的
|
||||
- timeout: 默认况下下,@Transactional的超时属性取决于底层的事务系统,如果底层事务系统不支持timeout,则timeout属性为none
|
||||
- rollbackFor: 默认情况下,@Transactional会针对unchecked异常和Error进行回滚操作
|
||||
- transactionManager: 默认情况下,@Transactional注解会使用项目中默认的事务管理器(即bean name为transactionManager的事务管理器)。可以为@Transactional注解指定value属性或是transactionManager属性来指定想要采用的事务管理器的bean name或是qualifier
|
||||
- ## Transaction Propagation
|
||||
- ### PROPAGATION.REQUIRED
|
||||
- 在Spring中,事务的传播行为默认是PROPAGATION_REQUIRED,默认情况下该选项会强制的要求一个物理事务
|
||||
- 如果当前作用域中不存在事务,那么会创建一个新的事务
|
||||
- 如果当前作用域的外层作用域已经存在事务,那么会加入到当前作用域的事务中去
|
||||
- 在Spring中,默认情况下,当嵌套事务加入到外层的事务中时,会忽略内层事务定义的隔离级别、timeout设置和读写标志等。
|
||||
> 如果想要对外层事务进行验证,可以手动将事务管理器的validateExistingTransaction属性设置为true。这样,当加入到一个隔离级别与内层事务完全不同的外层事务中时,该加入操作会被拒绝。在该设置下,如果read-write内层事务想要加入到外层的read-only事务中时,该加入操作也会被拒绝。
|
||||
- 在事务传播行为被设置为PROPAGATION_REQUIRED的情况下,会为每个被设置事务的方法创建一个逻辑的事务作用域。各个逻辑事务作用域之间都是相互独立的,在不同逻辑事务作用域之间都可以独立设置事务的rollback-only属性。但是,在PROPAGATION_REQUIRED的情况下,内层事务和外层事务都映射到同一个物理事务,内层事务加入到外层事务中,故而在内层逻辑事务中为物理事务设置rollback-only会切实影响到外层事务的提交。
|
||||
- 当事务传播行为被设置为PROPAGATION_REQUIRED时,如果内层事务设置了rollback-only标记,那么会导致外层物理事务的回滚。当外层事务尝试提交并失败回滚后,会抛出一个UnexceptedRollbackException异常,外层事务commit方法的调用者会接受到该UnexceptedRollbackException,代表内层发生了非预期的回滚操作
|
||||
- ### PROPAGATION.REQUIRES_NEW
|
||||
- 相对于PROPAGATION_REQUIRED,PROPAGATION.REQUIRES_NEW传播行为会一直使用独立的物理事务,而不会尝试区加入外部已经存在的物理事务。
|
||||
- 对于PROPAGATION_NEW,其内层事务和外层事务都可以独立的提交或回滚,内层事务的回滚并不会导致外层事务的回滚。
|
||||
- 将事务传播行为设置为PROPAGATION.REQUIRES_NEW时,内层事务可以独立定义自己的隔离级别、timeout值、read-only属性,而不必继承外部事务的这些属性。在PROPAGATION_REQUIRED中,内部事务自定义这些属性将会被忽略,内部事务加入外部事务后会采用外部事务的设置。
|
||||
- ### PROPAGATION.NESTED
|
||||
- 和PROPAGATION_REQUIRED类似,PROPAGATION_NESTED同样也只有一个物理事务。但是其支持多个savepoint(存档点),该物理事务可以回滚到特定的存档点而非必须回滚整个事务。
|
||||
- 由于PROPAGATION_NESTED对存档点的支持,故而在PROPAGATION_NESTED条件下,可以进行部分回滚。内层事务的回滚操作并不会造成外部事务的回滚,内层事务回滚后外层事务仍然能够继续执行和提交。
|
||||
> 由于PROPAGATION_NESTED需要JDBC savepoint(存档点)的支持,故而该设置仅仅对JDBC事务资源有效。
|
||||
|
||||
> 当事务被回滚之后,当前事务无法再被提交,故而:
|
||||
> 若在子事务中已经回滚(子事务传播行为为required),那么父事务的状态已经被回滚,即使父事务捕获子事务抛出的异常,那么在捕获异常之后执行的sql操作也不会被提交到数据库中,父事务状态处于已回滚,无法再次提交
|
||||
> ***但是,当子事务传播行为为nested时,子事务虽然和父事务共用一个事务,子事务回滚时只会回滚到子事务开启之前的存档点,父事务在捕获子事务抛出异常之后执行的sql语句仍然可以被提交***
|
||||
# Spring Data Access
|
||||
- ## Spring事务
|
||||
- 本地事务和全局事务:
|
||||
- 全局事务:全局事务允许使用多个事务资源,应用服务器来对全局事务进行管理
|
||||
- 本地事务:本地事务无法管理多个事务资源
|
||||
- 本地事务和全局事务的优缺点
|
||||
- 全局事务的使用需要和服务器环境相绑定,降低了代码的重用性
|
||||
- 本地事务无法使用多个事务资源,无法通过JTA等框架来对多个事务资源进行管理,无法使用分布式事务
|
||||
- ## 声明式事务
|
||||
- Spring中声明式事务是通过AOP来实现的
|
||||
- 在声明式事务中,可以为方法级的粒度指定事务行为
|
||||
- 声明式事务的回滚规则:
|
||||
- 在Spring声明式事务中,可以为事务指定回滚规则,即指定针对哪些异常,事务会自动执行回滚操作
|
||||
- 在默认情况下,只有抛出unchecked异常(通常为RuntimeException)时,声明式事务才会进行回滚
|
||||
- 声明式事务的实现细节:
|
||||
- 声明式事务通过aop代理实现,并且事务的advice是通过xml元数据配置来驱动的
|
||||
- aop和事务元数据联合产生了一个aop代理对象,并且该代理对象通过使用TransactionInterceptor和TransactionManager来实现事务
|
||||
- @Transactional通常和线程绑定的事务一起工作,线程绑定的事务由PlatformTransactionManager管理。@Transactional会将事务暴露给当前执行线程中所有的dao操作
|
||||
- 声明式事务的回滚:
|
||||
- 在Spring事务中推荐让事务回滚的方式是在事务执行的方法中抛出一个异常
|
||||
- Spring事务在默认情况下只会针对unchecked异常(RuntimeException)进行回滚,对于Error,Spring事务也会执行回滚操作
|
||||
- checked异常并不会导致事务的回滚操作,可以注册rollback rule来指定对特定的异常(包括checked异常)进行回滚操作
|
||||
- rollback rule:
|
||||
- 回滚规则(rollback rule)通常用来指定当一个异常被抛出时,是否为该异常执行事务的回滚操作
|
||||
- 在@Transactional注解中,可以指定rollbackFor/noRollbackFor、rollbackForClassName/noRollbackForClassName来指定为那些异常类执行回滚操作
|
||||
> 当指定rollbackFor属性为checked异常时(如rollbackFor=FileNotFoundException.class),此时指定的异常不会覆盖其默认行为(为RuntimeException和Error异常执行回滚操作)。
|
||||
> 故而指定后其默认会为Error、RuntimeException、FileNotFoundException三类异常执行回滚操作
|
||||
|
||||
```java
|
||||
@Transactional(rollbackFor={MyException.class})
|
||||
public void myOperation() {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
- ## 基于注解的声明式事务
|
||||
- @Transactional既可以作用于类上,也可以作用于方法上。当作用于类上时,该声明类中所有的方法都会被声明是事务的,该类子类中所有的方法也都是事务的
|
||||
- @Transactional是可以继承的,被@Inherited元注解修饰。
|
||||
>@Inherited类是元注解,用来修饰注解类。如果一个注解类被@Inherited注解标识,那么在对class查询该注解类时,如果当前class没有声明该注解,将会在当前class的父类中查找该注解,依次递归。直到在父类中找到该注解或是到达继承结构的顶部(Object类)。@Inherited标注的注解仅能在类继承中有效,如注解被标注在接口上,那么将@Inherited标注的注解将不会被递归查询到。
|
||||
- 并且,class级别的@Transactional并不应用在从父类继承的方法上,即若一个类被@Transactional注解标注,并且该类从父类继承方法,那么该类从父类继承的方法并不会被看作是事务的,除非在该类中重新声明继承的方法。
|
||||
- 通过在Configuration类上注解@EnableTransactionManagement,配合@Transactional可以将一个bean对象声明是事务的
|
||||
- 当基于标准spring配置时,应该仅将@Transactional注解标注于public方法,当将@Transactional注解标注于非public方法时无效
|
||||
- 在Spring中,仅推荐将@Transactional注解应用于类上,不推荐将其应用在接口(接口方法)上。如果将其应用在接口上,那么该事务配置仅仅对基于接口的动态代理有效,对基于class的代理无效。
|
||||
- 当类级别和方法级别都设置了@Transactional注解时,方法级别的设置会优先被使用
|
||||
- ## @Transactional注解的配置
|
||||
- 事务的传播: 默认情况下,@Transactional的propagation属性是PROPAGATION_REQUIRED
|
||||
- 事务的隔离级别: 默认情况下,@Transactional的isolation属性是ISOLATION_DEFAULT,使用数据库默认的隔离级别
|
||||
- readOnly: 默认情况下,@Transactional的readOnly属性是false,默认事务是可读写的
|
||||
- timeout: 默认况下下,@Transactional的超时属性取决于底层的事务系统,如果底层事务系统不支持timeout,则timeout属性为none
|
||||
- rollbackFor: 默认情况下,@Transactional会针对unchecked异常和Error进行回滚操作
|
||||
- transactionManager: 默认情况下,@Transactional注解会使用项目中默认的事务管理器(即bean name为transactionManager的事务管理器)。可以为@Transactional注解指定value属性或是transactionManager属性来指定想要采用的事务管理器的bean name或是qualifier
|
||||
- ## Transaction Propagation
|
||||
- ### PROPAGATION.REQUIRED
|
||||
- 在Spring中,事务的传播行为默认是PROPAGATION_REQUIRED,默认情况下该选项会强制的要求一个物理事务
|
||||
- 如果当前作用域中不存在事务,那么会创建一个新的事务
|
||||
- 如果当前作用域的外层作用域已经存在事务,那么会加入到当前作用域的事务中去
|
||||
- 在Spring中,默认情况下,当嵌套事务加入到外层的事务中时,会忽略内层事务定义的隔离级别、timeout设置和读写标志等。
|
||||
> 如果想要对外层事务进行验证,可以手动将事务管理器的validateExistingTransaction属性设置为true。这样,当加入到一个隔离级别与内层事务完全不同的外层事务中时,该加入操作会被拒绝。在该设置下,如果read-write内层事务想要加入到外层的read-only事务中时,该加入操作也会被拒绝。
|
||||
- 在事务传播行为被设置为PROPAGATION_REQUIRED的情况下,会为每个被设置事务的方法创建一个逻辑的事务作用域。各个逻辑事务作用域之间都是相互独立的,在不同逻辑事务作用域之间都可以独立设置事务的rollback-only属性。但是,在PROPAGATION_REQUIRED的情况下,内层事务和外层事务都映射到同一个物理事务,内层事务加入到外层事务中,故而在内层逻辑事务中为物理事务设置rollback-only会切实影响到外层事务的提交。
|
||||
- 当事务传播行为被设置为PROPAGATION_REQUIRED时,如果内层事务设置了rollback-only标记,那么会导致外层物理事务的回滚。当外层事务尝试提交并失败回滚后,会抛出一个UnexceptedRollbackException异常,外层事务commit方法的调用者会接受到该UnexceptedRollbackException,代表内层发生了非预期的回滚操作
|
||||
- ### PROPAGATION.REQUIRES_NEW
|
||||
- 相对于PROPAGATION_REQUIRED,PROPAGATION.REQUIRES_NEW传播行为会一直使用独立的物理事务,而不会尝试区加入外部已经存在的物理事务。
|
||||
- 对于PROPAGATION_NEW,其内层事务和外层事务都可以独立的提交或回滚,内层事务的回滚并不会导致外层事务的回滚。
|
||||
- 将事务传播行为设置为PROPAGATION.REQUIRES_NEW时,内层事务可以独立定义自己的隔离级别、timeout值、read-only属性,而不必继承外部事务的这些属性。在PROPAGATION_REQUIRED中,内部事务自定义这些属性将会被忽略,内部事务加入外部事务后会采用外部事务的设置。
|
||||
- ### PROPAGATION.NESTED
|
||||
- 和PROPAGATION_REQUIRED类似,PROPAGATION_NESTED同样也只有一个物理事务。但是其支持多个savepoint(存档点),该物理事务可以回滚到特定的存档点而非必须回滚整个事务。
|
||||
- 由于PROPAGATION_NESTED对存档点的支持,故而在PROPAGATION_NESTED条件下,可以进行部分回滚。内层事务的回滚操作并不会造成外部事务的回滚,内层事务回滚后外层事务仍然能够继续执行和提交。
|
||||
> 由于PROPAGATION_NESTED需要JDBC savepoint(存档点)的支持,故而该设置仅仅对JDBC事务资源有效。
|
||||
|
||||
> 当事务被回滚之后,当前事务无法再被提交,故而:
|
||||
> 若在子事务中已经回滚(子事务传播行为为required),那么父事务的状态已经被回滚,即使父事务捕获子事务抛出的异常,那么在捕获异常之后执行的sql操作也不会被提交到数据库中,父事务状态处于已回滚,无法再次提交
|
||||
> ***但是,当子事务传播行为为nested时,子事务虽然和父事务共用一个事务,子事务回滚时只会回滚到子事务开启之前的存档点,父事务在捕获子事务抛出异常之后执行的sql语句仍然可以被提交***
|
||||
|
||||
53
spring/Spring core/SpringIOC.md
Normal file
53
spring/Spring core/SpringIOC.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# Spring IOC
|
||||
## Bean Scope
|
||||
### 简介
|
||||
当创建一个bean定义时,实际上根据bean定义创建了一个配方,该配方用于创建类型的实际实例。可以基于一个配方创建多个对象实例。
|
||||
|
||||
### singleton
|
||||
对于一个被容器管理的singleton bean来说,只在容器中存在一个共享的实例。所有针对该bean的请求,spring容器都会返回同一个实例。
|
||||
|
||||
当创建一个singleton bean定义时,spring容器会为该bean定义创建一个实例,该单实例会存储在cache中,该cache专门用于存储类似的单实例对象。所有针对单实例对象的请求都会返回该cache中的对象。
|
||||
|
||||
singleton scope并不代表针对特定类型只创建一个实例对象。singleton scope代表在同一个容器中,只会存在一个该类型的bean对象。
|
||||
|
||||
### Prototype Scope
|
||||
对于prototype scope的bean对象,每次请求该bean对象时,都会创建一个新的bean实例。
|
||||
|
||||
故而,应该对拥有状态(stateful)的bean对象使用prototype scope,而对无状态(stateless)的bean而对象使用singleton scope。
|
||||
|
||||
和其他scope bean不同,spring容器并不管理prototype bean的完整生命周期。容器会实例化、配置、组装prototype对象,并将其传递给调用方,之后spring便不再记录prototype bean对象。
|
||||
|
||||
初始化生命周期的回调方法将会针对所有scope的bean对象调用,但是对于prototype bean,销毁生命周期的回调接口并不会被调用。用户在获取prototype bean之后,必须对自己对prototype bean对象的内容进行清除,并且释放bean对象中持有的资源。
|
||||
|
||||
### singleton bean中的prototype bean依赖
|
||||
如果一个singleton bean中含有prototype bean依赖,singleton bean中的依赖是在实例化时解析的。实例化时创建的prototype bean实例是singleton bean所应用的prototype bean的唯一实例。(**代表singleton bean引用的prototype bean有且仅有一个实例**)。
|
||||
|
||||
如果想要在singleton bean运行时不断的获取新的prototype bean实例,不能简单将prototype bean作为依赖注入到singleton bean中,因为依赖注入只能在singleton bean初始化时获取prototype bean一次。可以在运行时通过getBean方法不断获取prototype bean实例,每次获取的实例都是一个新的实例。
|
||||
|
||||
### request, session, application, websocket scopes
|
||||
`request`、`session`、`application`、`websocket` scope都只在web环境下可用,需要使用web-aware的ApplicationContext实现(例如XmlWebApplicationContext)。如果在常规spring ioc容器中使用上述scope,会抛出IllegalStateException 异常。
|
||||
|
||||
### request scope
|
||||
对于request scope,针对每个http请求都会创建一个新的bean实例。request scope bean的作用域范围在http request级别,可以在请求处理的过程中任意修改bean对象的状态,因为在其他请求中也有该request scope bean,一个请求对bean对象的修改对其他请求不可见。
|
||||
|
||||
当http请求完成后,该请求关联的request scope bean将会被丢弃。
|
||||
|
||||
注解`@RequestScope`可以在bean定义上进行标注,代表该bean对象为request scope
|
||||
|
||||
> #### request scope
|
||||
> spring mvc中,请求和线程相绑定,如果在接收到一个请求后,在新的线程中对请求进行处理,那么新的线程将不会绑定旧的http请求。
|
||||
>
|
||||
> 对于request scope,只有在接收请求的线程内,调用getBean方法才能获取到bean实例,若新开一个线程处理请求,那么在新线程内调用getBean方法将会抛出异常
|
||||
|
||||
### Session Scope
|
||||
对于一个http session,spring容器会针对session scope bean创建一个bean实例,该实例在session范围内有效。session范围内可以任意修改bean实例的状态,一个session内对bean的修改对其他session是不可见的。
|
||||
|
||||
当session会话结束时,与该http session关联的bean对象也会被丢弃。
|
||||
|
||||
注解`@SessionScope`可以将bean对象定义为session scope。
|
||||
|
||||
### application scope
|
||||
针对一个web application,spring容器会创建一个bean对象实例。在这种情况下,application scope bean将会范围将会被限定为ServletContext级别,并且bean实例会作为`ServletContext`的属性被存储。
|
||||
|
||||
注解`@ApplicationScope`可以将bean对象的范围限定为`serveltContext`级别。
|
||||
|
||||
@@ -1,5 +1,821 @@
|
||||
# SpringMVC
|
||||
## SpringMVC简介
|
||||
SpringMVC是一款基于Servlet API的web框架,并基于前端控制器的模式被设计。前端控制器有一个中央的控制器(DispatcherServlet),通过一个共享的算法来集中分发请求,请求实际是通过可配置的委托组件(@Controller类)来处理的。
|
||||
> Spring Boot遵循不同的初始化流程。相较于hooking到servlet容器的生命周期中,Spring Boot使用Spring配置来启动其自身和内置servlet容器。filter和servlet声明在在spring配置中被找到并且被注入到容器中。
|
||||
|
||||
- [SpringMVC](#springmvc)
|
||||
- [SpringMVC简介](#springmvc简介)
|
||||
- [DispatcherServlet](#dispatcherservlet)
|
||||
- [Context层次结构](#context层次结构)
|
||||
- [WebApplicationContext的继承关系](#webapplicationcontext的继承关系)
|
||||
- [Spring MVC中特殊的bean类型](#spring-mvc中特殊的bean类型)
|
||||
- [HandlerMapping](#handlermapping)
|
||||
- [HandlerAdapter](#handleradapter)
|
||||
- [HandlerExceptionResolver](#handlerexceptionresolver)
|
||||
- [ViewResolver](#viewresolver)
|
||||
- [Web MVC Config](#web-mvc-config)
|
||||
- [Servlet Config](#servlet-config)
|
||||
- [请求处理过程](#请求处理过程)
|
||||
- [路径匹配](#路径匹配)
|
||||
- [Interception](#interception)
|
||||
- [preHandle](#prehandle)
|
||||
- [postHandle](#posthandle)
|
||||
- [afterCompletion](#aftercompletion)
|
||||
- [Exceptions](#exceptions)
|
||||
- [Resolver Chain](#resolver-chain)
|
||||
- [exception resolver的返回规范](#exception-resolver的返回规范)
|
||||
- [container error page](#container-error-page)
|
||||
- [视图解析](#视图解析)
|
||||
- [处理](#处理)
|
||||
- [重定向](#重定向)
|
||||
- [转发](#转发)
|
||||
- [Controller](#controller)
|
||||
- [AOP代理](#aop代理)
|
||||
- [Request Mapping](#request-mapping)
|
||||
- [URI Pattern](#uri-pattern)
|
||||
- [Pattern Comparasion](#pattern-comparasion)
|
||||
- [消费media-type](#消费media-type)
|
||||
- [产生media-type](#产生media-type)
|
||||
- [Parameters \& Headers](#parameters--headers)
|
||||
- [handler method](#handler-method)
|
||||
- [类型转换](#类型转换)
|
||||
- [Matrix Variable](#matrix-variable)
|
||||
- [@RequestParam](#requestparam)
|
||||
- [@RequestHeader](#requestheader)
|
||||
- [@CookieValue](#cookievalue)
|
||||
- [@ModelAttribute](#modelattribute)
|
||||
- [@SessionAttributes](#sessionattributes)
|
||||
- [@SessionAttribute](#sessionattribute)
|
||||
- [Multipart](#multipart)
|
||||
- [@RequestBody](#requestbody)
|
||||
- [HttpEntity](#httpentity)
|
||||
- [@ResponseBody](#responsebody)
|
||||
- [ResponseEntity](#responseentity)
|
||||
- [Jackson JSON](#jackson-json)
|
||||
- [@JsonView](#jsonview)
|
||||
- [Model](#model)
|
||||
- [@ModelAttribute注解用法](#modelattribute注解用法)
|
||||
- [@ModelAttribute作用于Controller类中普通方法上](#modelattribute作用于controller类中普通方法上)
|
||||
- [@ModelAttribute作用于@RequestMapping方法上](#modelattribute作用于requestmapping方法上)
|
||||
- [DataBinder](#databinder)
|
||||
- [Model Design](#model-design)
|
||||
- [Exception](#exception)
|
||||
- [Controller](#controller-1)
|
||||
- [CORS](#cors)
|
||||
- [@CrossOrigin](#crossorigin)
|
||||
- [spring boot全局配置CORS](#spring-boot全局配置cors)
|
||||
- [Interceptor](#interceptor)
|
||||
|
||||
|
||||
# SpringMVC
|
||||
## SpringMVC简介
|
||||
SpringMVC是一款基于Servlet API的web框架,并基于前端控制器的模式被设计。前端控制器有一个中央的控制器(DispatcherServlet),通过一个共享的算法来集中分发请求,请求实际是通过可配置的委托组件(@Controller类)来处理的。
|
||||
> Spring Boot遵循不同的初始化流程。相较于hooking到servlet容器的生命周期中,Spring Boot使用Spring配置来启动其自身和内置servlet容器。filter和servlet声明在在spring配置中被找到并且被注入到容器中。
|
||||
|
||||
## DispatcherServlet
|
||||
### Context层次结构
|
||||
DispatcherServlet需要一个WebApplicationContext(ApplicationContext的拓展类,Spirng容器)来进行配置。WebApplicationContext拥有一个指向ServletContext和与ServletContext关联Servlet的链接。
|
||||
**同样的,也可以通过ServeltContext来获取关联的ApplicationContext,可以通过RequestContextUtils中的静态方法来获取ServletContext关联的ApplicationContext。**
|
||||
#### WebApplicationContext的继承关系
|
||||
对大多数应用,含有一个WebApplicationContext就足够了。也可以存在一个Root WebApplicationContext在多个DispatcherServlet实例之间共享,同时DispatcherServlet实例也含有自己的WebApplicationContext。
|
||||
通常,被共享的root WebApplicationContext含有repository或service的bean对象,这些bean对象可以被多个DispatcherServlet实例的子WebApplicationContext共享。同时,子WebApplicationContext在继承root WebApplicationContext中bean对象的同时,还能对root WebApplicationContext中的bean对象进行覆盖。
|
||||
> #### WebApplicationContext继承机制
|
||||
> 只有当Servlet私有的子WebApplicationContext中没有找到bean对象时,才会从root WebApplicationContext中查找bean对象,此行为即是对root WebApplicationContext的继承
|
||||
|
||||
### Spring MVC中特殊的bean类型
|
||||
DispatcherServlet将处理请求和渲染返回的工作委托给特定的bean对象。Spring MVC中核心的bean类型如下。
|
||||
#### HandlerMapping
|
||||
将请求映射到handler和一系列用于pre/post处理的拦截器。
|
||||
#### HandlerAdapter
|
||||
HandlerAdapter主要是用于帮助DispatcherServlet调用request请求映射到的handler对象。
|
||||
通常,在调用含有注解的controller时需要对注解进行解析,而HandlerAdapter可以向DispatcherServlet隐藏这些细节,DispatcherServlet不必关心handler是如何被调用的。
|
||||
#### HandlerExceptionResolver
|
||||
解析异常的策略,可能将异常映射到某个handler,或是映射到html error页面。
|
||||
#### ViewResolver
|
||||
将handler返回的基于字符串的view名称映射到一个特定的view,并通过view来渲染返回的响应。
|
||||
|
||||
### Web MVC Config
|
||||
在应用中可以定义上小节中包含的特殊类型bean对象。DispatcherServlet会检查WebApplicationContext中存在的特殊类型bean对象,如果某特殊类型的bean对象在容器中不存在,那么会存在一个回滚机制,使用DispatcherServlet.properties中默认的bean类型来创造bean对象(例如,DispatcherServlet.properties中指定的默认HandlerMapping类型是 org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping和org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping.)。
|
||||
|
||||
### Servlet Config
|
||||
在Servlet环境中,可以选择通过java代码的方式或者web.xml的方式来配置servlet容器。
|
||||
配置Servlet的详细方式,参照Spring MVC文档。
|
||||
|
||||
### 请求处理过程
|
||||
DispatcherServlet按照如下方式来处理Http请求
|
||||
1. 首先,会找到WebApplicationContext并且将其绑定到请求中,此后controller和其他元素在请求处理的过程中可以使用该WebApplicationContext。**默认情况下,WebApplicationContext被绑定到DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE属性中**
|
||||
2. locale resolver被绑定到请求中,在请求处理过程中可以被其他元素使用
|
||||
3. theme resolver被绑定到请求中,在请求处理过程中可以被其他元素使用
|
||||
4. 如果指定了multipart file resolver,请求会被检查是否存在multipart。如果multipart被找到,该请求会被包装在一个MultipartHttpServletRequest中并等待对其进行进一步的处理
|
||||
5. 一个合适的handler将会被找到,并且和该handler相关联的execution chain(pre/post/Controller)会被执行,返回一个model用于渲染。
|
||||
6. 如果model被返回,那么返回的model会被渲染。如果model没有返回,那么没有view会被渲染。
|
||||
|
||||
在WebApplicationContext中声明的HandlerExceptionResolver会用于解析处理请求中抛出的异常。
|
||||
### 路径匹配
|
||||
Servlet API将完整的请求路径暴露为requestURI,并且进一步划分requestURI为如下部分:contextPath,servletPath,pathInfo。
|
||||
> #### contextPath, servletPath, pathInfo区别
|
||||
> - contextPath:contextPath为应用被访问的路径,是整个应用的根路径。默认情况下,SpringBoot的contextPath为"/"。可以通过server.servlet.context-path="/demo"来改变应用的根路径。
|
||||
> - servletPath:servletPath代表main DispatcherServlet的路径。默认情况下,servletPath的值仍为"/"。可以通过spring.mvc.servlet.path来自定义该值。
|
||||
### Interception
|
||||
所有HandlerMapping的实现类都支持handler interceptor,当想要为特定请求执行指定逻辑时会相当有用。拦截器必须要实现HandlerInterceptor,该接口提供了三个方法:
|
||||
- preHandle:在实际handler处理之前
|
||||
- postHandle:在 实际handler处理之后
|
||||
- afterCompletion:在请求完成之后
|
||||
#### preHandle
|
||||
preHandle方法会返回一个boolean值,可以通过指定该方法的返回值来阻止执行链继续执行。当preHandle的返回值是true时,后续执行链会继续执行,当返回值是false时,DispatcherServlet会假设该拦截器本身已经处理了请求,并不会继续执行execution chain中的其他拦截器和实际handler。
|
||||
#### postHandle
|
||||
对于@ResponseBody或返回值为ResponseEntity的方法,postHandle不起作用,这些方法在HandlerAdapter中已经写入且提交了返回响应,时间位于postHandle之前。到postHandle方法执行时,已经无法再对响应做任何修改,如添加header也不再被允许。对于这些场景,可以**实现ResponseBodyAdvice或者声明一个ControllerAdvice**。
|
||||
#### afterCompletion
|
||||
调用时机位于DispaterServlet渲染view之后。
|
||||
### Exceptions
|
||||
如果异常在request mapping过程中被抛出或者从request handler中被抛出,DispatcherServlet会将改异常委托给HandlerExceptionResolver chain来处理,通常是返回一个异常响应。
|
||||
如下是可选的HandlerExceptionResolver实现类:
|
||||
1. SimpleMappingExceptionResolver:一个从exception class name到error view name的映射,用于渲染错误页面
|
||||
2. DefaultHandlerExceptionResolver:解析Spring MVC抛出的异常,并且将它们映射为Http状态码
|
||||
3. ResponseStatusExceptionResolver:通过@ResponseStatus注解来指定异常的状态码,并且将异常映射为Http状态码,状态码的值为@ResponseStatus注解指定的值
|
||||
4. ExceptionHandlerExceptionResolver:通过@Controller类内@ExceptionHandler方法或@ControllerAdvice类来解析异常
|
||||
#### Resolver Chain
|
||||
可以声明多个HandlerExceptionResolver bean对象来声明一个Exception Resolver链,并可以通过指定order属性来指定resolver chain中的顺序,order越大,resolver在链中的顺序越靠后。
|
||||
#### exception resolver的返回规范
|
||||
HandlerExceptionResolver返回值可以按照如下规范返回:
|
||||
- 一个指向ModelAndView的error view
|
||||
- 一个空的ModelAndView,代表异常已经在该Resolver中被处理
|
||||
- null,代表该exception仍未被解析,resolver chain中后续的resolver会继续尝试处理该exception,如果exception在chain的末尾仍然存在,该异常会被冒泡到servlet container中
|
||||
#### container error page
|
||||
如果exception直到resolver chain的最后都没有被解析,或者,response code被设置为错误的状态码(如4xx,5xx),servlet container会渲染一个默认的error html page。
|
||||
### 视图解析
|
||||
#### 处理
|
||||
类似于Exception Resolver,也可以声明一个view resolver chain,并且可以通过设置order属性来设置view resolver在resolver chain中的顺序。
|
||||
view resolver返回null时,代表该view无法被找到。
|
||||
#### 重定向
|
||||
以“redirect:”前缀开头的view name允许执行重定向,redirect:之后指定的是重定向的url。
|
||||
```shell
|
||||
# 重定向实例如下
|
||||
# 1. 基于servlet context重定向
|
||||
redirect:/myapp/some/resource
|
||||
# 2. 基于绝对路径进行重定向
|
||||
redirect:https://myhost.com/some/arbitrary/path
|
||||
```
|
||||
> 如果controller的方法用@ResponseStatus注解标识,该注解值的优先级**高于**RedirectView返回的response status。
|
||||
|
||||
#### 转发
|
||||
以“forward:”前缀开头的view name会被转发。
|
||||
## Controller
|
||||
### AOP代理
|
||||
在某些时候,可能需要AOP代理来装饰Controller,对于Controller AOP,推荐使用基于class的aop。
|
||||
例如想要为@Controller注解的类添加@Transactional注解,此时需要手动指定@Transactional注解的proxyTargetClass=true来启用基于class的动态代理。
|
||||
> 当为@Transactional注解指定了proxyTargetClass=true之后,其不光会将@Transactional的代理变为基于cglib的,还会将整个context中所有的autoproxy bean代理方式都变为基于cglib类代理的
|
||||
### Request Mapping
|
||||
可以通过@RequestMapping来指定请求对应的url、http请求种类、请求参数、header和media type。
|
||||
还可以使用如下注解来映射特定的http method:
|
||||
- @GetMapping
|
||||
- @PostMapping
|
||||
- @DeleteMapping
|
||||
- @PutMapping
|
||||
- @PatchMapping
|
||||
相对于上述的注解,@RequestMapping映射所有的http method。
|
||||
#### URI Pattern
|
||||
Spring MVC支持如下URI Pattern:
|
||||
- /resources/ima?e.png : ?匹配一个字符
|
||||
- /resources/*.png : *匹配0个或多个字符,但是位于同一个path segment内
|
||||
- /resource/** : **可以匹配多个path segment(但是\*\*只能用于末尾)
|
||||
- /projects/{project}/versions : 匹配一个path segment,并且将该path segment捕获到一个变量中,变量可以通过@PathVariable访问
|
||||
- /projects/{project:[a-z]+}/versions : 匹配一个path segment,并且该path segment需要满足指定的regex
|
||||
|
||||
{varName:regex}可以将符合regex的path segment捕获到varName变量中,例如:
|
||||
```java
|
||||
@GetMapping("/{name:[a-z-]+}-{version:\\d\\.\\d\\.\\d}{ext:\\.[a-z]+}")
|
||||
public void handle(@PathVariable String name, @PathVariable String version, @PathVariable String ext) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
#### Pattern Comparasion
|
||||
如果多个pattern都匹配当前URI,那么最佳匹配将会被采用。
|
||||
多个pattern中,更加具体的pattern会被采用。URI的计分方式如下:
|
||||
- 每个URI变量计1分
|
||||
- *符号计1分
|
||||
- **符号计两分
|
||||
- 如果分数相同,那么更长的pattern会被选择
|
||||
- 如果分数和pattern length都相同,那么拥有比wildchar更多的的URI变量的模式会被选择
|
||||
|
||||
分数越高,那么pattern将会是更佳的匹配
|
||||
#### 消费media-type
|
||||
可以在@RequestMapping中指定请求中的Content-Type类型来缩小请求映射范围
|
||||
```java
|
||||
@PostMapping(path = "/pets", consumes = "application/json")
|
||||
public void addPet(@RequestBody Pet pet) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
consumes属性还支持“非”的操作,如下所示:
|
||||
```java
|
||||
// 其映射除text/plain之外的content-type
|
||||
@PostMapping(path = "/pets", consumes = "!text/plain")
|
||||
```
|
||||
#### 产生media-type
|
||||
可以在@RequestMapping中指定produces属性来根据http请求header中的Accept属性来缩小映射范围
|
||||
```java
|
||||
// 该方法会映射Accept属性为application/json的http请求
|
||||
@GetMapping(path = "/pets/{petId}", produces = "application/json")
|
||||
@ResponseBody
|
||||
public Pet getPet(@PathVariable String petId) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
#### Parameters & Headers
|
||||
可以通过请求参数或header来缩小@RequestMapping的映射。
|
||||
支持过滤条件为某参数是否存在、某参数值是否为预期值,header中某值是否存在、header中某值是否等于预期值。
|
||||
```java
|
||||
// 参数是否存在 : myParam
|
||||
// 参数是否不存在 : !myParam
|
||||
// 参数是否为预期值 : myParam=myValue
|
||||
@GetMapping(path = "/pets/{petId}", params = "myParam=myValue")
|
||||
public void findPet(@PathVariable String petId) {
|
||||
// ...
|
||||
}
|
||||
|
||||
// headers校验同样类似于params
|
||||
@GetMapping(path = "/pets", headers = "myHeader=myValue")
|
||||
public void findPet(@PathVariable String petId) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
### handler method
|
||||
#### 类型转换
|
||||
当handler方法的参数为非String类型时,需要进行类型转换。在此种场景下,类型转换是自动执行的。默认情况下,简单类型(int,long,Date,others)是被支持的。
|
||||
可以通过WebDataBinder来进行自定义类型转换。
|
||||
在执行类型转换时,空字符串""在转换为其他类型时可能被转化为null(如转化为long,int,Date).如果允许null被注入给参数,**需要将参数注解的required属性指定为false,或者为参数指定@Nullable属性。**
|
||||
#### Matrix Variable
|
||||
在Matrix Variable中,允许在uri中指定key-value对。path segment中允许包含key-value对,其中变量之间通过分号分隔,值如果存在多个,多个值之间通过逗号分隔。
|
||||
```shell
|
||||
/cars;color=red,green;year=2012
|
||||
```
|
||||
当URL中预期含有Matrix变量时,被映射方法的URI中必须含有一个URI Variable({var})来覆盖该Matrix变量(即通过{var}来捕获URL中的Matrix变量),以此确保URL能被正确的映射。例子如下所示
|
||||
```java
|
||||
// GET /pets/42;q=11;r=22
|
||||
|
||||
@GetMapping("/pets/{petId}")
|
||||
public void findPet(@PathVariable String petId, @MatrixVariable int q) {
|
||||
|
||||
// petId == 42
|
||||
// q == 11
|
||||
}
|
||||
```
|
||||
在所有的path segment中都有可能含有matrix variable,如果在不同path segment中含有相同名称的matrix variable,可以通过如下方式进行区分:
|
||||
```java
|
||||
// GET /owners/42;q=11/pets/21;q=22
|
||||
|
||||
@GetMapping("/owners/{ownerId}/pets/{petId}")
|
||||
public void findPet(
|
||||
@MatrixVariable(name="q", pathVar="ownerId") int q1,
|
||||
@MatrixVariable(name="q", pathVar="petId") int q2) {
|
||||
|
||||
// q1 == 11
|
||||
// q2 == 22
|
||||
}
|
||||
```
|
||||
matrix variable参数也可以被设置为可选的,并且也能为其指派一个默认值:
|
||||
```java
|
||||
// GET /pets/42
|
||||
|
||||
@GetMapping("/pets/{petId}")
|
||||
public void findPet(@MatrixVariable(required=false, defaultValue="1") int q) {
|
||||
|
||||
// q == 1
|
||||
}
|
||||
```
|
||||
如果要获取所有的Matrix Variable并且将其缓存到一个map中,可以通过如下方式
|
||||
```java
|
||||
// GET /owners/42;q=11;r=12/pets/21;q=22;s=23
|
||||
|
||||
@GetMapping("/owners/{ownerId}/pets/{petId}")
|
||||
public void findPet(
|
||||
@MatrixVariable MultiValueMap<String, String> matrixVars,
|
||||
@MatrixVariable(pathVar="petId") MultiValueMap<String, String> petMatrixVars) {
|
||||
|
||||
// matrixVars: ["q" : [11,22], "r" : 12, "s" : 23]
|
||||
// petMatrixVars: ["q" : 22, "s" : 23]
|
||||
}
|
||||
```
|
||||
#### @RequestParam
|
||||
@RequestParam注解用于将http request中的param赋值给handler method的参数,使用方法如下:
|
||||
```java
|
||||
@GetMapping
|
||||
public String setupForm(@RequestParam("petId") int petId, Model model) {
|
||||
Pet pet = this.clinic.loadPet(petId);
|
||||
model.addAttribute("pet", pet);
|
||||
return "petForm";
|
||||
}
|
||||
```
|
||||
当@RequestParam注解的参数类型不是String时,类型转换会被自动的执行。
|
||||
将@RequestParam注解的参数类型指定为list或array时,会将具有相同param name的多个param value解析到其中。
|
||||
可以用@RequestParam来注解一个Map<String,String>或MultiValValue<String,String>类型,不要指定@RequestParam属性,此时,map将会被自动注入。
|
||||
#### @RequestHeader
|
||||
可以通过@RequestHeader注解来将http header值赋值给handler method参数。
|
||||
```shell
|
||||
# http header
|
||||
Host localhost:8080
|
||||
Accept text/html,application/xhtml+xml,application/xml;q=0.9
|
||||
Accept-Language fr,en-gb;q=0.7,en;q=0.3
|
||||
Accept-Encoding gzip,deflate
|
||||
Accept-Charset ISO-8859-1,utf-8;q=0.7,*;q=0.7
|
||||
Keep-Alive 300
|
||||
```
|
||||
可以通过如下方式来获取header中的值:
|
||||
```java
|
||||
@GetMapping("/demo")
|
||||
public void handle(
|
||||
@RequestHeader("Accept-Encoding") String encoding,
|
||||
@RequestHeader("Keep-Alive") long keepAlive) {
|
||||
//...
|
||||
}
|
||||
```
|
||||
同样的,如果被注解参数的类型不是String,类型转换将会被自动执行。
|
||||
同样,也可以用@RequestHeader注解来标注Map<String,String>或MultiValueMap<String,String>或HttpHeaders类型参数,map会自动被header name和header value注入。
|
||||
#### @CookieValue
|
||||
可以通过@CookieValue注解将Http Cookie赋值给handler method参数,如果存在如下Cookie:
|
||||
```cookie
|
||||
JSESSIONID=415A4AC178C59DACE0B2C9CA727CDD84
|
||||
```
|
||||
可以通过如下方式来获取cookie的值
|
||||
```java
|
||||
@GetMapping("/demo")
|
||||
public void handle(@CookieValue("JSESSIONID") String cookie) {
|
||||
//...
|
||||
}
|
||||
```
|
||||
如果注解标注参数的类型不是String,会自动执行类型转换。
|
||||
#### @ModelAttribute
|
||||
可以为handler method的参数添加@ModelAttribute注解,在添加该注解之后,参数属性的值可以从Model中进行获取,如果model中不存在则对该属性进行初始化。
|
||||
**Model属性会被http servlet param中的值覆盖,如果request param name和field name相同。**
|
||||
> @ModelAttribute注解,其name的默认值为方法参数或者返回值类型的首字母小写:
|
||||
> 例如,“orderAddress" for class "mypackage.OrderAddress"
|
||||
|
||||
@ModelAttribute的使用具体可如下所示:
|
||||
```java
|
||||
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
|
||||
public String processSubmit(@ModelAttribute Pet pet) {
|
||||
// method logic...
|
||||
}
|
||||
```
|
||||
参数pet可以通过如下方式进行获取:
|
||||
1. pet参数已经加上了@ModelAttribute属性,故而可以从model中进行获取
|
||||
2. 如果该model attribute已经出现在类级别的@SessionAttribute中,则可以在session中进行获取
|
||||
3. 如果model attribute name和request param name或者path variable name相匹配,那么可以通过converter来进行获取
|
||||
4. 通过默认的构造器进行初始化
|
||||
5. 通过primary constructor进行构造,主构造器参数和servlet request param相匹配
|
||||
|
||||
在使用@ModelAttribute时,可以创建一个Converter<Stirng,T>类型的bean对象用于类型转换,当model attribute name和path variable或者request param name相匹配,且Converter<String,T>对象存在时,会调用该Converter进行类型转换:
|
||||
```java
|
||||
@Component
|
||||
class StringPersonConverter implements Converter<String,Person> {
|
||||
@Override
|
||||
public Person convert(String source) {
|
||||
return new Person("kazusa",true,21);
|
||||
}
|
||||
}
|
||||
|
||||
@RestController
|
||||
public class ModelAttributeController {
|
||||
|
||||
@PostMapping("/hello")
|
||||
public Person sayHello(@ModelAttribute(name="name",binding=true) Person person, Model model) {
|
||||
System.out.println(model);
|
||||
return person;
|
||||
}
|
||||
}
|
||||
```
|
||||
在model attribute被获取之后,会执行data binding。WebDataBinder会将request param name和目标对象的field进行匹配,匹配的field会通过类型转换进行注入。
|
||||
如果想要访问Model Attribute但是不想使用data binding,可以直接在handler method参数中使用Model,或者将@ModelAttribute注解的binding属性设置为false。
|
||||
```java
|
||||
@PostMapping("update")
|
||||
public String update(@Valid AccountForm form, BindingResult result,
|
||||
@ModelAttribute(binding=false) Account account) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
#### @SessionAttributes
|
||||
@SessionAttributes注解用于将model attribute存储在http session中,从而在不同的请求间都可以访问该attribute。
|
||||
@SessionAttributes为class-level的注解,注解的value应该列出model attribute name或model attribute的类型。
|
||||
该注解使用如下所示:
|
||||
```java
|
||||
@Controller
|
||||
@SessionAttributes("pet")
|
||||
public class EditPetForm {
|
||||
|
||||
// ...
|
||||
|
||||
@PostMapping("/pets/{id}")
|
||||
public String handle(Pet pet, BindingResult errors, SessionStatus status) {
|
||||
if (errors.hasErrors) {
|
||||
// ...
|
||||
}
|
||||
status.setComplete();
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
当第一个请求到来时,name为pet的Model Attribute被添加到model中,其会被自动提升并保存到http session中,知道另一个controller method通过SessionStatus方法来清除存储。
|
||||
#### @SessionAttribute
|
||||
如果想要访问已经存在的Session Attribute,可以在handler method的参数上添加@SessionAttribute,如下所示:
|
||||
```java
|
||||
@RequestMapping("/")
|
||||
public String handle(@SessionAttribute User user) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
#### Multipart
|
||||
在MultipartResolver被启用之后,Post请求体中为multipart/form-data格式的数据将被转化并可以作为handler method的参数访问。如下显示了文件上传的用法:
|
||||
```java
|
||||
@Controller
|
||||
public class FileUploadController {
|
||||
|
||||
@PostMapping("/form")
|
||||
public String handleFormUpload(@RequestParam("name") String name,
|
||||
@RequestParam("file") MultipartFile file) {
|
||||
|
||||
if (!file.isEmpty()) {
|
||||
byte[] bytes = file.getBytes();
|
||||
// store the bytes somewhere
|
||||
return "redirect:uploadSuccess";
|
||||
}
|
||||
return "redirect:uploadFailure";
|
||||
}
|
||||
}
|
||||
```
|
||||
> 在上述代码中,可以将MultipartFile改为List\<MultipartFile\>来解析多个文件名
|
||||
> **当@RequestParam没有指定name属性并且参数类型为Map<String,MultipartFile>或MultiValueMap<String,MultipartFile>类型时,会根据name自动将所有匹配的MultipartFile注入到map中**
|
||||
|
||||
#### @RequestBody
|
||||
可以通过@RequestBody注解读取请求体内容,并且通过HttpMessageConverter将内容反序列化为Object。具体使用方法如下:
|
||||
```java
|
||||
@PostMapping("/accounts")
|
||||
public void handle(@RequestBody Account account) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
#### HttpEntity
|
||||
将参数类型指定为HttpEntity与使用@RequestBody注解类似,其作为一个容器包含了请求体转化后的对象(account实例)和请求头(HttpHeaders)。其使用如下:
|
||||
```java
|
||||
@PostMapping("/accounts")
|
||||
public void handle(HttpEntity<Account> entity) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
#### @ResponseBody
|
||||
通过使用@ResponseBody注解,可以将handler method的返回值序列化到响应体中,通过HttpMessageConverter。具体使用如下所示:
|
||||
```java
|
||||
@GetMapping("/accounts/{id}")
|
||||
@ResponseBody
|
||||
public Account handle() {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
同样的,@ResponseBody注解也支持class-level级别的使用,在使用@ResponseBody标注类后对所有controller method都会起作用。
|
||||
通过@RestController可以起到同样作用。
|
||||
#### ResponseEntity
|
||||
ResponseEntity使用和@ResponseBody类似,但是含有status和headers。ResponseEntity使用如下所示:
|
||||
```java
|
||||
@GetMapping("/something")
|
||||
public ResponseEntity<String> handle() {
|
||||
String body = ... ;
|
||||
String etag = ... ;
|
||||
return ResponseEntity.ok().eTag(etag).body(body);
|
||||
}
|
||||
```
|
||||
#### Jackson JSON
|
||||
Spring MVC为Jackson序列化view提供了内置的支持,可以渲染对象中的字段子集。
|
||||
如果想要在序列化返回值时仅仅序列化某些字段,可以通过@JsonView注解来指明序列化哪些字段。
|
||||
##### @JsonView
|
||||
@JsonView的使用如下所示,
|
||||
- 通过interface来指定渲染的视图
|
||||
- 在字段的getter方法上通过@JsonView来标注视图类名
|
||||
- interface支持继承,如WithPasswordView继承WithoutPasswordView,故而WithPasswordView在序列化时不仅会包含其自身的password字段,还会包含从WithoutPasswordView中继承而来的name字段
|
||||
> 对于一个handler method,只能够通过@JsonView指定view class,如果想要激活多个视图,可以使用合成的view class
|
||||
```java
|
||||
@RestController
|
||||
public class UserController {
|
||||
|
||||
@GetMapping("/user")
|
||||
@JsonView(User.WithoutPasswordView.class)
|
||||
public User getUser() {
|
||||
return new User("eric", "7!jd#h23");
|
||||
}
|
||||
}
|
||||
|
||||
public class User {
|
||||
|
||||
public interface WithoutPasswordView {};
|
||||
public interface WithPasswordView extends WithoutPasswordView {};
|
||||
|
||||
private String username;
|
||||
private String password;
|
||||
|
||||
public User() {
|
||||
}
|
||||
|
||||
public User(String username, String password) {
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
@JsonView(WithoutPasswordView.class)
|
||||
public String getUsername() {
|
||||
return this.username;
|
||||
}
|
||||
|
||||
@JsonView(WithPasswordView.class)
|
||||
public String getPassword() {
|
||||
return this.password;
|
||||
}
|
||||
}
|
||||
```
|
||||
### Model
|
||||
#### @ModelAttribute注解用法
|
||||
- 将@ModelAttribute注解标记在handler method的参数上,用于创建或访问一个对象,该对象从model中获取,并且该对象通过WebDataBinder与http request绑定在一起。
|
||||
- 将@ModelAttribute注解绑定在位于@Controller或@ControllerAdvice类中的方法上,用于在任何handler method调用之前初始化model
|
||||
- 将@ModelAttribute注解绑定在@RequestMapping方法上,用于标注该方法的返回值是一个model attribute
|
||||
|
||||
#### @ModelAttribute作用于Controller类中普通方法上
|
||||
对于上述的第二种使用,一个controller类中可以含有任意数量个@ModelAttribute方法,所有的这些方法都会在@RequestMapping方法调用之前被调用。(**同一个@ModelAttribute方法,也可以通过@ControllerAdvice在多个controllers之间进行共享**)。
|
||||
@ModelAttribute方法支持可变的参数形式,其参数形式可以和@RequestMapping方法中的参数形式一样。(**但是,@ModelAttribute方法的参数中不能含有@ModelAttribute注解本身,参数也不能含有和http请求体相关的内容**)。
|
||||
```java
|
||||
@ModelAttribute
|
||||
public void populateModel(@RequestParam String number, Model model) {
|
||||
model.addAttribute(accountRepository.findAccount(number));
|
||||
// add more ...
|
||||
}
|
||||
```
|
||||
也可以通过如下方式向model中添加attribute:
|
||||
```java
|
||||
// 只向model中添加一个属性
|
||||
@ModelAttribute
|
||||
public Account addAccount(@RequestParam String number) {
|
||||
return accountRepository.findAccount(number);
|
||||
}
|
||||
```
|
||||
> 在向model中添加属性时,如果attribute name没有显式指定,那么attribute name将会基于attribute value的类型来决定。可以通过model.addAttribute(attributeName,attributeValue)来指定attribute name,或者通过指定@ModelAttribute的name属性来指定attribute name
|
||||
|
||||
#### @ModelAttribute作用于@RequestMapping方法上
|
||||
对于第三种使用,可以将@ModelAttribute注解标注在@RequestMapping方法之上,在这种情况下方法的返回值将会被解释为model attribute。**在这种情况下@ModelAttribute注解不是必须的,应为该行为是html controller的默认行为,除非返回值是String类型,此时返回值会被解释为view name。**
|
||||
可以通过如下方式来自定义返回model attribute的attribute name,如下图所示:
|
||||
```java
|
||||
// 指定attribute name为“myAccount”
|
||||
@GetMapping("/accounts/{id}")
|
||||
@ModelAttribute("myAccount")
|
||||
public Account handle() {
|
||||
// ...
|
||||
return account;
|
||||
}
|
||||
```
|
||||
### DataBinder
|
||||
对于@Controller和@ControllerAdvice类,在类中可以包含@InitBinder方法,该方法用来初始化WebDataBinder实例:
|
||||
- 将请求参数绑定到model
|
||||
- 将基于String类型的请求值(例如请求参数,pathVariable,headers,cookies或其他)转化为controller method参数的类型
|
||||
- 在html渲染时,将model object value转化为String的形式
|
||||
|
||||
@InitBinder可以针对特定的Controller注册java.beans.PropertyEditor或Spring Converter和Formatter 组件。
|
||||
@InitBinder支持和@RequestMapping方法一样的参数形式,但是参数不能使用@ModelAttribute注解。通常,@InitBinder方法的参数为WebDataBinder,且返回类型为void:
|
||||
```java
|
||||
@Controller
|
||||
public class FormController {
|
||||
// WebDataBinder参数用于注册
|
||||
@InitBinder
|
||||
public void initBinder(WebDataBinder binder) {
|
||||
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
|
||||
dateFormat.setLenient(false);
|
||||
binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, false));
|
||||
}
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
同样,当使用一个共享的FormattingConversionService来设置格式时,可以注册针对特定Controller的Formatter实现,例如:
|
||||
```java
|
||||
@Controller
|
||||
public class FormController {
|
||||
|
||||
@InitBinder
|
||||
protected void initBinder(WebDataBinder binder) {
|
||||
binder.addCustomFormatter(new DateFormatter("yyyy-MM-dd"));
|
||||
}
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
### Model Design
|
||||
在web应用的上下文中,data binding涉及将http请求中的参数绑定到model object及其内层嵌套对象中。
|
||||
默认情况下,所有spring允许绑到model object中所有的公共属性(有public的getter和setter)。
|
||||
通常情况下,会自定义一个特定的model object类,并且该类中的public属性与表单中提交的参数相关联。
|
||||
```java
|
||||
// 只会对如下两个public属性进行data binding
|
||||
public class ChangeEmailForm {
|
||||
|
||||
private String oldEmailAddress;
|
||||
private String newEmailAddress;
|
||||
|
||||
public void setOldEmailAddress(String oldEmailAddress) {
|
||||
this.oldEmailAddress = oldEmailAddress;
|
||||
}
|
||||
|
||||
public String getOldEmailAddress() {
|
||||
return this.oldEmailAddress;
|
||||
}
|
||||
|
||||
public void setNewEmailAddress(String newEmailAddress) {
|
||||
this.newEmailAddress = newEmailAddress;
|
||||
}
|
||||
|
||||
public String getNewEmailAddress() {
|
||||
return this.newEmailAddress;
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
### Exception
|
||||
在@Controller和@ControllerAdvice类中,可以含有@ExceptionHandler方法,该方法用于处理controller方法中抛出的异常,使用如下所示:
|
||||
```java
|
||||
@Controller
|
||||
public class SimpleController {
|
||||
|
||||
// ...
|
||||
|
||||
@ExceptionHandler
|
||||
public ResponseEntity<String> handle(IOException ex) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
该异常参数会匹配被抛出的顶层异常(例如,被直接抛出的IOException),也会匹配被包装的内层cause(例如,被包装在IllegalStateException中的IOException)。**该参数会匹配任一层级的cause exception,并不是只有该异常的直接cause exception才会被处理。**
|
||||
> 只要@ExceptionHandler方法的异常参数类型匹配异常抛出stacktrace中任一层次的异常类型,异常都会被捕获并且处理。
|
||||
```java
|
||||
@PostMapping("/shiro")
|
||||
public String shiro() {
|
||||
throw new RuntimeException(new JarException("fuck"));
|
||||
}
|
||||
|
||||
@ExceptionHandler
|
||||
public String handleJarException(JarException e) {
|
||||
return e.getMessage() + ", Jar";
|
||||
}
|
||||
```
|
||||
> 如果有多个@ExceptionHandler方法匹配抛出的异常链,那么root exception匹配会优先于cause exception匹配。
|
||||
> ExceptionDepthComparator会根据各个异常类型相对于抛出异常类型(root exception)的深度来进行排序。
|
||||
> ```java
|
||||
> // 在调用/shiro接口时,IOException离root exception(RuntimeException)更近,
|
||||
> // 故而会优先调用handleIOException方法
|
||||
> @PostMapping("/shiro")
|
||||
> public String shiro() throws IOException {
|
||||
> throw new RuntimeException(new IOException(new SQLException("fuck")));
|
||||
> }
|
||||
>
|
||||
> @ExceptionHandler
|
||||
> public String handleIOException(IOException e) {
|
||||
> return e.getMessage() + ", IO";
|
||||
> }
|
||||
>
|
||||
> @ExceptionHandler
|
||||
> public String handleSQLException(SQLException e) {
|
||||
> return e.getMessage() + ",Exception";
|
||||
> }
|
||||
> ```
|
||||
|
||||
对于@ExceptionHandler,可以指定该方法处理的异常类型来缩小范围,如下方法都只会匹配root exception为指定异常或异常链中包含指定异常的场景:
|
||||
```java
|
||||
@ExceptionHandler({FileSystemException.class, RemoteException.class})
|
||||
public ResponseEntity<String> handle(IOException ex) {
|
||||
// ...
|
||||
}
|
||||
|
||||
// 甚至,可以将参数的异常类型指定为一个非常宽泛的类型,例如Exception
|
||||
@ExceptionHandler({FileSystemException.class, RemoteException.class})
|
||||
public ResponseEntity<String> handle(Exception ex) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
上述两种写法的区别是:
|
||||
1. 如果参数类型为IOException,那么当cause exception为FileSystemException或RemoteException,且root exception为IOException时,实际的cause exception要通过ex.getCause来获取
|
||||
2. 如果参数类型为Exception,那么当cause exception为FileSystemException或RemoteException且root exception不为指定类型异常时,指定类型异常统一都通过ex.getCause来获取
|
||||
> 通常情况下,@ExceptionHandler方法的参数类型应该尽可能的具体,而且推荐将一个宽泛的@ExceptionHandler拆分成多个独立的@ExceptionHandler方法,每个方法负责处理特定类型的异常。
|
||||
|
||||
**对于@ExceptionHandler方法,在捕获异常之后可以对异常进行rethrow操作,重新抛出之后的异常会在剩余的resolution chain中传播,就好像给定的@ExceptionHandler在开始就没有匹配**
|
||||
|
||||
### Controller
|
||||
对于@ModelAttribute、@InitBinder、@ExceptionHandler注解,若其在@Controller类中使用,那么该类仅对其所在的@Controller类有效。
|
||||
**如果这些注解定义在@ControllerAdvice或@RestControllerAdvice类中,那么它们对所有的@Controller类都有效。**
|
||||
**位于@ControllerAdvice类中的@ExceptionHandler方法,其可以用于处理任何@Controller类中抛出的异常或任何其他exception handler中抛出的异常。**
|
||||
> 对于@ControllerAdvice中的@ExceptionHandler方法,其会在其他所有位于@Controller类中的@ExceptionHandler方法执行完**之后**才会执行;而@InitBinder和@ModelAttribute方法,则是位于@ControllerAdive方法中的**优先于**位于@Controller类中的执行
|
||||
>
|
||||
@ControllerAdive使用如下:
|
||||
```java
|
||||
// Target all Controllers annotated with @RestController
|
||||
@ControllerAdvice(annotations = RestController.class)
|
||||
public class ExampleAdvice1 {}
|
||||
|
||||
// Target all Controllers within specific packages
|
||||
@ControllerAdvice("org.example.controllers")
|
||||
public class ExampleAdvice2 {}
|
||||
|
||||
// Target all Controllers assignable to specific classes
|
||||
@ControllerAdvice(assignableTypes = {ControllerInterface.class, AbstractController.class})
|
||||
public class ExampleAdvice3 {}
|
||||
```
|
||||
|
||||
## CORS
|
||||
Spring MVC允许处理跨域问题。
|
||||
> ### 跨域问题
|
||||
> 处于对安全的考虑,浏览器禁止ajax访问非当前origin的资源。
|
||||
> 对于简单请求,浏览器会直接通过请求和响应中特定header来判断当前资源是否能够被访问;对于非简单请求,则是在请求之前会发送一个预检请求(OPTIONS请求,判断当前资源能否被访问)。
|
||||
|
||||
在Spring MVC中,HandlerMapping实现提供了对CORS的支持。在实际将一个请求映射到handler后,HandlerMapping实现将会检查请求和handler的CORS配置并且做进一步的处理。
|
||||
如果想要启用CORS,需要显式声明CORS配置,如果匹配的CORS配置未被找到,那么预检请求将会被拒绝。(CORS header将不会添加到简单请求和实际CORS请求的响应中,浏览器将会拒绝没有CORS header的响应)。
|
||||
### @CrossOrigin
|
||||
可以对handler method使用@CrossOrigin注解以允许跨域请求,使用示例如下:
|
||||
```java
|
||||
@RestController
|
||||
@RequestMapping("/account")
|
||||
public class AccountController {
|
||||
|
||||
@CrossOrigin
|
||||
@GetMapping("/{id}")
|
||||
public Account retrieve(@PathVariable Long id) {
|
||||
// ...
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public void remove(@PathVariable Long id) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
默认情况下,@CrossOrigin会允许来自所有origin、含有任意header和所有http method。
|
||||
- allowCredentials默认情况下没有启用,除非allowOrigins或allowOriginPatterns被指定了一个非"*"的值。
|
||||
- maxAge默认会设置为30min
|
||||
|
||||
@CrossOrigin注解支持在类上使用,类上注解会被方法继承
|
||||
```java
|
||||
@CrossOrigin(origins = "https://domain2.com", maxAge = 3600)
|
||||
@RestController
|
||||
@RequestMapping("/account")
|
||||
public class AccountController {
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public Account retrieve(@PathVariable Long id) {
|
||||
// ...
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public void remove(@PathVariable Long id) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
@CrossOrigin注解可同时在类和方法上使用
|
||||
```java
|
||||
@CrossOrigin(maxAge = 3600)
|
||||
@RestController
|
||||
@RequestMapping("/account")
|
||||
public class AccountController {
|
||||
|
||||
@CrossOrigin("https://domain2.com")
|
||||
@GetMapping("/{id}")
|
||||
public Account retrieve(@PathVariable Long id) {
|
||||
// ...
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public void remove(@PathVariable Long id) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
### spring boot全局配置CORS
|
||||
在spring boot中,可以通过如下方式全局配置CORS
|
||||
```java
|
||||
@Configuration
|
||||
public class WebConfig implements WebMvcConfigurer {
|
||||
@Override
|
||||
public void addCorsMappings(CorsRegistry registry) {
|
||||
registry.addMapping("/api/**")
|
||||
.allowedOrigins("http://localhost:4200")
|
||||
.allowedMethods("*")
|
||||
.allowedHeaders("*")
|
||||
.allowCredentials(false)
|
||||
.maxAge(3600);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Interceptor
|
||||
通过注册拦截器,可以在请求的请求前、请求后阶段进行处理。
|
||||
|
||||
interceptor可以通过如下方式进行注册:
|
||||
```java
|
||||
@Configuration
|
||||
@EnableWebMvc
|
||||
public class WebConfig implements WebMvcConfigurer {
|
||||
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
registry.addInterceptor(new LocaleChangeInterceptor());
|
||||
registry.addInterceptor(new ThemeChangeInterceptor()).addPathPatterns("/**").excludePathPatterns("/admin/**");
|
||||
}
|
||||
}
|
||||
```
|
||||
其中,如果多次调用addInterceptor添加拦截器,那么拦截器顺序即是添加顺序。
|
||||
|
||||
|
||||
234
spring/caffeine/caffeine.md
Normal file
234
spring/caffeine/caffeine.md
Normal file
@@ -0,0 +1,234 @@
|
||||
- [caffeine](#caffeine)
|
||||
- [Cache](#cache)
|
||||
- [注入](#注入)
|
||||
- [手动](#手动)
|
||||
- [Loading](#loading)
|
||||
- [异步(手动)](#异步手动)
|
||||
- [Async Loading](#async-loading)
|
||||
- [淘汰](#淘汰)
|
||||
- [基于时间的](#基于时间的)
|
||||
- [基于时间的淘汰策略](#基于时间的淘汰策略)
|
||||
- [基于引用的淘汰策略](#基于引用的淘汰策略)
|
||||
- [移除](#移除)
|
||||
- [removal listener](#removal-listener)
|
||||
- [compute](#compute)
|
||||
- [统计](#统计)
|
||||
- [cleanup](#cleanup)
|
||||
- [软引用和弱引用](#软引用和弱引用)
|
||||
- [weakKeys](#weakkeys)
|
||||
- [weakValues](#weakvalues)
|
||||
- [softValues](#softvalues)
|
||||
|
||||
|
||||
# caffeine
|
||||
caffeine是一个高性能的java缓存库,其几乎能够提供最佳的命中率。
|
||||
cache类似于ConcurrentMap,但并不完全相同。在ConcurrentMap中,会维护所有添加到其中的元素,直到元素被显式移除;而Cache则是可以通过配置来自动的淘汰元素,从而限制cache的内存占用。
|
||||
## Cache
|
||||
### 注入
|
||||
Cache提供了如下的注入策略
|
||||
#### 手动
|
||||
```java
|
||||
Cache<Key, Graph> cache = Caffeine.newBuilder()
|
||||
.expireAfterWrite(10, TimeUnit.MINUTES)
|
||||
.maximumSize(10_000)
|
||||
.build();
|
||||
|
||||
// Lookup an entry, or null if not found
|
||||
Graph graph = cache.getIfPresent(key);
|
||||
// Lookup and compute an entry if absent, or null if not computable
|
||||
graph = cache.get(key, k -> createExpensiveGraph(key));
|
||||
// Insert or update an entry
|
||||
cache.put(key, graph);
|
||||
// Remove an entry
|
||||
cache.invalidate(key);
|
||||
```
|
||||
Cache接口可以显示的操作缓存条目的获取、失效、更新。
|
||||
条目可以直接通过cache.put(key,value)来插入到缓存中,该操作会覆盖已存在的key对应的条目。
|
||||
也可以使用cache.get(key,k->value)的形式来对缓存进行插入,该方法会在缓存中查找key对应的条目,如果不存在,会调用k->value来进行计算并将计算后的将计算后的结果插入到缓存中。该操作是原子的。如果该条目不可计算,会返回null,如果计算过程中发生异常,则是会抛出异常。
|
||||
除上述方法外,也可以通过cache.asMap()返回map对象,并且调用ConcurrentMap中的接口来对缓存条目进行修改。
|
||||
#### Loading
|
||||
```java
|
||||
// build方法可以指定一个CacheLoader参数
|
||||
LoadingCache<Key, Graph> cache = Caffeine.newBuilder()
|
||||
.maximumSize(10_000)
|
||||
.expireAfterWrite(10, TimeUnit.MINUTES)
|
||||
.build(key -> createExpensiveGraph(key));
|
||||
|
||||
// Lookup and compute an entry if absent, or null if not computable
|
||||
Graph graph = cache.get(key);
|
||||
// Lookup and compute entries that are absent
|
||||
Map<Key, Graph> graphs = cache.getAll(keys);
|
||||
```
|
||||
LoadingCache和CacheLoader相关联。
|
||||
可以通过getAll方法来执行批量查找,默认情况下,getAll方法会为每个cache中不存在的key向CacheLoader.load发送一个请求。当批量查找比许多单独的查找效率更加高时,可以重写CacheLoader.loadAll方法。
|
||||
#### 异步(手动)
|
||||
```java
|
||||
AsyncCache<Key, Graph> cache = Caffeine.newBuilder()
|
||||
.expireAfterWrite(10, TimeUnit.MINUTES)
|
||||
.maximumSize(10_000)
|
||||
.buildAsync();
|
||||
|
||||
// Lookup an entry, or null if not found
|
||||
CompletableFuture<Graph> graph = cache.getIfPresent(key);
|
||||
// Lookup and asynchronously compute an entry if absent
|
||||
graph = cache.get(key, k -> createExpensiveGraph(key));
|
||||
// Insert or update an entry
|
||||
cache.put(key, graph);
|
||||
// Remove an entry
|
||||
cache.synchronous().invalidate(key);
|
||||
```
|
||||
AsyncCache允许异步的计算条目,并且返回CompletableFuture。
|
||||
AsyncCache可以调用synchronous方法来提供同步的视图。
|
||||
默认情况下executor是ForkJoinPool.commonPool(),可以通过Caffeine.executor(threadPool)来进行覆盖。
|
||||
#### Async Loading
|
||||
```java
|
||||
AsyncLoadingCache<Key, Graph> cache = Caffeine.newBuilder()
|
||||
.maximumSize(10_000)
|
||||
.expireAfterWrite(10, TimeUnit.MINUTES)
|
||||
// Either: Build with a synchronous computation that is wrapped as asynchronous
|
||||
.buildAsync(key -> createExpensiveGraph(key));
|
||||
// Or: Build with a asynchronous computation that returns a future
|
||||
.buildAsync((key, executor) -> createExpensiveGraphAsync(key, executor));
|
||||
|
||||
// Lookup and asynchronously compute an entry if absent
|
||||
CompletableFuture<Graph> graph = cache.get(key);
|
||||
// Lookup and asynchronously compute entries that are absent
|
||||
CompletableFuture<Map<Key, Graph>> graphs = cache.getAll(keys);
|
||||
```
|
||||
AsyncLoadingCache是一个AsyncCache加上一个AsyncCacheLoader。
|
||||
同样地,AsyncCacheLoader支持重写load和loadAll方法。
|
||||
### 淘汰
|
||||
Caffeine提供了三种类型的淘汰:基于size的,基于时间的,基于引用的。
|
||||
#### 基于时间的
|
||||
```java
|
||||
// Evict based on the number of entries in the cache
|
||||
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
|
||||
.maximumSize(10_000)
|
||||
.build(key -> createExpensiveGraph(key));
|
||||
|
||||
// Evict based on the number of vertices in the cache
|
||||
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
|
||||
.maximumWeight(10_000)
|
||||
.weigher((Key key, Graph graph) -> graph.vertices().size())
|
||||
.build(key -> createExpensiveGraph(key));
|
||||
```
|
||||
如果你的缓存不应该超过特定的容量限制,应该使用`Caffeine.maximumSize(long)`方法。该缓存会对不常用的条目进行淘汰。
|
||||
如果每条记录的权重不同,那么可以通过`Caffeine.weigher(Weigher)`指定一个权重计算方法,并且通过`Caffeine.maximumWeight(long)`指定缓存最大的权重值。
|
||||
#### 基于时间的淘汰策略
|
||||
```java
|
||||
// Evict based on a fixed expiration policy
|
||||
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
|
||||
.expireAfterAccess(5, TimeUnit.MINUTES)
|
||||
.build(key -> createExpensiveGraph(key));
|
||||
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
|
||||
.expireAfterWrite(10, TimeUnit.MINUTES)
|
||||
.build(key -> createExpensiveGraph(key));
|
||||
|
||||
// Evict based on a varying expiration policy
|
||||
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
|
||||
.expireAfter(new Expiry<Key, Graph>() {
|
||||
public long expireAfterCreate(Key key, Graph graph, long currentTime) {
|
||||
// Use wall clock time, rather than nanotime, if from an external resource
|
||||
long seconds = graph.creationDate().plusHours(5)
|
||||
.minus(System.currentTimeMillis(), MILLIS)
|
||||
.toEpochSecond();
|
||||
return TimeUnit.SECONDS.toNanos(seconds);
|
||||
}
|
||||
public long expireAfterUpdate(Key key, Graph graph,
|
||||
long currentTime, long currentDuration) {
|
||||
return currentDuration;
|
||||
}
|
||||
public long expireAfterRead(Key key, Graph graph,
|
||||
long currentTime, long currentDuration) {
|
||||
return currentDuration;
|
||||
}
|
||||
})
|
||||
.build(key -> createExpensiveGraph(key));
|
||||
```
|
||||
caffine提供三种方法来进行基于时间的淘汰:
|
||||
- expireAfterAccess(long, TimeUnit):基于上次读写操作过后的时间来进行淘汰
|
||||
- expireAfterWrite(long, TimeUnit):基于创建时间、或上次写操作执行的时间来进行淘汰
|
||||
- expireAfter(Expire):基于自定义的策略来进行淘汰
|
||||
过期在写操作之间周期性的进行触发,偶尔也会在读操作之间进行出发。调度和发送过期事件都是在o(1)时间之内完成的。
|
||||
为了及时过期,而不是通过缓存活动来触发过期,可以通过`Caffeine.scheuler(scheduler)`来指定调度线程
|
||||
#### 基于引用的淘汰策略
|
||||
```java
|
||||
// Evict when neither the key nor value are strongly reachable
|
||||
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
|
||||
.weakKeys()
|
||||
.weakValues()
|
||||
.build(key -> createExpensiveGraph(key));
|
||||
|
||||
// Evict when the garbage collector needs to free memory
|
||||
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
|
||||
.softValues()
|
||||
.build(key -> createExpensiveGraph(key));
|
||||
```
|
||||
caffeine允许设置cache支持垃圾回收,通过使用为key指定weak reference,为value制定soft reference
|
||||
|
||||
### 移除
|
||||
可以通过下述方法显式移除条目:
|
||||
```java
|
||||
// individual key
|
||||
cache.invalidate(key)
|
||||
// bulk keys
|
||||
cache.invalidateAll(keys)
|
||||
// all keys
|
||||
cache.invalidateAll()
|
||||
```
|
||||
#### removal listener
|
||||
```java
|
||||
Cache<Key, Graph> graphs = Caffeine.newBuilder()
|
||||
.evictionListener((Key key, Graph graph, RemovalCause cause) ->
|
||||
System.out.printf("Key %s was evicted (%s)%n", key, cause))
|
||||
.removalListener((Key key, Graph graph, RemovalCause cause) ->
|
||||
System.out.printf("Key %s was removed (%s)%n", key, cause))
|
||||
.build();
|
||||
```
|
||||
在entry被移除时,可以指定listener来执行一系列操作,通过`Caffeine.removalListener(RemovalListener)`。操作是通过Executor异步执行的。
|
||||
当想要在缓存失效之后同步执行操作时,可以使用`Caffeine.evictionListener(RemovalListener)`.该监听器将会在`RemovalCause.wasEvicted()`时被触发
|
||||
### compute
|
||||
通过compute,caffeine可以在entry创建、淘汰、更新时,原子的执行一系列操作:
|
||||
```java
|
||||
Cache<Key, Graph> graphs = Caffeine.newBuilder()
|
||||
.evictionListener((Key key, Graph graph, RemovalCause cause) -> {
|
||||
// atomically intercept the entry's eviction
|
||||
}).build();
|
||||
|
||||
graphs.asMap().compute(key, (k, v) -> {
|
||||
Graph graph = createExpensiveGraph(key);
|
||||
... // update a secondary store
|
||||
return graph;
|
||||
});
|
||||
```
|
||||
### 统计
|
||||
通过`Caffeine.recordStats()`方法,可以启用统计信息的收集,`cache.stats()`方法将返回一个CacheStats对象,提供如下接口:
|
||||
- hitRate():返回请求命中率
|
||||
- evictionCount():cache淘汰次数
|
||||
- averageLoadPenalty():load新值花费的平均时间
|
||||
```java
|
||||
Cache<Key, Graph> graphs = Caffeine.newBuilder()
|
||||
.maximumSize(10_000)
|
||||
.recordStats()
|
||||
.build();
|
||||
```
|
||||
### cleanup
|
||||
默认情况下,Caffine并不会在自动淘汰entry后或entry失效之后立即进行清理,而是在写操作之后执行少量的清理工作,如果写操作很少,则是偶尔在读操作后执行少量读操作。
|
||||
如果你的缓存是高吞吐量的,那么不必担心过期缓存的清理,如果你的缓存读写操作都比较少,那么需要新建一个外部线程来调用`Cache.cleanUp()`来进行缓存清理。
|
||||
```java
|
||||
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
|
||||
.scheduler(Scheduler.systemScheduler())
|
||||
.expireAfterWrite(10, TimeUnit.MINUTES)
|
||||
.build(key -> createExpensiveGraph(key));
|
||||
```
|
||||
scheduler可以用于及时的清理过期缓存
|
||||
|
||||
### 软引用和弱引用
|
||||
caffeine支持基于引用来设置淘汰策略。caffeine支持针对key和value使用弱引用,针对value使用软引用。
|
||||
#### weakKeys
|
||||
`caffeine.weakKeys()`存储使用弱引用的key,如果没有强引用指向key,那么key将会被垃圾回收。垃圾回收时只会比较对象地址,故而整个缓存在比较key时会通过`==`而不是`equals`来进行比较
|
||||
#### weakValues
|
||||
`caffeine.weakValues()`存储使用弱引用的value,如果没有强引用指向value,value会被垃圾回收。同样地,在整个cache中,会使用`==`而不是`equals`来对value进行比较
|
||||
#### softValues
|
||||
软引用的value通常会在垃圾回收时按照lru的方式进行回收,根据内存情况决定是否进行回收。由于使用软引用会带来性能问题,通常更推荐使用基于max-size的回收策略。
|
||||
同样地,基于软引用的value在整个缓存中会通过`==`而不是`equals()`来进行垃圾回收。
|
||||
@@ -1,177 +1,177 @@
|
||||
- [gson](#gson)
|
||||
- [gson简介](#gson简介)
|
||||
- [gson使用](#gson使用)
|
||||
- [Gson库通过Maven引入](#gson库通过maven引入)
|
||||
- [基本类型的序列化和反序列化](#基本类型的序列化和反序列化)
|
||||
- [对象的序列化和反序列化](#对象的序列化和反序列化)
|
||||
- [gson和对象联用的使用规范](#gson和对象联用的使用规范)
|
||||
- [gson和嵌套类的关联使用](#gson和嵌套类的关联使用)
|
||||
- [gson和数组的关联使用](#gson和数组的关联使用)
|
||||
- [gson对java中的集合进行序列化和反序列化](#gson对java中的集合进行序列化和反序列化)
|
||||
- [gson对Map类型的序列化和反序列化](#gson对map类型的序列化和反序列化)
|
||||
- [序列化和反序列化泛型对象](#序列化和反序列化泛型对象)
|
||||
- [序列化和反序列化集合,集合中保存任意类型的对象](#序列化和反序列化集合集合中保存任意类型的对象)
|
||||
|
||||
# gson
|
||||
## gson简介
|
||||
gson是一个java库,通常用来将java对象转化为其json表示的字符串,或者将json格式的字符串转化为其等价的java对象。
|
||||
## gson使用
|
||||
在gson中,使用最频繁的类是Gson。可以通过new Gson()构造函数来创建Gson对象,也可以通过GsonBuilder来创建Gson对象,GsonBuidler在创建Gson对象时能够指定一些设置,如版本控制等。
|
||||
由于Gson对象在执行json操作时并不会保存任何状态,故而Gson对象是线程安全的,单一的Gson对象可以在多线程环境下被重复使用。
|
||||
### Gson库通过Maven引入
|
||||
```xml
|
||||
<dependencies>
|
||||
<!-- Gson: Java to JSON conversion -->
|
||||
<dependency>
|
||||
<groupId>com.google.code.gson</groupId>
|
||||
<artifactId>gson</artifactId>
|
||||
<version>2.9.1</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
```
|
||||
### 基本类型的序列化和反序列化
|
||||
```java
|
||||
// Serialization
|
||||
Gson gson = new Gson();
|
||||
gson.toJson(1); // ==> 1
|
||||
gson.toJson("abcd"); // ==> "abcd"
|
||||
gson.toJson(new Long(10)); // ==> 10
|
||||
int[] values = { 1 };
|
||||
gson.toJson(values); // ==> [1]
|
||||
|
||||
// Deserialization
|
||||
int one = gson.fromJson("1", int.class);
|
||||
Integer one = gson.fromJson("1", Integer.class);
|
||||
Long one = gson.fromJson("1", Long.class);
|
||||
Boolean false = gson.fromJson("false", Boolean.class);
|
||||
String str = gson.fromJson("\"abc\"", String.class);
|
||||
String[] anotherStr = gson.fromJson("[\"abc\"]", String[].class);
|
||||
```
|
||||
### 对象的序列化和反序列化
|
||||
类似于java自带的序列化机制,当成员字段被transient修饰时,并不会序列化该字段
|
||||
```java
|
||||
class BagOfPrimitives {
|
||||
private int value1 = 1;
|
||||
private String value2 = "abc";
|
||||
private transient int value3 = 3;
|
||||
BagOfPrimitives() {
|
||||
// no-args constructor
|
||||
}
|
||||
}
|
||||
|
||||
// Serialization
|
||||
BagOfPrimitives obj = new BagOfPrimitives();
|
||||
Gson gson = new Gson();
|
||||
String json = gson.toJson(obj);
|
||||
|
||||
// ==> json is {"value1":1,"value2":"abc"}
|
||||
|
||||
// Deserialization
|
||||
BagOfPrimitives obj2 = gson.fromJson(json, BagOfPrimitives.class);
|
||||
// ==> obj2 is just like obj
|
||||
```
|
||||
### gson和对象联用的使用规范
|
||||
- gson使用过程中,待序列化或反序列化的成员字段可以是private的,同时也推荐将待序列化或反序列化的成员字段声明为private
|
||||
- 没有必要对成员字段使用注解来特定标明在序列化或者反序列化中包含该字段,默认情况下该对象所有成员字段和该对象父类对象所包含的所有字段都会被序列化
|
||||
- 类似于jdk自带的序列化和反序列化机制,如果一个字段被标明为transient,该字段将不会被包含到序列化和反序列化过程中
|
||||
- gson实现能够正确处理字段为空的情况
|
||||
- 当序列化过程中,为null的字段将会被省略
|
||||
- 当反序列化过程中,如果一个字段在json串中并没有被设置,反序列化得到的对象中该字段将会被设置为默认值:引用类型默认值为null、数字类型默认值为0,boolean类型其默认值为false
|
||||
- 在内部类、匿名类、本地类中关联外部类的字段将会被忽略,并不会包含在序列化和反序列化过程中
|
||||
### gson和嵌套类的关联使用
|
||||
gson可以单独的对静态内部类进行序列化和反序列化,因为静态内部类并不包含对外部类的引用;但是gson无法单独的反序列化内部类,因为在反序列化内部类的过程中,其无参构造器需要一个指向其外部类对象的引用,但是该外部类对象在对内部类进行反序列化时是不可访问的。
|
||||
可以通过将内部类改为静态的内部类,此时对内部类的反序列化将不会需要指向外部类对象的引用。
|
||||
### gson和数组的关联使用
|
||||
gson支持多维数组,并且支持任意的复杂元素类型
|
||||
```java
|
||||
Gson gson = new Gson();
|
||||
int[] ints = {1, 2, 3, 4, 5};
|
||||
String[] strings = {"abc", "def", "ghi"};
|
||||
|
||||
// Serialization
|
||||
gson.toJson(ints); // ==> [1,2,3,4,5]
|
||||
gson.toJson(strings); // ==> ["abc", "def", "ghi"]
|
||||
|
||||
// Deserialization
|
||||
int[] ints2 = gson.fromJson("[1,2,3,4,5]", int[].class);
|
||||
// ==> ints2 will be same as ints
|
||||
```
|
||||
### gson对java中的集合进行序列化和反序列化
|
||||
gson可以序列化任意对象的集合,但是无法对其进行反序列化,因为在反序列化时用户没有任何方法去指定其生成的集合中元素的类型。因而,需要通过typeToken来告知Gson需要反序列化的类型。
|
||||
```java
|
||||
Gson gson = new Gson();
|
||||
Collection<Integer> ints = Arrays.asList(1,2,3,4,5);
|
||||
|
||||
// Serialization
|
||||
String json = gson.toJson(ints); // ==> json is [1,2,3,4,5]
|
||||
|
||||
// Deserialization
|
||||
Type collectionType = new TypeToken<Collection<Integer>>(){}.getType();
|
||||
Collection<Integer> ints2 = gson.fromJson(json, collectionType);
|
||||
// ==> ints2 is same as ints
|
||||
```
|
||||
### gson对Map类型的序列化和反序列化
|
||||
默认情况下,gson会将java中任意的Map实现类型序列化为JSON对象。由于JSON对象其key只支持字符串类型,gson会将待序列化的Map key调用toString转化为字符串。如果map中的key为null,则序列化后的key为"null"
|
||||
```java
|
||||
/**
|
||||
* gson对map进行序列化
|
||||
**/
|
||||
Gson gson = new Gson();
|
||||
Map<String, String> stringMap = new LinkedHashMap<>();
|
||||
stringMap.put("key", "value");
|
||||
stringMap.put(null, "null-entry");
|
||||
|
||||
// Serialization
|
||||
String json = gson.toJson(stringMap); // ==> json is {"key":"value","null":"null-entry"}
|
||||
|
||||
Map<Integer, Integer> intMap = new LinkedHashMap<>();
|
||||
intMap.put(2, 4);
|
||||
intMap.put(3, 6);
|
||||
|
||||
// Serialization
|
||||
String json = gson.toJson(intMap); // ==> json is {"2":4,"3":6}
|
||||
```
|
||||
在反序列化的过程中,gson会使用为Map key类型注册的TypeAdapter的read方法来进行反序列化。为了让gson知道反序列化得到的Map对象的key和value类型,需要使用TypeToken。
|
||||
```java
|
||||
Gson gson = new Gson();
|
||||
Type mapType = new TypeToken<Map<String, String>>(){}.getType();
|
||||
String json = "{\"key\": \"value\"}";
|
||||
|
||||
// Deserialization
|
||||
Map<String, String> stringMap = gson.fromJson(json, mapType);
|
||||
// ==> stringMap is {key=value}
|
||||
```
|
||||
默认情况下,gson序列化map时,复杂类型的key会调用toString方法来将其转化成字符串。但是,gson同样支持开启复杂类型key的序列化操作。通过Gson.enableComplexMapKeySerialization()方法来开启,Gson会调用为Map的key类型注册的TypeAdapter的write方法来序列化key,而不是通过toString方法将key转化为字符串。
|
||||
> 当Map中任意一条Entry的key通过Adapter被序列化为了JSON数组或者对象,那么Gson会将整个Map序列化为Json数组,数组元素为map中entry的键值对。如果map中所有的entry key都不会被序列化为json object或json array,那么该map将会被序列化为json对象
|
||||
|
||||
> 在对枚举型key进行反序列化的过程中,如果enum找不到一个具有匹配name()值的常量时,其会采用一个回退机制,根据枚举常量的toString()值来进行反序列化的匹配。
|
||||
|
||||
### 序列化和反序列化泛型对象
|
||||
当gson对object对象调用toJson方法时,gson会调用object.getClass()来获取需要序列化的字段信息。类似的,在gson调用fromJson时,会向fromJson方法传递一个MyClass.class对象。该方法在序列化和反序列化类型是非泛型类型时能够正常运行。
|
||||
但是,当待序列化和反序列化的类型是泛型类型时,在序列化和反序列化对象时泛型类型信息会丢失,因为泛型采用的是类型擦除。
|
||||
```java
|
||||
class Foo<T> {
|
||||
T value;
|
||||
}
|
||||
Gson gson = new Gson();
|
||||
Foo<Bar> foo = new Foo<Bar>();
|
||||
gson.toJson(foo); // May not serialize foo.value correctly
|
||||
|
||||
gson.fromJson(json, foo.getClass()); // Fails to deserialize foo.value as Bar
|
||||
```
|
||||
> 上述代码中,foo.getClass()方法返回的只是Foo.class对象,并不会包含泛型类型Bar的信息,故而在反序列化时gson并不知道应该将value反序列化为Bar类型
|
||||
|
||||
可以通过向fromJson中传入Type参数来详细指定想要将json串转化成的泛型类型信息
|
||||
```java
|
||||
Type fooType = new TypeToken<Foo<Bar>>() {}.getType();
|
||||
gson.toJson(foo, fooType);
|
||||
|
||||
gson.fromJson(json, fooType);
|
||||
```
|
||||
### 序列化和反序列化集合,集合中保存任意类型的对象
|
||||
当JSON串中数组包含各种类型元素时,将其转化为包含任意类型的java集合,可以有如下方法:
|
||||
- 使用Gson Parser API(JsonParser,底层parser api)将json串中数组转化为JsonArray,并且为每个元素调用Gson.fromJson。该方法是推荐的方法
|
||||
> gson.fromJson可以针对String、Reader、JsonElement来调用
|
||||
|
||||
|
||||
- [gson](#gson)
|
||||
- [gson简介](#gson简介)
|
||||
- [gson使用](#gson使用)
|
||||
- [Gson库通过Maven引入](#gson库通过maven引入)
|
||||
- [基本类型的序列化和反序列化](#基本类型的序列化和反序列化)
|
||||
- [对象的序列化和反序列化](#对象的序列化和反序列化)
|
||||
- [gson和对象联用的使用规范](#gson和对象联用的使用规范)
|
||||
- [gson和嵌套类的关联使用](#gson和嵌套类的关联使用)
|
||||
- [gson和数组的关联使用](#gson和数组的关联使用)
|
||||
- [gson对java中的集合进行序列化和反序列化](#gson对java中的集合进行序列化和反序列化)
|
||||
- [gson对Map类型的序列化和反序列化](#gson对map类型的序列化和反序列化)
|
||||
- [序列化和反序列化泛型对象](#序列化和反序列化泛型对象)
|
||||
- [序列化和反序列化集合,集合中保存任意类型的对象](#序列化和反序列化集合集合中保存任意类型的对象)
|
||||
|
||||
# gson
|
||||
## gson简介
|
||||
gson是一个java库,通常用来将java对象转化为其json表示的字符串,或者将json格式的字符串转化为其等价的java对象。
|
||||
## gson使用
|
||||
在gson中,使用最频繁的类是Gson。可以通过new Gson()构造函数来创建Gson对象,也可以通过GsonBuilder来创建Gson对象,GsonBuidler在创建Gson对象时能够指定一些设置,如版本控制等。
|
||||
由于Gson对象在执行json操作时并不会保存任何状态,故而Gson对象是线程安全的,单一的Gson对象可以在多线程环境下被重复使用。
|
||||
### Gson库通过Maven引入
|
||||
```xml
|
||||
<dependencies>
|
||||
<!-- Gson: Java to JSON conversion -->
|
||||
<dependency>
|
||||
<groupId>com.google.code.gson</groupId>
|
||||
<artifactId>gson</artifactId>
|
||||
<version>2.9.1</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
```
|
||||
### 基本类型的序列化和反序列化
|
||||
```java
|
||||
// Serialization
|
||||
Gson gson = new Gson();
|
||||
gson.toJson(1); // ==> 1
|
||||
gson.toJson("abcd"); // ==> "abcd"
|
||||
gson.toJson(new Long(10)); // ==> 10
|
||||
int[] values = { 1 };
|
||||
gson.toJson(values); // ==> [1]
|
||||
|
||||
// Deserialization
|
||||
int one = gson.fromJson("1", int.class);
|
||||
Integer one = gson.fromJson("1", Integer.class);
|
||||
Long one = gson.fromJson("1", Long.class);
|
||||
Boolean false = gson.fromJson("false", Boolean.class);
|
||||
String str = gson.fromJson("\"abc\"", String.class);
|
||||
String[] anotherStr = gson.fromJson("[\"abc\"]", String[].class);
|
||||
```
|
||||
### 对象的序列化和反序列化
|
||||
类似于java自带的序列化机制,当成员字段被transient修饰时,并不会序列化该字段
|
||||
```java
|
||||
class BagOfPrimitives {
|
||||
private int value1 = 1;
|
||||
private String value2 = "abc";
|
||||
private transient int value3 = 3;
|
||||
BagOfPrimitives() {
|
||||
// no-args constructor
|
||||
}
|
||||
}
|
||||
|
||||
// Serialization
|
||||
BagOfPrimitives obj = new BagOfPrimitives();
|
||||
Gson gson = new Gson();
|
||||
String json = gson.toJson(obj);
|
||||
|
||||
// ==> json is {"value1":1,"value2":"abc"}
|
||||
|
||||
// Deserialization
|
||||
BagOfPrimitives obj2 = gson.fromJson(json, BagOfPrimitives.class);
|
||||
// ==> obj2 is just like obj
|
||||
```
|
||||
### gson和对象联用的使用规范
|
||||
- gson使用过程中,待序列化或反序列化的成员字段可以是private的,同时也推荐将待序列化或反序列化的成员字段声明为private
|
||||
- 没有必要对成员字段使用注解来特定标明在序列化或者反序列化中包含该字段,默认情况下该对象所有成员字段和该对象父类对象所包含的所有字段都会被序列化
|
||||
- 类似于jdk自带的序列化和反序列化机制,如果一个字段被标明为transient,该字段将不会被包含到序列化和反序列化过程中
|
||||
- gson实现能够正确处理字段为空的情况
|
||||
- 当序列化过程中,为null的字段将会被省略
|
||||
- 当反序列化过程中,如果一个字段在json串中并没有被设置,反序列化得到的对象中该字段将会被设置为默认值:引用类型默认值为null、数字类型默认值为0,boolean类型其默认值为false
|
||||
- 在内部类、匿名类、本地类中关联外部类的字段将会被忽略,并不会包含在序列化和反序列化过程中
|
||||
### gson和嵌套类的关联使用
|
||||
gson可以单独的对静态内部类进行序列化和反序列化,因为静态内部类并不包含对外部类的引用;但是gson无法单独的反序列化内部类,因为在反序列化内部类的过程中,其无参构造器需要一个指向其外部类对象的引用,但是该外部类对象在对内部类进行反序列化时是不可访问的。
|
||||
可以通过将内部类改为静态的内部类,此时对内部类的反序列化将不会需要指向外部类对象的引用。
|
||||
### gson和数组的关联使用
|
||||
gson支持多维数组,并且支持任意的复杂元素类型
|
||||
```java
|
||||
Gson gson = new Gson();
|
||||
int[] ints = {1, 2, 3, 4, 5};
|
||||
String[] strings = {"abc", "def", "ghi"};
|
||||
|
||||
// Serialization
|
||||
gson.toJson(ints); // ==> [1,2,3,4,5]
|
||||
gson.toJson(strings); // ==> ["abc", "def", "ghi"]
|
||||
|
||||
// Deserialization
|
||||
int[] ints2 = gson.fromJson("[1,2,3,4,5]", int[].class);
|
||||
// ==> ints2 will be same as ints
|
||||
```
|
||||
### gson对java中的集合进行序列化和反序列化
|
||||
gson可以序列化任意对象的集合,但是无法对其进行反序列化,因为在反序列化时用户没有任何方法去指定其生成的集合中元素的类型。因而,需要通过typeToken来告知Gson需要反序列化的类型。
|
||||
```java
|
||||
Gson gson = new Gson();
|
||||
Collection<Integer> ints = Arrays.asList(1,2,3,4,5);
|
||||
|
||||
// Serialization
|
||||
String json = gson.toJson(ints); // ==> json is [1,2,3,4,5]
|
||||
|
||||
// Deserialization
|
||||
Type collectionType = new TypeToken<Collection<Integer>>(){}.getType();
|
||||
Collection<Integer> ints2 = gson.fromJson(json, collectionType);
|
||||
// ==> ints2 is same as ints
|
||||
```
|
||||
### gson对Map类型的序列化和反序列化
|
||||
默认情况下,gson会将java中任意的Map实现类型序列化为JSON对象。由于JSON对象其key只支持字符串类型,gson会将待序列化的Map key调用toString转化为字符串。如果map中的key为null,则序列化后的key为"null"
|
||||
```java
|
||||
/**
|
||||
* gson对map进行序列化
|
||||
**/
|
||||
Gson gson = new Gson();
|
||||
Map<String, String> stringMap = new LinkedHashMap<>();
|
||||
stringMap.put("key", "value");
|
||||
stringMap.put(null, "null-entry");
|
||||
|
||||
// Serialization
|
||||
String json = gson.toJson(stringMap); // ==> json is {"key":"value","null":"null-entry"}
|
||||
|
||||
Map<Integer, Integer> intMap = new LinkedHashMap<>();
|
||||
intMap.put(2, 4);
|
||||
intMap.put(3, 6);
|
||||
|
||||
// Serialization
|
||||
String json = gson.toJson(intMap); // ==> json is {"2":4,"3":6}
|
||||
```
|
||||
在反序列化的过程中,gson会使用为Map key类型注册的TypeAdapter的read方法来进行反序列化。为了让gson知道反序列化得到的Map对象的key和value类型,需要使用TypeToken。
|
||||
```java
|
||||
Gson gson = new Gson();
|
||||
Type mapType = new TypeToken<Map<String, String>>(){}.getType();
|
||||
String json = "{\"key\": \"value\"}";
|
||||
|
||||
// Deserialization
|
||||
Map<String, String> stringMap = gson.fromJson(json, mapType);
|
||||
// ==> stringMap is {key=value}
|
||||
```
|
||||
默认情况下,gson序列化map时,复杂类型的key会调用toString方法来将其转化成字符串。但是,gson同样支持开启复杂类型key的序列化操作。通过Gson.enableComplexMapKeySerialization()方法来开启,Gson会调用为Map的key类型注册的TypeAdapter的write方法来序列化key,而不是通过toString方法将key转化为字符串。
|
||||
> 当Map中任意一条Entry的key通过Adapter被序列化为了JSON数组或者对象,那么Gson会将整个Map序列化为Json数组,数组元素为map中entry的键值对。如果map中所有的entry key都不会被序列化为json object或json array,那么该map将会被序列化为json对象
|
||||
|
||||
> 在对枚举型key进行反序列化的过程中,如果enum找不到一个具有匹配name()值的常量时,其会采用一个回退机制,根据枚举常量的toString()值来进行反序列化的匹配。
|
||||
|
||||
### 序列化和反序列化泛型对象
|
||||
当gson对object对象调用toJson方法时,gson会调用object.getClass()来获取需要序列化的字段信息。类似的,在gson调用fromJson时,会向fromJson方法传递一个MyClass.class对象。该方法在序列化和反序列化类型是非泛型类型时能够正常运行。
|
||||
但是,当待序列化和反序列化的类型是泛型类型时,在序列化和反序列化对象时泛型类型信息会丢失,因为泛型采用的是类型擦除。
|
||||
```java
|
||||
class Foo<T> {
|
||||
T value;
|
||||
}
|
||||
Gson gson = new Gson();
|
||||
Foo<Bar> foo = new Foo<Bar>();
|
||||
gson.toJson(foo); // May not serialize foo.value correctly
|
||||
|
||||
gson.fromJson(json, foo.getClass()); // Fails to deserialize foo.value as Bar
|
||||
```
|
||||
> 上述代码中,foo.getClass()方法返回的只是Foo.class对象,并不会包含泛型类型Bar的信息,故而在反序列化时gson并不知道应该将value反序列化为Bar类型
|
||||
|
||||
可以通过向fromJson中传入Type参数来详细指定想要将json串转化成的泛型类型信息
|
||||
```java
|
||||
Type fooType = new TypeToken<Foo<Bar>>() {}.getType();
|
||||
gson.toJson(foo, fooType);
|
||||
|
||||
gson.fromJson(json, fooType);
|
||||
```
|
||||
### 序列化和反序列化集合,集合中保存任意类型的对象
|
||||
当JSON串中数组包含各种类型元素时,将其转化为包含任意类型的java集合,可以有如下方法:
|
||||
- 使用Gson Parser API(JsonParser,底层parser api)将json串中数组转化为JsonArray,并且为每个元素调用Gson.fromJson。该方法是推荐的方法
|
||||
> gson.fromJson可以针对String、Reader、JsonElement来调用
|
||||
|
||||
|
||||
|
||||
@@ -1,85 +1,85 @@
|
||||
- [Spring Logging](#spring-logging)
|
||||
- [Log Format](#log-format)
|
||||
- [控制台输出](#控制台输出)
|
||||
- [文件输出](#文件输出)
|
||||
- [File Rotation](#file-rotation)
|
||||
- [Log Level](#log-level)
|
||||
- [Log Group](#log-group)
|
||||
|
||||
# Spring Logging
|
||||
## Log Format
|
||||
默认Spring Boot输出日志的格式如下
|
||||
```console
|
||||
2022-08-18 05:33:51.660 INFO 16378 --- [ main] o.s.b.d.f.s.MyApplication : Starting MyApplication using Java 1.8.0_345 on myhost with PID 16378 (/opt/apps/myapp.jar started by myuser in /opt/apps/)
|
||||
2022-08-18 05:33:51.664 INFO 16378 --- [ main] o.s.b.d.f.s.MyApplication : No active profile set, falling back to 1 default profile: "default"
|
||||
2022-08-18 05:33:53.907 INFO 16378 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http)
|
||||
2022-08-18 05:33:53.939 INFO 16378 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
|
||||
2022-08-18 05:33:53.939 INFO 16378 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.65]
|
||||
2022-08-18 05:33:54.217 INFO 16378 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
|
||||
2022-08-18 05:33:54.217 INFO 16378 --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 2343 ms
|
||||
2022-08-18 05:33:55.396 INFO 16378 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
|
||||
2022-08-18 05:33:55.640 INFO 16378 --- [ main] o.s.b.d.f.s.MyApplication : Started MyApplication in 5.456 seconds (JVM running for 6.299)
|
||||
```
|
||||
- 日期与时间 : 精度为ms
|
||||
- log level : ERROR, WARN, INFO, DEBUG, TRACE
|
||||
- 进程ID
|
||||
- 线程名称 : [main]
|
||||
- logger name : 输出日志类的类名(通常为缩写)
|
||||
- log信息
|
||||
## 控制台输出
|
||||
默认情况下,log日志信息会回显输出到console中,默认ERROR, WARN, DEBUG三个级别的信息将会被日志输出。
|
||||
可以通过--debug选项来启用“debug”模式
|
||||
```shell
|
||||
java -jar xxx.jar --debug
|
||||
```
|
||||
通过在application.properties中指定debug=true也可以开启“debug”模式
|
||||
```properties
|
||||
debug=true
|
||||
```
|
||||
当“debug”模式被开启后,一部分核心的logger(内嵌容器、Hibernate、SpringBoot)将会被配置输出更多的信息。
|
||||
> ***开启debug模式并不意味着输出所有日志级别为Debug的信息***
|
||||
|
||||
> ***同样,也可以通过--trace或者在properties中指定trace=true来开启trace模式***
|
||||
|
||||
## 文件输出
|
||||
默认情况下,Spring Boot只会将日志输出到console中,如果想要额外定义将日志输出到文件中,需要在application.properties中定义logging.file.name或者logging.file.path
|
||||
| logging.file.name | logging.file.path | example | description |
|
||||
| :-: | :-: | :-: | :-: |
|
||||
| (none) | (none) | | 只在控制台输出 |
|
||||
| 特定文件 | (none) | my.log | 特定log文件路径,可以是绝对路径或相对路径) |
|
||||
| (none) | 特定目录 | /var/log | 将日志输出到该路径下的spring.log文件,可以是绝对路径或相对路径|
|
||||
> 当log文件大小到达10MB时将会旋转重写,和console log一样,log文件也会输出ERROR, WARN和INFO
|
||||
|
||||
> ***logging properties和实际的logging机制是相互独立的,因而,特定的配置属性(如logback.configurationFile)并不由SpringBoot管理***
|
||||
|
||||
## File Rotation
|
||||
如果使用的是Logback,则可以在application.properties中定义file rotation行为。
|
||||
| Name | Description |
|
||||
| :-: | :-: |
|
||||
| logging.logback.rollingpolicy.file-name-pattern | 定义创建log归档的命名模式 |
|
||||
| logging.logback.rollingpolicy.clean-history-on-start | 定义是否应该在项目启动时清理历史日志 |
|
||||
| logging.logback.rollingpolicy.max-file-size | 定义日志在归档前的最大大小 |
|
||||
| logging.logback.rollingpolicy.total-size-cap | 日志归档在被删除前可以占用的最大大小 |
|
||||
| logging.logback.rollingpolicy.max-history | 要保留归档日志文件的最大数量 |
|
||||
|
||||
## Log Level
|
||||
所有的日志系统都可以通过Application.properties定义logging.level.<logger-name>=<level>来定义事务级别,事务级别可以是TRACE, DEBUG, INFO, WARN, ERROR, FATAL, OFF。
|
||||
可以通过logging.level.root来定义root logger的隔离级别。
|
||||
```properties
|
||||
logging.level.root=warn
|
||||
logging.level.org.springframework.web=debug
|
||||
logging.level.org.hibernate=error
|
||||
```
|
||||
## Log Group
|
||||
可以通过log group将关联的logger组合在一起,并且对log group统一指定日志级别。
|
||||
```properties
|
||||
# 定义一个名为“tomcat”的log group
|
||||
logging.group.tomcat=org.apache.catalina,org.apache.coyote,org.apache.tomcat
|
||||
|
||||
# 为名为“tomcat”的log group统一指定log level
|
||||
logging.level.tomcat=trace
|
||||
```
|
||||
> Spring Boot具有如下先定义好的log group,可以开箱即用
|
||||
> - web : org.springframework.core.codec, org.springframework.http, org.springframework.web, org.springframework.boot.actuate.endpoint.web, org.springframework.boot.web.servlet.ServletContextInitializerBeans
|
||||
> - org.springframework.jdbc.core, org.hibernate.SQL, org.jooq.tools.LoggerListener
|
||||
|
||||
- [Spring Logging](#spring-logging)
|
||||
- [Log Format](#log-format)
|
||||
- [控制台输出](#控制台输出)
|
||||
- [文件输出](#文件输出)
|
||||
- [File Rotation](#file-rotation)
|
||||
- [Log Level](#log-level)
|
||||
- [Log Group](#log-group)
|
||||
|
||||
# Spring Logging
|
||||
## Log Format
|
||||
默认Spring Boot输出日志的格式如下
|
||||
```console
|
||||
2022-08-18 05:33:51.660 INFO 16378 --- [ main] o.s.b.d.f.s.MyApplication : Starting MyApplication using Java 1.8.0_345 on myhost with PID 16378 (/opt/apps/myapp.jar started by myuser in /opt/apps/)
|
||||
2022-08-18 05:33:51.664 INFO 16378 --- [ main] o.s.b.d.f.s.MyApplication : No active profile set, falling back to 1 default profile: "default"
|
||||
2022-08-18 05:33:53.907 INFO 16378 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http)
|
||||
2022-08-18 05:33:53.939 INFO 16378 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
|
||||
2022-08-18 05:33:53.939 INFO 16378 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.65]
|
||||
2022-08-18 05:33:54.217 INFO 16378 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
|
||||
2022-08-18 05:33:54.217 INFO 16378 --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 2343 ms
|
||||
2022-08-18 05:33:55.396 INFO 16378 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
|
||||
2022-08-18 05:33:55.640 INFO 16378 --- [ main] o.s.b.d.f.s.MyApplication : Started MyApplication in 5.456 seconds (JVM running for 6.299)
|
||||
```
|
||||
- 日期与时间 : 精度为ms
|
||||
- log level : ERROR, WARN, INFO, DEBUG, TRACE
|
||||
- 进程ID
|
||||
- 线程名称 : [main]
|
||||
- logger name : 输出日志类的类名(通常为缩写)
|
||||
- log信息
|
||||
## 控制台输出
|
||||
默认情况下,log日志信息会回显输出到console中,默认ERROR, WARN, DEBUG三个级别的信息将会被日志输出。
|
||||
可以通过--debug选项来启用“debug”模式
|
||||
```shell
|
||||
java -jar xxx.jar --debug
|
||||
```
|
||||
通过在application.properties中指定debug=true也可以开启“debug”模式
|
||||
```properties
|
||||
debug=true
|
||||
```
|
||||
当“debug”模式被开启后,一部分核心的logger(内嵌容器、Hibernate、SpringBoot)将会被配置输出更多的信息。
|
||||
> ***开启debug模式并不意味着输出所有日志级别为Debug的信息***
|
||||
|
||||
> ***同样,也可以通过--trace或者在properties中指定trace=true来开启trace模式***
|
||||
|
||||
## 文件输出
|
||||
默认情况下,Spring Boot只会将日志输出到console中,如果想要额外定义将日志输出到文件中,需要在application.properties中定义logging.file.name或者logging.file.path
|
||||
| logging.file.name | logging.file.path | example | description |
|
||||
| :-: | :-: | :-: | :-: |
|
||||
| (none) | (none) | | 只在控制台输出 |
|
||||
| 特定文件 | (none) | my.log | 特定log文件路径,可以是绝对路径或相对路径) |
|
||||
| (none) | 特定目录 | /var/log | 将日志输出到该路径下的spring.log文件,可以是绝对路径或相对路径|
|
||||
> 当log文件大小到达10MB时将会旋转重写,和console log一样,log文件也会输出ERROR, WARN和INFO
|
||||
|
||||
> ***logging properties和实际的logging机制是相互独立的,因而,特定的配置属性(如logback.configurationFile)并不由SpringBoot管理***
|
||||
|
||||
## File Rotation
|
||||
如果使用的是Logback,则可以在application.properties中定义file rotation行为。
|
||||
| Name | Description |
|
||||
| :-: | :-: |
|
||||
| logging.logback.rollingpolicy.file-name-pattern | 定义创建log归档的命名模式 |
|
||||
| logging.logback.rollingpolicy.clean-history-on-start | 定义是否应该在项目启动时清理历史日志 |
|
||||
| logging.logback.rollingpolicy.max-file-size | 定义日志在归档前的最大大小 |
|
||||
| logging.logback.rollingpolicy.total-size-cap | 日志归档在被删除前可以占用的最大大小 |
|
||||
| logging.logback.rollingpolicy.max-history | 要保留归档日志文件的最大数量 |
|
||||
|
||||
## Log Level
|
||||
所有的日志系统都可以通过Application.properties定义logging.level.<logger-name>=<level>来定义事务级别,事务级别可以是TRACE, DEBUG, INFO, WARN, ERROR, FATAL, OFF。
|
||||
可以通过logging.level.root来定义root logger的隔离级别。
|
||||
```properties
|
||||
logging.level.root=warn
|
||||
logging.level.org.springframework.web=debug
|
||||
logging.level.org.hibernate=error
|
||||
```
|
||||
## Log Group
|
||||
可以通过log group将关联的logger组合在一起,并且对log group统一指定日志级别。
|
||||
```properties
|
||||
# 定义一个名为“tomcat”的log group
|
||||
logging.group.tomcat=org.apache.catalina,org.apache.coyote,org.apache.tomcat
|
||||
|
||||
# 为名为“tomcat”的log group统一指定log level
|
||||
logging.level.tomcat=trace
|
||||
```
|
||||
> Spring Boot具有如下先定义好的log group,可以开箱即用
|
||||
> - web : org.springframework.core.codec, org.springframework.http, org.springframework.web, org.springframework.boot.actuate.endpoint.web, org.springframework.boot.web.servlet.ServletContextInitializerBeans
|
||||
> - sql : org.springframework.jdbc.core, org.hibernate.SQL, org.jooq.tools.LoggerListener
|
||||
|
||||
|
||||
722
spring/logback/logback.md
Normal file
722
spring/logback/logback.md
Normal file
@@ -0,0 +1,722 @@
|
||||
# Logback
|
||||
## Introduce
|
||||
`logback-classic`模块需要classpath下存在`slf4j-api.jar`, `logback-core.jar`, `logback-classic.jar`。
|
||||
|
||||
logback使用示例如下:
|
||||
```java
|
||||
package chapters.introduction;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class HelloWorld1 {
|
||||
|
||||
public static void main(String[] args) {
|
||||
|
||||
Logger logger = LoggerFactory.getLogger("chapters.introduction.HelloWorld1");
|
||||
logger.debug("Hello world.");
|
||||
|
||||
}
|
||||
}
|
||||
```
|
||||
`HelloWorld1`类定义在`chapters.introduction`包下,其引入了slf4j中的`Logger`和`LoggerFactory`类。
|
||||
|
||||
在上述示例中,并没有引入任何logback的类,在使用日志的大多数场景中,只需要引入slf4j的api类即可。
|
||||
|
||||
## Logback Architecture
|
||||
logback目前结构分为3个模块,`logback-core`,`logback-classic`,`logback-access`。
|
||||
|
||||
`logback-core`是其他两个模块的基础,`classic`模块拓展了`core`模块。`classic`模块实现了slf4j api,因而在使用slf4j时可以在logback和其他日志系统之间轻松切换。而`access`模块和servlet容器做了集成,可以通过http对日志进行访问。
|
||||
|
||||
### Logger, Appender, Layout
|
||||
logback基于3个主要的类构成:`Logger`, `Appender`, `Layout`。`Logger`类属于logback-classic模块,Appender和Layout类则是属于logback-core模块。
|
||||
|
||||
#### Log Context
|
||||
在logback-classic中,每个logger都关联了一个`Log Context`,log Context负责创建logger,并且log context将创建的logger按照树状结构排列。
|
||||
|
||||
Logger都拥有名称,其名称大小写敏感,并遵循分层的命名规则:
|
||||
|
||||
> 如果一个logger的名称,其名称加上`.`后形成的字符串,为另一个logger的前缀,那么可称后一个logger是前一个logger的后代。如果一个logger和后代logger之间没有其他的logger,那么可以称前一个logger是后一个logger的parent。
|
||||
|
||||
例如,名称为`"com.foo"`的logger是名称为`"com.foo.Bar"`的parent。
|
||||
|
||||
root logger是位于最顶层的logger,其可以通过如下方式进行获取:
|
||||
```java
|
||||
Logger rootLogger = LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME);
|
||||
```
|
||||
|
||||
其他所有的logger也可以通过`getLogger`方法进行获取,该方法接收logger name来作为参数,`Logger`接口具有如下方法:
|
||||
```java
|
||||
package org.slf4j;
|
||||
public interface Logger {
|
||||
|
||||
// Printing methods:
|
||||
public void trace(String message);
|
||||
public void debug(String message);
|
||||
public void info(String message);
|
||||
public void warn(String message);
|
||||
public void error(String message);
|
||||
}
|
||||
```
|
||||
|
||||
#### 有效级别
|
||||
logger都会被指定一个log level,level可以是`TRACE, DEBUG, INFO, WARN, ERROR`中的一个,其定义在`ch.qos.logback.classic.Level`类中。如果指定logger没有被指定level,那么其会从`离其最近并且被指定level的先祖logger`中继承level。
|
||||
|
||||
为了保证所有logger都可以继承level,root logger一定会被指定log level。默认情况下,level为`debug`。
|
||||
|
||||
> log level顺序为:
|
||||
>
|
||||
> `TRACE < DEBUG < INFO < WARN < ERROR`
|
||||
|
||||
#### 获取Logger
|
||||
在通过`LoggerFactory.getLogger`获取logger对象时,如果向方法传入相同的名称,那么会获得相同的logger对象。
|
||||
|
||||
通常,应在每个class中指定一个logger,logger name为class的全类名。
|
||||
|
||||
#### Appender
|
||||
在logback中,允许将日志打印到多个目的地,在logback中,目的地被称为`Appender`。目前,目的地可以是console、文件、remote socket server、数据库、JMS等。
|
||||
|
||||
对每个logger,都可以关联不止一个Appender。
|
||||
|
||||
`addAppender`将会将Appender关联到指定的logger。每个被允许打印的log请求都会被发送到该logger关联的所有Appender中,并且log请求还会被发送到树结构更高的Appender中。在logger结构中,Appender是增量继承的,后代logger会继承先祖logger的Appender。
|
||||
|
||||
如果为root logger指定一个console appender,并且为L指定一个file appender,那么对于L和L的后代logger打印日志的请求不仅会被发送到console,还会被打印到文件中。
|
||||
|
||||
> 可以手动指定logger L增量继承appender的标志为false,那么L和其所有后代logger都不会继承L先祖logger的appender。
|
||||
>
|
||||
> 默认情况下,appender是否增量继承的标志,默认值为true
|
||||
|
||||
#### Layout
|
||||
如果想要指定日志打印的格式,可以将Layout对象和logger相关联。`PatternLayout`可以通过c语言`printf`的格式来指定打印格式。
|
||||
|
||||
`PatternLayout`中,若指定conversion pattern为`%-4relative [%thread] %-5level %logger{32} - %msg%n`,其会按如下格式打印消息:
|
||||
```
|
||||
176 [main] DEBUG manual.architecture.HelloWorld2 - Hello world.
|
||||
```
|
||||
|
||||
#### 参数化打印
|
||||
可以通过如下形式来进行参数化打印
|
||||
```java
|
||||
Object entry = new SomeObject();
|
||||
logger.debug("The entry is {}.", entry);
|
||||
```
|
||||
只有当前消息被评估应该打印时,才会通过消息模板来构造消息,将`{}`替换为entry的实际值。
|
||||
|
||||
## Logback Configuration
|
||||
### logback配置顺序
|
||||
1. 如果`logback.configurationFile`系统变量设置
|
||||
2. 如果1步骤失败,会尝试在classpath中寻找`logback-test.xml`
|
||||
3. 如果2步骤失败,会尝试在classpath中寻找`logback.xml`
|
||||
|
||||
> #### 通过命令行System Properties设置logback配置文件位置
|
||||
> 可以通过向命令行传递`-Dlogback.configurationFile=/path/to/config.xml`来设置logback配置文件的位置
|
||||
|
||||
### 通过xml形式配置logback
|
||||
```xml
|
||||
<configuration>
|
||||
|
||||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<!-- encoders are assigned the type
|
||||
ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
|
||||
<encoder>
|
||||
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} -%kvp- %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<root level="debug">
|
||||
<appender-ref ref="STDOUT" />
|
||||
</root>
|
||||
</configuration>
|
||||
```
|
||||
|
||||
### 自动加载配置文件更新
|
||||
logback支持对配置文件的变动进行扫描,并且对扫描到的变动进行自动配置。为了令logback支持配置自动更新,需要为`configuration`元素配置scan属性,示例如下所示:
|
||||
```xml
|
||||
<configuration scan="true">
|
||||
...
|
||||
</configuration>
|
||||
```
|
||||
|
||||
在开启配置自动更新时,默认会每间隔`1 minute`就扫描配置变动。可以通过`scanPeriod`属性自定义扫描周期,周期单位可以是`milliseconds, seconds, minutes, hours`。示例如下所示:
|
||||
```xml
|
||||
<configuration scan="true" scanPeriod="30 seconds" >
|
||||
...
|
||||
</configuration>
|
||||
```
|
||||
> 如果在没有为scanPerod属性值指定单位时,默认单位为ms
|
||||
|
||||
### xml配置文件语法
|
||||
一个基础的xml配置文件可以包含如下结构:
|
||||
|
||||
1. 一个`<configuration>`元素
|
||||
1. 0或多个`<appender>`元素
|
||||
2. 0或多个`<logger>`元素
|
||||
3. 最多一个`<root>`元素
|
||||
|
||||
#### `<logger>`
|
||||
`<logger>`元素用于配置logger,其接收一个必填的`name`属性,一个可选的`level`属性,一个可选的`additivity`属性。
|
||||
- `additivity`属性的值可以为true或者false。
|
||||
- `level`属性的值可以为`TRACE, DEBUG, INFO, WARN, ERROR, ALL, OFF`,是大小写不敏感的。
|
||||
- 如果想要从上级继承level,可以将level属性的值设置为`INHERITED`或是`NULL`
|
||||
|
||||
`logger`元素中可以包含0个或多个`<appender-ref>`元素,这样每个引用的appender都会被加入到logger中。
|
||||
|
||||
#### `<root>`
|
||||
`root`元素用于配置root logger,其只支持一个属性`level`。由于root元素已经被命名为`ROOT`,故而也不允许指定`name`属性。
|
||||
|
||||
`<root>`元素的`level`属性只能被赋值为`TRACE, DEBUG, INFO, WARN, ERROR, ALL, OFF`,不能被赋值为`INHERITED`或是`NULL`.
|
||||
|
||||
和logger元素一样,root元素中也能包含0个或多个`<appender-ref>`元素。
|
||||
|
||||
```xml
|
||||
<configuration>
|
||||
|
||||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<!-- encoders are assigned the type
|
||||
ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
|
||||
<encoder>
|
||||
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} -%kvp- %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<logger name="chapters.configuration" level="INFO"/>
|
||||
|
||||
<!-- Strictly speaking, the level attribute is not necessary since -->
|
||||
<!-- the level of the root level is set to DEBUG by default. -->
|
||||
<root level="DEBUG">
|
||||
<appender-ref ref="STDOUT" />
|
||||
</root>
|
||||
|
||||
</configuration>
|
||||
```
|
||||
|
||||
#### `Appender`
|
||||
`<appender>`元素用于配置Appender,其接收两个必填属性:`name`和`class`。其中,`name`属性指定了appender的名称,而`class`属性则是指定了appender Class的全类名。
|
||||
|
||||
`appender`元素可以包含0或多个`<layout>`元素,0或多个`<encoder>`元素,0或多个`<filter>`元素。除了可以包含上述公共元素外,还`appender`元素还可以包含任意数量javaBean属性相关的元素,
|
||||
|
||||
#### `Layout`
|
||||
`<layout>`元素接收一个必填`class`属性,值为Layout类的全类名。和appender元素一样,layout也可以包含javaBean属性相关的元素。如果layout class为`PatternLayout`,那么class属性可以省略。
|
||||
|
||||
#### `encoder`
|
||||
`<encoder>`元素接收一个必填的class属性,值为Encoder的全类名。如果encoder类为`PatternLayoutEncoder`,那么class属性可以省略。
|
||||
|
||||
当想要向多个appender中输出日志时,只需要在logger中定义多个appender即可,示例如下
|
||||
|
||||
```xml
|
||||
<configuration>
|
||||
|
||||
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
|
||||
<file>myApp.log</file>
|
||||
|
||||
<encoder>
|
||||
<pattern>%date %level [%thread] %logger{10} [%file:%line] -%kvp- %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>%kvp %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<root level="debug">
|
||||
<appender-ref ref="FILE" />
|
||||
<appender-ref ref="STDOUT" />
|
||||
</root>
|
||||
</configuration>
|
||||
```
|
||||
上述配置中,定义了两个appender,`FILE`和`STDOUT`。 `FILE` appender将日志打印到`myApp.log`文件中。`STDOUT` appender则是将日志打印到控制台。
|
||||
|
||||
root logger通过`<appender-ref>`标签来引用appender,每个appender都有其自己的encoder,encoder通常在appender之间并不共享。同样的,layout也不会在多个appender之间共享。
|
||||
|
||||
> 由于logger不仅会将日志发送到其自身的logger,并且还会将日志发送给其先祖logger的appender,故而将相同的appender共享给拥有上下级关系的logger将会导致日志的重复打印。
|
||||
|
||||
可以通过设置`additivity`属性来设置后代logger不继承先祖logger的appender,配置示例如下所示:
|
||||
```xml
|
||||
<configuration>
|
||||
|
||||
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
|
||||
<file>foo.log</file>
|
||||
<encoder>
|
||||
<pattern>%date %level [%thread] %logger{10} [%file : %line] -%kvp- %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>%msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<logger name="chapters.configuration.Foo" additivity="false">
|
||||
<appender-ref ref="FILE" />
|
||||
</logger>
|
||||
|
||||
<root level="debug">
|
||||
<appender-ref ref="STDOUT" />
|
||||
</root>
|
||||
</configuration>
|
||||
```
|
||||
|
||||
### 变量替换
|
||||
可以以`${VAR_NAME}`的形式来使用变量替换,且`HOSTNAME`和`CONTEXT_NAME`变量是默认定义的。
|
||||
|
||||
如下示例展示了如何定义变量:
|
||||
```xml
|
||||
<configuration>
|
||||
|
||||
<variable name="USER_HOME" value="/home/sebastien" />
|
||||
|
||||
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
|
||||
<file>${USER_HOME}/myApp.log</file>
|
||||
<encoder>
|
||||
<pattern>%kvp %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<root level="debug">
|
||||
<appender-ref ref="FILE" />
|
||||
</root>
|
||||
</configuration>
|
||||
```
|
||||
|
||||
变量除了可以在配置文件中定义,还可以通过System Properties进行传递,
|
||||
```shell
|
||||
java -DUSER_HOME="/home/sebastien" MyApp2
|
||||
```
|
||||
```xml
|
||||
<configuration>
|
||||
|
||||
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
|
||||
<file>${USER_HOME}/myApp.log</file>
|
||||
<encoder>
|
||||
<pattern>%kvp %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<root level="debug">
|
||||
<appender-ref ref="FILE" />
|
||||
</root>
|
||||
</configuration>
|
||||
```
|
||||
当存在多个变量时,可以单独为变量定义一个文件:
|
||||
```xml
|
||||
<configuration>
|
||||
|
||||
<variable file="src/main/java/chapters/configuration/variables1.properties" />
|
||||
|
||||
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
|
||||
<file>${USER_HOME}/myApp.log</file>
|
||||
<encoder>
|
||||
<pattern>%kvp %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<root level="debug">
|
||||
<appender-ref ref="FILE" />
|
||||
</root>
|
||||
</configuration>
|
||||
```
|
||||
```properties
|
||||
USER_HOME=/home/sebastien
|
||||
```
|
||||
除了通过`<variable>`元素的file属性指定变量文件位置外,还可以通过`variable`元素的resource属性指定classpath下的文件路径:
|
||||
```xml
|
||||
<configuration>
|
||||
|
||||
<variable resource="resource1.properties" />
|
||||
|
||||
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
|
||||
<file>${USER_HOME}/myApp.log</file>
|
||||
<encoder>
|
||||
<pattern>%kvp %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<root level="debug">
|
||||
<appender-ref ref="FILE" />
|
||||
</root>
|
||||
</configuration>
|
||||
```
|
||||
|
||||
#### 变量作用域
|
||||
一个变量可以被定义在如下作用域:
|
||||
- LOCAL SCOPE: 具有local作用域的变量,生命周期为从其文件定义该变量的位置开始,一直到该配置文件的末尾
|
||||
- CONTEXT SCOPE: 具有context作用域的变量,在context存在期间一直存在,直到context执行clear操作
|
||||
- SYSTEM SCOPE: 具有system作用域的变量,将会添加到jvm的system properties中,生命周期和jvm相同,直到该变量被清空
|
||||
|
||||
在变量替换时,变量首先从local scope中查找,其次到context scope中找,再其次到system scope中找,最后到os环境变量中找。
|
||||
|
||||
`<variable>`元素中可以含有`scope`属性,其值可以为`local`,`context`或`system`。如果没有指定scope属性,默认是`local`。
|
||||
|
||||
```xml
|
||||
<configuration>
|
||||
|
||||
<variable scope="context" name="nodeId" value="firstNode" />
|
||||
|
||||
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
|
||||
<file>/opt/${nodeId}/myApp.log</file>
|
||||
<encoder>
|
||||
<pattern>%kvp %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<root level="debug">
|
||||
<appender-ref ref="FILE" />
|
||||
</root>
|
||||
</configuration>
|
||||
```
|
||||
在上述示例中,尽管`nodeId`变量被定义为context scope,其在每个logging event中都可以使用。
|
||||
|
||||
#### 为变量赋值默认值
|
||||
可以按`"${aName:-golden}"`的方式为指定返回的默认值,当aName变量未定义时,返回值为`golden`。
|
||||
|
||||
#### 变量嵌套
|
||||
在定义变量时,支持变量的嵌套:
|
||||
```properties
|
||||
USER_HOME=/home/sebastien
|
||||
fileName=myApp.log
|
||||
destination=${USER_HOME}/${fileName}
|
||||
```
|
||||
|
||||
并且,表达式也支持嵌套变量:
|
||||
`"${${userid}.password}"`表达式在`userid`变量为`asahi`的情况下,表示`asahi.password`变量的值
|
||||
|
||||
在为变量指定默认值时,默认值也可以通过变量来指定,例如`${id:-${userid}}`,在`id`变量未定义时,会返回`userid`变量的值。
|
||||
|
||||
### 条件处理配置文件
|
||||
logback配置文件支持通过`<if>`,`<then>`,`<else>`元素来条件指定配置。条件处理需要`janino`库。
|
||||
|
||||
条件处理语法如下:
|
||||
```xml
|
||||
<!-- if-then form -->
|
||||
<if condition="some conditional expression">
|
||||
<then>
|
||||
...
|
||||
</then>
|
||||
</if>
|
||||
|
||||
<!-- if-then-else form -->
|
||||
<if condition="some conditional expression">
|
||||
<then>
|
||||
...
|
||||
</then>
|
||||
<else>
|
||||
...
|
||||
</else>
|
||||
</if>
|
||||
```
|
||||
|
||||
条件为java表达式,且只允许访问system properties和context properties。`proerpty()`方法或`p()`方法将会返回变量的值,例如`p('k')`或`property('k')`将会返回变量`k`的值。如果`k`变量未定义,将会返回空字符串。
|
||||
|
||||
`isDefined`方法则是可以用来检查指定变量是否被定义。并且,可以通过`isNull`方法来检查指定变量的值是否为空。
|
||||
|
||||
```xml
|
||||
<configuration debug="true">
|
||||
|
||||
<if condition='property("HOSTNAME").contains("torino")'>
|
||||
<then>
|
||||
<appender name="CON" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>%d %-5level %logger{35} -%kvp- %msg %n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
<root>
|
||||
<appender-ref ref="CON" />
|
||||
</root>
|
||||
</then>
|
||||
</if>
|
||||
|
||||
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
|
||||
<file>${randomOutputDir}/conditional.log</file>
|
||||
<encoder>
|
||||
<pattern>%d %-5level %logger{35} -%kvp- %msg %n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<root level="ERROR">
|
||||
<appender-ref ref="FILE" />
|
||||
</root>
|
||||
</configuration>
|
||||
```
|
||||
## Appender
|
||||
logback把日志事件的写操作委托给了Appender组件。Appender必须实现`ch.qos.logback.core.Appender`接口,接口主要方法如下:
|
||||
```java
|
||||
package ch.qos.logback.core;
|
||||
|
||||
import ch.qos.logback.core.spi.ContextAware;
|
||||
import ch.qos.logback.core.spi.FilterAttachable;
|
||||
import ch.qos.logback.core.spi.LifeCycle;
|
||||
|
||||
|
||||
public interface Appender<E> extends LifeCycle, ContextAware, FilterAttachable {
|
||||
|
||||
public String getName();
|
||||
public void setName(String name);
|
||||
void doAppend(E event);
|
||||
|
||||
}
|
||||
```
|
||||
其中,doAppend方法负责将日志输出到对应的输出设备。
|
||||
|
||||
appender是命名实体,故而其可以通过name来进行引用。Appender实现了FilterAttachable接口,故而可以将一个或多个filter关联到appender。
|
||||
|
||||
appender负责将日志事件打印到输出设备,但是,appender可以将日志的格式化操作委托给`Layout`或`Encoder`对象。每个layout或encoder都只会关联一个appender,一些logger拥有内置的固定format格式,对于用于内置固定format的appender,其不需要encoder或layout。
|
||||
|
||||
> 例如,SocketAppender并没有encoder或layout,其只是将日志事件序列化,并且通过网络传输序列化后的数据
|
||||
|
||||
### AppenderBase
|
||||
`ch.qos.logback.core.AppenderBase`是一个实现了Appender接口的抽象类,其针对appender中的部分接口提供了实现。logback中内置的所有apender实现都实现了该抽象类。
|
||||
|
||||
`AppenderBase`类实现了doAppend方法,并留下了`append`方法供实现类进行实现。AppenderBase类中,doAppend方法被synchronized修饰,多线程环境下doAppend方法的调用是阻塞的。
|
||||
|
||||
### Logback Core
|
||||
logback core是其他logback模块的基础。其内置了如下开箱即用的Appender。
|
||||
|
||||
#### OutputStreamAppender
|
||||
`OutputStreamAppender`会将日志事件追加到`java.io.OutputStream`中,该类为其他基于`OutputSteamAppender`的appender提供了基础。
|
||||
|
||||
ConsoleAppender和FileAppender都继承了OutputStreamAppender。
|
||||
|
||||
#### ConsoleAppender
|
||||
ConsoleAppender会将日志追加到console,即`System.out`或`System.err`中,前者是默认的目标设备。`ConsoleAppender`通过用户指定的`encoder`来对日志事件进行格式化。
|
||||
|
||||
ConsoleAppender拥有如下property:
|
||||
- encoder : `Encoder`类型
|
||||
- target : 字符串类型,值为`System.out`或`System.err`
|
||||
- withJansi : boolean类型,默认为false、
|
||||
|
||||
如下为ConsoleAppender的使用示例:
|
||||
```xml
|
||||
<configuration>
|
||||
|
||||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<!-- encoders are assigned the type
|
||||
ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
|
||||
<encoder>
|
||||
<pattern>%-4relative [%thread] %-5level %logger{35} -%kvp- %msg %n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<root level="DEBUG">
|
||||
<appender-ref ref="STDOUT" />
|
||||
</root>
|
||||
</configuration>
|
||||
```
|
||||
#### FileAppender
|
||||
`FileAppender`是`OutputStreamAppender`的子类,将日志事件打印到文件中。目标文件通过`File`选项来指定,如果指定文件已经存在,是否清空文件内容或将日志追加到文件末尾,取决于`append`属性。
|
||||
|
||||
FileAppender拥有如下property:
|
||||
- append: boolean类型,当文件已经存在时,如果设置为true,日志追加到文件末尾,如果日志设置为false,已存在文件的内容将会被清空。默认该属性被设置为true
|
||||
- encoder:Encoder类型
|
||||
- file:字符串类型,为写入文件的文件名,如果文件不存在,那么文件会被创建。如果该路径值中存在不存在的目录,FileAppender会自动创建目录。
|
||||
- bufferSize:FileSize类型,当immediateFlush属性被设置为false时,可以通过bufferSize选项将会设置output buffer的大小。bufferSize的默认值为8192。在定义FileSize类型时,可以按`KB, MB, GB`来指定,只需要以`5MB`形式指定即可。在没有指定后缀单位时,默认为字节
|
||||
- prudent: 当prudent属性设置为true时,会将`append`属性设置为true。prudent依赖与排他的file lock,在开启prudent后,写日志开销通常是prudent关闭开销的3倍。
|
||||
|
||||
> ##### Immediate flush
|
||||
> 默认情况下,每个log事件都会立即刷新到outputstream中,这样可以避免日志的丢失(如果日志刷新存在延迟,那么应用在未刷新的情况下当即可能会使缓存中的日志丢失)。
|
||||
>
|
||||
> 当时,如果要提高吞吐量,可以将`immediateFlush`property设置为false。
|
||||
|
||||
配置示例如下所示:
|
||||
```xml
|
||||
<configuration>
|
||||
|
||||
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
|
||||
<file>testFile.log</file>
|
||||
<append>true</append>
|
||||
<!-- set immediateFlush to false for much higher logging throughput -->
|
||||
<immediateFlush>true</immediateFlush>
|
||||
<!-- encoders are assigned the type
|
||||
ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
|
||||
<encoder>
|
||||
<pattern>%-4relative [%thread] %-5level %logger{35} -%kvp- %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<root level="DEBUG">
|
||||
<appender-ref ref="FILE" />
|
||||
</root>
|
||||
</configuration>
|
||||
```
|
||||
|
||||
##### `<timestamp>`
|
||||
对于短期应用,可能会期望在每次程序运行时,都创建唯一的日志文件。可以通过`<timestamp>`元素来实现该需求,示例如下:
|
||||
```xml
|
||||
<configuration>
|
||||
|
||||
<!-- Insert the current time formatted as "yyyyMMdd'T'HHmmss" under
|
||||
the key "bySecond" into the logger context. This value will be
|
||||
available to all subsequent configuration elements. -->
|
||||
<timestamp key="bySecond" datePattern="yyyyMMdd'T'HHmmss"/>
|
||||
|
||||
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
|
||||
<!-- use the previously created timestamp to create a uniquely
|
||||
named log file -->
|
||||
<file>log-${bySecond}.txt</file>
|
||||
<encoder>
|
||||
<pattern>%logger{35} -%kvp- %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<root level="DEBUG">
|
||||
<appender-ref ref="FILE" />
|
||||
</root>
|
||||
</configuration>
|
||||
```
|
||||
|
||||
timestamp元素接收两个必填属性,`key`和`datePattern`,并接收一个非必填的属性`timeReference`。
|
||||
- datePattern : 格式和`SimpleDateFormat`相同
|
||||
- timeReference : 默认情况下,timestamp元素的值为当前配置文件被解析的事件,也可以将其设为`contextBirth`,即context创建时间
|
||||
|
||||
#### RollingFileAppender
|
||||
`RollingFileAppender`继承了FileAppender,并且支持了滚动日志的能力。RollgingAppender拥有如下子组件:
|
||||
- `RollingPolicy`:负责如何执行日志滚动操作
|
||||
- `TriggeringPolicy`:负责决定是否/何时出发日志滚动
|
||||
|
||||
RollingFileAppender同时需要RollingPolicy和TriggerPolicy组件,但是如果RollingPolicy实现了TriggerPolicy接口,那么只需要RollingPolicy即可。
|
||||
|
||||
如下是RollingFileAppender的属性:
|
||||
- file: 值为String类型,如果日志只被写到RollingPolicy指定的目标设备,那么file属性可以为空
|
||||
- append:是否追加
|
||||
- encoder:同FileOutputStream
|
||||
- rollingPolicy:值为`RollingPolicy`类型
|
||||
- triggeringPolicy:值为`TriggeringPolicy`类型
|
||||
- prudent
|
||||
|
||||
##### RollingPolicy
|
||||
`RollingPolicy`负责日志滚动过程,其中涉及文件移动和重命名。
|
||||
|
||||
RollingPolicy接口展示如下:
|
||||
```java
|
||||
package ch.qos.logback.core.rolling;
|
||||
|
||||
import ch.qos.logback.core.FileAppender;
|
||||
import ch.qos.logback.core.spi.LifeCycle;
|
||||
|
||||
public interface RollingPolicy extends LifeCycle {
|
||||
|
||||
public void rollover() throws RolloverFailure;
|
||||
public String getActiveFileName();
|
||||
public CompressionMode getCompressionMode();
|
||||
public void setParent(FileAppender appender);
|
||||
}
|
||||
```
|
||||
其中,各方法代表含义如下:
|
||||
- rollober: 将当前日志文件归档
|
||||
- getActiveFileName:计算当前日志文件的文件名(实时日志将会被写入到其中)
|
||||
- getCompressionMode: 决定压缩模式
|
||||
- setParent:设置关联的FileAppender
|
||||
|
||||
##### TimeBasedRollingPolicy
|
||||
TimeBasedRollingPolicy是最常用的rollingPolicy,其基于时间来执行滚动操作,可以通过其设置按天或按月滚动。
|
||||
|
||||
TimeBasedRollingPolicy同时实现了RollingPolicy和TriggeringPolicy接口。
|
||||
|
||||
TimeBasedRollingPolicy接收一个必填的`fileNamePattern`参数,并且拥有可选填参数:
|
||||
- `fileNamePattern`: 该属性定义了已经被滚动(已被归档)文件的文件名,该属性的应该含有文件名,并且在适当位置加上`%d转换说明符`(%d{yyyy-MM-dd})。
|
||||
> ##### %d转换说明符
|
||||
> %d转换说明符包含一个date-and-time格式的字符串,该格式和SimpleDateFormat相同。如果该date-and-time被省略,那么默认为yyyy-MM-dd.
|
||||
>
|
||||
> 可以在fileNamePattern中含有多个%d转换说明符,但是只能有一个`主%d转换说明符`,主%d转换说明符用于推断滚动周期。其他说有的%d转换说明符都需要通过`aux`来进行修饰(`%d{yyyy/MM, aux}`)。
|
||||
>
|
||||
> 示例如下
|
||||
>
|
||||
> `/var/log/%d{yyyy/MM, aux}/myapplication.%d{yyyy-MM-dd}.log`.
|
||||
|
||||
滚动周期的值将会从date-and-time表达式来推断。
|
||||
|
||||
RollingFileAppender中,file属性可以被省略或设置为null。如果为RollingFileAppender设置了file属性,那么可以解耦当前日志和已经被归档的日志。如果file属性的值,那么当前日志路径就是file文件的值,此时当前日志的路径不会随着日期的变动而改变。
|
||||
|
||||
但是,在省略file属性的情形下,那么当前日志文件路径将会根据fileNamePattern来计算
|
||||
|
||||
- `maxHistory`: `maxHistory`指定了保留归档日志的最大数量,会对旧日志进行异步删除。
|
||||
|
||||
如果将maxHistory设置为0,将会禁用旧日志删除。默认情况下,maxHistory被设置为0。
|
||||
|
||||
- `totalSizeCap`:`totalSizeCap`控制了所有归档日志文件的最大大小,当最大小超过阈值时,最老的文件将会被删除。`totalSizeCap`需要`maxHistory`属性也被设置,通常,totalSizeCap都在maxHistory之后被应用。
|
||||
|
||||
默认情况下,totalSizeCap被设置为0,代表不会有总大小的阈值限制。
|
||||
|
||||
- `cleanHistoryOnStart`:如果该属性被设置为true,当appender启动时归档日志将会被清除。
|
||||
|
||||
TimeBasedRollingPolicy支持文件自动压缩,如果`fileNamePattern`后缀以`.gz`或`.zip`结尾,文件压缩将会自动被应用。
|
||||
|
||||
示例如下
|
||||
```
|
||||
/wombat/foo.%d.gz
|
||||
```
|
||||
上述示例当前日志被输出到`/wombat/foo.yyyy-MM-dd`文件中,但是在第二天触发归档后,文件将会被压缩到`/wombat/foo.yyyy-MM-dd.gz`中。
|
||||
|
||||
|
||||
TimeBasedRollingPolicy示例如下:
|
||||
```xml
|
||||
<configuration>
|
||||
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>logFile.log</file>
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||
<!-- daily rollover -->
|
||||
<fileNamePattern>logFile.%d{yyyy-MM-dd}.log</fileNamePattern>
|
||||
|
||||
<!-- keep 30 days' worth of history capped at 3GB total size -->
|
||||
<maxHistory>30</maxHistory>
|
||||
<totalSizeCap>3GB</totalSizeCap>
|
||||
|
||||
</rollingPolicy>
|
||||
|
||||
<encoder>
|
||||
<pattern>%-4relative [%thread] %-5level %logger{35} -%kvp- %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<root level="DEBUG">
|
||||
<appender-ref ref="FILE" />
|
||||
</root>
|
||||
</configuration>
|
||||
```
|
||||
|
||||
#### SizeAndTimeBasedRollingPolicy
|
||||
SizeAndTimeBasedRollingPolicy支持配置单个日志文件的最大大小,其除了支持%d转换符外,还支持%i转换符,%i转换符也是必填的。每当当前日志文件达到最大文件大小时,其都会递增`%i`序列号,并且旧日志文件将会被归档。序列号从0开始。
|
||||
|
||||
SizeAndTimeBasedRollingPolicy还包含如下属性:
|
||||
- `maxFileSize`:每当当前日志文件达到`maxFileSize`指定的大小时,其都会递增序列号,序列号默认为0.
|
||||
|
||||
maxFileSize为FileSize类型,可以通过`KB, MB, GB`等单位来指定。
|
||||
|
||||
- `checkIncrement`:检查当前日志文大小的间隔时间,默认为60s
|
||||
|
||||
```xml
|
||||
<configuration>
|
||||
<appender name="ROLLING" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>mylog.txt</file>
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
|
||||
<!-- rollover daily -->
|
||||
<fileNamePattern>mylog-%d{yyyy-MM-dd}.%i.txt</fileNamePattern>
|
||||
<!-- each file should be at most 100MB, keep 60 days worth of history, but at most 20GB -->
|
||||
<maxFileSize>100MB</maxFileSize>
|
||||
<maxHistory>60</maxHistory>
|
||||
<totalSizeCap>20GB</totalSizeCap>
|
||||
</rollingPolicy>
|
||||
<encoder>
|
||||
<pattern>%msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
|
||||
<root level="DEBUG">
|
||||
<appender-ref ref="ROLLING" />
|
||||
</root>
|
||||
|
||||
</configuration>
|
||||
```
|
||||
|
||||
#### Encoder
|
||||
通常,PatternLayoutEncoder,其pattern可以指定为如下形式:
|
||||
```xml
|
||||
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
|
||||
<Pattern>
|
||||
%d{dd-MM-yyyy HH:mm:ss.SSS} [%thread] %-5level %logger{36}.%M - %msg%n
|
||||
</Pattern>
|
||||
</encoder>
|
||||
```
|
||||
@@ -1,13 +1,13 @@
|
||||
# lombok标签简介
|
||||
## @NonNull
|
||||
- 如果用@NonNull标签来修饰一个参数,在方法或构造器的开头会插入一个空校验来检查该参数是否为空。
|
||||
- 如果将@NonNull标签用来修饰一个field,任何通过注解产生的方法(如@Setter产生的Setter)都会在试图分配该field一个值时进行空校验
|
||||
## @RequiredArgsConstructor
|
||||
被该注解修饰的类,会产生一个包含required args的构造器。required args包含final field和被特殊约束的field(如被@NonNull约束)
|
||||
## @ToString
|
||||
产生一个toString方法的实现,并且该实现会被所有对象继承
|
||||
## @Data
|
||||
@Data是一个快捷的注解,其将@ToString,@EqualsAndHashCode,@Getter/@Setter,@RequiredArgsConstructor等注解整合到了一起
|
||||
- 对于@Data注解标注的类,如果类中包含一个方法,其方法名和@Data将要产生的方法相同并且参数个数也相同(不需要参数类型相同),那么该方法将不会被产生,并且不会产生任何警告或错误
|
||||
- 对于@Data标注的类,如果该类显示声明了一个构造器,那么@Data不会再生成任何构造器
|
||||
# lombok标签简介
|
||||
## @NonNull
|
||||
- 如果用@NonNull标签来修饰一个参数,在方法或构造器的开头会插入一个空校验来检查该参数是否为空。
|
||||
- 如果将@NonNull标签用来修饰一个field,任何通过注解产生的方法(如@Setter产生的Setter)都会在试图分配该field一个值时进行空校验
|
||||
## @RequiredArgsConstructor
|
||||
被该注解修饰的类,会产生一个包含required args的构造器。required args包含final field和被特殊约束的field(如被@NonNull约束)
|
||||
## @ToString
|
||||
产生一个toString方法的实现,并且该实现会被所有对象继承
|
||||
## @Data
|
||||
@Data是一个快捷的注解,其将@ToString,@EqualsAndHashCode,@Getter/@Setter,@RequiredArgsConstructor等注解整合到了一起
|
||||
- 对于@Data注解标注的类,如果类中包含一个方法,其方法名和@Data将要产生的方法相同并且参数个数也相同(不需要参数类型相同),那么该方法将不会被产生,并且不会产生任何警告或错误
|
||||
- 对于@Data标注的类,如果该类显示声明了一个构造器,那么@Data不会再生成任何构造器
|
||||
- 可以通过为@Data标注类中的方法添加@lombok.experimental.Tolerate来为lombok隐藏这些方法
|
||||
1589
spring/redisson/redisson.md
Normal file
1589
spring/redisson/redisson.md
Normal file
File diff suppressed because it is too large
Load Diff
238
spring/spring boot/Spring Boot Async.md
Normal file
238
spring/spring boot/Spring Boot Async.md
Normal file
@@ -0,0 +1,238 @@
|
||||
- [Spring Boot Async](#spring-boot-async)
|
||||
- [Spring Executor和Scheduler的自动配置](#spring-executor和scheduler的自动配置)
|
||||
- [Task Execution and Scheduling](#task-execution-and-scheduling)
|
||||
- [Task Execution Abstraction](#task-execution-abstraction)
|
||||
- [TaskExecutor接口的实现种类](#taskexecutor接口的实现种类)
|
||||
- [TaskExecutor的使用](#taskexecutor的使用)
|
||||
- [Spring TaskScheduler Abstraction](#spring-taskscheduler-abstraction)
|
||||
- [Trigger接口](#trigger接口)
|
||||
- [Trigger实现类](#trigger实现类)
|
||||
- [TaskScheduler实现类](#taskscheduler实现类)
|
||||
- [对任务调度和异步执行的注解支持](#对任务调度和异步执行的注解支持)
|
||||
- [启用Scheduling注解](#启用scheduling注解)
|
||||
- [@Scheduled注解](#scheduled注解)
|
||||
- [@Async注解](#async注解)
|
||||
- [@Async方法的异常处理](#async方法的异常处理)
|
||||
- [Cron表达式](#cron表达式)
|
||||
- [Macros(宏)](#macros宏)
|
||||
|
||||
|
||||
# Spring Boot Async
|
||||
## Spring Executor和Scheduler的自动配置
|
||||
当当前上下文中没有Executor类型的bean对象时,spring boot会自动配置一个ThreadPoolTaskExecutor类型的bean对象,并且将该bean对象和异步task执行(@EnableAsync)和spring mvc异步请求处理关联在一起。
|
||||
该默认创建的ThreadPoolTaskExecutor默认使用8个核心线程,并且线程数量可以根据负载动态的增加或者减少。
|
||||
可以通过如下方式对ThreadPoolTaskExecutor进行配置:
|
||||
```properties
|
||||
# 该线程池最多含有16个线程
|
||||
spring.task.execution.pool.max-size=16
|
||||
# 有有界队列存放task,上限为100
|
||||
spring.task.execution.pool.queue-capacity=100
|
||||
# 当线程空闲10s(默认60s)时会进行回收
|
||||
spring.task.execution.pool.keep-alive=10s
|
||||
# 设置核心线程数量
|
||||
spring.task.execution.pool.core-size=8
|
||||
```
|
||||
如果使用@EnableScheduling,一个ThreadPoolTaskScheduler也可以被配置。该线程池默认使用一个线程,但是也可以动态设置:
|
||||
```properties
|
||||
spring.task.scheduling.thread-name-prefix=scheduling-
|
||||
spring.task.scheduling.pool.size=2
|
||||
```
|
||||
## Task Execution and Scheduling
|
||||
Spring通过TaskExecutor和TaskScheduler接口为task的异步执行和调度提供了抽象。
|
||||
### Task Execution Abstraction
|
||||
Executor对应的是JDK中线程池的概念。在Spring中,TaskExecutor接口和java.util.concurrent.Executor接口相同,该接口中只有一个execute方法(execute(Runnable task)),接受一个task。
|
||||
### TaskExecutor接口的实现种类
|
||||
Spring中包含许多预制的TaskExecutor实现类,该实现类如下:
|
||||
- SyncTaskExecutor:该实现类不会执行异步的调用,所有任务的执行都会发生在调用该executor的线程中。(通常使用在不需要多线程的场景)
|
||||
- SimpleAsyncTaskExecutor:该实现类不会复用任何的线程,相反的,对于每次调用该executor,都会使用一个全新的线程。但是,该实现类的确支持对并发数量的限制,该executor会阻塞任何超过并发数量限制的调用,直到slot被释放为止。(SimpleAsyncTaskExecutor并不支持池化技术)
|
||||
- ConcurrentTaskExecutor:该实现类是java.util.concurrent.Executor实例的adapter,其可以将java.util.concurrent.Executor实例的配置参数以bean properties的形式暴露。(当ThreadPoolTaskExecutor的灵活性无法满足需求时,可以使用ConcurrentTaskExecutor)
|
||||
- ThreadPoolTaskExecutor:该实现类型是最广泛被使用的。该实现类可以通过bean properties来配置java.util.concurrent.ThreadPoolExecutor实例,并且将该实例wrap在TaskExecutor实例中。当想要使用另一种java.util.concurrent.Executor时,可以使用ConcurrentTaskExecutor。
|
||||
- DefaultManagedTaskExecutor:该实现使用了通过JNDI获取的ManagedExecutorService
|
||||
|
||||
### TaskExecutor的使用
|
||||
在springboot中,当使用@EnableAsync时,可以通过配置ThreadPoolExecutor的bean properties来配置线程池的核心线程数和最大线程数等属性。
|
||||
```properties
|
||||
# 该线程池最多含有16个线程
|
||||
spring.task.execution.pool.max-size=16
|
||||
# 有有界队列存放task,上限为100
|
||||
spring.task.execution.pool.queue-capacity=100
|
||||
# 当线程空闲10s(默认60s)时会进行回收
|
||||
spring.task.execution.pool.keep-alive=10s
|
||||
# 设置核心线程数量
|
||||
spring.task.execution.pool.core-size=8
|
||||
```
|
||||
|
||||
## Spring TaskScheduler Abstraction
|
||||
为了在特定的时间点执行task,Spring引入了TaskScheduler接口,接口定义如下:
|
||||
```java
|
||||
public interface TaskScheduler {
|
||||
|
||||
ScheduledFuture schedule(Runnable task, Trigger trigger);
|
||||
|
||||
ScheduledFuture schedule(Runnable task, Instant startTime);
|
||||
|
||||
ScheduledFuture scheduleAtFixedRate(Runnable task, Instant startTime, Duration period);
|
||||
|
||||
ScheduledFuture scheduleAtFixedRate(Runnable task, Duration period);
|
||||
|
||||
ScheduledFuture scheduleWithFixedDelay(Runnable task, Instant startTime, Duration delay);
|
||||
|
||||
ScheduledFuture scheduleWithFixedDelay(Runnable task, Duration delay);
|
||||
```
|
||||
> scheduleAtFixedRate和scheduleAtFixedDelay区别:
|
||||
> - fixed rate:表示两个task执行开始时间的间隔
|
||||
> - fixed delay:表示上一个task结束时间和下一个task开始时间的间隔
|
||||
>
|
||||
> 实例如下:
|
||||
> - fixed rate:TTWWWTTTWWT...(开始时间间隔为5)
|
||||
> - fixed delay:TTWWWWWTTTTWWWWWTTTTTTTWWWWWT...(上次结束和下次开始之间的间隔为5)
|
||||
>
|
||||
> **通常,TaskScheduler默认情况下是单线程执行的,故而fixed rate执行时,如果一个Task执行时间超过period时,在当前task执行完成之前,下一个task并不会开始执行。下一个task会等待当前task执行完成之后立马执行。**
|
||||
|
||||
### Trigger接口
|
||||
Trigger接口的核心理念是下次执行事件由上次执行的结果决定。上次执行的结果存储在TriggerContext中。
|
||||
Trigger接口如下:
|
||||
```java
|
||||
public interface Trigger {
|
||||
|
||||
Date nextExecutionTime(TriggerContext triggerContext);
|
||||
}
|
||||
```
|
||||
TriggerContext接口如下:(其具有默认的实现类SimpleTriggerContext)
|
||||
```java
|
||||
public interface TriggerContext {
|
||||
|
||||
Date lastScheduledExecutionTime();
|
||||
|
||||
Date lastActualExecutionTime();
|
||||
|
||||
Date lastCompletionTime();
|
||||
}
|
||||
```
|
||||
### Trigger实现类
|
||||
Spring为Trigger提供了两个实现类,其中CronTrigger允许task的调度按照cron expression来执行(类似linux中的crond)。
|
||||
```java
|
||||
scheduler.schedule(task, new CronTrigger("0 15 9-17 * * MON-FRI"));
|
||||
```
|
||||
Spring的另一个Trigger实现是PeriodicTrigger,其接受一个固定的period期间,一个可选的初始delay值,并接收一个boolean值标识该period是fixed-rate还是fixed-delay。
|
||||
### TaskScheduler实现类
|
||||
如果不需要外部的线程管理,可以使用spring提供的ThreadPoolTaskScheduler,其会将任务委托给ScheduledExecutorService来提供bean-properties形式的配置。
|
||||
|
||||
## 对任务调度和异步执行的注解支持
|
||||
Spring同时对任务调度和异步方法的执行提供注解支持。
|
||||
### 启用Scheduling注解
|
||||
想要启用@Async和@Scheduled注解,必须将@EnableAsync注解和@EnableScheduling注解添加到一个@Configuration类上。
|
||||
```java
|
||||
@Configuration
|
||||
@EnableAsync
|
||||
@EnableScheduling
|
||||
public class AppConfig {
|
||||
}
|
||||
```
|
||||
> **@Async注解的实现是通过proxy模式来实现的,故而如果在类内部调用位于同一个类中的@Async方法,那么代理拦截会失效,此时调用的@Async方法将会同步执行而非异步执行**
|
||||
|
||||
> @Async可以接收一个value值,用于指定目标的执行器(Executor或TaskExecutor的beanName)
|
||||
|
||||
### @Scheduled注解
|
||||
在将@Scheduled注解标注到方法时,可以为其指定一个trigger的元数据,使用示例如下:
|
||||
```java
|
||||
@Scheduled(fixedDelay = 5000)
|
||||
public void doSomething() {
|
||||
// something that should run periodically
|
||||
}
|
||||
```
|
||||
> 默认情况下,fixedDelay、fixedRate、initialDelay的时间单位都是ms,可以指定timeUnit来指定其他的时间单位:
|
||||
> ```java
|
||||
> @Scheduled(fixedDelay = 5, timeUnit = TimeUnit.SECONDS)
|
||||
> public void doSomething() {
|
||||
> // something that should run periodically
|
||||
> }
|
||||
> ```
|
||||
|
||||
可以为@Scheduled注解使用cron表达式
|
||||
```java
|
||||
@Scheduled(cron="*/5 * * * * MON-FRI")
|
||||
public void doSomething() {
|
||||
// something that should run on weekdays only
|
||||
}
|
||||
```
|
||||
|
||||
### @Async注解
|
||||
通过为方法指定@Async注解,可以让该方法异步执行,该方法的执行通过TaskExecutor。
|
||||
```java
|
||||
@Async
|
||||
void doSomething() {
|
||||
// this will be run asynchronously
|
||||
}
|
||||
```
|
||||
可以为@Async标注的方法指定参数和返回值,但是异步方法的返回值只能为void或是Future类型。
|
||||
在该异步方法的调用方调用返回Future实例的get方法之前,调用方仍然能够执行其他操作,异步方法的执行位于TaskExecutor的线程中。
|
||||
```java
|
||||
@Async
|
||||
Future<String> returnSomething(int i) {
|
||||
// this will be run asynchronously
|
||||
}
|
||||
```
|
||||
不能将@Async注解和生命周期回调(例如@PostConstruct)进行混用,如果想要异步的初始化bean对象,需要按照如下方法:
|
||||
```java
|
||||
public class SampleBeanImpl implements SampleBean {
|
||||
|
||||
@Async
|
||||
void doSomething() {
|
||||
// ...
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public class SampleBeanInitializer {
|
||||
|
||||
private final SampleBean bean;
|
||||
|
||||
public SampleBeanInitializer(SampleBean bean) {
|
||||
this.bean = bean;
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void initialize() {
|
||||
bean.doSomething();
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
### @Async方法的异常处理
|
||||
当@Async方法的返回类型是Future类型时,处理async方法执行时抛出的异常非常简单,当在Future对象上调用get方法时异常会被抛出。**但当@Async方法的返回类型是void时,执行时抛出的异常既无法被捕获也无法被传输。可以指定一个AsyncUncaughtExceptionHandler来处理此类异常。**
|
||||
```java
|
||||
public class MyAsyncUncaughtExceptionHandler implements AsyncUncaughtExceptionHandler {
|
||||
|
||||
@Override
|
||||
public void handleUncaughtException(Throwable ex, Method method, Object... params) {
|
||||
// handle exception
|
||||
}
|
||||
}
|
||||
```
|
||||
### Cron表达式
|
||||
在Spring中,cron表达式格式如下:
|
||||
\* \* \* \* \* \*
|
||||
其代表为(s,min,hour,day of month/\*(1~31)\*/,month/\*(1~12)\*/,day of week/\*(0~7,0 or 7 is sun)\*/)
|
||||
> 规则如下:
|
||||
> - 所有字段都可以用(*)来匹配所有值
|
||||
> - 逗号(,)可以用来分隔同一字段中多个值
|
||||
> - 分号(-)可以用来指定范围,指定的范围左右都包含,eg,1-5代表[1,5]
|
||||
> - 在范围(或是*)后跟随下划线(/)代表间隔,如在分钟字段指定*/20,代表该小时内每过20min
|
||||
> - 对于月份或者day of week,可以使用英文名的前三个字母来代替,大小写不敏感
|
||||
> - 在day of month或day of week字段中可以包含L字母
|
||||
> - 在day of month字段,L代表该月的最后一天,在该字段还可以为L指定一个负的偏移量,如L-n代表该月的倒数第n+1天
|
||||
> - 在day of week字段,L代表该周的最后一天,L前还可以前缀月份的数字或是月份的前三个字母(dL或DDDL,如7L或sunL,代表该月份的最后一个星期日),代该月份的最后一个day of week
|
||||
> - day of month字段可以指定为nW,代表离day of month为n最近的一个工作日,如果n为周六,则该该字段代表的值为周五的day of month,如果n为周六,则该字段代表下周一的day of month(如果n为1其位于周六,其也代表下周一的day of month,1W代表该月的第一个工作日)
|
||||
> - 如果day of month的值为LW,则代表该月的最后一个工作日
|
||||
> - day of week字段还能指定为d#n或DDD#n的形式,代表该月的的第几个d in week(例如SUN#2,代表当前月的第二个星期日)
|
||||
|
||||
#### Macros(宏)
|
||||
Cron表达式可读性不太好,可以使用如下预定义的宏:
|
||||
| Macro | meaning |
|
||||
|:-:|:-:|
|
||||
| @yearly (or @annually) | once a year (0 0 0 1 1 *) |
|
||||
| @monthly | once a month (0 0 0 1 * *) |
|
||||
| @weekly | once a week (0 0 0 * * 0) |
|
||||
| @daily (or @midnight) | once a day (0 0 0 * * *), or |
|
||||
| @hourly | once an hour, (0 0 * * * *) |
|
||||
@@ -1,53 +1,53 @@
|
||||
# Spring Data Redis
|
||||
- ## Spring Data Redis
|
||||
- 在Spring框架中,与Redis进行通信,既提供了低层次的api与字节数据进行交互,也提供了高度抽象的api供用户使用
|
||||
- Redis Connection支持通过连接器和Redis服务端进行低层次的交互
|
||||
- RedisTemplate则是向用户提供了高层次api与Rdis服务端进行交互
|
||||
- ## 连接Redis
|
||||
- 为了通过IOC容器连接Redis,需要使用统一的Spring Redis API。对于任何库提供的Redis Connector,都会统一向外提供一致的Spring Redis API。
|
||||
- Spring Redis API通过RedisConnection和RedisConnectionFactory接口进行工作,并且从Redis处获取连接
|
||||
- ## RedisConnection和RedisConnectionFactory
|
||||
- RdisConnection用于提供和Redis后端的交互。RedisConnection接口会自动将底层Connector库异常转化为统一的Spring DAO异常,因而在使用ReidsConnection的过程中,如果切换底层链接库,并不需要代码的改动。
|
||||
- RedisConnection时通过RedisConnectionFactory来获取的
|
||||
- ## RedisTemplate
|
||||
- RedisTemplate是线程安全的,可以在多个线程中被并发安全的使用
|
||||
- RedisTemplate使用java底层的序列化机制,在通过RedisTemplate读取或者写入数据时,会通过Java的序列化机制将对象序列化/反序列化。
|
||||
- RedisTemplate要求key是非空的,但是可以接受空的value值
|
||||
- 当想要使用RedisTemplate的某个视图时,可以将RedisTemplate对象注入给该名为xxxOperations的视图。
|
||||
```java
|
||||
@Service
|
||||
public class RedisTemplateOperations {
|
||||
// 直接注入
|
||||
@Autowired
|
||||
private RedisTemplate<String,String> redisTemplate;
|
||||
|
||||
// 将RedisTemplate转换成其某一个视图之后再注入
|
||||
@Resource(name="redisTemplate")
|
||||
private ListOperations<String,String> opsForList;
|
||||
}
|
||||
```
|
||||
- ## StringRedisTemplate
|
||||
- 在Redis操作中,key和Value通常都是String类型。故而,Spring Data Redis提供了操作StringRedisTemplate类
|
||||
- StringRedisTemplate是RedisTemplate的子类,并且在应用启动时IOC容器中会注入redisTemplate和stringRedisTemplate两个对象
|
||||
- 相比较于RedisTemplate,StringRedisTemplate底层使用StringRedisSerializer来进行序列化,其序列化的key和value都是可读的
|
||||
- ## 序列化Serializer选择
|
||||
- 除了先前提到的jdk默认的序列化机制和StringRedisSerializer,Spring Data Redis还提供了其他Serializer
|
||||
- 例如,可以选择将key和value化为json字符串格式,可以选用Jackson2JsonSerializer或者GenericJackson2JsonSerializer
|
||||
> 相对于Jackson2JsonSerializer,GenericJackson2JsonSerializer在序列化对象为json串时添加了对象的java类型信息,故而在将json串反序列化并且转换为原有类型时不会抛出异常
|
||||
```java
|
||||
/**
|
||||
* 如果想要自定义redisTemplate的序列化方式,可以添加如下配置类
|
||||
* RedisTemplate支持分别自定义key/value/hashkey/hashvalue的序列化方式
|
||||
* RedisTemplate也支持设置defaultSerializer,当key/value/hashkey/hashvalue的Serializer没有显式指定时,会应用defaultSerializer
|
||||
**/
|
||||
@Configuration
|
||||
public class SerializerConfig {
|
||||
@Bean(name="redisTemplate")
|
||||
RedisTemplate<Object,Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
|
||||
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
|
||||
redisTemplate.setConnectionFactory(redisConnectionFactory);
|
||||
redisTemplate.setDefaultSerializer(new GenericJackson2JsonRedisSerializer());
|
||||
return redisTemplate;
|
||||
}
|
||||
}
|
||||
# Spring Data Redis
|
||||
- ## Spring Data Redis
|
||||
- 在Spring框架中,与Redis进行通信,既提供了低层次的api与字节数据进行交互,也提供了高度抽象的api供用户使用
|
||||
- Redis Connection支持通过连接器和Redis服务端进行低层次的交互
|
||||
- RedisTemplate则是向用户提供了高层次api与Rdis服务端进行交互
|
||||
- ## 连接Redis
|
||||
- 为了通过IOC容器连接Redis,需要使用统一的Spring Redis API。对于任何库提供的Redis Connector,都会统一向外提供一致的Spring Redis API。
|
||||
- Spring Redis API通过RedisConnection和RedisConnectionFactory接口进行工作,并且从Redis处获取连接
|
||||
- ## RedisConnection和RedisConnectionFactory
|
||||
- RdisConnection用于提供和Redis后端的交互。RedisConnection接口会自动将底层Connector库异常转化为统一的Spring DAO异常,因而在使用ReidsConnection的过程中,如果切换底层链接库,并不需要代码的改动。
|
||||
- RedisConnection时通过RedisConnectionFactory来获取的
|
||||
- ## RedisTemplate
|
||||
- RedisTemplate是线程安全的,可以在多个线程中被并发安全的使用
|
||||
- RedisTemplate使用java底层的序列化机制,在通过RedisTemplate读取或者写入数据时,会通过Java的序列化机制将对象序列化/反序列化。
|
||||
- RedisTemplate要求key是非空的,但是可以接受空的value值
|
||||
- 当想要使用RedisTemplate的某个视图时,可以将RedisTemplate对象注入给该名为xxxOperations的视图。
|
||||
```java
|
||||
@Service
|
||||
public class RedisTemplateOperations {
|
||||
// 直接注入
|
||||
@Autowired
|
||||
private RedisTemplate<String,String> redisTemplate;
|
||||
|
||||
// 将RedisTemplate转换成其某一个视图之后再注入
|
||||
@Resource(name="redisTemplate")
|
||||
private ListOperations<String,String> opsForList;
|
||||
}
|
||||
```
|
||||
- ## StringRedisTemplate
|
||||
- 在Redis操作中,key和Value通常都是String类型。故而,Spring Data Redis提供了操作StringRedisTemplate类
|
||||
- StringRedisTemplate是RedisTemplate的子类,并且在应用启动时IOC容器中会注入redisTemplate和stringRedisTemplate两个对象
|
||||
- 相比较于RedisTemplate,StringRedisTemplate底层使用StringRedisSerializer来进行序列化,其序列化的key和value都是可读的
|
||||
- ## 序列化Serializer选择
|
||||
- 除了先前提到的jdk默认的序列化机制和StringRedisSerializer,Spring Data Redis还提供了其他Serializer
|
||||
- 例如,可以选择将key和value化为json字符串格式,可以选用Jackson2JsonSerializer或者GenericJackson2JsonSerializer
|
||||
> 相对于Jackson2JsonSerializer,GenericJackson2JsonSerializer在序列化对象为json串时添加了对象的java类型信息,故而在将json串反序列化并且转换为原有类型时不会抛出异常
|
||||
```java
|
||||
/**
|
||||
* 如果想要自定义redisTemplate的序列化方式,可以添加如下配置类
|
||||
* RedisTemplate支持分别自定义key/value/hashkey/hashvalue的序列化方式
|
||||
* RedisTemplate也支持设置defaultSerializer,当key/value/hashkey/hashvalue的Serializer没有显式指定时,会应用defaultSerializer
|
||||
**/
|
||||
@Configuration
|
||||
public class SerializerConfig {
|
||||
@Bean(name="redisTemplate")
|
||||
RedisTemplate<Object,Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
|
||||
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
|
||||
redisTemplate.setConnectionFactory(redisConnectionFactory);
|
||||
redisTemplate.setDefaultSerializer(new GenericJackson2JsonRedisSerializer());
|
||||
return redisTemplate;
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -1,9 +1,9 @@
|
||||
# Spring Data
|
||||
- ## Spring Boot中选择连接池的算法:
|
||||
- HikariCP的表现和并发性都很好,如果HikariCP可以被获取,选择HikariCP
|
||||
- 如果HikariCP不能获取,选用Tomcat Datasource
|
||||
- 如果Tomcat Datasource也不能获取,选用DBCP2
|
||||
- 如果上述都无法获取,选用Oracle UCP
|
||||
- Spring Boot使用自定义连接池:
|
||||
- 可以通过显式指定自定义的连接池种类来绕过该算法,通过指定spring.datasource.type来自定义连接池
|
||||
# Spring Data
|
||||
- ## Spring Boot中选择连接池的算法:
|
||||
- HikariCP的表现和并发性都很好,如果HikariCP可以被获取,选择HikariCP
|
||||
- 如果HikariCP不能获取,选用Tomcat Datasource
|
||||
- 如果Tomcat Datasource也不能获取,选用DBCP2
|
||||
- 如果上述都无法获取,选用Oracle UCP
|
||||
- Spring Boot使用自定义连接池:
|
||||
- 可以通过显式指定自定义的连接池种类来绕过该算法,通过指定spring.datasource.type来自定义连接池
|
||||
- 可以通过DatasourceBuilder来定义额外的datasource。如果定义了自己的datasouce bean,那么自动装配将不会发生。
|
||||
@@ -1,49 +1,49 @@
|
||||
# Spring单元测试
|
||||
## SpringBootTest
|
||||
在SpringBoot中,提供了@SpringBootTest注解。当需要SpringBoot特性时,可以通过使用@SpringBootTest注解来作为@ContextConfiguration的替代。@SprngBootTest创建ApplicationContext,该context在test中被使用。
|
||||
## @AfterAll
|
||||
该注解标明的方法,会在所有Test执行完成之后再执行
|
||||
> ### @AfterAll注解特性
|
||||
> - @AfterAll注解的方法必须含有void类型返回值
|
||||
> - @AfterAll注解标注方法不能为private
|
||||
> - 默认情况下,@AfterAll注解的方法必须是static修饰的
|
||||
|
||||
> ### 在非static方法中标注@AfterAll
|
||||
> 在非static方法上标注@AfterAll,需要在class上标注@TestInstance(TestInstance.Lifecycle.PER_CLASS)。
|
||||
> 因为默认情况下TestInstance的默认生命周期是PER_METHOD
|
||||
> ```JAVA
|
||||
> @TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
> public class BeforeAndAfterAnnotationsUnitTest {
|
||||
>
|
||||
> String input;
|
||||
> Long result;
|
||||
> @BeforeAll
|
||||
> public void setup() {
|
||||
> input = "77";
|
||||
> }
|
||||
>
|
||||
> @AfterAll
|
||||
> public void teardown() {
|
||||
> input = null;
|
||||
> result = null;
|
||||
> }
|
||||
>
|
||||
> @Test
|
||||
> public void whenConvertStringToLong_thenResultShouldBeLong() {
|
||||
> result = Long.valueOf(input);
|
||||
> Assertions.assertEquals(77l, result);
|
||||
> }
|
||||
> }
|
||||
> ```
|
||||
|
||||
## @AfterEach
|
||||
该注解标明的方法,在每次@Test标注方法执行完成之后都会被执行。
|
||||
> ### @AfterEach特性
|
||||
> - 标注方法返回值为空
|
||||
> - 标注方法不为private
|
||||
> - 标注方法不是static
|
||||
|
||||
## @BeforeAll
|
||||
类似@AfterAll
|
||||
## @BeforeEach
|
||||
# Spring单元测试
|
||||
## SpringBootTest
|
||||
在SpringBoot中,提供了@SpringBootTest注解。当需要SpringBoot特性时,可以通过使用@SpringBootTest注解来作为@ContextConfiguration的替代。@SprngBootTest创建ApplicationContext,该context在test中被使用。
|
||||
## @AfterAll
|
||||
该注解标明的方法,会在所有Test执行完成之后再执行
|
||||
> ### @AfterAll注解特性
|
||||
> - @AfterAll注解的方法必须含有void类型返回值
|
||||
> - @AfterAll注解标注方法不能为private
|
||||
> - 默认情况下,@AfterAll注解的方法必须是static修饰的
|
||||
|
||||
> ### 在非static方法中标注@AfterAll
|
||||
> 在非static方法上标注@AfterAll,需要在class上标注@TestInstance(TestInstance.Lifecycle.PER_CLASS)。
|
||||
> 因为默认情况下TestInstance的默认生命周期是PER_METHOD
|
||||
> ```JAVA
|
||||
> @TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
> public class BeforeAndAfterAnnotationsUnitTest {
|
||||
>
|
||||
> String input;
|
||||
> Long result;
|
||||
> @BeforeAll
|
||||
> public void setup() {
|
||||
> input = "77";
|
||||
> }
|
||||
>
|
||||
> @AfterAll
|
||||
> public void teardown() {
|
||||
> input = null;
|
||||
> result = null;
|
||||
> }
|
||||
>
|
||||
> @Test
|
||||
> public void whenConvertStringToLong_thenResultShouldBeLong() {
|
||||
> result = Long.valueOf(input);
|
||||
> Assertions.assertEquals(77l, result);
|
||||
> }
|
||||
> }
|
||||
> ```
|
||||
|
||||
## @AfterEach
|
||||
该注解标明的方法,在每次@Test标注方法执行完成之后都会被执行。
|
||||
> ### @AfterEach特性
|
||||
> - 标注方法返回值为空
|
||||
> - 标注方法不为private
|
||||
> - 标注方法不是static
|
||||
|
||||
## @BeforeAll
|
||||
类似@AfterAll
|
||||
## @BeforeEach
|
||||
类似@AfterEach
|
||||
@@ -1,71 +1,71 @@
|
||||
# Spring Boot JSON
|
||||
## Spring Boot JSON简介
|
||||
在Spring Boot中为JSON提供了三个集成的内置库:Gson、Jackson、JSON-B。
|
||||
> 其中,Jackson是Spring Boot推荐并且默认的JSON库。
|
||||
|
||||
Spring Boot项目为Jackson提供了自动装配,并且Jackson是spring-boot-starter-json启动器的一部分。当Jackson依赖在classpath下时,ObjectMapper的bean对象会自动的被配置。
|
||||
## 自定义序列化器和反序列化器
|
||||
如果使用Jackson进行json数据的序列化和反序列化,你可能需要实现自己的序列化类和反序列化类。可以通过@JsonComponent注解来定义自己的JsonSerializer, JsonDeserializer,JsonObjectSerializer,JsonObjectDeserializer
|
||||
```java
|
||||
@JsonComponent
|
||||
public class MyJsonComponent {
|
||||
|
||||
public static class Serializer extends JsonSerializer<MyObject> {
|
||||
|
||||
@Override
|
||||
public void serialize(MyObject value, JsonGenerator jgen, SerializerProvider serializers) throws IOException {
|
||||
jgen.writeStartObject();
|
||||
jgen.writeStringField("name", value.getName());
|
||||
jgen.writeNumberField("age", value.getAge());
|
||||
jgen.writeEndObject();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class Deserializer extends JsonDeserializer<MyObject> {
|
||||
|
||||
@Override
|
||||
public MyObject deserialize(JsonParser jsonParser, DeserializationContext ctxt) throws IOException {
|
||||
ObjectCodec codec = jsonParser.getCodec();
|
||||
JsonNode tree = codec.readTree(jsonParser);
|
||||
String name = tree.get("name").textValue();
|
||||
int age = tree.get("age").intValue();
|
||||
return new MyObject(name, age);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@JsonComponent
|
||||
public class MyJsonComponent {
|
||||
|
||||
public static class Serializer extends JsonObjectSerializer<MyObject> {
|
||||
|
||||
@Override
|
||||
protected void serializeObject(MyObject value, JsonGenerator jgen, SerializerProvider provider)
|
||||
throws IOException {
|
||||
jgen.writeStringField("name", value.getName());
|
||||
jgen.writeNumberField("age", value.getAge());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class Deserializer extends JsonObjectDeserializer<MyObject> {
|
||||
|
||||
@Override
|
||||
protected MyObject deserializeObject(JsonParser jsonParser, DeserializationContext context, ObjectCodec codec,
|
||||
JsonNode tree) throws IOException {
|
||||
String name = nullSafeValue(tree.get("name"), String.class);
|
||||
int age = nullSafeValue(tree.get("age"), Integer.class);
|
||||
return new MyObject(name, age);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
## Jackson使用
|
||||
Jackson使用ObjectMapper来将json转化为java对象,或者将java对象转化为json。
|
||||
### ObjectMapper处理json域字段和java类域字段的映射关系
|
||||
|
||||
# Spring Boot JSON
|
||||
## Spring Boot JSON简介
|
||||
在Spring Boot中为JSON提供了三个集成的内置库:Gson、Jackson、JSON-B。
|
||||
> 其中,Jackson是Spring Boot推荐并且默认的JSON库。
|
||||
|
||||
Spring Boot项目为Jackson提供了自动装配,并且Jackson是spring-boot-starter-json启动器的一部分。当Jackson依赖在classpath下时,ObjectMapper的bean对象会自动的被配置。
|
||||
## 自定义序列化器和反序列化器
|
||||
如果使用Jackson进行json数据的序列化和反序列化,你可能需要实现自己的序列化类和反序列化类。可以通过@JsonComponent注解来定义自己的JsonSerializer, JsonDeserializer,JsonObjectSerializer,JsonObjectDeserializer
|
||||
```java
|
||||
@JsonComponent
|
||||
public class MyJsonComponent {
|
||||
|
||||
public static class Serializer extends JsonSerializer<MyObject> {
|
||||
|
||||
@Override
|
||||
public void serialize(MyObject value, JsonGenerator jgen, SerializerProvider serializers) throws IOException {
|
||||
jgen.writeStartObject();
|
||||
jgen.writeStringField("name", value.getName());
|
||||
jgen.writeNumberField("age", value.getAge());
|
||||
jgen.writeEndObject();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class Deserializer extends JsonDeserializer<MyObject> {
|
||||
|
||||
@Override
|
||||
public MyObject deserialize(JsonParser jsonParser, DeserializationContext ctxt) throws IOException {
|
||||
ObjectCodec codec = jsonParser.getCodec();
|
||||
JsonNode tree = codec.readTree(jsonParser);
|
||||
String name = tree.get("name").textValue();
|
||||
int age = tree.get("age").intValue();
|
||||
return new MyObject(name, age);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@JsonComponent
|
||||
public class MyJsonComponent {
|
||||
|
||||
public static class Serializer extends JsonObjectSerializer<MyObject> {
|
||||
|
||||
@Override
|
||||
protected void serializeObject(MyObject value, JsonGenerator jgen, SerializerProvider provider)
|
||||
throws IOException {
|
||||
jgen.writeStringField("name", value.getName());
|
||||
jgen.writeNumberField("age", value.getAge());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class Deserializer extends JsonObjectDeserializer<MyObject> {
|
||||
|
||||
@Override
|
||||
protected MyObject deserializeObject(JsonParser jsonParser, DeserializationContext context, ObjectCodec codec,
|
||||
JsonNode tree) throws IOException {
|
||||
String name = nullSafeValue(tree.get("name"), String.class);
|
||||
int age = nullSafeValue(tree.get("age"), Integer.class);
|
||||
return new MyObject(name, age);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
## Jackson使用
|
||||
Jackson使用ObjectMapper来将json转化为java对象,或者将java对象转化为json。
|
||||
### ObjectMapper处理json域字段和java类域字段的映射关系
|
||||
|
||||
|
||||
604
spring/spring boot/spel.md
Normal file
604
spring/spring boot/spel.md
Normal file
@@ -0,0 +1,604 @@
|
||||
- [Spel](#spel)
|
||||
- [Spel表达式计算](#spel表达式计算)
|
||||
- [示例](#示例)
|
||||
- [Concept](#concept)
|
||||
- [调用方法](#调用方法)
|
||||
- [调用属性](#调用属性)
|
||||
- [调用构造器](#调用构造器)
|
||||
- [在特定对象上计算表达式](#在特定对象上计算表达式)
|
||||
- [EvaluationContext](#evaluationcontext)
|
||||
- [EvaluationContext实现类](#evaluationcontext实现类)
|
||||
- [TypeConversion](#typeconversion)
|
||||
- [在定义Bean对象时使用Spel表达式](#在定义bean对象时使用spel表达式)
|
||||
- [为field指定默认值](#为field指定默认值)
|
||||
- [通过setter为field指定默认值](#通过setter为field指定默认值)
|
||||
- [@Autowired方法和构造器](#autowired方法和构造器)
|
||||
- [Spel语法](#spel语法)
|
||||
- [字面量表达式](#字面量表达式)
|
||||
- [字符串类型字面量](#字符串类型字面量)
|
||||
- [Properties,Array,List,Map,Indexer](#propertiesarraylistmapindexer)
|
||||
- [访问属性](#访问属性)
|
||||
- [Array](#array)
|
||||
- [Map](#map)
|
||||
- [列表表示](#列表表示)
|
||||
- [数组构建](#数组构建)
|
||||
- [方法调用](#方法调用)
|
||||
- [操作符](#操作符)
|
||||
- [关系操作符](#关系操作符)
|
||||
- [instanceof操作符和matches操作符](#instanceof操作符和matches操作符)
|
||||
- [逻辑操作符](#逻辑操作符)
|
||||
- [数学运算符](#数学运算符)
|
||||
- [赋值运算符](#赋值运算符)
|
||||
- [类型操作符](#类型操作符)
|
||||
- [构造方法](#构造方法)
|
||||
- [变量](#变量)
|
||||
- [#this and #root](#this-and-root)
|
||||
- [方法注册](#方法注册)
|
||||
- [引用bean对象](#引用bean对象)
|
||||
- [三元操作符](#三元操作符)
|
||||
- [Elvis Operator](#elvis-operator)
|
||||
- [空安全操作符](#空安全操作符)
|
||||
- [集合过滤操作符](#集合过滤操作符)
|
||||
- [集合映射](#集合映射)
|
||||
- [表达式模板](#表达式模板)
|
||||
|
||||
|
||||
# Spel
|
||||
## Spel表达式计算
|
||||
### 示例
|
||||
如下是一个Spel表达式计算示例:
|
||||
```java
|
||||
ExpressionParser parser = new SpelExpressionParser();
|
||||
Expression exp = parser.parseExpression("'Hello World'");
|
||||
String message = (String) exp.getValue();
|
||||
// 最终message计算的值是'Hello World'
|
||||
```
|
||||
### Concept
|
||||
`ExpressionParser`接口用于对表达式字符串进行解析,在上述示例中,表达式字符串是一个由单引号包围起来的字符串字面量。
|
||||
|
||||
`Expression`接口则是负责对表达式字符串进行计算。
|
||||
|
||||
在调用`parser.parseExpression`和`exp.getValue`,分别会抛出`ParseException`和`EvaluationException`异常。
|
||||
|
||||
Spel支持许多特性,例如调用方法,访问属性,调用构造方法等,示例如下:
|
||||
|
||||
#### 调用方法
|
||||
```java
|
||||
ExpressionParser parser = new SpelExpressionParser();
|
||||
Expression exp = parser.parseExpression("'Hello World'.concat('!')");
|
||||
String message = (String) exp.getValue();
|
||||
// message值为Hello World!
|
||||
```
|
||||
|
||||
#### 调用属性
|
||||
```java
|
||||
ExpressionParser parser = new SpelExpressionParser();
|
||||
|
||||
// invokes 'getBytes().length'
|
||||
Expression exp = parser.parseExpression("'Hello World'.bytes.length");
|
||||
int length = (Integer) exp.getValue();
|
||||
```
|
||||
|
||||
#### 调用构造器
|
||||
```java
|
||||
ExpressionParser parser = new SpelExpressionParser();
|
||||
Expression exp = parser.parseExpression("new String('hello world').toUpperCase()");
|
||||
String message = exp.getValue(String.class);
|
||||
```
|
||||
|
||||
通过`public <T> T getValue(Class<T> desiredResultType)`方法,向getValue方法传递一个Class对象,可以避免在调用getValue方法调用之后需要将返回值手动转化为特定类型。如果类型转化失败,会抛出一个EvaluationException异常。
|
||||
|
||||
#### 在特定对象上计算表达式
|
||||
Spel支持在一个特定的对象上(该对象通常被称作root object)进行计算,例如如下示例:
|
||||
```java
|
||||
// Create and set a calendar
|
||||
GregorianCalendar c = new GregorianCalendar();
|
||||
c.set(1856, 7, 9);
|
||||
|
||||
// The constructor arguments are name, birthday, and nationality.
|
||||
Inventor tesla = new Inventor("Nikola Tesla", c.getTime(), "Serbian");
|
||||
|
||||
ExpressionParser parser = new SpelExpressionParser();
|
||||
|
||||
Expression exp = parser.parseExpression("name"); // Parse name as an expression
|
||||
String name = (String) exp.getValue(tesla);
|
||||
// name == "Nikola Tesla"
|
||||
|
||||
exp = parser.parseExpression("name == 'Nikola Tesla'");
|
||||
boolean result = exp.getValue(tesla, Boolean.class);
|
||||
// result == true
|
||||
```
|
||||
|
||||
### EvaluationContext
|
||||
EvalutaionContext用于给表达式提供一个执行的上下文环境,通过EvalutaionContext,可以向context中放置一个变量,并且在后续中使用该变量:
|
||||
```java
|
||||
// 向context中放入一个list变量
|
||||
ctx.setVariable("list",list)
|
||||
|
||||
// 在后续表达式中即可使用放入的list变量
|
||||
// 获取放入context中的list变量的值
|
||||
parser.parseExpression("#list[0]").getValue(ctx);
|
||||
// 设置context中list变量的值
|
||||
parser.parseExpression("#list[0]").setValue(ctx , "false");
|
||||
```
|
||||
#### EvaluationContext实现类
|
||||
- SimpleEvaluationContext:实现了Spel语言的部分特性
|
||||
- StandardEvaluationContext:实现了Spel语言的全部特性
|
||||
|
||||
### TypeConversion
|
||||
默认情况下,spel会使用`org.springframework.core.convert.ConversionService`中的Conversion Service。该conversion service包含内置的类型转换,并且可以为类型之间指定自定义的类型转换。该转换是支持泛型的。
|
||||
|
||||
如下为支持泛型的示例:
|
||||
```java
|
||||
class Simple {
|
||||
public List<Boolean> booleanList = new ArrayList<>();
|
||||
}
|
||||
|
||||
Simple simple = new Simple();
|
||||
simple.booleanList.add(true);
|
||||
|
||||
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
|
||||
|
||||
// "false" is passed in here as a String. SpEL and the conversion service
|
||||
// will recognize that it needs to be a Boolean and convert it accordingly.
|
||||
parser.parseExpression("booleanList[0]").setValue(context, simple, "false");
|
||||
|
||||
// b is false
|
||||
Boolean b = simple.booleanList.get(0);
|
||||
```
|
||||
|
||||
### 在定义Bean对象时使用Spel表达式
|
||||
可以通过@Value注解来为bean对象的域、方法参数、构造器参数指定默认值。通过如下方式来指定表达式:
|
||||
- `#{ <expression string> }.`
|
||||
|
||||
#### 为field指定默认值
|
||||
```java
|
||||
public class FieldValueTestBean {
|
||||
|
||||
@Value("#{ systemProperties['user.region'] }")
|
||||
private String defaultLocale;
|
||||
|
||||
public void setDefaultLocale(String defaultLocale) {
|
||||
this.defaultLocale = defaultLocale;
|
||||
}
|
||||
|
||||
public String getDefaultLocale() {
|
||||
return this.defaultLocale;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 通过setter为field指定默认值
|
||||
```java
|
||||
public class PropertyValueTestBean {
|
||||
|
||||
private String defaultLocale;
|
||||
|
||||
@Value("#{ systemProperties['user.region'] }")
|
||||
public void setDefaultLocale(String defaultLocale) {
|
||||
this.defaultLocale = defaultLocale;
|
||||
}
|
||||
|
||||
public String getDefaultLocale() {
|
||||
return this.defaultLocale;
|
||||
}
|
||||
}
|
||||
```
|
||||
#### @Autowired方法和构造器
|
||||
@Autowired方法和构造器可以使用@Value注解为参数指定默认值:
|
||||
```java
|
||||
public class SimpleMovieLister {
|
||||
|
||||
private MovieFinder movieFinder;
|
||||
private String defaultLocale;
|
||||
|
||||
@Autowired
|
||||
public void configure(MovieFinder movieFinder,
|
||||
@Value("#{ systemProperties['user.region'] }") String defaultLocale) {
|
||||
this.movieFinder = movieFinder;
|
||||
this.defaultLocale = defaultLocale;
|
||||
}
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
```java
|
||||
public class MovieRecommender {
|
||||
|
||||
private String defaultLocale;
|
||||
|
||||
private CustomerPreferenceDao customerPreferenceDao;
|
||||
|
||||
public MovieRecommender(CustomerPreferenceDao customerPreferenceDao,
|
||||
@Value("#{systemProperties['user.country']}") String defaultLocale) {
|
||||
this.customerPreferenceDao = customerPreferenceDao;
|
||||
this.defaultLocale = defaultLocale;
|
||||
}
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
## Spel语法
|
||||
### 字面量表达式
|
||||
字面量表达式支持字符串、数值(整数、实数、十六进制数)、boolean类型和null。
|
||||
#### 字符串类型字面量
|
||||
其中字符串类型的字面量通过单引号包围,如果想要将单引号本身放入字符串字面量中,可以使用两个单引号:
|
||||
```java
|
||||
// 该值为h''h
|
||||
parser.parseExpression("'h''''h'").getValue()
|
||||
```
|
||||
|
||||
字面量的使用如下所示:
|
||||
```java
|
||||
ExpressionParser parser = new SpelExpressionParser();
|
||||
|
||||
// evals to "Hello World"
|
||||
String helloWorld = (String) parser.parseExpression("'Hello World'").getValue();
|
||||
|
||||
double avogadrosNumber = (Double) parser.parseExpression("6.0221415E+23").getValue();
|
||||
|
||||
// evals to 2147483647
|
||||
int maxValue = (Integer) parser.parseExpression("0x7FFFFFFF").getValue();
|
||||
|
||||
boolean trueValue = (Boolean) parser.parseExpression("true").getValue();
|
||||
|
||||
Object nullValue = parser.parseExpression("null").getValue();
|
||||
```
|
||||
### Properties,Array,List,Map,Indexer
|
||||
#### 访问属性
|
||||
```java
|
||||
int year = (Integer) parser.parseExpression("Birthdate.Year + 1900").getValue(context);
|
||||
|
||||
|
||||
String city = (String) parser.parseExpression("placeOfBirth.City").getValue(context);
|
||||
```
|
||||
属性名的第一个字符不区分大小写。
|
||||
#### Array
|
||||
可以通过方括号来访问array和list中的元素内容:
|
||||
```java
|
||||
ExpressionParser parser = new SpelExpressionParser();
|
||||
|
||||
// Inventions Array
|
||||
StandardEvaluationContext teslaContext = new StandardEvaluationContext(tesla);
|
||||
|
||||
// evaluates to "Induction motor"
|
||||
String invention = parser.parseExpression("inventions[3]").getValue(teslaContext,
|
||||
String.class);
|
||||
|
||||
|
||||
// Members List
|
||||
StandardEvaluationContext societyContext = new StandardEvaluationContext(ieee);
|
||||
|
||||
// evaluates to "Nikola Tesla"
|
||||
String name = parser.parseExpression("Members[0].Name").getValue(societyContext, String.class);
|
||||
|
||||
// List and Array navigation
|
||||
// evaluates to "Wireless communication"
|
||||
String invention = parser.parseExpression("Members[0].Inventions[6]").getValue(societyContext,String.class);
|
||||
```
|
||||
#### Map
|
||||
map中的值可以在方括号中指定字面量key来访问,示例如下所示:
|
||||
```java
|
||||
// Officer's Dictionary
|
||||
|
||||
Inventor pupin = parser.parseExpression("Officers['president']").getValue(societyContext,Inventor.class);
|
||||
|
||||
// evaluates to "Idvor"
|
||||
String city =
|
||||
parser.parseExpression("Officers['president'].PlaceOfBirth.City").getValue(societyContext,
|
||||
String.class);
|
||||
|
||||
// setting values
|
||||
parser.parseExpression("Officers['advisors'][0].PlaceOfBirth.Country").setValue(societyContext,"Croatia");
|
||||
```
|
||||
#### 列表表示
|
||||
列表可以直接通过如下方式来表示:
|
||||
```java
|
||||
// evaluates to a Java list containing the four numbers
|
||||
List numbers = (List) parser.parseExpression("{1,2,3,4}").getValue(context);
|
||||
|
||||
List listOfLists = (List) parser.parseExpression("{{'a','b'},{'x','y'}}").getValue(context);
|
||||
```
|
||||
#### 数组构建
|
||||
spel表达式中数组构建可以使用java语法:
|
||||
```java
|
||||
int[] numbers1 = (int[]) parser.parseExpression("new int[4]").getValue(context);
|
||||
|
||||
// Array with initializer
|
||||
int[] numbers2 = (int[]) parser.parseExpression("new int[]{1,2,3}").getValue(context);
|
||||
|
||||
// Multi dimensional array
|
||||
int[][] numbers3 = (int[][]) parser.parseExpression("new int[4][5]").getValue(context);
|
||||
```
|
||||
### 方法调用
|
||||
表达式中方法调用也可以使用java中的语法:
|
||||
```java
|
||||
// string literal, evaluates to "bc"
|
||||
String c = parser.parseExpression("'abc'.substring(2, 3)").getValue(String.class);
|
||||
|
||||
// evaluates to true
|
||||
boolean isMember = parser.parseExpression("isMember('Mihajlo Pupin')").getValue(societyContext, Boolean.class);
|
||||
```
|
||||
### 操作符
|
||||
#### 关系操作符
|
||||
关系操作符示例如下:
|
||||
```java
|
||||
// evaluates to true
|
||||
boolean trueValue = parser.parseExpression("2 == 2").getValue(Boolean.class);
|
||||
|
||||
// evaluates to false
|
||||
boolean falseValue = parser.parseExpression("2 < -5.0").getValue(Boolean.class);
|
||||
|
||||
// evaluates to true
|
||||
boolean trueValue = parser.parseExpression("'black' < 'block'").getValue(Boolean.class);
|
||||
```
|
||||
|
||||
与null比较时的规则:
|
||||
- 任何值都比null大,`X>null`永远为true
|
||||
- 没有任何值比null小,`X<null`永远为false
|
||||
|
||||
#### instanceof操作符和matches操作符
|
||||
```java
|
||||
// evaluates to false
|
||||
boolean falseValue = parser.parseExpression("'xyz' instanceof T(int)").getValue(Boolean.class);
|
||||
|
||||
// evaluates to true
|
||||
boolean trueValue =
|
||||
parser.parseExpression("'5.00' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean.class);
|
||||
|
||||
//evaluates to false
|
||||
boolean falseValue =
|
||||
parser.parseExpression("'5.0067' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean.class);
|
||||
```
|
||||
#### 逻辑操作符
|
||||
逻辑操作符支持and、or、not
|
||||
```java
|
||||
// -- AND --
|
||||
|
||||
// evaluates to false
|
||||
boolean falseValue = parser.parseExpression("true and false").getValue(Boolean.class);
|
||||
|
||||
// evaluates to true
|
||||
String expression = "isMember('Nikola Tesla') and isMember('Mihajlo Pupin')";
|
||||
boolean trueValue = parser.parseExpression(expression).getValue(societyContext, Boolean.class);
|
||||
|
||||
// -- OR --
|
||||
|
||||
// evaluates to true
|
||||
boolean trueValue = parser.parseExpression("true or false").getValue(Boolean.class);
|
||||
|
||||
// evaluates to true
|
||||
String expression = "isMember('Nikola Tesla') or isMember('Albert Einstein')";
|
||||
boolean trueValue = parser.parseExpression(expression).getValue(societyContext, Boolean.class);
|
||||
|
||||
// -- NOT --
|
||||
|
||||
// evaluates to false
|
||||
boolean falseValue = parser.parseExpression("!true").getValue(Boolean.class);
|
||||
|
||||
|
||||
// -- AND and NOT --
|
||||
String expression = "isMember('Nikola Tesla') and !isMember('Mihajlo Pupin')";
|
||||
boolean falseValue = parser.parseExpression(expression).getValue(societyContext, Boolean.class);
|
||||
```
|
||||
#### 数学运算符
|
||||
```java
|
||||
// Addition
|
||||
int two = parser.parseExpression("1 + 1").getValue(Integer.class); // 2
|
||||
|
||||
String testString =
|
||||
parser.parseExpression("'test' + ' ' + 'string'").getValue(String.class); // 'test string'
|
||||
|
||||
// Subtraction
|
||||
int four = parser.parseExpression("1 - -3").getValue(Integer.class); // 4
|
||||
|
||||
double d = parser.parseExpression("1000.00 - 1e4").getValue(Double.class); // -9000
|
||||
|
||||
// Multiplication
|
||||
int six = parser.parseExpression("-2 * -3").getValue(Integer.class); // 6
|
||||
|
||||
double twentyFour = parser.parseExpression("2.0 * 3e0 * 4").getValue(Double.class); // 24.0
|
||||
|
||||
// Division
|
||||
int minusTwo = parser.parseExpression("6 / -3").getValue(Integer.class); // -2
|
||||
|
||||
double one = parser.parseExpression("8.0 / 4e0 / 2").getValue(Double.class); // 1.0
|
||||
|
||||
// Modulus
|
||||
int three = parser.parseExpression("7 % 4").getValue(Integer.class); // 3
|
||||
|
||||
int one = parser.parseExpression("8 / 5 % 2").getValue(Integer.class); // 1
|
||||
|
||||
// Operator precedence
|
||||
int minusTwentyOne = parser.parseExpression("1+2-3*8").getValue(Integer.class); // -21
|
||||
```
|
||||
#### 赋值运算符
|
||||
```java
|
||||
Inventor inventor = new Inventor();
|
||||
StandardEvaluationContext inventorContext = new StandardEvaluationContext(inventor);
|
||||
|
||||
parser.parseExpression("Name").setValue(inventorContext, "Alexander Seovic2");
|
||||
|
||||
// alternatively
|
||||
|
||||
String aleks = parser.parseExpression("Name = 'Alexandar Seovic'").getValue(inventorContext,
|
||||
String.class);
|
||||
|
||||
```
|
||||
#### 类型操作符
|
||||
T操作符会获取一个class对象,在传入T的类型为java.lang下类时,不用指定全类名,否则应指定全类名.
|
||||
类中的静态方法也可以通过该操作符进行调用:
|
||||
```java
|
||||
Class dateClass = parser.parseExpression("T(java.util.Date)").getValue(Class.class);
|
||||
|
||||
Class stringClass = parser.parseExpression("T(String)").getValue(Class.class);
|
||||
|
||||
boolean trueValue =
|
||||
parser.parseExpression("T(java.math.RoundingMode).CEILING < T(java.math.RoundingMode).FLOOR")
|
||||
.getValue(Boolean.class);
|
||||
```
|
||||
|
||||
#### 构造方法
|
||||
可以通过new操作符来调用构造方法,在调用构造方法需要指定全类名,但是基类和String可以不指定全类名:
|
||||
```java
|
||||
Inventor einstein =
|
||||
p.parseExpression("new org.spring.samples.spel.inventor.Inventor('Albert Einstein',
|
||||
'German')")
|
||||
.getValue(Inventor.class);
|
||||
|
||||
//create new inventor instance within add method of List
|
||||
p.parseExpression("Members.add(new org.spring.samples.spel.inventor.Inventor('Albert Einstein',
|
||||
'German'))")
|
||||
.getValue(societyContext);
|
||||
|
||||
```
|
||||
### 变量
|
||||
可以通过`#argName`语法来引用变量,可以通过`StandardEvaluationContext.setVariable`方法来设置变量:
|
||||
```java
|
||||
Inventor tesla = new Inventor("Nikola Tesla", "Serbian");
|
||||
StandardEvaluationContext context = new StandardEvaluationContext(tesla);
|
||||
context.setVariable("newName", "Mike Tesla");
|
||||
|
||||
parser.parseExpression("Name = #newName").getValue(context);
|
||||
|
||||
System.out.println(tesla.getName()) // "Mike Tesla"
|
||||
```
|
||||
#### #this and #root
|
||||
#this变量一直都是被定义的,用于引用当前评估对象,#root变量也是一直都被定义的,引用root context对象。
|
||||
```java
|
||||
// create an array of integers
|
||||
List<Integer> primes = new ArrayList<Integer>();
|
||||
primes.addAll(Arrays.asList(2,3,5,7,11,13,17));
|
||||
|
||||
// create parser and set variable 'primes' as the array of integers
|
||||
ExpressionParser parser = new SpelExpressionParser();
|
||||
StandardEvaluationContext context = new StandardEvaluationContext();
|
||||
context.setVariable("primes",primes);
|
||||
|
||||
// all prime numbers > 10 from the list (using selection ?{...})
|
||||
// evaluates to [11, 13, 17]
|
||||
List<Integer> primesGreaterThanTen =
|
||||
(List<Integer>) parser.parseExpression("#primes.?[#this>10]").getValue(context);
|
||||
```
|
||||
|
||||
### 方法注册
|
||||
可以向StandardEvaluationContext中注册方法,注册的方法可以在spel中使用:
|
||||
```java
|
||||
public abstract class StringUtils {
|
||||
|
||||
public static String reverseString(String input) {
|
||||
StringBuilder backwards = new StringBuilder();
|
||||
for (int i = 0; i < input.length(); i++)
|
||||
backwards.append(input.charAt(input.length() - 1 - i));
|
||||
}
|
||||
return backwards.toString();
|
||||
}
|
||||
}
|
||||
```
|
||||
注册方法到context中:
|
||||
```java
|
||||
ExpressionParser parser = new SpelExpressionParser();
|
||||
StandardEvaluationContext context = new StandardEvaluationContext();
|
||||
|
||||
context.registerFunction("reverseString",
|
||||
StringUtils.class.getDeclaredMethod("reverseString",
|
||||
new Class[] { String.class }));
|
||||
|
||||
String helloWorldReversed =
|
||||
parser.parseExpression("#reverseString('hello')").getValue(context, String.class);
|
||||
```
|
||||
### 引用bean对象
|
||||
如果为context配置了bean resolver,可以通过`@`语法来查询bean对象:
|
||||
```java
|
||||
ExpressionParser parser = new SpelExpressionParser();
|
||||
StandardEvaluationContext context = new StandardEvaluationContext();
|
||||
context.setBeanResolver(new MyBeanResolver());
|
||||
|
||||
// This will end up calling resolve(context,"foo") on MyBeanResolver during evaluation
|
||||
Object bean = parser.parseExpression("@foo").getValue(context);
|
||||
```
|
||||
### 三元操作符
|
||||
```java
|
||||
parser.parseExpression("Name").setValue(societyContext, "IEEE");
|
||||
societyContext.setVariable("queryName", "Nikola Tesla");
|
||||
|
||||
expression = "isMember(#queryName)? #queryName + ' is a member of the ' " +
|
||||
"+ Name + ' Society' : #queryName + ' is not a member of the ' + Name + ' Society'";
|
||||
|
||||
String queryResultString =
|
||||
parser.parseExpression(expression).getValue(societyContext, String.class);
|
||||
// queryResultString = "Nikola Tesla is a member of the IEEE Society"
|
||||
```
|
||||
### Elvis Operator
|
||||
类似于`Optional.ofNullable(var).orElse()`,`?:`表达式可以达成同样效果:
|
||||
```java
|
||||
ExpressionParser parser = new SpelExpressionParser();
|
||||
|
||||
Inventor tesla = new Inventor("Nikola Tesla", "Serbian");
|
||||
StandardEvaluationContext context = new StandardEvaluationContext(tesla);
|
||||
|
||||
String name = parser.parseExpression("Name?:'Elvis Presley'").getValue(context, String.class);
|
||||
|
||||
System.out.println(name); // Nikola Tesla
|
||||
|
||||
tesla.setName(null);
|
||||
|
||||
name = parser.parseExpression("Name?:'Elvis Presley'").getValue(context, String.class);
|
||||
|
||||
System.out.println(name); // Elvis Presley
|
||||
```
|
||||
### 空安全操作符
|
||||
`?.`操作符和js中的类似:
|
||||
```java
|
||||
ExpressionParser parser = new SpelExpressionParser();
|
||||
|
||||
Inventor tesla = new Inventor("Nikola Tesla", "Serbian");
|
||||
tesla.setPlaceOfBirth(new PlaceOfBirth("Smiljan"));
|
||||
|
||||
StandardEvaluationContext context = new StandardEvaluationContext(tesla);
|
||||
|
||||
String city = parser.parseExpression("PlaceOfBirth?.City").getValue(context, String.class);
|
||||
System.out.println(city); // Smiljan
|
||||
|
||||
tesla.setPlaceOfBirth(null);
|
||||
|
||||
city = parser.parseExpression("PlaceOfBirth?.City").getValue(context, String.class);
|
||||
|
||||
System.out.println(city); // null - does not throw NullPointerException!!!
|
||||
```
|
||||
### 集合过滤操作符
|
||||
`?[selectionExpression]`可以完成集合的过滤,该操作符可以同时针对list和map,在针对map操作时,操作的是`Map.Entry`,entry的key和value都可以作为属性访问。
|
||||
```java
|
||||
// 针对list
|
||||
List<Inventor> list = (List<Inventor>)
|
||||
parser.parseExpression("Members.?[Nationality == 'Serbian']").getValue(societyContext);
|
||||
|
||||
// 针对map
|
||||
Map newMap = parser.parseExpression("map.?[value<27]").getValue();
|
||||
```
|
||||
### 集合映射
|
||||
类似于Stream.map,spel支持集合映射操作符
|
||||
```java
|
||||
// returns [ 'Smiljan', 'Idvor' ]
|
||||
// 将Inventor集合映射为city集合
|
||||
List placesOfBirth = (List)parser.parseExpression("Members.![placeOfBirth.city]");
|
||||
```
|
||||
### 表达式模板
|
||||
表达式模板允许将字面量文本和一个或多个评估component混合在一起,评估块语法类似于`${}`
|
||||
```java
|
||||
String randomPhrase =
|
||||
parser.parseExpression("random number is #{T(java.lang.Math).random()}",
|
||||
new TemplateParserContext()).getValue(String.class);
|
||||
|
||||
// evaluates to "random number is 0.7038186818312008"
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
173
spring/spring cache/spring cache.md
Normal file
173
spring/spring cache/spring cache.md
Normal file
@@ -0,0 +1,173 @@
|
||||
- [Spring Cache](#spring-cache)
|
||||
- [Cache Abstract](#cache-abstract)
|
||||
- [使用cache abstraction的要点](#使用cache-abstraction的要点)
|
||||
- [缓存可能会存在的问题](#缓存可能会存在的问题)
|
||||
- [基于声明式注解的缓存](#基于声明式注解的缓存)
|
||||
- [@Cacheable](#cacheable)
|
||||
- [默认的key生成](#默认的key生成)
|
||||
- [自定义key生成的方式](#自定义key生成的方式)
|
||||
- [sync caching](#sync-caching)
|
||||
- [condition cache](#condition-cache)
|
||||
- [@Cacheable和Optional](#cacheable和optional)
|
||||
- [@CachePut](#cacheput)
|
||||
- [@CacheEvict](#cacheevict)
|
||||
- [@Caching](#caching)
|
||||
- [@CacheConfig](#cacheconfig)
|
||||
- [@EnableCaching](#enablecaching)
|
||||
- [配置cache 存储](#配置cache-存储)
|
||||
- [Cache Redis](#cache-redis)
|
||||
- [限制cache-names](#限制cache-names)
|
||||
|
||||
# Spring Cache
|
||||
## Cache Abstract
|
||||
Cache针对java方法进行缓存,当想要获取的信息在cache中可获取时,可以从cache中进行获取,从而降低了java方法的执行次数。
|
||||
每次在目标方法执行时,cache abstraction应用了一个cache check,检查该方法是否已经通过指定参数调用过。如果该方法已经通过指定参数调用过,会直接从缓存中获取已缓存的执行结果,而无需重复的调用方法。如果该方法尚未被调用过,那么调用该方法,并且将该方法的返回结果添加到缓存中,从而在方法下次调用时直接从cache中获取值。
|
||||
通过cache,可以缓存开销较高的方法结果(io开销较高和cpu开销较高的方法结果均可被缓存),cache逻辑对于调用者来说是透明的。
|
||||
> cache abstraction提供了一系列缓存相关的api,可以对cache内容进行更新或是移除,当缓存数据内容会改变的情况下这些api会很有用。
|
||||
|
||||
在spring中,cache abstraction的caching service是一个抽象接口而不是实现,需要使用实际的storage来对缓存数据进行存储。
|
||||
spring提供了cache abstraction的一系列实现,例如基于JDK java.util.concurrent.ConcurrentMap的缓存、Caffeine等
|
||||
### 使用cache abstraction的要点
|
||||
1. cache声明:表示需要使用cache的方法,并且指明cache策略
|
||||
2. cache配置:将cache数据存储到何处,并从何处读取
|
||||
## 缓存可能会存在的问题
|
||||
在使用缓存时,可能会面临如下问题:
|
||||
- 多进程环境问题:在多进程环境之下(如微服务在多个server上部署服务)使用缓存时,如果想要缓存在多个节点之间进行共享,需要解决缓存的共享问题
|
||||
- 并发安全问题:使用缓存实际上是get-if-not-found-then-proceed-and-put-eventually的过程,在这个过程中并不会加锁,故而会存在并发安全的问题。如果多个线程都同时发生缓存miss的问题,那么同一个方法会背调用多次并且返回结果会背load多次;淘汰时同样会面临竞争问题,并发环境下如果有一个线程想要淘汰过时key,但另一个线程load了最新的value并更新ttl,那么更新后的key仍有可能被淘汰。
|
||||
## 基于声明式注解的缓存
|
||||
### @Cacheable
|
||||
可以使用@Cacheable将方法声明为可缓存的,在将方法声明为@Cacheable之后,方法的返回结果会被存储到缓存中。在一系列具有相同参数的调用中,cache中缓存的值将会被返回,而无需真的调用方法。
|
||||
在@Cacheable注解最简单的使用场景中,需要指明和方法关联的cache name:
|
||||
```java
|
||||
// 如下示例中,findBook方法将会和名为books的缓存相关联
|
||||
@Cacheable("books")
|
||||
public Book findBook(ISBN isbn) {...}
|
||||
```
|
||||
> 虽然大多数场景下@Cacheable注解只会指定一个cache name,但是**也能为@Cacheable注解指定多个cache**。当任一缓存命中时,关联的值都会被返回。
|
||||
> 即使其他缓存没有命中,该命中值也会添加到所有其他缓存中(方法并未被实际调用)
|
||||
|
||||
如下实例为@Cacheable方法指定了多个cache:
|
||||
```java
|
||||
@Cacheable({"books", "isbns"})
|
||||
public Book findBook(ISBN isbn) {...}
|
||||
```
|
||||
#### 默认的key生成
|
||||
缓存实际上是通过key-value的形式存储的,每次对cached method的调用都需要被转化为一个合适的key。cache abstraction通过使用SimpleKeyGenerator来生成key,该generator生成算法如下:
|
||||
- 如果没有给定参数,那么返回SimpleKey.EMPTY
|
||||
- 如果给定了一个参数,返回该参数
|
||||
- 如果指定了多于一个参数,那么返回一个包含所有参数的SimpleKey
|
||||
|
||||
该算法在大多数用例之下运行正常,只要参数含有正常key并且具有有效的hashCode和equals实现。
|
||||
#### 自定义key生成的方式
|
||||
当在cached method的参数中,仅仅只有一部分参数应参与key的生成,另一部分不参与时,可以通过指定@Cacheable注解的key属性来指定如何生成key。可以在key属性中使用spel表达式:
|
||||
```java
|
||||
@Cacheable(cacheNames="books", key="#isbn")
|
||||
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
|
||||
|
||||
@Cacheable(cacheNames="books", key="#isbn.rawNumber")
|
||||
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
|
||||
|
||||
@Cacheable(cacheNames="books", key="T(someType).hash(#isbn)")
|
||||
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
|
||||
```
|
||||
#### sync caching
|
||||
在使用@Cacheable时,默认情况下cached method并没有使用任何lock。可以使用@Cacheable注解的sync属性来指示底层的缓存provider在value被计算时锁定指定条目(防止在竞争场景下同一个值被重复计算)。
|
||||
```java
|
||||
@Cacheable(cacheNames="foos", sync=true)
|
||||
public Foo executeExpensiveOperation(String id) {...}
|
||||
```
|
||||
#### condition cache
|
||||
可以通过指定@Cacheable注解的condition属性来指定该方法调用是否被缓存。condition可以为其指定一个spel表达式,如果表达式结果为true,该方法调用被缓存,否则不被缓存。
|
||||
```java
|
||||
// 仅当name参数的长度小于32时被缓存
|
||||
@Cacheable(cacheNames="book", condition="#name.length() < 32")
|
||||
public Book findBook(String name)
|
||||
```
|
||||
还可以使用unless属性来阻止方法被缓存,unless同样可以为其指定一个spel表达式。unless在方法调用完成之后被计算,可以通过#result来根据方法返回结果决定是否缓存该方法:
|
||||
```java
|
||||
@Cacheable(cacheNames="book", condition="#name.length() < 32", unless="#result.hardback")
|
||||
public Book findBook(String name)
|
||||
```
|
||||
#### @Cacheable和Optional
|
||||
cache abstraction支持Optional类型,如果Optional对象的value不为空,那么value将会被存储到cache中,如果Optional为空,那么null将会被存储到管来奶的cache中。
|
||||
在@Cacheable的spel表达式中,#result获取的是Optional对象的value而不是wrapper。
|
||||
```java
|
||||
@Cacheable(cacheNames="book", condition="#name.length() < 32", unless="#result?.hardback")
|
||||
public Optional<Book> findBook(String name)
|
||||
```
|
||||
### @CachePut
|
||||
在不影响方法执行的情况下,要将方法的执行结果更新到cache中,可以使用@CachePut注解。标注了@CachePut的方法总会被执行,并且执行的返回结果会被更新到cache中。
|
||||
@CachePut注解支持和@Cacheable相同的参数,且@CachePut用于的是缓存的注入,而不应用于优化方法流程。
|
||||
@CachePut的使用如下所示:
|
||||
```java
|
||||
@CachePut(cacheNames="book", key="#isbn")
|
||||
public Book updateBook(ISBN isbn, BookDescriptor descriptor)
|
||||
```
|
||||
> @CachePut注解和@Cacheable注解通常情况下不应用于同一方法上
|
||||
|
||||
### @CacheEvict
|
||||
cache abstraction不仅支持缓存的注入,而且支持缓存的淘汰,该淘汰过程可以从缓存中移除过时或未使用的数据。
|
||||
@CacheEvict注解具有allEntries属性,将该属性标记为true可以逐出所有的缓存条目:
|
||||
```java
|
||||
// 该场景下,cache中的条目将会被清空
|
||||
// 该场景下并不是依此逐出所有的key,而是一次性清空所有缓存
|
||||
@CacheEvict(cacheNames="books", allEntries=true)
|
||||
public void loadBooks(InputStream batch)
|
||||
```
|
||||
可以为@CacheEvict注解指定beforeInvocation属性,来指定该eviction行为是否应该在@CacheEvict方法被调用之前执行:
|
||||
- 默认情况下,beforeInvocation属性的默认值为false,eviction操作在方法成功返回之后才执行(如果该方法被cached或执行时抛出异常,eviction操作不会执行)
|
||||
- 如果beforeInvocation属性被设置为true,代表该eviction行为在方法被调用之前就会被执行
|
||||
### @Caching
|
||||
如果想要指定同一类型的多个注解(例如@CacheEvict或CachePut),例如对不同的cache,condition和key expression不同。
|
||||
@Caching注解允许对同一方法内嵌多个@Cacheable、@CachePut、@CacheEvict注解,使用示例如下:
|
||||
```java
|
||||
@Caching(evict = { @CacheEvict("primary"), @CacheEvict(cacheNames="secondary", key="#p0") })
|
||||
public Book importBooks(String deposit, Date date)
|
||||
```
|
||||
### @CacheConfig
|
||||
在使用cache注解时,可能在同一个类中需要为所有cache operation指定相同的选项(例如为类中所有的cache注解指定相同的cache name)。此时,可以通过一个class-level的@CacheConfig注解来指定类中所有cache operation的相同选项:
|
||||
```java
|
||||
@CacheConfig("books")
|
||||
public class BookRepositoryImpl implements BookRepository {
|
||||
|
||||
@Cacheable
|
||||
public Book findBook(ISBN isbn) {...}
|
||||
}
|
||||
```
|
||||
> 通过@CacheConfig注解指定的属性可以在cache operation中被覆盖
|
||||
|
||||
### @EnableCaching
|
||||
想要在项目中启用所有的缓存注解,需要在一个@Configuration类上指定@EnableCaching注解
|
||||
## 配置cache 存储
|
||||
cache abstraction提供了一些存储集成选项。如果没有指定任何cache library,那么spring boot会自动装配一个simple provider,该provider使用concurrent map来进行缓存存储。当需要一个cache时,cache provider会创建一个cache并返回给调用者。
|
||||
### Cache Redis
|
||||
如果redis在项目中配置并且可以获取,那么spring boot会自动配置一个RedisCacheManager。可以在启动时配置其他cache,通过在properties文件中指定spring.cache.cache-names属性,并且cache的默认属性可以通过spring.cache.redis.*属性来进行配置:
|
||||
```properties
|
||||
spring.cache.cache-names=cache1,cache2
|
||||
spring.cache.redis.time-to-live=10m
|
||||
```
|
||||
> 默认情况下,对于不同的cache,会添加一个key前缀,继而如果两个不同的cache使用相同的key,不会出现key冲突的情况
|
||||
|
||||
如果想要更细粒度的对cache进行配置,可以通过如下方式进行配置:
|
||||
```java
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
public class MyRedisCacheManagerConfiguration {
|
||||
|
||||
@Bean
|
||||
public RedisCacheManagerBuilderCustomizer myRedisCacheManagerBuilderCustomizer() {
|
||||
return (builder) -> builder
|
||||
.withCacheConfiguration("cache1", RedisCacheConfiguration
|
||||
.defaultCacheConfig().entryTtl(Duration.ofSeconds(10)))
|
||||
.withCacheConfiguration("cache2", RedisCacheConfiguration
|
||||
.defaultCacheConfig().entryTtl(Duration.ofMinutes(1)));
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
### 限制cache-names
|
||||
默认情况下,cache是在需要时才创建的,但如果想要限制可用的cache-names,可以设置`cache.cache-names`属性。
|
||||
在设置`cache.cache-names`属性之后,那么如果运行时应用想要获取cache-names之外的cache,将会在运行时抛出异常。
|
||||
```properties
|
||||
spring.cache.cache-names=cache1,cache2
|
||||
```
|
||||
117
spring/spring data r2dbc/r2dbc.md
Normal file
117
spring/spring data r2dbc/r2dbc.md
Normal file
@@ -0,0 +1,117 @@
|
||||
# R2DBC
|
||||
## Getting Started
|
||||
### 引入pom
|
||||
在spring boot项目中,如果要适配spring data r2dbc,可以引入如下pom依赖:
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-r2dbc</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.asyncer</groupId>
|
||||
<artifactId>r2dbc-mysql</artifactId>
|
||||
<version>1.4.1</version>
|
||||
</dependency>
|
||||
```
|
||||
### create pojo & create table
|
||||
之后,可以创建如下实体:
|
||||
```java
|
||||
public class Person {
|
||||
|
||||
private final String id;
|
||||
private final String name;
|
||||
private final int age;
|
||||
|
||||
public Person(String id, String name, int age) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.age = age;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public int getAge() {
|
||||
return age;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Person [id=" + id + ", name=" + name + ", age=" + age + "]";
|
||||
}
|
||||
}
|
||||
```
|
||||
创建table:
|
||||
```sql
|
||||
CREATE TABLE person(
|
||||
id VARCHAR(255) PRIMARY KEY,
|
||||
name VARCHAR(255),
|
||||
age INT
|
||||
);
|
||||
```
|
||||
### 声明ConnectionFactory
|
||||
```java
|
||||
@Configuration
|
||||
@Slf4j
|
||||
public class ConnectionFactoryConf extends AbstractR2dbcConfiguration {
|
||||
@Bean
|
||||
public ConnectionFactory connectionFactory() {
|
||||
// ConnectionFactories.get("jdbc:mysql://localhost:3306/innodb_demo?useSSL=false&rewriteBatchedStatements=true")
|
||||
// .
|
||||
ConnectionFactoryOptions options = ConnectionFactoryOptions.builder()
|
||||
.option(ConnectionFactoryOptions.HOST, "localhost")
|
||||
.option(ConnectionFactoryOptions.PORT, 3306)
|
||||
.option(ConnectionFactoryOptions.DRIVER, "mysql")
|
||||
.option(ConnectionFactoryOptions.USER, "rw_all_db")
|
||||
.option(ConnectionFactoryOptions.PASSWORD, "ab19971025")
|
||||
.option(ConnectionFactoryOptions.DATABASE, "asahi_schedule")
|
||||
.build();
|
||||
ConnectionPoolConfiguration poolConf = ConnectionPoolConfiguration.builder()
|
||||
.connectionFactory(ConnectionFactories.get(options))
|
||||
.maxSize(10)
|
||||
.validationQuery("SELECT 1")
|
||||
.build();
|
||||
return new ConnectionPool(poolConf);
|
||||
}
|
||||
}
|
||||
```
|
||||
在上述示例中,Conf类继承了AbstractR2dbcConfiguration类,相比于直接注入`connectionFactory`对象,通过AbstractR2dbcConfiguration注册能够提供ExceptionTranslator,将r2dbc中的异常翻译为DataAccessException。
|
||||
|
||||
### 通过R2dbcEntityTemplate操作数据库
|
||||
通过entityTemplate操作数据库的示例如下:
|
||||
```java
|
||||
R2dbcEntityTemplate template = new R2dbcEntityTemplate(connectionFactory);
|
||||
|
||||
template.getDatabaseClient().sql("CREATE TABLE person" +
|
||||
"(id VARCHAR(255) PRIMARY KEY," +
|
||||
"name VARCHAR(255)," +
|
||||
"age INT)")
|
||||
.fetch()
|
||||
.rowsUpdated()
|
||||
.as(StepVerifier::create)
|
||||
.expectNextCount(1)
|
||||
.verifyComplete();
|
||||
|
||||
template.insert(Person.class)
|
||||
.using(new Person("joe", "Joe", 34))
|
||||
.as(StepVerifier::create)
|
||||
.expectNextCount(1)
|
||||
.verifyComplete();
|
||||
|
||||
template.select(Person.class)
|
||||
.first()
|
||||
.doOnNext(it -> log.info(it))
|
||||
.as(StepVerifier::create)
|
||||
.expectNextCount(1)
|
||||
.verifyComplete();
|
||||
}
|
||||
```
|
||||
在上述示例中,pojo到table的映射会自动生效,并不需要额外的元数据。
|
||||
### Dialects
|
||||
spring data R2DBC会使用Dialect来对`特定数据库/driver的行为`进行封装。
|
||||
169
spring/spring email/spring email.md
Normal file
169
spring/spring email/spring email.md
Normal file
@@ -0,0 +1,169 @@
|
||||
- [Email](#email)
|
||||
- [使用示例](#使用示例)
|
||||
- [MailSender使用和SimpleMailMessage使用](#mailsender使用和simplemailmessage使用)
|
||||
- [使用JavaMailSender和MimeMessagePreparator](#使用javamailsender和mimemessagepreparator)
|
||||
- [使用MimeMessageHelper](#使用mimemessagehelper)
|
||||
- [发送附件和内联资源](#发送附件和内联资源)
|
||||
- [附件](#附件)
|
||||
- [inline resource](#inline-resource)
|
||||
- [Spring Boot集成](#spring-boot集成)
|
||||
|
||||
|
||||
# Email
|
||||
spring framework提供了工具库用于发送邮件,并且并不依赖具体的底层邮件系统。
|
||||
|
||||
`org.springframework.mail`包是spring邮件系统的root package,用于发送邮件的接口为MailSender。封装邮件属性的对象为`SimpleMailMessage`,其封装了邮件主要的属性(例如from和to)。
|
||||
|
||||
root package也包含了一套checked exception结构,root exception类型为`MailException`.
|
||||
|
||||
`org.springframework.mail.javamail.JavaMailSender`接口则是添加了专门的JavaMail特性,例如对MIME消息的支持(从MailSender接口继承)。`JavaMailSender`也提供了一个回调接口`org.springframework.mail.javamail.MimeMessagePreparator`用于准备Mime信息。
|
||||
|
||||
## 使用示例
|
||||
### MailSender使用和SimpleMailMessage使用
|
||||
```java
|
||||
import org.springframework.mail.MailException;
|
||||
import org.springframework.mail.MailSender;
|
||||
import org.springframework.mail.SimpleMailMessage;
|
||||
|
||||
public class SimpleOrderManager implements OrderManager {
|
||||
|
||||
private MailSender mailSender;
|
||||
private SimpleMailMessage templateMessage;
|
||||
|
||||
public void setMailSender(MailSender mailSender) {
|
||||
this.mailSender = mailSender;
|
||||
}
|
||||
|
||||
public void setTemplateMessage(SimpleMailMessage templateMessage) {
|
||||
this.templateMessage = templateMessage;
|
||||
}
|
||||
|
||||
public void placeOrder(Order order) {
|
||||
|
||||
// Do the business calculations...
|
||||
|
||||
// Call the collaborators to persist the order...
|
||||
|
||||
// Create a thread safe "copy" of the template message and customize it
|
||||
SimpleMailMessage msg = new SimpleMailMessage(this.templateMessage);
|
||||
msg.setTo(order.getCustomer().getEmailAddress());
|
||||
msg.setText(
|
||||
"Dear " + order.getCustomer().getFirstName()
|
||||
+ order.getCustomer().getLastName()
|
||||
+ ", thank you for placing order. Your order number is "
|
||||
+ order.getOrderNumber());
|
||||
try {
|
||||
this.mailSender.send(msg);
|
||||
}
|
||||
catch (MailException ex) {
|
||||
// simply log it and go on...
|
||||
System.err.println(ex.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
### 使用JavaMailSender和MimeMessagePreparator
|
||||
如下示例通过MimeMessagePreparator来发送MimeMessage
|
||||
```java
|
||||
public class SimpleOrderManager implements OrderManager {
|
||||
|
||||
private JavaMailSender mailSender;
|
||||
|
||||
public void setMailSender(JavaMailSender mailSender) {
|
||||
this.mailSender = mailSender;
|
||||
}
|
||||
|
||||
public void placeOrder(final Order order) {
|
||||
// Do the business calculations...
|
||||
// Call the collaborators to persist the order...
|
||||
|
||||
MimeMessagePreparator preparator = new MimeMessagePreparator() {
|
||||
public void prepare(MimeMessage mimeMessage) throws Exception {
|
||||
mimeMessage.setRecipient(Message.RecipientType.TO,
|
||||
new InternetAddress(order.getCustomer().getEmailAddress()));
|
||||
mimeMessage.setFrom(new InternetAddress("mail@mycompany.example"));
|
||||
mimeMessage.setText("Dear " + order.getCustomer().getFirstName() + " " +
|
||||
order.getCustomer().getLastName() + ", thanks for your order. " +
|
||||
"Your order number is " + order.getOrderNumber() + ".");
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
this.mailSender.send(preparator);
|
||||
}
|
||||
catch (MailException ex) {
|
||||
// simply log it and go on...
|
||||
System.err.println(ex.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
### 使用MimeMessageHelper
|
||||
通过使用`org.springframework.mail.javamail.MimeMessageHelper`,可以避免复杂的JavaMail API,通过其创建MimeMessage十分简单:
|
||||
```java
|
||||
// of course you would use DI in any real-world cases
|
||||
JavaMailSenderImpl sender = new JavaMailSenderImpl();
|
||||
sender.setHost("mail.host.com");
|
||||
|
||||
MimeMessage message = sender.createMimeMessage();
|
||||
MimeMessageHelper helper = new MimeMessageHelper(message);
|
||||
helper.setTo("test@host.com");
|
||||
helper.setText("Thank you for ordering!");
|
||||
|
||||
sender.send(message);
|
||||
```
|
||||
### 发送附件和内联资源
|
||||
多数邮件消息都允许使用附件和inline resource。inline resource包含图片、样式表等,但是不希望以附件的形式来显示inline resource。
|
||||
|
||||
#### 附件
|
||||
如下实例显示了如何通过`MimeMessageHelper`来发送带有图片附件的邮件:
|
||||
```java
|
||||
JavaMailSenderImpl sender = new JavaMailSenderImpl();
|
||||
sender.setHost("mail.host.com");
|
||||
|
||||
MimeMessage message = sender.createMimeMessage();
|
||||
|
||||
// use the true flag to indicate you need a multipart message
|
||||
MimeMessageHelper helper = new MimeMessageHelper(message, true);
|
||||
helper.setTo("test@host.com");
|
||||
|
||||
helper.setText("Check out this image!");
|
||||
|
||||
// let's attach the infamous windows Sample file (this time copied to c:/)
|
||||
FileSystemResource file = new FileSystemResource(new File("c:/Sample.jpg"));
|
||||
helper.addAttachment("CoolImage.jpg", file);
|
||||
|
||||
sender.send(message);
|
||||
```
|
||||
#### inline resource
|
||||
如下示例显示了如何通过MimeMessageHelper来发送带有inline resource的邮件:
|
||||
```java
|
||||
JavaMailSenderImpl sender = new JavaMailSenderImpl();
|
||||
sender.setHost("mail.host.com");
|
||||
|
||||
MimeMessage message = sender.createMimeMessage();
|
||||
|
||||
// use the true flag to indicate you need a multipart message
|
||||
MimeMessageHelper helper = new MimeMessageHelper(message, true);
|
||||
helper.setTo("test@host.com");
|
||||
|
||||
// use the true flag to indicate the text included is HTML
|
||||
helper.setText("<html><body><img src='cid:identifier1234'></body></html>", true);
|
||||
|
||||
// let's include the infamous windows Sample file (this time copied to c:/)
|
||||
FileSystemResource res = new FileSystemResource(new File("c:/Sample.jpg"));
|
||||
helper.addInline("identifier1234", res);
|
||||
|
||||
sender.send(message);
|
||||
```
|
||||
## Spring Boot集成
|
||||
如果`java.mail.host`和相应的库(`spring-boot-starter-mail`)存在,一个默认的JavaMailSender将会被自动创建(如果JavaMailSender不存在自己创建的bean对象)。
|
||||
|
||||
某些timeout值默认情况下是无限的,应该手动为其指定值,避免在邮件服务器未响应时线程被阻塞:
|
||||
```properties
|
||||
spring.mail.properties[mail.smtp.connectiontimeout]=5000
|
||||
spring.mail.properties[mail.smtp.timeout]=3000
|
||||
spring.mail.properties[mail.smtp.writetimeout]=5000
|
||||
```
|
||||
2394
spring/spring kafka/spring kafka.md
Normal file
2394
spring/spring kafka/spring kafka.md
Normal file
File diff suppressed because it is too large
Load Diff
539
spring/spring test/SpringTest.md
Normal file
539
spring/spring test/SpringTest.md
Normal file
@@ -0,0 +1,539 @@
|
||||
# Spirng Test
|
||||
## Unit Test
|
||||
### Introduce
|
||||
在运行单元测试时,无需实际启动容器,可以通过`mock objects`的方式来独立的对代码进行测试。故而,单元测试通常运行的十分快。
|
||||
|
||||
### Mock Objects
|
||||
spring包含如下专门用于`mock`的包
|
||||
- Environment
|
||||
- JNDI
|
||||
- Servlet API
|
||||
- Spring Web Reactive
|
||||
|
||||
#### Environment
|
||||
`org.springframework.mock.env`包中包含`Environment`和`PropertySource`抽象类的实现。在编写针对依赖环境变量代码的测试时,`MockEnvironment`和`MockPropertySource`将会非常有用。
|
||||
|
||||
#### JNDI
|
||||
`org.springframework.mock.jndi`包中包含`JNDI SPI`的部分实现,因而,可以建立一个简单的JNDI环境。
|
||||
|
||||
#### Servlet API
|
||||
`org.springframework.mock.web`包中包含一整套`servlet api mock object`,这些mock object针对spring web mvc框架使用。
|
||||
|
||||
### Unit Test Support Class
|
||||
spring中包含一系列类来帮助单元测试:
|
||||
- 通用测试组件
|
||||
- spring mvc测试组件
|
||||
|
||||
#### 通用测试组件
|
||||
`org.springframework.test.util`中包含一系列类用于单元测试和集成测试。
|
||||
|
||||
`AopTestUtils`中拥有一系列aop相关的方法。可以通过AopTestUtils中的方法获取隐藏在一层或多层代理下的`target`对象。
|
||||
|
||||
`ReflectionTestUtils`中拥有一系列反射相关的方法,可以通过ReflectionTestUtils中的方法修改常量值、访问非public的field或method。通常,ReflectionTestUtils可用于如下场景:
|
||||
- ORM框架中针对protected或private filed的访问
|
||||
- spring注解(@Autowired、@Inject,@Resource,@PostConstruct)中对于类中非公有成员的访问
|
||||
|
||||
`TestSocketUtils`可以查找本地可以连接的TCP端口,通常用于集成测试。
|
||||
|
||||
#### spring mvc测试组件
|
||||
`org.springframework.test.web`包包含了`ModelAndViewAssert`.
|
||||
|
||||
## 集成测试
|
||||
集成测试拥有如下特性:
|
||||
- 通过spring上下文正确注入
|
||||
- 可以通过JDBC或ORM工具进行数据库访问
|
||||
|
||||
集成测试会实际启动spring容器,故而速度要比单元测试慢。
|
||||
|
||||
### 集成测试主要目标
|
||||
集成测试主要支持如下目标:
|
||||
- 在测试之间管理ioc容器缓存
|
||||
- 在测试时提供依赖注入
|
||||
- 在集成测试时提供事务管理
|
||||
- 在编写集成测试时提供spring相关的类
|
||||
|
||||
### Context Management and Caching
|
||||
spring TestContext framework支持一致导入`ApplicationContext`和`WebApplicationContext`,并针对这些context做缓存。
|
||||
|
||||
> 支持对于已导入上下文的缓存是很重要的,因为spring实例化对象时花费的时间会很长。故而,如果针对每个测试的每个test fixture之前,都会加载对象的开销。
|
||||
|
||||
默认情况下,一旦`ApplicationContext`导入,那么每个test都会复用导入的上下文。故而,每个test suite都只会有一次导入开销,并且后续测试的执行要快得多。其中,`test suite`代表运行在相同jvm中的所有测试,
|
||||
|
||||
|
||||
### Test Fixtures依赖注入
|
||||
当TestContext framework导入应用上下文时,其可以针对test class使用依赖注入。并且,可以跨测试场景重复使用应用程序上下文,避免在独立的测试用例之间重复执行fixture设置。
|
||||
|
||||
### 事务管理
|
||||
TextContext framework默认会为每个test都创建并回滚一个事务。在编写test代码时,可以假定已经存在事务。默认情况下,test方法执行完后事务会被回滚,数据库状态会恢复到执行前的状态。
|
||||
|
||||
如果想要令test方法执行后事务被提交,可以使用`@Commit`注解。
|
||||
|
||||
### JDBC Support
|
||||
`org.springframework.test.jdbc`包中包含`JdbcTestUtils`,其中包含了一系列jdbc相关的工具方法。
|
||||
|
||||
## Spring TestContext Framework
|
||||
Spring TestContext Framework提供了通用的、注解驱动的单元测试和集成测试支持,并且该支持和底层的测试框架无关。并且,TestContext Framework的约定大于配置,并可以通过注解来覆盖默认值。
|
||||
|
||||
### Context Management
|
||||
每个`TestContext`都为其负责的测试类提供了上下文管理和caching支持。测试类实例并不直接访问配置好的`ApplicationContext`,但如果test class实现了`ApplciationContextAware`接口,那么测试类中将包含field指向ApplicationContext的引用。
|
||||
|
||||
在使用TestContext Framework时,test class并不需要继承特定类或实现特定接口来配置test class所属的application context,相应的,application context的配置是通过在类级别指定`@ContextConfiguration`来实现的。如果test class没有显式的声明application context resource location或component classes,那么配置好的`ContextLoader`将会决定如何从默认location或默认configuration classes中加载context。
|
||||
|
||||
#### Context Configuration with Xml Resource
|
||||
如果要通过xml配置的方式来导入`ApplicationContext`,可以指定`@ContextConfiguration`中的`location`属性,其是执行xml文件路径的数组。以`/`开头的路径代表classpath,而相对路径则是代表相对class文件的路径。示例如下:
|
||||
```java
|
||||
@ExtendWith(SpringExtension.class)
|
||||
// ApplicationContext will be loaded from "/app-config.xml" and
|
||||
// "/test-config.xml" in the root of the classpath
|
||||
@ContextConfiguration(locations = {"/app-config.xml", "/test-config.xml"})
|
||||
class MyTest {
|
||||
// class body...
|
||||
}
|
||||
```
|
||||
如果@ContextConfiguration省略了`location`和`value`属性,那么其默认会探测test class所在的路径。例如`com.example.MyTest`类,其默认会探测`classpath:com/example/MyTest-context.xml`。
|
||||
|
||||
#### Context Configuration with Component Classes
|
||||
为test加载ApplicationContext时,可以通过componet classes来实现。为此,可以为test class指定`@ConfigurationContext`注解,并且在注解的`classes`属性指定一个class数组,其实现如下:
|
||||
```java
|
||||
@ExtendWith(SpringExtension.class)
|
||||
// ApplicationContext will be loaded from AppConfig and TestConfig
|
||||
@ContextConfiguration(classes = {AppConfig.class, TestConfig.class})
|
||||
class MyTest {
|
||||
// class body...
|
||||
}
|
||||
```
|
||||
> 其中,component classes可以指定如下的任何一种对象:
|
||||
> - 有`@Configuration`注解的类
|
||||
> - 有`@Component`、`@Service`、`@Repository`等注解的类
|
||||
> - 有`@Bean`注解方法返回的bean对象所属类
|
||||
> - 其他任何被注册为spring bean对象的类
|
||||
|
||||
如果,在指定@ConfigurationContext注解时,省略了`classes`属性,那么TestContext Framework会尝试查找是否存在默认的classes。`AnnotationConfigContextLoader`和`AnnotationConfigWebContextLoader`会查找Test Class中所有的静态内部类,并判断其是否符合configuration class需求。
|
||||
|
||||
示例如下所示:
|
||||
```java
|
||||
@SpringJUnitConfig
|
||||
// ApplicationContext will be loaded from the static nested Config class
|
||||
class OrderServiceTest {
|
||||
|
||||
@Configuration
|
||||
static class Config {
|
||||
|
||||
// this bean will be injected into the OrderServiceTest class
|
||||
@Bean
|
||||
OrderService orderService() {
|
||||
OrderService orderService = new OrderServiceImpl();
|
||||
// set properties, etc.
|
||||
return orderService;
|
||||
}
|
||||
}
|
||||
|
||||
@Autowired
|
||||
OrderService orderService;
|
||||
|
||||
@Test
|
||||
void testOrderService() {
|
||||
// test the orderService
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
#### Context Configuration继承
|
||||
`@ContextConfiguration`支持从父类继承componet classes、resource locations、context initializers。可以指定`inheritLocations`和`inheritInitializers`来决定是否继承父类componet classes、resource locations、context initializers,两属性默认值为`true`。
|
||||
|
||||
示例如下:
|
||||
```java
|
||||
@ExtendWith(SpringExtension.class)
|
||||
// ApplicationContext will be loaded from "/base-config.xml"
|
||||
// in the root of the classpath
|
||||
@ContextConfiguration("/base-config.xml")
|
||||
class BaseTest {
|
||||
// class body...
|
||||
}
|
||||
|
||||
// ApplicationContext will be loaded from "/base-config.xml" and
|
||||
// "/extended-config.xml" in the root of the classpath
|
||||
@ContextConfiguration("/extended-config.xml")
|
||||
class ExtendedTest extends BaseTest {
|
||||
// class body...
|
||||
}
|
||||
```
|
||||
|
||||
```java
|
||||
// ApplicationContext will be loaded from BaseConfig
|
||||
@SpringJUnitConfig(BaseConfig.class)
|
||||
class BaseTest {
|
||||
// class body...
|
||||
}
|
||||
|
||||
// ApplicationContext will be loaded from BaseConfig and ExtendedConfig
|
||||
@SpringJUnitConfig(ExtendedConfig.class)
|
||||
class ExtendedTest extends BaseTest {
|
||||
// class body...
|
||||
}
|
||||
```
|
||||
|
||||
```java
|
||||
// ApplicationContext will be initialized by BaseInitializer
|
||||
@SpringJUnitConfig(initializers = BaseInitializer.class)
|
||||
class BaseTest {
|
||||
// class body...
|
||||
}
|
||||
|
||||
// ApplicationContext will be initialized by BaseInitializer
|
||||
// and ExtendedInitializer
|
||||
@SpringJUnitConfig(initializers = ExtendedInitializer.class)
|
||||
class ExtendedTest extends BaseTest {
|
||||
// class body...
|
||||
}
|
||||
```
|
||||
#### Context Configuration with Environment Profiles
|
||||
可以通过为test class指定`@ActiveProfiles`注解来指定激活的profiles。
|
||||
|
||||
```java
|
||||
@ExtendWith(SpringExtension.class)
|
||||
// ApplicationContext will be loaded from "classpath:/app-config.xml"
|
||||
@ContextConfiguration("/app-config.xml")
|
||||
@ActiveProfiles("dev")
|
||||
class TransferServiceTest {
|
||||
|
||||
@Autowired
|
||||
TransferService transferService;
|
||||
|
||||
@Test
|
||||
void testTransferService() {
|
||||
// test the transferService
|
||||
}
|
||||
}
|
||||
```
|
||||
`@ActiveProfiles`同样支持`inheritProfiles`属性,默认为true,可以关闭:
|
||||
```java
|
||||
// "dev" profile overridden with "production"
|
||||
@ActiveProfiles(profiles = "production", inheritProfiles = false)
|
||||
class ProductionTransferServiceTest extends AbstractIntegrationTest {
|
||||
// test body
|
||||
}
|
||||
```
|
||||
|
||||
#### Context Configuration with TestPropertySource
|
||||
可以通过在test class上声明`@TestPropertySource`注解来指定test properties文件位置。
|
||||
|
||||
`@TestPropertySource`可以指定lcoations和value属性,默认情况下支持xml和properties类型的文件,也可以通过`factory`指定一个`PropertySourceFactory`来支持不同格式的文件,例如`yaml`等。
|
||||
|
||||
每个path都会被解释为spring resource。
|
||||
|
||||
示例如下所示:
|
||||
```java
|
||||
@ContextConfiguration
|
||||
@TestPropertySource("/test.properties")
|
||||
class MyIntegrationTests {
|
||||
// class body...
|
||||
}
|
||||
```
|
||||
```java
|
||||
@ContextConfiguration
|
||||
@TestPropertySource(properties = {"timezone = GMT", "port = 4242"})
|
||||
class MyIntegrationTests {
|
||||
// class body...
|
||||
}
|
||||
```
|
||||
如果在声明`@TestPropertySource`时没有指定locations和value属性的值,其会在test class所在路径去查找。例如`com.example.MyTest`其默认会去查找`classpath:com/example/MyTest.properties`.
|
||||
|
||||
##### 优先级
|
||||
test properties的优先级将会比定义在操作系统environment、java system properties、应用程序手动添加的propertySource高。
|
||||
|
||||
##### 继承和覆盖
|
||||
`@TestPropertySource`注解也支持`inheritLocations`和`inheritProperties`属性,可以从父类中继承resource location和inline properties。默认情况下,两属性的值为true。
|
||||
|
||||
示例如下:
|
||||
```java
|
||||
@TestPropertySource("base.properties")
|
||||
@ContextConfiguration
|
||||
class BaseTest {
|
||||
// ...
|
||||
}
|
||||
|
||||
@TestPropertySource("extended.properties")
|
||||
@ContextConfiguration
|
||||
class ExtendedTest extends BaseTest {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
```java
|
||||
@TestPropertySource(properties = "key1 = value1")
|
||||
@ContextConfiguration
|
||||
class BaseTest {
|
||||
// ...
|
||||
}
|
||||
|
||||
@TestPropertySource(properties = "key2 = value2")
|
||||
@ContextConfiguration
|
||||
class ExtendedTest extends BaseTest {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
#### Loading WebApplicationContext
|
||||
如果要导入WebApplicationContext,可以使用`@WebAppConfiguration`.
|
||||
|
||||
在使用`@WebAppConfiguration`注解之后,还可以自由使用`@ConfigurationContext`等类。
|
||||
|
||||
```java
|
||||
@ExtendWith(SpringExtension.class)
|
||||
|
||||
// defaults to "file:src/main/webapp"
|
||||
@WebAppConfiguration
|
||||
|
||||
// detects "WacTests-context.xml" in the same package
|
||||
// or static nested @Configuration classes
|
||||
@ContextConfiguration
|
||||
class WacTests {
|
||||
//...
|
||||
}
|
||||
```
|
||||
|
||||
#### Context Caching
|
||||
一旦TestContext framework为test导入了Application Context,那么context将会被缓存,并且为之后相同test suite中所有有相同context configuration的test复用。
|
||||
|
||||
一个`ApplicationContext`可以被其创建时的参数组唯一标识,创建ApplicationContext所用到的参数将会产生一个唯一的key,该key将会作为context缓存的key。context cache key将会用到如下参数:
|
||||
- `locations`(`@ContextConfiguration`)
|
||||
- `classes`(`@ContextConfiguration`)
|
||||
- `contextInitializerClasses`(`@ContextConfiguration`)
|
||||
- `contextCustomizers`(`ContextCustomizerFactory`)
|
||||
- `contextLoader`(`@ContextConfiguration`)
|
||||
- `parent`(`@ContextHierarchy`)
|
||||
- `activeProfiles`(`@ActiveProfiles`)
|
||||
- `propertySourceDescriptors`(`@TestPropertySource`)
|
||||
- `propertySourceProperties`(`@TestPropertySource`)
|
||||
- `resourceBasePath`(来源于`@WebAppConfiguration`)
|
||||
|
||||
|
||||
如果两个test classes对应的key相同,那么它们将共用application context。
|
||||
|
||||
该context cache默认大小上限为32,到到达上限后,将采用`LRU`来淘汰被缓存的context。
|
||||
|
||||
### Test Fixture Dependency Injection
|
||||
test class中的依赖对象将会自动从application context中注入,可以使用setter注入、field注入。
|
||||
|
||||
示例如下所示:
|
||||
```java
|
||||
@ExtendWith(SpringExtension.class)
|
||||
// specifies the Spring configuration to load for this test fixture
|
||||
@ContextConfiguration("repository-config.xml")
|
||||
class HibernateTitleRepositoryTests {
|
||||
|
||||
// this instance will be dependency injected by type
|
||||
@Autowired
|
||||
HibernateTitleRepository titleRepository;
|
||||
|
||||
@Test
|
||||
void findById() {
|
||||
Title title = titleRepository.findById(new Long(10));
|
||||
assertNotNull(title);
|
||||
}
|
||||
}
|
||||
```
|
||||
setter注入示例如下所示:
|
||||
```java
|
||||
@ExtendWith(SpringExtension.class)
|
||||
// specifies the Spring configuration to load for this test fixture
|
||||
@ContextConfiguration("repository-config.xml")
|
||||
class HibernateTitleRepositoryTests {
|
||||
|
||||
// this instance will be dependency injected by type
|
||||
HibernateTitleRepository titleRepository;
|
||||
|
||||
@Autowired
|
||||
void setTitleRepository(HibernateTitleRepository titleRepository) {
|
||||
this.titleRepository = titleRepository;
|
||||
}
|
||||
|
||||
@Test
|
||||
void findById() {
|
||||
Title title = titleRepository.findById(new Long(10));
|
||||
assertNotNull(title);
|
||||
}
|
||||
}
|
||||
```
|
||||
### 事务管理
|
||||
为了启用事务支持,需要在`ApplicationContext`中配置`PlatformTransactionManager`bean,此外,必须在test方法上声明`@Transactional`注解。
|
||||
|
||||
test-managed transaction是由`TransactionalTestExecutionListener`管理的,spring-managed transaction和applicaion-managed transaction都会加入到test-managed transaction,但是当指定了spring-managed transaction和applicaion-managed transaction的传播行为时,可能会在独立事务中运行。
|
||||
|
||||
> 当为test method指定@Transactional注解时,会导致test方法在事务中运行,并且默认会在事务完成后自动回滚。若test method没有指定@Transactional注解,那么test method将不会在事务中运行。test lifecycle method并不支持添加@Transactional注解。
|
||||
|
||||
示例如下所示:
|
||||
```java
|
||||
@SpringJUnitConfig(TestConfig.class)
|
||||
@Transactional
|
||||
class HibernateUserRepositoryTests {
|
||||
|
||||
@Autowired
|
||||
HibernateUserRepository repository;
|
||||
|
||||
@Autowired
|
||||
SessionFactory sessionFactory;
|
||||
|
||||
JdbcTemplate jdbcTemplate;
|
||||
|
||||
@Autowired
|
||||
void setDataSource(DataSource dataSource) {
|
||||
this.jdbcTemplate = new JdbcTemplate(dataSource);
|
||||
}
|
||||
|
||||
@Test
|
||||
void createUser() {
|
||||
// track initial state in test database:
|
||||
final int count = countRowsInTable("user");
|
||||
|
||||
User user = new User(...);
|
||||
repository.save(user);
|
||||
|
||||
// Manual flush is required to avoid false positive in test
|
||||
sessionFactory.getCurrentSession().flush();
|
||||
assertNumUsers(count + 1);
|
||||
}
|
||||
|
||||
private int countRowsInTable(String tableName) {
|
||||
return JdbcTestUtils.countRowsInTable(this.jdbcTemplate, tableName);
|
||||
}
|
||||
|
||||
private void assertNumUsers(int expected) {
|
||||
assertEquals("Number of rows in the [user] table.", expected, countRowsInTable("user"));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 事务提交和回滚行为
|
||||
默认情况下,事务在test方法执行完后自动会回滚,但是,事务执行完后的行为可以通过`@Commit`或`@Rollback`注解来控制。
|
||||
|
||||
|
||||
如下是所有事务相关注解演示:
|
||||
```java
|
||||
@SpringJUnitConfig
|
||||
@Transactional(transactionManager = "txMgr")
|
||||
@Commit
|
||||
class FictitiousTransactionalTest {
|
||||
|
||||
@BeforeTransaction
|
||||
void verifyInitialDatabaseState() {
|
||||
// logic to verify the initial state before a transaction is started
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void setUpTestDataWithinTransaction() {
|
||||
// set up test data within the transaction
|
||||
}
|
||||
|
||||
@Test
|
||||
// overrides the class-level @Commit setting
|
||||
@Rollback
|
||||
void modifyDatabaseWithinTransaction() {
|
||||
// logic which uses the test data and modifies database state
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDownWithinTransaction() {
|
||||
// run "tear down" logic within the transaction
|
||||
}
|
||||
|
||||
@AfterTransaction
|
||||
void verifyFinalDatabaseState() {
|
||||
// logic to verify the final state after transaction has rolled back
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
### test request
|
||||
spring mvc中的测试示例如下所示:
|
||||
```java
|
||||
@SpringJUnitWebConfig
|
||||
class RequestScopedBeanTests {
|
||||
|
||||
@Autowired UserService userService;
|
||||
@Autowired MockHttpServletRequest request;
|
||||
|
||||
@Test
|
||||
void requestScope() {
|
||||
request.setParameter("user", "enigma");
|
||||
request.setParameter("pswd", "$pr!ng");
|
||||
|
||||
LoginResults results = userService.loginUser();
|
||||
// assert results
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## spring boot test
|
||||
spring boot提供了`@SpringBootTest`注解,可以作为`@ContextConfiguration`注解的代替。该类实际是通过spring boot项目的启动类来创建了一个ApplicationContext。
|
||||
|
||||
默认情况下,`@SpringBootTest`并不会启动server,可以使用注解的`webEnvironment`来对运行环境重新定义,该属性可选值如下:
|
||||
- `MOCK`(默认):将会导入一个ApplicationContext并且提供一个mock web environment。内置的server将不会被启动。如果classpath中没有web环境,那么其将会只创建一个非web的ApplicationContext。其可以和基于mock的`@AutoConfigureMockMvc`与`@AutoConfigureWebTestClient`一起使用
|
||||
- `RANDOM_PORT`:导入`WebServerApplicationContext`并提供一个真实的web环境,内部server启动,并监听随机端口
|
||||
- `DEFINED_PORT`导入`WebServerApplicationContext`并提供一个真实的web环境,内部server启动,监听指定端口(默认8080)
|
||||
- `NONE`:通过SpringApplication导入ApplicationContext,但是不提供任何web环境
|
||||
|
||||
### @TestConfiguration
|
||||
如果想要为测试新建Configuration顶级类,不应该使用`@Configuration`注解,这样会被@SpringBootApplciation或@ComponentScan扫描到,应该使用`@TestConfiguration`注解,并将类修改为嵌套类。
|
||||
|
||||
如果@TestConfiguration为顶层类,那么该类不会被注册,应该显式import:
|
||||
```java
|
||||
@RunWith(SpringRunner.class)
|
||||
@SpringBootTest
|
||||
@Import(MyTestsConfiguration.class)
|
||||
public class MyTests {
|
||||
|
||||
@Test
|
||||
public void exampleTest() {
|
||||
...
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
### testing with mock environment
|
||||
```java
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.test.context.junit4.SpringRunner;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
@RunWith(SpringRunner.class)
|
||||
@SpringBootTest
|
||||
@AutoConfigureMockMvc
|
||||
public class MockMvcExampleTests {
|
||||
|
||||
@Autowired
|
||||
private MockMvc mvc;
|
||||
|
||||
@Test
|
||||
public void exampleTest() throws Exception {
|
||||
this.mvc.perform(get("/")).andExpect(status().isOk())
|
||||
.andExpect(content().string("Hello World"));
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
### slice
|
||||
通常,测试时只需要部分configuration,例如只需要service层而不需要web层。
|
||||
|
||||
`spring-boot-test-autoconfigure`模块提供了一系列注解,可以进行slice操作。该模块提供了`@…Test`格式的注解用于导入ApplicationContext,并提供了一个或多个`@AutoConfigure…`来自定义自动装配设置。
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
1371
spring/webflux/Reactor.md
Normal file
1371
spring/webflux/Reactor.md
Normal file
File diff suppressed because it is too large
Load Diff
BIN
spring/webflux/image.png
Normal file
BIN
spring/webflux/image.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 42 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user