目录
1、undo 日志文件
undo log【逻辑日志】:回滚日志,InnoDB 引擎层生成的日志,用于记录数据被修改前的信息,记录的是逻辑操作【增、删、改操作】—— 记录的是如何撤销一个操作,而不是直接记录操作本身。
undo log 的作用:
- 事务回滚【原子性】:当事务回滚时或者数据库崩溃时,可以利用 undo log 来进行数据回滚
- MVCC【隔离性】:通过 ReadView + undo log 实现 【MVCC (多版本并发控制)】
undo log 会产生 redo log,也就是 undo log的产生会伴随着 redo log 的产生,这是因为 undo log 也需要持久性的保护
1.1、回滚段 & undo 页
InnoDB 对 undo log 采用回滚段(rollback segment)进行管理,即每个回滚段记录了 1024 个 undo log segment,而在每个 undo log segment 段中进行 undo 页的申请。
- 在 InnoDB1.1 版本之前(不包括 1.1 版本),只有一个 rollback segment,因此,并发事务数限制为1024
- 从 1.1 版本开始,InnoDB 支持最大 128 个 rollback segment,故并发事务数限制提高到了 128 * 1024
虽然 InnoDB1.1 版本支持了 128 个 rollback segment,但是这些 rollback segment 都存储于共享表空间 ibdata中。从 lnnoDB 1.2 版本开始,可通过参数对 rollback segment 做进一步的设置。这些参数包括:
- innodb_undo_directory:设置 rollback segment 文件所在的路径。这意味着 rollback segment 可以存放在共享表空间以外的位置,即可以设置为独立表空间。该参数的默认值为“./”,表示当前 InnoDB 存储引擎的目录
- innodb_undo_logs:设置 rollback segment 的个数,默认值为128。在 InnoDB1.2 版本中,该参数用来替换之前版本的参数 innodb_rollback_segments
- innodb_undo_tablespaces:设置构成 rollback segment 文件的数量,这样 rollback segment 可以较为平均地分布在多个文件中。设置该参数后,会在路径 innodb_undo_directory 看到 undo 为前缀的文件,该文件就代表 rollback segment 文件
1.2、回滚段 & 事务
- 回滚段存在于 undo 表空间中,在数据库中可以存在多个 undo 表空间,但同一时刻只能使用一个 undo 表空间
- 每个事务只会使用一个回滚段(rollback segment),一个回滚段在同一时刻可能会服务于多个事务
- 当一个事务开始的时候,会制定一个回滚段,在事务进行的过程中,当数据被修改时,原始的数据会被复制到回滚段
- 在回滚段中,事务会不断填充盘区,直到事务结束或所有的空间被用完。如果当前的盘区不够用,事务会在段中请求扩展下一个盘区,如果所有已分配的盘区都被用完,事务会覆盖最初的盘区或者在回滚段允许的情况下扩展新的盘区来使用
- 当事务提交时,InnoDB 存储引擎会做以下两件事情:
- 将 undo log 放入列表中,以供之后的 purge(清洗、清除)操作
- 判断 undo log 所在的页是否可以重用(低于 3/4 可以重用),若可以分配给下个事务使用
1.3、undo 页的重用
我们开启一个事务需要写 undo log 的时候,就得先去 Undo Log segment 中去找到一个空闲的位置,当有空位的时候,就去申请 undo 页,在这个申请到的 undo 页中进行 undo log 的写入。
MySQL 默认一页的大小是 16KB,如果为每一个事务分配一个页,是非常浪费的(除非你的事务非常长):假设你的应用的 TPS( 每秒处理的事务数目)为 1000,那么 1s 就需要1000个页,大概需要 16M 的存储,1分钟大概需要 1G 的存储。如果照这样下去,除非 MySQL 清理的非常勤快,否则随着时间的推移,磁盘空间会增长的非常快,而且很多空间都是浪费的。
于是,undo 页就被设计的可以重用了,当事务提交时,并不会立刻删除 undo 页。因为重用,所以这个 undo 页可能混杂着其它事务的 undo Log。undo log 在 commit 后,会被放到一个链表中,然后判断 undo 页的使用空间是否小于 3/4,如果小于 3/4 的话,则表示当前的 undo 页可以被重用,那么它就不会被回收,其它事务的 undo log 可以记录在当前 undo 页的后面。由于 undo log 是离散的,所以清理对应的磁盘空间时,效率不高。
1.4、回滚段中的数据分类
- 未提交的回滚数据(uncommitted undo information):该数据所关联的事务并未提交,用于实现读一致性,所以该数据不能被其他事务的数据覆盖。
- 已经提交但未过期的回滚数据(committed undo information):该数据关联的事务已经提交,但是仍受到undo retention 参数的保持时间的影响。
- 事务已经提交并过期的数据(expired undo information):事务已经提交,而且数据保存时间已经超过undo retention 参数指定的时间,属于已经过期的数据。当回滚段满了之后,会优先覆盖"事务已经提交并过期的数据"
1.5、undo log 日志的存储机制
不同的 SQL 修改操作,记录的 undo log 是不一样的:
- insert 插入操作,会在 undo log 中记录本次插入的主键 id,等事务回滚时,会 delete 此主键对应的记录
- update 更新操作,会记录一条相反的 update 的 undo log【旧值】,回滚时执行一次相反 update,更新回原来的数据
- delete 删除操作,会记录删除前的数据,回滚时,insert 原来的数据
单条日志的格式如下:
-- 原始SQL
BEGIN;
update user set name = '小李' where id = 1;
COMMIT;
-- 日志内容示意
-------------------------------------
| Transaction ID | 123456
| Roll Pointer | 789012
| Transaction Type | UPDATE
| Table ID | 456
| Page Number | 789
| Slot Number | 1
| Undo Record Type | Regular Undo
| Undo Record Size | 100 bytes
| SQL Operation | UPDATE user SET name = '小李' WHERE id = 1;
| Undo Segment ID | 789012
| Segment State | ACTIVE
| Transaction State | COMMITTED
| Prev Transaction ID| 789455
| 其他记录信息
-------------------------------------
undo log 记录了事务内的所有数据变更,用于回滚事务和实现一致性读。当数据行更新后,会生成一条 undo log,记录之前的值。然后数据行会有一个隐藏的字段 roll pointer 指针,指向上一条 undo log 的位置。如果同一条记录有多次更新,就会有多条 undo log,形成链表。如下图:
undo log 日志里面不仅存放着数据更新前的记录,还记录着 RowID、事务 ID、回滚指针。其中事务 ID 每次递增,回滚指针第一次如果是 INSERT 语句的话,回滚指针为 NULL,第二次 UPDATE 之后的 undo log 的回滚指针就会指向刚刚那一条 undo log 日志,以此类推,就会形成一条 undo log 的回滚链,方便找到该条记录的历史版本
1.6、 undo log 类型
在 InnoDB 存储引擎中,undo log 分为:
- insert undo log:在 insert 操作中产生的 undo Log。因为 insert 操作的记录,只对事务本身可见,对其它事务不可见(这是事务隔离性的要求),故该 undo log 可以在事务提交后直接删除。不需要进行 purge 操作。
- update undo log:对 delete 和 update 操作产生的 undo log。该 undo log 可能需要提供 MVCC 机制,因此不能在事务提交时就进行删除。提交时放入 undo log 链表,等待 purge 线程进行最后的删除
1.7、事务回滚
一条记录的每一次更新操作产生的 undo log 格式都有一个 roll_pointer 指针和一个 trx_id 事务id,如下图:
- 通过 trx_id 可以知道该记录是被哪个事务修改的
- 通过 roll_pointer 指针可以将这些 undo log 串成一个链表,这个链表就被称为版本链
而事务发生回滚,会读取 undo log 里的数据,本质上并不会以执行反 SQL 的模式还原数据,而是直接将 roll_ptr回滚指针指向的 undo 记录
来看 insert 和 update 产生的日志:
插入的数据都会生成一条 insert undo log,并且数据的回滚指针会指向它。undo log会记录undo log的序号、插入主键的列和值
1.8、MVCC
MVCC 是通过 ReadView + undo log 实现的。undo log 为每条记录保存多份历史数据,MySQL 在执行快照读(普通 select 语句)的时候,会根据事务的 Read View 里的信息,顺着 undo log 的版本链找到满足其可见性的记录
1.9、undo log 的工作原理
在更新数据之前,MySQL 会提前生成 undo log 日志,当事务提交的时候,并不会立即删除 undo log,因为后面可能需要进行回滚操作,要执行回滚(ROLLBACK)操作时,从缓存中读取数据。undo log 日志的删除是通过通过后台 purge 线程进行回收处理的
2、redo log
redo log【物理日志】:重做日志,InnoDB 引擎层生成的日志,用于记录数据被修改后的信息,记录的是某个数据页做了什么修改 —— 对 XXX 表空间中的 YYY 数据页 ZZZ 偏移量的地方做了AAA 更新。
redo log 作用:
● 持久化防止断电数据丢失【持久性】
redo log 日志分为两部分:
● 内存:重做日志缓冲(redo log Buffer),易丢失
● 磁盘:重做日志文件(redo log file)
redo log 日志格式:
-- 原始SQL
BEGIN;
update user set name = '小李' where id = 1;
insert into user(id, name) values(2, '小红');
delete from user where id = 3;
COMMIT;
-- 日志内容示意
TransactionID: 127
PageID: 462
Operation: MULTIPLE_UPDATES
Updates:
- UpdateType: UPDATE
Before Image: (1, '小明')
After Image: (1, '小李')
- UpdateType: INSERT
After Image: (2, '小红')
- UpdateType: DELETE
Before Image: (3, '小张')
每当执行一个事务就会产生这样的一条或者多条物理日志
- 物理日志:主要存储数据库中特定记录的变更,更侧重于记录数据库物理存储的变更
- 逻辑日志:侧重于记录事务的逻辑操作和 SQL 语句
2.1、为何需要 redo log
MYSQL 的读写操作都是在 Buffer Pool 中进行的,可以极大提高读写效率。因为 Buffer Pool 是基于内存的。但是,内存总是不可靠的,如果断电重启,还没来得及落盘的脏页数据就会丢失。
所以,就引入了 redo log 来解决此问题:它是搭配 Buffer Pool 缓冲池、Change Buffer 使用的,作用就是持久化记录的写操作,防止在写操作更新到磁盘前发生断电丢失这些写操作,直到该操作对应的脏页真正落盘
- 缓冲池 Buffer Pool:缓存磁盘上的数据页,减少磁盘的IO;
- Change Buffer:将写操作先存在内存中,等到下次需要读取这些操作涉及到的数据页时,就把数据页加载到缓冲池中,然后在缓冲池中更新,然后由后台线程再刷写到磁盘
2.2、WAL 技术
当有一条记录需要更新的时候,InnoDB 引擎就会先更新内存(同时标记为脏页),然后,将此次对这个页的修改以 redo log 的形式记录下来,写入到 redo log buffer,这个时候更新就算完成了。InnoDB 引擎会在适当的时候,由后台线程将缓存在 Buffer Pool 的脏页刷新到磁盘里 —— WAL (Write-Ahead Logging)技术,即 MySQL 的写操作并不是立刻写到磁盘上,而是先写日志,然后在合适的时间再写到磁盘上。
先将 redo log 日志持久化到磁盘中,即使系统奔溃,脏页刷盘失败,也可以通过 redo log 日志的内容,将数据恢复到当前最新的状态
2.3、checkpoint
redo log 默认情况下存储在 data 目录下 ib_logfile0 和 ib-logfile1 两个文件中
- innodb_log_file_size:设置大小
- innodb_log_files_in_group:设置文件个数
为什么默认情况下 redo log 由 ib_logfile0 和 ib-logfile1 两个日志文件?
MySQL 通过循环顺序写这两个文件的形式记录 redo log 日志,用两个日志文件组成一个“环形”,如下图:
redo log 是采用循环写的方式,图中各部分代表的意思如下:
- write pos:表示当前 redo log 文件写到了哪个位置
- check point:表示目前哪些 redo log 记录已经失效且可以被擦除(覆盖)
- write pos 和 checkpoint 的移动都是顺时针方向
- write pos ~ checkpoint 之间的部分(图中的红色部分),用来记录新的更新操作【可用空间】
- check point ~ write pos 之间的部分(图中蓝色部分),待落盘的脏数据页记录【脏页】
如:当一个事务写了 redo log 日志、并将数据写入缓冲区后,但数据还未写入磁盘文件中,此时这个事务对应的 redo log 记录就为上图中的蓝色,而当一个事务所写的数据也落盘后,对应的 redo log 记录就会变为红色
write pos 指针追上 check point 指针怎么办?
如果 write pos 指针追上 check point 指针,那么环中的红色区域就没了,也就意味着无法再写入 redo log 日志了,因为文件满了,再执行写入操作就会阻塞 MYSQL【针对并发量大的系统,适当设置 redo log 的文件大小非常重要】
此时,会触发 checkpoint 刷盘机制:将 Buffer Pool 中的脏页刷新到磁盘中,然后标记 redo log 哪些记录可以被擦除,接着对旧的 redo log 记录进行擦除,等擦除完旧记录腾出了空间,checkpoint 就会往后移动,红色区域也会不断增长,因此阻塞的写事务才能继续执行
2.4、刷盘时机
缓存在 redo log buffer 里的 redo log 是在内存中的,最终是要刷到磁盘中,下面 5 种场景会刷新到磁盘中:
- log buffer 空间不足时:log buffer 的大小是有限的,如果当前写入 log buffer 的 redo 日志量已经占满了log buffer 总容量的 50% 左右,就需要将这些日志刷新到磁盘中
- 事务提交时:为了保持持久性,必须要把页面修改时所对应的 redo 日志刷新到磁盘,否则系统崩溃后,无法将该事务对页面所做的修改恢复过来
- 后台线程:大约以每秒一次的频率将 redo log buffer 中的 redo 日志刷新到磁盘中
- 触发 checkpoint 时
- 正常关闭服务器
2.5、刷盘策略
刷盘策略:何时以何种方式刷新到真正的 redo log file 中
InnoDB 通过 innodb_flush_log_at_trx_commit 参数可以控制策略,该参数控制 commit 提交事务时,如何将 redo log buffer 中的日志刷新到 redo log file 中。其值可能为:
- 0【延迟写】:每次事务提交时不主动进行刷盘操作,redo log 依然留在 redo log buffer 中,然后每秒写入page cache中,然后持久化到磁盘中
- 1【 实时写,实时刷;默认值】:每次事务提交时都将直接将缓存在 redo log buffer 中的 redo log 直接持久化到磁盘中
- 2【实时写,延时刷】:表示每次事务提交时都只把 redo log buffer 内容写入 page cache,不进行同步,由 os 自己决定什么时候同步到磁盘文件
设置为 0 时,commit 事务会把缓存在 redo log buffer 中的 redo log ,然后后台线程每秒执行一次将 redo log buffer 操作系统的 page cache,并调用 fsync() 持久化到磁盘,即使 MySQL 崩溃只会导致上一秒钟所有事务数据的丢失:
设置为1时,commit 事务会把缓存在 redo log buffer 中的 redo log直接持久化到磁盘,这种场景下是不会丢失数据:
设置为 2 时,commit 事务会把缓存在 redo log buffer 中的 redo log 写入到 page cache,这种场景下是不会丢失数据,然后后台线程每秒执行一次将 page cache 的内容持久化到磁盘:
2.6、崩溃恢复
MySQL 崩溃也是一次关闭过程,只是比正常关闭突然和迅速了一些。正常关闭时,MySQL 会做一系列收尾工作,例如:清理 undo 日志、合并 change buffer 缓冲区等操作
在 MySQL 服务器正常的时候 undo log 看起来是个累赘,但是万一出问题就是个宝,可以在重启的时候恢复到崩溃钱的状态,主要通过以下步骤:
- 找到 last_checkpoint_lsn:读取 redo 日志之前,必须先确定一个起点,这个起点就是 InnoDB 最后一次 checkpoint 操作的 lsn,也就是 last_checkpoint_lsn
- 修复损坏的数据页:两次写文件中的所有数据页都加载到内存缓冲区之后,需要用这些页来把系统表空间、独立表空间、undo 表空间中损坏的数据页恢复到正常状态
- 读取 redo 日志:确定了读取 redo 日志的起点 last_checkpoint_lsn,接下来就该读取 redo 日志了
- 应用 redo 日志
2.7、两阶段
在事务内执行变更 sql 会生成 redo log,因为事务还没提交,以 redo log 还不能直接落盘。这时会把修改先记录到 redo log buffer,在提交事务时,先把 redo log 的状态设置为 prepare,并存入磁盘,然后提交 binlog,存入磁盘,最后将 redo log 设置为 commit 状态,存日志磁盘。这也是分布式事务常用的方式,是为了保证 redo log 和 binlog 的事务性。
3、bin log
bin log:事务提交后在 Server 层产生的日志,记录所有对数据库表结构变更和表数据修改的操作
bin log 的作用:
- 数据恢复 :bin log 详细记录了所有修改数据的 SQL,当某一时刻的数据误操作而导致出问题,或者数据库宕机数据丢失,那么可以根据 bin log 来回放历史数据
- 主从复制:想要做多机备份的业务,可以去监听当前写库的 bin log 日志,同步写库的所有更改
3.1、日志格式类型
bin log 日志有三种格式,分别为 statement、row 和 mixed
在 MySQL 5.7.7 之前,默认的格式是 statement,MySQL 5.7.7之后,默认值是 row。日志格式通过 binlog-format 指定
- statement:基于语句的模式。每一条修改数据的 SQL 都会被记录到 bin log 中。由于只记录对数据库产生变更操作的 SQL,日志不会太大,性能会比较不错。但是如果在 SQL 中使用了 sysdate()、now() 这类动态函数, 在恢复数据、主从同步数据时,有时会出现数据不一致的情况
-- 原始SQL
BEGIN;
update post set updated_date = now() where id <= 10;
COMMIT;
-- bin log文件
# at 123456
#210714 12:34:56 server id 1 end_log_pos 123 Query thread_id=123 exec_time=0 error_code=0
SET TIMESTAMP=123456;
BEGIN;
update post set updated_date = now() where id <= 10;
COMMIT;
- row:基于行变更的模式。记录具体哪一个分区中的、哪一个页中的、哪一行数据被修改了。所以就没有 statement 模式下动态函数问题,缺点是每行数据变化都会被记录,bin log 日志文件会比较大
-- 原始SQL
BEGIN;
update post set updated_date = now() where id <= 10;
COMMIT;
-- bin log文件
# at 123456
#210714 12:34:56 server id 1 end_log_pos 123 Xid = 456
BEGIN
#210714 12:35:00 server id 1 end_log_pos 234 Table_map: `demo`.`post` mapped to number 123
#210714 12:35:05 server id 1 end_log_pos 345 Update_rows: table id 123 flags: STMT_END_F
### UPDATE `demo`.`post`
### WHERE
### @1=1
### SET
### @2='2023-12-01 12:34:56'
#210714 12:35:10 server id 1 end_log_pos 456 Update_rows: table id 123 flags: STMT_END_F
### UPDATE `demo`.`post`
### WHERE
### @1=2
### SET
### @2='2023-12-01 12:34:57'
# ... (类似的 Update_rows 记录,覆盖 id 为 3 到 10)
#210714 12:35:15 server id 1 end_log_pos 567 Xid = 456
COMMIT
- mixed:statement 和 row 模式的结合版。默认情况下使用 statment 格式记录日志,日志内容会少一些,在特殊情况下会使用 row 格式。例如:动态函数
3.2、写入时机 & 方式
写入时机:MYSQL 在 InnoDB 引擎提交事务时,先保存 bin log,再提交事务。为了保证数据一致性,完整的流程:先把 redo log 设置成 prepare 状态,再提交 bin log,然后把 redo log 设置为 commit 状态
写入方式:bin log 是通过追加的方式进行写入的,可以通过 max_binlog_size 参数设置每个 binlog 文件的大小,当文件大小达到给定值之后,会生成新的文件来保存日志
每个日志文件的命名为 mysql-bin.000001、mysql-bin.000002、mysql-bin.00000x…,可以通过 show binary logs; 命令查看已有的 bin log 日志文件
3.3、刷盘时机和策略
事务执行时,会给每个线程在内存中分配一块地方叫 bin log cache,bin log 文件就记录在这里,事务提交的时候,再把 bin log cache 写到 bin log 文件中。
注意:一个事务的 bin log 不能被拆开提交,无论这个事务多大,也要确保一次性写入,这样才能保证原子性
对于 InnoDB 存储引擎而言,只有在事务提交时才会记录 bin log ,此时记录还在内存中。
在内存中的 bin log 何时保存到磁盘中,也就是【刷盘时机】。如图:
- fsync:将数据持久化到磁盘的操作
- write:把日志写到 page cache 的 bin log 文件中,还没有持久化到磁盘
write 和 fsync 的时机和频率,是由参数 sync_binlog 控制,该参数控制着二进制日志写入磁盘的过程,该参数的有效值为 0 、1、N
- sync_binlog = 0 :提交事务都只 write,不 fsync,此时数据在 page cache,后续交由操作系统决定何时将数据持久化到磁盘
- sync_binlog = 1 :提交事务都会 write,然后马上执行 fsync
- sync_binlog =N(N>1) :提交事务都 write,但累积 N 个事务后才 fsync
3.4、主从复制
主从复制主要是依赖 bin log,slave 从库从 master 主库读取 bin log 进行数据同步。
MySQL 主从复制是异步且串行化的 ,也就是说主库上执行事务操作的线程不会等待复制 bin log 的线程同步完成,流程如下图:
整个流程其实就是对bin log的写入、同步、重做过程:
- 写入:主库写 bin log 日志,提交事务,并更新本地存储数据
- 同步:把 bin log 复制到所有从库上,每个从库把 bin log 写到中继(relay log)日志中
- 重做:重做中继日志 bin log events,并更新存储引擎中的数据
主从同步设计当然有好处了,比如读写分离(数据写入在 master 上,数据读取在 slave)、数据备份(保留了多份一样的数据)。但是如果 slave 增加,从库连接上来的 I/O 线程较多,那么就会对主库资源消耗增大,造成主从延迟。
当然造成主从延迟的原因还有很多咯,网络问题,带宽问题,慢 SQL 等
3.5、主从复制方式
- 同步复制:MySQL 主库提交事务的线程要等待所有从库的复制成功响应,才返回客户端结果。这种方式在实际项目中,基本上没法用,原因有两个:一是性能很差,因为要复制到所有节点才返回响应;二是可用性也很差,主库和所有从库任何一个数据库出问题,都会影响业务。
- 异步复制(默认模型):MySQL 主库提交事务的线程并不会等待 bin log 同步到各从库,就返回客户端结果。这种模式一旦主库宕机,数据就会发生丢失。
- 半同步复制:MySQL 5.7 版本之后增加的一种复制方式,介于两者之间,事务线程不用等待所有的从库复制成功响应,只要一部分复制成功响应回来就行,比如一主二从的集群,只要数据成功复制到任意一个从库上,主库的事务线程就可以返回给客户端。这种半同步复制的方式,兼顾了异步复制和同步复制的优点,即使出现主库宕机,至少还有一个从库有最新的数据,不存在数据丢失的风险