目录
[undo log(回滚日志)](#undo log(回滚日志))
[undo log 两大作用](#undo log 两大作用)
[Buffer Pool](#Buffer Pool)
[redo log(重做日志)](#redo log(重做日志))
[Redo log 的刷盘时机](#Redo log 的刷盘时机)
[redo log 和 undo log 区别](#redo log 和 undo log 区别)
[binlog (归档日志)](#binlog (归档日志))
[redo log 和 binlog 区别](#redo log 和 binlog 区别)
undo log(回滚日志)
是 Innodb 存储引擎层生成 的日志,实现了事务中的原子性 ,主要用于事务回滚和 MVCC。
在事务没提交之前,MySQL 会先记录更新前的数据到 undo log 日志文件里面,当事务回滚时,可以利用 undo log 来进行回滚。
每当 InnoDB 引擎对一条记录进行操作(修改、删除、新增)时,要把回滚时需要的信息都记录到 undo log 里,比如:
- 在插入一条记录时,要把这条记录的主键值记下来,这样之后回滚时只需要把这个主键值对应的记录删掉就好了;
- 在删除一条记录时,要把这条记录中的内容都记下来,这样之后回滚时再把由这些内容组成的记录插入到表中就好了;
- 在更新一条记录时,要把被更新的列的旧值记下来,这样之后回滚时再把这些列更新为旧值就好了。
在发生回滚时,就读取 undo log 里的数据,然后做原先相反操作。比如当 delete 一条记录时,undo log 中会把记录中的内容都记下来,然后执行回滚操作的时候,就读取 undo log 里的数据,然后进行 insert 操作。
一条记录的每一次更新操作产生的 undo log 格式都有一个 roll_pointer 指针和一个 trx_id 事务id
- 通过 trx_id 可以知道该记录是被哪个事务修改的;
- 通过 roll_pointer 指针可以将这些 undo log 串成一个链表,这个链表就被称为版本链;
undo log 两大作用
实现事务回滚,保障事务的原子性。事务处理过程中,如果出现了错误或者用户执行了 ROLLBACK 语句,MySQL 可以利用 undo log 中的历史数据将数据恢复到事务开始之前的状态。
实现 MVCC(多版本并发控制)关键因素之一。MVCC 是通过 Read View + undo log 实现的。undo log 为每条记录保存多份历史数据,MySQL 在执行快照读(普通 select 语句)的时候,会根据事务的 Read View 里的信息,顺着 undo log 的版本链找到满足其可见性的记录。
Buffer Pool
Innodb 存储引擎设计了一个缓冲池(Buffer Pool) ,来提高数据库的读写性能。
Buffer Pool 除了缓存「索引页」和「数据页」,还包括了**undo 页,**插入缓存、自适应哈希索引、锁信息等等。
undo 页
- 开启事务后,InnoDB 层更新记录前,首先要记录相应的 undo log,如果是更新操作,需要把被更新的列的旧值记下来,也就是要生成一条 undo log,undo log 会写入 Buffer Pool 中的 Undo 页面。
redo log(重做日志)
是 Innodb 存储引擎层生成 的日志,实现了事务中的持久性 ,主要用于断电等故障恢复;
为了防止断电导致数据丢失的问题,当有一条记录需要更新的时候,InnoDB 引擎就会先更新内存(同时标记为脏页),然后将本次对这个页的修改以 redo log 的形式记录下来,这个时候更新就算完成了。
WAL
- WAL (Write-Ahead Logging)技术,MySQL 的写操作并不是立刻写到磁盘上,而是先写日志,然后在合适的时机再写到磁盘上。(由后台线程将缓存在 Buffer Pool 的脏页刷新到磁盘)
Redo log 的刷盘时机
- 事务提交时:受 innodb_flush_log_at_trx_commit 参数控制。
- **0:**事务提交时不将 Redo log 刷入磁盘,而是由后台线程每秒刷新一次。这种方式在事务提交时不会触发写入磁盘的操作,但可能会导致数据丢失(最多丢失1秒的数据)。
- **1:**事务提交时立即将 Redo log 刷入磁盘。这是默认值,可以保证事务的持久性,但性能开销较大。
- **2:**事务提交时将 Redo log 写入操作系统缓存(Page Cache),但不立即刷入磁盘。操作系统会在适当的时候将缓存中的数据刷入磁盘。这种方式在事务提交时性能较好,但在操作系统崩溃时可能会丢失数据。
- log buffer 空间不足时:当 log buffer 使用量达到其总容量的一半左右时。
- 后台线程定期刷新:大约每秒刷新一次。
- 正常关闭服务器时:所有未刷入磁盘的 Redo log 都会被强制刷入磁盘。
在事务提交时,只要先将 redo log 持久化到磁盘即可,可以不需要等到将缓存在 Buffer Pool 里的脏页数据持久化到磁盘。当系统崩溃时,虽然脏页数据没有持久化,但是 redo log 已经持久化,接着 MySQL 重启后,可以根据 redo log 的内容,将所有数据恢复到最新的状态。
写入数据表是磁盘操作,写入 redo log 也是磁盘操作,同样都是写入磁盘,为什么不直接写入数据,而要先写入日志
- 刷盘是随机 I/O,而写日志是顺序 I/O,顺序 I/O 效率更高。因此先把修改写入日志,可以延迟刷盘时机,进而提升系统吞吐量。
- 将写操作从**「随机写」变成了「顺序写」**,提升 MySQL 写入磁盘的性能。
被修改 Undo 页面也需要记录对应 redo log
- 开启事务后,InnoDB 层更新记录前,首先要记录相应的 undo log,如果是更新操作,需要把被更新的列的旧值记下来,也就是要生成一条 undo log,undo log 会写入 Buffer Pool 中的 Undo 页面。不过,在内存修改该 Undo 页面后,需要记录对应的 redo log。
redo log 和 undo log 区别
这两种日志是属于 InnoDB 存储引擎的日志,它们的区别在于:
- redo log 记录了此次事务**「完成后」** 的数据状态,记录 的是更新之后的值;
- undo log 记录了此次事务**「开始前」** 的数据状态,记录 的是更新之前的值;
事务提交之前发生了崩溃,重启后会通过 undo log 回滚事务,事务提交之后发生了崩溃,重启后会通过 redo log 恢复事务
binlog (归档日志)
是 Server 层生成的日志,主要用于数据备份和主从复制;
MySQL 在完成一条更新操作后,Server 层还会生成一条 binlog,等之后事务提交的时候,会将该事物执行过程中产生的所有 binlog 统一写 入 binlog 文件。binlog 文件是记录了所有数据库表结构变更和表数据修改的日志,不会记录查询类的操作,比如 SELECT 和 SHOW 操作
redo log 和 binlog 区别
1、适用对象不同:
- binlog 是 MySQL 的 Server 层实现的日志,所有存储引擎都可以使用;
- redo log 是 Innodb 存储引擎实现的日志;
2、文件格式不同:
binlog 有 3 种格式类型,分别是 STATEMENT(默认格式)、ROW、 MIXED
- STATEMENT
- Binlog 记录的是 SQL 语句的文本形式,而不是具体的行变化。
- 占用的空间较小,但可能会导致数据不一致。
- 适用于简单的SQL 语句和确定性的操作。
- ROW
- Binlog 记录的是每一行数据的变化,而不是 SQL 语句。
- 更加准确和可靠,但占用的空间较大。
- 适用于需要高可靠性和数据一致性的场景。
- MIXED
- MySQL 自动选择使用 STATEMENT 格式还是 ROW 格式来记录 Binlog。
- 兼具 STATEMENT 和 ROW 格式的优点,自动选择最适合的格式。但配置和管理相对复杂。
- 适用于需要兼顾性能和数据一致性的场景。
- redo log 是物理日志,记录的是在某个数据页做了什么修改,比如对 XXX 表空间中的 YYY 数据页 ZZZ 偏移量的地方做了AAA 更新;
3、写入方式不同:
- binlog 是追加写,写满一个文件,就创建一个新的文件继续写,不会覆盖以前的日志,保存的是全量的日志。
- redo log 是循环写,日志空间大小固定,全部写满就从头开始,保存未被刷入磁盘的脏页日志。
4、用途不同:
- binlog 用于备份恢复、主从复制;
- redo log 用于掉电等故障恢复。
如果不小心整个数据库的数据被删除了,能使用 redo log 文件恢复数据吗
- 不可以使用 redo log 文件恢复,只能使用 binlog 文件恢复。
- 因为 redo log 文件是循环写,是会边写边擦除日志的,只记录未被刷入磁盘的数据的物理日志,已经刷入磁盘的数据都会从 redo log 文件里擦除。
- binlog 文件保存的是全量的日志,也就是保存了所有数据变更的情况,理论上只要记录在 binlog 上的数据,都可以恢复,所以如果不小心整个数据库的数据被删除了,得用 binlog 文件恢复数据。
主从复制
MySQL 的主从复制依赖于 binlog ,也就是记录 MySQL 上的所有变化并以二进制形式保存在磁盘上。复制的过程就是将 binlog 中的数据从主库传输到从库上。
MySQL 集群的主从复制过程梳理成 3 个阶段:
- 写入 Binlog:主库写 binlog 日志,提交事务,并更新本地存储数据。
- 同步 Binlog:把 binlog 复制到所有从库上,每个从库把 binlog 写到暂存日志中。
- 回放 Binlog:回放 binlog,并更新存储引擎中的数据。
具体详细过程如下:
- MySQL 主库在收到客户端提交事务的请求之后,会先写入 binlog,再提交事务,更新存储引擎中的数据,事务提交完成后,返回给客户端"操作成功"的响应。
- 从库会创建一个专门的 I/O 线程,连接主库的 log dump 线程,来接收主库的 binlog 日志,再把 binlog 信息写入 relay log 的中继日志里,再返回给主库"复制成功"的响应。
- 从库会创建一个线程,去读 relay log 中继日志,然后回放 binlog ,更新存储引擎中的数据,最终实现主从的数据一致性
在完成主从复制之后,你就可以在写数据时只写主库 ,在读数据时只读从库,这样即使写请求会锁表或者锁记录,也不会影响读请求的执行。
从库不是越多越好,因为从库数量增加,从库连接上来的 I/O 线程也比较多,主库也要创建同样多的 log dump 线程来处理复制的请求,对主库资源消耗比较高,同时还受限于主库的网络带宽。所以在实际使用中,一个主库一般跟 2~3 个从库(1 套数据库,1 主 2 从 1 备主),这就是一主多从的 MySQL 集群结构。
两阶段提交
为什么需要两阶段提交
事务提交后,redo log 和 binlog 都要持久化到磁盘 ,但是这两个是独立的逻辑,可能出现半成功 的状态,这样就造成两份日志之间的逻辑不一致。
举个例子,假设 id = 1 这行数据的字段 name 的值原本是 'jay',然后执行 UPDATE t_user SET name = 'xiaolin' WHERE id = 1; 如果在持久化 redo log 和 binlog 两个日志的过程中,出现了半成功状态,那么就有两种情况:
- **如果在将 redo log 刷入到磁盘之后, MySQL 突然宕机了,而 binlog 还没有来得及写入。**MySQL 重启后,通过 redo log 能将 Buffer Pool 中 id = 1 这行数据的 name 字段恢复到新值 xiaolin,但是 binlog 里面没有记录这条更新语句,在主从架构中,binlog 会被复制到从库,由于 binlog 丢失了这条更新语句,从库的这一行 name 字段是旧值 jay,与主库的值不一致性;
- **如果在将 binlog 刷入到磁盘之后, MySQL 突然宕机了,而 redo log 还没有来得及写入。**由于 redo log 还没写,崩溃恢复以后这个事务无效,所以 id = 1 这行数据的 name 字段还是旧值 jay,而 binlog 里面记录了这条更新语句,在主从架构中,binlog 会被复制到从库,从库执行了这条更新语句,那么这一行 name 字段是新值 xiaolin,与主库的值不一致性;
可以看到,在持久化 redo log 和 binlog 这两份日志的时候,如果出现半成功的状态,就会造成主从环境的数据不一致性。这是因为 redo log 影响主库的数据,binlog 影响从库的数据,所以 redo log 和 binlog 必须保持一致才能保证主从数据一致。
MySQL 为了避免出现两份日志之间的逻辑不一致的问题,使用了「两阶段提交」来解决,两阶段提交其实是分布式事务一致性协议,它可以保证多个逻辑操作要不全部成功,要不全部失败,不会出现半成功的状态。
两阶段提交把单个事务的提交拆分成了 2 个阶段,分别是「准备(Prepare)阶段」和「提交(Commit)阶段」,每个阶段都由协调者(Coordinator)和参与者(Participant)共同完成。注意,不要把提交(Commit)阶段和 commit 语句混淆了,commit 语句执行的时候,会包含提交(Commit)阶段。
两阶段提交的过程
- MySQL 使用了内部 XA 事务,内部 XA 事务由 binlog 作为协调者,存储引擎是参与者。当客户端执行 commit 语句或者在自动提交的情况下,MySQL 内部开启一个 XA 事务,分两阶段来完成 XA 事务的提交。
- 事务的提交过程有两个阶段,就是将 redo log 的写入拆成了两个步骤:prepare 和 commit,中间再穿插写入binlog,具体如下:
- prepare 阶段:将 XID(内部 XA 事务的 ID) 写入到 redo log,同时将 redo log 对应的事务状态设置为 prepare,然后将 redo log 持久化到磁盘(innodb_flush_log_at_trx_commit = 1 的作用);
- commit 阶段:把 XID 写入到 binlog,然后将 binlog 持久化到磁盘(sync_binlog = 1 的作用),接着调用引擎的提交事务接口,将 redo log 状态设置为 commit,此时该状态并不需要持久化到磁盘,只需要 write 到文件系统的 page cache 中就够了,因为只要 binlog 写磁盘成功,就算 redo log 的状态还是 prepare 也没有关系,一样会被认为事务已经执行成功;
数据库备份
数据库备份分为全量备份、增量备份。
- 全量备份
- 指备份当前时间点数据库中的所有数据,根据备份内容的不同,全量备份可以分为逻辑备份、物理备份两种方式。
- 逻辑备份
- 指备份数据库的逻辑内容,就是每张表中的内容通过 INSERT 语句的形式进行备份。
- 逻辑备份虽然好,但是它所需要的时间比较长,因为本质上逻辑备份就是进行 INSERT ... SELECT ... 的操作。
- 物理备份
- 物理备份直接备份数据库的物理表空间文件和重做日志,不用通过逻辑的 SELECT 取出数据。所以物理备份的速度,通常是比逻辑备份快的,恢复速度也比较快。
- 增量备份
- 数据库中的数据不断变化,我们不可能每时每分对数据库进行增量的备份。所以通过"全量备份 + 增量备份"的方式,构建完整的备份策略。
- 增量备份就是对日志文件进行备份 ,在 MySQL 数据库中就是二进制日志文件。
- 因为二进制日志保存了对数据库所有变更的修改,所以"全量备份 + 增量备份",就可以实现基于时间点的恢复(point in time recovery),也就是"通过全量 + 增量备份"可以恢复到任意时间点。
- 全量备份时会记录这个备份对应的时间点位,一般是某个 GTID 位置,增量备份可以在这个点位后重放日志,这样就能实现基于时间点的恢复。
- 如果二进制日志存在一些删库的操作,可以跳过这些点,然后接着重放后续二进制日志,这样就能对极端删库场景进行灾难恢复了。