musql事务的ACID特性概述
在深入理解Undo Log、Redo Log和Binlog之前,首先需要明确事务的ACID特性,这些特性是确保数据库操作可靠性和一致性的基石
- 原子性(Atomicity) :事务中的所有操作要么全部成功,要么全部失败,不会出现部分完成的状态
- 一致性(Consistency) :事务的执行必须使数据库从一个一致性状态转变到另一个一致性状态,确保数据的完整性
- 隔离性(Isolation) :指在多事务并发执行时,一个事务的操作对其他事务的影响程度。它确保事务之间的操作是相互独立的,避免并发带来的数据不一致问题
- 持久性(Durability) :一旦事务提交,其对数据库的修改是永久性的,即使系统发生故障也不会丢失
Undo Log(回滚日志)
1 undo log 定义与主要作用
Undo Log记录了事务在修改数据之前的原始状态,用于在事务回滚时撤销未完成的修改,确保事务的原子性和隔离性
Undo Log主要的功能有两个:事务回滚和MVCC
- 实现事务回滚,保障事务原子性
- 与 Read-View 一起构成了 MVCC 的基础
首先来说事务回滚,事务如何通过Undo Log进行回滚操作呢?其实很简单,只需要在Undo Log日志中记录事务中的反向操作即可,发生回滚时直接通过Undo Log中记录的反向操作进行恢复。例如:
- 事务进行insert操作,Undo Log记录delete操作。
- 事务进行delete操作,Undo Log记录insert操作。
- 事务进行update操作(value1 改为value2 ),Undo Log记录update操作(value2 改为value1 )。
Undo Log 保存的是一个版本的链路,使用roll_pointer这个字段来连接的。多个事务的Undo Log 日志组成了一个版本链,如图:
在上图中: 一条记录的每一次更新操作产生的 undo log 格式都有一个 roll_pointer(回滚指针)和一个 trx_id( 事务 id)
-
trx_id代表事务id,记录了这一系列事务操作是基于哪个事务。
-
roll_pointer代表回滚指针,就是当要发生rollback回滚操作时,就通过roll_pointer进行回滚,这个链表称为版本链。 在事务执行过程中,如果发生错误或主动回滚,Undo Log 会将数据恢复到事务开始前的状态,确保事务的所有操作要么全部完成,要么全部撤销,从而实现原子性。
为了 提升 Undo Log 读写性能, Undo页还存在于Buffer Pool中,因为Buffer Pool 是 InnoDB 的内存缓存,用于存储数据页和索引页,以便快速访问。它通过减少磁盘 I/O,提高数据库的整体性能。将Undo页放在缓存中,可以加速事务的回滚和数据恢复。
当事务commit之后,不会立即删除,会保留至所有快照读完成。后续会通过后台线程中的Master Thread或Purge Thread进行Undo Page的回收工作。
再说MVCC,实现了自己 Copy-On-Write思想提升并发能力的时候, 也需要数据的副本,如上图,既然已经存在了这么多Undo Log的副本,那么MVCC可以直接复用这些副本数据。
所以,Undo Log中的副本,可以用于实现多版本并发控制(MVCC),提升事务的并发性能,同时每一个事务操作自己的副本,实现事务的隔离性。
实现MVCC主要通过三个元素,一个是我们上面已经提到的Undo Log版本链,一个是readView,最后就是我们上面已经提到的这些字段。因为整个课题比较大,在这里就不在过多的赘述。
2 undo log 的类型与清理时机
不同类型的写操作需要记录的内容也是不同的,所以产生的 undo log 格式也是不同。在 InnoDB 中,可分为
- insert undo log
在 insert 操作中产生的 undo log 被称为 insert undo log。由于 insert 操作的记录只对当前事务本身可见,对其他事务不可见(否则就是幻读了),因此该 uodo log 可以在事务提交后直接删除。不需要通过 purge 线程去清理。
- update undo log
在 delete 或 update 操作时产生的 undo log 被称为 update undo log。由于 MVCC 中可能会用到该 undo log,因此不能再事务提交后立刻删除。因此该 uodo log 会被加入到一个链表中,等待 purge 线程去清理。
3 undo log 生命周期
事务执行期间,在记录发生更新前,首先要记录相应的 undo log。如果是 update 操作,需要把被更新的列的旧值记下来,也就是要生成一条 undo log,并根据该 undo log 去修改 Buffer Pool 中的 Undo 页面。
在修改该 Undo 页面后,也是需要记录对应的 redo log,因为 undo log 需要基于 redo log 来实现持久化
此外,内存中的 undo log 是会被删除清理的,例如 insert 操作在事务提交之后就可以清除掉了对应的 undo log;update 或 delete 操作则由后台线程 purge 进行清理
Redo Log(重做日志)
1 Redo Log作用
Redo Log记录了事务对数据的修改操作,用于在系统崩溃后恢复已提交事务的修改,确保事务的持久性
2 为什么需要 redo log
为了提高数据库的读写能力,MySQL 引入了 Buffer Pool:
当读取数据时,如果数据存在于 Buffer Pool 中,客户端就会直接读取 Buffer Pool 中的数据,否则再去磁盘中读取。
当修改数据时,如果数据存在于 Buffer Pool 中,那直接修改 Buffer Pool 中数据所在的页,然后将其页设置为脏页(该页的内存数据和磁盘上的数据已经不一致),为了减少磁盘I/O,不会立即将脏页写入磁盘。 但 Buffer Pool 是基于内存的,而内存总是不可靠,万一断电重启,还没来得及落盘的脏页数据就会丢失。
为了避免这个问题,一种做法是:
- 在事务提交完成之前把该事务所修改的所有页面都刷新到磁盘?(记为方法一)
但 MySQL 的设计者并没有使用这种做法,因此这个简单粗暴的做法有两大问题:
- 修改量与刷新磁盘工作量严重不成正比
我们知道 MySQL 内存和磁盘交互的最小单位是页,这意味着在上面的做法中,即使我们只修改了一条记录,也需要把整个页都刷到磁盘(刷脏页),这就好比快递员送快递时每次只送一个包裹,效率是很低的。
- 随机 IO 速度慢
一个事务可能会修改多个数据页,加入这些页面并不相邻,就意味着将某个事务修改的 Buffer Pool 中的脏页刷新到磁盘时,会进行很多的随机 IO,而随机 IO 比顺序 IO 慢很多,尤其是对于传统的机械硬盘来说。
- 另一种做法是采用 redo log (记为方法二)。
redo log 是一种物理日志,记录了某个数据页做了什么修改,比如对 XXX 表空间中的 YYY 数据页 ZZZ 偏移量的地方做了AAA 更新,每当执行一个读写事务就会产生这样的一条或者多条这样的日志。当 MySQL 实例挂了或宕机了,重启时,InnoDB 存储引擎就会使用 redo log 恢复数据,保证数据的持久性与完整性。
3 Redo Log 日志执行过程
当一个更新事务到达时,Redo Log的处理过程如下:
- 首先,从磁盘读取原始数据到Buffer Pool(内存中),然后在内存中对数据进行修改,修改后的数据将被标记为脏页。
- 接着,系统会生成一系列重做日志,并将其写入Redo Log Buffer,日志内容记录的是数据修改后的新值。
- 当事务提交(commit)时,Redo Log Buffer中的内容会被刷新到Redo Log File中,并采用追加写的方式将日志记录写入文件。
- 定期会将内存中修改过的数据刷新回磁盘,以保证数据持久性。
**Redo Log利用WAL(Write-Ahead Logging)机制来保证故障恢复的安全性(crash-safe)。 **
WAL的核心思想是先写日志,再写磁盘。根本原因是机械磁盘的性能,日志是顺序写,而数据页是随机写。顺序写的性能更高,所以先把日志归档。具体来说,当缓存页被修改(即变成脏页)后,相关的操作会先记录到Redo Log Buffer中。在事务提交(commit)时,后台线程会将Redo Log Buffer中的内容刷新到磁盘上(事务提交是Redo Log默认刷盘的时机)。此时,虽然脏页还没有写回磁盘,但只要Redo Log成功写入磁盘,就可以认为此次修改操作已完成。这是因为,即使发生故障导致脏页丢失,我们也可以通过磁盘上的Redo Log来恢复数据。因此,Redo Log与Undo Log的配合作用如下:
- 若事务在提交前发生崩溃,则可以通过Undo Log回滚事务
- 若事务在提交后发生崩溃,则可以通过Redo Log来恢复事务
4 Redo Log日志文件写入机制
Redo Log采用固定大小并循环写入的方式,类似环形缓冲区。当日志文件写满时,会从头开始覆盖之前的内容。这样的设计是因为Redo Log记录的是数据页的修改,而一旦Buffer Pool中的数据页被刷写到磁盘,之前的Redo Log记录就不再有效。新的日志会覆盖这些过时的记录。此外,硬盘上的Redo Log文件并非单一存在,而是以文件组的形式存储,每个文件的大小都相同。比如可以配置为一组 4 个文件,每个文件的大小是 1GB,整个 redo log 日志文件组可以记录 4G 的内容。它采用的是环形数组形式,从头开始写,写到末尾又回到头循环写,如下图所示:
在写入数据的同时,也需要执行擦除操作。Redo Log成功刷盘到磁盘后,才可以进行擦除。因此,我们使用两个指针来管理这一过程:
- write pos:表示当前日志记录写入的位置,一边写一边后移,即当前Redo Log文件已写到哪里
- checkpoint:表示当前可以擦除的位置,即Redo Log文件中哪些记录已不再需要,可以被新的日志覆盖
在图示中,黄色部分表示已写入完成的区域,而绿色部分则代表空闲区域。
write pos和checkpoint指针之间的绿色区域,表示剩余的可写入空间,即Redo Log文件的空闲/可用部分。
当进行数据页刷盘操作(checkpoint)时,checkpoint指针会顺时针移动,覆盖掉已写入的黄色区域,使其变为绿色。
当write pos追赶上checkpoint时,意味着Redo Log文件已满,此时必须强制执行checkpoint操作,刷新Buffer Pool中的脏页并将其写入磁盘。随后,checkpoint指针会被移动,这样就可以继续向Redo Log文件中写入新的数据。
5 redo log 刷盘策略(非刷脏页)
事实上 redo log 刷盘时,并不是直接由 redo log buffer 刷到 redo log file,而是先写入到 page cache(文件系统缓存),再调用 fsync 方法同步到 redo log file 中
而什么时候写入到 page cache,什么时候同步到 redo log file,是由我们接下来要说的 redo log 刷盘策略决定的。
在 InnoDB 中通过 innodb_flush_log_at_trx_commit 参数来控制事务提交时 redo log 的刷盘策略:
- 设置为 0:
设置为 0 的时候,表示每次事务提交时即不写到 page cache,也不同步到 redo log file。由 InnoDB 后台线程每隔 1 秒去写入 page cache,然后调用 fsync 同步到 redo log file。这种方式性能最高,但是也最不安全,因为如果 MySQL 挂了或宕机了,可能会丢失最近 1 秒内的事务。
- 设置为 1:
设置为 1 的时候,表示每次事务提交时会写到 page cache,并同步到 redo log file。这种方式性能最低,但是也最安全,因为只要事务提交成功,redo log 记录就一定在磁盘里,不会有任何数据丢失。
- 设置为 2:
设置为 2 的时候,表示每次事务提交时都只把 redo log buffer 里的 redo log 内容写入 page cache。再由操作系统(os)决定什么时候同步到 redo log file 中(依赖于操作系统后台线程)。这种方式的性能和安全性都介于前两者中间。
下面是不同刷盘策略的流程图:
- 1.innodb_flush_log_at_trx_commit=0
参数为 0 时,如果 MySQL 挂了或宕机,可能会丢失 1 秒内的数据。
为什么是 1 秒?因为 InnoDB 有个后台线程每隔 1 秒将 redo log buffer 中的内容写到 page cache,然后调用 fsync 刷到磁盘。
- 2.innodb_flush_log_at_trx_commit=1
为 1 时, 只要事务提交成功,redo log 记录就一定在硬盘里(这点实际上是有二阶段提交来保证的),不会有任何数据丢失。
如果事务执行期间 MySQL 挂了或宕机,会导致这部分日志丢失。但由于事务并没有提交,所以日志丢了也不会有损失,直接回滚即可。
- 3.innodb_flush_log_at_trx_commit=2
为 2 时, 只要事务提交成功,redo log buffer 中的内容只写入文件系统缓存(page cache)。
如果仅仅只是 MySQL 挂了不会有任何数据丢失(会有操作系统的后台线程去把 page cache 中的 redo log 日志同步到 redo log file),但是宕机可能会有 1 秒数据的丢失
小贴士:
InnoDB 和操作系统(os)都有后台线程负责刷盘,不要把两者搞混了。
以上三种策略中:
- 如果是对数据安全性要求比较高的场景,则需要将参数设置为1,因为1的安全性最高。
- 如果是在一些可以容忍数据库崩溃时丢失 1s 数据的场景,我们可以将该值设置为 0,这样可以明显地减少日志同步到磁盘的 I/O 操作。
- 如果是需要安全性和性能折中的方案,可以将参数设置为2,虽然参数 2 没有参数 0 的性能高,但是数据安全性方面比参数 0 强,因为参数 2 只要操作系统不宕机,即使数据库崩溃了,也不会丢失数据,同时性能方便比参数 1 高。
Bin Log(二进制日志)
为什么需要Binlog
binlog 是逻辑日志,记录内容是语句的原始逻辑,类似于 “给 ID=2 这一行的 c 字段加 1”。binlog 会记录所有涉及更新数据的逻辑操作,并且是顺序写,属于 MySQL Server 层。
Binlog是MySQL特有的一种日志机制,记录了所有导致数据库状态变化的操作(如INSERT、UPDATE、DELETE)
不管用什么存储引擎,只要发生了表数据更新,都会产生 binlog 日志
那binlo 到底是用来干嘛的?
- 备份恢复,实现崩溃一致性(Crash Consistency)。原理是每次事务进行提交时,都会将增、删、改操作以追加的方式记录到Binlog文件中。
- 主从复制,实现 主从复制的一致性。原理是主从复制常用于MySQL主从集群搭建,MySQL从节点通过监听主节点Binlog日志进行同步即可。
binlog的记录格式
binlog 日志有三种格式,可以通过 binlog_format 参数指定。
- statement(默认格式):记录 SQL 原文,但有动态函数的问题;
- row:记录操作和具体数据,但占用空间,且消耗 IO 资源;
- mixed:折中方案,为前两者的混合;
指定 statement,记录的内容是 SQL 语句原文,比如执行一条 update T set update_time=now() where id=1,记录的内容如下:
同步数据时,会执行记录的 SQL 语句,但是有个问题,update_time=now() 这里会获取当前系统时间,直接执行会导致与原库的数据不一致。
为了解决这种问题,我们需要指定为 row,记录的内容不再是简单的 SQL 语句了,还包含操作的具体数据,记录内容如下
row 格式记录的内容看不到详细信息,要通过 mysqlbinlog 工具解析出来。
update_time=now() 变成了具体的时间 update_time=“1627112756247”,条件后面的@1、@2、@3 都是该行数据第 1 个 - 3 个字段的原始值(假设这张表只有 3 个字段)。
这样就能保证同步数据的一致性,通常情况下都是指定为 row,这样可以为数据库的恢复与同步带来更好的可靠性。
但是这种格式,需要更大的容量来记录,比较占用空间,恢复与同步时会更消耗 IO 资源,影响执行速度。
所以就有了一种折中的方案,指定为 mixed,记录的内容是前两者的混合
MySQL 会判断这条 SQL 语句是否可能引起数据不一致,如果是,就用 row 格式,否则就用 statement 格式。
binlog 刷盘策略(非刷脏页)
binlog 的写入时机非常简单,事务执行过程中,先把日志写到 binlog cache,事务提交的时候,再把 binlog cache 写到 binlog 文件中。
因为一个事务的 binlog 不能被拆开,无论这个事务多大,也要确保一次性写入,所以系统会给每个线程分配一个块内存作为 binlog cache。
我们可以通过 binlog_cache_size 参数控制单个线程 binlog cache 大小,如果存储内容超过了这个参数,就要暂存到磁盘。
binlog 日志刷盘流程如下:
- 图中的 write,指的就是指把日志写入到 Page Cache ,但是并没有把数据持久化到磁盘,因为数据还缓存在文件系统的 Page Cache 里,write 的写入速度还是比较快的,因为不涉及磁盘 I/O。
- 图中的 fsync,才是将数据持久化到磁盘的操作,这里就会涉及磁盘 I/O,所以频繁的 fsync 会导致磁盘的 I/O 升高。
MySQL提供了一个sync_binlog参数,用于控制Binlog日志写入磁盘的频率:
- sync_binlog = 0:每次事务提交时,只进行写操作(write),不执行fsync,具体何时将数据持久化到磁盘由操作系统决定。
- sync_binlog = 1:每次事务提交时,先进行写操作(write),然后立即执行fsync,确保日志被持久化到磁盘。
- sync_binlog = N(N > 1):每次事务提交时只执行写操作(write),但累积N个事务后才会执行fsync,将日志持久化到磁盘。 在MySQL中,默认的sync_binlog设置为0,意味着没有强制性的磁盘刷新操作,这样可以获得最佳的性能,但也伴随较高的风险。如果操作系统发生异常重启,尚未持久化到磁盘的Binlog数据将会丢失。
当sync_binlog设置为1时,系统提供最强的安全性,确保即使发生异常重启,也最多丢失一个事务的Binlog,而已经持久化的数据不会受到影响。然而,这种设置对性能的影响非常大。
如果能够接受少量事务Binlog丢失的风险,并希望提高写入性能,一般可以将sync_binlog设置为100到1000之间的某个值,从而在性能和安全性之间找到平衡。
总结
通过本文的深入解析,我们全面了解了MySQL中Undo Log、Redo Log和Binlog三大日志机制,以及它们在保障事务ACID特性中的关键作用。
- Undo Log:通过记录数据修改前的状态,确保事务在发生错误或回滚时能够恢复到初始状态,保障了原子性和隔离性。
- Redo Log:通过记录事务的修改操作,确保即使在系统崩溃后,已提交的事务也能被恢复,保障了持久性。
- Binlog:作为MySQL特有的二进制日志,支持数据复制和恢复,进一步增强了数据的持久性和系统的可扩展性。 理解和掌握这些日志机制,不仅有助于优化数据库性能,提升事务处理效率,还能在面对系统故障时快速进行数据恢复,确保业务的连续性和数据的安全性。同时,这些知识也为构建高可用、高性能的数据库架构提供了坚实的理论基础。
在实际应用中,合理配置和管理这些日志机制,结合具体业务需求进行优化,是每位数据库管理员和开发者需要掌握的重要技能。希望通过本文,您能够对MySQL的核心机制有更深刻的理解,并在实际工作中灵活运用,为构建稳定可靠的数据库系统贡献力量。