【MySQL】事务管理

 > 作者:დ旧言~
> 座右铭:松树千年终是朽,槿花一日自为荣。

> 目标:理解并掌握MySQL的事务管理。

> 毒鸡汤:有些事情,总是不明白,所以我不会坚持。早安!

> 专栏选自:带你玩转MySQL

> 望小伙伴们点赞????收藏✨加关注哟????????

​​​

一、前言

想必大家在学校也学习过MySQL,可能学的懵懵懂懂,这个板块我们从入门开始,从最新的安装MySQL到学习MySQL语句,一步一步开始,一切都是新的,新的板块新的开始,大家一起努力,一起进步!!!

  二主体

学习【MySQL】事务管理咱们按照下面的图解:

2.1 什么是事务

事务的基本概念:

  • 简单来说,事务可以看作是一组数据操作语言(DML)语句的组成,这些语句在逻辑上具有强烈的相关性。事务的目的是确保这组操作要么全部成功执行,要么全部失败回滚,以保持数据库的一致性。
  • 在事务中,如果其中任何一条语句执行失败,整个事务将会被回滚,即之前的操作都会被撤销,数据库状态回到事务开始之前的状态。只有当所有的操作都成功执行时,事务才会被提交,使得之前的操作生效并永久保存在数据库中。
  • 事务的使用可以确保数据库操作的完整性和可靠性,特别是在需要多个操作同时生效或者需要对数据进行一系列关联性修改时非常有用。通过将相关的操作组合在一个事务中,可以确保数据在多个操作之间保持一致,同时提供了对数据库的回滚和提交的控制能力。

事务的基本属性:

  • 原子性(Atomicity):事务被视为一个不可分割的原子操作。要么所有的操作都成功执行,将更改永久保存到数据库中,即事务被提交;要么如果任何一个操作失败,所有的操作都将被回滚,撤销对数据库的修改,即事务被中止。原子性确保了事务的完整性,即要么所有的操作都生效,要么都不生效。
  • 一致性(Consistency):事务执行前后,数据库的状态必须保持一致。这意味着事务开始之前的数据库状态应该满足所有的约束条件、完整性规则和触发器,事务结束后,数据库状态也应该满足这些规则。如果事务执行过程中违反了一致性规则,事务将会被回滚,以保持数据库的一致性。
  • 隔离性(Isolation):事务的隔离性指的是每个事务的执行都应该与其他事务相互隔离,使得每个事务感觉不到其他事务的存在。事务的隔离性可以防止并发执行的事务相互干扰,避免数据的不一致性和并发问题,保证事务的独立性和正确性。
  • 持久性(Durability):一旦事务提交,其所做的修改将会永久保存在数据库中,即使在系统故障或重启后也不会丢失。持久性确保了事务的持久效果,保证数据的可靠性和持久性。

这些事务的基本属性一起确保了数据库操作的可靠性、一致性和持久性。当所有的属性都满足时,可以说事务是正确执行的。数据库管理系统提供了机制来确保事务的ACID属性,以支持可靠的数据处理和并发控制。

2.2 为什么会出现事务

十五被MySQL编写者设计出来,本质是为了当应用程序访问数据库的时候,事务能够简化我们的编程模型,不需要我们去考虑各种各样的潜在错误和并发问题。如果MySQL只是单纯的提供数据存储服务,那么用户在访问数据库时就需要自行考虑各种潜在问题,包括网络异常、服务器宕机等。因此事务本质是为了应用服务的,而不是伴随着数据库系统天生就有的。

2.3 事务的版本支持

MySQL中只有使用了 InnoDB 数据库引擎的数据库或表才支持事务,MyISAM 不支持。

查看数据库的引擎:show engines

  • Engine: 表示存储引擎的名称。
  • Support: 表示服务器对存储引擎的支持级别,YES表示支持,NO表示不支持,DEFAULT表示数据库默认使用的存储引擎,DISABLED表示支持引擎但已将其禁用。
  • Comment: 表示存储引擎的简要说明。
  • Transactions: 表示存储引擎是否支持事务,可以看到InnoDB存储引擎支持事务,而MyISAM存储引擎不支持事务。
  • XA: 表示存储引擎是否支持XA事务。
  • Savepoints: 表示存储引擎是否支持保存点。

2.4 事务的提交方式

事务的提交方式主要有以下两种:

  • 自动提交(Auto Commit):在自动提交模式下,每个SQL语句都被视为一个独立的事务,并自动提交到数据库中。这意味着在执行每个SQL语句后,都会立即将其结果永久保存到数据库中,无需显式地执行提交语句。自动提交模式是一种默认的提交方式,在大多数数据库管理系统中,默认情况下处于自动提交模式。
  • 手动提交(Manual Commit):在手动提交模式下,事务的提交需要显式地执行提交语句。在手动提交模式下,通过执行提交语句(如COMMIT)将事务的操作结果永久保存到数据库中。在手动提交模式下,可以在事务执行过程中进行回滚(ROLLBACK)或提交(COMMIT)操作,具有更细粒度的事务控制。

查看事务的提交方式:

show variables like 'autocommit';

 设置事务的提交方式:

-- 关闭自动提交,及设置成手动提交
set autocommit = 0;

-- 设置事务为自动提交
set autocommit = 1;

2.5 事务的相关演示

当我们设置完隔离级别后需要重新登录才能生效:

mysql> set global transaction isolation level READ UNCOMMITTED;
Query OK, 0 rows affected (0.00 sec)

mysql> select @@tx_isolation;
+-----------------+
| @@tx_isolation  |
+-----------------+
| REPEATABLE-READ |
+-----------------+
1 row in set, 1 warning (0.00 sec)

创建一个银行用户表,表中包含用户的id、姓名和账户余额,如下:

create table if not exists account(
	id int primary key,
	name varchar(50) not null default '',
	blance decimal(10,2) not null default 0.0
)ENGINE=InnoDB DEFAULT CHARSET=UTF8;

事务的开始与回滚:

  • 使用 beginstart transaction 命令,可以启动一个事务。
  • 使用 savepoint 保存点命令,可以在事务中创建指定名称的保存点。
  • 使用 rollback to 保存点命令,可以让事务回滚到指定保存点。
  • 使用 rollback 命令,可以直接让事务回滚到最开始。
  • 使用 commit 命令,可以提交事务,提交事务后就不能回滚了。

演示1 - 证明未commit,客户端崩溃,MySQL自动会回滚(隔离级别设置为读未提交):

-- 终端A
mysql> select * from account;          -- 当前表内无数据
Empty set (0.00 sec)
mysql> show variables like 'autocommit'; -- 依旧自动提交
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit   | ON   |
+---------------+-------+
1 row in set (0.00 sec)
mysql> begin;                            --开启事务
Query OK, 0 rows affected (0.00 sec)
mysql> insert into account values (1, '张三', 100);   -- 插入记录
Query OK, 1 row affected (0.00 sec)
mysql> select * from account;           --数据已经存在,但没有commit,此时同时查看
终端B
+----+--------+--------+
| id | name   | blance |
+----+--------+--------+
|  1 | 张三   | 100.00 |
+----+--------+--------+
非正常演示2 - 证明commit了,客户端崩溃,MySQL数据不会在受影响,已经持久化
非正常演示3 - 对比试验。证明begin操作会自动更改提交方式,不会受MySQL是否自动提交影响
1 row in set (0.00 sec)
mysql> Aborted                          -- ctrl + \ 异常终止MySQL
--终端B
mysql> select * from account;           --终端A崩溃前
+----+--------+--------+
| id | name   | blance |
+----+--------+--------+
|  1 | 张三   | 100.00 |
+----+--------+--------+
1 row in set (0.00 sec)
mysql> select * from account;          --数据自动回滚
Empty set (0.00 sec)

演示2 - 证明commit了,客户端崩溃,MySQL数据不会在受影响,已经持久化:

--终端 A
mysql> show variables like 'autocommit'; -- 依旧自动提交
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit   | ON   |
+---------------+-------+
1 row in set (0.00 sec)
mysql> select * from account;   -- 当前表内无数据
Empty set (0.00 sec)
mysql> begin;                   -- 开启事务
Query OK, 0 rows affected (0.00 sec)
mysql> insert into account values (1, '张三', 100); -- 插入记录
Query OK, 1 row affected (0.00 sec)
mysql> commit;                         --提交事务
Query OK, 0 rows affected (0.04 sec)
mysql> Aborted                          -- ctrl + \ 异常终止MySQL
--终端 B
mysql> select * from account;          --数据存在了,所以commit的作用是将数据持久
化到MySQL中
+----+--------+--------+
| id | name   | blance |
+----+--------+--------+
|  1 | 张三   | 100.00 |
+----+--------+--------+
1 row in set (0.00 sec)

演示3 - 对比试验。证明begin操作会自动更改提交方式,不会受MySQL是否自动提交影响:

-- 终端 A
mysql> select *from account;               --查看历史数据
+----+--------+--------+
| id | name   | blance |
+----+--------+--------+
|  1 | 张三   | 100.00 |
+----+--------+--------+
1 row in set (0.00 sec)
mysql> show variables like 'autocommit';   --查看事务提交方式
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit   | ON   |
+---------------+-------+
1 row in set (0.00 sec)
mysql> set autocommit=0;                   --关闭自动提交
Query OK, 0 rows affected (0.00 sec)
mysql> show variables like 'autocommit';   --查看关闭之后结果
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit   | OFF   |
+---------------+-------+
1 row in set (0.00 sec)
mysql> begin;                             --开启事务
Query OK, 0 rows affected (0.00 sec)
mysql> insert into account values (2, '李四', 10000);   --插入记录
Query OK, 1 row affected (0.00 sec)
mysql> select *from account;              --查看插入记录,同时查看终端B
+----+--------+----------+
| id | name   | blance   |
+----+--------+----------+
|  1 | 张三   |   100.00 |
|  2 | 李四   | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)
mysql> Aborted                           --再次异常终止
-- 终端B
mysql> select * from account;       --终端A崩溃前
+----+--------+----------+
| id | name   | blance   |
+----+--------+----------+
|  1 | 张三   |   100.00 |
|  2 | 李四   | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)
mysql> select * from account;      --终端A崩溃后,自动回滚
+----+--------+--------+
| id | name   | blance |
+----+--------+--------+
|  1 | 张三   | 100.00 |

演示4 - 证明单条 SQL 与事务的关系:

--实验一
-- 终端A
mysql> select * from account;
+----+--------+--------+
| id | name   | blance |
+----+--------+--------+
|  1 | 张三   | 100.00 |
+----+--------+--------+
1 row in set (0.00 sec)
mysql> show variables like 'autocommit';  
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit   | ON   |
+---------------+-------+
1 row in set (0.00 sec)
mysql> set autocommit=0;                  --关闭自动提交
Query OK, 0 rows affected (0.00 sec)
mysql> insert into account values (2, '李四', 10000);   --插入记录
Query OK, 1 row affected (0.00 sec)
mysql> select *from account;                   --查看结果,已经插入。此时可以在查
看终端B
+----+--------+----------+
| id | name   | blance   |
+----+--------+----------+
|  1 | 张三   |   100.00 |
|  2 | 李四   | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)
mysql> ^DBye                                    --ctrl + \ or ctrl + d,终止终
端
--终端B
mysql> select * from account;                    --终端A崩溃前
+----+--------+----------+
| id | name   | blance   |
+----+--------+----------+
|  1 | 张三   |   100.00 |
|  2 | 李四   | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)
mysql> select * from account;                   --终端A崩溃后
+----+--------+--------+
| id | name   | blance |
+----+--------+--------+
|  1 | 张三   | 100.00 |
+----+--------+--------+
1 row in set (0.00 sec)
-- 实验二
--终端A
mysql> show variables like 'autocommit'; --开启默认提交
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit   | ON   |
+---------------+-------+
1 row in set (0.00 sec)
mysql> select * from account;
+----+--------+--------+
| id | name   | blance |
+----+--------+--------+
|  1 | 张三   | 100.00 |
+----+--------+--------+
1 row in set (0.00 sec)
mysql> insert into account values (2, '李四', 10000);
Query OK, 1 row affected (0.01 sec)
mysql> select *from account;   --数据已经插入
+----+--------+----------+
| id | name   | blance   |
+----+--------+----------+
|  1 | 张三   |   100.00 |
|  2 | 李四   | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)
mysql> Aborted              --异常终止
--终端B
mysql> select * from account;        --终端A崩溃前
+----+--------+----------+
| id | name   | blance   |
+----+--------+----------+
|  1 | 张三   |   100.00 |
|  2 | 李四   | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)
mysql> select * from account;      --终端A崩溃后,并不影响,已经持久化。autocommit
起作用
+----+--------+----------+
| id | name   | blance   |
+----+--------+----------+
|  1 | 张三   |   100.00 |
|  2 | 李四   | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)

结论:

  • 只要输入begin或者start transaction,事务便必须要通过commit提交,才会持久化,与是
  • 否设置set autocommit无关。
  • 事务可以手动回滚,同时,当操作异常,MySQL会自动回滚
  • 对于 InnoDB 每一条 SQL 语言都默认封装成事务,自动提交。(select有特殊情况,因为
  • MySQL 有 MVCC )
  • 从上面的例子,我们能看到事务本身的原子性(回滚),持久性(commit)

事务操作注意事项:

  • 如果没有设置保存点,也可以回滚,只能回滚到事务的开始。直接使用 rollback(前提是事务
  • 还没有提交)
  • 如果一个事务被提交了(commit),则不可以回退(rollback)
  • 可以选择回退到哪个保存点
  • InnoDB 支持事务, MyISAM 不支持事务
  • 开始事务可以使 start transaction 或者 begin

2.6 事务的隔离级别

概念:

  • 简单来说,事务的隔离性就是指数据库管理系统(DBMS)可以确保在同时运行多个事务时,每个事务都能独立地执行,并且不会互相干扰。换句话说,一个事务的执行不应该影响到其他事务的执行,同时也不受其他事务的影响。
  • 在事务隔离性中,每个事务都应该具有独立的执行空间和资源,如内存和CPU时间。这样可以避免一个事务修改了正在被其他事务使用的数据,从而导致数据的不一致性。DBMS通常通过锁定机制来实现隔离性,以确保事务之间不会互相干扰。这些锁可以是行级锁、页级锁或表级锁,具体实现取决于DBMS的架构和设计。

四种隔离级别:

  • 读未提交(Read Uncommitted): 在该隔离级别下,所有的事务都可以看到其他事务没有提交的执行结果,实际生产中不可能使用这种隔离级别,因为这种隔离级别相当于没有任何隔离性,会存在很多并发问题,如 脏读、幻读、不可重复读 等。
  • 读提交(Read Committed): 该隔离级别是大多数数据库的默认隔离级别,但它不是MySQL默认的隔离级别,它满足了隔离的简单定义:一个事务只能看到其他已经提交的事务所做的改变,但这种隔离级别存在 不可重复读 和 幻读 的问题。
  • 可重复读(Repeatable Read): 这是MySQL默认的隔离级别,该隔离级别确保同一个事务在执行过程中,多次读取操作数据时会看到同样的数据,即解决了不可重复读的问题,但这种隔离级别下仍然存在 幻读 的问题。
  • 串行化(Serializable): 这是事务的最高隔离级别,该隔离级别通过强制事务排序,使之不可能相互冲突,从而解决了幻读问题。它在每个读的数据行上面加上共享锁,但是可能会导致超时和锁竞争问题,这种隔离级别太极端,实际生成中基本不使用。

虽然数据库事务的隔离级别有以上四种,但一个稳态的数据库只会选择这其中的一种,作为自己的默认隔离级别。但数据库默认的隔离级别有时可能并不满足上层的业务需求,因此数据库提供了这四种隔离级别,可以让我们自行设置。隔离级别基本上都是通过加锁的方式实现的,不同的隔离级别对锁的使用是不同的,常见的有表锁、行锁、写锁、间隙锁(GAP)、Next-Key锁(GAP+行锁)等。

2.6.1 查看全局隔离级别

查看隔离级别:

mysql> select @@global.tx_isolation; -- 查看全局隔离级别
+-----------------------+
| @@global.tx_isolation |
+-----------------------+
| READ-UNCOMMITTED      |
+-----------------------+
1 row in set, 1 warning (0.00 sec)

mysql> select @@session.tx_isolation; -- 查看当前会话隔离级别
+------------------------+
| @@session.tx_isolation |
+------------------------+
| READ-UNCOMMITTED       |
+------------------------+
1 row in set, 1 warning (0.00 sec)

mysql> select @@tx_isolation; -- 查看当前会话隔离级别
+------------------+
| @@tx_isolation   |
+------------------+
| READ-UNCOMMITTED |
+------------------+
1 row in set, 1 warning (0.00 sec)

设置隔离级别:

-- 设置当前会话 或者 全局隔离级别语法
SET [SESSION | GLOBAL] TRANSACTION ISOLATION LEVEL 
	{READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE}

通过 set session transaction isolation level 隔离级别命令,可以设置当前会话的隔离级别:

设置会话的隔离级别只会影响当前会话,新起的会话依旧采用全局隔离级。

通过 set global transaction isolation level 隔离级别 命令,可以设置全局隔离级别:

设置全局隔离级别会影响后续的新会话,但当前会话的隔离级别没有发生变化,如果要让当前会话的隔离级别也改变,则需要重启会话。

2.6.2 读未提交(Read Uncommitted)

启动两个终端,将隔离级别设置为读未提交,并查看此时银行用户表中的数据,如下:

在两个终端各自启动一个事务,左终端中的事务所作的修改在没有提交之前,右终端中的事务就已经能够看到了:

总结:

  • 读未提交是事务的最低隔离级别,几乎没有加锁,虽然效率高,但是问题比较多,所以严重不建议使用。
  • 一个事务在执行过程中,读取到另一个执行中的事务所做的修改,但是该事务还没有进行提交,这种现象叫做脏读。

2.6.3 读提交(Read Committed)

启动两个终端,将隔离级别都设置为读提交,并查看此时银行用户表中的数据:

左边的事务所做的修改在没有提交之前,右边终端是无法看到的:

当左终端中的事务提交后,右终端中的事务才能看到修改后的数据:

细节:

一个事务在执行过程中,两个相同的 select 查询得到了不同的数据,这种现象叫做 不可重复读。

2.6.4 可重复读(Repeatable Read)

启动两个终端,将隔离级别都设置为可重复读,并查看此时银行用户表中的数据。如下:

两个终端各自启动一个事务,左终端中的事务所作的修改在没有提交之前,右终端中的事务无法看到:

并且当左终端中的事务提交后,右终端中的事务仍然看不到修改后的数据:

只有当右终端中的事务提交后再查看表中的数据,这时才能看到修改后的数据:

小结:

  • 在可重复读隔离级别下,一个事务在执行过程中,相同的select查询得到的是相同的数据,这就是所谓的可重复读。
  • 一般的数据库在可重复读隔离级别下,update数据是满足可重复读的,但insert数据会存在幻读问题,因为隔离性是通过对数据加锁完成的,而新插入的数据原本是不存在的,因此一般的加锁无法屏蔽这类问题。
  • 一个事务在执行过程中,相同的select查询得到了新的数据,如同出现了幻觉,这种现象叫做幻读。
  • MySQL是通过Next-Key锁(GAP+行锁)来解决幻读问题的。

2.6.5 串行化(Serializable)

启动两个终端,将隔离级别都设置为串行化,并查看此时银行用户表中的数据:

在两个终端各自启动一个事务,如果这两个事务都对表进行的是读操作,那么这两个事务可以并发执行,不会被阻塞:

但如果这两个事务中有一个事务要对表进行写操作,那么这个事务就会立即被阻塞:

直到访问这张表的其他事务都提交后,这个被阻塞的事务才会被唤醒,然后才能对表进行修改操作:

小结:

串行化是事务的最高隔离级别,多个事务同时进行读操作时加的是共享锁,因此可以并发执行读操作,但一旦需要进行写操作,就会进行串行化,效率很低,几乎不会使用。

2.6.6 总结

  • 隔离级别越严格,安全性越高,但数据库的并发性能也就越低,往往需要在两者之间找一个平衡点。
  • 不可重复读的重点是修改和删除:同样的条件, 你读取过的数据,再次读取出来发现值不一样了。
  • 幻读的重点在于新增:同样的条件, 第1次和第2次读出来的记录数不一样。
  • MySQL默认的隔离级别是可重复读,一般情况下不要修改。
  • 上面的例子可以看出,事务也有长短事务这样的概念。事务间互相影响,指的是事务在并行执行的时候,即都没有commit的时候,影响会比较大。

2.7 多版本并发控制

数据库并发的场景有三种:

  • 读-读 :不存在任何问题,也不需要并发控制。
  • 读-写 :有线程安全问题,可能会造成事务隔离性问题,可能遇到脏读,幻读,不可重复读。
  • 写-写 :有线程安全问题,可能会存在更新丢失问题,比如第一类更新丢失,第二类更新丢失。

解释说明:

  • 写-写并发场景下的第一类更新丢失又叫做回滚丢失,即一个事务的回滚把另一个已经提交的事务更新的数据覆盖了,第二类更新丢失又叫做覆盖丢失,即一个事务的提交把另一个已经提交的事务更新的数据覆盖了。
  • 读-读并发不需要进行并发控制,写-写并发实际也就是对数据进行加锁,这里最值得讨论的是读-写并发,读-写并发是数据库当中最高频的场景,在解决读-写并发时不仅需要考虑线程安全问题,还需要考虑并发的性能问题。

多版本并发控制概念:

多版本并发控制(Multi-Version Concurrency Control,MVCC) 是一种用来解决读写冲突的无锁并发控制,主要依赖记录中的 3个隐藏字段、undo日志和Read View 实现。

  • 为事务分配单向增长的事务ID,为每个修改保存一个版本,将版本与事务ID相关联,读操作只读该事务开始前的数据库快照。
  • MVCC保证读写并发时,读操作不会阻塞写操作,写操作也不会阻塞读操作,提高了数据库并发读写的性能,同时还可以解决脏读、幻读和不可重复读等事务隔离性问题。

2.7.1 三个记录隐藏列字段

数据库中的每条记录都会有如下3个隐藏字段:

  • DB_TRX_ID:6字节,创建或最近一次修改该记录的事务ID。
  • DB_ROW_ID:6字节,隐含的自增ID(隐藏主键)。
  • DB_ROLL_PTR:7字节,回滚指针,指向这条记录的上一个版本。

补重说明:

  1. 采用InnoDB存储引擎建立的每张表都会有一个主键,如果用户没有设置,InnoDB就会自动以DB_ROW_ID产生一个聚簇索引。
  2. 此外,数据库表中的每条记录还有一个删除flag隐藏字段,用于表示该条记录是否被删除,便于进行数据回滚。

举个栗子:创建一个学生表,表中包含学生的姓名和年龄

mysql> create table if not exists student(
name varchar(11) not null,
age int not null
);
mysql> insert into student (name, age) values ('张三', 28);
Query OK, 1 row affected (0.05 sec)
mysql> select * from student;
+--------+-----+
| name   | age |
+--------+-----+
| 张三   |  28 |
+--------+-----+
1 row in set (0.00 sec)

解释说明:

name age DB_TRX_ID(创建该记录的事务ID) DB_ROW_ID(隐式主键) DB_ROLL_PTR(回滚指针)
张三 28 9 1 null
  • 假设插入该记录的事务的事务ID为9,那么该记录的DB_TRX_ID字段填的就是9。
  • 因为这是插入的第一条记录,所以隐式主键DB_ROW_ID字段填的就是1。
  • 由于这条记录是新插入的,没有历史版本,所以回滚指针DB_ROLL_PTR的值设置为null。
  • MVCC重点需要的就是这三个隐藏字段,实际还有其他隐藏字段,只不过没有画出。

2.7.2 undo日志

MySQL的三大日志如下:

  • redo log:重做日志,用于MySQL崩溃后进行数据恢复,保证数据的持久性。
  • bin log:逻辑日志,用于主从数据备份时进行数据同步,保证数据的一致性。
  • undo log:回滚日志,用于对已经执行的操作进行回滚,保证事务的原子性。

补充说明:

  • 我们之前所讲的所有机制:索引,事务,隔离性,日志等,都是在内存中完成的,即在MySQL 内部的相关缓冲区中,保存相关数据,完成各种判断操作。然后在合适的时候,将相关数据刷新到磁盘当中的。
  • 我们这里理解undo log,简单理解成,就是 MySQL 中的一段内存缓冲区,用来保存日志数据的就行。

2.7.3 模拟 MVCC(快照)

现在有一个事务10(仅仅为了好区分),对student表中记录进行修改(update):将name(张三)改成
name(李四):

  • 事务10,因为要修改,所以要先给该记录加行锁。
  • 修改前,现将改行记录拷贝到undo log中,所以,undo log中就有了一行副本数据。(原理就是写时拷贝)
  • 所以现在 MySQL 中有两行同样的记录。现在修改原始记录中的name,改成 '李四'。并且修改原始记录的隐藏字段 DB_TRX_ID 为当前 事务10 的ID, 我们默认从 10 开始,之后递增。而原始记录的回滚指针 DB_ROLL_PTR 列,里面写入undo log中副本数据的地址,从而指向副本记录,既表示我的上一个版本就是它。
  • 事务10提交,释放锁。

现在又有一个事务11,对student表中记录进行修改(update):将age(28)改成age(38):

  • 事务11,因为也要修改,所以要先给该记录加行锁。(该记录是那条?)
  • 修改前,现将改行记录拷贝到undo log中,所以,undo log中就又有了一行副本数据。此时,新的副本,我们采用头插方式,插入undo log。
  • 现在修改原始记录中的age,改成 38。并且修改原始记录的隐藏字段 DB_TRX_ID 为当前 事务11 的ID。而原始记录的回滚指针 DB_ROLL_PTR 列,里面写入undo log中副本数据的地址,从而指向副本记录,既表示我的上一个版本就是它。
  • 事务11提交,释放锁。

这样,我们就有了一个基于链表记录的历史版本链。所谓的回滚,无非就是用历史数据,覆盖当前数据。上面的一个一个版本,我们可以称之为一个一个的快照。

解释说明:

  • 所谓的回滚实际就是用undo log中的历史数据覆盖当前数据,而所谓的创建保存点就可以理解成是给某些版本做了标记,让我们可以直接用这些版本数据来覆盖当前数据。
  • 这种技术实际就是基于版本的写时拷贝,当需要进行写操作时先将最新版本拷贝一份到undo log中,然后再进行写操作,和父子进程为了保证独立性而进行的写时拷贝是类似的。

2.7.4 Read View

概念:

  • Read View就是事务进行 快照读 操作的时候生产的 读视图 (Read View),在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的ID(当每个事务开启时,都会被分配一个ID, 这个ID是递增的,所以最新的事务,ID值越大)
  • Read View 在 MySQL 源码中,就是一个类,本质是用来进行可见性判断的。 即当我们某个事务执行快照读的时候,对该记录创建一个 Read View 读视图,把它比作条件,用来判断当前事务能够看到哪个版本的数据,既可能是当前最新的数据,也有可能是该行记录的 undo log 里面的某个版本的数据。
class ReadView {
	// 省略...
private:
	/** 高水位:大于等于这个ID的事务均不可见*/
	trx_id_t m_low_limit_id;
	
	/** 低水位:小于这个ID的事务均可见 */
	trx_id_t m_up_limit_id;
	
	/** 创建该 Read View 的事务ID*/
	trx_id_t m_creator_trx_id;
	
	/** 创建视图时的活跃事务id列表*/
	ids_t m_ids;
	
	/** 配合purge,标识该视图不需要小于m_low_limit_no的UNDO LOG,
	* 如果其他视图也不需要,则可以删除小于m_low_limit_no的UNDO LOG*/
	trx_id_t m_low_limit_no;
	
	/** 标记视图是否被关闭*/
	bool m_closed;
	
	// 省略...
};

部分成员说明:

  • m_ids: 一张列表,记录Read View生成时刻,系统中活跃的事务ID。
  • m_up_limit_id: 记录m_ids列表中事务ID最小的ID。
  • m_low_limit_id: 记录Read View生成时刻,系统尚未分配的下一个事务ID。
  • m_creator_trx_id: 记录创建该Read View的事务的事务ID。

由于事务ID是单向增长的,因此根据Read View中的m_up_limit_id和m_low_limit_id,可以将事务ID分为三个部分:

  • 事务ID小于m_up_limit_id的事务,一定是生成Read View时已经提交的事务,因为m_up_limit_id是生成Read View时刻系统中活跃事务ID中的最小ID,因此事务ID比它小的事务在生成Read View时一定已经提交了。
  • 事务ID大于等于m_low_limit_id的事务,一定是生成Read View时还没有启动的事务,因为m_low_limit_id是生成Read View时刻,系统尚未分配的下一个事务ID。
  • 事务ID位于m_up_limit_id和m_low_limit_id之间的事务,在生成Read View时可能正处于活跃状态,也可能已经提交了,这时需要通过判断事务ID是否存在于m_ids中来判断该事务是否已经提交。

  • 一个事务在进行读操作时,只应该看到自己或已经提交的事务所作的修改,因此我们可以根据Read View来判断当前事务能否看到另一个事务所作的修改。
  • 版本链中的每个版本的记录都有自己的DB_TRX_ID,即创建或最近一次修改该记录的事务ID,因此可以依次遍历版本链中的各个版本,通过Read View来判断当前事务能否看到这个版本,如果不能则继续遍历下一个版本。

源码策略如下:

bool changes_visible(trx_id_t id, const table_name_t& name) const 
	MY_ATTRIBUTE((warn_unused_result))
{
	ut_ad(id > 0);
	//1、事务id小于m_up_limit_id(已提交)或事务id为创建该Read View的事务的id,则可见
	if (id < m_up_limit_id || id == m_creator_trx_id) {
		return(true);
	}
	check_trx_id_sanity(id, name);
	//2、事务id大于等于m_low_limit_id(生成Read View时还没有启动的事务),则不可见
	if (id >= m_low_limit_id) {
		return(false);
	}
	//3、事务id位于m_up_limit_id和m_low_limit_id之间,并且活跃事务id列表为空(即不在活跃列表中),则可见
	else if (m_ids.empty()) {
		return(true);
	}
	const ids_t::value_type* p = m_ids.data();
	//4、事务id位于m_up_limit_id和m_low_limit_id之间,如果在活跃事务id列表中则不可见,如果不在则可见
	return (!std::binary_search(p, p + m_ids.size(), id));
}

使用该函数时将版本的DB_TRX_ID传给参数id,该函数的作用就是根据Read View,判断当前事务能否看到这个版本。

2.8 RR 与 RC 的本质区别

启动两个终端,将隔离级别都设置为可重复读,并查看此时银行用户表中的数据。如下:

--重启终端
mysql> select @@tx_isolation;
+-----------------+
| @@tx_isolation |
+-----------------+
| REPEATABLE-READ |
+-----------------+
1 row in set, 1 warning (0.00 sec)

--依旧用之前的表
create table if not exists account(
  id int primary key,
  name varchar(50) not null default '',
  blance decimal(10,2) not null default 0.0
)ENGINE=InnoDB DEFAULT CHARSET=UTF8;

--插入一条记录,用来测试
mysql> insert into user (id, age, name) values (1, 15,'黄蓉');
Query OK, 1 row affected (0.00 sec)

在两个终端各自启动一个事务,在左终端中的事务操作之前,先让右终端中的事务查看一下表中的信息。如下:

左终端中的事务对表中的信息进行修改并提交,右终端中的事务看不到修改后的数据。如下:

在右终端中使用 select ... lock in share mode 命令进行当前读,可以看到表中的数据确实是被修改了,只是右终端中的事务看不到而已。如下:

但如果修改一下SQL的执行顺序,在两个终端各自启动一个事务后,直接让左终端中的事务对表中的信息进行修改并提交,然后再让右终端中的事务进行查看,这时右终端中的事务就直接看到了修改后的数据。如下:

在右终端中使用select ... lock in share mode命令进行当前读,可以看到刚才读取到的确实是最新的数据。如下:

  • 上面两次实验的唯一区别在于,右终端中的事务在左终端中的事务修改数据之前是否进行过快照读。
  • 由于RR级别下要求事务内每次读取到的结果必须是相同的,因此事务首次进行快照读的地方,决定了该事务后续快照读结果的能力。

RR与RC的本质区别:

  • 正是因为Read View生成时机的不同,从而造成了RC和RR级别下快照读的结果的不同。
  • 在RR级别下,事务第一次进行快照读时会创建一个Read View,将当前系统中活跃的事务记录下来,此后再进行快照读时就会直接使用这个Read View进行可见性判断,因此当前事务看不到第一次快照读之后其他事务所作的修改。
  • 而在RC级别下,事务每次进行快照读时都会创建一个Read View,然后根据这个Read View进行可见性判断,因此每次快照读时都能读取到被提交了的最新的数据。
  • RR级别下快照读只会创建一次Read View,所以RR级别是可重复读的,而RC级别下每次快照读都会创建新的Read View,所以RC级别是不可重复读的。

三、结束语 

       今天内容就到这里啦,时间过得很快,大家沉下心来好好学习,会有一定的收获的,大家多多坚持,嘻嘻,成功路上注定孤独,因为坚持的人不多。那请大家举起自己的小手给博主一键三连,有你们的支持是我最大的动力????????????,回见。

​​​​ 

上一篇:Presto


下一篇:✨云桥计划✨