The underlying principles of MYSQL transactions




1. The underlying principles of transactions

In terms of the transaction implementation mechanism, MySQL uses the WAL: Write-ahead logging, write-ahead log, mechanism to implement it.
In a system using WAL, all changes are written to the log first and then applied to the system. Usually contains two parts of information: redo and undo.
Why do we need to use WAL and then include redo and undo information? For example, if a system applies changes directly to the system state, then after the machine is powered off and restarted, the system needs to know whether the operation was successful, only partially successful, or failed. If WAL is used, the system can decide whether to continue completing the operation or cancel the operation after restarting by comparing the log and system status.
The redo log is called the redo log. Whenever there is an operation, the operation is written to the redo log before the data changes, so that when a power outage or the like occurs, the system can continue to operate after restarting.
undo log is called undo log. When some changes cannot be completed halfway through execution, the undo log can be used to restore to the state between changes.
Redo log is used in MySQL to repair data when the system crashes and restarts, while undo log is used to ensure the atomicity of transactions.


2. Transaction ID

A transaction can be a read-only transaction, or a read-write transaction: a read-only transaction can be started with the START TRANSACTION READ ONLY statement.
In a read-only transaction, ordinary tables cannot be added, deleted, or modified, but user temporary tables can be added, deleted, or modified.
A read-write transaction can be started through the START TRANSACTION READ WRITE statement, or a transaction started using the BEGIN and START TRANSACTION statements is considered a read-write transaction by default.
In read and write transactions, you can perform add, delete, modify, and query operations on the table.
If a table is added, deleted, or modified during the execution of a transaction, the InnoDB storage engine will assign it a unique transaction ID. The allocation method for MySQL 5.7 is as follows:
  • For a read-only transaction, a transaction ID will be assigned to the transaction only when it first performs an add, delete, or modify operation on a temporary table created by a user. Otherwise, no transaction ID will be assigned.
  • For read-write transactions, a transaction ID will be assigned to the transaction only when it performs add, delete, or modify operations on a table for the first time. Otherwise, no transaction ID will be assigned.
  • Sometimes, although a read-write transaction is opened, the transaction contains only query statements and no add, delete, or modify statements are executed, which means that the transaction will not be assigned a transaction ID.
This transaction ID is essentially a number, and its allocation strategy is roughly the same as that of the hidden column row_id. The specific strategy is as follows:
  • The server will maintain a global variable in memory. Whenever a transaction id needs to be assigned to a transaction, the value of the variable will be assigned to the transaction as the transaction id, and the variable will be incremented by 1.
  • Whenever the value of this variable is a multiple of 256, the value of the variable will be refreshed to an attribute called Max Trx ID in page number 5 of the system table space. This attribute occupies 8 bytes. of storage space.
  • When the system restarts next time, the Max Trx ID attribute mentioned above will be loaded into the memory, the value will be added to 256 and then assigned to the global variable, because the value of the global variable may be greater than Max Trx when it was shut down last time. ID attribute value.
  • This ensures that the transaction ID value assigned throughout the system is an increasing number. The transaction whose ID is assigned first gets the smaller transaction ID, and the transaction whose ID is assigned later gets the larger transaction ID.


3. MVCC

The full name is Multi-Version Concurrency Control, which is multi-version concurrency control, mainly to improve the concurrency performance of the database.

When a read or write request occurs for the same row of data, it will be locked and blocked. But MVCC uses a better way to handle read and write requests, so that there is no need to lock when a conflict between read and write requests occurs.

This read refers to the snapshot read, not the current read. The current read is a locking operation and is a pessimistic lock.

MVCC principle

The problems encountered in concurrent execution of transactions are as follows:

  • Dirty read : If a transaction reads data modified by another uncommitted transaction, it means that a dirty read has occurred;

  • Non-repeatable read : If a transaction can only read the data modified by another transaction that has been submitted, and every time other transactions modify the data and submit it, the transaction can query and get the latest value, which means that it occurs Non-repeatable reading;

  • Phantom reading : If a transaction first queries some records based on certain conditions, and then another transaction inserts records that meet these conditions into the table, when the original transaction queries again according to the conditions, the records inserted by the other transaction can be If it is also read out, it means that a phantom read has occurred. The emphasis of phantom read is that when a transaction reads records multiple times according to the same conditions, the subsequent read reads a record that has not been read before. The phantom read only emphasizes A record that was not obtained before was read.

MySQL can avoid phantom read problems to a great extent under the REPEATABLE READ isolation level.

version chain

For tables using the InnoDB storage engine, its clustered index records contain two necessary hidden columns:

  • trx_id : Every time a transaction changes a clustered index record, the transaction ID of the transaction will be assigned to the trx_id hidden column;

  • roll_pointer : Every time a clustered index record is modified, the old version will be written to the undo log, and then this hidden column is equivalent to a pointer, through which the information before the record can be found is modified;

Demo

  
  
  
  
  
-- 创建表CREATE TABLE mvcc_test (id INT,name VARCHAR(100),domain varchar(100),PRIMARY KEY (id)) Engine=InnoDB CHARSET=utf8;
-- 添加数据INSERT INTO mvcc_test VALUES(1, 'habit', '演示mvcc');

Assuming that the transaction ID of inserting the record is 50, then the record is displayed as shown in the figure:

Assume that two subsequent transactions with transaction IDs of 70 and 90 perform UPDATE operations on this record.

Every time a record is modified, an undo log will be recorded. Each undo log also has a roll_pointer attribute. These undo logs can be connected to form a linked list.

After each update to the record, the old value will be placed in an undo log. Even if it is an old version of the record, as the number of updates increases, all versions will be connected into a linked list by the roll_pointer attribute. This linked list It is called a version chain, and the head node of the version chain is the latest value of the current record. In addition, each version also contains the transaction ID corresponding to when the version was generated. Therefore, the version chain of this record can be used to control the behavior of concurrent transactions accessing the same record. This mechanism is called: multi-version concurrency control, or MVCC.

ReadView

For transactions using the READ UNCOMMITTED isolation level, since records modified by uncommitted transactions can be read, it is enough to directly read the latest version of the record.

For transactions using the SERIALIZABLE isolation level, InnoDB uses locking to access records.

对于使用 READ COMMITTED 和 REPEATABLE READ 隔离级别的事务来说,都必须保证读到已经提交了的事务修改过的记录,也就是说假如另一个事务已经修改了记录但是尚未提交,是不能直接读取最新版本的记录的,核心问题就是:READ COMMITTED 和 REPEATABLE READ 隔离级别在不可重复读和幻读上的区别是从哪里来的,其实结合前面的知识,这两种隔离级别关键是需要判断一下版本链中的哪个版本是当前事务可见的。

为此,InnoDB 提出了一个 ReadView 的概念,这个 ReadView 中主要包含 4 个比较重要的内容:

  • m_ids:表示在生成 ReadView 时当前系统中活跃的读写事务的事务id 列表;

  • min_trx_id:表示在生成 ReadView 时当前系统中活跃的读写事务中最小的事务 id,也就是 m_ids 中的最小值;

  • max_trx_id:表示在生成 ReadView 时系统中应该分配给下一个事务的 id 值,注:max_trx_id 并不是 m_ids 中的最大值,事务 id 是递增分配的。比方说现在有 id 为 1,2,3 这三个事务,之后 id 为 3 的事务提交了。那么一个新的读事务在生成 ReadView 时,m_ids 就包括 1 和 2,min_trx_id 的值就是 1,max_trx_id 的值就是 4;

  • creator_trx_id:表示生成该 ReadView 的事务的事务 id;

有了这个 ReadView,这样在访问某条记录时,只需要按照下边的步骤判断记录的某个版本是否可见:

  1. 如果被访问版本的 trx_id 属性值与 ReadView 中的 creator_trx_id 值相同,意味着当前事务在访问它自己修改过的记录,所以该版本可以被当前事务访问;

  2. 如果被访问版本的 trx_id 属性值小于 ReadView 中的 min_trx_id 值,表明生成该版本的事务在当前事务生成 ReadView 前已经提交,所以该版本可以被当前事务访问;

  3. 如果被访问版本的 trx_id 属性值大于或等于 ReadView 中的 max_trx_id 值,表明生成该版本的事务在当前事务生成 ReadView 后才开启,所以该版本不可以被当前事务访问;

  4. 如果被访问版本的 trx_id 属性值在 ReadView 的 min_trx_id 和 max_trx_id之间 min_trx_id < trx_id < max_trx_id,那就需要判断一下 trx_id 属性值是不是在 m_ids 列表中,如果在,说明创建 ReadView 时生成该版本的事务还是活跃的,该版本不可以被访问;如果不在,说明创建 ReadView 时生成该版本的事务已经被提交,该版本可以被访问;

  5. 如果某个版本的数据对当前事不可见的话,那就顺着版本链找到下一个版本的数据,继续按照上边的步骤判断可见性,依此类推,直到版本链中的最后一个版本。如果最后一个版本也不可见的话,那么就意味着该条记录对该事务完全不可见,查询结果就不包含该记录;

在 MySQL 中,READ COMMITTED 和 REPEATABLE READ 隔离级别的一个非常大的区别就是它们生成 ReadView 的时机不同。

还是以表 mvcc_test 为例,假设现在表 mvcc_test 中只有一条由事务 id 为 50 的事务插入的一条记录,接下来看一下 READ COMMITTED 和 REPEATABLE READ 所谓的生成 ReadView 的时机不同到底不同在哪里。

READ COMMITTED:每次读取数据前都生成一个 ReadView;

比方说现在系统里有两个事务id 分别为 70、90 的事务在执行:

  
  
  
  
  
-- T 70UPDATE mvcc_test  SET name = 'habit_trx_id_70_01' WHERE id = 1;UPDATE mvcc_test  SET name = 'habit_trx_id_70_02' WHERE id = 1;

此时表 mvcc_test 中 id 为 1 的记录得到的版本链表如下所示:

假设现在有一个使用 READ COMMITTED 隔离级别的事务开始执行:

  
  
  
  
  
-- 使用 READ COMMITTED 隔离级别的事务BEGIN;-- SELECE1:Transaction 70、90 未提交SELECT * FROM mvcc_test  WHERE id = 1; -- 得到的列 name 的值为'habit'

这个 SELECE1 的执行过程如下:

在执行 SELECT 语句时会先生成一个 ReadView,ReadView 的 m_ids 列表的内容就是[70, 90],min_trx_id 为 70,max_trx_id 为 91,creator_trx_id 为 0。

然后从版本链中挑选可见的记录,从图中可以看出,最新版本的列 name 的内容是 habit_trx_id_70_02,该版本的 trx_id 值为 70,在 m_ids 列表内,所以不符合可见性要求第 4 条:如果被访问版本的 trx_id 属性值在 ReadView 的 min_trx_id 和 max_trx_id之间 min_trx_id < trx_id < max_trx_id,那就需要判断一下trx_id 属性值是不是在 m_ids 列表中,如果在,说明创建 ReadView 时生成该版本的事务还是活跃的,该版本不可以被访问;如果不在,说明创建 ReadView 时生成该版本的事务已经被提交,该版本可以被访问。根据 roll_pointer 跳到下一个版本。

下一个版本的列 name 的内容是 habit_trx_id_70_01,该版本的 trx_id 值也为 70,也在 m_ids 列表内,所以也不符合要求,继续跳到下一个版本。

下一个版本的列 name 的内容是 habit,该版本的 trx_id 值为 50,小于 ReadView 中的 min_trx_id 值,所以这个版本是符合要求的第 2 条:如果被访问版本的 trx_id 属性值小于 ReadView 中的 min_trx_id 值,表明生成该版本的事务在当前事务生成 ReadView 前已经提交,所以该版本可以被当前事务访问。最后返回的版本就是这条列 name 为 habit 的记录。

之后,把事务 id 为 70 的事务提交一下,然后再到事务 id 为 90 的事务中更新一下表 mvcc_test 中 id 为 1 的记录:

  
  
  
  
  
-- T 90UPDATE mvcc_test  SET name = 'habit_trx_id_90_01' WHERE id = 1;UPDATE mvcc_test  SET name = 'habit_trx_id_90_02' WHERE id = 1;

此时表 mvcc 中 id 为 1 的记录的版本链就长这样:

然后再到刚才使用 READ COMMITTED 隔离级别的事务中继续查找这个 id 为 1 的记录,如下:

  
  
  
  
  
-- 使用 READ COMMITTED 隔离级别的事务BEGIN;-- SELECE1:Transaction 70、90 均未提交SELECT * FROM mvcc_test WHERE id = 1; -- 得到的列 name 的值为'habit'-- SELECE2:Transaction 70 提交,Transaction 90 未提交SELECT * FROM mvcc_test WHERE id = 1; -- 得到的列 name 的值为'habit_trx_id_70_02'

这个SELECE2 的执行过程如下:

在执行 SELECT 语句时又会单独生成一个 ReadView,该 ReadView 的 m_ids 列表的内容就是[90],min_trx_id 为90,max_trx_id 为 91,creator_trx_id 为 0。

然后从版本链中挑选可见的记录,从图中可以看出,最新版本的列 name 的内容是 habit_trx_id_90_02,该版本的 trx_id 值为 90,在 m_ids 列表内,所以不符合可见性要求,根据 roll_pointer 跳到下一个版本。

下一个版本的列 name 的内容是 habit_trx_id_90_01,该版本的 trx_id 值为 90,也在 m_ids 列表内,所以也不符合要求,继续跳到下一个版本。

下一个版本的列 name 的内容是 habit_trx_id_70_02,该版本的 trx_id 值为 70,小于 ReadView 中的 min_trx_id 值 90,所以这个版本是符合要求的,最后返回这个版本中列 name 为 habit_trx_id_70_02 的记录。

以此类推,如果之后事务 id 为 90 的记录也提交了,再次在使用 READ COMMITTED 隔离级别的事务中查询表 mvcc_test 中 id 值为 1 的记录时,得到的结果就是 habit_trx_id_90_02 了。

总结:使用 READ COMMITTED 隔离级别的事务在每次查询开始时都会生成一个独立的 ReadView。

REPEATABLE READ:在第一次读取数据时生成一个 ReadView;

对于使用 REPEATABLE READ 隔离级别的事务来说,只会在第一次执行查询语句时生成一个 ReadView,之后的查询就不会重复生成了。

比方说现在系统里有两个事务id 分别为 70、90 的事务在执行:

  
  
  
  
  
-- T 70UPDATE mvcc_test  SET name = 'habit_trx_id_70_01' WHERE id = 1;UPDATE mvcc_test  SET name = 'habit_trx_id_70_02' WHERE id = 1;

此时表 mvcc_test 中 id 为 1 的记录得到的版本链表如下所示:

假设现在有一个使用 REPEATABLE READ 隔离级别的事务开始执行:

  
  
  
  
  
-- 使用 REPEATABLE READ 隔离级别的事务BEGIN;-- SELECE1:Transaction 70、90 未提交SELECT * FROM mvcc_test WHERE id = 1; -- 得到的列name 的值为'habit'

这个 SELECE1 的执行过程如下:

在执行 SELECT 语句时会先生成一个 ReadView,ReadView 的 m_ids 列表的内容就是[70, 90],min_trx_id 为 70,max_trx_id 为 91,creator_trx_id 为 0。

然后从版本链中挑选可见的记录,从图中可以看出,最新版本的列 name 的内容是 habit_trx_id_70_02,该版本的 trx_id 值为 70,在 m_ids 列表内,所以不符合可见性要求,根据 roll_pointer 跳到下一个版本。

下一个版本的列 name 的内容是 habit_trx_id_70_01,该版本的 trx_id 值也为 70,也在 m_ids 列表内,所以也不符合要求,继续跳到下一个版本。

下一个版本的列 name 的内容是 habit,该版本的 trx_id 值为 50,小于 ReadView 中的 min_trx_id 值,所以这个版本是符合要求的,最后返回的就是这条列name 为 habit 的记录。

之后,把事务 id 为 70 的事务提交一下,然后再到事务 id 为 90 的事务中更新一下表 mvcc_test 中 id 为 1 的记录:

  
  
  
  
  
-- 使用 REPEATABLE READ 隔离级别的事务BEGIN;UPDATE mvcc_test  SET name = 'habit_trx_id_90_01' WHERE id = 1;UPDATE mvcc_test  SET name = 'habit_trx_id_90_02' WHERE id = 1;

此刻,表 mvcc_test 中 id 为 1 的记录的版本链就长这样:

然后再到刚才使用 REPEATABLE READ 隔离级别的事务中继续查找这个 id 为 1 的记录,如下:

  
  
  
  
  
-- 使用 REPEATABLE READ 隔离级别的事务BEGIN;-- SELECE1:Transaction 70、90 均未提交SELECT * FROM mvcc_test WHERE id = 1; -- 得到的列 name 的值为'habit'-- SELECE2:Transaction 70 提交,Transaction 90 未提交SELECT * FROM mvcc_test WHERE id = 1;  -- 得到的列 name 的值为'habit'

这个 SELECE2 的执行过程如下:

因为当前事务的隔离级别为 REPEATABLE READ,而之前在执行 SELECE1 时已经生成过 ReadView 了,所以此时直接复用之前的 ReadView,之前的 ReadView的 m_ids 列表的内容就是[70, 90],min_trx_id 为 70,max_trx_id 为 91, creator_trx_id 为 0。

然后从版本链中挑选可见的记录,从图中可以看出,最新版本的列 name 的内容是 habit_trx_id_90_02,该版本的 trx_id 值为 90,在 m_ids 列表内,所以不符合可见性要求,根据 roll_pointer 跳到下一个版本。

下一个版本的列 name 的内容是 habit_trx_id_90_01,该版本的 trx_id 值为 90,也在 m_ids 列表内,所以也不符合要求,继续跳到下一个版本。

下一个版本的列 name 的内容是 habit_trx_id_70_02,该版本的 trx_id 值为 70,而 m_ids 列表中是包含值为 70 的事务 id 的,所以该版本也不符合要求,同理下一个列 name 的内容是 habit_trx_id_70_01 的版本也不符合要求。继续跳到下一个版本。

下一个版本的列 name 的内容是 habit,该版本的 trx_id 值为 50,小于 ReadView 中的 min_trx_id 值 70,所以这个版本是符合要求的,最后返回给用户的版本就是这条列 name 为 habit 的记录。

也就是说两次 SELECT 查询得到的结果是重复的,记录的列 name 值都是 habit,这就是可重复读的含义。如果之后再把事务 id 为 90 的记录提交了,然后再到刚才使用 REPEATABLE READ 隔离级别的事务中继续查找这个 id 为 1 的记录,得到的结果还是 habit。

MVCC 下的幻读解决和幻读现象

REPEATABLE READ 隔离级别下 MVCC 可以解决不可重复读问题,那么幻读呢?MVCC 是怎么解决的?幻读是一个事务按照某个相同条件多次读取记录时,后读取时读到了之前没有读到的记录,而这个记录来自另一个事务添加的新记录。

可以想想,在 REPEATABLE READ 隔离级别下的事务 T1 先根据某个搜索条件读取到多条记录,然后事务 T2 插入一条符合相应搜索条件的记录并提交,然后事务 T1 再根据相同搜索条件执行查询。结果会是什么?按照 ReadView 中的比较规则中的第 3 条和第 4 条不管事务 T2 比事务 T1 是否先开启,事务 T1 都是看不到 T2 的提交的。

但是,在 REPEATABLE READ 隔离级别下 InnoDB 中的 MVCC 可以很大程度地避免幻读现象,而不是完全禁止幻读。怎么回事呢?来看下面的情况:

首先在事务 T1 中执行:select * from mvcc_test where id = 30; 这个时候是找不到 id = 30 的记录的。

在事务 T2 中,执行插入语句:insert into mvcc_test values(30,'luxi','luxi');

此时回到事务 T1,执行:

  
  
  
  
  
update mvcc_test set domain='luxi_t1' where id=30;select * from mvcc_test where id = 30;

事务T1 很明显出现了幻读现象。

在 REPEATABLE READ 隔离级别下,T1 第一次执行普通的 SELECT 语句时生成了一个 ReadView,之后 T2 向 mvcc_test 表中新插入一条记录并提交。

ReadView 并不能阻止 T1 执行 UPDATE 或者 DELETE 语句来改动这个新插入的记录,由于 T2 已经提交,因此改动该记录并不会造成阻塞,但是这样一来,这条新记录的 trx_id 隐藏列的值就变成了 T1 的事务 id。之后 T1 再使用普通的 SELECT 语句去查询这条记录时就可以看到这条记录了,也就可以把这条记录返回给客户端。因为这个特殊现象的存在,可以认为 MVCC 并不能完全禁止幻读。

mvcc 总结

从上边的描述中可以看出来,所谓的 MVCC(Multi-Version Concurrency Control ,多版本并发控制)指的就是在使用 READ COMMITTD、REPEATABLE READ 这两种隔离级别的事务在执行普通的 SELECT 操作时访问记录的版本链的过程,这样子可以使不同事务的读写、写读操作并发执行,从而提升系统性能。

READ COMMITTD、REPEATABLE READ 这两个隔离级别的一个很大不同就是:生成 ReadView 的时机不同,READ COMMITTD 在每一次进行普通 SELECT 操作前都会生成一个 ReadView,而 REPEATABLE READ 只在第一次进行普通 SELECT 操作前生成一个 ReadView,之后的查询操作都重复使用这个 ReadView 就好了,从而基本上可以避免幻读现象。



四、InnoDB 的 Buffer Pool

对于使用 InnoDB 作为存储引擎的表来说,不管是用于存储用户数据的索引,包括:聚簇索引和二级索引,还是各种系统数据,都是以页的形式存放在表空间中的,而所谓的表空间只不过是 InnoDB 对文件系统上一个或几个实际文件的抽象,也就是说数据还是存储在磁盘上的。

但是磁盘的速度慢,所以 InnoDB 存储引擎在处理客户端的请求时,当需要访问某个页的数据时,就会把完整的页的数据全部加载到内存中,即使只需要访问一个页的一条记录,那也需要先把整个页的数据加载到内存中。将整个页加载到内存中后就可以进行读写访问了,在进行完读写访问之后并不着急把该页对应的内存空间释放掉,而是将其缓存起来,这样将来有请求再次访问该页面时,就可以省去磁盘 IO 的开销了。

Buffer Pool

InnoDB 为了缓存磁盘中的页,在 MySQL 服务器启动的时候就向操作系统申请了一片连续的内存,这块连续内存叫做:Buffer Pool,中文名:缓冲池。

默认情况下 Buffer Pool 只有 128M 大小。

查看该值:show variables like 'innodb_buffer_pool_size';

可以在启动服务器的时候配置 innodb_buffer_pool_size 参数的值,它表示 Buffer Pool 的大小,配置如下:

  
  
  
  
  
[server]innodb_buffer_pool_size = 268435456

其中,268435456 的单位是字节,也就是指定 Buffer Pool 的大小为 256M,Buffer Pool 也不能太小,最小值为 5M,当小于该值时会自动设置成 5M。

启动 MySQL 服务器的时候,需要完成对 Buffer Pool 的初始化过程,就是先向操作系统申请 Buffer Pool 的内存空间,然后把它划分成若干对控制块和缓 存页。但是此时并没有真实的磁盘页被缓存到 Buffer Pool 中,之后随着程序的运行,会不断的有磁盘上的页被缓存到 Buffer Pool 中。

在 Buffer Pool 中会创建多个缓存页,默认的缓存页大小和在磁盘上默认的页大小是一样的,都是 16KB。

那么怎么知道该页在不在 Buffer Pool 中呢?

在查找数据的时候,先通过哈希表中查找 key 是否在哈希表中,如果在证明 Buffer Pool 中存在该缓存也信息,如果不存在证明不存该缓存也信息,则通过读取磁盘加载该页信息放到 Buffer Pool 中,哈希表中的 key 是通过表空间号+ 页号作组成的,value 是 Buffer Pool 的缓存页。

flush 链表的管理

如果修改了 Buffer Pool 中某个缓存页的数据,那它就和磁盘上的页不一致了,这样的缓存页也被称为:脏页。最简单的做法就是每发生一次修改就立即同步到磁盘上对应的页上,但是频繁的往磁盘中写数据会严重的影响程序的性能。所以每次修改缓存页后,并不着急把修改同步到磁盘上,而是在未来的某个时间进行同步。但是如果不立即同步到磁盘的话,那之后再同步的时候怎么知道 Buffer Pool 中哪些页是脏页,哪些页从来没被修改过呢?总不能把所有的缓存页都同步到磁盘上吧,如果 Buffer Pool 被设置的很大,那一次性同步会非常慢。

所以,需要再创建一个存储脏页的链表,凡是修改过的缓存页对应的控制块都会作为一个节点加入到一个链表中,因为这个链表节点对应的缓存页都是需要被刷新到磁盘上的,所以也叫 flush 链表。

刷新脏页到磁盘

后台有专门的线程每隔一段时间负责把脏页刷新到磁盘,这样可以不影响用户线程处理正常的请求。

从 flush 链表中刷新一部分页面到磁盘,后台线程也会定时从 flush 链表中刷新一部分页面到磁盘,刷新的速率取决于当时系统是不是很繁忙。这种刷新页面的方式被称之为:BUF_FLUSH_LIST。



五、redo 日志

redo 日志的作用

InnoDB 存储引擎是以页为单位来管理存储空间的,增删改查操作其实本质上都是在访问页面,包括:读页面、写页面、创建新页面等操作。在真正访问页面之前,需要把在磁盘上的页缓存到内存中的 Buffer Pool 之后才可以访问。但是在事务的时候又强调过一个称之为持久性的特性,就是说对于一个已经提交的事务,在事务提交后即使系统发生了崩溃,这个事务对数据库中所做的更改也不能丢失。

如果只在内存的 Buffer Pool 中修改了页面,假设在事务提交后突然发生了某个故障,导致内存中的数据都失效了,那么这个已经提交了的事务对数据库中所做的更改也就跟着丢失了,这是所不能忍受的。那么如何保证这个持久性呢?一个很简单的做法就是在事务提交完成之前把该事务所修改的所有页面都刷新到磁盘,但是这个简单粗暴的做法有些问题:

  1. 刷新一个完整的数据页太浪费了;有时候仅仅修改了某个页面中的一个字节,但是在 InnoDB 中是以页为单位来进行磁盘 IO 的,也就是说在该事务提交时不得不将一个完整的页面从内存中刷新到磁盘,一个页面默认是16KB 大小,只修改一个字节就要刷新 16KB 的数据到磁盘上显然是太浪费了。

  2. 随机 IO 刷起来比较慢;一个事务可能包含很多语句,即使是一条语句也可能修改许多页面,该事务修改的这些页面可能并不相邻,这就意味着在将某个事务修改的 Buffer Pool 中的页面刷新到磁盘时,需要进行很多的随机 IO,随机 IO 比顺序 IO 要慢,尤其对于传统的机械硬盘来说。

只是想让已经提交了的事务对数据库中数据所做的修改永久生效,即使后来系统崩溃,在重启后也能把这种修改恢复出来。其实没有必要在每次事务提交时就把该事务在内存中修改过的全部页面刷新到磁盘,只需要把修改了哪些东西记录一下就好,比方说:某个事务将系统表空间中的第 5 号页面中偏移量为 5000 处的那个字节的值 0 改成 5 只需要记录一下:将第 5 号表空间的 5 号页面的偏移量为 5000 处的值更新为:5

这样在事务提交时,把上述内容刷新到磁盘中,即使之后系统崩溃了,重启之后只要按照上述内容所记录的步骤重新更新一下数据页,那么该事务对数据库中所做的修改又可以被恢复出来,也就意味着满足持久性的要求。因为在系统崩溃重启时需要按照上述内容所记录的步骤重新更新数据页,所以上述内容也被称之为:重做日志,即:redo log。与在事务提交时将所有修改过的内存中的页面刷新到磁盘中相比,只将该事务执行过程中产生的 redo log 刷新到磁盘的好处如下:

  1. redo log 占用的空间非常小存储表空间 ID、页号、偏移量以及需要更新的值所需的存储空间是很小的;

  2. redo log 是顺序写入磁盘的在执行事务的过程中,每执行一条语句,就可能产生若干条 redo log,这些日志是按照产生的顺序写入磁盘的,也就是使用顺序 IO;

redo log 的写入过程

InnoDB 为了更好的进行系统崩溃恢复,把一次原子操作生成的 redo log 都放在了大小为 512 字节的块(block)中。

为了解决磁盘速度过慢的问题而引入了 Buffer Pool。同理,写入 redo log 时也不能直接写到磁盘上,实际上在服务器启动时就向操作系统申请了一大片称之为 redo log buffer 的连续内存空间,即:redo log 缓冲区,也可以简称:log buffer。这片内存空间被划分成若干个连续的 redo log block,可以通过启动参数innodb_log_buffer_size 来指定 log buffer 的大小,该启动参数的默认值为:16MB。

向 log buffer 中写入 redo log 的过程是顺序的,也就是先往前边的 block 中写,当该 block 的空闲空间用完之后再往下一个 block 中写。

redo log 刷盘时机

log buffer 什么时候会写入到磁盘呢?

  • log buffer 空间不足时,如果不停地往这个有限大小的 log buffer 里塞入日志,很快它就会被填满。InnoDB 认为如果当前写入 log buffer 的 redo log 量已 经占满了 log buffer 总容量的大约一半左右,就需要把这些日志刷新到磁盘上。

  • 事务提交时,必须要把修改这些页面对应的 redo log 刷新到磁盘。

  • 后台有一个线程,大约每秒都会刷新一次 log buffer 中的 redo log 到磁盘。

  • 正常关闭服务器时等等。



六、undo 日志

事务需要保证原子性,也就是事务中的操作要么全部完成,要么什么也不做。但是偏偏有时候事务执行到一半会出现一些情况,比如:

  • 情况一:事务执行过程中可能遇到各种错误,比如服务器本身的错误,操作系统错误,甚至是突然断电导致的错误。

  • 情况二:程序员可以在事务执行过程中手动输入 ROLLBACK 语句结束当前的事务的执行。

这两种情况都会导致事务执行到一半就结束,但是事务执行过程中可能已经修改了很多东西,为了保证事务的原子性,需要把东西改回原先的样子,这个过程就称之为回滚,即:rollback,这样就可以造成这个事务看起来什么都没做,所以符合原子性要求。

每当要对一条记录做改动时,都需要把回滚时所需的东西都给记下来。

比方说:

  • 插入一条记录时,至少要把这条记录的主键值记下来,之后回滚的时候只需要把这个主键值对应的记录删掉。

  • 删除了一条记录,至少要把这条记录中的内容都记下来,这样之后回滚时再把由这些内容组成的记录插入到表中。

  • 修改了一条记录,至少要把修改这条记录前的旧值都记录下来,这样之后回滚时再把这条记录更新为旧值。

这些为了回滚而记录的这些东西称之为撤销日志,即:undo log。这里需要注意的一点是,由于查询操作并不会修改任何用户记录,所以在查询操作执行时,并不需要记录相应的 undo log。

undo 日志的格式

为了实现事务的原子性,InnoDB 存储引擎在实际进行增、删、改一条记录时,都需要先把对应的 undo 日志记下来。一般每对一条记录做一次改动,就对应着一条 undo 日志,但在某些更新记录的操作中,也可能会对应着 2 条 undo 日志。

一个事务在执行过程中可能新增、删除、更新若干条记录,也就是说需要记录很多条对应的 undo 日志,这些 undo 日志会被从 0 开始编号,也就是说根据生成的顺序分别被称为第 0 号 undo 日志、第 1 号 undo 日志、...、第 n 号 undo 日志等,这个编号也被称之为 undo no。

这些 undo 日志是被记录到类型为 FIL_PAGE_UNDO_LOG 的页面中。这些页面可以从系统表空间中分配,也可以从一种专门存放 undo 日志的表空间,也就是所谓的 undo tablespace 中分配。

-end-

本文分享自微信公众号 - 京东云开发者(JDT_Developers)。
如有侵权,请联系 [email protected] 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

阿里云严重故障,全线产品受影响(已恢复) 俄罗斯操作系统 Aurora OS 5.0 全新 UI 亮相 汤不热 (Tumblr) 凉了 多家互联网公司急招鸿蒙程序员 .NET 8 正式 GA,最新 LTS 版本 UNIX 时间即将进入 17 亿纪元(已进入) 小米官宣 Xiaomi Vela 全面开源,底层内核为 NuttX Linux 上的 .NET 8 独立体积减少 50% FFmpeg 6.1 "Heaviside" 发布 微软推出全新“Windows App”
{{o.name}}
{{m.name}}

Guess you like

Origin my.oschina.net/u/4090830/blog/10143831