type
status
date
slug
summary
tags
category
icon
password
创建时间
Nov 26, 2024 08:07 AM
引出
现在有这样一个问题,我有两张表需要对这两张表的一些字段进行修改,希望保证这两个数据,要么都不修改,要么都修改。也就是对两张表的修改操作是原子性的,我们该如何保证?我相信大家首先会想到使用数据库「事务」,将两个修改放在一个事务中,当发生异常时,可以通过回滚的机制,可以保证操作的原子性。我们通过事务的机制,一定可以保证最后的数据和我们的预期时一样的吗?
我们设想这样一种情况,当你修改完第一个表的数据,并已经落盘后。接下来,想修改继续修改第二个表的数据时,断电了。此时数据保证了原子性了没有。你说我们可以将数据先保存到内存中,等所有操作都完成后,再将所有数据落盘持久化。——未提交事务
那现在,又有一个新问题:我在写入数据的时候,刚将第一个表的数据修改完成后,此时恰好又断电了,结果第二个表的数据还在内存中,没来得及写入硬盘,又和我们的预期不一致了。——已提交事务(数据未落盘)
通过上面两个例子,我们发现原子性和持久性似乎是绑定在一起。原子性是指事务的多个操作要么都生效要么都不生效,不会存在中间状态;持久性是指一旦事务生效,就不会再因为任何原因而导致其修改的内容被撤销或丢失。第一种情况,我们及时将数据落盘,在一定程度上,保证了数据的“持久性”,但没办法保证原子性;第二张情况,我们将数据修改完成后,再落盘,保证了数据的“原子性”,但确没有保证数据的持久性。最终反映都是与我们预期的结果不一致。说到底,是“写入磁盘”这个操作不会是原子的,不仅有“写入”与“未写入”,还客观地存在着“正在写”的中间状态。这也是实现原子性和持久性所面临的困难。
三种日志
既然,写入磁盘不是原子性的,我们是不是可以加一个中间层,将数据的修改先写入到中间层。使用中间层保证数据是完整的,再同步到数据库中呢。答案是可行的,通过日志实现事务的原子性和持久性是当今的主流方案。(我们这里不说为什么)
我们知道在 MySQL 的 InnoDB 引擎的情况下,我们有两个比较重要的日志:Redo Log 、Undo log。(还有一个是 binlog,这个是 MySQL 的 Server 层面的日志文件)
- redo log 重做日志,是 InnoDB 存储引擎层生成的日志,实现了事务中的持久性;
- undo log 回滚日志,是 InnoDB 存储引擎层生成的日志,实现了事务中的原子性。
- binlog 二进制日志,是 Server 层生成的日志,主要用于数据备份和主从复制;
redo log 和 undo log 的区别
redo log 和 undo log 这两种日志是属于 InnoDB 存储引擎的日志,它们的区别在于:
- redo log 记录了此次事务「完成后」的数据状态,记录的是更新之后的值;
- undo log 记录了此次事务「完成前」的数据状态,记录的是更新之前的值;
redo log 和 binlog 的区别
redo log 和 binlog 的区别在于:
- redo log 是 InnoDB 引擎特有的;binlog 是 MySQL 的 Server 层实现的,所有引擎都可以使用。
- redo log 是物理日志,记录的是“在某个数据页上做了什么修改”;binlog 是逻辑日志,记录的是这个语句的原始逻辑,比如“给
ID=2
这一行的 c 字段加1
”。
- redo log 是循环写的,空间固定会用完;binlog 是可以追加写入的(“追加写”是指 binlog 文件写到一定大小后会切换到下一个,并不会覆盖以前的日志)
binlog为什么说是逻辑日志呢?它里面有内容也会存储成物理文件,怎么说是逻辑而不是物理?
逻辑日志可以给别的数据库,别的引擎使用,已经大家都讲得通这个“逻辑”;
物理日志就只有“我”自己能用,别人没有我的“物理格式”。
引入日志后的原子性与持久性
Undo log 是如何保证原子性的?
undo log 是一种用于撤销回退的日志,在事务没提交之前,MySQL 会先记录更新前的数据到 undo log 日志文件里面。在出现故障或需要回滚时,undo log 可以将数据库状态恢复到事务开始之前的状态,从而保证了原子性。当事务
Redo log 是如何保证持久性的?
在更改内存 Buffer Pool 的数据时,同时会将记录写到 Redo log,并将该页标记为脏页,并使用 WAL 技术,将内存中的脏页写入到磁盘中。
当数据完成修改后,内存中修改的数据在 Redo log 中也有对应的记录。
如果发生故障,即使有脏页没有写入到磁盘中,但是 redo log 已经持久化。当 MySQL 重启是,从 redo log 读取数据,在做一遍就行了。(不然怎么叫 redo log 呢!!!)
再次分析
我还是回到刚才提到的两种情况,在引入日志后,是如何变化的:
- 事务未提交:当事务未提交时,我们有 Undo log 在记录了数据的反向操作。
- 当此时执行的过程中发生异常时,我们可以通过 Undo log 中反向操作回滚内存中的数据;
- 如果是断电之类的故障,这一次事务没有执行完成。而我们采用 WAL 技术,延迟写入磁盘,且内存是易失性的,断电就没了,数据还没有完全写入到磁盘中,那就没办法了(不过在一定程度上,也保持了原子性)。此时,我们在重启的时候,可以直接通过 Redo log 回到上一次事务提交的状态。
- 事务已经提交:当事务准备提交时,此时 Undo log 的数据落盘了的,数据库的数据也落盘了。(这里不考虑两阶段提交,这个是保证 binlog 和 redo log 一致性的)
- 事务已经提交,redo log 正在落盘的时候,数据写入了部分,发生了故障。我们需要保证数据回到上一次。而 redo log 已经被部分污染,不准确了。此时,我们在恢复的时候,数据库的数据和 Undo log 的数据已经落盘,我们根据 Undo Log 中的信息回滚这些事务。
具体来说,在崩溃恢复时,会以此经历以下三个阶段:
- 分析阶段(Analysis):该阶段从最后一次检查点(Checkpoint,可理解为在这个点之前所有应该持久化的变动都已安全落盘)开始扫描日志,找出所有没有 End Record 的事务,组成待恢复的事务集合(一般包括 Transaction Table 和 Dirty Page Table)。
- 重做阶段(Redo):该阶段依据分析阶段中,产生的待恢复的事务集合来重演历史(Repeat History),找出所有包含 Commit Record 的日志,将它们写入磁盘,写入完成后增加一条 End Record,然后移除出待恢复事务集合。
- 回滚阶段(Undo):该阶段处理经过分析、重做阶段后剩余的恢复事务集合,此时剩下的都是需要回滚的事务(被称为 Loser),根据 Undo Log 中的信息回滚这些事务。
我的理解是
- 当这次事务未提交时,上次的事务中有完整的 Redo log(Commit Record 的日志),直接恢复就好了;
- 当事务已经提交了,此时 Redo log 中存在没有 Commit Record 的日志。这就到了第三个阶段,回滚这个事务。
update语句的具体执行过程是怎样的?
具体更新一条记录
UPDATE t_user SET name = 'xiaolin' WHERE id = 1;
的流程如下:- 执行器负责具体执行,会调用存储引擎的接口,通过主键索引树搜索获取
id = 1
这一行记录: - 如果
id=1
这一行所在的数据页本来就在 buffer pool 中,就直接返回给执行器更新; - 如果记录不在 buffer pool,将数据页从磁盘读入到 buffer pool,返回记录给执行器。
- 执行器得到聚簇索引记录后,会看一下更新前的记录和更新后的记录是否一样:
- 如果一样的话就不进行后续更新流程;
- 如果不一样的话就把更新前的记录和更新后的记录都当作参数传给 InnoDB 层,让 InnoDB 真正的执行更新记录的操作;
- 写入undo log:开启事务, InnoDB 层更新记录前,首先要记录相应的 undo log,因为这是更新操作,需要把被更新的列的旧值记下来,也就是要生成一条 undo log,undo log 会写入 Buffer Pool 中的 Undo 页面,不过在内存修改该 Undo 页面后,需要记录对应的 redo log。
- 更新内存中的数据(Buffer Pool),并记录 redo log:InnoDB 层开始更新记录,会先更新内存(同时标记为脏页),然后将记录写到 redo log 里面。至此,一条记录更新完了。
为了减少磁盘I/O,不会立即将脏页写入磁盘,后续由后台线程选择一个合适的时机将脏页写入到磁盘。这就是 WAL 技术:MySQL 的写操作并不是立刻写到磁盘上,而是先写 redo 日志,然后在合适的时间再将修改的行数据写到磁盘上。
- 事务提交——两阶段提交:
- prepare 阶段:将 redo log 对应的事务状态设置为 prepare,然后将 redo log 刷新到硬盘。同时,将相关信息记录到 binlog 的缓冲区;
- commit 阶段:将 binlog 刷新到磁盘,然后将 redo log 状态设置为 commit;
- 至此,一条更新语句执行完成。