UPDATE test SET name = 'abc' WHERE id = 1;
更新语句的执行流程与查询语句类似,同样是需要经过下图的流程。首先清空对应表的缓存,经过分析器,优化器,执行器最终操作存储引擎修改表中数据。
除此之外,MySQL为了保证在高并发更新数据时的效率以及数据的恢复能力,引入了两个日志模块,redo log
和bin log
。
redo log
redo log
实际上是用于提高更新效率的,尤其是短时间内有大量更新操作的时候,效果更加明显。
考虑没有redo log
的情况下更新操作的流程效率。到达执行器阶段,马上查找到对应数据,然后进行一次IO操作写入磁盘,这里要注意的是一次磁盘IO操作是成本很高的。更新操作不频繁的情况下也许勉强能应付,一旦操作非常频繁就会导致大量的磁盘IO,效率也就很低了。
redo log
正是为了解决这个问题而引入的,首先,redo log
是InnoDB
引擎内的。每一次的更新操作,都会记录在redo log
中,等数据库空闲的时候,再将这些操作通过一次IO操作写入表中。
这就像上课的时候可以将笔记临时记录在小本本上,回头再整理到笔记本。记账的时候也一样,redo log
就像这里的小本本。
实际执行更新语句时,首先会记录到buffer pool
中,同时记录到redo log buffer
,之后再将数据刷入redo log
(硬盘文件)中。
redo log
写入磁盘的情况有两种,一种是数据库空闲的时候,一种是redo log
写满的时候,写满的时候会将一部分记录写入磁盘。
如图,redo log
是可以循环写的,也就是写完最后一个文件时会重新从第一个文件开始写,因此这里画成了一个圈代表redo log
,其中write pos
指针表示当前写的位置,check pos
的位置表示当前写入磁盘的开始位置,即这两个指针中间空的部分是redo log
中空闲的部分,若write pos
追上check pos
时,那么就需要先将部分记录写入磁盘,check pos
后移即可。
redo log相关参数中,innodb_log_file_size
和innodb_log_files_in_group
的乘积表示 redo log 的大小。
innodb_flush_log_at_trx_commit
0:表示每次事务提交时都只是把 redo log 留在 redo log buffer 中 ;
1:表示每次事务提交时都将 redo log 直接持久化到磁盘;
2:表示每次事务提交时都只是把 redo log 写到 page cache。
mysql> show variables like '%innodb_log_file%';
+---------------------------+----------+
| Variable_name | Value |
+---------------------------+----------+
| innodb_log_file_size | 50331648 |
| innodb_log_files_in_group | 2 |
+---------------------------+----------+
2 rows in set
mysql> show variables like 'innodb_flush_log_at_trx_commit';
+--------------------------------+-------+
| Variable_name | Value |
+--------------------------------+-------+
| innodb_flush_log_at_trx_commit | 1 |
+--------------------------------+-------+
1 row in set
bin log
MySQL想要具备恢复到过去某一时刻的能力,就需要有每次数据变动的归档记录,bin log
就是用于解决这个问题的,例如,数据库每周进行一次整库备份,假如前天刚做完整库备份,要恢复到昨天某一时刻,只需要利用最近那次整库备份和bin log
即可恢复。
bin log 和 redo log 的区别:
bin log
是属于Server层的,而redo log
是属于InnoDB
特有的bin log
记录的是操作逻辑,即某时刻对表xxx中ID=2这一行字段c加1。- redo log 是循环写的,而 bin log 是追加写的,也就是不会覆盖以前的数据
结合 bin log 的定位很容易就能理解上述的区别,bin log 是归档日志,用于数据恢复的,恢复的时候需要按时间先后顺序恢复(相当于记录在 bin log 中的操作逻辑以整库备份的基点开始重新执行一遍)。
而 redo log 是临时性的,自然不需要像 bin log 那样记录完备。记录的内容也比 bin log 简单,因为只要 redo log 中的脏数据落盘,就不在需要了。
两阶段提交
在了解了两个日志模块后,再来看看一条更新语句会怎么执行:
- 执行器操作引擎找到相关数据
- 执行器拿到引擎给的行数据,执行UPDATE操作,得到新的一行数据,再调用引擎接口写入这行新数据。
- 引擎将这行新数据更新到内存中(buffer pool),同时将这个更新操作记录到 redo log 里面,此时 redo log 处于
prepare
状态。然后告知执行器执行完成了,随时可以提交事务。 - 执行器生成这个操作的 binlog,并把 binlog 写入磁盘。
- 执行器调用引擎的提交事务接口,引擎把刚刚写入的 redo log 改成提交(commit)状态,更新完成。
上述流程中,最关键的就是先写 redo log 并置于准备状态,再写 bin log,提交事务并将 redo log 置于 commit 状态。这就是两阶段提交。
为什么要这样操作呢?
两阶段提交是为了保证数据恢复时数据的准确性。
假设现在我们不使用两阶段提交,有两种情况,先写redo log,再写bin log,或者反之。
先写redo log,再写bin log
当写完 redo log时,数据库异常重启,这时候,表中数据可以通过 redo log 恢复,但是 bin log 没有记录归档,因此在数据恢复时,这一块数据会不准确
先写bin log,再写redo log
同样的,当写完 bin log 之后异常重启,由于没有写 redo log,因此表中数据还是原来的数据,没能恢复,但是归档日志中却已经记录了这个操作了。
因此两阶段提交实际上是保证了两个日志模块逻辑上的一致性。
至于为什么会有两个日志,是因为MySQL最开始默认的存储引擎是MyISAM,而InnoDB是后来增加的,也就是说 bin log 比 redo log 出现的更早,由于 MyISAM 引擎没有事务,而InnoDB是有事务的,因此需要有crash-safe的能力,所以就引入了redo log,为了保证这两个日志的逻辑一致,就需要两阶段提交。