MySQL 这个东西注定是可能会被多个用户/客户端来同时访问的,这是肯定的,MySQL 中存放的都是数据,数据可能有一个上层线程在用,也有可能另一个线程也要用...数据是被所有人共享的,所以就注定了 MySQL 这样的服务在一个时刻有多个请求过来,然后我们进行数据的 CURD 操作!
MySQL 内部是采用多线程的方式来是西安数据的存储相关的功能的,所以就注定会有对数据并发访问的场景,所以为了更好的去解决这样的问题,我们可以理解为:关系型数据库为我们提供了事务这样的概念!
像 Redis 这种K-V型的内存级数据库也是支持事务的!
MySQL 比其他的数据库支持的事务设计得更加巧妙!
接下来,我们先谈论场景,后面引出事物的概念!
CURD/增删查改不加控制,会有什么问题?
这和我们日常生活当中转账是一样的,我想给张三的中银银行卡转账,做法就是将我的账号的200块钱,减去100块钱,然后给张三的银行卡加上100,这就完成了转账,但是可能在减去100过候,这个操作出现异常,银行卡没加上来。这个的整个操作就会出现中间过程,这种情况下,我们允许一场产生,但是一旦产生问题了,操作没有完成,就需要让减掉的100,加回来,就好像什么都没错,等待下一次在合适的时候进行转账,这就相当于转账的时候,没有中间过程,而是在转的时候出现了异常,直接进行回滚!不回滚就有问题!!!必须要回滚 --- 保证我们的状态和初始状态一样!
CURD满足什么属性,能解决上述问题?
-
买票的过程是原子的吧
-
买票后相应数据不能被影响吧(双方之间要隔离,我买我的,你买你的)
-
买完票应该要永久有效吧(购买了,你直接删掉了???)
-
买前,和买后都要确定的状态吧
什么是事务?
在谈事务之前,我们要明白一点:
实际上,我们在编写 SQL 的时候,不一定一条 SQL 语句就能解决所有问题,我们有时候要做对应的操作,需要一批 SQL 来共同组合,才有意义!
举个例子,我今天要给大家进行转账,一定是 update 将我账号的钱减去100,然后一定是 update 将你账号上的钱 add 上100 --- 这注定是两条 SQL,在我么你任何一个场景下,单独抓出来,只能在计数层面上告诉我们在表中的特定位置进行+/-100,单独的一条 SQL 语句是没有意义的,就是两条语句,但是将两条 SQL 合在一起,就是看成一个整体,那么这个整体就是转账逻辑!所以我们将这两条 SQL,共同构成了 DML 数据管理语言!这就是事务。
所以事务就是要站在 MySQL 的上层,去看待 SQL 语句!
说大白话就是 -- 事务就是由一条或者多条 SQL 语句构成的一个 SQL 的集合体,要完成某种业务!!!
事务就是一组DML语句组成,这些语句在逻辑上存在相关性(减100,自然是要加100,这就是相关性),这一组DML语句要么全部成功,要么全部失败,是一个整体。是 MySQL提供一种机制,保证我们达到这样的效果。事务还规定不同的客户端看到的数据是不相同的。
事务就是要做的或所做的事情,主要用于处理操作量大、复杂度高的数据。假设一种场景:你毕业了,学校的教务系统后台MySQL中,不再需要你的数据,要删除你的所有信息(一般不会!),那么要删除你的基本信息(姓名、电话、籍贯等)的同时,也删除和你有关的其他信息,比如:你的各科成绩,你在校表现,甚至你在论坛发过的文章等。这样,就需要多条MySQL语句构成,那么所有这些操作合起来,就构成了一个事务。
正如我们上面所说,一个MySQL数据库,可不止你一个事务运行,同一时刻,甚至有大量的请求被包装成事务,在向MySQL服务器发起事务处理请求。而每条事务至少一条SQL,最多很多SQL这样如果大家都访问同样的表数据,在不加保护的情况下,就绝对会出现问题。甚至,因为事务由多条SQL构成,那么,也会存在执行到一半出错或者不想再执行的情况,那么已经执行的怎么办呢?
所以,一个完整的事务,绝对不是简单的sql集合,还需要满足如下四个属性:(原子-一致-隔离-持久)
-
原子性:一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。
-
一致性:在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设规则,这包含资料的精确度、串联性以及后续数据库可以自发性地完成预定的工作。(结果是可预期的!转账成功与失败的结果体现!)(这是没有 MySQL 的技术设计的,但是只要做到了原子性,隔离性,持久性,我们就可以在技术上做到一致性,三因一果)
-
隔离性:数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括读未提交(Read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(Serializable)。
-
持久性:事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。
上面四个属性,可以简称为ACID。
原子性(Atomicity,或称不可分割性)
一致性(Consistency)
隔离性(Isolation,又称独立性)
持久性(Durability)。
事务在 MySQL 里肯定是存在的,存在的形式具体是什么样的呢?我们可以这么理解:
MySQL 要同时为我们不同的客户端处理各种各样的事务请求,这就直接就定了 MySQL 在运行期间,一定会在自身内部存在大量的事务,MySQL 就需要把所有的事务管理起来 --- 先描述,再组织!所以,在我们看来,MySQL 所谓的事务,在我看来就是来了一批对应的 SQL,我把这批 SQL 打包成一个事务对象,然后将这个事务对象最后放入到事务的执行列表中,然后让 MySQL 去帮我们执行!
所以事务本质上就是 MySQL 内部的一个对象!--- 抽象概念具体化,我们就可以想象了!
为什么会出现事务
事务被MySQL编写者设计出来,本质是为了当应用程序访问数据库的时候,事务能够简化我们上层的编程模型,不需要我们去考虑各种各样的潜在错误和并发问题。可以想一下当我们使用事务时,要么提交,要么回滚,我们不会去考虑网络异常了,服务器宕机了,同时更改一个数据怎么办对吧?因此事务本质上是为了应用层服务的。而不是伴随着数据库系统天生就有的。
事物存在的意义是为了让上层更好的去使用 MySQL!
就想去柜台存钱,只需要告诉工作人员我们的需求,具体的过程交给工作人员,不需要问工作人员出错了怎么办,只给个需求,中间操作失败会告诉你,成功会告诉你!
备注:我们后面把MySQL中表中的一行信息,称为一行记录。未来就是对表中的记录做增删查改操作!
事务的版本支持
在MySQL中只有使用了Innodb数据库引擎的数据库或表才支持事务,MyISAM不支持。
查看数据库引擎
mysql> show engines; -- 表格显示
mysql> show engines \G -- 行显示
*************************** 1. row ***************************Engine: InnoDB -- 引擎名称Support: DEFAULT -- 默认引擎Comment: Supports transactions, row-level locking, and foreign keys -- 描述
Transactions: YES -- 支持事务XA: YESSavepoints: YES -- 支持事务保存点
*************************** 2. row ***************************Engine: MRG_MYISAMSupport: YESComment: Collection of identical MyISAM tables
Transactions: NOXA: NOSavepoints: NO
*************************** 3. row ***************************Engine: MEMORY --内存引擎Support: YESComment: Hash based, stored in memory, useful for temporary tables
Transactions: NOXA: NOSavepoints: NO
*************************** 4. row ***************************Engine: BLACKHOLESupport: YESComment: /dev/null storage engine (anything you write to it disappears)
Transactions: NOXA: NOSavepoints: NO
*************************** 5. row ***************************Engine: MyISAMSupport: YESComment: MyISAM storage engine
Transactions: NO -- MyISAM不支持事务XA: NOSavepoints: NO
*************************** 6. row ***************************Engine: CSVSupport: YESComment: CSV storage engine
Transactions: NOXA: NOSavepoints: NO
*************************** 7. row ***************************Engine: ARCHIVESupport: YESComment: Archive storage engine
Transactions: NOXA: NOSavepoints: NO
*************************** 8. row ***************************Engine: PERFORMANCE_SCHEMASupport: YESComment: Performance Schema
Transactions: NOXA: NOSavepoints: NO
*************************** 9. row ***************************Engine: FEDERATEDSupport: NOComment: Federated MySQL storage engine
Transactions: NULLXA: NULLSavepoints: NULL
9 rows in set (0.00 sec)
事务提交方式
事务的提交方式常见的有两种:
-
自动提交
-
手动提交
查看事务提交方式
mysql> show variables like 'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit | ON |
+---------------+-------+
1 row in set (0.41 sec)
是 “ON” 表示的是自动提交!
用 SET 来改变 MySQL 的自动提交模式
mysql> SET AUTOCOMMIT=0; #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> SET AUTOCOMMIT=1; #SET AUTOCOMMIT=1 开启自动提交
Query OK, 0 rows affected (0.00 sec)mysql> show variables like 'autocommit';
+---------------+-------+
| Variable_name | value |
+---------------+-------+
| autocommit | ON |
+---------------+-------+
1 row in set (0.01 sec)
事务常见操作方式
简单银行用户表
提前准备
我的 Ubuntu 云服务器,默认开启3306 mysqld服务
root@instance-hojuqq09:~# netstat -nltp | grep mysql*
tcp6 0 0 :::33060 :::* LISTEN 274596/mysqld
tcp6 0 0 :::3306 :::* LISTEN 274596/mysqld
我们一直使用的是客户端,也就是这个:
root@instance-hojuqq09:~# which mysql
/usr/bin/mysql
root@instance-hojuqq09:~# ls /usr/bin/mysql
/usr/bin/mysql
root@instance-hojuqq09:~# which mysqld
/usr/sbin/mysqld
我们一直使用的是命令行式的客户端,当然还有其他的图形化界面版的 MySQL 客户端,其他语言版的(可以使用 C/C++ 来连接 MySQL)...而且 MySQL 也是一套网络服务,所以我们可以使用远端连接的方式,来连接 MySQL 的,不一定要本地连接!
使用win cmd远程访问Ubuntu 22.04云服务器,mysqld服务(需要win上也安装了MySQL,这里看到结果即可)
注意,使用本地mysql客户端,可能看不到链接效果,本地可能使用域间套接字,查不到链接
有时候连不上 --- MySQL 的服务,一般不要暴露在公网上,暴露在公网上了,别人是可以连接的,但是可能不允许登入!MySQL 内部有自己的账号管理体系,不仅仅是用户名和密码就可以登上的,还是需要设置允许哪些用户在哪里登入,这是后面的 MySQL 的用户管理话题!需要在 MySQL 的用户表中进行配置,让某一个用户,比如说 root ,允许从远端的Windows的机器登入!
## 使用win cmd远程访问Centos 7云服务器,mysqld服务(需要win上也安装了MySQL,这里看到结果即可)
## 注意,使用本地mysql客户端,可能看不到链接效果,本地可能使用域间套接字,查不到链接
C:\Users\whb>mysql -uroot -p -h42.192.83.143
Enter password: **********
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 3484
Server version: 5.7.33 MySQL Community Server (GPL)Copyright (c) 2000, 2019, Oracle and/or its affiliates. All rights reserved.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.## 使用netstat查看链接情况,可知:mysql本质是一个客户端进程
[whb@VM-0-3-centos ~]$ sudo netstat -ntp
Active Internet connections (w/o servers)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp6 0 0 172.17.0.3:3306 113.132.141.236:19354 ESTABLISHED 30415/mysqld
所以我们要清楚,一个 MySQL 的服务端,是有可能会被多个客户端使用的!!!
为了便于演示,我们将mysql的默认隔离级别设置成读未提交。(具体操作我们后面专门会讲,现在已使用为主)
设置全局事务隔离级别:
mysql> set global transaction isolation level READ UNCOMMITTED;
Query OK, 0 rows affected (0.00 sec)mysql> select @@transaction_isolation;
+-------------------------+
| @@transaction_isolation |
+-------------------------+
| REPEATABLE-READ |
+-------------------------+
1 row in set (0.00 sec)mysql> quit
Bye
需要重启终端,进行查看
mysql> select @@transaction_isolation;
+-------------------------+
| @@transaction_isolation |
+-------------------------+
| READ-UNCOMMITTED |
+-------------------------+
1 row in set (0.00 sec)
我们将隔离级别设置为最低,在两个客户端之间就可以立马见效。是为了达到实验的目的!让我们能够清楚的看到双方事务在交叉时,带来的问题!
我们当前让客户端连接多个,我们可以通过如下方式看看:
mysql> show processlist;
+----+-----------------+-----------+---------+---------+--------+------------------------+------------------+
| Id | User | Host | db | Command | Time | State | Info |
+----+-----------------+-----------+---------+---------+--------+------------------------+------------------+
| 5 | event_scheduler | localhost | NULL | Daemon | 956019 | Waiting on empty queue | NULL |
| 41 | root | localhost | test_db | Query | 0 | init | show processlist |
| 42 | root | localhost | test_db | Sleep | 194 | | NULL |
+----+-----------------+-----------+---------+---------+--------+------------------------+------------------+
3 rows in set, 1 warning (0.00 sec)
有两个root用户都在test_db数据库下,其中一个用户的命令状态是sleep!
创建测试表 --- 员工工资表
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;
正常演示-证明事务的开始与回滚
我们先来查看当前事物的提交方式:默认是 ON 的!
我们开启事务的方式有两种:
- start transaction;
- begin;
一旦启动了事务,从开始到后面,所有的 SQL 都是一个事务的内部!
为了能看出交叉的效果,我们在另一个机器上也是begin一个事务:
我们以一个为主,我们在开始的时候设置一个保存点;
接下来我们就尝试向一个表中插入数据;
然后我们再设置一个保存点 --- 就像我们出去玩的时候,迷路了,会在走到的位置标记上,防止下次再走回来,走回来也就意识到了 --- 方便未来我们定向回滚!
接下来,我们再插入一条数据;
然后我们再设置一个保存点 --- 就可以看出,每做一次操作,我们都会设置保存点!
那么以上的所有的 SQL,都属于同一个事务!也就是上面的所有 SQL 最终都会被打包成一个事务,让我们的 MySQL 原子的,持久的,隔离的,一致的将我们插入操作做好!
可是今天事务正在运行中,突然后悔了,不想插入李四了,我们就可以进行定向回滚 --- rollback to save2.
我们可以进行 commit 进行事务的提交 --- 一旦事务提交,rollback 就没有用了!数据就被持久化保存了!
mysql> show variables like 'autocommit'; -- 查看事务是否自动提交。我们故意设置成自动提交,看看该选项是否影响begin
+---------------+-------+
| Variable_name | value |
+---------------+-------+
| autocommit | ON |
+---------------+-------+
1 row in set (0.00 sec)mysql> start transaction; -- 开始一个事务begin也可以,推荐begin
Query OK, 0 rows affected (0.00 sec)mysql> savepoint save1; -- 创建一个保存点save1
Query OK, 0 rows affected (0.00 sec)mysql> insert into account values (1, '张三', 100); -- 插入一条记录
Query OK, 1 row affected (0.05 sec)mysql> savepoint save2; -- 创建一个保存点save2
Query OK, 0 rows affected (0.01 sec)mysql> insert into account values (2, '李四', 10000); -- 在插入一条记录
Query OK, 1 row affected (0.00 sec)mysql> select * from account; -- 两条记录都在了
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 张三 | 100.00 |
| 2 | 李四 | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)mysql> rollback to save2; -- 回滚到保存点save2
Query OK, 0 rows affected (0.03 sec)mysql> select * from account; -- 一条记录没有了
+----+--------+---------+
| id | name | blance |
+----+--------+---------+
| 1 | 张三 | 100.00 |
+----+--------+---------+
1 row in set (0.00 sec)mysql> rollback; -- 直接rollback,回滚在最开始
Query OK, 0 rows affected (0.00 sec)mysql> select * from account; -- 所有刚刚的记录没有了
Empty set (0.00 sec)mysql> commit; -- 取消/提交事务 --- 数据持久化保存了,没有办法 rollback 了
Query OK, 0 rows affected (0.00 sec)
以上就是事务的启动-提交-回滚操作了!
非正常演示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 |
+----+--------+---------+
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 |
+----+--------+---------+
1 row in set (0.00 sec)
其表现和自动提交是没有任何关系的!自动提交影响的是什么,我们后面会说!
所以自动提交不影响我们命令行的begin --- commit,我们在命令行当中输入begin --- commit 的时候,代表我们启动事务,对应的就是相当于手动开启事务,只要是手动开启的事务,就必须要手动 commit!也就是和是否自动没有任何关系!
非正常演示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> Aborted --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)
对于InnoDB每一条SQL语言都默认封装成事务,自动提交。(select有特殊情况,因为MySQL有MVCC)
结论:
只要输入begin或者start transaction,事务便必须要通过commit提交,才会持久化,与是否设置set autocommit无关。
事务可以手动回滚,同时,当操作异常,MySQL会自动回滚
对于InnoDB每一条SQL语言都默认封装成事务,自动提交。(select有特殊情况,因为MySQL有MVCC)
从上面的例子,我们能看到事务本身的原子性(回滚),持久性(commit)
那么隔离性?一致性?
事务操作注意事项
如果没有设置保存点,也可以回滚,只能回滚到事务的开始。直接使用rollback(前提是事务还没有提交)
如果一个事务被提交了(commit),则不可以回退(rollback)
可以选择回退到哪个保存点
InnoDB支持事务,MyISAM不支持事务
开始事务可以使start transaction或者begin
事务隔离级别(重点)
如何理解隔离性?
-
我们清楚:MySQL服务可能会同时被多个客户端进程(线程)访问,访问的方式以事务方式进行(上面的测试)
-
一个事务可能由多条SQL构成,也就意味着,任何一个事务,都有执行前、执行中、执行后的阶段。而所谓的原子性,其实就是让用户层,要么看到执行前,要么看到执行后。执行中出现问题,可以随时回滚。所以单个事务,对用户表现出来的特性,就是原子性。
-
但,毕竟所有事务都要有个执行过程,那么在多个事务各自执行多个SQL的时候,就还是有可能会出现互相影响的情况。比如:多个事务同时访问同一张表,甚至同一行数据。
-
就如同你妈妈给你说:你要么别学,要学就学到最好。至于你怎么学,中间有什么困难,你妈妈不关心。那么你的学习,对你妈妈来讲,就是原子的。那么你学习过程中,很容易受别人干扰,此时,就需要将你的学习隔离开,保证你的学习环境是健康的。
-
数据库中,为了保证事务执行过程中尽量不受干扰,就有了一个重要特征:隔离性
-
数据库中,允许事务受不同程度的干扰,就有了一种重要特征:隔离级别
我对一个数据表中的第二行记录做 update ,他要做 select ,这就有一个问题:我要进行 update 进行更新,他要查找,那么是我先跑,还是他先跑?如果是我先跑,那么他查出来的数据一定是最新的,反之,就是老数据。
update 先跑,这是我们一贯的正常认知,但是这个观点是有问题的!这是时间线的问题的!取决于谁先来的!因为事务要保证原子性,不仅仅是在回滚上体现,原子性也要体现在时间范围内,谁先来就是谁先操作!虽然事务有执行前/中/后,而执行中我们彼此交叉,但是事务是一个原子整体!谁先来就应该是谁先执行,可是先到来的不一定先/后退出,可能在你执行期间,先到来的事务已经将数据更新了,这次他的更新,在你的 select 中就是不应该看到的!!!因为要保证隔离性!!!
隔离性确保了即使事务并发执行,每个事务也都像是在独立的环境中运行。
不同的隔离级别提供了不同级别的保护!要隔离到什么层度,因此我们就有了隔离级别的概念!
隔离级别(为了满足不同的应用场景)
读未提交【Read Uncommitted】:在该隔离级别,所有的事务都可以看到其他事务没有提交的执行结果。(实际生产中不可能使用这种隔离级别的),但是相当于没有任何隔离性,也会有很多并发问题,如脏读、幻读、不可重复读等。我们上面为了做实验方便,用的就是这个隔离性。
MySQL 事务隔离其实是依靠锁来实现的,加锁自然会带来性能的损失。而读未提交隔离级别是不加锁的,所以它的性能是最好的,没有加锁、解锁带来的性能开销。但有利就有弊,这基本上就相当于裸奔啊,所以它连脏读的问题都没办法解决。
任何事务对数据的修改都会第一时间暴露给其他事务,即使事务还没有提交。
下面来做个简单实验验证一下,首先设置全局隔离级别为读未提交。
set global transaction isolation level read uncommitted;
设置完成后,只对之后新起的 session 才起作用,对已经启动 session 无效。如果用 shell 客户端那就要重新连接 MySQL,如果用 Navicat 那就要创建新的查询窗口。
启动两个事务,分别为事务A和事务B,在事务A中使用 update 语句,修改 age 的值为10,初始是1 ,在执行完 update 语句之后,在事务B中查询 user 表,会看到 age 的值已经是 10 了,这时候事务A还没有提交,而此时事务B有可能拿着已经修改过的 age=10 去进行其他操作了。在事务B进行操作的过程中,很有可能事务A由于某些原因,进行了事务回滚操作,那其实事务B得到的就是脏数据了,拿着脏数据去进行其他的计算,那结果肯定也是有问题的。
顺着时间轴往表示两事务中操作的执行顺序,重点看图中 age 字段的值。
读未提交,其实就是可以读到其他事务未提交的数据,但没有办法保证你读到的数据最终一定是提交后的数据,如果中间发生回滚,那就会出现脏数据问题,读未提交没办法解决脏数据问题。更别提可重复读和幻读了,想都不要想。
读提交【Read Committed】:该隔离级别是大多数数据库的默认的隔离级别(不是MySQL默认的)。它满足了隔离的简单定义:一个事务只能看到其他的已经提交的事务所做的改变。这种隔离级别会引起不可重复读,即一个事务执行时,如果多次select,可能得到不同的结果。
既然读未提交没办法解决脏数据问题,那么就有了读提交。读提交就是一个事务只能读到其他事务已经提交过的数据,也就是其他事务调用 commit 命令之后的数据。那脏数据问题迎刃而解了。
读提交事务隔离级别是大多数流行数据库的默认事务隔离界别,比如 Oracle,但是不是 MySQL 的默认隔离界别。
我们继续来做一下验证,首先把事务隔离级别改为读提交级别。
set global transaction isolation level read committed;
之后需要重新打开新的 session 窗口,也就是新的 shell 窗口才可以。
同样开启事务A和事务B两个事务,在事务A中使用 update 语句将 id=1 的记录行 age 字段改为 10。此时,在事务B中使用 select 语句进行查询,我们发现在事务A提交之前,事务B中查询到的记录 age 一直是1,直到事务A提交,此时在事务B中 select 查询,发现 age 的值已经是 10 了。
这就出现了一个问题,在同一事务中(本例中的事务B),事务的不同时刻同样的查询条件,查询出来的记录内容是不一样的,事务A的提交影响了事务B的查询结果,这就是不可重复读,也就是读提交隔离级别。
每个 select 语句都有自己的一份快照,而不是一个事务一份,所以在不同的时刻,查询出来的数据可能是不一致的。
读提交解决了脏读的问题,但是无法做到可重复读,也没办法解决幻读。
可重复读【Repeatable Read】:这是MySQL默认的隔离级别,它确保同一个事务,在执行中,多次读取操作数据时,会看到同样的数据行。但是会有幻读问题。(双方都不清楚情况,是一种比较好的隔离级别)(MySQL 默认的隔离级别!)
可重复是对比不可重复而言的,上面说不可重复读是指同一事物不同时刻读到的数据值可能不一致。而可重复读是指,事务不会读到其他事务对已有数据的修改,及时其他事务已提交,也就是说,事务开始时读到的已有数据是什么,在事务提交前的任意时刻,这些数据的值都是一样的。但是,对于其他事务新插入的数据是可以读到的,这也就引发了幻读问题。
同样的,需改全局隔离级别为可重复读级别。
set global transaction isolation level repeatable read;
在这个隔离级别下,启动两个事务,两个事务同时开启。
首先看一下可重复读的效果,事务A启动后修改了数据,并且在事务B之前提交,事务B在事务开始和事务A提交之后两个时间节点都读取的数据相同,已经可以看出可重复读的效果。
可重复读做到了,这只是针对已有行的更改操作有效,但是对于新插入的行记录,就没这么幸运了,幻读就这么产生了。我们看一下这个过程:
事务A开始后,执行 update 操作,将 age = 1 的记录的 name 改为“风筝2号”;
事务B开始后,在事务执行完 update 后,执行 insert 操作,插入记录 age =1,name = 古时的风筝,这和事务A修改的那条记录值相同,然后提交。
事务B提交后,事务A中执行 select,查询 age=1 的数据,这时,会发现多了一行,并且发现还有一条 name = 古时的风筝,age = 1 的记录,这其实就是事务B刚刚插入的,这就是幻读。
要说明的是,当你在 MySQL 中测试幻读的时候,并不会出现上图的结果,幻读并没有发生,MySQL 的可重复读隔离级别其实解决了幻读问题!
串行化【Serializable】:这是事务的最高隔离级别,它通过强制事务排序,使之不可能相互冲突,从而解决了幻读的问题。它在每个读的数据行上加上共享锁,但是可能会导致超时和锁竞争(这种隔离级别太极端,实际生产基本不使用)(对听一条记录的操作肯定就是需要进行串行化了!)
隔离级别如何实现:隔离,基本都是通过锁实现的,不同的隔离级别,锁的使用是不同的。常见有,表锁、行锁、读锁、写锁、间隙锁(GAP),Next-Key锁(GAP+行锁)等。不过,我们目前现有这个认识就行,先关注上层使用。
查看与设置隔离性
下面的第二个和第三个是一样的!global 和 session 的差别是:
global 是 session 的默认配置,当我们登入 MySQL 的时候,默认 MySQL 会读取配置好的隔离级别,用来初始化本次登入的会话隔离级别,这就有点像 global 设置好了,然后我们就相当于一个全局的配置,我们的 session 的隔离级别就默认是将 global 的隔离级别拷贝过来,当然,我们可以更改我们当前的会话级别!
当然了,更改 session 的话,只会更改本次会话!更改全局的,会影响后续的所有客户端的登入!
-- 查看
mysql> SELECT @@global.transaction_isolation; --查看全局隔级别
+-----------------------+
| @@global.tx_isolation |
+-----------------------+
| REPEATABLE-READ |
+-----------------------+
1 row in set, 1 warning (0.00 sec)mysql> SELECT @@session.transaction_isolation; --查看会话(当前)全局隔级别
+------------------------+
| @@session.tx_isolation |
+------------------------+
| REPEATABLE-READ |
+------------------------+
1 row in set, 1 warning (0.00 sec)mysql> SELECT @@transaction_isolation; --默认同上
+-----------------+
| @@tx_isolation |
+-----------------+
| REPEATABLE-READ |
+-----------------+
1 row in set, 1 warning (0.00 sec)--设置
-- 设置当前会话 or 全局隔离级别语法
SET [SESSION | GLOBAL] TRANSACTION ISOLATION LEVEL {READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE}--设置当前会话隔离性,另起一个会话,看不到,只影响当前会话
mysql> set session transaction isolation level serializable; -- 串行化
Query OK, 0 rows affected (0.00 sec)mysql> SELECT @@global.transaction_isolation; --全局隔离性还是RR
+-----------------------+
| @@global.tx_isolation |
+-----------------------+
| REPEATABLE-READ |
+-----------------------+
1 row in set, 1 warning (0.00 sec)mysql> SELECT @@session.transaction_isolation; --会话隔离性成为串行化
+------------------------+
| @@session.tx_isolation |
+------------------------+
| SERIALIZABLE |
+------------------------+
1 row in set, 1 warning (0.00 sec)mysql> SELECT @@transaction_isolation; --同上
+----------------+
| @@tx_isolation |
+----------------+
| SERIALIZABLE |
+----------------+
1 row in set, 1 warning (0.00 sec)--设置全局隔离性,另起一个会话,会被影响
mysql> set global transaction isolation level READ UNCOMMITTED;
Query OK, 0 rows affected (0.00 sec)mysql> SELECT @@global.transaction_isolation;
+-----------------------+
| @@global.tx_isolation |
+-----------------------+
| READ-UNCOMMITTED |
+-----------------------+
1 row in set, 1 warning (0.00 sec)mysql> SELECT @@session.transaction_isolation;
+------------------------+
| @@session.tx_isolation |
+------------------------+
| READ-UNCOMMITTED |
+------------------------+
1 row in set, 1 warning (0.00 sec)mysql> SELECT @@transaction_isolation;
+------------------+
| @@tx_isolation |
+------------------+
| READ-UNCOMMITTED |
+------------------+
1 row in set, 1 warning (0.00 sec)-- 注意,如果没有现象,关闭mysql客户端,重新连接。
读未提交【Read Uncommitted】
--几乎没有加锁,虽然效率高,但是问题太多,严重不建议采用
--终端A
-- 设置隔离级别为 读未提交
mysql> set global transaction isolation level read uncommitted;
Query OK, 0 rows affected (0.00 sec)--重启客户端mysql> select @@tx_isolation;
+------------------+
| @@tx_isolation |
+------------------+
| READ-UNCOMMITTED |
+------------------+
1 row in set, 1 warning (0.00 sec)mysql> select * from account;
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 张三 | 100.00 |
| 2 | 李四 | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)mysql> begin; --开启事务
Query OK, 0 rows affected (0.00 sec)mysql> update account set blance=123.0 where id=1; --更新指定行
Query OK, 1 row affected (0.05 sec)
Rows matched: 1 Changed: 1 Warnings: 0--没有commit哦!!--终端B
mysql> begin;
mysql> select * from account;
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 张三 | 123.00 | <-- 脏读!
| 2 | 李四 | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)--一个事务在执行中,读到另一个执行中事务的更新(或其他操作)但是未commit的数据,这种现象叫做脏读(dirty read)
--读到终端A更新但是未commit的数据[insert, delete同样!]
读提交【Read Committed】
不可重复读就是在读提交的场景!
--终端A
mysql> set global transaction isolation level read committed;
Query OK, 0 rows affected (0.00 sec)
--重启客户端
mysql> select * from account; --查看当前数据
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 张三 | 123.00 |
| 2 | 李四 | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)mysql> begin; --手动开启事务,同步的开始终端B事务
Query OK, 0 rows affected (0.00 sec)mysql> update account set blance=321.0 where id=1; --更新张三数据
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0--切换终端到终端B,查看数据。mysql> commit; --commit提交!
Query OK, 0 rows affected (0.01 sec)--切换终端到终端B,再次查看数据。--终端B
mysql> begin; --手动开启事务,和终端A一前一后
Query OK, 0 rows affected (0.00 sec)mysql> select * from account; --终端A commit之前,查看不到更新
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 张三 | 123.00 |
| 2 | 李四 | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)--终端A commit之后,看到了!
--but,此时还在当前事务中,并未commit,那么就造成了,同一个事务内,同样的读取,在不同的时间段(依旧还在事务操作中!),读取到了不同的值,这种现象叫做不可重复读(non reapeatable read)!!(这个是问题吗??)
mysql> select * from account;
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 张三 | 321.00 |
| 2 | 李四 | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)
导致 Tom 的工资在3000到4000中出现了一次,也在4000到5000中出现了一次!
不可重复读(Non-repeatable Read)是数据库事务并发控制中的一个问题,它发生在一个事务在读取某个数据后,另一个并发事务修改了该数据并提交,导致第一个事务再次读取同一数据时得到不同的结果。这种情况破坏了事务的隔离性,因为事务的两次读取操作没有得到一致的数据视图。
在图中的例子中,小张的事务在“读已提交”(Read Committed)隔离级别下执行,这意味着它只能看到其他事务已经提交的更改。当小王的事务更新了Tom的薪资并提交后,小张的事务在之后的查询中看到了Tom薪资的更新,这就是不可重复读的一个实例。为了解决这个问题,数据库通常提供更高级别的隔离,如“可重复读”(Repeatable Read),它确保在一个事务中多次读取同一数据时,结果是一致的,即使其他事务对数据进行了修改。
可重复读【Repeatable Read】
--终端A
mysql> set global transaction isolation level repeatable read; --设置全局隔离级别RR
Query OK, 0 rows affected (0.01 sec)--关闭终端重启mysql> select @@transaction_isolation;
+-----------------+
| @@tx_isolation |
+-----------------+
| REPEATABLE-READ |
+-----------------+
1 row in set, 1 warning (0.00 sec)mysql> select * from account; --查看当前数据
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 张三 | 321.00 |
| 2 | 李四 | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)mysql> begin; --开启事务,同步的,终端B也开始事务
Query OK, 0 rows affected (0.00 sec)mysql> update account set blance=4321.0 where id=1; --更新数据
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0--切换到终端B,查看另一个事务是否能看到mysql> commit; --提交事务--切换终端到终端B,查看数据。--终端B
mysql> begin;
Query OK, 0 rows affected (0.00 sec)mysql> select * from account; --终端A中事务 commit之前,查看当前表中数据,数据未更新
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 张三 | 321.00 |
| 2 | 李四 | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)mysql> select * from account; --终端A中事务 commit 之后,查看当前表中数据,数据未更新
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 张三 | 321.00 |
| 2 | 李四 | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)--可以看到,在终端B中,事务无论什么时候进行查找,看到的结果都是一致的,这叫做可重复读!mysql> commit; --结束事务
Query OK, 0 rows affected (0.00 sec)mysql> select * from account; --再次查看,看到最新的更新数据
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 张三 | 4321.00 |
| 2 | 李四 | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)--如果将上面的终端A中的update操作,改成insert操作,会有什么问题??
--终端A
mysql> select * from account;
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 张三 | 321.00 |
| 2 | 李四 | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)mysql> begin; --开启事务,终端B同步开启
Query OK, 0 rows affected (0.00 sec)mysql> insert into account (id,name,blance) values(3, '王五', 5432.0);
Query OK, 1 row affected (0.00 sec)--切换到终端B,查看另一个事务是否能看到mysql> commit; --提交事务
Query OK, 0 rows affected (0.00 sec)--切换终端到终端B,查看数据。mysql> select * from account;
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 张三 | 4321.00 |
| 2 | 李四 | 10000.00 |
| 3 | 王五 | 5432.00 |
+----+--------+----------+
3 rows in set (0.00 sec)--终端B
mysql> begin; --开启事务
Query OK, 0 rows affected (0.00 sec)mysql> select * from account; --终端A commit前 查看
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 张三 | 4321.00 |
| 2 | 李四 | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)mysql> select * from account; --终端A commit后 查看
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 张三 | 4321.00 |
| 2 | 李四 | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)mysql> select * from account; --多次查看,发现终端A在对应事务中insert的数据,在终端B的事务周期中,也没有什么影响,也符合可重复的特点。但是,一般的数据库在可重复读情况的时候,无法屏蔽其他事务insert的数据(为什么?因为隔离性实现是对数据加锁完成的,而insert待插入的数据因为并不存在,那么一般加锁无法屏蔽这类问题),会造成虽然大部分内容是可重复读的,但是insert的数据在可重复读情况被读取出来,导致多次查找时,会多查找出来的记录,就如同产生了幻觉。这种现象,叫做幻读(phantom read)。很明显,MySQL在RR级别的时候,是解决了幻读问题的(解决的方式是用Next-Key锁(GAP+行锁)解决的。这块比较难,有兴趣同志了解一下)。
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 张三 | 4321.00 |
| 2 | 李四 | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)mysql> commit; --结束事务
Query OK, 0 rows affected (0.00 sec)mysql> select * from account; --看到更新
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 张三 | 4321.00 |
| 2 | 李四 | 10000.00 |
| 3 | 王五 | 5432.00 |
+----+--------+----------+
3 rows in set (0.00 sec)
多次查看,发现终端A在对应事务中insert的数据,在终端B的事务周期中,也没有什么影响,也符合可重复的特点。但是,一般的数据库在可重复读情况的时候,无法屏蔽其他事务insert的数据(为什么?因为隔离性实现是对数据加锁完成的,而insert待插入的数据因为并不存在,那么一般加锁无法屏蔽这类问题),会造成虽然大部分内容是可重复读的,但是insert的数据在可重复读情况被读取出来,导致多次查找时,会多查找出来的记录,就如同产生了幻觉。这种现象,叫做幻读(phantom read)。很明显,MySQL在RR级别的时候,是解决了幻读问题的(解决的方式是用Next-Key锁(GAP+行锁)解决的。这块比较难,有兴趣同志可以了解一下)。
串行化【Serializable】
是事务串行化 --- 导致到来的顺序串行化,保证我在执行的时候,没有人在跑,这就会带来“慢”的效率问题!
是安全,但是不高效!
--对所有操作全部加锁,进行串行化,不会有问题,但是只要串行化,效率很低,几乎完全不会被采用
--终端A
mysql> set global transaction isolation level serializable;
Query OK, 0 rows affected (0.00 sec)
mysql> select @@tx_isolation;
+----------------+
| @@tx_isolation |
+----------------+
| SERIALIZABLE |
+----------------+
1 row in set, 1 warning (0.00 sec)
mysql> begin; --开启事务,终端B同步开启
Query OK, 0 rows affected (0.00 sec)mysql> select * from account; --两个读取不会串行化,共享锁
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 张三 | 4321.00 |
| 2 | 李四 | 10000.00 |
| 3 | 王五 | 5432.00 |
+----+--------+----------+
3 rows in set (0.00 sec)mysql> update account set blance=1.00 where id=1; --终端A中有更新或者其他操作,会阻塞。直到终端B事务提交。
Query OK, 1 row affected (18.19 sec)
Rows matched: 1 Changed: 1 Warnings: 0--终端B
mysql> begin;
Query OK, 0 rows affected (0.00 sec)mysql> select * from account; --两个读取不会串行化
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 张三 | 4321.00 |
| 2 | 李四 | 10000.00 |
| 3 | 王五 | 5432.00 |
+----+--------+----------+
3 rows in set (0.00 sec)mysql> commit; --提交之后,终端A中的update才会提交。
Query OK, 0 rows affected (0.00 sec)
总结:
-
其中隔离级别越严格,安全性越高,但数据库的并发性能也就越低,往往需要在两者之间找一个平衡点。
-
不可重复读的重点是修改和删除:同样的条件,你读取过的数据,再次读取出来发现值不一样了
-
幻读的重点在于新增:同样的条件,第1次和第2次读出来的记录数不一样
-
说明:mysql默认的隔离级别是可重复读,一般情况下不要修改
-
上面的例子可以看出,事务也有长短事务这样的概念。事务间互相影响,指的是事务在并行执行的时候,即都没有commit的时候,影响会比较大。
隔离级别 | 脏读 | 不可重复读 | 幻读 | 加锁读 |
---|---|---|---|---|
读未提交 (read uncommitted) | ✔ | ✔ | ✔ | 不加锁 |
读已提交 (read committed) | ✘ | ✔ | ✔ | 不加锁 |
可重复读 (repeatable read) | ✘ | ✘ | ✘(MySQL中) | 不加锁 |
可串行化 (serializable) | ✘ | ✘ | ✘ | 加锁 |
✔:会发生该问题
✘:不会发生该问题
一致性(Consistency)
-
事务执行的结果,必须使数据库从一个一致性状态,变到另一个一致性状态。当数据库只包含事务成功提交的结果时,数据库处于一致性状态。如果系统运行发生中断,某个事务尚未完成而被迫中断,而改未完成的事务对数据库所做的修改已被写入数据库,此时数据库就处于一种不正确(不一致)的状态。因此一致性是通过原子性来保证的。
-
其实一致性和用户的业务逻辑相关,一般MySQL提供技术支持,但是一致性还是要用户业务逻辑做支撑,也就是,一致性,是由用户决定的。
-
而技术上,通过AID保证C:通过原子性(A)、隔离性(I)和持久性(D)来保证一致性(C)。也就是说,A、I 和 D 是实现 C 的手段
推荐阅读
https://www.jianshu.com/p/3984788e1083
Innodb中的事务隔离级别和锁的关系 - 美团技术团队
Mysql 间隙锁原理,以及Repeatable Read隔离级别下可以防止幻读原理(百度) - aspirant - 博客园mysql相关基础知识篇(五)_mysql串行化实现原理-CSDN博客
备注:
基本上,了解了上面的知识,在MySQL事务使用上,肯定没有问题。不过,这块设计很优秀,也是面试中可能被问到的,一般来说,如果能说出上面的内容,就已经不错了。但是如果我们能更详细,更深入的谈论这个问题,那么对我们的面试与学习肯定是有很大的帮助。
不过接下来的内容,会比较难一些,听的不明白,也没有太大问题。
如果有时间,我们演示一下,在RR级别的时候,多个事务的update,多个事务的insert,多个事务的delete,是否会有加锁现象。
现象结果是,update,insert,delete之间是会有加锁现象的,但是select和这些操作是不冲突的。这就是通过读写锁(锁有行锁或者表锁)+MVCC完成隔离性。
试学内容-如何理解隔离性2
数据库并发的场景有三种:
-
读-读:不存在任何问题,也不需要并发控制
-
读-写:有线程安全问题,可能会造成事务隔离性问题,可能遇到脏读、幻读、不可重复读
-
写-写:有线程安全问题,可能会存在更新丢失问题,比如第一类更新丢失,第二类更新丢失(后面补充)
读-写
多版本并发控制(MVCC)是一种用来解决读-写冲突的无锁并发控制
MySQL 会为每一个事务分配单向增长的事务ID,为每个修改保存一个版本,版本与事务ID关联,读操作只读该事务开始前的数据库的快照。
一般来说ID越小,来得越早!可以通过 ID 来判断事务的先后顺序;
MySQLD 可能会面临处理多个事物的情况,事务也有自己的生命周期;
这就意味着MySQLD 要对多个事务进行管理,要先描述,再组织!换句话说,MySQLD 中一定是对应的一个或一套结构体/类对象!事务也要有自己的结构体(原子性申请事务ID,初始化事务对象)!
MVCC可以为数据库解决以下问题:
-
在并发读写数据库时,可以做到在读操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了数据库并发读写的性能
-
同时还可以解决脏读、幻读、不可重复读等事务隔离问题,但不能解决更新丢失问题
但是理解MVCC我们需要知道三个前提知识:
-
3/4个记录隐藏字段
-
undo日志
-
Read View
3个记录隐藏列字段
-
DB_TRX_ID(列名称):6 byte,最近修改(修改/插入)事务ID,记录创建这条记录/最后一次修改该记录的事务ID(代表未来插入的任何数据,插入这一记录的事务是谁插入的?事务ID是什么?需要将这个事务ID放在表当中,无论是单SQL,系统默认封装的事务,还是我们手动begin开启的事务)
-
DB_ROLL_PTR(回滚指针):7 byte,回滚指针,指向这条记录的上一个版本(简单理解成,指向历史版本就行,这些数据一般在undo log中)(如果我们对 MySQL 当中的某一行记录作修改,MySQL 在特定的隔离级别下,并不会直接去改表中的数据,可能会将要改的记录,在改之前先把存一份,然后让我们改动最新的表中的数据,这样改完之后也可以知道历史的数据是谁 --- 这种策略就像之前的“ "写" 时拷贝 ”,所以就要保证改完之后要能找到历史的数据)(和回滚操作肯定是相关的)
-
DB_ROW_ID:6 byte,隐含的自增ID(隐藏主键),如果数据表没有主键,InnoDB会自动以DB_ROW_ID产生一个聚簇索引,默认构建 B+ 树,可以认为是缺省的,因为主键只能有一个!
-
补充:实际还有一个删除flag隐藏字段,既记录被更新或删除并不代表真的删除,而是删除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 | null | 1 | null |
我们目前并不知道创建该记录的事务ID,隐式主键,我们就默认设置成null,1。第一条记录也没有其他版本,我们设置回滚指针为null。
undo 日志(MySQL 当中比较重要的日志模块)
这里不想细讲,但是有一件事情得说清楚,MySQL 将来是以服务/守护进程的方式,在内存中运行。我们之前所讲的所有机制:索引、事务、隔离性、日志等,都是在内存中完成的,即在 MySQL 内部的相关缓冲区中,保存相关数据,完成各种判断操作。然后在合适的时候,将相关数据刷新到磁盘当中的,进行持久化。
所以,我们这里理解undo log,简单理解成,就是 MySQL 中的一段内存缓冲区,用来保存日志数据的就行。(buffer pool 中的一部分)
模拟 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提交,释放锁。
这样,我们就有了一个基于链表记录的历史版本链。所谓的回滚,无非就是用历史数据,覆盖当前数据。
实际上 MySQL 的回滚操作是有很多实现细节的,而且关于形成的版本链,我们是覆盖的方式理解,版本链主要是为了进行事务隔离,用来实现隔离性的,而回滚操作是 --- 比如说我们作insert操作,MySQL 就会记录一条相反的 SQL --- delete 未来在事务的内部作了delete,就会保存成 insert ,所以我们在进行定向回滚的时候,就会逆向的将 SQL 跑一边,数据就恢复出来了
上面的过程我们就成为 MVCC!
上面的一个一个版本,我们可以称之为一个一个的快照。
一些思考
一个事务已经提交了,是回滚不了的!因为 undo log 被清理掉了!
上面是以更新(update)主讲的,如果是delete呢?一样的,别忘了,删数据不是清空,而是设置flag为删除即可。也可以形成版本。
如果是insert呢?因为insert是插入,也就是之前没有数据,那么insert也就没有历史版本。但是一般为了回滚操作,insert的数据也是要被放入undo log中,为了隔离级别,如果当前事务commit了,那么这个undo log的历史insert记录就可以被清空了。
总结一下,也就是我们可以理解成,update和delete可以形成版本链,insert暂时不考虑。
那么select呢?
首先,select不会对数据做任何修改,所以,为select维护多版本,没有意义。不过,此时有个问题,就是:
select读取,是读取最新的版本呢?还是读取历史版本?
当前读:读取最新的记录,就是当前读。增删改,都叫做当前读,select也有可能当前读,比如:select lock in share mode(共享锁),select for update (这个好理解,我们后面不讨论)
快照读:读取历史版本(一般而言),就叫做快照读。(这个我们后面重点讨论)
这也就是我们读写可以并发!!!我们可以看到,在多个事务同时删改查的时候,都是当前读,是要加锁的。那同时有select过来,如果也要读取最新版(当前读),那么也就需要加锁,这就是串行化。
但如果是快照读,读取历史版本的话,是不受加锁限制的。也就是可以并行执行!换言之,提高了效率,即MVCC的意义所在。
那么,是什么决定了,select是当前读,还是快照读呢?隔离级别!
那为什么要有隔离级别呢?
因为事务都是原子的。所以,无论如何,事务总有先有后。
但是经过上面的操作我们发现,事务从begin->CURD->commit,是有一个阶段的。也就是事务有执行前,执行中,执行后的阶段。但,不管怎么启动多个事务,总是有先有后的。
那么多个事务在执行中,CURD操作是会交织在一起的。那么,为了保证事务的"有先有后",是不是应该让不同的事务看到它该看到的内容,这就是所谓的隔离性与隔离级别要解决的问题。
先来的事务,应不应该看到后来的事务所做的修改呢?
那么,如何保证,不同的事务,看到不同的内容呢?也就是如何如何实现隔离级别?
Read View
Read View就是事务进行快照读操作的时候生产的读视图(Read View),在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的ID(当每个事务开启时,都会被分配一个ID, 这个ID是递增的,所以最新的事务,ID值越大)
Read View 在 MySQL 源码中,就是一个类,本质是用来进行可见性判断的。即当我们某个事务执行快照读的时候,才会对该记录创建一个 Read View 读视图,把它比作条件,用来判断当前事务能够看到哪个版本的数据,既可能是当前最新的数据,也有可能是该行记录的 undo log 里面的某个版本的数据。
具体读那些,是由隔离级别确定的!
下面是ReadView结构,但为了减少大家的负担,我们简化一下
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
up_limit_id; //记录m_ids列表中事务ID最小的ID(没有写错)
low_limit_id; //ReadView生成时刻系统尚未分配的下一个事务ID,也就是目前已出现过的事务ID的最大值+1(也没有写错)
creator_trx_id //创建该ReadView的事务ID
我们在实际读取数据版本链的时候,是能读取到每一个版本对应的事务ID的,即:当前记录的DB_TRX_ID。
那么,我们现在手里面有的东西就有,当前快照读的ReadView和版本链中的某一个记录的DB_TRX_ID。
所以现在的问题就是,当前快照读,应不应该读到当前版本记录。一张图,解决所有问题!
所以当前进行快照读,应不应该读到版本链当中的某一个版本,未来进行可见性判断的时候,一定是拿着版本链当中存储的事务ID,和我的 Read View 中记录得事务ID,根据大小来决定先后,来判定应不应该看到!
注意:我们要时刻记住:
在大多数现代数据库系统中(如 MySQL InnoDB、PostgreSQL、Oracle),事务 ID(Transaction ID)通常是在 BEGIN
开始时就分配的,而不是在第一次执行写操作(INSERT/UPDATE/DELETE)时才分配。
事务ID不断在递增,所以,事务ID大小,就决定了先后顺序!但是:
物理存储的最新数据 = 最后成功提交 的版本(与事务ID大小无必然关系)。
begin期间对同一条记录的增删改操作是加锁的,不需要担心这种类似的问题!
事务能否看到"最新数据" 取决于:
-
事务的 隔离级别
-
事务开始的 快照时间点
-
其他事务的 提交状态
版本链(MVCC 多版本链)的核心作用正是为了实现不同隔离级别下的"快照读",确保事务能看到符合其隔离级别要求的数据版本。
read view 是事务可见的一个类,不是事务创建出来,就会有 read view,而是当这个事务(已经存在),首次进行快照读的时候,MySQL 才会 new 出来 read view!
对应源码策略:
/* Check whether the changes by id are visible.
@param[in] id transaction id to check against the view
@param[in] name table name
@return whether the view sees the modifications of id. */
bool changes_visible(trx_id_t id,const table_name_t& name) constMY_ATTRIBUTE((warn_unused_result))
{ut_ad(id > 0);if (id < m_up_limit_id || id == m_creator_trx_id) {return(true);}check_trx_id_sanity(id, name);if (id >= m_low_limit_id) {return(false);} else if (m_ids.empty()) {return(true);}const ids_t::value_type* p = m_ids.data();return(!std::binary_search(p, p + m_ids.size(), id));
}
如果查到不应该看到当前版本,接下来就是遍历下一个版本,直到符合条件,即可以看到。上面的readview是当你进行select的时候,会自动形成。
read view 整体流程
假设当前有条记录:
name | age | DB_TRX_ID(创建该记录的事务ID) | DB_ROW_ID(隐式主键) | DB_ROLL_PTR(回滚指针) |
---|---|---|---|---|
张三 | 28 | null | 1 | null |
事务操作:
事务1 [id=1] | 事务2 [id=2] | 事务3 [id=3] | 事务4 [id=4] |
---|---|---|---|
事务开始 | 事务开始 | 事务开始 | 事务开始 |
... | ... | ... | 修改且已提交 |
进行中 | 快照读 | 进行中 | |
... | ... | ... |
-
事务4:修改name(张三)变成name(李四)
-
当事务2对某行数据执行了快照读,数据库为该行数据生成一个Read View读视图
//事务2的 Read view
m_ids; // 1,3
up_limit_id; // 1
low_limit_id; // 4 + 1 = 5,原因:Readview生成时刻,系统尚未分配的下一个事务ID
creator_trx_id // 2
此时版本链是:
-
只有事务4修改过该行记录,并在事务2执行快照读前,就提交了事务。
-
我们的事务2在快照读该行记录的时候,就会拿该行记录的DB_TRX_ID去跟up_limit_id, low_limit_id和活跃事务ID列表(trx_list)进行比较,判断当前事务2能看到该记录的版本。
//事务2的 Read view
m_ids; // 1,3
up_limit_id; // 1
low_limit_id; // 4 + 1 = 5,原因:Readview生成时刻,系统尚未分配的下一个事务ID
creator_trx_id // 2//事务4提交的记录对应的事务ID
DB_TRX_ID=4//比较步骤
DB_TRX_ID (4) < up_limit_id (1) ? 不小于,下一步
DB_TRX_ID (4) >= low_limit_id(5) ? 不大于,下一步
m_ids.contains(DB_TRX_ID) ? 不包含,说明,事务4不在当前的活跃事务中。// 结论
故,事务4的更改,应该看到。
所以事务2能读到的最新数据记录是事务4所提交的版本,而事务4提交的版本也是全局角度上最新的版本
RR与RC的本质区别
当前读取和快照读在RR级别下的区别
下面的代码经过测试,是完全没有问题的。
select * from user lock in share mode, 以加共享锁方式进行读取,对应的就是当前读。此处只作为测试使用,不重讲。
测试表:
--设置RR模式下测试
mysql> set global transaction isolation level REPEATABLE READ;
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)--用表
create table if not exists user(id int primary key,age int not null,name varchar(50) not null default ''
)ENGINE=InnoDB DEFAULT CHARSET=UTF8;--插入一条记录,用来测试
mysql> insert into user values (1, 15, '黄蓉');
Query OK, 1 row affected (0.00 sec)
测试用例1-表1:
事务A操作 | 事务A描述 | 事务B描述 | 事务B操作 |
---|---|---|---|
begin | 开启事务 | 开启事务 | begin |
select * from user | 快照读(无影响)查询 | 快照读查询 | select * from user |
update user set age=18 where id=1; | 更新age=18 | - | - |
commit | 提交事务 | - | - |
select 快照读,没有读到age=18 | select * from user | ||
select lock in share mode当前读,读到age=18 | select * from user lock in share mode |
测试用例2-表2:
事务A操作 | 事务A描述 | 事务B描述 | 事务B操作 |
---|---|---|---|
begin | 开启事务 | 开启事务 | begin |
select * from user | 快照读,查到age=18 | - | - |
update user set age=28 where id=1; | 更新age=28 | - | - |
commit | 提交事务 | - | - |
select 快照读 age=28 | select * from user | ||
select lock in share mode当前读 age=28 | select * from user lock in share mode |
-
用例1与用例2:唯一区别仅仅是表1的事务B在事务A修改age前快照读过一次age数据
-
而表2的事务B在事务A修改age前没有进行过快照读。
结论:
-
事务中快照读的结果是非常依赖该事务首次出现快照读的地方,即某个事务中首次出现快照读,决定该事务后续快照读结果的能力
-
delete同样如此
RR与RC的本质区别
-
正是Read View生成时机的不同,从而造成RC,RR级别下快照读的结果的不同
-
在RR级别下的某个事务的某条记录的第一次快照读会创建一个快照及Read View,将当前系统活跃的其他事务记录起来
-
此后在调用快照读的时候,还是使用的是同一个Read View,所以只要当前事务在其他事务提交更新之前使用过快照读,那么之后的快照读使用的都是同一个Read View,所以对之后的修改不可见;
-
即RR级别下,快照读生成Read View时,Read View会记录此时所有其他活动事务的快照,这些事务的修改对于当前事务都是不可见的。而早于Read View创建的事务所做的修改均是可见
-
而在RC级别下的,事务中,每次快照读都会新生成一个快照和Read View,这就是我们在RC级别下的事务中可以看到别的事务提交的更新的原因
-
总之在RC隔离级别下,是每个快照读都会生成并获取最新的Read View;而在RR隔离级别下,则是同一个事务中的第一个快照读才会创建Read View,之后的快照读获取的都是同一个Read View。
-
正是RC每次快照读,都会形成Read View,所以,RC才会有不可重复读问题。
读-读
-
不讨论
写-写
-
现阶段,直接理解成都是当前读,当前不做深究
推荐阅读
关于这块,有很好的文章,推荐大家阅读
https://blog.csdn.net/Snailwann/article/details/94724197
详细分析MySQL事务日志(redo log和undo log) - 骏马金龙 - 博客园
【MySQL】InnoDB 如何避免脏读和不可重复读_innodb怎么解决脏读-CSDN博客
MySQL 中是如何实现事务隔离的
首先说读未提交,它是性能最好,也可以说它是最野蛮的方式,因为它压根儿就不加锁,所以根本谈不上什么隔离效果,可以理解为没有隔离。
再来说串行化。读的时候加共享锁,也就是其他事务可以并发读,但是不能写。写的时候加排它锁,其他事务不能并发写也不能并发读。
最后说读提交和可重复读。这两种隔离级别是比较复杂的,既要允许一定的并发,又想要兼顾的解决问题。
实现可重复读
为了解决不可重复读,或者为了实现可重复读,MySQL 采用了 MVVC (多版本并发控制) 的方式。
我们在数据库表中看到的一行记录可能实际上有多个版本,每个版本的记录除了有数据本身外,还要有一个表示版本的字段,记为 row trx_id,而这个字段就是使其产生的事务的 id,事务 ID 记为 transaction id,它在事务开始的时候向事务系统申请,按时间先后顺序递增。
按照上面这张图理解,一行记录现在有 3 个版本,每一个版本都记录这使其产生的事务 ID,比如事务A的transaction id 是100,那么版本1的row trx_id 就是 100,同理版本2和版本3。
在上面介绍读提交和可重复读的时候都提到了一个词,叫做快照,学名叫做一致性视图,这也是可重复读和不可重复读的关键,可重复读是在事务开始的时候生成一个当前事务全局性的快照,而读提交则是每次执行语句的时候都重新生成一次快照。
对于一个快照来说,它能够读到那些版本数据,要遵循以下规则:
- 当前事务内的更新,可以读到;
- 版本未提交,不能读到;
- 版本已提交,但是却在快照创建后提交的,不能读到;
- 版本已提交,且是在快照创建前提交的,可以读到;
利用上面的规则,再返回去套用到读提交和可重复读的那两张图上就很清晰了。还是要强调,两者主要的区别就是在快照的创建上,可重复读仅在事务开始是创建一次,而读提交每次执行语句的时候都要重新创建一次。
并发写问题
存在这的情况,两个事务,对同一条数据做修改。最后结果应该是哪个事务的结果呢,肯定要是时间靠后的那个对不对。并且更新之前要先读数据,这里所说的读和上面说到的读不一样,更新之前的读叫做“当前读”,总是当前版本的数据,也就是多版本中最新一次提交的那版。
假设事务A执行 update 操作, update 的时候要对所修改的行加行锁,这个行锁会在提交之后才释放。而在事务A提交之前,事务B也想 update 这行数据,于是申请行锁,但是由于已经被事务A占有,事务B是申请不到的,此时,事务B就会一直处于等待状态,直到事务A提交,事务B才能继续执行,如果事务A的时间太长,那么事务B很有可能出现超时异常。如下图所示。
加锁的过程要分有索引和无索引两种情况,比如下面这条语句
update user set age=11 where id = 1
id 是这张表的主键,是有索引的情况,那么 MySQL 直接就在索引数中找到了这行数据,然后干净利落的加上行锁就可以了。
而下面这条语句
update user set age=11 where age=10
表中并没有为 age 字段设置索引,所以, MySQL 无法直接定位到这行数据。那怎么办呢,当然也不是加表锁了。MySQL 会为这张表中所有行加行锁,没错,是所有行。但是呢,在加上行锁后,MySQL 会进行一遍过滤,发现不满足的行就释放锁,最终只留下符合条件的行。虽然最终只为符合条件的行加了锁,但是这一锁一释放的过程对性能也是影响极大的。所以,如果是大表的话,建议合理设计索引,如果真的出现这种情况,那很难保证并发度。
解决幻读
上面介绍可重复读的时候,那张图里标示着出现幻读的地方实际上在 MySQL 中并不会出现,MySQL 已经在可重复读隔离级别下解决了幻读的问题。
前面刚说了并发写问题的解决方式就是行锁,而解决幻读用的也是锁,叫做间隙锁,MySQL 把行锁和间隙锁合并在一起,解决了并发写和幻读的问题,这个锁叫做 Next-Key锁。
假设现在表中有两条记录,并且 age 字段已经添加了索引,两条记录 age 的值分别为 10 和 30。
此时,在数据库中会为索引维护一套B+树,用来快速定位行记录。B+索引树是有序的,所以会把这张表的索引分割成几个区间。
如图所示,分成了3 个区间,(负无穷,10]、(10,30]、(30,正无穷],在这3个区间是可以加间隙锁的。
之后,我用下面的两个事务演示一下加锁过程。
在事务A提交之前,事务B的插入操作只能等待,这就是间隙锁起得作用。当事务A执行update user set name='风筝2号’ where age = 10;
的时候,由于条件 where age = 10 ,数据库不仅在 age =10 的行上添加了行锁,而且在这条记录的两边,也就是(负无穷,10]、(10,30]这两个区间加了间隙锁,从而导致事务B插入操作无法完成,只能等待事务A提交。不仅插入 age = 10 的记录需要等待事务A提交,age<10、10<age<30 的记录页无法完成,而大于等于30的记录则不受影响,这足以解决幻读问题了。
这是有索引的情况,如果 age 不是索引列,那么数据库会为整个表加上间隙锁。所以,如果是没有索引的话,不管 age 是否大于等于30,都要等待事务A提交才可以成功插入。
总结
MySQL 的 InnoDB 引擎才支持事务,其中可重复读是默认的隔离级别。
读未提交和串行化基本上是不需要考虑的隔离级别,前者不加锁限制,后者相当于单线程执行,效率太差。
读提交解决了脏读问题,行锁解决了并发更新的问题。并且 MySQL 在可重复读级别解决了幻读问题,是通过行锁和间隙锁的组合 Next-Key 锁实现的。