Compare commits

...

501 Commits

Author SHA1 Message Date
asahi
00e751439e doc: 阅读G1垃圾收集器文档 2025-12-06 23:38:53 +08:00
asahi
db552403d2 doc: 阅读cms文档 2025-12-02 23:23:19 +08:00
asahi
9f51f13801 doc: 阅读jvm文档 2025-11-28 22:31:14 +08:00
asahi
640560d264 doc: 阅读jvm文档 2025-11-27 22:33:24 +08:00
asahi
89232ffb86 doc: 阅读jvm文档 2025-11-27 15:09:51 +08:00
asahi
8e1f2b72b4 doc: 阅读jwt文档 2025-11-24 16:42:09 +08:00
asahi
f07db479b0 doc: 阅读kafka exactly-once文档,并做出补充 2025-11-13 19:23:19 +08:00
asahi
c6e216db9a doc: kafka文档新增目录 2025-11-12 14:52:25 +08:00
asahi
598227ab92 doc: 阅读classloader委派机制文档 2025-11-09 23:25:34 +08:00
wu xiangkai
5546c1c0dc feat: 阅读QUIC文档 2025-10-29 16:19:50 +08:00
wu xiangkai
02d33f437a doc: 阅读HTTP/3 QUIC文档 2025-10-28 16:31:19 +08:00
wu xiangkai
cd1deff03e doc: 阅读CAP文档 2025-10-22 11:01:54 +08:00
wu xiangkai
1e29bf0ca7 doc: 阅读CAP文档 2025-10-21 16:28:55 +08:00
wu xiangkai
cca7672cbf doc: 阅读mysql relay log文档 2025-10-21 15:41:26 +08:00
wu xiangkai
4cdefcd2bb doc: 阅读replication文档 2025-10-21 15:09:37 +08:00
wu xiangkai
bca627e036 doc: 阅读mysql replication文档 2025-10-20 16:30:24 +08:00
wu xiangkai
ed656f4e53 doc: 阅读mysql group replication文档 2025-10-20 11:35:32 +08:00
wu xiangkai
ad29dddc5f doc: 阅读mysql group replication文档 2025-10-17 16:28:38 +08:00
wu xiangkai
274f7b6c62 doc: 阅读mysql group replication文档 2025-10-17 15:43:56 +08:00
wu xiangkai
7b360b4d06 doc: 阅读mysql group replication文档 2025-10-17 15:29:13 +08:00
wu xiangkai
d4890cb337 doc: 阅读mysql group replication相关文档 2025-10-17 11:32:48 +08:00
asahi
ccb112e375 doc: 阅读mysql group replication文档 2025-10-17 01:53:09 +08:00
asahi
c5bfadb2b1 doc: 阅读mysql group replication文档 2025-10-15 16:25:45 +08:00
asahi
04e8485dca doc: 阅读redis cluster文档 2025-10-15 14:54:17 +08:00
asahi
5e9a6fa7cd doc: 阅读redis cluster voting文档 2025-10-14 17:05:25 +08:00
asahi
5b79be33d1 doc: 阅读redis cluster failover选举文档 2025-10-14 16:29:12 +08:00
asahi
ae1497593c doc: 阅读redis文档 2025-10-14 10:39:38 +08:00
asahi
987620a2d6 doc: 阅读redis cluster文档 2025-10-05 16:23:43 +08:00
asahi
dc90ad42c2 doc: 阅读redis cluster文档 2025-09-30 16:28:31 +08:00
asahi
008fe45df1 doc: 阅读redis cluster replcia文档 2025-09-30 15:20:09 +08:00
asahi
964cd99e7a doc: 阅读redis multi-key operations的文档 2025-09-30 15:06:56 +08:00
asahi
6e02856586 doc: 阅读redis cluster slots文档 2025-09-30 13:40:15 +08:00
asahi
6d4e7c4375 doc: 阅读redis ASK redirection文档 2025-09-30 09:41:08 +08:00
asahi
f159c6cc0e doc: 阅读redis cluster的slot migration文档 2025-09-29 15:33:43 +08:00
asahi
e47dc4711e doc: 阅读redis cluster文档 2025-09-29 13:50:13 +08:00
asahi
97c4a830a9 doc: 阅读redis cluster文档 2025-09-28 16:30:47 +08:00
asahi
e9a8e69327 doc: 阅读redis cluster文档 2025-09-28 11:33:30 +08:00
asahi
67818df180 doc: 阅读redis cluster文档 2025-09-28 10:57:52 +08:00
asahi
c478a97a99 doc: 阅读redis文档 2025-09-26 13:34:36 +08:00
asahi
492eb5d64a doc: 阅读Redis keyspace notification文档 2025-09-26 10:03:47 +08:00
asahi
65cfe0e072 doc: 阅读Shard Pub/Sub相关文档 2025-09-25 16:14:23 +08:00
asahi
a38527cc17 doc: 阅读redis lua文档 2025-09-25 14:09:29 +08:00
asahi
3baa0caff2 doc: 阅读redis lua文档 2025-09-25 11:29:35 +08:00
asahi
10aca33a06 doc: 阅读redis文档 2025-09-24 16:39:45 +08:00
asahi
4a0ee84e7e doc: 阅读redis scripting with lua文档 2025-09-24 15:03:01 +08:00
asahi
4e3a3388d6 doc: 阅读redis script/function文档 2025-09-23 16:24:43 +08:00
asahi
3ed69c87a0 doc: 阅读redis t-digest文档 2025-09-23 14:27:27 +08:00
asahi
1a01da4c34 doc: 阅读Cuckoo Filter文档 2025-09-23 10:59:25 +08:00
asahi
840ae043d6 doc: 阅读redis cuckoo filter文档 2025-09-22 16:13:32 +08:00
asahi
df43af8900 doc: 阅读bloom filter相关文档 2025-09-22 14:16:28 +08:00
asahi
81c1ab11e0 doc: 阅读redis文档 2025-09-22 11:41:39 +08:00
asahi
8eb167073e doc: 阅读redis geospatial文档 2025-09-19 11:08:24 +08:00
asahi
35671a787c doc: 阅读redis stream文档 2025-09-18 10:17:16 +08:00
asahi
a95b64b7b2 doc: redis文档阅读 2025-09-18 02:05:19 +08:00
asahi
58fa0617ce doc: 阅读redis stream document 2025-09-17 14:32:35 +08:00
asahi
fe9e3ebbe7 doc: 阅读redis xinfo文档 2025-09-17 10:43:14 +08:00
asahi
33c29d2d88 doc: 阅读redis XAUTOCLAIM文档 2025-09-16 23:24:26 +08:00
asahi
235d84afff doc: 阅读redis XCLAIM文档 2025-09-16 14:43:15 +08:00
asahi
11ad9a1420 doc: 阅读redis文档 2025-09-16 11:35:23 +08:00
asahi
110b653c2d doc: 阅读redis stream文档 2025-09-11 17:29:24 +08:00
asahi
79d9d6d66f doc: 阅读redis stream文档 2025-09-10 11:24:24 +08:00
asahi
fd97d2a362 doc: 阅读redis stream XREAD文档 2025-09-09 22:09:26 +08:00
asahi
172fd4c0ae doc: 阅读redis stream文档 2025-09-09 21:02:33 +08:00
asahi
a1bded7e5e doc: 阅读redis stream文档 2025-09-08 16:39:47 +08:00
asahi
a7206d120e doc: 阅读sorted set文档 2025-09-08 14:51:17 +08:00
asahi
1c9b73cd5d doc: 阅读redis hash文档 2025-09-08 11:05:49 +08:00
asahi
8e4fca612a doc: 阅读redis sets文档 2025-09-06 16:21:49 +08:00
asahi
a9057466a8 doc: 阅读redis lists文档 2025-09-06 15:45:17 +08:00
asahi
20578b535e doc: 阅读redis list文档 2025-09-05 17:22:26 +08:00
asahi
39f792e911 doc: 阅读RedisJSON文档 2025-09-05 11:14:22 +08:00
asahi
42dbfbac36 doc: 阅读redis pipeline/transaction相关文档 2025-09-04 20:08:56 +08:00
asahi
57507cc3af doc: 阅读spring cloud circuitBreaker文档 2025-09-04 11:10:04 +08:00
asahi
491908e87a doc: 修改RateLimiter中错误的描述 2025-09-03 16:26:25 +08:00
asahi
388e18af2a doc: 阅读TimeLimiter文档 2025-09-03 15:51:32 +08:00
asahi
4ddad7d316 doc: 阅读Retry文档 2025-09-03 15:34:48 +08:00
asahi
7f060618a9 doc: 阅读RateLimiter文档 2025-09-03 11:07:26 +08:00
asahi
80b25ede3d doc: 阅读Bulkhead文档 2025-09-03 10:20:44 +08:00
asahi
c5c9380ecd doc: 阅读CircuitBreaker文档 2025-09-02 20:08:00 +08:00
asahi
a6fc0c68e9 doc: 阅读CircuitBreaker文档 2025-09-02 20:02:46 +08:00
asahi
530efc036d doc: 阅读CircuitBreaker文档 2025-09-02 17:32:49 +08:00
asahi
7252d739a0 doc: 阅读circuit-breaker文档 2025-09-02 12:43:39 +08:00
asahi
5d8eeee8ad doc: 阅读mysql备份文档 2025-09-01 19:34:39 +08:00
asahi
64436d8cd4 doc: 阅读mysql文档 2025-08-31 23:22:26 +08:00
asahi
702b26f361 doc: 阅读mysql数据备份文档 2025-08-31 00:52:37 +08:00
asahi
b0d8de4fd3 doc: mysql transaction文档补充标题 2025-08-29 11:33:27 +08:00
f6cacead3d doc: 阅读mysql transaction文档 2025-08-29 00:00:43 +08:00
asahi
ced1d2db95 doc: 阅读mysql BLGC文档 2025-08-28 20:09:19 +08:00
asahi
26ac8916bf doc: 阅读binary log group commit文档 2025-08-27 17:43:55 +08:00
asahi
2b2be0b6d9 doc: 阅读mysql BLGC文档 2025-08-27 13:33:21 +08:00
asahi
97990229c9 doc: 阅读seata AT文档 2025-08-27 10:25:59 +08:00
asahi
8edc81b4c2 doc: 阅读mysql文档 2025-08-26 21:01:33 +08:00
asahi
35b41067e1 doc: 阅读seata文档 2025-08-26 19:32:45 +08:00
asahi
44f2a8c540 doc: 阅读seata global exclusive write lock相关的文档 2025-08-26 19:28:59 +08:00
asahi
b649c1ec0c doc: 阅读mysql 2pc实现 2025-08-26 12:44:30 +08:00
asahi
a10ca0ad31 doc: 阅读group commit文档 2025-08-26 01:44:32 +08:00
asahi
d668c20082 doc: 阅读mysql文档 2025-08-25 17:38:17 +08:00
asahi
6b65bd4811 doc: 阅读r2dbc文档 2025-08-21 19:41:23 +08:00
asahi
2989b25ad6 doc: 阅读python文档 2025-08-21 02:20:25 +08:00
asahi
c20bea9ea7 doc: 阅读python await/async文档 2025-08-19 20:57:23 +08:00
asahi
ecd049c842 doc: 阅读python文档 2025-08-19 12:55:16 +08:00
asahi
ab6898d7eb doc: 阅读python面向对象文档 2025-08-18 23:57:34 +08:00
asahi
3f20f381a8 doc: 阅读mysql history list文档 2025-08-18 12:44:45 +08:00
asahi
e2fe983467 doc: 阅读python文档 2025-08-18 02:34:42 +08:00
asahi
0e2445ccbf doc: 阅读python文档 2025-08-17 05:28:33 +08:00
asahi
68c21c80d1 doc: 阅读python module文档 2025-08-16 22:09:44 +08:00
asahi
7d7d8746e6 doc: 阅读python文档
[doc url]  https://docs.python.org/3/tutorial/modules.html
2025-08-15 12:57:03 +08:00
asahi
d4bf768fe3 doc: 阅读mysql事务文档 2025-08-14 21:00:40 +08:00
asahi
dee8180b4d doc: 阅读mysql mvcc文档 2025-08-14 19:49:26 +08:00
asahi
dcafbb38d8 doc: 阅读mysql mvcc文档 2025-08-13 22:55:44 +08:00
asahi
b90bb79459 doc: 阅读mysql mvcc文档 2025-08-13 17:08:13 +08:00
asahi
49fb828650 doc: 阅读mysql mvcc文档 2025-08-13 02:22:17 +08:00
asahi
1bdc9db292 doc: 文档阅读 2025-08-12 01:41:07 +08:00
asahi
e67c9a4cae doc: 删除误解的文档内容 2025-08-10 21:18:53 +08:00
asahi
bcf0b15fd0 doc: 阅读mysql undo文档 2025-08-10 21:16:33 +08:00
asahi
ed2324ccb2 doc: 阅读undo log文档 2025-08-09 17:02:31 +08:00
asahi
3ab6dfcb48 doc: 阅读undo log文档 2025-08-07 13:35:55 +08:00
asahi
965f75a3ee doc: 阅读mysql undo log物理存储文档 2025-08-06 12:25:28 +08:00
asahi
3383d032a7 doc: 阅读mysql undo log相关文档 2025-08-05 21:14:36 +08:00
asahi
caf011a1fc doc: 阅读mysql undo log文档 2025-08-05 21:05:17 +08:00
asahi
d3e1f2b5c3 doc: 阅读innodb undo log文档 2025-08-05 00:00:06 +08:00
asahi
434e83d389 doc: 新增文档目录 2025-08-03 21:54:15 +08:00
asahi
5fe5320e73 doc: 阅读vim register文档 2025-08-02 23:48:45 +08:00
asahi
ed116d4bb7 doc: 阅读vim register文档 2025-08-01 13:00:19 +08:00
asahi
088e0ead13 doc: 阅读undo log文档 2025-08-01 12:26:41 +08:00
asahi
449caf7a6c doc: 阅读vim tab文档 2025-07-25 01:56:24 +08:00
asahi
4cbfb1bda2 doc: 阅读purge文档 2025-07-24 12:50:04 +08:00
asahi
e861ac1ece doc: 阅读vim相关文档 2025-07-24 01:27:14 +08:00
asahi
2c9278a410 doc: 阅读innodb undo log文档 2025-07-23 12:47:34 +08:00
asahi
63c89cf851 doc: 阅读undo log文档 2025-07-23 01:16:12 +08:00
asahi
2e854a4d9f doc: 阅读undo log文档 2025-07-22 12:49:06 +08:00
asahi
ad4e53c93d doc: 阅读mysql文档 2025-07-22 00:36:56 +08:00
asahi
1da87e132c doc: 阅读mysql innodb文档 2025-07-21 00:29:01 +08:00
asahi
92dc46a702 doc: 阅读undo log文档 2025-07-14 12:58:47 +08:00
asahi
fa0a80f1f9 doc: 阅读undo log文档 2025-07-10 12:53:40 +08:00
asahi
94a8f8eadc doc: 阅读undo log文档 2025-07-09 12:52:39 +08:00
asahi
e3e6bc99ac doc: 阅读redo log document 2025-07-08 23:33:59 +08:00
asahi
35d84c57b1 doc: 阅读innodb文档 2025-07-08 22:01:57 +08:00
asahi
3a808113ba doc: 阅读innodb文档 2025-07-08 12:47:42 +08:00
asahi
301102efe6 doc: 阅读文档 2025-07-07 00:39:39 +08:00
asahi
26d4122be9 doc: 阅读mysql文档 2025-06-30 00:35:19 +08:00
asahi
8e90e58987 doc: 阅读mysql redo log文档 2025-06-27 00:48:51 +08:00
asahi
8fad9108f1 doc: 阅读golang sqlx文档 2025-06-21 20:42:45 +08:00
asahi
a5476b5d09 doc: 阅读golang sqlx PreparedStatement相关文档 2025-06-20 12:32:34 +08:00
asahi
362ccaec26 阅读golang sqlx文档 2025-06-18 13:34:31 +08:00
asahi
ffe73a4a2a doc: 阅读golang-sqlx tx文档 2025-06-17 13:33:13 +08:00
asahi
48a3e7614b doc: 阅读golang-sqlx文档 2025-06-17 01:14:30 +08:00
asahi
65a711ad96 doc: 阅读golang sqlx文档 2025-06-16 12:49:28 +08:00
asahi
3ddc976a66 doc: 阅读golang sqlx文档 2025-06-16 01:09:29 +08:00
asahi
89f8bd9db4 doc: 阅读webclient文档 2025-06-03 23:27:54 +08:00
asahi
db56eddf84 doc: 阅读webclient文档 2025-06-03 12:55:01 +08:00
asahi
5e565ecfc6 doc: 阅读webclient filter文档 2025-05-29 12:44:43 +08:00
asahi
4b03f8d44e doc: 阅读webclient文档 2025-05-24 21:29:26 +08:00
asahi
1eea88ccb8 doc: 阅读webclient文档 2025-05-24 16:28:27 +08:00
asahi
a22cc0d592 doc: 阅读Flux.zip文档 2025-05-03 21:47:55 +08:00
asahi
e2de3c1a16 doc: 阅读webclient文档 2025-04-27 12:55:24 +08:00
asahi
7a53199fb8 doc: 阅读webclient文档 2025-04-26 18:37:17 +08:00
asahi
521d591047 doc: 阅读webflux文档 2025-04-25 12:38:40 +08:00
asahi
6a6dfd5bed doc: 阅读webclient文档 2025-04-22 12:52:03 +08:00
asahi
7e2c4317c3 doc: 阅读webclient文档 2025-04-21 12:42:42 +08:00
asahi
da4093838f doc: 阅读reactor文档 2025-04-21 01:00:07 +08:00
asahi
f437f84ba4 doc: 阅读flux分组文档 2025-04-19 19:18:48 +08:00
asahi
e71cde7bb8 doc: 阅读Sinks.one()和Sinks.empty()文档 2025-04-18 12:53:55 +08:00
asahi
f57adf0481 doc: 阅读Sinks.many().multicast()文档 2025-04-18 00:46:38 +08:00
asahi
c6be75ec7a doc: 阅读Sinks文档 2025-04-17 12:49:28 +08:00
asahi
2764227a0e doc: 阅读project reactor关于sinks的文档 2025-04-16 12:54:44 +08:00
asahi
0822e5052f doc: 阅读project reactor文档 2025-04-16 00:10:03 +08:00
asahi
1d858eb6c1 doc: Exceptions.propagate文档阅读 2025-04-15 12:45:54 +08:00
asahi
b69bd6cdd8 doc: 阅读transient errors文档 2025-04-14 12:47:33 +08:00
asahi
3ae2a0dc15 doc: 阅读project reactor文档 2025-04-13 22:32:39 +08:00
asahi
3928ac7dc6 doc: 阅读project reactor文档 2025-04-07 20:41:24 +08:00
asahi
e5b3cfead4 doc: 阅读spring reactor文档 2025-04-07 12:50:26 +08:00
asahi
d640bd00e8 doc: 阅读spring reactor文档 2025-04-06 00:46:29 +08:00
asahi
b5f09da87d doc: 阅读reactor schedulers文档 2025-04-04 22:16:36 +08:00
asahi
78eb257c4f doc: 阅读reactor文档 2025-04-03 12:54:05 +08:00
asahi
16e06d6be2 doc: 阅读spring reactor文档 2025-04-01 18:20:38 +08:00
asahi
f8c55e9709 doc: 阅读reactor文档 2025-04-01 01:32:49 +08:00
asahi
95793600ba doc: 阅读spring reactor文档 2025-03-31 12:54:04 +08:00
asahi
28af3a569b doc: 阅读spring reactor文档 2025-03-31 00:34:30 +08:00
asahi
ffcfb75f9e doc: 阅读reactor文档 2025-03-29 16:30:27 +08:00
asahi
45de3952b7 doc: 阅读spring reactor文档 2025-03-28 17:51:46 +08:00
asahi
cf6f069e79 doc: 阅读spring reactor文档 2025-03-28 01:34:50 +08:00
asahi
e55fa5a32b doc: 阅读reactor文档 2025-03-27 19:04:06 +08:00
asahi
25b226d1a6 doc: 阅读Flux.generate文档 2025-03-26 01:15:21 +08:00
asahi
a74f948934 doc: reactor文档添加目录 2025-03-25 18:59:09 +08:00
asahi
a60eeabdd6 doc: 阅读reactor subscription文档 2025-03-25 18:57:25 +08:00
asahi
de65d0a208 doc: 阅读reactor文档 2025-03-25 12:49:31 +08:00
asahi
e0829d4282 doc: 阅读reactor core文档 2025-03-24 20:40:40 +08:00
asahi
36dee5bcc4 doc: 阅读disposable文档 2025-03-24 12:46:06 +08:00
asahi
fda63dcc4a doc: 阅读reactor-core文档 2025-03-24 01:11:38 +08:00
asahi
fcf534828e doc: 阅读reactor文档 2025-03-21 12:56:59 +08:00
asahi
44763e6487 doc: 阅读spring reactor文档 2025-03-21 00:33:06 +08:00
asahi
2a4931ac51 doc: 阅读reactor assembly line文档 2025-03-20 12:49:41 +08:00
asahi
275372be35 doc: reactor文档阅读 2025-03-19 12:51:49 +08:00
asahi
79c4982792 doc: 阅读webflux文档 2025-03-14 01:23:59 +08:00
asahi
be52af362d 阅读webflux文档 2025-03-12 20:49:15 +08:00
asahi
4e9fed19e8 doc: 阅读webflux文档 2025-03-12 12:56:14 +08:00
asahi
012f4b44cb doc: webflux文档补充目录 2025-03-11 19:16:50 +08:00
asahi
d04f72efc0 doc: 阅读webflux controller文档 2025-03-11 19:15:02 +08:00
asahi
64d8c576ab doc: 阅读webflux文档 2025-03-11 13:00:50 +08:00
asahi
d44fd7921f doc: 阅读webflux文档 2025-03-11 00:05:23 +08:00
asahi
d2e278dcce 阅读文档 2025-03-10 20:50:50 +08:00
asahi
142b4deb8a doc: 阅读webflux文档 2025-03-10 12:56:28 +08:00
asahi
8b22a93919 doc: 阅读spring webflux文档 2025-03-09 23:57:09 +08:00
asahi
3ce8f7f062 doc: 阅读netty心跳机制 2025-03-08 19:20:18 +08:00
asahi
4fb1f7e7f7 doc: 阅读innodb事务文档 2025-03-03 20:33:48 +08:00
asahi
bb028ceafc doc: 阅读protobuf encoding文档 2025-03-03 00:15:49 +08:00
asahi
89a8616cfc doc: 阅读protobuf文档 2025-03-01 21:41:40 +08:00
asahi
25cd3fc411 阅读innodb事务文档 2025-02-25 23:55:57 +08:00
asahi
c3bf955058 阅读protobuf文档 2025-02-25 12:41:11 +08:00
asahi
aa006e2dab 阅读protobuf文档 2025-02-24 20:46:29 +08:00
asahi
4f13a6c959 阅读ngram文档 2025-02-24 19:09:16 +08:00
asahi
838ec5ccc8 阅读ngram文档 2025-02-24 18:52:34 +08:00
asahi
d2d901b37c 阅读ngram parser文档 2025-02-24 12:45:25 +08:00
asahi
6aca9f0a96 阅读innodb fulltext相关文档 2025-02-23 21:00:26 +08:00
asahi
6faa9ac410 阅读full-text index文档 2025-02-21 12:47:18 +08:00
asahi
c56c06555e 阅读全文索引文档 2025-02-19 20:47:10 +08:00
asahi
3c65a5f827 阅读golang ent relation文档 2025-02-18 20:56:24 +08:00
asahi
a569b77b6c 阅读ent文档 2025-02-18 00:50:33 +08:00
asahi
50e7d02355 阅读innodb icp优化文档 2025-02-17 20:41:03 +08:00
asahi
ac43337b3f 阅读innodb mutlti-range read文档 2025-02-17 12:56:00 +08:00
asahi
e9000354db 阅读innodb索引相关文档 2025-02-15 16:35:31 +08:00
asahi
ed5f2297cf 阅读mysql索引文档 2025-02-15 16:28:43 +08:00
asahi
f657d2e2ed 阅读mysql分区性能文档 2025-02-10 12:59:53 +08:00
asahi
130714fe74 阅读innodb分区文档 2025-02-09 22:20:26 +08:00
asahi
6b5e042d3d 阅读range分区文档 2025-02-08 01:47:49 +08:00
asahi
6ae820123d 阅读分区表文档 2025-02-07 12:54:57 +08:00
92658eb0b1 阅读inndob page directory相关文档 2025-02-03 17:38:20 +08:00
049240dc16 阅读innodb 存储结构文档 2025-02-02 19:57:30 +08:00
db8f9eb5c9 阅读redo log file相关文档 2025-01-31 16:42:45 +08:00
92dc60f7a3 阅读mysql binglog文档 2025-01-30 18:41:31 +08:00
31be54e703 阅读innodb自适应hash index以及mysql aio文档 2025-01-28 18:39:00 +08:00
60609e2442 阅读tcp header文档 2025-01-28 13:58:12 +08:00
asahi
36b9bc26b0 阅读tcp文档 2025-01-26 02:09:18 +08:00
asahi
04bb5d26c7 阅读tcp/ip ip协议文档 2025-01-25 20:20:28 +08:00
asahi
762decee30 补充netty文档阅读 2025-01-20 15:47:16 +08:00
asahi
f247130c0a 阅读netty pipeline文档 2025-01-20 14:33:53 +08:00
asahi
bf8b6b42d3 阅读golang bufio文档 2025-01-18 20:37:27 +08:00
asahi
93441d04fd 阅读buffered io文档 2025-01-18 01:45:05 +08:00
asahi
d417c36d9a 阅读ip协议 2025-01-17 18:34:54 +08:00
asahi
69df0c7a09 阅读golang time文档 2025-01-17 14:27:00 +08:00
asahi
07ac8271a7 阅读golang文档 2025-01-16 20:42:01 +08:00
asahi
630ed107ea 阅读golang time文档 2025-01-16 13:05:19 +08:00
asahi
0b58982907 阅读gorm model文档 2025-01-15 22:59:21 +08:00
asahi
b8c173c529 阅读gorm文档 2025-01-15 12:37:41 +08:00
asahi
1137d1c420 阅读logrus文档 2025-01-14 20:36:19 +08:00
asahi
4fe88021e4 阅读logrus文档 2025-01-14 13:03:09 +08:00
asahi
148c94e386 阅读golang反射文档 2025-01-13 14:10:58 +08:00
asahi
26c14bbc9d 阅读golang sync.Once文档 2025-01-11 16:17:27 +08:00
asahi
8a0d036a14 阅读sync.Pool文档 2025-01-11 15:31:52 +08:00
asahi
b96041b9e1 阅读golang const相关文档 2025-01-10 13:33:00 +08:00
asahi
b0e7e87e08 阅读unit test和fuzz test文档 2025-01-09 01:43:35 +08:00
asahi
dc13365c07 阅读type constraint文档 2025-01-08 12:40:11 +08:00
asahi
246983ea3a 阅读golang文档 2025-01-08 03:33:27 +08:00
asahi
06a9726bca 阅读golang gin restful文档 2025-01-07 23:41:43 +08:00
93395e1aae 阅读golang multi modules文档 2025-01-06 00:40:12 +08:00
asahi
14a371447f 阅读golang mod文档 2025-01-05 21:23:10 +08:00
asahi
a20f47a43f 阅读elastic search file system storage文档 2025-01-03 12:54:10 +08:00
asahi
807314bd10 阅读RFC 894和RFC 1042相关文档 2025-01-03 01:11:46 +08:00
asahi
5b53e6dc6f 阅读slow query log文档 2025-01-02 12:54:29 +08:00
asahi
3577791743 阅读index module文档 2025-01-02 12:23:14 +08:00
asahi
f3dcfd77f6 阅读es index module文档 2024-12-31 12:43:05 +08:00
asahi
bd6bb93bac 阅读tcp/ip协议文档 2024-12-31 01:15:30 +08:00
asahi
25678c96cd 阅读elastic search index module文档 2024-12-30 20:44:31 +08:00
wuxiangkai
500af61cc4 阅读es pipeline aggregation文档 2024-12-30 19:31:46 +08:00
asahi
bd1c088091 阅读es histogram aggregation文档 2024-12-30 12:45:42 +08:00
asahi
a204b15618 阅读es aggregations文档 2024-12-27 13:01:13 +08:00
asahi
b59337a36b 阅读es过滤相关文档 2024-12-25 20:30:58 +08:00
asahi
443cb1085b 阅读es文档 2024-12-25 12:55:27 +08:00
asahi
f4ac1d4427 阅读es文档 2024-12-24 12:56:22 +08:00
wuxiangkai
5bf56c33db 阅读innodb double write文档 2024-12-19 12:58:47 +08:00
asahi
ba967259bd 阅读change buffer相关文档 2024-12-16 12:46:16 +08:00
asahi
353e984695 阅读python日志文档 2024-12-04 20:42:36 +08:00
asahi
fef5679e49 阅读mysql文档 2024-12-04 19:21:10 +08:00
asahi
2976a65082 阅读自适应刷新文档 2024-12-04 12:53:02 +08:00
asahi
1e3a90a430 阅读buffer pool flush文档 2024-12-03 12:49:14 +08:00
asahi
864e4a3f43 阅读mysql文档 2024-12-02 20:46:50 +08:00
asahi
720230cd82 阅读mysqlcheckpoint文档 2024-12-02 12:35:55 +08:00
asahi
d689e04464 阅读mysql checkpoint相关文档 2024-11-25 20:40:33 +08:00
asahi
4ce97c08b5 阅读innodb made young相关文档 2024-11-23 16:54:15 +08:00
asahi
5362e59e11 阅读mysql LRU老化文档 2024-11-22 13:01:06 +08:00
asahi
e00dbdc108 阅读mysql LRU文档 2024-11-21 12:56:01 +08:00
asahi
785b694479 阅读innodb_buffer_pool相关文档 2024-11-20 13:30:21 +08:00
asahi
ce92d62bdf 阅读innodb体系结构文档 2024-11-19 12:49:31 +08:00
asahi
07c9a23f22 阅读mysql wait-for graph相关文档 2024-11-18 13:02:50 +08:00
asahi
6f56194fb5 阅读innodb可重复读隔离级别下加锁的文档 2024-11-17 22:39:41 +08:00
asahi
698530d66a 阅读mysql加锁文档 2024-11-16 18:29:21 +08:00
asahi
5d462efce6 阅读next-key lock文档 2024-11-13 12:52:52 +08:00
asahi
34562345a9 阅读间隙锁文档 2024-11-13 12:30:41 +08:00
asahi
d2f82e210c 阅读mysql一致性读的相关文档 2024-11-13 00:13:27 +08:00
asahi
183625869f 阅读记录锁文档 2024-11-11 20:32:48 +08:00
asahi
c629b0ba50 阅读mysql意向锁文档 2024-11-11 00:12:45 +08:00
asahi
22bf36ef47 阅读mysql innodb文档 2024-11-04 20:49:56 +08:00
asahi
21b761bd34 阅读mysql文档 2024-11-04 00:42:37 +08:00
asahi
d844663f86 阅读es文档 2024-10-29 20:03:05 +08:00
asahi
c06915ddfe 阅读es文档 2024-10-21 20:58:50 +08:00
asahi
73eab827a9 阅读es文档 2024-10-17 21:04:25 +08:00
asahi
3517559617 阅读es文档 2024-10-04 21:07:06 +08:00
asahi
c8d37b3e2f 阅读python文档 2024-09-22 15:19:24 +08:00
asahi
9fb7fccdf3 阅读general query log和slow sql log文档 2024-09-20 20:32:03 +08:00
asahi
cfc2c162df 阅读binary log文档 2024-09-20 20:07:38 +08:00
asahi
db8d2ddeb5 阅读binlog文档 2024-09-18 23:55:23 +08:00
asahi
1669041d56 阅读mysql binlog文档 2024-09-18 12:48:25 +08:00
asahi
b2ae12d2db mysql文档阅读 2024-09-18 01:16:12 +08:00
asahi
800763897e 阅读btrfs文档 2024-09-10 12:52:37 +08:00
asahi
bad8149951 阅读btrfs文档 2024-09-10 00:29:11 +08:00
asahi
316e3c56cd 阅读netty文档 2024-08-31 22:26:26 +08:00
asahi
293252d913 阅读netty文档 2024-08-31 17:07:16 +08:00
asahi
6edbdbd79e 阅读nio文档 2024-08-28 00:37:24 +08:00
asahi
8563c9aca0 阅读nio文档 2024-08-27 13:31:41 +08:00
asahi
c0f21f27b0 阅读nio文档 2024-08-26 12:53:12 +08:00
asahi
3c3ab1f41e 阅读nio文档 2024-08-25 23:38:56 +08:00
asahi
19ce2bfe4f 新增nio阅读笔记 2024-08-24 21:51:54 +08:00
asahi
093eb9e9b2 logback文档阅读 2024-08-06 21:35:24 +08:00
asahi
0f5f9327b9 阅读logback文档,FileAppender相关内容 2024-08-06 19:09:56 +08:00
asahi
69f07de1f6 阅读logback文档 2024-08-05 20:55:25 +08:00
asahi
080746df22 配置logback文档 2024-08-05 12:53:54 +08:00
asahi
fa251a7910 阅读logback文档 2024-08-05 00:22:58 +08:00
asahi
bb5cfc9c7b 新增rsync增量同步方案 2024-08-03 11:11:42 +08:00
asahi
a3d445549a 阅读spring text文档 2024-06-02 20:57:23 +08:00
asahi
572dd332b8 阅读spring test文档 2024-05-31 19:11:42 +08:00
asahi
0b72fb58f1 阅读mybatis插件文档 2024-05-25 02:27:45 +08:00
asahi
828d865ca8 阅读selenium文档 2024-05-06 00:57:09 +08:00
asahi
2e90007639 阅读argparse文档 2024-05-04 23:00:31 +08:00
asahi
3f77333e4e 阅读beautiful soup文档 2024-05-04 22:26:57 +08:00
asahi
19325fe22b 阅读python文档 2024-05-04 17:34:30 +08:00
asahi
baac0d4fee 阅读python文档 2024-05-04 03:19:50 +08:00
asahi
11850cbb51 阅读python文档 2024-05-03 20:52:04 +08:00
asahi
8f82f1fc8e 阅读es相关文档 2024-04-21 23:55:49 +08:00
asahi
ba440e4a32 阅读atomikos文档 2024-04-13 18:55:52 +08:00
asahi
1772fde535 阅读atomikos文档 2024-04-07 23:18:16 +08:00
asahi
e37ec41b6e 阅读atomikos文档 2024-04-07 12:47:12 +08:00
asahi
7c539ad040 阅读分布式事务文档 2024-04-07 00:24:36 +08:00
asahi
279c73e29f 阅读atomikos文档 2024-04-05 21:47:38 +08:00
asahi
34991d580a 阅读spring security基于方法的权限认证 2024-03-26 19:22:38 +08:00
asahi
df5101160b 阅读spring security文档 2024-03-22 21:04:16 +08:00
asahi
82a6acfe5e spring security文档阅读 2024-03-21 19:57:14 +08:00
asahi
ace697e380 阅读spring security文档 2024-03-18 21:09:23 +08:00
asahi
eff9847a5d 阅读spring security文档 2024-03-17 20:37:24 +08:00
asahi
553931005d 阅读spring security文档 2024-03-17 20:22:24 +08:00
asahi
35d39bcea0 阅读spring security文档 2024-03-15 23:14:26 +08:00
asahi
6c0404485c spring security文档阅读 2024-03-15 00:21:59 +08:00
asahi
e593bfaa81 阅读spring security文档 2024-03-14 00:19:13 +08:00
asahi
254062c161 阅读spring security文档 2024-03-12 00:41:28 +08:00
asahi
b2805148b6 阅读regex相关文档 2024-03-10 22:54:03 +08:00
asahi
8ef6aaa8c5 阅读kafka error handler文档 2024-03-07 20:06:39 +08:00
asahi
fc8cdbf2d9 阅读spring kafka transaction文档 2024-03-04 20:53:25 +08:00
asahi
08b7bc1a78 阅读kafka线程安全文档 2024-02-27 20:59:16 +08:00
asahi
3117618172 阅读spring kafka关于seek操作的文档 2024-02-27 20:16:01 +08:00
asahi
64b66a0b38 阅读spring kafka文档 2024-02-26 18:55:02 +08:00
asahi
3d89f7a47e 创建目录 2024-02-22 18:49:06 +08:00
asahi
24ce571d45 阅读关于kakfka接收消息的文档 2024-02-22 18:46:58 +08:00
asahi
0d653ba29f 阅读kafka关于@KafkaListener注解的文档 2024-02-21 19:47:40 +08:00
asahi
c88a6e4a2a 阅读kafka关于@KafkaListener注解的文档 2024-02-02 19:43:49 +08:00
asahi
ca7facff06 阅读kafka关于@KafkaListener注解的文档 2024-02-02 19:16:08 +08:00
asahi
a8c6f344de 阅读kafka关于@KafkaListener注解的文档 2024-01-25 21:13:55 +08:00
asahi
6bcbd361e7 阅读kafka关于@KafkaListener注解的文档 2024-01-25 16:41:12 +08:00
asahi
e9f9244199 阅读kafka关于@KafkaListener注解的文档 2024-01-22 20:54:17 +08:00
asahi
69b0aad449 阅读kafka关于@KafkaListener注解的文档 2024-01-22 19:42:53 +08:00
asahi
3cb2d5cdcc 阅读spring kafka关于接收消息文档 2024-01-19 21:24:42 +08:00
asahi
7499ea6ee3 阅读rsync文档 2024-01-08 02:28:21 +08:00
asahi
3381d8c2e8 阅读spring kafka listener container手动提交的文档 2024-01-07 20:20:02 +08:00
asahi
6d9a4eb48f 阅读spring kafka commit offset文档 2024-01-07 04:04:07 +08:00
asahi
4d28c2f1a6 阅读spring kafka接收消息的文档 2024-01-06 22:51:21 +08:00
asahi
c65d93d529 阅读spring kafka发送消息文档 2024-01-04 00:44:00 +08:00
asahi
40ffdb90b8 阅读kafka 生产者发送消息文档 2023-12-28 23:55:21 +08:00
asahi
308e53cce7 阅读kafka发送消息文档 2023-12-28 00:19:39 +08:00
asahi
48b5f46d52 阅读spring kafka发送消息的文档 2023-12-27 00:31:46 +08:00
asahi
f507171cfc 阅读spring kafka关于创建topic和发送消息的文档 2023-12-23 16:48:09 +08:00
asahi
969fcc285b 适配wsl终端 2023-12-23 14:44:52 +08:00
wuxiangkai
47f5222bbf 新增kafka commitSync api说明 2023-12-23 14:41:16 +08:00
ebf8665806 阅读kafka exactly-once语义文档 2023-12-20 01:15:16 +08:00
f631fe6ec4 kafka consumer相关文档阅读 2023-11-30 00:05:51 +08:00
wuxiangkai
bace3202f5 阅读kafka consumer commitSync/commitcommitAsync文档 2023-11-25 17:14:21 +08:00
wuxiangkai
01f013fbe8 阅读kafka consumer commitSync/commitAsync相关文档 2023-11-25 16:23:25 +08:00
d2930c17dd kafka consumer相关文档阅读 2023-11-25 02:09:57 +08:00
e4d198c890 kafka consumer相关文档阅读 2023-11-20 00:27:59 +08:00
828b7848cc kafka consumer相关文档阅读 2023-11-19 01:29:28 +08:00
7e1b790441 kafka consumer相关文档阅读 2023-11-08 00:04:09 +08:00
6f7f99f582 kafka consumer相关文档阅读 2023-11-07 00:37:09 +08:00
wuxiangkai
b856a9af7b 新增spring mvc关于interceptor文档 2023-10-30 21:21:45 +08:00
wuxiangkai
4e686152a4 补充reqeust scope bean内容 2023-10-24 14:34:52 +08:00
wuxiangkai
670539a025 spring ioc容器bean scope文档阅读 2023-10-23 14:09:31 +08:00
wuxiangkai
5f2fc404c2 ioc文档阅读 2023-10-22 22:55:22 +08:00
a871d01214 kafka broker相关文档阅读 2023-10-06 23:03:28 +08:00
4197045981 阅读kafka broker相关文档 2023-10-06 02:24:09 +08:00
48d52cf098 阅读kafka文档 2023-10-04 17:51:29 +08:00
fb01d0a2c4 spring mail文档阅读 2023-07-28 15:25:42 +08:00
267f93d544 sepl文档阅读 2023-06-29 20:16:26 +08:00
baea8f6802 sepl新增目录 2023-06-29 17:27:55 +08:00
0bb208569f 阅读spel文档 2023-06-29 17:25:28 +08:00
7765c48ffb 阅读next.js路由文档 2023-06-02 21:14:39 +08:00
e7b24a85b7 react next.js文档阅读 2023-06-01 00:52:14 +08:00
a0ed38fd29 react Context文档阅读 2023-05-30 00:27:04 +08:00
4ce0974356 react文档阅读 2023-05-30 00:02:50 +08:00
bb73633973 阅读react文档 2023-05-28 20:58:00 +08:00
44f37fc0ff react文档阅读 2023-05-25 00:58:49 +08:00
5e84650984 react文档阅读 2023-05-22 22:25:10 +08:00
8d6e169b8c 补充caffeine关于软引用和弱引用的文档阅读 2023-05-21 23:54:54 +08:00
d01dd144f4 阅读弱引用、软引用文档 2023-05-21 23:35:21 +08:00
a83e5897c5 阅读caffeine文档 2023-05-14 21:20:48 +08:00
33543a3445 添加目录 2023-05-08 00:12:58 +08:00
68b9b34345 阅读反射文档 2023-05-07 23:44:59 +08:00
2034b8f8c7 Spring AMQP文档阅读 2023-04-27 16:57:44 +08:00
2769db29e7 Spring AMQP文档阅读 2023-04-27 11:18:14 +08:00
e7ed863e79 补充目录 2023-04-24 09:37:24 +08:00
c78febc0d7 spring resttemplate文档阅读 2023-04-21 17:40:17 +08:00
e5b607eb9d spring cloud circuit breaker文档阅读 2023-04-21 15:29:31 +08:00
6e7fd59764 spring cloud circuit breaker文档阅读 2023-04-21 15:28:40 +08:00
0b6112106a Spring Cloud gateway文档阅读 2023-04-19 15:25:32 +08:00
4a5c580031 Spring Cloud gateway文档阅读 2023-04-18 20:37:30 +08:00
5cd38c6fd3 Spring Cloud gateway文档阅读 2023-04-18 20:24:12 +08:00
63725457cb Spring Cloud OpenFeign文档阅读 2023-04-14 15:29:10 +08:00
edc0638cb7 阅读Spring Cloud Netflix文档 2023-03-27 22:35:10 +08:00
88d8d4abd3 spring cloud 文档阅读 2023-03-27 18:28:44 +08:00
af390d79b2 spring cloud 文档阅读 2023-03-24 11:46:09 +08:00
6004b5fddb spring cloud 文档阅读 2023-03-21 11:40:19 +08:00
dab013e3bb 学习spring cloud 2023-03-13 23:38:17 +08:00
25af13f91f spring cloud 文档阅读 2023-03-13 18:19:47 +08:00
fa247a16c1 spring cache补充 2023-03-08 15:58:57 +08:00
52ad7e30aa 学习spring cloud 2023-03-07 23:28:07 +08:00
0cd5c1ef28 学习spring cloud 2023-03-06 23:50:41 +08:00
e608c50af1 继续关于Kafka文档的阅读 2023-03-02 16:21:35 +08:00
f7f3ce18ba 继续关于Kafka文档的阅读 2023-03-02 13:18:51 +08:00
44e6f4122f 继续关于Kafka文档的阅读 2023-02-28 18:48:19 +08:00
5400fe07ee 继续关于Kafka文档的阅读 2023-02-28 16:45:40 +08:00
63a9ed1bca 继续关于Kafka文档的阅读 2023-02-22 21:55:05 +08:00
8254dd60a5 继续关于Kafka文档的阅读 2023-02-21 19:03:30 +08:00
d541cd7dbc 继续关于Kafka文档的阅读 2023-02-21 10:47:01 +08:00
949bad547e 继续关于Kafka文档的阅读 2023-02-21 09:54:35 +08:00
9f8ee7e160 继续阅读kafka文档 2023-02-20 22:57:21 +08:00
46d3efb46d 继续关于Kafka文档的阅读 2023-02-20 18:47:15 +08:00
f9e5d27f6b 继续关于Kafka文档的阅读 2023-02-17 18:54:27 +08:00
64d6fcdee5 新增关于kafka mq文档的阅读 2023-02-16 16:10:55 +08:00
ed84bf7053 完成docker相关文档的阅读 2023-02-14 18:22:55 +08:00
d4e8a39fee 完成docker文档的阅读 2023-02-13 17:05:42 +08:00
207fbfd0ff react相关文档的阅读 2023-02-13 13:34:21 +08:00
ab88027378 继续dockerfile文档的阅读 2023-02-10 17:03:41 +08:00
1190043b71 继续阅读docker文档 2023-02-09 20:40:49 +08:00
9b63b9df01 继续docker文档的阅读 2023-02-09 00:02:24 +08:00
d911e59895 提交docker文档的相关笔记 2023-02-08 21:02:37 +08:00
36a2550982 完成redisson分布式锁文档的阅读 2023-02-08 12:35:36 +08:00
40918e6277 完成redisson分布式容器的文档阅读 2023-02-07 23:11:25 +08:00
3a57ae584a 继续redisson分布式集合的阅读 2023-02-07 19:58:33 +08:00
3c2996feeb 继续redisson文档阅读 2023-02-06 18:38:25 +08:00
33b098bea7 完成redisson文档阅读 2023-02-06 18:37:41 +08:00
0544303a88 继续redisson关于分布式集合的文档阅读 2023-02-02 19:08:52 +08:00
0fb3dc2432 阅读redisson相关文档阅读redisson相关文档 2023-02-01 18:13:48 +08:00
b894b5bbb6 完成对spring cache文档的阅读 2023-02-01 11:09:51 +08:00
1362552f08 阅读spring cache文档,学习有关@Cacheable注解的相关使用 2023-01-31 23:44:24 +08:00
dd0c822faf spring cache相关文档阅读 2023-01-31 17:58:10 +08:00
326b98edc9 提交关于git rebase的文档笔记 2023-01-18 15:56:24 +08:00
89c4ce2077 新增git rebase的文档笔记 2023-01-18 15:41:31 +08:00
3f2b6189ba 继续netty文档的学习 2023-01-17 15:29:09 +08:00
826d2c9615 git shell切换wsl 2023-01-16 18:36:59 +08:00
wu xiangkai
a51a1402d2 Merge branch 'master' of https://gitea.rikako.cc/rikako/rikako-note 2023-01-16 18:33:39 +08:00
wu xiangkai
c734d1973c netty文档阅读 2023-01-16 18:33:28 +08:00
63fab22414 完成@Valid和@Validated相关文档的学习 2023-01-15 02:14:08 +08:00
be40280c8c 切换默认shell到wsl 2023-01-10 21:59:13 +08:00
1d6198e9d8 完成java se多线程关于CompletableFuture文档的阅读 2023-01-09 02:50:38 +08:00
b1ff440a5f 1/8/2023 完成Spring 任务异步和调度部分的文档阅读 2023-01-08 22:50:58 +08:00
wu xiangkai
53cce93ee0 日常提交 2023-01-06 13:00:20 +08:00
wu xiangkai
4bbb9e44cf 日常提交 2023-01-05 13:05:06 +08:00
342715e493 日常提交 2023-01-04 23:54:51 +08:00
wu xiangkai
13f3a43ddc 日常提交 2023-01-04 13:00:19 +08:00
a6b6032921 日常提交 2023-01-04 00:15:10 +08:00
9ffa6b3eab 日常提交 2022-12-27 01:07:43 +08:00
wu xiangkai
ded0e315b8 日常提交 2022-12-21 21:23:36 +08:00
wu xiangkai
5cb990c270 日常提交 2022-12-20 20:00:04 +08:00
2a31c8f42d 日常提交 2022-12-19 23:51:08 +08:00
wu xiangkai
4016aa61e2 日常提交 2022-12-19 20:34:16 +08:00
3dec6613e8 日常提交 2022-12-19 01:34:09 +08:00
wu xiangkai
0f6c62e459 日常提交 2022-12-01 15:19:38 +08:00
632324d94b 日常提交 2022-11-30 23:51:11 +08:00
wu xiangkai
bd3884c617 日常提交 2022-11-30 16:50:59 +08:00
cb3ef91737 日常提交 2022-11-29 22:40:50 +08:00
wu xiangkai
fa8dd3f67f 日常提交 2022-11-29 16:41:45 +08:00
wu xiangkai
56e693cfa0 日常提交 2022-11-18 17:30:04 +08:00
wu xiangkai
bc069d1211 日常提交 2022-11-18 11:05:50 +08:00
wu xiangkai
c32dd7ead5 日常提交 2022-11-17 18:05:52 +08:00
wu xiangkai
e38a9a9333 日常提交 2022-11-14 17:24:14 +08:00
wu xiangkai
b52561bf68 日常提交 2022-11-14 15:04:19 +08:00
wu xiangkai
febfc63b9e 日常提交 2022-11-14 15:03:11 +08:00
wu xiangkai
f38edad36d 日常提交 2022-11-10 18:01:19 +08:00
wu xiangkai
38b3752860 日常提交 2022-11-09 11:11:01 +08:00
wu xiangkai
eff5492b2e 日常提交 2022-11-09 11:09:14 +08:00
wu xiangkai
2cbdca091c 日常提交 2022-11-02 10:48:09 +08:00
wu xiangkai
999ef6c2c4 日常提交 2022-10-27 11:04:38 +08:00
wu xiangkai
e00410e379 日常提交 2022-10-20 18:16:53 +08:00
wu xiangkai
28e6e75599 日常提交 2022-10-20 18:15:39 +08:00
wu xiangkai
9aca3a3e1e 日常提交 2022-10-14 19:24:59 +08:00
wu xiangkai
7e5eec2677 Merge branch 'master' of gitea.rikako.cc:rikako/rikako-note 2022-10-13 18:09:59 +08:00
wu xiangkai
ce79102b7f 日常提交 2022-10-13 18:08:54 +08:00
989ab8c59b 日常提交 2022-10-12 22:42:42 +08:00
wu xiangkai
7626248e9f 为部分md文档生成目录 2022-10-12 19:50:23 +08:00
wu xiangkai
3cf65df785 日常提交 2022-10-12 19:43:27 +08:00
wu xiangkai
a12f3c0091 日常提交 2022-10-10 20:26:53 +08:00
e8484aa581 日常提交 2022-10-09 23:26:14 +08:00
wu xiangkai
1c1cf55739 日常提交 2022-10-09 19:02:34 +08:00
2bf8e02062 日常提交 2022-10-08 23:26:07 +08:00
wu xiangkai
e805a65039 日常提交 2022-10-08 18:04:38 +08:00
7e15addd0d 日常提交 2022-10-01 18:48:22 +08:00
111 changed files with 43754 additions and 2776 deletions

2
.gitignore vendored
View File

@@ -1 +1 @@
.idea/
.idea/

22
Git/git rebase.md Normal file
View 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操作最终产物是相同的通过两种方式整合的分支最后都完全相同。但是相比较于mergerebase操作产生的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

File diff suppressed because it is too large Load Diff

239
Golang/ent.md Normal file
View 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
View 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 rootembeded 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
View 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`: 对于每个Modelgorm中将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 &gt; 13</code>, refer <a href="constraints.html">Constraints</a></td>
</tr>
<tr>
<td>&lt;-</td>
<td>set fields write permission, <code>&lt;-:create</code> create-only field, <code>&lt;-:update</code> update-only field, <code>&lt;-:false</code> no write permission, <code>&lt;-</code> create and update permission</td>
</tr>
<tr>
<td>-&gt;</td>
<td>set fields read permission, <code>-&gt;: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>

View File

@@ -1,2 +1,2 @@
# rikako-note
A Personal Note of Rikako Wu...
A personal learning note for Rikako Wu, which contains content about Computer Science Techs.

View File

@@ -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
View 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
挂载类型如下:
- binddefault绑定装载上下文目录只读
- 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 ...
```
或者可以考虑ARGARG不会持久化到最终的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
View 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 mountbind 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`命令来创建volumedocker知道我们要使用命名好的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

File diff suppressed because it is too large Load Diff

View 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用于对索引进行创建并且控制和索引相关的各个方面。
### 索引设置
索引级别的设置可以针对每个索引来进行设置,设置分类如下:
- staticstatic设置只能在如下时机应用于索引
- 索引创建时
- 索引处于closed状态
- 使用`update-index-settings` api并且带有`reopen=true`的查询参数,带有该参数时,会关闭受影响索引,更新后将受影响索引重新开启
- dynamicdynamic索引可以在索引处于活跃状态时通过`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>`: 向索引应用的阻塞类型
- &lt;block&gt;部分可选的值为`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 moudlescoring/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
View 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
对于先前存在的connectionsQUIC利用了TLS 1.3的`0-RTT`特性。
`0-RTT`代表zero round-trip time resumption是TLS 1.3中引入的一个新特性。
TLS session resumption通过复用先前建立的安全参数减少建立secure connection所花费的时间。当client和server频繁建立连接并断开时这将带来性能改善。
通过0-RTT resumptionclient可以在连接的第一个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
为了支持独立的streamQUIC采用了per-stream based flow control。其在两个级别控制了stream data的带宽消耗
- 对于每个独立的流,都设置了一个可分配给其的最大数据数量
- 在整个连接范的围内设置了active streams最大的累积数量
通过per-stream flow controlQUIC限制了同时可以发送的数据数量用于避免接收方过载并且在多个streams间大致公平的分配网络容量。
##### 拥塞控制算法
QUIC允许实现选择不同的拥塞控制算法使用最广泛的算法如下
- NewRenoTCP使用的拥塞控制算法
- CUBIC: 和NewReno类似但是使用了cubic function而不是linear function
- BBR
在网络状况较差的场景下,不同的拥塞控制算法性能可能存在较大差异。

View 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组合可以有如下常用方法
- thenApplyT -> U)对CompletableFuture对象的返回结果执行操作并且产生一个返回值
- thenAccept(T -> void)类似thenApply操作上一个future的返回值但是返回类型为void
- handle(T,Throwable)->U处理上一个future返回的result并且产生一个返回值
- thenCompose(T->CompletableFuture\<U\>):将上一个Future返回值作为参数传递并且返回CompletableFuture\<U\>
- whenCompleteT,Throwable->void类似于handle但是不产生返回值
- exceptionallyThrowable->T):处理异常并返回一个结果
- completeOnTimeoutT,long,TimeUnit)当超时时将传入的参数T作为返回结果
- orTimeOutlong,TimeUnit)当超时时产生一个TimeoutExcetpion作为结果
- thenRunRunnable:执行该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
- runAfterBothCompletableFuture\<\>,Runnable当两个future都执行完之后执行Runnable
- applyToEither(CompletableFuture\<T\>T-> U)当任一future执行完成通过该result产生返回值
- acceptEitherCompletableFuture\<T\>T->Void):类似applyToEither但是返回值为void
- runAfterEitherCompletableFuture\<\>,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
View File

@@ -0,0 +1,74 @@
# Classloader
Java ClassLoader是构成JREJava 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 ClassLoaderExtension 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 ClassLoaderApplication 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使用了委托层次结构算法来加载classclassLoader运行原则如下
- classLoader永远会遵循委托层次结构原则
- 当jvm遇到class时其首先会检查该class是否已经被加载
- 如果该类未被加载那么jvm将会请求classloader sub-system去加载该类而classloader sub-system则会将该加载工作委托给Application ClassLoader
- Application Classloader会将该类的加载任务委托给Extension ClassLoader而Extension ClassLoader则是会再次将该加载任务委托给Bootstrap Classloader
- Bootstrap Classloader会在bootstrap classpathJDK/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
View 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
View 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,Shortbuffer是线性并且有序的。
buffer的主要属性包括capacity、limit和position
- capacitybuffer的capacity是其可以包含的元素数量capacity不可以为负数并且不可以被改变
- limitbuffer的limit代表buffer中第一个不应该被读/写的元素下表,`例如buffer的capacity为5但是其中只包含3个元素那么limit则为3`limit不能为负数并且limit不可以比capacity大
- positionposition代表下一个被读/写元素的下标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设置为0limit保持不变
> 在向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 bufferjvm在将尽量避免在调用本地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
View 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)
- [getExceptionTypesMethod和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
ife.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()
```
#### getExceptionTypesMethod和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) ;
```

View 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
```
引用对象在构造时,会和特定的引用队列相关联,当被引用对象的可达性状态发生变化时,被添加到引用队列中。在被添加到队列之前,引用对象都已经被清空。
### 弱引用和软引用的使用
弱引用和软引用都提供两种类型的构造函数,
- 只接受被引用对象,并不将引用注册到引用队列
- 既接受被引用对象,还将引用注册到指定的引用队列,然后可以通过检查引用队列中的情况来监听被引用对象可达状态变更

View File

@@ -0,0 +1,466 @@
- [深入理解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缺陷)
- [G1垃圾收集器](#g1垃圾收集器)
- [G1垃圾收集器内存布局](#g1垃圾收集器内存布局)
- [Remembered Set](#remembered-set)
# 深入理解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、oopsOrdinary 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%的对象生命周期都很短暂完全不需要根据11来划分内存空间。`对于新生代内存空间将其划分为了一块较大的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收集器
CMSConcurrent 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后都进行碎片整理
#### G1垃圾收集器
G1是一项面对服务端应用的垃圾收集器和其他垃圾收集器相比G1具备如下特点
- `并行和并发`
- G1能够充分利用多cpu、多核环境下的硬件优势利用多个cpu/cpu核心来缩短STW停顿时间
- 在部分GC动作中其他垃圾收集器都需要暂停用户线程的执行而G1垃圾收集器则是在这些GC动作中仍然允许用户线程的同时执行
- `分代垃圾收集`和其他垃圾收集器一样G1中也保留了分代的概念。但是G1并不需要其他垃圾收集器的配合就能够独立管理整个GC堆
- `空间整合`和CMS的`标记-清理`算法不同G1在整体上采用了`标记-整理`算法实现垃圾收集而从局部两个Region上看G1采用了复制算法。不管从整体还是局部上看G1都不会产生内存碎片
- `可预测的停顿`G1相比于CMS的一大优势是G1造成的停顿可预测。G1和CMS都关注在垃圾回收中降低停顿但G1除了追求低停顿外还能建立可预测的停顿时间模型能让使用者指明`在一个时间为M的时间片段内消耗在垃圾收集上的时间不得超过N毫秒`
##### G1垃圾收集器内存布局
在G1之前其他的垃圾收集器在进行收集时都是只关注新生代或老年代对象但G1则并非这样。在使用G1垃圾收集器时java堆的内存布局和使用其他垃圾收集器有较大区别。
在使用G1垃圾收集器时其将整个java堆划分为多个大小相等的独立区域Region虽然仍然保留有新生代和老年代的概念但新生代和老年代并非在物理上是隔离的它们都是一部分并不需要连续Region的集合。
G1之所以能够建立可预测的停顿模型是因为其可以避免在整个java堆中进行全区域的垃圾收集。`G1会跟踪各个Region中垃圾堆积的价值大小回收所获取的空间大小以及回收所需时间的经验值在后台维护一个优先列表每次根据允许的垃圾回收时间优先回收价值最大的Region`这即是G1垃圾收集器的名称由来即Garbage-First
`上述将内存空间划分为Region以及有优先级的区域回收方式能够保证G1垃圾收集器在有限的时间内以尽可能高的效率进行垃圾回收`
##### Remembered Set
即使将内存区域划分为多个Region但是Region中的对象仍然会跨Region相互引用。故而在进行可达性分析时如果不做特殊处理在分析某个Region中的对象存活与否时仍然需要扫描整个堆内存空间。
> 在使用其他垃圾收集器时即使不对内存进行Region划分在新生代和老年代之间也会存在相互引用。如果不做特殊处理在分析新生代对象时也必须扫描老年代。这将会极大的影响GC效率
G1收集器中Region之间的对象引用以及其他收集器中新生代和老年代之间的对象引用虚拟机都是通过Remembered Set来避免整个堆的扫描的。`G1中每个Region都有一个与之对应的Remembered Set虚拟机发现程序在对reference类型的数据进行写操作时会产生一个Write Barrier暂时中断写操作检查Reference引用的对象是否处于不同的Region中如果是则将相关引用信息记录到被引用对象所属Region的Remembered Set中。`在进行内存回收时在GC根节点枚举范围中加入Remembered Set即可保证不对全堆扫描也不会有遗漏。
若不计算维护Remembered Set的操作G1垃圾收集器的运作可以分为如下步骤
- 初始标记:
- 初始标记会记录GC Roots能直接关联到的对象
- 其会修改TAMS令下一阶段用户线程并发运行时能在Region中创建新对象
- 初始标记阶段需要STW暂停用户线程
- 并发标记
- 对堆中对象进行可达性分析,找出存活对象
- 该阶段耗时较长,但是可以和用户线程并发执行
- 最终标记
- 修正在并发标记期间因用户线程运行而导致的标记变动
- 该阶段需要STW暂停用户线程但是可以并行执行
- 筛选回收
- 在筛选回收期间会对各个Region的回收价值和成本进行排序根据用户期望的GC停顿时间来制定回收计划
- 在筛选回收阶段,也是会暂停用户线程的
<img src="https://ask.qcloudimg.com/http-save/5427637/ouqa71pbc7.jpeg" style="width: 100%;">

View File

@@ -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 ExchangeJWT能够安全的在各方传递信息。由于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的不同方面
- validationjwt的格式是否正确
- verification确保jwt真实且未修改
### Validation
JWT validation会检查jwt的结构、格式、内容
- 结构确保jwt是被`.`符号分隔的三个部分header, payload, signature
- format校验每个部分都是使用Base64URL进行编码的的并且payload包含预期的claims
- content校验payload中的claims是否正确例如过期时间exp、issued_atiat、not beforenbf确保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
View 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只存在一个copysingle
如果只存在一个磁盘设备那么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允许234个copies分别叫做RAID1, RAIDC3 RAIDC4。
> 对于RAID5和RAID6raid5需要的最小设备数量为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
View 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
View 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 groupkafka mq会将topic中的消息广播给所有订阅了该topic的consume group而每条消息都只会被consume group中的一个消费者实例进行消费
>
> 在一个consume group中每个消费者实例都负责某一topic中相应的分区每个特定分区只有一个消费者实例负责。
> #### 消费者订阅
> 消费者的订阅操作是在消费者实例中调用的,在同一消费者组中,**不同消费者实例可以订阅不同的topic集合**。topic的分区只会在订阅了该topic的实例之间进行分配。
>
> 例如存在消费者实例c1,c2,c3, 其中c1订阅了t1c2订阅了t1和t2c3订阅了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 topictransaction 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-1broker-2broker-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的数据丢弃(由于宕机的是leaderLEO最大故而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消息默认保存小时默认为16824 * 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指定完分区分配计划之后会将分配计划发送给coordinatorcoordinator收到分配方案后则会将分配方案分发给消费者组内的各个消费者实例。
> ##### 消费者组的心跳机制
> 消费者组中的每个消费者都会和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返回的ackack代表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生产者支持幂等性操作即使相同的消息被多次发送给brokerbroker也只会向分区中写入一条消息。除此之外开启生产者的`enable.idempotence=true`属性,还代表生产者发送到同一分区的消息是顺序的。
生产者实现幂等性的原理和tcp类似每次被批量发送给broker集群的消息都含有一个序列号broker将会使用序列号进行去重操作。但是和tcp不同的是该序列号会持久化到replicate log中故而即使broker leader宕机重新成为leader的broker也能知道是否接受到的消息是重复发送。
并且kafka生产者开启幂等性带来的开销很小仅仅为批量提交的消息附加的序列号。
### 事务-在多个分区之间进行原子写入
通过事务apikafka支持在多个分区之间原子的进行写入。事务特性支持生产者在向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
View 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 APIStream 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个或多个消费者订阅。
对于每个topickafka集群都会维护一个分区日志每个分区日志都是一个有序的、不可变的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 grouprecord都会被发送给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允许管理和查看topicbroker和其他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支持GZIPLZ4ZStandard压缩协议。
### 生产者
#### 负载均衡
生产者会直接将数据发送给作为该分区leader的broker并不会经过任何的中间路由层。为了帮助生产者实现这些所有的Kafka节点都能响应对元数据的请求元数据中包含哪些server还存活以及topic中分区对应的leader都在哪。返回的元数据可以正确的告知生产者应该将请求发送到哪个server。
客户端会控制将消息发送到哪一个分区。这可以随机完成实现一种随机负载均衡或者通过语义分区函数完成。Kafka提供了一个接口用于语义分区可以允许用户来提供一个key进行分区并使用该key散列到一个分区也可以覆盖分区函数
#### 异步发送
批量处理是提高性能的重要因素之一为了支持批量处理Kafka生产者会在内存中累计数据并在单个请求中发送更大的批数据。批处理可以被配置为累计不超过固定数量的消息或不超过特定界限的延迟例如64K10ms
### 消费者
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 repliasISR中包含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分区数据那么消息将会被丢失
- -1all这种情况下leader broker会等待ISR队列中所有的副本broker都对该消息返回ack后才对生产者返回ack。此时只要当前集群中还存在一个in-sync副本那么消息就不会丢失
> `acks`的默认值是-1all
> acks=-1的情况下会产生重复数据的问题如果发送消息后消息已经全部存储到所有的broker但是再尚未ack的情况下leader宕机那么生产者会重新发送消息此时消息会被重复存储在消息队列中
#### 最小写入副本数
当acks设置为-1alltopic可以设置一个最小写入的副本数通过配置`min.insync.replicas`可以对最小写入副本数进行配置。即使消息已经同步到所有ISR副本后如果同步数目小于该值同步数目包含leader消息也无法被视为提交
> `min.insync.replicas`的值默认为1. 该值只有当生产者ack模式设置为-1all时才起作用
> kafka保证一条消息只要被提交只要有一个in-sync-replica处于活跃状态那么消息就不会被丢失

File diff suppressed because it is too large Load Diff

View 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

View File

@@ -1,6 +1,6 @@
# binlog 日志
* ## binlog二进制日志
* 定义binlog日志文件也称之为变更日志文件记录了数据库记录的所有DDL、DML等数据库更新事件的语句但是不包括任何没有更新数据的语句。
* 用途:
* 数据恢复如果mysql数据库进程意外停止可以通过binlog中的记录来恢复数据库中的数据
# binlog 日志
* ## binlog二进制日志
* 定义binlog日志文件也称之为变更日志文件记录了数据库记录的所有DDL、DML等数据库更新事件的语句但是不包括任何没有更新数据的语句。
* 用途:
* 数据恢复如果mysql数据库进程意外停止可以通过binlog中的记录来恢复数据库中的数据
* 数据复制可以通过binlog来实现mysql数据库的主从一致

View File

@@ -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中关于修改该条数据的记录
* ReadViewReadView是MVCC机制中事务对数据进行快照读时产生的一个读视图。
* ReadView原理
* 当事务开启时会产生当前数据库系统的一个快照innodb为每个事务构造了一个数组用来维护当前系统中活跃事务的ID活跃事务为当前开启了但是尚未提交的事务id
* ReadView中保存了如下信息
* creator_trx_id创建当前ReadView的事务id
* trx_ids记录创建失误时当前mysql系统中活跃的事务id集合(已经开始但是还没有被提交的事务id)
* up_limit_idtrx_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中关于修改该条数据的记录
* ReadViewReadView是MVCC机制中事务对数据进行快照读时产生的一个读视图。
* ReadView原理
* 当事务开启时会产生当前数据库系统的一个快照innodb为每个事务构造了一个数组用来维护当前系统中活跃事务的ID活跃事务为当前开启了但是尚未提交的事务id
* ReadView中保存了如下信息
* creator_trx_id创建当前ReadView的事务id
* trx_ids记录创建失误时当前mysql系统中活跃的事务id集合(已经开始但是还没有被提交的事务id)
* up_limit_idtrx_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在读已提交的隔离级别下并不能够解决幻读的问题。

View File

@@ -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操作会生成显示锁

View File

@@ -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的内容回滚事务时也能将数据恢复到崩溃前尚未被事务修改的状态。

View File

@@ -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

View 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中包含的消息发送给replciasreplicas则是会重新执行这些变动。
- `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中。

View 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`指定的目的地。

View 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 storage structure](https://www.mysqltutorial.org/wp-content/uploads/2024/01/mysq-innodb-architecture.png, "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 bufferundo页的回收等。
### 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将会被自动调整到10G10G是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 loginnodb会将未提交事务看作已提交
- 6(SRV_FORCE_NO_LOG_REDO):innodb不执行前滚操作
> 如果在数据库发生宕机时存在未提交事务那么将下次数据库启动时会先根据未提交事务的undo log来对数据进行回滚。

View 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可见

View 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最后被写入时的检查点第四字节的值。
LSNlog 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中其前2KB4个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&amp;pid=ImgDetMain&amp;o=7&amp;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`可能的取值如下:
- 12TRX_UNDO_UPD_EXIST_REC更新未被标记为delete的记录
- 13TRX_UNDO_UPD_DEL_REC更新`已经被标记为delete的记录`
- 14TRX_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 Nodeundo 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 logundo 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 typeundo page的类型
- last log record offset最后一条记录的offset
- free space offsetpage中空闲空间的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 Cachedupdate cached list中包含可以被重用的update undo segments
- Insert Listinsert list中包含了正在使用的insert undo segments
- Insert Cachedisnert 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复制数据的过程。通过binlogslave 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都存在一个queuesession在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中。`实际将内容刷新到系统缓冲区中但并不实际调用fsyncfsync的调用发生在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 stageleader将会逐个扫描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`来获取共享锁,保证事务在处理过程中没有其他的事务可以修改数据。

View 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`则是可以选择从二进制日志的某个时间点来进行恢复。

View 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当参数值被设置为非`01`的值时每当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: 如果参数被配置为mixedmysql默认会采用`statement`格式进行记录,但是在特定场景能够下会使用`row`格式:
- 使用了uuid, user, current_user,found_rows, row_count等不确定函数
- 使用了insert delay语句
- 使用了用户自定义函数
- 使用了临时表
##### 使用statement可能会存在的问题
在使用statement格式时可能会存在如下问题
- master运行randuuid等不确定函数时或使用触发器操作时会导致主从服务器上的数据不一致
- 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 filespare 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中

File diff suppressed because it is too large Load Diff

View 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中。表空间则由`段segementextentpage`组成。
组成如图所示:
<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的默认页大小设置为4K8K。
页是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代表infimum011代表supremum1xx保留 |
| 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 headerpage headerfile trailer的大小是固定的分别为38568字节这些空间用于标记页的一些信息例如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次磁盘io1000w数据构成的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
```

View 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 logrollback 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
View 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 LockGap Lock + Record Lock锁定一个范围并且锁定记录本身
Record Lock是针对索引加的锁`如果在建表时没有为表设置索引那么innodb会采用隐式的主键来进行锁定。`
#### Next-Key Lock
Next-Key Lock结合了record lock和gap lock如果一个索引中有10111320这四条记录那么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事务插入5B事务插入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来对锁进行管理页中不论有多少条记录进行了加锁锁占用的资源都是相同的。

View 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)
- [CAConsistency 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理论表明分布式系统中只能同时满足三种特性中的两种。
### CAConsistency 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中的AAvailability只是要求非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的用户`不可用。

View 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可以加入和离开groupgroup 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 modegroup_replication_single_primary_mode=ONgroup中只存在一个`primary server`被设置为read/write mode。group中所有其他的members都被设置为read-only modesuper_read_only=ON。该primary server通常会引导整个集群的启动所有其他加入到group中的servers都会识别到primary server并且自动被设置为read-only mode.
在single-primary mode时group replication会强制只有一个server能够向group中写入相比于multi-primary modesingle-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 weightmember 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 modegroup_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 ddlddl语句的执行要么提交、要么删除类似一个原子的事务。对于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 versionsgroup 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 engineXCom, 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仍然能够形成majority2~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 engineXCom
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.

View 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 serversreplicas。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 identifiersGTIDs是事务的并且不需要和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的当前positionreplica可以断开连接、重新连接并重启之前的过程。
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 identifiersGTIDs的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 identifierGTID是一个唯一标识符该标识符的创建并和`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将会被分配GTIDGTID由`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 shutdownserver会将所有写入到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 loggingGTID将会在提交时被原子的持久化写入到binary log中事务开始的位置。无论何时如果binary log发生rotation或server shutdownserver会将`先前写入到binary log中事务的GTIDs`写入到`mysql.gtid_executed`表中
8. 如果replica禁用binary logGTID将会原子的被持久化直接被写入到`mysql.gtid_executed`表中。mysql会向事务中追加一个statement并且将GTID插入到table中该操作是原子的在该场景下`mysql.gtid_executed`表记录了完整的`transactions applied on the replica`
9. 在replicated transaction提交后很短的时间GTID会被非原子的externalizedGTID会被添加到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都有优缺点。对大多数usersmixed 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 replicationrow-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 channelrelay 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 logsrelay log的删除由replciation sql thread来处理。但是`FLUSH LOGS`可以针对relay logs进行rotation。

227
netty/netty-doc.md Normal file
View 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 datainbound则是通常从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 eventhandler的执行顺序为`1,2,3,4,5`
但由于`3,4`没有实现ChannelInboundHandler故而Inbound event会跳过`3,4` handler实际Inbound event的handler顺序为`1,2,5`
#### outbound
对于outbound eventhandler的执行顺序为`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
View 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
- bossboss event loop用于接收incoming connections
- workerworker 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`参数。
- optionoption设置的是NioServerSocketCahnel该channel用于接收incoming connection
- childOptionchildOption用于设置被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 servertime 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
View File

@@ -0,0 +1,598 @@
- [protobuf](#protobuf)
- [language guideproto3](#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 guideproto3
### 定义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 typeclient会取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 formatwire 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`作为keyfield的类型以及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`varinti64, 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 packedrepeated 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
View 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
View 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
View 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!
```
> 默认输出的日志隔离级别为WARNINGinfo信息并不会被输出
当直接调用`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

File diff suppressed because it is too large Load Diff

575
python/selenium.md Normal file
View 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会远程调用driverdriver再调用处于同一系统的浏览器。
## 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
View 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
View 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
View 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`开头的函数被称为hookuseState是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方法的返回值是一个新的statereact会将旧的状态设置为新的状态。
### 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>
);
}
```

View 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).)*$`来表示。

View 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属性为trueautoDelete属性为false
#### Exchange类型
交换机类型通过`ExchangeTypes`中的常量进行表示常用的类型如下direct, topic, fanout, and headers。
不同类型交换机之间的区别,在于它们如何处理与队列之间的绑定:
- directdirect exchange会让一个queue被绑定到一个固定的routing key
- topictopic 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.#`
- fanoutfanout 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重启时是否仍会存在如果设置为falsebroker重启之后queue必须被重新decalre
- exclusive只能够被一个连接使用当该连接断开之后queue也会被删除
- auto-delete当最后一个consumer不再订阅该queue时该queue会被自动删除
> 当声明一个queue时如果该queue不存在那么queue会被创建如果该queue已经存在并且声明的属性和已存在队列的属性相同那么不会发生任何事如果queue已经存在并且声明属性和已存在队列不同那么会抛出异常并且406的异常codePRECONDITION_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。

View 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环境中使用rememberMelogout默认会从浏览器中删除rememberMe cookie。

View 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、deleteCRUD
#### 权限粒度级别
在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 {
//dont 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 {
//dont show the button?
}
```
#### 基于String的权限鉴定
如果不想构造Permission对象可以通过构造一个字符串来代表权限。该字符串可以是任何格式只要你的Realm能够识别该格式并且与权限进行交互。
```java
String perm = "printer:print:laserjet4400n";
if(currentUser.isPermitted(perm)){
//show the print button?
} else {
//dont show the button?
}
```
### 通过注解实现Authorization
可以通过java注解来实现Authorization过程**在使用注解之前必须先开启aop**。
如果在执行openAccount之前当前Subject必须拥有account:create权限那么可以通过如下方式来实现权限鉴定。如果当前用户未被直接授予或通过role间接授予该权限那么会抛出AuthorizationException异常。
```java
//Will throw an AuthorizationException if none
//of the callers roles imply the Account
//'create' permission
@RequiresPermissions("account:create")
public void openAccount( Account acct ) {
//create the account
}
```
如果要在执行方法之前进行角色校验,可以通过如下方式加上注解达到预期功能。
```java
//Throws an AuthorizationException if the caller
//doesnt have the teller role:
@RequiresRoles( "teller" )
public void openAccount( Account acct ) {
//do something in here that only a teller
//should do
}
```

View 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();
```

View 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和可自定义的customizableAuthenticationRealm支持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其实现都默认使用SimpleCredentialsMatcherSimpleCredentialsMatcher简单会对存储在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方法会被调用用来检测待检测权限是否隐含在其中

View File

@@ -0,0 +1,175 @@
- [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
SubjectSubject是一个安全术语通常意味着当前执行的用户。
```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。
对每个应用中只存在一个SecurityManagerSecurityManager是应用范围内的单例。默认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 {
//dont 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 files 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时的任何代码。**

View 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即measurementbucket被重置即代表oldest measurement会被重置
对于聚合结果的检查的开销是`O(1)`的,因为其是`pre-aggregated`并且和window size无关。
#### Time-based sliding window
Time-based sliding window其也是通过循环数组实现数组中含有`N`个partial aggregationbucket
如果time window大小是10秒那么circular array一直都会有10的buckets。每个bucket都对应了一个epoch secondbucket会对该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的permissionsnew 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接收一个Throwablethrowable为导致该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)
```

View 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
```

View 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中注册时其提供了关于其自身的元数据例如hostport判断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

View 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 clientSpring 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);
}
```

View 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核心概念
- Routegateway的基本构建单元Route由一个id、一个目标uripredicate集合、filter集合组成。如果聚合predicate为true那么route被匹配到
- PredicatePredicate的输入是` 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和filtershortcuts和全展开参数
### 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是一个用空格分隔的listlist元素为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;
}
}
```

View 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-mseureka-server会运行固定的scheduled task来清除过期的clienteviction-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。

View 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 objectfile 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

View 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项目的urlSpring 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的授权可以根据用户-权限、用户-角色、用户-角色-权限进行实现。
### 基于方法的授权
基于方法的授权可以通过向方法添加权限注解来实现。

View 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注解可以用于嵌套类的校验

View File

@@ -1,15 +1,15 @@
# POJO
## POJO定义
POJOPlain 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定义
POJOPlain 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接口也不一定要拥有无参构造方法。

View File

@@ -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);
```

View File

@@ -1,177 +1,177 @@
# Spring AOP
- ## Spring AOP的核心概念
- Aspect切面一个模块化的考虑
- Joint Point连接点程序执行时的一个时间点通常是方法的执行
- Advice当切面在一个切入点执行多做时执行的动作被称之为AdviceAdvice有不同的类型before、after、around
- Pointcut切入点advice通常运行在满足pointcut的join point上pointcut表达式与join point相关联Spring中默认使用AspectJ切入点表达式
- Introduction在类中声明新的方法、域变量甚至是接口实现
- linking将应用类型或对象和切面链接起来
- ## Spring AOP的类型
- before在连接点之前运行但是无法阻止后续连接点的执行
- after returning在连接点正常返回之后进行
- after throwing在链接点抛出异常正常退出之后进行
- after finally上两种的结合不管连接点是正常退出还是抛出异常退出都会在其之后执行
- aroundaround可以自定义连接点之前和之后的执行内容其也能够选择时候执行连接点的方法
- ## 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当切面在一个切入点执行多做时执行的动作被称之为AdviceAdvice有不同的类型before、after、around
- Pointcut切入点advice通常运行在满足pointcut的join point上pointcut表达式与join point相关联Spring中默认使用AspectJ切入点表达式
- Introduction在类中声明新的方法、域变量甚至是接口实现
- linking将应用类型或对象和切面链接起来
- ## Spring AOP的类型
- before在连接点之前运行但是无法阻止后续连接点的执行
- after returning在连接点正常返回之后进行
- after throwing在链接点抛出异常正常退出之后进行
- after finally上两种的结合不管连接点是正常退出还是抛出异常退出都会在其之后执行
- aroundaround可以自定义连接点之前和之后的执行内容其也能够选择时候执行连接点的方法
- ## 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

View File

@@ -1,183 +1,183 @@
# Spring Core IOC
- ## IOC容器和bean简介
- IOC简介
- IOC控制反转也被称之为依赖注入DI对象通过构造函数参数、工厂方法参数、或者在构造后通过setter来设置属性来定义依赖。在对象被创建时IOC容器会将依赖注入到bean对象中
- IOC容器
- IOC容器接口
- BeanFactoryBeanFactory是一个接口提供了高级配置功能来管理任何类型的对象
- ApplicationContextApplicationContext是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属性将会被作为其qualifierautowired时会根据@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容器接口
- BeanFactoryBeanFactory是一个接口提供了高级配置功能来管理任何类型的对象
- ApplicationContextApplicationContext是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属性将会被作为其qualifierautowired时会根据@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到类型匹配

View File

@@ -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进行回滚对于ErrorSpring事务也会执行回滚操作
- 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_REQUIREDPROPAGATION.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进行回滚对于ErrorSpring事务也会执行回滚操作
- 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_REQUIREDPROPAGATION.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语句仍然可以被提交***

View 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 sessionspring容器会针对session scope bean创建一个bean实例该实例在session范围内有效。session范围内可以任意修改bean实例的状态一个session内对bean的修改对其他session是不可见的。
当session会话结束时与该http session关联的bean对象也会被丢弃。
注解`@SessionScope`可以将bean对象定义为session scope。
### application scope
针对一个web applicationspring容器会创建一个bean对象实例。在这种情况下application scope bean将会范围将会被限定为ServletContext级别并且bean实例会作为`ServletContext`的属性被存储。
注解`@ApplicationScope`可以将bean对象的范围限定为`serveltContext`级别。

View File

@@ -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需要一个WebApplicationContextApplicationContext的拓展类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 chainpre/post/Controller)会被执行返回一个model用于渲染。
6. 如果model被返回那么返回的model会被渲染。如果model没有返回那么没有view会被渲染。
在WebApplicationContext中声明的HandlerExceptionResolver会用于解析处理请求中抛出的异常。
### 路径匹配
Servlet API将完整的请求路径暴露为requestURI并且进一步划分requestURI为如下部分contextPathservletPathpathInfo。
> #### contextPath, servletPath, pathInfo区别
> - contextPathcontextPath为应用被访问的路径是整个应用的根路径。默认情况下SpringBoot的contextPath为"/"。可以通过server.servlet.context-path="/demo"来改变应用的根路径。
> - servletPathservletPath代表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类型的请求值例如请求参数pathVariableheaderscookies或其他转化为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 exceptionRuntimeException更近
> // 故而会优先调用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
View 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
通过computecaffeine可以在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如果没有强引用指向valuevalue会被垃圾回收。同样地在整个cache中会使用`==`而不是`equals`来对value进行比较
#### softValues
软引用的value通常会在垃圾回收时按照lru的方式进行回收根据内存情况决定是否进行回收。由于使用软引用会带来性能问题通常更推荐使用基于max-size的回收策略。
同样地基于软引用的value在整个缓存中会通过`==`而不是`equals()`来进行垃圾回收。

View File

@@ -1,163 +1,177 @@
# 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、数字类型默认值为0boolean类型其默认值为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 APIJsonParser底层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、数字类型默认值为0boolean类型其默认值为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 APIJsonParser底层parser api将json串中数组转化为JsonArray并且为每个元素调用Gson.fromJson。该方法是推荐的方法
> gson.fromJson可以针对String、Reader、JsonElement来调用

View File

@@ -1,77 +1,85 @@
# 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.&lt;logger-name&gt;=&lt;level&gt;来定义事务级别事务级别可以是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.&lt;logger-name&gt;=&lt;level&gt;来定义事务级别事务级别可以是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
View 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 levellevel可以是`TRACE, DEBUG, INFO, WARN, ERROR`中的一个,其定义在`ch.qos.logback.classic.Level`类中。如果指定logger没有被指定level那么其会从`离其最近并且被指定level的先祖logger`中继承level。
为了保证所有logger都可以继承levelroot logger一定会被指定log level。默认情况下level为`debug`
> log level顺序为
>
> `TRACE < DEBUG < INFO < WARN < ERROR`
#### 获取Logger
在通过`LoggerFactory.getLogger`获取logger对象时如果向方法传入相同的名称那么会获得相同的logger对象。
通常应在每个class中指定一个loggerlogger 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都有其自己的encoderencoder通常在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
- encoderEncoder类型
- file字符串类型为写入文件的文件名如果文件不存在那么文件会被创建。如果该路径值中存在不存在的目录FileAppender会自动创建目录。
- bufferSizeFileSize类型当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>
```

View File

@@ -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

File diff suppressed because it is too large Load Diff

View 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
为了在特定的时间点执行taskSpring引入了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 rateTTWWWTTTWWT...(开始时间间隔为5)
> - fixed delayTTWWWWWTTTTWWWWWTTTTTTTWWWWWT...(上次结束和下次开始之间的间隔为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)\*/)
> 规则如下:
> - 所有字段都可以用(*)来匹配所有值
> - 逗号(,)可以用来分隔同一字段中多个值
> - 分号(-可以用来指定范围指定的范围左右都包含eg1-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 month1W代表该月的第一个工作日
> - 如果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 * * * *) |

View File

@@ -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两个对象
- 相比较于RedisTemplateStringRedisTemplate底层使用StringRedisSerializer来进行序列化其序列化的key和value都是可读的
- ## 序列化Serializer选择
- 除了先前提到的jdk默认的序列化机制和StringRedisSerializerSpring Data Redis还提供了其他Serializer
- 例如可以选择将key和value化为json字符串格式可以选用Jackson2JsonSerializer或者GenericJackson2JsonSerializer
> 相对于Jackson2JsonSerializerGenericJackson2JsonSerializer在序列化对象为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两个对象
- 相比较于RedisTemplateStringRedisTemplate底层使用StringRedisSerializer来进行序列化其序列化的key和value都是可读的
- ## 序列化Serializer选择
- 除了先前提到的jdk默认的序列化机制和StringRedisSerializerSpring Data Redis还提供了其他Serializer
- 例如可以选择将key和value化为json字符串格式可以选用Jackson2JsonSerializer或者GenericJackson2JsonSerializer
> 相对于Jackson2JsonSerializerGenericJackson2JsonSerializer在序列化对象为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;
}
}
```

View File

@@ -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那么自动装配将不会发生。

View File

@@ -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

View File

@@ -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
View 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.mapspel支持集合映射操作符
```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"
```

View 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属性的默认值为falseeviction操作在方法成功返回之后才执行如果该方法被cached或执行时抛出异常eviction操作不会执行
- 如果beforeInvocation属性被设置为true代表该eviction行为在方法被调用之前就会被执行
### @Caching
如果想要指定同一类型的多个注解(例如@CacheEvict或CachePut例如对不同的cachecondition和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
```

View 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的行为`进行封装。

View 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
```

File diff suppressed because it is too large Load Diff

View 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

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More