5
5
- MySQL
6
6
---
7
7
8
-
9
-
10
8
> 本文来自公号程序猿阿星投稿,JavaGuide 对其做了补充完善。
11
9
12
10
## 前言
13
11
14
12
` MySQL ` 日志 主要包括错误日志、查询日志、慢查询日志、事务日志、二进制日志几大类。其中,比较重要的还要属二进制日志 ` binlog ` (归档日志)和事务日志 ` redo log ` (重做日志)和 ` undo log ` (回滚日志)。
15
13
16
- ![ ] ( https://cdn.jsdelivr.net/gh/18702524676/CND5/image/mysql/03 /01.png )
14
+ ![ ] ( https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide /01.png )
17
15
18
16
今天就来聊聊 ` redo log ` (重做日志)、` binlog ` (归档日志)、两阶段提交、` undo log ` (回滚日志)。
19
17
23
21
24
22
比如 ` MySQL ` 实例挂了或宕机了,重启时,` InnoDB ` 存储引擎会使用` redo log ` 恢复数据,保证数据的持久性与完整性。
25
23
26
- ![ ] ( https://cdn.jsdelivr.net/gh/18702524676/CND5/image/mysql/03 /02.png )
24
+ ![ ] ( https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide /02.png )
27
25
28
26
` MySQL ` 中数据是以页为单位,你查询一条记录,会从硬盘把一页的数据加载出来,加载出来的数据叫数据页,会放入到 ` Buffer Pool ` 中。
29
27
33
31
34
32
然后会把“在某个数据页上做了什么修改”记录到重做日志缓存(` redo log buffer ` )里,接着刷盘到 ` redo log ` 文件里。
35
33
36
- ![ ] ( https://cdn.jsdelivr.net/gh/18702524676/CND5/image/mysql/03 /03.png )
34
+ ![ ] ( https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide /03.png )
37
35
38
- > 图片笔误提示:第4步 “清空 redo log buffe 刷盘到 redo 日志中”这句话中的 buffe 应该是 buffer。
36
+ > 图片笔误提示:第 4 步 “清空 redo log buffe 刷盘到 redo 日志中”这句话中的 buffe 应该是 buffer。
39
37
40
38
理想情况,事务一提交就会进行刷盘操作,但实际上,刷盘的时机是根据策略来进行的。
41
39
53
51
54
52
另外,` InnoDB ` 存储引擎有一个后台线程,每隔` 1 ` 秒,就会把 ` redo log buffer ` 中的内容写到文件系统缓存(` page cache ` ),然后调用 ` fsync ` 刷盘。
55
53
56
- ![ ] ( https://cdn.jsdelivr.net/gh/18702524676/CND5/image/mysql/03 /04.png )
54
+ ![ ] ( https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide /04.png )
57
55
58
56
也就是说,一个没有提交事务的 ` redo log ` 记录,也可能会刷盘。
59
57
60
58
** 为什么呢?**
61
59
62
60
因为在事务执行过程 ` redo log ` 记录是会写入` redo log buffer ` 中,这些 ` redo log ` 记录会被后台线程刷盘。
63
61
64
- ![ ] ( https://cdn.jsdelivr.net/gh/18702524676/CND5/image/mysql/03 /05.png )
62
+ ![ ] ( https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide /05.png )
65
63
66
64
除了后台线程每秒` 1 ` 次的轮询操作,还有一种情况,当 ` redo log buffer ` 占用的空间即将达到 ` innodb_log_buffer_size ` 一半的时候,后台线程会主动刷盘。
67
65
68
66
下面是不同刷盘策略的流程图。
69
67
70
68
#### innodb_flush_log_at_trx_commit=0
71
69
72
- ![ ] ( https://cdn.jsdelivr.net/gh/18702524676/CND5/image/mysql/03 /06.png )
70
+ ![ ] ( https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide /06.png )
73
71
74
72
为` 0 ` 时,如果` MySQL ` 挂了或宕机可能会有` 1 ` 秒数据的丢失。
75
73
76
74
#### innodb_flush_log_at_trx_commit=1
77
75
78
- ![ ] ( https://cdn.jsdelivr.net/gh/18702524676/CND5/image/mysql/03 /07.png )
76
+ ![ ] ( https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide /07.png )
79
77
80
78
为` 1 ` 时, 只要事务提交成功,` redo log ` 记录就一定在硬盘里,不会有任何数据丢失。
81
79
82
80
如果事务执行期间` MySQL ` 挂了或宕机,这部分日志丢了,但是事务并没有提交,所以日志丢了也不会有损失。
83
81
84
82
#### innodb_flush_log_at_trx_commit=2
85
83
86
- ![ ] ( https://cdn.jsdelivr.net/gh/18702524676/CND5/image/mysql/03 /09.png )
84
+ ![ ] ( https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide /09.png )
87
85
88
86
为` 2 ` 时, 只要事务提交成功,` redo log buffer ` 中的内容只写入文件系统缓存(` page cache ` )。
89
87
97
95
98
96
它采用的是环形数组形式,从头开始写,写到末尾又回到头循环写,如下图所示。
99
97
100
- ![ ] ( https://cdn.jsdelivr.net/gh/18702524676/CND5/image/mysql/03 /10.png )
98
+ ![ ] ( https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide /10.png )
101
99
102
100
在个** 日志文件组** 中还有两个重要的属性,分别是 ` write pos、checkpoint `
103
101
@@ -110,11 +108,11 @@ tag:
110
108
111
109
` write pos ` 和 ` checkpoint ` 之间的还空着的部分可以用来写入新的 ` redo log ` 记录。
112
110
113
- ![ ] ( https://cdn.jsdelivr.net/gh/18702524676/CND5/image/mysql/03 /11.png )
111
+ ![ ] ( https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide /11.png )
114
112
115
113
如果 ` write pos ` 追上 ` checkpoint ` ,表示** 日志文件组** 满了,这时候不能再写入新的 ` redo log ` 记录,` MySQL ` 得停下来,清空一些记录,把 ` checkpoint ` 推进一下。
116
114
117
- ![ ] ( https://cdn.jsdelivr.net/gh/18702524676/CND5/image/mysql/03 /12.png )
115
+ ![ ] ( https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide /12.png )
118
116
119
117
### redo log 小结
120
118
155
153
156
154
可以说` MySQL ` 数据库的** 数据备份、主备、主主、主从** 都离不开` binlog ` ,需要依靠` binlog ` 来同步数据,保证数据一致性。
157
155
158
- ![ ] ( https://cdn.jsdelivr.net/gh/18702524676/CND5/image/mysql/04/01 .png )
156
+ ![ ] ( https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/01-20220305234724956 .png )
159
157
160
158
` binlog ` 会记录所有涉及更新数据的逻辑操作,并且是顺序写。
161
159
@@ -169,13 +167,13 @@ tag:
169
167
170
168
指定` statement ` ,记录的内容是` SQL ` 语句原文,比如执行一条` update T set update_time=now() where id=1 ` ,记录的内容如下。
171
169
172
- ![ ] ( https://cdn.jsdelivr.net/gh/18702524676/CND5/image/mysql/04/02 .png )
170
+ ![ ] ( https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/02-20220305234738688 .png )
173
171
174
172
同步数据时,会执行记录的` SQL ` 语句,但是有个问题,` update_time=now() ` 这里会获取当前系统时间,直接执行会导致与原库的数据不一致。
175
173
176
174
为了解决这种问题,我们需要指定为` row ` ,记录的内容不再是简单的` SQL ` 语句了,还包含操作的具体数据,记录内容如下。
177
175
178
- ![ ] ( https://cdn.jsdelivr.net/gh/18702524676/CND5/image/mysql/04/03 .png )
176
+ ![ ] ( https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/03-20220305234742460 .png )
179
177
180
178
` row ` 格式记录的内容看不到详细信息,要通过` mysqlbinlog ` 工具解析出来。
181
179
199
197
200
198
` binlog ` 日志刷盘流程如下
201
199
202
- ![ ] ( https://cdn.jsdelivr.net/gh/18702524676/CND5/image/mysql/04/04 .png )
200
+ ![ ] ( https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/04-20220305234747840 .png )
203
201
204
202
- ** 上图的 write,是指把日志写入到文件系统的 page cache,并没有把数据持久化到磁盘,所以速度比较快**
205
203
- ** 上图的 fsync,才是将数据持久化到磁盘的操作**
@@ -208,15 +206,15 @@ tag:
208
206
209
207
为` 0 ` 的时候,表示每次提交事务都只` write ` ,由系统自行判断什么时候执行` fsync ` 。
210
208
211
- ![ ] ( https://cdn.jsdelivr.net/gh/18702524676/CND5/image/mysql/04/05 .png )
209
+ ![ ] ( https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/05-20220305234754405 .png )
212
210
213
211
虽然性能得到提升,但是机器宕机,` page cache ` 里面的 binlog 会丢失。
214
212
215
- 为了安全起见,可以设置为` 1 ` ,表示每次提交事务都会执行` fsync ` ,就如同** redo log 日志刷盘流程** 一样。
213
+ 为了安全起见,可以设置为` 1 ` ,表示每次提交事务都会执行` fsync ` ,就如同 ** redo log 日志刷盘流程** 一样。
216
214
217
215
最后还有一种折中方式,可以设置为` N(N>1) ` ,表示每次提交事务都` write ` ,但累积` N ` 个事务后才` fsync ` 。
218
216
219
- ![ ] ( https://cdn.jsdelivr.net/gh/18702524676/CND5/image/mysql/04/06 .png )
217
+ ![ ] ( https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/06-20220305234801592 .png )
220
218
221
219
在出现` IO ` 瓶颈的场景里,将` sync_binlog ` 设置成一个比较大的值,可以提升性能。
222
220
@@ -232,33 +230,33 @@ tag:
232
230
233
231
在执行更新语句过程,会记录` redo log ` 与` binlog ` 两块日志,以基本的事务为单位,` redo log ` 在事务执行过程中可以不断写入,而` binlog ` 只有在提交事务时才写入,所以` redo log ` 与` binlog ` 的写入时机不一样。
234
232
235
- ![ ] ( https://cdn.jsdelivr.net/gh/18702524676/CND5/image/mysql/05/01 .png )
233
+ ![ ] ( https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/01-20220305234816065 .png )
236
234
237
235
回到正题,` redo log ` 与` binlog ` 两份日志之间的逻辑不一致,会出现什么问题?
238
236
239
237
我们以` update ` 语句为例,假设` id=2 ` 的记录,字段` c ` 值是` 0 ` ,把字段` c ` 值更新成` 1 ` ,` SQL ` 语句为` update T set c=1 where id=2 ` 。
240
238
241
239
假设执行过程中写完` redo log ` 日志后,` binlog ` 日志写期间发生了异常,会出现什么情况呢?
242
240
243
- ![ ] ( https://cdn.jsdelivr.net/gh/18702524676/CND5/image/mysql/05/02 .png )
241
+ ![ ] ( https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/02-20220305234828662 .png )
244
242
245
243
由于` binlog ` 没写完就异常,这时候` binlog ` 里面没有对应的修改记录。因此,之后用` binlog ` 日志恢复数据时,就会少这一次更新,恢复出来的这一行` c ` 值是` 0 ` ,而原库因为` redo log ` 日志恢复,这一行` c ` 值是` 1 ` ,最终数据不一致。
246
244
247
- ![ ] ( https://cdn.jsdelivr.net/gh/18702524676/CND5/image/mysql/05/03 .png )
245
+ ![ ] ( https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/03-20220305235104445 .png )
248
246
249
247
为了解决两份日志之间的逻辑一致问题,` InnoDB ` 存储引擎使用** 两阶段提交** 方案。
250
248
251
249
原理很简单,将` redo log ` 的写入拆成了两个步骤` prepare ` 和` commit ` ,这就是** 两阶段提交** 。
252
250
253
- ![ ] ( https://cdn.jsdelivr.net/gh/18702524676/CND5/image/mysql/05/04 .png )
251
+ ![ ] ( https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/04-20220305234956774 .png )
254
252
255
253
使用** 两阶段提交** 后,写入` binlog ` 时发生异常也不会有影响,因为` MySQL ` 根据` redo log ` 日志恢复数据时,发现` redo log ` 还处于` prepare ` 阶段,并且没有对应` binlog ` 日志,就会回滚该事务。
256
254
257
- ![ ] ( https://cdn.jsdelivr.net/gh/18702524676/CND5/image/mysql/05/05 .png )
255
+ ![ ] ( https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/05-20220305234937243 .png )
258
256
259
257
再看一个场景,` redo log ` 设置` commit ` 阶段发生异常,那会不会回滚事务呢?
260
258
261
- ![ ] ( https://cdn.jsdelivr.net/gh/18702524676/CND5/image/mysql/05/06 .png )
259
+ ![ ] ( https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/06-20220305234907651 .png )
262
260
263
261
并不会回滚事务,它会执行上图框住的逻辑,虽然` redo log ` 是处于` prepare ` 阶段,但是能通过事务` id ` 找到对应的` binlog ` 日志,所以` MySQL ` 认为是完整的,就会提交事务恢复数据。
264
262
0 commit comments