当前位置: 首页 > news >正文

MySQL之事务与读视图

MySQL之事务与读视图

  • 一.事务的概念
  • 二.事务的ACID特性
  • 三.事务的使用
  • 四.事务的隔离性和隔离级别
    • 4.1查看和修改隔离级别
    • 4.2隔离级别的区别
  • 五.MVCC
    • 5.1记录隐藏字段
    • 5.2undo日志
    • 5.3快照读和当前读
  • 六.Read View
    • 6.1Read View的概念
    • 6.2RR和RC的实际区别

一.事务的概念

在之前对MySQL的学习时我们都是使用SQL来对数据库对表进行操作,所以我们是以一个一个的语句作为操作单元的。但是以语句为操作单元有时就会导致一个问题:当我们想进行一个复杂操作时需要多条语句全部成功才能达到预期效果但是只要其中只要在过程中有一条语句出现错误那么前置的语句就可能需要重新输入这就显著降低了我们的效率。所以我们是否可以将多条SQL语句打包成一个整体呢?我们可以直接运行这个整体,执行成功了就完成目标执行出错了就回到执行之前的状态这样我们就可以继续修改这个整体直到执行成功。
在MySQL中我们将多条语句组成得一个整体叫做一个事务,执行事务只有两个结果要么全部成功要么全部失败。注意一个事务可以是一条语句也可以是多条语句。
在日常生活中到处都需要用到事务也可以说MySQL的每个操作都是事务,例如我们在银行中进行转账,张三向李四的账户中转账500元,这就是一个事务其中涉及到对两个账户的更新需要将张三的账户减少500元,将李四的账务增加500元。
在这里插入图片描述

//创建银行客户表
mysql> create table bank(-> id int primary key,-> name varchar(20) not null,-> balance decimal(10,2) not null-> );
Query OK, 0 rows affected (0.01 sec)//插入张三和李四的信息
mysql> insert into bank values(1,'张三',1000),(2,'李四',1000);
Query OK, 2 rows affected (0.01 sec)
Records: 2  Duplicates: 0  Warnings: 0mysql> select * from bank;
+----+--------+---------+
| id | name   | balance |
+----+--------+---------+
|  1 | 张三   | 1000.00 |
|  2 | 李四   | 1000.00 |
+----+--------+---------+
2 rows in set (0.01 sec)//更新张三的余额信息
mysql> update bank set balance = balance -500 where name = '张三';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0//更新李四的余额信息
mysql> update bank set balance = balance +500 where name = '李四';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0mysql> select * from bank;
+----+--------+---------+
| id | name   | balance |
+----+--------+---------+
|  1 | 张三   |  500.00 |
|  2 | 李四   | 1500.00 |
+----+--------+---------+
2 rows in set (0.00 sec)

想要让一个事务无论是失败还是成功都是一个整体就需要让其拥有四个特性:原子性(Atomicity),一致性(Consistency),隔离性(Isolation),持久性(Durability)即ACID四大特性。

二.事务的ACID特性

  1. 原子性(Atomcity)
    在C++学习锁的时候我们就说过原子性,让一个事务拥有原子性的意义在于让其只有成功和失败这两种状态即非0即1。一个事务的所有操作不是全部成功就是全部失败,不会出现执行了一半的情况下当事务在执行中发生错误就会进行回滚操作让其恢复到还未执行事务的状态下。
    所以原子性保证了事务的非0即1。
  2. 一致性(Consisitency)
    事务的一致性是关联到我们之前说过的数据库的约束问题其的作用是保证数据库在事务执行之前和执行之后的完整性不会被破坏,所以需要规范事务中对数据库的写入操作是符合预期规则的即数据的精度数据的类型等等也就是符合约束条件。
    所以一致性保证了事务的写入是符合规则的。
  3. 隔离性(Isolation)
    事务是可以在数据库中并发执行的所以需要隔离性来保证多个事务并发执行时不会相互影响导致数据的不一致。事务可以修改自己的隔离等级来完成不同场景下的任务。
    所以隔离性保证了事务不会相互影响。
  4. 持久性(Durability)
    在事务执行完成后数据库的数据可能会发生改变,这种改变是从硬件层面的修改所以是永久性的即使之后系统发生故障也不会改变。
    所以持久性保证了事务对数据的修改是永久性的。

这四大特性保证了事务在使用过程中不会出现错误也可以说我们之所以使用事务就是因为它符合这四大特性所以其实事务是我们对ACID特性的具体实现。在日常的业务情况下我们需要大量的使用到事务,支持事务的数据库模型可以让我们不考虑执行过程中一些稀奇古怪杂七杂八的问题因为它要么完成要么回滚所以极大的便利了我们的使用也就是说事务其实是对应用层服务。

三.事务的使用

想要使用事务之前我们需要检查当前存储引擎是否支持事务,在MySQL中只有InnoDB是支持事务的,我们可以使用show engines来查看不同的引擎是否支持。

//Transaction为是否支持事务行
mysql> show engines;
+--------------------+---------+----------------------------------------------------------------+--------------+------+------------+
| Engine             | Support | Comment                                                        | Transactions/*事务*/| XA   | Savepoints |
+--------------------+---------+----------------------------------------------------------------+--------------+------+------------+
| ARCHIVE            | YES     | Archive storage engine                                         | NO           | NO   | NO         |
| BLACKHOLE          | YES     | /dev/null storage engine (anything you write to it disappears) | NO           | NO   | NO         |
| MRG_MYISAM         | YES     | Collection of identical MyISAM tables                          | NO           | NO   | NO         |
| FEDERATED          | NO      | Federated MySQL storage engine                                 | NULL         | NULL | NULL       |
| MyISAM             | YES     | MyISAM storage engine                                          | NO           | NO   | NO         |
| PERFORMANCE_SCHEMA | YES     | Performance Schema                                             | NO           | NO   | NO         |
| InnoDB             | DEFAULT | Supports transactions, row-level locking, and foreign keys     | YES          | YES  | YES        |
| MEMORY             | YES     | Hash based, stored in memory, useful for temporary tables      | NO           | NO   | NO         |
| CSV                | YES     | CSV storage engine                                             | NO           | NO   | NO         |
+--------------------+---------+----------------------------------------------------------------+--------------+------+------------+
9 rows in set (0.00 sec)

事务的操作是很简单的只有开始新事物,提交事务,回滚这三种操作,但是其中的细节还是比较多的例如是否自动提交,不同隔离级别的差异等等。我们先完成最简单的事务再谈细节。

//开始一个新事务
start transaction;
//或者
begin;//提交当前事务,并将更改进行持久化存储
commit;//回滚当前事务,取消其更改
rollback;
  1. 开启一个事务并在执行修改后回滚
//查看表中的数据
mysql> select * from bank;
+----+--------+---------+
| id | name   | balance |
+----+--------+---------+
|  1 | 张三   |  500.00 |
|  2 | 李四   | 1500.00 |
+----+--------+---------+
2 rows in set (0.00 sec)//开始一个事务
mysql> begin;
Query OK, 0 rows affected (0.00 sec)//更新数据
mysql> update bank set balance = balance + 500 where name = '张三';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0mysql> update bank set balance = balance - 500 where name = '李四';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0//再次查看表中数据发现更改已经生效
mysql> select * from bank;
+----+--------+---------+
| id | name   | balance |
+----+--------+---------+
|  1 | 张三   | 1000.00 |
|  2 | 李四   | 1000.00 |
+----+--------+---------+
2 rows in set (0.00 sec)//进行回滚
mysql> rollback;
Query OK, 0 rows affected (0.00 sec)//最后查看表中数据发现修改没有生效
mysql> select * from bank;
+----+--------+---------+
| id | name   | balance |
+----+--------+---------+
|  1 | 张三   |  500.00 |
|  2 | 李四   | 1500.00 |
+----+--------+---------+
2 rows in set (0.00 sec)
  1. 开始一个事务,执行修改后提交
//查看表中的数据
mysql> select * from bank;
+----+--------+---------+
| id | name   | balance |
+----+--------+---------+
|  1 | 张三   |  500.00 |
|  2 | 李四   | 1500.00 |
+----+--------+---------+
2 rows in set (0.00 sec)//开始一个事务
mysql> begin;
Query OK, 0 rows affected (0.00 sec)//更新数据
mysql> update bank set balance = balance + 500 where name = '张三';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0mysql> update bank set balance = balance - 500 where name = '李四';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0//再次查看表中的数据,发现数据更改已经生效
mysql> select * from bank;
+----+--------+---------+
| id | name   | balance |
+----+--------+---------+
|  1 | 张三   | 1000.00 |
|  2 | 李四   | 1000.00 |
+----+--------+---------+
2 rows in set (0.00 sec)//提交事务
mysql> commit;
Query OK, 0 rows affected (0.00 sec)//最后查看表中的数据,发现事务执行成功了。数据修改已经生效
mysql> select * from bank;
+----+--------+---------+
| id | name   | balance |
+----+--------+---------+
|  1 | 张三   | 1000.00 |
|  2 | 李四   | 1000.00 |
+----+--------+---------+
2 rows in set (0.00 sec)
  1. 保存点
//开始事务前查看表中数据
mysql> select * from bank;
+----+--------+---------+
| id | name   | balance |
+----+--------+---------+
|  1 | 张三   | 1000.00 |
|  2 | 李四   | 1000.00 |
+----+--------+---------+
2 rows in set (0.00 sec)//开始一个事务
mysql> begin;
Query OK, 0 rows affected (0.00 sec)//更新数据
mysql> update bank set balance = balance + 100 where name = '张三';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0mysql> update bank set balance = balance - 100 where name = '李四';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0//查看表中数据发现修改生效
mysql> select * from bank;
+----+--------+---------+
| id | name   | balance |
+----+--------+---------+
|  1 | 张三   | 1100.00 |
|  2 | 李四   |  900.00 |
+----+--------+---------+
2 rows in set (0.00 sec)//设置第一个保存点
mysql> savepoint sp1;
Query OK, 0 rows affected (0.00 sec)//再次进行数据更新
mysql> update bank set balance = 0 where name = '张三';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0//查看表中数据发现修改再次生效
mysql> select * from bank;
+----+--------+---------+
| id | name   | balance |
+----+--------+---------+
|  1 | 张三   |    0.00 |
|  2 | 李四   |  900.00 |
+----+--------+---------+
2 rows in set (0.00 sec)//设置第二个保存点
mysql> savepoint sp2;
Query OK, 0 rows affected (0.00 sec)//插入一个记录
mysql> insert into bank values(3,'王五',2000);
Query OK, 1 row affected (0.00 sec)//查看表中数据发现插入成功
mysql> select * from bank;
+----+--------+---------+
| id | name   | balance |
+----+--------+---------+
|  1 | 张三   |    0.00 |
|  2 | 李四   |  900.00 |
|  3 | 王五   | 2000.00 |
+----+--------+---------+
3 rows in set (0.00 sec)//设置第三个保存点
mysql> savepoint sp3;
Query OK, 0 rows affected (0.00 sec)//回滚到第二个保存点
mysql> rollback to sp2;
Query OK, 0 rows affected (0.00 sec)//查看数据发现已经回滚成功
mysql> select * from bank;
+----+--------+---------+
| id | name   | balance |
+----+--------+---------+
|  1 | 张三   |    0.00 |
|  2 | 李四   |  900.00 |
+----+--------+---------+
2 rows in set (0.00 sec)//回滚到第一个保存点
mysql> rollback to sp1;
Query OK, 0 rows affected (0.00 sec)//查看数据发现再次回滚成功
mysql> select * from bank;
+----+--------+---------+
| id | name   | balance |
+----+--------+---------+
|  1 | 张三   | 1100.00 |
|  2 | 李四   |  900.00 |
+----+--------+---------+
2 rows in set (0.00 sec)//回滚并且退出事务
mysql> rollback;
Query OK, 0 rows affected (0.00 sec)//最后查看数据发现事务的操作全部失效
mysql> select * from bank;
+----+--------+---------+
| id | name   | balance |
+----+--------+---------+
|  1 | 张三   | 1000.00 |
|  2 | 李四   | 1000.00 |
+----+--------+---------+
2 rows in set (0.00 sec)

最后我们来介绍一下事务的自动和手动提交。
事务有着自动提交和手动提交这两种模式,我们可以通过指令来查看当前事务是何种模式并且也可以自己设置事务是何种模式。

//查看当前事务是何种模式,ON为自动提交OFF为手动提交
mysql> show variables like 'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit    | ON    |
+---------------+-------+
1 row in set (0.01 sec)//将事务设置为手动提交
mysql> set autocommit = 0;
Query OK, 0 rows affected (0.00 sec)
mysql> set autocommit = off;
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> set autocommit = 1;
Query OK, 0 rows affected (0.00 sec)
mysql> set autocommit = on;
Query OK, 0 rows affected (0.00 sec)mysql> show variables like 'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit    | ON    |
+---------------+-------+
1 row in set (0.00 sec)

那么事务的自动提交和手动提交有什么区别呢?大家要知道一个事务是由一条或者多条语句组成的所以我们平时输入的语句其实都是一个事务但是我们都没有进行过提交啊这就是因为MySQL默认将事务设置自动提交,当我们使用语句进行插入更新删除操作时都会自动生成一个事务并且在执行完成后自动提交发生异常时自动回滚。所以是MySQL来为我们负重前行,我们表面上是没有进行提交和回滚操作的但是其实是MySQL来为我们擦屁股了。
当我们是手动提交模式下我们不需要每次都使用begin或者start transaction来表明自己开启了一个事务,只需要在完成修改更新删除操作后进行commit或rollback即可。因为虽然不是自动提交模式了但是MySQL同样会自动将我们的单个语句生成一个事务。
但是要注意:当我们使用begin或者start transaction后我们必须使用commit或者rollback来完成提交或回滚操作,因为这是我们主动开启一个事务所以和是否是autocommit自动提交模式没有关系。并且一个事务是无法在提交后再回滚的。

四.事务的隔离性和隔离级别

事务的隔离性和隔离级别是整个事务中最难理解也是最重要的部分,事务的四大特性中无论是原子性还是一致性又或者持久性都是很好理解的只有隔离性不好理解,在我们上面的介绍中说隔离性保证了事务之间不会互相影响但是什么样的情况才算是被影响了呢?不同的隔离级别有着它们自己的理解有些隔离级别注重安全性有些注重并发性有些并发现和安全性共同注重所以其中的道道还是很多的。
事务一共有四个隔离级别:READ UNCOMMITTED(读未提交),READ COMMITTED(读已提交),REPEATABLE READ(可重复读),SERIALIZABLE(串行化)。

4.1查看和修改隔离级别

首先我们来看如何查看当前事务的隔离级别以及如何修改事务的隔离级别

  1. 查看隔离级别
    SELECT @@[GLOBAL | SESSION].transaction_isolation;
//查看全局作用域下的隔离级别
mysql> select @@GLOBAL.transaction_isolation;
+--------------------------------+
| @@GLOBAL.transaction_isolation |
+--------------------------------+
| REPEATABLE-READ                |
+--------------------------------+
1 row in set (0.00 sec)//查看会话作用域下的隔离级别
mysql> select @@SESSION.transaction_isolation;
+---------------------------------+
| @@SESSION.transaction_isolation |
+---------------------------------+
| REPEATABLE-READ                 |
+---------------------------------+
1 row in set (0.00 sec)
  1. 修改隔离级别
    SET [GLOBAL|SESSION] TRANSACTION ISOLATION LEVEL level|access_mode;

隔离级别
level:{
READ UNCOMMITTED, //读未提交
| READ COMMITTED, //读已提交
| REPEATABLE READ, //可重复读
| SERIALIZABLE //串行化
}
访问模式
access_mode:{
READ WRITE, //事务可以对数据进行读写
| READ ONLY //事务只可以对数据进行读取不能写入

当我们设置全局作用域的隔离级别为串行化时,后续的所有事务都会生效但是当前事务是不生效的。这个后续是什么意思呢?即当我们关闭MySQL再登入后,其实是因为MySQL每次登入时会话作用域就会将自身的隔离级别设置为全局作用域的隔离级别。

//设置全局作用域的隔离级别为串行化
mysql> set global transaction_isolation = 'SERIALIZABLE';
Query OK, 0 rows affected (0.00 sec)//查看全局作用域的隔离级别
mysql> select @@GLOBAL.transaction_isolation;
+--------------------------------+
| @@GLOBAL.transaction_isolation |
+--------------------------------+
| SERIALIZABLE                   |
+--------------------------------+
1 row in set (0.00 sec)//查看会话作用域的隔离级别
mysql> select @@SESSION.transaction_isolation;
+---------------------------------+
| @@SESSION.transaction_isolation |
+---------------------------------+
| REPEATABLE-READ                 |
+---------------------------------+
1 row in set (0.00 sec)//关闭MySQL
mysql> quit;
Bye//重新登入MySQL
root@ly-VMware-Virtual-Platform:~# mysql
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 9
Server version: 8.0.42-0ubuntu0.24.04.1 (Ubuntu)Copyright (c) 2000, 2025, Oracle and/or its affiliates.Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.//查看全局作用域的隔离级别
mysql> select @@GLOBAL.transaction_isolation;
+---------------------------------+
| @@GLOBAL.transaction_isolation |
+---------------------------------+
| SERIALIZABLE                    |
+---------------------------------+
1 row in set (0.00 sec)//查看会话作用域的隔离级别,发现变为了串行化
mysql> select @@SESSION.transaction_isolation;
+--------------------------------+
| @@SESSION.transaction_isolation |
+--------------------------------+
| SERIALIZABLE                   |
+--------------------------------+
1 row in set (0.00 sec)

而当我们设置会话作用域的隔离级别时它会对这次会话的后续事务生效,当前事务也是不生效的。这个后续又是什么意思呢?就是在我们没有退出MySQL时,因为我们只修改了会话作用域的隔离级别所以在我们重新登入后会话作用域的隔离级别又会变成全局作用域的隔离级别。

//设置会话作用域的隔离级别
mysql> set session transaction_isolation = 'REPEATABLE-READ';
Query OK, 0 rows affected (0.00 sec)//查看全局作用域的隔离级别
mysql> select @@GLOBAL.transaction_isolation;
+--------------------------------+
| @@GLOBAL.transaction_isolation |
+--------------------------------+
| SERIALIZABLE                   |
+--------------------------------+
1 row in set (0.00 sec)//查看会话作用域的隔离级别,发现变成了可重复读
mysql> select @@SESSION.transaction_isolation;
+---------------------------------+
| @@SESSION.transaction_isolation |
+---------------------------------+
| REPEATABLE-READ                 |
+---------------------------------+
1 row in set (0.00 sec)//退出MySQL
mysql> quit
Bye//重新登入MySQL
root@ly-VMware-Virtual-Platform:~# mysql;
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 10
Server version: 8.0.42-0ubuntu0.24.04.1 (Ubuntu)Copyright (c) 2000, 2025, Oracle and/or its affiliates.Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.//再次查看全局作用域的隔离级别
mysql> select @@GLOBAL.transaction_isolation;
+--------------------------------+
| @@GLOBAL.transaction_isolation |
+--------------------------------+
| SERIALIZABLE                   |
+--------------------------------+
1 row in set (0.00 sec)//再次查看会话作用域的隔离级别,发现又重可重复读变成了串行化
mysql> select @@SESSION.transaction_isolation;
+---------------------------------+
| @@SESSION.transaction_isolation |
+---------------------------------+
| SERIALIZABLE                    |
+---------------------------------+
1 row in set (0.00 sec)

所以全局作用域和会话作用域以退出为重新登入为分界线,修改全局作用域的隔离级别就是设置重新登入后的隔离级别,修改会话作用域的隔离级别就是设置重新登入前的隔离级别。

4.2隔离级别的区别

我们现在来介绍不同的隔离级别

  1. READ UNCOMMITTED
    可以读取到尚未提交的数据。
    当多个事务并发执行时会读取到其他事务还未提交的数据。
    性能最好但是安全性最差。
    会造成三个并发性问题:脏读,不可重复读和幻读。
  2. READ COMMITTED
    可以读取到已经提交的数据。
    当多个事务并发执行时事务会被其他事务已经提交的数据影响,前脚查询的数据后脚就不一样了。
    性能较好但是安全性较差。
    会造成两个并发性问题:不可重复读和幻读。
  3. REPEATABLE READ(默认)
    只会读取到事务开启后那一刻的数据,无论其他事务是否对数据进行修改是否提交了都不会影响当前事务的数据。
    当多个事务并发时,当前事务先得到了一个结果集之后另外一个事务插入或者删除了某行后进行提交此时当前事务再得到的结果集可能就会与上一次结果集不相同了。
    性能较差但是安全性较好
    会造成一个并发性问题:幻读
  4. SERIALIZABLE
    对事务增加读写锁,当多个事务并发访问一个数据库时会先进行锁的申请,没有申请到的事务必须等申请到锁的事务执行完成后才能执行。
    性能最差但是安全性最好。
    不会造成任何一个并发性问题。

在大概介绍了这四种隔离级别后我们现在应该已经大概理解四种隔离级别的区别了同时我们也发现了这四种隔离级别其实就是对解决多事务并发性问题的一次次升级,为了解决脏读升级成了读已提交再为了解决不可重复读升级成可重复读再为了彻底解决幻读升级成串行化。所以我们想要彻底理解这四种隔离级别我们需要一个一个的讲述这三个并发性问题。

  1. 脏读
    在介绍读未提交时就已经大致讲述了脏读是如何形成的,其实就是因为读未提交这个隔离级别会读取到事务未提交的数据所以在多个事务并发访问一个数据库时一旦它们都对数据进行修改那么每个事务读取的数据都会发生突变。这种读取到其他事务未提交数据的行为就是脏读。
    在这里插入图片描述
    在这里插入图片描述

  2. 不可重复读
    为了解决脏读我们创建了读已提交,这个隔离级别确实解决了脏读的问题但是迎来了不可重复读的问题即当多个事务并发访问一个数据库时,当前事务将表中的某个记录行修改并且得到正确的结果集后有一个事务同时对这个记录行进行修改并且进行了提交那么当前事务再查询时就会发现和上一次查询结果不同了。
    在这里插入图片描述
    在这里插入图片描述

  3. 幻读
    同样为了解决不可重复读的问题,我们改进到可重复读这个隔离级别。而这个隔离级别还有最后一个可并发性问题即幻读。因为可重复读是让事务开启后每次读取的数据都是开始那一刻的数据但是话虽如此我们发现当当前事务对记录进行更新并且得到正确的结果集后另外一个事务开始并且向表中插入一条记录并提交,此时当前事务再查询结果集发现记录行和之前那次不一样了也就是发现了幻想行。
    这时有人发出疑问了这和不可重复读有什么区别呢?幻读是出现幻想行也就是记录行数不同的了,而不可重复读是记录行的内容发生了改变。我们要知道读已提交不仅又不可重复读的问题也有幻读的问题而可重复读是只有幻读的问题,所以可重复读只会发现记录行不一样了但是非幻想行的内容是不会发生改变的。简单来说不可重复读针对的是已有记录行的内容而幻读则是针对记录行的数量。
    虽然可重复读隔离级别还有幻读的现象但是其实已经很大程度上避免了,因为可重复读使用MVCC和next-key lock这两种方法。所以我们想要重现幻读现象我们只能继续使用读已提交这个隔离级别。一定要注意可重复读是很大程度上避免了幻读而不是根治了幻读不然也不会有串行化了。
    在这里插入图片描述
    在这里插入图片描述

五.MVCC

在了解MVCC之前我们先思考一个问题:为什么要有隔离级别呢?
这就牵涉到我们四大特性中的原子性因为事务是原子性的即使它是由多个语句组合成的一个整体所以它一定会有执行前执行中执行后这三种状态但是还是因为事务是原子的,这也就导致了事务一定是有前有后的。当多个事务并发运行时一定是按着一定的顺序依次开始执行的但是不一定是相同的顺序结束运行因为执行时间不同,早开始的事务可能先结束也有可能后结束。
为了解决事务的前后顺序关系MVCC会为事务自动分配单向增长的事务ID。
那么当多个事务并发时它们各自都会进行CURD操作也就会彼此交织在一起而隔离级别就是控制不同的事务看到它应该看到的看不到它不应该看到的。那么不同的隔离级别也就是看到的东西不同而已。
而MVCC就是来形成不同事务看到的东西,具体来说MVCC实现了一个一个的版本来存储每次修改数据时的原记录,再利用回滚指针将其连接起来成为一个版本链。在仔细解析MVCC的原理之前我们需要先讲述两个东西:记录的四个隐藏字段,undo日志。

5.1记录隐藏字段

在我们创建一个表时MySQL会自动为表另增四个字段分别是:DB_TRX_ID,DB_ROLL_PTR,DB_ROW_ID,FLAG。

  1. DB_TRX_ID
    此字段存储最近修改或者创建此纪录的事务ID。
  2. DB_ROLL_PTR
    此字段是一个指针存储的是当前记录的上一个版本的地址,想要理解这个字段我们需要联系undo日志才行所以我们将其在讲述undo日志时再说。
  3. DB_ROW_ID
    此字段是当我们创建表没有设置主键时生成的隐式主键其值是带有自增属性的记录id。
  4. FLAG
    此字段存储的是记录的是否被删除,当记录被删除时FLAG就会置为FALSE。这是因为在数据库中删除一个记录并不是物理意义上的将其删除而是让改变它的FLAG字段。

5.2undo日志

我们要知道MySQL是一个应用层的服务进程,它是在内存中运行的。所以无论是我们说的索引,事务,隔离性还是日志都是在内存中完成的。也就是说这些都是保存在MySQL开辟的缓冲区中并且来完成各类的判断操作直到在合适的时机下被刷到磁盘中。
undo日志就是MySQL所拥有的多种日志系统中一个比较重要的日志系统,所以它也是存储在MySQL自己的缓冲区中的也就是说undo日志就是MySQL的一段内存缓冲区。
那么undo中到底存储的是什么呢?我们之前说了MVCC会形成一个版本链这个形成的过程就是在undo中完成的。当我们想要对一个记录进行修改时MySQL不会直接进行修改而是先将没有被修改的记录复制一份存储到MySQL也就是在undo中创建一个此纪录的版本再将记录进行修改,这种行为大家可以联想一下C++中的写时拷贝,都是在“写入”时进行额外的操作。在形成一个版本之后对记录进行修改时我们还需要注意DB_ROLL_PTR和DB_TRX_ID这两个字段,由于对记录进行了修改所以DB_TRX_ID需要更改为当前事务的id并且在介绍DB_ROLL_PTR时我们说了它是指向上一个版本地址的指针所以需要将其更改为版本在undo中的地址。
在这里插入图片描述
在这里插入图片描述

undo日志的作用就是存储每个记录的各个版本再通过DB_ROLL_PTR形成一个版本链,但是undo的空间是有限的如果我们无限制的形成各个记录的版本迟早undo会被撑爆这又要如何解决呢?不用担心,当我们的事务进行提交后undo存储的记录的版本链就会被清除因为事务已经执行完毕了就不需要版本链来进行回滚操作了。
所以对于我们使用的update,delete都可以形成版本,那么insert和select呢?insert是插入记录也就是说之前是没有这个记录的要怎么形成版本呢?但是为了能实现回滚作用我们还是会把insert操作形成一个版本存储到undo上去这样我们想要回滚时只需要将insert改为delete即可。所以一般我们说的形成版本链的操作就是指update和delete,insert一般不考虑在其中。而select的作用是读取数据所以它是不会形成版本链的,但是既然现在是多版本管理那么select读取的到底是哪个版本的数据的呢?

5.3快照读和当前读

这就需要提到快照这个概念了,对于版本链中的一个个版本我们也将其叫做快照。同时我们将查询分为快照读和当前读这两者的差别就是快照读读取的是历史版本而当前读读取的则是当前版本。一般的增删查都是当前读以及特殊的select lock in share mode和select for update才是当前读至于其他的查询则是快照读如select * from table。
在有了当前读这个概念后也就很好解释了为什么串行化可以彻底解决并发性问题了,因为当前读是需要申请锁的所以无论是增删查改都是会申请锁的。那么在多个事务并发执行时就会因为锁而让事务的增删查改都是有序的。也就不会发生并发性问题了。
而快照读时因为读取的是之前版本所以不需要申请锁也就是可以有多个事务并发进行快照读这也就提高了效率这就是MVCC的意义之一。

那么MVCC到底是什么呢?MVCC其实就是基于undo产生的版本链形成的一种多版本管理模块,产生之初就是为了解决多事务的并发性问题也就是对事务进行管理。但是只有MVCC不足以解决并发性问题因为我们只构建了版本链但是不同的事务怎么判断哪些版本是应该看见的又是如何看见的呢?这就需要涉及视图了。

六.Read View

6.1Read View的概念

我们现在已经知道快照读读取的历史版本数据这个历史版本到底是版本链中的哪些呢?
其实在事务执行快照读操作时MySQL会同时为其创建一个读视图(Read View),其中记录并且维护着当前系统活跃的事务以及此时的高水位值与低水位值。而我们想要判断当前事务能够看见哪些版本就需要让各个版本中的事务ID与读视图中的数据进行判断,所以读视图的作用就是作为判断的条件来让事务看见应该看见的版本。
在知道了读视图中存储的数据我们也就很好发现它其实就是一个MySQL中的一个类,那么我们就来看看它在MySQL中的源码是怎么样的也方便我们对其进行理解。

class ReadView {// ... 省略
private:
/** The read should not see any transaction with trx id >= this
value. In other words, this is the "high water mark". */
trx_id_t m_low_limit_id; // ⼤于等于此值的是未开启的事务,不可⻅
/** The read should see all trx ids which are strictly
smaller (<) than this value. In other words, this is the
low water mark". */
trx_id_t m_up_limit_id; // ⼩于此值的是已提交事务,可⻅
/** trx id of creating transaction, set to TRX_ID_MAX for free
views. */
trx_id_t m_creator_trx_id; // 创建当前ReadView的事务Id
/** Set of RW transactions that was active when this snapshot
was taken */
ids_t m_ids; // 当前所有活跃事务的集合
/** The view does not need to see the undo logs for transactions
whose transaction number is strictly smaller (<) than this value:
they can be removed in purge if not needed by other views */
// 如果当前ReadView和其他ReadView不需要事务Id⼩于此值的Undo⽇志,可以在purge阶段删除 
trx_id_t m_low_limit_no; 
/** AC-NL-RO transaction view that has been "closed". */
bool m_closed; // 是否关闭标识
// ... 省略
};

我们来理解一下其中比较重要的几个成员变量。

  1. trx_id_t m_low_limit_id
    高水位值,存储的是当前当前系统活跃事务中最小的id。快照的事务id小于高水位值时能被看见。
  2. trx_id_t m_up_limit_id
    低水位值,存储的是系统下次分配的事务的id。快照的事务id大于低水位值能被看见。
  3. trx_id_t m_creator_trx_id
    创建此视图的事务id。快照的事务id等于此变量记录的id时可以被看见。
  4. ids_t m_ids
    当前系统活跃的事务集合。在这个集合中的事务id都不能被看见。

在这里插入图片描述

注意:事务的id是单向增加的所以高水位值是小于低水位值的。
那么我们来总结一下怎么通过视图来判断什么样的事务id能被看见以及不能被看见。
在这里插入图片描述
那么这都是为什么呢?为什么这些区间内事务id的快照可以被看见呢?我们按照判断顺序来一个一个的讲。
5. trx_id_t m_creator_trx_id == trx_id
先判断快照的trx_id是否等于creator_trx_id时如果等于说明这个快照就是改事务创建的当然是可以看见的。
6. trx_id_t m_low_limit_id > trx_id
再判断trx_id是否小于low_limit_id,因为高水位值是当前活跃事务的最小id如果trx_id比它还小就说明这个事务早就提交了当然可以看见。
7. trx_id_t m_up_limit_id <= trx_id
接着判断trx_id是否大于等于up_limit_id,因为低水位值是下次系统分配给事务的id如果trx_id大于等于这个值那就说明这个事务还没有被创建当然无法被看见。
8. ids_t m_ids
最后判断trx_id是否属于m_ids这个集合,在经过了上面三个判断后留下来的事务id都是大于等于low_limit_id并且小于up_limit_id的,而m_ids中存储的又是当前系统中活跃的事务id也就是还没有提交的事务id。那么在这个区间中只要一个事务id不属于m_ids这个集合那么说明在创建这个视图时这个事务已经提交了那也就可以被看见。
在这里插入图片描述
每个版本都需要经过这样的四步判断法从而来决定是否会被创建视图的事务看见。
在这里插入图片描述

6.2RR和RC的实际区别

说了这么多我们应该已经理解了视图的作用了但是大家还记得我们之前说的隔离级别是控制事务应该看到哪些版本不应该看到哪些版本的吗?但是我们发现说视图说到现在根本没提到隔离级别的影响啊那么隔离级别到底是如何实现控制的呢?我们下面用两张图作为引子来让大家更好理解。
在这里插入图片描述
在这里插入图片描述
我们对比着这两个图来看我们发现仅仅是少了一个快照读结果却截然不同,上面的事务B最后快照读时没有查询到更新的数据而下面的事务B则是查询到了更新的数据。这是为什么呢?这是因为上面的事务B在事务A还没有提交的时候就已经进行了快照读此时就创建了一个视图而这个视图里事务A还是系统中活跃的事务当然是看不见的,这也就导致了没有查询到数据的改变,而下面那个事务B则是在事务A提交后再进行的快照读此时的事务A是可见的所以查询到了数据的改变。理论上是这样的,但是我们之前不是说过每次进行快照读的时候都会创建一个读视图吗那么上面的事务B在第二次进行快照读的时候不应该也会创建一个读视图吗?
这就是隔离级别的影响了我们现在对比的是RC(读已提交)和RR(可重复读)的区别。RR和RC的实际区别其实就在于创建视图这件事,RR只会在第一次进行快照读时创建视图而RC则会在每次快照读时创建读视图。
这也就很好理解为什么上面的事务B第二次进行快照读的时候没有查询到更新的数据了因为此时的隔离级别是RR所以视图已经被固定住了,导致之后的快照读只会读取到与第一次快照读时相同的数据。这也就是为什么RR隔离级别被叫做可重复读的原因了即第一次快照读决定了之后快照读的结果!

相关文章:

  • 看板中如何管理技术债务
  • 【Java学习日记38】:C语言 fabs 与 Java abs 绝对值函数
  • Linux相关问题整理
  • Boring Blog
  • Vue 数据代理机制对属性名的要求
  • 前端将多个PDF链接的内容拼接成一个后返回出一个链接进行打开
  • 脑机新手指南(九):高性能脑文本通信:手写方式实现(上)
  • JS之Dom模型和Bom模型
  • Java SE - 类和对象入门指南
  • SQL29 验证刷题效果,输出题目真实通过率
  • Future与CompletableFuture:异步编程对比
  • Linux 文件内容的查询与统计
  • 万字深度解析注意力机制全景:掌握Transformer核心驱动力​
  • 【基于阿里云上Ubantu系统部署配置docker】
  • Haclon例程1-<剃须刀片检测程序详解>
  • < 买了个麻烦 (二) 618 京东云--轻量服务器 > “可以为您申请全额退订呢。“ 工单记录:可以“全额退款“
  • EtherCAT转CANopen网关与伺服器在汇川组态软件上的配置步骤
  • 免下载苹果 IPA 文件重签名工具:快速更换应用名称和 BID的教程
  • Python的LibreOffice命令行详解:自动化文档处理的终极指南
  • AUTOSAR图解==>AUTOSAR_TR_ModelingShowCases
  • 设备免费做网站推广/什么时候网络推广
  • 淘宝联盟建网站/长沙百度快速排名
  • phpcms内容管理系统/西安seo霸屏
  • 西双版纳傣族自治州房价/河源市企业网站seo价格
  • 做网站标题头像/石家庄最新消息
  • 宇说建筑网站/seo应该怎么做